Repository navigation
Hooks that write or read across objects as the triggering person fail silently for reps, agents and web forms — 11 hook sites, one fix pattern (runAs), measured on the real engine by #2013 #2014
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpm:queueReady for the PM dispatch loopReady for the PM dispatch looppriority:p2Medium: important, M3Medium: important, M3
on Oct 8, 2026 - changed the title
[-]Hooks and flows that write or read across objects as the triggering person fail silently for reps, agents and web forms (11 hooks) + 6 more app defects — measured on the real engine by #2013[/-][+]Hooks that write or read across objects as the triggering person fail silently for reps, agents and web forms — 11 hook sites, one fix pattern (runAs), measured on the real engine by #2013[/+]on Oct 8, 2026 - addedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchand removedpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
on Oct 8, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsClaim: PM loop round R74
Session:session_012zh91QzFgePbkmuHnugLN3
Account:os-zhuang(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-2014-hooks-run-as
Worktree:hotcrm-issue-2014
Domain:repo:hotcrm(single-lane repo, nodomain:*taxonomy)
Seat:repo:hotcrm#1
File surface: the eleven hook sites named in the card body:src/revenue/objects/quote.hook.ts,src/service/objects/{article_feedback.hook.ts,case.hook.ts,_case-assignment.ts},src/marketing/objects/campaign_member.hook.ts,src/sales/objects/{opportunity.campaign-metrics.hook.ts,lead.campaign-metrics.hook.ts,lead.hook.ts,opportunity.hook.ts}(the hook declarations and their bodies). A persona's profile undersrc/*/profiles/only where a site's measured answer is a grant rather than elevation. The tests that pin these facts (test/{knowledge-feedback,campaign-member-lifecycle,hooks-runtime-service,demo-staffing,case-assignment,opportunity-account-capability-gate}.test.ts, the F1 rep case beside the quote-acceptance tests), and the #1363 organization-predicate guard's roster where an elevated site joins it. One.changeset/2014-*.md. ⛔ No change to the five split-out cards' sites (#2015–#2019) (stop on breach; explain in the report)
Container & model:L,mode:subagent(the os-dev definition and thescripts/pmwrite tools live in this container's objectstack checkout),model: default(dispatch-gates --tier --repo objectstack-ai/hotcrmanswers "no path-derived mandate … the tier stays the PM's per-card judgment call";governedPathsInover the surface: none)
Clause-②: no
Responsibility:the app's hooks (they read or write across objects throughctx.apias the caller, andonError: 'log'swallows the refusal) | the platform's hookrunAs(spec 17.5.0 and later; objectstack#14010) already covers it and is unused here | every sales rep, service agent, marketing user and web-form submitter reaches it through REST or UI writes, on any install today
Thread-read: none
Serial constraints cleared: #1595 (PR #2013) landed at9451b6de; this card flips its pins. #2018 also editslead.campaign-metrics.hook.tsand waits for this card. #1919 (comments in the same hook files) waits for this card. Sibling claims in flight: #2015 (quote-generation.flow.ts) and #2016 (lead-conversion.flow.ts,sales-rep.profile.ts); disjoint files.
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsSeat note for the dev in flight (
repo:hotcrmseat,session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T14:37Z). It is a measured case on this card's F1 hook,quote_on_accepted, reported by the #2015 dev (report6062202342). ⛔ Not a scope change.- Measured (sales_rep, a deal pending
opportunity_approval, so the record is locked): the quote goes presented → accepted, both writes admitted.quote_on_accepted(async,onError: 'log') then has its close-won write refusedRECORD_LOCKED, and the refusal is logged and suppressed. The quote reads Accepted, the deal stays at qualification / pending, and an accepted quote is frozen, so close-won never re-runs once the approval lands. The same run also drafts no contract: the F1 permission refusal, independent of the lock. - What this card's fix must not do silently: if
runAs: 'system'on this hook also lifts the approval lock, an accepted quote would close-won a deal its manager has not approved. Measure that case. If elevation crosses the lock, the business question goes back asneeds_decision: may an accepted quote close a deal still under approval? Do not choose either answer.
Generated by Claude Code
- Measured (sales_rep, a deal pending
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 2014,
"status": "needs_decision",
"branch": "claude/issue-2014-hooks-run-as",
"pr": "#2022",
"session": "session_012zh91QzFgePbkmuHnugLN3 (subagent; the parent's harness Claude-Session line)",
"premise_still_valid": true,
"summary": "Mechanism assumption 1 HOLDS, measured on 17.7.0: runAs: 'system' on F12 elevates ctx.api on the in-process handler AND on the shipped lowered body (dist/objectstack.json booted with bootStack; control leg without runAs reproduces the defect), and updated_by keeps naming the person. Implemented (draft PR #2022, Part of #2014): runAs: 'system' with the rule-9 reason and rule-10 note beside each: F12 article_feedback_metrics_refresh, F13 campaign_member_optout_sync, F15b campaign_attribution_refresh, F15c campaign_lead_conversion_refresh, F14 case_escalation_reassign (pool read + load counts pinned to the case's organization_id; the tenant-less SLA sweep otherwise reads every organization's pool), F15a as a NEW hook case_resolution_account_activity split out of case_status_side_effects (runAs is per hook; the escalation task keeps the agent's narrow crm_task.allowTransfer grant), and the two extra members, both measured as defects and added: campaign_member_metrics_refresh (a marketer's recount drops Private won deals: 2 won / 1,100 became 1 / 100) and opportunity_promote_account (a rep's win on an account another user owns leaves it a prospect). No profile changed. Every #2013 pin for these sites flipped to the business fact; new cases: marketer Private-deal recount, rep promotes a non-owned account, two-organization escalation pin. Returned as needs_decision (open_questions): F1 quote_on_accepted (measured per the seat note: elevation closes a deal whose opportunity_approval is still pending), F9 x3 (elevation makes the anonymous form door hand back hook-derived CRM facts: an email-existence oracle and a staff user id), F11 (elevated refusal names and classifies an account the caller cannot read). Deviations: (1) PR first line isPart of #2014, not the closing-keyword line the dispatch named: three sites are in the decision box and os-dev forbids closing a card there; conflict noted. (2) F1 was first implemented as a split and then reverted to main on the seat's note; nothing of it is in the PR. (3) File surface beyond the claim: test/hooks-runtime-sales.test.ts (one new case in the opportunity_promote_account describe, nowhere near the #2019 deletion pins), docs/feature-inventory.md SVC-009 (names the split-out hook). (4) The harness attribution reminder names a model (Co-Authored-By: Claude Opus 5.5); commits carry the model-free pair per dispatch, not imitated. files_changed: src/service/objects/{article_feedback.hook.ts,case.hook.ts,_case-assignment.ts}, src/marketing/objects/campaign_member.hook.ts, src/sales/objects/{opportunity.campaign-metrics.hook.ts,lead.campaign-metrics.hook.ts,opportunity.hook.ts}, test/{knowledge-feedback,campaign-member-lifecycle,hooks-runtime-service,hooks-runtime-sales,demo-staffing,case-assignment}.test.ts, docs/feature-inventory.md, .changeset/2014-derived-writes-as-system.md. The #1363 guard walks scheduled flows only and does not reach hooks; it stays green and unchanged, and no new guard was added (new gates default no). Worktree removed after the report.",
"tests": "GATE pnpm verify @ ee2a7e2:OS_VERIFY_LOCK_SLOT=hotcrm-2014 bash scripts/pm/os-verify-lock.sh -c 'pnpm verify'->os-verify-lock: VERDICT command-exit 0 · held the lock 534s; validate✓ Validation passed; typecheck clean; lint --strict 18 suggestions no error; lint:i18n-gate✓ ... 0 i18n/missing-* issues; hygiene✓ source hygiene clean; hygiene:tokens✓ source token ratchet clean; build✓ Build complete; testTest Files 172 passed (172)Tests 3565 passed | 1 skipped (3566). Earlier verify on 33380ba: VERDICT command-exit 1, one failure (runtime-coverage: case_resolution_account_activity named only in a comment), fixed in ee2a7e2. MEASURE M1 F12 in-process: inherit helpful_after=1 / runAs system helpful_after=0, updated_by=admin both. M2 shipped artifact: runAs system (registered body + runAs) helpful_after=0; control build without runAs helpful_after=1. MEASURE M8 F1 (rep, deal pending opportunity_approval, rep edit refused RECORD_LOCKED): shipped -> oppStage proposal, approval pending, contracts 0; runAs system on quote_on_accepted -> oppStage closed_won, approval_status pending, request pending, contracts 1; split (draft elevated only) -> oppStage proposal, contracts 1. File restored to HEAD after each leg (blob f9f82930 == HEAD). MEASURE M6 F9 elevated: anonymous insert result carries duplicate_status suspected, duplicate_of_type crm_contact, duplicate_of_contact id, owner_id staff id (46 keys); restored. M3: rep cannot read another rep's lead/contact; rep's own dedupe flags nothing (elevated: flags both). M4/M5/M9 as in summary/findings. ABLATION (scripts/ablation-replace.mjs, anchor hit, restore blob == HEAD): F14 organization pin removed ->stays inside the escalating case's organizationred, other org's manager picked; runAs removed from campaign_member_metrics_refresh -> Private-deal case red with num_won 1 (exp 2), actual_revenue 100 (exp 1,100). NOT MEASURED locally, declared to CI: test:coverage thresholds, docs-app types:check, e2e.",
"mcp_calls": "0",
"api_writes": "3 relay writes as objectstack-fleet[bot], each one repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create -> POST /repos/objectstack-ai/hotcrm/pulls (draft, #2022, 11,496 bytes read back identical); (2) label-write --assign os-zhuang -> POST /repos//issues/2022/assignees (read back MATCHES); (3) this os-dev-report comment -> POST /repos//issues/2014/comments. Plus git push of the branch (not REST). Reads via gh api (issue, comments, PR).",
"open_questions": [
{
"question": "F1 quote_on_accepted: may an accepted quote close a deal whose opportunity approval is still pending, and how should a sales rep's acceptance get its contract drafted? Measured (sales_rep, deal pending opportunity_approval): as shipped, acceptance admitted, close-won refused RECORD_LOCKED and swallowed, 0 contracts; runAs system on the hook: deal closed_won with its approval still pending, contract drafted; contract draft alone split out and elevated: deal stays proposal/pending, contract drafted. The accepted quote is frozen, so close-won never re-runs once the approval lands.",
"options": [
"A. runAs: 'system' on the whole hook. Business: the CPQ path always closes the deal and drafts the contract, but a deal its manager has not approved closes anyway (the lock is skipped for a system write). Long-term: approval locks stop meaning anything for the CPQ path. AI-safety: teaches that elevating a hook is a way through a lock. Startup: one line.",
"B. Split: the contract draft becomes its own elevated hook; close-won stays the caller's write. Business: reps get their contract; a locked deal stays open, but silently, with an accepted quote that never re-closes it after approval. Long-term: rule 9 minimal elevation, but leaves an incoherent state (accepted quote, open deal) with no recovery path. AI-safety: neutral; the silent swallow remains. Startup: small (one hook split, test renames).",
"C. Refuse the acceptance itself while the linked deal's approval is pending (a transition gate on crm_quote, RECORD_LOCKED at the door), plus B's elevated draft. Business: an approval stays an approval, and the rep learns at the moment of acceptance why it cannot proceed. Long-term: no incoherent state to recover from. AI-safety: a loud refusal at the door instead of a swallowed one in a cascade. Startup: one new refusal branch in quote_workflow plus B.",
"D. Grant sales_rep crm_contract.allowCreate instead of elevating the draft; close-won unchanged. Business: reps can author contracts by hand, which the permission set withholds today; the lock question stays as shipped (silent)."
],
"recommendation": "C, because on the business axis an approval that an accepted quote can walk around is no approval, and B alone leaves a frozen accepted quote on an open deal with no path back; on the AI-safety axis a refusal at the acceptance beats a refusal swallowed by onError: 'log'; it is small for a startup and keeps rule 9 (only the draft is elevated). The seat asked that neither answer be chosen here, so nothing of F1 is in the PR."
},
{
"question": "F9 lead_auto_assign / case_auto_assign / lead_duplicate_check: may an elevated hook's findings reach the anonymous form caller? Measured with the three hooks elevated: the engine insert under the form door's context returns, and the door's 201 body is { record: result } (rest 17.7.0 createData), duplicate_status 'suspected', duplicate_of_type 'crm_contact', the existing contact's id, and owner_id = a staff user's id. guest_portal's own rule: guests must never read existing CRM data. Also measured: as a sales_rep the duplicate scan sees no other rep's lead or contact (private OWD), so cross-rep duplicates are never flagged either (the #1207 scenario).",
"options": [
"A. Elevate all three as-is. Business: web leads and cases are routed and web duplicates flagged; any visitor can test whether an email is in the CRM and learn staff user ids. Long-term: a public oracle. AI-safety: worst, it normalises elevated reads behind an anonymous door. Startup: three lines.",
"B. Platform first (AGENTS.md section 2): file upstream that the anonymous form door returns the full stored record to a caller whose grant has no read (return only what guest_portal may read, or only the id); the app WAITS, then elevates all three. Business: routing and dedupe for web intake land only when the platform ships it. Long-term: fixes the door for every app, contract-first. AI-safety: best, declaration (allowRead: false) becomes enforced on the write response. Startup: zero app code now.",
"C. Elevate now and stand the hooks down for the anonymous door explicitly (no user and not system): reps get cross-rep dedupe (M3) and system intake keeps routing; web intake keeps today's behaviour, by declaration instead of by denial. Business: the web-form half of #596 still does not hold. Long-term: a branch to delete when B lands. Startup: small.",
"D. Move the web-intake work after the response (an elevated async afterInsert hook stamping owner / duplicate flags), so the 201 body never carries them. Business: routing lands for web intake now, but lead_assignment's SLA alert (afterInsert flow) races the owner stamp. Long-term: restructures three hooks around a platform behaviour. Startup: largest."
],
"recommendation": "B, because the defect that makes elevation unsafe is the platform's (an anonymous write door echoing data its caller's grant cannot read), AGENTS.md section 2 says wait for the platform rather than route around it, and contract-first puts the fix where every app benefits; if the maintainer wants the rep half (M3) now, C is the interim that stays inside the guest rule."
},
{
"question": "F11 opportunity_account_capability: may the gate's refusal name an account the caller cannot read? Elevated (runAs: 'system'), the gate does refuse an outsider's deal on a Settlement Only account, but its refusal text names the account the way the UI does (ACC-000042 - Tender Agency Ltd, pinned by #1243) and states its Settlement Only capability to a caller with no read on it. Not implemented here.",
"options": [
"A. Elevate, keep the name. Business: the gate closes; an id-holder learns an unreadable account's number, name and capability. AI-safety: teaches echoing elevated reads to the caller. Startup: one line.",
"B. Elevate, and the refusal names no account ('this account'). Business: the gate closes; the caller already chose the account in the same form, so the dialog stays clear; it still reveals one bit (Settlement Only) about an account the caller named. Long-term: a single code path. AI-safety: a teachable rule (an elevated hook's refusal echoes nothing it read under elevation). Startup: one line plus re-pinning the #1243 naming case.",
"C. Keep inherit and refuse any account the caller cannot read. Business: a new access policy (no deals on unreadable accounts) that also captures Full accounts and moves the missing-account refusal ahead of the engine's own. Long-term and startup: a sharing-model decision taken inside a hook."
],
"recommendation": "B, because it closes the gate (the business requirement of REQ-0003) without disclosing what elevation read, keeps one code path, gives AI a simple rule to copy, and costs one line; the only loss is the account label in a dialog whose form already shows the account."
}
],
"out_of_scope_findings": [
"class: a · reach: the engine's write door (verify hooks.run, the REST data ingress's call) as a service_agent: completing a task on their own case leaves the case's account last_activity_date empty, while the admin's identical completion stamps today (measured on 17.7.0 after this PR) · evidence: task_activity_bubble writes crm_account / crm_contact / crm_lead recency through ctx.api as the caller and swallows each refusal in a best-effort catch; event_activity_bubble carries the same body verbatim (not measured separately) · same family as this card: name it for #2014's family (a sub-issue), not a single-point card · dedupe words: task_activity_bubble, event_activity_bubble, last_activity_date service agent, activity bubble runAs",
"carrier: objectstack#22301 (the verify-handle gaps) · noted, not filed · test/helpers/verify-stack.ts runShippedHook calls the bound body handler directly, outside wrapDeclarativeHook, with no ctx.api, so it never applies a hook's runAs: a suite driving an elevated hook through runShippedHook measures the caller path (read in runtime/objectql 17.7.0; no suite does so today)",
"carrier: 承接者:无 · noted, not filed · the anonymous form door returning the full stored record to a guest_portal caller (allowRead: false) is harmless today and becomes a disclosure the moment any hook on crm_lead / crm_case derives a field from existing data; it is option B of the F9 question rather than a separate card"
]
}objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsVerdict: os-dev report
6063106768, PR #2022 — ACCEPT the delivered part, Tier S (not governed): the seat lands it through the merge queue once every check on the head is green. Three sites go to the maintainer, so this card stays open.repo:hotcrmseat,session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T15:22Z.Claim amendment (file surface) · claim
6061539417. Two edits outside the declared surface are accepted:test/hooks-runtime-sales.test.ts: one new case in theopportunity_promote_accountdescribe, away from The account and contact deletion guards say "Close or reassign", but closed deals and expired or terminated contracts still block the delete (required lookups restrict) #2019's deletion pins.docs/feature-inventory.mdSVC-009: it names the split-out hook this change creates. Without it the row would be stale.
Checked on GitHub and in git, not taken from the report:
- Head
ee2a7e2e, 15 files, +324 / −133.check-governed-merges --prfinds no path on the register. - PR body: first line
Part of #2014, the right form for a partial delivery. No closing keyword appears anywhere in the body, so merging leaves this card open, and the seat removespm:dispatchedby hand at landing. - Mechanism measured first:
runAs: 'system'elevatesctx.apiboth in process and in the shipped lowered body (dist/objectstack.jsonbooted withbootStack). A control build without it reproduces F12.updated_bystill names the person. - Every elevated site carries its rule-9 reason and its rule-10 organization note beside the code.
- F14 pins the escalating case's
organization_idon the pool read and the load counts. An ablation that removes the pin turns the two-organization case red. - The rollups (F12, F15b, F15c, the campaign recount) are keyed to the triggering record's parent, and the elevated
ctx.apikeeps the trigger's tenant. - The [finding] The scheduled-flow org-partition guard only inspects
create_record— anupdate_recordwriting a value sourced outside the swept row is uncovered #1363 guard walks scheduled flows only. It stays green and unchanged, and no new guard is added.
- F14 pins the escalating case's
- Sites delivered: F12, F13, F14, F15a (as a new hook,
case_resolution_account_activity, so the agent's narrow task grant is not elevated with it), F15b and F15c. Two members the seat had not measured were measured as defects and added:campaign_member_metrics_refresh: a marketer's recount dropped Private won deals, 2 / 1,100 → 1 / 100. The ablation turns it red.opportunity_promote_account: a rep's win on another user's account left it a prospect.
- Every test: run the hook, flow and action suites on @objectstack/verify and retire the five hand-built harnesses #2013 pin for these sites now asserts the business fact.
- Changeset (
'hotcrm': patch), checked against the diff: each clause names a site the diff changes. "an agent's own escalation hands the case to a service manager" is F14; "a marketing user's enrollment no longer drops Private won deals" is the campaign recount. pnpm verifyis green onee2a7e2e: 3,565 passed, 1 skipped.
Held for the maintainer (not in the PR):
- F1
quote_on_accepted: elevation closes a deal whose approval is still pending. - F9, the three web-intake hooks: elevation makes the anonymous form door return CRM facts, an email-existence oracle and a staff user id.
- F11, the capability gate: an elevated refusal names an account the caller cannot read.
One more member of this family, reported by the dev:
task_activity_bubble(measured: a service agent completing a task on their own case leaves the account'slast_activity_dateempty, while the admin's identical completion stamps it) and its verbatim twinevent_activity_bubble(not measured separately). They join this card's remaining work.
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsLanded (part): PR #2022 MERGED as
dc58e047onmain(squash, single parent49fe305a). The seat landed it through the merge queue after 9 of 9 checks went green on headee2a7e2e:ready_for_review15:30:16Z →added_to_merge_queue15:30:20Z → merged 15:30:38Z.repo:hotcrmseat,session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T15:31Z.Transition:
pm:dispatched→needs-user-decision· Release: claim6061539417. The delivered sites are onmain. What remains needs the maintainer, so no dev may pick it up yet.Two readings:
- the PR object reads
merged: true; origin/mainisdc58e047 fix(hooks): land the follow-on writes a person's own save fires (runAs: 'system').
Not closed, as intended: the PR said
Part of #2014, and this card is still open. No card was closed by the merge.On
mainnow:- F12, F13, F14 (organization-pinned) and F15a–c;
campaign_member_metrics_refreshandopportunity_promote_account.
Remaining on this card:
- F1
quote_on_accepted: may an accepted quote close a deal still under approval, and how does a rep's acceptance get its contract? - F9, the three web-intake hooks: elevation makes the anonymous form door return CRM facts.
- F11, the capability gate: an elevated refusal names an account the caller cannot read.
task_activity_bubble/event_activity_bubble: the same caller-context shape, measured for a service agent's task.
All three questions are in the maintainer's batch hotcrm-R74b (items 3–5) in the seat's chat. The answer is recorded here, and the remaining work is dispatched as this card's second round.
Unblocked by this merge: #2018 (F16, the same hook file), #2019, and then #1919.
Generated by Claude Code
- the PR object reads
- addedneeds-user-decisionNeeds the maintainer's call before work proceedsNeeds the maintainer's call before work proceedsand removedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Oct 8, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsRuling: batch hotcrm-R74b items 3, 4, 5 · letters C, B, B · maintainer 「同意」 2026-10-09T07:53Z
repo:hotcrmseat,session_012zh91QzFgePbkmuHnugLN3. The maintainer answered 「同意」 in this session's chat to the batch presented there with the recommendations1D 2B 3C 4B 5B. Items 3–5 are the three questions the first-round dev raised (report6063106768).The decisions
- Item 3 · F1
quote_on_accepted· C. While the linked deal's approval is pending, accepting the quote is refused at the moment of acceptance, loudly. The contract draft also moves into its own elevated hook (B's split), and close-won stays the caller's write. So an approval stays an approval; the rep learns why at the acceptance; and no "accepted quote, open deal" state is left with no way back. Not chosen: A (elevate the whole hook; it closes a deal its manager has not approved), B alone (leaves that frozen state), D (grant reps contract create; the lock stays silent). - Item 4 · F9, the three web-intake hooks · B. Platform first. Filed: objectstack#22437 (the public form submit answers the anonymous caller with the whole stored record). hotcrm WAITS, then elevates
lead_auto_assign,case_auto_assignandlead_duplicate_check. Not chosen: A (a public email-existence oracle), C (elevate but skip anonymous), D (async tagging). - Item 5 · F11
opportunity_account_capability· B. Elevate, and the refusal names no account ("this account"); re-pin The raw-id-in-prose class survives in five more places after #1208 — 15 of 31 tasks in a demo org still name a record by its primary key #1243's naming case. Not chosen: A (echo an unreadable account's number, name and capability), C (a sharing policy decided inside a hook).
What this card does next
- Second round,
pm:queuein this act: F1 (C), F11 (B), and thetask_activity_bubble/event_activity_bubblepair the first-round dev reported (same caller-context shape, measured for a service agent). AGENTS.md rules 9 and 10 hold for every elevation, as in round 1. - F9 is not in the second round. When round 2 lands, this card moves to
pm:blockedwithBlocked-by: objectstack-ai/objectstack#22437and only F9 left. It is not split into a card of its own, because this round's filing quota (three per round) is spent on objectstack#22437, chore(deps): upgrade@objectstack/*past 17.7.0 once the next release publishes — it carries the platform fixes #2016, #2000, #1583 and #1585 wait on #2031 and With the 立项 gate armed, Generate Quote on a deal not yet 立项-approved reports failure but leaves its quote behind — refuse it before 立项, as ruled #2032.
Generated by Claude Code
- Item 3 · F1
- addedpm:queueReady for the PM dispatch loopReady for the PM dispatch looppm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchand removedneeds-user-decisionNeeds the maintainer's call before work proceedsNeeds the maintainer's call before work proceedspm:queueReady for the PM dispatch loopReady for the PM dispatch loop
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round R75
Session:session_012zh91QzFgePbkmuHnugLN3
Account:os-zhuang(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-2014-round-2
Worktree:hotcrm-issue-2014-r2
Domain:repo:hotcrm(single-lane repo, nodomain:*taxonomy)
Seat:repo:hotcrm#1
File surface:src/revenue/objects/quote.hook.ts(quote_workflow's acceptance refusal;quote_on_acceptedsplit, the contract draft into its own elevated hook);src/sales/objects/opportunity.hook.ts(opportunity_account_capabilityonly);src/sales/objects/task.hook.tsandsrc/sales/objects/event.hook.ts(task_activity_bubble/event_activity_bubble); their tests (test/hooks-runtime-sales.test.ts,test/hooks-runtime-service.test.ts,test/opportunity-account-capability-gate.test.ts,test/activity-recency.test.ts, and thetest/quote-accepted-*.test.ts/test/refusal-envelope.test.tspins the split moves); the acceptance and approval passages ofcontent/docs/sales/quotes*.mdxandcontent/docs/administration/state-machines*.mdx(⛔ not the "Generating a quote" section, #2032's); translations only for a new refusal message; one.changeset/2014-*.md(stop on breach; explain in the report)
Container & model:M,mode:subagent(the os-dev definition and thescripts/pmwrite tools live in this container's objectstack checkout),model: default(dispatch-gates --tier --repo objectstack-ai/hotcrmanswers "the tier stays the PM's per-card judgment call";governedPathsInover the surface: none)
Clause-②: no
Responsibility:the app (hooks write or read across objects as the triggering person, which the person's grants refuse; the acceptance path swallows the lock refusal) | none: the platform enforces the grants and the lock as declared | every rep accepting a quote on a deal under approval; every agent completing a task on a case; every outsider's deal on a Settlement Only account
Thread-read: 6076828219
Serial constraints cleared: round 1 (PR #2022,dc58e047) landed. #1919 (comments in hook files) waits behind this. Sibling claims in flight: #2030 (CHANGELOG.md), #2032 (quote-generation.flow.ts, the "Generating a quote" docs section); disjoint files, the shared docs pages split by section.
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 2014,
"status": "done",
"branch": "claude/issue-2014-round-2",
"pr": "#2035",
"session": "session_012zh91QzFgePbkmuHnugLN3 (subagent; the parent's harness Claude-Session line)",
"premise_still_valid": true,
"summary": "Round 2 delivered as draft PR #2035 (first linePart of #2014,Clause-②: no; the card stays open for F9, untouched: lead_auto_assign / case_auto_assign / lead_duplicate_check unchanged). MEASURED FIRST on base 08cfa20, all three reproduced: F1 a sales_rep's presented→accepted on a deal pending its Large Deal Approval admitted, close-won refused RECORD_LOCKED and swallowed, contract insert PERMISSION_DENIED, 0 contracts, deal proposal/pending (unlocked deal: closed_won, still 0 contracts); F11 an outsider who reads 0 rows of a Settlement Only account inserts a deal on it → STORED; bubble: an agent's completed task AND an agent's held call (event twin, measured this round) on their own case leave the account's last_activity_date empty (account write PERMISSION_DENIED) while the admin's identical write stamps it; a rep's completed task on their own deal whose account/contact a manager owns stamps neither. 'Held' measured per state with the rep's own direct close-won: approval_status pending (platform lock, request lockRecord true) refused; approved/rejected (a recall leaves rejected) admitted; status_change_approval_status pending/rejected refused (opportunity_lifecycle; plus the lock once a request is open); qualification_approval_status pending/rejected refused (opportunity_lifecycle; its request is lockRecord false); approved states admitted; a SYSTEM close-won on a locked deal admitted. A rep cannot read sys_approval_request (PERMISSION_DENIED), so the gate reads the deal's own mirror columns. FIX: (F1, C) quote_workflow refuses a USER's transition into accepted while the linked open deal is held in exactly those states, RECORD_LOCKED/409 (existing code), message names the quote by its title and the hold by the field label/value the rep sees, reads the deal AS THE CALLER (no elevation; unreadable deal or failed read stands down); the contract draft moved out of quote_on_accepted into its own hook quote_accepted_contract_draft (runAs: 'system', rule-9 reason beside it; rule 10: no read, the insert is stamped from the trigger's tenant which withRunAs('system') carries, pinned by test/hook-org-inheritance.test.ts); close-won stays the caller's write in quote_on_accepted. (F11, B) opportunity_account_capability runAs: 'system', reads only commercial_capability, pinned to the new deal's organization_id when present, refusal says 'this account' and names nothing; #1243's naming case re-pinned (the message has one locale, English; the three docs locales updated). Bubble: task_activity_bubble and event_activity_bubble runAs: 'system'; every walk-up read pinned to the triggering record's organization_id; because a by-id update cannot carry an extra where key (engine refuses it), each write target is confirmed inside the organization by one pinned read per object; the first-response stamp writes only when its pinned read found the case. Docs (3 locales): quotes (a new Built-in rules bullet, the acceptance passages; not the 'Generating a quote' section), state-machines (new paragraph after the CPQ edge sentence), accounts (the Settlement Only refusal no longer names the account). Changeset .changeset/2014-accept-quote-under-approval.md ('hotcrm': patch). files_changed (29 vs main): src/revenue/objects/quote.hook.ts, src/sales/objects/{opportunity.hook.ts,task.hook.ts,event.hook.ts,_picklists.ts (comment)}, src/revenue/objects/contract.object.ts (comment), test/{hooks-runtime-sales,hooks-runtime-service,opportunity-account-capability-gate,activity-recency,case-first-response,quote-accepted-draft-defaults,quote-accepted-lookups,quote-accepted-payment-terms,refusal-envelope,contract-write-depth,record-id-not-in-prose,quote-contact-required-when}.test.ts, content/docs/sales/{quotes,accounts}{,.zh-Hans,.zh-Hant}.mdx, content/docs/administration/state-machines{,.zh-Hans,.zh-Hant}.mdx, docs/feature-inventory.md (QUO-005/006), .changeset/2014-accept-quote-under-approval.md. DEVIATIONS: (1) surface beyond claim 6076910943: contract.object.ts + _picklists.ts comments (they name the payment-terms carrier, pinned by quote-accepted-payment-terms), accounts*.mdx (the dispatch names the Settlement Only refusal docs), comment-only renames in contract-write-depth / record-id-not-in-prose / quote-contact-required-when, case-first-response (pinned the caller-context behaviour removed: a writer who cannot read the case now stamps its first response — the whole event bubble is elevated per the ruling, so that write is too), feature-inventory QUO-005/006. (2) The three quote-accepted suites and the hooks-runtime-sales acceptance cases now accept as a SALES REP (their comments said the manager was only a workaround for this defect), beyond 'add the rep case'. (3) src/revenue sat ~178 tokens under its authored-total ceiling; the split + gate were paid by compacting quote.hook.ts code (no ceiling moved; authored total 18,990 / 19,000, business semantics 15,879 / 16,000); the refusal names 'its opportunity' rather than the deal's name for the same reason. (4) Source hygiene refused record ids in the two bare cascade diagnostics once they left the failures.push sink: they now name the quote by its quote_number - name label / quote number; quote-accepted-lookups' message pins follow. (5) Organization boundary recorded: with the F11 pin a TENANT-LESS system insert of a deal in org A on org B's Settlement Only account is admitted (was refused by the unscoped read) — the same answer a person's tenant-scoped read gives. (6) Merge commit 73c712a (origin/main f071b36 merged, no rebase) carries git's default message, no trailer; the harness attribution reminder names a model (Co-Authored-By: Claude Opus 5.5) — commits carry the model-free pair per dispatch, not imitated. (7) CI was in_progress at report time (Check Changeset, Quality Checks, link-check, Label already success). Worktree removed (node_modules first, no --force).",
"tests": "GATE pnpm verify @ a83103b:OS_VERIFY_LOCK_SLOT=hotcrm-2014 bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c 'pnpm verify'→os-verify-lock: VERDICT command-exit 0 · held the lock 477s (7m57s) · waited 0s; validate✓ Validation passed; tsc --noEmit clean; lint --strict18 suggestion(s), no error; lint:i18n-gate✓ i18n lint gate: 0 i18n/missing-* issues; hygiene✓ source hygiene clean; hygiene:tokens✓ source token ratchet clean(src/revenue authored ~18,990 / 19,000; src/sales headline unchanged ~57k / ~28k / ~101k); build✓ Build complete; testTest Files 172 passed (172)Tests 3589 passed | 1 skipped (3590). Earlier verify @ 73c712a: VERDICT command-exit 1, source hygienerecord id in prosex2 in quote.hook.ts cascade diagnostics, fixed in a83103b. MEASURE (scratch, never committed, through hooks.run on 08cfa20): M1 accept=admitted, closeWonWrite=RECORD_LOCKED, contractInsert=PERMISSION_DENIED, contracts 0, opp proposal/pending; M1b unlocked: closed_won, contracts 0; M1c per-state rep close-won table as in summary; M5 recall → approval_status rejected, request recalled, rep close admitted; M2 outsiderReadsAccount 0, outsiderInsert admitted; M3 agent task: account null (PERMISSION_DENIED) vs admin 2026-10-09; M4 agent held call: account null vs admin stamped. AFTER (same scratch on the fix): M1 RECORD_LOCKED/409, quote presented, 0 opportunity updates, 0 contract inserts; M1b contracts 1, closed_won; M2 outsider VALIDATION_FAILED 'this account'; M3/M4 stamped. ABLATION (one call under the lock,scripts/ablation-replace.mjs, each anchor x1 → x0, each restoreblob == HEADandgit diff HEADempty, final tree clean, scratch file removed): control two-org 3/3 green; A1 gate the held test flipped (held.length greater than 0 made less than 0): 5 failed (Large Deal case + 4 held rows) | 5 passed (free rows); A2 draft runAs→inherit: rep-draft case red (test timeout waiting for a contract); A3 gate runAs→inherit: outsider case red ('an outsider opened a deal'); A4 task bubble runAs→inherit: agent task case red; A4b event bubble runAs→inherit: agent call + first-response cases red; A5/A5b org pin removed (task/event): 'the bubble crossed into B' / 'the walk-up crossed into B'; A5c gate org pin removed: the cross-organization insert refused. Targeted runs before verify: 17 touched/adjacent suites, 509 passed then 3 fixed (refusal site count 22→23, payment-terms fixture owner, first-response re-pin). NOT MEASURED locally, declared to CI: test:coverage thresholds (the org-pinned branches run only on a multi-organization boot), docs-app types:check, e2e, link-check; CI in_progress at report time.",
"mcp_calls": "0",
"api_writes": "3 relay writes as objectstack-fleet[bot], each a repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create → POST /repos/objectstack-ai/hotcrm/pulls (draft, #2035; read-back 10638 bytes sent, 10638 stored, identical; REST read-back body identical, draft true); (2) label-write --assign os-zhuang → POST /repos//issues/2035/assignees (read-back MATCHES; labeler had set documentation, ci/cd, metadata, backend); (3) this os-dev-report comment → POST /repos//issues/2014/comments. Plus git push of the branch three times (empty-branch probe, 5674002, a83103b with merge 73c712a), not REST. Reads via gh api (issue, comments, PR, check-runs).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed · scripts/check-source-hygiene.mjs's DIAGNOSTIC_SINK comment saysquote.hook.tskeeps two failures.push diagnostics; after this PR it keeps none (both cascade throws name the quote by label / number). A stale comment in a gate script, not edited here.",
"carrier: 承接者:无 · noted, not filed · a tenant-less system acceptance on a two-organization install drafts no contract: the platform refuses an organization-less insert (measured before and after this PR); nothing in the app accepts quotes as the system, so no producer. In PR Acceptance notes.",
"carrier: 承接者:无 · noted, not filed · boundary of the F1 mirror: it covers the three approval flows the app ships on crm_opportunity; an install that adds another lockRecord approval there is not mirrored (narrower: acceptance admitted, close-won swallowed as before). In PR Acceptance notes.",
"carrier: 承接者:无 · noted, not filed · test/hooks-runtime-service.test.ts is at 82% of the 100KB file cap (hygiene advisory, 18KB headroom)."
]
}objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsVerdict: os-dev report
6078092344, PR #2035 (round 2) — ACCEPT, Tier S (not governed:check-governed-merges0 of 29 paths, 1,105 changed lines ≤ 5000): the seat lands it through the merge queue once every check on heada83103b5is green.repo:hotcrmseat,session_012zh91QzFgePbkmuHnugLN3, 2026-10-09T09:22Z.Checked on GitHub and in git, not taken from the report:
- PR shape: draft, base
main; first linePart of #2014(this card stays open for F9), thenClause-②: no, no closing keyword. F9's three hooks are untouched. A trial merge oforigin/mainf562142f(fix(quote_generation): refuse Generate Quote before qualification approval instead of leaving an orphan quote #2034, which edited the same Quotes and state-machines pages by section) is clean. - F1, ruled C, read line by line:
quote_workflowrefuses a user's move intoacceptedwhile the linked open deal is held, withRECORD_LOCKED/ 409 (an existing code). The message names the quote by its label and the hold by the field label the rep sees.- "Held" mirrors exactly what refuses a rep's close-won, measured state by state:
approval_statuspending (the platform lock; a recall leavesrejected, which does not hold), andstatus_change_approval_status/qualification_approval_statuspending or rejected (opportunity_lifecycle). No wider. - The deal is read as the caller, not elevated; an unreadable deal or a failed read stands the gate down.
- The contract draft moved into its own hook
quote_accepted_contract_draftwithrunAs: 'system'and the rule-9 reason beside it. It issues no read; its one insert inherits the trigger's organization (rule 10, pinned bytest/hook-org-inheritance.test.ts). Close-won stays the caller's write inquote_on_accepted, so a deal under approval is never closed by elevation.
- F11, ruled B:
opportunity_account_capabilityisrunAs: 'system'. It reads onlycommercial_capability, pinned to the new deal'sorganization_id, and the refusal says "this account" and names nothing. The raw-id-in-prose class survives in five more places after #1208 — 15 of 31 tasks in a demo org still name a record by its primary key #1243's naming case is re-pinned. - The activity bubble:
task_activity_bubbleandevent_activity_bubblearerunAs: 'system'. Every walk-up read is pinned to the triggering record's organization, and each write target is confirmed inside it by one pinned read per object (a by-id update cannot carry the extra key). Round 1's pattern. - Measured before and after through the handle on
08cfa209:- F1: acceptance admitted, close-won refused and swallowed, 0 contracts →
RECORD_LOCKED, quote stays presented, 0 writes; and on an unlocked deal, 1 contract. - F11: an outsider's deal on a Settlement Only account was stored → refused, "this account".
- Bubble: an agent's task and held call left the account empty → stamped.
- F1: acceptance admitted, close-won refused and swallowed, 0 contracts →
- Ablation, each restored to HEAD:
- gate inverted: 5 red;
- draft, gate and both bubbles de-elevated: each its case red;
- each organization pin removed: the cross-organization case red.
- Tests now accept as a sales rep. The three
quote-accepted-*suites and the acceptance cases had used a manager as a workaround for this defect. pnpm verifygreen ona83103b5: 3,589 passed, 1 skipped.src/revenueis at ~18,990 of its 19,000 ceiling, paid by compactingquote.hook.ts; no ceiling moved.
Surface beyond the claim, accepted:
accounts*.mdx(the Settlement Only refusal's docs, which the dispatch named),docs/feature-inventory.mdQUO-005/006, comment-only lines incontract.object.ts/_picklists.ts, and thecase-first-responsere-pin. The elevated event bubble now stamps first response even for a writer who cannot read the case, as the ruling elevates the whole bubble.Out-of-scope findings, carried (no card):
scripts/check-source-hygiene.mjs'sDIAGNOSTIC_SINKcomment still saysquote.hook.tskeeps twofailures.pushdiagnostics; it now keeps none. A stale comment for the next PR that touches that file.- The F1 mirror covers the three approval flows the app ships on
crm_opportunity; an install that adds another locking approval is not mirrored (PR Acceptance notes). - A tenant-less system acceptance on a two-organization install drafts no contract: the platform refuses an organization-less insert. No producer in the app (PR Acceptance notes).
test/hooks-runtime-service.test.tsis at 82% of the file cap.
After landing: this card goes
pm:blockedwithBlocked-by: objectstack-ai/objectstack#22437, only F9 left (ruling6076828219).
Generated by Claude Code
- PR shape: draft, base
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsLanded (round 2): PR #2035 MERGED as
a2f2a40aonmain(squash, single parentf562142f). The seat landed it through the merge queue after 10 of 10 checks went green on heada83103b5: ready and queued at 09:29Z, merged 09:29:56Z.repo:hotcrmseat,session_012zh91QzFgePbkmuHnugLN3, 2026-10-09T09:30Z.Two readings:
- the PR object reads
merged, merge commita2f2a40a; origin/mainisa2f2a40a fix(quotes,hooks): refuse an acceptance an approval holds; draft a rep's contract; the capability gate and activity bubble.git diff f562142f a2f2a40ais the PR's 29 files, +755 / −350, the reviewed change (ACCEPT6078126591).
What now holds:
- A rep's acceptance of a quote whose deal is held by an approval is refused at the acceptance (
RECORD_LOCKED). - A rep's acceptance on an unlocked deal drafts the contract (elevated draft); close-won stays the rep's own write.
- An outsider's deal on a Settlement Only account is refused, naming no account.
- An agent's or rep's completed task or held call stamps the account, contact and lead recency.
Not closed, by design: the PR says
Part of #2014. One item is left on this card: F9, the three web-intake hooks (lead_auto_assign,case_auto_assign,lead_duplicate_check), ruled B in6076828219: the platform fixes the anonymous form door first.Transition in this act:
pm:dispatched→pm:blocked, withBlocked-by: objectstack-ai/objectstack#22437at the top of the body; the assignee is removed. When #22437 closes, re-derive: unlock only once its fix is installable in the pin (#2031 carries the upgrade), then elevate the three hooks and pin that the anonymous answer carries none of their findings.
Generated by Claude Code
- the PR object reads
- added and removedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Oct 9, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsF9 measured in a browser on HotCRM 4.0.1 (
main1d7148bf,@objectstack/*17.7.0) by the maintainer's browser pass #2058 (findings A-08, B-02, C-20, B-21).repo:hotcrmseat,session_012zh91QzFgePbkmuHnugLN3, 2026-10-10T19:35Z.- Web-to-lead: after
pnpm demo:staff, two people holdsales_rep, yet every anonymous submission at/_console/f/contact-uslands withowner_id: null.lead_auto_assign's read ofsys_user_positionruns as the guest and comes back empty.- The trigger-fired
lead_assignmentthen fails: "Node 'notify_hot' / 'notify_std' failed: notify: at least one recipient is required …{record.owner_id}". - That flow failure is filed separately as item 6 of Browser pass #2058 — HotCRM-side defects (18 items: accepted quote still takes lines, half-converted leads, agents losing their cases, unreachable Sales Home, …) — list card, split per item at dispatch #2060: the flow should not fail on an ownerless lead, however F9 is ruled.
- The trigger-fired
- Web-to-case: all five anonymous submissions at
/_console/f/supportlanded ownerless in "Unassigned — triage" whileservice.agentheldservice_agent(case_auto_assign, same guest read).- The docs promise the routing:
content/docs/administration/setup.mdx:116andcontent/docs/service/cases.mdx:146,181. - The same setup page also says the anonymous form "cannot read the pool".
- The docs promise the routing:
- The response half of F9: an anonymous
POST /api/v1/forms/support/submitanswers with the whole stored case (organization_id,owner_id,internal_notes,sla_due_date…). That is rest(public forms):POST /forms/:slug/submitanswers the anonymous submitter with the whole stored record, so any field a hook derives from existing data (a duplicate match, an owner) reaches the internet objectstack#22437, closed 2026-10-09 after 17.7.0, so it is not installable yet.
Evidence:
qa/2026-10-10-browser-pass/{a,b,c}/REPORT.mdand the PNGs on branchesclaude/qa-browser-pass-a|b|c.F9 stays
pm:blockedon objectstack-ai/objectstack#22437 under ruling hotcrm-R74b item 4 (B). These readings are the acceptance baseline for when the pin bump (#2031) brings the fix.
Generated by Claude Code
- Web-to-lead: after
Blocked-by: #2031
Re-derived 2026-10-09T14:11Z by the
repo:hotcrmseat (session_018Mk4tab2eCyY41UTWK7y7V): the previous blocker objectstack-ai/objectstack#22437 closedcompletedat 2026-10-09T11:52:19Z, but no installable release carries it yet (npm view @objectstack/cli versionanswers 17.7.0, which is this repo's pin). So this card waits on #2031, the upgrade card, whoseRestart-when:is that release; re-derive again when #2031 closes.Ruled: 6076828219 · hotcrm-R74b items 3, 4, 5 · letters C, B, B · 2026-10-09 — second round = F1 (C) + F11 (B) + the activity-bubble pair; F9 waits on objectstack-ai/objectstack#22437
Filing gate: ① product defects with reach measured, one fix pattern. Class (a), plus F11 class (b) (a commercial gate fails open). reach: public doors (REST data writes, the flow trigger and resume doors, the anonymous form door's context), each measured once with a wrong result on
@objectstack/*17.7.0 by the dev of #1595 (PR #2013), sessionsession_012zh91QzFgePbkmuHnugLN3.Who acts on it: the
repo:hotcrmseat dispatches it after PR #2013 lands. That PR pins most of these as⚠️ … (measured defect)cases, and this card's PR flips those pins. ⛔ Not a claim.Why these surfaced now: the old hand-built harness ran every hook with a
ctx.apithat had no permission check, so a hook that writes or reads another object as the person who fired it looked fine. On the real engine that person often lacks the right,onError: 'log'swallows the refusal, and the derived write never happens.A. The caller-context family: one fix pattern, eleven hook sites
Since spec 17.5.0 a hook can declare
runAs: 'system' | 'user' | 'inherit'(defaultinherit). Seepackages/spec/src/data/hook.zod.tsat 17.7.0 and objectstack#14010.systemgives the hook'sctx.apithe elevated context, whilecreated_by/updated_byand the audit log still name the person. No hotcrm hook declares it today.The fix constraints are this repo's own: AGENTS.md rule 9 (elevate as little as possible, and say why per site) and rule 10 (every elevated scan or rollup pins an organization predicate; the #1363 guard is the acceptance). Granting the persona the right instead is the other option, where the business wants that persona to hold it. The dev measures and states which applies per site; a site whose answer is a business choice comes back
needs_decision.99d290ad)quote_on_accepted,src/revenue/objects/quote.hook.ts:192-198PATCH /api/v1/data/crm_quote/:id {status:'accepted'}→ 200; the deal goesclosed_wonbutcrm_contractgets 0 rows. Log: "could not draft the contract … You do not have permission" (sales_repcrm_contract.allowCreatefalse)article_feedback_metrics_refresh,src/service/objects/article_feedback.hook.ts:55-61test/knowledge-feedback.test.tscampaign_member_optout_sync,src/marketing/objects/campaign_member.hook.ts:122-128test/campaign-member-lifecycle.test.tscase_status_side_effects,src/service/objects/case.hook.ts:321-327last_activity_dateis never bumpedtest/hooks-runtime-service.test.tscampaign_attribution_refresh,src/sales/objects/opportunity.campaign-metrics.hook.ts:50-56test/hooks-runtime-service.test.tscampaign_lead_conversion_refresh,src/sales/objects/lead.campaign-metrics.hook.ts:54-60test/hooks-runtime-service.test.tscase_escalation_reassign,src/service/objects/_case-assignment.ts:304-309(pool read:336-338)PATCH {status:'escalated'}or theescalate_caseaction → the case stays on the agent while a service_manager is staffed; the agent'ssys_user_positionread is PERMISSION_DENIED. Only system escalations (the SLA sweep) reach the pooltest/demo-staffing.test.tslead_auto_assign(src/sales/objects/lead.hook.ts:54-59),case_auto_assign(_case-assignment.ts:174-180),lead_duplicate_check(lead.hook.ts:534-540)publicFormGrant,guest_portal, anonymous): web-to-lead and web-to-case are never round-robined (pool read denied → stand-down; a case lands unowned with an agent staffed), web leads alert nobody (owner null), and a web duplicate is never flagged. #596's acceptance never held under the real form contexttest/case-assignment.test.tsopportunity_account_capability,src/sales/objects/opportunity.hook.ts:508-514(branch:540-544)findOnereturns null and never throws; the comment at:494-498assumes a throwtest/opportunity-account-capability-gate.test.tsUnmeasured, possibly the same shape:
opportunity_promote_account(opportunity.hook.ts:374) andcampaign_member_metrics_refresh(campaign_member.hook.ts:182). The dev measures them as persons and either adds them or states why not.Split out (one card per defect, on the maintainer's word 「如果确实是缺陷,可以全部立卡」)
quote_generationleaves its quote behind on an approval-locked deal → quote_generation leaves its quote behind when the deal is approval-locked: a rep's run fails RECORD_LOCKED at update_opportunity and the created quote stays #2015annual_revenue→ A sales rep cannot convert a lead into a new account: lead_conversion writes annual_revenue, which the sales_rep profile makes non-editable, so the flow fails #2016id) → On the SQL datasource no campaign membership is ever promoted on conversion: lead.campaign-metrics projects without id, then updates by id #2018Not this card
test/hooks-runtime-sales.test.ts:679), objectstack#22306 (F10, insert-side readonly keys) and objectstack#22301 (the eight verify-handle gaps behindtest/helpers/verify-stack.ts, and the cel-dated seed replay).Acceptance
runAs: 'system'site carries its rule-9 reason beside the code and its organization predicate (rule 10), and the [finding] The scheduled-flow org-partition guard only inspectscreate_record— anupdate_recordwriting a value sourced outside the swept row is uncovered #1363 guard stays green.pnpm verifyis green.Duplicate check
gh searchis refused in this container (GraphQL and REST search answer 403). So every issue of both repos was listed (/issues?state=all: objectstack 9,565, hotcrm 754) and matched case-insensitively. Hits shown as objectstack / hotcrm:quote_on_accepted: 1 / 12article_feedback_metrics_refresh: 0 / 2campaign_member_optout_sync: 0 / 0case_status_side_effects: 1 / 8campaign_attribution_refresh: 0 / 2case_escalation_reassign: 0 / 4runAs hook ctx.api: 5 / 0quote_generation RECORD_LOCKED: 0 / 0lead_conversion annual_revenue: 0 / 1company_normalized null: 0 / 0account delete closed opportunit: 9 / 8contact delete expired contract: 2 (QA runs) / 0close or reassign: 34 / 8 (none about deletion after closing; hotcrm#1070 and A resolved unowned case sits in Unassigned — triage forever: the tab filters is_closed, which only flips onclosed#1145 are case routing)crm_contract.crm_contact required: 0 / 5 ([17.0-rc2验收] quote_on_accepted 把 boolean false 传入合同 lookup:凡缺 contact 或 opportunity 的报价被接受后,合同不起草、close-won 也不执行(静默失败) #714, crm_quote.crm_contact 要不要自 presented 起 requiredWhen——缺 contact 的报价永远起草不出合同,失败只出现在服务器日志里(源自 #714) #1017,contact_integrityis not organization-scoped — no organization after the first gets any contacts on a walled deployment #1176,crm_contractis granted a hierarchy write scope but is not tenant-scoped — fail-closed SECURITY error on every walled boot, andsales_manager's write scope is inert #1177: quote drafting and tenancy, closed)publicFormGrant round-robin: 0 / 0commercial_capability: 0 / 0projectFields id: 0 / 0None is a duplicate. Related: objectstack#14010 (closed) is where hooks gained
runAs; #596 (closed) is the web-to-case assignment whose acceptance never held; #684 (closed) applied the same fix to flows.Generated by Claude Code