T-4KYN-hooks-onto-shared-resolver-and-executor
State: closed/done · Impact: medium · Complexity: large
Give hook step lists the same per-layer provenance workflows now have,
instead of runHooks reading the pre-merged config directly with no
resolution step at all — and make runHooks execute each resolved step
through the same primitive runWorkflow uses, instead of its own inline
sh -c loop.
lib/services/hooks/run.ts both resolves (resolveVerb) and executes
(exec/deps.sh.run(['-c', ...]), its own requireTrustedVerbs call)
independently of step-runner.ts. runHooks reads
layers.merged.hooks?.[hook] directly — the list loadConfigLayers already
deep-merged, with no per-step provenance ever recorded.
Proposed
Section titled “Proposed”A new resolveHook(hookName, projectRoot, opts): Promise<ResolvedHook>
(mirroring chain.ts’s/workflows.ts’s one-module-per-section pattern)
gives hook step lists the same {layer, mode} provenance via T-JIBO’s
resolveLayeredSteps. step-runner.ts’s runWorkflow loop is refactored so
its per-step-kind body becomes a standalone executeStep, called by both
runWorkflow (unchanged behavior) and, new, runHooks — closing the “hooks
have their own executor” gap in the same pass as the resolution gap.
Approach
Section titled “Approach”- New
lib/config/hooks.ts:resolveHook(hookName, projectRoot, opts): Promise<ResolvedHook>. CallsT-JIBO’sresolveLayeredStepswith noenvlayer and an emptydefaultSteps(hooks have no built-in list), readingsdlc.local.yaml/sdlc.yaml’s rawhooks.<hookName>entries. A hook step’s{run: <name>}resolves throughworkflows.ts’sresolveWorkflow(<name>, ...)(T-4OUZ) and splices in its steps, with the existing “a hook step cannot be skill/prompt” rule now applied to the spliced-in steps too. lib/services/hooks/run.ts: replace the directlayers.merged.hooks?.[hook]read with a call toresolveHook; thread its per-step provenance intoHookStepResult(a new optionalsourcefield) so a futuredoctor/config explaincan show it.lib/services/apply/hooks.ts(the hooks applier): confirm it only needs the hook names present, not their resolved step content (shims callsdlc hooks run <hook>at commit time) — if so, no change beyond an import-path fix if it imported fromverbs.ts.- Extract the shared executor:
step-runner.ts’srunWorkflowloop’s per-iteration body (skill/prompt→session launch;run→ iterateresolvedVerb.verbs;script→ substitute and run — each gated byrequireTrustedVerbsfirst) becomes a standaloneexecuteStep(resolvedStep, deps): Promise<StepOutcome>, called once per iteration byrunWorkflow’s own loop (behavior-preserving refactor) and, new, byrunHooks. lib/services/hooks/run.ts: afterresolveHookreturns a flattenedrun/script-only step list, replace theexec/deps.sh.run(['-c', text, hook, ...args], …)closure and inlinerequireTrustedVerbscall with oneexecuteStepcall per resolved leaf, keepingwhen/on_fail/parkas the wrapper around each call exactly as today.
| Area | Note |
|---|---|
solutions/ontological/lib/config/hooks.ts | New: resolveHook, mirroring chain.ts’s pattern |
solutions/ontological/lib/config/hooks.test.ts | New |
solutions/ontological/lib/services/orchestrator/step-runner.ts | Extract executeStep from runWorkflow’s loop body; runWorkflow calls it unchanged in behavior |
solutions/ontological/lib/services/hooks/run.ts | Read resolveHook(...) instead of the merged config directly; execute via executeStep instead of its own exec/sh -c loop |
solutions/ontological/lib/services/hooks/run.test.ts | Add a local-vs-project layering case; assert execution goes through executeStep |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: A
pre-commithook step list set only insdlc.yamlresolves with every step tagged{layer: 'project', mode: 'replace'}. - AC-2: The same hook name additionally
{append: [...]}-set insdlc.local.yamlresolves to the project list followed by the local-appended steps, the latter tagged{layer: 'local', mode: 'append'}— a case structurally impossible to observe correctly before this task (today’s deep-merge collapses local/project into one indistinguishable list before any step sees it). - AC-3:
- run: setupinside a hook step resolves tosetupworkflow’s spliced script steps;- run: pr-review(whose target is askill:step) fails resolution with the existing “a hook step cannot be a skill or prompt step” message. - AC-4:
bun test solutions/ontological/lib/config/hooks.test.ts solutions/ontological/lib/services/hookspasses. - AC-5:
bun test solutions/ontological/lib/services/orchestrator/step-runner.test.tspasses unchanged after theexecuteStepextraction — proving the refactor is behavior-preserving forrunWorkflow’s existing callers. - AC-6: A failing
run/scripthook step’s trust-gate error message and exit behavior are byte-for-byte identical before and after this task (the gate call moved intoexecuteStep, but its inputs/outputs did not change).
Out of scope
Section titled “Out of scope”- Any change to
on_fail/when/park-marker behavior — untouched.
Dependencies
Section titled “Dependencies”T-JIBO, T-4OUZ (hook run: steps splice workflow steps). Touches
step-runner.ts, which T-Q65Q also edits (resolveEngine/
resolveHost) — land this task before T-Q65Q to avoid a rebase on the
same file.
Discovery context
Section titled “Discovery context”Drawn from D-LSLH-collapse-verbs-workflows-chain-hooks’s “Hooks execute
through the shared primitive” section.
Depends on
Section titled “Depends on”T-JIBO, T-4OUZ