Skip to content

approvals: a typed user decision output is checked for presence only — an id that resolves to no user records the decision, then answers 500 RESUME_FAILED and strands the run #28658

Closed

Description

@objectstack-fleet

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:

  1. 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 reads approved, with an approve action row appended, and the run reads failed. Reproduced twice.
  2. Bare string for a multiple: true output. {"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.
  3. 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

  1. Boot the stock showcase as admin. Retitle a showcase_announcement (PATCH /api/v1/data/showcase_announcement/ID {"title":"…"}). That starts showcase_dynamic_approval, and the stage-1 lead_review request lists the admin as approver.
  2. POST /api/v1/approvals/requests/LEAD_REVIEW_ID/approve {"outputs":{"next_reviewers":["usr_does_not_exist"]}} → 500 RESUME_FAILED.
  3. GET /api/v1/approvals/requests/LEAD_REVIEW_ID → status approved. 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

  1. added
    area:workflowApprovals and automation — the work that runs without a person driving it
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 1, 2026
  2. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    • Read fresh, and reproduced twice. A typed user decision output is checked for shape only. An id that resolves to no user records the decision, then answers 500 RESUME_FAILED and strands the run. A bare string is stored for a multiple: true output. p2.
    • The fix validates the outputs before recording: ids resolve, and the cardinality matches. A refused decision records nothing and names the output.

    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 1, 2026
  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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 is session_01FEkQ5onyCFb1sK8Uvu76U7
    Account: 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:server
    Seat: domain:server#5
    Ruling-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.ts only if the refusal mapping needs it; a @objectstack/plugin-approvals changeset. ⛔ No packages/runtime/**, packages/spec/** or ui/**.
    Container & model: M, mode:thread, model and effort as the thread was opened
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the code is @objectstack/plugin-approvals' decision-output validation · no platform path covers it today (stage 2's onEmptyApprovers turns the input into a 500 strand) · reached by any approver deciding a node with a typed user output, used today by showcase_dynamic_approval
    Thread-read: 6558249277
    Premise: holds (read at origin/main 2678600d: the decision path checks reserved, undeclared and missing-required outputs only; its comment calls type/multiple widget-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 names approval-service.ts.
    Resume-from: none — fresh from origin/main 2678600d

    State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28658.md in the project.

  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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 type and multiple as 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 treats key as the whitelist entry either way — type and multiple only shape the INPUT WIDGET, never the accepted value.' (2) same file L697-700, the required docblock: 'Unlike type/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 by key', and L347: 'Unlike type / multiple, this one is enforced by the runtime'. The one sentence pointing the other way is the multiple describe 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 enforce type/multiple at 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 required is 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: true takes 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

  6. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

    Release: session session_01FEkQ5oc56xChD6Wrc4cGSt · reason: the dev's needs_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.ts DecisionOutputDefSchema 文档注释: "The runtime treats key as the whitelist entry either way — type and multiple only shape the INPUT WIDGET, never the accepted value."
    • 同文件 required 注释: "Unlike type/multiple — which only shape the input widget — this one IS enforced by the runtime".
    • content/docs/automation/approvals.mdx 同义两句(dev 报告 6558577952 给出行号)。
    • 下游表达式审批人接受 "ids (string / CSV / string array)"(plugin-approvals/src/approval-service.ts 表达式分支),所以卡里"单个字符串"那一例下游照常解析。
    • re-check: git grep -n "only shape the INPUT WIDGET" packages/spec/src/automation/approval.zod.ts(应 1 处命中)。

    协议声明. 选 A 即改协议(一个已声明键获得运行时含义):按"改协议单独立卡",先立 spec 卡改文案,本卡挂 Blocked-by:,不在缺陷卡里改协议。

    选项 做什么 客户可感知后果 工作量
    A 改契约并校验 新立协议卡:通过审批时,typed 输出的 id 必须能解析到真实记录(user,同理 department/team/position 或写明为何只 user),multiple: true 必须是列表,列表不得重复;之后本卡按原派发单落地 填错的审批当场被 4xx 拒绝并点名字段,什么都不记录;流程不再卡死 server+spec 两车道 · 2 PR(spec 文案+文档小 PR;plugin-approvals 约 60–90 行+测试,dev 估算,无落地先例)
    B 维持契约 (a)(b) 按设计关闭;showcase 的第二步改用 admin_rescue 兜底 填错仍会被记为通过,下一步交由管理员救援,不再 500 卡死 1 PR(showcase 一处配置)+ 关卡
    C 暂缓 (a)(b) 停放,等有真实用户撞上再定 现状不变:填错 → 记录通过 → 500 卡死 0

    业务含义直译. A = 表单提交时就校验"这个人存不存在";B = 先收下,出问题交给管理员人工处理;C = 先不管。

    四轴(业务立场).

    • 实际业务需求:showcase 的动态审批正是"上一步选人、下一步按所选的人会签",这是真实场景(examples/app-showcase/src/automation/flows/dynamic-approval.flow.ts);填错一次就造成"已通过但流程卡死、客户端看到失败",重试还会叠加。
    • 项目长远合理性:主流平台(Salesforce 审批的 next approver、ServiceNow 的审批人字段)都是在提交时校验引用存在;"只管控件不管值"会让每个读取这些值的下游各自容错。A 收窄特例。
    • 防 AI 犯错:AI 生成的流程最容易写出"选了人但 id 不对"的数据;A 让错误在提交处被响亮拒绝并点名,B/C 让错误在下一步变成 500 或静默转人工,作者看不到原因。
    • 创业阶段不扩散:A 不新增任何键,只让已声明的 type/multiple 名副其实,属于"声明即强制";B 保留一个"声明了但不兑现"的键。

    os-decision-facets
    ① 项目长远合理性: A 让已声明的 type/multiple 兑现,缩小特例;B/C 保留声明与执行不一致。
    ② 实际业务拉动: 今天 showcase 动态审批与 QA 复测 #28656 撞上(500 后记录已落)。
    ③ 防 AI 犯错: A 在提交处响亮拒绝并点名字段;B 静默转人工,C 是 500 卡死。
    ④ 创业阶段不扩散: A 不加键不加门禁,只兑现已声明键;B 需要维持一条"不校验"的例外说明。

    Prior rulings read: "decision output" multiple, decisionOutputs type → 11 hits, none a ruling on enforcement; ADR none; thread: none.
    
    **推荐 A.** 只看①选 A;②③④ 是否翻转:否。置信缺口:department/team/position 三类是否一并校验,dev 未实测其解析代价,执行时在协议卡里定。B 不取:它把一个可在提交处拒绝的输入错误推给管理员善后。C 不取:已知 500 卡死不该搁置。
    
    **裁后执行.** A:本席立 spec 协议卡(文案+文档,引你的原话),本卡挂 `pm:blocked` + `Blocked-by:` 该卡,落地后按原派发单 `order-28658.md` 重派。B:本席立 showcase 配置小卡并以 by-design 关闭本卡 (a)(b)。C:本卡转 `pm:on-hold`,`Restart-when:` 写"出现第二个真实报告"。重复 id 的问题 (c) 不等裁决,已拆为 #28684 在做。
    
  7. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 1, 2026
  8. zhuangjianguo commented on Nov 2, 2026

    @zhuangjianguo
    Collaborator

    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:

    • Premise re-read on origin/main 00918c88: packages/spec/src/automation/approval.zod.ts:660 still reads "type and multiple only shape the INPUT WIDGET, never the accepted value", and :697-700 says only required is enforced. Premise holds. Re-check: git grep -n "only shape the INPUT WIDGET" origin/main -- packages/spec/src/automation/approval.zod.ts (1 hit).
    • One governing text the analysis did not cite. ADR-0104's 2026-10-30 amendment (action door: forwarding a type:api action drops the target's error.details and userMessage, and the ADR-0104 param check refuses approval decision outputs (outputs.KEY), so console step 3 (#27611) would regress #27738, around :304-320) already records: "The forwarded target judges the admitted values against the request's own decisionOutputs (required, type), as it judges the rest of the body it receives (ADR-0066 D4)." The ADR says the approvals door judges type; 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.
    • Facets, read by this seat: ① agree — a declared type: user that 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.
    • Workload (the card's own): A = domain:spec text PR (docblock + content/docs/automation/approvals.mdx) + domain:server plugin-approvals ~60–90 lines with tests; Clause-②: no (narrowing) ⇒ CONTRACT_REVIEW_TIER review (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.
    • Recommendation: A, reached on ① alone; ②③④ do not turn it. Execution parameters to rule in the same stroke: (1) the protocol card states the resolution rule for all four picker kinds (user, department, team by record id; position by machine name), or says why user alone — the same resolvers the expression approver already uses; (2) multiple: true takes a list and only a list, and a list holds no duplicates; (3) the refusal is 400 VALIDATION_ERROR naming outputs.<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|decisionOutputs over origin/main AGENTS.md, docs/adr → ADR-0104 :304-320 (the 2026-10-30 amendment, judged by decisionOutputs required+type); ADR none other; thread: 6558604103 (seat 5), 6558577952 (dev report)


    Generated by Claude Code

  9. zhuangjianguo commented on Nov 2, 2026

    @zhuangjianguo
    Collaborator

    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 type and multiple are enforced at the decision.

    What the ruling binds (letter A):

    • The approvals door judges an admitted decision output against its declaration: a picker kind (user, department, team by record id; position by machine name) resolves through the same resolvers the expression approver already uses, or the protocol card says why user alone is in scope first. multiple: true takes a list and only a list, with no duplicate entries; multiple absent or false takes a single value.
    • The refusal is loud and early: 400 VALIDATION_ERROR naming outputs.<key>, before any write; no decision record is stored, the request stays where it was, the flow never sees a dead approver.
    • The spec text stops contradicting ADR-0104: packages/spec/src/automation/approval.zod.ts:660 ("only shape the INPUT WIDGET") and :697-700 are rewritten to say what ADR-0104's 2026-10-30 amendment (:304-320, judged by decisionOutputs required + type) already records, and content/docs/automation/approvals.mdx follows. This is a narrowing: Clause-②: no (narrowing), Metadata-protocol: none, contract review at CONTRACT_REVIEW_TIER.
    • Not taken: B (fix the showcase config and keep a key that lies), C (leave the 500 strand in place). The duplicate-approver 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 proceeds regardless.

    Workload of the letter: from the card's own column: domain:spec text PR (docblock + docs page) + domain:server plugin-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:server seat 5 picks the card up with Ruling-ref: naming this comment and files or claims the domain:spec text slice; the needs-user-decision → pm:queue stroke is that seat's. Acceptance: a wrong id, a scalar where a list is declared, or a duplicated list entry is refused with 400 VALIDATION_ERROR naming the output and nothing is written; the showcase dynamic approval (#28656) completes with a valid picker value. The Ruled: body line is written by this act.


    Generated by Claude Code

  10. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 2, 2026
  11. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 branch
    Account: 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:server
    Seat: domain:server#5
    Ruling-ref: 6566731522 (batch #450 item 2 · letter A · a decision output's declared type and multiple are enforced at the decision)
    File surface: packages/plugins/plugin-approvals/src/ (the decision door in approval-service.ts, beside the required check; picker ids resolved through the resolvers the expression approver already uses), its tests, changeset. ⛔ No packages/spec/**: the docblock and content/docs/automation/approvals.mdx text slice is a separate domain:spec card 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_TIER by this seat on the delivered head
    Metadata-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/main 5780fa49d: approval-service.ts enforces required only; type / multiple shape 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/main

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.

  12. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 is session_01ADHkmPp7x4FxopdKR7keh7
    Account: 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:server
    Seat: domain:server#5
    Ruling-ref: 6566731522 (batch #450 item 2 · letter A · a decision output's declared type and multiple are enforced at the decision)
    File surface: packages/plugins/plugin-approvals/src/ (the decision door in approval-service.ts, beside the required check; picker ids resolved through the resolvers the expression approver already uses), its tests, changeset. ⛔ No packages/spec/**: the docblock and content/docs/automation/approvals.mdx text slice is a separate domain:spec card 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_TIER by this seat on the delivered head
    Metadata-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/main 5780fa49d: approval-service.ts enforces required only; type / multiple shape 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/main

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.

  13. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  14. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  15. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  16. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  17. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

    Landed · #28658 · 2026-11-02T20:16Z

    reading value
    PR #29372 MERGED by the merge queue at 2026-11-02T20:15:27Z, merge commit d3175d59d0 (head f9f2b6f892; round-3 contract record (b), delta (a) PASS, seat ACCEPT)
    this card closed completed by the PR's Fixes line
    spec text #29340, landed as #29371
    skills doc follow-up #29512

    State: pm:dispatched removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions