@tea-agent/loop-agent 0.44.0-next.7 → 0.44.0-next.9

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 (147) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +25 -41
  22. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  23. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  24. package/dist/executors/dag-pi/tools/design-terminal-tools.js +166 -0
  25. package/dist/executors/dag-pi/tools/plan-decision-tools.js +519 -0
  26. package/dist/executors/dag-pi/tools/review-terminal-tools.js +166 -0
  27. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  28. package/dist/executors/dag-pi-executor.js +198 -118
  29. package/dist/executors/pi-executor.js +6 -1
  30. package/dist/executors/pi-sdk-executor.js +76 -11
  31. package/dist/executors/shell-executor.js +88 -12
  32. package/dist/infrastructure/console/operation-store.js +2 -0
  33. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  34. package/dist/shared/operator/capabilities.js +10 -5
  35. package/dist/task/config-types.js +22 -9
  36. package/dist/task/contract/project.js +13 -0
  37. package/dist/task/contract/schema.js +2 -8
  38. package/dist/task/frontend-preflight.js +1 -1
  39. package/dist/task/source-prepare/parse-intent.js +124 -5
  40. package/dist/task/source-prepare/prepare.js +6 -1
  41. package/dist/task/source-prepare/semantic-intake.js +18 -3
  42. package/dist/task/source-prepare/source-execution.js +65 -0
  43. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  44. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  45. package/dist/worker/console/operator-actions.js +0 -1
  46. package/dist/worker/console/operator-user-error.js +1 -1
  47. package/dist/worker/console/prd-intake-bridge.js +5 -15
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DTuku_-7.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-CQbCsSqb.js → arc-dT5T4sVL.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C-xUntTR.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DhNlwGAl.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B7j3nx8o.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BCrcxEh2.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CsaePDb3.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H--SRQc1rN.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C5BFY2Po.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Caso6uN7.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-AY2ZSAx2.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DiMqZQ_8.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-gpvvriOp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUdyoJhI.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bp1xW3T4.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-D-JIcOIT.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C-NKFflx.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CSOcvOAA.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BQSBltMy.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0LVC45G.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cypg3nqy.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BBDU88XJ.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BZusLqSb.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Cfolotcr.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CjfyUy0m.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DJdz7txE.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CQqgPO6y.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BOpGfS2m.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-C6UCu0qx.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  80. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  81. package/dist/worker/console/static/assets/{index-DWKejNVJ.js → index-CTMZsDmh.js} +98 -98
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BWeaVEUB.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C8gmlBbk.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DF3bm8Bp.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D2fG2Vxi.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-BGKgMc2T.js → linear-DGRlieMH.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-DF6mMPJ9.js → mermaid.core-4P9yPC9c.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-AMhePheC.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DN1Cl0W0.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Cx9ngUeN.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BcYPTQJb.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BqHUZWnH.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CmqWZ0-B.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P--4YetH1y.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DGD4VMfD.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-QVw9q7rh.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CuemV9pe.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C7oKzZKs.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C2Of60nT.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DHtpQHyL.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-COi9OhSy.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DPHQfRrt.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/workflows/dag/budget-enforcement.js +31 -2
  107. package/dist/workflows/dag/frontend-closeout.js +3 -1
  108. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  109. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  110. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  111. package/dist/workflows/dag/frontend-implementation-contract.js +146 -59
  112. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  113. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  114. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  115. package/dist/workflows/dag/frontend-recovery-plan.js +11 -10
  116. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  117. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  118. package/dist/workflows/dag/frontend-review-context.js +29 -1
  119. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  120. package/dist/workflows/dag/frontend-risk.js +8 -3
  121. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  122. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  123. package/dist/workflows/dag/frontend-session-context.js +10 -0
  124. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  125. package/dist/workflows/dag/frontend-typed-event-store.js +11 -6
  126. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  127. package/dist/workflows/dag/frontend-writer-admission.js +2 -1
  128. package/dist/workflows/dag/init-hybrid.js +108 -37
  129. package/dist/workflows/dag/node-execution.js +11 -11
  130. package/dist/workflows/dag/rerun-feedback.js +35 -4
  131. package/dist/workflows/dag/rerun-task.js +62 -67
  132. package/dist/workflows/dag/runner.js +58 -12
  133. package/dist/workflows/dag/types.js +15 -8
  134. package/docs/architecture/runtime-boundaries.md +1 -1
  135. package/docs/templates/backend-test-dag.json +4 -4
  136. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  137. package/package.json +1 -1
  138. package/skills/frontend-design-review/SKILL.md +8 -11
  139. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  140. package/skills/frontend-review/SKILL.md +5 -1
  141. package/skills/loop-agent/references/command-reference.md +8 -0
  142. package/dist/worker/console/static/assets/channel-5_os7t_x.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +0 -1
  144. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +0 -1
  145. package/dist/worker/console/static/assets/index-CUT440A8.css +0 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +0 -1
  147. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +0 -8
@@ -1,3 +1,4 @@
1
+ import { capabilityBoundarySchema } from "../config-types.js";
1
2
  import { z } from "zod";
2
3
  import { backendTestConfigSchema, frontendOpenspecConfigSchema, taskKindSchema, taskVerifyCommandSchema, } from "../config-types.js";
3
4
  import { TASK_CONFIG_SCHEMA_VERSION, TASK_CONTRACT_CANONICALIZER_VERSION, TASK_CONTRACT_DRAFT_SCHEMA_VERSION, TASK_CONTRACT_PROJECTION_VERSION, TASK_CONTRACT_REF_SCHEMA_VERSION, } from "./constants.js";
@@ -84,14 +85,7 @@ export const taskContractDraftSchema = z
84
85
  })
85
86
  .strict())
86
87
  .optional(),
87
- capabilityBoundary: z
88
- .object({
89
- allowed: z.array(z.string().min(1)).optional(),
90
- forbidden: z.array(z.string().min(1)).optional(),
91
- requiredClauses: z.array(z.string().min(1)).optional(),
92
- })
93
- .strict()
94
- .optional(),
88
+ capabilityBoundary: capabilityBoundarySchema.optional(),
95
89
  interactionIds: z.array(z.string().min(1)).optional(),
96
90
  interviewAssessmentRef: z
