work-kit-cli 0.2.8 → 0.4.0

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 (99) hide show
  1. package/README.md +24 -13
  2. package/cli/src/commands/bootstrap.test.ts +40 -0
  3. package/cli/src/commands/bootstrap.ts +77 -13
  4. package/cli/src/commands/cancel.ts +1 -16
  5. package/cli/src/commands/complete.ts +92 -98
  6. package/cli/src/commands/completions.ts +2 -2
  7. package/cli/src/commands/doctor.ts +1 -1
  8. package/cli/src/commands/extract.ts +217 -0
  9. package/cli/src/commands/init.test.ts +50 -0
  10. package/cli/src/commands/init.ts +70 -35
  11. package/cli/src/commands/learn.test.ts +217 -0
  12. package/cli/src/commands/learn.ts +104 -0
  13. package/cli/src/commands/loopback.ts +8 -11
  14. package/cli/src/commands/next.ts +93 -60
  15. package/cli/src/commands/observe.ts +16 -21
  16. package/cli/src/commands/pause-resume.test.ts +142 -0
  17. package/cli/src/commands/pause.ts +34 -0
  18. package/cli/src/commands/report.ts +217 -0
  19. package/cli/src/commands/resume.ts +126 -0
  20. package/cli/src/commands/setup.ts +280 -0
  21. package/cli/src/commands/status.ts +8 -6
  22. package/cli/src/commands/uninstall.ts +8 -3
  23. package/cli/src/commands/workflow.ts +43 -33
  24. package/cli/src/config/agent-map.ts +9 -9
  25. package/cli/src/config/constants.ts +54 -0
  26. package/cli/src/config/loopback-routes.ts +13 -13
  27. package/cli/src/config/model-routing.test.ts +190 -0
  28. package/cli/src/config/model-routing.ts +208 -0
  29. package/cli/src/config/project-config.test.ts +127 -0
  30. package/cli/src/config/project-config.ts +106 -0
  31. package/cli/src/config/{phases.ts → workflow.ts} +40 -23
  32. package/cli/src/context/prompt-builder.ts +10 -9
  33. package/cli/src/index.ts +130 -9
  34. package/cli/src/observer/data.ts +196 -65
  35. package/cli/src/observer/renderer.ts +127 -107
  36. package/cli/src/observer/watcher.ts +28 -16
  37. package/cli/src/state/helpers.test.ts +28 -28
  38. package/cli/src/state/helpers.ts +37 -25
  39. package/cli/src/state/schema.ts +135 -45
  40. package/cli/src/state/store.ts +127 -7
  41. package/cli/src/state/validators.test.ts +13 -13
  42. package/cli/src/state/validators.ts +3 -4
  43. package/cli/src/utils/colors.ts +2 -0
  44. package/cli/src/utils/fs.ts +13 -0
  45. package/cli/src/utils/json.ts +20 -0
  46. package/cli/src/utils/knowledge.ts +471 -0
  47. package/cli/src/utils/time.ts +27 -0
  48. package/cli/src/{engine → workflow}/loopbacks.test.ts +2 -2
  49. package/cli/src/workflow/loopbacks.ts +42 -0
  50. package/cli/src/workflow/parallel.ts +64 -0
  51. package/cli/src/workflow/transitions.test.ts +129 -0
  52. package/cli/src/{engine → workflow}/transitions.ts +18 -22
  53. package/package.json +2 -2
  54. package/skills/auto-kit/SKILL.md +44 -27
  55. package/skills/cancel-kit/SKILL.md +4 -4
  56. package/skills/full-kit/SKILL.md +45 -28
  57. package/skills/pause-kit/SKILL.md +25 -0
  58. package/skills/resume-kit/SKILL.md +64 -0
  59. package/skills/wk-bootstrap/SKILL.md +11 -5
  60. package/skills/wk-build/SKILL.md +12 -11
  61. package/skills/wk-build/{stages → steps}/commit.md +1 -1
  62. package/skills/wk-build/{stages → steps}/core.md +3 -3
  63. package/skills/wk-build/{stages → steps}/integration.md +2 -2
  64. package/skills/wk-build/{stages → steps}/migration.md +1 -1
  65. package/skills/wk-build/{stages → steps}/red.md +1 -1
  66. package/skills/wk-build/{stages → steps}/refactor.md +1 -1
  67. package/skills/wk-build/{stages → steps}/setup.md +1 -1
  68. package/skills/wk-build/{stages → steps}/ui.md +1 -1
  69. package/skills/wk-deploy/SKILL.md +7 -6
  70. package/skills/wk-deploy/{stages → steps}/merge.md +1 -1
  71. package/skills/wk-deploy/{stages → steps}/monitor.md +1 -1
  72. package/skills/wk-deploy/{stages → steps}/remediate.md +1 -1
  73. package/skills/wk-plan/SKILL.md +15 -14
  74. package/skills/wk-plan/{stages → steps}/architecture.md +1 -1
  75. package/skills/wk-plan/{stages → steps}/audit.md +2 -2
  76. package/skills/wk-plan/{stages → steps}/blueprint.md +2 -2
  77. package/skills/wk-plan/{stages → steps}/clarify.md +1 -1
  78. package/skills/wk-plan/{stages → steps}/investigate.md +1 -1
  79. package/skills/wk-plan/{stages → steps}/scope.md +1 -1
  80. package/skills/wk-plan/{stages → steps}/sketch.md +1 -1
  81. package/skills/wk-plan/{stages → steps}/ux-flow.md +1 -1
  82. package/skills/wk-review/SKILL.md +11 -10
  83. package/skills/wk-review/{stages → steps}/compliance.md +1 -1
  84. package/skills/wk-review/{stages → steps}/handoff.md +2 -2
  85. package/skills/wk-review/{stages → steps}/performance.md +1 -1
  86. package/skills/wk-review/{stages → steps}/security.md +1 -1
  87. package/skills/wk-review/{stages → steps}/self-review.md +1 -1
  88. package/skills/wk-test/SKILL.md +9 -8
  89. package/skills/wk-test/steps/e2e.md +56 -0
  90. package/skills/wk-test/{stages → steps}/validate.md +1 -1
  91. package/skills/wk-test/{stages → steps}/verify.md +1 -1
  92. package/skills/wk-wrap-up/SKILL.md +19 -5
  93. package/skills/wk-wrap-up/steps/knowledge.md +76 -0
  94. package/skills/wk-wrap-up/steps/summary.md +86 -0
  95. package/cli/src/engine/loopbacks.ts +0 -32
  96. package/cli/src/engine/parallel.ts +0 -60
  97. package/cli/src/engine/transitions.test.ts +0 -129
  98. package/skills/wk-test/stages/e2e.md +0 -53
  99. /package/cli/src/{engine/phases.ts → workflow/gates.ts} +0 -0
@@ -0,0 +1,217 @@
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
+ let section: "observations" | "decisions" | "deviations" | null = null;
55
+
56
+ for (const rawLine of stateMd.split("\n")) {
57
+ const trimmed = rawLine.trim();
58
+
59
+ if (trimmed.startsWith("## ")) {
60
+ const header = trimmed.slice(3).trim().toLowerCase();
61
+ if (header === "observations") section = "observations";
62
+ else if (header === "decisions") section = "decisions";
63
+ else if (header === "deviations") section = "deviations";
64
+ else section = null;
65
+ continue;
66
+ }
67
+
68
+ if (section === null) continue;
69
+ if (!trimmed.startsWith("-") || trimmed.startsWith("<!--")) continue;
70
+
71
+ if (section === "observations") {
72
+ const m = trimmed.match(OBSERVATION_RE);
73
+ if (!m) continue;
74
+ const tag = m[1].toLowerCase();
75
+ if (!isKnowledgeType(tag)) continue;
76
+ const phaseStep = m[2];
77
+ const text = m[3].trim();
78
+ if (text.length === 0) continue;
79
+ const entry: RawEntry = { type: tag, text, source: "auto-state-md" };
80
+ if (phaseStep) {
81
+ const [p, s] = phaseStep.split("/");
82
+ entry.phase = p;
83
+ entry.step = s;
84
+ }
85
+ out.push(entry);
86
+ continue;
87
+ }
88
+
89
+ const text = trimmed.replace(/^-\s*/, "").trim();
90
+ if (text.length === 0) continue;
91
+
92
+ if (section === "decisions") {
93
+ out.push({ type: "convention", text, source: "auto-state-md" });
94
+ } else if (section === "deviations") {
95
+ out.push({ type: "workflow", text: `[deviation] ${text}`, source: "auto-state-md" });
96
+ }
97
+ }
98
+
99
+ return out;
100
+ }
101
+
102
+ // ── Tracker.json extraction ─────────────────────────────────────────
103
+
104
+ function fromLoopbacks(state: WorkKitState): RawEntry[] {
105
+ return (state.loopbacks ?? []).map((lb) => ({
106
+ type: "workflow" as const,
107
+ text: `[loopback] ${lb.from.phase}/${lb.from.step} → ${lb.to.phase}/${lb.to.step}: ${lb.reason}`,
108
+ phase: lb.from.phase,
109
+ step: lb.from.step,
110
+ source: "auto-tracker",
111
+ }));
112
+ }
113
+
114
+ function fromSkippedAndFailed(state: WorkKitState): RawEntry[] {
115
+ const out: RawEntry[] = [];
116
+ for (const [phaseName, phaseState] of Object.entries(state.phases)) {
117
+ for (const [stepName, stepState] of Object.entries(phaseState.steps)) {
118
+ if (stepState.status === "skipped") {
119
+ out.push({
120
+ type: "workflow",
121
+ text: `[skipped] ${phaseName}/${stepName} was skipped during this session.`,
122
+ phase: phaseName,
123
+ step: stepName,
124
+ source: "auto-tracker",
125
+ });
126
+ }
127
+ if (stepState.outcome === "broken" || stepState.outcome === "fix_needed") {
128
+ out.push({
129
+ type: "workflow",
130
+ text: `[failure] ${phaseName}/${stepName} reported outcome=${stepState.outcome}.`,
131
+ phase: phaseName,
132
+ step: stepName,
133
+ source: "auto-tracker",
134
+ });
135
+ }
136
+ }
137
+ }
138
+ return out;
139
+ }
140
+
141
+ // ── Main ────────────────────────────────────────────────────────────
142
+
143
+ export function extractCommand(opts: ExtractOptions = {}): ExtractResult {
144
+ const root = opts.worktreeRoot || findWorktreeRoot();
145
+ if (!root) {
146
+ return {
147
+ action: "error",
148
+ written: 0,
149
+ duplicates: 0,
150
+ byType: emptyByType(),
151
+ message: "No work-kit session found. Run from inside a worktree.",
152
+ };
153
+ }
154
+
155
+ let state: WorkKitState;
156
+ try {
157
+ state = readState(root);
158
+ } catch (e: any) {
159
+ return {
160
+ action: "error",
161
+ written: 0,
162
+ duplicates: 0,
163
+ byType: emptyByType(),
164
+ message: `Could not read state: ${e.message}`,
165
+ };
166
+ }
167
+
168
+ const mainRepoRoot = state.metadata?.mainRepoRoot ?? resolveMainRepoRoot(root);
169
+ ensureKnowledgeDir(mainRepoRoot);
170
+
171
+ const stateMd = readStateMd(root) ?? "";
172
+
173
+ const raw: RawEntry[] = [
174
+ ...parseStateMd(stateMd),
175
+ ...fromLoopbacks(state),
176
+ ...fromSkippedAndFailed(state),
177
+ ];
178
+
179
+ const ts = new Date().toISOString();
180
+ const sha = gitHeadSha(mainRepoRoot);
181
+
182
+ // Group entries by destination file for a single read-modify-write per file.
183
+ const grouped = new Map<string, KnowledgeEntry[]>();
184
+ for (const r of raw) {
185
+ const phase = r.phase ?? state.currentPhase ?? undefined;
186
+ const step = r.step ?? state.currentStep ?? undefined;
187
+ const skillPath = phase && step ? skillFilePath(phase as PhaseName, step) : undefined;
188
+ const { text } = redact(r.text);
189
+
190
+ const entry: KnowledgeEntry = {
191
+ ts,
192
+ sessionSlug: state.slug,
193
+ phase,
194
+ step,
195
+ skillPath,
196
+ gitSha: sha,
197
+ source: r.source,
198
+ text,
199
+ };
200
+
201
+ const file = fileForType(r.type);
202
+ const bucket = grouped.get(file);
203
+ if (bucket) bucket.push(entry);
204
+ else grouped.set(file, [entry]);
205
+ }
206
+
207
+ const result = appendAutoEntries(mainRepoRoot, grouped);
208
+
209
+ // Map per-file write counts back to per-type counts. Each KnowledgeType
210
+ // routes to exactly one file, so the lookup is unambiguous.
211
+ const byType = emptyByType();
212
+ for (const t of KNOWLEDGE_TYPES) {
213
+ byType[t] = result.perFile.get(fileForType(t))?.written ?? 0;
214
+ }
215
+
216
+ return { action: "extracted", written: result.written, duplicates: result.duplicates, byType };
217
+ }
@@ -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,8 +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, SUBSTAGES_BY_PHASE, WorkflowStep, Classification } from "../state/schema.js";
4
- import { writeState, writeStateMd, stateExists } from "../state/store.js";
5
- import { buildFullWorkflow, buildDefaultWorkflow, skillFilePath } from "../config/phases.js";
3
+ import { WorkKitState, PhaseState, PhaseName, PHASE_NAMES, STEPS_BY_PHASE, WorkflowStep, Classification, MODE_FULL, MODE_AUTO, ModelPolicy, isModelPolicy } from "../state/schema.js";
4
+ import { writeState, writeStateMd, stateExists, STATE_DIR, resolveMainRepoRoot } from "../state/store.js";
5
+ import { buildFullWorkflow, buildDefaultWorkflow, skillFilePath } from "../config/workflow.js";
6
+ import { BRANCH_PREFIX, CLI_BINARY } from "../config/constants.js";
7
+ import { loadProjectConfig } from "../config/project-config.js";
8
+ import { resolveModel } from "../config/model-routing.js";
6
9
  import type { Action } from "../state/schema.js";
7
10
 
8
11
  function toSlug(description: string): string {
@@ -18,23 +21,23 @@ function buildPhases(workflow?: WorkflowStep[]): Record<PhaseName, PhaseState> {
18
21
  const phases = {} as Record<PhaseName, PhaseState>;
19
22
 
20
23
  for (const phase of PHASE_NAMES) {
21
- const subStages: Record<string, { status: "pending" | "skipped" }> = {};
22
- const allSubStages = SUBSTAGES_BY_PHASE[phase];
24
+ const steps: Record<string, { status: "pending" | "skipped" }> = {};
25
+ const allSteps = STEPS_BY_PHASE[phase];
23
26
 
24
- for (const ss of allSubStages) {
27
+ for (const s of allSteps) {
25
28
  if (workflow) {
26
- const step = workflow.find((s) => s.phase === phase && s.subStage === ss);
27
- subStages[ss] = { status: step?.included ? "pending" : "skipped" };
29
+ const ws = workflow.find((w) => w.phase === phase && w.step === s);
30
+ steps[s] = { status: ws?.included ? "pending" : "skipped" };
28
31
  } else {
29
- subStages[ss] = { status: "pending" };
32
+ steps[s] = { status: "pending" };
30
33
  }
31
34
  }
32
35
 
33
- // Check if entire phase is skipped (all sub-stages skipped)
34
- const allSkipped = Object.values(subStages).every((s) => s.status === "skipped");
36
+ // Check if entire phase is skipped (all steps skipped)
37
+ const allSkipped = Object.values(steps).every((s) => s.status === "skipped");
35
38
  phases[phase] = {
36
39
  status: allSkipped ? "skipped" : "pending",
37
- subStages,
40
+ steps,
38
41
  };
39
42
  }
40
43
 
@@ -58,7 +61,7 @@ function generateStateMd(slug: string, branch: string, mode: string, description
58
61
  }
59
62
 
60
63
  md += `**Phase:** plan
61
- **Sub-stage:** clarify
64
+ **Step:** clarify
62
65
  **Status:** in-progress
63
66
 
64
67
  ## Description
@@ -67,9 +70,9 @@ ${description}
67
70
 
68
71
  if (workflow) {
69
72
  md += `\n## Workflow\n`;
70
- for (const step of workflow) {
71
- if (step.included) {
72
- const label = `${step.phase.charAt(0).toUpperCase() + step.phase.slice(1)}: ${step.subStage.charAt(0).toUpperCase() + step.subStage.slice(1)}`;
73
+ for (const ws of workflow) {
74
+ if (ws.included) {
75
+ const label = `${ws.phase.charAt(0).toUpperCase() + ws.phase.slice(1)}: ${ws.step.charAt(0).toUpperCase() + ws.step.slice(1)}`;
73
76
  md += `- [ ] ${label}\n`;
74
77
  }
75
78
  }
@@ -83,6 +86,15 @@ ${description}
83
86
  <!-- Append here whenever you choose between real alternatives -->
84
87
  <!-- Format: **<context>**: chose <X> over <Y> — <why> -->
85
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
+
86
98
  ## Deviations
87
99
  <!-- Append here whenever implementation diverges from the Blueprint -->
88
100
  <!-- Format: **<Blueprint step>**: <what changed> — <why> -->
@@ -91,9 +103,12 @@ ${description}
91
103
  return md;
92
104
  }
93
105
 
94
- function ensureGitignored(worktreeRoot: string): void {
95
- const gitignorePath = path.join(worktreeRoot, ".gitignore");
96
- const entry = ".work-kit/";
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");
97
112
 
98
113
  if (fs.existsSync(gitignorePath)) {
99
114
  const content = fs.readFileSync(gitignorePath, "utf-8");
@@ -105,14 +120,30 @@ function ensureGitignored(worktreeRoot: string): void {
105
120
  }
106
121
 
107
122
  export function initCommand(options: {
108
- mode: "full" | "auto";
123
+ mode?: "full" | "auto";
109
124
  description: string;
110
125
  classification?: Classification;
111
126
  gated?: boolean;
127
+ modelPolicy?: ModelPolicy;
112
128
  worktreeRoot?: string;
113
129
  }): Action {
114
- const { mode, description, classification, gated } = options;
115
130
  const worktreeRoot = options.worktreeRoot || process.cwd();
131
+ const mainRepoRoot = resolveMainRepoRoot(worktreeRoot);
132
+ const projectConfig = loadProjectConfig(mainRepoRoot);
133
+
134
+ const mode = options.mode ?? projectConfig.defaults?.mode ?? "full";
135
+ const classification = options.classification ?? projectConfig.defaults?.classification;
136
+ const gated = options.gated ?? projectConfig.defaults?.gated ?? false;
137
+ const modelPolicy: ModelPolicy = options.modelPolicy ?? "auto";
138
+ const { description } = options;
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
+ }
116
147
 
117
148
  // Guard: don't overwrite existing state
118
149
  if (stateExists(worktreeRoot)) {
@@ -140,63 +171,67 @@ export function initCommand(options: {
140
171
  }
141
172
 
142
173
  const slug = toSlug(description);
143
- const branch = `feature/${slug}`;
144
- const modeLabel = mode === "full" ? "full-kit" : "auto-kit";
174
+ const branch = `${BRANCH_PREFIX}${slug}`;
175
+ const modeLabel = mode === "full" ? MODE_FULL : MODE_AUTO;
145
176
 
146
177
  // Build workflow
147
178
  let workflow: WorkflowStep[] | undefined;
148
179
  if (mode === "auto" && classification) {
149
- workflow = buildDefaultWorkflow(classification);
180
+ workflow = buildDefaultWorkflow(classification, projectConfig.workflow);
150
181
  } else if (mode === "full") {
151
182
  workflow = buildFullWorkflow();
152
183
  }
153
184
 
154
- // Find first active sub-stage
185
+ // Find first active step
155
186
  let firstPhase: PhaseName = "plan";
156
- let firstSubStage = "clarify";
187
+ let firstStep = "clarify";
157
188
 
158
189
  if (workflow) {
159
190
  const first = workflow.find((s) => s.included);
160
191
  if (first) {
161
192
  firstPhase = first.phase;
162
- firstSubStage = first.subStage;
193
+ firstStep = first.step;
163
194
  }
164
195
  }
165
196
 
166
197
  // Build state
167
198
  const state: WorkKitState = {
168
- version: 1,
199
+ version: 2,
169
200
  slug,
170
201
  branch,
171
202
  started: new Date().toISOString(),
172
203
  mode: modeLabel,
173
204
  ...(gated && { gated: true }),
174
205
  ...(classification && { classification }),
206
+ ...(modelPolicy !== "auto" && { modelPolicy }),
175
207
  status: "in-progress",
176
208
  currentPhase: firstPhase,
177
- currentSubStage: firstSubStage,
209
+ currentStep: firstStep,
178
210
  phases: buildPhases(workflow),
179
211
  ...(mode === "auto" && workflow && { workflow }),
180
212
  loopbacks: [],
181
213
  metadata: {
182
214
  worktreeRoot,
183
- mainRepoRoot: worktreeRoot, // will be set properly by caller
215
+ mainRepoRoot,
184
216
  },
185
217
  };
186
218
 
187
219
  // Ensure .work-kit/ is gitignored (temp working state, not for commits)
188
- ensureGitignored(worktreeRoot);
220
+ ensureGitignored(worktreeRoot, `${STATE_DIR}/`);
189
221
 
190
222
  // Write state files
191
223
  writeState(worktreeRoot, state);
192
224
  writeStateMd(worktreeRoot, generateStateMd(slug, branch, modeLabel, description, classification, workflow));
193
225
 
226
+ const model = resolveModel(state, firstPhase, firstStep);
227
+
194
228
  return {
195
229
  action: "spawn_agent",
196
230
  phase: firstPhase,
197
- subStage: firstSubStage,
198
- skillFile: skillFilePath(firstPhase, firstSubStage),
199
- agentPrompt: `You are starting the ${firstPhase} phase. Begin with the ${firstSubStage} sub-stage. Read the skill file and follow its instructions. Write outputs to .work-kit/state.md.`,
200
- onComplete: `npx work-kit-cli complete ${firstPhase}/${firstSubStage}`,
231
+ step: firstStep,
232
+ skillFile: skillFilePath(firstPhase, firstStep),
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.`,
234
+ onComplete: `${CLI_BINARY} complete ${firstPhase}/${firstStep}`,
235
+ ...(model && { model }),
201
236
  };
202
237
  }