@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.
- package/CHANGELOG.md +1056 -1020
- package/README.md +8 -3
- package/bin/loop-agent.js +21 -21
- package/dist/application/dag/generate-task-dag.js +33 -0
- package/dist/cli/command-definitions.js +25 -10
- package/dist/cli/help.js +4 -3
- package/dist/cli/program.js +43 -17
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/import-prd.js +7 -2
- package/dist/commands/init.js +7 -5
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/commands/task-source-prepare.js +474 -0
- package/dist/executors/dag-pi-executor.js +40 -5
- package/dist/executors/shell-executor.js +111 -0
- package/dist/executors/shell-presets.js +12 -4
- package/dist/executors/shell-write-guard.js +161 -25
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/task/config-types.js +6 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/project.js +8 -0
- package/dist/task/contract/schema.js +1 -0
- package/dist/task/frontend-preflight.js +131 -0
- package/dist/task/runtime.js +2 -4
- package/dist/task/source-prepare/build-draft.js +224 -0
- package/dist/task/source-prepare/completeness.js +195 -0
- package/dist/task/source-prepare/index.js +7 -0
- package/dist/task/source-prepare/parse-intent.js +373 -0
- package/dist/task/source-prepare/path-policy.js +197 -0
- package/dist/task/source-prepare/prepare.js +506 -0
- package/dist/task/source-prepare/reference-integrity.js +274 -0
- package/dist/task/source-prepare/types.js +7 -0
- package/dist/worker/observability/read-model.js +134 -0
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-layout.d.ts +31 -31
- package/dist/worker/observe/static/dag-layout.js +83 -83
- package/dist/worker/observe/static/dom.js +220 -220
- package/dist/worker/observe/static/relations.js +133 -133
- package/dist/worker/observe/static/router.js +93 -93
- package/dist/worker/observe/static/run-processing.js +148 -148
- package/dist/worker/observe/static/state.js +61 -0
- package/dist/worker/observe/static/styles.css +8 -0
- package/dist/worker/observe/static/views/batch.js +227 -227
- package/dist/worker/observe/static/views/dag-graph.js +248 -172
- package/dist/worker/observe/static/views/dag-inspector.js +374 -157
- package/dist/worker/observe/static/views/dag.js +4 -11
- package/dist/worker/observe/static/views/failures.js +143 -143
- package/dist/worker/observe/static/views/feature.js +492 -492
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/workflows/dag/backend-test-pytest-collection.js +277 -0
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/convergence/controller.js +110 -21
- package/dist/workflows/dag/frontend-implementation-contract.js +218 -17
- package/dist/workflows/dag/frontend-review-context.js +7 -1
- package/dist/workflows/dag/frontend-verification-trace.js +14 -3
- package/dist/workflows/dag/frontend-worktree-diff.js +14 -3
- package/dist/workflows/dag/init-hybrid.js +96 -34
- package/dist/workflows/dag/output-protocol.js +180 -7
- package/dist/workflows/dag/runner.js +141 -52
- package/dist/workflows/dag/types.js +4 -0
- package/dist/workflows/dag/validate.js +3 -2
- package/docs/skills/README.md +7 -7
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +473 -473
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/backend-test-dag.json +100 -8
- package/docs/templates/backend-test-result.schema.json +99 -99
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/frontend-design-contract.md +42 -42
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
- package/docs/templates/frontend-eval/metrics.md +138 -138
- package/docs/templates/frontend-eval/smoke-targets.md +53 -53
- package/docs/templates/frontend-task-constraints.md +35 -35
- package/docs/templates/frontend-task-requirement.md +70 -70
- package/docs/templates/init-evolution-review.md +35 -35
- package/docs/templates/init-managed-agents.md +10 -5
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
- package/docs/templates/knowledge-sync-dag.json +178 -178
- package/docs/templates/knowledge-sync-draft.schema.json +71 -71
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +80 -80
- package/docs/templates/worker-dogfood-setup.md +68 -68
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
- package/scripts/kb-graph-incremental-prepare.mjs +386 -386
- package/scripts/kb-graph-materialize.mjs +105 -105
- package/scripts/kb-graph-promote.mjs +164 -164
- package/scripts/kb-query.mjs +554 -554
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/analyze-product-dependencies/SKILL.md +67 -67
- package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
- package/skills/analyze-product-dependencies/references/example.md +76 -76
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
- package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
- package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
- package/skills/analyze-product-requirements/SKILL.md +90 -90
- package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
- package/skills/analyze-product-requirements/references/example.md +86 -86
- package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
- package/skills/browser-tools/browser-content.js +103 -103
- package/skills/browser-tools/browser-cookies.js +35 -35
- package/skills/browser-tools/browser-eval.js +53 -53
- package/skills/browser-tools/browser-hn-scraper.js +108 -108
- package/skills/browser-tools/browser-nav.js +44 -44
- package/skills/browser-tools/browser-pick.js +162 -162
- package/skills/browser-tools/browser-screenshot.js +34 -34
- package/skills/browser-tools/browser-start.js +86 -86
- package/skills/browser-tools/package-lock.json +2556 -2556
- package/skills/browser-tools/package.json +19 -19
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/loop-agent/SKILL.md +5 -2
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +17 -15
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/harness-policy.md +3 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +57 -57
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
- package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
- package/skills/loop-agent/references/task-workflow.md +7 -5
- package/skills/playwright-cli/SKILL.md +420 -420
- package/skills/playwright-cli/references/element-attributes.md +23 -23
- package/skills/playwright-cli/references/playwright-tests.md +39 -39
- package/skills/playwright-cli/references/request-mocking.md +87 -87
- package/skills/playwright-cli/references/running-code.md +241 -241
- package/skills/playwright-cli/references/session-management.md +225 -225
- package/skills/playwright-cli/references/storage-state.md +275 -275
- package/skills/playwright-cli/references/test-generation.md +433 -433
- package/skills/playwright-cli/references/tracing.md +139 -139
- package/skills/playwright-cli/references/video-recording.md +143 -143
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/using-git-worktrees/SKILL.md +215 -215
- package/skills/verification-before-completion/SKILL.md +154 -154
- 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
|
+
}
|
|
@@ -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;
|