@shepai/cli 1.37.1 → 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 -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 -73
- 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/{UemLwbVtqCGOm4FHNOp05 → AaXDy79mMK8ywoCbOquJB}/_buildManifest.js +0 -0
- /package/web/.next/static/{UemLwbVtqCGOm4FHNOp05 → AaXDy79mMK8ywoCbOquJB}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{UemLwbVtqCGOm4FHNOp05 → 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;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,
|
|
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 } 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"}
|
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,78 +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
|
|
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') {
|