gsd-pi 2.57.0 → 2.58.0-dev.d63175c
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/resources/extensions/gsd/auto/infra-errors.js +4 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +3 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +7 -2
- package/dist/resources/extensions/gsd/auto.js +4 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -1
- package/dist/resources/extensions/gsd/dispatch-guard.js +11 -1
- package/dist/resources/extensions/gsd/gsd-db.js +8 -1
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +23 -6
- package/dist/resources/extensions/gsd/preferences.js +29 -15
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +4 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +4 -4
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/required-server-files.json +4 -4
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- 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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- 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 +3 -3
- 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/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- 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 +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
- package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- 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/standalone/.next/static/chunks/6502.8b732f67a11b11b4.js +9 -0
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-0c485498795110d6.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-4332cbd5dd1be584.js → webpack-61d3afac6d0f0ce7.js} +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/daemon/src/cli.ts +49 -0
- package/packages/daemon/src/daemon.test.ts +104 -1
- package/packages/daemon/src/daemon.ts +24 -1
- package/packages/daemon/src/discord-bot.ts +73 -3
- package/packages/daemon/src/event-bridge.ts +15 -9
- package/packages/daemon/src/event-formatter.ts +30 -2
- package/packages/daemon/src/index.ts +9 -0
- package/packages/daemon/src/launchd.test.ts +356 -0
- package/packages/daemon/src/launchd.ts +242 -0
- package/packages/daemon/src/message-batcher.test.ts +2 -2
- package/packages/daemon/src/message-batcher.ts +9 -3
- package/packages/daemon/src/orchestrator.test.ts +1 -0
- package/packages/daemon/src/orchestrator.ts +106 -2
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/infra-errors.ts +3 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +3 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +7 -2
- package/src/resources/extensions/gsd/auto.ts +5 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +2 -1
- package/src/resources/extensions/gsd/dispatch-guard.ts +12 -1
- package/src/resources/extensions/gsd/gsd-db.ts +6 -1
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +23 -6
- package/src/resources/extensions/gsd/preferences.ts +32 -14
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +9 -8
- package/src/resources/extensions/gsd/tests/preferences.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +23 -1
- package/src/resources/extensions/gsd/tests/validation-gate-patterns.test.ts +44 -2
- package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +175 -0
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +5 -0
- package/dist/web/standalone/.next/static/chunks/6502.2305d0afd2385711.js +0 -9
- package/dist/web/standalone/.next/static/chunks/app/page-62be3b5fa91e4c8f.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{yowc5qPtuKxjOr22KmOAy → 5DLsjFHdSB6_a1EDQVjr7}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{yowc5qPtuKxjOr22KmOAy → 5DLsjFHdSB6_a1EDQVjr7}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { writeFileSync, unlinkSync, existsSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// --------------- types ---------------
|
|
8
|
+
|
|
9
|
+
export interface PlistOptions {
|
|
10
|
+
/** Absolute path to the Node.js binary */
|
|
11
|
+
nodePath: string;
|
|
12
|
+
/** Absolute path to the daemon script (cli.js) */
|
|
13
|
+
scriptPath: string;
|
|
14
|
+
/** Absolute path to the config file */
|
|
15
|
+
configPath: string;
|
|
16
|
+
/** Directory to use as WorkingDirectory in the plist (defaults to homedir) */
|
|
17
|
+
workingDirectory?: string;
|
|
18
|
+
/** Override stdout log path */
|
|
19
|
+
stdoutPath?: string;
|
|
20
|
+
/** Override stderr log path */
|
|
21
|
+
stderrPath?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LaunchdStatus {
|
|
25
|
+
/** Whether the daemon is registered with launchd */
|
|
26
|
+
registered: boolean;
|
|
27
|
+
/** PID if currently running, null otherwise */
|
|
28
|
+
pid: number | null;
|
|
29
|
+
/** Last exit status code, null if never exited or not available */
|
|
30
|
+
lastExitStatus: number | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type RunCommandFn = (cmd: string) => string;
|
|
34
|
+
|
|
35
|
+
// --------------- constants ---------------
|
|
36
|
+
|
|
37
|
+
const LABEL = 'com.gsd.daemon';
|
|
38
|
+
const PLIST_FILENAME = `${LABEL}.plist`;
|
|
39
|
+
|
|
40
|
+
// --------------- helpers ---------------
|
|
41
|
+
|
|
42
|
+
/** Escape special XML characters in a string. */
|
|
43
|
+
export function escapeXml(str: string): string {
|
|
44
|
+
return str
|
|
45
|
+
.replace(/&/g, '&')
|
|
46
|
+
.replace(/</g, '<')
|
|
47
|
+
.replace(/>/g, '>')
|
|
48
|
+
.replace(/"/g, '"')
|
|
49
|
+
.replace(/'/g, ''');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Return the canonical plist path under ~/Library/LaunchAgents/. */
|
|
53
|
+
export function getPlistPath(): string {
|
|
54
|
+
return resolve(homedir(), 'Library', 'LaunchAgents', PLIST_FILENAME);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the NVM-aware PATH string.
|
|
59
|
+
* Includes the directory containing the Node binary so that launchd can find node
|
|
60
|
+
* even when launched outside a shell session (where NVM isn't sourced).
|
|
61
|
+
*/
|
|
62
|
+
function buildEnvPath(nodePath: string): string {
|
|
63
|
+
const nodeBinDir = dirname(nodePath);
|
|
64
|
+
// Keep system essentials and prepend the node binary's directory
|
|
65
|
+
return `${nodeBinDir}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --------------- plist generation ---------------
|
|
69
|
+
|
|
70
|
+
/** Generate valid launchd plist XML for the GSD daemon. */
|
|
71
|
+
export function generatePlist(opts: PlistOptions): string {
|
|
72
|
+
const home = homedir();
|
|
73
|
+
const workDir = opts.workingDirectory ?? home;
|
|
74
|
+
const stdoutPath = opts.stdoutPath ?? resolve(home, '.gsd', 'daemon-stdout.log');
|
|
75
|
+
const stderrPath = opts.stderrPath ?? resolve(home, '.gsd', 'daemon-stderr.log');
|
|
76
|
+
const envPath = buildEnvPath(opts.nodePath);
|
|
77
|
+
|
|
78
|
+
// Forward ANTHROPIC_API_KEY so the orchestrator LLM can authenticate.
|
|
79
|
+
// Captured at install time from the current process environment.
|
|
80
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
81
|
+
const anthropicKeyXml = anthropicKey
|
|
82
|
+
? `\n\t\t<key>ANTHROPIC_API_KEY</key>\n\t\t<string>${escapeXml(anthropicKey)}</string>`
|
|
83
|
+
: '';
|
|
84
|
+
|
|
85
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
86
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
87
|
+
<plist version="1.0">
|
|
88
|
+
<dict>
|
|
89
|
+
\t<key>Label</key>
|
|
90
|
+
\t<string>${escapeXml(LABEL)}</string>
|
|
91
|
+
|
|
92
|
+
\t<key>ProgramArguments</key>
|
|
93
|
+
\t<array>
|
|
94
|
+
\t\t<string>${escapeXml(opts.nodePath)}</string>
|
|
95
|
+
\t\t<string>${escapeXml(opts.scriptPath)}</string>
|
|
96
|
+
\t\t<string>--config</string>
|
|
97
|
+
\t\t<string>${escapeXml(opts.configPath)}</string>
|
|
98
|
+
\t</array>
|
|
99
|
+
|
|
100
|
+
\t<key>KeepAlive</key>
|
|
101
|
+
\t<dict>
|
|
102
|
+
\t\t<key>SuccessfulExit</key>
|
|
103
|
+
\t\t<false/>
|
|
104
|
+
\t</dict>
|
|
105
|
+
|
|
106
|
+
\t<key>RunAtLoad</key>
|
|
107
|
+
\t<true/>
|
|
108
|
+
|
|
109
|
+
\t<key>EnvironmentVariables</key>
|
|
110
|
+
\t<dict>
|
|
111
|
+
\t\t<key>PATH</key>
|
|
112
|
+
\t\t<string>${escapeXml(envPath)}</string>
|
|
113
|
+
\t\t<key>HOME</key>
|
|
114
|
+
\t\t<string>${escapeXml(home)}</string>${anthropicKeyXml}
|
|
115
|
+
\t</dict>
|
|
116
|
+
|
|
117
|
+
\t<key>WorkingDirectory</key>
|
|
118
|
+
\t<string>${escapeXml(workDir)}</string>
|
|
119
|
+
|
|
120
|
+
\t<key>StandardOutPath</key>
|
|
121
|
+
\t<string>${escapeXml(stdoutPath)}</string>
|
|
122
|
+
|
|
123
|
+
\t<key>StandardErrorPath</key>
|
|
124
|
+
\t<string>${escapeXml(stderrPath)}</string>
|
|
125
|
+
</dict>
|
|
126
|
+
</plist>
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --------------- install / uninstall / status ---------------
|
|
131
|
+
|
|
132
|
+
/** Default runCommand using execSync. */
|
|
133
|
+
function defaultRunCommand(cmd: string): string {
|
|
134
|
+
return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Install the launchd agent: write plist and load it.
|
|
139
|
+
* Idempotent — unloads first if already loaded.
|
|
140
|
+
*/
|
|
141
|
+
export function install(
|
|
142
|
+
opts: PlistOptions,
|
|
143
|
+
runCommand: RunCommandFn = defaultRunCommand,
|
|
144
|
+
): void {
|
|
145
|
+
const plistPath = getPlistPath();
|
|
146
|
+
const xml = generatePlist(opts);
|
|
147
|
+
|
|
148
|
+
// Unload first if already present (ignore errors)
|
|
149
|
+
if (existsSync(plistPath)) {
|
|
150
|
+
try {
|
|
151
|
+
runCommand(`launchctl unload ${plistPath}`);
|
|
152
|
+
} catch {
|
|
153
|
+
// already unloaded — fine
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
writeFileSync(plistPath, xml, 'utf-8');
|
|
158
|
+
chmodSync(plistPath, 0o644);
|
|
159
|
+
|
|
160
|
+
runCommand(`launchctl load ${plistPath}`);
|
|
161
|
+
|
|
162
|
+
// Verify it loaded
|
|
163
|
+
try {
|
|
164
|
+
runCommand(`launchctl list ${LABEL}`);
|
|
165
|
+
} catch {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Plist was written to ${plistPath} and launchctl load succeeded, but launchctl list ${LABEL} failed. The agent may not have started.`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Uninstall the launchd agent: unload and remove plist.
|
|
174
|
+
* Graceful — does not throw if already uninstalled.
|
|
175
|
+
*/
|
|
176
|
+
export function uninstall(runCommand: RunCommandFn = defaultRunCommand): void {
|
|
177
|
+
const plistPath = getPlistPath();
|
|
178
|
+
|
|
179
|
+
if (existsSync(plistPath)) {
|
|
180
|
+
try {
|
|
181
|
+
runCommand(`launchctl unload ${plistPath}`);
|
|
182
|
+
} catch {
|
|
183
|
+
// already unloaded — that's fine
|
|
184
|
+
}
|
|
185
|
+
unlinkSync(plistPath);
|
|
186
|
+
}
|
|
187
|
+
// If plist doesn't exist, nothing to do — already uninstalled
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Query launchd for the daemon's status.
|
|
192
|
+
* Returns structured information about registration, PID, and last exit code.
|
|
193
|
+
*
|
|
194
|
+
* Handles two launchctl output formats:
|
|
195
|
+
* 1. Tabular: "PID\tStatus\tLabel" (older macOS)
|
|
196
|
+
* 2. JSON-style dict: `"PID" = 1234;` / `"LastExitStatus" = 0;` (newer macOS)
|
|
197
|
+
*/
|
|
198
|
+
export function status(runCommand: RunCommandFn = defaultRunCommand): LaunchdStatus {
|
|
199
|
+
try {
|
|
200
|
+
const output = runCommand(`launchctl list ${LABEL}`);
|
|
201
|
+
|
|
202
|
+
// --- Try tabular format first ---
|
|
203
|
+
const lines = output.trim().split('\n');
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
const parts = line.trim().split(/\t+/);
|
|
206
|
+
if (parts.length >= 3 && parts[2] === LABEL) {
|
|
207
|
+
const pidStr = parts[0];
|
|
208
|
+
const statusStr = parts[1];
|
|
209
|
+
|
|
210
|
+
const pid = pidStr === '-' ? null : parseInt(pidStr, 10);
|
|
211
|
+
const lastExitStatus = statusStr != null ? parseInt(statusStr, 10) : null;
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
registered: true,
|
|
215
|
+
pid: Number.isNaN(pid!) ? null : pid,
|
|
216
|
+
lastExitStatus: Number.isNaN(lastExitStatus!) ? null : lastExitStatus,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- Try JSON-style dict format ---
|
|
222
|
+
// Matches: "PID" = 1234; or "LastExitStatus" = 0;
|
|
223
|
+
const pidMatch = output.match(/"PID"\s*=\s*(\d+)\s*;/);
|
|
224
|
+
const exitMatch = output.match(/"LastExitStatus"\s*=\s*(\d+)\s*;/);
|
|
225
|
+
|
|
226
|
+
if (pidMatch || exitMatch) {
|
|
227
|
+
const pid = pidMatch ? parseInt(pidMatch[1], 10) : null;
|
|
228
|
+
const lastExitStatus = exitMatch ? parseInt(exitMatch[1], 10) : null;
|
|
229
|
+
return {
|
|
230
|
+
registered: true,
|
|
231
|
+
pid: Number.isNaN(pid!) ? null : pid,
|
|
232
|
+
lastExitStatus: Number.isNaN(lastExitStatus!) ? null : lastExitStatus,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Label resolved (no error) but no parseable output — still registered
|
|
237
|
+
return { registered: true, pid: null, lastExitStatus: null };
|
|
238
|
+
} catch {
|
|
239
|
+
// launchctl list exits non-zero when the label isn't found
|
|
240
|
+
return { registered: false, pid: null, lastExitStatus: null };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -65,7 +65,7 @@ describe('MessageBatcher', () => {
|
|
|
65
65
|
await batcher.destroy();
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it('
|
|
68
|
+
it('skips embeds for batched messages (only content)', async () => {
|
|
69
69
|
const { fn, calls } = createSend();
|
|
70
70
|
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 2, flushIntervalMs: 60_000 });
|
|
71
71
|
|
|
@@ -74,7 +74,7 @@ describe('MessageBatcher', () => {
|
|
|
74
74
|
await new Promise((r) => setTimeout(r, 10));
|
|
75
75
|
|
|
76
76
|
assert.equal(calls.length, 1);
|
|
77
|
-
assert.equal(calls[0].embeds.length,
|
|
77
|
+
assert.equal(calls[0].embeds.length, 0, 'batched sends skip embeds to avoid duplication');
|
|
78
78
|
assert.equal(calls[0].content, 'a\nb');
|
|
79
79
|
|
|
80
80
|
await batcher.destroy();
|
|
@@ -162,6 +162,10 @@ export class MessageBatcher {
|
|
|
162
162
|
/**
|
|
163
163
|
* Build a SendPayload from a batch of FormattedEvents and invoke the send callback.
|
|
164
164
|
* Catches and logs errors — never throws.
|
|
165
|
+
*
|
|
166
|
+
* For batched messages (2+ events), we send content-only to avoid duplication
|
|
167
|
+
* between content text and embed descriptions, and to stay under Discord's
|
|
168
|
+
* 10-embed limit. Single-event sends include the embed for rich formatting.
|
|
165
169
|
*/
|
|
166
170
|
private async doSend(batch: FormattedEvent[]): Promise<void> {
|
|
167
171
|
if (batch.length === 0) return;
|
|
@@ -169,10 +173,12 @@ export class MessageBatcher {
|
|
|
169
173
|
// Combine content lines
|
|
170
174
|
const content = batch.map((e) => e.content).join('\n');
|
|
171
175
|
|
|
172
|
-
//
|
|
176
|
+
// For single events, include the embed for rich formatting.
|
|
177
|
+
// For batches, skip embeds — the content lines are self-descriptive and
|
|
178
|
+
// embeds would duplicate the information + risk hitting Discord's 10-embed cap.
|
|
173
179
|
const embeds: unknown[] = [];
|
|
174
|
-
|
|
175
|
-
|
|
180
|
+
if (batch.length === 1 && batch[0].embed) {
|
|
181
|
+
embeds.push(batch[0].embed);
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
// Collect all component rows (only from the last event with components —
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { z } from 'zod';
|
|
15
|
+
import { readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
15
18
|
import type Anthropic from '@anthropic-ai/sdk';
|
|
16
19
|
import type {
|
|
17
20
|
MessageParam,
|
|
@@ -26,6 +29,93 @@ import type { ChannelManager } from './channel-manager.js';
|
|
|
26
29
|
import type { ProjectInfo, ManagedSession } from './types.js';
|
|
27
30
|
import type { Logger } from './logger.js';
|
|
28
31
|
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// OAuth token resolution — reads GSD's auth.json, refreshes if expired
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
interface OAuthCredentials {
|
|
37
|
+
type: 'oauth';
|
|
38
|
+
refresh: string;
|
|
39
|
+
access: string;
|
|
40
|
+
expires: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
44
|
+
const CLIENT_ID = atob('OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read the Anthropic OAuth access token from GSD's auth.json.
|
|
48
|
+
* If expired, refresh it and write the new credentials back.
|
|
49
|
+
* Falls back to ANTHROPIC_API_KEY env var if no OAuth credential exists.
|
|
50
|
+
*/
|
|
51
|
+
async function resolveAnthropicApiKey(logger?: Logger): Promise<string> {
|
|
52
|
+
// Try env var first (explicit override)
|
|
53
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
54
|
+
return process.env.ANTHROPIC_API_KEY;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const authPath = join(homedir(), '.gsd', 'agent', 'auth.json');
|
|
58
|
+
let authData: Record<string, unknown>;
|
|
59
|
+
try {
|
|
60
|
+
authData = JSON.parse(readFileSync(authPath, 'utf-8'));
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'No Anthropic auth found. Run `gsd login` to authenticate, or set ANTHROPIC_API_KEY.',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cred = authData.anthropic as OAuthCredentials | undefined;
|
|
68
|
+
if (!cred || cred.type !== 'oauth' || !cred.access) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
'No Anthropic OAuth credential in auth.json. Run `gsd login` to authenticate.',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// If token is still valid, use it
|
|
75
|
+
if (Date.now() < cred.expires) {
|
|
76
|
+
return cred.access;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Token expired — refresh it
|
|
80
|
+
logger?.info('orchestrator: refreshing Anthropic OAuth token');
|
|
81
|
+
const response = await fetch(TOKEN_URL, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
grant_type: 'refresh_token',
|
|
86
|
+
client_id: CLIENT_ID,
|
|
87
|
+
refresh_token: cred.refresh,
|
|
88
|
+
}),
|
|
89
|
+
signal: AbortSignal.timeout(30_000),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const error = await response.text();
|
|
94
|
+
throw new Error(`Anthropic token refresh failed: ${error}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const data = (await response.json()) as {
|
|
98
|
+
access_token: string;
|
|
99
|
+
refresh_token: string;
|
|
100
|
+
expires_in: number;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const newCred: OAuthCredentials = {
|
|
104
|
+
type: 'oauth',
|
|
105
|
+
refresh: data.refresh_token,
|
|
106
|
+
access: data.access_token,
|
|
107
|
+
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Write back to auth.json
|
|
111
|
+
authData.anthropic = newCred;
|
|
112
|
+
writeFileSync(authPath, JSON.stringify(authData, null, 2), 'utf-8');
|
|
113
|
+
chmodSync(authPath, 0o600);
|
|
114
|
+
logger?.info('orchestrator: Anthropic OAuth token refreshed');
|
|
115
|
+
|
|
116
|
+
return newCred.access;
|
|
117
|
+
}
|
|
118
|
+
|
|
29
119
|
// ---------------------------------------------------------------------------
|
|
30
120
|
// Configuration
|
|
31
121
|
// ---------------------------------------------------------------------------
|
|
@@ -164,11 +254,13 @@ export class Orchestrator {
|
|
|
164
254
|
|
|
165
255
|
/**
|
|
166
256
|
* Lazily initialise the Anthropic client. Dynamic import handles K007 module resolution.
|
|
257
|
+
* Resolves auth from GSD's OAuth credentials (auth.json), refreshing if needed.
|
|
167
258
|
*/
|
|
168
259
|
private async getClient(): Promise<Anthropic> {
|
|
169
260
|
if (this.client) return this.client;
|
|
261
|
+
const apiKey = await resolveAnthropicApiKey(this.deps.logger);
|
|
170
262
|
const { default: AnthropicSDK } = await import('@anthropic-ai/sdk');
|
|
171
|
-
this.client = new AnthropicSDK();
|
|
263
|
+
this.client = new AnthropicSDK({ apiKey });
|
|
172
264
|
return this.client;
|
|
173
265
|
}
|
|
174
266
|
|
|
@@ -204,6 +296,9 @@ export class Orchestrator {
|
|
|
204
296
|
this.history.push({ role: 'user', content });
|
|
205
297
|
|
|
206
298
|
try {
|
|
299
|
+
// Show typing indicator while processing
|
|
300
|
+
await message.channel.sendTyping().catch(() => {});
|
|
301
|
+
|
|
207
302
|
const responseText = await this.runAgentLoop();
|
|
208
303
|
|
|
209
304
|
// Send response to Discord
|
|
@@ -215,6 +310,12 @@ export class Orchestrator {
|
|
|
215
310
|
});
|
|
216
311
|
} catch (err) {
|
|
217
312
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
313
|
+
|
|
314
|
+
// Invalidate cached client on auth errors so next call re-resolves OAuth token
|
|
315
|
+
if (errorMsg.includes('authentication') || errorMsg.includes('apiKey') || errorMsg.includes('authToken') || errorMsg.includes('401')) {
|
|
316
|
+
this.client = null;
|
|
317
|
+
}
|
|
318
|
+
|
|
218
319
|
this.deps.logger.error('orchestrator error', {
|
|
219
320
|
error: errorMsg,
|
|
220
321
|
userId: message.author.id,
|
|
@@ -436,5 +537,8 @@ export interface DiscordMessageLike {
|
|
|
436
537
|
author: { id: string; bot: boolean };
|
|
437
538
|
channelId: string;
|
|
438
539
|
content: string;
|
|
439
|
-
channel: {
|
|
540
|
+
channel: {
|
|
541
|
+
send: (content: string) => Promise<unknown>;
|
|
542
|
+
sendTyping: () => Promise<unknown>;
|
|
543
|
+
};
|
|
440
544
|
}
|
package/pkg/package.json
CHANGED
|
@@ -41,5 +41,8 @@ export function isInfrastructureError(err: unknown): string | null {
|
|
|
41
41
|
for (const code of INFRA_ERROR_CODES) {
|
|
42
42
|
if (msg.includes(code)) return code;
|
|
43
43
|
}
|
|
44
|
+
// SQLite WAL corruption is not transient — retrying burns LLM budget
|
|
45
|
+
// for guaranteed failures (#2823).
|
|
46
|
+
if (msg.includes("database disk image is malformed")) return "SQLITE_CORRUPT";
|
|
44
47
|
return null;
|
|
45
48
|
}
|
|
@@ -677,13 +677,13 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
677
677
|
if (validationPath) {
|
|
678
678
|
const validationContent = await loadFile(validationPath);
|
|
679
679
|
if (validationContent) {
|
|
680
|
-
// Accept either the structured template format (table with MET/N/A)
|
|
680
|
+
// Accept either the structured template format (table with MET/N/A/SATISFIED)
|
|
681
681
|
// or prose evidence patterns the validation agent may emit.
|
|
682
682
|
const structuredMatch =
|
|
683
683
|
validationContent.includes("Operational") &&
|
|
684
|
-
(validationContent.includes("MET") || validationContent.includes("N/A"));
|
|
684
|
+
(validationContent.includes("MET") || validationContent.includes("N/A") || validationContent.includes("SATISFIED"));
|
|
685
685
|
const proseMatch =
|
|
686
|
-
/[Oo]perational[\s
|
|
686
|
+
/[Oo]perational[\s\S]{0,500}?(?:✅|pass|verified|confirmed|met|complete|true|yes|addressed|covered|satisfied|partially|n\/a|not[\s-]+applicable)/i.test(validationContent);
|
|
687
687
|
const hasOperationalCheck = structuredMatch || proseMatch;
|
|
688
688
|
if (!hasOperationalCheck) {
|
|
689
689
|
return {
|
|
@@ -1264,12 +1264,17 @@ export function mergeMilestoneToMain(
|
|
|
1264
1264
|
// 1. Auto-commit dirty state in worktree before leaving
|
|
1265
1265
|
autoCommitDirtyState(worktreeCwd);
|
|
1266
1266
|
|
|
1267
|
-
// Reconcile worktree DB into main DB before leaving worktree context
|
|
1267
|
+
// Reconcile worktree DB into main DB before leaving worktree context.
|
|
1268
|
+
// Skip when both paths resolve to the same physical file (shared WAL /
|
|
1269
|
+
// symlink layout) — ATTACHing a WAL-mode file to itself corrupts the
|
|
1270
|
+
// database (#2823).
|
|
1268
1271
|
if (isDbAvailable()) {
|
|
1269
1272
|
try {
|
|
1270
1273
|
const worktreeDbPath = join(worktreeCwd, ".gsd", "gsd.db");
|
|
1271
1274
|
const mainDbPath = join(originalBasePath_, ".gsd", "gsd.db");
|
|
1272
|
-
|
|
1275
|
+
if (!isSamePath(worktreeDbPath, mainDbPath)) {
|
|
1276
|
+
reconcileWorktreeDb(mainDbPath, worktreeDbPath);
|
|
1277
|
+
}
|
|
1273
1278
|
} catch {
|
|
1274
1279
|
/* non-fatal */
|
|
1275
1280
|
}
|
|
@@ -1061,6 +1061,11 @@ export async function startAuto(
|
|
|
1061
1061
|
verboseMode: boolean,
|
|
1062
1062
|
options?: { step?: boolean },
|
|
1063
1063
|
): Promise<void> {
|
|
1064
|
+
if (s.active) {
|
|
1065
|
+
debugLog("startAuto", { phase: "already-active", skipping: true });
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1064
1069
|
const requestedStepMode = options?.step ?? false;
|
|
1065
1070
|
|
|
1066
1071
|
// Escape stale worktree cwd from a previous milestone (#608).
|
|
@@ -924,7 +924,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|
|
924
924
|
promptSnippet: "Validate a GSD milestone (DB write + VALIDATION.md render)",
|
|
925
925
|
promptGuidelines: [
|
|
926
926
|
"Use gsd_validate_milestone when all slices are done and the milestone needs validation before completion.",
|
|
927
|
-
"Parameters: milestoneId, verdict, remediationRound, successCriteriaChecklist, sliceDeliveryAudit, crossSliceIntegration, requirementCoverage, verdictRationale, remediationPlan (optional).",
|
|
927
|
+
"Parameters: milestoneId, verdict, remediationRound, successCriteriaChecklist, sliceDeliveryAudit, crossSliceIntegration, requirementCoverage, verificationClasses (optional), verdictRationale, remediationPlan (optional).",
|
|
928
928
|
"If verdict is 'needs-remediation', also provide remediationPlan and use gsd_reassess_roadmap to add remediation slices to the roadmap.",
|
|
929
929
|
"On success, returns validationPath where VALIDATION.md was written.",
|
|
930
930
|
],
|
|
@@ -936,6 +936,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|
|
936
936
|
sliceDeliveryAudit: Type.String({ description: "Markdown table auditing each slice's claimed vs delivered output" }),
|
|
937
937
|
crossSliceIntegration: Type.String({ description: "Markdown describing any cross-slice boundary mismatches" }),
|
|
938
938
|
requirementCoverage: Type.String({ description: "Markdown describing any unaddressed requirements" }),
|
|
939
|
+
verificationClasses: Type.Optional(Type.String({ description: "Markdown describing verification class compliance and gaps" })),
|
|
939
940
|
verdictRationale: Type.String({ description: "Why this verdict was chosen" }),
|
|
940
941
|
remediationPlan: Type.Optional(Type.String({ description: "Remediation plan (required if verdict is needs-remediation)" })),
|
|
941
942
|
}),
|
|
@@ -26,9 +26,20 @@ export function getPriorSliceCompletionBlocker(
|
|
|
26
26
|
const { milestone: targetMid, slice: targetSid } = parseUnitId(unitId);
|
|
27
27
|
if (!targetMid || !targetSid) return null;
|
|
28
28
|
|
|
29
|
+
// Parallel worker isolation: when GSD_MILESTONE_LOCK is set, this worker
|
|
30
|
+
// is scoped to a single milestone. Skip the cross-milestone dependency
|
|
31
|
+
// check — other milestones are being handled by their own workers.
|
|
32
|
+
// Without this, the dispatch guard sees incomplete slices in M010/M011
|
|
33
|
+
// (cloned into the worktree DB) and blocks M012 from ever starting. #2797
|
|
34
|
+
const milestoneLock = process.env.GSD_MILESTONE_LOCK;
|
|
35
|
+
|
|
29
36
|
// Use findMilestoneIds to respect custom queue order.
|
|
30
37
|
// Only check milestones that come BEFORE the target in queue order.
|
|
31
|
-
|
|
38
|
+
// When locked to a specific milestone, only check that milestone's
|
|
39
|
+
// intra-slice dependencies — skip all cross-milestone checks.
|
|
40
|
+
const allIds = milestoneLock && targetMid === milestoneLock
|
|
41
|
+
? [targetMid]
|
|
42
|
+
: findMilestoneIds(base);
|
|
32
43
|
const targetIdx = allIds.indexOf(targetMid);
|
|
33
44
|
if (targetIdx < 0) return null;
|
|
34
45
|
const milestoneIds = allIds.slice(0, targetIdx + 1);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Schema is initialized on first open with WAL mode for file-backed DBs.
|
|
7
7
|
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
|
-
import { existsSync, copyFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
10
10
|
import { dirname } from "node:path";
|
|
11
11
|
import type { Decision, Requirement, GateRow, GateId, GateScope, GateStatus, GateVerdict } from "./types.js";
|
|
12
12
|
import { GSDError, GSD_STALE_STATE } from "./errors.js";
|
|
@@ -1761,6 +1761,11 @@ export function reconcileWorktreeDb(
|
|
|
1761
1761
|
): ReconcileResult {
|
|
1762
1762
|
const zero: ReconcileResult = { decisions: 0, requirements: 0, artifacts: 0, milestones: 0, slices: 0, tasks: 0, memories: 0, verification_evidence: 0, conflicts: [] };
|
|
1763
1763
|
if (!existsSync(worktreeDbPath)) return zero;
|
|
1764
|
+
// Guard: bail when both paths resolve to the same physical file.
|
|
1765
|
+
// ATTACHing a WAL-mode DB to itself corrupts the WAL (#2823).
|
|
1766
|
+
try {
|
|
1767
|
+
if (realpathSync(mainDbPath) === realpathSync(worktreeDbPath)) return zero;
|
|
1768
|
+
} catch { /* path resolution failed — fall through to existing checks */ }
|
|
1764
1769
|
// Sanitize path: reject any characters that could break ATTACH syntax.
|
|
1765
1770
|
// ATTACH DATABASE doesn't support parameterized paths in all providers,
|
|
1766
1771
|
// so we use strict allowlist validation instead.
|
|
@@ -519,8 +519,19 @@ function createMilestoneWorktree(basePath: string, milestoneId: string): string
|
|
|
519
519
|
|
|
520
520
|
/**
|
|
521
521
|
* Spawn a worker process for a milestone.
|
|
522
|
-
* The worker runs `gsd --
|
|
522
|
+
* The worker runs `gsd headless --json auto` in the milestone's worktree
|
|
523
523
|
* with GSD_MILESTONE_LOCK set to isolate state derivation.
|
|
524
|
+
*
|
|
525
|
+
* IMPORTANT: We use `headless --json auto` instead of `--print "/gsd auto"`.
|
|
526
|
+
* --print mode calls session.prompt() which returns immediately after the
|
|
527
|
+
* extension command handler fires, because auto-mode's ctx.newSession()
|
|
528
|
+
* resets the session and unblocks the outer prompt() await. This causes
|
|
529
|
+
* process.exit(0) to fire before any LLM work happens. See #2792.
|
|
530
|
+
*
|
|
531
|
+
* The headless subcommand uses an RPC client that keeps the process alive
|
|
532
|
+
* until auto-mode emits a terminal notification or the idle timer fires.
|
|
533
|
+
* It outputs NDJSON events to stdout (with --json), which our
|
|
534
|
+
* processWorkerLine() parser already understands.
|
|
524
535
|
*/
|
|
525
536
|
export function spawnWorker(
|
|
526
537
|
basePath: string,
|
|
@@ -537,7 +548,7 @@ export function spawnWorker(
|
|
|
537
548
|
|
|
538
549
|
let child: ChildProcess;
|
|
539
550
|
try {
|
|
540
|
-
child = spawn(process.execPath, [binPath, "
|
|
551
|
+
child = spawn(process.execPath, [binPath, "headless", "--json", "auto"], {
|
|
541
552
|
cwd: worker.worktreePath,
|
|
542
553
|
env: {
|
|
543
554
|
...process.env,
|
|
@@ -577,9 +588,10 @@ export function spawnWorker(
|
|
|
577
588
|
}
|
|
578
589
|
|
|
579
590
|
// ── NDJSON stdout monitoring ────────────────────────────────────────
|
|
580
|
-
// Workers run
|
|
581
|
-
// We parse message_end events to extract
|
|
582
|
-
// the coordinator's cost tracking in sync
|
|
591
|
+
// Workers run via `headless --json`, which forwards all RPC events
|
|
592
|
+
// as NDJSON to stdout. We parse message_end events to extract
|
|
593
|
+
// cost/token usage, keeping the coordinator's cost tracking in sync
|
|
594
|
+
// with actual API spend.
|
|
583
595
|
if (child.stdout) {
|
|
584
596
|
let stdoutBuffer = "";
|
|
585
597
|
child.stdout.on("data", (data: Buffer) => {
|
|
@@ -808,7 +820,12 @@ export async function stopParallel(
|
|
|
808
820
|
} catch { /* process may already be dead */ }
|
|
809
821
|
}
|
|
810
822
|
|
|
811
|
-
|
|
823
|
+
// Wait for the headless process to cascade SIGTERM to its RPC child.
|
|
824
|
+
// The headless signal handler calls client.stop() which sends SIGTERM
|
|
825
|
+
// to the RPC child and waits up to 1000ms. The previous 750ms window
|
|
826
|
+
// was insufficient — the parent got SIGKILL before the child died,
|
|
827
|
+
// leaving orphaned RPC processes holding auto.lock. See #2798.
|
|
828
|
+
const exitedAfterTerm = await waitForWorkerExit(worker, 3000);
|
|
812
829
|
if (!exitedAfterTerm && worker.pid > 0) {
|
|
813
830
|
try {
|
|
814
831
|
if (worker.process) {
|