work-kit-cli 0.2.8 → 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 +101 -79
  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
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # work-kit
2
2
 
3
- Structured development workflow for [Claude Code](https://claude.com/claude-code). Two modes, 6 phases, 27 sub-stages — orchestrated by a TypeScript CLI with reusable skill files.
3
+ Structured development workflow for [Claude Code](https://claude.com/claude-code). Two modes, 6 phases, 27 steps — orchestrated by a TypeScript CLI with reusable skill files.
4
4
 
5
5
  ## Installation
6
6
 
@@ -33,13 +33,13 @@ npx work-kit-cli setup
33
33
 
34
34
  ### `/full-kit <description>`
35
35
 
36
- Runs every phase and sub-stage in strict order. No shortcuts.
36
+ Runs every phase and step in strict order. No shortcuts.
37
37
 
38
38
  Best for: large features, new systems, maximum rigor.
39
39
 
40
40
  ### `/auto-kit <description>`
41
41
 
42
- Classifies the request (bug-fix, small-change, refactor, feature, large-feature) and builds a dynamic workflow with only the sub-stages needed.
42
+ Classifies the request (bug-fix, small-change, refactor, feature, large-feature) and builds a dynamic workflow with only the steps needed.
43
43
 
44
44
  Best for: bug fixes, small changes, refactors, well-understood tasks.
45
45
 
@@ -48,9 +48,9 @@ Best for: bug fixes, small changes, refactors, well-understood tasks.
48
48
  | Command | Description |
49
49
  |---------|-------------|
50
50
  | `init <description>` | Initialize a new workflow with a task description |
51
- | `next` | Advance to the next sub-stage |
52
- | `complete` | Mark the current sub-stage as complete |
53
- | `status` | Show current workflow state (phase, sub-stage, progress) |
51
+ | `next` | Advance to the next step |
52
+ | `complete` | Mark the current step as complete |
53
+ | `status` | Show current workflow state (phase, step, progress) |
54
54
  | `context` | Generate context summary for the current phase |
55
55
  | `validate` | Validate state integrity and phase prerequisites |
56
56
  | `loopback` | Route back to a previous stage (max 2 per route) |
@@ -60,7 +60,7 @@ Best for: bug fixes, small changes, refactors, well-understood tasks.
60
60
 
61
61
  ## Phases
62
62
 
63
- | Phase | Sub-stages | Agent |
63
+ | Phase | Steps | Agent |
64
64
  |-------|-----------|-------|
65
65
  | **Plan** | Clarify, Investigate, Sketch, Scope, UX Flow, Architecture, Blueprint, Audit | Single |
66
66
  | **Build** | Setup, Migration, Red, Core, UI, Refactor, Integration, Commit | Single |
@@ -81,7 +81,7 @@ Phases communicate through **Final sections** in `.work-kit/state.md`. Each phas
81
81
 
82
82
  Dual state files in `.work-kit/`:
83
83
 
84
- - **tracker.json** — state machine (current phase, sub-stage, transitions, loop-back counts)
84
+ - **tracker.json** — state machine (current phase, step, transitions, loop-back counts)
85
85
  - **state.md** — content (working notes, Final sections, accumulated context)
86
86
 
87
87
  All writes are atomic to prevent state corruption.
@@ -129,15 +129,15 @@ work-kit/
129
129
  full-kit/SKILL.md # /full-kit orchestrator
130
130
  auto-kit/SKILL.md # /auto-kit orchestrator
131
131
  plan/SKILL.md # Plan phase runner
132
- plan/stages/ # 8 stage files
132
+ plan/steps/ # 8 step files
133
133
  build/SKILL.md # Build phase runner
134
- build/stages/ # 8 stage files
134
+ build/steps/ # 8 step files
135
135
  test/SKILL.md # Test phase runner
136
- test/stages/ # 3 stage files
136
+ test/steps/ # 3 step files
137
137
  review/SKILL.md # Review phase runner
138
- review/stages/ # 5 stage files
138
+ review/steps/ # 5 step files
139
139
  deploy/SKILL.md # Deploy phase runner
140
- deploy/stages/ # 3 stage files
140
+ deploy/steps/ # 3 step files
141
141
  wrap-up/SKILL.md # Final summary + cleanup
142
142
  package.json
143
143
  ```
@@ -1,5 +1,7 @@
1
1
  import fs from "node:fs";
2
- import { findWorktreeRoot, readState, statePath } from "../state/store.js";
2
+ import { findWorktreeRoot, readState, writeState, statePath } from "../state/store.js";
3
+ import { unpause } from "../state/helpers.js";
4
+ import { CLI_BINARY, STALE_THRESHOLD_MS } from "../config/constants.js";
3
5
 
4
6
  export interface BootstrapResult {
5
7
  active: boolean;
@@ -7,13 +9,20 @@ export interface BootstrapResult {
7
9
  branch?: string;
8
10
  mode?: string;
9
11
  phase?: string | null;
10
- subStage?: string | null;
12
+ step?: string | null;
11
13
  status?: string;
14
+ pausedAt?: string;
15
+ resumed?: boolean;
16
+ resumeReason?: string;
12
17
  nextAction?: string;
13
18
  recovery?: string | null;
14
19
  }
15
20
 
16
- export function bootstrapCommand(startDir?: string): BootstrapResult {
21
+ export interface BootstrapOptions {
22
+ autoResume?: boolean;
23
+ }
24
+
25
+ export function bootstrapCommand(startDir?: string, options: BootstrapOptions = {}): BootstrapResult {
17
26
  const root = findWorktreeRoot(startDir);
18
27
 
19
28
  if (!root) {
@@ -26,29 +35,44 @@ export function bootstrapCommand(startDir?: string): BootstrapResult {
26
35
 
27
36
  const state = readState(root);
28
37
 
29
- // Check for staleness: if state file hasn't been modified in over 1 hour
30
38
  let recovery: string | null = null;
39
+ let isStale = false;
31
40
  try {
32
- const stateFile = statePath(root);
33
- const stat = fs.statSync(stateFile);
34
- const hourAgo = Date.now() - 60 * 60 * 1000;
35
- if (stat.mtimeMs < hourAgo) {
41
+ const stat = fs.statSync(statePath(root));
42
+ if (Date.now() - stat.mtimeMs > STALE_THRESHOLD_MS) {
43
+ isStale = true;
36
44
  const hoursAgo = Math.round((Date.now() - stat.mtimeMs) / (60 * 60 * 1000));
37
- recovery = `State appears stale (last update ~${hoursAgo}h ago). Run \`npx work-kit-cli status\` to diagnose. If the agent crashed mid-stage, run \`npx work-kit-cli next\` to resume.`;
45
+ recovery = `State appears stale (last update ~${hoursAgo}h ago). Run \`${CLI_BINARY} status\` to diagnose. If the agent crashed mid-step, run \`${CLI_BINARY} next\` to resume.`;
38
46
  }
39
47
  } catch {
40
- // Ignore stat errors
48
+ // ignore stat errors
49
+ }
50
+
51
+ let resumed = false;
52
+ let resumeReason: string | undefined;
53
+ if (options.autoResume) {
54
+ if (unpause(state)) {
55
+ writeState(root, state);
56
+ resumed = true;
57
+ resumeReason = "Was paused — auto-resumed.";
58
+ } else if (state.status === "in-progress" && isStale) {
59
+ resumed = true;
60
+ resumeReason = "Stale in-progress — proceeding from current step.";
61
+ recovery = null;
62
+ }
41
63
  }
