Repository navigation
Track A · crm_opportunity: the 立项 (qualification) approval gate, which blocks stage advancement, bidding and won/lost until approved (REQ-0006 step 11), off by default #1995
Description
Activity
- addedenhancementNew feature or requestNew feature or requestpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchand removed
on Oct 3, 2026 objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsClaim: PM loop round R71
Session:session_01ER8ntXZhYebyQ66aXWdjfT
Account:hotlong(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-1995-qualification-approval-gate
Worktree:hotcrm-issue-1995
Domain:repo:hotcrm(single-lane repo, nodomain:*taxonomy)
Seat:repo:hotcrm#1
File surface:src/sales/objects/opportunity.{object,hook}.ts; one newsrc/sales/flows/opportunity-qualification-approval.flow.tsplus itsindex.tsandobjectstack.composition.tsregistration;src/sales/views/opportunity.view.tsonly if a filter requires it; the foursrc/sales/translations/*/objects.pipeline.ts; the qualification and automation docs pages in 3 locales;README.md/docs/STATUS.mdflow counts; the guards undertest/that pin them, plus one new runtime test; one.changeset/1995-*.md(stop on breach; explain in the report)
Container & model:L,mode:subagent,model: default(dispatch-gates --tier --repo objectstack-ai/hotcrmanswers "no path-derived mandate … the tier stays the PM's per-card judgment call"; a gate with design judgement)
Clause-②: no. This is app metadata oncrm_opportunityand adds one flow; it touches no published schema and no accept set.
Thread-read: none
Serial constraints cleared:Restart-when:holds. PR #1950 is merged (4e072fd0,"merged": true), sopm:blocked→pm:dispatchedin one write. The only open PR is dependabot #1962 (package.json, the lockfiles,apps/docs/**, one test-helper comment), which is disjoint. Track A has nothing else in flight.
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 1995, "status": "done", "branch": "claude/issue-1995-qualification-approval-gate", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1996", "session": "session_01ER8ntXZhYebyQ66aXWdjfT — R71 dev run, dispatched by the repo:hotcrm PM seat (subagent: the parent's id)", "premise_still_valid": true, "summary": "Built the REQ-0006 step-11 立项 gate on crm_opportunity, OFF by default, on the step-14 pattern without forking it. Two fields in the qualification group: qualification_approval_status (readonly verdict; its defaultValue not_required IS the switch) and qualification_requested (the rep's request, boolean, defaultValue false so the transition is visible on every driver). One flow, opportunity_qualification_approval (record_change after-update; opens one approval on a NEW request while the verdict is pending/rejected; has()-guarded, fail-closed; runAs system; admin_rescue; approvers sales_manager; approve stamps approved; reject stamps rejected and unticks the request). One hook branch in opportunity_lifecycle above the step-14 block: while the verdict reads pending/rejected (input-first), a user write that changes stage, records will_bid or raises a new requested_status is refused RECORD_LOCKED/409; every other edit stays open. Both new columns joined APPROVAL_FIELDS (the flow stamps under the triggering user; a deal closed meanwhile by an unjudged system write would otherwise refuse the stamp). One deliberate term differs from the step-14 gate: lockRecord false, taken from the lead gate on step 11's 「新增商机可跟进」 (and hypothesis 2's every-other-edit-stays-open while pending). Hypotheses 1-6 all held: 立项 comes first with both gates armed, the step-14 gate is unchanged once 立项 is approved, the 立项 write-back trips neither gate nor the freeze, either gate alone leaves the other inert; off by default proven by the suite plus a fresh boot. The three edited existing tests are exactly the hand-maintained count/rosters hypothesis 6 named. New runtime file pins the matrix (off/off, 立项 pending and rejected, 立项 approved, status only, both before and after 立项). Measured live, beyond the dispatch: a user-supplied readonly verdict is stripped BEFORE beforeUpdate hooks (so input-first cannot be fed approved), and the platform holds one pending approval per record, so an amount crossing $100K during an open 立项 request fails the amount flow's run (DUPLICATE_REQUEST) and the amount approval opens on the next write (the 立项 verdict stamp); recorded in the flow docstring and an admin tip.", "tests": "FULL VERDICT on HEAD 8aca1c64 (after `git merge origin/main` d4a3cafd/#1962, no conflict): `OS_VERIFY_LOCK_SLOT=hotcrm-issue-1995 bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c 'git rev-parse --short HEAD && pnpm verify'` printed 8aca1c64 · 'Validation passed' (Data: 18 Objects 363 Fields, Logic: 32 Flows) · lint '1 warning(s), 18 suggestion(s)' (the 18th = the new approval node's approvers-may-resolve-empty note, same class as its siblings; build author-time warnings 9, same note) · '0 i18n/missing-* issues' · 'source hygiene clean' · 'source token ratchet clean' · 'Build complete' · 'Test Files 176 passed (176)' · 'Tests 3787 passed | 1 skipped (3788)' · 'os-verify-lock: VERDICT command-exit 0'. Same command on e6c973c6 (pre-merge): VERDICT command-exit 0, 176 files. New file test/opportunity-qualification-approval-gate.test.ts: 47 tests (75 with the step-14 sibling file run beside it, all green). TOKENS src/sales (business semantics / interaction / authored total): main 4e072fd0 55,279 / 27,996 / 98,761 -> skeleton b739157d (fields+flow+hook, before docs) 56,505 / 27,996 / 99,987 -> final 8aca1c64 56,505 / 27,996 / 99,987; ceilings 59,000 / 31,000 / 107,000 (headroom 2,495 / 3,004 / 7,013); no ceiling raised. ABLATIONS (node /home/user/objectstack/scripts/ablation-replace.mjs; anchor x1->x0, blob changed; restore proven blob == HEAD and git diff HEAD empty; tests import src/ directly so no build/dist leg): (1) rejected removed from the hook refusal -> predicted 6 red, observed 6/75 red (the five held acts on a rejected deal + the matrix rejected row); (2) transition term deleted from the start condition -> predicted 2, observed 2/75 (own pending stamp; invisible prior row); (3) both columns removed from APPROVAL_FIELDS -> predicted 1, observed 1/75 ('Opportunity Big Deal is closed (closed_won); ... Attempted: qualification_approval_status.'). ENGINE PROBE (scratch test, not committed; real ObjectQL + the shipped hook): user update {stage, qualification_approval_status: approved} reached the hooks as keys [id, stage], refused RECORD_LOCKED 409, stage and verdict unchanged. BOOTS (fresh 17.6.0, port 4816, fresh DB each; served object/flow metadata == the artifact booted; armed artifacts built from LOCAL never-pushed throwaways via ablation-replace --hold, built with -o to scratch, source restored blob == HEAD before boot): GATE OFF (branch artifact): new $5,000 deal born not_required / request false; stage move 200, will_bid 200, tick 200, direct close 200; 0 approval requests (positive control: $150,000 raise opened 1 flow:opportunity_approval, 立项 verdict stayed not_required). 立项 ARMED: born pending; stage move / will_bid / direct close 409 RECORD_LOCKED, stage unchanged; amount+next_step+customer_background 200; tick -> exactly 1 flow:opportunity_qualification_approval pending, still 1 after 3 s more; edit while it waits 200 (not locked); stage move while it waits 409; approve -> approved; then stage move 200, will_bid 200, direct close 200 (closed_won). Reject (2nd deal) -> rejected, request false; stage move 409, direct close 409; re-tick -> new pending request beside the rejected one. Console (Chromium /opt/pw-browsers, Details edit mode): Stage=Qualification Save sent {stage: qualification} -> 409 and the Save bar showed 'This deal needs qualification approval first: tick Request Qualification Approval. Stage can change once it is approved.'; Will Bid toggle -> 409; ticking Request Qualification Approval -> 200 + 1 pending request; the console sends only the changed key. Re-entry: 0 're-entered for the same record' warnings across both armed boots; positive control (term deleted, own throwaway) 1 per request. Amount interplay: raise to $150,000 during an open 立项 request -> opportunity_approval run failed DUPLICATE_REQUEST (logged), approval_status stayed not_required; approving 立项 -> the amount request opened (approval_status pending). BOTH ARMED: born pending/pending; requested_status -> 409 with the 立项 sentence ('Requested Status can change once it is approved'); direct close -> 409 立项 sentence; 0 requests; tick+approve -> 立项 approved; direct close -> 409 step-14 sentence; requested_status closed_won -> 200 + 1 flow:opportunity_status_change_approval pending, stage stayed negotiation; approve -> closed_won / status approved. All servers stopped by recorded PID trees. CI on 8aca1c64 at report time: Check Changeset, Quality Checks, Typecheck and Build, Label Pull Request, link-check success; Build and Test (22.x), Playwright, Analyze Code in_progress (not waited for).", "mcp_calls": "0 — no MCP GitHub tool was called", "api_writes": "3 relay writes as objectstack-fleet[bot]: (1) POST /repos/objectstack-ai/hotcrm/pulls via fleet-write/dispatch.mjs pr_create (draft #1996, relay run 37101331713, body read back identical, 14017 bytes); (2) POST /repos/objectstack-ai/hotcrm/issues/1996/assignees via label-write.mjs --assign hotlong (relay run 37101384784, read back MATCHES; zero label writes — the five labels on #1996 are the labeler's); (3) POST /repos/objectstack-ai/hotcrm/issues/1995/comments via post-stamped.mjs (this report). Plus 5 git pushes of the branch (no force, no rebase, no amend). Reads: REST GETs on issue #1995 and its comments, claim 5965843747, PR #1950, comments 5965815876 and 5965611444, PR #1996, check-runs on 8aca1c64.", "open_questions": [ { "question": "Will Bid is held until 立项 is approved, as the card's done-when and dispatch hypothesis 2(b) say. Step 8, though, has the rep fill 是否投标 on the new-deal form BEFORE approval (it is input to 立项), and on 17.6.0 the New dialog renders no { group } tab (objectstack-ai/objectstack#21543), so through the UI Will Bid can only be set in Details edit mode after creation — with the gate armed, that waits for 立项. Keep holding it?", "options": [ "A. Keep as built: hold Will Bid on update; insert is not judged, so step 8's new-deal path stays open once the platform renders the group tab (a platform gap, waited for, not routed around).", "B. Release Will Bid: hold only stage and the won/lost call, reading 投标 as the act of bidding, which HotCRM does not model." ], "recommendation": "A, because it is what the card and the customer's literal step 11 list (投标), it is consistent with step 8 by construction (insert is open), and the only friction is the measured platform gap, which AGENTS.md §2 says to wait for. Not a blocker; recorded in the PR's Acceptance notes." } ], "out_of_scope_findings": [ "carrier: whoever arms the 立项 gate · noted, not filed, NOT MEASURED — quote_generation (screen flow, the rep's session) fast-forwards a pre-proposal deal to proposal, and quote_on_accepted closes the deal under the accepter's session; on an armed, unqualified deal this gate refuses both writes. The gate ships off; same class #1950 recorded for its own gate. In PR #1996 Acceptance notes.", "carrier: none · noted, not filed, NOT MEASURED — with both gates armed, a no-session write of requested_status before 立项 would open a status-change approval: the step-14 start condition does not read the 立项 column and was left unchanged by ruling. The order holds for user writes, which the matrix pins. In PR #1996 Acceptance notes." ], "deviations": [ "One #1950 term differs on purpose: lockRecord false (the step-14 node has true). Reason written beside the node and in the PR: step 11 「新增商机可跟进」, the lead gate's precedent, and the hook refusing the three acts whether or not a request is open. Pinned in the runtime test.", "File surface: content/docs/sales/opportunities{,.zh-Hans,.zh-Hant}.mdx is outside the claim's declared surface (one field-group table row each, adding the two fields; the table says it accounts for every field).", "Merged origin/main d4a3cafd (#1962, dependabot; package.json, lockfiles, apps/docs, one test-helper comment) before the final verify; no conflict; pnpm install re-run.", "The first both-gates throwaway flip was a usage error (I passed --count; the flag is --expect): exit 2, nothing written, its build output discarded, restore proven blob == HEAD. Redone with two separately anchored --hold flips.", "PR footer is the session-URL form the agent contract and the #1950 precedent use, not the harness reminder's 'Generated with' lines; the agent instructions take precedence." ], "files_changed_vs_main": ".changeset/1995-qualification-approval-gate.md (new), README.md, content/docs/administration/automation{,.zh-Hans,.zh-Hant}.mdx, content/docs/sales/opportunities{,.zh-Hans,.zh-Hant}.mdx, content/docs/sales/opportunity-qualification{,.zh-Hans,.zh-Hant}.mdx, docs/STATUS.md, objectstack.composition.ts, src/sales/flows/index.ts, src/sales/flows/opportunity-qualification-approval.flow.ts (new), src/sales/objects/opportunity.hook.ts, src/sales/objects/opportunity.object.ts, src/sales/translations/{en,es-ES,ja-JP,zh-CN}/objects.pipeline.ts, test/automation-docs-coverage.test.ts, test/opportunity-qualification-approval-gate.test.ts (new), test/refusal-envelope.test.ts, test/runtime-coverage.test.ts (25 files, +864/-22 vs main d4a3cafd)" }
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsACCEPT: PR #1996 at
8aca1c64. The REQ-0006 step-11 立项 gate oncrm_opportunity, off by default.repo:hotcrmseat,session_01ER8ntXZhYebyQ66aXWdjfT, 2026-10-03T06:00Z. Report: the dev'sos-dev-report5966148535.Probes taken by the seat itself:
- Ancestry and checks:
maind4a3cafdis an ancestor of8aca1c64. CI 10/10 success. Governed check: 0 of 25 paths, NOT governed; 886 lines. - Read line by line: the hook branch, the two fields and
opportunity-qualification-approval.flow.ts.- Off by default:
qualification_approval_statusisreadonly,defaultValue: 'not_required', and that is the switch.qualification_requestedis a boolean withdefaultValue: false, sohas(previous.…)holds on every driver. - What the hook holds: while the verdict reads
pendingorrejected(read input-first), the branch refuses three user acts: a stage change, awill_bidchange, and a newrequested_status. Every other edit is open. It sits above the step-14 block, so 立项 comes first. - The flow: its start condition tests the off→on TRANSITION of the request, with
has()guards, fail-closed.runAs: 'system',admin_rescue, thesales_managerbench. Approve stampsapproved; reject stampsrejectedand unticks the request. - Columns: both join
APPROVAL_FIELDS, with the reason written beside them. Neither reuses another gate's column. lockRecord: falsediffers from the step-14 node on purpose. The reason is written beside it: 「新增商机可跟进」, and the hook holds the three acts whether or not a request is open.
- Off by default:
The dev's measurements, accepted:
- Ablations: removing
rejected→ 6/75 red; deleting the transition term → 2/75; dropping theAPPROVAL_FIELDSmembership → 1/75. Each restore is proven. - Engine probe: a hand-supplied
approvedverdict is stripped before the hooks run, so the input-first read cannot be fed one. - Boots on 17.6.0:
- gate off: behaviour is as on
main; - 立项 armed: 409s on the three acts, other edits 200, one approval per request, approve releases, reject re-locks and allows a re-request, 0 re-entries;
- both armed: 立项 comes first, then the step-14 flow runs unchanged;
- console: the 409 sentence shows in the Save bar.
- gate off: behaviour is as on
- Gates:
pnpm verifyVERDICT command-exit 0at8aca1c64(176 files). Tokens:src/salesreads 56,505 / 27,996 / 99,987 against 59,000 / 31,000 / 107,000.
The dev's open question, answered by the seat: hold Will Bid (option A, as built).
- The card's Done when and the customer's literal step 11 both name 投标.
- Insert is not judged, so step 8's new-deal path stays open by construction.
- The only friction is Console 17.6.0: the tabbed Edit/New form dialog renders no tab for a
{ group }form section, andrecord:detailsedit mode offers no editor for a markdown field — so group-referenced fields and markdown fields have no editing surface objectstack#21543 (the New dialog renders no{ group }tab), a platform gap that is waited for.
Noted, not blocking (in the PR's Acceptance notes): on an armed, unqualified deal the gate also refuses
quote_generation's stage fast-forward andquote_on_accepted's close. An amount crossing $100K during an open 立项 request opens its approval on the next write, because the platform allows one pending approval per record.Landing:
- Authority: 「立一张后续卡」 (the card), and 「授权你执行pr合并」 at about 04:52Z.
- Path: ready → auto-merge → queue.
Generated by Claude Code
- Ancestry and checks:
- removedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Oct 3, 2026
Part of #1904 (Track A). This is a follow-up to #1916.
Filing gate: ③ a task the maintainer ordered directly. In the
repo:hotcrmseat's chat (session_01ER8ntXZhYebyQ66aXWdjfT) on 2026-10-03 at about 04:50Z, the seat asked whether REQ-0006's unbuilt step-11 gate needs a follow-up card. The maintainer answered, verbatim: 「立一张后续卡」.Who acts on it: the
repo:hotcrmseat dispatches one dev after PR #1950 merges. The two cards sharesrc/sales/objects/opportunity.{object,hook}.ts,src/sales/flows/**and the four locale packs, and Track A is hard-serial, per #1916's own serial constraint.Blocked-by: #1950 (shared file surface; Track A is hard-serial)
Restart-when: PR #1950 is merged — done when
GET /repos/objectstack-ai/hotcrm/pulls/1950answers"merged": trueWhat is missing, and the spec
The authoritative spec is
docs/requirements/0006-opportunity-qualification-and-status-approval.mdonmain. ⛔ It is not re-decided here.opportunity-status-change-approval.flow.ts, off by default). No acceptance line names step 11, and Track A · crm_opportunity: the qualification group, milestone dates, subcontracting, narrative, and approval on STATUS CHANGE — REQ-0006 #1916's title scopes only the status change, so this gate is unbuilt and had no card. The dev who built feat(opportunity): REQ-0006 qualification, the customer calendar, narrative and approval on status change #1950 flagged it in5965611444.Done when
crm_opportunity. It is off by default: with it off, behaviour is bit-for-bit today's. With it on:RECORD_LOCKED/ 409. The start condition tests the transition, not the current value. A rejected request can be re-requested.src/salesbusiness semantics at about 55,279 against the 59,000 ceiling raised by chore(ratchet): raise the two src/sales token ceilings to their anchor() (59,000 / 107,000) #1953. Measure the skeleton first. If it would not fit, stop and report the exact gap, which goes to a ceiling decision of its own.Duplicate check
All hotcrm issues, open and closed: 747 issues over 19 REST pages, read to the short page. Title and body were grepped for
立项|qualification approval|qualification gate|step 11|step-11: 2 hits, both closed and neither about this gate. #1911 is the Track A triage-filing card; #1185 is notify-node localization. Positive control:REQ-0006hit 4.Generated by Claude Code