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.
- package/README.md +13 -13
- package/cli/src/commands/bootstrap.ts +39 -13
- package/cli/src/commands/cancel.ts +1 -16
- package/cli/src/commands/complete.ts +92 -98
- package/cli/src/commands/completions.ts +2 -2
- package/cli/src/commands/doctor.ts +1 -1
- package/cli/src/commands/init.ts +40 -32
- package/cli/src/commands/loopback.ts +8 -11
- package/cli/src/commands/next.ts +64 -51
- package/cli/src/commands/pause-resume.test.ts +142 -0
- package/cli/src/commands/pause.ts +34 -0
- package/cli/src/commands/report.ts +217 -0
- package/cli/src/commands/resume.ts +38 -0
- package/cli/src/commands/setup.ts +136 -0
- package/cli/src/commands/status.ts +6 -6
- package/cli/src/commands/uninstall.ts +8 -3
- package/cli/src/commands/workflow.ts +27 -27
- package/cli/src/config/agent-map.ts +9 -9
- package/cli/src/config/constants.ts +44 -0
- package/cli/src/config/loopback-routes.ts +13 -13
- package/cli/src/config/project-config.test.ts +127 -0
- package/cli/src/config/project-config.ts +106 -0
- package/cli/src/config/{phases.ts → workflow.ts} +40 -23
- package/cli/src/context/prompt-builder.ts +10 -9
- package/cli/src/index.ts +63 -7
- package/cli/src/observer/data.ts +64 -56
- package/cli/src/observer/renderer.ts +101 -79
- package/cli/src/state/helpers.test.ts +28 -28
- package/cli/src/state/helpers.ts +37 -25
- package/cli/src/state/schema.ts +88 -45
- package/cli/src/state/store.ts +92 -7
- package/cli/src/state/validators.test.ts +13 -13
- package/cli/src/state/validators.ts +3 -4
- package/cli/src/utils/colors.ts +2 -0
- package/cli/src/utils/json.ts +20 -0
- package/cli/src/utils/time.ts +27 -0
- package/cli/src/{engine → workflow}/loopbacks.test.ts +2 -2
- package/cli/src/workflow/loopbacks.ts +42 -0
- package/cli/src/workflow/parallel.ts +64 -0
- package/cli/src/workflow/transitions.test.ts +129 -0
- package/cli/src/{engine → workflow}/transitions.ts +18 -22
- package/package.json +2 -2
- package/skills/auto-kit/SKILL.md +22 -22
- package/skills/cancel-kit/SKILL.md +4 -4
- package/skills/full-kit/SKILL.md +23 -23
- package/skills/pause-kit/SKILL.md +25 -0
- package/skills/resume-kit/SKILL.md +28 -0
- package/skills/wk-bootstrap/SKILL.md +5 -5
- package/skills/wk-build/SKILL.md +10 -10
- package/skills/wk-build/{stages → steps}/commit.md +1 -1
- package/skills/wk-build/{stages → steps}/core.md +3 -3
- package/skills/wk-build/{stages → steps}/integration.md +2 -2
- package/skills/wk-build/{stages → steps}/migration.md +1 -1
- package/skills/wk-build/{stages → steps}/red.md +1 -1
- package/skills/wk-build/{stages → steps}/refactor.md +1 -1
- package/skills/wk-build/{stages → steps}/setup.md +1 -1
- package/skills/wk-build/{stages → steps}/ui.md +1 -1
- package/skills/wk-deploy/SKILL.md +6 -6
- package/skills/wk-deploy/{stages → steps}/merge.md +1 -1
- package/skills/wk-deploy/{stages → steps}/monitor.md +1 -1
- package/skills/wk-deploy/{stages → steps}/remediate.md +1 -1
- package/skills/wk-plan/SKILL.md +13 -13
- package/skills/wk-plan/{stages → steps}/architecture.md +1 -1
- package/skills/wk-plan/{stages → steps}/audit.md +2 -2
- package/skills/wk-plan/{stages → steps}/blueprint.md +2 -2
- package/skills/wk-plan/{stages → steps}/clarify.md +1 -1
- package/skills/wk-plan/{stages → steps}/investigate.md +1 -1
- package/skills/wk-plan/{stages → steps}/scope.md +1 -1
- package/skills/wk-plan/{stages → steps}/sketch.md +1 -1
- package/skills/wk-plan/{stages → steps}/ux-flow.md +1 -1
- package/skills/wk-review/SKILL.md +10 -10
- package/skills/wk-review/{stages → steps}/compliance.md +1 -1
- package/skills/wk-review/{stages → steps}/handoff.md +2 -2
- package/skills/wk-review/{stages → steps}/performance.md +1 -1
- package/skills/wk-review/{stages → steps}/security.md +1 -1
- package/skills/wk-review/{stages → steps}/self-review.md +1 -1
- package/skills/wk-test/SKILL.md +8 -8
- package/skills/wk-test/{stages → steps}/e2e.md +1 -1
- package/skills/wk-test/{stages → steps}/validate.md +1 -1
- package/skills/wk-test/{stages → steps}/verify.md +1 -1
- package/skills/wk-wrap-up/SKILL.md +6 -5
- package/skills/wk-wrap-up/steps/summary.md +86 -0
- package/cli/src/engine/loopbacks.ts +0 -32
- package/cli/src/engine/parallel.ts +0 -60
- package/cli/src/engine/transitions.test.ts +0 -129
- /package/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
|
|
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
|
|
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
|
|
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
|
|
52
|
-
| `complete` | Mark the current
|
|
53
|
-
| `status` | Show current workflow state (phase,
|
|
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 |
|
|
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,
|
|
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/
|
|
132
|
+
plan/steps/ # 8 step files
|
|
133
133
|
build/SKILL.md # Build phase runner
|
|
134
|
-
build/
|
|
134
|
+
build/steps/ # 8 step files
|
|
135
135
|
test/SKILL.md # Test phase runner
|
|
136
|
-
test/
|
|
136
|
+
test/steps/ # 3 step files
|
|
137
137
|
review/SKILL.md # Review phase runner
|
|
138
|
-
review/
|
|
138
|
+
review/steps/ # 5 step files
|
|
139
139
|
deploy/SKILL.md # Deploy phase runner
|
|
140
|
-
deploy/
|
|
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
|
-
|
|
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
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
//
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
|
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,
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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 (
|
|
36
|
-
return { action: "error", message: `${phase}/${
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
const loopback = checkLoopback(phase, subStage, outcome);
|
|
59
|
+
const loopback = checkLoopback(phase, step, typedOutcome);
|
|
48
60
|
if (loopback) {
|
|
49
|
-
|
|
50
|
-
const sameRouteCount = state.loopbacks.
|
|
51
|
-
|
|
52
|
-
|
|
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}/${
|
|
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
|
|
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.
|
|
81
|
+
state.currentStep = loopback.to.step;
|
|
74
82
|
|
|
75
83
|
writeState(root, state);
|
|
76
84
|
|
|
77
85
|
return {
|
|
78
86
|
action: "loopback",
|
|
79
|
-
from
|
|
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
|
|
99
|
+
let nextPhaseName: PhaseName | null = null;
|
|
94
100
|
|
|
95
101
|
for (const np of nextPhases) {
|
|
96
102
|
if (state.phases[np].status !== "skipped") {
|
|
97
|
-
|
|
103
|
+
nextPhaseName = np;
|
|
98
104
|
break;
|
|
99
105
|
}
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
if (!
|
|
108
|
+
if (!nextPhaseName) {
|
|
103
109
|
state.status = "completed";
|
|
104
110
|
state.currentPhase = null;
|
|
105
|
-
state.
|
|
111
|
+
state.currentStep = null;
|
|
106
112
|
writeState(root, state);
|
|
107
|
-
|
|
113
|
+
archiveOnComplete(root, state);
|
|
108
114
|
return { action: "complete", message: "All phases complete. Work-kit finished." };
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
|
|
112
|
-
|
|
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 =
|
|
120
|
-
state.
|
|
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 ${
|
|
129
|
+
message: `${phase} phase complete. Ready to start ${nextPhaseName}. Proceed?`,
|
|
126
130
|
};
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
|
|
130
|
-
|
|
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}/${
|
|
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
|
|
148
|
-
|
|
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
|
-
|
|
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
|
|
168
|
-
const
|
|
169
|
-
|
|
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,
|
|
170
|
+
fs.writeFileSync(path.join(archiveDir, STATE_MD_FILE), stateMd, "utf-8");
|
|
178
171
|
}
|
|
179
172
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
187
|
-
const
|
|
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
|
-
|
|
201
|
-
|
|
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](
|
|
210
|
-
const archiveLink = `[archive](
|
|
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/
|
|
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/
|
|
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 ===
|
|
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" });
|