@tea-agent/loop-agent 0.10.0 → 0.11.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 +67 -25
- package/README.md +82 -11
- package/dist/application/dag/args.js +1 -12
- package/dist/application/dag/generate-task-dag.js +23 -2
- package/dist/application/dag/run-dag.js +1 -27
- package/dist/application/dag/validate-dag.js +2 -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 +476 -91
- 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 +353 -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 +28 -12
- package/dist/task/delegate.js +9 -21
- package/dist/task/runtime.js +1 -2
- package/dist/worker/cli.js +29 -2
- 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/read-model.js +27 -1
- package/dist/worker/observe/static/app.js +326 -45
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/styles.css +5 -4
- 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/convergence/controller.js +1 -1
- package/dist/workflows/dag/executor-registry.js +0 -2
- package/dist/workflows/dag/init-hybrid.js +402 -25
- package/dist/workflows/dag/node-execution.js +61 -7
- package/dist/workflows/dag/runner.js +45 -17
- 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 +527 -0
- package/dist/workflows/dag/types.js +42 -9
- package/dist/workflows/dag/validate.js +5 -8
- 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 +2 -2
- package/docs/architecture/README.md +26 -0
- package/docs/architecture/dag-execution.md +134 -0
- package/docs/architecture/evolution.md +52 -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 +42 -21
- package/docs/development-principles.md +2 -2
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +12 -0
- package/docs/feature-workflow.md +50 -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 +32 -1
- package/docs/reports/README.md +84 -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 +6 -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 +1 -2
- package/docs/templates/agent-dag.supervised-implementation.json +1 -6
- 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 +13 -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 +15 -18
- 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/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
|
@@ -1,11 +1,55 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { appendFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { computeBinarySha256, computePackageFingerprint, findPackageRoot, isJavaScriptScript, readPackageName, readPackageVersion, resolveBinPaths, resolveCmdShim, } from "../../shared/package-metadata.js";
|
|
5
6
|
import { parseCommandJson } from "./parse-json.js";
|
|
6
7
|
export const DEFAULT_WORKER_COMMAND_TIMEOUT_MS = 120_000;
|
|
7
8
|
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
9
|
+
export const LOOP_AGENT_PACKAGE_NAME = "@tea-agent/loop-agent";
|
|
8
10
|
const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
|
|
11
|
+
/**
|
|
12
|
+
* Bind evidence supplied by an orchestration layer to the identity exposed by
|
|
13
|
+
* the client that will actually launch commands. When the client exposes an
|
|
14
|
+
* identity, it is authoritative; observational fields such as reportedVersion
|
|
15
|
+
* must come from the real preflight probe rather than caller-supplied evidence.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveControllerIdentity(client, provided) {
|
|
18
|
+
const actual = client?.getIdentity?.();
|
|
19
|
+
if (actual && provided && !controllerIdentitiesMatch(actual, provided)) {
|
|
20
|
+
throw new Error("controller identity: provided evidence does not match the command client's pinned identity");
|
|
21
|
+
}
|
|
22
|
+
return actual ?? provided;
|
|
23
|
+
}
|
|
24
|
+
export function controllerIdentitiesMatch(left, right) {
|
|
25
|
+
return Boolean(left && right && identityAnchor(left) === identityAnchor(right));
|
|
26
|
+
}
|
|
27
|
+
/** Pure in-memory expectation gate. It performs no command or filesystem write. */
|
|
28
|
+
export function controllerIdentityExpectationFailure(identity, expectation) {
|
|
29
|
+
if (!expectation)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (!identity) {
|
|
32
|
+
return {
|
|
33
|
+
code: "identity-unavailable",
|
|
34
|
+
message: "controller identity expectation was set but no pinned identity is available",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (expectation.expectedVersion
|
|
38
|
+
&& identity.packageVersion !== expectation.expectedVersion) {
|
|
39
|
+
return {
|
|
40
|
+
code: "expected-version-mismatch",
|
|
41
|
+
message: `expected controller version ${expectation.expectedVersion}, got package version ${identity.packageVersion}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (expectation.expectedFingerprint
|
|
45
|
+
&& identity.packageFingerprint.value !== expectation.expectedFingerprint) {
|
|
46
|
+
return {
|
|
47
|
+
code: "identity-fingerprint-mismatch",
|
|
48
|
+
message: `expected controller fingerprint ${expectation.expectedFingerprint}, got ${identity.packageFingerprint.value}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
9
53
|
export class LoopAgentClient {
|
|
10
54
|
loopAgentBin;
|
|
11
55
|
baseArgs;
|
|
@@ -13,6 +57,10 @@ export class LoopAgentClient {
|
|
|
13
57
|
defaultTimeoutMs;
|
|
14
58
|
heartbeatIntervalMs;
|
|
15
59
|
env;
|
|
60
|
+
resolutionInputs;
|
|
61
|
+
identityRequired;
|
|
62
|
+
_identity;
|
|
63
|
+
_identityResolved = false;
|
|
16
64
|
constructor(options) {
|
|
17
65
|
this.loopAgentBin = options.loopAgentBin;
|
|
18
66
|
this.baseArgs = options.baseArgs ?? [];
|
|
@@ -22,14 +70,136 @@ export class LoopAgentClient {
|
|
|
22
70
|
this.heartbeatIntervalMs =
|
|
23
71
|
options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
24
72
|
this.env = options.env;
|
|
73
|
+
this.resolutionInputs = options.resolutionInputs;
|
|
74
|
+
this.identityRequired = options.resolveIdentity === true;
|
|
75
|
+
if (this.identityRequired)
|
|
76
|
+
this.resolveIdentity();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve and freeze the controller identity.
|
|
80
|
+
*/
|
|
81
|
+
resolveIdentity() {
|
|
82
|
+
if (this._identityResolved) {
|
|
83
|
+
if (!this._identity) {
|
|
84
|
+
throw new Error("controller identity: required identity resolution did not produce an identity");
|
|
85
|
+
}
|
|
86
|
+
return this._identity;
|
|
87
|
+
}
|
|
88
|
+
const resolvedAt = new Date().toISOString();
|
|
89
|
+
const resolved = resolveBinPaths(this.loopAgentBin, resolvedAt, this.resolutionInputs);
|
|
90
|
+
// Detect npm Windows .cmd shim and resolve to the actual JS script
|
|
91
|
+
let actualEntry = resolved.realEntry;
|
|
92
|
+
const isWin = this.resolutionInputs?.platform === "win32" || process.platform === "win32";
|
|
93
|
+
if (isWin) {
|
|
94
|
+
const lower = actualEntry.toLowerCase();
|
|
95
|
+
if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
|
|
96
|
+
const resolvedScript = resolveCmdShim(actualEntry);
|
|
97
|
+
if (resolvedScript) {
|
|
98
|
+
actualEntry = resolvedScript;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
throw new Error(`controller identity: cannot resolve npm .cmd shim: ${resolved.entry}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Detect "running from source" pattern: loopAgentBin is a Node
|
|
106
|
+
// executable and baseArgs points to a script. In this case,
|
|
107
|
+
// actualEntry must be the script, not the Node binary.
|
|
108
|
+
const sourceScript = findSourceScript(this.baseArgs, this.resolutionInputs?.cwd ?? process.cwd());
|
|
109
|
+
if (sourceScript) {
|
|
110
|
+
actualEntry = sourceScript.path;
|
|
111
|
+
}
|
|
112
|
+
// Validate the entry file is readable
|
|
113
|
+
try {
|
|
114
|
+
const s = statSync(actualEntry);
|
|
115
|
+
if (!s.isFile())
|
|
116
|
+
throw new Error(`not a file: ${actualEntry}`);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new Error(`controller identity: bin entry is not readable: ${actualEntry}`);
|
|
120
|
+
}
|
|
121
|
+
const packageRoot = findPackageRoot(path.dirname(actualEntry));
|
|
122
|
+
if (!packageRoot) {
|
|
123
|
+
throw new Error(`controller identity: cannot locate package root for ${actualEntry}`);
|
|
124
|
+
}
|
|
125
|
+
const packageName = readPackageName(packageRoot);
|
|
126
|
+
if (!packageName) {
|
|
127
|
+
throw new Error(`controller identity: package.json at ${packageRoot} is missing a "name" field`);
|
|
128
|
+
}
|
|
129
|
+
if (packageName !== LOOP_AGENT_PACKAGE_NAME) {
|
|
130
|
+
throw new Error(`controller identity: unexpected package name ${JSON.stringify(packageName)} at ${packageRoot}; expected ${JSON.stringify(LOOP_AGENT_PACKAGE_NAME)}`);
|
|
131
|
+
}
|
|
132
|
+
const pkgVersion = readPackageVersion(packageRoot);
|
|
133
|
+
if (!pkgVersion) {
|
|
134
|
+
throw new Error(`controller identity: package.json at ${packageRoot} is missing a "version" field`);
|
|
135
|
+
}
|
|
136
|
+
const binarySha256 = computeBinarySha256(actualEntry);
|
|
137
|
+
if (!binarySha256) {
|
|
138
|
+
throw new Error(`controller identity: cannot read binary for hashing: ${actualEntry}`);
|
|
139
|
+
}
|
|
140
|
+
// Determine launch command and argsPrefix
|
|
141
|
+
let launchCommand;
|
|
142
|
+
let launchArgsPrefix;
|
|
143
|
+
if (sourceScript) {
|
|
144
|
+
launchCommand = resolveNodeBinary(this.loopAgentBin, resolvedAt, this.resolutionInputs);
|
|
145
|
+
launchArgsPrefix = [...this.baseArgs];
|
|
146
|
+
launchArgsPrefix[sourceScript.index] = sourceScript.path;
|
|
147
|
+
}
|
|
148
|
+
else if (isJavaScriptScript(actualEntry)) {
|
|
149
|
+
launchCommand = frozenNodeExecutable();
|
|
150
|
+
launchArgsPrefix = [actualEntry, ...this.baseArgs];
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
launchCommand = actualEntry;
|
|
154
|
+
launchArgsPrefix = [...this.baseArgs];
|
|
155
|
+
}
|
|
156
|
+
const identity = {
|
|
157
|
+
schemaVersion: 1,
|
|
158
|
+
packageName,
|
|
159
|
+
binName: path.basename(actualEntry),
|
|
160
|
+
requested: this.loopAgentBin,
|
|
161
|
+
entry: resolved.entry,
|
|
162
|
+
realEntry: actualEntry,
|
|
163
|
+
launch: { command: launchCommand, argsPrefix: launchArgsPrefix },
|
|
164
|
+
binarySha256,
|
|
165
|
+
packageRoot,
|
|
166
|
+
packageVersion: pkgVersion,
|
|
167
|
+
packageFingerprint: computePackageFingerprint(packageRoot),
|
|
168
|
+
resolvedAt,
|
|
169
|
+
};
|
|
170
|
+
this._identity = identity;
|
|
171
|
+
this._identityResolved = true;
|
|
172
|
+
return identity;
|
|
173
|
+
}
|
|
174
|
+
getIdentity() {
|
|
175
|
+
return this._identity;
|
|
176
|
+
}
|
|
177
|
+
observeReportedVersion(reportedVersion) {
|
|
178
|
+
if (!this._identity) {
|
|
179
|
+
throw new Error("controller identity: cannot record a reported version without a pinned identity");
|
|
180
|
+
}
|
|
181
|
+
if (reportedVersion !== this._identity.packageVersion) {
|
|
182
|
+
throw new Error(`controller identity: reported version ${reportedVersion} does not match package version ${this._identity.packageVersion}`);
|
|
183
|
+
}
|
|
184
|
+
this._identity = { ...this._identity, reportedVersion };
|
|
185
|
+
return this._identity;
|
|
25
186
|
}
|
|
26
187
|
async run(args, options) {
|
|
188
|
+
if (this._identity) {
|
|
189
|
+
assertControllerIdentityUnchangedBeforeSpawn(this._identity);
|
|
190
|
+
const launch = this._identity.launch;
|
|
191
|
+
const fullArgs = [...launch.argsPrefix, ...args];
|
|
192
|
+
return this.runCommand(launch.command, fullArgs, args, options, () => assertControllerIdentityUnchangedBeforeSpawn(this._identity));
|
|
193
|
+
}
|
|
194
|
+
if (this.identityRequired || this._identityResolved) {
|
|
195
|
+
throw new Error("controller identity: pinned launch is unavailable");
|
|
196
|
+
}
|
|
27
197
|
return this.runCommand(this.loopAgentBin, [...this.baseArgs, ...args], args, options);
|
|
28
198
|
}
|
|
29
199
|
async runExternal(command, args, options) {
|
|
30
200
|
return this.runCommand(command, args, args, options);
|
|
31
201
|
}
|
|
32
|
-
async runCommand(command, commandArgs, resultArgs, options) {
|
|
202
|
+
async runCommand(command, commandArgs, resultArgs, options, beforeSpawn) {
|
|
33
203
|
const startedAt = Date.now();
|
|
34
204
|
const artifactDir = path.join(this.artifactRoot, sanitizeName(options.artifactName));
|
|
35
205
|
await mkdir(artifactDir, { recursive: true });
|
|
@@ -42,14 +212,13 @@ export class LoopAgentClient {
|
|
|
42
212
|
cwd: options.cwd,
|
|
43
213
|
timeoutMs: options.timeoutMs ?? this.defaultTimeoutMs,
|
|
44
214
|
env: { ...process.env, ...this.env, ...options.env },
|
|
45
|
-
stdoutPath,
|
|
46
|
-
stderrPath,
|
|
47
|
-
resultPath,
|
|
215
|
+
stdoutPath, stderrPath, resultPath,
|
|
48
216
|
heartbeatIntervalMs: this.heartbeatIntervalMs,
|
|
49
217
|
onSpawn: options.onSpawn,
|
|
50
218
|
onStdout: options.onStdout,
|
|
51
219
|
onStderr: options.onStderr,
|
|
52
220
|
onHeartbeat: options.onHeartbeat,
|
|
221
|
+
beforeSpawn,
|
|
53
222
|
});
|
|
54
223
|
const result = {
|
|
55
224
|
ok: exitCode === 0 && !timedOut,
|
|
@@ -58,19 +227,10 @@ export class LoopAgentClient {
|
|
|
58
227
|
cwd: options.cwd,
|
|
59
228
|
durationMs: Date.now() - startedAt,
|
|
60
229
|
exitCode,
|
|
61
|
-
stdout,
|
|
62
|
-
|
|
63
|
-
stdoutTruncated,
|
|
64
|
-
stderr,
|
|
65
|
-
stderrBytes,
|
|
66
|
-
stderrTruncated,
|
|
230
|
+
stdout, stdoutBytes, stdoutTruncated,
|
|
231
|
+
stderr, stderrBytes, stderrTruncated,
|
|
67
232
|
timedOut,
|
|
68
|
-
artifacts: {
|
|
69
|
-
dir: artifactDir,
|
|
70
|
-
stdoutPath,
|
|
71
|
-
stderrPath,
|
|
72
|
-
resultPath,
|
|
73
|
-
},
|
|
233
|
+
artifacts: { dir: artifactDir, stdoutPath, stderrPath, resultPath },
|
|
74
234
|
};
|
|
75
235
|
if (options.expectJson) {
|
|
76
236
|
const jsonSource = stdoutTruncated
|
|
@@ -89,10 +249,104 @@ export class LoopAgentClient {
|
|
|
89
249
|
return result;
|
|
90
250
|
}
|
|
91
251
|
}
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// Internal helpers
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
function assertControllerIdentityUnchangedBeforeSpawn(identity) {
|
|
256
|
+
const currentPackageRoot = findPackageRoot(path.dirname(identity.realEntry));
|
|
257
|
+
if (currentPackageRoot !== identity.packageRoot) {
|
|
258
|
+
throw controllerIdentityDriftError(`package root changed from ${JSON.stringify(identity.packageRoot)} to ${JSON.stringify(currentPackageRoot)}`);
|
|
259
|
+
}
|
|
260
|
+
const currentPackageName = readPackageName(identity.packageRoot);
|
|
261
|
+
if (currentPackageName !== identity.packageName) {
|
|
262
|
+
throw controllerIdentityDriftError(`package name changed from ${JSON.stringify(identity.packageName)} to ${JSON.stringify(currentPackageName)}`);
|
|
263
|
+
}
|
|
264
|
+
const currentPackageVersion = readPackageVersion(identity.packageRoot);
|
|
265
|
+
if (currentPackageVersion !== identity.packageVersion) {
|
|
266
|
+
throw controllerIdentityDriftError(`package version changed from ${JSON.stringify(identity.packageVersion)} to ${JSON.stringify(currentPackageVersion)}`);
|
|
267
|
+
}
|
|
268
|
+
const currentBinarySha256 = computeBinarySha256(identity.realEntry);
|
|
269
|
+
if (currentBinarySha256 !== identity.binarySha256) {
|
|
270
|
+
throw controllerIdentityDriftError(`entry binary sha256 changed from ${identity.binarySha256} to ${currentBinarySha256 ?? "unreadable"}`);
|
|
271
|
+
}
|
|
272
|
+
let currentPackageFingerprint;
|
|
273
|
+
try {
|
|
274
|
+
currentPackageFingerprint = computePackageFingerprint(identity.packageRoot);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
throw controllerIdentityDriftError(`package fingerprint cannot be recomputed: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
278
|
+
}
|
|
279
|
+
if (currentPackageFingerprint.algorithm !== identity.packageFingerprint.algorithm
|
|
280
|
+
|| currentPackageFingerprint.scopeVersion !== identity.packageFingerprint.scopeVersion
|
|
281
|
+
|| JSON.stringify(currentPackageFingerprint.scope)
|
|
282
|
+
!== JSON.stringify(identity.packageFingerprint.scope)
|
|
283
|
+
|| currentPackageFingerprint.value !== identity.packageFingerprint.value
|
|
284
|
+
|| currentPackageFingerprint.fileCount !== identity.packageFingerprint.fileCount) {
|
|
285
|
+
throw controllerIdentityDriftError(`package fingerprint changed from ${identity.packageFingerprint.value} to ${currentPackageFingerprint.value}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function controllerIdentityDriftError(message, cause) {
|
|
289
|
+
return new Error(`controller identity changed before spawn: ${message}`, {
|
|
290
|
+
cause,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Detect whether loopAgentBin is a Node-like executable and baseArgs
|
|
295
|
+
* contains a script file. Returns the absolute script path, or undefined.
|
|
296
|
+
*/
|
|
297
|
+
function findSourceScript(baseArgs, cwd) {
|
|
298
|
+
if (baseArgs.length === 0)
|
|
299
|
+
return undefined;
|
|
300
|
+
// Look for the last .ts/.js/.mjs/.cjs file in baseArgs
|
|
301
|
+
let scriptIdx = -1;
|
|
302
|
+
for (let i = baseArgs.length - 1; i >= 0; i--) {
|
|
303
|
+
const ext = path.extname(baseArgs[i]).toLowerCase();
|
|
304
|
+
if (ext === ".ts" || ext === ".js" || ext === ".mjs" || ext === ".cjs") {
|
|
305
|
+
scriptIdx = i;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (scriptIdx < 0)
|
|
310
|
+
return undefined;
|
|
311
|
+
const scriptPath = path.resolve(cwd, baseArgs[scriptIdx]);
|
|
312
|
+
try {
|
|
313
|
+
const s = statSync(scriptPath);
|
|
314
|
+
if (s.isFile())
|
|
315
|
+
return { index: scriptIdx, path: realpathSync(scriptPath) };
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// script not found
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Resolve the Node binary to an absolute path.
|
|
324
|
+
*/
|
|
325
|
+
function resolveNodeBinary(loopAgentBin, resolvedAt, inputs) {
|
|
326
|
+
if (path.isAbsolute(loopAgentBin))
|
|
327
|
+
return realpathSync(loopAgentBin);
|
|
328
|
+
const resolved = resolveBinPaths(loopAgentBin, resolvedAt, inputs);
|
|
329
|
+
return resolved.realEntry;
|
|
330
|
+
}
|
|
331
|
+
function frozenNodeExecutable() {
|
|
332
|
+
try {
|
|
333
|
+
return realpathSync(process.execPath);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return process.execPath;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
92
339
|
function spawnCommand(input) {
|
|
93
340
|
return new Promise((resolve, reject) => {
|
|
94
341
|
const startedAtMs = Date.now();
|
|
95
342
|
const startedAt = new Date(startedAtMs).toISOString();
|
|
343
|
+
try {
|
|
344
|
+
input.beforeSpawn?.();
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
reject(error);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
96
350
|
const child = spawn(input.command, input.args, {
|
|
97
351
|
cwd: input.cwd,
|
|
98
352
|
env: input.env,
|
|
@@ -103,136 +357,85 @@ function spawnCommand(input) {
|
|
|
103
357
|
writeFileSync(input.stdoutPath, "", "utf-8");
|
|
104
358
|
writeFileSync(input.stderrPath, "", "utf-8");
|
|
105
359
|
}
|
|
106
|
-
catch {
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
invokeSpawnCallback(input.onSpawn, {
|
|
110
|
-
pid: child.pid,
|
|
111
|
-
startedAt,
|
|
112
|
-
artifactRefs: {
|
|
113
|
-
stdoutPath: input.stdoutPath,
|
|
114
|
-
stderrPath: input.stderrPath,
|
|
115
|
-
resultPath: input.resultPath,
|
|
116
|
-
},
|
|
117
|
-
});
|
|
360
|
+
catch { /* best-effort */ }
|
|
361
|
+
invokeSpawnCallback(input.onSpawn, { pid: child.pid, startedAt, artifactRefs: { stdoutPath: input.stdoutPath, stderrPath: input.stderrPath, resultPath: input.resultPath } });
|
|
118
362
|
let stdout = createBoundedOutput();
|
|
119
363
|
let stderr = createBoundedOutput();
|
|
120
364
|
let timedOut = false;
|
|
121
|
-
const timeout = setTimeout(() => {
|
|
122
|
-
timedOut = true;
|
|
123
|
-
child.kill("SIGTERM");
|
|
124
|
-
}, input.timeoutMs);
|
|
365
|
+
const timeout = setTimeout(() => { timedOut = true; child.kill("SIGTERM"); }, input.timeoutMs);
|
|
125
366
|
let heartbeatTimer;
|
|
126
367
|
if (input.heartbeatIntervalMs > 0) {
|
|
127
|
-
heartbeatTimer = setInterval(() => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (heartbeatTimer !== undefined) {
|
|
134
|
-
clearInterval(heartbeatTimer);
|
|
135
|
-
heartbeatTimer = undefined;
|
|
136
|
-
}
|
|
137
|
-
};
|
|
368
|
+
heartbeatTimer = setInterval(() => { invokeHeartbeatCallback(input.onHeartbeat, startedAtMs); }, input.heartbeatIntervalMs);
|
|
369
|
+
}
|
|
370
|
+
const clearTimers = () => { clearTimeout(timeout); if (heartbeatTimer !== undefined) {
|
|
371
|
+
clearInterval(heartbeatTimer);
|
|
372
|
+
heartbeatTimer = undefined;
|
|
373
|
+
} };
|
|
138
374
|
child.stdout.setEncoding("utf8");
|
|
139
375
|
child.stderr.setEncoding("utf8");
|
|
140
|
-
child.stdout.on("data", (chunk) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
invokeChunkCallback(input.onStdout, chunk);
|
|
144
|
-
});
|
|
145
|
-
child.stderr.on("data", (chunk) => {
|
|
146
|
-
stderr = appendBoundedOutput(stderr, chunk);
|
|
147
|
-
appendChunkBestEffort(input.stderrPath, chunk);
|
|
148
|
-
invokeChunkCallback(input.onStderr, chunk);
|
|
149
|
-
});
|
|
150
|
-
child.on("error", (error) => {
|
|
151
|
-
clearTimers();
|
|
152
|
-
reject(error);
|
|
153
|
-
});
|
|
376
|
+
child.stdout.on("data", (chunk) => { stdout = appendBoundedOutput(stdout, chunk); appendChunkBestEffort(input.stdoutPath, chunk); invokeChunkCallback(input.onStdout, chunk); });
|
|
377
|
+
child.stderr.on("data", (chunk) => { stderr = appendBoundedOutput(stderr, chunk); appendChunkBestEffort(input.stderrPath, chunk); invokeChunkCallback(input.onStderr, chunk); });
|
|
378
|
+
child.on("error", (error) => { clearTimers(); reject(error); });
|
|
154
379
|
child.on("close", (exitCode) => {
|
|
155
380
|
clearTimers();
|
|
156
|
-
resolve({
|
|
157
|
-
stdout: formatBoundedOutput(stdout),
|
|
158
|
-
stdoutBytes: stdout.bytes,
|
|
159
|
-
stdoutTruncated: stdout.truncated,
|
|
160
|
-
stderr: formatBoundedOutput(stderr),
|
|
161
|
-
stderrBytes: stderr.bytes,
|
|
162
|
-
stderrTruncated: stderr.truncated,
|
|
163
|
-
exitCode,
|
|
164
|
-
timedOut,
|
|
165
|
-
});
|
|
381
|
+
resolve({ stdout: formatBoundedOutput(stdout), stdoutBytes: stdout.bytes, stdoutTruncated: stdout.truncated, stderr: formatBoundedOutput(stderr), stderrBytes: stderr.bytes, stderrTruncated: stderr.truncated, exitCode, timedOut });
|
|
166
382
|
});
|
|
167
383
|
});
|
|
168
384
|
}
|
|
169
|
-
function createBoundedOutput() {
|
|
170
|
-
return { bytes: 0, text: "", truncated: false };
|
|
171
|
-
}
|
|
385
|
+
function createBoundedOutput() { return { bytes: 0, text: "", truncated: false }; }
|
|
172
386
|
function appendBoundedOutput(current, chunk) {
|
|
173
387
|
const bytes = current.bytes + Buffer.byteLength(chunk);
|
|
174
388
|
const combined = current.text + chunk;
|
|
175
|
-
if (Buffer.byteLength(combined) <= MAX_BUFFERED_OUTPUT_BYTES)
|
|
389
|
+
if (Buffer.byteLength(combined) <= MAX_BUFFERED_OUTPUT_BYTES)
|
|
176
390
|
return { bytes, text: combined, truncated: current.truncated };
|
|
177
|
-
|
|
178
|
-
const tail = Buffer.from(combined)
|
|
179
|
-
.subarray(-MAX_BUFFERED_OUTPUT_BYTES)
|
|
180
|
-
.toString("utf-8")
|
|
181
|
-
.replace(/^\uFFFD/, "");
|
|
391
|
+
const tail = Buffer.from(combined).subarray(-MAX_BUFFERED_OUTPUT_BYTES).toString("utf-8").replace(/^\uFFFD/, "");
|
|
182
392
|
return { bytes, text: tail, truncated: true };
|
|
183
393
|
}
|
|
184
|
-
function formatBoundedOutput(output) {
|
|
185
|
-
|
|
394
|
+
function formatBoundedOutput(output) { return output.truncated ? `${output.text}\n...[truncated]` : output.text; }
|
|
395
|
+
async function readFullOutputArtifact(filePath, fallback) { try {
|
|
396
|
+
return await readFile(filePath, "utf-8");
|
|
186
397
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return fallback;
|
|
193
|
-
}
|
|
398
|
+
catch {
|
|
399
|
+
return fallback;
|
|
400
|
+
} }
|
|
401
|
+
function appendChunkBestEffort(filePath, chunk) { try {
|
|
402
|
+
appendFileSync(filePath, chunk, "utf-8");
|
|
194
403
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
catch {
|
|
200
|
-
// best-effort: degrade to in-memory buffer only
|
|
201
|
-
}
|
|
404
|
+
catch { /* best-effort */ } }
|
|
405
|
+
function invokeSpawnCallback(cb, info) { if (!cb)
|
|
406
|
+
return; try {
|
|
407
|
+
cb(info);
|
|
202
408
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
process.stderr.write(`LoopAgentClient onSpawn callback error: ${String(error)}\n`);
|
|
212
|
-
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
process.stderr.write(`LoopAgentClient onSpawn callback error: ${String(error)}\n`);
|
|
411
|
+
} }
|
|
412
|
+
function invokeChunkCallback(cb, chunk) { if (!cb)
|
|
413
|
+
return; try {
|
|
414
|
+
cb(chunk);
|
|
213
415
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
catch (error) {
|
|
222
|
-
process.stderr.write(`LoopAgentClient output callback error: ${String(error)}\n`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
function invokeHeartbeatCallback(callback, startedAtMs) {
|
|
226
|
-
if (!callback) {
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
try {
|
|
230
|
-
callback({ at: new Date().toISOString(), elapsedMs: Date.now() - startedAtMs });
|
|
231
|
-
}
|
|
232
|
-
catch (error) {
|
|
233
|
-
process.stderr.write(`LoopAgentClient onHeartbeat callback error: ${String(error)}\n`);
|
|
234
|
-
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
process.stderr.write(`LoopAgentClient output callback error: ${String(error)}\n`);
|
|
418
|
+
} }
|
|
419
|
+
function invokeHeartbeatCallback(cb, startedAtMs) { if (!cb)
|
|
420
|
+
return; try {
|
|
421
|
+
cb({ at: new Date().toISOString(), elapsedMs: Date.now() - startedAtMs });
|
|
235
422
|
}
|
|
236
|
-
|
|
237
|
-
|
|
423
|
+
catch (error) {
|
|
424
|
+
process.stderr.write(`LoopAgentClient onHeartbeat callback error: ${String(error)}\n`);
|
|
425
|
+
} }
|
|
426
|
+
function sanitizeName(name) { return name.replace(/[^a-zA-Z0-9._-]+/g, "-"); }
|
|
427
|
+
function identityAnchor(identity) {
|
|
428
|
+
return JSON.stringify({
|
|
429
|
+
schemaVersion: identity.schemaVersion,
|
|
430
|
+
packageName: identity.packageName,
|
|
431
|
+
binName: identity.binName,
|
|
432
|
+
requested: identity.requested,
|
|
433
|
+
entry: identity.entry,
|
|
434
|
+
realEntry: identity.realEntry,
|
|
435
|
+
launch: identity.launch,
|
|
436
|
+
binarySha256: identity.binarySha256,
|
|
437
|
+
packageRoot: identity.packageRoot,
|
|
438
|
+
packageVersion: identity.packageVersion,
|
|
439
|
+
packageFingerprint: identity.packageFingerprint,
|
|
440
|
+
});
|
|
238
441
|
}
|
|
@@ -1161,7 +1161,33 @@ async function loadDagNodeModels(runPath) {
|
|
|
1161
1161
|
}
|
|
1162
1162
|
}
|
|
1163
1163
|
catch {
|
|
1164
|
-
// Historical specs may predate the current schema
|
|
1164
|
+
// Historical specs may predate the current schema (including the removed
|
|
1165
|
+
// Cursor executor). Preserve their recorded model labels for observation
|
|
1166
|
+
// without allowing those specs back into the executable DAG path.
|
|
1167
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
1168
|
+
return models;
|
|
1169
|
+
const legacy = raw;
|
|
1170
|
+
const tasks = legacy.tasks;
|
|
1171
|
+
const executorModels = legacy.executorModels;
|
|
1172
|
+
if (!Array.isArray(tasks) || typeof executorModels !== "object" || executorModels === null || Array.isArray(executorModels)) {
|
|
1173
|
+
return models;
|
|
1174
|
+
}
|
|
1175
|
+
for (const task of tasks) {
|
|
1176
|
+
if (!task || typeof task !== "object" || Array.isArray(task))
|
|
1177
|
+
continue;
|
|
1178
|
+
const taskRecord = task;
|
|
1179
|
+
const taskId = readString(taskRecord, "id");
|
|
1180
|
+
const executor = readString(taskRecord, "executor") ?? "pi";
|
|
1181
|
+
const complexity = readString(taskRecord, "complexity");
|
|
1182
|
+
if (!taskId || !complexity)
|
|
1183
|
+
continue;
|
|
1184
|
+
const matrix = executorModels[executor];
|
|
1185
|
+
if (!matrix || typeof matrix !== "object" || Array.isArray(matrix))
|
|
1186
|
+
continue;
|
|
1187
|
+
const model = readString(matrix, complexity);
|
|
1188
|
+
if (model)
|
|
1189
|
+
models.set(taskId, model);
|
|
1190
|
+
}
|
|
1165
1191
|
}
|
|
1166
1192
|
return models;
|
|
1167
1193
|
}
|