@tea-agent/loop-agent 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/AGENTS.md +10 -2
  2. package/CHANGELOG.md +91 -24
  3. package/README.md +84 -12
  4. package/dist/application/dag/args.js +1 -12
  5. package/dist/application/dag/generate-task-dag.js +38 -2
  6. package/dist/application/dag/run-dag.js +11 -27
  7. package/dist/application/dag/validate-dag.js +13 -2
  8. package/dist/application/loop/run-action.js +0 -4
  9. package/dist/cli/command-definitions.js +44 -16
  10. package/dist/cli/program.js +40 -23
  11. package/dist/cli/update/notifier.js +117 -0
  12. package/dist/cli/update/npm-client.js +151 -0
  13. package/dist/cli/update/policy.js +58 -0
  14. package/dist/cli/update/state.js +68 -0
  15. package/dist/cli.js +33 -0
  16. package/dist/commands/cursor-prompt.js +42 -82
  17. package/dist/commands/dag-approve.js +36 -0
  18. package/dist/commands/delegate.js +75 -77
  19. package/dist/commands/doctor.js +0 -18
  20. package/dist/commands/init.js +547 -95
  21. package/dist/commands/instructions.js +7 -10
  22. package/dist/commands/loop.js +4 -20
  23. package/dist/commands/plan.js +50 -0
  24. package/dist/executors/config-core.js +0 -51
  25. package/dist/executors/dag-pi-executor.js +1 -1
  26. package/dist/executors/dag.js +0 -1
  27. package/dist/executors/index.js +0 -2
  28. package/dist/executors/model-routing.js +9 -9
  29. package/dist/executors/shell-executor.js +1 -1
  30. package/dist/governance/checks.js +6 -3
  31. package/dist/governance/exec-plans.js +545 -0
  32. package/dist/governance/manifest-types.js +24 -2
  33. package/dist/infrastructure/harness/loop-action-store.js +0 -3
  34. package/dist/records/harvest.js +2 -23
  35. package/dist/records/one-shot-runs.js +1 -1
  36. package/dist/shared/artifacts-core.js +24 -5
  37. package/dist/shared/output-truncation.js +37 -0
  38. package/dist/shared/package-metadata.js +488 -0
  39. package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
  40. package/dist/sidecars/cursor-prompt/index.js +3 -0
  41. package/dist/sidecars/cursor-prompt/stream.js +121 -0
  42. package/dist/task/config-types.js +29 -12
  43. package/dist/task/delegate.js +9 -21
  44. package/dist/task/runtime.js +1 -2
  45. package/dist/worker/cli.js +32 -3
  46. package/dist/worker/delivery/final-verification.js +47 -11
  47. package/dist/worker/delivery/package.js +63 -10
  48. package/dist/worker/feature/run.js +60 -8
  49. package/dist/worker/loop-agent/loop-agent-client.js +329 -126
  50. package/dist/worker/observability/event-history.js +216 -0
  51. package/dist/worker/observability/read-model.js +338 -83
  52. package/dist/worker/observe/paths.js +17 -0
  53. package/dist/worker/observe/routes.js +165 -21
  54. package/dist/worker/observe/server.js +59 -1
  55. package/dist/worker/observe/static/api.js +27 -0
  56. package/dist/worker/observe/static/app.js +120 -2317
  57. package/dist/worker/observe/static/constants.js +148 -0
  58. package/dist/worker/observe/static/copy.js +67 -0
  59. package/dist/worker/observe/static/dag-helpers.js +172 -0
  60. package/dist/worker/observe/static/dag-model.js +72 -0
  61. package/dist/worker/observe/static/dom.js +61 -0
  62. package/dist/worker/observe/static/format-pool.js +67 -0
  63. package/dist/worker/observe/static/format.js +292 -0
  64. package/dist/worker/observe/static/index.html +300 -82
  65. package/dist/worker/observe/static/kpi.js +94 -0
  66. package/dist/worker/observe/static/relations.js +128 -0
  67. package/dist/worker/observe/static/router.js +85 -0
  68. package/dist/worker/observe/static/run-processing.js +148 -0
  69. package/dist/worker/observe/static/shell-chrome.js +68 -0
  70. package/dist/worker/observe/static/state.js +253 -0
  71. package/dist/worker/observe/static/styles.css +1720 -495
  72. package/dist/worker/observe/static/views/batch.js +226 -0
  73. package/dist/worker/observe/static/views/dag-graph.js +172 -0
  74. package/dist/worker/observe/static/views/dag-inspector.js +477 -0
  75. package/dist/worker/observe/static/views/dag.js +362 -0
  76. package/dist/worker/observe/static/views/dashboard.js +442 -0
  77. package/dist/worker/observe/static/views/failures.js +143 -0
  78. package/dist/worker/observe/static/views/feature.js +453 -0
  79. package/dist/worker/observe/static/views/pool.js +347 -0
  80. package/dist/worker/observe/static/views/run.js +453 -0
  81. package/dist/worker/observe/static/views/session-timeline.js +205 -0
  82. package/dist/worker/observe/static/views/shell.js +7 -0
  83. package/dist/worker/observe/static/views/task.js +260 -0
  84. package/dist/worker/observe/static/views/timeline.js +163 -0
  85. package/dist/worker/preflight.js +49 -1
  86. package/dist/worker/run-task/run-task.js +22 -12
  87. package/dist/worker/runner/run-ready.js +76 -12
  88. package/dist/worker/task-spec/schema.js +0 -1
  89. package/dist/workflows/dag/controller-identity.js +104 -0
  90. package/dist/workflows/dag/convergence/controller.js +1 -1
  91. package/dist/workflows/dag/executor-registry.js +0 -2
  92. package/dist/workflows/dag/init-hybrid.js +797 -27
  93. package/dist/workflows/dag/node-execution.js +183 -35
  94. package/dist/workflows/dag/repair-artifact.js +91 -0
  95. package/dist/workflows/dag/report.js +50 -0
  96. package/dist/workflows/dag/retry-policy.js +138 -0
  97. package/dist/workflows/dag/runner.js +77 -17
  98. package/dist/workflows/dag/runtime-contract.js +87 -0
  99. package/dist/workflows/dag/scheduler.js +7 -2
  100. package/dist/workflows/dag/sdd-embedded.js +128 -0
  101. package/dist/workflows/dag/skill-instructions.js +5 -4
  102. package/dist/workflows/dag/skill-snapshot.js +529 -0
  103. package/dist/workflows/dag/types.js +86 -10
  104. package/dist/workflows/dag/validate.js +73 -12
  105. package/dist/workflows/loop/actions/dag-action.js +0 -2
  106. package/dist/workflows/loop/actions/shared.js +1 -1
  107. package/dist/workflows/loop/actions.js +14 -31
  108. package/dist/workflows/loop/benchmark.js +1 -1
  109. package/dist/workflows/loop/index.js +1 -1
  110. package/dist/workflows/loop/policy/auto-policy.js +22 -14
  111. package/dist/workflows/loop/policy/path-patterns.js +13 -0
  112. package/docs/README.md +36 -33
  113. package/docs/agent-dag-recovery-playbook.md +1 -1
  114. package/docs/agent-dag-runner.md +28 -3
  115. package/docs/architecture/README.md +26 -0
  116. package/docs/architecture/dag-execution.md +140 -0
  117. package/docs/architecture/evolution.md +53 -0
  118. package/docs/architecture/facts-and-state.md +58 -0
  119. package/docs/architecture/runtime-boundaries.md +45 -17
  120. package/docs/architecture/system-overview.md +93 -0
  121. package/docs/architecture/worker-and-feature.md +81 -0
  122. package/docs/cursor-prompt-sidecar.md +36 -0
  123. package/docs/decisions/README.md +13 -1
  124. package/docs/design/README.md +43 -21
  125. package/docs/development-principles.md +2 -2
  126. package/docs/exec-plans/active/README.md +1 -3
  127. package/docs/exec-plans/completed/README.md +23 -0
  128. package/docs/feature-workflow.md +78 -4
  129. package/docs/harness-methodology-debugging.md +1 -1
  130. package/docs/harness-methodology-tdd.md +3 -3
  131. package/docs/init-surface.manifest.json +60 -25
  132. package/docs/loop-agent-harness.md +28 -4
  133. package/docs/progress/README.md +50 -1
  134. package/docs/reports/README.md +90 -18
  135. package/docs/skills/README.md +2 -1
  136. package/docs/skills/vetted-skill-registry.md +2 -1
  137. package/docs/templates/agent-dag-report.schema.json +23 -6
  138. package/docs/templates/agent-dag.base.json +0 -5
  139. package/docs/templates/agent-dag.final-verification.json +0 -5
  140. package/docs/templates/agent-dag.schema.json +70 -3
  141. package/docs/templates/agent-dag.supervised-implementation.json +9 -8
  142. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
  143. package/docs/templates/backend-test-dag.json +276 -0
  144. package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
  145. package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
  146. package/docs/templates/frontend-design-contract.md +33 -0
  147. package/docs/templates/frontend-task-constraints.md +25 -0
  148. package/docs/templates/frontend-task-requirement.md +61 -0
  149. package/docs/templates/harness.schema.json +10 -12
  150. package/docs/templates/hybrid-dag.json +1 -6
  151. package/docs/templates/interactive-ui-round2-experiment.md +1 -1
  152. package/docs/templates/product-line/task.yaml +0 -1
  153. package/docs/templates/project-start-checklist.md +2 -2
  154. package/docs/templates/worker-dogfood-evidence.md +28 -0
  155. package/docs/templates/worker-dogfood-setup.md +20 -0
  156. package/docs/verification-matrix.md +10 -0
  157. package/examples/decision-gate-agent-dag.json +87 -33
  158. package/examples/example-dag.json +0 -5
  159. package/examples/hybrid-loop-agent-dag.json +0 -5
  160. package/harness.json +7 -15
  161. package/package.json +22 -46
  162. package/scripts/check-product-line-docs.sh +10 -7
  163. package/skills/agent-worker/SKILL.md +37 -0
  164. package/skills/agent-worker/references/agent-worker-operator.md +43 -0
  165. package/skills/frontend-design-review/SKILL.md +59 -0
  166. package/skills/frontend-design-review/references/review-checklist.md +37 -0
  167. package/skills/frontend-implementation/SKILL.md +51 -0
  168. package/skills/frontend-implementation/references/code-standards.md +34 -0
  169. package/skills/frontend-implementation/references/design-spec.md +46 -0
  170. package/skills/frontend-implementation/references/node-contracts.md +32 -0
  171. package/skills/frontend-review/SKILL.md +53 -0
  172. package/skills/frontend-review/references/review-findings.md +42 -0
  173. package/skills/frontend-verification/SKILL.md +40 -0
  174. package/skills/frontend-verification/references/verification-checklist.md +56 -0
  175. package/skills/grill-me/SKILL.md +10 -0
  176. package/skills/grill-with-docs/SKILL.md +88 -0
  177. package/skills/grill-with-docs/adr-format.md +47 -0
  178. package/skills/grill-with-docs/context-format.md +60 -0
  179. package/skills/loop-agent/SKILL.md +11 -9
  180. package/skills/loop-agent/references/command-reference.md +14 -15
  181. package/skills/loop-agent/references/docs-converge.md +126 -0
  182. package/skills/loop-agent/references/harness-policy.md +7 -7
  183. package/skills/loop-agent/references/hybrid-dag.md +36 -20
  184. package/skills/loop-agent/references/long-running-loop.md +4 -6
  185. package/skills/loop-agent/references/multi-worktree.md +6 -6
  186. package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
  187. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
  188. package/skills/loop-agent/references/task-workflow.md +1 -1
  189. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
  190. package/skills/using-git-worktrees/SKILL.md +215 -0
  191. package/dist/commands/cursor-worker.js +0 -43
  192. package/dist/cursor-worker-entry.js +0 -8
  193. package/dist/executors/cursor-artifacts.js +0 -33
  194. package/dist/executors/cursor-execution-log.js +0 -81
  195. package/dist/executors/cursor-executor-artifacts.js +0 -134
  196. package/dist/executors/cursor-run.js +0 -115
  197. package/dist/executors/cursor-tool.js +0 -94
  198. package/dist/executors/cursor-worker-client.js +0 -223
  199. package/dist/executors/cursor-worker-protocol.js +0 -18
  200. package/dist/executors/cursor-worker-server.js +0 -54
  201. package/dist/executors/cursor-worker.js +0 -3
  202. package/dist/executors/cursor.js +0 -6
  203. package/dist/executors/dag-cursor-executor.js +0 -87
  204. package/dist/workflows/loop/actions/cursor-fix.js +0 -191
  205. package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
  206. package/docs/cursor-executor-usage.md +0 -25
  207. package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
