Skip to content

T-4OUZ-collapse-verbs-into-workflows

State: closed/done · Impact: high · Complexity: large

Retire verbs: as a config section. workflows: becomes the one place every lifecycle command list lives, resolved through T-JIBO’s primitive, with run: redefined as recursive, cycle-guarded workflow composition instead of a verb lookup, and the CLI’s silent-no-op sharp edge for an unrecognized name closed in the same pass.

resolveVerb (verbs.ts:266) walks env → local → project → an implicit default heuristic (only for setup) → none, fully replacing per layer. resolveWorkflow (workflows.ts:182) starts from BUILTIN_WORKFLOWS[name] and lets a project’s workflows.<name> (read through the already-merged loadConfig, not raw per-layer files) replace or wrap it. BUILTIN_WORKFLOWS.check/.judge are self-referential indirections onto a same-named verb. sdlc run <name> silently no-ops (ok: true, zero steps) for a name nobody configured anywhere, indistinguishable from a name that deliberately resolves to an empty list.

verbs: is deleted from SdlcConfigSchema. workflows: grows a bare-string shorthand (a plain string element in a step list sugars to {script: '<string>'}) and optional engine/host fields. resolveWorkflow calls T-JIBO’s resolveLayeredSteps, reading raw per-layer config so local/project are distinguishable in provenance the way verbs already were. {run: <other>} resolves recursively via a visited-set cycle guard, not a one-level restriction. ResolvedWorkflow gains an envelope-level layer field so sdlc run <name> can throw INVALID_INPUT for a genuinely unrecognized name instead of silently succeeding on zero steps.

  1. lib/config/load.ts: delete the verbs schema key and its .meta(). Extend WorkflowStepSchema’s bare-array shorthand so a plain string element sugars to {script: <string>} (a preprocessing step ahead of WorkflowStepSchema’s union — do not change WorkflowScriptStepSchema itself). Add optional engine/host fields to WorkflowEntryObjectSchema (schema only in this task; dispatch wiring is T-Q65Q).
  2. lib/config/workflows.ts: rewrite resolveWorkflow to call T-JIBO’s resolveLayeredSteps, reading sdlc.local.yaml/sdlc.yaml raw per layer (reuse loadConfigLayers, the way verbs.ts already does), passing SDLC_WORKFLOW_<NAME> (same upper(name).replace('-','_') shape as today’s SDLC_VERB_<NAME>) as envSteps when set (newline-split, each line one {script} step). For name === 'setup' with no local/project/env entry, plug the existing defaultSetup heuristic in as the async default (mechanical move, no behavior change). Change BUILTIN_WORKFLOWS.check/.judge from [{run: 'check'}]/ [{run: 'judge'}] to [].
  3. Redefine run: step resolution: when a raw step is {run: <other>}, resolve <other> recursively via resolveWorkflow(<other>, projectRoot, { ...opts, visited: visited ∪ {name} }) and splice its resolved steps in place. Thread a visited: Set<string> through every recursive call; if <other> ∈ visited, throw OpError('SCHEMA_ERROR', 'workflow cycle: ' + [...visited, other].join(' → ')). Do not restrict to one level — check composed of run: fmt-check/run: lint/run: typecheck/run: test-unit must itself be a valid run: target for a hook’s run: check, which a depth-1 restriction would reject.
  4. Add an envelope-level layer: ConfigLayer to ResolvedWorkflow, 'none' iff name matched no builtin table entry, no setup-only detection, and no local/project/env config at all. Wire lib/services/workflow/ops/run.ts (sdlc run <name>) to throw OpError('INVALID_INPUT', "no workflow named '<name>'") when resolveWorkflow(name, ...).layer === 'none', instead of proceeding to runWorkflow with zero steps and reporting ok: true. No other caller of resolveWorkflow changes behavior.
  5. Add resolveWorkflowCommands(name, projectRoot, opts): Promise<{ commands: string[]; source: StepProvenance }> as the resolveVerb- compatible replacement: calls resolveWorkflow, requires every step to be script-shaped (OpError('SCHEMA_ERROR', ...) otherwise), returns the flat command strings plus the last (highest layer) provenance tag.
  6. Delete lib/config/verbs.ts’s resolveVerb/cascade functions; keep BASELINE_VERB_NAMES/DEFAULT_SETUP_TASK_NAMES/DEFAULT_SETUP_TASK_RUNNERS and defaultSetup (moved into workflows.ts or re-exported) since T-YZOM’s call sites still need the naming constant.
  7. lib/config/meta.ts: no shape change — workflows’s .meta() grows its consumers list once T-YZOM rewires call sites; this task only needs meta.test.ts green with verbs gone and workflows intact.
AreaNote
solutions/ontological/lib/config/load.tsDelete verbs key; bare-string sugar; engine/host fields
solutions/ontological/lib/config/workflows.tsRewrite resolveWorkflow; add resolveWorkflowCommands; run: composition via recursion + visited-set cycle guard; envelope layer field; BUILTIN_WORKFLOWS.check/.judge → []
solutions/ontological/lib/config/verbs.tsDelete cascade functions; keep/relocate BASELINE_VERB_NAMES, defaultSetup
solutions/ontological/lib/config/verbs.test.ts, workflows.test.tsRewrite for the merged behavior; add cycle-detection and multi-level run: composition cases
solutions/ontological/lib/services/workflow/ops/run.tssdlc run <name> throws INVALID_INPUT when layer === 'none' instead of silently succeeding on zero steps
  • AC-1: sdlc.yaml’s verbs: key fails schema validation (SdlcConfigSchema.strict() rejects the unknown key).
  • AC-2: A project workflows.check: ['bunx tsc --noEmit', 'bun test'] (bare string array) resolves to two script-kind steps, both tagged {layer: 'project', mode: 'replace'}.
  • AC-3: SDLC_WORKFLOW_CHECK env var (newline-separated) resolves check to the env-supplied commands regardless of sdlc.yaml/ sdlc.local.yaml content, tagged {layer: 'env', mode: 'replace'}.
  • AC-4: An unconfigured check/judge resolves to zero steps ([]), matching today’s pre-migration behavior exactly.
  • AC-5: A workflow entry check: [{run: 'fmt-check'}, {run: 'lint'}, {run: 'typecheck'}, {run: 'test-unit'}] (each a plain script-shaped leaf) resolves check to the four leaves’ spliced-together steps, in order.
  • AC-6: A workflow entry {run: 'a'} where a: [{run: 'b'}] and b: [{run: 'a'}] (a real cycle, two levels deep) throws OpError('SCHEMA_ERROR', ...) naming the cycle (a → b → a); a non-cyclical two-level chain (a hook’s run: check where check is AC-5’s composed workflow) resolves successfully, not error.
  • AC-7: resolveWorkflow('nonexistent-name', root) returns {layer: 'none', steps: []}; resolveWorkflow('judge', root) with no config anywhere returns {layer: 'default', steps: []} — distinguishable by layer, not just by an empty steps.
  • AC-8: sdlc run nonexistent-name exits non-zero with INVALID_INPUT; sdlc run judge in the same unconfigured project exits 0 having run zero steps.
  • AC-9: resolveWorkflowCommands('setup', root) with no config set still returns the defaultSetup() heuristic’s result unchanged (later widened, not broken, by T-VE84 — see completion_note).
  • AC-10: bun test solutions/ontological/lib/config passes; meta.test.ts passes with verbs gone.
  • Rewiring the ~10 direct resolveVerb call sites outside lib/config/ (T-YZOM), and engine/host dispatch wiring (T-Q65Q).

T-JIBO.

Drawn from D-LSLH-collapse-verbs-workflows-chain-hooks’s “One config section: workflows:” and “Composing check from named leaves” sections.

T-JIBO


← Back to Tasks