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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* async-bash-timeout.test.ts — Tests for async_bash timeout behavior.
|
|
3
|
+
*
|
|
4
|
+
* Reproduces issue #2186: when an async bash job exceeds its timeout and
|
|
5
|
+
* the child process ignores SIGTERM, the promise hangs indefinitely.
|
|
6
|
+
* The fix adds a SIGKILL fallback and a hard deadline that force-resolves
|
|
7
|
+
* the promise so execution can continue.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { createAsyncBashTool } from "./async-bash-tool.ts";
|
|
13
|
+
import { AsyncJobManager } from "./job-manager.ts";
|
|
14
|
+
|
|
15
|
+
function getTextFromResult(result: { content: Array<{ type: string; text?: string }> }): string {
|
|
16
|
+
return result.content.map((c) => c.text ?? "").join("\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const noopSignal = new AbortController().signal;
|
|
20
|
+
|
|
21
|
+
test("async_bash with timeout resolves even if process ignores SIGTERM", async () => {
|
|
22
|
+
const manager = new AsyncJobManager();
|
|
23
|
+
const tool = createAsyncBashTool(() => manager, () => process.cwd());
|
|
24
|
+
|
|
25
|
+
// Start a job that traps SIGTERM (ignores it), with a 2s timeout.
|
|
26
|
+
// The process installs a SIGTERM trap and sleeps for 60s.
|
|
27
|
+
// Before the fix, this would hang forever because SIGTERM is ignored
|
|
28
|
+
// and the close event never fires.
|
|
29
|
+
const result = await tool.execute(
|
|
30
|
+
"tc-timeout",
|
|
31
|
+
{
|
|
32
|
+
command: "trap '' TERM; sleep 60",
|
|
33
|
+
timeout: 2,
|
|
34
|
+
label: "sigterm-resistant",
|
|
35
|
+
},
|
|
36
|
+
noopSignal,
|
|
37
|
+
() => {},
|
|
38
|
+
undefined as never,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const text = getTextFromResult(result);
|
|
42
|
+
assert.match(text, /sigterm-resistant/);
|
|
43
|
+
|
|
44
|
+
const jobId = text.match(/\*\*(bg_[a-f0-9]+)\*\*/)?.[1];
|
|
45
|
+
assert.ok(jobId, "Should have returned a job ID");
|
|
46
|
+
|
|
47
|
+
// Now await the job — it should resolve within a reasonable time
|
|
48
|
+
// (timeout 2s + SIGKILL grace 5s + buffer = well under 15s)
|
|
49
|
+
const start = Date.now();
|
|
50
|
+
const job = manager.getJob(jobId)!;
|
|
51
|
+
assert.ok(job, "Job should exist");
|
|
52
|
+
|
|
53
|
+
await Promise.race([
|
|
54
|
+
job.promise,
|
|
55
|
+
new Promise<never>((_, reject) => {
|
|
56
|
+
const t = setTimeout(() => reject(new Error(
|
|
57
|
+
`Job promise hung for ${Date.now() - start}ms — ` +
|
|
58
|
+
`this is the bug from issue #2186: timeout hangs indefinitely`,
|
|
59
|
+
)), 15_000);
|
|
60
|
+
if (typeof t === "object" && "unref" in t) t.unref();
|
|
61
|
+
}),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const elapsed = Date.now() - start;
|
|
65
|
+
// Should have resolved well within 15s (timeout 2s + kill grace ~5s)
|
|
66
|
+
assert.ok(elapsed < 15_000, `Job took ${elapsed}ms — expected <15s`);
|
|
67
|
+
|
|
68
|
+
// Job should have completed (resolved, not rejected) with timeout message
|
|
69
|
+
assert.ok(
|
|
70
|
+
job.status === "completed" || job.status === "failed",
|
|
71
|
+
`Job status should be completed or failed, got: ${job.status}`,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (job.status === "completed") {
|
|
75
|
+
assert.ok(
|
|
76
|
+
job.resultText?.includes("timed out") || job.resultText?.includes("Timed out"),
|
|
77
|
+
`Result should mention timeout, got: ${job.resultText}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
manager.shutdown();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("async_bash with timeout resolves normally when process exits on SIGTERM", async () => {
|
|
85
|
+
const manager = new AsyncJobManager();
|
|
86
|
+
const tool = createAsyncBashTool(() => manager, () => process.cwd());
|
|
87
|
+
|
|
88
|
+
// Start a normal sleep that will die on SIGTERM, with a 1s timeout
|
|
89
|
+
const result = await tool.execute(
|
|
90
|
+
"tc-normal-timeout",
|
|
91
|
+
{
|
|
92
|
+
command: "sleep 60",
|
|
93
|
+
timeout: 1,
|
|
94
|
+
label: "normal-timeout",
|
|
95
|
+
},
|
|
96
|
+
noopSignal,
|
|
97
|
+
() => {},
|
|
98
|
+
undefined as never,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const text = getTextFromResult(result);
|
|
102
|
+
const jobId = text.match(/\*\*(bg_[a-f0-9]+)\*\*/)?.[1];
|
|
103
|
+
assert.ok(jobId, "Should have returned a job ID");
|
|
104
|
+
|
|
105
|
+
const job = manager.getJob(jobId)!;
|
|
106
|
+
const start = Date.now();
|
|
107
|
+
|
|
108
|
+
await Promise.race([
|
|
109
|
+
job.promise,
|
|
110
|
+
new Promise<never>((_, reject) => {
|
|
111
|
+
const t = setTimeout(() => reject(new Error("Job hung")), 10_000);
|
|
112
|
+
if (typeof t === "object" && "unref" in t) t.unref();
|
|
113
|
+
}),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const elapsed = Date.now() - start;
|
|
117
|
+
assert.ok(elapsed < 5_000, `Expected quick resolution after SIGTERM, took ${elapsed}ms`);
|
|
118
|
+
assert.equal(job.status, "completed");
|
|
119
|
+
assert.ok(job.resultText?.includes("timed out"), `Should mention timeout: ${job.resultText}`);
|
|
120
|
+
|
|
121
|
+
manager.shutdown();
|
|
122
|
+
});
|
|
@@ -109,6 +109,10 @@ function executeBashInBackground(
|
|
|
109
109
|
timeout?: number,
|
|
110
110
|
): Promise<string> {
|
|
111
111
|
return new Promise<string>((resolve, reject) => {
|
|
112
|
+
let settled = false;
|
|
113
|
+
const safeResolve = (value: string) => { if (!settled) { settled = true; resolve(value); } };
|
|
114
|
+
const safeReject = (err: unknown) => { if (!settled) { settled = true; reject(err); } };
|
|
115
|
+
|
|
112
116
|
const { shell, args } = getShellConfig();
|
|
113
117
|
const resolvedCommand = sanitizeCommand(command);
|
|
114
118
|
|
|
@@ -121,11 +125,39 @@ function executeBashInBackground(
|
|
|
121
125
|
|
|
122
126
|
let timedOut = false;
|
|
123
127
|
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
128
|
+
let sigkillHandle: ReturnType<typeof setTimeout> | undefined;
|
|
129
|
+
let hardDeadlineHandle: ReturnType<typeof setTimeout> | undefined;
|
|
130
|
+
|
|
131
|
+
/** Grace period (ms) between SIGTERM and SIGKILL. */
|
|
132
|
+
const SIGKILL_GRACE_MS = 5_000;
|
|
133
|
+
/** Hard deadline (ms) after SIGKILL to force-resolve the promise. */
|
|
134
|
+
const HARD_DEADLINE_MS = 3_000;
|
|
124
135
|
|
|
125
136
|
if (timeout !== undefined && timeout > 0) {
|
|
126
137
|
timeoutHandle = setTimeout(() => {
|
|
127
138
|
timedOut = true;
|
|
128
139
|
if (child.pid) killTree(child.pid);
|
|
140
|
+
|
|
141
|
+
// If the process ignores SIGTERM, escalate to SIGKILL
|
|
142
|
+
sigkillHandle = setTimeout(() => {
|
|
143
|
+
if (child.pid) {
|
|
144
|
+
try { process.kill(-child.pid, "SIGKILL"); } catch { /* ignore */ }
|
|
145
|
+
try { process.kill(child.pid, "SIGKILL"); } catch { /* ignore */ }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Hard deadline: if even SIGKILL doesn't trigger 'close',
|
|
149
|
+
// force-resolve so the job doesn't hang forever (#2186).
|
|
150
|
+
hardDeadlineHandle = setTimeout(() => {
|
|
151
|
+
const output = Buffer.concat(chunks).toString("utf-8");
|
|
152
|
+
safeResolve(
|
|
153
|
+
output
|
|
154
|
+
? `${output}\n\nCommand timed out after ${timeout} seconds (force-killed)`
|
|
155
|
+
: `Command timed out after ${timeout} seconds (force-killed)`,
|
|
156
|
+
);
|
|
157
|
+
}, HARD_DEADLINE_MS);
|
|
158
|
+
if (typeof hardDeadlineHandle === "object" && "unref" in hardDeadlineHandle) hardDeadlineHandle.unref();
|
|
159
|
+
}, SIGKILL_GRACE_MS);
|
|
160
|
+
if (typeof sigkillHandle === "object" && "unref" in sigkillHandle) sigkillHandle.unref();
|
|
129
161
|
}, timeout * 1000);
|
|
130
162
|
}
|
|
131
163
|
|
|
@@ -168,24 +200,28 @@ function executeBashInBackground(
|
|
|
168
200
|
|
|
169
201
|
child.on("error", (err) => {
|
|
170
202
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
203
|
+
if (sigkillHandle) clearTimeout(sigkillHandle);
|
|
204
|
+
if (hardDeadlineHandle) clearTimeout(hardDeadlineHandle);
|
|
171
205
|
signal.removeEventListener("abort", onAbort);
|
|
172
|
-
|
|
206
|
+
safeReject(err);
|
|
173
207
|
});
|
|
174
208
|
|
|
175
209
|
child.on("close", (code) => {
|
|
176
210
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
211
|
+
if (sigkillHandle) clearTimeout(sigkillHandle);
|
|
212
|
+
if (hardDeadlineHandle) clearTimeout(hardDeadlineHandle);
|
|
177
213
|
signal.removeEventListener("abort", onAbort);
|
|
178
214
|
if (spillStream) spillStream.end();
|
|
179
215
|
|
|
180
216
|
if (signal.aborted) {
|
|
181
217
|
const output = Buffer.concat(chunks).toString("utf-8");
|
|
182
|
-
|
|
218
|
+
safeResolve(output ? `${output}\n\nCommand aborted` : "Command aborted");
|
|
183
219
|
return;
|
|
184
220
|
}
|
|
185
221
|
|
|
186
222
|
if (timedOut) {
|
|
187
223
|
const output = Buffer.concat(chunks).toString("utf-8");
|
|
188
|
-
|
|
224
|
+
safeResolve(output ? `${output}\n\nCommand timed out after ${timeout} seconds` : `Command timed out after ${timeout} seconds`);
|
|
189
225
|
return;
|
|
190
226
|
}
|
|
191
227
|
|
|
@@ -208,7 +244,7 @@ function executeBashInBackground(
|
|
|
208
244
|
text += `\n\nCommand exited with code ${code}`;
|
|
209
245
|
}
|
|
210
246
|
|
|
211
|
-
|
|
247
|
+
safeResolve(text);
|
|
212
248
|
});
|
|
213
249
|
});
|
|
214
250
|
}
|
|
@@ -986,7 +986,7 @@ export async function buildPlanSlicePrompt(
|
|
|
986
986
|
const prefs = loadEffectiveGSDPreferences();
|
|
987
987
|
const commitDocsEnabled = prefs?.preferences?.git?.commit_docs !== false;
|
|
988
988
|
const commitInstruction = commitDocsEnabled
|
|
989
|
-
? `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.`
|
|
989
|
+
? `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.`
|
|
990
990
|
: "Do not commit — planning docs are not tracked in git for this project.";
|
|
991
991
|
return loadPrompt("plan-slice", {
|
|
992
992
|
workingDirectory: base,
|
|
@@ -20,21 +20,27 @@ import { saveActivityLog } from "../activity-log.js";
|
|
|
20
20
|
// printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
|
|
21
21
|
let isFirstSession = true;
|
|
22
22
|
|
|
23
|
+
async function syncServiceTierStatus(ctx: ExtensionContext): Promise<void> {
|
|
24
|
+
const { getEffectiveServiceTier, formatServiceTierFooterStatus } = await import("../service-tier.js");
|
|
25
|
+
ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus(getEffectiveServiceTier(), ctx.model?.id));
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
export function registerHooks(pi: ExtensionAPI): void {
|
|
24
29
|
pi.on("session_start", async (_event, ctx) => {
|
|
25
30
|
resetWriteGateState();
|
|
26
31
|
resetToolCallLoopGuard();
|
|
32
|
+
await syncServiceTierStatus(ctx);
|
|
27
33
|
if (isFirstSession) {
|
|
28
34
|
isFirstSession = false;
|
|
29
35
|
} else {
|
|
30
36
|
try {
|
|
31
37
|
const gsdBinPath = process.env.GSD_BIN_PATH;
|
|
32
38
|
if (gsdBinPath) {
|
|
33
|
-
const { dirname } = await import(
|
|
39
|
+
const { dirname } = await import("node:path");
|
|
34
40
|
const { printWelcomeScreen } = await import(
|
|
35
|
-
join(dirname(gsdBinPath),
|
|
41
|
+
join(dirname(gsdBinPath), "welcome-screen.js")
|
|
36
42
|
) as { printWelcomeScreen: (opts: { version: string; modelName?: string; provider?: string }) => void };
|
|
37
|
-
printWelcomeScreen({ version: process.env.GSD_VERSION ||
|
|
43
|
+
printWelcomeScreen({ version: process.env.GSD_VERSION || "0.0.0" });
|
|
38
44
|
}
|
|
39
45
|
} catch { /* non-fatal */ }
|
|
40
46
|
}
|
|
@@ -192,8 +198,11 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
192
198
|
markToolEnd(event.toolCallId);
|
|
193
199
|
});
|
|
194
200
|
|
|
201
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
202
|
+
await syncServiceTierStatus(ctx);
|
|
203
|
+
});
|
|
204
|
+
|
|
195
205
|
pi.on("before_provider_request", async (event) => {
|
|
196
|
-
if (!isAutoActive()) return;
|
|
197
206
|
const modelId = event.model?.id;
|
|
198
207
|
if (!modelId) return;
|
|
199
208
|
const { getEffectiveServiceTier, supportsServiceTier } = await import("../service-tier.js");
|
|
@@ -205,4 +214,3 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
205
214
|
return payload;
|
|
206
215
|
});
|
|
207
216
|
}
|
|
208
|
-
|
|
@@ -87,6 +87,18 @@ export const PROJECT_FILES = [
|
|
|
87
87
|
"mix.exs",
|
|
88
88
|
"deno.json",
|
|
89
89
|
"deno.jsonc",
|
|
90
|
+
// .NET
|
|
91
|
+
".sln",
|
|
92
|
+
".csproj",
|
|
93
|
+
"Directory.Build.props",
|
|
94
|
+
// Git submodules
|
|
95
|
+
".gitmodules",
|
|
96
|
+
// Xcode
|
|
97
|
+
"project.yml",
|
|
98
|
+
".xcodeproj",
|
|
99
|
+
".xcworkspace",
|
|
100
|
+
// Docker
|
|
101
|
+
"Dockerfile",
|
|
90
102
|
] as const;
|
|
91
103
|
|
|
92
104
|
const LANGUAGE_MAP: Record<string, string> = {
|
|
@@ -106,6 +118,13 @@ const LANGUAGE_MAP: Record<string, string> = {
|
|
|
106
118
|
"mix.exs": "elixir",
|
|
107
119
|
"deno.json": "typescript/deno",
|
|
108
120
|
"deno.jsonc": "typescript/deno",
|
|
121
|
+
".sln": "dotnet",
|
|
122
|
+
".csproj": "dotnet",
|
|
123
|
+
"Directory.Build.props": "dotnet",
|
|
124
|
+
"project.yml": "swift/xcode",
|
|
125
|
+
".xcodeproj": "swift/xcode",
|
|
126
|
+
".xcworkspace": "swift/xcode",
|
|
127
|
+
"Dockerfile": "docker",
|
|
109
128
|
};
|
|
110
129
|
|
|
111
130
|
const MONOREPO_MARKERS = [
|
|
@@ -2,7 +2,7 @@ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, rmSync,
|
|
|
2
2
|
import { basename, dirname, join, sep } from "node:path";
|
|
3
3
|
|
|
4
4
|
import type { DoctorIssue, DoctorIssueCode } from "./doctor-types.js";
|
|
5
|
-
import { readRepoMeta, externalProjectsRoot } from "./repo-identity.js";
|
|
5
|
+
import { readRepoMeta, externalProjectsRoot, cleanNumberedGsdVariants } from "./repo-identity.js";
|
|
6
6
|
import { loadFile, parseRoadmap } from "./files.js";
|
|
7
7
|
import { resolveMilestoneFile, milestonesDir, gsdRoot, resolveGsdRootFile, relGsdRootFile } from "./paths.js";
|
|
8
8
|
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
@@ -776,6 +776,37 @@ export async function checkRuntimeHealth(
|
|
|
776
776
|
// Non-fatal — external state check failed
|
|
777
777
|
}
|
|
778
778
|
|
|
779
|
+
// ── Numbered .gsd collision variants (#2205) ───────────────────────────
|
|
780
|
+
// macOS APFS can create ".gsd 2", ".gsd 3" etc. when a directory blocks
|
|
781
|
+
// symlink creation. These must be removed so the canonical .gsd is used.
|
|
782
|
+
try {
|
|
783
|
+
const variantPattern = /^\.gsd \d+$/;
|
|
784
|
+
const entries = readdirSync(basePath);
|
|
785
|
+
const variants = entries.filter(e => variantPattern.test(e));
|
|
786
|
+
if (variants.length > 0) {
|
|
787
|
+
for (const v of variants) {
|
|
788
|
+
issues.push({
|
|
789
|
+
severity: "warning",
|
|
790
|
+
code: "numbered_gsd_variant",
|
|
791
|
+
scope: "project",
|
|
792
|
+
unitId: "project",
|
|
793
|
+
message: `Found macOS collision variant "${v}" — this can cause GSD state to appear deleted.`,
|
|
794
|
+
file: v,
|
|
795
|
+
fixable: true,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (shouldFix("numbered_gsd_variant")) {
|
|
800
|
+
const removed = cleanNumberedGsdVariants(basePath);
|
|
801
|
+
for (const name of removed) {
|
|
802
|
+
fixesApplied.push(`removed numbered .gsd variant: ${name}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
} catch {
|
|
807
|
+
// Non-fatal — variant check failed
|
|
808
|
+
}
|
|
809
|
+
|
|
779
810
|
// ── Metrics ledger integrity ───────────────────────────────────────────
|
|
780
811
|
try {
|
|
781
812
|
const metricsPath = join(root, "metrics.json");
|
|
@@ -305,11 +305,24 @@ function checkOptionalProviders(): ProviderCheckResult[] {
|
|
|
305
305
|
const optional = ["brave", "tavily", "jina", "context7"] as const;
|
|
306
306
|
const results: ProviderCheckResult[] = [];
|
|
307
307
|
|
|
308
|
+
// Determine which search providers are configured so we can suppress
|
|
309
|
+
// "not configured" noise for alternative search providers when at least
|
|
310
|
+
// one is already active (e.g. don't warn about missing BRAVE_API_KEY
|
|
311
|
+
// when Tavily is configured).
|
|
312
|
+
const searchProviderIds = ["brave", "tavily"] as const;
|
|
313
|
+
const hasAnySearchProvider = searchProviderIds.some(id => resolveKey(id).found);
|
|
314
|
+
|
|
308
315
|
for (const providerId of optional) {
|
|
309
316
|
const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
|
|
310
317
|
if (!info) continue;
|
|
311
318
|
|
|
312
319
|
const lookup = resolveKey(providerId);
|
|
320
|
+
|
|
321
|
+
// Skip unconfigured search providers when another search provider is active
|
|
322
|
+
if (!lookup.found && hasAnySearchProvider && info.category === "search") {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
313
326
|
results.push({
|
|
314
327
|
name: providerId,
|
|
315
328
|
label: info.label,
|
|
@@ -30,6 +30,9 @@ import { loadPrompt } from "./prompt-loader.js";
|
|
|
30
30
|
import { gsdRoot } from "./paths.js";
|
|
31
31
|
import { formatDuration } from "../shared/format-utils.js";
|
|
32
32
|
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
33
|
+
import { loadEffectiveGSDPreferences, loadGlobalGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
|
|
34
|
+
import { showNextAction } from "../shared/tui.js";
|
|
35
|
+
import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
|
|
33
36
|
|
|
34
37
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
35
38
|
|
|
@@ -67,6 +70,71 @@ interface ForensicReport {
|
|
|
67
70
|
recentUnits: { type: string; id: string; cost: number; duration: number; model: string; finishedAt: number }[];
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
// ─── Duplicate Detection ──────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
const DEDUP_PROMPT_SECTION = `
|
|
76
|
+
## Duplicate Detection (REQUIRED before issue creation)
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
|
|
80
|
+
### Search Steps
|
|
81
|
+
|
|
82
|
+
1. **Search closed issues** for similar keywords from your diagnosis:
|
|
83
|
+
\`\`\`
|
|
84
|
+
gh issue list --repo gsd-build/gsd-2 --state closed --search "<keywords from root cause>" --limit 20
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
2. **Search open PRs** that might contain the fix:
|
|
88
|
+
\`\`\`
|
|
89
|
+
gh pr list --repo gsd-build/gsd-2 --state open --search "<keywords>" --limit 10
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
3. **Search merged PRs** that may have already fixed this:
|
|
93
|
+
\`\`\`
|
|
94
|
+
gh pr list --repo gsd-build/gsd-2 --state merged --search "<keywords>" --limit 10
|
|
95
|
+
\`\`\`
|
|
96
|
+
|
|
97
|
+
### Analysis
|
|
98
|
+
|
|
99
|
+
For each result, compare it against your root-cause diagnosis:
|
|
100
|
+
- Does the issue describe the same code path or file?
|
|
101
|
+
- Does the PR modify the same file:line you identified?
|
|
102
|
+
- Is the symptom description semantically similar even if keywords differ?
|
|
103
|
+
|
|
104
|
+
### Present Findings
|
|
105
|
+
|
|
106
|
+
If you find potential matches, present them to the user:
|
|
107
|
+
|
|
108
|
+
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.
|
|
109
|
+
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.
|
|
110
|
+
3. **"Create new issue anyway"** — when existing results do not cover this specific failure.
|
|
111
|
+
|
|
112
|
+
Only proceed to issue creation if no matches were found OR the user explicitly chooses "Create new issue anyway".
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
async function writeForensicsDedupPref(ctx: ExtensionCommandContext, enabled: boolean): Promise<void> {
|
|
116
|
+
const prefsPath = getGlobalGSDPreferencesPath();
|
|
117
|
+
await ensurePreferencesFile(prefsPath, ctx, "global");
|
|
118
|
+
const existing = loadGlobalGSDPreferences();
|
|
119
|
+
const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : {};
|
|
120
|
+
prefs.version = prefs.version || 1;
|
|
121
|
+
prefs.forensics_dedup = enabled;
|
|
122
|
+
|
|
123
|
+
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
124
|
+
const raw = existsSync(prefsPath) ? readFileSync(prefsPath, "utf-8") : "";
|
|
125
|
+
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
126
|
+
const start = raw.startsWith("---\n") ? 4 : raw.startsWith("---\r\n") ? 5 : -1;
|
|
127
|
+
if (start !== -1) {
|
|
128
|
+
const closingIdx = raw.indexOf("\n---", start);
|
|
129
|
+
if (closingIdx !== -1) {
|
|
130
|
+
const after = raw.slice(closingIdx + 4);
|
|
131
|
+
if (after.trim()) body = after;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
writeFileSync(prefsPath, `---\n${frontmatter}---${body}`, "utf-8");
|
|
136
|
+
}
|
|
137
|
+
|
|
70
138
|
// ─── Entry Point ──────────────────────────────────────────────────────────────
|
|
71
139
|
|
|
72
140
|
export async function handleForensics(
|
|
@@ -98,6 +166,29 @@ export async function handleForensics(
|
|
|
98
166
|
return;
|
|
99
167
|
}
|
|
100
168
|
|
|
169
|
+
// ─── Duplicate detection opt-in ─────────────────────────────────────────────
|
|
170
|
+
const effectivePrefs = loadEffectiveGSDPreferences()?.preferences;
|
|
171
|
+
let dedupEnabled = effectivePrefs?.forensics_dedup === true;
|
|
172
|
+
|
|
173
|
+
if (effectivePrefs?.forensics_dedup === undefined) {
|
|
174
|
+
const choice = await showNextAction(ctx, {
|
|
175
|
+
title: "Duplicate detection available",
|
|
176
|
+
summary: ["Before filing a GitHub issue, forensics can search existing issues and PRs to avoid duplicates.", "This uses additional AI tokens for analysis."],
|
|
177
|
+
actions: [
|
|
178
|
+
{ id: "enable", label: "Enable duplicate detection", description: "Search issues/PRs before filing (recommended)", recommended: true },
|
|
179
|
+
{ id: "skip", label: "Skip for now", description: "File without checking for duplicates" },
|
|
180
|
+
],
|
|
181
|
+
notYetMessage: "You can enable this later via preferences (forensics_dedup: true).",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (choice === "enable") {
|
|
185
|
+
await writeForensicsDedupPref(ctx, true);
|
|
186
|
+
dedupEnabled = true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const dedupSection = dedupEnabled ? DEDUP_PROMPT_SECTION : "";
|
|
191
|
+
|
|
101
192
|
ctx.ui.notify("Building forensic report...", "info");
|
|
102
193
|
|
|
103
194
|
const report = await buildForensicReport(basePath);
|
|
@@ -117,6 +208,7 @@ export async function handleForensics(
|
|
|
117
208
|
problemDescription,
|
|
118
209
|
forensicData,
|
|
119
210
|
gsdSourceDir,
|
|
211
|
+
dedupSection,
|
|
120
212
|
});
|
|
121
213
|
|
|
122
214
|
ctx.ui.notify(`Forensic report saved: ${relative(basePath, savedPath)}`, "info");
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { execFileSync, execSync } from "node:child_process";
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { join } from "node:path";
|
|
12
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join, relative } from "node:path";
|
|
14
14
|
import { gsdRoot } from "./paths.js";
|
|
15
15
|
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
16
16
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
@@ -486,11 +486,80 @@ export class GitServiceImpl {
|
|
|
486
486
|
// git add -A already skips it and the exclusions are harmless no-ops.
|
|
487
487
|
const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
|
|
488
488
|
nativeAddAllWithExclusions(this.basePath, allExclusions);
|
|
489
|
+
|
|
490
|
+
// Force-add .gsd/milestones/ when .gsd is a symlink (#2104).
|
|
491
|
+
// When .gsd is a symlink (external state projects), ensureGitignore adds
|
|
492
|
+
// `.gsd` to .gitignore. The nativeAddAllWithExclusions call above falls
|
|
493
|
+
// back to plain `git add -A` (symlink pathspec rejection), which respects
|
|
494
|
+
// .gitignore and silently skips new .gsd/milestones/ files.
|
|
495
|
+
//
|
|
496
|
+
// `git add -f` also fails with "beyond a symbolic link", so we use
|
|
497
|
+
// `git hash-object -w` + `git update-index --add --cacheinfo` to bypass
|
|
498
|
+
// the symlink restriction entirely. This stages each milestone artifact
|
|
499
|
+
// individually by hashing the file content and updating the index directly.
|
|
500
|
+
const gsdPath = join(this.basePath, ".gsd");
|
|
501
|
+
const milestonesDir = join(gsdPath, "milestones");
|
|
502
|
+
try {
|
|
503
|
+
if (
|
|
504
|
+
existsSync(gsdPath) &&
|
|
505
|
+
lstatSync(gsdPath).isSymbolicLink() &&
|
|
506
|
+
existsSync(milestonesDir)
|
|
507
|
+
) {
|
|
508
|
+
this._forceAddMilestoneArtifacts(milestonesDir);
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
// Non-fatal: if force-add fails, the commit proceeds without these files.
|
|
512
|
+
// This matches existing behavior where milestone artifacts were silently
|
|
513
|
+
// omitted — but now we at least attempt to include them.
|
|
514
|
+
}
|
|
489
515
|
}
|
|
490
516
|
|
|
491
517
|
/** Tracks whether runtime file cleanup has run this session. */
|
|
492
518
|
private _runtimeFilesCleanedUp = false;
|
|
493
519
|
|
|
520
|
+
/**
|
|
521
|
+
* Recursively collect all files under a directory.
|
|
522
|
+
* Returns paths relative to `basePath` (e.g. ".gsd/milestones/M009/SUMMARY.md").
|
|
523
|
+
*/
|
|
524
|
+
private _collectFiles(dir: string): string[] {
|
|
525
|
+
const files: string[] = [];
|
|
526
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
527
|
+
const full = join(dir, entry.name);
|
|
528
|
+
if (entry.isDirectory()) {
|
|
529
|
+
files.push(...this._collectFiles(full));
|
|
530
|
+
} else if (entry.isFile()) {
|
|
531
|
+
files.push(relative(this.basePath, full));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return files;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Stage milestone artifacts through a symlinked .gsd directory (#2104).
|
|
539
|
+
*
|
|
540
|
+
* `git add` (even with `-f`) refuses to stage files "beyond a symbolic link".
|
|
541
|
+
* This method bypasses that restriction by hashing each file with
|
|
542
|
+
* `git hash-object -w` and inserting the blob into the index with
|
|
543
|
+
* `git update-index --add --cacheinfo 100644 <hash> <path>`.
|
|
544
|
+
*/
|
|
545
|
+
private _forceAddMilestoneArtifacts(milestonesDir: string): void {
|
|
546
|
+
const files = this._collectFiles(milestonesDir);
|
|
547
|
+
for (const filePath of files) {
|
|
548
|
+
const hash = execFileSync("git", ["hash-object", "-w", filePath], {
|
|
549
|
+
cwd: this.basePath,
|
|
550
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
551
|
+
encoding: "utf-8",
|
|
552
|
+
env: GIT_NO_PROMPT_ENV,
|
|
553
|
+
}).trim();
|
|
554
|
+
execFileSync("git", ["update-index", "--add", "--cacheinfo", "100644", hash, filePath], {
|
|
555
|
+
cwd: this.basePath,
|
|
556
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
557
|
+
encoding: "utf-8",
|
|
558
|
+
env: GIT_NO_PROMPT_ENV,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
494
563
|
/**
|
|
495
564
|
* Stage files (smart staging) and commit.
|
|
496
565
|
* Returns the commit message string on success, or null if nothing to commit.
|
|
@@ -847,6 +847,7 @@ export function nativeMergeSquash(basePath: string, branch: string): GitMergeRes
|
|
|
847
847
|
cwd: basePath,
|
|
848
848
|
stdio: ["ignore", "pipe", "pipe"],
|
|
849
849
|
encoding: "utf-8",
|
|
850
|
+
env: GIT_NO_PROMPT_ENV,
|
|
850
851
|
});
|
|
851
852
|
return { success: true, conflicts: [] };
|
|
852
853
|
} catch (err: unknown) {
|
|
@@ -89,6 +89,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
89
89
|
"reactive_execution",
|
|
90
90
|
"github",
|
|
91
91
|
"service_tier",
|
|
92
|
+
"forensics_dedup",
|
|
92
93
|
]);
|
|
93
94
|
|
|
94
95
|
/** Canonical list of all dispatch unit types. */
|
|
@@ -223,6 +224,8 @@ export interface GSDPreferences {
|
|
|
223
224
|
github?: GitHubSyncConfig;
|
|
224
225
|
/** OpenAI service tier preference. "priority" = 2x cost, faster. "flex" = 0.5x cost, slower. Only affects gpt-5.4 models. */
|
|
225
226
|
service_tier?: "priority" | "flex";
|
|
227
|
+
/** Opt-in: search existing issues and PRs before filing from /gsd forensics. Uses additional AI tokens. */
|
|
228
|
+
forensics_dedup?: boolean;
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
export interface LoadedGSDPreferences {
|