taskplane 0.29.2 → 0.30.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 (41) hide show
  1. package/bin/gitignore-patterns.mjs +11 -8
  2. package/bin/rpc-wrapper.mjs +410 -357
  3. package/bin/taskplane.mjs +533 -250
  4. package/extensions/reviewer-extension.ts +17 -11
  5. package/extensions/taskplane/abort.ts +50 -18
  6. package/extensions/taskplane/agent-bridge-extension.ts +232 -105
  7. package/extensions/taskplane/agent-host.ts +224 -97
  8. package/extensions/taskplane/cleanup.ts +71 -42
  9. package/extensions/taskplane/config-loader.ts +142 -58
  10. package/extensions/taskplane/config-schema.ts +6 -13
  11. package/extensions/taskplane/config.ts +10 -2
  12. package/extensions/taskplane/diagnostic-reports.ts +59 -47
  13. package/extensions/taskplane/diagnostics.ts +13 -13
  14. package/extensions/taskplane/discovery.ts +35 -61
  15. package/extensions/taskplane/engine-worker.ts +53 -46
  16. package/extensions/taskplane/engine.ts +1760 -602
  17. package/extensions/taskplane/execution.ts +426 -206
  18. package/extensions/taskplane/extension.ts +1073 -598
  19. package/extensions/taskplane/formatting.ts +136 -124
  20. package/extensions/taskplane/git.ts +0 -2
  21. package/extensions/taskplane/lane-runner.ts +542 -311
  22. package/extensions/taskplane/mailbox.ts +57 -49
  23. package/extensions/taskplane/merge.ts +662 -383
  24. package/extensions/taskplane/messages.ts +109 -51
  25. package/extensions/taskplane/migrations.ts +1 -1
  26. package/extensions/taskplane/path-resolver.ts +8 -9
  27. package/extensions/taskplane/persistence.ts +425 -262
  28. package/extensions/taskplane/process-registry.ts +36 -7
  29. package/extensions/taskplane/quality-gate.ts +107 -55
  30. package/extensions/taskplane/resume.ts +774 -267
  31. package/extensions/taskplane/sessions.ts +1 -1
  32. package/extensions/taskplane/settings-tui.ts +505 -164
  33. package/extensions/taskplane/sidecar-telemetry.ts +25 -10
  34. package/extensions/taskplane/supervisor.ts +477 -270
  35. package/extensions/taskplane/task-executor-core.ts +178 -53
  36. package/extensions/taskplane/types.ts +186 -108
  37. package/extensions/taskplane/verification.ts +27 -22
  38. package/extensions/taskplane/waves.ts +59 -43
  39. package/extensions/taskplane/workspace.ts +14 -12
  40. package/extensions/taskplane/worktree.ts +218 -196
  41. package/package.json +14 -2
@@ -80,10 +80,16 @@ export function parsePromptMd(content: string, promptPath: string): CoreParsedTa
80
80
  const taskFolder = dirname(resolve(promptPath));
81
81
 
82
82
  // Task ID and name
