@shepai/cli 1.37.1 → 1.37.3

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 (57) hide show
  1. package/dist/packages/core/src/application/use-cases/agents/stop-agent-run.use-case.d.ts +5 -2
  2. package/dist/packages/core/src/application/use-cases/agents/stop-agent-run.use-case.d.ts.map +1 -1
  3. package/dist/packages/core/src/application/use-cases/agents/stop-agent-run.use-case.js +21 -10
  4. package/dist/packages/core/src/application/use-cases/features/resume-feature.use-case.d.ts.map +1 -1
  5. package/dist/packages/core/src/application/use-cases/features/resume-feature.use-case.js +11 -0
  6. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.d.ts.map +1 -1
  7. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.js +11 -1
  8. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.d.ts +8 -0
  9. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.d.ts.map +1 -1
  10. package/dist/packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.js +29 -0
  11. package/dist/src/presentation/cli/commands/agent/stop.command.d.ts +1 -1
  12. package/dist/src/presentation/cli/commands/agent/stop.command.js +1 -1
  13. package/dist/src/presentation/cli/commands/feat/show.command.d.ts.map +1 -1
  14. package/dist/src/presentation/cli/commands/feat/show.command.js +179 -75
  15. package/dist/tsconfig.build.tsbuildinfo +1 -1
  16. package/package.json +1 -1
  17. package/web/.next/BUILD_ID +1 -1
  18. package/web/.next/build-manifest.json +2 -2
  19. package/web/.next/cache/.previewinfo +1 -1
  20. package/web/.next/cache/.rscinfo +1 -1
  21. package/web/.next/cache/config.json +3 -3
  22. package/web/.next/fallback-build-manifest.json +2 -2
  23. package/web/.next/prerender-manifest.json +3 -3
  24. package/web/.next/required-server-files.js +1 -1
  25. package/web/.next/required-server-files.json +1 -1
  26. package/web/.next/server/app/_global-error.html +2 -2
  27. package/web/.next/server/app/_global-error.rsc +1 -1
  28. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  29. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  30. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  31. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  32. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  33. package/web/.next/server/chunks/ssr/[root-of-the-server]__f12b60be._.js +1 -1
  34. package/web/.next/server/pages/500.html +2 -2
  35. package/web/.next/server/server-reference-manifest.js +1 -1
  36. package/web/.next/server/server-reference-manifest.json +1 -1
  37. package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
  38. package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
  39. package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
  40. package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
  41. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
  42. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
  43. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  44. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  45. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  46. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  47. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  48. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__f12b60be._.js +1 -1
  49. package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
  50. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
  51. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +1 -1
  52. package/web/.next/standalone/src/presentation/web/server.js +1 -1
  53. package/web/.next/trace +1 -1
  54. package/web/.next/trace-build +1 -1
  55. /package/web/.next/static/{UemLwbVtqCGOm4FHNOp05 → O1yesm5Dga4m8eBi7EwM0}/_buildManifest.js +0 -0
  56. /package/web/.next/static/{UemLwbVtqCGOm4FHNOp05 → O1yesm5Dga4m8eBi7EwM0}/_clientMiddlewareManifest.json +0 -0
  57. /package/web/.next/static/{UemLwbVtqCGOm4FHNOp05 → O1yesm5Dga4m8eBi7EwM0}/_ssgManifest.js +0 -0
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * Stop Agent Run Use Case
3
3
  *
4
- * Sends SIGTERM to a running agent's worker process and marks it as cancelled.
4
+ * Sends SIGTERM to a running agent's worker process and marks it as interrupted.
5
+ * Using "interrupted" (not "cancelled") so the run is resumable via `shep feat resume`.
5
6
  */
6
7
  import type { IAgentRunRepository } from '../../ports/output/agents/agent-run-repository.interface.js';
