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.
- package/harness/e2e/auto-test-loop.ts +6 -2
- package/harness/e2e/tools-detector.test.ts +0 -1
- package/harness/index.ts +2 -1
- package/harness/langchain/scoreboard.test.ts +44 -22
- package/harness/master-planner.ts +1 -1
- package/harness/output-limit-handler.ts +1 -1
- package/harness/project-detector/detector.ts +2 -4
- package/harness/task-graph.ts +460 -113
- package/index.ts +14 -7
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/dist/types.d.ts +9 -2
- package/packages/clipboard-plugin/dist/types.d.ts.map +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/clipboard-plugin/src/types.ts +9 -2
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/release-all.ts +5 -3
- package/scripts/typecheck-packages.mjs +7 -4
- package/scripts/verify-package-contents.ts +13 -1
package/harness/task-graph.ts
CHANGED
|
@@ -1,143 +1,490 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Task Graph
|
|
2
|
+
* Task Graph — RFC-0016 (DAG) with the RFC-0028 compact view.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
/**
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
/**
|
|
37
|
-
|
|
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
|
-
/**
|
|
40
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
413
|
+
const manager = new TaskGraphManager({ jobId });
|
|
414
|
+
if (data?.nodes) {
|
|
415
|
+
manager.graph = data as TaskGraph;
|
|
416
|
+
}
|
|
81
417
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const depNode = nodes.get(depId);
|
|
85
|
-
if (!depNode) continue;
|
|
418
|
+
return manager;
|
|
419
|
+
}
|
|
86
420
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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:
|
|
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
|
-
|
|
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