@tea-agent/loop-agent 0.8.0 → 0.10.0-alpha.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.
Files changed (188) hide show
  1. package/AGENTS.md +10 -0
  2. package/CHANGELOG.md +101 -1
  3. package/README.md +69 -5
  4. package/dist/application/dag/args.js +13 -16
  5. package/dist/application/dag/generate-task-dag.js +32 -2
  6. package/dist/application/dag/run-dag.js +1 -27
  7. package/dist/application/dag/validate-dag.js +2 -2
  8. package/dist/application/loop/run-action.js +0 -4
  9. package/dist/cli/command-definitions.js +7 -11
  10. package/dist/cli/program.js +9 -21
  11. package/dist/commands/cursor-prompt.js +42 -82
  12. package/dist/commands/dag-approve.js +36 -0
  13. package/dist/commands/dag-reconcile-run.js +118 -0
  14. package/dist/commands/delegate.js +75 -77
  15. package/dist/commands/doctor.js +0 -18
  16. package/dist/commands/init.js +60 -40
  17. package/dist/commands/instructions.js +7 -10
  18. package/dist/commands/loop.js +4 -20
  19. package/dist/executors/config-core.js +0 -51
  20. package/dist/executors/dag-pi-executor.js +1 -1
  21. package/dist/executors/dag.js +0 -1
  22. package/dist/executors/index.js +0 -2
  23. package/dist/executors/model-routing.js +9 -9
  24. package/dist/executors/shell-executor.js +75 -9
  25. package/dist/governance/checks.js +6 -3
  26. package/dist/governance/manifest-types.js +33 -2
  27. package/dist/infrastructure/harness/loop-action-store.js +0 -3
  28. package/dist/records/harvest.js +2 -23
  29. package/dist/records/one-shot-runs.js +1 -1
  30. package/dist/shared/artifacts-core.js +24 -5
  31. package/dist/shared/output-truncation.js +37 -0
  32. package/dist/shared/package-metadata.js +353 -0
  33. package/dist/shared/reference-context.js +48 -22
  34. package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
  35. package/dist/sidecars/cursor-prompt/index.js +3 -0
  36. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  37. package/dist/task/config-types.js +30 -13
  38. package/dist/task/delegate.js +9 -21
  39. package/dist/task/runtime.js +2 -3
  40. package/dist/worker/cli.js +243 -0
  41. package/dist/worker/closeout/apply.js +73 -0
  42. package/dist/worker/closeout/preview.js +30 -0
  43. package/dist/worker/delivery/final-verification.js +194 -0
  44. package/dist/worker/delivery/git-transaction.js +354 -0
  45. package/dist/worker/delivery/package.js +502 -0
  46. package/dist/worker/feature/decision-loader.js +68 -0
  47. package/dist/worker/feature/discover.js +14 -0
  48. package/dist/worker/feature/next-action.js +74 -0
  49. package/dist/worker/feature/reducer.js +133 -0
  50. package/dist/worker/feature/review.js +502 -0
  51. package/dist/worker/feature/run.js +365 -0
  52. package/dist/worker/feature/types.js +1 -0
  53. package/dist/worker/follow-up/approve.js +270 -0
  54. package/dist/worker/follow-up/factory.js +234 -0
  55. package/dist/worker/follow-up/paths.js +25 -0
  56. package/dist/worker/follow-up/policy.js +26 -0
  57. package/dist/worker/follow-up/schema.js +93 -0
  58. package/dist/worker/follow-up/store.js +96 -0
  59. package/dist/worker/loop-agent/loop-agent-client.js +345 -101
  60. package/dist/worker/metrics/projector.js +139 -0
  61. package/dist/worker/observability/read-model.js +282 -15
  62. package/dist/worker/observe/paths.js +17 -5
  63. package/dist/worker/observe/routes.js +78 -20
  64. package/dist/worker/observe/server.js +8 -6
  65. package/dist/worker/observe/static/app.js +1045 -177
  66. package/dist/worker/observe/static/index.html +70 -43
  67. package/dist/worker/observe/static/styles.css +553 -610
  68. package/dist/worker/pool/run-store.js +14 -2
  69. package/dist/worker/pool/validation.js +59 -0
  70. package/dist/worker/preflight.js +49 -1
  71. package/dist/worker/report/morning-report.js +41 -6
  72. package/dist/worker/run-task/run-task.js +23 -13
  73. package/dist/worker/runner/run-ready.js +89 -11
  74. package/dist/worker/task-spec/schema.js +0 -1
  75. package/dist/workflows/dag/convergence/controller.js +1 -1
  76. package/dist/workflows/dag/executor-registry.js +0 -2
  77. package/dist/workflows/dag/governance-profile.js +10 -0
  78. package/dist/workflows/dag/init-hybrid.js +601 -26
  79. package/dist/workflows/dag/lifecycle.js +146 -0
  80. package/dist/workflows/dag/node-execution.js +64 -7
  81. package/dist/workflows/dag/prompt.js +16 -0
  82. package/dist/workflows/dag/report.js +2 -0
  83. package/dist/workflows/dag/runner.js +176 -119
  84. package/dist/workflows/dag/scheduler.js +7 -2
  85. package/dist/workflows/dag/skill-snapshot.js +527 -0
  86. package/dist/workflows/dag/types.js +45 -9
  87. package/dist/workflows/dag/validate.js +5 -8
  88. package/dist/workflows/loop/actions/dag-action.js +0 -2
  89. package/dist/workflows/loop/actions/shared.js +1 -1
  90. package/dist/workflows/loop/actions.js +14 -31
  91. package/dist/workflows/loop/benchmark.js +1 -1
  92. package/dist/workflows/loop/index.js +1 -1
  93. package/dist/workflows/loop/policy/auto-policy.js +22 -14
  94. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  95. package/docs/README.md +35 -12
  96. package/docs/agent-dag-recovery-playbook.md +1 -1
  97. package/docs/architecture/README.md +26 -0
  98. package/docs/architecture/dag-execution.md +134 -0
  99. package/docs/architecture/evolution.md +52 -0
  100. package/docs/architecture/facts-and-state.md +58 -0
  101. package/docs/architecture/runtime-boundaries.md +41 -15
  102. package/docs/architecture/system-overview.md +93 -0
  103. package/docs/architecture/worker-and-feature.md +81 -0
  104. package/docs/cursor-prompt-sidecar.md +36 -0
  105. package/docs/decisions/README.md +13 -1
  106. package/docs/design/README.md +39 -13
  107. package/docs/development-principles.md +1 -1
  108. package/docs/exec-plans/active/README.md +2 -2
  109. package/docs/exec-plans/completed/README.md +21 -0
  110. package/docs/feature-workflow.md +44 -4
  111. package/docs/init-surface.manifest.json +63 -1
  112. package/docs/loop-agent-harness.md +65 -3
  113. package/docs/progress/README.md +27 -0
  114. package/docs/reports/README.md +74 -5
  115. package/docs/skills/README.md +2 -1
  116. package/docs/skills/vetted-skill-registry.md +2 -1
  117. package/docs/templates/agent-dag-report.schema.json +4 -2
  118. package/docs/templates/agent-dag.base.json +0 -5
  119. package/docs/templates/agent-dag.final-verification.json +0 -5
  120. package/docs/templates/agent-dag.schema.json +1 -2
  121. package/docs/templates/agent-dag.supervised-implementation.json +1 -6
  122. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +131 -0
  123. package/docs/templates/backend-test-dag.json +213 -0
  124. package/docs/templates/backend-test-dag.retrospect.prompt.md +128 -0
  125. package/docs/templates/backend-test-dag.review-cases.prompt.md +85 -0
  126. package/docs/templates/frontend-design-contract.md +33 -0
  127. package/docs/templates/frontend-task-constraints.md +25 -0
  128. package/docs/templates/frontend-task-requirement.md +61 -0
  129. package/docs/templates/harness.schema.json +8 -5
  130. package/docs/templates/hybrid-dag.json +1 -6
  131. package/docs/templates/init-evolution-review.md +4 -2
  132. package/docs/templates/interactive-ui-round2-experiment.md +1 -1
  133. package/docs/templates/product-line/task.yaml +0 -1
  134. package/docs/templates/worker-dogfood-evidence.md +28 -0
  135. package/docs/templates/worker-dogfood-setup.md +20 -0
  136. package/docs/verification-matrix.md +17 -0
  137. package/examples/decision-gate-agent-dag.json +87 -33
  138. package/examples/example-dag.json +0 -5
  139. package/examples/hybrid-loop-agent-dag.json +0 -5
  140. package/harness.json +6 -11
  141. package/package.json +22 -44
  142. package/scripts/check-product-line-docs.sh +10 -3
  143. package/scripts/check-task-pool-root.sh +1 -1
  144. package/skills/agent-worker/SKILL.md +37 -0
  145. package/skills/agent-worker/references/agent-worker-operator.md +43 -0
  146. package/skills/frontend-design-review/SKILL.md +59 -0
  147. package/skills/frontend-design-review/references/review-checklist.md +37 -0
  148. package/skills/frontend-implementation/SKILL.md +48 -0
  149. package/skills/frontend-implementation/references/code-standards.md +34 -0
  150. package/skills/frontend-implementation/references/design-spec.md +46 -0
  151. package/skills/frontend-implementation/references/node-contracts.md +32 -0
  152. package/skills/frontend-review/SKILL.md +53 -0
  153. package/skills/frontend-review/references/review-findings.md +42 -0
  154. package/skills/frontend-verification/SKILL.md +40 -0
  155. package/skills/frontend-verification/references/verification-checklist.md +56 -0
  156. package/skills/grill-me/SKILL.md +10 -0
  157. package/skills/grill-with-docs/SKILL.md +88 -0
  158. package/skills/grill-with-docs/adr-format.md +47 -0
  159. package/skills/grill-with-docs/context-format.md +60 -0
  160. package/skills/init-capability-evolution/SKILL.md +1 -0
  161. package/skills/loop-agent/SKILL.md +11 -9
  162. package/skills/loop-agent/references/command-reference.md +28 -15
  163. package/skills/loop-agent/references/docs-converge.md +126 -0
  164. package/skills/loop-agent/references/harness-policy.md +7 -7
  165. package/skills/loop-agent/references/hybrid-dag.md +13 -15
  166. package/skills/loop-agent/references/long-running-loop.md +4 -6
  167. package/skills/loop-agent/references/multi-worktree.md +6 -6
  168. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  169. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
  170. package/skills/loop-agent/references/verification-and-failure-handling.md +8 -3
  171. package/skills/using-git-worktrees/SKILL.md +215 -0
  172. package/dist/commands/cursor-worker.js +0 -43
  173. package/dist/cursor-worker-entry.js +0 -8
  174. package/dist/executors/cursor-artifacts.js +0 -33
  175. package/dist/executors/cursor-execution-log.js +0 -81
  176. package/dist/executors/cursor-executor-artifacts.js +0 -134
  177. package/dist/executors/cursor-run.js +0 -115
  178. package/dist/executors/cursor-tool.js +0 -94
  179. package/dist/executors/cursor-worker-client.js +0 -223
  180. package/dist/executors/cursor-worker-protocol.js +0 -18
  181. package/dist/executors/cursor-worker-server.js +0 -54
  182. package/dist/executors/cursor-worker.js +0 -3
  183. package/dist/executors/cursor.js +0 -6
  184. package/dist/executors/dag-cursor-executor.js +0 -87
  185. package/dist/workflows/loop/actions/cursor-fix.js +0 -191
  186. package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
  187. package/docs/cursor-executor-usage.md +0 -25
  188. package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
@@ -1,10 +1,55 @@
1
1
  import { spawn } from "node:child_process";
2
- import { appendFileSync, writeFileSync } from "node:fs";
3
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { appendFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
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";
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
+ }
8
53
  export class LoopAgentClient {
9
54
  loopAgentBin;
10
55
  baseArgs;
@@ -12,6 +57,10 @@ export class LoopAgentClient {
12
57
  defaultTimeoutMs;
13
58
  heartbeatIntervalMs;
14
59
  env;
60
+ resolutionInputs;
61
+ identityRequired;
62
+ _identity;
63
+ _identityResolved = false;
15
64
  constructor(options) {
16
65
  this.loopAgentBin = options.loopAgentBin;
17
66
  this.baseArgs = options.baseArgs ?? [];
@@ -21,34 +70,155 @@ export class LoopAgentClient {
21
70
  this.heartbeatIntervalMs =
22
71
  options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
23
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;
24
186
  }
25
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
+ }
26
197
  return this.runCommand(this.loopAgentBin, [...this.baseArgs, ...args], args, options);
27
198
  }
28
199
  async runExternal(command, args, options) {
29
200
  return this.runCommand(command, args, args, options);
30
201
  }
31
- async runCommand(command, commandArgs, resultArgs, options) {
202
+ async runCommand(command, commandArgs, resultArgs, options, beforeSpawn) {
32
203
  const startedAt = Date.now();
33
204
  const artifactDir = path.join(this.artifactRoot, sanitizeName(options.artifactName));
34
205
  await mkdir(artifactDir, { recursive: true });
35
206
  const stdoutPath = path.join(artifactDir, "stdout.txt");
36
207
  const stderrPath = path.join(artifactDir, "stderr.txt");
37
208
  const resultPath = path.join(artifactDir, "result.json");
38
- const { stdout, stderr, exitCode, timedOut } = await spawnCommand({
209
+ const { stdout, stdoutBytes, stdoutTruncated, stderr, stderrBytes, stderrTruncated, exitCode, timedOut, } = await spawnCommand({
39
210
  command,
40
211
  args: commandArgs,
41
212
  cwd: options.cwd,
42
213
  timeoutMs: options.timeoutMs ?? this.defaultTimeoutMs,
43
214
  env: { ...process.env, ...this.env, ...options.env },
44
- stdoutPath,
45
- stderrPath,
46
- resultPath,
215
+ stdoutPath, stderrPath, resultPath,
47
216
  heartbeatIntervalMs: this.heartbeatIntervalMs,
48
217
  onSpawn: options.onSpawn,
49
218
  onStdout: options.onStdout,
50
219
  onStderr: options.onStderr,
51
220
  onHeartbeat: options.onHeartbeat,
221
+ beforeSpawn,
52
222
  });
53
223
  const result = {
54
224
  ok: exitCode === 0 && !timedOut,
@@ -57,18 +227,16 @@ export class LoopAgentClient {
57
227
  cwd: options.cwd,
58
228
  durationMs: Date.now() - startedAt,
59
229
  exitCode,
60
- stdout,
61
- stderr,
230
+ stdout, stdoutBytes, stdoutTruncated,
231
+ stderr, stderrBytes, stderrTruncated,
62
232
  timedOut,
63
- artifacts: {
64
- dir: artifactDir,
65
- stdoutPath,
66
- stderrPath,
67
- resultPath,
68
- },
233
+ artifacts: { dir: artifactDir, stdoutPath, stderrPath, resultPath },
69
234
  };
70
235
  if (options.expectJson) {
71
- const parsed = parseCommandJson(stdout);
236
+ const jsonSource = stdoutTruncated
237
+ ? await readFullOutputArtifact(stdoutPath, stdout)
238
+ : stdout;
239
+ const parsed = parseCommandJson(jsonSource);
72
240
  if (parsed.ok) {
73
241
  result.json = parsed.value;
74
242
  }
@@ -77,16 +245,108 @@ export class LoopAgentClient {
77
245
  result.parseFailure = parsed.failure;
78
246
  }
79
247
  }
80
- await writeFile(result.artifacts.stdoutPath, stdout, "utf-8");
81
- await writeFile(result.artifacts.stderrPath, stderr, "utf-8");
82
248
  await writeFile(result.artifacts.resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
83
249
  return result;
84
250
  }
85
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
+ }
86
339
  function spawnCommand(input) {
87
340
  return new Promise((resolve, reject) => {
88
341
  const startedAtMs = Date.now();
89
342
  const startedAt = new Date(startedAtMs).toISOString();
343
+ try {
344
+ input.beforeSpawn?.();
345
+ }
346
+ catch (error) {
347
+ reject(error);
348
+ return;
349
+ }
90
350
  const child = spawn(input.command, input.args, {
91
351
  cwd: input.cwd,
92
352
  env: input.env,
@@ -97,101 +357,85 @@ function spawnCommand(input) {
97
357
  writeFileSync(input.stdoutPath, "", "utf-8");
98
358
  writeFileSync(input.stderrPath, "", "utf-8");
99
359
  }
100
- catch {
101
- // best-effort: degrade to in-memory buffer only
102
- }
103
- invokeSpawnCallback(input.onSpawn, {
104
- pid: child.pid,
105
- startedAt,
106
- artifactRefs: {
107
- stdoutPath: input.stdoutPath,
108
- stderrPath: input.stderrPath,
109
- resultPath: input.resultPath,
110
- },
111
- });
112
- let stdout = "";
113
- let stderr = "";
360
+ catch { /* best-effort */ }
361
+ invokeSpawnCallback(input.onSpawn, { pid: child.pid, startedAt, artifactRefs: { stdoutPath: input.stdoutPath, stderrPath: input.stderrPath, resultPath: input.resultPath } });
362
+ let stdout = createBoundedOutput();
363
+ let stderr = createBoundedOutput();
114
364
  let timedOut = false;
115
- const timeout = setTimeout(() => {
116
- timedOut = true;
117
- child.kill("SIGTERM");
118
- }, input.timeoutMs);
365
+ const timeout = setTimeout(() => { timedOut = true; child.kill("SIGTERM"); }, input.timeoutMs);
119
366
  let heartbeatTimer;
120
367
  if (input.heartbeatIntervalMs > 0) {
121
- heartbeatTimer = setInterval(() => {
122
- invokeHeartbeatCallback(input.onHeartbeat, startedAtMs);
123
- }, input.heartbeatIntervalMs);
124
- }
125
- const clearTimers = () => {
126
- clearTimeout(timeout);
127
- if (heartbeatTimer !== undefined) {
128
- clearInterval(heartbeatTimer);
129
- heartbeatTimer = undefined;
130
- }
131
- };
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
+ } };
132
374
  child.stdout.setEncoding("utf8");
133
375
  child.stderr.setEncoding("utf8");
134
- child.stdout.on("data", (chunk) => {
135
- stdout += chunk;
136
- appendChunkBestEffort(input.stdoutPath, chunk);
137
- invokeChunkCallback(input.onStdout, chunk);
138
- });
139
- child.stderr.on("data", (chunk) => {
140
- stderr += chunk;
141
- appendChunkBestEffort(input.stderrPath, chunk);
142
- invokeChunkCallback(input.onStderr, chunk);
143
- });
144
- child.on("error", (error) => {
145
- clearTimers();
146
- reject(error);
147
- });
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); });
148
379
  child.on("close", (exitCode) => {
149
380
  clearTimers();
150
- resolve({ stdout, stderr, exitCode, timedOut });
381
+ resolve({ stdout: formatBoundedOutput(stdout), stdoutBytes: stdout.bytes, stdoutTruncated: stdout.truncated, stderr: formatBoundedOutput(stderr), stderrBytes: stderr.bytes, stderrTruncated: stderr.truncated, exitCode, timedOut });
151
382
  });
152
383
  });
153
384
  }
154
- function appendChunkBestEffort(filePath, chunk) {
155
- try {
156
- appendFileSync(filePath, chunk, "utf-8");
157
- }
158
- catch {
159
- // best-effort: degrade to in-memory buffer only
160
- }
385
+ function createBoundedOutput() { return { bytes: 0, text: "", truncated: false }; }
386
+ function appendBoundedOutput(current, chunk) {
387
+ const bytes = current.bytes + Buffer.byteLength(chunk);
388
+ const combined = current.text + chunk;
389
+ if (Buffer.byteLength(combined) <= MAX_BUFFERED_OUTPUT_BYTES)
390
+ return { bytes, text: combined, truncated: current.truncated };
391
+ const tail = Buffer.from(combined).subarray(-MAX_BUFFERED_OUTPUT_BYTES).toString("utf-8").replace(/^\uFFFD/, "");
392
+ return { bytes, text: tail, truncated: true };
161
393
  }
162
- function invokeSpawnCallback(callback, info) {
163
- if (!callback) {
164
- return;
165
- }
166
- try {
167
- callback(info);
168
- }
169
- catch (error) {
170
- process.stderr.write(`LoopAgentClient onSpawn callback error: ${String(error)}\n`);
171
- }
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");
172
397
  }
173
- function invokeChunkCallback(callback, chunk) {
174
- if (!callback) {
175
- return;
176
- }
177
- try {
178
- callback(chunk);
179
- }
180
- catch (error) {
181
- process.stderr.write(`LoopAgentClient output callback error: ${String(error)}\n`);
182
- }
398
+ catch {
399
+ return fallback;
400
+ } }
401
+ function appendChunkBestEffort(filePath, chunk) { try {
402
+ appendFileSync(filePath, chunk, "utf-8");
183
403
  }
184
- function invokeHeartbeatCallback(callback, startedAtMs) {
185
- if (!callback) {
186
- return;
187
- }
188
- try {
189
- callback({ at: new Date().toISOString(), elapsedMs: Date.now() - startedAtMs });
190
- }
191
- catch (error) {
192
- process.stderr.write(`LoopAgentClient onHeartbeat callback error: ${String(error)}\n`);
193
- }
404
+ catch { /* best-effort */ } }
405
+ function invokeSpawnCallback(cb, info) { if (!cb)
406
+ return; try {
407
+ cb(info);
194
408
  }
195
- function sanitizeName(name) {
196
- return name.replace(/[^a-zA-Z0-9._-]+/g, "-");
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);
415
+ }
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 });
422
+ }
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
+ });
197
441
  }