@tea-agent/loop-agent 0.10.0 → 0.12.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 +10 -2
- package/CHANGELOG.md +91 -24
- package/README.md +84 -12
- package/dist/application/dag/args.js +1 -12
- package/dist/application/dag/generate-task-dag.js +38 -2
- package/dist/application/dag/run-dag.js +11 -27
- package/dist/application/dag/validate-dag.js +13 -2
- package/dist/application/loop/run-action.js +0 -4
- package/dist/cli/command-definitions.js +44 -16
- package/dist/cli/program.js +40 -23
- package/dist/cli/update/notifier.js +117 -0
- package/dist/cli/update/npm-client.js +151 -0
- package/dist/cli/update/policy.js +58 -0
- package/dist/cli/update/state.js +68 -0
- package/dist/cli.js +33 -0
- package/dist/commands/cursor-prompt.js +42 -82
- package/dist/commands/dag-approve.js +36 -0
- package/dist/commands/delegate.js +75 -77
- package/dist/commands/doctor.js +0 -18
- package/dist/commands/init.js +547 -95
- package/dist/commands/instructions.js +7 -10
- package/dist/commands/loop.js +4 -20
- package/dist/commands/plan.js +50 -0
- package/dist/executors/config-core.js +0 -51
- package/dist/executors/dag-pi-executor.js +1 -1
- package/dist/executors/dag.js +0 -1
- package/dist/executors/index.js +0 -2
- package/dist/executors/model-routing.js +9 -9
- package/dist/executors/shell-executor.js +1 -1
- package/dist/governance/checks.js +6 -3
- package/dist/governance/exec-plans.js +545 -0
- package/dist/governance/manifest-types.js +24 -2
- package/dist/infrastructure/harness/loop-action-store.js +0 -3
- package/dist/records/harvest.js +2 -23
- package/dist/records/one-shot-runs.js +1 -1
- package/dist/shared/artifacts-core.js +24 -5
- package/dist/shared/output-truncation.js +37 -0
- package/dist/shared/package-metadata.js +488 -0
- package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
- package/dist/sidecars/cursor-prompt/index.js +3 -0
- package/dist/sidecars/cursor-prompt/stream.js +121 -0
- package/dist/task/config-types.js +29 -12
- package/dist/task/delegate.js +9 -21
- package/dist/task/runtime.js +1 -2
- package/dist/worker/cli.js +32 -3
- package/dist/worker/delivery/final-verification.js +47 -11
- package/dist/worker/delivery/package.js +63 -10
- package/dist/worker/feature/run.js +60 -8
- package/dist/worker/loop-agent/loop-agent-client.js +329 -126
- package/dist/worker/observability/event-history.js +216 -0
- package/dist/worker/observability/read-model.js +338 -83
- package/dist/worker/observe/paths.js +17 -0
- package/dist/worker/observe/routes.js +165 -21
- package/dist/worker/observe/server.js +59 -1
- package/dist/worker/observe/static/api.js +27 -0
- package/dist/worker/observe/static/app.js +120 -2317
- package/dist/worker/observe/static/constants.js +148 -0
- package/dist/worker/observe/static/copy.js +67 -0
- package/dist/worker/observe/static/dag-helpers.js +172 -0
- package/dist/worker/observe/static/dag-model.js +72 -0
- package/dist/worker/observe/static/dom.js +61 -0
- package/dist/worker/observe/static/format-pool.js +67 -0
- package/dist/worker/observe/static/format.js +292 -0
- package/dist/worker/observe/static/index.html +300 -82
- package/dist/worker/observe/static/kpi.js +94 -0
- package/dist/worker/observe/static/relations.js +128 -0
- package/dist/worker/observe/static/router.js +85 -0
- package/dist/worker/observe/static/run-processing.js +148 -0
- package/dist/worker/observe/static/shell-chrome.js +68 -0
- package/dist/worker/observe/static/state.js +253 -0
- package/dist/worker/observe/static/styles.css +1720 -495
- package/dist/worker/observe/static/views/batch.js +226 -0
- package/dist/worker/observe/static/views/dag-graph.js +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +477 -0
- package/dist/worker/observe/static/views/dag.js +362 -0
- package/dist/worker/observe/static/views/dashboard.js +442 -0
- package/dist/worker/observe/static/views/failures.js +143 -0
- package/dist/worker/observe/static/views/feature.js +453 -0
- package/dist/worker/observe/static/views/pool.js +347 -0
- package/dist/worker/observe/static/views/run.js +453 -0
- package/dist/worker/observe/static/views/session-timeline.js +205 -0
- package/dist/worker/observe/static/views/shell.js +7 -0
- package/dist/worker/observe/static/views/task.js +260 -0
- package/dist/worker/observe/static/views/timeline.js +163 -0
- package/dist/worker/preflight.js +49 -1
- package/dist/worker/run-task/run-task.js +22 -12
- package/dist/worker/runner/run-ready.js +76 -12
- package/dist/worker/task-spec/schema.js +0 -1
- package/dist/workflows/dag/controller-identity.js +104 -0
- package/dist/workflows/dag/convergence/controller.js +1 -1
- package/dist/workflows/dag/executor-registry.js +0 -2
- package/dist/workflows/dag/init-hybrid.js +797 -27
- package/dist/workflows/dag/node-execution.js +183 -35
- package/dist/workflows/dag/repair-artifact.js +91 -0
- package/dist/workflows/dag/report.js +50 -0
- package/dist/workflows/dag/retry-policy.js +138 -0
- package/dist/workflows/dag/runner.js +77 -17
- package/dist/workflows/dag/runtime-contract.js +87 -0
- package/dist/workflows/dag/scheduler.js +7 -2
- package/dist/workflows/dag/sdd-embedded.js +128 -0
- package/dist/workflows/dag/skill-instructions.js +5 -4
- package/dist/workflows/dag/skill-snapshot.js +529 -0
- package/dist/workflows/dag/types.js +86 -10
- package/dist/workflows/dag/validate.js +73 -12
- package/dist/workflows/loop/actions/dag-action.js +0 -2
- package/dist/workflows/loop/actions/shared.js +1 -1
- package/dist/workflows/loop/actions.js +14 -31
- package/dist/workflows/loop/benchmark.js +1 -1
- package/dist/workflows/loop/index.js +1 -1
- package/dist/workflows/loop/policy/auto-policy.js +22 -14
- package/dist/workflows/loop/policy/path-patterns.js +13 -0
- package/docs/README.md +36 -33
- package/docs/agent-dag-recovery-playbook.md +1 -1
- package/docs/agent-dag-runner.md +28 -3
- package/docs/architecture/README.md +26 -0
- package/docs/architecture/dag-execution.md +140 -0
- package/docs/architecture/evolution.md +53 -0
- package/docs/architecture/facts-and-state.md +58 -0
- package/docs/architecture/runtime-boundaries.md +45 -17
- package/docs/architecture/system-overview.md +93 -0
- package/docs/architecture/worker-and-feature.md +81 -0
- package/docs/cursor-prompt-sidecar.md +36 -0
- package/docs/decisions/README.md +13 -1
- package/docs/design/README.md +43 -21
- package/docs/development-principles.md +2 -2
- package/docs/exec-plans/active/README.md +1 -3
- package/docs/exec-plans/completed/README.md +23 -0
- package/docs/feature-workflow.md +78 -4
- package/docs/harness-methodology-debugging.md +1 -1
- package/docs/harness-methodology-tdd.md +3 -3
- package/docs/init-surface.manifest.json +60 -25
- package/docs/loop-agent-harness.md +28 -4
- package/docs/progress/README.md +50 -1
- package/docs/reports/README.md +90 -18
- package/docs/skills/README.md +2 -1
- package/docs/skills/vetted-skill-registry.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +23 -6
- package/docs/templates/agent-dag.base.json +0 -5
- package/docs/templates/agent-dag.final-verification.json +0 -5
- package/docs/templates/agent-dag.schema.json +70 -3
- package/docs/templates/agent-dag.supervised-implementation.json +9 -8
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
- package/docs/templates/backend-test-dag.json +276 -0
- package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
- package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
- package/docs/templates/frontend-design-contract.md +33 -0
- package/docs/templates/frontend-task-constraints.md +25 -0
- package/docs/templates/frontend-task-requirement.md +61 -0
- package/docs/templates/harness.schema.json +10 -12
- package/docs/templates/hybrid-dag.json +1 -6
- package/docs/templates/interactive-ui-round2-experiment.md +1 -1
- package/docs/templates/product-line/task.yaml +0 -1
- package/docs/templates/project-start-checklist.md +2 -2
- package/docs/templates/worker-dogfood-evidence.md +28 -0
- package/docs/templates/worker-dogfood-setup.md +20 -0
- package/docs/verification-matrix.md +10 -0
- package/examples/decision-gate-agent-dag.json +87 -33
- package/examples/example-dag.json +0 -5
- package/examples/hybrid-loop-agent-dag.json +0 -5
- package/harness.json +7 -15
- package/package.json +22 -46
- package/scripts/check-product-line-docs.sh +10 -7
- package/skills/agent-worker/SKILL.md +37 -0
- package/skills/agent-worker/references/agent-worker-operator.md +43 -0
- package/skills/frontend-design-review/SKILL.md +59 -0
- package/skills/frontend-design-review/references/review-checklist.md +37 -0
- package/skills/frontend-implementation/SKILL.md +51 -0
- package/skills/frontend-implementation/references/code-standards.md +34 -0
- package/skills/frontend-implementation/references/design-spec.md +46 -0
- package/skills/frontend-implementation/references/node-contracts.md +32 -0
- package/skills/frontend-review/SKILL.md +53 -0
- package/skills/frontend-review/references/review-findings.md +42 -0
- package/skills/frontend-verification/SKILL.md +40 -0
- package/skills/frontend-verification/references/verification-checklist.md +56 -0
- package/skills/grill-me/SKILL.md +10 -0
- package/skills/grill-with-docs/SKILL.md +88 -0
- package/skills/grill-with-docs/adr-format.md +47 -0
- package/skills/grill-with-docs/context-format.md +60 -0
- package/skills/loop-agent/SKILL.md +11 -9
- package/skills/loop-agent/references/command-reference.md +14 -15
- package/skills/loop-agent/references/docs-converge.md +126 -0
- package/skills/loop-agent/references/harness-policy.md +7 -7
- package/skills/loop-agent/references/hybrid-dag.md +36 -20
- package/skills/loop-agent/references/long-running-loop.md +4 -6
- package/skills/loop-agent/references/multi-worktree.md +6 -6
- package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
- package/skills/loop-agent/references/task-workflow.md +1 -1
- package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
- package/skills/using-git-worktrees/SKILL.md +215 -0
- package/dist/commands/cursor-worker.js +0 -43
- package/dist/cursor-worker-entry.js +0 -8
- package/dist/executors/cursor-artifacts.js +0 -33
- package/dist/executors/cursor-execution-log.js +0 -81
- package/dist/executors/cursor-executor-artifacts.js +0 -134
- package/dist/executors/cursor-run.js +0 -115
- package/dist/executors/cursor-tool.js +0 -94
- package/dist/executors/cursor-worker-client.js +0 -223
- package/dist/executors/cursor-worker-protocol.js +0 -18
- package/dist/executors/cursor-worker-server.js +0 -54
- package/dist/executors/cursor-worker.js +0 -3
- package/dist/executors/cursor.js +0 -6
- package/dist/executors/dag-cursor-executor.js +0 -87
- package/dist/workflows/loop/actions/cursor-fix.js +0 -191
- package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
- package/docs/cursor-executor-usage.md +0 -25
- package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import YAML from "yaml";
|
|
5
|
+
import { controllerIdentitiesMatch, controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
|
|
5
6
|
import { deriveFailureRoute, deriveFailureRouteFromError, } from "../pool/failure-routing.js";
|
|
6
7
|
import { findRunByWorkerRunId, getTaskPoolRoot, readAllTaskPoolStates, recordTaskPoolRun, writeTaskPoolState, } from "../pool/run-store.js";
|
|
7
8
|
import { runTaskSpec, } from "../run-task/run-task.js";
|
|
@@ -9,7 +10,26 @@ import { formatDuration, noopProgressReporter, } from "../progress-reporter.js";
|
|
|
9
10
|
import { computeReadyQueue } from "../task-graph/ready-queue.js";
|
|
10
11
|
import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
|
|
11
12
|
import { taskSpecSchema } from "../task-spec/schema.js";
|
|
13
|
+
import { preflightTargetRepo } from "../preflight.js";
|
|
12
14
|
export async function runReadyTasks(options) {
|
|
15
|
+
let controllerIdentity = resolveControllerIdentity(options.client, options.controllerIdentity);
|
|
16
|
+
const identityFailure = controllerIdentityExpectationFailure(controllerIdentity, options.controllerExpectation);
|
|
17
|
+
if (identityFailure) {
|
|
18
|
+
throw new Error(`${identityFailure.code}: ${identityFailure.message}`);
|
|
19
|
+
}
|
|
20
|
+
if (!options.skipTaskPreflight && !options.runTask) {
|
|
21
|
+
const preflight = await preflightTargetRepo({
|
|
22
|
+
repoRoot: options.repoRoot,
|
|
23
|
+
client: options.client,
|
|
24
|
+
runCheckRepo: options.runCheckRepo,
|
|
25
|
+
...(options.checkRepoCommand ? { checkRepoCommand: options.checkRepoCommand } : {}),
|
|
26
|
+
...(options.controllerExpectation ? { expectation: options.controllerExpectation } : {}),
|
|
27
|
+
});
|
|
28
|
+
if (!preflight.ok) {
|
|
29
|
+
throw new Error(`target repo preflight failed: ${preflight.code}: ${preflight.message}`);
|
|
30
|
+
}
|
|
31
|
+
controllerIdentity = preflight.controllerIdentity ?? controllerIdentity;
|
|
32
|
+
}
|
|
13
33
|
const now = options.now ?? new Date();
|
|
14
34
|
const startedAt = now.toISOString();
|
|
15
35
|
const batchRunId = options.batchRunId ?? buildBatchRunId(now);
|
|
@@ -43,12 +63,19 @@ export async function runReadyTasks(options) {
|
|
|
43
63
|
const taskSpecPath = path.join(options.featureDir, "tasks", node?.task ?? `${taskId}.yaml`);
|
|
44
64
|
const taskSpec = await loadTaskSpec(taskSpecPath);
|
|
45
65
|
const retryOfWorkerRunId = states[taskId]?.retryOfWorkerRunId;
|
|
46
|
-
|
|
66
|
+
let workerRunId = retryOfWorkerRunId
|
|
47
67
|
? buildRetryWorkerRunId(taskId, retryOfWorkerRunId, now)
|
|
48
68
|
: options.workerRunIdForTask?.(taskId, taskSpec) ??
|
|
49
|
-
buildStableWorkerRunId(taskId, taskSpec, now);
|
|
69
|
+
buildStableWorkerRunId(taskId, taskSpec, now, controllerIdentity);
|
|
50
70
|
if (workerRunId) {
|
|
51
|
-
|
|
71
|
+
let existing = await findRunByWorkerRunId(options.repoRoot, workerRunId);
|
|
72
|
+
if (existing && !controllerRunCanBeReused(existing, controllerIdentity)) {
|
|
73
|
+
workerRunId = buildControllerScopedWorkerRunId(workerRunId, controllerIdentity);
|
|
74
|
+
existing = await findRunByWorkerRunId(options.repoRoot, workerRunId);
|
|
75
|
+
if (existing && !controllerRunCanBeReused(existing, controllerIdentity)) {
|
|
76
|
+
throw new Error(`workerRunId collision has incompatible controller identity: ${workerRunId}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
52
79
|
if (existing) {
|
|
53
80
|
progress.task(`task ${index}/${total} ${taskId}: reuse existing run ${workerRunId}`);
|
|
54
81
|
emit(progress, {
|
|
@@ -81,7 +108,7 @@ export async function runReadyTasks(options) {
|
|
|
81
108
|
workerRunId,
|
|
82
109
|
status: "running",
|
|
83
110
|
});
|
|
84
|
-
progress.step(`
|
|
111
|
+
progress.step(`materialize → dag-run-task → validate → run-dag → report`);
|
|
85
112
|
let result;
|
|
86
113
|
try {
|
|
87
114
|
await writeTaskPoolState(options.repoRoot, {
|
|
@@ -107,14 +134,11 @@ export async function runReadyTasks(options) {
|
|
|
107
134
|
client: options.client,
|
|
108
135
|
now,
|
|
109
136
|
workerRunId,
|
|
110
|
-
preflight:
|
|
111
|
-
? false
|
|
112
|
-
: {
|
|
113
|
-
runCheckRepo: options.runCheckRepo,
|
|
114
|
-
...(options.checkRepoCommand ? { checkRepoCommand: options.checkRepoCommand } : {}),
|
|
115
|
-
},
|
|
137
|
+
preflight: false,
|
|
116
138
|
progress,
|
|
117
139
|
...(options.piModel ? { piModel: options.piModel } : {}),
|
|
140
|
+
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
141
|
+
...(options.controllerExpectation ? { controllerExpectation: options.controllerExpectation } : {}),
|
|
118
142
|
});
|
|
119
143
|
}
|
|
120
144
|
catch (error) {
|
|
@@ -146,6 +170,7 @@ export async function runReadyTasks(options) {
|
|
|
146
170
|
error: message,
|
|
147
171
|
failure,
|
|
148
172
|
...(retryOfWorkerRunId ? { retryOfWorkerRunId } : {}),
|
|
173
|
+
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
149
174
|
},
|
|
150
175
|
});
|
|
151
176
|
recorded = true;
|
|
@@ -214,6 +239,7 @@ export async function runReadyTasks(options) {
|
|
|
214
239
|
...(failure ? { failure } : {}),
|
|
215
240
|
...(retryOfWorkerRunId ? { retryOfWorkerRunId } : {}),
|
|
216
241
|
...(result.failureArtifacts ? { failureArtifacts: result.failureArtifacts } : {}),
|
|
242
|
+
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
217
243
|
};
|
|
218
244
|
let recorded = false;
|
|
219
245
|
try {
|
|
@@ -251,6 +277,7 @@ export async function runReadyTasks(options) {
|
|
|
251
277
|
summary,
|
|
252
278
|
tasks,
|
|
253
279
|
batchRunPath,
|
|
280
|
+
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
254
281
|
};
|
|
255
282
|
await mkdir(path.dirname(batchRunPath), { recursive: true });
|
|
256
283
|
await writeFile(batchRunPath, `${JSON.stringify(output, null, 2)}\n`, "utf-8");
|
|
@@ -274,18 +301,55 @@ function emit(progress, input) {
|
|
|
274
301
|
export function buildBatchRunId(now) {
|
|
275
302
|
return `batch-${now.toISOString().replace(/[-:.]/g, "").slice(0, 15)}`;
|
|
276
303
|
}
|
|
277
|
-
export function buildStableWorkerRunId(taskId, taskSpec, now) {
|
|
304
|
+
export function buildStableWorkerRunId(taskId, taskSpec, now, controllerIdentity) {
|
|
278
305
|
const date = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
279
306
|
const slug = taskId
|
|
280
307
|
.toLowerCase()
|
|
281
308
|
.replace(/[^a-z0-9]+/g, "-")
|
|
282
309
|
.replace(/^-+|-+$/g, "");
|
|
283
310
|
const hash = createHash("sha256")
|
|
284
|
-
.update(
|
|
311
|
+
.update([
|
|
312
|
+
taskSpec.feature_id,
|
|
313
|
+
taskSpec.id,
|
|
314
|
+
taskSpec.title,
|
|
315
|
+
...(controllerIdentity
|
|
316
|
+
? [
|
|
317
|
+
controllerIdentity.packageName,
|
|
318
|
+
controllerIdentity.packageVersion,
|
|
319
|
+
controllerIdentity.binarySha256,
|
|
320
|
+
controllerIdentity.packageFingerprint.value,
|
|
321
|
+
]
|
|
322
|
+
: []),
|
|
323
|
+
].join(":"))
|
|
285
324
|
.digest("hex")
|
|
286
325
|
.slice(0, 10);
|
|
287
326
|
return `wr-${date}-${slug}-${hash}`;
|
|
288
327
|
}
|
|
328
|
+
function buildControllerScopedWorkerRunId(baseWorkerRunId, controllerIdentity) {
|
|
329
|
+
const hash = createHash("sha256")
|
|
330
|
+
.update(controllerIdentity
|
|
331
|
+
? JSON.stringify({
|
|
332
|
+
packageName: controllerIdentity.packageName,
|
|
333
|
+
binName: controllerIdentity.binName,
|
|
334
|
+
packageVersion: controllerIdentity.packageVersion,
|
|
335
|
+
binarySha256: controllerIdentity.binarySha256,
|
|
336
|
+
packageFingerprint: controllerIdentity.packageFingerprint.value,
|
|
337
|
+
requested: controllerIdentity.requested,
|
|
338
|
+
entry: controllerIdentity.entry,
|
|
339
|
+
realEntry: controllerIdentity.realEntry,
|
|
340
|
+
launch: controllerIdentity.launch,
|
|
341
|
+
packageRoot: controllerIdentity.packageRoot,
|
|
342
|
+
})
|
|
343
|
+
: "legacy-unpinned-controller")
|
|
344
|
+
.digest("hex")
|
|
345
|
+
.slice(0, 10);
|
|
346
|
+
return `${baseWorkerRunId}-controller-${hash}`;
|
|
347
|
+
}
|
|
348
|
+
function controllerRunCanBeReused(existing, controllerIdentity) {
|
|
349
|
+
if (!existing.controllerIdentity && !controllerIdentity)
|
|
350
|
+
return true;
|
|
351
|
+
return controllerIdentitiesMatch(existing.controllerIdentity, controllerIdentity);
|
|
352
|
+
}
|
|
289
353
|
/** A retry is a distinct execution attempt and must not collide with failed evidence. */
|
|
290
354
|
export function buildRetryWorkerRunId(taskId, previousWorkerRunId, now) {
|
|
291
355
|
const date = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
@@ -97,7 +97,6 @@ export const taskSpecSchema = z
|
|
|
97
97
|
profile_policy: z.literal("mapped"),
|
|
98
98
|
strict_models: z.boolean().optional().default(true),
|
|
99
99
|
strict_governance: z.boolean().optional().default(true),
|
|
100
|
-
no_cursor: z.boolean().optional().default(false),
|
|
101
100
|
max_concurrent: z.number().int().positive().optional().default(1),
|
|
102
101
|
})
|
|
103
102
|
.strict(),
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
|
+
import { controllerIdentitiesMatch, resolveRunningControllerIdentity, } from "../../shared/package-metadata.js";
|
|
6
|
+
export const CONTROLLER_IDENTITY_ARTIFACT_NAME = "controller-identity.json";
|
|
7
|
+
function hashArtifact(artifact) {
|
|
8
|
+
return createHash("sha256")
|
|
9
|
+
.update(`${JSON.stringify(artifact, null, 2)}\n`)
|
|
10
|
+
.digest("hex");
|
|
11
|
+
}
|
|
12
|
+
function summaryFields(identity) {
|
|
13
|
+
if (!identity)
|
|
14
|
+
return {};
|
|
15
|
+
return {
|
|
16
|
+
packageName: identity.packageName,
|
|
17
|
+
packageVersion: identity.packageVersion,
|
|
18
|
+
packageFingerprint: identity.packageFingerprint.value,
|
|
19
|
+
binarySha256: identity.binarySha256,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Capture the running controller identity as a run-owned fact and persist it
|
|
24
|
+
* under `runDir/controller-identity.json`. Returns the ref (with content hash)
|
|
25
|
+
* to store on DagRunState. When the identity cannot be resolved, records a
|
|
26
|
+
* `legacy-unpinned` artifact instead of fabricating an identity.
|
|
27
|
+
*/
|
|
28
|
+
export async function captureControllerIdentity(input) {
|
|
29
|
+
const capturedAt = (input.now ?? new Date()).toISOString();
|
|
30
|
+
const identity = input.identity !== undefined
|
|
31
|
+
? input.identity
|
|
32
|
+
: (resolveRunningControllerIdentity({
|
|
33
|
+
...(input.argv ? { argv: input.argv } : {}),
|
|
34
|
+
...(input.now ? { now: input.now } : {}),
|
|
35
|
+
}) ?? null);
|
|
36
|
+
const artifact = identity
|
|
37
|
+
? { schemaVersion: 1, status: "pinned", capturedAt, identity }
|
|
38
|
+
: {
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
status: "legacy-unpinned",
|
|
41
|
+
capturedAt,
|
|
42
|
+
identity: null,
|
|
43
|
+
reason: "running controller identity could not be resolved at run creation",
|
|
44
|
+
};
|
|
45
|
+
await writeJsonAtomic(path.join(input.runDir, CONTROLLER_IDENTITY_ARTIFACT_NAME), artifact);
|
|
46
|
+
return {
|
|
47
|
+
schemaVersion: 1,
|
|
48
|
+
path: CONTROLLER_IDENTITY_ARTIFACT_NAME,
|
|
49
|
+
sha256: hashArtifact(artifact),
|
|
50
|
+
capturedAt,
|
|
51
|
+
status: artifact.status,
|
|
52
|
+
...summaryFields(identity),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function readControllerIdentityArtifact(runDir) {
|
|
56
|
+
try {
|
|
57
|
+
const raw = await readFile(path.join(runDir, CONTROLLER_IDENTITY_ARTIFACT_NAME), "utf-8");
|
|
58
|
+
return JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function shortIdentity(identity) {
|
|
65
|
+
return `${identity.packageName}@${identity.packageVersion} (fingerprint ${identity.packageFingerprint.value}, binary ${identity.binarySha256.slice(0, 12)})`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Recompute the current controller identity and compare it to the pinned run
|
|
69
|
+
* fact before resuming. Fails closed on drift; reports (without inventing an
|
|
70
|
+
* identity). Legacy/unpinned runs remain readable but cannot be resumed.
|
|
71
|
+
*/
|
|
72
|
+
export async function verifyControllerIdentityForResume(input) {
|
|
73
|
+
const ref = input.state.controllerIdentityRef;
|
|
74
|
+
if (!ref || ref.status === "legacy-unpinned") {
|
|
75
|
+
throw new Error(`dag run ${input.state.runId} has no pinned controller identity; legacy/unpinned runs remain readable but cannot be resumed safely`);
|
|
76
|
+
}
|
|
77
|
+
const artifact = await readControllerIdentityArtifact(input.runDir);
|
|
78
|
+
if (!artifact) {
|
|
79
|
+
throw new Error(`dag run ${input.state.runId} is pinned but ${CONTROLLER_IDENTITY_ARTIFACT_NAME} is missing; refuse to resume`);
|
|
80
|
+
}
|
|
81
|
+
const persistedHash = createHash("sha256")
|
|
82
|
+
.update(`${JSON.stringify(artifact, null, 2)}\n`)
|
|
83
|
+
.digest("hex");
|
|
84
|
+
if (persistedHash !== ref.sha256) {
|
|
85
|
+
throw new Error(`dag run ${input.state.runId} controller identity artifact was tampered with (hash mismatch); refuse to resume`);
|
|
86
|
+
}
|
|
87
|
+
const pinned = artifact.identity;
|
|
88
|
+
if (!pinned) {
|
|
89
|
+
throw new Error(`dag run ${input.state.runId} is pinned but the identity artifact has no identity payload; refuse to resume`);
|
|
90
|
+
}
|
|
91
|
+
const current = input.currentIdentity !== undefined
|
|
92
|
+
? input.currentIdentity
|
|
93
|
+
: (resolveRunningControllerIdentity({
|
|
94
|
+
...(input.argv ? { argv: input.argv } : {}),
|
|
95
|
+
...(input.now ? { now: input.now } : {}),
|
|
96
|
+
}) ?? null);
|
|
97
|
+
if (!current) {
|
|
98
|
+
throw new Error(`dag run ${input.state.runId} was pinned to ${shortIdentity(pinned)} but the current controller identity could not be resolved; refuse to resume`);
|
|
99
|
+
}
|
|
100
|
+
if (!controllerIdentitiesMatch(pinned, current)) {
|
|
101
|
+
throw new Error(`dag run ${input.state.runId} controller identity drifted; pinned=${shortIdentity(pinned)} current=${shortIdentity(current)}; start a new run instead of resuming`);
|
|
102
|
+
}
|
|
103
|
+
return { status: "pinned-match", identity: current };
|
|
104
|
+
}
|
|
@@ -5,7 +5,7 @@ import { freshNodeRecord } from "../dynamic-runtime/shared.js";
|
|
|
5
5
|
const CONVERGENCE_CHAIN_NODE_IDS = [
|
|
6
6
|
"process-supervisor-pi",
|
|
7
7
|
"process-gate-shell",
|
|
8
|
-
"repair-
|
|
8
|
+
"repair-pi",
|
|
9
9
|
"hard-verify-shell",
|
|
10
10
|
];
|
|
11
11
|
const CONVERGENCE_NON_RETRY_FAILURES = new Set([
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { executeDagCursorNode } from "../../executors/dag-cursor-executor.js";
|
|
2
1
|
import { executeDagPiNode } from "../../executors/dag-pi-executor.js";
|
|
3
2
|
import { executeDagShellNode } from "../../executors/shell-executor.js";
|
|
4
3
|
import { executeDagStaticNode } from "../../executors/dag-static-executor.js";
|
|
@@ -9,7 +8,6 @@ function requireModel(input) {
|
|
|
9
8
|
return input;
|
|
10
9
|
}
|
|
11
10
|
export const DEFAULT_DAG_EXECUTOR_REGISTRY = {
|
|
12
|
-
cursor: (input, meta) => executeDagCursorNode(requireModel(input), meta),
|
|
13
11
|
pi: (input, meta) => executeDagPiNode(requireModel(input), meta),
|
|
14
12
|
shell: executeDagShellNode,
|
|
15
13
|
static: executeDagStaticNode,
|