taskplane 0.25.8 → 0.26.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.
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Sidecar Telemetry Utilities
3
+ *
4
+ * Canonical home for sidecar JSONL tailing and telemetry delta parsing.
5
+ *
6
+ * These utilities are used by the orchestrator to poll live worker telemetry
7
+ * (token usage, tool calls, retry state) from sidecar JSONL files written by
8
+ * the pi coding agent during task execution.
9
+ *
10
+ * @since TP-161
11
+ */
12
+
13
+ import { existsSync, mkdirSync, statSync, openSync, readSync, closeSync } from "fs";
14
+ import { join, dirname } from "path";
15
+
16
+ // ── Sidecar Directory Resolution ──────────────────────────────────────
17
+
18
+ /**
19
+ * Returns the .pi directory path for sidecar files (lane state, conversation logs).
20
+ * In orchestrated mode, the orchestrator passes ORCH_SIDECAR_DIR pointing to the
21
+ * MAIN repo's .pi/ directory (not the worktree's).
22
+ */
23
+ export function getSidecarDir(): string {
24
+ // Orchestrator provides the main repo .pi path
25
+ const orchDir = process.env.ORCH_SIDECAR_DIR;
26
+ if (orchDir) {
27
+ if (!existsSync(orchDir)) mkdirSync(orchDir, { recursive: true });
28
+ return orchDir;
29
+ }
30
+ // Fallback: walk up from cwd
31
+ let dir = process.cwd();
32
+ for (let i = 0; i < 10; i++) {
33
+ const piDir = join(dir, ".pi");
34
+ if (existsSync(piDir)) return piDir;
35
+ const parent = dirname(dir);
36
+ if (parent === dir) break;
37
+ dir = parent;
38
+ }
39
+ const piDir = join(process.cwd(), ".pi");
40
+ if (!existsSync(piDir)) mkdirSync(piDir, { recursive: true });
41
+ return piDir;
42
+ }
43
+
44
+ // ── Sidecar Tail State ────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Mutable state for incremental byte-offset sidecar JSONL reading.
48
+ * One instance per sidecar file, persists across poll ticks within a session.
49
+ */
50
+ export interface SidecarTailState {
51
+ /** Byte offset of the next unread position in the sidecar file */
52
+ offset: number;
53
+ /** Partial trailing line from the last read (incomplete JSONL line) */
54
+ partial: string;
55
+ /** Whether a retry is currently active (persisted across ticks) */
56
+ retryActive: boolean;
57
+ }
58
+
59
+ export function createSidecarTailState(): SidecarTailState {
60
+ return { offset: 0, partial: "", retryActive: false };
61
+ }
62
+
63
+ // ── Sidecar Telemetry Delta ───────────────────────────────────────────
64
+
65
+ /**
66
+ * Parsed telemetry accumulated from sidecar JSONL events.
67
+ * Returned by tailSidecarJsonl() on each tick.
68
+ */
69
+ export interface SidecarTelemetryDelta {
70
+ /** Per-turn input tokens (sum of new message_end events in this tick) */
71
+ inputTokens: number;
72
+ outputTokens: number;
73
+ cacheReadTokens: number;
74
+ cacheWriteTokens: number;
75
+ /** Incremental cost from new message_end events */
76
+ cost: number;
77
+ /** Most recent totalTokens from message_end usage (cumulative, for context %) */
78
+ latestTotalTokens: number;
79
+ /** Tool calls observed in this tick */
80
+ toolCalls: number;
81
+ /** Last tool description from tool_execution_start */
82
+ lastTool: string;
83
+ /** Whether a retry is currently active (persisted across ticks via SidecarTailState) */
84
+ retryActive: boolean;
85
+ /** Total retries started in this tick */
86
+ retriesStarted: number;
87
+ /** Error message from the most recent auto_retry_start */
88
+ lastRetryError: string;
89
+ /** Whether any sidecar events were parsed in this tick (used for callback gating) */
90
+ hadEvents: boolean;
91
+ /** Authoritative context usage from pi get_session_stats (pi ≥ 0.63.0, null if unavailable) */
92
+ contextUsage: { percent: number; totalTokens: number; maxTokens: number } | null;
93
+ /** True when a get_session_stats response was seen but lacked contextUsage (older pi) */
94
+ sawStatsResponseWithoutContextUsage: boolean;
95
+ }
96
+
97
+ // ── Incremental JSONL Tailing ─────────────────────────────────────────
98
+
99
+ /**
100
+ * Incrementally read new lines from a sidecar JSONL file and parse telemetry events.
101
+ *
102
+ * O(new) per call — only reads bytes after the previous offset. Handles:
103
+ * - File not yet created (returns zero delta)
104
+ * - Empty reads (no new data since last tick)
105
+ * - Partial trailing lines (buffered for next call)
106
+ * - Malformed JSON lines (skipped with stderr warning, does not break iteration)
107
+ *
108
+ * The caller (poll loop) accumulates the returned deltas into TaskState.
109
+ */
110
+ export function tailSidecarJsonl(filePath: string, tailState: SidecarTailState): SidecarTelemetryDelta {
111
+ const delta: SidecarTelemetryDelta = {
112
+ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
113
+ cost: 0, latestTotalTokens: 0, toolCalls: 0, lastTool: "",
114
+ retryActive: tailState.retryActive, retriesStarted: 0, lastRetryError: "",
115
+ hadEvents: false, contextUsage: null, sawStatsResponseWithoutContextUsage: false,
116
+ };
117
+
118
+ // Gracefully handle missing file (wrapper hasn't written yet)
119
+ let fileSize: number;
120
+ try {
121
+ fileSize = statSync(filePath).size;
122
+ } catch {
123
+ return delta; // File doesn't exist yet — no-op
124
+ }
125
+
126
+ if (fileSize <= tailState.offset) {
127
+ return delta; // No new data
128
+ }
129
+
130
+ // Read new bytes from offset to end of file
131
+ const bytesToRead = fileSize - tailState.offset;
132
+ const buf = Buffer.alloc(bytesToRead);
133
+ let fd: number;
134
+ try {
135
+ fd = openSync(filePath, "r");
136
+ } catch {
137
+ return delta; // File became inaccessible between stat and open
138
+ }
139
+ try {
140
+ readSync(fd, buf, 0, bytesToRead, tailState.offset);
141
+ } catch {
142
+ closeSync(fd);
143
+ return delta; // Read error — try again next tick
144
+ }
145
+ closeSync(fd);
146
+ tailState.offset = fileSize;
147
+
148
+ // Split into lines, preserving any partial trailing line
149
+ const chunk = tailState.partial + buf.toString("utf-8");
150
+ const lines = chunk.split("\n");
151
+ // Last element is either "" (if chunk ended with \n) or a partial line
152
+ tailState.partial = lines.pop() || "";
153
+
154
+ for (const line of lines) {
155
+ const trimmed = line.trim();
156
+ if (!trimmed) continue;
157
+
158
+ let event: any;
159
+ try {
160
+ event = JSON.parse(trimmed);
161
+ } catch {
162
+ // Malformed JSON — skip silently (concurrent write race, truncated line)
163
+ continue;
164
+ }
165
+
166
+ if (!event || !event.type) continue;
167
+
168
+ delta.hadEvents = true;
169
+
170
+ switch (event.type) {
171
+ case "message_end": {
172
+ const usage = event.message?.usage;
173
+ if (usage) {
174
+ delta.inputTokens += usage.input || 0;
175
+ delta.outputTokens += usage.output || 0;
176
+ delta.cacheReadTokens += usage.cacheRead || 0;
177
+ delta.cacheWriteTokens += usage.cacheWrite || 0;
178
+ if (usage.cost) {
179
+ delta.cost += typeof usage.cost === "object"
180
+ ? (usage.cost.total || 0)
181
+ : (typeof usage.cost === "number" ? usage.cost : 0);
182
+ }
183
+ // totalTokens is cumulative (grows each turn) — use latest value.
184
+ // Include cacheRead tokens: pi's totalTokens and the
185
+ // input+output fallback both exclude cache reads, but cached
186
+ // tokens still consume context window capacity.
187
+ const rawTotal = usage.totalTokens
188
+ || ((usage.input || 0) + (usage.output || 0));
189
+ const totalTokens = rawTotal + (usage.cacheRead || 0);
190
+ if (totalTokens > delta.latestTotalTokens) {
191
+ delta.latestTotalTokens = totalTokens;
192
+ }
193
+ }
194
+ break;
195
+ }
196
+
197
+ case "tool_execution_start": {
198
+ delta.toolCalls++;
199
+ const toolDesc = event.toolName || "unknown";
200
+ let argPreview = "";
201
+ if (event.args) {
202
+ if (typeof event.args === "string") {
203
+ argPreview = event.args.slice(0, 80);
204
+ } else if (typeof event.args === "object") {
205
+ const firstVal = Object.values(event.args)[0];
206
+ if (typeof firstVal === "string") {
207
+ argPreview = (firstVal as string).slice(0, 80);
208
+ }
209
+ }
210
+ }
211
+ delta.lastTool = argPreview ? `${toolDesc} ${argPreview}` : toolDesc;
212
+ break;
213
+ }
214
+
215
+ case "auto_retry_start": {
216
+ delta.retriesStarted++;
217
+ delta.lastRetryError = event.errorMessage || event.error || "unknown";
218
+ tailState.retryActive = true;
219
+ break;
220
+ }
221
+
222
+ case "auto_retry_end": {
223
+ tailState.retryActive = false;
224
+ break;
225
+ }
226
+
227
+ case "response": {
228
+ // get_session_stats response from pi ≥ 0.63.0 — authoritative context usage
229
+ if (event.success === true && event.data?.contextUsage) {
230
+ const cu = event.data.contextUsage;
231
+ // pi sends `percent` (pi ≥ 0.63.0); accept `percentUsed` as legacy fallback
232
+ const pctValue = cu.percent ?? cu.percentUsed;
233
+ if (typeof pctValue === "number") {
234
+ delta.contextUsage = {
235
+ percent: pctValue,
236
+ totalTokens: cu.totalTokens || 0,
237
+ maxTokens: cu.maxTokens || 0,
238
+ };
239
+ }
240
+ } else if (event.success === true && event.data && !event.data.contextUsage) {
241
+ // Successful get_session_stats response but no contextUsage — older pi
242
+ delta.sawStatsResponseWithoutContextUsage = true;
243
+ }
244
+ break;
245
+ }
246
+ }
247
+ }
248
+
249
+ // Reflect persisted retry state into the delta for the caller
250
+ delta.retryActive = tailState.retryActive;
251
+ return delta;
252
+ }
@@ -335,6 +335,7 @@ git log --oneline orch/{branch}..task/{lane-branch} # empty = already merged
335
335
  cd /tmp/verify && cd extensions && node --experimental-strip-types --experimental-test-module-mocks --no-warnings --import ./tests/loader.mjs --test tests/*.test.ts
336
336
  ```
337
337
  5. Update batch state and advance.
338
+ 6. **IMPORTANT:** After any manual merge that completes the batch integration, always call `orch_integrate()` to record integration metadata (`integratedAt`, orch branch cleanup, batch history). Without this step, the dashboard will continue showing the batch in the active view rather than the history view, and `batch-state.json` will not reflect the completed integration.
338
339
 
339
340
  ### Pattern 1b: Merge Agent Stall (TP-056)
340
341
 
@@ -379,6 +380,7 @@ completion.
379
380
  - Add `mergeResults[N] = { waveIndex: N, status: "succeeded", ... }`
380
381
  - Advance `currentWaveIndex` past the merged wave
381
382
  - Set `phase = "paused"` for clean resume
383
+ 5. **IMPORTANT:** Once all waves are merged and the batch is complete, call `orch_integrate()` to record integration metadata. This ensures the dashboard moves the batch to history view and `integratedAt` is written to `batch-state.json`.
382
384
 
383
385
  ### Pattern 3: Resume Marks Pending Tasks as Failed
384
386
 
@@ -1958,7 +1958,7 @@ function parseSupervisorTemplate(filePath: string): { fm: Record<string, string>
1958
1958
  /**
1959
1959
  * Load a supervisor template: base (from package) + local override (from project).
1960
1960
  *
1961
- * Follows the same composition pattern as `loadAgentDef()` in task-runner.ts:
1961
+ * Follows the same composition pattern as `loadAgentDef()`:
1962
1962
  * - Base template: shipped in `templates/agents/{name}.md`
1963
1963
  * - Local override: `.pi/agents/{name}.md` in the project
1964
1964
  * - If local has `standalone: true`, use it exclusively
@@ -1,13 +1,11 @@
1
1
  /**
2
2
  * Task Executor Core — Headless execution semantics for Runtime V2
3
3
  *
4
- * This module owns the deterministic task execution logic that was
5
- * previously embedded inside the Pi extension host (task-runner.ts).
4
+ * This module owns the deterministic task execution logic for headless lane execution.
6
5
  * It has NO dependency on Pi's ExtensionAPI, ExtensionContext, UI
7
6
  * widgets, session lifecycle, TMUX, or TASK_AUTOSTART.
8
7
  *
9
8
  * Consumers:
10
- * - task-runner.ts (deprecated /task compatibility wrapper)
11
9
  * - lane-runner.ts (Runtime V2 headless lane execution, TP-105)
12
10
  *
13
11
  * Design rules:
@@ -29,8 +27,7 @@ import { spawnSync } from "child_process";
29
27
  /**
30
28
  * Parsed step information from PROMPT.md or STATUS.md.
31
29
  *
32
- * Re-exported from the core so consumers don't need to import
33
- * task-runner.ts for the type definition.
30
+ * Re-exported from the core for downstream consumers.
34
31
  */
35
32
  export interface StepInfo {
36
33
  number: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.25.8",
3
+ "version": "0.26.1",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -15,7 +15,6 @@
15
15
  },
16
16
  "pi": {
17
17
  "extensions": [
18
- "./extensions/task-runner.ts",
19
18
  "./extensions/task-orchestrator.ts"
20
19
  ],
21
20
  "skills": [
@@ -29,7 +28,6 @@
29
28
  "files": [
30
29
  "bin/",
31
30
  "dashboard/",
32
- "extensions/task-runner.ts",
33
31
  "extensions/task-orchestrator.ts",
34
32
  "extensions/reviewer-extension.ts",
35
33
  "extensions/taskplane/",