@abdullahsahmad/work-kit 0.1.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 +147 -0
- package/cli/bin/work-kit.mjs +18 -0
- package/cli/src/commands/complete.ts +123 -0
- package/cli/src/commands/completions.ts +137 -0
- package/cli/src/commands/context.ts +41 -0
- package/cli/src/commands/doctor.ts +79 -0
- package/cli/src/commands/init.test.ts +116 -0
- package/cli/src/commands/init.ts +184 -0
- package/cli/src/commands/loopback.ts +64 -0
- package/cli/src/commands/next.ts +172 -0
- package/cli/src/commands/observe.ts +144 -0
- package/cli/src/commands/setup.ts +159 -0
- package/cli/src/commands/status.ts +50 -0
- package/cli/src/commands/uninstall.ts +89 -0
- package/cli/src/commands/upgrade.ts +12 -0
- package/cli/src/commands/validate.ts +34 -0
- package/cli/src/commands/workflow.ts +125 -0
- package/cli/src/config/agent-map.ts +62 -0
- package/cli/src/config/loopback-routes.ts +45 -0
- package/cli/src/config/phases.ts +119 -0
- package/cli/src/context/extractor.test.ts +77 -0
- package/cli/src/context/extractor.ts +73 -0
- package/cli/src/context/prompt-builder.ts +70 -0
- package/cli/src/engine/loopbacks.test.ts +33 -0
- package/cli/src/engine/loopbacks.ts +32 -0
- package/cli/src/engine/parallel.ts +60 -0
- package/cli/src/engine/phases.ts +23 -0
- package/cli/src/engine/transitions.test.ts +117 -0
- package/cli/src/engine/transitions.ts +97 -0
- package/cli/src/index.ts +248 -0
- package/cli/src/observer/data.ts +237 -0
- package/cli/src/observer/renderer.ts +316 -0
- package/cli/src/observer/watcher.ts +99 -0
- package/cli/src/state/helpers.test.ts +91 -0
- package/cli/src/state/helpers.ts +65 -0
- package/cli/src/state/schema.ts +113 -0
- package/cli/src/state/store.ts +82 -0
- package/cli/src/state/validators.test.ts +105 -0
- package/cli/src/state/validators.ts +81 -0
- package/cli/src/utils/colors.ts +12 -0
- package/package.json +49 -0
- package/skills/auto-kit/SKILL.md +214 -0
- package/skills/build/SKILL.md +88 -0
- package/skills/build/stages/commit.md +43 -0
- package/skills/build/stages/core.md +48 -0
- package/skills/build/stages/integration.md +44 -0
- package/skills/build/stages/migration.md +41 -0
- package/skills/build/stages/red.md +44 -0
- package/skills/build/stages/refactor.md +48 -0
- package/skills/build/stages/setup.md +42 -0
- package/skills/build/stages/ui.md +51 -0
- package/skills/deploy/SKILL.md +62 -0
- package/skills/deploy/stages/merge.md +47 -0
- package/skills/deploy/stages/monitor.md +39 -0
- package/skills/deploy/stages/remediate.md +54 -0
- package/skills/full-kit/SKILL.md +195 -0
- package/skills/plan/SKILL.md +77 -0
- package/skills/plan/stages/architecture.md +53 -0
- package/skills/plan/stages/audit.md +58 -0
- package/skills/plan/stages/blueprint.md +60 -0
- package/skills/plan/stages/clarify.md +61 -0
- package/skills/plan/stages/investigate.md +47 -0
- package/skills/plan/stages/scope.md +46 -0
- package/skills/plan/stages/sketch.md +44 -0
- package/skills/plan/stages/ux-flow.md +49 -0
- package/skills/review/SKILL.md +104 -0
- package/skills/review/stages/compliance.md +48 -0
- package/skills/review/stages/handoff.md +59 -0
- package/skills/review/stages/performance.md +45 -0
- package/skills/review/stages/security.md +49 -0
- package/skills/review/stages/self-review.md +41 -0
- package/skills/test/SKILL.md +83 -0
- package/skills/test/stages/e2e.md +44 -0
- package/skills/test/stages/validate.md +51 -0
- package/skills/test/stages/verify.md +41 -0
- package/skills/wrap-up/SKILL.md +107 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { WorkKitState, PhaseName, SUBSTAGES_BY_PHASE } from "../state/schema.js";
|
|
2
|
+
import { PHASE_ORDER } from "../config/phases.js";
|
|
3
|
+
|
|
4
|
+
export interface NextStep {
|
|
5
|
+
type: "sub-stage" | "phase-boundary" | "complete" | "wait-for-user";
|
|
6
|
+
phase?: PhaseName;
|
|
7
|
+
subStage?: string;
|
|
8
|
+
message?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find the next pending sub-stage within a phase.
|
|
13
|
+
*/
|
|
14
|
+
export function nextSubStageInPhase(state: WorkKitState, phase: PhaseName): string | null {
|
|
15
|
+
const phaseState = state.phases[phase];
|
|
16
|
+
const subStages = SUBSTAGES_BY_PHASE[phase];
|
|
17
|
+
|
|
18
|
+
for (const ss of subStages) {
|
|
19
|
+
const ssState = phaseState.subStages[ss];
|
|
20
|
+
if (ssState && (ssState.status === "pending" || ssState.status === "in-progress")) {
|
|
21
|
+
return ss;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if all non-skipped sub-stages in a phase are completed.
|
|
29
|
+
*/
|
|
30
|
+
export function isPhaseComplete(state: WorkKitState, phase: PhaseName): boolean {
|
|
31
|
+
const phaseState = state.phases[phase];
|
|
32
|
+
return Object.values(phaseState.subStages).every(
|
|
33
|
+
(ss) => ss.status === "completed" || ss.status === "skipped"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find the next phase that needs work.
|
|
39
|
+
*/
|
|
40
|
+
export function nextPhase(state: WorkKitState): PhaseName | null {
|
|
41
|
+
for (const phase of PHASE_ORDER) {
|
|
42
|
+
const ps = state.phases[phase];
|
|
43
|
+
if (ps.status === "pending" || ps.status === "in-progress") {
|
|
44
|
+
return phase;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Determine the next step in the workflow.
|
|
52
|
+
*/
|
|
53
|
+
export function determineNextStep(state: WorkKitState): NextStep {
|
|
54
|
+
if (state.status === "completed") {
|
|
55
|
+
return { type: "complete", message: "Work-kit is complete" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const currentPhase = state.currentPhase;
|
|
59
|
+
|
|
60
|
+
if (!currentPhase) {
|
|
61
|
+
// Find the next phase
|
|
62
|
+
const next = nextPhase(state);
|
|
63
|
+
if (!next) {
|
|
64
|
+
return { type: "complete", message: "All phases complete" };
|
|
65
|
+
}
|
|
66
|
+
return { type: "phase-boundary", phase: next, message: `Starting ${next} phase` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check if current phase is complete
|
|
70
|
+
if (isPhaseComplete(state, currentPhase)) {
|
|
71
|
+
// Find next phase
|
|
72
|
+
const phaseIndex = PHASE_ORDER.indexOf(currentPhase);
|
|
73
|
+
const remainingPhases = PHASE_ORDER.slice(phaseIndex + 1);
|
|
74
|
+
|
|
75
|
+
for (const phase of remainingPhases) {
|
|
76
|
+
const ps = state.phases[phase];
|
|
77
|
+
if (ps.status !== "skipped") {
|
|
78
|
+
// Phase boundary — wait for user confirmation before crossing
|
|
79
|
+
return {
|
|
80
|
+
type: "wait-for-user",
|
|
81
|
+
phase,
|
|
82
|
+
message: `${currentPhase} phase complete. Ready to start ${phase}. Proceed?`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { type: "complete", message: "All phases complete" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Find next sub-stage within current phase
|
|
91
|
+
const nextSS = nextSubStageInPhase(state, currentPhase);
|
|
92
|
+
if (nextSS) {
|
|
93
|
+
return { type: "sub-stage", phase: currentPhase, subStage: nextSS };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { type: "complete", message: `${currentPhase} phase complete` };
|
|
97
|
+
}
|
package/cli/src/index.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { initCommand } from "./commands/init.js";
|
|
5
|
+
import { statusCommand } from "./commands/status.js";
|
|
6
|
+
import { nextCommand } from "./commands/next.js";
|
|
7
|
+
import { completeCommand } from "./commands/complete.js";
|
|
8
|
+
import { validateCommand } from "./commands/validate.js";
|
|
9
|
+
import { contextCommand } from "./commands/context.js";
|
|
10
|
+
import { loopbackCommand } from "./commands/loopback.js";
|
|
11
|
+
import { workflowCommand } from "./commands/workflow.js";
|
|
12
|
+
import { doctorCommand } from "./commands/doctor.js";
|
|
13
|
+
import { setupCommand } from "./commands/setup.js";
|
|
14
|
+
import { upgradeCommand } from "./commands/upgrade.js";
|
|
15
|
+
import { completionsCommand } from "./commands/completions.js";
|
|
16
|
+
import { observeCommand } from "./commands/observe.js";
|
|
17
|
+
import { uninstallCommand } from "./commands/uninstall.js";
|
|
18
|
+
import { bold, green, yellow, red } from "./utils/colors.js";
|
|
19
|
+
import type { Classification, PhaseName } from "./state/schema.js";
|
|
20
|
+
|
|
21
|
+
const program = new Command();
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.name("work-kit")
|
|
25
|
+
.description("State machine orchestrator for work-kit development workflow")
|
|
26
|
+
.version("1.0.0");
|
|
27
|
+
|
|
28
|
+
// ── init ─────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command("init")
|
|
32
|
+
.description("Create worktree and initialize state")
|
|
33
|
+
.requiredOption("--mode <mode>", "Workflow mode: full or auto")
|
|
34
|
+
.requiredOption("--description <text>", "Description of the work")
|
|
35
|
+
.option("--classification <type>", "Work classification (auto mode): bug-fix, small-change, refactor, feature, large-feature")
|
|
36
|
+
.option("--worktree-root <path>", "Override worktree root directory")
|
|
37
|
+
.action((opts) => {
|
|
38
|
+
try {
|
|
39
|
+
const result = initCommand({
|
|
40
|
+
mode: opts.mode as "full" | "auto",
|
|
41
|
+
description: opts.description,
|
|
42
|
+
classification: opts.classification as Classification | undefined,
|
|
43
|
+
worktreeRoot: opts.worktreeRoot,
|
|
44
|
+
});
|
|
45
|
+
console.log(JSON.stringify(result, null, 2));
|
|
46
|
+
} catch (e: any) {
|
|
47
|
+
console.error(JSON.stringify({ action: "error", message: e.message }));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ── next ─────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
program
|
|
55
|
+
.command("next")
|
|
56
|
+
.description("Get the next action to perform")
|
|
57
|
+
.option("--worktree-root <path>", "Override worktree root")
|
|
58
|
+
.action((opts) => {
|
|
59
|
+
try {
|
|
60
|
+
const result = nextCommand(opts.worktreeRoot);
|
|
61
|
+
console.log(JSON.stringify(result, null, 2));
|
|
62
|
+
} catch (e: any) {
|
|
63
|
+
console.error(JSON.stringify({ action: "error", message: e.message }));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── complete ─────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
program
|
|
71
|
+
.command("complete <target>")
|
|
72
|
+
.description("Mark a phase/sub-stage as complete (e.g., plan/clarify)")
|
|
73
|
+
.option("--outcome <value>", "Outcome of the step (e.g., done, revise, broken, changes_requested)")
|
|
74
|
+
.option("--worktree-root <path>", "Override worktree root")
|
|
75
|
+
.action((target, opts) => {
|
|
76
|
+
try {
|
|
77
|
+
const result = completeCommand(target, opts.outcome, opts.worktreeRoot);
|
|
78
|
+
console.log(JSON.stringify(result, null, 2));
|
|
79
|
+
} catch (e: any) {
|
|
80
|
+
console.error(JSON.stringify({ action: "error", message: e.message }));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── status ───────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
program
|
|
88
|
+
.command("status")
|
|
89
|
+
.description("Show current state summary")
|
|
90
|
+
.option("--worktree-root <path>", "Override worktree root")
|
|
91
|
+
.action((opts) => {
|
|
92
|
+
try {
|
|
93
|
+
const result = statusCommand(opts.worktreeRoot);
|
|
94
|
+
console.log(JSON.stringify(result, null, 2));
|
|
95
|
+
} catch (e: any) {
|
|
96
|
+
console.error(JSON.stringify({ action: "error", message: e.message }));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ── context ──────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
program
|
|
104
|
+
.command("context <phase>")
|
|
105
|
+
.description("Extract Final sections needed for a phase's agent")
|
|
106
|
+
.option("--worktree-root <path>", "Override worktree root")
|
|
107
|
+
.action((phase, opts) => {
|
|
108
|
+
try {
|
|
109
|
+
const result = contextCommand(phase as PhaseName, opts.worktreeRoot);
|
|
110
|
+
console.log(JSON.stringify(result, null, 2));
|
|
111
|
+
} catch (e: any) {
|
|
112
|
+
console.error(JSON.stringify({ action: "error", message: e.message }));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── validate ─────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command("validate <phase>")
|
|
121
|
+
.description("Check prerequisites for a phase")
|
|
122
|
+
.option("--worktree-root <path>", "Override worktree root")
|
|
123
|
+
.action((phase, opts) => {
|
|
124
|
+
try {
|
|
125
|
+
const result = validateCommand(phase as PhaseName, opts.worktreeRoot);
|
|
126
|
+
console.log(JSON.stringify(result, null, 2));
|
|
127
|
+
} catch (e: any) {
|
|
128
|
+
console.error(JSON.stringify({ action: "error", message: e.message }));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ── loopback ─────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
program
|
|
136
|
+
.command("loopback")
|
|
137
|
+
.description("Register a loop-back transition")
|
|
138
|
+
.requiredOption("--from <source>", "Source phase/sub-stage (e.g., review/handoff)")
|
|
139
|
+
.requiredOption("--to <target>", "Target phase/sub-stage (e.g., build/core)")
|
|
140
|
+
.requiredOption("--reason <text>", "Reason for loop-back")
|
|
141
|
+
.option("--worktree-root <path>", "Override worktree root")
|
|
142
|
+
.action((opts) => {
|
|
143
|
+
try {
|
|
144
|
+
const result = loopbackCommand({
|
|
145
|
+
from: opts.from,
|
|
146
|
+
to: opts.to,
|
|
147
|
+
reason: opts.reason,
|
|
148
|
+
worktreeRoot: opts.worktreeRoot,
|
|
149
|
+
});
|
|
150
|
+
console.log(JSON.stringify(result, null, 2));
|
|
151
|
+
} catch (e: any) {
|
|
152
|
+
console.error(JSON.stringify({ action: "error", message: e.message }));
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── workflow ─────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
program
|
|
160
|
+
.command("workflow")
|
|
161
|
+
.description("Manage auto-kit dynamic workflow")
|
|
162
|
+
.option("--add <step>", "Add a step (e.g., review/security)")
|
|
163
|
+
.option("--remove <step>", "Remove a step (e.g., test/e2e)")
|
|
164
|
+
.option("--worktree-root <path>", "Override worktree root")
|
|
165
|
+
.action((opts) => {
|
|
166
|
+
try {
|
|
167
|
+
const result = workflowCommand({
|
|
168
|
+
add: opts.add,
|
|
169
|
+
remove: opts.remove,
|
|
170
|
+
worktreeRoot: opts.worktreeRoot,
|
|
171
|
+
});
|
|
172
|
+
console.log(JSON.stringify(result, null, 2));
|
|
173
|
+
} catch (e: any) {
|
|
174
|
+
console.error(JSON.stringify({ action: "error", message: e.message }));
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ── doctor ───────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
program
|
|
182
|
+
.command("doctor")
|
|
183
|
+
.description("Check CLI installation, skills, and environment health")
|
|
184
|
+
.option("--json", "Output as JSON")
|
|
185
|
+
.option("--worktree-root <path>", "Override worktree root")
|
|
186
|
+
.action((opts) => {
|
|
187
|
+
const result = doctorCommand(opts.worktreeRoot);
|
|
188
|
+
if (opts.json) {
|
|
189
|
+
console.log(JSON.stringify(result, null, 2));
|
|
190
|
+
} else {
|
|
191
|
+
for (const check of result.checks) {
|
|
192
|
+
const icon = check.status === "pass" ? green("\u2713") : check.status === "warn" ? yellow("!") : red("\u2717");
|
|
193
|
+
console.error(` ${icon} ${bold(check.name)}: ${check.message}`);
|
|
194
|
+
}
|
|
195
|
+
console.error();
|
|
196
|
+
console.error(result.ok ? green("All checks passed.") : red("Some checks failed. Fix the issues above."));
|
|
197
|
+
}
|
|
198
|
+
process.exit(result.ok ? 0 : 1);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ── setup ────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
program
|
|
204
|
+
.command("setup [path]")
|
|
205
|
+
.description("Install work-kit skills into a project")
|
|
206
|
+
.action(async (targetPath) => {
|
|
207
|
+
await setupCommand(targetPath);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── upgrade ───────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
program
|
|
213
|
+
.command("upgrade")
|
|
214
|
+
.description("Update work-kit skills to the latest version")
|
|
215
|
+
.option("--worktree-root <path>", "Override project path")
|
|
216
|
+
.action(async (opts) => {
|
|
217
|
+
await upgradeCommand(opts.worktreeRoot);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ── completions ─────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
program
|
|
223
|
+
.command("completions <shell>")
|
|
224
|
+
.description("Output shell completions (bash, zsh, fish)")
|
|
225
|
+
.action((shell) => {
|
|
226
|
+
completionsCommand(shell);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ── observe ─────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
program
|
|
232
|
+
.command("observe")
|
|
233
|
+
.description("Real-time dashboard of all active work items")
|
|
234
|
+
.option("--repo <path>", "Main repository root")
|
|
235
|
+
.action(async (opts) => {
|
|
236
|
+
await observeCommand({ mainRepo: opts.repo });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ── uninstall ────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
program
|
|
242
|
+
.command("uninstall [path]")
|
|
243
|
+
.description("Remove work-kit skills from a project")
|
|
244
|
+
.action(async (targetPath) => {
|
|
245
|
+
await uninstallCommand(targetPath);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
program.parse();
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import type { WorkKitState, PhaseName } from "../state/schema.js";
|
|
5
|
+
import { PHASE_NAMES, SUBSTAGES_BY_PHASE } from "../state/schema.js";
|
|
6
|
+
import { readState, stateExists } from "../state/store.js";
|
|
7
|
+
|
|
8
|
+
// ── View Types ──────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface WorkItemView {
|
|
11
|
+
slug: string;
|
|
12
|
+
branch: string;
|
|
13
|
+
mode: string;
|
|
14
|
+
classification?: string;
|
|
15
|
+
status: string;
|
|
16
|
+
currentPhase: string | null;
|
|
17
|
+
currentSubStage: string | null;
|
|
18
|
+
startedAt: string;
|
|
19
|
+
progress: { completed: number; total: number; percent: number };
|
|
20
|
+
phases: { name: string; status: string }[];
|
|
21
|
+
loopbacks: { count: number; lastReason?: string; lastFrom?: string; lastTo?: string };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CompletedItemView {
|
|
25
|
+
slug: string;
|
|
26
|
+
pr?: string;
|
|
27
|
+
completedAt: string;
|
|
28
|
+
phases: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DashboardData {
|
|
32
|
+
activeItems: WorkItemView[];
|
|
33
|
+
completedItems: CompletedItemView[];
|
|
34
|
+
lastUpdated: Date;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Worktree Discovery ──────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export function discoverWorktrees(mainRepoRoot: string): string[] {
|
|
40
|
+
let output: string;
|
|
41
|
+
try {
|
|
42
|
+
output = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
43
|
+
cwd: mainRepoRoot,
|
|
44
|
+
encoding: "utf-8",
|
|
45
|
+
timeout: 5000,
|
|
46
|
+
});
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const worktrees: string[] = [];
|
|
52
|
+
const lines = output.split("\n");
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (line.startsWith("worktree ")) {
|
|
55
|
+
const wtPath = line.slice("worktree ".length).trim();
|
|
56
|
+
if (stateExists(wtPath)) {
|
|
57
|
+
worktrees.push(wtPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (stateExists(mainRepoRoot) && !worktrees.includes(mainRepoRoot)) {
|
|
63
|
+
worktrees.push(mainRepoRoot);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return worktrees;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Collect Single Work Item ────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
|
|
72
|
+
if (!stateExists(worktreeRoot)) return null;
|
|
73
|
+
|
|
74
|
+
let state: WorkKitState;
|
|
75
|
+
try {
|
|
76
|
+
state = readState(worktreeRoot);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Compute progress
|
|
82
|
+
let completed = 0;
|
|
83
|
+
let total = 0;
|
|
84
|
+
const phaseViews: { name: string; status: string }[] = [];
|
|
85
|
+
|
|
86
|
+
const phaseList: PhaseName[] = state.mode === "auto-kit" && state.workflow
|
|
87
|
+
? getAutoKitPhases(state)
|
|
88
|
+
: [...PHASE_NAMES];
|
|
89
|
+
|
|
90
|
+
for (const phaseName of phaseList) {
|
|
91
|
+
const phase = state.phases[phaseName];
|
|
92
|
+
if (!phase) {
|
|
93
|
+
phaseViews.push({ name: phaseName, status: "pending" });
|
|
94
|
+
// Count substages for total
|
|
95
|
+
const subs = SUBSTAGES_BY_PHASE[phaseName] || [];
|
|
96
|
+
total += subs.length;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
phaseViews.push({ name: phaseName, status: phase.status });
|
|
101
|
+
|
|
102
|
+
const subStageKeys = Object.keys(phase.subStages);
|
|
103
|
+
if (subStageKeys.length === 0) {
|
|
104
|
+
// Use default substages
|
|
105
|
+
const defaults = SUBSTAGES_BY_PHASE[phaseName] || [];
|
|
106
|
+
total += defaults.length;
|
|
107
|
+
if (phase.status === "completed") completed += defaults.length;
|
|
108
|
+
else if (phase.status === "skipped") completed += defaults.length;
|
|
109
|
+
} else {
|
|
110
|
+
for (const key of subStageKeys) {
|
|
111
|
+
total++;
|
|
112
|
+
const sub = phase.subStages[key];
|
|
113
|
+
if (sub.status === "completed" || sub.status === "skipped") {
|
|
114
|
+
completed++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
121
|
+
|
|
122
|
+
// Loopback info
|
|
123
|
+
const loopbacks = state.loopbacks || [];
|
|
124
|
+
const loopbackView = {
|
|
125
|
+
count: loopbacks.length,
|
|
126
|
+
lastReason: loopbacks.length > 0 ? loopbacks[loopbacks.length - 1].reason : undefined,
|
|
127
|
+
lastFrom: loopbacks.length > 0
|
|
128
|
+
? `${loopbacks[loopbacks.length - 1].from.phase}/${loopbacks[loopbacks.length - 1].from.subStage}`
|
|
129
|
+
: undefined,
|
|
130
|
+
lastTo: loopbacks.length > 0
|
|
131
|
+
? `${loopbacks[loopbacks.length - 1].to.phase}/${loopbacks[loopbacks.length - 1].to.subStage}`
|
|
132
|
+
: undefined,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
slug: state.slug,
|
|
137
|
+
branch: state.branch,
|
|
138
|
+
mode: state.mode,
|
|
139
|
+
classification: state.classification,
|
|
140
|
+
status: state.status,
|
|
141
|
+
currentPhase: state.currentPhase,
|
|
142
|
+
currentSubStage: state.currentSubStage,
|
|
143
|
+
startedAt: state.started,
|
|
144
|
+
progress: { completed, total, percent },
|
|
145
|
+
phases: phaseViews,
|
|
146
|
+
loopbacks: loopbackView,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getAutoKitPhases(state: WorkKitState): PhaseName[] {
|
|
151
|
+
if (!state.workflow) return [...PHASE_NAMES];
|
|
152
|
+
const phases = new Set<PhaseName>();
|
|
153
|
+
for (const step of state.workflow) {
|
|
154
|
+
if (step.included) phases.add(step.phase);
|
|
155
|
+
}
|
|
156
|
+
// Maintain canonical order
|
|
157
|
+
return PHASE_NAMES.filter(p => phases.has(p));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Collect Completed Items ─────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
export function collectCompletedItems(mainRepoRoot: string): CompletedItemView[] {
|
|
163
|
+
const indexPath = path.join(mainRepoRoot, ".claude", "work-kit", "index.md");
|
|
164
|
+
if (!fs.existsSync(indexPath)) return [];
|
|
165
|
+
|
|
166
|
+
let content: string;
|
|
167
|
+
try {
|
|
168
|
+
content = fs.readFileSync(indexPath, "utf-8");
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const items: CompletedItemView[] = [];
|
|
174
|
+
// Parse markdown table or list entries
|
|
175
|
+
// Expected format: | slug | PR | date | phases |
|
|
176
|
+
// or list format: - slug (#PR) - date - phases
|
|
177
|
+
const lines = content.split("\n");
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
// Try table format: | slug | #PR | date | phases |
|
|
180
|
+
const tableMatch = line.match(/^\|\s*(.+?)\s*\|\s*(#?\d+)?\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|/);
|
|
181
|
+
if (tableMatch) {
|
|
182
|
+
const slug = tableMatch[1].trim();
|
|
183
|
+
if (slug === "Slug" || slug === "---" || slug.startsWith("-")) continue; // skip header
|
|
184
|
+
items.push({
|
|
185
|
+
slug,
|
|
186
|
+
pr: tableMatch[2]?.trim() || undefined,
|
|
187
|
+
completedAt: tableMatch[3].trim(),
|
|
188
|
+
phases: tableMatch[4].trim(),
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Try list format: - **slug** (#38) completed 2d ago — plan→review
|
|
194
|
+
const listMatch = line.match(/^[-*]\s+\*?\*?(.+?)\*?\*?\s+\(?(#\d+)?\)?\s*[-—]?\s*(.+?)?\s*[-—]\s*(.+)$/);
|
|
195
|
+
if (listMatch) {
|
|
196
|
+
items.push({
|
|
197
|
+
slug: listMatch[1].trim(),
|
|
198
|
+
pr: listMatch[2]?.trim() || undefined,
|
|
199
|
+
completedAt: listMatch[3]?.trim() || "",
|
|
200
|
+
phases: listMatch[4]?.trim() || "",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return items;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Collect All Dashboard Data ──────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
export function collectDashboardData(mainRepoRoot: string, cachedWorktrees?: string[]): DashboardData {
|
|
211
|
+
const worktrees = cachedWorktrees ?? discoverWorktrees(mainRepoRoot);
|
|
212
|
+
const activeItems: WorkItemView[] = [];
|
|
213
|
+
|
|
214
|
+
for (const wt of worktrees) {
|
|
215
|
+
const item = collectWorkItem(wt);
|
|
216
|
+
if (item && item.status !== "completed") {
|
|
217
|
+
activeItems.push(item);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Sort: in-progress first, then paused, then by start time
|
|
222
|
+
activeItems.sort((a, b) => {
|
|
223
|
+
const statusOrder: Record<string, number> = { "in-progress": 0, "paused": 1, "failed": 2 };
|
|
224
|
+
const aOrder = statusOrder[a.status] ?? 3;
|
|
225
|
+
const bOrder = statusOrder[b.status] ?? 3;
|
|
226
|
+
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
227
|
+
return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const completedItems = collectCompletedItems(mainRepoRoot);
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
activeItems,
|
|
234
|
+
completedItems,
|
|
235
|
+
lastUpdated: new Date(),
|
|
236
|
+
};
|
|
237
|
+
}
|