@shepai/cli 1.38.0 → 1.39.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 (115) hide show
  1. package/dist/packages/core/src/application/ports/output/agents/feature-agent-process.interface.d.ts +1 -0
  2. package/dist/packages/core/src/application/ports/output/agents/feature-agent-process.interface.d.ts.map +1 -1
  3. package/dist/packages/core/src/application/use-cases/agents/approve-agent-run.use-case.d.ts +5 -4
  4. package/dist/packages/core/src/application/use-cases/agents/approve-agent-run.use-case.d.ts.map +1 -1
  5. package/dist/packages/core/src/application/use-cases/agents/approve-agent-run.use-case.js +36 -14
  6. package/dist/packages/core/src/application/use-cases/agents/reject-agent-run.use-case.d.ts +12 -3
  7. package/dist/packages/core/src/application/use-cases/agents/reject-agent-run.use-case.d.ts.map +1 -1
  8. package/dist/packages/core/src/application/use-cases/agents/reject-agent-run.use-case.js +88 -8
  9. package/dist/packages/core/src/application/use-cases/agents/review-feature.use-case.d.ts +35 -0
  10. package/dist/packages/core/src/application/use-cases/agents/review-feature.use-case.d.ts.map +1 -0
  11. package/dist/packages/core/src/application/use-cases/agents/review-feature.use-case.js +88 -0
  12. package/dist/packages/core/src/domain/generated/output.d.ts +102 -0
  13. package/dist/packages/core/src/domain/generated/output.d.ts.map +1 -1
  14. package/dist/packages/core/src/infrastructure/di/container.d.ts.map +1 -1
  15. package/dist/packages/core/src/infrastructure/di/container.js +2 -0
  16. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts +51 -0
  17. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.d.ts.map +1 -1
  18. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-graph.js +21 -2
  19. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.d.ts +1 -0
  20. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.d.ts.map +1 -1
  21. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-process.service.js +3 -0
  22. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.d.ts +1 -0
  23. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.d.ts.map +1 -1
  24. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.js +31 -1
  25. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.d.ts +19 -3
  26. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.d.ts.map +1 -1
  27. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.js +101 -16
  28. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/requirements.prompt.d.ts +2 -1
  29. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/requirements.prompt.d.ts.map +1 -1
  30. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/requirements.prompt.js +53 -27
  31. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.d.ts +3 -0
  32. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.d.ts.map +1 -1
  33. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.js +28 -1
  34. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.d.ts +3 -0
  35. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.d.ts.map +1 -1
  36. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/state.js +13 -0
  37. package/dist/src/presentation/cli/commands/agent/reject.command.js +1 -1
  38. package/dist/src/presentation/cli/commands/feat/reject.command.d.ts.map +1 -1
  39. package/dist/src/presentation/cli/commands/feat/reject.command.js +6 -4
  40. package/dist/src/presentation/cli/commands/feat/review.command.d.ts +3 -2
  41. package/dist/src/presentation/cli/commands/feat/review.command.d.ts.map +1 -1
  42. package/dist/src/presentation/cli/commands/feat/review.command.js +87 -12
  43. package/dist/src/presentation/cli/commands/feat/show.command.d.ts.map +1 -1
  44. package/dist/src/presentation/cli/commands/feat/show.command.js +38 -3
  45. package/dist/src/presentation/tui/prompts/prd-review-question.prompt.d.ts +30 -0
  46. package/dist/src/presentation/tui/prompts/prd-review-question.prompt.d.ts.map +1 -0
  47. package/dist/src/presentation/tui/prompts/prd-review-question.prompt.js +23 -0
  48. package/dist/src/presentation/tui/prompts/prd-review-summary.prompt.d.ts +29 -0
  49. package/dist/src/presentation/tui/prompts/prd-review-summary.prompt.d.ts.map +1 -0
  50. package/dist/src/presentation/tui/prompts/prd-review-summary.prompt.js +29 -0
  51. package/dist/src/presentation/tui/wizards/merge-review.wizard.d.ts +13 -0
  52. package/dist/src/presentation/tui/wizards/merge-review.wizard.d.ts.map +1 -0
  53. package/dist/src/presentation/tui/wizards/merge-review.wizard.js +43 -0
  54. package/dist/src/presentation/tui/wizards/plan-review.wizard.d.ts +13 -0
  55. package/dist/src/presentation/tui/wizards/plan-review.wizard.d.ts.map +1 -0
  56. package/dist/src/presentation/tui/wizards/plan-review.wizard.js +43 -0
  57. package/dist/src/presentation/tui/wizards/prd-review.wizard.d.ts +26 -0
  58. package/dist/src/presentation/tui/wizards/prd-review.wizard.d.ts.map +1 -0
  59. package/dist/src/presentation/tui/wizards/prd-review.wizard.js +42 -0
  60. package/dist/tsconfig.build.tsbuildinfo +1 -1
  61. package/package.json +1 -1
  62. package/web/.next/BUILD_ID +1 -1
  63. package/web/.next/build-manifest.json +2 -2
  64. package/web/.next/cache/.previewinfo +1 -1
  65. package/web/.next/cache/.rscinfo +1 -1
  66. package/web/.next/cache/.tsbuildinfo +1 -1
  67. package/web/.next/cache/config.json +3 -3
  68. package/web/.next/fallback-build-manifest.json +2 -2
  69. package/web/.next/prerender-manifest.json +3 -3
  70. package/web/.next/required-server-files.js +1 -1
  71. package/web/.next/required-server-files.json +1 -1
  72. package/web/.next/server/app/_global-error.html +2 -2
  73. package/web/.next/server/app/_global-error.rsc +1 -1
  74. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  75. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  76. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  77. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  78. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  79. package/web/.next/server/app/page/server-reference-manifest.json +9 -9
  80. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  81. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +1 -1
  82. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js.map +1 -1
  83. package/web/.next/server/chunks/ssr/[root-of-the-server]__19354aea._.js +2 -2
  84. package/web/.next/server/chunks/ssr/[root-of-the-server]__f12b60be._.js +1 -1
  85. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
  86. package/web/.next/server/chunks/ssr/src_presentation_web_ed0934e5._.js.map +1 -1
  87. package/web/.next/server/pages/500.html +2 -2
  88. package/web/.next/server/server-reference-manifest.js +1 -1
  89. package/web/.next/server/server-reference-manifest.json +10 -10
  90. package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
  91. package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
  92. package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
  93. package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
  94. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
  95. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
  96. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  97. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  98. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  99. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  100. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  101. package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +9 -9
  102. package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
  103. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +1 -1
  104. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__19354aea._.js +2 -2
  105. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__f12b60be._.js +1 -1
  106. package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
  107. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
  108. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +10 -10
  109. package/web/.next/standalone/src/presentation/web/server.js +1 -1
  110. package/web/.next/static/chunks/{eca1959e083d0a45.js → 429ee8f94032181c.js} +1 -1
  111. package/web/.next/trace +1 -1
  112. package/web/.next/trace-build +1 -1
  113. /package/web/.next/static/{yvV-GEMagQiZhZAF0YqQs → t8z43Apwz2m0TFf1MnAKq}/_buildManifest.js +0 -0
  114. /package/web/.next/static/{yvV-GEMagQiZhZAF0YqQs → t8z43Apwz2m0TFf1MnAKq}/_clientMiddlewareManifest.json +0 -0
  115. /package/web/.next/static/{yvV-GEMagQiZhZAF0YqQs → t8z43Apwz2m0TFf1MnAKq}/_ssgManifest.js +0 -0
