You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
app-showcase: the expense sign-off approval (会签) ends without writing its outcome back — EXP-2001 still reads "Submitted" after both groups approved #25469
Found during an exploratory walkthrough of the showcase.
Environment
objectstack main @ b9c7a8f, PORT=3000 pnpm dev (showcase, fresh DB), objectui main @ 6fb31dc
Steps
As the dev admin, open the pending approval 费用报告签批 · EXP-2001 from the home page's 待审批 card, choose 通过, add a comment, confirm. The request shows 会签进度 1 / 2 (manager 1/1, finance 0/1).
Sign in as auditor.demo@example.com, open the same request from the notification, approve.
Actual
The approval request moves to 已批准 and its timeline lists both approvals, which is right.
The expense report itself is unchanged: the request page still shows the record's Status: Submitted, and GET /api/v1/data/showcase_expense_report returns EXP-2001 → status: "submitted".
Why
showcase_expense_signoff (src/automation/flows/index.ts) is start → dual_signoff (approval) → approved | rejected, and both outcomes are bare end nodes. Nothing on either branch updates the record, so the business object never learns the decision. The node also sets lockRecord: true; I did not check whether the lock is released after the decision.
Expected
A reader of the showcase sees the approval's result on the record it governed, for example an update_record on each branch setting status to approved / rejected. As it stands the marquee 会签 demo ends with a "Submitted" expense report that has, in fact, been approved, which reads as a platform defect to anyone trying the demo. The other approval demos that end on bare end nodes (the committee quorum flow sits right below this one) may want the same treatment.
Triage: lands in examples/app-showcase/src/automation/flows/index.ts (showcase_expense_signoff, the dual_signoff node) — showcase metadata, routed to domain:server as the subsystem it exercises (approvals), per the precedent on earlier app-showcase cards; rationale: the platform already has the mechanism: an approval node's approvalStatusField mirrors each request transition onto the business record (checklist approvals.status-mirror-field). The flagship 会签 demo declares neither that nor an update_record branch, so an approved expense report keeps reading Submitted. Fix = declare the mirror (or an update_record per branch) on this flow, and check the committee quorum flow beside it for the same gap.
Path: workflow · 审批走得完 | approvals.per-group-signoff (P1, passes) / approvals.status-mirror-field (not declared by this demo) | step ②
Grade: priority:p2 — runs, but the demo people copy ends in a wrong record state; no data loss.
Type: Bug
Session: session_01AkmasAfXt3cu83xGzkuKhe (objectstack triage, run on the maintainer's request in the project thread)
Triage: lands in examples/app-showcase/src/automation/flows/index.ts (showcase_expense_signoff, the dual_signoff node) — showcase metadata, routed to domain:server as the subsystem it exercises (approvals), per the precedent on earlier app-showcase cards; rationale: the platform already has the mechanism: an approval node's approvalStatusField mirrors each request transition onto the business record (checklist approvals.status-mirror-field). The flagship 会签 demo declares neither that nor an update_record branch, so an approved expense report keeps reading Submitted. Fix = declare the mirror (or an update_record per branch) on this flow, and check the committee quorum flow beside it for the same gap.
Path: workflow · 审批走得完 | approvals.per-group-signoff (P1, passes) / approvals.status-mirror-field (not declared by this demo) | step ②
Grade: priority:p2 — runs, but the demo people copy ends in a wrong record state; no data loss.
Type: Bug
Session: session_01AkmasAfXt3cu83xGzkuKhe (objectstack triage, run on the maintainer's request in the project thread)
Claim: PM loop round 1 · domain:services seat 1 (seat post #6021, project run by the Sales account) · S mechanical card, in-seat subagent · 2026-10-23T18:24Z
Session: session_016g7nYP9wdNx1GDa5XDr7Uy
Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-25469-showcase-signoff-mirror
Worktree: objectstack-issue-25469
Domain: domain:server
Seat: domain:services#1
File surface: examples/app-showcase/src/automation/flows/index.ts (showcase_expense_signoff, the dual_signoff node: declare the approval node's approvalStatusField mirror, or an update_record per branch), and the committee quorum flow beside it if it has the same gap. Plus its showcase pin. ⛔ packages/spec is not edited. Stop on breach; explain in the report.
Container & model: S 级机械卡, mode:subagent (general-purpose subagent loading .claude/agents/os-dev.md as its role file), model: default tier, effort: medium
Clause-②: no
Metadata-protocol: none
Responsibility: the showcase's own flagship 会签 flow declares no write-back | an approved expense report keeps reading Submitted | anyone who copies the demo, today
Thread-read: none
Serial constraints cleared: no open PR touches the showcase flows file at claim (13 open PRs read at 2026-10-23T18:15:00Z on 842a0aef9d).
Resume-from: none
{
"issue": 25469,
"status": "done",
"branch": "claude/issue-25469-showcase-signoff-mirror",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25525",
"session": "session_016g7nYP9wdNx1GDa5XDr7Uy",
"premise_still_valid": true,
"summary": "Both showcase_expense_signoff (dual_signoff, per_group) and showcase_committee_quorum (committee, quorum) ended their approve/reject arms on bare end nodes, so the report kept reading submitted. Each now runs an update_record per outcome arm (approve -> status approved, reject -> status draft, the revert-on-reject precedent of showcase_invoice_signoff) under runAs system. approvalStatusField was measured and rejected: plugin-approvals mirrorStatusField writes the request vocabulary verbatim (pending on open; approved/rejected on decide; recalled on recall/dead-run sweep; returned on send-back; rejected on revision-limit auto-reject), has no mapping (spec key is a bare z.string; a mapping would need packages/spec), and skips with a warn any value the field's options do not declare. showcase_expense_report.status declares draft/submitted/approved/reimbursed, so a mirror would warn on every submit and leave a rejected report reading Submitted; the spec also advises a readonly mirror target and status is submitter-edited. Landing: examples/app-showcase/src/automation/flows/index.ts plus new pin examples/app-showcase/test/expense-approval-write-back.test.ts; base dc3e5b9d7b, head 3e661a73f6.",
"tests": "head 3e661a73f6. New pin test/expense-approval-write-back.test.ts (real ObjectKernel + ObjectQL + automation + approvals over sqlite, real objects/flows, decisions via ApprovalService.decide, describe.each over both flows: approve / reject / CONTROL with the pre-fix bare-end shape leaving status submitted): 6/6 passed. Ablation via scripts/ablation-replace.mjs on committed HEAD: anchor e2 dual_signoff -> mark_approved replaced with target approved, anchor x1 -> x0, blob a47c39e35483 -> 78466ece32b2; result Tests 1 failed | 5 passed (approve case: expected [start, dual_signoff] to include mark_approved); restored blob == HEAD, git diff HEAD empty. Source-imported via relative ../src, no dist leg. Showcase typecheck + full test under the lock: VERDICT command-exit 0, Test Files 38/38, Tests 461/461; new test is in the tsc program (--listFiles 1 hit). Dependency closure build pnpm --filter '@objectstack/example-showcase^...' build: VERDICT command-exit 0.",
"gates": "dispatch-gates --commands: 45 commands, each run and recorded with exit code; 44 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (unbuilt packages outside the closure: studio, client-react, embedder-openai, ...) = NOT MEASURED locally. --ran: '45 derived famil(ies) accounted for - 44 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. PR #25525 check-runs on 3e661a73f6, all completed: success - Lint & Repo Gates, TypeScript Type Check (+ workspace/source gates/debt ledger/consumer gates lanes), Test Core (1-6/6 + aggregate), Build Core, Governed Surface Queue Guard, Check Changeset, Check PR Size, Dogfood Regression Gate, Check Documentation Links, all Lint lanes (ESLint, PM dispatch-gates, CI release & engine gates, repo & tooling, docs & code guards, scoped x3), claim/single-writer/Part-of checks; skipped by design - Temporal Conformance, Dogfood Verify CLI, Dogfood shards, Console Pin Gate, Build Docs, opt-in harnesses.",
"dogfood_battery": "n/a - no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "3 - git push (direct, not REST); relay pr_create via fleet-write/dispatch.mjs (POST /repos/objectstack-ai/objectstack/dispatches -> pulls create + issues/25525/assignees os-sales + issues/25525/labels skip-changeset); this os-dev-report comment via post-stamped.mjs (issues/25469/comments)",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none - noted in PR Acceptance notes, not filed: docs/qa/platform-checklist/areas/approvals.json approvals.status-mirror-field knownGaps still says no showcase flow declares approvalStatusField; stays true by design (mirror vocabulary does not fit the report status options).",
"carrier: none - noted in PR Acceptance notes, not filed (read-only inference, not measured): showcase_expense_signoff start gate has no amount ceiling, so a report >= 5000 matches both expense flows' start conditions on submit; the seed routes the demos to different records deliberately."
]
}
Activity
objectstack-fleet commentedon Oct 23, 2026
Triage: lands in
examples/app-showcase/src/automation/flows/index.ts(showcase_expense_signoff, thedual_signoffnode) — showcase metadata, routed todomain:serveras the subsystem it exercises (approvals), per the precedent on earlier app-showcase cards; rationale: the platform already has the mechanism: an approval node'sapprovalStatusFieldmirrors each request transition onto the business record (checklistapprovals.status-mirror-field). The flagship 会签 demo declares neither that nor anupdate_recordbranch, so an approved expense report keeps reading Submitted. Fix = declare the mirror (or anupdate_recordper branch) on this flow, and check the committee quorum flow beside it for the same gap.Path: workflow · 审批走得完 | approvals.per-group-signoff (P1, passes) / approvals.status-mirror-field (not declared by this demo) | step ②
Grade:
priority:p2— runs, but the demo people copy ends in a wrong record state; no data loss.Type: Bug
Session:
session_01AkmasAfXt3cu83xGzkuKhe(objectstack triage, run on the maintainer's request in the project thread)Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
Triage: lands in
examples/app-showcase/src/automation/flows/index.ts(showcase_expense_signoff, thedual_signoffnode) — showcase metadata, routed todomain:serveras the subsystem it exercises (approvals), per the precedent on earlier app-showcase cards; rationale: the platform already has the mechanism: an approval node'sapprovalStatusFieldmirrors each request transition onto the business record (checklistapprovals.status-mirror-field). The flagship 会签 demo declares neither that nor anupdate_recordbranch, so an approved expense report keeps reading Submitted. Fix = declare the mirror (or anupdate_recordper branch) on this flow, and check the committee quorum flow beside it for the same gap.Path: workflow · 审批走得完 | approvals.per-group-signoff (P1, passes) / approvals.status-mirror-field (not declared by this demo) | step ②
Grade:
priority:p2— runs, but the demo people copy ends in a wrong record state; no data loss.Type: Bug
Session:
session_01AkmasAfXt3cu83xGzkuKhe(objectstack triage, run on the maintainer's request in the project thread)Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
Claim: PM loop round 1 ·
domain:servicesseat 1 (seat post #6021, project run by the Sales account) · S mechanical card, in-seat subagent · 2026-10-23T18:24ZSession:
session_016g7nYP9wdNx1GDa5XDr7UyAccount:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-25469-showcase-signoff-mirrorWorktree:
objectstack-issue-25469Domain:
domain:serverSeat:
domain:services#1File surface:
examples/app-showcase/src/automation/flows/index.ts(showcase_expense_signoff, thedual_signoffnode: declare the approval node'sapprovalStatusFieldmirror, or anupdate_recordper branch), and the committee quorum flow beside it if it has the same gap. Plus its showcase pin. ⛔packages/specis not edited. Stop on breach; explain in the report.Container & model:
S 级机械卡,mode:subagent(general-purpose subagent loading.claude/agents/os-dev.mdas its role file),model: default tier,effort: mediumClause-②: no
Metadata-protocol: none
Responsibility: the showcase's own flagship 会签 flow declares no write-back | an approved expense report keeps reading Submitted | anyone who copies the demo, today
Thread-read: none
Serial constraints cleared: no open PR touches the showcase flows file at claim (13 open PRs read at 2026-10-23T18:15:00Z on
842a0aef9d).Resume-from: none
objectstack-fleet commentedon Oct 23, 2026
os-dev-report
{ "issue": 25469, "status": "done", "branch": "claude/issue-25469-showcase-signoff-mirror", "pr": "https://github.com/objectstack-ai/objectstack/pull/25525", "session": "session_016g7nYP9wdNx1GDa5XDr7Uy", "premise_still_valid": true, "summary": "Both showcase_expense_signoff (dual_signoff, per_group) and showcase_committee_quorum (committee, quorum) ended their approve/reject arms on bare end nodes, so the report kept reading submitted. Each now runs an update_record per outcome arm (approve -> status approved, reject -> status draft, the revert-on-reject precedent of showcase_invoice_signoff) under runAs system. approvalStatusField was measured and rejected: plugin-approvals mirrorStatusField writes the request vocabulary verbatim (pending on open; approved/rejected on decide; recalled on recall/dead-run sweep; returned on send-back; rejected on revision-limit auto-reject), has no mapping (spec key is a bare z.string; a mapping would need packages/spec), and skips with a warn any value the field's options do not declare. showcase_expense_report.status declares draft/submitted/approved/reimbursed, so a mirror would warn on every submit and leave a rejected report reading Submitted; the spec also advises a readonly mirror target and status is submitter-edited. Landing: examples/app-showcase/src/automation/flows/index.ts plus new pin examples/app-showcase/test/expense-approval-write-back.test.ts; base dc3e5b9d7b, head 3e661a73f6.", "tests": "head 3e661a73f6. New pin test/expense-approval-write-back.test.ts (real ObjectKernel + ObjectQL + automation + approvals over sqlite, real objects/flows, decisions via ApprovalService.decide, describe.each over both flows: approve / reject / CONTROL with the pre-fix bare-end shape leaving status submitted): 6/6 passed. Ablation via scripts/ablation-replace.mjs on committed HEAD: anchor e2 dual_signoff -> mark_approved replaced with target approved, anchor x1 -> x0, blob a47c39e35483 -> 78466ece32b2; result Tests 1 failed | 5 passed (approve case: expected [start, dual_signoff] to include mark_approved); restored blob == HEAD, git diff HEAD empty. Source-imported via relative ../src, no dist leg. Showcase typecheck + full test under the lock: VERDICT command-exit 0, Test Files 38/38, Tests 461/461; new test is in the tsc program (--listFiles 1 hit). Dependency closure build pnpm --filter '@objectstack/example-showcase^...' build: VERDICT command-exit 0.", "gates": "dispatch-gates --commands: 45 commands, each run and recorded with exit code; 44 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (unbuilt packages outside the closure: studio, client-react, embedder-openai, ...) = NOT MEASURED locally. --ran: '45 derived famil(ies) accounted for - 44 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. PR #25525 check-runs on 3e661a73f6, all completed: success - Lint & Repo Gates, TypeScript Type Check (+ workspace/source gates/debt ledger/consumer gates lanes), Test Core (1-6/6 + aggregate), Build Core, Governed Surface Queue Guard, Check Changeset, Check PR Size, Dogfood Regression Gate, Check Documentation Links, all Lint lanes (ESLint, PM dispatch-gates, CI release & engine gates, repo & tooling, docs & code guards, scoped x3), claim/single-writer/Part-of checks; skipped by design - Temporal Conformance, Dogfood Verify CLI, Dogfood shards, Console Pin Gate, Build Docs, opt-in harnesses.", "dogfood_battery": "n/a - no ! in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "3 - git push (direct, not REST); relay pr_create via fleet-write/dispatch.mjs (POST /repos/objectstack-ai/objectstack/dispatches -> pulls create + issues/25525/assignees os-sales + issues/25525/labels skip-changeset); this os-dev-report comment via post-stamped.mjs (issues/25469/comments)", "open_questions": [], "out_of_scope_findings": [ "carrier: none - noted in PR Acceptance notes, not filed: docs/qa/platform-checklist/areas/approvals.json approvals.status-mirror-field knownGaps still says no showcase flow declares approvalStatusField; stays true by design (mirror vocabulary does not fit the report status options).", "carrier: none - noted in PR Acceptance notes, not filed (read-only inference, not measured): showcase_expense_signoff start gate has no amount ceiling, so a report >= 5000 matches both expense flows' start conditions on submit; the seed routes the demos to different records deliberately." ] }objectstack-fleet commentedon Oct 23, 2026
Landed ·
domain:servicesseat 1 (seat post #6021) ·os-sales·session_016g7nYP9wdNx1GDa5XDr7Uy· 2026-10-23T19:51Z6373071000), head3e661a73f6origin/main1ff43b67eaState:
Fixes #25469closed the card.pm:dispatchedis now off and the assignee is cleared.Generated by Claude Code