gsd-pi 2.31.2 → 2.32.0-dev.3d7932c
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/README.md +27 -20
- package/dist/cli.js +5 -5
- package/dist/resource-loader.js +13 -3
- package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +23 -27
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/dist/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/dist/resources/extensions/gsd/auto-idempotency.ts +3 -2
- package/dist/resources/extensions/gsd/auto-observability.ts +2 -4
- package/dist/resources/extensions/gsd/auto-post-unit.ts +32 -37
- package/dist/resources/extensions/gsd/auto-prompts.ts +84 -78
- package/dist/resources/extensions/gsd/auto-recovery.ts +8 -22
- package/dist/resources/extensions/gsd/auto-start.ts +16 -12
- package/dist/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
- package/dist/resources/extensions/gsd/auto-timers.ts +3 -2
- package/dist/resources/extensions/gsd/auto-verification.ts +6 -6
- package/dist/resources/extensions/gsd/auto-worktree.ts +5 -4
- package/dist/resources/extensions/gsd/auto.ts +82 -60
- package/dist/resources/extensions/gsd/commands-inspect.ts +2 -1
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
- package/dist/resources/extensions/gsd/commands.ts +19 -0
- package/dist/resources/extensions/gsd/complexity-classifier.ts +5 -7
- package/dist/resources/extensions/gsd/crash-recovery.ts +15 -2
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +2 -1
- package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
- package/dist/resources/extensions/gsd/doctor.ts +6 -0
- package/dist/resources/extensions/gsd/error-utils.ts +6 -0
- package/dist/resources/extensions/gsd/export.ts +2 -1
- package/dist/resources/extensions/gsd/git-service.ts +12 -2
- package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/dist/resources/extensions/gsd/guided-flow.ts +3 -2
- package/dist/resources/extensions/gsd/health-widget.ts +167 -0
- package/dist/resources/extensions/gsd/index.ts +18 -5
- package/dist/resources/extensions/gsd/key-manager.ts +2 -1
- package/dist/resources/extensions/gsd/marketplace-discovery.ts +4 -3
- package/dist/resources/extensions/gsd/metrics.ts +3 -3
- package/dist/resources/extensions/gsd/migrate-external.ts +21 -4
- package/dist/resources/extensions/gsd/milestone-ids.ts +2 -1
- package/dist/resources/extensions/gsd/native-git-bridge.ts +2 -1
- package/dist/resources/extensions/gsd/parallel-merge.ts +2 -1
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +8 -9
- package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
- package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/dist/resources/extensions/gsd/progress-score.ts +273 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/dist/resources/extensions/gsd/quick.ts +61 -8
- package/dist/resources/extensions/gsd/repo-identity.ts +22 -1
- package/dist/resources/extensions/gsd/session-lock.ts +12 -1
- package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/dist/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/dist/resources/extensions/gsd/undo.ts +5 -7
- package/dist/resources/extensions/gsd/unit-id.ts +14 -0
- package/dist/resources/extensions/gsd/unit-runtime.ts +2 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
- package/dist/resources/extensions/gsd/worktree-command.ts +8 -7
- package/dist/worktree-cli.d.ts +42 -6
- package/dist/worktree-cli.js +88 -48
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-constants.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +23 -27
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/src/resources/extensions/gsd/auto-idempotency.ts +3 -2
- package/src/resources/extensions/gsd/auto-observability.ts +2 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +32 -37
- package/src/resources/extensions/gsd/auto-prompts.ts +84 -78
- package/src/resources/extensions/gsd/auto-recovery.ts +8 -22
- package/src/resources/extensions/gsd/auto-start.ts +16 -12
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
- package/src/resources/extensions/gsd/auto-timers.ts +3 -2
- package/src/resources/extensions/gsd/auto-verification.ts +6 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +5 -4
- package/src/resources/extensions/gsd/auto.ts +82 -60
- package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
- package/src/resources/extensions/gsd/commands.ts +19 -0
- package/src/resources/extensions/gsd/complexity-classifier.ts +5 -7
- package/src/resources/extensions/gsd/crash-recovery.ts +15 -2
- package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +2 -1
- package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/src/resources/extensions/gsd/doctor-types.ts +14 -1
- package/src/resources/extensions/gsd/doctor.ts +6 -0
- package/src/resources/extensions/gsd/error-utils.ts +6 -0
- package/src/resources/extensions/gsd/export.ts +2 -1
- package/src/resources/extensions/gsd/git-service.ts +12 -2
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/src/resources/extensions/gsd/guided-flow.ts +3 -2
- package/src/resources/extensions/gsd/health-widget.ts +167 -0
- package/src/resources/extensions/gsd/index.ts +18 -5
- package/src/resources/extensions/gsd/key-manager.ts +2 -1
- package/src/resources/extensions/gsd/marketplace-discovery.ts +4 -3
- package/src/resources/extensions/gsd/metrics.ts +3 -3
- package/src/resources/extensions/gsd/migrate-external.ts +21 -4
- package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +2 -1
- package/src/resources/extensions/gsd/parallel-merge.ts +2 -1
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +8 -9
- package/src/resources/extensions/gsd/preferences-types.ts +8 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/src/resources/extensions/gsd/progress-score.ts +273 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/src/resources/extensions/gsd/quick.ts +61 -8
- package/src/resources/extensions/gsd/repo-identity.ts +22 -1
- package/src/resources/extensions/gsd/session-lock.ts +12 -1
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/src/resources/extensions/gsd/undo.ts +5 -7
- package/src/resources/extensions/gsd/unit-id.ts +14 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +2 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
- package/src/resources/extensions/gsd/worktree-command.ts +8 -7
|
@@ -64,6 +64,7 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
|
|
|
64
64
|
import { toPosixPath } from "../shared/mod.js";
|
|
65
65
|
import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
|
|
66
66
|
import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
|
|
67
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
70
|
* Ensure the GSD database is available, auto-initializing if needed.
|
|
@@ -374,7 +375,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
374
375
|
details: { operation: "save_decision", id },
|
|
375
376
|
};
|
|
376
377
|
} catch (err) {
|
|
377
|
-
const msg =
|
|
378
|
+
const msg = getErrorMessage(err);
|
|
378
379
|
process.stderr.write(`gsd-db: gsd_save_decision tool failed: ${msg}\n`);
|
|
379
380
|
return {
|
|
380
381
|
content: [{ type: "text" as const, text: `Error saving decision: ${msg}` }],
|
|
@@ -445,7 +446,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
445
446
|
details: { operation: "update_requirement", id: params.id },
|
|
446
447
|
};
|
|
447
448
|
} catch (err) {
|
|
448
|
-
const msg =
|
|
449
|
+
const msg = getErrorMessage(err);
|
|
449
450
|
process.stderr.write(`gsd-db: gsd_update_requirement tool failed: ${msg}\n`);
|
|
450
451
|
return {
|
|
451
452
|
content: [{ type: "text" as const, text: `Error updating requirement: ${msg}` }],
|
|
@@ -525,7 +526,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
525
526
|
details: { operation: "save_summary", path: relativePath, artifact_type: params.artifact_type },
|
|
526
527
|
};
|
|
527
528
|
} catch (err) {
|
|
528
|
-
const msg =
|
|
529
|
+
const msg = getErrorMessage(err);
|
|
529
530
|
process.stderr.write(`gsd-db: gsd_save_summary tool failed: ${msg}\n`);
|
|
530
531
|
return {
|
|
531
532
|
content: [{ type: "text" as const, text: `Error saving artifact: ${msg}` }],
|
|
@@ -574,7 +575,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
574
575
|
details: { operation: "generate_milestone_id", id: newId, existingCount: existingIds.length, reservedCount: reservedMilestoneIds.size, uniqueEnabled },
|
|
575
576
|
};
|
|
576
577
|
} catch (err) {
|
|
577
|
-
const msg =
|
|
578
|
+
const msg = getErrorMessage(err);
|
|
578
579
|
return {
|
|
579
580
|
content: [{ type: "text" as const, text: `Error generating milestone ID: ${msg}` }],
|
|
580
581
|
isError: true,
|
|
@@ -607,6 +608,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
607
608
|
// Load tool API keys from auth.json into environment
|
|
608
609
|
loadToolApiKeys();
|
|
609
610
|
|
|
611
|
+
// Always-on health widget — ambient system health signal below the editor
|
|
612
|
+
try {
|
|
613
|
+
const { initHealthWidget } = await import("./health-widget.js");
|
|
614
|
+
initHealthWidget(ctx);
|
|
615
|
+
} catch { /* non-fatal — widget is best-effort */ }
|
|
616
|
+
|
|
610
617
|
// Notify remote questions status if configured
|
|
611
618
|
try {
|
|
612
619
|
const [{ getRemoteConfigStatus }, { getLatestPromptSummary }] = await Promise.all([
|
|
@@ -789,6 +796,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
789
796
|
|
|
790
797
|
// ── agent_end: auto-mode advancement or auto-start after discuss ───────────
|
|
791
798
|
pi.on("agent_end", async (event, ctx: ExtensionContext) => {
|
|
799
|
+
// Clean up quick-task branch if one just completed (#1269)
|
|
800
|
+
try {
|
|
801
|
+
const { cleanupQuickBranch } = await import("./quick.js");
|
|
802
|
+
cleanupQuickBranch();
|
|
803
|
+
} catch { /* non-fatal */ }
|
|
804
|
+
|
|
792
805
|
// If discuss phase just finished, start auto-mode
|
|
793
806
|
if (checkAutoStartAfterDiscuss()) {
|
|
794
807
|
depthVerifiedMilestones.clear();
|
|
@@ -981,7 +994,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
981
994
|
} catch (err) {
|
|
982
995
|
// Safety net: if handleAgentEnd throws despite its internal try-catch,
|
|
983
996
|
// ensure auto-mode stops gracefully instead of silently stalling (#381).
|
|
984
|
-
const message =
|
|
997
|
+
const message = getErrorMessage(err);
|
|
985
998
|
ctx.ui.notify(
|
|
986
999
|
`Auto-mode error in agent_end handler: ${message}. Stopping auto-mode.`,
|
|
987
1000
|
"error",
|
|
@@ -16,6 +16,7 @@ import { getEnvApiKey } from "@gsd/pi-ai";
|
|
|
16
16
|
import { existsSync, statSync, chmodSync } from "node:fs";
|
|
17
17
|
import { join, dirname } from "node:path";
|
|
18
18
|
import { mkdirSync } from "node:fs";
|
|
19
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
19
20
|
|
|
20
21
|
// ─── Provider Registry ─────────────────────────────────────────────────────────
|
|
21
22
|
|
|
@@ -552,7 +553,7 @@ export async function testProviderKey(
|
|
|
552
553
|
return { provider, status: "error", message: `HTTP ${res.status}`, latencyMs };
|
|
553
554
|
} catch (err) {
|
|
554
555
|
const latencyMs = Date.now() - start;
|
|
555
|
-
const msg =
|
|
556
|
+
const msg = getErrorMessage(err);
|
|
556
557
|
if (msg.includes("timeout") || msg.includes("AbortError")) {
|
|
557
558
|
return { provider, status: "error", message: "timeout (15s)", latencyMs };
|
|
558
559
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import * as fs from 'node:fs';
|
|
18
18
|
import * as path from 'node:path';
|
|
19
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
19
20
|
|
|
20
21
|
// ============================================================================
|
|
21
22
|
// Type Definitions
|
|
@@ -194,7 +195,7 @@ export function parseMarketplaceJson(repoRoot: string):
|
|
|
194
195
|
} catch (err) {
|
|
195
196
|
return {
|
|
196
197
|
success: false,
|
|
197
|
-
error: `Failed to read marketplace.json: ${
|
|
198
|
+
error: `Failed to read marketplace.json: ${getErrorMessage(err)}`
|
|
198
199
|
};
|
|
199
200
|
}
|
|
200
201
|
|
|
@@ -204,7 +205,7 @@ export function parseMarketplaceJson(repoRoot: string):
|
|
|
204
205
|
} catch (err) {
|
|
205
206
|
return {
|
|
206
207
|
success: false,
|
|
207
|
-
error: `Failed to parse marketplace.json: ${
|
|
208
|
+
error: `Failed to parse marketplace.json: ${getErrorMessage(err)}`
|
|
208
209
|
};
|
|
209
210
|
}
|
|
210
211
|
|
|
@@ -293,7 +294,7 @@ export function inspectPlugin(
|
|
|
293
294
|
}
|
|
294
295
|
} catch (err) {
|
|
295
296
|
// Fall back to marketplace inline or derived
|
|
296
|
-
result.error = `Failed to parse plugin.json: ${
|
|
297
|
+
result.error = `Failed to parse plugin.json: ${getErrorMessage(err)}`;
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
|
|
@@ -18,6 +18,7 @@ import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
|
18
18
|
import { gsdRoot } from "./paths.js";
|
|
19
19
|
import { getAndClearSkills } from "./skill-telemetry.js";
|
|
20
20
|
import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
21
|
+
import { parseUnitId } from "./unit-id.js";
|
|
21
22
|
|
|
22
23
|
// Re-export from shared — canonical implementation lives in format-utils.
|
|
23
24
|
export { formatTokenCount } from "../shared/mod.js";
|
|
@@ -290,9 +291,8 @@ export function aggregateByPhase(units: UnitMetrics[]): PhaseAggregate[] {
|
|
|
290
291
|
export function aggregateBySlice(units: UnitMetrics[]): SliceAggregate[] {
|
|
291
292
|
const map = new Map<string, SliceAggregate>();
|
|
292
293
|
for (const u of units) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
const sliceId = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
294
|
+
const { milestone, slice } = parseUnitId(u.id);
|
|
295
|
+
const sliceId = slice ? `${milestone}/${slice}` : milestone;
|
|
296
296
|
let agg = map.get(sliceId);
|
|
297
297
|
if (!agg) {
|
|
298
298
|
agg = { sliceId, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { externalGsdRoot } from "./repo-identity.js";
|
|
12
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
12
13
|
|
|
13
14
|
export interface MigrationResult {
|
|
14
15
|
migrated: boolean;
|
|
@@ -47,7 +48,7 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
47
48
|
return { migrated: false, error: ".gsd exists but is not a directory or symlink" };
|
|
48
49
|
}
|
|
49
50
|
} catch (err) {
|
|
50
|
-
return { migrated: false, error: `Cannot stat .gsd: ${
|
|
51
|
+
return { migrated: false, error: `Cannot stat .gsd: ${getErrorMessage(err)}` };
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
const externalPath = externalGsdRoot(basePath);
|
|
@@ -57,8 +58,24 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
57
58
|
// mkdir -p the external dir
|
|
58
59
|
mkdirSync(externalPath, { recursive: true });
|
|
59
60
|
|
|
60
|
-
// Rename .gsd -> .gsd.migrating (atomic lock)
|
|
61
|
-
|
|
61
|
+
// Rename .gsd -> .gsd.migrating (atomic lock).
|
|
62
|
+
// On Windows, NTFS may reject rename with EPERM if file descriptors are
|
|
63
|
+
// open (VS Code watchers, antivirus on-access scan). Fall back to
|
|
64
|
+
// copy+delete (#1292).
|
|
65
|
+
try {
|
|
66
|
+
renameSync(localGsd, migratingPath);
|
|
67
|
+
} catch (renameErr: any) {
|
|
68
|
+
if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY") {
|
|
69
|
+
try {
|
|
70
|
+
cpSync(localGsd, migratingPath, { recursive: true, force: true });
|
|
71
|
+
rmSync(localGsd, { recursive: true, force: true });
|
|
72
|
+
} catch (copyErr) {
|
|
73
|
+
return { migrated: false, error: `Migration rename/copy failed: ${copyErr instanceof Error ? copyErr.message : String(copyErr)}` };
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
throw renameErr;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
62
79
|
|
|
63
80
|
// Copy contents to external dir, skipping worktrees/
|
|
64
81
|
const entries = readdirSync(migratingPath, { withFileTypes: true });
|
|
@@ -98,7 +115,7 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
98
115
|
|
|
99
116
|
return {
|
|
100
117
|
migrated: false,
|
|
101
|
-
error: `Migration failed: ${
|
|
118
|
+
error: `Migration failed: ${getErrorMessage(err)}`,
|
|
102
119
|
};
|
|
103
120
|
}
|
|
104
121
|
}
|
|
@@ -9,6 +9,7 @@ import { randomInt } from "node:crypto";
|
|
|
9
9
|
import { readdirSync, existsSync } from "node:fs";
|
|
10
10
|
import { milestonesDir } from "./paths.js";
|
|
11
11
|
import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js";
|
|
12
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
12
13
|
|
|
13
14
|
// ─── Regex ──────────────────────────────────────────────────────────────────
|
|
14
15
|
|
|
@@ -88,7 +89,7 @@ export function findMilestoneIds(basePath: string): string[] {
|
|
|
88
89
|
} catch (err) {
|
|
89
90
|
// Log why milestone scanning failed — silent [] here causes infinite loops (#456)
|
|
90
91
|
if (existsSync(dir)) {
|
|
91
|
-
console.error(`[gsd] findMilestoneIds: .gsd/milestones/ exists but readdirSync failed — ${
|
|
92
|
+
console.error(`[gsd] findMilestoneIds: .gsd/milestones/ exists but readdirSync failed — ${getErrorMessage(err)}`);
|
|
92
93
|
}
|
|
93
94
|
return [];
|
|
94
95
|
}
|
|
@@ -10,6 +10,7 @@ import { existsSync, readFileSync, unlinkSync, rmSync } from "node:fs";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { GSDError, GSD_GIT_ERROR } from "./errors.js";
|
|
12
12
|
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
13
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
13
14
|
|
|
14
15
|
// Issue #453: keep auto-mode bookkeeping on the stable git CLI path unless a
|
|
15
16
|
// caller explicitly opts into the native helper.
|
|
@@ -716,7 +717,7 @@ export function nativeCommit(
|
|
|
716
717
|
try {
|
|
717
718
|
return native.gitCommit(basePath, message, options?.allowEmpty);
|
|
718
719
|
} catch (e) {
|
|
719
|
-
const msg =
|
|
720
|
+
const msg = getErrorMessage(e);
|
|
720
721
|
if (msg.includes("nothing to commit")) return null;
|
|
721
722
|
throw e;
|
|
722
723
|
}
|
|
@@ -11,6 +11,7 @@ import { mergeMilestoneToMain } from "./auto-worktree.js";
|
|
|
11
11
|
import { MergeConflictError } from "./git-service.js";
|
|
12
12
|
import { removeSessionStatus } from "./session-status-io.js";
|
|
13
13
|
import type { WorkerInfo } from "./parallel-orchestrator.js";
|
|
14
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
14
15
|
|
|
15
16
|
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
16
17
|
|
|
@@ -99,7 +100,7 @@ export async function mergeCompletedMilestone(
|
|
|
99
100
|
return {
|
|
100
101
|
milestoneId,
|
|
101
102
|
success: false,
|
|
102
|
-
error:
|
|
103
|
+
error: getErrorMessage(err),
|
|
103
104
|
};
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
analyzeParallelEligibility,
|
|
39
39
|
type ParallelCandidates,
|
|
40
40
|
} from "./parallel-eligibility.js";
|
|
41
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
41
42
|
|
|
42
43
|
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
43
44
|
|
|
@@ -363,7 +364,7 @@ export async function startParallel(
|
|
|
363
364
|
|
|
364
365
|
started.push(mid);
|
|
365
366
|
} catch (err) {
|
|
366
|
-
const message =
|
|
367
|
+
const message = getErrorMessage(err);
|
|
367
368
|
errors.push({ mid, error: message });
|
|
368
369
|
}
|
|
369
370
|
}
|
|
@@ -15,6 +15,7 @@ import { resolvePostUnitHooks, resolvePreDispatchHooks } from "./preferences.js"
|
|
|
15
15
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { gsdRoot } from "./paths.js";
|
|
18
|
+
import { parseUnitId } from "./unit-id.js";
|
|
18
19
|
|
|
19
20
|
// ─── Hook Queue State ──────────────────────────────────────────────────────
|
|
20
21
|
|
|
@@ -149,7 +150,7 @@ function dequeueNextHook(basePath: string): HookDispatchResult | null {
|
|
|
149
150
|
};
|
|
150
151
|
|
|
151
152
|
// Build the prompt with variable substitution
|
|
152
|
-
const
|
|
153
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(triggerUnitId);
|
|
153
154
|
const prompt = config.prompt
|
|
154
155
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
155
156
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -208,16 +209,14 @@ function handleHookCompletion(basePath: string): HookDispatchResult | null {
|
|
|
208
209
|
* - Milestone-level (M001): .gsd/M001/{artifact}
|
|
209
210
|
*/
|
|
210
211
|
export function resolveHookArtifactPath(basePath: string, unitId: string, artifactName: string): string {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
const [mid, sid, tid] = parts;
|
|
212
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
213
|
+
if (mid && sid && tid) {
|
|
214
214
|
return join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-${artifactName}`);
|
|
215
215
|
}
|
|
216
|
-
if (
|
|
217
|
-
const [mid, sid] = parts;
|
|
216
|
+
if (mid && sid) {
|
|
218
217
|
return join(gsdRoot(basePath), mid, "slices", sid, artifactName);
|
|
219
218
|
}
|
|
220
|
-
return join(gsdRoot(basePath),
|
|
219
|
+
return join(gsdRoot(basePath), mid, artifactName);
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -253,7 +252,7 @@ export function runPreDispatchHooks(
|
|
|
253
252
|
return { action: "proceed", prompt, firedHooks: [] };
|
|
254
253
|
}
|
|
255
254
|
|
|
256
|
-
const
|
|
255
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
257
256
|
const substitute = (text: string): string =>
|
|
258
257
|
text
|
|
259
258
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
@@ -466,7 +465,7 @@ export function triggerHookManually(
|
|
|
466
465
|
activeHook.cycle = currentCycle;
|
|
467
466
|
|
|
468
467
|
// Build the prompt with variable substitution
|
|
469
|
-
const
|
|
468
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
470
469
|
const prompt = hook.prompt
|
|
471
470
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
472
471
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -86,6 +86,14 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
86
86
|
"context_selection",
|
|
87
87
|
]);
|
|
88
88
|
|
|
89
|
+
/** Canonical list of all dispatch unit types. */
|
|
90
|
+
export const KNOWN_UNIT_TYPES = [
|
|
91
|
+
"research-milestone", "plan-milestone", "research-slice", "plan-slice",
|
|
92
|
+
"execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
|
|
93
|
+
"run-uat", "complete-milestone",
|
|
94
|
+
] as const;
|
|
95
|
+
export type UnitType = (typeof KNOWN_UNIT_TYPES)[number];
|
|
96
|
+
|
|
89
97
|
export const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
|
|
90
98
|
|
|
91
99
|
export interface GSDSkillRule {
|
|
@@ -14,6 +14,7 @@ import { normalizeStringArray } from "../shared/mod.js";
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
KNOWN_PREFERENCE_KEYS,
|
|
17
|
+
KNOWN_UNIT_TYPES,
|
|
17
18
|
SKILL_ACTIONS,
|
|
18
19
|
type WorkflowMode,
|
|
19
20
|
type GSDPreferences,
|
|
@@ -239,11 +240,7 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
239
240
|
if (preferences.post_unit_hooks && Array.isArray(preferences.post_unit_hooks)) {
|
|
240
241
|
const validHooks: PostUnitHookConfig[] = [];
|
|
241
242
|
const seenNames = new Set<string>();
|
|
242
|
-
const knownUnitTypes = new Set(
|
|
243
|
-
"research-milestone", "plan-milestone", "research-slice", "plan-slice",
|
|
244
|
-
"execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
|
|
245
|
-
"run-uat", "complete-milestone",
|
|
246
|
-
]);
|
|
243
|
+
const knownUnitTypes = new Set<string>(KNOWN_UNIT_TYPES);
|
|
247
244
|
for (const hook of preferences.post_unit_hooks) {
|
|
248
245
|
if (!hook || typeof hook !== "object") {
|
|
249
246
|
errors.push("post_unit_hooks entry must be an object");
|
|
@@ -305,11 +302,7 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
305
302
|
if (preferences.pre_dispatch_hooks && Array.isArray(preferences.pre_dispatch_hooks)) {
|
|
306
303
|
const validPreHooks: PreDispatchHookConfig[] = [];
|
|
307
304
|
const seenPreNames = new Set<string>();
|
|
308
|
-
const knownUnitTypes = new Set(
|
|
309
|
-
"research-milestone", "plan-milestone", "research-slice", "plan-slice",
|
|
310
|
-
"execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
|
|
311
|
-
"run-uat", "complete-milestone",
|
|
312
|
-
]);
|
|
305
|
+
const knownUnitTypes = new Set<string>(KNOWN_UNIT_TYPES);
|
|
313
306
|
const validActions = new Set(["modify", "skip", "replace"]);
|
|
314
307
|
for (const hook of preferences.pre_dispatch_hooks) {
|
|
315
308
|
if (!hook || typeof hook !== "object") {
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Progress Score — Traffic Light Status Indicator (#1221)
|
|
3
|
+
*
|
|
4
|
+
* Combines existing health signals into a single at-a-glance status:
|
|
5
|
+
* - Green: progressing well
|
|
6
|
+
* - Yellow: struggling (retries, warnings)
|
|
7
|
+
* - Red: stuck (loops, persistent errors, no activity)
|
|
8
|
+
*
|
|
9
|
+
* Purely derived — no stored state. Reads from doctor-proactive health
|
|
10
|
+
* tracking, stuck detection counters, and working-tree activity.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getHealthTrend,
|
|
15
|
+
getConsecutiveErrorUnits,
|
|
16
|
+
getHealthHistory,
|
|
17
|
+
type HealthSnapshot,
|
|
18
|
+
} from "./doctor-proactive.js";
|
|
19
|
+
|
|
20
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export type ProgressLevel = "green" | "yellow" | "red";
|
|
23
|
+
|
|
24
|
+
export interface ProgressScore {
|
|
25
|
+
level: ProgressLevel;
|
|
26
|
+
summary: string;
|
|
27
|
+
signals: ProgressSignal[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProgressSignal {
|
|
31
|
+
name: string;
|
|
32
|
+
level: ProgressLevel;
|
|
33
|
+
detail: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Signal Evaluators ──────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function evaluateHealthTrend(): ProgressSignal {
|
|
39
|
+
const trend = getHealthTrend();
|
|
40
|
+
|
|
41
|
+
switch (trend) {
|
|
42
|
+
case "improving":
|
|
43
|
+
return { name: "health_trend", level: "green", detail: "Health improving" };
|
|
44
|
+
case "stable":
|
|
45
|
+
return { name: "health_trend", level: "green", detail: "Health stable" };
|
|
46
|
+
case "degrading":
|
|
47
|
+
return { name: "health_trend", level: "red", detail: "Health degrading" };
|
|
48
|
+
case "unknown":
|
|
49
|
+
return { name: "health_trend", level: "green", detail: "Insufficient data" };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function evaluateErrorStreak(): ProgressSignal {
|
|
54
|
+
const streak = getConsecutiveErrorUnits();
|
|
55
|
+
|
|
56
|
+
if (streak === 0) {
|
|
57
|
+
return { name: "error_streak", level: "green", detail: "No consecutive errors" };
|
|
58
|
+
}
|
|
59
|
+
if (streak <= 2) {
|
|
60
|
+
return { name: "error_streak", level: "yellow", detail: `${streak} consecutive error unit(s)` };
|
|
61
|
+
}
|
|
62
|
+
return { name: "error_streak", level: "red", detail: `${streak} consecutive error units` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function evaluateRecentErrors(): ProgressSignal {
|
|
66
|
+
const history = getHealthHistory();
|
|
67
|
+
if (history.length === 0) {
|
|
68
|
+
return { name: "recent_errors", level: "green", detail: "No health data yet" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const latest = history[history.length - 1]!;
|
|
72
|
+
|
|
73
|
+
if (latest.errors === 0 && latest.warnings <= 1) {
|
|
74
|
+
return { name: "recent_errors", level: "green", detail: `${latest.errors}E/${latest.warnings}W` };
|
|
75
|
+
}
|
|
76
|
+
if (latest.errors === 0) {
|
|
77
|
+
return { name: "recent_errors", level: "yellow", detail: `${latest.warnings} warning(s)` };
|
|
78
|
+
}
|
|
79
|
+
if (latest.errors <= 2) {
|
|
80
|
+
return { name: "recent_errors", level: "yellow", detail: `${latest.errors} error(s), ${latest.warnings} warning(s)` };
|
|
81
|
+
}
|
|
82
|
+
return { name: "recent_errors", level: "red", detail: `${latest.errors} error(s), ${latest.warnings} warning(s)` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function evaluateArtifactProduction(): ProgressSignal {
|
|
86
|
+
const history = getHealthHistory();
|
|
87
|
+
if (history.length < 2) {
|
|
88
|
+
return { name: "artifact_production", level: "green", detail: "Insufficient data" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const totalFixes = history.reduce((sum, s) => sum + s.fixesApplied, 0);
|
|
92
|
+
const recent = history.slice(-3);
|
|
93
|
+
const recentFixes = recent.reduce((sum, s) => sum + s.fixesApplied, 0);
|
|
94
|
+
|
|
95
|
+
// If recent units are all producing fixes but errors aren't decreasing,
|
|
96
|
+
// doctor is fighting fires but not making headway
|
|
97
|
+
if (recentFixes > 3 && recent.every(s => s.errors > 0)) {
|
|
98
|
+
return { name: "artifact_production", level: "yellow", detail: "Doctor applying fixes but errors persist" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { name: "artifact_production", level: "green", detail: `${totalFixes} total fixes applied` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function evaluateDispatchVelocity(): ProgressSignal {
|
|
105
|
+
const history = getHealthHistory();
|
|
106
|
+
if (history.length < 3) {
|
|
107
|
+
return { name: "dispatch_velocity", level: "green", detail: "Insufficient data" };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check time between recent snapshots — are units completing at a reasonable rate?
|
|
111
|
+
const recent = history.slice(-5);
|
|
112
|
+
if (recent.length < 2) {
|
|
113
|
+
return { name: "dispatch_velocity", level: "green", detail: "Insufficient data" };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const timeDiffs: number[] = [];
|
|
117
|
+
for (let i = 1; i < recent.length; i++) {
|
|
118
|
+
timeDiffs.push(recent[i]!.timestamp - recent[i - 1]!.timestamp);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const avgTimeMs = timeDiffs.reduce((a, b) => a + b, 0) / timeDiffs.length;
|
|
122
|
+
const avgTimeMins = Math.round(avgTimeMs / 60_000);
|
|
123
|
+
|
|
124
|
+
// If average unit time is > 15 minutes, something might be wrong
|
|
125
|
+
if (avgTimeMins > 15) {
|
|
126
|
+
return { name: "dispatch_velocity", level: "yellow", detail: `Units averaging ${avgTimeMins}min each` };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { name: "dispatch_velocity", level: "green", detail: `Units averaging ${avgTimeMins || "<1"}min each` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Main API ───────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Compute the current progress score by evaluating all available signals.
|
|
136
|
+
* Returns a composite score with individual signal details.
|
|
137
|
+
*/
|
|
138
|
+
export function computeProgressScore(): ProgressScore {
|
|
139
|
+
const signals: ProgressSignal[] = [
|
|
140
|
+
evaluateHealthTrend(),
|
|
141
|
+
evaluateErrorStreak(),
|
|
142
|
+
evaluateRecentErrors(),
|
|
143
|
+
evaluateArtifactProduction(),
|
|
144
|
+
evaluateDispatchVelocity(),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
// Overall level: worst of all signals
|
|
148
|
+
const level = signals.some(s => s.level === "red")
|
|
149
|
+
? "red"
|
|
150
|
+
: signals.some(s => s.level === "yellow")
|
|
151
|
+
? "yellow"
|
|
152
|
+
: "green";
|
|
153
|
+
|
|
154
|
+
// Build summary from the most important signals
|
|
155
|
+
const summary = buildSummary(level, signals);
|
|
156
|
+
|
|
157
|
+
return { level, summary, signals };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Compute progress score with additional context from the current unit.
|
|
162
|
+
*/
|
|
163
|
+
export function computeProgressScoreWithContext(context: {
|
|
164
|
+
currentUnitType?: string;
|
|
165
|
+
currentUnitId?: string;
|
|
166
|
+
completedUnits?: number;
|
|
167
|
+
totalUnits?: number;
|
|
168
|
+
retryCount?: number;
|
|
169
|
+
maxRetries?: number;
|
|
170
|
+
}): ProgressScore {
|
|
171
|
+
const base = computeProgressScore();
|
|
172
|
+
|
|
173
|
+
// Add retry signal if available
|
|
174
|
+
if (context.retryCount !== undefined && context.maxRetries !== undefined) {
|
|
175
|
+
const retrySignal: ProgressSignal = context.retryCount === 0
|
|
176
|
+
? { name: "retry_count", level: "green", detail: "No retries" }
|
|
177
|
+
: context.retryCount <= 2
|
|
178
|
+
? { name: "retry_count", level: "yellow", detail: `Retry ${context.retryCount}/${context.maxRetries}` }
|
|
179
|
+
: { name: "retry_count", level: "red", detail: `Retry ${context.retryCount}/${context.maxRetries} — looping` };
|
|
180
|
+
|
|
181
|
+
base.signals.push(retrySignal);
|
|
182
|
+
|
|
183
|
+
// Re-evaluate level
|
|
184
|
+
if (retrySignal.level === "red") base.level = "red";
|
|
185
|
+
else if (retrySignal.level === "yellow" && base.level === "green") base.level = "yellow";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Build richer summary with context
|
|
189
|
+
base.summary = buildSummaryWithContext(base.level, base.signals, context);
|
|
190
|
+
|
|
191
|
+
return base;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Formatting ─────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
function buildSummary(level: ProgressLevel, signals: ProgressSignal[]): string {
|
|
197
|
+
switch (level) {
|
|
198
|
+
case "green":
|
|
199
|
+
return "Progressing well";
|
|
200
|
+
case "yellow": {
|
|
201
|
+
const issues = signals.filter(s => s.level === "yellow").map(s => s.detail);
|
|
202
|
+
return `Struggling — ${issues[0] ?? "minor issues detected"}`;
|
|
203
|
+
}
|
|
204
|
+
case "red": {
|
|
205
|
+
const issues = signals.filter(s => s.level === "red").map(s => s.detail);
|
|
206
|
+
return `Stuck — ${issues[0] ?? "critical issues detected"}`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildSummaryWithContext(
|
|
212
|
+
level: ProgressLevel,
|
|
213
|
+
signals: ProgressSignal[],
|
|
214
|
+
context: {
|
|
215
|
+
currentUnitType?: string;
|
|
216
|
+
currentUnitId?: string;
|
|
217
|
+
completedUnits?: number;
|
|
218
|
+
totalUnits?: number;
|
|
219
|
+
retryCount?: number;
|
|
220
|
+
maxRetries?: number;
|
|
221
|
+
},
|
|
222
|
+
): string {
|
|
223
|
+
const unitLabel = context.currentUnitId
|
|
224
|
+
? ` ${context.currentUnitId}`
|
|
225
|
+
: "";
|
|
226
|
+
const progressLabel = context.completedUnits !== undefined && context.totalUnits !== undefined
|
|
227
|
+
? ` (${context.completedUnits} of ${context.totalUnits} done)`
|
|
228
|
+
: "";
|
|
229
|
+
|
|
230
|
+
switch (level) {
|
|
231
|
+
case "green":
|
|
232
|
+
return `Progressing well —${unitLabel}${progressLabel}`;
|
|
233
|
+
case "yellow": {
|
|
234
|
+
const issues = signals.filter(s => s.level === "yellow").map(s => s.detail);
|
|
235
|
+
const retryInfo = context.retryCount ? `, attempt ${context.retryCount}/${context.maxRetries}` : "";
|
|
236
|
+
return `Struggling —${unitLabel}${retryInfo}${progressLabel ? ` ${progressLabel}` : ""}, ${issues[0] ?? "issues detected"}`;
|
|
237
|
+
}
|
|
238
|
+
case "red": {
|
|
239
|
+
const issues = signals.filter(s => s.level === "red").map(s => s.detail);
|
|
240
|
+
return `Stuck —${unitLabel}${progressLabel ? ` ${progressLabel}` : ""}, ${issues[0] ?? "critical issues"}`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Format progress score as a single-line traffic light for TUI display.
|
|
247
|
+
*/
|
|
248
|
+
export function formatProgressLine(score: ProgressScore): string {
|
|
249
|
+
const icon = score.level === "green" ? "\uD83D\uDFE2"
|
|
250
|
+
: score.level === "yellow" ? "\uD83D\uDFE1"
|
|
251
|
+
: "\uD83D\uDD34";
|
|
252
|
+
return `${icon} ${score.summary}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Format a detailed progress report showing all signals.
|
|
257
|
+
*/
|
|
258
|
+
export function formatProgressReport(score: ProgressScore): string {
|
|
259
|
+
const lines: string[] = [];
|
|
260
|
+
|
|
261
|
+
lines.push(formatProgressLine(score));
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push("Signals:");
|
|
264
|
+
|
|
265
|
+
for (const signal of score.signals) {
|
|
266
|
+
const icon = signal.level === "green" ? "\u2705"
|
|
267
|
+
: signal.level === "yellow" ? "\u26A0\uFE0F"
|
|
268
|
+
: "\uD83D\uDED1";
|
|
269
|
+
lines.push(` ${icon} ${signal.name}: ${signal.detail}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return lines.join("\n");
|
|
273
|
+
}
|