@tea-agent/loop-agent 0.44.0-next.10 → 0.44.0-next.12

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 (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/dag-pi/sessions/index.js +6 -0
  4. package/dist/executors/dag-pi/sessions/plan-batches.js +280 -0
  5. package/dist/executors/dag-pi/sessions/plan-prompts.js +367 -0
  6. package/dist/executors/dag-pi/sessions/scout-parallel.js +197 -0
  7. package/dist/executors/dag-pi/sessions/segmented-plan.js +1184 -0
  8. package/dist/executors/dag-pi/sessions/writer-evidence.js +60 -0
  9. package/dist/executors/dag-pi-executor.js +12 -2075
  10. package/dist/executors/shell-executor.js +39 -11
  11. package/dist/worker/console/routes.js +5 -0
  12. package/dist/worker/console/static/app-icon.svg +39 -0
  13. package/dist/worker/console/static/index.html +3 -0
  14. package/dist/worker/console/static/manifest.webmanifest +19 -0
  15. package/dist/worker/observe/static/console-theme.js +38 -0
  16. package/dist/workflows/dag/checkpoint.js +686 -0
  17. package/dist/workflows/dag/frontend-repair.js +24 -0
  18. package/dist/workflows/dag/frontend-review-scopes.js +10 -2
  19. package/dist/workflows/dag/frontend-test-execution-evidence.js +163 -67
  20. package/dist/workflows/dag/frontend-test-framework-adapters.js +309 -0
  21. package/dist/workflows/dag/hybrid/sources.js +1812 -0
  22. package/dist/workflows/dag/hybrid/templates/backend-test.js +1807 -0
  23. package/dist/workflows/dag/hybrid/templates/frontend-test.js +702 -0
  24. package/dist/workflows/dag/hybrid/templates/frontend.js +1529 -0
  25. package/dist/workflows/dag/hybrid/templates/index.js +8 -0
  26. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +449 -0
  27. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +495 -0
  28. package/dist/workflows/dag/hybrid/templates/shared.js +101 -0
  29. package/dist/workflows/dag/hybrid/templates/standard.js +265 -0
  30. package/dist/workflows/dag/hybrid/types.js +32 -0
  31. package/dist/workflows/dag/init-hybrid.js +42 -7162
  32. package/dist/workflows/dag/runner.js +11 -1178
  33. package/dist/workflows/dag/terminal-status.js +502 -0
  34. package/docs/architecture/dag-execution.md +4 -4
  35. package/package.json +1 -1
@@ -0,0 +1,1529 @@
1
+ /** DAG hybrid frontend-implementation 模板族:writer/review/design 节点装配与 mock 阻断 DAG。 */
2
+ import path from "node:path";
3
+ import { inspectFrontendTestCommand } from "../../frontend-test-execution-evidence.js";
4
+ import { createHash } from "node:crypto";
5
+ import { frontendExecutionPolicySchema } from "../../../../shared/frontend-execution-policy.js";
6
+ import { readFile } from "node:fs/promises";
7
+ import { fileURLToPath } from "node:url";
8
+ import { assertValidDagSpec } from "../../validate.js";
9
+ import { DEFAULT_DAG_EXECUTOR_MODELS, DEFAULT_DAG_OUTPUT_LANGUAGE, parseDagSpec } from "../../types.js";
10
+ import { DEFAULT_FRONTEND_SPEC_ROOTS, DEFAULT_OPENSPEC_GOVERNANCE_ROOT, extractTaskSourceFrontendSpecPaths } from "../../../../shared/openspec-spec.js";
11
+ import { readFrontendSpecRegistry, scoreFrontendSpecCandidate } from "../../../../task/frontend-project-capability.js";
12
+ import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, FRONTEND_PLAN_LADDER_RETRY_POLICY, FRONTEND_REVIEW_TERMINAL_RETRY_POLICY, FRONTEND_SCOUT_COMPLETENESS_RETRY_POLICY } from "../../retry-policy.js";
13
+ import { DEFAULT_VERIFY_TIMEOUT_MS } from "../../../../executors/shell-verification.js";
14
+ import { classifyFrontendRisk } from "../../frontend-risk.js";
15
+ import { resolveFrontendSpecRootAliases } from "../../frontend-project-capability.js";
16
+ import { buildFrontendImplementationContractSkeleton, buildFrontendVerifyCommandDirectory, classifyFrontendVerifyCommandText } from "../../frontend-implementation-contract.js";
17
+ import { computeFrontendShapeSourceDigest, parseFrontendShapeTransitionCapsule, resolveFrontendTaskShape } from "../../frontend-shape.js";
18
+ import { FRONTEND_NO_VERIFICATION_MARKER_TEXT } from "../../frontend-verification-trace.js";
19
+ import { resolveWriterComplexity, writerDeliveryContract } from "../types.js";
20
+ import { FRONTEND_BOUNDED_IMPLEMENT_SKILLS, FRONTEND_CONTRACT_SKILLS, FRONTEND_DEFAULTS, FRONTEND_DESIGN_REVIEW_SKILLS, FRONTEND_PLAN_SKILLS, FRONTEND_REVIEW_SKILLS, FRONTEND_SCOUT_SKILLS, FRONTEND_SKILLS_BY_ROLE, GENERATED_DAG_RUNTIME_CONTRACT, STANDARD_GLOBAL_CONSTRAINTS, buildDagSourceBinding, buildExplicitFrontendVerifyCommands, buildSourceContextBlock, buildVerifyEvidence, buildVerifyShellCommands, collectDeclaredDependencies, discoverFrontendFallbackVerifyCommands, extractFrontendVerifyCommandsFromMarkdown, extractObjective, extractSuccessCriteria, frontendSourceMentionsMock, hasApiDependency, mergeForbiddenPaths, partitionFrontendStaticVerifyCommands, resolveDagVerifyStrategy, resolveFrontendMockMode, resolveImplementPaths, verifyCommandKey } from "../sources.js";
21
+ import { applyDefaultReadOnlyRetryPolicy, mapTaskComplexity, stampGeneratedArtifactBindings, } from "./shared.js";
22
+ export function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, fixedVerificationContext, readOnlyPaths, forbiddenPaths) {
23
+ const configuredPolicy = sources.taskConfig.frontendMock?.policy ?? "auto";
24
+ const mockMode = sources.frontendMockMode ?? "not-required";
25
+ const capabilityStatus = sources.frontendMockCapability?.status;
26
+ const autoMaySkipMissingMock = configuredPolicy === "auto" &&
27
+ mockMode === "not-required" &&
28
+ (capabilityStatus === "absent" || capabilityStatus === "ambiguous");
29
+ return {
30
+ id: "frontend-mock-assess-pi",
31
+ depends_on: ["frontend-contract-pi", "frontend-scout-pi"],
32
+ role: "planner",
33
+ executor: "pi",
34
+ complexity: "MED",
35
+ writePolicy: "read-only",
36
+ allowedPaths: readOnlyPaths,
37
+ forbiddenPaths,
38
+ skills: FRONTEND_PLAN_SKILLS,
39
+ firstProtocolLine: "MOCK_STRATEGY:",
40
+ outputContract: "Plain Markdown whose first line is MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked, followed by Mock Decision, API Contract Evidence, Specification Evidence, Service Evidence, Backend Readiness, Selection Evidence, Endpoint / Fixture Matrix, Activation, Target Files, Production Safety, Verification Plan, Real Integration Gap, and Blocking Issues. No file writes.",
41
+ subtask_prompt: [
42
+ "Perform read-only Mock assessment and select one safe frontend data strategy.",
43
+ "The first line must be exactly one of: MOCK_STRATEGY: native, MOCK_STRATEGY: browser-intercept, MOCK_STRATEGY: request-adapter, MOCK_STRATEGY: not-needed, or MOCK_STRATEGY: blocked. Do not emit blank lines, headings, or explanatory preamble before it.",
44
+ "Do not wrap the first line in Markdown bullets, quotes, emphasis, code fences, inline code, Chinese punctuation, or a split label/value form. Put any headings or explanation only after the exact first line.",
45
+ "Prefer an existing native Mock facility. Use browser-intercept only with an existing browser/e2e harness. When no Mock exists but the API layer is writable, use request-adapter by adding a minimal reversible adapter/DI seam within the approved writeSet; the real adapter must remain the production default.",
46
+ autoMaySkipMissingMock
47
+ ? "Auto mode may skip Mock when no project Mock capability is confirmed. Select not-needed with positive evidence from contract/scout that no project Mock capability is confirmed, continue without adding Mock files or dependencies, run the fixed verification entrypoints, and record any unproved real API behavior in Real Integration Gap. Do not block solely because no project Mock capability, browser interception harness, or request adapter exists."
48
+ : "Select not-needed only with positive evidence that no remote API is involved, a stable real backend will be exercised, or existing fixtures already cover the contract without changes. not-needed still requires the fixed behavior entrypoint to exercise applicable real or no-remote behavior verification. When configured policy is required, not-needed is forbidden.",
49
+ "Configured policy disabled requests no Mock but cannot override project specifications; if an actually-read project rule requires Mock, select blocked.",
50
+ "",
51
+ "## Required Output Sections:",
52
+ "- Mock Decision: required | not-required | blocked (with reasoning)",
53
+ "- API Contract Evidence and Specification Evidence: actual Mock/API/schema specs read (paths + excerpts)",
54
+ "- Service Evidence: detected Mock framework, service root, handler/fixture/bootstrap paths",
55
+ "- Backend Readiness and Selection Evidence: why the selected mechanism is available and appropriate",
56
+ "- Endpoint / Fixture Matrix: method/path, source, request, success, empty, error, permission, consumer, fixture/evidence",
57
+ "- Activation and Target Files: explicit dev/test activation and authorized implementation paths",
58
+ "- Production Safety: how Mock stays off and the real request remains default",
59
+ "- Verification Plan: map the strategy to the fixed entrypoints below; do not propose replacement shell commands",
60
+ "- Real Integration Gap: what remains unproved until the real backend is ready",
61
+ "- Blocking Issues: any spec gaps, path violations, missing verify commands, or conflicts",
62
+ "",
63
+ "## Rules:",
64
+ "- Read project Mock/API/schema specifications before making any judgment.",
65
+ "- Do not infer Mock service from lockfile-only or transitive dependency evidence.",
66
+ "- Output MOCK_STRATEGY: blocked if capability evidence conflicts, contract fields are missing/conflicting, paths or dependencies are unauthorized, specs were not actually read, sources conflict, production-default-off cannot be proven, the API layer is not writable for a new adapter, or the frozen entrypoints cannot verify the selected strategy.",
67
+ ...(autoMaySkipMissingMock
68
+ ? [
69
+ "- In auto mode with no confirmed project Mock capability, do not block solely because Mock is unavailable or Mock-specific verification is absent; choose not-needed and make the Real Integration Gap explicit unless project specs require Mock or another safety rule is violated.",
70
+ ]
71
+ : []),
72
+ "- Never comment out or replace the real request with inline data, hard-code Mock enablement, import test mocks from a production entrypoint, invent API fields, or place secrets/real user data in fixtures.",
73
+ "- Mock-backed behavior evidence proves the documented frontend contract only; it never proves real API integration.",
74
+ "",
75
+ "Read-only: do not modify repository files.",
76
+ fixedVerificationContext,
77
+ sourceContext,
78
+ mockContextBlock,
79
+ ].join("\n\n"),
80
+ };
81
+ }
82
+ export function buildFrontendMockContractGateNode(mockMode, configuredPolicy, readOnlyPaths, forbiddenPaths) {
83
+ // A generation-time blocked decision is a hard fail-closed contract. Keep a
84
+ // syntactically valid, impossible verdict so the shell gate can never pass
85
+ // regardless of what the assessment model emits.
86
+ const acceptedStrategies = mockMode === "blocked"
87
+ ? ["MOCK_STRATEGY: __blocked__"]
88
+ : configuredPolicy === "disabled"
89
+ ? ["MOCK_STRATEGY: not-needed"]
90
+ : [
91
+ "MOCK_STRATEGY: native",
92
+ "MOCK_STRATEGY: browser-intercept",
93
+ "MOCK_STRATEGY: request-adapter",
94
+ ...(configuredPolicy !== "required"
95
+ ? ["MOCK_STRATEGY: not-needed"]
96
+ : []),
97
+ ];
98
+ return {
99
+ id: "frontend-mock-contract-gate-shell",
100
+ depends_on: ["frontend-mock-assess-pi"],
101
+ role: "verifier",
102
+ executor: "shell",
103
+ complexity: "LOW",
104
+ writePolicy: "read-only",
105
+ allowedPaths: readOnlyPaths,
106
+ forbiddenPaths,
107
+ outputContract: "Deterministic Mock contract gate: exit 0 only when frontend-mock-assess-pi selects an allowed non-blocked strategy. Does not authorize code writes.",
108
+ subtask_prompt: "Deterministic gate: block plan/design/implement when frontend-mock-assess-pi selected blocked, selected not-needed under explicit policy=required, or emitted malformed output. Failure route: ContractMismatch.",
109
+ shell: {
110
+ commands: [],
111
+ verdictGate: {
112
+ fromNodeId: "frontend-mock-assess-pi",
113
+ accept: acceptedStrategies,
114
+ label: "frontend mock contract",
115
+ lineMode: "first-non-empty",
116
+ },
117
+ cwd: ".",
118
+ timeoutMs: 60000,
119
+ },
120
+ };
121
+ }
122
+ export function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbiddenPaths) {
123
+ const capability = sources.frontendMockCapability;
124
+ const taskConfig = sources.taskConfig;
125
+ // Collect verify commands from task config, capability seed, and manifest
126
+ const verifyCommands = [];
127
+ // 1. Task config commands (highest priority)
128
+ for (const cmd of taskConfig.frontendMock?.verifyCommands ?? []) {
129
+ verifyCommands.push({
130
+ label: cmd.label,
131
+ args: ["bash", "-lc", cmd.command],
132
+ cwd: sources.repoRoot ?? ".",
133
+ timeoutMs: cmd.timeoutMs,
134
+ });
135
+ }
136
+ // 2. Capability seed commands (from discovery)
137
+ if (capability) {
138
+ for (const cmd of capability.verifyCommands) {
139
+ if (!verifyCommands.some((existing) => existing.label === cmd.label)) {
140
+ verifyCommands.push(cmd);
141
+ }
142
+ }
143
+ }
144
+ // Fail closed: no commands = no verify shell
145
+ const commands = verifyCommands.length > 0
146
+ ? buildVerifyShellCommands({
147
+ repoRoot: sources.repoRoot ?? ".",
148
+ commands: verifyCommands,
149
+ fallbackCommands: [],
150
+ })
151
+ : [];
152
+ return {
153
+ id: "frontend-mock-verify-shell",
154
+ depends_on: [implementId],
155
+ role: "verifier",
156
+ executor: "shell",
157
+ complexity: "LOW",
158
+ writePolicy: "read-only",
159
+ allowedPaths: readOnlyPaths,
160
+ forbiddenPaths,
161
+ outputContract: "Archived shell stdout/stderr with exit codes for deterministic Mock-specific verification; no worktree writes.",
162
+ subtask_prompt: "Run deterministic Mock-specific verification (handler loading, endpoint matrix, fixture consumption, production boundary). Commands are frozen from generation-time trusted sources only.",
163
+ shell: {
164
+ commands,
165
+ verifyEvidence: buildVerifyEvidence({
166
+ phase: "intermediate",
167
+ quota: "full",
168
+ commandSource: commands.length > 0 ? "inline" : "adapter",
169
+ commands: verifyCommands.length > 0 ? verifyCommands : undefined,
170
+ fallbackCommands: [],
171
+ commandTexts: commands,
172
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
173
+ preflight: sources.verificationPreflight,
174
+ }),
175
+ cwd: ".",
176
+ timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
177
+ },
178
+ };
179
+ }
180
+ export function buildBlockedFrontendMockDag(sources, readOnlyPaths, forbiddenPaths, globalConstraints, blockedReason) {
181
+ const { taskConfig } = sources;
182
+ const reason = blockedReason ?? "Mock contract is blocked by deterministic generation-time Mock safety constraints.";
183
+ const spec = {
184
+ version: 3,
185
+ title: `Frontend implementation DAG (BLOCKED Mock): ${taskConfig.title}`,
186
+ runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
187
+ outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
188
+ objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
189
+ successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
190
+ globalConstraints: [
191
+ ...globalConstraints,
192
+ "Mock contract is BLOCKED: writer nodes must not be reachable. Resolve blocking issues and re-generate DAG.",
193
+ "Do not execute any write, verify, or closeout nodes. The DAG ends at the Mock contract gate.",
194
+ ],
195
+ defaults: {
196
+ ...FRONTEND_DEFAULTS,
197
+ contextProfile: taskConfig.contextProfile,
198
+ },
199
+ skillsByRole: FRONTEND_SKILLS_BY_ROLE,
200
+ executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
201
+ tasks: [
202
+ {
203
+ id: "frontend-mock-blocked-shell",
204
+ depends_on: [],
205
+ role: "verifier",
206
+ executor: "shell",
207
+ complexity: "LOW",
208
+ writePolicy: "read-only",
209
+ allowedPaths: readOnlyPaths,
210
+ forbiddenPaths,
211
+ outputContract: "Deterministic generation-time Mock blocker. Always exits nonzero and never reaches a writer.",
212
+ subtask_prompt: `Fail closed: ${reason} Resolve the task contract and regenerate the DAG.`,
213
+ shell: {
214
+ commands: [
215
+ `node -e ${JSON.stringify(`console.error(${JSON.stringify(`frontend Mock contract blocked: ${reason}`)}); process.exit(1)`)}`,
216
+ ],
217
+ cwd: ".",
218
+ timeoutMs: 60000,
219
+ },
220
+ },
221
+ ],
222
+ };
223
+ applyDefaultReadOnlyRetryPolicy(spec);
224
+ stampGeneratedArtifactBindings(spec);
225
+ parseDagSpec(spec);
226
+ assertValidDagSpec(spec);
227
+ return spec;
228
+ }
229
+ export function resolveFrontendMockContextBlock(sources) {
230
+ const capability = sources.frontendMockCapability;
231
+ const mode = sources.frontendMockMode ?? "not-required";
232
+ if (!capability)
233
+ return "";
234
+ const parts = [
235
+ "## Frontend Mock Context",
236
+ "",
237
+ `Configured Policy: ${sources.taskConfig.frontendMock?.policy ?? "auto"}`,
238
+ `Mock Decision: ${mode}`,
239
+ `Capability Status: ${capability.status}`,
240
+ ];
241
+ if (capability.framework) {
242
+ parts.push(`Detected Framework: ${capability.framework}`);
243
+ }
244
+ if (capability.serviceRoot) {
245
+ parts.push(`Service Root: ${capability.serviceRoot}`);
246
+ }
247
+ if (capability.evidencePaths.length > 0) {
248
+ parts.push(`Evidence Paths: ${capability.evidencePaths.join(", ")}`);
249
+ }
250
+ if (capability.verifyCommands.length > 0) {
251
+ parts.push(`Frozen Mock Verify Commands: ${capability.verifyCommands
252
+ .map((command) => command.label)
253
+ .join(", ")}`);
254
+ }
255
+ if (capability.safetyViolation) {
256
+ parts.push(`Safety Violation: ${capability.safetyViolation}`);
257
+ }
258
+ if (capability.reasons.length > 0) {
259
+ parts.push(`Reasons: ${capability.reasons.join("; ")}`);
260
+ }
261
+ if (mode === "required") {
262
+ parts.push("Mock-backed frontend verification is required. Prefer the detected native service; otherwise the assessment may select an existing browser interception harness or reversible request adapter. Any handler, fixture, adapter, and UI changes stay in the single frontend-implement-pi writeSet.");
263
+ }
264
+ if (mode === "not-required") {
265
+ if (frontendMockStrategyMustBeNotNeeded(sources)) {
266
+ parts.push('Generation-time evidence does not require Mock. The assessment must still use contract/scout evidence: select not-needed when Mock is intentionally skipped. Auto mode has no confirmed project Mock capability or no deterministic Mock verification command. The structured contract must set mockApi.strategy to "not-needed". Keep the real request path as the default, record any unproved backend behavior as Real Integration Gap, and do not add Mock files or dependencies within this run.');
267
+ parts.push('HARD CONSTRAINT (frozen at generation time): this DAG allows only mockApi.strategy "not-needed"; the prewrite gate rejects any other strategy. If project governance (openspec / ai_workspace / decision records, e.g. a DEC rule requiring native) demands Mock-backed verification, that is a generation-time contract gap, not a plan-revision defect: declare frontendMock.verifyCommands (or policy: "required") in task.json and regenerate the DAG.');
268
+ }
269
+ else {
270
+ parts.push("Generation-time evidence does not require Mock. The assessment must still use contract/scout evidence: select not-needed when Mock is intentionally skipped, or select a safe Mock strategy if project evidence supports one.");
271
+ }
272
+ }
273
+ if (mode === "blocked") {
274
+ parts.push("Mock contract is blocked. The DAG must stop before any write node executes.");
275
+ }
276
+ return parts.join("\n");
277
+ }
278
+ export async function buildFrontendHybridDagFromTask(sources) {
279
+ const { taskConfig } = sources;
280
+ const mockCapability = sources.frontendMockCapability ?? {
281
+ status: "absent",
282
+ evidencePaths: [],
283
+ verifyCommands: [],
284
+ reasons: [
285
+ "Frontend Mock capability was not precomputed; assessment must verify repository evidence.",
286
+ ],
287
+ };
288
+ const mockMode = sources.frontendMockMode ??
289
+ resolveFrontendMockMode(mockCapability, taskConfig, hasApiDependency(sources));
290
+ const frontendSources = {
291
+ ...sources,
292
+ frontendMockCapability: mockCapability,
293
+ frontendMockMode: mockMode,
294
+ };
295
+ const forbiddenPaths = mergeForbiddenPaths(taskConfig);
296
+ const implementPaths = resolveImplementPaths(taskConfig, {
297
+ repoRoot: sources.repoRoot,
298
+ });
299
+ const implementId = frontendImplementationNodeId();
300
+ const mockContextBlock = resolveFrontendMockContextBlock(frontendSources);
301
+ const capabilityContextBlock = resolveFrontendCapabilityContextBlock(frontendSources);
302
+ // Freeze the design-policy dependency allowlist from the project manifest:
303
+ // already-declared deps are authorized; anything else in the model's
304
+ // dependency policy that looks like a package name is an unauthorized new
305
+ // dependency (fail-closed at the policy shell and the plan pre-check).
306
+ const declaredDependencies = await collectDeclaredDependencies(sources.repoRoot ?? process.cwd());
307
+ const frontendRisk = frontendSources.frontendRisk ??
308
+ classifyFrontendRisk({
309
+ title: taskConfig.title,
310
+ requirementMarkdown: sources.requirementMarkdown,
311
+ constraintMarkdown: sources.constraintMarkdown ?? undefined,
312
+ allowedPaths: taskConfig.allowedPaths,
313
+ complexity: taskConfig.complexity,
314
+ });
315
+ const frontendTaskShape = resolveFrontendTaskShape({
316
+ complexity: taskConfig.complexity,
317
+ allowedPaths: taskConfig.allowedPaths,
318
+ requirementMarkdown: sources.requirementMarkdown,
319
+ constraintMarkdown: sources.constraintMarkdown ?? undefined,
320
+ splitSignal: { splitRequired: false, runtimeSupportsSplit: true },
321
+ taskId: sources.taskId,
322
+ sourceDigest: computeFrontendShapeSourceDigest({
323
+ requirementMarkdown: sources.requirementMarkdown,
324
+ constraintMarkdown: sources.constraintMarkdown ?? "",
325
+ }),
326
+ shapeTransitionCapsule: resolveFrontendShapeCapsuleGenerationInput(sources),
327
+ });
328
+ const frontendSourceBinding = buildDagSourceBinding(sources, taskConfig.taskKind);
329
+ const frontendContractSkeleton = buildFrontendImplementationContractSkeleton({
330
+ sourceBinding: frontendSourceBinding,
331
+ riskLevel: frontendRisk.selectedRisk,
332
+ targetFiles: implementPaths.writeSet,
333
+ });
334
+ const frontendContractFieldSummary = [
335
+ "## Contract field summary (authoritative JSON; no plan prose)",
336
+ "frontend-plan-pi records typed facts; frontend-design-policy-shell applies the runtime skeleton, validates, and materializes the canonical full contract JSON supplied here. There is no separate plan prose authority. Review these fields:",
337
+ "- requirements[]: id, expectedOutcome, implementationTargets, verificationTargetIds, evidenceGap",
338
+ "- uiStates[]: name, applicable, expectedBehavior, implementationTargets, verificationTargetIds, notApplicableReason",
339
+ "- interactions[]: name, trigger, expectedBehavior, implementationTargets, verificationTargetIds",
340
+ "- targets: routes, publicApiChanges (files are runtime-owned)",
341
+ "- mockApi: strategy, productionDefaultOff, activation, endpoints[]",
342
+ "- verificationTargets[]: id (stable target identity; execution binds commandId + file, never test titles), commandId (frozen directory key), mode + commandLabel (runtime-resolved), file, requirementIds, uiStates, scope? (display-only)",
343
+ "- designEvidence: source, paths, conflicts; evidenceGaps[] (optional)",
344
+ "- optional: stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap",
345
+ "- uiComponentChoices[]: purpose, component, decision (specified|reuse-existing|new), specReference { path, section, line } | null, rationale",
346
+ "Do not require or read a separate plan prose section; the contract JSON is the only plan surface.",
347
+ ].join("\n");
348
+ // Do not carry every attachment through the whole frontend pipeline. The
349
+ // contract node is the sole requirements/materials synthesis point; scout
350
+ // and plan only need the canonical request plus constraints, while design
351
+ // review consumes the materialized contract and only needs source provenance.
352
+ // This prevents attachment content from accumulating on later review calls.
353
+ const sourceContexts = {
354
+ contract: [buildSourceContextBlock(sources), capabilityContextBlock]
355
+ .filter(Boolean)
356
+ .join("\n\n"),
357
+ scout: [
358
+ buildSourceContextBlock(sources, { includeRequirementExcerpt: false, includeConstraintExcerpt: false, includeReferenceDocuments: false }),
359
+ capabilityContextBlock,
360
+ ]
361
+ .filter(Boolean)
362
+ .join("\n\n"),
363
+ designReview: buildSourceContextBlock(sources, {
364
+ includeRequirementExcerpt: false,
365
+ includeConstraintExcerpt: false,
366
+ includeReferenceDocuments: false,
367
+ }),
368
+ };
369
+ const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
370
+ mockCapability.verifyCommands.length > 0;
371
+ const requirementIds = frontendSourceBinding.requirementIds;
372
+ const strategy = resolveDagVerifyStrategy(taskConfig);
373
+ const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
374
+ const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
375
+ const globalConstraints = [
376
+ ...taskConfig.hardConstraints,
377
+ ...(sources.constraintMarkdown
378
+ ? [`See 执行约束.md in task source (${sources.taskId})`]
379
+ : []),
380
+ ...STANDARD_GLOBAL_CONSTRAINTS,
381
+ "Frontend design policy (frontend-design-policy-shell) and writer admission (frontend-writer-admission-shell) are the only write authorization; the writer runs only after both materialized the canonical contract and admitted a concrete writeSet.",
382
+ "Design review verdict is consumed only as admission data input; it never drives branch selection.",
383
+ "Verification failure is terminal for the run: frontend-verify-shell fails closed, routes to recovery, and never selects a same-run repair branch.",
384
+ "Frontend review is decided exclusively by the committed typed terminal tools (approve_review / request_review_changes); response-text JSON verdicts and first-line VERDICT markers carry no control-flow weight.",
385
+ "Frontend planning must consume the read-only Mock assessment strategy produced after scouting; MOCK_STRATEGY: blocked must not pass the deterministic Mock contract gate.",
386
+ "Mock implementations must preserve the real request path as the default, require explicit test/dev activation, and never rely on commenting out the real request.",
387
+ "Mock-backed behavior evidence proves only the documented frontend contract, never real API integration.",
388
+ "frontend-implementation DAGs must complete deterministic static verification before final review. Behavior verification is also required when the task declares a behavior entrypoint or the implementation contract contains a behavior-mode verification target; static-only contracts must map every target to the declared static entrypoint.",
389
+ "Frontend closeout renders only from committed facts; a weak status (failed / not-run / baseline-debt / mock-backed / pending) can never be rewritten into a stronger one (passed / real-integrated).",
390
+ `Frontend risk classification: ${frontendRisk.selectedRisk} — ${frontendRisk.reason}`,
391
+ frontendRisk.forceFullGates
392
+ ? "High-risk or supervised: keep full design gates; do not weaken write boundaries."
393
+ : "Risk-aware topology may omit redundant design revision nodes for small tasks only.",
394
+ ];
395
+ // Guard: blocked mode — generate assessment-only DAG with no writer reachable
396
+ if (mockMode === "blocked") {
397
+ const blockedReason = mockCapability.safetyViolation
398
+ ? `Mock contract blocked: ${mockCapability.safetyViolation}`
399
+ : (taskConfig.frontendMock?.policy ?? "auto") === "required"
400
+ ? "Mock strategy is required, but no authorized Mock verification command was found."
401
+ : "Mock contract is blocked by deterministic generation-time Mock safety constraints.";
402
+ return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints, blockedReason);
403
+ }
404
+ const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
405
+ // The verification bundle schema requires at least one static command. When
406
+ // a real project exposes no usable verification command at all, run an
407
+ // explicit no-op marker instead of a command that is guaranteed to fail:
408
+ // the trace then records not-run honestly and the advisory asks the task
409
+ // to declare verification commands and regenerate.
410
+ const staticFallbackCommands = fallbackVerifyCommands.staticCommands.length > 0
411
+ ? fallbackVerifyCommands.staticCommands
412
+ : [FRONTEND_NO_STATIC_VERIFICATION_MARKER];
413
+ const behaviorFallbackCommands = fallbackVerifyCommands.behaviorCommands;
414
+ const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
415
+ repoRoot: sources.repoRoot,
416
+ requirementMarkdown: sources.requirementMarkdown,
417
+ constraintMarkdown: sources.constraintMarkdown,
418
+ });
419
+ const explicitFrontendVerifyCommands = buildExplicitFrontendVerifyCommands(taskConfig, sources.repoRoot);
420
+ const explicitCommandKeys = new Set([
421
+ ...explicitFrontendVerifyCommands.staticCommands,
422
+ ...explicitFrontendVerifyCommands.behaviorCommands,
423
+ ].map(verifyCommandKey));
424
+ const adapterVerifyCommands = (sources.verifyCommands?.final ?? []).filter((command) => !explicitCommandKeys.has(verifyCommandKey(command)));
425
+ // Split adapter commands by lane BEFORE handing them to either lane. The
426
+ // same array used to be passed to both `chooseFrontendVerifyCommands` calls,
427
+ // so every adapter command entered static and behavior at once. The behavior
428
+ // lane is test-observed at writer admission, so a governance script from
429
+ // harness.json (`bash scripts/check-repo.sh`) demanded test-report
430
+ // capability it can never have and blocked admission deterministically.
431
+ // Adapter commands carry no declared lane, so classify them from their text;
432
+ // an explicit declaration still wins because it is chosen earlier.
433
+ //
434
+ // A declared task verifier owns this task's verification boundary and the
435
+ // adapter array is discarded for both lanes, so an unclassifiable adapter
436
+ // entry is harmless then and must not fail generation.
437
+ const hasDeclaredFrontendVerification = explicitFrontendVerifyCommands.staticCommands.length > 0 ||
438
+ explicitFrontendVerifyCommands.behaviorCommands.length > 0 ||
439
+ parsedFrontendVerifyCommands.staticCommands.length > 0 ||
440
+ parsedFrontendVerifyCommands.behaviorCommands.length > 0;
441
+ const adapterStaticCommands = [];
442
+ const adapterBehaviorCommands = [];
443
+ for (const command of adapterVerifyCommands) {
444
+ // Classify on the bare argv only. The frozen shell form prepends an env
445
+ // prefix and a `cd …` prologue, which would obscure the runner name.
446
+ const text = command.args.join(" ");
447
+ const mode = classifyFrontendVerifyCommandText(text);
448
+ if (mode === "static") {
449
+ adapterStaticCommands.push(command);
450
+ continue;
451
+ }
452
+ if (mode === "behavior") {
453
+ adapterBehaviorCommands.push(command);
454
+ continue;
455
+ }
456
+ // Unrecognized adapter command. When the task declares any verification
457
+ // of its own, it owns the verification boundary and the adapter array is
458
+ // discarded for both lanes, so an unclassifiable entry is harmless and
459
+ // must not fail generation. Only an adapter command that would actually
460
+ // be USED needs a lane, and defaulting it to `behavior` would make writer
461
+ // admission demand test-report capability it cannot provide. Fail loudly
462
+ // there and name the operator's fix.
463
+ if (hasDeclaredFrontendVerification)
464
+ continue;
465
+ throw new Error(`adapter verification command ${JSON.stringify(text)} (label ${JSON.stringify(command.label)}) has no recognizable lane; declare it explicitly with mode: "static" or mode: "behavior" in task verifyCommands, or remove it from the harness manifest`);
466
+ }
467
+ const staticVerifyCommands = chooseFrontendVerifyCommands({
468
+ explicitCommands: explicitFrontendVerifyCommands.staticCommands,
469
+ parsedCommands: parsedFrontendVerifyCommands.staticCommands,
470
+ adapterCommands: adapterStaticCommands,
471
+ });
472
+ const partitionedStaticVerifyCommands = partitionFrontendStaticVerifyCommands({
473
+ repoRoot: sources.repoRoot,
474
+ commands: staticVerifyCommands.commands,
475
+ commandSource: staticVerifyCommands.commandSource,
476
+ });
477
+ const behaviorVerifyCommands = chooseFrontendVerifyCommands({
478
+ explicitCommands: explicitFrontendVerifyCommands.behaviorCommands,
479
+ parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
480
+ adapterCommands: adapterBehaviorCommands,
481
+ // A declared task verifier owns this task's verification boundary. A
482
+ // static-only task must not inherit unrelated root-level test commands.
483
+ allowAdapter: !hasDeclaredFrontendVerification,
484
+ });
485
+ const staticShellCommands = buildVerifyShellCommands({
486
+ repoRoot: sources.repoRoot,
487
+ commands: partitionedStaticVerifyCommands.static.commands,
488
+ fallbackCommands: staticFallbackCommands,
489
+ });
490
+ const lintShellCommands = buildVerifyShellCommands({
491
+ repoRoot: sources.repoRoot,
492
+ commands: partitionedStaticVerifyCommands.lint.commands,
493
+ fallbackCommands: [],
494
+ });
495
+ const behaviorShellCommands = buildVerifyShellCommands({
496
+ repoRoot: sources.repoRoot,
497
+ commands: behaviorVerifyCommands.commands,
498
+ fallbackCommands: hasDeclaredFrontendVerification
499
+ ? []
500
+ : behaviorFallbackCommands,
501
+ });
502
+ const effectiveBehaviorFallbackCommands = hasDeclaredFrontendVerification
503
+ ? []
504
+ : behaviorFallbackCommands;
505
+ const staticVerifyEvidence = buildVerifyEvidence({
506
+ phase: "intermediate",
507
+ quota: strategy.intermediateQuota ?? "full",
508
+ commandSource: partitionedStaticVerifyCommands.static.commandSource,
509
+ commands: partitionedStaticVerifyCommands.static.commands,
510
+ fallbackCommands: staticFallbackCommands,
511
+ commandTexts: staticShellCommands,
512
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
513
+ preflight: sources.verificationPreflight,
514
+ });
515
+ const lintVerifyEvidence = lintShellCommands.length > 0
516
+ ? buildVerifyEvidence({
517
+ phase: "intermediate",
518
+ quota: strategy.intermediateQuota ?? "full",
519
+ commandSource: partitionedStaticVerifyCommands.lint.commandSource,
520
+ commands: partitionedStaticVerifyCommands.lint.commands,
521
+ fallbackCommands: [],
522
+ commandTexts: lintShellCommands,
523
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
524
+ preflight: sources.verificationPreflight,
525
+ })
526
+ : undefined;
527
+ const behaviorVerifyEvidence = buildVerifyEvidence({
528
+ phase: "final",
529
+ quota: "full",
530
+ commandSource: behaviorVerifyCommands.commandSource,
531
+ commands: behaviorVerifyCommands.commands,
532
+ fallbackCommands: effectiveBehaviorFallbackCommands,
533
+ commandTexts: behaviorShellCommands,
534
+ finalFullRequired: true,
535
+ commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
536
+ preflight: sources.verificationPreflight,
537
+ });
538
+ const mockVerifyTemplate = mockMode === "required" && hasMockVerifyCommands
539
+ ? buildFrontendMockVerifyNode(frontendSources, implementId, readOnlyPaths, forbiddenPaths)
540
+ : undefined;
541
+ const mockShellCommands = mockVerifyTemplate?.shell?.commands ?? [];
542
+ const mockVerifyEvidence = mockVerifyTemplate?.shell?.verifyEvidence;
543
+ // Contract v2: the frozen command directory is the only command reference
544
+ // the plan may use. Modes are assigned here once from the generation-time
545
+ // lane split; downstream materialization resolves the same directory from
546
+ // the run spec, so plan and runtime can never disagree on a mode.
547
+ const frontendVerifyDirectory = buildFrontendVerifyCommandDirectory({
548
+ staticLabels: staticVerifyEvidence.commandLabels,
549
+ behaviorLabels: behaviorVerifyEvidence.commandLabels,
550
+ mockLabels: mockVerifyEvidence?.commandLabels ?? [],
551
+ staticCommandTexts: staticVerifyEvidence.commandTexts,
552
+ behaviorCommandTexts: behaviorVerifyEvidence.commandTexts,
553
+ mockCommandTexts: mockVerifyEvidence?.commandTexts ?? [],
554
+ });
555
+ // Freeze capability only after final selection; discarded adapter commands
556
+ // cannot block this task. Unsupported inputs remain explicit preflight
557
+ // failures and writer admission refuses them before any implementation.
558
+ behaviorVerifyEvidence.preflight = await Promise.all(behaviorShellCommands.map(async (command, index) => {
559
+ const label = behaviorVerifyEvidence.commandLabels[index] ?? command;
560
+ const prior = sources.verificationPreflight?.find(entry => entry.label === label);
561
+ const inspected = await inspectFrontendTestCommand({ command, cwd: sources.repoRoot ?? process.cwd(), workspaceRoot: sources.repoRoot ?? process.cwd(), allowedPaths: implementPaths.allowedPaths, forbiddenPaths });
562
+ const commandId = frontendVerifyDirectory.find(entry => entry.mode === "behavior" && entry.label === label)?.commandId;
563
+ return { label, status: prior?.status ?? "ok", testObservation: { ...inspected.observation, commandId } };
564
+ }));
565
+ const fixedVerificationContext = [
566
+ "## Fixed frontend verification entrypoints",
567
+ "These shell entrypoints are fixed at DAG generation and are the only commands the static and behavior shell nodes execute. A strategy or plan may add tests behind an existing entrypoint inside writeSet, but must not invent or replace commands or assume subtask_prompt executes a command.",
568
+ "Reference frozen commands ONLY by commandId (record_plan_verification_target entry.commandId). The runtime resolves mode and label; never invent a mode or type.",
569
+ ...frontendVerifyDirectory.map((entry) => ` - ${entry.commandId} [${entry.mode}]: ${JSON.stringify(entry.label)}`),
570
+ `- Static command source: ${staticVerifyEvidence.commandSource}`,
571
+ `- Behavior command source: ${behaviorVerifyEvidence.commandSource}`,
572
+ ].join("\n");
573
+ const advisories = [];
574
+ if (!hasDeclaredFrontendVerification &&
575
+ fallbackVerifyCommands.staticCommands.length === 0 &&
576
+ fallbackVerifyCommands.behaviorCommands.length === 0) {
577
+ advisories.push("未发现可用的前端验证命令:目标项目没有可读取的 package.json scripts,也未探测到本地 TypeScript,verify 节点将没有静态/行为命令可执行。请在 task.json --verify 或执行约束.md 的验证约束中显式声明命令(例如 node --check src/app.js),然后重新生成 DAG。");
578
+ }
579
+ if (frontendSourceMentionsMock(frontendSources) &&
580
+ frontendMockStrategyMustBeNotNeeded(frontendSources)) {
581
+ advisories.push("auto 模式已将 Mock 策略收窄为 not-needed:任务源提到接口/API/Mock 需求,但仓库无确认 Mock 能力或无确定性 Mock 验证命令。若项目规范要求 Mock,请声明 frontendMock.verifyCommands 或 policy:required 后重新生成 DAG。");
582
+ }
583
+ const openspecGate = await resolveFrontendOpenspecGateConfig(sources);
584
+ const requiresOpenspecClassification = openspecGate.openspecPolicy === "cited" &&
585
+ openspecGate.openspecCandidatePaths.length > 0;
586
+ // Prompt 内联的候选只保留与任务相关的子集:mandatory(任务显式声明/
587
+ // 引用)始终保留;scan-strict 候选按路径段是否命中任务源关键词过滤。
588
+ // runtime 的选型/read 门禁仍消费完整 openspecCandidatePaths——这里只
589
+ // 减小 prompt 体积,不改变门禁语义;未提及的候选 runtime 默认 irrelevant。
590
+ const promptCandidatePaths = filterRelevantOpenspecCandidates({
591
+ candidates: openspecGate.openspecCandidateSummaries.map((candidate) => candidate.path),
592
+ mandatoryPaths: openspecGate.openspecMandatoryPaths,
593
+ sourceMarkdown: [
594
+ sources.requirementMarkdown,
595
+ sources.constraintMarkdown ?? "",
596
+ ].join("\n"),
597
+ maxCandidates: 24,
598
+ });
599
+ const candidateByPath = new Map(openspecGate.openspecCandidateSummaries.map((candidate) => [
600
+ candidate.path,
601
+ candidate,
602
+ ]));
603
+ // Preserve the filter's mandatory/relevance order. Sorting then slicing here
604
+ // used to be able to drop a mandatory path after it passed the Top-K filter.
605
+ const promptCandidates = promptCandidatePaths.flatMap((candidatePath) => {
606
+ const candidate = candidateByPath.get(candidatePath);
607
+ return candidate
608
+ ? [
609
+ {
610
+ path: candidate.path,
611
+ kind: candidate.kind,
612
+ score: candidate.score,
613
+ reasons: candidate.reasons,
614
+ source: candidate.source,
615
+ },
616
+ ]
617
+ : [];
618
+ });
619
+ const openspecSelectionContext = JSON.stringify({
620
+ schemaVersion: 1,
621
+ schemaId: "frontend-openspec-selection-v1",
622
+ candidates: promptCandidates,
623
+ mandatoryPaths: openspecGate.openspecMandatoryPaths,
624
+ });
625
+ const scopedOpenspecContext = promptCandidates.length > 0
626
+ ? [
627
+ "## Task-relevant OpenSpec Top-K (generation frozen)",
628
+ `Policy: ${openspecGate.openspecPolicy}. This is the bounded prompt index; the deterministic gate retains ${openspecGate.openspecCandidatePaths.length} frozen candidates.`,
629
+ ...promptCandidates.map((candidate) => `- ${openspecGate.openspecMandatoryPaths.includes(candidate.path) ? "mandatory" : "candidate"}: ${candidate.path} (${candidate.kind}; ${candidate.source})`),
630
+ "Apply or cite only paths relevant to the concrete contract. Report applied rules with path/section/line and surface conflicts or missing specifications; unlisted candidates default to irrelevant unless the deterministic gate requires them.",
631
+ ].join("\n")
632
+ : "";
633
+ // Generation-frozen component/theme specification bucket (ADR 0016). Derived
634
+ // from the classified component/theme/rule.components buckets; the prewrite
635
+ // gate consumes it to enforce uiComponentChoices presence and specReference
636
+ // cross-checking. `src/task/**` classification logic is intentionally
637
+ // unchanged — these buckets already partition normativePaths.
638
+ const classified = sources.frontendProjectCapability?.designEvidence.classified;
639
+ const componentSpecCandidatePaths = [
640
+ ...(classified?.component ?? []),
641
+ ...(classified?.theme ?? []),
642
+ ...(classified?.rule.components ?? []),
643
+ ].filter((value, index, array) => array.indexOf(value) === index).sort();
644
+ if (openspecGate.openspecPolicy === "cited") {
645
+ if (openspecGate.openspecCandidatePaths.length === 0) {
646
+ advisories.push("openspec 策略 cited:契约声明的 requiredReadPaths 与任务源引用均为空,prewrite gate 不强制读取 openspec;如需增强规范门禁,请在 task.json.frontendOpenspec.requiredReadPaths 声明必读路径或在任务源中显式引用 openspec 文件。");
647
+ }
648
+ else {
649
+ advisories.push(`openspec 策略 cited:候选 ${openspecGate.openspecCandidatePaths.length} 个(declared ${openspecGate.openspecCandidateSources.declared.length} / task-source-cited ${openspecGate.openspecCandidateSources.taskSourceCited.length}),plan 必须在 typed decision ledger 的 uiComponentChoices.specReference 中声明并通过真实 read 事件佐证。`);
650
+ }
651
+ }
652
+ else {
653
+ const openspecDiscovery = sources.frontendProjectCapability?.openspecDiscovery;
654
+ if (openspecDiscovery) {
655
+ advisories.push(`openspec 策略 scan-strict:候选来源 auto-discovered,共 ${openspecDiscovery.candidateCount} 个,排除治理目录 ${openspecDiscovery.governanceRoot} 下 ${openspecDiscovery.excludedGovernanceCount} 个文件`);
656
+ if (openspecDiscovery.truncated) {
657
+ advisories.push(`openspec 候选命中安全上限 ${openspecDiscovery.maxCandidates},已截断(非静默);请缩小 spec 范围或提高上限`);
658
+ }
659
+ }
660
+ }
661
+ const spec = {
662
+ version: 3,
663
+ title: `Frontend implementation DAG: ${taskConfig.title}`,
664
+ runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
665
+ objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
666
+ successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
667
+ globalConstraints,
668
+ defaults: {
669
+ ...FRONTEND_DEFAULTS,
670
+ contextProfile: taskConfig.contextProfile,
671
+ },
672
+ skillsByRole: FRONTEND_SKILLS_BY_ROLE,
673
+ executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
674
+ verifyStrategy: resolveDagVerifyStrategy(taskConfig),
675
+ advisories: advisories.length > 0 ? advisories : undefined,
676
+ tasks: [
677
+ {
678
+ id: "frontend-contract-pi",
679
+ depends_on: [],
680
+ role: "planner",
681
+ executor: "pi",
682
+ complexity: "MED",
683
+ writePolicy: "read-only",
684
+ allowedPaths: readOnlyPaths,
685
+ forbiddenPaths,
686
+ skills: FRONTEND_CONTRACT_SKILLS,
687
+ outputContract: "Incremental typed requirement facts; narrative is display-only and should be omitted. Submit through the incremental typed tools record_requirement / record_constraint / record_evidence_expectation / record_handoff_intent / record_open_question / record_split_proposal / record_ui_state / record_required_deliverables / record_openspec_selection, then complete each input scope with complete_contract_scope, then call finalize_contract; correct rejected calls until one successful terminal. record_requirement takes the canonical ledger requirement id and optional execution:{groupId,kind,summary} — the runtime owns the authoritative text, source spans, fragment bindings, and disposition. UI-visible or interactive requirements register a non-blocking frontend-test handoff intent, and any source-declared UI-state table is extracted verbatim through record_ui_state. End finalize_contract with a single contract disposition of ready | ready-with-assumptions | blocked. Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations; do not fix target files, components, or implementation methods as requirements. When OpenSpec candidates exist, classify only the ones you actually use: call record_openspec_selection once per required/relevant path; never enumerate irrelevant candidates (unmentioned defaults to irrelevant) and never emit a fenced selection JSON. After a successful typed terminal, stop without a Markdown summary. No file writes.",
688
+ subtask_prompt: [
689
+ "OUTPUT BUDGET DISCIPLINE: provider capacity is discovered at runtime; use small records — NEVER attempt to emit the whole contract in one response; a single large JSON dump will be truncated and rejected. Incremental submission through the typed tools is the ONLY supported output mode. Start submitting with the FIRST tool call: after each read, call record_requirement for the requirements you have already confirmed, one or a few per call. Every tool-call round MUST make progress by submitting at least one record_* fact. Do not re-read the same source file that is already materialized in this session; read each file at most once.",
690
+ "Consume the complete injected input scope and produce a concise frontend implementation contract as typed requirement facts from complete injected scopes.",
691
+ "Confirm each requirement by the SAME id as the ledger canonical requirement it covers (sourceBinding.requirementIds, e.g. AC-001) — do NOT invent new REQ/BR prefixed ids for canonical requirements: the compiled contract must match the ledger canonical requirement ids exactly or schema validation rejects it (unknown requirement id). record_requirement takes the canonical id and optional execution:{groupId,kind,summary}; the runtime commits the authoritative text and sourceFragmentIds from the frozen ledger. Never pass text/statement/sourceFragmentIds yourself — model rewrites and JSON-stringified fragment arrays are rejected.",
692
+ "Requirement semantics, source spans, dispositions, and fragment bindings are ledger/runtime-owned. If a canonical requirement is genuinely blocked, say so in the Markdown contract narrative and finalize with the matching disposition instead of trying to encode it in the requirement fact.",
693
+ "Register evidence expectations for each requirement across static, behavior, Mock, and real integration as required | optional | not-applicable; required must follow from user requirements, task risk, or project governance, never from model convenience. For UI-visible or interactive requirements, register a non-blocking frontend-test handoff intent.",
694
+ 'Use record_evidence_expectation with {requirementId,evidence:{static,behavior,mock,"real-integration"}}; every lane is required | optional | not-applicable. Requirement text and provenance remain runtime-owned.',
695
+ 'Before finalize_contract ready, call record_required_deliverables once with the complete {items:[{path,requirementId,sourceFragmentId}]} inventory, or {items:[]} when no file delivery is mandatory. Interpret the original source, including lists and tables: allowedPaths/only-allowed-to-modify is permission, not an obligation; do not promote prohibited files, examples or references into deliverables. Paths must occur exactly in a frozen source fragment bound to that canonical requirement. Correct the whole inventory before finalizing if needed.',
696
+ "Authoritative UI states: when the task source declares a UI-state table (state id / trigger / observable outcome), extract it VERBATIM through record_ui_state, one call per state, using the source's own state ids. The planner must bind these ids later — do not rename, merge, or invent states.",
697
+ "Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations. Do not fix target files, components, styling, or implementation methods as requirements; leave those to Scout and Plan.",
698
+ "If the task is too large for one bounded writer, record a task split proposal instead of silently widening scope.",
699
+ "End the contract with a single disposition: ready, ready-with-assumptions (bounded assumptions that do not change product behavior), or blocked.",
700
+ scopedOpenspecContext,
701
+ ...(requiresOpenspecClassification ? [
702
+ "Classify OpenSpec candidates incrementally while contracting — only the ones you actually use. Call record_openspec_selection once per path with disposition required (must be read and cited by the plan) or relevant (may inform planning). Never call it for irrelevant candidates and never list them: candidates you do not mention are treated as irrelevant by the runtime. Explicit task declarations / source citations are already required and must-read regardless; you never need to re-declare them.",
703
+ "Mandatory paths are enforced by the runtime from the frozen task configuration — do not enumerate them, do not downgrade them.",
704
+ openspecSelectionContext,
705
+ ] : []),
706
+ "Read-only: do not modify code, docs, artifacts, or repository files.",
707
+ sourceContexts.contract,
708
+ ].join("\n\n"),
709
+ },
710
+ {
711
+ id: "frontend-scout-pi",
712
+ depends_on: ["frontend-contract-pi"],
713
+ role: "scout",
714
+ executor: "pi",
715
+ complexity: mapTaskComplexity(taskConfig.complexity),
716
+ writePolicy: "read-only",
717
+ retryPolicy: FRONTEND_SCOUT_COMPLETENESS_RETRY_POLICY,
718
+ allowedPaths: readOnlyPaths,
719
+ forbiddenPaths,
720
+ skills: FRONTEND_SCOUT_SKILLS,
721
+ outputContract: "Markdown scout report covering target surface and design evidence (frontend stack, routes, components, styling system, existing design conventions, state/data flow, test entry points, reuse opportunities, risks). Submit through the incremental evidence tools record_target_surface / record_design_evidence. A complete target surface is mandatory before Plan; if it cannot be proven, commit blocked with unresolved paths so this Scout node retries rather than shifting discovery to Plan. No fixed TARGET_SURFACE section title is required — target surface and design evidence are reported as committed typed facts. No file writes.",
722
+ subtask_prompt: [
723
+ "Inspect frontend code, routing, components, styles, package scripts, and tests.",
724
+ "Return code and design observations, existing reuse opportunities, and verification entry points.",
725
+ "Report target surface and design evidence as facts (no fixed section title required): completeness, entrypoint, routeOrMount, implementationPaths, testPaths, dataSource, allowedPathConflicts, unresolvedPaths. Use repository-relative POSIX paths. A complete surface must name at least one target candidate and set unresolvedPaths to []; if any target ownership remains unknown, commit completeness=blocked with every unresolved path instead of guessing. Prefer existing files/directories when they exist. For a greenfield target explicitly pinned by the task source, future implementation/test paths are allowed, but every such path must be named by the source and runtime-enriched as sourceDeclared; do not invent paths merely because they fit allowedPaths. allowedPathConflicts must list every discovered path not covered by task allowedPaths, or [] when none exists.",
726
+ "Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
727
+ "Read-only: do not modify repository files.",
728
+ sourceContexts.scout,
729
+ scopedOpenspecContext,
730
+ ].join("\n\n"),
731
+ },
732
+ {
733
+ id: "frontend-plan-pi",
734
+ depends_on: ["frontend-contract-pi", "frontend-scout-pi"],
735
+ role: "planner",
736
+ executor: "pi",
737
+ // Small topology has already proven a concentrated, no-remote scope;
738
+ // keep its bounded plan on the LOW model tier. Standard/High retain
739
+ // MED for broader contract-to-surface decisions.
740
+ complexity: frontendTaskShape.shape === "small" ? "LOW" : "MED",
741
+ writePolicy: "read-only",
742
+ retryPolicy: FRONTEND_PLAN_LADDER_RETRY_POLICY,
743
+ allowedPaths: readOnlyPaths,
744
+ forbiddenPaths,
745
+ skills: FRONTEND_PLAN_SKILLS,
746
+ structuredContractOutput: {
747
+ schemaId: "frontend-implementation-contract-plan-patch-v1",
748
+ retryOnInvalid: true,
749
+ skeleton: frontendContractSkeleton,
750
+ },
751
+ outputContract: "Typed decision patch only: map frozen requirements to implementation/verification targets and select the needed component, state, data/Mock, styling, and dependency decisions. Use only the record_* tools needed to express those decisions, then call finalize_plan; correct rejected facts until one successful terminal. Contract owns requirement semantics; Scout owns repository discovery; deterministic runtime owns schema, protected fields, path containment, and command validation. No Markdown narrative or file writes.",
752
+ subtask_prompt: [
753
+ "Plan only the delta between the frozen frontend-contract-pi facts and frontend-scout-pi target surface. Do not reinterpret the task, repeat requirements, search the repository, or choose implementation order.",
754
+ "Record only: requirement-to-file/verification coverage; component/styling choices; applicable UI state and interaction behavior; data/Mock strategy; and a dependency policy or genuine evidence gap. Reuse Scout paths. If scope is missing, record a blocking gap instead of inventing a path.",
755
+ "Use the typed tool schemas as the field contract. Runtime owns schemaVersion, sourceBinding, riskLevel, targets.files, mockApi.productionDefaultOff, aliases, command allowlisting, path containment, and final validation; do not restate those rules or emit a full JSON contract.",
756
+ `Cover each frozen requirement ID exactly once: ${requirementIds.join(", ") || "(none)"}. Bind every verification target to a frozen commandId from the directory above plus a Scout-confirmed file. Behavior commands prove observable behavior: one target may cover multiple related requirementIds when one test behavior proves them together; do not mechanically create one target per requirement. A behavior target id is the stable machine trace token and its file must be a test file. Static commands are project-wide checks traced by file and command only.`,
757
+ "UX vocabulary protocol: record_state_registry FIRST with the full global vocabulary — one stable kebab-case behavior-domain name per UI state/interaction (e.g. planner-task-edit, focus-queue-move), never one name per AC number and never a rename of an already-recorded concept. Details consume complete execution-group scopes and reuse the same global names across scopes. Constraints/exclusions must not manufacture UI. Then record_state_flow entries whose names all come from that registry; uiState names must use the contract's declared authoritative ids (declaredUiStates in the plan input) when present. Retry attempts see committedUx in this input — reuse those exact names. Components: one choice may cover many state/interaction ids via covers; reuse-existing requires evidencePath naming an existing repo file (greenfield must be decision=new).",
758
+ ...(requiresOpenspecClassification ? ["When a component choice uses an OpenSpec selection, cite that selection; otherwise do not classify unrelated candidates."] : []),
759
+ "Call finalize_plan; correct rejected facts until one successful terminal after the necessary typed facts. Return no Markdown narrative.",
760
+ "Group members retain full source text and independent ACs. Use record_plan_group_coverage only for actually shared references; do not create components for exclusions. TOOL-ONLY PLAN: Do not read Contract/Scout stdout, task sources, or Scout-confirmed target files. Contract and Scout already own evidence discovery; use the injected upstream facts, record a genuine evidence gap when those facts are insufficient, and start committing record_* facts immediately. For decision=new, pass sourceRequirementIds to record_component_choice; runtime derives the exact PRD citation from the frozen ledger.",
761
+ "Incremental output protocol: process the current complete scope, committing small records immediately. Runtime packs full input and estimated work without assuming a model capacity from its name. Use record_plan_group_coverage for shared references, record_mock_endpoint per endpoint, and finalize only when all coverage is complete. On exhaustion, durable progress survives and remaining work is reduced; never omit source conditions or repeatedly retry the same exhausted scope.",
762
+ fixedVerificationContext,
763
+ scopedOpenspecContext,
764
+ mockContextBlock,
765
+ frontendContractFieldSummary,
766
+ frontendComponentConformanceInstruction,
767
+ ].join("\n\n"),
768
+ },
769
+ {
770
+ id: "frontend-design-policy-shell",
771
+ depends_on: ["frontend-plan-pi"],
772
+ role: "verifier",
773
+ executor: "shell",
774
+ complexity: "LOW",
775
+ writePolicy: "read-only",
776
+ allowedPaths: readOnlyPaths,
777
+ forbiddenPaths,
778
+ outputContract: "Deterministic design policy: materialize the canonical frontend implementation contract from the plan patch, enforce requirement-id retention, Mock strategy/frozen-command binding, writeSet containment, source freshness, and openspec invariants, then evaluate the design policy (write policy result).",
779
+ subtask_prompt: "Materialize the canonical contract and fail closed unless the deterministic design policy approves. The design verdict is not available at this stage; the writer admission shell enforces it.",
780
+ shell: {
781
+ commands: [],
782
+ frontendDesignPolicy: {
783
+ schemaVersion: 1,
784
+ planFromNodeId: "frontend-plan-pi",
785
+ requiredRequirementIds: requirementIds,
786
+ allowedMockStrategies: taskConfig.frontendMock?.policy === "disabled" ||
787
+ frontendMockStrategyMustBeNotNeeded(frontendSources)
788
+ ? ["not-needed"]
789
+ : taskConfig.frontendMock?.policy === "required"
790
+ ? [
791
+ "native",
792
+ "browser-intercept",
793
+ "request-adapter",
794
+ ]
795
+ : [
796
+ "native",
797
+ "browser-intercept",
798
+ "request-adapter",
799
+ "not-needed",
800
+ ],
801
+ mockCommandLabels: mockVerifyEvidence?.commandLabels ?? [],
802
+ artifactName: "frontend-implementation-contract.json",
803
+ outputDir: "contracts",
804
+ requireSourceFreshness: true,
805
+ implementationWriteSet: implementPaths.writeSet,
806
+ ...(taskConfig.requirementOwnership?.length
807
+ ? { requirementOwnership: taskConfig.requirementOwnership }
808
+ : {}),
809
+ ...(taskConfig.capabilityBoundary
810
+ ? { capabilityBoundary: taskConfig.capabilityBoundary }
811
+ : {}),
812
+ ...(taskConfig.interactionIds?.length
813
+ ? { interactionIds: taskConfig.interactionIds }
814
+ : {}),
815
+ openspecPolicy: openspecGate.openspecPolicy,
816
+ openspecSpecRoots: taskConfig.frontendOpenspec?.specRoots ?? [
817
+ ...DEFAULT_FRONTEND_SPEC_ROOTS,
818
+ ],
819
+ ...(requiresOpenspecClassification
820
+ ? { openspecSelectionNodeId: "frontend-contract-pi" }
821
+ : {}),
822
+ openspecMandatoryPaths: openspecGate.openspecMandatoryPaths,
823
+ openspecCandidateSources: {
824
+ declared: openspecGate.openspecCandidateSources.declared,
825
+ taskSourceCited: openspecGate.openspecCandidateSources.taskSourceCited,
826
+ scanStrict: openspecGate.openspecCandidateSources.scanStrict,
827
+ },
828
+ openspecCandidatePaths: openspecGate.openspecCandidatePaths,
829
+ ...(openspecGate.openspecCandidateSnapshots
830
+ ? {
831
+ openspecCandidateSnapshots: openspecGate.openspecCandidateSnapshots,
832
+ }
833
+ : {}),
834
+ componentSpecCandidatePaths,
835
+ allowedDependencies: declaredDependencies,
836
+ },
837
+ cwd: ".",
838
+ timeoutMs: 60000,
839
+ },
840
+ },
841
+ {
842
+ id: "frontend-design-review-pi",
843
+ depends_on: ["frontend-design-policy-shell"],
844
+ role: "reviewer",
845
+ executor: "pi",
846
+ complexity: "MED",
847
+ writePolicy: "read-only",
848
+ retryPolicy: DEFAULT_READ_ONLY_PI_RETRY_POLICY,
849
+ allowedPaths: readOnlyPaths,
850
+ forbiddenPaths,
851
+ skills: FRONTEND_DESIGN_REVIEW_SKILLS,
852
+ outputContract: "Authoritative typed design terminal via approve_design / request_design_changes tools. No JSON verdict; the committed typed design fact is the only authority. No file writes.",
853
+ subtask_prompt: [
854
+ "Submit findings individually with record_design_finding and stable IDs; terminal tools aggregate saved findings. Correct rejected calls, stop after a successful terminal. Check execution groups against every member source outcome, including permission, threshold and failure-path differences; shared verification is valid only when it proves each independent AC.",
855
+ "After the typed design terminal succeeds, stop immediately; do not emit a Markdown recap.",
856
+ "Audit the frontend plan before implementation. frontend-plan-pi is emitted to you as canonical full-contract JSON after the runtime applied and validated the planner's editable patch against its protected skeleton; there is no separate plan prose.",
857
+ "Your authoritative terminal verdict is exactly one committed typed tool call: approve_design or request_design_changes. Call exactly one of them; after calling one, do not call the other.",
858
+ "request_design_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
859
+ "Your verdict is consumed as deterministic data input by frontend-writer-admission-shell. approve_design permits admission; request_design_changes blocks writer admission until a recovery plan incorporates every Critical/Important finding.",
860
+ "Request design changes when the Mock strategy is blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence; a static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint; not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists, in which case the plan must preserve the real request path and record the Real Integration Gap.",
861
+ "Also request design changes for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
862
+ "Component selection conformance is a hard blocking condition: request_design_changes when the frontend spec (component/theme/rule.components bucket) already defines a component for a purpose but the plan selects another or self-invents one without a declared deviation; when uiComponentChoices is missing/empty for UI-visible work while the frozen component/theme bucket is non-empty; when a decision=specified specReference.path is missing a ledger OpenSpec reference or successful read event; or when a decision=new component lacks a traceable task-source/PRD specReference. A PRD reference for decision=new is not an OpenSpec citation and must not be rejected merely for lacking an OpenSpec read event. For uiComponentChoices, coverage is judged by the covers list: every applicable UI state and interaction must appear in some choice's purpose or covers, and the runtime already pre-checks this deterministically at plan finalize — do not re-litigate it. purpose is a human-readable description of what the choice is for and must NOT be rejected merely for not matching an interaction or uiState name; judge responsibility semantics from the covered ids' expectedBehavior plus the choice rationale.",
863
+ "You must NOT make authoritative assertions about the execution result of frozen verification commands: command results are deterministically established by frontend-verify-shell. Record a verification-feasibility concern only as a non-blocking finding (severity must not be Critical, and it must never be the sole fatal basis for request_design_changes). Only semantic design defects (component selection, state flow, interaction contract, or conflicts with the specification) may be Critical; a pure command-will-fail prediction must not be classified as contract-requirement-gap.",
864
+ "Read-only: do not modify repository files.",
865
+ "LARGE-FILE AUDIT (avoid full reads): style/theme audit files can be large (e.g. styles.css is often hundreds of KB). Prefer grep to locate the exact rules/variables you must verify (e.g. grep the oc- class, is-* modifier, or --oc- theme variables with their line numbers), then read only the narrow line range when surrounding context is needed. Do not read a large style/test file in full — a single full read can exhaust the read budget and fail the attempt.",
866
+ "Canonical contract reading: frontend-design-policy-shell prints absolute paths for Contract, Contract index, and the non-blocking Capacity diagnostic. Read the capacity diagnostic first. When it recommends full-contract, read the exact Contract path. When it recommends indexed-sections, read the Contract index and its hash-bound section files instead of opening the full contract. Never resolve a bare contracts/... path against the repository root or hunt for substitutes. Implementation target files inside the writeSet are created later by the implement node: do not read them and do not treat their absence as a design defect.",
867
+ fixedVerificationContext,
868
+ sourceContexts.designReview,
869
+ scopedOpenspecContext,
870
+ mockContextBlock,
871
+ ].join("\n\n"),
872
+ },
873
+ {
874
+ id: "frontend-writer-admission-shell",
875
+ depends_on: ["frontend-design-policy-shell", "frontend-design-review-pi"],
876
+ role: "verifier",
877
+ executor: "shell",
878
+ complexity: "LOW",
879
+ writePolicy: "read-only",
880
+ allowedPaths: readOnlyPaths,
881
+ forbiddenPaths,
882
+ outputContract: "Deterministic writer admission: require design review verdict pass, freeze the pre-writer worktree/lint baselines, derive the concrete writeSet + admission digest, and write contracts/frontend-writer-admission-result.json (schemaId frontend-writer-admission-shell-v1).",
883
+ subtask_prompt: "Fail closed unless the design review approved and the deterministic admission derived a concrete, non-empty writeSet. The admission result is the only write authorization.",
884
+ shell: {
885
+ commands: [],
886
+ frontendWriterAdmission: {
887
+ schemaVersion: 1,
888
+ designReviewFromNodeId: "frontend-design-review-pi",
889
+ frozenCommandLabels: [
890
+ ...staticVerifyEvidence.commandLabels,
891
+ ...behaviorVerifyEvidence.commandLabels,
892
+ ...(mockVerifyEvidence?.commandLabels ?? []),
893
+ ],
894
+ allowedMockStrategies: taskConfig.frontendMock?.policy === "disabled" ||
895
+ frontendMockStrategyMustBeNotNeeded(frontendSources)
896
+ ? ["not-needed"]
897
+ : taskConfig.frontendMock?.policy === "required"
898
+ ? [
899
+ "native",
900
+ "browser-intercept",
901
+ "request-adapter",
902
+ ]
903
+ : [
904
+ "native",
905
+ "browser-intercept",
906
+ "request-adapter",
907
+ "not-needed",
908
+ ],
909
+ ...(lintShellCommands.length > 0 && lintVerifyEvidence
910
+ ? {
911
+ lintCommands: lintShellCommands,
912
+ lintEvidence: lintVerifyEvidence,
913
+ }
914
+ : {}),
915
+ },
916
+ cwd: ".",
917
+ timeoutMs: 60000,
918
+ },
919
+ },
920
+ {
921
+ id: implementId,
922
+ depends_on: ["frontend-writer-admission-shell"],
923
+ ...buildFrontendWriterNodeDefaults({
924
+ complexity: resolveWriterComplexity(taskConfig),
925
+ writeSet: implementPaths.writeSet,
926
+ allowedPaths: implementPaths.allowedPaths,
927
+ forbiddenPaths,
928
+ writerOutcomePolicyType: "frontend-facts-v1",
929
+ }),
930
+ outputContract: "The implementation status is derived by the executor from mechanical facts (write-tool events, run delta, write guard, requirement coverage, focused-check), not from any IMPLEMENTATION_OUTCOME first line. Deliver a Markdown 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.",
931
+ subtask_prompt: [
932
+ "Implement against the validated run-owned Frontend Implementation Contract materialized by frontend-design-policy-shell (path/schema/hash) and authorized by frontend-writer-admission-shell. Do not rebuild the contract from Markdown alone.",
933
+ "WRITER TOOL PROTOCOL (hard): the response text is not delivery. Never paste source code, test code, or full file contents into chat. After reading the canonical contract, make the first implementation change with the structured write/edit tool (one file per call); continue writing through those tools until the writeSet is complete. If a write/edit tool is unavailable, stop and report the blocked capability instead of drafting code in the response.",
934
+ "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.",
935
+ "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.",
936
+ "Map every requirement id, expectedOutcome, interaction trigger/expectedBehavior, and applicable UI state from the contract to concrete files. Do not invent shell verification commands; only frozen static/behavior entrypoints will run.",
937
+ "A behavior verification target's target.id is only the contract's identifier for that entry; it does not need to appear in test titles. Never add tests, rename describe/it/test titles, or restructure files just to carry generated ids — reuse affected existing test files and their names. The requirement ↔ verification-target association lives in the contract (requirementIds / verificationTargetIds), not in title strings.",
938
+ "Begin implementation after the contract and its target files are confirmed. Do not spend the turn collecting optional context. If the canonical contract lacks behavior needed to edit safely, stop and state the blocking reason in the summary instead of reopening broad discovery.",
939
+ "Your implementation status is derived by the executor from mechanical facts (persisted write-tool events, run delta, write guard, requirement coverage, focused-check failures), never from any IMPLEMENTATION_OUTCOME first line. Do not emit an IMPLEMENTATION_OUTCOME first line.",
940
+ "The node runs a bounded micro-loop: after each write attempt the executor re-runs frozen focused checks and records a per-round diff checkpoint; the write guard stays active every round. Only repair local issues attributable to the current diff (syntax/type/import/format/unit-assert/obvious omission). Never change requirements, design, writeSet, or verification strictness inside the loop.",
941
+ "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.",
942
+ "frontend-design-policy-shell materialized and validated the canonical contract; frontend-writer-admission-shell authorized the writeSet. Stay within writeSet and preserve unrelated files.",
943
+ "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.",
944
+ "Do not write root artifacts/** unless explicitly included in writeSet. Do not claim Browser/visual verification.",
945
+ "Edit existing files with the structured edit/write tools. NEVER rewrite Markdown (or any file with quoting/backticks/indentation-sensitive content) via bash sed/awk/echo redirection: escaping mistakes silently corrupt the file and self-repair loops burn the run.",
946
+ ...(implementPaths.docIndexCompanions.length > 0
947
+ ? [
948
+ `Doc index sync is MANDATORY: ${implementPaths.docIndexCompanions.join(", ")} are catalog index files for this writeSet. When you add, rename, or remove any indexed file, you MUST also update ${implementPaths.docIndexCompanions.join(" and ")} in the same run (append or fix the matching index entry following the existing line style). Verification runs check-doc-index and fails the run when a new file is missing from the index.`,
949
+ ]
950
+ : []),
951
+ writerDeliveryContract(taskConfig),
952
+ ]
953
+ .filter((value) => Boolean(value))
954
+ .join("\n\n"),
955
+ },
956
+ {
957
+ id: "frontend-verify-shell",
958
+ depends_on: [implementId],
959
+ role: "verifier",
960
+ executor: "shell",
961
+ complexity: "LOW",
962
+ writePolicy: "read-only",
963
+ allowedPaths: readOnlyPaths,
964
+ forbiddenPaths,
965
+ outputContract: "Run frozen Mock/static/behavior commands and materialize the verification trace. Any failure is terminal: no same-run repair branch, failure ownership facts are materialized for recovery.",
966
+ subtask_prompt: "Execute the frontend verification bundle. Preserve per-command evidence; a failure fails this node (terminal) and routes to recovery.",
967
+ shell: {
968
+ commands: [],
969
+ frontendVerificationBundle: {
970
+ schemaVersion: 1,
971
+ mockCommands: mockShellCommands,
972
+ lintCommands: lintShellCommands,
973
+ staticCommands: staticShellCommands,
974
+ behaviorCommands: behaviorShellCommands,
975
+ mockEvidence: mockVerifyEvidence,
976
+ lintEvidence: lintVerifyEvidence,
977
+ staticEvidence: staticVerifyEvidence,
978
+ behaviorEvidence: behaviorVerifyEvidence,
979
+ lintBaselineNodeId: lintShellCommands.length > 0
980
+ ? "frontend-writer-admission-shell"
981
+ : undefined,
982
+ writerNodeIds: lintShellCommands.length > 0 ? [implementId] : [],
983
+ mode: "initial",
984
+ },
985
+ cwd: ".",
986
+ timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
987
+ },
988
+ },
989
+ {
990
+ id: "frontend-review-context-shell",
991
+ depends_on: ["frontend-verify-shell", implementId],
992
+ role: "verifier",
993
+ executor: "shell",
994
+ complexity: "LOW",
995
+ writePolicy: "read-only",
996
+ allowedPaths: readOnlyPaths,
997
+ forbiddenPaths,
998
+ outputContract: "Canonical frontend review context containing a hash-bound contract reference and field index, lint assessment when configured, effective verification trace, optional verify-failure facts, and actual worktree diff.",
999
+ subtask_prompt: "Capture the actual diff and bind it to the effective verification evidence for final review.",
1000
+ shell: {
1001
+ commands: [],
1002
+ frontendReviewContext: { schemaVersion: 1, requireBaseline: true },
1003
+ cwd: ".",
1004
+ timeoutMs: 120000,
1005
+ },
1006
+ },
1007
+ {
1008
+ id: "frontend-review-pi",
1009
+ depends_on: ["frontend-review-context-shell"],
1010
+ role: "reviewer",
1011
+ executor: "pi",
1012
+ complexity: "HIGH",
1013
+ writePolicy: "read-only",
1014
+ retryPolicy: FRONTEND_REVIEW_TERMINAL_RETRY_POLICY,
1015
+ allowedPaths: readOnlyPaths,
1016
+ forbiddenPaths,
1017
+ skills: FRONTEND_REVIEW_SKILLS,
1018
+ outputContract: 'Authoritative typed review terminal via approve_review / request_review_changes tools. No JSON verdict is required in the response text; the typed terminal fact is the only authority. No file writes.',
1019
+ subtask_prompt: [
1020
+ "Submit findings individually with record_review_finding and stable IDs; terminal tools aggregate saved findings. Correct rejected calls, stop after a successful terminal. Check execution groups against every member source outcome, including permission, threshold and failure-path differences; shared verification is valid only when it proves each independent AC.",
1021
+ "After the typed review terminal succeeds, stop immediately; do not emit a Markdown recap.",
1022
+ "Review the frontend implementation and verification evidence.",
1023
+ "Your authoritative terminal verdict is exactly one committed typed tool call: approve_review or request_review_changes. Call it once and do not call the other afterwards.",
1024
+ "approve_review means the implementation passes; it must not carry Critical or Important findings. request_review_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
1025
+ "Do NOT emit an equivalent JSON verdict in the response text: the committed typed terminal fact is the only authority and no branch or gate reads response-text JSON verdicts.",
1026
+ "Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds a hash-verified canonical contract reference, a field-to-section index, frontend lint assessment when configured, the per-command verification evidence (verificationEvidence.commands: commandId/label/lane/exitCode/ok plus allPassed, and lintStatus with lintConfigured) , the effective verification trace, and the run-owned actual diff. Read contractRef.capacityDiagnosticPath first: use contractRef.path only when full-contract is recommended; otherwise read only the hash-bound contractRef.sections needed for the changed surface and verification claims. Then read diff.reviewSummaryPath. The full artifacts/diff_patch.patch is retained only as audit evidence: do NOT read it in full. For semantic review, read only the named per-file diff fragment in the summary/index (in part order when needed) and then the current source file when necessary. 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/stable-target-id binding only—not semantic correctness.",
1027
+ "Treat lint status exactly as passed | baseline-debt | failed | unavailable, and read lintConfigured: when it is false the task declares no lint commands, so lintStatus unavailable means it is not part of this task — report no lint finding either way. 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, judged from verificationEvidence.commands exit codes rather than from the binding trace.",
1028
+ "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).",
1029
+ "The contract referenced and hash-bound by frontend-review-context.json is the effective plan materialized by frontend-design-policy-shell. Do not re-open task sources, OpenSpec, AI workspace, design-review, writer summary, or verification node prose. Inspect only the canonical review context, its indexed contract sections, its bound diff, and diff-referenced files when semantic review requires source code.",
1030
+ "For uiComponentChoices, coverage is judged by the covers list and is deterministically pre-checked at plan finalize; purpose is a human-readable description and must not be rejected merely for not matching an interaction or uiState name. Responsibility is expressed by the covered ids' expectedBehavior plus the choice rationale.",
1031
+ "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.",
1032
+ "Inspect the frontend-verify-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.",
1033
+ "Distinguish Mock-backed evidence from real API integration evidence and preserve the Real Integration Gap when the backend was not exercised.",
1034
+ "Review implementation quality, behavior/state coverage, verification evidence, and maintainability. Read-only: do not modify files.",
1035
+ ].join("\n\n"),
1036
+ },
1037
+ {
1038
+ id: "frontend-closeout-shell",
1039
+ depends_on: ["frontend-review-context-shell", "frontend-review-pi"],
1040
+ role: "closeout",
1041
+ executor: "shell",
1042
+ complexity: "LOW",
1043
+ writePolicy: "read-only",
1044
+ allowedPaths: readOnlyPaths,
1045
+ forbiddenPaths,
1046
+ outputContract: "Deterministic closeout rendered from committed facts only: coverage matrix, lint status, integration facts, typed review verdict, cumulative diff, browser/visual not-run, risks, and follow-up. No model summaries are re-interpreted.",
1047
+ subtask_prompt: "Render the closeout deterministically from frontend-review-context.json, the verification trace, and the committed typed review terminal fact.",
1048
+ shell: {
1049
+ commands: [],
1050
+ frontendCloseout: {
1051
+ schemaVersion: 1,
1052
+ reviewFromNodeId: "frontend-review-pi",
1053
+ },
1054
+ cwd: ".",
1055
+ timeoutMs: 60000,
1056
+ },
1057
+ },
1058
+ ],
1059
+ };
1060
+ // The static DAG template is the topology source of truth: the generated
1061
+ // chain must match the template's node set and order, and the template's
1062
+ // depends_on must be a subset of the generated one (the runtime may add
1063
+ // dependencies, e.g. Mock-required planning depending on the contract
1064
+ // node's Mock facts). The template's own tasks carry simplified placeholder
1065
+ // configs; the generated node definitions (budgets, retry, skeleton,
1066
+ // skills, prompts) and any runtime-added dependencies win.
1067
+ const frontendTemplate = await loadFrontendDagTemplate(sources.repoRoot);
1068
+ if (frontendTemplate) {
1069
+ const byId = new Map(spec.tasks.map((task) => [task.id, task]));
1070
+ const templateIds = frontendTemplate.tasks.map((task) => task.id);
1071
+ const missing = templateIds.filter((id) => !byId.has(id));
1072
+ if (missing.length > 0) {
1073
+ throw new Error(`frontend DAG template topology drift: generated chain is missing template node(s): ${missing.join(", ")}`);
1074
+ }
1075
+ for (const templateTask of frontendTemplate.tasks) {
1076
+ const generated = byId.get(templateTask.id);
1077
+ const generatedDeps = new Set(generated.depends_on);
1078
+ const templateOnly = templateTask.depends_on.filter((dep) => !generatedDeps.has(dep));
1079
+ if (templateOnly.length > 0) {
1080
+ throw new Error(`frontend DAG template topology drift: generated node ${templateTask.id} is missing template dependency ${templateOnly.join(", ")}`);
1081
+ }
1082
+ }
1083
+ const templateSet = new Set(templateIds);
1084
+ const ordered = templateIds.map((id) => byId.get(id));
1085
+ const extra = spec.tasks.filter((task) => !templateSet.has(task.id));
1086
+ spec.tasks = [...ordered, ...extra];
1087
+ }
1088
+ if (frontendTaskShape.shape === "split-required") {
1089
+ spec.tasks = pruneFrontendTasksForSplitRequired(spec.tasks);
1090
+ }
1091
+ else {
1092
+ const reshapeRaisedTopology = frontendTaskShape.signals.includes("shape-transition-reshape");
1093
+ if (!reshapeRaisedTopology) {
1094
+ spec.tasks = pruneFrontendTasksForRisk(spec.tasks, frontendRisk);
1095
+ }
1096
+ if (frontendTaskShape.shape === "micro") {
1097
+ spec.tasks = pruneFrontendTasksForMicro(spec.tasks);
1098
+ }
1099
+ }
1100
+ spec.advisories = [
1101
+ ...(spec.advisories ?? []),
1102
+ `frontend-task-shape: ${frontendTaskShape.shape} (${frontendTaskShape.reason})`,
1103
+ ];
1104
+ // Freeze the frontend recovery continuation quota from task.json so the
1105
+ // runner can bound M6 auto-recovery without re-reading the task config.
1106
+ const frontendMaxContinuations = sources.taskConfig.frontendRecovery?.maxContinuations ?? 1;
1107
+ spec.frontendRecovery = { maxContinuations: frontendMaxContinuations };
1108
+ // Configurable execution safety quota, independent of model context/output capacity.
1109
+ spec.budget = { schemaVersion: 1, mode: "hard", limits: { maxProviderRequests: sources.taskConfig.frontendRecovery?.maxProviderRequests ?? 1024 } };
1110
+ spec.frontendExecutionPolicy = frontendExecutionPolicySchema.parse(sources.taskConfig.frontendExecutionPolicy ?? {});
1111
+ applyDefaultReadOnlyRetryPolicy(spec);
1112
+ stampGeneratedArtifactBindings(spec);
1113
+ parseDagSpec(spec);
1114
+ assertValidDagSpec(spec);
1115
+ return spec;
1116
+ }
1117
+ export async function loadFrontendDagTemplate(repoRoot) {
1118
+ const candidates = [];
1119
+ if (repoRoot) {
1120
+ candidates.push(path.join(repoRoot, "docs", "templates", "frontend-implementation-dag.json"));
1121
+ }
1122
+ candidates.push(fileURLToPath(new URL("../../../docs/templates/frontend-implementation-dag.json", import.meta.url)));
1123
+ for (const candidate of candidates) {
1124
+ try {
1125
+ const parsed = JSON.parse(await readFile(candidate, "utf8"));
1126
+ if (!Array.isArray(parsed.tasks))
1127
+ continue;
1128
+ const tasks = parsed.tasks.filter((item) => typeof item === "object" &&
1129
+ item !== null &&
1130
+ typeof item.id === "string" &&
1131
+ Array.isArray(item.depends_on) &&
1132
+ typeof item.executor === "string");
1133
+ if (tasks.length === 0)
1134
+ continue;
1135
+ return { tasks };
1136
+ }
1137
+ catch {
1138
+ // try next candidate
1139
+ }
1140
+ }
1141
+ return null;
1142
+ }
1143
+ export function frontendMockStrategyMustBeNotNeeded(sources) {
1144
+ const policy = sources.taskConfig.frontendMock?.policy ?? "auto";
1145
+ const capability = sources.frontendMockCapability;
1146
+ const capabilityStatus = capability?.status;
1147
+ const hasDeterministicMockVerification = (capability?.verifyCommands.length ?? 0) > 0;
1148
+ return (policy === "auto" &&
1149
+ (sources.frontendMockMode ?? "not-required") === "not-required" &&
1150
+ (capabilityStatus === "absent" ||
1151
+ capabilityStatus === "ambiguous" ||
1152
+ !hasDeterministicMockVerification));
1153
+ }
1154
+ export function frontendImplementationNodeId() {
1155
+ return "frontend-implement-pi";
1156
+ }
1157
+ export function resolveFrontendCapabilityContextBlock(sources) {
1158
+ const risk = sources.frontendRisk;
1159
+ const capability = sources.frontendProjectCapability;
1160
+ const parts = ["## Frontend risk & project capability", ""];
1161
+ if (risk) {
1162
+ parts.push(`Risk level: ${risk.selectedRisk}`, `Risk reason: ${risk.reason}`, `Risk signals: ${risk.signals.join(", ") || "(none)"}`, `Rejected signals: ${risk.rejectedSignals.join(", ") || "(none)"}`, `Force full gates: ${risk.forceFullGates}`);
1163
+ }
1164
+ else {
1165
+ parts.push("Risk level: standard (not precomputed)");
1166
+ }
1167
+ if (capability) {
1168
+ parts.push("", capability.adapterGuidance);
1169
+ parts.push("Task-relevant OpenSpec candidates are injected separately as a bounded Top-K index. The deterministic prewrite gate retains the complete frozen candidate set; unlisted candidates are neither silently required nor evidence of a missing specification.");
1170
+ parts.push(`A11y capability: ${capability.a11y.status}` +
1171
+ (capability.a11y.tools.length
1172
+ ? ` (${capability.a11y.tools.join(", ")})`
1173
+ : ""));
1174
+ parts.push("Browser accessibility verification: not-run (out of scope for this workflow).");
1175
+ }
1176
+ return parts.join("\n");
1177
+ }
1178
+ export function resolveFrontendShapeCapsuleGenerationInput(sources) {
1179
+ const explicit = sources.frontendShapeTransitionCapsule == null
1180
+ ? undefined
1181
+ : parseFrontendShapeTransitionCapsule(sources.frontendShapeTransitionCapsule);
1182
+ const autoloaded = sources.autoloadedFrontendShapeTransitionCapsule == null
1183
+ ? undefined
1184
+ : parseFrontendShapeTransitionCapsule(sources.autoloadedFrontendShapeTransitionCapsule);
1185
+ if (explicit && autoloaded && explicit.capsuleDigest !== autoloaded.capsuleDigest) {
1186
+ throw new Error("explicit frontend shape transition capsule conflicts with runtime autoload");
1187
+ }
1188
+ return explicit ?? autoloaded;
1189
+ }
1190
+ export function deriveFrontendBehaviorPaths(taskConfig) {
1191
+ if (taskConfig.allowedPaths.length === 0)
1192
+ return ["**"];
1193
+ const explicitTestPaths = taskConfig.allowedPaths.filter((entry) => entry.includes("/test/") ||
1194
+ entry.includes("/tests/") ||
1195
+ entry.includes("/e2e/") ||
1196
+ entry.includes("/spec/"));
1197
+ if (explicitTestPaths.length > 0)
1198
+ return explicitTestPaths;
1199
+ return taskConfig.allowedPaths;
1200
+ }
1201
+ export const FRONTEND_NO_STATIC_VERIFICATION_MARKER = `node -e "console.log('${FRONTEND_NO_VERIFICATION_MARKER_TEXT}; static/behavior verification not-run')"`;
1202
+ export function chooseFrontendVerifyCommands(input) {
1203
+ if (input.explicitCommands && input.explicitCommands.length > 0) {
1204
+ return { commands: input.explicitCommands, commandSource: "inline" };
1205
+ }
1206
+ if (input.parsedCommands.length > 0) {
1207
+ return { commands: input.parsedCommands, commandSource: "inline" };
1208
+ }
1209
+ if (input.allowAdapter !== false &&
1210
+ input.adapterCommands &&
1211
+ input.adapterCommands.length > 0) {
1212
+ return { commands: input.adapterCommands, commandSource: "adapter" };
1213
+ }
1214
+ return { commandSource: "inline" };
1215
+ }
1216
+ export async function resolveFrontendOpenspecGateConfig(sources) {
1217
+ const taskConfig = sources.taskConfig;
1218
+ const policy = taskConfig.frontendOpenspec?.policy ?? "cited";
1219
+ const declared = taskConfig.frontendOpenspec?.requiredReadPaths ?? [];
1220
+ const governanceRoot = sources.frontendProjectCapability?.openspecDiscovery?.governanceRoot ??
1221
+ DEFAULT_OPENSPEC_GOVERNANCE_ROOT;
1222
+ const specRoots = taskConfig.frontendOpenspec?.specRoots ?? DEFAULT_FRONTEND_SPEC_ROOTS;
1223
+ const rootAliases = sources.repoRoot
1224
+ ? await resolveFrontendSpecRootAliases(sources.repoRoot, [...specRoots])
1225
+ : [];
1226
+ const remappedSourceMarkdown = rootAliases.reduce((markdown, { alias, logicalRoot }) => markdown.replace(new RegExp(`(^|[^A-Za-z0-9_.-])${alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`, "g"), `$1${logicalRoot}/`), [sources.requirementMarkdown, sources.constraintMarkdown ?? ""].join("\n"));
1227
+ const taskSourceCited = extractTaskSourceFrontendSpecPaths(remappedSourceMarkdown, specRoots, governanceRoot);
1228
+ const scanStrict = sources.frontendProjectCapability?.designEvidence.normativePaths ?? [];
1229
+ const registry = sources.repoRoot
1230
+ ? await readFrontendSpecRegistry(sources.repoRoot)
1231
+ : null;
1232
+ const registryModes = new Map();
1233
+ for (const root of registry?.roots ?? []) {
1234
+ if (root.scope && !root.scope.includes("frontend"))
1235
+ continue;
1236
+ if (!root.mode)
1237
+ continue;
1238
+ for (const candidate of scanStrict) {
1239
+ if (candidate === root.path || candidate.startsWith(`${root.path}/`)) {
1240
+ registryModes.set(candidate, root.mode);
1241
+ }
1242
+ }
1243
+ }
1244
+ const dedupeSorted = (paths) => [...new Set(paths)].sort();
1245
+ const openspecCandidateSources = {
1246
+ declared: dedupeSorted(declared),
1247
+ taskSourceCited: dedupeSorted(taskSourceCited),
1248
+ scanStrict: dedupeSorted(scanStrict),
1249
+ };
1250
+ // Candidate discovery is frozen independently from the eventual must-read
1251
+ // set. The selector may mark candidates irrelevant, while explicit task
1252
+ // declarations/source citations are never allowed to be downgraded.
1253
+ const openspecMandatoryPaths = dedupeSorted([
1254
+ ...declared,
1255
+ ...taskSourceCited,
1256
+ ]);
1257
+ // `cited` must stay demand-driven: a normative registry makes a path
1258
+ // discoverable, not implicitly task-mandatory. Only scan-strict may offer
1259
+ // the global discovery set to the bounded task-relevance selector.
1260
+ const openspecCandidatePaths = policy === "cited"
1261
+ ? openspecMandatoryPaths
1262
+ : dedupeSorted([
1263
+ ...openspecCandidateSources.scanStrict,
1264
+ ...openspecMandatoryPaths,
1265
+ ]);
1266
+ const result = {
1267
+ openspecPolicy: policy,
1268
+ openspecCandidatePaths,
1269
+ openspecCandidateSources,
1270
+ openspecMandatoryPaths,
1271
+ openspecCandidateSummaries: buildFrontendSpecCandidateSummaries({
1272
+ paths: openspecCandidatePaths,
1273
+ mandatoryPaths: openspecMandatoryPaths,
1274
+ registryModes,
1275
+ sourceMarkdown: [sources.requirementMarkdown, sources.constraintMarkdown ?? ""].join("\n"),
1276
+ }),
1277
+ };
1278
+ if (sources.repoRoot && openspecCandidatePaths.length > 0) {
1279
+ result.openspecCandidateSnapshots = await Promise.all(openspecCandidatePaths.map(async (candidate) => ({
1280
+ path: candidate,
1281
+ sha256: createHash("sha256")
1282
+ .update(await readFile(path.join(sources.repoRoot, candidate)))
1283
+ .digest("hex"),
1284
+ })));
1285
+ }
1286
+ return result;
1287
+ }
1288
+ export function filterRelevantOpenspecCandidates(input) {
1289
+ const mandatory = new Set(input.mandatoryPaths);
1290
+ const relevant = [];
1291
+ for (const candidate of input.candidates) {
1292
+ if (mandatory.has(candidate)) {
1293
+ relevant.push(candidate);
1294
+ continue;
1295
+ }
1296
+ if (openspecCandidateMatchesSource(candidate, input.sourceMarkdown)) {
1297
+ relevant.push(candidate);
1298
+ }
1299
+ }
1300
+ // 保持候选发现顺序(确定性);mandatory 与相关候选都按原始顺序出现。
1301
+ const maxCandidates = input.maxCandidates ?? 24;
1302
+ if (relevant.length <= maxCandidates)
1303
+ return relevant;
1304
+ const mandatoryRelevant = relevant.filter((candidate) => mandatory.has(candidate));
1305
+ const optionalRelevant = relevant.filter((candidate) => !mandatory.has(candidate));
1306
+ return [
1307
+ ...mandatoryRelevant,
1308
+ ...optionalRelevant.slice(0, Math.max(0, maxCandidates - mandatoryRelevant.length)),
1309
+ ];
1310
+ }
1311
+ export const frontendComponentConformanceInstruction = [
1312
+ "## Component Selection conformance (uiComponentChoices; hard rule)",
1313
+ "每个 UI 用途必须在契约的 uiComponentChoices[] 中声明组件选型:{ purpose, component, decision, specReference, rationale }。",
1314
+ "覆盖关系以 covers 列表为准:每个 applicable UI state 与 interaction 必须出现在某条 choice 的 purpose 或 covers 中,且运行时已在 plan finalize 阶段确定性预检——不要重复翻案。purpose 是描述用途的自由文本,不得仅因未精确匹配 interaction.name 或 uiState.name 而判缺陷;职责语义由被覆盖 id 的 expectedBehavior 与 rationale 表达。",
1315
+ "- decision=specified:前端规范(候选组件/主题桶 + 任务源显式引用)已定义该用途组件 → 必须使用该组件,并给精确 specReference { path, section, line }(path 必须是 openspec/ai_workspace 受支持规范路径)。",
1316
+ "- decision=reuse-existing:仅当该组件/惯例**确实已存在于仓库当前代码**(如复用现有 ActiveRunBadge 的 oc- class 惯例)→ specReference 可为 null,rationale 必须指明复用的具体现有组件/文件与依据。",
1317
+ "- decision=new:任务源/PRD 要求**新增**该组件(仓库当前不存在该组件文件)→ decision 必须为 new,不得标 reuse-existing;调用 record_component_choice 时传 sourceRequirementIds(关联的 frozen requirement ID)与 plan checklist 列出的 sourceFragmentId,runtime 校验其隶属关系并物化精确的任务源 PRD { path, section, line }。不要读取 PRD 或手填/猜测 specReference;rationale 说明新增纯展示组件、复用既有 CSS 命名与主题变量约定。",
1318
+ "不得静默替换规范组件或自创组件而无偏差声明;spec 已定义该用途组件时不得改选其它组件。",
1319
+ "prewrite gate 确定性交叉校验:仅 decision=specified 的 specReference 必须是候选 OpenSpec 路径、在 typed decision ledger 中声明且有成功 read 事件;decision=new 的 PRD 引用走任务源可追溯性审查,不得按 OpenSpec 候选拒绝。候选桶非空且契约有 UI 可见工作而 uiComponentChoices 缺失/空 → component-choices-missing。",
1320
+ ].join("\n");
1321
+ export function buildFrontendWriterNodeDefaults(input) {
1322
+ return {
1323
+ role: "implementer",
1324
+ executor: "pi",
1325
+ toolProfile: "write",
1326
+ complexity: input.complexity,
1327
+ writePolicy: "exclusive",
1328
+ writeSet: input.writeSet,
1329
+ allowedPaths: input.allowedPaths,
1330
+ forbiddenPaths: input.forbiddenPaths,
1331
+ skills: FRONTEND_BOUNDED_IMPLEMENT_SKILLS,
1332
+ writerOutcomePolicy: {
1333
+ type: input.writerOutcomePolicyType ?? "implementation-outcome-v1",
1334
+ },
1335
+ };
1336
+ }
1337
+ export function pruneFrontendTasksForSplitRequired(tasks) {
1338
+ const writerChain = new Set([
1339
+ "frontend-writer-admission-shell",
1340
+ "frontend-implement-pi",
1341
+ "frontend-verify-shell",
1342
+ "frontend-review-context-shell",
1343
+ "frontend-review-pi",
1344
+ "frontend-closeout-shell",
1345
+ ]);
1346
+ return tasks.filter((task) => !writerChain.has(task.id));
1347
+ }
1348
+ export function pruneFrontendTasksForRisk(tasks, risk) {
1349
+ if (risk.forceFullGates || risk.selectedRisk !== "small") {
1350
+ return tasks;
1351
+ }
1352
+ // Small topology keeps one design review and removes only the redundant
1353
+ // design-review node; the deterministic design-policy and writer-admission
1354
+ // shells consume the surviving plan directly.
1355
+ const drop = new Set(["frontend-design-review-pi"]);
1356
+ const filtered = tasks.filter((task) => !drop.has(task.id));
1357
+ const byId = new Map(filtered.map((task) => [task.id, task]));
1358
+ const remap = (deps) => {
1359
+ if (!deps)
1360
+ return [];
1361
+ const next = [];
1362
+ for (const dep of deps) {
1363
+ if (dep === "frontend-design-review-pi")
1364
+ continue;
1365
+ if (byId.has(dep) || dep === "frontend-implement-pi")
1366
+ next.push(dep);
1367
+ }
1368
+ return [...new Set(next)];
1369
+ };
1370
+ return filtered.map((task) => {
1371
+ const depends_on = remap(task.depends_on);
1372
+ if (task.id === "frontend-writer-admission-shell") {
1373
+ // Small topology: no design review node, so the admission shell drops
1374
+ // the design-review dependency and the verdict requirement.
1375
+ const admission = task.shell?.frontendWriterAdmission;
1376
+ return {
1377
+ ...task,
1378
+ depends_on: ["frontend-design-policy-shell"],
1379
+ shell: admission
1380
+ ? {
1381
+ ...task.shell,
1382
+ commands: task.shell?.commands ?? [],
1383
+ frontendWriterAdmission: {
1384
+ ...admission,
1385
+ designReviewFromNodeId: undefined,
1386
+ },
1387
+ }
1388
+ : task.shell,
1389
+ };
1390
+ }
1391
+ if (task.id === "frontend-implement-pi") {
1392
+ for (const need of ["frontend-writer-admission-shell"]) {
1393
+ if (byId.has(need) && !depends_on.includes(need))
1394
+ depends_on.push(need);
1395
+ }
1396
+ }
1397
+ return { ...task, depends_on };
1398
+ });
1399
+ }
1400
+ export function pruneFrontendTasksForMicro(tasks) {
1401
+ // Micro topology (7 nodes): contract-import-shell → writer-admission →
1402
+ // implement → verify → review-context → review → closeout. Drops scout/plan/
1403
+ // design-policy/design-review and replaces the model contract node with a
1404
+ // deterministic contract-import shell (§7.3: no model, no requirement
1405
+ // rewrites — micro consumes a pre-validated managed Contract).
1406
+ const drop = new Set([
1407
+ "frontend-contract-pi",
1408
+ "frontend-scout-pi",
1409
+ "frontend-plan-pi",
1410
+ "frontend-design-policy-shell",
1411
+ "frontend-design-review-pi",
1412
+ ]);
1413
+ const contractPi = tasks.find((task) => task.id === "frontend-contract-pi");
1414
+ const contractImportShell = contractPi
1415
+ ? {
1416
+ id: "frontend-contract-import-shell",
1417
+ role: "verifier",
1418
+ executor: "shell",
1419
+ complexity: "LOW",
1420
+ writePolicy: "read-only",
1421
+ depends_on: [],
1422
+ allowedPaths: contractPi.allowedPaths,
1423
+ forbiddenPaths: contractPi.forbiddenPaths,
1424
+ subtask_prompt: "Deterministic managed-Contract import + source/binding/schema freshness re-validation; no model, no requirement rewrite.",
1425
+ shell: {
1426
+ commands: [],
1427
+ frontendContractImport: {
1428
+ schemaVersion: 1,
1429
+ artifactName: "frontend-task-contract.json",
1430
+ outputDir: "contracts",
1431
+ requireSourceFreshness: true,
1432
+ },
1433
+ cwd: ".",
1434
+ timeoutMs: 60000,
1435
+ },
1436
+ }
1437
+ : undefined;
1438
+ const filtered = [
1439
+ ...(contractImportShell ? [contractImportShell] : []),
1440
+ ...tasks.filter((task) => !drop.has(task.id)),
1441
+ ];
1442
+ const byId = new Map(filtered.map((task) => [task.id, task]));
1443
+ const remap = (deps) => {
1444
+ if (!deps)
1445
+ return [];
1446
+ const next = [];
1447
+ for (const dep of deps) {
1448
+ if (drop.has(dep))
1449
+ continue;
1450
+ if (byId.has(dep))
1451
+ next.push(dep);
1452
+ }
1453
+ return [...new Set(next)];
1454
+ };
1455
+ return filtered.map((task) => {
1456
+ const depends_on = remap(task.depends_on);
1457
+ if (task.id === "frontend-writer-admission-shell") {
1458
+ const admission = task.shell?.frontendWriterAdmission;
1459
+ return {
1460
+ ...task,
1461
+ depends_on: ["frontend-contract-import-shell"],
1462
+ shell: admission
1463
+ ? {
1464
+ ...task.shell,
1465
+ commands: task.shell?.commands ?? [],
1466
+ frontendWriterAdmission: {
1467
+ ...admission,
1468
+ designReviewFromNodeId: undefined,
1469
+ },
1470
+ }
1471
+ : task.shell,
1472
+ };
1473
+ }
1474
+ return { ...task, depends_on };
1475
+ });
1476
+ }
1477
+ export function buildFrontendSpecCandidateSummaries(input) {
1478
+ const mandatory = new Set(input.mandatoryPaths);
1479
+ return input.paths
1480
+ .map((candidate) => scoreFrontendSpecCandidate(candidate, "", {
1481
+ registryMode: input.registryModes.get(candidate),
1482
+ taskRelated: mandatory.has(candidate) || openspecCandidateMatchesSource(candidate, input.sourceMarkdown),
1483
+ }))
1484
+ .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
1485
+ }
1486
+ export function openspecCandidateMatchesSource(candidate, sourceMarkdown) {
1487
+ const sourceTokens = extractOpenspecSourceTokens(sourceMarkdown);
1488
+ if (sourceTokens.size === 0)
1489
+ return true;
1490
+ const candidateLower = candidate.toLowerCase().replace(/\\/g, "/");
1491
+ const candidateSegments = candidateLower.split("/");
1492
+ const candidateBasename = candidateSegments[candidateSegments.length - 1] ?? "";
1493
+ for (const token of sourceTokens) {
1494
+ if (candidateBasename.includes(token) ||
1495
+ candidateLower.includes(`/${token}/`) ||
1496
+ candidateLower.includes(`${token}.`)) {
1497
+ return true;
1498
+ }
1499
+ }
1500
+ return false;
1501
+ }
1502
+ export function extractOpenspecSourceTokens(markdown) {
1503
+ const tokens = new Set();
1504
+ const push = (raw) => {
1505
+ const cleaned = raw
1506
+ .trim()
1507
+ .replace(/[.*+?^${}()|[\]\\]/g, "")
1508
+ .toLowerCase();
1509
+ if (cleaned.length >= 2 && cleaned.length <= 40)
1510
+ tokens.add(cleaned);
1511
+ };
1512
+ // 反引号内联代码(文件名/组件名)
1513
+ for (const match of markdown.matchAll(/`([^`\n]+)`/g)) {
1514
+ push(match[1] ?? "");
1515
+ }
1516
+ // markdown 链接文本
1517
+ for (const match of markdown.matchAll(/\[([^\]]+)\]\([^)]+\)/g)) {
1518
+ push(match[1] ?? "");
1519
+ }
1520
+ // 路径 token(openspec/ 或 xxx/xxx.md)
1521
+ for (const match of markdown.matchAll(/(?:[A-Za-z0-9_-]+\/)+[A-Za-z0-9_.-]+/g)) {
1522
+ const segments = (match[0] ?? "").split("/");
1523
+ const basename = segments[segments.length - 1] ?? "";
1524
+ push(basename.replace(/\.[a-z0-9]+$/i, ""));
1525
+ for (const segment of segments)
1526
+ push(segment);
1527
+ }
1528
+ return tokens;
1529
+ }