@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.
Files changed (138) hide show
  1. package/dist/eslint.config.mjs +3 -0
  2. package/dist/src/application/ports/output/agents/feature-agent-process.interface.d.ts +2 -1
  3. package/dist/src/application/ports/output/agents/feature-agent-process.interface.d.ts.map +1 -1
  4. package/dist/src/application/ports/output/agents/index.d.ts +1 -0
  5. package/dist/src/application/ports/output/agents/index.d.ts.map +1 -1
  6. package/dist/src/application/ports/output/agents/phase-timing-repository.interface.d.ts +44 -0
  7. package/dist/src/application/ports/output/agents/phase-timing-repository.interface.d.ts.map +1 -0
  8. package/dist/src/application/ports/output/agents/phase-timing-repository.interface.js +11 -0
  9. package/dist/src/application/use-cases/agents/approve-agent-run.use-case.d.ts.map +1 -1
  10. package/dist/src/application/use-cases/agents/approve-agent-run.use-case.js +6 -2
  11. package/dist/src/application/use-cases/agents/reject-agent-run.use-case.d.ts.map +1 -1
  12. package/dist/src/application/use-cases/agents/reject-agent-run.use-case.js +0 -1
  13. package/dist/src/application/use-cases/features/create-feature.use-case.d.ts +12 -2
  14. package/dist/src/application/use-cases/features/create-feature.use-case.d.ts.map +1 -1
  15. package/dist/src/application/use-cases/features/create-feature.use-case.js +45 -10
  16. package/dist/src/application/use-cases/features/resume-feature.use-case.js +2 -2
  17. package/dist/src/domain/generated/output.d.ts +40 -6
  18. package/dist/src/domain/generated/output.d.ts.map +1 -1
  19. package/dist/src/infrastructure/di/container.d.ts.map +1 -1
  20. package/dist/src/infrastructure/di/container.js +7 -0
  21. package/dist/src/infrastructure/persistence/sqlite/mappers/agent-run.mapper.d.ts +1 -2
  22. package/dist/src/infrastructure/persistence/sqlite/mappers/agent-run.mapper.d.ts.map +1 -1
  23. package/dist/src/infrastructure/persistence/sqlite/mappers/agent-run.mapper.js +4 -4
  24. package/dist/src/infrastructure/persistence/sqlite/migrations.d.ts.map +1 -1
  25. package/dist/src/infrastructure/persistence/sqlite/migrations.js +31 -0
  26. package/dist/src/infrastructure/repositories/agent-run.repository.d.ts.map +1 -1
  27. package/dist/src/infrastructure/repositories/agent-run.repository.js +5 -9
  28. package/dist/src/infrastructure/repositories/sqlite-phase-timing.repository.d.ts +21 -0
  29. package/dist/src/infrastructure/repositories/sqlite-phase-timing.repository.d.ts.map +1 -0
  30. package/dist/src/infrastructure/repositories/sqlite-phase-timing.repository.js +100 -0
  31. package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts +305 -13
  32. package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts.map +1 -1
  33. package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-graph.js +124 -7
  34. package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.d.ts +2 -1
  35. package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.d.ts.map +1 -1
  36. package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.js +2 -2
  37. package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-worker.d.ts +2 -1
  38. package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-worker.d.ts.map +1 -1
  39. package/dist/src/infrastructure/services/agents/feature-agent/feature-agent-worker.js +19 -7
  40. package/dist/src/infrastructure/services/agents/feature-agent/nodes/implement.node.js +1 -1
  41. package/dist/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.d.ts +11 -2
  42. package/dist/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.d.ts.map +1 -1
  43. package/dist/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.js +46 -15
  44. package/dist/src/infrastructure/services/agents/feature-agent/nodes/repair.node.d.ts +23 -0
  45. package/dist/src/infrastructure/services/agents/feature-agent/nodes/repair.node.d.ts.map +1 -0
  46. package/dist/src/infrastructure/services/agents/feature-agent/nodes/repair.node.js +77 -0
  47. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/plan.schema.d.ts +4 -0
  48. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/plan.schema.d.ts.map +1 -0
  49. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/plan.schema.js +54 -0
  50. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/research.schema.d.ts +3 -0
  51. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/research.schema.d.ts.map +1 -0
  52. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/research.schema.js +24 -0
  53. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/spec.schema.d.ts +4 -0
  54. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/spec.schema.d.ts.map +1 -0
  55. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/spec.schema.js +50 -0
  56. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/validation.d.ts +8 -0
  57. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/validation.d.ts.map +1 -0
  58. package/dist/src/infrastructure/services/agents/feature-agent/nodes/schemas/validation.js +36 -0
  59. package/dist/src/infrastructure/services/agents/feature-agent/nodes/validate.node.d.ts +12 -0
  60. package/dist/src/infrastructure/services/agents/feature-agent/nodes/validate.node.d.ts.map +1 -0
  61. package/dist/src/infrastructure/services/agents/feature-agent/nodes/validate.node.js +58 -0
  62. package/dist/src/infrastructure/services/agents/feature-agent/phase-timing-context.d.ts +30 -0
  63. package/dist/src/infrastructure/services/agents/feature-agent/phase-timing-context.d.ts.map +1 -0
  64. package/dist/src/infrastructure/services/agents/feature-agent/phase-timing-context.js +68 -0
  65. package/dist/src/infrastructure/services/agents/feature-agent/state.d.ts +5 -1
  66. package/dist/src/infrastructure/services/agents/feature-agent/state.d.ts.map +1 -1
  67. package/dist/src/infrastructure/services/agents/feature-agent/state.js +14 -1
  68. package/dist/src/presentation/cli/commands/feat/approve.command.d.ts +12 -0
  69. package/dist/src/presentation/cli/commands/feat/approve.command.d.ts.map +1 -0
  70. package/dist/src/presentation/cli/commands/feat/approve.command.js +48 -0
  71. package/dist/src/presentation/cli/commands/feat/index.d.ts.map +1 -1
  72. package/dist/src/presentation/cli/commands/feat/index.js +7 -1
  73. package/dist/src/presentation/cli/commands/feat/ls.command.d.ts.map +1 -1
  74. package/dist/src/presentation/cli/commands/feat/ls.command.js +13 -5
  75. package/dist/src/presentation/cli/commands/feat/new.command.d.ts.map +1 -1
  76. package/dist/src/presentation/cli/commands/feat/new.command.js +34 -15
  77. package/dist/src/presentation/cli/commands/feat/reject.command.d.ts +12 -0
  78. package/dist/src/presentation/cli/commands/feat/reject.command.d.ts.map +1 -0
  79. package/dist/src/presentation/cli/commands/feat/reject.command.js +50 -0
  80. package/dist/src/presentation/cli/commands/feat/resolve-waiting-feature.d.ts +30 -0
  81. package/dist/src/presentation/cli/commands/feat/resolve-waiting-feature.d.ts.map +1 -0
  82. package/dist/src/presentation/cli/commands/feat/resolve-waiting-feature.js +57 -0
  83. package/dist/src/presentation/cli/commands/feat/review.command.d.ts +12 -0
  84. package/dist/src/presentation/cli/commands/feat/review.command.d.ts.map +1 -0
  85. package/dist/src/presentation/cli/commands/feat/review.command.js +49 -0
  86. package/dist/src/presentation/cli/commands/feat/show.command.d.ts.map +1 -1
  87. package/dist/src/presentation/cli/commands/feat/show.command.js +51 -6
  88. package/dist/tsconfig.build.tsbuildinfo +1 -1
  89. package/package.json +1 -1
  90. package/web/.next/BUILD_ID +1 -1
  91. package/web/.next/build-manifest.json +2 -2
  92. package/web/.next/cache/.previewinfo +1 -1
  93. package/web/.next/cache/.rscinfo +1 -1
  94. package/web/.next/cache/config.json +3 -3
  95. package/web/.next/fallback-build-manifest.json +2 -2
  96. package/web/.next/prerender-manifest.json +3 -3
  97. package/web/.next/required-server-files.js +1 -1
  98. package/web/.next/required-server-files.json +1 -1
  99. package/web/.next/server/app/_global-error.html +2 -2
  100. package/web/.next/server/app/_global-error.rsc +1 -1
  101. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  102. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  103. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  104. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  105. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  106. package/web/.next/server/app/_not-found.html +2 -2
  107. package/web/.next/server/app/_not-found.rsc +1 -1
  108. package/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  109. package/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  110. package/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  111. package/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  112. package/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  113. package/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  114. package/web/.next/server/app/index.html +2 -2
  115. package/web/.next/server/app/index.rsc +1 -1
  116. package/web/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  117. package/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  118. package/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  119. package/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  120. package/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  121. package/web/.next/server/app/version.html +2 -2
  122. package/web/.next/server/app/version.rsc +1 -1
  123. package/web/.next/server/app/version.segments/_full.segment.rsc +1 -1
  124. package/web/.next/server/app/version.segments/_head.segment.rsc +1 -1
  125. package/web/.next/server/app/version.segments/_index.segment.rsc +1 -1
  126. package/web/.next/server/app/version.segments/_tree.segment.rsc +1 -1
  127. package/web/.next/server/app/version.segments/version/__PAGE__.segment.rsc +1 -1
  128. package/web/.next/server/app/version.segments/version.segment.rsc +1 -1
  129. package/web/.next/server/chunks/ssr/_19be0743._.js +1 -1
  130. package/web/.next/server/pages/404.html +2 -2
  131. package/web/.next/server/pages/500.html +2 -2
  132. package/web/.next/server/server-reference-manifest.js +1 -1
  133. package/web/.next/server/server-reference-manifest.json +1 -1
  134. package/web/.next/trace +1 -1
  135. package/web/.next/trace-build +1 -1
  136. /package/web/.next/static/{nw4IT0rr_mE5Tx22X9OgO → u-tyZUMnsIne6K_0rPmPO}/_buildManifest.js +0 -0
  137. /package/web/.next/static/{nw4IT0rr_mE5Tx22X9OgO → u-tyZUMnsIne6K_0rPmPO}/_clientMiddlewareManifest.json +0 -0
  138. /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 delegates work to the injected IAgentExecutor (e.g. Claude Code).
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', 'requirements')
31
- .addEdge('requirements', 'research')
32
- .addEdge('research', 'plan')
33
- .addEdge('plan', 'implement')
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
  }
