T-0W31-per-host-engine-mapping-turning-a-workflow-step-into-the
State: closed/done · Impact: medium · Complexity: medium
A per-host/engine mapping turns a workflow step into the first message sent to that host.
Approach
Section titled “Approach”The mapping lives in solutions/ontological/lib/services/session/engine-message.ts
(not under lib/services/host/ — that package owns the host CONTRACT, not
first-message construction), keyed by AgentEngine ('claude' | 'codex'),
not by HostName:
claudeFirstMessage(step)(pre-existing, unchanged): askill:step becomes Claude’s/sdlc:<skill> <args>slash command; aprompt:step passes through as literal text;run:/script:have no message form and throw.skillBodyFirstMessage(caller, step)(new): the non-Claude mapping for askill:step. Codex/cursor have no/sdlc:<skill>slash-command surface, so instead of naming the skill, this reads the skill’s own SOURCE prose note —plugin/plugins/sdlc/skills/<slug>/<slug>.mdunder this tool’s OWN packaged surface root (@lib/util/plugin-root’spluginSkillsDir(), which already handles all three install modes: dev tree, npm install, compiled binary) — strips its frontmatter (@lib/util/frontmatter’ssplitFrontmatter), and hands the body over verbatim as the first message. NoprojectRoot/plugin_sourceparameter is involved: the skill corpus ships WITH this tool, not with the consumer projectsession launchruns against, unlikegate/ops/skill-prose.ts’spluginSourceDir-based walk (a dev-repo-only lint over this repo’s own source tree). No{{args}}/templating convention exists in skill prose today (skills like/sdlc:backlog-capturejust describe “everything after the skill name” in prose and rely on Claude Code’s own slash-command mechanics), sostep.args, when present, are appended as one plain trailing line rather than substituted into a template. A missing skill note throwsOpError('INVALID_INPUT', …)rather than a bare ENOENT.firstMessageFor(caller, engine, step)(pre-existing, extended):engine === 'claude'→ branch 1; otherwise askill:step now uses branch 2 instead of throwing (its prior behavior — see the module’s git history/T-AD81); aprompt:step passes through either way;run:/script:still throwOpError. NoprojectRootparameter was added — see branch 2 above for why the mapping never needs one.- Wired into both existing callers, unchanged in shape beyond the new
parameter:
session/ops/launch.ts’sresolveLaunch(interactive AND headless — same function serves both) andorchestrator/step-runner.ts’s per-step loop (headless only).
Depends on T-QO8E: this mapping’s non-Claude branch only makes sense
because that task establishes claude/codex/cursor as ENGINE-NATIVE hosts with
no slash-command surface of their own — the mapping is a direct consequence
of that host-contract shape, even though the code change itself lives
entirely in engine-message.ts, not in lib/services/host/.
The mapping stays keyed purely by engine ('claude'/'codex'), never by
the three engine-native HostNames T-QO8E added — those hosts ignore
req.engine and hardcode their own CLI, so which branch a caller gets can’t
be decided in this module without a HostName it deliberately never sees.
That host-awareness was initially left as an open gap (a --host cursor/--host codex launch with no explicit --engine codex silently got
Claude’s slash-command text), then closed one layer up in ops/launch.ts:
runLaunch now resolves the interactive launch’s HOST before computing the
message, and a small engineForHost helper there picks the message-producing
engine from that host ('claude' host → engine: 'claude'; 'codex'/'cursor'
host → engine: 'codex') before calling firstMessageFor. See
ops/launch.ts’s module doc and engine-message.ts’s module doc for the
resolved shape.
| Area | Note |
|---|---|
solutions/ontological/lib/services/session/engine-message.ts | skillBodyFirstMessage, firstMessageFor’s extended branch (no projectRoot param — skill notes resolve via pluginSkillsDir()) |
solutions/ontological/lib/services/session/ops/launch.ts | firstMessageFor call sites, unchanged signature |
solutions/ontological/lib/services/orchestrator/step-runner.ts | firstMessageFor call site, unchanged signature |
solutions/ontological/lib/services/session/engine-message.test.ts | New unit tests: skill/prompt steps, claude + codex engines |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: A
skill:step and aprompt:step each produce a distinct, correctly-shaped first message for at least two engines/hosts:engine: 'claude'(Claude’s/sdlc:<skill>slash command) andengine: 'codex'(the skill’s own prose-note body) — proven inengine-message.test.tsagainst a real fixture skill note written to a throwaway project root (no mocked filesystem, no realclaude/codexprocess).
Out of scope
Section titled “Out of scope”- The engine/host coupling described above was closed in
ops/launch.ts(engineForHost), not in this module — this task’s mapping itself stays engine-keyed only. session attach/session listand any.sdlc/sessions/record wiring —T-QMGC’s job.- A
sessions:config section — not part of this mapping.
Dependencies
Section titled “Dependencies”T-QO8E— the engine-native claude/codex/cursor hosts this mapping’s non-Claude branch exists to serve.
Depends on
Section titled “Depends on”T-QO8E