T-F8NL-hooks-workflow-step-grammar-when-deps-missing-on-fail-warn
State: closed/done · Impact: medium · Complexity: large
hooks: uses the workflow step grammar, plus when: deps-missing
(reusing detectDeps/NODE_LOCKFILE_MANAGERS) and
on_fail: warn|block|park. park signals the loop to park the task
and notify rather than stall. apply writes shims into the
--git-common-dir hooks dir, chaining existing hooks and printing
entries when lefthook or husky owns the dir.
Delivered in two parts. Part 1 (this change): the hooks: schema, the
sdlc hooks run runtime a shim calls, the park marker and the work-tick
consumer. Part 2 (a follow-up change): sdlc apply writing the shims.
Approach
Section titled “Approach”Part 1 — schema and runtime:
hooks:inSdlcConfigSchemabecomes a partial record keyed by a closed enum of client-side git hook names (GIT_HOOK_NAMES: pre-commit, pre-merge-commit, prepare-commit-msg, commit-msg, post-commit, pre-rebase, post-checkout, post-merge, pre-push, post-rewrite) whose values are ordered hook-step lists. A hook step extendsWorkflowRunStepSchema/WorkflowScriptStepSchemawithname?,when(always|deps-missing, defaultalways) andon_fail(warn|block|park, defaultblock). Askill/promptstep is rejected with a message saying a git hook cannot drive a session. The key keeps its.meta()(applies_via: apply:hooks,runs_in: [hook]).depsMissing(root)/missingDeps(root)inlib/config/detect-deps.ts: adetectDepscandidate whose local deps dir is absent (node_modulesfor the JS managers, also.pnp.cjsfor yarn;.venvfor uv). Ecosystems with no project-local deps dir (cargo, go, bundler, poetry, pip) never count as missing.sdlc hooks run <hook> [--stdin -] [-- args...](lib/services/hooks/ops/run.tsoverlib/services/hooks/run.ts): resolves the checkout root (Git#topLevel), reads config once, and exits 0 silently when that hook has no steps. Otherwise it filterswhen: deps-missingsteps (skipped for a post-checkout file checkout, flag arg0), gates the remaining steps on repo trust ONCE before any step runs (exit 21 when untrusted; bothrunandscriptsteps come from repo-writable config), then runs them in order:runthrough theresolveVerbcascade,scriptthrough the command seam, each assh -c <text> <hook> <args...>with cwd = checkout root, so hook args are the step’s$1…(no{placeholder}templating).on_fail:warnprints and continues;blockstops and exits 1 (git ignores the exit code of post-commit/post-checkout/post-merge/ post-rewrite);parkstops, exits 0, writes the park marker and fireshook.parked. Ablockfailure fireshook.blocked.- Park marker (
lib/services/hooks/park.ts):<git-common-dir>/sdlc/park/<branch-slug>.jsoncarrying branch, hook, step, exit code, stderr tail and timestamp — visible from every worktree. Theworktick consumes the marker fortask/<basename>before dispatching a candidate and after each workflow step, parks that task (stops its sequence,parkedon the dispatch entry) and firestask.parked.
Part 2 — apply framework and applier (delivered):
- Apply framework (
lib/services/apply/):types.ts—Applier { name, plan(ctx): Promise<Change[]>, apply(ctx, changes): Promise<void> },Change { kind: create|update|remove|manual, target, summary, instructions? }.registry.ts—APPLIERSkeyed byApplierName(lib/config/meta.ts),planAll/applyAll;registry.test.tsasserts everyapply:<x>some config key’s meta declares has a registered applier (completeness, mirroringmeta.test.ts).harness.ts— a thin adapter over the existingharness install claudeop (calls its.handlerdirectly rather than throughinvokeOp, which needs the op registered viadiscoverOps()— not guaranteed for a caller likedoctorthat imports this applier without running discovery);planpassesdryRun: true,applycalls it for real.mcp.ts— a no-op planner: onemanualnote whenmcp.allow/mcp.denyis non-empty (“not yet applied — Phase 9”), else[]. sdlc apply [--dry-run|--check] [--only <applier>] [--trust](lib/services/apply/run.ts’srunApply, registered as a normal op atpath: ['apply', 'run']inlib/services/apply/ops/run.ts) — reachable ONLY as the baresdlc apply:T-OOOE’sTOP_LEVEL_VERBSmechanism (cli/sdlc.ts) claims that path exclusively, excluding it from theapplynoun’s own subtree entirely (so the noun — which would otherwise have this as its sole verb — never appears;sdlc apply rundoes not resolve).--dry-run(the adapter’s own global flag) plans and exits 0 always;--checkplans and exits 1 iff any change is pending (0 clean); neither ever gates trust. A real apply gates trust ONCE (OpError('UNTRUSTED'), exit 21, reusingrequireTrustedVerbs) before any write, unless every planned change ismanual.- The
hooksapplier (lib/services/apply/hooks.ts, one of the threeApplierssdlc applyruns):- Unowned hooks dir (no lefthook/husky detected):
planproposescreate/update/removechanges for a small POSIXshshim per configured hook, marked with# sdlc-managed hook shim v1so re-apply recognizes it. The shim setsSDLC_CWD_DEV=0(so the INSTALLEDsdlcanswers even in a fresh worktree with nonode_modules) and execssdlc hooks run <hook> [--stdin -] -- "$@". A pre-existing FOREIGN hook is chained: moved to<hook>.pre-sdlc, run first by the new shim (stdin captured once and replayed to both, forpre-push/post-rewrite, since a pipe reads empty the second time). Unconfiguring a hook removes the shim and restores the chained original.sdlcmissing from PATH always warns on stderr and exits 0 — an environment gap never breaks git. - Owned hooks dir (a lefthook config file, or
.husky/a husky fingerprint —detectHookManager): nothing is written;planreports amanualchange carrying the exact entry a person adds (lefthookEntry(hook)— acommands: sdlc: run: SDLC_CWD_DEV=0 sdlc hooks run <hook> [--stdin -] -- {0}block,{0}being lefthook’s space-joined all-args placeholder,use_stdin: truefor pre-push/post-rewrite;huskyLine(hook)— the one line to append to.husky/<hook>), and reports no drift once that entry is actually present. - Trust:
sdlc applygates on repo trust (OpError('UNTRUSTED'), exit 21) before any write, UNLESS every planned change ismanual. project doctorgained anapplyDriftsection listing every applier’s pending change with thesdlc applyfix hint.
- Unowned hooks dir (no lefthook/husky detected):
| Area | Note |
|---|---|
solutions/ontological/lib/config/load.ts | hooks: schema, GIT_HOOK_NAMES, hook-step schemas |
solutions/ontological/lib/config/detect-deps.ts | depsMissing / missingDeps |
solutions/ontological/lib/services/hooks/ | runner, park marker, hooks run op |
solutions/ontological/lib/services/notify/dispatch.ts | hook.parked, hook.blocked, task.parked events |
solutions/ontological/lib/services/orchestrator/ticks/work.ts | park-marker consumption |
solutions/ontological/lib/services/apply/ | Applier/Change types, the applier registry, runApply, the hooks/harness/mcp appliers |
solutions/ontological/lib/services/apply/ops/run.ts | The apply run op (sdlc apply’s registry backing) |
solutions/ontological/cli/sdlc.ts | TOP_LEVEL_VERBS entry for apply (shared with T-OOOE’s init/run) |
solutions/ontological/lib/services/project/ops/doctor.ts | applyDrift section |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: A hooks: entry with on_fail: park causes a failing hook to park the task rather than block the commit.
- AC-2:
sdlc applychains a pre-existing FOREIGN hook (moves it to<hook>.pre-sdlc, runs it first) in an unowned hooks dir, and restores it on unconfigure. In a lefthook/husky-owned dir it writes nothing and reports the exact entry to add instead (revised from the original wording, which conflated the two cases before the design was resolved). - AC-3: The schema rejects a
skill/prompthook step and an unknown hook name, and acceptsrun/scriptsteps withname,when,on_fail. - AC-4: A
when: deps-missingstep runs in a temp repo with a lockfile and nonode_modules, and is skipped oncenode_modulesexists. - AC-5:
on_fail: warnexits 0,blockexits non-zero,parkexits 0 and writes the park marker and fires the notifier. - AC-6: An untrusted repo with a configured step exits 21; an untrusted repo with no steps for the hook exits 0 silently.
- AC-7: The work tick consumes a park marker for a task’s branch, parks that task and sends
a
task.parkednotification.
Out of scope
Section titled “Out of scope”- Notifier channels and the event catalog beyond the three new event names (Phase 8).
Dependencies
Section titled “Dependencies”T-OOOE(theTOP_LEVEL_VERBSmechanismsdlc apply’s CLI reachability reuses).