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
@@ -19,7 +19,16 @@
19
19
  * @since TP-104
20
20
  */
21
21
 
22
- import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync, appendFileSync, renameSync } from "fs";
22
+ import {
23
+ existsSync,
24
+ mkdirSync,
25
+ readFileSync,
26
+ writeFileSync,
27
+ readdirSync,
28
+ rmSync,
29
+ appendFileSync,
30
+ renameSync,
31
+ } from "fs";
23
32
  import { join, dirname } from "path";
24
33
 
25
34
  import {
@@ -41,6 +50,12 @@ import {
41
50
  type PacketPaths,
42
51
  } from "./types.ts";
43
52
 
53
+ // TP-195: Re-export RuntimeRegistry so dynamic-import references in
54
+ // execution.ts (`import("./process-registry.ts").RuntimeRegistry`) resolve
55
+ // without each call site having to import directly from types.ts. Pure
56
+ // re-export — no runtime impact.
57
+ export type { RuntimeRegistry };
58
+
44
59
  // ── Manifest Lifecycle ───────────────────────────────────────────────
45
60
 
46
61
  /**
@@ -66,7 +81,11 @@ export function writeManifest(stateRoot: string, manifest: RuntimeAgentManifest)
66
81
  *
67
82
  * @since TP-104
68
83
  */
69
- export function readManifest(stateRoot: string, batchId: string, agentId: RuntimeAgentId): RuntimeAgentManifest | null {
84
+ export function readManifest(
85
+ stateRoot: string,
86
+ batchId: string,
87
+ agentId: RuntimeAgentId,
88
+ ): RuntimeAgentManifest | null {
70
89
  const path = runtimeManifestPath(stateRoot, batchId, agentId);
71
90
  if (!existsSync(path)) return null;
72
91
  try {
@@ -237,7 +256,7 @@ export function isTerminalStatus(status: RuntimeAgentStatus): boolean {
237
256
  * @since TP-104
238
257
  */
239
258
  export function getLiveAgents(registry: RuntimeRegistry): RuntimeAgentManifest[] {
240
- return Object.values(registry.agents).filter(m => !isTerminalStatus(m.status));
259
+ return Object.values(registry.agents).filter((m) => !isTerminalStatus(m.status));
241
260
  }
242
261
 
243
262
  /**
@@ -245,8 +264,11 @@ export function getLiveAgents(registry: RuntimeRegistry): RuntimeAgentManifest[]
245
264
  *
246
265
  * @since TP-104
247
266
  */
248
- export function getAgentsByRole(registry: RuntimeRegistry, role: RuntimeAgentRole): RuntimeAgentManifest[] {
249
- return Object.values(registry.agents).filter(m => m.role === role);
267
+ export function getAgentsByRole(
268
+ registry: RuntimeRegistry,
269
+ role: RuntimeAgentRole,
270
+ ): RuntimeAgentManifest[] {
271
+ return Object.values(registry.agents).filter((m) => m.role === role);
250
272
  }
251
273
 
252
274
  // ── Orphan Detection ─────────────────────────────────────────────────
@@ -276,7 +298,11 @@ export function detectOrphans(registry: RuntimeRegistry): RuntimeAgentId[] {
276
298
  *
277
299
  * @since TP-104
278
300
  */
279
- export function markOrphansCrashed(stateRoot: string, batchId: string, orphanIds: RuntimeAgentId[]): void {
301
+ export function markOrphansCrashed(
302
+ stateRoot: string,
303
+ batchId: string,
304
+ orphanIds: RuntimeAgentId[],
305
+ ): void {
280
306
  for (const agentId of orphanIds) {
281
307
  updateManifestStatus(stateRoot, batchId, agentId, "crashed");
282
308
  }
@@ -291,7 +317,10 @@ export function markOrphansCrashed(stateRoot: string, batchId: string, orphanIds
291
317
  *
292
318
  * @since TP-104
293
319
  */
294
- export function cleanupBatchRuntime(stateRoot: string, batchId: string): { removed: boolean; error?: string } {
320
+ export function cleanupBatchRuntime(
321
+ stateRoot: string,
322
+ batchId: string,
323
+ ): { removed: boolean; error?: string } {
295
324
  const root = runtimeRoot(stateRoot, batchId);
296
325
  if (!existsSync(root)) return { removed: false };
297
326
  try {
@@ -115,9 +115,7 @@ export function applyVerdictRules(
115
115
  const failReasons: VerdictFailReason[] = [];
116
116
 
117
117
  // Rule 1: Any status_mismatch category → NEEDS_FIXES
118
- const statusMismatches = verdict.findings.filter(
119
- (f) => f.category === "status_mismatch",
120
- );
118
+ const statusMismatches = verdict.findings.filter((f) => f.category === "status_mismatch");
121
119
  if (statusMismatches.length > 0) {
122
120
  failReasons.push({
123
121
  rule: "status_mismatch",
@@ -135,9 +133,7 @@ export function applyVerdictRules(
135
133
  }
136
134
 
137
135
  // Rule 3: Threshold-dependent important check
138
- const importants = verdict.findings.filter(
139
- (f) => f.severity === "important",
140
- );
136
+ const importants = verdict.findings.filter((f) => f.severity === "important");
141
137
 
142
138
  if (threshold === "no_important" && importants.length >= 3) {
143
139
  failReasons.push({
@@ -167,14 +163,8 @@ export function applyVerdictRules(
167
163
  }
168
164
 
169
165
  // For all_clear threshold: even suggestions-only should fail
170
- if (
171
- threshold === "all_clear" &&
172
- failReasons.length === 0 &&
173
- verdict.findings.length > 0
174
- ) {
175
- const suggestions = verdict.findings.filter(
176
- (f) => f.severity === "suggestion",
177
- );
166
+ if (threshold === "all_clear" && failReasons.length === 0 && verdict.findings.length > 0) {
167
+ const suggestions = verdict.findings.filter((f) => f.severity === "suggestion");
178
168
  if (suggestions.length > 0) {
179
169
  failReasons.push({
180
170
  rule: "important_threshold",
@@ -228,7 +218,10 @@ export function parseVerdict(jsonString: string | undefined | null): ReviewVerdi
228
218
  }
229
219
 
230
220
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
231
- return { ...FAIL_OPEN_VERDICT, summary: "Verdict is not a JSON object — fail-open policy applied" };
221
+ return {
222
+ ...FAIL_OPEN_VERDICT,
223
+ summary: "Verdict is not a JSON object — fail-open policy applied",
224
+ };
232
225
  }
233
226
 
234
227
  const obj = raw as Record<string, unknown>;
@@ -236,7 +229,10 @@ export function parseVerdict(jsonString: string | undefined | null): ReviewVerdi
236
229
  // Validate verdict field
237
230
  const verdict = obj.verdict;
238
231
  if (verdict !== "PASS" && verdict !== "NEEDS_FIXES") {
239
- return { ...FAIL_OPEN_VERDICT, summary: `Invalid verdict value "${String(verdict)}" — fail-open policy applied` };
232
+ return {
233
+ ...FAIL_OPEN_VERDICT,
234
+ summary: `Invalid verdict value "${String(verdict)}" — fail-open policy applied`,
235
+ };
240
236
  }
241
237
 
242
238
  // Parse confidence with fallback
@@ -396,7 +392,10 @@ function buildGitDiff(cwd: string): { diff: string; fileList: string } {
396
392
  try {
397
393
  const base = computeDiffBase(cwd);
398
394
  if (!base) {
399
- return { diff: "(git diff unavailable — could not determine base)", fileList: "(file list unavailable)" };
395
+ return {
396
+ diff: "(git diff unavailable — could not determine base)",
397
+ fileList: "(file list unavailable)",
398
+ };
400
399
  }
401
400
 
402
401
  const range = `${base}..HEAD`;
@@ -407,9 +406,7 @@ function buildGitDiff(cwd: string): { diff: string; fileList: string } {
407
406
  cwd,
408
407
  timeout: 30000,
409
408
  });
410
- const fileList = fileListResult.status === 0
411
- ? fileListResult.stdout.trim()
412
- : "";
409
+ const fileList = fileListResult.status === 0 ? fileListResult.stdout.trim() : "";
413
410
 
414
411
  // Get full diff (truncated to avoid blowing up context)
415
412
  const diffResult = spawnSync("git", ["diff", range], {
@@ -418,9 +415,7 @@ function buildGitDiff(cwd: string): { diff: string; fileList: string } {
418
415
  timeout: 30000,
419
416
  maxBuffer: 200 * 1024, // 200KB max
420
417
  });
421
- const diff = diffResult.status === 0
422
- ? diffResult.stdout.trim()
423
- : "(git diff unavailable)";
418
+ const diff = diffResult.status === 0 ? diffResult.stdout.trim() : "(git diff unavailable)";
424
419
 
425
420
  return { diff, fileList };
426
421
  } catch {
@@ -455,13 +450,17 @@ function buildThresholdRules(threshold: PassThreshold): string[] {
455
450
  const rules: string[] = [];
456
451
 
457
452
  // Common rules — always apply
458
- rules.push(`- **NEEDS_FIXES** if any finding has category \`status_mismatch\` (checkbox claims work is done but it isn't)`);
453
+ rules.push(
454
+ `- **NEEDS_FIXES** if any finding has category \`status_mismatch\` (checkbox claims work is done but it isn't)`,
455
+ );
459
456
  rules.push(`- **NEEDS_FIXES** if any finding has severity \`critical\``);
460
457
 
461
458
  // Threshold-specific rules
462
459
  switch (threshold) {
463
460
  case "no_critical":
464
- rules.push(`- **PASS** even if there are \`important\` or \`suggestion\` findings (threshold: \`no_critical\`)`);
461
+ rules.push(
462
+ `- **PASS** even if there are \`important\` or \`suggestion\` findings (threshold: \`no_critical\`)`,
463
+ );
465
464
  break;
466
465
  case "no_important":
467
466
  rules.push(`- **NEEDS_FIXES** if 3 or more findings have severity \`important\``);
@@ -488,22 +487,27 @@ export function generateQualityGatePrompt(context: QualityGateContext, cwd: stri
488
487
  if (existsSync(context.promptPath)) {
489
488
  promptContent = readFileSync(context.promptPath, "utf-8");
490
489
  }
491
- } catch { /* fail-open: proceed without */ }
490
+ } catch {
491
+ /* fail-open: proceed without */
492
+ }
492
493
 
493
494
  let statusContent = "(STATUS.md not found)";
494
495
  try {
495
496
  if (existsSync(statusPath)) {
496
497
  statusContent = readFileSync(statusPath, "utf-8");
497
498
  }
498
- } catch { /* fail-open: proceed without */ }
499
+ } catch {
500
+ /* fail-open: proceed without */
501
+ }
499
502
 
500
503
  const { diff, fileList } = buildGitDiff(cwd);
501
504
 
502
505
  // Truncate diff if too long (keep first 100KB)
503
506
  const maxDiffLen = 100 * 1024;
504
- const truncatedDiff = diff.length > maxDiffLen
505
- ? diff.slice(0, maxDiffLen) + "\n\n... (diff truncated at 100KB) ..."
506
- : diff;
507
+ const truncatedDiff =
508
+ diff.length > maxDiffLen
509
+ ? diff.slice(0, maxDiffLen) + "\n\n... (diff truncated at 100KB) ..."
510
+ : diff;
507
511
 
508
512
  return [
509
513
  `# Quality Gate Review`,
@@ -670,9 +674,9 @@ export interface ReconciliationAction {
670
674
  */
671
675
  function normalizeCheckboxText(text: string): string {
672
676
  return text
673
- .replace(/\*\*|__|``|`/g, "") // strip bold/code formatting
674
- .replace(/\s+/g, " ") // collapse whitespace
675
- .replace(/^\s*[-*•]\s*/, "") // strip leading bullets
677
+ .replace(/\*\*|__|``|`/g, "") // strip bold/code formatting
678
+ .replace(/\s+/g, " ") // collapse whitespace
679
+ .replace(/^\s*[-*•]\s*/, "") // strip leading bullets
676
680
  .trim()
677
681
  .toLowerCase();
678
682
  }
@@ -718,7 +722,11 @@ export function applyStatusReconciliation(
718
722
  // No STATUS.md — mark all as unmatched
719
723
  for (const r of reconciliations) {
720
724
  result.unmatched++;
721
- result.actions.push({ checkbox: r.checkbox, outcome: "unmatched", reason: "STATUS.md not found" });
725
+ result.actions.push({
726
+ checkbox: r.checkbox,
727
+ outcome: "unmatched",
728
+ reason: "STATUS.md not found",
729
+ });
722
730
  }
723
731
  return result;
724
732
  }
@@ -726,7 +734,11 @@ export function applyStatusReconciliation(
726
734
  } catch {
727
735
  for (const r of reconciliations) {
728
736
  result.unmatched++;
729
- result.actions.push({ checkbox: r.checkbox, outcome: "unmatched", reason: "STATUS.md unreadable" });
737
+ result.actions.push({
738
+ checkbox: r.checkbox,
739
+ outcome: "unmatched",
740
+ reason: "STATUS.md unreadable",
741
+ });
730
742
  }
731
743
  return result;
732
744
  }
@@ -742,7 +754,11 @@ export function applyStatusReconciliation(
742
754
  const normalizedRecon = normalizeCheckboxText(recon.checkbox);
743
755
  if (!normalizedRecon) {
744
756
  result.unmatched++;
745
- result.actions.push({ checkbox: recon.checkbox, outcome: "unmatched", reason: "Empty checkbox text after normalization" });
757
+ result.actions.push({
758
+ checkbox: recon.checkbox,
759
+ outcome: "unmatched",
760
+ reason: "Empty checkbox text after normalization",
761
+ });
746
762
  continue;
747
763
  }
748
764
 
@@ -755,7 +771,11 @@ export function applyStatusReconciliation(
755
771
 
756
772
  const lineText = normalizeCheckboxText(cbMatch[4]);
757
773
  // Match if either contains the other (handles paraphrasing)
758
- if (lineText === normalizedRecon || lineText.includes(normalizedRecon) || normalizedRecon.includes(lineText)) {
774
+ if (
775
+ lineText === normalizedRecon ||
776
+ lineText.includes(normalizedRecon) ||
777
+ normalizedRecon.includes(lineText)
778
+ ) {
759
779
  matchedIdx = i;
760
780
  break;
761
781
  }
@@ -763,7 +783,11 @@ export function applyStatusReconciliation(
763
783
 
764
784
  if (matchedIdx === -1) {
765
785
  result.unmatched++;
766
- result.actions.push({ checkbox: recon.checkbox, outcome: "unmatched", reason: "No matching checkbox found in STATUS.md" });
786
+ result.actions.push({
787
+ checkbox: recon.checkbox,
788
+ outcome: "unmatched",
789
+ reason: "No matching checkbox found in STATUS.md",
790
+ });
767
791
  continue;
768
792
  }
769
793
 
@@ -779,7 +803,11 @@ export function applyStatusReconciliation(
779
803
  if (shouldBeChecked && currentlyChecked) {
780
804
  // Already correct
781
805
  result.alreadyCorrect++;
782
- result.actions.push({ checkbox: recon.checkbox, outcome: "no_change", reason: "Already checked (done)" });
806
+ result.actions.push({
807
+ checkbox: recon.checkbox,
808
+ outcome: "no_change",
809
+ reason: "Already checked (done)",
810
+ });
783
811
  } else if (!shouldBeChecked && !currentlyChecked) {
784
812
  // Already correct (unchecked for not_done or partial)
785
813
  // But if partial, might need annotation
@@ -787,25 +815,38 @@ export function applyStatusReconciliation(
787
815
  // Add partial annotation
788
816
  lines[matchedIdx] = `${cbMatch[1]} ${cbMatch[3]}${currentText} (partial)`;
789
817
  result.changed++;
790
- result.actions.push({ checkbox: recon.checkbox, outcome: "unchecked", reason: "Added (partial) annotation" });
818
+ result.actions.push({
819
+ checkbox: recon.checkbox,
820
+ outcome: "unchecked",
821
+ reason: "Added (partial) annotation",
822
+ });
791
823
  } else {
792
824
  result.alreadyCorrect++;
793
- result.actions.push({ checkbox: recon.checkbox, outcome: "no_change", reason: `Already unchecked (${recon.actualState})` });
825
+ result.actions.push({
826
+ checkbox: recon.checkbox,
827
+ outcome: "no_change",
828
+ reason: `Already unchecked (${recon.actualState})`,
829
+ });
794
830
  }
795
831
  } else if (shouldBeChecked && !currentlyChecked) {
796
832
  // Need to check
797
833
  lines[matchedIdx] = `${cbMatch[1]}x${cbMatch[3]}${currentText}`;
798
834
  result.changed++;
799
- result.actions.push({ checkbox: recon.checkbox, outcome: "checked", reason: "Work done but box was unchecked" });
835
+ result.actions.push({
836
+ checkbox: recon.checkbox,
837
+ outcome: "checked",
838
+ reason: "Work done but box was unchecked",
839
+ });
800
840
  } else {
801
841
  // currentlyChecked but should not be (not_done or partial)
802
842
  const annotation = recon.actualState === "partial" ? " (partial)" : "";
803
843
  const cleanText = currentText.replace(/\s*\(partial\)\s*$/, "");
804
844
  lines[matchedIdx] = `${cbMatch[1]} ${cbMatch[3]}${cleanText}${annotation}`;
805
845
  result.changed++;
806
- const outcomeReason = recon.actualState === "partial"
807
- ? "Unchecked — work partially done"
808
- : "Unchecked — work not done";
846
+ const outcomeReason =
847
+ recon.actualState === "partial"
848
+ ? "Unchecked — work partially done"
849
+ : "Unchecked — work not done";
809
850
  result.actions.push({ checkbox: recon.checkbox, outcome: "unchecked", reason: outcomeReason });
810
851
  }
811
852
  }
@@ -860,10 +901,10 @@ export function generateFeedbackMd(
860
901
  maxCycles: number,
861
902
  passThreshold: PassThreshold = "no_critical",
862
903
  ): string {
863
- const criticals = verdict.findings.filter(f => f.severity === "critical");
864
- const importants = verdict.findings.filter(f => f.severity === "important");
865
- const suggestions = verdict.findings.filter(f => f.severity === "suggestion");
866
- const mismatches = verdict.statusReconciliation.filter(r => r.actualState !== "done");
904
+ const criticals = verdict.findings.filter((f) => f.severity === "critical");
905
+ const importants = verdict.findings.filter((f) => f.severity === "important");
906
+ const suggestions = verdict.findings.filter((f) => f.severity === "suggestion");
907
+ const mismatches = verdict.statusReconciliation.filter((r) => r.actualState !== "done");
867
908
 
868
909
  // Under all_clear, suggestions are also blocking
869
910
  const includeSuggestions = passThreshold === "all_clear";
@@ -940,14 +981,21 @@ export function generateFeedbackMd(
940
981
  lines.push(``);
941
982
  }
942
983
 
943
- const totalBlocking = criticals.length + importants.length
944
- + (includeSuggestions ? suggestions.length : 0) + mismatches.length;
984
+ const totalBlocking =
985
+ criticals.length +
986
+ importants.length +
987
+ (includeSuggestions ? suggestions.length : 0) +
988
+ mismatches.length;
945
989
 
946
990
  if (totalBlocking === 0) {
947
991
  lines.push(`## No blocking findings`);
948
992
  lines.push(``);
949
- lines.push(`The review returned NEEDS_FIXES but no blocking findings were extracted for threshold \`${passThreshold}\`.`);
950
- lines.push(`This may indicate a threshold or verdict-rule mismatch. Review the REVIEW_VERDICT.json for details.`);
993
+ lines.push(
994
+ `The review returned NEEDS_FIXES but no blocking findings were extracted for threshold \`${passThreshold}\`.`,
995
+ );
996
+ lines.push(
997
+ `This may indicate a threshold or verdict-rule mismatch. Review the REVIEW_VERDICT.json for details.`,
998
+ );
951
999
  lines.push(``);
952
1000
  }
953
1001
 
@@ -978,14 +1026,18 @@ export function buildFixAgentPrompt(
978
1026
  if (existsSync(statusPath)) {
979
1027
  statusContent = readFileSync(statusPath, "utf-8");
980
1028
  }
981
- } catch { /* proceed without */ }
1029
+ } catch {
1030
+ /* proceed without */
1031
+ }
982
1032
 
983
1033
  let promptContent = "(PROMPT.md not found)";
984
1034
  try {
985
1035
  if (existsSync(context.promptPath)) {
986
1036
  promptContent = readFileSync(context.promptPath, "utf-8");
987
1037
  }
988
- } catch { /* proceed without */ }
1038
+ } catch {
1039
+ /* proceed without */
1040
+ }
989
1041
 
990
1042
  return [
991
1043
  `# Quality Gate Remediation — Fix Cycle ${cycleNum}`,