@@ -5,10 +5,14 @@ import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/r
5
5
  import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
6
6
  import { moveToCompletedRunDir, moveToPausedRunDir, prepareActiveRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
7
7
  import { createDagNodeExecutor } from "./executor-registry.js";
8
- import { executeDagCursorNode } from "../../executors/dag-cursor-executor.js";
8
+ import { executeDagPiNode } from "../../executors/dag-pi-executor.js";
9
9
  import { assertValidDagSpec } from "./validate.js";
10
+ import { assertRuntimeContractCompatible, DAG_CONTROLLER_CAPABILITIES, } from "./runtime-contract.js";
11
+ import { captureControllerIdentity, verifyControllerIdentityForResume, } from "./controller-identity.js";
12
+ import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
10
13
  import { normalizeDagPromptSources } from "./prompt-source.js";
11
14
  import { relocateConvergenceArtifactPaths, relocateRunArtifactPaths, } from "./upstream-artifacts.js";
15
+ import { createSkillSnapshot, prepareSkillSnapshotForContinuation, writeSkillSnapshot, } from "./skill-snapshot.js";
12
16
  import { buildNodePrompt, buildNodePromptWithResolvedSkillInstructions, executeDagNode, } from "./node-execution.js";
13
17
  import { runConvergencePassController, shouldEnableDagConvergence, } from "./convergence/controller.js";
14
18
  import { executeDagRanksOnce, isConditionSkippedReason, } from "./scheduler.js";
@@ -144,6 +148,15 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
144
148
  }
145
149
  export async function runDag(spec, opts) {
146
150
  assertValidDagSpec(spec);
151
+ // Capability preflight must fail closed before any run directory or node work.
152
+ const runningIdentity = resolveRunningControllerIdentity();
153
+ if (!runningIdentity) {
154
+ throw new Error("running controller identity could not be resolved; refuse to create an unpinned DAG run");
155
+ }
156
+ assertRuntimeContractCompatible(spec, {
157
+ ...DAG_CONTROLLER_CAPABILITIES,
158
+ controllerVersion: runningIdentity.packageVersion,
159
+ });
147
160
  const { ranks } = topoSortToRanks(spec);
148
161
  const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
149
162
  let runId = opts.runId;
@@ -162,6 +175,25 @@ export async function runDag(spec, opts) {
162
175
  const runDir = activeRunDir;
163
176
  await prepareActiveRunDir(runDir);
164
177
  await writeRunSpec(runDir, spec);
178
+ // Freeze the running controller identity as a run-owned fact before the
179
+ // first canonical state write, reusing the identity resolved for preflight.
180
+ state.controllerIdentityRef = await captureControllerIdentity({
181
+ runDir,
182
+ identity: runningIdentity,
183
+ });
184
+ // Resolve and snapshot all skill profiles before any node executes.
185
+ try {
186
+ const snapshot = await createSkillSnapshot({
187
+ runId: state.runId,
188
+ spec,
189
+ cwd: opts.cwd,
190
+ mode: "run-start",
191
+ });
192
+ state.skillSnapshotRef = await writeSkillSnapshot(runDir, snapshot);
193
+ }
194
+ catch (error) {
195
+ throw new Error(`failed to create run-start skill snapshot: ${error instanceof Error ? error.message : String(error)}`);
196
+ }
165
197
  await writeRunState(runDir, state);
166
198
  await notifyRunObserver(opts.observer, "onRunStart", state);
167
199
  if (opts.dryRun || opts.initOnly) {
@@ -190,7 +222,6 @@ export async function runDag(spec, opts) {
190
222
  });
191
223
  }
192
224
  finally {
193
- await stopCursorWorkerIfRunning();
194
225
  }
195
226
  }
196
227
  export async function resumeDagRun(opts) {
@@ -211,6 +242,20 @@ export async function resumeDagRun(opts) {
211
242
  if (isTerminalDagRunStatus(state.status)) {
212
243
  throw new Error(`dag run ${opts.runId} is already terminal (status=${state.status})`);
213
244
  }
245
+ // Runtime contract + controller identity must be re-verified before executing
246
+ // any remaining node on resume; drift fails closed.
247
+ const runningIdentity = resolveRunningControllerIdentity();
248
+ if (!runningIdentity) {
249
+ throw new Error("running controller identity could not be resolved; refuse to resume an unpinned DAG run");
250
+ }
251
+ assertRuntimeContractCompatible(spec, {
252
+ ...DAG_CONTROLLER_CAPABILITIES,
253
+ controllerVersion: runningIdentity.packageVersion,
254
+ });
255
+ await verifyControllerIdentityForResume({
256
+ runDir,
257
+ state,
258
+ });
214
259
  const decisionNodeId = state.humanDecisionNodeId;
215
260
  if (!decisionNodeId) {
216
261
  throw new Error(`dag run ${opts.runId} has no human decision checkpoint; was it approved?`);
@@ -221,6 +266,25 @@ export async function resumeDagRun(opts) {
221
266
  catch {
222
267
  throw new Error(`dag run ${opts.runId} is missing human-approval.json for node ${decisionNodeId}`);
223
268
  }
269
+ // Validate or backfill before mutating runner/status for this resume attempt.
270
+ try {
271
+ const prepared = await prepareSkillSnapshotForContinuation({
272
+ runDir,
273
+ runId: state.runId,
274
+ spec,
275
+ state,
276
+ cwd: state.cwd,
277
+ ref: state.skillSnapshotRef,
278
+ });
279
+ if (prepared.backfilled) {
280
+ console.warn(`[dag resume] warning: run ${state.runId} has no skill snapshot ref; creating a legacy-resume-backfill that freezes remaining nodes only`);
281
+ state.skillSnapshotRef = prepared.ref;
282
+ await writeRunState(runDir, state);
283
+ }
284
+ }
285
+ catch (error) {
286
+ throw new Error(`skill snapshot validation failed on resume: ${error instanceof Error ? error.message : String(error)}`);
287
+ }
224
288
  const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
225
289
  state.status = "running";
226
290
  const resumedAt = new Date().toISOString();
@@ -249,7 +313,6 @@ export async function resumeDagRun(opts) {
249
313
  });
250
314
  }
251
315
  finally {
252
- await stopCursorWorkerIfRunning();
253
316
  }
254
317
  }
255
318
  async function executeDagCheckpoint(input) {
@@ -314,10 +377,10 @@ async function executeDagCheckpoint(input) {
314
377
  tasksById,
315
378
  maxConcurrent,
316
379
  persistState,
317
- createExecuteNodeForRank: (rankCursorNodeIds) => buildRankAwareExecuteNode({
380
+ createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
318
381
  baseExecuteNode,
319
382
  customExecuteNode: input.executeNode,
320
- rankCursorNodeIds,
383
+ rankWriterNodeIds,
321
384
  tasksById,
322
385
  meta: { runDir, runId: state.runId, spec },
323
386
  }),
@@ -413,16 +476,10 @@ function finalizeTerminalRunStatus(state, taskCount) {
413
476
  state.status = "failed";
414
477
  }
415
478
  }
416
- async function stopCursorWorkerIfRunning() {
417
- const { isCursorWorkerRunning, stopCursorWorker } = await import("../../executors/cursor-worker-client.js");
418
- if (isCursorWorkerRunning()) {
419
- await stopCursorWorker();
420
- }
421
- }
422
- function concurrentSiblingWriteSetsForNode(rankCursorNodeIds, nodeId, tasksById) {
423
- if (rankCursorNodeIds.length <= 1)
479
+ function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
480
+ if (rankWriterNodeIds.length <= 1)
424
481
  return [];
425
- return rankCursorNodeIds
482
+ return rankWriterNodeIds
426
483
  .filter((id) => id !== nodeId)
427
484
  .map((id) => tasksById.get(id)?.writeSet ?? [])
428
485
  .filter((writeSet) => writeSet.length > 0);
@@ -432,13 +489,16 @@ function buildRankAwareExecuteNode(input) {
432
489
  return input.baseExecuteNode;
433
490
  }
434
491
  return async (nodeInput) => {
435
- if (nodeInput.task.executor !== "cursor" || !nodeInput.model) {
492
+ const isPiWriter = nodeInput.task.executor === "pi" &&
493
+ nodeInput.task.toolProfile === "write" &&
494
+ Boolean(nodeInput.model);
495
+ if (!isPiWriter) {
436
496
  return input.baseExecuteNode(nodeInput);
437
497
  }
438
498
  const siblingWriteSets = nodeInput.task.writePolicy === "exclusive"
439
- ? concurrentSiblingWriteSetsForNode(input.rankCursorNodeIds, nodeInput.task.id, input.tasksById)
499
+ ? concurrentSiblingWriteSetsForNode(input.rankWriterNodeIds, nodeInput.task.id, input.tasksById)
440
500
  : [];
441
- return executeDagCursorNode({ ...nodeInput, model: nodeInput.model }, {
501
+ return executeDagPiNode({ ...nodeInput, model: nodeInput.model }, {
442
502
  ...input.meta,
443
503
  concurrentSiblingWriteSets: siblingWriteSets.length > 0 ? siblingWriteSets : undefined,
444
504
  writeGuardAttribution: siblingWriteSets.length > 0 ? "best-effort" : "per-node",
@@ -0,0 +1,87 @@
1
+ import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1, DAG_RUNTIME_CONTRACT_SCHEMA_VERSION, } from "./types.js";
2
+ export const DAG_CONTROLLER_CAPABILITIES = {
3
+ schemaVersion: DAG_RUNTIME_CONTRACT_SCHEMA_VERSION,
4
+ agentRuntime: DAG_AGENT_RUNTIME_PI_ONLY,
5
+ repairWriterProtocol: DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1,
6
+ };
7
+ function parseSemver(value) {
8
+ const match = value.match(/^(\d+)\.(\d+)\.(\d+)/);
9
+ if (!match)
10
+ return undefined;
11
+ return {
12
+ major: Number(match[1]),
13
+ minor: Number(match[2]),
14
+ patch: Number(match[3]),
15
+ };
16
+ }
17
+ /** Returns true when `actual` is greater than or equal to `required`. */
18
+ export function semverGte(actual, required) {
19
+ const a = parseSemver(actual);
20
+ const r = parseSemver(required);
21
+ if (!a || !r)
22
+ return false;
23
+ if (a.major !== r.major)
24
+ return a.major > r.major;
25
+ if (a.minor !== r.minor)
26
+ return a.minor > r.minor;
27
+ return a.patch >= r.patch;
28
+ }
29
+ /**
30
+ * Decide whether a controller can run a DagSpec's declared runtimeContract.
31
+ *
32
+ * Capability fields are authoritative: an unsupported agentRuntime or
33
+ * repairWriterProtocol fails closed regardless of version. minimumControllerVersion
34
+ * is only consulted for diagnosis when the controller version is known and the
35
+ * capabilities otherwise match.
36
+ *
37
+ * Legacy DagSpecs without a runtimeContract are always compatible (they predate
38
+ * the capability handshake and rely on runtime-side derivation).
39
+ */
40
+ export function checkRuntimeContractCompatible(contract, capabilities = DAG_CONTROLLER_CAPABILITIES) {
41
+ if (!contract)
42
+ return { ok: true };
43
+ if (contract.schemaVersion !== capabilities.schemaVersion) {
44
+ return {
45
+ ok: false,
46
+ reason: `runtime contract schemaVersion ${contract.schemaVersion} is not supported by this controller (supports ${capabilities.schemaVersion}); regenerate the DAG or upgrade the controller`,
47
+ };
48
+ }
49
+ if (contract.agentRuntime !== capabilities.agentRuntime) {
50
+ return {
51
+ ok: false,
52
+ reason: `runtime contract requires agentRuntime "${contract.agentRuntime}" but this controller advertises "${capabilities.agentRuntime}"; upgrade the controller or regenerate the DAG`,
53
+ };
54
+ }
55
+ if (contract.repairWriterProtocol !== capabilities.repairWriterProtocol) {
56
+ return {
57
+ ok: false,
58
+ reason: `runtime contract requires repairWriterProtocol "${contract.repairWriterProtocol}" but this controller advertises "${capabilities.repairWriterProtocol}"; upgrade the controller or regenerate the DAG`,
59
+ };
60
+ }
61
+ if (contract.minimumControllerVersion &&
62
+ !capabilities.controllerVersion) {
63
+ return {
64
+ ok: false,
65
+ reason: `runtime contract requires controller version >= ${contract.minimumControllerVersion} but the running controller version could not be resolved; refuse to continue unpinned`,
66
+ };
67
+ }
68
+ if (contract.minimumControllerVersion &&
69
+ capabilities.controllerVersion &&
70
+ !semverGte(capabilities.controllerVersion, contract.minimumControllerVersion)) {
71
+ return {
72
+ ok: false,
73
+ reason: `runtime contract requires controller version >= ${contract.minimumControllerVersion} but this controller reports ${capabilities.controllerVersion}; upgrade the controller`,
74
+ };
75
+ }
76
+ return { ok: true };
77
+ }
78
+ /**
79
+ * Fail closed before any node execution when the DagSpec's runtimeContract is
80
+ * incompatible with this controller.
81
+ */
82
+ export function assertRuntimeContractCompatible(spec, capabilities = DAG_CONTROLLER_CAPABILITIES) {
83
+ const result = checkRuntimeContractCompatible(spec.runtimeContract, capabilities);
84
+ if (!result.ok) {
85
+ throw new Error(`incompatible DAG runtime contract: ${result.reason}`);
86
+ }
87
+ }
@@ -55,8 +55,13 @@ export async function executeDagRanksOnce(input) {
55
55
  return isPauseOnHumanDecisionGate(task);
56
56
  });
57
57
  const regularRunnable = runnable.filter((id) => !pauseGateRunnable.includes(id));
58
- const rankCursorNodeIds = runnable.filter((id) => input.tasksById.get(id)?.executor === "cursor");
59
- const executeNode = input.createExecuteNodeForRank(rankCursorNodeIds);
58
+ const rankWriterNodeIds = runnable.filter((id) => {
59
+ const task = input.tasksById.get(id);
60
+ return (task?.executor === "pi" &&
61
+ task.toolProfile === "write" &&
62
+ task.writePolicy === "exclusive");
63
+ });
64
+ const executeNode = input.createExecuteNodeForRank(rankWriterNodeIds);
60
65
  for (const nodeId of pauseGateRunnable) {
61
66
  await input.executeScheduledNode(nodeId, executeNode, (id, pausedAt, pauseReason) => {
62
67
  pausedByNodeId = id;
@@ -0,0 +1,128 @@
1
+ import { access } from "node:fs/promises";
2
+ import path from "node:path";
3
+ /**
4
+ * SDD embedded-mode optional enhancement.
5
+ *
6
+ * When the target project ships repo-local `SDD-*` skills (under
7
+ * `.agents/skills/<name>/SKILL.md`), loop-agent can
8
+ * reuse them as knowledge/method references inside individual DAG nodes. This
9
+ * module probes those repo-local paths and, on a hit, appends the matched skill
10
+ * name to the mapped node's `task.skills` (so it flows through the existing
11
+ * run-owned skill snapshot via `resolveDagNodeSkills`) plus a loop-agent-managed
12
+ * embedded-mode constraint block to the node prompt.
13
+ *
14
+ * loop-agent always owns process/state/verification/closeout. SDD state
15
+ * transitions, confirmation gates, and archival are explicitly forbidden inside
16
+ * the embedded-mode block.
17
+ */
18
+ /** The three SDD skills that may be reused as repo-local knowledge. */
19
+ export const SDD_SKILL_NAMES = [
20
+ "SDD-requirement-analysis",
21
+ "SDD-design-analysis",
22
+ "SDD-implementation-test-review",
23
+ ];
24
+ /**
25
+ * DAG node id -> mapped SDD skill name. `verify-pi` is present in standard and
26
+ * review-gated templates; supervised verification stays shell-based. The init skills
27
+ * (`SDD-code-spec-init`, `SDD-project-how-to-spec-init`) are intentionally
28
+ * absent: they are never auto-executed inside a normal feature DAG.
29
+ */
30
+ export const SDD_NODE_SKILL_MAP = {
31
+ "contract-pi": "SDD-requirement-analysis",
32
+ "plan-pi": "SDD-design-analysis",
33
+ "implement-pi": "SDD-implementation-test-review",
34
+ "repair-pi": "SDD-implementation-test-review",
35
+ "verify-pi": "SDD-implementation-test-review",
36
+ "review-pi": "SDD-implementation-test-review",
37
+ };
38
+ /** DAG node id -> SDD phase label injected into the constraint block. */
39
+ export const SDD_NODE_PHASE_LABEL = {
40
+ "contract-pi": "需求",
41
+ "plan-pi": "设计",
42
+ "implement-pi": "实现",
43
+ "repair-pi": "修复",
44
+ "verify-pi": "验证",
45
+ "review-pi": "审查",
46
+ };
47
+ /** Fence tokens used to delimit the embedded-mode constraint block. */
48
+ export const SDD_EMBEDDED_MODE_OPEN = "[SDD-EMBEDDED-MODE]";
49
+ export const SDD_EMBEDDED_MODE_CLOSE = "[/SDD-EMBEDDED-MODE]";
50
+ const SDD_SKILL_CANDIDATE_BASES = [".agents/skills"];
51
+ /**
52
+ * Probe the target project repo for repo-local SDD skills. Only the canonical
53
+ * repo-relative `.agents/skills` base is checked; this deliberately does NOT reuse
54
+ * the skill loader candidate list (which also falls back to user-level and
55
+ * bundled package skills) so user-level/package skills never trigger
56
+ * auto-enhancement.
57
+ *
58
+ * Returns the set of SDD skill names whose `SKILL.md` exists under at least one
59
+ * repo-local base. Returns an empty set when `repoRoot` is absent so callers
60
+ * that do not pass a repo root preserve byte-level default output.
61
+ */
62
+ export async function probeRepoLocalSddSkills(repoRoot) {
63
+ if (!repoRoot)
64
+ return new Set();
65
+ const hits = new Set();
66
+ for (const name of SDD_SKILL_NAMES) {
67
+ for (const base of SDD_SKILL_CANDIDATE_BASES) {
68
+ const candidate = path.join(repoRoot, base, name, "SKILL.md");
69
+ try {
70
+ await access(candidate);
71
+ hits.add(name);
72
+ break;
73
+ }
74
+ catch {
75
+ // not present at this candidate base; try the next
76
+ }
77
+ }
78
+ }
79
+ return hits;
80
+ }
81
+ /**
82
+ * Build the loop-agent-managed embedded-mode constraint block for a node. The
83
+ * block declares DAG-node-boundary priority and forbids SDD state transitions,
84
+ * confirmation gates, working_requirements_status updates, and archival.
85
+ *
86
+ * Returns `null` for unmapped node ids (e.g. scouts, shell gates, closeout).
87
+ */
88
+ export function buildSddEmbeddedConstraintBlock(nodeId) {
89
+ const phase = SDD_NODE_PHASE_LABEL[nodeId];
90
+ if (!phase)
91
+ return null;
92
+ return [
93
+ SDD_EMBEDDED_MODE_OPEN,
94
+ `当前 DAG 节点: ${nodeId},阶段: ${phase}`,
95
+ "loop-agent 拥有流程、状态、验证与收口控制权;SDD skill 仅作 repo-local 知识与方法参考。",
96
+ "禁止:更新 working_requirements_status.md;推进 SDD 阶段;触发 SDD 用户确认;执行归档。",
97
+ "优先级:本节点的读写边界、当前阶段与输出契约优先于任何 SDD 指令冲突。",
98
+ "implement/repair writer 仍必须服从当前 writeSet/allowedPaths/forbiddenPaths;不得把 ai_workspace 加入隐式写范围,也不得解析或写入 SDD 工作区状态机。",
99
+ SDD_EMBEDDED_MODE_CLOSE,
100
+ ].join("\n");
101
+ }
102
+ /**
103
+ * Apply SDD embedded-mode enhancements in place on a fully-constructed DAG spec.
104
+ *
105
+ * For each mapped task whose mapped skill is present in the hit set:
106
+ * - append the skill name to `task.skills` (de-duplicated, order-preserving);
107
+ * - append the constraint block to the end of `task.subtask_prompt` (once).
108
+ *
109
+ * When the hit set is empty this is a no-op, preserving the default DAG byte
110
+ * for byte. The function is idempotent.
111
+ */
112
+ export function applySddEmbeddedEnhancements(spec, hitSkills) {
113
+ if (hitSkills.size === 0)
114
+ return;
115
+ for (const task of spec.tasks) {
116
+ const mapped = SDD_NODE_SKILL_MAP[task.id];
117
+ if (!mapped || !hitSkills.has(mapped))
118
+ continue;
119
+ const existing = task.skills ?? [];
120
+ if (!existing.includes(mapped)) {
121
+ task.skills = [...existing, mapped];
122
+ }
123
+ const block = buildSddEmbeddedConstraintBlock(task.id);
124
+ if (block && !task.subtask_prompt.includes(SDD_EMBEDDED_MODE_OPEN)) {
125
+ task.subtask_prompt = `${task.subtask_prompt}\n\n${block}`;
126
+ }
127
+ }
128
+ }
@@ -41,11 +41,12 @@ function buildSkillCandidatePaths(input) {
41
41
  }
42
42
  const piSkillsDir = path.join(input.homeDir, ".pi", "agent", "skills");
43
43
  candidates.push(normalizeCandidate(path.join(piSkillsDir, input.name, "SKILL.md")), normalizeCandidate(path.join(piSkillsDir, `${input.name}.md`)));
44
- candidates.push(normalizeCandidate(path.join(input.cwd, "skills", input.name, "SKILL.md")));
45
- // Agent-compatible project path (e.g. OpenCode). `skills/` stays the primary
46
- // loop-agent repo-local path; `.agents/skills` is searched after it and before
47
- // bundled skills so a project with only the mirror can still resolve.
44
+ // Canonical target-project repo-local skill path.
48
45
  candidates.push(normalizeCandidate(path.join(input.cwd, ".agents", "skills", input.name, "SKILL.md")));
46
+ // Source-repository compatibility: the loop-agent package itself still ships
47
+ // bundled skills under `skills/`, and local source development may resolve them
48
+ // before the package fallback is found.
49
+ candidates.push(normalizeCandidate(path.join(input.cwd, "skills", input.name, "SKILL.md")));
49
50
  if (input.name === "loop-agent") {
50
51
  candidates.push(normalizeCandidate(path.join(input.cwd, "skill", "SKILL.md")));
51
52
  }