Skip to content

Scenario loader refuses an operation its persona cannot reach (row scope, hidden field or object); fix the three cards it catches #30802

Closed

Description

@objectstack-fleet

epic:#3600#1 · hotlong · session_01Q4EjjPXwX1YfHyKJUh4agu

Filing-gate category: ① defect class with named locations and a proven guard, split from #30770 (report comment 6676516411). Maintainer, verbatim (Claude Code session, 2026-11-06), on routing: 「能交给车队开发的你不要自己开发」. Epic context: objectstack-ai/cloud#3600; this card is the lane's, not the epic's.

Blocked-by: #30770

(Serial: PR #30801 for #30770 edits the crm card and the runner tests this card's branch builds on.)

Problem

A scenario card can declare an operation that a faithful build can never perform as declared: the acting persona's row scope excludes the target row, or the persona hides a field or object the write touches. The run then grades a correct app as failing. #30770 fixed the crm card; the same class exists in three more cards.

The guard is already built and proven

Branch claude/issue-30770-loader-reach-held @ 82b1acd8 (pushed, not landed): a loader refusal that is statically decidable from the card alone (row scope against the seed, hidden objects and fields against the written values). Measured there: refusals.test.ts 48/48 with the rule; ablation (the loader restored to the pre-rule bytes) gives 43 passed / 5 failed, exactly the 5 new reach cases. Resume from that sha.

Cards it refuses today (fix each in this card)

  • docs/qa/scenarios/invoice_with_line_items.json, ops add_line and line_without_product: sales_rep hides invoice_line.unit_price, which the create writes.
  • docs/qa/scenarios/project_with_tasks.json, ops add_task and task_without_title: contractor hides task.estimate_hours, which the create writes.
  • docs/qa/scenarios/travel_expense.json, op employee_approves_colleague: the target approval_step row is outside employee's row scope. The guard expects RECORD_NOT_FOUND (the platform answers a by-id write on an unreadable row with not-found); the card declares PERMISSION_DENIED. Measure once against a booted app whether this decision goes through the by-id data update (→ RECORD_NOT_FOUND) or an approval door that refuses first (→ PERMISSION_DENIED), make the card match the measurement, and make the guard's row arm agree. If the two paths contradict a documented contract, stop and report it instead of choosing.

Use the same moves #30770 used: drop the hidden field from the write, or switch to a persona that can perform it, keeping what each operation was meant to test. Keep each card in its counts band. Also fix packages/qa/scenarios/README.md, which still shows gradeCardOperation(card, 'remove_chen', observed); the operation is now remove_ben.

Any card the devx mid-tier lane lands meanwhile (#30477, #30479, #30481) must pass the guard too.

Reader: the domain:devx lane seat, from pm:queue once #30770 closes.


Generated by Claude Code

Activity

  1. added
    priority:p1High: required for production / M2
    domain:devxDomain lane — seat card indexed by label:pm:seat
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    on Nov 6, 2026
  2. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p1 kept · domain:devx · pm:blocked · 2026-11-06T04:57Z

    ⛔ Not a claim.

    • The scenario loader refuses an operation its persona cannot reach. A scenario-library card, p1 as filed.

    Generated by Claude Code

  3. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 6, 2026
  4. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#3600#1 · hotlong · session_01Q4EjjPXwX1YfHyKJUh4agu

    Blocked-by: #30770 closed (PR #30801 merged as 974209d6). pm:blocked → pm:queue for the domain:devx lane.

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 6, 2026
  6. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#3 · os-elon-musk · session_016WV4cz3RTQjLh7hZBLzgtZ

    Claim: PM loop round 2 · 2026-11-06T05:18Z
    Session: session_016WV4cz3RTQjLh7hZBLzgtZ (devx seat 3) · dev: session_016WV4cyjYBCnWT8iGQXxoeH (card thread)
    Account: os-elon-musk
    Branch: claude/project-thread-9464u3 (amended: the card thread's branch)
    Worktree: card thread container
    Domain: domain:devx
    Seat: domain:devx#3
    File surface: packages/qa/scenarios/src/ (the loader's reach rule) · packages/qa/scenarios/test/ (refusals + any card truth pin the fixes move) · packages/qa/scenarios/README.md · docs/qa/scenarios/*.json (the cards the guard refuses: the card's four named ops, plus any of the landed mid-tier cards #30477 / #30479 / #30481 that fail it) (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: xhigh (p1)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the scenario-card authors' (cards declare operations their persona cannot reach) | the loader has no reach rule on main; the guard is built on claude/issue-30770-loader-reach-held | reached by every run of the scenario runner today, grading a correct app as failing
    Thread-read: 6677574503
    Premise: holds (Blocked-by #30770 closed by PR #30801 974209d6; hotlong unblocked 6677574503)
    Resume-from: claude/issue-30770-loader-reach-held@82b1acd8 (named in the card body)
    Serial constraints cleared: #30795 (p1, same lane, unclaimed at this reading) edits the loader and every card under docs/qa/scenarios/ to add code; parallel by the lane rule, the later lander merges main and re-runs both cards' tests. No other in-flight claim touches packages/qa/scenarios/ or docs/qa/scenarios/.

  7. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · os-elon-musk · session_016WV4cymvo9YoyLwKCXXBNw

    Claim: PM loop round 6
    Session: session_016WV4cymvo9YoyLwKCXXBNw
    Account: os-elon-musk
    Branch: the card thread's platform-assigned claude/project-thread-* branch (a measured platform exception to claude/issue-<n>-<slug>); this line is amended with the exact name when the thread opens. The work resumes from claude/issue-30770-loader-reach-held @ 82b1acd8.
    Worktree: objectstack-issue-30802
    Domain: domain:devx
    Seat: domain:devx#1
    File surface: packages/qa/scenarios/src/loader.ts (the reach refusal) with refusals.test.ts; docs/qa/scenarios/invoice_with_line_items.json, project_with_tasks.json and travel_expense.json; packages/qa/scenarios/README.md (remove_chen → remove_ben).
    Container & model: M, mode:thread, model: default tier, effort: xhigh (p1)
    Clause-②: no — a devx instrument (@objectstack/scenarios is private, "not a protocol"); no platform door changes.
    Metadata-protocol: none
    Responsibility: the loader accepts an operation its acting persona cannot reach, so a faithful build is graded failing; class split from #30770 (report comment 6676516411).
    Thread-read: 6677574503
    Premise: holds — Blocked-by: #30770 closed (PR #30801 merged as 974209d6); the held branch claude/issue-30770-loader-reach-held resolves to 82b1acd85a on origin.
    Serial constraints cleared: no open PR touches packages/qa/scenarios/** or docs/qa/scenarios/** (checked 2026-11-06T05:18Z). #30795 (claimed by this seat beside this card) edits the same loader and every card; it lands AFTER this card and merges main before its PR.

  8. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · os-elon-musk · session_016WV4cymvo9YoyLwKCXXBNw

    Release: claim 6677604452 (session_016WV4cymvo9YoyLwKCXXBNw, devx seat 1) · 2026-11-06T05:22Z · claim race: devx seat 3's claim 6677601301 came first, so seat 1 yields. Seat 3's claim stands alone. The card thread opened on seat 1's order (branch claude/project-thread-9464u3, session session_016WV4cyjYBCnWT8iGQXxoeH) now works under seat 3. Labels and assignee are unchanged (pm:dispatched, os-elon-musk, the same account).

  9. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#3 · os-elon-musk · session_016WV4cyjYBCnWT8iGQXxoeH
    os-dev-report

    {
      "issue": 30802,
      "status": "needs_decision",
      "branch": "claude/project-thread-9464u3",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30846",
      "session": "session_016WV4cyjYBCnWT8iGQXxoeH",
      "premise_still_valid": true,
      "summary": "Merged the held rule (claude/issue-30770-loader-reach-held@82b1acd8) with main and opened draft PR #30846 (Part of #30802, skip-changeset, head d1909669, main merged at b6182249). The loader refuses an operation whose acting persona cannot reach its object, row or written field unless the card declares the refusal a faithful build gives; four of the five operations it catches on main are fixed by dropping the hidden field from the write (invoice_with_line_items add_line / line_without_product, project_with_tasks add_task / task_without_title), and the README example names remove_ben. travel_expense employee_approves_colleague is left as on main: the booted-app measurement found the two doors contradict a documented contract, so per the card it is reported for a ruling (open_questions) instead of chosen. The reach codes are literals typed ErrorCode plus a load-time ErrorCode.parse, because check:dispatcher-error-vocabulary reads a code constant only when it resolves to a literal.",
      "tests": "At d1909669: `pnpm --filter @objectstack/scenarios test` → Test Files 2 failed | 16 passed (18), Tests 1 failed | 224 passed (225); both failing files (library.test.ts 'loads every card with no refusal', travel-expense.test.ts at load) report exactly one refusal, travel_expense.json run.operations[4].outcome.code (wants RECORD_NOT_FOUND; card declares PERMISSION_DENIED). test/refusals.test.ts 48 passed (48). Ablation via scripts/ablation-replace.mjs --delete on the call site `    if (actor !== undefined) checkReach(op, actor, at);\\n` (1→0 hits, blob 20339cae→bb319f1a): refusals 43 passed / 5 failed (exactly the five reach cases), library.test.ts and travel-expense.test.ts pass; restored blob == HEAD 20339cae, git diff HEAD empty. Same 48/48 and 43/5 first read at f4757520 (82b1acd8 + main) and at 4ff318eb. tsc --noEmit -p packages/qa/scenarios exit 0; pnpm check:nul-bytes exit 0. Reverse check of the code typing: a code outside the catalogue gave check:dispatcher-error-vocabulary exit 1, tsc TS2322, vitest load failure. travel_expense measurement: `objectstack dev --fresh --no-watch` (server 18.3.0 from main c17719dd), throwaway app with the card's approval_step shape and decide_approval_step as a declarative update, personas via the runner's HttpApp; colleague's step as_112_1 as employee: read-only grant → MCP update_record / REST PATCH 403 PERMISSION_DENIED, MCP run_action 404 RECORD_NOT_FOUND; edit grant narrowed by update rule approver==me → 404 at both; edit with row scope only → 404 at both; a missing id answers the same as the hidden row everywhere; employee get_record on as_112_1 → RECORD_NOT_FOUND. Driver, app and full output: /mnt/project-files/pm-devx-seat3-30802/measurement/.",
      "gates": "dispatch-gates --repo objectstack-ai/objectstack --commands: 52 families (merge base 2038c56e; re-derived at d1909669, merge base b6182249: identical list). Run one at a time from 4ff318eb, each exit recorded before any pipe; during the run loader.ts changed only in its two code constants and their comment (0f084113..dc5fdd5e). Re-run at dc5fdd5e: pnpm check:dispatcher-error-vocabulary exit 1 → fixed by the typed literals → exit 0; node scripts/check-plugin-teardown-shape.mjs --self-test exit 3 (fixture commit 621a4876 absent from the shallow clone) → fetched → exit 0, 48 cases. NOT MEASURED: pnpm check:dual-build-cjs-loads exit 3, prerequisite not met (8 packages without dist), left to CI Lint & Repo Gates. --ran at d1909669: '52 derived famil(ies) accounted for — 51 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. Changed lines 99 (+88 / -11, 5 files) vs 3000. CI on d1909669: running at report time; the seat thread is told when it is green on the final head.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "5 — mcp__github__issue_read get #30802, mcp__github__issue_read get_comments #30802, mcp__github__pull_request_read get #30801, mcp__github__list_pull_requests (head filter, before pr_create), mcp__github__pull_request_read get #30846 (read-back); no write tool. Also subscribe_pr_activity on #30846 (session subscription, not a GitHub write).",
      "api_writes": "2 — relay pr_create (dispatch fw-20261106T062127Z-d92c45, run 42173911420 → #30846, body read back identical 13435/13435 bytes, labels skip-changeset, draft); this os-dev-report comment on #30802 via post-stamped. git push ×5 to claude/project-thread-9464u3 (f4757520, e167c992, 4ff318eb, dc5fdd5e, d1909669), no force.",
      "open_questions": [
        {
          "question": "travel_expense op 4 employee_approves_colleague (employee decides colleague's step as_112_1, outside its row scope; card declares PERMISSION_DENIED, the guard wants RECORD_NOT_FOUND). The booted app answers 403 at the data update and 404 at the approval action door when the employee holds no edit grant, and 404 at both when it holds one; the card cannot state the grant. Which fix?",
          "options": [
            "A — retarget to a step the employee can see (a step of its own claim, e.g. EC-0098's), keep PERMISSION_DENIED. Measured PERMISSION_DENIED at both doors under both faithful grant models; guard unchanged; the agent can find the row. Cost: the op becomes 'employee approves its own claim' (renamed id + one line in test/travel-expense.test.ts, one dev commit, S). Every pending step in the seed is on someone else's claim, so the ask names an already-decided step unless the seed gains one pending step on an employee claim, which moves computed truths (M, recheck counts band).",
            "B — declare RECORD_NOT_FOUND (guard already accepts it). Right at the action door under every model and at the data door with an edit grant; wrong at the data door under least privilege, and the agent still cannot find the row (runner likely observes '(no write attempted)'). Cost: one-line card + test edit (S).",
            "C — keep PERMISSION_DENIED and widen the guard's row arm to accept it when the card gives the persona no applied write on that object. Right at the data door under least privilege only; wrong at the action door under every model; makes the rule depend on an inferred grant. Cost: loader + refusals test change, re-ablation (S/M)."
          ],
          "recommendation": "A, because it is the only option measured to give one answer at both doors under every faithful grant model and the only one where the agent can reach the row; the door disagreement is a platform question filed separately (out_of_scope_findings)."
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: b · reach: MCP run_action on a declarative update action (decide_approval_step, operation 'update') answers 404 RECORD_NOT_FOUND where MCP update_record and REST PATCH /api/v1/data on the same row answer 403 PERMISSION_DENIED, for a caller with read but no edit grant on the object (server 18.3.0, main c17719dd, measured) · contract: content/docs/protocol/objectui/actions.mdx says a declarative update fires the caller's permissions 'exactly as for a user edit'; content/docs/protocol/kernel/http-protocol.mdx 'Refusals come before the record lookup' · Seam: spec:action operation 'update' → runtime:packages/runtime/src/action-execution.ts invokeBusinessAction (loadActionSubjectRecord + refuseDeniedSubjectLoad before the write) · evidence: /mnt/project-files/pm-devx-seat3-30802/measurement/measure.out · dedupe words: declarative update action, run_action, refuseDeniedSubjectLoad, RECORD_NOT_FOUND vs PERMISSION_DENIED, action door permission order",
        "carrier: none · docs/qa/scenarios/README.md (format reference) does not describe the reach refusal; outside the claim's file surface; noted in PR Acceptance notes, not filed"
      ],
      "deviations": "(1) Early in the run I executed `env | grep -i -E 'session|claude_code'` (a sed masked only TOKEN-named values), which printed values of these variables to the session transcript, names only: CLAUDE_CODE_ACCOUNT_UUID, CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD, CLAUDE_CODE_ARTIFACT_ASSETS, CLAUDE_CODE_ARTIFACT_MULTI_FILE, CLAUDE_CODE_ARTIFACT_TYPE_CATALOG, CLAUDE_CODE_ARTIFACT_TYPE_CLOUD_CREATE, CLAUDE_CODE_BG_TASKS_REPORT_RUNNING, CLAUDE_CODE_CCR_EVENT_FLUSH_POLICY, CLAUDE_CODE_CHILD_SESSION, CLAUDE_CODE_CONTAINER_ID, CLAUDE_CODE_DEBUG, CLAUDE_CODE_GZIP_REQUEST_BODIES, CLAUDE_CODE_HIDE_SETTINGS_HINT, CLAUDE_CODE_MESSAGING_SOCKET, CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST, CLAUDE_CODE_REMOTE_SDK_URL, CLAUDE_CODE_RETRY_WATCHDOG, CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS, CLAUDE_CODE_SESSION_ID, CLAUDE_CODE_USER_EMAIL. No value reached a file, commit or GitHub write. (2) Five pushes, not one: the merge with main twice (main moved) and three small commits fixing the dispatcher-vocabulary gate. (3) Commit 9c02bb55's message wrongly claims the typed constant admits any string (its reverse check used RECORD_GONE, which is a registered code); corrected in dc5fdd5e's comment and message, history not rewritten. (4) The 52-family gate run spanned 4ff318eb..dc5fdd5e (see gates); the two families that read the changed constants were re-run at dc5fdd5e.",
      "line_budget": "99 changed lines (+88 / -11, 5 files) vs the 3,000 human-merge threshold",
      "files_changed": [
        "packages/qa/scenarios/src/loader.ts",
        "packages/qa/scenarios/test/refusals.test.ts",
        "packages/qa/scenarios/README.md",
        "docs/qa/scenarios/invoice_with_line_items.json",
        "docs/qa/scenarios/project_with_tasks.json"
      ]
    }

    Generated by Claude Code

  10. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#3 · os-elon-musk · session_016WV4cz3RTQjLh7hZBLzgtZ

    Claim: re-post of claim 6677601301 (unchanged holder) · 2026-11-06T11:43Z
    Session: session_016WV4cz3RTQjLh7hZBLzgtZ (devx seat 3) · dev: session_016WV4cyjYBCnWT8iGQXxoeH (card thread)
    Account: os-elon-musk
    Branch: claude/project-thread-9464u3
    Domain: domain:devx
    Seat: domain:devx#3
    Clause-②: no
    Metadata-protocol: none
    Why re-posted: check:closing-target-claim on PR #30846 read devx seat 1's later claim 6677604452 as governing, because its yield 6677645250 is spelled as a Release: and not as the already claimed — yielding marker. Seat 1 yielded to this claim at 2026-11-06T05:22Z; nothing about the holder, branch or surface changes. File surface, premise and serial constraints stand as in 6677601301.

  11. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#3 · os-elon-musk · session_016WV4cz3RTQjLh7hZBLzgtZ

    Landed · 2026-11-06T12:07Z

    Reading Value
    PR #30846 MERGED 2026-11-06T12:06Z as 26c965cb on main, head 72472e89 (seat ACCEPT on the PR)
    Card #30802 closed completed by Fixes #30802

    State: pm:dispatched removed in this stroke. Delivered: the scenario loader refuses an operation its acting persona cannot reach (checkReach), and every card in the library loads with no refusal. travel_expense op 4 is employee_approves_own_claim per the maintainer's ruling (option A, 2026-11-06T11:08Z). Left for a later card, as the ruling says: coverage of a write across row scope. Related platform finding #30848 closed by PR #30878.

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

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain:devxDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions