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.
Proposed
Section titled “Proposed”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.
Approach
Section titled “Approach”lib/config/load.ts: delete theverbsschema key and its.meta(). ExtendWorkflowStepSchema’s bare-array shorthand so a plainstringelement sugars to{script: <string>}(a preprocessing step ahead ofWorkflowStepSchema’s union — do not changeWorkflowScriptStepSchemaitself). Add optionalengine/hostfields toWorkflowEntryObjectSchema(schema only in this task; dispatch wiring isT-Q65Q).lib/config/workflows.ts: rewriteresolveWorkflowto callT-JIBO’sresolveLayeredSteps, readingsdlc.local.yaml/sdlc.yamlraw per layer (reuseloadConfigLayers, the wayverbs.tsalready does), passingSDLC_WORKFLOW_<NAME>(sameupper(name).replace('-','_')shape as today’sSDLC_VERB_<NAME>) asenvStepswhen set (newline-split, each line one{script}step). Forname === 'setup'with nolocal/project/enventry, plug the existingdefaultSetupheuristic in as the async default (mechanical move, no behavior change). ChangeBUILTIN_WORKFLOWS.check/.judgefrom[{run: 'check'}]/[{run: 'judge'}]to[].- Redefine
run:step resolution: when a raw step is{run: <other>}, resolve<other>recursively viaresolveWorkflow(<other>, projectRoot, { ...opts, visited: visited ∪ {name} })and splice its resolved steps in place. Thread avisited: Set<string>through every recursive call; if<other> ∈ visited, throwOpError('SCHEMA_ERROR', 'workflow cycle: ' + [...visited, other].join(' → ')). Do not restrict to one level —checkcomposed ofrun: fmt-check/run: lint/run: typecheck/run: test-unitmust itself be a validrun:target for a hook’srun: check, which a depth-1 restriction would reject. - Add an envelope-level
layer: ConfigLayertoResolvedWorkflow,'none'iffnamematched no builtin table entry, nosetup-only detection, and nolocal/project/envconfig at all. Wirelib/services/workflow/ops/run.ts(sdlc run <name>) to throwOpError('INVALID_INPUT', "no workflow named '<name>'")whenresolveWorkflow(name, ...).layer === 'none', instead of proceeding torunWorkflowwith zero steps and reportingok: true. No other caller ofresolveWorkflowchanges behavior. - Add
resolveWorkflowCommands(name, projectRoot, opts): Promise<{ commands: string[]; source: StepProvenance }>as theresolveVerb- compatible replacement: callsresolveWorkflow, requires every step to bescript-shaped (OpError('SCHEMA_ERROR', ...)otherwise), returns the flat command strings plus the last (highest layer) provenance tag. - Delete
lib/config/verbs.ts’sresolveVerb/cascade functions; keepBASELINE_VERB_NAMES/DEFAULT_SETUP_TASK_NAMES/DEFAULT_SETUP_TASK_RUNNERSanddefaultSetup(moved intoworkflows.tsor re-exported) sinceT-YZOM’s call sites still need the naming constant. lib/config/meta.ts: no shape change —workflows’s.meta()grows itsconsumerslist onceT-YZOMrewires call sites; this task only needsmeta.test.tsgreen withverbsgone andworkflowsintact.
| Area | Note |
|---|---|
solutions/ontological/lib/config/load.ts | Delete verbs key; bare-string sugar; engine/host fields |
solutions/ontological/lib/config/workflows.ts | Rewrite resolveWorkflow; add resolveWorkflowCommands; run: composition via recursion + visited-set cycle guard; envelope layer field; BUILTIN_WORKFLOWS.check/.judge → [] |
solutions/ontological/lib/config/verbs.ts | Delete cascade functions; keep/relocate BASELINE_VERB_NAMES, defaultSetup |
solutions/ontological/lib/config/verbs.test.ts, workflows.test.ts | Rewrite for the merged behavior; add cycle-detection and multi-level run: composition cases |
solutions/ontological/lib/services/workflow/ops/run.ts | sdlc run <name> throws INVALID_INPUT when layer === 'none' instead of silently succeeding on zero steps |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
sdlc.yaml’sverbs: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 twoscript-kind steps, both tagged{layer: 'project', mode: 'replace'}. - AC-3:
SDLC_WORKFLOW_CHECKenv var (newline-separated) resolvescheckto the env-supplied commands regardless ofsdlc.yaml/sdlc.local.yamlcontent, tagged{layer: 'env', mode: 'replace'}. - AC-4: An unconfigured
check/judgeresolves 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 plainscript-shaped leaf) resolvescheckto the four leaves’ spliced-together steps, in order. - AC-6: A workflow entry
{run: 'a'}wherea: [{run: 'b'}]andb: [{run: 'a'}](a real cycle, two levels deep) throwsOpError('SCHEMA_ERROR', ...)naming the cycle (a → b → a); a non-cyclical two-level chain (a hook’srun: checkwherecheckis 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 bylayer, not just by an emptysteps. - AC-8:
sdlc run nonexistent-nameexits non-zero withINVALID_INPUT;sdlc run judgein the same unconfigured project exits 0 having run zero steps. - AC-9:
resolveWorkflowCommands('setup', root)with no config set still returns thedefaultSetup()heuristic’s result unchanged (later widened, not broken, byT-VE84— see completion_note). - AC-10:
bun test solutions/ontological/lib/configpasses;meta.test.tspasses withverbsgone.
Out of scope
Section titled “Out of scope”- Rewiring the ~10 direct
resolveVerbcall sites outsidelib/config/(T-YZOM), andengine/hostdispatch wiring (T-Q65Q).
Dependencies
Section titled “Dependencies”T-JIBO.
Discovery context
Section titled “Discovery context”Drawn from D-LSLH-collapse-verbs-workflows-chain-hooks’s “One config
section: workflows:” and “Composing check from named leaves” sections.
Depends on
Section titled “Depends on”T-JIBO