@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,184 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { WorkKitState, PhaseState, PhaseName, PHASE_NAMES, SUBSTAGES_BY_PHASE, WorkflowStep, Classification } from "../state/schema.js";
|
|
4
|
+
import { writeState, writeStateMd, stateExists } from "../state/store.js";
|
|
5
|
+
import { buildFullWorkflow, buildDefaultWorkflow, skillFilePath } from "../config/phases.js";
|
|
6
|
+
import type { Action } from "../state/schema.js";
|
|
7
|
+
|
|
8
|
+
function toSlug(description: string): string {
|
|
9
|
+
return description
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
12
|
+
.replace(/\s+/g, "-")
|
|
13
|
+
.slice(0, 40)
|
|
14
|
+
.replace(/-+$/, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildPhases(workflow?: WorkflowStep[]): Record<PhaseName, PhaseState> {
|
|
18
|
+
const phases = {} as Record<PhaseName, PhaseState>;
|
|
19
|
+
|
|
20
|
+
for (const phase of PHASE_NAMES) {
|
|
21
|
+
const subStages: Record<string, { status: "pending" | "skipped" }> = {};
|
|
22
|
+
const allSubStages = SUBSTAGES_BY_PHASE[phase];
|
|
23
|
+
|
|
24
|
+
for (const ss of allSubStages) {
|
|
25
|
+
if (workflow) {
|
|
26
|
+
const step = workflow.find((s) => s.phase === phase && s.subStage === ss);
|
|
27
|
+
subStages[ss] = { status: step?.included ? "pending" : "skipped" };
|
|
28
|
+
} else {
|
|
29
|
+
subStages[ss] = { status: "pending" };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if entire phase is skipped (all sub-stages skipped)
|
|
34
|
+
const allSkipped = Object.values(subStages).every((s) => s.status === "skipped");
|
|
35
|
+
phases[phase] = {
|
|
36
|
+
status: allSkipped ? "skipped" : "pending",
|
|
37
|
+
subStages,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return phases;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function generateStateMd(slug: string, branch: string, mode: string, description: string, classification?: string, workflow?: WorkflowStep[]): string {
|
|
45
|
+
const title = slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
46
|
+
const date = new Date().toISOString().split("T")[0];
|
|
47
|
+
|
|
48
|
+
let md = `# ${title}
|
|
49
|
+
|
|
50
|
+
**Slug:** ${slug}
|
|
51
|
+
**Branch:** ${branch}
|
|
52
|
+
**Started:** ${date}
|
|
53
|
+
**Mode:** ${mode}
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
if (classification) {
|
|
57
|
+
md += `**Classification:** ${classification}\n`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
md += `**Phase:** plan
|
|
61
|
+
**Sub-stage:** clarify
|
|
62
|
+
**Status:** in-progress
|
|
63
|
+
|
|
64
|
+
## Description
|
|
65
|
+
${description}
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
if (workflow) {
|
|
69
|
+
md += `\n## Workflow\n`;
|
|
70
|
+
for (const step of workflow) {
|
|
71
|
+
if (step.included) {
|
|
72
|
+
const label = `${step.phase.charAt(0).toUpperCase() + step.phase.slice(1)}: ${step.subStage.charAt(0).toUpperCase() + step.subStage.slice(1)}`;
|
|
73
|
+
md += `- [ ] ${label}\n`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
md += `
|
|
79
|
+
## Criteria
|
|
80
|
+
<!-- Added during Plan/Clarify, checked off during test/review -->
|
|
81
|
+
|
|
82
|
+
## Decisions
|
|
83
|
+
<!-- Append here whenever you choose between real alternatives -->
|
|
84
|
+
<!-- Format: **<context>**: chose <X> over <Y> — <why> -->
|
|
85
|
+
|
|
86
|
+
## Deviations
|
|
87
|
+
<!-- Append here whenever implementation diverges from the Blueprint -->
|
|
88
|
+
<!-- Format: **<Blueprint step>**: <what changed> — <why> -->
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
return md;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function initCommand(options: {
|
|
95
|
+
mode: "full" | "auto";
|
|
96
|
+
description: string;
|
|
97
|
+
classification?: Classification;
|
|
98
|
+
worktreeRoot?: string;
|
|
99
|
+
}): Action {
|
|
100
|
+
const { mode, description, classification } = options;
|
|
101
|
+
const worktreeRoot = options.worktreeRoot || process.cwd();
|
|
102
|
+
|
|
103
|
+
// Guard: don't overwrite existing state
|
|
104
|
+
if (stateExists(worktreeRoot)) {
|
|
105
|
+
return {
|
|
106
|
+
action: "error",
|
|
107
|
+
message: "State already exists in this directory. Use `work-kit status` to check current state.",
|
|
108
|
+
suggestion: "To start fresh, delete .work-kit/state.json first.",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Auto mode requires classification
|
|
113
|
+
if (mode === "auto" && !classification) {
|
|
114
|
+
return {
|
|
115
|
+
action: "error",
|
|
116
|
+
message: "Auto mode requires --classification (bug-fix, small-change, refactor, feature, large-feature).",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Validate mode
|
|
121
|
+
if (mode !== "full" && mode !== "auto") {
|
|
122
|
+
return {
|
|
123
|
+
action: "error",
|
|
124
|
+
message: `Invalid mode "${mode}". Use "full" or "auto".`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const slug = toSlug(description);
|
|
129
|
+
const branch = `feature/${slug}`;
|
|
130
|
+
const modeLabel = mode === "full" ? "full-kit" : "auto-kit";
|
|
131
|
+
|
|
132
|
+
// Build workflow
|
|
133
|
+
let workflow: WorkflowStep[] | undefined;
|
|
134
|
+
if (mode === "auto" && classification) {
|
|
135
|
+
workflow = buildDefaultWorkflow(classification);
|
|
136
|
+
} else if (mode === "full") {
|
|
137
|
+
workflow = buildFullWorkflow();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Find first active sub-stage
|
|
141
|
+
let firstPhase: PhaseName = "plan";
|
|
142
|
+
let firstSubStage = "clarify";
|
|
143
|
+
|
|
144
|
+
if (workflow) {
|
|
145
|
+
const first = workflow.find((s) => s.included);
|
|
146
|
+
if (first) {
|
|
147
|
+
firstPhase = first.phase;
|
|
148
|
+
firstSubStage = first.subStage;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Build state
|
|
153
|
+
const state: WorkKitState = {
|
|
154
|
+
version: 1,
|
|
155
|
+
slug,
|
|
156
|
+
branch,
|
|
157
|
+
started: new Date().toISOString().split("T")[0],
|
|
158
|
+
mode: modeLabel,
|
|
159
|
+
...(classification && { classification }),
|
|
160
|
+
status: "in-progress",
|
|
161
|
+
currentPhase: firstPhase,
|
|
162
|
+
currentSubStage: firstSubStage,
|
|
163
|
+
phases: buildPhases(workflow),
|
|
164
|
+
...(mode === "auto" && workflow && { workflow }),
|
|
165
|
+
loopbacks: [],
|
|
166
|
+
metadata: {
|
|
167
|
+
worktreeRoot,
|
|
168
|
+
mainRepoRoot: worktreeRoot, // will be set properly by caller
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Write state files
|
|
173
|
+
writeState(worktreeRoot, state);
|
|
174
|
+
writeStateMd(worktreeRoot, generateStateMd(slug, branch, modeLabel, description, classification, workflow));
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
action: "spawn_agent",
|
|
178
|
+
phase: firstPhase,
|
|
179
|
+
subStage: firstSubStage,
|
|
180
|
+
skillFile: skillFilePath(firstPhase, firstSubStage),
|
|
181
|
+
agentPrompt: `You are starting the ${firstPhase} phase. Begin with the ${firstSubStage} sub-stage. Read the skill file and follow its instructions. Write outputs to .work-kit/state.md.`,
|
|
182
|
+
onComplete: `npx work-kit complete ${firstPhase}/${firstSubStage}`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readState, writeState, findWorktreeRoot } from "../state/store.js";
|
|
2
|
+
import { parseLocation, resetToLocation } from "../state/helpers.js";
|
|
3
|
+
import type { Action } from "../state/schema.js";
|
|
4
|
+
|
|
5
|
+
const MAX_LOOPBACKS_PER_ROUTE = 2;
|
|
6
|
+
|
|
7
|
+
export function loopbackCommand(opts: {
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
worktreeRoot?: string;
|
|
12
|
+
}): Action {
|
|
13
|
+
const root = opts.worktreeRoot || findWorktreeRoot();
|
|
14
|
+
if (!root) {
|
|
15
|
+
return { action: "error", message: "No work-kit state found." };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const state = readState(root);
|
|
19
|
+
const from = parseLocation(opts.from);
|
|
20
|
+
const to = parseLocation(opts.to);
|
|
21
|
+
|
|
22
|
+
if (!state.phases[from.phase]?.subStages[from.subStage]) {
|
|
23
|
+
return { action: "error", message: `Invalid source: ${opts.from}` };
|
|
24
|
+
}
|
|
25
|
+
if (!state.phases[to.phase]?.subStages[to.subStage]) {
|
|
26
|
+
return { action: "error", message: `Invalid target: ${opts.to}` };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Can't loop back to a skipped sub-stage
|
|
30
|
+
if (state.phases[to.phase].subStages[to.subStage].status === "skipped") {
|
|
31
|
+
return { action: "error", message: `Cannot loop back to ${opts.to} — it is skipped.` };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Enforce max loopback count per route
|
|
35
|
+
const sameRouteCount = state.loopbacks.filter(
|
|
36
|
+
(lb) => lb.from.phase === from.phase && lb.from.subStage === from.subStage
|
|
37
|
+
&& lb.to.phase === to.phase && lb.to.subStage === to.subStage
|
|
38
|
+
).length;
|
|
39
|
+
if (sameRouteCount >= MAX_LOOPBACKS_PER_ROUTE) {
|
|
40
|
+
return {
|
|
41
|
+
action: "error",
|
|
42
|
+
message: `Max loopback count (${MAX_LOOPBACKS_PER_ROUTE}) reached for ${opts.from} → ${opts.to}. Proceeding with noted caveats.`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
state.loopbacks.push({
|
|
47
|
+
from,
|
|
48
|
+
to,
|
|
49
|
+
reason: opts.reason,
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
resetToLocation(state, to);
|
|
54
|
+
state.currentPhase = to.phase;
|
|
55
|
+
state.currentSubStage = to.subStage;
|
|
56
|
+
writeState(root, state);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
action: "loopback",
|
|
60
|
+
from,
|
|
61
|
+
to,
|
|
62
|
+
reason: opts.reason,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { readState, writeState, findWorktreeRoot, readStateMd } from "../state/store.js";
|
|
2
|
+
import { determineNextStep } from "../engine/transitions.js";
|
|
3
|
+
import { validatePhasePrerequisites } from "../state/validators.js";
|
|
4
|
+
import { buildAgentPrompt } from "../context/prompt-builder.js";
|
|
5
|
+
import { getParallelGroup } from "../engine/parallel.js";
|
|
6
|
+
import { skillFilePath } from "../config/phases.js";
|
|
7
|
+
import type { Action, PhaseName, WorkKitState } from "../state/schema.js";
|
|
8
|
+
|
|
9
|
+
export function nextCommand(worktreeRoot?: string): Action {
|
|
10
|
+
const root = worktreeRoot || findWorktreeRoot();
|
|
11
|
+
if (!root) {
|
|
12
|
+
return { action: "error", message: "No work-kit state found. Run `work-kit init` first." };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const state = readState(root);
|
|
16
|
+
|
|
17
|
+
if (state.status === "completed") {
|
|
18
|
+
return { action: "complete", message: "Work-kit is already complete." };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (state.status === "failed") {
|
|
22
|
+
return { action: "error", message: "Work-kit is in failed state.", suggestion: "Review the state and restart." };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const step = determineNextStep(state);
|
|
26
|
+
|
|
27
|
+
switch (step.type) {
|
|
28
|
+
case "complete":
|
|
29
|
+
return { action: "complete", message: step.message! };
|
|
30
|
+
|
|
31
|
+
case "wait-for-user":
|
|
32
|
+
return { action: "wait_for_user", message: step.message! };
|
|
33
|
+
|
|
34
|
+
case "phase-boundary": {
|
|
35
|
+
const phase = step.phase!;
|
|
36
|
+
|
|
37
|
+
const validation = validatePhasePrerequisites(state, phase);
|
|
38
|
+
if (!validation.valid) {
|
|
39
|
+
return {
|
|
40
|
+
action: "error",
|
|
41
|
+
message: validation.message,
|
|
42
|
+
suggestion: `Complete ${validation.missingPrerequisite} first.`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
state.currentPhase = phase;
|
|
47
|
+
state.phases[phase].status = "in-progress";
|
|
48
|
+
state.phases[phase].startedAt = new Date().toISOString();
|
|
49
|
+
|
|
50
|
+
const subStages = Object.entries(state.phases[phase].subStages);
|
|
51
|
+
const firstActive = subStages.find(([_, ss]) => ss.status === "pending");
|
|
52
|
+
|
|
53
|
+
if (!firstActive) {
|
|
54
|
+
return { action: "error", message: `No pending sub-stages in ${phase}` };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const [subStage] = firstActive;
|
|
58
|
+
state.currentSubStage = subStage;
|
|
59
|
+
state.phases[phase].subStages[subStage].status = "in-progress";
|
|
60
|
+
state.phases[phase].subStages[subStage].startedAt = new Date().toISOString();
|
|
61
|
+
writeState(root, state);
|
|
62
|
+
|
|
63
|
+
return buildSpawnAction(root, state, phase, subStage);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case "sub-stage": {
|
|
67
|
+
const phase = step.phase!;
|
|
68
|
+
const subStage = step.subStage!;
|
|
69
|
+
|
|
70
|
+
state.currentPhase = phase;
|
|
71
|
+
state.currentSubStage = subStage;
|
|
72
|
+
if (state.phases[phase].status === "pending") {
|
|
73
|
+
state.phases[phase].status = "in-progress";
|
|
74
|
+
state.phases[phase].startedAt = new Date().toISOString();
|
|
75
|
+
}
|
|
76
|
+
state.phases[phase].subStages[subStage].status = "in-progress";
|
|
77
|
+
state.phases[phase].subStages[subStage].startedAt = new Date().toISOString();
|
|
78
|
+
writeState(root, state);
|
|
79
|
+
|
|
80
|
+
return buildSpawnAction(root, state, phase, subStage);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
default:
|
|
84
|
+
return { action: "error", message: `Unknown step type: ${step.type}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildSpawnAction(root: string, state: WorkKitState, phase: PhaseName, subStage: string): Action {
|
|
89
|
+
// Read state.md once for all prompt builds
|
|
90
|
+
const stateMd = readStateMd(root);
|
|
91
|
+
const parallelGroup = getParallelGroup(phase, subStage, state);
|
|
92
|
+
|
|
93
|
+
if (parallelGroup) {
|
|
94
|
+
const agents = parallelGroup.parallel
|
|
95
|
+
.filter((ss) => {
|
|
96
|
+
const ssState = state.phases[phase].subStages[ss];
|
|
97
|
+
return ssState && ssState.status !== "skipped" && ssState.status !== "completed";
|
|
98
|
+
})
|
|
99
|
+
.map((ss) => ({
|
|
100
|
+
phase,
|
|
101
|
+
subStage: ss,
|
|
102
|
+
skillFile: skillFilePath(phase, ss),
|
|
103
|
+
agentPrompt: buildAgentPrompt(root, state, phase, ss, stateMd),
|
|
104
|
+
outputFile: `.work-kit/${phase}-${ss}.md`,
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
// If all parallel members were filtered out, fall through to single agent
|
|
108
|
+
if (agents.length === 0) {
|
|
109
|
+
// Skip to thenSequential if it exists, otherwise nothing to do
|
|
110
|
+
if (parallelGroup.thenSequential) {
|
|
111
|
+
const seqSS = parallelGroup.thenSequential;
|
|
112
|
+
return {
|
|
113
|
+
action: "spawn_agent",
|
|
114
|
+
phase,
|
|
115
|
+
subStage: seqSS,
|
|
116
|
+
skillFile: skillFilePath(phase, seqSS),
|
|
117
|
+
agentPrompt: buildAgentPrompt(root, state, phase, seqSS, stateMd),
|
|
118
|
+
onComplete: `npx work-kit complete ${phase}/${seqSS}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { action: "error", message: `No active sub-stages in parallel group for ${phase}` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// If only 1 agent remains, run as single agent (no need for parallel)
|
|
125
|
+
if (agents.length === 1 && !parallelGroup.thenSequential) {
|
|
126
|
+
const agent = agents[0];
|
|
127
|
+
return {
|
|
128
|
+
action: "spawn_agent",
|
|
129
|
+
phase: agent.phase,
|
|
130
|
+
subStage: agent.subStage,
|
|
131
|
+
skillFile: agent.skillFile,
|
|
132
|
+
agentPrompt: agent.agentPrompt,
|
|
133
|
+
onComplete: `npx work-kit complete ${agent.phase}/${agent.subStage}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const agent of agents) {
|
|
138
|
+
state.phases[phase].subStages[agent.subStage].status = "in-progress";
|
|
139
|
+
state.phases[phase].subStages[agent.subStage].startedAt = new Date().toISOString();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const thenSequential = parallelGroup.thenSequential
|
|
143
|
+
? {
|
|
144
|
+
phase,
|
|
145
|
+
subStage: parallelGroup.thenSequential,
|
|
146
|
+
skillFile: skillFilePath(phase, parallelGroup.thenSequential),
|
|
147
|
+
agentPrompt: buildAgentPrompt(root, state, phase, parallelGroup.thenSequential, stateMd),
|
|
148
|
+
}
|
|
149
|
+
: undefined;
|
|
150
|
+
|
|
151
|
+
writeState(root, state);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
action: "spawn_parallel_agents",
|
|
155
|
+
agents,
|
|
156
|
+
thenSequential,
|
|
157
|
+
onComplete: `npx work-kit complete ${phase}/${parallelGroup.thenSequential || parallelGroup.parallel[parallelGroup.parallel.length - 1]}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const skill = skillFilePath(phase, subStage);
|
|
162
|
+
const prompt = buildAgentPrompt(root, state, phase, subStage, stateMd);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
action: "spawn_agent",
|
|
166
|
+
phase,
|
|
167
|
+
subStage,
|
|
168
|
+
skillFile: skill,
|
|
169
|
+
agentPrompt: prompt,
|
|
170
|
+
onComplete: `npx work-kit complete ${phase}/${subStage}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
renderDashboard,
|
|
5
|
+
enterAlternateScreen,
|
|
6
|
+
exitAlternateScreen,
|
|
7
|
+
moveCursorHome,
|
|
8
|
+
renderTooSmall,
|
|
9
|
+
} from "../observer/renderer.js";
|
|
10
|
+
import { collectDashboardData } from "../observer/data.js";
|
|
11
|
+
import { startWatching } from "../observer/watcher.js";
|
|
12
|
+
|
|
13
|
+
function findMainRepoRoot(startDir: string): string {
|
|
14
|
+
// Find the git toplevel
|
|
15
|
+
try {
|
|
16
|
+
const result = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
17
|
+
cwd: startDir,
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
timeout: 5000,
|
|
20
|
+
});
|
|
21
|
+
return result.trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return startDir;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function observeCommand(opts: { mainRepo?: string }): Promise<void> {
|
|
28
|
+
const mainRepoRoot = opts.mainRepo
|
|
29
|
+
? path.resolve(opts.mainRepo)
|
|
30
|
+
: findMainRepoRoot(process.cwd());
|
|
31
|
+
|
|
32
|
+
let scrollOffset = 0;
|
|
33
|
+
let cleanedUp = false;
|
|
34
|
+
|
|
35
|
+
function cleanup(): void {
|
|
36
|
+
if (cleanedUp) return;
|
|
37
|
+
cleanedUp = true;
|
|
38
|
+
|
|
39
|
+
// Restore terminal
|
|
40
|
+
exitAlternateScreen();
|
|
41
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
42
|
+
try { process.stdin.setRawMode(false); } catch { /* ignore */ }
|
|
43
|
+
}
|
|
44
|
+
process.stdin.removeAllListeners("data");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function render(): void {
|
|
48
|
+
const width = process.stdout.columns || 80;
|
|
49
|
+
const height = process.stdout.rows || 24;
|
|
50
|
+
|
|
51
|
+
if (width < 60 || height < 10) {
|
|
52
|
+
process.stdout.write(renderTooSmall(width, height));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = collectDashboardData(mainRepoRoot, watcher.getWorktrees());
|
|
57
|
+
const frame = moveCursorHome() + renderDashboard(data, width, height, scrollOffset);
|
|
58
|
+
process.stdout.write(frame);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Enter alternate screen
|
|
62
|
+
enterAlternateScreen();
|
|
63
|
+
|
|
64
|
+
// Set up signal handlers
|
|
65
|
+
const onSignal = () => {
|
|
66
|
+
cleanup();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
};
|
|
69
|
+
process.on("SIGINT", onSignal);
|
|
70
|
+
process.on("SIGTERM", onSignal);
|
|
71
|
+
|
|
72
|
+
let watcher!: ReturnType<typeof startWatching>;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Set up file watching (before initial render so worktrees are cached)
|
|
76
|
+
watcher = startWatching(mainRepoRoot, () => {
|
|
77
|
+
render();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Initial render
|
|
81
|
+
render();
|
|
82
|
+
|
|
83
|
+
// Handle terminal resize
|
|
84
|
+
process.stdout.on("resize", () => {
|
|
85
|
+
render();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Set up keyboard input
|
|
89
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
90
|
+
process.stdin.setRawMode(true);
|
|
91
|
+
process.stdin.resume();
|
|
92
|
+
process.stdin.setEncoding("utf-8");
|
|
93
|
+
|
|
94
|
+
await new Promise<void>((resolve) => {
|
|
95
|
+
process.stdin.on("data", (key: string) => {
|
|
96
|
+
// Ctrl+C
|
|
97
|
+
if (key === "\x03") {
|
|
98
|
+
watcher.stop();
|
|
99
|
+
resolve();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 'q' to quit
|
|
104
|
+
if (key === "q" || key === "Q") {
|
|
105
|
+
watcher.stop();
|
|
106
|
+
resolve();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 'r' to refresh
|
|
111
|
+
if (key === "r" || key === "R") {
|
|
112
|
+
scrollOffset = 0;
|
|
113
|
+
render();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Up arrow: \x1b[A
|
|
118
|
+
if (key === "\x1b[A") {
|
|
119
|
+
scrollOffset = Math.max(0, scrollOffset - 1);
|
|
120
|
+
render();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Down arrow: \x1b[B
|
|
125
|
+
if (key === "\x1b[B") {
|
|
126
|
+
scrollOffset++;
|
|
127
|
+
render();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
// Non-TTY: just keep running until interrupted
|
|
134
|
+
await new Promise<void>((resolve) => {
|
|
135
|
+
process.on("SIGINT", () => {
|
|
136
|
+
watcher.stop();
|
|
137
|
+
resolve();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
cleanup();
|
|
143
|
+
}
|
|
144
|
+
}
|