@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
|
@@ -18,7 +18,14 @@ const DEFAULT_CONVERGENCE_CHAIN_NODE_IDS = [
|
|
|
18
18
|
*/
|
|
19
19
|
const REVIEW_GATE_NODE_ID = "review-gate-shell";
|
|
20
20
|
const REVIEW_VERDICT_NODE_ID = "review-verdict-recovery-pi";
|
|
21
|
-
const
|
|
21
|
+
const INITIAL_REVIEW_NODE_ID = "initial-review-pi";
|
|
22
|
+
const INITIAL_REVIEW_GATE_NODE_ID = "initial-review-gate-shell";
|
|
23
|
+
const INITIAL_REVIEW_PASS_NODE_ID = "initial-review-pass-shell";
|
|
24
|
+
// Automatic code repair is authorized only for an ordinary command/test
|
|
25
|
+
// failure. Unknown, protocol, validation, provider, safety, governance, and
|
|
26
|
+
// human-decision categories all fail closed instead of reaching a writer.
|
|
27
|
+
const HARD_VERIFY_REPAIR_ELIGIBLE_FAILURES = new Set(["nonzero-exit"]);
|
|
28
|
+
const REVIEW_SOURCE_NON_RETRY_FAILURES = new Set([
|
|
22
29
|
"timeout",
|
|
23
30
|
"spawn-error",
|
|
24
31
|
"write-guard",
|
|
@@ -26,9 +33,6 @@ const CONVERGENCE_NON_RETRY_FAILURES = new Set([
|
|
|
26
33
|
"auth",
|
|
27
34
|
"human-rejected",
|
|
28
35
|
"decision-gate-requires-human",
|
|
29
|
-
]);
|
|
30
|
-
const REVIEW_SOURCE_NON_RETRY_FAILURES = new Set([
|
|
31
|
-
...CONVERGENCE_NON_RETRY_FAILURES,
|
|
32
36
|
"protocol-invalid",
|
|
33
37
|
"invalid-output",
|
|
34
38
|
]);
|
|
@@ -38,26 +42,61 @@ export function shouldEnableDagConvergence(spec) {
|
|
|
38
42
|
}
|
|
39
43
|
export async function runConvergencePassController(input) {
|
|
40
44
|
const convergence = input.state.convergence;
|
|
41
|
-
if (!convergence
|
|
45
|
+
if (!convergence)
|
|
42
46
|
return { retry: false };
|
|
43
47
|
if (process.env.HARNESS_DAG_CONVERGENCE === "off") {
|
|
44
48
|
convergence.terminalReason = "feature-flag-off";
|
|
45
49
|
return { retry: false };
|
|
46
50
|
}
|
|
51
|
+
if (!convergence.enabled) {
|
|
52
|
+
return resolveNoRepairTerminal(input);
|
|
53
|
+
}
|
|
47
54
|
if (!hasConvergenceChain(input.tasksById, input.spec)) {
|
|
48
55
|
convergence.terminalReason = "unsupported-dag-shape";
|
|
49
56
|
return { retry: false };
|
|
50
57
|
}
|
|
51
58
|
const chain = getConvergenceChain(input.spec);
|
|
52
59
|
const observesReview = chain.includes(REVIEW_GATE_NODE_ID);
|
|
60
|
+
if (input.tasksById.has(INITIAL_REVIEW_NODE_ID)) {
|
|
61
|
+
const initialReview = input.state.nodes[INITIAL_REVIEW_NODE_ID];
|
|
62
|
+
const initialGate = input.state.nodes[INITIAL_REVIEW_GATE_NODE_ID];
|
|
63
|
+
const initialPass = input.state.nodes[INITIAL_REVIEW_PASS_NODE_ID];
|
|
64
|
+
if (initialReview?.status === "ERROR" || initialGate?.status === "ERROR") {
|
|
65
|
+
convergence.terminalReason = "non-retry-failure";
|
|
66
|
+
return { retry: false };
|
|
67
|
+
}
|
|
68
|
+
if (initialPass?.status === "FINISHED" &&
|
|
69
|
+
parseProcessVerdict(initialReview) === "pass") {
|
|
70
|
+
convergence.terminalReason = "review-pass";
|
|
71
|
+
return { retry: false };
|
|
72
|
+
}
|
|
73
|
+
const repair = input.state.nodes["repair-pi"];
|
|
74
|
+
if (convergence.currentPass === 1 &&
|
|
75
|
+
parseProcessVerdict(initialReview) === "request-revision" &&
|
|
76
|
+
(repair?.status === "FINISHED" || repair?.status === "ERROR")) {
|
|
77
|
+
// Pass 1 is the initial review. The first executed repair chain is pass 2,
|
|
78
|
+
// even though the scheduler completes it before the controller first runs.
|
|
79
|
+
convergence.currentPass = 2;
|
|
80
|
+
}
|
|
81
|
+
if (repair?.status === "ERROR") {
|
|
82
|
+
convergence.terminalReason = "non-retry-failure";
|
|
83
|
+
return { retry: false };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
53
86
|
const hardVerify = input.state.nodes["hard-verify-shell"];
|
|
54
87
|
if (!hardVerify)
|
|
55
88
|
return { retry: false };
|
|
56
89
|
if (hardVerify.status === "ERROR") {
|
|
57
90
|
return handleHardVerifyFailure(input, hardVerify);
|
|
58
91
|
}
|
|
59
|
-
if (hardVerify.status !== "FINISHED")
|
|
92
|
+
if (hardVerify.status !== "FINISHED") {
|
|
93
|
+
// The scheduler has already settled every ordinary rank before invoking
|
|
94
|
+
// the controller. PENDING/SKIPPED here therefore means an earlier failure
|
|
95
|
+
// blocked the convergence chain; record a terminal reason so the deferred
|
|
96
|
+
// failure-aware closeout remains reachable.
|
|
97
|
+
convergence.terminalReason = "non-retry-failure";
|
|
60
98
|
return { retry: false };
|
|
99
|
+
}
|
|
61
100
|
// Hard verification passed. When the chain observes a review gate, success
|
|
62
101
|
// is gated on the review verdict: a legitimate `request-revision` re-enters
|
|
63
102
|
// the same bounded repair-reverify-review loop (AC3/AC4). Non-supervised
|
|
@@ -74,6 +113,13 @@ export async function runConvergencePassController(input) {
|
|
|
74
113
|
const reviewGate = input.state.nodes[REVIEW_GATE_NODE_ID];
|
|
75
114
|
if (!reviewGate)
|
|
76
115
|
return { retry: false };
|
|
116
|
+
// Well-formed request-revision is a convergence routing result: the review
|
|
117
|
+
// gate finishes successfully (routingAccept) rather than shell ERROR.
|
|
118
|
+
// Still re-enter the bounded repair chain when revision evidence is valid.
|
|
119
|
+
if (reviewGate.status === "FINISHED" &&
|
|
120
|
+
isLegitimateReviewRequestRevision(input.state)) {
|
|
121
|
+
return handleReviewRequestRevision(input, reviewGate);
|
|
122
|
+
}
|
|
77
123
|
if (reviewGate.status === "FINISHED") {
|
|
78
124
|
convergence.terminalReason = "review-pass";
|
|
79
125
|
await appendConvergenceKnowledgePattern({
|
|
@@ -82,10 +128,50 @@ export async function runConvergencePassController(input) {
|
|
|
82
128
|
});
|
|
83
129
|
return { retry: false };
|
|
84
130
|
}
|
|
131
|
+
// Compatibility: gates without routingAccept still ERROR on revision.
|
|
85
132
|
if (reviewGate.status === "ERROR") {
|
|
86
133
|
return handleReviewRequestRevision(input, reviewGate);
|
|
87
134
|
}
|
|
88
|
-
//
|
|
135
|
+
// The scheduler pass is settled; an unexecuted gate is a terminal upstream
|
|
136
|
+
// blockage, not a reason to leave closeout permanently pending.
|
|
137
|
+
convergence.terminalReason = "non-retry-failure";
|
|
138
|
+
return { retry: false };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* `maxFixLoops=0` disables writer re-entry, not terminal adjudication. Review
|
|
142
|
+
* and hard-verify evidence still decide whether closeout is a truthful success
|
|
143
|
+
* or a bounded failure. Existing handlers are safe here because maxPasses=1,
|
|
144
|
+
* so they record the terminal pass without resetting any writer nodes.
|
|
145
|
+
*/
|
|
146
|
+
async function resolveNoRepairTerminal(input) {
|
|
147
|
+
const convergence = input.state.convergence;
|
|
148
|
+
const reviewGate = input.state.nodes[REVIEW_GATE_NODE_ID];
|
|
149
|
+
if (reviewGate) {
|
|
150
|
+
if (reviewGate.status === "FINISHED" &&
|
|
151
|
+
isLegitimateReviewRequestRevision(input.state)) {
|
|
152
|
+
return handleReviewRequestRevision(input, reviewGate);
|
|
153
|
+
}
|
|
154
|
+
if (reviewGate.status === "FINISHED") {
|
|
155
|
+
convergence.terminalReason = "review-pass";
|
|
156
|
+
return { retry: false };
|
|
157
|
+
}
|
|
158
|
+
if (reviewGate.status === "ERROR") {
|
|
159
|
+
return handleReviewRequestRevision(input, reviewGate);
|
|
160
|
+
}
|
|
161
|
+
convergence.terminalReason = "non-retry-failure";
|
|
162
|
+
return { retry: false };
|
|
163
|
+
}
|
|
164
|
+
const hardVerify = input.state.nodes["hard-verify-shell"];
|
|
165
|
+
if (hardVerify?.status === "ERROR") {
|
|
166
|
+
return handleHardVerifyFailure(input, hardVerify);
|
|
167
|
+
}
|
|
168
|
+
if (hardVerify?.status === "FINISHED") {
|
|
169
|
+
convergence.terminalReason = "hard-verify-pass";
|
|
170
|
+
return { retry: false };
|
|
171
|
+
}
|
|
172
|
+
convergence.terminalReason = hardVerify
|
|
173
|
+
? "non-retry-failure"
|
|
174
|
+
: "unsupported-dag-shape";
|
|
89
175
|
return { retry: false };
|
|
90
176
|
}
|
|
91
177
|
/**
|
|
@@ -104,7 +190,7 @@ async function handleHardVerifyFailure(input, hardVerify) {
|
|
|
104
190
|
runDir: input.runDir,
|
|
105
191
|
spec: input.spec,
|
|
106
192
|
});
|
|
107
|
-
if (
|
|
193
|
+
if (!HARD_VERIFY_REPAIR_ELIGIBLE_FAILURES.has(hardFailure)) {
|
|
108
194
|
passRecord.status = "terminal";
|
|
109
195
|
passRecord.reason = "non-retry-failure";
|
|
110
196
|
convergence.passHistory.push(passRecord);
|
|
@@ -145,29 +231,32 @@ async function handleHardVerifyFailure(input, hardVerify) {
|
|
|
145
231
|
await input.persistState();
|
|
146
232
|
return { retry: true };
|
|
147
233
|
}
|
|
234
|
+
function isLegitimateReviewRequestRevision(state) {
|
|
235
|
+
const reviewNode = state.nodes["review-pi"];
|
|
236
|
+
const reviewVerdictNode = state.nodes[REVIEW_VERDICT_NODE_ID];
|
|
237
|
+
return (reviewNode?.status === "FINISHED" &&
|
|
238
|
+
parseProcessVerdict(reviewNode) === "request-revision" &&
|
|
239
|
+
reviewVerdictNode?.status === "FINISHED" &&
|
|
240
|
+
parseProcessVerdict(reviewVerdictNode) === "request-revision");
|
|
241
|
+
}
|
|
148
242
|
/**
|
|
149
|
-
* Hard verification passed but the review
|
|
150
|
-
*
|
|
151
|
-
* not
|
|
152
|
-
* the
|
|
153
|
-
* and
|
|
154
|
-
* stay fail-closed and never enter code repair.
|
|
243
|
+
* Hard verification passed but the review path produced a legitimate
|
|
244
|
+
* `request-revision`. With routingAccept the review gate finishes as a routing
|
|
245
|
+
* result (not shell ERROR); older gates may still ERROR. Re-enter the same
|
|
246
|
+
* bounded recovery chain so repair can address the findings, then re-verify
|
|
247
|
+
* and re-review. Protocol/safety failures stay fail-closed.
|
|
155
248
|
*/
|
|
156
249
|
async function handleReviewRequestRevision(input, reviewGate) {
|
|
157
250
|
const convergence = input.state.convergence;
|
|
158
251
|
const currentPass = convergence.currentPass || 1;
|
|
159
252
|
const reviewNode = input.state.nodes["review-pi"];
|
|
160
253
|
const reviewVerdictNode = input.state.nodes[REVIEW_VERDICT_NODE_ID];
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
// into automatic code repair.
|
|
254
|
+
// Inspect the complete review chain so recovery output cannot launder
|
|
255
|
+
// provider/safety/protocol failures into automatic code repair.
|
|
164
256
|
const reviewFailure = [reviewGate, reviewVerdictNode, reviewNode]
|
|
165
257
|
.map((node) => node?.failureCategory)
|
|
166
258
|
.find((category) => category && REVIEW_SOURCE_NON_RETRY_FAILURES.has(category));
|
|
167
|
-
const legitimateRequestRevision =
|
|
168
|
-
parseProcessVerdict(reviewNode) === "request-revision" &&
|
|
169
|
-
reviewVerdictNode?.status === "FINISHED" &&
|
|
170
|
-
parseProcessVerdict(reviewVerdictNode) === "request-revision";
|
|
259
|
+
const legitimateRequestRevision = isLegitimateReviewRequestRevision(input.state);
|
|
171
260
|
if (reviewFailure || !legitimateRequestRevision) {
|
|
172
261
|
const passRecord = await buildConvergencePassRecord({
|
|
173
262
|
pass: currentPass,
|
|
@@ -84,6 +84,7 @@ export function loadFrontendImplementationContractJsonSchema(startDir = path.dir
|
|
|
84
84
|
return JSON.stringify(parsed);
|
|
85
85
|
}
|
|
86
86
|
const id = z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/);
|
|
87
|
+
const REQUIREMENT_ID_PATTERN = /^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/;
|
|
87
88
|
const safePath = z
|
|
88
89
|
.string()
|
|
89
90
|
.min(1)
|
|
@@ -366,7 +367,7 @@ export function canonicalFrontendContractSourceBinding(binding) {
|
|
|
366
367
|
.filter((source) => source.kind === "reference")
|
|
367
368
|
.map((source) => source.path)
|
|
368
369
|
.sort(),
|
|
369
|
-
requirementIds:
|
|
370
|
+
requirementIds: binding.requirementIds.map(canonicalizeRequirementId),
|
|
370
371
|
};
|
|
371
372
|
}
|
|
372
373
|
function asRecord(value) {
|
|
@@ -384,6 +385,137 @@ function asStringArray(value) {
|
|
|
384
385
|
.map((item) => asString(item))
|
|
385
386
|
.filter((item) => item.length > 0);
|
|
386
387
|
}
|
|
388
|
+
function isRequirementId(value) {
|
|
389
|
+
return REQUIREMENT_ID_PATTERN.test(value);
|
|
390
|
+
}
|
|
391
|
+
/** Normalize the compact IDs models commonly emit (for example AC1) before
|
|
392
|
+
* strict schema validation. This keeps governance strict while making the
|
|
393
|
+
* boundary tolerant of presentation-only formatting differences. */
|
|
394
|
+
function canonicalizeRequirementId(value) {
|
|
395
|
+
const trimmed = value.trim().toUpperCase();
|
|
396
|
+
if (REQUIREMENT_ID_PATTERN.test(trimmed))
|
|
397
|
+
return trimmed;
|
|
398
|
+
const compact = /^(REQ|BR|AC)(\d+)$/.exec(trimmed);
|
|
399
|
+
if (compact)
|
|
400
|
+
return `${compact[1]}-${compact[2]}`;
|
|
401
|
+
return value;
|
|
402
|
+
}
|
|
403
|
+
function canonicalizeRequirementIdsInPayload(value) {
|
|
404
|
+
if (Array.isArray(value))
|
|
405
|
+
return value.map(canonicalizeRequirementIdsInPayload);
|
|
406
|
+
if (!value || typeof value !== "object")
|
|
407
|
+
return value;
|
|
408
|
+
const record = value;
|
|
409
|
+
const out = {};
|
|
410
|
+
for (const [key, child] of Object.entries(record)) {
|
|
411
|
+
if (key === "id" || key === "requirementId") {
|
|
412
|
+
out[key] = typeof child === "string" ? canonicalizeRequirementId(child) : child;
|
|
413
|
+
}
|
|
414
|
+
else if (key === "requirementIds" && Array.isArray(child)) {
|
|
415
|
+
out[key] = child.map((item) => typeof item === "string" ? canonicalizeRequirementId(item) : item);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
out[key] = canonicalizeRequirementIdsInPayload(child);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
function canonicalizeVerificationTargetAliases(value) {
|
|
424
|
+
const record = asRecord(value);
|
|
425
|
+
if (!record || !Array.isArray(record.verificationTargets))
|
|
426
|
+
return value;
|
|
427
|
+
const targets = record.verificationTargets
|
|
428
|
+
.map((item) => asRecord(item))
|
|
429
|
+
.filter((item) => Boolean(item));
|
|
430
|
+
const aliases = new Map();
|
|
431
|
+
for (const target of targets) {
|
|
432
|
+
const id = asString(target.id);
|
|
433
|
+
const label = asString(target.commandLabel).toLowerCase();
|
|
434
|
+
if (!id)
|
|
435
|
+
continue;
|
|
436
|
+
aliases.set(id.toLowerCase(), id);
|
|
437
|
+
if (label.includes("typecheck"))
|
|
438
|
+
aliases.set("vt-typecheck", id);
|
|
439
|
+
if (label.includes("unified surface"))
|
|
440
|
+
aliases.set("vt-unified-surface", id);
|
|
441
|
+
}
|
|
442
|
+
const requirements = Array.isArray(record.requirements)
|
|
443
|
+
? record.requirements.map((item) => {
|
|
444
|
+
const requirement = asRecord(item);
|
|
445
|
+
if (!requirement || !Array.isArray(requirement.verificationTargetIds))
|
|
446
|
+
return item;
|
|
447
|
+
return {
|
|
448
|
+
...requirement,
|
|
449
|
+
verificationTargetIds: requirement.verificationTargetIds.map((id) => typeof id === "string" ? aliases.get(id.toLowerCase()) ?? id : id),
|
|
450
|
+
};
|
|
451
|
+
})
|
|
452
|
+
: record.requirements;
|
|
453
|
+
return { ...record, requirements };
|
|
454
|
+
}
|
|
455
|
+
function assertFrontendContractPathsSafe(value) {
|
|
456
|
+
const record = asRecord(value);
|
|
457
|
+
if (!record)
|
|
458
|
+
return;
|
|
459
|
+
const pathFields = ["files", "file", "implementationTargets", "fixture", "consumer"];
|
|
460
|
+
for (const [key, child] of Object.entries(record)) {
|
|
461
|
+
if (pathFields.includes(key) && Array.isArray(child)) {
|
|
462
|
+
for (const item of child) {
|
|
463
|
+
if (typeof item === "string" && (item.startsWith("/") || item.includes("\\") || item.split("/").includes("..")))
|
|
464
|
+
throw new Error(`invalid-output: unsafe frontend contract path ${item}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else if (pathFields.includes(key) && typeof child === "string" && (child.startsWith("/") || child.includes("\\") || child.split("/").includes(".."))) {
|
|
468
|
+
throw new Error(`invalid-output: unsafe frontend contract path ${child}`);
|
|
469
|
+
}
|
|
470
|
+
if (Array.isArray(child))
|
|
471
|
+
child.forEach(assertFrontendContractPathsSafe);
|
|
472
|
+
else if (child && typeof child === "object")
|
|
473
|
+
assertFrontendContractPathsSafe(child);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function deriveContractRequirementIds(value) {
|
|
477
|
+
const record = asRecord(value);
|
|
478
|
+
if (!record)
|
|
479
|
+
return [];
|
|
480
|
+
const ids = [];
|
|
481
|
+
const push = (candidate) => {
|
|
482
|
+
const value = asString(candidate);
|
|
483
|
+
const canonical = canonicalizeRequirementId(value);
|
|
484
|
+
if (!canonical || !isRequirementId(canonical) || ids.includes(canonical))
|
|
485
|
+
return;
|
|
486
|
+
ids.push(canonical);
|
|
487
|
+
};
|
|
488
|
+
if (Array.isArray(record.requirements)) {
|
|
489
|
+
for (const item of record.requirements)
|
|
490
|
+
push(asRecord(item)?.id);
|
|
491
|
+
}
|
|
492
|
+
if (Array.isArray(record.requirementCoverage)) {
|
|
493
|
+
for (const item of record.requirementCoverage)
|
|
494
|
+
push(asRecord(item)?.id);
|
|
495
|
+
}
|
|
496
|
+
const rawVerification = Array.isArray(record.verificationTargets)
|
|
497
|
+
? record.verificationTargets
|
|
498
|
+
: asRecord(record.verificationTargets)
|
|
499
|
+
? Object.values(asRecord(record.verificationTargets))
|
|
500
|
+
: [];
|
|
501
|
+
for (const item of rawVerification) {
|
|
502
|
+
for (const requirementId of asStringArray(asRecord(item)?.requirementIds)) {
|
|
503
|
+
push(requirementId);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
for (const item of Array.isArray(record.evidenceGaps) ? record.evidenceGaps : []) {
|
|
507
|
+
push(asRecord(item)?.requirementId);
|
|
508
|
+
}
|
|
509
|
+
return ids;
|
|
510
|
+
}
|
|
511
|
+
function withDerivedRequirementIdsWhenUnscoped(binding, parsed) {
|
|
512
|
+
if (binding.requirementIds.length > 0)
|
|
513
|
+
return binding;
|
|
514
|
+
const derivedIds = deriveContractRequirementIds(parsed);
|
|
515
|
+
return derivedIds.length > 0
|
|
516
|
+
? { ...binding, requirementIds: derivedIds }
|
|
517
|
+
: binding;
|
|
518
|
+
}
|
|
387
519
|
function looksLikeStrictFrontendContract(value) {
|
|
388
520
|
const record = asRecord(value);
|
|
389
521
|
if (!record)
|
|
@@ -401,6 +533,11 @@ function looksLikeStrictFrontendContract(value) {
|
|
|
401
533
|
*/
|
|
402
534
|
export function coerceFrontendImplementationContractInput(value, canonicalBinding) {
|
|
403
535
|
const rawRecord = asRecord(value);
|
|
536
|
+
const rawMockApi = asRecord(rawRecord?.mockApi);
|
|
537
|
+
if (rawMockApi &&
|
|
538
|
+
typeof rawMockApi.strategy === "string" &&
|
|
539
|
+
!["native", "browser-intercept", "request-adapter", "not-needed"].includes(rawMockApi.strategy))
|
|
540
|
+
return value;
|
|
404
541
|
const verificationTargetIds = rawRecord && Array.isArray(rawRecord.verificationTargets)
|
|
405
542
|
? new Set(rawRecord.verificationTargets
|
|
406
543
|
.map((item) => asString(asRecord(item)?.id))
|
|
@@ -414,9 +551,10 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
414
551
|
const requirement = asRecord(item);
|
|
415
552
|
if (!requirement || !Array.isArray(requirement.verificationTargetIds))
|
|
416
553
|
return item;
|
|
554
|
+
const filtered = requirement.verificationTargetIds.filter((id) => typeof id === "string" && verificationTargetIds.has(id));
|
|
417
555
|
return {
|
|
418
556
|
...requirement,
|
|
419
|
-
verificationTargetIds:
|
|
557
|
+
verificationTargetIds: filtered.length > 0 ? filtered : requirement.verificationTargetIds,
|
|
420
558
|
};
|
|
421
559
|
})
|
|
422
560
|
: rawRecord.requirements,
|
|
@@ -444,8 +582,39 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
444
582
|
: rawRecord.uiStates,
|
|
445
583
|
}
|
|
446
584
|
: value;
|
|
447
|
-
|
|
448
|
-
|
|
585
|
+
// A payload can have the strict top-level shape while still containing
|
|
586
|
+
// empty requirement coverage arrays. Do not trust shape alone: route such
|
|
587
|
+
// payloads through the compatibility normalizer so targets and verification
|
|
588
|
+
// references are deterministically filled from the contract context.
|
|
589
|
+
if (looksLikeStrictFrontendContract(normalizedValue)) {
|
|
590
|
+
const strictRecord = asRecord(normalizedValue);
|
|
591
|
+
const strictMockApi = asRecord(strictRecord?.mockApi);
|
|
592
|
+
if (strictMockApi &&
|
|
593
|
+
typeof strictMockApi.strategy === "string" &&
|
|
594
|
+
!["native", "browser-intercept", "request-adapter", "not-needed"].includes(strictMockApi.strategy))
|
|
595
|
+
return normalizedValue;
|
|
596
|
+
const targetFiles = asStringArray(asRecord(strictRecord?.targets)?.files);
|
|
597
|
+
const verificationIds = Array.isArray(strictRecord?.verificationTargets)
|
|
598
|
+
? strictRecord.verificationTargets.map((item) => asString(asRecord(item)?.id)).filter(Boolean)
|
|
599
|
+
: [];
|
|
600
|
+
if (Array.isArray(strictRecord?.requirements)) {
|
|
601
|
+
const requirements = strictRecord.requirements.map((item) => {
|
|
602
|
+
const requirement = asRecord(item);
|
|
603
|
+
if (!requirement)
|
|
604
|
+
return item;
|
|
605
|
+
return {
|
|
606
|
+
...requirement,
|
|
607
|
+
implementationTargets: asStringArray(requirement.implementationTargets).length > 0
|
|
608
|
+
? requirement.implementationTargets
|
|
609
|
+
: targetFiles,
|
|
610
|
+
verificationTargetIds: asStringArray(requirement.verificationTargetIds).length > 0
|
|
611
|
+
? requirement.verificationTargetIds
|
|
612
|
+
: verificationIds,
|
|
613
|
+
};
|
|
614
|
+
});
|
|
615
|
+
return { ...strictRecord, requirements };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
449
618
|
const record = asRecord(normalizedValue);
|
|
450
619
|
if (!record)
|
|
451
620
|
return value;
|
|
@@ -729,8 +898,15 @@ export async function materializeFrontendImplementationContract(input) {
|
|
|
729
898
|
throw new Error("frontend implementation contract gate requires DAG sourceBinding");
|
|
730
899
|
const record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
|
|
731
900
|
let parsed;
|
|
901
|
+
const rawContractText = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
902
|
+
if (rawContractText.includes('"files":["/'))
|
|
903
|
+
throw new Error("invalid-output: absolute frontend contract path is forbidden");
|
|
904
|
+
if (/(?:"(?:files|file|implementationTargets|fixture|consumer)"\s*:\s*(?:\[\s*)?)"\//.test(rawContractText))
|
|
905
|
+
throw new Error("invalid-output: absolute frontend contract path is forbidden");
|
|
906
|
+
if (/(?:"strategy"\s*:\s*")(?!native\b|browser-intercept\b|request-adapter\b|not-needed\b)[^"]+"/.test(rawContractText))
|
|
907
|
+
throw new Error("invalid-output: unsupported mock strategy");
|
|
732
908
|
try {
|
|
733
|
-
parsed = extractFrontendImplementationJson(
|
|
909
|
+
parsed = extractFrontendImplementationJson(rawContractText);
|
|
734
910
|
}
|
|
735
911
|
catch (error) {
|
|
736
912
|
throw new Error(`invalid-output: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -740,21 +916,46 @@ export async function materializeFrontendImplementationContract(input) {
|
|
|
740
916
|
throw new Error(`invalid-output: ${secrets.join("; ")}`);
|
|
741
917
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
742
918
|
throw new Error("invalid-output: frontend contract must be a JSON object");
|
|
743
|
-
const
|
|
919
|
+
const parsedTargetObject = parsed.targets;
|
|
920
|
+
const parsedFiles = parsedTargetObject && typeof parsedTargetObject === "object"
|
|
921
|
+
? parsedTargetObject.files
|
|
922
|
+
: undefined;
|
|
923
|
+
if (Array.isArray(parsedFiles) && parsedFiles.some((file) => typeof file === "string" && file.startsWith("/")))
|
|
924
|
+
throw new Error("invalid-output: absolute frontend contract path is forbidden");
|
|
925
|
+
// Normalize model-emitted compact requirement IDs before deriving the
|
|
926
|
+
// canonical binding or invoking the strict zod schema.
|
|
927
|
+
parsed = canonicalizeRequirementIdsInPayload(parsed);
|
|
928
|
+
parsed = canonicalizeVerificationTargetAliases(parsed);
|
|
929
|
+
assertFrontendContractPathsSafe(parsed);
|
|
930
|
+
const parsedTargets = asRecord(parsed)?.targets;
|
|
931
|
+
const parsedTargetFiles = asStringArray(asRecord(parsedTargets)?.files);
|
|
932
|
+
if (parsedTargetFiles.some((file) => file.startsWith("/") || file.includes("\\")))
|
|
933
|
+
throw new Error("invalid-output: frontend contract target paths must be relative POSIX paths");
|
|
934
|
+
const parsedStates = asRecord(parsed)?.uiStates;
|
|
935
|
+
if (Array.isArray(parsedStates) && parsedStates.some((item) => {
|
|
936
|
+
const state = asRecord(item);
|
|
937
|
+
return state?.applicable === true &&
|
|
938
|
+
(!asString(state.expectedBehavior) || asStringArray(state.implementationTargets).length === 0 || asStringArray(state.verificationTargetIds).length === 0);
|
|
939
|
+
}))
|
|
940
|
+
throw new Error("invalid-output: applicable UI state requires behavior, implementation, and verification");
|
|
941
|
+
const parsedMockApi = asRecord(parsed)?.mockApi;
|
|
942
|
+
if (asRecord(parsedMockApi) &&
|
|
943
|
+
typeof asRecord(parsedMockApi)?.strategy === "string" &&
|
|
944
|
+
!["native", "browser-intercept", "request-adapter", "not-needed"].includes(String(asRecord(parsedMockApi)?.strategy)))
|
|
945
|
+
throw new Error(`invalid-output: unsupported mock strategy ${String(asRecord(parsedMockApi)?.strategy)}`);
|
|
946
|
+
const baseCanonicalBinding = canonicalFrontendContractSourceBinding(input.sourceBinding);
|
|
947
|
+
const canonicalBinding = withDerivedRequirementIdsWhenUnscoped(baseCanonicalBinding, parsed);
|
|
744
948
|
// Always inject DAG-owned identity. Model-provided sourceBinding is advisory
|
|
745
949
|
// only and must not fail a otherwise-valid contract (common live failure:
|
|
746
950
|
// wrong requirementPath/sha, extra referencePaths, or omitted binding).
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
if (!result.success) {
|
|
756
|
-
result = frontendImplementationContractSchema.safeParse(candidates[1]);
|
|
757
|
-
}
|
|
951
|
+
const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
|
|
952
|
+
// There is exactly one post-security candidate. A fallback candidate would
|
|
953
|
+
// allow malformed raw fields to bypass the boundary checks above.
|
|
954
|
+
const candidate = {
|
|
955
|
+
...(asRecord(normalizedContract) ?? parsed),
|
|
956
|
+
sourceBinding: canonicalBinding,
|
|
957
|
+
};
|
|
958
|
+
const result = frontendImplementationContractSchema.safeParse(candidate);
|
|
758
959
|
if (!result.success)
|
|
759
960
|
throw new Error(`invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
|
|
760
961
|
const blockingGaps = [
|
|
@@ -76,8 +76,14 @@ function assertReviewEvidence(input) {
|
|
|
76
76
|
throw new Error("frontend review context invalid repair assessment");
|
|
77
77
|
}
|
|
78
78
|
export async function runFrontendReviewContextGate(input) {
|
|
79
|
-
const diff = await runFrontendWorktreeDiffGate(input);
|
|
80
79
|
const contract = await readRequiredJson(input.runDir, "contracts/frontend-implementation-contract.json");
|
|
80
|
+
const parsedContract = frontendImplementationContractSchema.safeParse(contract);
|
|
81
|
+
if (!parsedContract.success)
|
|
82
|
+
throw new Error("frontend review context invalid implementation contract");
|
|
83
|
+
const diff = await runFrontendWorktreeDiffGate({
|
|
84
|
+
...input,
|
|
85
|
+
authorizedChangedPaths: parsedContract.data.targets.files,
|
|
86
|
+
});
|
|
81
87
|
const verificationTrace = await readRequiredJson(input.runDir, "contracts/frontend-verification-trace.json");
|
|
82
88
|
const repairAssessment = await readRequiredJson(input.runDir, "contracts/frontend-repair-assessment.json");
|
|
83
89
|
const lintAssessment = await readOptionalLintAssessment(input.runDir);
|
|
@@ -30,6 +30,8 @@ function symbolEvidenceCandidates(symbol) {
|
|
|
30
30
|
const normalized = symbol.trim().toLowerCase();
|
|
31
31
|
if (normalized === "all describe blocks")
|
|
32
32
|
return ["describe("];
|
|
33
|
+
// A legacy contract may encode several symbols plus prose in one label.
|
|
34
|
+
// Validate the actual slash-delimited symbols individually.
|
|
33
35
|
const describeTitle = symbol.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
|
|
34
36
|
if (describeTitle)
|
|
35
37
|
return [symbol, describeTitle, "describe("];
|
|
@@ -58,9 +60,18 @@ async function assertFileAndSymbol(input) {
|
|
|
58
60
|
}
|
|
59
61
|
if (input.symbol) {
|
|
60
62
|
const content = await readFile(absolute, "utf8");
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
const symbols = input.symbol.includes(" / ")
|
|
64
|
+
? input.symbol
|
|
65
|
+
.split(/[((]/, 1)[0]
|
|
66
|
+
.split("/")
|
|
67
|
+
.map((symbol) => symbol.trim())
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
: [input.symbol];
|
|
70
|
+
for (const symbol of symbols) {
|
|
71
|
+
const matched = symbolEvidenceCandidates(symbol).some((candidate) => content.includes(candidate));
|
|
72
|
+
if (!matched) {
|
|
73
|
+
issues.push(`symbol not found: ${symbol} in ${input.file}`);
|
|
74
|
+
}
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
77
|
return issues;
|
|
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
6
|
+
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
6
7
|
export const FRONTEND_WORKTREE_DIFF_SCHEMA_ID = "frontend-worktree-diff-v1";
|
|
7
8
|
export const FRONTEND_WORKTREE_BASELINE_SCHEMA_ID = "frontend-worktree-baseline-v1";
|
|
8
9
|
function runGit(cwd, args) {
|
|
@@ -128,17 +129,27 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
128
129
|
}
|
|
129
130
|
const changedFilesAll = splitLines(nameOnly.stdout).sort();
|
|
130
131
|
const untrackedFilesAll = splitLines(untracked.stdout).sort();
|
|
132
|
+
const authorizedChangedPaths = input.authorizedChangedPaths ?? [];
|
|
131
133
|
if (baseline) {
|
|
132
134
|
for (const file of baseline.files) {
|
|
133
135
|
const currentHash = await sha256File(path.join(root, file));
|
|
134
|
-
|
|
136
|
+
const authorizedOverlap = authorizedChangedPaths.some((pattern) => pathMatchesPattern(file, pattern));
|
|
137
|
+
if (currentHash !== baseline.hashes[file] && !authorizedOverlap) {
|
|
135
138
|
throw new Error(`frontend worktree diff overlaps pre-existing change: ${file}`);
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
const baselineFiles = new Set(baseline?.files ?? []);
|
|
140
|
-
const changedFiles = changedFilesAll.filter((file) =>
|
|
141
|
-
|
|
143
|
+
const changedFiles = changedFilesAll.filter((file) => {
|
|
144
|
+
if (!baselineFiles.has(file))
|
|
145
|
+
return true;
|
|
146
|
+
return authorizedChangedPaths.some((pattern) => pathMatchesPattern(file, pattern));
|
|
147
|
+
});
|
|
148
|
+
const untrackedFiles = untrackedFilesAll.filter((file) => {
|
|
149
|
+
if (!baselineFiles.has(file))
|
|
150
|
+
return true;
|
|
151
|
+
return authorizedChangedPaths.some((pattern) => pathMatchesPattern(file, pattern));
|
|
152
|
+
});
|
|
142
153
|
const patchSource = baseline
|
|
143
154
|
? changedFiles.length > 0
|
|
144
155
|
? await runGit(root, ["diff", "--binary", "HEAD", "--", ...changedFiles])
|