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
A closed deal still takes line items: a rep adds a line to a closed-won opportunity, the deal amount keeps its closed figure, and the lines stop summing to it (family of #2060 item 1) #2077
Filing gate ①: a product defect with reach measured (class a). Filed by the repo:hotcrm seat (session_01WnMDsLSaUQUD41WaWnADcF) from the os-dev report on #2060 item 1 (PR #2076). ⛔ Not a claim.
This is the family card for "line items of a settled parent still take user writes". It enumerates every known position of the family; a new sighting is added here, ⛔ not filed as its own card.
What happens (measured on 17.7.0 at hotcrm a4d0170)
Through the engine write door, as a sales rep (the path POST /api/v1/data/crm_opportunity_line_item takes): a 3 × 500 line inserted onto the rep's own closed_won deal was admitted. The deal's amount stayed 1,000 while its lines sum to 1,500.
opportunity_lifecycle freezes a closed deal's own fields, but nothing guards its lines.
The docs promise otherwise: content/docs/sales/opportunities.mdx:40 "Closed deals are locked", and :116 "The sum of all line items rolls up into the opportunity Amount".
The delete half is ⛔ not in scope; it waits on the decision card.
Duplicate check
All 1,911 hotcrm issues and PRs (/issues?state=all, page walk) matched case-insensitively: closed deal line item 0, opportunity_line_item closed 0, Closed deals are locked 0, line items of a closed 0. Control: opportunity_amount_rollup 13 hits, all closed and none on point (#495, #591, #616–#619, #624, #1265, #1274, #1275, #1757, #1758, #1762).
The delete half of this family, for quote lines and deal lines alike, is decision card #2078. repo:hotcrm seat, session_01WnMDsLSaUQUD41WaWnADcF, 2026-10-12T10:05Z.
Claim: PM loop round R87
Session: session_01P8mZuzuyZAHSrgQKmv6ASv
Account: sunhaolin (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-2077-closed-deal-lines
Worktree: hotcrm-issue-2077
Domain: repo:hotcrm (single-lane repo, no domain:* taxonomy)
Seat: repo:hotcrm#1
File surface: the insert / update half only (the delete half is decision card #2078): the freeze guard for a line whose old or new parent deal is closed, in src/sales/objects/opportunity.hook.ts (opportunity_lifecycle, the card's "one freeze, one envelope, in the existing lifecycle hook") or, only if the measured hook wiring requires it, src/revenue/objects/opportunity_line_item.hook.ts; their tests under test/; content/docs/sales/opportunities.mdx (+ zh-Hans / zh-Hant) only if a sentence becomes false; one .changeset/2077-*.md. ⛔ Not src/sales/objects/opportunity.object.ts or src/revenue/objects/opportunity_line_item.object.ts (#2000 holds the object files this round) (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: default (dispatch-gates --tier --repo objectstack-ai/hotcrm: "no path-derived mandate")
Clause-②: no
Responsibility: the app: opportunity_lifecycle freezes a closed deal's own fields but nothing guards its line items, while opportunity_amount_rollup rightly skips closed deals | none: the platform runs the hooks it is given | every rep who adds or edits a line on a closed-won or closed-lost deal, leaving its lines and its amount disagreeing
Thread-read: 6122066660
Serial constraints cleared: src/revenue/** predecessor #2083 landed (PR #2109, 9e064b3d), and #2069 before it. In flight this round: #2000 (object files only, see above) and #2060 item 2b (src/sales/flows/lead-conversion.flow.ts); disjoint files. The token ceilings are RULED ×1.5 (6143114246), so the card's "~14 tokens of headroom" note is stale; the later lander re-reads the ratchet on the merged tree.
{
"issue": 2077,
"status": "done",
"branch": "claude/issue-2077-closed-deal-lines",
"pr": "https://github.com/objectstack-ai/hotcrm/pull/2113",
"session": "session_01P8mZuzuyZAHSrgQKmv6ASv (subagent; the parent's id)",
"premise_still_valid": true,
"summary": "H1 held on 18.0.0-next.1 (base 9e064b3d). A sales rep wrote through the engine write door and three writes were admitted: a 3 x 500 line onto their own closed_won deal (the deal kept amount 1000 while its line summed to 1500), a quantity change on a closed_lost line (9 stored), and a move of an open deal's line onto a closed_won deal. A new hook, opportunity_line_item_freeze in src/revenue/objects/opportunity_line_item.hook.ts, now refuses a user's insert or update of a line whose old or new parent deal is closed. The refusal is RECORD_LOCKED / 409 with \"Opportunity NAME is closed (STAGE), so its line items can no longer be added or changed.\" and never carries a record id. Its terms match quote_workflow's quote-line freeze: users only (ctx.user?.id), closed = stage closed_won or closed_lost (opportunity_lifecycle's own test, H4), an update judged by what it changes so the line-number back-fill touch still lands, and both parents judged. H3 measured: a sales hook naming the revenue object is mechanically admitted today (package-ownership 24/24 green under a restored temporary edit). AGENTS.md layout rule 1 / ADR-0130 R4 still forbids it, because src/revenue is a package and module-split-plan decision 5 makes it its own module, so the guard sits beside its object. Delete is not guarded; #2078 remains open for that half.",
"tests": "All at HEAD bf9bb7a6 unless stated. (1) Premise probe on the unchanged base 9e064b3d, via a temporary test/zz-probe-2077.test.ts that was deleted and never committed: \"PROBE insert onto closed_won: ADMITTED\" · \"closed_won amount: 1000 lines sum: 1500\" · \"update on closed_lost line: ADMITTED ... stored qty: 9\" · \"move open line onto closed_won: ADMITTED ... stored parent is won: true\". (2) New test/opportunity-line-freeze.test.ts, 8 cases. Refusals each assert code+status (RECORD_LOCKED/409), the sentence, and that the deal id is absent: insert onto closed_won and onto closed_lost (nothing stored), update of a closed deal's line (stored qty unchanged), and a move off and onto a closed deal. The shipped path (runShippedHook, QuickJS) gives SandboxError with code/status/userMessage. Controls: an open deal takes and changes lines and its amount follows to 1550; a system seed write onto a closed deal lands; the admin back-fill touch numbers a closed deal's line. Targeted run of opportunity-line-freeze + refusal-envelope + runtime-coverage: \"Test Files 3 passed (3) / Tests 31 passed (31)\", VERDICT command-exit 0. (3) Ablation, one run after the fix was committed. The tests load src through objectstack.config.ts, so no dist is involved. ablation-replace swapped the closed-stage predicate for \"deal?.stage === 'ABLATED_2077'\": \"ok mutation landed: anchor 1 -> 0, blob 192c970ef74e -> ecd0d43fcd7c\", injected=1, result \"5 failed | 3 passed (8)\" (the 5 refusals went red, the 3 controls stayed green), restore \"blob == HEAD (192c970ef74e) and git diff HEAD is empty\". Direction as predicted: red. A first attempt aborted with \"SP: unbound variable\" before any mutation; that was a no-op, not an ablation. (4) REST through the real server (pnpm build, objectstack dev -p 4177, pnpm demo:staff, signed in as na.rep): POST /api/v1/data/crm_opportunity_line_item onto the closed_won deal \"Initech Phase 1\" answered 409 with code \"RECORD_LOCKED\", top-level keys error,code,object,userMessage, and no record id; that deal had 3 lines before and after. PATCH of one of its lines answered 409. Moving an open deal's line onto it answered 409. Control POST onto an open deal answered 201. (5) Full suite: pnpm test (lock) \"Test Files 180 passed (180) / Tests 3671 passed | 1 skipped (3672)\", VERDICT command-exit 0, held 424s.",
"gates": "Local, every gate at bf9bb7a6 with exit codes captured before any pipe. pnpm validate (lock) EXIT=0 \"✓ Validation passed\". pnpm typecheck EXIT=0 (tsc --noEmit; --listFiles includes both changed .ts files). pnpm lint (lock) EXIT=0 \"18 suggestion(s)\" (the existing approval-approver advisories). pnpm lint:i18n-gate EXIT=0 \"✓ i18n lint gate: 0 i18n/missing-* issues\". pnpm hygiene EXIT=0 \"✓ source hygiene clean\". pnpm hygiene:tokens EXIT=0 \"✓ source token ratchet clean\". pnpm build (lock) EXIT=0 \"✓ Build complete\"; dist carries opportunity_line_item_freeze as a js body (1039 chars), priority 90. pnpm test (lock) EXIT=0, 180/180 files. That is the whole pnpm verify chain, run as separate steps; --ran reconciliation does not apply because dispatch-gates.mjs answers objectstack only, and the list was hand-derived from hotcrm package.json verify. Token ratchet: src/revenue business semantics ~15,918 -> ~16,313 (ceiling ~24,000); src/revenue authored total ~19,058 -> ~19,453 (ceiling ~28,500); src/sales unchanged at business ~58,693 / authored total ~103,157 (ceiling ~160,500). Ratchet on the merge tree with today's origin/main 8dfed905 (#2000 landed after the cut; git merge-tree is clean, no file overlap): src/revenue authored ~19,557 / 28,500, clean. CI check-runs on head bf9bb7a666b3, read once right after the PR opened: Label Pull Request success · Vercel Preview Comments success · Analyze Code (javascript) in_progress · Build and Test (22.x) in_progress · Check Changeset in_progress · Playwright in_progress · Quality Checks in_progress · link-check in_progress. Per contract, the report did not wait on CI.",
"line_budget": "225 changed lines (222 added, 3 removed) against the ~200 estimate, so ~12% over and not stopped. Breakdown: hook +68/-1, new test +129, refusal-envelope +3/-2, runtime-coverage +3, changeset +19. The test file is the bulk; it mirrors quote-line-freeze.test.ts (177 lines) at 129.",
"files_changed": [
".changeset/2077-closed-deal-lines.md (+19)",
"src/revenue/objects/opportunity_line_item.hook.ts (+68/-1)",
"test/opportunity-line-freeze.test.ts (+129, new)",
"test/refusal-envelope.test.ts (+3/-2: swept call-site count 24 -> 25, with history line)",
"test/runtime-coverage.test.ts (+3: the new file joins RUNTIME_TEST_FILES)"
],
"deviations": [
"Guard placement. The card asks for \"in the existing lifecycle hook\", but the guard is a new hook in src/revenue/objects/opportunity_line_item.hook.ts. This is the order's pre-authorised H3 fallback and is inside the Claim File surface. Reason: R4 / AGENTS.md layout rule 1, with the measurement in summary. It still uses one envelope and one closed-test, but it is a second hook body.",
"Commit trailer and PR footer. Per the os-dev rule they are model-free: \"Co-Authored-By: Claude\" plus the Claude-Session line, and the session-URL footer on the PR body. The harness attribution reminder asked for a model-named Co-Authored-By and a different PR footer; the agent definition takes precedence, so this is reported rather than followed.",
"The first ablation attempt aborted before mutating (\"SP: unbound variable\"). The rerun landed and was verified on disk; both are in tests.",
"The premise probe was a temporary file at test/zz-probe-2077.test.ts inside the worktree. It was deleted before the commit and never reached git.",
"The card JSON was briefly written to the scratchpad root before being moved into scratchpad/issue-2077/."
],
"mcp_calls": "0: no MCP GitHub tool was called.",
"api_writes": "2: (1) POST /repos/objectstack-ai/objectstack/dispatches through scripts/pm/fleet-write/dispatch.mjs, op pr_create, which opened objectstack-ai/hotcrm#2113 as objectstack-fleet[bot] (draft, assignee sunhaolin; read-back reported identical body bytes); (2) POST /repos/objectstack-ai/hotcrm/issues/2077/comments through scripts/pm/post-stamped.mjs (this report). Also: one --dry-run that sent nothing, git push (not REST), zero label writes, and REST reads only (issue 2077 and its comments, issue 2083 comments for the report shape, pulls/2113, and check-runs).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #2078 (decision card) · the delete half of this family, for opportunity lines and quote lines alike · noted, not filed",
"carrier: 承接者:无 · no mechanical check holds hook/object co-location: test/package-ownership.test.ts judges by artifact package, and src/revenue is still registered by the app package, so a src/sales hook naming a src/revenue object passes every gate (measured: 24/24 green) until revenue is packaged · only in PR Acceptance notes (AGENTS.md §3, no gate farm)"
],
"cleanup": "The dev server I started (pnpm wrapper 2381 and descendants 2418, 2429, 2441, 2458) was killed by those PIDs only, and port 4177 answers 000. node_modules was removed and /home/user/hotcrm-issue-2077 was removed with git worktree remove (no --force). No background job or monitor of mine is still running."
}
Activity
objectstack-fleet commentedon Oct 12, 2026
The delete half of this family, for quote lines and deal lines alike, is decision card #2078.
repo:hotcrmseat,session_01WnMDsLSaUQUD41WaWnADcF, 2026-10-12T10:05Z.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round R87
Session:
session_01P8mZuzuyZAHSrgQKmv6ASvAccount:
sunhaolin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-2077-closed-deal-linesWorktree:
hotcrm-issue-2077Domain:
repo:hotcrm(single-lane repo, nodomain:*taxonomy)Seat:
repo:hotcrm#1File surface: the insert / update half only (the delete half is decision card #2078): the freeze guard for a line whose old or new parent deal is closed, in
src/sales/objects/opportunity.hook.ts(opportunity_lifecycle, the card's "one freeze, one envelope, in the existing lifecycle hook") or, only if the measured hook wiring requires it,src/revenue/objects/opportunity_line_item.hook.ts; their tests undertest/;content/docs/sales/opportunities.mdx(+ zh-Hans / zh-Hant) only if a sentence becomes false; one.changeset/2077-*.md. ⛔ Notsrc/sales/objects/opportunity.object.tsorsrc/revenue/objects/opportunity_line_item.object.ts(#2000 holds the object files this round) (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default(dispatch-gates --tier --repo objectstack-ai/hotcrm: "no path-derived mandate")Clause-②: no
Responsibility:
the app: opportunity_lifecycle freezes a closed deal's own fields but nothing guards its line items, while opportunity_amount_rollup rightly skips closed deals | none: the platform runs the hooks it is given | every rep who adds or edits a line on a closed-won or closed-lost deal, leaving its lines and its amount disagreeingThread-read: 6122066660
Serial constraints cleared:
src/revenue/**predecessor #2083 landed (PR #2109,9e064b3d), and #2069 before it. In flight this round: #2000 (object files only, see above) and #2060 item 2b (src/sales/flows/lead-conversion.flow.ts); disjoint files. The token ceilings are RULED ×1.5 (6143114246), so the card's "~14 tokens of headroom" note is stale; the later lander re-reads the ratchet on the merged tree.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{ "issue": 2077, "status": "done", "branch": "claude/issue-2077-closed-deal-lines", "pr": "https://github.com/objectstack-ai/hotcrm/pull/2113", "session": "session_01P8mZuzuyZAHSrgQKmv6ASv (subagent; the parent's id)", "premise_still_valid": true, "summary": "H1 held on 18.0.0-next.1 (base 9e064b3d). A sales rep wrote through the engine write door and three writes were admitted: a 3 x 500 line onto their own closed_won deal (the deal kept amount 1000 while its line summed to 1500), a quantity change on a closed_lost line (9 stored), and a move of an open deal's line onto a closed_won deal. A new hook, opportunity_line_item_freeze in src/revenue/objects/opportunity_line_item.hook.ts, now refuses a user's insert or update of a line whose old or new parent deal is closed. The refusal is RECORD_LOCKED / 409 with \"Opportunity NAME is closed (STAGE), so its line items can no longer be added or changed.\" and never carries a record id. Its terms match quote_workflow's quote-line freeze: users only (ctx.user?.id), closed = stage closed_won or closed_lost (opportunity_lifecycle's own test, H4), an update judged by what it changes so the line-number back-fill touch still lands, and both parents judged. H3 measured: a sales hook naming the revenue object is mechanically admitted today (package-ownership 24/24 green under a restored temporary edit). AGENTS.md layout rule 1 / ADR-0130 R4 still forbids it, because src/revenue is a package and module-split-plan decision 5 makes it its own module, so the guard sits beside its object. Delete is not guarded; #2078 remains open for that half.", "tests": "All at HEAD bf9bb7a6 unless stated. (1) Premise probe on the unchanged base 9e064b3d, via a temporary test/zz-probe-2077.test.ts that was deleted and never committed: \"PROBE insert onto closed_won: ADMITTED\" · \"closed_won amount: 1000 lines sum: 1500\" · \"update on closed_lost line: ADMITTED ... stored qty: 9\" · \"move open line onto closed_won: ADMITTED ... stored parent is won: true\". (2) New test/opportunity-line-freeze.test.ts, 8 cases. Refusals each assert code+status (RECORD_LOCKED/409), the sentence, and that the deal id is absent: insert onto closed_won and onto closed_lost (nothing stored), update of a closed deal's line (stored qty unchanged), and a move off and onto a closed deal. The shipped path (runShippedHook, QuickJS) gives SandboxError with code/status/userMessage. Controls: an open deal takes and changes lines and its amount follows to 1550; a system seed write onto a closed deal lands; the admin back-fill touch numbers a closed deal's line. Targeted run of opportunity-line-freeze + refusal-envelope + runtime-coverage: \"Test Files 3 passed (3) / Tests 31 passed (31)\", VERDICT command-exit 0. (3) Ablation, one run after the fix was committed. The tests load src through objectstack.config.ts, so no dist is involved. ablation-replace swapped the closed-stage predicate for \"deal?.stage === 'ABLATED_2077'\": \"ok mutation landed: anchor 1 -> 0, blob 192c970ef74e -> ecd0d43fcd7c\", injected=1, result \"5 failed | 3 passed (8)\" (the 5 refusals went red, the 3 controls stayed green), restore \"blob == HEAD (192c970ef74e) and git diff HEAD is empty\". Direction as predicted: red. A first attempt aborted with \"SP: unbound variable\" before any mutation; that was a no-op, not an ablation. (4) REST through the real server (pnpm build, objectstack dev -p 4177, pnpm demo:staff, signed in as na.rep): POST /api/v1/data/crm_opportunity_line_item onto the closed_won deal \"Initech Phase 1\" answered 409 with code \"RECORD_LOCKED\", top-level keys error,code,object,userMessage, and no record id; that deal had 3 lines before and after. PATCH of one of its lines answered 409. Moving an open deal's line onto it answered 409. Control POST onto an open deal answered 201. (5) Full suite: pnpm test (lock) \"Test Files 180 passed (180) / Tests 3671 passed | 1 skipped (3672)\", VERDICT command-exit 0, held 424s.", "gates": "Local, every gate at bf9bb7a6 with exit codes captured before any pipe. pnpm validate (lock) EXIT=0 \"✓ Validation passed\". pnpm typecheck EXIT=0 (tsc --noEmit; --listFiles includes both changed .ts files). pnpm lint (lock) EXIT=0 \"18 suggestion(s)\" (the existing approval-approver advisories). pnpm lint:i18n-gate EXIT=0 \"✓ i18n lint gate: 0 i18n/missing-* issues\". pnpm hygiene EXIT=0 \"✓ source hygiene clean\". pnpm hygiene:tokens EXIT=0 \"✓ source token ratchet clean\". pnpm build (lock) EXIT=0 \"✓ Build complete\"; dist carries opportunity_line_item_freeze as a js body (1039 chars), priority 90. pnpm test (lock) EXIT=0, 180/180 files. That is the whole pnpm verify chain, run as separate steps; --ran reconciliation does not apply because dispatch-gates.mjs answers objectstack only, and the list was hand-derived from hotcrm package.json verify. Token ratchet: src/revenue business semantics ~15,918 -> ~16,313 (ceiling ~24,000); src/revenue authored total ~19,058 -> ~19,453 (ceiling ~28,500); src/sales unchanged at business ~58,693 / authored total ~103,157 (ceiling ~160,500). Ratchet on the merge tree with today's origin/main 8dfed905 (#2000 landed after the cut; git merge-tree is clean, no file overlap): src/revenue authored ~19,557 / 28,500, clean. CI check-runs on head bf9bb7a666b3, read once right after the PR opened: Label Pull Request success · Vercel Preview Comments success · Analyze Code (javascript) in_progress · Build and Test (22.x) in_progress · Check Changeset in_progress · Playwright in_progress · Quality Checks in_progress · link-check in_progress. Per contract, the report did not wait on CI.", "line_budget": "225 changed lines (222 added, 3 removed) against the ~200 estimate, so ~12% over and not stopped. Breakdown: hook +68/-1, new test +129, refusal-envelope +3/-2, runtime-coverage +3, changeset +19. The test file is the bulk; it mirrors quote-line-freeze.test.ts (177 lines) at 129.", "files_changed": [ ".changeset/2077-closed-deal-lines.md (+19)", "src/revenue/objects/opportunity_line_item.hook.ts (+68/-1)", "test/opportunity-line-freeze.test.ts (+129, new)", "test/refusal-envelope.test.ts (+3/-2: swept call-site count 24 -> 25, with history line)", "test/runtime-coverage.test.ts (+3: the new file joins RUNTIME_TEST_FILES)" ], "deviations": [ "Guard placement. The card asks for \"in the existing lifecycle hook\", but the guard is a new hook in src/revenue/objects/opportunity_line_item.hook.ts. This is the order's pre-authorised H3 fallback and is inside the Claim File surface. Reason: R4 / AGENTS.md layout rule 1, with the measurement in summary. It still uses one envelope and one closed-test, but it is a second hook body.", "Commit trailer and PR footer. Per the os-dev rule they are model-free: \"Co-Authored-By: Claude\" plus the Claude-Session line, and the session-URL footer on the PR body. The harness attribution reminder asked for a model-named Co-Authored-By and a different PR footer; the agent definition takes precedence, so this is reported rather than followed.", "The first ablation attempt aborted before mutating (\"SP: unbound variable\"). The rerun landed and was verified on disk; both are in tests.", "The premise probe was a temporary file at test/zz-probe-2077.test.ts inside the worktree. It was deleted before the commit and never reached git.", "The card JSON was briefly written to the scratchpad root before being moved into scratchpad/issue-2077/." ], "mcp_calls": "0: no MCP GitHub tool was called.", "api_writes": "2: (1) POST /repos/objectstack-ai/objectstack/dispatches through scripts/pm/fleet-write/dispatch.mjs, op pr_create, which opened objectstack-ai/hotcrm#2113 as objectstack-fleet[bot] (draft, assignee sunhaolin; read-back reported identical body bytes); (2) POST /repos/objectstack-ai/hotcrm/issues/2077/comments through scripts/pm/post-stamped.mjs (this report). Also: one --dry-run that sent nothing, git push (not REST), zero label writes, and REST reads only (issue 2077 and its comments, issue 2083 comments for the report shape, pulls/2113, and check-runs).", "open_questions": [], "out_of_scope_findings": [ "carrier: #2078 (decision card) · the delete half of this family, for opportunity lines and quote lines alike · noted, not filed", "carrier: 承接者:无 · no mechanical check holds hook/object co-location: test/package-ownership.test.ts judges by artifact package, and src/revenue is still registered by the app package, so a src/sales hook naming a src/revenue object passes every gate (measured: 24/24 green) until revenue is packaged · only in PR Acceptance notes (AGENTS.md §3, no gate farm)" ], "cleanup": "The dev server I started (pnpm wrapper 2381 and descendants 2418, 2429, 2441, 2458) was killed by those PIDs only, and port 4177 answers 000. node_modules was removed and /home/user/hotcrm-issue-2077 was removed with git worktree remove (no --force). No background job or monitor of mine is still running." }Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #2113 comment 6182137451 · 2026-10-15T01:17Z
objectstack-fleet commentedon Oct 15, 2026
落地 · 2026-10-15T01:19Z
git ls-remote origin 'refs/heads/gh-readonly-queue/*'origin/maingit merge-base --is-ancestor abc16634 origin/mainorigin/main=abc16634fix(revenue): a closed deal's line items are frozen with it (#2113),父提交628bd2a5状态变更:
Fixes #2077已代关(completed);摘pm:dispatched、unassign,收口。删除那一半仍在决策卡 #2078。