Skip to content

Hooks that write or read across objects as the triggering person fail silently for reps, agents and web forms — 11 hook sites, one fix pattern (runAs), measured on the real engine by #2013 #2014

Description

@objectstack-fleet

Blocked-by: #2031
Re-derived 2026-10-09T14:11Z by the repo:hotcrm seat (session_018Mk4tab2eCyY41UTWK7y7V): the previous blocker objectstack-ai/objectstack#22437 closed completed at 2026-10-09T11:52:19Z, but no installable release carries it yet (npm view @objectstack/cli version answers 17.7.0, which is this repo's pin). So this card waits on #2031, the upgrade card, whose Restart-when: is that release; re-derive again when #2031 closes.
Ruled: 6076828219 · hotcrm-R74b items 3, 4, 5 · letters C, B, B · 2026-10-09 — second round = F1 (C) + F11 (B) + the activity-bubble pair; F9 waits on objectstack-ai/objectstack#22437

Filing gate: ① product defects with reach measured, one fix pattern. Class (a), plus F11 class (b) (a commercial gate fails open). reach: public doors (REST data writes, the flow trigger and resume doors, the anonymous form door's context), each measured once with a wrong result on @objectstack/* 17.7.0 by the dev of #1595 (PR #2013), session session_012zh91QzFgePbkmuHnugLN3.

Who acts on it: the repo:hotcrm seat dispatches it after PR #2013 lands. That PR pins most of these as ⚠️ … (measured defect) cases, and this card's PR flips those pins. ⛔ Not a claim.

Why these surfaced now: the old hand-built harness ran every hook with a ctx.api that had no permission check, so a hook that writes or reads another object as the person who fired it looked fine. On the real engine that person often lacks the right, onError: 'log' swallows the refusal, and the derived write never happens.

A. The caller-context family: one fix pattern, eleven hook sites

Since spec 17.5.0 a hook can declare runAs: 'system' | 'user' | 'inherit' (default inherit). See packages/spec/src/data/hook.zod.ts at 17.7.0 and objectstack#14010. system gives the hook's ctx.api the elevated context, while created_by / updated_by and the audit log still name the person. No hotcrm hook declares it today.

The fix constraints are this repo's own: AGENTS.md rule 9 (elevate as little as possible, and say why per site) and rule 10 (every elevated scan or rollup pins an organization predicate; the #1363 guard is the acceptance). Granting the persona the right instead is the other option, where the business wants that persona to hold it. The dev measures and states which applies per site; a site whose answer is a business choice comes back needs_decision.

id hook (file:line on 99d290ad) measured at the public door pin in #2013
F1 quote_on_accepted, src/revenue/objects/quote.hook.ts:192-198 sales_rep PATCH /api/v1/data/crm_quote/:id {status:'accepted'} → 200; the deal goes closed_won but crm_contract gets 0 rows. Log: "could not draft the contract … You do not have permission" (sales_rep crm_contract.allowCreate false) acceptance now runs as a manager; add the rep case
F12 article_feedback_metrics_refresh, src/service/objects/article_feedback.hook.ts:55-61 admin deletes a vote → the article still counts it (readonly counters are dropped for a non-system caller) test/knowledge-feedback.test.ts
F13 campaign_member_optout_sync, src/marketing/objects/campaign_member.hook.ts:122-128 marketing_user unsubscribes a CONTACT member → the contact is never opted out ("update on crm_contact requires edit access to its master record") test/campaign-member-lifecycle.test.ts
F15a case_status_side_effects, src/service/objects/case.hook.ts:321-327 a service agent resolves a case → the account's last_activity_date is never bumped test/hooks-runtime-service.test.ts
F15b campaign_attribution_refresh, src/sales/objects/opportunity.campaign-metrics.hook.ts:50-56 a rep (and a manager, on a marketing-owned campaign) wins an attributed deal → campaign revenue stays 0 test/hooks-runtime-service.test.ts
F15c campaign_lead_conversion_refresh, src/sales/objects/lead.campaign-metrics.hook.ts:54-60 a rep's conversion → the lead's memberships are never promoted test/hooks-runtime-service.test.ts
F14 case_escalation_reassign, src/service/objects/_case-assignment.ts:304-309 (pool read :336-338) an agent PATCH {status:'escalated'} or the escalate_case action → the case stays on the agent while a service_manager is staffed; the agent's sys_user_position read is PERMISSION_DENIED. Only system escalations (the SLA sweep) reach the pool test/demo-staffing.test.ts
F9 lead_auto_assign (src/sales/objects/lead.hook.ts:54-59), case_auto_assign (_case-assignment.ts:174-180), lead_duplicate_check (lead.hook.ts:534-540) under the form door's context (publicFormGrant, guest_portal, anonymous): web-to-lead and web-to-case are never round-robined (pool read denied → stand-down; a case lands unowned with an agent staffed), web leads alert nobody (owner null), and a web duplicate is never flagged. #596's acceptance never held under the real form context test/case-assignment.test.ts
F11 opportunity_account_capability, src/sales/objects/opportunity.hook.ts:508-514 (branch :540-544) a rep who cannot read a Settlement Only account inserts a deal on its id → STORED. Under the caller's RLS findOne returns null and never throws; the comment at :494-498 assumes a throw test/opportunity-account-capability-gate.test.ts

Unmeasured, possibly the same shape: opportunity_promote_account (opportunity.hook.ts:374) and campaign_member_metrics_refresh (campaign_member.hook.ts:182). The dev measures them as persons and either adds them or states why not.

Split out (one card per defect, on the maintainer's word 「如果确实是缺陷,可以全部立卡」)

Not this card

  • Platform defects, filed upstream; per AGENTS.md §2 the app WAITs for them with no workaround: objectstack#22300 (F6, the projection that sends owner/org/audit columns to the billing endpoint), objectstack#22305 (F8, the cascade order, which also refuses an account whose contracts are all draft/expired/terminated, pinned at test/hooks-runtime-sales.test.ts:679), objectstack#22306 (F10, insert-side readonly keys) and objectstack#22301 (the eight verify-handle gaps behind test/helpers/verify-stack.ts, and the cel-dated seed replay).
  • Stale comments citing deleted suites belong to [finding] 26 source comments in src/ cite src/flows/ — a directory ADR-0130 removed (blocked on Track A: file surface collides) #1919.

Acceptance

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: objectstack 9,565, hotcrm 754) and matched case-insensitively. Hits shown as objectstack / hotcrm:

None is a duplicate. Related: objectstack#14010 (closed) is where hooks gained runAs; #596 (closed) is the web-to-case assignment whose acceptance never held; #684 (closed) applied the same fix to flows.


Generated by Claude Code

Activity

  1. changed the title [-]Hooks and flows that write or read across objects as the triggering person fail silently for reps, agents and web forms (11 hooks) + 6 more app defects — measured on the real engine by #2013[/-] [+]Hooks that write or read across objects as the triggering person fail silently for reps, agents and web forms — 11 hook sites, one fix pattern (runAs), measured on the real engine by #2013[/+] on Oct 8, 2026
  2. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 8, 2026
  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    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-2014-hooks-run-as
    Worktree: hotcrm-issue-2014
    Domain: repo:hotcrm (single-lane repo, no domain:* taxonomy)
    Seat: repo:hotcrm#1
    File surface: the eleven hook sites named in the card body: src/revenue/objects/quote.hook.ts, src/service/objects/{article_feedback.hook.ts,case.hook.ts,_case-assignment.ts}, src/marketing/objects/campaign_member.hook.ts, src/sales/objects/{opportunity.campaign-metrics.hook.ts,lead.campaign-metrics.hook.ts,lead.hook.ts,opportunity.hook.ts} (the hook declarations and their bodies). A persona's profile under src/*/profiles/ only where a site's measured answer is a grant rather than elevation. The tests that pin these facts (test/{knowledge-feedback,campaign-member-lifecycle,hooks-runtime-service,demo-staffing,case-assignment,opportunity-account-capability-gate}.test.ts, the F1 rep case beside the quote-acceptance tests), and the #1363 organization-predicate guard's roster where an elevated site joins it. One .changeset/2014-*.md. ⛔ No change to the five split-out cards' sites (#2015–#2019) (stop on breach; explain in the report)
    Container & model: L, mode:subagent (the os-dev definition and 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's hooks (they read or write across objects through ctx.apias the caller, andonError: 'log'swallows the refusal) | the platform's hookrunAs (spec 17.5.0 and later; objectstack#14010) already covers it and is unused here | every sales rep, service agent, marketing user and web-form submitter reaches it through REST or UI writes, on any install today
    Thread-read: none
    Serial constraints cleared: #1595 (PR #2013) landed at 9451b6de; this card flips its pins. #2018 also edits lead.campaign-metrics.hook.ts and waits for this card. #1919 (comments in the same hook files) waits for this card. Sibling claims in flight: #2015 (quote-generation.flow.ts) and #2016 (lead-conversion.flow.ts, sales-rep.profile.ts); disjoint files.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat note for the dev in flight (repo:hotcrm seat, session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T14:37Z). It is a measured case on this card's F1 hook, quote_on_accepted, reported by the #2015 dev (report 6062202342). ⛔ Not a scope change.

    • Measured (sales_rep, a deal pending opportunity_approval, so the record is locked): the quote goes presented → accepted, both writes admitted. quote_on_accepted (async, onError: 'log') then has its close-won write refused RECORD_LOCKED, and the refusal is logged and suppressed. The quote reads Accepted, the deal stays at qualification / pending, and an accepted quote is frozen, so close-won never re-runs once the approval lands. The same run also drafts no contract: the F1 permission refusal, independent of the lock.
    • What this card's fix must not do silently: if runAs: 'system' on this hook also lifts the approval lock, an accepted quote would close-won a deal its manager has not approved. Measure that case. If elevation crosses the lock, the business question goes back as needs_decision: may an accepted quote close a deal still under approval? Do not choose either answer.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 2014,
    "status": "needs_decision",
    "branch": "claude/issue-2014-hooks-run-as",
    "pr": "#2022",
    "session": "session_012zh91QzFgePbkmuHnugLN3 (subagent; the parent's harness Claude-Session line)",
    "premise_still_valid": true,
    "summary": "Mechanism assumption 1 HOLDS, measured on 17.7.0: runAs: 'system' on F12 elevates ctx.api on the in-process handler AND on the shipped lowered body (dist/objectstack.json booted with bootStack; control leg without runAs reproduces the defect), and updated_by keeps naming the person. Implemented (draft PR #2022, Part of #2014): runAs: 'system' with the rule-9 reason and rule-10 note beside each: F12 article_feedback_metrics_refresh, F13 campaign_member_optout_sync, F15b campaign_attribution_refresh, F15c campaign_lead_conversion_refresh, F14 case_escalation_reassign (pool read + load counts pinned to the case's organization_id; the tenant-less SLA sweep otherwise reads every organization's pool), F15a as a NEW hook case_resolution_account_activity split out of case_status_side_effects (runAs is per hook; the escalation task keeps the agent's narrow crm_task.allowTransfer grant), and the two extra members, both measured as defects and added: campaign_member_metrics_refresh (a marketer's recount drops Private won deals: 2 won / 1,100 became 1 / 100) and opportunity_promote_account (a rep's win on an account another user owns leaves it a prospect). No profile changed. Every #2013 pin for these sites flipped to the business fact; new cases: marketer Private-deal recount, rep promotes a non-owned account, two-organization escalation pin. Returned as needs_decision (open_questions): F1 quote_on_accepted (measured per the seat note: elevation closes a deal whose opportunity_approval is still pending), F9 x3 (elevation makes the anonymous form door hand back hook-derived CRM facts: an email-existence oracle and a staff user id), F11 (elevated refusal names and classifies an account the caller cannot read). Deviations: (1) PR first line is Part of #2014, not the closing-keyword line the dispatch named: three sites are in the decision box and os-dev forbids closing a card there; conflict noted. (2) F1 was first implemented as a split and then reverted to main on the seat's note; nothing of it is in the PR. (3) File surface beyond the claim: test/hooks-runtime-sales.test.ts (one new case in the opportunity_promote_account describe, nowhere near the #2019 deletion pins), docs/feature-inventory.md SVC-009 (names the split-out hook). (4) The harness attribution reminder names a model (Co-Authored-By: Claude Opus 5.5); commits carry the model-free pair per dispatch, not imitated. files_changed: src/service/objects/{article_feedback.hook.ts,case.hook.ts,_case-assignment.ts}, src/marketing/objects/campaign_member.hook.ts, src/sales/objects/{opportunity.campaign-metrics.hook.ts,lead.campaign-metrics.hook.ts,opportunity.hook.ts}, test/{knowledge-feedback,campaign-member-lifecycle,hooks-runtime-service,hooks-runtime-sales,demo-staffing,case-assignment}.test.ts, docs/feature-inventory.md, .changeset/2014-derived-writes-as-system.md. The #1363 guard walks scheduled flows only and does not reach hooks; it stays green and unchanged, and no new guard was added (new gates default no). Worktree removed after the report.",
    "tests": "GATE pnpm verify @ ee2a7e2: OS_VERIFY_LOCK_SLOT=hotcrm-2014 bash scripts/pm/os-verify-lock.sh -c 'pnpm verify' -> os-verify-lock: VERDICT command-exit 0 · held the lock 534s; validate ✓ Validation passed; typecheck clean; lint --strict 18 suggestions no error; lint:i18n-gate ✓ ... 0 i18n/missing-* issues; hygiene ✓ source hygiene clean; hygiene:tokens ✓ source token ratchet clean; build ✓ Build complete; test Test Files 172 passed (172) Tests 3565 passed | 1 skipped (3566). Earlier verify on 33380ba: VERDICT command-exit 1, one failure (runtime-coverage: case_resolution_account_activity named only in a comment), fixed in ee2a7e2. MEASURE M1 F12 in-process: inherit helpful_after=1 / runAs system helpful_after=0, updated_by=admin both. M2 shipped artifact: runAs system (registered body + runAs) helpful_after=0; control build without runAs helpful_after=1. MEASURE M8 F1 (rep, deal pending opportunity_approval, rep edit refused RECORD_LOCKED): shipped -> oppStage proposal, approval pending, contracts 0; runAs system on quote_on_accepted -> oppStage closed_won, approval_status pending, request pending, contracts 1; split (draft elevated only) -> oppStage proposal, contracts 1. File restored to HEAD after each leg (blob f9f82930 == HEAD). MEASURE M6 F9 elevated: anonymous insert result carries duplicate_status suspected, duplicate_of_type crm_contact, duplicate_of_contact id, owner_id staff id (46 keys); restored. M3: rep cannot read another rep's lead/contact; rep's own dedupe flags nothing (elevated: flags both). M4/M5/M9 as in summary/findings. ABLATION (scripts/ablation-replace.mjs, anchor hit, restore blob == HEAD): F14 organization pin removed -> stays inside the escalating case's organization red, other org's manager picked; runAs removed from campaign_member_metrics_refresh -> Private-deal case red with num_won 1 (exp 2), actual_revenue 100 (exp 1,100). NOT MEASURED locally, declared to CI: test:coverage thresholds, docs-app types:check, e2e.",
    "mcp_calls": "0",
    "api_writes": "3 relay writes as objectstack-fleet[bot], each one repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create -> POST /repos/objectstack-ai/hotcrm/pulls (draft, #2022, 11,496 bytes read back identical); (2) label-write --assign os-zhuang -> POST /repos//issues/2022/assignees (read back MATCHES); (3) this os-dev-report comment -> POST /repos//issues/2014/comments. Plus git push of the branch (not REST). Reads via gh api (issue, comments, PR).",
    "open_questions": [
    {
    "question": "F1 quote_on_accepted: may an accepted quote close a deal whose opportunity approval is still pending, and how should a sales rep's acceptance get its contract drafted? Measured (sales_rep, deal pending opportunity_approval): as shipped, acceptance admitted, close-won refused RECORD_LOCKED and swallowed, 0 contracts; runAs system on the hook: deal closed_won with its approval still pending, contract drafted; contract draft alone split out and elevated: deal stays proposal/pending, contract drafted. The accepted quote is frozen, so close-won never re-runs once the approval lands.",
    "options": [
    "A. runAs: 'system' on the whole hook. Business: the CPQ path always closes the deal and drafts the contract, but a deal its manager has not approved closes anyway (the lock is skipped for a system write). Long-term: approval locks stop meaning anything for the CPQ path. AI-safety: teaches that elevating a hook is a way through a lock. Startup: one line.",
    "B. Split: the contract draft becomes its own elevated hook; close-won stays the caller's write. Business: reps get their contract; a locked deal stays open, but silently, with an accepted quote that never re-closes it after approval. Long-term: rule 9 minimal elevation, but leaves an incoherent state (accepted quote, open deal) with no recovery path. AI-safety: neutral; the silent swallow remains. Startup: small (one hook split, test renames).",
    "C. Refuse the acceptance itself while the linked deal's approval is pending (a transition gate on crm_quote, RECORD_LOCKED at the door), plus B's elevated draft. Business: an approval stays an approval, and the rep learns at the moment of acceptance why it cannot proceed. Long-term: no incoherent state to recover from. AI-safety: a loud refusal at the door instead of a swallowed one in a cascade. Startup: one new refusal branch in quote_workflow plus B.",
    "D. Grant sales_rep crm_contract.allowCreate instead of elevating the draft; close-won unchanged. Business: reps can author contracts by hand, which the permission set withholds today; the lock question stays as shipped (silent)."
    ],
    "recommendation": "C, because on the business axis an approval that an accepted quote can walk around is no approval, and B alone leaves a frozen accepted quote on an open deal with no path back; on the AI-safety axis a refusal at the acceptance beats a refusal swallowed by onError: 'log'; it is small for a startup and keeps rule 9 (only the draft is elevated). The seat asked that neither answer be chosen here, so nothing of F1 is in the PR."
    },
    {
    "question": "F9 lead_auto_assign / case_auto_assign / lead_duplicate_check: may an elevated hook's findings reach the anonymous form caller? Measured with the three hooks elevated: the engine insert under the form door's context returns, and the door's 201 body is { record: result } (rest 17.7.0 createData), duplicate_status 'suspected', duplicate_of_type 'crm_contact', the existing contact's id, and owner_id = a staff user's id. guest_portal's own rule: guests must never read existing CRM data. Also measured: as a sales_rep the duplicate scan sees no other rep's lead or contact (private OWD), so cross-rep duplicates are never flagged either (the #1207 scenario).",
    "options": [
    "A. Elevate all three as-is. Business: web leads and cases are routed and web duplicates flagged; any visitor can test whether an email is in the CRM and learn staff user ids. Long-term: a public oracle. AI-safety: worst, it normalises elevated reads behind an anonymous door. Startup: three lines.",
    "B. Platform first (AGENTS.md section 2): file upstream that the anonymous form door returns the full stored record to a caller whose grant has no read (return only what guest_portal may read, or only the id); the app WAITS, then elevates all three. Business: routing and dedupe for web intake land only when the platform ships it. Long-term: fixes the door for every app, contract-first. AI-safety: best, declaration (allowRead: false) becomes enforced on the write response. Startup: zero app code now.",
    "C. Elevate now and stand the hooks down for the anonymous door explicitly (no user and not system): reps get cross-rep dedupe (M3) and system intake keeps routing; web intake keeps today's behaviour, by declaration instead of by denial. Business: the web-form half of #596 still does not hold. Long-term: a branch to delete when B lands. Startup: small.",
    "D. Move the web-intake work after the response (an elevated async afterInsert hook stamping owner / duplicate flags), so the 201 body never carries them. Business: routing lands for web intake now, but lead_assignment's SLA alert (afterInsert flow) races the owner stamp. Long-term: restructures three hooks around a platform behaviour. Startup: largest."
    ],
    "recommendation": "B, because the defect that makes elevation unsafe is the platform's (an anonymous write door echoing data its caller's grant cannot read), AGENTS.md section 2 says wait for the platform rather than route around it, and contract-first puts the fix where every app benefits; if the maintainer wants the rep half (M3) now, C is the interim that stays inside the guest rule."
    },
    {
    "question": "F11 opportunity_account_capability: may the gate's refusal name an account the caller cannot read? Elevated (runAs: 'system'), the gate does refuse an outsider's deal on a Settlement Only account, but its refusal text names the account the way the UI does (ACC-000042 - Tender Agency Ltd, pinned by #1243) and states its Settlement Only capability to a caller with no read on it. Not implemented here.",
    "options": [
    "A. Elevate, keep the name. Business: the gate closes; an id-holder learns an unreadable account's number, name and capability. AI-safety: teaches echoing elevated reads to the caller. Startup: one line.",
    "B. Elevate, and the refusal names no account ('this account'). Business: the gate closes; the caller already chose the account in the same form, so the dialog stays clear; it still reveals one bit (Settlement Only) about an account the caller named. Long-term: a single code path. AI-safety: a teachable rule (an elevated hook's refusal echoes nothing it read under elevation). Startup: one line plus re-pinning the #1243 naming case.",
    "C. Keep inherit and refuse any account the caller cannot read. Business: a new access policy (no deals on unreadable accounts) that also captures Full accounts and moves the missing-account refusal ahead of the engine's own. Long-term and startup: a sharing-model decision taken inside a hook."
    ],
    "recommendation": "B, because it closes the gate (the business requirement of REQ-0003) without disclosing what elevation read, keeps one code path, gives AI a simple rule to copy, and costs one line; the only loss is the account label in a dialog whose form already shows the account."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: the engine's write door (verify hooks.run, the REST data ingress's call) as a service_agent: completing a task on their own case leaves the case's account last_activity_date empty, while the admin's identical completion stamps today (measured on 17.7.0 after this PR) · evidence: task_activity_bubble writes crm_account / crm_contact / crm_lead recency through ctx.api as the caller and swallows each refusal in a best-effort catch; event_activity_bubble carries the same body verbatim (not measured separately) · same family as this card: name it for #2014's family (a sub-issue), not a single-point card · dedupe words: task_activity_bubble, event_activity_bubble, last_activity_date service agent, activity bubble runAs",
    "carrier: objectstack#22301 (the verify-handle gaps) · noted, not filed · test/helpers/verify-stack.ts runShippedHook calls the bound body handler directly, outside wrapDeclarativeHook, with no ctx.api, so it never applies a hook's runAs: a suite driving an elevated hook through runShippedHook measures the caller path (read in runtime/objectql 17.7.0; no suite does so today)",
    "carrier: 承接者:无 · noted, not filed · the anonymous form door returning the full stored record to a guest_portal caller (allowRead: false) is harmless today and becomes a disclosure the moment any hook on crm_lead / crm_case derives a field from existing data; it is option B of the F9 question rather than a separate card"
    ]
    }

  6. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Verdict: os-dev report 6063106768, PR #2022 — ACCEPT the delivered part, Tier S (not governed): the seat lands it through the merge queue once every check on the head is green. Three sites go to the maintainer, so this card stays open. repo:hotcrm seat, session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T15:22Z.

    Claim amendment (file surface) · claim 6061539417. Two edits outside the declared surface are accepted:

    Checked on GitHub and in git, not taken from the report:

    • Head ee2a7e2e, 15 files, +324 / −133. check-governed-merges --pr finds no path on the register.
    • PR body: first line Part of #2014, the right form for a partial delivery. No closing keyword appears anywhere in the body, so merging leaves this card open, and the seat removes pm:dispatched by hand at landing.
    • Mechanism measured first: runAs: 'system' elevates ctx.api both in process and in the shipped lowered body (dist/objectstack.json booted with bootStack). A control build without it reproduces F12. updated_by still names the person.
    • Every elevated site carries its rule-9 reason and its rule-10 organization note beside the code.
    • Sites delivered: F12, F13, F14, F15a (as a new hook, case_resolution_account_activity, so the agent's narrow task grant is not elevated with it), F15b and F15c. Two members the seat had not measured were measured as defects and added:
      • campaign_member_metrics_refresh: a marketer's recount dropped Private won deals, 2 / 1,100 → 1 / 100. The ablation turns it red.
      • opportunity_promote_account: a rep's win on another user's account left it a prospect.
    • Every test: run the hook, flow and action suites on @objectstack/verify and retire the five hand-built harnesses #2013 pin for these sites now asserts the business fact.
    • Changeset ('hotcrm': patch), checked against the diff: each clause names a site the diff changes. "an agent's own escalation hands the case to a service manager" is F14; "a marketing user's enrollment no longer drops Private won deals" is the campaign recount.
    • pnpm verify is green on ee2a7e2e: 3,565 passed, 1 skipped.

    Held for the maintainer (not in the PR):

    • F1 quote_on_accepted: elevation closes a deal whose approval is still pending.
    • F9, the three web-intake hooks: elevation makes the anonymous form door return CRM facts, an email-existence oracle and a staff user id.
    • F11, the capability gate: an elevated refusal names an account the caller cannot read.

    One more member of this family, reported by the dev: task_activity_bubble (measured: a service agent completing a task on their own case leaves the account's last_activity_date empty, while the admin's identical completion stamps it) and its verbatim twin event_activity_bubble (not measured separately). They join this card's remaining work.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed (part): PR #2022 MERGED as dc58e047 on main (squash, single parent 49fe305a). The seat landed it through the merge queue after 9 of 9 checks went green on head ee2a7e2e: ready_for_review 15:30:16Z → added_to_merge_queue 15:30:20Z → merged 15:30:38Z. repo:hotcrm seat, session_012zh91QzFgePbkmuHnugLN3, 2026-10-08T15:31Z.

    Transition: pm:dispatched → needs-user-decision · Release: claim 6061539417. The delivered sites are on main. What remains needs the maintainer, so no dev may pick it up yet.

    Two readings:

    • the PR object reads merged: true;
    • origin/main is dc58e047 fix(hooks): land the follow-on writes a person's own save fires (runAs: 'system').

    Not closed, as intended: the PR said Part of #2014, and this card is still open. No card was closed by the merge.

    On main now:

    • F12, F13, F14 (organization-pinned) and F15a–c;
    • campaign_member_metrics_refresh and opportunity_promote_account.

    Remaining on this card:

    • F1 quote_on_accepted: may an accepted quote close a deal still under approval, and how does a rep's acceptance get its contract?
    • F9, the three web-intake hooks: elevation makes the anonymous form door return CRM facts.
    • F11, the capability gate: an elevated refusal names an account the caller cannot read.
    • task_activity_bubble / event_activity_bubble: the same caller-context shape, measured for a service agent's task.

    All three questions are in the maintainer's batch hotcrm-R74b (items 3–5) in the seat's chat. The answer is recorded here, and the remaining work is dispatched as this card's second round.

    Unblocked by this merge: #2018 (F16, the same hook file), #2019, and then #1919.


    Generated by Claude Code

  8. added
    needs-user-decisionNeeds the maintainer's call before work proceeds
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Oct 8, 2026
  9. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch hotcrm-R74b items 3, 4, 5 · letters C, B, B · maintainer 「同意」 2026-10-09T07:53Z

    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. Items 3–5 are the three questions the first-round dev raised (report 6063106768).

    The decisions

    • Item 3 · F1 quote_on_accepted · C. While the linked deal's approval is pending, accepting the quote is refused at the moment of acceptance, loudly. The contract draft also moves into its own elevated hook (B's split), and close-won stays the caller's write. So an approval stays an approval; the rep learns why at the acceptance; and no "accepted quote, open deal" state is left with no way back. Not chosen: A (elevate the whole hook; it closes a deal its manager has not approved), B alone (leaves that frozen state), D (grant reps contract create; the lock stays silent).
    • Item 4 · F9, the three web-intake hooks · B. Platform first. Filed: objectstack#22437 (the public form submit answers the anonymous caller with the whole stored record). hotcrm WAITS, then elevates lead_auto_assign, case_auto_assign and lead_duplicate_check. Not chosen: A (a public email-existence oracle), C (elevate but skip anonymous), D (async tagging).
    • Item 5 · F11 opportunity_account_capability · B. Elevate, and the refusal names no account ("this account"); re-pin The raw-id-in-prose class survives in five more places after #1208 — 15 of 31 tasks in a demo org still name a record by its primary key #1243's naming case. Not chosen: A (echo an unreadable account's number, name and capability), C (a sharing policy decided inside a hook).

    What this card does next


    Generated by Claude Code

  10. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    needs-user-decisionNeeds the maintainer's call before work proceeds
    pm:queueReady for the PM dispatch loop
    on Oct 9, 2026
  11. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round R75
    Session: session_012zh91QzFgePbkmuHnugLN3
    Account: os-zhuang (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-2014-round-2
    Worktree: hotcrm-issue-2014-r2
    Domain: repo:hotcrm (single-lane repo, no domain:* taxonomy)
    Seat: repo:hotcrm#1
    File surface: src/revenue/objects/quote.hook.ts (quote_workflow's acceptance refusal; quote_on_accepted split, the contract draft into its own elevated hook); src/sales/objects/opportunity.hook.ts (opportunity_account_capability only); src/sales/objects/task.hook.ts and src/sales/objects/event.hook.ts (task_activity_bubble / event_activity_bubble); their tests (test/hooks-runtime-sales.test.ts, test/hooks-runtime-service.test.ts, test/opportunity-account-capability-gate.test.ts, test/activity-recency.test.ts, and the test/quote-accepted-*.test.ts / test/refusal-envelope.test.ts pins the split moves); the acceptance and approval passages of content/docs/sales/quotes*.mdx and content/docs/administration/state-machines*.mdx (⛔ not the "Generating a quote" section, #2032's); translations only for a new refusal message; one .changeset/2014-*.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent (the os-dev definition and the scripts/pm write tools live in this container's objectstack checkout), model: default (dispatch-gates --tier --repo objectstack-ai/hotcrm answers "the tier stays the PM's per-card judgment call"; governedPathsIn over the surface: none)
    Clause-②: no
    Responsibility: the app (hooks write or read across objects as the triggering person, which the person's grants refuse; the acceptance path swallows the lock refusal) | none: the platform enforces the grants and the lock as declared | every rep accepting a quote on a deal under approval; every agent completing a task on a case; every outsider's deal on a Settlement Only account
    Thread-read: 6076828219
    Serial constraints cleared: round 1 (PR #2022, dc58e047) landed. #1919 (comments in hook files) waits behind this. Sibling claims in flight: #2030 (CHANGELOG.md), #2032 (quote-generation.flow.ts, the "Generating a quote" docs section); disjoint files, the shared docs pages split by section.


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 2014,
    "status": "done",
    "branch": "claude/issue-2014-round-2",
    "pr": "#2035",
    "session": "session_012zh91QzFgePbkmuHnugLN3 (subagent; the parent's harness Claude-Session line)",
    "premise_still_valid": true,
    "summary": "Round 2 delivered as draft PR #2035 (first line Part of #2014, Clause-②: no; the card stays open for F9, untouched: lead_auto_assign / case_auto_assign / lead_duplicate_check unchanged). MEASURED FIRST on base 08cfa20, all three reproduced: F1 a sales_rep's presented→accepted on a deal pending its Large Deal Approval admitted, close-won refused RECORD_LOCKED and swallowed, contract insert PERMISSION_DENIED, 0 contracts, deal proposal/pending (unlocked deal: closed_won, still 0 contracts); F11 an outsider who reads 0 rows of a Settlement Only account inserts a deal on it → STORED; bubble: an agent's completed task AND an agent's held call (event twin, measured this round) on their own case leave the account's last_activity_date empty (account write PERMISSION_DENIED) while the admin's identical write stamps it; a rep's completed task on their own deal whose account/contact a manager owns stamps neither. 'Held' measured per state with the rep's own direct close-won: approval_status pending (platform lock, request lockRecord true) refused; approved/rejected (a recall leaves rejected) admitted; status_change_approval_status pending/rejected refused (opportunity_lifecycle; plus the lock once a request is open); qualification_approval_status pending/rejected refused (opportunity_lifecycle; its request is lockRecord false); approved states admitted; a SYSTEM close-won on a locked deal admitted. A rep cannot read sys_approval_request (PERMISSION_DENIED), so the gate reads the deal's own mirror columns. FIX: (F1, C) quote_workflow refuses a USER's transition into accepted while the linked open deal is held in exactly those states, RECORD_LOCKED/409 (existing code), message names the quote by its title and the hold by the field label/value the rep sees, reads the deal AS THE CALLER (no elevation; unreadable deal or failed read stands down); the contract draft moved out of quote_on_accepted into its own hook quote_accepted_contract_draft (runAs: 'system', rule-9 reason beside it; rule 10: no read, the insert is stamped from the trigger's tenant which withRunAs('system') carries, pinned by test/hook-org-inheritance.test.ts); close-won stays the caller's write in quote_on_accepted. (F11, B) opportunity_account_capability runAs: 'system', reads only commercial_capability, pinned to the new deal's organization_id when present, refusal says 'this account' and names nothing; #1243's naming case re-pinned (the message has one locale, English; the three docs locales updated). Bubble: task_activity_bubble and event_activity_bubble runAs: 'system'; every walk-up read pinned to the triggering record's organization_id; because a by-id update cannot carry an extra where key (engine refuses it), each write target is confirmed inside the organization by one pinned read per object; the first-response stamp writes only when its pinned read found the case. Docs (3 locales): quotes (a new Built-in rules bullet, the acceptance passages; not the 'Generating a quote' section), state-machines (new paragraph after the CPQ edge sentence), accounts (the Settlement Only refusal no longer names the account). Changeset .changeset/2014-accept-quote-under-approval.md ('hotcrm': patch). files_changed (29 vs main): src/revenue/objects/quote.hook.ts, src/sales/objects/{opportunity.hook.ts,task.hook.ts,event.hook.ts,_picklists.ts (comment)}, src/revenue/objects/contract.object.ts (comment), test/{hooks-runtime-sales,hooks-runtime-service,opportunity-account-capability-gate,activity-recency,case-first-response,quote-accepted-draft-defaults,quote-accepted-lookups,quote-accepted-payment-terms,refusal-envelope,contract-write-depth,record-id-not-in-prose,quote-contact-required-when}.test.ts, content/docs/sales/{quotes,accounts}{,.zh-Hans,.zh-Hant}.mdx, content/docs/administration/state-machines{,.zh-Hans,.zh-Hant}.mdx, docs/feature-inventory.md (QUO-005/006), .changeset/2014-accept-quote-under-approval.md. DEVIATIONS: (1) surface beyond claim 6076910943: contract.object.ts + _picklists.ts comments (they name the payment-terms carrier, pinned by quote-accepted-payment-terms), accounts*.mdx (the dispatch names the Settlement Only refusal docs), comment-only renames in contract-write-depth / record-id-not-in-prose / quote-contact-required-when, case-first-response (pinned the caller-context behaviour removed: a writer who cannot read the case now stamps its first response — the whole event bubble is elevated per the ruling, so that write is too), feature-inventory QUO-005/006. (2) The three quote-accepted suites and the hooks-runtime-sales acceptance cases now accept as a SALES REP (their comments said the manager was only a workaround for this defect), beyond 'add the rep case'. (3) src/revenue sat ~178 tokens under its authored-total ceiling; the split + gate were paid by compacting quote.hook.ts code (no ceiling moved; authored total 18,990 / 19,000, business semantics 15,879 / 16,000); the refusal names 'its opportunity' rather than the deal's name for the same reason. (4) Source hygiene refused record ids in the two bare cascade diagnostics once they left the failures.push sink: they now name the quote by its quote_number - name label / quote number; quote-accepted-lookups' message pins follow. (5) Organization boundary recorded: with the F11 pin a TENANT-LESS system insert of a deal in org A on org B's Settlement Only account is admitted (was refused by the unscoped read) — the same answer a person's tenant-scoped read gives. (6) Merge commit 73c712a (origin/main f071b36 merged, no rebase) carries git's default message, no trailer; the harness attribution reminder names a model (Co-Authored-By: Claude Opus 5.5) — commits carry the model-free pair per dispatch, not imitated. (7) CI was in_progress at report time (Check Changeset, Quality Checks, link-check, Label already success). Worktree removed (node_modules first, no --force).",
    "tests": "GATE pnpm verify @ a83103b: OS_VERIFY_LOCK_SLOT=hotcrm-2014 bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c 'pnpm verify' → os-verify-lock: VERDICT command-exit 0 · held the lock 477s (7m57s) · waited 0s; validate ✓ Validation passed; tsc --noEmit clean; lint --strict 18 suggestion(s), no error; lint:i18n-gate ✓ i18n lint gate: 0 i18n/missing-* issues; hygiene ✓ source hygiene clean; hygiene:tokens ✓ source token ratchet clean (src/revenue authored ~18,990 / 19,000; src/sales headline unchanged ~57k / ~28k / ~101k); build ✓ Build complete; test Test Files 172 passed (172) Tests 3589 passed | 1 skipped (3590). Earlier verify @ 73c712a: VERDICT command-exit 1, source hygiene record id in prose x2 in quote.hook.ts cascade diagnostics, fixed in a83103b. MEASURE (scratch, never committed, through hooks.run on 08cfa20): M1 accept=admitted, closeWonWrite=RECORD_LOCKED, contractInsert=PERMISSION_DENIED, contracts 0, opp proposal/pending; M1b unlocked: closed_won, contracts 0; M1c per-state rep close-won table as in summary; M5 recall → approval_status rejected, request recalled, rep close admitted; M2 outsiderReadsAccount 0, outsiderInsert admitted; M3 agent task: account null (PERMISSION_DENIED) vs admin 2026-10-09; M4 agent held call: account null vs admin stamped. AFTER (same scratch on the fix): M1 RECORD_LOCKED/409, quote presented, 0 opportunity updates, 0 contract inserts; M1b contracts 1, closed_won; M2 outsider VALIDATION_FAILED 'this account'; M3/M4 stamped. ABLATION (one call under the lock, scripts/ablation-replace.mjs, each anchor x1 → x0, each restore blob == HEAD and git diff HEAD empty, final tree clean, scratch file removed): control two-org 3/3 green; A1 gate the held test flipped (held.length greater than 0 made less than 0): 5 failed (Large Deal case + 4 held rows) | 5 passed (free rows); A2 draft runAs→inherit: rep-draft case red (test timeout waiting for a contract); A3 gate runAs→inherit: outsider case red ('an outsider opened a deal'); A4 task bubble runAs→inherit: agent task case red; A4b event bubble runAs→inherit: agent call + first-response cases red; A5/A5b org pin removed (task/event): 'the bubble crossed into B' / 'the walk-up crossed into B'; A5c gate org pin removed: the cross-organization insert refused. Targeted runs before verify: 17 touched/adjacent suites, 509 passed then 3 fixed (refusal site count 22→23, payment-terms fixture owner, first-response re-pin). NOT MEASURED locally, declared to CI: test:coverage thresholds (the org-pinned branches run only on a multi-organization boot), docs-app types:check, e2e, link-check; CI in_progress at report time.",
    "mcp_calls": "0",
    "api_writes": "3 relay writes as objectstack-fleet[bot], each a repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create → POST /repos/objectstack-ai/hotcrm/pulls (draft, #2035; read-back 10638 bytes sent, 10638 stored, identical; REST read-back body identical, draft true); (2) label-write --assign os-zhuang → POST /repos//issues/2035/assignees (read-back MATCHES; labeler had set documentation, ci/cd, metadata, backend); (3) this os-dev-report comment → POST /repos//issues/2014/comments. Plus git push of the branch three times (empty-branch probe, 5674002, a83103b with merge 73c712a), not REST. Reads via gh api (issue, comments, PR, check-runs).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed · scripts/check-source-hygiene.mjs's DIAGNOSTIC_SINK comment says quote.hook.ts keeps two failures.push diagnostics; after this PR it keeps none (both cascade throws name the quote by label / number). A stale comment in a gate script, not edited here.",
    "carrier: 承接者:无 · noted, not filed · a tenant-less system acceptance on a two-organization install drafts no contract: the platform refuses an organization-less insert (measured before and after this PR); nothing in the app accepts quotes as the system, so no producer. In PR Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed · boundary of the F1 mirror: it covers the three approval flows the app ships on crm_opportunity; an install that adds another lockRecord approval there is not mirrored (narrower: acceptance admitted, close-won swallowed as before). In PR Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed · test/hooks-runtime-service.test.ts is at 82% of the 100KB file cap (hygiene advisory, 18KB headroom)."
    ]
    }

  13. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Verdict: os-dev report 6078092344, PR #2035 (round 2) — ACCEPT, Tier S (not governed: check-governed-merges 0 of 29 paths, 1,105 changed lines ≤ 5000): the seat lands it through the merge queue once every check on head a83103b5 is green. repo:hotcrm seat, session_012zh91QzFgePbkmuHnugLN3, 2026-10-09T09:22Z.

    Checked on GitHub and in git, not taken from the report:

    • PR shape: draft, base main; first line Part of #2014 (this card stays open for F9), then Clause-②: no, no closing keyword. F9's three hooks are untouched. A trial merge of origin/main f562142f (fix(quote_generation): refuse Generate Quote before qualification approval instead of leaving an orphan quote #2034, which edited the same Quotes and state-machines pages by section) is clean.
    • F1, ruled C, read line by line:
      • quote_workflow refuses a user's move into accepted while the linked open deal is held, with RECORD_LOCKED / 409 (an existing code). The message names the quote by its label and the hold by the field label the rep sees.
      • "Held" mirrors exactly what refuses a rep's close-won, measured state by state: approval_status pending (the platform lock; a recall leaves rejected, which does not hold), and status_change_approval_status / qualification_approval_status pending or rejected (opportunity_lifecycle). No wider.
      • The deal is read as the caller, not elevated; an unreadable deal or a failed read stands the gate down.
      • The contract draft moved into its own hook quote_accepted_contract_draft with runAs: 'system' and the rule-9 reason beside it. It issues no read; its one insert inherits the trigger's organization (rule 10, pinned by test/hook-org-inheritance.test.ts). Close-won stays the caller's write in quote_on_accepted, so a deal under approval is never closed by elevation.
    • F11, ruled B: opportunity_account_capability is runAs: 'system'. It reads only commercial_capability, pinned to the new deal's organization_id, and the refusal says "this account" and names nothing. The raw-id-in-prose class survives in five more places after #1208 — 15 of 31 tasks in a demo org still name a record by its primary key #1243's naming case is re-pinned.
    • The activity bubble: task_activity_bubble and event_activity_bubble are runAs: 'system'. Every walk-up read is pinned to the triggering record's organization, and each write target is confirmed inside it by one pinned read per object (a by-id update cannot carry the extra key). Round 1's pattern.
    • Measured before and after through the handle on 08cfa209:
      • F1: acceptance admitted, close-won refused and swallowed, 0 contracts → RECORD_LOCKED, quote stays presented, 0 writes; and on an unlocked deal, 1 contract.
      • F11: an outsider's deal on a Settlement Only account was stored → refused, "this account".
      • Bubble: an agent's task and held call left the account empty → stamped.
    • Ablation, each restored to HEAD:
      • gate inverted: 5 red;
      • draft, gate and both bubbles de-elevated: each its case red;
      • each organization pin removed: the cross-organization case red.
    • Tests now accept as a sales rep. The three quote-accepted-* suites and the acceptance cases had used a manager as a workaround for this defect.
    • pnpm verify green on a83103b5: 3,589 passed, 1 skipped. src/revenue is at ~18,990 of its 19,000 ceiling, paid by compacting quote.hook.ts; no ceiling moved.

    Surface beyond the claim, accepted: accounts*.mdx (the Settlement Only refusal's docs, which the dispatch named), docs/feature-inventory.md QUO-005/006, comment-only lines in contract.object.ts / _picklists.ts, and the case-first-response re-pin. The elevated event bubble now stamps first response even for a writer who cannot read the case, as the ruling elevates the whole bubble.

    Out-of-scope findings, carried (no card):

    • scripts/check-source-hygiene.mjs's DIAGNOSTIC_SINK comment still says quote.hook.ts keeps two failures.push diagnostics; it now keeps none. A stale comment for the next PR that touches that file.
    • The F1 mirror covers the three approval flows the app ships on crm_opportunity; an install that adds another locking approval is not mirrored (PR Acceptance notes).
    • A tenant-less system acceptance on a two-organization install drafts no contract: the platform refuses an organization-less insert. No producer in the app (PR Acceptance notes).
    • test/hooks-runtime-service.test.ts is at 82% of the file cap.

    After landing: this card goes pm:blocked with Blocked-by: objectstack-ai/objectstack#22437, only F9 left (ruling 6076828219).


    Generated by Claude Code

  14. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed (round 2): PR #2035 MERGED as a2f2a40a on main (squash, single parent f562142f). The seat landed it through the merge queue after 10 of 10 checks went green on head a83103b5: ready and queued at 09:29Z, merged 09:29:56Z. repo:hotcrm seat, session_012zh91QzFgePbkmuHnugLN3, 2026-10-09T09:30Z.

    Two readings:

    • the PR object reads merged, merge commit a2f2a40a;
    • origin/main is a2f2a40a fix(quotes,hooks): refuse an acceptance an approval holds; draft a rep's contract; the capability gate and activity bubble. git diff f562142f a2f2a40a is the PR's 29 files, +755 / −350, the reviewed change (ACCEPT 6078126591).

    What now holds:

    • A rep's acceptance of a quote whose deal is held by an approval is refused at the acceptance (RECORD_LOCKED).
    • A rep's acceptance on an unlocked deal drafts the contract (elevated draft); close-won stays the rep's own write.
    • An outsider's deal on a Settlement Only account is refused, naming no account.
    • An agent's or rep's completed task or held call stamps the account, contact and lead recency.

    Not closed, by design: the PR says Part of #2014. One item is left on this card: F9, the three web-intake hooks (lead_auto_assign, case_auto_assign, lead_duplicate_check), ruled B in 6076828219: the platform fixes the anonymous form door first.

    Transition in this act: pm:dispatched → pm:blocked, with Blocked-by: objectstack-ai/objectstack#22437 at the top of the body; the assignee is removed. When #22437 closes, re-derive: unlock only once its fix is installable in the pin (#2031 carries the upgrade), then elevate the three hooks and pin that the anonymous answer carries none of their findings.


    Generated by Claude Code

  15. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    F9 measured in a browser on HotCRM 4.0.1 (main 1d7148bf, @objectstack/* 17.7.0) by the maintainer's browser pass #2058 (findings A-08, B-02, C-20, B-21). repo:hotcrm seat, session_012zh91QzFgePbkmuHnugLN3, 2026-10-10T19:35Z.

    Evidence: qa/2026-10-10-browser-pass/{a,b,c}/REPORT.md and the PNGs on branches claude/qa-browser-pass-a|b|c.

    F9 stays pm:blocked on objectstack-ai/objectstack#22437 under ruling hotcrm-R74b item 4 (B). These readings are the acceptance baseline for when the pin bump (#2031) brings the fix.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions