@opengsd/gsd-pi 1.3.0-dev.65546769 → 1.3.0-dev.eed73bea
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/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +11 -2
- package/dist/resources/extensions/google-cli/stream-adapter.js +82 -15
- package/dist/resources/extensions/gsd/auto/orchestrator.js +12 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +17 -14
- package/dist/resources/extensions/gsd/auto-prompts.js +43 -12
- package/dist/resources/extensions/gsd/auto-recovery.js +13 -6
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +103 -13
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +6 -1
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -3
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -19
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +75 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
- package/dist/resources/extensions/gsd/commands-context.js +19 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +16 -10
- package/dist/resources/extensions/gsd/commands-worktree.js +12 -10
- package/dist/resources/extensions/gsd/dashboard-overlay.js +32 -3
- package/dist/resources/extensions/gsd/db/queries.js +60 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +92 -8
- package/dist/resources/extensions/gsd/exec-sandbox.js +45 -9
- package/dist/resources/extensions/gsd/forensics.js +2 -32
- package/dist/resources/extensions/gsd/git-service.js +4 -4
- package/dist/resources/extensions/gsd/guided-flow-queue.js +59 -5
- package/dist/resources/extensions/gsd/health-widget.js +55 -29
- package/dist/resources/extensions/gsd/markdown-renderer.js +6 -2
- package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +44 -21
- package/dist/resources/extensions/gsd/milestone-implementation-evidence.js +26 -20
- package/dist/resources/extensions/gsd/quick.js +45 -2
- package/dist/resources/extensions/gsd/session-forensics.js +11 -1
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +52 -3
- package/dist/resources/extensions/gsd/tools/complete-slice.js +34 -3
- package/dist/resources/extensions/gsd/tools/complete-task.js +78 -16
- package/dist/resources/extensions/gsd/tools/exec-tool.js +7 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +23 -7
- package/dist/resources/extensions/gsd/unit-registry.js +25 -3
- package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +33 -3
- package/dist/resources/extensions/gsd/validation-block-guard.js +9 -4
- package/dist/resources/extensions/gsd/workspace-git-preflight.js +30 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- 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/server/app/_global-error.html +1 -1
- 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/visualizer/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 +14 -14
- 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/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/{796.e0bdc932325d7e03.js → 796.3976108148518f7d.js} +3 -3
- package/dist/web/standalone/.next/static/chunks/{webpack-f46ea08200a0227e.js → webpack-7c1d97e39be2da11.js} +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/dist/workflow.d.ts +1 -0
- package/packages/contracts/dist/workflow.d.ts.map +1 -1
- package/packages/contracts/dist/workflow.js +2 -0
- package/packages/contracts/dist/workflow.js.map +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +21 -9
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/README.md +1 -1
- package/packages/mcp-server/dist/server.d.ts +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +3 -3
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +13 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +34 -20
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +20 -2
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +80 -0
- package/src/resources/extensions/google-cli/stream-adapter.ts +106 -19
- package/src/resources/extensions/gsd/auto/orchestrator.ts +25 -11
- package/src/resources/extensions/gsd/auto-dispatch.ts +18 -17
- package/src/resources/extensions/gsd/auto-prompts.ts +54 -12
- package/src/resources/extensions/gsd/auto-recovery.ts +13 -6
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -12
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +6 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +52 -18
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +82 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
- package/src/resources/extensions/gsd/commands-context.ts +18 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -9
- package/src/resources/extensions/gsd/commands-worktree.ts +12 -10
- package/src/resources/extensions/gsd/dashboard-overlay.ts +32 -3
- package/src/resources/extensions/gsd/db/queries.ts +79 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +103 -9
- package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
- package/src/resources/extensions/gsd/forensics.ts +2 -33
- package/src/resources/extensions/gsd/git-service.ts +5 -5
- package/src/resources/extensions/gsd/guided-flow-queue.ts +82 -4
- package/src/resources/extensions/gsd/health-widget.ts +69 -32
- package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
- package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +51 -19
- package/src/resources/extensions/gsd/milestone-implementation-evidence.ts +35 -21
- package/src/resources/extensions/gsd/quick.ts +43 -2
- package/src/resources/extensions/gsd/session-forensics.ts +11 -1
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +76 -8
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -1
- package/src/resources/extensions/gsd/tests/commands-context.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +80 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +48 -8
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +55 -2
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +26 -1
- package/src/resources/extensions/gsd/tests/doctor-forensics-db-open-regression.test.ts +70 -2
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/exec-tool.test.ts +45 -1
- package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +268 -3
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +119 -1
- package/src/resources/extensions/gsd/tests/integration/queue-active-milestone-context-budget.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/integration/quick-branch-lifecycle.test.ts +56 -9
- package/src/resources/extensions/gsd/tests/knowledge-cold-start.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +78 -0
- package/src/resources/extensions/gsd/tests/orchestrator-logs.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +54 -1
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +195 -1
- package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +193 -14
- package/src/resources/extensions/gsd/tests/unmerged-milestone-guard.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +151 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +30 -3
- package/src/resources/extensions/gsd/tools/complete-task.ts +86 -16
- package/src/resources/extensions/gsd/tools/exec-tool.ts +7 -3
- package/src/resources/extensions/gsd/unit-context-composer.ts +33 -7
- package/src/resources/extensions/gsd/unit-registry.ts +25 -3
- package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +41 -5
- package/src/resources/extensions/gsd/validation-block-guard.ts +13 -7
- package/src/resources/extensions/gsd/workspace-git-preflight.ts +31 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_ssgManifest.js +0 -0
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - Optional search/tool integrations (Brave, Tavily, Jina, Context7)
|
|
12
12
|
*/
|
|
13
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { access, readFile } from "node:fs/promises";
|
|
14
15
|
import { delimiter, join } from "node:path";
|
|
15
16
|
import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
16
17
|
import { getEnvApiKey } from "@gsd/pi-ai";
|
|
@@ -133,15 +134,11 @@ const CLI_AUTH_PATH_CHECK_PROVIDERS = new Set([
|
|
|
133
134
|
"google-gemini-cli",
|
|
134
135
|
"google-antigravity",
|
|
135
136
|
]);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
* Fast filesystem scan — no subprocess, no network, sub-1ms.
|
|
139
|
-
*/
|
|
140
|
-
function isCliBinaryInPath(providerId) {
|
|
137
|
+
let asyncCliBinaryPathCache = null;
|
|
138
|
+
function cliExecutableNames(providerId) {
|
|
141
139
|
const binaries = CLI_BINARY_MAP[providerId];
|
|
142
140
|
if (!binaries)
|
|
143
|
-
return
|
|
144
|
-
const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
141
|
+
return [];
|
|
145
142
|
// On Windows, command shims are commonly installed as .cmd/.exe/.bat/.com.
|
|
146
143
|
// Scan PATHEXT candidates in addition to the bare binary name.
|
|
147
144
|
const executableNames = [...binaries];
|
|
@@ -160,8 +157,45 @@ function isCliBinaryInPath(providerId) {
|
|
|
160
157
|
}
|
|
161
158
|
}
|
|
162
159
|
}
|
|
160
|
+
return executableNames;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if a CLI provider's binary exists anywhere in PATH.
|
|
164
|
+
* Fast filesystem scan — no subprocess, no network, sub-1ms.
|
|
165
|
+
*/
|
|
166
|
+
function isCliBinaryInPath(providerId) {
|
|
167
|
+
const cached = asyncCliBinaryPathCache?.get(providerId);
|
|
168
|
+
if (cached !== undefined)
|
|
169
|
+
return cached;
|
|
170
|
+
const executableNames = cliExecutableNames(providerId);
|
|
171
|
+
if (executableNames.length === 0)
|
|
172
|
+
return false;
|
|
173
|
+
const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
163
174
|
return pathDirs.some(dir => executableNames.some(name => existsSync(join(dir, name))));
|
|
164
175
|
}
|
|
176
|
+
async function isCliBinaryInPathAsync(providerId) {
|
|
177
|
+
const executableNames = cliExecutableNames(providerId);
|
|
178
|
+
if (executableNames.length === 0)
|
|
179
|
+
return false;
|
|
180
|
+
const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
181
|
+
const candidates = pathDirs.flatMap(dir => executableNames.map(name => join(dir, name)));
|
|
182
|
+
if (candidates.length === 0)
|
|
183
|
+
return false;
|
|
184
|
+
try {
|
|
185
|
+
await Promise.any(candidates.map(candidate => access(candidate)));
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function loadCliBinaryPathCache() {
|
|
193
|
+
const entries = await Promise.all(Object.keys(CLI_BINARY_MAP).map(async (providerId) => [
|
|
194
|
+
providerId,
|
|
195
|
+
await isCliBinaryInPathAsync(providerId),
|
|
196
|
+
]));
|
|
197
|
+
return new Map(entries);
|
|
198
|
+
}
|
|
165
199
|
function modelsJsonPaths() {
|
|
166
200
|
const home = homedir();
|
|
167
201
|
return [
|
|
@@ -170,7 +204,11 @@ function modelsJsonPaths() {
|
|
|
170
204
|
join(home, ".pi", "agent", "models.json"),
|
|
171
205
|
];
|
|
172
206
|
}
|
|
207
|
+
let asyncModelsJsonApiKeyCache = null;
|
|
173
208
|
function hasModelsJsonApiKey(providerId) {
|
|
209
|
+
if (asyncModelsJsonApiKeyCache) {
|
|
210
|
+
return asyncModelsJsonApiKeyCache.has(providerId);
|
|
211
|
+
}
|
|
174
212
|
for (const path of modelsJsonPaths()) {
|
|
175
213
|
if (!existsSync(path))
|
|
176
214
|
continue;
|
|
@@ -187,7 +225,25 @@ function hasModelsJsonApiKey(providerId) {
|
|
|
187
225
|
}
|
|
188
226
|
return false;
|
|
189
227
|
}
|
|
190
|
-
function
|
|
228
|
+
async function loadModelsJsonApiKeyCache() {
|
|
229
|
+
const providersWithKeys = new Set();
|
|
230
|
+
for (const path of modelsJsonPaths()) {
|
|
231
|
+
try {
|
|
232
|
+
const parsed = JSON.parse(await readFile(path, "utf-8"));
|
|
233
|
+
for (const [providerId, provider] of Object.entries(parsed.providers ?? {})) {
|
|
234
|
+
const apiKey = provider.apiKey;
|
|
235
|
+
if (typeof apiKey === "string" && apiKey.trim().length > 0) {
|
|
236
|
+
providersWithKeys.add(providerId);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Missing or malformed models.json should not break dashboard health checks.
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return providersWithKeys;
|
|
245
|
+
}
|
|
246
|
+
function resolveKeyFromAuthOrEnv(providerId) {
|
|
191
247
|
const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
|
|
192
248
|
if (providerId === "anthropic-vertex" && process.env.ANTHROPIC_VERTEX_PROJECT_ID) {
|
|
193
249
|
return { found: true, source: "env", backedOff: false };
|
|
@@ -234,6 +290,12 @@ function resolveKey(providerId) {
|
|
|
234
290
|
if (info?.envVar && process.env[info.envVar]) {
|
|
235
291
|
return { found: true, source: "env", backedOff: false };
|
|
236
292
|
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
function resolveKey(providerId) {
|
|
296
|
+
const direct = resolveKeyFromAuthOrEnv(providerId);
|
|
297
|
+
if (direct)
|
|
298
|
+
return direct;
|
|
237
299
|
if (hasModelsJsonApiKey(providerId)) {
|
|
238
300
|
return { found: true, source: "models.json", backedOff: false };
|
|
239
301
|
}
|
|
@@ -432,6 +494,28 @@ export function runProviderChecks() {
|
|
|
432
494
|
results.push(...checkOptionalProviders());
|
|
433
495
|
return results;
|
|
434
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Non-blocking equivalent of `runProviderChecks` for the health-widget
|
|
499
|
+
* background refresh. PATH checks and custom models.json discovery use async
|
|
500
|
+
* filesystem APIs so periodic widget refreshes do not stall the input loop.
|
|
501
|
+
*/
|
|
502
|
+
export async function runProviderChecksAsync() {
|
|
503
|
+
const [cliCache, modelsJsonCache] = await Promise.all([
|
|
504
|
+
loadCliBinaryPathCache(),
|
|
505
|
+
loadModelsJsonApiKeyCache(),
|
|
506
|
+
]);
|
|
507
|
+
const previousCliCache = asyncCliBinaryPathCache;
|
|
508
|
+
const previousModelsJsonCache = asyncModelsJsonApiKeyCache;
|
|
509
|
+
asyncCliBinaryPathCache = cliCache;
|
|
510
|
+
asyncModelsJsonApiKeyCache = modelsJsonCache;
|
|
511
|
+
try {
|
|
512
|
+
return runProviderChecks();
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
asyncCliBinaryPathCache = previousCliCache;
|
|
516
|
+
asyncModelsJsonApiKeyCache = previousModelsJsonCache;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
435
519
|
/**
|
|
436
520
|
* Format provider check results as a human-readable report string.
|
|
437
521
|
*/
|
|
@@ -180,10 +180,22 @@ export function runExecSandbox(request, opts) {
|
|
|
180
180
|
const effectiveGraceMs = opts.kill_grace_ms ?? SIGKILL_GRACE_MS;
|
|
181
181
|
const effectiveForceResolveDelay = opts.force_resolve_delay_ms ?? (effectiveGraceMs + HARD_DEADLINE_MS);
|
|
182
182
|
let timedOut = false;
|
|
183
|
+
let aborted = false;
|
|
183
184
|
let settled = false;
|
|
185
|
+
let killInitiated = false;
|
|
186
|
+
let timer;
|
|
184
187
|
let forceResolveTimer;
|
|
185
|
-
|
|
186
|
-
|
|
188
|
+
let abortListener;
|
|
189
|
+
const removeAbortListener = () => {
|
|
190
|
+
if (opts.signal && abortListener) {
|
|
191
|
+
opts.signal.removeEventListener("abort", abortListener);
|
|
192
|
+
abortListener = undefined;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const initiateKill = () => {
|
|
196
|
+
if (killInitiated)
|
|
197
|
+
return;
|
|
198
|
+
killInitiated = true;
|
|
187
199
|
// killProcessTree handles both platforms and kills the whole tree: on Unix
|
|
188
200
|
// it signals the process group (SIGTERM -> grace -> SIGKILL); on Windows it
|
|
189
201
|
// force-kills the tree via taskkill /F /T. Using child.kill("SIGTERM") here
|
|
@@ -201,14 +213,14 @@ export function runExecSandbox(request, opts) {
|
|
|
201
213
|
finalize(null, "SIGKILL", true);
|
|
202
214
|
}, effectiveForceResolveDelay);
|
|
203
215
|
forceResolveTimer.unref?.();
|
|
204
|
-
}
|
|
205
|
-
timer.unref?.();
|
|
216
|
+
};
|
|
206
217
|
const finalize = (exitCode, signal, forceResolved = false) => {
|
|
207
218
|
if (settled)
|
|
208
219
|
return;
|
|
209
220
|
settled = true;
|
|
210
221
|
clearTimeout(timer);
|
|
211
222
|
clearTimeout(forceResolveTimer);
|
|
223
|
+
removeAbortListener();
|
|
212
224
|
const duration = Date.now() - started;
|
|
213
225
|
const stdoutBuf = Buffer.concat(stdoutChunks);
|
|
214
226
|
const stderrBuf = Buffer.concat(stderrChunks);
|
|
@@ -219,17 +231,20 @@ export function runExecSandbox(request, opts) {
|
|
|
219
231
|
const digestBody = tail(stdoutBuf, opts.digest_chars);
|
|
220
232
|
const digest = digestBody.length > 0
|
|
221
233
|
? digestBody
|
|
222
|
-
:
|
|
223
|
-
? "[no stdout —
|
|
224
|
-
:
|
|
225
|
-
?
|
|
226
|
-
:
|
|
234
|
+
: aborted
|
|
235
|
+
? "[no stdout — aborted]"
|
|
236
|
+
: timedOut
|
|
237
|
+
? "[no stdout — timed out]"
|
|
238
|
+
: stderrBuf.length > 0
|
|
239
|
+
? `[no stdout — tail of stderr]\n${tail(stderrBuf, opts.digest_chars)}`
|
|
240
|
+
: "[no output]";
|
|
227
241
|
const result = {
|
|
228
242
|
id,
|
|
229
243
|
runtime: request.runtime,
|
|
230
244
|
exit_code: exitCode,
|
|
231
245
|
signal,
|
|
232
246
|
timed_out: timedOut,
|
|
247
|
+
aborted,
|
|
233
248
|
force_resolved: forceResolved,
|
|
234
249
|
duration_ms: duration,
|
|
235
250
|
stdout_bytes: stdoutBytes,
|
|
@@ -244,6 +259,26 @@ export function runExecSandbox(request, opts) {
|
|
|
244
259
|
writeMeta(metaPath, result, request, now);
|
|
245
260
|
resolveP(result);
|
|
246
261
|
};
|
|
262
|
+
timer = setTimeout(() => {
|
|
263
|
+
timedOut = true;
|
|
264
|
+
initiateKill();
|
|
265
|
+
}, timeoutMs);
|
|
266
|
+
timer.unref?.();
|
|
267
|
+
if (opts.signal) {
|
|
268
|
+
abortListener = () => {
|
|
269
|
+
if (settled || timedOut)
|
|
270
|
+
return;
|
|
271
|
+
aborted = true;
|
|
272
|
+
clearTimeout(timer);
|
|
273
|
+
initiateKill();
|
|
274
|
+
};
|
|
275
|
+
if (opts.signal.aborted) {
|
|
276
|
+
abortListener();
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
247
282
|
child.on("error", (err) => {
|
|
248
283
|
const message = err instanceof Error ? err.message : String(err);
|
|
249
284
|
const line = `child error: ${message}\n`;
|
|
@@ -274,6 +309,7 @@ function writeMeta(path, result, request, now) {
|
|
|
274
309
|
exit_code: result.exit_code,
|
|
275
310
|
signal: result.signal,
|
|
276
311
|
timed_out: result.timed_out,
|
|
312
|
+
aborted: result.aborted === true,
|
|
277
313
|
force_resolved: result.force_resolved,
|
|
278
314
|
duration_ms: result.duration_ms,
|
|
279
315
|
stdout_bytes: result.stdout_bytes,
|
|
@@ -22,8 +22,7 @@ import { deriveState } from "./state.js";
|
|
|
22
22
|
import { isAutoActive } from "./auto.js";
|
|
23
23
|
import { loadPrompt } from "./prompt-loader.js";
|
|
24
24
|
import { gsdRoot } from "./paths.js";
|
|
25
|
-
import { isDbAvailable,
|
|
26
|
-
import { isClosedStatus } from "./status-guards.js";
|
|
25
|
+
import { isDbAvailable, getHierarchyCompletionCounts } from "./gsd-db.js";
|
|
27
26
|
import { formatDuration } from "../shared/format-utils.js";
|
|
28
27
|
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
29
28
|
import { clearGSDPreferencesCache, loadEffectiveGSDPreferences, loadGlobalGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
|
|
@@ -593,36 +592,7 @@ function loadCompletedKeys(basePath) {
|
|
|
593
592
|
function getDbCompletionCounts() {
|
|
594
593
|
if (!isDbAvailable())
|
|
595
594
|
return null;
|
|
596
|
-
|
|
597
|
-
let completedMilestones = 0;
|
|
598
|
-
let totalSlices = 0;
|
|
599
|
-
let completedSlices = 0;
|
|
600
|
-
let totalTasks = 0;
|
|
601
|
-
let completedTasks = 0;
|
|
602
|
-
for (const m of milestones) {
|
|
603
|
-
if (isClosedStatus(m.status))
|
|
604
|
-
completedMilestones++;
|
|
605
|
-
const slices = getMilestoneSlices(m.id);
|
|
606
|
-
for (const s of slices) {
|
|
607
|
-
totalSlices++;
|
|
608
|
-
if (isClosedStatus(s.status))
|
|
609
|
-
completedSlices++;
|
|
610
|
-
const tasks = getSliceTasks(m.id, s.id);
|
|
611
|
-
for (const t of tasks) {
|
|
612
|
-
totalTasks++;
|
|
613
|
-
if (isClosedStatus(t.status))
|
|
614
|
-
completedTasks++;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
return {
|
|
619
|
-
milestones: completedMilestones,
|
|
620
|
-
milestonesTotal: milestones.length,
|
|
621
|
-
slices: completedSlices,
|
|
622
|
-
slicesTotal: totalSlices,
|
|
623
|
-
tasks: completedTasks,
|
|
624
|
-
tasksTotal: totalTasks,
|
|
625
|
-
};
|
|
595
|
+
return getHierarchyCompletionCounts();
|
|
626
596
|
}
|
|
627
597
|
// ─── Anomaly Detectors ───────────────────────────────────────────────────────
|
|
628
598
|
/**
|
|
@@ -225,6 +225,7 @@ export const RUNTIME_EXCLUSION_PATHS = [
|
|
|
225
225
|
".gsd/event-log.jsonl",
|
|
226
226
|
".gsd/DISCUSSION-MANIFEST.json",
|
|
227
227
|
];
|
|
228
|
+
const runtimeFilesCleanedUpRepos = new Set();
|
|
228
229
|
// ─── Integration Branch Metadata ───────────────────────────────────────────
|
|
229
230
|
/**
|
|
230
231
|
* Path to the milestone metadata file that stores the integration branch.
|
|
@@ -565,7 +566,8 @@ export class GitServiceImpl {
|
|
|
565
566
|
// and the worktree is torn down. This prevents a mid-execution behavioral
|
|
566
567
|
// discontinuity where the first half of a milestone has .gsd/ artifacts
|
|
567
568
|
// committed but the second half doesn't (#1326).
|
|
568
|
-
|
|
569
|
+
const cleanupRepoKey = resolve(this.basePath);
|
|
570
|
+
if (!runtimeFilesCleanedUpRepos.has(cleanupRepoKey)) {
|
|
569
571
|
let cleaned = false;
|
|
570
572
|
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
|
|
571
573
|
const removed = nativeRmCached(this.basePath, [exclusion]);
|
|
@@ -575,7 +577,7 @@ export class GitServiceImpl {
|
|
|
575
577
|
if (cleaned) {
|
|
576
578
|
nativeCommit(this.basePath, "chore: untrack .gsd/ runtime files from git index", { allowEmpty: false });
|
|
577
579
|
}
|
|
578
|
-
|
|
580
|
+
runtimeFilesCleanedUpRepos.add(cleanupRepoKey);
|
|
579
581
|
}
|
|
580
582
|
// Stage everything using pathspec exclusions so excluded paths are never
|
|
581
583
|
// hashed by git. The old approach of `git add -A` followed by unstaging
|
|
@@ -683,8 +685,6 @@ export class GitServiceImpl {
|
|
|
683
685
|
return false;
|
|
684
686
|
}
|
|
685
687
|
}
|
|
686
|
-
/** Tracks whether runtime file cleanup has run this session. */
|
|
687
|
-
_runtimeFilesCleanedUp = false;
|
|
688
688
|
/**
|
|
689
689
|
* Stage files (smart staging) and commit.
|
|
690
690
|
* Returns the commit message string on success, or null if nothing to commit.
|
|
@@ -12,7 +12,7 @@ import { loadFile } from "./files.js";
|
|
|
12
12
|
import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
|
|
13
13
|
import { deriveState } from "./state.js";
|
|
14
14
|
import { invalidateAllCaches } from "./cache.js";
|
|
15
|
-
import { gsdRoot, resolveMilestoneFile, resolveGsdRootFile, relGsdRootFile, } from "./paths.js";
|
|
15
|
+
import { gsdRoot, resolveMilestoneFile, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, } from "./paths.js";
|
|
16
16
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
17
17
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
18
18
|
import { nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
|
|
@@ -20,6 +20,9 @@ import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
|
20
20
|
import { saveQueueOrder } from "./queue-order.js";
|
|
21
21
|
import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
|
|
22
22
|
import { isFutureMilestoneStatus } from "./status-guards.js";
|
|
23
|
+
const QUEUE_ARTIFACT_EXCERPT_MAX_CHARS = 20_000;
|
|
24
|
+
const QUEUE_EXISTING_MILESTONES_CONTEXT_MAX_CHARS = 120_000;
|
|
25
|
+
const QUEUE_CONTEXT_SECTION_SEPARATOR = "\n\n---\n\n";
|
|
23
26
|
// ─── Queue Entry Point ──────────────────────────────────────────────────────
|
|
24
27
|
/**
|
|
25
28
|
* Queue future milestones via conversational intake.
|
|
@@ -215,7 +218,7 @@ export async function buildExistingMilestonesContext(basePath, milestoneIds, sta
|
|
|
215
218
|
if (contextFile) {
|
|
216
219
|
const content = await loadFile(contextFile);
|
|
217
220
|
if (content) {
|
|
218
|
-
parts.push(`\n**Context:**\n${content
|
|
221
|
+
parts.push(`\n**Context:**\n${summarizeArtifactForQueue(content, relMilestoneFile(basePath, mid, "CONTEXT"))}`);
|
|
219
222
|
}
|
|
220
223
|
}
|
|
221
224
|
else {
|
|
@@ -224,7 +227,7 @@ export async function buildExistingMilestonesContext(basePath, milestoneIds, sta
|
|
|
224
227
|
if (draftFile) {
|
|
225
228
|
const draftContent = await loadFile(draftFile);
|
|
226
229
|
if (draftContent) {
|
|
227
|
-
parts.push(`\n**Draft context available:**\n${draftContent
|
|
230
|
+
parts.push(`\n**Draft context available:**\n${summarizeArtifactForQueue(draftContent, relMilestoneFile(basePath, mid, "CONTEXT-DRAFT"))}`);
|
|
228
231
|
}
|
|
229
232
|
}
|
|
230
233
|
}
|
|
@@ -235,7 +238,7 @@ export async function buildExistingMilestonesContext(basePath, milestoneIds, sta
|
|
|
235
238
|
if (roadmapFile) {
|
|
236
239
|
const content = await loadFile(roadmapFile);
|
|
237
240
|
if (content) {
|
|
238
|
-
parts.push(`\n**Roadmap:**\n${content
|
|
241
|
+
parts.push(`\n**Roadmap:**\n${summarizeArtifactForQueue(content, relMilestoneFile(basePath, mid, "ROADMAP"))}`);
|
|
239
242
|
}
|
|
240
243
|
}
|
|
241
244
|
}
|
|
@@ -249,7 +252,58 @@ export async function buildExistingMilestonesContext(basePath, milestoneIds, sta
|
|
|
249
252
|
sections.push(`### Previous Queue Entries\nSource: \`${relGsdRootFile("QUEUE")}\`\n\n${queueContent.trim()}`);
|
|
250
253
|
}
|
|
251
254
|
}
|
|
252
|
-
return sections
|
|
255
|
+
return capExistingMilestonesContext(sections);
|
|
256
|
+
}
|
|
257
|
+
function summarizeArtifactForQueue(content, sourcePath, cap = QUEUE_ARTIFACT_EXCERPT_MAX_CHARS) {
|
|
258
|
+
const trimmed = content.trim();
|
|
259
|
+
if (trimmed.length <= cap) {
|
|
260
|
+
return `Source: \`${sourcePath}\`\n\n${trimmed}`;
|
|
261
|
+
}
|
|
262
|
+
const excerpt = trimmed.slice(0, cap).trimEnd();
|
|
263
|
+
const omittedChars = trimmed.length - excerpt.length;
|
|
264
|
+
return [
|
|
265
|
+
`Source: \`${sourcePath}\``,
|
|
266
|
+
"",
|
|
267
|
+
excerpt,
|
|
268
|
+
"",
|
|
269
|
+
`[Truncated ${omittedChars} chars. Read \`${sourcePath}\` for full content.]`,
|
|
270
|
+
].join("\n");
|
|
271
|
+
}
|
|
272
|
+
function capExistingMilestonesContext(sections, cap = QUEUE_EXISTING_MILESTONES_CONTEXT_MAX_CHARS) {
|
|
273
|
+
const fullContext = sections.join(QUEUE_CONTEXT_SECTION_SEPARATOR);
|
|
274
|
+
if (fullContext.length <= cap)
|
|
275
|
+
return fullContext;
|
|
276
|
+
const notice = `[Existing milestones context truncated to ${cap} chars. Read source paths in this prompt or the corresponding .gsd artifacts for full details.]`;
|
|
277
|
+
const compactSections = sections.map(compactSectionForQueueBudget);
|
|
278
|
+
const selected = [];
|
|
279
|
+
for (let i = 0; i < sections.length; i++) {
|
|
280
|
+
const withFullSection = [
|
|
281
|
+
...selected,
|
|
282
|
+
sections[i],
|
|
283
|
+
...compactSections.slice(i + 1),
|
|
284
|
+
notice,
|
|
285
|
+
].join(QUEUE_CONTEXT_SECTION_SEPARATOR);
|
|
286
|
+
selected.push(withFullSection.length <= cap ? sections[i] : compactSections[i]);
|
|
287
|
+
}
|
|
288
|
+
const capped = [...selected, notice].join(QUEUE_CONTEXT_SECTION_SEPARATOR);
|
|
289
|
+
if (capped.length <= cap)
|
|
290
|
+
return capped;
|
|
291
|
+
return `${capped.slice(0, Math.max(0, cap - notice.length - 2)).trimEnd()}\n\n${notice}`;
|
|
292
|
+
}
|
|
293
|
+
function compactSectionForQueueBudget(section) {
|
|
294
|
+
const lines = section.split("\n");
|
|
295
|
+
const compact = [];
|
|
296
|
+
if (lines[0])
|
|
297
|
+
compact.push(lines[0]);
|
|
298
|
+
const statusLine = lines.find(line => line.startsWith("**Status:**"));
|
|
299
|
+
if (statusLine)
|
|
300
|
+
compact.push(statusLine);
|
|
301
|
+
const sourceLines = lines.filter(line => line.startsWith("Source: `"));
|
|
302
|
+
if (sourceLines.length > 0) {
|
|
303
|
+
compact.push("", "**Sources:**", ...sourceLines);
|
|
304
|
+
compact.push("", "[Artifact excerpts omitted due to total queue/rethink context budget.]");
|
|
305
|
+
}
|
|
306
|
+
return compact.join("\n");
|
|
253
307
|
}
|
|
254
308
|
// ─── Internal Helpers ───────────────────────────────────────────────────────
|
|
255
309
|
/**
|
|
@@ -1,29 +1,61 @@
|
|
|
1
1
|
// Project/App: gsd-pi
|
|
2
2
|
// File Purpose: Always-on ambient health signal rendered below the editor.
|
|
3
|
-
import {
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { runProviderChecks, runProviderChecksAsync, summariseProviderIssues } from "./doctor-providers.js";
|
|
4
5
|
import { runEnvironmentChecks, runEnvironmentChecksAsync } from "./doctor-environment.js";
|
|
5
6
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
6
|
-
import {
|
|
7
|
+
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
7
8
|
import { loadLedgerFromDisk, getProjectTotals } from "./metrics.js";
|
|
8
9
|
import { projectRoot } from "./commands/context.js";
|
|
9
10
|
import { buildHealthLines, detectHealthWidgetProjectState, } from "./health-widget-core.js";
|
|
10
11
|
export const HEALTH_WIDGET_ACTIVE_HINTS = " /gsd auto to run · /gsd status to inspect · /gsd report for snapshots · /gsd notifications for history · /gsd help";
|
|
12
|
+
const LAST_COMMIT_LOOKUP_TIMEOUT_MS = 3_000;
|
|
13
|
+
const REFRESH_INTERVAL_MS = 60_000;
|
|
14
|
+
const PROJECT_STATE_CACHE_TTL_MS = REFRESH_INTERVAL_MS;
|
|
11
15
|
// ── Data loader ────────────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
const projectStateCache = new Map();
|
|
17
|
+
export function getCachedProjectState(basePath, force) {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const cached = projectStateCache.get(basePath);
|
|
20
|
+
if (!force && cached && now - cached.computedAt <= PROJECT_STATE_CACHE_TTL_MS) {
|
|
21
|
+
return cached.state;
|
|
22
|
+
}
|
|
23
|
+
const state = detectHealthWidgetProjectState(basePath);
|
|
24
|
+
projectStateCache.set(basePath, { state, computedAt: now });
|
|
25
|
+
return state;
|
|
26
|
+
}
|
|
27
|
+
function runHealthWidgetGit(basePath, args) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const child = execFile("git", args, {
|
|
30
|
+
cwd: basePath,
|
|
31
|
+
timeout: LAST_COMMIT_LOOKUP_TIMEOUT_MS,
|
|
32
|
+
encoding: "utf-8",
|
|
33
|
+
env: GIT_NO_PROMPT_ENV,
|
|
34
|
+
}, (err, stdout) => resolve(err ? null : String(stdout).trimEnd()));
|
|
35
|
+
child.on("error", () => resolve(null));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function loadLastCommitInfoAsync(basePath) {
|
|
16
39
|
try {
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
const epoch = nativeLastCommitEpoch(basePath, branch || "HEAD");
|
|
20
|
-
if (epoch > 0) {
|
|
21
|
-
return { epoch, message: nativeCommitSubject(basePath, branch || "HEAD") || null };
|
|
22
|
-
}
|
|
40
|
+
if ((await runHealthWidgetGit(basePath, ["rev-parse", "--git-dir"])) === null) {
|
|
41
|
+
return { epoch: null, message: null };
|
|
23
42
|
}
|
|
43
|
+
const branch = await runHealthWidgetGit(basePath, ["branch", "--show-current"]);
|
|
44
|
+
const ref = branch || "HEAD";
|
|
45
|
+
const raw = await runHealthWidgetGit(basePath, ["log", "-1", "--format=%ct%x00%s", ref]);
|
|
46
|
+
if (!raw)
|
|
47
|
+
return { epoch: null, message: null };
|
|
48
|
+
const separator = raw.indexOf("\0");
|
|
49
|
+
const epochText = separator >= 0 ? raw.slice(0, separator) : raw;
|
|
50
|
+
const epoch = parseInt(epochText.trim(), 10) || 0;
|
|
51
|
+
if (epoch <= 0)
|
|
52
|
+
return { epoch: null, message: null };
|
|
53
|
+
const message = separator >= 0 ? raw.slice(separator + 1).trim() : "";
|
|
54
|
+
return { epoch, message: message || null };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { epoch: null, message: null };
|
|
24
58
|
}
|
|
25
|
-
catch { /* non-fatal */ }
|
|
26
|
-
return { epoch: null, message: null };
|
|
27
59
|
}
|
|
28
60
|
function loadHealthWidgetData(basePath, options) {
|
|
29
61
|
// `includeChecks` gates the expensive subprocess-backed checks (provider +
|
|
@@ -38,7 +70,7 @@ function loadHealthWidgetData(basePath, options) {
|
|
|
38
70
|
let environmentWarningCount = 0;
|
|
39
71
|
let lastCommitEpoch = null;
|
|
40
72
|
let lastCommitMessage = null;
|
|
41
|
-
const projectState =
|
|
73
|
+
const projectState = getCachedProjectState(basePath, options?.forceProjectState);
|
|
42
74
|
try {
|
|
43
75
|
const prefs = loadEffectiveGSDPreferences();
|
|
44
76
|
budgetCeiling = prefs?.preferences?.budget_ceiling;
|
|
@@ -66,12 +98,6 @@ function loadHealthWidgetData(basePath, options) {
|
|
|
66
98
|
}
|
|
67
99
|
catch { /* non-fatal */ }
|
|
68
100
|
}
|
|
69
|
-
// ── Last commit info ── (git spawns — gated like the other expensive checks)
|
|
70
|
-
if (includeChecks) {
|
|
71
|
-
const commit = loadLastCommitInfo(basePath);
|
|
72
|
-
lastCommitEpoch = commit.epoch;
|
|
73
|
-
lastCommitMessage = commit.message;
|
|
74
|
-
}
|
|
75
101
|
return {
|
|
76
102
|
projectState,
|
|
77
103
|
budgetCeiling,
|
|
@@ -85,17 +111,15 @@ function loadHealthWidgetData(basePath, options) {
|
|
|
85
111
|
};
|
|
86
112
|
}
|
|
87
113
|
// Non-blocking variant used by the widget's background refresh: the cheap fields
|
|
88
|
-
// come from the synchronous snapshot, then provider
|
|
89
|
-
// layered in off the event-loop critical path
|
|
90
|
-
// runEnvironmentChecksAsync). Keeps the always-on widget from stalling the UI on
|
|
91
|
-
// its initial enrichment or its 60s refresh.
|
|
114
|
+
// come from the synchronous snapshot, then provider, environment, and last-commit
|
|
115
|
+
// checks are layered in off the event-loop critical path.
|
|
92
116
|
async function loadHealthWidgetDataAsync(basePath) {
|
|
93
117
|
const data = loadHealthWidgetData(basePath, { includeChecks: false });
|
|
94
118
|
let providerIssue = data.providerIssue;
|
|
95
119
|
let environmentErrorCount = 0;
|
|
96
120
|
let environmentWarningCount = 0;
|
|
97
121
|
try {
|
|
98
|
-
providerIssue = summariseProviderIssues(
|
|
122
|
+
providerIssue = summariseProviderIssues(await runProviderChecksAsync());
|
|
99
123
|
}
|
|
100
124
|
catch { /* non-fatal */ }
|
|
101
125
|
try {
|
|
@@ -108,7 +132,7 @@ async function loadHealthWidgetDataAsync(basePath) {
|
|
|
108
132
|
}
|
|
109
133
|
}
|
|
110
134
|
catch { /* non-fatal */ }
|
|
111
|
-
const commit =
|
|
135
|
+
const commit = await loadLastCommitInfoAsync(basePath);
|
|
112
136
|
return {
|
|
113
137
|
...data,
|
|
114
138
|
providerIssue,
|
|
@@ -120,7 +144,6 @@ async function loadHealthWidgetDataAsync(basePath) {
|
|
|
120
144
|
};
|
|
121
145
|
}
|
|
122
146
|
// ── Widget init ────────────────────────────────────────────────────────────────
|
|
123
|
-
const REFRESH_INTERVAL_MS = 60_000;
|
|
124
147
|
/**
|
|
125
148
|
* Initialize the always-on gsd-health widget (belowEditor).
|
|
126
149
|
* Call once from the extension entry point after context is available.
|
|
@@ -129,13 +152,16 @@ export function initHealthWidget(ctx) {
|
|
|
129
152
|
if (!ctx.hasUI)
|
|
130
153
|
return;
|
|
131
154
|
const basePath = projectRoot();
|
|
155
|
+
// Re-init must reflect filesystem changes immediately; the TTL cache is for
|
|
156
|
+
// interval refreshes, not this one-off synchronous paint.
|
|
157
|
+
projectStateCache.delete(basePath);
|
|
132
158
|
// String-array fallback — used in RPC mode (factory is a no-op there).
|
|
133
159
|
// Skip the expensive provider/environment doctor checks here: this runs
|
|
134
160
|
// synchronously on the interactive-startup path, where running them would
|
|
135
161
|
// block first paint by ~0.9s (lsof/docker probes, otherwise run again
|
|
136
162
|
// immediately by the factory below). The factory's async refresh fills in
|
|
137
163
|
// real health once the screen is up.
|
|
138
|
-
const initialData = loadHealthWidgetData(basePath, { includeChecks: false });
|
|
164
|
+
const initialData = loadHealthWidgetData(basePath, { includeChecks: false, forceProjectState: true });
|
|
139
165
|
ctx.ui.setWidget("gsd-health", buildHealthLines(initialData), { placement: "belowEditor" });
|
|
140
166
|
// Factory-based widget for TUI mode — replaces the string-array above
|
|
141
167
|
ctx.ui.setWidget("gsd-health", (_tui, _theme) => {
|
|
@@ -12,7 +12,7 @@ import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from "node:
|
|
|
12
12
|
import { logWarning } from "./workflow-logger.js";
|
|
13
13
|
import { isClosedStatus } from "./status-guards.js";
|
|
14
14
|
import { dirname, join, relative } from "node:path";
|
|
15
|
-
import { getAllMilestones, getMilestone, getMilestoneScopedArtifacts, getSliceScopedArtifacts, getMilestoneSlices, getSliceTasks, getTask, getSlice, insertArtifact, deleteArtifactByPath, getGateResults, } from "./gsd-db.js";
|
|
15
|
+
import { getAllMilestones, getMilestone, getMilestoneScopedArtifacts, getSliceScopedArtifacts, getMilestoneSlices, getSliceTasks, getTask, getSlice, insertArtifact, deleteArtifactByPath, getGateResults, isDbAvailable, } from "./gsd-db.js";
|
|
16
16
|
import { resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, gsdProjectionRoot, gsdRoot, buildMilestoneFileName, buildTaskFileName, } from "./paths.js";
|
|
17
17
|
import { saveFile, clearParseCache, registerCacheClearCallback } from "./files.js";
|
|
18
18
|
import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
|
|
@@ -604,11 +604,15 @@ function isAutoRecoveryPlaceholderPlan(content) {
|
|
|
604
604
|
* projection (the 4S/0T-vs-5S/13T drift class). The artifacts table is an
|
|
605
605
|
* output sink, never a render input.
|
|
606
606
|
*
|
|
607
|
-
* @returns true if the plan was written, false
|
|
607
|
+
* @returns true if the plan was written, false when the DB slice has no tasks
|
|
608
|
+
* @throws when the DB connection is unavailable or the render write fails
|
|
608
609
|
*/
|
|
609
610
|
export async function renderPlanCheckboxes(basePath, milestoneId, sliceId, outputPath) {
|
|
610
611
|
const tasks = getSliceTasks(milestoneId, sliceId);
|
|
611
612
|
if (tasks.length === 0) {
|
|
613
|
+
if (!isDbAvailable()) {
|
|
614
|
+
throw new Error(`database unavailable while rendering plan checkboxes for ${milestoneId}/${sliceId}`);
|
|
615
|
+
}
|
|
612
616
|
process.stderr.write(`markdown-renderer: no tasks found for ${milestoneId}/${sliceId}\n`);
|
|
613
617
|
return false;
|
|
614
618
|
}
|