work-kit-cli 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +11 -0
  2. package/cli/src/commands/bootstrap.test.ts +40 -0
  3. package/cli/src/commands/bootstrap.ts +38 -0
  4. package/cli/src/commands/extract.ts +207 -0
  5. package/cli/src/commands/init.test.ts +50 -0
  6. package/cli/src/commands/init.ts +32 -5
  7. package/cli/src/commands/learn.test.ts +244 -0
  8. package/cli/src/commands/learn.ts +104 -0
  9. package/cli/src/commands/next.ts +30 -10
  10. package/cli/src/commands/observe.ts +16 -21
  11. package/cli/src/commands/pause-resume.test.ts +2 -2
  12. package/cli/src/commands/resume.ts +95 -7
  13. package/cli/src/commands/setup.ts +160 -0
  14. package/cli/src/commands/status.ts +2 -0
  15. package/cli/src/commands/workflow.ts +19 -9
  16. package/cli/src/config/constants.ts +10 -0
  17. package/cli/src/config/model-routing.test.ts +190 -0
  18. package/cli/src/config/model-routing.ts +208 -0
  19. package/cli/src/config/workflow.ts +5 -5
  20. package/cli/src/index.ts +70 -5
  21. package/cli/src/observer/data.ts +132 -9
  22. package/cli/src/observer/renderer.ts +34 -36
  23. package/cli/src/observer/watcher.ts +28 -16
  24. package/cli/src/state/schema.ts +50 -3
  25. package/cli/src/state/store.ts +39 -4
  26. package/cli/src/utils/fs.ts +13 -0
  27. package/cli/src/utils/knowledge.ts +471 -0
  28. package/package.json +1 -1
  29. package/skills/auto-kit/SKILL.md +27 -10
  30. package/skills/full-kit/SKILL.md +25 -8
  31. package/skills/resume-kit/SKILL.md +44 -8
  32. package/skills/wk-bootstrap/SKILL.md +6 -0
  33. package/skills/wk-build/SKILL.md +3 -2
  34. package/skills/wk-deploy/SKILL.md +1 -0
  35. package/skills/wk-plan/SKILL.md +3 -2
  36. package/skills/wk-review/SKILL.md +1 -0
  37. package/skills/wk-test/SKILL.md +1 -0
  38. package/skills/wk-test/steps/e2e.md +15 -12
  39. package/skills/wk-wrap-up/SKILL.md +15 -2
  40. package/skills/wk-wrap-up/steps/knowledge.md +76 -0
package/README.md CHANGED
@@ -55,9 +55,20 @@ Best for: bug fixes, small changes, refactors, well-understood tasks.
55
55
  | `validate` | Validate state integrity and phase prerequisites |
56
56
  | `loopback` | Route back to a previous stage (max 2 per route) |
57
57
  | `workflow` | Display the full workflow plan |
58
+ | `pause` | Pause the active session (state preserved on disk) |
59
+ | `resume [--slug <slug>]` | Without `--slug`: list resumable sessions in this repo. With `--slug`: resume the named session |
60
+ | `observe [--all]` | Live TUI dashboard of active/paused/completed sessions. `--all` watches every work-kit project on the system |
58
61
  | `doctor` | Run environment health checks (supports `--json`) |
59
62
  | `setup` | Install work-kit skills into a Claude Code project |
60
63
 
64
+ ### Picking up where you left off
65
+
66
+ `work-kit resume` (or `/resume-kit` in Claude Code) scans every worktree of the current repo for `.work-kit/tracker.json` files in `paused` or `in-progress` state and lets you pick one. It works from the main repo root — no need to `cd` into a worktree first. In-progress sessions are listed too, so a terminal you closed without pausing can be recovered: just look for the row with a stale `lastUpdatedAgoMs`.
67
+
68
+ ### Watching multiple projects
69
+
70
+ `work-kit observe --all` discovers every work-kit-enabled repo from your `~/.claude/projects/` history and watches them all in one dashboard. Each row shows the project name, work item slug, mode, type, current state, and worktree.
71
+
61
72
  ## Phases
62
73
 
63
74
  | Phase | Steps | Agent |
@@ -6,6 +6,7 @@ import * as os from "node:os";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { bootstrapCommand } from "./bootstrap.js";
8
8
  import { initCommand } from "./init.js";
9
+ import { learnCommand } from "./learn.js";
9
10
 
10
11
  function makeTmpDir(): string {
11
12
  const dir = path.join(os.tmpdir(), `work-kit-test-${randomUUID()}`);
@@ -94,6 +95,45 @@ describe("bootstrapCommand", () => {
94
95
  assert.ok(result.nextAction?.includes("complete"));
95
96
  });
96
97
 
98
+ it("injects knowledge field when knowledge files exist", () => {
99
+ const tmp = makeTmpDir();
100
+ tmpDirs.push(tmp);
101
+
102
+ initCommand({
103
+ mode: "full",
104
+ description: "Knowledge test",
105
+ worktreeRoot: tmp,
106
+ });
107
+
108
+ // Write entries to lessons, conventions, risks, and workflow
109
+ learnCommand({ type: "lesson", text: "A useful lesson", worktreeRoot: tmp });
110
+ learnCommand({ type: "convention", text: "A coding convention", worktreeRoot: tmp });
111
+ learnCommand({ type: "risk", text: "A fragile area", worktreeRoot: tmp });
112
+ learnCommand({ type: "workflow", text: "Workflow feedback", worktreeRoot: tmp });
113
+
114
+ const result = bootstrapCommand(tmp);
115
+ assert.ok(result.knowledge, "knowledge field should be present");
116
+ assert.ok(result.knowledge?.lessons?.includes("A useful lesson"));
117
+ assert.ok(result.knowledge?.conventions?.includes("A coding convention"));
118
+ assert.ok(result.knowledge?.risks?.includes("A fragile area"));
119
+ // workflow.md is intentionally NOT injected
120
+ assert.equal((result.knowledge as any).workflow, undefined);
121
+ });
122
+
123
+ it("does not inject knowledge field when no knowledge files exist", () => {
124
+ const tmp = makeTmpDir();
125
+ tmpDirs.push(tmp);
126
+
127
+ initCommand({
128
+ mode: "full",
129
+ description: "No knowledge test",
130
+ worktreeRoot: tmp,
131
+ });
132
+
133
+ const result = bootstrapCommand(tmp);
134
+ assert.equal(result.knowledge, undefined);
135
+ });
136
+
97
137
  it("reports failed state", () => {
98
138
  const tmp = makeTmpDir();
99
139
  tmpDirs.push(tmp);
@@ -2,6 +2,13 @@ import fs from "node:fs";
2
2
  import { findWorktreeRoot, readState, writeState, statePath } from "../state/store.js";
3
3
  import { unpause } from "../state/helpers.js";
4
4
  import { CLI_BINARY, STALE_THRESHOLD_MS } from "../config/constants.js";
5
+ import { fileForType, readKnowledgeFile } from "../utils/knowledge.js";
6
+
7
+ export interface BootstrapKnowledge {
8
+ lessons?: string;
9
+ conventions?: string;
10
+ risks?: string;
11
+ }
5
12
 
6
13
  export interface BootstrapResult {
7
14
  active: boolean;
@@ -16,6 +23,13 @@ export interface BootstrapResult {
16
23
  resumeReason?: string;
17
24
  nextAction?: string;
18
25
  recovery?: string | null;
26
+ /**
27
+ * Project-level knowledge files (lessons/conventions/risks) read from
28
+ * <mainRepoRoot>/.work-kit-knowledge/. Capped at 200 lines per file.
29
+ * workflow.md is intentionally excluded — it's a write-only artifact for
30
+ * human curators, not session context.
31
+ */
32
+ knowledge?: BootstrapKnowledge;
19
33
  }
20
34
 
21
35
  export interface BootstrapOptions {
@@ -75,6 +89,29 @@ export function bootstrapCommand(startDir?: string, options: BootstrapOptions =
75
89
  nextAction = `Continue ${state.currentPhase ?? "next phase"}${state.currentStep ? "/" + state.currentStep : ""}. Run \`${CLI_BINARY} next\` to get the agent prompt.`;
76
90
  }
77
91
 
92
+ // Load project-level knowledge files (best effort, never breaks bootstrap).
93
+ // workflow.md is intentionally excluded — it's a write-only artifact for
94
+ // human curators, not session context.
95
+ let knowledge: BootstrapKnowledge | undefined;
96
+ try {
97
+ const mainRepoRoot = state.metadata?.mainRepoRoot;
98
+ if (mainRepoRoot) {
99
+ const lessons = readKnowledgeFile(mainRepoRoot, fileForType("lesson"));
100
+ const conventions = readKnowledgeFile(mainRepoRoot, fileForType("convention"));
101
+ const risks = readKnowledgeFile(mainRepoRoot, fileForType("risk"));
102
+ if (lessons || conventions || risks) {
103
+ knowledge = {
104
+ ...(lessons && { lessons }),
105
+ ...(conventions && { conventions }),
106
+ ...(risks && { risks }),
107
+ };
108
+ }
109
+ }
110
+ } catch (err: any) {
111
+ // Non-fatal: log to stderr but don't break bootstrap
112
+ process.stderr.write(`work-kit: failed to load knowledge files: ${err.message}\n`);
113
+ }
114
+
78
115
  return {
79
116
  active: true,
80
117
  slug: state.slug,
@@ -87,5 +124,6 @@ export function bootstrapCommand(startDir?: string, options: BootstrapOptions =
87
124
  ...(resumed && { resumed: true, resumeReason }),
88
125
  nextAction,
89
126
  recovery,
127
+ ...(knowledge && { knowledge }),
90
128
  };
91
129
  }
@@ -0,0 +1,207 @@
1
+ import { findWorktreeRoot, readState, readStateMd, resolveMainRepoRoot, gitHeadSha } from "../state/store.js";
2
+ import {
3
+ appendAutoEntries,
4
+ ensureKnowledgeDir,
5
+ fileForType,
6
+ isKnowledgeType,
7
+ KNOWLEDGE_TYPES,
8
+ redact,
9
+ type KnowledgeEntry,
10
+ type KnowledgeType,
11
+ } from "../utils/knowledge.js";
12
+ import { skillFilePath } from "../config/workflow.js";
13
+ import type { PhaseName, WorkKitState } from "../state/schema.js";
14
+
15
+ export interface ExtractOptions {
16
+ worktreeRoot?: string;
17
+ }
18
+
19
+ export interface ExtractResult {
20
+ action: "extracted" | "error";
21
+ written: number;
22
+ duplicates: number;
23
+ byType: Record<KnowledgeType, number>;
24
+ message?: string;
25
+ }
26
+
27
+ interface RawEntry {
28
+ type: KnowledgeType;
29
+ text: string;
30
+ phase?: string;
31
+ step?: string;
32
+ source: string;
33
+ }
34
+
35
+ function emptyByType(): Record<KnowledgeType, number> {
36
+ const out = {} as Record<KnowledgeType, number>;
37
+ for (const t of KNOWLEDGE_TYPES) out[t] = 0;
38
+ return out;
39
+ }
40
+
41
+ // ── Single-pass state.md parser ─────────────────────────────────────
42
+
43
+ const OBSERVATION_RE = /^-\s*\[([a-z]+)(?::([a-z0-9-]+\/[a-z0-9-]+))?\]\s*(.+)$/i;
44
+
45
+ /**
46
+ * Walk state.md once and emit raw entries from the three sections we know:
47
+ * Observations (typed bullets), Decisions (any bullet → convention),
48
+ * Deviations (any bullet → workflow with [deviation] prefix).
49
+ */
50
+ function parseStateMd(stateMd: string): RawEntry[] {
51
+ const out: RawEntry[] = [];
52
+ if (!stateMd) return out;
53
+
54
+ // Only `## Observations` is auto-harvested. `## Decisions` and `## Deviations`
55
+ // are agent scratch space during normal phase work — they routinely contain
56
+ // test plans, acceptance-criteria checklists, and self-review dumps. Auto-
57
+ // routing them floods workflow.md with noise. Agents opt into harvesting by
58
+ // writing typed bullets (`- [lesson|convention|risk|workflow] text`) under
59
+ // `## Observations`.
60
+ let inObservations = false;
61
+
62
+ for (const rawLine of stateMd.split("\n")) {
63
+ const trimmed = rawLine.trim();
64
+
65
+ if (trimmed.startsWith("## ")) {
66
+ inObservations = trimmed.slice(3).trim().toLowerCase() === "observations";
67
+ continue;
68
+ }
69
+
70
+ if (!inObservations) continue;
71
+ if (!trimmed.startsWith("-") || trimmed.startsWith("<!--")) continue;
72
+
73
+ const m = trimmed.match(OBSERVATION_RE);
74
+ if (!m) continue;
75
+ const tag = m[1].toLowerCase();
76
+ if (!isKnowledgeType(tag)) continue;
77
+ const phaseStep = m[2];
78
+ const text = m[3].trim();
79
+ if (text.length === 0) continue;
80
+ const entry: RawEntry = { type: tag, text, source: "auto-state-md" };
81
+ if (phaseStep) {
82
+ const [p, s] = phaseStep.split("/");
83
+ entry.phase = p;
84
+ entry.step = s;
85
+ }
86
+ out.push(entry);
87
+ }
88
+
89
+ return out;
90
+ }
91
+
92
+ // ── Tracker.json extraction ─────────────────────────────────────────
93
+
94
+ function fromLoopbacks(state: WorkKitState): RawEntry[] {
95
+ return (state.loopbacks ?? []).map((lb) => ({
96
+ type: "workflow" as const,
97
+ text: `[loopback] ${lb.from.phase}/${lb.from.step} → ${lb.to.phase}/${lb.to.step}: ${lb.reason}`,
98
+ phase: lb.from.phase,
99
+ step: lb.from.step,
100
+ source: "auto-tracker",
101
+ }));
102
+ }
103
+
104
+ function fromSkippedAndFailed(state: WorkKitState): RawEntry[] {
105
+ const out: RawEntry[] = [];
106
+ for (const [phaseName, phaseState] of Object.entries(state.phases)) {
107
+ for (const [stepName, stepState] of Object.entries(phaseState.steps)) {
108
+ if (stepState.status === "skipped") {
109
+ out.push({
110
+ type: "workflow",
111
+ text: `[skipped] ${phaseName}/${stepName} was skipped during this session.`,
112
+ phase: phaseName,
113
+ step: stepName,
114
+ source: "auto-tracker",
115
+ });
116
+ }
117
+ if (stepState.outcome === "broken" || stepState.outcome === "fix_needed") {
118
+ out.push({
119
+ type: "workflow",
120
+ text: `[failure] ${phaseName}/${stepName} reported outcome=${stepState.outcome}.`,
121
+ phase: phaseName,
122
+ step: stepName,
123
+ source: "auto-tracker",
124
+ });
125
+ }
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+
131
+ // ── Main ────────────────────────────────────────────────────────────
132
+
133
+ export function extractCommand(opts: ExtractOptions = {}): ExtractResult {
134
+ const root = opts.worktreeRoot || findWorktreeRoot();
135
+ if (!root) {
136
+ return {
137
+ action: "error",
138
+ written: 0,
139
+ duplicates: 0,
140
+ byType: emptyByType(),
141
+ message: "No work-kit session found. Run from inside a worktree.",
142
+ };
143
+ }
144
+
145
+ let state: WorkKitState;
146
+ try {
147
+ state = readState(root);
148
+ } catch (e: any) {
149
+ return {
150
+ action: "error",
151
+ written: 0,
152
+ duplicates: 0,
153
+ byType: emptyByType(),
154
+ message: `Could not read state: ${e.message}`,
155
+ };
156
+ }
157
+
158
+ const mainRepoRoot = state.metadata?.mainRepoRoot ?? resolveMainRepoRoot(root);
159
+ ensureKnowledgeDir(mainRepoRoot);
160
+
161
+ const stateMd = readStateMd(root) ?? "";
162
+
163
+ const raw: RawEntry[] = [
164
+ ...parseStateMd(stateMd),
165
+ ...fromLoopbacks(state),
166
+ ...fromSkippedAndFailed(state),
167
+ ];
168
+
169
+ const ts = new Date().toISOString();
170
+ const sha = gitHeadSha(mainRepoRoot);
171
+
172
+ // Group entries by destination file for a single read-modify-write per file.
173
+ const grouped = new Map<string, KnowledgeEntry[]>();
174
+ for (const r of raw) {
175
+ const phase = r.phase ?? state.currentPhase ?? undefined;
176
+ const step = r.step ?? state.currentStep ?? undefined;
177
+ const skillPath = phase && step ? skillFilePath(phase as PhaseName, step) : undefined;
178
+ const { text } = redact(r.text);
179
+
180
+ const entry: KnowledgeEntry = {
181
+ ts,
182
+ sessionSlug: state.slug,
183
+ phase,
184
+ step,
185
+ skillPath,
186
+ gitSha: sha,
187
+ source: r.source,
188
+ text,
189
+ };
190
+
191
+ const file = fileForType(r.type);
192
+ const bucket = grouped.get(file);
193
+ if (bucket) bucket.push(entry);
194
+ else grouped.set(file, [entry]);
195
+ }
196
+
197
+ const result = appendAutoEntries(mainRepoRoot, grouped);
198
+
199
+ // Map per-file write counts back to per-type counts. Each KnowledgeType
200
+ // routes to exactly one file, so the lookup is unambiguous.
201
+ const byType = emptyByType();
202
+ for (const t of KNOWLEDGE_TYPES) {
203
+ byType[t] = result.perFile.get(fileForType(t))?.written ?? 0;
204
+ }
205
+
206
+ return { action: "extracted", written: result.written, duplicates: result.duplicates, byType };
207
+ }
@@ -94,6 +94,56 @@ describe("initCommand", () => {
94
94
  }
95
95
  });
96
96
 
97
+ it("persists model policy when provided", () => {
98
+ const tmp = makeTmpDir();
99
+ tmpDirs.push(tmp);
100
+
101
+ initCommand({
102
+ mode: "full",
103
+ description: "Ship the avatar feature",
104
+ modelPolicy: "opus",
105
+ worktreeRoot: tmp,
106
+ });
107
+
108
+ const state = JSON.parse(
109
+ fs.readFileSync(path.join(tmp, ".work-kit", "tracker.json"), "utf-8")
110
+ );
111
+ assert.equal(state.modelPolicy, "opus");
112
+ });
113
+
114
+ it("omits modelPolicy from state when defaulting to auto", () => {
115
+ const tmp = makeTmpDir();
116
+ tmpDirs.push(tmp);
117
+
118
+ initCommand({
119
+ mode: "full",
120
+ description: "Some default task",
121
+ worktreeRoot: tmp,
122
+ });
123
+
124
+ const state = JSON.parse(
125
+ fs.readFileSync(path.join(tmp, ".work-kit", "tracker.json"), "utf-8")
126
+ );
127
+ assert.equal(state.modelPolicy, undefined);
128
+ });
129
+
130
+ it("rejects invalid model policy", () => {
131
+ const tmp = makeTmpDir();
132
+ tmpDirs.push(tmp);
133
+
134
+ const result = initCommand({
135
+ mode: "full",
136
+ description: "Task with bad policy",
137
+ modelPolicy: "turbo" as any,
138
+ worktreeRoot: tmp,
139
+ });
140
+
141
+ assert.equal(result.action, "error");
142
+ if (result.action === "error") {
143
+ assert.ok(result.message.includes("model-policy"));
144
+ }
145
+ });
146
+
97
147
  it("auto mode with classification succeeds", () => {
98
148
  const tmp = makeTmpDir();
99
149
  tmpDirs.push(tmp);
@@ -1,10 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { WorkKitState, PhaseState, PhaseName, PHASE_NAMES, STEPS_BY_PHASE, WorkflowStep, Classification, MODE_FULL, MODE_AUTO } from "../state/schema.js";
3
+ import { WorkKitState, PhaseState, PhaseName, PHASE_NAMES, STEPS_BY_PHASE, WorkflowStep, Classification, MODE_FULL, MODE_AUTO, ModelPolicy, isModelPolicy } from "../state/schema.js";
4
4
  import { writeState, writeStateMd, stateExists, STATE_DIR, resolveMainRepoRoot } from "../state/store.js";
5
5
  import { buildFullWorkflow, buildDefaultWorkflow, skillFilePath } from "../config/workflow.js";
6
6
  import { BRANCH_PREFIX, CLI_BINARY } from "../config/constants.js";
7
7
  import { loadProjectConfig } from "../config/project-config.js";
8
+ import { resolveModel } from "../config/model-routing.js";
8
9
  import type { Action } from "../state/schema.js";
9
10
 
10
11
  function toSlug(description: string): string {
@@ -85,6 +86,15 @@ ${description}
85
86
  <!-- Append here whenever you choose between real alternatives -->
86
87
  <!-- Format: **<context>**: chose <X> over <Y> — <why> -->
87
88
 
89
+ ## Observations
90
+ <!-- Append typed bullets as you notice things worth preserving across sessions. -->
91
+ <!-- wrap-up/knowledge routes these to .work-kit-knowledge/. -->
92
+ <!-- Grammar: - [lesson|convention|risk|workflow] text (workflow tag may include :phase/step) -->
93
+ <!-- Examples: -->
94
+ <!-- - [risk] auth.middleware.ts breaks if SESSION_SECRET is unset. -->
95
+ <!-- - [convention] All API errors must use createApiError() helper. -->
96
+ <!-- - [workflow:test/e2e] The e2e step doesn't tell agents to start the dev server first. -->
97
+
88
98
  ## Deviations
89
99
  <!-- Append here whenever implementation diverges from the Blueprint -->
90
100
  <!-- Format: **<Blueprint step>**: <what changed> — <why> -->
@@ -93,9 +103,12 @@ ${description}
93
103
  return md;
94
104
  }
95
105
 
96
- function ensureGitignored(worktreeRoot: string): void {
97
- const gitignorePath = path.join(worktreeRoot, ".gitignore");
98
- const entry = `${STATE_DIR}/`;
106
+ /**
107
+ * Append `entry` to `<root>/.gitignore` if it isn't already present.
108
+ * Idempotent. Creates the file if missing. Reused by setup.ts.
109
+ */
110
+ export function ensureGitignored(root: string, entry: string): void {
111
+ const gitignorePath = path.join(root, ".gitignore");
99
112
 
100
113
  if (fs.existsSync(gitignorePath)) {
101
114
  const content = fs.readFileSync(gitignorePath, "utf-8");
@@ -111,6 +124,7 @@ export function initCommand(options: {
111
124
  description: string;
112
125
  classification?: Classification;
113
126
  gated?: boolean;
127
+ modelPolicy?: ModelPolicy;
114
128
  worktreeRoot?: string;
115
129
  }): Action {
116
130
  const worktreeRoot = options.worktreeRoot || process.cwd();
@@ -120,8 +134,17 @@ export function initCommand(options: {
120
134
  const mode = options.mode ?? projectConfig.defaults?.mode ?? "full";
121
135
  const classification = options.classification ?? projectConfig.defaults?.classification;
122
136
  const gated = options.gated ?? projectConfig.defaults?.gated ?? false;
137
+ const modelPolicy: ModelPolicy = options.modelPolicy ?? "auto";
123
138
  const { description } = options;
124
139
 
140
+ // Validate model policy (guards against CLI callers that bypass the commander layer)
141
+ if (!isModelPolicy(modelPolicy)) {
142
+ return {
143
+ action: "error",
144
+ message: `Invalid --model-policy "${modelPolicy}". Use one of: auto, opus, sonnet, haiku, inherit.`,
145
+ };
146
+ }
147
+
125
148
  // Guard: don't overwrite existing state
126
149
  if (stateExists(worktreeRoot)) {
127
150
  return {
@@ -180,6 +203,7 @@ export function initCommand(options: {
180
203
  mode: modeLabel,
181
204
  ...(gated && { gated: true }),
182
205
  ...(classification && { classification }),
206
+ ...(modelPolicy !== "auto" && { modelPolicy }),
183
207
  status: "in-progress",
184
208
  currentPhase: firstPhase,
185
209
  currentStep: firstStep,
@@ -193,12 +217,14 @@ export function initCommand(options: {
193
217
  };
194
218
 
195
219
  // Ensure .work-kit/ is gitignored (temp working state, not for commits)
196
- ensureGitignored(worktreeRoot);
220
+ ensureGitignored(worktreeRoot, `${STATE_DIR}/`);
197
221
 
198
222
  // Write state files
199
223
  writeState(worktreeRoot, state);
200
224
  writeStateMd(worktreeRoot, generateStateMd(slug, branch, modeLabel, description, classification, workflow));
201
225
 
226
+ const model = resolveModel(state, firstPhase, firstStep);
227
+
202
228
  return {
203
229
  action: "spawn_agent",
204
230
  phase: firstPhase,
@@ -206,5 +232,6 @@ export function initCommand(options: {
206
232
  skillFile: skillFilePath(firstPhase, firstStep),
207
233
  agentPrompt: `You are starting the ${firstPhase} phase. Begin with the ${firstStep} step. Read the skill file and follow its instructions. Write outputs to .work-kit/state.md.`,
208
234
  onComplete: `${CLI_BINARY} complete ${firstPhase}/${firstStep}`,
235
+ ...(model && { model }),
209
236
  };
210
237
  }