gsd-pi 2.78.1-dev.84a383f51 → 2.78.1-dev.9d08d820b
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/bundled-resource-path.d.ts +7 -0
- package/dist/bundled-resource-path.js +34 -2
- package/dist/claude-cli-check.js +18 -6
- package/dist/headless-query.js +21 -6
- package/dist/loader.js +2 -3
- package/dist/resource-loader.js +2 -8
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/readiness.js +19 -7
- package/dist/resources/extensions/gsd/auto/phases.js +3 -11
- package/dist/resources/extensions/gsd/auto/session.js +2 -6
- package/dist/resources/extensions/gsd/auto-dashboard.js +3 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +18 -6
- package/dist/resources/extensions/gsd/auto-prompts.js +63 -2
- package/dist/resources/extensions/gsd/auto-worktree.js +30 -13
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -1
- package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +22 -0
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +84 -2
- package/dist/resources/extensions/gsd/commands-config.js +3 -2
- package/dist/resources/extensions/gsd/commands-extensions.js +46 -3
- package/dist/resources/extensions/gsd/commands-handlers.js +3 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +6 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +2 -1
- package/dist/resources/extensions/gsd/forensics.js +8 -6
- package/dist/resources/extensions/gsd/guided-flow.js +2 -1
- package/dist/resources/extensions/gsd/home-dir.js +16 -0
- package/dist/resources/extensions/gsd/key-manager.js +2 -1
- package/dist/resources/extensions/gsd/migrate/command.js +3 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +10 -0
- package/dist/resources/extensions/gsd/prompts/refine-slice.md +10 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +29 -4
- package/dist/resources/extensions/gsd/worktree-manager.js +20 -1
- package/dist/resources/extensions/gsd/worktree-resolver.js +4 -13
- package/dist/resources/extensions/gsd/worktree-root.js +124 -0
- package/dist/resources/extensions/gsd/worktree.js +4 -115
- package/dist/resources/extensions/ollama/index.js +15 -2
- package/dist/resources/extensions/ollama/model-capabilities.js +31 -0
- package/dist/resources/extensions/ollama/ollama-client.js +40 -4
- package/dist/resources/extensions/subagent/index.js +324 -178
- 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 +9 -9
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +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/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 +9 -9
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- 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/worktree-cli.d.ts +1 -0
- package/dist/worktree-cli.js +9 -3
- package/package.json +1 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +52 -0
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/readiness.ts +20 -7
- package/src/resources/extensions/gsd/auto/phases.ts +3 -11
- package/src/resources/extensions/gsd/auto/session.ts +2 -6
- package/src/resources/extensions/gsd/auto-dashboard.ts +3 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +18 -6
- package/src/resources/extensions/gsd/auto-prompts.ts +60 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +44 -12
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -0
- package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +20 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +103 -1
- package/src/resources/extensions/gsd/commands-config.ts +3 -2
- package/src/resources/extensions/gsd/commands-extensions.ts +43 -3
- package/src/resources/extensions/gsd/commands-handlers.ts +3 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +6 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +2 -1
- package/src/resources/extensions/gsd/forensics.ts +10 -5
- package/src/resources/extensions/gsd/guided-flow.ts +2 -1
- package/src/resources/extensions/gsd/home-dir.ts +19 -0
- package/src/resources/extensions/gsd/journal.ts +4 -1
- package/src/resources/extensions/gsd/key-manager.ts +2 -1
- package/src/resources/extensions/gsd/migrate/command.ts +3 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +10 -0
- package/src/resources/extensions/gsd/prompts/refine-slice.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/commands-extensions-version-compare.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/home-dir.test.ts +52 -0
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +50 -1
- package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +18 -1
- package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +17 -1
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +38 -3
- package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +34 -33
- package/src/resources/extensions/gsd/tests/worktree.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +116 -1
- package/src/resources/extensions/gsd/unit-context-manifest.ts +36 -4
- package/src/resources/extensions/gsd/worktree-manager.ts +40 -1
- package/src/resources/extensions/gsd/worktree-resolver.ts +4 -14
- package/src/resources/extensions/gsd/worktree-root.ts +144 -0
- package/src/resources/extensions/gsd/worktree.ts +8 -119
- package/src/resources/extensions/ollama/index.ts +16 -2
- package/src/resources/extensions/ollama/model-capabilities.ts +34 -0
- package/src/resources/extensions/ollama/ollama-client.ts +41 -4
- package/src/resources/extensions/ollama/tests/model-capabilities.test.ts +96 -0
- package/src/resources/extensions/ollama/tests/ollama-client-timeout-env.test.ts +147 -0
- package/src/resources/extensions/subagent/index.ts +165 -7
- /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → -Ukk6_YxRd4GY4iUOnRUE}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → -Ukk6_YxRd4GY4iUOnRUE}/_ssgManifest.js +0 -0
|
@@ -19,8 +19,45 @@ import type {
|
|
|
19
19
|
import { parseNDJsonStream } from "./ndjson-stream.js";
|
|
20
20
|
|
|
21
21
|
const DEFAULT_HOST = "http://localhost:11434";
|
|
22
|
-
const
|
|
23
|
-
const
|
|
22
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 1500;
|
|
23
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 10000;
|
|
24
|
+
export const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse a positive integer from an environment variable, falling back to
|
|
28
|
+
* `fallback` when the var is unset, empty, non-numeric, zero, or negative.
|
|
29
|
+
*
|
|
30
|
+
* Defensive parsing: a typo like `OLLAMA_PROBE_TIMEOUT_MS=abc` or
|
|
31
|
+
* `OLLAMA_PROBE_TIMEOUT_MS=0` should not silently disable the timeout —
|
|
32
|
+
* fall back to the documented default instead.
|
|
33
|
+
*/
|
|
34
|
+
export function envPositiveInt(name: string, fallback: number): number {
|
|
35
|
+
const raw = process.env[name];
|
|
36
|
+
if (!raw) return fallback;
|
|
37
|
+
const parsed = Number.parseInt(raw, 10);
|
|
38
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
39
|
+
return Math.min(parsed, MAX_TIMER_DELAY_MS);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Effective probe timeout for the startup `isRunning()` health check.
|
|
44
|
+
* Override with `OLLAMA_PROBE_TIMEOUT_MS=<ms>` for slower networks (LAN
|
|
45
|
+
* Ollama hosts, cloud endpoints, contended cold starts).
|
|
46
|
+
*
|
|
47
|
+
* Resolved at call time — tests and downstream callers can mutate
|
|
48
|
+
* `process.env` between invocations and pick up the new value.
|
|
49
|
+
*/
|
|
50
|
+
export function getProbeTimeoutMs(): number {
|
|
51
|
+
return envPositiveInt("OLLAMA_PROBE_TIMEOUT_MS", DEFAULT_PROBE_TIMEOUT_MS);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Effective per-request timeout for REST calls. Override with
|
|
56
|
+
* `OLLAMA_REQUEST_TIMEOUT_MS=<ms>`.
|
|
57
|
+
*/
|
|
58
|
+
export function getRequestTimeoutMs(): number {
|
|
59
|
+
return envPositiveInt("OLLAMA_REQUEST_TIMEOUT_MS", DEFAULT_REQUEST_TIMEOUT_MS);
|
|
60
|
+
}
|
|
24
61
|
|
|
25
62
|
/**
|
|
26
63
|
* Get the Ollama host URL from OLLAMA_HOST or default.
|
|
@@ -57,7 +94,7 @@ function withAuth(options: RequestInit = {}): RequestInit {
|
|
|
57
94
|
};
|
|
58
95
|
}
|
|
59
96
|
|
|
60
|
-
async function fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs =
|
|
97
|
+
async function fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs = getRequestTimeoutMs()): Promise<Response> {
|
|
61
98
|
const controller = new AbortController();
|
|
62
99
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
63
100
|
try {
|
|
@@ -77,7 +114,7 @@ export async function isRunning(): Promise<boolean> {
|
|
|
77
114
|
const host = getOllamaHost();
|
|
78
115
|
const isCloud = host.includes("ollama.com") || host.includes("cloud");
|
|
79
116
|
const probeUrl = isCloud ? `${host}/api/tags` : `${host}/`;
|
|
80
|
-
const timeout = isCloud ?
|
|
117
|
+
const timeout = isCloud ? getRequestTimeoutMs() : getProbeTimeoutMs();
|
|
81
118
|
const response = await fetchWithTimeout(probeUrl, isCloud ? { method: "GET" } : {}, timeout);
|
|
82
119
|
return response.ok;
|
|
83
120
|
} catch {
|
|
@@ -84,6 +84,102 @@ describe("getModelCapabilities", () => {
|
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
// ─── Ordering / prefix-shadowing regression (#4991) ──────────────────────────
|
|
88
|
+
//
|
|
89
|
+
// The lookup is a linear scan over KNOWN_MODELS using `baseName.startsWith(pattern)`.
|
|
90
|
+
// Cloud and long-variant model names share prefixes with their base families,
|
|
91
|
+
// so the longer entries MUST appear earlier in the table — otherwise a base
|
|
92
|
+
// like `qwen3` shadows `qwen3-coder`/`qwen3-next`/`qwen3.5` and the picker
|
|
93
|
+
// reports the wrong context window. These tests pin the ordering.
|
|
94
|
+
|
|
95
|
+
describe("getModelCapabilities — long-variant overrides aren't shadowed (#4991)", () => {
|
|
96
|
+
it("qwen3-coder reports 256K, not the qwen3 131K", () => {
|
|
97
|
+
const caps = getModelCapabilities("qwen3-coder:480b");
|
|
98
|
+
assert.equal(caps.contextWindow, 262144);
|
|
99
|
+
assert.equal(caps.ollamaOptions?.num_ctx, 262144);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("qwen3-coder-next still resolves via the qwen3-coder entry", () => {
|
|
103
|
+
const caps = getModelCapabilities("qwen3-coder-next");
|
|
104
|
+
assert.equal(caps.contextWindow, 262144);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("qwen3-next:80b reports 1M, not the qwen3 131K", () => {
|
|
108
|
+
const caps = getModelCapabilities("qwen3-next:80b");
|
|
109
|
+
assert.equal(caps.contextWindow, 1048576);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("qwen3.5 / qwen3.6 cloud variants report 1M", () => {
|
|
113
|
+
assert.equal(getModelCapabilities("qwen3.5:397b").contextWindow, 1048576);
|
|
114
|
+
assert.equal(getModelCapabilities("qwen3.6:cloud").contextWindow, 1048576);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("base qwen3 still resolves to its 131K entry", () => {
|
|
118
|
+
const caps = getModelCapabilities("qwen3:8b");
|
|
119
|
+
assert.equal(caps.contextWindow, 131072);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("glm-5.1:cloud reports 200K", () => {
|
|
123
|
+
const caps = getModelCapabilities("glm-5.1:cloud");
|
|
124
|
+
assert.equal(caps.contextWindow, 204800);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("glm-4.6:cloud reports 200K", () => {
|
|
128
|
+
const caps = getModelCapabilities("glm-4.6:cloud");
|
|
129
|
+
assert.equal(caps.contextWindow, 204800);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("glm-4 base still resolves to its 131K entry", () => {
|
|
133
|
+
const caps = getModelCapabilities("glm-4:9b");
|
|
134
|
+
assert.equal(caps.contextWindow, 131072);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("kimi-k2-thinking reports 256K (not shadowed by kimi-k2)", () => {
|
|
138
|
+
const caps = getModelCapabilities("kimi-k2-thinking");
|
|
139
|
+
assert.equal(caps.contextWindow, 262144);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("kimi-k2.5:cloud and kimi-k2.6:cloud both report 256K", () => {
|
|
143
|
+
assert.equal(getModelCapabilities("kimi-k2.5:cloud").contextWindow, 262144);
|
|
144
|
+
assert.equal(getModelCapabilities("kimi-k2.6:cloud").contextWindow, 262144);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("kimi-k2 base resolves to 256K", () => {
|
|
148
|
+
const caps = getModelCapabilities("kimi-k2:cloud");
|
|
149
|
+
assert.equal(caps.contextWindow, 262144);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("minimax-m2.5:cloud and minimax-m2.7:cloud report 1M", () => {
|
|
153
|
+
assert.equal(getModelCapabilities("minimax-m2.5:cloud").contextWindow, 1048576);
|
|
154
|
+
assert.equal(getModelCapabilities("minimax-m2.7:cloud").contextWindow, 1048576);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("minimax-m2 base resolves to 1M", () => {
|
|
158
|
+
const caps = getModelCapabilities("minimax-m2:cloud");
|
|
159
|
+
assert.equal(caps.contextWindow, 1048576);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("ollamaOptions.num_ctx mirrors contextWindow for all new entries", () => {
|
|
163
|
+
// Inference time: num_ctx is what gets sent to Ollama on each chat.
|
|
164
|
+
// If contextWindow is right but num_ctx is stale, the model still
|
|
165
|
+
// gets truncated. Pin both sides.
|
|
166
|
+
for (const name of [
|
|
167
|
+
"qwen3-next:80b",
|
|
168
|
+
"qwen3-coder:480b",
|
|
169
|
+
"glm-5.1:cloud",
|
|
170
|
+
"kimi-k2-thinking",
|
|
171
|
+
"minimax-m2.7:cloud",
|
|
172
|
+
]) {
|
|
173
|
+
const caps = getModelCapabilities(name);
|
|
174
|
+
assert.equal(
|
|
175
|
+
caps.ollamaOptions?.num_ctx,
|
|
176
|
+
caps.contextWindow,
|
|
177
|
+
`${name}: num_ctx (${caps.ollamaOptions?.num_ctx}) must equal contextWindow (${caps.contextWindow})`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
87
183
|
// ─── estimateContextFromParams ───────────────────────────────────────────────
|
|
88
184
|
|
|
89
185
|
describe("estimateContextFromParams", () => {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// GSD2 — Tests for OLLAMA_PROBE_TIMEOUT_MS / OLLAMA_REQUEST_TIMEOUT_MS env vars (#5003 / #4982)
|
|
2
|
+
//
|
|
3
|
+
// Pinned defaults: 1500 ms probe, 10 000 ms request. The defaults must be
|
|
4
|
+
// preserved when the env var is unset, empty, non-numeric, zero, or negative
|
|
5
|
+
// so a typo or fat-fingered value can't silently disable the timeout. When
|
|
6
|
+
// the env var is set to a valid positive integer it overrides the default.
|
|
7
|
+
|
|
8
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import {
|
|
11
|
+
envPositiveInt,
|
|
12
|
+
getProbeTimeoutMs,
|
|
13
|
+
getRequestTimeoutMs,
|
|
14
|
+
MAX_TIMER_DELAY_MS,
|
|
15
|
+
} from "../ollama-client.js";
|
|
16
|
+
|
|
17
|
+
const PROBE_VAR = "OLLAMA_PROBE_TIMEOUT_MS";
|
|
18
|
+
const REQUEST_VAR = "OLLAMA_REQUEST_TIMEOUT_MS";
|
|
19
|
+
|
|
20
|
+
function withEnv(name: string, value: string | undefined, run: () => void): void {
|
|
21
|
+
const prior = process.env[name];
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
delete process.env[name];
|
|
24
|
+
} else {
|
|
25
|
+
process.env[name] = value;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
run();
|
|
29
|
+
} finally {
|
|
30
|
+
if (prior === undefined) {
|
|
31
|
+
delete process.env[name];
|
|
32
|
+
} else {
|
|
33
|
+
process.env[name] = prior;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("envPositiveInt — defensive fallback", () => {
|
|
39
|
+
it("returns fallback when var is unset", () => {
|
|
40
|
+
withEnv("__GSD_TEST_INT__", undefined, () => {
|
|
41
|
+
assert.equal(envPositiveInt("__GSD_TEST_INT__", 42), 42);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns fallback when var is empty string", () => {
|
|
46
|
+
withEnv("__GSD_TEST_INT__", "", () => {
|
|
47
|
+
assert.equal(envPositiveInt("__GSD_TEST_INT__", 42), 42);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns fallback when var is non-numeric", () => {
|
|
52
|
+
withEnv("__GSD_TEST_INT__", "abc", () => {
|
|
53
|
+
assert.equal(envPositiveInt("__GSD_TEST_INT__", 42), 42);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns fallback when var is zero (would silently disable timeout)", () => {
|
|
58
|
+
withEnv("__GSD_TEST_INT__", "0", () => {
|
|
59
|
+
assert.equal(envPositiveInt("__GSD_TEST_INT__", 42), 42);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns fallback when var is negative", () => {
|
|
64
|
+
withEnv("__GSD_TEST_INT__", "-100", () => {
|
|
65
|
+
assert.equal(envPositiveInt("__GSD_TEST_INT__", 42), 42);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns parsed value when var is a positive integer", () => {
|
|
70
|
+
withEnv("__GSD_TEST_INT__", "5000", () => {
|
|
71
|
+
assert.equal(envPositiveInt("__GSD_TEST_INT__", 42), 5000);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("parses leading digits and discards trailing junk (parseInt semantics)", () => {
|
|
76
|
+
withEnv("__GSD_TEST_INT__", "1500ms", () => {
|
|
77
|
+
assert.equal(envPositiveInt("__GSD_TEST_INT__", 42), 1500);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("clamps values above MAX_TIMER_DELAY_MS to prevent setTimeout overflow", () => {
|
|
82
|
+
withEnv("__GSD_TEST_INT__", String(MAX_TIMER_DELAY_MS + 1), () => {
|
|
83
|
+
assert.equal(envPositiveInt("__GSD_TEST_INT__", 42), MAX_TIMER_DELAY_MS);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("accepts MAX_TIMER_DELAY_MS exactly", () => {
|
|
88
|
+
withEnv("__GSD_TEST_INT__", String(MAX_TIMER_DELAY_MS), () => {
|
|
89
|
+
assert.equal(envPositiveInt("__GSD_TEST_INT__", 42), MAX_TIMER_DELAY_MS);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("getProbeTimeoutMs — OLLAMA_PROBE_TIMEOUT_MS override", () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
delete process.env[PROBE_VAR];
|
|
97
|
+
});
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
delete process.env[PROBE_VAR];
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("defaults to 1500 ms when unset", () => {
|
|
103
|
+
assert.equal(getProbeTimeoutMs(), 1500);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("honours a positive override", () => {
|
|
107
|
+
process.env[PROBE_VAR] = "5000";
|
|
108
|
+
assert.equal(getProbeTimeoutMs(), 5000);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("falls back to 1500 ms on a zero override (typo guard)", () => {
|
|
112
|
+
process.env[PROBE_VAR] = "0";
|
|
113
|
+
assert.equal(getProbeTimeoutMs(), 1500);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("re-reads the env var on every call", () => {
|
|
117
|
+
process.env[PROBE_VAR] = "2000";
|
|
118
|
+
assert.equal(getProbeTimeoutMs(), 2000);
|
|
119
|
+
process.env[PROBE_VAR] = "8000";
|
|
120
|
+
assert.equal(getProbeTimeoutMs(), 8000);
|
|
121
|
+
delete process.env[PROBE_VAR];
|
|
122
|
+
assert.equal(getProbeTimeoutMs(), 1500);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("getRequestTimeoutMs — OLLAMA_REQUEST_TIMEOUT_MS override", () => {
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
delete process.env[REQUEST_VAR];
|
|
129
|
+
});
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
delete process.env[REQUEST_VAR];
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("defaults to 10 000 ms when unset", () => {
|
|
135
|
+
assert.equal(getRequestTimeoutMs(), 10000);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("honours a positive override", () => {
|
|
139
|
+
process.env[REQUEST_VAR] = "30000";
|
|
140
|
+
assert.equal(getRequestTimeoutMs(), 30000);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("falls back to 10 000 ms on non-numeric input", () => {
|
|
144
|
+
process.env[REQUEST_VAR] = "thirty-seconds";
|
|
145
|
+
assert.equal(getRequestTimeoutMs(), 10000);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from "./isolation.js";
|
|
37
37
|
import { registerWorker, updateWorker } from "./worker-registry.js";
|
|
38
38
|
import { loadEffectiveGSDPreferences } from "../gsd/preferences.js";
|
|
39
|
+
import { emitJournalEvent } from "../gsd/journal.js";
|
|
39
40
|
import { CmuxClient, shellEscape } from "../cmux/index.js";
|
|
40
41
|
|
|
41
42
|
const MAX_PARALLEL_TASKS = 8;
|
|
@@ -662,11 +663,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
662
663
|
"Use chain mode to pipeline: scout finds context, planner designs, worker implements.",
|
|
663
664
|
].join(" "),
|
|
664
665
|
promptGuidelines: [
|
|
665
|
-
"
|
|
666
|
-
"
|
|
667
|
-
"
|
|
668
|
-
"
|
|
669
|
-
"
|
|
666
|
+
"Prefer subagent dispatch over inline work whenever a task is self-contained — recon, planning, review, refactor, test writing, security audit, doc writing. Each dispatch gets a fresh context window, so your main session stays focused on synthesis.",
|
|
667
|
+
"Before reading more than ~3 files to understand something, dispatch the scout agent and work from its compressed report instead.",
|
|
668
|
+
"Before any change touching ≥2 packages, the orchestration kernel, auto-mode, or a public API, dispatch the planner agent first. Plan first, then implement.",
|
|
669
|
+
"You MUST use parallel mode when ≥2 ready tasks are independent of each other's output. Do not serialize independent tasks manually — that wastes wall time and context.",
|
|
670
|
+
"Use chain mode for sequential pipelines where each step's output feeds the next: scout → planner → worker, or worker → reviewer → worker.",
|
|
671
|
+
"Before opening a PR or marking a slice complete, dispatch the reviewer agent (and security agent if the change touches auth, network, parsing, file IO, or shell exec).",
|
|
672
|
+
"Always check available agents with /subagent before choosing one — there are bundled specialists plus any project-scoped agents.",
|
|
670
673
|
],
|
|
671
674
|
parameters: SubagentParams,
|
|
672
675
|
|
|
@@ -709,6 +712,141 @@ export default function (pi: ExtensionAPI) {
|
|
|
709
712
|
};
|
|
710
713
|
}
|
|
711
714
|
|
|
715
|
+
// Dispatch telemetry — emit invoked once per dispatch and completed before each return.
|
|
716
|
+
// Fresh flowId per dispatch (subagent runs aren't currently plumbed with the parent
|
|
717
|
+
// auto-mode flowId; per-dispatch ids still let us measure frequency, batch size, mode).
|
|
718
|
+
const dispatchMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
|
|
719
|
+
const dispatchAgents = hasChain
|
|
720
|
+
? params.chain!.map((s) => s.agent)
|
|
721
|
+
: hasTasks
|
|
722
|
+
? params.tasks!.map((t) => t.agent)
|
|
723
|
+
: params.agent
|
|
724
|
+
? [params.agent]
|
|
725
|
+
: [];
|
|
726
|
+
const dispatchTasks = hasChain
|
|
727
|
+
? params.chain!.map((s) => s.task)
|
|
728
|
+
: hasTasks
|
|
729
|
+
? params.tasks!.map((t) => t.task)
|
|
730
|
+
: params.task
|
|
731
|
+
? [params.task]
|
|
732
|
+
: [];
|
|
733
|
+
const dispatchId = crypto.randomUUID();
|
|
734
|
+
const dispatchStartMs = Date.now();
|
|
735
|
+
let finalResults: SingleResult[] = [];
|
|
736
|
+
let dispatchCompletedEmitted = false;
|
|
737
|
+
|
|
738
|
+
emitJournalEvent(ctx.cwd, {
|
|
739
|
+
ts: new Date().toISOString(),
|
|
740
|
+
flowId: dispatchId,
|
|
741
|
+
seq: 0,
|
|
742
|
+
eventType: "subagent-invoked",
|
|
743
|
+
data: {
|
|
744
|
+
dispatchId,
|
|
745
|
+
mode: dispatchMode,
|
|
746
|
+
agents: dispatchAgents,
|
|
747
|
+
batchSize: dispatchAgents.length,
|
|
748
|
+
unitType: getCurrentPhase() ?? null,
|
|
749
|
+
isolated: useIsolation,
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
const zeroUsage = (): UsageStats => ({
|
|
754
|
+
input: 0,
|
|
755
|
+
output: 0,
|
|
756
|
+
cacheRead: 0,
|
|
757
|
+
cacheWrite: 0,
|
|
758
|
+
cost: 0,
|
|
759
|
+
contextTokens: 0,
|
|
760
|
+
turns: 0,
|
|
761
|
+
});
|
|
762
|
+
const errorMessageFor = (err: unknown): string =>
|
|
763
|
+
err instanceof Error ? err.message : String(err || "subagent dispatch failed");
|
|
764
|
+
const makeFailureResult = (err: unknown, agent: string, task: string, step?: number): SingleResult => {
|
|
765
|
+
const message = errorMessageFor(err);
|
|
766
|
+
return {
|
|
767
|
+
agent,
|
|
768
|
+
agentSource: "unknown",
|
|
769
|
+
task,
|
|
770
|
+
exitCode: 1,
|
|
771
|
+
messages: [],
|
|
772
|
+
stderr: message,
|
|
773
|
+
usage: zeroUsage(),
|
|
774
|
+
stopReason: signal?.aborted ? "aborted" : "error",
|
|
775
|
+
errorMessage: message,
|
|
776
|
+
...(step !== undefined ? { step } : {}),
|
|
777
|
+
};
|
|
778
|
+
};
|
|
779
|
+
const synthesizeFailureResults = (err: unknown): SingleResult[] => {
|
|
780
|
+
if (finalResults.length > 0) {
|
|
781
|
+
let patchedRunning = false;
|
|
782
|
+
const patched = finalResults.map((result) => {
|
|
783
|
+
if (result.exitCode !== -1) return result;
|
|
784
|
+
patchedRunning = true;
|
|
785
|
+
const message = errorMessageFor(err);
|
|
786
|
+
return {
|
|
787
|
+
...result,
|
|
788
|
+
exitCode: 1,
|
|
789
|
+
stderr: result.stderr || message,
|
|
790
|
+
stopReason: signal?.aborted ? "aborted" : "error",
|
|
791
|
+
errorMessage: result.errorMessage || message,
|
|
792
|
+
usage: result.usage ?? zeroUsage(),
|
|
793
|
+
};
|
|
794
|
+
});
|
|
795
|
+
if (patchedRunning || patched.some((result) => result.exitCode !== 0)) return patched;
|
|
796
|
+
|
|
797
|
+
const nextIndex = finalResults.length < dispatchAgents.length ? finalResults.length : 0;
|
|
798
|
+
if (nextIndex > 0) {
|
|
799
|
+
return [
|
|
800
|
+
...finalResults,
|
|
801
|
+
makeFailureResult(
|
|
802
|
+
err,
|
|
803
|
+
dispatchAgents[nextIndex] ?? "unknown",
|
|
804
|
+
dispatchTasks[nextIndex] ?? "",
|
|
805
|
+
dispatchMode === "chain" ? nextIndex + 1 : undefined,
|
|
806
|
+
),
|
|
807
|
+
];
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const agentsForFailure = dispatchAgents.length > 0 ? dispatchAgents : ["unknown"];
|
|
812
|
+
return agentsForFailure.map((agent, index) =>
|
|
813
|
+
makeFailureResult(
|
|
814
|
+
err,
|
|
815
|
+
agent,
|
|
816
|
+
dispatchTasks[index] ?? "",
|
|
817
|
+
dispatchMode === "chain" ? index + 1 : undefined,
|
|
818
|
+
),
|
|
819
|
+
);
|
|
820
|
+
};
|
|
821
|
+
const finishDispatch = (results: SingleResult[]): void => {
|
|
822
|
+
if (dispatchCompletedEmitted) return;
|
|
823
|
+
finalResults = results;
|
|
824
|
+
dispatchCompletedEmitted = true;
|
|
825
|
+
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
826
|
+
const failureCount = results.filter((r) => r.exitCode !== 0).length;
|
|
827
|
+
const totalCost = results.reduce((s, r) => s + (r.usage?.cost ?? 0), 0);
|
|
828
|
+
const totalInputTokens = results.reduce((s, r) => s + (r.usage?.input ?? 0), 0);
|
|
829
|
+
const totalOutputTokens = results.reduce((s, r) => s + (r.usage?.output ?? 0), 0);
|
|
830
|
+
emitJournalEvent(ctx.cwd, {
|
|
831
|
+
ts: new Date().toISOString(),
|
|
832
|
+
flowId: dispatchId,
|
|
833
|
+
seq: 1,
|
|
834
|
+
eventType: "subagent-completed",
|
|
835
|
+
data: {
|
|
836
|
+
dispatchId,
|
|
837
|
+
mode: dispatchMode,
|
|
838
|
+
agents: dispatchAgents,
|
|
839
|
+
successCount,
|
|
840
|
+
failureCount,
|
|
841
|
+
totalCost,
|
|
842
|
+
totalInputTokens,
|
|
843
|
+
totalOutputTokens,
|
|
844
|
+
wallTimeMs: Date.now() - dispatchStartMs,
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
try {
|
|
712
850
|
if ((agentScope === "project" || agentScope === "both") && confirmProjectAgents && ctx.hasUI) {
|
|
713
851
|
const requestedAgentNames = new Set<string>();
|
|
714
852
|
if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
|
|
@@ -726,16 +864,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
726
864
|
"Run project-local agents?",
|
|
727
865
|
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
728
866
|
);
|
|
729
|
-
if (!ok)
|
|
867
|
+
if (!ok) {
|
|
868
|
+
finishDispatch([]);
|
|
730
869
|
return {
|
|
731
870
|
content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
|
|
732
871
|
details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
|
|
733
872
|
};
|
|
873
|
+
}
|
|
734
874
|
}
|
|
735
875
|
}
|
|
736
876
|
|
|
737
877
|
if (params.chain && params.chain.length > 0) {
|
|
738
878
|
const results: SingleResult[] = [];
|
|
879
|
+
finalResults = results;
|
|
739
880
|
let previousOutput = "";
|
|
740
881
|
|
|
741
882
|
for (let i = 0; i < params.chain.length; i++) {
|
|
@@ -775,6 +916,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
775
916
|
if (isError) {
|
|
776
917
|
const errorMsg =
|
|
777
918
|
result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
919
|
+
finishDispatch(results);
|
|
778
920
|
return {
|
|
779
921
|
content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
|
780
922
|
details: makeDetails("chain")(results),
|
|
@@ -783,6 +925,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
783
925
|
}
|
|
784
926
|
previousOutput = getFinalOutput(result.messages);
|
|
785
927
|
}
|
|
928
|
+
finishDispatch(results);
|
|
786
929
|
return {
|
|
787
930
|
content: [{ type: "text", text: getFinalOutput(results[results.length - 1].messages) || "(no output)" }],
|
|
788
931
|
details: makeDetails("chain")(results),
|
|
@@ -790,7 +933,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
790
933
|
}
|
|
791
934
|
|
|
792
935
|
if (params.tasks && params.tasks.length > 0) {
|
|
793
|
-
if (params.tasks.length > MAX_PARALLEL_TASKS)
|
|
936
|
+
if (params.tasks.length > MAX_PARALLEL_TASKS) {
|
|
937
|
+
finishDispatch([]);
|
|
794
938
|
return {
|
|
795
939
|
content: [
|
|
796
940
|
{
|
|
@@ -800,6 +944,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
800
944
|
],
|
|
801
945
|
details: makeDetails("parallel")([]),
|
|
802
946
|
};
|
|
947
|
+
}
|
|
803
948
|
|
|
804
949
|
// Track all results for streaming updates
|
|
805
950
|
const allResults: SingleResult[] = new Array(params.tasks.length);
|
|
@@ -816,6 +961,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
816
961
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
817
962
|
};
|
|
818
963
|
}
|
|
964
|
+
finalResults = allResults;
|
|
819
965
|
|
|
820
966
|
const emitParallelUpdate = () => {
|
|
821
967
|
if (onUpdate) {
|
|
@@ -887,6 +1033,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
887
1033
|
emitParallelUpdate();
|
|
888
1034
|
return result;
|
|
889
1035
|
});
|
|
1036
|
+
finalResults = results;
|
|
890
1037
|
|
|
891
1038
|
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
892
1039
|
const summaries = results.map((r) => {
|
|
@@ -896,6 +1043,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
896
1043
|
: getFinalOutput(r.messages);
|
|
897
1044
|
return `[${r.agent}] ${r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`}: ${output || "(no output)"}`;
|
|
898
1045
|
});
|
|
1046
|
+
finishDispatch(results);
|
|
899
1047
|
return {
|
|
900
1048
|
content: [
|
|
901
1049
|
{
|
|
@@ -943,6 +1091,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
943
1091
|
onUpdate,
|
|
944
1092
|
makeDetails("single"),
|
|
945
1093
|
);
|
|
1094
|
+
finalResults = [result];
|
|
946
1095
|
|
|
947
1096
|
// Capture and merge delta if isolated
|
|
948
1097
|
if (isolation) {
|
|
@@ -956,6 +1105,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
956
1105
|
if (isError) {
|
|
957
1106
|
const errorMsg =
|
|
958
1107
|
result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
1108
|
+
finishDispatch([result]);
|
|
959
1109
|
return {
|
|
960
1110
|
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
|
961
1111
|
details: makeDetails("single")([result]),
|
|
@@ -967,6 +1117,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
967
1117
|
if (mergeResult && !mergeResult.success) {
|
|
968
1118
|
outputText += `\n\n⚠ Patch merge failed: ${mergeResult.error || "unknown error"}`;
|
|
969
1119
|
}
|
|
1120
|
+
finishDispatch([result]);
|
|
970
1121
|
return {
|
|
971
1122
|
content: [{ type: "text", text: outputText }],
|
|
972
1123
|
details: makeDetails("single")([result]),
|
|
@@ -978,11 +1129,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
978
1129
|
}
|
|
979
1130
|
}
|
|
980
1131
|
|
|
1132
|
+
finishDispatch([]);
|
|
981
1133
|
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
982
1134
|
return {
|
|
983
1135
|
content: [{ type: "text", text: `Invalid parameters. Available agents: ${available}` }],
|
|
984
1136
|
details: makeDetails("single")([]),
|
|
985
1137
|
};
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
if (!dispatchCompletedEmitted) finalResults = synthesizeFailureResults(err);
|
|
1140
|
+
throw err;
|
|
1141
|
+
} finally {
|
|
1142
|
+
finishDispatch(finalResults);
|
|
1143
|
+
}
|
|
986
1144
|
},
|
|
987
1145
|
|
|
988
1146
|
renderCall(args, theme) {
|
|
File without changes
|
|
File without changes
|