work-kit-cli 0.2.7 → 0.3.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 (86) hide show
  1. package/README.md +13 -13
  2. package/cli/src/commands/bootstrap.ts +39 -13
  3. package/cli/src/commands/cancel.ts +1 -16
  4. package/cli/src/commands/complete.ts +92 -98
  5. package/cli/src/commands/completions.ts +2 -2
  6. package/cli/src/commands/doctor.ts +1 -1
  7. package/cli/src/commands/init.ts +40 -32
  8. package/cli/src/commands/loopback.ts +8 -11
  9. package/cli/src/commands/next.ts +64 -51
  10. package/cli/src/commands/pause-resume.test.ts +142 -0
  11. package/cli/src/commands/pause.ts +34 -0
  12. package/cli/src/commands/report.ts +217 -0
  13. package/cli/src/commands/resume.ts +38 -0
  14. package/cli/src/commands/setup.ts +136 -0
  15. package/cli/src/commands/status.ts +6 -6
  16. package/cli/src/commands/uninstall.ts +8 -3
  17. package/cli/src/commands/workflow.ts +27 -27
  18. package/cli/src/config/agent-map.ts +9 -9
  19. package/cli/src/config/constants.ts +44 -0
  20. package/cli/src/config/loopback-routes.ts +13 -13
  21. package/cli/src/config/project-config.test.ts +127 -0
  22. package/cli/src/config/project-config.ts +106 -0
  23. package/cli/src/config/{phases.ts → workflow.ts} +40 -23
  24. package/cli/src/context/prompt-builder.ts +10 -9
  25. package/cli/src/index.ts +63 -7
  26. package/cli/src/observer/data.ts +64 -56
  27. package/cli/src/observer/renderer.ts +162 -75
  28. package/cli/src/state/helpers.test.ts +28 -28
  29. package/cli/src/state/helpers.ts +37 -25
  30. package/cli/src/state/schema.ts +88 -45
  31. package/cli/src/state/store.ts +92 -7
  32. package/cli/src/state/validators.test.ts +13 -13
  33. package/cli/src/state/validators.ts +3 -4
  34. package/cli/src/utils/colors.ts +2 -0
  35. package/cli/src/utils/json.ts +20 -0
  36. package/cli/src/utils/time.ts +27 -0
  37. package/cli/src/{engine → workflow}/loopbacks.test.ts +2 -2
  38. package/cli/src/workflow/loopbacks.ts +42 -0
  39. package/cli/src/workflow/parallel.ts +64 -0
  40. package/cli/src/workflow/transitions.test.ts +129 -0
  41. package/cli/src/{engine → workflow}/transitions.ts +18 -22
  42. package/package.json +2 -2
  43. package/skills/auto-kit/SKILL.md +22 -22
  44. package/skills/cancel-kit/SKILL.md +4 -4
  45. package/skills/full-kit/SKILL.md +23 -23
  46. package/skills/pause-kit/SKILL.md +25 -0
  47. package/skills/resume-kit/SKILL.md +28 -0
  48. package/skills/wk-bootstrap/SKILL.md +5 -5
  49. package/skills/wk-build/SKILL.md +10 -10
  50. package/skills/wk-build/{stages → steps}/commit.md +1 -1
  51. package/skills/wk-build/{stages → steps}/core.md +3 -3
  52. package/skills/wk-build/{stages → steps}/integration.md +2 -2
  53. package/skills/wk-build/{stages → steps}/migration.md +1 -1
  54. package/skills/wk-build/{stages → steps}/red.md +1 -1
  55. package/skills/wk-build/{stages → steps}/refactor.md +1 -1
  56. package/skills/wk-build/{stages → steps}/setup.md +1 -1
  57. package/skills/wk-build/{stages → steps}/ui.md +1 -1
  58. package/skills/wk-deploy/SKILL.md +6 -6
  59. package/skills/wk-deploy/{stages → steps}/merge.md +1 -1
  60. package/skills/wk-deploy/{stages → steps}/monitor.md +1 -1
  61. package/skills/wk-deploy/{stages → steps}/remediate.md +1 -1
  62. package/skills/wk-plan/SKILL.md +13 -13
  63. package/skills/wk-plan/{stages → steps}/architecture.md +1 -1
  64. package/skills/wk-plan/{stages → steps}/audit.md +2 -2
  65. package/skills/wk-plan/{stages → steps}/blueprint.md +2 -2
  66. package/skills/wk-plan/{stages → steps}/clarify.md +1 -1
  67. package/skills/wk-plan/{stages → steps}/investigate.md +1 -1
  68. package/skills/wk-plan/{stages → steps}/scope.md +1 -1
  69. package/skills/wk-plan/{stages → steps}/sketch.md +1 -1
  70. package/skills/wk-plan/{stages → steps}/ux-flow.md +1 -1
  71. package/skills/wk-review/SKILL.md +10 -10
  72. package/skills/wk-review/{stages → steps}/compliance.md +1 -1
  73. package/skills/wk-review/{stages → steps}/handoff.md +2 -2
  74. package/skills/wk-review/{stages → steps}/performance.md +1 -1
  75. package/skills/wk-review/{stages → steps}/security.md +1 -1
  76. package/skills/wk-review/{stages → steps}/self-review.md +1 -1
  77. package/skills/wk-test/SKILL.md +8 -8
  78. package/skills/wk-test/{stages → steps}/e2e.md +1 -1
  79. package/skills/wk-test/{stages → steps}/validate.md +1 -1
  80. package/skills/wk-test/{stages → steps}/verify.md +1 -1
  81. package/skills/wk-wrap-up/SKILL.md +6 -5
  82. package/skills/wk-wrap-up/steps/summary.md +86 -0
  83. package/cli/src/engine/loopbacks.ts +0 -32
  84. package/cli/src/engine/parallel.ts +0 -60
  85. package/cli/src/engine/transitions.test.ts +0 -129
  86. /package/cli/src/{engine/phases.ts → workflow/gates.ts} +0 -0
@@ -1,45 +1,45 @@
1
- import { PhaseName, Location } from "../state/schema.js";
1
+ import { Location, StepOutcome } from "../state/schema.js";
2
2
 
3
3
  /**
4
- * Defines when a completed sub-stage should trigger a loop-back
4
+ * Defines when a completed step should trigger a loop-back
5
5
  * based on its outcome.
6
6
  */
7
7
  export interface LoopbackRoute {
8
8
  from: Location;
9
- triggerOutcome: string;
9
+ triggerOutcome: StepOutcome;
10
10
  to: Location;
11
11
  reason: string;
12
12
  }
13
13
 
14
14
  export const LOOPBACK_ROUTES: LoopbackRoute[] = [
15
15
  {
16
- from: { phase: "plan", subStage: "audit" },
16
+ from: { phase: "plan", step: "audit" },
17
17
  triggerOutcome: "revise",
18
- to: { phase: "plan", subStage: "blueprint" },
18
+ to: { phase: "plan", step: "blueprint" },
19
19
  reason: "Audit found gaps — revising Blueprint",
20
20
  },
21
21
  {
22
- from: { phase: "build", subStage: "refactor" },
22
+ from: { phase: "build", step: "refactor" },
23
23
  triggerOutcome: "broken",
24
- to: { phase: "build", subStage: "core" },
24
+ to: { phase: "build", step: "core" },
25
25
  reason: "Refactor broke tests — re-running Core to fix",
26
26
  },
27
27
  {
28
- from: { phase: "review", subStage: "handoff" },
28
+ from: { phase: "review", step: "handoff" },
29
29
  triggerOutcome: "changes_requested",
30
- to: { phase: "build", subStage: "core" },
30
+ to: { phase: "build", step: "core" },
31
31
  reason: "Review requested changes — looping back to Build/Core",
32
32
  },
33
33
  {
34
- from: { phase: "deploy", subStage: "merge" },
34
+ from: { phase: "deploy", step: "merge" },
35
35
  triggerOutcome: "fix_needed",
36
- to: { phase: "build", subStage: "core" },
36
+ to: { phase: "build", step: "core" },
37
37
  reason: "Merge blocked — fix needed in Build/Core",
38
38
  },
39
39
  {
40
- from: { phase: "deploy", subStage: "remediate" },
40
+ from: { phase: "deploy", step: "remediate" },
41
41
  triggerOutcome: "fix_and_redeploy",
42
- to: { phase: "build", subStage: "core" },
42
+ to: { phase: "build", step: "core" },
43
43
  reason: "Deployment issue — fix and redeploy from Build/Core",
44
44
  },
45
45
  ];
