Skip to content

[finding] docs(getting-started): the Build-with-Claude-Code tutorial's resolve_ticket action names a target handler nothing registers — the "Resolve" button it tells you to verify in the Console is the dead-button trap the Actions page warns about #22152

Description

@objectstack-fleet

Filing gate: ① product defect with a named landing spot and a reproduction (finding classes (a) — the example copied verbatim produces a non-working button — and (c) — metadata the runtime accepts but that does nothing at click time).
reach: public entry measured once — os validate on the tutorial's verbatim files passes (1 Actions), and os dev then prints WARN [action-governance] declared script actions with NO handler … ["my_app_ticket:resolve_ticket"]; the Actions page documents the click-time outcome as Action 'x' on object 'y' not found.
Reader: domain:devx seat (content/docs/getting-started/build-with-claude-code.mdx step 3 and step 5, and the mirrored example on quick-start.mdx); the optional authoring-time check is domain:spec / packages/lint.
Dedup: search_issues "docs build-with-claude-code resolve_ticket script action target resolveTicket no handler button wired to nothing action-governance" (open + closed) → 0 cards.
Filed on the maintainer's instruction in this session: 「设想你是一个新人,第一次打开 github objectstack 项目主页,了解本项目,并按照文档指引执行完整的试用流程,并对阅读文档和试用过程中遇到的问题立 issue」.

Summary

content/docs/getting-started/build-with-claude-code.mdx is the main on-ramp ("This is the main way you build on ObjectStack"). Step 3 shows the "representative result" the agent writes, including:

export const ResolveTicketAction = defineAction({
  name: 'resolve_ticket',
  label: 'Resolve',
  objectName: 'support_desk_ticket',
  type: 'script',
  target: 'resolveTicket',
  …
});

Nothing on the page registers a resolveTicket handler — there is no body, no onEnable / registerAction, no functions export, and the scaffolded objectstack.config.ts carries none. Step 5 then tells the reader to open the Console and "Check the Resolve action shows on an open ticket — and disappears once the ticket is resolved". The button shows, but clicking it cannot resolve anything.

The Actions page (content/docs/ui/actions.mdx, "The dead-button trap" callout) already documents this exact shape: a script action whose target names a handler that was never registerAction-ed "compiles fine but throws Action 'x' on object 'y' not found at click time". The tutorial reproduces the trap as its model answer, and the runtime says so on every boot:

WARN [action-governance] declared script actions with NO handler — a button wired to nothing (ADR-0078);
add a `body`, or register a handler under the declared `target` {"count":1,"actions":["my_app_ticket:resolve_ticket"]}

The same action is reused as the reading example on getting-started/quick-start.mdx ("Reading the other pieces → Actions").

Reproduction

create-objectstack@17.7.0 / @objectstack/cli@17.7.0, Node v22.22.0.

  1. npm create objectstack@latest my-app && cd my-app
  2. Author the four files from the tutorial's step 3 verbatim (object, action, view, app), with the namespace rewritten to my_app_*, and add the four barrel export lines.
  3. pnpm run validate → ✓ Validation passed, Data: 2 Objects 6 Fields UI: 1 Apps 1 Views 1 Actions — exactly the counts the tutorial prints. The gate does not mention the missing handler.
  4. npx os dev --ui → boot diagnostics print the [action-governance] … NO handler warning quoted above.
  5. The Actions page states the click-time outcome: Action 'resolve_ticket' on object 'my_app_ticket' not found.

So the tutorial's own sentence — "Every example on this page passes os validate verbatim in a freshly scaffolded project" — is true, and that is the problem: the gate the tutorial advertises as the thing that catches AI mistakes lets this one through, and the page never tells the reader the button is inert.

Expected

The model answer on the main tutorial page is a working action, and the reader's verification step in the Console succeeds. Either the example carries an inline body (Path A on the Actions page — a metadata-only handler, no code to register), or the page shows the onEnable registration (Path B) alongside the action, and the prose in step 5 asks the reader to click Resolve and watch the status change.

Actual

The tutorial ships a script action with an unregistered target, os validate accepts it, the Console renders a button that fails at click time, and the only signal is a boot-time WARN the tutorial never mentions.

Also worth deciding

