pi-harness-runtime 1.1.439 → 1.1.440

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 (125) hide show
  1. package/harness/e2e/auto-test-loop.ts +6 -2
  2. package/harness/e2e/tools-detector.test.ts +0 -1
  3. package/harness/index.ts +2 -1
  4. package/harness/langchain/scoreboard.test.ts +44 -22
  5. package/harness/master-planner.ts +1 -1
  6. package/harness/output-limit-handler.ts +1 -1
  7. package/harness/project-detector/detector.ts +2 -4
  8. package/harness/task-graph.ts +460 -113
  9. package/index.ts +14 -7
  10. package/package.json +1 -1
  11. package/packages/a2a-adapter/package.json +1 -1
  12. package/packages/agent-policy/package.json +1 -1
  13. package/packages/architecture-generator/package.json +1 -1
  14. package/packages/auth/package.json +1 -1
  15. package/packages/autonomous-refactor/package.json +1 -1
  16. package/packages/autonomous-runtime/package.json +1 -1
  17. package/packages/cache-strategy/package.json +1 -1
  18. package/packages/capability-registry/package.json +1 -1
  19. package/packages/checkpoint/package.json +1 -1
  20. package/packages/cli-plugin-sdk/package.json +1 -1
  21. package/packages/clipboard/package.json +1 -1
  22. package/packages/clipboard-plugin/dist/types.d.ts +9 -2
  23. package/packages/clipboard-plugin/dist/types.d.ts.map +1 -1
  24. package/packages/clipboard-plugin/package.json +1 -1
  25. package/packages/clipboard-plugin/src/types.ts +9 -2
  26. package/packages/code-generation/package.json +1 -1
  27. package/packages/code-review/package.json +1 -1
  28. package/packages/codex-adapter/package.json +1 -1
  29. package/packages/codex-watcher/package.json +1 -1
  30. package/packages/config-capture/package.json +1 -1
  31. package/packages/connection-runtime/package.json +1 -1
  32. package/packages/context-compiler/package.json +1 -1
  33. package/packages/context-discovery/package.json +1 -1
  34. package/packages/context-manager/package.json +1 -1
  35. package/packages/contracts/package.json +1 -1
  36. package/packages/cookie-sanitizer/package.json +1 -1
  37. package/packages/cost-optimizer/package.json +1 -1
  38. package/packages/dependency-analyzer/package.json +1 -1
  39. package/packages/django-plugin/package.json +1 -1
  40. package/packages/doc-generator/package.json +1 -1
  41. package/packages/durable-review/package.json +1 -1
  42. package/packages/e2e-tester/package.json +1 -1
  43. package/packages/evaluation-engine/package.json +1 -1
  44. package/packages/evaluation-runner/package.json +1 -1
  45. package/packages/event-bus/package.json +1 -1
  46. package/packages/event-sink/package.json +1 -1
  47. package/packages/event-store/package.json +1 -1
  48. package/packages/experience-replay/package.json +1 -1
  49. package/packages/feedback-collector/package.json +1 -1
  50. package/packages/file-copy-helper/package.json +1 -1
  51. package/packages/framework-detector/package.json +1 -1
  52. package/packages/framework-plugin-sdk/package.json +1 -1
  53. package/packages/frappe-plugin/package.json +1 -1
  54. package/packages/generic-web-plugin/package.json +1 -1
  55. package/packages/health-monitor/package.json +1 -1
  56. package/packages/intent-analyzer/package.json +1 -1
  57. package/packages/jev-judge/package.json +1 -1
  58. package/packages/knowledge-graph/package.json +1 -1
  59. package/packages/knowledge-retrieval/package.json +1 -1
  60. package/packages/laravel-plugin/package.json +1 -1
  61. package/packages/learning-engine/package.json +1 -1
  62. package/packages/logging/package.json +1 -1
  63. package/packages/mcp-adapter/package.json +1 -1
  64. package/packages/memory-engine/package.json +1 -1
  65. package/packages/milestone-manager/package.json +1 -1
  66. package/packages/model-registry/package.json +1 -1
  67. package/packages/nextjs-plugin/package.json +1 -1
  68. package/packages/notification/package.json +1 -1
  69. package/packages/observability/package.json +1 -1
  70. package/packages/okf-indexer/package.json +1 -1
  71. package/packages/paperclip-integration/package.json +1 -1
  72. package/packages/performance-optimizer/package.json +1 -1
  73. package/packages/permission-mode/package.json +1 -1
  74. package/packages/privilege-broker/package.json +1 -1
  75. package/packages/project-analyzer/package.json +1 -1
  76. package/packages/project-bootstrap/package.json +1 -1
  77. package/packages/projection-engine/package.json +1 -1
  78. package/packages/prompt-compiler/package.json +1 -1
  79. package/packages/prompt-versioning/package.json +1 -1
  80. package/packages/provider-adapter-sdk/package.json +1 -1
  81. package/packages/provider-router/package.json +1 -1
  82. package/packages/provider-selector/package.json +1 -1
  83. package/packages/providers/package.json +1 -1
  84. package/packages/qdrant-skills/package.json +1 -1
  85. package/packages/quota-manager/package.json +1 -1
  86. package/packages/rate-limiter/package.json +1 -1
  87. package/packages/react-vite-plugin/package.json +1 -1
  88. package/packages/relay/package.json +1 -1
  89. package/packages/release-manager/package.json +1 -1
  90. package/packages/requirement-compiler/package.json +1 -1
  91. package/packages/runtime/package.json +1 -1
  92. package/packages/scheduler/package.json +1 -1
  93. package/packages/scheduler-adapter/package.json +1 -1
  94. package/packages/session/package.json +1 -1
  95. package/packages/session-api/package.json +1 -1
  96. package/packages/session-export/package.json +1 -1
  97. package/packages/shared-context/package.json +1 -1
  98. package/packages/skill-mcp-client/package.json +1 -1
  99. package/packages/skill-registry/package.json +1 -1
  100. package/packages/skills/package.json +1 -1
  101. package/packages/sprint-planner/package.json +1 -1
  102. package/packages/subscription-engine/package.json +1 -1
  103. package/packages/task-compiler/package.json +1 -1
  104. package/packages/tencentdb-memory/package.json +1 -1
  105. package/packages/tencentdb-sync/package.json +1 -1
  106. package/packages/test-data-generator/package.json +1 -1
  107. package/packages/test-generator/package.json +1 -1
  108. package/packages/todo-bd-sync/package.json +1 -1
  109. package/packages/token-estimation/package.json +1 -1
  110. package/packages/token-optimizer/package.json +1 -1
  111. package/packages/tooling-preference/package.json +1 -1
  112. package/packages/trajectory/package.json +1 -1
  113. package/packages/tui/package.json +1 -1
  114. package/packages/two-phase-review/package.json +1 -1
  115. package/packages/types/package.json +1 -1
  116. package/packages/voice-api/package.json +1 -1
  117. package/packages/webmcp-tester/package.json +1 -1
  118. package/packages/wiki/package.json +1 -1
  119. package/packages/workflow-events/package.json +1 -1
  120. package/packages/workspace-scanner/package.json +1 -1
  121. package/packages/worktree/package.json +1 -1
  122. package/packages/write-review/package.json +1 -1
  123. package/scripts/release-all.ts +5 -3
  124. package/scripts/typecheck-packages.mjs +7 -4
  125. package/scripts/verify-package-contents.ts +13 -1
@@ -1,143 +1,490 @@
1
1
  /**
2
- * Task Graph Manager — RFC-0028 Phase 3
2
+ * Task Graph — RFC-0016 (DAG) with the RFC-0028 compact view.
3
3
  *
4
- * Manages task dependency graph for compact-aware scheduling.
5
- * Currently a stub — integrates with CompactOrchestrator for
6
- * dependency-aware compaction prioritization.
4
+ * There is one implementation and one API. The concrete RFC-0016 graph owns
5
+ * the dependency tracking, the status transitions, the retry state, the
6
+ * persistence and the topological order. RFC-0028 only added a
7
+ * compact-priority view on top of a task list, so that view is adapted onto
8
+ * the concrete task nodes here instead of living in a second, incompatible
9
+ * implementation.
7
10
  *
8
- * Future work:
9
- * - Track task dependencies (blocking/ready tasks)
10
- * - Prioritize compaction to keep blocking path messages
11
- * - Track which tasks depend on which checkpoint artifacts
11
+ * A task is READY only when all of its dependencies are DONE.
12
12
  */
13
13
 
14
- import type { RuntimeTask } from "../packages/types/src/runtime-types.js";
14
+ import type {
15
+ TaskGraph,
16
+ TaskNode,
17
+ TaskStatus,
18
+ } from "../packages/types/dist/runtime-types.js";
19
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { dirname, join } from "node:path";
15
21
 
16
- export interface TaskNode {
17
- task: RuntimeTask;
18
- dependencies: Set<string>;
19
- dependents: Set<string>;
20
- status: "pending" | "ready" | "running" | "done" | "blocked";
22
+ /**
23
+ * Write a JSON file, creating the parent directory when needed.
24
+ *
25
+ * The two helpers below are local on purpose: this module is loaded by the
26
+ * node test suite, and node resolves a module path literally. A cross
27
+ * directory import would either need the compiled ".js" convention (which
28
+ * node cannot map back to a TypeScript source) or a ".ts" specifier (which
29
+ * the compiler rejects unless a project opts into it).
30
+ */
31
+ function writeJsonFile(path: string, data: unknown): void {
32
+ mkdirSync(dirname(path), { recursive: true });
33
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
34
+ }
35
+
36
+ /** Read a JSON file. Returns null when it is missing or unreadable. */
37
+ function readJsonFile(path: string): unknown | null {
38
+ try {
39
+ return JSON.parse(readFileSync(path, "utf-8"));
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ export interface TaskGraphOptions {
46
+ jobId: string;
47
+ rootDir?: string;
48
+ }
49
+
50
+ /**
51
+ * A task that is added to the graph.
52
+ *
53
+ * `status` is optional. When it is missing the status is derived from the
54
+ * dependencies: a task without dependencies is READY, a task with
55
+ * dependencies waits in PENDING until they are done.
56
+ */
57
+ export interface TaskDescription {
58
+ id: string;
59
+ title: string;
60
+ description: string;
61
+ status?: TaskStatus;
62
+ dependencies?: string[];
63
+ acceptanceCriteria?: string[];
21
64
  }
22
65
 
23
- export interface TaskGraphManager {
24
- /** Add a task to the graph */
25
- addTask(task: RuntimeTask): void;
66
+ export class TaskGraphManager {
67
+ private graph: TaskGraph;
68
+ private dirty = false;
69
+
70
+ constructor(options: TaskGraphOptions) {
71
+ this.graph = {
72
+ jobId: options.jobId,
73
+ nodes: {},
74
+ topologicalOrder: [],
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Wrap an existing graph, for example a planned graph or a loaded one.
80
+ * The manager then works on that graph instead of an empty one.
81
+ */
82
+ static fromGraph(graph: TaskGraph): TaskGraphManager {
83
+ const manager = new TaskGraphManager({ jobId: graph.jobId });
84
+ manager.graph = graph;
85
+ return manager;
86
+ }
87
+
88
+ /**
89
+ * Add a task node to the graph.
90
+ *
91
+ * Both call forms are supported: the planner describes a task by its
92
+ * parts, and the runtime hands over a task object it already holds.
93
+ */
94
+ addTask(task: TaskDescription): TaskNode;
95
+ addTask(
96
+ id: string,
97
+ title: string,
98
+ description: string,
99
+ dependencies?: string[],
100
+ acceptanceCriteria?: string[],
101
+ ): TaskNode;
102
+ addTask(
103
+ taskOrId: TaskDescription | string,
104
+ title?: string,
105
+ description?: string,
106
+ dependencies: string[] = [],
107
+ acceptanceCriteria?: string[],
108
+ ): TaskNode {
109
+ const spec: TaskDescription =
110
+ typeof taskOrId === "string"
111
+ ? {
112
+ id: taskOrId,
113
+ title: title ?? "",
114
+ description: description ?? "",
115
+ dependencies,
116
+ acceptanceCriteria,
117
+ }
118
+ : taskOrId;
119
+
120
+ const id = spec.id;
121
+ if (this.graph.nodes[id]) {
122
+ throw new Error(`Task ${id} already exists`);
123
+ }
124
+
125
+ const deps = spec.dependencies ?? [];
126
+
127
+ // Validate dependencies exist
128
+ for (const depId of deps) {
129
+ if (!this.graph.nodes[depId] && depId !== id) {
130
+ console.warn(
131
+ `Warning: dependency ${depId} does not exist yet (task ${id})`,
132
+ );
133
+ }
134
+ }
135
+
136
+ const now = new Date().toISOString();
137
+ const node: TaskNode = {
138
+ id,
139
+ title: spec.title,
140
+ description: spec.description,
141
+ status: spec.status ?? (deps.length === 0 ? "ready" : "pending"),
142
+ dependencies: deps,
143
+ dependents: [],
144
+ acceptanceCriteria: spec.acceptanceCriteria,
145
+ retryCount: 0,
146
+ maxRetries: 3,
147
+ createdAt: now,
148
+ updatedAt: now,
149
+ };
150
+
151
+ // Update dependents of dependencies
152
+ for (const depId of deps) {
153
+ const dependency = this.graph.nodes[depId];
154
+ if (dependency && !dependency.dependents.includes(id)) {
155
+ dependency.dependents.push(id);
156
+ }
157
+ }
158
+
159
+ this.graph.nodes[id] = node;
160
+
161
+ // A task may name a dependency before that dependency is added. Now
162
+ // that the dependency is here, the waiting task becomes its dependent.
163
+ for (const other of Object.values(this.graph.nodes)) {
164
+ if (
165
+ other.id !== id &&
166
+ other.dependencies.includes(id) &&
167
+ !node.dependents.includes(other.id)
168
+ ) {
169
+ node.dependents.push(other.id);
170
+ }
171
+ }
172
+ this.recomputeTopologicalOrder();
173
+ this.dirty = true;
174
+
175
+ return node;
176
+ }
177
+
178
+ /**
179
+ * Update task status
180
+ */
181
+ updateTaskStatus(taskId: string, status: TaskStatus): TaskNode | null {
182
+ const node = this.graph.nodes[taskId];
183
+ if (!node) return null;
184
+
185
+ node.status = status;
186
+ node.updatedAt = new Date().toISOString();
187
+
188
+ // If task is done, check if dependents can now be ready
189
+ if (status === "done") {
190
+ this.updateDependentStatuses(taskId);
191
+ }
192
+
193
+ this.recomputeTopologicalOrder();
194
+ this.dirty = true;
195
+ return node;
196
+ }
197
+
198
+ /**
199
+ * Mark a task as done and update the tasks that wait for it.
200
+ */
201
+ completeTask(taskId: string): TaskNode | null {
202
+ return this.updateTaskStatus(taskId, "done");
203
+ }
26
204
 
27
- /** Mark a task as done and update dependents */
28
- completeTask(taskId: string): void;
205
+ /**
206
+ * Update dependent tasks when a dependency is completed
207
+ */
208
+ private updateDependentStatuses(completedTaskId: string): void {
209
+ for (const dependentId of this.graph.nodes[completedTaskId]?.dependents ??
210
+ []) {
211
+ const dependent = this.graph.nodes[dependentId];
212
+ if (!dependent) continue;
29
213
 
30
- /** Get tasks ready for execution */
31
- getReadyTasks(): RuntimeTask[];
214
+ // Check if ALL dependencies are done
215
+ const allDepsDone = dependent.dependencies.every(
216
+ (depId) => this.graph.nodes[depId]?.status === "done",
217
+ );
32
218
 
33
- /** Get tasks blocked by a given task */
34
- getBlockedTasks(taskId: string): RuntimeTask[];
219
+ if (allDepsDone && dependent.status === "pending") {
220
+ dependent.status = "ready";
221
+ dependent.updatedAt = new Date().toISOString();
222
+ }
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Get all tasks ready for execution (status = "ready")
228
+ */
229
+ getReadyTasks(): TaskNode[] {
230
+ return Object.values(this.graph.nodes).filter((n) => n.status === "ready");
231
+ }
232
+
233
+ /**
234
+ * Get the blocked tasks.
235
+ *
236
+ * Without an argument this returns every task that still waits for a
237
+ * dependency. With a task id it returns the tasks that wait for that
238
+ * task: once the task is done, nothing is blocked by it any more.
239
+ */
240
+ getBlockedTasks(taskId?: string): TaskNode[] {
241
+ if (taskId === undefined) {
242
+ return Object.values(this.graph.nodes).filter(
243
+ (node) => node.status === "pending" || node.status === "blocked",
244
+ );
245
+ }
246
+ const node = this.graph.nodes[taskId];
247
+ if (!node || node.status === "done") return [];
248
+ return node.dependents
249
+ .map((id) => this.graph.nodes[id])
250
+ .filter((n): n is TaskNode => n !== undefined && n.status !== "done");
251
+ }
252
+
253
+ /**
254
+ * Get all tasks that still wait for a dependency.
255
+ */
256
+ getPendingTasks(): TaskNode[] {
257
+ return Object.values(this.graph.nodes).filter((n) => n.status === "pending");
258
+ }
259
+
260
+ /**
261
+ * Get task by ID
262
+ */
263
+ getTask(taskId: string): TaskNode | null {
264
+ return this.graph.nodes[taskId] ?? null;
265
+ }
266
+
267
+ /**
268
+ * Get all tasks
269
+ */
270
+ getAllTasks(): TaskNode[] {
271
+ return Object.values(this.graph.nodes);
272
+ }
273
+
274
+ /**
275
+ * Get topological order of tasks
276
+ */
277
+ getTopologicalOrder(): string[] {
278
+ return this.graph.topologicalOrder;
279
+ }
280
+
281
+ /**
282
+ * Check if all tasks are done
283
+ */
284
+ isComplete(): boolean {
285
+ return Object.values(this.graph.nodes).every((n) => n.status === "done");
286
+ }
35
287
 
36
- /** Get all pending tasks */
37
- getPendingTasks(): RuntimeTask[];
288
+ /**
289
+ * Check if there are any failed tasks
290
+ */
291
+ hasFailedTasks(): boolean {
292
+ return Object.values(this.graph.nodes).some((n) => n.status === "failed");
293
+ }
38
294
 
39
- /** Get all tasks */
40
- getAllTasks(): RuntimeTask[];
295
+ /**
296
+ * Get failed tasks
297
+ */
298
+ getFailedTasks(): TaskNode[] {
299
+ return Object.values(this.graph.nodes).filter((n) => n.status === "failed");
300
+ }
41
301
 
42
- /** Get current progress summary */
302
+ /**
303
+ * Increment retry count for a task
304
+ */
305
+ incrementRetry(taskId: string): TaskNode | null {
306
+ const node = this.graph.nodes[taskId];
307
+ if (!node) return null;
308
+ node.retryCount = (node.retryCount ?? 0) + 1;
309
+ node.updatedAt = new Date().toISOString();
310
+ this.dirty = true;
311
+ return node;
312
+ }
313
+
314
+ /**
315
+ * Assign agent to task
316
+ */
317
+ assignAgent(
318
+ taskId: string,
319
+ agentId: string,
320
+ worktreePath?: string,
321
+ ): TaskNode | null {
322
+ const node = this.graph.nodes[taskId];
323
+ if (!node) return null;
324
+ node.assignedAgent = agentId;
325
+ if (worktreePath) node.worktreePath = worktreePath;
326
+ node.updatedAt = new Date().toISOString();
327
+ this.dirty = true;
328
+ return node;
329
+ }
330
+
331
+ /**
332
+ * Unassign agent from task
333
+ */
334
+ unassignAgent(taskId: string): TaskNode | null {
335
+ const node = this.graph.nodes[taskId];
336
+ if (!node) return null;
337
+ node.assignedAgent = undefined;
338
+ node.worktreePath = undefined;
339
+ node.updatedAt = new Date().toISOString();
340
+ this.dirty = true;
341
+ return node;
342
+ }
343
+
344
+ /**
345
+ * Check if task can be retried
346
+ */
347
+ canRetry(taskId: string): boolean {
348
+ const node = this.graph.nodes[taskId];
349
+ if (!node) return false;
350
+ const retries = node.retryCount ?? 0;
351
+ const max = node.maxRetries ?? 3;
352
+ return retries < max;
353
+ }
354
+
355
+ /**
356
+ * Get progress summary
357
+ */
43
358
  getProgressSummary(): {
44
359
  total: number;
45
360
  done: number;
46
- running: number;
47
361
  failed: number;
48
- };
362
+ pending: number;
363
+ running: number;
364
+ } {
365
+ const tasks = Object.values(this.graph.nodes);
366
+ return {
367
+ total: tasks.length,
368
+ done: tasks.filter((t) => t.status === "done").length,
369
+ failed: tasks.filter((t) => t.status === "failed").length,
370
+ pending: tasks.filter(
371
+ (t) => t.status === "pending" || t.status === "ready",
372
+ ).length,
373
+ running: tasks.filter(
374
+ (t) =>
375
+ t.status === "running" ||
376
+ t.status === "testing" ||
377
+ t.status === "reviewing",
378
+ ).length,
379
+ };
380
+ }
49
381
 
50
- /** Get compact priority: tasks to keep in context vs prune */
382
+ /**
383
+ * Get compact priority: the task ids to keep in the context and the
384
+ * task ids that can be pruned from it.
385
+ */
51
386
  getCompactPriority(): {
52
387
  keep: string[];
53
388
  prune: string[];
54
- };
55
- }
389
+ } {
390
+ const tasks = Object.values(this.graph.nodes);
391
+ return {
392
+ keep: tasks.filter((t) => t.status === "done").map((t) => t.id),
393
+ prune: tasks.filter((t) => t.status === "pending").map((t) => t.id),
394
+ };
395
+ }
56
396
 
57
- /**
58
- * Creates a new TaskGraphManager instance.
59
- * Stub implementation — returns minimal working graph.
60
- */
61
- export function createTaskGraphManager(): TaskGraphManager {
62
- const nodes = new Map<string, TaskNode>();
63
-
64
- return {
65
- addTask(task: RuntimeTask) {
66
- if (!nodes.has(task.id)) {
67
- nodes.set(task.id, {
68
- task,
69
- dependencies: new Set(),
70
- dependents: new Set(),
71
- status: "pending",
72
- });
73
- }
74
- },
397
+ /**
398
+ * Save graph to file
399
+ */
400
+ async save(rootDir: string): Promise<void> {
401
+ const path = join(rootDir, "jobs", this.graph.jobId, "task-graph.json");
402
+ writeJsonFile(path, this.graph);
403
+ this.dirty = false;
404
+ }
75
405
 
76
- completeTask(taskId: string) {
77
- const node = nodes.get(taskId);
78
- if (!node) return;
406
+ /**
407
+ * Load graph from file
408
+ */
409
+ static async load(jobId: string, rootDir: string): Promise<TaskGraphManager> {
410
+ const path = join(rootDir, "jobs", jobId, "task-graph.json");
411
+ const data = readJsonFile(path) as TaskGraph | null;
79
412
 
80
- node.status = "done";
413
+ const manager = new TaskGraphManager({ jobId });
414
+ if (data?.nodes) {
415
+ manager.graph = data as TaskGraph;
416
+ }
81
417
 
82
- // Update dependents to ready
83
- for (const depId of node.dependents) {
84
- const depNode = nodes.get(depId);
85
- if (!depNode) continue;
418
+ return manager;
419
+ }
86
420
 
87
- // Check if all dependencies are met
88
- const allDepsMet = [...depNode.dependencies].every(
89
- (depId) => nodes.get(depId)?.status === "done",
90
- );
91
- if (allDepsMet) {
92
- depNode.status = "ready";
421
+ /**
422
+ * Check if graph has unsaved changes
423
+ */
424
+ isDirty(): boolean {
425
+ return this.dirty;
426
+ }
427
+
428
+ /**
429
+ * Get the full graph
430
+ */
431
+ getGraph(): TaskGraph {
432
+ return this.graph;
433
+ }
434
+
435
+ /**
436
+ * Compute topological order using Kahn's algorithm
437
+ */
438
+ private recomputeTopologicalOrder(): void {
439
+ const order: string[] = [];
440
+ const inDegree: Record<string, number> = {};
441
+ const nodes = this.graph.nodes;
442
+
443
+ // Initialize in-degrees. A dependency that is not in the graph yet
444
+ // does not hold a task back: the order is recomputed when it arrives.
445
+ for (const id of Object.keys(nodes)) {
446
+ inDegree[id] = nodes[id].dependencies.filter(
447
+ (depId) => nodes[depId] !== undefined,
448
+ ).length;
449
+ }
450
+
451
+ // Start with nodes that have no dependencies
452
+ const queue: string[] = [];
453
+ for (const id of Object.keys(nodes)) {
454
+ if (inDegree[id] === 0) {
455
+ queue.push(id);
456
+ }
457
+ }
458
+
459
+ while (queue.length > 0) {
460
+ const current = queue.shift()!;
461
+ order.push(current);
462
+
463
+ for (const dependent of nodes[current].dependents) {
464
+ inDegree[dependent]--;
465
+ if (inDegree[dependent] === 0) {
466
+ queue.push(dependent);
93
467
  }
94
468
  }
95
- },
96
-
97
- getReadyTasks() {
98
- return [...nodes.values()]
99
- .filter((n) => n.status === "ready")
100
- .map((n) => n.task);
101
- },
102
-
103
- getBlockedTasks(_taskId: string) {
104
- // Stub: return empty array
105
- return [];
106
- },
107
-
108
- getPendingTasks() {
109
- return [...nodes.values()]
110
- .filter((n) => n.status === "pending")
111
- .map((n) => n.task);
112
- },
113
-
114
- getAllTasks() {
115
- return [...nodes.values()].map((n) => n.task);
116
- },
117
-
118
- getProgressSummary() {
119
- const allNodes = [...nodes.values()];
120
- return {
121
- total: allNodes.length,
122
- done: allNodes.filter((n) => n.status === "done").length,
123
- running: allNodes.filter((n) => n.status === "running").length,
124
- failed: allNodes.filter((n) => n.status === "blocked").length,
125
- };
126
- },
127
-
128
- getCompactPriority() {
129
- // Stub: keep done tasks, prune pending ones
130
- const done = [...nodes.values()]
131
- .filter((n) => n.status === "done")
132
- .map((n) => n.task.id);
133
- const pending = [...nodes.values()]
134
- .filter((n) => n.status === "pending")
135
- .map((n) => n.task.id);
136
-
137
- return {
138
- keep: done, // Keep done task context (they're relevant)
139
- prune: pending, // Prune pending task messages first
140
- };
141
- },
142
- };
469
+ }
470
+
471
+ // Check for cycles (shouldn't happen with valid input)
472
+ if (order.length !== Object.keys(nodes).length) {
473
+ console.warn(
474
+ "Warning: topological sort detected a cycle in the task graph",
475
+ );
476
+ }
477
+
478
+ this.graph.topologicalOrder = order;
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Create a task graph manager. The job id is optional for the callers that
484
+ * only keep the graph in memory.
485
+ */
486
+ export function createTaskGraphManager(
487
+ options: Partial<TaskGraphOptions> = {},
488
+ ): TaskGraphManager {
489
+ return new TaskGraphManager({ jobId: options.jobId ?? "" });
143
490
  }
package/index.ts CHANGED
@@ -86,10 +86,7 @@ import {
86
86
  JobStateMachine,
87
87
  type CheckpointManager,
88
88
  } from "./harness/job-state-machine.ts";
89
- import {
90
- createTaskGraphManager,
91
- type TaskGraphManager,
92
- } from "./harness/task-graph.js";
89
+ import { TaskGraphManager } from "./harness/task-graph.js";
93
90
  import { MasterPlanner } from "./harness/master-planner.ts";
94
91
  import { RepairEngine } from "./harness/repair-engine.ts";
95
92
  import {
@@ -3351,7 +3348,7 @@ async function initMemory(pi: ExtensionAPI): Promise<void> {
3351
3348
 
3352
3349
  if (!allMemory.trim()) {
3353
3350
  logToFile("[memory]", "No memory to inject");
3354
- return;
3351
+ return undefined;
3355
3352
  }
3356
3353
 
3357
3354
  // Status on first injection
@@ -4815,6 +4812,9 @@ Server process examples that must be detached: uvicorn, fastapi dev server, guni
4815
4812
  // Return the additional prompt to append to systemPrompt
4816
4813
  return { systemPrompt: event.systemPrompt + "\n" + additionalPrompt };
4817
4814
  }
4815
+
4816
+ // Only the first turn carries the additional prompt.
4817
+ return undefined;
4818
4818
  });
4819
4819
 
4820
4820
  // --- Auto-Todo Reminder on Build Commands ---------------------------
@@ -5813,7 +5813,7 @@ Run \`bd ready\` to see current bd issues.
5813
5813
  jobId,
5814
5814
  requirement,
5815
5815
  machine,
5816
- graph: createTaskGraphManager(),
5816
+ graph: TaskGraphManager.fromGraph(planResult.graph!),
5817
5817
  blackboard,
5818
5818
  repairEngine,
5819
5819
  createdAt: new Date().toISOString(),
@@ -5829,7 +5829,14 @@ Run \`bd ready\` to see current bd issues.
5829
5829
  "info",
5830
5830
  );
5831
5831
  } catch (e) {
5832
- ctx.ui.notify(`Error starting harness: ${e}`, "error");
5832
+ // The UI can already be gone. A failing notify must not hide
5833
+ // the original error, so the log line is the durable report.
5834
+ logToFile("[Harness]", `Error starting harness: ${e}`);
5835
+ try {
5836
+ ctx.ui.notify(`Error starting harness: ${e}`, "error");
5837
+ } catch {
5838
+ // The UI is not available; the log line above is the report.
5839
+ }
5833
5840
  }
5834
5841
  },
5835
5842
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "1.1.439",
3
+ "version": "1.1.440",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/a2a-adapter",
3
- "version": "1.1.439",
3
+ "version": "1.1.440",
4
4
  "type": "module",
5
5
  "description": "Google Agent-to-Agent (A2A) protocol adapter for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/agent-policy",
3
- "version": "1.1.439",
3
+ "version": "1.1.440",
4
4
  "description": "Agent policy runtime for pi-harness - enforces mandatory rule delivery and mutation gating",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/architecture-generator",
3
- "version": "1.1.439",
3
+ "version": "1.1.440",
4
4
  "description": "Generate ADR and Mermaid diagrams from project analysis",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",