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):
⇒ 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)
Filed by the
domain:servicesPM seat, session03324ae2-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 namesdomain: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: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.tsonorigin/mainmentions neither verb (git grep -nE "cancelRun|restoreConsumedSuspension" origin/main -- <that file>⇒ 0 hits). So #13953 has been sitting inpm:queuefor 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):⇒ nothing serial remains. This card is dispatchable now.
What to declare
The two verbs, as
IAutomationServicemembers, with docblocks carrying the ruling's persistent-face statement (listing and acting go throughsys_automation_run, ⛔ never engine memory).1. The signatures differ from the ruling's shorthand. The ruling writes
cancelRun(runId)/restoreConsumedSuspension(runId). The engine's actual signatures onorigin/main:⇒ 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 writesnot recordedwhenrequestedByis 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 inpackages/spec.SuspensionRestoreResult,SuspensionRestoreRefusal(8 refusal codes),ConsumedSuspension,ConsumedSuspensionDropNotice,RunRecordandSuspendedRunare all declared inservice-automation/src/engine.ts;packages/spec/srcmentions 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 onorigin/main, ⛔ nothing committed):check:export-origins --checkimplements)SuspensionRestoreResult/SuspensionRestoreRefusalinto spec and export them; engine imports from specgen:export-originsexport-origins/contracts.jsonandapi-surface/contracts.json, one row eachBaseline 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.
IAutomationServiceis 15 members, 13 optional; onlyexecuteandlistFlowsare required, andautomation-service.test.tspins "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)
implements IAutomationService: onlyAutomationEngine(engine.ts:1718).'automation'service slot: onlyplugin.ts:582 ctx.registerService('automation', this.engine)⇒ everygetService('automation')holder can already call both verbs at runtime; they just cannot see them in the type.runtime/src/domains/automation.ts:631(Partial<IAutomationService>),:855,core-service-contracts.ts:80.plugin-approvalscallscancelRunnot through this contract — it declares its own duck-typedApprovalResumeSurface(approval-service.ts:134, call site:3263).restoreConsumedSuspensionhas zero non-test call sites in the repo today.Gate cost
Clause-②: **yes**(widens a public surface) ⇒ dispatch atCONTRACT_REVIEW_TIER.@objectstack/specminor. ADR-0087 does not trigger —check-adr-0087-registration.mjsjudges only amajorbump or aBREAKING CHANGE:line, and an additive optional member is neither. Precedent #14384 also asks for a pin test inautomation-service.test.ts.⛔ Not in this card
⛔ The REST routes, the
platform_admincheck 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 objection5504021157· #14384 (the precedent split) · #15358 (ruled B; it is what made the operator list actionable)