@shepai/cli 1.19.0 → 1.20.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/dist/eslint.config.mjs +3 -0
- package/dist/src/application/ports/output/agents/feature-agent-process.interface.d.ts +2 -1
- package/dist/src/application/ports/output/agents/feature-agent-process.interface.d.ts.map +1 -1
- package/dist/src/application/ports/output/agents/index.d.ts +1 -0
- package/dist/src/application/ports/output/agents/index.d.ts.map +1 -1
- package/dist/src/application/ports/output/agents/phase-timing-repository.interface.d.ts +44 -0
- package/dist/src/application/ports/output/agents/phase-timing-repository.interface.d.ts.map +1 -0
- package/dist/src/application/ports/output/agents/phase-timing-repository.interface.js +11 -0
- package/dist/src/application/use-cases/agents/approve-agent-run.use-case.d.ts.map +1 -1
- package/dist/src/application/use-cases/agents/approve-agent-run.use-case.js +6 -2
- package/dist/src/application/use-cases/agents/reject-agent-run.use-case.d.ts.map +1 -1
- package/dist/src/application/use-cases/agents/reject-agent-run.use-case.js +0 -1
- package/dist/src/application/use-cases/features/create-feature.use-case.d.ts +12 -2
- package/dist/src/application/use-cases/features/create-feature.use-case.d.ts.map +1 -1
- package/dist/src/application/use-cases/features/create-feature.use-case.js +45 -10
- package/dist/src/application/use-cases/features/resume-feature.use-case.js +2 -2
- package/dist/src/domain/generated/output.d.ts +40 -6
- package/dist/src/domain/generated/output.d.ts.map +1 -1
- package/dist/src/infrastructure/di/container.d.ts.map +1 -1
- package/dist/src/infrastructure/di/container.js +7 -0
- package/dist/src/infrastructure/persistence/sqlite/mappers/agent-run.mapper.d.ts +1 -2
- package/dist/src/infrastructure/persistence/sqlite/mappers/agent-run.mapper.d.ts.map +1 -1
- package/dist/src/infrastructure/persistence/sqlite/mappers/agent-run.mapper.js +4 -4
- package/dist/src/infrastructure/persistence/sqlite/migrations.d.ts.map +1 -1
- package/dist/src/infrastructure/persistence/sqlite/migrations.js +31 -0
- package/dist/src/infrastructure/repositories/agent-run.repository.d.ts.map +1 -1
- package/dist/src/infrastructure/repositories/agent-run.repository.js +5 -9
- package/dist/src/infrastructure/repositories/sqlite-phase-timing.repository.d.ts +21 -0
- package/dist/src/infrastructure/repositories/sqlite-phase-timing.repository.d.ts.map +1 -0
- package/dist/src/infrastructure/repositories/sqlite-phase-timing.repository.js +100 -0
- package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts +305 -13
- package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts.map +1 -1
- package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-graph.js +124 -7
- package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.d.ts +2 -1
- package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.d.ts.map +1 -1
- package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.js +2 -2
- package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-worker.d.ts +2 -1
- package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-worker.d.ts.map +1 -1
- package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-worker.js +19 -7
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/implement.node.js +1 -1
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.d.ts +11 -2
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.d.ts.map +1 -1
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.js +46 -15
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/repair.node.d.ts +23 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/repair.node.d.ts.map +1 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/repair.node.js +77 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/plan.schema.d.ts +4 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/plan.schema.d.ts.map +1 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/plan.schema.js +54 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/research.schema.d.ts +3 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/research.schema.d.ts.map +1 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/research.schema.js +24 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/spec.schema.d.ts +4 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/spec.schema.d.ts.map +1 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/spec.schema.js +50 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/validation.d.ts +8 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/validation.d.ts.map +1 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/validation.js +36 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/validate.node.d.ts +12 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/validate.node.d.ts.map +1 -0
- package/dist/src/infrastructure/services/agents/feature-agent/nodes/validate.node.js +58 -0
- package/dist/src/infrastructure/services/agents/feature-agent/phase-timing-context.d.ts +30 -0
- package/dist/src/infrastructure/services/agents/feature-agent/phase-timing-context.d.ts.map +1 -0
- package/dist/src/infrastructure/services/agents/feature-agent/phase-timing-context.js +68 -0
- package/dist/src/infrastructure/services/agents/feature-agent/state.d.ts +5 -1
- package/dist/src/infrastructure/services/agents/feature-agent/state.d.ts.map +1 -1
- package/dist/src/infrastructure/services/agents/feature-agent/state.js +14 -1
- package/dist/src/presentation/cli/commands/feat/approve.command.d.ts +12 -0
- package/dist/src/presentation/cli/commands/feat/approve.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/feat/approve.command.js +48 -0
- package/dist/src/presentation/cli/commands/feat/index.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/feat/index.js +7 -1
- package/dist/src/presentation/cli/commands/feat/ls.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/feat/ls.command.js +13 -5
- package/dist/src/presentation/cli/commands/feat/new.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/feat/new.command.js +34 -15
- package/dist/src/presentation/cli/commands/feat/reject.command.d.ts +12 -0
- package/dist/src/presentation/cli/commands/feat/reject.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/feat/reject.command.js +50 -0
- package/dist/src/presentation/cli/commands/feat/resolve-waiting-feature.d.ts +30 -0
- package/dist/src/presentation/cli/commands/feat/resolve-waiting-feature.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/feat/resolve-waiting-feature.js +57 -0
- package/dist/src/presentation/cli/commands/feat/review.command.d.ts +12 -0
- package/dist/src/presentation/cli/commands/feat/review.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/feat/review.command.js +49 -0
- package/dist/src/presentation/cli/commands/feat/show.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/feat/show.command.js +51 -6
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/cache/.previewinfo +1 -1
- package/web/.next/cache/.rscinfo +1 -1
- package/web/.next/cache/config.json +3 -3
- package/web/.next/fallback-build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +3 -3
- package/web/.next/required-server-files.js +1 -1
- package/web/.next/required-server-files.json +1 -1
- package/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/_not-found.html +2 -2
- package/web/.next/server/app/_not-found.rsc +1 -1
- package/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/index.html +2 -2
- package/web/.next/server/app/index.rsc +1 -1
- package/web/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/version.html +2 -2
- package/web/.next/server/app/version.rsc +1 -1
- package/web/.next/server/app/version.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/version.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/version.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/version.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/version.segments/version/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/version.segments/version.segment.rsc +1 -1
- package/web/.next/server/chunks/ssr/_19be0743._.js +1 -1
- package/web/.next/server/pages/404.html +2 -2
- package/web/.next/server/pages/500.html +2 -2
- package/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/trace +1 -1
- package/web/.next/trace-build +1 -1
- /package/web/.next/static/{nw4IT0rr_mE5Tx22X9OgO → u-tyZUMnsIne6K_0rPmPO}/_buildManifest.js +0 -0
- /package/web/.next/static/{nw4IT0rr_mE5Tx22X9OgO → u-tyZUMnsIne6K_0rPmPO}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{nw4IT0rr_mE5Tx22X9OgO → u-tyZUMnsIne6K_0rPmPO}/_ssgManifest.js +0 -0
|
@@ -5,15 +5,112 @@ import { createRequirementsNode } from './nodes/requirements.node.js';
|
|
|
5
5
|
import { createResearchNode } from './nodes/research.node.js';
|
|
6
6
|
import { createPlanNode } from './nodes/plan.node.js';
|
|
7
7
|
import { createImplementNode } from './nodes/implement.node.js';
|
|
8
|
+
import { createValidateNode } from './nodes/validate.node.js';
|
|
9
|
+
import { createRepairNode } from './nodes/repair.node.js';
|
|
10
|
+
import { validateSpecAnalyze, validateSpecRequirements } from './nodes/schemas/spec.schema.js';
|
|
11
|
+
import { validateResearch } from './nodes/schemas/research.schema.js';
|
|
12
|
+
import { validatePlan, validateTasks } from './nodes/schemas/plan.schema.js';
|
|
13
|
+
import { readSpecFile, safeYamlLoad, createNodeLogger } from './nodes/node-helpers.js';
|
|
8
14
|
// Re-export state types for consumers
|
|
9
15
|
export { FeatureAgentAnnotation } from './state.js';
|
|
16
|
+
/**
|
|
17
|
+
* Factory that creates a conditional edge function for validation routing.
|
|
18
|
+
*
|
|
19
|
+
* Routes to successNode on pass, repairNode on fail, throws after maxRetries.
|
|
20
|
+
*/
|
|
21
|
+
function routeValidation(successNode, repairNode, maxRetries = 3) {
|
|
22
|
+
return (state) => {
|
|
23
|
+
if (state.lastValidationErrors.length === 0)
|
|
24
|
+
return successNode;
|
|
25
|
+
if (state.validationRetries >= maxRetries) {
|
|
26
|
+
throw new Error(`Validation failed after ${maxRetries} repair attempts for '${state.lastValidationTarget}': ${state.lastValidationErrors.join('; ')}`);
|
|
27
|
+
}
|
|
28
|
+
return repairNode;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates a combined plan+tasks validator node.
|
|
33
|
+
*
|
|
34
|
+
* Validates plan.yaml first, then tasks.yaml with cross-referencing
|
|
35
|
+
* of phaseIds from the plan. Combines all errors.
|
|
36
|
+
*/
|
|
37
|
+
function createPlanTasksValidator() {
|
|
38
|
+
const log = createNodeLogger('validate:plan+tasks');
|
|
39
|
+
return async (state) => {
|
|
40
|
+
log.info('Validating plan.yaml and tasks.yaml');
|
|
41
|
+
const allErrors = [];
|
|
42
|
+
// Validate plan.yaml
|
|
43
|
+
const planContent = readSpecFile(state.specDir, 'plan.yaml');
|
|
44
|
+
if (!planContent) {
|
|
45
|
+
allErrors.push("File 'plan.yaml' not found or empty");
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
try {
|
|
49
|
+
const planData = safeYamlLoad(planContent);
|
|
50
|
+
const planResult = validatePlan(planData);
|
|
51
|
+
allErrors.push(...planResult.errors);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55
|
+
allErrors.push(`YAML parse error in plan.yaml: ${msg}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Validate tasks.yaml with cross-reference
|
|
59
|
+
const tasksContent = readSpecFile(state.specDir, 'tasks.yaml');
|
|
60
|
+
if (!tasksContent) {
|
|
61
|
+
allErrors.push("File 'tasks.yaml' not found or empty");
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
try {
|
|
65
|
+
const tasksData = safeYamlLoad(tasksContent);
|
|
66
|
+
// Extract phaseIds from plan for cross-validation
|
|
67
|
+
let phaseIds = [];
|
|
68
|
+
if (planContent) {
|
|
69
|
+
try {
|
|
70
|
+
const planData = safeYamlLoad(planContent);
|
|
71
|
+
const phases = Array.isArray(planData?.phases) ? planData.phases : [];
|
|
72
|
+
phaseIds = phases
|
|
73
|
+
.filter((p) => typeof p?.id === 'string')
|
|
74
|
+
.map((p) => p.id);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Plan parse already reported above
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const tasksResult = validateTasks(tasksData, phaseIds);
|
|
81
|
+
allErrors.push(...tasksResult.errors);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
85
|
+
allErrors.push(`YAML parse error in tasks.yaml: ${msg}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (allErrors.length === 0) {
|
|
89
|
+
log.info('Validation passed');
|
|
90
|
+
return {
|
|
91
|
+
lastValidationTarget: 'plan.yaml+tasks.yaml',
|
|
92
|
+
lastValidationErrors: [],
|
|
93
|
+
validationRetries: 0,
|
|
94
|
+
messages: ['[validate:plan+tasks] PASS'],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
log.error(`Validation failed: ${allErrors.join('; ')}`);
|
|
98
|
+
return {
|
|
99
|
+
lastValidationTarget: 'plan.yaml+tasks.yaml',
|
|
100
|
+
lastValidationErrors: allErrors,
|
|
101
|
+
validationRetries: state.validationRetries + 1,
|
|
102
|
+
messages: [`[validate:plan+tasks] FAIL: ${allErrors.length} error(s)`],
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
}
|
|
10
106
|
/**
|
|
11
107
|
* Factory function that creates and compiles the feature-agent LangGraph.
|
|
12
108
|
*
|
|
13
|
-
* The graph defines a linear SDLC workflow:
|
|
14
|
-
* analyze → requirements → research → plan → implement
|
|
109
|
+
* The graph defines a linear SDLC workflow with validation gates:
|
|
110
|
+
* analyze → validate → requirements → validate → research → validate → plan → validate → implement
|
|
15
111
|
*
|
|
16
|
-
* Each node
|
|
112
|
+
* Each YAML-producing node is followed by a validate/repair loop that ensures
|
|
113
|
+
* the output YAML conforms to its schema before proceeding.
|
|
17
114
|
*
|
|
18
115
|
* @param executor - The agent executor to delegate prompt execution to
|
|
19
116
|
* @param checkpointer - Optional checkpoint saver for state persistence
|
|
@@ -21,16 +118,36 @@ export { FeatureAgentAnnotation } from './state.js';
|
|
|
21
118
|
*/
|
|
22
119
|
export function createFeatureAgentGraph(executor, checkpointer) {
|
|
23
120
|
const graph = new StateGraph(FeatureAgentAnnotation)
|
|
121
|
+
// --- Producer nodes ---
|
|
24
122
|
.addNode('analyze', createAnalyzeNode(executor))
|
|
25
123
|
.addNode('requirements', createRequirementsNode(executor))
|
|
26
124
|
.addNode('research', createResearchNode(executor))
|
|
27
125
|
.addNode('plan', createPlanNode(executor))
|
|
28
126
|
.addNode('implement', createImplementNode(executor))
|
|
127
|
+
// --- Validate nodes ---
|
|
128
|
+
.addNode('validate_spec_analyze', createValidateNode('spec.yaml', validateSpecAnalyze))
|
|
129
|
+
.addNode('validate_spec_requirements', createValidateNode('spec.yaml', validateSpecRequirements))
|
|
130
|
+
.addNode('validate_research', createValidateNode('research.yaml', validateResearch))
|
|
131
|
+
.addNode('validate_plan_tasks', createPlanTasksValidator())
|
|
132
|
+
// --- Repair nodes ---
|
|
133
|
+
.addNode('repair_spec_analyze', createRepairNode('spec.yaml', executor))
|
|
134
|
+
.addNode('repair_spec_requirements', createRepairNode('spec.yaml', executor))
|
|
135
|
+
.addNode('repair_research', createRepairNode('research.yaml', executor))
|
|
136
|
+
.addNode('repair_plan_tasks', createRepairNode(['plan.yaml', 'tasks.yaml'], executor))
|
|
137
|
+
// --- Edges: linear flow with validation gates ---
|
|
29
138
|
.addEdge(START, 'analyze')
|
|
30
|
-
.addEdge('analyze', '
|
|
31
|
-
.
|
|
32
|
-
.addEdge('
|
|
33
|
-
.addEdge('
|
|
139
|
+
.addEdge('analyze', 'validate_spec_analyze')
|
|
140
|
+
.addConditionalEdges('validate_spec_analyze', routeValidation('requirements', 'repair_spec_analyze'))
|
|
141
|
+
.addEdge('repair_spec_analyze', 'validate_spec_analyze')
|
|
142
|
+
.addEdge('requirements', 'validate_spec_requirements')
|
|
143
|
+
.addConditionalEdges('validate_spec_requirements', routeValidation('research', 'repair_spec_requirements'))
|
|
144
|
+
.addEdge('repair_spec_requirements', 'validate_spec_requirements')
|
|
145
|
+
.addEdge('research', 'validate_research')
|
|
146
|
+
.addConditionalEdges('validate_research', routeValidation('plan', 'repair_research'))
|
|
147
|
+
.addEdge('repair_research', 'validate_research')
|
|
148
|
+
.addEdge('plan', 'validate_plan_tasks')
|
|
149
|
+
.addConditionalEdges('validate_plan_tasks', routeValidation('implement', 'repair_plan_tasks'))
|
|
150
|
+
.addEdge('repair_plan_tasks', 'validate_plan_tasks')
|
|
34
151
|
.addEdge('implement', END);
|
|
35
152
|
return graph.compile({ checkpointer });
|
|
36
153
|
}
|
package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.d.ts
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { IFeatureAgentProcessService } from '../../../../application/ports/output/agents/feature-agent-process.interface.js';
|
|
9
9
|
import type { IAgentRunRepository } from '../../../../application/ports/output/agents/agent-run-repository.interface.js';
|
|
10
|
+
import { type ApprovalGates } from '../../../../domain/generated/output.js';
|
|
10
11
|
export declare class FeatureAgentProcessService implements IFeatureAgentProcessService {
|
|
11
12
|
private readonly runRepository;
|
|
12
13
|
constructor(runRepository: IAgentRunRepository);
|
|
13
14
|
spawn(featureId: string, runId: string, repoPath: string, specDir: string, worktreePath?: string, options?: {
|
|
14
|
-
|
|
15
|
+
approvalGates?: ApprovalGates;
|
|
15
16
|
resume?: boolean;
|
|
16
17
|
threadId?: string;
|
|
17
18
|
resumeFromInterrupt?: boolean;
|
package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feature-agent-process.service.d.ts","sourceRoot":"","sources":["../../../../../../src/infrastructure/services/agents/feature-agent/feature-agent-process.service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,sEAAsE,CAAC;AACxH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qEAAqE,CAAC;
|
|
1
|
+
{"version":3,"file":"feature-agent-process.service.d.ts","sourceRoot":"","sources":["../../../../../../src/infrastructure/services/agents/feature-agent/feature-agent-process.service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,sEAAsE,CAAC;AACxH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qEAAqE,CAAC;AAC/G,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAYlF,qBAAa,0BAA2B,YAAW,2BAA2B;IAChE,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAAb,aAAa,EAAE,mBAAmB;IAE/D,KAAK,CACH,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE;QACR,aAAa,CAAC,EAAE,aAAa,CAAC;QAC9B,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;KAC/B,GACA,MAAM;IAkDT,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IASvB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAexD"}
|
package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.js
CHANGED
|
@@ -41,8 +41,8 @@ export class FeatureAgentProcessService {
|
|
|
41
41
|
if (worktreePath) {
|
|
42
42
|
args.push('--worktree-path', worktreePath);
|
|
43
43
|
}
|
|
44
|
-
if (options?.
|
|
45
|
-
args.push('--approval-
|
|
44
|
+
if (options?.approvalGates) {
|
|
45
|
+
args.push('--approval-gates', JSON.stringify(options.approvalGates));
|
|
46
46
|
}
|
|
47
47
|
if (options?.threadId) {
|
|
48
48
|
args.push('--thread-id', options.threadId);
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* CLI Args: --feature-id <id> --run-id <id> --repo <path> --spec-dir <path>
|
|
10
10
|
*/
|
|
11
11
|
import 'reflect-metadata';
|
|
12
|
+
import type { ApprovalGates } from '../../../../domain/generated/output.js';
|
|
12
13
|
export interface WorkerArgs {
|
|
13
14
|
featureId: string;
|
|
14
15
|
runId: string;
|
|
15
16
|
repo: string;
|
|
16
17
|
specDir: string;
|
|
17
18
|
worktreePath?: string;
|
|
18
|
-
|
|
19
|
+
approvalGates?: ApprovalGates;
|
|
19
20
|
resume?: boolean;
|
|
20
21
|
threadId?: string;
|
|
21
22
|
resumeFromInterrupt?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feature-agent-worker.d.ts","sourceRoot":"","sources":["../../../../../../src/infrastructure/services/agents/feature-agent/feature-agent-worker.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAEH,OAAO,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"feature-agent-worker.d.ts","sourceRoot":"","sources":["../../../../../../src/infrastructure/services/agents/feature-agent/feature-agent-worker.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAEH,OAAO,kBAAkB,CAAC;AAgB1B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA0C1D;AAgCD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAsI/D"}
|
|
@@ -19,6 +19,7 @@ import { AgentRunStatus } from '../../../../domain/generated/output.js';
|
|
|
19
19
|
import { initializeSettings } from '../../../../infrastructure/services/settings.service.js';
|
|
20
20
|
import { InitializeSettingsUseCase } from '../../../../application/use-cases/settings/initialize-settings.use-case.js';
|
|
21
21
|
import { setHeartbeatContext } from './heartbeat.js';
|
|
22
|
+
import { setPhaseTimingContext } from './phase-timing-context.js';
|
|
22
23
|
/**
|
|
23
24
|
* Parse CLI arguments into a WorkerArgs object.
|
|
24
25
|
* Throws if any required argument is missing.
|
|
@@ -33,8 +34,17 @@ export function parseWorkerArgs(args) {
|
|
|
33
34
|
};
|
|
34
35
|
const worktreeIdx = args.indexOf('--worktree-path');
|
|
35
36
|
const worktreePath = worktreeIdx !== -1 && worktreeIdx + 1 < args.length ? args[worktreeIdx + 1] : undefined;
|
|
36
|
-
const approvalIdx = args.indexOf('--approval-
|
|
37
|
-
|
|
37
|
+
const approvalIdx = args.indexOf('--approval-gates');
|
|
38
|
+
let approvalGates;
|
|
39
|
+
if (approvalIdx !== -1 && approvalIdx + 1 < args.length) {
|
|
40
|
+
try {
|
|
41
|
+
approvalGates = JSON.parse(args[approvalIdx + 1]);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Fall back to no gates if JSON parse fails
|
|
45
|
+
approvalGates = undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
38
48
|
const resume = args.includes('--resume');
|
|
39
49
|
const resumeFromInterrupt = args.includes('--resume-from-interrupt');
|
|
40
50
|
const threadIdx = args.indexOf('--thread-id');
|
|
@@ -45,7 +55,7 @@ export function parseWorkerArgs(args) {
|
|
|
45
55
|
repo: getArg('repo'),
|
|
46
56
|
specDir: getArg('spec-dir'),
|
|
47
57
|
worktreePath,
|
|
48
|
-
|
|
58
|
+
approvalGates,
|
|
49
59
|
resume,
|
|
50
60
|
threadId,
|
|
51
61
|
resumeFromInterrupt,
|
|
@@ -87,7 +97,7 @@ export async function runWorker(args) {
|
|
|
87
97
|
log(` repo=${args.repo}`);
|
|
88
98
|
log(` specDir=${args.specDir}`);
|
|
89
99
|
log(` worktreePath=${args.worktreePath ?? '(none)'}`);
|
|
90
|
-
log(`
|
|
100
|
+
log(` approvalGates=${args.approvalGates ? JSON.stringify(args.approvalGates) : '(none)'}`);
|
|
91
101
|
log(` resume=${args.resume}`);
|
|
92
102
|
log('Initializing container...');
|
|
93
103
|
await initializeContainer();
|
|
@@ -121,6 +131,9 @@ export async function runWorker(args) {
|
|
|
121
131
|
const stopHeartbeat = startHeartbeat(args.runId, runRepository);
|
|
122
132
|
// Set heartbeat context so node-helpers can update current node
|
|
123
133
|
setHeartbeatContext(args.runId, runRepository);
|
|
134
|
+
// Set phase timing context so executeNode() records per-phase durations
|
|
135
|
+
const timingRepository = container.resolve('IPhaseTimingRepository');
|
|
136
|
+
setPhaseTimingContext(args.runId, timingRepository);
|
|
124
137
|
try {
|
|
125
138
|
const graphConfig = { configurable: { thread_id: checkpointId } };
|
|
126
139
|
let result;
|
|
@@ -139,7 +152,7 @@ export async function runWorker(args) {
|
|
|
139
152
|
worktreePath: args.worktreePath ?? args.repo,
|
|
140
153
|
specDir: args.specDir,
|
|
141
154
|
error: undefined, // Clear previous error state
|
|
142
|
-
...(args.
|
|
155
|
+
...(args.approvalGates ? { approvalGates: args.approvalGates } : {}),
|
|
143
156
|
}, graphConfig);
|
|
144
157
|
}
|
|
145
158
|
else {
|
|
@@ -149,7 +162,7 @@ export async function runWorker(args) {
|
|
|
149
162
|
repositoryPath: args.repo,
|
|
150
163
|
worktreePath: args.worktreePath ?? args.repo,
|
|
151
164
|
specDir: args.specDir,
|
|
152
|
-
...(args.
|
|
165
|
+
...(args.approvalGates ? { approvalGates: args.approvalGates } : {}),
|
|
153
166
|
}, graphConfig);
|
|
154
167
|
}
|
|
155
168
|
log(`Graph invocation completed. Error: ${result.error ?? 'none'}`);
|
|
@@ -159,7 +172,6 @@ export async function runWorker(args) {
|
|
|
159
172
|
if (interruptPayload && interruptPayload.length > 0) {
|
|
160
173
|
const now = new Date();
|
|
161
174
|
await runRepository.updateStatus(args.runId, AgentRunStatus.waitingApproval, {
|
|
162
|
-
approvalStatus: 'waiting',
|
|
163
175
|
updatedAt: now,
|
|
164
176
|
});
|
|
165
177
|
log('Run paused — waiting for human approval');
|
|
@@ -132,7 +132,7 @@ export function createImplementNode(executor) {
|
|
|
132
132
|
log.info(`All phases complete — ${completedTasks}/${totalTasks} tasks (${elapsed}s)`);
|
|
133
133
|
updateFeatureProgress(state.specDir, totalTasks, totalTasks, 'implementation-complete', null, log);
|
|
134
134
|
messages.push(`[implement] Complete: ${totalTasks} tasks across ${totalPhases} phases (${elapsed}s)`);
|
|
135
|
-
if (shouldInterrupt('implement', state.
|
|
135
|
+
if (shouldInterrupt('implement', state.approvalGates)) {
|
|
136
136
|
log.info('Interrupting for human approval');
|
|
137
137
|
interrupt({
|
|
138
138
|
node: 'implement',
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* and error handling so every node follows the same patterns.
|
|
6
6
|
*/
|
|
7
7
|
import type { IAgentExecutor, AgentExecutionOptions, AgentExecutionResult } from '../../../../../application/ports/output/agents/agent-executor.interface.js';
|
|
8
|
+
import type { ApprovalGates } from '../../../../../domain/generated/output.js';
|
|
8
9
|
import type { FeatureAgentState } from '../state.js';
|
|
9
10
|
/**
|
|
10
11
|
* Create a scoped logger that prefixes messages with the node name.
|
|
@@ -31,9 +32,17 @@ export declare function buildExecutorOptions(state: FeatureAgentState): AgentExe
|
|
|
31
32
|
export declare function safeYamlLoad(content: string): unknown;
|
|
32
33
|
/**
|
|
33
34
|
* Determine whether the current node should trigger an interrupt
|
|
34
|
-
* for human approval given the configured approval
|
|
35
|
+
* for human approval given the configured approval gates.
|
|
36
|
+
*
|
|
37
|
+
* Gates control which phases auto-approve:
|
|
38
|
+
* - allowPrd: when true, skip interrupt after requirements phase
|
|
39
|
+
* - allowPlan: when true, skip interrupt after plan phase
|
|
40
|
+
*
|
|
41
|
+
* Nodes not covered by a gate (analyze, research) never interrupt.
|
|
42
|
+
* The implement node always interrupts when gates are present
|
|
43
|
+
* (unless both gates are true, meaning fully autonomous).
|
|
35
44
|
*/
|
|
36
|
-
export declare function shouldInterrupt(nodeName: string,
|
|
45
|
+
export declare function shouldInterrupt(nodeName: string, gates: ApprovalGates | undefined): boolean;
|
|
37
46
|
export type ErrorCategory = 'retryable-api' | 'retryable-network' | 'non-retryable' | 'unknown';
|
|
38
47
|
/**
|
|
39
48
|
* Classify an error message into a retry category.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-helpers.d.ts","sourceRoot":"","sources":["../../../../../../../src/infrastructure/services/agents/feature-agent/nodes/node-helpers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,+DAA+D,CAAC;AACvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"node-helpers.d.ts","sourceRoot":"","sources":["../../../../../../../src/infrastructure/services/agents/feature-agent/nodes/node-helpers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,+DAA+D,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIrD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM;kBAE/B,MAAM,GAAG,IAAI;mBAIZ,MAAM,GAAG,IAAI;EAK/B;AAED,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE7D;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMtE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,iBAAiB,GAAG,qBAAqB,CAMpF;AAeD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQrD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,SAAS,GAAG,OAAO,CAO3F;AAMD,MAAM,MAAM,aAAa,GAAG,eAAe,GAAG,mBAAmB,GAAG,eAAe,GAAG,SAAS,CAAC;AAMhG;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,aAAa,CAKjE;AAED,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,qBAAqB,EAC9B,SAAS,CAAC,EAAE,YAAY,GACvB,OAAO,CAAC,oBAAoB,CAAC,CA6B/B;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAW5D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,GAAG,IAAI,CAwB1F;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,UAAU,KAAK,MAAM,GACjE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CA4EnE"}
|
|
@@ -9,6 +9,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { interrupt, isGraphBubbleUp } from '@langchain/langgraph';
|
|
11
11
|
import { reportNodeStart } from '../heartbeat.js';
|
|
12
|
+
import { recordPhaseStart, recordPhaseEnd } from '../phase-timing-context.js';
|
|
12
13
|
/**
|
|
13
14
|
* Create a scoped logger that prefixes messages with the node name.
|
|
14
15
|
* Output goes to stdout which the worker redirects to the log file.
|
|
@@ -70,22 +71,30 @@ export function safeYamlLoad(content) {
|
|
|
70
71
|
return yaml.load(sanitized);
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
|
-
/** Nodes that are auto-approved (no interrupt) for each approval mode. */
|
|
74
|
-
const AUTO_APPROVED_NODES = {
|
|
75
|
-
interactive: [],
|
|
76
|
-
'allow-prd': ['analyze', 'requirements'],
|
|
77
|
-
'allow-plan': ['analyze', 'requirements', 'research', 'plan'],
|
|
78
|
-
'allow-all': ['analyze', 'requirements', 'research', 'plan', 'implement'],
|
|
79
|
-
};
|
|
80
74
|
/**
|
|
81
75
|
* Determine whether the current node should trigger an interrupt
|
|
82
|
-
* for human approval given the configured approval
|
|
76
|
+
* for human approval given the configured approval gates.
|
|
77
|
+
*
|
|
78
|
+
* Gates control which phases auto-approve:
|
|
79
|
+
* - allowPrd: when true, skip interrupt after requirements phase
|
|
80
|
+
* - allowPlan: when true, skip interrupt after plan phase
|
|
81
|
+
*
|
|
82
|
+
* Nodes not covered by a gate (analyze, research) never interrupt.
|
|
83
|
+
* The implement node always interrupts when gates are present
|
|
84
|
+
* (unless both gates are true, meaning fully autonomous).
|
|
83
85
|
*/
|
|
84
|
-
export function shouldInterrupt(nodeName,
|
|
85
|
-
if (!
|
|
86
|
+
export function shouldInterrupt(nodeName, gates) {
|
|
87
|
+
if (!gates)
|
|
88
|
+
return false;
|
|
89
|
+
if (gates.allowPrd && gates.allowPlan)
|
|
86
90
|
return false;
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
if (nodeName === 'requirements')
|
|
92
|
+
return !gates.allowPrd;
|
|
93
|
+
if (nodeName === 'plan')
|
|
94
|
+
return !gates.allowPlan;
|
|
95
|
+
if (nodeName === 'implement')
|
|
96
|
+
return true;
|
|
97
|
+
return false;
|
|
89
98
|
}
|
|
90
99
|
const API_ERROR_RE = /API Error: (400|429|5\d{2})/;
|
|
91
100
|
const NETWORK_ERROR_RE = /ECONNREFUSED|ETIMEDOUT|ENOTFOUND|timed out/i;
|
|
@@ -193,21 +202,41 @@ export function executeNode(nodeName, executor, buildPrompt) {
|
|
|
193
202
|
return async (state) => {
|
|
194
203
|
log.info('Starting...');
|
|
195
204
|
reportNodeStart(nodeName);
|
|
205
|
+
// On resume from interrupt, LangGraph re-executes the node function from
|
|
206
|
+
// the top. Skip the expensive executor call if this phase already completed
|
|
207
|
+
// (markPhaseComplete is called before interrupt, so completed = done but
|
|
208
|
+
// awaiting approval; the approval has now been granted).
|
|
209
|
+
const completedPhases = getCompletedPhases(state.specDir);
|
|
210
|
+
if (completedPhases.includes(nodeName)) {
|
|
211
|
+
log.info('Phase already completed (resuming from approval), skipping execution');
|
|
212
|
+
return {
|
|
213
|
+
currentNode: nodeName,
|
|
214
|
+
messages: [`[${nodeName}] Approved — continuing`],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
196
217
|
const startTime = Date.now();
|
|
218
|
+
// Record phase start (no-op if timing context not set)
|
|
219
|
+
const timingId = await recordPhaseStart(nodeName);
|
|
197
220
|
try {
|
|
198
221
|
const prompt = buildPrompt(state, log);
|
|
199
222
|
const options = buildExecutorOptions(state);
|
|
200
223
|
log.info(`Executing agent at cwd=${options.cwd}`);
|
|
201
224
|
log.info(`Prompt length: ${prompt.length} chars`);
|
|
202
225
|
const result = await executor.execute(prompt, options);
|
|
203
|
-
const
|
|
226
|
+
const durationMs = Date.now() - startTime;
|
|
227
|
+
const elapsed = (durationMs / 1000).toFixed(1);
|
|
204
228
|
log.info(`Complete (${result.result.length} chars, ${elapsed}s)`);
|
|
229
|
+
// Record phase completion
|
|
230
|
+
await recordPhaseEnd(timingId, durationMs);
|
|
231
|
+
// Mark phase complete BEFORE interrupting so that on resume the
|
|
232
|
+
// node detects the work is already done and skips re-execution.
|
|
233
|
+
markPhaseComplete(state.specDir, nodeName, log);
|
|
205
234
|
const nodeResult = {
|
|
206
235
|
currentNode: nodeName,
|
|
207
236
|
messages: [`[${nodeName}] Complete (${result.result.length} chars, ${elapsed}s)`],
|
|
208
237
|
};
|
|
209
238
|
// Human-in-the-loop: interrupt after node execution for review
|
|
210
|
-
if (shouldInterrupt(nodeName, state.
|
|
239
|
+
if (shouldInterrupt(nodeName, state.approvalGates)) {
|
|
211
240
|
log.info('Interrupting for human approval');
|
|
212
241
|
interrupt({
|
|
213
242
|
node: nodeName,
|
|
@@ -222,8 +251,10 @@ export function executeNode(nodeName, executor, buildPrompt) {
|
|
|
222
251
|
if (isGraphBubbleUp(err))
|
|
223
252
|
throw err;
|
|
224
253
|
const message = err instanceof Error ? err.message : String(err);
|
|
225
|
-
const
|
|
254
|
+
const durationMs = Date.now() - startTime;
|
|
255
|
+
const elapsed = (durationMs / 1000).toFixed(1);
|
|
226
256
|
log.error(`${message} (after ${elapsed}s)`);
|
|
257
|
+
// Leave timingId without completedAt (partial timing for failed nodes)
|
|
227
258
|
// Throw so LangGraph does NOT checkpoint this node as "completed".
|
|
228
259
|
// The worker catch block marks the run as failed, and on resume
|
|
229
260
|
// LangGraph re-executes from the last successfully checkpointed node.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repair node factory for the feature-agent graph.
|
|
3
|
+
*
|
|
4
|
+
* Reads broken YAML content and validation errors from state,
|
|
5
|
+
* builds a constrained repair prompt, and sends it to the executor
|
|
6
|
+
* with limited tools (write-only) to fix the YAML files.
|
|
7
|
+
*/
|
|
8
|
+
import type { FeatureAgentState } from '../state.js';
|
|
9
|
+
import type { IAgentExecutor } from '../../../../../application/ports/output/agents/agent-executor.interface.js';
|
|
10
|
+
/**
|
|
11
|
+
* Build a repair prompt containing the broken YAML, validation errors,
|
|
12
|
+
* and file paths for the executor to write fixed files.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildRepairPrompt(filename: string | string[], content: string, errors: string[], specDir: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Factory that creates a repair node for a specific YAML file (or files).
|
|
17
|
+
*
|
|
18
|
+
* Reads the broken YAML, builds a repair prompt with validation errors
|
|
19
|
+
* from state, and calls the executor with constrained options (maxTurns=5,
|
|
20
|
+
* write-only tools, no MCP).
|
|
21
|
+
*/
|
|
22
|
+
export declare function createRepairNode(filename: string | string[], executor: IAgentExecutor): (state: FeatureAgentState) => Promise<Partial<FeatureAgentState>>;
|
|
23
|
+
//# sourceMappingURL=repair.node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repair.node.d.ts","sourceRoot":"","sources":["../../../../../../../src/infrastructure/services/agents/feature-agent/nodes/repair.node.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+DAA+D,CAAC;AAGpG;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,EAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,MAAM,GACd,MAAM,CA0BR;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,EAC3B,QAAQ,EAAE,cAAc,GACvB,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAuCnE"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repair node factory for the feature-agent graph.
|
|
3
|
+
*
|
|
4
|
+
* Reads broken YAML content and validation errors from state,
|
|
5
|
+
* builds a constrained repair prompt, and sends it to the executor
|
|
6
|
+
* with limited tools (write-only) to fix the YAML files.
|
|
7
|
+
*/
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { readSpecFile, createNodeLogger } from './node-helpers.js';
|
|
10
|
+
/**
|
|
11
|
+
* Build a repair prompt containing the broken YAML, validation errors,
|
|
12
|
+
* and file paths for the executor to write fixed files.
|
|
13
|
+
*/
|
|
14
|
+
export function buildRepairPrompt(filename, content, errors, specDir) {
|
|
15
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
16
|
+
const filePaths = filenames.map((f) => join(specDir, f));
|
|
17
|
+
return [
|
|
18
|
+
'## YAML Repair Task',
|
|
19
|
+
'',
|
|
20
|
+
`Fix the following YAML validation errors in ${filenames.join(' and ')}.`,
|
|
21
|
+
'',
|
|
22
|
+
'### Validation Errors',
|
|
23
|
+
...errors.map((e) => `- ${e}`),
|
|
24
|
+
'',
|
|
25
|
+
'### Current File Content',
|
|
26
|
+
'```yaml',
|
|
27
|
+
content,
|
|
28
|
+
'```',
|
|
29
|
+
'',
|
|
30
|
+
'### Output Files',
|
|
31
|
+
...filePaths.map((p) => `- Write fixed YAML to: ${p}`),
|
|
32
|
+
'',
|
|
33
|
+
'### Rules',
|
|
34
|
+
'- Fix ONLY the reported errors',
|
|
35
|
+
'- Preserve all existing valid content',
|
|
36
|
+
'- Use proper YAML formatting',
|
|
37
|
+
'- Do not add fields that were not in the original',
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Factory that creates a repair node for a specific YAML file (or files).
|
|
42
|
+
*
|
|
43
|
+
* Reads the broken YAML, builds a repair prompt with validation errors
|
|
44
|
+
* from state, and calls the executor with constrained options (maxTurns=5,
|
|
45
|
+
* write-only tools, no MCP).
|
|
46
|
+
*/
|
|
47
|
+
export function createRepairNode(filename, executor) {
|
|
48
|
+
const label = Array.isArray(filename) ? filename.join('+') : filename;
|
|
49
|
+
const log = createNodeLogger(`repair:${label}`);
|
|
50
|
+
return async (state) => {
|
|
51
|
+
log.info(`Repairing ${label} (attempt ${state.validationRetries})`);
|
|
52
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
53
|
+
const contents = filenames.map((f) => readSpecFile(state.specDir, f));
|
|
54
|
+
const combinedContent = filenames.map((f, i) => `--- ${f} ---\n${contents[i]}`).join('\n\n');
|
|
55
|
+
const prompt = buildRepairPrompt(filename, combinedContent, state.lastValidationErrors, state.specDir);
|
|
56
|
+
const options = {
|
|
57
|
+
cwd: state.worktreePath || state.repositoryPath,
|
|
58
|
+
maxTurns: 5,
|
|
59
|
+
disableMcp: true,
|
|
60
|
+
allowedTools: ['write'],
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
const result = await executor.execute(prompt, options);
|
|
64
|
+
log.info(`Repair complete (${result.result.length} chars)`);
|
|
65
|
+
return {
|
|
66
|
+
messages: [`[repair:${label}] Repair attempt ${state.validationRetries} complete`],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71
|
+
log.error(`Repair failed: ${msg}`);
|
|
72
|
+
return {
|
|
73
|
+
messages: [`[repair:${label}] Repair failed: ${msg}`],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/plan.schema.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.schema.d.ts","sourceRoot":"","sources":["../../../../../../../../src/infrastructure/services/agents/feature-agent/nodes/schemas/plan.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,iBAAiB,CAAC;AAEzB,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,gBAAgB,CA+B5D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,gBAAgB,CA+BjF"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { requireString, requireNonEmptyArray, requireArrayOfShape, } from './validation.js';
|
|
2
|
+
export function validatePlan(data) {
|
|
3
|
+
const errors = [];
|
|
4
|
+
if (!data || typeof data !== 'object') {
|
|
5
|
+
errors.push('YAML parsed to null or non-object');
|
|
6
|
+
return { valid: false, errors };
|
|
7
|
+
}
|
|
8
|
+
const d = data;
|
|
9
|
+
requireString(d, 'content', errors);
|
|
10
|
+
requireArrayOfShape(d, 'phases', errors, (item, idx, errs) => {
|
|
11
|
+
const phase = item;
|
|
12
|
+
if (!phase || typeof phase !== 'object') {
|
|
13
|
+
errs.push(`phases[${idx}] must be an object`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
requireString(phase, 'id', errs, `phases[${idx}].`);
|
|
17
|
+
requireString(phase, 'name', errs, `phases[${idx}].`);
|
|
18
|
+
if (typeof phase.parallel !== 'boolean') {
|
|
19
|
+
errs.push(`phases[${idx}].parallel must be a boolean`);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const ftc = Array.isArray(d.filesToCreate) ? d.filesToCreate : [];
|
|
23
|
+
const ftm = Array.isArray(d.filesToModify) ? d.filesToModify : [];
|
|
24
|
+
if (ftc.length === 0 && ftm.length === 0) {
|
|
25
|
+
errors.push("At least one of 'filesToCreate' or 'filesToModify' must be non-empty");
|
|
26
|
+
}
|
|
27
|
+
return { valid: errors.length === 0, errors };
|
|
28
|
+
}
|
|
29
|
+
export function validateTasks(data, phaseIds) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
if (!data || typeof data !== 'object') {
|
|
32
|
+
errors.push('YAML parsed to null or non-object');
|
|
33
|
+
return { valid: false, errors };
|
|
34
|
+
}
|
|
35
|
+
const d = data;
|
|
36
|
+
requireArrayOfShape(d, 'tasks', errors, (item, idx, errs) => {
|
|
37
|
+
const task = item;
|
|
38
|
+
if (!task || typeof task !== 'object') {
|
|
39
|
+
errs.push(`tasks[${idx}] must be an object`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
requireString(task, 'id', errs, `tasks[${idx}].`);
|
|
43
|
+
requireString(task, 'title', errs, `tasks[${idx}].`);
|
|
44
|
+
requireString(task, 'state', errs, `tasks[${idx}].`);
|
|
45
|
+
requireNonEmptyArray(task, 'acceptanceCriteria', errs, `tasks[${idx}].`);
|
|
46
|
+
if (typeof task.phaseId === 'string' && !phaseIds.includes(task.phaseId)) {
|
|
47
|
+
errs.push(`tasks[${idx}].phaseId '${task.phaseId}' does not match any phase (valid: ${phaseIds.join(', ')})`);
|
|
48
|
+
}
|
|
49
|
+
else if (typeof task.phaseId !== 'string') {
|
|
50
|
+
requireString(task, 'phaseId', errs, `tasks[${idx}].`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return { valid: errors.length === 0, errors };
|
|
54
|
+
}
|
package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/research.schema.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"research.schema.d.ts","sourceRoot":"","sources":["../../../../../../../../src/infrastructure/services/agents/feature-agent/nodes/schemas/research.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,iBAAiB,CAAC;AAEzB,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,gBAAgB,CA0BhE"}
|