@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.
Files changed (76) hide show
  1. package/README.md +147 -0
  2. package/cli/bin/work-kit.mjs +18 -0
  3. package/cli/src/commands/complete.ts +123 -0
  4. package/cli/src/commands/completions.ts +137 -0
  5. package/cli/src/commands/context.ts +41 -0
  6. package/cli/src/commands/doctor.ts +79 -0
  7. package/cli/src/commands/init.test.ts +116 -0
  8. package/cli/src/commands/init.ts +184 -0
  9. package/cli/src/commands/loopback.ts +64 -0
  10. package/cli/src/commands/next.ts +172 -0
  11. package/cli/src/commands/observe.ts +144 -0
  12. package/cli/src/commands/setup.ts +159 -0
  13. package/cli/src/commands/status.ts +50 -0
  14. package/cli/src/commands/uninstall.ts +89 -0
  15. package/cli/src/commands/upgrade.ts +12 -0
  16. package/cli/src/commands/validate.ts +34 -0
  17. package/cli/src/commands/workflow.ts +125 -0
  18. package/cli/src/config/agent-map.ts +62 -0
  19. package/cli/src/config/loopback-routes.ts +45 -0
  20. package/cli/src/config/phases.ts +119 -0
  21. package/cli/src/context/extractor.test.ts +77 -0
  22. package/cli/src/context/extractor.ts +73 -0
  23. package/cli/src/context/prompt-builder.ts +70 -0
  24. package/cli/src/engine/loopbacks.test.ts +33 -0
  25. package/cli/src/engine/loopbacks.ts +32 -0
  26. package/cli/src/engine/parallel.ts +60 -0
  27. package/cli/src/engine/phases.ts +23 -0
  28. package/cli/src/engine/transitions.test.ts +117 -0
  29. package/cli/src/engine/transitions.ts +97 -0
  30. package/cli/src/index.ts +248 -0
  31. package/cli/src/observer/data.ts +237 -0
  32. package/cli/src/observer/renderer.ts +316 -0
  33. package/cli/src/observer/watcher.ts +99 -0
  34. package/cli/src/state/helpers.test.ts +91 -0
  35. package/cli/src/state/helpers.ts +65 -0
  36. package/cli/src/state/schema.ts +113 -0
  37. package/cli/src/state/store.ts +82 -0
  38. package/cli/src/state/validators.test.ts +105 -0
  39. package/cli/src/state/validators.ts +81 -0
  40. package/cli/src/utils/colors.ts +12 -0
  41. package/package.json +49 -0
  42. package/skills/auto-kit/SKILL.md +214 -0
  43. package/skills/build/SKILL.md +88 -0
  44. package/skills/build/stages/commit.md +43 -0
  45. package/skills/build/stages/core.md +48 -0
  46. package/skills/build/stages/integration.md +44 -0
  47. package/skills/build/stages/migration.md +41 -0
  48. package/skills/build/stages/red.md +44 -0
  49. package/skills/build/stages/refactor.md +48 -0
  50. package/skills/build/stages/setup.md +42 -0
  51. package/skills/build/stages/ui.md +51 -0
  52. package/skills/deploy/SKILL.md +62 -0
  53. package/skills/deploy/stages/merge.md +47 -0
  54. package/skills/deploy/stages/monitor.md +39 -0
  55. package/skills/deploy/stages/remediate.md +54 -0
  56. package/skills/full-kit/SKILL.md +195 -0
  57. package/skills/plan/SKILL.md +77 -0
  58. package/skills/plan/stages/architecture.md +53 -0
  59. package/skills/plan/stages/audit.md +58 -0
  60. package/skills/plan/stages/blueprint.md +60 -0
  61. package/skills/plan/stages/clarify.md +61 -0
  62. package/skills/plan/stages/investigate.md +47 -0
  63. package/skills/plan/stages/scope.md +46 -0
  64. package/skills/plan/stages/sketch.md +44 -0
  65. package/skills/plan/stages/ux-flow.md +49 -0
  66. package/skills/review/SKILL.md +104 -0
  67. package/skills/review/stages/compliance.md +48 -0
  68. package/skills/review/stages/handoff.md +59 -0
  69. package/skills/review/stages/performance.md +45 -0
  70. package/skills/review/stages/security.md +49 -0
  71. package/skills/review/stages/self-review.md +41 -0
  72. package/skills/test/SKILL.md +83 -0
  73. package/skills/test/stages/e2e.md +44 -0
  74. package/skills/test/stages/validate.md +51 -0
  75. package/skills/test/stages/verify.md +41 -0
  76. package/skills/wrap-up/SKILL.md +107 -0
