@shepai/cli 1.19.0 → 1.20.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.
- package/dist/eslint.config.mjs +4 -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/create-feature.use-case.d.ts +37 -0
- package/dist/src/application/use-cases/features/create/create-feature.use-case.d.ts.map +1 -0
- package/dist/src/application/use-cases/features/{create-feature.use-case.js → create/create-feature.use-case.js} +22 -70
- package/dist/src/application/use-cases/features/create/metadata-generator.d.ts +28 -0
- package/dist/src/application/use-cases/features/create/metadata-generator.d.ts.map +1 -0
- package/dist/src/application/use-cases/features/create/metadata-generator.js +109 -0
- package/dist/src/application/use-cases/features/create/slug-resolver.d.ts +24 -0
- package/dist/src/application/use-cases/features/create/slug-resolver.d.ts.map +1 -0
- package/dist/src/application/use-cases/features/create/slug-resolver.js +66 -0
- package/dist/src/application/use-cases/features/create/types.d.ts +16 -0
- package/dist/src/application/use-cases/features/create/types.d.ts.map +1 -0
- package/dist/src/application/use-cases/features/create/types.js +1 -0
- 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 +12 -1
- 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/common/executors/mock-executor.service.js +2 -2
- 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 +35 -16
- 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 +3 -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/dist/src/application/use-cases/features/create-feature.use-case.d.ts +0 -43
- package/dist/src/application/use-cases/features/create-feature.use-case.d.ts.map +0 -1
- /package/web/.next/static/{nw4IT0rr_mE5Tx22X9OgO → xKJulVMYO_ZRGoqa6eM7G}/_buildManifest.js +0 -0
- /package/web/.next/static/{nw4IT0rr_mE5Tx22X9OgO → xKJulVMYO_ZRGoqa6eM7G}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{nw4IT0rr_mE5Tx22X9OgO → xKJulVMYO_ZRGoqa6eM7G}/_ssgManifest.js +0 -0
|
@@ -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"}
|
package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/research.schema.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { requireString, requireArrayOfShape, requireNonEmptyArray, } from './validation.js';
|
|
2
|
+
export function validateResearch(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, 'name', errors);
|
|
10
|
+
requireString(d, 'summary', errors);
|
|
11
|
+
requireString(d, 'content', errors);
|
|
12
|
+
requireArrayOfShape(d, 'decisions', errors, (item, idx, errs) => {
|
|
13
|
+
const decision = item;
|
|
14
|
+
if (!decision || typeof decision !== 'object') {
|
|
15
|
+
errs.push(`decisions[${idx}] must be an object`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
requireString(decision, 'title', errs, `decisions[${idx}].`);
|
|
19
|
+
requireString(decision, 'chosen', errs, `decisions[${idx}].`);
|
|
20
|
+
requireNonEmptyArray(decision, 'rejected', errs, `decisions[${idx}].`);
|
|
21
|
+
requireString(decision, 'rationale', errs, `decisions[${idx}].`);
|
|
22
|
+
});
|
|
23
|
+
return { valid: errors.length === 0, errors };
|
|
24
|
+
}
|
package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/spec.schema.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.schema.d.ts","sourceRoot":"","sources":["../../../../../../../../src/infrastructure/services/agents/feature-agent/nodes/schemas/spec.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AA4B7F,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,gBAAgB,CAInE;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,OAAO,GAAG,gBAAgB,CAqBxE"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { requireString, requireNonEmptyArray } from './validation.js';
|
|
2
|
+
const VALID_SIZE_ESTIMATES = ['S', 'M', 'L', 'XL'];
|
|
3
|
+
function validateBaseSpec(data, errors) {
|
|
4
|
+
if (!data || typeof data !== 'object') {
|
|
5
|
+
errors.push('YAML parsed to null or non-object');
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const d = data;
|
|
9
|
+
requireString(d, 'name', errors);
|
|
10
|
+
requireString(d, 'oneLiner', errors);
|
|
11
|
+
requireString(d, 'summary', errors);
|
|
12
|
+
requireString(d, 'phase', errors);
|
|
13
|
+
requireString(d, 'content', errors);
|
|
14
|
+
requireNonEmptyArray(d, 'technologies', errors);
|
|
15
|
+
if (typeof d.sizeEstimate === 'string' && !VALID_SIZE_ESTIMATES.includes(d.sizeEstimate)) {
|
|
16
|
+
errors.push(`Field 'sizeEstimate' must be one of ${VALID_SIZE_ESTIMATES.join(', ')}, got '${d.sizeEstimate}'`);
|
|
17
|
+
}
|
|
18
|
+
else if (typeof d.sizeEstimate !== 'string') {
|
|
19
|
+
requireString(d, 'sizeEstimate', errors);
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
export function validateSpecAnalyze(data) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
validateBaseSpec(data, errors);
|
|
26
|
+
return { valid: errors.length === 0, errors };
|
|
27
|
+
}
|
|
28
|
+
export function validateSpecRequirements(data) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
if (!validateBaseSpec(data, errors))
|
|
31
|
+
return { valid: false, errors };
|
|
32
|
+
const d = data;
|
|
33
|
+
if (!('openQuestions' in d)) {
|
|
34
|
+
errors.push("Missing required field 'openQuestions'");
|
|
35
|
+
}
|
|
36
|
+
else if (Array.isArray(d.openQuestions)) {
|
|
37
|
+
for (let i = 0; i < d.openQuestions.length; i++) {
|
|
38
|
+
const q = d.openQuestions[i];
|
|
39
|
+
if (!q || typeof q !== 'object') {
|
|
40
|
+
errors.push(`openQuestions[${i}] must be an object`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
requireString(q, 'question', errors, `openQuestions[${i}].`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
errors.push("Field 'openQuestions' must be an array");
|
|
48
|
+
}
|
|
49
|
+
return { valid: errors.length === 0, errors };
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ValidationResult {
|
|
2
|
+
valid: boolean;
|
|
3
|
+
errors: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function requireString(data: Record<string, unknown>, field: string, errors: string[], prefix?: string): void;
|
|
6
|
+
export declare function requireNonEmptyArray(data: Record<string, unknown>, field: string, errors: string[], prefix?: string): unknown[] | null;
|
|
7
|
+
export declare function requireArrayOfShape(data: Record<string, unknown>, field: string, errors: string[], itemChecker: (item: unknown, index: number, errors: string[]) => void, prefix?: string): void;
|
|
8
|
+
//# sourceMappingURL=validation.d.ts.map
|
package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/validation.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../../../../../../src/infrastructure/services/agents/feature-agent/nodes/schemas/validation.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EAAE,EAChB,MAAM,SAAK,GACV,IAAI,CASN;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EAAE,EAChB,MAAM,SAAK,GACV,OAAO,EAAE,GAAG,IAAI,CAelB;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EAAE,EAChB,WAAW,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,EACrE,MAAM,SAAK,GACV,IAAI,CAMN"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function requireString(data, field, errors, prefix = '') {
|
|
2
|
+
const val = data[field];
|
|
3
|
+
if (val === undefined || val === null) {
|
|
4
|
+
errors.push(`Missing required string field '${prefix}${field}'`);
|
|
5
|
+
}
|
|
6
|
+
else if (typeof val !== 'string') {
|
|
7
|
+
errors.push(`Field '${prefix}${field}' must be a string, got ${typeof val}`);
|
|
8
|
+
}
|
|
9
|
+
else if (val.trim() === '') {
|
|
10
|
+
errors.push(`Field '${prefix}${field}' must not be empty`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function requireNonEmptyArray(data, field, errors, prefix = '') {
|
|
14
|
+
const val = data[field];
|
|
15
|
+
if (val === undefined || val === null) {
|
|
16
|
+
errors.push(`Missing required array field '${prefix}${field}'`);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(val)) {
|
|
20
|
+
errors.push(`Field '${prefix}${field}' must be an array, got ${typeof val}`);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (val.length === 0) {
|
|
24
|
+
errors.push(`Field '${prefix}${field}' must not be empty`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return val;
|
|
28
|
+
}
|
|
29
|
+
export function requireArrayOfShape(data, field, errors, itemChecker, prefix = '') {
|
|
30
|
+
const arr = requireNonEmptyArray(data, field, errors, prefix);
|
|
31
|
+
if (!arr)
|
|
32
|
+
return;
|
|
33
|
+
for (let i = 0; i < arr.length; i++) {
|
|
34
|
+
itemChecker(arr[i], i, errors);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FeatureAgentState } from '../state.js';
|
|
2
|
+
import type { ValidationResult } from './schemas/validation.js';
|
|
3
|
+
type SchemaValidator = (data: unknown) => ValidationResult;
|
|
4
|
+
/**
|
|
5
|
+
* Factory that creates a validate node for a specific YAML file.
|
|
6
|
+
*
|
|
7
|
+
* On success: resets validationRetries to 0, clears errors.
|
|
8
|
+
* On failure: increments validationRetries, sets errors for repair node.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createValidateNode(filename: string, schema: SchemaValidator): (state: FeatureAgentState) => Promise<Partial<FeatureAgentState>>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=validate.node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.node.d.ts","sourceRoot":"","sources":["../../../../../../../src/infrastructure/services/agents/feature-agent/nodes/validate.node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,KAAK,eAAe,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,gBAAgB,CAAC;AAE3D;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,eAAe,GACtB,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAsDnE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readSpecFile, safeYamlLoad, createNodeLogger } from './node-helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Factory that creates a validate node for a specific YAML file.
|
|
4
|
+
*
|
|
5
|
+
* On success: resets validationRetries to 0, clears errors.
|
|
6
|
+
* On failure: increments validationRetries, sets errors for repair node.
|
|
7
|
+
*/
|
|
8
|
+
export function createValidateNode(filename, schema) {
|
|
9
|
+
const log = createNodeLogger(`validate:${filename}`);
|
|
10
|
+
return async (state) => {
|
|
11
|
+
log.info(`Validating ${filename}`);
|
|
12
|
+
const content = readSpecFile(state.specDir, filename);
|
|
13
|
+
if (!content) {
|
|
14
|
+
const errors = [`File '${filename}' not found or empty in ${state.specDir}`];
|
|
15
|
+
log.error(errors[0]);
|
|
16
|
+
return {
|
|
17
|
+
lastValidationTarget: filename,
|
|
18
|
+
lastValidationErrors: errors,
|
|
19
|
+
validationRetries: state.validationRetries + 1,
|
|
20
|
+
messages: [`[validate:${filename}] FAIL: ${errors[0]}`],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Try to parse
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = safeYamlLoad(content);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
30
|
+
const errors = [`YAML parse error in ${filename}: ${msg}`];
|
|
31
|
+
log.error(errors[0]);
|
|
32
|
+
return {
|
|
33
|
+
lastValidationTarget: filename,
|
|
34
|
+
lastValidationErrors: errors,
|
|
35
|
+
validationRetries: state.validationRetries + 1,
|
|
36
|
+
messages: [`[validate:${filename}] FAIL: parse error`],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Run schema validation
|
|
40
|
+
const result = schema(parsed);
|
|
41
|
+
if (result.valid) {
|
|
42
|
+
log.info('Validation passed');
|
|
43
|
+
return {
|
|
44
|
+
lastValidationTarget: filename,
|
|
45
|
+
lastValidationErrors: [],
|
|
46
|
+
validationRetries: 0,
|
|
47
|
+
messages: [`[validate:${filename}] PASS`],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
log.error(`Validation failed: ${result.errors.join('; ')}`);
|
|
51
|
+
return {
|
|
52
|
+
lastValidationTarget: filename,
|
|
53
|
+
lastValidationErrors: result.errors,
|
|
54
|
+
validationRetries: state.validationRetries + 1,
|
|
55
|
+
messages: [`[validate:${filename}] FAIL: ${result.errors.length} error(s)`],
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
}
|