@tea-agent/loop-agent 0.24.0 → 0.24.2-beta.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.
- package/CHANGELOG.md +45 -0
- package/dist/commands/init.js +2 -2
- package/dist/task/dag-source-paths.js +50 -0
- package/dist/task/frontend-project-capability.js +316 -0
- package/dist/task/task-demand-routing.js +432 -0
- package/dist/worker/console/chat/compaction-errors.js +64 -0
- package/dist/worker/console/chat/interview-adapter.js +17 -4
- package/dist/worker/console/chat/routes.js +6 -1
- package/dist/worker/console/interview/grill-me.js +28 -5
- package/dist/worker/console/static/assets/index-DePjoS_G.js +253 -0
- package/dist/worker/console/static/assets/index-Dp_2sKGk.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/routes.js +8 -4
- package/dist/workflows/dag/backend-test-intake-context.js +10 -12
- package/dist/workflows/dag/backend-test-markdown-workflow.js +223 -42
- package/dist/workflows/dag/backend-test-result-contract.js +6 -2
- package/dist/workflows/dag/frontend-implementation-contract.js +23 -17
- package/dist/workflows/dag/frontend-project-capability.js +1 -316
- package/dist/workflows/dag/frontend-worktree-diff.js +4 -1
- package/dist/workflows/dag/init-hybrid.js +44 -42
- package/dist/workflows/dag/rerun-plan.js +6 -10
- package/dist/workflows/dag/task-demand-routing.js +1 -383
- package/docs/templates/frontend-implementation-contract.schema.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-implementation/SKILL.md +7 -0
- package/skills/frontend-implementation/references/node-contracts.md +1 -1
- package/skills/frontend-verification/SKILL.md +4 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/index-BTbrEHnO.css +0 -1
- 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())
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
|
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
|
|
1066
|
-
return
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
"
|
|
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-
|
|
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)) {
|