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
Starting now: review_status (trackHistory select) + state_machine rule + review_note, dispatch gating on approved, the two views, seed data, zh-CN entries. Will measure field-level/record-level write permission by position before deciding on the "who may advance which step" half — no hand-written authorization hook either way.
{
"issue": 107,
"status": "done",
"branch": "claude/issue-107-duty-review-workflow",
"pr": "https://github.com/objectstack-ai/duly/pull/114",
"premise_still_valid": true,
"summary": "review_status (to_confirm/to_review/approved/returned) on duly_duty with a CEL default keyed on source, a state_machine rule pinning the transitions AND initialStates (so a duty cannot be born approved), review_note required on a return, dispatch gated on approved in both DISPATCH_DUTY_FIELDS and the sweep's query, the two views in the navigation, a seed mix of 25/2/2/1, and zh-CN for every new label. TWO OF THE CARD'S MECHANISM CLAIMS DID NOT SURVIVE MEASUREMENT, neither fatal: (1) trackHistory does not render the pipeline — stageField does; absent it the renderer heuristically picked duly_duty.status (Active/Paused/Retired), so the card's own acceptance screen was showing the wrong field. Fixed by declaring stageField: 'review_status' (ADR-0085). (2) The pipeline is NOT clickable — the stages are div role=listitem with no handler, on duly_task.status too. That is an upstream DECISION, not a defect: objectui#5768 asked for exactly this and was closed 2026-08-24 by objectui#5917, 'render record:path as a readout, not a fake segmented control'. Nothing filed; advancing happens in the form, which is the other half of the card's own plan and is what the screenshots show. On 'who may advance which step': the platform CAN restrict a value by caller — per-option visibleWhen is the one predicate scope that binds current_user and the rule validator enforces it on the write path (measured through REST: 400 invalid_option). It is used, but keyed on record.owner != current_user.id rather than on a position, because current_user.positions comes from sys_user_position rows a package may not declare, so a position predicate fails CLOSED in every deployment that has not done that manual step, pnpm demo included. Landed second: merged main (#110, #112), kept both nav entries, and narrowed #110's import-sample gate to exempt record-derived defaults only.",
"tests": "All four gates green on ddcd075 (run after the final commit, exits captured before any pipe): `pnpm validate` → '✓ Validation passed (567ms)' + the one expected @objectstack/security-enterprise warning; `pnpm typecheck` → exit 0; `pnpm test` → 'Test Files 32 passed (32) / Tests 808 passed (808)'; `pnpm build` → 'Artifact: dist/objectstack.json (196.3 KB)'. RED FIRST on the dispatch gate: adding the review check to planForDuty took test/dispatch.test.ts to 'Tests 32 failed | 25 passed (57)' before any fixture was touched. New test/duty-review.test.ts (23 assertions) drives the real engine through ordinary writes — no isSystem, no skipStateMachine — covering the source-keyed defaults, initialStates refusing a born-approved insert, every legal and illegal transition, the note rule (including blanking the note while still returned), and the owner gate driven with three callers (owner refused, other caller allowed, no-user write allowed and pinned as the seed path). REVERSE VERIFICATION on the narrowed import gate: deleted the `owner` column from samples/duties.csv, confirmed the mutation on disk ('owner' occurrences 3 → 2, header printed), gate went red naming ['owner']; a trap EXIT restored the file and the restore was confirmed (git diff on samples/ empty). BROWSER, against pnpm demo on port 4107: walked to_confirm → to_review on the demo account's own duty, to_review → approved and → returned on other people's, returned → to_review, with the refusal for a return with no reason rendering in product voice; the picker offers ['To confirm','To review'] on the caller's own duty and all four on somebody else's. Both server refusals reproduced over REST as the signed-in admin (400 VALIDATION_FAILED / invalid_option and invalid_transition). DISPATCH, real dispatcher against the live demo database: to_confirm → sweep read 22 duties, created 0, that duty has 0 tasks; approved → 23 duties, created 1, one task for 2026-09.",
"open_questions": [
{
"question": "The state machine admits `approved → returned`, which the card's table does not draw (its 谁推进 column for approved is '—'). Keep it?",
"options": [
"A — keep `approved: ['returned']`. A manager may pull an approved duty back, with a mandatory reason; tasks already dispatched are untouched.",
"B — revert to `approved: []`, exactly as the card's table reads."
],
"recommendation": "A. Measured: with approved terminal a live duty has no correction path at all — not for its owner, not for a manager, not for an administrator (state_machine has no role exemption; the only remaining doors are a treatAsHistorical import and a re-seed). A cadence that turns out wrong, a duty approved on the wrong person, a catalog sync nobody re-read: each then dispatches work every night with no way back into the pipeline that governs it. Returning is the auditable way out and is not a loophole — approval is still not self-issuable. One line to revert if you meant the table literally."
},
{
"question": "No Duly permission set can write `review_status` in a real deployment. Which half do you want opened?",
"options": [
"A — grant `duly_member.duly_duty.allowEdit` (writeScope stays `own`) so an owner can confirm their own list. The bit is object-level, not per-field, so it also lets a member edit their own cadence — which src/security/permission-sets.ts refuses on purpose ('editing cadence after the fact is a correction, which belongs to duly_admin and to duly_catalog_sync').",
"B — widen `duly_manager.duly_duty.writeScope` to `unit_and_below` so a manager can approve a report's duty. Contradicts an explicit ⛔ in that file and is pinned by test/security.test.ts; also brushes AGENTS.md's 'Managers do not enter status. Assigning is their only write.'",
"C — both, which is what makes the workflow usable end to end by real users.",
"D — neither for now: the demo runs as a platform admin and works, and the deployment question waits for a permission-model pass."
],
"recommendation": "D for this card and C as the next one, filed separately. Real business need: the feature is p0 for a DEMO, and the demo account is a platform admin, so nothing is blocked this month; a deployment needs C or the pipeline is decoration. Long-term soundness: A and B each overturn a decision that is written down with its reasoning, and B's cost is not 'a manager writes duties' but that the object-level bit cannot say WHICH field — approving is indistinguishable from rewriting cadence on somebody else's duty. That is a permission-model question (field-level write permission, or an approval that is not an ordinary field write), not a line to slip into this PR. Hard-to-get-wrong: opening object edit to get one field is exactly the over-grant that never gets narrowed later. Startup scope: shipping the visible, demoable workflow now and deciding the permission model deliberately is the cheaper order."
}
],
"out_of_scope_findings": [
"filed as objectstack-ai/objectstack#14416: a gated select option logs one WARN per seeded row ('option visibleWhen … failed to evaluate — allowed through') because a system write can never bind current_user — 27 lines per demo boot, and the identical line is what a genuine unevaluable predicate produces for an authenticated caller",
"not filed, already decided upstream: the record-page stage pipeline is a readout, not a control (objectui#5768 → objectui#5917, 2026-08-24). Recorded here because this card was written on the opposite assumption",
"no change made, reported above as an open question: no Duly permission set can write review_status — duly_member has no allowEdit on duly_duty and every set's writeScope there is `own`"
]
}
(Reposting the report above with a plain-text marker: GitHub's body sanitizer ate the HTML-comment os-dev-report marker on the previous comment, so a scan for it would not have found this. Same JSON, unchanged.)
{
"issue": 107,
"status": "done",
"branch": "claude/issue-107-duty-review-workflow",
"pr": "https://github.com/objectstack-ai/duly/pull/114",
"premise_still_valid": true,
"summary": "review_status (to_confirm/to_review/approved/returned) on duly_duty with a CEL default keyed on source, a state_machine rule pinning the transitions AND initialStates (so a duty cannot be born approved), review_note required on a return, dispatch gated on approved in both DISPATCH_DUTY_FIELDS and the sweep's query, the two views in the navigation, a seed mix of 25/2/2/1, and zh-CN for every new label. TWO OF THE CARD'S MECHANISM CLAIMS DID NOT SURVIVE MEASUREMENT, neither fatal: (1) trackHistory does not render the pipeline — stageField does; absent it the renderer heuristically picked duly_duty.status (Active/Paused/Retired), so the card's own acceptance screen was showing the wrong field. Fixed by declaring stageField: 'review_status' (ADR-0085). (2) The pipeline is NOT clickable — the stages are div role=listitem with no handler, on duly_task.status too. That is an upstream DECISION, not a defect: objectui#5768 asked for exactly this and was closed 2026-08-24 by objectui#5917, 'render record:path as a readout, not a fake segmented control'. Nothing filed; advancing happens in the form, which is the other half of the card's own plan and is what the screenshots show. On 'who may advance which step': the platform CAN restrict a value by caller — per-option visibleWhen is the one predicate scope that binds current_user and the rule validator enforces it on the write path (measured through REST: 400 invalid_option). It is used, but keyed on record.owner != current_user.id rather than on a position, because current_user.positions comes from sys_user_position rows a package may not declare, so a position predicate fails CLOSED in every deployment that has not done that manual step, pnpm demo included. Landed second: merged main (#110, #112), kept both nav entries, and narrowed #110's import-sample gate to exempt record-derived defaults only.",
"tests": "All four gates green on ddcd075 (run after the final commit, exits captured before any pipe): `pnpm validate` → '✓ Validation passed (567ms)' + the one expected @objectstack/security-enterprise warning; `pnpm typecheck` → exit 0; `pnpm test` → 'Test Files 32 passed (32) / Tests 808 passed (808)'; `pnpm build` → 'Artifact: dist/objectstack.json (196.3 KB)'. RED FIRST on the dispatch gate: adding the review check to planForDuty took test/dispatch.test.ts to 'Tests 32 failed | 25 passed (57)' before any fixture was touched. New test/duty-review.test.ts (23 assertions) drives the real engine through ordinary writes — no isSystem, no skipStateMachine — covering the source-keyed defaults, initialStates refusing a born-approved insert, every legal and illegal transition, the note rule (including blanking the note while still returned), and the owner gate driven with three callers (owner refused, other caller allowed, no-user write allowed and pinned as the seed path). REVERSE VERIFICATION on the narrowed import gate: deleted the `owner` column from samples/duties.csv, confirmed the mutation on disk ('owner' occurrences 3 → 2, header printed), gate went red naming ['owner']; a trap EXIT restored the file and the restore was confirmed (git diff on samples/ empty). BROWSER, against pnpm demo on port 4107: walked to_confirm → to_review on the demo account's own duty, to_review → approved and → returned on other people's, returned → to_review, with the refusal for a return with no reason rendering in product voice; the picker offers ['To confirm','To review'] on the caller's own duty and all four on somebody else's. Both server refusals reproduced over REST as the signed-in admin (400 VALIDATION_FAILED / invalid_option and invalid_transition). DISPATCH, real dispatcher against the live demo database: to_confirm → sweep read 22 duties, created 0, that duty has 0 tasks; approved → 23 duties, created 1, one task for 2026-09.",
"open_questions": [
{
"question": "The state machine admits `approved → returned`, which the card's table does not draw (its 谁推进 column for approved is '—'). Keep it?",
"options": [
"A — keep `approved: ['returned']`. A manager may pull an approved duty back, with a mandatory reason; tasks already dispatched are untouched.",
"B — revert to `approved: []`, exactly as the card's table reads."
],
"recommendation": "A. Measured: with approved terminal a live duty has no correction path at all — not for its owner, not for a manager, not for an administrator (state_machine has no role exemption; the only remaining doors are a treatAsHistorical import and a re-seed). A cadence that turns out wrong, a duty approved on the wrong person, a catalog sync nobody re-read: each then dispatches work every night with no way back into the pipeline that governs it. Returning is the auditable way out and is not a loophole — approval is still not self-issuable. One line to revert if you meant the table literally."
},
{
"question": "No Duly permission set can write `review_status` in a real deployment. Which half do you want opened?",
"options": [
"A — grant `duly_member.duly_duty.allowEdit` (writeScope stays `own`) so an owner can confirm their own list. The bit is object-level, not per-field, so it also lets a member edit their own cadence — which src/security/permission-sets.ts refuses on purpose ('editing cadence after the fact is a correction, which belongs to duly_admin and to duly_catalog_sync').",
"B — widen `duly_manager.duly_duty.writeScope` to `unit_and_below` so a manager can approve a report's duty. Contradicts an explicit ⛔ in that file and is pinned by test/security.test.ts; also brushes AGENTS.md's 'Managers do not enter status. Assigning is their only write.'",
"C — both, which is what makes the workflow usable end to end by real users.",
"D — neither for now: the demo runs as a platform admin and works, and the deployment question waits for a permission-model pass."
],
"recommendation": "D for this card and C as the next one, filed separately. Real business need: the feature is p0 for a DEMO, and the demo account is a platform admin, so nothing is blocked this month; a deployment needs C or the pipeline is decoration. Long-term soundness: A and B each overturn a decision that is written down with its reasoning, and B's cost is not 'a manager writes duties' but that the object-level bit cannot say WHICH field — approving is indistinguishable from rewriting cadence on somebody else's duty. That is a permission-model question (field-level write permission, or an approval that is not an ordinary field write), not a line to slip into this PR. Hard-to-get-wrong: opening object edit to get one field is exactly the over-grant that never gets narrowed later. Startup scope: shipping the visible, demoable workflow now and deciding the permission model deliberately is the cheaper order."
}
],
"out_of_scope_findings": [
"filed as objectstack-ai/objectstack#14416: a gated select option logs one WARN per seeded row ('option visibleWhen … failed to evaluate — allowed through') because a system write can never bind current_user — 27 lines per demo boot, and the identical line is what a genuine unevaluable predicate produces for an authenticated caller",
"not filed, already decided upstream: the record-page stage pipeline is a readout, not a control (objectui#5768 → objectui#5917, 2026-08-24). Recorded here because this card was written on the opposite assumption",
"no change made, reported above as an open question: no Duly permission set can write review_status — duly_member has no allowEdit on duly_duty and every set's writeScope there is `own`"
]
}
Activity
os-warren commentedon Sep 2, 2026
Claiming this issue.
01SqkTcrxUFci7nqXdbBSe2p(https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p)claude/issue-107-duty-review-workflow../duly-issue-107Starting now:
review_status(trackHistory select) + state_machine rule +review_note, dispatch gating onapproved, the two views, seed data, zh-CN entries. Will measure field-level/record-level write permission by position before deciding on the "who may advance which step" half — no hand-written authorization hook either way.Generated by Claude Code
Confirm and approve a duty before it dispatches (#107)
Point the record-page stepper at the review pipeline (#107)
Keep the import sample gate about columns whose absence refuses a row (…
Widen the helper's return type to match its cast (#107)
os-warren commentedon Sep 2, 2026
{ "issue": 107, "status": "done", "branch": "claude/issue-107-duty-review-workflow", "pr": "https://github.com/objectstack-ai/duly/pull/114", "premise_still_valid": true, "summary": "review_status (to_confirm/to_review/approved/returned) on duly_duty with a CEL default keyed on source, a state_machine rule pinning the transitions AND initialStates (so a duty cannot be born approved), review_note required on a return, dispatch gated on approved in both DISPATCH_DUTY_FIELDS and the sweep's query, the two views in the navigation, a seed mix of 25/2/2/1, and zh-CN for every new label. TWO OF THE CARD'S MECHANISM CLAIMS DID NOT SURVIVE MEASUREMENT, neither fatal: (1) trackHistory does not render the pipeline — stageField does; absent it the renderer heuristically picked duly_duty.status (Active/Paused/Retired), so the card's own acceptance screen was showing the wrong field. Fixed by declaring stageField: 'review_status' (ADR-0085). (2) The pipeline is NOT clickable — the stages are div role=listitem with no handler, on duly_task.status too. That is an upstream DECISION, not a defect: objectui#5768 asked for exactly this and was closed 2026-08-24 by objectui#5917, 'render record:path as a readout, not a fake segmented control'. Nothing filed; advancing happens in the form, which is the other half of the card's own plan and is what the screenshots show. On 'who may advance which step': the platform CAN restrict a value by caller — per-option visibleWhen is the one predicate scope that binds current_user and the rule validator enforces it on the write path (measured through REST: 400 invalid_option). It is used, but keyed on record.owner != current_user.id rather than on a position, because current_user.positions comes from sys_user_position rows a package may not declare, so a position predicate fails CLOSED in every deployment that has not done that manual step, pnpm demo included. Landed second: merged main (#110, #112), kept both nav entries, and narrowed #110's import-sample gate to exempt record-derived defaults only.", "tests": "All four gates green on ddcd075 (run after the final commit, exits captured before any pipe): `pnpm validate` → '✓ Validation passed (567ms)' + the one expected @objectstack/security-enterprise warning; `pnpm typecheck` → exit 0; `pnpm test` → 'Test Files 32 passed (32) / Tests 808 passed (808)'; `pnpm build` → 'Artifact: dist/objectstack.json (196.3 KB)'. RED FIRST on the dispatch gate: adding the review check to planForDuty took test/dispatch.test.ts to 'Tests 32 failed | 25 passed (57)' before any fixture was touched. New test/duty-review.test.ts (23 assertions) drives the real engine through ordinary writes — no isSystem, no skipStateMachine — covering the source-keyed defaults, initialStates refusing a born-approved insert, every legal and illegal transition, the note rule (including blanking the note while still returned), and the owner gate driven with three callers (owner refused, other caller allowed, no-user write allowed and pinned as the seed path). REVERSE VERIFICATION on the narrowed import gate: deleted the `owner` column from samples/duties.csv, confirmed the mutation on disk ('owner' occurrences 3 → 2, header printed), gate went red naming ['owner']; a trap EXIT restored the file and the restore was confirmed (git diff on samples/ empty). BROWSER, against pnpm demo on port 4107: walked to_confirm → to_review on the demo account's own duty, to_review → approved and → returned on other people's, returned → to_review, with the refusal for a return with no reason rendering in product voice; the picker offers ['To confirm','To review'] on the caller's own duty and all four on somebody else's. Both server refusals reproduced over REST as the signed-in admin (400 VALIDATION_FAILED / invalid_option and invalid_transition). DISPATCH, real dispatcher against the live demo database: to_confirm → sweep read 22 duties, created 0, that duty has 0 tasks; approved → 23 duties, created 1, one task for 2026-09.", "open_questions": [ { "question": "The state machine admits `approved → returned`, which the card's table does not draw (its 谁推进 column for approved is '—'). Keep it?", "options": [ "A — keep `approved: ['returned']`. A manager may pull an approved duty back, with a mandatory reason; tasks already dispatched are untouched.", "B — revert to `approved: []`, exactly as the card's table reads." ], "recommendation": "A. Measured: with approved terminal a live duty has no correction path at all — not for its owner, not for a manager, not for an administrator (state_machine has no role exemption; the only remaining doors are a treatAsHistorical import and a re-seed). A cadence that turns out wrong, a duty approved on the wrong person, a catalog sync nobody re-read: each then dispatches work every night with no way back into the pipeline that governs it. Returning is the auditable way out and is not a loophole — approval is still not self-issuable. One line to revert if you meant the table literally." }, { "question": "No Duly permission set can write `review_status` in a real deployment. Which half do you want opened?", "options": [ "A — grant `duly_member.duly_duty.allowEdit` (writeScope stays `own`) so an owner can confirm their own list. The bit is object-level, not per-field, so it also lets a member edit their own cadence — which src/security/permission-sets.ts refuses on purpose ('editing cadence after the fact is a correction, which belongs to duly_admin and to duly_catalog_sync').", "B — widen `duly_manager.duly_duty.writeScope` to `unit_and_below` so a manager can approve a report's duty. Contradicts an explicit ⛔ in that file and is pinned by test/security.test.ts; also brushes AGENTS.md's 'Managers do not enter status. Assigning is their only write.'", "C — both, which is what makes the workflow usable end to end by real users.", "D — neither for now: the demo runs as a platform admin and works, and the deployment question waits for a permission-model pass." ], "recommendation": "D for this card and C as the next one, filed separately. Real business need: the feature is p0 for a DEMO, and the demo account is a platform admin, so nothing is blocked this month; a deployment needs C or the pipeline is decoration. Long-term soundness: A and B each overturn a decision that is written down with its reasoning, and B's cost is not 'a manager writes duties' but that the object-level bit cannot say WHICH field — approving is indistinguishable from rewriting cadence on somebody else's duty. That is a permission-model question (field-level write permission, or an approval that is not an ordinary field write), not a line to slip into this PR. Hard-to-get-wrong: opening object edit to get one field is exactly the over-grant that never gets narrowed later. Startup scope: shipping the visible, demoable workflow now and deciding the permission model deliberately is the cheaper order." } ], "out_of_scope_findings": [ "filed as objectstack-ai/objectstack#14416: a gated select option logs one WARN per seeded row ('option visibleWhen … failed to evaluate — allowed through') because a system write can never bind current_user — 27 lines per demo boot, and the identical line is what a genuine unevaluable predicate produces for an authenticated caller", "not filed, already decided upstream: the record-page stage pipeline is a readout, not a control (objectui#5768 → objectui#5917, 2026-08-24). Recorded here because this card was written on the opposite assumption", "no change made, reported above as an open question: no Duly permission set can write review_status — duly_member has no allowEdit on duly_duty and every set's writeScope there is `own`" ] }Generated by Claude Code
os-warren commentedon Sep 2, 2026
os-dev-report
(Reposting the report above with a plain-text marker: GitHub's body sanitizer ate the HTML-comment
os-dev-reportmarker on the previous comment, so a scan for it would not have found this. Same JSON, unchanged.){ "issue": 107, "status": "done", "branch": "claude/issue-107-duty-review-workflow", "pr": "https://github.com/objectstack-ai/duly/pull/114", "premise_still_valid": true, "summary": "review_status (to_confirm/to_review/approved/returned) on duly_duty with a CEL default keyed on source, a state_machine rule pinning the transitions AND initialStates (so a duty cannot be born approved), review_note required on a return, dispatch gated on approved in both DISPATCH_DUTY_FIELDS and the sweep's query, the two views in the navigation, a seed mix of 25/2/2/1, and zh-CN for every new label. TWO OF THE CARD'S MECHANISM CLAIMS DID NOT SURVIVE MEASUREMENT, neither fatal: (1) trackHistory does not render the pipeline — stageField does; absent it the renderer heuristically picked duly_duty.status (Active/Paused/Retired), so the card's own acceptance screen was showing the wrong field. Fixed by declaring stageField: 'review_status' (ADR-0085). (2) The pipeline is NOT clickable — the stages are div role=listitem with no handler, on duly_task.status too. That is an upstream DECISION, not a defect: objectui#5768 asked for exactly this and was closed 2026-08-24 by objectui#5917, 'render record:path as a readout, not a fake segmented control'. Nothing filed; advancing happens in the form, which is the other half of the card's own plan and is what the screenshots show. On 'who may advance which step': the platform CAN restrict a value by caller — per-option visibleWhen is the one predicate scope that binds current_user and the rule validator enforces it on the write path (measured through REST: 400 invalid_option). It is used, but keyed on record.owner != current_user.id rather than on a position, because current_user.positions comes from sys_user_position rows a package may not declare, so a position predicate fails CLOSED in every deployment that has not done that manual step, pnpm demo included. Landed second: merged main (#110, #112), kept both nav entries, and narrowed #110's import-sample gate to exempt record-derived defaults only.", "tests": "All four gates green on ddcd075 (run after the final commit, exits captured before any pipe): `pnpm validate` → '✓ Validation passed (567ms)' + the one expected @objectstack/security-enterprise warning; `pnpm typecheck` → exit 0; `pnpm test` → 'Test Files 32 passed (32) / Tests 808 passed (808)'; `pnpm build` → 'Artifact: dist/objectstack.json (196.3 KB)'. RED FIRST on the dispatch gate: adding the review check to planForDuty took test/dispatch.test.ts to 'Tests 32 failed | 25 passed (57)' before any fixture was touched. New test/duty-review.test.ts (23 assertions) drives the real engine through ordinary writes — no isSystem, no skipStateMachine — covering the source-keyed defaults, initialStates refusing a born-approved insert, every legal and illegal transition, the note rule (including blanking the note while still returned), and the owner gate driven with three callers (owner refused, other caller allowed, no-user write allowed and pinned as the seed path). REVERSE VERIFICATION on the narrowed import gate: deleted the `owner` column from samples/duties.csv, confirmed the mutation on disk ('owner' occurrences 3 → 2, header printed), gate went red naming ['owner']; a trap EXIT restored the file and the restore was confirmed (git diff on samples/ empty). BROWSER, against pnpm demo on port 4107: walked to_confirm → to_review on the demo account's own duty, to_review → approved and → returned on other people's, returned → to_review, with the refusal for a return with no reason rendering in product voice; the picker offers ['To confirm','To review'] on the caller's own duty and all four on somebody else's. Both server refusals reproduced over REST as the signed-in admin (400 VALIDATION_FAILED / invalid_option and invalid_transition). DISPATCH, real dispatcher against the live demo database: to_confirm → sweep read 22 duties, created 0, that duty has 0 tasks; approved → 23 duties, created 1, one task for 2026-09.", "open_questions": [ { "question": "The state machine admits `approved → returned`, which the card's table does not draw (its 谁推进 column for approved is '—'). Keep it?", "options": [ "A — keep `approved: ['returned']`. A manager may pull an approved duty back, with a mandatory reason; tasks already dispatched are untouched.", "B — revert to `approved: []`, exactly as the card's table reads." ], "recommendation": "A. Measured: with approved terminal a live duty has no correction path at all — not for its owner, not for a manager, not for an administrator (state_machine has no role exemption; the only remaining doors are a treatAsHistorical import and a re-seed). A cadence that turns out wrong, a duty approved on the wrong person, a catalog sync nobody re-read: each then dispatches work every night with no way back into the pipeline that governs it. Returning is the auditable way out and is not a loophole — approval is still not self-issuable. One line to revert if you meant the table literally." }, { "question": "No Duly permission set can write `review_status` in a real deployment. Which half do you want opened?", "options": [ "A — grant `duly_member.duly_duty.allowEdit` (writeScope stays `own`) so an owner can confirm their own list. The bit is object-level, not per-field, so it also lets a member edit their own cadence — which src/security/permission-sets.ts refuses on purpose ('editing cadence after the fact is a correction, which belongs to duly_admin and to duly_catalog_sync').", "B — widen `duly_manager.duly_duty.writeScope` to `unit_and_below` so a manager can approve a report's duty. Contradicts an explicit ⛔ in that file and is pinned by test/security.test.ts; also brushes AGENTS.md's 'Managers do not enter status. Assigning is their only write.'", "C — both, which is what makes the workflow usable end to end by real users.", "D — neither for now: the demo runs as a platform admin and works, and the deployment question waits for a permission-model pass." ], "recommendation": "D for this card and C as the next one, filed separately. Real business need: the feature is p0 for a DEMO, and the demo account is a platform admin, so nothing is blocked this month; a deployment needs C or the pipeline is decoration. Long-term soundness: A and B each overturn a decision that is written down with its reasoning, and B's cost is not 'a manager writes duties' but that the object-level bit cannot say WHICH field — approving is indistinguishable from rewriting cadence on somebody else's duty. That is a permission-model question (field-level write permission, or an approval that is not an ordinary field write), not a line to slip into this PR. Hard-to-get-wrong: opening object edit to get one field is exactly the over-grant that never gets narrowed later. Startup scope: shipping the visible, demoable workflow now and deciding the permission model deliberately is the cheaper order." } ], "out_of_scope_findings": [ "filed as objectstack-ai/objectstack#14416: a gated select option logs one WARN per seeded row ('option visibleWhen … failed to evaluate — allowed through') because a system write can never bind current_user — 27 lines per demo boot, and the identical line is what a genuine unevaluable predicate produces for an authenticated caller", "not filed, already decided upstream: the record-page stage pipeline is a readout, not a control (objectui#5768 → objectui#5917, 2026-08-24). Recorded here because this card was written on the opposite assumption", "no change made, reported above as an open question: no Duly permission set can write review_status — duly_member has no allowEdit on duly_duty and every set's writeScope there is `own`" ] }Generated by Claude Code