@tea-agent/loop-agent 0.1.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 (264) hide show
  1. package/AGENTS.md +121 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +144 -0
  4. package/bin/loop-agent.js +21 -0
  5. package/dist/adapters/aimax.js +91 -0
  6. package/dist/adapters/context.js +32 -0
  7. package/dist/adapters/index.js +28 -0
  8. package/dist/adapters/loop-agent.js +98 -0
  9. package/dist/adapters/types.js +1 -0
  10. package/dist/cli/catalog.js +259 -0
  11. package/dist/cli/help.js +55 -0
  12. package/dist/cli/index.js +3 -0
  13. package/dist/cli/program.js +505 -0
  14. package/dist/cli.js +12 -0
  15. package/dist/commands/closeout.js +13 -0
  16. package/dist/commands/coverage-audit.js +14 -0
  17. package/dist/commands/cursor-prompt.js +222 -0
  18. package/dist/commands/cursor-worker.js +43 -0
  19. package/dist/commands/dag-approve.js +102 -0
  20. package/dist/commands/dag-final-verification.js +76 -0
  21. package/dist/commands/dag-init-hybrid.js +56 -0
  22. package/dist/commands/dag-reconcile-tasks.js +51 -0
  23. package/dist/commands/dag-reject.js +91 -0
  24. package/dist/commands/dag-report.js +177 -0
  25. package/dist/commands/dag-resume.js +34 -0
  26. package/dist/commands/dag-run-task.js +470 -0
  27. package/dist/commands/dag-validate.js +186 -0
  28. package/dist/commands/dag-workflow-compile.js +91 -0
  29. package/dist/commands/dag-workflow-plan.js +130 -0
  30. package/dist/commands/dag-workflow-validate.js +66 -0
  31. package/dist/commands/delegate.js +132 -0
  32. package/dist/commands/docs-archive.js +5 -0
  33. package/dist/commands/docs-audit.js +5 -0
  34. package/dist/commands/doctor.js +50 -0
  35. package/dist/commands/goal.js +92 -0
  36. package/dist/commands/handoff-check.js +5 -0
  37. package/dist/commands/harvest.js +44 -0
  38. package/dist/commands/inspect.js +11 -0
  39. package/dist/commands/instructions.js +195 -0
  40. package/dist/commands/knowledge.js +64 -0
  41. package/dist/commands/loop-benchmark.js +72 -0
  42. package/dist/commands/loop.js +241 -0
  43. package/dist/commands/new-task.js +5 -0
  44. package/dist/commands/pi-prompt.js +181 -0
  45. package/dist/commands/pi-reuse-benchmark.js +153 -0
  46. package/dist/commands/plan-list.js +5 -0
  47. package/dist/commands/promote-run.js +29 -0
  48. package/dist/commands/reference-index.js +16 -0
  49. package/dist/commands/run-dag.js +184 -0
  50. package/dist/commands/spine.js +38 -0
  51. package/dist/commands/stats.js +84 -0
  52. package/dist/commands/status.js +56 -0
  53. package/dist/commands/study-init.js +192 -0
  54. package/dist/commands/workflow.js +259 -0
  55. package/dist/commands/worktree-create.js +31 -0
  56. package/dist/commands/worktree-list.js +5 -0
  57. package/dist/commands/worktree-remove.js +26 -0
  58. package/dist/cursor-worker-entry.js +8 -0
  59. package/dist/executors/config-core.js +55 -0
  60. package/dist/executors/config.js +2 -0
  61. package/dist/executors/cursor-artifacts.js +33 -0
  62. package/dist/executors/cursor-execution-log.js +81 -0
  63. package/dist/executors/cursor-executor-artifacts.js +135 -0
  64. package/dist/executors/cursor-executor.js +468 -0
  65. package/dist/executors/cursor-run.js +115 -0
  66. package/dist/executors/cursor-tool.js +94 -0
  67. package/dist/executors/cursor-worker-client.js +213 -0
  68. package/dist/executors/cursor-worker-protocol.js +18 -0
  69. package/dist/executors/cursor-worker-server.js +54 -0
  70. package/dist/executors/cursor-worker.js +3 -0
  71. package/dist/executors/cursor.js +6 -0
  72. package/dist/executors/dag-cursor-executor.js +88 -0
  73. package/dist/executors/dag-pi-executor.js +322 -0
  74. package/dist/executors/dag-static-executor.js +45 -0
  75. package/dist/executors/dag.js +4 -0
  76. package/dist/executors/index.js +8 -0
  77. package/dist/executors/model-routing.js +60 -0
  78. package/dist/executors/pi-event-serializer.js +43 -0
  79. package/dist/executors/pi-executor.js +606 -0
  80. package/dist/executors/pi-reuse-benchmark.js +316 -0
  81. package/dist/executors/pi-runtime-reuse.js +29 -0
  82. package/dist/executors/pi-sdk-executor.js +255 -0
  83. package/dist/executors/pi-sdk.js +1 -0
  84. package/dist/executors/pi.js +3 -0
  85. package/dist/executors/shell-executor.js +300 -0
  86. package/dist/executors/shell-presets.js +47 -0
  87. package/dist/executors/shell-verification.js +251 -0
  88. package/dist/executors/shell-write-guard.js +126 -0
  89. package/dist/executors/shell.js +3 -0
  90. package/dist/executors/static.js +1 -0
  91. package/dist/governance/checks.js +434 -0
  92. package/dist/governance/harness.js +9 -0
  93. package/dist/governance/index.js +3 -0
  94. package/dist/governance/manifest-types.js +128 -0
  95. package/dist/governance/manifest.js +2 -0
  96. package/dist/governance/path-guard.js +69 -0
  97. package/dist/governance/path-guards.js +2 -0
  98. package/dist/governance/profiles.js +3 -0
  99. package/dist/governance/requirement-coverage.js +425 -0
  100. package/dist/governance/skill-safety.js +135 -0
  101. package/dist/governance/spine-audit.js +152 -0
  102. package/dist/records/closeout.js +2 -0
  103. package/dist/records/harvest.js +236 -0
  104. package/dist/records/index.js +3 -0
  105. package/dist/records/one-shot-runs.js +421 -0
  106. package/dist/records/promotion.js +199 -0
  107. package/dist/shared/artifacts-core.js +88 -0
  108. package/dist/shared/artifacts.js +2 -0
  109. package/dist/shared/context-files.js +32 -0
  110. package/dist/shared/context.js +2 -0
  111. package/dist/shared/copy-dir.js +17 -0
  112. package/dist/shared/git-progress.js +165 -0
  113. package/dist/shared/index.js +5 -0
  114. package/dist/shared/logger.js +23 -0
  115. package/dist/shared/one-shot-prompt-args.js +98 -0
  116. package/dist/shared/path-refs.js +31 -0
  117. package/dist/shared/prompts.js +26 -0
  118. package/dist/shared/reference-context.js +238 -0
  119. package/dist/shared/timeout-policy.js +19 -0
  120. package/dist/shared/timeout.js +1 -0
  121. package/dist/shared/types.js +5 -0
  122. package/dist/task/config-types.js +97 -0
  123. package/dist/task/config.js +2 -0
  124. package/dist/task/delegate.js +220 -0
  125. package/dist/task/goal-audit.js +51 -0
  126. package/dist/task/goal-policy.js +8 -0
  127. package/dist/task/goal.js +3 -0
  128. package/dist/task/ids.js +1 -0
  129. package/dist/task/index.js +9 -0
  130. package/dist/task/lifecycle.js +1 -0
  131. package/dist/task/paths.js +1 -0
  132. package/dist/task/read-model.js +149 -0
  133. package/dist/task/runtime.js +699 -0
  134. package/dist/task/source-state.js +1 -0
  135. package/dist/task/state.js +55 -0
  136. package/dist/task/subagent-guidance.js +1 -0
  137. package/dist/task/workflow-state-types.js +92 -0
  138. package/dist/task/worktree-cleanup.js +140 -0
  139. package/dist/task/worktree.js +171 -0
  140. package/dist/workflows/dag/authoring.js +8 -0
  141. package/dist/workflows/dag/authority-surface.js +138 -0
  142. package/dist/workflows/dag/canvas-observer.js +474 -0
  143. package/dist/workflows/dag/decision-envelope.js +502 -0
  144. package/dist/workflows/dag/decision-evidence.js +153 -0
  145. package/dist/workflows/dag/decision-gates.js +1 -0
  146. package/dist/workflows/dag/executor-registry.js +25 -0
  147. package/dist/workflows/dag/facts.js +4 -0
  148. package/dist/workflows/dag/failure-category.js +111 -0
  149. package/dist/workflows/dag/final-verification.js +180 -0
  150. package/dist/workflows/dag/governance-constants.js +5 -0
  151. package/dist/workflows/dag/governance-profile.js +405 -0
  152. package/dist/workflows/dag/index.js +6 -0
  153. package/dist/workflows/dag/init-hybrid.js +855 -0
  154. package/dist/workflows/dag/knowledge-curator.js +162 -0
  155. package/dist/workflows/dag/lifecycle.js +484 -0
  156. package/dist/workflows/dag/prompt-source.js +88 -0
  157. package/dist/workflows/dag/prompt.js +130 -0
  158. package/dist/workflows/dag/reconcile-tasks.js +404 -0
  159. package/dist/workflows/dag/recovery-recommendation.js +226 -0
  160. package/dist/workflows/dag/repair-artifact.js +136 -0
  161. package/dist/workflows/dag/report.js +1019 -0
  162. package/dist/workflows/dag/runner.js +1677 -0
  163. package/dist/workflows/dag/runtime.js +5 -0
  164. package/dist/workflows/dag/skill-instructions.js +471 -0
  165. package/dist/workflows/dag/skills.js +41 -0
  166. package/dist/workflows/dag/spec.js +3 -0
  167. package/dist/workflows/dag/topo.js +30 -0
  168. package/dist/workflows/dag/types.js +275 -0
  169. package/dist/workflows/dag/upstream-artifacts.js +95 -0
  170. package/dist/workflows/dag/validate.js +527 -0
  171. package/dist/workflows/dynamic/artifacts.js +65 -0
  172. package/dist/workflows/dynamic/compile.js +360 -0
  173. package/dist/workflows/dynamic/compileTypes.js +1 -0
  174. package/dist/workflows/dynamic/errors.js +5 -0
  175. package/dist/workflows/dynamic/index.js +7 -0
  176. package/dist/workflows/dynamic/profiles.js +156 -0
  177. package/dist/workflows/dynamic/spec.js +114 -0
  178. package/dist/workflows/dynamic/validate.js +275 -0
  179. package/dist/workflows/loop/actions.js +1334 -0
  180. package/dist/workflows/loop/benchmark.js +510 -0
  181. package/dist/workflows/loop/closeout.js +134 -0
  182. package/dist/workflows/loop/context.js +48 -0
  183. package/dist/workflows/loop/events.js +25 -0
  184. package/dist/workflows/loop/hash.js +32 -0
  185. package/dist/workflows/loop/index.js +8 -0
  186. package/dist/workflows/loop/paths.js +17 -0
  187. package/dist/workflows/loop/rounds.js +81 -0
  188. package/dist/workflows/loop/signals.js +55 -0
  189. package/dist/workflows/loop/state.js +116 -0
  190. package/dist/workflows/loop/templates.js +54 -0
  191. package/dist/workflows/loop/types.js +28 -0
  192. package/docs/README.md +62 -0
  193. package/docs/agent-dag-recovery-playbook.md +158 -0
  194. package/docs/agent-dag-runner.md +40 -0
  195. package/docs/cursor-executor-usage.md +25 -0
  196. package/docs/decisions/README.md +3 -0
  197. package/docs/design/README.md +36 -0
  198. package/docs/development-principles.md +71 -0
  199. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
  200. package/docs/exec-plans/README.md +6 -0
  201. package/docs/exec-plans/active/README.md +5 -0
  202. package/docs/exec-plans/completed/README.md +5 -0
  203. package/docs/feature-workflow.md +184 -0
  204. package/docs/harness-methodology-debugging.md +153 -0
  205. package/docs/harness-methodology-tdd.md +130 -0
  206. package/docs/harness-methodology-verification.md +27 -0
  207. package/docs/loop-agent-harness.md +42 -0
  208. package/docs/progress/README.md +3 -0
  209. package/docs/reports/README.md +3 -0
  210. package/docs/templates/adr.md +60 -0
  211. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
  212. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
  213. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
  214. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
  215. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
  216. package/docs/templates/agent-dag-report.schema.json +423 -0
  217. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
  218. package/docs/templates/agent-dag.base.json +195 -0
  219. package/docs/templates/agent-dag.final-verification.json +190 -0
  220. package/docs/templates/agent-dag.schema.json +316 -0
  221. package/docs/templates/agent-dag.supervised-implementation.json +500 -0
  222. package/docs/templates/exec-plan.md +64 -0
  223. package/docs/templates/feature-spec.md +53 -0
  224. package/docs/templates/hybrid-dag.json +193 -0
  225. package/docs/templates/progress-log.md +17 -0
  226. package/docs/templates/project-start-checklist.md +9 -0
  227. package/docs/templates/qa-report.md +42 -0
  228. package/docs/templates/sprint-contract.md +29 -0
  229. package/docs/verification-matrix.md +30 -0
  230. package/examples/decision-gate-agent-dag.json +123 -0
  231. package/examples/example-dag.json +51 -0
  232. package/examples/hybrid-loop-agent-dag.json +194 -0
  233. package/harness.json +92 -0
  234. package/package.json +61 -0
  235. package/skills/ai-engineering-context/SKILL.md +48 -0
  236. package/skills/loop-agent/SKILL.md +260 -0
  237. package/skills/loop-agent/references/README.md +63 -0
  238. package/skills/loop-agent/references/command-reference.md +315 -0
  239. package/skills/loop-agent/references/harness-policy.md +258 -0
  240. package/skills/loop-agent/references/hybrid-dag.md +216 -0
  241. package/skills/loop-agent/references/learned/README.md +21 -0
  242. package/skills/loop-agent/references/model-routing.md +36 -0
  243. package/skills/loop-agent/references/multi-worktree.md +54 -0
  244. package/skills/loop-agent/references/one-shot-runs.md +85 -0
  245. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
  246. package/skills/loop-agent/references/pi-prompt.md +23 -0
  247. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
  248. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
  249. package/skills/loop-agent/references/task-workflow.md +84 -0
  250. package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
  251. package/skills/requesting-code-review/SKILL.md +101 -0
  252. package/skills/requesting-code-review/code-reviewer.md +168 -0
  253. package/skills/systematic-debugging/CREATION-LOG.md +119 -0
  254. package/skills/systematic-debugging/SKILL.md +296 -0
  255. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  256. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  257. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  258. package/skills/systematic-debugging/find-polluter.sh +63 -0
  259. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  260. package/skills/systematic-debugging/test-academic.md +14 -0
  261. package/skills/systematic-debugging/test-pressure-1.md +58 -0
  262. package/skills/systematic-debugging/test-pressure-2.md +68 -0
  263. package/skills/systematic-debugging/test-pressure-3.md +69 -0
  264. package/skills/verification-before-completion/SKILL.md +154 -0
