@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
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
* and blocks when the same signature appears more than MAX_CONSECUTIVE
|
|
11
11
|
* times in a row. Resets on each agent turn (session_start, agent_end)
|
|
12
12
|
* and when a different tool call breaks the streak.
|
|
13
|
+
*
|
|
14
|
+
* A second, independent check (#783 Brief C) tracks per-tool-name call
|
|
15
|
+
* counts within a turn regardless of args. This catches improvisation
|
|
16
|
+
* loops where the model attempts the same missing workflow tool through
|
|
17
|
+
* varied surfaces (bash → `node -e` → CLI), each with a different
|
|
18
|
+
* signature, so the identical-args streak never trips. Whichever guard
|
|
19
|
+
* trips first blocks.
|
|
13
20
|
*/
|
|
14
21
|
|
|
15
22
|
import { createHash } from "node:crypto";
|
|
@@ -20,11 +27,48 @@ const MAX_CONSECUTIVE_IDENTICAL_CALLS = 4;
|
|
|
20
27
|
const STRICT_LOOP_TOOLS = new Set(["ask_user_questions"]);
|
|
21
28
|
const MAX_CONSECUTIVE_STRICT = 1;
|
|
22
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Per-turn cap on calls to the SAME tool name, regardless of args (#783).
|
|
32
|
+
*
|
|
33
|
+
* General-purpose execution tools are routinely called many times per turn
|
|
34
|
+
* (touching multiple files, running several commands), so they get a higher
|
|
35
|
+
* ceiling. Everything else — workflow one-shot tools (e.g. gsd_complete_milestone)
|
|
36
|
+
* and any non-allowlisted tool — gets the default cap. The default is generous
|
|
37
|
+
* enough to absorb legitimate retries but catches the reported improvisation
|
|
38
|
+
* loop (~51 calls) well before a cost spike.
|
|
39
|
+
*/
|
|
40
|
+
const PER_TOOL_DEFAULT_CAP = 6;
|
|
41
|
+
const PER_TOOL_REPEATABLE_CAP = 15;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Inherently-repeatable tools: called many times per turn in normal work
|
|
45
|
+
* (reading/writing several files, running several commands, searching). These
|
|
46
|
+
* get PER_TOOL_REPEATABLE_CAP rather than the default. Keep this list
|
|
47
|
+
* conservative — a tool here can be invoked up to PER_TOOL_REPEATABLE_CAP times
|
|
48
|
+
* per turn before the guard blocks.
|
|
49
|
+
*/
|
|
50
|
+
const REPEATABLE_TOOLS = new Set([
|
|
51
|
+
"read",
|
|
52
|
+
"write",
|
|
53
|
+
"edit",
|
|
54
|
+
"multi_edit",
|
|
55
|
+
"bash",
|
|
56
|
+
"grep",
|
|
57
|
+
"glob",
|
|
58
|
+
"search-the-web",
|
|
59
|
+
"fetch_page",
|
|
60
|
+
"todo_write",
|
|
61
|
+
"notebook_edit",
|
|
62
|
+
]);
|
|
63
|
+
|
|
23
64
|
let consecutiveCount = 0;
|
|
24
65
|
let lastSignature = "";
|
|
25
66
|
let lastToolName = "";
|
|
26
67
|
let enabled = true;
|
|
27
68
|
|
|
69
|
+
/** Per-tool-name call counts within the current turn (#783 Brief C). */
|
|
70
|
+
const perToolCounts = new Map<string, number>();
|
|
71
|
+
|
|
28
72
|
/** Hash tool name + args into a compact signature for comparison. */
|
|
29
73
|
function hashToolCall(toolName: string, args: Record<string, unknown>): string {
|
|
30
74
|
const h = createHash("sha256");
|
|
@@ -46,6 +90,12 @@ function hashToolCall(toolName: string, args: Record<string, unknown>): string {
|
|
|
46
90
|
*
|
|
47
91
|
* Returns `{ block: false }` for allowed calls.
|
|
48
92
|
* Returns `{ block: true, reason }` when the loop threshold is exceeded.
|
|
93
|
+
*
|
|
94
|
+
* Two independent guards run; whichever trips first blocks:
|
|
95
|
+
* 1. Identical-signature streak (MAX_CONSECUTIVE_IDENTICAL_CALLS, strict for
|
|
96
|
+
* ask_user_questions).
|
|
97
|
+
* 2. Per-tool-name cap (PER_TOOL_DEFAULT_CAP / PER_TOOL_REPEATABLE_CAP),
|
|
98
|
+
* independent of args — catches improvisation loops (#783).
|
|
49
99
|
*/
|
|
50
100
|
export function checkToolCallLoop(
|
|
51
101
|
toolName: string,
|
|
@@ -63,6 +113,7 @@ export function checkToolCallLoop(
|
|
|
63
113
|
lastToolName = toolName;
|
|
64
114
|
}
|
|
65
115
|
|
|
116
|
+
// ── Guard 1: identical-signature streak ──
|
|
66
117
|
const threshold = STRICT_LOOP_TOOLS.has(toolName)
|
|
67
118
|
? MAX_CONSECUTIVE_STRICT
|
|
68
119
|
: MAX_CONSECUTIVE_IDENTICAL_CALLS;
|
|
@@ -71,13 +122,33 @@ export function checkToolCallLoop(
|
|
|
71
122
|
return {
|
|
72
123
|
block: true,
|
|
73
124
|
reason:
|
|
74
|
-
`Tool loop detected: ${toolName} called ${consecutiveCount} times ` +
|
|
125
|
+
`Tool loop detected (identical args): ${toolName} called ${consecutiveCount} times ` +
|
|
75
126
|
`with identical arguments. Blocking to prevent infinite loop. ` +
|
|
76
127
|
`Try a different approach or modify your arguments.`,
|
|
77
128
|
count: consecutiveCount,
|
|
78
129
|
};
|
|
79
130
|
}
|
|
80
131
|
|
|
132
|
+
// ── Guard 2: per-tool-name cap, independent of args (#783 Brief C) ──
|
|
133
|
+
// Catches improvisation loops where the same tool is invoked many times with
|
|
134
|
+
// varied args (e.g. retrying a missing workflow tool via bash/node -e/CLI).
|
|
135
|
+
const perToolCount = (perToolCounts.get(toolName) ?? 0) + 1;
|
|
136
|
+
perToolCounts.set(toolName, perToolCount);
|
|
137
|
+
const perToolCap = REPEATABLE_TOOLS.has(toolName)
|
|
138
|
+
? PER_TOOL_REPEATABLE_CAP
|
|
139
|
+
: PER_TOOL_DEFAULT_CAP;
|
|
140
|
+
|
|
141
|
+
if (perToolCount > perToolCap) {
|
|
142
|
+
return {
|
|
143
|
+
block: true,
|
|
144
|
+
reason:
|
|
145
|
+
`Tool loop detected (repeated tool): ${toolName} called ${perToolCount} times ` +
|
|
146
|
+
`this turn (cap ${perToolCap}). Blocking to prevent infinite loop. ` +
|
|
147
|
+
`The tool may be unavailable or failing repeatedly — try a different approach.`,
|
|
148
|
+
count: perToolCount,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
81
152
|
return { block: false, count: consecutiveCount };
|
|
82
153
|
}
|
|
83
154
|
|
|
@@ -87,6 +158,7 @@ export function resetToolCallLoopGuard(): void {
|
|
|
87
158
|
lastSignature = "";
|
|
88
159
|
lastToolName = "";
|
|
89
160
|
enabled = true;
|
|
161
|
+
perToolCounts.clear();
|
|
90
162
|
}
|
|
91
163
|
|
|
92
164
|
/** Disable the guard (e.g. during shutdown). */
|
|
@@ -95,9 +167,18 @@ export function disableToolCallLoopGuard(): void {
|
|
|
95
167
|
consecutiveCount = 0;
|
|
96
168
|
lastSignature = "";
|
|
97
169
|
lastToolName = "";
|
|
170
|
+
perToolCounts.clear();
|
|
98
171
|
}
|
|
99
172
|
|
|
100
173
|
/** Get current consecutive count for diagnostics. */
|
|
101
174
|
export function getToolCallLoopCount(): number {
|
|
102
175
|
return consecutiveCount;
|
|
103
176
|
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get the per-tool-name call count for the current turn (#783 Brief C).
|
|
180
|
+
* Returns 0 for tools not yet called. Diagnostic only.
|
|
181
|
+
*/
|
|
182
|
+
export function getToolCallCountForTool(toolName: string): number {
|
|
183
|
+
return perToolCounts.get(toolName) ?? 0;
|
|
184
|
+
}
|
|
@@ -180,7 +180,7 @@ function ensureWriteGateSnapshotDirectory(basePath: string): void {
|
|
|
180
180
|
mkdirSync(join(gsdPath, "runtime"), { recursive: true });
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
function currentWriteGateSnapshot(basePath: string = process.cwd()): WriteGateSnapshot {
|
|
183
|
+
export function currentWriteGateSnapshot(basePath: string = process.cwd()): WriteGateSnapshot {
|
|
184
184
|
const state = getWriteGateState(basePath);
|
|
185
185
|
return {
|
|
186
186
|
verifiedDepthMilestones: [...state.verifiedDepthMilestones].sort(),
|
|
@@ -11,6 +11,7 @@ import { formatPercent, formatTokenCount } from "./metrics.js";
|
|
|
11
11
|
import { countTokensSync, type TokenProvider } from "./token-counter.js";
|
|
12
12
|
import { writeContextChartHtml } from "./context-chart-html.js";
|
|
13
13
|
import { openInBrowser } from "./export.js";
|
|
14
|
+
import { truncateWithEllipsis } from "../shared/format-utils.js";
|
|
14
15
|
|
|
15
16
|
export interface ContextSectionBreakdown {
|
|
16
17
|
label: string;
|
|
@@ -34,6 +35,7 @@ export interface ContextBreakdownReport {
|
|
|
34
35
|
subagentSpawns: number;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
const REDACTED_TOOL_ARGUMENT_KEYS = new Set(["content", "oldText", "newText"]);
|
|
37
39
|
|
|
38
40
|
function resolveProvider(provider: string | undefined): TokenProvider {
|
|
39
41
|
const normalized = (provider ?? "unknown").toLowerCase();
|
|
@@ -206,6 +208,21 @@ export function parseSystemPromptSections(systemPrompt: string, provider: TokenP
|
|
|
206
208
|
return sections;
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
function redactToolCallArguments(value: unknown): unknown {
|
|
212
|
+
if (Array.isArray(value)) return value.map(redactToolCallArguments);
|
|
213
|
+
if (!value || typeof value !== "object") return value;
|
|
214
|
+
|
|
215
|
+
const safe: Record<string, unknown> = {};
|
|
216
|
+
for (const [key, child] of Object.entries(value)) {
|
|
217
|
+
if (REDACTED_TOOL_ARGUMENT_KEYS.has(key)) {
|
|
218
|
+
safe[key] = typeof child === "string" ? truncateWithEllipsis(child, 101) : "[redacted]";
|
|
219
|
+
} else {
|
|
220
|
+
safe[key] = redactToolCallArguments(child);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return safe;
|
|
224
|
+
}
|
|
225
|
+
|
|
209
226
|
function messageToText(message: SessionMessageEntry["message"]): string {
|
|
210
227
|
const role = message.role;
|
|
211
228
|
|
|
@@ -220,7 +237,7 @@ function messageToText(message: SessionMessageEntry["message"]): string {
|
|
|
220
237
|
if (typed.type === "thinking" && typed.thinking) parts.push(typed.thinking);
|
|
221
238
|
if (typed.type === "toolCall") {
|
|
222
239
|
parts.push(typed.name ?? "tool");
|
|
223
|
-
parts.push(JSON.stringify(typed.arguments ?? {}));
|
|
240
|
+
parts.push(JSON.stringify(redactToolCallArguments(typed.arguments ?? {})));
|
|
224
241
|
}
|
|
225
242
|
}
|
|
226
243
|
}
|
|
@@ -1724,17 +1724,22 @@ export function serializePreferencesToFrontmatter(prefs: Record<string, unknown>
|
|
|
1724
1724
|
const entries = Object.entries(item as Record<string, unknown>);
|
|
1725
1725
|
if (entries.length > 0) {
|
|
1726
1726
|
const [firstKey, firstVal] = entries[0];
|
|
1727
|
-
|
|
1727
|
+
if (Array.isArray(firstVal)) {
|
|
1728
|
+
lines.push(`${prefix} - ${firstKey}:`);
|
|
1729
|
+
for (const arrItem of firstVal) {
|
|
1730
|
+
lines.push(`${prefix} - ${yamlSafeString(arrItem)}`);
|
|
1731
|
+
}
|
|
1732
|
+
} else if (typeof firstVal === "object" && firstVal !== null) {
|
|
1733
|
+
lines.push(`${prefix} - ${firstKey}:`);
|
|
1734
|
+
for (const [k, v] of Object.entries(firstVal as Record<string, unknown>)) {
|
|
1735
|
+
serializeValue(k, v, indent + 3);
|
|
1736
|
+
}
|
|
1737
|
+
} else {
|
|
1738
|
+
lines.push(`${prefix} - ${firstKey}: ${yamlSafeString(firstVal)}`);
|
|
1739
|
+
}
|
|
1728
1740
|
for (let i = 1; i < entries.length; i++) {
|
|
1729
1741
|
const [k, v] = entries[i];
|
|
1730
|
-
|
|
1731
|
-
lines.push(`${prefix} ${k}:`);
|
|
1732
|
-
for (const arrItem of v) {
|
|
1733
|
-
lines.push(`${prefix} - ${yamlSafeString(arrItem)}`);
|
|
1734
|
-
}
|
|
1735
|
-
} else {
|
|
1736
|
-
lines.push(`${prefix} ${k}: ${yamlSafeString(v)}`);
|
|
1737
|
-
}
|
|
1742
|
+
serializeValue(k, v, indent + 2);
|
|
1738
1743
|
}
|
|
1739
1744
|
}
|
|
1740
1745
|
} else {
|
|
@@ -42,9 +42,9 @@ export interface WorktreeStatus {
|
|
|
42
42
|
|
|
43
43
|
// ─── Status helper ─────────────────────────────────────────────────────────
|
|
44
44
|
|
|
45
|
-
function getStatus(basePath: string, name: string, wtPath: string): WorktreeStatus {
|
|
46
|
-
const diff = diffWorktreeAll(basePath, name);
|
|
47
|
-
const numstat = diffWorktreeNumstat(basePath, name);
|
|
45
|
+
function getStatus(basePath: string, name: string, wtPath: string, mainBranch: string): WorktreeStatus {
|
|
46
|
+
const diff = diffWorktreeAll(basePath, name, undefined, mainBranch);
|
|
47
|
+
const numstat = diffWorktreeNumstat(basePath, name, undefined, mainBranch);
|
|
48
48
|
const filesChanged = diff.added.length + diff.modified.length + diff.removed.length;
|
|
49
49
|
let linesAdded = 0;
|
|
50
50
|
let linesRemoved = 0;
|
|
@@ -62,8 +62,7 @@ function getStatus(basePath: string, name: string, wtPath: string): WorktreeStat
|
|
|
62
62
|
|
|
63
63
|
let commits = 0;
|
|
64
64
|
try {
|
|
65
|
-
|
|
66
|
-
commits = nativeCommitCountBetween(basePath, main, worktreeBranchName(name));
|
|
65
|
+
commits = nativeCommitCountBetween(basePath, mainBranch, worktreeBranchName(name));
|
|
67
66
|
} catch {
|
|
68
67
|
// commit count unavailable → leave at 0
|
|
69
68
|
}
|
|
@@ -129,7 +128,8 @@ export function formatCleanKeepReason(status: WorktreeStatus): string {
|
|
|
129
128
|
async function handleList(ctx: ExtensionCommandContext): Promise<void> {
|
|
130
129
|
const basePath = projectRoot();
|
|
131
130
|
const worktrees = listWorktrees(basePath);
|
|
132
|
-
const
|
|
131
|
+
const mainBranch = worktrees.length > 0 ? nativeDetectMainBranch(basePath) : "";
|
|
132
|
+
const statuses = worktrees.map((wt) => getStatus(basePath, wt.name, wt.path, mainBranch));
|
|
133
133
|
ctx.ui.notify(formatWorktreeList(statuses), "info");
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -161,7 +161,8 @@ async function handleMerge(args: string, ctx: ExtensionCommandContext): Promise<
|
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
const
|
|
164
|
+
const mainBranch = nativeDetectMainBranch(basePath);
|
|
165
|
+
const status = getStatus(basePath, target, wt.path, mainBranch);
|
|
165
166
|
if (status.filesChanged === 0 && !status.uncommitted) {
|
|
166
167
|
try {
|
|
167
168
|
removeWorktree(basePath, target, { deleteBranch: true });
|
|
@@ -194,7 +195,6 @@ async function handleMerge(args: string, ctx: ExtensionCommandContext): Promise<
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
const commitType = inferCommitType(target);
|
|
197
|
-
const mainBranch = nativeDetectMainBranch(basePath);
|
|
198
198
|
const commitMessage = `${commitType}: merge worktree ${target}\n\nGSD-Worktree: ${target}`;
|
|
199
199
|
|
|
200
200
|
try {
|
|
@@ -250,8 +250,9 @@ async function handleClean(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
250
250
|
|
|
251
251
|
const removed: string[] = [];
|
|
252
252
|
const kept: string[] = [];
|
|
253
|
+
const mainBranch = nativeDetectMainBranch(basePath);
|
|
253
254
|
for (const wt of worktrees) {
|
|
254
|
-
const status = getStatus(basePath, wt.name, wt.path);
|
|
255
|
+
const status = getStatus(basePath, wt.name, wt.path, mainBranch);
|
|
255
256
|
if (status.filesChanged === 0 && !status.uncommitted) {
|
|
256
257
|
try {
|
|
257
258
|
removeWorktree(basePath, wt.name, { deleteBranch: true });
|
|
@@ -298,7 +299,8 @@ async function handleRemove(args: string, ctx: ExtensionCommandContext): Promise
|
|
|
298
299
|
return;
|
|
299
300
|
}
|
|
300
301
|
|
|
301
|
-
const
|
|
302
|
+
const mainBranch = nativeDetectMainBranch(basePath);
|
|
303
|
+
const status = getStatus(basePath, name, wt.path, mainBranch);
|
|
302
304
|
if ((status.filesChanged > 0 || status.uncommitted) && !force) {
|
|
303
305
|
ctx.ui.notify(
|
|
304
306
|
[
|
|
@@ -29,7 +29,7 @@ import { getWorkerBatches, hasActiveWorkers, type WorkerEntry } from "../subagen
|
|
|
29
29
|
import { formatDuration, padRight, joinColumns, centerLine, fitColumns, STATUS_GLYPH, STATUS_COLOR } from "../shared/mod.js";
|
|
30
30
|
import { estimateTimeRemaining } from "./auto-dashboard.js";
|
|
31
31
|
import { computeProgressScore, formatProgressLine } from "./progress-score.js";
|
|
32
|
-
import {
|
|
32
|
+
import { runEnvironmentChecksAsync, type EnvironmentCheckResult } from "./doctor-environment.js";
|
|
33
33
|
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
34
34
|
import { renderDialogFrame, renderKeyHints } from "./tui/render-kit.js";
|
|
35
35
|
|
|
@@ -71,6 +71,9 @@ export class GSDDashboardOverlay {
|
|
|
71
71
|
private loading = true;
|
|
72
72
|
private loadedDashboardIdentity?: string;
|
|
73
73
|
private refreshInFlight: Promise<void> | null = null;
|
|
74
|
+
private envRefreshInFlight: Promise<void> | null = null;
|
|
75
|
+
private cachedEnvBasePath?: string;
|
|
76
|
+
private cachedEnvIssues: EnvironmentCheckResult[] = [];
|
|
74
77
|
private disposed = false;
|
|
75
78
|
private resizeHandler: (() => void) | null = null;
|
|
76
79
|
private cachedMetrics: {
|
|
@@ -181,12 +184,39 @@ export class GSDDashboardOverlay {
|
|
|
181
184
|
this.loading = false;
|
|
182
185
|
}
|
|
183
186
|
|
|
187
|
+
this.scheduleEnvironmentRefresh(this.dashData.basePath || process.cwd());
|
|
188
|
+
|
|
184
189
|
if (identityChanged) {
|
|
185
190
|
this.invalidate();
|
|
186
191
|
}
|
|
187
192
|
this.tui.requestRender();
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
private scheduleEnvironmentRefresh(basePath: string): void {
|
|
196
|
+
if (this.cachedEnvBasePath !== basePath) {
|
|
197
|
+
this.cachedEnvBasePath = basePath;
|
|
198
|
+
this.cachedEnvIssues = [];
|
|
199
|
+
this.invalidate();
|
|
200
|
+
}
|
|
201
|
+
if (this.envRefreshInFlight || this.disposed) return;
|
|
202
|
+
this.envRefreshInFlight = this.refreshEnvironmentHealth(basePath)
|
|
203
|
+
.finally(() => {
|
|
204
|
+
this.envRefreshInFlight = null;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async refreshEnvironmentHealth(basePath: string): Promise<void> {
|
|
209
|
+
try {
|
|
210
|
+
const envResults = await runEnvironmentChecksAsync(basePath);
|
|
211
|
+
if (this.disposed || this.cachedEnvBasePath !== basePath) return;
|
|
212
|
+
this.cachedEnvIssues = envResults.filter(r => r.status !== "ok");
|
|
213
|
+
this.invalidate();
|
|
214
|
+
this.tui.requestRender();
|
|
215
|
+
} catch {
|
|
216
|
+
// Non-fatal — keep last known environment issues
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
190
220
|
private async loadData(): Promise<boolean> {
|
|
191
221
|
const base = this.dashData.basePath || process.cwd();
|
|
192
222
|
try {
|
|
@@ -629,8 +659,7 @@ export class GSDDashboardOverlay {
|
|
|
629
659
|
}
|
|
630
660
|
|
|
631
661
|
// Environment health section (#1221) — only show issues
|
|
632
|
-
const
|
|
633
|
-
const envIssues = envResults.filter(r => r.status !== "ok");
|
|
662
|
+
const envIssues = this.cachedEnvIssues;
|
|
634
663
|
if (envIssues.length > 0) {
|
|
635
664
|
lines.push(blank());
|
|
636
665
|
lines.push(hr());
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { rowToGate } from "../db-gate-rows.js";
|
|
29
29
|
import { rowToArtifact, rowToMilestone, type ArtifactRow, type MilestoneRow } from "../db-milestone-artifact-rows.js";
|
|
30
30
|
import { rowToSlice, rowToTask, type SliceRow, type TaskRow } from "../db-task-slice-rows.js";
|
|
31
|
+
import { TERMINAL_STATUS_SQL } from "./sql-constants.js";
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
function parseStringArrayColumn(raw: unknown): string[] {
|
|
@@ -49,6 +50,59 @@ function normalizeRepoPath(file: string): string {
|
|
|
49
50
|
return file.trim().replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
export interface HierarchyCompletionCounts {
|
|
54
|
+
milestones: number;
|
|
55
|
+
milestonesTotal: number;
|
|
56
|
+
slices: number;
|
|
57
|
+
slicesTotal: number;
|
|
58
|
+
tasks: number;
|
|
59
|
+
tasksTotal: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function numberColumn(row: Record<string, unknown> | undefined, column: string): number {
|
|
63
|
+
const value = row?.[column];
|
|
64
|
+
if (typeof value === "number") return value;
|
|
65
|
+
if (typeof value === "bigint") return Number(value);
|
|
66
|
+
if (typeof value === "string") {
|
|
67
|
+
const parsed = Number(value);
|
|
68
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getCompletionCount(table: "milestones" | "slices" | "tasks"): { completed: number; total: number } {
|
|
74
|
+
const row = getDbOrNull()!.prepare(
|
|
75
|
+
`SELECT
|
|
76
|
+
COUNT(*) AS total,
|
|
77
|
+
COALESCE(SUM(CASE WHEN status IN (${TERMINAL_STATUS_SQL}) THEN 1 ELSE 0 END), 0) AS completed
|
|
78
|
+
FROM ${table}`,
|
|
79
|
+
).get();
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
completed: numberColumn(row, "completed"),
|
|
83
|
+
total: numberColumn(row, "total"),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getHierarchyCompletionCounts(): HierarchyCompletionCounts {
|
|
88
|
+
if (!getDbOrNull()!) {
|
|
89
|
+
return { milestones: 0, milestonesTotal: 0, slices: 0, slicesTotal: 0, tasks: 0, tasksTotal: 0 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const milestones = getCompletionCount("milestones");
|
|
93
|
+
const slices = getCompletionCount("slices");
|
|
94
|
+
const tasks = getCompletionCount("tasks");
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
milestones: milestones.completed,
|
|
98
|
+
milestonesTotal: milestones.total,
|
|
99
|
+
slices: slices.completed,
|
|
100
|
+
slicesTotal: slices.total,
|
|
101
|
+
tasks: tasks.completed,
|
|
102
|
+
tasksTotal: tasks.total,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
52
106
|
export function getDecisionById(id: string): Decision | null {
|
|
53
107
|
if (!getDbOrNull()!) return null;
|
|
54
108
|
const row = getDbOrNull()!.prepare("SELECT * FROM decisions WHERE id = ?").get(id);
|
|
@@ -490,6 +544,31 @@ export function getAssessment(path: string): Record<string, unknown> | null {
|
|
|
490
544
|
return row ?? null;
|
|
491
545
|
}
|
|
492
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Look up a slice's `run-uat` assessment by (milestoneId, sliceId) identity,
|
|
549
|
+
* independent of the artifact `path`. Used as a DB fallback by the UAT
|
|
550
|
+
* closeout gate when a path migration orphans the ASSESSMENT markdown from its
|
|
551
|
+
* canonical expected path (ADR-017: DB-authoritative UAT sign-off).
|
|
552
|
+
*
|
|
553
|
+
* `status` holds the normalized verdict (`pass`/`fail`/…) written by
|
|
554
|
+
* `executeUatResultSave`; `fullContent` carries the ASSESSMENT body so callers
|
|
555
|
+
* can derive `uatType` without re-reading a file that may not exist.
|
|
556
|
+
*/
|
|
557
|
+
export function getSliceRunUatAssessment(
|
|
558
|
+
milestoneId: string,
|
|
559
|
+
sliceId: string,
|
|
560
|
+
): { status: string; fullContent: string } | null {
|
|
561
|
+
if (!getDbOrNull()!) return null;
|
|
562
|
+
const row = getDbOrNull()!.prepare(
|
|
563
|
+
`SELECT status, full_content AS fullContent FROM assessments
|
|
564
|
+
WHERE milestone_id = :mid AND slice_id = :sid AND scope = 'run-uat'
|
|
565
|
+
ORDER BY created_at DESC, ROWID DESC
|
|
566
|
+
LIMIT 1`,
|
|
567
|
+
).get({ ":mid": milestoneId, ":sid": sliceId });
|
|
568
|
+
if (!row) return null;
|
|
569
|
+
return { status: String(row["status"] ?? ""), fullContent: String(row["fullContent"] ?? "") };
|
|
570
|
+
}
|
|
571
|
+
|
|
493
572
|
export function getLatestAssessmentByScope(
|
|
494
573
|
milestoneId: string,
|
|
495
574
|
scope: string,
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { access, readFile } from "node:fs/promises";
|
|
15
16
|
import { delimiter, join } from "node:path";
|
|
16
17
|
import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
17
18
|
import { getEnvApiKey } from "@gsd/pi-ai";
|
|
@@ -166,15 +167,11 @@ const CLI_AUTH_PATH_CHECK_PROVIDERS = new Set([
|
|
|
166
167
|
"google-antigravity",
|
|
167
168
|
]);
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
* Check if a CLI provider's binary exists anywhere in PATH.
|
|
171
|
-
* Fast filesystem scan — no subprocess, no network, sub-1ms.
|
|
172
|
-
*/
|
|
173
|
-
function isCliBinaryInPath(providerId: string): boolean {
|
|
174
|
-
const binaries = CLI_BINARY_MAP[providerId];
|
|
175
|
-
if (!binaries) return false;
|
|
170
|
+
let asyncCliBinaryPathCache: Map<string, boolean> | null = null;
|
|
176
171
|
|
|
177
|
-
|
|
172
|
+
function cliExecutableNames(providerId: string): string[] {
|
|
173
|
+
const binaries = CLI_BINARY_MAP[providerId];
|
|
174
|
+
if (!binaries) return [];
|
|
178
175
|
|
|
179
176
|
// On Windows, command shims are commonly installed as .cmd/.exe/.bat/.com.
|
|
180
177
|
// Scan PATHEXT candidates in addition to the bare binary name.
|
|
@@ -196,9 +193,51 @@ function isCliBinaryInPath(providerId: string): boolean {
|
|
|
196
193
|
}
|
|
197
194
|
}
|
|
198
195
|
|
|
196
|
+
return executableNames;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if a CLI provider's binary exists anywhere in PATH.
|
|
201
|
+
* Fast filesystem scan — no subprocess, no network, sub-1ms.
|
|
202
|
+
*/
|
|
203
|
+
function isCliBinaryInPath(providerId: string): boolean {
|
|
204
|
+
const cached = asyncCliBinaryPathCache?.get(providerId);
|
|
205
|
+
if (cached !== undefined) return cached;
|
|
206
|
+
|
|
207
|
+
const executableNames = cliExecutableNames(providerId);
|
|
208
|
+
if (executableNames.length === 0) return false;
|
|
209
|
+
|
|
210
|
+
const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
211
|
+
|
|
199
212
|
return pathDirs.some(dir => executableNames.some(name => existsSync(join(dir, name))));
|
|
200
213
|
}
|
|
201
214
|
|
|
215
|
+
async function isCliBinaryInPathAsync(providerId: string): Promise<boolean> {
|
|
216
|
+
const executableNames = cliExecutableNames(providerId);
|
|
217
|
+
if (executableNames.length === 0) return false;
|
|
218
|
+
|
|
219
|
+
const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
220
|
+
const candidates = pathDirs.flatMap(dir => executableNames.map(name => join(dir, name)));
|
|
221
|
+
if (candidates.length === 0) return false;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await Promise.any(candidates.map(candidate => access(candidate)));
|
|
225
|
+
return true;
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function loadCliBinaryPathCache(): Promise<Map<string, boolean>> {
|
|
232
|
+
const entries = await Promise.all(
|
|
233
|
+
Object.keys(CLI_BINARY_MAP).map(async providerId => [
|
|
234
|
+
providerId,
|
|
235
|
+
await isCliBinaryInPathAsync(providerId),
|
|
236
|
+
] as const),
|
|
237
|
+
);
|
|
238
|
+
return new Map(entries);
|
|
239
|
+
}
|
|
240
|
+
|
|
202
241
|
function modelsJsonPaths(): string[] {
|
|
203
242
|
const home = homedir();
|
|
204
243
|
return [
|
|
@@ -208,7 +247,13 @@ function modelsJsonPaths(): string[] {
|
|
|
208
247
|
];
|
|
209
248
|
}
|
|
210
249
|
|
|
250
|
+
let asyncModelsJsonApiKeyCache: Set<string> | null = null;
|
|
251
|
+
|
|
211
252
|
function hasModelsJsonApiKey(providerId: string): boolean {
|
|
253
|
+
if (asyncModelsJsonApiKeyCache) {
|
|
254
|
+
return asyncModelsJsonApiKeyCache.has(providerId);
|
|
255
|
+
}
|
|
256
|
+
|
|
212
257
|
for (const path of modelsJsonPaths()) {
|
|
213
258
|
if (!existsSync(path)) continue;
|
|
214
259
|
try {
|
|
@@ -226,7 +271,27 @@ function hasModelsJsonApiKey(providerId: string): boolean {
|
|
|
226
271
|
return false;
|
|
227
272
|
}
|
|
228
273
|
|
|
229
|
-
function
|
|
274
|
+
async function loadModelsJsonApiKeyCache(): Promise<Set<string>> {
|
|
275
|
+
const providersWithKeys = new Set<string>();
|
|
276
|
+
for (const path of modelsJsonPaths()) {
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(await readFile(path, "utf-8")) as {
|
|
279
|
+
providers?: Record<string, { apiKey?: unknown }>;
|
|
280
|
+
};
|
|
281
|
+
for (const [providerId, provider] of Object.entries(parsed.providers ?? {})) {
|
|
282
|
+
const apiKey = provider.apiKey;
|
|
283
|
+
if (typeof apiKey === "string" && apiKey.trim().length > 0) {
|
|
284
|
+
providersWithKeys.add(providerId);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// Missing or malformed models.json should not break dashboard health checks.
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return providersWithKeys;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function resolveKeyFromAuthOrEnv(providerId: string): KeyLookup | null {
|
|
230
295
|
const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
|
|
231
296
|
|
|
232
297
|
if (providerId === "anthropic-vertex" && process.env.ANTHROPIC_VERTEX_PROJECT_ID) {
|
|
@@ -276,6 +341,13 @@ function resolveKey(providerId: string): KeyLookup {
|
|
|
276
341
|
return { found: true, source: "env", backedOff: false };
|
|
277
342
|
}
|
|
278
343
|
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function resolveKey(providerId: string): KeyLookup {
|
|
348
|
+
const direct = resolveKeyFromAuthOrEnv(providerId);
|
|
349
|
+
if (direct) return direct;
|
|
350
|
+
|
|
279
351
|
if (hasModelsJsonApiKey(providerId)) {
|
|
280
352
|
return { found: true, source: "models.json", backedOff: false };
|
|
281
353
|
}
|
|
@@ -495,6 +567,28 @@ export function runProviderChecks(): ProviderCheckResult[] {
|
|
|
495
567
|
return results;
|
|
496
568
|
}
|
|
497
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Non-blocking equivalent of `runProviderChecks` for the health-widget
|
|
572
|
+
* background refresh. PATH checks and custom models.json discovery use async
|
|
573
|
+
* filesystem APIs so periodic widget refreshes do not stall the input loop.
|
|
574
|
+
*/
|
|
575
|
+
export async function runProviderChecksAsync(): Promise<ProviderCheckResult[]> {
|
|
576
|
+
const [cliCache, modelsJsonCache] = await Promise.all([
|
|
577
|
+
loadCliBinaryPathCache(),
|
|
578
|
+
loadModelsJsonApiKeyCache(),
|
|
579
|
+
]);
|
|
580
|
+
const previousCliCache = asyncCliBinaryPathCache;
|
|
581
|
+
const previousModelsJsonCache = asyncModelsJsonApiKeyCache;
|
|
582
|
+
asyncCliBinaryPathCache = cliCache;
|
|
583
|
+
asyncModelsJsonApiKeyCache = modelsJsonCache;
|
|
584
|
+
try {
|
|
585
|
+
return runProviderChecks();
|
|
586
|
+
} finally {
|
|
587
|
+
asyncCliBinaryPathCache = previousCliCache;
|
|
588
|
+
asyncModelsJsonApiKeyCache = previousModelsJsonCache;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
498
592
|
/**
|
|
499
593
|
* Format provider check results as a human-readable report string.
|
|
500
594
|
*/
|