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,27 +1,27 @@
1
1
  import { describe, it } from "node:test";
2
2
  import * as assert from "node:assert/strict";
3
3
  import { parseLocation, resetToLocation } from "./helpers.js";
4
- import type { WorkKitState, PhaseName, PhaseState, SubStageState } from "./schema.js";
5
- import { PHASE_NAMES, SUBSTAGES_BY_PHASE } from "./schema.js";
4
+ import type { WorkKitState, PhaseName, PhaseState, StepState } from "./schema.js";
5
+ import { PHASE_NAMES, STEPS_BY_PHASE } from "./schema.js";
6
6
 
7
7
  function makeState(): WorkKitState {
8
8
  const phases = {} as Record<PhaseName, PhaseState>;
9
9
  for (const phase of PHASE_NAMES) {
10
- const subStages: Record<string, SubStageState> = {};
11
- for (const ss of SUBSTAGES_BY_PHASE[phase]) {
12
- subStages[ss] = { status: "pending" };
10
+ const steps: Record<string, StepState> = {};
11
+ for (const s of STEPS_BY_PHASE[phase]) {
12
+ steps[s] = { status: "pending" };
13
13
  }
14
- phases[phase] = { status: "pending", subStages };
14
+ phases[phase] = { status: "pending", steps };
15
15
  }
16
16
  return {
17
- version: 1,
17
+ version: 2,
18
18
  slug: "test",
19
19
  branch: "feature/test",
20
20
  started: "2026-01-01",
21
21
  mode: "full-kit",
22
22
  status: "in-progress",
23
23
  currentPhase: "plan",
24
- currentSubStage: "clarify",
24
+ currentStep: "clarify",
25
25
  phases,
26
26
  loopbacks: [],
27
27
  metadata: { worktreeRoot: "/tmp/test", mainRepoRoot: "/tmp/test" },
@@ -31,7 +31,7 @@ function makeState(): WorkKitState {
31
31
  describe("parseLocation", () => {
32
32
  it("parses plan/clarify correctly", () => {
33
33
  const loc = parseLocation("plan/clarify");
34
- assert.deepStrictEqual(loc, { phase: "plan", subStage: "clarify" });
34
+ assert.deepStrictEqual(loc, { phase: "plan", step: "clarify" });
35
35
  });
36
36
 
37
37
  it("throws on invalid format (no slash)", () => {
@@ -42,8 +42,8 @@ describe("parseLocation", () => {
42
42
  assert.throws(() => parseLocation("foobar/baz"), /Unknown phase/);
43
43
  });
44
44
 
45
- it("throws on unknown sub-stage", () => {
46
- assert.throws(() => parseLocation("plan/nonexistent"), /Unknown sub-stage/);
45
+ it("throws on unknown step", () => {
46
+ assert.throws(() => parseLocation("plan/nonexistent"), /Unknown step/);
47
47
  });
48
48
  });
49
49
 
@@ -52,40 +52,40 @@ describe("resetToLocation", () => {
52
52
  const state = makeState();
53
53
 
54
54
  // Mark plan and build as completed
55
- for (const ss of Object.values(state.phases.plan.subStages)) {
56
- ss.status = "completed";
57
- ss.completedAt = "2026-01-01";
55
+ for (const s of Object.values(state.phases.plan.steps)) {
56
+ s.status = "completed";
57
+ s.completedAt = "2026-01-01";
58
58
  }
59
59
  state.phases.plan.status = "completed";
60
60
  state.phases.plan.completedAt = "2026-01-01";
61
61
 
62
- for (const ss of Object.values(state.phases.build.subStages)) {
63
- ss.status = "completed";
64
- ss.completedAt = "2026-01-02";
62
+ for (const s of Object.values(state.phases.build.steps)) {
63
+ s.status = "completed";
64
+ s.completedAt = "2026-01-02";
65
65
  }
66
66
  state.phases.build.status = "completed";
67
67
  state.phases.build.completedAt = "2026-01-02";
68
68
 
69
69
  // Reset to plan/blueprint
70
- resetToLocation(state, { phase: "plan", subStage: "blueprint" });
70
+ resetToLocation(state, { phase: "plan", step: "blueprint" });
71
71
 
72
- // Sub-stages before blueprint should stay completed
73
- assert.equal(state.phases.plan.subStages.clarify.status, "completed");
74
- assert.equal(state.phases.plan.subStages.investigate.status, "completed");
75
- assert.equal(state.phases.plan.subStages.sketch.status, "completed");
76
- assert.equal(state.phases.plan.subStages.scope.status, "completed");
77
- assert.equal(state.phases.plan.subStages["ux-flow"].status, "completed");
78
- assert.equal(state.phases.plan.subStages.architecture.status, "completed");
72
+ // Steps before blueprint should stay completed
73
+ assert.equal(state.phases.plan.steps.clarify.status, "completed");
74
+ assert.equal(state.phases.plan.steps.investigate.status, "completed");
75
+ assert.equal(state.phases.plan.steps.sketch.status, "completed");
76
+ assert.equal(state.phases.plan.steps.scope.status, "completed");
77
+ assert.equal(state.phases.plan.steps["ux-flow"].status, "completed");
78
+ assert.equal(state.phases.plan.steps.architecture.status, "completed");
79
79
 
80
80
  // Blueprint and audit should be reset
81
- assert.equal(state.phases.plan.subStages.blueprint.status, "pending");
82
- assert.equal(state.phases.plan.subStages.audit.status, "pending");
81
+ assert.equal(state.phases.plan.steps.blueprint.status, "pending");
82
+ assert.equal(state.phases.plan.steps.audit.status, "pending");
83
83
 
84
84
  // Plan phase should be in-progress
85
85
  assert.equal(state.phases.plan.status, "in-progress");
86
86
 
87
87
  // Build (later phase) should be reset
88
88
  assert.equal(state.phases.build.status, "pending");
89
- assert.equal(state.phases.build.subStages.core.status, "pending");
89
+ assert.equal(state.phases.build.steps.core.status, "pending");
90
90
  });
91
91
  });
@@ -1,47 +1,59 @@
1
- import { PHASE_NAMES, SUBSTAGES_BY_PHASE } from "./schema.js";
1
+ import { PHASE_NAMES, STEPS_BY_PHASE } from "./schema.js";
2
2
  import type { Location, PhaseName, WorkKitState } from "./schema.js";
3
- import { PHASE_ORDER } from "../config/phases.js";
4
3
 
5
4
  /**
6
- * Parse "phase/sub-stage" string into a Location object.
7
- * Validates that the phase is a known phase name and the sub-stage exists.
5
+ * Mutate a state object to flip a paused session into in-progress.
6
+ * Caller is responsible for persisting via writeState. Returns true
7
+ * when state changed, false when no transition was applicable.
8
+ */
9
+ export function unpause(state: WorkKitState): boolean {
10
+ if (state.status !== "paused") return false;
11
+ state.status = "in-progress";
12
+ delete state.pausedAt;
13
+ return true;
14
+ }
15
+ import { PHASE_ORDER } from "../config/workflow.js";
16
+
17
+ /**
18
+ * Parse "phase/step" string into a Location object.
19
+ * Validates that the phase is a known phase name and the step exists.
8
20
  */
9
21
  export function parseLocation(input: string): Location {
10
22
  const parts = input.split("/");
11
23
  if (parts.length !== 2) {
12
- throw new Error(`Invalid location "${input}". Expected format: phase/sub-stage (e.g., plan/clarify)`);
24
+ throw new Error(`Invalid location "${input}". Expected format: phase/step (e.g., plan/clarify)`);
13
25
  }
14
- const [phase, subStage] = parts;
26
+ const [phase, step] = parts;
15
27
  if (!PHASE_NAMES.includes(phase as PhaseName)) {
16
28
  throw new Error(`Unknown phase "${phase}". Valid phases: ${PHASE_NAMES.join(", ")}`);
17
29
  }
18
- const validSubStages = SUBSTAGES_BY_PHASE[phase as PhaseName];
19
- if (!validSubStages.includes(subStage)) {
20
- throw new Error(`Unknown sub-stage "${subStage}" in phase "${phase}". Valid: ${validSubStages.join(", ")}`);
30
+ const validSteps = STEPS_BY_PHASE[phase as PhaseName];
31
+ if (!validSteps.includes(step)) {
32
+ throw new Error(`Unknown step "${step}" in phase "${phase}". Valid: ${validSteps.join(", ")}`);
21
33
  }
22
- return { phase: phase as PhaseName, subStage };
34
+ return { phase: phase as PhaseName, step };
23
35
  }
24
36
 
25
37
  /**
26
- * Reset state from a target location forward: marks the target sub-stage
27
- * and all subsequent sub-stages/phases as pending.
38
+ * Reset state from a target location forward: marks the target step
39
+ * and all subsequent steps/phases as pending.
28
40
  */
29
41
  export function resetToLocation(state: WorkKitState, location: Location): void {
30
42
  const targetPhaseState = state.phases[location.phase];
31
43
  if (!targetPhaseState) {
32
44
  throw new Error(`Phase "${location.phase}" not found in state`);
33
45
  }
34
- if (!targetPhaseState.subStages[location.subStage]) {
35
- throw new Error(`Sub-stage "${location.subStage}" not found in phase "${location.phase}"`);
46
+ if (!targetPhaseState.steps[location.step]) {
47
+ throw new Error(`Step "${location.step}" not found in phase "${location.phase}"`);
36
48
  }
37
49
 
38
50
  let reset = false;
39
- for (const [ss, ssState] of Object.entries(targetPhaseState.subStages)) {
40
- if (ss === location.subStage) reset = true;
41
- if (reset && (ssState.status === "completed" || ssState.status === "waiting")) {
42
- ssState.status = "pending";
43
- delete ssState.completedAt;
44
- delete ssState.outcome;
51
+ for (const [s, sState] of Object.entries(targetPhaseState.steps)) {
52
+ if (s === location.step) reset = true;
53
+ if (reset && (sState.status === "completed" || sState.status === "waiting")) {
54
+ sState.status = "pending";
55
+ delete sState.completedAt;
56
+ delete sState.outcome;
45
57
  }
46
58
  }
47
59
  targetPhaseState.status = "in-progress";
@@ -53,11 +65,11 @@ export function resetToLocation(state: WorkKitState, location: Location): void {
53
65
  if (laterPhaseState.status === "completed") {
54
66
  laterPhaseState.status = "pending";
55
67
  delete laterPhaseState.completedAt;
56
- for (const ssState of Object.values(laterPhaseState.subStages)) {
57
- if (ssState.status === "completed") {
58
- ssState.status = "pending";
59
- delete ssState.completedAt;
60
- delete ssState.outcome;
68
+ for (const sState of Object.values(laterPhaseState.steps)) {
69
+ if (sState.status === "completed") {
70
+ sState.status = "pending";
71
+ delete sState.completedAt;
72
+ delete sState.outcome;
61
73
  }
62
74
  }
63
75
  }
@@ -1,61 +1,96 @@
1
- // ── Phase & Sub-stage Types ──────────────────────────────────────────
1
+ // ── Phase & Step Types ──────────────────────────────────────────────
2
2
 
3
3
  export const PHASE_NAMES = ["plan", "build", "test", "review", "deploy", "wrap-up"] as const;
4
4
  export type PhaseName = (typeof PHASE_NAMES)[number];
5
5
 
6
- export const PLAN_SUBSTAGES = ["clarify", "investigate", "sketch", "scope", "ux-flow", "architecture", "blueprint", "audit"] as const;
7
- export const BUILD_SUBSTAGES = ["setup", "migration", "red", "core", "ui", "refactor", "integration", "commit"] as const;
8
- export const TEST_SUBSTAGES = ["verify", "e2e", "validate"] as const;
9
- export const REVIEW_SUBSTAGES = ["self-review", "security", "performance", "compliance", "handoff"] as const;
10
- export const DEPLOY_SUBSTAGES = ["merge", "monitor", "remediate"] as const;
11
- export const WRAPUP_SUBSTAGES = ["wrap-up"] as const;
12
-
13
- export type PlanSubStage = (typeof PLAN_SUBSTAGES)[number];
14
- export type BuildSubStage = (typeof BUILD_SUBSTAGES)[number];
15
- export type TestSubStage = (typeof TEST_SUBSTAGES)[number];
16
- export type ReviewSubStage = (typeof REVIEW_SUBSTAGES)[number];
17
- export type DeploySubStage = (typeof DEPLOY_SUBSTAGES)[number];
18
- export type WrapUpSubStage = (typeof WRAPUP_SUBSTAGES)[number];
19
-
20
- export type SubStageName = PlanSubStage | BuildSubStage | TestSubStage | ReviewSubStage | DeploySubStage | WrapUpSubStage;
21
-
22
- export const SUBSTAGES_BY_PHASE: Record<PhaseName, readonly string[]> = {
23
- plan: PLAN_SUBSTAGES,
24
- build: BUILD_SUBSTAGES,
25
- test: TEST_SUBSTAGES,
26
- review: REVIEW_SUBSTAGES,
27
- deploy: DEPLOY_SUBSTAGES,
28
- "wrap-up": WRAPUP_SUBSTAGES,
6
+ export const PLAN_STEPS = ["clarify", "investigate", "sketch", "scope", "ux-flow", "architecture", "blueprint", "audit"] as const;
7
+ export const BUILD_STEPS = ["setup", "migration", "red", "core", "ui", "refactor", "integration", "commit"] as const;
8
+ export const TEST_STEPS = ["verify", "e2e", "validate"] as const;
9
+ export const REVIEW_STEPS = ["self-review", "security", "performance", "compliance", "handoff"] as const;
10
+ export const DEPLOY_STEPS = ["merge", "monitor", "remediate"] as const;
11
+ export const WRAPUP_STEPS = ["summary"] as const;
12
+
13
+ export type PlanStep = (typeof PLAN_STEPS)[number];
14
+ export type BuildStep = (typeof BUILD_STEPS)[number];
15
+ export type TestStep = (typeof TEST_STEPS)[number];
16
+ export type ReviewStep = (typeof REVIEW_STEPS)[number];
17
+ export type DeployStep = (typeof DEPLOY_STEPS)[number];
18
+ export type WrapUpStep = (typeof WRAPUP_STEPS)[number];
19
+
20
+ export type StepName = PlanStep | BuildStep | TestStep | ReviewStep | DeployStep | WrapUpStep;
21
+
22
+ export const STEPS_BY_PHASE: Record<PhaseName, readonly string[]> = {
23
+ plan: PLAN_STEPS,
24
+ build: BUILD_STEPS,
25
+ test: TEST_STEPS,
26
+ review: REVIEW_STEPS,
27
+ deploy: DEPLOY_STEPS,
28
+ "wrap-up": WRAPUP_STEPS,
29
29
  };
30
30
 
31
- // ── Classification ───────────────────────────────────────────────────
31
+ // ── Mode Constants ──────────────────────────────────────────────────
32
32
 
33
- export type Classification = "bug-fix" | "small-change" | "refactor" | "feature" | "large-feature";
33
+ export const MODE_FULL = "full-kit" as const;
34
+ export const MODE_AUTO = "auto-kit" as const;
34
35
 
35
- // ── Phase & Sub-stage State ──────────────────────────────────────────
36
+ // ── Classification ──────────────────────────────────────────────────
37
+
38
+ export const CLASSIFICATIONS = ["bug-fix", "small-change", "refactor", "feature", "large-feature"] as const;
39
+ export type Classification = (typeof CLASSIFICATIONS)[number];
40
+
41
+ export function isClassification(value: string): value is Classification {
42
+ return (CLASSIFICATIONS as readonly string[]).includes(value);
43
+ }
44
+
45
+ // ── Step Outcomes ───────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Closed set of outcomes a step can report when completing.
49
+ * Outcomes drive loop-back routing (see config/loopback-routes.ts).
50
+ */
51
+ export const STEP_OUTCOMES = [
52
+ "done", // generic success
53
+ "ok", // alias for done
54
+ "proceed", // explicit "no loopback, advance"
55
+ "approved", // review/handoff cleared for deploy
56
+ "revise", // audit / review found gaps; loop back to fix
57
+ "broken", // refactor or change broke something downstream
58
+ "changes_requested", // review handoff requested changes
59
+ "fix_needed", // deploy merge blocked, fix required
60
+ "fix_and_redeploy", // remediation requires another deploy cycle
61
+ "blocked", // step cannot proceed without external input
62
+ "skipped", // step intentionally skipped at runtime
63
+ ] as const;
64
+ export type StepOutcome = (typeof STEP_OUTCOMES)[number];
65
+
66
+ export function isStepOutcome(value: string): value is StepOutcome {
67
+ return (STEP_OUTCOMES as readonly string[]).includes(value);
68
+ }
69
+
70
+ // ── Phase & Step State ──────────────────────────────────────────────
36
71
 
37
72
  export type PhaseStatus = "pending" | "in-progress" | "completed" | "skipped";
38
- export type SubStageStatus = "pending" | "in-progress" | "completed" | "skipped" | "waiting";
73
+ export type StepStatus = "pending" | "in-progress" | "completed" | "skipped" | "waiting";
39
74
 
40
- export interface SubStageState {
41
- status: SubStageStatus;
42
- outcome?: string;
75
+ export interface StepState {
76
+ status: StepStatus;
77
+ outcome?: StepOutcome;
43
78
  startedAt?: string;
44
79
  completedAt?: string;
45
80
  }
46
81
 
47
82
  export interface PhaseState {
48
83
  status: PhaseStatus;
49
- subStages: Record<string, SubStageState>;
84
+ steps: Record<string, StepState>;
50
85
  startedAt?: string;
51
86
  completedAt?: string;
52
87
  }
53
88
 
54
- // ── Loopback ─────────────────────────────────────────────────────────
89
+ // ── Loopback ────────────────────────────────────────────────────────
55
90
 
56
91
  export interface Location {
57
92
  phase: PhaseName;
58
- subStage: string;
93
+ step: string;
59
94
  }
60
95
 
61
96
  export interface LoopbackRecord {
@@ -65,27 +100,33 @@ export interface LoopbackRecord {
65
100
  timestamp: string;
66
101
  }
67
102
 
68
- // ── Workflow (auto-kit) ──────────────────────────────────────────────
103
+ // ── Workflow (auto-kit) ─────────────────────────────────────────────
69
104
 
70
105
  export interface WorkflowStep {
71
106
  phase: PhaseName;
72
- subStage: string;
107
+ step: string;
73
108
  included: boolean;
74
109
  }
75
110
 
76
- // ── Main State ───────────────────────────────────────────────────────
111
+ // ── Work Status ─────────────────────────────────────────────────────
112
+
113
+ export type WorkStatus = "in-progress" | "paused" | "completed" | "failed";
114
+
115
+ // ── Main State ──────────────────────────────────────────────────────
77
116
 
78
117
  export interface WorkKitState {
79
- version: 1;
118
+ version: 2;
80
119
  slug: string;
81
120
  branch: string;
82
121
  started: string;
83
- mode: "full-kit" | "auto-kit";
122
+ mode: typeof MODE_FULL | typeof MODE_AUTO;
84
123
  gated?: boolean;
85
124
  classification?: Classification;
86
- status: "in-progress" | "paused" | "completed" | "failed";
125
+ status: WorkStatus;
126
+ /** ISO timestamp the work was paused; cleared on resume. */
127
+ pausedAt?: string;
87
128
  currentPhase: PhaseName | null;
88
- currentSubStage: string | null;
129
+ currentStep: string | null;
89
130
  phases: Record<PhaseName, PhaseState>;
90
131
  workflow?: WorkflowStep[];
91
132
  loopbacks: LoopbackRecord[];
@@ -95,20 +136,22 @@ export interface WorkKitState {
95
136
  };
96
137
  }
97
138
 
98
- // ── Actions (CLI → Claude) ───────────────────────────────────────────
139
+ // ── Actions (CLI → Claude) ──────────────────────────────────────────
99
140
 
100
141
  export interface AgentSpec {
101
142
  phase: PhaseName;
102
- subStage: string;
143
+ step: string;
103
144
  skillFile: string;
104
145
  agentPrompt: string;
105
- outputFile?: string; // for parallel agents writing to separate files
146
+ outputFile?: string;
106
147
  }
107
148
 
108
149
  export type Action =
109
- | { action: "spawn_agent"; phase: PhaseName; subStage: string; skillFile: string; agentPrompt: string; onComplete: string }
150
+ | { action: "spawn_agent"; phase: PhaseName; step: string; skillFile: string; agentPrompt: string; onComplete: string }
110
151
  | { action: "spawn_parallel_agents"; agents: AgentSpec[]; thenSequential?: AgentSpec; onComplete: string }
111
152
  | { action: "wait_for_user"; message: string }
112
153
  | { action: "loopback"; from: Location; to: Location; reason: string }
113
154
  | { action: "complete"; message: string }
155
+ | { action: "paused"; message: string }
156
+ | { action: "resumed"; message: string; phase: PhaseName | null; step: string | null }
114
157
  | { action: "error"; message: string; suggestion?: string };
@@ -1,12 +1,30 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
+ import { execFileSync } from "node:child_process";
4
5
  import { WorkKitState } from "./schema.js";
5
6
 
6
- const STATE_DIR = ".work-kit";
7
- const STATE_FILE = "tracker.json";
7
+ export const STATE_DIR = ".work-kit";
8
+ export const STATE_FILE = "tracker.json";
9
+ export const STATE_MD_FILE = "state.md";
10
+ export const AWAITING_INPUT_MARKER_FILE = "awaiting-input";
11
+ export const IDLE_MARKER_FILE = "idle";
8
12
 
9
- // ── Worktree Discovery ───────────────────────────────────────────────
13
+ /**
14
+ * Remove any "blocked on user" marker files (awaiting-input, idle).
15
+ *
16
+ * Hook-installed cleanup normally handles these (PostToolUse/Stop),
17
+ * but edge cases (denied permission, killed session, sentinel drift)
18
+ * can leave stale markers. Any forward state transition should clear
19
+ * them as a belt-and-suspenders safety net.
20
+ */
21
+ export function clearBlockingMarkers(worktreeRoot: string): void {
22
+ const dir = path.join(worktreeRoot, STATE_DIR);
23
+ fs.rmSync(path.join(dir, AWAITING_INPUT_MARKER_FILE), { force: true });
24
+ fs.rmSync(path.join(dir, IDLE_MARKER_FILE), { force: true });
25
+ }
26
+
27
+ // ── Worktree Discovery ──────────────────────────────────────────────
10
28
 
11
29
  export function findWorktreeRoot(startDir?: string): string | null {
12
30
  let dir = startDir || process.cwd();
@@ -29,10 +47,10 @@ export function statePath(worktreeRoot: string): string {
29
47
  }
30
48
 
31
49
  export function stateMdPath(worktreeRoot: string): string {
32
- return path.join(worktreeRoot, STATE_DIR, "state.md");
50
+ return path.join(worktreeRoot, STATE_DIR, STATE_MD_FILE);
33
51
  }
34
52
 
35
- // ── Read / Write ─────────────────────────────────────────────────────
53
+ // ── Read / Write ────────────────────────────────────────────────────
36
54
 
37
55
  export function stateExists(worktreeRoot: string): boolean {
38
56
  return fs.existsSync(statePath(worktreeRoot));
@@ -44,11 +62,13 @@ export function readState(worktreeRoot: string): WorkKitState {
44
62
  throw new Error(`No tracker.json found at ${filePath}`);
45
63
  }
46
64
  const raw = fs.readFileSync(filePath, "utf-8");
65
+ let parsed: any;
47
66
  try {
48
- return JSON.parse(raw) as WorkKitState;
67
+ parsed = JSON.parse(raw);
49
68
  } catch {
50
69
  throw new Error(`Corrupted tracker.json at ${filePath}. File contains invalid JSON.`);
51
70
  }
71
+ return migrateState(parsed, worktreeRoot);
52
72
  }
53
73
 
54
74
  export function writeState(worktreeRoot: string, state: WorkKitState): void {
@@ -62,7 +82,7 @@ export function writeState(worktreeRoot: string, state: WorkKitState): void {
62
82
  fs.renameSync(tmp, target);
63
83
  }
64
84
 
65
- // ── State.md ─────────────────────────────────────────────────────────
85
+ // ── State.md ────────────────────────────────────────────────────────
66
86
 
67
87
  export function writeStateMd(worktreeRoot: string, content: string): void {
68
88
  const dir = stateDir(worktreeRoot);
@@ -80,3 +100,68 @@ export function readStateMd(worktreeRoot: string): string | null {
80
100
  if (!fs.existsSync(filePath)) return null;
81
101
  return fs.readFileSync(filePath, "utf-8");
82
102
  }
103
+
104
+ // ── Git Helpers ─────────────────────────────────────────────────
105
+
106
+ export function resolveMainRepoRoot(worktreeRoot: string): string {
107
+ try {
108
+ const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
109
+ cwd: worktreeRoot,
110
+ encoding: "utf-8",
111
+ timeout: 5000,
112
+ });
113
+ const firstLine = output.split("\n").find(l => l.startsWith("worktree "));
114
+ if (firstLine) return firstLine.slice("worktree ".length).trim();
115
+ } catch {
116
+ // fallback
117
+ }
118
+ return worktreeRoot;
119
+ }
120
+
121
+ // ── Migration ───────────────────────────────────────────────────────
122
+
123
+ function migrateState(raw: any, worktreeRoot: string): WorkKitState {
124
+ if (raw.version === 2) return raw as WorkKitState;
125
+
126
+ // v1 → v2: rename subStage fields to step
127
+ if (raw.version === 1 || !raw.version) {
128
+ if ("currentSubStage" in raw) {
129
+ raw.currentStep = raw.currentSubStage;
130
+ delete raw.currentSubStage;
131
+ }
132
+
133
+ for (const phase of Object.values(raw.phases) as any[]) {
134
+ if (phase.subStages) {
135
+ phase.steps = phase.subStages;
136
+ delete phase.subStages;
137
+ }
138
+ }
139
+
140
+ if (raw.workflow) {
141
+ for (const ws of raw.workflow) {
142
+ if ("subStage" in ws) {
143
+ ws.step = ws.subStage;
144
+ delete ws.subStage;
145
+ }
146
+ }
147
+ }
148
+
149
+ if (raw.loopbacks) {
150
+ for (const lb of raw.loopbacks) {
151
+ if (lb.from?.subStage !== undefined) {
152
+ lb.from.step = lb.from.subStage;
153
+ delete lb.from.subStage;
154
+ }
155
+ if (lb.to?.subStage !== undefined) {
156
+ lb.to.step = lb.to.subStage;
157
+ delete lb.to.subStage;
158
+ }
159
+ }
160
+ }
161
+
162
+ raw.version = 2;
163
+ writeState(worktreeRoot, raw as WorkKitState);
164
+ }
165
+
166
+ return raw as WorkKitState;
167
+ }
@@ -1,27 +1,27 @@
1
1
  import { describe, it } from "node:test";
2
2
  import * as assert from "node:assert/strict";
3
3
  import { validatePhasePrerequisites } from "./validators.js";
4
- import type { WorkKitState, PhaseName, PhaseState, SubStageState } from "./schema.js";
5
- import { PHASE_NAMES, SUBSTAGES_BY_PHASE } from "./schema.js";
4
+ import type { WorkKitState, PhaseName, PhaseState, StepState } from "./schema.js";
5
+ import { PHASE_NAMES, STEPS_BY_PHASE } from "./schema.js";
6
6
 
7
7
  function makeState(): WorkKitState {
8
8
  const phases = {} as Record<PhaseName, PhaseState>;
9
9
  for (const phase of PHASE_NAMES) {
10
- const subStages: Record<string, SubStageState> = {};
11
- for (const ss of SUBSTAGES_BY_PHASE[phase]) {
12
- subStages[ss] = { status: "pending" };
10
+ const steps: Record<string, StepState> = {};
11
+ for (const s of STEPS_BY_PHASE[phase]) {
12
+ steps[s] = { status: "pending" };
13
13
  }
14
- phases[phase] = { status: "pending", subStages };
14
+ phases[phase] = { status: "pending", steps };
15
15
  }
16
16
  return {
17
- version: 1,
17
+ version: 2,
18
18
  slug: "test",
19
19
  branch: "feature/test",
20
20
  started: "2026-01-01",
21
21
  mode: "full-kit",
22
22
  status: "in-progress",
23
23
  currentPhase: "plan",
24
- currentSubStage: "clarify",
24
+ currentStep: "clarify",
25
25
  phases,
26
26
  loopbacks: [],
27
27
  metadata: { worktreeRoot: "/tmp/test", mainRepoRoot: "/tmp/test" },
@@ -31,9 +31,9 @@ function makeState(): WorkKitState {
31
31
  function completePhase(state: WorkKitState, phase: PhaseName): void {
32
32
  state.phases[phase].status = "completed";
33
33
  state.phases[phase].completedAt = "2026-01-01";
34
- for (const ss of Object.values(state.phases[phase].subStages)) {
35
- ss.status = "completed";
36
- ss.completedAt = "2026-01-01";
34
+ for (const s of Object.values(state.phases[phase].steps)) {
35
+ s.status = "completed";
36
+ s.completedAt = "2026-01-01";
37
37
  }
38
38
  }
39
39
 
@@ -65,7 +65,7 @@ describe("validatePhasePrerequisites", () => {
65
65
  completePhase(state, "test");
66
66
  completePhase(state, "review");
67
67
  // handoff completed but without "approved" outcome
68
- state.phases.review.subStages.handoff.outcome = "pending_review";
68
+ state.phases.review.steps.handoff.outcome = "blocked";
69
69
  const result = validatePhasePrerequisites(state, "deploy");
70
70
  assert.equal(result.valid, false);
71
71
  });
@@ -76,7 +76,7 @@ describe("validatePhasePrerequisites", () => {
76
76
  completePhase(state, "build");
77
77
  completePhase(state, "test");
78
78
  completePhase(state, "review");
79
- state.phases.review.subStages.handoff.outcome = "approved";
79
+ state.phases.review.steps.handoff.outcome = "approved";
80
80
  const result = validatePhasePrerequisites(state, "deploy");
81
81
  assert.equal(result.valid, true);
82
82
  });
@@ -1,5 +1,5 @@
1
1
  import { WorkKitState, PhaseName } from "./schema.js";
2
- import { PHASE_PREREQUISITES } from "../config/phases.js";
2
+ import { PHASE_PREREQUISITES } from "../config/workflow.js";
3
3
 
4
4
  export interface ValidationResult {
5
5
  valid: boolean;
@@ -35,7 +35,6 @@ export function validatePhasePrerequisites(state: WorkKitState, phase: PhaseName
35
35
  };
36
36
  }
37
37
 
38
- // deploy completed, skipped, or pending (never started) — all ok if review is done
39
38
  return { valid: true, message: "Prerequisites met for wrap-up" };
40
39
  }
41
40
 
@@ -49,11 +48,11 @@ export function validatePhasePrerequisites(state: WorkKitState, phase: PhaseName
49
48
  missingPrerequisite: "review",
50
49
  };
51
50
  }
52
- const handoff = reviewState.subStages["handoff"];
51
+ const handoff = reviewState.steps["handoff"];
53
52
  if (!handoff) {
54
53
  return {
55
54
  valid: false,
56
- message: `deploy requires review/handoff to exist and be approved. Handoff sub-stage not found.`,
55
+ message: `deploy requires review/handoff to exist and be approved. Handoff step not found.`,
57
56
  missingPrerequisite: "review",
58
57
  };
59
58
  }
@@ -17,7 +17,9 @@ export const bgCyan = (s: string) => `${code(46)}${code(30)}${s}${reset}`;
17
17
  export const bgRed = (s: string) => `${code(41)}${code(97)}${s}${reset}`;
18
18
  export const bgDim = (s: string) => `${code(100)}${code(97)}${s}${reset}`;
19
19
  export const bgMagenta = (s: string) => `${code(45)}${code(97)}${s}${reset}`;
20
+ export const bgBlue = (s: string) => `${code(44)}${code(97)}${s}${reset}`;
20
21
  export const boldCyan = (s: string) => `${code(1)}${code(36)}${s}${reset}`;
21
22
  export const boldGreen = (s: string) => `${code(1)}${code(32)}${s}${reset}`;
22
23
  export const boldYellow = (s: string) => `${code(1)}${code(33)}${s}${reset}`;
23
24
  export const boldRed = (s: string) => `${code(1)}${code(31)}${s}${reset}`;
25
+ export const boldMagenta = (s: string) => `${code(1)}${code(35)}${s}${reset}`;