@tea-agent/loop-agent 0.9.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 (183) hide show
  1. package/AGENTS.md +8 -0
  2. package/CHANGELOG.md +92 -11
  3. package/README.md +69 -5
  4. package/dist/application/dag/args.js +4 -14
  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 +1 -1
  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/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
  34. package/dist/sidecars/cursor-prompt/index.js +3 -0
  35. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  36. package/dist/task/config-types.js +29 -12
  37. package/dist/task/delegate.js +9 -21
  38. package/dist/task/runtime.js +1 -2
  39. package/dist/worker/cli.js +243 -0
  40. package/dist/worker/closeout/apply.js +73 -0
  41. package/dist/worker/closeout/preview.js +30 -0
  42. package/dist/worker/delivery/final-verification.js +194 -0
  43. package/dist/worker/delivery/git-transaction.js +354 -0
  44. package/dist/worker/delivery/package.js +502 -0
  45. package/dist/worker/feature/decision-loader.js +68 -0
  46. package/dist/worker/feature/discover.js +14 -0
  47. package/dist/worker/feature/next-action.js +74 -0
  48. package/dist/worker/feature/reducer.js +133 -0
  49. package/dist/worker/feature/review.js +502 -0
  50. package/dist/worker/feature/run.js +365 -0
  51. package/dist/worker/feature/types.js +1 -0
  52. package/dist/worker/follow-up/approve.js +270 -0
  53. package/dist/worker/follow-up/factory.js +234 -0
  54. package/dist/worker/follow-up/paths.js +25 -0
  55. package/dist/worker/follow-up/policy.js +26 -0
  56. package/dist/worker/follow-up/schema.js +93 -0
  57. package/dist/worker/follow-up/store.js +96 -0
  58. package/dist/worker/loop-agent/loop-agent-client.js +329 -126
  59. package/dist/worker/metrics/projector.js +139 -0
  60. package/dist/worker/observability/read-model.js +282 -15
  61. package/dist/worker/observe/paths.js +17 -5
  62. package/dist/worker/observe/static/app.js +443 -61
  63. package/dist/worker/observe/static/index.html +3 -1
  64. package/dist/worker/observe/static/styles.css +85 -18
  65. package/dist/worker/pool/run-store.js +14 -2
  66. package/dist/worker/pool/validation.js +59 -0
  67. package/dist/worker/preflight.js +49 -1
  68. package/dist/worker/report/morning-report.js +41 -6
  69. package/dist/worker/run-task/run-task.js +23 -13
  70. package/dist/worker/runner/run-ready.js +89 -11
  71. package/dist/worker/task-spec/schema.js +0 -1
  72. package/dist/workflows/dag/convergence/controller.js +1 -1
  73. package/dist/workflows/dag/executor-registry.js +0 -2
  74. package/dist/workflows/dag/governance-profile.js +10 -0
  75. package/dist/workflows/dag/init-hybrid.js +601 -26
  76. package/dist/workflows/dag/lifecycle.js +146 -0
  77. package/dist/workflows/dag/node-execution.js +64 -7
  78. package/dist/workflows/dag/prompt.js +16 -0
  79. package/dist/workflows/dag/report.js +2 -0
  80. package/dist/workflows/dag/runner.js +176 -119
  81. package/dist/workflows/dag/scheduler.js +7 -2
  82. package/dist/workflows/dag/skill-snapshot.js +527 -0
  83. package/dist/workflows/dag/types.js +45 -9
  84. package/dist/workflows/dag/validate.js +5 -8
  85. package/dist/workflows/loop/actions/dag-action.js +0 -2
  86. package/dist/workflows/loop/actions/shared.js +1 -1
  87. package/dist/workflows/loop/actions.js +14 -31
  88. package/dist/workflows/loop/benchmark.js +1 -1
  89. package/dist/workflows/loop/index.js +1 -1
  90. package/dist/workflows/loop/policy/auto-policy.js +22 -14
  91. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  92. package/docs/README.md +35 -16
  93. package/docs/agent-dag-recovery-playbook.md +1 -1
  94. package/docs/architecture/README.md +26 -0
  95. package/docs/architecture/dag-execution.md +134 -0
  96. package/docs/architecture/evolution.md +52 -0
  97. package/docs/architecture/facts-and-state.md +58 -0
  98. package/docs/architecture/runtime-boundaries.md +41 -15
  99. package/docs/architecture/system-overview.md +93 -0
  100. package/docs/architecture/worker-and-feature.md +81 -0
  101. package/docs/cursor-prompt-sidecar.md +36 -0
  102. package/docs/decisions/README.md +13 -1
  103. package/docs/design/README.md +39 -13
  104. package/docs/development-principles.md +1 -1
  105. package/docs/exec-plans/active/README.md +2 -2
  106. package/docs/exec-plans/completed/README.md +14 -1
  107. package/docs/feature-workflow.md +44 -4
  108. package/docs/init-surface.manifest.json +63 -1
  109. package/docs/loop-agent-harness.md +65 -3
  110. package/docs/progress/README.md +27 -0
  111. package/docs/reports/README.md +74 -8
  112. package/docs/skills/README.md +2 -1
  113. package/docs/skills/vetted-skill-registry.md +2 -1
  114. package/docs/templates/agent-dag-report.schema.json +4 -2
  115. package/docs/templates/agent-dag.base.json +0 -5
  116. package/docs/templates/agent-dag.final-verification.json +0 -5
  117. package/docs/templates/agent-dag.schema.json +1 -2
  118. package/docs/templates/agent-dag.supervised-implementation.json +1 -6
  119. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +131 -0
  120. package/docs/templates/backend-test-dag.json +213 -0
  121. package/docs/templates/backend-test-dag.retrospect.prompt.md +128 -0
  122. package/docs/templates/backend-test-dag.review-cases.prompt.md +85 -0
  123. package/docs/templates/frontend-design-contract.md +33 -0
  124. package/docs/templates/frontend-task-constraints.md +25 -0
  125. package/docs/templates/frontend-task-requirement.md +61 -0
  126. package/docs/templates/harness.schema.json +8 -5
  127. package/docs/templates/hybrid-dag.json +1 -6
  128. package/docs/templates/init-evolution-review.md +4 -2
  129. package/docs/templates/interactive-ui-round2-experiment.md +1 -1
  130. package/docs/templates/product-line/task.yaml +0 -1
  131. package/docs/templates/worker-dogfood-evidence.md +28 -0
  132. package/docs/templates/worker-dogfood-setup.md +20 -0
  133. package/docs/verification-matrix.md +17 -0
  134. package/examples/decision-gate-agent-dag.json +87 -33
  135. package/examples/example-dag.json +0 -5
  136. package/examples/hybrid-loop-agent-dag.json +0 -5
  137. package/harness.json +6 -11
  138. package/package.json +22 -44
  139. package/scripts/check-product-line-docs.sh +10 -3
  140. package/skills/agent-worker/SKILL.md +37 -0
  141. package/skills/agent-worker/references/agent-worker-operator.md +43 -0
  142. package/skills/frontend-design-review/SKILL.md +59 -0
  143. package/skills/frontend-design-review/references/review-checklist.md +37 -0
  144. package/skills/frontend-implementation/SKILL.md +48 -0
  145. package/skills/frontend-implementation/references/code-standards.md +34 -0
  146. package/skills/frontend-implementation/references/design-spec.md +46 -0
  147. package/skills/frontend-implementation/references/node-contracts.md +32 -0
  148. package/skills/frontend-review/SKILL.md +53 -0
  149. package/skills/frontend-review/references/review-findings.md +42 -0
  150. package/skills/frontend-verification/SKILL.md +40 -0
  151. package/skills/frontend-verification/references/verification-checklist.md +56 -0
  152. package/skills/grill-me/SKILL.md +10 -0
  153. package/skills/grill-with-docs/SKILL.md +88 -0
  154. package/skills/grill-with-docs/adr-format.md +47 -0
  155. package/skills/grill-with-docs/context-format.md +60 -0
  156. package/skills/init-capability-evolution/SKILL.md +1 -0
  157. package/skills/loop-agent/SKILL.md +11 -9
  158. package/skills/loop-agent/references/command-reference.md +28 -15
  159. package/skills/loop-agent/references/docs-converge.md +126 -0
  160. package/skills/loop-agent/references/harness-policy.md +7 -7
  161. package/skills/loop-agent/references/hybrid-dag.md +13 -15
  162. package/skills/loop-agent/references/long-running-loop.md +4 -6
  163. package/skills/loop-agent/references/multi-worktree.md +6 -6
  164. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  165. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
  166. package/skills/using-git-worktrees/SKILL.md +215 -0
  167. package/dist/commands/cursor-worker.js +0 -43
  168. package/dist/cursor-worker-entry.js +0 -8
  169. package/dist/executors/cursor-artifacts.js +0 -33
  170. package/dist/executors/cursor-execution-log.js +0 -81
  171. package/dist/executors/cursor-executor-artifacts.js +0 -134
  172. package/dist/executors/cursor-run.js +0 -115
  173. package/dist/executors/cursor-tool.js +0 -94
  174. package/dist/executors/cursor-worker-client.js +0 -223
  175. package/dist/executors/cursor-worker-protocol.js +0 -18
  176. package/dist/executors/cursor-worker-server.js +0 -54
  177. package/dist/executors/cursor-worker.js +0 -3
  178. package/dist/executors/cursor.js +0 -6
  179. package/dist/executors/dag-cursor-executor.js +0 -87
  180. package/dist/workflows/loop/actions/cursor-fix.js +0 -191
  181. package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
  182. package/docs/cursor-executor-usage.md +0 -25
  183. 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
- stdoutBytes,
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
- // best-effort: degrade to in-memory buffer only
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
- invokeHeartbeatCallback(input.onHeartbeat, startedAtMs);
129
- }, input.heartbeatIntervalMs);
130
- }
131
- const clearTimers = () => {
132
- clearTimeout(timeout);
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
- stdout = appendBoundedOutput(stdout, chunk);
142
- appendChunkBestEffort(input.stdoutPath, chunk);
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
- return output.truncated ? `${output.text}\n...[truncated]` : output.text;
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
- async function readFullOutputArtifact(filePath, fallback) {
188
- try {
189
- return await readFile(filePath, "utf-8");
190
- }
191
- catch {
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
- function appendChunkBestEffort(filePath, chunk) {
196
- try {
197
- appendFileSync(filePath, chunk, "utf-8");
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
- function invokeSpawnCallback(callback, info) {
204
- if (!callback) {
205
- return;
206
- }
207
- try {
208
- callback(info);
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
- function invokeChunkCallback(callback, chunk) {
215
- if (!callback) {
216
- return;
217
- }
218
- try {
219
- callback(chunk);
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
- function sanitizeName(name) {
237
- return name.replace(/[^a-zA-Z0-9._-]+/g, "-");
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
  }