@tea-agent/loop-agent 0.35.1-beta.3 → 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 -49
  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 ?? {
@@ -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
- ...buildFrontendWriterNodeDefaults({
2757
- complexity: resolveWriterComplexity(taskConfig),
2758
- writeSet: implementPaths.writeSet,
2759
- allowedPaths: implementPaths.allowedPaths,
2760
- forbiddenPaths,
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
- ...buildFrontendWriterNodeDefaults({
2816
- complexity: resolveWriterComplexity(taskConfig),
2817
- writeSet: implementPaths.writeSet,
2818
- allowedPaths: implementPaths.allowedPaths,
2819
- forbiddenPaths,
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,