@@ -5,7 +5,7 @@
5
5
  * and error handling so every node follows the same patterns.
6
6
  */
7
7
  import yaml from 'js-yaml';
8
- import { readFileSync, writeFileSync } from 'node:fs';
8
+ import { readFileSync, writeFileSync, renameSync, unlinkSync } 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';
@@ -81,9 +81,8 @@ export function safeYamlLoad(content) {
81
81
  * - allowPlan: when true, skip interrupt after plan phase
82
82
  * - allowMerge: when true, skip interrupt after merge phase
83
83
  *
84
- * Nodes not covered by a gate (analyze, research) never interrupt.
85
- * The implement node always interrupts when gates are present
86
- * (unless all three gates are true, meaning fully autonomous).
84
+ * Nodes not covered by a gate (analyze, research, implement) never interrupt.
85
+ * Implementation always proceeds to merge; the merge node handles its own gate.
87
86
  *
88
87
  */
89
88
  export function shouldInterrupt(nodeName, gates) {
@@ -97,8 +96,6 @@ export function shouldInterrupt(nodeName, gates) {
97
96
  return !gates.allowPlan;
98
97
  if (nodeName === 'merge')
99
98
  return !gates.allowMerge;
100
- if (nodeName === 'implement')
101
- return true;
102
99
  return false;
103
100
  }
104
101
  const API_ERROR_RE = /API Error: (400|429|5\d{2})/;
@@ -169,6 +166,38 @@ export function getCompletedPhases(specDir) {
169
166
  return [];
170
167
  }
171
168
  }
169
+ /**
170
+ * Remove a phase from completedPhases in feature.yaml.
171
+ * Used when a phase is rejected and needs to re-execute.
172
+ */
173
+ export function clearCompletedPhase(specDir, phaseId, log) {
174
+ try {
175
+ const content = readSpecFile(specDir, 'feature.yaml');
176
+ if (!content)
177
+ return;
178
+ const data = yaml.load(content);
179
+ if (!data || typeof data !== 'object')
180
+ return;
181
+ const status = (data.status ?? {});
182
+ const completedPhases = Array.isArray(status.completedPhases)
183
+ ? status.completedPhases.filter((p) => p !== phaseId)
184
+ : [];
185
+ data.status = { ...status, completedPhases };
186
+ writeFileSync(join(specDir, 'feature.yaml'), yaml.dump(data, { indent: 2, lineWidth: -1 }), 'utf-8');
187
+ }
188
+ catch (err) {
189
+ log?.error(`Failed to clear completed phase: ${err instanceof Error ? err.message : String(err)}`);
190
+ }
191
+ }
192
+ /**
193
+ * Type guard to detect rejection payloads from interrupt() return value.
194
+ */
195
+ export function isRejectionPayload(value) {
196
+ return (typeof value === 'object' &&
197
+ value !== null &&
198
+ 'rejected' in value &&
199
+ value.rejected === true);
200
+ }
172
201
  /**
173
202
  * Mark a phase as complete in feature.yaml by adding its ID to completedPhases.
174
203
  */
@@ -194,6 +223,26 @@ export function markPhaseComplete(specDir, phaseId, log) {
194
223
  log?.error(`Failed to mark phase complete: ${err instanceof Error ? err.message : String(err)}`);
195
224
  }
196
225
  }
