taskplane 0.20.6 → 0.21.1

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.
@@ -331,6 +331,8 @@ function createSessionState() {
331
331
  currentTool: null,
332
332
  error: null,
333
333
  agentEnded: false,
334
+ /** Authoritative context usage from pi get_session_stats (null if unavailable) */
335
+ contextUsage: null,
334
336
  };
335
337
  }
336
338
 
@@ -414,6 +416,10 @@ function applyEvent(state, event) {
414
416
  if (event.success === false && event.error) {
415
417
  state.error = event.error;
416
418
  }
419
+ // get_session_stats response — extract authoritative contextUsage
420
+ if (event.success === true && event.data?.contextUsage) {
421
+ state.contextUsage = event.data.contextUsage;
422
+ }
417
423
  break;
418
424
  }
419
425
 
@@ -455,6 +461,8 @@ function buildExitSummary(state, exitCode, exitSignal, errorOverride, startTime)
455
461
  durationSec,
456
462
  lastToolCall: state.lastToolCall,
457
463
  error: finalError,
464
+ // Authoritative context usage from pi ≥ 0.63.0 (null if unavailable)
465
+ contextUsage: state.contextUsage || null,
458
466
  };
459
467
 
460
468
  return redactSummary(rawSummary);
@@ -614,6 +622,22 @@ function closeStdin() {
614
622
  }
615
623
  }
616
624
 
625
+ /**
626
+ * Query pi for authoritative session stats including contextUsage.
627
+ * Available in pi ≥ 0.63.0 (RPC get_session_stats exposes contextUsage).
628
+ * Safe to call on older versions — the command is ignored or returns
629
+ * without the field, and state.contextUsage stays null.
630
+ */
631
+ function querySessionStats() {
632
+ try {
633
+ if (proc.stdin && !proc.stdin.destroyed) {
634
+ proc.stdin.write(JSON.stringify({ type: "get_session_stats" }) + "\n");
635
+ }
636
+ } catch {
637
+ // stdin may be closed — ignore
638
+ }
639
+ }
640
+
617
641
  // ── Route RPC events ─────────────────────────────────────────────────
618
642
 
619
643
  function handleEvent(event) {
@@ -628,6 +652,13 @@ function handleEvent(event) {
628
652
  // Side effects that depend on the event type (IO, stdin lifecycle, display)
629
653
  switch (event.type) {
630
654
  case "message_end":
655
+ displayProgress(state);
656
+ // Query pi for authoritative context usage (pi ≥ 0.63.0).
657
+ // Falls back gracefully: older pi versions ignore the command
658
+ // or return a response without contextUsage — state.contextUsage stays null.
659
+ querySessionStats();
660
+ break;
661
+
631
662
  case "tool_execution_start":
632
663
  displayProgress(state);
633
664
  break;
package/bin/taskplane.mjs CHANGED
@@ -1112,9 +1112,33 @@ async function cmdInit(args) {
1112
1112
  if (effectiveAlreadyInitialized && resolvedMode === "workspace" && effectiveConfigPath) {
1113
1113
  const configRepo = path.basename(path.dirname(effectiveConfigPath));
1114
1114
  const configRepoRoot = path.join(projectRoot, configRepo);
1115
+ // Read existing routing from config repo first, then fall back to
1116
+ // the workspace root's .pi/taskplane-workspace.yaml (which --force may overwrite).
1117
+ // This preserves user's tasks_root and default_repo on reinit.
1115
1118
  const existingWorkspaceJson = readWorkspaceJson(configRepoRoot);
1116
- const workspaceTasksRoot = existingWorkspaceJson?.routing?.tasks_root || "taskplane-tasks";
1117
- const workspaceDefaultRepo = existingWorkspaceJson?.routing?.default_repo || configRepo;
1119
+ const existingRootYaml = (() => {
1120
+ try {
1121
+ const yamlPath = path.join(projectRoot, ".pi", "taskplane-workspace.yaml");
1122
+ if (fs.existsSync(yamlPath)) {
1123
+ const raw = fs.readFileSync(yamlPath, "utf-8");
1124
+ const tasksMatch = raw.match(/tasks_root:\s*"?([^"\n]+)"?/);
1125
+ const defaultMatch = raw.match(/default_repo:\s*"?([^"\n]+)"?/);
1126
+ return {
1127
+ routing: {
1128
+ tasks_root: tasksMatch?.[1]?.trim() || null,
1129
+ default_repo: defaultMatch?.[1]?.trim() || null,
1130
+ },
1131
+ };
1132
+ }
1133
+ } catch {}
1134
+ return null;
1135
+ })();
1136
+ const workspaceTasksRoot = existingWorkspaceJson?.routing?.tasks_root
1137
+ || existingRootYaml?.routing?.tasks_root
1138
+ || "taskplane-tasks";
1139
+ const workspaceDefaultRepo = existingWorkspaceJson?.routing?.default_repo
1140
+ || existingRootYaml?.routing?.default_repo
1141
+ || configRepo;
1118
1142
  const workspaceRepoNames = Array.from(
1119
1143
  new Set([
1120
1144
  ...detection.subRepos,
@@ -1193,6 +1217,15 @@ async function cmdInit(args) {
1193
1217
  { skipIfExists: !force, label: ".pi/taskplane-workspace.yaml" },
1194
1218
  );
1195
1219
 
1220
+ // ── Gitignore enforcement in config repo (Scenario D) ───
1221
+ // Ensure .gitignore exists even when reusing existing config
1222
+ const gitignoreResult = ensureGitignoreEntries(configRepoRoot, { dryRun: false, prefix: ".taskplane/" });
1223
+ if (gitignoreResult.created) {
1224
+ console.log(` ${c.green}create${c.reset} ${configRepo}/.gitignore`);
1225
+ } else if (gitignoreResult.added.length > 0) {
1226
+ console.log(` ${c.green}update${c.reset} ${configRepo}/.gitignore (${gitignoreResult.added.length} entries added)`);
1227
+ }
1228
+
1196
1229
  console.log(`\n${OK} ${c.bold}Workspace pointer created.${c.reset}\n`);
1197
1230
  console.log(` Config: ${c.cyan}${configRepo}/.taskplane/${c.reset}`);
1198
1231
  console.log(` Pointer: ${c.cyan}.pi/taskplane-pointer.json${c.reset}`);
@@ -2510,18 +2543,20 @@ function cmdDoctor() {
2510
2543
  console.log();
2511
2544
  const hasUnifiedJson = fs.existsSync(path.join(configLocation.root, configLocation.prefix, "taskplane-config.json"));
2512
2545
  const configFiles = [
2513
- { path: "taskplane-config.json", required: false },
2514
- { path: "task-runner.yaml", required: !hasUnifiedJson },
2515
- { path: "task-orchestrator.yaml", required: !hasUnifiedJson },
2516
- { path: "agents/task-worker.md", required: true },
2517
- { path: "agents/task-reviewer.md", required: true },
2518
- { path: "agents/task-merger.md", required: true },
2519
- { path: "agents/supervisor.md", required: false },
2520
- { path: "taskplane.json", required: false },
2546
+ { path: "taskplane-config.json", required: false, hide: false },
2547
+ // YAML configs are legacy fallback — hide when taskplane-config.json exists
2548
+ { path: "task-runner.yaml", required: !hasUnifiedJson, hide: hasUnifiedJson },
2549
+ { path: "task-orchestrator.yaml", required: !hasUnifiedJson, hide: hasUnifiedJson },
2550
+ { path: "agents/task-worker.md", required: true, hide: false },
2551
+ { path: "agents/task-reviewer.md", required: true, hide: false },
2552
+ { path: "agents/task-merger.md", required: true, hide: false },
2553
+ // supervisor.md is created by /orch; taskplane.json is created at runtime
2554
+ { path: "agents/supervisor.md", required: false, hide: true },
2555
+ { path: "taskplane.json", required: false, hide: true },
2521
2556
  ];
2522
2557
 
2523
2558
  let missingRequiredConfigs = 0;
2524
- for (const { path: relPath, required } of configFiles) {
2559
+ for (const { path: relPath, required, hide } of configFiles) {
2525
2560
  const fullPath = path.join(configLocation.root, configLocation.prefix, relPath);
2526
2561
  const displayPath = `${configLocation.label}/${relPath}`;
2527
2562
  const exists = fs.existsSync(fullPath);
@@ -2531,7 +2566,8 @@ function cmdDoctor() {
2531
2566
  console.log(` ${FAIL} ${displayPath} missing`);
2532
2567
  missingRequiredConfigs++;
2533
2568
  issues++;
2534
- } else {
2569
+ } else if (!hide) {
2570
+ // Show optional files only when they're relevant (not superseded)
2535
2571
  console.log(` ${WARN} ${displayPath} missing ${c.dim}(optional)${c.reset}`);
2536
2572
  }
2537
2573
  }
@@ -1367,6 +1367,8 @@ interface SidecarTelemetryDelta {
1367
1367
  lastRetryError: string;
1368
1368
  /** Whether any sidecar events were parsed in this tick (used for callback gating) */
1369
1369
  hadEvents: boolean;
1370
+ /** Authoritative context usage from pi get_session_stats (pi ≥ 0.63.0, null if unavailable) */
1371
+ contextUsage: { percentUsed: number; totalTokens: number; maxTokens: number } | null;
1370
1372
  }
1371
1373
 
1372
1374
  /**
@@ -1385,7 +1387,7 @@ function tailSidecarJsonl(filePath: string, tailState: SidecarTailState): Sideca
1385
1387
  inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
1386
1388
  cost: 0, latestTotalTokens: 0, toolCalls: 0, lastTool: "",
1387
1389
  retryActive: tailState.retryActive, retriesStarted: 0, lastRetryError: "",
1388
- hadEvents: false,
1390
+ hadEvents: false, contextUsage: null,
1389
1391
  };
1390
1392
 
1391
1393
  // Gracefully handle missing file (wrapper hasn't written yet)
@@ -1496,6 +1498,21 @@ function tailSidecarJsonl(filePath: string, tailState: SidecarTailState): Sideca
1496
1498
  tailState.retryActive = false;
1497
1499
  break;
1498
1500
  }
1501
+
1502
+ case "response": {
1503
+ // get_session_stats response from pi ≥ 0.63.0 — authoritative context usage
1504
+ if (event.success === true && event.data?.contextUsage) {
1505
+ const cu = event.data.contextUsage;
1506
+ if (typeof cu.percentUsed === "number") {
1507
+ delta.contextUsage = {
1508
+ percentUsed: cu.percentUsed,
1509
+ totalTokens: cu.totalTokens || 0,
1510
+ maxTokens: cu.maxTokens || 0,
1511
+ };
1512
+ }
1513
+ }
1514
+ break;
1515
+ }
1499
1516
  }
1500
1517
  }
1501
1518
 
@@ -2405,8 +2422,10 @@ export default function (pi: ExtensionAPI) {
2405
2422
  state.reviewerLastTool = delta.lastTool;
2406
2423
  }
2407
2424
 
2408
- // Context %
2409
- if (delta.latestTotalTokens > 0 && contextWindow > 0) {
2425
+ // Context % — prefer authoritative contextUsage (pi ≥ 0.63.0)
2426
+ if (delta.contextUsage) {
2427
+ state.reviewerContextPct = delta.contextUsage.percentUsed;
2428
+ } else if (delta.latestTotalTokens > 0 && contextWindow > 0) {
2410
2429
  state.reviewerContextPct = (delta.latestTotalTokens / contextWindow) * 100;
2411
2430
  }
2412
2431
 
@@ -2566,7 +2585,10 @@ export default function (pi: ExtensionAPI) {
2566
2585
  state.reviewerCostUsd += delta.cost;
2567
2586
  state.reviewerToolCount += delta.toolCalls;
2568
2587
  if (delta.lastTool) state.reviewerLastTool = delta.lastTool;
2569
- if (delta.latestTotalTokens > 0 && contextWindow > 0) {
2588
+ // Context % prefer authoritative contextUsage (pi ≥ 0.63.0)
2589
+ if (delta.contextUsage) {
2590
+ state.reviewerContextPct = delta.contextUsage.percentUsed;
2591
+ } else if (delta.latestTotalTokens > 0 && contextWindow > 0) {
2570
2592
  state.reviewerContextPct = (delta.latestTotalTokens / contextWindow) * 100;
2571
2593
  }
2572
2594
  writeLaneState(state);
@@ -3140,18 +3162,24 @@ export default function (pi: ExtensionAPI) {
3140
3162
  state.workerLastRetryError = delta.lastRetryError;
3141
3163
  }
3142
3164
 
3143
- // Context % (same as subprocess onContextPct)
3144
- // totalTokens is cumulative from the most recent message_end
3145
- if (delta.latestTotalTokens > 0 && contextWindow > 0) {
3146
- const pct = (delta.latestTotalTokens / contextWindow) * 100;
3147
- state.workerContextPct = pct;
3148
- if (pct >= warnPct) {
3149
- writeWrapUpSignal(`Wrap up (context ${Math.round(pct)}%)`);
3150
- }
3151
- if (pct >= killPct && state.workerStatus === "running") {
3152
- console.error(`[task-runner] tmux worker: context limit (${Math.round(pct)}%) — killing session '${sessionName}'`);
3153
- killReason = "context";
3154
- spawned.kill();
3165
+ // Context % prefer authoritative contextUsage from pi ≥ 0.63.0,
3166
+ // fall back to manual calculation from totalTokens + cacheRead.
3167
+ {
3168
+ const pct = delta.contextUsage
3169
+ ? delta.contextUsage.percentUsed
3170
+ : (delta.latestTotalTokens > 0 && contextWindow > 0)
3171
+ ? (delta.latestTotalTokens / contextWindow) * 100
3172
+ : 0;
3173
+ if (pct > 0) {
3174
+ state.workerContextPct = pct;
3175
+ if (pct >= warnPct) {
3176
+ writeWrapUpSignal(`Wrap up (context ${Math.round(pct)}%)`);
3177
+ }
3178
+ if (pct >= killPct && state.workerStatus === "running") {
3179
+ console.error(`[task-runner] tmux worker: context limit (${Math.round(pct)}%) — killing session '${sessionName}'`);
3180
+ killReason = "context";
3181
+ spawned.kill();
3182
+ }
3155
3183
  }
3156
3184
  }
3157
3185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.20.6",
3
+ "version": "0.21.1",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",