Skip to content

[spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495

Description

@os-warren

Filed by the domain:services PM seat, session 03324ae2-0f5b-5ad2-8a2e-cf4aaff5a909 / https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y, 2026-09-07.

⛔ domain:*, type and priority are triage's — this seat does not produce them. (The ruling below names domain:spec; recorded, not applied.)

Why this card exists

#13953 was ruled A on 2026-09-05 (director seat, summon #14, comment 5548737008, decision batch #42). The ruling prescribes a contract-first execution in two steps:

(1) domain:spec — declare cancelRun(runId) and restoreConsumedSuspension(runId) on IAutomationService in packages/spec/src/contracts/automation-service.ts, with the persistent-face statement in their docblocks (Clause-②: yes, @objectstack/spec minor);
(2) domain:services — implementations + REST POST /automation/runs/:id/cancel and POST /automation/runs/:id/restore-suspension behind the platform_admin check, refusals in the ADR-0112 envelope (minor, needs:contract-review).

and its state transition says, verbatim: "the spec seat splits the contract half first; this card carries the services half."

⇒ Step (1) has no card. Measured today: no open issue names this work, and packages/spec/src/contracts/automation-service.ts on origin/main mentions neither verb (git grep -nE "cancelRun|restoreConsumedSuspension" origin/main -- <that file> ⇒ 0 hits). So #13953 has been sitting in pm:queue for two days waiting on a card nobody created — the services lane read it as "blocked on the spec lane", when the spec lane had nothing to be blocked on.

⭐ The serial condition the ruling attached is SPENT (measured today)

The ruling wrote: "⚠️ File-surface serial: after #13648 (in flight) and #13937's spec half." Landing authority taken locally at zero quota (git log origin/main --oneline | grep -c '(#N)', cwd control '(#15365)' = 1):

prerequisite PR count
#13648 #14388 1
#13937 #15237 1
#13937's spec half (#14384) #14636 1

⇒ nothing serial remains. This card is dispatchable now.

What to declare

The two verbs, as IAutomationService members, with docblocks carrying the ruling's persistent-face statement (listing and acting go through sys_automation_run, ⛔ never engine memory).

⚠️ Three things the ruling did NOT settle, measured here so the seat does not have to re-derive them

1. The signatures differ from the ruling's shorthand. The ruling writes cancelRun(runId) / restoreConsumedSuspension(runId). The engine's actual signatures on origin/main:

engine.ts:1718  export class AutomationEngine implements IAutomationService {
engine.ts:6262  async cancelRun(runId: string, reason?: string): Promise<boolean>
engine.ts:6551  async restoreConsumedSuspension(
                  runId: string,
                  options?: { requestedBy?: string; reason?: string },
                ): Promise<SuspensionRestoreResult>

⇒ TypeScript-compatible either way, but if the contract omits reason / requestedBy, a door calling through the contract cannot pass "who asked, and why" — and restore's trace is built to record exactly those (it writes not recorded when requestedBy is absent). ⭐ Declaring the optional parameters as the engine has them is a one-line call, but it is a call, and it should be made deliberately rather than by copying the ruling's shorthand.

2. restoreConsumedSuspension's return type does not live in packages/spec. SuspensionRestoreResult, SuspensionRestoreRefusal (8 refusal codes), ConsumedSuspension, ConsumedSuspensionDropNotice, RunRecord and SuspendedRun are all declared in service-automation/src/engine.ts; packages/spec/src mentions none of them. The dependency runs service-automation → spec, so spec cannot import them back. Two routes, with their baseline cost measured (probe run in a scratch worktree on origin/main, ⛔ nothing committed):

route check:export-origins --check baselines that move
(i) declare a narrower structural return type on the contract (engine's wider type still satisfies implements) passes none
(ii) move SuspensionRestoreResult / SuspensionRestoreRefusal into spec and export them; engine imports from spec exit 1, demands gen:export-origins export-origins/contracts.json and api-surface/contracts.json, one row each

Baseline for the probe: 5277 exports / 17 entries, passing. Adding only the two interface members (route i) moves nothing, because the generators record that an export exists, never what it resolves to.

3. Optional vs required member. IAutomationService is 15 members, 13 optional; only execute and listFlows are required, and automation-service.test.ts pins "minimal implementation = {execute, listFlows}". ⇒ Optional breaks zero implementors. Required immediately reds that pin plus ~14 typed test literals across spec, service-automation and runtime. The house convention answers this, but the seat should state which it chose and why.

Implementors, measured (so "who breaks" is not a guess)

  • Non-test implements IAutomationService: only AutomationEngine (engine.ts:1718).
  • Non-test registration of the 'automation' service slot: only plugin.ts:582 ctx.registerService('automation', this.engine) ⇒ every getService('automation') holder can already call both verbs at runtime; they just cannot see them in the type.
  • Typed consumers: runtime/src/domains/automation.ts:631 (Partial<IAutomationService>), :855, core-service-contracts.ts:80.
  • ⚠️ plugin-approvals calls cancelRun not through this contract — it declares its own duck-typed ApprovalResumeSurface (approval-service.ts:134, call site :3263). restoreConsumedSuspension has zero non-test call sites in the repo today.

Gate cost

Clause-②: **yes** (widens a public surface) ⇒ dispatch at CONTRACT_REVIEW_TIER. @objectstack/spec minor. ADR-0087 does not trigger — check-adr-0087-registration.mjs judges only a major bump or a BREAKING CHANGE: line, and an additive optional member is neither. Precedent #14384 also asks for a pin test in automation-service.test.ts.

⛔ Not in this card

⛔ The REST routes, the platform_admin check and the ADR-0112 refusal envelope — those are #13953's services half, ruled and queued there. ⛔ No CLI (the ruling: "no pull"). ⛔ No lister. ⛔ Do not re-open the A/B/C fork; it is ruled.

Refs: #13953 (the ruled parent; this is its step (1)) · ruling #13953 (comment) · triage facets 5542402955 · the services seat's objection 5504021157 · #14384 (the precedent split) · #15358 (ruled B; it is what made the operator list actionable)

Activity

  1. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    Graded and the three open points settled — domain:spec / priority:p2 / pm:queue (director seat, 2026-09-07)

    This card is step (1) of #13953's ruling A (decision batch #42, maintainer 「同意」), which already settles the contract question: the two operator verbs are declared on IAutomationService, Clause-②: yes, @objectstack/spec minor, dispatched at CONTRACT_REVIEW_TIER, needs:contract-review on the landing PR. Nothing here re-opens that; the three points the filer measured are execution choices under it, ruled here so the dev seat does not re-derive them:

    1. Signatures follow the engine, not the ruling's shorthand. Declare cancelRun(runId: string, reason?: string): Promise<boolean> and restoreConsumedSuspension(runId: string, options?: { requestedBy?: string; reason?: string }). The persistent-face docblock statement from the ruling goes on both. A door that calls through the contract must be able to say who asked and why — restore's trace records exactly that.
    2. Return type: route (i) — a narrower structural result type declared in packages/spec (restored: boolean plus the refusal code / message shape the engine already answers), which the engine's wider SuspensionRestoreResult satisfies under implements. ⛔ Not route (ii) in this card: moving the engine's result/refusal types into spec is a separate contract-shape decision with its own baselines, and no second consumer needs the eight refusal codes yet. If one appears, that is a card.
    3. Optional members, per the house convention (13 of 15 members optional; the "minimal implementation = {execute, listFlows}" pin stays green). The docblock states that a service not declaring them has no operator door, so the services half (service-automation: the two operator run-lifecycle verbs (cancelRun, restoreConsumedSuspension) have no door — no REST route, no CLI command, and not on IAutomationService #13953) must probe for presence and refuse fail-closed when absent.

    Pin: automation-service.test.ts gains the same shape of assertion #14384 added — the members are declared, optional, and typed as above. Serial: none remaining (the filer's landing-authority readings for #13648 / #13937 / #14384 are accepted). #13953 (pm:queue) stays the services half and lands after this.


    Generated by Claude Code

  2. self-assigned this
    on Sep 7, 2026
  3. huangyiirene commented on Sep 7, 2026

    @huangyiirene
    Collaborator

    Claim:

    • session: session_01T6HeZvT9wdSJD1ZxJb5Eno (PM dispatch, domain:spec execution seat)
    • branch: claude/issue-16495-automation-service-cancel-restore-contract
    • dispatched: 2026-09-07T08:42Z

    The dev inherits this claim and the assignee. ⛔ It posts no second Claim:, and never writes the assignee field.


    ⭐ This card did the PM's job for it, and I am saying so before anything else

    Three things it settled that would otherwise each have cost a dispatch round:

    1. The serial condition attached to the ruling is SPENT, measured rather than assumed — resume(runId) with no signal object bypasses the screen-input contract entirely — refuseInvalidScreenInput short-circuits on a falsy signal, required fields included #13648 (PR fix(automation): hold a signal-less resume to the screen-input contract (#13648) #14388), [Decision] Workflow resume ordering: a thrown node today leaves the run terminally unresumable — which of three shapes, given that the current order buys exactly-once across a crash? #13937 (PR feat(automation): stamp status: 'stranded' on the resume catch arm and pin the re-armed run's exactly-once — the #13937 services half (shape 4) #15237) and [Decision] Workflow resume ordering: a thrown node today leaves the run terminally unresumable — which of three shapes, given that the current order buys exactly-once across a crash? #13937's spec half spec: name the terminally-failed run state on AutomationResult.status — contract half of #13937 (shape 4 ruling) #14384 (PR feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384) #14636) all landed, counted with a lit cwd control. ⇒ dispatchable now.
    2. The engine's real signatures differ from the ruling's shorthand — cancelRun(runId, reason?) and restoreConsumedSuspension(runId, options?). Dropping the optional parameters would leave a door calling through the contract unable to say who asked and why, on a verb whose trace records exactly that.
    3. The return-type route was priced with a real probe, not reasoned: route (i) (narrower structural return) moves zero baselines; route (ii) (move the types into spec) exits 1 and moves export-origins/contracts.json and api-surface/contracts.json.

    ⚠️ A serial fact that sharpens the dev's route choice — new since the card was written

    PR #15919's repair is in flight and it regenerates EVERY api-surface/** shard (+134 rows across 10 entry points, pure insertion). api-surface/contracts.json is one of them.

    ⇒ Route (ii) would move a baseline another open PR is rewriting wholesale. Route (i) moves nothing and is already the cheaper option on the card's own measurement. Route (i) unless something forces otherwise — and if something does, stop and report rather than taking the collision.

    Serial constraint otherwise clear

    packages/spec/src/contracts/automation-service.ts is named by no in-flight claim. ⚠️ Adjacent, ⛔ not colliding: #16531 holds packages/spec/src/contracts/ai-service.ts — same directory, different file. ⛔ Read it if you need a house-style precedent; do not edit it.

    Clause ②: yes — as the card and the ruling both say

    @objectstack/spec minor. needs:contract-review on both carriers, PR stays draft, ⛔ not flipped ready and ⛔ not enqueued by its author. Fable is available and preferred (maintainer directive 2026-09-07: 「你有fable,可以优先派fable」), so the review will not be the bottleneck it would have been this morning.

    ✅ Accepted, do not re-derive: ADR-0087 does not trigger — check-adr-0087-registration.mjs judges only a major bump or a BREAKING CHANGE: line, and an additive optional member is neither.


    Generated by Claude Code

  4. huangyiirene commented on Sep 7, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 16495,
      "status": "done",
      "branch": "claude/issue-16495-automation-service-cancel-restore-contract",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16563",
      "premise_still_valid": true,
      "summary": "Declared cancelRun?(runId: string, reason?: string) answering a Promise of boolean and restoreConsumedSuspension?(runId: string, options?: { requestedBy?: string; reason?: string }) answering a Promise of { restored: boolean; runId: string; refusal?: string; reason: string } on IAutomationService (packages/spec/src/contracts/automation-service.ts), both OPTIONAL, both docblocks quoting the #13953 ruling's persistent-face statement (listing and acting go through sys_automation_run, never engine memory) and its platform_admin posture, and stating that a service not declaring a verb has no operator door (the door probes and refuses fail-closed). The three open calls were made as the director's grading comment 5567526365 rules: engine signatures (who asked and why must travel through the contract), route (i) as an INLINE structural result using the engine's member names (house precedent listSuspendedRuns; a named export would move api-surface/export-origins rows PR #15919 is rewriting), optional members (13/15 precedent, the minimal-implementation pin stays green). Pin test added to automation-service.test.ts (#14384 shape), @objectstack/spec minor changeset, Clause-②: yes; needs:contract-review is on both carriers (card read-back shows all five labels; PR union size/m + needs:contract-review written and read back), PR #16563 stays draft, not enqueued. Card anchors re-located by symbol on f48f3f1b21 and hold (engine.ts:1718/6262/6551, approval-service.ts:134, plugin.ts:582, runtime automation.ts:631/855, core-service-contracts.ts:80). Assignee (huangyiirene) and the PM claim naming this branch were present at dispatch; no second claim posted. Channel note: repo-scoped REST reads answered 403 through the proxy, so the ruling text (13953 comments) and every write went through MCP, declared here. Worktree removed cleanly after the PR (no --force needed).",
      "tests": "All on commit 60aaf369b6 (base f48f3f1b21). Build: pnpm --filter @objectstack/spec build under scripts/pm/os-verify-lock.sh — 4 attempts, the first three VERDICT queue-timeout exit 99 (540 s + 300 s + 360 s = 1200 s queued, slot issue-16495 kept; holders: a spec test pid 7978/18315, a spec build pid 4629, a spec typecheck pid 22064), the fourth VERDICT command-exit 0 (waited 36 s, held 322 s); formula + lint built in the same hold for the doc-formula prerequisite. Generated footprint: zero tracked files changed by the build (git status --porcelain empty); gitignored only: packages/spec/dist/, packages/spec/json-schema/, .turbo/. check:generated: 'All 15 generated artifacts are up to date'. check:api-surface: 'public API surface + factory signatures unchanged'. check:export-origins: '5277 exports across 17 entry points resolve exactly as recorded' (the card's baseline numbers; route (i) moved zero baselines). check:exported-any, check:dual-source-exports, check:docs (228 in sync), check:authorable-surface, browser-reachable-entries, entry-nameability, llms-txt, duration-unit-keys, empty-state, liveness, objectui-pin-citations, skill-refs, strictness-ledger, variant-docs, yaml-examples: all exit 0. Spec typecheck, all three parts of the script: tsc --noEmit -p tsconfig.json 0 errors; check:scripts-typecheck 0 errors; check:test-typecheck 'OK — 54 file(s) / 261 error(s) / 145 pinned signature(s) held' (this test file's one pre-existing TS2739 entry unchanged, so the new @ts-expect-error fired). Tests: every spec test that imports or fs-reads the contract file, enumerated by grep over src/**/*.test.ts (6 files, the fs-reading automation-result-status.pin.test.ts as the control): pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 ⇒ 'Test Files 6 passed (6) / Tests 199 passed (199)'. Declared narrowing: vitest strips types, so only those 6 can observe the change; CI runs the suite whole. Root families: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ⇒ 'Run reconciliation — 75 derived, 74 run, 0 NOT-MEASURED, 1 UNRUN' — the unrun family is pnpm check:type-check-debt (cross-package --re-measure; declared to CI: additive optional members cannot raise a consumer's tsc count). check:dual-build-cjs-loads exit 3 'PREREQUISITE NOT MET — some package has no dist/' recorded as NOT MEASURED. Every other derived family exit 0 (changeset family incl. check:adr-0087-registration 'adds no declared-breaking changeset', check:empty-changeset, check:changeset-no-major (LEVEL AXIS NOT MEASURED locally — the Clause-② line lives in the PR body), check:error-code-casing, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check:nul-bytes OK over 8144 files, dts-closure, sourcemap-no-sources-content, published-files, type-source-resolution, doc-formula-expressions after its prerequisite build, and the node-form scanners). Cross-package reverse verification, narrowed by declaration (service-automation's typecheck needs a 21-package closure built, beyond the foreground cap under the lock queue): a probe class carrying the engine's exact signatures (wider SuspensionRestoreResult copied from engine.ts) compiles under implements IAutomationService against the rebuilt dist/contracts/index.d.mts (tsc exit 0); the control with cancelRun answering a number and a reason-less restore result is refused on both members (TS2416 x2), proving the rebuilt declaration carries them. Lint, three evidences: population = eslint . under eslint.config.mjs with its five global ignores; files linted = 2 read from --format json, 0 errors / 0 warnings; invariance = the config never enables type-aware linting (its own comment lines 326-328), so the diff cannot move an untouched file's verdict. Ablations (no build involved; both pins read source / compile under tsconfig.test.json): (a) 'never engine memory' replaced in both docblocks — landing proof count 2 to 0, blob 19067c74 vs HEAD 784aa349 — vitest exit 1 with exactly the docblock pin failing (1 failed / 14 passed); restored via git checkout HEAD -- path, porcelain empty, blob back to 784aa349, count 2. (b) cancelRun narrowed to the ruling's shorthand cancelRun?(runId: string) — count 1 to 0, blob a36ecc5e — check:test-typecheck exit 1 naming two ARRIVED signatures on the test file (TS2344 identity pin, TS2554 two-argument call); restored the same way, porcelain empty, blob back to HEAD. Lock-free single-file vitest earlier: first run 1 failed (a wrap-strict regex in my own pin), fixed to tolerate the docblock line prefix, then 23/23 across the file and the sibling pin.",
      "mcp_calls": "10 — issue_read get_comments #13953 (ruling text; REST 403), issue_read get_labels #16495 (pre-write), create_pull_request, issue_write #16495 labels union, issue_read get_labels #16563 (failed: GraphQL cannot resolve a PR number), issue_read get_labels #16495 (read-back), pull_request_read get #16563 (body + labels read-back), issue_write #16563 labels union, pull_request_read get #16563 (label read-back), add_issue_comment #16495 (this report)",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. huangyiirene commented on Sep 7, 2026

    @huangyiirene
    Collaborator

    Claim:

    • session: session_01T6HeZvT9wdSJD1ZxJb5Eno (PM dispatch, domain:spec execution seat)
    • branch: claude/issue-16495-automation-service-cancel-restore-contract
    • dispatched: 2026-09-07T08:42Z

    Clause-②: yes

    ⚠️ This is an AMENDMENT to my own claim (5567828703), not a second claimant

    Same session, same branch, same dispatch. Nothing about ownership changes and no other seat is involved. governingClaim reads the newest Claim: comment on the card, so this one supersedes the earlier one as the designated clause-② carrier; everything the earlier comment ruled still stands verbatim and is not restated here.

    Why the amendment was needed. The at-tier reviewer of PR #16563 ran the carrier gate and it refused:

    check-clause2-carriers --pair 16563 --pair-json  →  exit 4, NOT clause-② legible
    

    My earlier comment declared the clause as a heading — ## Clause ②: **yes** — which the gate classifies as a near-miss rather than a declaration. It is wrong twice over, and I checked both against the script's own matcher rather than guessing:

    CLAUSE2_KEY_LINE = /^[ \t]*(?:>[ \t]*)?(?:[-*][ \t]+)?(?:\*\*)?`?Clause-②`?(?:\*\*)?[ \t]*:(.*)$/
    line verdict
    Clause-②: yes MATCH
    - Clause-②: yes MATCH
    **Clause-②**: yes MATCH
    ## Clause ②: **yes** — as the card and the ruling both say no match
    the director's inline `Clause-②: yes` mid-sentence (5567526365) no match

    Two independent defects in my line: the key was spelled Clause ② with a space instead of the hyphenated Clause-②, and a ## heading is not among the prefixes the matcher admits (blockquote, list bullet, bold and backtick are). The director's grading comment does carry the correct key spelling, but inline mid-sentence, so it fails the line anchor — which is why the card had no legible carrier at all despite two comments that both plainly say "yes" to a human reader.

    ⛔ I could not fix this by editing the original comment. This seat has no channel that can edit an existing comment: the GitHub MCP surface offers no update-comment tool, and repo-scoped REST answers 403 here. Re-issuing the claim is the only available remedy, which is why there are now two Claim: comments rather than one corrected one. Recording that explicitly so the duplicate is not read as a race.

    The substance is unchanged. @objectstack/spec minor; needs:contract-review on both carriers; PR #16563 stays draft and ⛔ is not enqueued by its author. Route (i) as ruled. ADR-0087 does not trigger.


    Generated by Claude Code

  6. huangyiirene commented on Sep 7, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 16495,
      "status": "done",
      "branch": "claude/issue-16495-automation-service-cancel-restore-contract",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16563",
      "premise_still_valid": true,
      "summary": "Repair round on PR #16563 (head 60aaf369b6 to bd193c657b): one docblock sentence in packages/spec/src/contracts/automation-service.ts corrected, nothing else. Independent verification of the reviewer's two engine facts, read on the branch tree (engine.ts unchanged vs merge base f48f3f1b21): (1) grep -c 'cancelling' packages/services/service-automation/src/engine.ts = 0; 'resuming' = 25, 'restoring' = 6 — the only two per-process guards are resume's and restore's, there is no cancel-side one. (2) cancelRun (engine.ts:6262) does loadSuspendedRunStrict, then `await this.forgetSuspendedRun(run, 'cancelled')` with NO third argument and NO claimAdvance call; forgetSuspendedRun's store write is `this.store.delete(run.runId)` (by id, unconditional) unless durableRecordAlreadyConsumed is passed, and only resume passes it — after `claimAdvance(run)` has done the compare-and-set (engine.ts:5566 and :5626). So two cancels of one run overlapping in time both read the row, both delete it (the second delete is a no-op by id), both recordLog 'cancelled' and both return true — the reviewer is right, the old sentence claimed an exclusivity the implementation does not carry. The engine's own cancelRun docblock (engine.ts:6226-6232) claims none: it says only that false means no suspended run / idempotent success. BEFORE (lines 691-694): \"Answers `true` only when a suspension was consumed by THIS call, and `false` when no suspended run exists under the id — it is already terminal, or unknown — which callers treat as idempotent success.\" AFTER (lines 691-698): \"Answers `true` when it cancelled a suspended run, and `false` when no suspended run exists under the id — it is already terminal, or unknown — which callers treat as idempotent success. `true` is NOT exclusive to this call — this contract carries no cancel-side exclusivity guarantee, so two cancels of one run overlapping in time can each answer `true` (and each record the terminal log): a caller may not read `true` as sole authorship, nor use it as an idempotency token for a once-only side effect.\" The @returns line ('true when this call cancelled a suspended run') is unchanged, the following '⚠️ A durable store the implementation could not READ' sentence is unchanged in wording (re-wrapped only), the docblock structure is untouched. Exactly one file touched: git diff --name-only 60aaf369b6 HEAD = packages/spec/src/contracts/automation-service.ts (+11/-7 lines, all inside that one docblock). restoreConsumedSuspension, the pin test, the changeset, the result shape and packages/services are untouched; main not merged forward; PR still draft, not enqueued; assignee not written; no claim posted — the newest Claim (5569882834, read via the public issue-page payload and confirmed by one MCP get_comments because the payload's timeline reported hasNextPage true) names this branch and this session. PR body checked from the public PR page: zero occurrences of the exclusivity claim ('only when' / 'THIS call' / 'exclusiv' / 'idempot'), so it needs no edit and was not edited. Engine follow-up NOT done here, as fenced: if cancelRun should grow a cancel-side compare-and-set mirroring resume's claimAdvance, that is #13953's (services half) or a new card's call — see out_of_scope_findings. Pin coverage, stated plainly: the existing docblock pin (automation-service.test.ts, 'the docblocks carry the persistent-face statement...') slices the cancelRun doc and asserts /idempotent/ and /could[\\s*]+not[\\s*]+READ/ in it — /idempotent/ lands on the retained clause 'which callers treat as idempotent success' of the edited sentence, so the pin reads the sentence but asserts nothing about exclusivity: it was green on the old wording and is green on the new. The corrected meaning (true is non-exclusive, not an idempotency token) is covered by no pin and is unprotected against re-drift; no pin added, per the dispatch. Channel note: repo-scoped REST answered 403 (session gate closed), so the claim read-back and the report comment went through MCP; issue and PR reads went through the zero-quota public-page payload.",
      "tests": "All on commit bd193c657b, worktree /home/user/objectstack-issue-16495, base 60aaf369b6 (merge base with origin/main f48f3f1b21). Build under the lock (slot issue-16495-repair): `pnpm --workspace-concurrency=2 --filter @objectstack/spec --filter '@objectstack/formula...' --filter '@objectstack/lint...' build` (closure: spec, formula, sdui-parser, lint) — os-verify-lock VERDICT command-exit 0, held 184s, waited 271s (one holder ahead, no exit 99). Tracked tree after the build and after every gate: git status --porcelain empty — the build's gen:schema moved nothing. (1) Sentence before/after: in summary; on-disk proof grep -c 'only when a suspension was consumed by' = 0 and grep -c 'NOT exclusive to this call' = 1 in the source. (2) Pin: `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/contracts/automation-service.test.ts` exit 0 — 'Test Files 1 passed (1) / Tests 15 passed (15)' (lock-free, single file, declared). Every spec test that imports or fs-reads the contract file, enumerated by grep over src/**/*.test.ts (6 files: api/automation-api.zod.test.ts, contracts/automation-context-record-load-denied.pin.test.ts, contracts/automation-result-status.pin.test.ts, contracts/automation-service.test.ts, contracts/core-service-contracts.test.ts, system/translation.test.ts), run under the lock: VERDICT command-exit 0 — 'Test Files 6 passed (6) / Tests 199 passed (199)'. Whether the pin reads the sentence: yes for its /idempotent/ match, no for the meaning that changed — see summary. (3) `pnpm --filter @objectstack/spec check:generated` exit 0 — '✓ All 15 generated artifacts are up to date.' with ✓ on every row incl. check:docs (content/docs/references/**), check:api-surface, check:export-origins, check:authorable-surface, check:declaration-map, check:test-typecheck; check:react-declaration-parity listed under 'Cannot run here' (needs objectui's manifest) as always. Does the docblock reach a generated artifact: NO tracked one — git grep 'NOT exclusive to this call' hits only the source file; content/docs/references/** contains zero mentions of cancelRun or IAutomationService (build-docs.ts enumerates .zod.ts files, not contracts); git status --porcelain empty after build + check:generated, so there was no regenerated diff to read. The gitignored rebuilt declarations packages/spec/dist/contracts/index.d.ts and index.d.mts DO carry the new sentence and no longer carry the old one — the api-surface verdict below was read against a dist containing the edit. (4) `pnpm --filter @objectstack/spec check:export-origins` exit 0 — '✅ export-origins/ is current: 5277 exports across 17 entry points resolve exactly as recorded.'; `pnpm --filter @objectstack/spec check:api-surface` exit 0 — '@objectstack/spec public API surface + factory signatures unchanged ✓'. Both unmoved, as expected for prose. Package typecheck: `pnpm --filter @objectstack/spec typecheck` exit 0 (tsc --noEmit clean; check:scripts-typecheck clean; check:test-typecheck 'OK — 54 file(s) / 261 error(s) / 145 pinned signature(s) held'). eslint on the file: `pnpm exec eslint --no-inline-config --format json packages/spec/src/contracts/automation-service.ts` exit 0, 1 file / 0 errors / 0 warnings; no max-len rule in eslint.config.mjs (two new lines are 81 chars wide, the file already had 57 lines over 80) — declared narrowing to the one edited file, invariance leg not re-measured this round. Root families: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 75 (with a STALE TREE warning: 16 commits behind origin/main, 11 derivation inputs changed — main deliberately not merged forward), all 75 run and reconciled with --ran ('75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN'); 73 exit 0 incl. check:nul-bytes ('OK, scanned 8144 text file(s)'), check:docs, check:doc-formula-expressions, check:adr-0087-registration, check:changeset-no-major, check:empty-changeset, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check:dts-closure, check:published-files. Two are NOT MEASURED, not red: check:dual-build-cjs-loads exit 3 'PREREQUISITE NOT MET' (75+ packages without dist) and check:type-check-debt exit 3 'PREREQUISITE NOT MET' (27 workspace dependencies without a built type entry point; --re-measure refuses to measure a different world) — both declared to CI. Control-character self-scan of the edited file: 0 hits. Affected-package narrowing, declared: TURBO_SCM_BASE=60aaf369b6 turbo ls --affected lists 76 packages (every spec dependent); the diff is a comment inside a TS interface docblock, which vitest strips, so only a test that fs-reads the contract source can observe it — the 6 spec files above were run; outside spec the three service-automation tests that name the path (flow-terminal-messages, resume-store-unreachable-log-cause, runas-attribution-contract) mention it only in their own header comments and contain no readFileSync (count 0), so they cannot observe it. CI runs the whole affected set. No ablation: nothing was added that could fail to fire — the one pin's coverage of the new meaning is reported as absent rather than proven.",
      "mcp_calls": "3 — issue_read get_comments #16495 (claim confirmation, closing the payload's hasNextPage gap), add_issue_comment #16495 (this report), issue_read get_comments #16495 (read-back of this report)",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT filed, reported for PM routing per the dispatch fence: the engine's cancelRun has no cancel-side compare-and-set (forgetSuspendedRun(run, 'cancelled') deletes by id unconditionally; no `cancelling` guard), so two overlapping cancels of one run both return true and both record a terminal cancelled log — the reviewer's measurement, confirmed here. The contract now says so; whether the engine should mirror resume's claimAdvance on the cancel path is #13953's (services half) or a new card's call.",
        "Observation, not filed: the docblock pin in automation-service.test.ts asserts /idempotent/ and /could not READ/ on the cancelRun doc but nothing about the (now corrected) exclusivity semantics of `true`, so this correction can drift back silently; recorded here because the dispatch asked for the fact, not a pin."
      ]
    }

    Generated by Claude Code

  7. removed their assignment
    on Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions