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.
- package/README.md +24 -13
- package/cli/src/commands/bootstrap.test.ts +40 -0
- package/cli/src/commands/bootstrap.ts +77 -13
- package/cli/src/commands/cancel.ts +1 -16
- package/cli/src/commands/complete.ts +92 -98
- package/cli/src/commands/completions.ts +2 -2
- package/cli/src/commands/doctor.ts +1 -1
- package/cli/src/commands/extract.ts +217 -0
- package/cli/src/commands/init.test.ts +50 -0
- package/cli/src/commands/init.ts +70 -35
- package/cli/src/commands/learn.test.ts +217 -0
- package/cli/src/commands/learn.ts +104 -0
- package/cli/src/commands/loopback.ts +8 -11
- package/cli/src/commands/next.ts +93 -60
- package/cli/src/commands/observe.ts +16 -21
- package/cli/src/commands/pause-resume.test.ts +142 -0
- package/cli/src/commands/pause.ts +34 -0
- package/cli/src/commands/report.ts +217 -0
- package/cli/src/commands/resume.ts +126 -0
- package/cli/src/commands/setup.ts +280 -0
- package/cli/src/commands/status.ts +8 -6
- package/cli/src/commands/uninstall.ts +8 -3
- package/cli/src/commands/workflow.ts +43 -33
- package/cli/src/config/agent-map.ts +9 -9
- package/cli/src/config/constants.ts +54 -0
- package/cli/src/config/loopback-routes.ts +13 -13
- package/cli/src/config/model-routing.test.ts +190 -0
- package/cli/src/config/model-routing.ts +208 -0
- package/cli/src/config/project-config.test.ts +127 -0
- package/cli/src/config/project-config.ts +106 -0
- package/cli/src/config/{phases.ts → workflow.ts} +40 -23
- package/cli/src/context/prompt-builder.ts +10 -9
- package/cli/src/index.ts +130 -9
- package/cli/src/observer/data.ts +196 -65
- package/cli/src/observer/renderer.ts +127 -107
- package/cli/src/observer/watcher.ts +28 -16
- package/cli/src/state/helpers.test.ts +28 -28
- package/cli/src/state/helpers.ts +37 -25
- package/cli/src/state/schema.ts +135 -45
- package/cli/src/state/store.ts +127 -7
- package/cli/src/state/validators.test.ts +13 -13
- package/cli/src/state/validators.ts +3 -4
- package/cli/src/utils/colors.ts +2 -0
- package/cli/src/utils/fs.ts +13 -0
- package/cli/src/utils/json.ts +20 -0
- package/cli/src/utils/knowledge.ts +471 -0
- package/cli/src/utils/time.ts +27 -0
- package/cli/src/{engine → workflow}/loopbacks.test.ts +2 -2
- package/cli/src/workflow/loopbacks.ts +42 -0
- package/cli/src/workflow/parallel.ts +64 -0
- package/cli/src/workflow/transitions.test.ts +129 -0
- package/cli/src/{engine → workflow}/transitions.ts +18 -22
- package/package.json +2 -2
- package/skills/auto-kit/SKILL.md +44 -27
- package/skills/cancel-kit/SKILL.md +4 -4
- package/skills/full-kit/SKILL.md +45 -28
- package/skills/pause-kit/SKILL.md +25 -0
- package/skills/resume-kit/SKILL.md +64 -0
- package/skills/wk-bootstrap/SKILL.md +11 -5
- package/skills/wk-build/SKILL.md +12 -11
- package/skills/wk-build/{stages → steps}/commit.md +1 -1
- package/skills/wk-build/{stages → steps}/core.md +3 -3
- package/skills/wk-build/{stages → steps}/integration.md +2 -2
- package/skills/wk-build/{stages → steps}/migration.md +1 -1
- package/skills/wk-build/{stages → steps}/red.md +1 -1
- package/skills/wk-build/{stages → steps}/refactor.md +1 -1
- package/skills/wk-build/{stages → steps}/setup.md +1 -1
- package/skills/wk-build/{stages → steps}/ui.md +1 -1
- package/skills/wk-deploy/SKILL.md +7 -6
- package/skills/wk-deploy/{stages → steps}/merge.md +1 -1
- package/skills/wk-deploy/{stages → steps}/monitor.md +1 -1
- package/skills/wk-deploy/{stages → steps}/remediate.md +1 -1
- package/skills/wk-plan/SKILL.md +15 -14
- package/skills/wk-plan/{stages → steps}/architecture.md +1 -1
- package/skills/wk-plan/{stages → steps}/audit.md +2 -2
- package/skills/wk-plan/{stages → steps}/blueprint.md +2 -2
- package/skills/wk-plan/{stages → steps}/clarify.md +1 -1
- package/skills/wk-plan/{stages → steps}/investigate.md +1 -1
- package/skills/wk-plan/{stages → steps}/scope.md +1 -1
- package/skills/wk-plan/{stages → steps}/sketch.md +1 -1
- package/skills/wk-plan/{stages → steps}/ux-flow.md +1 -1
- package/skills/wk-review/SKILL.md +11 -10
- package/skills/wk-review/{stages → steps}/compliance.md +1 -1
- package/skills/wk-review/{stages → steps}/handoff.md +2 -2
- package/skills/wk-review/{stages → steps}/performance.md +1 -1
- package/skills/wk-review/{stages → steps}/security.md +1 -1
- package/skills/wk-review/{stages → steps}/self-review.md +1 -1
- package/skills/wk-test/SKILL.md +9 -8
- package/skills/wk-test/steps/e2e.md +56 -0
- package/skills/wk-test/{stages → steps}/validate.md +1 -1
- package/skills/wk-test/{stages → steps}/verify.md +1 -1
- package/skills/wk-wrap-up/SKILL.md +19 -5
- package/skills/wk-wrap-up/steps/knowledge.md +76 -0
- package/skills/wk-wrap-up/steps/summary.md +86 -0
- package/cli/src/engine/loopbacks.ts +0 -32
- package/cli/src/engine/parallel.ts +0 -60
- package/cli/src/engine/transitions.test.ts +0 -129
- package/skills/wk-test/stages/e2e.md +0 -53
- /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
|
-
|
|
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
|
|
30
|
-
if (
|
|
30
|
+
for (const s of Object.values(ps.steps)) {
|
|
31
|
+
if (s.status === "skipped") continue;
|
|
31
32
|
total++;
|
|
32
|
-
if (
|
|
33
|
-
else if (
|
|
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
|
-
|
|
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", "
|
|
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,
|
|
60
|
+
const trackerFile = path.join(projectDir, STATE_DIR, STATE_FILE);
|
|
56
61
|
if (fs.existsSync(trackerFile)) {
|
|
57
|
-
console.error(yellow(
|
|
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 {
|
|
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
|
-
|
|
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 !==
|
|
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,
|
|
36
|
+
const { phase, step } = parseLocation(opts.add);
|
|
35
37
|
|
|
36
|
-
if (!PHASE_NAMES.includes(phase) || !
|
|
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.
|
|
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
|
|
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
|
|
56
|
+
const wiStepIdx = STEPS_BY_PHASE[wi.phase].indexOf(wi.step);
|
|
55
57
|
|
|
56
|
-
if (wiPhaseIdx > phaseIdx || (wiPhaseIdx === phaseIdx &&
|
|
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,
|
|
64
|
+
state.workflow.splice(insertIdx, 0, { phase, step, included: true });
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
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 (!
|
|
70
|
-
state.phases[phase].
|
|
71
|
-
} else if (
|
|
72
|
-
|
|
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,
|
|
86
|
+
const { phase, step } = parseLocation(opts.remove);
|
|
85
87
|
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
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
|
|
92
|
-
if (
|
|
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 (
|
|
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
|
-
|
|
101
|
+
ws.included = false;
|
|
100
102
|
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
+
if (stepState) {
|
|
104
|
+
stepState.status = "skipped";
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
const allSkipped = Object.values(state.phases[phase].
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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/
|
|
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
|
-
//
|
|
36
|
-
export const
|
|
37
|
-
// Test
|
|
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
|
|
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,
|
|
57
|
-
if (
|
|
58
|
-
const key = `${phase}/${
|
|
59
|
-
if (
|
|
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;
|