@@ -0,0 +1,527 @@
1
+ import { DEFAULT_DAG_EXECUTOR_MODELS, ENV_VAR_NAME_PATTERN, } from "./types.js";
2
+ import { resolveShellCommands } from "../../executors/shell-executor.js";
3
+ import { pathMatchesPattern } from "../../shared/git-progress.js";
4
+ import { topoSortToRanks } from "./topo.js";
5
+ const GOVERNANCE_WARNING_TYPES = new Set([
6
+ "read-only-missing-artifacts-forbidden",
7
+ "read-only-prompt-mentions-artifact-writes",
8
+ "shell-dag-active-check-repo-without-env",
9
+ "shell-verdict-gate-summary-md-source",
10
+ "shell-verdict-gate-grep-pattern",
11
+ "shell-verdict-gate-multi-command-state",
12
+ "shell-verdict-gate-handwritten-inline",
13
+ ]);
14
+ const ROOT_ARTIFACT_PATH_PROBES = [
15
+ "artifacts/修改记录.md",
16
+ "artifacts/验证结果.md",
17
+ "artifacts/_probe_.md",
18
+ ];
19
+ const ARTIFACT_WRITE_SIGNAL_PATTERNS = [
20
+ "artifacts/",
21
+ "修改记录.md",
22
+ "验证结果.md",
23
+ ];
24
+ function isGovernanceWarning(issue) {
25
+ return GOVERNANCE_WARNING_TYPES.has(issue.type);
26
+ }
27
+ export function collectGovernanceWarnings(issues) {
28
+ return issues.filter(isGovernanceWarning);
29
+ }
30
+ function collectBlockingIssues(issues, options = {}) {
31
+ if (options.strictGovernance) {
32
+ return [...issues];
33
+ }
34
+ return issues.filter((issue) => !isGovernanceWarning(issue));
35
+ }
36
+ function normalizePath(value) {
37
+ return value.replace(/\\/g, "/").replace(/^\.\//, "");
38
+ }
39
+ function probePathsForWriteSetEntry(entry) {
40
+ const normalized = normalizePath(entry);
41
+ if (!normalized.includes("*")) {
42
+ return [normalized];
43
+ }
44
+ const concrete = normalized
45
+ .replace(/\/\*\*$/, "/_probe_")
46
+ .replace(/\*\*$/, "_probe_")
47
+ .replace(/\*/g, "_");
48
+ return Array.from(new Set([normalized, concrete]));
49
+ }
50
+ function writeSetEntryWithinAllowed(entry, allowedPaths) {
51
+ const probes = probePathsForWriteSetEntry(entry);
52
+ return probes.some((probe) => allowedPaths.some((allowed) => pathMatchesPattern(probe, allowed)));
53
+ }
54
+ function writeSetEntryHitsForbidden(entry, forbiddenPaths) {
55
+ const probes = probePathsForWriteSetEntry(entry);
56
+ return probes.some((probe) => forbiddenPaths.some((forbidden) => pathMatchesPattern(probe, forbidden)));
57
+ }
58
+ function forbiddenPathsExcludeRootArtifacts(forbiddenPaths) {
59
+ return ROOT_ARTIFACT_PATH_PROBES.some((probe) => forbiddenPaths.some((forbidden) => pathMatchesPattern(probe, forbidden)));
60
+ }
61
+ function promptMentionsRootArtifactWrite(prompt) {
62
+ const lower = prompt.toLowerCase();
63
+ for (const signal of ARTIFACT_WRITE_SIGNAL_PATTERNS) {
64
+ const normalizedSignal = signal.toLowerCase();
65
+ const index = lower.indexOf(normalizedSignal);
66
+ if (index === -1)
67
+ continue;
68
+ const context = lower.slice(Math.max(0, index - 48), index + 96);
69
+ const negated = /(do not|don't|must not|should not|禁止|不得|不要|不应).{0,48}(write|edit|modify|create|update|artifacts|修改记录|验证结果)/i.test(context) ||
70
+ /不(改|写|更新|创建|编辑).{0,48}(artifacts|修改记录|验证结果)/i.test(context);
71
+ if (negated)
72
+ continue;
73
+ const hasWriteVerb = /(write|update|create|edit|modify|save|append|record|output).{0,64}(artifacts|修改记录|验证结果)/i.test(context) ||
74
+ /(artifacts|修改记录|验证结果).{0,64}(write|update|create|edit|modify|save|append|record|output)/i.test(context) ||
75
+ /(写入|写|更新|修改|保存|追加|记录|输出).{0,64}(artifacts|修改记录|验证结果)/i.test(context) ||
76
+ /(artifacts|修改记录|验证结果).{0,64}(写入|写|更新|修改|保存|追加|记录|输出)/i.test(context);
77
+ if (hasWriteVerb) {
78
+ return signal;
79
+ }
80
+ }
81
+ return undefined;
82
+ }
83
+ function shellCommandRunsCheckRepo(command) {
84
+ return /(^|[\s/])check-repo\.sh(\s|$)|(^|\s)bash\s+scripts\/check-repo\.sh(\s|$)/.test(command);
85
+ }
86
+ function shellCommandSetsActiveDagEnv(command) {
87
+ return /(^|[^A-Za-z0-9_])HARNESS_ALLOW_ACTIVE_DAG_RUNS=1(\s|;|&&|$)/.test(command);
88
+ }
89
+ function shellCommandUsesSummaryMdVerdictSource(command) {
90
+ return (/result\.summary\.md/.test(command) &&
91
+ (/\bVERDICT\b/i.test(command) ||
92
+ /\.harness\/dag-runs\/active/.test(command) ||
93
+ /\bgate\b/i.test(command)));
94
+ }
95
+ function shellCommandUsesGrepVerdictSearch(command) {
96
+ return (/\bgrep\b/.test(command) &&
97
+ (/\bVERDICT\b/.test(command) || /grep.*VERDICT|VERDICT.*grep/.test(command)));
98
+ }
99
+ function shellCommandReadsActiveOrCurrentRunJson(command) {
100
+ return (/\.harness\/dag-runs\/active/.test(command) ||
101
+ /\bHARNESS_DAG_RUN_DIR\b/.test(command) ||
102
+ /find\s+\.harness\/dag-runs\/active/.test(command));
103
+ }
104
+ function shellCommandUsesVerdictJsonParser(command) {
105
+ return (/assistantText\s*\?\?\s*raw\.stdout/.test(command) ||
106
+ /raw\.assistantText\s*\|\|\s*raw\.stdout/.test(command) ||
107
+ /raw\.assistantText\s*\?\?\s*raw\.stdout/.test(command) ||
108
+ /assistantText\s*\|\|\s*raw\.stdout/.test(command) ||
109
+ /assistantText\s*\|\|\s*stdout/.test(command));
110
+ }
111
+ function shellCommandLooksLikeHandWrittenVerdictGate(command) {
112
+ return (shellCommandReadsActiveOrCurrentRunJson(command) &&
113
+ (/\bVERDICT\b/i.test(command) ||
114
+ shellCommandUsesVerdictJsonParser(command)));
115
+ }
116
+ function shellCommandsLookLikeMultiCommandVerdictGate(commands) {
117
+ if (commands.length <= 1)
118
+ return false;
119
+ const combined = commands.join("\n");
120
+ return (shellCommandReadsActiveOrCurrentRunJson(combined) &&
121
+ (/\bVERDICT\b/i.test(combined) ||
122
+ shellCommandUsesVerdictJsonParser(combined)));
123
+ }
124
+ function validateVerdictGateConfig(task, spec, issues) {
125
+ const verdictGate = task.shell?.verdictGate;
126
+ if (!verdictGate) {
127
+ return;
128
+ }
129
+ if (task.executor !== "shell") {
130
+ issues.push({
131
+ type: "invalid-verdict-gate-config",
132
+ message: `task ${task.id} shell.verdictGate requires executor=shell`,
133
+ });
134
+ return;
135
+ }
136
+ const upstreamIds = new Set(task.depends_on);
137
+ if (!upstreamIds.has(verdictGate.fromNodeId)) {
138
+ issues.push({
139
+ type: "invalid-verdict-gate-config",
140
+ message: `task ${task.id} shell.verdictGate.fromNodeId "${verdictGate.fromNodeId}" must appear in depends_on`,
141
+ });
142
+ }
143
+ const upstreamTask = spec.tasks.find((candidate) => candidate.id === verdictGate.fromNodeId);
144
+ if (upstreamTask && upstreamTask.executor === "shell") {
145
+ issues.push({
146
+ type: "invalid-verdict-gate-config",
147
+ message: `task ${task.id} shell.verdictGate.fromNodeId "${verdictGate.fromNodeId}" must reference a non-shell upstream verdict node`,
148
+ });
149
+ }
150
+ for (const accepted of verdictGate.accept) {
151
+ if (accepted.trim().length === 0) {
152
+ issues.push({
153
+ type: "invalid-verdict-gate-config",
154
+ message: `task ${task.id} shell.verdictGate.accept entries must be non-empty`,
155
+ });
156
+ break;
157
+ }
158
+ }
159
+ }
160
+ function validateRepairArtifactGateConfig(task, spec, issues) {
161
+ const repairArtifactGate = task.shell?.repairArtifactGate;
162
+ if (!repairArtifactGate)
163
+ return;
164
+ if (task.executor !== "shell") {
165
+ issues.push({
166
+ type: "invalid-repair-artifact-gate-config",
167
+ message: `task ${task.id} shell.repairArtifactGate requires executor=shell`,
168
+ });
169
+ return;
170
+ }
171
+ const upstreamIds = new Set(task.depends_on);
172
+ if (!upstreamIds.has(repairArtifactGate.fromNodeId)) {
173
+ issues.push({
174
+ type: "invalid-repair-artifact-gate-config",
175
+ message: `task ${task.id} shell.repairArtifactGate.fromNodeId "${repairArtifactGate.fromNodeId}" must appear in depends_on`,
176
+ });
177
+ }
178
+ const upstream = spec.tasks.find((candidate) => candidate.id === repairArtifactGate.fromNodeId);
179
+ if (!upstream)
180
+ return;
181
+ if (upstream.executor === "shell") {
182
+ issues.push({
183
+ type: "invalid-repair-artifact-gate-config",
184
+ message: `task ${task.id} shell.repairArtifactGate.fromNodeId "${repairArtifactGate.fromNodeId}" must reference a non-shell upstream artifact node`,
185
+ });
186
+ }
187
+ }
188
+ function validateShellVerdictGateGovernance(task, commands, issues) {
189
+ if (task.executor !== "shell") {
190
+ return;
191
+ }
192
+ const explicitCommands = (task.shell?.commands ?? []).filter((command) => command.trim().length > 0);
193
+ for (const command of commands) {
194
+ if (shellCommandUsesSummaryMdVerdictSource(command)) {
195
+ issues.push({
196
+ type: "shell-verdict-gate-summary-md-source",
197
+ message: `task ${task.id} shell command reads result.summary.md for verdict gating; use shell.verdictGate and canonical <fromNodeId>.json instead`,
198
+ });
199
+ }
200
+ if (shellCommandUsesGrepVerdictSearch(command)) {
201
+ issues.push({
202
+ type: "shell-verdict-gate-grep-pattern",
203
+ message: `task ${task.id} shell command uses grep-based VERDICT search; use shell.verdictGate with exact accept matching instead`,
204
+ });
205
+ }
206
+ }
207
+ if (shellCommandsLookLikeMultiCommandVerdictGate(explicitCommands)) {
208
+ issues.push({
209
+ type: "shell-verdict-gate-multi-command-state",
210
+ message: `task ${task.id} spreads verdict gate state across multiple shell.commands entries; use shell.verdictGate so the gate is a single command`,
211
+ });
212
+ }
213
+ if (!task.shell?.verdictGate &&
214
+ explicitCommands.some(shellCommandLooksLikeHandWrittenVerdictGate)) {
215
+ issues.push({
216
+ type: "shell-verdict-gate-handwritten-inline",
217
+ message: `task ${task.id} uses fragile inline verdict gate shell; prefer shell.verdictGate with fromNodeId and accept`,
218
+ });
219
+ }
220
+ }
221
+ function writeSetEntriesOverlap(a, b) {
222
+ if (pathMatchesPattern(a, b) || pathMatchesPattern(b, a)) {
223
+ return true;
224
+ }
225
+ const probesA = probePathsForWriteSetEntry(a);
226
+ const probesB = probePathsForWriteSetEntry(b);
227
+ return probesA.some((probeA) => probesB.some((probeB) => probeA === probeB ||
228
+ pathMatchesPattern(probeA, b) ||
229
+ pathMatchesPattern(probeB, a)));
230
+ }
231
+ function shouldValidateWritePolicy(spec, task) {
232
+ return (spec.version === 2 ||
233
+ task.writePolicy !== undefined ||
234
+ task.writeSet !== undefined);
235
+ }
236
+ function resolveWritePolicy(task, spec) {
237
+ return task.writePolicy ?? spec.defaults?.writePolicy;
238
+ }
239
+ function validateTaskWritePolicy(task, spec, issues) {
240
+ if (!shouldValidateWritePolicy(spec, task)) {
241
+ return;
242
+ }
243
+ const writePolicy = resolveWritePolicy(task, spec);
244
+ const writeSet = task.writeSet ?? [];
245
+ const allowedPaths = task.allowedPaths ?? [];
246
+ const forbiddenPaths = task.forbiddenPaths ?? [];
247
+ if (writePolicy === "read-only") {
248
+ if (!forbiddenPathsExcludeRootArtifacts(forbiddenPaths)) {
249
+ issues.push({
250
+ type: "read-only-missing-artifacts-forbidden",
251
+ message: `task ${task.id} is read-only but forbiddenPaths does not exclude artifacts/**; root artifacts are not per-node scratchpads`,
252
+ });
253
+ }
254
+ const artifactWriteSignal = promptMentionsRootArtifactWrite(task.subtask_prompt);
255
+ if (artifactWriteSignal) {
256
+ issues.push({
257
+ type: "read-only-prompt-mentions-artifact-writes",
258
+ message: `task ${task.id} is read-only but subtask_prompt mentions root artifact write target "${artifactWriteSignal}"; return findings in node output instead`,
259
+ });
260
+ }
261
+ }
262
+ if (writePolicy === "exclusive" && writeSet.length === 0) {
263
+ issues.push({
264
+ type: "missing-write-set",
265
+ message: `task ${task.id} (exclusive) requires a non-empty writeSet`,
266
+ });
267
+ return;
268
+ }
269
+ if (writeSet.length === 0) {
270
+ return;
271
+ }
272
+ for (const entry of writeSet) {
273
+ if (forbiddenPaths.length > 0 &&
274
+ writeSetEntryHitsForbidden(entry, forbiddenPaths)) {
275
+ issues.push({
276
+ type: "write-set-forbidden",
277
+ message: `task ${task.id} writeSet entry "${entry}" matches forbiddenPaths`,
278
+ });
279
+ continue;
280
+ }
281
+ if (allowedPaths.length > 0 &&
282
+ !writeSetEntryWithinAllowed(entry, allowedPaths)) {
283
+ issues.push({
284
+ type: "write-set-out-of-scope",
285
+ message: `task ${task.id} writeSet entry "${entry}" is outside allowedPaths`,
286
+ });
287
+ }
288
+ }
289
+ }
290
+ function validateSameRankWriteSetConflicts(spec, ranks, issues) {
291
+ const taskById = new Map(spec.tasks.map((task) => [task.id, task]));
292
+ for (const rank of ranks) {
293
+ const exclusiveWriters = rank
294
+ .map((id) => taskById.get(id))
295
+ .filter((task) => {
296
+ if (!task)
297
+ return false;
298
+ if (!shouldValidateWritePolicy(spec, task))
299
+ return false;
300
+ return resolveWritePolicy(task, spec) === "exclusive";
301
+ });
302
+ for (let i = 0; i < exclusiveWriters.length; i += 1) {
303
+ for (let j = i + 1; j < exclusiveWriters.length; j += 1) {
304
+ const left = exclusiveWriters[i];
305
+ const right = exclusiveWriters[j];
306
+ const leftSet = left.writeSet ?? [];
307
+ const rightSet = right.writeSet ?? [];
308
+ for (const leftEntry of leftSet) {
309
+ for (const rightEntry of rightSet) {
310
+ if (writeSetEntriesOverlap(leftEntry, rightEntry)) {
311
+ issues.push({
312
+ type: "write-set-overlap",
313
+ message: `tasks ${left.id} and ${right.id} have overlapping writeSet entries at same rank: "${leftEntry}" and "${rightEntry}"`,
314
+ });
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+ function isWriteGuardedAgentTask(task) {
323
+ return (task?.executor === "cursor" ||
324
+ (task?.executor === "pi" && task.toolProfile === "write"));
325
+ }
326
+ function validateSameRankAgentAttributionRisks(spec, ranks, issues) {
327
+ const taskById = new Map(spec.tasks.map((task) => [task.id, task]));
328
+ 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");
333
+ if (!hasExclusiveWriter)
334
+ continue;
335
+ for (const task of guardedTasks) {
336
+ const policy = resolveWritePolicy(task, spec) ?? "read-only";
337
+ if (policy === "read-only" || policy === "none") {
338
+ issues.push({
339
+ type: "same-rank-writer-attribution-risk",
340
+ message: `same rank mixes read-only ${task.executor} task ${task.id} with a write-capable agent; add depends_on or run serially to avoid write attribution risk`,
341
+ });
342
+ }
343
+ }
344
+ }
345
+ }
346
+ function validateShellTaskConfig(task, spec, issues) {
347
+ if (task.executor !== "shell") {
348
+ return;
349
+ }
350
+ const shell = task.shell;
351
+ if (!shell) {
352
+ issues.push({
353
+ type: "missing-shell-commands",
354
+ message: `shell task ${task.id} requires shell.preset, shell.verdictGate, and/or shell.commands`,
355
+ });
356
+ return;
357
+ }
358
+ const commands = resolveShellCommands(shell);
359
+ if (commands.length === 0) {
360
+ issues.push({
361
+ type: "missing-shell-commands",
362
+ message: `shell task ${task.id} requires shell.preset, shell.verdictGate, and/or non-empty shell.commands`,
363
+ });
364
+ }
365
+ if (commands.some((command) => command.trim().length === 0)) {
366
+ issues.push({
367
+ type: "missing-shell-commands",
368
+ message: `shell task ${task.id} requires non-empty shell command strings`,
369
+ });
370
+ }
371
+ const envAllowlist = shell.envAllowlist ?? [];
372
+ const runsRepoGovernance = commands.some(shellCommandRunsCheckRepo);
373
+ const hasActiveDagEnv = envAllowlist.includes("HARNESS_ALLOW_ACTIVE_DAG_RUNS") ||
374
+ commands.some(shellCommandSetsActiveDagEnv);
375
+ if (runsRepoGovernance && !hasActiveDagEnv) {
376
+ issues.push({
377
+ type: "shell-dag-active-check-repo-without-env",
378
+ message: `shell task ${task.id} runs check-repo.sh but does not set HARNESS_ALLOW_ACTIVE_DAG_RUNS=1; this can fail inside an active DAG`,
379
+ });
380
+ }
381
+ for (const envName of envAllowlist) {
382
+ if (!ENV_VAR_NAME_PATTERN.test(envName)) {
383
+ issues.push({
384
+ type: "invalid-shell-env",
385
+ message: `shell task ${task.id} envAllowlist entry "${envName}" is not a valid env var name`,
386
+ });
387
+ }
388
+ }
389
+ const timeoutMs = shell.timeoutMs;
390
+ if (timeoutMs !== undefined && !Number.isInteger(timeoutMs)) {
391
+ issues.push({
392
+ type: "invalid-shell-timeout",
393
+ message: `shell task ${task.id} timeoutMs must be an integer`,
394
+ });
395
+ }
396
+ const cwd = shell.cwd;
397
+ if (cwd &&
398
+ (cwd.startsWith("/") || normalizePath(cwd).split("/").includes(".."))) {
399
+ issues.push({
400
+ type: "shell-cwd-out-of-scope",
401
+ message: `shell task ${task.id} cwd must be relative to the DAG cwd and stay within it`,
402
+ });
403
+ }
404
+ validateVerdictGateConfig(task, spec, issues);
405
+ validateRepairArtifactGateConfig(task, spec, issues);
406
+ validateShellVerdictGateGovernance(task, commands, issues);
407
+ }
408
+ function validateStaticTaskConfig(task, issues) {
409
+ if (task.executor !== "static") {
410
+ return;
411
+ }
412
+ const staticConfig = task.static;
413
+ if (!staticConfig?.resultMarkdown?.trim()) {
414
+ issues.push({
415
+ type: "missing-static-config",
416
+ message: `static task ${task.id} requires static.resultMarkdown`,
417
+ });
418
+ }
419
+ }
420
+ function validateDecisionGateTaskConfig(task, issues) {
421
+ if (!task.decisionGate?.enabled) {
422
+ return;
423
+ }
424
+ if (task.executor !== "pi") {
425
+ issues.push({
426
+ type: "invalid-decision-gate-executor",
427
+ message: `task ${task.id} decisionGate requires executor=pi`,
428
+ });
429
+ }
430
+ }
431
+ export function collectExecutorModelWarnings(spec) {
432
+ const warnings = [];
433
+ const executors = Object.keys(DEFAULT_DAG_EXECUTOR_MODELS);
434
+ for (const executor of executors) {
435
+ const actualByComplexity = spec.executorModels?.[executor];
436
+ if (!actualByComplexity) {
437
+ continue;
438
+ }
439
+ for (const [complexityKey, actual] of Object.entries(actualByComplexity)) {
440
+ if (actual === undefined) {
441
+ continue;
442
+ }
443
+ const expected = DEFAULT_DAG_EXECUTOR_MODELS[executor][complexityKey];
444
+ if (actual !== expected) {
445
+ warnings.push({
446
+ executor,
447
+ complexity: complexityKey,
448
+ expected,
449
+ actual,
450
+ });
451
+ }
452
+ }
453
+ }
454
+ return warnings;
455
+ }
456
+ export function collectForbiddenExecutorIssues(spec, forbiddenExecutors) {
457
+ if (forbiddenExecutors.length === 0)
458
+ return [];
459
+ const forbidden = new Set(forbiddenExecutors);
460
+ const rawTasks = spec.tasks ?? [];
461
+ return spec.tasks
462
+ .filter((task) => forbidden.has(task.executor))
463
+ .map((task, index) => ({
464
+ executor: task.executor,
465
+ taskId: task.id,
466
+ implicitDefault: rawTasks[index]?.executor === undefined,
467
+ }));
468
+ }
469
+ export function validateDagSpec(spec) {
470
+ const issues = [];
471
+ if (spec.tasks.length === 0) {
472
+ issues.push({
473
+ type: "empty-tasks",
474
+ message: "DAG must contain at least one task",
475
+ });
476
+ return issues;
477
+ }
478
+ const ids = new Set();
479
+ for (const task of spec.tasks) {
480
+ if (ids.has(task.id)) {
481
+ issues.push({
482
+ type: "duplicate-id",
483
+ message: `duplicate task id: ${task.id}`,
484
+ });
485
+ }
486
+ ids.add(task.id);
487
+ }
488
+ for (const task of spec.tasks) {
489
+ for (const dep of task.depends_on) {
490
+ if (dep === task.id) {
491
+ issues.push({
492
+ type: "self-dependency",
493
+ message: `task ${task.id} depends on itself`,
494
+ });
495
+ }
496
+ if (!ids.has(dep)) {
497
+ issues.push({
498
+ type: "missing-dependency",
499
+ message: `task ${task.id} depends on unknown id: ${dep}`,
500
+ });
501
+ }
502
+ }
503
+ }
504
+ const { hasCycle, cycleNodes, ranks } = topoSortToRanks(spec);
505
+ if (hasCycle) {
506
+ issues.push({
507
+ type: "cycle",
508
+ message: `cycle detected involving: ${cycleNodes.join(", ")}`,
509
+ });
510
+ return issues;
511
+ }
512
+ for (const task of spec.tasks) {
513
+ validateTaskWritePolicy(task, spec, issues);
514
+ validateShellTaskConfig(task, spec, issues);
515
+ validateStaticTaskConfig(task, issues);
516
+ validateDecisionGateTaskConfig(task, issues);
517
+ }
518
+ validateSameRankWriteSetConflicts(spec, ranks, issues);
519
+ validateSameRankAgentAttributionRisks(spec, ranks, issues);
520
+ return issues;
521
+ }
522
+ export function assertValidDagSpec(spec, options = {}) {
523
+ const issues = collectBlockingIssues(options.issues ?? validateDagSpec(spec), options);
524
+ if (issues.length > 0) {
525
+ throw new Error(`invalid DAG: ${issues.map((issue) => issue.message).join("; ")}`);
526
+ }
527
+ }
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+ export const evidenceRefSchema = z.object({
3
+ path: z.string().min(1),
4
+ kind: z.string().optional(),
5
+ status: z.string().optional(),
6
+ summary: z.string().min(1),
7
+ });
8
+ export const findingSchema = z.object({
9
+ id: z.string().min(1),
10
+ sourceNodeId: z.string().min(1),
11
+ targetRef: z.string().min(1),
12
+ title: z.string().min(1),
13
+ severity: z.enum(["low", "medium", "high", "critical"]),
14
+ evidence: z.array(evidenceRefSchema).default([]),
15
+ recommendation: z.string().min(1),
16
+ });
17
+ export const verificationSchema = z.object({
18
+ findingId: z.string().min(1),
19
+ verifierNodeId: z.string().min(1),
20
+ status: z.enum(["verified", "refuted", "uncertain"]),
21
+ reason: z.string().min(1),
22
+ evidence: z.array(evidenceRefSchema).default([]),
23
+ });
24
+ export const workflowReportSchema = z.object({
25
+ summary: z.string().min(1),
26
+ verifiedFindings: z.array(findingSchema),
27
+ uncertainFindings: z.array(findingSchema),
28
+ refutedFindingsRef: z.string().optional(),
29
+ evidenceRefs: z.array(evidenceRefSchema).default([]),
30
+ });
31
+ export function buildVerifiedFindingsReport(input) {
32
+ const findingsById = new Map(input.findings.map((finding) => [finding.id, finding]));
33
+ const verifiedFindings = [];
34
+ const uncertainFindings = [];
35
+ const refutedFindings = [];
36
+ const evidenceRefs = [];
37
+ for (const verification of input.verifications) {
38
+ const finding = findingsById.get(verification.findingId);
39
+ if (!finding)
40
+ continue;
41
+ if (verification.verifierNodeId === finding.sourceNodeId) {
42
+ uncertainFindings.push(finding);
43
+ evidenceRefs.push(...verification.evidence);
44
+ continue;
45
+ }
46
+ if (verification.status === "verified") {
47
+ verifiedFindings.push(finding);
48
+ }
49
+ else if (verification.status === "uncertain") {
50
+ uncertainFindings.push(finding);
51
+ }
52
+ else {
53
+ refutedFindings.push(finding);
54
+ }
55
+ evidenceRefs.push(...verification.evidence);
56
+ }
57
+ const report = workflowReportSchema.parse({
58
+ summary: `${verifiedFindings.length} verified, ${uncertainFindings.length} uncertain, ${refutedFindings.length} refuted finding(s).`,
59
+ verifiedFindings,
60
+ uncertainFindings,
61
+ refutedFindingsRef: refutedFindings.length > 0 ? input.refutedFindingsRef : undefined,
62
+ evidenceRefs,
63
+ });
64
+ return { report, refutedFindings };
65
+ }