@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
@@ -0,0 +1,529 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ import { writeTextAtomic } from "../../infrastructure/harness/atomic-write.js";
6
+ import { buildDagNodePromptEnvelope } from "./prompt.js";
7
+ import { DEFAULT_SKILL_INSTRUCTION_MAX_CHARS, DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS, resolveDagSkillInstructions, } from "./skill-instructions.js";
8
+ import { resolveDagNodeSkills } from "./skills.js";
9
+ export const SKILL_SNAPSHOT_SCHEMA_VERSION = 1;
10
+ export const SKILL_SNAPSHOT_RESOLVER_VERSION = 1;
11
+ export const SKILL_SNAPSHOT_REL_PATH = ".runtime/skill-snapshot.json";
12
+ const skillReferenceSchema = z.object({
13
+ name: z.string().min(1),
14
+ path: z.string().min(1),
15
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
16
+ bytes: z.number().int().nonnegative(),
17
+ truncated: z.boolean().optional(),
18
+ }).strict();
19
+ const unresolvedReferenceSchema = z.object({
20
+ name: z.string().min(1).optional(),
21
+ path: z.string().min(1),
22
+ reason: z.enum([
23
+ "invalid-path",
24
+ "missing",
25
+ "budget",
26
+ "read-error",
27
+ "cycle",
28
+ "already-resolved",
29
+ ]),
30
+ }).strict();
31
+ const resolvedInstructionSchema = z.object({
32
+ name: z.string().min(1),
33
+ resolution: z.enum(["resolved", "missing", "error"]),
34
+ path: z.string().min(1).optional(),
35
+ sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
36
+ bytes: z.number().int().nonnegative().optional(),
37
+ truncated: z.boolean().optional(),
38
+ searchedPaths: z.array(z.string().min(1)).optional(),
39
+ error: z.string().min(1).optional(),
40
+ injectionMode: z.enum(["bounded-inline", "name-only"]),
41
+ references: z.array(skillReferenceSchema).optional(),
42
+ unresolvedReferences: z.array(unresolvedReferenceSchema).optional(),
43
+ promptText: z.string(),
44
+ }).strict();
45
+ const skillSnapshotProfileSchema = z.object({
46
+ id: z.string().regex(/^sha256:[a-f0-9]{64}$/),
47
+ skills: z.array(z.string()),
48
+ includeLearnedPatterns: z.boolean(),
49
+ perSkillMaxChars: z.number().int().nonnegative(),
50
+ totalMaxChars: z.number().int().nonnegative(),
51
+ resolvedInstructions: z.array(resolvedInstructionSchema),
52
+ }).strict();
53
+ const skillSnapshotBindingSchema = z.discriminatedUnion("kind", [
54
+ z.object({
55
+ kind: z.literal("task"),
56
+ taskId: z.string().min(1),
57
+ profileId: z.string().regex(/^sha256:[a-f0-9]{64}$/),
58
+ }).strict(),
59
+ z.object({
60
+ kind: z.literal("dynamic-map-child"),
61
+ parentTaskId: z.string().min(1),
62
+ workflowNodeId: z.string().min(1),
63
+ profileId: z.string().regex(/^sha256:[a-f0-9]{64}$/),
64
+ }).strict(),
65
+ z.object({
66
+ kind: z.literal("dynamic-loop-body"),
67
+ parentTaskId: z.string().min(1),
68
+ workflowNodeId: z.string().min(1),
69
+ bodyTaskId: z.string().min(1),
70
+ profileId: z.string().regex(/^sha256:[a-f0-9]{64}$/),
71
+ }).strict(),
72
+ ]);
73
+ const skillSnapshotSchema = z.object({
74
+ schemaVersion: z.literal(SKILL_SNAPSHOT_SCHEMA_VERSION),
75
+ resolverVersion: z.literal(SKILL_SNAPSHOT_RESOLVER_VERSION),
76
+ runId: z.string().min(1),
77
+ createdAt: z.string().datetime(),
78
+ mode: z.enum(["run-start", "legacy-resume-backfill"]),
79
+ initialTaskIds: z.array(z.string().min(1)),
80
+ bindings: z.array(skillSnapshotBindingSchema),
81
+ profiles: z.array(skillSnapshotProfileSchema),
82
+ }).strict();
83
+ const skillSnapshotRefSchema = z.object({
84
+ schemaVersion: z.literal(SKILL_SNAPSHOT_SCHEMA_VERSION),
85
+ path: z.literal(SKILL_SNAPSHOT_REL_PATH),
86
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
87
+ createdAt: z.string().datetime(),
88
+ mode: z.enum(["run-start", "legacy-resume-backfill"]),
89
+ }).strict();
90
+ export class DagSkillSnapshotIntegrityError extends Error {
91
+ constructor(message, options) {
92
+ super(message, options);
93
+ this.name = "DagSkillSnapshotIntegrityError";
94
+ }
95
+ }
96
+ export function isDagSkillSnapshotIntegrityError(error) {
97
+ return error instanceof DagSkillSnapshotIntegrityError;
98
+ }
99
+ function sha256(content) {
100
+ return createHash("sha256").update(content).digest("hex");
101
+ }
102
+ function serializeSnapshot(snapshot) {
103
+ return `${JSON.stringify(snapshot, null, 2)}\n`;
104
+ }
105
+ function profileAnchor(input) {
106
+ return JSON.stringify({
107
+ resolverVersion: SKILL_SNAPSHOT_RESOLVER_VERSION,
108
+ skills: [...input.skills],
109
+ includeLearnedPatterns: input.includeLearnedPatterns,
110
+ perSkillMaxChars: input.perSkillMaxChars,
111
+ totalMaxChars: input.totalMaxChars,
112
+ });
113
+ }
114
+ function profileId(input) {
115
+ return `sha256:${sha256(profileAnchor(input))}`;
116
+ }
117
+ export function buildDagSkillSnapshotProfileRequest(spec, task) {
118
+ if (task.executor !== "pi")
119
+ return undefined;
120
+ const request = {
121
+ skills: [...resolveDagNodeSkills(spec, task)],
122
+ includeLearnedPatterns: task.role === "implementer",
123
+ perSkillMaxChars: DEFAULT_SKILL_INSTRUCTION_MAX_CHARS,
124
+ totalMaxChars: DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS,
125
+ };
126
+ return { id: profileId(request), ...request };
127
+ }
128
+ function isDynamicRuntimeTask(task) {
129
+ return Boolean(task.dynamicExpansion
130
+ || task.dynamicReduction
131
+ || task.dynamicCondition
132
+ || task.dynamicLoopUntil);
133
+ }
134
+ function bindingKey(binding) {
135
+ switch (binding.kind) {
136
+ case "task":
137
+ return `task:${binding.taskId}`;
138
+ case "dynamic-map-child":
139
+ return `dynamic-map-child:${binding.parentTaskId}:${binding.workflowNodeId}`;
140
+ case "dynamic-loop-body":
141
+ return `dynamic-loop-body:${binding.parentTaskId}:${binding.workflowNodeId}:${binding.bodyTaskId}`;
142
+ }
143
+ }
144
+ function collectDeclaredSkillSnapshotTargets(spec, initialTaskIds) {
145
+ const tasksById = new Map(spec.tasks.map((task) => [task.id, task]));
146
+ const targets = [];
147
+ for (const taskId of initialTaskIds) {
148
+ const task = tasksById.get(taskId);
149
+ if (!task) {
150
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot initial task is missing from run spec: ${taskId}`);
151
+ }
152
+ if (!isDynamicRuntimeTask(task)) {
153
+ targets.push({
154
+ binding: { kind: "task", taskId: task.id },
155
+ task,
156
+ });
157
+ }
158
+ if (task.dynamicExpansion) {
159
+ targets.push({
160
+ binding: {
161
+ kind: "dynamic-map-child",
162
+ parentTaskId: task.id,
163
+ workflowNodeId: task.dynamicExpansion.workflowNodeId,
164
+ },
165
+ task: task.dynamicExpansion.childTask,
166
+ });
167
+ }
168
+ for (const bodyTask of task.dynamicLoopUntil?.bodyTasks ?? []) {
169
+ targets.push({
170
+ binding: {
171
+ kind: "dynamic-loop-body",
172
+ parentTaskId: task.id,
173
+ workflowNodeId: task.dynamicLoopUntil.workflowNodeId,
174
+ bodyTaskId: bodyTask.id,
175
+ },
176
+ task: bodyTask,
177
+ });
178
+ }
179
+ }
180
+ return targets;
181
+ }
182
+ function matchesExpandedMapChild(taskId, parent) {
183
+ const expansion = parent.dynamicExpansion;
184
+ if (!expansion)
185
+ return false;
186
+ const prefix = `${expansion.childIdPrefix}-`;
187
+ if (!taskId.startsWith(prefix))
188
+ return false;
189
+ const suffix = taskId.slice(prefix.length);
190
+ if (!/^\d+$/.test(suffix))
191
+ return false;
192
+ const index = Number(suffix);
193
+ return (Number.isSafeInteger(index)
194
+ && index >= 1
195
+ && index <= Math.min(expansion.maxItems, expansion.maxExpandedNodes)
196
+ && suffix === String(index).padStart(4, "0"));
197
+ }
198
+ function matchesExpandedLoopChild(taskId, parent, bodyTaskId) {
199
+ const loop = parent.dynamicLoopUntil;
200
+ if (!loop)
201
+ return false;
202
+ const prefix = `${parent.id}-r`;
203
+ const suffix = `-${bodyTaskId}`;
204
+ if (!taskId.startsWith(prefix) || !taskId.endsWith(suffix))
205
+ return false;
206
+ const iterationText = taskId.slice(prefix.length, -suffix.length);
207
+ if (!/^\d+$/.test(iterationText))
208
+ return false;
209
+ const iteration = Number(iterationText);
210
+ return (Number.isSafeInteger(iteration)
211
+ && iteration >= 1
212
+ && iteration <= loop.maxIterations
213
+ && iterationText === String(iteration));
214
+ }
215
+ function resolveGeneratedTaskTarget(spec, task, initialTaskIds) {
216
+ const matches = [];
217
+ for (const parent of spec.tasks) {
218
+ if (!initialTaskIds.has(parent.id))
219
+ continue;
220
+ if (matchesExpandedMapChild(task.id, parent)) {
221
+ matches.push({
222
+ binding: {
223
+ kind: "dynamic-map-child",
224
+ parentTaskId: parent.id,
225
+ workflowNodeId: parent.dynamicExpansion.workflowNodeId,
226
+ },
227
+ task,
228
+ });
229
+ }
230
+ for (const bodyTask of parent.dynamicLoopUntil?.bodyTasks ?? []) {
231
+ if (!matchesExpandedLoopChild(task.id, parent, bodyTask.id))
232
+ continue;
233
+ matches.push({
234
+ binding: {
235
+ kind: "dynamic-loop-body",
236
+ parentTaskId: parent.id,
237
+ workflowNodeId: parent.dynamicLoopUntil.workflowNodeId,
238
+ bodyTaskId: bodyTask.id,
239
+ },
240
+ task,
241
+ });
242
+ }
243
+ }
244
+ if (matches.length > 1) {
245
+ throw new DagSkillSnapshotIntegrityError(`dynamic task ${task.id} matches multiple skill snapshot bindings`);
246
+ }
247
+ return matches[0];
248
+ }
249
+ export function collectDagSkillSnapshotBindings(spec, initialTaskIds = spec.tasks.map((task) => task.id)) {
250
+ const bindings = [];
251
+ for (const target of collectDeclaredSkillSnapshotTargets(spec, initialTaskIds)) {
252
+ const request = buildDagSkillSnapshotProfileRequest(spec, target.task);
253
+ if (!request)
254
+ continue;
255
+ bindings.push({ ...target.binding, profileId: request.id });
256
+ }
257
+ return bindings;
258
+ }
259
+ export function collectDagSkillSnapshotProfileRequests(spec, initialTaskIds = spec.tasks.map((task) => task.id)) {
260
+ const profiles = new Map();
261
+ for (const target of collectDeclaredSkillSnapshotTargets(spec, initialTaskIds)) {
262
+ const profile = buildDagSkillSnapshotProfileRequest(spec, target.task);
263
+ if (profile && !profiles.has(profile.id))
264
+ profiles.set(profile.id, profile);
265
+ }
266
+ return [...profiles.values()];
267
+ }
268
+ function validateResolvedInstructionOrder(profile) {
269
+ const primaryNames = profile.resolvedInstructions
270
+ .slice(0, profile.skills.length)
271
+ .map((instruction) => instruction.name);
272
+ if (JSON.stringify(primaryNames) !== JSON.stringify(profile.skills)) {
273
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot profile ${profile.id} resolved instruction order does not match its requested skills`);
274
+ }
275
+ const extras = profile.resolvedInstructions.slice(profile.skills.length);
276
+ if (extras.length > 1
277
+ || (extras.length === 1
278
+ && (!profile.includeLearnedPatterns
279
+ || !profile.skills.includes("loop-agent")
280
+ || extras[0]?.name !== "loop-agent-learned"))) {
281
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot profile ${profile.id} contains unexpected resolved instructions`);
282
+ }
283
+ }
284
+ function validateSnapshotSemantics(snapshot) {
285
+ if (new Set(snapshot.initialTaskIds).size !== snapshot.initialTaskIds.length) {
286
+ throw new DagSkillSnapshotIntegrityError("skill snapshot contains duplicate initial task ids");
287
+ }
288
+ const seen = new Set();
289
+ for (const profile of snapshot.profiles) {
290
+ if (profile.id !== profileId(profile)) {
291
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot profile id mismatch: ${profile.id}`);
292
+ }
293
+ if (seen.has(profile.id)) {
294
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot contains duplicate profile: ${profile.id}`);
295
+ }
296
+ seen.add(profile.id);
297
+ validateResolvedInstructionOrder(profile);
298
+ }
299
+ const bindingKeys = new Set();
300
+ const referencedProfiles = new Set();
301
+ for (const binding of snapshot.bindings) {
302
+ const key = bindingKey(binding);
303
+ if (bindingKeys.has(key)) {
304
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot contains duplicate binding: ${key}`);
305
+ }
306
+ if (!seen.has(binding.profileId)) {
307
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot binding ${key} references missing profile ${binding.profileId}`);
308
+ }
309
+ bindingKeys.add(key);
310
+ referencedProfiles.add(binding.profileId);
311
+ }
312
+ for (const profile of snapshot.profiles) {
313
+ if (!referencedProfiles.has(profile.id)) {
314
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot contains unbound profile: ${profile.id}`);
315
+ }
316
+ }
317
+ }
318
+ function parseSnapshot(value) {
319
+ try {
320
+ const snapshot = skillSnapshotSchema.parse(value);
321
+ validateSnapshotSemantics(snapshot);
322
+ return snapshot;
323
+ }
324
+ catch (error) {
325
+ if (isDagSkillSnapshotIntegrityError(error))
326
+ throw error;
327
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot schema validation failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
328
+ }
329
+ }
330
+ export async function createSkillSnapshot(input) {
331
+ const initialTaskIds = input.initialTaskIds
332
+ ?? input.spec.tasks.map((task) => task.id);
333
+ const profiles = [];
334
+ for (const request of collectDagSkillSnapshotProfileRequests(input.spec, initialTaskIds)) {
335
+ const resolvedInstructions = await resolveDagSkillInstructions(request.skills, {
336
+ cwd: input.cwd,
337
+ includeLearnedPatterns: request.includeLearnedPatterns,
338
+ perSkillMaxChars: request.perSkillMaxChars,
339
+ totalMaxChars: request.totalMaxChars,
340
+ ...(input.homeDir !== undefined ? { homeDir: input.homeDir } : {}),
341
+ ...(input.env !== undefined ? { env: input.env } : {}),
342
+ });
343
+ profiles.push({ ...request, resolvedInstructions });
344
+ }
345
+ return {
346
+ schemaVersion: SKILL_SNAPSHOT_SCHEMA_VERSION,
347
+ resolverVersion: SKILL_SNAPSHOT_RESOLVER_VERSION,
348
+ runId: input.runId,
349
+ createdAt: (input.now ?? new Date()).toISOString(),
350
+ mode: input.mode,
351
+ initialTaskIds,
352
+ bindings: collectDagSkillSnapshotBindings(input.spec, initialTaskIds),
353
+ profiles,
354
+ };
355
+ }
356
+ export async function writeSkillSnapshot(runDir, snapshotValue) {
357
+ const snapshot = parseSnapshot(snapshotValue);
358
+ const raw = serializeSnapshot(snapshot);
359
+ await writeTextAtomic(path.join(runDir, ...SKILL_SNAPSHOT_REL_PATH.split("/")), raw);
360
+ return {
361
+ schemaVersion: SKILL_SNAPSHOT_SCHEMA_VERSION,
362
+ path: SKILL_SNAPSHOT_REL_PATH,
363
+ sha256: sha256(raw),
364
+ createdAt: snapshot.createdAt,
365
+ mode: snapshot.mode,
366
+ };
367
+ }
368
+ export async function readSkillSnapshot(runDir, refValue, options) {
369
+ let ref;
370
+ try {
371
+ ref = skillSnapshotRefSchema.parse(refValue);
372
+ }
373
+ catch (error) {
374
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot ref is invalid: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
375
+ }
376
+ const snapshotPath = path.join(runDir, ...ref.path.split("/"));
377
+ let raw;
378
+ try {
379
+ raw = await readFile(snapshotPath);
380
+ }
381
+ catch (error) {
382
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot missing or unreadable at ${ref.path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
383
+ }
384
+ const actualHash = sha256(raw);
385
+ if (actualHash !== ref.sha256) {
386
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot integrity check failed at ${ref.path}: expected sha256 ${ref.sha256}, got ${actualHash}`);
387
+ }
388
+ let decoded;
389
+ try {
390
+ decoded = JSON.parse(raw.toString("utf-8"));
391
+ }
392
+ catch (error) {
393
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot JSON is invalid: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
394
+ }
395
+ const snapshot = parseSnapshot(decoded);
396
+ if (snapshot.createdAt !== ref.createdAt || snapshot.mode !== ref.mode) {
397
+ throw new DagSkillSnapshotIntegrityError("skill snapshot ref metadata does not match the snapshot artifact");
398
+ }
399
+ if (options?.expectedRunId && snapshot.runId !== options.expectedRunId) {
400
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot run ownership mismatch: expected ${options.expectedRunId}, got ${snapshot.runId}`);
401
+ }
402
+ return snapshot;
403
+ }
404
+ export async function prepareSkillSnapshotForContinuation(input) {
405
+ if (input.ref) {
406
+ const snapshot = await readSkillSnapshot(input.runDir, input.ref, {
407
+ expectedRunId: input.runId,
408
+ });
409
+ assertSkillSnapshotCoversSpec(snapshot, input.spec, input.state);
410
+ return { ref: input.ref, backfilled: false };
411
+ }
412
+ const snapshotPath = path.join(input.runDir, ...SKILL_SNAPSHOT_REL_PATH.split("/"));
413
+ try {
414
+ await readFile(snapshotPath);
415
+ throw new DagSkillSnapshotIntegrityError("skill snapshot artifact exists without a state ref; refusing legacy backfill");
416
+ }
417
+ catch (error) {
418
+ if (isDagSkillSnapshotIntegrityError(error))
419
+ throw error;
420
+ if (!(error && typeof error === "object" && "code" in error
421
+ && error.code === "ENOENT")) {
422
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot artifact is unreadable during legacy backfill: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
423
+ }
424
+ }
425
+ const snapshot = await createSkillSnapshot({
426
+ runId: input.runId,
427
+ spec: input.spec,
428
+ cwd: input.cwd,
429
+ mode: "legacy-resume-backfill",
430
+ });
431
+ assertSkillSnapshotCoversSpec(snapshot, input.spec, input.state);
432
+ return {
433
+ ref: await writeSkillSnapshot(input.runDir, snapshot),
434
+ backfilled: true,
435
+ };
436
+ }
437
+ export function assertSkillSnapshotCoversSpec(snapshot, spec, state) {
438
+ const currentInitialIds = spec.tasks
439
+ .slice(0, snapshot.initialTaskIds.length)
440
+ .map((task) => task.id);
441
+ if (JSON.stringify(currentInitialIds) !== JSON.stringify(snapshot.initialTaskIds)) {
442
+ throw new DagSkillSnapshotIntegrityError("skill snapshot initial task scope does not match the current run spec");
443
+ }
444
+ const expectedBindings = collectDagSkillSnapshotBindings(spec, snapshot.initialTaskIds);
445
+ const actualByKey = new Map(snapshot.bindings.map((binding) => [bindingKey(binding), binding]));
446
+ const expectedByKey = new Map(expectedBindings.map((binding) => [bindingKey(binding), binding]));
447
+ for (const [key, expected] of expectedByKey) {
448
+ const actual = actualByKey.get(key);
449
+ if (!actual) {
450
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot binding mismatch: missing ${key}`);
451
+ }
452
+ if (actual.profileId !== expected.profileId) {
453
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot binding mismatch for ${key}: expected ${actual.profileId}, got ${expected.profileId}`);
454
+ }
455
+ }
456
+ for (const key of actualByKey.keys()) {
457
+ if (!expectedByKey.has(key)) {
458
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot binding mismatch: unexpected ${key}`);
459
+ }
460
+ }
461
+ const initialIds = new Set(snapshot.initialTaskIds);
462
+ for (const task of spec.tasks.slice(snapshot.initialTaskIds.length)) {
463
+ if (!state
464
+ || !state.nodes[task.id]
465
+ || !state.ranks.some((rank) => rank.includes(task.id))) {
466
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot cannot prove runtime origin for appended task: ${task.id}`);
467
+ }
468
+ const generated = resolveGeneratedTaskTarget(spec, task, initialIds);
469
+ if (!generated) {
470
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot run spec contains an unexpected task: ${task.id}`);
471
+ }
472
+ const key = bindingKey(generated.binding);
473
+ const binding = actualByKey.get(key);
474
+ const request = buildDagSkillSnapshotProfileRequest(spec, generated.task);
475
+ if (!request) {
476
+ if (binding) {
477
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot generated task binding mismatch for ${task.id}`);
478
+ }
479
+ continue;
480
+ }
481
+ if (!binding || binding.profileId !== request.id) {
482
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot generated task binding mismatch for ${task.id}`);
483
+ }
484
+ }
485
+ }
486
+ export function resolveNodeSkillsFromSnapshot(snapshot, spec, task) {
487
+ const request = buildDagSkillSnapshotProfileRequest(spec, task);
488
+ if (!request)
489
+ return [];
490
+ const initialIds = new Set(snapshot.initialTaskIds);
491
+ const generated = initialIds.has(task.id)
492
+ ? undefined
493
+ : resolveGeneratedTaskTarget(spec, task, initialIds);
494
+ const targetBinding = generated?.binding ?? {
495
+ kind: "task",
496
+ taskId: task.id,
497
+ };
498
+ const key = bindingKey(targetBinding);
499
+ const binding = snapshot.bindings.find((candidate) => bindingKey(candidate) === key);
500
+ if (!binding || binding.profileId !== request.id) {
501
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot binding mismatch for ${key}`);
502
+ }
503
+ const profile = snapshot.profiles.find((candidate) => candidate.id === binding.profileId);
504
+ if (!profile) {
505
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot profile mismatch: missing ${binding.profileId}`);
506
+ }
507
+ return profile.resolvedInstructions;
508
+ }
509
+ function stripPromptText(instruction) {
510
+ const { promptText: _promptText, ...metadata } = instruction;
511
+ return metadata;
512
+ }
513
+ export function buildNodePromptFromSnapshot(input) {
514
+ const skillNames = resolveDagNodeSkills(input.spec, input.task);
515
+ const resolvedSkillInstructions = resolveNodeSkillsFromSnapshot(input.snapshot, input.spec, input.task);
516
+ return {
517
+ prompt: buildDagNodePromptEnvelope({
518
+ spec: input.spec,
519
+ task: input.task,
520
+ upstream: input.upstream,
521
+ resolvedSkills: skillNames,
522
+ resolvedSkillInstructions,
523
+ }),
524
+ resolvedSkills: resolvedSkillInstructions.map(stripPromptText),
525
+ };
526
+ }
527
+ export function hashSkillSnapshot(snapshot) {
528
+ return sha256(serializeSnapshot(snapshot));
529
+ }
@@ -1,12 +1,14 @@
1
1
  import { z } from "zod";
2
2
  import { assertDagPromptSourceRule } from "./prompt-source.js";
3
+ import { dagRetryPolicySchema } from "./retry-policy.js";
3
4
  export const dagComplexitySchema = z.enum(["HIGH", "MED", "LOW"]);
4
5
  export const dagNodeExecutorSchema = z.enum([
5
- "cursor",
6
6
  "pi",
7
7
  "shell",
8
8
  "static",
9
9
  ]);
10
+ export const CURSOR_DAG_EXECUTOR_REMOVED_ERROR = 'executor "cursor" is no longer supported; regenerate the DAG with Pi-only writers (implement-pi / repair-pi)';
11
+ export const CURSOR_EXECUTOR_MODELS_REMOVED_ERROR = 'executorModels.cursor is no longer supported; use executorModels.pi only';
10
12
  export const dagToolProfileSchema = z.enum(["read-only", "write"]);
11
13
  export const dagShellPresetSchema = z.enum(["loop-agent-standard-verify"]);
12
14
  export const dagVerifyQuotaSchema = z.enum(["1", "3", "full"]);
@@ -32,6 +34,32 @@ export const dagRepairArtifactGateSchema = z.object({
32
34
  fromNodeId: z
33
35
  .string()
34
36
  .regex(/^[a-z][a-z0-9-]*$/, "fromNodeId must be kebab-case"),
37
+ /**
38
+ * Explicit reference to the governed Pi repair writer this gate feeds.
39
+ * New generated supervised DAGs must set this. Legacy DAGs without it are
40
+ * only accepted when a unique safe downstream Pi writer can be derived.
41
+ */
42
+ repairNodeId: z
43
+ .string()
44
+ .regex(/^[a-z][a-z0-9-]*$/, "repairNodeId must be kebab-case")
45
+ .optional(),
46
+ });
47
+ export const DAG_RUNTIME_CONTRACT_SCHEMA_VERSION = 1;
48
+ export const DAG_AGENT_RUNTIME_PI_ONLY = "pi-only";
49
+ export const DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1 = "explicit-node-v1";
50
+ export const dagRuntimeContractSchema = z.object({
51
+ schemaVersion: z.literal(DAG_RUNTIME_CONTRACT_SCHEMA_VERSION),
52
+ agentRuntime: z.literal(DAG_AGENT_RUNTIME_PI_ONLY),
53
+ repairWriterProtocol: z.literal(DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1),
54
+ /**
55
+ * Optional minimum controller version for diagnosis / fail-below. Capability
56
+ * fields above are primary; semver is only used for guidance when the
57
+ * controller is otherwise compatible.
58
+ */
59
+ minimumControllerVersion: z
60
+ .string()
61
+ .regex(/^\d+\.\d+\.\d+([-+].+)?$/, "minimumControllerVersion must be semver")
62
+ .optional(),
35
63
  });
36
64
  export const dagVerdictGateSchema = z.object({
37
65
  fromNodeId: z
@@ -43,7 +71,7 @@ export const dagVerdictGateSchema = z.object({
43
71
  });
44
72
  export const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
45
73
  export const dagVersionSchema = z
46
- .union([z.literal(1), z.literal(2)])
74
+ .union([z.literal(1), z.literal(2), z.literal(3)])
47
75
  .default(1);
48
76
  export const dagRoleSchema = z.enum([
49
77
  "planner",
@@ -216,6 +244,7 @@ export const dagTaskSchema = z.object({
216
244
  allowedPaths: z.array(z.string()).optional().default([]),
217
245
  forbiddenPaths: z.array(z.string()).optional().default([]),
218
246
  decisionGate: dagDecisionGateSchema.optional(),
247
+ retryPolicy: dagRetryPolicySchema.optional(),
219
248
  dynamicExpansion: dagDynamicExpansionSchema.optional(),
220
249
  dynamicReduction: dagDynamicReductionSchema.optional(),
221
250
  dynamicCondition: dagDynamicConditionSchema.optional(),
@@ -230,15 +259,16 @@ export const dagModelsSchema = z
230
259
  .partial();
231
260
  export const dagExecutorModelsSchema = z
232
261
  .object({
233
- cursor: dagModelsSchema.optional(),
234
262
  pi: dagModelsSchema.optional(),
235
263
  })
236
- .partial();
264
+ .strict();
237
265
  export const dagOutputLanguageSchema = z.enum(["zh-CN", "en"]);
238
266
  export const DEFAULT_DAG_OUTPUT_LANGUAGE = "zh-CN";
239
- export const dagSpecSchema = z.object({
267
+ export const dagSpecSchema = z
268
+ .object({
240
269
  version: dagVersionSchema,
241
270
  title: z.string().min(1),
271
+ runtimeContract: dagRuntimeContractSchema.optional(),
242
272
  outputLanguage: dagOutputLanguageSchema.optional(),
243
273
  objective: z.string().optional(),
244
274
  successCriteria: z.array(z.string()).optional(),
@@ -249,13 +279,34 @@ export const dagSpecSchema = z.object({
249
279
  skillsByRole: z.record(z.string(), z.array(z.string())).optional(),
250
280
  executorModels: dagExecutorModelsSchema.optional(),
251
281
  tasks: z.array(dagTaskSchema).min(1),
282
+ })
283
+ .superRefine((spec, ctx) => {
284
+ if (spec.runtimeContract && spec.version !== 3) {
285
+ ctx.addIssue({
286
+ code: z.ZodIssueCode.custom,
287
+ message: "runtimeContract requires DagSpec version 3",
288
+ path: ["version"],
289
+ });
290
+ }
291
+ if (spec.version === 3 && !spec.runtimeContract) {
292
+ ctx.addIssue({
293
+ code: z.ZodIssueCode.custom,
294
+ message: "DagSpec version 3 requires runtimeContract",
295
+ path: ["runtimeContract"],
296
+ });
297
+ }
298
+ // Catch raw cursor keys that Zod .strict() on nested objects already rejects when parsed
299
+ // via parseDagSpec; this refine covers typed object construction paths.
300
+ const models = spec.executorModels;
301
+ if (models && "cursor" in models) {
302
+ ctx.addIssue({
303
+ code: z.ZodIssueCode.custom,
304
+ message: CURSOR_EXECUTOR_MODELS_REMOVED_ERROR,
305
+ path: ["executorModels", "cursor"],
306
+ });
307
+ }
252
308
  });
253
309
  export const DEFAULT_DAG_EXECUTOR_MODELS = {
254
- cursor: {
255
- LOW: "composer-2.5",
256
- MED: "composer-2.5",
257
- HIGH: "gpt-5.5",
258
- },
259
310
  pi: {
260
311
  LOW: "gpt-5.3-codex-spark",
261
312
  MED: "glm-5.2",
@@ -269,6 +320,31 @@ export function parseDagSpec(raw) {
269
320
  if (typeof raw === "object" && raw !== null && "models" in raw) {
270
321
  throw new Error(LEGACY_TOP_LEVEL_MODELS_ERROR);
271
322
  }
323
+ if (typeof raw === "object" && raw !== null) {
324
+ const obj = raw;
325
+ const executorModels = obj.executorModels;
326
+ if (typeof executorModels === "object" &&
327
+ executorModels !== null &&
328
+ "cursor" in executorModels) {
329
+ throw new Error(CURSOR_EXECUTOR_MODELS_REMOVED_ERROR);
330
+ }
331
+ const tasks = obj.tasks;
332
+ if (Array.isArray(tasks)) {
333
+ for (const task of tasks) {
334
+ if (typeof task === "object" &&
335
+ task !== null &&
336
+ task.executor === "cursor") {
337
+ throw new Error(CURSOR_DAG_EXECUTOR_REMOVED_ERROR);
338
+ }
339
+ }
340
+ }
341
+ const defaults = obj.defaults;
342
+ if (typeof defaults === "object" &&
343
+ defaults !== null &&
344
+ defaults.executor === "cursor") {
345
+ throw new Error(CURSOR_DAG_EXECUTOR_REMOVED_ERROR);
346
+ }
347
+ }
272
348
  return dagSpecSchema.parse(raw);
273
349
  }
274
350
  export function resolveModelForTask(task, executorModels) {