@tea-agent/loop-agent 0.35.1-beta.4 → 0.35.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/AGENTS.md +0 -2
- package/CHANGELOG.md +43 -26
- package/bin/loop-agent.js +1 -37
- package/dist/application/dag/generate-task-dag.js +4 -1
- package/dist/application/task-lifecycle/advance.js +14 -0
- package/dist/cli/program.js +2 -2
- package/dist/commands/task-advance.js +1 -0
- package/dist/executors/dag-pi-executor.js +0 -44
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/config-types.js +2 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +1 -0
- package/dist/task/source-prepare/build-draft.js +7 -0
- package/dist/task/source-prepare/semantic-intake.js +37 -10
- package/dist/task/task-demand-routing.js +10 -0
- package/dist/worker/console/chat/assistant-content.js +110 -0
- package/dist/worker/console/chat/pi-runtime.js +13 -32
- package/dist/worker/console/chat/routes.js +9 -4
- package/dist/worker/console/chat/session-store.js +3 -0
- package/dist/worker/console/chat/shortcuts.js +9 -7
- package/dist/worker/console/chat/turn-process.js +62 -15
- package/dist/worker/console/operator-actions.js +72 -6
- package/dist/worker/console/prd-intake-bridge.js +10 -3
- package/dist/worker/console/prd-reference-discovery.js +124 -0
- package/dist/worker/console/static/assets/index-DRqZiQ7J.css +1 -0
- package/dist/worker/console/static/assets/index-gVHrlqI9.js +56 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +19 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +0 -5
- package/dist/worker/console/static-src/app/useTaskWizard.js +12 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +10 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -6
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +11 -6
- package/dist/worker/console/static-src/operator-chat/useComposer.js +15 -1
- 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/views/dag-inspector.js +71 -6
- package/dist/worker/preflight.js +1 -2
- package/dist/workflows/dag/backend-test-scenario-param.js +23 -33
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
- 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 +26 -49
- package/dist/workflows/dag/node-execution.js +0 -89
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/runner.js +11 -245
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +2 -130
- package/harness.json +1 -1
- package/package.json +2 -2
- package/dist/build-stamp.json +0 -6
- package/dist/worker/console/static/assets/index-CvsQgALl.js +0 -56
- package/dist/worker/console/static/assets/index-hJqCPs_g.css +0 -1
- 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
|
@@ -20,14 +20,6 @@ export const frontendRepairFailureClassSchema = z.enum([
|
|
|
20
20
|
"spec-unclear",
|
|
21
21
|
"unknown",
|
|
22
22
|
]);
|
|
23
|
-
export const frontendCommandCategorySchema = z.enum([
|
|
24
|
-
"typecheck",
|
|
25
|
-
"build",
|
|
26
|
-
"lint",
|
|
27
|
-
"component-test",
|
|
28
|
-
"unit-test",
|
|
29
|
-
"trace",
|
|
30
|
-
]);
|
|
31
23
|
const REPAIRABLE = new Set([
|
|
32
24
|
"typecheck",
|
|
33
25
|
"build",
|
|
@@ -35,72 +27,12 @@ const REPAIRABLE = new Set([
|
|
|
35
27
|
"component-test",
|
|
36
28
|
"unit-test",
|
|
37
29
|
"trace",
|
|
30
|
+
"review",
|
|
38
31
|
]);
|
|
39
32
|
export function isFrontendRepairable(failureClass) {
|
|
40
33
|
return REPAIRABLE.has(failureClass);
|
|
41
34
|
}
|
|
42
|
-
/**
|
|
43
|
-
* Deterministic commandLabel → commandCategory mapping (AC-3).
|
|
44
|
-
* `targetType` comes from the contract verificationTargets[].type
|
|
45
|
-
* (static | unit | component | integration | mock); the literal commandLabel
|
|
46
|
-
* wins over type so a `static` typecheck target is never misread as a test.
|
|
47
|
-
*/
|
|
48
|
-
export function mapCommandLabelToCategory(commandLabel, targetType) {
|
|
49
|
-
const label = commandLabel.toLowerCase();
|
|
50
|
-
if (/(?:typecheck|check-types|type-check|\btsc\b)/.test(label))
|
|
51
|
-
return "typecheck";
|
|
52
|
-
if (/(?:vite\s+build|next\s+build|webpack|rollup|\bbuild\b)/.test(label))
|
|
53
|
-
return "build";
|
|
54
|
-
if (/(?:eslint|\blint\b)/.test(label))
|
|
55
|
-
return "lint";
|
|
56
|
-
if (targetType === "component")
|
|
57
|
-
return "component-test";
|
|
58
|
-
if (targetType === "unit")
|
|
59
|
-
return "unit-test";
|
|
60
|
-
if (/(?:component|testing-library|render\()/.test(label))
|
|
61
|
-
return "component-test";
|
|
62
|
-
return "unit-test";
|
|
63
|
-
}
|
|
64
|
-
export function isReviewRepairable(finding, implementWriteSet) {
|
|
65
|
-
const hasFile = Boolean(finding.file && finding.file.trim().length > 0);
|
|
66
|
-
const hasLocation = Boolean((finding.line !== undefined && finding.line > 0) ||
|
|
67
|
-
(finding.symbol && finding.symbol.trim().length > 0));
|
|
68
|
-
const hasRequirement = Boolean((finding.requirementId && finding.requirementId.trim().length > 0) ||
|
|
69
|
-
(finding.acceptanceCriteriaId &&
|
|
70
|
-
finding.acceptanceCriteriaId.trim().length > 0));
|
|
71
|
-
const fixScope = finding.fixScope ?? (hasFile ? [finding.file] : []);
|
|
72
|
-
const fixScopeSubset = fixScope.length > 0 &&
|
|
73
|
-
fixScope.every((entry) => isPathWithinWriteSet(entry, implementWriteSet));
|
|
74
|
-
return (hasFile &&
|
|
75
|
-
hasLocation &&
|
|
76
|
-
hasRequirement &&
|
|
77
|
-
fixScopeSubset &&
|
|
78
|
-
finding.requiresContractReplan !== true);
|
|
79
|
-
}
|
|
80
35
|
export function classifyFrontendFailure(input) {
|
|
81
|
-
// AC-2: structured evidence takes priority over string-blob heuristics.
|
|
82
|
-
if (input.failureCategory === "write-guard") {
|
|
83
|
-
return "path";
|
|
84
|
-
}
|
|
85
|
-
if (input.traceTarget || input.commandCategory === "trace") {
|
|
86
|
-
return "trace";
|
|
87
|
-
}
|
|
88
|
-
if (input.commandCategory) {
|
|
89
|
-
switch (input.commandCategory) {
|
|
90
|
-
case "typecheck":
|
|
91
|
-
return "typecheck";
|
|
92
|
-
case "build":
|
|
93
|
-
return "build";
|
|
94
|
-
case "lint":
|
|
95
|
-
return "lint";
|
|
96
|
-
case "component-test":
|
|
97
|
-
return "component-test";
|
|
98
|
-
case "unit-test":
|
|
99
|
-
return "unit-test";
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
// Fallback: string-blob heuristic, preserved in its original branch order so
|
|
103
|
-
// legacy assertions (error TS2304 → typecheck, write-guard → path, etc.) stay green.
|
|
104
36
|
const blob = `${input.nodeId}\n${input.failureCategory ?? ""}\n${input.stdout ?? ""}\n${input.stderr ?? ""}`.toLowerCase();
|
|
105
37
|
if (input.nodeId.includes("contract") ||
|
|
106
38
|
blob.includes("contract mismatch") ||
|
|
@@ -188,23 +120,6 @@ export const frontendRepairAssessmentSchema = z
|
|
|
188
120
|
allowedRepairWriteSet: z.array(z.string().min(1)),
|
|
189
121
|
evidenceRefs: z.array(z.string().min(1)),
|
|
190
122
|
browserStatus: z.literal("not-run"),
|
|
191
|
-
// AC-5 structured evidence fields (all optional so existing minimal
|
|
192
|
-
// fixtures and frontend-review-context safeParse stay compatible).
|
|
193
|
-
verificationTargetId: z.string().min(1).optional(),
|
|
194
|
-
commandLabel: z.string().min(1).optional(),
|
|
195
|
-
exitCode: z.number().int().optional(),
|
|
196
|
-
commandCategory: frontendCommandCategorySchema.optional(),
|
|
197
|
-
requirementIds: z.array(z.string().min(1)).optional(),
|
|
198
|
-
acceptanceCriteriaIds: z.array(z.string().min(1)).optional(),
|
|
199
|
-
fixScope: z.array(z.string().min(1)).optional(),
|
|
200
|
-
changedPaths: z.array(z.string().min(1)).optional(),
|
|
201
|
-
traceRef: z
|
|
202
|
-
.object({
|
|
203
|
-
relativePath: z.string().min(1),
|
|
204
|
-
schemaId: z.string().min(1),
|
|
205
|
-
})
|
|
206
|
-
.strict()
|
|
207
|
-
.optional(),
|
|
208
123
|
})
|
|
209
124
|
.strict();
|
|
210
125
|
function nodeHadFailure(record) {
|
|
@@ -220,24 +135,19 @@ function nodeHadFailure(record) {
|
|
|
220
135
|
function normalize(value) {
|
|
221
136
|
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
222
137
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Whether a concrete path or glob entry is contained by the implement writeSet.
|
|
225
|
-
* Exact glob equality is the norm (repair writeSet === implement writeSet);
|
|
226
|
-
* concrete (non-glob) paths may also be covered by an implement glob.
|
|
227
|
-
*/
|
|
228
|
-
export function isPathWithinWriteSet(entry, implementWriteSet) {
|
|
229
|
-
const normalizedEntry = normalize(entry);
|
|
230
|
-
const normalizedImplement = implementWriteSet.map(normalize);
|
|
231
|
-
if (normalizedImplement.includes(normalizedEntry))
|
|
232
|
-
return true;
|
|
233
|
-
if (!normalizedEntry.includes("*")) {
|
|
234
|
-
return implementWriteSet.some((pattern) => pathMatchesPattern(normalizedEntry, pattern));
|
|
235
|
-
}
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
138
|
export function assertWriteSetSubset(repairWriteSet, implementWriteSet) {
|
|
239
139
|
for (const entry of repairWriteSet) {
|
|
240
|
-
|
|
140
|
+
const ok = implementWriteSet.some((pattern) => normalize(entry) === normalize(pattern) ||
|
|
141
|
+
pathMatchesPattern(normalize(entry), pattern) ||
|
|
142
|
+
pathMatchesPattern(normalize(pattern), entry));
|
|
143
|
+
// exact equality preferred for generation-time copy
|
|
144
|
+
if (!implementWriteSet.map(normalize).includes(normalize(entry))) {
|
|
145
|
+
// allow identical globs only
|
|
146
|
+
if (!ok || normalize(entry) !== normalize(entry)) {
|
|
147
|
+
// require exact membership for exclusive writeSet entries
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!implementWriteSet.map(normalize).includes(normalize(entry))) {
|
|
241
151
|
throw new Error(`repair writeSet entry "${entry}" is not ⊆ implement writeSet`);
|
|
242
152
|
}
|
|
243
153
|
}
|
|
@@ -260,63 +170,6 @@ async function loadImplementWriteSet(runDir, fallback) {
|
|
|
260
170
|
}
|
|
261
171
|
return fallback;
|
|
262
172
|
}
|
|
263
|
-
function matchVerificationTarget(contract, commandLabel, command) {
|
|
264
|
-
for (const target of contract.verificationTargets) {
|
|
265
|
-
if (commandLabel && target.commandLabel === commandLabel)
|
|
266
|
-
return target;
|
|
267
|
-
if (command &&
|
|
268
|
-
(command.includes(target.commandLabel) ||
|
|
269
|
-
target.commandLabel.includes(command))) {
|
|
270
|
-
return target;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return undefined;
|
|
274
|
-
}
|
|
275
|
-
function deriveFailureEvidence(input) {
|
|
276
|
-
const evidence = {};
|
|
277
|
-
for (const { record } of input.failed) {
|
|
278
|
-
const results = record.commandResults ?? [];
|
|
279
|
-
const failedResult = results.find((item) => item.ok === false) ?? results[0];
|
|
280
|
-
if (failedResult &&
|
|
281
|
-
evidence.exitCode === undefined &&
|
|
282
|
-
typeof failedResult.exitCode === "number") {
|
|
283
|
-
evidence.exitCode = failedResult.exitCode;
|
|
284
|
-
}
|
|
285
|
-
if (failedResult?.commandLabel && !evidence.commandLabel) {
|
|
286
|
-
evidence.commandLabel = failedResult.commandLabel;
|
|
287
|
-
}
|
|
288
|
-
// Structured trace facts passed by the verification bundle / trace gate.
|
|
289
|
-
const failedTraceTarget = (record.traceTargets ?? []).find((target) => target.status === "failed");
|
|
290
|
-
if (failedTraceTarget && !evidence.verificationTargetId) {
|
|
291
|
-
evidence.verificationTargetId = failedTraceTarget.id;
|
|
292
|
-
if (!evidence.commandLabel)
|
|
293
|
-
evidence.commandLabel = failedTraceTarget.commandLabel;
|
|
294
|
-
evidence.traceTarget = failedTraceTarget;
|
|
295
|
-
evidence.traceRef = {
|
|
296
|
-
relativePath: "contracts/frontend-verification-trace.json",
|
|
297
|
-
schemaId: "frontend-verification-trace-v1",
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
if (record.changedPaths?.length && !evidence.changedPaths) {
|
|
301
|
-
evidence.changedPaths = [...record.changedPaths];
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
// Resolve commandLabel → contract verification target → category + scope.
|
|
305
|
-
const target = matchVerificationTarget(input.contract, evidence.commandLabel);
|
|
306
|
-
if (target) {
|
|
307
|
-
if (!evidence.verificationTargetId)
|
|
308
|
-
evidence.verificationTargetId = target.id;
|
|
309
|
-
if (!evidence.commandLabel)
|
|
310
|
-
evidence.commandLabel = target.commandLabel;
|
|
311
|
-
evidence.requirementIds ??= [...target.requirementIds];
|
|
312
|
-
evidence.acceptanceCriteriaIds ??= [...target.requirementIds];
|
|
313
|
-
evidence.fixScope ??= [target.file];
|
|
314
|
-
}
|
|
315
|
-
if (evidence.commandLabel) {
|
|
316
|
-
evidence.commandCategory ??= mapCommandLabelToCategory(evidence.commandLabel, target?.type);
|
|
317
|
-
}
|
|
318
|
-
return evidence;
|
|
319
|
-
}
|
|
320
173
|
export async function runFrontendFailureAssessGate(input) {
|
|
321
174
|
const attempt = input.attempt ?? 1;
|
|
322
175
|
const implementWriteSet = await loadImplementWriteSet(input.runDir, input.implementWriteSet ?? []);
|
|
@@ -326,14 +179,12 @@ export async function runFrontendFailureAssessGate(input) {
|
|
|
326
179
|
const contractRel = "contracts/frontend-implementation-contract.json";
|
|
327
180
|
const contractPath = path.join(input.runDir, contractRel);
|
|
328
181
|
let contractSchemaId = FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID;
|
|
329
|
-
let contract;
|
|
330
182
|
try {
|
|
331
183
|
const raw = JSON.parse(await readFile(contractPath, "utf8"));
|
|
332
184
|
const parsed = frontendImplementationContractSchema.safeParse(raw);
|
|
333
185
|
if (!parsed.success) {
|
|
334
186
|
throw new Error("invalid frontend implementation contract");
|
|
335
187
|
}
|
|
336
|
-
contract = parsed.data;
|
|
337
188
|
contractSchemaId = FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID;
|
|
338
189
|
}
|
|
339
190
|
catch {
|
|
@@ -367,6 +218,7 @@ export async function runFrontendFailureAssessGate(input) {
|
|
|
367
218
|
// missing optional nodes ok
|
|
368
219
|
}
|
|
369
220
|
}
|
|
221
|
+
assertWriteSetSubset(implementWriteSet, implementWriteSet);
|
|
370
222
|
if (failed.length === 0) {
|
|
371
223
|
const assessment = {
|
|
372
224
|
schemaVersion: 1,
|
|
@@ -389,49 +241,16 @@ export async function runFrontendFailureAssessGate(input) {
|
|
|
389
241
|
return assessment;
|
|
390
242
|
}
|
|
391
243
|
const primary = failed[0];
|
|
392
|
-
const evidence = deriveFailureEvidence({ contract, failed });
|
|
393
244
|
const failureClass = classifyFrontendFailure({
|
|
394
245
|
nodeId: primary.nodeId,
|
|
395
246
|
failureCategory: primary.record.failureCategory,
|
|
396
247
|
stdout: primary.record.stdout,
|
|
397
248
|
stderr: primary.record.stderr,
|
|
398
|
-
commandLabel: evidence.commandLabel,
|
|
399
|
-
commandCategory: evidence.commandCategory,
|
|
400
|
-
exitCode: evidence.exitCode,
|
|
401
|
-
traceTarget: evidence.traceTarget ?? null,
|
|
402
|
-
changedPaths: evidence.changedPaths,
|
|
403
249
|
});
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
let eligible;
|
|
409
|
-
let reason;
|
|
410
|
-
if (failureClass === "review") {
|
|
411
|
-
const finding = primary.record.reviewFinding ?? {};
|
|
412
|
-
eligible = attempt <= 1 && isReviewRepairable(finding, implementWriteSet);
|
|
413
|
-
reason = eligible
|
|
414
|
-
? `repairable review finding on ${primary.nodeId}`
|
|
415
|
-
: `review finding lacks file/line-symbol/requirement evidence or is outside writeSet on ${primary.nodeId}`;
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
const hasStructuredEvidence = evidence.exitCode !== undefined ||
|
|
419
|
-
evidence.commandCategory !== undefined ||
|
|
420
|
-
Boolean(evidence.verificationTargetId);
|
|
421
|
-
eligible = baseEligible && hasStructuredEvidence;
|
|
422
|
-
reason = eligible
|
|
423
|
-
? `repairable failureClass=${failureClass} on ${primary.nodeId}`
|
|
424
|
-
: !baseEligible
|
|
425
|
-
? `non-repairable or ineligible: failureClass=${failureClass} attempt=${attempt}`
|
|
426
|
-
: `missing structured evidence for failureClass=${failureClass} on ${primary.nodeId}`;
|
|
427
|
-
}
|
|
428
|
-
// fixScope and changedPaths must stay inside the implement writeSet.
|
|
429
|
-
if (evidence.fixScope?.length) {
|
|
430
|
-
assertWriteSetSubset(evidence.fixScope, implementWriteSet);
|
|
431
|
-
}
|
|
432
|
-
if (evidence.changedPaths?.length) {
|
|
433
|
-
assertWriteSetSubset(evidence.changedPaths, implementWriteSet);
|
|
434
|
-
}
|
|
250
|
+
const eligible = isFrontendRepairable(failureClass) && attempt <= 1;
|
|
251
|
+
const reason = eligible
|
|
252
|
+
? `repairable failureClass=${failureClass} on ${primary.nodeId}`
|
|
253
|
+
: `non-repairable or ineligible: failureClass=${failureClass} attempt=${attempt}`;
|
|
435
254
|
const assessment = {
|
|
436
255
|
schemaVersion: 1,
|
|
437
256
|
schemaId: FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID,
|
|
@@ -448,25 +267,6 @@ export async function runFrontendFailureAssessGate(input) {
|
|
|
448
267
|
allowedRepairWriteSet: [...implementWriteSet],
|
|
449
268
|
evidenceRefs: [contractRel, ...failed.map((item) => `${item.nodeId}.json`)],
|
|
450
269
|
browserStatus: "not-run",
|
|
451
|
-
...(evidence.verificationTargetId
|
|
452
|
-
? { verificationTargetId: evidence.verificationTargetId }
|
|
453
|
-
: {}),
|
|
454
|
-
...(evidence.commandLabel ? { commandLabel: evidence.commandLabel } : {}),
|
|
455
|
-
...(evidence.exitCode !== undefined ? { exitCode: evidence.exitCode } : {}),
|
|
456
|
-
...(evidence.commandCategory
|
|
457
|
-
? { commandCategory: evidence.commandCategory }
|
|
458
|
-
: {}),
|
|
459
|
-
...(evidence.requirementIds?.length
|
|
460
|
-
? { requirementIds: evidence.requirementIds }
|
|
461
|
-
: {}),
|
|
462
|
-
...(evidence.acceptanceCriteriaIds?.length
|
|
463
|
-
? { acceptanceCriteriaIds: evidence.acceptanceCriteriaIds }
|
|
464
|
-
: {}),
|
|
465
|
-
...(evidence.fixScope?.length ? { fixScope: evidence.fixScope } : {}),
|
|
466
|
-
...(evidence.changedPaths?.length
|
|
467
|
-
? { changedPaths: evidence.changedPaths }
|
|
468
|
-
: {}),
|
|
469
|
-
...(evidence.traceRef ? { traceRef: evidence.traceRef } : {}),
|
|
470
270
|
};
|
|
471
271
|
await writeAssessment(input.runDir, assessment);
|
|
472
272
|
return assessment;
|
|
@@ -522,8 +322,7 @@ export async function runFrontendRepairContractGate(input) {
|
|
|
522
322
|
if (!assessment.eligible) {
|
|
523
323
|
throw new Error(`repair-contract: non-repairable failure (${assessment.failureClass}): ${assessment.reason}`);
|
|
524
324
|
}
|
|
525
|
-
if (!isFrontendRepairable(assessment.failureClass)
|
|
526
|
-
assessment.failureClass !== "review") {
|
|
325
|
+
if (!isFrontendRepairable(assessment.failureClass)) {
|
|
527
326
|
throw new Error(`repair-contract: failureClass ${assessment.failureClass} is not repairable`);
|
|
528
327
|
}
|
|
529
328
|
return { ok: true, assessment };
|
|
@@ -40,48 +40,6 @@ function symbolEvidenceCandidates(symbol) {
|
|
|
40
40
|
function isConfigurationVerificationFile(file) {
|
|
41
41
|
return /(?:^|\/)(?:package\.json|tsconfig(?:\.[^/]+)?\.json|(?:vite|webpack|rollup|docusaurus)\.config\.[cm]?[jt]s)$/.test(file.replace(/\\/g, "/"));
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Pure verification-target evaluation shared by the trace gate and the repair
|
|
45
|
-
* assess gate. It derives, for each contract verification target, whether the
|
|
46
|
-
* target bound to frozen evidence (commandLabel owners), and whether the file
|
|
47
|
-
* and optional symbol exist in the workspace. It performs no writes and throws
|
|
48
|
-
* nothing: failures are returned as `hardIssues` plus per-target `status`.
|
|
49
|
-
*/
|
|
50
|
-
export async function evaluateVerificationTargets(input) {
|
|
51
|
-
const targets = [];
|
|
52
|
-
const hardIssues = [];
|
|
53
|
-
for (const target of input.contract.verificationTargets) {
|
|
54
|
-
const issues = [];
|
|
55
|
-
const owners = input.labelOwners.get(target.commandLabel);
|
|
56
|
-
if (!owners?.length) {
|
|
57
|
-
issues.push(`commandLabel not executed in current-run frozen evidence: ${target.commandLabel}`);
|
|
58
|
-
}
|
|
59
|
-
const fileIssues = await assertFileAndSymbol({
|
|
60
|
-
workspaceRoot: input.workspaceRoot,
|
|
61
|
-
file: target.file,
|
|
62
|
-
// Configuration files prove that the command's entrypoint exists;
|
|
63
|
-
// they do not expose source symbols. LLMs sometimes derive a
|
|
64
|
-
// filename fragment such as "build" from tsconfig.build.json.
|
|
65
|
-
symbol: isConfigurationVerificationFile(target.file)
|
|
66
|
-
? undefined
|
|
67
|
-
: target.symbol,
|
|
68
|
-
});
|
|
69
|
-
issues.push(...fileIssues);
|
|
70
|
-
const status = issues.length ? "failed" : "ok";
|
|
71
|
-
if (issues.length)
|
|
72
|
-
hardIssues.push(...issues.map((i) => `${target.id}: ${i}`));
|
|
73
|
-
targets.push({
|
|
74
|
-
id: target.id,
|
|
75
|
-
commandLabel: target.commandLabel,
|
|
76
|
-
file: target.file,
|
|
77
|
-
symbol: target.symbol,
|
|
78
|
-
status,
|
|
79
|
-
matchedNodeIds: owners ?? [],
|
|
80
|
-
issues,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
return { targets, hardIssues };
|
|
84
|
-
}
|
|
85
43
|
async function assertFileAndSymbol(input) {
|
|
86
44
|
const issues = [];
|
|
87
45
|
const absolute = path.resolve(input.workspaceRoot, input.file);
|
|
@@ -217,11 +175,38 @@ export async function runFrontendVerificationTraceGate(input) {
|
|
|
217
175
|
}
|
|
218
176
|
}
|
|
219
177
|
}
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
178
|
+
const targets = [];
|
|
179
|
+
const hardIssues = [];
|
|
180
|
+
for (const target of contract.verificationTargets) {
|
|
181
|
+
const issues = [];
|
|
182
|
+
const owners = labelOwners.get(target.commandLabel);
|
|
183
|
+
if (!owners?.length) {
|
|
184
|
+
issues.push(`commandLabel not executed in current-run frozen evidence: ${target.commandLabel}`);
|
|
185
|
+
}
|
|
186
|
+
const fileIssues = await assertFileAndSymbol({
|
|
187
|
+
workspaceRoot: input.workspaceRoot,
|
|
188
|
+
file: target.file,
|
|
189
|
+
// Configuration files prove that the command's entrypoint exists;
|
|
190
|
+
// they do not expose source symbols. LLMs sometimes derive a
|
|
191
|
+
// filename fragment such as "build" from tsconfig.build.json.
|
|
192
|
+
symbol: isConfigurationVerificationFile(target.file)
|
|
193
|
+
? undefined
|
|
194
|
+
: target.symbol,
|
|
195
|
+
});
|
|
196
|
+
issues.push(...fileIssues);
|
|
197
|
+
const status = issues.length ? "failed" : "ok";
|
|
198
|
+
if (issues.length)
|
|
199
|
+
hardIssues.push(...issues.map((i) => `${target.id}: ${i}`));
|
|
200
|
+
targets.push({
|
|
201
|
+
id: target.id,
|
|
202
|
+
commandLabel: target.commandLabel,
|
|
203
|
+
file: target.file,
|
|
204
|
+
symbol: target.symbol,
|
|
205
|
+
status,
|
|
206
|
+
matchedNodeIds: owners ?? [],
|
|
207
|
+
issues,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
225
210
|
if (hardIssues.length) {
|
|
226
211
|
throw new Error(`trace: ${hardIssues.join("; ")}`);
|
|
227
212
|
}
|
|
@@ -2199,26 +2199,6 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
2199
2199
|
return { ...task, depends_on };
|
|
2200
2200
|
});
|
|
2201
2201
|
}
|
|
2202
|
-
/**
|
|
2203
|
-
* AC-1: shared frontend writer node defaults. frontend-implement-pi and
|
|
2204
|
-
* frontend-repair-pi share one prompt/contract/writeSet-guard surface and an
|
|
2205
|
-
* identical writeSet; only id, depends_on, runIf, outputContract, and
|
|
2206
|
-
* subtask_prompt differ per phase.
|
|
2207
|
-
*/
|
|
2208
|
-
function buildFrontendWriterNodeDefaults(input) {
|
|
2209
|
-
return {
|
|
2210
|
-
role: "implementer",
|
|
2211
|
-
executor: "pi",
|
|
2212
|
-
toolProfile: "write",
|
|
2213
|
-
complexity: input.complexity,
|
|
2214
|
-
writePolicy: "exclusive",
|
|
2215
|
-
writeSet: input.writeSet,
|
|
2216
|
-
allowedPaths: input.allowedPaths,
|
|
2217
|
-
forbiddenPaths: input.forbiddenPaths,
|
|
2218
|
-
skills: FRONTEND_BOUNDED_IMPLEMENT_SKILLS,
|
|
2219
|
-
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
2220
|
-
};
|
|
2221
|
-
}
|
|
2222
2202
|
async function buildFrontendHybridDagFromTask(sources) {
|
|
2223
2203
|
const { taskConfig } = sources;
|
|
2224
2204
|
const mockCapability = sources.frontendMockCapability ?? {
|
|
@@ -2359,12 +2339,6 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2359
2339
|
`Every requirement MUST have a non-empty expectedOutcome. Every interaction MUST have non-empty trigger and expectedBehavior. UI states with applicable=true MUST have non-empty expectedBehavior. Empty strings or omitted fields for these will cause contract rejection.`,
|
|
2360
2340
|
].join("\n")
|
|
2361
2341
|
: "";
|
|
2362
|
-
const verificationTargetFileInstruction = [
|
|
2363
|
-
`## Verification target file semantics`,
|
|
2364
|
-
`verificationTargets[].file is the code file that the target verifies (the file the writer changes), NOT where the command is defined.`,
|
|
2365
|
-
`Non-static targets (type unit/component/integration/mock) MUST set file to a concrete code file inside the implementation writeSet (task allowedPaths); the prewrite gate rejects any non-static target whose file falls outside the writeSet.`,
|
|
2366
|
-
`Command-level checks that run project-wide (all tests, typecheck, build, governance) MUST use type "static" and must NOT be bound as non-static targets with file=package.json/tsconfig.json/vite.config.ts/scripts/*. Static targets are exempt from the writeSet containment check.`,
|
|
2367
|
-
].join("\n");
|
|
2368
2342
|
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
2369
2343
|
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
2370
2344
|
const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
|
|
@@ -2558,10 +2532,6 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2558
2532
|
writePolicy: "read-only",
|
|
2559
2533
|
outputMode: "structured-required",
|
|
2560
2534
|
retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
|
|
2561
|
-
structuredContractOutput: {
|
|
2562
|
-
schemaId: "frontend-implementation-contract-v1",
|
|
2563
|
-
retryOnInvalid: true,
|
|
2564
|
-
},
|
|
2565
2535
|
allowedPaths: readOnlyPaths,
|
|
2566
2536
|
forbiddenPaths,
|
|
2567
2537
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
@@ -2575,8 +2545,6 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2575
2545
|
"End with exactly one fenced json object conforming to frontend-implementation-contract-v1. This node is the single contract JSON producer: the fenced block is the authoritative contract the prewrite gate materializes. Do not emit any raw JSON, JSON in prose, or a second fenced block anywhere in the response; the plan text must not contain other balanced JSON objects.",
|
|
2576
2546
|
"Each requirement must state its user-observable or logic-observable expectedOutcome. Each interaction must state its trigger and expectedBehavior. IDs plus file paths are not sufficient behavior semantics.",
|
|
2577
2547
|
requirementCoverageInstruction,
|
|
2578
|
-
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2579
|
-
verificationTargetFileInstruction,
|
|
2580
2548
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2581
2549
|
fixedVerificationContext,
|
|
2582
2550
|
sourceContext,
|
|
@@ -2616,10 +2584,6 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2616
2584
|
writePolicy: "read-only",
|
|
2617
2585
|
outputMode: "structured-required",
|
|
2618
2586
|
retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
|
|
2619
|
-
structuredContractOutput: {
|
|
2620
|
-
schemaId: "frontend-implementation-contract-v1",
|
|
2621
|
-
retryOnInvalid: true,
|
|
2622
|
-
},
|
|
2623
2587
|
allowedPaths: readOnlyPaths,
|
|
2624
2588
|
forbiddenPaths,
|
|
2625
2589
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
@@ -2634,7 +2598,6 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2634
2598
|
"End the response with exactly one fenced json object conforming to frontend-implementation-contract-v1. This node is the single contract JSON producer when the design review requests revision: the fenced block is the authoritative contract the prewrite gate materializes. Do not emit any raw JSON, JSON in prose, or a second fenced block anywhere in the response. Bind it to the supplied task sources; map every requirement and applicable UI state to concrete implementation and verification targets or an explicit blocking evidence gap. Do not include secrets or unsafe paths.",
|
|
2635
2599
|
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the revised contract; do not reduce behavior semantics to IDs and paths.",
|
|
2636
2600
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2637
|
-
verificationTargetFileInstruction,
|
|
2638
2601
|
fixedVerificationContext,
|
|
2639
2602
|
sourceContext,
|
|
2640
2603
|
frontendContractSchemaBlock,
|
|
@@ -2753,12 +2716,18 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2753
2716
|
? ["frontend-lint-baseline-shell"]
|
|
2754
2717
|
: []),
|
|
2755
2718
|
],
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2719
|
+
role: "implementer",
|
|
2720
|
+
executor: "pi",
|
|
2721
|
+
toolProfile: "write",
|
|
2722
|
+
complexity: resolveWriterComplexity(taskConfig),
|
|
2723
|
+
writePolicy: "exclusive",
|
|
2724
|
+
writeSet: implementPaths.writeSet,
|
|
2725
|
+
allowedPaths: implementPaths.allowedPaths,
|
|
2726
|
+
forbiddenPaths,
|
|
2727
|
+
skills: FRONTEND_BOUNDED_IMPLEMENT_SKILLS,
|
|
2728
|
+
writerOutcomePolicy: {
|
|
2729
|
+
type: "implementation-outcome-v1",
|
|
2730
|
+
},
|
|
2762
2731
|
outputContract: "First non-empty line must be exactly one of: IMPLEMENTATION_OUTCOME: changed; IMPLEMENTATION_OUTCOME: already-satisfied; IMPLEMENTATION_OUTCOME: blocked. Then a Markdown delivery summary with Contract Ref (path/schema/hash), Changed Files, Requirements Implemented, UI States, Tests Changed, Verification Attempts, Deviations, and Residual Risks. Follow fixed stages: contract confirm → tests → component/state → API/Mock → focused checks → diff cleanup.",
|
|
2763
2732
|
subtask_prompt: [
|
|
2764
2733
|
"Implement against the validated run-owned Frontend Implementation Contract from frontend-prewrite-gate-shell (path/schema/hash). Do not rebuild the contract from Markdown alone.",
|
|
@@ -2812,12 +2781,18 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2812
2781
|
id: "frontend-repair-pi",
|
|
2813
2782
|
depends_on: ["frontend-verify-assess-shell", implementId],
|
|
2814
2783
|
runIf: "$.nodes['frontend-verify-assess-shell'].json.eligible == true",
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2784
|
+
role: "implementer",
|
|
2785
|
+
executor: "pi",
|
|
2786
|
+
toolProfile: "write",
|
|
2787
|
+
complexity: resolveWriterComplexity(taskConfig),
|
|
2788
|
+
writePolicy: "exclusive",
|
|
2789
|
+
writeSet: implementPaths.writeSet,
|
|
2790
|
+
allowedPaths: implementPaths.allowedPaths,
|
|
2791
|
+
forbiddenPaths,
|
|
2792
|
+
skills: FRONTEND_BOUNDED_IMPLEMENT_SKILLS,
|
|
2793
|
+
writerOutcomePolicy: {
|
|
2794
|
+
type: "implementation-outcome-v1",
|
|
2795
|
+
},
|
|
2821
2796
|
outputContract: "First non-empty line must be exactly one of: IMPLEMENTATION_OUTCOME: changed; IMPLEMENTATION_OUTCOME: already-satisfied; IMPLEMENTATION_OUTCOME: blocked. Then a repair summary for an eligible repairable assessment. Must not expand writeSet, re-interpret requirements, skip tests, or enable Mock by default.",
|
|
2822
2797
|
subtask_prompt: [
|
|
2823
2798
|
"Read contracts/frontend-repair-assessment.json and the validated frontend implementation contract.",
|
|
@@ -5679,6 +5654,7 @@ export async function buildHybridDagFromTask(sources, options = {}) {
|
|
|
5679
5654
|
: sources.frontendProjectCapability;
|
|
5680
5655
|
const selection = resolveTaskDagTemplateSelection({
|
|
5681
5656
|
taskKind: sources.taskConfig.taskKind,
|
|
5657
|
+
taskKindExplicit: sources.taskConfig.taskKindExplicit,
|
|
5682
5658
|
title: sources.taskConfig.title,
|
|
5683
5659
|
requirementMarkdown: sources.requirementMarkdown,
|
|
5684
5660
|
allowedPaths: sources.taskConfig.allowedPaths,
|
|
@@ -6453,6 +6429,7 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
|
|
|
6453
6429
|
: sources.frontendProjectCapability;
|
|
6454
6430
|
const templateSelection = resolveTaskDagTemplateSelection({
|
|
6455
6431
|
taskKind: sources.taskConfig.taskKind,
|
|
6432
|
+
taskKindExplicit: sources.taskConfig.taskKindExplicit,
|
|
6456
6433
|
title: sources.taskConfig.title,
|
|
6457
6434
|
requirementMarkdown: sources.requirementMarkdown,
|
|
6458
6435
|
allowedPaths: sources.taskConfig.allowedPaths,
|