@shepai/cli 1.37.0 → 1.37.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/core/src/application/use-cases/agents/stop-agent-run.use-case.d.ts +5 -2
- package/dist/packages/core/src/application/use-cases/agents/stop-agent-run.use-case.d.ts.map +1 -1
- package/dist/packages/core/src/application/use-cases/agents/stop-agent-run.use-case.js +21 -10
- package/dist/packages/core/src/application/use-cases/features/resume-feature.use-case.d.ts.map +1 -1
- package/dist/packages/core/src/application/use-cases/features/resume-feature.use-case.js +11 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.js +11 -3
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/implement.node.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/implement.node.js +2 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge.node.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge.node.js +4 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.js +2 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.d.ts +8 -0
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.js +29 -0
- package/dist/src/presentation/cli/commands/agent/stop.command.d.ts +1 -1
- package/dist/src/presentation/cli/commands/agent/stop.command.js +1 -1
- package/dist/src/presentation/cli/commands/feat/show.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/feat/show.command.js +177 -54
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/cache/.previewinfo +1 -1
- package/web/.next/cache/.rscinfo +1 -1
- package/web/.next/cache/config.json +3 -3
- package/web/.next/fallback-build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +3 -3
- package/web/.next/required-server-files.js +1 -1
- package/web/.next/required-server-files.json +1 -1
- package/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__f12b60be._.js +1 -1
- package/web/.next/server/pages/500.html +2 -2
- package/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__f12b60be._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/src/presentation/web/server.js +1 -1
- package/web/.next/trace +1 -1
- package/web/.next/trace-build +1 -1
- /package/web/.next/static/{zoaB52IxAh-lSl8Qreb0t → AaXDy79mMK8ywoCbOquJB}/_buildManifest.js +0 -0
- /package/web/.next/static/{zoaB52IxAh-lSl8Qreb0t → AaXDy79mMK8ywoCbOquJB}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{zoaB52IxAh-lSl8Qreb0t → AaXDy79mMK8ywoCbOquJB}/_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
|
|
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
|
-
|
|
11
|
+
private readonly phaseTimingRepository;
|
|
12
|
+
constructor(agentRunRepository: IAgentRunRepository, phaseTimingRepository: IPhaseTimingRepository);
|
|
10
13
|
execute(id: string): Promise<{
|
|
11
14
|
stopped: boolean;
|
|
12
15
|
reason: string;
|
package/dist/packages/core/src/application/use-cases/agents/stop-agent-run.use-case.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
44
|
+
// No PID — just mark as interrupted (resumable)
|
|
41
45
|
const now = new Date();
|
|
42
|
-
await this.agentRunRepository.updateStatus(id, AgentRunStatus.
|
|
43
|
-
error: '
|
|
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
|
-
|
|
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.
|
|
69
|
-
error: '
|
|
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
|
|
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
|
-
|
|
94
|
+
__param(1, inject('IPhaseTimingRepository')),
|
|
95
|
+
__metadata("design:paramtypes", [Object, Object])
|
|
85
96
|
], StopAgentRunUseCase);
|
|
86
97
|
export { StopAgentRunUseCase };
|
package/dist/packages/core/src/application/use-cases/features/resume-feature.use-case.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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"}
|
package/dist/packages/core/src/infrastructure/services/agents/feature-agent/feature-agent-worker.js
CHANGED
|
@@ -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,
|
|
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;
|
|
@@ -193,8 +198,6 @@ export async function runWorker(args) {
|
|
|
193
198
|
// Check if graph was interrupted (human-in-the-loop approval needed)
|
|
194
199
|
const interruptPayload = result.__interrupt__;
|
|
195
200
|
if (interruptPayload && interruptPayload.length > 0) {
|
|
196
|
-
// Record approval wait start on the last phase timing
|
|
197
|
-
await recordApprovalWaitStart(getLastTimingId());
|
|
198
201
|
const now = new Date();
|
|
199
202
|
await runRepository.updateStatus(args.runId, AgentRunStatus.waitingApproval, {
|
|
200
203
|
updatedAt: now,
|
|
@@ -210,6 +213,7 @@ export async function runWorker(args) {
|
|
|
210
213
|
completedAt: failedAt,
|
|
211
214
|
updatedAt: failedAt,
|
|
212
215
|
});
|
|
216
|
+
await recordLifecycleEvent('run:failed');
|
|
213
217
|
log(`Run marked as failed: ${result.error}`);
|
|
214
218
|
return;
|
|
215
219
|
}
|
|
@@ -219,6 +223,7 @@ export async function runWorker(args) {
|
|
|
219
223
|
completedAt,
|
|
220
224
|
updatedAt: completedAt,
|
|
221
225
|
});
|
|
226
|
+
await recordLifecycleEvent('run:completed');
|
|
222
227
|
log('Run marked as completed');
|
|
223
228
|
}
|
|
224
229
|
catch (error) {
|
|
@@ -231,6 +236,7 @@ export async function runWorker(args) {
|
|
|
231
236
|
completedAt: failedAt,
|
|
232
237
|
updatedAt: failedAt,
|
|
233
238
|
});
|
|
239
|
+
await recordLifecycleEvent('run:failed');
|
|
234
240
|
log('Run marked as failed');
|
|
235
241
|
}
|
|
236
242
|
}
|
|
@@ -251,6 +257,7 @@ process.on('disconnect', () => {
|
|
|
251
257
|
// Handle SIGTERM for graceful shutdown
|
|
252
258
|
let runIdForSignal;
|
|
253
259
|
let runRepoForSignal;
|
|
260
|
+
let timingRepoForSignal;
|
|
254
261
|
process.on('SIGTERM', async () => {
|
|
255
262
|
log('Received SIGTERM, shutting down...');
|
|
256
263
|
if (runIdForSignal && runRepoForSignal) {
|
|
@@ -260,6 +267,7 @@ process.on('SIGTERM', async () => {
|
|
|
260
267
|
completedAt: now,
|
|
261
268
|
updatedAt: now,
|
|
262
269
|
});
|
|
270
|
+
await recordLifecycleEvent('run:stopped', runIdForSignal, timingRepoForSignal);
|
|
263
271
|
}
|
|
264
272
|
process.exit(0);
|
|
265
273
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"implement.node.d.ts","sourceRoot":"","sources":["../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/implement.node.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+DAA+D,CAAC;AACpG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"implement.node.d.ts","sourceRoot":"","sources":["../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/implement.node.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+DAA+D,CAAC;AACpG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAqErD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,cAAc,IAG5C,OAAO,iBAAiB,KAAG,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAgL7E"}
|
package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/implement.node.js
CHANGED
|
@@ -12,7 +12,7 @@ import { join } from 'node:path';
|
|
|
12
12
|
import { interrupt, isGraphBubbleUp } from '@langchain/langgraph';
|
|
13
13
|
import { createNodeLogger, readSpecFile, buildExecutorOptions, shouldInterrupt, safeYamlLoad, retryExecute, getCompletedPhases, markPhaseComplete, } from './node-helpers.js';
|
|
14
14
|
import { reportNodeStart } from '../heartbeat.js';
|
|
15
|
-
import { recordPhaseStart, recordPhaseEnd } from '../phase-timing-context.js';
|
|
15
|
+
import { recordPhaseStart, recordPhaseEnd, recordApprovalWaitStart, } from '../phase-timing-context.js';
|
|
16
16
|
import { updateNodeLifecycle } from '../lifecycle-context.js';
|
|
17
17
|
import { buildImplementPhasePrompt, } from './prompts/implement.prompt.js';
|
|
18
18
|
/**
|
|
@@ -145,6 +145,7 @@ export function createImplementNode(executor) {
|
|
|
145
145
|
await recordPhaseEnd(implementTimingId, Date.now() - startTime);
|
|
146
146
|
if (shouldInterrupt('implement', state.approvalGates)) {
|
|
147
147
|
log.info('Interrupting for human approval');
|
|
148
|
+
await recordApprovalWaitStart(implementTimingId);
|
|
148
149
|
interrupt({
|
|
149
150
|
node: 'implement',
|
|
150
151
|
message: `Implementation complete: ${totalTasks} tasks across ${totalPhases} phases. Approve to finish.`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge.node.d.ts","sourceRoot":"","sources":["../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge.node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iEAAiE,CAAC;AACrG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yEAAyE,CAAC;
|
|
1
|
+
{"version":3,"file":"merge.node.d.ts","sourceRoot":"","sources":["../../../../../../../../../packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge.node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iEAAiE,CAAC;AACrG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yEAAyE,CAAC;AAWlH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,aAAa,CAAC;IAC5B,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAChF,iBAAiB,EAAE,IAAI,CAAC,kBAAkB,EAAE,UAAU,GAAG,QAAQ,CAAC,CAAC;CACpE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,IAGnC,OAAO,iBAAiB,KAAG,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAwJ7E"}
|
package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/merge.node.js
CHANGED
|
@@ -13,7 +13,7 @@ import { interrupt, isGraphBubbleUp } from '@langchain/langgraph';
|
|
|
13
13
|
import { SdlcLifecycle, PrStatus } from '../../../../../domain/generated/output.js';
|
|
14
14
|
import { createNodeLogger, shouldInterrupt } from './node-helpers.js';
|
|
15
15
|
import { reportNodeStart } from '../heartbeat.js';
|
|
16
|
-
import { recordPhaseStart, recordPhaseEnd } from '../phase-timing-context.js';
|
|
16
|
+
import { recordPhaseStart, recordPhaseEnd, recordApprovalWaitStart, } from '../phase-timing-context.js';
|
|
17
17
|
import { updateNodeLifecycle } from '../lifecycle-context.js';
|
|
18
18
|
/**
|
|
19
19
|
* Factory that creates the merge node function.
|
|
@@ -82,6 +82,9 @@ export function createMergeNode(deps) {
|
|
|
82
82
|
// --- Step 4: Merge approval gate ---
|
|
83
83
|
if (shouldInterrupt('merge', state.approvalGates)) {
|
|
84
84
|
log.info('Interrupting for merge approval');
|
|
85
|
+
// Close timing before interrupt so the pre-approval work has a duration
|
|
86
|
+
await recordPhaseEnd(mergeTimingId, Date.now() - startTime);
|
|
87
|
+
await recordApprovalWaitStart(mergeTimingId);
|
|
85
88
|
const diffSummary = await gitPrService.getPrDiffSummary(cwd, baseBranch);
|
|
86
89
|
interrupt({
|
|
87
90
|
node: 'merge',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-helpers.d.ts","sourceRoot":"","sources":["../../../../../../../../../packages/core/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;
|
|
1
|
+
{"version":3,"file":"node-helpers.d.ts","sourceRoot":"","sources":["../../../../../../../../../packages/core/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;AASrD;;;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;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,SAAS,GAAG,OAAO,CAS3F;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,CAiFnE"}
|
package/dist/packages/core/src/infrastructure/services/agents/feature-agent/nodes/node-helpers.js
CHANGED
|
@@ -9,7 +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
|
+
import { recordPhaseStart, recordPhaseEnd, recordApprovalWaitStart, } from '../phase-timing-context.js';
|
|
13
13
|
import { updateNodeLifecycle } from '../lifecycle-context.js';
|
|
14
14
|
/**
|
|
15
15
|
* Create a scoped logger that prefixes messages with the node name.
|
|
@@ -245,6 +245,7 @@ export function executeNode(nodeName, executor, buildPrompt) {
|
|
|
245
245
|
// Human-in-the-loop: interrupt after node execution for review
|
|
246
246
|
if (shouldInterrupt(nodeName, state.approvalGates)) {
|
|
247
247
|
log.info('Interrupting for human approval');
|
|
248
|
+
await recordApprovalWaitStart(timingId);
|
|
248
249
|
interrupt({
|
|
249
250
|
node: nodeName,
|
|
250
251
|
result: result.result.slice(0, 500),
|
|
@@ -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"}
|
package/dist/packages/core/src/infrastructure/services/agents/feature-agent/phase-timing-context.js
CHANGED
|
@@ -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 +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;
|
|
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
|
-
|
|
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,59 +292,7 @@ export function createShowCommand() {
|
|
|
117
292
|
});
|
|
118
293
|
}
|
|
119
294
|
if (timings.length > 0) {
|
|
120
|
-
const
|
|
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 ? Math.max(1, Math.round((ms / maxDurationMs) * MAX_BAR)) : 1;
|
|
135
|
-
const bar = `${colors.success('\u2588'.repeat(barLen))}${colors.muted('\u2591'.repeat(MAX_BAR - barLen))}`;
|
|
136
|
-
lines.push(`${label} ${bar} ${secs}s`);
|
|
137
|
-
}
|
|
138
|
-
// Waiting for approval - only the LAST timing
|
|
139
|
-
else if (isLast && isWaiting) {
|
|
140
|
-
lines.push(`${label} ${colors.warning('awaiting review')}`);
|
|
141
|
-
}
|
|
142
|
-
// Running phase - blue bar with elapsed time
|
|
143
|
-
else {
|
|
144
|
-
const elapsedMs = Math.max(0, Date.now() - new Date(t.startedAt).getTime());
|
|
145
|
-
const secs = (elapsedMs / 1000).toFixed(1);
|
|
146
|
-
const barLen = maxDurationMs > 0
|
|
147
|
-
? Math.min(MAX_BAR, Math.max(1, Math.round((elapsedMs / maxDurationMs) * MAX_BAR)))
|
|
148
|
-
: 1;
|
|
149
|
-
const bar = `${colors.info('\u2588'.repeat(barLen))}${colors.muted('\u2591'.repeat(MAX_BAR - barLen))}`;
|
|
150
|
-
lines.push(`${label} ${bar} ${secs}s (running)`);
|
|
151
|
-
}
|
|
152
|
-
// Show approval wait time under gated phases
|
|
153
|
-
if (t.approvalWaitMs != null && Number(t.approvalWaitMs) > 0) {
|
|
154
|
-
const waitMs = Number(t.approvalWaitMs);
|
|
155
|
-
const waitSecs = (waitMs / 1000).toFixed(1);
|
|
156
|
-
const waitLabel = ' \u21b3 approval'.padEnd(16);
|
|
157
|
-
const waitBarLen = maxDurationMs > 0 ? Math.max(1, Math.round((waitMs / maxDurationMs) * MAX_BAR)) : 1;
|
|
158
|
-
const waitBar = `${colors.warning('\u2588'.repeat(waitBarLen))}${colors.muted('\u2591'.repeat(MAX_BAR - waitBarLen))}`;
|
|
159
|
-
lines.push(`${waitLabel} ${waitBar} ${waitSecs}s`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// Summary totals
|
|
163
|
-
const totalExecMs = timings.reduce((sum, t) => sum + (t.durationMs != null ? Number(t.durationMs) : 0), 0);
|
|
164
|
-
const totalWaitMs = timings.reduce((sum, t) => sum + (t.approvalWaitMs != null ? Number(t.approvalWaitMs) : 0), 0);
|
|
165
|
-
if (totalExecMs > 0) {
|
|
166
|
-
lines.push('');
|
|
167
|
-
lines.push(`${'Total execution'.padEnd(16)} ${formatDuration(totalExecMs)}`);
|
|
168
|
-
if (totalWaitMs > 0) {
|
|
169
|
-
lines.push(`${'Total wait'.padEnd(16)} ${formatDuration(totalWaitMs)}`);
|
|
170
|
-
lines.push(`${'Total wall-clock'.padEnd(16)} ${formatDuration(totalExecMs + totalWaitMs)}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
295
|
+
const lines = renderPhaseTimings(timings, run);
|
|
173
296
|
textBlocks.push({ title: 'Phase Timing', content: lines.join('\n') });
|
|
174
297
|
}
|
|
175
298
|
if (run?.status === 'waiting_approval') {
|