@tea-agent/loop-agent 0.26.0 → 0.26.2

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 (197) hide show
  1. package/CHANGELOG.md +1056 -1020
  2. package/README.md +8 -3
  3. package/bin/loop-agent.js +21 -21
  4. package/dist/application/dag/generate-task-dag.js +33 -0
  5. package/dist/cli/command-definitions.js +25 -10
  6. package/dist/cli/help.js +4 -3
  7. package/dist/cli/program.js +43 -17
  8. package/dist/commands/cursor-prompt.js +6 -6
  9. package/dist/commands/import-prd.js +7 -2
  10. package/dist/commands/init.js +7 -5
  11. package/dist/commands/loop-benchmark.js +11 -11
  12. package/dist/commands/pi-reuse-benchmark.js +16 -16
  13. package/dist/commands/task-source-prepare.js +474 -0
  14. package/dist/executors/dag-pi-executor.js +40 -5
  15. package/dist/executors/shell-executor.js +111 -0
  16. package/dist/executors/shell-presets.js +12 -4
  17. package/dist/executors/shell-write-guard.js +161 -25
  18. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  19. package/dist/task/config-types.js +6 -0
  20. package/dist/task/contract/constants.js +1 -0
  21. package/dist/task/contract/project.js +8 -0
  22. package/dist/task/contract/schema.js +1 -0
  23. package/dist/task/frontend-preflight.js +131 -0
  24. package/dist/task/runtime.js +2 -4
  25. package/dist/task/source-prepare/build-draft.js +224 -0
  26. package/dist/task/source-prepare/completeness.js +195 -0
  27. package/dist/task/source-prepare/index.js +7 -0
  28. package/dist/task/source-prepare/parse-intent.js +373 -0
  29. package/dist/task/source-prepare/path-policy.js +197 -0
  30. package/dist/task/source-prepare/prepare.js +506 -0
  31. package/dist/task/source-prepare/reference-integrity.js +274 -0
  32. package/dist/task/source-prepare/types.js +7 -0
  33. package/dist/worker/observability/read-model.js +134 -0
  34. package/dist/worker/observe/static/copy.js +67 -67
  35. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  36. package/dist/worker/observe/static/dag-layout.js +83 -83
  37. package/dist/worker/observe/static/dom.js +220 -220
  38. package/dist/worker/observe/static/relations.js +133 -133
  39. package/dist/worker/observe/static/router.js +93 -93
  40. package/dist/worker/observe/static/run-processing.js +148 -148
  41. package/dist/worker/observe/static/state.js +61 -0
  42. package/dist/worker/observe/static/styles.css +8 -0
  43. package/dist/worker/observe/static/views/batch.js +227 -227
  44. package/dist/worker/observe/static/views/dag-graph.js +248 -172
  45. package/dist/worker/observe/static/views/dag-inspector.js +374 -157
  46. package/dist/worker/observe/static/views/dag.js +4 -11
  47. package/dist/worker/observe/static/views/failures.js +143 -143
  48. package/dist/worker/observe/static/views/feature.js +492 -492
  49. package/dist/worker/observe/static/views/run.js +453 -453
  50. package/dist/worker/observe/static/views/shell.js +7 -7
  51. package/dist/worker/observe/static/views/timeline.js +163 -163
  52. package/dist/workflows/dag/backend-test-pytest-collection.js +277 -0
  53. package/dist/workflows/dag/canvas-observer.js +275 -275
  54. package/dist/workflows/dag/convergence/controller.js +110 -21
  55. package/dist/workflows/dag/frontend-implementation-contract.js +218 -17
  56. package/dist/workflows/dag/frontend-review-context.js +7 -1
  57. package/dist/workflows/dag/frontend-verification-trace.js +14 -3
  58. package/dist/workflows/dag/frontend-worktree-diff.js +14 -3
  59. package/dist/workflows/dag/init-hybrid.js +96 -34
  60. package/dist/workflows/dag/output-protocol.js +180 -7
  61. package/dist/workflows/dag/runner.js +141 -52
  62. package/dist/workflows/dag/types.js +4 -0
  63. package/dist/workflows/dag/validate.js +3 -2
  64. package/docs/skills/README.md +7 -7
  65. package/docs/templates/adr.md +60 -60
  66. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  67. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  68. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  69. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  70. package/docs/templates/agent-dag-report.schema.json +473 -473
  71. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  72. package/docs/templates/backend-test-dag.json +100 -8
  73. package/docs/templates/backend-test-result.schema.json +99 -99
  74. package/docs/templates/feature-spec.md +53 -53
  75. package/docs/templates/frontend-design-contract.md +42 -42
  76. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
  77. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
  78. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
  79. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
  80. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
  81. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
  82. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
  83. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
  84. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
  85. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
  86. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
  87. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
  88. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
  89. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
  90. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
  91. package/docs/templates/frontend-eval/metrics.md +138 -138
  92. package/docs/templates/frontend-eval/smoke-targets.md +53 -53
  93. package/docs/templates/frontend-task-constraints.md +35 -35
  94. package/docs/templates/frontend-task-requirement.md +70 -70
  95. package/docs/templates/init-evolution-review.md +35 -35
  96. package/docs/templates/init-managed-agents.md +10 -5
  97. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  98. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  99. package/docs/templates/knowledge-sync-dag.json +178 -178
  100. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  101. package/docs/templates/product-line/closeout.yaml +9 -9
  102. package/docs/templates/product-line/design.md +13 -13
  103. package/docs/templates/product-line/links.md +10 -10
  104. package/docs/templates/product-line/requirement.md +17 -17
  105. package/docs/templates/product-line/test-plan.md +7 -7
  106. package/docs/templates/production-readiness-checklist.md +57 -57
  107. package/docs/templates/project-start-checklist.md +9 -9
  108. package/docs/templates/qa-report.md +48 -48
  109. package/docs/templates/sprint-contract.md +29 -29
  110. package/docs/templates/worker-dogfood-evidence.md +80 -80
  111. package/docs/templates/worker-dogfood-setup.md +68 -68
  112. package/package.json +1 -1
  113. package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
  114. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  115. package/scripts/kb-graph-materialize.mjs +105 -105
  116. package/scripts/kb-graph-promote.mjs +164 -164
  117. package/scripts/kb-query.mjs +554 -554
  118. package/skills/ai-engineering-context/SKILL.md +48 -48
  119. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  120. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  121. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  122. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  123. package/skills/analyze-product-dependencies/references/example.md +76 -76
  124. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  125. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  126. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  127. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  128. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  129. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  130. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  131. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  132. package/skills/analyze-product-requirements/SKILL.md +90 -90
  133. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  134. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  135. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  136. package/skills/analyze-product-requirements/references/example.md +86 -86
  137. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  138. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  139. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  140. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  141. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  142. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  143. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  144. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  145. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  146. package/skills/browser-tools/browser-content.js +103 -103
  147. package/skills/browser-tools/browser-cookies.js +35 -35
  148. package/skills/browser-tools/browser-eval.js +53 -53
  149. package/skills/browser-tools/browser-hn-scraper.js +108 -108
  150. package/skills/browser-tools/browser-nav.js +44 -44
  151. package/skills/browser-tools/browser-pick.js +162 -162
  152. package/skills/browser-tools/browser-screenshot.js +34 -34
  153. package/skills/browser-tools/browser-start.js +86 -86
  154. package/skills/browser-tools/package-lock.json +2556 -2556
  155. package/skills/browser-tools/package.json +19 -19
  156. package/skills/code-review-core/SKILL.md +20 -20
  157. package/skills/codebase-scout/SKILL.md +19 -19
  158. package/skills/grill-me/SKILL.md +10 -10
  159. package/skills/loop-agent/SKILL.md +5 -2
  160. package/skills/loop-agent/references/README.md +67 -67
  161. package/skills/loop-agent/references/command-reference.md +17 -15
  162. package/skills/loop-agent/references/docs-converge.md +126 -126
  163. package/skills/loop-agent/references/harness-policy.md +3 -4
  164. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  165. package/skills/loop-agent/references/learned/README.md +21 -21
  166. package/skills/loop-agent/references/long-running-loop.md +57 -57
  167. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  168. package/skills/loop-agent/references/pi-prompt.md +23 -23
  169. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  170. package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
  171. package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
  172. package/skills/loop-agent/references/task-workflow.md +7 -5
  173. package/skills/playwright-cli/SKILL.md +420 -420
  174. package/skills/playwright-cli/references/element-attributes.md +23 -23
  175. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  176. package/skills/playwright-cli/references/request-mocking.md +87 -87
  177. package/skills/playwright-cli/references/running-code.md +241 -241
  178. package/skills/playwright-cli/references/session-management.md +225 -225
  179. package/skills/playwright-cli/references/storage-state.md +275 -275
  180. package/skills/playwright-cli/references/test-generation.md +433 -433
  181. package/skills/playwright-cli/references/tracing.md +139 -139
  182. package/skills/playwright-cli/references/video-recording.md +143 -143
  183. package/skills/requesting-code-review/SKILL.md +101 -101
  184. package/skills/requesting-code-review/code-reviewer.md +168 -168
  185. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  186. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  187. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  188. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  189. package/skills/systematic-debugging/find-polluter.sh +63 -63
  190. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  191. package/skills/systematic-debugging/test-academic.md +14 -14
  192. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  193. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  194. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  195. package/skills/using-git-worktrees/SKILL.md +215 -215
  196. package/skills/verification-before-completion/SKILL.md +154 -154
  197. package/skills/webapp-testing/SKILL.md +19 -19
@@ -0,0 +1,274 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, open, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { getTaskPaths } from "../runtime.js";
5
+ import { readSourceManifest, } from "../source-references.js";
6
+ import { PREPARE_MAX_FILE_BYTES, PREPARE_MAX_PARSE_DOCS, PREPARE_MAX_TOTAL_BYTES, } from "./types.js";
7
+ const PARSEABLE_EXTENSIONS = new Set([".md", ".markdown", ".txt"]);
8
+ const PARSEABLE_ROLES = new Set(["requirement", "acceptance"]);
9
+ function isSafeRelativeReferencePath(materializedPath) {
10
+ const normalized = materializedPath.replace(/\\/g, "/").trim();
11
+ if (!normalized)
12
+ return false;
13
+ if (normalized.includes("\0"))
14
+ return false;
15
+ if (path.isAbsolute(normalized) || /^[a-zA-Z]:[\\/]/.test(normalized)) {
16
+ return false;
17
+ }
18
+ if (normalized.split("/").some((part) => part === ".."))
19
+ return false;
20
+ if (!normalized.startsWith("references/"))
21
+ return false;
22
+ return true;
23
+ }
24
+ async function streamSha256AndBytes(filePath, maxBytes) {
25
+ const handle = await open(filePath, "r");
26
+ try {
27
+ const hash = createHash("sha256");
28
+ const buffer = Buffer.alloc(64 * 1024);
29
+ let bytes = 0;
30
+ while (true) {
31
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
32
+ if (bytesRead === 0)
33
+ break;
34
+ bytes += bytesRead;
35
+ if (maxBytes !== undefined && bytes > maxBytes) {
36
+ throw Object.assign(new Error("INPUT_TOO_LARGE"), {
37
+ code: "INPUT_TOO_LARGE",
38
+ bytes,
39
+ });
40
+ }
41
+ hash.update(buffer.subarray(0, bytesRead));
42
+ }
43
+ return { sha256: hash.digest("hex"), bytes };
44
+ }
45
+ finally {
46
+ await handle.close();
47
+ }
48
+ }
49
+ function isParseableDoc(doc) {
50
+ const ext = path.extname(doc.materializedPath).toLowerCase();
51
+ if (!PARSEABLE_EXTENSIONS.has(ext))
52
+ return false;
53
+ return PARSEABLE_ROLES.has(doc.role);
54
+ }
55
+ /**
56
+ * Validate source-manifest + reference files before deriving a draft.
57
+ * Fail-closed on taskId/path/hash/symlink/size issues.
58
+ */
59
+ export async function validateImportedPrdReferences(input) {
60
+ const paths = getTaskPaths(input.repoRoot, input.taskId);
61
+ const issues = [];
62
+ const documents = [];
63
+ const manifest = await readSourceManifest(paths.sourceDir);
64
+ if (!manifest) {
65
+ issues.push({
66
+ code: "MANIFEST_MISSING",
67
+ level: "blocking",
68
+ message: "source-manifest.json missing; run import-prd before --use-imported-prd",
69
+ path: path.join(paths.sourceDir, "source-manifest.json"),
70
+ });
71
+ return { ok: false, issues, documents, parseableDocuments: [] };
72
+ }
73
+ if (manifest.schemaVersion !== 1) {
74
+ issues.push({
75
+ code: "MANIFEST_SCHEMA",
76
+ level: "blocking",
77
+ message: `unsupported source-manifest schemaVersion: ${String(manifest.schemaVersion)}`,
78
+ });
79
+ }
80
+ if (manifest.taskId !== input.taskId) {
81
+ issues.push({
82
+ code: "MANIFEST_TASK_MISMATCH",
83
+ level: "blocking",
84
+ message: `manifest.taskId "${manifest.taskId}" does not match task ${input.taskId}`,
85
+ });
86
+ }
87
+ if (!Array.isArray(manifest.documents)) {
88
+ issues.push({
89
+ code: "MANIFEST_INVALID",
90
+ level: "blocking",
91
+ message: "source-manifest.documents must be an array",
92
+ });
93
+ return { ok: false, issues, documents, parseableDocuments: [] };
94
+ }
95
+ // Stable order by materializedPath (import already sorts; re-sort for safety).
96
+ const ordered = [...manifest.documents].sort((a, b) => a.materializedPath < b.materializedPath
97
+ ? -1
98
+ : a.materializedPath > b.materializedPath
99
+ ? 1
100
+ : 0);
101
+ let totalParseBytes = 0;
102
+ let parseableCount = 0;
103
+ for (const doc of ordered) {
104
+ if (!doc ||
105
+ typeof doc.role !== "string" ||
106
+ typeof doc.name !== "string" ||
107
+ typeof doc.materializedPath !== "string" ||
108
+ typeof doc.sha256 !== "string" ||
109
+ typeof doc.bytes !== "number") {
110
+ issues.push({
111
+ code: "MANIFEST_DOC_INVALID",
112
+ level: "blocking",
113
+ message: "manifest document missing required fields",
114
+ });
115
+ continue;
116
+ }
117
+ if (!isSafeRelativeReferencePath(doc.materializedPath)) {
118
+ issues.push({
119
+ code: "REFERENCE_PATH_UNSAFE",
120
+ level: "blocking",
121
+ message: `reference path not contained under references/: ${doc.materializedPath}`,
122
+ path: doc.materializedPath,
123
+ });
124
+ continue;
125
+ }
126
+ const absolutePath = path.resolve(paths.sourceDir, doc.materializedPath);
127
+ const relativeToSource = path
128
+ .relative(paths.sourceDir, absolutePath)
129
+ .split(path.sep)
130
+ .join("/");
131
+ if (relativeToSource.startsWith("..") ||
132
+ path.isAbsolute(relativeToSource)) {
133
+ issues.push({
134
+ code: "REFERENCE_PATH_ESCAPE",
135
+ level: "blocking",
136
+ message: `reference path escapes source dir: ${doc.materializedPath}`,
137
+ path: doc.materializedPath,
138
+ });
139
+ continue;
140
+ }
141
+ let stats;
142
+ try {
143
+ stats = await lstat(absolutePath);
144
+ }
145
+ catch {
146
+ issues.push({
147
+ code: "REFERENCE_MISSING",
148
+ level: "blocking",
149
+ message: `reference file missing: ${doc.materializedPath}`,
150
+ path: doc.materializedPath,
151
+ });
152
+ continue;
153
+ }
154
+ if (stats.isSymbolicLink()) {
155
+ issues.push({
156
+ code: "REFERENCE_SYMLINK",
157
+ level: "blocking",
158
+ message: `reference must be a regular file (symlink rejected): ${doc.materializedPath}`,
159
+ path: doc.materializedPath,
160
+ });
161
+ continue;
162
+ }
163
+ if (!stats.isFile()) {
164
+ issues.push({
165
+ code: "REFERENCE_NOT_FILE",
166
+ level: "blocking",
167
+ message: `reference is not a regular file: ${doc.materializedPath}`,
168
+ path: doc.materializedPath,
169
+ });
170
+ continue;
171
+ }
172
+ const parseable = isParseableDoc(doc);
173
+ if (parseable) {
174
+ parseableCount += 1;
175
+ if (parseableCount > PREPARE_MAX_PARSE_DOCS) {
176
+ issues.push({
177
+ code: "INPUT_TOO_LARGE",
178
+ level: "blocking",
179
+ message: `more than ${PREPARE_MAX_PARSE_DOCS} parseable imported docs`,
180
+ });
181
+ continue;
182
+ }
183
+ }
184
+ let hashResult;
185
+ try {
186
+ hashResult = await streamSha256AndBytes(absolutePath, parseable ? PREPARE_MAX_FILE_BYTES : undefined);
187
+ }
188
+ catch (error) {
189
+ if (error &&
190
+ typeof error === "object" &&
191
+ "code" in error &&
192
+ error.code === "INPUT_TOO_LARGE") {
193
+ issues.push({
194
+ code: "INPUT_TOO_LARGE",
195
+ level: "blocking",
196
+ message: `reference exceeds ${PREPARE_MAX_FILE_BYTES} bytes: ${doc.materializedPath}`,
197
+ path: doc.materializedPath,
198
+ });
199
+ continue;
200
+ }
201
+ issues.push({
202
+ code: "REFERENCE_READ_FAILED",
203
+ level: "blocking",
204
+ message: `failed to hash reference: ${doc.materializedPath}`,
205
+ path: doc.materializedPath,
206
+ });
207
+ continue;
208
+ }
209
+ if (hashResult.bytes !== doc.bytes) {
210
+ issues.push({
211
+ code: "REFERENCE_BYTES_MISMATCH",
212
+ level: "blocking",
213
+ message: `bytes mismatch for ${doc.materializedPath}: manifest=${doc.bytes} disk=${hashResult.bytes}`,
214
+ path: doc.materializedPath,
215
+ });
216
+ }
217
+ if (hashResult.sha256 !== doc.sha256) {
218
+ issues.push({
219
+ code: "REFERENCE_HASH_MISMATCH",
220
+ level: "blocking",
221
+ message: `sha256 mismatch for ${doc.materializedPath}`,
222
+ path: doc.materializedPath,
223
+ });
224
+ }
225
+ let content;
226
+ if (parseable) {
227
+ totalParseBytes += hashResult.bytes;
228
+ if (totalParseBytes > PREPARE_MAX_TOTAL_BYTES) {
229
+ issues.push({
230
+ code: "INPUT_TOO_LARGE",
231
+ level: "blocking",
232
+ message: `total parseable reference bytes exceed ${PREPARE_MAX_TOTAL_BYTES}`,
233
+ });
234
+ }
235
+ else {
236
+ content = await readFile(absolutePath, "utf-8");
237
+ }
238
+ }
239
+ documents.push({
240
+ role: doc.role,
241
+ name: doc.name,
242
+ materializedPath: doc.materializedPath,
243
+ absolutePath,
244
+ sha256: hashResult.sha256,
245
+ bytes: hashResult.bytes,
246
+ content,
247
+ parseable,
248
+ });
249
+ }
250
+ const parseableDocuments = documents.filter((doc) => doc.parseable);
251
+ const hasRequirement = parseableDocuments.some((doc) => doc.role === "requirement");
252
+ if (input.requireParseableRequirement !== false && !hasRequirement) {
253
+ // Still allow if caller will supply supplemental flags/text; prepare.ts decides.
254
+ // Here we only mark warning-level absence; prepare elevates when needed.
255
+ issues.push({
256
+ code: "NO_PARSEABLE_REQUIREMENT",
257
+ level: "warning",
258
+ message: "no parseable requirement-role document under source/references (role=requirement, .md/.txt)",
259
+ });
260
+ }
261
+ const blocking = issues.some((issue) => issue.level === "blocking");
262
+ return {
263
+ ok: !blocking,
264
+ issues,
265
+ documents,
266
+ parseableDocuments,
267
+ };
268
+ }
269
+ export function draftReferencesFromManifest(documents) {
270
+ return documents.map((doc) => ({
271
+ role: doc.role,
272
+ ref: doc.materializedPath,
273
+ }));
274
+ }
@@ -0,0 +1,7 @@
1
+ export const PREPARE_MAX_FILE_BYTES = 2 * 1024 * 1024;
2
+ export const PREPARE_MAX_PARSE_DOCS = 8;
3
+ export const PREPARE_MAX_TOTAL_BYTES = 8 * 1024 * 1024;
4
+ export const PREPARE_DEFAULT_FORBIDDEN_PATHS = [
5
+ ".harness/**",
6
+ "node_modules/**",
7
+ ];
@@ -1287,6 +1287,12 @@ function mergeDagRun(existing, incoming) {
1287
1287
  ...(ranks && ranks.length > 0 ? { ranks } : {}),
1288
1288
  nodes: mergeDagNodes(existing.nodes, incoming.nodes),
1289
1289
  edges: incoming.edges.length > 0 ? incoming.edges : existing.edges,
1290
+ ...((incoming.controlEdges ?? existing.controlEdges)
1291
+ ? { controlEdges: incoming.controlEdges ?? existing.controlEdges }
1292
+ : {}),
1293
+ ...((incoming.convergence ?? existing.convergence)
1294
+ ? { convergence: incoming.convergence ?? existing.convergence }
1295
+ : {}),
1290
1296
  ...((incoming.dagPath ?? existing.dagPath)
1291
1297
  ? { dagPath: incoming.dagPath ?? existing.dagPath }
1292
1298
  : {}),
@@ -1321,6 +1327,10 @@ function mergeDagNode(existing, incoming) {
1321
1327
  finishedAt: incoming.finishedAt ?? existing.finishedAt,
1322
1328
  outputPreview: incoming.outputPreview ?? existing.outputPreview,
1323
1329
  errorPreview: incoming.errorPreview ?? existing.errorPreview,
1330
+ attemptCount: incoming.attemptCount ?? existing.attemptCount,
1331
+ failureCategory: incoming.failureCategory ?? existing.failureCategory,
1332
+ originKind: incoming.originKind ?? existing.originKind,
1333
+ passes: incoming.passes ?? existing.passes,
1324
1334
  };
1325
1335
  }
1326
1336
  function computeDurationMs(startedAt, finishedAt) {
@@ -1569,6 +1579,9 @@ async function parseDagStateFile(statePath, now) {
1569
1579
  const nodes = parseDagNodes(parsed, rankByNode, modelByNode);
1570
1580
  const edges = await parseDagEdges(runSpecPath, nodes);
1571
1581
  const state = parsed;
1582
+ const controlEdges = await deriveControlLoopEdges(runSpecPath, nodes, state);
1583
+ const convergence = projectConvergenceSummary(state);
1584
+ applyNodePasses(nodes, state);
1572
1585
  const liveness = assessDagRunLiveness({ state, now });
1573
1586
  const effectiveStatus = lifecycle
1574
1587
  ? deriveDagRunEffectiveStatus({
@@ -1647,6 +1660,8 @@ async function parseDagStateFile(statePath, now) {
1647
1660
  ...(ranks.length > 0 ? { ranks } : {}),
1648
1661
  nodes,
1649
1662
  edges,
1663
+ ...(controlEdges.length > 0 ? { controlEdges } : {}),
1664
+ ...(convergence ? { convergence } : {}),
1650
1665
  dagPath: runDir,
1651
1666
  ...(continuation ? { continuation } : {}),
1652
1667
  ...(backendTest ? { backendTest } : {}),
@@ -1683,6 +1698,125 @@ async function parseDagEdges(runPath, nodes) {
1683
1698
  }
1684
1699
  return [...edges.values()].sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
1685
1700
  }
1701
+ /**
1702
+ * Derive presentation-only control-loop edges from the run spec's convergence
1703
+ * chain. When the chain contains both `review-gate-shell` and `repair-pi`, the
1704
+ * bounded repair-reverify-review loop exists at runtime and Observe renders it
1705
+ * as a dashed feedback loop. The derived edge is never merged into `edges` or
1706
+ * `ranks`, so DAG topological layout stays acyclic.
1707
+ */
1708
+ async function deriveControlLoopEdges(runPath, nodes, state) {
1709
+ const runtimeConvergence = state.convergence;
1710
+ if (!runtimeConvergence?.enabled ||
1711
+ runtimeConvergence.terminalReason === "feature-flag-off" ||
1712
+ runtimeConvergence.terminalReason === "unsupported-dag-shape") {
1713
+ return [];
1714
+ }
1715
+ const parsed = await safeReadJson(runPath);
1716
+ if (!parsed)
1717
+ return [];
1718
+ const convergence = readObject(parsed, "convergence");
1719
+ if (!convergence)
1720
+ return [];
1721
+ const chainNodeIds = readStringArray(convergence, "chainNodeIds");
1722
+ if (chainNodeIds.length === 0)
1723
+ return [];
1724
+ if (!chainNodeIds.includes("review-gate-shell") ||
1725
+ !chainNodeIds.includes("repair-pi")) {
1726
+ return [];
1727
+ }
1728
+ const known = new Set(nodes.map((node) => node.nodeId));
1729
+ if (!known.has("review-gate-shell") || !known.has("repair-pi")) {
1730
+ return [];
1731
+ }
1732
+ return [{ from: "review-gate-shell", to: "repair-pi", kind: "control-loop" }];
1733
+ }
1734
+ /**
1735
+ * Project `state.convergence` into the slim Observe summary. Keeps the budget
1736
+ * envelope even when passHistory is empty.
1737
+ */
1738
+ function projectConvergenceSummary(state) {
1739
+ const convergence = state.convergence;
1740
+ if (!convergence)
1741
+ return undefined;
1742
+ return {
1743
+ enabled: convergence.enabled,
1744
+ maxPasses: convergence.maxPasses,
1745
+ currentPass: convergence.currentPass,
1746
+ ...(convergence.terminalReason
1747
+ ? { terminalReason: convergence.terminalReason }
1748
+ : {}),
1749
+ passHistory: convergence.passHistory.map((pass) => ({
1750
+ pass: pass.pass,
1751
+ ...(pass.reason ? { reason: pass.reason } : {}),
1752
+ ...(pass.status ? { status: pass.status } : {}),
1753
+ ...(pass.hardVerifyStatus
1754
+ ? { hardVerifyStatus: pass.hardVerifyStatus }
1755
+ : {}),
1756
+ ...(pass.reviewVerdict ? { reviewVerdict: pass.reviewVerdict } : {}),
1757
+ ...(pass.reviewGateStatus
1758
+ ? { reviewGateStatus: pass.reviewGateStatus }
1759
+ : {}),
1760
+ })),
1761
+ };
1762
+ }
1763
+ /**
1764
+ * Record convergence participation from archived pass artifacts plus the live
1765
+ * current pass. The controller archives only retry/terminal failures, so an
1766
+ * active pass or a successful final pass must be derived from current node
1767
+ * state to avoid dropping its pass marker from Observe.
1768
+ */
1769
+ function applyNodePasses(nodes, state) {
1770
+ const passesByNode = new Map();
1771
+ const addPass = (nodeId, pass) => {
1772
+ const passes = passesByNode.get(nodeId) ?? new Set();
1773
+ passes.add(pass);
1774
+ passesByNode.set(nodeId, passes);
1775
+ };
1776
+ for (const pass of state.convergence?.passHistory ?? []) {
1777
+ for (const ref of pass.artifactRefs)
1778
+ addPass(ref.nodeId, pass.pass);
1779
+ }
1780
+ const initialPassNodeIds = new Set([
1781
+ "initial-review-pi",
1782
+ "initial-review-gate-shell",
1783
+ "initial-review-pass-shell",
1784
+ ]);
1785
+ for (const node of nodes) {
1786
+ if (!initialPassNodeIds.has(node.nodeId))
1787
+ continue;
1788
+ const status = (node.status ?? "").toUpperCase();
1789
+ if (["RUNNING", "FINISHED", "ERROR"].includes(status)) {
1790
+ addPass(node.nodeId, 1);
1791
+ }
1792
+ }
1793
+ const currentPass = state.convergence?.currentPass;
1794
+ if (currentPass && currentPass > 0) {
1795
+ const convergenceNodeIds = new Set([
1796
+ "process-supervisor-pi",
1797
+ "process-gate-shell",
1798
+ "repair-pi",
1799
+ "hard-verify-shell",
1800
+ "review-pi",
1801
+ "review-verdict-recovery-pi",
1802
+ "review-gate-shell",
1803
+ ]);
1804
+ for (const node of nodes) {
1805
+ if (!convergenceNodeIds.has(node.nodeId))
1806
+ continue;
1807
+ const status = (node.status ?? "").toUpperCase();
1808
+ if (["RUNNING", "FINISHED", "ERROR"].includes(status)) {
1809
+ addPass(node.nodeId, currentPass);
1810
+ }
1811
+ }
1812
+ }
1813
+ for (const node of nodes) {
1814
+ const passes = passesByNode.get(node.nodeId);
1815
+ if (passes && passes.size > 0) {
1816
+ node.passes = [...passes].sort((left, right) => left - right);
1817
+ }
1818
+ }
1819
+ }
1686
1820
  function buildRankIndex(ranks) {
1687
1821
  const index = new Map();
1688
1822
  for (let i = 0; i < ranks.length; i++) {
@@ -1,67 +1,67 @@
1
- /** Copy-only recommended commands (Observe UI R5). */
2
- import { el } from "./dom.js";
3
-
4
- export async function copyText(text) {
5
- const value = String(text ?? "");
6
- if (!value) return false;
7
- try {
8
- if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
9
- await navigator.clipboard.writeText(value);
10
- return true;
11
- }
12
- } catch {
13
- // fall through
14
- }
15
- try {
16
- const ta = document.createElement("textarea");
17
- ta.value = value;
18
- ta.setAttribute("readonly", "");
19
- ta.style.position = "fixed";
20
- ta.style.left = "-9999px";
21
- document.body.appendChild(ta);
22
- ta.select();
23
- const ok = document.execCommand("copy");
24
- document.body.removeChild(ta);
25
- return ok;
26
- } catch {
27
- return false;
28
- }
29
- }
30
-
31
- /** Advisory recommended command: copy-only, never executes. */
32
- export function renderRecommendedCommand(action) {
33
- const block = el("div", "recommended-command");
34
- if (!action) {
35
- block.appendChild(el("span", "muted", "缺失"));
36
- return block;
37
- }
38
- const head = el("div", "recommended-command-head");
39
- if (action.kind) {
40
- head.appendChild(el("span", "recommended-command-kind", action.kind));
41
- }
42
- head.appendChild(
43
- el("span", "recommended-command-label", action.label ?? "—"),
44
- );
45
- block.appendChild(head);
46
- if (action.command) {
47
- const body = el("div", "recommended-command-body");
48
- const code = el("code", "recommended-command-text", action.command);
49
- body.appendChild(code);
50
- const btn = el("button", "copy-command", "复制");
51
- btn.type = "button";
52
- btn.addEventListener("click", (e) => {
53
- e.preventDefault();
54
- e.stopPropagation();
55
- void copyText(action.command).then((ok) => {
56
- btn.textContent = ok ? "已复制" : "复制失败";
57
- setTimeout(() => {
58
- btn.textContent = "复制";
59
- }, 1500);
60
- });
61
- });
62
- body.appendChild(btn);
63
- block.appendChild(body);
64
- }
65
- return block;
66
- }
67
-
1
+ /** Copy-only recommended commands (Observe UI R5). */
2
+ import { el } from "./dom.js";
3
+
4
+ export async function copyText(text) {
5
+ const value = String(text ?? "");
6
+ if (!value) return false;
7
+ try {
8
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
9
+ await navigator.clipboard.writeText(value);
10
+ return true;
11
+ }
12
+ } catch {
13
+ // fall through
14
+ }
15
+ try {
16
+ const ta = document.createElement("textarea");
17
+ ta.value = value;
18
+ ta.setAttribute("readonly", "");
19
+ ta.style.position = "fixed";
20
+ ta.style.left = "-9999px";
21
+ document.body.appendChild(ta);
22
+ ta.select();
23
+ const ok = document.execCommand("copy");
24
+ document.body.removeChild(ta);
25
+ return ok;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /** Advisory recommended command: copy-only, never executes. */
32
+ export function renderRecommendedCommand(action) {
33
+ const block = el("div", "recommended-command");
34
+ if (!action) {
35
+ block.appendChild(el("span", "muted", "缺失"));
36
+ return block;
37
+ }
38
+ const head = el("div", "recommended-command-head");
39
+ if (action.kind) {
40
+ head.appendChild(el("span", "recommended-command-kind", action.kind));
41
+ }
42
+ head.appendChild(
43
+ el("span", "recommended-command-label", action.label ?? "—"),
44
+ );
45
+ block.appendChild(head);
46
+ if (action.command) {
47
+ const body = el("div", "recommended-command-body");
48
+ const code = el("code", "recommended-command-text", action.command);
49
+ body.appendChild(code);
50
+ const btn = el("button", "copy-command", "复制");
51
+ btn.type = "button";
52
+ btn.addEventListener("click", (e) => {
53
+ e.preventDefault();
54
+ e.stopPropagation();
55
+ void copyText(action.command).then((ok) => {
56
+ btn.textContent = ok ? "已复制" : "复制失败";
57
+ setTimeout(() => {
58
+ btn.textContent = "复制";
59
+ }, 1500);
60
+ });
61
+ });
62
+ body.appendChild(btn);
63
+ block.appendChild(body);
64
+ }
65
+ return block;
66
+ }
67
+
@@ -1,31 +1,31 @@
1
- export type DagLayoutNodeInput = { nodeId: string };
2
- export type DagLayoutEdgeInput = { from: string; to: string };
3
- export type DagLayoutNode = {
4
- nodeId: string;
5
- rank: number;
6
- index: number;
7
- x: number;
8
- y: number;
9
- width: number;
10
- height: number;
11
- };
12
- export type DagLayoutEdge = DagLayoutEdgeInput & { path: string };
13
- export type DagLayout = {
14
- width: number;
15
- height: number;
16
- nodes: DagLayoutNode[];
17
- edges: DagLayoutEdge[];
18
- };
19
-
20
- export function layoutDag(
21
- nodes?: DagLayoutNodeInput[],
22
- edges?: DagLayoutEdgeInput[],
23
- ranks?: string[][],
24
- options?: Partial<{
25
- nodeWidth: number;
26
- nodeHeight: number;
27
- rankGap: number;
28
- nodeGap: number;
29
- padding: number;
30
- }>,
31
- ): DagLayout;
1
+ export type DagLayoutNodeInput = { nodeId: string };
2
+ export type DagLayoutEdgeInput = { from: string; to: string };
3
+ export type DagLayoutNode = {
4
+ nodeId: string;
5
+ rank: number;
6
+ index: number;
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
+ };
12
+ export type DagLayoutEdge = DagLayoutEdgeInput & { path: string };
13
+ export type DagLayout = {
14
+ width: number;
15
+ height: number;
16
+ nodes: DagLayoutNode[];
17
+ edges: DagLayoutEdge[];
18
+ };
19
+
20
+ export function layoutDag(
21
+ nodes?: DagLayoutNodeInput[],
22
+ edges?: DagLayoutEdgeInput[],
23
+ ranks?: string[][],
24
+ options?: Partial<{
25
+ nodeWidth: number;
26
+ nodeHeight: number;
27
+ rankGap: number;
28
+ nodeGap: number;
29
+ padding: number;
30
+ }>,
31
+ ): DagLayout;