@tea-agent/loop-agent 0.24.0 → 0.24.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 (30) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/commands/init.js +2 -2
  3. package/dist/task/dag-source-paths.js +50 -0
  4. package/dist/task/frontend-project-capability.js +316 -0
  5. package/dist/task/task-demand-routing.js +432 -0
  6. package/dist/worker/console/chat/compaction-errors.js +64 -0
  7. package/dist/worker/console/chat/interview-adapter.js +17 -4
  8. package/dist/worker/console/chat/routes.js +6 -1
  9. package/dist/worker/console/interview/grill-me.js +28 -5
  10. package/dist/worker/console/static/assets/index-Cv68Ge5n.js +27 -0
  11. package/dist/worker/console/static/assets/index-Dp_2sKGk.css +1 -0
  12. package/dist/worker/console/static/index.html +2 -2
  13. package/dist/worker/observe/routes.js +8 -4
  14. package/dist/workflows/dag/backend-test-intake-context.js +10 -12
  15. package/dist/workflows/dag/backend-test-markdown-workflow.js +223 -42
  16. package/dist/workflows/dag/backend-test-result-contract.js +6 -2
  17. package/dist/workflows/dag/frontend-implementation-contract.js +23 -17
  18. package/dist/workflows/dag/frontend-project-capability.js +1 -316
  19. package/dist/workflows/dag/frontend-worktree-diff.js +4 -1
  20. package/dist/workflows/dag/init-hybrid.js +44 -42
  21. package/dist/workflows/dag/rerun-plan.js +6 -10
  22. package/dist/workflows/dag/task-demand-routing.js +1 -383
  23. package/docs/templates/frontend-implementation-contract.schema.json +1 -1
  24. package/package.json +1 -1
  25. package/skills/frontend-implementation/SKILL.md +7 -0
  26. package/skills/frontend-implementation/references/node-contracts.md +1 -1
  27. package/skills/frontend-verification/SKILL.md +4 -3
  28. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  29. package/dist/worker/console/static/assets/index-BTbrEHnO.css +0 -1
  30. package/dist/worker/console/static/assets/index-D9qLevoP.js +0 -27
@@ -7,6 +7,7 @@ import { z } from "zod";
7
7
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
8
8
  import { findPackageRoot } from "../../shared/package-metadata.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
+ import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
10
11
  export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