8
+ import type { IPhaseTimingRepository } from '../../ports/output/agents/phase-timing-repository.interface.js';
7
9
  export declare class StopAgentRunUseCase {
8
10
  private readonly agentRunRepository;
9
- constructor(agentRunRepository: IAgentRunRepository);
11
+ private readonly phaseTimingRepository;
12
+ constructor(agentRunRepository: IAgentRunRepository, phaseTimingRepository: IPhaseTimingRepository);
10
13
  execute(id: string): Promise<{
11
14
  stopped: boolean;
12
15
  reason: string;
@@ -1 +1 @@
1
- {"version":3,"file":"stop-agent-run.use-case.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/application/use-cases/agents/stop-agent-run.use-case.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6DAA6D,CAAC;AAGvG,qBACa,mBAAmB;IAG5B,OAAO,CAAC,QAAQ,CAAC,kBAAkB;gBAAlB,kBAAkB,EAAE,mBAAmB;IAGpD,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CA4DzE"}
1
+ {"version":3,"file":"stop-agent-run.use-case.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/application/use-cases/agents/stop-agent-run.use-case.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6DAA6D,CAAC;AACvG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gEAAgE,CAAC;AAI7G,qBACa,mBAAmB;IAG5B,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IAEnC,OAAO,CAAC,QAAQ,CAAC,qBAAqB;gBAFrB,kBAAkB,EAAE,mBAAmB,EAEvC,qBAAqB,EAAE,sBAAsB;IAG1D,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAmEzE"}
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Stop Agent Run Use Case
3
3
  *
4
- * Sends SIGTERM to a running agent's worker process and marks it as cancelled.
4
+ * Sends SIGTERM to a running agent's worker process and marks it as interrupted.
5
+ * Using "interrupted" (not "cancelled") so the run is resumable via `shep feat resume`.
5
6
  */
6
7
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
7
8
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -17,10 +18,13 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
17
18
  };
18
19
  import { injectable, inject } from 'tsyringe';
19
20
  import { AgentRunStatus } from '../../../domain/generated/output.js';
21
+ import { recordLifecycleEvent } from '../../../infrastructure/services/agents/feature-agent/phase-timing-context.js';
20
22
  let StopAgentRunUseCase = class StopAgentRunUseCase {
21
23
  agentRunRepository;
22
- constructor(agentRunRepository) {
24
+ phaseTimingRepository;
25
+ constructor(agentRunRepository, phaseTimingRepository) {
23
26
  this.agentRunRepository = agentRunRepository;
27
+ this.phaseTimingRepository = phaseTimingRepository;
24
28
  }
25
29
  async execute(id) {
26
30
  const run = await this.agentRunRepository.findById(id);
@@ -37,14 +41,15 @@ let StopAgentRunUseCase = class StopAgentRunUseCase {
37
41
  return { stopped: false, reason: `Agent run already in terminal state: ${run.status}` };
38
42
  }
39
43
  if (!run.pid) {
40
- // No PID — just mark as cancelled
44
+ // No PID — just mark as interrupted (resumable)
41
45
  const now = new Date();
42
- await this.agentRunRepository.updateStatus(id, AgentRunStatus.cancelled, {
43
- error: 'Cancelled by user (no PID)',
46
+ await this.agentRunRepository.updateStatus(id, AgentRunStatus.interrupted, {
47
+ error: 'Stopped by user (no PID)',
44
48
  completedAt: now,
45
49
  updatedAt: now,
46
50
  });
47
- return { stopped: true, reason: 'Marked as cancelled (no PID to signal)' };
51
+ await recordLifecycleEvent('run:stopped', id, this.phaseTimingRepository);
52
+ return { stopped: true, reason: 'Marked as interrupted (no PID to signal)' };
48
53
  }
49
54
  // Check if process is alive
50
55
  let alive = false;
@@ -65,22 +70,28 @@ let StopAgentRunUseCase = class StopAgentRunUseCase {
65
70
  }
66
71
  }
67
72
  const now = new Date();
68
- await this.agentRunRepository.updateStatus(id, AgentRunStatus.cancelled, {
69
- error: 'Cancelled by user',
73
+ await this.agentRunRepository.updateStatus(id, AgentRunStatus.interrupted, {
74
+ error: 'Stopped by user',
70
75
  completedAt: now,
71
76
  updatedAt: now,
72
77
  });
78
+ // For alive processes, the worker's SIGTERM handler records run:stopped.
79
+ // For dead processes, record it here since the worker can't.
80
+ if (!alive) {
81
+ await recordLifecycleEvent('run:stopped', id, this.phaseTimingRepository);
82
+ }
73
83
  return {
74
84
  stopped: true,
75
85
  reason: alive
76
86
  ? `Sent SIGTERM to PID ${run.pid}`
77
- : `Process already dead, marked as cancelled`,
87
+ : `Process already dead, marked as interrupted`,
78
88
  };
79
89
  }
80
90
  };
81
91
  StopAgentRunUseCase = __decorate([
82
92
  injectable(),
83
93
  __param(0, inject('IAgentRunRepository')),
84
- __metadata("design:paramtypes", [Object])
94
+ __param(1, inject('IPhaseTimingRepository')),
95
+ __metadata("design:paramtypes", [Object, Object])
85
96
  ], StopAgentRunUseCase);
86
97
  export { StopAgentRunUseCase };
@@ -1 +1 @@
1
- {"version":3,"file":"resume-feature.use-case.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/application/use-cases/features/resume-feature.use-case.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,qCAAqC,CAAC;AAE7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iEAAiE,CAAC;AAC1G,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6DAA6D,CAAC;AACvG,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,8DAA8D,CAAC;AAChH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAQlG,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED,qBACa,oBAAoB;IAG7B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAE5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAExB,OAAO,CAAC,QAAQ,CAAC,cAAc;IAE/B,OAAO,CAAC,QAAQ,CAAC,eAAe;gBANf,WAAW,EAAE,kBAAkB,EAE/B,OAAO,EAAE,mBAAmB,EAE5B,cAAc,EAAE,2BAA2B,EAE3C,eAAe,EAAE,gBAAgB;IAG9C,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;CA+E/D"}
1
+ {"version":3,"file":"resume-feature.use-case.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/application/use-cases/features/resume-feature.use-case.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,qCAAqC,CAAC;AAE7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iEAAiE,CAAC;AAC1G,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6DAA6D,CAAC;AACvG,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,8DAA8D,CAAC;AAChH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2DAA2D,CAAC;AAQlG,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED,qBACa,oBAAoB;IAG7B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAE5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAExB,OAAO,CAAC,QAAQ,CAAC,cAAc;IAE/B,OAAO,CAAC,QAAQ,CAAC,eAAe;gBANf,WAAW,EAAE,kBAAkB,EAE/B,OAAO,EAAE,mBAAmB,EAE5B,cAAc,EAAE,2BAA2B,EAE3C,eAAe,EAAE,gBAAgB;IAG9C,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;CA6F/D"}
@@ -50,6 +50,17 @@ let ResumeFeatureUseCase = class ResumeFeatureUseCase {
50
50
  if (!lastRun) {
51
51
  throw new Error(`No agent run found for feature "${feature.name}"`);
52
52
  }
53
+ // Detect crashed processes: if DB says "running" but the process is dead,
54
+ // mark it as interrupted so it becomes resumable
55
+ if ((lastRun.status === AgentRunStatus.running || lastRun.status === AgentRunStatus.pending) &&
56
+ feature.agentRunId) {
57
+ await this.processService.checkAndMarkCrashed(feature.agentRunId);
58
+ // Re-read after potential status change
59
+ const refreshed = await this.runRepo.findById(feature.agentRunId);
60
+ if (refreshed) {
61
+ Object.assign(lastRun, refreshed);
62
+ }
63
+ }
53
64
  // Validate the run is in a resumable state
54
65
  if (lastRun.status === AgentRunStatus.running) {
55
66
  throw new Error('Agent is still running — stop it first before resuming');
@@ -1 +1 @@
1
- {"version":3,"file":"feature-agent-worker.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAEH,OAAO,kBAAkB,CAAC;AAqB1B,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;IAC9B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA6C1D;AAgCD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0J/D"}
1
+ {"version":3,"file":"feature-agent-worker.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAEH,OAAO,kBAAkB,CAAC;AAqB1B,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;IAC9B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA6C1D;AAgCD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAoK/D"}
@@ -19,7 +19,7 @@ import { AgentRunStatus } from '../../../../domain/generated/output.js';
19
19
  import { initializeSettings } from '../../../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
+ import { setPhaseTimingContext, recordLifecycleEvent } from './phase-timing-context.js';
23
23
  import { setLifecycleContext } from './lifecycle-context.js';
24
24
  import { generatePrYaml } from './nodes/pr-yaml-generator.js';
25
25
  /**
@@ -151,8 +151,13 @@ export async function runWorker(args) {
151
151
  // Set phase timing context so executeNode() records per-phase durations
152
152
  const timingRepository = container.resolve('IPhaseTimingRepository');
153
153
  setPhaseTimingContext(args.runId, timingRepository);
154
+ // Capture repos for SIGTERM handler
155
+ runRepoForSignal = runRepository;
156
+ timingRepoForSignal = timingRepository;
154
157
  // Set lifecycle context so nodes update the feature lifecycle as they execute
155
158
  setLifecycleContext(args.featureId, featureRepository);
159
+ // Record lifecycle event
160
+ await recordLifecycleEvent(args.resume ? 'run:resumed' : 'run:started');
156
161
  try {
157
162
  const graphConfig = { configurable: { thread_id: checkpointId } };
158
163
  let result;
@@ -208,6 +213,7 @@ export async function runWorker(args) {
208
213
  completedAt: failedAt,
209
214
  updatedAt: failedAt,
210
215
  });
216
+ await recordLifecycleEvent('run:failed');
211
217
  log(`Run marked as failed: ${result.error}`);
212
218
  return;
213
219
  }
@@ -217,6 +223,7 @@ export async function runWorker(args) {
217
223
  completedAt,
218
224
  updatedAt: completedAt,
219
225
  });
226
+ await recordLifecycleEvent('run:completed');
220
227
  log('Run marked as completed');
221
228
  }
222
229
  catch (error) {
@@ -229,6 +236,7 @@ export async function runWorker(args) {
229
236
  completedAt: failedAt,
230
237
  updatedAt: failedAt,
231
238
  });
239
+ await recordLifecycleEvent('run:failed');
232
240
  log('Run marked as failed');
233
241
  }
234
242
  }
@@ -249,6 +257,7 @@ process.on('disconnect', () => {
249
257
  // Handle SIGTERM for graceful shutdown
250
258
  let runIdForSignal;
251
259
  let runRepoForSignal;
260
+ let timingRepoForSignal;
252
261
  process.on('SIGTERM', async () => {
253
262
  log('Received SIGTERM, shutting down...');
254
263
  if (runIdForSignal && runRepoForSignal) {
@@ -258,6 +267,7 @@ process.on('SIGTERM', async () => {
258
267
  completedAt: now,
259
268
  updatedAt: now,
260
269
  });
270
+ await recordLifecycleEvent('run:stopped', runIdForSignal, timingRepoForSignal);
261
271
  }
262
272
  process.exit(0);
263
273
  });
@@ -31,6 +31,14 @@ export declare function recordPhaseStart(phase: string): Promise<string | null>;
31
31
  * No-op if timingId is null (phase start was skipped or failed).
32
32
  */
33
33
  export declare function recordPhaseEnd(timingId: string | null, durationMs: number): Promise<void>;
34
+ /**
35
+ * Record an instant lifecycle event (e.g., run:started, run:stopped, run:crashed).
36
+ * These are phase timing records with zero duration, used to mark events in the timeline.
37
+ *
38
+ * Can be called with explicit runId and repository (for use outside the worker context,
39
+ * e.g., from use cases like StopAgentRunUseCase or ResumeFeatureUseCase).
40
+ */
41
+ export declare function recordLifecycleEvent(phase: string, explicitRunId?: string, explicitRepo?: IPhaseTimingRepository): Promise<void>;
34
42
  /**
35
43
  * Record the start of an approval wait. Sets waitingApprovalAt on the timing record.
36
44
  * No-op if timingId is null or context is not set.
@@ -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;;;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;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"}
@@ -76,6 +76,35 @@ export async function recordPhaseEnd(timingId, durationMs) {
76
76
  // Swallow — timing update failure is non-fatal
77
77
  }
78
78
  }
79
+ /**
80
+ * Record an instant lifecycle event (e.g., run:started, run:stopped, run:crashed).
81
+ * These are phase timing records with zero duration, used to mark events in the timeline.
82
+ *
83
+ * Can be called with explicit runId and repository (for use outside the worker context,
84
+ * e.g., from use cases like StopAgentRunUseCase or ResumeFeatureUseCase).
85
+ */
86
+ export async function recordLifecycleEvent(phase, explicitRunId, explicitRepo) {
87
+ const runId = explicitRunId ?? contextRunId;
88
+ const repo = explicitRepo ?? contextRepository;
89
+ if (!runId || !repo)
90
+ return;
91
+ const now = new Date();
92
+ try {
93
+ await repo.save({
94
+ id: randomUUID(),
95
+ agentRunId: runId,
96
+ phase,
97
+ startedAt: now,
98
+ completedAt: now,
99
+ durationMs: BigInt(0),
100
+ createdAt: now,
101
+ updatedAt: now,
102
+ });
103
+ }
104
+ catch {
105
+ // Swallow — lifecycle event recording is non-fatal
106
+ }
107
+ }
79
108
  /**
80
109
  * Record the start of an approval wait. Sets waitingApprovalAt on the timing record.
81
110
  * No-op if timingId is null or context is not set.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agent Stop Command
3
3
  *
4
- * Sends SIGTERM to a running agent and marks it as cancelled.
4
+ * Sends SIGTERM to a running agent and marks it as interrupted (resumable).
5
5
  *
6
6
  * Usage:
7
7
  * shep agent stop <id>
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agent Stop Command
3
3
  *
4
- * Sends SIGTERM to a running agent and marks it as cancelled.
4
+ * Sends SIGTERM to a running agent and marks it as interrupted (resumable).
5
5
  *
6
6
  * Usage:
7
7
  * shep agent stop <id>
@@ -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;AAmFpC,wBAAgB,iBAAiB,IAAI,OAAO,CAwP3C"}
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"}
@@ -29,6 +29,19 @@ const NODE_TO_APPROVE = {
29
29
  implement: 'Approve Implementation',
30
30
  merge: 'Approve Merge',
31
31
  };
32
+ /** Map lifecycle event phases to display labels and symbols. */
33
+ const LIFECYCLE_EVENTS = {
34
+ 'run:started': { label: 'started', color: colors.info },
35
+ 'run:resumed': { label: 'resumed', color: colors.info },
36
+ 'run:completed': { label: 'completed', color: colors.success },
37
+ 'run:failed': { label: 'failed', color: colors.error },
38
+ 'run:stopped': { label: 'stopped', color: colors.warning },
39
+ 'run:crashed': { label: 'crashed', color: colors.error },
40
+ };
41
+ /** Check if a phase is a lifecycle event (run:*). */
42
+ function isLifecycleEvent(phase) {
43
+ return phase.startsWith('run:');
44
+ }
32
45
  function formatStatus(feature, run) {
33
46
  if (!run) {
34
47
  return `${colors.muted(symbols.dotEmpty)} ${colors.muted(feature.lifecycle)}`;
@@ -81,6 +94,167 @@ function isProcessAlive(pid) {
81
94
  return false;
82
95
  }
83
96
  }
97
+ /**
98
+ * Group timings by agentRunId, preserving order.
99
+ */
100
+ function groupTimingsByRun(timings) {
101
+ const groups = [];
102
+ let current = null;
103
+ for (const t of timings) {
104
+ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
105
+ if (!current || current.runId !== t.agentRunId) {
106
+ current = { runId: t.agentRunId, timings: [] };
107
+ groups.push(current);
108
+ }
109
+ current.timings.push(t);
110
+ }
111
+ return groups;
112
+ }
113
+ /**
114
+ * Render a single timing entry (node phase, not lifecycle event).
115
+ */
116
+ function renderNodeTiming(t, isLast, isWaiting, isRunTerminal, run, maxDurationMs, maxBar) {
117
+ const lines = [];
118
+ const isSubPhase = t.phase.includes(':');
119
+ const label = isSubPhase
120
+ ? ` \u21b3 ${t.phase.split(':')[1]}`.padEnd(16)
121
+ : (NODE_TO_PHASE[t.phase] ?? t.phase).padEnd(16);
122
+ // Completed phase - green bar with duration
123
+ if (t.completedAt && t.durationMs != null) {
124
+ const ms = Number(t.durationMs);
125
+ const secs = (ms / 1000).toFixed(1);
126
+ const barLen = maxDurationMs > 0
127
+ ? Math.min(maxBar, Math.max(1, Math.round((ms / maxDurationMs) * maxBar)))
128
+ : 1;
129
+ const bar = `${colors.success('\u2588'.repeat(barLen))}${colors.muted('\u2591'.repeat(maxBar - barLen))}`;
130
+ lines.push(`${label} ${bar} ${secs}s`);
131
+ }
132
+ // Waiting for approval - only the LAST timing
133
+ else if (isLast && isWaiting) {
134
+ lines.push(`${label} ${colors.warning('awaiting review')}`);
135
+ }
136
+ // Crashed/stopped phase - red bar with frozen duration
137
+ else if (isRunTerminal) {
138
+ const endTime = run?.updatedAt
139
+ ? new Date(run.updatedAt).getTime()
140
+ : Date.now();
141
+ const elapsedMs = Math.max(0, endTime - new Date(t.startedAt).getTime());
142
+ const secs = (elapsedMs / 1000).toFixed(1);
143
+ const barLen = maxDurationMs > 0
144
+ ? Math.min(maxBar, Math.max(1, Math.round((elapsedMs / maxDurationMs) * maxBar)))
145
+ : 1;
146
+ const bar = `${colors.error('\u2588'.repeat(barLen))}${colors.muted('\u2591'.repeat(maxBar - barLen))}`;
147
+ lines.push(`${label} ${bar} ${secs}s (crashed)`);
148
+ }
149
+ // Running phase - blue bar with elapsed time
150
+ else {
151
+ const elapsedMs = Math.max(0, Date.now() - new Date(t.startedAt).getTime());
152
+ const secs = (elapsedMs / 1000).toFixed(1);
153
+ const barLen = maxDurationMs > 0
154
+ ? Math.min(maxBar, Math.max(1, Math.round((elapsedMs / maxDurationMs) * maxBar)))
155
+ : 1;
156
+ const bar = `${colors.info('\u2588'.repeat(barLen))}${colors.muted('\u2591'.repeat(maxBar - barLen))}`;
157
+ lines.push(`${label} ${bar} ${secs}s (running)`);
158
+ }
159
+ // Show approval wait time under gated phases
160
+ if (t.approvalWaitMs != null && Number(t.approvalWaitMs) > 0) {
161
+ const waitMs = Number(t.approvalWaitMs);
162
+ const waitSecs = (waitMs / 1000).toFixed(1);
163
+ const waitLabel = ' \u21b3 approval'.padEnd(16);
164
+ const waitBarLen = maxDurationMs > 0
165
+ ? Math.min(maxBar, Math.max(1, Math.round((waitMs / maxDurationMs) * maxBar)))
166
+ : 1;
167
+ const waitBar = `${colors.warning('\u2588'.repeat(waitBarLen))}${colors.muted('\u2591'.repeat(maxBar - waitBarLen))}`;
168
+ lines.push(`${waitLabel} ${waitBar} ${waitSecs}s`);
169
+ }
170
+ else if (isLast && isWaiting && t.waitingApprovalAt) {
171
+ const waitStart = t.waitingApprovalAt instanceof Date
172
+ ? t.waitingApprovalAt.getTime()
173
+ : Number(t.waitingApprovalAt);
174
+ const waitElapsedMs = Math.max(0, Date.now() - waitStart);
175
+ const waitSecs = (waitElapsedMs / 1000).toFixed(1);
176
+ const waitLabel = ' \u21b3 approval'.padEnd(16);
177
+ const waitBarLen = maxDurationMs > 0
178
+ ? Math.min(maxBar, Math.max(1, Math.round((waitElapsedMs / maxDurationMs) * maxBar)))
179
+ : 1;
180
+ const waitBar = `${colors.warning('\u2588'.repeat(waitBarLen))}${colors.muted('\u2591'.repeat(maxBar - waitBarLen))}`;
181
+ lines.push(`${waitLabel} ${waitBar} ${waitSecs}s (waiting)`);
182
+ }
183
+ return lines;
184
+ }
185
+ /**
186
+ * Render the full phase timing section, grouped by agent run with lifecycle events.
187
+ */
188
+ function renderPhaseTimings(timings, currentRun) {
189
+ const groups = groupTimingsByRun(timings);
190
+ const multiRun = groups.length > 1;
191
+ // Compute max duration across ALL timings for consistent bar scaling
192
+ const nodeTimings = timings.filter((t) => !isLifecycleEvent(t.phase));
193
+ const maxDurationMs = Math.max(...nodeTimings.map((t) => (t.durationMs != null ? Number(t.durationMs) : 0)), 0);
194
+ const MAX_BAR = 20;
195
+ const lines = [];
196
+ for (const [groupIdx, group] of groups.entries()) {
197
+ const isCurrentRun = group.runId === currentRun?.id;
198
+ const isLastGroup = groupIdx === groups.length - 1;
199
+ // Run header for multi-run display
200
+ if (multiRun) {
201
+ if (groupIdx > 0)
202
+ lines.push('');
203
+ const runLabel = `Run #${groupIdx + 1}`;
204
+ // Check if this run started with a resume event
205
+ const firstEvent = group.timings[0];
206
+ const isResumed = firstEvent && firstEvent.phase === 'run:resumed';
207
+ const suffix = isResumed ? ' (resumed)' : '';
208
+ lines.push(colors.muted(` ${runLabel}${suffix}`));
209
+ }
210
+ // Determine if this run is in a terminal state (for the crashed bar display)
211
+ const isCrashed = isCurrentRun &&
212
+ currentRun &&
213
+ (currentRun.status === 'running' || currentRun.status === 'pending') &&
214
+ currentRun.pid != null &&
215
+ !isProcessAlive(currentRun.pid);
216
+ // For non-current runs, they are always terminal (otherwise they'd be current)
217
+ const isRunTerminal = !isCurrentRun ||
218
+ isCrashed ||
219
+ currentRun?.status === 'interrupted' ||
220
+ currentRun?.status === 'failed' ||
221
+ currentRun?.status === 'cancelled';
222
+ const isWaiting = isCurrentRun && currentRun?.status === 'waiting_approval';
223
+ for (const [idx, t] of group.timings.entries()) {
224
+ const isLast = isLastGroup && idx === group.timings.length - 1;
225
+ // Lifecycle event - render as a marker line
226
+ if (isLifecycleEvent(t.phase)) {
227
+ const event = LIFECYCLE_EVENTS[t.phase];
228
+ if (event) {
229
+ const sym = t.phase === 'run:completed'
230
+ ? symbols.success
231
+ : t.phase === 'run:failed' || t.phase === 'run:crashed'
232
+ ? symbols.error
233
+ : t.phase === 'run:stopped'
234
+ ? symbols.warning
235
+ : symbols.info;
236
+ lines.push(` ${event.color(`${sym} ${event.label}`)}`);
237
+ }
238
+ continue;
239
+ }
240
+ // Regular node phase
241
+ const rendered = renderNodeTiming(t, isLast, isWaiting ?? false, isRunTerminal ?? false, isCurrentRun ? currentRun : null, maxDurationMs, MAX_BAR);
242
+ lines.push(...rendered);
243
+ }
244
+ }
245
+ // Summary totals (across all runs, excluding lifecycle events)
246
+ const totalExecMs = nodeTimings.reduce((sum, t) => sum + (t.durationMs != null ? Number(t.durationMs) : 0), 0);
247
+ const totalWaitMs = nodeTimings.reduce((sum, t) => sum + (t.approvalWaitMs != null ? Number(t.approvalWaitMs) : 0), 0);
248
+ if (totalExecMs > 0) {
249
+ lines.push('');
250
+ lines.push(`${'Total execution'.padEnd(16)} ${formatDuration(totalExecMs)}`);
251
+ if (totalWaitMs > 0) {
252
+ lines.push(`${'Total wait'.padEnd(16)} ${formatDuration(totalWaitMs)}`);
253
+ lines.push(`${'Total wall-clock'.padEnd(16)} ${formatDuration(totalExecMs + totalWaitMs)}`);
254
+ }
255
+ }
256
+ return lines;
257
+ }
84
258
  export function createShowCommand() {
85
259
  return new Command('show')
86
260
  .description('Show feature details')
@@ -92,7 +266,8 @@ export function createShowCommand() {
92
266
  const timingRepo = container.resolve('IPhaseTimingRepository');
93
267
  const feature = await useCase.execute(featureId);
94
268
  const run = feature.agentRunId ? await runRepo.findById(feature.agentRunId) : null;
95
- const timings = feature.agentRunId ? await timingRepo.findByRunId(feature.agentRunId) : [];
269
+ // Fetch timings across ALL runs for the feature (not just the current run)
270
+ const timings = await timingRepo.findByFeatureId(feature.id);
96
271
  const worktreePath = feature.worktreePath ?? computeWorktreePath(feature.repositoryPath, feature.branch);
97
272
  const formatTs = (ts) => {
98
273
  if (ts instanceof Date)
@@ -117,78 +292,7 @@ export function createShowCommand() {
117
292
  });
118
293
  }
119
294
  if (timings.length > 0) {
120
- const isWaiting = run?.status === 'waiting_approval';
121
- const maxDurationMs = Math.max(...timings.map((t) => (t.durationMs != null ? Number(t.durationMs) : 0)));
122
- const MAX_BAR = 20;
123
- const lines = [];
124
- for (const [idx, t] of timings.entries()) {
125
- const isLast = idx === timings.length - 1;
126
- const isSubPhase = t.phase.includes(':');
127
- const label = isSubPhase
128
- ? ` \u21b3 ${t.phase.split(':')[1]}`.padEnd(16)
129
- : (NODE_TO_PHASE[t.phase] ?? t.phase).padEnd(16);
130
- // Completed phase - green bar with duration
131
- if (t.completedAt && t.durationMs != null) {
132
- const ms = Number(t.durationMs);
133
- const secs = (ms / 1000).toFixed(1);
134
- const barLen = maxDurationMs > 0
135
- ? Math.min(MAX_BAR, Math.max(1, Math.round((ms / maxDurationMs) * MAX_BAR)))
136
- : 1;
137
- const bar = `${colors.success('\u2588'.repeat(barLen))}${colors.muted('\u2591'.repeat(MAX_BAR - barLen))}`;
138
- lines.push(`${label} ${bar} ${secs}s`);
139
- }
140
- // Waiting for approval - only the LAST timing
141
- else if (isLast && isWaiting) {
142
- lines.push(`${label} ${colors.warning('awaiting review')}`);
143
- }
144
- // Running phase - blue bar with elapsed time
145
- else {
146
- const elapsedMs = Math.max(0, Date.now() - new Date(t.startedAt).getTime());
147
- const secs = (elapsedMs / 1000).toFixed(1);
148
- const barLen = maxDurationMs > 0
149
- ? Math.min(MAX_BAR, Math.max(1, Math.round((elapsedMs / maxDurationMs) * MAX_BAR)))
150
- : 1;
151
- const bar = `${colors.info('\u2588'.repeat(barLen))}${colors.muted('\u2591'.repeat(MAX_BAR - barLen))}`;
152
- lines.push(`${label} ${bar} ${secs}s (running)`);
153
- }
154
- // Show approval wait time under gated phases
155
- if (t.approvalWaitMs != null && Number(t.approvalWaitMs) > 0) {
156
- // Completed approval wait - show final duration
157
- const waitMs = Number(t.approvalWaitMs);
158
- const waitSecs = (waitMs / 1000).toFixed(1);
159
- const waitLabel = ' \u21b3 approval'.padEnd(16);
160
- const waitBarLen = maxDurationMs > 0
161
- ? Math.min(MAX_BAR, Math.max(1, Math.round((waitMs / maxDurationMs) * MAX_BAR)))
162
- : 1;
163
- const waitBar = `${colors.warning('\u2588'.repeat(waitBarLen))}${colors.muted('\u2591'.repeat(MAX_BAR - waitBarLen))}`;
164
- lines.push(`${waitLabel} ${waitBar} ${waitSecs}s`);
165
- }
166
- else if (isLast && isWaiting && t.waitingApprovalAt) {
167
- // Live approval wait - show elapsed time
168
- const waitStart = t.waitingApprovalAt instanceof Date
169
- ? t.waitingApprovalAt.getTime()
170
- : Number(t.waitingApprovalAt);
171
- const waitElapsedMs = Math.max(0, Date.now() - waitStart);
172
- const waitSecs = (waitElapsedMs / 1000).toFixed(1);
173
- const waitLabel = ' \u21b3 approval'.padEnd(16);
174
- const waitBarLen = maxDurationMs > 0
175
- ? Math.min(MAX_BAR, Math.max(1, Math.round((waitElapsedMs / maxDurationMs) * MAX_BAR)))
176
- : 1;
177
- const waitBar = `${colors.warning('\u2588'.repeat(waitBarLen))}${colors.muted('\u2591'.repeat(MAX_BAR - waitBarLen))}`;
178
- lines.push(`${waitLabel} ${waitBar} ${waitSecs}s (waiting)`);
179
- }
180
- }
181
- // Summary totals
182
- const totalExecMs = timings.reduce((sum, t) => sum + (t.durationMs != null ? Number(t.durationMs) : 0), 0);
183
- const totalWaitMs = timings.reduce((sum, t) => sum + (t.approvalWaitMs != null ? Number(t.approvalWaitMs) : 0), 0);
184
- if (totalExecMs > 0) {
185
- lines.push('');
186
- lines.push(`${'Total execution'.padEnd(16)} ${formatDuration(totalExecMs)}`);
187
- if (totalWaitMs > 0) {
188
- lines.push(`${'Total wait'.padEnd(16)} ${formatDuration(totalWaitMs)}`);
189
- lines.push(`${'Total wall-clock'.padEnd(16)} ${formatDuration(totalExecMs + totalWaitMs)}`);
190
- }
191
- }
295
+ const lines = renderPhaseTimings(timings, run);
192
296
  textBlocks.push({ title: 'Phase Timing', content: lines.join('\n') });
193
297
  }
194
298
  if (run?.status === 'waiting_approval') {
@@ -200,8 +304,8 @@ export function createShowCommand() {
200
304
  content: [
201
305
  `${phase} is waiting for your review.`,
202
306
  '',
203
- ` ${colors.accent('shep feat approve')} Resume the agent`,
204
- ` ${colors.accent('shep feat reject')} Cancel with feedback`,
307
+ ` ${colors.accent(`shep feat approve ${feature.id.slice(0, 8)}`)} Resume the agent`,
308
+ ` ${colors.accent(`shep feat reject ${feature.id.slice(0, 8)}`)} Cancel with feedback`,
205
309
  ].join('\n'),
206
310
  });
207
311
  }