Skip to content

fix(runtime,spec): the resume door asks the engine whether a status-less exit is repairable - #18792

Merged
os-litant merged 2 commits into
mainfrom
claude/issue-17541-resume-door-consults-inspection
Sep 17, 2026
Merged

os-litant merged 2 commits into
mainfrom
claude/issue-17541-resume-door-consults-inspection

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #17541

Clause-②: yes (widening)

Ruling: batch #148 item 5, letter 1 (comment 5716043121), maintainer 「同意」 2026-09-17T14:27Z.

The defect, as the ruling asked for it: a WIRE reading

Step one of the ruling is a measurement through the HTTP route, not through the engine. Driven through HttpDispatcher.handleAutomation on a parked two-level delegation — the parent parked at its subflow node, the child parked at a pausing node, the child's downstream node then throwing — POST /automation/parent_flow/runs/:parentRunId/resume answered:

BEFORE (origin/main at f1c9bb3056, the defect reproduced on trunk):

400 {
  "success": false,
  "error": {
    "code": "FLOW_FAILED",
    "message": "subflow run 'run_8a89ceab…' (child_flow) failed: update_record(crm_leave_request) failed: Record 9SEmlyRfw8D9-J7Z not found",
    "httpStatus": 400,
    "details": { "runId": "run_240f641d…", "repairable": false }
  }
}

AFTER (this branch):

400 {
  "success": false,
  "error": {
    "code": "FLOW_FAILED",
    "message": "subflow run 'run_bc8d989f…' (child_flow) failed: update_record(crm_leave_request) failed: Record 9SEmlyRfw8D9-J7Z not found",
    "httpStatus": 400,
    "details": { "runId": "run_337c1bca…", "repairable": true }
  }
}

At that same instant, on the same engine, both readings taken in the same probe run:

engine.inspectConsumedSuspension(parentRunId)
  = { repairable: true, runId: "run_…", flowName: "parent_flow", nodeId: "sub",
      correlation: "subflow:run_…", witness: "journal", consumedAt: "…" }
engine.restoreConsumedSuspension(parentRunId, { requestedBy: "ops" })
  = { restored: true, chain: [childRunId, parentRunId] }

So the wire told an operator not to retry a repair that works, and the schema's own .describe() stated the opposite reason — projected verbatim into the generated reference page.

What changed

