@tea-agent/loop-agent 0.5.0 → 0.6.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/AGENTS.md +142 -142
- package/CHANGELOG.md +116 -98
- package/README.md +195 -195
- package/bin/agent-worker.js +22 -22
- package/bin/loop-agent.js +21 -21
- package/dist/application/dag/args.js +9 -1
- package/dist/application/dag/run-dag.js +16 -2
- package/dist/cli/command-definitions.js +22 -4
- package/dist/cli/help.js +3 -2
- package/dist/cli/program.js +7 -5
- package/dist/commands/import-prd.js +76 -0
- package/dist/commands/init.js +467 -457
- package/dist/commands/instructions.js +90 -58
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/cursor-executor.js +1 -1
- package/dist/executors/dag-pi-executor.js +1 -0
- package/dist/executors/pi-sdk-executor.js +63 -1
- package/dist/shared/preview.js +39 -0
- package/dist/task/runtime.js +27 -27
- package/dist/task/source-references.js +221 -0
- package/dist/worker/cli.js +62 -1
- package/dist/worker/loop-agent/loop-agent-client.js +97 -5
- package/dist/worker/materialize/harness-task-materializer.js +162 -5
- package/dist/worker/observability/event-store.js +82 -0
- package/dist/worker/observability/events.js +79 -0
- package/dist/worker/observability/progress-composite.js +33 -0
- package/dist/worker/observability/read-model.js +1013 -0
- package/dist/worker/observability/snapshot-store.js +43 -0
- package/dist/worker/observability/types.js +1 -0
- package/dist/worker/observe/paths.js +64 -0
- package/dist/worker/observe/routes.js +423 -0
- package/dist/worker/observe/server.js +61 -0
- package/dist/worker/observe/static/app.js +1419 -0
- package/dist/worker/observe/static/index.html +63 -0
- package/dist/worker/observe/static/styles.css +613 -0
- package/dist/worker/pool/failure-routing.js +41 -6
- package/dist/worker/pool/run-store.js +50 -0
- package/dist/worker/progress-reporter.js +0 -18
- package/dist/worker/run-task/run-task.js +327 -92
- package/dist/worker/runner/run-ready.js +112 -4
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/event-observer.js +132 -0
- package/dist/workflows/dag/init-hybrid.js +146 -13
- package/dist/workflows/dag/observer-compose.js +52 -0
- package/docs/README.md +74 -72
- package/docs/agent-dag-recovery-playbook.md +184 -184
- package/docs/agent-dag-runner.md +42 -42
- package/docs/architecture/runtime-boundaries.md +162 -147
- package/docs/cursor-executor-usage.md +25 -25
- package/docs/decisions/README.md +3 -3
- package/docs/design/README.md +49 -36
- package/docs/development-principles.md +73 -73
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
- package/docs/exec-plans/README.md +6 -6
- package/docs/exec-plans/active/README.md +12 -7
- package/docs/exec-plans/completed/README.md +31 -19
- package/docs/feature-workflow.md +186 -186
- package/docs/harness-methodology-debugging.md +153 -153
- package/docs/harness-methodology-tdd.md +130 -130
- package/docs/harness-methodology-verification.md +27 -27
- package/docs/init-surface.manifest.json +205 -199
- package/docs/loop-agent-harness.md +55 -42
- package/docs/production-readiness.md +96 -96
- package/docs/progress/README.md +3 -3
- package/docs/reports/README.md +9 -5
- package/docs/skills/README.md +6 -6
- package/docs/skills/vetted-skill-registry.md +26 -26
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +454 -454
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/agent-dag.base.json +195 -195
- package/docs/templates/agent-dag.final-verification.json +190 -190
- package/docs/templates/agent-dag.schema.json +316 -316
- package/docs/templates/agent-dag.supervised-implementation.json +500 -500
- package/docs/templates/exec-plan.md +64 -64
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/hybrid-dag.json +193 -193
- package/docs/templates/init-evolution-review.md +33 -33
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/progress-log.md +17 -17
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +52 -0
- package/docs/templates/worker-dogfood-setup.md +48 -0
- package/docs/verification-matrix.md +41 -41
- package/examples/decision-gate-agent-dag.json +123 -123
- package/examples/example-dag.json +51 -51
- package/examples/hybrid-loop-agent-dag.json +194 -194
- package/harness.json +70 -69
- package/package.json +66 -66
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/init-capability-evolution/SKILL.md +69 -69
- package/skills/loop-agent/SKILL.md +149 -147
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +412 -403
- package/skills/loop-agent/references/harness-policy.md +263 -259
- package/skills/loop-agent/references/hybrid-dag.md +216 -216
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +59 -59
- package/skills/loop-agent/references/model-routing.md +36 -36
- package/skills/loop-agent/references/multi-worktree.md +54 -54
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
- package/skills/loop-agent/references/task-workflow.md +89 -84
- package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/SKILL.md +296 -296
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/test-driven-development/SKILL.md +20 -20
- package/skills/verification-before-completion/SKILL.md +154 -154
- package/skills/webapp-testing/SKILL.md +19 -19
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import YAML from "yaml";
|
|
4
5
|
import { writeTaskConfig, writeTaskArtifactFile } from "../../infrastructure/harness/task-store.js";
|
|
@@ -51,7 +52,25 @@ export async function materializeTaskSpec(options) {
|
|
|
51
52
|
taskYamlPath: path.join(paths.sourceDir, "task.yaml"),
|
|
52
53
|
};
|
|
53
54
|
await mkdir(paths.sourceDir, { recursive: true });
|
|
54
|
-
await
|
|
55
|
+
const referencePaths = await materializeSourceDocs({
|
|
56
|
+
taskSpec: options.taskSpec,
|
|
57
|
+
taskSpecPath: options.taskSpecPath,
|
|
58
|
+
sourceDir: paths.sourceDir,
|
|
59
|
+
});
|
|
60
|
+
const sourceDocTrace = await buildSourceDocTrace({
|
|
61
|
+
taskSpec: options.taskSpec,
|
|
62
|
+
taskSpecPath: options.taskSpecPath,
|
|
63
|
+
referencePaths,
|
|
64
|
+
});
|
|
65
|
+
const acceptanceSummaries = await buildAcceptanceSummaries({
|
|
66
|
+
taskSpec: options.taskSpec,
|
|
67
|
+
taskSpecPath: options.taskSpecPath,
|
|
68
|
+
referencePaths,
|
|
69
|
+
});
|
|
70
|
+
await writeFile(sourcePaths.requirementPath, renderRequirementMarkdown(options.taskSpec, {
|
|
71
|
+
sourceDocTrace,
|
|
72
|
+
acceptanceSummaries,
|
|
73
|
+
}), "utf-8");
|
|
55
74
|
await writeFile(sourcePaths.constraintsPath, renderConstraintsMarkdown(options.taskSpec), "utf-8");
|
|
56
75
|
await writeFile(sourcePaths.taskYamlPath, YAML.stringify(options.taskSpec), "utf-8");
|
|
57
76
|
const profileMapping = resolveLoopAgentProfile(options.taskSpec);
|
|
@@ -62,11 +81,31 @@ export async function materializeTaskSpec(options) {
|
|
|
62
81
|
featureId: options.taskSpec.feature_id,
|
|
63
82
|
loopAgentProfile: profileMapping.loopAgentProfile,
|
|
64
83
|
taskConfigPath: paths.taskConfigPath,
|
|
65
|
-
source:
|
|
84
|
+
source: {
|
|
85
|
+
...sourcePaths,
|
|
86
|
+
// Reference copies make the upstream requirement/design/test cases concrete.
|
|
87
|
+
referencePaths,
|
|
88
|
+
},
|
|
66
89
|
};
|
|
67
90
|
await writeTaskArtifactFile(options.repoRoot, harnessTaskId, "materialize-manifest.json", `${JSON.stringify(manifest, null, 2)}\n`);
|
|
68
91
|
return manifest;
|
|
69
92
|
}
|
|
93
|
+
async function materializeSourceDocs(input) {
|
|
94
|
+
const referenceDir = path.join(input.sourceDir, "references");
|
|
95
|
+
const taskSpecDir = path.dirname(path.resolve(input.taskSpecPath));
|
|
96
|
+
const referencePaths = {};
|
|
97
|
+
await mkdir(referenceDir, { recursive: true });
|
|
98
|
+
for (const [key, relativePath] of Object.entries(input.taskSpec.source_docs)) {
|
|
99
|
+
if (!relativePath)
|
|
100
|
+
continue;
|
|
101
|
+
const sourcePath = path.resolve(taskSpecDir, relativePath);
|
|
102
|
+
const extension = path.extname(sourcePath) || ".md";
|
|
103
|
+
const targetPath = path.join(referenceDir, `${key}${extension}`);
|
|
104
|
+
await copyFile(sourcePath, targetPath);
|
|
105
|
+
referencePaths[key] = targetPath;
|
|
106
|
+
}
|
|
107
|
+
return referencePaths;
|
|
108
|
+
}
|
|
70
109
|
export function buildHarnessTaskId(taskSpec, now) {
|
|
71
110
|
const date = now.toISOString().slice(0, 10);
|
|
72
111
|
const slug = slugify(`${taskSpec.id}-${taskSpec.title}`);
|
|
@@ -81,10 +120,15 @@ function slugify(value) {
|
|
|
81
120
|
.replace(/^-+|-+$/g, "")
|
|
82
121
|
.replace(/-{2,}/g, "-");
|
|
83
122
|
}
|
|
84
|
-
|
|
123
|
+
/** Exported for unit tests: derived contract must point back to immutable references. */
|
|
124
|
+
export function renderRequirementMarkdown(taskSpec, options = {}) {
|
|
85
125
|
const lines = [
|
|
86
126
|
`# ${taskSpec.title}`,
|
|
87
127
|
"",
|
|
128
|
+
"> Derived execution contract from TaskSpec.",
|
|
129
|
+
"> Authoritative sources: `source/references/*` (immutable copies of TaskSpec source_docs).",
|
|
130
|
+
"> On conflict, prefer `source/references/*` over this file. Do not invent missing acceptance text.",
|
|
131
|
+
"",
|
|
88
132
|
`TaskSpec business id: ${taskSpec.id}`,
|
|
89
133
|
`Feature id: ${taskSpec.feature_id}`,
|
|
90
134
|
`Task type: ${taskSpec.type}`,
|
|
@@ -104,10 +148,123 @@ function renderRequirementMarkdown(taskSpec) {
|
|
|
104
148
|
if (taskSpec.scope.open_questions.length > 0) {
|
|
105
149
|
lines.push("## Open Questions", "", ...bulletLines(taskSpec.scope.open_questions), "");
|
|
106
150
|
}
|
|
107
|
-
|
|
151
|
+
const acceptanceLines = options.acceptanceSummaries && options.acceptanceSummaries.length > 0
|
|
152
|
+
? options.acceptanceSummaries.map((entry) => `- ${entry.id}: ${entry.summary}`)
|
|
153
|
+
: bulletLines(taskSpec.acceptance_refs);
|
|
154
|
+
lines.push("## Acceptance References", "", ...acceptanceLines, "");
|
|
155
|
+
if (options.sourceDocTrace && options.sourceDocTrace.length > 0) {
|
|
156
|
+
lines.push("## Source Docs / Traceability", "", ...options.sourceDocTrace.map((entry) => `- ${entry.key}: ${entry.materializedPath} (from ${entry.sourcePath}; sha256=${entry.sha256.slice(0, 12)}…)`), "");
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
lines.push("## Source Docs / Traceability", "", ...bulletLines(Object.entries(taskSpec.source_docs).map(([key, value]) => `${key}: source/references/${key}.* ← ${value}`)), "");
|
|
160
|
+
}
|
|
108
161
|
lines.push("## Required Outputs", "", ...bulletLines(taskSpec.outputs.required), "");
|
|
109
162
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
110
163
|
}
|
|
164
|
+
async function buildSourceDocTrace(input) {
|
|
165
|
+
const entries = [];
|
|
166
|
+
for (const [key, relativePath] of Object.entries(input.taskSpec.source_docs)) {
|
|
167
|
+
if (!relativePath)
|
|
168
|
+
continue;
|
|
169
|
+
const materializedAbsolute = input.referencePaths[key];
|
|
170
|
+
if (!materializedAbsolute)
|
|
171
|
+
continue;
|
|
172
|
+
const content = await readFile(materializedAbsolute);
|
|
173
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
174
|
+
const extension = path.extname(materializedAbsolute) || ".md";
|
|
175
|
+
entries.push({
|
|
176
|
+
key,
|
|
177
|
+
sourcePath: relativePath.split(path.sep).join("/"),
|
|
178
|
+
materializedPath: `references/${key}${extension}`,
|
|
179
|
+
sha256,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
entries.sort((left, right) => left.key.localeCompare(right.key));
|
|
183
|
+
return entries;
|
|
184
|
+
}
|
|
185
|
+
async function buildAcceptanceSummaries(input) {
|
|
186
|
+
const acceptanceAbsolute = input.referencePaths.acceptance ??
|
|
187
|
+
path.resolve(path.dirname(path.resolve(input.taskSpecPath)), input.taskSpec.source_docs.acceptance);
|
|
188
|
+
let raw = "";
|
|
189
|
+
try {
|
|
190
|
+
raw = await readFile(acceptanceAbsolute, "utf-8");
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return input.taskSpec.acceptance_refs.map((id) => ({
|
|
194
|
+
id,
|
|
195
|
+
summary: "(acceptance source unreadable; see source/references/acceptance.*)",
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
const byId = indexAcceptanceItems(raw);
|
|
199
|
+
return input.taskSpec.acceptance_refs.map((id) => ({
|
|
200
|
+
id,
|
|
201
|
+
summary: byId.get(id) ??
|
|
202
|
+
"(no summary found in acceptance source; re-read source/references/acceptance.*)",
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
/** Pull short human summaries for acceptance IDs from YAML or Markdown sources. */
|
|
206
|
+
export function indexAcceptanceItems(raw) {
|
|
207
|
+
const map = new Map();
|
|
208
|
+
const trimmed = raw.trim();
|
|
209
|
+
if (!trimmed)
|
|
210
|
+
return map;
|
|
211
|
+
// YAML AcceptanceSpec: { acceptance: [ { id, title, then, ... } ] }
|
|
212
|
+
try {
|
|
213
|
+
const parsed = YAML.parse(trimmed);
|
|
214
|
+
const items = extractAcceptanceArray(parsed);
|
|
215
|
+
for (const item of items) {
|
|
216
|
+
if (!item || typeof item !== "object")
|
|
217
|
+
continue;
|
|
218
|
+
const record = item;
|
|
219
|
+
const id = typeof record.id === "string" ? record.id : undefined;
|
|
220
|
+
if (!id)
|
|
221
|
+
continue;
|
|
222
|
+
const title = typeof record.title === "string" ? record.title.trim() : "";
|
|
223
|
+
const then = typeof record.then === "string" ? record.then.trim() : "";
|
|
224
|
+
const given = typeof record.given === "string" ? record.given.trim() : "";
|
|
225
|
+
const when = typeof record.when === "string" ? record.when.trim() : "";
|
|
226
|
+
const parts = [
|
|
227
|
+
title,
|
|
228
|
+
then ? `then: ${then}` : "",
|
|
229
|
+
!then && given ? `given: ${given}` : "",
|
|
230
|
+
!then && when ? `when: ${when}` : "",
|
|
231
|
+
].filter(Boolean);
|
|
232
|
+
map.set(id, compactSummary(parts.join(" — ") || id));
|
|
233
|
+
}
|
|
234
|
+
if (map.size > 0)
|
|
235
|
+
return map;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// fall through to markdown heuristics
|
|
239
|
+
}
|
|
240
|
+
// Markdown / free text: lines like "AC-BE-001: ..." or "- AC-BE-001 ..."
|
|
241
|
+
for (const line of trimmed.split("\n")) {
|
|
242
|
+
const match = line.match(/(?:^|\s)((?:AC|REQ)[-A-Z0-9]+)\s*[::\-–—]\s*(.+)$/i);
|
|
243
|
+
if (!match)
|
|
244
|
+
continue;
|
|
245
|
+
const id = match[1];
|
|
246
|
+
const summary = compactSummary(match[2]);
|
|
247
|
+
if (!map.has(id))
|
|
248
|
+
map.set(id, summary);
|
|
249
|
+
}
|
|
250
|
+
return map;
|
|
251
|
+
}
|
|
252
|
+
function extractAcceptanceArray(parsed) {
|
|
253
|
+
if (!parsed || typeof parsed !== "object")
|
|
254
|
+
return [];
|
|
255
|
+
const root = parsed;
|
|
256
|
+
if (Array.isArray(root.acceptance))
|
|
257
|
+
return root.acceptance;
|
|
258
|
+
if (Array.isArray(parsed))
|
|
259
|
+
return parsed;
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
function compactSummary(value, maxChars = 180) {
|
|
263
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
264
|
+
if (normalized.length <= maxChars)
|
|
265
|
+
return normalized;
|
|
266
|
+
return `${normalized.slice(0, maxChars - 1)}…`;
|
|
267
|
+
}
|
|
111
268
|
function renderConstraintsMarkdown(taskSpec) {
|
|
112
269
|
const lines = [
|
|
113
270
|
`# Execution Constraints for ${taskSpec.id}`,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function writeDiagnostic(context, error) {
|
|
5
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6
|
+
process.stderr.write(`[worker-event-store] ${context}: ${message}\n`);
|
|
7
|
+
}
|
|
8
|
+
function serializeEvent(event) {
|
|
9
|
+
return `${JSON.stringify(event)}\n`;
|
|
10
|
+
}
|
|
11
|
+
/** Identifiers used as event-store path segments must not alter the route. */
|
|
12
|
+
export function isSafeObservabilityIdentifier(value) {
|
|
13
|
+
return value !== "." && value !== ".." && !/[\\/\0]/.test(value);
|
|
14
|
+
}
|
|
15
|
+
export function createWorkerEventStore(jsonlPath) {
|
|
16
|
+
return {
|
|
17
|
+
async append(event) {
|
|
18
|
+
try {
|
|
19
|
+
await mkdir(path.dirname(jsonlPath), { recursive: true });
|
|
20
|
+
await appendFile(jsonlPath, serializeEvent(event), "utf-8");
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
writeDiagnostic(`append failed (${jsonlPath})`, error);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
async appendMany(events) {
|
|
27
|
+
for (const event of events) {
|
|
28
|
+
await this.append(event);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
appendSyncSafe(event) {
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(path.dirname(jsonlPath), { recursive: true });
|
|
34
|
+
appendFileSync(jsonlPath, serializeEvent(event), "utf-8");
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
writeDiagnostic(`appendSyncSafe failed (${jsonlPath})`, error);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
path() {
|
|
41
|
+
return jsonlPath;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function createRoutedWorkerEventStore(repoRoot) {
|
|
46
|
+
const observabilityRoot = path.join(path.resolve(repoRoot), ".task-pool", "observability");
|
|
47
|
+
const globalStore = createWorkerEventStore(path.join(observabilityRoot, "events.jsonl"));
|
|
48
|
+
let queue = Promise.resolve();
|
|
49
|
+
function storesFor(event) {
|
|
50
|
+
const stores = [globalStore];
|
|
51
|
+
if (event.batchRunId && isSafeObservabilityIdentifier(event.batchRunId)) {
|
|
52
|
+
stores.push(createWorkerEventStore(path.join(observabilityRoot, "batches", event.batchRunId, "events.jsonl")));
|
|
53
|
+
}
|
|
54
|
+
if (event.workerRunId && isSafeObservabilityIdentifier(event.workerRunId)) {
|
|
55
|
+
stores.push(createWorkerEventStore(path.join(observabilityRoot, "runs", event.workerRunId, "events.jsonl")));
|
|
56
|
+
}
|
|
57
|
+
return stores;
|
|
58
|
+
}
|
|
59
|
+
const store = {
|
|
60
|
+
append(event) {
|
|
61
|
+
const write = queue.then(async () => {
|
|
62
|
+
await Promise.all(storesFor(event).map((target) => target.append(event)));
|
|
63
|
+
});
|
|
64
|
+
queue = write.catch(() => { });
|
|
65
|
+
return write;
|
|
66
|
+
},
|
|
67
|
+
async appendMany(events) {
|
|
68
|
+
for (const event of events) {
|
|
69
|
+
await store.append(event);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
appendSyncSafe(event) {
|
|
73
|
+
for (const target of storesFor(event)) {
|
|
74
|
+
target.appendSyncSafe(event);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
path() {
|
|
78
|
+
return globalStore.path();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
return store;
|
|
82
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
|
|
3
|
+
const WORKER_EVENT_SOURCES = new Set([
|
|
4
|
+
"worker",
|
|
5
|
+
"loop-agent-command",
|
|
6
|
+
"dag",
|
|
7
|
+
"shell",
|
|
8
|
+
"artifact",
|
|
9
|
+
"observe",
|
|
10
|
+
]);
|
|
11
|
+
const WORKER_EVENT_TYPES = new Set([
|
|
12
|
+
"batch.started",
|
|
13
|
+
"batch.finished",
|
|
14
|
+
"readyQueue.computed",
|
|
15
|
+
"task.queued",
|
|
16
|
+
"task.started",
|
|
17
|
+
"task.finished",
|
|
18
|
+
"task.reused",
|
|
19
|
+
"step.started",
|
|
20
|
+
"step.heartbeat",
|
|
21
|
+
"step.output",
|
|
22
|
+
"step.finished",
|
|
23
|
+
"command.started",
|
|
24
|
+
"command.output",
|
|
25
|
+
"command.heartbeat",
|
|
26
|
+
"command.finished",
|
|
27
|
+
"dag.run.started",
|
|
28
|
+
"dag.node.started",
|
|
29
|
+
"dag.node.output",
|
|
30
|
+
"dag.node.finished",
|
|
31
|
+
"dag.run.finished",
|
|
32
|
+
"artifact.written",
|
|
33
|
+
"failure.routed",
|
|
34
|
+
"state.updated",
|
|
35
|
+
]);
|
|
36
|
+
export function createWorkerEvent(input, options) {
|
|
37
|
+
const now = options?.now ?? (() => new Date());
|
|
38
|
+
const randomUuid = options?.randomUuid ?? randomUUID;
|
|
39
|
+
return {
|
|
40
|
+
...input,
|
|
41
|
+
schemaVersion: 1,
|
|
42
|
+
id: input.id ?? randomUuid(),
|
|
43
|
+
at: input.at ?? now().toISOString(),
|
|
44
|
+
spanId: input.spanId ?? randomUuid(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function parseWorkerEventLine(line) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(line);
|
|
50
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return isWorkerEvent(parsed) ? parsed : undefined;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function isWorkerEvent(value) {
|
|
60
|
+
const event = value;
|
|
61
|
+
return (event.schemaVersion === 1 &&
|
|
62
|
+
typeof event.id === "string" &&
|
|
63
|
+
event.id.length > 0 &&
|
|
64
|
+
typeof event.at === "string" &&
|
|
65
|
+
!Number.isNaN(Date.parse(event.at)) &&
|
|
66
|
+
typeof event.spanId === "string" &&
|
|
67
|
+
event.spanId.length > 0 &&
|
|
68
|
+
typeof event.source === "string" &&
|
|
69
|
+
WORKER_EVENT_SOURCES.has(event.source) &&
|
|
70
|
+
typeof event.type === "string" &&
|
|
71
|
+
WORKER_EVENT_TYPES.has(event.type) &&
|
|
72
|
+
typeof event.label === "string");
|
|
73
|
+
}
|
|
74
|
+
export function truncatePreview(text, maxBytes = 4096) {
|
|
75
|
+
return truncateUtf8Preview(text, maxBytes);
|
|
76
|
+
}
|
|
77
|
+
export function redactForPreview(text) {
|
|
78
|
+
return redactSecrets(text);
|
|
79
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createProgressReporter, } from "../progress-reporter.js";
|
|
2
|
+
import { createWorkerEvent } from "./events.js";
|
|
3
|
+
export function createCompositeProgressReporter(options) {
|
|
4
|
+
const textReporter = createProgressReporter({
|
|
5
|
+
quiet: options.quiet,
|
|
6
|
+
now: options.now,
|
|
7
|
+
isTty: options.isTty,
|
|
8
|
+
sink: options.sink,
|
|
9
|
+
});
|
|
10
|
+
const eventStore = options.eventStore;
|
|
11
|
+
const context = options.context ?? {};
|
|
12
|
+
const now = options.now ?? (() => new Date());
|
|
13
|
+
return {
|
|
14
|
+
...textReporter,
|
|
15
|
+
async event(input) {
|
|
16
|
+
if (!eventStore)
|
|
17
|
+
return;
|
|
18
|
+
try {
|
|
19
|
+
const event = createWorkerEvent({
|
|
20
|
+
...input,
|
|
21
|
+
batchRunId: input.batchRunId ?? context.batchRunId,
|
|
22
|
+
workerRunId: input.workerRunId ?? context.workerRunId,
|
|
23
|
+
taskId: input.taskId ?? context.taskId,
|
|
24
|
+
}, { now });
|
|
25
|
+
await eventStore.append(event);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
process.stderr.write(`[progress-composite] event append failed: ${message}\n`);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|