@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,119 @@
1
+ import {
2
+ PhaseName,
3
+ SUBSTAGES_BY_PHASE,
4
+ Classification,
5
+ WorkflowStep,
6
+ } from "../state/schema.js";
7
+
8
+ // ── Phase Order ──────────────────────────────────────────────────────
9
+
10
+ export const PHASE_ORDER: PhaseName[] = ["plan", "build", "test", "review", "deploy", "wrap-up"];
11
+
12
+ // ── Prerequisites ────────────────────────────────────────────────────
13
+
14
+ export const PHASE_PREREQUISITES: Record<PhaseName, PhaseName | null> = {
15
+ plan: null,
16
+ build: "plan",
17
+ test: "build",
18
+ review: "test",
19
+ deploy: "review",
20
+ "wrap-up": "review", // or deploy if deploy was included
21
+ };
22
+
23
+ // ── Skill File Paths ─────────────────────────────────────────────────
24
+
25
+ export function skillFilePath(phase: PhaseName, subStage?: string): string {
26
+ if (phase === "wrap-up") return ".claude/skills/wrap-up/SKILL.md";
27
+ if (!subStage) return `.claude/skills/${phase}/SKILL.md`;
28
+ return `.claude/skills/${phase}/stages/${subStage}.md`;
29
+ }
30
+
31
+ // ── Auto-kit Default Workflows ───────────────────────────────────────
32
+
33
+ type InclusionRule = "YES" | "skip" | "if UI" | "if DB" | "optional";
34
+
35
+ const WORKFLOW_MATRIX: Record<Classification, Record<string, InclusionRule>> = {
36
+ "bug-fix": {
37
+ "plan/clarify": "YES", "plan/investigate": "YES", "plan/sketch": "skip", "plan/scope": "skip",
38
+ "plan/ux-flow": "skip", "plan/architecture": "skip", "plan/blueprint": "skip", "plan/audit": "skip",
39
+ "build/setup": "skip", "build/migration": "skip", "build/red": "YES", "build/core": "YES",
40
+ "build/ui": "if UI", "build/refactor": "skip", "build/integration": "skip", "build/commit": "YES",
41
+ "test/verify": "YES", "test/e2e": "skip", "test/validate": "YES",
42
+ "review/self-review": "YES", "review/security": "skip", "review/performance": "skip",
43
+ "review/compliance": "skip", "review/handoff": "YES",
44
+ "deploy/merge": "optional", "deploy/monitor": "optional", "deploy/remediate": "optional",
45
+ "wrap-up/wrap-up": "YES",
46
+ },
47
+ "small-change": {
48
+ "plan/clarify": "YES", "plan/investigate": "skip", "plan/sketch": "skip", "plan/scope": "skip",
49
+ "plan/ux-flow": "skip", "plan/architecture": "skip", "plan/blueprint": "skip", "plan/audit": "skip",
50
+ "build/setup": "skip", "build/migration": "skip", "build/red": "skip", "build/core": "YES",
51
+ "build/ui": "if UI", "build/refactor": "skip", "build/integration": "skip", "build/commit": "YES",
52
+ "test/verify": "YES", "test/e2e": "skip", "test/validate": "skip",
53
+ "review/self-review": "YES", "review/security": "skip", "review/performance": "skip",
54
+ "review/compliance": "skip", "review/handoff": "YES",
55
+ "deploy/merge": "optional", "deploy/monitor": "optional", "deploy/remediate": "optional",
56
+ "wrap-up/wrap-up": "YES",
57
+ },
58
+ refactor: {
59
+ "plan/clarify": "YES", "plan/investigate": "YES", "plan/sketch": "skip", "plan/scope": "skip",
60
+ "plan/ux-flow": "skip", "plan/architecture": "skip", "plan/blueprint": "skip", "plan/audit": "skip",
61
+ "build/setup": "skip", "build/migration": "skip", "build/red": "skip", "build/core": "YES",
62
+ "build/ui": "if UI", "build/refactor": "YES", "build/integration": "skip", "build/commit": "YES",
63
+ "test/verify": "YES", "test/e2e": "skip", "test/validate": "skip",
64
+ "review/self-review": "YES", "review/security": "skip", "review/performance": "YES",
65
+ "review/compliance": "skip", "review/handoff": "YES",
66
+ "deploy/merge": "optional", "deploy/monitor": "optional", "deploy/remediate": "optional",
67
+ "wrap-up/wrap-up": "YES",
68
+ },
69
+ feature: {
70
+ "plan/clarify": "YES", "plan/investigate": "YES", "plan/sketch": "YES", "plan/scope": "YES",
71
+ "plan/ux-flow": "if UI", "plan/architecture": "YES", "plan/blueprint": "YES", "plan/audit": "skip",
72
+ "build/setup": "YES", "build/migration": "if DB", "build/red": "YES", "build/core": "YES",
73
+ "build/ui": "if UI", "build/refactor": "skip", "build/integration": "YES", "build/commit": "YES",
74
+ "test/verify": "YES", "test/e2e": "if UI", "test/validate": "YES",
75
+ "review/self-review": "YES", "review/security": "YES", "review/performance": "skip",
76
+ "review/compliance": "YES", "review/handoff": "YES",
77
+ "deploy/merge": "optional", "deploy/monitor": "optional", "deploy/remediate": "optional",
78
+ "wrap-up/wrap-up": "YES",
79
+ },
80
+ "large-feature": {
81
+ "plan/clarify": "YES", "plan/investigate": "YES", "plan/sketch": "YES", "plan/scope": "YES",
82
+ "plan/ux-flow": "if UI", "plan/architecture": "YES", "plan/blueprint": "YES", "plan/audit": "YES",
83
+ "build/setup": "YES", "build/migration": "if DB", "build/red": "YES", "build/core": "YES",
84
+ "build/ui": "if UI", "build/refactor": "YES", "build/integration": "YES", "build/commit": "YES",
85
+ "test/verify": "YES", "test/e2e": "YES", "test/validate": "YES",
86
+ "review/self-review": "YES", "review/security": "YES", "review/performance": "YES",
87
+ "review/compliance": "YES", "review/handoff": "YES",
88
+ "deploy/merge": "optional", "deploy/monitor": "optional", "deploy/remediate": "optional",
89
+ "wrap-up/wrap-up": "YES",
90
+ },
91
+ };
92
+
93
+ export function buildDefaultWorkflow(classification: Classification): WorkflowStep[] {
94
+ const matrix = WORKFLOW_MATRIX[classification];
95
+ const steps: WorkflowStep[] = [];
96
+
97
+ for (const [key, rule] of Object.entries(matrix)) {
98
+ const [phase, subStage] = key.split("/") as [PhaseName, string];
99
+ // "YES" always included, "skip" excluded, conditional ones included by default (user can remove)
100
+ const included = rule === "YES" || rule === "if UI" || rule === "if DB";
101
+ if (rule !== "skip") {
102
+ steps.push({ phase, subStage, included });
103
+ }
104
+ }
105
+
106
+ return steps;
107
+ }
108
+
109
+ export function buildFullWorkflow(): WorkflowStep[] {
110
+ const steps: WorkflowStep[] = [];
111
+ for (const phase of PHASE_ORDER) {
112
+ for (const subStage of SUBSTAGES_BY_PHASE[phase]) {
113
+ // Deploy is optional by default
114
+ const included = phase !== "deploy";
115
+ steps.push({ phase, subStage, included });
116
+ }
117
+ }
118
+ return steps;
119
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, it } from "node:test";
2
+ import * as assert from "node:assert/strict";
3
+ import { extractSection, extractTopSection } from "./extractor.js";
4
+
5
+ const SAMPLE_MD = `# Title
6
+
7
+ ## Description
8
+ This is the description content.
9
+ It spans multiple lines.
10
+
11
+ ## Criteria
12
+ - Criterion one
13
+ - Criterion two
14
+
15
+ ### Sub Heading A
16
+ Content A here.
17
+ More content A.
18
+
19
+ ### Sub Heading B
20
+ Content B here.
21
+
22
+ ## Decisions
23
+ Some decisions.
24
+ `;
25
+
26
+ describe("extractSection", () => {
27
+ it("finds ### section by heading", () => {
28
+ const result = extractSection(SAMPLE_MD, "Sub Heading A");
29
+ assert.notEqual(result, null);
30
+ assert.ok(result!.includes("Content A here."));
31
+ assert.ok(result!.includes("More content A."));
32
+ });
33
+
34
+ it("stops at the next ### heading", () => {
35
+ const result = extractSection(SAMPLE_MD, "Sub Heading A");
36
+ assert.notEqual(result, null);
37
+ assert.ok(!result!.includes("Content B here."));
38
+ });
39
+
40
+ it("returns null for missing section", () => {
41
+ const result = extractSection(SAMPLE_MD, "Nonexistent");
42
+ assert.equal(result, null);
43
+ });
44
+
45
+ it("works when heading already includes ###", () => {
46
+ const result = extractSection(SAMPLE_MD, "### Sub Heading B");
47
+ assert.notEqual(result, null);
48
+ assert.ok(result!.includes("Content B here."));
49
+ });
50
+ });
51
+
52
+ describe("extractTopSection", () => {
53
+ it("finds ## section", () => {
54
+ const result = extractTopSection(SAMPLE_MD, "Description");
55
+ assert.notEqual(result, null);
56
+ assert.ok(result!.includes("This is the description content."));
57
+ });
58
+
59
+ it("stops at next ## section", () => {
60
+ const result = extractTopSection(SAMPLE_MD, "Description");
61
+ assert.notEqual(result, null);
62
+ assert.ok(!result!.includes("Criterion one"));
63
+ });
64
+
65
+ it("returns null for missing section", () => {
66
+ const result = extractTopSection(SAMPLE_MD, "Nonexistent");
67
+ assert.equal(result, null);
68
+ });
69
+
70
+ it("includes ### sub-sections within the ## section", () => {
71
+ const result = extractTopSection(SAMPLE_MD, "Criteria");
72
+ assert.notEqual(result, null);
73
+ assert.ok(result!.includes("Criterion one"));
74
+ assert.ok(result!.includes("Sub Heading A"));
75
+ assert.ok(result!.includes("Content A here."));
76
+ });
77
+ });
@@ -0,0 +1,73 @@
1
+ import { readStateMd } from "../state/store.js";
2
+
3
+ /**
4
+ * Extract a specific ### section from state.md by heading.
5
+ * Returns the content between the heading and the next ### heading (or end of file).
6
+ */
7
+ export function extractSection(stateMd: string, heading: string): string | null {
8
+ // Normalize heading — ensure it starts with ###
9
+ const prefix = heading.startsWith("###") ? heading : `### ${heading}`;
10
+ const lines = stateMd.split("\n");
11
+ let capturing = false;
12
+ const captured: string[] = [];
13
+
14
+ for (const line of lines) {
15
+ if (line.trim().startsWith(prefix)) {
16
+ capturing = true;
17
+ captured.push(line);
18
+ continue;
19
+ }
20
+ if (capturing && line.trim().startsWith("### ")) {
21
+ break; // Hit next section
22
+ }
23
+ if (capturing) {
24
+ captured.push(line);
25
+ }
26
+ }
27
+
28
+ return captured.length > 0 ? captured.join("\n").trim() : null;
29
+ }
30
+
31
+ /**
32
+ * Extract a ## section (top-level section like Description, Criteria).
33
+ */
34
+ export function extractTopSection(stateMd: string, heading: string): string | null {
35
+ const prefix = heading.startsWith("##") ? heading : `## ${heading}`;
36
+ const lines = stateMd.split("\n");
37
+ let capturing = false;
38
+ const captured: string[] = [];
39
+
40
+ for (const line of lines) {
41
+ if (line.trim().startsWith(prefix) && !line.trim().startsWith("### ")) {
42
+ capturing = true;
43
+ captured.push(line);
44
+ continue;
45
+ }
46
+ if (capturing && line.trim().startsWith("## ") && !line.trim().startsWith("### ")) {
47
+ break;
48
+ }
49
+ if (capturing) {
50
+ captured.push(line);
51
+ }
52
+ }
53
+
54
+ return captured.length > 0 ? captured.join("\n").trim() : null;
55
+ }
56
+
57
+ /**
58
+ * Extract multiple sections from state.md.
59
+ */
60
+ export function extractSections(worktreeRoot: string, sectionNames: string[]): Record<string, string | null> {
61
+ const stateMd = readStateMd(worktreeRoot);
62
+ if (!stateMd) return {};
63
+
64
+ const result: Record<string, string | null> = {};
65
+ for (const name of sectionNames) {
66
+ if (name.startsWith("### ") || !name.startsWith("## ")) {
67
+ result[name] = extractSection(stateMd, name);
68
+ } else {
69
+ result[name] = extractTopSection(stateMd, name);
70
+ }
71
+ }
72
+ return result;
73
+ }
@@ -0,0 +1,70 @@
1
+ import { WorkKitState, PhaseName } from "../state/schema.js";
2
+ import { getContextFor } from "../config/agent-map.js";
3
+ import { extractSection, extractTopSection } from "./extractor.js";
4
+ import { readStateMd } from "../state/store.js";
5
+ import { skillFilePath } from "../config/phases.js";
6
+
7
+ /**
8
+ * Build a complete agent prompt for a given phase/sub-stage.
9
+ * Accepts optional pre-read stateMd to avoid repeated file reads in parallel scenarios.
10
+ */
11
+ export function buildAgentPrompt(
12
+ worktreeRoot: string,
13
+ state: WorkKitState,
14
+ phase: PhaseName,
15
+ subStage: string,
16
+ stateMd?: string | null
17
+ ): string {
18
+ const ctx = getContextFor(phase, subStage);
19
+ const md = stateMd ?? readStateMd(worktreeRoot);
20
+ const skill = skillFilePath(phase, subStage);
21
+
22
+ const parts: string[] = [];
23
+
24
+ parts.push(`# Agent: ${phase}/${subStage}`);
25
+ parts.push(`**Worktree:** ${worktreeRoot}`);
26
+ parts.push(`**Slug:** ${state.slug}`);
27
+ parts.push(`**Branch:** ${state.branch}`);
28
+ parts.push(`**Mode:** ${state.mode}`);
29
+ parts.push("");
30
+
31
+ parts.push(`## Instructions`);
32
+ parts.push(`Read and follow the skill file: \`${skill}\``);
33
+ parts.push("");
34
+
35
+ if (phase === "wrap-up" && md) {
36
+ parts.push(`## Full State`);
37
+ parts.push(md);
38
+ parts.push("");
39
+ } else if (md && ctx.sections.length > 0) {
40
+ parts.push(`## Context from state.md`);
41
+ parts.push("");
42
+
43
+ for (const sectionName of ctx.sections) {
44
+ const content = sectionName.startsWith("### ")
45
+ ? extractSection(md, sectionName)
46
+ : extractTopSection(md, sectionName);
47
+ if (content) {
48
+ parts.push(content);
49
+ parts.push("");
50
+ }
51
+ }
52
+ }
53
+
54
+ if (ctx.needsGitDiff) {
55
+ parts.push(`## Git Diff`);
56
+ parts.push(`Run \`git diff main...HEAD\` to review the changes.`);
57
+ parts.push("");
58
+ }
59
+
60
+ parts.push(`## Output`);
61
+ parts.push(`Write your outputs to \`.work-kit/state.md\` under a section for this sub-stage.`);
62
+
63
+ if (subStage === "wrap-up") {
64
+ parts.push(`Follow the wrap-up skill file instructions for archiving and cleanup.`);
65
+ } else {
66
+ parts.push(`When done, report your outcome so the orchestrator can run: \`npx work-kit complete ${phase}/${subStage} --outcome <outcome>\``);
67
+ }
68
+
69
+ return parts.join("\n");
70
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, it } from "node:test";
2
+ import * as assert from "node:assert/strict";
3
+ import { checkLoopback } from "./loopbacks.js";
4
+
5
+ describe("checkLoopback", () => {
6
+ it("plan/audit with 'revise' loops back to plan/blueprint", () => {
7
+ const result = checkLoopback("plan", "audit", "revise");
8
+ assert.notEqual(result, null);
9
+ assert.deepStrictEqual(result!.to, { phase: "plan", subStage: "blueprint" });
10
+ assert.ok(result!.reason.length > 0);
11
+ });
12
+
13
+ it("plan/audit with 'proceed' returns null (no loopback)", () => {
14
+ const result = checkLoopback("plan", "audit", "proceed");
15
+ assert.equal(result, null);
16
+ });
17
+
18
+ it("review/handoff with 'changes_requested' loops back to build/core", () => {
19
+ const result = checkLoopback("review", "handoff", "changes_requested");
20
+ assert.notEqual(result, null);
21
+ assert.deepStrictEqual(result!.to, { phase: "build", subStage: "core" });
22
+ });
23
+
24
+ it("build/core with 'done' returns null", () => {
25
+ const result = checkLoopback("build", "core", "done");
26
+ assert.equal(result, null);
27
+ });
28
+
29
+ it("returns null when outcome is undefined", () => {
30
+ const result = checkLoopback("plan", "audit");
31
+ assert.equal(result, null);
32
+ });
33
+ });
@@ -0,0 +1,32 @@
1
+ import { PhaseName, Location } from "../state/schema.js";
2
+ import { LOOPBACK_ROUTES } from "../config/loopback-routes.js";
3
+
4
+ interface LoopbackResult {
5
+ to: Location;
6
+ reason: string;
7
+ }
8
+
9
+ /**
10
+ * Check if completing a sub-stage with a given outcome should trigger a loop-back.
11
+ */
12
+ export function checkLoopback(
13
+ phase: PhaseName,
14
+ subStage: string,
15
+ outcome?: string
16
+ ): LoopbackResult | null {
17
+ if (!outcome) return null;
18
+
19
+ const route = LOOPBACK_ROUTES.find(
20
+ (r) =>
21
+ r.from.phase === phase &&
22
+ r.from.subStage === subStage &&
23
+ r.triggerOutcome === outcome
24
+ );
25
+
26
+ if (!route) return null;
27
+
28
+ return {
29
+ to: route.to,
30
+ reason: route.reason,
31
+ };
32
+ }
@@ -0,0 +1,60 @@
1
+ import type { PhaseName, WorkKitState } from "../state/schema.js";
2
+
3
+ /**
4
+ * Defines which sub-stages run in parallel and which runs sequentially after.
5
+ */
6
+ export interface ParallelGroup {
7
+ parallel: string[]; // sub-stages that run concurrently
8
+ thenSequential?: string; // sub-stage that runs after all parallel complete
9
+ }
10
+
11
+ /**
12
+ * Parallel group definitions per phase.
13
+ */
14
+ const PARALLEL_GROUPS: Record<string, ParallelGroup> = {
15
+ test: {
16
+ parallel: ["verify", "e2e"],
17
+ thenSequential: "validate",
18
+ },
19
+ review: {
20
+ parallel: ["self-review", "security", "performance", "compliance"],
21
+ thenSequential: "handoff",
22
+ },
23
+ };
24
+
25
+ /**
26
+ * Check if a sub-stage triggers a parallel group.
27
+ * Triggers on any parallel member that is the first non-skipped one in the group.
28
+ * Returns null if the sub-stage is not a parallel trigger or the group doesn't apply.
29
+ */
30
+ export function getParallelGroup(phase: PhaseName, subStage: string, state?: WorkKitState): ParallelGroup | null {
31
+ const group = PARALLEL_GROUPS[phase];
32
+ if (!group) return null;
33
+
34
+ if (!group.parallel.includes(subStage)) return null;
35
+
36
+ // Find the first non-skipped parallel member
37
+ if (state) {
38
+ const phaseState = state.phases[phase];
39
+ const firstActive = group.parallel.find((ss) => {
40
+ const ssState = phaseState?.subStages[ss];
41
+ return ssState && ssState.status !== "skipped" && ssState.status !== "completed";
42
+ });
43
+ // Only trigger if this sub-stage is the first active parallel member
44
+ if (firstActive !== subStage) return null;
45
+ } else {
46
+ // No state provided — fall back to first-member trigger
47
+ if (group.parallel[0] !== subStage) return null;
48
+ }
49
+
50
+ return group;
51
+ }
52
+
53
+ /**
54
+ * Check if a sub-stage is a parallel member (part of a group, not necessarily trigger).
55
+ */
56
+ export function isParallelMember(phase: PhaseName, subStage: string): boolean {
57
+ const group = PARALLEL_GROUPS[phase];
58
+ if (!group) return false;
59
+ return group.parallel.includes(subStage);
60
+ }
@@ -0,0 +1,23 @@
1
+ import { PhaseName } from "../state/schema.js";
2
+
3
+ // ── Phase-level Wait Points ──────────────────────────────────────────
4
+ // After completing these phases, the orchestrator should wait for user
5
+ // confirmation before proceeding to the next phase.
6
+
7
+ export const WAIT_AFTER_PHASE: Set<PhaseName> = new Set([
8
+ "plan", // User reviews the blueprint before build
9
+ "build", // User reviews the PR before test
10
+ "test", // User reviews test results before review
11
+ "review", // User reviews the ship decision before deploy
12
+ ]);
13
+
14
+ // ── Phase Display Names ──────────────────────────────────────────────
15
+
16
+ export const PHASE_DISPLAY_NAMES: Record<PhaseName, string> = {
17
+ plan: "Plan",
18
+ build: "Build",
19
+ test: "Test",
20
+ review: "Review",
21
+ deploy: "Deploy",
22
+ "wrap-up": "Wrap-up",
23
+ };
@@ -0,0 +1,117 @@
1
+ import { describe, it } from "node:test";
2
+ import * as assert from "node:assert/strict";
3
+ import { nextSubStageInPhase, isPhaseComplete, determineNextStep } from "./transitions.js";
4
+ import type { WorkKitState, PhaseName, PhaseState, SubStageState } from "../state/schema.js";
5
+ import { PHASE_NAMES, SUBSTAGES_BY_PHASE } from "../state/schema.js";
6
+
7
+ function makeState(): WorkKitState {
8
+ const phases = {} as Record<PhaseName, PhaseState>;
9
+ for (const phase of PHASE_NAMES) {
10
+ const subStages: Record<string, SubStageState> = {};
11
+ for (const ss of SUBSTAGES_BY_PHASE[phase]) {
12
+ subStages[ss] = { status: "pending" };
13
+ }
14
+ phases[phase] = { status: "pending", subStages };
15
+ }
16
+ return {
17
+ version: 1,
18
+ slug: "test",
19
+ branch: "feature/test",
20
+ started: "2026-01-01",
21
+ mode: "full-kit",
22
+ status: "in-progress",
23
+ currentPhase: "plan",
24
+ currentSubStage: "clarify",
25
+ phases,
26
+ loopbacks: [],
27
+ metadata: { worktreeRoot: "/tmp/test", mainRepoRoot: "/tmp/test" },
28
+ };
29
+ }
30
+
31
+ describe("nextSubStageInPhase", () => {
32
+ it("returns first pending sub-stage", () => {
33
+ const state = makeState();
34
+ const result = nextSubStageInPhase(state, "plan");
35
+ assert.equal(result, "clarify");
36
+ });
37
+
38
+ it("returns null when all complete or skipped", () => {
39
+ const state = makeState();
40
+ for (const ss of Object.values(state.phases.plan.subStages)) {
41
+ ss.status = "completed";
42
+ }
43
+ const result = nextSubStageInPhase(state, "plan");
44
+ assert.equal(result, null);
45
+ });
46
+
47
+ it("skips completed sub-stages and returns next pending", () => {
48
+ const state = makeState();
49
+ state.phases.plan.subStages.clarify.status = "completed";
50
+ state.phases.plan.subStages.investigate.status = "completed";
51
+ const result = nextSubStageInPhase(state, "plan");
52
+ assert.equal(result, "sketch");
53
+ });
54
+ });
55
+
56
+ describe("isPhaseComplete", () => {
57
+ it("returns true when all complete or skipped", () => {
58
+ const state = makeState();
59
+ for (const ss of Object.values(state.phases.plan.subStages)) {
60
+ ss.status = "completed";
61
+ }
62
+ assert.equal(isPhaseComplete(state, "plan"), true);
63
+ });
64
+
65
+ it("returns true with mix of completed and skipped", () => {
66
+ const state = makeState();
67
+ let first = true;
68
+ for (const ss of Object.values(state.phases.plan.subStages)) {
69
+ ss.status = first ? "skipped" : "completed";
70
+ first = false;
71
+ }
72
+ assert.equal(isPhaseComplete(state, "plan"), true);
73
+ });
74
+
75
+ it("returns false when some sub-stages are pending", () => {
76
+ const state = makeState();
77
+ assert.equal(isPhaseComplete(state, "plan"), false);
78
+ });
79
+ });
80
+
81
+ describe("determineNextStep", () => {
82
+ it("returns complete when state is completed", () => {
83
+ const state = makeState();
84
+ state.status = "completed";
85
+ const step = determineNextStep(state);
86
+ assert.equal(step.type, "complete");
87
+ });
88
+
89
+ it("returns phase-boundary when no current phase", () => {
90
+ const state = makeState();
91
+ state.currentPhase = null;
92
+ const step = determineNextStep(state);
93
+ assert.equal(step.type, "phase-boundary");
94
+ assert.equal(step.phase, "plan");
95
+ });
96
+
97
+ it("returns sub-stage for current phase with pending work", () => {
98
+ const state = makeState();
99
+ state.currentPhase = "plan";
100
+ state.phases.plan.status = "in-progress";
101
+ const step = determineNextStep(state);
102
+ assert.equal(step.type, "sub-stage");
103
+ assert.equal(step.phase, "plan");
104
+ assert.equal(step.subStage, "clarify");
105
+ });
106
+
107
+ it("returns wait-for-user when current phase is complete", () => {
108
+ const state = makeState();
109
+ state.currentPhase = "plan";
110
+ for (const ss of Object.values(state.phases.plan.subStages)) {
111
+ ss.status = "completed";
112
+ }
113
+ const step = determineNextStep(state);
114
+ assert.equal(step.type, "wait-for-user");
115
+ assert.equal(step.phase, "build");
116
+ });
117
+ });