97
91
  .object({
@@ -2,7 +2,7 @@ import { mkdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
4
  import { executePiStep } from "../executors/pi-executor.js";
5
- import { resolveDagPiModelConfig } from "../executors/dag-pi-executor.js";
5
+ import { resolveDagPiModelConfig } from "../executors/dag-pi/model-config.js";
6
6
  import { writeJsonAtomic } from "../infrastructure/harness/atomic-write.js";
7
7
  import { pathMatchesPattern } from "../shared/git-progress.js";
8
8
  import { loadHarnessManifest } from "../governance/harness.js";
@@ -261,8 +261,37 @@ export function extractRequirementFactsFromMarkdown(markdown, options) {
261
261
  let current = { key: "other", lines: [] };
262
262
  sections.push(current);
263
263
  let acCounter = options?.acCounterStart ?? 1;
264
+ // Ids the source states explicitly anywhere in the document (as a bullet
265
+ // `- AC-002: …`, a `#### AC-002 …` heading, or a list/heading under a
266
+ // requirement section). Auto-numbering must not hand one of these ids to an
267
+ // unrelated unlabeled bullet: doing so silently REPLACES the authored
268
+ // criterion with the auto-assigned one, so the requirement the PRD actually
269
+ // stated disappears while the import still looks complete. That is how a
270
+ // `## 需求` list of AC-001..AC-004 became three acceptance bullets taken
271
+ // from `## 验收`, with AC-001/AC-002 overwritten and only AC-003/AC-004
272
+ // reported as dropped.
273
+ const explicitlyClaimedIds = new Set();
274
+ for (const line of lines) {
275
+ const heading = isAtxHeading(line);
276
+ if (heading) {
277
+ const embedded = parseEmbeddedAcHeading(heading.text);
278
+ if (embedded)
279
+ explicitlyClaimedIds.add(embedded.id);
280
+ continue;
281
+ }
282
+ const item = parseListItem(line);
283
+ if (!item)
284
+ continue;
285
+ const labeled = /^(AC[-_]?\d+)\s*[::]/iu.exec(item);
286
+ if (labeled)
287
+ explicitlyClaimedIds.add(normalizeAcceptanceId(labeled[1]));
288
+ }
264
289
  const nextId = () => {
265
- const id = `AC-${String(acCounter).padStart(3, "0")}`;
290
+ let id = `AC-${String(acCounter).padStart(3, "0")}`;
291
+ while (explicitlyClaimedIds.has(id)) {
292
+ acCounter += 1;
293
+ id = `AC-${String(acCounter).padStart(3, "0")}`;
294
+ }
266
295
  acCounter += 1;
267
296
  return id;
268
297
  };
@@ -277,7 +306,15 @@ export function extractRequirementFactsFromMarkdown(markdown, options) {
277
306
  key: "acceptance",
278
307
  lines: [`- ${embeddedAc.id}: ${embeddedAc.text}`],
279
308
  });
280
- current = { key: "other", lines: [] };
309
+ // The body under an embedded AC heading must not become an extra
310
+ // criterion, but it must not be thrown away either: the derived
311
+ // projection and the frontend shape classifier read requirement
312
+ // prose, so a capability signal stated only in the body (for
313
+ // example a required remote /api/… fetch) would otherwise be
314
+ // invisible at generation time and only resurface at
315
+ // post-Contract as `reshape-required`. Attribute the body to its
316
+ // heading instead.
317
+ current = { key: "acBody", lines: [], acBodyId: embeddedAc.id };
281
318
  sections.push(current);
282
319
  continue;
283
320
  }
@@ -295,6 +332,9 @@ export function extractRequirementFactsFromMarkdown(markdown, options) {
295
332
  const invariants = [];
296
333
  const openQuestions = [];
297
334
  const assumptions = [];
335
+ // Requirement ids that a requirement list loses when its first item is
336
+ // lifted into the Objective (see the objective branch below).
337
+ const droppedRequirementIds = [];
298
338
  for (const section of sections) {
299
339
  if (section.key === "objective" && !objective) {
300
340
  const paragraph = firstParagraph(section.lines);
@@ -324,6 +364,14 @@ export function extractRequirementFactsFromMarkdown(markdown, options) {
324
364
  acceptanceCriteria.push(ac);
325
365
  }
326
366
  }
367
+ else if (section.key === "acBody") {
368
+ const body = section.lines.join("\n").trim();
369
+ if (!body || !section.acBodyId)
370
+ continue;
371
+ const owner = acceptanceCriteria.find((criterion) => criterion.id === section.acBodyId);
372
+ if (owner)
373
+ owner.body = owner.body ? `${owner.body}\n${body}` : body;
374
+ }
327
375
  else if (section.key === "constraints") {
328
376
  invariants.push(...collectListItems(section.lines));
329
377
  }
@@ -343,6 +391,30 @@ export function extractRequirementFactsFromMarkdown(markdown, options) {
343
391
  paragraph.length > OBJECTIVE_MAX_CHARS
344
392
  ? paragraph.slice(0, OBJECTIVE_MAX_CHARS)
345
393
  : paragraph;
394
+ // Requirement prose living in an unrecognized section (a bare
395
+ // `## 需求` is not a known heading alias) is scraped for the objective
396
+ // and then abandoned: the remaining AC-shaped bullets never reach
397
+ // `acceptanceCriteria`. A later `requirementOwnership` declaration
398
+ // referencing one of those ids then fails at plan finalize with
399
+ // `unknown requirement id`. Surface the ids so callers can warn.
400
+ //
401
+ // A bullet counts as dropped when it carries an AC id that no
402
+ // criterion claims. That includes the bullet scraped into the
403
+ // objective: it survives only as free objective prose, not as a
404
+ // requirement any plan fact can reference.
405
+ const accepted = new Set(acceptanceCriteria.map((criterion) => criterion.id));
406
+ for (const line of lines) {
407
+ const item = parseListItem(line);
408
+ if (!item)
409
+ continue;
410
+ const demoted = /^(AC[-_]?\d+)\s*[::]/iu.exec(item);
411
+ if (!demoted)
412
+ continue;
413
+ const id = normalizeAcceptanceId(demoted[1]);
414
+ if (accepted.has(id))
415
+ continue;
416
+ droppedRequirementIds.push(id);
417
+ }
346
418
  }
347
419
  }
348
420
  const acceptance = dedupeAcceptance(acceptanceCriteria);
@@ -357,6 +429,7 @@ export function extractRequirementFactsFromMarkdown(markdown, options) {
357
429
  assumptions: dedupeText(assumptions),
358
430
  sourceLabels: options?.sourceLabel ? [options.sourceLabel] : [],
359
431
  acceptanceConflicts: acceptance.conflicts,
432
+ droppedRequirementIds: [...new Set(droppedRequirementIds)],
360
433
  };
361
434
  }
362
435
  function dedupeText(items) {
@@ -373,6 +446,7 @@ function dedupeText(items) {
373
446
  }
374
447
  function dedupeAcceptance(items) {
375
448
  const byId = new Map();
449
+ const bodyById = new Map();
376
450
  const order = [];
377
451
  const conflicts = new Map();
378
452
  for (const item of items) {
@@ -380,12 +454,25 @@ function dedupeAcceptance(items) {
380
454
  const text = item.text.trim();
381
455
  if (!id || !text)
382
456
  continue;
457
+ const body = item.body?.trim();
383
458
  const existing = byId.get(id);
384
459
  if (existing === undefined) {
385
460
  byId.set(id, text);
461
+ if (body)
462
+ bodyById.set(id, body);
386
463
  order.push(id);
387
464
  continue;
388
465
  }
466
+ // A duplicate id keeps the first text (or records a conflict) but still
467
+ // accumulates the criterion body, which carries the requirement prose the
468
+ // derived projection and shape classifier depend on.
469
+ if (body) {
470
+ const prior = bodyById.get(id);
471
+ if (!prior)
472
+ bodyById.set(id, body);
473
+ else if (!prior.includes(body))
474
+ bodyById.set(id, `${prior}\n${body}`);
475
+ }
389
476
  if (existing !== text) {
390
477
  const texts = conflicts.get(id) ?? new Set([existing]);
391
478
  texts.add(text);
@@ -393,12 +480,16 @@ function dedupeAcceptance(items) {
393
480
  }
394
481
  }
395
482
  return {
396
- items: order.map((id) => ({ id, text: byId.get(id) })),
483
+ items: order.map((id) => {
484
+ const body = bodyById.get(id);
485
+ return body ? { id, text: byId.get(id), body } : { id, text: byId.get(id) };
486
+ }),
397
487
  conflicts: [...conflicts].map(([id, texts]) => ({ id, texts: [...texts] })),
398
488
  };
399
489
  }
400
490
  export function mergeAcceptanceCriteria(parts) {
401
491
  const byId = new Map();
492
+ const bodyById = new Map();
402
493
  const order = [];
403
494
  for (const group of parts) {
404
495
  for (const item of group) {
@@ -411,16 +502,29 @@ export function mergeAcceptanceCriteria(parts) {
411
502
  order.push(id);
412
503
  }
413
504
  byId.get(id)?.add(text);
505
+ // Bodies accumulate across documents; a conflict on the statement
506
+ // text must not discard the prose that carries the requirement's
507
+ // substance.
508
+ const body = item.body?.trim();
509
+ if (body) {
510
+ const prior = bodyById.get(id);
511
+ if (!prior)
512
+ bodyById.set(id, body);
513
+ else if (!prior.includes(body))
514
+ bodyById.set(id, `${prior}\n${body}`);
515
+ }
414
516
  }
415
517
  }
416
518
  const conflicts = [];
417
519
  const items = [];
418
520
  for (const id of order) {
419
521
  const texts = [...(byId.get(id) ?? [])];
522
+ const body = bodyById.get(id);
420
523
  if (texts.length === 1) {
421
524
  const [onlyText] = texts;
422
- if (onlyText !== undefined)
423
- items.push({ id, text: onlyText });
525
+ if (onlyText !== undefined) {
526
+ items.push(body ? { id, text: onlyText, body } : { id, text: onlyText });
527
+ }
424
528
  }
425
529
  else if (texts.length > 1) {
426
530
  conflicts.push({ id, texts });
@@ -536,6 +640,21 @@ export function parseVerifyTimeoutFlag(raw) {
536
640
  }
537
641
  return { label, timeoutMs };
538
642
  }
643
+ /** Parse `<label>:<static|behavior>` for `--verify-lane`. Declaring the lane
644
+ * overrides text classification; it is the operator's escape hatch for a
645
+ * command that provably cannot emit a test report. */
646
+ export function parseVerifyLaneFlag(raw) {
647
+ const idx = raw.indexOf(":");
648
+ if (idx <= 0 || idx === raw.length - 1) {
649
+ throw new Error(`invalid --verify-lane value "${raw}"; expected <label>:<static|behavior>, e.g. --verify-lane "repo check:static"`);
650
+ }
651
+ const label = raw.slice(0, idx).trim();
652
+ const mode = raw.slice(idx + 1).trim().toLowerCase();
653
+ if (mode !== "static" && mode !== "behavior") {
654
+ throw new Error(`invalid --verify-lane value "${raw}"; lane must be "static" or "behavior"`);
655
+ }
656
+ return { label, mode };
657
+ }
539
658
  export function parseAcceptanceFlag(raw) {
540
659
  const idx = raw.indexOf(":");
541
660
  if (idx <= 0 || idx === raw.length - 1) {
@@ -1,3 +1,4 @@
1
+ import { observeSourceExecution } from "./source-execution.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { lstat, readFile } from "node:fs/promises";
3
4
  import path from "node:path";
@@ -463,6 +464,9 @@ function emptyObservedHashPlaceholder() {
463
464
  * No LLM. Production md writes only via applyTaskContract projection.
464
465
  */
465
466
  export async function prepareTaskSource(input) {
467
+ return observeSourceExecution({ repoRoot: input.repoRoot, taskId: input.taskId, taskKind: input.flags.taskKind, phase: "prepare", dryRun: input.mode === "dry-run", inputIdentity: { intent: input.intent, flags: input.flags } }, () => prepareTaskSourceInExecution(input));
468
+ }
469
+ async function prepareTaskSourceInExecution(input) {
466
470
  const { repoRoot, taskId } = input;
467
471
  const contractPaths = getTaskContractPaths(repoRoot, taskId);
468
472
  const state = await observeTaskContractState(repoRoot, taskId);
@@ -798,13 +802,14 @@ export async function prepareTaskSource(input) {
798
802
  let ledgerSummary;
799
803
  const hasAuthoritativeImportedDocs = Boolean(hasImportedPrd &&
800
804
  sourceIntegrity?.parseableDocuments.some((doc) => doc.role === "requirement" || doc.role === "acceptance"));
801
- // Frontend tasks created from text still need the same source-fidelity
805
+ // Frontend tasks created from text or structured drafts need the same source-fidelity
802
806
  // contract as imported-PRD tasks. In that case the projected requirement
803
807
  // document is the canonical, task-owned source that the DAG can bind to;
804
808
  // without it init-hybrid would silently emit a v1 binding and the first
805
809
  // frontend-contract node would fail before making a model call.
806
810
  if (built.draft.taskKind === "frontend-implementation" &&
807
811
  (hasAuthoritativeImportedDocs ||
812
+ input.intent.kind === "draft" ||
808
813
  (input.intent.kind === "facts" && Boolean(input.intent.text)))) {
809
814
  try {
810
815
  const authoritativeDocs = sourceIntegrity?.parseableDocuments.filter((doc) => (doc.role === "requirement" || doc.role === "acceptance") &&
@@ -1,3 +1,6 @@
1
+ import { observeSourceExecution } from "./source-execution.js";
2
+ import { openTaskSourceProviderBudget } from "./source-provider-budget.js";
3
+ import { sha256OfCanonicalJson } from "../contract/hash.js";
1
4
  import { randomUUID } from "node:crypto";
2
5
  import { observeFrontendSession } from "../../workflows/dag/frontend-session-budget.js";
3
6
  /**
@@ -13,12 +16,12 @@ import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
13
16
  import path from "node:path";
14
17
  import { z } from "zod";
15
18
  import { executePiStep } from "../../executors/pi-executor.js";
16
- import { resolveDagPiModelConfig } from "../../executors/dag-pi-executor.js";
19
+ import { resolveDagPiModelConfig } from "../../executors/dag-pi/model-config.js";
17
20
  import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
18
21
  import { listPiSdkAvailableModels } from "../../executors/pi-sdk-executor.js";
19
22
  import { loadHarnessManifest } from "../../governance/harness.js";
20
23
  import { TASK_CONTRACT_DRAFT_SCHEMA_VERSION } from "../contract/constants.js";
21
- import { getTaskPaths } from "../runtime.js";
24
+ import { getTaskPaths, loadTaskConfig } from "../runtime.js";
22
25
  import { detectProductArtifactMeta } from "./artifact-meta.js";
23
26
  import { filterPlaceholderPaths } from "./placeholder-paths.js";
24
27
  export const SEMANTIC_INTAKE_ARTIFACT_REL = "artifacts/intake/semantic-draft.json";
@@ -47,6 +50,7 @@ const engineeringRecommendationSchema = z
47
50
  label: z.string().min(1),
48
51
  command: z.string().min(1),
49
52
  timeoutMs: z.number().int().positive().optional(),
53
+ mode: z.enum(["static", "behavior"]).optional(),
50
54
  }))
51
55
  .optional(),
52
56
  })
@@ -521,6 +525,9 @@ async function writeAttemptMarker(repoRoot, taskId, payload) {
521
525
  * per advance invocation). Writes artifacts under taskDir/artifacts/intake/.
522
526
  */
523
527
  export async function runSemanticIntake(input) {
528
+ return observeSourceExecution({ repoRoot: input.repoRoot, taskId: input.taskId, taskKind: input.taskKind ?? input.flags.taskKind, phase: "semantic-intake", inputIdentity: { documents: input.documents, flags: input.flags, title: input.title } }, () => runSemanticIntakeInExecution(input));
529
+ }
530
+ async function runSemanticIntakeInExecution(input) {
524
531
  const paths = getTaskPaths(input.repoRoot, input.taskId);
525
532
  const intakeDir = path.join(paths.taskDir, "artifacts", "intake");
526
533
  await mkdir(intakeDir, { recursive: true });
@@ -593,6 +600,11 @@ export async function runSemanticIntake(input) {
593
600
  message: "semantic intake has no MED/HIGH/LOW or Pi-supported models to try",
594
601
  };
595
602
  }
603
+ const config = await loadTaskConfig(input.repoRoot, input.taskId);
604
+ const taskKind = input.taskKind ?? input.flags.taskKind ?? config.taskKind;
605
+ const reserveProviderRequest = taskKind === "frontend-implementation"
606
+ ? await openTaskSourceProviderBudget({ repoRoot: input.repoRoot, taskId: input.taskId, maxProviderRequests: config.frontendRecovery?.maxProviderRequests ?? 1024 })
607
+ : undefined;
596
608
  // Complete text is already inline; attaching the same documents duplicates it.
597
609
  const prompt = await buildPrompt({
598
610
  taskId: input.taskId,
@@ -606,10 +618,11 @@ export async function runSemanticIntake(input) {
606
618
  for (const modelRef of queue) {
607
619
  try {
608
620
  const result = await observeFrontendSession({
609
- phase: "semantic-intake", userMessage: "Structure the imported requirement documents into the required JSON only.", prompt, model: modelRef, taskId: input.taskId,
621
+ phase: "semantic-intake", sourceDigest: sha256OfCanonicalJson(usableDocs), userMessage: "Structure the imported requirement documents into the required JSON only.", prompt, model: modelRef, taskId: input.taskId,
610
622
  artifactPath: path.join(getTaskPaths(input.repoRoot, input.taskId).taskDir, "artifacts", "session-budget", `${randomUUID()}.json`),
611
623
  }, observer => execute({
612
624
  onAttemptObservation: observer,
625
+ reserveProviderRequest,
613
626
  attachedFiles: [],
614
627
  modelConfig: resolveDagPiModelConfig(modelRef),
615
628
  prompt,
@@ -642,6 +655,8 @@ export async function runSemanticIntake(input) {
642
655
  }
643
656
  lastFailure =
644
657
  `${result.failureCategory}: ${result.stderr || result.assistantText}`.slice(0, 2000);
658
+ if (result.failureCategory === "budget_breach")
659
+ break;
645
660
  if (result.stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory)) {
646
661
  lastFailure = `SEMANTIC_INTAKE_CAPACITY_EXHAUSTED: refine the complete source into independently reviewable documents; ${lastFailure}`;
647
662
  break;
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { randomUUID } from "node:crypto";
4
+ import { mkdir } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
7
+ import { observeTaskContractState } from "../contract/observe.js";
8
+ import { sha256OfCanonicalJson } from "../contract/hash.js";
9
+ import { getTaskPaths, loadTaskConfig } from "../runtime.js";
10
+ const context = new AsyncLocalStorage();
11
+ export function currentSourceExecution(taskId) {
12
+ const current = context.getStore();
13
+ return current && current.taskId === taskId ? current.executionId : undefined;
14
+ }
15
+ /** An invocation identity, independent of request/session identities and of
16
+ * input equality. Persist before requests; a crash leaves an incomplete record. */
17
+ export async function observeSourceExecution(input, execute) {
18
+ if (input.dryRun)
19
+ return execute();
20
+ let taskKind = input.taskKind;
21
+ if (!taskKind) {
22
+ try {
23
+ taskKind = (await loadTaskConfig(input.repoRoot, input.taskId)).taskKind;
24
+ }
25
+ catch {
26
+ return execute();
27
+ }
28
+ }
29
+ if (taskKind !== "frontend-implementation")
30
+ return execute();
31
+ // Preserve a missing-task result without creating a phantom artifact tree.
32
+ try {
33
+ await loadTaskConfig(input.repoRoot, input.taskId);
34
+ }
35
+ catch {
36
+ return execute();
37
+ }
38
+ const contract = async () => {
39
+ const state = await observeTaskContractState(input.repoRoot, input.taskId);
40
+ return { revision: state.ref?.revision ?? null, canonicalHash: state.ref?.canonicalHash ?? null, observedCanonicalHash: state.observedCanonicalHash ?? null };
41
+ };
42
+ const executionId = randomUUID();
43
+ const directory = path.join(getTaskPaths(input.repoRoot, input.taskId).taskDir, "artifacts/source-executions");
44
+ await mkdir(directory, { recursive: true });
45
+ const file = path.join(directory, `${executionId}.json`);
46
+ const initial = { schemaVersion: 1, executionId, taskId: input.taskId, phase: input.phase,
47
+ inputSha256: sha256OfCanonicalJson(input.inputIdentity), beforeContract: await contract(),
48
+ startedAt: new Date().toISOString(), completion: "incomplete" };
49
+ await writeJsonAtomic(file, initial, { repoRoot: input.repoRoot });
50
+ let outcome = "threw";
51
+ try {
52
+ const result = await context.run({ executionId, taskId: input.taskId }, execute);
53
+ outcome = result !== null && typeof result === "object" && "ok" in result ? result.ok === true ? "succeeded" : "failed" : "returned";
54
+ return result;
55
+ }
56
+ finally {
57
+ await writeJsonAtomic(file, { ...initial, completion: "complete", outcome, finishedAt: new Date().toISOString(), afterContract: await contract() }, { repoRoot: input.repoRoot });
58
+ }
59
+ }
60
+ const contractSnapshotSchema = z.object({ revision: z.number().int().nonnegative().nullable(), canonicalHash: z.string().regex(/^[a-f0-9]{64}$/).nullable(), observedCanonicalHash: z.string().regex(/^[a-f0-9]{64}$/).nullable() }).strict();
61
+ export const completedSourceExecutionSchema = z.object({
62
+ schemaVersion: z.literal(1), executionId: z.string().uuid(), taskId: z.string().min(1), phase: z.enum(["prepare", "semantic-intake"]),
63
+ inputSha256: z.string().regex(/^[a-f0-9]{64}$/), beforeContract: contractSnapshotSchema, afterContract: contractSnapshotSchema,
64
+ startedAt: z.string().datetime(), finishedAt: z.string().datetime(), completion: z.literal("complete"), outcome: z.enum(["succeeded", "failed", "threw", "returned"]),
65
+ }).strict().refine(value => Date.parse(value.finishedAt) >= Date.parse(value.startedAt));
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { openTaskSourceProviderBudget } from "./source-provider-budget.js";
2
3
  import { createHash, randomUUID } from "node:crypto";
3
4
  import { observeFrontendSession } from "../../workflows/dag/frontend-session-budget.js";
4
5
  /**
@@ -11,7 +12,7 @@ import { observeFrontendSession } from "../../workflows/dag/frontend-session-bud
11
12
  * - review:context-only / duplicate / conflict 的独立 reviewer(与 reconciler
12
13
  * 是不同调用阶段,最多一次有界修订,LEDGER_REVIEW_MAX_REVISIONS=1)。
13
14
  *
14
- * 模型队列镜像 semantic-intake.ts(MED→HIGH→LOW harness 档位 + Pi SDK 目录),
15
+ * 模型队列仅使用 MED→HIGH→LOW harness 显式档位,不追加 Pi SDK 全局目录,
15
16
  * 单次尝试沿用有界超时(SOURCE_FIDELITY_ATTEMPT_TIMEOUT_MS),工具面仅只读
16
17
  * read/grep/find/ls(无写工具)。所有模型输出经 validateOutput + zod 严格校验:
17
18
  * 无效 JSON/schema、越界 canonical ID、排除类 disposition 缺理由一律抛
@@ -21,7 +22,7 @@ import { observeFrontendSession } from "../../workflows/dag/frontend-session-bud
21
22
  */
22
23
  import { z } from "zod";
23
24
  import { executePiStep } from "../../executors/pi-executor.js";
24
- import { resolveDagPiModelConfig } from "../../executors/dag-pi-executor.js";
25
+ import { resolveDagPiModelConfig } from "../../executors/dag-pi/model-config.js";
25
26
  import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
26
27
  import { listPiSdkAvailableModels } from "../../executors/pi-sdk-executor.js";
27
28
  import { loadHarnessManifest } from "../../governance/harness.js";
@@ -72,12 +73,12 @@ const reviewResponseSchema = z.object({
72
73
  .nullish(),
73
74
  });
74
75
  /**
75
- * 模型队列构建镜像 semantic-intake.ts:MED→HIGH→LOW(harness executors.pi)+
76
- * Pi SDK 目录(已认证优先、非 chat 模型过滤、额外模型上限沿用
77
- * SEMANTIC_INTAKE_MAX_EXTRA_MODELS)。队列空即 fail closed。
76
+ * Only configured MED→HIGH→LOW routes authorize a provider attempt. Global
77
+ * catalog availability or credentials do not authorize another model. An empty
78
+ * configured queue fails closed.
78
79
  */
79
80
  export function buildSourceFidelityModelQueue(input) {
80
- return buildSemanticIntakeModelQueue(input);
81
+ return buildSemanticIntakeModelQueue({ harness: input.harness });
81
82
  }
82
83
  async function buildProductionModelQueue(repoRoot, listAvailableModels) {
83
84
  const manifest = await loadHarnessManifest(repoRoot);
@@ -248,13 +249,16 @@ export async function runProductionLedgerReconciliation(request, options) {
248
249
  const execute = options.executePi ?? executePiStep;
249
250
  const timeoutMs = options.timeoutMs ?? SOURCE_FIDELITY_ATTEMPT_TIMEOUT_MS;
250
251
  let lastFailure = "";
252
+ let attemptedModels = 0;
251
253
  for (const modelRef of queue) {
254
+ attemptedModels++;
252
255
  try {
253
256
  const result = await observeFrontendSession({
254
257
  phase: "source-reconciliation", userMessage: "Reconcile the source fragment into the required JSON only.", sourceDigest: createHash("sha256").update(JSON.stringify(request)).digest("hex"), scopeIds: [request.fragment.id], prompt, model: modelRef, taskId: options.taskId, sink: options.observationSink,
255
258
  artifactPath: options.taskId ? path.join(getTaskPaths(options.repoRoot, options.taskId).taskDir, "artifacts", "session-budget", `${randomUUID()}.json`) : undefined,
256
259
  }, observer => execute({
257
260
  onAttemptObservation: observer,
261
+ reserveProviderRequest: options.reserveProviderRequest,
258
262
  attachedFiles: [],
259
263
  modelConfig: resolveDagPiModelConfig(modelRef),
260
264
  prompt,
@@ -277,6 +281,8 @@ export async function runProductionLedgerReconciliation(request, options) {
277
281
  if (!result.ok) {
278
282
  lastFailure =
279
283
  `${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
284
+ if (result.failureCategory === "budget_breach")
285
+ break;
280
286
  if (result.stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory)) {
281
287
  lastFailure = `SOURCE_FIDELITY_CAPACITY_EXHAUSTED: refine this complete source fragment without losing conditions; ${lastFailure}`;
282
288
  break;
@@ -297,7 +303,7 @@ export async function runProductionLedgerReconciliation(request, options) {
297
303
  `unknown: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2000);
298
304
  }
299
305
  }
300
- throw new LedgerReconciliationError(`source fidelity reconciliation pi failed after ${queue.length} model(s): ${lastFailure || "no attempt produced valid output"}`);
306
+ throw new LedgerReconciliationError(`source fidelity reconciliation pi failed after ${attemptedModels} model(s): ${lastFailure || "no attempt produced valid output"}`);
301
307
  }
302
308
  /**
303
309
  * 执行一次生产独立 review:模型队列串行尝试(默认真实 executePiStep),
@@ -315,13 +321,16 @@ export async function runProductionLedgerReview(request, options) {
315
321
  const execute = options.executePi ?? executePiStep;
316
322
  const timeoutMs = options.timeoutMs ?? SOURCE_FIDELITY_ATTEMPT_TIMEOUT_MS;
317
323
  let lastFailure = "";
324
+ let attemptedModels = 0;
318
325
  for (const modelRef of queue) {
326
+ attemptedModels++;
319
327
  try {
320
328
  const result = await observeFrontendSession({
321
329
  phase: "source-review", userMessage: "Review the fragment disposition into the required JSON only.", sourceDigest: createHash("sha256").update(JSON.stringify(request)).digest("hex"), scopeIds: [request.fragment.id], prompt, model: modelRef, taskId: options.taskId, sink: options.observationSink,
322
330
  artifactPath: options.taskId ? path.join(getTaskPaths(options.repoRoot, options.taskId).taskDir, "artifacts", "session-budget", `${randomUUID()}.json`) : undefined,
323
331
  }, observer => execute({
324
332
  onAttemptObservation: observer,
333
+ reserveProviderRequest: options.reserveProviderRequest,
325
334
  attachedFiles: [],
326
335
  modelConfig: resolveDagPiModelConfig(modelRef),
327
336
  prompt,
@@ -344,6 +353,8 @@ export async function runProductionLedgerReview(request, options) {
344
353
  if (!result.ok) {
345
354
  lastFailure =
346
355
  `${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
356
+ if (result.failureCategory === "budget_breach")
357
+ break;
347
358
  if (result.stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory)) {
348
359
  lastFailure = `SOURCE_FIDELITY_CAPACITY_EXHAUSTED: refine this complete source fragment without losing conditions; ${lastFailure}`;
349
360
  break;
@@ -363,7 +374,7 @@ export async function runProductionLedgerReview(request, options) {
363
374
  `unknown: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2000);
364
375
  }
365
376
  }
366
- throw new LedgerReviewError(`source fidelity review pi failed after ${queue.length} model(s): ${lastFailure || "no attempt produced valid output"}`);
377
+ throw new LedgerReviewError(`source fidelity review pi failed after ${attemptedModels} model(s): ${lastFailure || "no attempt produced valid output"}`);
367
378
  }
368
379
  /**
369
380
  * 生产适配器工厂:返回 prepare 可注入的 reconcile executor(budgetChars=4000)
@@ -399,5 +410,7 @@ export async function resolveProductionSourceFidelityPi(input) {
399
410
  }
400
411
  if (effectiveTaskKind !== SOURCE_FIDELITY_TASK_KIND)
401
412
  return undefined;
402
- return createProductionSourceFidelityPi({ repoRoot: input.repoRoot, taskId: input.taskId });
413
+ const config = await loadTaskConfig(input.repoRoot, input.taskId);
414
+ const reserveProviderRequest = await openTaskSourceProviderBudget({ ...input, maxProviderRequests: config.frontendRecovery?.maxProviderRequests ?? 1024 });
415
+ return createProductionSourceFidelityPi({ repoRoot: input.repoRoot, taskId: input.taskId, reserveProviderRequest });
403
416
  }