gsd-pi 2.19.0 → 2.20.0
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 +5 -1
- package/dist/cli.js +3 -3
- package/dist/onboarding.d.ts +3 -1
- package/dist/onboarding.js +77 -3
- package/dist/remote-questions-config.d.ts +1 -1
- package/dist/resources/extensions/google-search/index.ts +164 -47
- package/dist/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/dist/resources/extensions/gsd/auto.ts +424 -30
- package/dist/resources/extensions/gsd/commands.ts +518 -36
- package/dist/resources/extensions/gsd/context-budget.ts +243 -0
- package/dist/resources/extensions/gsd/context-store.ts +195 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/dist/resources/extensions/gsd/db-writer.ts +341 -0
- package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/dist/resources/extensions/gsd/doctor.ts +283 -2
- package/dist/resources/extensions/gsd/export.ts +81 -2
- package/dist/resources/extensions/gsd/files.ts +39 -9
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
- package/dist/resources/extensions/gsd/history.ts +0 -1
- package/dist/resources/extensions/gsd/index.ts +277 -1
- package/dist/resources/extensions/gsd/md-importer.ts +526 -0
- package/dist/resources/extensions/gsd/metrics.ts +39 -3
- package/dist/resources/extensions/gsd/notifications.ts +0 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/dist/resources/extensions/gsd/preferences.ts +125 -150
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/quick.ts +156 -0
- package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/dist/resources/extensions/gsd/skill-health.ts +417 -0
- package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/dist/resources/extensions/gsd/types.ts +29 -0
- package/dist/resources/extensions/gsd/undo.ts +0 -1
- package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/dist/resources/extensions/remote-questions/config.ts +4 -2
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/dist/resources/extensions/remote-questions/format.ts +154 -8
- package/dist/resources/extensions/remote-questions/manager.ts +9 -7
- package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/dist/resources/extensions/remote-questions/types.ts +2 -1
- package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/dist/resources/extensions/voice/index.ts +4 -3
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
- package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
- package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
- package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
- package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
- package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
- package/src/resources/extensions/google-search/index.ts +164 -47
- package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/src/resources/extensions/gsd/auto.ts +424 -30
- package/src/resources/extensions/gsd/commands.ts +518 -36
- package/src/resources/extensions/gsd/context-budget.ts +243 -0
- package/src/resources/extensions/gsd/context-store.ts +195 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/src/resources/extensions/gsd/db-writer.ts +341 -0
- package/src/resources/extensions/gsd/debug-logger.ts +178 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/src/resources/extensions/gsd/doctor.ts +283 -2
- package/src/resources/extensions/gsd/export.ts +81 -2
- package/src/resources/extensions/gsd/files.ts +39 -9
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gsd-db.ts +752 -0
- package/src/resources/extensions/gsd/guided-flow.ts +26 -1
- package/src/resources/extensions/gsd/history.ts +0 -1
- package/src/resources/extensions/gsd/index.ts +277 -1
- package/src/resources/extensions/gsd/md-importer.ts +526 -0
- package/src/resources/extensions/gsd/metrics.ts +39 -3
- package/src/resources/extensions/gsd/notifications.ts +0 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/src/resources/extensions/gsd/preferences.ts +125 -150
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/quick.ts +156 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/src/resources/extensions/gsd/skill-health.ts +417 -0
- package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/src/resources/extensions/gsd/types.ts +29 -0
- package/src/resources/extensions/gsd/undo.ts +0 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/src/resources/extensions/gsd/worktree-command.ts +18 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/src/resources/extensions/remote-questions/config.ts +4 -2
- package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/src/resources/extensions/remote-questions/format.ts +154 -8
- package/src/resources/extensions/remote-questions/manager.ts +9 -7
- package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/src/resources/extensions/remote-questions/types.ts +2 -1
- package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/src/resources/extensions/voice/index.ts +4 -3
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Theme } from "@gsd/pi-coding-agent";
|
|
4
4
|
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
5
5
|
import type { VisualizerData, VisualizerMilestone } from "./visualizer-data.js";
|
|
6
|
-
import { formatCost, formatTokenCount } from "./metrics.js";
|
|
6
|
+
import { formatCost, formatTokenCount, classifyUnitPhase } from "./metrics.js";
|
|
7
7
|
|
|
8
8
|
// ─── Local Helpers ───────────────────────────────────────────────────────────
|
|
9
9
|
|
|
@@ -32,16 +32,46 @@ function joinColumns(left: string, right: string, width: number): string {
|
|
|
32
32
|
return left + " ".repeat(width - leftW - rightW) + right;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function sparkline(values: number[]): string {
|
|
36
|
+
if (values.length === 0) return "";
|
|
37
|
+
const chars = "▁▂▃▄▅▆▇█";
|
|
38
|
+
const max = Math.max(...values);
|
|
39
|
+
if (max === 0) return chars[0].repeat(values.length);
|
|
40
|
+
return values.map(v => chars[Math.min(7, Math.floor((v / max) * 7))]).join("");
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
// ─── Progress View ───────────────────────────────────────────────────────────
|
|
36
44
|
|
|
45
|
+
export interface ProgressFilter {
|
|
46
|
+
text: string;
|
|
47
|
+
field: "all" | "status" | "risk" | "keyword";
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
export function renderProgressView(
|
|
38
51
|
data: VisualizerData,
|
|
39
52
|
th: Theme,
|
|
40
53
|
width: number,
|
|
54
|
+
filter?: ProgressFilter,
|
|
41
55
|
): string[] {
|
|
42
56
|
const lines: string[] = [];
|
|
43
57
|
|
|
58
|
+
// Risk Heatmap
|
|
59
|
+
lines.push(...renderRiskHeatmap(data, th, width));
|
|
60
|
+
if (data.milestones.length > 0) lines.push("");
|
|
61
|
+
|
|
62
|
+
// Filter indicator
|
|
63
|
+
if (filter && filter.text) {
|
|
64
|
+
lines.push(th.fg("accent", `Filter (${filter.field}): ${filter.text}`));
|
|
65
|
+
lines.push("");
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
for (const ms of data.milestones) {
|
|
69
|
+
// Apply filter to milestones
|
|
70
|
+
if (filter && filter.text) {
|
|
71
|
+
const matchesMs = matchesFilter(ms, filter);
|
|
72
|
+
if (!matchesMs) continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
45
75
|
// Milestone header line
|
|
46
76
|
const statusGlyph =
|
|
47
77
|
ms.status === "complete"
|
|
@@ -70,6 +100,11 @@ export function renderProgressView(
|
|
|
70
100
|
}
|
|
71
101
|
|
|
72
102
|
for (const sl of ms.slices) {
|
|
103
|
+
// Apply filter to slices
|
|
104
|
+
if (filter && filter.text) {
|
|
105
|
+
if (!matchesSliceFilter(sl, filter)) continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
73
108
|
// Slice line
|
|
74
109
|
const slGlyph = sl.done
|
|
75
110
|
? th.fg("success", "✓")
|
|
@@ -103,6 +138,78 @@ export function renderProgressView(
|
|
|
103
138
|
return lines;
|
|
104
139
|
}
|
|
105
140
|
|
|
141
|
+
function matchesFilter(ms: VisualizerMilestone, filter: ProgressFilter): boolean {
|
|
142
|
+
const text = filter.text.toLowerCase();
|
|
143
|
+
if (filter.field === "status") {
|
|
144
|
+
return ms.status.includes(text);
|
|
145
|
+
}
|
|
146
|
+
if (filter.field === "risk") {
|
|
147
|
+
return ms.slices.some(s => s.risk.toLowerCase().includes(text));
|
|
148
|
+
}
|
|
149
|
+
// "all" or "keyword"
|
|
150
|
+
if (ms.id.toLowerCase().includes(text)) return true;
|
|
151
|
+
if (ms.title.toLowerCase().includes(text)) return true;
|
|
152
|
+
if (ms.status.includes(text)) return true;
|
|
153
|
+
return ms.slices.some(s => matchesSliceFilter(s, filter));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function matchesSliceFilter(sl: { id: string; title: string; risk: string }, filter: ProgressFilter): boolean {
|
|
157
|
+
const text = filter.text.toLowerCase();
|
|
158
|
+
if (filter.field === "status") return true; // slices don't have named status
|
|
159
|
+
if (filter.field === "risk") return sl.risk.toLowerCase().includes(text);
|
|
160
|
+
return sl.id.toLowerCase().includes(text) ||
|
|
161
|
+
sl.title.toLowerCase().includes(text) ||
|
|
162
|
+
sl.risk.toLowerCase().includes(text);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Risk Heatmap ────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
function renderRiskHeatmap(data: VisualizerData, th: Theme, width: number): string[] {
|
|
168
|
+
const allSlices = data.milestones.flatMap(m => m.slices);
|
|
169
|
+
if (allSlices.length === 0) return [];
|
|
170
|
+
|
|
171
|
+
const lines: string[] = [];
|
|
172
|
+
lines.push(th.fg("accent", th.bold("Risk Heatmap")));
|
|
173
|
+
lines.push("");
|
|
174
|
+
|
|
175
|
+
for (const ms of data.milestones) {
|
|
176
|
+
if (ms.slices.length === 0) continue;
|
|
177
|
+
const blocks = ms.slices.map(s => {
|
|
178
|
+
const color = s.risk === "high" ? "error" : s.risk === "medium" ? "warning" : "success";
|
|
179
|
+
return th.fg(color, "██");
|
|
180
|
+
});
|
|
181
|
+
const row = ` ${padRight(ms.id, 6)} ${blocks.join(" ")}`;
|
|
182
|
+
lines.push(truncateToWidth(row, width));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push(
|
|
187
|
+
` ${th.fg("success", "██")} low ${th.fg("warning", "██")} med ${th.fg("error", "██")} high`,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Summary counts
|
|
191
|
+
let low = 0, med = 0, high = 0;
|
|
192
|
+
let highNotStarted = 0;
|
|
193
|
+
for (const sl of allSlices) {
|
|
194
|
+
if (sl.risk === "high") {
|
|
195
|
+
high++;
|
|
196
|
+
if (!sl.done && !sl.active) highNotStarted++;
|
|
197
|
+
} else if (sl.risk === "medium") {
|
|
198
|
+
med++;
|
|
199
|
+
} else {
|
|
200
|
+
low++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let summary = ` Risk: ${low} low, ${med} med, ${high} high`;
|
|
205
|
+
if (highNotStarted > 0) {
|
|
206
|
+
summary += ` | ${th.fg("error", `${highNotStarted} high-risk not started`)}`;
|
|
207
|
+
}
|
|
208
|
+
lines.push(summary);
|
|
209
|
+
|
|
210
|
+
return lines;
|
|
211
|
+
}
|
|
212
|
+
|
|
106
213
|
// ─── Dependencies View ───────────────────────────────────────────────────────
|
|
107
214
|
|
|
108
215
|
export function renderDepsView(
|
|
@@ -153,6 +260,65 @@ export function renderDepsView(
|
|
|
153
260
|
}
|
|
154
261
|
}
|
|
155
262
|
|
|
263
|
+
lines.push("");
|
|
264
|
+
|
|
265
|
+
// Critical Path section
|
|
266
|
+
lines.push(...renderCriticalPath(data, th, width));
|
|
267
|
+
|
|
268
|
+
return lines;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── Critical Path ───────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
function renderCriticalPath(data: VisualizerData, th: Theme, _width: number): string[] {
|
|
274
|
+
const lines: string[] = [];
|
|
275
|
+
const cp = data.criticalPath;
|
|
276
|
+
|
|
277
|
+
lines.push(th.fg("accent", th.bold("Critical Path")));
|
|
278
|
+
lines.push("");
|
|
279
|
+
|
|
280
|
+
if (cp.milestonePath.length === 0) {
|
|
281
|
+
lines.push(th.fg("dim", " No critical path data."));
|
|
282
|
+
return lines;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Milestone chain
|
|
286
|
+
const chain = cp.milestonePath.map(id => {
|
|
287
|
+
const ms = data.milestones.find(m => m.id === id);
|
|
288
|
+
const badge = th.fg("error", "[CRITICAL]");
|
|
289
|
+
return `${id} ${badge}`;
|
|
290
|
+
}).join(` ${th.fg("accent", "──►")} `);
|
|
291
|
+
lines.push(` ${chain}`);
|
|
292
|
+
lines.push("");
|
|
293
|
+
|
|
294
|
+
// Non-critical milestones with slack
|
|
295
|
+
for (const ms of data.milestones) {
|
|
296
|
+
if (cp.milestonePath.includes(ms.id)) continue;
|
|
297
|
+
const slack = cp.milestoneSlack.get(ms.id) ?? 0;
|
|
298
|
+
lines.push(th.fg("dim", ` ${ms.id} (slack: ${slack})`));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Slice-level critical path
|
|
302
|
+
if (cp.slicePath.length > 0) {
|
|
303
|
+
lines.push("");
|
|
304
|
+
lines.push(th.fg("accent", th.bold("Slice Critical Path")));
|
|
305
|
+
lines.push("");
|
|
306
|
+
|
|
307
|
+
const sliceChain = cp.slicePath.join(` ${th.fg("accent", "──►")} `);
|
|
308
|
+
lines.push(` ${sliceChain}`);
|
|
309
|
+
|
|
310
|
+
// Bottleneck warnings
|
|
311
|
+
const activeMs = data.milestones.find(m => m.status === "active");
|
|
312
|
+
if (activeMs) {
|
|
313
|
+
for (const sid of cp.slicePath) {
|
|
314
|
+
const sl = activeMs.slices.find(s => s.id === sid);
|
|
315
|
+
if (sl && !sl.done && !sl.active) {
|
|
316
|
+
lines.push(th.fg("warning", ` ⚠ ${sid}: critical but not yet started`));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
156
322
|
return lines;
|
|
157
323
|
}
|
|
158
324
|
|
|
@@ -232,12 +398,66 @@ export function renderMetricsView(
|
|
|
232
398
|
const pctStr = `${pct.toFixed(1)}%`;
|
|
233
399
|
lines.push(` ${label} ${bar} ${costStr} ${pctStr}`);
|
|
234
400
|
}
|
|
401
|
+
|
|
402
|
+
lines.push("");
|
|
235
403
|
}
|
|
236
404
|
|
|
405
|
+
// Cost Projections
|
|
406
|
+
lines.push(...renderCostProjections(data, th, width));
|
|
407
|
+
|
|
237
408
|
return lines;
|
|
238
409
|
}
|
|
239
410
|
|
|
240
|
-
// ───
|
|
411
|
+
// ─── Cost Projections ────────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
function renderCostProjections(data: VisualizerData, th: Theme, _width: number): string[] {
|
|
414
|
+
const lines: string[] = [];
|
|
415
|
+
|
|
416
|
+
if (!data.totals || data.bySlice.length === 0) return lines;
|
|
417
|
+
|
|
418
|
+
lines.push(th.fg("accent", th.bold("Projections")));
|
|
419
|
+
lines.push("");
|
|
420
|
+
|
|
421
|
+
// Average cost per slice
|
|
422
|
+
const sliceLevelEntries = data.bySlice.filter(s => s.sliceId.includes("/"));
|
|
423
|
+
if (sliceLevelEntries.length < 2) {
|
|
424
|
+
lines.push(th.fg("dim", " Insufficient data for projections (need 2+ completed slices)."));
|
|
425
|
+
return lines;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const totalSliceCost = sliceLevelEntries.reduce((sum, s) => sum + s.cost, 0);
|
|
429
|
+
const avgCostPerSlice = totalSliceCost / sliceLevelEntries.length;
|
|
430
|
+
const projectedRemaining = avgCostPerSlice * data.remainingSliceCount;
|
|
431
|
+
|
|
432
|
+
lines.push(` Avg cost/slice: ${th.fg("text", formatCost(avgCostPerSlice))}`);
|
|
433
|
+
lines.push(
|
|
434
|
+
` Projected remaining: ${th.fg("text", formatCost(projectedRemaining))} ` +
|
|
435
|
+
`(${formatCost(avgCostPerSlice)}/slice × ${data.remainingSliceCount} remaining)`,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// Burn rate
|
|
439
|
+
if (data.totals.duration > 0) {
|
|
440
|
+
const costPerHour = data.totals.cost / (data.totals.duration / 3_600_000);
|
|
441
|
+
lines.push(` Burn rate: ${th.fg("text", formatCost(costPerHour) + "/hr")}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Sparkline of per-slice costs
|
|
445
|
+
const sliceCosts = sliceLevelEntries.map(s => s.cost);
|
|
446
|
+
if (sliceCosts.length > 0) {
|
|
447
|
+
const spark = sparkline(sliceCosts);
|
|
448
|
+
lines.push(` Cost trend: ${spark}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Budget warning: projected total > 2× current spend
|
|
452
|
+
const projectedTotal = data.totals.cost + projectedRemaining;
|
|
453
|
+
if (projectedTotal > 2 * data.totals.cost && data.remainingSliceCount > 0) {
|
|
454
|
+
lines.push(th.fg("warning", ` ⚠ Projected total ${formatCost(projectedTotal)} exceeds 2× current spend`));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return lines;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ─── Timeline View (Gantt) ──────────────────────────────────────────────────
|
|
241
461
|
|
|
242
462
|
export function renderTimelineView(
|
|
243
463
|
data: VisualizerData,
|
|
@@ -251,6 +471,17 @@ export function renderTimelineView(
|
|
|
251
471
|
return lines;
|
|
252
472
|
}
|
|
253
473
|
|
|
474
|
+
// Gantt mode for wide terminals, list mode for narrow
|
|
475
|
+
if (width >= 90) {
|
|
476
|
+
return renderGanttView(data, th, width);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return renderTimelineList(data, th, width);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function renderTimelineList(data: VisualizerData, th: Theme, width: number): string[] {
|
|
483
|
+
const lines: string[] = [];
|
|
484
|
+
|
|
254
485
|
// Show up to 20 most recent (units are sorted by startedAt asc, show most recent)
|
|
255
486
|
const recent = data.units.slice(-20).reverse();
|
|
256
487
|
|
|
@@ -291,3 +522,234 @@ export function renderTimelineView(
|
|
|
291
522
|
|
|
292
523
|
return lines;
|
|
293
524
|
}
|
|
525
|
+
|
|
526
|
+
function renderGanttView(data: VisualizerData, th: Theme, width: number): string[] {
|
|
527
|
+
const lines: string[] = [];
|
|
528
|
+
const recent = data.units.slice(-20);
|
|
529
|
+
if (recent.length === 0) return lines;
|
|
530
|
+
|
|
531
|
+
const finishedUnits = recent.filter(u => u.finishedAt > 0);
|
|
532
|
+
if (finishedUnits.length === 0) return renderTimelineList(data, th, width);
|
|
533
|
+
|
|
534
|
+
const minStart = Math.min(...recent.map(u => u.startedAt));
|
|
535
|
+
const maxEnd = Math.max(...recent.map(u => u.finishedAt > 0 ? u.finishedAt : Date.now()));
|
|
536
|
+
const totalSpan = maxEnd - minStart;
|
|
537
|
+
if (totalSpan <= 0) return renderTimelineList(data, th, width);
|
|
538
|
+
|
|
539
|
+
const gutterWidth = 20;
|
|
540
|
+
const barArea = Math.max(10, width - gutterWidth - 25);
|
|
541
|
+
|
|
542
|
+
// Time axis labels
|
|
543
|
+
const startLabel = formatTimeLabel(minStart);
|
|
544
|
+
const endLabel = formatTimeLabel(maxEnd);
|
|
545
|
+
lines.push(
|
|
546
|
+
`${" ".repeat(gutterWidth)} ${th.fg("dim", startLabel)}` +
|
|
547
|
+
`${" ".repeat(Math.max(1, barArea - startLabel.length - endLabel.length))}` +
|
|
548
|
+
`${th.fg("dim", endLabel)}`,
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Phase tracking for separators
|
|
552
|
+
let lastPhase = "";
|
|
553
|
+
|
|
554
|
+
for (const unit of recent) {
|
|
555
|
+
const phase = classifyUnitPhase(unit.type);
|
|
556
|
+
if (phase !== lastPhase && lastPhase !== "") {
|
|
557
|
+
lines.push(th.fg("dim", " " + "─".repeat(width - 4)));
|
|
558
|
+
}
|
|
559
|
+
lastPhase = phase;
|
|
560
|
+
|
|
561
|
+
const end = unit.finishedAt > 0 ? unit.finishedAt : Date.now();
|
|
562
|
+
const startPos = Math.round(((unit.startedAt - minStart) / totalSpan) * barArea);
|
|
563
|
+
const endPos = Math.round(((end - minStart) / totalSpan) * barArea);
|
|
564
|
+
const barLen = Math.max(1, endPos - startPos);
|
|
565
|
+
|
|
566
|
+
const phaseColor =
|
|
567
|
+
phase === "research" ? "dim" :
|
|
568
|
+
phase === "planning" ? "accent" :
|
|
569
|
+
phase === "execution" ? "success" :
|
|
570
|
+
"warning";
|
|
571
|
+
|
|
572
|
+
const barStr =
|
|
573
|
+
" ".repeat(startPos) +
|
|
574
|
+
th.fg(phaseColor, "█".repeat(barLen)) +
|
|
575
|
+
" ".repeat(Math.max(0, barArea - startPos - barLen));
|
|
576
|
+
|
|
577
|
+
const gutter = padRight(
|
|
578
|
+
truncateToWidth(`${unit.type.slice(0, 8)} ${unit.id}`, gutterWidth - 1),
|
|
579
|
+
gutterWidth,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
const duration = end - unit.startedAt;
|
|
583
|
+
const durStr = formatDuration(duration);
|
|
584
|
+
const costStr = formatCost(unit.cost);
|
|
585
|
+
|
|
586
|
+
lines.push(truncateToWidth(`${gutter}${barStr} ${durStr} ${costStr}`, width));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return lines;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function formatTimeLabel(ts: number): string {
|
|
593
|
+
const dt = new Date(ts);
|
|
594
|
+
return `${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ─── Agent View ──────────────────────────────────────────────────────────────
|
|
598
|
+
|
|
599
|
+
export function renderAgentView(
|
|
600
|
+
data: VisualizerData,
|
|
601
|
+
th: Theme,
|
|
602
|
+
width: number,
|
|
603
|
+
): string[] {
|
|
604
|
+
const lines: string[] = [];
|
|
605
|
+
const activity = data.agentActivity;
|
|
606
|
+
|
|
607
|
+
if (!activity) {
|
|
608
|
+
lines.push(th.fg("dim", "No agent activity data."));
|
|
609
|
+
return lines;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Status line
|
|
613
|
+
const statusDot = activity.active
|
|
614
|
+
? th.fg("success", "●")
|
|
615
|
+
: th.fg("dim", "○");
|
|
616
|
+
const statusText = activity.active ? "ACTIVE" : "IDLE";
|
|
617
|
+
const elapsedStr = activity.active ? formatDuration(activity.elapsed) : "—";
|
|
618
|
+
|
|
619
|
+
lines.push(
|
|
620
|
+
joinColumns(
|
|
621
|
+
`Status: ${statusDot} ${statusText}`,
|
|
622
|
+
`Elapsed: ${elapsedStr}`,
|
|
623
|
+
width,
|
|
624
|
+
),
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
if (activity.currentUnit) {
|
|
628
|
+
lines.push(`Current: ${th.fg("accent", `${activity.currentUnit.type} ${activity.currentUnit.id}`)}`);
|
|
629
|
+
} else {
|
|
630
|
+
lines.push(th.fg("dim", "Not in auto mode"));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
lines.push("");
|
|
634
|
+
|
|
635
|
+
// Progress bar
|
|
636
|
+
const completed = activity.completedUnits;
|
|
637
|
+
const total = Math.max(completed, activity.totalSlices);
|
|
638
|
+
if (total > 0) {
|
|
639
|
+
const pct = Math.min(1, completed / total);
|
|
640
|
+
const barW = Math.max(10, Math.min(30, width - 30));
|
|
641
|
+
const fillLen = Math.round(pct * barW);
|
|
642
|
+
const bar =
|
|
643
|
+
th.fg("accent", "█".repeat(fillLen)) +
|
|
644
|
+
th.fg("dim", "░".repeat(barW - fillLen));
|
|
645
|
+
lines.push(`Progress ${bar} ${completed}/${total} slices`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Rate and session stats
|
|
649
|
+
const rateStr = activity.completionRate > 0
|
|
650
|
+
? `${activity.completionRate.toFixed(1)} units/hr`
|
|
651
|
+
: "—";
|
|
652
|
+
lines.push(
|
|
653
|
+
`Rate: ${th.fg("text", rateStr)} ` +
|
|
654
|
+
`Session: ${th.fg("text", formatCost(activity.sessionCost))} ` +
|
|
655
|
+
`${th.fg("text", formatTokenCount(activity.sessionTokens))} tokens`,
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
lines.push("");
|
|
659
|
+
|
|
660
|
+
// Recent completed units (last 5)
|
|
661
|
+
const recentUnits = data.units.filter(u => u.finishedAt > 0).slice(-5).reverse();
|
|
662
|
+
if (recentUnits.length > 0) {
|
|
663
|
+
lines.push(th.fg("accent", th.bold("Recent (last 5):")));
|
|
664
|
+
for (const u of recentUnits) {
|
|
665
|
+
const dt = new Date(u.startedAt);
|
|
666
|
+
const hh = String(dt.getHours()).padStart(2, "0");
|
|
667
|
+
const mm = String(dt.getMinutes()).padStart(2, "0");
|
|
668
|
+
const dur = formatDuration(u.finishedAt - u.startedAt);
|
|
669
|
+
const cost = formatCost(u.cost);
|
|
670
|
+
const typeLabel = padRight(u.type, 16);
|
|
671
|
+
lines.push(
|
|
672
|
+
truncateToWidth(
|
|
673
|
+
` ${hh}:${mm} ${th.fg("success", "✓")} ${typeLabel} ${padRight(u.id, 16)} ${dur} ${cost}`,
|
|
674
|
+
width,
|
|
675
|
+
),
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
lines.push(th.fg("dim", "No completed units yet."));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return lines;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ─── Changelog View ──────────────────────────────────────────────────────────
|
|
686
|
+
|
|
687
|
+
export function renderChangelogView(
|
|
688
|
+
data: VisualizerData,
|
|
689
|
+
th: Theme,
|
|
690
|
+
width: number,
|
|
691
|
+
): string[] {
|
|
692
|
+
const lines: string[] = [];
|
|
693
|
+
const changelog = data.changelog;
|
|
694
|
+
|
|
695
|
+
if (changelog.entries.length === 0) {
|
|
696
|
+
lines.push(th.fg("dim", "No completed slices yet."));
|
|
697
|
+
return lines;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
lines.push(th.fg("accent", th.bold("Changes")));
|
|
701
|
+
lines.push("");
|
|
702
|
+
|
|
703
|
+
for (const entry of changelog.entries) {
|
|
704
|
+
const header = `${entry.milestoneId}/${entry.sliceId}: ${entry.title}`;
|
|
705
|
+
lines.push(th.fg("success", header));
|
|
706
|
+
|
|
707
|
+
if (entry.oneLiner) {
|
|
708
|
+
lines.push(` "${th.fg("text", entry.oneLiner)}"`);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (entry.filesModified.length > 0) {
|
|
712
|
+
lines.push(" Files:");
|
|
713
|
+
for (const f of entry.filesModified) {
|
|
714
|
+
lines.push(
|
|
715
|
+
truncateToWidth(
|
|
716
|
+
` ${th.fg("success", "✓")} ${f.path} — ${f.description}`,
|
|
717
|
+
width,
|
|
718
|
+
),
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (entry.completedAt) {
|
|
724
|
+
lines.push(th.fg("dim", ` Completed: ${entry.completedAt}`));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
lines.push("");
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return lines;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ─── Export View ─────────────────────────────────────────────────────────────
|
|
734
|
+
|
|
735
|
+
export function renderExportView(
|
|
736
|
+
_data: VisualizerData,
|
|
737
|
+
th: Theme,
|
|
738
|
+
_width: number,
|
|
739
|
+
lastExportPath?: string,
|
|
740
|
+
): string[] {
|
|
741
|
+
const lines: string[] = [];
|
|
742
|
+
|
|
743
|
+
lines.push(th.fg("accent", th.bold("Export Options")));
|
|
744
|
+
lines.push("");
|
|
745
|
+
lines.push(` ${th.fg("accent", "[m]")} Markdown report — full project summary with tables`);
|
|
746
|
+
lines.push(` ${th.fg("accent", "[j]")} JSON report — machine-readable project data`);
|
|
747
|
+
lines.push(` ${th.fg("accent", "[s]")} Snapshot — current view as plain text`);
|
|
748
|
+
|
|
749
|
+
if (lastExportPath) {
|
|
750
|
+
lines.push("");
|
|
751
|
+
lines.push(th.fg("dim", `Last export: ${lastExportPath}`));
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return lines;
|
|
755
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
14
14
|
import { loadPrompt } from "./prompt-loader.js";
|
|
15
15
|
import { autoCommitCurrentBranch } from "./worktree.js";
|
|
16
|
+
import { runWorktreePostCreateHook } from "./auto-worktree.js";
|
|
16
17
|
import { showConfirm } from "../shared/confirm-ui.js";
|
|
17
18
|
import { gsdRoot, milestonesDir } from "./paths.js";
|
|
18
19
|
import {
|
|
@@ -360,6 +361,12 @@ async function handleCreate(
|
|
|
360
361
|
const mainBase = originalCwd ?? basePath;
|
|
361
362
|
const info = createWorktree(mainBase, name);
|
|
362
363
|
|
|
364
|
+
// Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
|
|
365
|
+
const hookError = runWorktreePostCreateHook(mainBase, info.path);
|
|
366
|
+
if (hookError) {
|
|
367
|
+
ctx.ui.notify(hookError, "warning");
|
|
368
|
+
}
|
|
369
|
+
|
|
363
370
|
// Track original cwd before switching
|
|
364
371
|
if (!originalCwd) originalCwd = basePath;
|
|
365
372
|
|
|
@@ -672,6 +679,17 @@ async function handleMerge(
|
|
|
672
679
|
// Try a direct squash-merge first. Only fall back to LLM on conflict.
|
|
673
680
|
const commitType = inferCommitType(name);
|
|
674
681
|
const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
|
|
682
|
+
|
|
683
|
+
// Reconcile worktree DB into main DB before squash merge
|
|
684
|
+
const wtDbPath = join(worktreePath(basePath, name), ".gsd", "gsd.db");
|
|
685
|
+
const mainDbPath = join(basePath, ".gsd", "gsd.db");
|
|
686
|
+
if (existsSync(wtDbPath) && existsSync(mainDbPath)) {
|
|
687
|
+
try {
|
|
688
|
+
const { reconcileWorktreeDb } = await import("./gsd-db.js");
|
|
689
|
+
reconcileWorktreeDb(mainDbPath, wtDbPath);
|
|
690
|
+
} catch { /* non-fatal */ }
|
|
691
|
+
}
|
|
692
|
+
|
|
675
693
|
try {
|
|
676
694
|
mergeWorktreeToMain(basePath, name, commitMessage);
|
|
677
695
|
ctx.ui.notify(
|
|
@@ -94,7 +94,7 @@ export function worktreeBranchName(name: string): string {
|
|
|
94
94
|
*
|
|
95
95
|
* @param opts.branch — override the default `worktree/<name>` branch name
|
|
96
96
|
*/
|
|
97
|
-
export function createWorktree(basePath: string, name: string, opts: { branch?: string; startPoint?: string } = {}): WorktreeInfo {
|
|
97
|
+
export function createWorktree(basePath: string, name: string, opts: { branch?: string; startPoint?: string; reuseExistingBranch?: boolean } = {}): WorktreeInfo {
|
|
98
98
|
// Validate name: alphanumeric, hyphens, underscores only
|
|
99
99
|
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
100
100
|
throw new Error(`Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
|
|
@@ -133,9 +133,16 @@ export function createWorktree(basePath: string, name: string, opts: { branch?:
|
|
|
133
133
|
);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
if (opts.reuseExistingBranch) {
|
|
137
|
+
// Attach worktree to the existing branch as-is (preserving commits).
|
|
138
|
+
// Used when resuming auto-mode: the milestone branch has valid work
|
|
139
|
+
// from prior sessions that must not be reset.
|
|
140
|
+
nativeWorktreeAdd(basePath, wtPath, branch);
|
|
141
|
+
} else {
|
|
142
|
+
// Reset the stale branch to the start point, then attach worktree to it
|
|
143
|
+
nativeBranchForceReset(basePath, branch, startPoint);
|
|
144
|
+
nativeWorktreeAdd(basePath, wtPath, branch);
|
|
145
|
+
}
|
|
139
146
|
} else {
|
|
140
147
|
nativeWorktreeAdd(basePath, wtPath, branch, true, startPoint);
|
|
141
148
|
}
|
|
@@ -16,12 +16,14 @@ export interface ResolvedConfig {
|
|
|
16
16
|
const ENV_KEYS: Record<RemoteChannel, string> = {
|
|
17
17
|
slack: "SLACK_BOT_TOKEN",
|
|
18
18
|
discord: "DISCORD_BOT_TOKEN",
|
|
19
|
+
telegram: "TELEGRAM_BOT_TOKEN",
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
// Channel ID format validation — prevents SSRF if preferences are attacker-controlled
|
|
22
23
|
const CHANNEL_ID_PATTERNS: Record<RemoteChannel, RegExp> = {
|
|
23
24
|
slack: /^[A-Z0-9]{9,12}$/,
|
|
24
25
|
discord: /^\d{17,20}$/,
|
|
26
|
+
telegram: /^-?\d{5,20}$/,
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
const DEFAULT_TIMEOUT_MINUTES = 5;
|
|
@@ -35,7 +37,7 @@ export function resolveRemoteConfig(): ResolvedConfig | null {
|
|
|
35
37
|
const prefs = loadEffectiveGSDPreferences();
|
|
36
38
|
const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
|
|
37
39
|
if (!rq || !rq.channel || !rq.channel_id) return null;
|
|
38
|
-
if (rq.channel !== "slack" && rq.channel !== "discord") return null;
|
|
40
|
+
if (rq.channel !== "slack" && rq.channel !== "discord" && rq.channel !== "telegram") return null;
|
|
39
41
|
|
|
40
42
|
const channelId = String(rq.channel_id);
|
|
41
43
|
if (!CHANNEL_ID_PATTERNS[rq.channel].test(channelId)) return null;
|
|
@@ -59,7 +61,7 @@ export function getRemoteConfigStatus(): string {
|
|
|
59
61
|
const prefs = loadEffectiveGSDPreferences();
|
|
60
62
|
const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
|
|
61
63
|
if (!rq || !rq.channel || !rq.channel_id) return "Remote questions: not configured";
|
|
62
|
-
if (rq.channel !== "slack" && rq.channel !== "discord") return `Remote questions: unknown channel type \"${rq.channel}\"`;
|
|
64
|
+
if (rq.channel !== "slack" && rq.channel !== "discord" && rq.channel !== "telegram") return `Remote questions: unknown channel type \"${rq.channel}\"`;
|
|
63
65
|
const channelId = String(rq.channel_id);
|
|
64
66
|
if (!CHANNEL_ID_PATTERNS[rq.channel].test(channelId)) return `Remote questions: invalid ${rq.channel} channel ID format`;
|
|
65
67
|
const envVar = ENV_KEYS[rq.channel];
|
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
|
|
6
|
-
import { formatForDiscord, parseDiscordResponse } from "./format.js";
|
|
6
|
+
import { formatForDiscord, parseDiscordResponse, DISCORD_NUMBER_EMOJIS } from "./format.js";
|
|
7
7
|
|
|
8
8
|
const DISCORD_API = "https://discord.com/api/v10";
|
|
9
9
|
const PER_REQUEST_TIMEOUT_MS = 15_000;
|
|
10
|
-
const NUMBER_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"];
|
|
11
|
-
|
|
12
10
|
export class DiscordAdapter implements ChannelAdapter {
|
|
13
11
|
readonly name = "discord" as const;
|
|
14
12
|
private botUserId: string | null = null;
|
|
@@ -102,7 +100,7 @@ export class DiscordAdapter implements ChannelAdapter {
|
|
|
102
100
|
|
|
103
101
|
private async checkReactions(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null> {
|
|
104
102
|
const reactions: Array<{ emoji: string; count: number }> = [];
|
|
105
|
-
for (const emoji of
|
|
103
|
+
for (const emoji of DISCORD_NUMBER_EMOJIS) {
|
|
106
104
|
try {
|
|
107
105
|
const users = await this.discordApi("GET", `/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent(emoji)}`);
|
|
108
106
|
if (Array.isArray(users)) {
|