Whether os validate should at least warn on a script action whose target names no handler the stack declares (functions, onEnable registration) — today the same condition is a boot-time WARN under ADR-0078 but silent at authoring time, which is the gap the tutorial falls into.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: the road — start: the tutorial builds a working app | 缺项 | P2

    Triage: first grade, bug · priority:p2 · domain:devx · area:devpath · pm:queue (finding removed). Direction: the tutorial's action does what it says, and the page tells the reader how to check

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T05:00Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in content/docs/getting-started/build-with-claude-code.mdx (steps 3 and 5) and the mirrored example on quick-start.mdx ⇒ domain:devx; rationale: content/docs/** is that lane's. Read on main ec8f37c890.

    • Why p2: a newcomer who copies the tutorial gets a Resolve button that does nothing; os dev warns about it, but the page tells them to verify it (measured).
    • Direction:
      • give resolve_ticket a handler the tutorial also writes, or make it a declarative action that needs none
      • step 5's check exercises the button and names what to expect
    • Not this card: an authoring-time refusal of a script action with no handler (a packages/lint rule) would be a new gate, so it defaults to no.
    • Clause-②: no. No changeset (docs).
  2. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8 · anchor of a two-card bundle (#22152 + #22165): both edit the getting-started pages, so they are carried by one PR
    Session: session_01VF48aw8RPG6wzDnMgp6rtw
    Account: os-justin (the seat's linked user as GET /user answers it; the cards' assignee)
    Branch: claude/issue-22152-getting-started-on-ramp (new, cut from origin/main 6ed0c0f3e5)
    Worktree: objectstack-issue-22152
    Domain: domain:devx
    Seat: domain:devx#2
    Cards closed by the one PR: #22152 (p2, this anchor) and #22165 (p3, the maintainer's ③; a pointer claim is on that card).
    File surface:

  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 22152,
    "status": "done",
    "branch": "claude/issue-22152-getting-started-on-ramp",
    "pr": "#22204",
    "session": "session_01VF48aw8RPG6wzDnMgp6rtw (subagent run: the parent PM session id, as the Claude-Session trailer on both commits records)",
    "premise_still_valid": true,
    "summary": "Bundle #22152 + #22165 in one draft PR (#22204, docs only, 5 files under content/docs/getting-started/, +64/-25, BASE c6fe02d, head 9bfd965). #22152: the tutorial's resolve_ticket is now the declarative single-record write (operation: 'update' + patch: { status: 'resolved' }, type left at its default) instead of type: 'script' + an unregistered target; a paragraph under step 3 says what it does and what a target handler would need; step 5 asks the reader to click Resolve and names the toast, the status change, the button disappearing and the ticket leaving Support → Open. #22165: index.mdx opens with the maintainer's routing line (agent writes the app → Build with Claude Code; by hand, every file → Your First Project) and the four entry pages' opening callouts follow it in one shape (agent path / by-hand path / the map, not a starting point / the why, not a starting point), each linking back to it. Route choice, measured in a project scaffolded from the published create-objectstack@17.7.0: the declarative route is a 4-line change with no new file and no config edit; the handler route needs a handler file plus an onEnable export in objectstack.config.ts, which contradicts the page's own 'the config is not edited' paragraph, and its ctx.engine runs elevated. The handler route was costed from the scaffolded config, not booted. Mechanism assumption 3 partly falsified: quick-start.mdx carries no action code (no defineAction, no target, no type: 'script'); the mirror is its Actions reading bullet, which now tells a reviewer to check what a click does and that an unregistered target passes os validate and is named only by the os dev boot warning. Assumptions 1, 2 and 4 held as written. No new lint rule; #22156's items untouched. Deviation to note: os dev on this host generated /root/.objectstack/dev-crypto-key (a dev-only key the CLI persists for every later os dev on the host); left in place because a dev server started after mine may have loaded it. The scaffolded scratch project and the worktree are removed after this report.",
    "tests": "Scaffold: npx create-objectstack@17.7.0 support-desk --template blank --skip-skills (exit 0); step 3's four files extracted VERBATIM from the page by script, plus the four barrel export lines; extraction from head 9bfd965 is byte-identical to the measured files (diff -r clean). BEFORE (main page): os validate → 'Validation passed', 'Data: 2 Objects 6 Fields', 'UI: 1 Apps 1 Views 1 Actions'; os dev --ui boot diagnostics 4 warnings incl. 'WARN [action-governance] declared script actions with NO handler ... {"count":1,"actions":["support_desk_ticket:resolve_ticket"]}'; POST /api/v1/actions/support_desk_ticket/resolve_ticket {recordId} → HTTP 404 RESOURCE_NOT_FOUND 'Action resolve_ticket on object support_desk_ticket not found', status stays open. AFTER (this branch): os validate → 'Validation passed (211ms)', same counts; tsc --noEmit in the project exit 0; os dev --ui boot diagnostics 3 warnings, grep -c action-governance = 0; same POST → HTTP 200 {"success":true,"data":{"operation":"update",...,"status":"resolved"}}, GET reads status resolved. Console (headless Chromium /opt/pw-browsers/chromium, the console shipped in @objectstack/cli 17.7.0): create a ticket via the Create Ticket form, the record opens; Resolve buttons before = 1; click Resolve → toast 'Ticket resolved.' seen; STATUS reads Resolved; Resolve buttons after = 0; the Console POSTed /api/v1/actions/support_desk_ticket/resolve_ticket → 200; after reload the ticket is absent from Support → Open (rows matching subject = 0). MCP run_action via an API key (tools/call run_action resolve_ticket) → "ok": true, "operation": "update"; GET reads status resolved. Step 4 still holds: a bare visible: 'status != "resolved"' on the declarative action → os validate exit 1 with the page's message (bare reference status ... Write record.status; rule: expression-invalid); restored and re-checked by grep count. Gates: node scripts/pm/dispatch-gates.mjs --commands at head derived the same 40 commands as at dispatch; all 40 run after the final commit at 9bfd965, all exit 0; dispatch-gates --ran: '40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN' (derived zero, every line carries its exit code). First pass: check:docs-transcript-drift and check:skill-examples exited 3 PREREQUISITE NOT MET (lint / client-react unbuilt) → built pnpm --filter '@objectstack/lint...' and '@objectstack/client-react...' through os-verify-lock.sh (VERDICT command-exit 0 both) and re-ran green. check:doc-anchors: 460 fragment links all resolve (incl. the new /docs/ui/actions#give-it-server-behavior--two-paths). check:nul-bytes green; control-byte grep over the 5 pages: no hits. @mdx-js/mdx 3.1.1 compiles all 5 pages. No package touched, so no build/test/typecheck owed; no changeset (docs only, skip-changeset). Reverse verification = the BEFORE column: the only difference between the two runs is the page's action block.",
    "mcp_calls": "0",
    "api_writes": "3 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]) carrying 4 writes: POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, #22204, body read back identical); POST /repos//issues/22204/labels (skip-changeset); POST /repos//issues/22204/assignees (os-justin); POST /repos//issues/22152/comments (this os-dev-report). Plus 2 git pushes (not REST). Local-only: POST /api/v1/keys on the scratch dev server (localhost, not GitHub).",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: public door, measured once - the Console Create Ticket form (console shipped in @objectstack/cli 17.7.0) renders Priority and Status as "Select an option", both required, although each declares an option with default: true; REST POST /api/v1/data/support_desk_ticket without status stores open (the declared default), so only the form ignores it · evidence: the tutorial's verbatim ticket object in a fresh create-objectstack@17.7.0 project, headless Chromium, New → Create Ticket; objectui-side reader, not re-measured against the current pin · dedupe words: console create form select option default true not preselected; picklist default value form required; Field.select options default",
    "class: a · reach: public door, measured twice - os dev --ui on a freshly scaffolded create-objectstack@17.7.0 blank project (empty .objectstack/data, 2 of 2 fresh boots) lists in its boot diagnostics WARN Insert operation failed {"object":"sys_migration","error":{"message":"UNIQUE constraint failed: sys_migration.id ..."}} with a full knex stack; not re-measured on main, cause not investigated · dedupe words: sys_migration UNIQUE constraint failed boot warning; Insert operation failed sys_migration fresh database; migration recovery duplicate insert os dev",
    "carrier: #22156 item 6 (serial after this PR, devx seat) · noted, not filed - the field-no-consumers verdict on the tutorial's description field reads "nothing in this stack reads or displays it ... inert", yet the Console Create Ticket form renders a Description textarea (measured); recorded in PR #22204 Acceptance notes so whoever takes item 6 picks between placing description on a view and showing the warning with this measurement in hand"
    ]
    }

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #22204 (bundle #22152 + #22165) · domain:devx#2 · 2026-10-08T06:44Z

    Seat review of PR #22204 against GitHub, at head 9bfd965329. Report: the os-dev-report on this card. Claims: 6053072940 (this card) and the member claim on #22165.

    Checklist conclusions

    • Shape: draft PR to main. Body line 1 is Fixes #22152 and line 2 is Fixes #22165, with no other closing keyword; [finding] docs on-ramp drift a first-time reader hits in sequence: stale CONTRIBUTING.md (retired spec repo), README's first curl gets 401, three pnpm floors, "three examples" vs five, os dev --help / os init strings, tutorial transcript #22156 is untouched. Clause-②: no is at line start. Both closed cards carry a Claim: naming the head branch, so the closing-target claim guard is satisfied.
    • Files: five pages under content/docs/getting-started/ (+64 −25): build-with-claude-code.mdx, how-ai-development-works.mdx, index.mdx, quick-start.mdx and your-first-project.mdx, the claim's surface.
      • No lint rule was added. skip-changeset is correct (docs only).
    • [finding] docs(getting-started): the Build-with-Claude-Code tutorial's resolve_ticket action names a target handler nothing registers — the "Resolve" button it tells you to verify in the Console is the dead-button trap the Actions page warns about #22152, read in the diff: ResolveTicketAction drops type: 'script' + target: 'resolveTicket' and becomes the declarative single-record write operation: 'update' + patch: { status: 'resolved' }. A new paragraph says what the click does, and what a target handler would need.
      • Checked against main's spec (packages/spec/src/ui/action.zod.ts:1136, :1155): patch is "written on the data plane as the caller: object permissions, hooks and validations fire as for a user edit". So the page's "runs as the signed-in user" sentence is the spec's own.
      • Step 5 now has the reader click Resolve, and names the toast, the status change, the button hiding and the ticket leaving Support → Open.
    • Route choice, measured in a project scaffolded from create-objectstack@17.7.0: the declarative route is a 4-line change with no new file. The handler route needs a handler file plus an onEnable export in objectstack.config.ts, which contradicts the page's own "the config is not edited". It was costed from the config, not booted.
    • Before and after, on the verbatim page files:
      • Before: os validate passes; the boot log warns [action-governance] … NO handler; POST /api/v1/actions/support_desk_ticket/resolve_ticket returns 404 "not found".
      • After: os validate passes with the same counts; the boot log has 0 action-governance lines; the same POST returns 200 with status: resolved.
      • In the Console (headless Chromium): clicking Resolve shows the "Ticket resolved." toast; Status reads Resolved; the Resolve buttons go from 1 to 0; after a reload the ticket is gone from Support → Open.
      • The step-4 predicate trap still refuses at os validate.
    • Zone 2 assumption 3, partly falsified and accepted: quick-start.mdx carries no action code. Its mirror is the Actions reading bullet, which now tells a reviewer to check what a click does.
    • [maintainer] docs(getting-started): four overlapping entry pages (How AI development works / Build with Claude Code / Your First Project / Anatomy) — add one routing line at the top of the index so a newcomer does not have to choose #22165: index.mdx opens with the maintainer's routing line (agent writes the app → Build with Claude Code; by hand, every file → Your First Project). The four entry pages' opening callouts follow one shape and link back to it.
    • Gates: 40 derived, 40 run, all exit 0. check:docs-transcript-drift and check:skill-examples first exited 3 (prerequisite not met); lint and client-react were built under the verify lock and both re-ran green. check:doc-anchors resolves 460 fragment links, including the new /docs/ui/actions#give-it-server-behavior--two-paths.
    • CI at head 9bfd965329: 26 success, 10 skipped (path-filtered: Build Core, Temporal and the dogfood matrix; Check Changeset under skip-changeset), 0 red. Every required context is green: Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate and Governed Surface Queue Guard. The only non-green item is Vercel's preview status, which is not required. check-governed-merges --pr 22204: NOT governed, 89 changed lines.

    Deviation, accepted: os dev on this host created /root/.objectstack/dev-crypto-key, a dev-only key the CLI persists. It was left in place, since another dev server may have loaded it, and it touches no repo state.

    Out-of-scope findings:

    Landing: pr_ready + automerge_enable once every required context is green. The merge closes #22152 and #22165.

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — PR #22204 → 7b926f7600 (bundle #22152 + #22165) · domain:devx#2 · 2026-10-08T07:20Z

  6. added a commit that references this issue on Oct 9, 2026
    7b926f7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:devxpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions