@danypops/papyrus 0.7.0 → 0.9.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.
- package/README.md +21 -30
- package/extension/src/active-task-continuation.ts +9 -0
- package/extension/src/context-injection-telemetry.ts +80 -0
- package/extension/src/domain-tools.ts +15 -3
- package/extension/src/index.ts +38 -17
- package/extension/src/task-widget.ts +2 -1
- package/extension/src/tasks.ts +28 -20
- package/package.json +1 -1
- package/src/adapters/sqlite-artifact-store.ts +6 -1
- package/src/adapters/sqlite-task-focus-store.ts +27 -14
- package/src/cli.ts +60 -40
- package/src/constants.ts +10 -14
- package/src/daemon.ts +2 -18
- package/src/db.ts +12 -2
- package/src/domain/artifact.ts +6 -0
- package/src/domain/task-event.ts +5 -2
- package/src/ops.ts +17 -1
- package/src/ports/artifact-store.ts +2 -0
- package/src/ports/task-focus-store.ts +30 -8
- package/src/service.ts +33 -21
- package/src/task-service.ts +98 -29
- package/src/task-automation.ts +0 -188
package/src/task-service.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
TASK_BODY_MAX_LENGTH,
|
|
3
|
+
TASK_EXECUTION_MAX_DEGREE,
|
|
4
|
+
TASK_EXECUTION_MAX_EDGES,
|
|
5
|
+
TASK_EXECUTION_MAX_NODES,
|
|
6
|
+
TASK_LABEL_MAX_COUNT,
|
|
7
|
+
TASK_LABEL_MAX_LENGTH,
|
|
8
|
+
TASK_SCOPE_MAX_TASKS,
|
|
9
|
+
TASK_TITLE_MAX_LENGTH,
|
|
10
|
+
} from "./constants.ts";
|
|
2
11
|
import type { Artifact } from "./domain/artifact.ts";
|
|
3
12
|
import { checklistEntries, validateChecklist, type Checklist, type ProofReference } from "./domain/checklist.ts";
|
|
4
13
|
import type { Gate, GateResult } from "./domain/gate.ts";
|
|
@@ -6,11 +15,17 @@ import type { AppendTaskEvent, TaskEventContext, TaskHistoryPage, TaskHistoryQue
|
|
|
6
15
|
import { normalizeProjectRoot, taskScopeLabel, type TaskScopeSource, type TaskViewMode, type TaskViewSelection } from "./domain/task-scope.ts";
|
|
7
16
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
8
17
|
import type { GateRunner } from "./ports/gate-runner.ts";
|
|
9
|
-
import { InMemoryTaskFocusStore, type TaskFocusStore } from "./ports/task-focus-store.ts";
|
|
18
|
+
import { InMemoryTaskFocusStore, type TaskFocusStatus, type TaskFocusStore } from "./ports/task-focus-store.ts";
|
|
10
19
|
import { InMemoryTaskEventStore, type TaskEventStore } from "./ports/task-event-store.ts";
|
|
11
20
|
import { InMemoryTaskScopeStore, type TaskScopeStore } from "./ports/task-scope-store.ts";
|
|
12
21
|
import { assertDependencyEdgeAllowed } from "./task-execution.ts";
|
|
13
22
|
|
|
23
|
+
export interface UpdateTaskInput {
|
|
24
|
+
title?: string;
|
|
25
|
+
body?: string;
|
|
26
|
+
labels?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
export interface TaskFilter {
|
|
15
30
|
status?: string;
|
|
16
31
|
text?: string;
|
|
@@ -53,6 +68,13 @@ export interface ChecklistReview {
|
|
|
53
68
|
reason?: string;
|
|
54
69
|
}
|
|
55
70
|
|
|
71
|
+
export interface TaskFocus {
|
|
72
|
+
artifact: Artifact;
|
|
73
|
+
status: TaskFocusStatus;
|
|
74
|
+
updatedAt: string;
|
|
75
|
+
pauseReason?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
export interface TaskCompletionOptions {
|
|
57
79
|
focusSuccessor?: boolean;
|
|
58
80
|
gateDeadlineMs?: number;
|
|
@@ -70,6 +92,7 @@ export interface TaskCompletion {
|
|
|
70
92
|
export interface TaskNode {
|
|
71
93
|
task: Artifact;
|
|
72
94
|
active?: boolean;
|
|
95
|
+
focusStatus?: TaskFocusStatus;
|
|
73
96
|
parentIds: string[];
|
|
74
97
|
childIds: string[];
|
|
75
98
|
dependencyIds: string[];
|
|
@@ -138,6 +161,28 @@ export class Tasks {
|
|
|
138
161
|
});
|
|
139
162
|
}
|
|
140
163
|
|
|
164
|
+
update(id: string, input: UpdateTaskInput, context: TaskEventContext = {}): Artifact {
|
|
165
|
+
const fields = (["title", "body", "labels"] as const).filter((field) => input[field] !== undefined);
|
|
166
|
+
if (fields.length === 0) throw new Error("task update requires title, body, or labels");
|
|
167
|
+
if (input.title !== undefined && (input.title.trim().length === 0 || input.title.length > TASK_TITLE_MAX_LENGTH)) {
|
|
168
|
+
throw new Error(`title must be between 1 and ${TASK_TITLE_MAX_LENGTH} characters`);
|
|
169
|
+
}
|
|
170
|
+
if (input.body !== undefined && input.body.length > TASK_BODY_MAX_LENGTH) throw new Error(`body cannot exceed ${TASK_BODY_MAX_LENGTH} characters`);
|
|
171
|
+
if (input.labels !== undefined) {
|
|
172
|
+
if (input.labels.length > TASK_LABEL_MAX_COUNT) throw new Error(`labels cannot exceed ${TASK_LABEL_MAX_COUNT} entries`);
|
|
173
|
+
if (input.labels.some((label) => label.length === 0 || label.length > TASK_LABEL_MAX_LENGTH)) {
|
|
174
|
+
throw new Error(`each label must be between 1 and ${TASK_LABEL_MAX_LENGTH} characters`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return this.events.atomic(() => {
|
|
178
|
+
this.require(id);
|
|
179
|
+
const updated = this.artifacts.updateContent(id, input);
|
|
180
|
+
if (!updated) throw new Error(`task "${id}" not found`);
|
|
181
|
+
this.appendEvent({ taskId: id, type: "updated", evidence: { result: `fields:${fields.sort().join(",")}` } }, context);
|
|
182
|
+
return updated;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
141
186
|
list(filter: TaskFilter = {}): Artifact[] {
|
|
142
187
|
const selection = this.scopeSelection(filter.projectRoot, filter.scope, filter.rootTaskId);
|
|
143
188
|
const limit = filter.limit ?? TASK_SCOPE_MAX_TASKS;
|
|
@@ -204,10 +249,12 @@ export class Tasks {
|
|
|
204
249
|
throw new Error(`task execution graph exceeds ${TASK_EXECUTION_MAX_NODES} nodes`);
|
|
205
250
|
}
|
|
206
251
|
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
207
|
-
const
|
|
252
|
+
const focus = this.focusStore.get();
|
|
253
|
+
const focusedId = focus?.taskId;
|
|
208
254
|
const nodes = new Map(tasks.map((task) => [task.id, {
|
|
209
255
|
task,
|
|
210
256
|
active: task.id === focusedId,
|
|
257
|
+
...(task.id === focusedId ? { focusStatus: focus!.status } : {}),
|
|
211
258
|
parentIds: [] as string[],
|
|
212
259
|
childIds: [] as string[],
|
|
213
260
|
dependencyIds: [] as string[],
|
|
@@ -247,25 +294,60 @@ export class Tasks {
|
|
|
247
294
|
return this.artifacts.get(id, { tree: true })!;
|
|
248
295
|
}
|
|
249
296
|
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
if (!
|
|
253
|
-
const task = this.artifacts.get(
|
|
297
|
+
focused(filter?: TaskFilter): TaskFocus | null {
|
|
298
|
+
const focus = this.focusStore.get();
|
|
299
|
+
if (!focus) return null;
|
|
300
|
+
const task = this.artifacts.get(focus.taskId);
|
|
254
301
|
if (!task || task.kind !== "task" || task.status === "done" || task.status === "canceled") {
|
|
255
|
-
this.focusStore.clear(
|
|
302
|
+
this.focusStore.clear(focus.taskId);
|
|
256
303
|
return null;
|
|
257
304
|
}
|
|
258
305
|
if (filter?.projectRoot && !this.list(filter).some((candidate) => candidate.id === task.id)) return null;
|
|
259
|
-
return task;
|
|
306
|
+
return { artifact: task, status: focus.status, updatedAt: focus.updatedAt, ...(focus.pauseReason ? { pauseReason: focus.pauseReason } : {}) };
|
|
260
307
|
}
|
|
261
308
|
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return
|
|
309
|
+
active(filter?: TaskFilter): Artifact | null {
|
|
310
|
+
const focus = this.focused(filter);
|
|
311
|
+
return focus?.status === "active" ? focus.artifact : null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
focus(id: string, context: TaskEventContext = {}): Artifact {
|
|
315
|
+
return this.events.atomic(() => {
|
|
316
|
+
const task = this.require(id);
|
|
317
|
+
if (task.status === "done" || task.status === "canceled") throw new Error(`cannot focus task from ${task.status}`);
|
|
318
|
+
this.focusStore.set(id);
|
|
319
|
+
this.appendEvent({ taskId: id, type: "focus_set" }, context);
|
|
320
|
+
return task;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
pauseFocus(context: TaskEventContext = {}): TaskFocus {
|
|
325
|
+
return this.events.atomic(() => {
|
|
326
|
+
const focus = this.focused();
|
|
327
|
+
if (!focus) throw new Error("no focused task");
|
|
328
|
+
const state = this.focusStore.pause(focus.artifact.id, context.reason);
|
|
329
|
+
this.appendEvent({ taskId: focus.artifact.id, type: "focus_paused" }, context);
|
|
330
|
+
return { artifact: focus.artifact, status: state.status, updatedAt: state.updatedAt, ...(state.pauseReason ? { pauseReason: state.pauseReason } : {}) };
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
unpauseFocus(context: TaskEventContext = {}): TaskFocus {
|
|
335
|
+
return this.events.atomic(() => {
|
|
336
|
+
const focus = this.focused();
|
|
337
|
+
if (!focus) throw new Error("no focused task");
|
|
338
|
+
const state = this.focusStore.unpause(focus.artifact.id);
|
|
339
|
+
this.appendEvent({ taskId: focus.artifact.id, type: "focus_unpaused" }, context);
|
|
340
|
+
return { artifact: focus.artifact, status: state.status, updatedAt: state.updatedAt };
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
clearFocus(context: TaskEventContext = {}): { cleared: boolean } {
|
|
345
|
+
return this.events.atomic(() => {
|
|
346
|
+
const focus = this.focusStore.get();
|
|
347
|
+
if (focus) this.appendEvent({ taskId: focus.taskId, type: "focus_cleared" }, context);
|
|
348
|
+
this.focusStore.clear();
|
|
349
|
+
return { cleared: focus !== undefined };
|
|
350
|
+
});
|
|
269
351
|
}
|
|
270
352
|
|
|
271
353
|
transition(id: string, action: TaskTransition, context: TaskEventContext = {}): Artifact {
|
|
@@ -324,19 +406,6 @@ export class Tasks {
|
|
|
324
406
|
return this.artifacts.setExtra(id, { ...task.extra, checklist: validateChecklist(checklist) })!;
|
|
325
407
|
}
|
|
326
408
|
|
|
327
|
-
setAutomation(id: string, enabled: boolean, context: TaskEventContext = {}): Artifact {
|
|
328
|
-
return this.events.atomic(() => {
|
|
329
|
-
const task = this.require(id);
|
|
330
|
-
const current = task.extra["automation"];
|
|
331
|
-
const automation = typeof current === "object" && current !== null && !Array.isArray(current)
|
|
332
|
-
? current as Record<string, unknown>
|
|
333
|
-
: {};
|
|
334
|
-
const updated = this.artifacts.setExtra(id, { ...task.extra, automation: { ...automation, enabled } })!;
|
|
335
|
-
this.appendEvent({ taskId: id, type: enabled ? "automation_enabled" : "automation_disabled" }, context);
|
|
336
|
-
return updated;
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
409
|
depend(id: string, dependencyId: string): Artifact {
|
|
341
410
|
this.require(id);
|
|
342
411
|
this.require(dependencyId);
|
package/src/task-automation.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
TASK_AUTOMATION_ERROR_ID_MAX_LENGTH,
|
|
3
|
-
TASK_AUTOMATION_ERROR_MESSAGE_MAX_LENGTH,
|
|
4
|
-
TASK_AUTOMATION_GATE_CONCURRENCY,
|
|
5
|
-
TASK_AUTOMATION_HARD_MAX_RUNTIME_MS,
|
|
6
|
-
TASK_AUTOMATION_HARD_MAX_TASKS_PER_SWEEP,
|
|
7
|
-
TASK_AUTOMATION_INTERVAL_MS,
|
|
8
|
-
TASK_AUTOMATION_MAX_CANDIDATE_SCAN,
|
|
9
|
-
TASK_AUTOMATION_MAX_GATE_CONCURRENCY,
|
|
10
|
-
TASK_AUTOMATION_MAX_INTERVAL_MS,
|
|
11
|
-
TASK_AUTOMATION_MAX_RUNTIME_MS,
|
|
12
|
-
TASK_AUTOMATION_MAX_TASKS_PER_SWEEP,
|
|
13
|
-
TASK_AUTOMATION_MIN_INTERVAL_MS,
|
|
14
|
-
} from "./constants.ts";
|
|
15
|
-
import type { Artifact } from "./domain/artifact.ts";
|
|
16
|
-
import { projectTaskExecution } from "./task-execution.ts";
|
|
17
|
-
import type { Tasks } from "./task-service.ts";
|
|
18
|
-
|
|
19
|
-
export interface TaskAutomationSettings {
|
|
20
|
-
enabled: boolean;
|
|
21
|
-
intervalMs: number;
|
|
22
|
-
maxTasksPerSweep: number;
|
|
23
|
-
gateConcurrency: number;
|
|
24
|
-
maxRuntimeMs: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface TaskAutomationResult {
|
|
28
|
-
skipped?: "disabled" | "in-flight";
|
|
29
|
-
examined: number;
|
|
30
|
-
completed: number;
|
|
31
|
-
rejected: number;
|
|
32
|
-
started: number;
|
|
33
|
-
errors: Array<{ taskId: string; message: string }>;
|
|
34
|
-
timedOut: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function boundedInteger(
|
|
38
|
-
env: Record<string, string | undefined>,
|
|
39
|
-
name: string,
|
|
40
|
-
fallback: number,
|
|
41
|
-
minimum: number,
|
|
42
|
-
maximum: number,
|
|
43
|
-
): number {
|
|
44
|
-
const source = env[name];
|
|
45
|
-
if (source === undefined || source === "") return fallback;
|
|
46
|
-
const value = Number(source);
|
|
47
|
-
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
|
48
|
-
throw new Error(`${name} must be an integer between ${minimum} and ${maximum}`);
|
|
49
|
-
}
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function taskAutomationSettings(env: Record<string, string | undefined> = process.env): TaskAutomationSettings {
|
|
54
|
-
const enabled = env["PAPYRUS_AUTOMATION_ENABLED"] === "1";
|
|
55
|
-
if (env["PAPYRUS_AUTOMATION_ENABLED"] !== undefined && env["PAPYRUS_AUTOMATION_ENABLED"] !== "0" && !enabled) {
|
|
56
|
-
throw new Error("PAPYRUS_AUTOMATION_ENABLED must be 0 or 1");
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
enabled,
|
|
60
|
-
intervalMs: boundedInteger(env, "PAPYRUS_AUTOMATION_INTERVAL_MS", TASK_AUTOMATION_INTERVAL_MS, TASK_AUTOMATION_MIN_INTERVAL_MS, TASK_AUTOMATION_MAX_INTERVAL_MS),
|
|
61
|
-
maxTasksPerSweep: boundedInteger(env, "PAPYRUS_AUTOMATION_MAX_TASKS", TASK_AUTOMATION_MAX_TASKS_PER_SWEEP, 1, TASK_AUTOMATION_HARD_MAX_TASKS_PER_SWEEP),
|
|
62
|
-
gateConcurrency: boundedInteger(env, "PAPYRUS_AUTOMATION_GATE_CONCURRENCY", TASK_AUTOMATION_GATE_CONCURRENCY, 1, TASK_AUTOMATION_MAX_GATE_CONCURRENCY),
|
|
63
|
-
maxRuntimeMs: boundedInteger(env, "PAPYRUS_AUTOMATION_MAX_RUNTIME_MS", TASK_AUTOMATION_MAX_RUNTIME_MS, 1, TASK_AUTOMATION_HARD_MAX_RUNTIME_MS),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function automationEnabled(task: Artifact): boolean {
|
|
68
|
-
const automation = task.extra["automation"];
|
|
69
|
-
return typeof automation === "object"
|
|
70
|
-
&& automation !== null
|
|
71
|
-
&& !Array.isArray(automation)
|
|
72
|
-
&& (automation as Record<string, unknown>)["enabled"] === true;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function emptyResult(skipped?: TaskAutomationResult["skipped"]): TaskAutomationResult {
|
|
76
|
-
return { ...(skipped ? { skipped } : {}), examined: 0, completed: 0, rejected: 0, started: 0, errors: [], timedOut: false };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function boundedError(taskId: string, error: unknown): TaskAutomationResult["errors"][number] {
|
|
80
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
-
return {
|
|
82
|
-
taskId: taskId.slice(0, TASK_AUTOMATION_ERROR_ID_MAX_LENGTH),
|
|
83
|
-
message: message.slice(0, TASK_AUTOMATION_ERROR_MESSAGE_MAX_LENGTH),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface TaskAutomationScheduler {
|
|
88
|
-
setInterval(callback: () => void, intervalMs: number): unknown;
|
|
89
|
-
clearInterval(handle: unknown): void;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const SYSTEM_SCHEDULER: TaskAutomationScheduler = {
|
|
93
|
-
setInterval: (callback, intervalMs) => setInterval(callback, intervalMs),
|
|
94
|
-
clearInterval: (handle) => clearInterval(handle as ReturnType<typeof setInterval>),
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
export function scheduleTaskAutomation(
|
|
98
|
-
settings: TaskAutomationSettings,
|
|
99
|
-
sweep: () => Promise<unknown>,
|
|
100
|
-
onError: (error: unknown) => void,
|
|
101
|
-
scheduler: TaskAutomationScheduler = SYSTEM_SCHEDULER,
|
|
102
|
-
): () => void {
|
|
103
|
-
if (!settings.enabled) return () => {};
|
|
104
|
-
const handle = scheduler.setInterval(() => { void sweep().catch(onError); }, settings.intervalMs);
|
|
105
|
-
return () => scheduler.clearInterval(handle);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export class TaskAutomationReconciler {
|
|
109
|
-
private inFlight = false;
|
|
110
|
-
|
|
111
|
-
constructor(
|
|
112
|
-
private readonly tasks: Tasks,
|
|
113
|
-
private readonly settings: TaskAutomationSettings,
|
|
114
|
-
private readonly now: () => number = () => Date.now(),
|
|
115
|
-
) {}
|
|
116
|
-
|
|
117
|
-
status(): TaskAutomationSettings & { inFlight: boolean } {
|
|
118
|
-
return { ...this.settings, inFlight: this.inFlight };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async reconcile(): Promise<TaskAutomationResult> {
|
|
122
|
-
if (!this.settings.enabled) return emptyResult("disabled");
|
|
123
|
-
if (this.inFlight) return emptyResult("in-flight");
|
|
124
|
-
this.inFlight = true;
|
|
125
|
-
try { return await this.runSweep(); }
|
|
126
|
-
finally { this.inFlight = false; }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private async runSweep(): Promise<TaskAutomationResult> {
|
|
130
|
-
const result = emptyResult();
|
|
131
|
-
const deadline = this.now() + this.settings.maxRuntimeMs;
|
|
132
|
-
const candidates = this.tasks.list({ status: "review", limit: TASK_AUTOMATION_MAX_CANDIDATE_SCAN })
|
|
133
|
-
.filter(automationEnabled)
|
|
134
|
-
.sort((left, right) => left.id.localeCompare(right.id))
|
|
135
|
-
.slice(0, this.settings.maxTasksPerSweep);
|
|
136
|
-
const completedIds = new Set<string>();
|
|
137
|
-
|
|
138
|
-
for (let offset = 0; offset < candidates.length; offset += this.settings.gateConcurrency) {
|
|
139
|
-
if (this.now() >= deadline) { result.timedOut = true; break; }
|
|
140
|
-
const batch = candidates.slice(offset, offset + this.settings.gateConcurrency);
|
|
141
|
-
await Promise.all(batch.map(async (task) => {
|
|
142
|
-
result.examined += 1;
|
|
143
|
-
try {
|
|
144
|
-
const completion = await this.tasks.completeAsync(task.id, {
|
|
145
|
-
actor: "daemon",
|
|
146
|
-
source: "automation-reconciler",
|
|
147
|
-
reason: "automation-enabled review reconciliation",
|
|
148
|
-
}, { focusSuccessor: false, gateDeadlineMs: deadline });
|
|
149
|
-
if (completion.completed) {
|
|
150
|
-
result.completed += 1;
|
|
151
|
-
completedIds.add(task.id);
|
|
152
|
-
} else result.rejected += 1;
|
|
153
|
-
} catch (error) {
|
|
154
|
-
result.errors.push(boundedError(task.id, error));
|
|
155
|
-
}
|
|
156
|
-
}));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
let remaining = Math.max(0, this.settings.maxTasksPerSweep - result.examined);
|
|
160
|
-
if (remaining > 0 && completedIds.size > 0 && this.now() < deadline) {
|
|
161
|
-
let graph: ReturnType<Tasks["graph"]>;
|
|
162
|
-
try { graph = this.tasks.graph(); }
|
|
163
|
-
catch (error) {
|
|
164
|
-
result.errors.push(boundedError("graph", error));
|
|
165
|
-
return result;
|
|
166
|
-
}
|
|
167
|
-
const stateById = new Map(projectTaskExecution(graph).nodes.map((node) => [node.id, node.state]));
|
|
168
|
-
for (const node of [...graph.nodes].sort((left, right) => left.task.id.localeCompare(right.task.id))) {
|
|
169
|
-
if (remaining === 0 || this.now() >= deadline) break;
|
|
170
|
-
if (node.task.status !== "todo" || !automationEnabled(node.task) || stateById.get(node.task.id) !== "ready") continue;
|
|
171
|
-
if (!node.dependencyIds.some((id) => completedIds.has(id))) continue;
|
|
172
|
-
try {
|
|
173
|
-
this.tasks.transition(node.task.id, "start", {
|
|
174
|
-
actor: "daemon",
|
|
175
|
-
source: "automation-reconciler",
|
|
176
|
-
reason: "automation-enabled successor became ready",
|
|
177
|
-
});
|
|
178
|
-
result.started += 1;
|
|
179
|
-
remaining -= 1;
|
|
180
|
-
} catch (error) {
|
|
181
|
-
result.errors.push(boundedError(node.task.id, error));
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (this.now() >= deadline) result.timedOut = true;
|
|
186
|
-
return result;
|
|
187
|
-
}
|
|
188
|
-
}
|