11
12
  /**
12
13
  * Load the canonical frontend-implementation-contract-v1 JSON Schema from the
@@ -133,7 +134,7 @@ export const frontendImplementationContractSchema = z
133
134
  verificationTargetIds: z.array(z.string().min(1)).optional(),
134
135
  notApplicableReason: z.string().min(1).optional(),
135
136
  })
136
- .strict()).min(1),
137
+ .strict()),
137
138
  interactions: z.array(z
138
139
  .object({
139
140
  name: z.string().min(1),
@@ -300,17 +301,11 @@ export const frontendImplementationContractSchema = z
300
301
  });
301
302
  export async function assertFrontendSourceBindingFresh(input) {
302
303
  for (const source of input.binding.sources) {
303
- // DAG source bindings are task-relative (for example source/需求.md),
304
- // while older bindings may already contain the repository-relative
305
- // .harness/tasks/<taskId>/ prefix. Resolve both forms before hashing.
306
- const taskDir = path.resolve(input.workspaceRoot, ".harness", "tasks", input.binding.taskId);
307
- const absolute = source.path.startsWith(".harness/")
308
- ? path.resolve(input.workspaceRoot, source.path)
309
- : path.resolve(taskDir, source.path);
310
- const relative = path.relative(input.workspaceRoot, absolute);
311
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
312
- throw new Error(`frontend source binding escapes workspace: ${source.path}`);
313
- }
304
+ const absolute = resolveDagTaskSourcePath({
305
+ workspaceRoot: input.workspaceRoot,
306
+ taskId: input.binding.taskId,
307
+ sourcePath: source.path,
308
+ });
314
309
  let content;
315
310
  try {
316
311
  content = await readFile(absolute);
@@ -425,12 +420,23 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
425
420
  uiStates: Array.isArray(rawRecord.uiStates)
426
421
  ? rawRecord.uiStates.map((item) => {
427
422
  const state = asRecord(item);
428
- if (!state ||
429
- state.applicable === false ||
430
- (state.notApplicableReason !== "" && state.notApplicableReason !== null))
423
+ if (!state)
431
424
  return item;
432
- const { notApplicableReason: _emptyReason, ...withoutEmptyReason } = state;
433
- return withoutEmptyReason;
425
+ if (state.applicable === true &&
426
+ (state.notApplicableReason === null ||
427
+ (typeof state.notApplicableReason === "string" &&
428
+ state.notApplicableReason.trim() === ""))) {
429
+ const { notApplicableReason: _emptyReason, ...withoutEmptyReason } = state;
430
+ return withoutEmptyReason;
431
+ }
432
+ if (state.applicable === false &&
433
+ (state.expectedBehavior === null ||
434
+ (typeof state.expectedBehavior === "string" &&
435
+ state.expectedBehavior.trim() === ""))) {
436
+ const { expectedBehavior: _emptyBehavior, ...withoutEmptyBehavior } = state;
437
+ return withoutEmptyBehavior;
438
+ }
439
+ return item;
434
440
  })
435
441
  : rawRecord.uiStates,
436
442
  }
@@ -1,316 +1 @@
1
- /**
2
- * Generation-time frontend project capability discovery (M5/M6).
3
- * Strong evidence only: package.json deps, config files, source imports.
4
- * Lockfile-only or directory-name-only must NOT activate adapters.
5
- */
6
- import { readFile, readdir, stat } from "node:fs/promises";
7
- import path from "node:path";
8
- import { OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE } from "../../shared/openspec-spec.js";
9
- async function exists(filePath) {
10
- try {
11
- await stat(filePath);
12
- return true;
13
- }
14
- catch {
15
- return false;
16
- }
17
- }
18
- async function readJson(filePath) {
19
- try {
20
- return JSON.parse(await readFile(filePath, "utf8"));
21
- }
22
- catch {
23
- return null;
24
- }
25
- }
26
- function allDeps(pkg) {
27
- return {
28
- ...(pkg.dependencies ?? {}),
29
- ...(pkg.devDependencies ?? {}),
30
- ...(pkg.peerDependencies ?? {}),
31
- };
32
- }
33
- function hasDep(deps, name) {
34
- return Object.hasOwn(deps, name);
35
- }
36
- async function listOpenspec(repoRoot) {
37
- const out = [];
38
- for (const specDir of OPENSPEC_SPEC_DIRS) {
39
- const root = path.join(repoRoot, specDir);
40
- if (!(await exists(root)))
41
- continue;
42
- await walk(root, 0);
43
- }
44
- return out.slice(0, 40);
45
- async function walk(dir, depth) {
46
- if (depth > 4)
47
- return;
48
- let entries = [];
49
- try {
50
- entries = await readdir(dir);
51
- }
52
- catch {
53
- return;
54
- }
55
- for (const name of entries) {
56
- const full = path.join(dir, name);
57
- try {
58
- const info = await stat(full);
59
- if (info.isDirectory())
60
- await walk(full, depth + 1);
61
- else if (OPENSPEC_SPEC_EXT_RE.test(name)) {
62
- out.push(path.relative(repoRoot, full).replace(/\\/g, "/"));
63
- }
64
- }
65
- catch {
66
- // skip
67
- }
68
- }
69
- }
70
- }
71
- export function buildAdapterGuidance(capability) {
72
- const lines = [
73
- "## Frontend project capability (generation-time)",
74
- `Framework: ${capability.framework}${capability.frameworkVersion ? `@${capability.frameworkVersion}` : ""}`,
75
- `Evidence: ${capability.evidencePaths.join(", ") || "(none)"}`,
76
- "Rules: openspec specs and task sources outrank adapter tips; do not invent APIs for unknown versions; lockfile-only is not enough.",
77
- ];
78
- if (capability.framework === "react") {
79
- lines.push("React adapter: prefer function components + hooks; reuse existing Testing Library / Vitest patterns; do not introduce new state libs without authorization.");
80
- }
81
- else if (capability.framework === "next") {
82
- lines.push("Next.js adapter: respect App vs Pages router from evidence; keep server/client boundaries explicit; avoid guessing cache/navigation APIs across major versions.");
83
- }
84
- else if (capability.framework === "vue") {
85
- lines.push("Vue adapter: prefer Composition API if project already uses it; reuse Pinia/Vue Router/Vue Query only when evidenced; Vitest + Vue Test Utils when present.");
86
- }
87
- else {
88
- lines.push("Unknown framework: use generic frontend-implementation skill only; do not assume React/Vue/Next APIs.");
89
- }
90
- if (capability.a11y.status === "present") {
91
- lines.push(`A11y tools present: ${capability.a11y.tools.join(", ")}. Report static/component-level evidence only; Browser a11y remains not-run.`);
92
- }
93
- else {
94
- lines.push("A11y tools: absent/unknown — mark a11y checks unavailable/not-run; do not install tools.");
95
- }
96
- if (capability.designEvidence.normativePaths.length) {
97
- lines.push(`Normative design paths: ${capability.designEvidence.normativePaths.slice(0, 8).join(", ")}`);
98
- }
99
- return lines.join("\n");
100
- }
101
- /**
102
- * Discover frontend project capability from repo root.
103
- * Strong evidence: package.json direct deps + config/bootstrap files.
104
- */
105
- export async function discoverFrontendProjectCapability(repoRoot) {
106
- const evidencePaths = [];
107
- const reasons = [];
108
- const pkgPath = path.join(repoRoot, "package.json");
109
- const pkgRaw = await readJson(pkgPath);
110
- if (!pkgRaw || typeof pkgRaw !== "object") {
111
- const openspec = await listOpenspec(repoRoot);
112
- const base = {
113
- schemaVersion: 1,
114
- framework: "unknown",
115
- state: [],
116
- dataFetching: [],
117
- componentLibrary: [],
118
- style: [],
119
- testRunner: [],
120
- mock: [],
121
- a11y: { status: "unknown", tools: [], evidencePaths: [] },
122
- evidencePaths: openspec.slice(0, 5),
123
- reasons: ["package.json missing or unreadable"],
124
- designEvidence: {
125
- normativePaths: openspec,
126
- advisoryPaths: [],
127
- conflicts: [],
128
- },
129
- };
130
- if (openspec.length) {
131
- base.reasons.push(`openspec spec files discovered: ${openspec.slice(0, 5).join(", ")}`);
132
- }
133
- return { ...base, adapterGuidance: buildAdapterGuidance(base) };
134
- }
135
- evidencePaths.push("package.json");
136
- const deps = allDeps(pkgRaw);
137
- // Lockfile alone does not activate
138
- for (const lock of ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]) {
139
- if (await exists(path.join(repoRoot, lock))) {
140
- reasons.push(`${lock} present but not used as sole activation evidence`);
141
- }
142
- }
143
- let framework = "unknown";
144
- let frameworkVersion;
145
- const configHits = [];
146
- const nextConfig = ["next.config.js", "next.config.mjs", "next.config.ts"];
147
- for (const name of nextConfig) {
148
- if (await exists(path.join(repoRoot, name))) {
149
- configHits.push(name);
150
- evidencePaths.push(name);
151
- }
152
- }
153
- if (hasDep(deps, "next") && configHits.some((c) => c.startsWith("next"))) {
154
- framework = "next";
155
- frameworkVersion = deps.next?.replace(/^[^\d]*/, "");
156
- reasons.push("next dependency + next.config*");
157
- }
158
- else if (hasDep(deps, "next") && configHits.length === 0) {
159
- // dep without config: still weak if only transitive — require direct dep
160
- framework = "next";
161
- frameworkVersion = deps.next?.replace(/^[^\d]*/, "");
162
- reasons.push("next direct dependency (config not found)");
163
- evidencePaths.push("package.json#dependencies.next");
164
- }
165
- if (framework === "unknown") {
166
- const vueConfig = ["vite.config.ts", "vite.config.js", "vue.config.js"];
167
- let vueCfg = false;
168
- for (const name of vueConfig) {
169
- if (await exists(path.join(repoRoot, name))) {
170
- vueCfg = true;
171
- evidencePaths.push(name);
172
- }
173
- }
174
- if (hasDep(deps, "vue") && (vueCfg || hasDep(deps, "nuxt"))) {
175
- framework = "vue";
176
- frameworkVersion = deps.vue?.replace(/^[^\d]*/, "");
177
- reasons.push("vue direct dependency + config/tooling");
178
- }
179
- }
180
- if (framework === "unknown" && hasDep(deps, "react")) {
181
- // Need config or jsx tooling — not lockfile-only
182
- const reactCfg = (await exists(path.join(repoRoot, "vite.config.ts"))) ||
183
- (await exists(path.join(repoRoot, "vite.config.js"))) ||
184
- (await exists(path.join(repoRoot, "webpack.config.js"))) ||
185
- hasDep(deps, "react-scripts") ||
186
- hasDep(deps, "vite");
187
- if (reactCfg) {
188
- framework = "react";
189
- frameworkVersion = deps.react?.replace(/^[^\d]*/, "");
190
- reasons.push("react direct dependency + bundler/tooling evidence");
191
- evidencePaths.push("package.json#dependencies.react");
192
- }
193
- else {
194
- reasons.push("react listed but no strong config/bundler evidence — framework=unknown");
195
- }
196
- }
197
- const state = [];
198
- for (const lib of [
199
- "redux",
200
- "@reduxjs/toolkit",
201
- "zustand",
202
- "jotai",
203
- "recoil",
204
- "pinia",
205
- "vuex",
206
- ]) {
207
- if (hasDep(deps, lib))
208
- state.push(lib);
209
- }
210
- const dataFetching = [];
211
- for (const lib of [
212
- "@tanstack/react-query",
213
- "swr",
214
- "axios",
215
- "@tanstack/vue-query",
216
- "ofetch",
217
- ]) {
218
- if (hasDep(deps, lib))
219
- dataFetching.push(lib);
220
- }
221
- const componentLibrary = [];
222
- for (const lib of [
223
- "antd",
224
- "@mui/material",
225
- "element-plus",
226
- "vuetify",
227
- "@chakra-ui/react",
228
- ]) {
229
- if (hasDep(deps, lib))
230
- componentLibrary.push(lib);
231
- }
232
- const style = [];
233
- for (const lib of [
234
- "tailwindcss",
235
- "sass",
236
- "less",
237
- "styled-components",
238
- "@emotion/react",
239
- ]) {
240
- if (hasDep(deps, lib))
241
- style.push(lib);
242
- }
243
- const testRunner = [];
244
- for (const lib of [
245
- "vitest",
246
- "jest",
247
- "@testing-library/react",
248
- "@testing-library/vue",
249
- "@playwright/test",
250
- ]) {
251
- if (hasDep(deps, lib))
252
- testRunner.push(lib);
253
- }
254
- const mock = [];
255
- for (const lib of ["msw", "nock", "miragejs"]) {
256
- if (hasDep(deps, lib))
257
- mock.push(lib);
258
- }
259
- const a11yTools = [];
260
- const a11yEvidence = [];
261
- for (const lib of [
262
- "eslint-plugin-jsx-a11y",
263
- "@axe-core/react",
264
- "jest-axe",
265
- "vitest-axe",
266
- "axe-core",
267
- ]) {
268
- if (hasDep(deps, lib)) {
269
- a11yTools.push(lib);
270
- a11yEvidence.push(`package.json#${lib}`);
271
- }
272
- }
273
- const a11y = {
274
- status: (a11yTools.length > 0 ? "present" : "absent"),
275
- tools: a11yTools,
276
- evidencePaths: a11yEvidence,
277
- };
278
- let router;
279
- if (hasDep(deps, "react-router-dom") || hasDep(deps, "react-router")) {
280
- router = "react-router";
281
- }
282
- if (framework === "next")
283
- router = router ?? "next-router";
284
- if (hasDep(deps, "vue-router"))
285
- router = "vue-router";
286
- const openspec = await listOpenspec(repoRoot);
287
- const designEvidence = {
288
- normativePaths: openspec,
289
- advisoryPaths: [],
290
- conflicts: [],
291
- };
292
- if (openspec.length)
293
- evidencePaths.push(...openspec.slice(0, 5));
294
- const base = {
295
- schemaVersion: 1,
296
- framework,
297
- frameworkVersion,
298
- router,
299
- state,
300
- dataFetching,
301
- componentLibrary,
302
- style,
303
- testRunner,
304
- mock,
305
- a11y,
306
- packageManager: (await exists(path.join(repoRoot, "pnpm-lock.yaml")))
307
- ? "pnpm"
308
- : (await exists(path.join(repoRoot, "yarn.lock")))
309
- ? "yarn"
310
- : "npm",
311
- evidencePaths: [...new Set(evidencePaths)],
312
- reasons,
313
- designEvidence,
314
- };
315
- return { ...base, adapterGuidance: buildAdapterGuidance(base) };
316
- }
1
+ export * from "../../task/frontend-project-capability.js";
@@ -33,7 +33,10 @@ function splitLines(text) {
33
33
  .filter(Boolean);
34
34
  }
35
35
  function statusPaths(status) {
36
- return splitLines(status)
36
+ return status
37
+ .split(/\r?\n/)
38
+ .map((line) => line.trimEnd())
39
+ .filter((line) => line.length >= 4)
37
40
  .map((line) => line.slice(3).split(" -> ").at(-1) ?? "")
38
41
  .filter(Boolean)
39
42
  .sort();
@@ -29,6 +29,7 @@ import { buildFrontendCaseEvidenceValidateShellSnippet, buildFrontendTestOutcome
29
29
  import { classifyFrontendRisk, } from "./frontend-risk.js";
30
30
  import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
31
31
  import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
32
+ import { serializeDagTaskSourcePath } from "../../task/dag-source-paths.js";
32
33
  const REQUIREMENT_FILE = "需求.md";
33
34
  const CONSTRAINT_FILE = "执行约束.md";
34
35
  const REFERENCE_DIRECTORY = "references";
@@ -1062,8 +1063,12 @@ function deriveFrontendBehaviorPaths(taskConfig) {
1062
1063
  return explicitTestPaths;
1063
1064
  return taskConfig.allowedPaths;
1064
1065
  }
1065
- function toTaskRelativeSourcePath(sources, absolutePath) {
1066
- return path.relative(sources.taskDir, absolutePath).replaceAll(path.sep, "/");
1066
+ function toDagSourcePath(sources, absolutePath) {
1067
+ return serializeDagTaskSourcePath({
1068
+ repoRoot: sources.repoRoot,
1069
+ taskDir: sources.taskDir,
1070
+ absolutePath,
1071
+ });
1067
1072
  }
1068
1073
  function extractExplicitRequirementIds(...markdownInputs) {
1069
1074
  const ids = [];
@@ -1124,7 +1129,7 @@ function buildDagSourceBinding(sources) {
1124
1129
  taskId: sources.taskId,
1125
1130
  sources: sourceEntries.map((source) => ({
1126
1131
  kind: source.kind,
1127
- path: toTaskRelativeSourcePath(sources, source.path),
1132
+ path: toDagSourcePath(sources, source.path),
1128
1133
  sha256: createHash("sha256")
1129
1134
  .update(source.markdown, "utf8")
1130
1135
  .digest("hex"),
@@ -1149,13 +1154,13 @@ function buildBackendTestAnalysisSourceBindingContract(sources) {
1149
1154
  };
1150
1155
  }
1151
1156
  function buildSourceContextBlock(sources) {
1152
- const requirementRef = toTaskRelativeSourcePath(sources, sources.requirementPath);
1157
+ const requirementRef = toDagSourcePath(sources, sources.requirementPath);
1153
1158
  const requirementExcerpt = excerptMarkdown(sources.requirementMarkdown, {
1154
1159
  sourceRef: requirementRef,
1155
1160
  });
1156
1161
  const parts = ["## Task source: 需求.md", requirementExcerpt.text];
1157
1162
  if (sources.constraintMarkdown) {
1158
- const constraintRef = toTaskRelativeSourcePath(sources, sources.constraintPath);
1163
+ const constraintRef = toDagSourcePath(sources, sources.constraintPath);
1159
1164
  const constraintExcerpt = excerptMarkdown(sources.constraintMarkdown, {
1160
1165
  sourceRef: constraintRef,
1161
1166
  });
@@ -1165,7 +1170,7 @@ function buildSourceContextBlock(sources) {
1165
1170
  const relativePath = path
1166
1171
  .relative(path.join(sources.taskDir, "source"), reference.path)
1167
1172
  .replaceAll(path.sep, "/");
1168
- const referenceRef = toTaskRelativeSourcePath(sources, reference.path);
1173
+ const referenceRef = toDagSourcePath(sources, reference.path);
1169
1174
  const referenceExcerpt = excerptMarkdown(reference.markdown, {
1170
1175
  sourceRef: referenceRef,
1171
1176
  });
@@ -1289,7 +1294,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
1289
1294
  };
1290
1295
  return sources;
1291
1296
  }
1292
- async function prepareFrontendMockSources(sources) {
1297
+ async function prepareFrontendMockSources(sources, discoveredProjectCapability) {
1293
1298
  const repoRoot = sources.repoRoot ?? process.cwd();
1294
1299
  const capability = await discoverFrontendMockCapability(repoRoot, sources.taskConfig);
1295
1300
  const sourceMockVerifyCommands = extractFrontendMockVerifyCommandsFromMarkdown({
@@ -1302,7 +1307,8 @@ async function prepareFrontendMockSources(sources) {
1302
1307
  capability.verifyCommands.push(command);
1303
1308
  }
1304
1309
  }
1305
- const projectCapability = await discoverFrontendProjectCapability(repoRoot);
1310
+ const projectCapability = discoveredProjectCapability ??
1311
+ (await discoverFrontendProjectCapability(repoRoot));
1306
1312
  const frontendRisk = classifyFrontendRisk({
1307
1313
  title: sources.taskConfig.title,
1308
1314
  requirementMarkdown: sources.requirementMarkdown,
@@ -1936,6 +1942,8 @@ async function buildFrontendHybridDagFromTask(sources) {
1936
1942
  "## Critical rules",
1937
1943
  "- verificationTargets is a TOP-LEVEL required array",
1938
1944
  "- uiStates items use name/applicable/expectedBehavior/implementationTargets/verificationTargetIds/notApplicableReason",
1945
+ "- Use uiStates: [] for frontend logic changes with no user-visible UI state. Do not invent UI states.",
1946
+ "- For applicable=true, provide non-empty expectedBehavior plus non-empty implementationTargets and verificationTargetIds. For applicable=false, provide non-empty notApplicableReason and omit expectedBehavior instead of emitting an empty string.",
1939
1947
  "- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
1940
1948
  "- All implementation files, verification files, symbols, and commands must be discovered from the current target workspace and current task. Never copy paths, symbols, or commands from the loop-agent repository, an example task, or prior run output.",
1941
1949
  "- Use relative POSIX paths rooted at the target workspace. Do not assume a particular src/test directory layout; preserve the target project's actual app/, packages/, spec/, __tests__, or other layout.",
@@ -2299,11 +2307,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2299
2307
  ...(lintShellCommands.length > 0
2300
2308
  ? ["frontend-lint-baseline-shell"]
2301
2309
  : []),
2302
- "frontend-plan-revision-pi",
2303
- "frontend-final-design-review-pi",
2304
- "frontend-plan-pi",
2305
2310
  ],
2306
- dependsPolicy: "all-or-condition-skip",
2307
2311
  role: "implementer",
2308
2312
  executor: "pi",
2309
2313
  toolProfile: "write",
@@ -2316,6 +2320,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2316
2320
  outputContract: "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.",
2317
2321
  subtask_prompt: [
2318
2322
  "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.",
2323
+ "The canonical contract already contains the approved requirement, target-file, UI-state, verification, design, and Mock/API decisions. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, or design-review prose, and do not repeat broad repository research. Inspect only contract target files and directly related local code needed to implement them.",
2319
2324
  "Execute in fixed stages and report each in the delivery summary: (1) Contract confirm, (2) Tests sync, (3) Component/UI state implementation, (4) API/Mock wiring per contract.mockApi, (5) Focused checks behind frozen entrypoints only, (6) Diff cleanup.",
2320
2325
  "Map every requirement id and applicable UI state from the contract to concrete files. Do not invent shell verification commands; only frozen static/behavior entrypoints will run.",
2321
2326
  "Implement only the approved Mock strategy carried by the validated contract. Preserve the real request path as the default, require explicit test/dev activation, and never comment out or replace the real request with inline data.",
@@ -2323,8 +2328,6 @@ async function buildFrontendHybridDagFromTask(sources) {
2323
2328
  "For native, browser-intercept, or request-adapter, implement contract-aligned fixtures/states and a dev/test-only activation boundary in this same writer. For not-needed, do not add Mock files or a framework and state the positive reason.",
2324
2329
  "Do not write root artifacts/** unless explicitly included in writeSet. Do not claim Browser/visual verification.",
2325
2330
  writerDeliveryContract(taskConfig),
2326
- sourceContext,
2327
- mockContextBlock,
2328
2331
  ]
2329
2332
  .filter((value) => Boolean(value))
2330
2333
  .join("\n\n"),
@@ -2379,11 +2382,10 @@ async function buildFrontendHybridDagFromTask(sources) {
2379
2382
  subtask_prompt: [
2380
2383
  "Read contracts/frontend-repair-assessment.json and the validated frontend implementation contract.",
2381
2384
  "This node runs only for eligible=true. Apply the smallest fix for the classified repairable failure inside the original implement writeSet only.",
2385
+ "The repair assessment and canonical implementation contract are complete inputs for this phase. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, or design-review prose, and do not repeat repository-wide discovery.",
2382
2386
  "Do not change lint/type/test config, do not add .skip/.only, do not comment out real requests, do not default-enable Mock, do not add dependencies.",
2383
2387
  "Do not re-plan requirements or expand allowed paths. Browser/visual remain not-run.",
2384
2388
  writerDeliveryContract(taskConfig),
2385
- sourceContext,
2386
- mockContextBlock,
2387
2389
  ]
2388
2390
  .filter((value) => Boolean(value))
2389
2391
  .join("\n\n"),
@@ -2449,19 +2451,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2449
2451
  },
2450
2452
  {
2451
2453
  id: "frontend-review-pi",
2452
- depends_on: [
2453
- "frontend-review-context-shell",
2454
- "frontend-reverify-shell",
2455
- "frontend-repair-pi",
2456
- "frontend-verify-assess-shell",
2457
- implementId,
2458
- "frontend-prewrite-gate-shell",
2459
- "frontend-plan-pi",
2460
- "frontend-design-review-pi",
2461
- "frontend-plan-revision-pi",
2462
- "frontend-final-design-review-pi",
2463
- ],
2464
- dependsPolicy: "all-or-condition-skip",
2454
+ depends_on: ["frontend-review-context-shell"],
2465
2455
  role: "reviewer",
2466
2456
  executor: "pi",
2467
2457
  complexity: "HIGH",
@@ -2477,13 +2467,11 @@ async function buildFrontendHybridDagFromTask(sources) {
2477
2467
  "Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, frontend lint assessment when lint is configured, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
2478
2468
  "Treat lint status exactly as passed | baseline-debt | failed | unavailable. baseline-debt may continue only with intact evidence and zero diagnostics on writer-changed files; report the tolerated debt count and never rewrite it as lint passed. Typecheck, build, and test still require successful final exits.",
2479
2469
  "Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
2480
- "Use the direct contract and the effective plan/design branch: original plan plus initial pass when revision was skipped, or revised plan plus final design review when revision ran. Do not infer them from the implementation summary.",
2470
+ "The contract embedded in frontend-review-context.json is the effective reviewed plan materialized by the prewrite gate. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, design-review, writer summary, or verification node prose. Inspect only the canonical review context, its bound diff, and diff-referenced files when semantic review requires source code.",
2481
2471
  "Treat a commented-out real request, default-enabled Mock, production entrypoint importing test mocks, API/fixture contract drift, unauthorized Mock dependency/path, or missing behavior evidence for the selected strategy as at least Important. Mock strategies require Mock-backed evidence. not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case verify that the real request remains the default and the Real Integration Gap is preserved.",
2482
2472
  "Inspect the frontend-verify-assess-shell or selected frontend-reverify-shell evidence in the review context directly, including the production/default-real-path static check, and require Mock activation to be off for that check.",
2483
2473
  "Distinguish Mock-backed evidence from real API integration evidence and preserve the Real Integration Gap when the backend was not exercised.",
2484
2474
  "Review implementation quality, behavior/state coverage, verification evidence, and maintainability. Read-only: do not modify files.",
2485
- sourceContext,
2486
- mockContextBlock,
2487
2475
  ].join("\n\n"),
2488
2476
  },
2489
2477
  {
@@ -2512,15 +2500,9 @@ async function buildFrontendHybridDagFromTask(sources) {
2512
2500
  {
2513
2501
  id: "frontend-closeout-pi",
2514
2502
  depends_on: [
2515
- "frontend-review-gate-shell",
2516
- "frontend-review-pi",
2517
2503
  "frontend-review-context-shell",
2518
- "frontend-reverify-shell",
2519
- "frontend-repair-pi",
2520
- "frontend-verify-assess-shell",
2521
- "frontend-prewrite-gate-shell",
2504
+ "frontend-review-gate-shell",
2522
2505
  ],
2523
- dependsPolicy: "all-or-condition-skip",
2524
2506
  role: "closeout",
2525
2507
  executor: "pi",
2526
2508
  complexity: "MED",
@@ -2533,12 +2515,11 @@ async function buildFrontendHybridDagFromTask(sources) {
2533
2515
  outputContract: "Markdown closeout summary with Changes, Mock Decision / Strategy / Files / Verification / Production Boundary, Verification Evidence, Review Result, Frontend Status, Real Integration Status, Known Risks, and Follow-up. No file writes.",
2534
2516
  subtask_prompt: [
2535
2517
  "Return a frontend closeout summary covering Mock decision/strategy/files/verification/production boundary, changes, verification evidence, review result, known risks, and follow-up.",
2518
+ "Use only the canonical frontend-review-context.json plus the final frontend review gate verdict. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, design-review, writer, repair, or verification prose, and do not perform new repository research during closeout.",
2536
2519
  "Include a coverage matrix for each requirement id, applicable UI state, and verification target/check with status passed|failed|not-run|blocked|unavailable. Report lint separately as passed|baseline-debt|failed|unavailable; baseline-debt is explicit debt, not passed. Always state Browser accessibility verification: not-run and Visual regression: not-run. Use contracts/frontend-review-context.json and the effective frontend-verify-assess-shell or frontend-reverify-shell facts; do not invent Browser evidence from component tests.",
2537
2520
  `When only Mock-backed evidence passed, state exactly Frontend status: mock-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task to create/run after backend readiness. This follow-up is not auto-created or auto-executed. Never describe Mock evidence as real API integration.`,
2538
2521
  `When Mock was skipped in auto mode and no real API evidence passed, state exactly Frontend status: locally-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task when backend readiness matters.`,
2539
2522
  "Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
2540
- sourceContext,
2541
- mockContextBlock,
2542
2523
  ].join("\n\n"),
2543
2524
  },
2544
2525
  ],
@@ -4940,11 +4921,22 @@ function assertNoGovernanceFlagOnDisallowedTemplate(spec, template) {
4940
4921
  }
4941
4922
  }
4942
4923
  export async function buildHybridDagFromTask(sources, options = {}) {
4924
+ const routingProjectCapability = sources.taskConfig.taskKind === "standard" &&
4925
+ (options.template === undefined || options.template === "standard-dag") &&
4926
+ sources.repoRoot
4927
+ ? await discoverFrontendProjectCapability(sources.repoRoot)
4928
+ : sources.frontendProjectCapability;
4943
4929
  const selection = resolveTaskDagTemplateSelection({
4944
4930
  taskKind: sources.taskConfig.taskKind,
4945
4931
  title: sources.taskConfig.title,
4946
4932
  requirementMarkdown: sources.requirementMarkdown,
4947
4933
  allowedPaths: sources.taskConfig.allowedPaths,
4934
+ frontendProject: routingProjectCapability
4935
+ ? {
4936
+ framework: routingProjectCapability.framework,
4937
+ evidencePaths: routingProjectCapability.evidencePaths,
4938
+ }
4939
+ : undefined,
4948
4940
  requestedTemplate: options.template,
4949
4941
  });
4950
4942
  return buildHybridDagForTemplate(sources, selection.template);
@@ -5566,17 +5558,27 @@ export function defaultHybridDagOutputPath(taskId) {
5566
5558
  return path.join(os.tmpdir(), `${taskId}-hybrid-dag.json`);
5567
5559
  }
5568
5560
  export async function writeHybridDagDraft(sources, outputPath, options = {}) {
5561
+ const routingProjectCapability = sources.taskConfig.taskKind === "standard" &&
5562
+ (options.template === undefined || options.template === "standard-dag")
5563
+ ? await discoverFrontendProjectCapability(sources.repoRoot ?? process.cwd())
5564
+ : sources.frontendProjectCapability;
5569
5565
  const templateSelection = resolveTaskDagTemplateSelection({
5570
5566
  taskKind: sources.taskConfig.taskKind,
5571
5567
  title: sources.taskConfig.title,
5572
5568
  requirementMarkdown: sources.requirementMarkdown,
5573
5569
  allowedPaths: sources.taskConfig.allowedPaths,
5570
+ frontendProject: routingProjectCapability
5571
+ ? {
5572
+ framework: routingProjectCapability.framework,
5573
+ evidencePaths: routingProjectCapability.evidencePaths,
5574
+ }
5575
+ : undefined,
5574
5576
  requestedTemplate: options.template,
5575
5577
  });
5576
5578
  const template = templateSelection.template;
5577
5579
  assertTaskAllowedPathsPreflight(sources.taskConfig);
5578
5580
  const preparedSources = template === "frontend-implementation"
5579
- ? await prepareFrontendMockSources(sources)
5581
+ ? await prepareFrontendMockSources(sources, routingProjectCapability)
5580
5582
  : sources;
5581
5583
  const spec = await buildHybridDagForTemplate(preparedSources, template);
5582
5584
  if (preparedSources.repoRoot && dagHasWriterExecution(spec)) {