gsd-pi 2.30.0 → 2.31.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/dist/cli.js +51 -0
- package/dist/help-text.js +35 -0
- package/dist/resources/extensions/aws-auth/index.ts +144 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +65 -186
- package/dist/resources/extensions/gsd/auto-prompts.ts +2 -10
- package/dist/resources/extensions/gsd/auto-start.ts +3 -10
- package/dist/resources/extensions/gsd/auto-worktree.ts +12 -8
- package/dist/resources/extensions/gsd/auto.ts +2 -2
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +2 -12
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -4
- package/dist/resources/extensions/gsd/git-service.ts +4 -22
- package/dist/resources/extensions/gsd/gitignore.ts +6 -7
- package/dist/resources/extensions/gsd/guided-flow-queue.ts +3 -7
- package/dist/resources/extensions/gsd/guided-flow.ts +8 -11
- package/dist/resources/extensions/gsd/index.ts +13 -0
- package/dist/resources/extensions/gsd/init-wizard.ts +2 -30
- package/dist/resources/extensions/gsd/preferences-types.ts +0 -2
- package/dist/resources/extensions/gsd/preferences-validation.ts +1 -2
- package/dist/resources/extensions/gsd/roadmap-slices.ts +22 -7
- package/dist/resources/extensions/gsd/session-lock.ts +53 -4
- package/dist/resources/extensions/gsd/templates/preferences.md +0 -1
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +14 -42
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +3 -9
- package/dist/resources/extensions/gsd/tests/preferences.test.ts +1 -9
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -4
- package/dist/resources/extensions/gsd/worktree.ts +2 -2
- package/dist/worktree-cli.d.ts +34 -0
- package/dist/worktree-cli.js +294 -0
- package/dist/worktree-name-gen.d.ts +7 -0
- package/dist/worktree-name-gen.js +44 -0
- 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 +14 -0
- 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 +4 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +14 -0
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +5 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +1 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +8 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/aws-auth/index.ts +144 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +65 -186
- package/src/resources/extensions/gsd/auto-prompts.ts +2 -10
- package/src/resources/extensions/gsd/auto-start.ts +3 -10
- package/src/resources/extensions/gsd/auto-worktree.ts +12 -8
- package/src/resources/extensions/gsd/auto.ts +2 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +2 -12
- package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -4
- package/src/resources/extensions/gsd/git-service.ts +4 -22
- package/src/resources/extensions/gsd/gitignore.ts +6 -7
- package/src/resources/extensions/gsd/guided-flow-queue.ts +3 -7
- package/src/resources/extensions/gsd/guided-flow.ts +8 -11
- package/src/resources/extensions/gsd/index.ts +13 -0
- package/src/resources/extensions/gsd/init-wizard.ts +2 -30
- package/src/resources/extensions/gsd/preferences-types.ts +0 -2
- package/src/resources/extensions/gsd/preferences-validation.ts +1 -2
- package/src/resources/extensions/gsd/roadmap-slices.ts +22 -7
- package/src/resources/extensions/gsd/session-lock.ts +53 -4
- package/src/resources/extensions/gsd/templates/preferences.md +0 -1
- package/src/resources/extensions/gsd/tests/git-service.test.ts +14 -42
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +3 -9
- package/src/resources/extensions/gsd/tests/preferences.test.ts +1 -9
- package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -4
- package/src/resources/extensions/gsd/worktree.ts +2 -2
package/dist/cli.js
CHANGED
|
@@ -59,6 +59,15 @@ function parseCliArgs(argv) {
|
|
|
59
59
|
process.stdout.write((process.env.GSD_VERSION || '0.0.0') + '\n');
|
|
60
60
|
process.exit(0);
|
|
61
61
|
}
|
|
62
|
+
else if (arg === '--worktree' || arg === '-w') {
|
|
63
|
+
// -w with no value → auto-generate name; -w <name> → use that name
|
|
64
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
65
|
+
flags.worktree = args[++i];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
flags.worktree = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
62
71
|
else if (arg === '--help' || arg === '-h') {
|
|
63
72
|
printHelp(process.env.GSD_VERSION || '0.0.0');
|
|
64
73
|
process.exit(0);
|
|
@@ -343,6 +352,48 @@ if (isPrintMode) {
|
|
|
343
352
|
process.exit(0);
|
|
344
353
|
}
|
|
345
354
|
// ---------------------------------------------------------------------------
|
|
355
|
+
// Worktree subcommand — `gsd worktree <list|merge|clean|remove>`
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
if (cliFlags.messages[0] === 'worktree' || cliFlags.messages[0] === 'wt') {
|
|
358
|
+
const { handleList, handleMerge, handleClean, handleRemove } = await import('./worktree-cli.js');
|
|
359
|
+
const sub = cliFlags.messages[1];
|
|
360
|
+
const subArgs = cliFlags.messages.slice(2);
|
|
361
|
+
if (!sub || sub === 'list') {
|
|
362
|
+
handleList(process.cwd());
|
|
363
|
+
}
|
|
364
|
+
else if (sub === 'merge') {
|
|
365
|
+
await handleMerge(process.cwd(), subArgs);
|
|
366
|
+
}
|
|
367
|
+
else if (sub === 'clean') {
|
|
368
|
+
handleClean(process.cwd());
|
|
369
|
+
}
|
|
370
|
+
else if (sub === 'remove' || sub === 'rm') {
|
|
371
|
+
handleRemove(process.cwd(), subArgs);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
process.stderr.write(`Unknown worktree command: ${sub}\n`);
|
|
375
|
+
process.stderr.write('Commands: list, merge [name], clean, remove <name>\n');
|
|
376
|
+
}
|
|
377
|
+
process.exit(0);
|
|
378
|
+
}
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
// Worktree flag (-w) — create/resume a worktree for the interactive session
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
if (cliFlags.worktree) {
|
|
383
|
+
const { handleWorktreeFlag } = await import('./worktree-cli.js');
|
|
384
|
+
handleWorktreeFlag(cliFlags.worktree);
|
|
385
|
+
}
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// Active worktree banner — remind user of unmerged worktrees on normal launch
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
if (!cliFlags.worktree && !isPrintMode) {
|
|
390
|
+
try {
|
|
391
|
+
const { handleStatusBanner } = await import('./worktree-cli.js');
|
|
392
|
+
handleStatusBanner(process.cwd());
|
|
393
|
+
}
|
|
394
|
+
catch { /* non-fatal */ }
|
|
395
|
+
}
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
346
397
|
// Interactive mode — normal TTY session
|
|
347
398
|
// ---------------------------------------------------------------------------
|
|
348
399
|
// Per-directory session storage — same encoding as the upstream SDK so that
|
package/dist/help-text.js
CHANGED
|
@@ -29,6 +29,37 @@ const SUBCOMMAND_HELP = {
|
|
|
29
29
|
'',
|
|
30
30
|
'Compare with --continue (-c) which always resumes the most recent session.',
|
|
31
31
|
].join('\n'),
|
|
32
|
+
worktree: [
|
|
33
|
+
'Usage: gsd worktree <command> [args]',
|
|
34
|
+
'',
|
|
35
|
+
'Manage isolated git worktrees for parallel work streams.',
|
|
36
|
+
'',
|
|
37
|
+
'Commands:',
|
|
38
|
+
' list List worktrees with status (files changed, commits, dirty)',
|
|
39
|
+
' merge [name] Squash-merge a worktree into main and clean up',
|
|
40
|
+
' clean Remove all worktrees that have been merged or are empty',
|
|
41
|
+
' remove <name> Remove a worktree (--force to remove with unmerged changes)',
|
|
42
|
+
'',
|
|
43
|
+
'The -w flag creates/resumes worktrees for interactive sessions:',
|
|
44
|
+
' gsd -w Auto-name a new worktree, or resume the only active one',
|
|
45
|
+
' gsd -w my-feature Create or resume a named worktree',
|
|
46
|
+
'',
|
|
47
|
+
'Lifecycle:',
|
|
48
|
+
' 1. gsd -w Create worktree, start session inside it',
|
|
49
|
+
' 2. (work normally) All changes happen on the worktree branch',
|
|
50
|
+
' 3. Ctrl+C Exit — dirty work is auto-committed',
|
|
51
|
+
' 4. gsd -w Resume where you left off',
|
|
52
|
+
' 5. gsd worktree merge Squash-merge into main when done',
|
|
53
|
+
'',
|
|
54
|
+
'Examples:',
|
|
55
|
+
' gsd -w Start in a new auto-named worktree',
|
|
56
|
+
' gsd -w auth-refactor Create/resume "auth-refactor" worktree',
|
|
57
|
+
' gsd worktree list See all worktrees and their status',
|
|
58
|
+
' gsd worktree merge auth-refactor Merge and clean up',
|
|
59
|
+
' gsd worktree clean Remove all merged/empty worktrees',
|
|
60
|
+
' gsd worktree remove old-branch Remove a specific worktree',
|
|
61
|
+
' gsd worktree remove old-branch --force Remove even with unmerged changes',
|
|
62
|
+
].join('\n'),
|
|
32
63
|
headless: [
|
|
33
64
|
'Usage: gsd headless [flags] [command] [args...]',
|
|
34
65
|
'',
|
|
@@ -72,6 +103,8 @@ const SUBCOMMAND_HELP = {
|
|
|
72
103
|
'Exit codes: 0 = complete, 1 = error/timeout, 2 = blocked',
|
|
73
104
|
].join('\n'),
|
|
74
105
|
};
|
|
106
|
+
// Alias: `gsd wt --help` → same as `gsd worktree --help`
|
|
107
|
+
SUBCOMMAND_HELP['wt'] = SUBCOMMAND_HELP['worktree'];
|
|
75
108
|
export function printHelp(version) {
|
|
76
109
|
process.stdout.write(`GSD v${version} — Get Shit Done\n\n`);
|
|
77
110
|
process.stdout.write('Usage: gsd [options] [message...]\n\n');
|
|
@@ -79,6 +112,7 @@ export function printHelp(version) {
|
|
|
79
112
|
process.stdout.write(' --mode <text|json|rpc|mcp> Output mode (default: interactive)\n');
|
|
80
113
|
process.stdout.write(' --print, -p Single-shot print mode\n');
|
|
81
114
|
process.stdout.write(' --continue, -c Resume the most recent session\n');
|
|
115
|
+
process.stdout.write(' --worktree, -w [name] Start in an isolated worktree (auto-named if omitted)\n');
|
|
82
116
|
process.stdout.write(' --model <id> Override model (e.g. claude-opus-4-6)\n');
|
|
83
117
|
process.stdout.write(' --no-session Disable session persistence\n');
|
|
84
118
|
process.stdout.write(' --extension <path> Load additional extension\n');
|
|
@@ -90,6 +124,7 @@ export function printHelp(version) {
|
|
|
90
124
|
process.stdout.write(' config Re-run the setup wizard\n');
|
|
91
125
|
process.stdout.write(' update Update GSD to the latest version\n');
|
|
92
126
|
process.stdout.write(' sessions List and resume a past session\n');
|
|
127
|
+
process.stdout.write(' worktree <cmd> Manage worktrees (list, merge, clean, remove)\n');
|
|
93
128
|
process.stdout.write(' headless [cmd] [args] Run /gsd commands without TUI (default: auto)\n');
|
|
94
129
|
process.stdout.write('\nRun gsd <subcommand> --help for subcommand-specific help.\n');
|
|
95
130
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Auth Refresh Extension
|
|
3
|
+
*
|
|
4
|
+
* Automatically refreshes AWS credentials when Bedrock API requests fail
|
|
5
|
+
* with authentication/token errors, then retries the user's message.
|
|
6
|
+
*
|
|
7
|
+
* ## How it works
|
|
8
|
+
*
|
|
9
|
+
* Hooks into `agent_end` to check if the last assistant message failed with
|
|
10
|
+
* an AWS auth error (expired SSO token, missing credentials, etc.). If so:
|
|
11
|
+
*
|
|
12
|
+
* 1. Runs the configured `awsAuthRefresh` command (e.g. `aws sso login`)
|
|
13
|
+
* 2. Streams the SSO auth URL and verification code to the TUI so users
|
|
14
|
+
* can copy/paste if the browser doesn't auto-open
|
|
15
|
+
* 3. Calls `retryLastTurn()` which removes the failed assistant response
|
|
16
|
+
* and re-runs the agent from the user's original message
|
|
17
|
+
*
|
|
18
|
+
* ## Activation
|
|
19
|
+
*
|
|
20
|
+
* This extension is completely inert unless BOTH conditions are met:
|
|
21
|
+
* 1. A Bedrock API request fails with a recognized AWS auth error
|
|
22
|
+
* 2. `awsAuthRefresh` is configured in settings.json
|
|
23
|
+
*
|
|
24
|
+
* Non-Bedrock users and Bedrock users without `awsAuthRefresh` configured
|
|
25
|
+
* are not affected in any way.
|
|
26
|
+
*
|
|
27
|
+
* ## Setup
|
|
28
|
+
*
|
|
29
|
+
* Add to ~/.gsd/agent/settings.json (or project-level .gsd/settings.json):
|
|
30
|
+
*
|
|
31
|
+
* { "awsAuthRefresh": "aws sso login --profile my-profile" }
|
|
32
|
+
*
|
|
33
|
+
* ## Matched error patterns
|
|
34
|
+
*
|
|
35
|
+
* The extension recognizes errors from the AWS SDK, Bedrock, and SSO
|
|
36
|
+
* credential providers including:
|
|
37
|
+
* - ExpiredTokenException / ExpiredToken
|
|
38
|
+
* - The security token included in the request is expired
|
|
39
|
+
* - The SSO session associated with this profile has expired or is invalid
|
|
40
|
+
* - Unable to locate credentials / Could not load credentials
|
|
41
|
+
* - UnrecognizedClientException
|
|
42
|
+
* - Error loading SSO Token / Token does not exist
|
|
43
|
+
* - SSOTokenProviderFailure
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { exec } from "node:child_process";
|
|
47
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
48
|
+
import { join } from "node:path";
|
|
49
|
+
import { homedir } from "node:os";
|
|
50
|
+
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
51
|
+
|
|
52
|
+
/** Matches AWS SDK / Bedrock / SSO credential and token errors. */
|
|
53
|
+
const AWS_AUTH_ERROR_RE =
|
|
54
|
+
/ExpiredToken|security token.*expired|unable to locate credentials|SSO.*(?:session|token).*(?:expired|not found|invalid)|UnrecognizedClient|Could not load credentials|Invalid identity token|token is expired|credentials.*(?:could not|cannot|failed to).*(?:load|resolve|find)|The.*token.*is.*not.*valid|token has expired|SSOTokenProviderFailure|Error loading SSO Token|Token.*does not exist/i;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reads the `awsAuthRefresh` command from settings.json.
|
|
58
|
+
* Checks project-level first, then global (~/.gsd/agent/settings.json).
|
|
59
|
+
*/
|
|
60
|
+
function getAwsAuthRefreshCommand(): string | undefined {
|
|
61
|
+
const configDir = process.env.PI_CONFIG_DIR || ".gsd";
|
|
62
|
+
const paths = [
|
|
63
|
+
join(process.cwd(), configDir, "settings.json"),
|
|
64
|
+
join(homedir(), configDir, "agent", "settings.json"),
|
|
65
|
+
];
|
|
66
|
+
for (const settingsPath of paths) {
|
|
67
|
+
if (!existsSync(settingsPath)) continue;
|
|
68
|
+
try {
|
|
69
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
70
|
+
if (settings.awsAuthRefresh) return settings.awsAuthRefresh;
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Runs the refresh command with a 2-minute timeout (for SSO browser flows).
|
|
78
|
+
* Streams stdout/stderr to capture and display the SSO auth URL and
|
|
79
|
+
* verification code in real-time via TUI notifications.
|
|
80
|
+
*/
|
|
81
|
+
async function runRefresh(
|
|
82
|
+
command: string,
|
|
83
|
+
notify: (msg: string, level: "info" | "warning" | "error") => void,
|
|
84
|
+
): Promise<boolean> {
|
|
85
|
+
notify("Refreshing AWS credentials...", "info");
|
|
86
|
+
try {
|
|
87
|
+
await new Promise<void>((resolve, reject) => {
|
|
88
|
+
const child = exec(command, { timeout: 120_000, env: { ...process.env } });
|
|
89
|
+
const onData = (data: Buffer | string) => {
|
|
90
|
+
const text = data.toString();
|
|
91
|
+
const urlMatch = text.match(/https?:\/\/\S+/);
|
|
92
|
+
if (urlMatch) {
|
|
93
|
+
notify(`Open this URL if the browser didn't launch: ${urlMatch[0]}`, "warning");
|
|
94
|
+
}
|
|
95
|
+
const codeMatch = text.match(/code[:\s]+([A-Z]{4}-[A-Z]{4})/i);
|
|
96
|
+
if (codeMatch) {
|
|
97
|
+
notify(`Verification code: ${codeMatch[1]}`, "info");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
child.stdout?.on("data", onData);
|
|
101
|
+
child.stderr?.on("data", onData);
|
|
102
|
+
child.on("close", (code) => {
|
|
103
|
+
if (code === 0) resolve();
|
|
104
|
+
else reject(new Error(`Refresh command exited with code ${code}`));
|
|
105
|
+
});
|
|
106
|
+
child.on("error", reject);
|
|
107
|
+
});
|
|
108
|
+
notify("AWS credentials refreshed successfully ✓", "info");
|
|
109
|
+
return true;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
112
|
+
const isTimeout = /timed out|ETIMEDOUT|killed/i.test(msg);
|
|
113
|
+
if (isTimeout) {
|
|
114
|
+
notify("AWS credential refresh timed out. The SSO login may have been cancelled or the browser window was closed.", "error");
|
|
115
|
+
} else {
|
|
116
|
+
notify(`AWS credential refresh failed: ${msg}`, "error");
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default function (pi: ExtensionAPI) {
|
|
123
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
124
|
+
const refreshCommand = getAwsAuthRefreshCommand();
|
|
125
|
+
if (!refreshCommand) return;
|
|
126
|
+
|
|
127
|
+
const messages = event.messages;
|
|
128
|
+
const lastAssistant = messages[messages.length - 1];
|
|
129
|
+
if (
|
|
130
|
+
!lastAssistant ||
|
|
131
|
+
lastAssistant.role !== "assistant" ||
|
|
132
|
+
!("errorMessage" in lastAssistant) ||
|
|
133
|
+
!lastAssistant.errorMessage ||
|
|
134
|
+
!AWS_AUTH_ERROR_RE.test(lastAssistant.errorMessage)
|
|
135
|
+
) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const refreshed = await runRefresh(refreshCommand, (m, level) => ctx.ui.notify(m, level));
|
|
140
|
+
if (!refreshed) return;
|
|
141
|
+
|
|
142
|
+
pi.retryLastTurn();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -205,13 +205,6 @@ export function estimateTimeRemaining(): string | null {
|
|
|
205
205
|
|
|
206
206
|
// ─── Slice Progress Cache ─────────────────────────────────────────────────────
|
|
207
207
|
|
|
208
|
-
/** Cached task detail for the widget task checklist */
|
|
209
|
-
interface CachedTaskDetail {
|
|
210
|
-
id: string;
|
|
211
|
-
title: string;
|
|
212
|
-
done: boolean;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
208
|
/** Cached slice progress for the widget — avoid async in render */
|
|
216
209
|
let cachedSliceProgress: {
|
|
217
210
|
done: number;
|
|
@@ -219,8 +212,6 @@ let cachedSliceProgress: {
|
|
|
219
212
|
milestoneId: string;
|
|
220
213
|
/** Real task progress for the active slice, if its plan file exists */
|
|
221
214
|
activeSliceTasks: { done: number; total: number } | null;
|
|
222
|
-
/** Full task list for the active slice checklist */
|
|
223
|
-
taskDetails: CachedTaskDetail[] | null;
|
|
224
215
|
} | null = null;
|
|
225
216
|
|
|
226
217
|
export function updateSliceProgressCache(base: string, mid: string, activeSid?: string): void {
|
|
@@ -231,7 +222,6 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
|
|
|
231
222
|
const roadmap = parseRoadmap(content);
|
|
232
223
|
|
|
233
224
|
let activeSliceTasks: { done: number; total: number } | null = null;
|
|
234
|
-
let taskDetails: CachedTaskDetail[] | null = null;
|
|
235
225
|
if (activeSid) {
|
|
236
226
|
try {
|
|
237
227
|
const planFile = resolveSliceFile(base, mid, activeSid, "PLAN");
|
|
@@ -242,7 +232,6 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
|
|
|
242
232
|
done: plan.tasks.filter(t => t.done).length,
|
|
243
233
|
total: plan.tasks.length,
|
|
244
234
|
};
|
|
245
|
-
taskDetails = plan.tasks.map(t => ({ id: t.id, title: t.title, done: t.done }));
|
|
246
235
|
}
|
|
247
236
|
} catch {
|
|
248
237
|
// Non-fatal — just omit task count
|
|
@@ -254,19 +243,13 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
|
|
|
254
243
|
total: roadmap.slices.length,
|
|
255
244
|
milestoneId: mid,
|
|
256
245
|
activeSliceTasks,
|
|
257
|
-
taskDetails,
|
|
258
246
|
};
|
|
259
247
|
} catch {
|
|
260
248
|
// Non-fatal — widget just won't show progress bar
|
|
261
249
|
}
|
|
262
250
|
}
|
|
263
251
|
|
|
264
|
-
export function getRoadmapSlicesSync(): {
|
|
265
|
-
done: number;
|
|
266
|
-
total: number;
|
|
267
|
-
activeSliceTasks: { done: number; total: number } | null;
|
|
268
|
-
taskDetails: CachedTaskDetail[] | null;
|
|
269
|
-
} | null {
|
|
252
|
+
export function getRoadmapSlicesSync(): { done: number; total: number; activeSliceTasks: { done: number; total: number } | null } | null {
|
|
270
253
|
return cachedSliceProgress;
|
|
271
254
|
}
|
|
272
255
|
|
|
@@ -367,84 +350,87 @@ export function updateProgressWidget(
|
|
|
367
350
|
const lines: string[] = [];
|
|
368
351
|
const pad = INDENT.base;
|
|
369
352
|
|
|
370
|
-
// ── Top bar
|
|
353
|
+
// ── Line 1: Top bar ───────────────────────────────────────────────
|
|
371
354
|
lines.push(...ui.bar());
|
|
372
355
|
|
|
373
|
-
// ── Header: GSD AUTO ... elapsed ────────────────────────────────
|
|
374
356
|
const dot = pulseBright
|
|
375
357
|
? theme.fg("accent", GLYPH.statusActive)
|
|
376
358
|
: theme.fg("dim", GLYPH.statusPending);
|
|
377
359
|
const elapsed = formatAutoElapsed(accessors.getAutoStartTime());
|
|
378
360
|
const modeTag = accessors.isStepMode() ? "NEXT" : "AUTO";
|
|
379
|
-
const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))}
|
|
361
|
+
const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
|
|
380
362
|
const headerRight = elapsed ? theme.fg("dim", elapsed) : "";
|
|
381
363
|
lines.push(rightAlign(headerLeft, headerRight, width));
|
|
382
364
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (mid)
|
|
365
|
+
lines.push("");
|
|
366
|
+
|
|
367
|
+
if (mid) {
|
|
368
|
+
lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}`, width));
|
|
369
|
+
}
|
|
370
|
+
|
|
386
371
|
if (slice && unitType !== "research-milestone" && unitType !== "plan-milestone") {
|
|
387
|
-
|
|
372
|
+
lines.push(truncateToWidth(
|
|
373
|
+
`${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`,
|
|
374
|
+
width,
|
|
375
|
+
));
|
|
388
376
|
}
|
|
377
|
+
|
|
378
|
+
lines.push("");
|
|
379
|
+
|
|
389
380
|
const isHook = unitType.startsWith("hook/");
|
|
390
381
|
const target = isHook
|
|
391
382
|
? (unitId.split("/").pop() ?? unitId)
|
|
392
383
|
: (task ? `${task.id}: ${task.title}` : unitId);
|
|
393
|
-
|
|
394
|
-
|
|
384
|
+
const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
|
|
395
385
|
const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
|
|
396
386
|
const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
|
|
397
|
-
|
|
398
|
-
lines.push(
|
|
399
|
-
|
|
400
|
-
// ── Two-column body ─────────────────────────────────────────────
|
|
401
|
-
// Left: progress, ETA, next, stats (fixed) | Right: task checklist (fixed, adjacent)
|
|
402
|
-
// Both columns sit left-to-center; empty space is on the right.
|
|
403
|
-
const divider = theme.fg("dim", "│");
|
|
404
|
-
const minTwoColWidth = 100;
|
|
405
|
-
const rightColFixed = 44;
|
|
406
|
-
const colGap = 5; // breathing room between columns
|
|
407
|
-
// Left column takes remaining space — no truncation on wide terminals
|
|
408
|
-
const useTwoCol = width >= minTwoColWidth;
|
|
409
|
-
const rightColWidth = useTwoCol ? rightColFixed : 0;
|
|
410
|
-
const leftColWidth = useTwoCol ? width - rightColWidth - colGap : width;
|
|
411
|
-
|
|
412
|
-
const roadmapSlices = mid ? getRoadmapSlicesSync() : null;
|
|
413
|
-
|
|
414
|
-
// Build left column: progress bar, ETA, next step, token stats
|
|
415
|
-
const leftLines: string[] = [];
|
|
416
|
-
|
|
417
|
-
if (roadmapSlices) {
|
|
418
|
-
const { done, total, activeSliceTasks } = roadmapSlices;
|
|
419
|
-
const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
|
|
420
|
-
const pct = total > 0 ? done / total : 0;
|
|
421
|
-
const filled = Math.round(pct * barWidth);
|
|
422
|
-
const bar = theme.fg("success", "█".repeat(filled))
|
|
423
|
-
+ theme.fg("dim", "░".repeat(barWidth - filled));
|
|
424
|
-
|
|
425
|
-
let meta = theme.fg("dim", `${done}/${total} slices`);
|
|
426
|
-
if (activeSliceTasks && activeSliceTasks.total > 0) {
|
|
427
|
-
const taskNum = isHook
|
|
428
|
-
? Math.max(activeSliceTasks.done, 1)
|
|
429
|
-
: Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
|
|
430
|
-
meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
|
|
431
|
-
}
|
|
432
|
-
leftLines.push(truncateToWidth(`${pad}${bar} ${meta}`, leftColWidth));
|
|
387
|
+
lines.push(rightAlign(actionLeft, phaseBadge, width));
|
|
388
|
+
lines.push("");
|
|
433
389
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
390
|
+
if (mid) {
|
|
391
|
+
const roadmapSlices = getRoadmapSlicesSync();
|
|
392
|
+
if (roadmapSlices) {
|
|
393
|
+
const { done, total, activeSliceTasks } = roadmapSlices;
|
|
394
|
+
const barWidth = Math.max(8, Math.min(24, Math.floor(width * 0.3)));
|
|
395
|
+
const pct = total > 0 ? done / total : 0;
|
|
396
|
+
const filled = Math.round(pct * barWidth);
|
|
397
|
+
const bar = theme.fg("success", "█".repeat(filled))
|
|
398
|
+
+ theme.fg("dim", "░".repeat(barWidth - filled));
|
|
399
|
+
|
|
400
|
+
let meta = theme.fg("dim", `${done}/${total} slices`);
|
|
401
|
+
|
|
402
|
+
if (activeSliceTasks && activeSliceTasks.total > 0) {
|
|
403
|
+
// For hooks, show the trigger task number (done), not the next task (done + 1)
|
|
404
|
+
const taskNum = isHook
|
|
405
|
+
? Math.max(activeSliceTasks.done, 1)
|
|
406
|
+
: Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
|
|
407
|
+
meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ETA estimate
|
|
411
|
+
const eta = estimateTimeRemaining();
|
|
412
|
+
if (eta) {
|
|
413
|
+
meta += theme.fg("dim", ` · ${eta}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
|
|
437
417
|
}
|
|
438
418
|
}
|
|
439
419
|
|
|
420
|
+
lines.push("");
|
|
421
|
+
|
|
440
422
|
if (next) {
|
|
441
|
-
|
|
423
|
+
lines.push(truncateToWidth(
|
|
442
424
|
`${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
|
|
443
|
-
|
|
425
|
+
width,
|
|
444
426
|
));
|
|
445
427
|
}
|
|
446
428
|
|
|
447
|
-
//
|
|
429
|
+
// ── Footer info (pwd, tokens, cost, context, model) ──────────────
|
|
430
|
+
lines.push("");
|
|
431
|
+
lines.push(truncateToWidth(theme.fg("dim", `${pad}${widgetPwd}`), width, theme.fg("dim", "…")));
|
|
432
|
+
|
|
433
|
+
// Token stats from current unit session + cumulative cost from metrics
|
|
448
434
|
{
|
|
449
435
|
const cmdCtx = accessors.getCmdCtx();
|
|
450
436
|
let totalInput = 0, totalOutput = 0;
|
|
@@ -479,6 +465,7 @@ export function updateProgressWidget(
|
|
|
479
465
|
if (totalOutput) sp.push(`↓${formatWidgetTokens(totalOutput)}`);
|
|
480
466
|
if (totalCacheRead) sp.push(`R${formatWidgetTokens(totalCacheRead)}`);
|
|
481
467
|
if (totalCacheWrite) sp.push(`W${formatWidgetTokens(totalCacheWrite)}`);
|
|
468
|
+
// Cache hit rate for current unit
|
|
482
469
|
if (totalCacheRead + totalInput > 0) {
|
|
483
470
|
const hitRate = Math.round((totalCacheRead / (totalCacheRead + totalInput)) * 100);
|
|
484
471
|
sp.push(`\u26A1${hitRate}%`);
|
|
@@ -497,134 +484,33 @@ export function updateProgressWidget(
|
|
|
497
484
|
sp.push(cxDisplay);
|
|
498
485
|
}
|
|
499
486
|
|
|
500
|
-
const
|
|
487
|
+
const sLeft = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
|
|
501
488
|
.join(theme.fg("dim", " "));
|
|
502
|
-
leftLines.push(truncateToWidth(`${pad}${tokenLine}`, leftColWidth));
|
|
503
489
|
|
|
504
490
|
const modelId = cmdCtx?.model?.id ?? "";
|
|
505
491
|
const modelProvider = cmdCtx?.model?.provider ?? "";
|
|
492
|
+
const modelPhase = phaseLabel ? theme.fg("dim", `[${phaseLabel}] `) : "";
|
|
506
493
|
const modelDisplay = modelProvider && modelId
|
|
507
494
|
? `${modelProvider}/${modelId}`
|
|
508
495
|
: modelId;
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
496
|
+
const sRight = modelDisplay
|
|
497
|
+
? `${modelPhase}${theme.fg("dim", modelDisplay)}`
|
|
498
|
+
: "";
|
|
499
|
+
lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
|
|
512
500
|
|
|
513
|
-
// Dynamic routing savings
|
|
501
|
+
// Dynamic routing savings summary
|
|
514
502
|
if (mLedger && mLedger.units.some(u => u.tier)) {
|
|
515
503
|
const savings = formatTierSavings(mLedger.units);
|
|
516
504
|
if (savings) {
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Build right column: task checklist (pegged to right edge)
|
|
523
|
-
const rightLines: string[] = [];
|
|
524
|
-
const taskDetails = roadmapSlices?.taskDetails ?? null;
|
|
525
|
-
const maxVisibleTasks = 8;
|
|
526
|
-
const rpad = " ";
|
|
527
|
-
|
|
528
|
-
if (useTwoCol) {
|
|
529
|
-
if (taskDetails && taskDetails.length > 0) {
|
|
530
|
-
const visibleTasks = taskDetails.slice(0, maxVisibleTasks);
|
|
531
|
-
for (const t of visibleTasks) {
|
|
532
|
-
const isCurrent = task && t.id === task.id;
|
|
533
|
-
const glyph = t.done
|
|
534
|
-
? theme.fg("success", GLYPH.statusDone)
|
|
535
|
-
: isCurrent
|
|
536
|
-
? theme.fg("accent", "▸")
|
|
537
|
-
: theme.fg("dim", " ");
|
|
538
|
-
const label = isCurrent
|
|
539
|
-
? theme.fg("text", `${t.id}: ${t.title}`)
|
|
540
|
-
: t.done
|
|
541
|
-
? theme.fg("dim", `${t.id}: ${t.title}`)
|
|
542
|
-
: theme.fg("text", `${t.id}: ${t.title}`);
|
|
543
|
-
rightLines.push(truncateToWidth(`${rpad}${glyph} ${label}`, rightColWidth));
|
|
544
|
-
}
|
|
545
|
-
if (taskDetails.length > maxVisibleTasks) {
|
|
546
|
-
rightLines.push(truncateToWidth(
|
|
547
|
-
`${rpad}${theme.fg("dim", ` …+${taskDetails.length - maxVisibleTasks} more`)}`,
|
|
548
|
-
rightColWidth,
|
|
549
|
-
));
|
|
550
|
-
}
|
|
551
|
-
} else if (roadmapSlices?.activeSliceTasks) {
|
|
552
|
-
const { done: tDone, total: tTotal } = roadmapSlices.activeSliceTasks;
|
|
553
|
-
rightLines.push(`${rpad}${theme.fg("dim", `${tDone}/${tTotal} tasks`)}`);
|
|
554
|
-
}
|
|
555
|
-
} else {
|
|
556
|
-
// Narrow single-column: task list goes into left column
|
|
557
|
-
if (taskDetails && taskDetails.length > 0) {
|
|
558
|
-
for (const t of taskDetails.slice(0, maxVisibleTasks)) {
|
|
559
|
-
const isCurrent = task && t.id === task.id;
|
|
560
|
-
const glyph = t.done
|
|
561
|
-
? theme.fg("success", GLYPH.statusDone)
|
|
562
|
-
: isCurrent
|
|
563
|
-
? theme.fg("accent", "▸")
|
|
564
|
-
: theme.fg("dim", " ");
|
|
565
|
-
const label = isCurrent
|
|
566
|
-
? theme.fg("text", `${t.id}: ${t.title}`)
|
|
567
|
-
: t.done
|
|
568
|
-
? theme.fg("dim", `${t.id}: ${t.title}`)
|
|
569
|
-
: theme.fg("text", `${t.id}: ${t.title}`);
|
|
570
|
-
leftLines.push(truncateToWidth(`${pad}${glyph} ${label}`, leftColWidth));
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
// Add progress bar inline
|
|
574
|
-
if (roadmapSlices) {
|
|
575
|
-
const { done, total, activeSliceTasks } = roadmapSlices;
|
|
576
|
-
const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
|
|
577
|
-
const pct = total > 0 ? done / total : 0;
|
|
578
|
-
const filled = Math.round(pct * barWidth);
|
|
579
|
-
const bar = theme.fg("success", "█".repeat(filled))
|
|
580
|
-
+ theme.fg("dim", "░".repeat(barWidth - filled));
|
|
581
|
-
let meta = theme.fg("dim", `${done}/${total} slices`);
|
|
582
|
-
if (activeSliceTasks && activeSliceTasks.total > 0) {
|
|
583
|
-
const taskNum = isHook
|
|
584
|
-
? Math.max(activeSliceTasks.done, 1)
|
|
585
|
-
: Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
|
|
586
|
-
meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
|
|
505
|
+
lines.push(truncateToWidth(theme.fg("dim", `${pad}${savings}`), width));
|
|
587
506
|
}
|
|
588
|
-
const eta = estimateTimeRemaining();
|
|
589
|
-
if (eta) meta += theme.fg("dim", ` · ${eta}`);
|
|
590
|
-
leftLines.push(truncateToWidth(`${pad}${bar} ${meta}`, leftColWidth));
|
|
591
|
-
}
|
|
592
|
-
if (next) {
|
|
593
|
-
leftLines.push(truncateToWidth(
|
|
594
|
-
`${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
|
|
595
|
-
leftColWidth,
|
|
596
|
-
));
|
|
597
507
|
}
|
|
598
508
|
}
|
|
599
509
|
|
|
600
|
-
// Compose columns
|
|
601
|
-
if (useTwoCol) {
|
|
602
|
-
const maxRows = Math.max(leftLines.length, rightLines.length);
|
|
603
|
-
if (maxRows > 0) {
|
|
604
|
-
lines.push(""); // spacer before columns
|
|
605
|
-
for (let i = 0; i < maxRows; i++) {
|
|
606
|
-
const left = padToWidth(leftLines[i] ?? "", leftColWidth);
|
|
607
|
-
const gap = " ".repeat(colGap - 2); // colGap minus divider and its trailing space
|
|
608
|
-
const right = rightLines[i] ?? "";
|
|
609
|
-
lines.push(truncateToWidth(`${left}${gap}${divider} ${right}`, width));
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
} else {
|
|
613
|
-
// Narrow single-column: just stack
|
|
614
|
-
if (leftLines.length > 0) {
|
|
615
|
-
lines.push("");
|
|
616
|
-
for (const l of leftLines) lines.push(l);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// ── Footer: pwd + hints ─────────────────────────────────────────
|
|
621
|
-
lines.push("");
|
|
622
510
|
const hintParts: string[] = [];
|
|
623
511
|
hintParts.push("esc pause");
|
|
624
512
|
hintParts.push(process.platform === "darwin" ? "⌃⌥G dashboard" : "Ctrl+Alt+G dashboard");
|
|
625
|
-
|
|
626
|
-
const pwdStr = theme.fg("dim", widgetPwd);
|
|
627
|
-
lines.push(rightAlign(`${pad}${pwdStr}`, hintStr, width));
|
|
513
|
+
lines.push(...ui.hints(hintParts));
|
|
628
514
|
|
|
629
515
|
lines.push(...ui.bar());
|
|
630
516
|
|
|
@@ -742,10 +628,3 @@ function rightAlign(left: string, right: string, width: number): string {
|
|
|
742
628
|
const gap = Math.max(1, width - leftVis - rightVis);
|
|
743
629
|
return truncateToWidth(left + " ".repeat(gap) + right, width);
|
|
744
630
|
}
|
|
745
|
-
|
|
746
|
-
/** Pad a string with trailing spaces to fill exactly `colWidth` (ANSI-aware). */
|
|
747
|
-
function padToWidth(s: string, colWidth: number): string {
|
|
748
|
-
const vis = visibleWidth(s);
|
|
749
|
-
if (vis >= colWidth) return truncateToWidth(s, colWidth);
|
|
750
|
-
return s + " ".repeat(colWidth - vis);
|
|
751
|
-
}
|
|
@@ -732,11 +732,7 @@ export async function buildPlanSlicePrompt(
|
|
|
732
732
|
const executorContextConstraints = formatExecutorConstraints();
|
|
733
733
|
|
|
734
734
|
const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
|
|
735
|
-
const
|
|
736
|
-
const commitDocsEnabled = prefs?.preferences?.git?.commit_docs !== false;
|
|
737
|
-
const commitInstruction = commitDocsEnabled
|
|
738
|
-
? `Commit the plan files only: \`git add ${relSlicePath(base, mid, sid)}/ .gsd/DECISIONS.md .gitignore && git commit -m "docs(${sid}): add slice plan"\`. Do not stage .gsd/STATE.md or other runtime files — the system manages those.`
|
|
739
|
-
: "Do not commit — planning docs are not tracked in git for this project.";
|
|
735
|
+
const commitInstruction = "Do not commit planning artifacts — .gsd/ is managed externally.";
|
|
740
736
|
return loadPrompt("plan-slice", {
|
|
741
737
|
workingDirectory: base,
|
|
742
738
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
|
|
@@ -1194,11 +1190,7 @@ export async function buildReassessRoadmapPrompt(
|
|
|
1194
1190
|
// Non-fatal — captures module may not be available
|
|
1195
1191
|
}
|
|
1196
1192
|
|
|
1197
|
-
const
|
|
1198
|
-
const reassessCommitDocsEnabled = reassessPrefs?.preferences?.git?.commit_docs !== false;
|
|
1199
|
-
const reassessCommitInstruction = reassessCommitDocsEnabled
|
|
1200
|
-
? `Commit: \`docs(${mid}): reassess roadmap after ${completedSliceId}\`. Stage only the .gsd/milestones/ files you changed — do not stage .gsd/STATE.md or other runtime files.`
|
|
1201
|
-
: "Do not commit — planning docs are not tracked in git for this project.";
|
|
1193
|
+
const reassessCommitInstruction = "Do not commit planning artifacts — .gsd/ is managed externally.";
|
|
1202
1194
|
|
|
1203
1195
|
return loadPrompt("reassess-roadmap", {
|
|
1204
1196
|
workingDirectory: base,
|