packages/spec — IAutomationService declares the read-only member. inspectConsumedSuspension(runId), optional, exactly the shape the engine already implements publicly (AutomationEngine.inspectConsumedSuspension, landed for #15358). The declared result is deliberately narrower than the implementation's, on the precedent the sibling restoreConsumedSuspension set (#16495 route (i)): { repairable: boolean; runId: string; reason?: string }, with reason typed as the string the implementation answers rather than as an enumeration this contract would have to keep in step with. The engine's wider ConsumedSuspensionInspection satisfies it under implements — proved below, in both directions.

packages/runtime — the resume door consults the declared contract. On a 400 FLOW_FAILED whose result carries a status, that stamp still decides and the engine is not consulted at all. On a result that stamps none, the door asks the declared optional member and relays its repairable.

⛔ The door never probes an undeclared member: the member is on IAutomationService first, and the door reads it through that optionality, the same way it already reads resume. Every way of not getting an answer is FAIL-CLOSED — a service that declares no inspection member answers false exactly as it did before, and an inspection that REJECTS (a store it could not read) answers false and says so once at warn. An unreadable store is UNKNOWN, not "nothing to restore", and it is never allowed to replace the 400 the caller asked for with a 500.

⛔ The fence the nested-chain work was dispatched with is untouched: a cascade-failed ancestor is still never STAMPED 'stranded'. Its repairability is carried by the journal and REPORTED by the inspection, which is exactly why the door has to ask instead of reading a word. ⛔ And no new AutomationResult.status member is minted (ruled-out option 2): there is nothing new for a client to learn, and details.repairable is the member a client was already told to branch on.

Docs. ResumeFailureDetailsSchema.repairable's .describe() is rewritten to the truth; content/docs/references/api/automation-api.mdx is REGENERATED with the repo's own tooling (pnpm --filter @objectstack/spec check:generated --fix, which proved exactly that one artifact stale and regenerated only it — a one-line diff). The two hand-written pages were re-read for the same sentence and both carried it, so both are corrected by hand: content/docs/automation/flows.mdx said repairable is "true exactly when status is stranded", and content/docs/api/client-sdk.mdx asserted verdict.data.status === 'stranded' inside its if (verdict.data.repairable) sample.

Tests

Evidence below is from the final commit, 228275a473.

A test that fails without the change and passes with it, plus its controls. New, in two layers:

  • packages/verify/src/automation-resume-delegation-repairable.test.ts — the wire, through the real engine and the real route. Three cases: the DELEGATION exit (repairable: true, no status, the whole details parsing under ResumeFailureDetailsSchema, and the operator verb then accepting exactly that run and the re-issued resume completing the tree); a CONTROL that the non-delegation stranded exit is unchanged (status: 'stranded', repairable: true); and a FIRING CONTROL — a delegation whose leaf is NOT repairable answers repairable: false on that same status-less arm.
  • packages/runtime/src/domains/automation-resume-delegation-repairable.test.ts — the door's shaping with a fake service, so the arms a real engine will not produce on demand are reachable by name: the two fail-closed arms, and two controls proving a stamped 'stranded' / 'failed' is answered by the STAMP and the engine is never asked.

The pre-existing packages/runtime/src/domains/automation-resume-stranded-details.test.ts is left untouched on purpose and is the standing control that every non-delegation exit answers exactly what it did.

Reverse verification — behaviour. The pre-change expression was put back on the committed tree, proven on disk (injected marker count 1, deleted call count 0, blob hash moved off HEAD's), @objectstack/runtime rebuilt, and the mutation proven to have reached the artifact the suite consumes (node scripts/ablation-dist-preflight.mjs runtime … — marker present in 2 built files). Direction observed, as predicted: turned red, and only there — 1 failed | 4 passed, the failure being the DELEGATION wire case, with both controls and the two pre-existing wire cases still green. Restore leg: source restored from HEAD, whole-tree git status --porcelain clean, blob hash equal to HEAD's, rebuilt, --absent preflight confirming the marker is gone from all 6 built files, suite green again (5 passed).

Reverse verification — types. AutomationEngine implements IAutomationService, so the new declaration is enforced on the implementer. With the engine's ConsumedSuspensionInspection mutated to drop runId from its repairable arm, pnpm --filter @objectstack/service-automation exec tsc --noEmit exits 1 naming the member:

src/engine.ts(8133,11): error TS2416: Property 'inspectConsumedSuspension' in type
  'AutomationEngine' is not assignable to the same property in base type 'IAutomationService'.

Restored, the same command exits 0. That is also the proof the typecheck reads the rebuilt spec .d.ts rather than a cached one.

Suites — both projects run where a package has two, exit codes read from $? after a redirect, never through a pipe:

command exit
pnpm --filter @objectstack/spec test (--project local) 0 — 486 files, 14017 tests
pnpm --filter @objectstack/spec test:repo (--project repo) 0 — 31 files, 536 tests
pnpm --filter @objectstack/runtime test (--project local) 0 — 266 files, 3667 passed, 1 skipped
pnpm --filter @objectstack/runtime test:repo (--project repo) 0 — 2 files, 69 tests
pnpm --filter @objectstack/verify test 0 — 110 tests
pnpm --filter @objectstack/spec typecheck 0
pnpm --filter @objectstack/runtime typecheck 0
pnpm --filter @objectstack/verify typecheck 0
pnpm --filter @objectstack/service-automation typecheck 0
pnpm --filter '@objectstack/verify^...' build 0

Gates run locally (all exit 0): pnpm --filter @objectstack/spec check:generated (15 of 15 up to date after the regeneration), check:nul-bytes, check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main, check:objectui-changeset, check:pm-widening-tells, check-closing-keyword-parity, check-spec-docblock-symbol-anchors, check-doc-frontmatter, check:docs-single-h1, check:doc-anchors, check-docs-section-name, docs-audit/check-affected-docs, check:route-envelope, check:dispatcher-error-vocabulary, check:cross-package-test-inputs, check:test-source-alias, check-undeclared-dep-imports, check:tier-file-adoption, check:type-check-coverage, check:type-check-debt.

The three changeset gates first exited 1 with no merge base between 'origin/main' and 'HEAD' — a shallow-clone PREREQUISITE, not a verdict. git fetch --deepen 300 origin main restored the merge base (f1c9bb3056) and all three then exited 0.

Lint: the whole population, not a narrowing. node --stack-size=4000 node_modules/eslint/bin/eslint.js . --no-inline-config --format json exits 0 over 6834 files (count read from the JSON output), 0 errors and 0 warnings.

NOT measured here, deliberately: the rest of CI's gate farm (the derivation names 107 runnable commands plus 50 artifact-roster families, 10 wide-population families and 6 path-scheduled CI jobs), and the Temporal Conformance / Dogfood / Build Docs jobs, which have no local invocation.

Acceptance notes

Two observations found on the way, neither filed — each is an omission rather than an error, and neither is a reproducible defect, a broken declared contract, or an authoring trap:

  • registerSubflowNode is exported from packages/services/service-automation/src/builtin/index.ts but is not re-exported from that package's root index.ts, while five sibling registrars are. Nothing is broken by it: installBuiltinNodes reaches the same executor and is what the new wire test uses. Next party to meet it: an out-of-package test author who wants the subflow node alone.
  • ResumeFailureDetailsSchema.status's own describe names "a subflow child that failed terminally" as the status-less case and does not also name the delegation frame. Incomplete, not false, and the ruling named only the repairable describe. Next party to meet it: whoever next edits that enum's text.

Generated by Claude Code

…ess exit is repairable

The `400 FLOW_FAILED` details computed `repairable` as `status === 'stranded'`.
The subflow delegation exit deliberately stamps no status, so the wire answered
`repairable: false` for a run `restoreConsumedSuspension` re-arms as a chain.

`IAutomationService` now declares the read-only `inspectConsumedSuspension(runId)`
the engine already implements; the door consults it on the status-less arm and
relays the verdict, fail-closed both when the member is absent and when the read
rejects. The schema's own `.describe()` is rewritten to that truth and the two
hand-written docs pages corrected with it.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
… repairable describe

Adds the changeset for the spec contract widening and the runtime door change.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/spec, touching 6 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via ResumeFailureDetailsSchema (symbol, a top-level const))
  • content/docs/automation/flows.mdx (via ResumeFailureDetailsSchema (symbol, a top-level const))
  • content/docs/permissions/system-context.mdx (via handleAutomationRequest (symbol, a top-level function))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via IAutomationService (symbol, a top-level interface))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 141 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e5705a8ea528573e67eb0ea1ca269dff6c70d512 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 15839e289a60c5edb7cfa314bcb2f42a47dc229e — the merge of head 228275a473565ce3784ca84bea3f1ea80dae9a65 into base e5705a8ea528573e67eb0ea1ca269dff6c70d512, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 15839e289a60c5edb7cfa314bcb2f42a47dc229e && git checkout 15839e289a60c5edb7cfa314bcb2f42a47dc229e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e5705a8ea528573e67eb0ea1ca269dff6c70d512 228275a473565ce3784ca84bea3f1ea80dae9a65 && git checkout -B drift-repro e5705a8ea528573e67eb0ea1ca269dff6c70d512 && git merge --no-ff 228275a473565ce3784ca84bea3f1ea80dae9a65