@@ -0,0 +1,159 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as readline from "node:readline";
4
+ import { doctorCommand } from "./doctor.js";
5
+ import { bold, dim, green, yellow, red, cyan } from "../utils/colors.js";
6
+
7
+ const SKILLS_SOURCE = path.resolve(import.meta.dirname, "..", "..", "..", "skills");
8
+
9
+ function findClaudeProjects(): { name: string; path: string }[] {
10
+ const claudeDir = path.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "projects");
11
+ if (!fs.existsSync(claudeDir)) return [];
12
+
13
+ const entries = fs.readdirSync(claudeDir, { withFileTypes: true });
14
+ const projects: { name: string; path: string }[] = [];
15
+
16
+ for (const entry of entries) {
17
+ if (!entry.isDirectory()) continue;
18
+ // Convert directory name back to filesystem path: -home-user-project → /home/user/project
19
+ const projectPath = entry.name.replace(/^-/, "/").replace(/-/g, "/");
20
+ if (fs.existsSync(projectPath)) {
21
+ projects.push({ name: path.basename(projectPath), path: projectPath });
22
+ }
23
+ }
24
+
25
+ return projects.sort((a, b) => a.path.localeCompare(b.path));
26
+ }
27
+
28
+ function isClaudeProject(dir: string): boolean {
29
+ return fs.existsSync(path.join(dir, ".claude"));
30
+ }
31
+
32
+ function copySkills(targetDir: string): { copied: string[]; skipped: string[] } {
33
+ const skillsTarget = path.join(targetDir, ".claude", "skills");
34
+ const copied: string[] = [];
35
+ const skipped: string[] = [];
36
+
37
+ if (!fs.existsSync(SKILLS_SOURCE)) {
38
+ throw new Error(`Skills source not found at ${SKILLS_SOURCE}. Is work-kit installed correctly?`);
39
+ }
40
+
41
+ function copyDir(src: string, dest: string, prefix: string = "") {
42
+ if (!fs.existsSync(dest)) {
43
+ fs.mkdirSync(dest, { recursive: true });
44
+ }
45
+ const entries = fs.readdirSync(src, { withFileTypes: true });
46
+ for (const entry of entries) {
47
+ const srcPath = path.join(src, entry.name);
48
+ const destPath = path.join(dest, entry.name);
49
+ const label = prefix ? `${prefix}/${entry.name}` : entry.name;
50
+
51
+ if (entry.isDirectory()) {
52
+ copyDir(srcPath, destPath, label);
53
+ } else {
54
+ if (fs.existsSync(destPath)) {
55
+ const srcContent = fs.readFileSync(srcPath, "utf-8");
56
+ const destContent = fs.readFileSync(destPath, "utf-8");
57
+ if (srcContent === destContent) {
58
+ skipped.push(label);
59
+ continue;
60
+ }
61
+ }
62
+ fs.copyFileSync(srcPath, destPath);
63
+ copied.push(label);
64
+ }
65
+ }
66
+ }
67
+
68
+ copyDir(SKILLS_SOURCE, skillsTarget);
69
+ return { copied, skipped };
70
+ }
71
+
72
+ async function promptUser(question: string): Promise<string> {
73
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
74
+ return new Promise((resolve) => {
75
+ rl.question(question, (answer) => {
76
+ rl.close();
77
+ resolve(answer.trim());
78
+ });
79
+ });
80
+ }
81
+
82
+ export async function setupCommand(targetPath?: string): Promise<void> {
83
+ let projectDir: string;
84
+
85
+ if (targetPath) {
86
+ // Explicit path provided
87
+ projectDir = path.resolve(targetPath);
88
+ if (!fs.existsSync(projectDir)) {
89
+ console.error(`Directory not found: ${projectDir}`);
90
+ process.exit(1);
91
+ }
92
+ } else if (isClaudeProject(process.cwd())) {
93
+ // Current directory is a Claude project
94
+ projectDir = process.cwd();
95
+ console.error(`Installing to current project: ${bold(projectDir)}`);
96
+ } else {
97
+ // Discover Claude projects and let user pick
98
+ const projects = findClaudeProjects();
99
+
100
+ if (projects.length === 0) {
101
+ console.error("No Claude Code projects found on this machine.");
102
+ const manual = await promptUser("Enter project path: ");
103
+ if (!manual || !fs.existsSync(manual)) {
104
+ console.error("Invalid path. Exiting.");
105
+ process.exit(1);
106
+ }
107
+ projectDir = path.resolve(manual);
108
+ } else {
109
+ console.error("Found Claude Code projects:\n");
110
+ for (let i = 0; i < projects.length; i++) {
111
+ console.error(` ${cyan(String(i + 1))}. ${projects[i].path}`);
112
+ }
113
+ console.error();
114
+
115
+ const answer = await promptUser("Install work-kit skills to (number or path): ");
116
+ const idx = parseInt(answer, 10);
117
+
118
+ if (!isNaN(idx) && idx >= 1 && idx <= projects.length) {
119
+ projectDir = projects[idx - 1].path;
120
+ } else if (fs.existsSync(answer)) {
121
+ projectDir = path.resolve(answer);
122
+ } else {
123
+ console.error("Invalid selection. Exiting.");
124
+ process.exit(1);
125
+ }
126
+ }
127
+ }
128
+
129
+ // Copy skills
130
+ console.error(`\nCopying skills to ${projectDir}/.claude/skills/...`);
131
+ const { copied, skipped } = copySkills(projectDir);
132
+
133
+ if (copied.length > 0) {
134
+ for (const f of copied) {
135
+ console.error(` ${green("+")} ${f}`);
136
+ }
137
+ }
138
+ if (skipped.length > 0) {
139
+ console.error(` (${skipped.length} files unchanged)`);
140
+ }
141
+ if (copied.length === 0 && skipped.length > 0) {
142
+ console.error(` ${dim("Already up to date.")}`);
143
+ }
144
+
145
+ // Run doctor against the target project
146
+ console.error("\nRunning doctor...");
147
+ const result = doctorCommand(projectDir);
148
+ for (const check of result.checks) {
149
+ const icon = check.status === "pass" ? green("\u2713") : check.status === "warn" ? yellow("!") : red("\u2717");
150
+ console.error(` ${icon} ${bold(check.name)}: ${check.message}`);
151
+ }
152
+
153
+ console.error();
154
+ if (result.ok) {
155
+ console.error(green(bold("Ready. Use /full-kit or /auto-kit in Claude Code.")));
156
+ } else {
157
+ console.error(red("Setup complete but some checks failed. Review the issues above."));
158
+ }
159
+ }
@@ -0,0 +1,50 @@
1
+ import { PHASE_NAMES } from "../state/schema.js";
2
+ import { readState, findWorktreeRoot } from "../state/store.js";
3
+
4
+ interface StatusOutput {
5
+ slug: string;
6
+ branch: string;
7
+ mode: string;
8
+ classification?: string;
9
+ status: string;
10
+ currentPhase: string | null;
11
+ currentSubStage: string | null;
12
+ started: string;
13
+ phases: Record<string, { status: string; completed: number; total: number; active: number }>;
14
+ loopbackCount: number;
15
+ }
16
+
17
+ export function statusCommand(worktreeRoot?: string): StatusOutput {
18
+ const root = worktreeRoot || findWorktreeRoot();
19
+ if (!root) {
20
+ throw new Error("No work-kit state found. Run `work-kit init` first.");
21
+ }
22
+
23
+ const state = readState(root);
24
+
25
+ const phases: StatusOutput["phases"] = {};
26
+ for (const phase of PHASE_NAMES) {
27
+ const ps = state.phases[phase];
28
+ let completed = 0, total = 0, active = 0;
29
+ for (const ss of Object.values(ps.subStages)) {
30
+ if (ss.status === "skipped") continue;
31
+ total++;
32
+ if (ss.status === "completed") completed++;
33
+ else if (ss.status === "in-progress") active++;
34
+ }
35
+ phases[phase] = { status: ps.status, completed, total, active };
36
+ }
37
+
38
+ return {
39
+ slug: state.slug,
40
+ branch: state.branch,
41
+ mode: state.mode,
42
+ ...(state.classification && { classification: state.classification }),
43
+ status: state.status,
44
+ currentPhase: state.currentPhase,
45
+ currentSubStage: state.currentSubStage,
46
+ started: state.started,
47
+ phases,
48
+ loopbackCount: state.loopbacks.length,
49
+ };
50
+ }
@@ -0,0 +1,89 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as readline from "node:readline";
4
+ import { bold, dim, green, red, yellow } from "../utils/colors.js";
5
+
6
+ const WORK_KIT_SKILLS = [
7
+ "full-kit", "auto-kit", "plan", "build", "test", "review", "deploy", "wrap-up",
8
+ ];
9
+
10
+ async function promptUser(question: string): Promise<string> {
11
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
12
+ return new Promise((resolve) => {
13
+ rl.question(question, (answer) => {
14
+ rl.close();
15
+ resolve(answer.trim());
16
+ });
17
+ });
18
+ }
19
+
20
+ function removeDir(dir: string): boolean {
21
+ if (!fs.existsSync(dir)) return false;
22
+ fs.rmSync(dir, { recursive: true, force: true });
23
+ return true;
24
+ }
25
+
26
+ export async function uninstallCommand(targetPath?: string): Promise<void> {
27
+ const projectDir = targetPath ? path.resolve(targetPath) : process.cwd();
28
+ const skillsDir = path.join(projectDir, ".claude", "skills");
29
+
30
+ if (!fs.existsSync(skillsDir)) {
31
+ console.error(red("No .claude/skills/ directory found. Nothing to uninstall."));
32
+ process.exit(1);
33
+ }
34
+
35
+ // Check which work-kit skills are installed
36
+ const installed: string[] = [];
37
+ for (const skill of WORK_KIT_SKILLS) {
38
+ const skillPath = path.join(skillsDir, skill);
39
+ if (fs.existsSync(skillPath)) {
40
+ installed.push(skill);
41
+ }
42
+ }
43
+
44
+ if (installed.length === 0) {
45
+ console.error(dim("No work-kit skills found in this project."));
46
+ return;
47
+ }
48
+
49
+ console.error(`\nFound ${bold(String(installed.length))} work-kit skills in ${bold(projectDir)}:`);
50
+ for (const skill of installed) {
51
+ console.error(` ${skill}/`);
52
+ }
53
+
54
+ // Check for active state
55
+ const stateFile = path.join(projectDir, ".work-kit", "state.json");
56
+ if (fs.existsSync(stateFile)) {
57
+ console.error(yellow("\nWarning: Active work-kit state found (.work-kit/state.json)."));
58
+ console.error(yellow("Uninstalling will not remove in-progress state files."));
59
+ }
60
+
61
+ const answer = await promptUser("\nRemove all work-kit skills? (y/N): ");
62
+ if (answer.toLowerCase() !== "y") {
63
+ console.error(dim("Cancelled."));
64
+ return;
65
+ }
66
+
67
+ // Remove skill directories
68
+ let removed = 0;
69
+ for (const skill of installed) {
70
+ const skillPath = path.join(skillsDir, skill);
71
+ if (removeDir(skillPath)) {
72
+ console.error(` ${red("-")} ${skill}/`);
73
+ removed++;
74
+ }
75
+ }
76
+
77
+ console.error(`\n${green(bold(`Removed ${removed} work-kit skill(s).`))}`);
78
+
79
+ // Check if .claude/skills/ is now empty
80
+ if (fs.existsSync(skillsDir)) {
81
+ const remaining = fs.readdirSync(skillsDir);
82
+ if (remaining.length === 0) {
83
+ fs.rmdirSync(skillsDir);
84
+ console.error(dim("Removed empty .claude/skills/ directory."));
85
+ } else {
86
+ console.error(dim(`${remaining.length} other skill(s) remain in .claude/skills/.`));
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,12 @@
1
+ import { setupCommand } from "./setup.js";
2
+
3
+ export async function upgradeCommand(worktreeRoot?: string): Promise<void> {
4
+ const target = worktreeRoot || process.cwd();
5
+ // setupCommand already handles:
6
+ // - detecting .claude/ in current dir
7
+ // - copying skills (skipping unchanged)
8
+ // - running doctor
9
+ // Just add upgrade-specific messaging
10
+ console.error("Upgrading work-kit skills to latest version...\n");
11
+ await setupCommand(target);
12
+ }
@@ -0,0 +1,34 @@
1
+ import { readState, findWorktreeRoot } from "../state/store.js";
2
+ import { validatePhasePrerequisites } from "../state/validators.js";
3
+ import type { PhaseName } from "../state/schema.js";
4
+
5
+ interface ValidateResult {
6
+ phase: PhaseName;
7
+ valid: boolean;
8
+ message: string;
9
+ missingPrerequisite?: PhaseName;
10
+ phaseStatus: string;
11
+ }
12
+
13
+ export function validateCommand(phase: PhaseName, worktreeRoot?: string): ValidateResult {
14
+ const root = worktreeRoot || findWorktreeRoot();
15
+ if (!root) {
16
+ throw new Error("No work-kit state found. Run `work-kit init` first.");
17
+ }
18
+
19
+ const state = readState(root);
20
+
21
+ if (!state.phases[phase]) {
22
+ throw new Error(`Unknown phase: ${phase}`);
23
+ }
24
+
25
+ const result = validatePhasePrerequisites(state, phase);
26
+
27
+ return {
28
+ phase,
29
+ valid: result.valid,
30
+ message: result.message,
31
+ missingPrerequisite: result.missingPrerequisite,
32
+ phaseStatus: state.phases[phase].status,
33
+ };
34
+ }
@@ -0,0 +1,125 @@
1
+ import { readState, writeState, findWorktreeRoot } from "../state/store.js";
2
+ import { SUBSTAGES_BY_PHASE, PHASE_NAMES } from "../state/schema.js";
3
+ import { parseLocation } from "../state/helpers.js";
4
+ import type { Action } from "../state/schema.js";
5
+
6
+ interface WorkflowStatus {
7
+ action: "workflow_status";
8
+ workflow: { step: string; status: string }[];
9
+ }
10
+
11
+ export type WorkflowResult = Action | WorkflowStatus;
12
+
13
+ export function workflowCommand(opts: {
14
+ add?: string;
15
+ remove?: string;
16
+ worktreeRoot?: string;
17
+ }): WorkflowResult {
18
+ const root = opts.worktreeRoot || findWorktreeRoot();
19
+ if (!root) {
20
+ return { action: "error", message: "No work-kit state found." };
21
+ }
22
+
23
+ const state = readState(root);
24
+
25
+ if (state.mode !== "auto-kit") {
26
+ return { action: "error", message: "Workflow management is only available in auto-kit mode." };
27
+ }
28
+
29
+ if (!state.workflow) {
30
+ return { action: "error", message: "No workflow defined in state." };
31
+ }
32
+
33
+ if (opts.add) {
34
+ const { phase, subStage } = parseLocation(opts.add);
35
+
36
+ if (!PHASE_NAMES.includes(phase) || !SUBSTAGES_BY_PHASE[phase].includes(subStage)) {
37
+ return { action: "error", message: `Invalid step: ${opts.add}` };
38
+ }
39
+
40
+ const existing = state.workflow.find((s) => s.phase === phase && s.subStage === subStage);
41
+ if (existing) {
42
+ if (existing.included) {
43
+ return { action: "error", message: `${opts.add} is already in the workflow.` };
44
+ }
45
+ existing.included = true;
46
+ } else {
47
+ const phaseIdx = PHASE_NAMES.indexOf(phase);
48
+ const subStageIdx = SUBSTAGES_BY_PHASE[phase].indexOf(subStage);
49
+
50
+ let insertIdx = state.workflow.length;
51
+ for (let i = 0; i < state.workflow.length; i++) {
52
+ const wi = state.workflow[i];
53
+ const wiPhaseIdx = PHASE_NAMES.indexOf(wi.phase);
54
+ const wiSubIdx = SUBSTAGES_BY_PHASE[wi.phase].indexOf(wi.subStage);
55
+
56
+ if (wiPhaseIdx > phaseIdx || (wiPhaseIdx === phaseIdx && wiSubIdx > subStageIdx)) {
57
+ insertIdx = i;
58
+ break;
59
+ }
60
+ }
61
+
62
+ state.workflow.splice(insertIdx, 0, { phase, subStage, included: true });
63
+ }
64
+
65
+ const currentSS = state.phases[phase].subStages[subStage];
66
+ if (currentSS?.status === "completed") {
67
+ return { action: "error", message: `Cannot add ${opts.add} — it's already completed.` };
68
+ }
69
+ if (!currentSS) {
70
+ state.phases[phase].subStages[subStage] = { status: "pending" };
71
+ } else if (currentSS.status === "skipped") {
72
+ currentSS.status = "pending";
73
+ }
74
+
75
+ if (state.phases[phase].status === "skipped") {
76
+ state.phases[phase].status = "pending";
77
+ }
78
+
79
+ writeState(root, state);
80
+ return { action: "wait_for_user", message: `Added ${opts.add} to workflow.` };
81
+ }
82
+
83
+ if (opts.remove) {
84
+ const { phase, subStage } = parseLocation(opts.remove);
85
+
86
+ const step = state.workflow.find((s) => s.phase === phase && s.subStage === subStage);
87
+ if (!step) {
88
+ return { action: "error", message: `${opts.remove} is not in the workflow.` };
89
+ }
90
+
91
+ const ssState = state.phases[phase]?.subStages[subStage];
92
+ if (ssState?.status === "completed") {
93
+ return { action: "error", message: `Cannot remove ${opts.remove} — it's already completed.` };
94
+ }
95
+ if (ssState?.status === "in-progress") {
96
+ return { action: "error", message: `Cannot remove ${opts.remove} — it's currently in progress.` };
97
+ }
98
+
99
+ step.included = false;
100
+
101
+ if (ssState) {
102
+ ssState.status = "skipped";
103
+ }
104
+
105
+ const allSkipped = Object.values(state.phases[phase].subStages).every(
106
+ (s) => s.status === "skipped"
107
+ );
108
+ if (allSkipped) {
109
+ state.phases[phase].status = "skipped";
110
+ }
111
+
112
+ writeState(root, state);
113
+ return { action: "wait_for_user", message: `Removed ${opts.remove} from workflow.` };
114
+ }
115
+
116
+ // No add/remove — show current workflow
117
+ const workflow = state.workflow
118
+ .filter((s) => s.included)
119
+ .map((s) => ({
120
+ step: `${s.phase}/${s.subStage}`,
121
+ status: state.phases[s.phase]?.subStages[s.subStage]?.status || "unknown",
122
+ }));
123
+
124
+ return { action: "workflow_status", workflow };
125
+ }
@@ -0,0 +1,62 @@
1
+ import { PhaseName } from "../state/schema.js";
2
+
3
+ /**
4
+ * Maps each phase/sub-stage to the sections it needs from state.md.
5
+ * "##" prefix = top-level section, "###" prefix = Final section.
6
+ */
7
+
8
+ export interface AgentContext {
9
+ sections: string[]; // sections to extract from state.md
10
+ needsGitDiff?: boolean; // whether the agent needs `git diff main...HEAD`
11
+ }
12
+
13
+ // Phase-level context (what the phase runner agent reads)
14
+ export const PHASE_CONTEXT: Record<PhaseName, AgentContext> = {
15
+ plan: {
16
+ sections: ["## Description", "## Criteria"],
17
+ },
18
+ build: {
19
+ sections: ["### Plan: Final", "## Criteria", "## Description"],
20
+ },
21
+ test: {
22
+ sections: ["### Build: Final", "### Plan: Final", "## Criteria"],
23
+ },
24
+ review: {
25
+ sections: ["### Plan: Final", "### Build: Final", "### Test: Final", "## Criteria"],
26
+ },
27
+ deploy: {
28
+ sections: ["### Review: Final", "### Build: Final", "## Criteria"],
29
+ },
30
+ "wrap-up": {
31
+ sections: [], // reads full state.md
32
+ },
33
+ };
34
+
35
+ // Sub-stage-level context (for parallel sub-agents that need specific sections)
36
+ export const SUBSTAGE_CONTEXT: Record<string, AgentContext> = {
37
+ // Test sub-agents
38
+ "test/verify": { sections: ["### Build: Final", "## Criteria"] },
39
+ "test/e2e": { sections: ["### Build: Final", "### Plan: Final"] },
40
+ "test/validate": { sections: ["### Test: Verify", "### Test: E2E", "## Criteria"] },
41
+
42
+ // Review sub-agents
43
+ "review/self-review": { sections: ["### Build: Final"], needsGitDiff: true },
44
+ "review/security": { sections: ["### Build: Final"], needsGitDiff: true },
45
+ "review/performance": { sections: ["### Build: Final"], needsGitDiff: true },
46
+ "review/compliance": { sections: ["### Plan: Final", "### Build: Final"], needsGitDiff: true },
47
+ "review/handoff": {
48
+ sections: [
49
+ "### Review: Self-Review", "### Review: Security",
50
+ "### Review: Performance", "### Review: Compliance",
51
+ "### Test: Final", "## Criteria",
52
+ ],
53
+ },
54
+ };
55
+
56
+ export function getContextFor(phase: PhaseName, subStage?: string): AgentContext {
57
+ if (subStage) {
58
+ const key = `${phase}/${subStage}`;
59
+ if (SUBSTAGE_CONTEXT[key]) return SUBSTAGE_CONTEXT[key];
60
+ }
61
+ return PHASE_CONTEXT[phase];
62
+ }
@@ -0,0 +1,45 @@
1
+ import { PhaseName, Location } from "../state/schema.js";
2
+
3
+ /**
4
+ * Defines when a completed sub-stage should trigger a loop-back
5
+ * based on its outcome.
6
+ */
7
+ export interface LoopbackRoute {
8
+ from: Location;
9
+ triggerOutcome: string;
10
+ to: Location;
11
+ reason: string;
12
+ }
13
+
14
+ export const LOOPBACK_ROUTES: LoopbackRoute[] = [
15
+ {
16
+ from: { phase: "plan", subStage: "audit" },
17
+ triggerOutcome: "revise",
18
+ to: { phase: "plan", subStage: "blueprint" },
19
+ reason: "Audit found gaps — revising Blueprint",
20
+ },
21
+ {
22
+ from: { phase: "build", subStage: "refactor" },
23
+ triggerOutcome: "broken",
24
+ to: { phase: "build", subStage: "core" },
25
+ reason: "Refactor broke tests — re-running Core to fix",
26
+ },
27
+ {
28
+ from: { phase: "review", subStage: "handoff" },
29
+ triggerOutcome: "changes_requested",
30
+ to: { phase: "build", subStage: "core" },
31
+ reason: "Review requested changes — looping back to Build/Core",
32
+ },
33
+ {
34
+ from: { phase: "deploy", subStage: "merge" },
35
+ triggerOutcome: "fix_needed",
36
+ to: { phase: "build", subStage: "core" },
37
+ reason: "Merge blocked — fix needed in Build/Core",
38
+ },
39
+ {
40
+ from: { phase: "deploy", subStage: "remediate" },
41
+ triggerOutcome: "fix_and_redeploy",
42
+ to: { phase: "build", subStage: "core" },
43
+ reason: "Deployment issue — fix and redeploy from Build/Core",
44
+ },
45
+ ];