42
64
 
43
65
  let nextAction: string;
44
66
  if (state.status === "completed") {
45
67
  nextAction = "Work-kit session is complete. Run wrap-up or start a new session.";
46
68
  } else if (state.status === "failed") {
47
- nextAction = "Work-kit session failed. Run `npx work-kit-cli status` to see details.";
69
+ nextAction = `Work-kit session failed. Run \`${CLI_BINARY} status\` to see details.`;
70
+ } else if (state.status === "paused" && !resumed) {
71
+ nextAction = `Work-kit is paused${state.pausedAt ? ` (since ${state.pausedAt})` : ""}. Run \`${CLI_BINARY} resume\` to continue.`;
48
72
  } else if (recovery) {
49
73
  nextAction = recovery;
50
74
  } else {
51
- nextAction = `Continue ${state.currentPhase ?? "next phase"}${state.currentSubStage ? "/" + state.currentSubStage : ""}. Run \`npx work-kit-cli next\` to get the agent prompt.`;
75
+ nextAction = `Continue ${state.currentPhase ?? "next phase"}${state.currentStep ? "/" + state.currentStep : ""}. Run \`${CLI_BINARY} next\` to get the agent prompt.`;
52
76
  }
53
77
 
54
78
  return {
@@ -57,8 +81,10 @@ export function bootstrapCommand(startDir?: string): BootstrapResult {
57
81
  branch: state.branch,
58
82
  mode: state.mode,
59
83
  phase: state.currentPhase,
60
- subStage: state.currentSubStage,
84
+ step: state.currentStep,
61
85
  status: state.status,
86
+ ...(state.pausedAt && { pausedAt: state.pausedAt }),
87
+ ...(resumed && { resumed: true, resumeReason }),
62
88
  nextAction,
63
89
  recovery,
64
90
  };
@@ -1,7 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { readState, findWorktreeRoot, stateDir } from "../state/store.js";
4
+ import { readState, findWorktreeRoot, stateDir, resolveMainRepoRoot } from "../state/store.js";
5
5
 
6
6
  export interface CancelResult {
7
7
  action: "cancelled" | "error";
@@ -12,21 +12,6 @@ export interface CancelResult {
12
12
  message: string;
13
13
  }
14
14
 
15
- function resolveMainRepoRoot(worktreeRoot: string): string {
16
- try {
17
- const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
18
- cwd: worktreeRoot,
19
- encoding: "utf-8",
20
- timeout: 5000,
21
- });
22
- const firstLine = output.split("\n").find(l => l.startsWith("worktree "));
23
- if (firstLine) return firstLine.slice("worktree ".length).trim();
24
- } catch {
25
- // fallback
26
- }
27
- return worktreeRoot;
28
- }
29
-
30
15
  export function cancelCommand(worktreeRoot?: string): CancelResult {
31
16
  const root = worktreeRoot || findWorktreeRoot();
32
17
  if (!root) {
@@ -1,12 +1,13 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { execFileSync } from "node:child_process";
4
- import { readState, writeState, findWorktreeRoot, readStateMd, statePath } from "../state/store.js";
5
- import { isPhaseComplete, nextSubStageInPhase } from "../engine/transitions.js";
6
- import { checkLoopback } from "../engine/loopbacks.js";
7
- import { PHASE_ORDER } from "../config/phases.js";
3
+ import { readState, writeState, findWorktreeRoot, readStateMd, statePath, resolveMainRepoRoot, clearBlockingMarkers, STATE_MD_FILE, STATE_FILE } from "../state/store.js";
4
+ import { isPhaseComplete, nextStepInPhase } from "../workflow/transitions.js";
5
+ import { checkLoopback, countLoopbacksForRoute } from "../workflow/loopbacks.js";
6
+ import { PHASE_ORDER } from "../config/workflow.js";
8
7
  import { parseLocation, resetToLocation } from "../state/helpers.js";
9
- import type { Action, PhaseName, WorkKitState } from "../state/schema.js";
8
+ import { TRACKER_DIR, ARCHIVE_DIR, INDEX_FILE, SUMMARY_FILE, MAX_LOOPBACKS_PER_ROUTE, CLI_BINARY } from "../config/constants.js";
9
+ import { isStepOutcome, STEP_OUTCOMES, type Action, type PhaseName, type StepOutcome, type WorkKitState } from "../state/schema.js";
10
+ import { stateMdPath } from "../state/store.js";
10
11
 
11
12
  export function completeCommand(target: string, outcome?: string, worktreeRoot?: string): Action {
12
13
  const root = worktreeRoot || findWorktreeRoot();
@@ -14,55 +15,62 @@ export function completeCommand(target: string, outcome?: string, worktreeRoot?:
14
15
  return { action: "error", message: "No work-kit state found. Run `work-kit init` first." };
15
16
  }
16
17
 
18
+ // Forward state transition → clear any stale "blocked on user" markers
19
+ clearBlockingMarkers(root);
20
+
21
+ // Validate outcome against the closed enum
22
+ let typedOutcome: StepOutcome | undefined;
23
+ if (outcome) {
24
+ if (!isStepOutcome(outcome)) {
25
+ return {
26
+ action: "error",
27
+ message: `Invalid outcome "${outcome}".`,
28
+ suggestion: `Valid outcomes: ${STEP_OUTCOMES.join(", ")}`,
29
+ };
30
+ }
31
+ typedOutcome = outcome;
32
+ }
33
+
17
34
  const state = readState(root);
18
- const { phase, subStage } = parseLocation(target);
35
+ const { phase, step } = parseLocation(target);
19
36
 
20
- // Validate phase exists
21
37
  if (!state.phases[phase]) {
22
38
  return { action: "error", message: `Unknown phase: ${phase}` };
23
39
  }
24
40
 
25
- // Validate sub-stage exists
26
- const ssState = state.phases[phase].subStages[subStage];
27
- if (!ssState) {
28
- return { action: "error", message: `Unknown sub-stage: ${phase}/${subStage}` };
41
+ const stepState = state.phases[phase].steps[step];
42
+ if (!stepState) {
43
+ return { action: "error", message: `Unknown step: ${phase}/${step}` };
29
44
  }
30
45
 
31
- // Validate sub-stage is in a completable state
32
- if (ssState.status === "completed") {
33
- return { action: "error", message: `${phase}/${subStage} is already completed.` };
46
+ if (stepState.status === "completed") {
47
+ return { action: "error", message: `${phase}/${step} is already completed.` };
34
48
  }
35
- if (ssState.status === "skipped") {
36
- return { action: "error", message: `${phase}/${subStage} is skipped and cannot be completed. Add it to the workflow first.` };
49
+ if (stepState.status === "skipped") {
50
+ return { action: "error", message: `${phase}/${step} is skipped and cannot be completed. Add it to the workflow first.` };
37
51
  }
38
52
 
39
- // Mark sub-stage complete
40
- ssState.status = "completed";
41
- ssState.completedAt = new Date().toISOString();
42
- if (outcome) {
43
- ssState.outcome = outcome;
53
+ stepState.status = "completed";
54
+ stepState.completedAt = new Date().toISOString();
55
+ if (typedOutcome) {
56
+ stepState.outcome = typedOutcome;
44
57
  }
45
58
 
46
- // Check for loop-back triggers
47
- const loopback = checkLoopback(phase, subStage, outcome);
59
+ const loopback = checkLoopback(phase, step, typedOutcome);
48
60
  if (loopback) {
49
- // Enforce max 2 loopbacks per route
50
- const sameRouteCount = state.loopbacks.filter(
51
- (lb) => lb.from.phase === phase && lb.from.subStage === subStage
52
- && lb.to.phase === loopback.to.phase && lb.to.subStage === loopback.to.subStage
53
- ).length;
54
-
55
- if (sameRouteCount >= 2) {
56
- // Max reached — proceed without looping back, note the caveat
61
+ const from = { phase, step };
62
+ const sameRouteCount = countLoopbacksForRoute(state.loopbacks, from, loopback.to);
63
+
64
+ if (sameRouteCount >= MAX_LOOPBACKS_PER_ROUTE) {
57
65
  writeState(root, state);
58
66
  return {
59
67
  action: "wait_for_user",
60
- message: `${phase}/${subStage} triggered loopback (outcome: ${outcome}) but max loopback count (2) reached for this route. Proceeding with noted caveats.`,
68
+ message: `${phase}/${step} triggered loopback (outcome: ${typedOutcome}) but max loopback count (${MAX_LOOPBACKS_PER_ROUTE}) reached for this route. Proceeding with noted caveats.`,
61
69
  };
62
70
  }
63
71
 
64
72
  state.loopbacks.push({
65
- from: { phase, subStage },
73
+ from,
66
74
  to: loopback.to,
67
75
  reason: loopback.reason,
68
76
  timestamp: new Date().toISOString(),
@@ -70,135 +78,121 @@ export function completeCommand(target: string, outcome?: string, worktreeRoot?:
70
78
 
71
79
  resetToLocation(state, loopback.to);
72
80
  state.currentPhase = loopback.to.phase;
73
- state.currentSubStage = loopback.to.subStage;
81
+ state.currentStep = loopback.to.step;
74
82
 
75
83
  writeState(root, state);
76
84
 
77
85
  return {
78
86
  action: "loopback",
79
- from: { phase, subStage },
87
+ from,
80
88
  to: loopback.to,
81
89
  reason: loopback.reason,
82
90
  };
83
91
  }
84
92
 
85
- // Check if the phase is now complete
86
93
  if (isPhaseComplete(state, phase)) {
87
94
  state.phases[phase].status = "completed";
88
95
  state.phases[phase].completedAt = new Date().toISOString();
89
96
 
90
- // Find next phase
91
97
  const phaseIdx = PHASE_ORDER.indexOf(phase);
92
98
  const nextPhases = PHASE_ORDER.slice(phaseIdx + 1);
93
- let nextPhase: PhaseName | null = null;
99
+ let nextPhaseName: PhaseName | null = null;
94
100
 
95
101
  for (const np of nextPhases) {
96
102
  if (state.phases[np].status !== "skipped") {
97
- nextPhase = np;
103
+ nextPhaseName = np;
98
104
  break;
99
105
  }
100
106
  }
101
107
 
102
- if (!nextPhase) {
108
+ if (!nextPhaseName) {
103
109
  state.status = "completed";
104
110
  state.currentPhase = null;
105
- state.currentSubStage = null;
111
+ state.currentStep = null;
106
112
  writeState(root, state);
107
- archiveCompleted(root, state);
113
+ archiveOnComplete(root, state);
108
114
  return { action: "complete", message: "All phases complete. Work-kit finished." };
109
115
  }
110
116
 
111
- // Mark the first pending sub-stage of the next phase as "waiting"
112
- // so the observer can distinguish "running" from "waiting for human"
113
- const nextSubs = Object.entries(state.phases[nextPhase].subStages);
114
- const firstPending = nextSubs.find(([_, ss]) => ss.status === "pending");
117
+ const nextSteps = Object.entries(state.phases[nextPhaseName].steps);
118
+ const firstPending = nextSteps.find(([_, s]) => s.status === "pending");
115
119
  if (firstPending) {
116
120
  firstPending[1].status = "waiting";
117
121
  }
118
122
 
119
- state.currentPhase = nextPhase;
120
- state.currentSubStage = firstPending ? firstPending[0] : null;
123
+ state.currentPhase = nextPhaseName;
124
+ state.currentStep = firstPending ? firstPending[0] : null;
121
125
  writeState(root, state);
122
126
 
123
127
  return {
124
128
  action: "wait_for_user",
125
- message: `${phase} phase complete. Ready to start ${nextPhase}. Proceed?`,
129
+ message: `${phase} phase complete. Ready to start ${nextPhaseName}. Proceed?`,
126
130
  };
127
131
  }
128
132
 
129
- // Advance currentSubStage to the next pending sub-stage so the observer refreshes
130
- const nextSS = nextSubStageInPhase(state, phase);
131
- if (nextSS) {
132
- state.currentSubStage = nextSS;
133
- } else {
134
- state.currentSubStage = null;
135
- }
133
+ const next = nextStepInPhase(state, phase);
134
+ state.currentStep = next ?? null;
136
135
 
137
136
  writeState(root, state);
138
137
 
139
138
  return {
140
139
  action: "wait_for_user",
141
- message: `${phase}/${subStage} complete${outcome ? ` (outcome: ${outcome})` : ""}. Run \`npx work-kit-cli next\` to continue.`,
140
+ message: `${phase}/${step} complete${typedOutcome ? ` (outcome: ${typedOutcome})` : ""}. Run \`${CLI_BINARY} next\` to continue.`,
142
141
  };
143
142
  }
144
143
 
145
144
  // ── Archive on completion ──────────────────────────────────────────
146
145
 
147
- function resolveMainRepoRoot(worktreeRoot: string): string {
148
- try {
149
- // git worktree list --porcelain — first "worktree" line is always the main repo
150
- const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
151
- cwd: worktreeRoot,
152
- encoding: "utf-8",
153
- timeout: 5000,
154
- });
155
- const firstLine = output.split("\n").find(l => l.startsWith("worktree "));
156
- if (firstLine) return firstLine.slice("worktree ".length).trim();
157
- } catch {
158
- // fallback
159
- }
160
- return worktreeRoot;
146
+ function archiveFolderName(slug: string, completedAt: string): string {
147
+ return `${slug}-${completedAt.split("T")[0]}`;
161
148
  }
162
149
 
163
- function archiveCompleted(worktreeRoot: string, state: WorkKitState): void {
150
+ /**
151
+ * Single-step archive: copies state.md, tracker.json, and summary.md (if the
152
+ * wrap-up step wrote one) into `<main>/.work-kit-tracker/archive/<slug>-<date>/`,
153
+ * then appends a row to the index. Uses a single timestamp captured at the
154
+ * moment of completion to avoid date drift across multiple archive calls.
155
+ */
156
+ function archiveOnComplete(worktreeRoot: string, state: WorkKitState): void {
164
157
  const mainRoot = resolveMainRepoRoot(worktreeRoot);
165
- const date = new Date().toISOString().split("T")[0];
166
158
  const slug = state.slug;
167
- const wkDir = path.join(mainRoot, ".work-kit-tracker");
168
- const folderName = `${slug}-${date}`;
169
- const archiveDir = path.join(wkDir, "archive", folderName);
159
+ const completedAt = new Date().toISOString();
160
+ const date = completedAt.split("T")[0];
161
+
162
+ const wkDir = path.join(mainRoot, TRACKER_DIR);
163
+ const folderName = archiveFolderName(slug, completedAt);
164
+ const archiveDir = path.join(wkDir, ARCHIVE_DIR, folderName);
170
165
 
171
- // Ensure archive folder exists
172
166
  fs.mkdirSync(archiveDir, { recursive: true });
173
167
 
174
- // Archive state.md (full phase outputs)
175
168
  const stateMd = readStateMd(worktreeRoot);
176
169
  if (stateMd) {
177
- fs.writeFileSync(path.join(archiveDir, "state.md"), stateMd, "utf-8");
170
+ fs.writeFileSync(path.join(archiveDir, STATE_MD_FILE), stateMd, "utf-8");
178
171
  }
179
172
 
180
- // Archive tracker.json (full JSON with phases, timing, status)
181
- const trackerPath = statePath(worktreeRoot);
182
- if (fs.existsSync(trackerPath)) {
183
- fs.copyFileSync(trackerPath, path.join(archiveDir, "tracker.json"));
173
+ const trackerSrc = statePath(worktreeRoot);
174
+ if (fs.existsSync(trackerSrc)) {
175
+ fs.copyFileSync(trackerSrc, path.join(archiveDir, STATE_FILE));
184
176
  }
185
177
 
186
- // Write placeholder summary.md (wrap-up skill will overwrite with distilled summary)
187
- const summaryPath = path.join(archiveDir, "summary.md");
188
- if (!fs.existsSync(summaryPath)) {
189
- const completedPhases = PHASE_ORDER
190
- .filter(p => state.phases[p].status === "completed")
191
- .join("→");
192
- fs.writeFileSync(summaryPath, `---\nslug: ${slug}\nbranch: ${state.branch}\nstarted: ${state.started.split("T")[0]}\ncompleted: ${date}\nstatus: completed\n---\n\n## Summary\n\nPhases: ${completedPhases}\n\n_Pending wrap-up summary._\n`, "utf-8");
193
- }
194
-
195
- // Compute completed phases
178
+ const summarySrc = path.join(path.dirname(stateMdPath(worktreeRoot)), SUMMARY_FILE);
179
+ const summaryDest = path.join(archiveDir, SUMMARY_FILE);
196
180
  const completedPhases = PHASE_ORDER
197
181
  .filter(p => state.phases[p].status === "completed")
198
182
  .join("→");
199
183
 
200
- // Append to index.md with links to summary and archive folder
201
- const indexPath = path.join(wkDir, "index.md");
184
+ if (fs.existsSync(summarySrc)) {
185
+ fs.copyFileSync(summarySrc, summaryDest);
186
+ } else {
187
+ // Placeholder summary if the wrap-up agent didn't write one
188
+ fs.writeFileSync(
189
+ summaryDest,
190
+ `---\nslug: ${slug}\nbranch: ${state.branch}\nstarted: ${state.started.split("T")[0]}\ncompleted: ${date}\nstatus: completed\n---\n\n## Summary\n\nPhases: ${completedPhases}\n\n_Pending wrap-up summary._\n`,
191
+ "utf-8"
192
+ );
193
+ }
194
+
195
+ const indexPath = path.join(wkDir, INDEX_FILE);
202
196
  let indexContent = "";
203
197
  if (fs.existsSync(indexPath)) {
204
198
  indexContent = fs.readFileSync(indexPath, "utf-8");
@@ -206,8 +200,8 @@ function archiveCompleted(worktreeRoot: string, state: WorkKitState): void {
206
200
  if (!indexContent.includes("| Date ")) {
207
201
  indexContent = "| Date | Slug | PR | Status | Phases | Summary | Archive |\n| --- | --- | --- | --- | --- | --- | --- |\n";
208
202
  }
209
- const summaryLink = `[summary](archive/${folderName}/summary.md)`;
210
- const archiveLink = `[archive](archive/${folderName}/)`;
203
+ const summaryLink = `[summary](${ARCHIVE_DIR}/${folderName}/${SUMMARY_FILE})`;
204
+ const archiveLink = `[archive](${ARCHIVE_DIR}/${folderName}/)`;
211
205
  indexContent += `| ${date} | ${slug} | n/a | completed | ${completedPhases} | ${summaryLink} | ${archiveLink} |\n`;
212
206
  fs.writeFileSync(indexPath, indexContent, "utf-8");
213
207
  }
@@ -33,7 +33,7 @@ _work_kit() {
33
33
  commands=(
34
34
  'init:Create worktree and initialize state'
35
35
  'next:Get the next action to perform'
36
- 'complete:Mark a phase/sub-stage as complete'
36
+ 'complete:Mark a phase/step as complete'
37
37
  'status:Show current state summary'
38
38
  'context:Extract Final sections needed for a phase agent'
39
39
  'validate:Check prerequisites for a phase'
@@ -92,7 +92,7 @@ complete -c work-kit -f
92
92
  # Top-level commands
93
93
  complete -c work-kit -n '__fish_use_subcommand' -a 'init' -d 'Create worktree and initialize state'
94
94
  complete -c work-kit -n '__fish_use_subcommand' -a 'next' -d 'Get the next action to perform'
95
- complete -c work-kit -n '__fish_use_subcommand' -a 'complete' -d 'Mark a phase/sub-stage as complete'
95
+ complete -c work-kit -n '__fish_use_subcommand' -a 'complete' -d 'Mark a phase/step as complete'
96
96
  complete -c work-kit -n '__fish_use_subcommand' -a 'status' -d 'Show current state summary'
97
97
  complete -c work-kit -n '__fish_use_subcommand' -a 'context' -d 'Extract Final sections needed for a phase agent'
98
98
  complete -c work-kit -n '__fish_use_subcommand' -a 'validate' -d 'Check prerequisites for a phase'
@@ -62,7 +62,7 @@ export function doctorCommand(worktreeRoot?: string): { ok: boolean; checks: Che
62
62
  if (root && stateExists(root)) {
63
63
  try {
64
64
  const state = readState(root);
65
- if (state.version === 1 && state.slug && state.status) {
65
+ if (state.version === 2 && state.slug && state.status) {
66
66
  checks.push({ name: "state", status: "pass", message: `Active work-kit: "${state.slug}" (${state.status})` });
67
67
  } else {
68
68
  checks.push({ name: "state", status: "warn", message: "tracker.json exists but has unexpected structure" });