83
- let taskId = "", taskName = "";
83
+ let taskId = "",
84
+ taskName = "";
84
85
  const titleMatch = text.match(/^#\s+(?:Task:\s*)?(\S+-\d+)\s*[-–:]\s*(.+)/m);
85
- if (titleMatch) { taskId = titleMatch[1]; taskName = titleMatch[2].trim(); }
86
- else { taskId = basename(taskFolder); taskName = taskId; }
86
+ if (titleMatch) {
87
+ taskId = titleMatch[1];
88
+ taskName = titleMatch[2].trim();
89
+ } else {
90
+ taskId = basename(taskFolder);
91
+ taskName = taskId;
92
+ }
87
93
 
88
94
  // Review level
89
95
  let reviewLevel = 0;
@@ -99,22 +105,27 @@ export function parsePromptMd(content: string, promptPath: string): CoreParsedTa
99
105
  const steps: StepInfo[] = [];
100
106
  const stepRegex = /###\s+Step\s+(\d+):\s*(.+)/g;
101
107
  const positions: { number: number; name: string; start: number }[] = [];
102
- let m;
108
+ let m: RegExpExecArray | null;
103
109
  while ((m = stepRegex.exec(text)) !== null) {
104
110
  positions.push({ number: parseInt(m[1]), name: m[2].trim(), start: m.index });
105
111
  }
106
112
  for (let i = 0; i < positions.length; i++) {
107
- const section = text.slice(positions[i].start, i + 1 < positions.length ? positions[i + 1].start : text.length);
113
+ const section = text.slice(
114
+ positions[i].start,
115
+ i + 1 < positions.length ? positions[i + 1].start : text.length,
116
+ );
108
117
  const checkboxes: { text: string; checked: boolean }[] = [];
109
118
  const cbRegex = /^\s*-\s*\[([ xX])\]\s*(.*)/gm;
110
- let cb;
119
+ let cb: RegExpExecArray | null;
111
120
  while ((cb = cbRegex.exec(section)) !== null) {
112
121
  checkboxes.push({ text: cb[2].trim(), checked: cb[1].toLowerCase() === "x" });
113
122
  }
114
123
  steps.push({
115
- number: positions[i].number, name: positions[i].name,
116
- status: "not-started", checkboxes,
117
- totalChecked: checkboxes.filter(c => c.checked).length,
124
+ number: positions[i].number,
125
+ name: positions[i].name,
126
+ status: "not-started",
127
+ checkboxes,
128
+ totalChecked: checkboxes.filter((c) => c.checked).length,
118
129
  totalItems: checkboxes.length,
119
130
  });
120
131
  }
@@ -124,7 +135,7 @@ export function parsePromptMd(content: string, promptPath: string): CoreParsedTa
124
135
  const ctxMatch = text.match(/##\s+Context to Read First\s*\n+([\s\S]*?)(?=\n##\s|$)/);
125
136
  if (ctxMatch) {
126
137
  const pathRegex = /`([^\s`]+\.(?:md|yaml|json|go|ts|js))`/g;
127
- let pm;
138
+ let pm: RegExpExecArray | null;
128
139
  while ((pm = pathRegex.exec(ctxMatch[1])) !== null) contextDocs.push(pm[1]);
129
140
  }
130
141
 
@@ -145,7 +156,8 @@ export function parseStatusMd(content: string): ParsedStatus {
145
156
  const text = content.replace(/\r\n/g, "\n");
146
157
  const steps: StepInfo[] = [];
147
158
  let currentStep: StepInfo | null = null;
148
- let reviewCounter = 0, iteration = 0;
159
+ let reviewCounter = 0,
160
+ iteration = 0;
149
161
 
150
162
  for (const line of text.split("\n")) {
151
163
  const rcMatch = line.match(/\*\*Review Counter:\*\*\s*(\d+)/);
@@ -156,11 +168,18 @@ export function parseStatusMd(content: string): ParsedStatus {
156
168
  const stepMatch = line.match(/^###\s+Step\s+(\d+):\s*(.+)/);
157
169
  if (stepMatch) {
158
170
  if (currentStep) {
159
- currentStep.totalChecked = currentStep.checkboxes.filter(c => c.checked).length;
171
+ currentStep.totalChecked = currentStep.checkboxes.filter((c) => c.checked).length;
160
172
  currentStep.totalItems = currentStep.checkboxes.length;
161
173
  steps.push(currentStep);
162
174
  }
163
- currentStep = { number: parseInt(stepMatch[1]), name: stepMatch[2].trim(), status: "not-started", checkboxes: [], totalChecked: 0, totalItems: 0 };
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
+ };
164
183
  continue;
165
184
  }
166
185
  if (currentStep) {
@@ -168,14 +187,16 @@ export function parseStatusMd(content: string): ParsedStatus {
168
187
  if (ss) {
169
188
  const s = ss[1];
170
189
  if (s.includes("✅") || s.toLowerCase().includes("complete")) currentStep.status = "complete";
171
- else if (s.includes("🟨") || s.toLowerCase().includes("progress")) currentStep.status = "in-progress";
190
+ else if (s.includes("🟨") || s.toLowerCase().includes("progress"))
191
+ currentStep.status = "in-progress";
172
192
  }
173
193
  const cb = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)/);
174
- if (cb) currentStep.checkboxes.push({ text: cb[2].trim(), checked: cb[1].toLowerCase() === "x" });
194
+ if (cb)
195
+ currentStep.checkboxes.push({ text: cb[2].trim(), checked: cb[1].toLowerCase() === "x" });
175
196
  }
176
197
  }
177
198
  if (currentStep) {
178
- currentStep.totalChecked = currentStep.checkboxes.filter(c => c.checked).length;
199
+ currentStep.totalChecked = currentStep.checkboxes.filter((c) => c.checked).length;
179
200
  currentStep.totalItems = currentStep.checkboxes.length;
180
201
  steps.push(currentStep);
181
202
  }
@@ -190,17 +211,27 @@ export function parseStatusMd(content: string): ParsedStatus {
190
211
  * @param task - Parsed task (from parsePromptMd or orchestrator ParsedTask)
191
212
  * @returns Complete STATUS.md content string
192
213
  */
193
- export function generateStatusMd(task: { taskId: string; taskName: string; reviewLevel: number; size: string; steps: StepInfo[] }): string {
214
+ export function generateStatusMd(task: {
215
+ taskId: string;
216
+ taskName: string;
217
+ reviewLevel: number;
218
+ size: string;
219
+ steps: StepInfo[];
220
+ }): string {
194
221
  const now = new Date().toISOString().slice(0, 10);
195
222
  const lines: string[] = [
196
- `# ${task.taskId}: ${task.taskName} — Status`, "",
223
+ `# ${task.taskId}: ${task.taskName} — Status`,
224
+ "",
197
225
  `**Current Step:** Not Started`,
198
226
  `**Status:** 🔵 Ready for Execution`,
199
227
  `**Last Updated:** ${now}`,
200
228
  `**Review Level:** ${task.reviewLevel}`,
201
229
  `**Review Counter:** 0`,
202
230
  `**Iteration:** 0`,
203
- `**Size:** ${task.size}`, "", "---", "",
231
+ `**Size:** ${task.size}`,
232
+ "",
233
+ "---",
234
+ "",
204
235
  ];
205
236
  for (const step of task.steps) {
206
237
  lines.push(`### Step ${step.number}: ${step.name}`, `**Status:** ⬜ Not Started`, "");
@@ -208,11 +239,37 @@ export function generateStatusMd(task: { taskId: string; taskName: string; revie
208
239
  lines.push("", "---", "");
209
240
  }
210
241
  lines.push(
211
- "## Reviews", "", "| # | Type | Step | Verdict | File |", "|---|------|------|---------|------|", "", "---", "",
212
- "## Discoveries", "", "| Discovery | Disposition | Location |", "|-----------|-------------|----------|", "", "---", "",
213
- "## Execution Log", "", "| Timestamp | Action | Outcome |", "|-----------|--------|---------|",
214
- `| ${now} | Task staged | STATUS.md auto-generated by task-runner |`, "", "---", "",
215
- "## Blockers", "", "*None*", "", "---", "", "## Notes", "", "*Reserved for execution notes*",
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*",
216
273
  );
217
274
  return lines.join("\n");
218
275
  }
@@ -230,7 +287,9 @@ export function generateStatusMd(task: { taskId: string; taskName: string; revie
230
287
  */
231
288
  export function updateStatusField(statusPath: string, field: string, value: string): void {
232
289
  let content = readFileSync(statusPath, "utf-8").replace(/\r\n/g, "\n");
233
- const pattern = new RegExp(`(\\*\\*${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\*\\*\\s*)(.+)`);
290
+ const pattern = new RegExp(
291
+ `(\\*\\*${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\*\\*\\s*)(.+)`,
292
+ );
234
293
  if (pattern.test(content)) {
235
294
  content = content.replace(pattern, `$1${value}`);
236
295
  } else {
@@ -246,9 +305,18 @@ export function updateStatusField(statusPath: string, field: string, value: stri
246
305
  * @param stepNum - Step number to update
247
306
  * @param status - New status
248
307
  */
249
- export function updateStepStatus(statusPath: string, stepNum: number, status: "not-started" | "in-progress" | "complete"): void {
308
+ export function updateStepStatus(
309
+ statusPath: string,
310
+ stepNum: number,
311
+ status: "not-started" | "in-progress" | "complete",
312
+ ): void {
250
313
  let content = readFileSync(statusPath, "utf-8").replace(/\r\n/g, "\n");
251
- const emoji = status === "complete" ? "✅ Complete" : status === "in-progress" ? "🟨 In Progress" : "⬜ Not Started";
314
+ const emoji =
315
+ status === "complete"
316
+ ? "✅ Complete"
317
+ : status === "in-progress"
318
+ ? "🟨 In Progress"
319
+ : "⬜ Not Started";
252
320
  const lines = content.split("\n");
253
321
  let inTarget = false;
254
322
  for (let i = 0; i < lines.length; i++) {
@@ -272,7 +340,9 @@ export function updateStepStatus(statusPath: string, stepNum: number, status: "n
272
340
  export function appendTableRow(statusPath: string, sectionName: string, row: string): void {
273
341
  let content = readFileSync(statusPath, "utf-8").replace(/\r\n/g, "\n");
274
342
  const lines = content.split("\n");
275
- let insertIdx = -1, inSection = false, lastTableRow = -1;
343
+ let insertIdx = -1,
344
+ inSection = false,
345
+ lastTableRow = -1;
276
346
  for (let i = 0; i < lines.length; i++) {
277
347
  if (lines[i].match(new RegExp(`^##\\s+${sectionName}`))) {
278
348
  inSection = true;
@@ -306,8 +376,19 @@ export function logExecution(statusPath: string, action: string, outcome: string
306
376
  /**
307
377
  * Log a review entry to the Reviews table in STATUS.md.
308
378
  */
309
- export function logReview(statusPath: string, num: string, type: string, stepNum: number, verdict: string, file: string): void {
310
- appendTableRow(statusPath, "Reviews", `| ${num} | ${type} | Step ${stepNum} | ${verdict} | ${file} |`);
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
+ );
311
392
  }
312
393
 
313
394
  /**
@@ -370,8 +451,18 @@ export function extractVerdict(reviewContent: string): string {
370
451
 
371
452
  // Tolerate non-standard verdict formats
372
453
  const lower = reviewContent.toLowerCase();
373
- if (lower.includes("changes requested") || lower.includes("request changes") || lower.includes("needs revision")) return "REVISE";
374
- if (lower.includes("approve") && !lower.includes("do not approve") && !lower.includes("cannot approve")) return "APPROVE";
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";
375
466
  if (lower.includes("rethink") || lower.includes("re-think")) return "RETHINK";
376
467
 
377
468
  return "UNKNOWN";
@@ -409,7 +500,17 @@ export function getHeadCommitSha(): string {
409
500
  */
410
501
  export function findStepBoundaryCommit(stepNumber: number, taskId: string, since?: string): string {
411
502
  try {
412
- const args = ["log", "--oneline", "--grep", `complete Step ${stepNumber}`, "--grep", taskId, "--all-match", "-1", "--format=%H"];
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
+ ];
413
514
  if (since) args.push(`${since}..HEAD`);
414
515
  const result = spawnSync("git", args, {
415
516
  encoding: "utf-8",
@@ -443,7 +544,7 @@ export interface StandardsConfig {
443
544
  export function resolveStandards(
444
545
  globalStandards: StandardsConfig,
445
546
  overrides: Record<string, Partial<StandardsConfig>>,
446
- taskAreas: Record<string, { path: string;[key: string]: any }>,
547
+ taskAreas: Record<string, { path: string; [key: string]: any }>,
447
548
  taskFolder: string,
448
549
  ): StandardsConfig {
449
550
  const normalizedFolder = taskFolder.replace(/\\/g, "/");
@@ -488,44 +589,60 @@ export function generateReviewRequest(
488
589
  outputPath: string,
489
590
  stepBaselineCommit?: string,
490
591
  ): string {
491
- const standardsDocs = standards.docs.map(d => ` - ${d}`).join("\n");
492
- const standardsRules = standards.rules.map(r => `- ${r}`).join("\n");
592
+ const standardsDocs = standards.docs.map((d) => ` - ${d}`).join("\n");
593
+ const standardsRules = standards.rules.map((r) => `- ${r}`).join("\n");
493
594
  const statusPath = join(taskFolder, "STATUS.md");
494
595
 
495
596
  if (type === "plan") {
496
597
  return [
497
- `# Review Request: Plan Review`, "",
598
+ `# Review Request: Plan Review`,
599
+ "",
498
600
  `You are reviewing an implementation plan for a ${projectName} task.`,
499
- `You have full tool access — use \`read\` to examine files and \`bash\` to run commands.`, "",
500
- `## Task Context`, "",
601
+ `You have full tool access — use \`read\` to examine files and \`bash\` to run commands.`,
602
+ "",
603
+ `## Task Context`,
604
+ "",
501
605
  `- **Task PROMPT:** ${taskPromptPath}`,
502
606
  `- **Task STATUS:** ${statusPath}`,
503
- `- **Step being planned:** Step ${stepNum}: ${stepName}`, "",
504
- `## Instructions`, "",
607
+ `- **Step being planned:** Step ${stepNum}: ${stepName}`,
608
+ "",
609
+ `## Instructions`,
610
+ "",
505
611
  `1. Read the PROMPT.md for full requirements`,
506
612
  `2. Read STATUS.md for progress so far`,
507
613
  `3. Check relevant source files for existing patterns:`,
508
- standardsDocs, "",
509
- `## Project Standards`, "", standardsRules, "",
510
- `## Output`, "",
614
+ standardsDocs,
615
+ "",
616
+ `## Project Standards`,
617
+ "",
618
+ standardsRules,
619
+ "",
620
+ `## Output`,
621
+ "",
511
622
  `Write your review to: \`${outputPath}\``,
512
623
  ].join("\n");
513
624
  }
514
625
 
515
- const diffCmd = stepBaselineCommit ? `git diff ${stepBaselineCommit}..HEAD --name-only` : `git diff --name-only`;
626
+ const diffCmd = stepBaselineCommit
627
+ ? `git diff ${stepBaselineCommit}..HEAD --name-only`
628
+ : `git diff --name-only`;
516
629
  const diffFullCmd = stepBaselineCommit ? `git diff ${stepBaselineCommit}..HEAD` : `git diff`;
517
630
 
518
631
  return [
519
- `# Review Request: Code Review`, "",
632
+ `# Review Request: Code Review`,
633
+ "",
520
634
  `You are reviewing code changes for a ${projectName} task.`,
521
- `You have full tool access — use \`read\` to examine files and \`bash\` to run commands.`, "",
522
- `## Task Context`, "",
635
+ `You have full tool access — use \`read\` to examine files and \`bash\` to run commands.`,
636
+ "",
637
+ `## Task Context`,
638
+ "",
523
639
  `- **Task PROMPT:** ${taskPromptPath}`,
524
640
  `- **Task STATUS:** ${statusPath}`,
525
641
  `- **Step reviewed:** Step ${stepNum}: ${stepName}`,
526
642
  ...(stepBaselineCommit ? [`- **Step baseline commit:** ${stepBaselineCommit}`] : []),
527
643
  "",
528
- `## Instructions`, "",
644
+ `## Instructions`,
645
+ "",
529
646
  `1. Run \`${diffCmd}\` to see files changed in this step`,
530
647
  ` Then \`${diffFullCmd}\` for the full diff`,
531
648
  ` **Important:** The worker commits code via checkpoints, so plain \`git diff\` may show nothing.`,
@@ -533,9 +650,14 @@ export function generateReviewRequest(
533
650
  `2. Read changed files in full for context`,
534
651
  `3. Check neighboring files for pattern consistency`,
535
652
  `4. Check standards:`,
536
- standardsDocs, "",
537
- `## Project Standards`, "", standardsRules, "",
538
- `## Output`, "",
653
+ standardsDocs,
654
+ "",
655
+ `## Project Standards`,
656
+ "",
657
+ standardsRules,
658
+ "",
659
+ `## Output`,
660
+ "",
539
661
  `Write your review to: \`${outputPath}\``,
540
662
  ].join("\n");
541
663
  }
@@ -546,5 +668,8 @@ export function generateReviewRequest(
546
668
  * Convert a kebab-case name to Title Case for display.
547
669
  */
548
670
  export function displayName(name: string): string {
549
- return name.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
671
+ return name
672
+ .split("-")
673
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
674
+ .join(" ");
550
675
  }