@tea-agent/loop-agent 0.13.0-alpha.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +57 -53
  3. package/README.md +12 -3
  4. package/dist/application/dag/generate-task-dag.js +28 -58
  5. package/dist/application/evaluation/candidate-hash.js +75 -0
  6. package/dist/application/evaluation/candidate.js +52 -0
  7. package/dist/application/evaluation/replay.js +289 -0
  8. package/dist/application/evaluation/types.js +130 -0
  9. package/dist/cli/command-definitions.js +17 -4
  10. package/dist/cli/program.js +8 -4
  11. package/dist/commands/eval.js +235 -0
  12. package/dist/commands/init.js +131 -24
  13. package/dist/executors/pi-sdk-executor.js +38 -24
  14. package/dist/executors/shell-executor.js +226 -15
  15. package/dist/executors/shell-presets.js +20 -0
  16. package/dist/executors/shell-verification.js +7 -0
  17. package/dist/governance/manifest-types.js +1 -0
  18. package/dist/infrastructure/evaluation/candidate-store.js +439 -0
  19. package/dist/infrastructure/evaluation/store.js +40 -0
  20. package/dist/task/config-types.js +23 -0
  21. package/dist/worker/observe/routes.js +18 -3
  22. package/dist/worker/observe/spec-evidence.js +1 -1
  23. package/dist/worker/observe/static/dom.js +160 -1
  24. package/dist/worker/observe/static/state.js +14 -0
  25. package/dist/worker/observe/static/views/dag-inspector.js +35 -4
  26. package/dist/worker/observe/static/views/dag.js +9 -0
  27. package/dist/worker/observe/static/views/dashboard.js +702 -445
  28. package/dist/worker/observe/static/views/session-timeline.js +15 -1
  29. package/dist/workflows/dag/backend-test-analysis-contract.js +120 -0
  30. package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
  31. package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
  32. package/dist/workflows/dag/backend-test-result-contract.js +568 -0
  33. package/dist/workflows/dag/decision-envelope.js +57 -2
  34. package/dist/workflows/dag/dynamic-runtime/map.js +90 -2
  35. package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
  36. package/dist/workflows/dag/frontend-project-capability.js +309 -0
  37. package/dist/workflows/dag/frontend-repair.js +341 -0
  38. package/dist/workflows/dag/frontend-risk.js +161 -0
  39. package/dist/workflows/dag/frontend-verification-trace.js +190 -0
  40. package/dist/workflows/dag/init-hybrid.js +2407 -297
  41. package/dist/workflows/dag/node-execution.js +9 -0
  42. package/dist/workflows/dag/prompt.js +9 -0
  43. package/dist/workflows/dag/repair-artifact.js +43 -3
  44. package/dist/workflows/dag/report.js +35 -1
  45. package/dist/workflows/dag/runner.js +28 -2
  46. package/dist/workflows/dag/skill-instructions.js +4 -2
  47. package/dist/workflows/dag/task-demand-routing.js +383 -0
  48. package/dist/workflows/dag/types.js +71 -13
  49. package/dist/workflows/dag/upstream-artifacts.js +1 -0
  50. package/dist/workflows/dag/validate.js +59 -1
  51. package/docs/README.md +6 -3
  52. package/docs/agent-dag-recovery-playbook.md +5 -3
  53. package/docs/agent-dag-runner.md +3 -3
  54. package/docs/architecture/README.md +3 -3
  55. package/docs/architecture/dag-execution.md +1 -1
  56. package/docs/architecture/evolution.md +13 -13
  57. package/docs/architecture/facts-and-state.md +1 -1
  58. package/docs/architecture/runtime-boundaries.md +7 -7
  59. package/docs/architecture/system-overview.md +3 -3
  60. package/docs/architecture/worker-and-feature.md +3 -3
  61. package/docs/design/README.md +124 -42
  62. package/docs/development-principles.md +4 -4
  63. package/docs/exec-plans/active/README.md +12 -11
  64. package/docs/exec-plans/completed/README.md +33 -0
  65. package/docs/feature-workflow.md +114 -39
  66. package/docs/init-surface.manifest.json +30 -3
  67. package/docs/loop-agent-harness.md +9 -8
  68. package/docs/production-readiness.md +1 -1
  69. package/docs/progress/README.md +23 -1
  70. package/docs/reports/README.md +65 -6
  71. package/docs/skills/vetted-skill-registry.md +2 -0
  72. package/docs/templates/agent-dag.schema.json +29 -1
  73. package/docs/templates/agent-dag.supervised-implementation.json +127 -8
  74. package/docs/templates/backend-test-analysis.schema.json +44 -0
  75. package/docs/templates/backend-test-case-manifest.schema.json +190 -0
  76. package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
  77. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +81 -16
  78. package/docs/templates/backend-test-dag.json +311 -40
  79. package/docs/templates/backend-test-dag.retrospect.prompt.md +44 -30
  80. package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -8
  81. package/docs/templates/backend-test-execution.schema.json +133 -0
  82. package/docs/templates/backend-test-result.schema.json +99 -0
  83. package/docs/templates/branch-merge-report.md +93 -0
  84. package/docs/templates/frontend-design-contract.md +9 -0
  85. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
  86. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
  87. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
  88. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
  89. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
  90. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
  91. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
  92. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
  93. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
  94. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
  95. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
  96. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
  97. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
  98. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
  99. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
  100. package/docs/templates/frontend-eval/metrics.md +138 -0
  101. package/docs/templates/frontend-eval/smoke-targets.md +53 -0
  102. package/docs/templates/frontend-implementation-contract.schema.json +27 -0
  103. package/docs/templates/frontend-task-constraints.md +10 -0
  104. package/docs/templates/frontend-task-requirement.md +9 -0
  105. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -0
  106. package/docs/templates/frontend-test-dag.json +23 -0
  107. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -0
  108. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -0
  109. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -0
  110. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
  111. package/docs/templates/knowledge-graph-bootstrap-dag.json +1 -1
  112. package/docs/templates/knowledge-sync-dag.json +1 -0
  113. package/docs/verification-matrix.md +4 -1
  114. package/examples/decision-gate-agent-dag.json +4 -4
  115. package/examples/hybrid-loop-agent-dag.json +1 -1
  116. package/package.json +2 -2
  117. package/scripts/kb-bootstrap-init-skeleton.sh +2 -1
  118. package/scripts/kb-graph-incremental-prepare.mjs +19 -5
  119. package/scripts/kb-graph-promote.mjs +12 -1
  120. package/skills/ai-engineering-context/SKILL.md +2 -2
  121. package/skills/analyze-product-dependencies/SKILL.md +67 -0
  122. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -0
  123. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -0
  124. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -0
  125. package/skills/analyze-product-dependencies/references/example.md +76 -0
  126. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -0
  127. package/skills/analyze-product-dependencies/references/input-contract.md +11 -0
  128. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -0
  129. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -0
  130. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -0
  131. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -0
  132. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -0
  133. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -0
  134. package/skills/analyze-product-requirements/SKILL.md +90 -0
  135. package/skills/analyze-product-requirements/agents/openai.yaml +4 -0
  136. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -0
  137. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -0
  138. package/skills/analyze-product-requirements/references/example.md +86 -0
  139. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -0
  140. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -0
  141. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -0
  142. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -0
  143. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -0
  144. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -0
  145. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -0
  146. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -0
  147. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -0
  148. package/skills/browser-tools/SKILL.md +196 -0
  149. package/skills/browser-tools/browser-content.js +103 -0
  150. package/skills/browser-tools/browser-cookies.js +35 -0
  151. package/skills/browser-tools/browser-eval.js +53 -0
  152. package/skills/browser-tools/browser-hn-scraper.js +108 -0
  153. package/skills/browser-tools/browser-nav.js +44 -0
  154. package/skills/browser-tools/browser-pick.js +162 -0
  155. package/skills/browser-tools/browser-screenshot.js +34 -0
  156. package/skills/browser-tools/browser-start.js +86 -0
  157. package/skills/browser-tools/package-lock.json +2556 -0
  158. package/skills/browser-tools/package.json +19 -0
  159. package/skills/frontend-design-review/SKILL.md +6 -1
  160. package/skills/frontend-design-review/references/review-checklist.md +25 -4
  161. package/skills/frontend-implementation/SKILL.md +25 -30
  162. package/skills/frontend-implementation/references/code-standards.md +20 -22
  163. package/skills/frontend-implementation/references/node-contracts.md +17 -53
  164. package/skills/frontend-review/SKILL.md +10 -4
  165. package/skills/frontend-review/references/review-findings.md +8 -3
  166. package/skills/frontend-verification/SKILL.md +22 -9
  167. package/skills/frontend-verification/references/verification-checklist.md +17 -5
  168. package/skills/grill-with-docs/SKILL.md +5 -5
  169. package/skills/grill-with-docs/adr-format.md +3 -3
  170. package/skills/init-capability-evolution/SKILL.md +5 -5
  171. package/skills/loop-agent/SKILL.md +5 -5
  172. package/skills/loop-agent/references/README.md +3 -3
  173. package/skills/loop-agent/references/command-reference.md +98 -24
  174. package/skills/loop-agent/references/docs-converge.md +15 -15
  175. package/skills/loop-agent/references/harness-policy.md +2 -2
  176. package/skills/loop-agent/references/hybrid-dag.md +32 -22
  177. package/skills/loop-agent/references/multi-worktree.md +1 -1
  178. package/skills/loop-agent/references/orchestrator-and-interventions.md +8 -8
  179. package/skills/loop-agent/references/task-workflow.md +1 -1
  180. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -4
  181. package/skills/playwright-cli/SKILL.md +420 -0
  182. package/skills/playwright-cli/references/element-attributes.md +23 -0
  183. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  184. package/skills/playwright-cli/references/request-mocking.md +87 -0
  185. package/skills/playwright-cli/references/running-code.md +241 -0
  186. package/skills/playwright-cli/references/session-management.md +225 -0
  187. package/skills/playwright-cli/references/storage-state.md +275 -0
  188. package/skills/playwright-cli/references/test-generation.md +433 -0
  189. package/skills/playwright-cli/references/tracing.md +139 -0
  190. package/skills/playwright-cli/references/video-recording.md +143 -0
  191. package/skills/playwright-cli-case-generator/SKILL.md +74 -0
  192. package/skills/requesting-code-review/SKILL.md +1 -1
  193. package/skills/systematic-debugging/CREATION-LOG.md +3 -3
  194. package/skills/systematic-debugging/SKILL.md +1 -1
  195. package/skills/systematic-debugging/test-academic.md +1 -1
  196. package/skills/systematic-debugging/test-pressure-1.md +1 -1
  197. package/skills/systematic-debugging/test-pressure-2.md +1 -1
  198. package/skills/systematic-debugging/test-pressure-3.md +1 -1
  199. package/skills/verification-before-completion/SKILL.md +1 -1
