gsd-pi 2.31.2 → 2.32.0-dev.d792ba5
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/resources/extensions/gsd/auto-constants.ts +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +20 -26
- 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-post-unit.ts +27 -32
- package/dist/resources/extensions/gsd/auto-prompts.ts +38 -34
- package/dist/resources/extensions/gsd/auto-start.ts +8 -6
- package/dist/resources/extensions/gsd/auto.ts +54 -33
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
- package/dist/resources/extensions/gsd/commands.ts +19 -0
- 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/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/git-service.ts +9 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/dist/resources/extensions/gsd/health-widget.ts +167 -0
- package/dist/resources/extensions/gsd/index.ts +12 -0
- package/dist/resources/extensions/gsd/migrate-external.ts +18 -2
- 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 +59 -7
- package/dist/resources/extensions/gsd/repo-identity.ts +22 -1
- package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- 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/visualizer-data.ts +60 -2
- package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
- 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 +20 -26
- 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-post-unit.ts +27 -32
- package/src/resources/extensions/gsd/auto-prompts.ts +38 -34
- package/src/resources/extensions/gsd/auto-start.ts +8 -6
- package/src/resources/extensions/gsd/auto.ts +54 -33
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
- package/src/resources/extensions/gsd/commands.ts +19 -0
- 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/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/git-service.ts +9 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/src/resources/extensions/gsd/health-widget.ts +167 -0
- package/src/resources/extensions/gsd/index.ts +12 -0
- package/src/resources/extensions/gsd/migrate-external.ts +18 -2
- 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 +59 -7
- package/src/resources/extensions/gsd/repo-identity.ts +22 -1
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- 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/visualizer-data.ts +60 -2
- package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* progress-score.test.ts — Tests for progress score / traffic light (#1221).
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* - Score computation from health signals
|
|
6
|
+
* - Signal evaluation (trend, error streak, recent errors)
|
|
7
|
+
* - Context-aware scoring (retry counts, unit progress)
|
|
8
|
+
* - Formatting (single-line, detailed report)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
recordHealthSnapshot,
|
|
13
|
+
resetProactiveHealing,
|
|
14
|
+
} from "../doctor-proactive.ts";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
computeProgressScore,
|
|
18
|
+
computeProgressScoreWithContext,
|
|
19
|
+
formatProgressLine,
|
|
20
|
+
formatProgressReport,
|
|
21
|
+
} from "../progress-score.ts";
|
|
22
|
+
|
|
23
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
24
|
+
|
|
25
|
+
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
26
|
+
|
|
27
|
+
async function main(): Promise<void> {
|
|
28
|
+
try {
|
|
29
|
+
// ── Base Score: No Data ─────────────────────────────────────────────
|
|
30
|
+
console.log("\n=== progress: green with no data ===");
|
|
31
|
+
{
|
|
32
|
+
resetProactiveHealing();
|
|
33
|
+
const score = computeProgressScore();
|
|
34
|
+
assertEq(score.level, "green", "green when no data available");
|
|
35
|
+
assertTrue(score.summary.includes("Progressing well"), "summary says progressing");
|
|
36
|
+
assertTrue(score.signals.length > 0, "has signals");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Green: Clean Health Data ────────────────────────────────────────
|
|
40
|
+
console.log("\n=== progress: green with clean health ===");
|
|
41
|
+
{
|
|
42
|
+
resetProactiveHealing();
|
|
43
|
+
for (let i = 0; i < 5; i++) {
|
|
44
|
+
recordHealthSnapshot(0, 0, 0);
|
|
45
|
+
}
|
|
46
|
+
const score = computeProgressScore();
|
|
47
|
+
assertEq(score.level, "green", "green with all clean snapshots");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Yellow: Some Warnings ──────────────────────────────────────────
|
|
51
|
+
console.log("\n=== progress: yellow with error streak ===");
|
|
52
|
+
{
|
|
53
|
+
resetProactiveHealing();
|
|
54
|
+
recordHealthSnapshot(1, 2, 0);
|
|
55
|
+
recordHealthSnapshot(1, 1, 0);
|
|
56
|
+
const score = computeProgressScore();
|
|
57
|
+
assertEq(score.level, "yellow", "yellow with consecutive errors");
|
|
58
|
+
assertTrue(score.summary.includes("Struggling"), "summary says struggling");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Red: Degrading Health ──────────────────────────────────────────
|
|
62
|
+
console.log("\n=== progress: red with degrading trend ===");
|
|
63
|
+
{
|
|
64
|
+
resetProactiveHealing();
|
|
65
|
+
// 5 older clean snapshots
|
|
66
|
+
for (let i = 0; i < 5; i++) {
|
|
67
|
+
recordHealthSnapshot(0, 0, 0);
|
|
68
|
+
}
|
|
69
|
+
// 5 recent error snapshots — triggers degrading trend
|
|
70
|
+
for (let i = 0; i < 5; i++) {
|
|
71
|
+
recordHealthSnapshot(3, 5, 0);
|
|
72
|
+
}
|
|
73
|
+
const score = computeProgressScore();
|
|
74
|
+
assertEq(score.level, "red", "red with degrading trend and persistent errors");
|
|
75
|
+
assertTrue(score.summary.includes("Stuck"), "summary says stuck");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Red: High Error Streak ─────────────────────────────────────────
|
|
79
|
+
console.log("\n=== progress: red with high error streak ===");
|
|
80
|
+
{
|
|
81
|
+
resetProactiveHealing();
|
|
82
|
+
for (let i = 0; i < 4; i++) {
|
|
83
|
+
recordHealthSnapshot(2, 0, 0);
|
|
84
|
+
}
|
|
85
|
+
const score = computeProgressScore();
|
|
86
|
+
assertEq(score.level, "red", "red with 4 consecutive error units");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Context-Aware Scoring ──────────────────────────────────────────
|
|
90
|
+
console.log("\n=== progress: context with retries ===");
|
|
91
|
+
{
|
|
92
|
+
resetProactiveHealing();
|
|
93
|
+
for (let i = 0; i < 3; i++) {
|
|
94
|
+
recordHealthSnapshot(0, 0, 0);
|
|
95
|
+
}
|
|
96
|
+
const score = computeProgressScoreWithContext({
|
|
97
|
+
currentUnitId: "M001/S01/T03",
|
|
98
|
+
completedUnits: 2,
|
|
99
|
+
totalUnits: 5,
|
|
100
|
+
retryCount: 0,
|
|
101
|
+
maxRetries: 5,
|
|
102
|
+
});
|
|
103
|
+
assertEq(score.level, "green", "green with no retries");
|
|
104
|
+
assertTrue(score.summary.includes("M001/S01/T03"), "summary includes unit ID");
|
|
105
|
+
assertTrue(score.summary.includes("2 of 5"), "summary includes progress");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log("\n=== progress: context with high retry count ===");
|
|
109
|
+
{
|
|
110
|
+
resetProactiveHealing();
|
|
111
|
+
for (let i = 0; i < 3; i++) {
|
|
112
|
+
recordHealthSnapshot(0, 0, 0);
|
|
113
|
+
}
|
|
114
|
+
const score = computeProgressScoreWithContext({
|
|
115
|
+
currentUnitId: "M001/S01/T03",
|
|
116
|
+
retryCount: 4,
|
|
117
|
+
maxRetries: 5,
|
|
118
|
+
});
|
|
119
|
+
assertEq(score.level, "red", "red with high retry count");
|
|
120
|
+
assertTrue(score.summary.includes("looping"), "summary mentions looping");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log("\n=== progress: context with moderate retries ===");
|
|
124
|
+
{
|
|
125
|
+
resetProactiveHealing();
|
|
126
|
+
for (let i = 0; i < 3; i++) {
|
|
127
|
+
recordHealthSnapshot(0, 0, 0);
|
|
128
|
+
}
|
|
129
|
+
const score = computeProgressScoreWithContext({
|
|
130
|
+
currentUnitId: "M001/S01/T03",
|
|
131
|
+
retryCount: 1,
|
|
132
|
+
maxRetries: 5,
|
|
133
|
+
});
|
|
134
|
+
assertEq(score.level, "yellow", "yellow with 1 retry");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Formatting ─────────────────────────────────────────────────────
|
|
138
|
+
console.log("\n=== progress: formatProgressLine ===");
|
|
139
|
+
{
|
|
140
|
+
resetProactiveHealing();
|
|
141
|
+
const score = computeProgressScore();
|
|
142
|
+
const line = formatProgressLine(score);
|
|
143
|
+
assertTrue(line.includes("Progressing well"), "line includes summary");
|
|
144
|
+
// Should start with green circle emoji
|
|
145
|
+
assertTrue(line.startsWith("\uD83D\uDFE2"), "starts with green circle");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log("\n=== progress: formatProgressLine yellow ===");
|
|
149
|
+
{
|
|
150
|
+
resetProactiveHealing();
|
|
151
|
+
recordHealthSnapshot(1, 0, 0);
|
|
152
|
+
recordHealthSnapshot(1, 0, 0);
|
|
153
|
+
const score = computeProgressScore();
|
|
154
|
+
const line = formatProgressLine(score);
|
|
155
|
+
assertTrue(line.startsWith("\uD83D\uDFE1"), "starts with yellow circle");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log("\n=== progress: formatProgressReport ===");
|
|
159
|
+
{
|
|
160
|
+
resetProactiveHealing();
|
|
161
|
+
recordHealthSnapshot(0, 1, 0);
|
|
162
|
+
const score = computeProgressScore();
|
|
163
|
+
const detailed = formatProgressReport(score);
|
|
164
|
+
assertTrue(detailed.includes("Signals:"), "report has signals section");
|
|
165
|
+
assertTrue(detailed.includes("health_trend"), "report includes trend signal");
|
|
166
|
+
assertTrue(detailed.includes("error_streak"), "report includes streak signal");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Signal Details ─────────────────────────────────────────────────
|
|
170
|
+
console.log("\n=== progress: signal names are consistent ===");
|
|
171
|
+
{
|
|
172
|
+
resetProactiveHealing();
|
|
173
|
+
recordHealthSnapshot(0, 0, 0);
|
|
174
|
+
const score = computeProgressScore();
|
|
175
|
+
const names = score.signals.map(s => s.name);
|
|
176
|
+
assertTrue(names.includes("health_trend"), "has health_trend signal");
|
|
177
|
+
assertTrue(names.includes("error_streak"), "has error_streak signal");
|
|
178
|
+
assertTrue(names.includes("recent_errors"), "has recent_errors signal");
|
|
179
|
+
assertTrue(names.includes("artifact_production"), "has artifact_production signal");
|
|
180
|
+
assertTrue(names.includes("dispatch_velocity"), "has dispatch_velocity signal");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log("\n=== progress: all signals have valid levels ===");
|
|
184
|
+
{
|
|
185
|
+
resetProactiveHealing();
|
|
186
|
+
for (let i = 0; i < 5; i++) {
|
|
187
|
+
recordHealthSnapshot(1, 1, 1);
|
|
188
|
+
}
|
|
189
|
+
const score = computeProgressScore();
|
|
190
|
+
for (const signal of score.signals) {
|
|
191
|
+
assertTrue(
|
|
192
|
+
signal.level === "green" || signal.level === "yellow" || signal.level === "red",
|
|
193
|
+
`signal ${signal.name} has valid level: ${signal.level}`,
|
|
194
|
+
);
|
|
195
|
+
assertTrue(signal.detail.length > 0, `signal ${signal.name} has non-empty detail`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
} finally {
|
|
200
|
+
resetProactiveHealing();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
report();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
main();
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// (a)–(j) extractUatType classification (17 assertions from T01)
|
|
7
7
|
// (k) run-uat prompt template loading and content integrity (8 assertions)
|
|
8
8
|
// (l) dispatch precondition assertions via resolveSliceFile (4 assertions)
|
|
9
|
-
// (m)
|
|
9
|
+
// (m) non-artifact UAT skip: human-experience UATs are not dispatched (1 assertion)
|
|
10
|
+
// (n) stale replay guard: existing UAT-RESULT never re-dispatches (1 assertion)
|
|
10
11
|
|
|
11
12
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
13
|
import { join, dirname } from 'node:path';
|
|
@@ -254,8 +255,8 @@ async function main(): Promise<void> {
|
|
|
254
255
|
'prompt contains artifact-driven execution language (artifact/execute/run)',
|
|
255
256
|
);
|
|
256
257
|
assertTrue(
|
|
257
|
-
|
|
258
|
-
'prompt
|
|
258
|
+
!/surfaced for human review/i.test(promptResult ?? ''),
|
|
259
|
+
'prompt does not contain "surfaced for human review" (non-artifact UATs are skipped, not dispatched)',
|
|
259
260
|
);
|
|
260
261
|
|
|
261
262
|
// ─── (l) dispatch precondition assertions via resolveSliceFile ────────────
|
|
@@ -310,8 +311,56 @@ async function main(): Promise<void> {
|
|
|
310
311
|
}
|
|
311
312
|
}
|
|
312
313
|
|
|
313
|
-
// ─── (m)
|
|
314
|
-
console.log('\n── (m)
|
|
314
|
+
// ─── (m) non-artifact UATs are skipped (not dispatched) ─────────────────
|
|
315
|
+
console.log('\n── (m) non-artifact UAT skip');
|
|
316
|
+
|
|
317
|
+
{
|
|
318
|
+
const base = createFixtureBase();
|
|
319
|
+
try {
|
|
320
|
+
const roadmapDir = join(base, '.gsd', 'milestones', 'M001');
|
|
321
|
+
mkdirSync(roadmapDir, { recursive: true });
|
|
322
|
+
writeFileSync(
|
|
323
|
+
join(roadmapDir, 'M001-ROADMAP.md'),
|
|
324
|
+
[
|
|
325
|
+
'# M001: Test roadmap',
|
|
326
|
+
'',
|
|
327
|
+
'## Slices',
|
|
328
|
+
'',
|
|
329
|
+
'- [x] **S01: First slice** `risk:low` `depends:[]`',
|
|
330
|
+
'- [ ] **S02: Next slice** `risk:low` `depends:[S01]`',
|
|
331
|
+
'',
|
|
332
|
+
'## Boundary Map',
|
|
333
|
+
'',
|
|
334
|
+
].join('\n'),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// human-experience UAT — should not dispatch
|
|
338
|
+
writeSliceFile(base, 'M001', 'S01', 'UAT', makeUatContent('human-experience'));
|
|
339
|
+
|
|
340
|
+
const state = {
|
|
341
|
+
activeMilestone: { id: 'M001', title: 'Test roadmap' },
|
|
342
|
+
activeSlice: { id: 'S02', title: 'Next slice' },
|
|
343
|
+
activeTask: null,
|
|
344
|
+
phase: 'planning',
|
|
345
|
+
recentDecisions: [],
|
|
346
|
+
blockers: [],
|
|
347
|
+
nextAction: 'Plan S02',
|
|
348
|
+
registry: [],
|
|
349
|
+
} as const;
|
|
350
|
+
|
|
351
|
+
const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
|
|
352
|
+
assertEq(
|
|
353
|
+
result,
|
|
354
|
+
null,
|
|
355
|
+
'human-experience UAT is skipped — auto-mode only dispatches artifact-driven UATs',
|
|
356
|
+
);
|
|
357
|
+
} finally {
|
|
358
|
+
cleanup(base);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ─── (n) existing UAT-RESULT never re-dispatches ──────────────────────
|
|
363
|
+
console.log('\n── (n) stale replay guard');
|
|
315
364
|
|
|
316
365
|
{
|
|
317
366
|
const base = createFixtureBase();
|
|
@@ -334,7 +383,7 @@ async function main(): Promise<void> {
|
|
|
334
383
|
);
|
|
335
384
|
|
|
336
385
|
writeSliceFile(base, 'M001', 'S01', 'UAT', makeUatContent('artifact-driven'));
|
|
337
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict:
|
|
386
|
+
writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: FAIL\n---\n');
|
|
338
387
|
|
|
339
388
|
const state = {
|
|
340
389
|
activeMilestone: { id: 'M001', title: 'Test roadmap' },
|
|
@@ -351,7 +400,7 @@ async function main(): Promise<void> {
|
|
|
351
400
|
assertEq(
|
|
352
401
|
result,
|
|
353
402
|
null,
|
|
354
|
-
'existing UAT-RESULT with
|
|
403
|
+
'existing UAT-RESULT with FAIL verdict does not re-dispatch; verdict gate owns blocking',
|
|
355
404
|
);
|
|
356
405
|
} finally {
|
|
357
406
|
cleanup(base);
|
|
@@ -60,6 +60,9 @@ function makeVisualizerData(overrides: Partial<VisualizerData> = {}): Visualizer
|
|
|
60
60
|
toolCalls: 0,
|
|
61
61
|
assistantMessages: 0,
|
|
62
62
|
userMessages: 0,
|
|
63
|
+
providers: [],
|
|
64
|
+
skillSummary: { total: 0, warningCount: 0, criticalCount: 0, topIssue: null },
|
|
65
|
+
environmentIssues: [],
|
|
63
66
|
},
|
|
64
67
|
discussion: [],
|
|
65
68
|
stats: {
|
|
@@ -503,6 +506,9 @@ console.log("\n=== renderAgentView ===");
|
|
|
503
506
|
truncationRate: 15.5, continueHereRate: 5.0,
|
|
504
507
|
tierBreakdown: [], tierSavingsLine: "",
|
|
505
508
|
toolCalls: 20, assistantMessages: 15, userMessages: 8,
|
|
509
|
+
providers: [],
|
|
510
|
+
skillSummary: { total: 0, warningCount: 0, criticalCount: 0, topIssue: null },
|
|
511
|
+
environmentIssues: [],
|
|
506
512
|
},
|
|
507
513
|
captures: { entries: [], pendingCount: 3, totalCount: 5 },
|
|
508
514
|
});
|
|
@@ -669,6 +675,9 @@ console.log("\n=== renderHealthView ===");
|
|
|
669
675
|
toolCalls: 50,
|
|
670
676
|
assistantMessages: 30,
|
|
671
677
|
userMessages: 15,
|
|
678
|
+
providers: [],
|
|
679
|
+
skillSummary: { total: 0, warningCount: 0, criticalCount: 0, topIssue: null },
|
|
680
|
+
environmentIssues: [],
|
|
672
681
|
},
|
|
673
682
|
});
|
|
674
683
|
|
|
@@ -693,6 +702,9 @@ console.log("\n=== renderHealthView ===");
|
|
|
693
702
|
truncationRate: 0, continueHereRate: 0,
|
|
694
703
|
tierBreakdown: [], tierSavingsLine: "",
|
|
695
704
|
toolCalls: 0, assistantMessages: 0, userMessages: 0,
|
|
705
|
+
providers: [],
|
|
706
|
+
skillSummary: { total: 0, warningCount: 0, criticalCount: 0, topIssue: null },
|
|
707
|
+
environmentIssues: [],
|
|
696
708
|
},
|
|
697
709
|
});
|
|
698
710
|
|
|
@@ -18,6 +18,9 @@ import {
|
|
|
18
18
|
} from './metrics.js';
|
|
19
19
|
import { loadAllCaptures, countPendingCaptures } from './captures.js';
|
|
20
20
|
import { loadEffectiveGSDPreferences } from './preferences.js';
|
|
21
|
+
import { runProviderChecks, type ProviderCheckResult } from './doctor-providers.js';
|
|
22
|
+
import { generateSkillHealthReport } from './skill-health.js';
|
|
23
|
+
import { runEnvironmentChecks, type EnvironmentCheckResult } from './doctor-environment.js';
|
|
21
24
|
|
|
22
25
|
import type { Phase } from './types.js';
|
|
23
26
|
import type { CaptureEntry } from './captures.js';
|
|
@@ -142,6 +145,22 @@ export interface CapturesInfo {
|
|
|
142
145
|
totalCount: number;
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
export interface ProviderStatusSummary {
|
|
149
|
+
name: string;
|
|
150
|
+
label: string;
|
|
151
|
+
category: string;
|
|
152
|
+
ok: boolean;
|
|
153
|
+
required: boolean;
|
|
154
|
+
message: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface SkillSummaryInfo {
|
|
158
|
+
total: number;
|
|
159
|
+
warningCount: number;
|
|
160
|
+
criticalCount: number;
|
|
161
|
+
topIssue: string | null;
|
|
162
|
+
}
|
|
163
|
+
|
|
145
164
|
export interface HealthInfo {
|
|
146
165
|
budgetCeiling: number | undefined;
|
|
147
166
|
tokenProfile: string;
|
|
@@ -152,6 +171,9 @@ export interface HealthInfo {
|
|
|
152
171
|
toolCalls: number;
|
|
153
172
|
assistantMessages: number;
|
|
154
173
|
userMessages: number;
|
|
174
|
+
providers: ProviderStatusSummary[];
|
|
175
|
+
skillSummary: SkillSummaryInfo;
|
|
176
|
+
environmentIssues: import("./doctor-environment.js").EnvironmentCheckResult[];
|
|
155
177
|
}
|
|
156
178
|
|
|
157
179
|
export interface VisualizerData {
|
|
@@ -538,7 +560,7 @@ function loadKnowledge(basePath: string): KnowledgeInfo {
|
|
|
538
560
|
|
|
539
561
|
// ─── Health Loader ────────────────────────────────────────────────────────────
|
|
540
562
|
|
|
541
|
-
function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null): HealthInfo {
|
|
563
|
+
function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null, basePath: string): HealthInfo {
|
|
542
564
|
const prefs = loadEffectiveGSDPreferences();
|
|
543
565
|
const budgetCeiling = prefs?.preferences?.budget_ceiling;
|
|
544
566
|
const tokenProfile = prefs?.preferences?.token_profile ?? 'standard';
|
|
@@ -553,6 +575,39 @@ function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null): HealthI
|
|
|
553
575
|
const tierBreakdown = aggregateByTier(units);
|
|
554
576
|
const tierSavingsLine = formatTierSavings(units);
|
|
555
577
|
|
|
578
|
+
// Provider checks — fast (auth.json + env vars only, no network)
|
|
579
|
+
let providers: ProviderStatusSummary[] = [];
|
|
580
|
+
try {
|
|
581
|
+
providers = runProviderChecks().map((r: ProviderCheckResult) => ({
|
|
582
|
+
name: r.name,
|
|
583
|
+
label: r.label,
|
|
584
|
+
category: r.category,
|
|
585
|
+
ok: r.status === "ok" || r.status === "unconfigured",
|
|
586
|
+
required: r.required,
|
|
587
|
+
message: r.message,
|
|
588
|
+
}));
|
|
589
|
+
} catch { /* non-fatal */ }
|
|
590
|
+
|
|
591
|
+
// Skill health summary
|
|
592
|
+
let skillSummary: SkillSummaryInfo = { total: 0, warningCount: 0, criticalCount: 0, topIssue: null };
|
|
593
|
+
try {
|
|
594
|
+
const report = generateSkillHealthReport(basePath);
|
|
595
|
+
const warnings = report.suggestions.filter(s => s.severity === "warning");
|
|
596
|
+
const criticals = report.suggestions.filter(s => s.severity === "critical");
|
|
597
|
+
skillSummary = {
|
|
598
|
+
total: report.skills.length,
|
|
599
|
+
warningCount: warnings.length,
|
|
600
|
+
criticalCount: criticals.length,
|
|
601
|
+
topIssue: report.suggestions[0]?.message ?? null,
|
|
602
|
+
};
|
|
603
|
+
} catch { /* non-fatal */ }
|
|
604
|
+
|
|
605
|
+
// Environment issues (from doctor-environment.ts, #1221)
|
|
606
|
+
let environmentIssues: EnvironmentCheckResult[] = [];
|
|
607
|
+
try {
|
|
608
|
+
environmentIssues = runEnvironmentChecks(basePath).filter(r => r.status !== "ok");
|
|
609
|
+
} catch { /* non-fatal */ }
|
|
610
|
+
|
|
556
611
|
return {
|
|
557
612
|
budgetCeiling,
|
|
558
613
|
tokenProfile,
|
|
@@ -563,6 +618,9 @@ function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null): HealthI
|
|
|
563
618
|
toolCalls: totals?.toolCalls ?? 0,
|
|
564
619
|
assistantMessages: totals?.assistantMessages ?? 0,
|
|
565
620
|
userMessages: totals?.userMessages ?? 0,
|
|
621
|
+
providers,
|
|
622
|
+
skillSummary,
|
|
623
|
+
environmentIssues,
|
|
566
624
|
};
|
|
567
625
|
}
|
|
568
626
|
|
|
@@ -780,7 +838,7 @@ export async function loadVisualizerData(basePath: string): Promise<VisualizerDa
|
|
|
780
838
|
totalCount: allCaptures.length,
|
|
781
839
|
};
|
|
782
840
|
|
|
783
|
-
const health = loadHealth(units, totals);
|
|
841
|
+
const health = loadHealth(units, totals, basePath);
|
|
784
842
|
const stats = buildVisualizerStats(milestones, changelog.entries);
|
|
785
843
|
const discussion = loadDiscussionState(basePath, milestones);
|
|
786
844
|
|
|
@@ -1113,5 +1113,59 @@ export function renderHealthView(
|
|
|
1113
1113
|
lines.push(` Tool calls: ${th.fg("text", String(health.toolCalls))}`);
|
|
1114
1114
|
lines.push(` Messages: ${th.fg("text", String(health.assistantMessages))} sent / ${th.fg("text", String(health.userMessages))} received`);
|
|
1115
1115
|
|
|
1116
|
+
// Environment section — issues only (from doctor-environment.ts, #1221)
|
|
1117
|
+
if (health.environmentIssues?.length > 0) {
|
|
1118
|
+
lines.push("");
|
|
1119
|
+
lines.push(th.fg("accent", th.bold("Environment")));
|
|
1120
|
+
lines.push("");
|
|
1121
|
+
for (const r of health.environmentIssues) {
|
|
1122
|
+
const icon = r.status === "error" ? th.fg("error", "✗") : th.fg("warning", "⚠");
|
|
1123
|
+
lines.push(` ${icon} ${th.fg("text", r.message)}`);
|
|
1124
|
+
if (r.detail) lines.push(` ${th.fg("dim", r.detail)}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Providers section
|
|
1129
|
+
if (health.providers?.length > 0) {
|
|
1130
|
+
lines.push("");
|
|
1131
|
+
lines.push(th.fg("accent", th.bold("Providers")));
|
|
1132
|
+
lines.push("");
|
|
1133
|
+
const categoryOrder = ["llm", "remote", "search", "tool"];
|
|
1134
|
+
const categoryLabels: Record<string, string> = { llm: "LLM", remote: "Notifications", search: "Search", tool: "Tools" };
|
|
1135
|
+
const grouped = new Map<string, typeof health.providers>();
|
|
1136
|
+
for (const p of health.providers) {
|
|
1137
|
+
const cat = p.category;
|
|
1138
|
+
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
1139
|
+
grouped.get(cat)!.push(p);
|
|
1140
|
+
}
|
|
1141
|
+
for (const cat of categoryOrder) {
|
|
1142
|
+
const items = grouped.get(cat);
|
|
1143
|
+
if (!items || items.length === 0) continue;
|
|
1144
|
+
lines.push(` ${th.fg("dim", categoryLabels[cat] ?? cat)}`);
|
|
1145
|
+
for (const p of items) {
|
|
1146
|
+
const icon = p.ok ? th.fg("success", "✓") : th.fg("error", "✗");
|
|
1147
|
+
const msg = p.ok ? th.fg("dim", p.message) : th.fg("text", p.message);
|
|
1148
|
+
lines.push(` ${icon} ${msg}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Skills section
|
|
1154
|
+
if (health.skillSummary?.total > 0) {
|
|
1155
|
+
lines.push("");
|
|
1156
|
+
lines.push(th.fg("accent", th.bold("Skills")));
|
|
1157
|
+
lines.push("");
|
|
1158
|
+
const { total, warningCount, criticalCount, topIssue } = health.skillSummary;
|
|
1159
|
+
const issueColor = criticalCount > 0 ? "error" : warningCount > 0 ? "warning" : "success";
|
|
1160
|
+
const issueTag = criticalCount > 0
|
|
1161
|
+
? `${criticalCount} critical`
|
|
1162
|
+
: warningCount > 0
|
|
1163
|
+
? `${warningCount} warning${warningCount > 1 ? "s" : ""}`
|
|
1164
|
+
: "all healthy";
|
|
1165
|
+
lines.push(` ${th.fg("text", String(total))} skills tracked · ${th.fg(issueColor, issueTag)}`);
|
|
1166
|
+
if (topIssue) lines.push(` ${th.fg("warning", "⚠")} ${th.fg("dim", topIssue)}`);
|
|
1167
|
+
lines.push(` ${th.fg("dim", "→ /gsd skill-health for full report")}`);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1116
1170
|
return lines;
|
|
1117
1171
|
}
|
package/dist/worktree-cli.d.ts
CHANGED
|
@@ -12,7 +12,43 @@
|
|
|
12
12
|
* On session exit (via session_shutdown event), auto-commits dirty work
|
|
13
13
|
* so nothing is lost. The GSD extension reads GSD_CLI_WORKTREE to know
|
|
14
14
|
* when a session was launched via -w.
|
|
15
|
+
*
|
|
16
|
+
* Note: Extension modules are .ts files loaded via jiti (not compiled to .js).
|
|
17
|
+
* We use createJiti() here because this module is compiled by tsc but imports
|
|
18
|
+
* from resources/extensions/gsd/ which are shipped as raw .ts (#1283).
|
|
15
19
|
*/
|
|
20
|
+
interface ExtensionModules {
|
|
21
|
+
createWorktree: (basePath: string, name: string) => {
|
|
22
|
+
path: string;
|
|
23
|
+
branch: string;
|
|
24
|
+
};
|
|
25
|
+
listWorktrees: (basePath: string) => Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
path: string;
|
|
28
|
+
branch: string;
|
|
29
|
+
}>;
|
|
30
|
+
removeWorktree: (basePath: string, name: string, opts?: {
|
|
31
|
+
deleteBranch?: boolean;
|
|
32
|
+
}) => void;
|
|
33
|
+
mergeWorktreeToMain: (basePath: string, name: string, commitMessage: string) => void;
|
|
34
|
+
diffWorktreeAll: (basePath: string, name: string) => {
|
|
35
|
+
added: any[];
|
|
36
|
+
modified: any[];
|
|
37
|
+
removed: any[];
|
|
38
|
+
};
|
|
39
|
+
diffWorktreeNumstat: (basePath: string, name: string) => Array<{
|
|
40
|
+
added: number;
|
|
41
|
+
removed: number;
|
|
42
|
+
}>;
|
|
43
|
+
worktreeBranchName: (name: string) => string;
|
|
44
|
+
worktreePath: (basePath: string, name: string) => string;
|
|
45
|
+
runWorktreePostCreateHook: (basePath: string, wtPath: string) => string | null;
|
|
46
|
+
nativeHasChanges: (path: string) => boolean;
|
|
47
|
+
nativeDetectMainBranch: (basePath: string) => string;
|
|
48
|
+
nativeCommitCountBetween: (basePath: string, from: string, to: string) => number;
|
|
49
|
+
inferCommitType: (name: string) => string;
|
|
50
|
+
autoCommitCurrentBranch: (wtPath: string, reason: string, name: string) => void;
|
|
51
|
+
}
|
|
16
52
|
interface WorktreeStatus {
|
|
17
53
|
name: string;
|
|
18
54
|
path: string;
|
|
@@ -24,11 +60,11 @@ interface WorktreeStatus {
|
|
|
24
60
|
uncommitted: boolean;
|
|
25
61
|
commits: number;
|
|
26
62
|
}
|
|
27
|
-
declare function getWorktreeStatus(basePath: string, name: string, wtPath: string): WorktreeStatus;
|
|
28
|
-
declare function handleList(basePath: string): void
|
|
63
|
+
declare function getWorktreeStatus(ext: ExtensionModules, basePath: string, name: string, wtPath: string): WorktreeStatus;
|
|
64
|
+
declare function handleList(basePath: string): Promise<void>;
|
|
29
65
|
declare function handleMerge(basePath: string, args: string[]): Promise<void>;
|
|
30
|
-
declare function handleClean(basePath: string): void
|
|
31
|
-
declare function handleRemove(basePath: string, args: string[]): void
|
|
32
|
-
declare function handleStatusBanner(basePath: string): void
|
|
33
|
-
declare function handleWorktreeFlag(worktreeFlag: boolean | string): void
|
|
66
|
+
declare function handleClean(basePath: string): Promise<void>;
|
|
67
|
+
declare function handleRemove(basePath: string, args: string[]): Promise<void>;
|
|
68
|
+
declare function handleStatusBanner(basePath: string): Promise<void>;
|
|
69
|
+
declare function handleWorktreeFlag(worktreeFlag: boolean | string): Promise<void>;
|
|
34
70
|
export { handleList, handleMerge, handleClean, handleRemove, handleStatusBanner, handleWorktreeFlag, getWorktreeStatus, };
|