@pi-agents/orchid 0.1.0-beta.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 (163) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/agents/AGENTS-MANIFEST.md +42 -0
  5. package/agents/brain.md +42 -0
  6. package/agents/context-builder.md +46 -0
  7. package/agents/delegate.md +12 -0
  8. package/agents/dev-1.md +42 -0
  9. package/agents/oracle.md +73 -0
  10. package/agents/planner.md +55 -0
  11. package/agents/researcher.md +52 -0
  12. package/agents/reviewer.md +79 -0
  13. package/agents/scout.md +50 -0
  14. package/agents/tester.md +45 -0
  15. package/agents/worker.md +55 -0
  16. package/extensions/ralph.ts +1 -0
  17. package/extensions/reviewer-extension.ts +125 -0
  18. package/extensions/task-orchestrator.ts +28 -0
  19. package/package.json +63 -0
  20. package/prompts/gather-context-and-clarify.md +13 -0
  21. package/prompts/parallel-cleanup.md +59 -0
  22. package/prompts/parallel-context-build.md +53 -0
  23. package/prompts/parallel-handoff-plan.md +59 -0
  24. package/prompts/parallel-research.md +50 -0
  25. package/prompts/parallel-review.md +54 -0
  26. package/prompts/review-loop.md +41 -0
  27. package/skills/orchid/SKILL.md +214 -0
  28. package/skills/orchid/orchid-cleanup/SKILL.md +122 -0
  29. package/skills/orchid/orchid-converge/SKILL.md +124 -0
  30. package/skills/orchid/orchid-decompose/SKILL.md +201 -0
  31. package/skills/orchid/orchid-doctor/SKILL.md +162 -0
  32. package/skills/orchid/orchid-investigate/SKILL.md +102 -0
  33. package/skills/orchid/orchid-launch/SKILL.md +147 -0
  34. package/skills/ralph/SKILL.md +73 -0
  35. package/skills/subagents/pi-subagents/SKILL.md +813 -0
  36. package/src/index.ts +7 -0
  37. package/src/orchestrator/abort.ts +534 -0
  38. package/src/orchestrator/agent-bridge-extension.ts +1020 -0
  39. package/src/orchestrator/agent-host.ts +954 -0
  40. package/src/orchestrator/cleanup.ts +776 -0
  41. package/src/orchestrator/config-loader.ts +1412 -0
  42. package/src/orchestrator/config-schema.ts +690 -0
  43. package/src/orchestrator/config.ts +81 -0
  44. package/src/orchestrator/context-window.ts +66 -0
  45. package/src/orchestrator/diagnostic-reports.ts +475 -0
  46. package/src/orchestrator/diagnostics.ts +394 -0
  47. package/src/orchestrator/discovery.ts +1833 -0
  48. package/src/orchestrator/engine-worker.ts +415 -0
  49. package/src/orchestrator/engine.ts +5940 -0
  50. package/src/orchestrator/execution.ts +3104 -0
  51. package/src/orchestrator/extension.ts +5934 -0
  52. package/src/orchestrator/formatting.ts +785 -0
  53. package/src/orchestrator/git.ts +88 -0
  54. package/src/orchestrator/index.ts +28 -0
  55. package/src/orchestrator/lane-runner.ts +1787 -0
  56. package/src/orchestrator/mailbox.ts +780 -0
  57. package/src/orchestrator/merge.ts +3414 -0
  58. package/src/orchestrator/messages.ts +1062 -0
  59. package/src/orchestrator/migrations.ts +278 -0
  60. package/src/orchestrator/naming.ts +117 -0
  61. package/src/orchestrator/path-resolver.ts +275 -0
  62. package/src/orchestrator/persistence.ts +2625 -0
  63. package/src/orchestrator/process-registry.ts +452 -0
  64. package/src/orchestrator/quality-gate.ts +1085 -0
  65. package/src/orchestrator/resume.ts +3488 -0
  66. package/src/orchestrator/sessions.ts +57 -0
  67. package/src/orchestrator/settings-loader.ts +136 -0
  68. package/src/orchestrator/settings-tui.ts +2208 -0
  69. package/src/orchestrator/sidecar-telemetry.ts +267 -0
  70. package/src/orchestrator/supervisor.ts +4548 -0
  71. package/src/orchestrator/task-executor-core.ts +675 -0
  72. package/src/orchestrator/tmux-compat.ts +37 -0
  73. package/src/orchestrator/tool-allowlist-constants.ts +37 -0
  74. package/src/orchestrator/types.ts +4465 -0
  75. package/src/orchestrator/verification.ts +547 -0
  76. package/src/orchestrator/waves.ts +1564 -0
  77. package/src/orchestrator/workspace.ts +707 -0
  78. package/src/orchestrator/worktree.ts +2725 -0
  79. package/src/ralph/index.ts +825 -0
  80. package/src/subagents/agents/agent-management.ts +648 -0
  81. package/src/subagents/agents/agent-scope.ts +6 -0
  82. package/src/subagents/agents/agent-selection.ts +23 -0
  83. package/src/subagents/agents/agent-serializer.ts +86 -0
  84. package/src/subagents/agents/agents.ts +832 -0
  85. package/src/subagents/agents/chain-serializer.ts +137 -0
  86. package/src/subagents/agents/frontmatter.ts +29 -0
  87. package/src/subagents/agents/identity.ts +30 -0
  88. package/src/subagents/agents/skills.ts +632 -0
  89. package/src/subagents/extension/config.ts +16 -0
  90. package/src/subagents/extension/control-notices.ts +92 -0
  91. package/src/subagents/extension/doctor.ts +199 -0
  92. package/src/subagents/extension/fanout-child.ts +170 -0
  93. package/src/subagents/extension/index.ts +573 -0
  94. package/src/subagents/extension/schemas.ts +168 -0
  95. package/src/subagents/intercom/intercom-bridge.ts +379 -0
  96. package/src/subagents/intercom/result-intercom.ts +377 -0
  97. package/src/subagents/runs/background/async-execution.ts +712 -0
  98. package/src/subagents/runs/background/async-job-tracker.ts +310 -0
  99. package/src/subagents/runs/background/async-resume.ts +345 -0
  100. package/src/subagents/runs/background/async-status.ts +325 -0
  101. package/src/subagents/runs/background/completion-dedupe.ts +63 -0
  102. package/src/subagents/runs/background/notify.ts +108 -0
  103. package/src/subagents/runs/background/parallel-groups.ts +45 -0
  104. package/src/subagents/runs/background/result-watcher.ts +307 -0
  105. package/src/subagents/runs/background/run-id-resolver.ts +83 -0
  106. package/src/subagents/runs/background/run-status.ts +269 -0
  107. package/src/subagents/runs/background/stale-run-reconciler.ts +336 -0
  108. package/src/subagents/runs/background/subagent-runner.ts +1808 -0
  109. package/src/subagents/runs/background/top-level-async.ts +13 -0
  110. package/src/subagents/runs/foreground/chain-clarify.ts +1333 -0
  111. package/src/subagents/runs/foreground/chain-execution.ts +938 -0
  112. package/src/subagents/runs/foreground/execution.ts +918 -0
  113. package/src/subagents/runs/foreground/subagent-executor.ts +2527 -0
  114. package/src/subagents/runs/shared/completion-guard.ts +147 -0
  115. package/src/subagents/runs/shared/long-running-guard.ts +175 -0
  116. package/src/subagents/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
  117. package/src/subagents/runs/shared/model-fallback.ts +103 -0
  118. package/src/subagents/runs/shared/nested-events.ts +819 -0
  119. package/src/subagents/runs/shared/nested-path.ts +52 -0
  120. package/src/subagents/runs/shared/nested-render.ts +115 -0
  121. package/src/subagents/runs/shared/parallel-utils.ts +109 -0
  122. package/src/subagents/runs/shared/pi-args.ts +220 -0
  123. package/src/subagents/runs/shared/pi-spawn.ts +115 -0
  124. package/src/subagents/runs/shared/run-history.ts +60 -0
  125. package/src/subagents/runs/shared/single-output.ts +164 -0
  126. package/src/subagents/runs/shared/subagent-control.ts +226 -0
  127. package/src/subagents/runs/shared/subagent-prompt-runtime.ts +170 -0
  128. package/src/subagents/runs/shared/worktree.ts +577 -0
  129. package/src/subagents/shared/artifacts.ts +98 -0
  130. package/src/subagents/shared/atomic-json.ts +16 -0
  131. package/src/subagents/shared/file-coalescer.ts +40 -0
  132. package/src/subagents/shared/fork-context.ts +76 -0
  133. package/src/subagents/shared/formatters.ts +133 -0
  134. package/src/subagents/shared/jsonl-writer.ts +81 -0
  135. package/src/subagents/shared/model-info.ts +78 -0
  136. package/src/subagents/shared/post-exit-stdio-guard.ts +85 -0
  137. package/src/subagents/shared/session-identity.ts +10 -0
  138. package/src/subagents/shared/session-tokens.ts +44 -0
  139. package/src/subagents/shared/settings.ts +397 -0
  140. package/src/subagents/shared/status-format.ts +49 -0
  141. package/src/subagents/shared/types.ts +822 -0
  142. package/src/subagents/shared/utils.ts +450 -0
  143. package/src/subagents/slash/prompt-template-bridge.ts +397 -0
  144. package/src/subagents/slash/slash-bridge.ts +174 -0
  145. package/src/subagents/slash/slash-commands.ts +528 -0
  146. package/src/subagents/slash/slash-live-state.ts +292 -0
  147. package/src/subagents/tui/render-helpers.ts +80 -0
  148. package/src/subagents/tui/render.ts +1358 -0
  149. package/templates/agents/local/supervisor.md +33 -0
  150. package/templates/agents/local/task-merger.md +27 -0
  151. package/templates/agents/local/task-reviewer.md +30 -0
  152. package/templates/agents/local/task-worker.md +34 -0
  153. package/templates/agents/supervisor-routing.md +92 -0
  154. package/templates/agents/supervisor.md +229 -0
  155. package/templates/agents/task-merger.md +214 -0
  156. package/templates/agents/task-reviewer.md +260 -0
  157. package/templates/agents/task-worker-segment.md +44 -0
  158. package/templates/agents/task-worker.md +557 -0
  159. package/templates/tasks/CONTEXT.md +30 -0
  160. package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -0
  161. package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
  162. package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -0
  163. package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -0
@@ -0,0 +1,675 @@
1
+ /**
2
+ * Task Executor Core — Headless execution semantics for Runtime V2
3
+ *
4
+ * This module owns the deterministic task execution logic for headless lane execution.
5
+ * It has NO dependency on Pi's ExtensionAPI, ExtensionContext, UI
6
+ * widgets, session lifecycle, TMUX, or TASK_AUTOSTART.
7
+ *
8
+ * Consumers:
9
+ * - lane-runner.ts (Runtime V2 headless lane execution, TP-105)
10
+ *
11
+ * Design rules:
12
+ * 1. No Pi imports. No ExtensionContext. No ctx.ui.
13
+ * 2. File I/O is explicit (path parameters, not cwd inference).
14
+ * 3. All functions are independently testable.
15
+ * 4. STATUS.md and .DONE semantics are preserved exactly.
16
+ *
17
+ * @module orchid/task-executor-core
18
+ * @since TP-103
19
+ */
20
+
21
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
22
+ import { dirname, basename, resolve, join } from "path";
23
+ import { spawnSync } from "child_process";
24
+
25
+ // ── Types ────────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Parsed step information from PROMPT.md or STATUS.md.
29
+ *
30
+ * Re-exported from the core for downstream consumers.
31
+ */
32
+ export interface StepInfo {
33
+ number: number;
34
+ name: string;
35
+ status: "not-started" | "in-progress" | "complete";
36
+ checkboxes: { text: string; checked: boolean }[];
37
+ totalChecked: number;
38
+ totalItems: number;
39
+ }
40
+
41
+ /**
42
+ * Parsed task metadata from PROMPT.md.
43
+ *
44
+ * This is the core's view of a task — independent of the orchestrator's
45
+ * ParsedTask which carries additional scheduling/routing metadata.
46
+ */
47
+ export interface CoreParsedTask {
48
+ taskId: string;
49
+ taskName: string;
50
+ reviewLevel: number;
51
+ size: string;
52
+ steps: StepInfo[];
53
+ contextDocs: string[];
54
+ taskFolder: string;
55
+ promptPath: string;
56
+ }
57
+
58
+ /**
59
+ * Parsed STATUS.md data.
60
+ */
61
+ export interface ParsedStatus {
62
+ steps: StepInfo[];
63
+ reviewCounter: number;
64
+ iteration: number;
65
+ }
66
+
67
+ // ── PROMPT.md Parsing ────────────────────────────────────────────────
68
+
69
+ /**
70
+ * Parse a PROMPT.md file into structured task metadata.
71
+ *
72
+ * Pure function — no file I/O. Caller provides content and path.
73
+ *
74
+ * @param content - Raw PROMPT.md content
75
+ * @param promptPath - Absolute path to the PROMPT.md file (used to derive taskFolder)
76
+ * @returns Parsed task metadata
77
+ */
78
+ export function parsePromptMd(content: string, promptPath: string): CoreParsedTask {
79
+ const text = content.replace(/\r\n/g, "\n");
80
+ const taskFolder = dirname(resolve(promptPath));
81
+
82
+ // Task ID and name
83
+ let taskId = "",
84
+ taskName = "";
85
+ const titleMatch = text.match(/^#\s+(?:Task:\s*)?(\S+-\d+)\s*[-–:]\s*(.+)/m);
86
+ if (titleMatch) {
87
+ taskId = titleMatch[1];
88
+ taskName = titleMatch[2].trim();
89
+ } else {
90
+ taskId = basename(taskFolder);
91
+ taskName = taskId;
92
+ }
93
+
94
+ // Review level
95
+ let reviewLevel = 0;
96
+ const rlMatch = text.match(/##\s+Review Level[:\s]*(\d)/);
97
+ if (rlMatch) reviewLevel = parseInt(rlMatch[1]);
98
+
99
+ // Size
100
+ let size = "M";
101
+ const sizeMatch = text.match(/\*\*Size:\*\*\s*(\w+)/);
102
+ if (sizeMatch) size = sizeMatch[1];
103
+
104
+ // Steps
105
+ const steps: StepInfo[] = [];
106
+ const stepRegex = /###\s+Step\s+(\d+):\s*(.+)/g;
107
+ const positions: { number: number; name: string; start: number }[] = [];
108
+ let m: RegExpExecArray | null;
109
+ while ((m = stepRegex.exec(text)) !== null) {
110
+ positions.push({ number: parseInt(m[1]), name: m[2].trim(), start: m.index });
111
+ }
112
+ for (let i = 0; i < positions.length; i++) {
113
+ const section = text.slice(
114
+ positions[i].start,
115
+ i + 1 < positions.length ? positions[i + 1].start : text.length,
116
+ );
117
+ const checkboxes: { text: string; checked: boolean }[] = [];
118
+ const cbRegex = /^\s*-\s*\[([ xX])\]\s*(.*)/gm;
119
+ let cb: RegExpExecArray | null;
120
+ while ((cb = cbRegex.exec(section)) !== null) {
121
+ checkboxes.push({ text: cb[2].trim(), checked: cb[1].toLowerCase() === "x" });
122
+ }
123
+ steps.push({
124
+ number: positions[i].number,
125
+ name: positions[i].name,
126
+ status: "not-started",
127
+ checkboxes,
128
+ totalChecked: checkboxes.filter((c) => c.checked).length,
129
+ totalItems: checkboxes.length,
130
+ });
131
+ }
132
+
133
+ // Context docs
134
+ const contextDocs: string[] = [];
135
+ const ctxMatch = text.match(/##\s+Context to Read First\s*\n+([\s\S]*?)(?=\n##\s|$)/);
136
+ if (ctxMatch) {
137
+ const pathRegex = /`([^\s`]+\.(?:md|yaml|json|go|ts|js))`/g;
138
+ let pm: RegExpExecArray | null;
139
+ while ((pm = pathRegex.exec(ctxMatch[1])) !== null) contextDocs.push(pm[1]);
140
+ }
141
+
142
+ return { taskId, taskName, reviewLevel, size, steps, contextDocs, taskFolder, promptPath };
143
+ }
144
+
145
+ // ── STATUS.md Parsing ────────────────────────────────────────────────
146
+
147
+ /**
148
+ * Parse a STATUS.md file into structured execution state.
149
+ *
150
+ * Pure function — no file I/O. Caller provides content.
151
+ *
152
+ * @param content - Raw STATUS.md content
153
+ * @returns Parsed status with steps, review counter, and iteration
154
+ */
155
+ export function parseStatusMd(content: string): ParsedStatus {
156
+ const text = content.replace(/\r\n/g, "\n");
157
+ const steps: StepInfo[] = [];
158
+ let currentStep: StepInfo | null = null;
159
+ let reviewCounter = 0,
160
+ iteration = 0;
161
+
162
+ for (const line of text.split("\n")) {
163
+ const rcMatch = line.match(/\*\*Review Counter:\*\*\s*(\d+)/);
164
+ if (rcMatch) reviewCounter = parseInt(rcMatch[1]);
165
+ const itMatch = line.match(/\*\*Iteration:\*\*\s*(\d+)/);
166
+ if (itMatch) iteration = parseInt(itMatch[1]);
167
+
168
+ const stepMatch = line.match(/^###\s+Step\s+(\d+):\s*(.+)/);
169
+ if (stepMatch) {
170
+ if (currentStep) {
171
+ currentStep.totalChecked = currentStep.checkboxes.filter((c) => c.checked).length;
172
+ currentStep.totalItems = currentStep.checkboxes.length;
173
+ steps.push(currentStep);
174
+ }
175
+ currentStep = {
176
+ number: parseInt(stepMatch[1]),
177
+ name: stepMatch[2].trim(),
178
+ status: "not-started",
179
+ checkboxes: [],
180
+ totalChecked: 0,
181
+ totalItems: 0,
182
+ };
183
+ continue;
184
+ }
185
+ if (currentStep) {
186
+ const ss = line.match(/\*\*Status:\*\*\s*(.*)/);
187
+ if (ss) {
188
+ const s = ss[1];
189
+ if (s.includes("✅") || s.toLowerCase().includes("complete")) currentStep.status = "complete";
190
+ else if (s.includes("🟨") || s.toLowerCase().includes("progress"))
191
+ currentStep.status = "in-progress";
192
+ }
193
+ const cb = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)/);
194
+ if (cb)
195
+ currentStep.checkboxes.push({ text: cb[2].trim(), checked: cb[1].toLowerCase() === "x" });
196
+ }
197
+ }
198
+ if (currentStep) {
199
+ currentStep.totalChecked = currentStep.checkboxes.filter((c) => c.checked).length;
200
+ currentStep.totalItems = currentStep.checkboxes.length;
201
+ steps.push(currentStep);
202
+ }
203
+ return { steps, reviewCounter, iteration };
204
+ }
205
+
206
+ // ── STATUS.md Generation ─────────────────────────────────────────────
207
+
208
+ /**
209
+ * Generate an initial STATUS.md from a parsed task.
210
+ *
211
+ * @param task - Parsed task (from parsePromptMd or orchestrator ParsedTask)
212
+ * @returns Complete STATUS.md content string
213
+ */
214
+ export function generateStatusMd(task: {
215
+ taskId: string;
216
+ taskName: string;
217
+ reviewLevel: number;
218
+ size: string;
219
+ steps: StepInfo[];
220
+ }): string {
221
+ const now = new Date().toISOString().slice(0, 10);
222
+ const lines: string[] = [
223
+ `# ${task.taskId}: ${task.taskName} — Status`,
224
+ "",
225
+ `**Current Step:** Not Started`,
226
+ `**Status:** 🔵 Ready for Execution`,
227
+ `**Last Updated:** ${now}`,
228
+ `**Review Level:** ${task.reviewLevel}`,
229
+ `**Review Counter:** 0`,
230
+ `**Iteration:** 0`,
231
+ `**Size:** ${task.size}`,
232
+ "",
233
+ "---",
234
+ "",
235
+ ];
236
+ for (const step of task.steps) {
237
+ lines.push(`### Step ${step.number}: ${step.name}`, `**Status:** ⬜ Not Started`, "");
238
+ for (const cb of step.checkboxes) lines.push(`- [ ] ${cb.text}`);
239
+ lines.push("", "---", "");
240
+ }
241
+ lines.push(
242
+ "## Reviews",
243
+ "",
244
+ "| # | Type | Step | Verdict | File |",
245
+ "|---|------|------|---------|------|",
246
+ "",
247
+ "---",
248
+ "",
249
+ "## Discoveries",
250
+ "",
251
+ "| Discovery | Disposition | Location |",
252
+ "|-----------|-------------|----------|",
253
+ "",
254
+ "---",
255
+ "",
256
+ "## Execution Log",
257
+ "",
258
+ "| Timestamp | Action | Outcome |",
259
+ "|-----------|--------|---------|",
260
+ `| ${now} | Task staged | STATUS.md auto-generated by task-runner |`,
261
+ "",
262
+ "---",
263
+ "",
264
+ "## Blockers",
265
+ "",
266
+ "*None*",
267
+ "",
268
+ "---",
269
+ "",
270
+ "## Notes",
271
+ "",
272
+ "*Reserved for execution notes*",
273
+ );
274
+ return lines.join("\n");
275
+ }
276
+
277
+ // ── STATUS.md Mutation ───────────────────────────────────────────────
278
+
279
+ /**
280
+ * Update a metadata field in STATUS.md.
281
+ *
282
+ * Matches `**Field:** value` patterns and replaces the value.
283
+ *
284
+ * @param statusPath - Absolute path to STATUS.md
285
+ * @param field - Field name (e.g., "Status", "Current Step")
286
+ * @param value - New value
287
+ */
288
+ export function updateStatusField(statusPath: string, field: string, value: string): void {
289
+ let content = readFileSync(statusPath, "utf-8").replace(/\r\n/g, "\n");
290
+ const pattern = new RegExp(
291
+ `(\\*\\*${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\*\\*\\s*)(.+)`,
292
+ );
293
+ if (pattern.test(content)) {
294
+ content = content.replace(pattern, `$1${value}`);
295
+ } else {
296
+ content = content.replace(/(\*\*[^*]+:\*\*\s*.+\n)/, `$1**${field}:** ${value}\n`);
297
+ }
298
+ writeFileSync(statusPath, content);
299
+ }
300
+
301
+ /**
302
+ * Update a step's status in STATUS.md.
303
+ *
304
+ * @param statusPath - Absolute path to STATUS.md
305
+ * @param stepNum - Step number to update
306
+ * @param status - New status
307
+ */
308
+ export function updateStepStatus(
309
+ statusPath: string,
310
+ stepNum: number,
311
+ status: "not-started" | "in-progress" | "complete",
312
+ ): void {
313
+ let content = readFileSync(statusPath, "utf-8").replace(/\r\n/g, "\n");
314
+ const emoji =
315
+ status === "complete"
316
+ ? "✅ Complete"
317
+ : status === "in-progress"
318
+ ? "🟨 In Progress"
319
+ : "⬜ Not Started";
320
+ const lines = content.split("\n");
321
+ let inTarget = false;
322
+ for (let i = 0; i < lines.length; i++) {
323
+ const sm = lines[i].match(/^###\s+Step\s+(\d+):/);
324
+ if (sm) inTarget = parseInt(sm[1]) === stepNum;
325
+ if (inTarget && lines[i].match(/^\*\*Status:\*\*/)) {
326
+ lines[i] = `**Status:** ${emoji}`;
327
+ break;
328
+ }
329
+ }
330
+ writeFileSync(statusPath, lines.join("\n"));
331
+ }
332
+
333
+ /**
334
+ * Append a row to a named table section in STATUS.md.
335
+ *
336
+ * @param statusPath - Absolute path to STATUS.md
337
+ * @param sectionName - Section heading (e.g., "Execution Log", "Reviews")
338
+ * @param row - Markdown table row to append
339
+ */
340
+ export function appendTableRow(statusPath: string, sectionName: string, row: string): void {
341
+ let content = readFileSync(statusPath, "utf-8").replace(/\r\n/g, "\n");
342
+ const lines = content.split("\n");
343
+ let insertIdx = -1,
344
+ inSection = false,
345
+ lastTableRow = -1;
346
+ for (let i = 0; i < lines.length; i++) {
347
+ if (lines[i].match(new RegExp(`^##\\s+${sectionName}`))) {
348
+ inSection = true;
349
+ continue;
350
+ }
351
+ if (inSection) {
352
+ if (lines[i].match(/^##\s/) || lines[i].trim() === "---") {
353
+ insertIdx = lastTableRow >= 0 ? lastTableRow + 1 : i;
354
+ break;
355
+ }
356
+ if (lines[i].startsWith("|") && !lines[i].match(/^\|[\s-|]+\|$/)) {
357
+ lastTableRow = i;
358
+ }
359
+ }
360
+ }
361
+ if (insertIdx === -1) {
362
+ insertIdx = lastTableRow >= 0 ? lastTableRow + 1 : lines.length;
363
+ }
364
+ lines.splice(insertIdx, 0, row);
365
+ writeFileSync(statusPath, lines.join("\n"));
366
+ }
367
+
368
+ /**
369
+ * Log an execution event to the Execution Log table in STATUS.md.
370
+ */
371
+ export function logExecution(statusPath: string, action: string, outcome: string): void {
372
+ const ts = new Date().toISOString().slice(0, 16).replace("T", " ");
373
+ appendTableRow(statusPath, "Execution Log", `| ${ts} | ${action} | ${outcome} |`);
374
+ }
375
+
376
+ /**
377
+ * Log a review entry to the Reviews table in STATUS.md.
378
+ */
379
+ export function logReview(
380
+ statusPath: string,
381
+ num: string,
382
+ type: string,
383
+ stepNum: number,
384
+ verdict: string,
385
+ file: string,
386
+ ): void {
387
+ appendTableRow(
388
+ statusPath,
389
+ "Reviews",
390
+ `| ${num} | ${type} | Step ${stepNum} | ${verdict} | ${file} |`,
391
+ );
392
+ }
393
+
394
+ /**
395
+ * Sanitize steering message content for safe injection into a markdown table row.
396
+ * Collapses newlines, escapes pipe characters, and truncates to 200 chars.
397
+ */
398
+ export function sanitizeSteeringContent(content: string): string {
399
+ let s = content.replace(/\r?\n/g, " / ").replace(/\|/g, "\\|");
400
+ if (s.length > 200) s = s.slice(0, 197) + "...";
401
+ return s;
402
+ }
403
+
404
+ // ── Step Completion Logic ────────────────────────────────────────────
405
+
406
+ /**
407
+ * Determine whether a parsed step is complete.
408
+ *
409
+ * A step is complete when its status is explicitly "complete" OR
410
+ * when all checkboxes are checked (with at least one checkbox present).
411
+ *
412
+ * @param step - Parsed step info (or undefined)
413
+ * @returns true if the step should be considered complete
414
+ */
415
+ export function isStepComplete(step: StepInfo | undefined): boolean {
416
+ if (!step) return false;
417
+ if (step.status === "complete") return true;
418
+ return step.totalChecked === step.totalItems && step.totalItems > 0;
419
+ }
420
+
421
+ /**
422
+ * Determine whether a step is "low-risk" and should skip reviews.
423
+ *
424
+ * Low-risk steps: Step 0 (Preflight) and the final step (Delivery/Docs).
425
+ *
426
+ * @param stepNumber - The 0-based step number
427
+ * @param totalSteps - Total number of steps in the task
428
+ * @returns true if the step should skip plan and code reviews
429
+ */
430
+ export function isLowRiskStep(stepNumber: number, totalSteps: number): boolean {
431
+ if (totalSteps <= 0) return false;
432
+ const lastStepIndex = totalSteps - 1;
433
+ return stepNumber === 0 || stepNumber === lastStepIndex;
434
+ }
435
+
436
+ // ── Review Helpers ───────────────────────────────────────────────────
437
+
438
+ /**
439
+ * Extract a review verdict from review file content.
440
+ *
441
+ * Searches for standard verdict patterns (APPROVE, REVISE, RETHINK)
442
+ * with fallback to non-standard formats.
443
+ *
444
+ * @param reviewContent - Raw content of a review output file
445
+ * @returns Uppercase verdict string
446
+ */
447
+ export function extractVerdict(reviewContent: string): string {
448
+ // Primary: standard format "### Verdict: APPROVE|REVISE|RETHINK"
449
+ const match = reviewContent.match(/###?\s*Verdict[:\s]*(APPROVE|REVISE|RETHINK)/i);
450
+ if (match) return match[1].toUpperCase();
451
+
452
+ // Tolerate non-standard verdict formats
453
+ const lower = reviewContent.toLowerCase();
454
+ if (
455
+ lower.includes("changes requested") ||
456
+ lower.includes("request changes") ||
457
+ lower.includes("needs revision")
458
+ )
459
+ return "REVISE";
460
+ if (
461
+ lower.includes("approve") &&
462
+ !lower.includes("do not approve") &&
463
+ !lower.includes("cannot approve")
464
+ )
465
+ return "APPROVE";
466
+ if (lower.includes("rethink") || lower.includes("re-think")) return "RETHINK";
467
+
468
+ return "UNKNOWN";
469
+ }
470
+
471
+ // ── Git Helpers ──────────────────────────────────────────────────────
472
+
473
+ /**
474
+ * Get the current HEAD commit SHA (short form).
475
+ *
476
+ * @returns Short commit SHA or empty string on failure
477
+ */
478
+ export function getHeadCommitSha(): string {
479
+ try {
480
+ const result = spawnSync("git", ["rev-parse", "--short", "HEAD"], {
481
+ encoding: "utf-8",
482
+ timeout: 5000,
483
+ });
484
+ return result.status === 0 ? (result.stdout || "").trim() : "";
485
+ } catch {
486
+ return "";
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Find the git commit SHA where a specific step was completed.
492
+ *
493
+ * Workers commit at step boundaries with messages like:
494
+ * feat(TP-048): complete Step N — description
495
+ *
496
+ * @param stepNumber - Step number to search for
497
+ * @param taskId - Task ID prefix in commit message
498
+ * @param since - Optional base commit to search from
499
+ * @returns Commit SHA if found, or empty string
500
+ */
501
+ export function findStepBoundaryCommit(stepNumber: number, taskId: string, since?: string): string {
502
+ try {
503
+ const args = [
504
+ "log",
505
+ "--oneline",
506
+ "--grep",
507
+ `complete Step ${stepNumber}`,
508
+ "--grep",
509
+ taskId,
510
+ "--all-match",
511
+ "-1",
512
+ "--format=%H",
513
+ ];
514
+ if (since) args.push(`${since}..HEAD`);
515
+ const result = spawnSync("git", args, {
516
+ encoding: "utf-8",
517
+ timeout: 5000,
518
+ });
519
+ return result.status === 0 ? (result.stdout || "").trim() : "";
520
+ } catch {
521
+ return "";
522
+ }
523
+ }
524
+
525
+ // ── Review Request Generation ────────────────────────────────────────
526
+
527
+ /**
528
+ * Standards resolution config shape (minimal, no TaskConfig dependency).
529
+ */
530
+ export interface StandardsConfig {
531
+ docs: string[];
532
+ rules: string[];
533
+ }
534
+
535
+ /**
536
+ * Resolve which standards apply to a task based on its area.
537
+ *
538
+ * @param globalStandards - Project-level standards
539
+ * @param overrides - Per-area overrides keyed by area name
540
+ * @param taskAreas - Task area definitions keyed by area name
541
+ * @param taskFolder - Absolute task folder path
542
+ * @returns Resolved standards for this task
543
+ */
544
+ export function resolveStandards(
545
+ globalStandards: StandardsConfig,
546
+ overrides: Record<string, Partial<StandardsConfig>>,
547
+ taskAreas: Record<string, { path: string; [key: string]: any }>,
548
+ taskFolder: string,
549
+ ): StandardsConfig {
550
+ const normalizedFolder = taskFolder.replace(/\\/g, "/");
551
+ for (const [areaName, areaCfg] of Object.entries(taskAreas)) {
552
+ const areaPath = areaCfg.path.replace(/\\/g, "/");
553
+ if (normalizedFolder.includes(areaPath)) {
554
+ const override = overrides[areaName];
555
+ if (override) {
556
+ return {
557
+ docs: override.docs ?? globalStandards.docs,
558
+ rules: override.rules ?? globalStandards.rules,
559
+ };
560
+ }
561
+ break;
562
+ }
563
+ }
564
+ return { docs: globalStandards.docs, rules: globalStandards.rules };
565
+ }
566
+
567
+ /**
568
+ * Generate a review request document for a plan or code review.
569
+ *
570
+ * @param type - Review type (plan or code)
571
+ * @param stepNum - Step number being reviewed
572
+ * @param stepName - Step name
573
+ * @param taskPromptPath - Path to the task's PROMPT.md
574
+ * @param taskFolder - Path to the task folder
575
+ * @param projectName - Project name from config
576
+ * @param standards - Resolved standards for this task
577
+ * @param outputPath - Path where the reviewer should write output
578
+ * @param stepBaselineCommit - Optional baseline commit for code review diffs
579
+ * @returns Complete review request markdown content
580
+ */
581
+ export function generateReviewRequest(
582
+ type: "plan" | "code",
583
+ stepNum: number,
584
+ stepName: string,
585
+ taskPromptPath: string,
586
+ taskFolder: string,
587
+ projectName: string,
588
+ standards: StandardsConfig,
589
+ outputPath: string,
590
+ stepBaselineCommit?: string,
591
+ ): string {
592
+ const standardsDocs = standards.docs.map((d) => ` - ${d}`).join("\n");
593
+ const standardsRules = standards.rules.map((r) => `- ${r}`).join("\n");
594
+ const statusPath = join(taskFolder, "STATUS.md");
595
+
596
+ if (type === "plan") {
597
+ return [
598
+ `# Review Request: Plan Review`,
599
+ "",
600
+ `You are reviewing an implementation plan for a ${projectName} task.`,
601
+ `You have full tool access — use \`read\` to examine files and \`bash\` to run commands.`,
602
+ "",
603
+ `## Task Context`,
604
+ "",
605
+ `- **Task PROMPT:** ${taskPromptPath}`,
606
+ `- **Task STATUS:** ${statusPath}`,
607
+ `- **Step being planned:** Step ${stepNum}: ${stepName}`,
608
+ "",
609
+ `## Instructions`,
610
+ "",
611
+ `1. Read the PROMPT.md for full requirements`,
612
+ `2. Read STATUS.md for progress so far`,
613
+ `3. Check relevant source files for existing patterns:`,
614
+ standardsDocs,
615
+ "",
616
+ `## Project Standards`,
617
+ "",
618
+ standardsRules,
619
+ "",
620
+ `## Output`,
621
+ "",
622
+ `Write your review to: \`${outputPath}\``,
623
+ ].join("\n");
624
+ }
625
+
626
+ const diffCmd = stepBaselineCommit
627
+ ? `git diff ${stepBaselineCommit}..HEAD --name-only`
628
+ : `git diff --name-only`;
629
+ const diffFullCmd = stepBaselineCommit ? `git diff ${stepBaselineCommit}..HEAD` : `git diff`;
630
+
631
+ return [
632
+ `# Review Request: Code Review`,
633
+ "",
634
+ `You are reviewing code changes for a ${projectName} task.`,
635
+ `You have full tool access — use \`read\` to examine files and \`bash\` to run commands.`,
636
+ "",
637
+ `## Task Context`,
638
+ "",
639
+ `- **Task PROMPT:** ${taskPromptPath}`,
640
+ `- **Task STATUS:** ${statusPath}`,
641
+ `- **Step reviewed:** Step ${stepNum}: ${stepName}`,
642
+ ...(stepBaselineCommit ? [`- **Step baseline commit:** ${stepBaselineCommit}`] : []),
643
+ "",
644
+ `## Instructions`,
645
+ "",
646
+ `1. Run \`${diffCmd}\` to see files changed in this step`,
647
+ ` Then \`${diffFullCmd}\` for the full diff`,
648
+ ` **Important:** The worker commits code via checkpoints, so plain \`git diff\` may show nothing.`,
649
+ ` Always use the baseline commit range above to see all step changes.`,
650
+ `2. Read changed files in full for context`,
651
+ `3. Check neighboring files for pattern consistency`,
652
+ `4. Check standards:`,
653
+ standardsDocs,
654
+ "",
655
+ `## Project Standards`,
656
+ "",
657
+ standardsRules,
658
+ "",
659
+ `## Output`,
660
+ "",
661
+ `Write your review to: \`${outputPath}\``,
662
+ ].join("\n");
663
+ }
664
+
665
+ // ── Display Helpers ──────────────────────────────────────────────────
666
+
667
+ /**
668
+ * Convert a kebab-case name to Title Case for display.
669
+ */
670
+ export function displayName(name: string): string {
671
+ return name
672
+ .split("-")
673
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
674
+ .join(" ");
675
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Migration-only helpers for legacy TMUX-shaped persisted lane fields.
3
+ *
4
+ * Runtime V2 no longer accepts TMUX config/runtime contracts. The only
5
+ * compatibility retained here is one-release state ingress normalization for
6
+ * `lanes[].tmuxSessionName` → `lanes[].laneSessionId`.
7
+ */
8
+
9
+ export interface LaneSessionAliasTarget {
10
+ laneSessionId?: unknown;
11
+ tmuxSessionName?: unknown;
12
+ }
13
+
14
+ /**
15
+ * Read canonical + legacy lane session fields from a lane-like record.
16
+ */
17
+ export function readLaneSessionAliases(target: LaneSessionAliasTarget): {
18
+ laneSessionId: unknown;
19
+ tmuxSessionName: unknown;
20
+ } {
21
+ return {
22
+ laneSessionId: target.laneSessionId,
23
+ tmuxSessionName: target.tmuxSessionName,
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Normalize tmuxSessionName -> laneSessionId in place and remove legacy key.
29
+ */
30
+ export function normalizeLaneSessionAlias(target: LaneSessionAliasTarget): void {
31
+ if (typeof target.laneSessionId !== "string" && typeof target.tmuxSessionName === "string") {
32
+ target.laneSessionId = target.tmuxSessionName;
33
+ }
34
+ if ("tmuxSessionName" in target) {
35
+ delete target.tmuxSessionName;
36
+ }
37
+ }