@@ -1,3 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
1
3
  import path from "node:path";
2
4
  import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
3
5
  import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
@@ -405,6 +407,13 @@ export async function executeDagNode(input) {
405
407
  assistantText: node.assistantText,
406
408
  });
407
409
  Object.assign(node, artifactMeta);
410
+ if (task.shell?.jsonArtifactGate) {
411
+ const artifactPath = path.join(runDir, task.shell.jsonArtifactGate.outputDir, task.shell.jsonArtifactGate.artifactName);
412
+ const bytes = await readFile(artifactPath);
413
+ node.structuredArtifactPath = artifactPath;
414
+ node.structuredArtifactSha256 = createHash("sha256").update(bytes).digest("hex");
415
+ node.structuredArtifactSchemaId = task.shell.jsonArtifactGate.schemaId;
416
+ }
408
417
  }
409
418
  state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
410
419
  await writeNodeRecord(runDir, nodeId, state.nodes[nodeId]);
@@ -73,6 +73,15 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
73
73
  : record.assistantArtifactPath;
74
74
  const { preview, truncated } = formatStdoutPreview(upstreamText, maxChars);
75
75
  let section = `## Upstream output: ${depId}\n${preview}`;
76
+ if (record.structuredArtifactPath) {
77
+ section += [
78
+ "",
79
+ "Validated structured artifact (read-only runner evidence — use read tool to fetch; do not edit):",
80
+ `- path: ${record.structuredArtifactPath}`,
81
+ `- schema: ${record.structuredArtifactSchemaId ?? "unknown"}`,
82
+ `- sha256: ${record.structuredArtifactSha256 ?? "unknown"}`,
83
+ ].join("\n");
84
+ }
76
85
  if (truncated && artifactPath) {
77
86
  section += formatUpstreamArtifactPointerMap(record, artifactKind);
78
87
  }
