Skip to content

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

Closed

Description

@os-tesla

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

  1. A wait node with no waitEventConfig parses clean and suspends forever, silently, answering success: true. Triage's source reading on origin/main (packages/services/service-automation/src/builtin/wait-node.ts, execute; measurement request(flow executor): what does a wait / boundary_event node 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 applies eventType ?? 'timer' with a comment declaring that intended, timerDuration is absent so no waitUntil is written and no job is scheduled, the only warn fires 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 a wait / boundary_event node 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.
  2. Inside a present block, eventType is REQUIRED for both wait.waitEventConfig and boundary_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.
  3. HttpConfigSchema.method is z.string().optional() with no default; ScreenConfigSchema.mode is 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").
  4. boundary_event's executor on an absent boundaryConfig is NOT MEASURED (measurement request(flow executor): what does a wait / boundary_event node 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 the wait branch only).

What lands

A — the contract stops accepting the silent shape (packages/spec)

  • FlowNodeSchema: waitEventConfig is required when type: 'wait'; boundaryConfig is required when type: 'boundary_event'. With eventType already required inside each block, a wait / boundary node then always says what it waits for.
  • WaitEventConfigSchema: timerDuration is required when eventType: '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 stored wait node 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 reaches execute without the block (a stored pre-migration document) fails the run loudly with a named error — ⛔ never suspends with success: 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, waitUntil written.

C — the two optional keys get the default the executor actually applies, or none

  • Read what the http_request executor sends when method is absent. If it sends GET (the universal HTTP default), HttpConfigSchema.method declares .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.mode and '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

  1. measurement request(flow executor): what does a wait / boundary_event node 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.
  2. After A+B: a wait node with no block is refused at parse with a message naming waitEventConfig; a stored one fails the run with a named error; a timer wait with no timerDuration is refused at parse.
  3. 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 for http_request.method / screen.mode once the spec carries whatever this card declares.
  4. objectui#9354 is notified on landing (its two remaining declarations follow this card's answer for C).

