frontend-harness 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -5
- package/CLAUDE.md +9 -5
- package/README.md +56 -11
- package/dist/cli/index.js +33 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/runtime/clean.js +6 -1
- package/dist/runtime/clean.js.map +1 -1
- package/dist/runtime/command-taxonomy.js +4 -1
- package/dist/runtime/command-taxonomy.js.map +1 -1
- package/dist/runtime/common/naming.d.ts +2 -0
- package/dist/runtime/common/naming.js +13 -0
- package/dist/runtime/common/naming.js.map +1 -0
- package/dist/runtime/common/parsing.d.ts +11 -0
- package/dist/runtime/common/parsing.js +30 -0
- package/dist/runtime/common/parsing.js.map +1 -0
- package/dist/runtime/context.js +3 -2
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/knowledge.js +16 -12
- package/dist/runtime/knowledge.js.map +1 -1
- package/dist/runtime/plan/component-resolver.d.ts +8 -0
- package/dist/runtime/plan/component-resolver.js +344 -0
- package/dist/runtime/plan/component-resolver.js.map +1 -0
- package/dist/runtime/plan/guidance.d.ts +3 -0
- package/dist/runtime/plan/guidance.js +119 -0
- package/dist/runtime/plan/guidance.js.map +1 -0
- package/dist/runtime/plan/proposal.d.ts +2 -0
- package/dist/runtime/plan/proposal.js +251 -0
- package/dist/runtime/plan/proposal.js.map +1 -0
- package/dist/runtime/plan/workflow.d.ts +8 -0
- package/dist/runtime/plan/workflow.js +228 -0
- package/dist/runtime/plan/workflow.js.map +1 -0
- package/dist/runtime/plan.d.ts +4 -3
- package/dist/runtime/plan.js +123 -448
- package/dist/runtime/plan.js.map +1 -1
- package/dist/runtime/policy-provenance.js +30 -17
- package/dist/runtime/policy-provenance.js.map +1 -1
- package/dist/runtime/project-discovery.js +12 -3
- package/dist/runtime/project-discovery.js.map +1 -1
- package/dist/runtime/project-paths.js +3 -0
- package/dist/runtime/project-paths.js.map +1 -1
- package/dist/runtime/protocol-init.js +7 -4
- package/dist/runtime/protocol-init.js.map +1 -1
- package/dist/runtime/repair-decision.js +14 -27
- package/dist/runtime/repair-decision.js.map +1 -1
- package/dist/runtime/repair-packet.js +8 -1
- package/dist/runtime/repair-packet.js.map +1 -1
- package/dist/runtime/scaffold/vue-template.d.ts +7 -0
- package/dist/runtime/scaffold/vue-template.js +167 -0
- package/dist/runtime/scaffold/vue-template.js.map +1 -0
- package/dist/runtime/scaffold.d.ts +21 -0
- package/dist/runtime/scaffold.js +80 -0
- package/dist/runtime/scaffold.js.map +1 -0
- package/dist/runtime/skills.js +3 -3
- package/dist/runtime/skills.js.map +1 -1
- package/dist/runtime/state.js +32 -16
- package/dist/runtime/state.js.map +1 -1
- package/dist/runtime/units.js +36 -1
- package/dist/runtime/units.js.map +1 -1
- package/dist/runtime/verification-commands.js +8 -4
- package/dist/runtime/verification-commands.js.map +1 -1
- package/dist/runtime/verify.js +76 -17
- package/dist/runtime/verify.js.map +1 -1
- package/dist/schemas/types.d.ts +64 -6
- package/dist/schemas/validation.js +4 -0
- package/dist/schemas/validation.js.map +1 -1
- package/dist/storage/json.js +6 -1
- package/dist/storage/json.js.map +1 -1
- package/package.json +3 -4
package/dist/runtime/plan.js
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
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 { inferPlanComponents, resolveComponentSource, resolvePlanComponents } from "./plan/component-resolver.js";
|
|
9
|
+
import { buildExecutionGuidance, renderGuidance } from "./plan/guidance.js";
|
|
10
|
+
import { readAgentPlanningProposal } from "./plan/proposal.js";
|
|
11
|
+
import { inferFrontendWorkflow, inferTaskIntent, verificationForWorkflow } from "./plan/workflow.js";
|
|
12
|
+
import { unitIdFromName } from "./common/naming.js";
|
|
10
13
|
export function createPlan(projectRoot, input) {
|
|
11
14
|
if (!input.taskText.trim()) {
|
|
12
15
|
throw new Error("plan requires a task");
|
|
13
16
|
}
|
|
14
17
|
const normalizedInput = normalizePlanInput(projectRoot, input);
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
+
const proposal = normalizedInput.agentProposal ? readAgentPlanningProposal(projectRoot, normalizedInput.agentProposal) : null;
|
|
19
|
+
const task = understandTask(projectRoot, normalizedInput, proposal);
|
|
20
|
+
const resolved = resolvePlan(projectRoot, task, proposal);
|
|
21
|
+
const components = resolved.components;
|
|
22
|
+
const units = buildExecutionUnits(components, task, proposal);
|
|
18
23
|
const componentGraph = buildComponentGraph(components, units);
|
|
19
24
|
const executionGuidance = buildExecutionGuidance();
|
|
20
|
-
const guidance = renderGuidance(task, components, units, executionGuidance);
|
|
25
|
+
const guidance = renderGuidance(task, components, units, executionGuidance, resolved.meta);
|
|
21
26
|
const output = {
|
|
22
27
|
task,
|
|
23
28
|
components,
|
|
@@ -25,7 +30,8 @@ export function createPlan(projectRoot, input) {
|
|
|
25
30
|
executionGuidance,
|
|
26
31
|
componentGraphPath: relativeHarnessPath("component-graph", "latest.json"),
|
|
27
32
|
guidancePath: relativeHarnessPath("guidance", "latest.md"),
|
|
28
|
-
statePath: relativeHarnessPath("state.json")
|
|
33
|
+
statePath: relativeHarnessPath("state.json"),
|
|
34
|
+
meta: resolved.meta
|
|
29
35
|
};
|
|
30
36
|
writeJson(harnessPath(projectRoot, "plans", "latest.json"), output);
|
|
31
37
|
writeJson(harnessPath(projectRoot, "execution-units", "latest.json"), {
|
|
@@ -36,19 +42,20 @@ export function createPlan(projectRoot, input) {
|
|
|
36
42
|
updatePlannedState(projectRoot, task, components, units);
|
|
37
43
|
return output;
|
|
38
44
|
}
|
|
39
|
-
export function
|
|
45
|
+
export function checkAgentPlanningProposal(projectRoot, file) {
|
|
40
46
|
if (!file) {
|
|
41
|
-
throw new Error("
|
|
47
|
+
throw new Error("proposal check requires --file <agent-proposal.json>");
|
|
42
48
|
}
|
|
43
49
|
const artifactPath = file;
|
|
44
50
|
try {
|
|
45
|
-
const relativePath = validateRequiredReferencedFile(projectRoot, file, "agent
|
|
46
|
-
const
|
|
51
|
+
const relativePath = validateRequiredReferencedFile(projectRoot, file, "agent proposal");
|
|
52
|
+
const proposal = readAgentPlanningProposal(projectRoot, relativePath);
|
|
53
|
+
validateProposalTargets(projectRoot, proposal);
|
|
47
54
|
return {
|
|
48
55
|
status: "passed",
|
|
49
56
|
artifactPath: relativePath,
|
|
50
|
-
contractVersion:
|
|
51
|
-
|
|
57
|
+
contractVersion: proposal.contractVersion ?? null,
|
|
58
|
+
proposal,
|
|
52
59
|
errors: []
|
|
53
60
|
};
|
|
54
61
|
}
|
|
@@ -57,7 +64,7 @@ export function checkAgentPlanningDecision(projectRoot, file) {
|
|
|
57
64
|
status: "failed",
|
|
58
65
|
artifactPath,
|
|
59
66
|
contractVersion: null,
|
|
60
|
-
|
|
67
|
+
proposal: null,
|
|
61
68
|
errors: [error instanceof Error ? error.message : String(error)]
|
|
62
69
|
};
|
|
63
70
|
}
|
|
@@ -67,7 +74,7 @@ function normalizePlanInput(projectRoot, input) {
|
|
|
67
74
|
taskText: input.taskText,
|
|
68
75
|
...optionalString("prd", validateReferencedFile(projectRoot, input.prd, "PRD")),
|
|
69
76
|
...optionalString("ui", validateReferencedFile(projectRoot, input.ui, "UI")),
|
|
70
|
-
...optionalString("
|
|
77
|
+
...optionalString("agentProposal", validateReferencedFile(projectRoot, input.agentProposal, "agent proposal"))
|
|
71
78
|
};
|
|
72
79
|
}
|
|
73
80
|
function validateRequiredReferencedFile(projectRoot, file, label) {
|
|
@@ -83,14 +90,11 @@ function validateReferencedFile(projectRoot, file, label) {
|
|
|
83
90
|
}
|
|
84
91
|
return relativePath;
|
|
85
92
|
}
|
|
86
|
-
function understandTask(projectRoot, input) {
|
|
93
|
+
function understandTask(projectRoot, input, proposal) {
|
|
87
94
|
const text = input.taskText.trim();
|
|
88
|
-
const decision = input.agentDecision
|
|
89
|
-
? readAgentPlanningDecision(projectRoot, input.agentDecision)
|
|
90
|
-
: null;
|
|
91
95
|
const scope = text;
|
|
92
|
-
const
|
|
93
|
-
const frontendWorkflow = inferFrontendWorkflow(scope, input);
|
|
96
|
+
const proposalConstraints = proposal?.constraintHints ?? [];
|
|
97
|
+
const frontendWorkflow = inferFrontendWorkflow(scope, input, proposal);
|
|
94
98
|
const intent = inferTaskIntent(scope, frontendWorkflow.kind);
|
|
95
99
|
return {
|
|
96
100
|
intent,
|
|
@@ -98,206 +102,118 @@ function understandTask(projectRoot, input) {
|
|
|
98
102
|
constraints: [
|
|
99
103
|
"Use the agent-invoked harness loop.",
|
|
100
104
|
"CLI owns project facts and verification.",
|
|
101
|
-
"System owns
|
|
105
|
+
"System owns final planning decisions; agent proposals are bounded inputs resolved through policy.",
|
|
102
106
|
...frontendWorkflow.implementationConstraints,
|
|
103
|
-
...
|
|
107
|
+
...proposalConstraints
|
|
104
108
|
],
|
|
105
109
|
frontendWorkflow,
|
|
106
110
|
policyProvenance: buildPolicyProvenance(projectRoot, scope, frontendWorkflow.kind),
|
|
107
111
|
inputs: {
|
|
108
112
|
...(input.prd ? { prd: input.prd } : {}),
|
|
109
113
|
...(input.ui ? { ui: input.ui } : {}),
|
|
110
|
-
...(input.
|
|
114
|
+
...(input.agentProposal ? { agentProposal: input.agentProposal } : {})
|
|
111
115
|
},
|
|
112
|
-
...buildAgentPlanningMetadata(input.
|
|
116
|
+
...buildAgentPlanningMetadata(input.agentProposal, proposal)
|
|
113
117
|
};
|
|
114
118
|
}
|
|
115
|
-
function
|
|
116
|
-
|
|
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) {
|
|
119
|
+
function buildAgentPlanningMetadata(proposalPath, proposal) {
|
|
120
|
+
if (!proposalPath) {
|
|
130
121
|
return {
|
|
131
122
|
reasoningMode: "deterministic"
|
|
132
123
|
};
|
|
133
124
|
}
|
|
134
125
|
return {
|
|
135
126
|
reasoningMode: "agent_assisted",
|
|
136
|
-
|
|
137
|
-
summary:
|
|
138
|
-
artifactPath:
|
|
139
|
-
contractVersion:
|
|
140
|
-
intentSuggestion:
|
|
141
|
-
...(
|
|
142
|
-
...(
|
|
127
|
+
agentProposal: {
|
|
128
|
+
summary: buildAgentProposalSummary(proposalPath),
|
|
129
|
+
artifactPath: proposalPath ?? null,
|
|
130
|
+
contractVersion: proposal?.contractVersion ?? null,
|
|
131
|
+
intentSuggestion: proposal?.intentSuggestion ?? null,
|
|
132
|
+
...(proposal?.constraintHints ? { constraintHints: proposal.constraintHints } : {}),
|
|
133
|
+
...(proposal?.componentHints ? { componentHints: proposal.componentHints } : {}),
|
|
134
|
+
...(proposal?.planningEvidence ? { planningEvidence: proposal.planningEvidence } : {})
|
|
143
135
|
}
|
|
144
136
|
};
|
|
145
137
|
}
|
|
146
|
-
function
|
|
147
|
-
return
|
|
148
|
-
? `Planning
|
|
149
|
-
: "Planning
|
|
150
|
-
}
|
|
151
|
-
function
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
138
|
+
function buildAgentProposalSummary(proposalPath) {
|
|
139
|
+
return proposalPath
|
|
140
|
+
? `Planning resolves structured agent proposal input from ${proposalPath}.`
|
|
141
|
+
: "Planning resolves structured agent proposal input.";
|
|
142
|
+
}
|
|
143
|
+
function resolvePlan(projectRoot, task, proposal) {
|
|
144
|
+
const evidenceTargets = validateProposalTargets(projectRoot, proposal);
|
|
145
|
+
const systemComponents = inferPlanComponents(projectRoot, task, evidenceTargets);
|
|
146
|
+
const warnings = [];
|
|
147
|
+
const resolvedComponents = resolvePlanComponents(projectRoot, systemComponents, proposal?.componentHints, task, warnings);
|
|
148
|
+
const components = resolvedComponents.components;
|
|
149
|
+
const evidenceWorkflowAccepted = Boolean(proposal?.planningEvidence?.workflowCandidates?.some((candidate) => {
|
|
150
|
+
return candidate.kind === task.frontendWorkflow?.kind && candidate.confidence >= 0.75;
|
|
151
|
+
}));
|
|
152
|
+
const componentSource = resolveComponentSource(resolvedComponents.trace);
|
|
153
|
+
return {
|
|
154
|
+
components,
|
|
155
|
+
meta: {
|
|
156
|
+
decisionTrace: {
|
|
157
|
+
workflow: {
|
|
158
|
+
source: evidenceWorkflowAccepted ? "agent" : "system",
|
|
159
|
+
reason: evidenceWorkflowAccepted
|
|
160
|
+
? `agent planningEvidence workflow accepted: ${task.frontendWorkflow?.kind}`
|
|
161
|
+
: `system classified workflow as ${task.frontendWorkflow?.kind ?? "unknown"}`
|
|
162
|
+
},
|
|
163
|
+
targets: {
|
|
164
|
+
source: evidenceTargets.length ? "merged" : "system",
|
|
165
|
+
reason: evidenceTargets.length
|
|
166
|
+
? "safe agent planningEvidence targetCandidates were merged into planning boundaries"
|
|
167
|
+
: "system inferred planning boundary"
|
|
168
|
+
},
|
|
169
|
+
components: {
|
|
170
|
+
source: componentSource,
|
|
171
|
+
reason: componentSource === "system"
|
|
172
|
+
? "system component boundaries retained"
|
|
173
|
+
: componentSource === "agent"
|
|
174
|
+
? "agent componentHints expanded the UI boundary"
|
|
175
|
+
: "agent componentHints merged into system UI boundary"
|
|
176
|
+
},
|
|
177
|
+
componentHints: resolvedComponents.trace,
|
|
178
|
+
...(proposal?.planningEvidence
|
|
179
|
+
? {
|
|
180
|
+
agentEvidence: {
|
|
181
|
+
source: "merged",
|
|
182
|
+
reason: describePlanningEvidence(proposal.planningEvidence)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
: {})
|
|
179
186
|
},
|
|
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"]
|
|
187
|
+
warnings
|
|
292
188
|
}
|
|
293
189
|
};
|
|
294
|
-
return policies[kind];
|
|
295
190
|
}
|
|
296
|
-
function
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
191
|
+
function validateProposalTargets(projectRoot, proposal) {
|
|
192
|
+
const evidenceTargets = (proposal?.planningEvidence?.targetCandidates ?? [])
|
|
193
|
+
.map((candidate, index) => ({
|
|
194
|
+
path: candidate.path,
|
|
195
|
+
confidence: candidate.confidence,
|
|
196
|
+
label: `agent proposal planningEvidence.targetCandidates[${index}].path`
|
|
197
|
+
}));
|
|
198
|
+
const safeTargets = evidenceTargets.map((target) => ({
|
|
199
|
+
path: resolveProjectRelativePath(projectRoot, target.path, target.label).relativePath,
|
|
200
|
+
confidence: target.confidence
|
|
201
|
+
}));
|
|
202
|
+
return [...new Set(safeTargets
|
|
203
|
+
.filter((target) => target.confidence >= 0.7)
|
|
204
|
+
.map((target) => target.path))];
|
|
205
|
+
}
|
|
206
|
+
function describePlanningEvidence(evidence) {
|
|
207
|
+
return [
|
|
208
|
+
`${evidence.workflowCandidates?.length ?? 0} workflow candidate(s)`,
|
|
209
|
+
`${evidence.targetCandidates?.length ?? 0} target candidate(s)`,
|
|
210
|
+
`${evidence.verificationCandidates?.length ?? 0} verification candidate(s)`,
|
|
211
|
+
`${evidence.ambiguities?.length ?? 0} ambiguity item(s)`
|
|
212
|
+
].join(", ");
|
|
213
|
+
}
|
|
214
|
+
function buildExecutionUnits(components, task, proposal) {
|
|
215
|
+
const verification = resolveVerificationFocus(task, proposal);
|
|
216
|
+
const units = components.map((component) => toUnit(component, task, verification));
|
|
301
217
|
const pageUnit = units.find((unit) => unit.id.endsWith("-page"));
|
|
302
218
|
if (pageUnit && units.length > 1) {
|
|
303
219
|
pageUnit.dependsOn = units
|
|
@@ -306,266 +222,25 @@ function buildExecutionUnits(components, task) {
|
|
|
306
222
|
}
|
|
307
223
|
return units;
|
|
308
224
|
}
|
|
309
|
-
function toUnit(component, task) {
|
|
225
|
+
function toUnit(component, task, verification) {
|
|
310
226
|
return {
|
|
311
227
|
id: unitIdFromName(component.name),
|
|
312
228
|
file: component.file,
|
|
313
229
|
task: `${task.intent}: ${task.scope}`,
|
|
314
|
-
verification
|
|
230
|
+
verification,
|
|
315
231
|
dependsOn: [],
|
|
316
232
|
parallelGroup: null
|
|
317
233
|
};
|
|
318
234
|
}
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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");
|
|
358
|
-
}
|
|
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");
|
|
373
|
-
return {
|
|
374
|
-
contractVersion: 1,
|
|
375
|
-
...decision
|
|
376
|
-
};
|
|
377
|
-
}
|
|
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
|
-
});
|
|
235
|
+
function resolveVerificationFocus(task, proposal) {
|
|
236
|
+
const base = verificationForWorkflow(task.frontendWorkflow?.kind);
|
|
237
|
+
const evidence = (proposal?.planningEvidence?.verificationCandidates ?? [])
|
|
238
|
+
.map((candidate) => candidate.kind.trim())
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.slice(0, 10);
|
|
241
|
+
return [...new Set([...base, ...evidence])];
|
|
429
242
|
}
|
|
430
243
|
function optionalString(key, value) {
|
|
431
244
|
return value === undefined ? {} : { [key]: value };
|
|
432
245
|
}
|
|
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
246
|
//# sourceMappingURL=plan.js.map
|