@@ -13,12 +13,51 @@ export const repairArtifactSchema = z.object({
13
13
  schemaVersion: z.literal(1),
14
14
  verdict: z.enum(["pass", "request-revision"]),
15
15
  failureClass: repairFailureClassSchema,
16
- rootCause: z.string().min(1),
16
+ rootCause: z
17
+ .string()
18
+ .min(1, { message: 'repair artifact field "rootCause" must be a non-empty string' }),
17
19
  fixScope: z.array(z.string().min(1)).default([]),
18
20
  invariant: z.string().min(1),
19
21
  evidenceRefs: z.array(z.string().min(1)).default([]),
20
22
  rawLogFallbackAllowed: z.boolean().default(false),
21
23
  });
24
+ /**
25
+ * Canonical rootCause substituted when a `verdict: "pass"` repair artifact
26
+ * arrives with an empty or whitespace-only rootCause. Keeps the persisted
27
+ * RepairArtifact schema-valid without weakening request-revision diagnostics.
28
+ */
29
+ export const PASS_VERDICT_DEFAULT_ROOT_CAUSE = "No repair required.";
30
+ /**
31
+ * Normalize model-emitted repair artifacts deterministically at the parser
32
+ * boundary: a `verdict: "pass"` artifact with an empty/whitespace-only
33
+ * rootCause is rewritten to the canonical default so it still satisfies the
34
+ * strict schema. request-revision and other fields are left untouched so the
35
+ * strict diagnostics and write boundaries remain fail-closed.
36
+ */
37
+ function normalizePassArtifactRootCause(parsed) {
38
+ if (parsed &&
39
+ typeof parsed === "object" &&
40
+ !Array.isArray(parsed) &&
41
+ parsed.verdict === "pass") {
42
+ const record = parsed;
43
+ const rootCause = record.rootCause;
44
+ if (typeof rootCause === "string" && rootCause.trim().length === 0) {
45
+ return { ...record, rootCause: PASS_VERDICT_DEFAULT_ROOT_CAUSE };
46
+ }
47
+ }
48
+ return parsed;
49
+ }
50
+ /**
51
+ * Render a single Zod issue as a field-path-aware message so callers can locate
52
+ * invalid repair-artifact fields instead of seeing bare min-length text.
53
+ */
54
+ function describeZodIssue(issue) {
55
+ const path = issue.path && issue.path.length > 0 ? issue.path.join(".") : "(root)";
56
+ if (issue.message && !/^String must contain/.test(issue.message)) {
57
+ return `field "${path}": ${issue.message} (code ${issue.code})`;
58
+ }
59
+ return `field "${path}" is invalid (code ${issue.code})`;
60
+ }
22
61
  const REPAIR_ARTIFACT_FENCE = /```(?:REPAIR_ARTIFACT_JSON|json)\s*([\s\S]*?)```/gi;
23
62
  function extractCandidateJsonBlocks(text) {
24
63
  const blocks = [];
@@ -42,11 +81,12 @@ export function parseRepairArtifactFromText(text) {
42
81
  for (const candidate of candidates) {
43
82
  try {
44
83
  const parsed = JSON.parse(candidate);
45
- const result = repairArtifactSchema.safeParse(parsed);
84
+ const normalized = normalizePassArtifactRootCause(parsed);
85
+ const result = repairArtifactSchema.safeParse(normalized);
46
86
  if (result.success) {
47
87
  return { ok: true, artifact: result.data };
48
88
  }
49
- errors.push(result.error.issues.map((issue) => issue.message).join(", "));
89
+ errors.push(result.error.issues.map((issue) => describeZodIssue(issue)).join(", "));
50
90
  }
51
91
  catch (error) {
52
92
  errors.push(error instanceof Error ? error.message : String(error));
@@ -186,6 +186,20 @@ const dagRunReportEntrySchema = z
186
186
  runtimeContractReason: z.string().optional(),
187
187
  })
188
188
  .optional(),
189
+ evaluationAssociation: z.discriminatedUnion("status", [
190
+ z.object({ status: z.literal("absent-legacy") }).strict(),
191
+ z
192
+ .object({
193
+ status: z.literal("present"),
194
+ campaignId: z.string().min(1),
195
+ candidateId: z.string().min(1),
196
+ bundleHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
197
+ seed: z.number().int().nonnegative(),
198
+ split: z.enum(["public", "private", "held_out"]).optional(),
199
+ taskRef: z.string().min(1).optional(),
200
+ })
201
+ .strict(),
202
+ ]),
189
203
  executorJsonl: dagArtifactRefSchema,
190
204
  nodes: z.array(dagNodeReportRowSchema),
191
205
  })
@@ -502,6 +516,9 @@ export async function buildDagRunReportEntry(input) {
502
516
  pausedByNodeId: input.state.pausedByNodeId,
503
517
  pauseReason: input.state.pauseReason,
504
518
  ...summarizeControllerIdentityForReport(input.state.controllerIdentityRef, input.spec),
519
+ evaluationAssociation: input.state.evaluation
520
+ ? { status: "present", ...input.state.evaluation }
521
+ : { status: "absent-legacy" },
505
522
  nodes,
506
523
  };
507
524
  const primaryNode = resolvePrimaryFailureNode(partialEntry);
@@ -757,7 +774,9 @@ function formatConvergenceSummary(run) {
757
774
  `- convergence: enabled=${convergence.enabled}, pass=${convergence.currentPass}/${convergence.maxPasses}, terminal=${convergence.terminalReason ?? "-"}`,
758
775
  ];
759
776
  for (const pass of convergence.passHistory) {
760
- const verifySummary = pass.verifyPhase || pass.verifyQuota || pass.verifyCommandCount !== undefined
777
+ const verifySummary = pass.verifyPhase ||
778
+ pass.verifyQuota ||
779
+ pass.verifyCommandCount !== undefined
761
780
  ? `, verify=${pass.verifyPhase ?? "-"} quota=${pass.verifyQuota ?? "-"} commands=${pass.verifyCommandCount ?? "-"}`
762
781
  : "";
763
782
  lines.push(`- pass ${pass.pass}: ${pass.status}, reason=${pass.reason}, hardVerify=${pass.hardVerifyStatus ?? "-"}(${pass.hardVerifyFailureCategory ?? "-"})${verifySummary}, artifacts=${pass.artifactRefs.length}`);
@@ -809,6 +828,21 @@ export function formatDagReportMarkdown(report) {
809
828
  `- runDir: ${run.runDir}`,
810
829
  `- startedAt: ${run.startedAt}`,
811
830
  `- finishedAt: ${run.finishedAt ?? "-"}`,
831
+ ...(run.evaluationAssociation.status === "present"
832
+ ? [
833
+ `- evaluationAssociation: present`,
834
+ `- campaignId: ${run.evaluationAssociation.campaignId}`,
835
+ `- candidateId: ${run.evaluationAssociation.candidateId}`,
836
+ `- bundleHash: ${run.evaluationAssociation.bundleHash}`,
837
+ `- seed: ${run.evaluationAssociation.seed}`,
838
+ ...(run.evaluationAssociation.split
839
+ ? [`- split: ${run.evaluationAssociation.split}`]
840
+ : []),
841
+ ...(run.evaluationAssociation.taskRef
842
+ ? [`- taskRef: ${run.evaluationAssociation.taskRef}`]
843
+ : []),
844
+ ]
845
+ : ["- evaluationAssociation: absent/legacy"]),
812
846
  `- executorJsonl: ${run.executorJsonl.path} (${run.executorJsonl.exists ? "exists" : "missing"})`,
813
847
  ...formatConvergenceSummary(run),
814
848
  "",
@@ -1,6 +1,7 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import { hostname } from "node:os";
3
3
  import path from "node:path";
4
+ import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
4
5
  import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
5
6
  import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
6
7
  import { moveToCompletedRunDir, moveToPausedRunDir, prepareActiveRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
@@ -15,7 +16,7 @@ import { relocateConvergenceArtifactPaths, relocateRunArtifactPaths, } from "./u
15
16
  import { createSkillSnapshot, prepareSkillSnapshotForContinuation, writeSkillSnapshot, } from "./skill-snapshot.js";
16
17
  import { buildNodePrompt, buildNodePromptWithResolvedSkillInstructions, executeDagNode, } from "./node-execution.js";
17
18
  import { runConvergencePassController, shouldEnableDagConvergence, } from "./convergence/controller.js";
18
- import { executeDagRanksOnce, isConditionSkippedReason, } from "./scheduler.js";
19
+ import { executeDagRanksOnce, isConditionSkippedReason } from "./scheduler.js";
19
20
  import { topoSortToRanks } from "./topo.js";
20
21
  import { parseDagSpec, resolveModelForTask, } from "./types.js";
21
22
  import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
@@ -134,6 +135,7 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
134
135
  status: opts.initOnly || opts.dryRun ? "pending" : "running",
135
136
  ranks,
136
137
  nodes,
138
+ ...(spec.evaluation ? { evaluation: { ...spec.evaluation } } : {}),
137
139
  ...(shouldEnableDagConvergence(spec)
138
140
  ? {
139
141
  convergence: {
@@ -146,9 +148,32 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
146
148
  : {}),
147
149
  };
148
150
  }
151
+ export function assertFrozenEvaluationBinding(spec, state) {
152
+ const declared = spec.evaluation;
153
+ const frozen = state.evaluation;
154
+ if (!declared && !frozen)
155
+ return;
156
+ if (!declared ||
157
+ !frozen ||
158
+ JSON.stringify(declared) !== JSON.stringify(frozen)) {
159
+ throw new Error(`dag run ${state.runId} evaluation binding drifted between run.json and state.json; refuse to continue`);
160
+ }
161
+ }
162
+ export async function assertEvaluationBindingPreflight(cwd, binding) {
163
+ if (!binding)
164
+ return;
165
+ const record = await readCandidateRecord(cwd, binding.candidateId);
166
+ if (record.manifest.bundleHash !== binding.bundleHash) {
167
+ throw new Error(`evaluation bundleHash mismatch for ${binding.candidateId}: declared ${binding.bundleHash}, registry ${record.manifest.bundleHash}`);
168
+ }
169
+ if (!["eligible", "experimenting", "accepted"].includes(record.status)) {
170
+ throw new Error(`evaluation candidate ${binding.candidateId} lifecycle status ${record.status} is not executable`);
171
+ }
172
+ }
149
173
  export async function runDag(spec, opts) {
150
174
  assertValidDagSpec(spec);
151
- // Capability preflight must fail closed before any run directory or node work.
175
+ // Candidate identity is validated before run-id allocation or directory creation.
176
+ await assertEvaluationBindingPreflight(opts.cwd, spec.evaluation);
152
177
  const runningIdentity = resolveRunningControllerIdentity();
153
178
  if (!runningIdentity) {
154
179
  throw new Error("running controller identity could not be resolved; refuse to create an unpinned DAG run");
@@ -242,6 +267,7 @@ export async function resumeDagRun(opts) {
242
267
  if (isTerminalDagRunStatus(state.status)) {
243
268
  throw new Error(`dag run ${opts.runId} is already terminal (status=${state.status})`);
244
269
  }
270
+ assertFrozenEvaluationBinding(spec, state);
245
271
  // Runtime contract + controller identity must be re-verified before executing
246
272
  // any remaining node on resume; drift fails closed.
247
273
  const runningIdentity = resolveRunningControllerIdentity();
@@ -3,8 +3,8 @@ import { access, readFile } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- export const DEFAULT_SKILL_INSTRUCTION_MAX_CHARS = 2_500;
7
- export const DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS = 10_000;
6
+ export const DEFAULT_SKILL_INSTRUCTION_MAX_CHARS = 3_000;
7
+ export const DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS = 12_000;
8
8
  const SKILL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
9
9
  async function fileExists(filePath) {
10
10
  try {
@@ -413,7 +413,9 @@ async function findFirstExistingFile(candidates) {
413
413
  async function resolveLearnedPatternInstructions(input) {
414
414
  const repoSlug = path.basename(input.cwd).replace(/[^A-Za-z0-9_-]+/g, "-");
415
415
  const candidates = [
416
+ normalizeCandidate(path.join(input.cwd, ".agents", "skills", "loop-agent", "references", "learned", `${repoSlug}.md`)),
416
417
  normalizeCandidate(path.join(input.cwd, "skills", "loop-agent", "references", "learned", `${repoSlug}.md`)),
418
+ normalizeCandidate(path.join(input.cwd, ".agents", "skills", "loop-agent", "references", "learned", "default.md")),
417
419
  normalizeCandidate(path.join(input.cwd, "skills", "loop-agent", "references", "learned", "default.md")),
418
420
  normalizeCandidate(path.join(input.cwd, "tools", "loop-agent", "skill", "references", "learned", `${repoSlug}.md`)),
419
421
  normalizeCandidate(path.join(input.cwd, "tools", "loop-agent", "skill", "references", "learned", "default.md")),
@@ -0,0 +1,383 @@
1
+ const OUT_OF_SCOPE_HEADING = /^(?:非目标|不包含|范围外|不在范围|out\s+of\s+scope|non[-\s]?goals?)(?:\s*(?:[((].*[))]|[::].*|[/—–-]\s*.*))?$/i;
2
+ const FRONTEND_TASK_TYPE = /^\s*(?:[-*]\s*)?task\s+type\s*:\s*frontend-feature\s*$/im;
3
+ const BACKEND_TASK_TYPE = /^\s*(?:[-*]\s*)?task\s+type\s*:\s*backend-feature\s*$/im;
4
+ const FRONTEND_NEGATION_PATTERNS = [
5
+ /(?:不涉及|无需|不需要|不包含|不修改|不实现|不交付|不要|禁止|不得|排除).{0,16}(?:前端(?:页面|组件|界面|功能|交互)?|页面|组件|界面|用户界面|浏览器交互)/,
6
+ /(?:前端(?:页面|组件|界面|功能|交互)?|页面|组件|界面|用户界面).{0,12}(?:不在(?:本次)?范围|无需|不需要|不修改|不实现|不交付|不要|禁止|不得)/,
7
+ /(?:不涉及|无需|不需要|不包含|不修改|不实现|不交付|不要|禁止|不得|排除).{0,16}(?<![A-Za-z0-9_])UI(?![A-Za-z0-9_])/i,
8
+ /(?<![A-Za-z0-9_])UI(?![A-Za-z0-9_]).{0,12}(?:不在(?:本次)?范围|无需|不需要|不修改|不实现|不交付|不要|禁止|不得)/i,
9
+ /\b(?:backend|server[-\s]?side)\s+only\b/i,
10
+ /\b(?:no|without)\s+(?:frontend|front[-\s]?end|ui|user\s+interface)\b/i,
11
+ /\b(?:do\s+not|don't|must\s+not|should\s+not)\s+(?:(?:add|implement|build|create|update|modify|fix|refactor|render|style|change|touch)\s+)?(?:(?:the|any|a|an)\s+)?(?:frontend|front[-\s]?end|ui|user\s+interface|page|component|form|modal|dialog)\b/i,
12
+ /\bwithout\s+(?:(?:adding|implementing|building|creating|updating|modifying|fixing|refactoring|rendering|styling|changing|touching)\s+)?(?:(?:the|any|a|an)\s+)?(?:frontend|front[-\s]?end|ui|user\s+interface|page|component|form|modal|dialog)\b/i,
13
+ /\b(?:frontend|front[-\s]?end|ui|user\s+interface|page|component|form|modal|dialog)\b[^.?!;\n]{0,20}\b(?:is|are)?\s*(?:out\s+of\s+scope|excluded|not\s+(?:required|needed)|unchanged)\b/i,
14
+ ];
15
+ const FRONTEND_DELIVERY_PATTERNS = [
16
+ /(?:新增|实现|开发|修改|重构|修复|优化|创建|渲染|搭建|改造)[^。!?;,,\n]{0,24}(?:前端(?:页面|组件|界面|功能|交互)|页面|组件|界面|表单|弹窗|对话框|用户界面|浏览器(?:页面|交互|行为))/,
17
+ /(?:前端(?:页面|组件|界面|功能|交互)|页面|组件|界面|表单|弹窗|对话框|用户界面|浏览器交互)[^。!?;,,\n]{0,16}(?:新增|实现|开发|修改|重构|修复|优化|创建|渲染|搭建|改造)/,
18
+ /\b(?:add|implement|build|create|update|modify|fix|refactor|render|style)\b[^.?!;\n]{0,48}\b(?:front[-\s]?end\s+(?:page|component|view|form|modal|interaction|ui)|ui|form|modal|dialog|dashboard|navigation|toast|button|dropdown|user\s+interface|browser\s+interaction)\b/i,
19
+ ];
20
+ const PATH_SUPPORTED_FRONTEND_DELIVERY_PATTERNS = [
21
+ /(?:新增|实现|开发|修改|重构|修复|优化|创建|渲染|搭建|改造)[^。!?;,,\n]{0,24}(?:页(?!\p{Script=Han})|表格|列表|按钮|导航|菜单|标签页|下拉框|选择器|提示|通知|加载状态|空状态|错误状态|交互|样式|布局)/u,
22
+ /(?:新增|实现|开发|修改|重构|修复|优化|创建|渲染|搭建|改造)[^。!?;,,\n]{0,24}(?<![A-Za-z0-9_])(?:DOM|UI|React|Vue|Svelte|Angular)(?![A-Za-z0-9_])/i,
23
+ /\b(?:add|implement|build|create|update|modify|fix|refactor|render|style)\b[^.?!;\n]{0,48}\b(?:page|component|screen|layout|dom|react|vue|svelte|angular)\b/i,
24
+ ];
25
+ const BACKEND_DELIVERY_PATTERNS = [
26
+ /(?:新增|实现|开发|修改|重构|修复|提供|创建|调整|优化|迁移|扩展)[^。!?;,,\n]{0,24}(?:后端(?:服务|接口|逻辑|API)?|服务端(?:服务|接口|逻辑)?|接口|数据库|数据表(?!格)|数据库索引|索引|控制器|服务层|仓储层)/i,
27
+ /(?:后端(?:服务|接口|逻辑|API)?|服务端(?:服务|接口|逻辑)?|接口|数据库|数据表(?!格)|数据库索引|控制器|服务层|仓储层)[^。!?;,,\n]{0,16}(?:新增|实现|开发|修改|重构|修复|提供|创建|调整|优化|迁移|扩展)/i,
28
+ /(?:新增|实现|开发|修改|重构|修复|提供|创建|调整|优化|迁移|扩展)[^。!?;,,\n]{0,24}(?<![A-Za-z0-9_])API(?![A-Za-z0-9_])/i,
29
+ /(?<![A-Za-z0-9_])API(?![A-Za-z0-9_])[^。!?;,,\n]{0,16}(?:新增|实现|开发|修改|重构|修复|提供|创建|调整|优化|迁移|扩展)/i,
30
+ /\b(?:add|implement|build|create|update|modify|fix|refactor|migrate|extend)\b[^.?!;\n]{0,48}\b(?:backend|server[-\s]?side|api|endpoint|database|migration|controller|service|repository)\b/i,
31
+ ];
32
+ const BACKEND_NEGATION_PATTERNS = [
33
+ /(?:不涉及|无需|不需要|不包含|不修改|不实现|不交付|不要|禁止|不得|排除).{0,16}(?:后端|服务端|接口|数据库|数据表(?!格)|索引)/,
34
+ /(?:不涉及|无需|不需要|不包含|不修改|不实现|不交付|不要|禁止|不得|排除).{0,16}(?<![A-Za-z0-9_])API(?![A-Za-z0-9_])/i,
35
+ /\b(?:no|without)\s+(?:backend|server[-\s]?side|api|endpoint|database)\b/i,
36
+ /\b(?:do\s+not|don't|must\s+not|should\s+not)\s+(?:(?:add|implement|build|create|update|modify|fix|refactor|migrate|change|touch)\s+)?(?:(?:the|any|a|an)\s+)?(?:backend|server[-\s]?side|api|endpoint|database)\b/i,
37
+ ];
38
+ const FRONTEND_PATH_PATTERNS = [
39
+ /(?:^|\/)apps\/web\/src(?:\/|$)/i,
40
+ /(?:^|\/)(?:frontend|client)(?:\/|$)/i,
41
+ /(?:^|\/)src\/(?:pages?|components?|views?)(?:\/|$)/i,
42
+ /\.(?:tsx|jsx|vue|svelte|css|scss|less)(?:$|[*?])/i,
43
+ ];
44
+ const BACKEND_PATH_PATTERNS = [
45
+ /(?:^|\/)(?:backend|server|api|controllers?|services?|repositories?|db|database|migrations?)(?:\/|$)/i,
46
+ /\.(?:sql)(?:$|[*?])/i,
47
+ ];
48
+ const NON_PRODUCT_PATH_SEGMENT = /(?:^|\/)(?:docs?|skills?|tests?|__tests__|e2e|cypress)(?:\/|$)/i;
49
+ const FRONTEND_PATH_EXCLUSIONS = NON_PRODUCT_PATH_SEGMENT;
50
+ const DOCUMENTATION_ONLY_MARKERS = /(?:文档|说明|指南|规范|测试用例|测试计划|评审|审查|\b(?:docs?|documentation|guide|specification|test\s+cases?|test\s+plan|review)\b)/i;
51
+ const DOCUMENTATION_ONLY_ACTIONS = /(?:新增|实现|开发|创建|编写|更新|修改|重构|修复|优化|渲染|搭建|改造|补充|生成|评审|审查|\b(?:add|implement|build|create|write|update|modify|refactor|fix|optimize|render|style|document|review|generate)\b)/i;
52
+ const PRODUCT_AND_DOCUMENT_DELIVERY = /(?:新增|实现|开发|修改|重构|修复|优化|创建|渲染|搭建|改造)[^。!?;,,\n]{0,24}(?:前端|页面|组件|界面|表单|交互|样式|布局|DOM|UI|React|Vue|Svelte|Angular|源码|代码)[^。!?;,,\n]{0,16}(?:并|同时|以及|和|与|\band\b)[^。!?;,,\n]{0,16}(?:文档|说明|指南|规范|\b(?:docs?|documentation|guide)\b)/i;
53
+ const TEST_ONLY_MARKERS = /(?:测试|用例|\b(?:e2e|unit|integration|component)\s+tests?\b|\btests?\b)/i;
54
+ const PRODUCT_AND_TEST_DELIVERY = /(?:并|同时|以及|和|与|\band\b).{0,12}(?:添加|新增|编写|补充|测试|\b(?:add|write|test)\b)/i;
55
+ const NON_PRODUCT_ALLOWED_PATH_PATTERNS = [
56
+ NON_PRODUCT_PATH_SEGMENT,
57
+ /\.(?:test|spec)\.[^/]+$/i,
58
+ /\.cy\.[^/]+$/i,
59
+ /\.mdx?(?:$|[*?])/i,
60
+ ];
61
+ const SPECIAL_TEMPLATE_BY_TASK_KIND = {
62
+ "frontend-implementation": "frontend-implementation",
63
+ "frontend-test": "frontend-test-dag",
64
+ "backend-test": "backend-test-dag",
65
+ "knowledge-sync": "knowledge-sync-dag",
66
+ "knowledge-graph-bootstrap": "knowledge-graph-bootstrap-dag",
67
+ };
68
+ const TASK_KIND_ROUTING_REASON = {
69
+ "frontend-implementation": 'taskKind "frontend-implementation" selects the dedicated frontend DAG template',
70
+ "frontend-test": 'taskKind "frontend-test" selects the dedicated frontend test DAG template',
71
+ "backend-test": 'taskKind "backend-test" selects the dedicated backend test DAG template',
72
+ "knowledge-sync": 'taskKind "knowledge-sync" selects the dedicated knowledge-sync DAG template',
73
+ "knowledge-graph-bootstrap": 'taskKind "knowledge-graph-bootstrap" selects the dedicated knowledge-graph bootstrap DAG template',
74
+ };
75
+ const SPECIAL_REQUESTED_TEMPLATES = new Set([
76
+ "frontend-implementation",
77
+ "frontend-test-dag",
78
+ "backend-test-dag",
79
+ "knowledge-sync-dag",
80
+ "knowledge-graph-bootstrap-dag",
81
+ ]);
82
+ function stripOutOfScopeSections(markdown) {
83
+ const output = [];
84
+ let skippedHeadingLevel = null;
85
+ for (const line of markdown.split(/\r?\n/)) {
86
+ const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line.trim());
87
+ if (heading) {
88
+ const level = heading[1].length;
89
+ if (skippedHeadingLevel !== null && level <= skippedHeadingLevel) {
90
+ skippedHeadingLevel = null;
91
+ }
92
+ if (OUT_OF_SCOPE_HEADING.test(heading[2].trim())) {
93
+ skippedHeadingLevel = level;
94
+ continue;
95
+ }
96
+ }
97
+ if (skippedHeadingLevel === null)
98
+ output.push(line);
99
+ }
100
+ return output.join("\n");
101
+ }
102
+ function stripFencedCodeBlocks(markdown) {
103
+ const output = [];
104
+ let fenceCharacter = null;
105
+ for (const line of markdown.split(/\r?\n/)) {
106
+ const fence = /^\s*(`{3,}|~{3,})/.exec(line);
107
+ if (fence) {
108
+ const character = fence[1][0];
109
+ if (fenceCharacter === null)
110
+ fenceCharacter = character;
111
+ else if (fenceCharacter === character)
112
+ fenceCharacter = null;
113
+ continue;
114
+ }
115
+ if (fenceCharacter === null)
116
+ output.push(line);
117
+ }
118
+ return output.join("\n");
119
+ }
120
+ /**
121
+ * Normalize task prose before deterministic demand/capability signal checks.
122
+ * Examples and explicit non-goals must not become positive routing evidence.
123
+ */
124
+ export function normalizeTaskRequirementText(markdown) {
125
+ return stripOutOfScopeSections(stripFencedCodeBlocks(markdown));
126
+ }
127
+ function splitClauses(text) {
128
+ return text
129
+ .split(/[。!?!?;;,,\r\n]+/)
130
+ .map((clause) => clause.trim())
131
+ .filter(Boolean);
132
+ }
133
+ function matchesAny(value, patterns) {
134
+ return patterns.some((pattern) => pattern.test(value));
135
+ }
136
+ function isFrontendNegated(clause) {
137
+ return matchesAny(clause, FRONTEND_NEGATION_PATTERNS);
138
+ }
139
+ function isDocumentationOnlyClause(clause) {
140
+ if (!DOCUMENTATION_ONLY_ACTIONS.test(clause))
141
+ return false;
142
+ if (DOCUMENTATION_ONLY_MARKERS.test(clause)) {
143
+ return !PRODUCT_AND_DOCUMENT_DELIVERY.test(clause);
144
+ }
145
+ return TEST_ONLY_MARKERS.test(clause) && !PRODUCT_AND_TEST_DELIVERY.test(clause);
146
+ }
147
+ function masksExistingBackendDependency(clause) {
148
+ return clause
149
+ .replace(/(?:调用|使用|消费|对接|依赖)(?:现有|已有)?[^。!?;,,\n]{0,12}(?:后端|服务端|API|接口)/gi, " ")
150
+ .replace(/\b(?:call|consume|use|integrate\s+with|depend\s+on)\b[^.?!;\n]{0,24}\b(?:existing|current)?\s*(?:backend|server[-\s]?side|api|endpoint)\b/gi, " ");
151
+ }
152
+ function addSignal(signals, signal) {
153
+ if (signals.some((current) => current.id === signal.id))
154
+ return;
155
+ signals.push(signal);
156
+ }
157
+ function collectTextSignals(input) {
158
+ let frontendDelivery = false;
159
+ let pathSupportedFrontendDelivery = false;
160
+ let backendDelivery = false;
161
+ let frontendNegated = false;
162
+ for (const clause of splitClauses(input.text)) {
163
+ const negated = isFrontendNegated(clause);
164
+ frontendNegated ||= negated;
165
+ const documentationOnly = isDocumentationOnlyClause(clause);
166
+ if (!negated &&
167
+ !documentationOnly &&
168
+ matchesAny(clause, FRONTEND_DELIVERY_PATTERNS)) {
169
+ frontendDelivery = true;
170
+ addSignal(input.signals, {
171
+ id: `frontend-delivery-${input.source}`,
172
+ source: input.source,
173
+ message: `${input.source} contains an implementation action and a frontend UI deliverable`,
174
+ });
175
+ }
176
+ if (!negated &&
177
+ !documentationOnly &&
178
+ matchesAny(clause, PATH_SUPPORTED_FRONTEND_DELIVERY_PATTERNS)) {
179
+ pathSupportedFrontendDelivery = true;
180
+ }
181
+ const backendClause = masksExistingBackendDependency(clause);
182
+ if (!matchesAny(clause, BACKEND_NEGATION_PATTERNS) &&
183
+ matchesAny(backendClause, BACKEND_DELIVERY_PATTERNS)) {
184
+ backendDelivery = true;
185
+ addSignal(input.signals, {
186
+ id: `backend-delivery-${input.source}`,
187
+ source: input.source,
188
+ message: `${input.source} contains an implementation action and a backend deliverable`,
189
+ });
190
+ }
191
+ }
192
+ return {
193
+ frontendDelivery,
194
+ pathSupportedFrontendDelivery,
195
+ backendDelivery,
196
+ frontendNegated,
197
+ };
198
+ }
199
+ function normalizedAllowedPath(value) {
200
+ return value.trim().replace(/\\/g, "/").replace(/^\.\//, "");
201
+ }
202
+ function reasonsForClassification(input) {
203
+ const signalIds = input.signals.map((signal) => signal.id);
204
+ const reasons = [`task demand classified as ${input.kind}`];
205
+ if (signalIds.length > 0)
206
+ reasons.push(`signals: ${signalIds.join(", ")}`);
207
+ if (input.blockers.length > 0)
208
+ reasons.push(`blockers: ${input.blockers.join(", ")}`);
209
+ return reasons;
210
+ }
211
+ export function classifyTaskDemand(input) {
212
+ const signals = [];
213
+ const blockers = [];
214
+ const requirement = normalizeTaskRequirementText(input.requirementMarkdown);
215
+ const hasFrontendTaskType = FRONTEND_TASK_TYPE.test(requirement);
216
+ const hasBackendTaskType = BACKEND_TASK_TYPE.test(requirement);
217
+ if (hasFrontendTaskType) {
218
+ addSignal(signals, {
219
+ id: "task-type-frontend-feature",
220
+ source: "requirement",
221
+ message: "requirement contains the materialized frontend-feature task type",
222
+ });
223
+ }
224
+ if (hasBackendTaskType) {
225
+ addSignal(signals, {
226
+ id: "task-type-backend-feature",
227
+ source: "requirement",
228
+ message: "requirement contains the materialized backend-feature task type",
229
+ });
230
+ }
231
+ const titleSignals = collectTextSignals({
232
+ text: input.title,
233
+ source: "title",
234
+ signals,
235
+ });
236
+ const requirementSignals = collectTextSignals({
237
+ text: requirement,
238
+ source: "requirement",
239
+ signals,
240
+ });
241
+ const frontendNegated = titleSignals.frontendNegated || requirementSignals.frontendNegated;
242
+ if (frontendNegated)
243
+ blockers.push("frontend-negated");
244
+ let frontendPath = false;
245
+ let backendPath = false;
246
+ for (const rawPath of input.allowedPaths) {
247
+ const allowedPath = normalizedAllowedPath(rawPath);
248
+ if (!FRONTEND_PATH_EXCLUSIONS.test(allowedPath) &&
249
+ matchesAny(allowedPath, FRONTEND_PATH_PATTERNS)) {
250
+ frontendPath = true;
251
+ }
252
+ if (matchesAny(allowedPath, BACKEND_PATH_PATTERNS))
253
+ backendPath = true;
254
+ }
255
+ if (frontendPath) {
256
+ addSignal(signals, {
257
+ id: "frontend-path",
258
+ source: "allowed-path",
259
+ message: "allowedPaths include a frontend-specific source path",
260
+ });
261
+ }
262
+ if (backendPath) {
263
+ addSignal(signals, {
264
+ id: "backend-path",
265
+ source: "allowed-path",
266
+ message: "allowedPaths include a backend-specific source path",
267
+ });
268
+ }
269
+ const pathSupportedFrontendDelivery = frontendPath &&
270
+ (titleSignals.pathSupportedFrontendDelivery ||
271
+ requirementSignals.pathSupportedFrontendDelivery);
272
+ if (pathSupportedFrontendDelivery) {
273
+ addSignal(signals, {
274
+ id: requirementSignals.pathSupportedFrontendDelivery
275
+ ? "frontend-path-supported-delivery-requirement"
276
+ : "frontend-path-supported-delivery-title",
277
+ source: requirementSignals.pathSupportedFrontendDelivery
278
+ ? "requirement"
279
+ : "title",
280
+ message: "task text contains a frontend UI artifact and allowedPaths provide frontend source-path evidence",
281
+ });
282
+ }
283
+ const frontendDelivery = titleSignals.frontendDelivery ||
284
+ requirementSignals.frontendDelivery ||
285
+ pathSupportedFrontendDelivery;
286
+ const allowedPathsOnlyCoverNonProductArtifacts = input.allowedPaths.length > 0 &&
287
+ input.allowedPaths.every((rawPath) => {
288
+ const allowedPath = normalizedAllowedPath(rawPath);
289
+ return matchesAny(allowedPath, NON_PRODUCT_ALLOWED_PATH_PATTERNS);
290
+ });
291
+ if (allowedPathsOnlyCoverNonProductArtifacts && frontendDelivery) {
292
+ blockers.push("non-product-allowed-paths");
293
+ }
294
+ const eligibleFrontendDelivery = frontendDelivery && !allowedPathsOnlyCoverNonProductArtifacts;
295
+ const backendDelivery = titleSignals.backendDelivery || requirementSignals.backendDelivery;
296
+ const taskTypeConflict = (hasFrontendTaskType && (backendDelivery || frontendNegated)) ||
297
+ (hasBackendTaskType && eligibleFrontendDelivery);
298
+ if (taskTypeConflict)
299
+ blockers.push("task-type-conflict");
300
+ let kind;
301
+ if ((hasFrontendTaskType && hasBackendTaskType) || taskTypeConflict) {
302
+ kind = "mixed";
303
+ }
304
+ else if (hasFrontendTaskType) {
305
+ kind = "frontend";
306
+ }
307
+ else if (hasBackendTaskType) {
308
+ kind = "backend";
309
+ }
310
+ else if (eligibleFrontendDelivery && backendDelivery) {
311
+ kind = "mixed";
312
+ }
313
+ else if (eligibleFrontendDelivery && frontendNegated) {
314
+ kind = "mixed";
315
+ }
316
+ else if (eligibleFrontendDelivery) {
317
+ kind = "frontend";
318
+ }
319
+ else if (backendDelivery) {
320
+ kind = "backend";
321
+ }
322
+ else {
323
+ kind = "unknown";
324
+ }
325
+ return {
326
+ kind,
327
+ signals,
328
+ blockers,
329
+ reasons: reasonsForClassification({ kind, signals, blockers }),
330
+ };
331
+ }
332
+ export function resolveTaskDagTemplateSelection(input) {
333
+ const requestedTemplate = input.requestedTemplate ?? "standard-dag";
334
+ const taskKindTemplate = SPECIAL_TEMPLATE_BY_TASK_KIND[input.taskKind];
335
+ if (taskKindTemplate) {
336
+ return {
337
+ template: taskKindTemplate,
338
+ source: "taskKind",
339
+ reasons: [TASK_KIND_ROUTING_REASON[input.taskKind]],
340
+ };
341
+ }
342
+ if (SPECIAL_REQUESTED_TEMPLATES.has(requestedTemplate)) {
343
+ return {
344
+ template: requestedTemplate,
345
+ source: "requested-template",
346
+ reasons: [],
347
+ };
348
+ }
349
+ if (input.taskKind !== "standard") {
350
+ return {
351
+ template: requestedTemplate,
352
+ source: "requested-template",
353
+ reasons: [],
354
+ };
355
+ }
356
+ if (requestedTemplate !== "standard-dag") {
357
+ return {
358
+ template: requestedTemplate,
359
+ source: "requested-template",
360
+ reasons: [],
361
+ };
362
+ }
363
+ const classification = classifyTaskDemand(input);
364
+ if (classification.kind === "frontend") {
365
+ const frontendSignals = classification.signals
366
+ .map((signal) => signal.id)
367
+ .filter((id) => id === "task-type-frontend-feature" || id.startsWith("frontend-"));
368
+ return {
369
+ template: "frontend-implementation",
370
+ source: "task-source",
371
+ reasons: [
372
+ `task source classified as frontend from signals: ${frontendSignals.join(", ")}`,
373
+ ],
374
+ classification,
375
+ };
376
+ }
377
+ return {
378
+ template: requestedTemplate,
379
+ source: "requested-template",
380
+ reasons: [],
381
+ classification,
382
+ };
383
+ }