@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.
- package/CHANGELOG.md +41 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/agents/AGENTS-MANIFEST.md +42 -0
- package/agents/brain.md +42 -0
- package/agents/context-builder.md +46 -0
- package/agents/delegate.md +12 -0
- package/agents/dev-1.md +42 -0
- package/agents/oracle.md +73 -0
- package/agents/planner.md +55 -0
- package/agents/researcher.md +52 -0
- package/agents/reviewer.md +79 -0
- package/agents/scout.md +50 -0
- package/agents/tester.md +45 -0
- package/agents/worker.md +55 -0
- package/extensions/ralph.ts +1 -0
- package/extensions/reviewer-extension.ts +125 -0
- package/extensions/task-orchestrator.ts +28 -0
- package/package.json +63 -0
- package/prompts/gather-context-and-clarify.md +13 -0
- package/prompts/parallel-cleanup.md +59 -0
- package/prompts/parallel-context-build.md +53 -0
- package/prompts/parallel-handoff-plan.md +59 -0
- package/prompts/parallel-research.md +50 -0
- package/prompts/parallel-review.md +54 -0
- package/prompts/review-loop.md +41 -0
- package/skills/orchid/SKILL.md +214 -0
- package/skills/orchid/orchid-cleanup/SKILL.md +122 -0
- package/skills/orchid/orchid-converge/SKILL.md +124 -0
- package/skills/orchid/orchid-decompose/SKILL.md +201 -0
- package/skills/orchid/orchid-doctor/SKILL.md +162 -0
- package/skills/orchid/orchid-investigate/SKILL.md +102 -0
- package/skills/orchid/orchid-launch/SKILL.md +147 -0
- package/skills/ralph/SKILL.md +73 -0
- package/skills/subagents/pi-subagents/SKILL.md +813 -0
- package/src/index.ts +7 -0
- package/src/orchestrator/abort.ts +534 -0
- package/src/orchestrator/agent-bridge-extension.ts +1020 -0
- package/src/orchestrator/agent-host.ts +954 -0
- package/src/orchestrator/cleanup.ts +776 -0
- package/src/orchestrator/config-loader.ts +1412 -0
- package/src/orchestrator/config-schema.ts +690 -0
- package/src/orchestrator/config.ts +81 -0
- package/src/orchestrator/context-window.ts +66 -0
- package/src/orchestrator/diagnostic-reports.ts +475 -0
- package/src/orchestrator/diagnostics.ts +394 -0
- package/src/orchestrator/discovery.ts +1833 -0
- package/src/orchestrator/engine-worker.ts +415 -0
- package/src/orchestrator/engine.ts +5940 -0
- package/src/orchestrator/execution.ts +3104 -0
- package/src/orchestrator/extension.ts +5934 -0
- package/src/orchestrator/formatting.ts +785 -0
- package/src/orchestrator/git.ts +88 -0
- package/src/orchestrator/index.ts +28 -0
- package/src/orchestrator/lane-runner.ts +1787 -0
- package/src/orchestrator/mailbox.ts +780 -0
- package/src/orchestrator/merge.ts +3414 -0
- package/src/orchestrator/messages.ts +1062 -0
- package/src/orchestrator/migrations.ts +278 -0
- package/src/orchestrator/naming.ts +117 -0
- package/src/orchestrator/path-resolver.ts +275 -0
- package/src/orchestrator/persistence.ts +2625 -0
- package/src/orchestrator/process-registry.ts +452 -0
- package/src/orchestrator/quality-gate.ts +1085 -0
- package/src/orchestrator/resume.ts +3488 -0
- package/src/orchestrator/sessions.ts +57 -0
- package/src/orchestrator/settings-loader.ts +136 -0
- package/src/orchestrator/settings-tui.ts +2208 -0
- package/src/orchestrator/sidecar-telemetry.ts +267 -0
- package/src/orchestrator/supervisor.ts +4548 -0
- package/src/orchestrator/task-executor-core.ts +675 -0
- package/src/orchestrator/tmux-compat.ts +37 -0
- package/src/orchestrator/tool-allowlist-constants.ts +37 -0
- package/src/orchestrator/types.ts +4465 -0
- package/src/orchestrator/verification.ts +547 -0
- package/src/orchestrator/waves.ts +1564 -0
- package/src/orchestrator/workspace.ts +707 -0
- package/src/orchestrator/worktree.ts +2725 -0
- package/src/ralph/index.ts +825 -0
- package/src/subagents/agents/agent-management.ts +648 -0
- package/src/subagents/agents/agent-scope.ts +6 -0
- package/src/subagents/agents/agent-selection.ts +23 -0
- package/src/subagents/agents/agent-serializer.ts +86 -0
- package/src/subagents/agents/agents.ts +832 -0
- package/src/subagents/agents/chain-serializer.ts +137 -0
- package/src/subagents/agents/frontmatter.ts +29 -0
- package/src/subagents/agents/identity.ts +30 -0
- package/src/subagents/agents/skills.ts +632 -0
- package/src/subagents/extension/config.ts +16 -0
- package/src/subagents/extension/control-notices.ts +92 -0
- package/src/subagents/extension/doctor.ts +199 -0
- package/src/subagents/extension/fanout-child.ts +170 -0
- package/src/subagents/extension/index.ts +573 -0
- package/src/subagents/extension/schemas.ts +168 -0
- package/src/subagents/intercom/intercom-bridge.ts +379 -0
- package/src/subagents/intercom/result-intercom.ts +377 -0
- package/src/subagents/runs/background/async-execution.ts +712 -0
- package/src/subagents/runs/background/async-job-tracker.ts +310 -0
- package/src/subagents/runs/background/async-resume.ts +345 -0
- package/src/subagents/runs/background/async-status.ts +325 -0
- package/src/subagents/runs/background/completion-dedupe.ts +63 -0
- package/src/subagents/runs/background/notify.ts +108 -0
- package/src/subagents/runs/background/parallel-groups.ts +45 -0
- package/src/subagents/runs/background/result-watcher.ts +307 -0
- package/src/subagents/runs/background/run-id-resolver.ts +83 -0
- package/src/subagents/runs/background/run-status.ts +269 -0
- package/src/subagents/runs/background/stale-run-reconciler.ts +336 -0
- package/src/subagents/runs/background/subagent-runner.ts +1808 -0
- package/src/subagents/runs/background/top-level-async.ts +13 -0
- package/src/subagents/runs/foreground/chain-clarify.ts +1333 -0
- package/src/subagents/runs/foreground/chain-execution.ts +938 -0
- package/src/subagents/runs/foreground/execution.ts +918 -0
- package/src/subagents/runs/foreground/subagent-executor.ts +2527 -0
- package/src/subagents/runs/shared/completion-guard.ts +147 -0
- package/src/subagents/runs/shared/long-running-guard.ts +175 -0
- package/src/subagents/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
- package/src/subagents/runs/shared/model-fallback.ts +103 -0
- package/src/subagents/runs/shared/nested-events.ts +819 -0
- package/src/subagents/runs/shared/nested-path.ts +52 -0
- package/src/subagents/runs/shared/nested-render.ts +115 -0
- package/src/subagents/runs/shared/parallel-utils.ts +109 -0
- package/src/subagents/runs/shared/pi-args.ts +220 -0
- package/src/subagents/runs/shared/pi-spawn.ts +115 -0
- package/src/subagents/runs/shared/run-history.ts +60 -0
- package/src/subagents/runs/shared/single-output.ts +164 -0
- package/src/subagents/runs/shared/subagent-control.ts +226 -0
- package/src/subagents/runs/shared/subagent-prompt-runtime.ts +170 -0
- package/src/subagents/runs/shared/worktree.ts +577 -0
- package/src/subagents/shared/artifacts.ts +98 -0
- package/src/subagents/shared/atomic-json.ts +16 -0
- package/src/subagents/shared/file-coalescer.ts +40 -0
- package/src/subagents/shared/fork-context.ts +76 -0
- package/src/subagents/shared/formatters.ts +133 -0
- package/src/subagents/shared/jsonl-writer.ts +81 -0
- package/src/subagents/shared/model-info.ts +78 -0
- package/src/subagents/shared/post-exit-stdio-guard.ts +85 -0
- package/src/subagents/shared/session-identity.ts +10 -0
- package/src/subagents/shared/session-tokens.ts +44 -0
- package/src/subagents/shared/settings.ts +397 -0
- package/src/subagents/shared/status-format.ts +49 -0
- package/src/subagents/shared/types.ts +822 -0
- package/src/subagents/shared/utils.ts +450 -0
- package/src/subagents/slash/prompt-template-bridge.ts +397 -0
- package/src/subagents/slash/slash-bridge.ts +174 -0
- package/src/subagents/slash/slash-commands.ts +528 -0
- package/src/subagents/slash/slash-live-state.ts +292 -0
- package/src/subagents/tui/render-helpers.ts +80 -0
- package/src/subagents/tui/render.ts +1358 -0
- package/templates/agents/local/supervisor.md +33 -0
- package/templates/agents/local/task-merger.md +27 -0
- package/templates/agents/local/task-reviewer.md +30 -0
- package/templates/agents/local/task-worker.md +34 -0
- package/templates/agents/supervisor-routing.md +92 -0
- package/templates/agents/supervisor.md +229 -0
- package/templates/agents/task-merger.md +214 -0
- package/templates/agents/task-reviewer.md +260 -0
- package/templates/agents/task-worker-segment.md +44 -0
- package/templates/agents/task-worker.md +557 -0
- package/templates/tasks/CONTEXT.md +30 -0
- package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -0
- package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
- package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -0
- 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
|
+
}
|