226
+ /**
227
+ * Write a spec file atomically using temp-file-then-rename pattern.
228
+ * Prevents corruption on crash mid-write.
229
+ */
230
+ export function writeSpecFileAtomic(specDir, filename, content) {
231
+ const targetPath = join(specDir, filename);
232
+ const tempPath = join(specDir, `.${filename}.tmp`);
233
+ try {
234
+ writeFileSync(tempPath, content, 'utf-8');
235
+ renameSync(tempPath, targetPath);
236
+ }
237
+ finally {
238
+ try {
239
+ unlinkSync(tempPath);
240
+ }
241
+ catch {
242
+ // Temp file already renamed or doesn't exist
243
+ }
244
+ }
245
+ }
197
246
  /**
198
247
  * Execute a node with consistent logging and error handling.
199
248
  *
@@ -210,16 +259,46 @@ export function executeNode(nodeName, executor, buildPrompt) {
210
259
  // Update feature lifecycle to reflect the current phase
211
260
  await updateNodeLifecycle(nodeName);
212
261
  // On resume from interrupt, LangGraph re-executes the node function from
213
- // the top. Skip the expensive executor call if this phase already completed
214
- // (markPhaseComplete is called before interrupt, so completed = done but
215
- // awaiting approval; the approval has now been granted).
262
+ // the top. Instead of re-executing within this invocation (which would
263
+ // require a second interrupt() call and trigger the stale-replay bug),
264
+ // we return early. Rejection sets _needsReexecution=true so a conditional
265
+ // edge in the graph routes back to this node for a fresh invocation.
216
266
  const completedPhases = getCompletedPhases(state.specDir);
217
267
  if (completedPhases.includes(nodeName)) {
218
- log.info('Phase already completed (resuming from approval), skipping execution');
219
- return {
220
- currentNode: nodeName,
221
- messages: [`[${nodeName}] Approved continuing`],
222
- };
268
+ if (shouldInterrupt(nodeName, state.approvalGates)) {
269
+ if (state._approvalAction === 'rejected') {
270
+ const feedback = state._rejectionFeedback ?? '(no feedback)';
271
+ log.info(`Phase rejected with feedback: "${feedback}"scheduling re-execution`);
272
+ clearCompletedPhase(state.specDir, nodeName, log);
273
+ // Return early — the conditional edge will route back to this node
274
+ // for a fresh invocation with a clean interrupt index.
275
+ return {
276
+ currentNode: nodeName,
277
+ messages: [`[${nodeName}] Rejected — will re-execute`],
278
+ _approvalAction: null,
279
+ _rejectionFeedback: null,
280
+ _needsReexecution: true,
281
+ };
282
+ }
283
+ else {
284
+ log.info('Phase approved, skipping re-execution');
285
+ return {
286
+ currentNode: nodeName,
287
+ messages: [`[${nodeName}] Approved — continuing`],
288
+ _approvalAction: null,
289
+ _rejectionFeedback: null,
290
+ _needsReexecution: false,
291
+ };
292
+ }
293
+ }
294
+ else {
295
+ log.info('Phase already completed (no gate), skipping execution');
296
+ return {
297
+ currentNode: nodeName,
298
+ messages: [`[${nodeName}] Approved — continuing`],
299
+ _needsReexecution: false,
300
+ };
301
+ }
223
302
  }
224
303
  const startTime = Date.now();
225
304
  // Record phase start (no-op if timing context not set)
@@ -236,13 +315,19 @@ export function executeNode(nodeName, executor, buildPrompt) {
236
315
  // Record phase completion
237
316
  await recordPhaseEnd(timingId, durationMs);
238
317
  // Mark phase complete BEFORE interrupting so that on resume the
239
- // node detects the work is already done and skips re-execution.
318
+ // node detects the work is already done and returns early.
240
319
  markPhaseComplete(state.specDir, nodeName, log);
241
320
  const nodeResult = {
242
321
  currentNode: nodeName,
243
322
  messages: [`[${nodeName}] Complete (${result.result.length} chars, ${elapsed}s)`],
323
+ _approvalAction: null,
324
+ _rejectionFeedback: null,
325
+ _needsReexecution: false,
244
326
  };
245
- // Human-in-the-loop: interrupt after node execution for review
327
+ // Human-in-the-loop: interrupt after node execution for review.
328
+ // This is the ONLY interrupt() call in the entire execution path.
329
+ // On resume, the node returns early (above) without calling interrupt(),
330
+ // so there is no stale interrupt replay.
246
331
  if (shouldInterrupt(nodeName, state.approvalGates)) {
247
332
  log.info('Interrupting for human approval');
248
333
  await recordApprovalWaitStart(timingId);
@@ -2,7 +2,8 @@
2
2
  * Requirements Phase Prompt
3
3
  *
4
4
  * Instructs the agent to build comprehensive requirements from the analysis,
5
- * including product questions with AI-recommended default answers.
5
+ * including product questions with structured QuestionOption[] arrays
6
+ * and AI-recommended default answers.
6
7
  * Writes back to spec.yaml with full requirements.
7
8
  */