@@ -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
- approvalMode?: string;
15
+ approvalGates?: ApprovalGates;
15
16
  resume?: boolean;
16
17
  threadId?: string;
17
18
  resumeFromInterrupt?: boolean;
@@ -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;AAa/G,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,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,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"}
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"}
@@ -41,8 +41,8 @@ export class FeatureAgentProcessService {
41
41
  if (worktreePath) {
42
42
  args.push('--worktree-path', worktreePath);
43
43
  }
44
- if (options?.approvalMode) {
45
- args.push('--approval-mode', options.approvalMode);
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
- approvalMode?: string;
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;AAc1B,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,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,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,CAmC1D;AAgCD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmI/D"}
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-mode');
37
- const approvalMode = approvalIdx !== -1 && approvalIdx + 1 < args.length ? args[approvalIdx + 1] : undefined;
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
- approvalMode,
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(` approvalMode=${args.approvalMode ?? '(none)'}`);
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.approvalMode ? { approvalMode: args.approvalMode } : {}),
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.approvalMode ? { approvalMode: args.approvalMode } : {}),
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.approvalMode)) {
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 mode.
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, approvalMode: string | undefined): boolean;
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;AAGrD;;;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;AAUD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAI3F;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,CAgDnE"}
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 mode.
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, approvalMode) {
85
- if (!approvalMode || approvalMode === 'allow-all')
86
+ export function shouldInterrupt(nodeName, gates) {
87
+ if (!gates)
88
+ return false;
89
+ if (gates.allowPrd && gates.allowPlan)
86
90
  return false;
87
- const autoApproved = AUTO_APPROVED_NODES[approvalMode] ?? [];
88
- return !autoApproved.includes(nodeName);
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 elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
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.approvalMode)) {
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 elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
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
+ }
@@ -0,0 +1,4 @@
1
+ import { type ValidationResult } from './validation.js';
2
+ export declare function validatePlan(data: unknown): ValidationResult;
3
+ export declare function validateTasks(data: unknown, phaseIds: string[]): ValidationResult;
4
+ //# sourceMappingURL=plan.schema.d.ts.map
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { type ValidationResult } from './validation.js';
2
+ export declare function validateResearch(data: unknown): ValidationResult;
3
+ //# sourceMappingURL=research.schema.d.ts.map
@@ -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"}