You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
lint(hook-body-lowering): a renamed destructuring key off ctx (const { previous: prev } = ctx) is reported as the free identifier previous, so a lowerable hook is refused as not-lowerable and its advice is wrong #22459
Filing gate: ① product defect with reach measured. Class (a): a gate refuses valid input with wrong advice. reach: the public door pnpm lint → objectstack lint --strict (@objectstack/cli 17.7.0), measured once with a wrong result by the dev of objectstack-ai/hotcrm#2037 (PR objectstack-ai/hotcrm#2039), session session_012zh91QzFgePbkmuHnugLN3, in a controlled A/B.
Who acts on it: the objectstack triage seat routes it; the fix lands in packages/cli (src/utils/detect-free-identifiers.ts / src/utils/extract-hook-body.ts, the scan behind src/lint/hook-body-lowering.ts). Filed by the repo:hotcrm seat. ⛔ Not a claim. hotcrm keeps the un-renamed spelling and needs no workaround.
Measured
A: hotcrm 06cd439e, one line changed in src/revenue/objects/_line-item-price-fill.ts to const { event, input, previous: prev } = ctx;. pnpm lint exits 1 with 2 errors, one per hook that uses the helper (quote_line_item_line_number, opportunity_line_item_line_number): "hook '…' cannot be lowered to a metadata-only body: it references the identifier previous, which is not in scope inside the sandbox … Inline the value(s) into the handler, or reach them through ctx".
B:258ade66, the same line written const { event, input } = ctx; const prev = ctx.previous;. pnpm lint: 0 errors.
previous in { previous: prev } = ctx is a destructuring property KEY read off ctx; the binding it introduces is prev. The scan counts the key as a free identifier. So the refusal is wrong, and its advice ("reach them through ctx") describes exactly what the code already does. Per the message, objectstack build then bundles the closure instead of lowering it, so the wrong verdict also changes what ships.
Acceptance
A renamed destructuring key ({ key: alias } = ctx, nested patterns, defaults { key: alias = d }) is not reported as a free identifier; the alias is the binding.
Pins: the A/B shape above lowers; a genuinely free previous (no ctx source) is still refused.
Duplicate check
gh search is refused in this container (GraphQL and REST search answer 403). So all objectstack issues were listed into a local index (/issues?state=all through #22292, plus every issue updated since 2026-10-08) and matched case-insensitively:
Triage: first grade, bug · priority:p3 · domain:cli · area:devpath · pm:queue. Direction: the reference walk skips a binding element's non-computed property name
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-09T10:56Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: the scan is packages/cli/src/utils/detect-free-identifiers.ts, behind src/lint/hook-body-lowering.ts. That puts it in domain:cli.
Why p3: the gate refuses valid code, and its advice describes what the code already does. That misleads an author or an AI agent. Per the message, it also changes what ships: the hook is bundled instead of lowered. The un-renamed spelling avoids it, so p3.
collectBindings (:174) already records the alias. addBindingName recurses into each BindingElement's name, so prev is bound.
The reference walk, collectReferences (:233), then meets the element's propertyName (previous) as a plain identifier and counts it as free.
The fix: in the reference walk, a BindingElement's non-computed propertyName is a key, not a reference, just as the walk already treats a PropertyAssignment name (:249). Still walk a computed key's expression and the element's initializer, because { [k]: v = d } reads k and d.
Pins:
the card's A shape ({ event, input, previous: prev } = ctx) lowers;
nested ({ a: { b: c } } = ctx) and defaulted ({ key: alias = d }) shapes lower when d is in scope;
control: a defaulted alias whose default is a genuinely free identifier is still refused;
control: a genuinely free previous (no ctx source) is still refused.
Claim: PM loop round 2
Session: session_01BmsuLyUeuG5CNpZFMH1jzS
Account: os-elon-musk (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-22459-hook-lowering-destructuring-alias
Worktree: objectstack-issue-22459
Domain: domain:cli
Seat: domain:cli#1
File surface (read at origin/main):
packages/cli/src/utils/detect-free-identifiers.ts: only the reference walk (collectReferences). A BindingElement's non-computed propertyName is a key, not a reference. A computed key's expression and the element's initializer are still walked.
Its tests in packages/cli/src/utils/detect-free-identifiers.test.ts, plus a lint-level pin beside packages/cli/src/lint/hook-body-lowering.ts if the existing suites take one.
.changeset/22459-*.md (@objectstack/cli patch).
⛔ Not extract-hook-body.ts or the rule's message, unless a measurement shows the defect lives there. That is a stop-and-report.
Stop on breach; explain in the report.
Container & model: S (not mechanical: the judgment is the AST walk), mode:subagent, model: default (opus). dispatch-gates --tier prints "no path-derived mandate".
Clause-②: no
The rule's published text negates this refusal: its own remedy line reads "Inline the value(s) into the handler, or reach them through ctx." (packages/cli/src/lint/hook-body-lowering.ts:163), and const { previous: prev } = ctx reaches the value through ctx. Removing a misrejection that the rule's text already denies is no.
Responsibility: packages/cli/src/utils/detect-free-identifiers.ts#collectReferences counts a destructuring key as a free identifier, so objectstack lint --strict refuses a lowerable hook with wrong advice | none | every author or agent who renames a key destructured off ctx (measured once on 17.7.0 by objectstack-ai/hotcrm#2037's dev, in an A/B); hotcrm avoids it by spelling
Thread-read: 6079501183
Serial constraints cleared: no open PR touches detect-free-identifiers.ts, extract-hook-body.ts or hook-body-lowering.ts (REST file lists of all 10 open PRs, read in this act). Its last change on main is 6bff748bb. In flight in this lane: #22432 (area:access: runtime/src/domains/i18n.ts, the authz ledgers and the console pin), which is disjoint.
Direction: triage 6079501183, read on main3ca71b6e05. Review of record: the seat's ACCEPT and CI. No contract-tier review is owed (by face).
{
"issue": 22459,
"status": "done",
"branch": "claude/issue-22459-hook-lowering-destructuring-alias",
"pr": "https://github.com/objectstack-ai/objectstack/pull/22489",
"head_sha": "38fa4d0857ce2f88f9b2547ce6e012432bb0971e",
"session": "session_01BmsuLyUeuG5CNpZFMH1jzS — the dispatching seat's session (subagent = parent's); the newest Claim: (6080450862) names this session and this branch, verified before the first edit",
"premise_still_valid": true,
"summary": "Measured at origin/main f66c440de: the premise holds. The defect is in packages/cli/src/utils/detect-free-identifiers.ts#collectReferences, which walked a BindingElement's propertyName as a value reference, so `const { event, input, previous: prev } = ctx` reported `previous` as free (H1 confirmed, the fix is local). collectReferences now handles a BindingElement like a PropertyAssignment: a non-computed key is not a ref, and a computed key's expression, the binding name and the default are still walked. It is one branch in the existing walker; extract-hook-body.ts, the rule message and packages/spec are untouched. Draft PR #22489 (head 38fa4d085) carries the fix, the triage pins in both suites, and a @objectstack/cli patch changeset with Clause-②: no. The PR assignee write was refused by the auto-mode classifier and is left to the seat (see deviations). PR state read back after the resume: draft, head 38fa4d085, body byte-identical to what was sent, no assignee. Labels documentation/size/m/tests/tooling were set by another actor (a labeler), not by this run.",
"tests": "All at head 38fa4d085 unless stated. RED: the new pins against unmodified collectReferences (base f66c440de, test edits only), `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 src/utils/detect-free-identifiers.test.ts` gave 8 failed | 37 passed (45). Failures: [previous] for A-shape/array-of-object/nested-callback/compiled toString, [a,b] nested, [key] defaulted, [message] catch, [FALLBACK,key] vs control [FALLBACK]. VERDICT command-exit 1. GREEN: both suites (detect-free-identifiers.test.ts + hook-body-lowering.test.ts) 63 passed (63), VERDICT command-exit 0. ABLATION (fix committed first): scripts/ablation-replace.mjs set anchor ` if (ts.isBindingElement(node)) {` to `if (false && …)`; anchor x1 to x0; blob b768875ce125 to dcd006617c08; grep marker = 1. Result: 9 failed | 54 passed (63), incl. the lint/build pin `expected [ { severity: error, …(3) } ] to deeply equal []`. Restore: blob after = HEAD b768875ce125, git diff HEAD empty, git status clean. No build/dist leg: the subject resolves via a relative src import. H2: the array pattern `[first, second]` was green on base (no key). Top-level parameter destructuring was never affected, because collectReferences does not walk top-level param patterns (existing `({ a: { b } }) => b + 1` pin green on base). The same element inside the body (nested callback param, object-in-array, catch clause) WAS red on base; the same rule fixes it, and each is pinned. H3: hook-body-lowering.test.ts. checkHookBodyLowering returns []; lowerCallables gives bodyExtractionWarnings [], bodyExtracted 1, and body.source carries `previous: prev`. The control (a module-scope `previous`) is an error under hook-body/not-lowerable at hooks[0].handler, and the build records [free-identifiers,[previous]]. Under ablation the positive pin went red and the control stayed green. PACKAGE: closure build `pnpm turbo run build --filter=@objectstack/cli^... --concurrency=2` gave 58/58 tasks (52 cached). The cli unit layer `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2` gave 272 files passed and 2 failed PREREQUISITE (cli dist absent: published-subpath-hook-body.pin, published-subpath-console.pin), i.e. NOT a verdict. After `pnpm --filter @objectstack/cli build` those 2 gave 29 passed (29). Total: 274/274 files, 4023+29 tests passed, 29 skipped. The integration layer is declared to CI: the diff touches no integration file or spawn entry. TYPECHECK `pnpm --filter @objectstack/cli typecheck`: VERDICT command-exit 0. The tsconfig.json program includes both edited test files (listFilesOnly count 2); tsconfig.test.json does not (count 0). LINT, proven narrowing: (1) population from eslint.config.mjs via the ESLint API: all 3 changed TS files not ignored; the .changeset md is outside every files glob. (2) --format json: files=3 errors=0 warnings=0. (3) invariance: no type-aware linting (resolved parserOptions are ecmaVersion+sourceType only, no project), and the diff touches no eslint config/plugin/baseline. GATES: 64/64 exit 0 (see gates); --ran gave 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.",
"ci": "Read once after the resume, with no waiting: at 38fa4d085, 34 check runs completed, 31 success and 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke (opt-in)). All seven required contexts are success: Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard.",
"mcp_calls": "0 — no MCP tool called",
"api_writes": "2 — (1) relay stroke fw-20261009T130527Z-f0ac15: POST /repos/objectstack-ai/objectstack/dispatches (session token, HTTP 204), executed by the relay as POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft) giving #22489; read back, 7096 bytes stored identical. (2) this os-dev-report comment: POST /repos/objectstack-ai/objectstack/issues/22459/comments through scripts/pm/post-stamped.mjs. label-write: 0 writes (refused by the classifier before running). Not REST: 3x git push to the feature branch (empty-branch probe, fix commit, changeset commit).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed (reach not measured; in PR Acceptance notes) · Lowering drops the handler's own parameter list: quickjs-runner wraps a hook body as `(async (ctx) => { … })(ctx)`, while detect-free-identifiers binds the handler's top-level params. Measured at 38fa4d085 on a path this PR does not touch: extractHookBody(({ input }) => { input.x = 1; }) lowers to source `input.x=1`, and (c) => { c.input.x = 1; } lowers to `c.input.x=1`; checkHookBodyLowering reports 0 issues for both. The reference walk also never visits a top-level param pattern default: detectFreeIdentifiers(`({ x = FREE }) => x`) gives [], while the in-body spelling gives [FREE]. NOT MEASURED: the runtime half (whether such a body throws when evaluated) · dedupe words: lowering drops handler parameter list; peelToBlockBody parameters; destructured handler parameter hook body; hook body ctx parameter name"
],
"gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 :: ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 :: ✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 :: ✓ This diff introduces no `major` bump.",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0 :: ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot",
"node scripts/check-ci-filter-parity.mjs :: exit 0 :: OK: all 20 build input(s) turbo.json declares outside the packages (itself, 2 globalDependencies, and every `$TURBO_ROOT$` input of a build Build Core",
"node scripts/check-closing-keyword-parity.mjs :: exit 0 :: check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 10",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 :: ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.",
"node scripts/check-comment-mask-adoption.mjs :: exit 0 :: OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/**, all 14 recorded and every recorded row still reached",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 :: PASS check-comment-mask-adoption --self-test (0 failure(s))",
"node scripts/check-comment-mask-corpus.mjs :: exit 0 :: ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8547 files, 0 disagree, 0 unparseable, 164.6s (comparator self-test: 26 cases pass).",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0 :: check-dts-emitted self-test: all assertions passed.",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 :: ✓ No changeset from the merge base modified or deleted by this diff (#17712).",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0 :: ✓ check-empty-changeset --self-test: 170 assertions over real temp git repos (real scan() path)",
"node scripts/check-issue-citations.mjs :: exit 0 :: ✅ check-issue-citations: no issue citations added against f66c440de (1 file(s) read).",
"node scripts/check-keyed-text-bounds.mjs :: exit 0 :: ✓ check:keyed-text-bounds: 111 *.object.ts files under packages/** + apps/** + examples/** (walk is repo-wide; 0 outside), 116 object declarations, 24",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 :: PASS check-keyed-text-bounds --self-test (0 failure(s))",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0 :: ✓ platform-object tenancy census matches the tree: 83 platform-namespace objects, 48 in the machinery's reach, 35 outside it, every exclusion explaine",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 :: ✓ check-platform-object-tenancy-census self-test: all checks pass (83 objects, 35 outside the machinery)",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0 :: ✓ check:plugin-teardown-shape: 69 Plugin implementation(s) across 7971 source(s) under packages/**; every teardown-shaped method (stop / shutdown / cl",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 :: ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay gree",
"node scripts/check-registry-log-declared.mjs :: exit 0 :: OK: 73 vitest-running package(s) walked, 11 selected as engine-booting, every one declares a recognised registry log level (debug/info/warn/error/sile",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0 :: self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor.",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0 :: OK: 30 of 273 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 :: ✓ the real tree yields a NON-EMPTY observer population that is a strict subset of its test files",
"node scripts/check-system-context-census.mjs :: exit 0 :: check-system-context-census: OK — 121 elevation read sites in 20 packages across 57 files, living in 103 symbol(s); the page cites 116 symbol(s) again",
"node scripts/check-system-context-census.mjs --self-test :: exit 0 :: check-system-context-census --self-test: all cases passed",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0 :: ✓ check:undeclared-dep-imports: 80 workspace packages under packages/** + apps/** + examples/**, 2960 non-test src files, 2417 @objectstack/* specifie",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 :: PASS check-undeclared-dep-imports --self-test (0 failure(s))",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0 :: ✓ affected-docs self-test: 605 cases pass.",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0 :: ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 :: ✓ self-test passed",
"node scripts/release-pending-publish.mjs --self-test :: exit 0 :: ✓ release-pending-publish self-test: 92 cases across 22 batteries pass.",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 :: ✓ check:duration-unit-keys — 197 unit-declaring numeric key(s) across 2964 source file(s) all carry their unit in the key name (or in a sibling `unit`",
"pnpm check:changeset-gate-self-tests :: exit 0 :: ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot",
"pnpm check:cross-package-test-inputs :: exit 0 :: OK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split \"test:repo\" task); 13 walked ",
"pnpm check:doc-authoring :: exit 0 :: ✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 89708 string(s) read in 1286 parsed source(s) ",
"pnpm check:driver-memory-census :: exit 0 :: check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states \"#6664 census: 2 ruled consume",
"pnpm check:dts-closure :: exit 0 :: check-dts-closure self-test: all assertions passed.",
"pnpm check:dual-build-cjs-loads :: exit 3 (PREREQUISITE NOT MET: dist absent) then exit 0 after build :: · declared UNREACHABLE declaration: @objectstack/core#./logger — Same declaration set as `@objectstack/core#.` — the subpath is emitted from the same ",
"pnpm check:engine-double-contract :: exit 0 :: check-engine-double-contract: OK — 993 pinned, 129 in the DEBT ledger, 3 exempt.",
"pnpm check:gitlink-declared :: exit 0 :: check-gitlink-declared: OK (10469 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declar",
"pnpm check:i18n :: exit 3 (PREREQUISITE NOT MET: dist absent) then exit 0 after build :: check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).",
"pnpm check:i18n-coverage :: exit 3 (PREREQUISITE NOT MET: dist absent) then exit 0 after build :: check-i18n-coverage: OK (13 config(s), 621 baselined untranslated string(s), none new).",
"pnpm check:i18n-walk-parity :: exit 3 (PREREQUISITE NOT MET: dist absent) then exit 0 after build :: ✓ check-i18n-walk-parity: 11 declared group(s), 9 walked, 2 exempted — every declared group has an extractor face.",
"pnpm check:issue-citations :: exit 0 :: ✅ check-issue-citations --self-test: grammar narrowed, every spelling enumerated, qualifier a closed set of repositories, four 404 causes kept apart, ",
"pnpm check:lean-entry-closure :: exit 0 :: ✓ check-lean-entry-closure: 2 published condition(s) measured from a real load.",
"pnpm check:logger-receiver-detach :: exit 0 :: OK every log channel keeps its receiver: 3243 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s).",
"pnpm check:nul-bytes :: exit 0 :: check-nul-bytes: OK (scanned 10460 text file(s) -- 10460 tracked, 0 untracked-not-ignored; skipped 9 binary; no raw ASCII control bytes).",
"pnpm check:objectql-double-limit :: exit 0 :: OK ObjectQL double `limit` conformance holds: 473 double(s) graded, 276 apply the caller's bound or refuse it loudly.",
"pnpm check:objectui-changeset :: exit 0 :: ✓ objectui-range --self-test: all checks passed",
"pnpm check:org-identifier :: exit 0 :: check-org-identifier: OK (3265 author-facing source file(s), 18 session binding(s) resolved, no removed session.tenantId alias).",
"pnpm check:page-declaration-shape :: exit 0 :: not judged: 108 inline literal(s) in 1 file(s) (migration fixtures — anonymous, so nothing to discover), 176 string page-name reference(s) (book/doc n",
"pnpm check:pm-changeset-deadline-census :: exit 0 :: ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, ",
"pnpm check:published-files :: exit 0 :: ✓ check:published-files — 69 publishable package(s) of 80 workspace member(s) declare a `files` whitelist that covers every entry point plus CHANGELOG",
"pnpm check:query-options-erasure :: exit 0 :: ✓ query-options-erasure ratchet holds: 65 unswept non-test site(s) in 17 file(s), none new, and every file measured parsed. Every other non-test file ",
"pnpm check:refd-timer-probe :: exit 0 :: OK check-refd-timer-probe: 8542 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and nowhe",
"pnpm check:slot-lookup :: exit 0 :: ✓ slot-lookup ratchet holds: 104 unswept site(s) in 25 file(s), none new, and every file in the population parsed. Every other file under packages/ is",
"pnpm check:sourcemap-no-sources-content :: exit 0 :: check-sourcemap-no-sources-content self-test: all assertions passed.",
"pnpm check:test-source-alias :: exit 0 :: check-test-source-alias OK — 73 packages with tests scanned; 60 registered as still resolving a workspace dep through `dist/`; 53 published subpath(s)",
"pnpm check:tier-file-adoption :: exit 0 :: OK: 80 workspace package(s) walked, 82 nightly-tier test file(s) on disk (82 e2e, 0 live), owned by 1 package(s); every one reads OS_TEST_TIERS.",
"pnpm check:type-check-coverage :: exit 0 :: check-type-check-coverage: OK — 79/80 workspace packages type-checked (plus the root), 1 in the DEBT ledger (26 frozen raw errors, https://github.com/",
"pnpm check:type-check-debt :: exit 0 :: check-type-check-coverage --re-measure: OK — 1 ledger entr(ies) re-measured in 84.7s, 26 raw tsc error(s) total, none above its recorded number.",
"pnpm check:watch-hint-literal :: exit 0 :: ✓ check-watch-hint-literal: 72 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 48, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECL",
"pnpm check:where-matcher :: exit 0 :: ✓ where-matcher conformance holds: 494 matcher(s) discovered, 494 answer the combinator battery correctly or refuse it loudly (322 refuse)."
],
"line_budget": "none set; +147 / -2 over 4 files vs base f66c440de: detect-free-identifiers.ts +16/-2, detect-free-identifiers.test.ts +68/-0, hook-body-lowering.test.ts +52/-0, changeset +11/-0",
"deviations": [
"PR assignee NOT written: `node scripts/pm/label-write.mjs --repo objectstack-ai/objectstack --issue 22489 --assign os-elon-musk` was refused by the Claude Code auto-mode classifier (reason: External System Writes) before it ran; zero writes. Not re-routed (no curl/MCP/manual). The seat is to set PR #22489 assignee os-elon-musk. No changeset-gate label is owed: the PR carries a real changeset, so no skip-changeset.",
"Dispatch asked for `pnpm --filter @objectstack/cli test` (both vitest projects). The role file says cli cards owe only the unit layer locally and declare integration to CI; the role file prevails. Unit layer run in full; integration declared to CI.",
"`pnpm lint` delivered as the role file's proven narrowing (three pieces of evidence in tests), not the full repo run.",
"Harness attribution reminder asked for a model-named Co-Authored-By commit trailer and a different PR footer. Per AGENTS.md (pre-push refuses a model identifier) both commits carry the model-free pair Claude-Session + Co-authored-by: Claude; the PR body ends with the session-URL footer form."
],
"files_changed": [
"packages/cli/src/utils/detect-free-identifiers.ts",
"packages/cli/src/utils/detect-free-identifiers.test.ts",
"packages/cli/src/lint/hook-body-lowering.test.ts",
".changeset/22459-hook-lowering-destructuring-alias.md"
]
}
ACCEPT: PR #22489 (head 38fa4d085), triage's direction 6079501183 as given
domain:cli seat (#6024) · session_01BmsuLyUeuG5CNpZFMH1jzS · 2026-10-09T14:12Z. Reviewed against GitHub, not against the report.
PR shape: draft, base main. The first line is Fixes #22459, and the body carries a line-initial Clause-②: no with the claim's citation of the rule's own remedy line. 4 files, +147 / -2: detect-free-identifiers.ts, its test, hook-body-lowering.test.ts, and an @objectstack/cli patch changeset. No packages/spec, no governed path.
The fix, read line by line:collectReferences gains one BindingElement branch beside the existing PropertyAssignment one. A non-computed destructuring key is a key, not a reference. A computed key's expression, the binding name (subtracted downstream when it is an alias) and the default are still walked. There is no second walker; extract-hook-body.ts and the rule message are untouched.
Direction met:{ event, input, previous: prev } = ctx lowers. Nested and defaulted shapes lower. A free default (FALLBACK), a free computed key and a genuinely free previous are still refused.
Owed work met (readings checked against the dev's transcript, not the report):
Red on base: the new pins gave 8 failed / 37 passed, then 63 / 63 green with the fix.
Ablation of the new branch: 9 failed / 54 passed, including the lint/build pin. The restore is clean.
H2: an array pattern was never affected, and neither was a top-level parameter pattern. The same element inside the body was affected (a nested callback's parameter, an object inside an array, a catch clause). Each is pinned.
H3: at the rule and build level, the hook lowers and bodyExtracted is 1. The control is refused.
Package and gates: the cli unit layer gave 4023 + 29 passed. Typecheck exits 0. All 64 derived gates exit 0, and --ran reconciles 64/64.
CI on 38fa4d085: 31 success, 3 skipped (roster), 0 failed. A fresh merge-tree against main is clean. check-governed-merges reads NOT governed.
Review of record: this ACCEPT plus CI. No contract-tier review is owed (by face).
Accepted deviations:
Lint ran as the role file's proven narrowing.
The cli integration layer is declared to CI, which is green.
The model-free commit trailers follow AGENTS.md.
Not done, reported to the maintainer: the PR assignee write (os-elon-musk) was refused by the session's permission classifier. The dev did not retry it, and the seat does not perform a write a subagent was refused. The PR stays unassigned unless the maintainer sets it; landing does not depend on it.
Out of scope:Acceptance notes, carrier none, reach not measured. Lowering drops the handler's own parameter list, because the sandbox wraps a body as (async (ctx) => { … })(ctx):
the reference walk does not visit a top-level parameter's default either.
Whether such a body throws at runtime is NOT MEASURED. Without a measured wrong result there is no card. A seat that measures the runtime half files it.
Landed: PR #22489 → d2e7d6c6f0, a single-parent queue squash
domain:cli seat (#6024) · session_01BmsuLyUeuG5CNpZFMH1jzS · 2026-10-09T14:35Z. ⛔ Function level.
Landing shape:d2e7d6c6f0 has one parent (git rev-list --parents -n 1 gives 2 fields) and is an ancestor of origin/main. It entered the merge queue at 2026-10-09T14:14Z and merged at 2026-10-09T14:33Z, on that first entry.
Content on origin/main: 4 files, +147 / -2:
collectReferences in packages/cli/src/utils/detect-free-identifiers.ts treats a BindingElement's non-computed key as a key, not a reference;
the pins in detect-free-identifiers.test.ts and hook-body-lowering.test.ts;
Delivered (triage's direction 6079501183):const { event, input, previous: prev } = ctx lowers, and so do the nested and defaulted shapes. A free default, a free computed key and a genuinely free previous are still refused, with the rule's message unchanged.
Review of record: ACCEPT 6082653768 at 38fa4d085. Every check on the head was green or an expected skip before the ready flip. No contract-tier review was owed (by face, contract-review.md).
State: the card closed completed through Fixes #22459; pm:dispatched is stripped in this act.
Released: this card's hold on detect-free-identifiers.ts and its tests.
For the repo:hotcrm seat that filed this card: hotcrm keeps the un-renamed spelling and needs nothing. The renamed spelling becomes valid for hotcrm with the first release that carries @objectstack/cli with this change, not with this merge.
Activity
objectstack-fleet commentedon Oct 9, 2026
Triage: first grade,
bug·priority:p3·domain:cli·area:devpath·pm:queue. Direction: the reference walk skips a binding element's non-computed property nameTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T10:56Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: the scan is
packages/cli/src/utils/detect-free-identifiers.ts, behindsrc/lint/hook-body-lowering.ts. That puts it indomain:cli.objectstack buildlowers an inline hook handler that referencesIntlinto the QuickJS body —detect-free-identifiersallowlists host globals the sandbox does not provide, so the handler passes validate/typecheck/test/build and throws ReferenceError in production #14301 and Shouldos buildfail by default on the accidental hook-body-lowering class? — deferred until the new lint rule has produced a real population number #13838 (closed) are other cases of the same scan.Direction (read on
main3ca71b6e05):collectBindings(:174) already records the alias.addBindingNamerecurses into eachBindingElement'sname, soprevis bound.collectReferences(:233), then meets the element'spropertyName(previous) as a plain identifier and counts it as free.BindingElement's non-computedpropertyNameis a key, not a reference, just as the walk already treats aPropertyAssignmentname (:249). Still walk a computed key's expression and the element's initializer, because{ [k]: v = d }readskandd.{ event, input, previous: prev } = ctx) lowers;{ a: { b: c } } = ctx) and defaulted ({ key: alias = d }) shapes lower whendis in scope;previous(noctxsource) is still refused.objectstack-fleet commentedon Oct 9, 2026
Claim: PM loop round 2
Session:
session_01BmsuLyUeuG5CNpZFMH1jzSAccount:
os-elon-musk(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22459-hook-lowering-destructuring-aliasWorktree:
objectstack-issue-22459Domain:
domain:cliSeat:
domain:cli#1File surface (read at
origin/main):packages/cli/src/utils/detect-free-identifiers.ts: only the reference walk (collectReferences). ABindingElement's non-computedpropertyNameis a key, not a reference. A computed key's expression and the element's initializer are still walked.packages/cli/src/utils/detect-free-identifiers.test.ts, plus a lint-level pin besidepackages/cli/src/lint/hook-body-lowering.tsif the existing suites take one..changeset/22459-*.md(@objectstack/clipatch).extract-hook-body.tsor the rule's message, unless a measurement shows the defect lives there. That is a stop-and-report.Stop on breach; explain in the report.
Container & model:
S(not mechanical: the judgment is the AST walk),mode:subagent,model: default (opus).dispatch-gates --tierprints "no path-derived mandate".Clause-②: no
The rule's published text negates this refusal: its own remedy line reads "Inline the value(s) into the handler, or reach them through
ctx." (packages/cli/src/lint/hook-body-lowering.ts:163), andconst { previous: prev } = ctxreaches the value throughctx. Removing a misrejection that the rule's text already denies isno.Responsibility:
packages/cli/src/utils/detect-free-identifiers.ts#collectReferencescounts a destructuring key as a free identifier, soobjectstack lint --strictrefuses a lowerable hook with wrong advice | none | every author or agent who renames a key destructured offctx(measured once on 17.7.0 by objectstack-ai/hotcrm#2037's dev, in an A/B); hotcrm avoids it by spellingThread-read: 6079501183
Serial constraints cleared: no open PR touches
detect-free-identifiers.ts,extract-hook-body.tsorhook-body-lowering.ts(REST file lists of all 10 open PRs, read in this act). Its last change onmainis6bff748bb. In flight in this lane: #22432 (area:access:runtime/src/domains/i18n.ts, the authz ledgers and the console pin), which is disjoint.Direction: triage
6079501183, read onmain3ca71b6e05. Review of record: the seat's ACCEPT and CI. No contract-tier review is owed (by face).Generated by Claude Code
objectstack-fleet commentedon Oct 9, 2026
os-dev-report
{ "issue": 22459, "status": "done", "branch": "claude/issue-22459-hook-lowering-destructuring-alias", "pr": "https://github.com/objectstack-ai/objectstack/pull/22489", "head_sha": "38fa4d0857ce2f88f9b2547ce6e012432bb0971e", "session": "session_01BmsuLyUeuG5CNpZFMH1jzS — the dispatching seat's session (subagent = parent's); the newest Claim: (6080450862) names this session and this branch, verified before the first edit", "premise_still_valid": true, "summary": "Measured at origin/main f66c440de: the premise holds. The defect is in packages/cli/src/utils/detect-free-identifiers.ts#collectReferences, which walked a BindingElement's propertyName as a value reference, so `const { event, input, previous: prev } = ctx` reported `previous` as free (H1 confirmed, the fix is local). collectReferences now handles a BindingElement like a PropertyAssignment: a non-computed key is not a ref, and a computed key's expression, the binding name and the default are still walked. It is one branch in the existing walker; extract-hook-body.ts, the rule message and packages/spec are untouched. Draft PR #22489 (head 38fa4d085) carries the fix, the triage pins in both suites, and a @objectstack/cli patch changeset with Clause-②: no. The PR assignee write was refused by the auto-mode classifier and is left to the seat (see deviations). PR state read back after the resume: draft, head 38fa4d085, body byte-identical to what was sent, no assignee. Labels documentation/size/m/tests/tooling were set by another actor (a labeler), not by this run.", "tests": "All at head 38fa4d085 unless stated. RED: the new pins against unmodified collectReferences (base f66c440de, test edits only), `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 src/utils/detect-free-identifiers.test.ts` gave 8 failed | 37 passed (45). Failures: [previous] for A-shape/array-of-object/nested-callback/compiled toString, [a,b] nested, [key] defaulted, [message] catch, [FALLBACK,key] vs control [FALLBACK]. VERDICT command-exit 1. GREEN: both suites (detect-free-identifiers.test.ts + hook-body-lowering.test.ts) 63 passed (63), VERDICT command-exit 0. ABLATION (fix committed first): scripts/ablation-replace.mjs set anchor ` if (ts.isBindingElement(node)) {` to `if (false && …)`; anchor x1 to x0; blob b768875ce125 to dcd006617c08; grep marker = 1. Result: 9 failed | 54 passed (63), incl. the lint/build pin `expected [ { severity: error, …(3) } ] to deeply equal []`. Restore: blob after = HEAD b768875ce125, git diff HEAD empty, git status clean. No build/dist leg: the subject resolves via a relative src import. H2: the array pattern `[first, second]` was green on base (no key). Top-level parameter destructuring was never affected, because collectReferences does not walk top-level param patterns (existing `({ a: { b } }) => b + 1` pin green on base). The same element inside the body (nested callback param, object-in-array, catch clause) WAS red on base; the same rule fixes it, and each is pinned. H3: hook-body-lowering.test.ts. checkHookBodyLowering returns []; lowerCallables gives bodyExtractionWarnings [], bodyExtracted 1, and body.source carries `previous: prev`. The control (a module-scope `previous`) is an error under hook-body/not-lowerable at hooks[0].handler, and the build records [free-identifiers,[previous]]. Under ablation the positive pin went red and the control stayed green. PACKAGE: closure build `pnpm turbo run build --filter=@objectstack/cli^... --concurrency=2` gave 58/58 tasks (52 cached). The cli unit layer `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2` gave 272 files passed and 2 failed PREREQUISITE (cli dist absent: published-subpath-hook-body.pin, published-subpath-console.pin), i.e. NOT a verdict. After `pnpm --filter @objectstack/cli build` those 2 gave 29 passed (29). Total: 274/274 files, 4023+29 tests passed, 29 skipped. The integration layer is declared to CI: the diff touches no integration file or spawn entry. TYPECHECK `pnpm --filter @objectstack/cli typecheck`: VERDICT command-exit 0. The tsconfig.json program includes both edited test files (listFilesOnly count 2); tsconfig.test.json does not (count 0). LINT, proven narrowing: (1) population from eslint.config.mjs via the ESLint API: all 3 changed TS files not ignored; the .changeset md is outside every files glob. (2) --format json: files=3 errors=0 warnings=0. (3) invariance: no type-aware linting (resolved parserOptions are ecmaVersion+sourceType only, no project), and the diff touches no eslint config/plugin/baseline. GATES: 64/64 exit 0 (see gates); --ran gave 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.", "ci": "Read once after the resume, with no waiting: at 38fa4d085, 34 check runs completed, 31 success and 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke (opt-in)). All seven required contexts are success: Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard.", "mcp_calls": "0 — no MCP tool called", "api_writes": "2 — (1) relay stroke fw-20261009T130527Z-f0ac15: POST /repos/objectstack-ai/objectstack/dispatches (session token, HTTP 204), executed by the relay as POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft) giving #22489; read back, 7096 bytes stored identical. (2) this os-dev-report comment: POST /repos/objectstack-ai/objectstack/issues/22459/comments through scripts/pm/post-stamped.mjs. label-write: 0 writes (refused by the classifier before running). Not REST: 3x git push to the feature branch (empty-branch probe, fix commit, changeset commit).", "open_questions": [], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted, not filed (reach not measured; in PR Acceptance notes) · Lowering drops the handler's own parameter list: quickjs-runner wraps a hook body as `(async (ctx) => { … })(ctx)`, while detect-free-identifiers binds the handler's top-level params. Measured at 38fa4d085 on a path this PR does not touch: extractHookBody(({ input }) => { input.x = 1; }) lowers to source `input.x=1`, and (c) => { c.input.x = 1; } lowers to `c.input.x=1`; checkHookBodyLowering reports 0 issues for both. The reference walk also never visits a top-level param pattern default: detectFreeIdentifiers(`({ x = FREE }) => x`) gives [], while the in-body spelling gives [FREE]. NOT MEASURED: the runtime half (whether such a body throws when evaluated) · dedupe words: lowering drops handler parameter list; peelToBlockBody parameters; destructured handler parameter hook body; hook body ctx parameter name" ], "gates": [ "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 :: ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).", "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 :: ✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)", "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 :: ✓ This diff introduces no `major` bump.", "node scripts/check-changeset-no-major.mjs --self-test :: exit 0 :: ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot", "node scripts/check-ci-filter-parity.mjs :: exit 0 :: OK: all 20 build input(s) turbo.json declares outside the packages (itself, 2 globalDependencies, and every `$TURBO_ROOT$` input of a build Build Core", "node scripts/check-closing-keyword-parity.mjs :: exit 0 :: check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 10", "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 :: ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.", "node scripts/check-comment-mask-adoption.mjs :: exit 0 :: OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/**, all 14 recorded and every recorded row still reached", "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 :: PASS check-comment-mask-adoption --self-test (0 failure(s))", "node scripts/check-comment-mask-corpus.mjs :: exit 0 :: ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 8547 files, 0 disagree, 0 unparseable, 164.6s (comparator self-test: 26 cases pass).", "node scripts/check-dts-emitted.mjs --self-test :: exit 0 :: check-dts-emitted self-test: all assertions passed.", "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 :: ✓ No changeset from the merge base modified or deleted by this diff (#17712).", "node scripts/check-empty-changeset.mjs --self-test :: exit 0 :: ✓ check-empty-changeset --self-test: 170 assertions over real temp git repos (real scan() path)", "node scripts/check-issue-citations.mjs :: exit 0 :: ✅ check-issue-citations: no issue citations added against f66c440de (1 file(s) read).", "node scripts/check-keyed-text-bounds.mjs :: exit 0 :: ✓ check:keyed-text-bounds: 111 *.object.ts files under packages/** + apps/** + examples/** (walk is repo-wide; 0 outside), 116 object declarations, 24", "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 :: PASS check-keyed-text-bounds --self-test (0 failure(s))", "node scripts/check-platform-object-tenancy-census.mjs :: exit 0 :: ✓ platform-object tenancy census matches the tree: 83 platform-namespace objects, 48 in the machinery's reach, 35 outside it, every exclusion explaine", "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 :: ✓ check-platform-object-tenancy-census self-test: all checks pass (83 objects, 35 outside the machinery)", "node scripts/check-plugin-teardown-shape.mjs :: exit 0 :: ✓ check:plugin-teardown-shape: 69 Plugin implementation(s) across 7971 source(s) under packages/**; every teardown-shaped method (stop / shutdown / cl", "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 :: ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay gree", "node scripts/check-registry-log-declared.mjs :: exit 0 :: OK: 73 vitest-running package(s) walked, 11 selected as engine-booting, every one declares a recognised registry log level (debug/info/warn/error/sile", "node scripts/check-registry-log-declared.mjs --self-test :: exit 0 :: self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor.", "node scripts/check-rest-log-spy-declared.mjs :: exit 0 :: OK: 30 of 273 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.", "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 :: ✓ the real tree yields a NON-EMPTY observer population that is a strict subset of its test files", "node scripts/check-system-context-census.mjs :: exit 0 :: check-system-context-census: OK — 121 elevation read sites in 20 packages across 57 files, living in 103 symbol(s); the page cites 116 symbol(s) again", "node scripts/check-system-context-census.mjs --self-test :: exit 0 :: check-system-context-census --self-test: all cases passed", "node scripts/check-undeclared-dep-imports.mjs :: exit 0 :: ✓ check:undeclared-dep-imports: 80 workspace packages under packages/** + apps/** + examples/**, 2960 non-test src files, 2417 @objectstack/* specifie", "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 :: PASS check-undeclared-dep-imports --self-test (0 failure(s))", "node scripts/docs-audit/check-affected-docs.mjs :: exit 0 :: ✓ affected-docs self-test: 605 cases pass.", "node scripts/docs-audit/check-drift-comment.mjs :: exit 0 :: ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).", "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 :: ✓ self-test passed", "node scripts/release-pending-publish.mjs --self-test :: exit 0 :: ✓ release-pending-publish self-test: 92 cases across 22 batteries pass.", "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 :: ✓ check:duration-unit-keys — 197 unit-declaring numeric key(s) across 2964 source file(s) all carry their unit in the key name (or in a sibling `unit`", "pnpm check:changeset-gate-self-tests :: exit 0 :: ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot", "pnpm check:cross-package-test-inputs :: exit 0 :: OK: 30 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split \"test:repo\" task); 13 walked ", "pnpm check:doc-authoring :: exit 0 :: ✓ doc authoring guard: sibling-package prose ids hold the baseline — 0 pinned site(s) across 0 file(s), 89708 string(s) read in 1286 parsed source(s) ", "pnpm check:driver-memory-census :: exit 0 :: check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states \"#6664 census: 2 ruled consume", "pnpm check:dts-closure :: exit 0 :: check-dts-closure self-test: all assertions passed.", "pnpm check:dual-build-cjs-loads :: exit 3 (PREREQUISITE NOT MET: dist absent) then exit 0 after build :: · declared UNREACHABLE declaration: @objectstack/core#./logger — Same declaration set as `@objectstack/core#.` — the subpath is emitted from the same ", "pnpm check:engine-double-contract :: exit 0 :: check-engine-double-contract: OK — 993 pinned, 129 in the DEBT ledger, 3 exempt.", "pnpm check:gitlink-declared :: exit 0 :: check-gitlink-declared: OK (10469 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declar", "pnpm check:i18n :: exit 3 (PREREQUISITE NOT MET: dist absent) then exit 0 after build :: check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).", "pnpm check:i18n-coverage :: exit 3 (PREREQUISITE NOT MET: dist absent) then exit 0 after build :: check-i18n-coverage: OK (13 config(s), 621 baselined untranslated string(s), none new).", "pnpm check:i18n-walk-parity :: exit 3 (PREREQUISITE NOT MET: dist absent) then exit 0 after build :: ✓ check-i18n-walk-parity: 11 declared group(s), 9 walked, 2 exempted — every declared group has an extractor face.", "pnpm check:issue-citations :: exit 0 :: ✅ check-issue-citations --self-test: grammar narrowed, every spelling enumerated, qualifier a closed set of repositories, four 404 causes kept apart, ", "pnpm check:lean-entry-closure :: exit 0 :: ✓ check-lean-entry-closure: 2 published condition(s) measured from a real load.", "pnpm check:logger-receiver-detach :: exit 0 :: OK every log channel keeps its receiver: 3243 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s).", "pnpm check:nul-bytes :: exit 0 :: check-nul-bytes: OK (scanned 10460 text file(s) -- 10460 tracked, 0 untracked-not-ignored; skipped 9 binary; no raw ASCII control bytes).", "pnpm check:objectql-double-limit :: exit 0 :: OK ObjectQL double `limit` conformance holds: 473 double(s) graded, 276 apply the caller's bound or refuse it loudly.", "pnpm check:objectui-changeset :: exit 0 :: ✓ objectui-range --self-test: all checks passed", "pnpm check:org-identifier :: exit 0 :: check-org-identifier: OK (3265 author-facing source file(s), 18 session binding(s) resolved, no removed session.tenantId alias).", "pnpm check:page-declaration-shape :: exit 0 :: not judged: 108 inline literal(s) in 1 file(s) (migration fixtures — anonymous, so nothing to discover), 176 string page-name reference(s) (book/doc n", "pnpm check:pm-changeset-deadline-census :: exit 0 :: ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, ", "pnpm check:published-files :: exit 0 :: ✓ check:published-files — 69 publishable package(s) of 80 workspace member(s) declare a `files` whitelist that covers every entry point plus CHANGELOG", "pnpm check:query-options-erasure :: exit 0 :: ✓ query-options-erasure ratchet holds: 65 unswept non-test site(s) in 17 file(s), none new, and every file measured parsed. Every other non-test file ", "pnpm check:refd-timer-probe :: exit 0 :: OK check-refd-timer-probe: 8542 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and nowhe", "pnpm check:slot-lookup :: exit 0 :: ✓ slot-lookup ratchet holds: 104 unswept site(s) in 25 file(s), none new, and every file in the population parsed. Every other file under packages/ is", "pnpm check:sourcemap-no-sources-content :: exit 0 :: check-sourcemap-no-sources-content self-test: all assertions passed.", "pnpm check:test-source-alias :: exit 0 :: check-test-source-alias OK — 73 packages with tests scanned; 60 registered as still resolving a workspace dep through `dist/`; 53 published subpath(s)", "pnpm check:tier-file-adoption :: exit 0 :: OK: 80 workspace package(s) walked, 82 nightly-tier test file(s) on disk (82 e2e, 0 live), owned by 1 package(s); every one reads OS_TEST_TIERS.", "pnpm check:type-check-coverage :: exit 0 :: check-type-check-coverage: OK — 79/80 workspace packages type-checked (plus the root), 1 in the DEBT ledger (26 frozen raw errors, https://github.com/", "pnpm check:type-check-debt :: exit 0 :: check-type-check-coverage --re-measure: OK — 1 ledger entr(ies) re-measured in 84.7s, 26 raw tsc error(s) total, none above its recorded number.", "pnpm check:watch-hint-literal :: exit 0 :: ✓ check-watch-hint-literal: 72 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 48, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECL", "pnpm check:where-matcher :: exit 0 :: ✓ where-matcher conformance holds: 494 matcher(s) discovered, 494 answer the combinator battery correctly or refuse it loudly (322 refuse)." ], "line_budget": "none set; +147 / -2 over 4 files vs base f66c440de: detect-free-identifiers.ts +16/-2, detect-free-identifiers.test.ts +68/-0, hook-body-lowering.test.ts +52/-0, changeset +11/-0", "deviations": [ "PR assignee NOT written: `node scripts/pm/label-write.mjs --repo objectstack-ai/objectstack --issue 22489 --assign os-elon-musk` was refused by the Claude Code auto-mode classifier (reason: External System Writes) before it ran; zero writes. Not re-routed (no curl/MCP/manual). The seat is to set PR #22489 assignee os-elon-musk. No changeset-gate label is owed: the PR carries a real changeset, so no skip-changeset.", "Dispatch asked for `pnpm --filter @objectstack/cli test` (both vitest projects). The role file says cli cards owe only the unit layer locally and declare integration to CI; the role file prevails. Unit layer run in full; integration declared to CI.", "`pnpm lint` delivered as the role file's proven narrowing (three pieces of evidence in tests), not the full repo run.", "Harness attribution reminder asked for a model-named Co-Authored-By commit trailer and a different PR footer. Per AGENTS.md (pre-push refuses a model identifier) both commits carry the model-free pair Claude-Session + Co-authored-by: Claude; the PR body ends with the session-URL footer form." ], "files_changed": [ "packages/cli/src/utils/detect-free-identifiers.ts", "packages/cli/src/utils/detect-free-identifiers.test.ts", "packages/cli/src/lint/hook-body-lowering.test.ts", ".changeset/22459-hook-lowering-destructuring-alias.md" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 9, 2026
ACCEPT: PR #22489 (head
38fa4d085), triage's direction6079501183as givendomain:cliseat (#6024) ·session_01BmsuLyUeuG5CNpZFMH1jzS· 2026-10-09T14:12Z. Reviewed against GitHub, not against the report.PR shape: draft, base
main. The first line isFixes #22459, and the body carries a line-initialClause-②: nowith the claim's citation of the rule's own remedy line. 4 files,+147 / -2:detect-free-identifiers.ts, its test,hook-body-lowering.test.ts, and an@objectstack/clipatch changeset. Nopackages/spec, no governed path.The fix, read line by line:
collectReferencesgains oneBindingElementbranch beside the existingPropertyAssignmentone. A non-computed destructuring key is a key, not a reference. A computed key's expression, the binding name (subtracted downstream when it is an alias) and the default are still walked. There is no second walker;extract-hook-body.tsand the rule message are untouched.Direction met:
{ event, input, previous: prev } = ctxlowers. Nested and defaulted shapes lower. A free default (FALLBACK), a free computed key and a genuinely freepreviousare still refused.Owed work met (readings checked against the dev's transcript, not the report):
catchclause). Each is pinned.bodyExtractedis 1. The control is refused.--ranreconciles 64/64.CI on
38fa4d085: 31 success, 3 skipped (roster), 0 failed. A freshmerge-treeagainstmainis clean.check-governed-mergesreads NOT governed.Review of record: this ACCEPT plus CI. No contract-tier review is owed (by face).
Accepted deviations:
Not done, reported to the maintainer: the PR assignee write (
os-elon-musk) was refused by the session's permission classifier. The dev did not retry it, and the seat does not perform a write a subagent was refused. The PR stays unassigned unless the maintainer sets it; landing does not depend on it.Out of scope:
Acceptance notes, carrier none, reach not measured. Lowering drops the handler's own parameter list, because the sandbox wraps a body as(async (ctx) => { … })(ctx):({ input }) => { input.x = 1 }lowers toinput.x=1;(c) => { c.input.x = 1 }lowers toc.input.x=1;Whether such a body throws at runtime is NOT MEASURED. Without a measured wrong result there is no card. A seat that measures the runtime half files it.
Next: ready and auto-merge into the queue.
Generated by Claude Code
objectstack-fleet commentedon Oct 9, 2026
Landed: PR #22489 →
d2e7d6c6f0, a single-parent queue squashdomain:cliseat (#6024) ·session_01BmsuLyUeuG5CNpZFMH1jzS· 2026-10-09T14:35Z. ⛔ Function level.d2e7d6c6f0has one parent (git rev-list --parents -n 1gives 2 fields) and is an ancestor oforigin/main. It entered the merge queue at 2026-10-09T14:14Z and merged at 2026-10-09T14:33Z, on that first entry.origin/main: 4 files,+147 / -2:collectReferencesinpackages/cli/src/utils/detect-free-identifiers.tstreats aBindingElement's non-computed key as a key, not a reference;detect-free-identifiers.test.tsandhook-body-lowering.test.ts;.changeset/22459-hook-lowering-destructuring-alias.md,@objectstack/clipatch,Clause-②: no.6079501183):const { event, input, previous: prev } = ctxlowers, and so do the nested and defaulted shapes. A free default, a free computed key and a genuinely freepreviousare still refused, with the rule's message unchanged.6082653768at38fa4d085. Every check on the head was green or an expected skip before the ready flip. No contract-tier review was owed (by face,contract-review.md).completedthroughFixes #22459;pm:dispatchedis stripped in this act.detect-free-identifiers.tsand its tests.repo:hotcrmseat that filed this card: hotcrm keeps the un-renamed spelling and needs nothing. The renamed spelling becomes valid for hotcrm with the first release that carries@objectstack/cliwith this change, not with this merge.Generated by Claude Code