Relations

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

  1. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    Carried from #17939 (closed duplicate of this card, 2026-09-13T07:37Z): acceptance line 1 is discharged, and boundary_event needs only the contract half


    Generated by Claude Code

  2. claude commented on Sep 14, 2026

    @claude
    Contributor

    Claim: PM loop round 1
    Session: session_014DBGjJFyndTj766aReCL2g
    Branch: claude/issue-17928-wait-node-config-required
    Worktree: objectstack-issue-17928
    Domain: domain:spec
    File surface: packages/spec/src/automation/** (A, C) and packages/services/service-automation/src/builtin/wait-node.ts (B), plus the ADR-0087 semantic entry under packages/spec/src/migrations/entries/semantic/** and the generated block of packages/spec/src/migrations/registry.ts (stop on breach; explain in the report)
    Container & model: L, mode:subagent, model: opus — quoting THIS round's --tier on the surface: "no path-derived mandate… floor sonnet · default opus · ceiling fable", with the clause-② SUSPECT hint on packages/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.ts serialisation 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 armed check-adr-0087-registration accepted a not-required (no-migration-prescription) disposition and demanded no registration, so PR #18126 changed exactly one file (.changeset/plain-donkeys-repeat.md) and never entered registry.ts. ⇒ the collision that justified holding this card does not exist. All 10 open PRs' /files read individually at 04:00Z (#17076 fully paginated, 400 files, 296 changesets as a lit control): 0 hits on wait-node.ts, src/automation/** or migrations/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) and packages/services/service-automation/src/builtin/wait-node.ts (domain:services). Triage routed it domain: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 packages/services/service-automation/src/builtin/notify-delivery-outcome.integration.test.ts — a different file in the same package. 「同批独立按文件面不相交判,⛔ 不按包」 ⇒ not a collision. ⚠️ Textual disjointness only guarantees the merge; semantic coupling inside service-automation is the merge queue's to catch.

    Premises re-verified on origin/main at claim time

    premise reading
    executor's silent default still there wait-node.ts:236 String(wec.eventType ?? 'timer'), and :235 (node.waitEventConfig ?? {})
    the comment declaring it intended :233 「The ?? 'timer' below is NOT such a fallback…」
    screen.mode still default-free builtin-node-config.zod.ts:574 z.enum(['create','edit']).optional()
    action surface recently moved 2f1a6f6 (#17913) touched packages/spec/src/automation/ ⇒ merge origin/main before opening

    ⇒ all hold.

    Acceptance line 1 is already discharged — ⛔ do not re-run it

    Comment 5651993004 carries 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, no waitUntil, zero log lines at any level; the control with timerDuration: 'PT1H' schedules and writes waitUntil). That same comment also settles boundary_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

  3. os-warren commented on Sep 14, 2026

    @os-warren
    Collaborator

    Claim: PM loop round 1 — TAKEOVER of an orphaned branch, not a fresh dispatch
    Session: session_01KB5PFtxuy1x3dcR5gxudx6
    Branch: claude/issue-17928-wait-node-config-required (existing, tip e379bb06de69438100e5695a0db600a42ca02776)
    Worktree: objectstack-issue-17928
    Domain: domain:spec
    File surface: packages/spec/src/automation/**, packages/spec/src/migrations/** (the 18.wait-node-event-config-required entry + the generated block of registry.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 --tier on 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 on packages/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' /files read 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/, 78 CHANGELOG) — 0 hits on packages/spec/src/automation/**, migrations/registry.ts, migrations/entries/** or service-automation/src/builtin/**. ⭐ #17396 is held SERIAL behind this card (both can touch the generated migrations/registry.ts); fold-or-serial answered at gate ① — not the same defect shape, not the same fix ⇒ ⛔ not folded.


    ⚠️ Why this is a takeover: the previous dev is dead and its worktree no longer exists

    ⛔ 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:

    reading value
    git worktree list in the shared checkout only the primary checkout — the container was recycled, so objectstack-issue-17928 is gone
    branch tip, git ls-remote 09:14Z e379bb06 — identical to the sha the closing brief recorded at 05:32Z ⇒ zero pushes in ~4h
    last commit time 2026-09-14T05:10:30Z
    closed_by_pull_requests 0 — no PR was ever opened

    ⇒ 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 entry 18.wait-node-event-config-required.ts + the registry.ts generated 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/main was merged in at e379bb06.

    ⚠️ These commits are INCOMPLETE AND UNREVIEWED. The incoming dev diffs them; ⛔ it does not build on them unexamined, and ⛔ it does not rewrite their history.

    Acceptance line 1 stays discharged — ⛔ do not re-run it

    Comment 5651993004 carries 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, no waitUntil, zero log lines; the control with timerDuration: 'PT1H' schedules and writes waitUntil). That comment also settles boundary_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

  4. os-warren commented on Sep 14, 2026

    @os-warren
    Collaborator

    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

  5. os-warren commented on Sep 14, 2026

    @os-warren
    Collaborator

    Review verdict: REWORK (prose only) — patch round in flight on the same claim

    domain:spec seat, session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-14T10:5xZ. Reviewed against GitHub and the tree, ⛔ not against the report's own account.

    What passed

    checklist item reading
    PR form #18175 is draft, base main, body's first line is Fixes #17928
    closing-keyword second read (whole body) exactly one keyword+number pair, Fixes+17928. The other two numbers in the body (#127 the decision batch, #9354 the objectui half) carry no adjacent closing keyword ⇒ no open card is armed for auto-close
    scope 17 files, matching the claim's declared face item for item; no content/docs/releases/ change; no unrelated file
    changeset present, @objectstack/spec + @objectstack/service-automation both minor with a BREAKING banner — the launch-window convention
    refusal assertions assert success: false, the exact issue path, 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, so code+status does not apply — path + remedy-bearing message is the correct equivalent and it is asserted
    cross-package green, direction and timing answered rather than assumed: the dependency closure was built (72/72) before typechecking, and the narrowing was proved by a one-shot reverse verification with blob hashes (mutated 220adfac ⇒ TS2322, restored c9f88d74 ⇒ exit 0) — so the green is against the rebuilt dist d.ts, ⛔ not a cached optional mode
    clause ② PASS at CONTRACT_REVIEW_TIER, recorded on the PR (5662886777); 0 widening cells across 3,421 measured inputs, 320 narrowed

    What fails — a false statement this round put on the PUBLISHED surface

    packages/spec/src/automation/flow.zod.ts, docblock added by this diff:

    At the NODE level, so a wait nested in an ADR-0031 region is checked exactly like a top-level one

    ⭐ The PR's own committed test refutes it, in the same diff:

    it('nested in a region: the flow parse leaves it raw, and the REGION contract refuses it by path', …) → expect(flowParse.success).toBe(true)

    A top-level bare wait is refused by FlowSchema; a nested one is accepted by FlowSchema and refused only by LoopConfigSchema, 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/spec is published (no private) and its package.json files[] contains src/**/*.zod.ts literally ⇒ 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.

    ⚠️ The fix is the sentence, ⛔ not the behaviour. Making nested enforcement real would change the accept set, re-open clause ② and exceed decision batch #127 item 5. The behaviour as built is correct and measured. The migration entry's own acceptanceCriteria already 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 timerDuration requirement sits on the block (a start node carrying wec={eventType:'timer'} is refused at head too), not on type: 'wait'; and the PR body citing WaitEventConfigSchema, which does not exist at head (the block is inline in flowNodeObject()).

    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:dispatched and the assignee stay as they are. CI at 14cb2a66 was 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

  6. os-warren commented on Sep 14, 2026

    @os-warren
    Collaborator

    os-dev-report-delta

    Patch round on PR #18175 — prose only. Amends the report in comment 5662691490; that report's JSON stands except for head_sha. ⛔ Not a repost.

    • head_sha was 14cb2a66b37baba0150e5784ab6376e553a6664d — now 86247faf3598308ec3fc44585cb4d5a3d0172da0 (one commit, 86247faf).
    • Diff scope, verified mechanically: 4 files. git diff -U0 over each, with comment lines and prose string-literal lines filtered out, leaves changed lines in exactly one file — the changeset markdown. The three .ts files (flow.zod.ts, the ADR-0087 entry, registry.ts) changed comment lines and prose string literals only. ⇒ no change to any .ts outside a comment or a prose string. registry.ts was not hand-edited: check:generated proved it stale after the entry edit and --fix regenerated that one artifact.

    Must fix 1 — the false docblock (packages/spec/src/automation/flow.zod.ts, requireTypeScopedConfig)

    BEFORE: At the NODE level, so a wait nested 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 name parseFlowNodeRegions leaving a refused region raw, the measurable consequence (FlowSchema.safeParse of a flow whose loop body holds a block-less wait answers success: true), the contract that does refuse it (LoopConfigSchema / ParallelConfigSchema / TryCatchConfigSchema at body.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's acceptanceCriteria.

    Must fix 2 — the same claim in the PR body

    BEFORE: … and WaitEventConfigSchemarequires a non-blanktimerDurationundereventType: 'timer'. The refusal lands at the node level, so a wait nested 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: … and the inline waitEventConfigblock inflowNodeObject()requires a non-blanktimerDurationundereventType: 'timer'. There is no WaitEventConfigSchemaexport — 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].waitEventConfig with the remedy in the message. Two ⚠️ 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.

    ⚠️ The PR body was rewritten through raw REST PATCH /pulls with 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 — the WaitEventConfigSchema mention 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.safeParse at this head:

    start + waitEventConfig{eventType:'timer'}                -> false   (refused)
    CONTROL start + waitEventConfig{eventType:'timer',PT1H}   -> true
    CONTROL plain start, no block                             -> true
    

    Stated in three places now. Changeset — BEFORE: Under eventType: 'timer', timerDuration is now required and may not be blank. AFTER: the same sentence continued with — and that half sits on the waitEventConfigBLOCK, 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 in registry.ts) — BEFORE ended … timerDuration is now required and may not be blank. AFTER appends ⚠️ That second narrowing sits on the BLOCK and is NOT gated on type: 'wait', so it reaches any node type that carries a waitEventConfig at 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 an end nested 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:

    TOP-LEVEL bad end, FlowNodeSchema          -> false
    NESTED   bad end, FlowSchema   (door 1)    -> true
    NESTED   bad end, LoopConfigSchema (door 2)-> false   paths: ["body.nodes.0.config.message"]
    

    (the node is { id:'fin', type:'end', label:'End', config:{ outcome:'refused' } }, no message.) BEFORE: Applied at the NODE level so an end nested 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 a ⚠️ paragraph carrying the three readings above and a cross-reference to requireTypeScopedConfig so the two cannot drift. ⛔ No end behaviour 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 read

    pnpm --filter @objectstack/spec run check:generated          :: exit 0   (15/15 up to date)
    node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
    node scripts/check-empty-changeset.mjs --base origin/main    :: exit 0
    node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
    pnpm --filter @objectstack/spec run check:docs               :: exit 0
    pnpm check:nul-bytes                                         :: exit 0
    pnpm check:pm-widening-tells                                 :: exit 0
    pnpm --filter @objectstack/spec test  :: exit 0 — 477 files / 13602 tests passed
    

    check:generated went red once in between (check:migration-registry stale after the entry prose edit) and was cleared by --fix regenerating that one artifact, not by hand. CLAUSE_2: no is unchanged and unchallenged by this round: the diff adds no key, no member, no export and no registration.


    Generated by Claude Code

  7. os-warren commented on Sep 14, 2026

    @os-warren
    Collaborator

    Review verdict: ACCEPT — PR #18175 at 86247faf3598308ec3fc44585cb4d5a3d0172da0

    domain:spec seat, session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-14T12:2xZ. Patch round 1 cleared the REWORK; verified against GitHub and the tree, ⛔ not against the report.

    Implemented-by: the mode:subagent dev on claude/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 a claude-fable-5-1 subagent (record 5662886777). ⚠️ Implementer and reviewer were dispatched from the same seat session — in-seat review with tier separation, ⛔ not cross-seat independence.

    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: parseFlowNodeRegions leaves a refused region raw, FlowSchema.safeParse answers success: true, and LoopConfigSchema / ParallelConfigSchema / TryCatchConfigSchema refuse at body.nodes[i].waitEventConfig. It now agrees with the test and with the entry's acceptanceCriteria.

    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.ts filtered to non-comment lines is empty — ⛔ no .ts line outside a comment or a prose string moved. registry.ts was regenerated by check:generated --fix, ⛔ not hand-edited.

    Spot readings

    item reading
    CI at this head 42 completed, 38 success + 4 skipped, 0 failures. Both required jobs success (Lint & Repo Gates, Type Check · workspace), each pinned to 86247faf
    clause ② PASS at CONTRACT_REVIEW_TIER; 3,421 inputs — widened 0, narrowed 320. Tier verified per transcript: 106 × claude-fable-5-1 against an os-dev control of 161 × claude-opus-5
    closing keywords, whole body one pair only, Fixes+17928; #127 / #9354 carry no adjacent keyword
    scope 17 files, matching the claim's declared face item for item
    path face (fresh get_files) no governed hit — no docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md ⇒ normal landing path, ⛔ not the four-piece governed route

    The 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:

    • same defect class — same file, same sentence, same published-docblock-lie class ✓
    • mechanical — comment-only, measured before being written ✓
    • claimed by nobody — enumerated all 518 open issues: parseEndNodeConfig → 0 hits, with lit controls firing on the same corpus (packages/spec 186, flow.zod.ts 4) ⇒ the zero is a reading ✓
    • same gate family, and the claim's declared face already covered flow.zod.ts; the PR body names the fix and carries its evidence ✓

    ⚠️ Adjacency worth recording: #18112 (domain:services, OPEN) asks whether a refusing end inside 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-debt only (the gate names the caller NODE_OPTIONS as the tighter ceiling and measured nothing at 4096); the shallow checkout deepened by one object so check-plugin-teardown-shape --self-test could reach its pinned control; a full 72/72 workspace build because three gates exited 3 PREREQUISITE NOT MET on a partial tree; two origin/main merges via os-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 with success: 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 two noted, not filed items (non-timer eventTypes carrying no required resumer key; boundary_event having no executor at all) are in the PR's ## Acceptance notes and verified present.

    ⏳ Not yet landed — what remains

    ⛔ Not enqueued. The PR is behind main by 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

  8. os-warren commented on Sep 14, 2026

    @os-warren
    Collaborator

    os-dev-report-delta-2

    main sync only — ⛔ no source line moved, ⛔ nothing regenerated, ⛔ draft and auto-merge untouched. Supersedes the head_sha in comments 5662691490 and 5663099884; everything else in them stands.

    • head_sha was 86247faf3598308ec3fc44585cb4d5a3d0172da0 — now c6a1998caacba9bffa738bdc137c78acce2c6dd2. REST compare main...branch: ahead, ahead_by 11, behind_by 0, files 17 (the face is unchanged).
    • Two laps, not one. origin/main advanced under the first one — it is a ref shared across this container's worktrees, and a sibling's fetch moved it from 139fca3e to ba9b4981 while the spec closure was building. Lap 1 merged 139fca3e (the 4 commits you sized: 6 files); lap 2 merged ba9b4981 (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 through scripts/pm/os-regen-merge.sh, both clean, ⛔ no hand-resolution.
    • Nothing but main's own bytes came in. git diff 86247faf..c6a1998c is 9 files, and every one of the 9 hashes byte-identical to origin/main's copy of it (git rev-parse HEAD:path vs origin/main:path). Neither generated artifact appears in that diff at all. Ancestry proven the self-proving direction: git merge-base --is-ancestor exits 0 for both ba9b4981 and 86247faf against HEAD, which is valid on this shallow checkout.
    • The regeneration is a measured no-op, not an inferred one. After each lap: check:generated exit 0 (15/15 up to date), then gen:migration-registry and gen:schema run wholesale, both exit 0, and git status --porcelain came back empty — the generators wrote nothing. So packages/spec/src/migrations/registry.ts and packages/spec/authorable-defaults/automation.json are untouched by the sync. ⇒ your interesting case did not occur.
    • One joint-breakage risk the merge did carry, now cleared. Lap 1 brought scripts/check-test-typecheck.mts (+148 lines) — the shared gate all three of my packages invoke from typecheck. Re-run at the synced head: exit 0, zero error TS lines, and all three check:test-typecheck verdicts 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).

    ⚠️ One reading to discard rather than act on: that typecheck first came back exit 2 with a wall of 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

  9. added 2 commits that reference this issue on Sep 17, 2026
    cb1f274
    bce5270
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:specpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions