@tea-agent/loop-agent 0.36.4-beta.0 → 0.37.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -6
- package/CHANGELOG.md +72 -163
- package/README.md +1 -1
- package/bin/loop-agent.js +1 -37
- package/dist/application/task-lifecycle/advance.js +0 -1
- package/dist/application/task-lifecycle/observe.js +0 -15
- package/dist/application/task-lifecycle/plan-transitions.js +0 -15
- package/dist/cli/command-definitions.js +0 -7
- package/dist/cli/program.js +3 -8
- package/dist/commands/init-upgrade.js +19 -351
- package/dist/commands/init.js +67 -14
- package/dist/commands/run-dag-progress.js +0 -14
- package/dist/commands/task-advance.js +3 -33
- package/dist/executors/dag-pi-executor.js +30 -47
- package/dist/executors/pi-sdk-executor.js +0 -34
- package/dist/executors/shell-executor.js +95 -6
- package/dist/infrastructure/harness/atomic-write.js +1 -2
- package/dist/shared/operator/capabilities.js +13 -200
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/frontend-project-capability.js +12 -162
- package/dist/task/source-prepare/completeness.js +0 -17
- package/dist/task/source-prepare/parse-intent.js +1 -15
- package/dist/task/source-prepare/semantic-intake.js +23 -144
- package/dist/worker/console/chat/artifact-card.js +1 -8
- package/dist/worker/console/chat/chat-event-store.js +0 -61
- package/dist/worker/console/chat/human-gate-card.js +1 -9
- package/dist/worker/console/chat/operation-card.js +2 -71
- package/dist/worker/console/chat/pi-runtime.js +62 -86
- package/dist/worker/console/chat/routes.js +8 -36
- package/dist/worker/console/chat/session-store.js +0 -3
- package/dist/worker/console/chat/shortcuts.js +7 -9
- package/dist/worker/console/chat/turn-process.js +23 -97
- package/dist/worker/console/chat/workspace-landing.js +1 -2
- package/dist/worker/console/operation-runner.js +6 -155
- package/dist/worker/console/operator-actions.js +13 -373
- package/dist/worker/console/operator-user-error.js +0 -4
- package/dist/worker/console/recovery-cta.js +3 -50
- package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
- package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -13
- package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
- package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -89
- package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -30
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
- package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/observe/spec-evidence.js +8 -3
- package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
- package/dist/worker/observe/static/operator-chrome.js +1 -6
- package/dist/worker/observe/static/views/dag-inspector.js +71 -6
- package/dist/worker/observe/static/views/dag.js +0 -12
- package/dist/worker/preflight.js +1 -2
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
- package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
- package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
- package/dist/workflows/dag/failure-routing.js +4 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
- package/dist/workflows/dag/frontend-repair.js +18 -219
- package/dist/workflows/dag/frontend-verification-trace.js +32 -47
- package/dist/workflows/dag/init-hybrid.js +86 -238
- package/dist/workflows/dag/lifecycle.js +0 -4
- package/dist/workflows/dag/node-execution.js +6 -105
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/report.js +0 -6
- package/dist/workflows/dag/retry-policy.js +0 -11
- package/dist/workflows/dag/runner.js +12 -314
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +9 -132
- package/dist/workflows/dag/validate.js +2 -7
- package/docs/README.md +3 -3
- package/docs/architecture/evolution.md +67 -102
- package/docs/templates/backend-test-dag.json +50 -32
- package/docs/templates/frontend-design-contract.md +14 -34
- package/docs/templates/frontend-task-constraints.md +13 -33
- package/docs/templates/frontend-task-requirement.md +20 -54
- package/docs/templates/init-managed-agents.md +2 -5
- package/harness.json +2 -2
- package/package.json +3 -4
- package/skills/loop-agent/SKILL.md +0 -1
- package/skills/loop-agent/references/command-reference.md +0 -3
- package/dist/build-stamp.json +0 -6
- package/dist/commands/dag-request-interrupt.js +0 -20
- package/dist/worker/console/chat/assistant-content.js +0 -121
- package/dist/worker/console/chat/semantic-activity.js +0 -471
- package/dist/worker/console/operation-run-facts.js +0 -190
- package/dist/worker/console/operation-wait.js +0 -355
- package/dist/worker/console/recovery-error-copy.js +0 -198
- package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
- package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
- package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
- package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- package/dist/worker/observability/interrupt-eligibility.js +0 -264
- package/dist/workflows/dag/contract-output-registry.js +0 -14
- package/dist/workflows/dag/contract-validator-registrations.js +0 -8
- package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
- package/dist/workflows/dag/frontend-recovery-run.js +0 -539
- package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
- package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
- package/dist/workflows/dag/interrupt-request.js +0 -559
|
@@ -1,126 +1,9 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
3
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
|
-
import {
|
|
4
|
+
import { frontendImplementationContractSchema, materializeFrontendImplementationContract, assertFrontendSourceBindingFresh, } from "./frontend-implementation-contract.js";
|
|
6
5
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
7
6
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
8
|
-
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
9
|
-
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
10
|
-
/**
|
|
11
|
-
* Machine-readable failure classification for blocked/retryable-invalid
|
|
12
|
-
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
13
|
-
* downstream routing (repair prompting, dashboards, retry policies) can act on
|
|
14
|
-
* a stable code instead of parsing prose.
|
|
15
|
-
*/
|
|
16
|
-
export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
17
|
-
"plan-text-unreadable",
|
|
18
|
-
"review-text-unreadable",
|
|
19
|
-
"verdict-not-pass",
|
|
20
|
-
"missing-requirement-ids",
|
|
21
|
-
"source-binding-not-fresh",
|
|
22
|
-
"contract-invalid",
|
|
23
|
-
"contract-blocked",
|
|
24
|
-
"mock-strategy-outside-allowed",
|
|
25
|
-
"mock-strategy-no-verification-commands",
|
|
26
|
-
"target-outside-write-set",
|
|
27
|
-
"verification-target-outside-write-set",
|
|
28
|
-
"openspec-not-read",
|
|
29
|
-
]);
|
|
30
|
-
export const frontendPrewriteResultV1Schema = z
|
|
31
|
-
.object({
|
|
32
|
-
schemaVersion: z.literal(1),
|
|
33
|
-
schemaId: z.literal(FRONTEND_PREWRITE_RESULT_SCHEMA_ID),
|
|
34
|
-
classification: z.enum([
|
|
35
|
-
"accepted",
|
|
36
|
-
"accepted-normalized",
|
|
37
|
-
"retryable-invalid",
|
|
38
|
-
"blocked",
|
|
39
|
-
]),
|
|
40
|
-
failureCode: frontendPrewriteFailureCodeSchema.nullable(),
|
|
41
|
-
normalizationActions: z.array(frontendNormalizationActionSchema),
|
|
42
|
-
candidateRawSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
43
|
-
candidateJsonSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
44
|
-
canonicalSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
45
|
-
selectedPlanNodeId: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
46
|
-
sourceBindingSha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
47
|
-
writeSetDigest: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
|
48
|
-
retryCount: z.number().int().min(0).max(1),
|
|
49
|
-
maxRetryCount: z.literal(1),
|
|
50
|
-
failureReason: z.string().nullable(),
|
|
51
|
-
digestAlgorithm: z.literal("sha256"),
|
|
52
|
-
})
|
|
53
|
-
.strict()
|
|
54
|
-
.superRefine((value, ctx) => {
|
|
55
|
-
const acceptedLike = value.classification === "accepted" ||
|
|
56
|
-
value.classification === "accepted-normalized";
|
|
57
|
-
const failed = value.classification === "retryable-invalid" ||
|
|
58
|
-
value.classification === "blocked";
|
|
59
|
-
if (acceptedLike) {
|
|
60
|
-
if (value.failureReason !== null)
|
|
61
|
-
ctx.addIssue({
|
|
62
|
-
code: "custom",
|
|
63
|
-
message: "failureReason must be null for accepted/accepted-normalized",
|
|
64
|
-
path: ["failureReason"],
|
|
65
|
-
});
|
|
66
|
-
if (value.failureCode !== null)
|
|
67
|
-
ctx.addIssue({
|
|
68
|
-
code: "custom",
|
|
69
|
-
message: "failureCode must be null for accepted/accepted-normalized",
|
|
70
|
-
path: ["failureCode"],
|
|
71
|
-
});
|
|
72
|
-
if (!value.candidateJsonSha256 || !value.canonicalSha256)
|
|
73
|
-
ctx.addIssue({
|
|
74
|
-
code: "custom",
|
|
75
|
-
message: "candidateJsonSha256 and canonicalSha256 are required for accepted/accepted-normalized",
|
|
76
|
-
path: ["candidateJsonSha256"],
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
if (value.classification === "accepted" && value.normalizationActions.length !== 0)
|
|
80
|
-
ctx.addIssue({
|
|
81
|
-
code: "custom",
|
|
82
|
-
message: "accepted must have empty normalizationActions",
|
|
83
|
-
path: ["normalizationActions"],
|
|
84
|
-
});
|
|
85
|
-
if (value.classification === "accepted-normalized" &&
|
|
86
|
-
value.normalizationActions.length === 0)
|
|
87
|
-
ctx.addIssue({
|
|
88
|
-
code: "custom",
|
|
89
|
-
message: "accepted-normalized must have non-empty normalizationActions",
|
|
90
|
-
path: ["normalizationActions"],
|
|
91
|
-
});
|
|
92
|
-
if (failed) {
|
|
93
|
-
if (value.normalizationActions.length !== 0)
|
|
94
|
-
ctx.addIssue({
|
|
95
|
-
code: "custom",
|
|
96
|
-
message: "retryable-invalid/blocked must have empty normalizationActions",
|
|
97
|
-
path: ["normalizationActions"],
|
|
98
|
-
});
|
|
99
|
-
if (!value.failureReason)
|
|
100
|
-
ctx.addIssue({
|
|
101
|
-
code: "custom",
|
|
102
|
-
message: "failureReason must be non-empty for retryable-invalid/blocked",
|
|
103
|
-
path: ["failureReason"],
|
|
104
|
-
});
|
|
105
|
-
if (value.failureCode === null)
|
|
106
|
-
ctx.addIssue({
|
|
107
|
-
code: "custom",
|
|
108
|
-
message: "failureCode must be non-null for retryable-invalid/blocked",
|
|
109
|
-
path: ["failureCode"],
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
export const frontendWriterAdmissionSchema = z
|
|
114
|
-
.object({
|
|
115
|
-
schemaVersion: z.literal(1),
|
|
116
|
-
writerNodeId: z.enum(["frontend-implement-pi", "frontend-repair-pi"]),
|
|
117
|
-
decision: z.enum(["authorized", "denied"]),
|
|
118
|
-
sourceArtifact: z.string().min(1),
|
|
119
|
-
artifactHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
120
|
-
checkedAt: z.string().min(1),
|
|
121
|
-
reason: z.string().nullable(),
|
|
122
|
-
})
|
|
123
|
-
.strict();
|
|
124
7
|
async function selectNode(runDir, primary, fallbacks) {
|
|
125
8
|
const candidates = [primary, ...fallbacks.filter((id) => id !== primary)];
|
|
126
9
|
const rejections = [];
|
|
@@ -154,10 +37,6 @@ async function readNodeText(runDir, nodeId) {
|
|
|
154
37
|
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
155
38
|
return text;
|
|
156
39
|
}
|
|
157
|
-
async function readNodeRawText(runDir, nodeId) {
|
|
158
|
-
const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
159
|
-
return record.assistantText ?? record.stdout ?? "";
|
|
160
|
-
}
|
|
161
40
|
function firstNonEmptyVerdictLine(text) {
|
|
162
41
|
const lines = text
|
|
163
42
|
.split(/\r?\n/)
|
|
@@ -253,141 +132,16 @@ async function checkOpenspecReadEvidence(input) {
|
|
|
253
132
|
}
|
|
254
133
|
return [...matched];
|
|
255
134
|
}
|
|
256
|
-
async function finalizePrewrite(input, pending) {
|
|
257
|
-
const sourceBindingSha256 = input.sourceBinding
|
|
258
|
-
? deterministicSha256(input.sourceBinding)
|
|
259
|
-
: null;
|
|
260
|
-
const writeSetDigest = input.config.implementationWriteSet
|
|
261
|
-
? deterministicSha256([...new Set(input.config.implementationWriteSet)].sort())
|
|
262
|
-
: null;
|
|
263
|
-
const prewriteResult = frontendPrewriteResultV1Schema.parse({
|
|
264
|
-
schemaVersion: 1,
|
|
265
|
-
schemaId: FRONTEND_PREWRITE_RESULT_SCHEMA_ID,
|
|
266
|
-
classification: pending.classification,
|
|
267
|
-
normalizationActions: pending.normalizationActions,
|
|
268
|
-
candidateRawSha256: pending.candidateRawSha256,
|
|
269
|
-
candidateJsonSha256: pending.candidateJsonSha256,
|
|
270
|
-
canonicalSha256: pending.canonicalSha256,
|
|
271
|
-
selectedPlanNodeId: pending.planNodeId,
|
|
272
|
-
sourceBindingSha256,
|
|
273
|
-
writeSetDigest,
|
|
274
|
-
retryCount: 0,
|
|
275
|
-
maxRetryCount: 1,
|
|
276
|
-
failureReason: pending.failureReason,
|
|
277
|
-
failureCode: pending.failureCode,
|
|
278
|
-
digestAlgorithm: "sha256",
|
|
279
|
-
});
|
|
280
|
-
const written = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(input.config.outputDir, FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME), prewriteResult);
|
|
281
|
-
return {
|
|
282
|
-
ok: true,
|
|
283
|
-
classification: pending.classification,
|
|
284
|
-
planNodeId: pending.planNodeId,
|
|
285
|
-
reviewNodeId: pending.reviewNodeId,
|
|
286
|
-
verdict: pending.verdict,
|
|
287
|
-
mockStrategy: pending.mockStrategy,
|
|
288
|
-
artifact: pending.artifact,
|
|
289
|
-
prewriteResult: {
|
|
290
|
-
path: written.path,
|
|
291
|
-
sha256: written.sha256,
|
|
292
|
-
schemaId: FRONTEND_PREWRITE_RESULT_SCHEMA_ID,
|
|
293
|
-
},
|
|
294
|
-
failureReason: pending.failureReason,
|
|
295
|
-
failureCode: pending.failureCode,
|
|
296
|
-
normalizationActions: pending.normalizationActions,
|
|
297
|
-
candidateRawSha256: pending.candidateRawSha256,
|
|
298
|
-
candidateJsonSha256: pending.candidateJsonSha256,
|
|
299
|
-
canonicalSha256: pending.canonicalSha256,
|
|
300
|
-
openspecReadPaths: pending.openspecReadPaths,
|
|
301
|
-
openspecCandidatePaths: pending.openspecCandidatePaths,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
135
|
export async function runFrontendPrewriteGate(input) {
|
|
305
136
|
const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
|
|
306
|
-
// selectNode throws for a scheduling defect (no FINISHED plan/review). That
|
|
307
|
-
// remains an ERROR and is intentionally not classified into prewrite-result.
|
|
308
|
-
const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
|
|
309
|
-
const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
|
|
310
|
-
const candidateRawSha256 = sha256Hex(await readNodeRawText(input.runDir, planNodeId));
|
|
311
|
-
const basePending = {
|
|
312
|
-
planNodeId,
|
|
313
|
-
reviewNodeId,
|
|
314
|
-
verdict: "",
|
|
315
|
-
candidateRawSha256,
|
|
316
|
-
candidateJsonSha256: null,
|
|
317
|
-
canonicalSha256: null,
|
|
318
|
-
normalizationActions: [],
|
|
319
|
-
mockStrategy: undefined,
|
|
320
|
-
artifact: undefined,
|
|
321
|
-
failureReason: null,
|
|
322
|
-
failureCode: null,
|
|
323
|
-
classification: "accepted",
|
|
324
|
-
openspecReadPaths: [],
|
|
325
|
-
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
326
|
-
};
|
|
327
|
-
let planText;
|
|
328
|
-
try {
|
|
329
|
-
planText = await readNodeText(input.runDir, planNodeId);
|
|
330
|
-
}
|
|
331
|
-
catch (error) {
|
|
332
|
-
return finalizePrewrite(input, {
|
|
333
|
-
...basePending,
|
|
334
|
-
classification: "retryable-invalid",
|
|
335
|
-
failureReason: error instanceof Error ? error.message : String(error),
|
|
336
|
-
failureCode: "plan-text-unreadable",
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
let reviewText;
|
|
340
|
-
try {
|
|
341
|
-
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
342
|
-
}
|
|
343
|
-
catch (error) {
|
|
344
|
-
return finalizePrewrite(input, {
|
|
345
|
-
...basePending,
|
|
346
|
-
classification: "blocked",
|
|
347
|
-
failureReason: `frontend prewrite gate blocked by ${reviewNodeId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
348
|
-
failureCode: "review-text-unreadable",
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
const verdict = firstNonEmptyVerdictLine(reviewText);
|
|
352
|
-
if (verdict !== "VERDICT: pass") {
|
|
353
|
-
return finalizePrewrite(input, {
|
|
354
|
-
...basePending,
|
|
355
|
-
verdict,
|
|
356
|
-
classification: "blocked",
|
|
357
|
-
failureReason: `frontend prewrite gate blocked by ${reviewNodeId}: ${verdict || "missing VERDICT"}`,
|
|
358
|
-
failureCode: "verdict-not-pass",
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
const missingIds = input.config.requiredRequirementIds.filter((id) => !planText.includes(id));
|
|
362
|
-
if (missingIds.length > 0) {
|
|
363
|
-
return finalizePrewrite(input, {
|
|
364
|
-
...basePending,
|
|
365
|
-
verdict,
|
|
366
|
-
classification: "blocked",
|
|
367
|
-
failureReason: `frontend prewrite gate missing requirement ids: ${missingIds.join(", ")}`,
|
|
368
|
-
failureCode: "missing-requirement-ids",
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
137
|
if (workspaceRoot) {
|
|
372
138
|
if (input.config.requireSourceFreshness) {
|
|
373
|
-
if (!input.sourceBinding)
|
|
139
|
+
if (!input.sourceBinding)
|
|
374
140
|
throw new Error("frontend prewrite gate requires sourceBinding for freshness check");
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
binding: input.sourceBinding,
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
catch (error) {
|
|
383
|
-
return finalizePrewrite(input, {
|
|
384
|
-
...basePending,
|
|
385
|
-
verdict,
|
|
386
|
-
classification: "blocked",
|
|
387
|
-
failureReason: error instanceof Error ? error.message : String(error),
|
|
388
|
-
failureCode: "source-binding-not-fresh",
|
|
389
|
-
});
|
|
390
|
-
}
|
|
141
|
+
await assertFrontendSourceBindingFresh({
|
|
142
|
+
workspaceRoot,
|
|
143
|
+
binding: input.sourceBinding,
|
|
144
|
+
});
|
|
391
145
|
}
|
|
392
146
|
let hasGitMetadata = true;
|
|
393
147
|
try {
|
|
@@ -405,34 +159,31 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
405
159
|
workspaceRoot,
|
|
406
160
|
});
|
|
407
161
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
sourceBinding: input.sourceBinding,
|
|
416
|
-
});
|
|
162
|
+
const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
|
|
163
|
+
const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
|
|
164
|
+
const planText = await readNodeText(input.runDir, planNodeId);
|
|
165
|
+
const reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
166
|
+
const verdict = firstNonEmptyVerdictLine(reviewText);
|
|
167
|
+
if (verdict !== "VERDICT: pass") {
|
|
168
|
+
throw new Error(`frontend prewrite gate blocked by ${reviewNodeId}: ${verdict || "missing VERDICT"}`);
|
|
417
169
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
...basePending,
|
|
422
|
-
verdict,
|
|
423
|
-
classification: error.kind,
|
|
424
|
-
failureReason: error.message,
|
|
425
|
-
failureCode: error.kind === "retryable-invalid"
|
|
426
|
-
? "contract-invalid"
|
|
427
|
-
: "contract-blocked",
|
|
428
|
-
candidateJsonSha256: error.candidateJsonSha256,
|
|
429
|
-
normalizationActions: [],
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
throw error;
|
|
170
|
+
const missingIds = input.config.requiredRequirementIds.filter((id) => !planText.includes(id));
|
|
171
|
+
if (missingIds.length > 0) {
|
|
172
|
+
throw new Error(`frontend prewrite gate missing requirement ids: ${missingIds.join(", ")}`);
|
|
433
173
|
}
|
|
434
|
-
|
|
435
|
-
|
|
174
|
+
// Always rematerialize from the selected plan node. Reusing a previous
|
|
175
|
+
// contracts/*.json would accept stale contracts when a higher-priority
|
|
176
|
+
// FINISHED primary is present but invalid (see composite-shell invalid-primary).
|
|
177
|
+
const artifact = await materializeFrontendImplementationContract({
|
|
178
|
+
runDir: input.runDir,
|
|
179
|
+
fromNodeId: planNodeId,
|
|
180
|
+
artifactName: input.config.artifactName,
|
|
181
|
+
outputDir: input.config.outputDir,
|
|
182
|
+
sourceBinding: input.sourceBinding,
|
|
183
|
+
});
|
|
184
|
+
const raw = JSON.parse(await readFile(artifact.path, "utf8"));
|
|
185
|
+
const contract = frontendImplementationContractSchema.parse(raw);
|
|
186
|
+
if (!input.config.allowedMockStrategies.includes(contract.mockApi.strategy)) {
|
|
436
187
|
const quotedAllowed = input.config.allowedMockStrategies
|
|
437
188
|
.map((strategy) => JSON.stringify(strategy))
|
|
438
189
|
.join(", ");
|
|
@@ -441,67 +192,30 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
441
192
|
const reason = forcedNotNeeded
|
|
442
193
|
? "the task is auto/not-required or lacks authorized deterministic Mock verification commands"
|
|
443
194
|
: `allowed strategies are [${quotedAllowed}]`;
|
|
444
|
-
|
|
445
|
-
...basePending,
|
|
446
|
-
verdict,
|
|
447
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
448
|
-
normalizationActions: [],
|
|
449
|
-
mockStrategy,
|
|
450
|
-
classification: "blocked",
|
|
451
|
-
failureReason: `frontend contract mock strategy "${mockStrategy}" is incompatible with the generated allowed strategies [${quotedAllowed}]; ${reason}. Regenerate the DAG or set frontendMock.policy=required with authorized Mock verification commands.`,
|
|
452
|
-
failureCode: "mock-strategy-outside-allowed",
|
|
453
|
-
});
|
|
195
|
+
throw new Error(`frontend contract mock strategy "${contract.mockApi.strategy}" is incompatible with the generated allowed strategies [${quotedAllowed}]; ${reason}. Regenerate the DAG or set frontendMock.policy=required with authorized Mock verification commands.`);
|
|
454
196
|
}
|
|
455
|
-
//
|
|
456
|
-
// by the DAG's frozen Mock verification
|
|
457
|
-
//
|
|
458
|
-
|
|
197
|
+
// Fail early instead of at the verification trace: a non-not-needed strategy
|
|
198
|
+
// can only be proven by executing the DAG's frozen Mock verification
|
|
199
|
+
// commands. If none were materialized at generation time the contract would
|
|
200
|
+
// fail "trace: selected Mock strategy requires successful Mock verification
|
|
201
|
+
// commands" after implementation, so block before any write is authorized.
|
|
202
|
+
if (contract.mockApi.strategy !== "not-needed" &&
|
|
459
203
|
(input.config.mockCommandLabels?.length ?? 0) === 0) {
|
|
460
|
-
|
|
461
|
-
...basePending,
|
|
462
|
-
verdict,
|
|
463
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
464
|
-
normalizationActions: [],
|
|
465
|
-
mockStrategy,
|
|
466
|
-
classification: "blocked",
|
|
467
|
-
failureReason: `frontend prewrite gate blocked: contract selected Mock strategy ${mockStrategy} but the DAG has no authorized Mock verification commands; add frontendMock.verifyCommands or a project mock script, or select not-needed`,
|
|
468
|
-
failureCode: "mock-strategy-no-verification-commands",
|
|
469
|
-
});
|
|
204
|
+
throw new Error(`frontend prewrite gate blocked: contract selected Mock strategy ${contract.mockApi.strategy} but the DAG has no authorized Mock verification commands; add frontendMock.verifyCommands or a project mock script, or select not-needed`);
|
|
470
205
|
}
|
|
471
206
|
if (input.config.implementationWriteSet) {
|
|
472
207
|
const writeSet = new Set(input.config.implementationWriteSet);
|
|
473
|
-
const contractTargets = new Set(
|
|
208
|
+
const contractTargets = new Set(contract.targets.files);
|
|
474
209
|
const uncoveredTargets = [...contractTargets].filter((target) => ![...writeSet].some((pattern) => pathMatchesPattern(target, pattern) || target === pattern));
|
|
475
210
|
if (uncoveredTargets.length > 0) {
|
|
476
|
-
|
|
477
|
-
...basePending,
|
|
478
|
-
verdict,
|
|
479
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
480
|
-
normalizationActions: [],
|
|
481
|
-
mockStrategy,
|
|
482
|
-
classification: "blocked",
|
|
483
|
-
failureReason: `frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`,
|
|
484
|
-
failureCode: "target-outside-write-set",
|
|
485
|
-
});
|
|
211
|
+
throw new Error(`frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`);
|
|
486
212
|
}
|
|
487
|
-
const nonStaticVTs =
|
|
213
|
+
const nonStaticVTs = contract.verificationTargets.filter((vt) => vt.type !== "static");
|
|
488
214
|
const uncoveredVTs = nonStaticVTs.filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
|
|
489
215
|
if (uncoveredVTs.length > 0) {
|
|
490
|
-
|
|
491
|
-
...basePending,
|
|
492
|
-
verdict,
|
|
493
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
494
|
-
normalizationActions: [],
|
|
495
|
-
mockStrategy,
|
|
496
|
-
classification: "blocked",
|
|
497
|
-
failureReason: `frontend prewrite gate verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`,
|
|
498
|
-
failureCode: "verification-target-outside-write-set",
|
|
499
|
-
});
|
|
216
|
+
throw new Error(`frontend prewrite gate verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`);
|
|
500
217
|
}
|
|
501
218
|
}
|
|
502
|
-
// Effective plan/review must actually read the candidate openspec paths when
|
|
503
|
-
// any exist. This fail-closes BEFORE the canonical contract is materialized
|
|
504
|
-
// so a writer is never authorized on top of unread normative evidence.
|
|
505
219
|
const candidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
506
220
|
const openspecReadPaths = await checkOpenspecReadEvidence({
|
|
507
221
|
runDir: input.runDir,
|
|
@@ -510,82 +224,36 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
510
224
|
reviewNodeId,
|
|
511
225
|
repoRoot: workspaceRoot ?? process.cwd(),
|
|
512
226
|
});
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
mockStrategy,
|
|
523
|
-
classification: "retryable-invalid",
|
|
524
|
-
failureReason: `frontend prewrite gate: effective plan/review did not read these openspec candidate paths: ${unread.join(", ")}. Read each candidate and report applicable rules plus hit path/section/line number, conflicts, and missing specifications, then rerun.`,
|
|
525
|
-
failureCode: "openspec-not-read",
|
|
526
|
-
openspecReadPaths,
|
|
527
|
-
openspecCandidatePaths: candidatePaths,
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
// Materialize the canonical contract only after every governance check passed.
|
|
532
|
-
const artifact = await writeFrontendImplementationContractArtifact({
|
|
533
|
-
runDir: input.runDir,
|
|
534
|
-
outputDir: input.config.outputDir,
|
|
535
|
-
artifactName: input.config.artifactName,
|
|
536
|
-
canonical: analysis.canonical,
|
|
537
|
-
});
|
|
538
|
-
const classification = analysis.normalizationActions.length > 0 ? "accepted-normalized" : "accepted";
|
|
539
|
-
return finalizePrewrite(input, {
|
|
540
|
-
...basePending,
|
|
227
|
+
// OpenSpec is advisory design evidence, not an authorization boundary.
|
|
228
|
+
// Model read behavior is nondeterministic, and a missed read must not block
|
|
229
|
+
// a contract/schema/write-set-authorized implementation. Preserve any
|
|
230
|
+
// successful evidence for review context, while allowing unavailable
|
|
231
|
+
// evidence to be reported downstream.
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
planNodeId,
|
|
235
|
+
reviewNodeId,
|
|
541
236
|
verdict,
|
|
542
|
-
|
|
543
|
-
failureReason: null,
|
|
544
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
545
|
-
canonicalSha256: artifact.sha256,
|
|
546
|
-
normalizationActions: analysis.normalizationActions,
|
|
547
|
-
mockStrategy,
|
|
237
|
+
mockStrategy: contract.mockApi.strategy,
|
|
548
238
|
artifact,
|
|
549
239
|
openspecReadPaths,
|
|
550
240
|
openspecCandidatePaths: candidatePaths,
|
|
551
|
-
}
|
|
241
|
+
};
|
|
552
242
|
}
|
|
553
243
|
export function formatFrontendPrewriteGateStdout(result) {
|
|
554
|
-
const lines = [
|
|
555
|
-
|
|
556
|
-
result.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
lines.push("Frontend prewrite gate: retryable-invalid");
|
|
564
|
-
}
|
|
565
|
-
lines.push(`Classification: ${result.classification}`);
|
|
566
|
-
lines.push(`Plan: ${result.planNodeId}`);
|
|
567
|
-
lines.push(`Review: ${result.reviewNodeId}`);
|
|
568
|
-
if (result.mockStrategy)
|
|
569
|
-
lines.push(`Mock strategy: ${result.mockStrategy}`);
|
|
570
|
-
if (result.artifact) {
|
|
571
|
-
lines.push(`Structured artifact: ${result.artifact.path}`);
|
|
572
|
-
lines.push(`Schema: ${result.artifact.schemaId}`);
|
|
573
|
-
lines.push(`SHA-256: ${result.artifact.sha256}`);
|
|
574
|
-
}
|
|
575
|
-
lines.push(`Prewrite result: ${result.prewriteResult.path}`);
|
|
576
|
-
if (result.failureReason)
|
|
577
|
-
lines.push(`Failure reason: ${result.failureReason}`);
|
|
244
|
+
const lines = [
|
|
245
|
+
"Frontend prewrite gate: pass",
|
|
246
|
+
`Plan: ${result.planNodeId}`,
|
|
247
|
+
`Review: ${result.reviewNodeId}`,
|
|
248
|
+
`Mock strategy: ${result.mockStrategy}`,
|
|
249
|
+
`Structured artifact: ${result.artifact.path}`,
|
|
250
|
+
`Schema: ${result.artifact.schemaId}`,
|
|
251
|
+
`SHA-256: ${result.artifact.sha256}`,
|
|
252
|
+
];
|
|
578
253
|
if (result.openspecReadPaths.length > 0) {
|
|
579
254
|
lines.push(`openspec read: ${result.openspecReadPaths.join(", ")}`);
|
|
580
255
|
}
|
|
581
|
-
if (result.openspecCandidatePaths.length
|
|
582
|
-
const readSet = new Set(result.openspecReadPaths);
|
|
583
|
-
const unread = result.openspecCandidatePaths.filter((candidate) => !readSet.has(candidate));
|
|
584
|
-
if (unread.length > 0) {
|
|
585
|
-
lines.push(`openspec unread: ${unread.join(", ")}`);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
else {
|
|
256
|
+
else if (result.openspecCandidatePaths.length === 0) {
|
|
589
257
|
lines.push("openspec: unavailable");
|
|
590
258
|
}
|
|
591
259
|
return lines.join("\n");
|