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.
- 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 +162 -75
- 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
|
@@ -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,
|
|
5
|
-
import { PHASE_NAMES,
|
|
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
|
|
11
|
-
for (const
|
|
12
|
-
|
|
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",
|
|
14
|
+
phases[phase] = { status: "pending", steps };
|
|
15
15
|
}
|
|
16
16
|
return {
|
|
17
|
-
version:
|
|
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
|
-
|
|
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",
|
|
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
|
|
46
|
-
assert.throws(() => parseLocation("plan/nonexistent"), /Unknown
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
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",
|
|
70
|
+
resetToLocation(state, { phase: "plan", step: "blueprint" });
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
assert.equal(state.phases.plan.
|
|
74
|
-
assert.equal(state.phases.plan.
|
|
75
|
-
assert.equal(state.phases.plan.
|
|
76
|
-
assert.equal(state.phases.plan.
|
|
77
|
-
assert.equal(state.phases.plan.
|
|
78
|
-
assert.equal(state.phases.plan.
|
|
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.
|
|
82
|
-
assert.equal(state.phases.plan.
|
|
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.
|
|
89
|
+
assert.equal(state.phases.build.steps.core.status, "pending");
|
|
90
90
|
});
|
|
91
91
|
});
|
package/cli/src/state/helpers.ts
CHANGED
|
@@ -1,47 +1,59 @@
|
|
|
1
|
-
import { PHASE_NAMES,
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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/
|
|
24
|
+
throw new Error(`Invalid location "${input}". Expected format: phase/step (e.g., plan/clarify)`);
|
|
13
25
|
}
|
|
14
|
-
const [phase,
|
|
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
|
|
19
|
-
if (!
|
|
20
|
-
throw new Error(`Unknown
|
|
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,
|
|
34
|
+
return { phase: phase as PhaseName, step };
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
/**
|
|
26
|
-
* Reset state from a target location forward: marks the target
|
|
27
|
-
* and all subsequent
|
|
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.
|
|
35
|
-
throw new Error(`
|
|
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 [
|
|
40
|
-
if (
|
|
41
|
-
if (reset && (
|
|
42
|
-
|
|
43
|
-
delete
|
|
44
|
-
delete
|
|
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
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
delete
|
|
60
|
-
delete
|
|
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
|
}
|
package/cli/src/state/schema.ts
CHANGED
|
@@ -1,61 +1,96 @@
|
|
|
1
|
-
// ── Phase &
|
|
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
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
9
|
-
export const
|
|
10
|
-
export const
|
|
11
|
-
export const
|
|
12
|
-
|
|
13
|
-
export type
|
|
14
|
-
export type
|
|
15
|
-
export type
|
|
16
|
-
export type
|
|
17
|
-
export type
|
|
18
|
-
export type
|
|
19
|
-
|
|
20
|
-
export type
|
|
21
|
-
|
|
22
|
-
export const
|
|
23
|
-
plan:
|
|
24
|
-
build:
|
|
25
|
-
test:
|
|
26
|
-
review:
|
|
27
|
-
deploy:
|
|
28
|
-
"wrap-up":
|
|
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
|
-
// ──
|
|
31
|
+
// ── Mode Constants ──────────────────────────────────────────────────
|
|
32
32
|
|
|
33
|
-
export
|
|
33
|
+
export const MODE_FULL = "full-kit" as const;
|
|
34
|
+
export const MODE_AUTO = "auto-kit" as const;
|
|
34
35
|
|
|
35
|
-
// ──
|
|
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
|
|
73
|
+
export type StepStatus = "pending" | "in-progress" | "completed" | "skipped" | "waiting";
|
|
39
74
|
|
|
40
|
-
export interface
|
|
41
|
-
status:
|
|
42
|
-
outcome?:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
+
step: string;
|
|
73
108
|
included: boolean;
|
|
74
109
|
}
|
|
75
110
|
|
|
76
|
-
// ──
|
|
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:
|
|
118
|
+
version: 2;
|
|
80
119
|
slug: string;
|
|
81
120
|
branch: string;
|
|
82
121
|
started: string;
|
|
83
|
-
mode:
|
|
122
|
+
mode: typeof MODE_FULL | typeof MODE_AUTO;
|
|
84
123
|
gated?: boolean;
|
|
85
124
|
classification?: Classification;
|
|
86
|
-
status:
|
|
125
|
+
status: WorkStatus;
|
|
126
|
+
/** ISO timestamp the work was paused; cleared on resume. */
|
|
127
|
+
pausedAt?: string;
|
|
87
128
|
currentPhase: PhaseName | null;
|
|
88
|
-
|
|
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
|
-
|
|
143
|
+
step: string;
|
|
103
144
|
skillFile: string;
|
|
104
145
|
agentPrompt: string;
|
|
105
|
-
outputFile?: string;
|
|
146
|
+
outputFile?: string;
|
|
106
147
|
}
|
|
107
148
|
|
|
108
149
|
export type Action =
|
|
109
|
-
| { action: "spawn_agent"; phase: PhaseName;
|
|
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 };
|
package/cli/src/state/store.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
5
|
-
import { PHASE_NAMES,
|
|
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
|
|
11
|
-
for (const
|
|
12
|
-
|
|
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",
|
|
14
|
+
phases[phase] = { status: "pending", steps };
|
|
15
15
|
}
|
|
16
16
|
return {
|
|
17
|
-
version:
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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.
|
|
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.
|
|
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/
|
|
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.
|
|
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
|
|
55
|
+
message: `deploy requires review/handoff to exist and be approved. Handoff step not found.`,
|
|
57
56
|
missingPrerequisite: "review",
|
|
58
57
|
};
|
|
59
58
|
}
|
package/cli/src/utils/colors.ts
CHANGED
|
@@ -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}`;
|