Repository navigation
Description
Filed by the domain:services execution PM seat on hitting this lane's hard red line while landing #15556 (PR #17908). ⛔ This lane holds zero packages/spec — both remedies below are in packages/spec/src/api/error-code-ledger.zod.ts, so this routes to the domain:spec seat.
⛔ Filed unlabelled: domain:* and priority grading are triage's.
The contradiction, measured
Two spec-side declarations disagree, and nothing reconciles them:
(a) The contract makes the code REQUIRED and names it. packages/spec/src/contracts/approval-service.ts:609-617:
export interface ResumeFailureReport extends ResumeFailureDetails {
/**
* The registered error code the failure is told under (ADR-0112 ledger):
* `RESUME_FAILED` for a run that could not be advanced, …
* Required here, on purpose: a success answer has no envelope `code` to fall back on.
*/
code: ErrorCode;
}
⇒ An implementer filling ApprovalDecisionResult.resumeFailure or ApprovalRecallResult.resumeFailure must stamp a code, and for the stranded-resume case the docblock prescribes RESUME_FAILED.
(b) The provenance ledger owns that code elsewhere. packages/spec/src/api/error-code-ledger.zod.ts — RESUME_FAILED sits at :287, inside the '@objectstack/rest' owner block that opens at :191. The '@objectstack/plugin-approvals' block (:1062) does not list it.
(c) check:error-code-provenance then refuses the implementation. Its rule, from the script's own header (packages/spec/scripts/check-error-code-provenance.ts):
every stamp site of a REGISTERED code in
packages/**non-test source must be listed under the stamping package's own owner key, or carry a recordedPROVENANCE_WAIVERSentry naming the owner key that deliberately holds the row instead ("the door, not the producer, names the wire vocabulary" — theFLOW_DISABLED/UPDATE_ID_MISMATCHclass).
Measured on PR #17908, head 109c9ce13
| reading | result |
|---|---|
CI Lint & Repo Gates step #103 "Error-code provenance guard" |
failure (the only failing step of 174) |
| the stamp site | packages/plugins/plugin-approvals/src/approval-service.ts:3370 — code: 'RESUME_FAILED', |
| is it new? | yes — a + line in this PR's diff |
| such stamps at the merge base | 0 |
does plugin-approvals own the row? |
no |
pnpm --filter @objectstack/spec check:error-code-provenance in a fresh worktree answered tsx: not found / node_modules missing, which is NOT MEASURED, not a verdict. Whoever takes this should run the guard with a real install and read its refusal text before choosing a remedy.
⭐ Why this is a spec-side defect and not an implementer's mistake
The family ruling #16472 (director seat, decision batch #76, maintainer 「同意」) required that a resume failure told to the caller be told in a machine-readable shape including a registered error code. Spec then landed the carrier — ResumeFailureReport, the resumeFailure members on both approval carriers, ResumeFailureDetailsSchema, and the pin test — but no provenance row or waiver was added for the package that would fill it. So the declaration shipped in a state where satisfying it trips a sibling gate. The first implementer to fill the slot is simply the first to find out.
Shape of a fix — ⛔ not prescribed, it is the spec seat's call
Two routes exist, and the guard names both:
- A row: list
RESUME_FAILEDunder'@objectstack/plugin-approvals'(and, when approvals: arecallwhose resume strands reports it as an ordinary non-failure — norepairablediscriminator, where the identical strand throughdecidecarries one #15970 lands, the same site set). - A waiver: a
PROVENANCE_WAIVERSentry naming'@objectstack/rest'as the owner that deliberately holds the row.⚠️ The guard's own header describes this as the "the door, not the producer, names the wire vocabulary" class and citesFLOW_DISABLED/UPDATE_ID_MISMATCHas the precedent — which reads like the matching shape here, since the REST approvals door is what publishes this vocabulary. Stated as the apparent precedent, ⛔ not as a recommendation.
⛔ Neither remedy may be taken by dodging the gate — e.g. hoisting the literal into a constant so the textual scan stops matching. The guard's header states its scan is textual precisely because the failure mode is a string literal in a few syntactic positions; evading it changes nothing and removes the reading.
⚠️ This blocks more than one card
- service-automation: a subflow's
bubbleToParentfailure is swallowed, so an approval decision answers 200resumed: truewhile the run behind it is stranded — #13807's three-outcome shape, one level up #15556 / PR fix(automation): tell approval decide() callers when a subflow parent strands #17908 — blocked on this now; the PR is otherwise complete and carries a PASSed in-seat contract review. - approvals: a
recallwhose resume strands reports it as an ordinary non-failure — norepairablediscriminator, where the identical strand throughdecidecarries one #15970 (recall's door, same file, same family ruling) — will hit this identically:ApprovalRecallResult.resumeFailurehas the same requiredcode. ⇒ Fixing this once unblocks both. Recorded so the second card does not re-derive it.
Refs
#15556 / PR #17908 (where it surfaced) · #15970 (the sibling that will hit it) · #16472 (the family ruling, batch #76) · ADR-0112 D3 / #13353 (the provenance gate's own charter)
Filed by the domain:services execution seat · seat post #6021 · R3
Generated by Claude Code
Activity
os-bill commentedon Sep 13, 2026
Claim: session_01MkQhmuuJAVDjmeWNixwDDH · branch claude/issue-17909-resume-failure-code-provenance⚠️ 若轮次发现补法必须新增导出符号,翻成
Branch: claude/issue-17909-resume-failure-code-provenance
Clause-②: no — ⛔ 待轮次证伪的前提。两条备选补法都落在
packages/spec/src/api/error-code-ledger.zod.ts的数据表里,既不新增导出符号、也不在已发布载荷上加键 ⇒ 按机械限应为no。yes并同笔挂双载体。授权:维护者直派通道,指令逐字引用
domain:spec执行席,sessionsession_01MkQhmuuJAVDjmeWNixwDDH,2026-09-13T04:05Z。维护者在本会话中当面指挥,按直派通道直接路由。domain:*、无pm:*、无 assignee)。domain:spec由本席在直派授权下补上,⛔priority:*未打 —— 定级是分诊席的独有产出,插队是调度信号而不是级别。分诊席可随时改判。标签一笔写入并回读对 diff(MATCH True,04:05:28Z)。本席亲取的读数(对
origin/main,2026-09-13T04:04Z)卡面两条承重断言成立:
packages/spec/src/contracts/approval-service.ts——export interface ResumeFailureReport extends ResumeFailureDetails { code: ErrorCode },必填,docblock 明写「Required here, on purpose: a success answer has no envelopecodeto fall back on.」packages/spec/src/api/error-code-ledger.zod.ts——'RESUME_FAILED'在:287,落在'@objectstack/rest'块(:191)内;'@objectstack/plugin-approvals'块在:1062。⭐ 但卡面低估了面:docblock 点名的是三个码,不是一个
ResumeFailureReport.code的 docblock 三个都点名了(RESUME_FAILED/RESUME_TARGET_LOST/RESUME_IN_PROGRESS)。⇒ 补法要一次覆盖三个,还是只覆盖实际被 stamp 的那些,是轮次要测的第一件事,⛔ 不是照抄卡面的单码框架。⭐ 本席亲读了
PROVENANCE_WAIVERS的真实形状 —— 它不是「绕过门禁」条目形如
{ package, code, registeredUnder, reason },reason是带裁决出处的长散文。卡面点名的先例就在里面,且正是「门而非生产者命名 wire 词表」那一类:⇒ waiver 是声明过的、带裁决痕迹的通道,⛔ 不是门禁削弱。但它记录的是一条例外,所以选它要拿出与上面这条同等强度的理由。
⭐ 轮次要回答的真问题(⛔ 不是「A 还是 B」的口味题)
那条先例的判据是一句可测的话:「门自己 stamp 这个码,并且从不读生产者的声明」。⇒ 测:
packages/plugins/plugin-approvals/src/approval-service.ts:3370,在 plugin-approvals 里 —— 这与「门 stamp」的先例形状方向相反,表面上支持加行而不是 waiver。⛔ 红线,照抄卡面
⛔ 两条补法都不许靠躲开门禁来实现 —— 例如把字面量提成常量让文本扫描不再命中。守卫 header 自己写明扫描是文本式的,正因为失败形态就是几个语法位置上的字符串字面量;躲开它什么都没修,只是把读数删了。
依赖与解锁面
bubbleToParentfailure is swallowed, so an approval decision answers 200resumed: truewhile the run behind it is stranded — #13807's three-outcome shape, one level up #15556 / PR fix(automation): tell approval decide() callers when a subflow parent strands #17908 —— 已被此卡卡住,PR 其余部分完整且带席内复核 PASS。recallwhose resume strands reports it as an ordinary non-failure — norepairablediscriminator, where the identical strand throughdecidecarries one #15970 —— 同族同文件,ApprovalRecallResult.resumeFailure有同样的必填code,会撞同一堵墙。⇒ 一次修好解锁两张。pnpm --filter @objectstack/spec check:error-code-provenance在裸工作树里答tsx: not found⇒ NOT MEASURED,不是判决。先装好依赖跑出守卫自己的拒收文本,再选补法。os-verify-lock.sh --status,04:04:54Z):holder 在跑,queue: empty⇒ 到达深度 1,可派。Generated by Claude Code
os-bill commentedon Sep 13, 2026
os-dev-report
{ "issue": 17909, "status": "done", "branch": "claude/issue-17909-resume-failure-code-provenance", "pr": "https://github.com/objectstack-ai/objectstack/pull/17924", "premise_still_valid": true, "summary": "The card's two load-bearing premises hold and its NOT MEASURED half is now measured. ONE remedy landed: a provenance row for 'RESUME_FAILED' under '@objectstack/plugin-approvals' in packages/spec/src/api/error-code-ledger.zod.ts, plus a minor changeset. The real surface is three codes but only ONE is owed a row: RESUME_FAILED (stamped by plugin-approvals at approval-service.ts:3370, new in PR #17908); RESUME_TARGET_LOST (stamped by nobody in a scanned position -- plugin-approvals throws it as a message prefix at :3189 and rest's catch maps that prefix, so rest's existing row stands); RESUME_IN_PROGRESS (the seat's open premise is FALSIFIED -- its owner block at ledger :1044 is '@objectstack/service-automation', not rest, and its stamp sites are service-automation/src/engine.ts:5795 and :6165, already listed; plugin-approvals only compares it at :3304). Route A (a row), not B (a waiver), chosen on the precedent's own measurable predicate rather than on taste -- see tests. Clause-2 flipped to yes on a measured T4 tell; needs:contract-review hung on the PR. The card half of that dual carrier, and the claim comment's own 'Clause-2: no', are the seat's to flip -- this round posts no second claim.", "tests": "GUARD REPRODUCTION (the card's NOT MEASURED half). Base 5741ff10c30, deps installed. Baseline, no mutation: exit 0, 'scanned 2301 files; 335 registered-code stamp site(s): 318 listed, 17 waived'. BEFORE leg -- PR #17908's approval-service.ts fetched at its head 109c9ce13 and copied in, ledger UNCHANGED: VERDICT guard-exit=1, guard's own text 'FAIL - 1 stamp site(s) of a registered code with no provenance row: @objectstack/plugin-approvals stamps RESUME_FAILED (objlit) at packages/plugins/plugin-approvals/src/approval-service.ts:3370 - not listed under its own owner key'. AFTER leg -- same injected stamp site, row present: VERDICT guard-exit=0, 'scanned 2301 files; 336 registered-code stamp site(s): 319 listed, 17 waived' (318 -> 319; admitted as LISTED, not waived). Row without the stamp site also green: exit 0. ABLATION (row removed from the committed tree, stamp site present): VERDICT guard-exit=1, names the SAME site at :3370, listed back to 318. COST DIRECTION (row present, a DIFFERENT resume code stamped in the same package): VERDICT guard-exit=1, names RESUME_TARGET_LOST at :3290 -- the row admits exactly the one measured code and over-permits nothing. #15970 COVERAGE LEG (row present, TWO RESUME_FAILED sites in plugin-approvals): VERDICT guard-exit=0, 320 listed -- the reconciliation is on (package, code), not on line. ABLATION HYGIENE: every leg proved its mutation landed by occurrence count on disk (0->1 for the stamp, 2->1 ledger rows for the ablation, 1 probe for cost direction), ran under trap RESTORE_FN EXIT INT TERM with absolute REPO_ROOT paths, restored with 'git checkout HEAD -- path' (never bare), and verified restore by git hash-object equality against the HEAD blob PLUS an empty 'git diff HEAD'. No dist preflight is owed and this is declared rather than skipped: the guard is a tsx script importing '../src/api/error-code-ledger.zod' (line 88), i.e. SOURCE -- no build is interposed between the edit and the reading. VERIFICATION (each exit captured before any pipe, heavy steps via scripts/pm/os-verify-lock.sh slot issue-17909, lock VERDICT command-exit 0 after a 246s wait): pnpm --filter @objectstack/spec test (--project local) exit=0, 473 files passed 1 skipped, 13483 tests passed 1 skipped; test:repo (--project repo) exit=0, 31 files, 523 tests; typecheck exit=0; build exit=0; check:api-surface / check:authorable-surface / check:docs / check:exported-any / check:dual-source-exports / check:entry-nameability / check:browser-reachable-entries / check:generated all exit=0 (no generated artifact moves); check:error-code-provenance exit=0; check:nul-bytes exit=0 (8541 text files) plus a manual control-char grep over both edited files, no hits. DERIVED GATE SWEEP: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, all 78 families run, reconciled with --ran carrying exit codes: 75 exit=0, 3 exit=3 NOT MEASURED -- check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure, each printing PREREQUISITE NOT MET for a repo-wide build this diff does not otherwise need (CI's Build Core supplies it). Recorded as NOT MEASURED, never as green. CLAUSE 2: node scripts/pm/check-widening-tells.mjs --declaration no --diff exit=4 with 'T4 packages/spec/src/api/error-code-ledger.zod.ts:1084 - a new registration in a registry / catalog'; --declaration yes exit=0. node scripts/pm/check-clause2-carriers.mjs --pair 17924 exit=4, two rows, BOTH the card half: C1 the card lacks needs:contract-review (the PR carries it), C5 the claim comment still declares Clause-2: no. Post-run tree clean (git status empty).", "mcp_calls": "0 - every GitHub read and write went through repo-scoped REST (probe 200) and git; MCP GitHub was not called at all, the dedup search included (no card was filed)", "open_questions": [ { "question": "Is a SECOND provenance row for an already-registered code really a Clause-2 widening? Declared yes because the mechanical judge says so, but nothing an implementation may emit actually changed.", "options": [ "A - leave the yes standing: the ledger header says 'Registering a code widens this face and is therefore a Clause-2 change, door or no door', check-widening-tells fires T4, and the gate's exit-0 condition is 'no tell OR the declaration is not no'. Cost: a minor changeset and a contract review on a diff whose accept set is byte-identical.", "B - declare no with a Contract-text citation of the header sentence 'A code emitted by several packages is listed once per emitting package - the union dedupes; the per-package rows are provenance, not identity'. Cost: the enqueue gate blocks on a non-zero --pair exit until someone overturns the tell, and the reference explicitly calls the false-declaration route the wrong repair.", "C - keep yes here and file a separate card to teach T4 the difference between a NEW code and a second owner row for an existing one. Cost: one more card; the matcher is the reference's own prescribed place to repair a demonstrated false positive." ], "recommendation": "A for this PR (it is landed that way), with C as the durable fix if the seat agrees the tell is over-broad. Not filed from here: this is a governance-tool judgement, not one of the three fileable classes, and the gate's own header records false positives as the accepted cost." } ], "out_of_scope_findings": [ "noted, not filed: check-widening-tells' T4 cannot distinguish a NEW error-code registration (which widens the published union) from a SECOND provenance row for a code already in it (which does not), so a provenance-only diff is forced to a conservative Clause-2 yes and a minor changeset. Successor who hits this file: the next PR adding a second-emitter row - the shape already exists in the tree, e.g. plugin-security's CONTROLLED_BY_PARENT row beside metadata-protocol's.", "noted, not filed: the REST approvals door's success path serves decide/recall with res.json(out), so ResumeFailureReport.code reaches the wire with no door-side validation that the string is a registered ErrorCode at all - the guard checks provenance, never that the value a producer stamps is in the union. Not a defect on any measured path here (the type is ErrorCode) and no reproduction exists. Successor: whoever next widens ResumeFailureReport or adds a third producer." ] }Generated by Claude Code
os-bill commentedon Sep 13, 2026
Claim: session_01MkQhmuuJAVDjmeWNixwDDH · branch claude/issue-17909-resume-failure-code-provenance
Branch: claude/issue-17909-resume-failure-code-provenance
Clause-②: yes
⭐ 取代性认领 —— 唯一目的是把卡侧声明从
no改成yes。 ⛔ 不是第二次认领,⛔ 不改 assignee、不改分支、不改轮次归属。本条取代先前认领5651052097的Clause-②: no一行,其余一切照旧。domain:spec执行席,2026-09-13T05:1xZ。先前那条声明是本席写的待证伪前提(「两条备选补法都落在数据表里 ⇒ 应为
no」)。轮次实测把它推翻了:⇒ 机械判子说
yes,且台账自己的 header 写着「Registering a code widens this face and is therefore a Clause-② change, door or no door」。⛔ 本席不越过机械判子。needs:contract-review已挂 PR 与卡两侧(卡侧 05:1xZ 写入回读MATCH True)。⭐ 轮次把本席的一条前提证伪了 —— 记在这里
本席在派发令里写:
RESUME_IN_PROGRESS(台账:1044)「在另一个 owner 块,本席未读是哪个 —— 待证伪前提」。轮次读了,是'@objectstack/service-automation'。本席独立复核确认:⇒ 标为「待证伪前提」是对的做法;若当时把它写成亲取读数,就会把一条错事实带进派发记录。
三个码,只有一个欠补 —— 这是本卡真正的形状
RESUME_FAILEDapproval-service.ts:3370(PR #17908 新增)RESUME_TARGET_LOST:3189以消息前缀抛出,rest 的 catch 映射该前缀)RESUME_IN_PROGRESSengine.ts:5795/:6165,已列:3304只做比较)⇒ 卡面的「一个码」框架与本席派发令的「三个码都要查」都不完全对:面是三个,欠补的是一个。
⭐ 路线 A(加行)而非 B(waiver),是按可测判据选的
派发令写死:若结论是 waiver 就停手报告,⛔ 不自行实施。轮次没有触发那一条 —— 它用先例
EXTERNAL_IMPORT_ERROR自己的判据(「门自己 stamp 且从不读生产者的声明」)去测,发现 stamp 点在生产者侧,方向与先例相反 ⇒ 加行。⛔ 不是口味,是测量。轮次补上了卡面自报的 NOT MEASURED
卡面说守卫在裸工作树答
tsx: not found⇒ 因果链是从守卫的规则推的,不是守卫的输出。轮次装好依赖跑出了守卫自己的拒收文本:补行后 exit 0,
318 → 319 listed(admitted as LISTED, not waived)。另有三腿:去掉行 ⇒ 同一站点再红;代价方向(同包 stamp 另一个 resume 码)⇒ 红并点名RESUME_TARGET_LOST,说明这一行恰好只放行那一个码;#15970 覆盖腿(同包两处RESUME_FAILED站点)⇒ exit 0,因为对账是按 (package, code) 而不是按行 ⇒ 一次修好确实解锁两张卡,这是实测不是断言。第二条 provenance 行(码本身早已注册)到底算不算条款②扩宽?轮次荐 A(照现状,已落地)+ C(另立卡教 T4 区分「新码」与「已有码的第二个 owner 行」)。⛔ 本席按 A 保留声明,C 另立裸卡。
Generated by Claude Code
os-bill commentedon Sep 13, 2026
收口 —— PR #17924 已合并,按内容在
origin/main上核实domain:spec执行席,2026-09-13T06:4xZ。维护者直派「17909 插队」的产出,一轮交付。合并84e6b05b6d2,merged2026-09-13T06:41:43Z。卡由关闭关键字自动关闭(completed);本席同笔摘pm:dispatched,domain:spec留下。⇒ 一码两行,正是台账 header 说的「a code emitted by several packages is listed once per emitting package — provenance, not identity」。
它解锁了什么
PR #17908(#15556,另一席) —— 在档复核读了那张 PR 的失败 job(
103643973971),第 #103 步「Error-code provenance guard」的拒收文本与本轮复现的 L1 逐字一致;补行后的 L2 exit 0 ⇒ 对那一步是充分的。never_ran=70),仍是未测量,不是通过。⇒ 本卡不宣称「#17908 已解锁」,只宣称「那一步的拒收原因已消除」。#15970 —— 覆盖是实测的,不是断言:对账按 (package, code) 而非按行(⚠️ 该卡分支目前还没有实现可测(head 是 2603 文件之前的旧 main),所以覆盖是条件性成立。
check-error-code-provenance.ts:98,133),复核的覆盖腿放了两处RESUME_FAILED站点进去,exit 0、320 listed。本轮把两个「面」的错都纠正了
RESUME_TARGET_LOST以模板字符串抛出(门禁 header 自陈不扫模板),由 rest 的 catch 映射前缀 ⇒ rest 现有行成立;RESUME_IN_PROGRESS的 owner 是@objectstack/service-automation。:1044属于哪个 owner 块,标了而不是断言。这是本班第五次同类风险,也是第一次因为标注而没有变成错误。加行而非 waiver,是按可测判据选的
派发令写死:若结论是 waiver,停手报告,⛔ 不自行实施。轮次没触发那条 —— 它用先例
EXTERNAL_IMPORT_ERROR自己的判据(「门自己 stamp 且从不读生产者的声明」)去测,发现 stamp 在生产者侧,方向相反。在档复核复核了这条,并补了一个本席和轮次都没读到的读数:rest 的门确实也 stamp 这个码 —— 但只在 throw 路径,而ResumeFailureReport走的是成功路径(res.json(out)逐字下发,门不读不改不盖章)⇒ 先例的判据在这条路径上不成立,waiver 没有落脚点。A 正确。守卫的拒收文本已复现(卡面那半 NOT MEASURED 已补)
卡面自陈:守卫在裸工作树答
tsx: not found⇒ 因果链是从规则推的,不是守卫的输出。轮次装好依赖跑出了真文本,补行后318 → 319 **listed**(⛔ 不是 waived);代价方向腿(同包 stamp 另一个码)红并点名RESUME_TARGET_LOST⇒ 这一行只放行那一个码。条款②
轮次声明
no,机械判子check-widening-tellsT4 fire(--declaration noexit 4 /--declaration yesexit 0)。本席发取代性认领翻成yes,⛔ 不手工推翻机械判子。yes—— 本 diff 没有注册任何码,REGISTERED_ERROR_CODES联合体逐字节不变(check:api-surface实测 unchanged)。真正支持yes的是:台账是as const satisfies …,所以导出常量的类型多了一个 tuple 成员 —— 一个微小但真实的公开类型变化。结论相同,理由更正。⇒ T4 过宽这件事已立卡 #17926(并已更正本席在那张卡上写错的先例名:真名是
INVALID_METADATA,⛔ 不是不存在的CONTROLLED_BY_PARENT)。档位
在档复核 served 86/86
claude-fable-5-1,本席从转录亲核;对照一份 dev 转录读回claude-opus-5⇒ 仪器可区分。裁定 ACCEPT-WITH-NOTES 并明写清除双载体。载体于 06:09:12Z / 06:09:14Z 相隔两秒两次移除 —— 合法清除的签名。遗留,已立卡
#17929 ——⚠️ 卡上写明调用点人口未测。
ResumeFailureReport.code是必填,而该类型的 docblock 逐字邀请调用方用ResumeFailureDetailsSchema解析这个成员,而那个 schema 不声明code⇒z.object静默剥掉。文档指的路把文档自己的论点抹掉。ready_for_review连回 4 次 HTTP 503(GitHub 自陈 "temporarily unavailable"),回读draft=True⇒ 没翻成。读 API 与 rate_limit 均 200、代理健康 ⇒ 上游写路由的临时故障。第 5 次 200 且回读draft=False才继续。⭐ 回读救了一次:只看 HTTP 码会把 503 之后的流程全跑在一个没翻牌的 PR 上。另:arm 之后 11 秒读队列 ref 为空,本席实时重读才看到
pr-17924-dbea1756d9ba…。⇒ 取数时机不对的读数,同样不是读数。Generated by Claude Code
fix(spec): register plugin-approvals as a second emitter of RESUME_FA…
fix(approvals): recall tells its caller WHICH resume failure stranded…