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 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 #2016
Unblocked: #2031 landed as 2f3d898d (PR #2091, 2026-10-13); see the unlock-scan comment.
Ruled: 6076798874 · hotcrm-R74b item 1 · letter D · 2026-10-09
Filing gate: ① product defect with reach measured. Class (a). reach: the flow trigger and resume doors, a sales_rep converting into a NEW account.
Measured by the dev of #1595 on @objectstack/* 17.7.0 (PR #2013, merged as 9451b6de), session session_012zh91QzFgePbkmuHnugLN3. Split out of #2014 on the maintainer's word in the seat's chat (「如果确实是缺陷,可以全部立卡」): one card per defect.
Who acts on it: the repo:hotcrm seat dispatches it. Serial: it edits src/ files that #2014 and #1919 also edit, so the seat orders it on the hot-file queue. ⛔ Not a claim.
Measured
F3:lead_conversion into a NEW account by a sales_rep → FLOW_FAILED create_record(crm_account): "[Security] Field write denied: not permitted to edit [annual_revenue]". lead-conversion.flow.ts:382 writes the field and src/sales/profiles/sales-rep.profile.ts:100 makes it non-editable.
The core conversion path is broken for the persona who converts most leads. #2013 moved the conversion tests to a sales manager because of this; the rep case is not pinned.
Acceptance
A sales_rep's conversion into a new account succeeds. Either the flow stops writing the field, or the account is created through a dedicated system sub-flow (AGENTS.md rule 9). Which one is the dev's to measure and state.
A test pins the rep's conversion.
pnpm verify is green.
Duplicate check
gh search is refused in this container (GraphQL and REST search answer 403), so every issue of both repos was listed (/issues?state=all) and matched case-insensitively. Hits shown as objectstack / hotcrm: lead_conversion annual_revenue 0 / 1; Field write denied annual_revenue 4 (QA runs) / 0. None is a duplicate.
Claim: PM loop round R74
Session: session_012zh91QzFgePbkmuHnugLN3
Account: os-zhuang (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-2016-rep-conversion-annual-revenue
Worktree: hotcrm-issue-2016
Domain: repo:hotcrm (single-lane repo, no domain:* taxonomy)
Seat: repo:hotcrm#1
File surface: src/sales/flows/lead-conversion.flow.ts and/or src/sales/profiles/sales-rep.profile.ts (the dev measures which, per AGENTS.md rule 9; a new dedicated system sub-flow file under src/sales/flows/ if that is the answer, registered in its barrel); test/flow-conversion.test.ts (the rep's conversion into a new account); one .changeset/2016-*.md (stop on breach; explain in the report)
Container & model: M, mode:subagent (the os-dev definition and the scripts/pm write tools live in this container's objectstack checkout), model: default (dispatch-gates --tier --repo objectstack-ai/hotcrm answers "no path-derived mandate … the tier stays the PM's per-card judgment call"; governedPathsIn over the surface: none)
Clause-②: no
Responsibility: the app (the flow writes annual_revenue, which the app's own sales_rep profile makes non-editable) | none: the platform enforces FLS as declared | every sales rep converting a lead into a new account, on any install today
Thread-read: none
Serial constraints cleared: #1595 (PR #2013) landed at 9451b6de (it moved the conversion tests to a manager because of this). #2017 also edits lead-conversion.flow.ts and waits for this card. Sibling claims in flight: #2014 (hooks) and #2015 (quote-generation.flow.ts); disjoint files.
{
"issue": 2016,
"status": "needs_decision",
"branch": "claude/issue-2016-rep-conversion-annual-revenue",
"pr": null,
"session": "session_012zh91QzFgePbkmuHnugLN3 (subagent; parent seat's id)",
"premise_still_valid": true,
"summary": "Defect reproduced on 9451b6de exactly as filed: a sales_rep's lead_conversion into a NEW account fails at create_account with FLOW_FAILED 400 '[Security] Field write denied: not permitted to edit [annual_revenue] on crm_account', writing nothing (0 accounts, lead.is_converted false), identically whether the lead carries annual_revenue 4200000 or none (the refusal is on the KEY). Measuring the card's two options changed the question, so no fix was written. (1) The business does need the copy: lead.object.ts:231-235 documents annual_revenue as 'the value lead_conversion copies onto the account it creates', and crm_account.annual_revenue is read by the account list views (sort, sum column, the high-value filter at account.view.ts:143), the child_account_revenue roll-up, account_metrics.annual_revenue_sum (account.dataset.ts:18) and from there the executive dashboard (executive.dashboard.ts:220) and the account report; so 'stop writing it' (A) loses a documented business fact on every conversion, managers' included (prototype A measured: rep converts, account.annual_revenue null although the lead held 4200000). (2) The rep lock is published policy, not residue: content/docs/administration/profiles.mdx:87 (and its zh-Hans/zh-Hant pages) tells admins 'Sales manager (read-only for reps)'. (3) The card's other option, a runAs:'system' sub-flow (B), works (prototype measured: rep converts, account carries 4200000, owner_id = created_by = the rep, approval_status pending) but on 17.7.0 every runAs:'system' flow is reachable by ANY signed-in member through POST /automation/:name/trigger: a member holding no permission set, refused a direct crm_account insert (PERMISSION_DENIED), created an account with annual_revenue 999 by triggering the prototype sub-flow directly; the shipped precedent case_escalation_stamp has the same reach (same member refused a direct crm_case update, yet the trigger stamped is_escalated true). The spec Flow schema has no key to make a flow unreachable from that door and templates expose no parent-run token, so B ships a new elevated create door to every member, or the app must compensate for a platform gap (AGENTS.md rule 2/3). Choosing between losing the copy (A), relaxing a published policy (C), shipping a member-reachable elevation (B now), or waiting for the platform (D) is a business/security call, so this returns needs_decision with the four-axis analysis. files_changed: none (all three prototypes and the scratch repro test were reverted; git status --short and git diff HEAD --stat both empty at 9451b6de). Deviations: (i) the empty branch was pushed as the write-route probe and stays at 9451b6de with zero commits; (ii) the 'unshallow' fetch (git fetch --unshallow origin main) changed the shared .git shallow state of /home/user/hotcrm to read the FLS line's history (additive, needed for the 456283aa / 553936a7 history below); (iii) objectstack dev was booted once on port 4904 under 'timeout 150' to check a seed observation (exit 124 by design, process gone, nothing listening). History read: the FLS line dates from the Feb-2026 security baseline (553936a7) with no rationale; an earlier fix for the same defect (456283aa on unmerged branch claude/issue-1033-1030-lead-conversion) elevated the WHOLE flow, the shape rule 9 later forbade.",
"tests": "All runs: OS_VERIFY_LOCK_SLOT=hotcrm-2016 NODE_OPTIONS=--max-old-space-size=4096 bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 test/zz-repro-2016.test.ts' (a scratch file, never committed, outcomes logged to scratchpad issue-2016/repro-out.jsonl), worktree HEAD 9451b6de. [repro, unmodified src] VERDICT command-exit 0; outcome rejected code FLOW_FAILED status 400 message \"Node 'create_account' failed: create_record(crm_account) failed: [Security] Field write denied: not permitted to edit [annual_revenue] on 'crm_account'\"; accounts 0; lead.is_converted false; same for lead annual_revenue 4200000 and absent. [trigger-door reach, unmodified src] member with no permission set: hooks.run crm_case update → PERMISSION_DENIED; flows.run('case_escalation_stamp', recordId) → ok, case is_escalated true. [prototype A: annual_revenue line removed] VERDICT command-exit 0; rep conversion success, lead converted, account annual_revenue null (lead held 4200000). [prototype B: account created by a runAs system autolaunched sub-flow lead_conversion_account via a subflow node; barrel + objectstack.composition.ts appFlows registration] VERDICT command-exit 0; rep conversion success, account annual_revenue 4200000, owner_id and created_by = rep, approval_status pending; member with no permission set: hooks.run crm_account insert → PERMISSION_DENIED, flows.run('lead_conversion_account', name 'Squat Co', annualRevenue 999) → ok, output accountId, account annual_revenue 999 owned by that member. [prototype C: sales_rep crm_account.annual_revenue editable true] VERDICT command-exit 0; rep conversion success, account annual_revenue 4200000. Restore: git checkout HEAD -- on every touched path, prototype flow file and scratch test deleted; proof = git status --short empty and git diff HEAD --stat empty. pnpm verify: NOT MEASURED, reason: no change to verify (needs_decision, no commit). B-prime (stamp-only sub-flow keyed on the converted lead): NOT MEASURED, analysis only.",
"mcp_calls": "0",
"api_writes": "1 — POST issues/2016/comments (this os-dev-report, via post-stamped.mjs on the fleet relay). Not REST: one git push of the empty branch. Reads only: GET issues/2016, issues/2016/comments; GET issues/1033, 1434, 1575 answered 404.",
"open_questions": [
{
"question": "How should a sales rep's conversion into a NEW account carry the lead's annual_revenue, when crm_account.annual_revenue is published as read-only for reps (profiles.mdx:87) and, on 17.7.0, any runAs:'system' flow is triggerable by every signed-in member (measured above)?",
"options": [
"A — Stop writing annual_revenue in create_account (1 line; lead.object.ts:231-235 then states a copy that no longer happens and needs rewording, outside the claimed surface). Axis 1 business: loses the lead's revenue on EVERY conversion (managers' too) for views, roll-up, dataset, dashboard and report; the figure survives on the converted lead, so a later backfill is possible. Axis 2 long-term: a reversal to undo once B is safe. Axis 3 AI-error: simplest shape, nothing copyable; the rep pin test would catch a re-add. Axis 4 startup: smallest diff, no gate. Cost: ~1 line + comment + test + changeset.",
"B-now — The card's system sub-flow: create_account becomes a subflow node calling a dedicated runAs:'system' autolaunched flow that creates the account (measured working). Axis 1: keeps the copy and the published lock. Axis 2: the house pattern (rule 9) and the end state. Axis 3: declares a sub-flow-scoped elevation the runtime publishes to every member (measured: a member with no grants created an account with revenue 999), and becomes the second copyable instance after case_escalation_stamp; 'declared is not enforced'. Axis 4: no gate; adds one flow plus registration in objectstack.composition.ts (outside the claimed surface). Cost: new flow file, barrel, composition, parent node swap, test, changeset.",
"D — B, Blocked-by an upstream objectstack card that makes a flow unreachable from POST /automation/:name/trigger (or gates that door for runAs:'system' flows). Until it lands in the pin, a rep's new-account conversion keeps failing CLEANLY (measured: nothing written, lead untouched) and a manager converts it, which works today. Axis 1: no data loss, a workflow detour for reps. Axis 2: lands the end state once, no interim reversal. Axis 3: elevation becomes enforced as declared. Axis 4: no gate; the cost is the wait. Matches AGENTS.md rule 2 (platform defect: wait, do not route around).",
"B-prime — Narrower sub-flow: user creates the account without annual_revenue; a system sub-flow keyed on the CONVERTED lead copies its (converted-lead-locked) revenue onto its converted_account, called after mark_converted on the new-account branch only. NOT MEASURED. Reach of a direct call shrinks to re-copying a converted lead's revenue, but still overwrites a reused account's revenue; its guards exist only to compensate for the trigger-door gap (rule 2/3), and it adds a branch marker + decision to the flow #2017 edits next.",
"C — Make crm_account.annual_revenue editable for sales_rep (1 profile line + the three profiles docs pages). Measured working. Axis 1: keeps the copy but reverses a policy published to admins in three locales; reps could then edit revenue on any account they can edit. Axis 2: no elevation, simplest runtime. Axis 3: declared FLS stays honest. Axis 4: smallest change set. It is the maintainer's governance call, not a defect fix."
],
"recommendation": "D, with B-now as the fallback only if the maintainer judges the rep detour too costly to wait for the platform. Axis 1: A destroys a documented business fact on every conversion and C reverses published policy, while D and B keep both; the rep failure is clean and managers convert today, so waiting costs a detour, not data. Axis 2: B is the end state and D reaches it without an interim reversal to undo. Axis 3: B-now and B-prime ship elevation the runtime does not scope (the measured member-reachable create), which is exactly the declared-not-enforced shape the framework rejects; D waits for the platform to enforce it. Axis 4: no option adds a gate; D adds nothing to the app now beyond the pin test when B lands. Whichever option is chosen, the rep pin (flow-conversion.test.ts, sales_rep converting into a new account, manager case kept) lands with it."
}
],
"out_of_scope_findings": [
"class: a · reach: exception: security (also measured at the public door POST /automation/:name/trigger, driven in-process by @objectstack/verify flows.run) · evidence: on 17.7.0 a signed-in member holding NO permission set is refused a direct crm_case update (PERMISSION_DENIED) yet flows.run('case_escalation_stamp', recordId) stamps is_escalated true on that case; a prototype runAs:'system' autolaunched flow created a crm_account with annual_revenue 999 for a member refused a direct crm_account insert. Seam: spec:Flow (top-level keys name..runAs, errorHandling, protection; nothing declares a flow unreachable from the trigger door, and templates resolve only $User tokens, no parent-run token) → runtime:respondToFlowTrigger (runtime dist index.js:9197) → automationService.execute; handleAutomationRequest refuses only anonymous callers. Lands in objectstack (platform); hotcrm's case_escalation_stamp and any future rule-9 sub-flow inherit it. dedupe words: 'trigger runAs system', 'automation trigger door elevation', 'subflow-only flow', 'case_escalation_stamp direct trigger', 'system flow any member'",
"carrier: 承接者:无 · noted, not filed — on every @objectstack/verify boot of 9451b6de the SeedLoader refuses marketing seed crm_campaign rows #0 'Q3 Enterprise Email Nurture' and #3 'Operations Platform Launch' (status in_progress, dates written as cel daysAgo/daysFromNow, marketing.seed.ts:178-180 and 223-227) with campaign.hook 'Campaign cannot move to in_progress without both start_date and end_date', then crm_campaign_member rows #0-#2 with 'Campaign is required'; a 150 s objectstack dev boot on port 4904 logged neither, so no public-door reach is measured (likely a source-config vs dev seed divergence in the verify boot; the hook reads start_date only when typeof is string, campaign.hook.ts:74-81). dedupe words: 'verify boot campaign seed in_progress', 'SeedLoader crm_campaign refused', 'campaign_member seed Campaign is required'"
]
}
Transition: pm:dispatched → needs-user-decision · Release: claim 6061587588 (the dev returned needs_decision, report 6062013748; no commit, branch left at 9451b6de). repo:hotcrm seat, session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T14:29Z.
Ruling: batch hotcrm-R74b item 1 · letter D · maintainer 「同意」 2026-10-09T07:50Z
repo:hotcrm seat, session_012zh91QzFgePbkmuHnugLN3. The maintainer answered 「同意」 in this session's chat to the batch presented there with the recommendations 1D 2B 3C 4B 5B.
The decision: D. Wait until the platform fix is released and hotcrm is upgraded, then build B: the account is created through a dedicated runAs: 'system' sub-flow called by a subflow node (AGENTS.md rule 9). Managers convert in the meantime; a rep's conversion into a new account keeps failing loudly, and no data is lost. The fallback was C (let reps edit annual_revenue). A (stop copying the field) and building B now were not chosen.
Why B cannot be built today: on 17.7.0 any signed-in member can start a runAs: 'system' autolaunched flow directly, so the sub-flow would be a door that creates accounts past the rep's field permissions (measured: an account with annual_revenue: 999).
Platform state, re-read before this ruling was recorded:
objectstack#22310 is closed (PR #22404, landed 2026-10-09). The trigger door and flows.run now refuse a non-system caller that starts a runAs: 'system' flow of type autolaunched, record_change or schedule. A parent's subflow call still runs, which is the path B needs.
objectstack#22407 is open (PR #22424, draft): two more doors that start a flow by name, the action door and the declared flow endpoint.
Neither fix is released: npm's newest @objectstack/cli is 17.7.0, the version hotcrm pins.
State:needs-user-decision → pm:blocked in this act, with Blocked-by: #2031 in the body. #2031 is the upgrade chore card, on hold until a release after 17.7.0 publishes. When #2031 lands, this card unblocks only if BOTH #22310 and #22407 are in the installed release; if #22407 is missing, it re-blocks on the next upgrade.
{
"issue": 2016,
"status": "done",
"branch": "claude/issue-2016-rep-conversion-system-subflow",
"pr": "https://github.com/objectstack-ai/hotcrm/pull/2102",
"session": "session_01P8mZuzuyZAHSrgQKmv6ASv (subagent; the parent seat's id)",
"premise_still_valid": true,
"summary": "A sales rep's conversion into a NEW account now succeeds and carries the lead's annual_revenue; the sales_rep annual_revenue lock is unchanged and pinned. lead_conversion stays runAs:'user', and create_account (still the rep's own write) no longer carries annual_revenue. A new runAs:'system' autolaunched flow, lead_conversion_account_revenue, writes that one column onto the account id it is handed, through a new subflow node copy_account_revenue right after create_account (e7 retired, e41/e42 added; no other node moved). It is registered in the sales barrel and appFlows. ROUTE NOTE, put to the seat as open question 1: the ruling's wording is 'the account is created through' the system sub-flow. I built that shape first as a scratch prototype and measured it: the system context lifts the crm_account object gate along with the field lock, so a marketing user or a service agent (both read leads, neither may create accounts) left an orphan account they own, written past allowCreate:false, before the run failed at create_contact. Today nothing is written for them. So per AGENTS.md rule 9, the ruling's own basis, the elevated write is the revenue column only, the same shape as case_escalation_stamp. Measured cost of that shape: account_approval opens the request on the insert, before the copy lands, so the request's submission snapshot has annual_revenue null on every new-account conversion, a manager's included (on e094ec71 a manager's had 4200000). In the console's Approvals Inbox that row reads 'Account' with no amount; a control account created with its revenue in one write reads 'Account· 4,200,000'. Hypotheses: H1 held on e094ec71 (rep: FLOW_FAILED 400 'Field write denied ... [annual_revenue]', 0 accounts, lead stays qualified, with and without revenue on the lead). H2 held: the subflow call is admitted, and the trigger door refuses a direct start PERMISSION_DENIED/403 to a rep, a no-grant member, a marketing user and the admin (measured, also as raw HTTP). The action door and the declared endpoint were not run against this flow because no action or endpoint in this app names it; the installed runtime dist calls the same refusesElevatedSelfTriggeredStart in both. H3: I read both campaign flows and followed their registration path; the closer precedent is case_escalation_stamp. H4 held: owner_id = created_by = updated_by = the rep, organization_id = the rep's tenantId, approval_status pending (vitest and console). Files outside the claim surface, each required by a guard: the automation docs table (3 locales) and its coverage ledger, README.md and docs/STATUS.md flow counts (32 to 33), and test/flow-variable-conditions.test.ts (its harness registers flows by name).",
"tests": "All heavy runs went through bash /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=hotcrm-2016. [final, HEAD 4e876995] pnpm verify: 'os-verify-lock: VERDICT command-exit 0' (held 539s, shared-box seconds). Verdict lines: validate '✓ Validation passed (3058ms)', 'Logic: 33 Flows' (10 warnings, all present before this change: hierarchy-security capability and approval slates; none on a conversion flow); typecheck tsc --noEmit exit 0 (--listFiles includes test/flow-conversion.test.ts and 188 other test files); lint '18 suggestion(s) (3814ms)' exit 0, none on a conversion flow; lint:i18n-gate '✓ i18n lint gate: 0 `i18n/missing-*` issues (18 total lint issue(s) reported, unaffected by this gate)'; hygiene '✓ source hygiene clean'; hygiene:tokens '✓ src/sales authored total ~102,954 tokens (ceiling ~160,500; headroom ~57,546).' and '✓ source token ratchet clean'; build '✓ Build complete (3568ms)', 'Logic: 33 Flows'; test 'Test Files 179 passed (179)', 'Tests 3666 passed | 1 skipped (3667)'. An earlier full pnpm test on d6467748 found 4 red: README/STATUS flow counts and two flow-variable-conditions cases whose harness did not register the callee ('Flow lead_conversion_account_revenue not found'). Fixed in 4e876995; the three files re-ran 92/92 green. [new pins, test/flow-conversion.test.ts #2016 block, 7 tests] rep (revenue 4200000), rep (no revenue) and manager convert into a new account: 1 account, contact and opportunity, annual_revenue = the lead's, owner_id = created_by = the converter, organization_id = contextFor(token).tenantId. The rep's own update of annual_revenue is refused {code PERMISSION_DENIED, status 403} and the value is unchanged. A rep and the admin each start lead_conversion_account_revenue directly: {code PERMISSION_DENIED, status 403}, revenue unchanged. A marketing user's conversion into a new company: {code FLOW_FAILED, status 400, accounts 0, isConverted false}. [reverse verification, git checkout e094ec71 -- the flow, barrel and composition, trap restore from HEAD] predicted 5 red / 2 green; measured 'Tests 5 failed | 3 passed | 24 skipped (32)': both rep conversions red (VerifyRefusal 'Field write denied ... [annual_revenue]'), the lock test red, both door tests red ('RESOURCE_NOT_FOUND' 404); manager and marketing green. Restore proof: git diff HEAD empty. [ablation, callee runAs 'system' changed to 'user' via objectstack scripts/ablation-replace.mjs] 'anchor 1 to 0, blob 78bf234f1e90 to 867fea7227dd'; predicted 5 red; measured 'Tests 5 failed | 2 passed | 18 skipped (25)'. The rep's direct start was admitted (FLOW_FAILED 400) and the admin's direct start was admitted and succeeded (refusal null). Restore: 'blob == HEAD (78bf234f1e90) and git diff HEAD is empty'. [discriminator, whole create in the system flow, scratch prototype applied over HEAD, trap restore] predicted only the marketing case red; measured 'Tests 1 failed | 6 passed | 18 skipped (25)', received accounts 1. git diff HEAD empty afterwards, prototype file gone. [console, my worktree on port 4106 after pnpm demo:staff, Playwright with executablePath /opt/pw-browsers/chromium, DOM probes] As na.rep: Convert Lead, Create Opportunity ticked, Amount 40000, Submit. POST /api/v1/actions/crm_lead/convert_lead returned 200, then the resume returned 200. Toast 'Lead converted successfully!'; the text 'Field write denied' is nowhere on the page. Read back: lead converted; account annual_revenue 4200000, owner = created_by = updated_by = na.rep, approval_status pending. As sales.manager in the Approvals Inbox, that row reads 'Account' with no amount, and a control account created with its revenue in one write reads 'Account· 4,200,000'. Dev server PIDs 13466/13502/13518 killed; port 4106 free. NOT MEASURED: action door and declared endpoint against the new flow (reason: no action or endpoint in this app names it; read only: both call refusesElevatedSelfTriggeredStart in the installed runtime dist). CI checks on #2102: in_progress when this report was written.",
"gates": {
"head": "4e876995",
"pnpm validate": "✓ Validation passed (3058ms) · Logic: 33 Flows",
"pnpm typecheck": "tsc --noEmit exit 0",
"pnpm lint": "18 suggestion(s) (3814ms), exit 0",
"pnpm lint:i18n-gate": "✓ i18n lint gate: 0 `i18n/missing-*` issues (18 total lint issue(s) reported, unaffected by this gate)",
"pnpm hygiene": "✓ source hygiene clean",
"pnpm hygiene:tokens": "✓ src/sales authored total ~102,954 tokens (ceiling ~160,500; headroom ~57,546). · ✓ source token ratchet clean",
"pnpm build": "✓ Build complete (3568ms) · Logic: 33 Flows",
"pnpm test": "Test Files 179 passed (179) · Tests 3666 passed | 1 skipped (3667)",
"lock verdict": "os-verify-lock: VERDICT command-exit 0 (one pnpm verify run)",
"ci": "in_progress (not awaited)"
},
"line_budget": "src/sales authored ~102,954 tokens (ceiling ~160,500; headroom ~57,546; the dispatch quoted ~57,900, so this change uses ~354); src/sales business semantics ~58,300 (ceiling ~88,500); interaction layer ~28,311 (ceiling ~46,500). Diff vs e094ec71: 13 files, +279 / -24 lines.",
"deviations": [
"Route shape: the ruling's wording is 'the account is created through a dedicated runAs:system sub-flow'; the shipped sub-flow copies annual_revenue onto the account the converting user's own write created. Reason: the whole-create shape was measured to leave orphan accounts for marketing_user and service_agent past allowCreate:false (rule 9, the ruling's own basis). Raised as open question 1, not chosen silently.",
"File surface breach, each forced by a guard: content/docs/administration/automation.mdx, .zh-Hans.mdx and .zh-Hant.mdx plus test/automation-docs-coverage.test.ts (every shipped flow has a row and a Chinese label); README.md and docs/STATUS.md (registered flow count 32 to 33, docs-metadata-counts and docs-declared-versions); test/flow-variable-conditions.test.ts (its end-to-end harness registers flows by name, so the two lead_conversion cases now register the callee). objectstack.composition.ts was touched as the claim allowed (appFlows registration).",
"All 4 commits carry the harness-supplied Co-Authored-By trailer that names the model (Claude Opus 5.5), which I copied from the attribution reminder. The os-dev rule wants a model-free trailer pair. History is not rewritten.",
"origin/main moved from e094ec71 to c0f3485b after the branch was cut (#2099, #2100, #2101). Their files are disjoint from this diff and git merge-tree is clean. Not merged into the branch; the PR merges through the queue.",
"A scratch measurement file, test/zz-scratch-2016.test.ts, sat untracked in the worktree's test/ directory during measurement. It was never committed and was moved to the scratchpad before the gates ran."
],
"files_changed": [
".changeset/2016-rep-conversion-new-account.md (+11/-0, new)",
"src/sales/flows/lead-conversion-account-revenue.flow.ts (+107/-0, new)",
"src/sales/flows/lead-conversion.flow.ts (+34/-6)",
"src/sales/flows/index.ts (+1/-0)",
"objectstack.composition.ts (+2/-1)",
"test/flow-conversion.test.ts (+109/-9)",
"test/automation-docs-coverage.test.ts (+4/-0)",
"test/flow-variable-conditions.test.ts (+2/-2)",
"content/docs/administration/automation.mdx (+2/-1)",
"content/docs/administration/automation.zh-Hans.mdx (+2/-1)",
"content/docs/administration/automation.zh-Hant.mdx (+2/-1)",
"README.md (+2/-2)",
"docs/STATUS.md (+1/-1)"
],
"mcp_calls": "0",
"api_writes": "2: (1) one fleet-write relay stroke, POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204), executed by run 38544888912 as objectstack-fleet[bot]: pr_create, i.e. POST /repos/objectstack-ai/hotcrm/pulls (draft, #2102) with assignees [sunhaolin]; the read-back reported 8923 bytes sent and 8923 stored. (2) POST /repos/objectstack-ai/hotcrm/issues/2016/comments, this os-dev-report, via post-stamped.mjs. Zero label writes. Not REST: git push of the branch (empty probe, then 4 commits). Reads only: GET issues/2016, its comments, pulls/2102.",
"open_questions": [
{
"question": "The dedicated runAs:'system' sub-flow can take one of two shapes. Which one should ship? Ruling 6076798874 says the account is created through the sub-flow. #2102 ships the narrower shape, a sub-flow that copies annual_revenue onto the account the converting user's own write created, because the create-shape was measured to widen elevation.",
"options": [
"A (shipped in #2102): revenue copy only. Business need: reps convert with revenue, and marketing users and service agents still write nothing (measured). The cost is that the account approval request's submission snapshot has no annual_revenue on every new-account conversion, a manager's included, so its Approvals Inbox row shows no amount (measured: 'Account' vs a control 'Account· 4,200,000'); the account record itself carries the value. Long term: exactly AGENTS.md rule 9 and the case_escalation_stamp shape; the elevated node writes one column behind a do-not-add-fields guard. AI error: crm_account allowCreate:false stays enforced for every persona, and a future author copying this sub-flow copies a one-column elevation. Startup: one flow, one node, two edges, no new gate.",
"B (ruling wording): the whole account created in the system sub-flow. Business need: reps convert with revenue and the approval snapshot keeps it. But a marketing user or service agent converting into a new company leaves an orphan account owned by them, pending in the managers' approval inbox, written past allowCreate:false, before the run fails at create_contact (measured; today nothing is written). Long term: elevates the object gate along with the one field lock. AI error: a declared allowCreate:false becomes porous through Convert Lead (declared, not enforced), and the elevated node writes about 9 columns that a later author can extend. Startup: about the same size; switching #2102 to it is about 30 lines, and the marketing-user pin is the test that goes red."
],
"recommendation": "A. On the business axis, an approval row without its amount is a display loss on a record that carries the value, while B writes accounts for personas the permission sets forbid. On the long-term and AI-error axes, A is the literal rule 9 and keeps every declared object gate enforced, which is the property the maintainer's D ruling waited for (no non-entitled path to an elevated create). The startup axis is a tie. If the maintainer wants the approval snapshot complete instead, B is a small swap, and the existing tests say which persona pays for it."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed. scripts/demo-staff.ts step 4 says plugin-sharing materialises grants from a record-write hook that 'returns early on isSystem writes'. In the installed 18.0.0-next.0 plugin-sharing dist, the sharing-rule afterInsert and afterUpdate hooks carry no isSystem early return; only afterDelete does. This is a read-only inference and nothing was measured, so it is not in the PR's Acceptance notes either. Dedupe words: 'demo-staff sharing isSystem early return', 'sharing rule afterInsert system write', 'demo staff re-evaluate sharing rules'."
]
}
Review of PR #2102 (head 4e876995) against the os-dev report 6165609905 (R85, seat repo:hotcrm, session_01P8mZuzuyZAHSrgQKmv6ASv): the change is sound, but it departs from the ruling's wording, on a permission boundary. So it goes to the maintainer and is not landed.
Transition: pm:dispatched → needs-user-decision · Release: claim 6164424116 (the PR stays a draft; nothing is queued).
Checked on GitHub, not taken from the report:
Draft, base main; the body opens Fixes #2016, with Clause-②: no. 13 files, +279 −24. CI on the head: every check green except Build and Test (22.x), still running at this review.
New lead-conversion-account-revenue.flow.ts: runAs: 'system', autolaunched; one update_record on crm_account by the id it is handed, writing annual_revenue only.
lead-conversion.flow.ts: create_account stays the converting user's write and drops the annual_revenue key; a subflow node copy_account_revenue follows it (e7 retired, e41/e42 added).
Outside the claim, forced by guards: the automation docs rows (3 locales) and their coverage pin, and the flow count 32 → 33 in README.md / docs/STATUS.md.
Deviations: four commits carry a model-named Co-Authored-By: trailer (not rewritten, os-dev rule). The branch is not merged with main (c0f3485b); the files are disjoint.
Governing text: AGENTS.md rule 9, verbatim: "9. Elevate as little as possible. A screen flow stays runAs: 'user'; a write that genuinely needs elevation is a dedicated system sub-flow called through a subflow node. ⛔ Never elevate a whole flow to make readonly take effect." Ruling 6076798874 (D): "build B: the account is created through a dedicated runAs: 'system' sub-flow called by a subflow node (AGENTS.md rule 9)."
维护者速读
一句话问题: 按裁决原文让 system 子流程"把整个客户建出来",实测发现市场人员、客服这些本无权建客户的人,也能借"转化线索"建出一个孤儿客户。dev 改成只让 system 子流程补抄"年营收"这一个字段。代价是这类新客户的审批记录里看不到营收数额。两种形状都能让业务员转化成功,要你定选哪一种。
Ruling: A, the maintainer, in the repo:hotcrm seat's chat (session_01P8mZuzuyZAHSrgQKmv6ASv), answering the analysis 6165657983: 「A 只补抄营收(推荐)」.
The decision: PR #2102's shape ships. A runAs: 'system' sub-flow copies only annual_revenue onto the account the converting user's own write created; crm_account.allowCreate stays enforced for every persona. Not chosen: B, the whole account created in the system sub-flow, measured to let marketing / service users create accounts past allowCreate:false. This narrows ruling 6076798874 (D, "build B") to rule 9's literal shape. Its accepted cost: a new account's approval-inbox row shows no amount (the record carries it). No card is filed for that unless the maintainer asks.
State:needs-user-decision → pm:dispatched in this act; the re-claim follows.
Claim: PM loop round R86 (re-claim after ruling A; landing only)
Session: session_01P8mZuzuyZAHSrgQKmv6ASv
Account: sunhaolin (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-2016-rep-conversion-system-subflow
Worktree: none (the seat merges main into the PR head through the relay's pr_update_branch; no dev is dispatched)
Domain: repo:hotcrm (single-lane repo, no domain:* taxonomy)
Seat: repo:hotcrm#1
Ruling-ref: 6168552280 (A)
File surface: PR #2102 as reviewed at 4e876995 (13 files), plus the merge commit of mainf2b679af. A trial merge (git merge-tree) is clean (stop on breach; explain in the report)
Container & model: S, mode:subagent (none needed: landing only), model: default
Clause-②: no
Responsibility: the app: lead_conversion writes annual_revenue, which the app's own sales_rep profile makes non-editable | the platform's runAs: 'system' sub-flow, safe since objectstack-ai/objectstack#22310 and #22407 shipped in 18.0.0-next.0 | every sales rep converting a lead into a new account, today
Thread-read: 6168552280
Resume-from: claude/issue-2016-rep-conversion-system-subflow@4e876995
Serial constraints cleared: lead-conversion.flow.ts: nothing else in flight. Since 4e876995, main took #2101, #2103 and #2104. #2104 converted test/flow-variable-conditions.test.ts to SQL; #2102 adds 2 lines there. The trial merge is clean, and CI on the merged head decides.
Activity
objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round R74
Session:
session_012zh91QzFgePbkmuHnugLN3Account:
os-zhuang(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-2016-rep-conversion-annual-revenueWorktree:
hotcrm-issue-2016Domain:
repo:hotcrm(single-lane repo, nodomain:*taxonomy)Seat:
repo:hotcrm#1File surface:
src/sales/flows/lead-conversion.flow.tsand/orsrc/sales/profiles/sales-rep.profile.ts(the dev measures which, per AGENTS.md rule 9; a new dedicatedsystemsub-flow file undersrc/sales/flows/if that is the answer, registered in its barrel);test/flow-conversion.test.ts(the rep's conversion into a new account); one.changeset/2016-*.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 "no path-derived mandate … the tier stays the PM's per-card judgment call";governedPathsInover the surface: none)Clause-②: no
Responsibility:
the app (the flow writesannual_revenue, which the app's own sales_rep profile makes non-editable) | none: the platform enforces FLS as declared | every sales rep converting a lead into a new account, on any install todayThread-read: none
Serial constraints cleared: #1595 (PR #2013) landed at
9451b6de(it moved the conversion tests to a manager because of this). #2017 also editslead-conversion.flow.tsand waits for this card. Sibling claims in flight: #2014 (hooks) and #2015 (quote-generation.flow.ts); disjoint files.Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{ "issue": 2016, "status": "needs_decision", "branch": "claude/issue-2016-rep-conversion-annual-revenue", "pr": null, "session": "session_012zh91QzFgePbkmuHnugLN3 (subagent; parent seat's id)", "premise_still_valid": true, "summary": "Defect reproduced on 9451b6de exactly as filed: a sales_rep's lead_conversion into a NEW account fails at create_account with FLOW_FAILED 400 '[Security] Field write denied: not permitted to edit [annual_revenue] on crm_account', writing nothing (0 accounts, lead.is_converted false), identically whether the lead carries annual_revenue 4200000 or none (the refusal is on the KEY). Measuring the card's two options changed the question, so no fix was written. (1) The business does need the copy: lead.object.ts:231-235 documents annual_revenue as 'the value lead_conversion copies onto the account it creates', and crm_account.annual_revenue is read by the account list views (sort, sum column, the high-value filter at account.view.ts:143), the child_account_revenue roll-up, account_metrics.annual_revenue_sum (account.dataset.ts:18) and from there the executive dashboard (executive.dashboard.ts:220) and the account report; so 'stop writing it' (A) loses a documented business fact on every conversion, managers' included (prototype A measured: rep converts, account.annual_revenue null although the lead held 4200000). (2) The rep lock is published policy, not residue: content/docs/administration/profiles.mdx:87 (and its zh-Hans/zh-Hant pages) tells admins 'Sales manager (read-only for reps)'. (3) The card's other option, a runAs:'system' sub-flow (B), works (prototype measured: rep converts, account carries 4200000, owner_id = created_by = the rep, approval_status pending) but on 17.7.0 every runAs:'system' flow is reachable by ANY signed-in member through POST /automation/:name/trigger: a member holding no permission set, refused a direct crm_account insert (PERMISSION_DENIED), created an account with annual_revenue 999 by triggering the prototype sub-flow directly; the shipped precedent case_escalation_stamp has the same reach (same member refused a direct crm_case update, yet the trigger stamped is_escalated true). The spec Flow schema has no key to make a flow unreachable from that door and templates expose no parent-run token, so B ships a new elevated create door to every member, or the app must compensate for a platform gap (AGENTS.md rule 2/3). Choosing between losing the copy (A), relaxing a published policy (C), shipping a member-reachable elevation (B now), or waiting for the platform (D) is a business/security call, so this returns needs_decision with the four-axis analysis. files_changed: none (all three prototypes and the scratch repro test were reverted; git status --short and git diff HEAD --stat both empty at 9451b6de). Deviations: (i) the empty branch was pushed as the write-route probe and stays at 9451b6de with zero commits; (ii) the 'unshallow' fetch (git fetch --unshallow origin main) changed the shared .git shallow state of /home/user/hotcrm to read the FLS line's history (additive, needed for the 456283aa / 553936a7 history below); (iii) objectstack dev was booted once on port 4904 under 'timeout 150' to check a seed observation (exit 124 by design, process gone, nothing listening). History read: the FLS line dates from the Feb-2026 security baseline (553936a7) with no rationale; an earlier fix for the same defect (456283aa on unmerged branch claude/issue-1033-1030-lead-conversion) elevated the WHOLE flow, the shape rule 9 later forbade.", "tests": "All runs: OS_VERIFY_LOCK_SLOT=hotcrm-2016 NODE_OPTIONS=--max-old-space-size=4096 bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 test/zz-repro-2016.test.ts' (a scratch file, never committed, outcomes logged to scratchpad issue-2016/repro-out.jsonl), worktree HEAD 9451b6de. [repro, unmodified src] VERDICT command-exit 0; outcome rejected code FLOW_FAILED status 400 message \"Node 'create_account' failed: create_record(crm_account) failed: [Security] Field write denied: not permitted to edit [annual_revenue] on 'crm_account'\"; accounts 0; lead.is_converted false; same for lead annual_revenue 4200000 and absent. [trigger-door reach, unmodified src] member with no permission set: hooks.run crm_case update → PERMISSION_DENIED; flows.run('case_escalation_stamp', recordId) → ok, case is_escalated true. [prototype A: annual_revenue line removed] VERDICT command-exit 0; rep conversion success, lead converted, account annual_revenue null (lead held 4200000). [prototype B: account created by a runAs system autolaunched sub-flow lead_conversion_account via a subflow node; barrel + objectstack.composition.ts appFlows registration] VERDICT command-exit 0; rep conversion success, account annual_revenue 4200000, owner_id and created_by = rep, approval_status pending; member with no permission set: hooks.run crm_account insert → PERMISSION_DENIED, flows.run('lead_conversion_account', name 'Squat Co', annualRevenue 999) → ok, output accountId, account annual_revenue 999 owned by that member. [prototype C: sales_rep crm_account.annual_revenue editable true] VERDICT command-exit 0; rep conversion success, account annual_revenue 4200000. Restore: git checkout HEAD -- on every touched path, prototype flow file and scratch test deleted; proof = git status --short empty and git diff HEAD --stat empty. pnpm verify: NOT MEASURED, reason: no change to verify (needs_decision, no commit). B-prime (stamp-only sub-flow keyed on the converted lead): NOT MEASURED, analysis only.", "mcp_calls": "0", "api_writes": "1 — POST issues/2016/comments (this os-dev-report, via post-stamped.mjs on the fleet relay). Not REST: one git push of the empty branch. Reads only: GET issues/2016, issues/2016/comments; GET issues/1033, 1434, 1575 answered 404.", "open_questions": [ { "question": "How should a sales rep's conversion into a NEW account carry the lead's annual_revenue, when crm_account.annual_revenue is published as read-only for reps (profiles.mdx:87) and, on 17.7.0, any runAs:'system' flow is triggerable by every signed-in member (measured above)?", "options": [ "A — Stop writing annual_revenue in create_account (1 line; lead.object.ts:231-235 then states a copy that no longer happens and needs rewording, outside the claimed surface). Axis 1 business: loses the lead's revenue on EVERY conversion (managers' too) for views, roll-up, dataset, dashboard and report; the figure survives on the converted lead, so a later backfill is possible. Axis 2 long-term: a reversal to undo once B is safe. Axis 3 AI-error: simplest shape, nothing copyable; the rep pin test would catch a re-add. Axis 4 startup: smallest diff, no gate. Cost: ~1 line + comment + test + changeset.", "B-now — The card's system sub-flow: create_account becomes a subflow node calling a dedicated runAs:'system' autolaunched flow that creates the account (measured working). Axis 1: keeps the copy and the published lock. Axis 2: the house pattern (rule 9) and the end state. Axis 3: declares a sub-flow-scoped elevation the runtime publishes to every member (measured: a member with no grants created an account with revenue 999), and becomes the second copyable instance after case_escalation_stamp; 'declared is not enforced'. Axis 4: no gate; adds one flow plus registration in objectstack.composition.ts (outside the claimed surface). Cost: new flow file, barrel, composition, parent node swap, test, changeset.", "D — B, Blocked-by an upstream objectstack card that makes a flow unreachable from POST /automation/:name/trigger (or gates that door for runAs:'system' flows). Until it lands in the pin, a rep's new-account conversion keeps failing CLEANLY (measured: nothing written, lead untouched) and a manager converts it, which works today. Axis 1: no data loss, a workflow detour for reps. Axis 2: lands the end state once, no interim reversal. Axis 3: elevation becomes enforced as declared. Axis 4: no gate; the cost is the wait. Matches AGENTS.md rule 2 (platform defect: wait, do not route around).", "B-prime — Narrower sub-flow: user creates the account without annual_revenue; a system sub-flow keyed on the CONVERTED lead copies its (converted-lead-locked) revenue onto its converted_account, called after mark_converted on the new-account branch only. NOT MEASURED. Reach of a direct call shrinks to re-copying a converted lead's revenue, but still overwrites a reused account's revenue; its guards exist only to compensate for the trigger-door gap (rule 2/3), and it adds a branch marker + decision to the flow #2017 edits next.", "C — Make crm_account.annual_revenue editable for sales_rep (1 profile line + the three profiles docs pages). Measured working. Axis 1: keeps the copy but reverses a policy published to admins in three locales; reps could then edit revenue on any account they can edit. Axis 2: no elevation, simplest runtime. Axis 3: declared FLS stays honest. Axis 4: smallest change set. It is the maintainer's governance call, not a defect fix." ], "recommendation": "D, with B-now as the fallback only if the maintainer judges the rep detour too costly to wait for the platform. Axis 1: A destroys a documented business fact on every conversion and C reverses published policy, while D and B keep both; the rep failure is clean and managers convert today, so waiting costs a detour, not data. Axis 2: B is the end state and D reaches it without an interim reversal to undo. Axis 3: B-now and B-prime ship elevation the runtime does not scope (the measured member-reachable create), which is exactly the declared-not-enforced shape the framework rejects; D waits for the platform to enforce it. Axis 4: no option adds a gate; D adds nothing to the app now beyond the pin test when B lands. Whichever option is chosen, the rep pin (flow-conversion.test.ts, sales_rep converting into a new account, manager case kept) lands with it." } ], "out_of_scope_findings": [ "class: a · reach: exception: security (also measured at the public door POST /automation/:name/trigger, driven in-process by @objectstack/verify flows.run) · evidence: on 17.7.0 a signed-in member holding NO permission set is refused a direct crm_case update (PERMISSION_DENIED) yet flows.run('case_escalation_stamp', recordId) stamps is_escalated true on that case; a prototype runAs:'system' autolaunched flow created a crm_account with annual_revenue 999 for a member refused a direct crm_account insert. Seam: spec:Flow (top-level keys name..runAs, errorHandling, protection; nothing declares a flow unreachable from the trigger door, and templates resolve only $User tokens, no parent-run token) → runtime:respondToFlowTrigger (runtime dist index.js:9197) → automationService.execute; handleAutomationRequest refuses only anonymous callers. Lands in objectstack (platform); hotcrm's case_escalation_stamp and any future rule-9 sub-flow inherit it. dedupe words: 'trigger runAs system', 'automation trigger door elevation', 'subflow-only flow', 'case_escalation_stamp direct trigger', 'system flow any member'", "carrier: 承接者:无 · noted, not filed — on every @objectstack/verify boot of 9451b6de the SeedLoader refuses marketing seed crm_campaign rows #0 'Q3 Enterprise Email Nurture' and #3 'Operations Platform Launch' (status in_progress, dates written as cel daysAgo/daysFromNow, marketing.seed.ts:178-180 and 223-227) with campaign.hook 'Campaign cannot move to in_progress without both start_date and end_date', then crm_campaign_member rows #0-#2 with 'Campaign is required'; a 150 s objectstack dev boot on port 4904 logged neither, so no public-door reach is measured (likely a source-config vs dev seed divergence in the verify boot; the hook reads start_date only when typeof is string, campaign.hook.ts:74-81). dedupe words: 'verify boot campaign seed in_progress', 'SeedLoader crm_campaign refused', 'campaign_member seed Campaign is required'" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Transition:
pm:dispatched→needs-user-decision· Release: claim6061587588(the dev returnedneeds_decision, report6062013748; no commit, branch left at9451b6de).repo:hotcrmseat,session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T14:29Z.维护者速读
一句话问题: 业务员把线索转成「新客户」时直接失败。转化流程要把线索的「年营收」抄到新客户上,而「年营收」对业务员是只读的(这是对管理员公开写明的权限策略)。
实测(dev,
9451b6de):account_metrics数据集、高管看板和客户报表。POST /automation/:name/trigger直接运行任何runAs: 'system'的 flow。一个没有任何权限集的成员,借这个子 flow 建出了年营收为 999 的客户。现有的case_escalation_stamp也一样可以被任何成员直接触发。已立平台卡 objectstack#22310(security)。选项 × 真实代价
业务含义直译: A = 为了让业务员能干活,把报表里的营收数字砍掉;B = 为了修一个流程,给全员发一把万能钥匙;C = 改公司的权限制度;D = 等锁匠先把锁修好,这段时间由经理开门。
os-decision-facets
Prior rulings read:
runAs systemsubflow rule 9 → AGENTS.md rule 9 (Decision: shouldescalate_casedeclarerunAs: 'system'? It would let the three escalation flags becomereadonlyand delete a guard exemption — at the cost of raising a user-facing screen flow's privilege #1434);annual_revenuesales_rep FLS → 553936a (2026-02 security baseline, no rationale) and an unmerged earlier fix 456283a that elevated the whole flow; ADR none; thread:6062013748.推荐 D(objectstack#22310 修复并能在本仓 pin 中安装后,按 B 做:专用 system 子 flow,加上业务员转化的用例)。回退 C(如果你判断业务员本来就该能维护客户营收;这是权限制度的决定,不是缺陷修复)。⛔ 不荐 B-现在(全员可调的提权门),⛔ 不荐 A(丢掉文档写明的业务数据)。
自检:只看 ① 选 D(终态 B);②③④ 是否翻转:否(② 有经理代转,等待不丢数据)。
置信缺口:看不到 #22310 何时修;看不到真实部署里业务员转化新客户的频率。
裁后执行:
pm:blocked,Blocked-by: objectstack-ai/objectstack#22310;解锁后派发,dev 原型(B)现成可用。lead_conversion converts a lead with a blank company onto an UNRELATED key-less account: a null company_normalized becomes a "has no value" match #2017 同文件,先派它,不受影响。sales-rep.profile.ts一行、三语profiles.mdx,再加业务员转化用例。lead.object.ts的说明,再加用例。Generated by Claude Code
objectstack-fleet commentedon Oct 9, 2026
Ruling: batch hotcrm-R74b item 1 · letter D · maintainer 「同意」 2026-10-09T07:50Z
repo:hotcrmseat,session_012zh91QzFgePbkmuHnugLN3. The maintainer answered 「同意」 in this session's chat to the batch presented there with the recommendations1D 2B 3C 4B 5B.The decision: D. Wait until the platform fix is released and hotcrm is upgraded, then build B: the account is created through a dedicated
runAs: 'system'sub-flow called by asubflownode (AGENTS.md rule 9). Managers convert in the meantime; a rep's conversion into a new account keeps failing loudly, and no data is lost. The fallback was C (let reps editannual_revenue). A (stop copying the field) and building B now were not chosen.Why B cannot be built today: on 17.7.0 any signed-in member can start a
runAs: 'system'autolaunched flow directly, so the sub-flow would be a door that creates accounts past the rep's field permissions (measured: an account withannual_revenue: 999).Platform state, re-read before this ruling was recorded:
flows.runnow refuse a non-system caller that starts arunAs: 'system'flow of typeautolaunched,record_changeorschedule. A parent'ssubflowcall still runs, which is the path B needs.@objectstack/cliis 17.7.0, the version hotcrm pins.State:
needs-user-decision→pm:blockedin this act, withBlocked-by: #2031in the body. #2031 is the upgrade chore card, on hold until a release after 17.7.0 publishes. When #2031 lands, this card unblocks only if BOTH #22310 and #22407 are in the installed release; if #22407 is missing, it re-blocks on the next upgrade.Generated by Claude Code
9 remaining items
objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{ "issue": 2016, "status": "done", "branch": "claude/issue-2016-rep-conversion-system-subflow", "pr": "https://github.com/objectstack-ai/hotcrm/pull/2102", "session": "session_01P8mZuzuyZAHSrgQKmv6ASv (subagent; the parent seat's id)", "premise_still_valid": true, "summary": "A sales rep's conversion into a NEW account now succeeds and carries the lead's annual_revenue; the sales_rep annual_revenue lock is unchanged and pinned. lead_conversion stays runAs:'user', and create_account (still the rep's own write) no longer carries annual_revenue. A new runAs:'system' autolaunched flow, lead_conversion_account_revenue, writes that one column onto the account id it is handed, through a new subflow node copy_account_revenue right after create_account (e7 retired, e41/e42 added; no other node moved). It is registered in the sales barrel and appFlows. ROUTE NOTE, put to the seat as open question 1: the ruling's wording is 'the account is created through' the system sub-flow. I built that shape first as a scratch prototype and measured it: the system context lifts the crm_account object gate along with the field lock, so a marketing user or a service agent (both read leads, neither may create accounts) left an orphan account they own, written past allowCreate:false, before the run failed at create_contact. Today nothing is written for them. So per AGENTS.md rule 9, the ruling's own basis, the elevated write is the revenue column only, the same shape as case_escalation_stamp. Measured cost of that shape: account_approval opens the request on the insert, before the copy lands, so the request's submission snapshot has annual_revenue null on every new-account conversion, a manager's included (on e094ec71 a manager's had 4200000). In the console's Approvals Inbox that row reads 'Account' with no amount; a control account created with its revenue in one write reads 'Account· 4,200,000'. Hypotheses: H1 held on e094ec71 (rep: FLOW_FAILED 400 'Field write denied ... [annual_revenue]', 0 accounts, lead stays qualified, with and without revenue on the lead). H2 held: the subflow call is admitted, and the trigger door refuses a direct start PERMISSION_DENIED/403 to a rep, a no-grant member, a marketing user and the admin (measured, also as raw HTTP). The action door and the declared endpoint were not run against this flow because no action or endpoint in this app names it; the installed runtime dist calls the same refusesElevatedSelfTriggeredStart in both. H3: I read both campaign flows and followed their registration path; the closer precedent is case_escalation_stamp. H4 held: owner_id = created_by = updated_by = the rep, organization_id = the rep's tenantId, approval_status pending (vitest and console). Files outside the claim surface, each required by a guard: the automation docs table (3 locales) and its coverage ledger, README.md and docs/STATUS.md flow counts (32 to 33), and test/flow-variable-conditions.test.ts (its harness registers flows by name).", "tests": "All heavy runs went through bash /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=hotcrm-2016. [final, HEAD 4e876995] pnpm verify: 'os-verify-lock: VERDICT command-exit 0' (held 539s, shared-box seconds). Verdict lines: validate '✓ Validation passed (3058ms)', 'Logic: 33 Flows' (10 warnings, all present before this change: hierarchy-security capability and approval slates; none on a conversion flow); typecheck tsc --noEmit exit 0 (--listFiles includes test/flow-conversion.test.ts and 188 other test files); lint '18 suggestion(s) (3814ms)' exit 0, none on a conversion flow; lint:i18n-gate '✓ i18n lint gate: 0 `i18n/missing-*` issues (18 total lint issue(s) reported, unaffected by this gate)'; hygiene '✓ source hygiene clean'; hygiene:tokens '✓ src/sales authored total ~102,954 tokens (ceiling ~160,500; headroom ~57,546).' and '✓ source token ratchet clean'; build '✓ Build complete (3568ms)', 'Logic: 33 Flows'; test 'Test Files 179 passed (179)', 'Tests 3666 passed | 1 skipped (3667)'. An earlier full pnpm test on d6467748 found 4 red: README/STATUS flow counts and two flow-variable-conditions cases whose harness did not register the callee ('Flow lead_conversion_account_revenue not found'). Fixed in 4e876995; the three files re-ran 92/92 green. [new pins, test/flow-conversion.test.ts #2016 block, 7 tests] rep (revenue 4200000), rep (no revenue) and manager convert into a new account: 1 account, contact and opportunity, annual_revenue = the lead's, owner_id = created_by = the converter, organization_id = contextFor(token).tenantId. The rep's own update of annual_revenue is refused {code PERMISSION_DENIED, status 403} and the value is unchanged. A rep and the admin each start lead_conversion_account_revenue directly: {code PERMISSION_DENIED, status 403}, revenue unchanged. A marketing user's conversion into a new company: {code FLOW_FAILED, status 400, accounts 0, isConverted false}. [reverse verification, git checkout e094ec71 -- the flow, barrel and composition, trap restore from HEAD] predicted 5 red / 2 green; measured 'Tests 5 failed | 3 passed | 24 skipped (32)': both rep conversions red (VerifyRefusal 'Field write denied ... [annual_revenue]'), the lock test red, both door tests red ('RESOURCE_NOT_FOUND' 404); manager and marketing green. Restore proof: git diff HEAD empty. [ablation, callee runAs 'system' changed to 'user' via objectstack scripts/ablation-replace.mjs] 'anchor 1 to 0, blob 78bf234f1e90 to 867fea7227dd'; predicted 5 red; measured 'Tests 5 failed | 2 passed | 18 skipped (25)'. The rep's direct start was admitted (FLOW_FAILED 400) and the admin's direct start was admitted and succeeded (refusal null). Restore: 'blob == HEAD (78bf234f1e90) and git diff HEAD is empty'. [discriminator, whole create in the system flow, scratch prototype applied over HEAD, trap restore] predicted only the marketing case red; measured 'Tests 1 failed | 6 passed | 18 skipped (25)', received accounts 1. git diff HEAD empty afterwards, prototype file gone. [console, my worktree on port 4106 after pnpm demo:staff, Playwright with executablePath /opt/pw-browsers/chromium, DOM probes] As na.rep: Convert Lead, Create Opportunity ticked, Amount 40000, Submit. POST /api/v1/actions/crm_lead/convert_lead returned 200, then the resume returned 200. Toast 'Lead converted successfully!'; the text 'Field write denied' is nowhere on the page. Read back: lead converted; account annual_revenue 4200000, owner = created_by = updated_by = na.rep, approval_status pending. As sales.manager in the Approvals Inbox, that row reads 'Account' with no amount, and a control account created with its revenue in one write reads 'Account· 4,200,000'. Dev server PIDs 13466/13502/13518 killed; port 4106 free. NOT MEASURED: action door and declared endpoint against the new flow (reason: no action or endpoint in this app names it; read only: both call refusesElevatedSelfTriggeredStart in the installed runtime dist). CI checks on #2102: in_progress when this report was written.", "gates": { "head": "4e876995", "pnpm validate": "✓ Validation passed (3058ms) · Logic: 33 Flows", "pnpm typecheck": "tsc --noEmit exit 0", "pnpm lint": "18 suggestion(s) (3814ms), exit 0", "pnpm lint:i18n-gate": "✓ i18n lint gate: 0 `i18n/missing-*` issues (18 total lint issue(s) reported, unaffected by this gate)", "pnpm hygiene": "✓ source hygiene clean", "pnpm hygiene:tokens": "✓ src/sales authored total ~102,954 tokens (ceiling ~160,500; headroom ~57,546). · ✓ source token ratchet clean", "pnpm build": "✓ Build complete (3568ms) · Logic: 33 Flows", "pnpm test": "Test Files 179 passed (179) · Tests 3666 passed | 1 skipped (3667)", "lock verdict": "os-verify-lock: VERDICT command-exit 0 (one pnpm verify run)", "ci": "in_progress (not awaited)" }, "line_budget": "src/sales authored ~102,954 tokens (ceiling ~160,500; headroom ~57,546; the dispatch quoted ~57,900, so this change uses ~354); src/sales business semantics ~58,300 (ceiling ~88,500); interaction layer ~28,311 (ceiling ~46,500). Diff vs e094ec71: 13 files, +279 / -24 lines.", "deviations": [ "Route shape: the ruling's wording is 'the account is created through a dedicated runAs:system sub-flow'; the shipped sub-flow copies annual_revenue onto the account the converting user's own write created. Reason: the whole-create shape was measured to leave orphan accounts for marketing_user and service_agent past allowCreate:false (rule 9, the ruling's own basis). Raised as open question 1, not chosen silently.", "File surface breach, each forced by a guard: content/docs/administration/automation.mdx, .zh-Hans.mdx and .zh-Hant.mdx plus test/automation-docs-coverage.test.ts (every shipped flow has a row and a Chinese label); README.md and docs/STATUS.md (registered flow count 32 to 33, docs-metadata-counts and docs-declared-versions); test/flow-variable-conditions.test.ts (its end-to-end harness registers flows by name, so the two lead_conversion cases now register the callee). objectstack.composition.ts was touched as the claim allowed (appFlows registration).", "All 4 commits carry the harness-supplied Co-Authored-By trailer that names the model (Claude Opus 5.5), which I copied from the attribution reminder. The os-dev rule wants a model-free trailer pair. History is not rewritten.", "origin/main moved from e094ec71 to c0f3485b after the branch was cut (#2099, #2100, #2101). Their files are disjoint from this diff and git merge-tree is clean. Not merged into the branch; the PR merges through the queue.", "A scratch measurement file, test/zz-scratch-2016.test.ts, sat untracked in the worktree's test/ directory during measurement. It was never committed and was moved to the scratchpad before the gates ran." ], "files_changed": [ ".changeset/2016-rep-conversion-new-account.md (+11/-0, new)", "src/sales/flows/lead-conversion-account-revenue.flow.ts (+107/-0, new)", "src/sales/flows/lead-conversion.flow.ts (+34/-6)", "src/sales/flows/index.ts (+1/-0)", "objectstack.composition.ts (+2/-1)", "test/flow-conversion.test.ts (+109/-9)", "test/automation-docs-coverage.test.ts (+4/-0)", "test/flow-variable-conditions.test.ts (+2/-2)", "content/docs/administration/automation.mdx (+2/-1)", "content/docs/administration/automation.zh-Hans.mdx (+2/-1)", "content/docs/administration/automation.zh-Hant.mdx (+2/-1)", "README.md (+2/-2)", "docs/STATUS.md (+1/-1)" ], "mcp_calls": "0", "api_writes": "2: (1) one fleet-write relay stroke, POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204), executed by run 38544888912 as objectstack-fleet[bot]: pr_create, i.e. POST /repos/objectstack-ai/hotcrm/pulls (draft, #2102) with assignees [sunhaolin]; the read-back reported 8923 bytes sent and 8923 stored. (2) POST /repos/objectstack-ai/hotcrm/issues/2016/comments, this os-dev-report, via post-stamped.mjs. Zero label writes. Not REST: git push of the branch (empty probe, then 4 commits). Reads only: GET issues/2016, its comments, pulls/2102.", "open_questions": [ { "question": "The dedicated runAs:'system' sub-flow can take one of two shapes. Which one should ship? Ruling 6076798874 says the account is created through the sub-flow. #2102 ships the narrower shape, a sub-flow that copies annual_revenue onto the account the converting user's own write created, because the create-shape was measured to widen elevation.", "options": [ "A (shipped in #2102): revenue copy only. Business need: reps convert with revenue, and marketing users and service agents still write nothing (measured). The cost is that the account approval request's submission snapshot has no annual_revenue on every new-account conversion, a manager's included, so its Approvals Inbox row shows no amount (measured: 'Account' vs a control 'Account· 4,200,000'); the account record itself carries the value. Long term: exactly AGENTS.md rule 9 and the case_escalation_stamp shape; the elevated node writes one column behind a do-not-add-fields guard. AI error: crm_account allowCreate:false stays enforced for every persona, and a future author copying this sub-flow copies a one-column elevation. Startup: one flow, one node, two edges, no new gate.", "B (ruling wording): the whole account created in the system sub-flow. Business need: reps convert with revenue and the approval snapshot keeps it. But a marketing user or service agent converting into a new company leaves an orphan account owned by them, pending in the managers' approval inbox, written past allowCreate:false, before the run fails at create_contact (measured; today nothing is written). Long term: elevates the object gate along with the one field lock. AI error: a declared allowCreate:false becomes porous through Convert Lead (declared, not enforced), and the elevated node writes about 9 columns that a later author can extend. Startup: about the same size; switching #2102 to it is about 30 lines, and the marketing-user pin is the test that goes red." ], "recommendation": "A. On the business axis, an approval row without its amount is a display loss on a record that carries the value, while B writes accounts for personas the permission sets forbid. On the long-term and AI-error axes, A is the literal rule 9 and keeps every declared object gate enforced, which is the property the maintainer's D ruling waited for (no non-entitled path to an elevated create). The startup axis is a tie. If the maintainer wants the approval snapshot complete instead, B is a small swap, and the existing tests say which persona pays for it." } ], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed. scripts/demo-staff.ts step 4 says plugin-sharing materialises grants from a record-write hook that 'returns early on isSystem writes'. In the installed 18.0.0-next.0 plugin-sharing dist, the sharing-rule afterInsert and afterUpdate hooks carry no isSystem early return; only afterDelete does. This is a read-only inference and nothing was measured, so it is not in the PR's Acceptance notes either. Dedupe words: 'demo-staff sharing isSystem early return', 'sharing rule afterInsert system write', 'demo staff re-evaluate sharing rules'." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Review of PR #2102 (head
4e876995) against the os-dev report6165609905(R85, seatrepo:hotcrm,session_01P8mZuzuyZAHSrgQKmv6ASv): the change is sound, but it departs from the ruling's wording, on a permission boundary. So it goes to the maintainer and is not landed.Transition:
pm:dispatched→needs-user-decision· Release: claim6164424116(the PR stays a draft; nothing is queued).Checked on GitHub, not taken from the report:
main; the body opensFixes #2016, withClause-②: no. 13 files, +279 −24. CI on the head: every check green exceptBuild and Test (22.x), still running at this review.lead-conversion-account-revenue.flow.ts:runAs: 'system',autolaunched; oneupdate_recordoncrm_accountby the id it is handed, writingannual_revenueonly.lead-conversion.flow.ts:create_accountstays the converting user's write and drops theannual_revenuekey; asubflownodecopy_account_revenuefollows it (e7retired,e41/e42added).README.md/docs/STATUS.md.Co-Authored-By:trailer (not rewritten, os-dev rule). The branch is not merged withmain(c0f3485b); the files are disjoint.Governing text: AGENTS.md rule 9, verbatim: "9. Elevate as little as possible. A screen flow stays
runAs: 'user'; a write that genuinely needs elevation is a dedicatedsystemsub-flow called through asubflownode. ⛔ Never elevate a whole flow to makereadonlytake effect." Ruling6076798874(D): "build B: the account is created through a dedicatedrunAs: 'system'sub-flow called by asubflownode (AGENTS.md rule 9)."维护者速读
一句话问题: 按裁决原文让 system 子流程"把整个客户建出来",实测发现市场人员、客服这些本无权建客户的人,也能借"转化线索"建出一个孤儿客户。dev 改成只让 system 子流程补抄"年营收"这一个字段。代价是这类新客户的审批记录里看不到营收数额。两种形状都能让业务员转化成功,要你定选哪一种。
实测(dev,
e094ec71上,18.0.0-next.0):选项 × 真实代价 × 工作量
业务含义直译: A = 只把"抄营收"这一件需要特权的事交给系统,审批单上少显示一个数;B = 把"建客户"整件事交给系统,审批单完整,但给本不该建客户的人开了一扇门。
os-decision-facets
case_escalation_stamp同形;B 连对象级的建客户权限一起提权,扩大了特例。主流平台(Salesforce 的 system-context 子流程)也是把特权写入收得越窄越好。main2f3d898d (18.0.0-next.0) against df5cbb99 (17.7.0), plus one in-place 17.7.0 → 18 upgrade of a database, and report every defect the upgrade caused #2093 finding 05),两个选项都修好它;A 的代价是审批箱这一行没有金额,经理点进记录能看到;B 的代价落在市场/客服误转化时留下的孤儿客户上。crm_account的allowCreate:false对每个角色都继续生效(声明即强制);B 让它经"转化线索"静默失效,出错时没人看到拒绝,只多出一条不该有的客户,是静默泄露。Prior rulings read:
annual_revenue/runAs systemsub-flow → ruling6076798874(D, then B) and AGENTS.md rule 9 (Decision: shouldescalate_casedeclarerunAs: 'system'? It would let the three escalation flags becomereadonlyand delete a guard exemption — at the cost of raising a user-facing screen flow's privilege #1434); ADR none; thread:6062013748,6145908019,6147445336.推荐 A(#2102 原样落地)。回退 B:如果你认为审批单上的金额比"非授权角色经转化建客户"更要紧,B 是对 #2102 的小改。置信缺口:审批箱缺金额对经理审批实际影响多大,没有使用数据。
自检:只看 ① 选 A;②③④ 是否翻转:否。
裁后执行:
pm:dispatched,fix(flows): a sales rep converts a lead into a new account; a system sub-flow copies the revenue #2102 在 head 全绿时落地(合并即关卡)。审批快照缺营收这一点,先记在卡上,不另立卡,除非你要求。Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Ruling: A, the maintainer, in the
repo:hotcrmseat's chat (session_01P8mZuzuyZAHSrgQKmv6ASv), answering the analysis6165657983: 「A 只补抄营收(推荐)」.The decision: PR #2102's shape ships. A
runAs: 'system'sub-flow copies onlyannual_revenueonto the account the converting user's own write created;crm_account.allowCreatestays enforced for every persona. Not chosen: B, the whole account created in the system sub-flow, measured to let marketing / service users create accounts pastallowCreate:false. This narrows ruling6076798874(D, "build B") to rule 9's literal shape. Its accepted cost: a new account's approval-inbox row shows no amount (the record carries it). No card is filed for that unless the maintainer asks.State:
needs-user-decision→pm:dispatchedin this act; the re-claim follows.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round R86 (re-claim after ruling A; landing only)
Session:
session_01P8mZuzuyZAHSrgQKmv6ASvAccount:
sunhaolin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-2016-rep-conversion-system-subflowWorktree: none (the seat merges
maininto the PR head through the relay'spr_update_branch; no dev is dispatched)Domain:
repo:hotcrm(single-lane repo, nodomain:*taxonomy)Seat:
repo:hotcrm#1Ruling-ref: 6168552280 (A)
File surface: PR #2102 as reviewed at
4e876995(13 files), plus the merge commit ofmainf2b679af. A trial merge (git merge-tree) is clean (stop on breach; explain in the report)Container & model:
S,mode:subagent(none needed: landing only),model: defaultClause-②: no
Responsibility:
the app: lead_conversion writes annual_revenue, which the app's own sales_rep profile makes non-editable | the platform's runAs: 'system' sub-flow, safe since objectstack-ai/objectstack#22310 and #22407 shipped in 18.0.0-next.0 | every sales rep converting a lead into a new account, todayThread-read: 6168552280
Resume-from:
claude/issue-2016-rep-conversion-system-subflow@4e876995Serial constraints cleared:
lead-conversion.flow.ts: nothing else in flight. Since4e876995,maintook #2101, #2103 and #2104. #2104 convertedtest/flow-variable-conditions.test.tsto SQL; #2102 adds 2 lines there. The trial merge is clean, and CI on the merged head decides.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
ACCEPT → PR #2102 comment 6168832524
Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Landed: PR #2102 MERGED through the merge queue. Seat record: PR #2102 comment
6168832524.merged: true, merge commit850f75c2, 2026-10-14T12:56:55Zorigin/main850f75c2 fix(flows): a sales rep converts a lead into a new account; a system sub-flow copies the revenue (#2102)State: closed
completedby the merge;pm:dispatchedremoved in this act. Lane re-read: 22 open, as expected (#2016 gone, #2105 new).Generated by Claude Code