node scripts/docs-audit/affected-docs.mjs --json e5705a8ea528573e67eb0ea1ca269dff6c70d512

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e5705a8ea528573e67eb0ea1ca269dff6c70d512 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 228275a473565ce3784ca84bea3f1ea80dae9a65

① Derived judgments

  1. Measurement basis. Own worktree at that head, complete tree, merge base with origin/main = f1c9bb3056 (present without deepening in this clone). pnpm install --frozen-lockfile exit 0, pnpm --filter '@objectstack/verify^...' build exit 0 before any gate or ablation. Every exit read from $? after a redirect. The six gate scripts run by path (check-empty-changeset.mjs, check-adr-0087-registration.mjs, check-changeset-no-major.mjs, spec's check-generated.ts, build-docs.ts, build-api-surface.ts) are blob-identical between the head and origin/main. The head's scripts/pm is behind origin/main on five files; the only one of them that is a gate for this class, check-widening-tells.mjs, was run in both copies. Tree restored to HEAD's blobs and empty porcelain after every mutation.

  2. Precedent and narrowing (attack 1). The precedent exists in exactly the claimed shape: restoreConsumedSuspension? at packages/spec/src/contracts/automation-service.ts:893 declares {restored; runId; refusal?: string; reason} against the engine's wider SuspensionRestoreResult, its docblock naming [spec] Declare cancelRun and restoreConsumedSuspension on IAutomationService — #13953's ruled contract half, step (1), never filed #16495 route (i). The new member at :973 declares a Promise of {repairable: boolean; runId: string; reason?: string}. The engine's ConsumedSuspensionInspection (packages/services/service-automation/src/engine.ts:1831) is a four-arm union: the true arm {repairable: true; runId; flowName; nodeId; correlation?; witness; consumedAt} and three false arms each {repairable: false; runId; reason: one literal}. The narrowing is sound in the direction used: the door reads only repairable, coerced with === true; every engine arm carries runId and a boolean-literal repairable; the false arms' literal reason is a string; the true arm carries no reason. So the declared docs ("absent on the true arm", "runId echoed both ways", "throws when a store cannot be read") describe exactly what the engine produces — inspectConsumedSuspension at engine.ts:8133 reads through loadSuspendedRunStrict, whose throw propagates, matching the @throws. The shapes the declared type admits that the engine cannot produce (a reason beside true; a false with no reason) are never read by the door, so the wire cannot inherit them. Enforcement measured: service-automation tsc --noEmit exit 0 at head; with runId deleted from the engine's true arm (blob moved off HEAD's, true-arm runId count 0) exit 1 with three errors, all consequences of that one edit — engine.ts(8132,11) TS2416 Property 'inspectConsumedSuspension' in type 'AutomationEngine' is not assignable to the same property in base type 'IAutomationService', engine.ts(8144,17) TS2353 (the implementation still supplies runId) and engine.test.ts(922,19) TS2322 (a test assigning the engine to the contract). Restored to HEAD's blob: exit 0.

  3. Optionality and the fence (attack 2). The read at packages/runtime/src/domains/automation.ts:1484 is automationService.inspectConsumedSuspension?.(runId) on a variable statically typed IAutomationService (deps.getService(context, CoreServiceName.enum.automation) resolves through CoreServiceContract, packages/spec/src/contracts/core-service-contracts.ts:80). That this is a declaration-bound read and not a duck-check wearing a declaration was measured: with the member renamed inside the built packages/spec/dist/contracts/index.d.mts and index.d.ts (sha256 recorded before and after), pnpm --filter @objectstack/runtime exec tsc --noEmit exits 1 with exactly one error, automation.ts(1484,41) TS2551 Property 'inspectConsumedSuspension' does not exist on type 'IAutomationService'; files restored byte-identical, exit 0. A typeof probe on an untyped host would have compiled through that rename. Both no-answer arms measured fail-closed through the real HttpDispatcher with a fake service (packages/runtime/src/domains/automation-resume-delegation-repairable.test.ts, 6 cases, run per-file with the verbose reporter so each case is named): a service with no member answers 400 FLOW_FAILED {runId, repairable: false} with no warning; a member that rejects (connection reset) answers the same 400 with repairable: false and exactly one warn naming the member, the runId and the rejection text — the 400 is never replaced by a 500. The try wraps the await, so a synchronous throw is caught as well; a non-Error rejection is stringified. Absent is silent and rejected is said once, which the helper's docblock keeps apart as two different facts.

  4. Only the delegation exit moves (attack 3). resumeFailureDetails has one caller, the resume door's FLOW_FAILED arm (automation.ts:2336); the stamped branch keeps the literal expression status === 'stranded' and the status relay ...(status !== undefined ? { status } : {}) is unchanged. Measured: two stamp controls (a stamped stranded answers repairable: true, a stamped failed answers false) with the fake inspection primed to answer the opposite and not.toHaveBeenCalled() on the spy — whose same-subject hit is the delegation case asserting toHaveBeenCalledWith('run_parent'). The pre-existing runtime control file automation-resume-stranded-details.test.ts (14 cases; git diff --quiet against the merge base exit 0, untouched) and the two pre-existing automation resume door: the 400 FLOW_FAILED envelope drops the engine's status: 'stranded' verdict — the wire mirror's member is unreachable on the wire #15221 wire cases in packages/verify/src/automation-resume-stranded-details.test.ts stay green at head and under the behavioural ablation alike, i.e. they are insensitive to this change as they should be. The verify non-delegation control (resume the CHILD directly) still answers {status: 'stranded', repairable: true} through the real engine. The trigger door's 400 still carries no repairable (pre-existing case green).

  5. The service-automation: restoreConsumedSuspension cannot reach a nested run — a stranded child's cascade-failed ancestors are consumed without a snapshot, so restoring the child continues into a dead parent #15222 fence (attack 4). engine.ts is not in the diff (9 files, none under packages/services). The only status: 'stranded' stamp remains engine.ts:6906, the run whose own pause was consumed; failSuspendedRun still records the ancestor 'failed' and journals only when a repairable descendant is below it. The door synthesises no status. Measured on the wire through the real engine: the delegation case asserts details.status is undefined while repairable is true, then the operator verb accepts exactly that run (restored: true, chain [child, parent]) and the re-issued resume completes the tree. No new AutomationResult.status member; ResumeFailureDetailsSchema keeps runId, status?, repairable with only a describe string changed. Ruled-out options 2 and 3 are absent.

  6. The two ablations (attack 5). Behavioural, re-derived on the committed tree: pre-change expression put back (marker count 1, deleted call count 0, blob off HEAD's), runtime rebuilt, mutation proven in the artifact verify consumes (packages/runtime/dist/index.js: consumedSuspensionSurvives call count 1 to 0, inspectConsumedSuspension 2 to 0; verify resolves @objectstack/runtime from dist). Verify wire files: 1 failed | 4 passed, the failure being the DELEGATION case only; the two automation resume door: the 400 FLOW_FAILED envelope drops the engine's status: 'stranded' verdict — the wire mirror's member is unreachable on the wire #15221 wire cases, the non-delegation control and the firing control green. Runtime door files: 3 failed | 17 passed — the DELEGATION case, the FIRING CONTROL (asserts the engine was asked) and the rejecting FAIL-CLOSED case (asserts one warn) red; all 14 pre-existing automation resume door: the 400 FLOW_FAILED envelope drops the engine's status: 'stranded' verdict — the wire mirror's member is unreachable on the wire #15221 door cases, the no-member arm and both stamp controls green — exactly the cases that assert the engine is consulted turn, none that pin the prior answers. Restore leg: blob equals HEAD's, rebuilt, dist counts back to 1 and 2, verify 5 passed, runtime 20 passed, porcelain empty. Type-level: re-derived in item 1 (TS2416 at engine.ts:8132; the dev's 8133 is the same site, their edit kept the line count where mine removed a line). The cache reasoning holds: no incremental, composite or tsbuildinfo anywhere in the tsconfig chain of runtime or service-automation, so tsc reads the .d.ts on disk at each invocation; TS2416 can only name a member the read .d.ts contains, so it proves the read was post-PR; the rename ablation in item 2 independently proves runtime's tsc reads the .d.mts present at invocation time.

  7. Docs (attack 6). After a full build, pnpm --filter @objectstack/spec check:generated exit 0, 15 of 15 up to date (check:docs and check:api-surface included); a fresh gen:docs exit 0 leaves content/docs/references with an empty porcelain. Controls that hit: the new describe phrase is in 2 built dist files and in the page; the repairable table cell is byte-equal to the source describe text (1046 chars); the exact pre-PR wording exactly when \status` is `stranded`hits the merge-base page once and hits the current page, the dist and the whole ofcontent/ zero times. The committed regeneration diff is exactly the one table row. The two hand-written pages (content/docs/automation/flows.mdx, content/docs/api/client-sdk.mdx) carried the sentence and are corrected in the diff. Whole-tree grep for the sentence outside content/: packages/*/CHANGELOG.mdentries (release-owned, historical, correctly untouched) and one live docblock — see ③ flag 1. The remainingstatus === 'stranded' derivations are the approvals door's own (packages/plugins/plugin-approvals/src/approval-service.ts:3499, :3775; packages/types/src/stranded-decision.ts:85`), a different door outside this ruling and still true of that door.

  8. Gates and suites at head, all exit 0 and matching the dev's table: spec test (--project local) 486 files / 14017 tests; spec test:repo 31 / 536; runtime test (--project local) 266 / 3667 passed, 1 skipped; runtime test:repo 2 / 69; verify test 15 / 110; typecheck for spec, runtime, verify, service-automation; the three changeset gates by script path with --base origin/main (merge base f1c9bb305); check-widening-tells.mjs --declaration yes --diff in both HEAD's and origin/main's copy — a yes declaration is never blocked by that gate, so it decides nothing here and this record is the deciding step. CI at this head: 39 check runs, every completed run success or skipped, none failed, including Type Check · workspace, Lint & Repo Gates, Test Core, Build Docs, Temporal Conformance and the Dogfood jobs.

② Semver level

@objectstack/spec minor, @objectstack/runtime minor, Clause-②: yes (widening) — confirmed. Additive only: one optional method added to an existing exported interface; no export added, removed or renamed (check:api-surface, check:export-origins, check:declaration-map all up to date on a fresh build); no wire key added, renamed or retired. Implementer side: the repo's sole implements IAutomationService (AutomationEngine) already carried a conforming member; plugin-approvals' structurally separate ApprovalResumeSurface.inspectConsumedSuspension? never receives an IAutomationService-typed value (attachAutomation(automation: ApprovalResumeSurface)), and its two source tsc --noEmit halves exit 0 against the rebuilt spec dist; the pinned objectui sibling 53ded82bf7 names neither the member nor implements IAutomationService (git grep at that object; control repairable hits 1). Under AGENTS.md's declaration rule, yes takes at least minor and only (narrowing) is BREAKING, so a widening owes no BREAKING banner and no ADR-0087 marker — check-adr-0087-registration reads "adds no declared-breaking changeset", check-changeset-no-major "no major bump", check-empty-changeset "1 declaring changeset added, none from the merge base modified or deleted". Runtime's minor is the wire-observable widening of the set of runs that answer true; a patch would have under-declared a consumer-visible truth change.

③ Boundary flags

  1. A live public docblock still carries the retired sentence, outside the ruling's enumerated docs surfaces: packages/client/src/index.ts:5551, the JSDoc on client.automation.resume, reads err.details?.repairable; // true exactly when \status` is 'stranded'. It ships as hover documentation in @objectstack/client's .d.tsand is not projected intocontent/(grep 0; control: the same grep hits the source). Not blocking under letter 1, which named the.describe(), the regenerated reference and the two hand-written pages; the seat may prefer a docs-only follow-up. The fix is one line: replace the comment with the answer's true shape, e.g. // the engine's own answer; can be true with no `status` (subflow delegation)`.

  2. Two structural declarations of the same engine member now coexist: spec's IAutomationService.inspectConsumedSuspension? (a Promise of {repairable: boolean; runId: string; reason?: string}) and plugin-approvals' ApprovalResumeSurface.inspectConsumedSuspension? (approval-service.ts:225, {repairable: true} | {repairable: false; reason: literal union}). The spec shape is not assignable to the plugin shape (reason?: string against a required literal union), so a future card that hands plugin-approvals a spec-typed service will need a local widening; today nothing assigns across. Observation for whoever next lets plugin-approvals consume the spec declaration instead of its own.

  3. Local prerequisite-not-met readings, recorded so they are not mistaken for reds: pnpm --filter @objectstack/plugin-approvals typecheck exits 1 only in its check:test-typecheck half, on src/status-mirror-cascade.integration.test.ts (untouched by the PR) with TS2307 for @objectstack/trigger-record-change, whose dist does not exist under a build scoped to verify's dependency closure; CI's Type Check · workspace is green at this head. The three changeset gates needed no --deepen in this clone because the merge base was already within reach.

  4. The dev's two unfiled observations (registerSubflowNode not re-exported from service-automation's root; the status describe not naming the delegation frame by name) are omissions rather than defects; concur that neither needs a card from this review.

NOT measured: the full eslint population locally (CI Lint & Repo Gates green); @objectstack/service-automation's and @objectstack/plugin-approvals' own test suites (the engine is untouched by the diff; plugin-approvals is not in the diff); the Console Pin Gate (skipped in CI; nothing removed or renamed, so the sibling rule is not engaged); Temporal Conformance, Dogfood and Build Docs locally (CI green); the remainder of CI's gate farm beyond the gates named above.

Implemented-by: claude/issue-17541-resume-door-consults-inspection
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 17, 2026 21:52
@os-litant
os-litant added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 182bbde Sep 17, 2026
44 checks passed
@os-litant
os-litant deleted the claude/issue-17541-resume-door-consults-inspection branch September 17, 2026 22:17
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ast 100 is UNJUDGED rather than a truncated claim pool (objectstack-ai#18799)

Fixes objectstack-ai#18683

Clause-②: no

## The defect

`scripts/pm/check-clause2-carriers.mjs` read a card's comment thread
with ONE request — `/issues/{n}/comments?per_page=100`, no `page=`
ladder, no short-read check — while the two sibling list reads in the
same file paged to a declared cap and answered `null` (UNJUDGED, never
clean) when they hit it. One file, two OPPOSITE defaults on "I did not
read everything", and the fail-OPEN one was the read that arbitrates
OWNERSHIP: the governing-claim pool, its membership, and the `Clause-②`
declaration read out of it all come from those rows. A thread past 100
comments handed the pool its first page and nothing said the tail had
been dropped, so a claim written past row 100 was not superseded — it
was never a candidate — and a superseded carrier governed in its place.

## The before-reading, on a 101-row fixture

Driven end to end through the real CLI against a stubbed board
(`--pair`, no network), on `main` `d9ba33df4c` (script blob
`d753e2a8cf06d8f72c436e0a1917b2fecb8be813`). Two fixtures, both 101
rows, both differing from a complete thread only past the page boundary.

| fixture | BEFORE (`main`) | AFTER (this PR) |
|---|---|---|
| 100 claim-free rows, the 101st the only `Claim:` | `card-comments: 100
row(s)` · `claim.selected: none — no comment on this thread carries a
line beginning \`Claim:\`` · **exit 4, row C2 `absent`** |
`card-comments: 101 row(s)` · the 101st claim is the pool ·
`claim.clause2-line: DECLARED \`no\`` · **exit 0** |
| row 1 an older `Claim:` declaring `yes`, the 101st a newer one
declaring `no` | `claim.clause2-line: DECLARED \`yes\`` from the
SUPERSEDED carrier, which is not even listed as rejected · **exit 4, row
C3** | the newer claim governs, the older is listed REJECTED/SUPERSEDED
· `DECLARED \`no\`` · **exit 0** |

The second row is the fail-OPEN direction stated as a measurement: one
thread, two readings, and they disagree on the declaration itself.

## The ladder, and the cap

All three list reads now go through one `pagedListRead` helper — it
pages to a declared cap, stops on the FIRST short page (no wasted
request), and on the cap files the one shared `pageCapNote` sentence and
answers `null`. `readCarrierEvents` (`EVENT_PAGE_CAP` 10) and
`readPullFiles` (`FILE_PAGE_CAP` 3) keep their caps to the number; what
they gain is that the third read can no longer hold a different default.

`COMMENT_PAGE_CAP` is **10** pages = 1,000 comments. Sized on this
board, read 2026-09-17 off the open-issue list rows (550 rows listed,
cross-checked against `open_issues_count` = 550):

- longest open thread of any kind: seat post objectstack-ai#6015 at **895** comments —
nine pages;
- next four: objectstack-ai#12708 at 365, objectstack-ai#6023 at 241, objectstack-ai#6017 at 206, objectstack-ai#6024 at 187;
seat post objectstack-ai#7623 at 71;
- longest thread carrying a queue label: objectstack-ai#13799 at **117** (`pm:queue`,
p2, unassigned);
- longest card in the clause-② population (28 pairs the sweep derived
that day): objectstack-ai#17534 at **14**.

So ten pages clears the whole board today with a page to spare, and it
is the same ten `EVENT_PAGE_CAP` uses — a reader comparing two caps in
one file should have to remember one number.

## The input record

The diagnosis key stays `comments`, so every sentence already keyed to
it still finds its diagnosis. Two declared fields are added to
`INPUT_RECORD_PAIR_FIELDS`, one per thread this file reads:

```
pair.1.card-comments: 101 row(s)
pair.1.card-comment-pages: 2 of 10 page(s) requested — the ladder stopped on a SHORT page, so the thread is COMPLETE
pair.1.pr-comment-pages: 1 of 10 page(s) requested — the ladder stopped on a SHORT page, so the thread is COMPLETE
```

and, when the cap is what stopped the read:

```
pair.1.card-comment-pages: CAPPED — 10 of 10 page(s) of 100 comments each were requested and EVERY ONE came back full, so the tail is past the cap and the thread is UNREAD (UNJUDGED) — ⛔ never a truncated pool, ⛔ never a clean reading
```

A thread of exactly 100 rows and a thread whose tail was dropped are the
same `100 row(s)` in every other line the block prints; they differ
here, because the complete one stopped on a short page and the truncated
one did not stop at all. The request ledger PR objectstack-ai#18681 added shows the
same ladder from the other side — request objectstack-ai#3 is now
`…/comments?per_page=100&page=1` and objectstack-ai#4 is `&page=2`.

## The pins

A new `--self-test` battery, `objectstack-ai#18683: the card-comment read pages to a
cap — past 100 is UNJUDGED, ⛔ never a truncated pool`, 27 cases,
declared in `SELF_TEST_BATTERIES` with the roster floor raised 29 → 30.
It drives the ladder with an offline page server that reproduces
GitHub's own semantics and counts the requests; ⛔ no network. What it
holds: the 101st claim ENTERS the pool and GOVERNS, and its line is what
the limb reads; the same thread cut at 100 reads `absent` (the CONTROL —
the reading the un-paged read produced); the newer claim past the
boundary supersedes the older one inside it, and cut at 100 the
superseded carrier's `yes` is what the limb reads; a capped read is
`null`, which is neither `missing` nor `absent` nor a carrier but
`unreadable`; the ladder stops on the first short page (2 requests for
101 rows, 1 for a short thread, 2 for exactly 100 — a full page is
indistinguishable from a finished one); a page that came back unread
ends the ladder and the record says the cap was NOT what stopped it; the
input record declares and prints both ladder fields; the sibling caps
are untouched; and all three reads render ONE cap sentence.

## The census, and the triage's upgrade probe

Report-only, no state write. Over the 550 open rows (521 issues, 29 PRs)
read on 2026-09-17:

- open `pm:queue` / `pm:dispatched` cards: **274**, of which **1**
exceeds 100 comments — objectstack-ai#13799 at 117;
- all open issues over 100 comments: **8** — objectstack-ai#6015 (895), objectstack-ai#12708 (365),
objectstack-ai#6023 (241), objectstack-ai#6017 (206), objectstack-ai#6024 (187), objectstack-ai#6021 (145), objectstack-ai#6367 (127), objectstack-ai#13799
(117). Seven are `pm:seat` posts;
- open PRs over 100 comments: **0**; the longest is objectstack-ai#18638 at 10.

**The upgrade probe's result: the condition is NOT met today.** The
clause-② population is what a `--pair`/sweep derivation actually pairs,
not what carries a queue label: the sweep derived **28 pairs from 29
open PRs**, and the longest card thread among them is **14** rows
(objectstack-ai#17534). The two open PRs that mention a 100+-comment card in prose —
objectstack-ai#18786 (objectstack-ai#6015, objectstack-ai#7623) and objectstack-ai#18765 (objectstack-ai#6024) — deliver objectstack-ai#18693 and objectstack-ai#18652
respectively, both under 10 comments; driven live before and after, both
answer exit 0 with an identical pair reading. So no recorded `--pair`
verdict on this board today was taken on a truncated pool, and the
triage's p1 condition (「找到任一进入条款②认领池、评论数 > 100 的卡并驱动一次」) has no live
instance to drive. The exposure is one PR away rather than realised:
objectstack-ai#13799 is `pm:queue` at 117 and enters the population the moment a PR
delivers it.

The cost is unchanged by the ladder, measured on the same board: **64
reads for 28 pairs, before and after**, because every live thread fits
one page and the ladder stops on a short page. The live `--pair 18765`
input records differ in exactly three lines — the two request paths
gaining `&page=1`, and the two new ladder fields.

⚠️ One thing the two full sweeps do NOT compare: the sweep's finding
COUNT moved 4 → 3 between them, and that is the board, not this diff.
`needs:contract-review` was hung on PR objectstack-ai#18792 at `2026-09-17T21:04:46Z`,
between the two runs, closing the C1 split on objectstack-ai#18792 / objectstack-ai#17541 on its
own. The controlled A/B is the `--pair 18765` diff above.

## The ablation

Two legs, each from the COMMITTED fix, each proving the mutation reached
disk by blob hash and occurrence count before reading any result, each
restored under a `trap` with `git checkout HEAD --` and verified by hash
and an empty `git diff HEAD`. HEAD blob
`ccd5ad7c9a00fe703d644be261a24f1ed847915a`.

| leg | mutation | blob after | self-test |
|---|---|---|---|
| A — the ENTRY side | `COMMENT_PAGE_CAP` 10 → 1 |
`1a0a952d07748101937420006b9475042d93a5cb` | **exit 1, 11 of 865 red** —
the 101st-claim pins, the superseding pins, the request-count pins, the
input-record pin |
| B — the UNJUDGED side | the cap branch returns the pages that DID
arrive (the pre-fix fail-OPEN default) |
`c176dfe7e54e7de6bc45737487841a346f509b79` | **exit 1, 3 of 865 red** —
a capped read is no longer `null`, no longer `unreadable`, and files no
cap sentence |

Every red in both legs belongs to the new battery; nothing pre-existing
went red in either. A third, unplanned reading came for free: leg B's
first attempt was a `perl -0pi` substitution whose anchor contained a
`/`, so the edit silently did nothing — the on-disk proof refused it
with `ABLATION VOID: the edit did not reach disk` instead of reporting a
green as a measurement.

## Self-test

```
✓ check-clause2-carriers self-test: 865 cases pass
```

838 before, 865 after — the 27 the new battery registers, which is what
its floor pins.

## Derived gates

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, no hand-fed path list, re-derived after
each `origin/main` merge (identical list both times). All 34 run at head
`993cb89e18`, each exit code captured by redirect-then-`$?`:

```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-changeset-no-major.mjs --self-test :: exit 0
node scripts/check-ci-filter-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-scripts-symbol-anchors.mjs :: exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:pm-clause2-carriers :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm lint :: exit 0
```

`--ran` reconciles 34 derived / 34 run / 0 UNRUN. `pnpm
check:pm-dispatch-gates` was run detached to a file — 1,788 cases,
748.6s on this box — and waited on in the foreground rather than under a
timeout, so it is a measurement and not a SIGTERM.

## Out of scope, deliberately

objectstack-ai#18764 (a decorated `**Claim:**` never enters the pool — the ENTRY side)
was read and NOT folded in: this card is WHICH rows reach the reader,
not what the reader does with them, and the two repairs touch different
lines. `claimRetractions` (PR objectstack-ai#18770, the EXIT side) was read for the
words it uses and not touched. The header's request-budget paragraph is
amended in the same commit, because it stated "2 reads per card" as a
fact and the thread is now a ladder — a cost statement that stopped
being true is the shape this file exists against.

`skip-changeset`: `scripts/pm/**` ships in no package's `files[]`, so
nothing published moves.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…nifest (objectstack-ai#18803)

Fixes objectstack-ai#18776

## What was wrong

`content/docs/api/client-sdk.mdx:337` — the `client.packages` fenced
block on the SDK page an integrator reads before the README — showed:

```ts
await client.packages.install({ id: 'com.objectstack.plugin-auth', version: '1.0.0' });
```

Parsed against the contract that door itself declares —
`PackageInstallRequestSchema`, whose `manifest` key is `ManifestSchema`
(`packages/spec/src/kernel/manifest.zod.ts:244`) — the literal is
refused twice:

```
invalid_value at [manifest, type]
invalid_type  at [manifest, name]
```

`type` and `name` are both required root manifest keys and both were
absent, so the example fails when copied verbatim. Same kind as objectstack-ai#18607,
one page along.

## The repair

Two required keys added, nothing else, in the key order the package's
own published README uses for the same door
(`packages/client/README.md:247`, landed by objectstack-ai#18607) so the two copies of
this example agree:

```ts
await client.packages.install({
  id: 'com.objectstack.plugin-auth',
  type: 'plugin',
  name: 'Auth Plugin',
  version: '1.0.0',
});
```

⛔ No schema was touched. This card is an example that is wrong, not a
contract that is wrong, and the measurement below did not invert that.

## Triage's escalation probe — the second deliverable

Triage attached a mandatory escalation condition: parse every
install/bootstrap example across `content/docs/api/**`, and if more than
this one is refused, the finding is not one broken example but a
published-example surface with no gate.

**Method** (⛔ not an eye-pass). A throwaway AST probe over all 13 files
of `content/docs/api/**`: `ts/typescript/tsx` fences are parsed with the
TypeScript compiler API, every call site in the population below is
located mechanically, and its argument literal is parsed against the
contract that call site declares. An object member shape the reader does
not model **throws** rather than passing quietly, and an empty
population **throws** — a probe that measures nothing must not read as
green.

| door | population | contract it is parsed against |
|:---|---:|:---|
| `packages.install(<literal>)` | 1 | `PackageInstallRequestSchema`
(`manifest: ManifestSchema`) |
| `packages.enable/disable/uninstall(<arg>)` | 3 | the declared `id:
string` |
| `packages.list(<arg?>)` | 2 | the declared `filters?: { status, type,
enabled }` |
| `new ObjectStackClient(<literal>)` | 4 | `ClientConfig`, read out of
`packages/client/src/index.ts` itself |
| `defineStack(<literal>)` | 2 | `ObjectStackDefinitionSchema` |
| shell fences `pnpm add` / `npm install` | 2 | the named workspace
package exists and is not `private` |
| **total** | **14** across 3 files | |

**Positive control** — the probe must catch the refusal we already know
about, or it is not measuring this. It does:

```
BEFORE  tree /home/user/objectstack-18776-base @ 6de7a2d (origin/main at the branch point)
  population: 14 example(s) across 3 file(s)
  REFUSED: 1
    content/docs/api/client-sdk.mdx:337  [packages.install]
      -> invalid_value at [manifest, type]
      -> invalid_type at [manifest, name]

AFTER   tree /home/user/objectstack-issue-18776 @ fd887ab
  population: 14 example(s) across 3 file(s)   (unchanged — the repair moved a verdict, not the corpus)
  REFUSED: 0
```

**Verdict: exactly one refusal across `content/docs/api/**`, and it is
the one this card names.** The escalation condition is **not met** —
nothing here re-grades the card or calls for the separate "published
examples have no executability gate" carrier. Two things are reported
rather than silently excluded:

- `content/docs/api/metadata-api.mdx:108` and `:116` carry the install
and publish request bodies as **inline prose with an explicit ellipsis**
— `{ manifest: { id: "plugin-auth", name: "Plugin Auth", version:
"1.0.0", ... }, ... }`. They omit `type`, but the ellipsis is written
in, they are not valid TS/JSON as printed, and nothing can be copied
verbatim out of them. On this lane's boundary that is 不完整, not 错误 —
outside the probe's parseable population and left alone.
- `content/docs/api/environment-routing.mdx:31` is a `defineStack`
example whose manifest is an explicit `// ...your manifest, objects,
apis, etc.` placeholder. It is accepted by `ObjectStackDefinitionSchema`
as written (that key is optional), so it is a pass, not a waiver.

⛔ No gate was built, wired or modified. The card measured why three
existing gates cannot see this class and recorded that wiring a census
into a **required** gate is a maintainer's floor decision; this PR does
not take that decision, and it deliberately does not extend objectstack-ai#18607's
README pin to this page for the same reason.

## Verification

- **Gate families** — `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` derives **39** families for this
change set (1 path). All 39 run on the final commit `fd887ab61`, each
exit code captured before any pipe: **39 exit 0**. `--ran`
reconciliation: `39 derived, 39 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED
zero)`.
- Four of them first answered a **prerequisite**, not a finding —
`check:doc-formula-expressions`, `check:doc-security-posture` and
`check:docs-transcript-drift` exited **3** (`@objectstack/lint` /
`@objectstack/formula` not built) and `check:skill-examples` exited 1 on
the same shape (`packages/client-react/dist` holds no `.d.ts`). Built
what each one named, re-ran, all four exit 0. `check:skill-examples` is
the gate whose `SDK_DOCS_PAGES` population contains this page.
- **`pnpm lint` is CI's run; the local narrowing is measured, not
skipped.** ① The universe read from eslint's own config: every `files:`
block in `eslint.config.mjs` names `{ts,tsx,mts,cts,js,jsx,mjs,cjs}` and
**zero** of them name `md` or `mdx`. ② `eslint --no-inline-config
--format json content/docs/api/client-sdk.mdx` reports 1 file, 0 errors,
1 warning — *"File ignored because no matching configuration was
supplied."* ③ Invariance: neither `projectService` nor
`parserOptions.project` appears in `eslint.config.mjs`, so type-aware
linting is off and this diff cannot move the verdict on any untouched
file — and the changed file is outside the linted set entirely.
- **`check:pm-dispatch-gates`** is not in this change set's derived
families (it is not reachable from a `content/docs/**` path), so its
detached-run prescription does not apply here.
- **Merged `origin/main` at `9846f2763`** before opening; no conflict,
and nothing on `main` had touched this file since the branch point.
Re-derived the families from the merged tree with `main`'s newer
`scripts/pm/dispatch-gates.mjs`: the same 39, no additions. Rebuilt
`spec` / `lint` / `formula` / `client-react` after the merge and re-ran
all 39 on the merge commit — the reading above is that run.
- **No same-file collision with objectstack-ai#18792**, which edits `:767-783` of this
file. Untouched here.

## Changeset — measured, not defaulted

`skip-changeset`. The criterion is whether anything published moves.

- **Positive control** (the instrument can say "this ships"): `npm pack
--dry-run --ignore-scripts --json` in `packages/client` lists
`README.md` in the tarball — a documentation file that really does
publish, and the one that carried the sibling instance in objectstack-ai#18607.
- **Reading**: that same listing contains **zero** `content/docs`
entries; no `package.json` in the repo declares a `files[]` entry naming
`content/docs` or escaping its own directory; `content/` contains no
`package.json` at all; and its only consumer, `apps/docs`, is `private:
true`. Nothing copies `content/docs/**` into a tarball at build time.

⇒ this diff publishes nothing from any released package.

## Acceptance notes

- `content/docs/api/metadata-api.mdx:108` / `:116` omit the required
`manifest.type` inside an explicitly-abbreviated inline body. Noted, not
filed: 不完整, not an example that fails when copied, and the carrier for
the class (published examples parsed by nothing) is the open question
already recorded on objectstack-ai#18607's PR — not a new card from this one.
- The probe lives in this session's scratchpad and is deliberately not
committed: turning it into a test is the census-into-a-required-gate
decision this card says is the maintainer's.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk

---
_Generated by [Claude
Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_

Co-authored-by: claude[bot] <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants