frontend-harness 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/AGENTS.md +17 -5
  2. package/CLAUDE.md +17 -5
  3. package/README.md +65 -11
  4. package/dist/cli/index.js +166 -11
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/runtime/builtin-skills.js +10 -1
  7. package/dist/runtime/builtin-skills.js.map +1 -1
  8. package/dist/runtime/clean.js +6 -1
  9. package/dist/runtime/clean.js.map +1 -1
  10. package/dist/runtime/command-taxonomy.js +12 -1
  11. package/dist/runtime/command-taxonomy.js.map +1 -1
  12. package/dist/runtime/common/naming.d.ts +2 -0
  13. package/dist/runtime/common/naming.js +13 -0
  14. package/dist/runtime/common/naming.js.map +1 -0
  15. package/dist/runtime/common/parsing.d.ts +11 -0
  16. package/dist/runtime/common/parsing.js +30 -0
  17. package/dist/runtime/common/parsing.js.map +1 -0
  18. package/dist/runtime/common/text.d.ts +1 -0
  19. package/dist/runtime/common/text.js +4 -0
  20. package/dist/runtime/common/text.js.map +1 -0
  21. package/dist/runtime/context.js +3 -2
  22. package/dist/runtime/context.js.map +1 -1
  23. package/dist/runtime/graph.js +1 -1
  24. package/dist/runtime/graph.js.map +1 -1
  25. package/dist/runtime/knowledge.d.ts +136 -1
  26. package/dist/runtime/knowledge.js +658 -17
  27. package/dist/runtime/knowledge.js.map +1 -1
  28. package/dist/runtime/plan/component-resolver.d.ts +8 -0
  29. package/dist/runtime/plan/component-resolver.js +350 -0
  30. package/dist/runtime/plan/component-resolver.js.map +1 -0
  31. package/dist/runtime/plan/guidance.d.ts +3 -0
  32. package/dist/runtime/plan/guidance.js +143 -0
  33. package/dist/runtime/plan/guidance.js.map +1 -0
  34. package/dist/runtime/plan/proposal.d.ts +2 -0
  35. package/dist/runtime/plan/proposal.js +251 -0
  36. package/dist/runtime/plan/proposal.js.map +1 -0
  37. package/dist/runtime/plan/workflow.d.ts +8 -0
  38. package/dist/runtime/plan/workflow.js +234 -0
  39. package/dist/runtime/plan/workflow.js.map +1 -0
  40. package/dist/runtime/plan.d.ts +4 -3
  41. package/dist/runtime/plan.js +163 -445
  42. package/dist/runtime/plan.js.map +1 -1
  43. package/dist/runtime/policy-provenance.js +30 -17
  44. package/dist/runtime/policy-provenance.js.map +1 -1
  45. package/dist/runtime/project-discovery.js +12 -4
  46. package/dist/runtime/project-discovery.js.map +1 -1
  47. package/dist/runtime/project-paths.js +5 -2
  48. package/dist/runtime/project-paths.js.map +1 -1
  49. package/dist/runtime/protocol-init.js +7 -4
  50. package/dist/runtime/protocol-init.js.map +1 -1
  51. package/dist/runtime/repair-decision.js +15 -30
  52. package/dist/runtime/repair-decision.js.map +1 -1
  53. package/dist/runtime/repair-packet.js +8 -1
  54. package/dist/runtime/repair-packet.js.map +1 -1
  55. package/dist/runtime/scaffold/vue-template.d.ts +7 -0
  56. package/dist/runtime/scaffold/vue-template.js +187 -0
  57. package/dist/runtime/scaffold/vue-template.js.map +1 -0
  58. package/dist/runtime/scaffold.d.ts +21 -0
  59. package/dist/runtime/scaffold.js +80 -0
  60. package/dist/runtime/scaffold.js.map +1 -0
  61. package/dist/runtime/skills.js +3 -3
  62. package/dist/runtime/skills.js.map +1 -1
  63. package/dist/runtime/state.d.ts +4 -2
  64. package/dist/runtime/state.js +84 -20
  65. package/dist/runtime/state.js.map +1 -1
  66. package/dist/runtime/ui-restoration.d.ts +4 -0
  67. package/dist/runtime/ui-restoration.js +38 -0
  68. package/dist/runtime/ui-restoration.js.map +1 -0
  69. package/dist/runtime/units.js +38 -2
  70. package/dist/runtime/units.js.map +1 -1
  71. package/dist/runtime/verification-commands.js +8 -4
  72. package/dist/runtime/verification-commands.js.map +1 -1
  73. package/dist/runtime/verify.js +107 -40
  74. package/dist/runtime/verify.js.map +1 -1
  75. package/dist/schemas/types.d.ts +73 -6
  76. package/dist/schemas/validation.js +21 -0
  77. package/dist/schemas/validation.js.map +1 -1
  78. package/dist/storage/json.js +6 -1
  79. package/dist/storage/json.js.map +1 -1
  80. package/docs/DIRECTION.md +1 -1
  81. package/package.json +3 -4
@@ -1,23 +1,29 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
2
  import { harnessPath, relativeHarnessPath } from "../storage/paths.js";
4
3
  import { writeJson, writeText } from "../storage/json.js";
5
4
  import { updatePlannedState } from "./state.js";
6
- import { resolveComponentPlacement } from "./project-discovery.js";
7
5
  import { buildComponentGraph } from "./graph.js";
8
6
  import { resolveProjectRelativePath } from "./project-paths.js";
9
7
  import { buildPolicyProvenance } from "./policy-provenance.js";
8
+ import { discoverVerificationCommands } from "./verification-commands.js";
9
+ import { inferPlanComponents, resolveComponentSource, resolvePlanComponents } from "./plan/component-resolver.js";
10
+ import { buildExecutionGuidance, renderGuidance } from "./plan/guidance.js";
11
+ import { readAgentPlanningProposal } from "./plan/proposal.js";
12
+ import { inferFrontendWorkflow, inferTaskIntent, verificationForWorkflow } from "./plan/workflow.js";
13
+ import { unitIdFromName } from "./common/naming.js";
10
14
  export function createPlan(projectRoot, input) {
11
15
  if (!input.taskText.trim()) {
12
16
  throw new Error("plan requires a task");
13
17
  }
14
18
  const normalizedInput = normalizePlanInput(projectRoot, input);
15
- const task = understandTask(projectRoot, normalizedInput);
16
- const components = inferComponents(projectRoot, task);
17
- const units = buildExecutionUnits(components, task);
19
+ const proposal = normalizedInput.agentProposal ? readAgentPlanningProposal(projectRoot, normalizedInput.agentProposal) : null;
20
+ const task = understandTask(projectRoot, normalizedInput, proposal);
21
+ const resolved = resolvePlan(projectRoot, task, proposal);
22
+ const components = resolved.components;
23
+ const units = buildExecutionUnits(projectRoot, components, task, proposal);
18
24
  const componentGraph = buildComponentGraph(components, units);
19
25
  const executionGuidance = buildExecutionGuidance();
20
- const guidance = renderGuidance(task, components, units, executionGuidance);
26
+ const guidance = renderGuidance(task, components, units, executionGuidance, resolved.meta);
21
27
  const output = {
22
28
  task,
23
29
  components,
@@ -25,7 +31,8 @@ export function createPlan(projectRoot, input) {
25
31
  executionGuidance,
26
32
  componentGraphPath: relativeHarnessPath("component-graph", "latest.json"),
27
33
  guidancePath: relativeHarnessPath("guidance", "latest.md"),
28
- statePath: relativeHarnessPath("state.json")
34
+ statePath: relativeHarnessPath("state.json"),
35
+ meta: resolved.meta
29
36
  };
30
37
  writeJson(harnessPath(projectRoot, "plans", "latest.json"), output);
31
38
  writeJson(harnessPath(projectRoot, "execution-units", "latest.json"), {
@@ -33,22 +40,23 @@ export function createPlan(projectRoot, input) {
33
40
  });
34
41
  writeJson(harnessPath(projectRoot, "component-graph", "latest.json"), componentGraph);
35
42
  writeText(harnessPath(projectRoot, "guidance", "latest.md"), guidance);
36
- updatePlannedState(projectRoot, task, components, units);
43
+ updatePlannedState(projectRoot, task, components, units, highSeverityAmbiguityBlockers(proposal));
37
44
  return output;
38
45
  }
39
- export function checkAgentPlanningDecision(projectRoot, file) {
46
+ export function checkAgentPlanningProposal(projectRoot, file) {
40
47
  if (!file) {
41
- throw new Error("decision check requires --file <agent-decision.json>");
48
+ throw new Error("proposal check requires --file <agent-proposal.json>");
42
49
  }
43
50
  const artifactPath = file;
44
51
  try {
45
- const relativePath = validateRequiredReferencedFile(projectRoot, file, "agent decision");
46
- const decision = readAgentPlanningDecision(projectRoot, relativePath);
52
+ const relativePath = validateRequiredReferencedFile(projectRoot, file, "agent proposal");
53
+ const proposal = readAgentPlanningProposal(projectRoot, relativePath);
54
+ validateProposalTargets(projectRoot, proposal);
47
55
  return {
48
56
  status: "passed",
49
57
  artifactPath: relativePath,
50
- contractVersion: decision.contractVersion ?? null,
51
- decision,
58
+ contractVersion: proposal.contractVersion ?? null,
59
+ proposal,
52
60
  errors: []
53
61
  };
54
62
  }
@@ -57,7 +65,7 @@ export function checkAgentPlanningDecision(projectRoot, file) {
57
65
  status: "failed",
58
66
  artifactPath,
59
67
  contractVersion: null,
60
- decision: null,
68
+ proposal: null,
61
69
  errors: [error instanceof Error ? error.message : String(error)]
62
70
  };
63
71
  }
@@ -67,7 +75,7 @@ function normalizePlanInput(projectRoot, input) {
67
75
  taskText: input.taskText,
68
76
  ...optionalString("prd", validateReferencedFile(projectRoot, input.prd, "PRD")),
69
77
  ...optionalString("ui", validateReferencedFile(projectRoot, input.ui, "UI")),
70
- ...optionalString("agentDecision", validateReferencedFile(projectRoot, input.agentDecision, "agent decision"))
78
+ ...optionalString("agentProposal", validateReferencedFile(projectRoot, input.agentProposal, "agent proposal"))
71
79
  };
72
80
  }
73
81
  function validateRequiredReferencedFile(projectRoot, file, label) {
@@ -83,14 +91,11 @@ function validateReferencedFile(projectRoot, file, label) {
83
91
  }
84
92
  return relativePath;
85
93
  }
86
- function understandTask(projectRoot, input) {
94
+ function understandTask(projectRoot, input, proposal) {
87
95
  const text = input.taskText.trim();
88
- const decision = input.agentDecision
89
- ? readAgentPlanningDecision(projectRoot, input.agentDecision)
90
- : null;
91
96
  const scope = text;
92
- const decisionConstraints = decision?.constraintHints ?? [];
93
- const frontendWorkflow = inferFrontendWorkflow(scope, input);
97
+ const proposalConstraints = proposal?.constraintHints ?? [];
98
+ const frontendWorkflow = inferFrontendWorkflow(scope, input, proposal);
94
99
  const intent = inferTaskIntent(scope, frontendWorkflow.kind);
95
100
  return {
96
101
  intent,
@@ -98,206 +103,116 @@ function understandTask(projectRoot, input) {
98
103
  constraints: [
99
104
  "Use the agent-invoked harness loop.",
100
105
  "CLI owns project facts and verification.",
101
- "System owns deterministic planning; agent planning input is limited to bounded hints.",
106
+ "System owns final planning decisions; agent proposals are bounded inputs resolved through policy.",
102
107
  ...frontendWorkflow.implementationConstraints,
103
- ...decisionConstraints
108
+ ...proposalConstraints
104
109
  ],
105
110
  frontendWorkflow,
106
111
  policyProvenance: buildPolicyProvenance(projectRoot, scope, frontendWorkflow.kind),
107
112
  inputs: {
108
113
  ...(input.prd ? { prd: input.prd } : {}),
109
114
  ...(input.ui ? { ui: input.ui } : {}),
110
- ...(input.agentDecision ? { agentDecision: input.agentDecision } : {})
115
+ ...(input.agentProposal ? { agentProposal: input.agentProposal } : {})
111
116
  },
112
- ...buildAgentPlanningMetadata(input.agentDecision, decision)
117
+ ...buildAgentPlanningMetadata(input.agentProposal, proposal)
113
118
  };
114
119
  }
115
- function inferTaskIntent(scope, workflowKind) {
116
- const normalized = scope.toLowerCase();
117
- if (workflowKind === "bug_fix" || /\b(fix|bug)\b|缺陷|修复|错误|报错/.test(normalized)) {
118
- return "fix_bug";
119
- }
120
- if (/\brefactor\b|重构/.test(normalized)) {
121
- return "refactor";
122
- }
123
- if (workflowKind === "frontend_test" || /\b(test|e2e|playwright|vitest)\b|测试|端到端|集成测试/.test(normalized)) {
124
- return "add_test";
125
- }
126
- return "implement_page";
127
- }
128
- function buildAgentPlanningMetadata(decisionPath, decision) {
129
- if (!decisionPath) {
120
+ function buildAgentPlanningMetadata(proposalPath, proposal) {
121
+ if (!proposalPath) {
130
122
  return {
131
123
  reasoningMode: "deterministic"
132
124
  };
133
125
  }
134
126
  return {
135
127
  reasoningMode: "agent_assisted",
136
- agentDecision: {
137
- summary: buildAgentDecisionSummary(decisionPath),
138
- artifactPath: decisionPath ?? null,
139
- contractVersion: decision?.contractVersion ?? null,
140
- intentSuggestion: decision?.intentSuggestion ?? null,
141
- ...(decision?.constraintHints ? { constraintHints: decision.constraintHints } : {}),
142
- ...(decision?.componentHints ? { componentHints: decision.componentHints } : {})
128
+ agentProposal: {
129
+ summary: buildAgentProposalSummary(proposalPath),
130
+ artifactPath: proposalPath ?? null,
131
+ contractVersion: proposal?.contractVersion ?? null,
132
+ intentSuggestion: proposal?.intentSuggestion ?? null,
133
+ ...(proposal?.constraintHints ? { constraintHints: proposal.constraintHints } : {}),
134
+ ...(proposal?.componentHints ? { componentHints: proposal.componentHints } : {}),
135
+ ...(proposal?.planningEvidence ? { planningEvidence: proposal.planningEvidence } : {})
143
136
  }
144
137
  };
145
138
  }
146
- function buildAgentDecisionSummary(decisionPath) {
147
- return decisionPath
148
- ? `Planning uses structured decision input from ${decisionPath}.`
149
- : "Planning uses structured decision input.";
150
- }
151
- function inferComponents(projectRoot, task) {
152
- const baseName = toPascalCase(task.scope) || "PlannedComponent";
153
- const placement = resolveComponentPlacement(projectRoot, task.scope);
154
- const componentKinds = inferComponentKinds(task.scope);
155
- return componentKinds.map((kind) => {
156
- const name = componentName(baseName, kind.suffix);
157
- return {
158
- name,
159
- file: `${placement.sourceRoot}/${name}${placement.extension}`,
160
- responsibility: task.intent === "fix_bug" ? "bug fix target" : kind.responsibility,
161
- status: "unknown"
162
- };
163
- });
164
- }
165
- function inferComponentKinds(scope) {
166
- const normalized = scope.toLowerCase();
167
- const asksForTable = /\b(table|grid|list)\b/.test(normalized);
168
- const asksForForm = /\b(form|create|edit|filter|search)\b/.test(normalized);
169
- const asksForPage = /\b(page|screen|dashboard|view)\b/.test(normalized);
170
- if (asksForPage && asksForTable && asksForForm) {
171
- return [
172
- {
173
- suffix: "Page",
174
- responsibility: "page composition and data-flow boundary"
175
- },
176
- {
177
- suffix: "Table",
178
- responsibility: "tabular list rendering"
139
+ function buildAgentProposalSummary(proposalPath) {
140
+ return proposalPath
141
+ ? `Planning resolves structured agent proposal input from ${proposalPath}.`
142
+ : "Planning resolves structured agent proposal input.";
143
+ }
144
+ function resolvePlan(projectRoot, task, proposal) {
145
+ const evidenceTargets = validateProposalTargets(projectRoot, proposal);
146
+ const systemComponents = inferPlanComponents(projectRoot, task, evidenceTargets);
147
+ const warnings = [];
148
+ const resolvedComponents = resolvePlanComponents(projectRoot, systemComponents, proposal?.componentHints, task, warnings);
149
+ const components = resolvedComponents.components;
150
+ const workflowEvidence = acceptedWorkflowEvidence(task.frontendWorkflow?.kind, proposal?.planningEvidence);
151
+ const componentSource = resolveComponentSource(resolvedComponents.trace);
152
+ return {
153
+ components,
154
+ meta: {
155
+ decisionTrace: {
156
+ workflow: {
157
+ source: workflowEvidence.accepted ? "agent" : "system",
158
+ reason: workflowEvidence.accepted
159
+ ? `agent planningEvidence workflow accepted: ${task.frontendWorkflow?.kind}`
160
+ : `system classified workflow as ${task.frontendWorkflow?.kind ?? "unknown"}`
161
+ },
162
+ targets: {
163
+ source: evidenceTargets.length ? "merged" : "system",
164
+ reason: evidenceTargets.length
165
+ ? "safe agent planningEvidence targetCandidates were merged into planning boundaries"
166
+ : "system inferred planning boundary"
167
+ },
168
+ components: {
169
+ source: componentSource,
170
+ reason: componentSource === "system"
171
+ ? "system component boundaries retained"
172
+ : componentSource === "agent"
173
+ ? "agent componentHints expanded the UI boundary"
174
+ : "agent componentHints merged into system UI boundary"
175
+ },
176
+ componentHints: resolvedComponents.trace,
177
+ ...(proposal?.planningEvidence
178
+ ? {
179
+ agentEvidence: {
180
+ source: "merged",
181
+ reason: describePlanningEvidence(proposal.planningEvidence)
182
+ }
183
+ }
184
+ : {})
179
185
  },
180
- {
181
- suffix: "Form",
182
- responsibility: "form or filter interaction"
183
- }
184
- ];
185
- }
186
- return [
187
- {
188
- suffix: "",
189
- responsibility: "planned UI/component implementation"
190
- }
191
- ];
192
- }
193
- function inferFrontendWorkflow(scope, input) {
194
- const normalized = scope.toLowerCase();
195
- const hasUiSource = Boolean(input.ui) || /\b(ui|stitch|figma|html|design|page|screen|component|view|dashboard)\b|页面|界面|组件|视图|看板|设计稿/.test(normalized);
196
- const hasPrdSource = Boolean(input.prd) || /\b(prd|product|requirement|requirements|knowledge|document)\b|产品|需求|知识库|文档/.test(normalized);
197
- const hasBugSignal = /\b(bug|fix|regression|error)\b|缺陷|修复|错误|报错/.test(normalized);
198
- const hasTestSignal = /\b(e2e|integration test|test|playwright|vitest)\b|测试|端到端|集成测试/.test(normalized);
199
- const hasApiSignal = /\b(api|swagger|endpoint|interface|request|dto)\b|接口|请求|对接|联调/.test(normalized);
200
- const hasRequirementChange = /\b(requirement changes?|changed [a-z0-9 -]*requirements?|update [a-z0-9 -]*requirements?|sync [a-z0-9 -]*requirements?)\b|需求变更|变更需求|需求[^,。,.]*同步|同步[^,。,.]*需求/.test(normalized);
201
- const kind = hasBugSignal
202
- ? "bug_fix"
203
- : hasRequirementChange
204
- ? "requirement_change"
205
- : hasTestSignal
206
- ? "frontend_test"
207
- : hasApiSignal
208
- ? "api_integration"
209
- : hasPrdSource && !hasUiSource
210
- ? "prd_knowledge"
211
- : "ui_implementation";
212
- return workflowPolicy(kind, { hasPrdSource, hasUiSource });
213
- }
214
- function workflowPolicy(kind, inputSignals) {
215
- const policies = {
216
- ui_implementation: {
217
- kind,
218
- summary: "Implement or adapt frontend UI while preserving project semantics, component boundaries, and validation evidence.",
219
- requiredKnowledgeActions: inputSignals.hasUiSource
220
- ? ["Capture reusable UI semantics, component responsibilities, and design assumptions in project knowledge when they affect future work."]
221
- : ["Reuse existing project knowledge before inventing new UI semantics."],
222
- implementationConstraints: [
223
- "Treat imported MCP/Stitch/Figma/HTML output as source material, not project-ready code.",
224
- "Normalize generated UI into project components, naming, state, routing, and styling conventions."
225
- ],
226
- verificationFocus: ["typecheck", "component behavior", "responsive rendering", "visual regressions when UI source is provided"]
227
- },
228
- prd_knowledge: {
229
- kind,
230
- summary: "Convert PRD or requirement material into durable project knowledge before implementation.",
231
- requiredKnowledgeActions: [
232
- "Promote stable product concepts, terminology, permissions, workflows, and acceptance rules into .frontend-harness/knowledge/.",
233
- "Keep raw PRD text separate from distilled project knowledge."
234
- ],
235
- implementationConstraints: [
236
- "Do not implement product behavior until PRD-derived assumptions are explicit.",
237
- "Mark unknown product decisions as blockers or assumptions instead of guessing."
238
- ],
239
- verificationFocus: ["knowledge coverage", "acceptance criteria traceability", "terminology consistency"]
240
- },
241
- api_integration: {
242
- kind,
243
- summary: "Integrate frontend behavior with API contracts using project knowledge plus Swagger/API evidence.",
244
- requiredKnowledgeActions: [
245
- "Reference relevant domain knowledge before mapping request parameters, DTOs, and response states.",
246
- "Promote durable API assumptions when Swagger or backend behavior clarifies product rules."
247
- ],
248
- implementationConstraints: [
249
- "Do not invent API fields, enum values, pagination contracts, or error shapes.",
250
- "Keep request mapping, loading, empty, and error states inside the project API/state conventions."
251
- ],
252
- verificationFocus: ["typecheck", "API mapping", "loading/empty/error states", "integration tests or mocks"]
253
- },
254
- frontend_test: {
255
- kind,
256
- summary: "Add or update frontend E2E/integration coverage for user-visible behavior.",
257
- requiredKnowledgeActions: [
258
- "Use project knowledge to choose behavior-level assertions instead of implementation-detail assertions.",
259
- "Promote durable testing conventions only when they apply beyond the current task."
260
- ],
261
- implementationConstraints: [
262
- "Prefer behavior-focused integration or E2E tests over brittle snapshot-only checks.",
263
- "Keep fixtures and mocks aligned with known API/product contracts."
264
- ],
265
- verificationFocus: ["targeted test", "typecheck", "critical user path", "failure diagnostics"]
266
- },
267
- bug_fix: {
268
- kind,
269
- summary: "Fix a frontend bug with evidence, minimal scope, and regression coverage proportional to risk.",
270
- requiredKnowledgeActions: [
271
- "Check project knowledge for intended behavior before changing code.",
272
- "Promote the resolved product or technical rule if the bug revealed durable knowledge."
273
- ],
274
- implementationConstraints: [
275
- "Reproduce or explain the bug before patching when evidence is available.",
276
- "Keep the fix scoped to the failing behavior and avoid unrelated refactors."
277
- ],
278
- verificationFocus: ["regression test", "typecheck", "affected workflow", "repair evidence"]
279
- },
280
- requirement_change: {
281
- kind,
282
- summary: "Apply requirement changes and keep implementation, tests, and knowledge synchronized.",
283
- requiredKnowledgeActions: [
284
- "Update project knowledge for changed product rules before or alongside code changes.",
285
- "Record obsolete assumptions so future agents do not reuse stale behavior."
286
- ],
287
- implementationConstraints: [
288
- "Identify impacted UI, API mapping, tests, and knowledge before editing.",
289
- "Do not leave previous requirement behavior undocumented when it changes."
290
- ],
291
- verificationFocus: ["changed acceptance criteria", "knowledge sync", "affected regression tests", "typecheck"]
186
+ warnings
292
187
  }
293
188
  };
294
- return policies[kind];
295
- }
296
- function componentName(baseName, suffix) {
297
- return suffix && !baseName.endsWith(suffix) ? `${baseName}${suffix}` : baseName;
298
189
  }
299
- function buildExecutionUnits(components, task) {
300
- const units = components.map((component) => toUnit(component, task));
190
+ function validateProposalTargets(projectRoot, proposal) {
191
+ const evidenceTargets = (proposal?.planningEvidence?.targetCandidates ?? [])
192
+ .map((candidate, index) => ({
193
+ path: candidate.path,
194
+ confidence: candidate.confidence,
195
+ label: `agent proposal planningEvidence.targetCandidates[${index}].path`
196
+ }));
197
+ const safeTargets = evidenceTargets.map((target) => ({
198
+ path: resolveProjectRelativePath(projectRoot, target.path, target.label).relativePath,
199
+ confidence: target.confidence
200
+ }));
201
+ return [...new Set(safeTargets
202
+ .filter((target) => target.confidence >= 0.7)
203
+ .map((target) => target.path))];
204
+ }
205
+ function describePlanningEvidence(evidence) {
206
+ return [
207
+ `${evidence.workflowCandidates?.length ?? 0} workflow candidate(s)`,
208
+ `${evidence.targetCandidates?.length ?? 0} target candidate(s)`,
209
+ `${evidence.verificationCandidates?.length ?? 0} verification candidate(s)`,
210
+ `${evidence.ambiguities?.length ?? 0} ambiguity item(s)`
211
+ ].join(", ");
212
+ }
213
+ function buildExecutionUnits(projectRoot, components, task, proposal) {
214
+ const verification = resolveVerificationFocus(projectRoot, task, proposal);
215
+ const units = components.map((component) => toUnit(component, task, verification));
301
216
  const pageUnit = units.find((unit) => unit.id.endsWith("-page"));
302
217
  if (pageUnit && units.length > 1) {
303
218
  pageUnit.dependsOn = units
@@ -306,266 +221,69 @@ function buildExecutionUnits(components, task) {
306
221
  }
307
222
  return units;
308
223
  }
309
- function toUnit(component, task) {
224
+ function toUnit(component, task, verification) {
310
225
  return {
311
226
  id: unitIdFromName(component.name),
312
227
  file: component.file,
313
228
  task: `${task.intent}: ${task.scope}`,
314
- verification: verificationForWorkflow(task.frontendWorkflow?.kind),
229
+ verification,
315
230
  dependsOn: [],
316
231
  parallelGroup: null
317
232
  };
318
233
  }
319
- function verificationForWorkflow(kind) {
320
- if (kind === "frontend_test") {
321
- return ["targeted test", "typecheck"];
322
- }
323
- if (kind === "bug_fix") {
324
- return ["regression test", "typecheck"];
325
- }
326
- if (kind === "api_integration") {
327
- return ["typecheck", "integration test or API mock"];
328
- }
329
- if (kind === "requirement_change") {
330
- return ["typecheck", "affected regression test"];
331
- }
332
- if (kind === "prd_knowledge") {
333
- return ["knowledge review"];
334
- }
335
- return ["typecheck"];
336
- }
337
- function readAgentPlanningDecision(projectRoot, file) {
338
- const fullPath = path.resolve(projectRoot, file);
339
- let parsed;
340
- try {
341
- parsed = JSON.parse(fs.readFileSync(fullPath, "utf8"));
342
- }
343
- catch (error) {
344
- const detail = error instanceof Error ? error.message : String(error);
345
- throw new Error(`agent decision file must contain valid JSON: ${file}: ${detail}`);
346
- }
347
- return parseAgentPlanningDecision(parsed, file);
348
- }
349
- function parseAgentPlanningDecision(value, file) {
350
- if (!isRecord(value) || Array.isArray(value)) {
351
- throw new Error(`agent decision file must contain a JSON object: ${file}`);
352
- }
353
- if ("decision" in value || "contractVersion" in value) {
354
- return parseAgentPlanningDecisionEnvelope(value, file);
355
- }
356
- const allowedKeys = new Set(["intentSuggestion", "constraintHints", "componentHints"]);
357
- return parseAgentPlanningDecisionObject(value, allowedKeys, "agent decision file");
234
+ function resolveVerificationFocus(projectRoot, task, proposal) {
235
+ const base = verificationForWorkflow(task.frontendWorkflow?.kind);
236
+ const known = new Set([
237
+ "targeted test",
238
+ "regression test",
239
+ "integration test or API mock",
240
+ "affected regression test",
241
+ "knowledge review",
242
+ "documentation review",
243
+ "review findings",
244
+ ...discoverVerificationCommands(projectRoot)
245
+ .filter((command) => command.command)
246
+ .map((command) => command.name)
247
+ ]);
248
+ const evidence = (proposal?.planningEvidence?.verificationCandidates ?? [])
249
+ .map((candidate) => normalizeVerificationFocus(candidate.kind))
250
+ .filter((candidate) => candidate && known.has(candidate))
251
+ .slice(0, 10);
252
+ return [...new Set([...base, ...evidence])];
253
+ }
254
+ function normalizeVerificationFocus(value) {
255
+ const normalized = value.trim().toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
256
+ const aliases = {
257
+ "targeted test": "targeted test",
258
+ "regression test": "regression test",
259
+ "integration test": "integration test or API mock",
260
+ "integration test or api mock": "integration test or API mock",
261
+ "api mock": "integration test or API mock",
262
+ "affected regression test": "affected regression test",
263
+ "knowledge review": "knowledge review",
264
+ "documentation review": "documentation review",
265
+ "review findings": "review findings",
266
+ "typecheck": "typecheck",
267
+ "test": "test",
268
+ "build": "build"
269
+ };
270
+ return aliases[normalized] ?? value.trim();
358
271
  }
359
- function parseAgentPlanningDecisionEnvelope(value, file) {
360
- const allowedKeys = new Set(["contractVersion", "decision"]);
361
- for (const key of Object.keys(value)) {
362
- if (!allowedKeys.has(key)) {
363
- throw new Error(`agent decision file contains unsupported field: ${key}`);
364
- }
365
- }
366
- if (value["contractVersion"] !== 1) {
367
- throw new Error("agent decision contractVersion must be 1");
368
- }
369
- if (!isRecord(value["decision"]) || Array.isArray(value["decision"])) {
370
- throw new Error(`agent decision file decision must be a JSON object: ${file}`);
371
- }
372
- const decision = parseAgentPlanningDecisionObject(value["decision"], new Set(["intentSuggestion", "constraintHints", "componentHints"]), "agent decision file decision");
272
+ function acceptedWorkflowEvidence(workflowKind, evidence) {
273
+ const candidates = [...(evidence?.workflowCandidates ?? [])].sort((left, right) => right.confidence - left.confidence);
274
+ const primary = candidates[0];
275
+ const runnerUp = candidates[1];
276
+ const margin = primary && runnerUp ? primary.confidence - runnerUp.confidence : primary?.confidence ?? 0;
373
277
  return {
374
- contractVersion: 1,
375
- ...decision
278
+ accepted: Boolean(primary && primary.kind === workflowKind && primary.confidence >= 0.75 && margin >= 0.2)
376
279
  };
377
280
  }
378
- function parseAgentPlanningDecisionObject(value, allowedKeys, label) {
379
- for (const key of Object.keys(value)) {
380
- if (!allowedKeys.has(key)) {
381
- throw new Error(`${label} contains unsupported field: ${key}`);
382
- }
383
- }
384
- const decision = {};
385
- if (value["intentSuggestion"] !== undefined) {
386
- decision.intentSuggestion = parseBoundedString(value["intentSuggestion"], "intentSuggestion");
387
- }
388
- if (value["constraintHints"] !== undefined) {
389
- decision.constraintHints = parseStringArray(value["constraintHints"], "constraintHints");
390
- }
391
- if (value["componentHints"] !== undefined) {
392
- decision.componentHints = parseComponentHints(value["componentHints"]);
393
- }
394
- return decision;
395
- }
396
- function parseStringArray(value, label) {
397
- if (!Array.isArray(value)) {
398
- throw new Error(`agent decision ${label} must be an array of strings`);
399
- }
400
- if (value.length > 10) {
401
- throw new Error(`agent decision ${label} cannot contain more than 10 items`);
402
- }
403
- return value.map((item, index) => parseBoundedString(item, `${label}[${index}]`));
404
- }
405
- function parseComponentHints(value) {
406
- if (!Array.isArray(value)) {
407
- throw new Error("agent decision componentHints must be an array");
408
- }
409
- if (value.length > 10) {
410
- throw new Error("agent decision componentHints cannot contain more than 10 items");
411
- }
412
- return value.map((item, index) => {
413
- if (!isRecord(item) || Array.isArray(item)) {
414
- throw new Error(`agent decision componentHints[${index}] must be an object`);
415
- }
416
- const allowedKeys = new Set(["name", "responsibility"]);
417
- for (const key of Object.keys(item)) {
418
- if (!allowedKeys.has(key)) {
419
- throw new Error(`agent decision componentHints[${index}] contains unsupported field: ${key}`);
420
- }
421
- }
422
- return {
423
- ...(item["name"] !== undefined ? { name: parseBoundedString(item["name"], `componentHints[${index}].name`) } : {}),
424
- ...(item["responsibility"] !== undefined
425
- ? { responsibility: parseBoundedString(item["responsibility"], `componentHints[${index}].responsibility`) }
426
- : {})
427
- };
428
- });
281
+ function highSeverityAmbiguityBlockers(proposal) {
282
+ return (proposal?.planningEvidence?.ambiguities ?? [])
283
+ .filter((ambiguity) => ambiguity.severity === "high")
284
+ .map((ambiguity) => `High-severity planning ambiguity: ${ambiguity.question}`);
429
285
  }
430
286
  function optionalString(key, value) {
431
287
  return value === undefined ? {} : { [key]: value };
432
288
  }
433
- function parseBoundedString(value, label) {
434
- if (typeof value !== "string") {
435
- throw new Error(`agent decision ${label} must be a string`);
436
- }
437
- const trimmed = value.trim();
438
- if (!trimmed) {
439
- throw new Error(`agent decision ${label} cannot be empty`);
440
- }
441
- if (trimmed.length > 500) {
442
- throw new Error(`agent decision ${label} cannot exceed 500 characters`);
443
- }
444
- return trimmed;
445
- }
446
- function isRecord(value) {
447
- return typeof value === "object" && value !== null;
448
- }
449
- function unitIdFromName(name) {
450
- return `unit-${name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}`;
451
- }
452
- function buildExecutionGuidance() {
453
- return [
454
- {
455
- order: 1,
456
- action: "inspect_plan",
457
- description: `Read ${relativeHarnessPath("plans", "latest.json")} and ${relativeHarnessPath("execution-units", "latest.json")} before editing.`,
458
- command: null,
459
- condition: null
460
- },
461
- {
462
- order: 2,
463
- action: "edit_files",
464
- description: "Edit only the project files required by the planned execution units.",
465
- command: null,
466
- condition: null
467
- },
468
- {
469
- order: 3,
470
- action: "record_changes",
471
- description: "Record each changed project file so the harness state reflects the execution boundary.",
472
- command: "frontend-harness state record-change <file>",
473
- condition: "after editing"
474
- },
475
- {
476
- order: 4,
477
- action: "verify",
478
- description: "Run the configured verification commands and inspect the JSON result.",
479
- command: "frontend-harness verify --json",
480
- condition: "after recording changes"
481
- },
482
- {
483
- order: 5,
484
- action: "fix_failed_verification",
485
- description: `If verification fails, generate and inspect ${relativeHarnessPath("repair", "latest.json")} before patching the relevant project files, recording new changes, and rerunning verification.`,
486
- command: "frontend-harness repair packet --json",
487
- condition: "when verification status is failed or error"
488
- }
489
- ];
490
- }
491
- function renderGuidance(task, components, units, executionGuidance) {
492
- return `# Agent Execution Guidance
493
-
494
- ## Task
495
-
496
- - Intent: ${task.intent}
497
- - Scope: ${task.scope}
498
-
499
- ## Frontend Workflow Policy
500
-
501
- - Kind: ${task.frontendWorkflow?.kind ?? "unknown"}
502
- - Summary: ${task.frontendWorkflow?.summary ?? "No workflow policy inferred."}
503
- - Knowledge: ${(task.frontendWorkflow?.requiredKnowledgeActions ?? []).join(" ")}
504
- - Verification focus: ${(task.frontendWorkflow?.verificationFocus ?? []).join(", ")}
505
-
506
- ## Project References
507
-
508
- ${renderProjectReferences(task)}
509
-
510
- ## Loop
511
-
512
- ${executionGuidance.map(renderExecutionGuidanceStep).join("\n")}
513
-
514
- ## Components
515
-
516
- ${components.map((component) => `- ${component.name}: ${component.file} (${component.responsibility})`).join("\n")}
517
-
518
- ## Units
519
-
520
- ${units.map(renderUnitGuidance).join("\n")}
521
- `;
522
- }
523
- function renderProjectReferences(task) {
524
- const provenance = task.policyProvenance;
525
- if (!provenance) {
526
- return "- No project-local skills or knowledge selected.";
527
- }
528
- const lines = [];
529
- if (provenance.skillRoutes.length) {
530
- lines.push("Selected skills:");
531
- for (const route of provenance.skillRoutes) {
532
- lines.push(`- ${route.name}: ${route.path}; source: ${route.source}; score: ${route.score}; reasons: ${route.reasons.join(", ")}`);
533
- }
534
- }
535
- else {
536
- lines.push("Selected skills: none.");
537
- }
538
- if (provenance.knowledgeReferences.length) {
539
- lines.push("Selected knowledge:");
540
- for (const knowledgePath of provenance.knowledgeReferences) {
541
- const sha256 = provenance.knowledgeSha256[knowledgePath];
542
- lines.push(`- ${knowledgePath}${sha256 ? `; sha256: ${sha256}` : ""}`);
543
- }
544
- }
545
- else {
546
- lines.push("Selected knowledge: none.");
547
- }
548
- lines.push(`Selection reason: ${provenance.selectionReason}`);
549
- return lines.join("\n");
550
- }
551
- function renderExecutionGuidanceStep(step) {
552
- const command = step.command ? ` Command: \`${step.command}\`.` : "";
553
- const condition = step.condition ? ` Condition: ${step.condition}.` : "";
554
- return `${step.order}. ${step.description}${command}${condition}`;
555
- }
556
- function renderUnitGuidance(unit) {
557
- const dependencies = JSON.stringify(unit.dependsOn);
558
- const parallelGroup = JSON.stringify(unit.parallelGroup);
559
- const independent = unit.dependsOn.length === 0;
560
- return `- ${unit.id}: ${unit.task}; file ${unit.file}; independent: ${independent}; dependsOn: ${dependencies}; parallelGroup: ${parallelGroup}; verification focus: ${unit.verification.join(", ")}`;
561
- }
562
- function toPascalCase(value) {
563
- return value
564
- .replace(/[^a-zA-Z0-9]+/g, " ")
565
- .trim()
566
- .split(/\s+/)
567
- .slice(0, 4)
568
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
569
- .join("");
570
- }
571
289
  //# sourceMappingURL=plan.js.map