gsd-pi 2.35.0 → 2.36.0-dev.d612764
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 +3 -1
- package/dist/cli.js +7 -2
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +13 -1
- package/dist/resources/extensions/async-jobs/await-tool.js +0 -2
- package/dist/resources/extensions/async-jobs/job-manager.js +0 -6
- package/dist/resources/extensions/bg-shell/output-formatter.js +1 -19
- package/dist/resources/extensions/bg-shell/process-manager.js +0 -4
- package/dist/resources/extensions/bg-shell/types.js +0 -2
- package/dist/resources/extensions/cmux/index.js +321 -0
- package/dist/resources/extensions/context7/index.js +5 -0
- package/dist/resources/extensions/get-secrets-from-user.js +2 -30
- package/dist/resources/extensions/google-search/index.js +5 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
- package/dist/resources/extensions/gsd/auto-dispatch.js +43 -1
- package/dist/resources/extensions/gsd/auto-loop.js +28 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +15 -3
- package/dist/resources/extensions/gsd/auto-recovery.js +35 -0
- package/dist/resources/extensions/gsd/auto-start.js +35 -2
- package/dist/resources/extensions/gsd/auto.js +75 -4
- package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
- package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
- package/dist/resources/extensions/gsd/commands-inspect.js +10 -3
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands-rate.js +31 -0
- package/dist/resources/extensions/gsd/commands.js +94 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +26 -17
- package/dist/resources/extensions/gsd/files.js +11 -2
- package/dist/resources/extensions/gsd/gitignore.js +54 -7
- package/dist/resources/extensions/gsd/guided-flow.js +8 -2
- package/dist/resources/extensions/gsd/health-widget-core.js +96 -0
- package/dist/resources/extensions/gsd/health-widget.js +97 -46
- package/dist/resources/extensions/gsd/index.js +31 -33
- package/dist/resources/extensions/gsd/migrate-external.js +55 -2
- package/dist/resources/extensions/gsd/milestone-ids.js +3 -2
- package/dist/resources/extensions/gsd/notifications.js +10 -1
- package/dist/resources/extensions/gsd/paths.js +74 -7
- package/dist/resources/extensions/gsd/post-unit-hooks.js +4 -1
- package/dist/resources/extensions/gsd/preferences-types.js +2 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +45 -1
- package/dist/resources/extensions/gsd/preferences.js +15 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
- package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +55 -0
- package/dist/resources/extensions/gsd/session-lock.js +53 -2
- package/dist/resources/extensions/gsd/state.js +2 -1
- package/dist/resources/extensions/gsd/templates/plan.md +8 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +12 -0
- package/dist/resources/extensions/remote-questions/remote-command.js +2 -22
- package/dist/resources/extensions/search-the-web/native-search.js +45 -4
- package/dist/resources/extensions/shared/mod.js +1 -1
- package/dist/resources/extensions/shared/sanitize.js +30 -0
- package/dist/resources/extensions/shared/terminal.js +5 -0
- package/dist/resources/extensions/subagent/index.js +186 -74
- package/dist/resources/skills/core-web-vitals/SKILL.md +1 -1
- package/dist/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
- package/dist/resources/skills/github-workflows/SKILL.md +0 -2
- package/dist/resources/skills/web-quality-audit/SKILL.md +0 -2
- package/package.json +2 -1
- package/packages/pi-agent-core/dist/agent.d.ts +10 -2
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +19 -8
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/src/agent.ts +31 -10
- package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-responses.ts +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 +20 -4
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +13 -2
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +36 -12
- package/packages/pi-coding-agent/src/core/resource-loader.ts +13 -2
- package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal-image.js +4 -0
- package/packages/pi-tui/dist/terminal-image.js.map +1 -1
- package/packages/pi-tui/src/terminal-image.ts +5 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/async-jobs/await-tool.ts +0 -2
- package/src/resources/extensions/async-jobs/job-manager.ts +0 -7
- package/src/resources/extensions/bg-shell/output-formatter.ts +0 -17
- package/src/resources/extensions/bg-shell/process-manager.ts +0 -4
- package/src/resources/extensions/bg-shell/types.ts +0 -12
- package/src/resources/extensions/cmux/index.ts +384 -0
- package/src/resources/extensions/context7/index.ts +7 -0
- package/src/resources/extensions/get-secrets-from-user.ts +2 -35
- package/src/resources/extensions/google-search/index.ts +7 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
- package/src/resources/extensions/gsd/auto-dispatch.ts +49 -1
- package/src/resources/extensions/gsd/auto-loop.ts +64 -2
- package/src/resources/extensions/gsd/auto-model-selection.ts +23 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +39 -0
- package/src/resources/extensions/gsd/auto-start.ts +42 -2
- package/src/resources/extensions/gsd/auto.ts +82 -3
- package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
- package/src/resources/extensions/gsd/commands-inspect.ts +10 -3
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands-rate.ts +55 -0
- package/src/resources/extensions/gsd/commands.ts +97 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +26 -16
- package/src/resources/extensions/gsd/files.ts +12 -2
- package/src/resources/extensions/gsd/gitignore.ts +54 -7
- package/src/resources/extensions/gsd/guided-flow.ts +8 -2
- package/src/resources/extensions/gsd/health-widget-core.ts +129 -0
- package/src/resources/extensions/gsd/health-widget.ts +103 -59
- package/src/resources/extensions/gsd/index.ts +37 -32
- package/src/resources/extensions/gsd/migrate-external.ts +47 -2
- package/src/resources/extensions/gsd/milestone-ids.ts +3 -2
- package/src/resources/extensions/gsd/notifications.ts +10 -1
- package/src/resources/extensions/gsd/paths.ts +73 -7
- package/src/resources/extensions/gsd/post-unit-hooks.ts +5 -1
- package/src/resources/extensions/gsd/preferences-types.ts +13 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
- package/src/resources/extensions/gsd/preferences.ts +18 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
- package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +66 -0
- package/src/resources/extensions/gsd/session-lock.ts +59 -2
- package/src/resources/extensions/gsd/state.ts +2 -1
- package/src/resources/extensions/gsd/templates/plan.md +8 -0
- package/src/resources/extensions/gsd/templates/preferences.md +6 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/cmux.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +214 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +158 -0
- package/src/resources/extensions/gsd/tests/paths.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +35 -2
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/test-utils.ts +165 -0
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +32 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +11 -0
- package/src/resources/extensions/remote-questions/remote-command.ts +2 -23
- package/src/resources/extensions/search-the-web/native-search.ts +50 -4
- package/src/resources/extensions/shared/mod.ts +1 -1
- package/src/resources/extensions/shared/sanitize.ts +36 -0
- package/src/resources/extensions/shared/terminal.ts +5 -0
- package/src/resources/extensions/subagent/index.ts +242 -91
- package/src/resources/skills/core-web-vitals/SKILL.md +1 -1
- package/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
- package/src/resources/skills/github-workflows/SKILL.md +0 -2
- package/src/resources/skills/web-quality-audit/SKILL.md +0 -2
- package/dist/resources/extensions/shared/wizard-ui.js +0 -478
- package/dist/resources/skills/swiftui/SKILL.md +0 -208
- package/dist/resources/skills/swiftui/references/animations.md +0 -921
- package/dist/resources/skills/swiftui/references/architecture.md +0 -1561
- package/dist/resources/skills/swiftui/references/layout-system.md +0 -1186
- package/dist/resources/skills/swiftui/references/navigation.md +0 -1492
- package/dist/resources/skills/swiftui/references/networking-async.md +0 -214
- package/dist/resources/skills/swiftui/references/performance.md +0 -1706
- package/dist/resources/skills/swiftui/references/platform-integration.md +0 -204
- package/dist/resources/skills/swiftui/references/state-management.md +0 -1443
- package/dist/resources/skills/swiftui/references/swiftdata.md +0 -297
- package/dist/resources/skills/swiftui/references/testing-debugging.md +0 -247
- package/dist/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
- package/dist/resources/skills/swiftui/workflows/add-feature.md +0 -191
- package/dist/resources/skills/swiftui/workflows/build-new-app.md +0 -311
- package/dist/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
- package/dist/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
- package/dist/resources/skills/swiftui/workflows/ship-app.md +0 -203
- package/dist/resources/skills/swiftui/workflows/write-tests.md +0 -235
- package/src/resources/extensions/shared/wizard-ui.ts +0 -551
- package/src/resources/skills/swiftui/SKILL.md +0 -208
- package/src/resources/skills/swiftui/references/animations.md +0 -921
- package/src/resources/skills/swiftui/references/architecture.md +0 -1561
- package/src/resources/skills/swiftui/references/layout-system.md +0 -1186
- package/src/resources/skills/swiftui/references/navigation.md +0 -1492
- package/src/resources/skills/swiftui/references/networking-async.md +0 -214
- package/src/resources/skills/swiftui/references/performance.md +0 -1706
- package/src/resources/skills/swiftui/references/platform-integration.md +0 -204
- package/src/resources/skills/swiftui/references/state-management.md +0 -1443
- package/src/resources/skills/swiftui/references/swiftdata.md +0 -297
- package/src/resources/skills/swiftui/references/testing-debugging.md +0 -247
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
- package/src/resources/skills/swiftui/workflows/add-feature.md +0 -191
- package/src/resources/skills/swiftui/workflows/build-new-app.md +0 -311
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
- package/src/resources/skills/swiftui/workflows/ship-app.md +0 -203
- package/src/resources/skills/swiftui/workflows/write-tests.md +0 -235
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { clearCmuxSidebar, CmuxClient, detectCmuxEnvironment, resolveCmuxConfig } from "../cmux/index.js";
|
|
4
|
+
import { saveFile } from "./files.js";
|
|
5
|
+
import {
|
|
6
|
+
getProjectGSDPreferencesPath,
|
|
7
|
+
loadEffectiveGSDPreferences,
|
|
8
|
+
loadProjectGSDPreferences,
|
|
9
|
+
} from "./preferences.js";
|
|
10
|
+
import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
|
|
11
|
+
|
|
12
|
+
function extractBodyAfterFrontmatter(content: string): string | null {
|
|
13
|
+
const start = content.startsWith("---\n") ? 4 : content.startsWith("---\r\n") ? 5 : -1;
|
|
14
|
+
if (start === -1) return null;
|
|
15
|
+
const closingIdx = content.indexOf("\n---", start);
|
|
16
|
+
if (closingIdx === -1) return null;
|
|
17
|
+
const after = content.slice(closingIdx + 4);
|
|
18
|
+
return after.trim() ? after : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function writeProjectCmuxPreferences(
|
|
22
|
+
ctx: ExtensionCommandContext,
|
|
23
|
+
updater: (prefs: Record<string, unknown>) => void,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const path = getProjectGSDPreferencesPath();
|
|
26
|
+
await ensurePreferencesFile(path, ctx, "project");
|
|
27
|
+
|
|
28
|
+
const existing = loadProjectGSDPreferences();
|
|
29
|
+
const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : { version: 1 };
|
|
30
|
+
updater(prefs);
|
|
31
|
+
prefs.version = prefs.version || 1;
|
|
32
|
+
|
|
33
|
+
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
34
|
+
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
35
|
+
if (existsSync(path)) {
|
|
36
|
+
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
37
|
+
if (preserved) body = preserved;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await saveFile(path, `---\n${frontmatter}---${body}`);
|
|
41
|
+
await ctx.waitForIdle();
|
|
42
|
+
await ctx.reload();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatCmuxStatus(): string {
|
|
46
|
+
const loaded = loadEffectiveGSDPreferences();
|
|
47
|
+
const detected = detectCmuxEnvironment();
|
|
48
|
+
const resolved = resolveCmuxConfig(loaded?.preferences);
|
|
49
|
+
const capabilities = new CmuxClient(resolved).getCapabilities() as Record<string, unknown> | null;
|
|
50
|
+
const accessMode = typeof capabilities?.mode === "string"
|
|
51
|
+
? capabilities.mode
|
|
52
|
+
: typeof capabilities?.access_mode === "string"
|
|
53
|
+
? capabilities.access_mode
|
|
54
|
+
: "unknown";
|
|
55
|
+
const methods = Array.isArray(capabilities?.methods) ? capabilities.methods.length : 0;
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
"cmux status",
|
|
59
|
+
"",
|
|
60
|
+
`Detected: ${detected.available ? "yes" : "no"}`,
|
|
61
|
+
`Enabled: ${resolved.enabled ? "yes" : "no"}`,
|
|
62
|
+
`CLI available: ${detected.cliAvailable ? "yes" : "no"}`,
|
|
63
|
+
`Socket: ${detected.socketPath}`,
|
|
64
|
+
`Workspace: ${detected.workspaceId ?? "(none)"}`,
|
|
65
|
+
`Surface: ${detected.surfaceId ?? "(none)"}`,
|
|
66
|
+
`Features: notifications=${resolved.notifications ? "on" : "off"}, sidebar=${resolved.sidebar ? "on" : "off"}, splits=${resolved.splits ? "on" : "off"}, browser=${resolved.browser ? "on" : "off"}`,
|
|
67
|
+
`Capabilities: access=${accessMode}, methods=${methods}`,
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ensureCmuxAvailableForEnable(ctx: ExtensionCommandContext): boolean {
|
|
72
|
+
const detected = detectCmuxEnvironment();
|
|
73
|
+
if (detected.available) return true;
|
|
74
|
+
ctx.ui.notify(
|
|
75
|
+
"cmux not detected. Install it from https://cmux.com and run gsd inside a cmux terminal.",
|
|
76
|
+
"warning",
|
|
77
|
+
);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function handleCmux(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
82
|
+
const trimmed = args.trim();
|
|
83
|
+
if (!trimmed || trimmed === "status") {
|
|
84
|
+
ctx.ui.notify(formatCmuxStatus(), "info");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (trimmed === "on") {
|
|
89
|
+
if (!ensureCmuxAvailableForEnable(ctx)) return;
|
|
90
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
91
|
+
prefs.cmux = {
|
|
92
|
+
enabled: true,
|
|
93
|
+
notifications: true,
|
|
94
|
+
sidebar: true,
|
|
95
|
+
splits: false,
|
|
96
|
+
browser: false,
|
|
97
|
+
...((prefs.cmux as Record<string, unknown> | undefined) ?? {}),
|
|
98
|
+
};
|
|
99
|
+
(prefs.cmux as Record<string, unknown>).enabled = true;
|
|
100
|
+
});
|
|
101
|
+
ctx.ui.notify("cmux integration enabled in project preferences.", "info");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (trimmed === "off") {
|
|
106
|
+
const effective = loadEffectiveGSDPreferences()?.preferences;
|
|
107
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
108
|
+
prefs.cmux = { ...((prefs.cmux as Record<string, unknown> | undefined) ?? {}), enabled: false };
|
|
109
|
+
});
|
|
110
|
+
clearCmuxSidebar(effective);
|
|
111
|
+
ctx.ui.notify("cmux integration disabled in project preferences.", "info");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const parts = trimmed.split(/\s+/);
|
|
116
|
+
if (parts.length === 2 && ["notifications", "sidebar", "splits", "browser"].includes(parts[0]) && ["on", "off"].includes(parts[1])) {
|
|
117
|
+
const feature = parts[0] as "notifications" | "sidebar" | "splits" | "browser";
|
|
118
|
+
const enabled = parts[1] === "on";
|
|
119
|
+
if (enabled && !ensureCmuxAvailableForEnable(ctx)) return;
|
|
120
|
+
|
|
121
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
122
|
+
const next = { ...((prefs.cmux as Record<string, unknown> | undefined) ?? {}) };
|
|
123
|
+
next[feature] = enabled;
|
|
124
|
+
if (enabled) next.enabled = true;
|
|
125
|
+
prefs.cmux = next;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!enabled && feature === "sidebar") {
|
|
129
|
+
clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const note = feature === "browser" && enabled
|
|
133
|
+
? " Browser surfaces are still a follow-up path."
|
|
134
|
+
: "";
|
|
135
|
+
ctx.ui.notify(`cmux ${feature} ${enabled ? "enabled" : "disabled"}.${note}`, "info");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ctx.ui.notify(
|
|
140
|
+
"Usage: /gsd cmux <status|on|off|notifications on|notifications off|sidebar on|sidebar off|splits on|splits off|browser on|browser off>",
|
|
141
|
+
"info",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -24,7 +24,7 @@ import { projectRoot } from "./commands.js";
|
|
|
24
24
|
import { loadPrompt } from "./prompt-loader.js";
|
|
25
25
|
|
|
26
26
|
export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
|
|
27
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
27
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
28
28
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
29
29
|
const prompt = loadPrompt("doctor-heal", {
|
|
30
30
|
doctorSummary: reportText,
|
|
@@ -187,7 +187,7 @@ export async function handleTriage(ctx: ExtensionCommandContext, pi: ExtensionAP
|
|
|
187
187
|
roadmapContext: roadmapContext || "(no active roadmap)",
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
190
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
191
191
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
192
192
|
|
|
193
193
|
pi.sendMessage(
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { gsdRoot } from "./paths.js";
|
|
8
11
|
import { getErrorMessage } from "./error-utils.js";
|
|
9
12
|
|
|
10
13
|
export interface InspectData {
|
|
@@ -44,11 +47,15 @@ export function formatInspectOutput(data: InspectData): string {
|
|
|
44
47
|
|
|
45
48
|
export async function handleInspect(ctx: ExtensionCommandContext): Promise<void> {
|
|
46
49
|
try {
|
|
47
|
-
const { isDbAvailable, _getAdapter } = await import("./gsd-db.js");
|
|
50
|
+
const { isDbAvailable, _getAdapter, openDatabase } = await import("./gsd-db.js");
|
|
48
51
|
|
|
49
52
|
if (!isDbAvailable()) {
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
const gsdDir = gsdRoot(process.cwd());
|
|
54
|
+
const dbPath = join(gsdDir, "gsd.db");
|
|
55
|
+
if (!existsSync(gsdDir) || !existsSync(dbPath) || !openDatabase(dbPath)) {
|
|
56
|
+
ctx.ui.notify("No GSD database available. Run /gsd auto to create one.", "info");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
const adapter = _getAdapter();
|
|
@@ -740,7 +740,7 @@ export function serializePreferencesToFrontmatter(prefs: Record<string, unknown>
|
|
|
740
740
|
"skill_rules", "custom_instructions", "models", "skill_discovery",
|
|
741
741
|
"skill_staleness_days", "auto_supervisor", "uat_dispatch", "unique_milestone_ids",
|
|
742
742
|
"budget_ceiling", "budget_enforcement", "context_pause_threshold",
|
|
743
|
-
"notifications", "remote_questions", "git",
|
|
743
|
+
"notifications", "cmux", "remote_questions", "git",
|
|
744
744
|
"post_unit_hooks", "pre_dispatch_hooks",
|
|
745
745
|
"dynamic_routing", "token_profile", "phases", "parallel",
|
|
746
746
|
"auto_visualize", "auto_report",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /gsd rate — Submit feedback on the last unit's model tier assignment.
|
|
3
|
+
* Feeds into the adaptive routing history so future dispatches improve.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
7
|
+
import { loadLedgerFromDisk } from "./metrics.js";
|
|
8
|
+
import { recordFeedback, initRoutingHistory } from "./routing-history.js";
|
|
9
|
+
import type { ComplexityTier } from "./complexity-classifier.js";
|
|
10
|
+
|
|
11
|
+
const VALID_RATINGS = new Set(["over", "under", "ok"]);
|
|
12
|
+
|
|
13
|
+
export async function handleRate(
|
|
14
|
+
args: string,
|
|
15
|
+
ctx: ExtensionCommandContext,
|
|
16
|
+
basePath: string,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const rating = args.trim().toLowerCase();
|
|
19
|
+
|
|
20
|
+
if (!rating || !VALID_RATINGS.has(rating)) {
|
|
21
|
+
ctx.ui.notify(
|
|
22
|
+
"Usage: /gsd rate <over|ok|under>\n" +
|
|
23
|
+
" over — model was overpowered for that task (encourage cheaper)\n" +
|
|
24
|
+
" ok — model was appropriate\n" +
|
|
25
|
+
" under — model was too weak (encourage stronger)",
|
|
26
|
+
"info",
|
|
27
|
+
);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ledger = loadLedgerFromDisk(basePath);
|
|
32
|
+
if (!ledger || ledger.units.length === 0) {
|
|
33
|
+
ctx.ui.notify("No completed units found — nothing to rate.", "warning");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lastUnit = ledger.units[ledger.units.length - 1];
|
|
38
|
+
const tier = lastUnit.tier as ComplexityTier | undefined;
|
|
39
|
+
|
|
40
|
+
if (!tier) {
|
|
41
|
+
ctx.ui.notify(
|
|
42
|
+
"Last unit has no tier data (dynamic routing was not active). Rating skipped.",
|
|
43
|
+
"warning",
|
|
44
|
+
);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
initRoutingHistory(basePath);
|
|
49
|
+
recordFeedback(lastUnit.type, lastUnit.id, tier, rating as "over" | "under" | "ok");
|
|
50
|
+
|
|
51
|
+
ctx.ui.notify(
|
|
52
|
+
`Recorded "${rating}" for ${lastUnit.type}/${lastUnit.id} at tier ${tier}.`,
|
|
53
|
+
"info",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -48,6 +48,8 @@ import { computeProgressScore, formatProgressLine } from "./progress-score.js";
|
|
|
48
48
|
import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
49
49
|
import { handleLogs } from "./commands-logs.js";
|
|
50
50
|
import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
|
|
51
|
+
import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
|
|
52
|
+
import { handleCmux } from "./commands-cmux.js";
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
/** Resolve the effective project root, accounting for worktree paths. */
|
|
@@ -69,9 +71,42 @@ export function projectRoot(): string {
|
|
|
69
71
|
return root;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Check if another process holds the auto-mode session lock.
|
|
76
|
+
* Returns the lock data if a remote session is alive, null otherwise.
|
|
77
|
+
*/
|
|
78
|
+
function getRemoteAutoSession(basePath: string): { pid: number } | null {
|
|
79
|
+
const lockData = readSessionLockData(basePath);
|
|
80
|
+
if (!lockData) return null;
|
|
81
|
+
if (lockData.pid === process.pid) return null;
|
|
82
|
+
if (!isSessionLockProcessAlive(lockData)) return null;
|
|
83
|
+
return { pid: lockData.pid };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Show a steering menu when auto-mode is running in another process.
|
|
88
|
+
* Returns true if a remote session was detected (caller should return early).
|
|
89
|
+
*/
|
|
90
|
+
function notifyRemoteAutoActive(ctx: ExtensionCommandContext, basePath: string): boolean {
|
|
91
|
+
const remote = getRemoteAutoSession(basePath);
|
|
92
|
+
if (!remote) return false;
|
|
93
|
+
ctx.ui.notify(
|
|
94
|
+
`Auto-mode is running in another process (PID ${remote.pid}).\n` +
|
|
95
|
+
`Use these commands to interact with it:\n` +
|
|
96
|
+
` /gsd status — check progress\n` +
|
|
97
|
+
` /gsd discuss — discuss architecture decisions\n` +
|
|
98
|
+
` /gsd queue — queue the next milestone\n` +
|
|
99
|
+
` /gsd steer — apply an override to active work\n` +
|
|
100
|
+
` /gsd capture — fire-and-forget thought\n` +
|
|
101
|
+
` /gsd stop — stop auto-mode`,
|
|
102
|
+
"warning",
|
|
103
|
+
);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
72
107
|
export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
73
108
|
pi.registerCommand("gsd", {
|
|
74
|
-
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|update",
|
|
109
|
+
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|update",
|
|
75
110
|
getArgumentCompletions: (prefix: string) => {
|
|
76
111
|
const subcommands = [
|
|
77
112
|
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
|
@@ -80,6 +115,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
80
115
|
{ cmd: "stop", desc: "Stop auto mode gracefully" },
|
|
81
116
|
{ cmd: "pause", desc: "Pause auto-mode (preserves state, /gsd auto to resume)" },
|
|
82
117
|
{ cmd: "status", desc: "Progress dashboard" },
|
|
118
|
+
{ cmd: "widget", desc: "Cycle widget: full → small → min → off" },
|
|
83
119
|
{ cmd: "visualize", desc: "Open 10-tab workflow visualizer (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)" },
|
|
84
120
|
{ cmd: "queue", desc: "Queue and reorder future milestones" },
|
|
85
121
|
{ cmd: "quick", desc: "Execute a quick task without full planning overhead" },
|
|
@@ -89,6 +125,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
89
125
|
{ cmd: "triage", desc: "Manually trigger triage of pending captures" },
|
|
90
126
|
{ cmd: "dispatch", desc: "Dispatch a specific phase directly" },
|
|
91
127
|
{ cmd: "history", desc: "View execution history" },
|
|
128
|
+
{ cmd: "rate", desc: "Rate last unit's model tier (over/ok/under) — improves adaptive routing" },
|
|
92
129
|
{ cmd: "undo", desc: "Revert last completed unit" },
|
|
93
130
|
{ cmd: "skip", desc: "Prevent a unit from auto-mode dispatch" },
|
|
94
131
|
{ cmd: "export", desc: "Export milestone/slice results" },
|
|
@@ -112,6 +149,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
112
149
|
{ cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
|
|
113
150
|
{ cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
|
|
114
151
|
{ cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
|
|
152
|
+
{ cmd: "cmux", desc: "Manage cmux integration (status, sidebar, notifications, splits)" },
|
|
115
153
|
{ cmd: "park", desc: "Park a milestone — skip without deleting" },
|
|
116
154
|
{ cmd: "unpark", desc: "Reactivate a parked milestone" },
|
|
117
155
|
{ cmd: "update", desc: "Update GSD to the latest version" },
|
|
@@ -168,6 +206,38 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
168
206
|
.map((s) => ({ value: `parallel ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
169
207
|
}
|
|
170
208
|
|
|
209
|
+
if (parts[0] === "cmux") {
|
|
210
|
+
if (parts.length <= 2) {
|
|
211
|
+
const subPrefix = parts[1] ?? "";
|
|
212
|
+
const subs = [
|
|
213
|
+
{ cmd: "status", desc: "Show cmux detection, prefs, and capabilities" },
|
|
214
|
+
{ cmd: "on", desc: "Enable cmux integration" },
|
|
215
|
+
{ cmd: "off", desc: "Disable cmux integration" },
|
|
216
|
+
{ cmd: "notifications", desc: "Toggle cmux desktop notifications" },
|
|
217
|
+
{ cmd: "sidebar", desc: "Toggle cmux sidebar metadata" },
|
|
218
|
+
{ cmd: "splits", desc: "Toggle cmux visual subagent splits" },
|
|
219
|
+
{ cmd: "browser", desc: "Toggle future browser integration flag" },
|
|
220
|
+
];
|
|
221
|
+
return subs
|
|
222
|
+
.filter((s) => s.cmd.startsWith(subPrefix))
|
|
223
|
+
.map((s) => ({ value: `cmux ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (parts.length <= 3 && ["notifications", "sidebar", "splits", "browser"].includes(parts[1])) {
|
|
227
|
+
const togglePrefix = parts[2] ?? "";
|
|
228
|
+
return [
|
|
229
|
+
{ cmd: "on", desc: "Enable this cmux area" },
|
|
230
|
+
{ cmd: "off", desc: "Disable this cmux area" },
|
|
231
|
+
]
|
|
232
|
+
.filter((item) => item.cmd.startsWith(togglePrefix))
|
|
233
|
+
.map((item) => ({
|
|
234
|
+
value: `cmux ${parts[1]} ${item.cmd}`,
|
|
235
|
+
label: item.cmd,
|
|
236
|
+
description: item.desc,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
171
241
|
if (parts[0] === "setup" && parts.length <= 2) {
|
|
172
242
|
const subPrefix = parts[1] ?? "";
|
|
173
243
|
const subs = [
|
|
@@ -439,6 +509,18 @@ export async function handleGSDCommand(
|
|
|
439
509
|
return;
|
|
440
510
|
}
|
|
441
511
|
|
|
512
|
+
if (trimmed === "widget" || trimmed.startsWith("widget ")) {
|
|
513
|
+
const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
|
|
514
|
+
const arg = trimmed.replace(/^widget\s*/, "").trim();
|
|
515
|
+
if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
|
|
516
|
+
setWidgetMode(arg);
|
|
517
|
+
} else {
|
|
518
|
+
cycleWidgetMode();
|
|
519
|
+
}
|
|
520
|
+
ctx.ui.notify(`Widget: ${getWidgetMode()}`, "info");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
442
524
|
if (trimmed === "visualize") {
|
|
443
525
|
await handleVisualize(ctx);
|
|
444
526
|
return;
|
|
@@ -458,6 +540,11 @@ export async function handleGSDCommand(
|
|
|
458
540
|
return;
|
|
459
541
|
}
|
|
460
542
|
|
|
543
|
+
if (trimmed === "cmux" || trimmed.startsWith("cmux ")) {
|
|
544
|
+
await handleCmux(trimmed.replace(/^cmux\s*/, "").trim(), ctx);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
461
548
|
if (trimmed === "init") {
|
|
462
549
|
const { detectProjectState } = await import("./detection.js");
|
|
463
550
|
const { showProjectInit, handleReinit } = await import("./init-wizard.js");
|
|
@@ -511,6 +598,7 @@ export async function handleGSDCommand(
|
|
|
511
598
|
await handleDryRun(ctx, projectRoot());
|
|
512
599
|
return;
|
|
513
600
|
}
|
|
601
|
+
if (notifyRemoteAutoActive(ctx, projectRoot())) return;
|
|
514
602
|
const verboseMode = trimmed.includes("--verbose");
|
|
515
603
|
const debugMode = trimmed.includes("--debug");
|
|
516
604
|
if (debugMode) enableDebug(projectRoot());
|
|
@@ -566,6 +654,12 @@ export async function handleGSDCommand(
|
|
|
566
654
|
return;
|
|
567
655
|
}
|
|
568
656
|
|
|
657
|
+
if (trimmed === "rate" || trimmed.startsWith("rate ")) {
|
|
658
|
+
const { handleRate } = await import("./commands-rate.js");
|
|
659
|
+
await handleRate(trimmed.replace(/^rate\s*/, "").trim(), ctx, projectRoot());
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
569
663
|
if (trimmed.startsWith("skip ")) {
|
|
570
664
|
await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx, projectRoot());
|
|
571
665
|
return;
|
|
@@ -899,7 +993,7 @@ Examples:
|
|
|
899
993
|
}
|
|
900
994
|
|
|
901
995
|
if (trimmed === "") {
|
|
902
|
-
|
|
996
|
+
if (notifyRemoteAutoActive(ctx, projectRoot())) return;
|
|
903
997
|
await startAuto(ctx, pi, projectRoot(), false, { step: true });
|
|
904
998
|
return;
|
|
905
999
|
}
|
|
@@ -954,6 +1048,7 @@ function showHelp(ctx: ExtensionCommandContext): void {
|
|
|
954
1048
|
" /gsd setup Global setup status [llm|search|remote|keys|prefs]",
|
|
955
1049
|
" /gsd mode Set workflow mode (solo/team) [global|project]",
|
|
956
1050
|
" /gsd prefs Manage preferences [global|project|status|wizard|setup|import-claude]",
|
|
1051
|
+
" /gsd cmux Manage cmux integration [status|on|off|notifications|sidebar|splits|browser]",
|
|
957
1052
|
" /gsd config Set API keys for external tools",
|
|
958
1053
|
" /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
|
|
959
1054
|
" /gsd hooks Show post-unit hook configuration",
|
|
@@ -173,6 +173,13 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
173
173
|
- `on_milestone`: boolean — notify when a milestone finishes. Default: `true`.
|
|
174
174
|
- `on_attention`: boolean — notify when manual attention is needed. Default: `true`.
|
|
175
175
|
|
|
176
|
+
- `cmux`: configures cmux terminal integration when GSD is running inside a cmux workspace. Keys:
|
|
177
|
+
- `enabled`: boolean — master toggle for cmux integration. Default: `false`.
|
|
178
|
+
- `notifications`: boolean — route desktop notifications through cmux. Default: `true` when enabled.
|
|
179
|
+
- `sidebar`: boolean — publish status, progress, and log metadata to the cmux sidebar. Default: `true` when enabled.
|
|
180
|
+
- `splits`: boolean — run supported subagent work in visible cmux splits. Default: `false`.
|
|
181
|
+
- `browser`: boolean — reserve the future browser integration flag. Default: `false`.
|
|
182
|
+
|
|
176
183
|
- `dynamic_routing`: configures the dynamic model router that adjusts model selection based on task complexity. Keys:
|
|
177
184
|
- `enabled`: boolean — enable dynamic routing. Default: `false`.
|
|
178
185
|
- `tier_models`: object — model overrides per complexity tier. Keys: `light`, `standard`, `heavy`. Values are model ID strings.
|
|
@@ -477,6 +484,24 @@ Disables per-unit completion notifications (noisy in long runs) while keeping er
|
|
|
477
484
|
|
|
478
485
|
---
|
|
479
486
|
|
|
487
|
+
## cmux Example
|
|
488
|
+
|
|
489
|
+
```yaml
|
|
490
|
+
---
|
|
491
|
+
version: 1
|
|
492
|
+
cmux:
|
|
493
|
+
enabled: true
|
|
494
|
+
notifications: true
|
|
495
|
+
sidebar: true
|
|
496
|
+
splits: true
|
|
497
|
+
browser: false
|
|
498
|
+
---
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Enables cmux-aware notifications, sidebar metadata, and visible subagent splits when GSD is running inside a cmux terminal.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
480
505
|
## Post-Unit Hooks Example
|
|
481
506
|
|
|
482
507
|
```yaml
|
|
@@ -180,26 +180,36 @@ function checkPortConflicts(basePath: string): EnvironmentCheckResult[] {
|
|
|
180
180
|
const portsToCheck = new Set<number>();
|
|
181
181
|
const pkgPath = join(basePath, "package.json");
|
|
182
182
|
|
|
183
|
-
if (existsSync(pkgPath)) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
183
|
+
if (!existsSync(pkgPath)) {
|
|
184
|
+
// No package.json — this isn't a Node.js project. Skip port checks
|
|
185
|
+
// entirely to avoid false positives from system services (e.g., macOS
|
|
186
|
+
// AirPlay Receiver on port 5000). (#1381)
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
192
|
+
const scripts = pkg.scripts ?? {};
|
|
193
|
+
const scriptText = Object.values(scripts).join(" ");
|
|
194
|
+
|
|
195
|
+
// Look for --port NNNN, -p NNNN, PORT=NNNN, :NNNN patterns
|
|
196
|
+
const portMatches = scriptText.matchAll(/(?:--port\s+|(?:^|[^a-z])PORT[=:]\s*|-p\s+|:)(\d{4,5})\b/gi);
|
|
197
|
+
for (const m of portMatches) {
|
|
198
|
+
const port = parseInt(m[1], 10);
|
|
199
|
+
if (port >= 1024 && port <= 65535) portsToCheck.add(port);
|
|
197
200
|
}
|
|
201
|
+
} catch {
|
|
202
|
+
// parse failed — skip port checks rather than using defaults
|
|
203
|
+
return [];
|
|
198
204
|
}
|
|
199
205
|
|
|
200
|
-
// If no ports found in scripts, check common defaults
|
|
206
|
+
// If no ports found in scripts, check common defaults.
|
|
207
|
+
// Filter out port 5000 on macOS — AirPlay Receiver uses it by default (#1381).
|
|
201
208
|
if (portsToCheck.size === 0) {
|
|
202
|
-
for (const p of DEFAULT_DEV_PORTS)
|
|
209
|
+
for (const p of DEFAULT_DEV_PORTS) {
|
|
210
|
+
if (p === 5000 && process.platform === "darwin") continue;
|
|
211
|
+
portsToCheck.add(p);
|
|
212
|
+
}
|
|
203
213
|
}
|
|
204
214
|
|
|
205
215
|
for (const port of portsToCheck) {
|
|
@@ -590,7 +590,8 @@ export async function loadFile(path: string): Promise<string | null> {
|
|
|
590
590
|
try {
|
|
591
591
|
return await fs.readFile(path, 'utf-8');
|
|
592
592
|
} catch (err: unknown) {
|
|
593
|
-
|
|
593
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
594
|
+
if (code === 'ENOENT' || code === 'EISDIR') return null;
|
|
594
595
|
throw err;
|
|
595
596
|
}
|
|
596
597
|
}
|
|
@@ -804,7 +805,7 @@ export async function inlinePriorMilestoneSummary(mid: string, base: string): Pr
|
|
|
804
805
|
* file not on disk) - callers can distinguish "no manifest" from "empty manifest".
|
|
805
806
|
*/
|
|
806
807
|
export async function getManifestStatus(
|
|
807
|
-
base: string, milestoneId: string,
|
|
808
|
+
base: string, milestoneId: string, projectRoot?: string,
|
|
808
809
|
): Promise<ManifestStatus | null> {
|
|
809
810
|
const resolvedPath = resolveMilestoneFile(base, milestoneId, 'SECRETS');
|
|
810
811
|
if (!resolvedPath) return null;
|
|
@@ -814,9 +815,18 @@ export async function getManifestStatus(
|
|
|
814
815
|
|
|
815
816
|
const manifest = parseSecretsManifest(content);
|
|
816
817
|
const keys = manifest.entries.map(e => e.key);
|
|
818
|
+
|
|
819
|
+
// Check both the base path .env AND the project root .env (#1387).
|
|
820
|
+
// In worktree mode, base is the worktree path which may not have .env.
|
|
821
|
+
// The project root's .env is where the user actually defined their keys.
|
|
817
822
|
const existingKeys = await checkExistingEnvKeys(keys, resolve(base, '.env'));
|
|
818
823
|
const existingSet = new Set(existingKeys);
|
|
819
824
|
|
|
825
|
+
if (projectRoot && projectRoot !== base) {
|
|
826
|
+
const rootKeys = await checkExistingEnvKeys(keys, resolve(projectRoot, '.env'));
|
|
827
|
+
for (const k of rootKeys) existingSet.add(k);
|
|
828
|
+
}
|
|
829
|
+
|
|
820
830
|
const result: ManifestStatus = {
|
|
821
831
|
pending: [],
|
|
822
832
|
collected: [],
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { nativeRmCached } from "./native-git-bridge.js";
|
|
10
|
+
import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { nativeRmCached, nativeLsFiles } from "./native-git-bridge.js";
|
|
12
12
|
import { gsdRoot } from "./paths.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -79,12 +79,47 @@ const BASELINE_PATTERNS = [
|
|
|
79
79
|
];
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
82
|
+
* Check whether `.gsd/` contains files tracked by git.
|
|
83
|
+
* If so, the project intentionally keeps `.gsd/` in version control
|
|
84
|
+
* and we must NOT add `.gsd` to `.gitignore` or attempt migration.
|
|
85
|
+
*
|
|
86
|
+
* Returns true if git tracks at least one file under `.gsd/`.
|
|
87
|
+
* Returns false (safe to ignore) if:
|
|
88
|
+
* - Not a git repo
|
|
89
|
+
* - `.gsd/` is a symlink (external state, should be ignored)
|
|
90
|
+
* - `.gsd/` doesn't exist
|
|
91
|
+
* - No tracked files found under `.gsd/`
|
|
92
|
+
*/
|
|
93
|
+
export function hasGitTrackedGsdFiles(basePath: string): boolean {
|
|
94
|
+
const localGsd = join(basePath, ".gsd");
|
|
95
|
+
|
|
96
|
+
// If .gsd doesn't exist or is already a symlink, no tracked files concern
|
|
97
|
+
if (!existsSync(localGsd)) return false;
|
|
98
|
+
try {
|
|
99
|
+
if (lstatSync(localGsd).isSymbolicLink()) return false;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if git tracks any files under .gsd/
|
|
105
|
+
try {
|
|
106
|
+
const tracked = nativeLsFiles(basePath, ".gsd");
|
|
107
|
+
return tracked.length > 0;
|
|
108
|
+
} catch {
|
|
109
|
+
// Not a git repo or git not available — safe to proceed
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Ensure basePath/.gitignore contains baseline ignore patterns.
|
|
116
|
+
* Creates the file if missing; appends missing patterns.
|
|
84
117
|
* Returns true if the file was created or modified, false if already complete.
|
|
85
118
|
*
|
|
86
|
-
* `.gsd/`
|
|
87
|
-
*
|
|
119
|
+
* **Safety check:** If `.gsd/` contains git-tracked files (i.e., the project
|
|
120
|
+
* intentionally keeps `.gsd/` in version control), the `.gsd` ignore pattern
|
|
121
|
+
* is excluded to prevent data loss. Only the `.gsd` pattern is affected —
|
|
122
|
+
* all other baseline patterns are still applied normally.
|
|
88
123
|
*/
|
|
89
124
|
export function ensureGitignore(
|
|
90
125
|
basePath: string,
|
|
@@ -108,8 +143,15 @@ export function ensureGitignore(
|
|
|
108
143
|
.filter((l) => l && !l.startsWith("#")),
|
|
109
144
|
);
|
|
110
145
|
|
|
146
|
+
// Determine which patterns to apply. If .gsd/ has tracked files,
|
|
147
|
+
// exclude the ".gsd" pattern to prevent deleting tracked state.
|
|
148
|
+
const gsdIsTracked = hasGitTrackedGsdFiles(basePath);
|
|
149
|
+
const patternsToApply = gsdIsTracked
|
|
150
|
+
? BASELINE_PATTERNS.filter((p) => p !== ".gsd")
|
|
151
|
+
: BASELINE_PATTERNS;
|
|
152
|
+
|
|
111
153
|
// Find patterns not yet present
|
|
112
|
-
const missing =
|
|
154
|
+
const missing = patternsToApply.filter((p) => !existingLines.has(p));
|
|
113
155
|
|
|
114
156
|
if (missing.length === 0) return false;
|
|
115
157
|
|
|
@@ -135,6 +177,11 @@ export function ensureGitignore(
|
|
|
135
177
|
* already in the index even after .gitignore is updated.
|
|
136
178
|
*
|
|
137
179
|
* Only removes from the index (`--cached`), never from disk. Idempotent.
|
|
180
|
+
*
|
|
181
|
+
* Note: These are strictly runtime/ephemeral paths (activity logs, lock files,
|
|
182
|
+
* metrics, STATE.md). They are always safe to untrack, even when the project
|
|
183
|
+
* intentionally keeps other `.gsd/` files (like PROJECT.md, milestones/) in
|
|
184
|
+
* version control.
|
|
138
185
|
*/
|
|
139
186
|
export function untrackRuntimeFiles(basePath: string): void {
|
|
140
187
|
const runtimePaths = GSD_RUNTIME_PATTERNS;
|