gsd-pi 2.42.0-dev.97e9e30 → 2.42.0-dev.eedc83f
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 +23 -0
- package/dist/cli.js +15 -1
- package/dist/resource-loader.js +39 -6
- package/dist/resources/extensions/async-jobs/async-bash-tool.js +52 -4
- package/dist/resources/extensions/gsd/auto-prompts.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +11 -5
- package/dist/resources/extensions/gsd/detection.js +19 -0
- package/dist/resources/extensions/gsd/doctor-checks.js +31 -1
- package/dist/resources/extensions/gsd/doctor-providers.js +10 -0
- package/dist/resources/extensions/gsd/forensics.js +84 -0
- package/dist/resources/extensions/gsd/git-constants.js +1 -0
- package/dist/resources/extensions/gsd/git-service.js +68 -2
- package/dist/resources/extensions/gsd/native-git-bridge.js +1 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences.js +59 -8
- package/dist/resources/extensions/gsd/prompts/forensics.md +12 -5
- package/dist/resources/extensions/gsd/repo-identity.js +46 -5
- package/dist/resources/extensions/gsd/service-tier.js +13 -4
- package/dist/resources/extensions/gsd/session-lock.js +2 -2
- package/dist/resources/extensions/gsd/worktree-resolver.js +2 -2
- package/dist/resources/extensions/mcp-client/index.js +2 -1
- package/dist/resources/extensions/search-the-web/tool-search.js +3 -3
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
- 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/api/git/route.js +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 +12 -12
- package/dist/web/standalone/.next/server/chunks/229.js +2 -2
- 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/web-mode.d.ts +2 -0
- package/dist/web-mode.js +40 -4
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +2 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +6 -0
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent.test.ts +53 -0
- package/packages/pi-agent-core/src/agent.ts +3 -0
- package/packages/pi-agent-core/src/types.ts +6 -0
- package/packages/pi-agent-core/tsconfig.json +1 -1
- package/packages/pi-ai/dist/models.d.ts +5 -3
- package/packages/pi-ai/dist/models.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +801 -1468
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +1135 -1588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js +60 -2
- package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
- package/packages/pi-ai/scripts/generate-models.ts +1543 -0
- package/packages/pi-ai/src/models.generated.ts +1140 -1593
- package/packages/pi-ai/src/models.ts +7 -4
- package/packages/pi-ai/src/utils/oauth/github-copilot.ts +74 -2
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +8 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +29 -2
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +60 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +18 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +23 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +63 -11
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +20 -6
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -5
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.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 +9 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- 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 +30 -10
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +7 -1
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +68 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -2
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +18 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +29 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +3 -0
- package/packages/pi-coding-agent/src/core/package-manager.ts +99 -58
- package/packages/pi-coding-agent/src/core/resource-loader.ts +24 -6
- package/packages/pi-coding-agent/src/core/system-prompt.ts +6 -5
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts +3 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +10 -6
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +31 -11
- package/src/resources/extensions/async-jobs/async-bash-timeout.test.ts +122 -0
- package/src/resources/extensions/async-jobs/async-bash-tool.ts +40 -4
- package/src/resources/extensions/gsd/auto-prompts.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -5
- package/src/resources/extensions/gsd/detection.ts +19 -0
- package/src/resources/extensions/gsd/doctor-checks.ts +32 -1
- package/src/resources/extensions/gsd/doctor-providers.ts +13 -0
- package/src/resources/extensions/gsd/doctor-types.ts +1 -0
- package/src/resources/extensions/gsd/forensics.ts +92 -0
- package/src/resources/extensions/gsd/git-constants.ts +1 -0
- package/src/resources/extensions/gsd/git-service.ts +71 -2
- package/src/resources/extensions/gsd/native-git-bridge.ts +1 -0
- package/src/resources/extensions/gsd/preferences-types.ts +3 -0
- package/src/resources/extensions/gsd/preferences.ts +62 -6
- package/src/resources/extensions/gsd/prompts/forensics.md +12 -5
- package/src/resources/extensions/gsd/repo-identity.ts +48 -5
- package/src/resources/extensions/gsd/service-tier.ts +17 -4
- package/src/resources/extensions/gsd/session-lock.ts +2 -2
- package/src/resources/extensions/gsd/tests/activity-log.test.ts +31 -69
- package/src/resources/extensions/gsd/tests/forensics-dedup.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/git-locale.test.ts +133 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/journal.test.ts +82 -127
- package/src/resources/extensions/gsd/tests/manifest-status.test.ts +73 -82
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +30 -1
- package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +156 -263
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +35 -78
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +81 -74
- package/src/resources/extensions/gsd/worktree-resolver.ts +2 -2
- package/src/resources/extensions/mcp-client/index.ts +5 -1
- package/src/resources/extensions/search-the-web/tool-search.ts +3 -3
- /package/dist/web/standalone/.next/static/{PXrI5DoWsm7rwAVnEU2rD → JUBX5FUR73jiViQU5a-Cx}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{PXrI5DoWsm7rwAVnEU2rD → JUBX5FUR73jiViQU5a-Cx}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -24,6 +24,29 @@ One command. Walk away. Come back to a built project with clean git history.
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
+
## What's New in v2.42.0
|
|
28
|
+
|
|
29
|
+
### New Features
|
|
30
|
+
|
|
31
|
+
- **Declarative workflow engine** — define YAML workflows that execute through auto-loop, enabling repeatable multi-step automations without code. (#2024)
|
|
32
|
+
- **Unified rule registry & event journal** — centralized rule registry, event journal with query tool, and standardized tool naming convention. (#1928)
|
|
33
|
+
- **PR risk checker** — CI classifies changed files by system area and surfaces risk level on pull requests. (#1930)
|
|
34
|
+
- **`/gsd fast`** — toggle service tier for supported models, enabling prioritized API routing for faster responses. (#1862)
|
|
35
|
+
- **Web mode CLI flags** — `--host`, `--port`, and `--allowed-origins` flags give full control over the web server bind address and CORS policy. (#1873)
|
|
36
|
+
- **ADR attribution** — architecture decision records now distinguish human, agent, and collaborative authorship. (#1830)
|
|
37
|
+
|
|
38
|
+
### Key Fixes
|
|
39
|
+
|
|
40
|
+
- **Node v24 web boot** — resolved `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` that prevented `gsd --web` from starting on Node v24. (#1864)
|
|
41
|
+
- **Worktree health check for all ecosystems** — broadened from JS-only to 17+ ecosystems (Rust, Go, Python, Java, etc.). (#1860)
|
|
42
|
+
- **Doctor roadmap atomicity** — roadmap checkbox gating now checks summary on disk, not issue detection, preventing false unchecks. (#1915)
|
|
43
|
+
- **Windows path handling** — 8.3 short path resolution, backslash normalization in bash commands, PowerShell browser launch, and parenthesis escaping. (#1960, #1863, #1870, #1872)
|
|
44
|
+
- **Auth token persistence** — web UI auth token survives page refreshes via sessionStorage. (#1877)
|
|
45
|
+
- **German/non-English locale git errors** — git commands now force `LC_ALL=C` to prevent locale-dependent parse failures.
|
|
46
|
+
- **Orphan web server process** — stale web server processes on port 3000 are now cleaned up automatically.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
27
50
|
## What's New in v2.41.0
|
|
28
51
|
|
|
29
52
|
### New Features
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,14 @@ import { parseCliArgs as parseWebCliArgs, runWebCliBranch, migrateLegacyFlatSess
|
|
|
14
14
|
import { stopWebMode } from './web-mode.js';
|
|
15
15
|
import { getProjectSessionsDir } from './project-sessions.js';
|
|
16
16
|
import { markStartup, printStartupTimings } from './startup-timings.js';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// V8 compile cache — Node 22+ can cache compiled bytecode across runs,
|
|
19
|
+
// eliminating repeated parse/compile overhead for unchanged modules.
|
|
20
|
+
// Must be set early so dynamic imports (extensions, lazy subcommands) benefit.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
if (parseInt(process.versions.node) >= 22) {
|
|
23
|
+
process.env.NODE_COMPILE_CACHE ??= join(agentDir, '.compile-cache');
|
|
24
|
+
}
|
|
17
25
|
function exitIfManagedResourcesAreNewer(currentAgentDir) {
|
|
18
26
|
const currentVersion = process.env.GSD_VERSION || '0.0.0';
|
|
19
27
|
const managedVersion = getNewerManagedResourceVersion(currentAgentDir, currentVersion);
|
|
@@ -461,8 +469,14 @@ const sessionManager = cliFlags._selectedSessionPath
|
|
|
461
469
|
exitIfManagedResourcesAreNewer(agentDir);
|
|
462
470
|
initResources(agentDir);
|
|
463
471
|
markStartup('initResources');
|
|
472
|
+
// Overlap resource loading with session manager setup — both are independent.
|
|
473
|
+
// resourceLoader.reload() is the most expensive step (jiti compilation), so
|
|
474
|
+
// starting it early shaves ~50-200ms off interactive startup.
|
|
464
475
|
const resourceLoader = buildResourceLoader(agentDir);
|
|
465
|
-
|
|
476
|
+
const resourceLoadPromise = resourceLoader.reload();
|
|
477
|
+
// While resources load, let session manager finish any async I/O it needs.
|
|
478
|
+
// Then await the resource promise before creating the agent session.
|
|
479
|
+
await resourceLoadPromise;
|
|
466
480
|
markStartup('resourceLoader.reload');
|
|
467
481
|
const { session, extensionsResult } = await createAgentSession({
|
|
468
482
|
authStorage,
|
package/dist/resource-loader.js
CHANGED
|
@@ -48,14 +48,25 @@ function getBundledGsdVersion() {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
function writeManagedResourceManifest(agentDir) {
|
|
51
|
-
// Record root-level files
|
|
52
|
-
// future upgrades can detect and prune any
|
|
51
|
+
// Record root-level files and subdirectory extension names currently in the
|
|
52
|
+
// bundled extensions source so that future upgrades can detect and prune any
|
|
53
|
+
// that get removed or moved.
|
|
53
54
|
let installedExtensionRootFiles = [];
|
|
55
|
+
let installedExtensionDirs = [];
|
|
54
56
|
try {
|
|
55
57
|
if (existsSync(bundledExtensionsDir)) {
|
|
56
|
-
|
|
58
|
+
const entries = readdirSync(bundledExtensionsDir, { withFileTypes: true });
|
|
59
|
+
installedExtensionRootFiles = entries
|
|
57
60
|
.filter(e => e.isFile())
|
|
58
61
|
.map(e => e.name);
|
|
62
|
+
installedExtensionDirs = entries
|
|
63
|
+
.filter(e => e.isDirectory())
|
|
64
|
+
.filter(e => {
|
|
65
|
+
// Only track directories that are actual extensions (contain index.js or index.ts)
|
|
66
|
+
const dirPath = join(bundledExtensionsDir, e.name);
|
|
67
|
+
return existsSync(join(dirPath, 'index.js')) || existsSync(join(dirPath, 'index.ts'));
|
|
68
|
+
})
|
|
69
|
+
.map(e => e.name);
|
|
59
70
|
}
|
|
60
71
|
}
|
|
61
72
|
catch { /* non-fatal */ }
|
|
@@ -64,6 +75,7 @@ function writeManagedResourceManifest(agentDir) {
|
|
|
64
75
|
syncedAt: Date.now(),
|
|
65
76
|
contentHash: computeResourceFingerprint(),
|
|
66
77
|
installedExtensionRootFiles,
|
|
78
|
+
installedExtensionDirs,
|
|
67
79
|
};
|
|
68
80
|
writeFileSync(getManagedResourceManifestPath(agentDir), JSON.stringify(manifest));
|
|
69
81
|
}
|
|
@@ -284,16 +296,20 @@ function pruneRemovedBundledExtensions(manifest, agentDir) {
|
|
|
284
296
|
return;
|
|
285
297
|
// Current bundled root-level files (what the new version provides)
|
|
286
298
|
const currentSourceFiles = new Set();
|
|
299
|
+
// Current bundled subdirectory extensions
|
|
300
|
+
const currentSourceDirs = new Set();
|
|
287
301
|
try {
|
|
288
302
|
if (existsSync(bundledExtensionsDir)) {
|
|
289
303
|
for (const e of readdirSync(bundledExtensionsDir, { withFileTypes: true })) {
|
|
290
304
|
if (e.isFile())
|
|
291
305
|
currentSourceFiles.add(e.name);
|
|
306
|
+
if (e.isDirectory())
|
|
307
|
+
currentSourceDirs.add(e.name);
|
|
292
308
|
}
|
|
293
309
|
}
|
|
294
310
|
}
|
|
295
311
|
catch { /* non-fatal */ }
|
|
296
|
-
const
|
|
312
|
+
const removeFileIfStale = (fileName) => {
|
|
297
313
|
if (currentSourceFiles.has(fileName))
|
|
298
314
|
return; // still in bundle, not stale
|
|
299
315
|
const stale = join(extensionsDir, fileName);
|
|
@@ -303,17 +319,33 @@ function pruneRemovedBundledExtensions(manifest, agentDir) {
|
|
|
303
319
|
}
|
|
304
320
|
catch { /* non-fatal */ }
|
|
305
321
|
};
|
|
322
|
+
const removeDirIfStale = (dirName) => {
|
|
323
|
+
if (currentSourceDirs.has(dirName))
|
|
324
|
+
return; // still in bundle, not stale
|
|
325
|
+
const stale = join(extensionsDir, dirName);
|
|
326
|
+
try {
|
|
327
|
+
if (existsSync(stale))
|
|
328
|
+
rmSync(stale, { recursive: true, force: true });
|
|
329
|
+
}
|
|
330
|
+
catch { /* non-fatal */ }
|
|
331
|
+
};
|
|
306
332
|
if (manifest?.installedExtensionRootFiles) {
|
|
307
333
|
// Manifest-based: remove previously-installed root files that are no longer bundled
|
|
308
334
|
for (const prevFile of manifest.installedExtensionRootFiles) {
|
|
309
|
-
|
|
335
|
+
removeFileIfStale(prevFile);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (manifest?.installedExtensionDirs) {
|
|
339
|
+
// Manifest-based: remove previously-installed subdirectory extensions that are no longer bundled
|
|
340
|
+
for (const prevDir of manifest.installedExtensionDirs) {
|
|
341
|
+
removeDirIfStale(prevDir);
|
|
310
342
|
}
|
|
311
343
|
}
|
|
312
344
|
// Always remove known stale files regardless of manifest state.
|
|
313
345
|
// These were installed by pre-manifest versions so they may not appear in
|
|
314
346
|
// installedExtensionRootFiles even when a manifest exists.
|
|
315
347
|
// env-utils.js was moved from extensions/ root → gsd/ in v2.39.x (#1634)
|
|
316
|
-
|
|
348
|
+
removeFileIfStale('env-utils.js');
|
|
317
349
|
}
|
|
318
350
|
/**
|
|
319
351
|
* Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
|
|
@@ -416,5 +448,6 @@ export function buildResourceLoader(agentDir) {
|
|
|
416
448
|
return new DefaultResourceLoader({
|
|
417
449
|
agentDir,
|
|
418
450
|
additionalExtensionPaths: piExtensionPaths,
|
|
451
|
+
bundledExtensionNames: bundledKeys,
|
|
419
452
|
});
|
|
420
453
|
}
|
|
@@ -83,6 +83,15 @@ export function createAsyncBashTool(getManager, getCwd) {
|
|
|
83
83
|
*/
|
|
84
84
|
function executeBashInBackground(command, cwd, signal, timeout) {
|
|
85
85
|
return new Promise((resolve, reject) => {
|
|
86
|
+
let settled = false;
|
|
87
|
+
const safeResolve = (value) => { if (!settled) {
|
|
88
|
+
settled = true;
|
|
89
|
+
resolve(value);
|
|
90
|
+
} };
|
|
91
|
+
const safeReject = (err) => { if (!settled) {
|
|
92
|
+
settled = true;
|
|
93
|
+
reject(err);
|
|
94
|
+
} };
|
|
86
95
|
const { shell, args } = getShellConfig();
|
|
87
96
|
const resolvedCommand = sanitizeCommand(command);
|
|
88
97
|
const child = spawn(shell, [...args, resolvedCommand], {
|
|
@@ -93,11 +102,42 @@ function executeBashInBackground(command, cwd, signal, timeout) {
|
|
|
93
102
|
});
|
|
94
103
|
let timedOut = false;
|
|
95
104
|
let timeoutHandle;
|
|
105
|
+
let sigkillHandle;
|
|
106
|
+
let hardDeadlineHandle;
|
|
107
|
+
/** Grace period (ms) between SIGTERM and SIGKILL. */
|
|
108
|
+
const SIGKILL_GRACE_MS = 5_000;
|
|
109
|
+
/** Hard deadline (ms) after SIGKILL to force-resolve the promise. */
|
|
110
|
+
const HARD_DEADLINE_MS = 3_000;
|
|
96
111
|
if (timeout !== undefined && timeout > 0) {
|
|
97
112
|
timeoutHandle = setTimeout(() => {
|
|
98
113
|
timedOut = true;
|
|
99
114
|
if (child.pid)
|
|
100
115
|
killTree(child.pid);
|
|
116
|
+
// If the process ignores SIGTERM, escalate to SIGKILL
|
|
117
|
+
sigkillHandle = setTimeout(() => {
|
|
118
|
+
if (child.pid) {
|
|
119
|
+
try {
|
|
120
|
+
process.kill(-child.pid, "SIGKILL");
|
|
121
|
+
}
|
|
122
|
+
catch { /* ignore */ }
|
|
123
|
+
try {
|
|
124
|
+
process.kill(child.pid, "SIGKILL");
|
|
125
|
+
}
|
|
126
|
+
catch { /* ignore */ }
|
|
127
|
+
}
|
|
128
|
+
// Hard deadline: if even SIGKILL doesn't trigger 'close',
|
|
129
|
+
// force-resolve so the job doesn't hang forever (#2186).
|
|
130
|
+
hardDeadlineHandle = setTimeout(() => {
|
|
131
|
+
const output = Buffer.concat(chunks).toString("utf-8");
|
|
132
|
+
safeResolve(output
|
|
133
|
+
? `${output}\n\nCommand timed out after ${timeout} seconds (force-killed)`
|
|
134
|
+
: `Command timed out after ${timeout} seconds (force-killed)`);
|
|
135
|
+
}, HARD_DEADLINE_MS);
|
|
136
|
+
if (typeof hardDeadlineHandle === "object" && "unref" in hardDeadlineHandle)
|
|
137
|
+
hardDeadlineHandle.unref();
|
|
138
|
+
}, SIGKILL_GRACE_MS);
|
|
139
|
+
if (typeof sigkillHandle === "object" && "unref" in sigkillHandle)
|
|
140
|
+
sigkillHandle.unref();
|
|
101
141
|
}, timeout * 1000);
|
|
102
142
|
}
|
|
103
143
|
const chunks = [];
|
|
@@ -139,23 +179,31 @@ function executeBashInBackground(command, cwd, signal, timeout) {
|
|
|
139
179
|
child.on("error", (err) => {
|
|
140
180
|
if (timeoutHandle)
|
|
141
181
|
clearTimeout(timeoutHandle);
|
|
182
|
+
if (sigkillHandle)
|
|
183
|
+
clearTimeout(sigkillHandle);
|
|
184
|
+
if (hardDeadlineHandle)
|
|
185
|
+
clearTimeout(hardDeadlineHandle);
|
|
142
186
|
signal.removeEventListener("abort", onAbort);
|
|
143
|
-
|
|
187
|
+
safeReject(err);
|
|
144
188
|
});
|
|
145
189
|
child.on("close", (code) => {
|
|
146
190
|
if (timeoutHandle)
|
|
147
191
|
clearTimeout(timeoutHandle);
|
|
192
|
+
if (sigkillHandle)
|
|
193
|
+
clearTimeout(sigkillHandle);
|
|
194
|
+
if (hardDeadlineHandle)
|
|
195
|
+
clearTimeout(hardDeadlineHandle);
|
|
148
196
|
signal.removeEventListener("abort", onAbort);
|
|
149
197
|
if (spillStream)
|
|
150
198
|
spillStream.end();
|
|
151
199
|
if (signal.aborted) {
|
|
152
200
|
const output = Buffer.concat(chunks).toString("utf-8");
|
|
153
|
-
|
|
201
|
+
safeResolve(output ? `${output}\n\nCommand aborted` : "Command aborted");
|
|
154
202
|
return;
|
|
155
203
|
}
|
|
156
204
|
if (timedOut) {
|
|
157
205
|
const output = Buffer.concat(chunks).toString("utf-8");
|
|
158
|
-
|
|
206
|
+
safeResolve(output ? `${output}\n\nCommand timed out after ${timeout} seconds` : `Command timed out after ${timeout} seconds`);
|
|
159
207
|
return;
|
|
160
208
|
}
|
|
161
209
|
const fullOutput = Buffer.concat(chunks).toString("utf-8");
|
|
@@ -176,7 +224,7 @@ function executeBashInBackground(command, cwd, signal, timeout) {
|
|
|
176
224
|
if (code !== 0 && code !== null) {
|
|
177
225
|
text += `\n\nCommand exited with code ${code}`;
|
|
178
226
|
}
|
|
179
|
-
|
|
227
|
+
safeResolve(text);
|
|
180
228
|
});
|
|
181
229
|
});
|
|
182
230
|
}
|
|
@@ -849,7 +849,7 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
|
|
|
849
849
|
const prefs = loadEffectiveGSDPreferences();
|
|
850
850
|
const commitDocsEnabled = prefs?.preferences?.git?.commit_docs !== false;
|
|
851
851
|
const commitInstruction = commitDocsEnabled
|
|
852
|
-
? `Commit the plan files only: \`git add ${relSlicePath(base, mid, sid)}/ .gsd/DECISIONS.md .gitignore && git commit -m "docs(${sid}): add slice plan"\`. Do not stage .gsd/STATE.md or other runtime files — the system manages those.`
|
|
852
|
+
? `Commit the plan files only: \`git add --force ${relSlicePath(base, mid, sid)}/ .gsd/DECISIONS.md .gitignore && git commit -m "docs(${sid}): add slice plan"\`. Do not stage .gsd/STATE.md or other runtime files — the system manages those.`
|
|
853
853
|
: "Do not commit — planning docs are not tracked in git for this project.";
|
|
854
854
|
return loadPrompt("plan-slice", {
|
|
855
855
|
workingDirectory: base,
|
|
@@ -15,10 +15,15 @@ import { saveActivityLog } from "../activity-log.js";
|
|
|
15
15
|
// Skip the welcome screen on the very first session_start — cli.ts already
|
|
16
16
|
// printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
|
|
17
17
|
let isFirstSession = true;
|
|
18
|
+
async function syncServiceTierStatus(ctx) {
|
|
19
|
+
const { getEffectiveServiceTier, formatServiceTierFooterStatus } = await import("../service-tier.js");
|
|
20
|
+
ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus(getEffectiveServiceTier(), ctx.model?.id));
|
|
21
|
+
}
|
|
18
22
|
export function registerHooks(pi) {
|
|
19
23
|
pi.on("session_start", async (_event, ctx) => {
|
|
20
24
|
resetWriteGateState();
|
|
21
25
|
resetToolCallLoopGuard();
|
|
26
|
+
await syncServiceTierStatus(ctx);
|
|
22
27
|
if (isFirstSession) {
|
|
23
28
|
isFirstSession = false;
|
|
24
29
|
}
|
|
@@ -26,9 +31,9 @@ export function registerHooks(pi) {
|
|
|
26
31
|
try {
|
|
27
32
|
const gsdBinPath = process.env.GSD_BIN_PATH;
|
|
28
33
|
if (gsdBinPath) {
|
|
29
|
-
const { dirname } = await import(
|
|
30
|
-
const { printWelcomeScreen } = await import(join(dirname(gsdBinPath),
|
|
31
|
-
printWelcomeScreen({ version: process.env.GSD_VERSION ||
|
|
34
|
+
const { dirname } = await import("node:path");
|
|
35
|
+
const { printWelcomeScreen } = await import(join(dirname(gsdBinPath), "welcome-screen.js"));
|
|
36
|
+
printWelcomeScreen({ version: process.env.GSD_VERSION || "0.0.0" });
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
catch { /* non-fatal */ }
|
|
@@ -179,9 +184,10 @@ export function registerHooks(pi) {
|
|
|
179
184
|
pi.on("tool_execution_end", async (event) => {
|
|
180
185
|
markToolEnd(event.toolCallId);
|
|
181
186
|
});
|
|
187
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
188
|
+
await syncServiceTierStatus(ctx);
|
|
189
|
+
});
|
|
182
190
|
pi.on("before_provider_request", async (event) => {
|
|
183
|
-
if (!isAutoActive())
|
|
184
|
-
return;
|
|
185
191
|
const modelId = event.model?.id;
|
|
186
192
|
if (!modelId)
|
|
187
193
|
return;
|
|
@@ -29,6 +29,18 @@ export const PROJECT_FILES = [
|
|
|
29
29
|
"mix.exs",
|
|
30
30
|
"deno.json",
|
|
31
31
|
"deno.jsonc",
|
|
32
|
+
// .NET
|
|
33
|
+
".sln",
|
|
34
|
+
".csproj",
|
|
35
|
+
"Directory.Build.props",
|
|
36
|
+
// Git submodules
|
|
37
|
+
".gitmodules",
|
|
38
|
+
// Xcode
|
|
39
|
+
"project.yml",
|
|
40
|
+
".xcodeproj",
|
|
41
|
+
".xcworkspace",
|
|
42
|
+
// Docker
|
|
43
|
+
"Dockerfile",
|
|
32
44
|
];
|
|
33
45
|
const LANGUAGE_MAP = {
|
|
34
46
|
"package.json": "javascript/typescript",
|
|
@@ -47,6 +59,13 @@ const LANGUAGE_MAP = {
|
|
|
47
59
|
"mix.exs": "elixir",
|
|
48
60
|
"deno.json": "typescript/deno",
|
|
49
61
|
"deno.jsonc": "typescript/deno",
|
|
62
|
+
".sln": "dotnet",
|
|
63
|
+
".csproj": "dotnet",
|
|
64
|
+
"Directory.Build.props": "dotnet",
|
|
65
|
+
"project.yml": "swift/xcode",
|
|
66
|
+
".xcodeproj": "swift/xcode",
|
|
67
|
+
".xcworkspace": "swift/xcode",
|
|
68
|
+
"Dockerfile": "docker",
|
|
50
69
|
};
|
|
51
70
|
const MONOREPO_MARKERS = [
|
|
52
71
|
"lerna.json",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, sep } from "node:path";
|
|
3
|
-
import { readRepoMeta, externalProjectsRoot } from "./repo-identity.js";
|
|
3
|
+
import { readRepoMeta, externalProjectsRoot, cleanNumberedGsdVariants } from "./repo-identity.js";
|
|
4
4
|
import { loadFile, parseRoadmap } from "./files.js";
|
|
5
5
|
import { resolveMilestoneFile, milestonesDir, gsdRoot, resolveGsdRootFile } from "./paths.js";
|
|
6
6
|
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
@@ -733,6 +733,36 @@ export async function checkRuntimeHealth(basePath, issues, fixesApplied, shouldF
|
|
|
733
733
|
catch {
|
|
734
734
|
// Non-fatal — external state check failed
|
|
735
735
|
}
|
|
736
|
+
// ── Numbered .gsd collision variants (#2205) ───────────────────────────
|
|
737
|
+
// macOS APFS can create ".gsd 2", ".gsd 3" etc. when a directory blocks
|
|
738
|
+
// symlink creation. These must be removed so the canonical .gsd is used.
|
|
739
|
+
try {
|
|
740
|
+
const variantPattern = /^\.gsd \d+$/;
|
|
741
|
+
const entries = readdirSync(basePath);
|
|
742
|
+
const variants = entries.filter(e => variantPattern.test(e));
|
|
743
|
+
if (variants.length > 0) {
|
|
744
|
+
for (const v of variants) {
|
|
745
|
+
issues.push({
|
|
746
|
+
severity: "warning",
|
|
747
|
+
code: "numbered_gsd_variant",
|
|
748
|
+
scope: "project",
|
|
749
|
+
unitId: "project",
|
|
750
|
+
message: `Found macOS collision variant "${v}" — this can cause GSD state to appear deleted.`,
|
|
751
|
+
file: v,
|
|
752
|
+
fixable: true,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
if (shouldFix("numbered_gsd_variant")) {
|
|
756
|
+
const removed = cleanNumberedGsdVariants(basePath);
|
|
757
|
+
for (const name of removed) {
|
|
758
|
+
fixesApplied.push(`removed numbered .gsd variant: ${name}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
// Non-fatal — variant check failed
|
|
765
|
+
}
|
|
736
766
|
// ── Metrics ledger integrity ───────────────────────────────────────────
|
|
737
767
|
try {
|
|
738
768
|
const metricsPath = join(root, "metrics.json");
|
|
@@ -260,11 +260,21 @@ function checkRemoteQuestionsProvider() {
|
|
|
260
260
|
function checkOptionalProviders() {
|
|
261
261
|
const optional = ["brave", "tavily", "jina", "context7"];
|
|
262
262
|
const results = [];
|
|
263
|
+
// Determine which search providers are configured so we can suppress
|
|
264
|
+
// "not configured" noise for alternative search providers when at least
|
|
265
|
+
// one is already active (e.g. don't warn about missing BRAVE_API_KEY
|
|
266
|
+
// when Tavily is configured).
|
|
267
|
+
const searchProviderIds = ["brave", "tavily"];
|
|
268
|
+
const hasAnySearchProvider = searchProviderIds.some(id => resolveKey(id).found);
|
|
263
269
|
for (const providerId of optional) {
|
|
264
270
|
const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
|
|
265
271
|
if (!info)
|
|
266
272
|
continue;
|
|
267
273
|
const lookup = resolveKey(providerId);
|
|
274
|
+
// Skip unconfigured search providers when another search provider is active
|
|
275
|
+
if (!lookup.found && hasAnySearchProvider && info.category === "search") {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
268
278
|
results.push({
|
|
269
279
|
name: providerId,
|
|
270
280
|
label: info.label,
|
|
@@ -24,6 +24,70 @@ import { loadPrompt } from "./prompt-loader.js";
|
|
|
24
24
|
import { gsdRoot } from "./paths.js";
|
|
25
25
|
import { formatDuration } from "../shared/format-utils.js";
|
|
26
26
|
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
27
|
+
import { loadEffectiveGSDPreferences, loadGlobalGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
|
|
28
|
+
import { showNextAction } from "../shared/tui.js";
|
|
29
|
+
import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
|
|
30
|
+
// ─── Duplicate Detection ──────────────────────────────────────────────────────
|
|
31
|
+
const DEDUP_PROMPT_SECTION = `
|
|
32
|
+
## Duplicate Detection (REQUIRED before issue creation)
|
|
33
|
+
|
|
34
|
+
Before offering to create a GitHub issue, you MUST search for existing issues and PRs that may already address this bug. This step uses the user's AI tokens for analysis.
|
|
35
|
+
|
|
36
|
+
### Search Steps
|
|
37
|
+
|
|
38
|
+
1. **Search closed issues** for similar keywords from your diagnosis:
|
|
39
|
+
\`\`\`
|
|
40
|
+
gh issue list --repo gsd-build/gsd-2 --state closed --search "<keywords from root cause>" --limit 20
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
2. **Search open PRs** that might contain the fix:
|
|
44
|
+
\`\`\`
|
|
45
|
+
gh pr list --repo gsd-build/gsd-2 --state open --search "<keywords>" --limit 10
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
3. **Search merged PRs** that may have already fixed this:
|
|
49
|
+
\`\`\`
|
|
50
|
+
gh pr list --repo gsd-build/gsd-2 --state merged --search "<keywords>" --limit 10
|
|
51
|
+
\`\`\`
|
|
52
|
+
|
|
53
|
+
### Analysis
|
|
54
|
+
|
|
55
|
+
For each result, compare it against your root-cause diagnosis:
|
|
56
|
+
- Does the issue describe the same code path or file?
|
|
57
|
+
- Does the PR modify the same file:line you identified?
|
|
58
|
+
- Is the symptom description semantically similar even if keywords differ?
|
|
59
|
+
|
|
60
|
+
### Present Findings
|
|
61
|
+
|
|
62
|
+
If you find potential matches, present them to the user:
|
|
63
|
+
|
|
64
|
+
1. **"Already fixed by PR #X — skip issue creation"** — when a merged PR or closed issue clearly addresses the same root cause. Explain why you believe it matches.
|
|
65
|
+
2. **"Add my findings to existing issue #Y"** — when an open issue exists for the same bug. Use \`gh issue comment #Y --repo gsd-build/gsd-2\` to add forensic evidence.
|
|
66
|
+
3. **"Create new issue anyway"** — when existing results do not cover this specific failure.
|
|
67
|
+
|
|
68
|
+
Only proceed to issue creation if no matches were found OR the user explicitly chooses "Create new issue anyway".
|
|
69
|
+
`;
|
|
70
|
+
async function writeForensicsDedupPref(ctx, enabled) {
|
|
71
|
+
const prefsPath = getGlobalGSDPreferencesPath();
|
|
72
|
+
await ensurePreferencesFile(prefsPath, ctx, "global");
|
|
73
|
+
const existing = loadGlobalGSDPreferences();
|
|
74
|
+
const prefs = existing?.preferences ? { ...existing.preferences } : {};
|
|
75
|
+
prefs.version = prefs.version || 1;
|
|
76
|
+
prefs.forensics_dedup = enabled;
|
|
77
|
+
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
78
|
+
const raw = existsSync(prefsPath) ? readFileSync(prefsPath, "utf-8") : "";
|
|
79
|
+
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
80
|
+
const start = raw.startsWith("---\n") ? 4 : raw.startsWith("---\r\n") ? 5 : -1;
|
|
81
|
+
if (start !== -1) {
|
|
82
|
+
const closingIdx = raw.indexOf("\n---", start);
|
|
83
|
+
if (closingIdx !== -1) {
|
|
84
|
+
const after = raw.slice(closingIdx + 4);
|
|
85
|
+
if (after.trim())
|
|
86
|
+
body = after;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
writeFileSync(prefsPath, `---\n${frontmatter}---${body}`, "utf-8");
|
|
90
|
+
}
|
|
27
91
|
// ─── Entry Point ──────────────────────────────────────────────────────────────
|
|
28
92
|
export async function handleForensics(args, ctx, pi) {
|
|
29
93
|
if (isAutoActive()) {
|
|
@@ -44,6 +108,25 @@ export async function handleForensics(args, ctx, pi) {
|
|
|
44
108
|
ctx.ui.notify("Problem description required for forensic analysis.", "warning");
|
|
45
109
|
return;
|
|
46
110
|
}
|
|
111
|
+
// ─── Duplicate detection opt-in ─────────────────────────────────────────────
|
|
112
|
+
const effectivePrefs = loadEffectiveGSDPreferences()?.preferences;
|
|
113
|
+
let dedupEnabled = effectivePrefs?.forensics_dedup === true;
|
|
114
|
+
if (effectivePrefs?.forensics_dedup === undefined) {
|
|
115
|
+
const choice = await showNextAction(ctx, {
|
|
116
|
+
title: "Duplicate detection available",
|
|
117
|
+
summary: ["Before filing a GitHub issue, forensics can search existing issues and PRs to avoid duplicates.", "This uses additional AI tokens for analysis."],
|
|
118
|
+
actions: [
|
|
119
|
+
{ id: "enable", label: "Enable duplicate detection", description: "Search issues/PRs before filing (recommended)", recommended: true },
|
|
120
|
+
{ id: "skip", label: "Skip for now", description: "File without checking for duplicates" },
|
|
121
|
+
],
|
|
122
|
+
notYetMessage: "You can enable this later via preferences (forensics_dedup: true).",
|
|
123
|
+
});
|
|
124
|
+
if (choice === "enable") {
|
|
125
|
+
await writeForensicsDedupPref(ctx, true);
|
|
126
|
+
dedupEnabled = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const dedupSection = dedupEnabled ? DEDUP_PROMPT_SECTION : "";
|
|
47
130
|
ctx.ui.notify("Building forensic report...", "info");
|
|
48
131
|
const report = await buildForensicReport(basePath);
|
|
49
132
|
const savedPath = saveForensicReport(basePath, report, problemDescription);
|
|
@@ -61,6 +144,7 @@ export async function handleForensics(args, ctx, pi) {
|
|
|
61
144
|
problemDescription,
|
|
62
145
|
forensicData,
|
|
63
146
|
gsdSourceDir,
|
|
147
|
+
dedupSection,
|
|
64
148
|
});
|
|
65
149
|
ctx.ui.notify(`Forensic report saved: ${relative(basePath, savedPath)}`, "info");
|
|
66
150
|
pi.sendMessage({ customType: "gsd-forensics", content, display: false }, { triggerTurn: true });
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* paths, commit type inference, and the runGit shell helper.
|
|
9
9
|
*/
|
|
10
10
|
import { execFileSync, execSync } from "node:child_process";
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
-
import { join } from "node:path";
|
|
11
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join, relative } from "node:path";
|
|
13
13
|
import { gsdRoot } from "./paths.js";
|
|
14
14
|
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
15
15
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
@@ -342,9 +342,75 @@ export class GitServiceImpl {
|
|
|
342
342
|
// git add -A already skips it and the exclusions are harmless no-ops.
|
|
343
343
|
const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
|
|
344
344
|
nativeAddAllWithExclusions(this.basePath, allExclusions);
|
|
345
|
+
// Force-add .gsd/milestones/ when .gsd is a symlink (#2104).
|
|
346
|
+
// When .gsd is a symlink (external state projects), ensureGitignore adds
|
|
347
|
+
// `.gsd` to .gitignore. The nativeAddAllWithExclusions call above falls
|
|
348
|
+
// back to plain `git add -A` (symlink pathspec rejection), which respects
|
|
349
|
+
// .gitignore and silently skips new .gsd/milestones/ files.
|
|
350
|
+
//
|
|
351
|
+
// `git add -f` also fails with "beyond a symbolic link", so we use
|
|
352
|
+
// `git hash-object -w` + `git update-index --add --cacheinfo` to bypass
|
|
353
|
+
// the symlink restriction entirely. This stages each milestone artifact
|
|
354
|
+
// individually by hashing the file content and updating the index directly.
|
|
355
|
+
const gsdPath = join(this.basePath, ".gsd");
|
|
356
|
+
const milestonesDir = join(gsdPath, "milestones");
|
|
357
|
+
try {
|
|
358
|
+
if (existsSync(gsdPath) &&
|
|
359
|
+
lstatSync(gsdPath).isSymbolicLink() &&
|
|
360
|
+
existsSync(milestonesDir)) {
|
|
361
|
+
this._forceAddMilestoneArtifacts(milestonesDir);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Non-fatal: if force-add fails, the commit proceeds without these files.
|
|
366
|
+
// This matches existing behavior where milestone artifacts were silently
|
|
367
|
+
// omitted — but now we at least attempt to include them.
|
|
368
|
+
}
|
|
345
369
|
}
|
|
346
370
|
/** Tracks whether runtime file cleanup has run this session. */
|
|
347
371
|
_runtimeFilesCleanedUp = false;
|
|
372
|
+
/**
|
|
373
|
+
* Recursively collect all files under a directory.
|
|
374
|
+
* Returns paths relative to `basePath` (e.g. ".gsd/milestones/M009/SUMMARY.md").
|
|
375
|
+
*/
|
|
376
|
+
_collectFiles(dir) {
|
|
377
|
+
const files = [];
|
|
378
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
379
|
+
const full = join(dir, entry.name);
|
|
380
|
+
if (entry.isDirectory()) {
|
|
381
|
+
files.push(...this._collectFiles(full));
|
|
382
|
+
}
|
|
383
|
+
else if (entry.isFile()) {
|
|
384
|
+
files.push(relative(this.basePath, full));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return files;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Stage milestone artifacts through a symlinked .gsd directory (#2104).
|
|
391
|
+
*
|
|
392
|
+
* `git add` (even with `-f`) refuses to stage files "beyond a symbolic link".
|
|
393
|
+
* This method bypasses that restriction by hashing each file with
|
|
394
|
+
* `git hash-object -w` and inserting the blob into the index with
|
|
395
|
+
* `git update-index --add --cacheinfo 100644 <hash> <path>`.
|
|
396
|
+
*/
|
|
397
|
+
_forceAddMilestoneArtifacts(milestonesDir) {
|
|
398
|
+
const files = this._collectFiles(milestonesDir);
|
|
399
|
+
for (const filePath of files) {
|
|
400
|
+
const hash = execFileSync("git", ["hash-object", "-w", filePath], {
|
|
401
|
+
cwd: this.basePath,
|
|
402
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
403
|
+
encoding: "utf-8",
|
|
404
|
+
env: GIT_NO_PROMPT_ENV,
|
|
405
|
+
}).trim();
|
|
406
|
+
execFileSync("git", ["update-index", "--add", "--cacheinfo", "100644", hash, filePath], {
|
|
407
|
+
cwd: this.basePath,
|
|
408
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
409
|
+
encoding: "utf-8",
|
|
410
|
+
env: GIT_NO_PROMPT_ENV,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
348
414
|
/**
|
|
349
415
|
* Stage files (smart staging) and commit.
|
|
350
416
|
* Returns the commit message string on success, or null if nothing to commit.
|