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
examples/app-crm + app-showcase: every authored hook reads the record off ctx.input instead of ctx.input.data, so each one silently does nothing at runtime #7225
Found while implementing #7036 (examples/app-todo's completion stamp). Filed standalone and unassigned: #7036 repairs only app-todo's own hook, and the same mistake is live in the other two example apps under a different owner.
The defect
HookContext.input is an envelope, never the record. The contract table on HookContextSchema.input (packages/spec/src/data/hook.zod.ts), pinned against the real engine in packages/objectql/src/hook-input-shape-contract.test.ts, says:
So the record is at ctx.input.data. Assigning to ctx.input.<field> sets a key on the envelope that no write path reads — the engine dispatches the driver write from hookContext.input.data.
Three shipped hooks do exactly that, and all three are registered (hooks: allHooks), so they run and have no effect:
Same file — NormalizeTaskTitleHook: if (ctx.input.title) ctx.input.title = ctx.input.title.trim(); — never trims.
Script bodies get the same envelope — no flattening anywhere
Worth stating because 2 and 3 are sandboxed body hooks rather than code handlers, so one might expect the runner to hand them the record. It does not. buildSandboxContext (packages/runtime/src/sandbox/body-runner.ts) sets input: unwrapProxyToPlain(engineCtx?.input) ?? {} — a snapshot of the envelope, unchanged. Its own comment is explicit that input is "the engine's own spelling, and the only one", and #5906 removed the alias limbs precisely so there would not be a second de-facto contract.
The after* hooks in that file (AuditTaskCompletionHook, WarnOverBudgetHook) read ctx.result || ctx.input and are not obviously affected — ctx.result is bound on after events. They are worth a look but are not the claim here.
Why it matters more than the three behaviours
These are reference-corpus apps. An AI author asked to write a hook copies the shape it sees, and every shape it can see in this repo's examples is the broken one. #7036 is the same mistake in app-todo, where it had been invisible because that hook was never registered at all.
Not verified
I did not run either app — the reading is from the contract table, the engine's dispatch site, and buildSandboxContext. A one-line probe per app (assert the persisted row) would confirm before fixing.
Classification: concrete defect, three named landing sites, all re-verified live on origin/main @ f40c5b4: examples/app-crm/src/hooks/opportunity.hook.ts:15-17 (reads the record off ctx.input), examples/app-showcase/src/data/hooks/index.ts:34 (ctx.input.title) and :108 (ctx.input.status / ctx.input.source). OpportunityStageHook is registered via allHooks (examples/app-crm/src/hooks/index.ts:5), so the hooks run and silently no-op exactly as claimed.
Claim: PM loop round 2 (domain:services seat #6021)
Session: session_015fkdTyGmMD5s8ZtEifvuGy
Branch: claude/issue-7225-example-hooks-envelope
Worktree: objectstack-issue-7225
Domain: domain:services
File surface: examples/app-crm/src/hooks/opportunity.hook.ts, examples/app-showcase/src/data/hooks/index.ts, plus one new probe/regression test per app. ⛔ NOT examples/app-todo/** (its hook repair landed via #7036 / PR #7222, still converging) and NOT packages/runtime/** (buildSandboxContext is domain:cli territory — if measurement says the fix belongs there, stop and report). (Stop on breach; explain in the report.)
Container & model: M, mode:subagent, model: opus
Serial constraints cleared: no in-flight claim touches app-crm or app-showcase. Same-corpus siblings: PR #7222 (app-todo, distinct package) in patch round; PRs #7224/#7227 in the merge queue (disjoint packages). Triage premise re-verified @ f40c5b4; per its dispatch note, a persisted-row probe per app is step 1 of the brief.
Premise falsified — all three hooks work today, and the prescribed fix would break two of them
Session: session_015fkdTyGmMD5s8ZtEifvuGy · branch claude/issue-7225-example-hooks-envelope (nothing pushed — no PR). Step 1 of the dispatch note was the persisted-row probe per app, and it answered the other way.
The card's own "Not verified" caveat was the right instinct. The reading was from the contract table, the engine dispatch site and buildSandboxContext — all three accurate, and all three describing the raw-engine surface rather than the declarative one the examples are written on.
What the missing link is
bindHooksToEngine wraps every declarative hook in wrapDeclarativeHook, which calls installFlatInput (packages/objectql/src/hook-wrappers.ts:446, helper at :502). It swaps ctx.input for a Proxy presenting a flat record view over the envelope — reads of a non-wrapper key resolve against data, and writes always land in data. Its own comment states the purpose: "so the engine's downstream input.data read picks up mutations made by user code as input.field = value".
For sandboxed bodies, buildSandboxContext's unwrapProxyToPlain(engineCtx.input) runs through that proxy's ownKeys trap, which enumerates only the flat record fields. So a body receives the record, and ctx.input.data is undefined there.
Measured, on the production wiring
Real ObjectQL + real SqlDriver (better-sqlite3) + real QuickJSScriptRunner, driving each app's real object and real hook, asserting the persisted row. Same shape AppPlugin boots with (ql.bindHooks(...) plus bodyRunner: hookBodyRunnerFactory(new QuickJSScriptRunner(), …), packages/runtime/src/app-plugin.ts:717-742).
The public web-to-lead insert goes from working to refusing every submission. NormalizeTaskTitleHook is the same shape (onError: 'abort'), so it would abort every task write. On the code-handler path both spellings work (the proxy passes data through), so app-crm would merely churn — but the showcase half is a live regression.
Re-scoped
The real defect is the contract table, not the corpus: it documents only the raw-engine envelope while addressing authors who only ever meet the declarative surface, on which the documented spelling is redundant at best and fatal in a body. Filed unassigned as #7254. This card is a false alarm and its three landing sites should be left exactly as they are.
Not probed, and unchanged from the card's own note: the after* hooks (AuditTaskCompletionHook, WarnOverBudgetHook) — stated as not the claim, and nothing here contradicts that.
Worth considering as a cheap follow-up rather than part of this card: the probes above are written and passing, and a pinned-down version of them would have made this report unnecessary. app-crm needs two devDeps (@objectstack/objectql, @objectstack/driver-sql) to host one.
Premise falsified — closing as not planned. Reviewed by the domain:services seat (#6021, session session_015fkdTyGmMD5s8ZtEifvuGy); the dev returned premise_still_valid: false with zero files changed, which is the successful outcome for this dispatch, not a failed one.
What the measurement showed (real kernel, real drivers, real QuickJS runner, production wiring via AppPlugin's ql.bindHooks + hookBodyRunnerFactory, persisted-row assertions):
All three named hooks work today. bindHooksToEngine wraps every declarative hook in wrapDeclarativeHook, which installs a flat record view over the envelope (installFlatInput, packages/objectql/src/hook-wrappers.ts:446, helper :502 — verified at those exact lines on origin/main by this seat): reads of a non-wrapper key resolve against data, writes land in data. CRM probe 6/6 (closed_won persists probability=100, control untouched); showcase probe 5/5 (status=new/source=web stamped, title trimmed).
The card's prescribed fix is a live regression on the sandboxed half: inside a body hook, unwrapProxyToPlain enumerates the proxy's flat keys, so ctx.input.data is undefined — the re-spelled StampInquiryDefaultsHook (which carries onError: 'abort') threw TypeError and persisted 0 rows: the public web-to-lead insert would go from working to refusing every submission.
This card's own filing caveat ("Not verified — a one-line probe per app would confirm before fixing") was the honest hinge, and the probe answered it.
The real defect is filed as #7254 (unassigned, awaiting triage): HookContextSchema.input's contract table documents only the raw-engine envelope while every author-facing declarative hook sees the flat view — the documentation gap that produced this false alarm, where the documented input.data spelling is fatal inside a body.
Follow-up (this seat's scope ruling on the dev's option B): the measurement probes will be landed as regression pins via a separate test-only card, so the corpus's working behaviour becomes a stated fact rather than something each reader re-derives and gets wrong — filed next, domain:* left to triage.
Note for the record: the dispatch prompt's quoted direction ("examples converge on ctx.input.data") presumed the card's facts and is spent with them — the dev correctly proposed no engine/sandbox change, and no re-adjudication is needed. Maintainer veto window: reopen if you read the evidence differently; it is all reproducible from the probes described above.
Activity
os-zhuang commentedon Aug 10, 2026
Triage:
pm:queue+domain:services.origin/main@f40c5b4:examples/app-crm/src/hooks/opportunity.hook.ts:15-17(reads the record offctx.input),examples/app-showcase/src/data/hooks/index.ts:34(ctx.input.title) and:108(ctx.input.status/ctx.input.source).OpportunityStageHookis registered viaallHooks(examples/app-crm/src/hooks/index.ts:5), so the hooks run and silently no-op exactly as claimed.completed_dateisreadonly(stripped on update) andcompleted_date_requiredthen refuses the write, so the app's owncompleteTaskaction always fails #7036 (domain:services) — same example corpus, this card was found while implementing it, and the services seat is already inside the app-todo hook face. Keeping the corpus in one lane avoids a cross-lane collision on adjacent files;examples/**routes with the engaged subsystem per the domain table's examples rule.completed_dateisreadonly(stripped on update) andcompleted_date_requiredthen refuses the write, so the app's owncompleteTaskaction always fails #7036 covers app-todo only and says so; no other open issue names these hooks. No objectui/cloud shadow (backend example code).target:v17: not applied, matching examples/app-todo: a normal user can never mark a task complete —completed_dateisreadonly(stripped on update) andcompleted_date_requiredthen refuses the write, so the app's owncompleteTaskaction always fails #7036's grading (example-corpus defect, not a published API face). Re-grade trigger: if the release positions these examples as the quick-start path, the "copy the example and it silently no-ops" shape reads as class ④.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-help commentedon Aug 10, 2026
Claim: PM loop round 2 (
domain:servicesseat #6021)Session:
session_015fkdTyGmMD5s8ZtEifvuGyBranch:
claude/issue-7225-example-hooks-envelopeWorktree:
objectstack-issue-7225Domain:
domain:servicesFile surface:
examples/app-crm/src/hooks/opportunity.hook.ts,examples/app-showcase/src/data/hooks/index.ts, plus one new probe/regression test per app. ⛔ NOTexamples/app-todo/**(its hook repair landed via #7036 / PR #7222, still converging) and NOTpackages/runtime/**(buildSandboxContextisdomain:cliterritory — if measurement says the fix belongs there, stop and report). (Stop on breach; explain in the report.)Container & model: M,
mode:subagent,model: opusSerial constraints cleared: no in-flight claim touches app-crm or app-showcase. Same-corpus siblings: PR #7222 (app-todo, distinct package) in patch round; PRs #7224/#7227 in the merge queue (disjoint packages). Triage premise re-verified @
f40c5b4; per its dispatch note, a persisted-row probe per app is step 1 of the brief.Generated by Claude Code
os-help commentedon Aug 10, 2026
Premise falsified — all three hooks work today, and the prescribed fix would break two of them
Session:
session_015fkdTyGmMD5s8ZtEifvuGy· branchclaude/issue-7225-example-hooks-envelope(nothing pushed — no PR). Step 1 of the dispatch note was the persisted-row probe per app, and it answered the other way.The card's own "Not verified" caveat was the right instinct. The reading was from the contract table, the engine dispatch site and
buildSandboxContext— all three accurate, and all three describing the raw-engine surface rather than the declarative one the examples are written on.What the missing link is
bindHooksToEnginewraps every declarative hook inwrapDeclarativeHook, which callsinstallFlatInput(packages/objectql/src/hook-wrappers.ts:446, helper at:502). It swapsctx.inputfor a Proxy presenting a flat record view over the envelope — reads of a non-wrapper key resolve againstdata, and writes always land indata. Its own comment states the purpose: "so the engine's downstreaminput.dataread picks up mutations made by user code asinput.field = value".For sandboxed bodies,
buildSandboxContext'sunwrapProxyToPlain(engineCtx.input)runs through that proxy'sownKeystrap, which enumerates only the flat record fields. So a body receives the record, andctx.input.dataisundefinedthere.Measured, on the production wiring
Real
ObjectQL+ realSqlDriver(better-sqlite3) + realQuickJSScriptRunner, driving each app's real object and real hook, asserting the persisted row. Same shapeAppPluginboots with (ql.bindHooks(...)plusbodyRunner: hookBodyRunnerFactory(new QuickJSScriptRunner(), …),packages/runtime/src/app-plugin.ts:717-742).app-crm
OpportunityStageHook— 6/6 green:input.stageis bound, notundefined. Probability is pinned.app-showcase body hooks — 5/5 green:
The inverted direction — why this is not merely "no change needed"
Applying the prescribed
ctx.input.dataspelling toStampInquiryDefaultsHookverbatim, keeping its ownonError: 'abort':The public web-to-lead insert goes from working to refusing every submission.
NormalizeTaskTitleHookis the same shape (onError: 'abort'), so it would abort every task write. On the code-handler path both spellings work (the proxy passesdatathrough), so app-crm would merely churn — but the showcase half is a live regression.Re-scoped
The real defect is the contract table, not the corpus: it documents only the raw-engine envelope while addressing authors who only ever meet the declarative surface, on which the documented spelling is redundant at best and fatal in a body. Filed unassigned as #7254. This card is a false alarm and its three landing sites should be left exactly as they are.
Not probed, and unchanged from the card's own note: the
after*hooks (AuditTaskCompletionHook,WarnOverBudgetHook) — stated as not the claim, and nothing here contradicts that.Worth considering as a cheap follow-up rather than part of this card: the probes above are written and passing, and a pinned-down version of them would have made this report unnecessary. app-crm needs two devDeps (
@objectstack/objectql,@objectstack/driver-sql) to host one.Generated by Claude Code
os-help commentedon Aug 10, 2026
Premise falsified — closing as not planned. Reviewed by the
domain:servicesseat (#6021, sessionsession_015fkdTyGmMD5s8ZtEifvuGy); the dev returnedpremise_still_valid: falsewith zero files changed, which is the successful outcome for this dispatch, not a failed one.What the measurement showed (real kernel, real drivers, real QuickJS runner, production wiring via
AppPlugin'sql.bindHooks+hookBodyRunnerFactory, persisted-row assertions):bindHooksToEnginewraps every declarative hook inwrapDeclarativeHook, which installs a flat record view over the envelope (installFlatInput,packages/objectql/src/hook-wrappers.ts:446, helper:502— verified at those exact lines onorigin/mainby this seat): reads of a non-wrapper key resolve againstdata, writes land indata. CRM probe 6/6 (closed_won persists probability=100, control untouched); showcase probe 5/5 (status=new/source=web stamped, title trimmed).bodyhook,unwrapProxyToPlainenumerates the proxy's flat keys, soctx.input.dataisundefined— the re-spelledStampInquiryDefaultsHook(which carriesonError: 'abort') threwTypeErrorand persisted 0 rows: the public web-to-lead insert would go from working to refusing every submission.The real defect is filed as #7254 (unassigned, awaiting triage):
HookContextSchema.input's contract table documents only the raw-engine envelope while every author-facing declarative hook sees the flat view — the documentation gap that produced this false alarm, where the documentedinput.dataspelling is fatal inside a body.Follow-up (this seat's scope ruling on the dev's option B): the measurement probes will be landed as regression pins via a separate test-only card, so the corpus's working behaviour becomes a stated fact rather than something each reader re-derives and gets wrong — filed next,
domain:*left to triage.Note for the record: the dispatch prompt's quoted direction ("examples converge on
ctx.input.data") presumed the card's facts and is spent with them — the dev correctly proposed no engine/sandbox change, and no re-adjudication is needed. Maintainer veto window: reopen if you read the evidence differently; it is all reproducible from the probes described above.Generated by Claude Code
test(examples): pin the objectstack-ai#7225 measurement — one persist…