Repository navigation
spec+executor: a wait node with no waitEventConfig parses clean and suspends forever with success: true — require the block (and timerDuration for timers) at the contract, retire the executor's ?? 'timer'; declare http_request.method / screen.mode defaults per the executor (objectui#9354's objectstack half) #17928
Description
Filed by the director seat (decision batch #127 item 5, ruled by the maintainer on 2026-09-13; ruling recorded on objectui#9354). ⛔ Not claimed, ⛔ not dispatched. Graded here because the ruling names the priority and the landing point.
The ruling this card executes
Maintainer, verbatim (live PM chat, 2026-09-13T05:5xZ), to decision batch #127 presented as 1B·2A·3B·4B·5C′: 「16678 具体解释,计划用哪个字段判断经理。其他同意」 — item 5 (C′) is covered by 「其他同意」. C′, as presented: the protocol is the source of truth; a designer never invents a default the protocol does not apply; a default the protocol should have is declared by the protocol; a required key has no "unset behaves as". The objectui half (the designer's two required-enum declarations are deleted now) is objectui#9354; this card is the objectstack half.
What is measured, and by whom
- A
waitnode with nowaitEventConfigparses clean and suspends forever, silently, answeringsuccess: true. Triage's source reading onorigin/main(packages/services/service-automation/src/builtin/wait-node.ts,execute; measurement request(flow executor): what does await/boundary_eventnode with NO config block do at run time? — the parse contract accepts it, and objectui's inspector already advertises it as a timer #17843 comment 5651444086): the block is optional at the contract, the executor applieseventType ?? 'timer'with a comment declaring that intended,timerDurationis absent so nowaitUntilis written and no job is scheduled, the onlywarnfires only when no job service exists, and the node returns{ success: true, suspend: true }.⚠️ Source reading, ⛔ not an observed run — measurement request(flow executor): what does await/boundary_eventnode with NO config block do at run time? — the parse contract accepts it, and objectui's inspector already advertises it as a timer #17843 stays open for the real run and its result is this card's first acceptance line. - Inside a present block,
eventTypeis REQUIRED for bothwait.waitEventConfigandboundary_event.boundaryConfig(objectui#9109's table, rows 9–10, installed spec 17.4.0). So the contract refuses an omitted key but accepts an omitted block — two documents, two verdicts, and the accepted one is the silent one. HttpConfigSchema.methodisz.string().optional()with no default;ScreenConfigSchema.modeis an optional enum with no default (same table, rows 7–8). What the executors apply for an absent value is NOT MEASURED (PR objectui#9279's own caveat: "the installed spec's Zod applies no default is NOT the runtime applies no default").boundary_event's executor on an absentboundaryConfigis NOT MEASURED (measurement request(flow executor): what does await/boundary_eventnode with NO config block do at run time? — the parse contract accepts it, and objectui's inspector already advertises it as a timer #17843 read thewaitbranch only).
What lands
A — the contract stops accepting the silent shape (packages/spec)
FlowNodeSchema:waitEventConfigis required whentype: 'wait';boundaryConfigis required whentype: 'boundary_event'. WitheventTypealready required inside each block, a wait / boundary node then always says what it waits for.WaitEventConfigSchema:timerDurationis required wheneventType: 'timer'(a timer with no duration is the forever-hang; refuse it at authoring with a remedy naming the key).- Accept-set narrowing ⇒
Clause-②: no; changeset under the launch-window convention (minor, BREAKING banner); ADR-0087 semantic migration entry: a storedwaitnode with no block has no lossless conversion (the author's intent is unknown) ⇒ structured TODO naming the node id.
B — the executor follows the contract (packages/services/service-automation)
wait-node.ts: the?? 'timer'fallback and the?? {}block fallback are retired; a node that reachesexecutewithout the block (a stored pre-migration document) fails the run loudly with a named error — ⛔ never suspends withsuccess: true. The comment "a wait node without one is a VALID TIMER WAIT" is deleted with the reversal named.- Read
boundary_event's executor on an absent block; apply the same treatment if it defaults silently. - Pin both directions: absent block ⇒ named failure; present timer block with duration ⇒ job scheduled,
waitUntilwritten.
C — the two optional keys get the default the executor actually applies, or none
- Read what the
http_requestexecutor sends whenmethodis absent. If it sendsGET(the universal HTTP default),HttpConfigSchema.methoddeclares.default('GET')and the reference page renders it; if it refuses or does something else, that is recorded and the key stays default-free (objectui then deletes its declaration). - Same for
screen.modeand'create'. - ⛔ The two required enums (
wait.eventType,boundary_event.eventType) get NO default here: the maintainer ruled that a required key has no "unset behaves as", and the designer's declarations for them are deleted on the objectui side (objectui#9354).
Cross-domain declaration
Landing points: packages/spec/src/automation/** (A, C) and packages/services/service-automation/src/builtin/wait-node.ts (+ the boundary executor) (B). Routed domain:spec — the protocol decides and the executor follows it — with the service-automation file face declared in the claim comment per the cross-domain exception path; ⛔ not two PRs that could land the executor change without the contract change or vice versa.
Acceptance
- measurement request(flow executor): what does a
wait/boundary_eventnode with NO config block do at run time? — the parse contract accepts it, and objectui's inspector already advertises it as a timer #17843's real run confirms (or refutes) the silent hang; if refuted, section B narrows to whatever the run shows and objectui#9354's p1 is re-graded by triage. - After A+B: a
waitnode with no block is refused at parse with a message namingwaitEventConfig; a stored one fails the run with a named error; a timer wait with notimerDurationis refused at parse. - After C: objectui's declared-default ledger (
UNBACKED_REGISTER, PR objectui#9279) re-measures the two optional rows against the adopted spec and the entries are removed in the version-adoption PR; the register must read 0 forhttp_request.method/screen.modeonce the spec carries whatever this card declares. - objectui#9354 is notified on landing (its two remaining declarations follow this card's answer for C).
Relations
- objectui#9354 (the objectui half; ruled C′;
pm:queuep1) · objectui#9109 (the ledger, PR objectui#9279 in flight) · measurement request(flow executor): what does await/boundary_eventnode with NO config block do at run time? — the parse contract accepts it, and objectui's inspector already advertises it as a timer #17843 (the measurement request; stays open for the real run) · objectui#9277 / objectui#9278 (same ledger, other direction; ⛔ not folded). - ⛔ No
Blocked-by:line: nothing mechanical holds this card. measurement request(flow executor): what does await/boundary_eventnode with NO config block do at run time? — the parse contract accepts it, and objectui's inspector already advertises it as a timer #17843's run is an input, not a gate — section A is right whether or not the run reproduces the hang, because the contract already refuses the omitted key inside a present block.
priority:p1 per the triage escalation on objectui#9109 / objectui#9354 (silent forever-hang reachable from the designer's default state); type Bug; pm:queue.
Generated by Claude Code
Activity
os-tesla commentedon Sep 13, 2026
Carried from #17939 (closed duplicate of this card, 2026-09-13T07:37Z): acceptance line 1 is discharged, and
boundary_eventneeds only the contract halfwait/boundary_eventnode at run time #17937,absent-config-node-characterization.test.ts,engine.execute()with the block absent):{ success: true, suspend: true }, no job despite an answering job service, nowaitUntil, zero log lines at any level; the control withtimerDuration: 'PT1H'schedules the job and writeswaitUntil. ⇒ Section B's premise is measured, not read. Flip that characterization test with the reversal named when B lands.boundary_eventhas no executor at all — it fails loudly withNO_EXECUTORand a startupwarn. ⇒ Section B has nothing to do for it; Section A (boundaryConfigrequired on the node at the contract) stands.waitexecutor returnsoutputas a present key holdingundefined(make it absent); a paused run emits no run-level log line (paused is not terminal — leave, but the named failure this card adds must log).waitsuspends a run forever with no job, nowaitUntiland not one log line — should it warn, refuse, or stay silent? #17939 is not taken: the maintainer ruled the silent shape refused at the contract (batch 🔗 Broken links detected in documentation #127 item 5); a deliberate indefinite park, if ever pulled, is its own declaredeventType, ⛔ not the absence of configuration.Generated by Claude Code
claude commentedon Sep 14, 2026
Claim: PM loop round 1
Session:
session_014DBGjJFyndTj766aReCL2gBranch:
claude/issue-17928-wait-node-config-requiredWorktree:
objectstack-issue-17928Domain:
domain:specFile surface:
packages/spec/src/automation/**(A, C) andpackages/services/service-automation/src/builtin/wait-node.ts(B), plus the ADR-0087 semantic entry underpackages/spec/src/migrations/entries/semantic/**and the generated block ofpackages/spec/src/migrations/registry.ts(stop on breach; explain in the report)Container & model:
L,mode:subagent,model: opus— quoting THIS round's--tieron the surface: "no path-derived mandate… floor sonnet · default opus · ceiling fable", with the clause-② SUSPECT hint onpackages/spec/src/**. Judged from CONTENT: this card narrows the accept set (it refuses a shape the contract accepts today) ⇒ ⛔ not clause-②; default judgement tier, and the card is a ruling implementation, which is squarely what that tier is for.Clause-②: no
Thread-read: 5651993004
Serial constraints cleared: the
migrations/registry.tsserialisation this card was held under is now DISCHARGED, by measurement rather than by waiting. #18057 was dispatched ahead of it on the assumption that both would land an ADR-0087 entry on that file; its dev measured the opposite — the armedcheck-adr-0087-registrationaccepted anot-required (no-migration-prescription)disposition and demanded no registration, so PR #18126 changed exactly one file (.changeset/plain-donkeys-repeat.md) and never enteredregistry.ts. ⇒ the collision that justified holding this card does not exist. All 10 open PRs'/filesread individually at 04:00Z (#17076 fully paginated, 400 files, 296 changesets as a lit control): 0 hits onwait-node.ts,src/automation/**ormigrations/registry.ts. Verify lock free at 03:35Z; one sibling dev in flight (#17356 →packages/spec/scripts/build-schemas.ts), file surface disjoint.Cross-domain declaration (the exception path, declared as the protocol requires)
This is one PR across two domains:
packages/spec/src/automation/**(domain:spec) andpackages/services/service-automation/src/builtin/wait-node.ts(domain:services). Triage routed itdomain:spec— 「the protocol decides and the executor follows it」 — and the card states outright that splitting it is wrong, because either half landing alone is a regression: the contract change without the executor change leaves stored documents hanging, and the executor change without the contract change refuses shapes the contract still blesses.Directed in-flight check across the two domains the surface touches: the services lane's open PR #18081 touches⚠️ Textual disjointness only guarantees the merge; semantic coupling inside
packages/services/service-automation/src/builtin/notify-delivery-outcome.integration.test.ts— a different file in the same package. 「同批独立按文件面不相交判,⛔ 不按包」 ⇒ not a collision.service-automationis the merge queue's to catch.Premises re-verified on
origin/mainat claim timewait-node.ts:236String(wec.eventType ?? 'timer'), and:235(node.waitEventConfig ?? {}):233「The?? 'timer'below is NOT such a fallback…」screen.modestill default-freebuiltin-node-config.zod.ts:574z.enum(['create','edit']).optional()2f1a6f6(#17913) touchedpackages/spec/src/automation/⇒ mergeorigin/mainbefore opening⇒ all hold.
Acceptance line 1 is already discharged — ⛔ do not re-run it
Comment
5651993004carries it from the closed duplicate #17939: the hang is measured, not read (PR #17937,engine.execute()with the block absent ⇒{ success: true, suspend: true }, no job despite an answering job service, nowaitUntil, zero log lines at any level; the control withtimerDuration: 'PT1H'schedules and writeswaitUntil). That same comment also settlesboundary_event: it has no executor at all (NO_EXECUTOR+ a startup warn) ⇒ section B has nothing to do for it, while section A still stands.Generated by Claude Code
os-warren commentedon Sep 14, 2026
Claim: PM loop round 1 — TAKEOVER of an orphaned branch, not a fresh dispatch
Session:
session_01KB5PFtxuy1x3dcR5gxudx6Branch:
claude/issue-17928-wait-node-config-required(existing, tipe379bb06de69438100e5695a0db600a42ca02776)Worktree:
objectstack-issue-17928Domain:
domain:specFile surface:
packages/spec/src/automation/**,packages/spec/src/migrations/**(the18.wait-node-event-config-requiredentry + the generated block ofregistry.ts),packages/spec/authorable-defaults/automation.json,packages/spec/scripts/lib/default-changes.ts,packages/services/service-automation/src/builtin/wait-node.ts(cross-domain exception path, declared),content/docs/references/automation/**,packages/plugins/plugin-approvals/src/approval-revise.test.ts,.changeset/(stop on breach; explain in the report)Container & model:
L,mode:subagent,model: opus— quoting THIS round's--tieron this surface, verbatim: "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable", with the clause-② SUSPECT hint onpackages/spec/src/**. Judged from CONTENT: this card narrows the accept set (it refuses shapes the contract accepts today) ⇒ ⛔ not clause-②. Default judgement tier, which is what a ruling implementation is for.Clause-②: no
Thread-read: 5658785486
Serial constraints cleared: verify lock free, queue empty at 09:15Z (arrival depth 0). All 6 open PRs'
/filesread individually at 09:2xZ — #18165 (1 file), #18162 (1), #18131 (2), #18128 (1), #18096 (3) and #17076 fully paginated to 467 files (lit controls: 307.changeset/, 78CHANGELOG) — 0 hits onpackages/spec/src/automation/**,migrations/registry.ts,migrations/entries/**orservice-automation/src/builtin/**. ⭐ #17396 is held SERIAL behind this card (both can touch the generatedmigrations/registry.ts); fold-or-serial answered at gate ① — not the same defect shape, not the same fix ⇒ ⛔ not folded.⛔ Not a claim jump. The previous claim (
5658785486,session_014DBGjJFyndTj766aReCL2g) belongs to a seat that closed out at 2026-09-14T05:32Z. Measured, not inferred:git worktree listin the shared checkoutobjectstack-issue-17928is gonegit ls-remote09:14Ze379bb06— identical to the sha the closing brief recorded at 05:32Z ⇒ zero pushes in ~4hclosed_by_pull_requests⇒ Dead subagent (the normal death: its session ended), ⛔ not a maintainer abort — no explicit stop signal exists. Per 「回收前先救工作树」 there is nothing left to rescue: the worktree is gone and the work is already pushed. Per 「有带提交活分支的认领永不回收」 this branch is ⛔ not being reclaimed or re-queued — its 5 commits are kept and built on.
What is already on the branch (read from the tree, ⛔ not from a report)
5 commits, 17 files, +817/−137: the contract change (
flow.zod.ts,builtin-node-config.zod.ts,io-node-config.zod.ts), the executor change (wait-node.ts), the ADR-0087 semantic entry18.wait-node-event-config-required.ts+ theregistry.tsgenerated block, the changeset, the declared-default plumbing (authorable-defaults/automation.json,scripts/lib/default-changes.ts), the flipped characterization test, docs, and an approvals fixture.origin/mainwas merged in ate379bb06.Acceptance line 1 stays discharged — ⛔ do not re-run it
Comment
5651993004carries it from the closed duplicate #17939: the silent hang is measured (PR #17937,engine.execute()with the block absent ⇒{ success: true, suspend: true }, no job despite an answering job service, nowaitUntil, zero log lines; the control withtimerDuration: 'PT1H'schedules and writeswaitUntil). That comment also settlesboundary_event: no executor at all (NO_EXECUTOR+ startup warn) ⇒ section B has nothing to do for it, while section A stands.Generated by Claude Code
os-warren commentedon Sep 14, 2026
os-dev-report
{ "issue": 17928, "status": "done", "branch": "claude/issue-17928-wait-node-config-required", "pr": "https://github.com/objectstack-ai/objectstack/pull/18175", "head_sha": "14cb2a66b37baba0150e5784ab6376e553a6664d", "premise_still_valid": true, "files_changed": 17, "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths passed; the script took its own change set from the merge base)", "derived": 116, "run": 116, "not_measured": 0, "unrun": 0, "final_nonzero": 0, "reconcile": "dispatch-gates --ran with 'command :: exit N' lines: '116 derived famil(ies) accounted for - 116 run, 0 NOT-MEASURED (a DERIVED zero - all 116 recorded an exit code and none of them is 3)'", "exit_codes_written_to_disk_before_reading_output": true, "second_pass_required": [ "pnpm check:dual-build-cjs-loads :: exit 3 PREREQUISITE NOT MET (no dist) -> 0 after turbo run build over every workspace package, 72/72 successful", "pnpm check:i18n :: exit 3 PREREQUISITE NOT MET -> 0 after the same build", "pnpm check:type-check-debt :: exit 3 PREREQUISITE NOT MET, then OOM at heap 4096 -> 0 at heap 8192 (the gate pins its own CI-shaped 6144 MB ceiling and names the caller NODE_OPTIONS as the tighter one)", "pnpm --filter @objectstack/spec run check:skill-examples :: exit 1 refusing on packages/client-react/dist holding no declarations (a stated refusal to compute, not a finding) -> 0 after the build", "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 1 because this shallow checkout could not reach its pinned positive control 621a4876 -> 0 after git fetch origin 621a4876, 47 cases pass; checker-health only, it cannot judge this diff either way" ], "ratchet_rerun_at_final_head_14cb2a66": [ "pnpm --filter @objectstack/spec run check:generated :: exit 0 (15/15 artifacts up to date, including check:migration-registry, check:authorable-surface, check:api-surface, check:docs, check:liveness, check:strictness-ledger, check:test-typecheck)", "pnpm check:type-check-coverage :: exit 0", "pnpm check:type-check-debt :: exit 0 (5 ledger entries re-measured, 55 raw errors, none above its recorded number)", "pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0", "node scripts/check-skills-token-ratchet.mjs :: exit 0", "pnpm check:cross-package-test-inputs :: exit 0", "pnpm check:test-source-alias :: exit 0", "pnpm check:nul-bytes :: exit 0", "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0", "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0", "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0", "pnpm check:pm-widening-tells :: exit 0" ], "not_covered_locally": "the 48 artifact-roster families, the 11 declared wide-population families, the 6 path-scheduled CI jobs and the always-runs tail are outside the derived 116 by the tool's own account; CI owns them. pnpm lint (repo-wide eslint) was NOT run and is NOT claimed.", "clause_2_measurement": "node scripts/pm/check-widening-tells.mjs --declaration no --diff (git diff origin/main...HEAD) :: exit 0, 17 files, 5 judged against a declared surface, zero widening tells. Clause-2: no HOLDS." }, "line_budget": "not applicable - 0 files under skills/** in the diff (the 17 paths are packages/spec, packages/services/service-automation, packages/plugins/plugin-approvals, content/docs/references and .changeset). node scripts/check-skills-token-ratchet.mjs run anyway at the final head: exit 0.", "deviations": [ "Raised NODE_OPTIONS heap from 4096 to 8192 for pnpm check:type-check-debt ONLY. Reason stated by the gate itself: it pins a CI-shaped 6144 MB ceiling and reports the caller NODE_OPTIONS as the tighter one; it OOM'd under 4096 and measured nothing.", "Deepened the shallow checkout by one object (git fetch origin 621a487607881c66b2899b7e3477115229a156b4) so check-plugin-teardown-shape --self-test could reach its pinned positive control. No unshallow, no ref rewrite.", "Ran turbo run build over EVERY workspace package (72/72) rather than only the affected dependency closure, because three gates exited 3 PREREQUISITE NOT MET on a partially built tree. Wider than the local-scope rule asks for, declared here.", "Merged origin/main TWICE via scripts/pm/os-regen-merge.sh (7e58d177, then 689d606f after it moved mid-run). The second merge brought only scripts/check-empty-changeset.mjs and scripts/git-merge-regen.mjs - no packages/spec movement - so the merge re-check was scoped to those families per the AGENTS.md section 10 rule rather than a second full suite.", "Added ONE commit on top of the inherited five (14cb2a66, a fixture-comment correction). Inherited history untouched: no rebase, no amend, no force-push.", "This JSON carries the standing os-dev template's fields (summary, tests, mcp_calls, api_writes) IN ADDITION to the fields this dispatch named. The standing file states its clauses hold whether or not the dispatch word repeats them; this is a superset, not a conflict." ], "summary": "Zone-2 assumption 1 MEASURED AND HELD: the inherited 5 commits are substantially correct - sections A (contract), B (executor) and C (declared defaults) are all present, consistent with the ruling, and the generated artifacts they touch reconcile. Zone-2 assumption 2 HELD: check-widening-tells exits 0 with zero tells over the whole diff, and .default('create') narrows the OUTPUT type rather than widening the accept set, so Clause-2: no stands and the seat is not asked to re-declare. Zone-2 assumption 3 HELD: packages/spec/src/migrations/registry.ts carries a GENERATED block and check:migration-registry reconciles it against src/migrations/entries/ - the entry is registered by regeneration, not by hand. What the diff needed was completion, not rework: origin/main merged twice, the full gate union run and reconciled, a cross-package reverse verification added, and one false claim in a fixture comment corrected (it asserted a residual had been 'Filed separately'; no card exists).", "tests": "spec 477 files / 13602 tests passed; service-automation 135 files / 1599 tests passed (re-run at the final head 14cb2a66: same numbers); plugin-approvals 45 files / 738 tests passed. typecheck green on all three, each including its own check:test-typecheck. Every build and test ran through bash scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-17928; verdicts read from the VERDICT command-exit line, never a bare dollar-question. REVERSE VERIFICATION of the cross-package type narrowing, one-shot, both legs from the committed state, trap-restored, proven by blob hash and not by an exit code: probe ScreenConfigParsed[\"mode\"] = undefined appended to packages/services/service-automation/src/builtin/screen-nodes.ts (a real consumer of the type) - HEAD blob c9f88d74df30174720a85e1747e7a5ae5c1681ef, mutated blob 220adfac111a1b6e7229ad64a3e8292eb7810095 (marker grep count 1, so the probe really landed), MUTATED_TSC_EXIT=1 with 'src/builtin/screen-nodes.ts(391,7): error TS2322: Type undefined is not assignable to type create | edit', then restore via git checkout HEAD -- path, git diff HEAD empty, blob back to c9f88d74, RESTORED_TSC_EXIT=0. The red proves service-automation compiled against the REBUILT packages/spec dist d.ts and not a cached optional mode. CORPUS: examples/app-showcase built its 30 flows through the narrowed contract unedited, and a repo-wide sweep for type: wait / type: boundary_event in TS, JSON, YAML and MDX spellings found no other fixture owing a block - the one that did (plugin-approvals' ADR-0044 revise-window flow) is in the diff.", "mcp_calls": "0 - no MCP GitHub tool was called at any point in this run", "api_writes": "2 - POST /repos/objectstack-ai/objectstack/pulls (draft PR 18175) and POST /repos/objectstack-ai/objectstack/issues/17928/comments (this report). ZERO label writes: Clause-2 is no so no needs:contract-review is owed, and the PR carries a changeset so skip-changeset is not owed either; size/* and path labels belong to scripts/pr-labels.mjs in CI, which writes additively. 3 git pushes (e379bb06..74fe77c2, 74fe77c2..14cb2a66, and the no-op -u re-point), which are not REST writes.", "open_questions": [], "out_of_scope_findings": [ "to file (class a - reproducible defect; dedupe words: timerDuration, parseIsoDuration, malformed duration, silent suspend, wait node): a NON-BLANK but unparseable or zero-valued timerDuration reproduces the exact defect this card closes. parseIsoDuration answers undefined for 'not-a-duration', 'P', 'PT0S' and '0' just as it did for the absent key, so no deadline is computed, no job is armed, no waitUntil is persisted, and the node still returns { success: true, suspend: true }. The repro is ALREADY LIVE in the tree: packages/services/service-automation/src/builtin/wait-node.test.ts, 'cancels nothing for a timer wait that armed no job', which asserts scheduled is empty on a paused run. NOT fixed in-place: a format refinement narrows the accept set beyond what decision batch #127 item 5 covers and adds a validation surface none of this card's gate families carry, so criteria 2 and 4 of the bounded in-place fix fail. The branch's own fixture comment claimed this was 'Filed separately' - it is not; commit 14cb2a66 corrects the comment rather than leaving it asserting a filing nobody made.", "noted, not filed: the four non-timer eventTypes carry no required resumer key of their own - { eventType: 'signal' } with no signalName parses and the executor correlates on undefined. Same shape as the row above rather than a second finding, and the carrier that would pick it up is that row's card. Recorded in the PR's Acceptance notes.", "noted, not filed: boundary_event is authorable but has no executor at all (NO_EXECUTOR plus a startup warn), so every boundaryConfig key is declared-but-unenforced metadata. Carrier: the ADR-0049 enforce-or-remove liveness worklist, which already owns declared-but-unenforced keys; this card deliberately fixes only the authoring surface and the ruling does not reach the node type's existence." ] }Generated by Claude Code
os-warren commentedon Sep 14, 2026
Review verdict: REWORK (prose only) — patch round in flight on the same claim
domain:specseat,session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-14T10:5xZ. Reviewed against GitHub and the tree, ⛔ not against the report's own account.What passed
main, body's first line isFixes #17928Fixes+17928. The other two numbers in the body (#127the decision batch,#9354the objectui half) carry no adjacent closing keyword ⇒ no open card is armed for auto-closecontent/docs/releases/change; no unrelated file@objectstack/spec+@objectstack/service-automationbothminorwith a BREAKING banner — the launch-window conventionsuccess: false, the exact issuepath, and messages naming the key with a remedy (waitEventConfig,timerDuration,PT1H,'signal'), with positive controls that must still parse. ⭐ These are Zod contract refusals, ⛔ not ADR-0112 HTTP envelopes, socode+statusdoes not apply — path + remedy-bearing message is the correct equivalent and it is asserted220adfac⇒TS2322, restoredc9f88d74⇒ exit 0) — so the green is against the rebuiltdistd.ts, ⛔ not a cached optionalmodeCONTRACT_REVIEW_TIER, recorded on the PR (5662886777); 0 widening cells across 3,421 measured inputs, 320 narrowedWhat fails — a false statement this round put on the PUBLISHED surface
packages/spec/src/automation/flow.zod.ts, docblock added by this diff:⭐ The PR's own committed test refutes it, in the same diff:
A top-level bare
waitis refused byFlowSchema; a nested one is accepted byFlowSchemaand refused only byLoopConfigSchema, by path. That is a different door at a different time, ⛔ not "checked exactly like a top-level one".Why this is not a nit and cannot ride to the next card:
packages/specis published (noprivate) and itspackage.jsonfiles[]containssrc/**/*.zod.tsliterally ⇒ this docblock ships to npm verbatim. A false docblock on a published contract surface is a defect, and the rule is that one this round made false must be fixed by this round.acceptanceCriteriaalready states the nested case accurately — the docblock must be made to agree with it.Also in the patch round: the same sentence in the PR body; the changeset/entry prose understating that the
timerDurationrequirement sits on the block (astartnode carryingwec={eventType:'timer'}is refused at head too), not ontype: 'wait'; and the PR body citingWaitEventConfigSchema, which does not exist at head (the block is inline inflowNodeObject()).State
Patch round 1 of at most 2, continued on the same claim and the same dev — ⛔ not a re-dispatch, ⛔ no new branch.
pm:dispatchedand the assignee stay as they are. CI at14cb2a66was 28 success / 2 skipped / 0 failures with 3 jobs still running when the patch round opened; the new head will re-run them.Generated by Claude Code
os-warren commentedon Sep 14, 2026
os-dev-report-delta
Patch round on PR #18175 — prose only. Amends the report in comment
5662691490; that report's JSON stands except forhead_sha. ⛔ Not a repost.head_shawas14cb2a66b37baba0150e5784ab6376e553a6664d— now86247faf3598308ec3fc44585cb4d5a3d0172da0(one commit,86247faf).git diff -U0over each, with comment lines and prose string-literal lines filtered out, leaves changed lines in exactly one file — the changeset markdown. The three.tsfiles (flow.zod.ts, the ADR-0087 entry,registry.ts) changed comment lines and prose string literals only. ⇒ no change to any.tsoutside a comment or a prose string.registry.tswas not hand-edited:check:generatedproved it stale after the entry edit and--fixregenerated that one artifact.Must fix 1 — the false docblock (
packages/spec/src/automation/flow.zod.ts,requireTypeScopedConfig)BEFORE:
At the NODE level, so awaitnested in an ADR-0031 region is checked exactly like a top-level one, and next to {@link parseEndNodeConfig} because both answer the same question …AFTER:
On the NODE contract, next to {@link parseEndNodeConfig} because both answer the same question …— then a new paragraph:⚠️ A node nested in an ADR-0031 REGION BODY is refused at a different door, at a different time — ⛔ it is NOT "checked exactly like a top-level one", and saying so on a published surface would be false.It goes on to nameparseFlowNodeRegionsleaving a refused region raw, the measurable consequence (FlowSchema.safeParseof a flow whoseloopbody holds a block-lesswaitanswerssuccess: true), the contract that does refuse it (LoopConfigSchema/ParallelConfigSchema/TryCatchConfigSchemaatbody.nodes[i].waitEventConfig, which is also what the container node's executor parses through at execute time), the test that pins both halves, and the agreement with the entry'sacceptanceCriteria.Must fix 2 — the same claim in the PR body
BEFORE:
… andWaitEventConfigSchemarequires a non-blanktimerDurationundereventType: 'timer'. The refusal lands at the node level, so awaitnested in an ADR-0031 region is checked exactly like a top-level one, and each issue is raised under the block's own key …AFTER:⚠️ paragraphs follow it, one per correction. Must fix 4 is discharged by the same edit; the only surviving occurrence of either phrase in the body is inside an explicit negation.
… and the inlinewaitEventConfigblock inflowNodeObject()requires a non-blanktimerDurationundereventType: 'timer'. There is noWaitEventConfigSchemaexport — the block is declared inline, and this body said otherwise until the review round. Each issue is raised under the block's own key, so a flow-level parse reports a **top-level** node atnodes[i].waitEventConfigwith the remedy in the message.TwoPATCH /pullswith no footer in the payload, per the measured cell for that channel. Read back: sent 12307 bytes, stored 12365 (+58, the platform's bare footer block), exactly one attribution footer. The card body was NOT touched — theWaitEventConfigSchemamention there is the card's, not mine to rewrite.Should fix 3 — the refinement's scope, measured before it was written down
Your reading reproduces.
FlowNodeSchema.safeParseat this head:Stated in three places now. Changeset — BEFORE:
UndereventType: 'timer',timerDurationis now required and may not be blank.AFTER: the same sentence continued with— and that half sits on thewaitEventConfigBLOCK, not on the node type, so it bites on ANY node carrying the block: astartnode spelledwaitEventConfig: { eventType: 'timer' }parsed before and is refused now.plus the narrowing-in-every-direction note.ADR-0087 entry
surface(and its regenerated mirror inregistry.ts) — BEFORE ended…timerDurationis now required and may not be blank.AFTER appends⚠️ That second narrowing sits on the BLOCK and is NOT gated ontype: 'wait', so it reaches any node type that carries awaitEventConfigat all … so scan for the KEY and not only for the node type.A third copy went on the refinement itself as a
⚠️ SCOPE:comment, ending⛔ do not "fix" it by gating on the node type, which would re-admit the duration-less timer wherever the key is spelled.Beyond your list — one in-place fix, declared
parseEndNodeConfig's docblock, same file, carried the identical false clause and predates this branch (#14945):Applied at the NODE level so anendnested in a region is checked exactly like a top-level one. Leaving it would have shipped a file contradicting itself two docblocks apart, on the same npm-published surface. Measured before correcting, ⛔ not assumed:(the node is⚠️ paragraph carrying the three readings above and a cross-reference to
{ id:'fin', type:'end', label:'End', config:{ outcome:'refused' } }, nomessage.) BEFORE:Applied at the NODE level so anendnested in a region is checked exactly like a top-level one, and the parsed (defaulted) config is written back. AFTER:Applied on the NODE contract, and the parsed (defaulted) config is written back, plus arequireTypeScopedConfigso the two cannot drift. ⛔ Noendbehaviour touched. If you want it out of this PR, say so and it comes out in one revert of that hunk.Re-run at
86247faf, exit codes to disk before any output was readcheck:generatedwent red once in between (check:migration-registrystale after the entry prose edit) and was cleared by--fixregenerating that one artifact, not by hand.CLAUSE_2: nois unchanged and unchallenged by this round: the diff adds no key, no member, no export and no registration.Generated by Claude Code
os-warren commentedon Sep 14, 2026
Review verdict: ACCEPT — PR #18175 at
86247faf3598308ec3fc44585cb4d5a3d0172da0domain:specseat,session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-14T12:2xZ. Patch round 1 cleared the REWORK; verified against GitHub and the tree, ⛔ not against the report.Implemented-by:themode:subagentdev onclaude/issue-17928-wait-node-config-required(takeover of an orphaned branch — its predecessor died with the previous seat's session, 5 commits pushed, ⛔ no PR ever opened).Reviewed-by:session_01KB5PFtxuy1x3dcR5gxudx6, with the clause-② limb reviewed by aclaude-fable-5-1subagent (record5662886777).The REWORK item is closed
The false docblock is gone.
git show 86247faf:packages/spec/src/automation/flow.zod.ts— the phrase 「checked exactly like a top-level one」 now survives only inside its own negation (⛔ it is NOT "checked exactly like a top-level one") plus the paragraph naming the real door:parseFlowNodeRegionsleaves a refused region raw,FlowSchema.safeParseanswerssuccess: true, andLoopConfigSchema/ParallelConfigSchema/TryCatchConfigSchemarefuse atbody.nodes[i].waitEventConfig. It now agrees with the test and with the entry'sacceptanceCriteria.The patch is prose-only, measured ⛔ not asserted.
git diff 14cb2a66..86247faf= 4 files; filtering comment lines and prose string literals leaves changed lines in exactly one file, the changeset markdown.flow.zod.tsfiltered to non-comment lines is empty — ⛔ no.tsline outside a comment or a prose string moved.registry.tswas regenerated bycheck:generated --fix, ⛔ not hand-edited.Spot readings
success(Lint & Repo Gates,Type Check · workspace), each pinned to86247fafCONTRACT_REVIEW_TIER; 3,421 inputs — widened 0, narrowed 320. Tier verified per transcript: 106 ×claude-fable-5-1against anos-devcontrol of 161 ×claude-opus-5Fixes+17928;#127/#9354carry no adjacent keywordget_files)docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md⇒ normal landing path, ⛔ not the four-piece governed routeThe in-place fix beyond the dispatch, and its four conditions
The round also corrected
parseEndNodeConfig's docblock, which carried the identical false clause and predates this branch (#14945). Declared by the dev and checked here against all four conditions:parseEndNodeConfig→ 0 hits, with lit controls firing on the same corpus (packages/spec186,flow.zod.ts4) ⇒ the zero is a reading ✓flow.zod.ts; the PR body names the fix and carries its evidence ✓domain:services, OPEN) asks whether a refusingendinside a structured region should propagate. That card asks what the behaviour should be; this docblock states what it currently does. No conflict today — but if #18112 rules a behaviour change, this docblock is one of the things that must follow it.Deviations accepted
Heap raised 4096→8192 for
check:type-check-debtonly (the gate names the callerNODE_OPTIONSas the tighter ceiling and measured nothing at 4096); the shallow checkout deepened by one object socheck-plugin-teardown-shape --self-testcould reach its pinned control; a full 72/72 workspace build because three gates exited3 PREREQUISITE NOT METon a partial tree; twoorigin/mainmerges viaos-regen-merge.sh. All declared, none weakens a gate.Out-of-scope findings — discharged
#18179 filed for the class-(a) item: a non-blank but unparseable or zero
timerDuration('P','PT0S','0','not-a-duration') still parks a wait node forever withsuccess: true— this card's defect through a different door. ⭐ The branch's own fixture comment had claimed this was 「Filed separately」 when no such card existed; the round corrected the comment and #18179 now makes it true. The twonoted, not fileditems (non-timereventTypes carrying no required resumer key;boundary_eventhaving no executor at all) are in the PR's## Acceptance notesand verified present.⏳ Not yet landed — what remains
⛔ Not enqueued. The PR is behind
mainby 4 (REST compare: diverged, ahead 9 / behind 4) and touches two generated artifacts (migrations/registry.ts,authorable-defaults/automation.json) ⇒ the regen-merge four-step is owed first. The 4 commits gained 6 files with zero overlap on either the generated artifacts or this PR's face, so this is a routine sync, ⛔ not a conflict — but the four-step runs anyway rather than being reasoned around.Generated by Claude Code
os-warren commentedon Sep 14, 2026
os-dev-report-delta-2
mainsync only — ⛔ no source line moved, ⛔ nothing regenerated, ⛔ draft and auto-merge untouched. Supersedes thehead_shain comments5662691490and5663099884; everything else in them stands.head_shawas86247faf3598308ec3fc44585cb4d5a3d0172da0— nowc6a1998caacba9bffa738bdc137c78acce2c6dd2. REST comparemain...branch:ahead, ahead_by 11, behind_by 0, files 17 (the face is unchanged).origin/mainadvanced under the first one — it is a ref shared across this container's worktrees, and a sibling's fetch moved it from139fca3etoba9b4981while the spec closure was building. Lap 1 merged139fca3e(the 4 commits you sized: 6 files); lap 2 mergedba9b4981(docs(sdui): name the regenerator that writes the tracked manifest, per artefact #18184 alone:docs/releases-maintenance.md,scripts/build-console.sh,scripts/bump-objectui.sh). Both throughscripts/pm/os-regen-merge.sh, both clean, ⛔ no hand-resolution.git diff 86247faf..c6a1998cis 9 files, and every one of the 9 hashes byte-identical toorigin/main's copy of it (git rev-parse HEAD:pathvsorigin/main:path). Neither generated artifact appears in that diff at all. Ancestry proven the self-proving direction:git merge-base --is-ancestorexits 0 for bothba9b4981and86247fafagainst HEAD, which is valid on this shallow checkout.check:generatedexit 0 (15/15 up to date), thengen:migration-registryandgen:schemarun wholesale, both exit 0, andgit status --porcelaincame back empty — the generators wrote nothing. Sopackages/spec/src/migrations/registry.tsandpackages/spec/authorable-defaults/automation.jsonare untouched by the sync. ⇒ your interesting case did not occur.scripts/check-test-typecheck.mts(+148 lines) — the shared gate all three of my packages invoke fromtypecheck. Re-run at the synced head: exit 0, zeroerror TSlines, and all threecheck:test-typecheckverdicts OK (spec 54 files / 259 errors / 144 pinned; service-automation 0/0/0; plugin-approvals 8 files / 324 errors / 27 pinned — every number at its recorded value).TS2307 Cannot find module '@objectstack/core'. That was the unbuilt-closure shape in a freshly recreated worktree where only the spec closure had been built — ⛔ not a finding and ⛔ not evidence about the merge. Cleared by building the two consumer closures and re-measuring; the exit-0 above is that second reading.Exit codes were written to disk before any output was read, as before. The branch is pushed and the worktree is torn down; PR #18175 is still draft with
auto_merge: null— both remain yours.Generated by Claude Code
fix(automation): refuse a wait node whose timerDuration yields no wait
fix(automation): require the wait / boundary_event config block at th…
fix(automation): refuse a wait node whose `timerDuration` yields no w…