@tea-agent/loop-agent 0.35.1-beta.2 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/AGENTS.md +0 -2
  2. package/CHANGELOG.md +25 -24
  3. package/bin/loop-agent.js +1 -37
  4. package/dist/application/dag/generate-task-dag.js +4 -1
  5. package/dist/application/task-lifecycle/advance.js +14 -0
  6. package/dist/cli/program.js +2 -2
  7. package/dist/commands/task-advance.js +1 -0
  8. package/dist/executors/dag-pi-executor.js +0 -44
  9. package/dist/shared/package-metadata.js +0 -42
  10. package/dist/task/config-types.js +2 -0
  11. package/dist/task/contract/project.js +3 -0
  12. package/dist/task/contract/schema.js +1 -0
  13. package/dist/task/source-prepare/build-draft.js +7 -0
  14. package/dist/task/source-prepare/semantic-intake.js +37 -10
  15. package/dist/task/task-demand-routing.js +10 -0
  16. package/dist/worker/console/operator-actions.js +72 -6
  17. package/dist/worker/console/prd-intake-bridge.js +10 -3
  18. package/dist/worker/console/prd-reference-discovery.js +124 -0
  19. package/dist/worker/console/static/assets/{index-hJqCPs_g.css → index-HX1pbOyl.css} +1 -1
  20. package/dist/worker/console/static/assets/{index-CvsQgALl.js → index-M0BLEBfh.js} +25 -25
  21. package/dist/worker/console/static/index.html +2 -2
  22. package/dist/worker/console/static-src/app/useOperatorActions.js +19 -1
  23. package/dist/worker/console/static-src/app/useRecoveryConsole.js +0 -5
  24. package/dist/worker/console/static-src/app/useTaskWizard.js +12 -0
  25. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  26. package/dist/worker/observability/read-model.js +0 -20
  27. package/dist/worker/preflight.js +1 -2
  28. package/dist/workflows/dag/backend-test-scenario-param.js +23 -33
  29. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  30. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  31. package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
  32. package/dist/workflows/dag/frontend-repair.js +18 -219
  33. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  34. package/dist/workflows/dag/init-hybrid.js +26 -41
  35. package/dist/workflows/dag/node-execution.js +0 -89
  36. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  37. package/dist/workflows/dag/runner.js +11 -245
  38. package/dist/workflows/dag/scheduler.js +3 -257
  39. package/dist/workflows/dag/types.js +2 -130
  40. package/package.json +2 -2
  41. package/dist/build-stamp.json +0 -6
  42. package/dist/workflows/dag/contract-output-registry.js +0 -14
  43. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  44. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  45. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  46. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  47. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  48. 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
- if (!isPathWithinWriteSet(entry, implementWriteSet)) {
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
- // AC-5: repairable=true requires structured evidence (a resolved exitCode,
405
- // commandCategory, or trace-bound verification target). review has its own
406
- // stricter gate (AC-4) and is never unconditionally repairable.
407
- const baseEligible = isFrontendRepairable(failureClass) && attempt <= 1;
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 { targets, hardIssues } = await evaluateVerificationTargets({
221
- contract,
222
- labelOwners,
223
- workspaceRoot: input.workspaceRoot,
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 ?? {
@@ -2552,10 +2532,6 @@ async function buildFrontendHybridDagFromTask(sources) {
2552
2532
  writePolicy: "read-only",
2553
2533
  outputMode: "structured-required",
2554
2534
  retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
2555
- structuredContractOutput: {
2556
- schemaId: "frontend-implementation-contract-v1",
2557
- retryOnInvalid: true,
2558
- },
2559
2535
  allowedPaths: readOnlyPaths,
2560
2536
  forbiddenPaths,
2561
2537
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
@@ -2569,7 +2545,6 @@ async function buildFrontendHybridDagFromTask(sources) {
2569
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.",
2570
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.",
2571
2547
  requirementCoverageInstruction,
2572
- "verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
2573
2548
  "Read-only: do not modify code, docs, artifacts, or repository files.",
2574
2549
  fixedVerificationContext,
2575
2550
  sourceContext,
@@ -2609,10 +2584,6 @@ async function buildFrontendHybridDagFromTask(sources) {
2609
2584
  writePolicy: "read-only",
2610
2585
  outputMode: "structured-required",
2611
2586
  retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
2612
- structuredContractOutput: {
2613
- schemaId: "frontend-implementation-contract-v1",
2614
- retryOnInvalid: true,
2615
- },
2616
2587
  allowedPaths: readOnlyPaths,
2617
2588
  forbiddenPaths,
2618
2589
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
@@ -2745,12 +2716,18 @@ async function buildFrontendHybridDagFromTask(sources) {
2745
2716
  ? ["frontend-lint-baseline-shell"]
2746
2717
  : []),
2747
2718
  ],
2748
- ...buildFrontendWriterNodeDefaults({
2749
- complexity: resolveWriterComplexity(taskConfig),
2750
- writeSet: implementPaths.writeSet,
2751
- allowedPaths: implementPaths.allowedPaths,
2752
- forbiddenPaths,
2753
- }),
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
+ },
2754
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.",
2755
2732
  subtask_prompt: [
2756
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.",
@@ -2804,12 +2781,18 @@ async function buildFrontendHybridDagFromTask(sources) {
2804
2781
  id: "frontend-repair-pi",
2805
2782
  depends_on: ["frontend-verify-assess-shell", implementId],
2806
2783
  runIf: "$.nodes['frontend-verify-assess-shell'].json.eligible == true",
2807
- ...buildFrontendWriterNodeDefaults({
2808
- complexity: resolveWriterComplexity(taskConfig),
2809
- writeSet: implementPaths.writeSet,
2810
- allowedPaths: implementPaths.allowedPaths,
2811
- forbiddenPaths,
2812
- }),
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
+ },
2813
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.",
2814
2797
  subtask_prompt: [
2815
2798
  "Read contracts/frontend-repair-assessment.json and the validated frontend implementation contract.",
@@ -5671,6 +5654,7 @@ export async function buildHybridDagFromTask(sources, options = {}) {
5671
5654
  : sources.frontendProjectCapability;
5672
5655
  const selection = resolveTaskDagTemplateSelection({
5673
5656
  taskKind: sources.taskConfig.taskKind,
5657
+ taskKindExplicit: sources.taskConfig.taskKindExplicit,
5674
5658
  title: sources.taskConfig.title,
5675
5659
  requirementMarkdown: sources.requirementMarkdown,
5676
5660
  allowedPaths: sources.taskConfig.allowedPaths,
@@ -6445,6 +6429,7 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
6445
6429
  : sources.frontendProjectCapability;
6446
6430
  const templateSelection = resolveTaskDagTemplateSelection({
6447
6431
  taskKind: sources.taskConfig.taskKind,
6432
+ taskKindExplicit: sources.taskConfig.taskKindExplicit,
6448
6433
  title: sources.taskConfig.title,
6449
6434
  requirementMarkdown: sources.requirementMarkdown,
6450
6435
  allowedPaths: sources.taskConfig.allowedPaths,
@@ -4,7 +4,6 @@ import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { pathMatchesPattern } from "../../shared/git-progress.js";
6
6
  import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
7
- import { FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isFrontendWriterAuthorized, readFrontendPrewriteResult, } from "./scheduler.js";
8
7
  import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
9
8
  import { resolveContextPolicy } from "./context-policy.js";
10
9
  import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
@@ -13,8 +12,6 @@ import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry,
13
12
  import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
14
13
  import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
15
14
  import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
16
- import { getStructuredContractValidator } from "./contract-output-registry.js";
17
- import "./contract-validator-registrations.js";
18
15
  import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
19
16
  import { buildProjectGovernanceContext, readCompletedWriterChangeManifests, writeProjectGovernanceContext, } from "./project-governance-context.js";
20
17
  import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
@@ -141,20 +138,6 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
141
138
  buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
142
139
  ].join("\n");
143
140
  }
144
- if (previousFailureCategory === "invalid-output" &&
145
- task.structuredContractOutput &&
146
- previousProtocolReason) {
147
- return [
148
- basePrompt,
149
- "",
150
- "<retry_instruction>",
151
- "Previous attempt produced an invalid frontend implementation contract:",
152
- previousProtocolReason,
153
- "Return exactly one fenced json block conforming to the frontend-implementation-contract-v1 schema.",
154
- "Fix every reported field violation: do not emit null for optional fields, do not misspell field names, and match the required types exactly.",
155
- "</retry_instruction>",
156
- ].join("\n");
157
- }
158
141
  if (previousFailureCategory === "writer-empty-diff") {
159
142
  const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
160
143
  // When a completeness progress exists for this writer, fold the concrete
@@ -412,21 +395,6 @@ export async function executeDagNode(input) {
412
395
  await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
413
396
  };
414
397
  const failSkillSnapshot = (error) => failBeforePrompt(error, "skill-snapshot-integrity");
415
- const skipFrontendWriter = async (admission) => {
416
- const skippedAt = new Date().toISOString();
417
- node.startedAt ??= skippedAt;
418
- node.frontendWriterAdmission = admission;
419
- node.status = "SKIPPED";
420
- node.skippedReason = "frontend-prewrite-not-authorized";
421
- node.finishedAt = skippedAt;
422
- node.lastActivityAt = skippedAt;
423
- node.durationMs = durationBetween(node.startedAt, node.finishedAt);
424
- node.timing = { retryBackoffMs: 0, settlementCleanupMs: 0 };
425
- state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
426
- await writeNodeRecord(runDir, nodeId, state.nodes[nodeId]);
427
- await input.persistState();
428
- await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
429
- };
430
398
  if (task.finalWriteSetApproval) {
431
399
  const authorization = parseAndValidateFinalWriteSetApproval({ task, spec, state });
432
400
  if (!authorization.ok) {
@@ -455,30 +423,6 @@ export async function executeDagNode(input) {
455
423
  effectiveWriteSet: [...authorization.effectiveWriteSet],
456
424
  };
457
425
  }
458
- if (FRONTEND_WRITER_NODE_IDS.includes(nodeId)) {
459
- const admission = await readFrontendPrewriteResult(runDir);
460
- if (!admission.ok) {
461
- await skipFrontendWriter(undefined);
462
- return;
463
- }
464
- const decision = isFrontendWriterAuthorized(admission.result);
465
- const record = {
466
- schemaVersion: 1,
467
- writerNodeId: nodeId,
468
- decision,
469
- sourceArtifact: FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT,
470
- artifactHash: admission.artifactHash,
471
- checkedAt: new Date().toISOString(),
472
- reason: decision === "denied"
473
- ? `classification: ${admission.result.classification}`
474
- : null,
475
- };
476
- if (decision === "denied") {
477
- await skipFrontendWriter(record);
478
- return;
479
- }
480
- node.frontendWriterAdmission = record;
481
- }
482
426
  let projectGovernanceContext;
483
427
  if (task.governanceStandardReview) {
484
428
  try {
@@ -745,39 +689,6 @@ export async function executeDagNode(input) {
745
689
  previousProtocolReason = undefined;
746
690
  }
747
691
  }
748
- // R1: structured contract nodes self-validate their output so schema,
749
- // typo, and null violations surface as retryable invalid-output at the
750
- // producing node instead of failing the whole run at the prewrite gate.
751
- if (result.ok && task.structuredContractOutput) {
752
- const validator = getStructuredContractValidator(task.structuredContractOutput.schemaId);
753
- if (validator) {
754
- const contractText = canonicalNodeOutput(result);
755
- const contractCheck = await validator({
756
- runDir,
757
- text: contractText,
758
- sourceBinding: spec.sourceBinding,
759
- });
760
- if (!contractCheck.ok) {
761
- result = {
762
- ...result,
763
- ok: false,
764
- failureCategory: "invalid-output",
765
- stderr: [result.stderr, contractCheck.reason]
766
- .filter(Boolean)
767
- .join("\n"),
768
- };
769
- previousProtocolReason = contractCheck.reason;
770
- }
771
- else {
772
- previousProtocolReason = undefined;
773
- }
774
- }
775
- else {
776
- // DAG spec validation rejects unknown schemaIds; this is a
777
- // defensive fallback for a registry that has not been populated.
778
- console.warn(`[run-dag] warning: no structured contract validator registered for schemaId ${task.structuredContractOutput.schemaId}; skipping node self-check`);
779
- }
780
- }
781
692
  const attemptFinishedAt = new Date().toISOString();
782
693
  const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
783
694
  totalAttemptWallDurationMs += attemptWallDurationMs;