@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
@@ -0,0 +1,527 @@
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
+ });
341
+ profiles.push({ ...request, resolvedInstructions });
342
+ }
343
+ return {
344
+ schemaVersion: SKILL_SNAPSHOT_SCHEMA_VERSION,
345
+ resolverVersion: SKILL_SNAPSHOT_RESOLVER_VERSION,
346
+ runId: input.runId,
347
+ createdAt: (input.now ?? new Date()).toISOString(),
348
+ mode: input.mode,
349
+ initialTaskIds,
350
+ bindings: collectDagSkillSnapshotBindings(input.spec, initialTaskIds),
351
+ profiles,
352
+ };
353
+ }
354
+ export async function writeSkillSnapshot(runDir, snapshotValue) {
355
+ const snapshot = parseSnapshot(snapshotValue);
356
+ const raw = serializeSnapshot(snapshot);
357
+ await writeTextAtomic(path.join(runDir, ...SKILL_SNAPSHOT_REL_PATH.split("/")), raw);
358
+ return {
359
+ schemaVersion: SKILL_SNAPSHOT_SCHEMA_VERSION,
360
+ path: SKILL_SNAPSHOT_REL_PATH,
361
+ sha256: sha256(raw),
362
+ createdAt: snapshot.createdAt,
363
+ mode: snapshot.mode,
364
+ };
365
+ }
366
+ export async function readSkillSnapshot(runDir, refValue, options) {
367
+ let ref;
368
+ try {
369
+ ref = skillSnapshotRefSchema.parse(refValue);
370
+ }
371
+ catch (error) {
372
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot ref is invalid: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
373
+ }
374
+ const snapshotPath = path.join(runDir, ...ref.path.split("/"));
375
+ let raw;
376
+ try {
377
+ raw = await readFile(snapshotPath);
378
+ }
379
+ catch (error) {
380
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot missing or unreadable at ${ref.path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
381
+ }
382
+ const actualHash = sha256(raw);
383
+ if (actualHash !== ref.sha256) {
384
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot integrity check failed at ${ref.path}: expected sha256 ${ref.sha256}, got ${actualHash}`);
385
+ }
386
+ let decoded;
387
+ try {
388
+ decoded = JSON.parse(raw.toString("utf-8"));
389
+ }
390
+ catch (error) {
391
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot JSON is invalid: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
392
+ }
393
+ const snapshot = parseSnapshot(decoded);
394
+ if (snapshot.createdAt !== ref.createdAt || snapshot.mode !== ref.mode) {
395
+ throw new DagSkillSnapshotIntegrityError("skill snapshot ref metadata does not match the snapshot artifact");
396
+ }
397
+ if (options?.expectedRunId && snapshot.runId !== options.expectedRunId) {
398
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot run ownership mismatch: expected ${options.expectedRunId}, got ${snapshot.runId}`);
399
+ }
400
+ return snapshot;
401
+ }
402
+ export async function prepareSkillSnapshotForContinuation(input) {
403
+ if (input.ref) {
404
+ const snapshot = await readSkillSnapshot(input.runDir, input.ref, {
405
+ expectedRunId: input.runId,
406
+ });
407
+ assertSkillSnapshotCoversSpec(snapshot, input.spec, input.state);
408
+ return { ref: input.ref, backfilled: false };
409
+ }
410
+ const snapshotPath = path.join(input.runDir, ...SKILL_SNAPSHOT_REL_PATH.split("/"));
411
+ try {
412
+ await readFile(snapshotPath);
413
+ throw new DagSkillSnapshotIntegrityError("skill snapshot artifact exists without a state ref; refusing legacy backfill");
414
+ }
415
+ catch (error) {
416
+ if (isDagSkillSnapshotIntegrityError(error))
417
+ throw error;
418
+ if (!(error && typeof error === "object" && "code" in error
419
+ && error.code === "ENOENT")) {
420
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot artifact is unreadable during legacy backfill: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
421
+ }
422
+ }
423
+ const snapshot = await createSkillSnapshot({
424
+ runId: input.runId,
425
+ spec: input.spec,
426
+ cwd: input.cwd,
427
+ mode: "legacy-resume-backfill",
428
+ });
429
+ assertSkillSnapshotCoversSpec(snapshot, input.spec, input.state);
430
+ return {
431
+ ref: await writeSkillSnapshot(input.runDir, snapshot),
432
+ backfilled: true,
433
+ };
434
+ }
435
+ export function assertSkillSnapshotCoversSpec(snapshot, spec, state) {
436
+ const currentInitialIds = spec.tasks
437
+ .slice(0, snapshot.initialTaskIds.length)
438
+ .map((task) => task.id);
439
+ if (JSON.stringify(currentInitialIds) !== JSON.stringify(snapshot.initialTaskIds)) {
440
+ throw new DagSkillSnapshotIntegrityError("skill snapshot initial task scope does not match the current run spec");
441
+ }
442
+ const expectedBindings = collectDagSkillSnapshotBindings(spec, snapshot.initialTaskIds);
443
+ const actualByKey = new Map(snapshot.bindings.map((binding) => [bindingKey(binding), binding]));
444
+ const expectedByKey = new Map(expectedBindings.map((binding) => [bindingKey(binding), binding]));
445
+ for (const [key, expected] of expectedByKey) {
446
+ const actual = actualByKey.get(key);
447
+ if (!actual) {
448
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot binding mismatch: missing ${key}`);
449
+ }
450
+ if (actual.profileId !== expected.profileId) {
451
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot binding mismatch for ${key}: expected ${actual.profileId}, got ${expected.profileId}`);
452
+ }
453
+ }
454
+ for (const key of actualByKey.keys()) {
455
+ if (!expectedByKey.has(key)) {
456
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot binding mismatch: unexpected ${key}`);
457
+ }
458
+ }
459
+ const initialIds = new Set(snapshot.initialTaskIds);
460
+ for (const task of spec.tasks.slice(snapshot.initialTaskIds.length)) {
461
+ if (!state
462
+ || !state.nodes[task.id]
463
+ || !state.ranks.some((rank) => rank.includes(task.id))) {
464
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot cannot prove runtime origin for appended task: ${task.id}`);
465
+ }
466
+ const generated = resolveGeneratedTaskTarget(spec, task, initialIds);
467
+ if (!generated) {
468
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot run spec contains an unexpected task: ${task.id}`);
469
+ }
470
+ const key = bindingKey(generated.binding);
471
+ const binding = actualByKey.get(key);
472
+ const request = buildDagSkillSnapshotProfileRequest(spec, generated.task);
473
+ if (!request) {
474
+ if (binding) {
475
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot generated task binding mismatch for ${task.id}`);
476
+ }
477
+ continue;
478
+ }
479
+ if (!binding || binding.profileId !== request.id) {
480
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot generated task binding mismatch for ${task.id}`);
481
+ }
482
+ }
483
+ }
484
+ export function resolveNodeSkillsFromSnapshot(snapshot, spec, task) {
485
+ const request = buildDagSkillSnapshotProfileRequest(spec, task);
486
+ if (!request)
487
+ return [];
488
+ const initialIds = new Set(snapshot.initialTaskIds);
489
+ const generated = initialIds.has(task.id)
490
+ ? undefined
491
+ : resolveGeneratedTaskTarget(spec, task, initialIds);
492
+ const targetBinding = generated?.binding ?? {
493
+ kind: "task",
494
+ taskId: task.id,
495
+ };
496
+ const key = bindingKey(targetBinding);
497
+ const binding = snapshot.bindings.find((candidate) => bindingKey(candidate) === key);
498
+ if (!binding || binding.profileId !== request.id) {
499
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot binding mismatch for ${key}`);
500
+ }
501
+ const profile = snapshot.profiles.find((candidate) => candidate.id === binding.profileId);
502
+ if (!profile) {
503
+ throw new DagSkillSnapshotIntegrityError(`skill snapshot profile mismatch: missing ${binding.profileId}`);
504
+ }
505
+ return profile.resolvedInstructions;
506
+ }
507
+ function stripPromptText(instruction) {
508
+ const { promptText: _promptText, ...metadata } = instruction;
509
+ return metadata;
510
+ }
511
+ export function buildNodePromptFromSnapshot(input) {
512
+ const skillNames = resolveDagNodeSkills(input.spec, input.task);
513
+ const resolvedSkillInstructions = resolveNodeSkillsFromSnapshot(input.snapshot, input.spec, input.task);
514
+ return {
515
+ prompt: buildDagNodePromptEnvelope({
516
+ spec: input.spec,
517
+ task: input.task,
518
+ upstream: input.upstream,
519
+ resolvedSkills: skillNames,
520
+ resolvedSkillInstructions,
521
+ }),
522
+ resolvedSkills: resolvedSkillInstructions.map(stripPromptText),
523
+ };
524
+ }
525
+ export function hashSkillSnapshot(snapshot) {
526
+ return sha256(serializeSnapshot(snapshot));
527
+ }
@@ -2,11 +2,12 @@ import { z } from "zod";
2
2
  import { assertDagPromptSourceRule } from "./prompt-source.js";
3
3
  export const dagComplexitySchema = z.enum(["HIGH", "MED", "LOW"]);
4
4
  export const dagNodeExecutorSchema = z.enum([
5
- "cursor",
6
5
  "pi",
7
6
  "shell",
8
7
  "static",
9
8
  ]);
9
+ export const CURSOR_DAG_EXECUTOR_REMOVED_ERROR = 'executor "cursor" is no longer supported; regenerate the DAG with Pi-only writers (implement-pi / repair-pi)';
10
+ export const CURSOR_EXECUTOR_MODELS_REMOVED_ERROR = 'executorModels.cursor is no longer supported; use executorModels.pi only';
10
11
  export const dagToolProfileSchema = z.enum(["read-only", "write"]);
11
12
  export const dagShellPresetSchema = z.enum(["loop-agent-standard-verify"]);
12
13
  export const dagVerifyQuotaSchema = z.enum(["1", "3", "full"]);
@@ -230,13 +231,16 @@ export const dagModelsSchema = z
230
231
  .partial();
231
232
  export const dagExecutorModelsSchema = z
232
233
  .object({
233
- cursor: dagModelsSchema.optional(),
234
234
  pi: dagModelsSchema.optional(),
235
235
  })
236
- .partial();
237
- export const dagSpecSchema = z.object({
236
+ .strict();
237
+ export const dagOutputLanguageSchema = z.enum(["zh-CN", "en"]);
238
+ export const DEFAULT_DAG_OUTPUT_LANGUAGE = "zh-CN";
239
+ export const dagSpecSchema = z
240
+ .object({
238
241
  version: dagVersionSchema,
239
242
  title: z.string().min(1),
243
+ outputLanguage: dagOutputLanguageSchema.optional(),
240
244
  objective: z.string().optional(),
241
245
  successCriteria: z.array(z.string()).optional(),
242
246
  globalConstraints: z.array(z.string()).optional(),
@@ -246,13 +250,20 @@ export const dagSpecSchema = z.object({
246
250
  skillsByRole: z.record(z.string(), z.array(z.string())).optional(),
247
251
  executorModels: dagExecutorModelsSchema.optional(),
248
252
  tasks: z.array(dagTaskSchema).min(1),
253
+ })
254
+ .superRefine((spec, ctx) => {
255
+ // Catch raw cursor keys that Zod .strict() on nested objects already rejects when parsed
256
+ // via parseDagSpec; this refine covers typed object construction paths.
257
+ const models = spec.executorModels;
258
+ if (models && "cursor" in models) {
259
+ ctx.addIssue({
260
+ code: z.ZodIssueCode.custom,
261
+ message: CURSOR_EXECUTOR_MODELS_REMOVED_ERROR,
262
+ path: ["executorModels", "cursor"],
263
+ });
264
+ }
249
265
  });
250
266
  export const DEFAULT_DAG_EXECUTOR_MODELS = {
251
- cursor: {
252
- LOW: "composer-2.5",
253
- MED: "composer-2.5",
254
- HIGH: "gpt-5.5",
255
- },
256
267
  pi: {
257
268
  LOW: "gpt-5.3-codex-spark",
258
269
  MED: "glm-5.2",
@@ -266,6 +277,31 @@ export function parseDagSpec(raw) {
266
277
  if (typeof raw === "object" && raw !== null && "models" in raw) {
267
278
  throw new Error(LEGACY_TOP_LEVEL_MODELS_ERROR);
268
279
  }
280
+ if (typeof raw === "object" && raw !== null) {
281
+ const obj = raw;
282
+ const executorModels = obj.executorModels;
283
+ if (typeof executorModels === "object" &&
284
+ executorModels !== null &&
285
+ "cursor" in executorModels) {
286
+ throw new Error(CURSOR_EXECUTOR_MODELS_REMOVED_ERROR);
287
+ }
288
+ const tasks = obj.tasks;
289
+ if (Array.isArray(tasks)) {
290
+ for (const task of tasks) {
291
+ if (typeof task === "object" &&
292
+ task !== null &&
293
+ task.executor === "cursor") {
294
+ throw new Error(CURSOR_DAG_EXECUTOR_REMOVED_ERROR);
295
+ }
296
+ }
297
+ }
298
+ const defaults = obj.defaults;
299
+ if (typeof defaults === "object" &&
300
+ defaults !== null &&
301
+ defaults.executor === "cursor") {
302
+ throw new Error(CURSOR_DAG_EXECUTOR_REMOVED_ERROR);
303
+ }
304
+ }
269
305
  return dagSpecSchema.parse(raw);
270
306
  }
271
307
  export function resolveModelForTask(task, executorModels) {
@@ -319,20 +319,17 @@ function validateSameRankWriteSetConflicts(spec, ranks, issues) {
319
319
  }
320
320
  }
321
321
  }
322
- function isWriteGuardedAgentTask(task) {
323
- return (task?.executor === "cursor" ||
324
- (task?.executor === "pi" && task.toolProfile === "write"));
322
+ function isAgentTask(task) {
323
+ return task?.executor === "pi";
325
324
  }
326
325
  function validateSameRankAgentAttributionRisks(spec, ranks, issues) {
327
326
  const taskById = new Map(spec.tasks.map((task) => [task.id, task]));
328
327
  for (const rank of ranks) {
329
- const guardedTasks = rank
330
- .map((id) => taskById.get(id))
331
- .filter(isWriteGuardedAgentTask);
332
- const hasExclusiveWriter = guardedTasks.some((task) => resolveWritePolicy(task, spec) === "exclusive");
328
+ const agentTasks = rank.map((id) => taskById.get(id)).filter(isAgentTask);
329
+ const hasExclusiveWriter = agentTasks.some((task) => resolveWritePolicy(task, spec) === "exclusive");
333
330
  if (!hasExclusiveWriter)
334
331
  continue;
335
- for (const task of guardedTasks) {
332
+ for (const task of agentTasks) {
336
333
  const policy = resolveWritePolicy(task, spec) ?? "read-only";
337
334
  if (policy === "read-only" || policy === "none") {
338
335
  issues.push({
@@ -64,8 +64,6 @@ export async function runLoopDagAction(repoRoot, taskId, options = {}) {
64
64
  if (options.maxConcurrent !== undefined) {
65
65
  runArgs.push("--max-concurrent", String(options.maxConcurrent));
66
66
  }
67
- if (options.noCursor)
68
- runArgs.push("--no-cursor");
69
67
  const runSummary = asRecord(await runner("run-dag", runArgs), "run-dag");
70
68
  runId = stringField(runSummary, "runId", "run-dag");
71
69
  const report = await runner("dag-report", [
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { repoRelativePath, toPosixPath } from "../../../shared/path-refs.js";
3
3
  import { workflowReportSchema } from "../../dynamic/artifacts.js";
4
- import { normalizePattern } from "../policy/cursor-fix-policy.js";
4
+ import { normalizePattern } from "../policy/path-patterns.js";
5
5
  export const SUMMARY_LIMIT = 1200;
6
6
  export function truncateSummary(text) {
7
7
  const normalized = text.trim();