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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read and parse a JSON file. Returns null when the file is missing,
|
|
5
|
+
* unreadable, or contains invalid JSON. Use only when null is a meaningful
|
|
6
|
+
* "no config" answer; use direct fs/JSON for hard-required files.
|
|
7
|
+
*/
|
|
8
|
+
export function readJsonFile<T>(filePath: string): T | null {
|
|
9
|
+
let raw: string;
|
|
10
|
+
try {
|
|
11
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(raw) as T;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Difference in ms between two ISO timestamps; 0 on missing/invalid input. */
|
|
2
|
+
export function durationMs(start?: string, end?: string): number {
|
|
3
|
+
if (!start || !end) return 0;
|
|
4
|
+
const a = new Date(start).getTime();
|
|
5
|
+
const b = new Date(end).getTime();
|
|
6
|
+
if (isNaN(a) || isNaN(b)) return 0;
|
|
7
|
+
return Math.max(0, b - a);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Human-readable duration: "12s", "5m", "2h30m", or "—" for non-positive. */
|
|
11
|
+
export function formatDurationMs(ms: number): string {
|
|
12
|
+
if (ms <= 0) return "—";
|
|
13
|
+
const sec = Math.round(ms / 1000);
|
|
14
|
+
if (sec < 60) return `${sec}s`;
|
|
15
|
+
const min = Math.round(sec / 60);
|
|
16
|
+
if (min < 60) return `${min}m`;
|
|
17
|
+
const hr = Math.floor(min / 60);
|
|
18
|
+
const rem = min % 60;
|
|
19
|
+
return rem > 0 ? `${hr}h${rem}m` : `${hr}h`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Elapsed since `start` formatted with `formatDurationMs`. Returns "" on bad input. */
|
|
23
|
+
export function formatDurationSince(start: string): string {
|
|
24
|
+
const startMs = new Date(start).getTime();
|
|
25
|
+
if (isNaN(startMs)) return "";
|
|
26
|
+
return formatDurationMs(Date.now() - startMs);
|
|
27
|
+
}
|
|
@@ -6,7 +6,7 @@ describe("checkLoopback", () => {
|
|
|
6
6
|
it("plan/audit with 'revise' loops back to plan/blueprint", () => {
|
|
7
7
|
const result = checkLoopback("plan", "audit", "revise");
|
|
8
8
|
assert.notEqual(result, null);
|
|
9
|
-
assert.deepStrictEqual(result!.to, { phase: "plan",
|
|
9
|
+
assert.deepStrictEqual(result!.to, { phase: "plan", step: "blueprint" });
|
|
10
10
|
assert.ok(result!.reason.length > 0);
|
|
11
11
|
});
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ describe("checkLoopback", () => {
|
|
|
18
18
|
it("review/handoff with 'changes_requested' loops back to build/core", () => {
|
|
19
19
|
const result = checkLoopback("review", "handoff", "changes_requested");
|
|
20
20
|
assert.notEqual(result, null);
|
|
21
|
-
assert.deepStrictEqual(result!.to, { phase: "build",
|
|
21
|
+
assert.deepStrictEqual(result!.to, { phase: "build", step: "core" });
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it("build/core with 'done' returns null", () => {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { PhaseName, Location, LoopbackRecord, StepOutcome } from "../state/schema.js";
|
|
2
|
+
import { LOOPBACK_ROUTES } from "../config/loopback-routes.js";
|
|
3
|
+
|
|
4
|
+
interface LoopbackResult {
|
|
5
|
+
to: Location;
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Count how many times a specific loopback route has been taken.
|
|
11
|
+
*/
|
|
12
|
+
export function countLoopbacksForRoute(loopbacks: LoopbackRecord[], from: Location, to: Location): number {
|
|
13
|
+
return loopbacks.filter(
|
|
14
|
+
(lb) => lb.from.phase === from.phase && lb.from.step === from.step
|
|
15
|
+
&& lb.to.phase === to.phase && lb.to.step === to.step
|
|
16
|
+
).length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if completing a step with a given outcome should trigger a loop-back.
|
|
21
|
+
*/
|
|
22
|
+
export function checkLoopback(
|
|
23
|
+
phase: PhaseName,
|
|
24
|
+
step: string,
|
|
25
|
+
outcome?: StepOutcome
|
|
26
|
+
): LoopbackResult | null {
|
|
27
|
+
if (!outcome) return null;
|
|
28
|
+
|
|
29
|
+
const route = LOOPBACK_ROUTES.find(
|
|
30
|
+
(r) =>
|
|
31
|
+
r.from.phase === phase &&
|
|
32
|
+
r.from.step === step &&
|
|
33
|
+
r.triggerOutcome === outcome
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!route) return null;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
to: route.to,
|
|
40
|
+
reason: route.reason,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PhaseName, WorkKitState } from "../state/schema.js";
|
|
2
|
+
import { loadProjectConfig } from "../config/project-config.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Defines which steps run in parallel and which runs sequentially after.
|
|
6
|
+
*/
|
|
7
|
+
export interface ParallelGroup {
|
|
8
|
+
parallel: string[]; // steps that run concurrently
|
|
9
|
+
thenSequential?: string; // step that runs after all parallel complete
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default parallel groups per phase. Most projects should not need to override
|
|
14
|
+
* these — the defaults reflect the canonical work-kit pipeline.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_PARALLEL_GROUPS: Record<string, ParallelGroup> = {
|
|
17
|
+
test: {
|
|
18
|
+
parallel: ["verify", "e2e"],
|
|
19
|
+
thenSequential: "validate",
|
|
20
|
+
},
|
|
21
|
+
review: {
|
|
22
|
+
parallel: ["self-review", "security", "performance", "compliance"],
|
|
23
|
+
thenSequential: "handoff",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve parallel groups for a project, merging defaults with optional
|
|
29
|
+
* project config overrides at `<mainRepoRoot>/.work-kit-config.json`.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveParallelGroups(mainRepoRoot?: string): Record<string, ParallelGroup> {
|
|
32
|
+
if (!mainRepoRoot) return DEFAULT_PARALLEL_GROUPS;
|
|
33
|
+
const config = loadProjectConfig(mainRepoRoot);
|
|
34
|
+
if (!config.parallel || Object.keys(config.parallel).length === 0) {
|
|
35
|
+
return DEFAULT_PARALLEL_GROUPS;
|
|
36
|
+
}
|
|
37
|
+
return { ...DEFAULT_PARALLEL_GROUPS, ...config.parallel };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a step triggers a parallel group.
|
|
42
|
+
* Triggers on the first non-skipped, non-completed parallel member.
|
|
43
|
+
*/
|
|
44
|
+
export function getParallelGroup(phase: PhaseName, step: string, state?: WorkKitState): ParallelGroup | null {
|
|
45
|
+
const groups = resolveParallelGroups(state?.metadata?.mainRepoRoot);
|
|
46
|
+
const group = groups[phase];
|
|
47
|
+
if (!group) return null;
|
|
48
|
+
|
|
49
|
+
if (!group.parallel.includes(step)) return null;
|
|
50
|
+
|
|
51
|
+
if (state) {
|
|
52
|
+
const phaseState = state.phases[phase];
|
|
53
|
+
const firstActive = group.parallel.find((s) => {
|
|
54
|
+
const sState = phaseState?.steps[s];
|
|
55
|
+
return sState && sState.status !== "skipped" && sState.status !== "completed";
|
|
56
|
+
});
|
|
57
|
+
if (firstActive !== step) return null;
|
|
58
|
+
} else {
|
|
59
|
+
if (group.parallel[0] !== step) return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return group;
|
|
63
|
+
}
|
|
64
|
+
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import * as assert from "node:assert/strict";
|
|
3
|
+
import { nextStepInPhase, isPhaseComplete, determineNextStep } from "./transitions.js";
|
|
4
|
+
import type { WorkKitState, PhaseName, PhaseState, StepState } from "../state/schema.js";
|
|
5
|
+
import { PHASE_NAMES, STEPS_BY_PHASE } from "../state/schema.js";
|
|
6
|
+
|
|
7
|
+
function makeState(): WorkKitState {
|
|
8
|
+
const phases = {} as Record<PhaseName, PhaseState>;
|
|
9
|
+
for (const phase of PHASE_NAMES) {
|
|
10
|
+
const steps: Record<string, StepState> = {};
|
|
11
|
+
for (const s of STEPS_BY_PHASE[phase]) {
|
|
12
|
+
steps[s] = { status: "pending" };
|
|
13
|
+
}
|
|
14
|
+
phases[phase] = { status: "pending", steps };
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
version: 2,
|
|
18
|
+
slug: "test",
|
|
19
|
+
branch: "feature/test",
|
|
20
|
+
started: "2026-01-01",
|
|
21
|
+
mode: "full-kit",
|
|
22
|
+
status: "in-progress",
|
|
23
|
+
currentPhase: "plan",
|
|
24
|
+
currentStep: "clarify",
|
|
25
|
+
phases,
|
|
26
|
+
loopbacks: [],
|
|
27
|
+
metadata: { worktreeRoot: "/tmp/test", mainRepoRoot: "/tmp/test" },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("nextStepInPhase", () => {
|
|
32
|
+
it("returns first pending step", () => {
|
|
33
|
+
const state = makeState();
|
|
34
|
+
const result = nextStepInPhase(state, "plan");
|
|
35
|
+
assert.equal(result, "clarify");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns null when all complete or skipped", () => {
|
|
39
|
+
const state = makeState();
|
|
40
|
+
for (const s of Object.values(state.phases.plan.steps)) {
|
|
41
|
+
s.status = "completed";
|
|
42
|
+
}
|
|
43
|
+
const result = nextStepInPhase(state, "plan");
|
|
44
|
+
assert.equal(result, null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("skips completed steps and returns next pending", () => {
|
|
48
|
+
const state = makeState();
|
|
49
|
+
state.phases.plan.steps.clarify.status = "completed";
|
|
50
|
+
state.phases.plan.steps.investigate.status = "completed";
|
|
51
|
+
const result = nextStepInPhase(state, "plan");
|
|
52
|
+
assert.equal(result, "sketch");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("isPhaseComplete", () => {
|
|
57
|
+
it("returns true when all complete or skipped", () => {
|
|
58
|
+
const state = makeState();
|
|
59
|
+
for (const s of Object.values(state.phases.plan.steps)) {
|
|
60
|
+
s.status = "completed";
|
|
61
|
+
}
|
|
62
|
+
assert.equal(isPhaseComplete(state, "plan"), true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns true with mix of completed and skipped", () => {
|
|
66
|
+
const state = makeState();
|
|
67
|
+
let first = true;
|
|
68
|
+
for (const s of Object.values(state.phases.plan.steps)) {
|
|
69
|
+
s.status = first ? "skipped" : "completed";
|
|
70
|
+
first = false;
|
|
71
|
+
}
|
|
72
|
+
assert.equal(isPhaseComplete(state, "plan"), true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns false when some steps are pending", () => {
|
|
76
|
+
const state = makeState();
|
|
77
|
+
assert.equal(isPhaseComplete(state, "plan"), false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("determineNextStep", () => {
|
|
82
|
+
it("returns complete when state is completed", () => {
|
|
83
|
+
const state = makeState();
|
|
84
|
+
state.status = "completed";
|
|
85
|
+
const result = determineNextStep(state);
|
|
86
|
+
assert.equal(result.type, "complete");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns phase-boundary when no current phase", () => {
|
|
90
|
+
const state = makeState();
|
|
91
|
+
state.currentPhase = null;
|
|
92
|
+
const result = determineNextStep(state);
|
|
93
|
+
assert.equal(result.type, "phase-boundary");
|
|
94
|
+
assert.equal(result.phase, "plan");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns step for current phase with pending work", () => {
|
|
98
|
+
const state = makeState();
|
|
99
|
+
state.currentPhase = "plan";
|
|
100
|
+
state.phases.plan.status = "in-progress";
|
|
101
|
+
const result = determineNextStep(state);
|
|
102
|
+
assert.equal(result.type, "step");
|
|
103
|
+
assert.equal(result.phase, "plan");
|
|
104
|
+
assert.equal(result.step, "clarify");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("auto-proceeds to next phase by default when current phase is complete", () => {
|
|
108
|
+
const state = makeState();
|
|
109
|
+
state.currentPhase = "plan";
|
|
110
|
+
for (const s of Object.values(state.phases.plan.steps)) {
|
|
111
|
+
s.status = "completed";
|
|
112
|
+
}
|
|
113
|
+
const result = determineNextStep(state);
|
|
114
|
+
assert.equal(result.type, "phase-boundary");
|
|
115
|
+
assert.equal(result.phase, "build");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("returns wait-for-user when gated and current phase is complete", () => {
|
|
119
|
+
const state = makeState();
|
|
120
|
+
state.gated = true;
|
|
121
|
+
state.currentPhase = "plan";
|
|
122
|
+
for (const s of Object.values(state.phases.plan.steps)) {
|
|
123
|
+
s.status = "completed";
|
|
124
|
+
}
|
|
125
|
+
const result = determineNextStep(state);
|
|
126
|
+
assert.equal(result.type, "wait-for-user");
|
|
127
|
+
assert.equal(result.phase, "build");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { WorkKitState, PhaseName,
|
|
2
|
-
import { PHASE_ORDER } from "../config/
|
|
1
|
+
import { WorkKitState, PhaseName, STEPS_BY_PHASE } from "../state/schema.js";
|
|
2
|
+
import { PHASE_ORDER } from "../config/workflow.js";
|
|
3
3
|
|
|
4
4
|
export interface NextStep {
|
|
5
|
-
type: "
|
|
5
|
+
type: "step" | "phase-boundary" | "complete" | "wait-for-user";
|
|
6
6
|
phase?: PhaseName;
|
|
7
|
-
|
|
7
|
+
step?: string;
|
|
8
8
|
message?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Find the next pending
|
|
12
|
+
* Find the next pending step within a phase.
|
|
13
13
|
*/
|
|
14
|
-
export function
|
|
14
|
+
export function nextStepInPhase(state: WorkKitState, phase: PhaseName): string | null {
|
|
15
15
|
const phaseState = state.phases[phase];
|
|
16
|
-
const
|
|
16
|
+
const steps = STEPS_BY_PHASE[phase];
|
|
17
17
|
|
|
18
|
-
for (const
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
return
|
|
18
|
+
for (const step of steps) {
|
|
19
|
+
const stepState = phaseState.steps[step];
|
|
20
|
+
if (stepState && (stepState.status === "pending" || stepState.status === "in-progress" || stepState.status === "waiting")) {
|
|
21
|
+
return step;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Check if all non-skipped
|
|
28
|
+
* Check if all non-skipped steps in a phase are completed.
|
|
29
29
|
*/
|
|
30
30
|
export function isPhaseComplete(state: WorkKitState, phase: PhaseName): boolean {
|
|
31
31
|
const phaseState = state.phases[phase];
|
|
32
|
-
return Object.values(phaseState.
|
|
33
|
-
(
|
|
32
|
+
return Object.values(phaseState.steps).every(
|
|
33
|
+
(s) => s.status === "completed" || s.status === "skipped"
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -58,7 +58,6 @@ export function determineNextStep(state: WorkKitState): NextStep {
|
|
|
58
58
|
const currentPhase = state.currentPhase;
|
|
59
59
|
|
|
60
60
|
if (!currentPhase) {
|
|
61
|
-
// Find the next phase
|
|
62
61
|
const next = nextPhase(state);
|
|
63
62
|
if (!next) {
|
|
64
63
|
return { type: "complete", message: "All phases complete" };
|
|
@@ -68,7 +67,6 @@ export function determineNextStep(state: WorkKitState): NextStep {
|
|
|
68
67
|
|
|
69
68
|
// Check if current phase is complete
|
|
70
69
|
if (isPhaseComplete(state, currentPhase)) {
|
|
71
|
-
// Find next phase
|
|
72
70
|
const phaseIndex = PHASE_ORDER.indexOf(currentPhase);
|
|
73
71
|
const remainingPhases = PHASE_ORDER.slice(phaseIndex + 1);
|
|
74
72
|
|
|
@@ -76,14 +74,12 @@ export function determineNextStep(state: WorkKitState): NextStep {
|
|
|
76
74
|
const ps = state.phases[phase];
|
|
77
75
|
if (ps.status !== "skipped") {
|
|
78
76
|
if (state.gated) {
|
|
79
|
-
// Gated mode — wait for user confirmation before crossing
|
|
80
77
|
return {
|
|
81
78
|
type: "wait-for-user",
|
|
82
79
|
phase,
|
|
83
80
|
message: `${currentPhase} phase complete. Ready to start ${phase}. Proceed?`,
|
|
84
81
|
};
|
|
85
82
|
}
|
|
86
|
-
// Default — auto-proceed to next phase
|
|
87
83
|
return { type: "phase-boundary", phase, message: `${currentPhase} complete → starting ${phase}` };
|
|
88
84
|
}
|
|
89
85
|
}
|
|
@@ -91,10 +87,10 @@ export function determineNextStep(state: WorkKitState): NextStep {
|
|
|
91
87
|
return { type: "complete", message: "All phases complete" };
|
|
92
88
|
}
|
|
93
89
|
|
|
94
|
-
// Find next
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
return { type: "
|
|
90
|
+
// Find next step within current phase
|
|
91
|
+
const next = nextStepInPhase(state, currentPhase);
|
|
92
|
+
if (next) {
|
|
93
|
+
return { type: "step", phase: currentPhase, step: next };
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
return { type: "complete", message: `${currentPhase} phase complete` };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "work-kit-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Structured development workflow for Claude Code. Two modes, 6 phases, 27
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Structured development workflow for Claude Code. Two modes, 6 phases, 27 steps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"work-kit": "cli/bin/work-kit.mjs",
|
package/skills/auto-kit/SKILL.md
CHANGED
|
@@ -6,7 +6,7 @@ argument-hint: "[--gated] [description]"
|
|
|
6
6
|
allowed-tools: Agent, Bash, Read, Write, Edit, Glob, Grep
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
You are the **Work Orchestrator (Auto Mode)**. You analyze the request first, then build a tailored workflow with only the phases and
|
|
9
|
+
You are the **Work Orchestrator (Auto Mode)**. You analyze the request first, then build a tailored workflow with only the phases and steps that are actually needed.
|
|
10
10
|
|
|
11
11
|
Best for: bug fixes, small changes, refactors, or well-understood tasks.
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ Best for: bug fixes, small changes, refactors, or well-understood tasks.
|
|
|
14
14
|
|
|
15
15
|
Before starting, verify the CLI is installed:
|
|
16
16
|
```bash
|
|
17
|
-
|
|
17
|
+
work-kit doctor
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
If `work-kit` is not found, ask the user to install it:
|
|
@@ -22,7 +22,7 @@ If `work-kit` is not found, ask the user to install it:
|
|
|
22
22
|
|
|
23
23
|
Do not proceed until `doctor` reports all checks passed.
|
|
24
24
|
|
|
25
|
-
## All Available
|
|
25
|
+
## All Available Steps
|
|
26
26
|
|
|
27
27
|
These are the building blocks you pick from:
|
|
28
28
|
|
|
@@ -47,13 +47,13 @@ Before creating the worktree, perform a quick analysis:
|
|
|
47
47
|
- **feature** — new capability (small to medium scope)
|
|
48
48
|
- **large-feature** — new capability (large scope, multiple systems)
|
|
49
49
|
3. **Scan the codebase** — quick look at affected areas (not a full investigation)
|
|
50
|
-
4. **Build the workflow** — select only the
|
|
50
|
+
4. **Build the workflow** — select only the steps needed
|
|
51
51
|
|
|
52
52
|
### Step 2: Build Dynamic Workflow
|
|
53
53
|
|
|
54
|
-
Based on the classification, select
|
|
54
|
+
Based on the classification, select steps. Use this table as a starting point, then adjust based on the specific request:
|
|
55
55
|
|
|
56
|
-
|
|
|
56
|
+
| Step | bug-fix | small-change | refactor | feature | large-feature |
|
|
57
57
|
|------------------------|---------|--------------|----------|---------|---------------|
|
|
58
58
|
| **Plan: Clarify** | YES | YES | YES | YES | YES |
|
|
59
59
|
| **Plan: Investigate** | YES | skip | YES | YES | YES |
|
|
@@ -95,34 +95,34 @@ The table is a guide, not a rigid rule. Adjust based on the actual request:
|
|
|
95
95
|
```bash
|
|
96
96
|
git worktree add worktrees/<slug> -b feature/<slug>
|
|
97
97
|
cd worktrees/<slug>
|
|
98
|
-
|
|
98
|
+
work-kit init --mode auto --description "<description>" --classification <classification>
|
|
99
99
|
```
|
|
100
100
|
If the user passed `--gated` (e.g., `/auto-kit --gated fix login bug`), add `--gated` to the init command. Strip `--gated` from the description text.
|
|
101
|
-
2. Show the workflow to the user: `
|
|
102
|
-
3. User can adjust: `
|
|
101
|
+
2. Show the workflow to the user: `work-kit workflow`
|
|
102
|
+
3. User can adjust: `work-kit workflow --add review/security` or `work-kit workflow --remove test/e2e`
|
|
103
103
|
4. **Wait for approval** — user can add/remove steps before proceeding
|
|
104
104
|
5. Once approved, start the execution loop
|
|
105
105
|
|
|
106
106
|
## Continuing Work (`/auto-kit` with no args)
|
|
107
107
|
|
|
108
|
-
1. Run `
|
|
108
|
+
1. Run `work-kit bootstrap` to detect session state
|
|
109
109
|
2. Parse the JSON response:
|
|
110
110
|
- If `active: false` — no session found, ask the user for a description and start new work
|
|
111
111
|
- If `recovery` is set — report the recovery suggestion to the user before continuing
|
|
112
|
-
- If `active: true` — report current state (slug, phase,
|
|
112
|
+
- If `active: true` — report current state (slug, phase, step) to the user
|
|
113
113
|
3. `cd` into the worktree directory
|
|
114
|
-
4. Run `
|
|
114
|
+
4. Run `work-kit next` to get the next action
|
|
115
115
|
5. Follow the execution loop below
|
|
116
116
|
|
|
117
117
|
## Step Validation
|
|
118
118
|
|
|
119
119
|
All validation is handled by the CLI. The `next` command enforces order, phase boundaries, and prerequisites automatically.
|
|
120
120
|
|
|
121
|
-
To add/remove steps mid-work: `
|
|
121
|
+
To add/remove steps mid-work: `work-kit workflow --add <phase/step>` or `--remove <phase/step>`. Completed steps cannot be removed.
|
|
122
122
|
|
|
123
123
|
## Agent Architecture
|
|
124
124
|
|
|
125
|
-
Same as full-kit: each phase runs as a **fresh agent** to keep context focused. The difference is that each agent only runs the
|
|
125
|
+
Same as full-kit: each phase runs as a **fresh agent** to keep context focused. The difference is that each agent only runs the steps in the `## Workflow` checklist.
|
|
126
126
|
|
|
127
127
|
```
|
|
128
128
|
Orchestrator (main agent — you)
|
|
@@ -177,23 +177,23 @@ Orchestrator (main agent — you)
|
|
|
177
177
|
|
|
178
178
|
Each phase writes a `### <Phase>: Final` section — a self-contained summary. The next agent reads **only** the Final sections it needs.
|
|
179
179
|
|
|
180
|
-
If a phase has fewer
|
|
180
|
+
If a phase has fewer steps in the workflow, the Final section still covers the same output — just with less detail where steps were skipped.
|
|
181
181
|
|
|
182
182
|
## Execution Loop
|
|
183
183
|
|
|
184
184
|
The CLI manages all state transitions, prerequisites, and loopbacks. Follow this loop:
|
|
185
185
|
|
|
186
|
-
1. Run `
|
|
186
|
+
1. Run `work-kit next` to get the next action
|
|
187
187
|
2. Parse the JSON response
|
|
188
188
|
3. Follow the action type:
|
|
189
|
-
- **`spawn_agent`**: Use the Agent tool with the provided `agentPrompt`. Pass `skillFile` path for reference. After the agent completes: `
|
|
190
|
-
- **`spawn_parallel_agents`**: Spawn all agents in the `agents` array in parallel using the Agent tool. Wait for all to complete. Then spawn `thenSequential` if provided. After all complete: `
|
|
191
|
-
- **`wait_for_user`**: Report the message to the user and stop. Wait for them to say "proceed" before running `
|
|
192
|
-
- **`loopback`**: Report the loopback to the user, then run `
|
|
189
|
+
- **`spawn_agent`**: Use the Agent tool with the provided `agentPrompt`. Pass `skillFile` path for reference. After the agent completes: `work-kit complete <phase>/<step> --outcome <outcome>`
|
|
190
|
+
- **`spawn_parallel_agents`**: Spawn all agents in the `agents` array in parallel using the Agent tool. Wait for all to complete. Then spawn `thenSequential` if provided. After all complete: `work-kit complete <onComplete target>`
|
|
191
|
+
- **`wait_for_user`**: Report the message to the user and stop. Wait for them to say "proceed" before running `work-kit next` again.
|
|
192
|
+
- **`loopback`**: Report the loopback to the user, then run `work-kit next` to continue from the target.
|
|
193
193
|
- **`complete`**: Done — run wrap-up if not already done.
|
|
194
194
|
- **`error`**: Report the error and suggestion to the user. Stop.
|
|
195
|
-
4. After each agent completes: `
|
|
196
|
-
5. Then `
|
|
195
|
+
4. After each agent completes: `work-kit complete <phase>/<step> --outcome <outcome>`
|
|
196
|
+
5. Then `work-kit next` again to continue
|
|
197
197
|
|
|
198
198
|
## Loop-Back Rules
|
|
199
199
|
|
|
@@ -13,7 +13,7 @@ Cancels the active work-kit session and cleans up all artifacts.
|
|
|
13
13
|
|
|
14
14
|
1. Finds the active work-kit session (via `bootstrap`)
|
|
15
15
|
2. Confirms with the user before proceeding
|
|
16
|
-
3. Runs `
|
|
16
|
+
3. Runs `work-kit cancel` to:
|
|
17
17
|
- Remove `.work-kit/` state directory
|
|
18
18
|
- Remove the git worktree
|
|
19
19
|
- Delete the feature branch
|
|
@@ -21,14 +21,14 @@ Cancels the active work-kit session and cleans up all artifacts.
|
|
|
21
21
|
|
|
22
22
|
## Instructions
|
|
23
23
|
|
|
24
|
-
1. Run `
|
|
24
|
+
1. Run `work-kit bootstrap` to detect the active session
|
|
25
25
|
2. If no active session: tell the user there's nothing to cancel
|
|
26
26
|
3. If active: show the user what will be cancelled:
|
|
27
27
|
- Slug and branch name
|
|
28
|
-
- Current phase and
|
|
28
|
+
- Current phase and step
|
|
29
29
|
- Any uncommitted work in the worktree will be lost
|
|
30
30
|
4. **Ask the user to confirm** — do not proceed without explicit confirmation
|
|
31
31
|
5. `cd` into the worktree directory
|
|
32
|
-
6. Run `
|
|
32
|
+
6. Run `work-kit cancel`
|
|
33
33
|
7. `cd` back to the main repo root
|
|
34
34
|
8. Report the result to the user
|