gsd-pi 2.58.0-dev.778d6ac → 2.58.0-dev.e002a57
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 +11 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +11 -8
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -16
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +22 -1
- package/dist/resources/extensions/gsd/codebase-generator.js +279 -0
- package/dist/resources/extensions/gsd/commands/catalog.js +10 -1
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/commands-codebase.js +115 -0
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +41 -4
- package/dist/resources/extensions/gsd/complexity-classifier.js +8 -6
- package/dist/resources/extensions/gsd/doctor-git-checks.js +48 -1
- package/dist/resources/extensions/gsd/doctor-proactive.js +34 -1
- package/dist/resources/extensions/gsd/error-classifier.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +82 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +22 -0
- package/dist/resources/extensions/gsd/paths.js +2 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/watch/header-renderer.js +241 -0
- package/dist/resources/extensions/search-the-web/url-utils.js +17 -0
- package/dist/security-overrides.d.ts +11 -0
- package/dist/security-overrides.js +41 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/welcome-screen.d.ts +1 -0
- package/dist/welcome-screen.js +32 -6
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts +8 -0
- package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js +23 -2
- package/packages/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +89 -2
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager-security.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/settings-manager-security.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager-security.test.js +83 -0
- package/packages/pi-coding-agent/dist/core/settings-manager-security.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +14 -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 +36 -3
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -0
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/armin.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/armin.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/armin.js +9 -8
- package/packages/pi-coding-agent/dist/modes/interactive/components/armin.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +0 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bordered-loader.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bordered-loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.js +5 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.js +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/countdown-timer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/custom-message.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.js +4 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/daxnuts.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +8 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.js +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +26 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/oauth-selector.js +4 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +46 -14
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js +2 -8
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/session-selector.js +4 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +8 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message-selector.js +3 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +15 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +16 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +27 -4
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +6 -0
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
- package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +111 -1
- package/packages/pi-coding-agent/src/core/resolve-config-value.ts +26 -2
- package/packages/pi-coding-agent/src/core/settings-manager-security.test.ts +102 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +44 -3
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/armin.ts +9 -9
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +0 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +3 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/bordered-loader.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts +7 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts +3 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/custom-message.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts +4 -3
- package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +2 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +3 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts +4 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +27 -13
- package/packages/pi-coding-agent/src/modes/interactive/components/oauth-selector.ts +4 -4
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +45 -14
- package/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts +2 -7
- package/packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts +4 -4
- package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +2 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +8 -3
- package/packages/pi-coding-agent/src/modes/interactive/components/user-message-selector.ts +3 -2
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +17 -1
- package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +14 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +35 -3
- package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +7 -0
- package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +1 -1
- package/pkg/dist/modes/interactive/theme/themes.js +1 -1
- package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +10 -7
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +10 -16
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +22 -1
- package/src/resources/extensions/gsd/codebase-generator.ts +351 -0
- package/src/resources/extensions/gsd/commands/catalog.ts +10 -1
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/commands-codebase.ts +164 -0
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +46 -4
- package/src/resources/extensions/gsd/complexity-classifier.ts +8 -6
- package/src/resources/extensions/gsd/doctor-git-checks.ts +49 -1
- package/src/resources/extensions/gsd/doctor-proactive.ts +35 -1
- package/src/resources/extensions/gsd/doctor-types.ts +2 -0
- package/src/resources/extensions/gsd/error-classifier.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +93 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +24 -0
- package/src/resources/extensions/gsd/paths.ts +2 -0
- package/src/resources/extensions/gsd/preferences-types.ts +8 -0
- package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +488 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +44 -0
- package/src/resources/extensions/gsd/watch/header-renderer.ts +275 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +19 -0
- /package/dist/web/standalone/.next/static/{R0D4xaIPl5kg93edN7Oo0 → nUA6d2OJrDSVq9RNb-c8b}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{R0D4xaIPl5kg93edN7Oo0 → nUA6d2OJrDSVq9RNb-c8b}/_ssgManifest.js +0 -0
|
@@ -206,5 +206,10 @@ Examples:
|
|
|
206
206
|
await handleRethink(trimmed, ctx, pi);
|
|
207
207
|
return true;
|
|
208
208
|
}
|
|
209
|
+
if (trimmed === "codebase" || trimmed.startsWith("codebase ")) {
|
|
210
|
+
const { handleCodebase } = await import("../../commands-codebase.js");
|
|
211
|
+
await handleCodebase(trimmed.replace(/^codebase\s*/, "").trim(), ctx, pi);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
209
214
|
return false;
|
|
210
215
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Command — /gsd codebase
|
|
3
|
+
*
|
|
4
|
+
* Generate and manage the codebase map (.gsd/CODEBASE.md).
|
|
5
|
+
* Subcommands: generate, update, stats, help
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
generateCodebaseMap,
|
|
12
|
+
updateCodebaseMap,
|
|
13
|
+
writeCodebaseMap,
|
|
14
|
+
getCodebaseMapStats,
|
|
15
|
+
readCodebaseMap,
|
|
16
|
+
} from "./codebase-generator.js";
|
|
17
|
+
|
|
18
|
+
const USAGE =
|
|
19
|
+
"Usage: /gsd codebase [generate|update|stats]\n\n" +
|
|
20
|
+
" generate [--max-files N] — Generate or regenerate CODEBASE.md\n" +
|
|
21
|
+
" update — Incremental update (preserves descriptions)\n" +
|
|
22
|
+
" stats — Show file count, coverage, and generation time\n" +
|
|
23
|
+
" help — Show this help\n\n" +
|
|
24
|
+
"With no subcommand, shows stats if a map exists or help if not.";
|
|
25
|
+
|
|
26
|
+
export async function handleCodebase(
|
|
27
|
+
args: string,
|
|
28
|
+
ctx: ExtensionCommandContext,
|
|
29
|
+
_pi: ExtensionAPI,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const basePath = process.cwd();
|
|
32
|
+
const parts = args.trim().split(/\s+/);
|
|
33
|
+
const sub = parts[0] ?? "";
|
|
34
|
+
|
|
35
|
+
switch (sub) {
|
|
36
|
+
case "generate": {
|
|
37
|
+
const maxFiles = parseMaxFiles(args, ctx);
|
|
38
|
+
if (maxFiles === false) return; // validation failed, message already shown
|
|
39
|
+
|
|
40
|
+
const existing = readCodebaseMap(basePath);
|
|
41
|
+
const existingDescriptions = existing
|
|
42
|
+
? (await import("./codebase-generator.js")).parseCodebaseMap(existing)
|
|
43
|
+
: undefined;
|
|
44
|
+
|
|
45
|
+
const result = generateCodebaseMap(basePath, { maxFiles: maxFiles ?? undefined }, existingDescriptions);
|
|
46
|
+
|
|
47
|
+
if (result.fileCount === 0) {
|
|
48
|
+
ctx.ui.notify(
|
|
49
|
+
"Codebase map generated with 0 files.\n" +
|
|
50
|
+
"Is this a git repository? Run 'git ls-files' to verify.",
|
|
51
|
+
"warning",
|
|
52
|
+
);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const outPath = writeCodebaseMap(basePath, result.content);
|
|
57
|
+
ctx.ui.notify(
|
|
58
|
+
`Codebase map generated: ${result.fileCount} files\n` +
|
|
59
|
+
`Written to: ${outPath}` +
|
|
60
|
+
(result.truncated ? `\n⚠ Truncated — increase --max-files to include all files` : ""),
|
|
61
|
+
"success",
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case "update": {
|
|
67
|
+
const existing = readCodebaseMap(basePath);
|
|
68
|
+
if (!existing) {
|
|
69
|
+
ctx.ui.notify(
|
|
70
|
+
"No codebase map found. Run /gsd codebase generate to create one.",
|
|
71
|
+
"warning",
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const maxFiles = parseMaxFiles(args, ctx);
|
|
77
|
+
if (maxFiles === false) return;
|
|
78
|
+
|
|
79
|
+
const result = updateCodebaseMap(basePath, { maxFiles: maxFiles ?? undefined });
|
|
80
|
+
writeCodebaseMap(basePath, result.content);
|
|
81
|
+
|
|
82
|
+
ctx.ui.notify(
|
|
83
|
+
`Codebase map updated: ${result.fileCount} files\n` +
|
|
84
|
+
` Added: ${result.added} | Removed: ${result.removed} | Unchanged: ${result.unchanged}` +
|
|
85
|
+
(result.truncated ? `\n⚠ Truncated — increase --max-files to include all files` : ""),
|
|
86
|
+
"success",
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case "stats": {
|
|
92
|
+
showStats(basePath, ctx);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case "help":
|
|
97
|
+
ctx.ui.notify(USAGE, "info");
|
|
98
|
+
return;
|
|
99
|
+
|
|
100
|
+
case "": {
|
|
101
|
+
// Safe default: show stats if map exists, help if not
|
|
102
|
+
const existing = readCodebaseMap(basePath);
|
|
103
|
+
if (existing) {
|
|
104
|
+
showStats(basePath, ctx);
|
|
105
|
+
} else {
|
|
106
|
+
ctx.ui.notify(USAGE, "info");
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
default:
|
|
112
|
+
ctx.ui.notify(
|
|
113
|
+
`Unknown subcommand "${sub}".\n\n${USAGE}`,
|
|
114
|
+
"warning",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function showStats(basePath: string, ctx: ExtensionCommandContext): void {
|
|
120
|
+
const stats = getCodebaseMapStats(basePath);
|
|
121
|
+
if (!stats.exists) {
|
|
122
|
+
ctx.ui.notify("No codebase map found. Run /gsd codebase generate to create one.", "info");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const coverage = stats.fileCount > 0
|
|
127
|
+
? Math.round((stats.describedCount / stats.fileCount) * 100)
|
|
128
|
+
: 0;
|
|
129
|
+
|
|
130
|
+
ctx.ui.notify(
|
|
131
|
+
`Codebase Map Stats:\n` +
|
|
132
|
+
` Files: ${stats.fileCount}\n` +
|
|
133
|
+
` Described: ${stats.describedCount} (${coverage}%)\n` +
|
|
134
|
+
` Undescribed: ${stats.undescribedCount}\n` +
|
|
135
|
+
` Generated: ${stats.generatedAt ?? "unknown"}\n\n` +
|
|
136
|
+
(stats.undescribedCount > 0
|
|
137
|
+
? `Tip: Run /gsd codebase update to refresh after file changes.`
|
|
138
|
+
: `Coverage is complete.`),
|
|
139
|
+
"info",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Parse and validate --max-files flag.
|
|
145
|
+
* Returns the parsed number, undefined if flag not present, or false if invalid.
|
|
146
|
+
*/
|
|
147
|
+
function parseMaxFiles(args: string, ctx: ExtensionCommandContext): number | undefined | false {
|
|
148
|
+
const maxFilesStr = extractFlag(args, "--max-files");
|
|
149
|
+
if (!maxFilesStr) return undefined;
|
|
150
|
+
|
|
151
|
+
const maxFiles = parseInt(maxFilesStr, 10);
|
|
152
|
+
if (isNaN(maxFiles) || maxFiles < 1) {
|
|
153
|
+
ctx.ui.notify("--max-files must be a positive integer (e.g. --max-files 200).", "warning");
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
return maxFiles;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractFlag(args: string, flag: string): string | undefined {
|
|
160
|
+
const escaped = flag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
161
|
+
const regex = new RegExp(`${escaped}[=\\s]+(\\S+)`);
|
|
162
|
+
const match = args.match(regex);
|
|
163
|
+
return match?.[1];
|
|
164
|
+
}
|
|
@@ -184,11 +184,23 @@ export function buildCategorySummaries(prefs: Record<string, unknown>): Record<s
|
|
|
184
184
|
|
|
185
185
|
// Git
|
|
186
186
|
const git = prefs.git as Record<string, unknown> | undefined;
|
|
187
|
+
const staleThreshold = prefs.stale_commit_threshold_minutes;
|
|
188
|
+
const absorbSnapshots = git?.absorb_snapshot_commits;
|
|
187
189
|
let gitSummary = "(defaults)";
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
{
|
|
191
|
+
const parts: string[] = [];
|
|
192
|
+
if (git && Object.keys(git).length > 0) {
|
|
193
|
+
const branch = git.main_branch ?? "main";
|
|
194
|
+
const push = git.auto_push ? "on" : "off";
|
|
195
|
+
parts.push(`main: ${branch}, push: ${push}`);
|
|
196
|
+
}
|
|
197
|
+
if (staleThreshold !== undefined) {
|
|
198
|
+
parts.push(`stale: ${staleThreshold === 0 ? "off" : `${staleThreshold}m`}`);
|
|
199
|
+
}
|
|
200
|
+
if (absorbSnapshots !== undefined) {
|
|
201
|
+
parts.push(`absorb: ${absorbSnapshots ? "on" : "off"}`);
|
|
202
|
+
}
|
|
203
|
+
if (parts.length > 0) gitSummary = parts.join(", ");
|
|
192
204
|
}
|
|
193
205
|
|
|
194
206
|
// Skills
|
|
@@ -469,9 +481,39 @@ async function configureGit(ctx: ExtensionCommandContext, prefs: Record<string,
|
|
|
469
481
|
git.isolation = isolationChoice;
|
|
470
482
|
}
|
|
471
483
|
|
|
484
|
+
// absorb_snapshot_commits (git sub-key)
|
|
485
|
+
const currentAbsorb = git.absorb_snapshot_commits;
|
|
486
|
+
const absorbStr = currentAbsorb !== undefined ? String(currentAbsorb) : "";
|
|
487
|
+
const absorbChoice = await ctx.ui.select(
|
|
488
|
+
`Absorb snapshot commits into real commits${absorbStr ? ` (current: ${absorbStr})` : " (default: true)"}:`,
|
|
489
|
+
["true", "false", "(keep current)"],
|
|
490
|
+
);
|
|
491
|
+
if (absorbChoice && absorbChoice !== "(keep current)") {
|
|
492
|
+
git.absorb_snapshot_commits = absorbChoice === "true";
|
|
493
|
+
}
|
|
494
|
+
|
|
472
495
|
if (Object.keys(git).length > 0) {
|
|
473
496
|
prefs.git = git;
|
|
474
497
|
}
|
|
498
|
+
|
|
499
|
+
// stale_commit_threshold_minutes (top-level pref, shown in Git section)
|
|
500
|
+
const currentThreshold = prefs.stale_commit_threshold_minutes;
|
|
501
|
+
const thresholdStr = currentThreshold !== undefined ? String(currentThreshold) : "";
|
|
502
|
+
const thresholdInput = await ctx.ui.input(
|
|
503
|
+
`Stale commit threshold (minutes, 0 to disable)${thresholdStr ? ` (current: ${thresholdStr})` : " (default: 30)"}:`,
|
|
504
|
+
thresholdStr || "30",
|
|
505
|
+
);
|
|
506
|
+
if (thresholdInput !== null && thresholdInput !== undefined) {
|
|
507
|
+
const val = thresholdInput.trim();
|
|
508
|
+
const parsed = tryParseInteger(val);
|
|
509
|
+
if (val && parsed !== null && parsed >= 0) {
|
|
510
|
+
prefs.stale_commit_threshold_minutes = parsed;
|
|
511
|
+
} else if (val && parsed === null) {
|
|
512
|
+
ctx.ui.notify(`Invalid value "${val}" — must be a whole number. Keeping previous value.`, "warning");
|
|
513
|
+
} else if (!val && currentThreshold !== undefined) {
|
|
514
|
+
delete prefs.stale_commit_threshold_minutes;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
475
517
|
}
|
|
476
518
|
|
|
477
519
|
async function configureSkills(ctx: ExtensionCommandContext, prefs: Record<string, unknown>): Promise<void> {
|
|
@@ -35,15 +35,17 @@ const UNIT_TYPE_TIERS: Record<string, ComplexityTier> = {
|
|
|
35
35
|
"complete-slice": "light",
|
|
36
36
|
"run-uat": "light",
|
|
37
37
|
|
|
38
|
-
// Tier 2 — Standard: research, routine
|
|
38
|
+
// Tier 2 — Standard: research, routine discussion
|
|
39
39
|
"discuss-milestone": "standard",
|
|
40
40
|
"discuss-slice": "standard",
|
|
41
41
|
"research-milestone": "standard",
|
|
42
42
|
"research-slice": "standard",
|
|
43
|
-
"plan-milestone": "standard",
|
|
44
|
-
"plan-slice": "standard",
|
|
45
43
|
|
|
46
|
-
// Tier 3 — Heavy: execution, replanning (requires deep reasoning)
|
|
44
|
+
// Tier 3 — Heavy: planning, execution, replanning (requires deep reasoning)
|
|
45
|
+
// Planning is heavy so it uses the best configured model (e.g. Opus) and is
|
|
46
|
+
// not downgraded by dynamic routing when a capable model is configured.
|
|
47
|
+
"plan-milestone": "heavy",
|
|
48
|
+
"plan-slice": "heavy",
|
|
47
49
|
"execute-task": "standard", // default standard, upgraded by metadata
|
|
48
50
|
"replan-slice": "heavy",
|
|
49
51
|
"reassess-roadmap": "heavy",
|
|
@@ -185,8 +187,8 @@ function analyzePlanComplexity(
|
|
|
185
187
|
// Check if this is a milestone-level plan (more complex) vs single slice
|
|
186
188
|
const { milestone: mid, slice: sid } = parseUnitId(unitId);
|
|
187
189
|
if (!sid) {
|
|
188
|
-
// Milestone-level planning is always
|
|
189
|
-
return { tier: "
|
|
190
|
+
// Milestone-level planning is always heavy — requires full context and best model
|
|
191
|
+
return { tier: "heavy", reason: "milestone-level planning" };
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
// For slice planning, try to read the context/research to gauge complexity
|
|
@@ -10,7 +10,7 @@ import { deriveState, isMilestoneComplete } from "./state.js";
|
|
|
10
10
|
import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
|
|
11
11
|
import { abortAndReset } from "./git-self-heal.js";
|
|
12
12
|
import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
|
|
13
|
-
import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached } from "./native-git-bridge.js";
|
|
13
|
+
import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
|
|
14
14
|
import { getAllWorktreeHealth } from "./worktree-health.js";
|
|
15
15
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
16
16
|
|
|
@@ -363,6 +363,54 @@ export async function checkGitHealth(
|
|
|
363
363
|
// Non-fatal — orphaned worktree directory check failed
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
// ── Stale uncommitted changes ────────────────────────────────────────────
|
|
367
|
+
// If the working tree has uncommitted changes and the last commit was
|
|
368
|
+
// longer ago than the configured threshold, flag it and optionally
|
|
369
|
+
// auto-commit a safety snapshot so work isn't lost.
|
|
370
|
+
try {
|
|
371
|
+
const prefs = loadEffectiveGSDPreferences()?.preferences ?? {};
|
|
372
|
+
const thresholdMinutes = prefs.stale_commit_threshold_minutes ?? 30;
|
|
373
|
+
|
|
374
|
+
if (thresholdMinutes > 0) {
|
|
375
|
+
const dirty = nativeHasChanges(basePath);
|
|
376
|
+
if (dirty) {
|
|
377
|
+
const branch = nativeGetCurrentBranch(basePath);
|
|
378
|
+
const lastEpoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
|
|
379
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
380
|
+
const minutesSinceCommit = lastEpoch > 0 ? (nowEpoch - lastEpoch) / 60 : Infinity;
|
|
381
|
+
|
|
382
|
+
if (minutesSinceCommit >= thresholdMinutes) {
|
|
383
|
+
const mins = Math.floor(minutesSinceCommit);
|
|
384
|
+
issues.push({
|
|
385
|
+
severity: "warning",
|
|
386
|
+
code: "stale_uncommitted_changes",
|
|
387
|
+
scope: "project",
|
|
388
|
+
unitId: "project",
|
|
389
|
+
message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting tracked files.`,
|
|
390
|
+
fixable: true,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
if (shouldFix("stale_uncommitted_changes")) {
|
|
394
|
+
try {
|
|
395
|
+
nativeAddTracked(basePath);
|
|
396
|
+
const commitMsg = `gsd snapshot: uncommitted changes after ${mins}m inactivity`;
|
|
397
|
+
const result = nativeCommit(basePath, commitMsg);
|
|
398
|
+
if (result) {
|
|
399
|
+
fixesApplied.push(`created gsd snapshot after ${mins}m of uncommitted changes`);
|
|
400
|
+
} else {
|
|
401
|
+
fixesApplied.push("gsd snapshot skipped — nothing to commit after staging tracked files");
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
fixesApplied.push("failed to create gsd snapshot commit");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
// Non-fatal — stale commit check failed
|
|
412
|
+
}
|
|
413
|
+
|
|
366
414
|
// ── Worktree lifecycle checks ──────────────────────────────────────────
|
|
367
415
|
// Check GSD-managed worktrees for: merged branches, stale work, dirty
|
|
368
416
|
// state, and unpushed commits. Only worktrees under .gsd/worktrees/.
|
|
@@ -22,7 +22,7 @@ import { abortAndReset } from "./git-self-heal.js";
|
|
|
22
22
|
import { rebuildState } from "./doctor.js";
|
|
23
23
|
import { deriveState } from "./state.js";
|
|
24
24
|
import { resolveMilestoneIntegrationBranch } from "./git-service.js";
|
|
25
|
-
import { nativeIsRepo } from "./native-git-bridge.js";
|
|
25
|
+
import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
|
|
26
26
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
27
27
|
import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
28
28
|
|
|
@@ -295,6 +295,40 @@ export async function preDispatchHealthGate(basePath: string): Promise<PreDispat
|
|
|
295
295
|
// Non-fatal — dispatch continues if state/branch check fails
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
// ── Stale uncommitted changes — auto-snapshot before dispatch ──
|
|
299
|
+
// If the working tree is dirty and no commit has happened recently,
|
|
300
|
+
// create a safety snapshot so work isn't lost if the next unit crashes.
|
|
301
|
+
try {
|
|
302
|
+
if (nativeIsRepo(basePath)) {
|
|
303
|
+
const prefs = loadEffectiveGSDPreferences()?.preferences ?? {};
|
|
304
|
+
const thresholdMinutes = prefs.stale_commit_threshold_minutes ?? 30;
|
|
305
|
+
|
|
306
|
+
if (thresholdMinutes > 0 && nativeHasChanges(basePath)) {
|
|
307
|
+
const branch = nativeGetCurrentBranch(basePath);
|
|
308
|
+
const lastEpoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
|
|
309
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
310
|
+
const minutesSinceCommit = lastEpoch > 0 ? (nowEpoch - lastEpoch) / 60 : Infinity;
|
|
311
|
+
|
|
312
|
+
if (minutesSinceCommit >= thresholdMinutes) {
|
|
313
|
+
const mins = Math.floor(minutesSinceCommit);
|
|
314
|
+
try {
|
|
315
|
+
nativeAddTracked(basePath);
|
|
316
|
+
const commitMsg = `gsd snapshot: pre-dispatch, uncommitted changes after ${mins}m inactivity`;
|
|
317
|
+
const result = nativeCommit(basePath, commitMsg);
|
|
318
|
+
if (result) {
|
|
319
|
+
fixesApplied.push(`pre-dispatch: created gsd snapshot after ${mins}m of uncommitted changes`);
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
// Non-blocking — snapshot failed but dispatch can continue
|
|
323
|
+
fixesApplied.push("pre-dispatch: gsd snapshot failed");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
// Non-fatal
|
|
330
|
+
}
|
|
331
|
+
|
|
298
332
|
// ── Disk space check ──
|
|
299
333
|
// Catches low-disk conditions before dispatch rather than letting the unit
|
|
300
334
|
// fail mid-execution with ENOSPC (which wastes a full LLM turn).
|
|
@@ -48,7 +48,9 @@ const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fet
|
|
|
48
48
|
const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i;
|
|
49
49
|
// ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
|
|
50
50
|
const CONNECTION_RE = /terminated|connection.?refused|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
|
|
51
|
-
|
|
51
|
+
// Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
|
|
52
|
+
// This eliminates the need to enumerate every error message variant individually.
|
|
53
|
+
const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
|
|
52
54
|
const RESET_DELAY_RE = /reset in (\d+)s/i;
|
|
53
55
|
|
|
54
56
|
/**
|
|
@@ -91,9 +93,6 @@ export function classifyError(errorMsg: string, retryAfterMs?: number): ErrorCla
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
// 4. Stream truncation — downstream symptom of connection drop
|
|
94
|
-
// Checked before server/connection because JSON parse errors can contain
|
|
95
|
-
// substrings like "position 500" (matches SERVER_RE) or "Unterminated"
|
|
96
|
-
// (matches CONNECTION_RE's "terminated" pattern).
|
|
97
96
|
if (STREAM_RE.test(errorMsg)) {
|
|
98
97
|
return { kind: "stream", retryAfterMs: retryAfterMs ?? 15_000 };
|
|
99
98
|
}
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
nativeRmCached,
|
|
33
33
|
nativeUpdateRef,
|
|
34
34
|
nativeAddPaths,
|
|
35
|
+
nativeResetSoft,
|
|
36
|
+
nativeCommitSubject,
|
|
35
37
|
} from "./native-git-bridge.js";
|
|
36
38
|
import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js";
|
|
37
39
|
import { getErrorMessage } from "./error-utils.js";
|
|
@@ -77,6 +79,11 @@ export interface GitPreferences {
|
|
|
77
79
|
* Default: the main branch (from `main_branch` or auto-detected).
|
|
78
80
|
*/
|
|
79
81
|
pr_target_branch?: string;
|
|
82
|
+
/** Whether to squash `gsd snapshot:` commits into the next real autoCommit.
|
|
83
|
+
* Enabled by default. Set to false to keep snapshot commits in history
|
|
84
|
+
* for forensic inspection.
|
|
85
|
+
*/
|
|
86
|
+
absorb_snapshot_commits?: boolean;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
|
|
@@ -563,9 +570,95 @@ export class GitServiceImpl {
|
|
|
563
570
|
? buildTaskCommitMessage(taskContext)
|
|
564
571
|
: `chore: auto-commit after ${unitType}\n\nGSD-Unit: ${unitId}`;
|
|
565
572
|
nativeCommit(this.basePath, message, { allowEmpty: false });
|
|
573
|
+
|
|
574
|
+
// Absorb any preceding gsd snapshot commits into this real commit.
|
|
575
|
+
// Walk backwards from HEAD~1 counting consecutive snapshot subjects,
|
|
576
|
+
// then soft-reset to before them and re-commit with the same message.
|
|
577
|
+
this.absorbSnapshotCommits(message);
|
|
578
|
+
|
|
566
579
|
return message;
|
|
567
580
|
}
|
|
568
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Squash consecutive `gsd snapshot:` commits that sit immediately below
|
|
584
|
+
* HEAD into the current HEAD commit. This keeps the git history clean
|
|
585
|
+
* after automated snapshot commits are superseded by real work.
|
|
586
|
+
*
|
|
587
|
+
* Guards:
|
|
588
|
+
* - Opt-in via `absorb_snapshot_commits` preference (default: true).
|
|
589
|
+
* - Refuses to rewrite commits that have been pushed to the remote
|
|
590
|
+
* tracking branch (checks merge-base ancestry).
|
|
591
|
+
* - Saves HEAD SHA before reset; restores it if the re-commit fails.
|
|
592
|
+
*
|
|
593
|
+
* Does nothing if there are no snapshot commits to absorb.
|
|
594
|
+
*/
|
|
595
|
+
private absorbSnapshotCommits(headMessage: string): void {
|
|
596
|
+
try {
|
|
597
|
+
// Opt-in guard — users can disable to keep snapshot commits for forensics
|
|
598
|
+
if (this.prefs.absorb_snapshot_commits === false) return;
|
|
599
|
+
|
|
600
|
+
const GSD_SNAPSHOT_PREFIX = "gsd snapshot:";
|
|
601
|
+
let count = 0;
|
|
602
|
+
|
|
603
|
+
// Walk back from HEAD~1 counting consecutive snapshot commits (cap at 10)
|
|
604
|
+
for (let i = 1; i <= 10; i++) {
|
|
605
|
+
const subject = nativeCommitSubject(this.basePath, `HEAD~${i}`);
|
|
606
|
+
if (!subject.startsWith(GSD_SNAPSHOT_PREFIX)) break;
|
|
607
|
+
count = i;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (count === 0) return;
|
|
611
|
+
|
|
612
|
+
// Guard: don't rewrite history that has been pushed to the remote.
|
|
613
|
+
// Check whether the newest snapshot commit (HEAD~1) is already
|
|
614
|
+
// reachable from the remote tracking branch. If it is, the snapshots
|
|
615
|
+
// have been pushed and must not be squashed via local history rewrite.
|
|
616
|
+
// (Checking resetTarget instead would false-positive when the remote
|
|
617
|
+
// is at the pre-snapshot base but the snapshots themselves are local.)
|
|
618
|
+
const resetTarget = `HEAD~${count + 1}`;
|
|
619
|
+
try {
|
|
620
|
+
const branch = nativeGetCurrentBranch(this.basePath);
|
|
621
|
+
if (branch) {
|
|
622
|
+
const remoteBranch = `origin/${branch}`;
|
|
623
|
+
// merge-base --is-ancestor exits 0 if HEAD~1 is ancestor of remote
|
|
624
|
+
execFileSync("git", ["merge-base", "--is-ancestor", "HEAD~1", remoteBranch], {
|
|
625
|
+
cwd: this.basePath,
|
|
626
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
627
|
+
});
|
|
628
|
+
// If we get here, newest snapshot IS reachable from remote — already pushed
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
} catch {
|
|
632
|
+
// Not an ancestor or remote doesn't exist — safe to proceed
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Save HEAD SHA so we can restore if the re-commit fails
|
|
636
|
+
const savedHead = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
637
|
+
cwd: this.basePath,
|
|
638
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
639
|
+
encoding: "utf-8",
|
|
640
|
+
}).trim();
|
|
641
|
+
|
|
642
|
+
nativeResetSoft(this.basePath, resetTarget);
|
|
643
|
+
|
|
644
|
+
// Re-run smartStage so the same RUNTIME_EXCLUSION_PATHS apply.
|
|
645
|
+
// Snapshot commits used nativeAddTracked (git add -u) which stages
|
|
646
|
+
// ALL tracked modifications including .gsd/ state files. Without
|
|
647
|
+
// re-staging, those .gsd/ changes leak into the absorbed commit.
|
|
648
|
+
this.smartStage();
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
nativeCommit(this.basePath, headMessage, { allowEmpty: false });
|
|
652
|
+
} catch {
|
|
653
|
+
// Re-commit failed — restore original HEAD to avoid leaving the
|
|
654
|
+
// repo in a partially-reset state with no commit
|
|
655
|
+
nativeResetSoft(this.basePath, savedHead);
|
|
656
|
+
}
|
|
657
|
+
} catch {
|
|
658
|
+
// Non-fatal — if squash fails, the commits remain unsquashed
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
569
662
|
// ─── Branch Queries ────────────────────────────────────────────────────
|
|
570
663
|
|
|
571
664
|
/**
|
|
@@ -680,6 +680,16 @@ export function nativeAddAll(basePath: string): void {
|
|
|
680
680
|
gitFileExec(basePath, ["add", "-A"]);
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Stage only already-tracked files (git add -u).
|
|
685
|
+
* Does NOT add new untracked files — only updates modifications and deletions
|
|
686
|
+
* for files git already knows about. Safe for automated snapshots where
|
|
687
|
+
* pulling in unknown untracked files (secrets, binaries) would be dangerous.
|
|
688
|
+
*/
|
|
689
|
+
export function nativeAddTracked(basePath: string): void {
|
|
690
|
+
gitFileExec(basePath, ["add", "-u"]);
|
|
691
|
+
}
|
|
692
|
+
|
|
683
693
|
/**
|
|
684
694
|
* Stage all files with pathspec exclusions (git add -A -- ':!pattern' ...).
|
|
685
695
|
* Excluded paths are never hashed by git, preventing hangs on large
|
|
@@ -931,6 +941,20 @@ export function nativeResetHard(basePath: string): void {
|
|
|
931
941
|
execSync("git reset --hard HEAD", { cwd: basePath, stdio: "pipe" });
|
|
932
942
|
}
|
|
933
943
|
|
|
944
|
+
/**
|
|
945
|
+
* Soft reset to a target ref (git reset --soft <ref>).
|
|
946
|
+
* Moves HEAD to `target` while keeping all changes staged in the index.
|
|
947
|
+
* Used to squash snapshot commits back into a single real commit.
|
|
948
|
+
*/
|
|
949
|
+
export function nativeResetSoft(basePath: string, target: string): void {
|
|
950
|
+
execFileSync("git", ["reset", "--soft", target], {
|
|
951
|
+
cwd: basePath,
|
|
952
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
953
|
+
encoding: "utf-8",
|
|
954
|
+
env: GIT_NO_PROMPT_ENV,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
934
958
|
/**
|
|
935
959
|
* Get the subject line of a commit (git log -1 --format=%s <ref>).
|
|
936
960
|
* Returns empty string if the ref doesn't exist.
|
|
@@ -264,6 +264,7 @@ export const GSD_ROOT_FILES = {
|
|
|
264
264
|
REQUIREMENTS: "REQUIREMENTS.md",
|
|
265
265
|
OVERRIDES: "OVERRIDES.md",
|
|
266
266
|
KNOWLEDGE: "KNOWLEDGE.md",
|
|
267
|
+
CODEBASE: "CODEBASE.md",
|
|
267
268
|
} as const;
|
|
268
269
|
|
|
269
270
|
export type GSDRootFileKey = keyof typeof GSD_ROOT_FILES;
|
|
@@ -276,6 +277,7 @@ const LEGACY_GSD_ROOT_FILES: Record<GSDRootFileKey, string> = {
|
|
|
276
277
|
REQUIREMENTS: "requirements.md",
|
|
277
278
|
OVERRIDES: "overrides.md",
|
|
278
279
|
KNOWLEDGE: "knowledge.md",
|
|
280
|
+
CODEBASE: "codebase.md",
|
|
279
281
|
};
|
|
280
282
|
|
|
281
283
|
// ─── GSD Root Discovery ───────────────────────────────────────────────────────
|
|
@@ -93,6 +93,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
93
93
|
"service_tier",
|
|
94
94
|
"forensics_dedup",
|
|
95
95
|
"show_token_cost",
|
|
96
|
+
"stale_commit_threshold_minutes",
|
|
96
97
|
"experimental",
|
|
97
98
|
]);
|
|
98
99
|
|
|
@@ -253,6 +254,13 @@ export interface GSDPreferences {
|
|
|
253
254
|
forensics_dedup?: boolean;
|
|
254
255
|
/** Opt-in: show per-prompt and cumulative session token cost in the footer. Default: false. */
|
|
255
256
|
show_token_cost?: boolean;
|
|
257
|
+
/**
|
|
258
|
+
* Minutes without a commit before flagging uncommitted changes as stale.
|
|
259
|
+
* When the threshold is exceeded and the working tree is dirty, doctor will
|
|
260
|
+
* auto-commit a safety snapshot tagged with `[gsd safety]`. Default: 30.
|
|
261
|
+
* Set to 0 to disable.
|
|
262
|
+
*/
|
|
263
|
+
stale_commit_threshold_minutes?: number;
|
|
256
264
|
/**
|
|
257
265
|
* Opt-in experimental features. All features here are disabled by default.
|
|
258
266
|
* See the preferences reference for details on each feature.
|