8
9
  import type { FeatureAgentState } from '../../state.js';
@@ -1 +1 @@
1
- {"version":3,"file":"requirements.prompt.d.ts","sourceRoot":"","sources":["../../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/requirements.prompt.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAoIxE"}
1
+ {"version":3,"file":"requirements.prompt.d.ts","sourceRoot":"","sources":["../../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/prompts/requirements.prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAkKxE"}
@@ -2,14 +2,40 @@
2
2
  * Requirements Phase Prompt
3
3
  *
4
4
  * Instructs the agent to build comprehensive requirements from the analysis,
5
- * including product questions with AI-recommended default answers.
5
+ * including product questions with structured QuestionOption[] arrays
6
+ * and AI-recommended default answers.
6
7
  * Writes back to spec.yaml with full requirements.
7
8
  */
9
+ import yaml from 'js-yaml';
8
10
  import { readSpecFile } from '../node-helpers.js';
9
11
  export function buildRequirementsPrompt(state) {
10
12
  const specContent = readSpecFile(state.specDir, 'spec.yaml');
13
+ // Extract rejection feedback if present
14
+ let rejectionFeedbackSection = '';
15
+ try {
16
+ const specData = yaml.load(specContent);
17
+ const rejectionFeedback = specData?.rejectionFeedback;
18
+ if (rejectionFeedback && rejectionFeedback.length > 0) {
19
+ const entries = rejectionFeedback
20
+ .map((entry) => `- **Iteration ${entry.iteration}** (${entry.timestamp}): ${entry.message}`)
21
+ .join('\n');
22
+ rejectionFeedbackSection = `
23
+ ## Previous Rejection Feedback
24
+
25
+ The user has previously rejected this PRD with the following feedback. You MUST address these concerns in your revised output:
26
+
27
+ ${entries}
28
+
29
+ Focus on the most recent feedback (highest iteration number) while ensuring earlier feedback is still addressed.
30
+
31
+ `;
32
+ }
33
+ }
34
+ catch {
35
+ // If YAML parsing fails, continue without rejection feedback
36
+ }
11
37
  return `You are a product analyst performing the REQUIREMENTS phase of feature development.
12
-
38
+ ${rejectionFeedbackSection}
13
39
  ## Your Task
14
40
 
15
41
  1. Read the current spec with codebase analysis below
@@ -29,13 +55,20 @@ ${specContent}
29
55
  - **Non-functional requirements (NFR-N)**: performance, security, UX, maintainability constraints
30
56
  - **Success criteria**: measurable checkboxes that define "done"
31
57
 
32
- ### Product Questions with AI Defaults
58
+ ### Product Questions with Structured Options
33
59
  For every ambiguous product decision, create an openQuestion entry with:
34
60
  - A clear question
35
61
  - resolved: true (your recommendation is the default)
36
- - Your recommended answer with reasoning
62
+ - Structured options array with EXACTLY ONE option marked selected: true
63
+ - A selectionRationale explaining the recommendation
64
+ - An answer field containing the text of the selected option (for backward compatibility)
65
+
66
+ Each option MUST have:
67
+ - \`option\`: Short label for the approach (2-5 words)
68
+ - \`description\`: Explanation of this option's trade-offs and benefits
69
+ - \`selected\`: boolean — exactly ONE option per question must be true
37
70
 
38
- The user can later review and override these defaults. This is the AI-recommended path — not the only option.
71
+ The user can later review and override these defaults via interactive TUI.
39
72
 
40
73
  ## Output Instructions
41
74
 
@@ -63,32 +96,25 @@ Write the COMPLETE file. Preserve name/number/branch/technologies from analysis
63
96
  - question: 'Should X use approach A, B, or C?'
64
97
  resolved: true
65
98
  options:
66
- - option: Approach A
67
- description: (What approach A means and its trade-offs)
99
+ - option: 'Approach A'
100
+ description: 'Benefits of A, when to use it, trade-offs'
68
101
  selected: true
69
- - option: Approach B
70
- description: (What approach B means and its trade-offs)
71
- selected: false
72
- - option: Approach C
73
- description: (What approach C means and its trade-offs)
102
+ - option: 'Approach B'
103
+ description: 'Benefits of B, when to use it, trade-offs'
74
104
  selected: false
75
- selectionRationale: >
76
- (Detailed reasoning for why approach A is recommended.
77
- Reference codebase patterns where relevant.)
105
+ selectionRationale: 'Approach A is recommended because [detailed reasoning]'
106
+ answer: 'Approach A'
78
107
  - question: 'What level of Y is needed?'
79
108
  resolved: true
80
109
  options:
81
- - option: Level X
82
- description: (What level X means and its trade-offs)
83
- selected: true
84
- - option: Level Y
85
- description: (What level Y means and its trade-offs)
86
- selected: false
87
- - option: Level Z
88
- description: (What level Z means and its trade-offs)
110
+ - option: 'Basic'
111
+ description: 'Minimal implementation, fast to build'
89
112
  selected: false
90
- selectionRationale: >
91
- (Detailed reasoning for why level X is recommended.)
113
+ - option: 'Advanced'
114
+ description: 'Full-featured, handles edge cases'
115
+ selected: true
116
+ selectionRationale: 'Advanced is recommended because [reasoning]'
117
+ answer: 'Advanced'
92
118
 
93
119
  content: |
94
120
  ## Problem Statement
@@ -132,8 +158,8 @@ Write the COMPLETE file. Preserve name/number/branch/technologies from analysis
132
158
  ## Constraints
133
159
 
134
160
  - Write ONLY to ${state.specDir}/spec.yaml
135
- - Every open question MUST have an AI-recommended default answer (resolved: true)
136
- - Every open question MUST have exactly 3 options to choose from
161
+ - Every open question MUST have structured options with exactly ONE option marked selected: true
162
+ - Every open question MUST also have an \`answer\` field matching the selected option text
137
163
  - Requirements must be specific and testable, not vague
138
164
  - Do NOT create any other files
139
165
  - Do NOT modify any source code`;
@@ -24,6 +24,9 @@ export declare function clearPhaseTimingContext(): void;
24
24
  /**
25
25
  * Record the start of a phase. Returns the timing record ID (for later update)
26
26
  * or null if context is not set or save fails.
27
+ *
28
+ * Automatically appends an iteration suffix (`:2`, `:3`, …) when the same
29
+ * phase has already been recorded for this run (e.g. after a rejection loop).
27
30
  */
28
31
  export declare function recordPhaseStart(phase: string): Promise<string | null>;
29
32
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"phase-timing-context.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wEAAwE,CAAC;AAMrH;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAE/C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,sBAAsB,GAAG,IAAI,CAG7F;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAI9C;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqB5E;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/F;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,MAAM,EACtB,YAAY,CAAC,EAAE,sBAAsB,GACpC,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAUpF"}
1
+ {"version":3,"file":"phase-timing-context.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wEAAwE,CAAC;AAMrH;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAE/C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,sBAAsB,GAAG,IAAI,CAG7F;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAI9C;AA+BD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsB5E;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/F;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,MAAM,EACtB,YAAY,CAAC,EAAE,sBAAsB,GACpC,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAUpF"}
@@ -33,9 +33,35 @@ export function clearPhaseTimingContext() {
33
33
  contextRepository = undefined;
34
34
  lastTimingId = null;
35
35
  }
36
+ /**
37
+ * Compute the iteration-aware phase name.
38
+ *
39
+ * Queries existing PhaseTiming rows for the current run, counts how many
40
+ * share the same base phase name, and appends `:N` for iterations > 1.
41
+ *
42
+ * Examples:
43
+ * - First "requirements" execution → "requirements"
44
+ * - Second "requirements" execution → "requirements:2"
45
+ * - Third "requirements" execution → "requirements:3"
46
+ */
47
+ async function resolveIterationPhase(phase, runId, repository) {
48
+ try {
49
+ const existing = await repository.findByRunId(runId);
50
+ // Count rows whose phase is the base name or matches the base:N pattern
51
+ const count = existing.filter((t) => t.phase === phase || t.phase.startsWith(`${phase}:`)).length;
52
+ return count === 0 ? phase : `${phase}:${count + 1}`;
53
+ }
54
+ catch {
55
+ // On error, fall back to bare phase name
56
+ return phase;
57
+ }
58
+ }
36
59
  /**
37
60
  * Record the start of a phase. Returns the timing record ID (for later update)
38
61
  * or null if context is not set or save fails.
62
+ *
63
+ * Automatically appends an iteration suffix (`:2`, `:3`, …) when the same
64
+ * phase has already been recorded for this run (e.g. after a rejection loop).
39
65
  */
40
66
  export async function recordPhaseStart(phase) {
41
67
  if (!contextRunId || !contextRepository)
@@ -43,10 +69,11 @@ export async function recordPhaseStart(phase) {
43
69
  const id = randomUUID();
44
70
  const now = new Date();
45
71
  try {
72
+ const iterationPhase = await resolveIterationPhase(phase, contextRunId, contextRepository);
46
73
  await contextRepository.save({
47
74
  id,
48
75
  agentRunId: contextRunId,
49
- phase,
76
+ phase: iterationPhase,
50
77
  startedAt: now,
51
78
  createdAt: now,
52
79
  updatedAt: now,
@@ -38,6 +38,9 @@ export declare const FeatureAgentAnnotation: import("@langchain/langgraph").Anno
38
38
  validationRetries: import("@langchain/langgraph").BinaryOperatorAggregate<number, number>;
39
39
  lastValidationTarget: import("@langchain/langgraph").BinaryOperatorAggregate<string, string>;
40
40
  lastValidationErrors: import("@langchain/langgraph").BinaryOperatorAggregate<string[], string[]>;
41
+ _approvalAction: import("@langchain/langgraph").BinaryOperatorAggregate<string | null, string | null>;
42
+ _rejectionFeedback: import("@langchain/langgraph").BinaryOperatorAggregate<string | null, string | null>;
43
+ _needsReexecution: import("@langchain/langgraph").BinaryOperatorAggregate<boolean, boolean>;
41
44
  prUrl: import("@langchain/langgraph").BinaryOperatorAggregate<string | null, string | null>;
42
45
  prNumber: import("@langchain/langgraph").BinaryOperatorAggregate<number | null, number | null>;
43
46
  commitHash: import("@langchain/langgraph").BinaryOperatorAggregate<string | null, string | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/state.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwDjC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,OAAO,sBAAsB,CAAC,KAAK,CAAC"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/state.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqEjC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,OAAO,sBAAsB,CAAC,KAAK,CAAC"}
@@ -37,6 +37,19 @@ export const FeatureAgentAnnotation = Annotation.Root({
37
37
  reducer: (_prev, next) => next,
38
38
  default: () => [],
39
39
  }),
40
+ // --- Approval action channels (set by Command({update}) on resume) ---
41
+ _approvalAction: Annotation({
42
+ reducer: (_prev, next) => (next !== undefined ? next : _prev),
43
+ default: () => null,
44
+ }),
45
+ _rejectionFeedback: Annotation({
46
+ reducer: (_prev, next) => (next !== undefined ? next : _prev),
47
+ default: () => null,
48
+ }),
49
+ _needsReexecution: Annotation({
50
+ reducer: (_prev, next) => next,
51
+ default: () => false,
52
+ }),
40
53
  // --- Merge and workflow state channels ---
41
54
  prUrl: Annotation({
42
55
  reducer: (_prev, next) => (next !== undefined ? next : _prev),
@@ -25,7 +25,7 @@ export function createRejectCommand() {
25
25
  return;
26
26
  }
27
27
  const useCase = container.resolve(RejectAgentRunUseCase);
28
- const result = await useCase.execute(resolved.run.id, opts.reason);
28
+ const result = await useCase.execute(resolved.run.id, opts.reason ?? 'Rejected by user');
29
29
  if (result.rejected) {
30
30
  messages.success(`Rejected ${colors.accent(resolved.run.id.substring(0, 8))}: ${result.reason}`);
31
31
  }
@@ -1 +1 @@
1
- {"version":3,"file":"reject.command.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/feat/reject.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,mBAAmB,IAAI,OAAO,CAsC7C"}
1
+ {"version":3,"file":"reject.command.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/feat/reject.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,mBAAmB,IAAI,OAAO,CAwC7C"}
@@ -16,7 +16,7 @@ export function createRejectCommand() {
16
16
  return new Command('reject')
17
17
  .description('Reject a feature waiting for review')
18
18
  .argument('[id]', 'Feature ID (auto-resolves if omitted)')
19
- .option('--reason <text>', 'Rejection reason')
19
+ .requiredOption('--reason <text>', 'Rejection feedback (required)')
20
20
  .action(async (featureId, options) => {
21
21
  try {
22
22
  const featureRepo = container.resolve('IFeatureRepository');
@@ -35,10 +35,12 @@ export function createRejectCommand() {
35
35
  }
36
36
  messages.newline();
37
37
  messages.warning(`Rejected: ${feature.name}`);
38
- if (options.reason) {
39
- console.log(` ${colors.muted('Reason:')} ${options.reason}`);
38
+ console.log(` ${colors.muted('Reason:')} ${options.reason}`);
39
+ console.log(` ${colors.muted('Iteration:')} ${result.iteration}`);
40
+ if (result.iterationWarning) {
41
+ messages.warning('Warning: 5+ iterations. Consider refining the prompt instead.');
40
42
  }
41
- console.log(` ${colors.muted('Agent:')} cancelled`);
43
+ console.log(` ${colors.muted('Agent:')} re-running requirements`);
42
44
  messages.newline();
43
45
  }
44
46
  catch (error) {
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Feature Review Command
3
3
  *
4
- * Shows the current state of a feature waiting for approval,
5
- * including which phase triggered the interrupt and generated content.
4
+ * Interactive review of a feature waiting for approval.
5
+ * Routes to phase-specific TUI wizard (PRD, Plan, or Merge),
6
+ * then calls approve or reject use case based on user action.
6
7
  *
7
8
  * Usage:
8
9
  * shep feat review [id]
@@ -1 +1 @@
1
- {"version":3,"file":"review.command.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/feat/review.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,wBAAgB,mBAAmB,IAAI,OAAO,CAqC7C"}
1
+ {"version":3,"file":"review.command.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/feat/review.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiEpC,wBAAgB,mBAAmB,IAAI,OAAO,CA2E7C"}
@@ -1,19 +1,60 @@
1
1
  /**
2
2
  * Feature Review Command
3
3
  *
4
- * Shows the current state of a feature waiting for approval,
5
- * including which phase triggered the interrupt and generated content.
4
+ * Interactive review of a feature waiting for approval.
5
+ * Routes to phase-specific TUI wizard (PRD, Plan, or Merge),
6
+ * then calls approve or reject use case based on user action.
6
7
  *
7
8
  * Usage:
8
9
  * shep feat review [id]
9
10
  */
10
11
  import { Command } from 'commander';
11
12
  import { container } from '../../../../../packages/core/src/infrastructure/di/container.js';
13
+ import { ReviewFeatureUseCase } from '../../../../../packages/core/src/application/use-cases/agents/review-feature.use-case.js';
14
+ import { ApproveAgentRunUseCase } from '../../../../../packages/core/src/application/use-cases/agents/approve-agent-run.use-case.js';
15
+ import { RejectAgentRunUseCase } from '../../../../../packages/core/src/application/use-cases/agents/reject-agent-run.use-case.js';
12
16
  import { resolveWaitingFeature } from './resolve-waiting-feature.js';
17
+ import { prdReviewWizard } from '../../../tui/wizards/prd-review.wizard.js';
18
+ import { planReviewWizard } from '../../../tui/wizards/plan-review.wizard.js';
19
+ import { mergeReviewWizard } from '../../../tui/wizards/merge-review.wizard.js';
13
20
  import { colors, messages } from '../../ui/index.js';
21
+ function parsePhase(runResult) {
22
+ return runResult?.startsWith('node:') ? runResult.slice(5) : 'unknown';
23
+ }
24
+ async function runPhaseWizard(phase, featureId, repoPath) {
25
+ if (phase === 'requirements') {
26
+ const reviewUseCase = container.resolve(ReviewFeatureUseCase);
27
+ const reviewResult = await reviewUseCase.execute(featureId, repoPath);
28
+ if (!reviewResult.success) {
29
+ throw new Error(reviewResult.reason);
30
+ }
31
+ return prdReviewWizard(reviewResult.questions);
32
+ }
33
+ if (phase === 'plan') {
34
+ const result = await planReviewWizard();
35
+ return { action: result.action, feedback: result.feedback };
36
+ }
37
+ if (phase === 'merge') {
38
+ const result = await mergeReviewWizard();
39
+ return { action: result.action, feedback: result.feedback };
40
+ }
41
+ throw new Error(`Unknown approval phase: ${phase}`);
42
+ }
43
+ function phaseNextStep(phase) {
44
+ switch (phase) {
45
+ case 'requirements':
46
+ return 'research';
47
+ case 'plan':
48
+ return 'implementation';
49
+ case 'merge':
50
+ return 'merge';
51
+ default:
52
+ return 'next step';
53
+ }
54
+ }
14
55
  export function createReviewCommand() {
15
56
  return new Command('review')
16
- .description('Review a feature waiting for approval')
57
+ .description('Interactive review of a feature waiting for approval')
17
58
  .argument('[id]', 'Feature ID (auto-resolves if omitted)')
18
59
  .action(async (featureId) => {
19
60
  try {
@@ -26,19 +67,53 @@ export function createReviewCommand() {
26
67
  featureRepo,
27
68
  runRepo,
28
69
  });
29
- const phase = run.result?.startsWith('node:') ? run.result.slice(5) : 'unknown';
70
+ const phase = parsePhase(run.result);
30
71
  messages.newline();
31
- messages.info(`Feature waiting for approval: ${colors.accent(feature.name)}`);
32
- console.log(` ${colors.muted('ID:')} ${feature.id.slice(0, 8)}`);
72
+ messages.info(`Reviewing: ${colors.accent(feature.name)}`);
33
73
  console.log(` ${colors.muted('Phase:')} ${colors.warning(phase)}`);
34
74
  console.log(` ${colors.muted('Branch:')} ${colors.accent(feature.branch)}`);
35
- if (feature.specPath) {
36
- console.log(` ${colors.muted('Specs:')} ${feature.specPath}`);
37
- }
38
- messages.newline();
39
- console.log(` ${colors.muted('To approve:')} shep feat approve ${feature.id.slice(0, 8)}`);
40
- console.log(` ${colors.muted('To reject:')} shep feat reject ${feature.id.slice(0, 8)}`);
41
75
  messages.newline();
76
+ const wizardResult = await runPhaseWizard(phase, feature.id, repoPath);
77
+ if (wizardResult.action === 'approve') {
78
+ const approveUseCase = container.resolve(ApproveAgentRunUseCase);
79
+ const payload = wizardResult.changedSelections && wizardResult.changedSelections.length > 0
80
+ ? { approved: true, changedSelections: wizardResult.changedSelections }
81
+ : undefined;
82
+ const approveResult = await approveUseCase.execute(run.id, payload);
83
+ if (approveResult.approved) {
84
+ messages.newline();
85
+ messages.success(`Approved: ${feature.name}`);
86
+ if (wizardResult.changedSelections && wizardResult.changedSelections.length > 0) {
87
+ console.log(` ${colors.muted('Changes:')} ${wizardResult.changedSelections.length} selection(s) updated`);
88
+ }
89
+ if (wizardResult.feedback) {
90
+ console.log(` ${colors.muted('Comment:')} ${wizardResult.feedback}`);
91
+ }
92
+ console.log(` ${colors.muted('Agent:')} proceeding to ${phaseNextStep(phase)}`);
93
+ messages.newline();
94
+ }
95
+ else {
96
+ throw new Error(approveResult.reason);
97
+ }
98
+ }
99
+ else {
100
+ const rejectUseCase = container.resolve(RejectAgentRunUseCase);
101
+ const rejectResult = await rejectUseCase.execute(run.id, wizardResult.feedback);
102
+ if (rejectResult.rejected) {
103
+ messages.newline();
104
+ messages.warning(`Rejected: ${feature.name}`);
105
+ console.log(` ${colors.muted('Feedback:')} ${wizardResult.feedback}`);
106
+ console.log(` ${colors.muted('Iteration:')} ${rejectResult.iteration}`);
107
+ if (rejectResult.iterationWarning) {
108
+ messages.warning('Warning: 5+ iterations. Consider refining the prompt instead.');
109
+ }
110
+ console.log(` ${colors.muted('Agent:')} re-running ${phase}`);
111
+ messages.newline();
112
+ }
113
+ else {
114
+ throw new Error(rejectResult.reason);
115
+ }
116
+ }
42
117
  }
43
118
  catch (error) {
44
119
  const err = error instanceof Error ? error : new Error(String(error));
@@ -1 +1 @@
1
- {"version":3,"file":"show.command.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/feat/show.command.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsTpC,wBAAgB,iBAAiB,IAAI,OAAO,CAwJ3C"}
1
+ {"version":3,"file":"show.command.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/feat/show.command.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+VpC,wBAAgB,iBAAiB,IAAI,OAAO,CAyJ3C"}