@@ -0,0 +1,127 @@
1
+ import { describe, it, afterEach } from "node:test";
2
+ import * as assert from "node:assert/strict";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import * as os from "node:os";
6
+ import { randomUUID } from "node:crypto";
7
+ import { loadProjectConfig } from "./project-config.js";
8
+ import { resolveParallelGroups, DEFAULT_PARALLEL_GROUPS } from "../workflow/parallel.js";
9
+ import { PROJECT_CONFIG_FILE } from "./constants.js";
10
+
11
+ function makeTmpDir(): string {
12
+ const dir = path.join(os.tmpdir(), `wk-config-${randomUUID()}`);
13
+ fs.mkdirSync(dir, { recursive: true });
14
+ return dir;
15
+ }
16
+
17
+ let tmpDirs: string[] = [];
18
+
19
+ afterEach(() => {
20
+ for (const dir of tmpDirs) fs.rmSync(dir, { recursive: true, force: true });
21
+ tmpDirs = [];
22
+ });
23
+
24
+ describe("loadProjectConfig", () => {
25
+ it("returns empty config when file is missing", () => {
26
+ const tmp = makeTmpDir();
27
+ tmpDirs.push(tmp);
28
+ const config = loadProjectConfig(tmp);
29
+ assert.deepStrictEqual(config, {});
30
+ });
31
+
32
+ it("loads valid defaults", () => {
33
+ const tmp = makeTmpDir();
34
+ tmpDirs.push(tmp);
35
+ fs.writeFileSync(
36
+ path.join(tmp, PROJECT_CONFIG_FILE),
37
+ JSON.stringify({ defaults: { mode: "auto", classification: "feature", gated: true } }),
38
+ );
39
+ const config = loadProjectConfig(tmp);
40
+ assert.equal(config.defaults?.mode, "auto");
41
+ assert.equal(config.defaults?.classification, "feature");
42
+ assert.equal(config.defaults?.gated, true);
43
+ });
44
+
45
+ it("ignores invalid mode and classification", () => {
46
+ const tmp = makeTmpDir();
47
+ tmpDirs.push(tmp);
48
+ fs.writeFileSync(
49
+ path.join(tmp, PROJECT_CONFIG_FILE),
50
+ JSON.stringify({ defaults: { mode: "wat", classification: "nope" } }),
51
+ );
52
+ const config = loadProjectConfig(tmp);
53
+ assert.equal(config.defaults?.mode, undefined);
54
+ assert.equal(config.defaults?.classification, undefined);
55
+ });
56
+
57
+ it("loads parallel group overrides", () => {
58
+ const tmp = makeTmpDir();
59
+ tmpDirs.push(tmp);
60
+ fs.writeFileSync(
61
+ path.join(tmp, PROJECT_CONFIG_FILE),
62
+ JSON.stringify({
63
+ parallel: {
64
+ test: { parallel: ["verify"], thenSequential: "validate" },
65
+ },
66
+ }),
67
+ );
68
+ const config = loadProjectConfig(tmp);
69
+ assert.deepStrictEqual(config.parallel?.test, { parallel: ["verify"], thenSequential: "validate" });
70
+ });
71
+
72
+ it("filters invalid step names from parallel overrides", () => {
73
+ const tmp = makeTmpDir();
74
+ tmpDirs.push(tmp);
75
+ fs.writeFileSync(
76
+ path.join(tmp, PROJECT_CONFIG_FILE),
77
+ JSON.stringify({
78
+ parallel: { test: { parallel: ["verify", "made-up"] } },
79
+ }),
80
+ );
81
+ const config = loadProjectConfig(tmp);
82
+ assert.deepStrictEqual(config.parallel?.test?.parallel, ["verify"]);
83
+ });
84
+
85
+ it("validates workflow include/exclude refs", () => {
86
+ const tmp = makeTmpDir();
87
+ tmpDirs.push(tmp);
88
+ fs.writeFileSync(
89
+ path.join(tmp, PROJECT_CONFIG_FILE),
90
+ JSON.stringify({
91
+ workflow: { include: ["review/security", "bogus/step"], exclude: ["test/e2e"] },
92
+ }),
93
+ );
94
+ const config = loadProjectConfig(tmp);
95
+ assert.deepStrictEqual(config.workflow?.include, ["review/security"]);
96
+ assert.deepStrictEqual(config.workflow?.exclude, ["test/e2e"]);
97
+ });
98
+
99
+ it("survives invalid JSON", () => {
100
+ const tmp = makeTmpDir();
101
+ tmpDirs.push(tmp);
102
+ fs.writeFileSync(path.join(tmp, PROJECT_CONFIG_FILE), "not json");
103
+ const config = loadProjectConfig(tmp);
104
+ assert.deepStrictEqual(config, {});
105
+ });
106
+ });
107
+
108
+ describe("resolveParallelGroups", () => {
109
+ it("returns defaults with no project root", () => {
110
+ assert.deepStrictEqual(resolveParallelGroups(), DEFAULT_PARALLEL_GROUPS);
111
+ });
112
+
113
+ it("merges project overrides over defaults", () => {
114
+ const tmp = makeTmpDir();
115
+ tmpDirs.push(tmp);
116
+ fs.writeFileSync(
117
+ path.join(tmp, PROJECT_CONFIG_FILE),
118
+ JSON.stringify({
119
+ parallel: { test: { parallel: ["verify"], thenSequential: "validate" } },
120
+ }),
121
+ );
122
+ const groups = resolveParallelGroups(tmp);
123
+ assert.deepStrictEqual(groups.test, { parallel: ["verify"], thenSequential: "validate" });
124
+ // review (not overridden) keeps default
125
+ assert.deepStrictEqual(groups.review, DEFAULT_PARALLEL_GROUPS.review);
126
+ });
127
+ });
@@ -0,0 +1,106 @@
1
+ import * as path from "node:path";
2
+ import {
3
+ PHASE_NAMES,
4
+ STEPS_BY_PHASE,
5
+ isClassification,
6
+ type PhaseName,
7
+ type Classification,
8
+ } from "../state/schema.js";
9
+ import { readJsonFile } from "../utils/json.js";
10
+ import { PROJECT_CONFIG_FILE } from "./constants.js";
11
+
12
+ /**
13
+ * Optional `.work-kit-config.json` lives at the main repo root.
14
+ *
15
+ * Example:
16
+ * {
17
+ * "defaults": { "mode": "auto", "classification": "feature", "gated": false },
18
+ * "parallel": {
19
+ * "review": { "parallel": ["self-review", "security"], "thenSequential": "handoff" }
20
+ * },
21
+ * "workflow": {
22
+ * "include": ["review/security"],
23
+ * "exclude": ["review/performance"]
24
+ * }
25
+ * }
26
+ */
27
+ export interface ProjectParallelGroup {
28
+ parallel: string[];
29
+ thenSequential?: string;
30
+ }
31
+
32
+ export interface ProjectConfig {
33
+ defaults?: {
34
+ mode?: "full" | "auto";
35
+ classification?: Classification;
36
+ gated?: boolean;
37
+ };
38
+ /** Override or extend the per-phase parallel groups. */
39
+ parallel?: Partial<Record<PhaseName, ProjectParallelGroup>>;
40
+ /** Force-include or force-exclude specific steps (phase/step). */
41
+ workflow?: {
42
+ include?: string[];
43
+ exclude?: string[];
44
+ };
45
+ }
46
+
47
+ const EMPTY_CONFIG: ProjectConfig = {};
48
+
49
+ /** Load and validate the project config. Returns empty config when missing/invalid. */
50
+ export function loadProjectConfig(mainRepoRoot: string): ProjectConfig {
51
+ const parsed = readJsonFile<unknown>(path.join(mainRepoRoot, PROJECT_CONFIG_FILE));
52
+ if (!parsed) return EMPTY_CONFIG;
53
+ return validateConfig(parsed);
54
+ }
55
+
56
+ function validateConfig(raw: any): ProjectConfig {
57
+ const out: ProjectConfig = {};
58
+
59
+ if (raw && typeof raw === "object") {
60
+ if (raw.defaults && typeof raw.defaults === "object") {
61
+ const d = raw.defaults;
62
+ out.defaults = {};
63
+ if (d.mode === "full" || d.mode === "auto") out.defaults.mode = d.mode;
64
+ if (typeof d.classification === "string" && isClassification(d.classification)) {
65
+ out.defaults.classification = d.classification;
66
+ }
67
+ if (typeof d.gated === "boolean") out.defaults.gated = d.gated;
68
+ }
69
+
70
+ if (raw.parallel && typeof raw.parallel === "object") {
71
+ out.parallel = {};
72
+ for (const [phase, group] of Object.entries(raw.parallel)) {
73
+ if (!(PHASE_NAMES as readonly string[]).includes(phase)) continue;
74
+ const g = group as any;
75
+ if (!g || !Array.isArray(g.parallel)) continue;
76
+ const validSteps = STEPS_BY_PHASE[phase as PhaseName];
77
+ const parallel = g.parallel.filter((s: any) => typeof s === "string" && validSteps.includes(s));
78
+ if (parallel.length === 0) continue;
79
+ const entry: ProjectParallelGroup = { parallel };
80
+ if (typeof g.thenSequential === "string" && validSteps.includes(g.thenSequential)) {
81
+ entry.thenSequential = g.thenSequential;
82
+ }
83
+ out.parallel[phase as PhaseName] = entry;
84
+ }
85
+ }
86
+
87
+ if (raw.workflow && typeof raw.workflow === "object") {
88
+ out.workflow = {};
89
+ for (const key of ["include", "exclude"] as const) {
90
+ if (Array.isArray(raw.workflow[key])) {
91
+ out.workflow[key] = raw.workflow[key].filter((s: any) => isValidStepRef(s));
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ return out;
98
+ }
99
+
100
+ function isValidStepRef(ref: any): boolean {
101
+ if (typeof ref !== "string") return false;
102
+ const [phase, step] = ref.split("/");
103
+ if (!phase || !step) return false;
104
+ if (!(PHASE_NAMES as readonly string[]).includes(phase)) return false;
105
+ return STEPS_BY_PHASE[phase as PhaseName].includes(step);
106
+ }
@@ -1,15 +1,17 @@
1
1
  import {
2
2
  PhaseName,
3
- SUBSTAGES_BY_PHASE,
3
+ PHASE_NAMES,
4
+ STEPS_BY_PHASE,
4
5
  Classification,
5
6
  WorkflowStep,
6
7
  } from "../state/schema.js";
8
+ import { SKILL_DIR_PREFIX } from "./constants.js";
7
9
 
8
- // ── Phase Order ──────────────────────────────────────────────────────
10
+ // ── Phase Order ─────────────────────────────────────────────────────
9
11
 
10
- export const PHASE_ORDER: PhaseName[] = ["plan", "build", "test", "review", "deploy", "wrap-up"];
12
+ export const PHASE_ORDER: PhaseName[] = [...PHASE_NAMES];
11
13
 
12
- // ── Prerequisites ────────────────────────────────────────────────────
14
+ // ── Prerequisites ───────────────────────────────────────────────────
13
15
 
14
16
  export const PHASE_PREREQUISITES: Record<PhaseName, PhaseName | null> = {
15
17
  plan: null,
@@ -20,15 +22,15 @@ export const PHASE_PREREQUISITES: Record<PhaseName, PhaseName | null> = {
20
22
  "wrap-up": "review", // or deploy if deploy was included
21
23
  };
22
24
 
23
- // ── Skill File Paths ─────────────────────────────────────────────────
25
+ // ── Skill File Paths ────────────────────────────────────────────────
24
26
 
25
- export function skillFilePath(phase: PhaseName, subStage?: string): string {
26
- const dir = `wk-${phase}`;
27
- if (!subStage) return `.claude/skills/${dir}/SKILL.md`;
28
- return `.claude/skills/${dir}/stages/${subStage}.md`;
27
+ export function skillFilePath(phase: PhaseName, step?: string): string {
28
+ const dir = `${SKILL_DIR_PREFIX}${phase}`;
29
+ if (!step) return `.claude/skills/${dir}/SKILL.md`;
30
+ return `.claude/skills/${dir}/steps/${step}.md`;
29
31
  }
30
32
 
31
- // ── Auto-kit Default Workflows ───────────────────────────────────────
33
+ // ── Auto-kit Default Workflows ──────────────────────────────────────
32
34
 
33
35
  type InclusionRule = "YES" | "skip" | "if UI" | "if DB" | "optional";
34
36
 
@@ -42,7 +44,7 @@ const WORKFLOW_MATRIX: Record<Classification, Record<string, InclusionRule>> = {
42
44
  "review/self-review": "YES", "review/security": "skip", "review/performance": "skip",
43
45
  "review/compliance": "skip", "review/handoff": "YES",
44
46
  "deploy/merge": "YES", "deploy/monitor": "optional", "deploy/remediate": "optional",
45
- "wrap-up/wrap-up": "YES",
47
+ "wrap-up/summary": "YES",
46
48
  },
47
49
  "small-change": {
48
50
  "plan/clarify": "YES", "plan/investigate": "skip", "plan/sketch": "skip", "plan/scope": "skip",
@@ -53,7 +55,7 @@ const WORKFLOW_MATRIX: Record<Classification, Record<string, InclusionRule>> = {
53
55
  "review/self-review": "YES", "review/security": "skip", "review/performance": "skip",
54
56
  "review/compliance": "skip", "review/handoff": "YES",
55
57
  "deploy/merge": "YES", "deploy/monitor": "optional", "deploy/remediate": "optional",
56
- "wrap-up/wrap-up": "YES",
58
+ "wrap-up/summary": "YES",
57
59
  },
58
60
  refactor: {
59
61
  "plan/clarify": "YES", "plan/investigate": "YES", "plan/sketch": "skip", "plan/scope": "skip",
@@ -64,7 +66,7 @@ const WORKFLOW_MATRIX: Record<Classification, Record<string, InclusionRule>> = {
64
66
  "review/self-review": "YES", "review/security": "skip", "review/performance": "YES",
65
67
  "review/compliance": "skip", "review/handoff": "YES",
66
68
  "deploy/merge": "YES", "deploy/monitor": "optional", "deploy/remediate": "optional",
67
- "wrap-up/wrap-up": "YES",
69
+ "wrap-up/summary": "YES",
68
70
  },
69
71
  feature: {
70
72
  "plan/clarify": "YES", "plan/investigate": "YES", "plan/sketch": "YES", "plan/scope": "YES",
@@ -75,7 +77,7 @@ const WORKFLOW_MATRIX: Record<Classification, Record<string, InclusionRule>> = {
75
77
  "review/self-review": "YES", "review/security": "YES", "review/performance": "skip",
76
78
  "review/compliance": "YES", "review/handoff": "YES",
77
79
  "deploy/merge": "YES", "deploy/monitor": "optional", "deploy/remediate": "optional",
78
- "wrap-up/wrap-up": "YES",
80
+ "wrap-up/summary": "YES",
79
81
  },
80
82
  "large-feature": {
81
83
  "plan/clarify": "YES", "plan/investigate": "YES", "plan/sketch": "YES", "plan/scope": "YES",
@@ -86,20 +88,35 @@ const WORKFLOW_MATRIX: Record<Classification, Record<string, InclusionRule>> = {
86
88
  "review/self-review": "YES", "review/security": "YES", "review/performance": "YES",
87
89
  "review/compliance": "YES", "review/handoff": "YES",
88
90
  "deploy/merge": "YES", "deploy/monitor": "optional", "deploy/remediate": "optional",
89
- "wrap-up/wrap-up": "YES",
91
+ "wrap-up/summary": "YES",
90
92
  },
91
93
  };
92
94
 
93
- export function buildDefaultWorkflow(classification: Classification): WorkflowStep[] {
95
+ export function buildDefaultWorkflow(
96
+ classification: Classification,
97
+ overrides?: { include?: string[]; exclude?: string[] }
98
+ ): WorkflowStep[] {
94
99
  const matrix = WORKFLOW_MATRIX[classification];
95
100
  const steps: WorkflowStep[] = [];
96
101
 
102
+ const forceInclude = new Set(overrides?.include ?? []);
103
+ const forceExclude = new Set(overrides?.exclude ?? []);
104
+
97
105
  for (const [key, rule] of Object.entries(matrix)) {
98
- const [phase, subStage] = key.split("/") as [PhaseName, string];
99
- // "YES" always included, "skip" excluded, conditional ones included by default (user can remove)
100
- const included = rule === "YES" || rule === "if UI" || rule === "if DB";
101
- if (rule !== "skip") {
102
- steps.push({ phase, subStage, included });
106
+ const [phase, step] = key.split("/") as [PhaseName, string];
107
+ let included = rule === "YES" || rule === "if UI" || rule === "if DB";
108
+ if (forceInclude.has(key)) included = true;
109
+ if (forceExclude.has(key)) included = false;
110
+ // "skip" excluded by default, but project config may force-include
111
+ if (rule === "skip" && !forceInclude.has(key)) continue;
112
+ steps.push({ phase, step, included });
113
+ }
114
+
115
+ // Add any force-included steps not in the matrix
116
+ for (const ref of forceInclude) {
117
+ const [phase, step] = ref.split("/") as [PhaseName, string];
118
+ if (!steps.some(s => s.phase === phase && s.step === step)) {
119
+ steps.push({ phase, step, included: true });
103
120
  }
104
121
  }
105
122
 
@@ -109,8 +126,8 @@ export function buildDefaultWorkflow(classification: Classification): WorkflowSt
109
126
  export function buildFullWorkflow(): WorkflowStep[] {
110
127
  const steps: WorkflowStep[] = [];
111
128
  for (const phase of PHASE_ORDER) {
112
- for (const subStage of SUBSTAGES_BY_PHASE[phase]) {
113
- steps.push({ phase, subStage, included: true });
129
+ for (const step of STEPS_BY_PHASE[phase]) {
130
+ steps.push({ phase, step, included: true });
114
131
  }
115
132
  }
116
133
  return steps;
@@ -2,27 +2,28 @@ import { WorkKitState, PhaseName } from "../state/schema.js";
2
2
  import { getContextFor } from "../config/agent-map.js";
3
3
  import { extractSection, extractTopSection } from "./extractor.js";
4
4
  import { readStateMd } from "../state/store.js";
5
- import { skillFilePath } from "../config/phases.js";
5
+ import { skillFilePath } from "../config/workflow.js";
6
6
  import { redactIgnoredBlocks } from "./redactor.js";
7
+ import { CLI_BINARY } from "../config/constants.js";
7
8
 
8
9
  /**
9
- * Build a complete agent prompt for a given phase/sub-stage.
10
+ * Build a complete agent prompt for a given phase/step.
10
11
  * Accepts optional pre-read stateMd to avoid repeated file reads in parallel scenarios.
11
12
  */
12
13
  export function buildAgentPrompt(
13
14
  worktreeRoot: string,
14
15
  state: WorkKitState,
15
16
  phase: PhaseName,
16
- subStage: string,
17
+ step: string,
17
18
  stateMd?: string | null
18
19
  ): string {
19
- const ctx = getContextFor(phase, subStage);
20
+ const ctx = getContextFor(phase, step);
20
21
  const md = stateMd ?? readStateMd(worktreeRoot);
21
- const skill = skillFilePath(phase, subStage);
22
+ const skill = skillFilePath(phase, step);
22
23
 
23
24
  const parts: string[] = [];
24
25
 
25
- parts.push(`# Agent: ${phase}/${subStage}`);
26
+ parts.push(`# Agent: ${phase}/${step}`);
26
27
  parts.push(`**Worktree:** ${worktreeRoot}`);
27
28
  parts.push(`**Slug:** ${state.slug}`);
28
29
  parts.push(`**Branch:** ${state.branch}`);
@@ -59,12 +60,12 @@ export function buildAgentPrompt(
59
60
  }
60
61
 
61
62
  parts.push(`## Output`);
62
- parts.push(`Write your outputs to \`.work-kit/state.md\` under a section for this sub-stage.`);
63
+ parts.push(`Write your outputs to \`.work-kit/state.md\` under a section for this step.`);
63
64
 
64
- if (subStage === "wrap-up") {
65
+ if (step === "wrap-up") {
65
66
  parts.push(`Follow the wrap-up skill file instructions for archiving and cleanup.`);
66
67
  } else {
67
- parts.push(`When done, report your outcome so the orchestrator can run: \`npx work-kit-cli complete ${phase}/${subStage} --outcome <outcome>\``);
68
+ parts.push(`When done, report your outcome so the orchestrator can run: \`${CLI_BINARY} complete ${phase}/${step} --outcome <outcome>\``);
68
69
  }
69
70
 
70
71
  return redactIgnoredBlocks(parts.join("\n"));
package/cli/src/index.ts CHANGED
@@ -17,6 +17,9 @@ import { observeCommand } from "./commands/observe.js";
17
17
  import { uninstallCommand } from "./commands/uninstall.js";
18
18
  import { bootstrapCommand } from "./commands/bootstrap.js";
19
19
  import { cancelCommand } from "./commands/cancel.js";
20
+ import { pauseCommand } from "./commands/pause.js";
21
+ import { resumeCommand } from "./commands/resume.js";
22
+ import { reportCommand } from "./commands/report.js";
20
23
  import { bold, green, yellow, red } from "./utils/colors.js";
21
24
  import type { Classification, PhaseName } from "./state/schema.js";
22
25
 
@@ -37,7 +40,7 @@ program
37
40
  program
38
41
  .command("init")
39
42
  .description("Create worktree and initialize state")
40
- .requiredOption("--mode <mode>", "Workflow mode: full or auto")
43
+ .option("--mode <mode>", "Workflow mode: full or auto (default: from project config or 'full')")
41
44
  .requiredOption("--description <text>", "Description of the work")
42
45
  .option("--classification <type>", "Work classification (auto mode): bug-fix, small-change, refactor, feature, large-feature")
43
46
  .option("--gated", "Wait for user approval between phases (default: auto-proceed)")
@@ -45,10 +48,10 @@ program
45
48
  .action((opts) => {
46
49
  try {
47
50
  const result = initCommand({
48
- mode: opts.mode as "full" | "auto",
51
+ mode: opts.mode as "full" | "auto" | undefined,
49
52
  description: opts.description,
50
53
  classification: opts.classification as Classification | undefined,
51
- gated: opts.gated || false,
54
+ gated: opts.gated,
52
55
  worktreeRoot: opts.worktreeRoot,
53
56
  });
54
57
  console.log(JSON.stringify(result, null, 2));
@@ -78,7 +81,7 @@ program
78
81
 
79
82
  program
80
83
  .command("complete <target>")
81
- .description("Mark a phase/sub-stage as complete (e.g., plan/clarify)")
84
+ .description("Mark a phase/step as complete (e.g., plan/clarify)")
82
85
  .option("--outcome <value>", "Outcome of the step (e.g., done, revise, broken, changes_requested)")
83
86
  .option("--worktree-root <path>", "Override worktree root")
84
87
  .action((target, opts) => {
@@ -144,8 +147,8 @@ program
144
147
  program
145
148
  .command("loopback")
146
149
  .description("Register a loop-back transition")
147
- .requiredOption("--from <source>", "Source phase/sub-stage (e.g., review/handoff)")
148
- .requiredOption("--to <target>", "Target phase/sub-stage (e.g., build/core)")
150
+ .requiredOption("--from <source>", "Source phase/step (e.g., review/handoff)")
151
+ .requiredOption("--to <target>", "Target phase/step (e.g., build/core)")
149
152
  .requiredOption("--reason <text>", "Reason for loop-back")
150
153
  .option("--worktree-root <path>", "Override worktree root")
151
154
  .action((opts) => {
@@ -260,9 +263,10 @@ program
260
263
  .command("bootstrap")
261
264
  .description("Detect work-kit state and output session orientation")
262
265
  .option("--json", "Output as JSON", true)
266
+ .option("--auto-resume", "If paused or stale, auto-flip to in-progress")
263
267
  .action((opts) => {
264
268
  try {
265
- const result = bootstrapCommand();
269
+ const result = bootstrapCommand(undefined, { autoResume: opts.autoResume });
266
270
  console.log(JSON.stringify(result, null, 2));
267
271
  } catch (e: any) {
268
272
  console.error(JSON.stringify({ action: "error", message: e.message }));
@@ -270,6 +274,58 @@ program
270
274
  }
271
275
  });
272
276
 
277
+ // ── pause ───────────────────────────────────────────────────────────
278
+
279
+ program
280
+ .command("pause")
281
+ .description("Pause the active work-kit session")
282
+ .option("--reason <text>", "Optional reason for pausing")
283
+ .option("--worktree-root <path>", "Override worktree root")
284
+ .action((opts) => {
285
+ try {
286
+ const result = pauseCommand(opts.reason, opts.worktreeRoot);
287
+ console.log(JSON.stringify(result, null, 2));
288
+ process.exit(result.action === "error" ? 1 : 0);
289
+ } catch (e: any) {
290
+ console.error(JSON.stringify({ action: "error", message: e.message }));
291
+ process.exit(1);
292
+ }
293
+ });
294
+
295
+ // ── resume ──────────────────────────────────────────────────────────
296
+
297
+ program
298
+ .command("resume")
299
+ .description("Resume a paused work-kit session")
300
+ .option("--worktree-root <path>", "Override worktree root")
301
+ .action((opts) => {
302
+ try {
303
+ const result = resumeCommand(opts.worktreeRoot);
304
+ console.log(JSON.stringify(result, null, 2));
305
+ process.exit(result.action === "error" ? 1 : 0);
306
+ } catch (e: any) {
307
+ console.error(JSON.stringify({ action: "error", message: e.message }));
308
+ process.exit(1);
309
+ }
310
+ });
311
+
312
+ // ── report ──────────────────────────────────────────────────────────
313
+
314
+ program
315
+ .command("report")
316
+ .description("Show stats across completed work-kits")
317
+ .option("--json", "Output as JSON")
318
+ .option("--repo <path>", "Main repository root")
319
+ .option("--worktree-root <path>", "Override worktree root")
320
+ .action((opts) => {
321
+ try {
322
+ reportCommand({ json: opts.json, repo: opts.repo, worktreeRoot: opts.worktreeRoot });
323
+ } catch (e: any) {
324
+ console.error(JSON.stringify({ action: "error", message: e.message }));
325
+ process.exit(1);
326
+ }
327
+ });
328
+
273
329
  // ── cancel ──────────────────────────────────────────────────────────
274
330
 
275
331
  program