taskplane 0.22.17 → 0.23.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.
@@ -38,6 +38,28 @@ import {
38
38
  } from "./taskplane/types.ts";
39
39
  import { classifyExit } from "./taskplane/diagnostics.ts";
40
40
  import type { TaskExitDiagnostic, ExitSummary } from "./taskplane/diagnostics.ts";
41
+ import {
42
+ parsePromptMd as coreParsePromptMd,
43
+ parseStatusMd as coreParseStatusMd,
44
+ generateStatusMd as coreGenerateStatusMd,
45
+ updateStatusField as coreUpdateStatusField,
46
+ updateStepStatus as coreUpdateStepStatus,
47
+ appendTableRow as coreAppendTableRow,
48
+ logExecution as coreLogExecution,
49
+ logReview as coreLogReview,
50
+ sanitizeSteeringContent as coreSanitizeSteeringContent,
51
+ isStepComplete as coreIsStepComplete,
52
+ isLowRiskStep as coreIsLowRiskStep,
53
+ extractVerdict as coreExtractVerdict,
54
+ getHeadCommitSha as coreGetHeadCommitSha,
55
+ findStepBoundaryCommit as coreFindStepBoundaryCommit,
56
+ resolveStandards as coreResolveStandards,
57
+ generateReviewRequest as coreGenerateReviewRequest,
58
+ displayName as coreDisplayName,
59
+ type StepInfo,
60
+ type CoreParsedTask,
61
+ type ParsedStatus,
62
+ } from "./taskplane/task-executor-core.ts";
41
63
  import {
42
64
  generateQualityGatePrompt,
43
65
  generateFeedbackMd,
@@ -782,194 +804,38 @@ function loadAgentDef(cwd: string, name: string): { systemPrompt: string; tools:
782
804
  // ── PROMPT.md Parser ─────────────────────────────────────────────────
783
805
 
784
806
  function parsePromptMd(content: string, promptPath: string): ParsedTask {
785
- const text = content.replace(/\r\n/g, "\n");
786
- const taskFolder = dirname(resolve(promptPath));
787
-
788
- // Task ID and name
789
- let taskId = "", taskName = "";
790
- const titleMatch = text.match(/^#\s+(?:Task:\s*)?(\S+-\d+)\s*[-–:]\s*(.+)/m);
791
- if (titleMatch) { taskId = titleMatch[1]; taskName = titleMatch[2].trim(); }
792
- else { taskId = basename(taskFolder); taskName = taskId; }
793
-
794
- // Review level
795
- let reviewLevel = 0;
796
- const rlMatch = text.match(/##\s+Review Level[:\s]*(\d)/);
797
- if (rlMatch) reviewLevel = parseInt(rlMatch[1]);
798
-
799
- // Size
800
- let size = "M";
801
- const sizeMatch = text.match(/\*\*Size:\*\*\s*(\w+)/);
802
- if (sizeMatch) size = sizeMatch[1];
803
-
804
- // Steps
805
- const steps: StepInfo[] = [];
806
- const stepRegex = /###\s+Step\s+(\d+):\s*(.+)/g;
807
- const positions: { number: number; name: string; start: number }[] = [];
808
- let m;
809
- while ((m = stepRegex.exec(text)) !== null) {
810
- positions.push({ number: parseInt(m[1]), name: m[2].trim(), start: m.index });
811
- }
812
- for (let i = 0; i < positions.length; i++) {
813
- const section = text.slice(positions[i].start, i + 1 < positions.length ? positions[i + 1].start : text.length);
814
- const checkboxes: { text: string; checked: boolean }[] = [];
815
- const cbRegex = /^\s*-\s*\[([ xX])\]\s*(.*)/gm;
816
- let cb;
817
- while ((cb = cbRegex.exec(section)) !== null) {
818
- checkboxes.push({ text: cb[2].trim(), checked: cb[1].toLowerCase() === "x" });
819
- }
820
- steps.push({
821
- number: positions[i].number, name: positions[i].name,
822
- status: "not-started", checkboxes,
823
- totalChecked: checkboxes.filter(c => c.checked).length,
824
- totalItems: checkboxes.length,
825
- });
826
- }
827
-
828
- // Context docs
829
- const contextDocs: string[] = [];
830
- const ctxMatch = text.match(/##\s+Context to Read First\s*\n+([\s\S]*?)(?=\n##\s|$)/);
831
- if (ctxMatch) {
832
- const pathRegex = /`([^\s`]+\.(?:md|yaml|json|go|ts|js))`/g;
833
- let pm;
834
- while ((pm = pathRegex.exec(ctxMatch[1])) !== null) contextDocs.push(pm[1]);
835
- }
836
-
837
- return { taskId, taskName, reviewLevel, size, steps, contextDocs, taskFolder, promptPath };
807
+ const core = coreParsePromptMd(content, promptPath);
808
+ return { ...core };
838
809
  }
839
810
 
840
811
  // ── STATUS.md Parser ─────────────────────────────────────────────────
841
812
 
842
813
  function parseStatusMd(content: string): { steps: StepInfo[]; reviewCounter: number; iteration: number } {
843
- const text = content.replace(/\r\n/g, "\n");
844
- const steps: StepInfo[] = [];
845
- let currentStep: StepInfo | null = null;
846
- let reviewCounter = 0, iteration = 0;
847
-
848
- for (const line of text.split("\n")) {
849
- const rcMatch = line.match(/\*\*Review Counter:\*\*\s*(\d+)/);
850
- if (rcMatch) reviewCounter = parseInt(rcMatch[1]);
851
- const itMatch = line.match(/\*\*Iteration:\*\*\s*(\d+)/);
852
- if (itMatch) iteration = parseInt(itMatch[1]);
853
-
854
- const stepMatch = line.match(/^###\s+Step\s+(\d+):\s*(.+)/);
855
- if (stepMatch) {
856
- if (currentStep) {
857
- currentStep.totalChecked = currentStep.checkboxes.filter(c => c.checked).length;
858
- currentStep.totalItems = currentStep.checkboxes.length;
859
- steps.push(currentStep);
860
- }
861
- currentStep = { number: parseInt(stepMatch[1]), name: stepMatch[2].trim(), status: "not-started", checkboxes: [], totalChecked: 0, totalItems: 0 };
862
- continue;
863
- }
864
- if (currentStep) {
865
- const ss = line.match(/\*\*Status:\*\*\s*(.*)/);
866
- if (ss) {
867
- const s = ss[1];
868
- if (s.includes("✅") || s.toLowerCase().includes("complete")) currentStep.status = "complete";
869
- else if (s.includes("🟨") || s.toLowerCase().includes("progress")) currentStep.status = "in-progress";
870
- }
871
- const cb = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)/);
872
- if (cb) currentStep.checkboxes.push({ text: cb[2].trim(), checked: cb[1].toLowerCase() === "x" });
873
- }
874
- }
875
- if (currentStep) {
876
- currentStep.totalChecked = currentStep.checkboxes.filter(c => c.checked).length;
877
- currentStep.totalItems = currentStep.checkboxes.length;
878
- steps.push(currentStep);
879
- }
880
- return { steps, reviewCounter, iteration };
814
+ return coreParseStatusMd(content);
881
815
  }
882
816
 
883
817
  // ── STATUS.md Generator ──────────────────────────────────────────────
884
818
 
885
819
  function generateStatusMd(task: ParsedTask): string {
886
- const now = new Date().toISOString().slice(0, 10);
887
- const lines: string[] = [
888
- `# ${task.taskId}: ${task.taskName} — Status`, "",
889
- `**Current Step:** Not Started`,
890
- `**Status:** 🔵 Ready for Execution`,
891
- `**Last Updated:** ${now}`,
892
- `**Review Level:** ${task.reviewLevel}`,
893
- `**Review Counter:** 0`,
894
- `**Iteration:** 0`,
895
- `**Size:** ${task.size}`, "", "---", "",
896
- ];
897
- for (const step of task.steps) {
898
- lines.push(`### Step ${step.number}: ${step.name}`, `**Status:** ⬜ Not Started`, "");
899
- for (const cb of step.checkboxes) lines.push(`- [ ] ${cb.text}`);
900
- lines.push("", "---", "");
901
- }
902
- lines.push(
903
- "## Reviews", "", "| # | Type | Step | Verdict | File |", "|---|------|------|---------|------|", "", "---", "",
904
- "## Discoveries", "", "| Discovery | Disposition | Location |", "|-----------|-------------|----------|", "", "---", "",
905
- "## Execution Log", "", "| Timestamp | Action | Outcome |", "|-----------|--------|---------|",
906
- `| ${now} | Task staged | STATUS.md auto-generated by task-runner |`, "", "---", "",
907
- "## Blockers", "", "*None*", "", "---", "", "## Notes", "", "*Reserved for execution notes*",
908
- );
909
- return lines.join("\n");
820
+ return coreGenerateStatusMd(task);
910
821
  }
911
822
 
912
823
  // ── STATUS.md Updaters ───────────────────────────────────────────────
913
824
 
914
825
  function updateStatusField(statusPath: string, field: string, value: string): void {
915
- let content = readFileSync(statusPath, "utf-8").replace(/\r\n/g, "\n");
916
- const pattern = new RegExp(`(\\*\\*${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\*\\*\\s*)(.+)`);
917
- if (pattern.test(content)) {
918
- content = content.replace(pattern, `$1${value}`);
919
- } else {
920
- // Append after last ** field
921
- content = content.replace(/(\*\*[^*]+:\*\*\s*.+\n)/, `$1**${field}:** ${value}\n`);
922
- }
923
- writeFileSync(statusPath, content);
826
+ coreUpdateStatusField(statusPath, field, value);
924
827
  }
925
828
 
926
829
  function updateStepStatus(statusPath: string, stepNum: number, status: "not-started" | "in-progress" | "complete"): void {
927
- let content = readFileSync(statusPath, "utf-8").replace(/\r\n/g, "\n");
928
- const emoji = status === "complete" ? "✅ Complete" : status === "in-progress" ? "🟨 In Progress" : "⬜ Not Started";
929
- const lines = content.split("\n");
930
- let inTarget = false;
931
- for (let i = 0; i < lines.length; i++) {
932
- const sm = lines[i].match(/^###\s+Step\s+(\d+):/);
933
- if (sm) inTarget = parseInt(sm[1]) === stepNum;
934
- if (inTarget && lines[i].match(/^\*\*Status:\*\*/)) {
935
- lines[i] = `**Status:** ${emoji}`;
936
- break;
937
- }
938
- }
939
- writeFileSync(statusPath, lines.join("\n"));
830
+ coreUpdateStepStatus(statusPath, stepNum, status);
940
831
  }
941
832
 
942
833
  function appendTableRow(statusPath: string, sectionName: string, row: string): void {
943
- let content = readFileSync(statusPath, "utf-8").replace(/\r\n/g, "\n");
944
- const lines = content.split("\n");
945
- let insertIdx = -1, inSection = false, lastTableRow = -1;
946
- for (let i = 0; i < lines.length; i++) {
947
- if (lines[i].match(new RegExp(`^##\\s+${sectionName}`))) {
948
- inSection = true;
949
- continue;
950
- }
951
- if (inSection) {
952
- // End of section — hit another ## heading or ---
953
- if (lines[i].match(/^##\s/) || lines[i].trim() === "---") {
954
- insertIdx = lastTableRow >= 0 ? lastTableRow + 1 : i;
955
- break;
956
- }
957
- // Track last table data row (skip header separator |---|)
958
- if (lines[i].startsWith("|") && !lines[i].match(/^\|[\s-|]+\|$/)) {
959
- lastTableRow = i;
960
- }
961
- }
962
- }
963
- if (insertIdx === -1) {
964
- insertIdx = lastTableRow >= 0 ? lastTableRow + 1 : lines.length;
965
- }
966
- lines.splice(insertIdx, 0, row);
967
- writeFileSync(statusPath, lines.join("\n"));
834
+ coreAppendTableRow(statusPath, sectionName, row);
968
835
  }
969
836
 
970
837
  function logExecution(statusPath: string, action: string, outcome: string): void {
971
- const ts = new Date().toISOString().slice(0, 16).replace("T", " ");
972
- appendTableRow(statusPath, "Execution Log", `| ${ts} | ${action} | ${outcome} |`);
838
+ coreLogExecution(statusPath, action, outcome);
973
839
  }
974
840
 
975
841
  /**
@@ -977,13 +843,11 @@ function logExecution(statusPath: string, action: string, outcome: string): void
977
843
  * Collapses newlines to " / ", escapes pipe characters, and truncates to 200 chars.
978
844
  */
979
845
  function sanitizeSteeringContent(content: string): string {
980
- let s = content.replace(/\r?\n/g, " / ").replace(/\|/g, "\\|");
981
- if (s.length > 200) s = s.slice(0, 197) + "...";
982
- return s;
846
+ return coreSanitizeSteeringContent(content);
983
847
  }
984
848
 
985
849
  function logReview(statusPath: string, num: string, type: string, stepNum: number, verdict: string, file: string): void {
986
- appendTableRow(statusPath, "Reviews", `| ${num} | ${type} | Step ${stepNum} | ${verdict} | ${file} |`);
850
+ coreLogReview(statusPath, num, type, stepNum, verdict, file);
987
851
  }
988
852
 
989
853
  // ── Project Context Builder ──────────────────────────────────────────
@@ -1020,15 +884,7 @@ function buildProjectContext(config: TaskConfig, taskFolder: string): string {
1020
884
  * can diff against the correct range instead of just uncommitted changes.
1021
885
  */
1022
886
  function getHeadCommitSha(): string {
1023
- try {
1024
- const result = spawnSync("git", ["rev-parse", "--short", "HEAD"], {
1025
- encoding: "utf-8",
1026
- timeout: 5000,
1027
- });
1028
- return result.status === 0 ? (result.stdout || "").trim() : "";
1029
- } catch {
1030
- return "";
1031
- }
887
+ return coreGetHeadCommitSha();
1032
888
  }
1033
889
 
1034
890
  /**
@@ -1038,18 +894,7 @@ function getHeadCommitSha(): string {
1038
894
  * Returns the commit SHA if found, or empty string.
1039
895
  */
1040
896
  function findStepBoundaryCommit(stepNumber: number, taskId: string, since?: string): string {
1041
- try {
1042
- // Search git log for the step completion commit
1043
- const args = ["log", "--oneline", "--grep", `complete Step ${stepNumber}`, "--grep", taskId, "--all-match", "-1", "--format=%H"];
1044
- if (since) args.push(`${since}..HEAD`);
1045
- const result = spawnSync("git", args, {
1046
- encoding: "utf-8",
1047
- timeout: 5000,
1048
- });
1049
- return result.status === 0 ? (result.stdout || "").trim() : "";
1050
- } catch {
1051
- return "";
1052
- }
897
+ return coreFindStepBoundaryCommit(stepNumber, taskId, since);
1053
898
  }
1054
899
 
1055
900
  // ── Standards Resolution ─────────────────────────────────────────────
@@ -1065,24 +910,7 @@ function findStepBoundaryCommit(stepNumber: number, taskId: string, since?: stri
1065
910
  * different review standards than Go backend service tasks.
1066
911
  */
1067
912
  function resolveStandards(config: TaskConfig, taskFolder: string): { docs: string[]; rules: string[] } {
1068
- const normalizedFolder = taskFolder.replace(/\\/g, "/");
1069
-
1070
- // Find which area this task belongs to
1071
- for (const [areaName, areaCfg] of Object.entries(config.task_areas)) {
1072
- const areaPath = areaCfg.path.replace(/\\/g, "/");
1073
- if (normalizedFolder.includes(areaPath)) {
1074
- const override = config.standards_overrides[areaName];
1075
- if (override) {
1076
- return {
1077
- docs: override.docs ?? config.standards.docs,
1078
- rules: override.rules ?? config.standards.rules,
1079
- };
1080
- }
1081
- break; // Area found but no override — use global
1082
- }
1083
- }
1084
-
1085
- return { docs: config.standards.docs, rules: config.standards.rules };
913
+ return coreResolveStandards(config.standards, config.standards_overrides, config.task_areas, taskFolder);
1086
914
  }
1087
915
 
1088
916
  // ── Review Request Generator ─────────────────────────────────────────
@@ -1092,84 +920,12 @@ function generateReviewRequest(
1092
920
  task: ParsedTask, config: TaskConfig, outputPath: string,
1093
921
  stepBaselineCommit?: string,
1094
922
  ): string {
1095
- const resolved = resolveStandards(config, task.taskFolder);
1096
- const standardsDocs = resolved.docs.map(d => ` - ${d}`).join("\n");
1097
- const standardsRules = resolved.rules.map(r => `- ${r}`).join("\n");
1098
-
1099
- if (type === "plan") {
1100
- return [
1101
- `# Review Request: Plan Review`, "",
1102
- `You are reviewing an implementation plan for a ${config.project.name} task.`,
1103
- `You have full tool access — use \`read\` to examine files and \`bash\` to run commands.`, "",
1104
- `## Task Context`, "",
1105
- `- **Task PROMPT:** ${task.promptPath}`,
1106
- `- **Task STATUS:** ${join(task.taskFolder, "STATUS.md")}`,
1107
- `- **Step being planned:** Step ${stepNum}: ${stepName}`, "",
1108
- `## Instructions`, "",
1109
- `1. Read the PROMPT.md for full requirements`,
1110
- `2. Read STATUS.md for progress so far`,
1111
- `3. Check relevant source files for existing patterns:`,
1112
- standardsDocs, "",
1113
- `## Project Standards`, "", standardsRules, "",
1114
- `## Output`, "",
1115
- `Write your review to: \`${outputPath}\``,
1116
- ].join("\n");
1117
- } else {
1118
- // For code reviews, provide the baseline commit so the reviewer can
1119
- // diff the full step's changes — not just uncommitted changes.
1120
- // Workers commit via checkpoints, so `git diff` alone sees nothing.
1121
- const diffCmd = stepBaselineCommit
1122
- ? `git diff ${stepBaselineCommit}..HEAD --name-only`
1123
- : `git diff --name-only`;
1124
- const diffFullCmd = stepBaselineCommit
1125
- ? `git diff ${stepBaselineCommit}..HEAD`
1126
- : `git diff`;
1127
-
1128
- return [
1129
- `# Review Request: Code Review`, "",
1130
- `You are reviewing code changes for a ${config.project.name} task.`,
1131
- `You have full tool access — use \`read\` to examine files and \`bash\` to run commands.`, "",
1132
- `## Task Context`, "",
1133
- `- **Task PROMPT:** ${task.promptPath}`,
1134
- `- **Task STATUS:** ${join(task.taskFolder, "STATUS.md")}`,
1135
- `- **Step reviewed:** Step ${stepNum}: ${stepName}`,
1136
- ...(stepBaselineCommit ? [`- **Step baseline commit:** ${stepBaselineCommit}`] : []),
1137
- "",
1138
- `## Instructions`, "",
1139
- `1. Run \`${diffCmd}\` to see files changed in this step`,
1140
- ` Then \`${diffFullCmd}\` for the full diff`,
1141
- ` **Important:** The worker commits code via checkpoints, so plain \`git diff\` may show nothing.`,
1142
- ` Always use the baseline commit range above to see all step changes.`,
1143
- `2. Read changed files in full for context`,
1144
- `3. Check neighboring files for pattern consistency`,
1145
- `4. Check standards:`,
1146
- standardsDocs, "",
1147
- `## Project Standards`, "", standardsRules, "",
1148
- `## Output`, "",
1149
- `Write your review to: \`${outputPath}\``,
1150
- ].join("\n");
1151
- }
923
+ const standards = resolveStandards(config, task.taskFolder);
924
+ return coreGenerateReviewRequest(type, stepNum, stepName, task.promptPath, task.taskFolder, config.project.name, standards, outputPath, stepBaselineCommit);
1152
925
  }
1153
926
 
1154
927
  function extractVerdict(reviewContent: string): string {
1155
- // Primary: standard format "### Verdict: APPROVE|REVISE|RETHINK"
1156
- const match = reviewContent.match(/###?\s*Verdict[:\s]*(APPROVE|REVISE|RETHINK)/i);
1157
- if (match) return match[1].toUpperCase();
1158
-
1159
- // TP-068: Tolerate non-standard verdict formats from models that don't
1160
- // follow the exact template (e.g., "Changes requested", "Needs revision").
1161
- const lower = reviewContent.toLowerCase();
1162
- if (/\b(request\s+changes?|changes?\s+requested|needs?\s+revision|please\s+revise|must\s+revise)\b/.test(lower)) {
1163
- return "REVISE";
1164
- }
1165
- if (/\b(looks?\s+good|no\s+issues?\s+found|approved?)\b/.test(lower)) {
1166
- return "APPROVE";
1167
- }
1168
- if (/\b(fundamentally\s+wrong|rethink|reconsider\s+the\s+approach)\b/.test(lower)) {
1169
- return "RETHINK";
1170
- }
1171
-
1172
- return "UNKNOWN";
928
+ return coreExtractVerdict(reviewContent);
1173
929
  }
1174
930
 
1175
931
  /**
@@ -1756,9 +1512,7 @@ export type { BuildExitDiagnosticInput };
1756
1512
  * @returns true if the step should skip plan and code reviews
1757
1513
  */
1758
1514
  export function isLowRiskStep(stepNumber: number, totalSteps: number): boolean {
1759
- if (totalSteps <= 0) return false;
1760
- const lastStepIndex = totalSteps - 1;
1761
- return stepNumber === 0 || stepNumber === lastStepIndex;
1515
+ return coreIsLowRiskStep(stepNumber, totalSteps);
1762
1516
  }
1763
1517
 
1764
1518
  // ── TMUX Agent Spawner ───────────────────────────────────────────────
@@ -2255,7 +2009,7 @@ export const _cleanupOrphanProcesses = cleanupOrphanProcesses;
2255
2009
  // ── Display Helpers ──────────────────────────────────────────────────
2256
2010
 
2257
2011
  function displayName(name: string): string {
2258
- return name.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2012
+ return coreDisplayName(name);
2259
2013
  }
2260
2014
 
2261
2015
  // ── Extension ────────────────────────────────────────────────────────
@@ -3519,80 +3273,41 @@ export default function (pi: ExtensionAPI) {
3519
3273
  || workerDef?.model
3520
3274
  || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514"));
3521
3275
 
3522
- const contextDocsList = task.contextDocs.length > 0
3523
- ? "\n\nContext docs to read if needed:\n" + task.contextDocs.map(d => `- ${d}`).join("\n")
3524
- : "";
3276
+ // ── Lean worker prompt: pass file paths, not content ──────────
3277
+ // The worker reads PROMPT.md and STATUS.md itself using the read tool.
3278
+ // This keeps the initial prompt small (~500 chars) instead of embedding
3279
+ // 50K+ of compiled content that exceeds Windows command line limits
3280
+ // and wastes initial context window capacity.
3281
+ const promptLines = [
3282
+ `Read your task instructions at: ${task.promptPath}`,
3283
+ `Read your execution state at: ${statusPath}`,
3284
+ ``,
3285
+ `Task: ${task.taskId}`,
3286
+ `Task folder: ${task.taskFolder}/`,
3287
+ `Iteration: ${state.totalIterations}`,
3288
+ `Wrap-up signal file: ${wrapUpFile}`,
3289
+ ];
3525
3290
 
3526
- // When running under the parallel orchestrator, workers must NOT
3527
- // archive or move the task folder the orchestrator polls for .DONE
3528
- // at the original path and handles post-merge archival itself.
3529
- const archiveSuppression = isOrchestratedMode()
3530
- ? "\n\n⚠️ ORCHESTRATED RUN: Do NOT archive or move the task folder. " +
3531
- "Do NOT rename, relocate, or reorganize the task folder path. " +
3532
- "The orchestrator handles post-merge archival. " +
3533
- "Just create the .DONE file in the task folder when complete."
3534
- : "";
3291
+ if (isOrchestratedMode()) {
3292
+ promptLines.push(``, `⚠️ ORCHESTRATED RUN: Do NOT archive or move the task folder. The orchestrator handles post-merge archival.`);
3293
+ }
3535
3294
 
3536
- // Build step listing for the worker prompt — show ALL steps with status
3537
- const remainingSet = new Set(remainingSteps.map(s => s.number));
3538
- const stepListing = task.steps.map(s =>
3539
- remainingSet.has(s.number)
3540
- ? ` - Step ${s.number}: ${s.name}`
3541
- : ` - Step ${s.number}: ${s.name} [already complete — skip]`
3542
- ).join("\n");
3543
-
3544
- // TP-073: Build nudge for subsequent iterations (iter > 0)
3545
- // When the worker exited without completing all steps, the next iteration
3546
- // gets an explicit nudge listing completed/remaining steps and a warning
3547
- // not to exit prematurely again.
3548
- let iterationNudge = "";
3549
3295
  if (state.totalIterations > 1 && remainingSteps.length > 0) {
3296
+ const remainingSet = new Set(remainingSteps.map(s => s.number));
3550
3297
  const completedSteps = task.steps.filter(s => !remainingSet.has(s.number));
3551
3298
  const completedList = completedSteps.length > 0
3552
3299
  ? completedSteps.map(s => `Step ${s.number}: ${s.name}`).join(", ")
3553
3300
  : "(none)";
3554
3301
  const remainingList = remainingSteps.map(s => `Step ${s.number}: ${s.name}`).join(", ");
3555
- iterationNudge = [
3556
- ``,
3557
- `IMPORTANT: You exited on your previous iteration without completing all steps.`,
3558
- `Do NOT repeat this — you must complete all remaining steps before stopping.`,
3302
+ promptLines.push(
3559
3303
  ``,
3560
- `Completed steps (do not redo): ${completedList}`,
3561
- `Remaining steps (focus here): ${remainingList}`,
3562
- ``,
3563
- `Your final action MUST be a tool call (update STATUS.md). Do NOT produce a`,
3564
- `text-only response — that will terminate your session prematurely.`,
3565
- ``,
3566
- ].join("\n");
3304
+ `IMPORTANT: You exited previously without completing all steps.`,
3305
+ `Completed (do not redo): ${completedList}`,
3306
+ `Remaining (focus here): ${remainingList}`,
3307
+ );
3567
3308
  }
3568
3309
 
3569
- const prompt = [
3570
- `Execute all remaining steps for task ${task.taskId}.`,
3571
- ``,
3572
- `Task: ${task.taskId} — ${task.taskName}`,
3573
- `Task folder: ${task.taskFolder}/`,
3574
- `PROMPT: ${task.promptPath}`,
3575
- `STATUS: ${statusPath}`,
3576
- ``,
3577
- `This is iteration ${state.totalIterations}.`,
3578
- `Read STATUS.md FIRST to find where you left off.`,
3579
- iterationNudge,
3580
- `Steps:`,
3581
- stepListing,
3582
- ``,
3583
- `Work through these steps in order. For each step:`,
3584
- `1. Read STATUS.md to find unchecked items for that step`,
3585
- `2. Complete all items for the step`,
3586
- `3. Update STATUS.md step status to "complete"`,
3587
- `4. Commit your changes: feat(${task.taskId}): complete Step N — description`,
3588
- `5. Check for wrap-up signal files before starting the next step`,
3589
- `6. Proceed to the next incomplete step`,
3590
- ``,
3591
- `Wrap-up signal file: ${wrapUpFile}`,
3592
- `Check for this file after each checkpoint. If it exists, stop.`,
3593
- archiveSuppression,
3594
- contextDocsList,
3595
- ].join("\n");
3310
+ const prompt = promptLines.join("\n");
3596
3311
 
3597
3312
  state.workerStatus = "running";
3598
3313
  state.workerElapsed = 0;
@@ -7,6 +7,7 @@ import { execSync } from "child_process";
7
7
  import { join } from "path";
8
8
 
9
9
  import { execLog, resolveCanonicalTaskPaths, tmuxHasSession, tmuxKillSession } from "./execution.ts";
10
+ import { killMergeAgentV2, killAllMergeAgentsV2 } from "./merge.ts";
10
11
  import { deleteBatchState, parseOrchSessionNames, persistRuntimeState } from "./persistence.ts";
11
12
  import type { AbortActionStep, AbortErrorCode, AbortLaneResult, AbortMode, AbortResult, AbortTargetSession, AllocatedLane, OrchBatchRuntimeState, PersistedBatchState, PersistedLaneRecord } from "./types.ts";
12
13
 
@@ -267,6 +268,8 @@ export function killOrchSessions(
267
268
  // Best-effort child cleanup even if not explicitly targeted.
268
269
  tmuxKillSession(`${name}-worker`);
269
270
  tmuxKillSession(`${name}-reviewer`);
271
+ // TP-108: Also kill V2 merge agents (no-op if not V2)
272
+ killMergeAgentV2(name);
270
273
 
271
274
  const killed = tmuxKillSession(name);
272
275
  results.push({
@@ -332,7 +335,14 @@ export async function executeAbort(
332
335
  execLog("abort", batchState.batchId, `Failed to persist state during abort: ${err instanceof Error ? err.message : String(err)}`);
333
336
  }
334
337
 
335
- // Step 3: List all orch sessions
338
+ // TP-108: Kill all V2 merge agents (process-owned, not TMUX)
339
+ // This catches V2 merge agents that have no TMUX session.
340
+ const v2MergeKilled = killAllMergeAgentsV2();
341
+ if (v2MergeKilled > 0) {
342
+ execLog("abort", batchState.batchId, `killed ${v2MergeKilled} V2 merge agent(s)`);
343
+ }
344
+
345
+ // Step 3: List all orch sessions (TMUX — legacy + fallback)
336
346
  let allSessionNames: string[];
337
347
  try {
338
348
  allSessionNames = parseOrchSessionNames(