Repository navigation
Description
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk
Ruled: 6566731522 · letter A · 2026-11-02T01:15Z
Ruling-ref: 6566731522
Filed by the PM seat for the W3-RT runner session_017xRBcRX4wwwVw5bEckffdp, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).
QA-source: #28656 · approvals.dynamic-approver-routing · observed beside the clauses
What happens
showcase_dynamic_approval stage 1 declares a required decision output next_reviewers (type: user, multiple: true). Stage 2 resolves an expression approver over it with onEmptyApprovers: 'fail'. The decision validation checks only that the output is present and the right shape, not that the ids resolve to users. As a result:
- Unknown id → 500 after recording. As admin,
POST /api/v1/approvals/requests/STAGE1_ID/approve{"outputs":{"next_reviewers":["usr_does_not_exist"]}}→ HTTP 500{"code":"RESUME_FAILED","error":"the approve decision was recorded on request …, but its flow run '…' could not be resumed and is now stranded: … NO_APPROVERS: approval node 'co_sign' … resolved to no concrete approver and its onEmptyApprovers policy is 'fail'…"}. The stage-1 request then readsapproved, with anapproveaction row appended, and the run readsfailed. Reproduced twice. - Bare string for a
multiple: trueoutput.{"outputs":{"next_reviewers":"USER_ID"}}is accepted and stored as a scalar (variables['lead_review.next_reviewers'] = "USER_ID", not a list). Stage 2 still resolved. - Duplicate ids.
["ADMIN_ID","ADMIN_ID"]creates two co_sign slots for one user.
Expected, in the spirit of the checklist's negative[0] for this item ("refused at the decision, not at the crash site"): an id that resolves to no user, a scalar for a list output, and duplicates are refused at the decision with a 4xx naming the output, and nothing is recorded.
Measured at framework 6e11a929 (main, 2026-11-01), on a stock showcase boot.
Reproduction
- Boot the stock showcase as admin. Retitle a
showcase_announcement(PATCH /api/v1/data/showcase_announcement/ID{"title":"…"}). That startsshowcase_dynamic_approval, and the stage-1lead_reviewrequest lists the admin as approver. POST /api/v1/approvals/requests/LEAD_REVIEW_ID/approve{"outputs":{"next_reviewers":["usr_does_not_exist"]}}→ 500 RESUME_FAILED.GET /api/v1/approvals/requests/LEAD_REVIEW_ID→ statusapproved.GET /api/v1/automation/showcase_dynamic_approval/runs/RUN_ID→failed.
Mechanism
Required-output validation in the approvals decision path (plugin-approvals: decision-output checks before the resume) validates presence and type shape. It does not resolve type: user values against sys_user, and does not enforce multiple or uniqueness. So caller-fixable input reaches the resume, and the stage-2 onEmptyApprovers: 'fail' policy turns it into a 500 strand after the stage-1 decision is persisted. The same 500-after-record class as #26477, but caused by unvalidated input rather than a deleted flow.
Activity
objectstack-fleet commentedon Nov 1, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 审批与自动化 | none | P②
Triage: p2 ·
bug·domain:server·area:workflow·pm:queue· 2026-11-01T12:56Z⛔ Not a claim.
userdecision output is checked for shape only. An id that resolves to no user records the decision, then answers 500RESUME_FAILEDand strands the run. A bare string is stored for amultiple: trueoutput. p2.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Claim: PM loop round 1 · card thread · 2026-11-01T13:22Z
Session:
session_01FEkQ5oc56xChD6Wrc4cGSt(the seat); the card thread issession_01FEkQ5onyCFb1sK8Uvu76U7Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-v6mwdv(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z) with the triage grade as filed; split among the server seats agreed in the project (seat 1 takes #28564, seat 5 takes #28565 #28661 #28658 #28663 #28665).
File surface:
packages/plugins/plugin-approvals/src/approval-service.ts(the decision-output validation block before any write) and its tests;packages/rest/src/approvals-routes.tsonly if the refusal mapping needs it; a@objectstack/plugin-approvalschangeset. ⛔ Nopackages/runtime/**,packages/spec/**orui/**.Container & model:
M,mode:thread, model and effort as the thread was openedClause-②: no
Metadata-protocol: none
Responsibility: the code is
@objectstack/plugin-approvals' decision-output validation · no platform path covers it today (stage 2'sonEmptyApproversturns the input into a 500 strand) · reached by any approver deciding a node with a typeduseroutput, used today byshowcase_dynamic_approvalThread-read: 6558249277
Premise: holds (read at
origin/main2678600d: the decision path checks reserved, undeclared and missing-required outputs only; its comment callstype/multiplewidget-only, which the order makes the dev verify against the spec first)Serial constraints cleared: #28657 (seat 1) changes
packages/runtime/src/domains/actions.ts, the action door that forwards to the approvals routes; this card stays in plugin-approvals. No other in-flight claim namesapproval-service.ts.Resume-from: none — fresh from
origin/main2678600dState:
pm:queue→pm:dispatched, assigned toos-project-manager, recorded by this claim. Order:pm-orders/server/order-28658.mdin the project.objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5onyCFb1sK8Uvu76U7
os-dev-report
{
"issue": 28658,
"status": "needs_decision",
"branch": "claude/project-thread-v6mwdv",
"pr": null,
"session": "session_01FEkQ5onyCFb1sK8Uvu76U7",
"premise_still_valid": false,
"summary": "Stopped at the order's premise check, before any edit. The published contract declares the decision output's
typeandmultipleas input-widget hints that never shape the accepted value, so the order's own fork applies (do not enforce, report needs_decision). Governing text, read at origin/main 2678600: (1) packages/spec/src/automation/approval.zod.ts L656-660, DecisionOutputDefSchema docblock: 'The runtime treatskeyas the whitelist entry either way —typeandmultipleonly shape the INPUT WIDGET, never the accepted value.' (2) same file L697-700, therequireddocblock: 'Unliketype/multiple— which only shape the input widget — this one IS enforced by the runtime'. (3) the published page content/docs/automation/approvals.mdx L342-343: 'The type shapes only the input widget; the whitelist still works bykey', and L347: 'Unliketype/multiple, this one is enforced by the runtime'. The one sentence pointing the other way is themultipledescribe string (L695, 'stored as a list'), which names the widget's output, not a refusal. Downstream also accepts the card's case (b) by contract: the expression approver takes 'ids (string / CSV / string array)' (approval-service.ts L2721), which is why the card's bare-string stage 2 still resolved. Rulings 1(a)-(c) all enforcetype/multipleat the decision, so each contradicts this text; no code, test or changeset was written.","tests": "none run: stopped at the premise check by the order's fork, before the claim's edit phase. Readings are git show origin/main (2678600) of the three files cited in summary.",
"gates": "none: no diff.",
"dogfood_battery": "n/a — no diff",
"mcp_calls": "0",
"api_writes": "1 — POST repos//issues/28658/comments (this report, via scripts/pm/post-stamped.mjs relay)",
"open_questions": [
{
"question": "The card's fix (refuse an unresolved user id, a scalar for multiple:true, and duplicate ids at the decision) contradicts the published contract, which says type/multiple only shape the widget and only
requiredis enforced. Which way?","options": [
"A — Amend the contract first, on its own protocol card: spec docblock + describe strings for DecisionOutputDefSchema and content/docs/automation/approvals.mdx declare that on APPROVE a typed output's values must resolve (user, and by the same rule department/team/position, or say why only user),
multiple: truetakes a list, and a list holds no duplicates. #28658 then carries Blocked-by on that card and lands ruling 1 as written (about 60-90 lines in approval-service.ts plus tests; Clause-② narrowing; Metadata-protocol no longer none, since a declaration key gains runtime meaning).","B — Keep the contract as published. Close #28658 as by-design for (a) and (b); the strand in (a) is the author's stage-2 choice of onEmptyApprovers 'fail', and the showcase can switch that flow to admin_rescue or fallback. File (c) separately as a slate-assembly defect (one user, two co_sign slots from one expression), fixed by deduping the expression approver's resolved ids, which contradicts no text.",
"C — Enforce only what the published text already allows: nothing at the decision; fix (c) at slate assembly as in B, and leave (a)/(b) on the card as parked pending a protocol decision."
],
"recommendation": "A, because the expression approver downstream does read these values as user ids, and a refusal at the decision (strict contract, loud error naming the output) is structurally harder for an AI-authored flow to get wrong than a strand discovered at the next node. But per pm-dispatch, changing the protocol is its own card and not an option on a defect card, so A means: file the protocol card, mark #28658 Blocked-by it, and re-dispatch this order unchanged once it lands. Duplicates (c) can go ahead now at slate assembly in either case."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed — (c) duplicate ids producing two co_sign slots for one user is the card's own measurement, not re-measured here; it is a slate-assembly behaviour (expression approver resolution in approval-service.ts around L2742-2760 pushes each id without a dedupe) independent of the type/multiple contract. Dedupe words: approver duplicate slot, expression approver dedupe, co_sign same user twice."
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Release: session
session_01FEkQ5oc56xChD6Wrc4cGSt· reason: the dev'sneeds_decision(os-dev-report 6558577952,premise_still_valid: false, no branch pushed) · destination:needs-user-decision(items (a) and (b)); item (c) moved to #28684, dispatched. Claim 6558537451 ends here; assignee cleared.待裁:审批"决策输出"的 type / multiple 要不要在通过时真正校验 · 2026-11-01T13:28Z
一句话问题. 审批人在"下一步审批人"里填了一个不存在的用户 id,审批照样记为通过,然后流程在下一步崩掉、报 500、卡死;但公开契约明文写着这两个设置"只管输入控件、不约束提交的值",所以按契约这不算缺陷,要不要改契约得由你定。
Governing text.
packages/spec/src/automation/approval.zod.tsDecisionOutputDefSchema文档注释: "The runtime treatskeyas the whitelist entry either way —typeandmultipleonly shape the INPUT WIDGET, never the accepted value."required注释: "Unliketype/multiple— which only shape the input widget — this one IS enforced by the runtime".content/docs/automation/approvals.mdx同义两句(dev 报告 6558577952 给出行号)。plugin-approvals/src/approval-service.ts表达式分支),所以卡里"单个字符串"那一例下游照常解析。git grep -n "only shape the INPUT WIDGET" packages/spec/src/automation/approval.zod.ts(应 1 处命中)。协议声明. 选 A 即改协议(一个已声明键获得运行时含义):按"改协议单独立卡",先立 spec 卡改文案,本卡挂
Blocked-by:,不在缺陷卡里改协议。multiple: true必须是列表,列表不得重复;之后本卡按原派发单落地admin_rescue兜底业务含义直译. A = 表单提交时就校验"这个人存不存在";B = 先收下,出问题交给管理员人工处理;C = 先不管。
四轴(业务立场).
examples/app-showcase/src/automation/flows/dynamic-approval.flow.ts);填错一次就造成"已通过但流程卡死、客户端看到失败",重试还会叠加。type/multiple名副其实,属于"声明即强制";B 保留一个"声明了但不兑现"的键。os-decision-facets
① 项目长远合理性: A 让已声明的 type/multiple 兑现,缩小特例;B/C 保留声明与执行不一致。
② 实际业务拉动: 今天 showcase 动态审批与 QA 复测 #28656 撞上(500 后记录已落)。
③ 防 AI 犯错: A 在提交处响亮拒绝并点名字段;B 静默转人工,C 是 500 卡死。
④ 创业阶段不扩散: A 不加键不加门禁,只兑现已声明键;B 需要维持一条"不校验"的例外说明。
zhuangjianguo commentedon Nov 2, 2026
director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw
Brief-claimed-by: session_01QsR2jV5sBSzCYNEDgpYLJw · batch #450 · 2026-11-02T00:46Z
Director seat (第 56 场). Presented to the maintainer in batch #450, item 2. The seat-5 analysis 6558604103 (options A/B/C, facets, recommendation A) is the reading; this seat's increment:
origin/main00918c88:packages/spec/src/automation/approval.zod.ts:660still reads "typeandmultipleonly shape the INPUT WIDGET, never the accepted value", and:697-700says onlyrequiredis enforced. Premise holds. Re-check:git grep -n "only shape the INPUT WIDGET" origin/main -- packages/spec/src/automation/approval.zod.ts(1 hit).:304-320) already records: "The forwarded target judges the admitted values against the request's owndecisionOutputs(required, type), as it judges the rest of the body it receives (ADR-0066 D4)." The ADR says the approvals door judgestype; the spec docblock says it never does. The two records disagree today, and under the standing ruling (spec declared > implementation > prose; a wrong protocol is fixed first) letter A is the one that makes the spec say what ADR-0104 already says. Noted for ④: A does not invent a rule, it closes a contradiction.type: userthat nothing checks is a "declared but unenforced" key, which AGENTS.md's North Star treats as a defect class; Salesforce approval next-approver fields and ServiceNow approver references are validated at submit. ② agree — the showcase dynamic approval is the live scenario and QA QA run · release-18.3.0-w3-retest (13/13) · 6e11a929 · 2026-11-01 · 11 PASS / 0 PARTIAL / 2 FAIL / 0 BLOCKED / 0 NOT-RUN #28656 hit it; the symptom is a request that reads "approved" while its flow is dead with a 500, and the approver sees a server error, not a field error. ③ agree — an AI-authored flow or an API client is the likeliest to pass a wrong id; A refuses at the decision with a 4xx naming the output and records nothing; B records the bad decision and hands it to an admin rescue; C leaves the 500. ④ A adds no key; it gives two existing keys the meaning their names claim.domain:spectext PR (docblock +content/docs/automation/approvals.mdx) +domain:serverplugin-approvals ~60–90 lines with tests;Clause-②: no (narrowing)⇒CONTRACT_REVIEW_TIERreview (this seat). B = 1 showcase config PR + close. C = 0. Duplicates (item c) already split to approvals: an expression approver that yields the same user id twice opens two co_sign slots for one person — the slate is not deduped #28684 and proceed regardless.user,department,teamby record id;positionby machine name), or says whyuseralone — the same resolvers the expression approver already uses; (2)multiple: truetakes a list and only a list, and a list holds no duplicates; (3) the refusal is400 VALIDATION_ERRORnamingoutputs.<key>, before any write. Not taken: B (keeps a key that lies), C (a known 500 strand left in place).os-decision-facets
① 长远:A 让已声明的 type/multiple 名副其实,并消除 ADR-0104(已写"按 required、type 判值")与 spec 文案的矛盾;B/C 保留声明与执行不一致
② 拉动:showcase 动态审批与 QA #28656 今天就撞上;审批人看到的是 500,不是字段错误
③ 防 AI 犯错:A 在提交处响亮拒绝并点名字段、不落记录;B 静默记录后转人工;C 500 卡死
④ 不扩散:A 不加键不加门禁;B 要维持一条"不校验"的例外说明
Prior rulings read:
decision output|decisionOutputsoverorigin/mainAGENTS.md, docs/adr → ADR-0104:304-320(the 2026-10-30 amendment, judged bydecisionOutputsrequired+type); ADR none other; thread: 6558604103 (seat 5), 6558577952 (dev report)Generated by Claude Code
zhuangjianguo commentedon Nov 2, 2026
director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw
Ruling: batch #450 item 2 · letter A · maintainer 「同意」 2026-11-02T01:12Z
Director seat (第 56 场; brief-claimed above in 6566202773 and presented in the 总监决裁 thread as batch #450 item 2 with the business read-out). The maintainer answered the batch verbatim: 「同意」 = every item's recommendation, so this card's letter is A: a decision output's declared
typeandmultipleare enforced at the decision.What the ruling binds (letter A):
user,department,teamby record id;positionby machine name) resolves through the same resolvers the expression approver already uses, or the protocol card says whyuseralone is in scope first.multiple: truetakes a list and only a list, with no duplicate entries;multipleabsent or false takes a single value.400 VALIDATION_ERRORnamingoutputs.<key>, before any write; no decision record is stored, the request stays where it was, the flow never sees a dead approver.packages/spec/src/automation/approval.zod.ts:660("only shape the INPUT WIDGET") and:697-700are rewritten to say what ADR-0104's 2026-10-30 amendment (:304-320, judged bydecisionOutputsrequired + type) already records, andcontent/docs/automation/approvals.mdxfollows. This is a narrowing:Clause-②: no (narrowing),Metadata-protocol: none, contract review atCONTRACT_REVIEW_TIER.Workload of the letter: from the card's own column:
domain:spectext PR (docblock + docs page) +domain:serverplugin-approvals change of about 60–90 lines with tests; two PRs, spec text first. No same-shape landed PR with line counts, so the estimate is the card's, not measured.Execution:
domain:serverseat 5 picks the card up withRuling-ref:naming this comment and files or claims thedomain:spectext slice; theneeds-user-decision→pm:queuestroke is that seat's. Acceptance: a wrong id, a scalar where a list is declared, or a duplicated list entry is refused with400 VALIDATION_ERRORnaming the output and nothing is written; the showcase dynamic approval (#28656) completes with a valid picker value. TheRuled:body line is written by this act.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Claim: PM loop round 7 · card thread · 2026-11-02T12:20Z
Session:
session_01ADHkmQKwKFCp1m6Pb6Q8c1(the seat); the card thread is named in a follow-up claim once it reports its branchAccount:
os-steve(the seat's linked user; the card's assignee)Branch: pending: the card thread's harness-pinned branch, written into a follow-up claim before the dev starts
Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: 6566731522 (batch #450 item 2 · letter A · a decision output's declared
typeandmultipleare enforced at the decision)File surface:
packages/plugins/plugin-approvals/src/(the decision door inapproval-service.ts, beside therequiredcheck; picker ids resolved through the resolvers the expression approver already uses), its tests, changeset. ⛔ Nopackages/spec/**: the docblock andcontent/docs/automation/approvals.mdxtext slice is a separatedomain:speccard that this seat files, and it lands first.Container & model:
M,mode:thread, model: the thread's default judgement tier,effort: high(p2)Clause-②: no (narrowing), as the ruling states; contract review at
CONTRACT_REVIEW_TIERby this seat on the delivered headMetadata-protocol: none
Responsibility: the code is the approvals decision door · a typed decision output accepts any value, so a wrong id records the decision and the run strands · reached by flows that route through a dynamic approver
Thread-read: 6566731522
Premise: holds (read at
origin/main5780fa49d:approval-service.tsenforcesrequiredonly;type/multipleshape the widget)Serial constraints cleared: no open PR touches
plugin-approvals/src/; the server PR waits for the spec text slice to merge before it lands.Resume-from: none: fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Claim: PM loop round 7 · card thread · branch named · 2026-11-02T12:22Z (supersedes 6576983171, which had the branch pending)
Session:
session_01ADHkmQKwKFCp1m6Pb6Q8c1(the seat); the card thread issession_01ADHkmPp7x4FxopdKR7keh7Account:
os-steve(the seat's linked user; the card's assignee)Branch:
claude/project-thread-rsqu0h(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: 6566731522 (batch #450 item 2 · letter A · a decision output's declared
typeandmultipleare enforced at the decision)File surface:
packages/plugins/plugin-approvals/src/(the decision door inapproval-service.ts, beside therequiredcheck; picker ids resolved through the resolvers the expression approver already uses), its tests, changeset. ⛔ Nopackages/spec/**: the docblock andcontent/docs/automation/approvals.mdxtext slice is a separatedomain:speccard that this seat files, and it lands first.Container & model:
M,mode:thread, model: the thread's default judgement tier,effort: high(p2)Clause-②: no (narrowing), as the ruling states; contract review at
CONTRACT_REVIEW_TIERby this seat on the delivered headMetadata-protocol: none
Responsibility: the code is the approvals decision door · a typed decision output accepts any value, so a wrong id records the decision and the run strands · reached by flows that route through a dynamic approver
Thread-read: 6576983171
Premise: holds (read at
origin/main5780fa49d:approval-service.tsenforcesrequiredonly;type/multipleshape the widget)Serial constraints cleared: no open PR touches
plugin-approvals/src/; the server PR waits for the spec text slice to merge before it lands.Resume-from: none: fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmPp7x4FxopdKR7keh7
os-dev-report
{ "issue": 28658, "status": "done", "branch": "claude/project-thread-rsqu0h", "pr": "https://github.com/objectstack-ai/objectstack/pull/29372", "head": "59bdcb565", "session": "session_01ADHkmPp7x4FxopdKR7keh7", "premise_still_valid": true, "summary": "Ruling A implemented in ApprovalService.decideNode: after the required check and before assertRunResumable and any write, assertDecisionOutputsMatchDeclarations judges every admitted output of type user/department/team/position against its declaration. multiple: true takes a list with no duplicates, otherwise one value; each value resolves through the expression approver's own lookups (user: reassignTargetRefusal with no org; department: businessUnitIsLive, extracted from expandBusinessUnitUsers; team: readTeamRow, extracted from teamIsProvablyOutsideOrg; position: expandPositionUsers by machine name, so a sys_position id is refused). One VALIDATION_FAILED names every outputs.KEY, nothing is written and the request stays pending. text and bare keys are unchanged; blanks stay required's question; a reject carrying a bad output is refused too (outputs ride the reject's resume and snapshot). All four picker kinds are in scope.", "tests": "At 59bdcb565: pnpm --filter @objectstack/plugin-approvals test -> Test Files 107 passed (107), Tests 1399 passed (1399); typecheck (tsc + scripts + check:test-typecheck OK, 8 files / 321 errors, ledger unchanged) -> VERDICT command-exit 0. New: 17 tests in approval-service.test.ts (describe 'typed decision outputs are judged against their declaration (#28658)') + 1 end-to-end two-stage flow test in approval-node.test.ts. Ablations (ablation-replace.mjs, anchor x1, restore blob == HEAD, git diff HEAD empty; src imports so no dist leg): 11 unit legs at 0b23046c5, each red (call removed 14 failed; approve-only 1; scalar-for-list 2; list-for-scalar 2; duplicates 1; user lookup 4; position by name 2; user address guard 1; department 1; team missing 1; team foreign 1); end-to-end leg at 665d0599d red with 'expected ... VALIDATION_FAILED: `outputs.next_re… but got RESUME_FAILED: the approve decision w…' (the card's defect). approval-service.ts byte-identical at 59bdcb565.", "gates": "At 59bdcb565: dispatch-gates --commands derived 63; all 63 exit 0 (check-plugin-teardown-shape --self-test, check:type-check-debt, check:dual-build-cjs-loads first exit 3 = prerequisite, re-run green after fetching 621a4876 / closure builds); --ran: '63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED'. Lint: root eslint --no-inline-config over packages/plugins/plugin-approvals exit 0. check:nul-bytes exit 0. CI check-runs: in_progress at report time, not waited on.", "dogfood_battery": "59bdcb565 · os-verify-lock: VERDICT command-exit 0 (held 33m27s) · Test Files 393 passed | 3 skipped (396) · Tests 3243 passed | 28 skipped (3271) · no failures", "mcp_calls": "5 — all reads: issue_read get #28658, issue_read get_comments #28658 (x2 pages), issue_read get #29340, pull_request_read get_files #29372", "api_writes": "2 — pr_create objectstack-ai/objectstack#29372 via fleet-write relay (with-fleet.sh --via dispatch); POST repos/objectstack-ai/objectstack/issues/28658/comments (this report, post-stamped.mjs). Plus git push (not REST).", "files_changed": [ ".changeset/28658-decision-output-type-multiple.md (+24)", "packages/plugins/plugin-approvals/src/approval-service.ts (+180 -22)", "packages/plugins/plugin-approvals/src/approval-service.test.ts (+222 -2)", "packages/plugins/plugin-approvals/src/approval-node.test.ts (+65)" ], "line_budget": "card estimate 60-90 lines of service code; delivered +180 -22 in approval-service.ts (about 110 lines of logic and docblocks for the two new methods, the rest is the two extracted lookups and comments), +287 test lines, 515 changed lines total", "deviations": [ "Refusal code is 400 VALIDATION_FAILED, not the ruling's spelling VALIDATION_ERROR: the approvals door answers every validation refusal (the sibling required refusal included) as VALIDATION_FAILED through one prefix row; see open_questions[0].", "Forced test edits (named in the PR body): 'required outputs: a filled approve goes through' now seeds holders for positions pos_1/pos_2; 'required outputs: every approver of a multi-approver node' now adds accounts u7/u8.", "The session's attribution reminder asks to set the PR assignee and reviewer through GitHub MCP writes; not done, because the standard order forbids MCP writes and PR assignees.", "Unit ablation legs ran at 0b23046c5 and the end-to-end leg at 665d0599d, before the origin/main merge; approval-service.ts is byte-identical at the delivered head." ], "open_questions": [ { "question": "The ruling spells the refusal 400 VALIDATION_ERROR; the approvals door answers all its validation refusals as 400 VALIDATION_FAILED. Which code?", "options": ["A keep VALIDATION_FAILED, the door's one validation code (as delivered)", "B change handleApprovalError in packages/rest to answer VALIDATION_ERROR for this refusal or for the whole door (a wire change, its own card)"], "recommendation": "A, because one door with two codes for the same class of refusal is a dialect, and the runtime action forward and its tests already relay VALIDATION_FAILED verbatim." }, { "question": "Department, team and position values resolve in the request's organization. A downstream expression approver that declares organization (ADR-0105 D9) resolves in its directory organization, which the decision cannot know. Keep?", "options": ["A request organization (as delivered)", "B skip the organization screen for graph kinds"], "recommendation": "A, because it is the organization the request lives in and no declaration ties an output to one downstream approver." }, { "question": "position is judged by holders (expandPositionUsers), so a valid machine name nobody holds is refused too. Keep, or add a sys_position by-name registry lookup?", "options": ["A holders (as delivered: the expression approver's only position resolver; the card says no second lookup)", "B add a registry lookup so an unstaffed position passes and routes to the admin-rescue slot"], "recommendation": "A, because an unstaffed position is exactly the dead approver the ruling says the flow must never see." }, { "question": "Per the card delta, type text keeps today's behaviour, including a text output declared multiple: true. Should multiple be enforced on text too?", "options": ["A no (as delivered, per the delta)", "B yes, list-only for text with multiple: true"], "recommendation": "A, because the delta names text as unchanged and free text has no resolver; B can be its own card if the spec text card #29340 says otherwise." } ], "security_findings": "none", "out_of_scope_findings": [] }Stamped 2026-11-02T13:22Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmPp7x4FxopdKR7keh7
os-dev-report
{ "issue": 28658, "status": "done", "round": 2, "branch": "claude/project-thread-rsqu0h", "pr": "https://github.com/objectstack-ai/objectstack/pull/29372", "head": "3eb0a962b", "session": "session_01ADHkmPp7x4FxopdKR7keh7", "premise_still_valid": true, "summary": "Round 2 on the seat's answers (Q1-Q3 A, Q4 B). multiple now binds every declaration, text and bare keys included: a list with no entry twice when multiple: true, one value otherwise. Text is judged on that shape only, with no resolver. A bare key (decisionOutputs: ['note']) therefore takes one value; { key, multiple: true } collects a list. Picker behaviour unchanged from round 1. Refusal code stays VALIDATION_FAILED.", "tests": "At 3eb0a962b: pnpm --filter @objectstack/plugin-approvals test -> Test Files 107 passed (107), Tests 1402 passed (1402); typecheck incl. check:test-typecheck OK (8 files / 321 errors, ledger unchanged) -> VERDICT command-exit 0. The 'text unchanged' test is replaced by 4: text values free in the declared shape; text multiple: true refuses a scalar; text (declared and bare key) without multiple refuses a list; a text list refuses a repeated entry. New ablation legs at 3eb0a962b (ablation-replace.mjs, anchor x1, restored blob == HEAD 721225e9): text_skipped (text skipped as in round 1) -> 3 failed; text_duplicates (text repeat check off) -> 1 failed. Round-1 legs stand: the picker code path is unchanged.", "gates": "At 3eb0a962b: dispatch-gates --commands derived the same 63; all 63 exit 0 (check-comment-mask-corpus first exit 1 on ENOENT of a transient examples/app-showcase/objectstack.config.bundled_*.mjs written by the concurrent dogfood boot, re-run exit 0: 20071 files, 0 disagree); --ran: '63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED'. Lint: root eslint --no-inline-config over packages/plugins/plugin-approvals exit 0. CI check-runs not waited on.", "dogfood_battery": "3eb0a962b · os-verify-lock: VERDICT command-exit 0 (held 29m54s) · Test Files 393 passed | 3 skipped (396) · Tests 3243 passed | 28 skipped (3271) · no failures", "mcp_calls": "0 this round (5 reads in round 1)", "api_writes": "1 this round — POST repos/objectstack-ai/objectstack/issues/28658/comments (this report, post-stamped.mjs). Plus git push.", "files_changed": [ ".changeset/28658-decision-output-type-multiple.md", "packages/plugins/plugin-approvals/src/approval-service.ts", "packages/plugins/plugin-approvals/src/approval-service.test.ts", "packages/plugins/plugin-approvals/src/approval-node.test.ts" ], "line_budget": "round 2 adds about 25 service lines (text shape branch + docblocks) and about 35 test lines over round 1", "deviations": [ "PR body not edited (os-dev: the dev writes it once). The corrections for this head are in project file server-seat5/29372-body-r2-patch.md for the seat to apply.", "Two more forced test edits, both a bare key sent a list: approval-service.test.ts 'decision outputs: accepted keys return from decideNode and snapshot as __decisionOutputs' and approval-node.test.ts 'decide outputs feed the NEXT approval node via vars.NODE.KEY (#3447 P2)' now declare { key: 'next_reviewers', multiple: true }.", "Told spec seat 1 (for #29340 / PR #29371) that the correction's point 2 stands: text is shape-checked on this head." ], "open_questions": [ { "question": "Q4 = B makes a bare key take one value: a step that used a bare key to collect a list (the #3447 P2 headline shape) is now refused until it declares multiple: true. The changeset keeps Metadata-protocol: none and ADR-0087 not-required (no-migration-prescription), with the one-line remedy in prose. Does the contract review accept that, or does the absent-multiple meaning count as semantic with a migration note?", "options": ["A keep none / not-required, remedy in the changeset prose (as delivered)", "B relabel Metadata-protocol: semantic and add a migration prescription"], "recommendation": "A, because no key, value or default of the schema changes; the declared meaning of an absent multiple is what is now enforced, and the ruling fixed the declaration lines." } ], "security_findings": "none", "out_of_scope_findings": [] }Stamped 2026-11-02T14:01Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmPp7x4FxopdKR7keh7
os-dev-report
{ "issue": 28658, "status": "done", "round": 3, "branch": "claude/project-thread-rsqu0h", "pr": "https://github.com/objectstack-ai/objectstack/pull/29372", "head": "c7ca5d18f", "session": "session_01ADHkmPp7x4FxopdKR7keh7", "premise_still_valid": true, "summary": "Round 3 (contract review exit (a)). Must-fix: the Console now sends a list for a free-value decision output declared multiple: true. One place both decide paths pass through: decisionOutputParams (ui/packages/app-shell/src/utils/decisionOutputParams.ts) renders a text or untyped output declared multiple: true, and a position output with no registry reader, as a `tags` param. TagsField stores a string[], trims each entry and skips a blank or repeat, so the decide body carries a list the duplicate refusal never trips on. Both folds hand values on untouched: foldDecisionOutputs (record header) and the runtime outputs.KEY fold in packages/runtime/src/action-execution.ts (Approval Center), whose list pass-through is already pinned in api-action-forward.test.ts. No fold changed. A single-value output stays one text box and its comma is literal. The comma hint actions.decisionOutput.helpMultiValue leaves all ten locale packs (its only call site is gone); every output's help is actions.decisionOutput.help. Should-fixes: ADR-0087 remedy reworded with no rewrite prescription (disposition kept: not-required (no-migration-prescription)); changeset adds the pre-#29371 CSV wording, pending bare-key snapshots, and the ADR-0105 D9 organization note; Skills-docs line in the body corrections (seat set it to follow-up #29512). Merged origin/main incl. spec #29371 (8e6ab05eb); the merged docblock and this head agree on text + multiple: true: a list, shape only.", "tests": "At c7ca5d18f: plugin-approvals test -> Test Files 107 passed (107), Tests 1404 passed (1404); tsc --noEmit exit 0. New server pins: 'the Console's decide body passes' (user list, text tags list, untyped tags list, bare key with a literal comma) and 'the comma string the Console used to send for a list output is refused'. ui: app-shell vitest -> Test Files 2132 passed | 1 skipped (2133), Tests 18804 passed | 9 skipped; i18n vitest -> 95 files, 1443 passed | 13 skipped. New ui pins: decisionOutputParams.test.ts describe 'a free-value output is sent in its declared shape (objectstack#28658)' (6 tests) and ActionParamDialog.decisionOutputList-28658.test.tsx (real dialog: ' alpha ', 'alpha', blank, 'beta' by Enter and 'gamma' by blur resolve { 'outputs.labels': ['alpha','beta','gamma'], 'outputs.note': 'a, b' }, folded the same). Forced edits: three help-text expectations move helpMultiValue -> help (decisionOutputParams.test.ts x2, DeclaredActionsBar.test.tsx x1). Ablations at a913892b7 (tree of c8c7e730b; ablation-replace.mjs, anchor x1, restored blob == HEAD): console_tags (type 'tags' -> 'text') -> 4 failed, dialog quoting `expected { 'outputs.labels': 'gamma', …(1) } to deeply equal { 'outputs.labels': [ …(3) ], …(1) }`; console_csv (scalar-for-list check off) -> 1 failed, 'Error: expected a refusal'.", "gates": "At c7ca5d18f: dispatch-gates --commands derived 67 (4 more than round 2: check-sdui-manifest + self-test, ui scripts vitest, check:skill-identifier-liveness); all 67 exit 0; --ran: '67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED'. ui extras all exit 0: app-shell and i18n type-check, eslint on the touched ui files, check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:eager-locale-catalogues (after the console build). CI check-runs not waited on.", "dogfood_battery": "c7ca5d18f · os-verify-lock: VERDICT command-exit 0 (held 40m12s) · Test Files 397 passed | 3 skipped (400) · Tests 3277 passed | 28 skipped (3305) · no failures", "mcp_calls": "0 this round", "api_writes": "1 this round — POST repos/objectstack-ai/objectstack/issues/28658/comments (this report, post-stamped.mjs). Plus git push (3eb0a962b..c7ca5d18f).", "files_changed": [ ".changeset/28658-decision-output-type-multiple.md", ".changeset/28658-console-decision-output-list.md", "packages/plugins/plugin-approvals/src/approval-service.test.ts", "ui/packages/app-shell/src/utils/decisionOutputParams.ts", "ui/packages/app-shell/src/utils/decisionOutputParams.test.ts", "ui/packages/app-shell/src/views/ActionParamDialog.decisionOutputList-28658.test.tsx", "ui/packages/app-shell/src/views/__tests__/DeclaredActionsBar.test.tsx", "ui/packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts" ], "line_budget": "round 3: about 20 ui source lines, 10 locale lines removed, about 140 test lines, two changesets", "deviations": [ "PR body not edited (os-dev: the dev writes it once). Corrections for this head: project file server-seat5/29372-body-r3-patch.md, on top of 29372-body-r2-patch.md.", "The pre-push check:commit-card-trailers refused the first round-3 messages (a model name in the co-author trailer, a card ref). The three UNPUBLISHED commits were rebuilt with identical trees and model-free trailers before the first push; nothing published was rewritten.", "New ui changeset .changeset/28658-console-decision-output-list.md (@object-ui/app-shell, @object-ui/i18n patch; Clause-② no (narrowing); Metadata-protocol none; ADR-0087 not-required (no-migration-prescription); BREAKING only for TranslationKeys naming the deleted key)." ], "open_questions": [], "security_findings": "none", "out_of_scope_findings": [] }Stamped 2026-11-02T19:24Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Landed · #28658 · 2026-11-02T20:16Z
d3175d59d0(headf9f2b6f892; round-3 contract record (b), delta (a) PASS, seat ACCEPT)completedby the PR'sFixeslineState:
pm:dispatchedremoved.