Skip to content

Object-less ("global") actions have no UI home in protocol 17 — the onboarding flow is API-only #27

Closed

Description

@os-warren

Found while implementing catalog instantiation (#5). Reported rather than redesigned around, because which way this goes is a product call.

What was measured

duly_catalog_apply and duly_catalog_sync are global actions — neither operates on a record, so neither declares an objectName. In @objectstack/spec@17.2.0 that means they have no place to surface in the UI:

  • global_nav was removed from ACTION_LOCATIONS in spec 17 (#6888, ADR-0049 enforce-or-remove). The console's ⌘K palette composes its groups from nav items, objects, dashboards, pages, reports and record search — it reads no action metadata, so the location never rendered anything.
  • Every surviving location is object-bound: list_toolbar, list_item, record_header, record_more, record_related, record_section.

The spec's own prescription for this case is locations: [] (headless), which is what both actions declare. They keep their param contract, capability gate and audit trail, and they are invoked over the platform action route:

POST /api/v1/actions/global/duly_catalog_apply
{ "position_code": "plant_compliance_officer", "users": ["u1","u2","u3"] }

...or over MCP. There is no button.

Why it is worth a decision

#5 opens by calling this "the onboarding path, and the single biggest adoption risk in the product" — the thing that has to be easy or the rollout dies in week one. An admin who has just imported their catalog currently cannot click anything to apply it; they need an API client, or the MCP surface (#21).

Options

  • A. Leave it headless. Honest to the platform, and day-one bulk onboarding over API/MCP is arguably the right shape for "26 duties × 40 people". Pairs naturally with 清单导入走平台标准 Import:提供样例 CSV,浏览器走通目录项与职责的导入并留档 #19 (spreadsheet import) and Document the MCP surface — ask the system what is late #21 (MCP surface).
  • B. Also attach an object-bound twin to duly_catalog_item with objectName: 'duly_catalog_item' and locations: ['list_toolbar'] — a real button on the Role catalog list view, which is exactly where an admin is standing when they want it. Costs a second declaration whose handler can be the same function.
  • C. A page (src/pages/) that collects the inputs and calls the action. Most control over the onboarding experience, most work.
  • D. ai: { exposed: true, requiresConfirmation: true, description: … } so the MCP/agent surface can drive it conversationally. Deliberately not done in Catalog instantiation — apply a position's duty catalog to a person #5: nothing needs it for reachability (the REST route works without it), and arming an agent to bulk-create duties for arbitrary users is a decision to take deliberately rather than as a side effect.

Filing unassigned for triage. B is the cheapest thing that gives the onboarding flow a button, and it does not invalidate A.

Activity

  1. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Adjudicated. auto-adjudicated — PM seat, round 1. Veto window open.

    Good find, and correctly reported rather than designed around. This is the product's single biggest adoption path with no button on it, which makes it worth more than the card that surfaced it.

    Decision: the object-bound twin on duly_catalog_item's list toolbar. Queued.

    四维分析

    实际业务需求 — 客户带着现成的岗位清单来,落地第一步就是"把这份清单套到这些人身上"。这一步只能走 API 或 MCP,等于试点第一天要有人写 curl——这不是不方便,是试点做不下去。而且 toolbar 的位置在体验上反而更对:人正看着「岗位清单」列表、按 position_code 筛好了,直接点「应用到人员」,比一个全局菜单项更符合动作发生的地方。

    长远合理性 — 这不是新增能力,是给已经建好的能力接上唯一缺的那段路。协议 17 退掉 global_nav、把 location 全部收成对象绑定,说明平台的方向就是动作住在它操作的对象上;顺着走比另开一条路对。

    防 AI 写错 — 中性偏正:对象绑定的动作,它的 objectName 会被作者期校验,全局动作没有这层。

    创业阶段不扩散 — 不新增能力面,只挪位置,处理器可完全复用。

    四维同向,且不在人工地板上——不是功能新增(能力已存在且已合入),不是 ADR,不是协议变化,故代裁。

    本分析看不见什么:我不知道 list toolbar 上的动作能不能弹出「选人」这种多值输入。如果不能,就退成两步(先在列表选中条目 → 弹窗只选人),这依然可用;真做不成再回来讨论。

    Scope

    1. Object-bound twin on duly_catalog_item, location list_toolbar, reusing the existing handler unchanged — no second implementation.
    2. Keep the global action registered. It is what MCP and the REST path use, and Document the MCP surface — ask the system what is late #21 will document it.
    3. Do not add ai.exposed. Arming an agent to bulk-create duties for arbitrary people is a deliberate decision and belongs to Security model — positions, permission sets, sharing rules #8's capability work, not to a UI-placement card.

    Depends on nothing; sized S/M.


    Generated by Claude Code

  2. self-assigned this
    on Sep 1, 2026
  3. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Claiming per PM dispatch (assignee left as-is — PM already claimed).

    • session: session_01SqkTcrxUFci7nqXdbBSe2p
    • branch: claude/issue-27-catalog-action-ui
    • worktree: /home/user/duly-issue-27

    Scope taken from the adjudication comment: object-bound twin on duly_catalog_item at list_toolbar, reusing the existing handler unchanged; global action stays registered; no ai.exposed. First measurement is whether a list_toolbar action's input can express position_code + a multi-user picker.

    Generated by Claude Code


    Generated by Claude Code

  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor
    {
      "issue": 27,
      "status": "done",
      "branch": "claude/issue-27-catalog-action-ui",
      "pr": "https://github.com/objectstack-ai/duly/pull/59",
      "premise_still_valid": true,
      "summary": "Added the adjudicated object-bound twin `duly_catalog_apply_to_people` on `duly_catalog_item` at `list_toolbar`, spread from `CatalogApplyAction` so `params`, `requiredPermissions: ['duly.catalog.apply']`, `description`, `icon`, `variant` and `type` have exactly one copy and cannot drift; it registers under `duly_catalog_item:duly_catalog_apply_to_people` to the very same `applyCatalogHandler` FUNCTION REFERENCE (asserted by identity, not equivalence). The global action stays object-less, headless and registered; no `ai.exposed`; no handler body touched. I measured the input shape first and took the ONE-STEP form, not the fallback: a `list_toolbar` action can carry `position_code` + a multi-user picker at every layer (spec parse, objectui param-dialog collection, the multi-select user widget, and the dispatcher's own `validateActionParams`) — and the two-step fallback would have required the handler to read `_selectedIds` instead of `position_code`, i.e. the second implementation the card forbade. The twin has a DISTINCT name because `defineStack` was measured to accept two actions sharing one name silently. `src/apps/duly.app.ts` was deliberately not touched: `nav_catalog` already lands on the Role catalog list, so the toolbar button completes the path, and nav is not in the adjudicated three-item scope.",
      "tests": "All four gates green at final commit 06416fb (re-run after it): `pnpm validate` exit 0 — '✓ Validation passed (370ms)', 'UI: 1 Apps  5 Views  6 Actions'; `pnpm typecheck` exit 0; `pnpm test` exit 0 — 'Test Files 13 passed (13) / Tests 382 passed (382)'; `pnpm build` exit 0 — '✓ Build complete (653ms)', 6 Actions, 'Bundling 2 handlers' (unchanged from a pristine origin/main tree — that count is dulyFunctions, not action handlers). Exit codes captured with `cmd > log 2>&1; EXIT=$?` before any pipe, and the verdicts quoted above are the gates' own printed lines. `pnpm test` re-run with dist/ present: still 382 passed. NEW FILE test/catalog-action-placement.test.ts (14 assertions): barrel membership, objectName+location, handler-reference identity vs the global, registration under the key dispatch reaches and NOT under global, target==name, param-contract equality with a non-vacuous length check, capability-gate equality, ai.exposed falsy, and two `validateActionParams` legs (the multi-user array accepted; a scalar in `users` refused with code `invalid_shape` — the negative leg, without which the positive one would pass against an open value shape). ABLATIONS — both run from a COMMITTED tree, each with `trap '<restore>' EXIT INT TERM`, each mutation confirmed on disk by grepping the anchored text BEFORE reading any result. NO REBUILD STEP APPLIES AND THAT IS VERIFIED, NOT ASSUMED: tests import `../src/...` by relative path (no package `exports` map, no vitest alias, no vitest config at all), `pnpm validate` printed 'Config: /home/user/duly-issue-27/objectstack.config.ts' (source, not dist), and the kernel-booting suites already pin `artifactPath` at a nonexistent file — so no dist/ sits between mutation and reading. (1) Deleting the twin's `registerAction` line: marker count 1→0 on disk, `git diff --stat` = 1 deletion; `pnpm validate` stayed exit 0 with '✓ Validation passed' and still counted 6 Actions, while `pnpm test` went exit 1 with 3 failed | 379 passed — 'is wired under the key its dispatch reaches, and nowhere else', 'is wired to the SAME handler function reference as the global action', and the pre-existing repo-wide 'every declared script action has a handler under a key that can reach it'. That is the card's ungated trap reproduced exactly. Restore leg confirmed on disk: marker back to 1, tree clean. (2) Deleting the barrel entry: array-entry count 1→0 with import/re-export asserted intact; `pnpm validate` exit 0 and quietly reported 5 Actions instead of 6; `pnpm test` exit 1, 8 failed | 374 passed. Restore leg confirmed: identifier occurrences back to 3, tree clean. HONESTY NOTE: my FIRST attempt at ablation 2 was voided by its own on-disk check — the marker I chose also matched the import line, so the count went 2→1 rather than →0. The mutation had in fact landed; the CHECK was wrong. I re-anchored and re-ran rather than reading the first result, and said so in the PR body.",
      "open_questions": [
        {
          "question": "Should the Setup nav get a one-click 'Apply role catalog' entry using ObjectNavItemSchema.runAction, now that the twin makes it possible? Not blocking — I shipped without it and the path is complete via nav_catalog → Role catalog → toolbar button.",
          "options": [
            "A. Leave nav as-is (what I shipped). The adjudicated scope has three items and none is nav; nav_catalog already lands on the list, so the button completes the onboarding path. No new declared surface.",
            "B. Add `runAction: 'duly_catalog_apply_to_people'` to a new Setup nav entry. Measured as real: ObjectNavItemSchema.runAction is the declared 'navigate = run action' deep-link (#4848), validated by defineStack's cross-reference walk, and objectui arms it ONLY for an action rendering at list_toolbar on that object (ObjectView.tsx, actionRendersAt) — so this twin is precisely its precondition. Costs one nav entry; saves the admin one click and one act of finding the button."
          ],
          "recommendation": "A for now. Real business need is already met by the button — the card's failure mode was 'no button at all', not 'one click too many' — and startup scope discipline says do not add declared surface without pull. B becomes cheap and obvious the moment a pilot says the button is hard to find; it is a one-line follow-up, not a rework."
        }
      ],
      "out_of_scope_findings": [
        "filed as objectstack-ai/objectstack#14123: startup `[action-governance]` reports EVERY object-less declared action as 'registered handler with NO declaration — REFUSED at dispatch (ADR-0110 D3)', and the REST route contradicts it. PRE-EXISTING on duly main — reproduced on a pristine origin/main worktree, where `global:duly_catalog_apply` and `global:duly_catalog_sync` are named in that warning on every boot. Root cause measured: the audit sources declarations from meta.listObjects() + meta.loadMany('action'), and meta.loadMany('action') returns [] (meta.load('action', name) is undefined for both actions too), so only object-BOUND actions survive it — my twin is conspicuously absent from the warning, which is corroborating evidence the object binding worked. The actual route resolves the same actions through ql.registry.getItem('action', name) (probed: FOUND, objectName undefined) and dispatches fine. PM-relevant: the warning's second remedy, 'drop the registration if nothing should invoke it over HTTP', would have silently broken this app's onboarding path; and this is worth carrying into #21, because the REST route named in #27's premise DOES work.",
        "filed as objectstack-ai/objectstack#14124: `defineStack` accepts two actions sharing one `name` without a word — including two GLOBAL ones (both survive into stack.actions), where the `<object>:<name>` handler map then collapses to one key and whichever handler registers second silently shadows the other. The nav-runAction cross-reference walk correctly rejects a name resolving to NO action but says nothing about one resolving to TWO. This is why the twin got a distinct name rather than being a second declaration of `duly_catalog_apply`; the safe shape was a measurement result, not something the platform told me."
      ]
    }

    Generated by Claude Code

  5. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    (Supersedes the comment immediately above. That one led with the HTML-comment marker and the sanitizer ate it on the way into the store — the same class of loss this repo already documents, reproduced here: the literal <restore> and <object>:<name> fragments inside it were eaten too. This copy leads with the marker as literal text and spells those out. Content is otherwise identical.)

    {
      "issue": 27,
      "status": "done",
      "branch": "claude/issue-27-catalog-action-ui",
      "pr": "https://github.com/objectstack-ai/duly/pull/59",
      "premise_still_valid": true,
      "summary": "Added the adjudicated object-bound twin `duly_catalog_apply_to_people` on `duly_catalog_item` at `list_toolbar`, spread from `CatalogApplyAction` so `params`, `requiredPermissions: ['duly.catalog.apply']`, `description`, `icon`, `variant` and `type` have exactly one copy and cannot drift; it registers under the engine key `duly_catalog_item:duly_catalog_apply_to_people` to the very same `applyCatalogHandler` FUNCTION REFERENCE (asserted by identity, not equivalence). The global action stays object-less, headless and registered; no `ai.exposed`; no handler body touched. I measured the input shape first and took the ONE-STEP form, not the fallback: a `list_toolbar` action can carry `position_code` plus a multi-user picker at every layer (spec parse, objectui param-dialog collection, the multi-select user widget, and the dispatcher's own `validateActionParams`) — and the two-step fallback would have required the handler to read `_selectedIds` instead of `position_code`, i.e. the second implementation the card forbade. The twin has a DISTINCT name because `defineStack` was measured to accept two actions sharing one name silently. `src/apps/duly.app.ts` was deliberately not touched: `nav_catalog` already lands on the Role catalog list, so the toolbar button completes the path, and nav is not in the adjudicated three-item scope.",
      "tests": "All four gates green at final commit 06416fb (re-run after it): `pnpm validate` exit 0 — '✓ Validation passed (370ms)', 'UI: 1 Apps  5 Views  6 Actions'; `pnpm typecheck` exit 0; `pnpm test` exit 0 — 'Test Files 13 passed (13) / Tests 382 passed (382)'; `pnpm build` exit 0 — '✓ Build complete (653ms)', 6 Actions, 'Bundling 2 handlers' (unchanged from a pristine origin/main tree — that count is dulyFunctions, not action handlers). Exit codes were captured by redirecting to a file and reading $? BEFORE any pipe, and every verdict quoted here is the gate's own printed line, not a bare $?. `pnpm test` re-run with dist/ present: still 382 passed. NEW FILE test/catalog-action-placement.test.ts (14 assertions): barrel membership, objectName plus location, handler-reference identity vs the global, registration under the key dispatch reaches and NOT under global, target==name, param-contract equality with a non-vacuous length check, capability-gate equality, ai.exposed falsy, and two `validateActionParams` legs (the multi-user array accepted; a scalar in `users` refused with code `invalid_shape` — the negative leg, without which the positive one would pass just as happily against an open value shape). ABLATIONS — both run from a COMMITTED tree, each script carrying its own trap on EXIT INT TERM to restore, and each mutation confirmed on disk by grepping the anchored text BEFORE any result was read. NO REBUILD STEP APPLIES, AND THAT IS VERIFIED RATHER THAN ASSUMED: the tests import ../src/... by relative path (no package exports map, no vitest alias, no vitest config file at all), `pnpm validate` printed 'Config: /home/user/duly-issue-27/objectstack.config.ts' (source, not dist), and the kernel-booting suites already pin artifactPath at a nonexistent file for exactly this reason — so no dist/ sits between mutation and reading. (1) Deleting the twin's registerAction line: marker count 1 to 0 on disk, git diff --stat = 1 deletion; `pnpm validate` STAYED exit 0 with '✓ Validation passed' and still counted 6 Actions, while `pnpm test` went exit 1 with 3 failed | 379 passed — 'is wired under the key its dispatch reaches, and nowhere else', 'is wired to the SAME handler function reference as the global action', and the pre-existing repo-wide 'every declared script action has a handler under a key that can reach it'. That is the card's ungated trap reproduced exactly: green validate, clickable button, 404 at call time. Restore leg confirmed on disk — marker back to 1, tree clean. (2) Deleting the barrel entry: array-entry count 1 to 0 with the import and re-export asserted intact; `pnpm validate` exit 0 and quietly reported 5 Actions instead of 6; `pnpm test` exit 1, 8 failed | 374 passed. Restore leg confirmed — identifier occurrences back to 3, tree clean. HONESTY NOTE: my FIRST attempt at ablation 2 was voided by its own on-disk check — the marker I chose also matched the import line, so the count went 2 to 1 rather than to 0. The mutation had in fact landed; the CHECK was wrong. I re-anchored and re-ran rather than reading that first result, and said so in the PR body too.",
      "open_questions": [
        {
          "question": "Should the Setup nav get a one-click 'Apply role catalog' entry using ObjectNavItemSchema.runAction, now that the twin makes it possible? Not blocking — I shipped without it and the path is already complete via nav_catalog to the Role catalog list to the toolbar button.",
          "options": [
            "A. Leave nav as-is (what I shipped). The adjudicated scope has three items and none is nav; nav_catalog already lands on the list, so the button completes the onboarding path. No new declared surface.",
            "B. Add `runAction: 'duly_catalog_apply_to_people'` to a new Setup nav entry. Measured as real, not speculative: ObjectNavItemSchema.runAction is the declared 'navigate = run action' deep-link (#4848), validated by defineStack's cross-reference walk, and objectui arms it ONLY for an action that renders at list_toolbar on that object (ObjectView.tsx, actionRendersAt) — so this twin is precisely its precondition. Costs one nav entry; saves the admin one click and one act of finding the button."
          ],
          "recommendation": "A for now. Real business need is already met by the button — the card's failure mode was 'no button at all', not 'one click too many' — and startup scope discipline says do not add declared surface without pull. B becomes cheap and obvious the moment a pilot says the button is hard to find; it is a one-line follow-up, not a rework."
        }
      ],
      "out_of_scope_findings": [
        "filed as objectstack-ai/objectstack#14123: startup [action-governance] reports EVERY object-less declared action as 'registered handler with NO declaration — REFUSED at dispatch (ADR-0110 D3)', and the REST route contradicts it. PRE-EXISTING on duly main — reproduced on a pristine origin/main worktree, where global:duly_catalog_apply and global:duly_catalog_sync are named in that warning on every boot. Root cause measured: the audit sources declarations from meta.listObjects() plus meta.loadMany('action'), and meta.loadMany('action') returns [] (meta.load('action', name) is undefined for both actions too), so only object-BOUND actions survive it — my twin is conspicuously ABSENT from the warning, which is corroborating evidence the object binding worked. The actual route resolves the same actions through ql.registry.getItem('action', name) (probed: FOUND, objectName undefined) and dispatches fine. PM-relevant: the warning's second remedy, 'drop the registration if nothing should invoke it over HTTP', would have silently broken this app's onboarding path; and this is worth carrying into #21, because the REST route named in #27's premise DOES work.",
        "filed as objectstack-ai/objectstack#14124: `defineStack` accepts two actions sharing one `name` without a word — including two GLOBAL ones (both survive into stack.actions), where the object-colon-name handler map then collapses to one key and whichever handler registers second silently shadows the other. The nav-runAction cross-reference walk correctly rejects a name resolving to NO action but says nothing about one resolving to TWO. This is why the twin got a distinct name rather than being a second declaration of `duly_catalog_apply`; the safe shape was a measurement result, not something the platform told me."
      ]
    }

    Generated by Claude Code

  6. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    Authoritative copy. The two comments above are truncated in storage: each was cut at its first angle-bracket fragment (the GitHub body sanitizer discards the remainder), which in the first report landed inside the last finding and in the second landed in its opening note. This copy contains no angle brackets anywhere.

    {
      "issue": 27,
      "status": "done",
      "branch": "claude/issue-27-catalog-action-ui",
      "pr": "https://github.com/objectstack-ai/duly/pull/59",
      "premise_still_valid": true,
      "summary": "Added the adjudicated object-bound twin `duly_catalog_apply_to_people` on `duly_catalog_item` at `list_toolbar`, spread from `CatalogApplyAction` so `params`, `requiredPermissions: ['duly.catalog.apply']`, `description`, `icon`, `variant` and `type` have exactly one copy and cannot drift; it registers under the engine key `duly_catalog_item:duly_catalog_apply_to_people` to the very same `applyCatalogHandler` FUNCTION REFERENCE (asserted by identity, not equivalence). The global action stays object-less, headless and registered; no `ai.exposed`; no handler body touched. I measured the input shape first and took the ONE-STEP form, not the fallback: a `list_toolbar` action can carry `position_code` plus a multi-user picker at every layer (spec parse, objectui param-dialog collection, the multi-select user widget, and the dispatcher's own `validateActionParams`) — and the two-step fallback would have required the handler to read `_selectedIds` instead of `position_code`, i.e. the second implementation the card forbade. The twin has a DISTINCT name because `defineStack` was measured to accept two actions sharing one name silently. `src/apps/duly.app.ts` was deliberately not touched: `nav_catalog` already lands on the Role catalog list, so the toolbar button completes the path, and nav is not in the adjudicated three-item scope.",
      "tests": "All four gates green at final commit 06416fb, re-run after it. `pnpm validate` exit 0 — '✓ Validation passed (370ms)', 'UI: 1 Apps  5 Views  6 Actions'. `pnpm typecheck` exit 0. `pnpm test` exit 0 — 'Test Files 13 passed (13) / Tests 382 passed (382)'. `pnpm build` exit 0 — '✓ Build complete (653ms)', 6 Actions, 'Bundling 2 handlers' (unchanged from a pristine origin/main tree; that count is dulyFunctions, not action handlers). Exit codes were captured by redirecting to a file and reading the status BEFORE any pipe, and every verdict quoted here is the gate's own printed line rather than a bare shell status. `pnpm test` was re-run with dist/ present and stayed at 382 passed. NEW FILE test/catalog-action-placement.test.ts, 14 assertions: barrel membership; objectName plus location; handler-reference identity against the global; registration under the key dispatch reaches and NOT under global; target equals name; param-contract equality with a non-vacuous length check; capability-gate equality; ai.exposed falsy; and two `validateActionParams` legs — the multi-user array accepted, and a scalar in `users` refused with code `invalid_shape`, the negative leg without which the positive one would pass just as happily against an open value shape. ABLATIONS: both run from a COMMITTED tree, each script carrying its own trap on EXIT INT TERM to restore, and each mutation confirmed on disk by grepping the anchored text BEFORE any result was read. NO REBUILD STEP APPLIES, AND THAT IS VERIFIED RATHER THAN ASSUMED — the tests import ../src/... by relative path (no package exports map, no vitest alias, no vitest config file at all), `pnpm validate` printed 'Config: /home/user/duly-issue-27/objectstack.config.ts' (source, not dist), and the kernel-booting suites already pin artifactPath at a nonexistent file for exactly this reason, so no dist/ sits between mutation and reading. (1) Deleting the twin's registerAction line: marker count 1 to 0 on disk, git diff --stat showing 1 deletion; `pnpm validate` STAYED exit 0 with '✓ Validation passed' and still counted 6 Actions, while `pnpm test` went exit 1 with 3 failed and 379 passed — 'is wired under the key its dispatch reaches, and nowhere else', 'is wired to the SAME handler function reference as the global action', and the pre-existing repo-wide 'every declared script action has a handler under a key that can reach it'. That is the card's ungated trap reproduced exactly: green validate, clickable button, 404 at call time. Restore leg confirmed on disk — marker back to 1, tree clean. (2) Deleting the barrel entry: array-entry count 1 to 0 with the import and re-export asserted intact; `pnpm validate` exit 0 and quietly reported 5 Actions instead of 6; `pnpm test` exit 1 with 8 failed and 374 passed. Restore leg confirmed — identifier occurrences back to 3, tree clean. HONESTY NOTE: my FIRST attempt at ablation 2 was voided by its own on-disk check, because the marker I chose also matched the import line, so the count went 2 to 1 rather than to 0. The mutation had in fact landed; the CHECK was wrong. I re-anchored and re-ran rather than reading that first result, and said so in the PR body too.",
      "open_questions": [
        {
          "question": "Should the Setup nav get a one-click 'Apply role catalog' entry using ObjectNavItemSchema.runAction, now that the twin makes it possible? Not blocking — I shipped without it and the path is already complete via nav_catalog to the Role catalog list to the toolbar button.",
          "options": [
            "A. Leave nav as-is, which is what I shipped. The adjudicated scope has three items and none is nav; nav_catalog already lands on the list, so the button completes the onboarding path. No new declared surface.",
            "B. Add `runAction: 'duly_catalog_apply_to_people'` to a new Setup nav entry. Measured as real, not speculative: ObjectNavItemSchema.runAction is the declared navigate-equals-run-action deep-link (#4848), validated by defineStack's cross-reference walk, and objectui arms it ONLY for an action that renders at list_toolbar on that object (ObjectView.tsx, actionRendersAt) — so this twin is precisely its precondition. Costs one nav entry; saves the admin one click and one act of finding the button."
          ],
          "recommendation": "A for now. Real business need is already met by the button — the card's failure mode was 'no button at all', not 'one click too many' — and startup scope discipline says do not add declared surface without pull. B becomes cheap and obvious the moment a pilot says the button is hard to find; it is a one-line follow-up, not a rework."
        }
      ],
      "out_of_scope_findings": [
        "filed as objectstack-ai/objectstack#14123: startup [action-governance] reports EVERY object-less declared action as 'registered handler with NO declaration — REFUSED at dispatch (ADR-0110 D3)', and the REST route contradicts it. PRE-EXISTING on duly main, reproduced on a pristine origin/main worktree, where global:duly_catalog_apply and global:duly_catalog_sync are named in that warning on every boot. Root cause measured: the audit sources declarations from meta.listObjects() plus meta.loadMany('action'), and meta.loadMany('action') returns an empty array (meta.load('action', name) is undefined for both actions too), so only object-BOUND actions survive it — my twin is conspicuously ABSENT from the warning, which is corroborating evidence the object binding worked. The actual route resolves the same actions through ql.registry.getItem('action', name), probed as FOUND with objectName undefined, and dispatches fine. PM-relevant: the warning's second remedy, 'drop the registration if nothing should invoke it over HTTP', would have silently broken this app's onboarding path; and this is worth carrying into #21, because the REST route named in #27's premise DOES work.",
        "filed as objectstack-ai/objectstack#14124: `defineStack` accepts two actions sharing one `name` without a word, including two GLOBAL ones (both survive into stack.actions), where the object-colon-name handler map then collapses to one key and whichever handler registers second silently shadows the other. The nav runAction cross-reference walk correctly rejects a name resolving to NO action but says nothing about one resolving to TWO. This is why the twin got a distinct name rather than being a second declaration of `duly_catalog_apply`; the safe shape was a measurement result, not something the platform told me.",
        "NOT filed, reported here for PM: the GitHub body sanitizer does not merely strip a short angle-bracket fragment, it TRUNCATES THE REMAINDER of the stored body. Both earlier copies of this report are cut mid-sentence at their first such fragment. The repo's dev contract documents the marker being eaten; the truncation-of-everything-after is the sharper half and is worth adding wherever that guidance lives."
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions