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
@@ -1,8 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import * as readline from "node:readline";
4
+ import { spawnSync } from "node:child_process";
4
5
  import { doctorCommand } from "./doctor.js";
5
6
  import { bold, dim, green, yellow, red, cyan } from "../utils/colors.js";
7
+ import { ensureKnowledgeDir, KNOWLEDGE_DIR, KNOWLEDGE_LOCK } from "../utils/knowledge.js";
8
+ import { ensureGitignored } from "./init.js";
6
9
 
7
10
  const SKILLS_SOURCE = path.resolve(import.meta.dirname, "..", "..", "..", "skills");
8
11
 
@@ -69,6 +72,267 @@ function copySkills(targetDir: string): { copied: string[]; skipped: string[] }
69
72
  return { copied, skipped };
70
73
  }
71
74
 
75
+ // ── Hooks installation ──────────────────────────────────────────────
76
+ //
77
+ // Writes marker files the observer polls to detect "blocked on user" state.
78
+ // Each hook command carries a sentinel comment so setup can be re-run
79
+ // idempotently — existing work-kit entries are stripped and re-added.
80
+
81
+ const HOOK_SENTINEL = "# work-kit-hook";
82
+
83
+ type HookEntry = { type: "command"; command: string };
84
+ type HookMatcherGroup = { matcher?: string; hooks: HookEntry[] };
85
+ type HookSettings = Record<string, HookMatcherGroup[]>;
86
+
87
+ interface WorkKitHookSpec {
88
+ event: string;
89
+ matcher: string;
90
+ command: string;
91
+ }
92
+
93
+ // Markers live under the per-worktree `.work-kit/` state dir.
94
+ // Hook commands run from Claude Code's CWD, which is the project/worktree root.
95
+ const WK_HOOKS: WorkKitHookSpec[] = [
96
+ // PermissionRequest → agent blocked on a tool permission prompt
97
+ {
98
+ event: "PermissionRequest",
99
+ matcher: "",
100
+ command: `mkdir -p .work-kit && date -u +%s > .work-kit/awaiting-input ${HOOK_SENTINEL}`,
101
+ },
102
+ // AskUserQuestion tool call → agent explicitly asking the user
103
+ {
104
+ event: "PreToolUse",
105
+ matcher: "AskUserQuestion",
106
+ command: `mkdir -p .work-kit && date -u +%s > .work-kit/awaiting-input ${HOOK_SENTINEL}`,
107
+ },
108
+ {
109
+ event: "PostToolUse",
110
+ matcher: "AskUserQuestion",
111
+ command: `rm -f .work-kit/awaiting-input ${HOOK_SENTINEL}`,
112
+ },
113
+ // Any tool call → clear idle marker (agent is active again)
114
+ {
115
+ event: "PreToolUse",
116
+ matcher: "",
117
+ command: `rm -f .work-kit/idle ${HOOK_SENTINEL}`,
118
+ },
119
+ // Stop → turn ended. Write idle marker (soft signal); also clear
120
+ // any stale awaiting-input marker that wasn't paired with PostToolUse
121
+ // (e.g. permission prompt was denied).
122
+ {
123
+ event: "Stop",
124
+ matcher: "",
125
+ command: `mkdir -p .work-kit && date -u +%s > .work-kit/idle && rm -f .work-kit/awaiting-input ${HOOK_SENTINEL}`,
126
+ },
127
+ ];
128
+
129
+ function stripWorkKitHooks(hooks: HookSettings): HookSettings {
130
+ const cleaned: HookSettings = {};
131
+ for (const [event, groups] of Object.entries(hooks)) {
132
+ if (!Array.isArray(groups)) continue;
133
+ const cleanedGroups: HookMatcherGroup[] = [];
134
+ for (const group of groups) {
135
+ const cleanedEntries = (group.hooks || []).filter(
136
+ (h) => !(h.type === "command" && typeof h.command === "string" && h.command.includes(HOOK_SENTINEL))
137
+ );
138
+ if (cleanedEntries.length > 0) {
139
+ cleanedGroups.push({ ...group, hooks: cleanedEntries });
140
+ }
141
+ }
142
+ if (cleanedGroups.length > 0) {
143
+ cleaned[event] = cleanedGroups;
144
+ }
145
+ }
146
+ return cleaned;
147
+ }
148
+
149
+ function addWorkKitHooks(hooks: HookSettings): HookSettings {
150
+ const out: HookSettings = { ...hooks };
151
+ for (const spec of WK_HOOKS) {
152
+ const groups = out[spec.event] ? [...out[spec.event]] : [];
153
+ const entry: HookEntry = { type: "command", command: spec.command };
154
+ // Try to reuse an existing matcher group with the same matcher
155
+ const existingIdx = groups.findIndex((g) => (g.matcher ?? "") === spec.matcher);
156
+ if (existingIdx >= 0) {
157
+ groups[existingIdx] = {
158
+ ...groups[existingIdx],
159
+ hooks: [...(groups[existingIdx].hooks || []), entry],
160
+ };
161
+ } else {
162
+ groups.push({ matcher: spec.matcher, hooks: [entry] });
163
+ }
164
+ out[spec.event] = groups;
165
+ }
166
+ return out;
167
+ }
168
+
169
+ function installHooks(projectDir: string): { added: number; file: string } {
170
+ const settingsDir = path.join(projectDir, ".claude");
171
+ const settingsFile = path.join(settingsDir, "settings.json");
172
+
173
+ if (!fs.existsSync(settingsDir)) {
174
+ fs.mkdirSync(settingsDir, { recursive: true });
175
+ }
176
+
177
+ let settings: Record<string, unknown> = {};
178
+ if (fs.existsSync(settingsFile)) {
179
+ try {
180
+ const raw = fs.readFileSync(settingsFile, "utf-8");
181
+ settings = raw.trim() ? JSON.parse(raw) : {};
182
+ } catch (err) {
183
+ throw new Error(
184
+ `Failed to parse ${settingsFile}: ${(err as Error).message}. Fix or remove the file and re-run setup.`
185
+ );
186
+ }
187
+ }
188
+
189
+ const existingHooks: HookSettings =
190
+ (settings.hooks && typeof settings.hooks === "object" ? (settings.hooks as HookSettings) : {}) ?? {};
191
+
192
+ const stripped = stripWorkKitHooks(existingHooks);
193
+ const merged = addWorkKitHooks(stripped);
194
+
195
+ settings.hooks = merged;
196
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
197
+
198
+ return { added: WK_HOOKS.length, file: settingsFile };
199
+ }
200
+
201
+ // ── Playwright detection / install ─────────────────────────────────
202
+ //
203
+ // Work-kit's Test phase requires a real E2E framework. We standardize on
204
+ // Playwright. setup/upgrade detect whether the target project already has
205
+ // it; if not, we offer to install it (and optionally scaffold a config).
206
+
207
+ type PackageManager = "pnpm" | "yarn" | "npm";
208
+
209
+ function detectPackageManager(projectDir: string): PackageManager {
210
+ if (fs.existsSync(path.join(projectDir, "pnpm-lock.yaml"))) return "pnpm";
211
+ if (fs.existsSync(path.join(projectDir, "yarn.lock"))) return "yarn";
212
+ return "npm";
213
+ }
214
+
215
+ function hasPlaywrightInstalled(projectDir: string): boolean {
216
+ const pkgPath = path.join(projectDir, "package.json");
217
+ if (!fs.existsSync(pkgPath)) return false;
218
+ try {
219
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as {
220
+ dependencies?: Record<string, string>;
221
+ devDependencies?: Record<string, string>;
222
+ };
223
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
224
+ return Boolean(deps["@playwright/test"] || deps["playwright"]);
225
+ } catch {
226
+ return false;
227
+ }
228
+ }
229
+
230
+ function hasPlaywrightConfig(projectDir: string): boolean {
231
+ return ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"]
232
+ .some((f) => fs.existsSync(path.join(projectDir, f)));
233
+ }
234
+
235
+ function runStreamed(cmd: string, args: string[], cwd: string): boolean {
236
+ const result = spawnSync(cmd, args, { cwd, stdio: "inherit" });
237
+ return result.status === 0;
238
+ }
239
+
240
+ function installPlaywrightPackage(pm: PackageManager, projectDir: string): boolean {
241
+ const args =
242
+ pm === "pnpm" ? ["add", "-D", "@playwright/test"] :
243
+ pm === "yarn" ? ["add", "-D", "@playwright/test"] :
244
+ ["install", "-D", "@playwright/test"];
245
+ console.error(` ${dim(`$ ${pm} ${args.join(" ")}`)}`);
246
+ return runStreamed(pm, args, projectDir);
247
+ }
248
+
249
+ function installPlaywrightBrowsers(projectDir: string): boolean {
250
+ // Chromium-only — fastest install, covers most E2E needs.
251
+ console.error(` ${dim("$ npx playwright install chromium")}`);
252
+ return runStreamed("npx", ["playwright", "install", "chromium"], projectDir);
253
+ }
254
+
255
+ function scaffoldPlaywrightConfig(pm: PackageManager, projectDir: string): boolean {
256
+ // `npm init playwright@latest` works regardless of pm (it just runs the create-playwright bin).
257
+ // Yarn/pnpm have their own equivalents but npm init is universally available.
258
+ console.error(` ${dim("$ npm init playwright@latest -- --quiet --browser=chromium --no-examples")}`);
259
+ return runStreamed(
260
+ "npm",
261
+ ["init", "playwright@latest", "--", "--quiet", "--browser=chromium", "--no-examples"],
262
+ projectDir
263
+ );
264
+ }
265
+
266
+ async function ensurePlaywright(projectDir: string): Promise<void> {
267
+ console.error(`\nChecking Playwright (required for work-kit's E2E test step)...`);
268
+
269
+ // Non-Node project — nothing to do.
270
+ if (!fs.existsSync(path.join(projectDir, "package.json"))) {
271
+ console.error(` ${dim("No package.json found — skipping Playwright setup.")}`);
272
+ return;
273
+ }
274
+
275
+ const pm = detectPackageManager(projectDir);
276
+ const installed = hasPlaywrightInstalled(projectDir);
277
+ const configured = hasPlaywrightConfig(projectDir);
278
+
279
+ if (installed && configured) {
280
+ console.error(` ${green("\u2713")} Playwright already installed and configured.`);
281
+ return;
282
+ }
283
+
284
+ if (!installed) {
285
+ const answer = (await promptUser(` Install Playwright (@playwright/test) via ${pm}? [y/N]: `)).toLowerCase();
286
+ if (answer !== "y" && answer !== "yes") {
287
+ console.error(` ${yellow("!")} Skipped. The wk-test E2E step will fail until Playwright is installed.`);
288
+ return;
289
+ }
290
+ if (!installPlaywrightPackage(pm, projectDir)) {
291
+ console.error(` ${red("\u2717")} Failed to install @playwright/test.`);
292
+ return;
293
+ }
294
+ if (!installPlaywrightBrowsers(projectDir)) {
295
+ console.error(` ${red("\u2717")} Failed to install Chromium browser.`);
296
+ return;
297
+ }
298
+ console.error(` ${green("+")} Installed @playwright/test and Chromium.`);
299
+ }
300
+
301
+ if (!hasPlaywrightConfig(projectDir)) {
302
+ const answer = (await promptUser(` No playwright.config found. Scaffold one now? [y/N]: `)).toLowerCase();
303
+ if (answer !== "y" && answer !== "yes") {
304
+ console.error(` ${yellow("!")} Skipped scaffolding. Create a playwright.config.ts before running wk-test.`);
305
+ return;
306
+ }
307
+ if (!scaffoldPlaywrightConfig(pm, projectDir)) {
308
+ console.error(` ${red("\u2717")} Scaffolding failed. Run \`npm init playwright@latest\` manually.`);
309
+ return;
310
+ }
311
+ console.error(` ${green("+")} Playwright config scaffolded.`);
312
+ }
313
+ }
314
+
315
+ // Project knowledge files (lessons/conventions/risks/workflow) are committed
316
+ // to the repo. Only the lockfile is gitignored.
317
+ function setupKnowledgeDir(projectDir: string): void {
318
+ console.error(`\nScaffolding ${KNOWLEDGE_DIR}/ (project knowledge files)...`);
319
+ try {
320
+ const { created } = ensureKnowledgeDir(projectDir);
321
+ if (created.length > 0) {
322
+ for (const f of created) {
323
+ console.error(` ${green("+")} ${KNOWLEDGE_DIR}/${f}`);
324
+ }
325
+ console.error(` ${yellow("!")} ${bold("These files are committed to your repo.")} Don't write secrets in them.`);
326
+ console.error(` ${dim("work-kit redacts known secret shapes at write time, but the regex sweep is best-effort.")}`);
327
+ } else {
328
+ console.error(` ${dim("Already scaffolded.")}`);
329
+ }
330
+ ensureGitignored(projectDir, `${KNOWLEDGE_DIR}/${KNOWLEDGE_LOCK}`);
331
+ } catch (err) {
332
+ console.error(` ${red("\u2717")} ${(err as Error).message}`);
333
+ }
334
+ }
335
+
72
336
  async function promptUser(question: string): Promise<string> {
73
337
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
74
338
  return new Promise((resolve) => {
@@ -142,6 +406,22 @@ export async function setupCommand(targetPath?: string): Promise<void> {
142
406
  console.error(` ${dim("Already up to date.")}`);
143
407
  }
144
408
 
409
+ // Install Claude Code hooks so the observer can detect "blocked on user" state
410
+ console.error(`\nInstalling Claude Code hooks into ${projectDir}/.claude/settings.json...`);
411
+ try {
412
+ const { added, file } = installHooks(projectDir);
413
+ console.error(` ${green("+")} ${added} hook${added === 1 ? "" : "s"} merged into ${path.relative(projectDir, file) || file}`);
414
+ console.error(` ${dim("Observer will now detect permission prompts and AskUserQuestion calls.")}`);
415
+ } catch (err) {
416
+ console.error(` ${red("✗")} ${(err as Error).message}`);
417
+ }
418
+
419
+ // Ensure Playwright is available — wk-test's E2E step requires it.
420
+ await ensurePlaywright(projectDir);
421
+
422
+ // Scaffold the project-level knowledge directory.
423
+ setupKnowledgeDir(projectDir);
424
+
145
425
  // Run doctor against the target project
146
426
  console.error("\nRunning doctor...");
147
427
  const result = doctorCommand(projectDir);
@@ -6,9 +6,10 @@ interface StatusOutput {
6
6
  branch: string;
7
7
  mode: string;
8
8
  classification?: string;
9
+ modelPolicy: string;
9
10
  status: string;
10
11
  currentPhase: string | null;
11
- currentSubStage: string | null;
12
+ currentStep: string | null;
12
13
  started: string;
13
14
  phases: Record<string, { status: string; completed: number; total: number; active: number }>;
14
15
  loopbackCount: number;
@@ -26,11 +27,11 @@ export function statusCommand(worktreeRoot?: string): StatusOutput {
26
27
  for (const phase of PHASE_NAMES) {
27
28
  const ps = state.phases[phase];
28
29
  let completed = 0, total = 0, active = 0;
29
- for (const ss of Object.values(ps.subStages)) {
30
- if (ss.status === "skipped") continue;
30
+ for (const s of Object.values(ps.steps)) {
31
+ if (s.status === "skipped") continue;
31
32
  total++;
32
- if (ss.status === "completed") completed++;
33
- else if (ss.status === "in-progress" || ss.status === "waiting") active++;
33
+ if (s.status === "completed") completed++;
34
+ else if (s.status === "in-progress" || s.status === "waiting") active++;
34
35
  }
35
36
  phases[phase] = { status: ps.status, completed, total, active };
36
37
  }
@@ -40,9 +41,10 @@ export function statusCommand(worktreeRoot?: string): StatusOutput {
40
41
  branch: state.branch,
41
42
  mode: state.mode,
42
43
  ...(state.classification && { classification: state.classification }),
44
+ modelPolicy: state.modelPolicy ?? "auto",
43
45
  status: state.status,
44
46
  currentPhase: state.currentPhase,
45
- currentSubStage: state.currentSubStage,
47
+ currentStep: state.currentStep,
46
48
  started: state.started,
47
49
  phases,
48
50
  loopbackCount: state.loopbacks.length,
@@ -2,9 +2,14 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import * as readline from "node:readline";
4
4
  import { bold, dim, green, red, yellow } from "../utils/colors.js";
5
+ import { PHASE_NAMES } from "../state/schema.js";
6
+ import { SKILL_DIR_PREFIX } from "../config/constants.js";
7
+ import { STATE_DIR, STATE_FILE } from "../state/store.js";
5
8
 
6
9
  const WORK_KIT_SKILLS = [
7
- "full-kit", "auto-kit", "wk-plan", "wk-build", "wk-test", "wk-review", "wk-deploy", "wk-wrap-up",
10
+ "full-kit", "auto-kit", "cancel-kit", "pause-kit", "resume-kit",
11
+ ...PHASE_NAMES.map(p => `${SKILL_DIR_PREFIX}${p}`),
12
+ `${SKILL_DIR_PREFIX}bootstrap`,
8
13
  ];
9
14
 
10
15
  async function promptUser(question: string): Promise<string> {
@@ -52,9 +57,9 @@ export async function uninstallCommand(targetPath?: string): Promise<void> {
52
57
  }
53
58
 
54
59
  // Check for active state
55
- const trackerFile = path.join(projectDir, ".work-kit", "tracker.json");
60
+ const trackerFile = path.join(projectDir, STATE_DIR, STATE_FILE);
56
61
  if (fs.existsSync(trackerFile)) {
57
- console.error(yellow("\nWarning: Active work-kit state found (.work-kit/tracker.json)."));
62
+ console.error(yellow(`\nWarning: Active work-kit state found (${STATE_DIR}/${STATE_FILE}).`));
58
63
  console.error(yellow("Uninstalling will not remove in-progress state files."));
59
64
  }
60
65
 
@@ -1,11 +1,13 @@
1
1
  import { readState, writeState, findWorktreeRoot } from "../state/store.js";
2
- import { SUBSTAGES_BY_PHASE, PHASE_NAMES } from "../state/schema.js";
2
+ import { STEPS_BY_PHASE, PHASE_NAMES, MODE_AUTO, PhaseName } from "../state/schema.js";
3
3
  import { parseLocation } from "../state/helpers.js";
4
+ import { resolveModel } from "../config/model-routing.js";
4
5
  import type { Action } from "../state/schema.js";
5
6
 
6
7
  interface WorkflowStatus {
7
8
  action: "workflow_status";
8
- workflow: { step: string; status: string }[];
9
+ modelPolicy: string;
10
+ workflow: { step: string; status: string; model?: string }[];
9
11
  }
10
12
 
11
13
  export type WorkflowResult = Action | WorkflowStatus;
@@ -22,7 +24,7 @@ export function workflowCommand(opts: {
22
24
 
23
25
  const state = readState(root);
24
26
 
25
- if (state.mode !== "auto-kit") {
27
+ if (state.mode !== MODE_AUTO) {
26
28
  return { action: "error", message: "Workflow management is only available in auto-kit mode." };
27
29
  }
28
30
 
@@ -31,13 +33,13 @@ export function workflowCommand(opts: {
31
33
  }
32
34
 
33
35
  if (opts.add) {
34
- const { phase, subStage } = parseLocation(opts.add);
36
+ const { phase, step } = parseLocation(opts.add);
35
37
 
36
- if (!PHASE_NAMES.includes(phase) || !SUBSTAGES_BY_PHASE[phase].includes(subStage)) {
38
+ if (!PHASE_NAMES.includes(phase) || !STEPS_BY_PHASE[phase].includes(step)) {
37
39
  return { action: "error", message: `Invalid step: ${opts.add}` };
38
40
  }
39
41
 
40
- const existing = state.workflow.find((s) => s.phase === phase && s.subStage === subStage);
42
+ const existing = state.workflow.find((s) => s.phase === phase && s.step === step);
41
43
  if (existing) {
42
44
  if (existing.included) {
43
45
  return { action: "error", message: `${opts.add} is already in the workflow.` };
@@ -45,31 +47,31 @@ export function workflowCommand(opts: {
45
47
  existing.included = true;
46
48
  } else {
47
49
  const phaseIdx = PHASE_NAMES.indexOf(phase);
48
- const subStageIdx = SUBSTAGES_BY_PHASE[phase].indexOf(subStage);
50
+ const stepIdx = STEPS_BY_PHASE[phase].indexOf(step);
49
51
 
50
52
  let insertIdx = state.workflow.length;
51
53
  for (let i = 0; i < state.workflow.length; i++) {
52
54
  const wi = state.workflow[i];
53
55
  const wiPhaseIdx = PHASE_NAMES.indexOf(wi.phase);
54
- const wiSubIdx = SUBSTAGES_BY_PHASE[wi.phase].indexOf(wi.subStage);
56
+ const wiStepIdx = STEPS_BY_PHASE[wi.phase].indexOf(wi.step);
55
57
 
56
- if (wiPhaseIdx > phaseIdx || (wiPhaseIdx === phaseIdx && wiSubIdx > subStageIdx)) {
58
+ if (wiPhaseIdx > phaseIdx || (wiPhaseIdx === phaseIdx && wiStepIdx > stepIdx)) {
57
59
  insertIdx = i;
58
60
  break;
59
61
  }
60
62
  }
61
63
 
62
- state.workflow.splice(insertIdx, 0, { phase, subStage, included: true });
64
+ state.workflow.splice(insertIdx, 0, { phase, step, included: true });
63
65
  }
64
66
 
65
- const currentSS = state.phases[phase].subStages[subStage];
66
- if (currentSS?.status === "completed") {
67
+ const currentStep = state.phases[phase].steps[step];
68
+ if (currentStep?.status === "completed") {
67
69
  return { action: "error", message: `Cannot add ${opts.add} — it's already completed.` };
68
70
  }
69
- if (!currentSS) {
70
- state.phases[phase].subStages[subStage] = { status: "pending" };
71
- } else if (currentSS.status === "skipped") {
72
- currentSS.status = "pending";
71
+ if (!currentStep) {
72
+ state.phases[phase].steps[step] = { status: "pending" };
73
+ } else if (currentStep.status === "skipped") {
74
+ currentStep.status = "pending";
73
75
  }
74
76
 
75
77
  if (state.phases[phase].status === "skipped") {
@@ -81,28 +83,28 @@ export function workflowCommand(opts: {
81
83
  }
82
84
 
83
85
  if (opts.remove) {
84
- const { phase, subStage } = parseLocation(opts.remove);
86
+ const { phase, step } = parseLocation(opts.remove);
85
87
 
86
- const step = state.workflow.find((s) => s.phase === phase && s.subStage === subStage);
87
- if (!step) {
88
+ const ws = state.workflow.find((s) => s.phase === phase && s.step === step);
89
+ if (!ws) {
88
90
  return { action: "error", message: `${opts.remove} is not in the workflow.` };
89
91
  }
90
92
 
91
- const ssState = state.phases[phase]?.subStages[subStage];
92
- if (ssState?.status === "completed") {
93
+ const stepState = state.phases[phase]?.steps[step];
94
+ if (stepState?.status === "completed") {
93
95
  return { action: "error", message: `Cannot remove ${opts.remove} — it's already completed.` };
94
96
  }
95
- if (ssState?.status === "in-progress") {
97
+ if (stepState?.status === "in-progress") {
96
98
  return { action: "error", message: `Cannot remove ${opts.remove} — it's currently in progress.` };
97
99
  }
98
100
 
99
- step.included = false;
101
+ ws.included = false;
100
102
 
101
- if (ssState) {
102
- ssState.status = "skipped";
103
+ if (stepState) {
104
+ stepState.status = "skipped";
103
105
  }
104
106
 
105
- const allSkipped = Object.values(state.phases[phase].subStages).every(
107
+ const allSkipped = Object.values(state.phases[phase].steps).every(
106
108
  (s) => s.status === "skipped"
107
109
  );
108
110
  if (allSkipped) {
@@ -113,13 +115,21 @@ export function workflowCommand(opts: {
113
115
  return { action: "wait_for_user", message: `Removed ${opts.remove} from workflow.` };
114
116
  }
115
117
 
116
- // No add/remove — show current workflow
118
+ // No add/remove — show current workflow with resolved model per step
117
119
  const workflow = state.workflow
118
120
  .filter((s) => s.included)
119
- .map((s) => ({
120
- step: `${s.phase}/${s.subStage}`,
121
- status: state.phases[s.phase]?.subStages[s.subStage]?.status || "unknown",
122
- }));
123
-
124
- return { action: "workflow_status", workflow };
121
+ .map((s) => {
122
+ const model = resolveModel(state, s.phase as PhaseName, s.step);
123
+ return {
124
+ step: `${s.phase}/${s.step}`,
125
+ status: state.phases[s.phase]?.steps[s.step]?.status || "unknown",
126
+ ...(model && { model }),
127
+ };
128
+ });
129
+
130
+ return {
131
+ action: "workflow_status",
132
+ modelPolicy: state.modelPolicy ?? "auto",
133
+ workflow,
134
+ };
125
135
  }
@@ -1,7 +1,7 @@
1
1
  import { PhaseName } from "../state/schema.js";
2
2
 
3
3
  /**
4
- * Maps each phase/sub-stage to the sections it needs from state.md.
4
+ * Maps each phase/step to the sections it needs from state.md.
5
5
  * "##" prefix = top-level section, "###" prefix = Final section.
6
6
  */
7
7
 
@@ -32,14 +32,14 @@ export const PHASE_CONTEXT: Record<PhaseName, AgentContext> = {
32
32
  },
33
33
  };
34
34
 
35
- // Sub-stage-level context (for parallel sub-agents that need specific sections)
36
- export const SUBSTAGE_CONTEXT: Record<string, AgentContext> = {
37
- // Test sub-agents
35
+ // Step-level context (for parallel sub-agents that need specific sections)
36
+ export const STEP_CONTEXT: Record<string, AgentContext> = {
37
+ // Test steps
38
38
  "test/verify": { sections: ["### Build: Final", "## Criteria"] },
39
39
  "test/e2e": { sections: ["### Build: Final", "### Plan: Final"] },
40
40
  "test/validate": { sections: ["### Test: Verify", "### Test: E2E", "## Criteria"] },
41
41
 
42
- // Review sub-agents
42
+ // Review steps
43
43
  "review/self-review": { sections: ["### Build: Final"], needsGitDiff: true },
44
44
  "review/security": { sections: ["### Build: Final"], needsGitDiff: true },
45
45
  "review/performance": { sections: ["### Build: Final"], needsGitDiff: true },
@@ -53,10 +53,10 @@ export const SUBSTAGE_CONTEXT: Record<string, AgentContext> = {
53
53
  },
54
54
  };
55
55
 
56
- export function getContextFor(phase: PhaseName, subStage?: string): AgentContext {
57
- if (subStage) {
58
- const key = `${phase}/${subStage}`;
59
- if (SUBSTAGE_CONTEXT[key]) return SUBSTAGE_CONTEXT[key];
56
+ export function getContextFor(phase: PhaseName, step?: string): AgentContext {
57
+ if (step) {
58
+ const key = `${phase}/${step}`;
59
+ if (STEP_CONTEXT[key]) return STEP_CONTEXT[key];
60
60
  }
61
61
  return PHASE_CONTEXT[phase];
62
62
  }
@@ -0,0 +1,54 @@
1
+ // ── Archive Paths ───────────────────────────────────────────────────
2
+
3
+ export const TRACKER_DIR = ".work-kit-tracker";
4
+ export const ARCHIVE_DIR = "archive";
5
+ export const INDEX_FILE = "index.md";
6
+ export const SUMMARY_FILE = "summary.md";
7
+
8
+ // ── Git ─────────────────────────────────────────────────────────────
9
+
10
+ export const BRANCH_PREFIX = "feature/";
11
+
12
+ // ── Skills ──────────────────────────────────────────────────────────
13
+
14
+ export const SKILL_DIR_PREFIX = "wk-";
15
+
16
+ // ── CLI ─────────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Binary used in onComplete actions emitted to the orchestrator.
20
+ * Resolves on PATH after `npm install -g work-kit-cli`.
21
+ */
22
+ export const CLI_BINARY = "work-kit";
23
+
24
+ /**
25
+ * Fallback / install messaging form. Used in error and setup output.
26
+ */
27
+ export const CLI_NPX_BINARY = "npx work-kit-cli";
28
+
29
+ // ── Project Config ──────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Optional project-level config file. Lives at the main repo root and
33
+ * lets a project override workflow defaults, parallel groups, etc.
34
+ */
35
+ export const PROJECT_CONFIG_FILE = ".work-kit-config.json";
36
+
37
+ // ── Knowledge ───────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Project knowledge directory at the main repo root. Holds curated
41
+ * lessons/conventions/risks/workflow files. Committed to git; only
42
+ * KNOWLEDGE_LOCK is gitignored.
43
+ */
44
+ export const KNOWLEDGE_DIR = ".work-kit-knowledge";
45
+ export const KNOWLEDGE_LOCK = ".lock";
46
+
47
+ // ── Limits ──────────────────────────────────────────────────────────
48
+
49
+ export const MAX_LOOPBACKS_PER_ROUTE = 2;
50
+
51
+ // ── Staleness ───────────────────────────────────────────────────────
52
+
53
+ /** Threshold (ms) after which an in-progress state is considered stale. */
54
+ export const STALE_THRESHOLD_MS = 60 * 60 * 1000;