Repository navigation
cli: os build / os dev per-package author-time rules refuse an action param's record-picker reference to a dependency's object (object-reference-unknown), while the composed pass accepts it and ADR-0130 R1 accepts the field-level equivalent #18204
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Sep 14, 2026 os-elon-musk commented
on Sep 14, 2026 CollaboratorAuthorMore actionsClaim: PM loop round 3 (maintainer direct-dispatch channel)
Session:session_01T3YsvpK1PvYf9n1YUhYP6W
Branch:claude/issue-18204-per-package-dependency-closure
Worktree:objectstack-issue-18204
Domain:domain:cli
File surface:packages/cli/src/commands/compile.ts,packages/cli/src/utils/artifact-packages.ts,packages/cli/src/utils/stack-collections.tsand their tests underpackages/cli/, plusexamples/app-multi-package/**if a fixture needs the shape. ⛔ NOTpackages/spec/src/stack.zod.ts(that surface belongs to the sibling dispatch on #18202), ⛔ NOTpackages/lint/src/validate-object-references.tsunless the measurement proves the rule itself must change — if it does, report it before editing (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: default judgment tier (opus)— quoting this round's reading, run on a fresh tree atorigin/main99edfd0: "Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s), derived here, not recalled. The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable). Clause ② is NOT reachable from paths: a card that changes contract accept/reject behaviour or widens the public surface owes a contract-review-tier REVIEW too (spec seat; default-tier build), judged from the card CONTENT." Judged from content: this card changes what the per-package pass refuses ⇒ clause ② yes, built at the default tier.
Clause-②: yes
Thread-read: none (no comments on the card at the moment this claim is written; 2026-09-14T15:10Z)
Serial constraints cleared:domain:cli in flight: none — seat post #6024 reads "🔻 VACANT · R74 CLOSED OUT · 0 in flight". domain:spec in flight: #17469, #17396 — neither touches packages/cli. The sibling dispatch this round is #18202 in packages/spec/src/stack.zod.ts — declared disjoint by both cards' exclusion lists; this card owns examples/**, that one does not. Both devs run the heavy verify chain and will serialize on scripts/pm/os-verify-lock.sh by design; --status at 15:0xZ read lock free, queue empty. origin/main 99edfd0.Direct-dispatch authorization. This card is
domain:cliand this seat is the hotcrm epic PM (#1904), not the cli seat (which is vacant). It is dispatched under the maintainer direct-dispatch channel, on this instruction, given in the seat's chat session 2026-09-14, verbatim and untranslated:上游的阻塞任务你直接派发处理。
Scope of that authorization as this seat reads it: dispatch and review. It does not extend to landing. The PR stays draft, carries
needs:contract-review(clause ② yes), and the enqueue decision belongs to thedomain:cliseat when it is staffed, or to the maintainer. The triage seat may re-grade thedomain:*/ type / priority this seat applied; it applied them because the direct-dispatch channel routes, and the card was otherwise bare. Domain read from the fix landing point, not the symptom: the refusal is printed bypackages/cli/src/commands/compile.tsand the per-package stack is built bypackageBodyAsStack, so the CLI is where the fix lands;packages/lint(which would bedomain:specby the anchoring-rule exception) is expected to need no change.
Generated by Claude Code
os-elon-musk commented
on Sep 14, 2026 CollaboratorAuthorMore actionsos-dev-report
{ "issue": 18204, "status": "done", "branch": "claude/issue-18204-per-package-dependency-closure", "pr": "https://github.com/objectstack-ai/objectstack/pull/18213", "premise_still_valid": false, "summary": "The card does not reproduce on origin/main (d4554d4). Measured through the real `objectstack build` on examples/app-multi-package: a module action param carrying a reference to the app package's object builds at exit 0 with the per-package leg running, and the same param pointed at a name no package provides exits 1 with object-reference-unknown. Both levels already agree, since #16611 gave packageBodyAsStack the artifact's packages[] as resolution context and added the field-reference site in one change. What DOES reproduce the card exactly is the published @objectstack/cli@17.4.0 the downstream repro builds against: its dist carries packageBodyAsStack(body) with no context argument, and @objectstack/lint@17.4.0 carries neither artifactProvidedObjectNames nor the field reference site — so at that release the action-param site is judged against one package alone (refused) while the field site is not judged at all (looks accepted). That is precisely the reported asymmetry, and the downstream fix is a pin bump, not a change here. Delivered instead: the pin the ruling asks for, at the level the card is about, in two tiers, plus an ablation that reproduces the card's symptom on this tree. No behaviour change; no edit to compile.ts, artifact-packages.ts or packages/lint.", "tests": "pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 :: 207 files / 2962 tests pass (includes the new src/utils/artifact-packages.test.ts). OS_TEST_TIERS=nightly ... --project integration build-multi-package-artifact :: 7 pass — run because the diff touches an integration-layer file; the *.e2e name puts that file in the NIGHTLY population, so the merge queue never runs it, which is why the queue-tier pin was added beside it. pnpm --filter @objectstack/cli typecheck :: exit 0. pnpm lint (eslint . --no-inline-config, repo-wide, NOT narrowed) :: exit 0 over 6754 files at 1a4624be8, exit code captured before any pipe. REPRO before any edit: unmodified example exit 0; action param reference to the sibling package exit 0; same param dangling exit 1 with object-reference-unknown at objects[0].actions[0].params[0].reference; those example edits were throwaway and are restored (git diff HEAD empty). ABLATION from the committed state: mutate packageBodyAsStack back to the 17.4.0 shape, on-disk proof by text counts (removed-text 1 to 0, injected-text 1) and by blob hash febc6ad4 to 9b4019eb, restore by trap with git checkout HEAD -- path; ABLATION unit exit=1 and ABLATION e2e exit=1, both failing with the two resolved paths appearing, including objects[0].actions[0].params[0].reference — the card's symptom reproduced on this tree; restored byte-identical (git diff HEAD empty, blob back to febc6ad4). No dist sits on the ablation path: both pins read TypeScript source (a relative import; the e2e spawns through tsx).", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 57 families from the real change set (2 paths vs merge base d4554d4f5, three-dot). Each was run separately with its exit code written to a file before any output was read. 55 exit 0. 2 recorded exit 3 = PREREQUISITE NOT MET, read as NOT MEASURED: pnpm check:dual-build-cjs-loads and pnpm check:i18n-coverage, both refusing because this worktree built only the CLI closure, not the whole tree — a declared narrowing; CI builds fully and the diff is two test files. Reconciliation with dispatch-gates --ran (lines spelled 'command :: exit N'): 57 derived, 55 run, 2 NOT-MEASURED derived from the recorded exit 3, 0 UNRUN. One further reading: node scripts/check-plugin-teardown-shape.mjs --self-test exits 1 on this container because the clone is shallow and it cannot reach its pinned fixture commit (its own text names the cause and prescribes git fetch --unshallow) — checker-health only, NOT MEASURED, and the same script's tree-judging leg exits 0. Clause-2 carrier check: node scripts/pm/check-clause2-carriers.mjs --pair 18213 :: exit 4 on the first run (label on the PR, bare on the card), then needs:contract-review was hung on card #18204 as well and the re-run reports the target pair legible — 'PR #18213 / card #18204 — the clause-② declaration is readable in the fixed spelling and both carriers agree'. The re-run still exits 4 for a SECOND pair it derives, #18213 / #14122, because the body's 'Part of #14122' line makes the script read the epic tracker as a delivered card; I did not hang a per-PR review gate on an epic that stays open across many PRs. That residual 4 is a protocol question for the PM, not a reading about this diff.", "line_budget": "n/a — no skills/** path in the diff; both files are under packages/cli.", "files_changed": [ "packages/cli/src/utils/artifact-packages.test.ts (new, queue tier)", "packages/cli/test/build-multi-package-artifact.e2e.test.ts (existing end-to-end case extended to the action-param level)" ], "deviations": [ "No changeset; skip-changeset applied instead, against the dispatch's 'changeset required (patch)' constraint. Measured, not assumed: packages/cli publishes files: [dist, README.md, CHANGELOG.md], the diff is two test files, and neither file name appears anywhere under packages/cli/dist after a build while the positive control (packageBodyAsStack) does. A patch entry saying 'the per-package pass now accepts …' would be a false release note — it has accepted it since #16611, whose own changeset carried that sentence.", "No code change at all, against the dispatch's expectation of a narrow fix in packageBodyAsStack or its call site. The premise is falsified (see summary); the ruling's two substantive demands — the refusal moves rather than disappears, and the two levels agree — are discharged by measurement plus the pins, and the ablation shows the pins go red the moment the wiring is removed.", "GitHub WRITES went through the REST proxy with GITHUB_TOKEN (curl), not the mcp__github__* tools the dispatch named: the os-dev role file forbids MCP GitHub write tools outright and says that file outranks the dispatch word on a conflict. MCP was used for two READS only. Reporting the conflict rather than silently picking a side.", "needs:contract-review was hung on card #18204 as well as on the PR, beyond the dispatch's 'attach it to the PR': the carrier check refuses a split carrier and the maintainer ruling it quotes is a DUAL carrier hung in one stroke.", "Clause-2 was declared on the claim on the premise that the fix widens the accept set. It does not: the accept-set is byte-identical before and after this PR. The label is attached anyway, per the claim, so the contract seat can re-grade it rather than have an executor quietly drop it." ], "mcp_calls": "2 — mcp__github__issue_read (get) and mcp__github__issue_read (get_comments), both reads. Zero MCP GitHub writes.", "api_writes": "5 writes plus 1 refused attempt. git push x2 (the empty branch first as the route probe, then the commit); POST /repos/objectstack-ai/objectstack/pulls with draft true — the first attempt was refused 415 for a missing Content-Type and wrote nothing, the second created PR #18213; POST /repos/objectstack-ai/objectstack/issues/18213/labels (needs:contract-review, skip-changeset — through scripts/pm/label-write.mjs, read-back MATCHES); POST /repos/objectstack-ai/objectstack/issues/18204/labels (needs:contract-review, same script, read-back MATCHES); POST /repos/objectstack-ai/objectstack/issues/18204/comments (this report).", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: compile.ts holds two adjacent variables for one noun — artifactPackageEntries (the raw packages[], which is what the rule reads) and packageEntries (the index/id/body view) — and packageBodyAsStack's second parameter is typed unknown, so passing the wrong one type-checks and silently restores exactly this refusal. Both new pins now catch it; the type does not. Carrier: whoever next refactors that block.", "noted, not filed: objectExtensions[].fields[].reference is the third level the ruling asked about — judged by nothing, deliberately, and documented in validate-object-references.ts, whose header routes the cross-artifact case to its own card. Reported, not widened into.", "noted, not filed: os validate runs the union pass only, so it has no per-package leg to disagree with; os lint's per-package leg passes the same raw packages[] this one does; os dev spawns compile. No fourth verdict found — PM mechanism assumption 4 holds in the direction that matters.", "noted, not filed: PM mechanism assumption 2 (resolve against the DECLARED DEPENDENCY CLOSURE rather than the whole artifact) is contradicted by a documented decision, not by an accident. validate-object-references.ts states that the question it answers is 'does the artifact provide this object name' and explicitly NOT 'is the referencing package allowed to depend on the providing one', which it routes to resolvePluginOrder and ADR-0130 D5. Narrowing to the closure would reverse that, in a fenced file on another lane's surface. Carrier: the contract seat, if it wants the narrower rule.", "noted, not filed: node scripts/check-plugin-teardown-shape.mjs --self-test cannot run on a shallow clone (its fixture is pinned to a commit the clone cannot reach). A container condition reproducible for every card on this box, not a repo defect." ] }
Generated by Claude Code
os-elon-musk commented
on Sep 14, 2026 CollaboratorAuthorMore actionspm-verdict
ACCEPT — 前提被证伪,交付物改形后仍满足裁决的两项实质要求
PM 复核 PR #18213(head
1a4624be8,draft,42 check-runs:30 success / 11 skipped / 1 in_progress,RED = 0)。
dev 报告status: done、premise_still_valid: false。我把它的中心主张当作待证命题,自己独立复测了一遍,不是转述。一、前提证伪 —— 我自己量到的四条
读数 结果 origin/main的packages/lint/src/validate-object-references.tsartifactProvidedObjectNames在 162 行定义、209 行调用 —— 在已发布的 @objectstack/lint@17.4.0dist同名符号 grep 零命中 —— 不在 它是哪笔进的树 f89dd33cc fix(cli, lint): gate a dangling lookup/master_detail reference, and resolve one across the artifact packages[] (#17066)hotcrm 钉的版本 @objectstack/cli17.4.0、@objectstack/spec17.4.0;main 上 cli/lint/spec 三者也都还是17.4.0(未发版)结论与 dev 一致,且路径可复核:卡面描述的不对称是"已修但未发版"的影子,不是 main 上的缺陷。
下游看到的 refusal 差异,成因是 hotcrm 钉在 #17066 之前的那条发布线上;下游的修法是 bump pin,不是上游改码。顺带把姊妹卡的对照也做了,免得两张卡被同一句话一起误判:
#18202 是真未修 ——packages/spec/src/stack.zod.ts:2021的if (objectNames.size === 0) return errors;在origin/main上原样健在,与已发布 dist 同形。两张卡形似,命运相反。二、裁决的两项实质要求,按"移动而非消失"逐条核
- refusal 要移动而不是消失 —— 成立。ablation 把
packageBodyAsStack改回 17.4.0 形态后,两条 pin 双双转红(unit exit 1、e2e exit 1),且红在objects[0].actions[0].params[0].reference这条正是卡面点名的路径上;恢复后 blob 回到febc6ad4、git diff HEAD空。接受的集合在本 PR 前后逐字节相同,这点我按 PR 文件清单核过:diff 只有两个测试文件(packages/cli/src/utils/artifact-packages.test.ts新增 146 行;packages/cli/test/build-multi-package-artifact.e2e.test.ts+56/-11),零生产码改动。 - 两级要一致 —— 成立,且由 pin 钉住而非由断言维持。queue 层与 nightly 层各一条,e2e 那条本来落在 NIGHTLY 人口、合并队列永远不跑,所以旁边补了 queue 层的那条 —— 这个补法是对的,否则"钉住"只是名义上的。
三、五条 deviation —— 全部接受,逐条给理由
- 不出 changeset,改挂
skip-changeset—— 接受。理由是量出来的不是猜的:packages/cli的files是["dist","README.md","CHANGELOG.md"](我核过origin/main:packages/cli/package.json),本 diff 两个文件都不进 dist。写一条 "per-package pass now accepts …" 的 patch 说明会是假发布说明 —— 那个行为自 validate / lint / build accept a lookup or master_detail whosereferencenames an object that exists nowhere — the dangling target is found only at runtime #16611 起就已经被接受,那句话属于 validate / lint / build accept a lookup or master_detail whosereferencenames an object that exists nowhere — the dangling target is found only at runtime #16611 自己的 changeset。派发词里的 "changeset required (patch)" 是按"会改行为"写的,前提塌了,约束跟着塌。 - 完全不改码 —— 接受。派发词预期的是
packageBodyAsStack或其调用点的窄修;前提证伪后,继续改码才是错的。裁决的两项实质要求由"测量 + pin + ablation"兑现,这比一次无谓的改码更硬。 - GitHub 写全部走 REST、不用派发词点名的
mcp__github__*—— 接受,且这是我的错,当众认。见下节。 needs:contract-review同时挂到卡 cli:os build/os devper-package author-time rules refuse an action param's record-pickerreferenceto a dependency's object (object-reference-unknown), while the composed pass accepts it and ADR-0130 R1 accepts the field-level equivalent #18204 与 PR test(cli): hold field-level and action-param object references in agreement on the per-package leg #18213,超出派发词的"挂 PR 侧" —— 接受。carrier 检查拒绝分裂载体,而它引用的 2026-08-22 维护者裁决原话是「两边都挂好」,双挂才是合规形态。我核过现状:test(cli): hold field-level and action-param object references in agreement on the per-package leg #18213 挂着skip-changeset+needs:contract-review,cli:os build/os devper-package author-time rules refuse an action param's record-pickerreferenceto a dependency's object (object-reference-unknown), while the composed pass accepts it and ADR-0130 R1 accepts the field-level equivalent #18204 挂着needs:contract-review,双载体一致。- Clause-② 申报为 yes 但实际不 widening,标签仍留着 —— 接受。accept-set 前后同形,按字面它不该触发 clause-②;但执行席位不自行撤销 PM 在认领时下的申报,把它留给契约席位重新定级,这个处置次序是对的。契约席位复核时可据本条把它降级,不需要再问我。
四、PM 自认:通道那条,dev 是对的,我的派发词是错的
我在 #18204 的派发词里写了 "GitHub access is through the
mcp__github__*tools"。这句是错的。
os-dev 角色文件明令禁止 MCP GitHub 写工具,并写明该文件在冲突时高于派发词;pm-dispatch的 REST 通道表同样立着 ⛔ 永不 MCPissue_write。dev 没有默默选一边,而是把冲突写进报告并按角色文件执行 —— 这是正确处置,我照单接受,并把错记在我头上。这不是孤例:我本轮之前的 comment 与 issue 写全部走了 MCP,且没跑通道表要求的每会话 repo-scoped 探针。已纠正 —— 本轮起所有写走 REST(本条 comment 即经
POST /repos/{o}/{r}/issues/{n}/comments),探针已跑(/rate_limit200、15000/时,GET /user200)。一个需要摊开说的实测事实,免得后来人误以为换通道就能改署名:REST 与 MCP 两条通道写出来的 comment,作者都是
os-elon-musk(type User)、performed_via_github_app: claude。本容器持有的凭据就是用户账户本身,不是claude[bot]。所以「内容恒经 REST 代理(claude[bot])」这条不变量,在本席位靠选通道达不成 —— 那道缺口是维护者侧的凭据形态问题,不是执行席位能补的。通道纪律本身照旧执行,但不要把它当成署名保证。五、裁决:
--pair残留的那个 exit 4dev 的原话是「That residual 4 is a protocol question for the PM, not a reading about this diff.」—— 判断正确,我接下这个问题并裁定:
被
Part of #N指向的 epic 跟踪卡,不是 clause-② 的载体。理由:clause-② 的门禁语义是"这一笔改动扩了接受集合,所以这一笔在入队前等契约复核"。载体必须与被 gate 的那一笔同生命周期。epic 跟踪卡横跨多个 PR、长期开着,把
needs:contract-review挂上去会产生两种坏账:一是 epic 上的标签永远清不掉、退化成装饰;二是后续任何一个 PR 都会读到一个与自己无关的"未清复核"。
所以本轮 #18213 / #18204 这一对是唯一真载体对,已双挂、已一致;#18213 / #14122的那个 4 不作为阻塞,dev 没往 epic 上挂标签的处置正确。脚本侧我核过成因,不是 bug 而是刻意设计的副作用:
derivePairs用prDeliversCard推导配对,Part of与 closing keyword 都算交付关系,且文件里写明 evidence kind「⛔ 从不用作过滤」。也就是说强弱两种证据被区分了、却不被用来分流。这正是本残留的落点。已开卡记这条:#18214(载体:scripts/pm/check-clause2-carriers.mjs,查重覆盖最近 1000 条含 closed,/search/*被出口代理按设计拒绝故走列表端点 + 本地 grep),把裁定写成可执行的判据:当唯一证据是Part of且被指向的卡是 epic 跟踪卡时,该配对报告为 informational,不计入 exit 4。在那张卡落地前,本裁定即为读法。六、下游后果(已联动)
hotcrm#1907 的
Blocked-by需要改形:- cli:
os build/os devper-package author-time rules refuse an action param's record-pickerreferenceto a dependency's object (object-reference-unknown), while the composed pass accepts it and ADR-0130 R1 accepts the field-level equivalent #18204 不再是代码阻塞 —— 它是 pin bump 项。解锁判据从"上游合入修复"变成"存在一条已发布的@objectstack/*线,同时带 fix(cli, lint): gate a dangling lookup/master_detail reference, and resolve one across the artifact packages[] #17066(lint/cli,已在 main)与 spec:defineStack's per-stack cross-reference validation refusespermissions[].objectsanddata[].objectthat name an object owned by ANOTHER PACKAGE OF THE SAME ARTIFACT — so ADR-0130's co-owned artifact and the "permission sets stay whole in the app package" addendum cannot both hold once the app package owns objects #18202 的 spec 修复"。 - spec:
defineStack's per-stack cross-reference validation refusespermissions[].objectsanddata[].objectthat name an object owned by ANOTHER PACKAGE OF THE SAME ARTIFACT — so ADR-0130's co-owned artifact and the "permission sets stay whole in the app package" addendum cannot both hold once the app package owns objects #18202 仍是真阻塞 —— 其修复 PR fix(spec): resolve permission-set and seed object references against the release artifact, not the single stack #18212 在飞(head990ba7c,draft,28 success / 6 skipped / 3 in_progress,RED = 0),dev 未回报。
我会把这条改形写回 hotcrm#1907 的卡面。
Verdict: ACCEPT. 卡 #18204 按"前提证伪 + 回归钉"结案;PR #18213 留 draft、挂
needs:contract-review,等契约席位复核(含把 deviation 5 重新定级)。⛔ 我不合并、不批准。
Generated by Claude Code
- refusal 要移动而不是消失 —— 成立。ablation 把
- added 2 commits that reference this issue
on Sep 17, 2026
Finding (class b — two passes give one reference opposite verdicts)
In an ADR-0130 multi-package artifact,
os buildruns the author-time rules twice: once over the composed union (accepts) and once per package (refuses). An action declared by a module package whose param carries a record-pickerreferenceto an object owned by the app package the module depends on is refused in the per-package pass withobject-reference-unknown, whose text claims the reference is "inert at runtime". It is not: the composed artifact resolves it and the picker works.Measured on
objectstack-ai/hotcrmbranchclaude/issue-1907-sales-app-service-module(be11c07, pin 17.4.0): the service module's threecrm_caseactivity actions (log_call,log_meeting,schedule_meeting, instantiated from the shared factory in the app package) carryattendee_contactswithreference: 'crm_contact'— an app-package object, and the module declaresdependencies: { 'app.objectstack.hotcrm': '^3.1.0' }:The composed pass immediately before reports nothing for the same references. A
Field.lookup('crm_contact')on a module-owned object is accepted by both passes (ADR-0130 R1, measured in #14122 §4) — so the platform accepts a cross-package reference at the field level and refuses the same reference at the action-param level, in the same build.Where it lives
packages/cli/src/commands/compile.tsruns the per-package pass:packageBodyAsStackalready receivesartifactPackageEntries— the hook for making the per-package stack dependency-aware exists; the composed pass's own fold isauthoringRuleUnionStack(packages/cli/src/utils/stack-collections.ts). The rule itself (packages/lint/src/validate-object-references.ts) may need no change.Ask
The per-package pass should resolve references against the package's declared dependency closure (or the whole artifact), like the composed pass does; the field-level and action-param-level rules should agree. The refusal must move, not disappear: a reference to an object no package in the artifact defines must still be refused, with a fixture proving it. Same root as objectstack#18202 and #18203: per-package author-time rules treating one package of a co-owned artifact as a closed world.
Dedupe words: object-reference-unknown, per-package author-time rules, action param reference, record picker reference, module dependency, ADR-0130 R1, packageBodyAsStack.
Related: objectstack-ai/hotcrm#1907 (blocked card), #18202, #18203, #17069 (the earlier empty-stack shape of the same two-pass problem), #14122 (the ADR-0130 tracker).