gsd-pi 2.62.0-dev.f6ad485 → 2.62.1-dev.1ae2b74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/ask-user-questions.js +47 -3
- package/dist/resources/extensions/gsd/auto/loop.js +8 -1
- package/dist/resources/extensions/gsd/auto/phases.js +10 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +6 -4
- package/dist/resources/extensions/gsd/auto-start.js +11 -6
- package/dist/resources/extensions/gsd/auto-timers.js +8 -2
- package/dist/resources/extensions/gsd/auto-verification.js +14 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +19 -0
- package/dist/resources/extensions/gsd/auto.js +24 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +11 -1
- package/dist/resources/extensions/gsd/db-writer.js +64 -28
- package/dist/resources/extensions/gsd/preferences-models.js +74 -0
- package/dist/resources/extensions/gsd/preferences-skills.js +6 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/skill-catalog.js +6 -4
- package/dist/resources/extensions/gsd/skill-discovery.js +24 -6
- package/dist/resources/extensions/gsd/skill-health.js +7 -3
- package/dist/resources/extensions/gsd/skill-telemetry.js +5 -2
- package/dist/resources/extensions/gsd/state.js +1 -0
- package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -3
- package/dist/resources/extensions/gsd/workflow-logger.js +13 -8
- package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +20 -20
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/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 +20 -20
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/src/cli.ts +1 -1
- package/packages/mcp-server/src/index.ts +15 -1
- package/packages/mcp-server/src/readers/captures.ts +119 -0
- package/packages/mcp-server/src/readers/doctor-lite.ts +225 -0
- package/packages/mcp-server/src/readers/index.ts +16 -0
- package/packages/mcp-server/src/readers/knowledge.ts +111 -0
- package/packages/mcp-server/src/readers/metrics.ts +118 -0
- package/packages/mcp-server/src/readers/paths.ts +217 -0
- package/packages/mcp-server/src/readers/readers.test.ts +509 -0
- package/packages/mcp-server/src/readers/roadmap.ts +263 -0
- package/packages/mcp-server/src/readers/state.ts +223 -0
- package/packages/mcp-server/src/server.ts +134 -3
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts +26 -6
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.js +67 -9
- package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +73 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
- package/packages/pi-ai/src/utils/repair-tool-json.ts +74 -10
- package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +94 -1
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +16 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +4 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +48 -16
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +20 -3
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +21 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +30 -3
- package/packages/pi-coding-agent/src/core/retry-handler.ts +49 -16
- package/pkg/package.json +1 -1
- package/src/resources/extensions/ask-user-questions.ts +60 -4
- package/src/resources/extensions/gsd/auto/loop.ts +8 -1
- package/src/resources/extensions/gsd/auto/phases.ts +8 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +6 -3
- package/src/resources/extensions/gsd/auto-start.ts +11 -6
- package/src/resources/extensions/gsd/auto-timers.ts +8 -2
- package/src/resources/extensions/gsd/auto-verification.ts +14 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +18 -0
- package/src/resources/extensions/gsd/auto.ts +25 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +13 -1
- package/src/resources/extensions/gsd/db-writer.ts +67 -30
- package/src/resources/extensions/gsd/preferences-models.ts +78 -0
- package/src/resources/extensions/gsd/preferences-skills.ts +6 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/skill-catalog.ts +6 -3
- package/src/resources/extensions/gsd/skill-discovery.ts +23 -6
- package/src/resources/extensions/gsd/skill-health.ts +7 -3
- package/src/resources/extensions/gsd/skill-telemetry.ts +5 -2
- package/src/resources/extensions/gsd/state.ts +1 -0
- package/src/resources/extensions/gsd/tests/ask-user-questions-dedup.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +22 -2
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +41 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +75 -1
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +17 -4
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +17 -41
- package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +81 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -5
- package/src/resources/extensions/gsd/workflow-logger.ts +13 -8
- package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -1
- package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +6 -1
- /package/dist/web/standalone/.next/static/{fbkSIi4k8fmB8mi0Sq9sF → erQZ_8_1lkclnPJLJnCxG}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{fbkSIi4k8fmB8mi0Sq9sF → erQZ_8_1lkclnPJLJnCxG}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Claude Code skill directory support in getSkillSearchDirs().
|
|
3
|
+
*
|
|
4
|
+
* Verifies that ~/.claude/skills/ and .claude/skills/ are included in
|
|
5
|
+
* the skill search path alongside ~/.agents/skills/ and .agents/skills/.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { getSkillSearchDirs } from "../preferences-skills.ts";
|
|
13
|
+
|
|
14
|
+
describe("getSkillSearchDirs — Claude Code directory support", () => {
|
|
15
|
+
const cwd = "/tmp/test-project";
|
|
16
|
+
|
|
17
|
+
test("includes ~/.agents/skills/ as user-skill", () => {
|
|
18
|
+
const dirs = getSkillSearchDirs(cwd);
|
|
19
|
+
const agents = dirs.find((d) => d.dir === join(homedir(), ".agents", "skills"));
|
|
20
|
+
assert.ok(agents, "should include ~/.agents/skills/");
|
|
21
|
+
assert.equal(agents!.method, "user-skill");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("includes .agents/skills/ as project-skill", () => {
|
|
25
|
+
const dirs = getSkillSearchDirs(cwd);
|
|
26
|
+
const projectAgents = dirs.find((d) => d.dir === join(cwd, ".agents", "skills"));
|
|
27
|
+
assert.ok(projectAgents, "should include .agents/skills/");
|
|
28
|
+
assert.equal(projectAgents!.method, "project-skill");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("includes ~/.claude/skills/ as user-skill", () => {
|
|
32
|
+
const dirs = getSkillSearchDirs(cwd);
|
|
33
|
+
const claude = dirs.find((d) => d.dir === join(homedir(), ".claude", "skills"));
|
|
34
|
+
assert.ok(claude, "should include ~/.claude/skills/");
|
|
35
|
+
assert.equal(claude!.method, "user-skill");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("includes .claude/skills/ as project-skill", () => {
|
|
39
|
+
const dirs = getSkillSearchDirs(cwd);
|
|
40
|
+
const projectClaude = dirs.find((d) => d.dir === join(cwd, ".claude", "skills"));
|
|
41
|
+
assert.ok(projectClaude, "should include .claude/skills/");
|
|
42
|
+
assert.equal(projectClaude!.method, "project-skill");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("~/.agents/skills/ appears before ~/.claude/skills/ (priority order)", () => {
|
|
46
|
+
const dirs = getSkillSearchDirs(cwd);
|
|
47
|
+
const agentsIdx = dirs.findIndex((d) => d.dir === join(homedir(), ".agents", "skills"));
|
|
48
|
+
const claudeIdx = dirs.findIndex((d) => d.dir === join(homedir(), ".claude", "skills"));
|
|
49
|
+
assert.ok(agentsIdx < claudeIdx, "~/.agents/skills/ should have higher priority than ~/.claude/skills/");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -358,6 +358,47 @@ describe('db-writer', () => {
|
|
|
358
358
|
}
|
|
359
359
|
});
|
|
360
360
|
|
|
361
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
362
|
+
// Parallel save race condition regression (#3326, #3339, #3459)
|
|
363
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
364
|
+
|
|
365
|
+
test('parallel saveDecisionToDb calls produce unique IDs', async () => {
|
|
366
|
+
const tmpDir = makeTmpDir();
|
|
367
|
+
const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
|
|
368
|
+
openDatabase(dbPath);
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
// Fire 5 saves concurrently — before the fix, all would get D001
|
|
372
|
+
const results = await Promise.all([
|
|
373
|
+
saveDecisionToDb({ scope: 'a', decision: 'd1', choice: 'c1', rationale: 'r1' }, tmpDir),
|
|
374
|
+
saveDecisionToDb({ scope: 'b', decision: 'd2', choice: 'c2', rationale: 'r2' }, tmpDir),
|
|
375
|
+
saveDecisionToDb({ scope: 'c', decision: 'd3', choice: 'c3', rationale: 'r3' }, tmpDir),
|
|
376
|
+
saveDecisionToDb({ scope: 'd', decision: 'd4', choice: 'c4', rationale: 'r4' }, tmpDir),
|
|
377
|
+
saveDecisionToDb({ scope: 'e', decision: 'd5', choice: 'c5', rationale: 'r5' }, tmpDir),
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
const ids = results.map((r) => r.id);
|
|
381
|
+
const uniqueIds = new Set(ids);
|
|
382
|
+
|
|
383
|
+
// All 5 IDs must be unique
|
|
384
|
+
assert.equal(uniqueIds.size, 5, `Expected 5 unique IDs, got ${uniqueIds.size}: ${ids.join(', ')}`);
|
|
385
|
+
|
|
386
|
+
// IDs should be D001-D005 (order may vary due to concurrency)
|
|
387
|
+
for (const id of ids) {
|
|
388
|
+
assert.match(id, /^D\d{3}$/, `ID ${id} should match D### pattern`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Verify all 5 exist in DB
|
|
392
|
+
for (const id of ids) {
|
|
393
|
+
const row = getDecisionById(id);
|
|
394
|
+
assert.ok(row, `Decision ${id} should exist in DB`);
|
|
395
|
+
}
|
|
396
|
+
} finally {
|
|
397
|
+
closeDatabase();
|
|
398
|
+
cleanupDir(tmpDir);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
361
402
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
362
403
|
// updateRequirementInDb Tests
|
|
363
404
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for model config isolation between concurrent instances (#650, #1065)
|
|
2
|
+
* Tests for model config isolation between concurrent instances (#650, #1065)
|
|
3
|
+
* and GSD preferences override of settings.json defaults (#3517).
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
@@ -155,3 +156,76 @@ describe("session model recovery on error (#1065)", () => {
|
|
|
155
156
|
"Recovery should be skipped when no session model was captured");
|
|
156
157
|
});
|
|
157
158
|
});
|
|
159
|
+
|
|
160
|
+
// ─── GSD Preferences override settings.json (#3517) ─────────────────────────
|
|
161
|
+
|
|
162
|
+
describe("GSD preferences override settings.json for session model (#3517)", () => {
|
|
163
|
+
it("preferredModel takes priority over ctx.model when both are available", () => {
|
|
164
|
+
// Simulates auto-start.ts logic: preferredModel ?? ctx.model snapshot
|
|
165
|
+
const preferredModel = { provider: "openai-codex", id: "gpt-5.4" };
|
|
166
|
+
const ctxModel = { provider: "claude-code", id: "claude-sonnet-4-6" };
|
|
167
|
+
|
|
168
|
+
const startModelSnapshot = preferredModel
|
|
169
|
+
?? { provider: ctxModel.provider, id: ctxModel.id };
|
|
170
|
+
|
|
171
|
+
assert.equal(startModelSnapshot.provider, "openai-codex",
|
|
172
|
+
"preferredModel provider should win over ctx.model");
|
|
173
|
+
assert.equal(startModelSnapshot.id, "gpt-5.4",
|
|
174
|
+
"preferredModel id should win over ctx.model");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("falls back to ctx.model when no GSD preferences are configured", () => {
|
|
178
|
+
const preferredModel: { provider: string; id: string } | undefined = undefined;
|
|
179
|
+
const ctxModel = { provider: "claude-code", id: "claude-sonnet-4-6" };
|
|
180
|
+
|
|
181
|
+
const startModelSnapshot = preferredModel
|
|
182
|
+
?? { provider: ctxModel.provider, id: ctxModel.id };
|
|
183
|
+
|
|
184
|
+
assert.equal(startModelSnapshot.provider, "claude-code",
|
|
185
|
+
"should fall back to ctx.model provider when no preferences");
|
|
186
|
+
assert.equal(startModelSnapshot.id, "claude-sonnet-4-6",
|
|
187
|
+
"should fall back to ctx.model id when no preferences");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("handles null ctx.model with no preferences gracefully", () => {
|
|
191
|
+
const preferredModel: { provider: string; id: string } | undefined = undefined;
|
|
192
|
+
// Use a function to prevent TS from narrowing to `never` in the ternary
|
|
193
|
+
function getCtxModel(): { provider: string; id: string } | null { return null; }
|
|
194
|
+
const ctxModel = getCtxModel();
|
|
195
|
+
|
|
196
|
+
const startModelSnapshot = preferredModel
|
|
197
|
+
?? (ctxModel ? { provider: ctxModel.provider, id: ctxModel.id } : null);
|
|
198
|
+
|
|
199
|
+
assert.equal(startModelSnapshot, null,
|
|
200
|
+
"should be null when neither preferences nor ctx.model exist");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("bare model ID uses session provider when available", () => {
|
|
204
|
+
// Simulates: PREFERENCES.md has "gpt-5.4" (no provider), session is openai-codex
|
|
205
|
+
const preferredModel = { provider: "openai-codex", id: "gpt-5.4" }; // from resolveDefaultSessionModel("openai-codex")
|
|
206
|
+
const ctxModel = { provider: "openai-codex", id: "claude-sonnet-4-6" };
|
|
207
|
+
|
|
208
|
+
const startModelSnapshot = preferredModel
|
|
209
|
+
?? { provider: ctxModel.provider, id: ctxModel.id };
|
|
210
|
+
|
|
211
|
+
assert.equal(startModelSnapshot.provider, "openai-codex");
|
|
212
|
+
assert.equal(startModelSnapshot.id, "gpt-5.4",
|
|
213
|
+
"bare model ID from preferences should still override ctx.model");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("stale settings.json does not leak when preferences are set", () => {
|
|
217
|
+
// Scenario: settings.json has claude-code, PREFERENCES.md has openai-codex
|
|
218
|
+
const settingsJsonDefault = { provider: "claude-code", id: "claude-sonnet-4-6" };
|
|
219
|
+
const preferencesModel = { provider: "openai-codex", id: "gpt-5.4" };
|
|
220
|
+
|
|
221
|
+
// auto-start.ts captures preferredModel first, which preempts settingsJsonDefault
|
|
222
|
+
const startModelSnapshot = preferencesModel ?? settingsJsonDefault;
|
|
223
|
+
|
|
224
|
+
assert.equal(startModelSnapshot.provider, "openai-codex",
|
|
225
|
+
"PREFERENCES.md must override stale settings.json provider");
|
|
226
|
+
assert.equal(startModelSnapshot.id, "gpt-5.4",
|
|
227
|
+
"PREFERENCES.md must override stale settings.json model");
|
|
228
|
+
assert.notEqual(startModelSnapshot.provider, settingsJsonDefault.provider,
|
|
229
|
+
"settings.json provider must NOT leak through");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -136,17 +136,30 @@ console.log('\n── Loop guard: nested args are not stripped ──');
|
|
|
136
136
|
assert.deepStrictEqual(getToolCallLoopCount(), 1, `Each unique nested call should reset count to 1`);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Truly identical nested calls should still be detected
|
|
139
|
+
// Truly identical nested calls should still be detected.
|
|
140
|
+
// ask_user_questions has a strict threshold of 1, so the 2nd identical call is blocked.
|
|
141
|
+
resetToolCallLoopGuard();
|
|
142
|
+
const first = checkToolCallLoop('ask_user_questions', {
|
|
143
|
+
questions: [{ id: 'same', question: 'Same?' }],
|
|
144
|
+
});
|
|
145
|
+
assert.ok(first.block === false, 'First ask_user_questions call should be allowed');
|
|
146
|
+
const blocked = checkToolCallLoop('ask_user_questions', {
|
|
147
|
+
questions: [{ id: 'same', question: 'Same?' }],
|
|
148
|
+
});
|
|
149
|
+
assert.ok(blocked.block === true, '2nd identical ask_user_questions call should be blocked (strict threshold)');
|
|
150
|
+
|
|
151
|
+
// Non-strict tools still allow up to 4 identical calls
|
|
140
152
|
resetToolCallLoopGuard();
|
|
141
153
|
for (let i = 1; i <= 4; i++) {
|
|
142
|
-
checkToolCallLoop('
|
|
154
|
+
const r = checkToolCallLoop('web_search', {
|
|
143
155
|
questions: [{ id: 'same', question: 'Same?' }],
|
|
144
156
|
});
|
|
157
|
+
assert.ok(r.block === false, `web_search call ${i} should be allowed (normal threshold)`);
|
|
145
158
|
}
|
|
146
|
-
const
|
|
159
|
+
const blockedNormal = checkToolCallLoop('web_search', {
|
|
147
160
|
questions: [{ id: 'same', question: 'Same?' }],
|
|
148
161
|
});
|
|
149
|
-
assert.ok(
|
|
162
|
+
assert.ok(blockedNormal.block === true, '5th identical web_search call should be blocked');
|
|
150
163
|
}
|
|
151
164
|
|
|
152
165
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -217,12 +217,26 @@ describe("workflow-logger", () => {
|
|
|
217
217
|
assert.ok(formatted.includes("\n"));
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
-
test("
|
|
220
|
+
test("includes context fields in formatted output", () => {
|
|
221
221
|
logError("tool", "failed", { cmd: "complete_task" });
|
|
222
222
|
const entries = drainLogs();
|
|
223
223
|
const formatted = formatForNotification(entries);
|
|
224
|
-
assert.equal(formatted, "[tool] failed");
|
|
225
|
-
|
|
224
|
+
assert.equal(formatted, "[tool] failed (cmd: complete_task)");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("excludes error key from context to avoid redundancy", () => {
|
|
228
|
+
logError("tool", "disk write failed", { error: "ENOSPC", path: "/tmp/foo" });
|
|
229
|
+
const entries = drainLogs();
|
|
230
|
+
const formatted = formatForNotification(entries);
|
|
231
|
+
assert.ok(formatted.includes("path: /tmp/foo"));
|
|
232
|
+
assert.ok(!formatted.includes("error: ENOSPC"));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("formats entry without context unchanged", () => {
|
|
236
|
+
logError("intercept", "blocked write");
|
|
237
|
+
const entries = drainLogs();
|
|
238
|
+
const formatted = formatForNotification(entries);
|
|
239
|
+
assert.equal(formatted, "[intercept] blocked write");
|
|
226
240
|
});
|
|
227
241
|
});
|
|
228
242
|
|
|
@@ -279,44 +293,6 @@ describe("workflow-logger", () => {
|
|
|
279
293
|
});
|
|
280
294
|
});
|
|
281
295
|
|
|
282
|
-
describe("audit log persistence", () => {
|
|
283
|
-
let dir: string;
|
|
284
|
-
|
|
285
|
-
beforeEach(() => {
|
|
286
|
-
dir = makeTempDir("wl-audit-");
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
afterEach(() => {
|
|
290
|
-
setLogBasePath("");
|
|
291
|
-
cleanup(dir);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("writes entry to .gsd/audit-log.jsonl after setLogBasePath", () => {
|
|
295
|
-
setLogBasePath(dir);
|
|
296
|
-
logError("engine", "audit test entry");
|
|
297
|
-
|
|
298
|
-
const auditPath = join(dir, ".gsd", "audit-log.jsonl");
|
|
299
|
-
assert.ok(existsSync(auditPath), "audit-log.jsonl should exist");
|
|
300
|
-
const content = readFileSync(auditPath, "utf-8");
|
|
301
|
-
const entry = JSON.parse(content.trim());
|
|
302
|
-
assert.equal(entry.severity, "error");
|
|
303
|
-
assert.equal(entry.component, "engine");
|
|
304
|
-
assert.equal(entry.message, "audit test entry");
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test("_resetLogs does not clear the audit base path", () => {
|
|
308
|
-
setLogBasePath(dir);
|
|
309
|
-
_resetLogs();
|
|
310
|
-
logError("engine", "post-reset entry");
|
|
311
|
-
|
|
312
|
-
const auditPath = join(dir, ".gsd", "audit-log.jsonl");
|
|
313
|
-
assert.ok(existsSync(auditPath), "audit-log.jsonl should exist after _resetLogs");
|
|
314
|
-
const content = readFileSync(auditPath, "utf-8");
|
|
315
|
-
const entry = JSON.parse(content.trim());
|
|
316
|
-
assert.equal(entry.message, "post-reset entry");
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
|
|
320
296
|
describe("new log components (db, dispatch)", () => {
|
|
321
297
|
test("logError with 'db' component stores correct component", () => {
|
|
322
298
|
logError("db", "failed to copy DB to worktree", { error: "ENOENT" });
|
|
@@ -100,8 +100,87 @@ describe('worktree-db-respawn-truncation (#2815)', async () => {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// ─── 3.
|
|
104
|
-
console.log('\n=== 3.
|
|
103
|
+
// ─── 3. WAL/SHM sidecar files cleaned up when empty DB is deleted (#2478) ──
|
|
104
|
+
console.log('\n=== 3. orphaned WAL/SHM cleaned up alongside empty gsd.db (#2478) ===');
|
|
105
|
+
{
|
|
106
|
+
const mainBase = createBase('main');
|
|
107
|
+
const wtBase = createBase('wt');
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
|
|
111
|
+
mkdirSync(m001Dir, { recursive: true });
|
|
112
|
+
writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
|
|
113
|
+
|
|
114
|
+
// Create an empty (0-byte) gsd.db plus orphaned WAL and SHM files —
|
|
115
|
+
// this is the exact state that causes Node 24 node:sqlite CPU spin (#2478).
|
|
116
|
+
const wtGsd = join(wtBase, '.gsd');
|
|
117
|
+
writeFileSync(join(wtGsd, 'gsd.db'), '');
|
|
118
|
+
writeFileSync(join(wtGsd, 'gsd.db-wal'), Buffer.alloc(605672, 0xAA));
|
|
119
|
+
writeFileSync(join(wtGsd, 'gsd.db-shm'), Buffer.alloc(32768, 0xBB));
|
|
120
|
+
|
|
121
|
+
assert.ok(existsSync(join(wtGsd, 'gsd.db')), 'gsd.db exists before sync');
|
|
122
|
+
assert.ok(existsSync(join(wtGsd, 'gsd.db-wal')), 'gsd.db-wal exists before sync');
|
|
123
|
+
assert.ok(existsSync(join(wtGsd, 'gsd.db-shm')), 'gsd.db-shm exists before sync');
|
|
124
|
+
|
|
125
|
+
syncProjectRootToWorktree(mainBase, wtBase, 'M001');
|
|
126
|
+
|
|
127
|
+
assert.ok(
|
|
128
|
+
!existsSync(join(wtGsd, 'gsd.db')),
|
|
129
|
+
'#2478: empty gsd.db must be deleted',
|
|
130
|
+
);
|
|
131
|
+
assert.ok(
|
|
132
|
+
!existsSync(join(wtGsd, 'gsd.db-wal')),
|
|
133
|
+
'#2478: orphaned gsd.db-wal must be deleted alongside gsd.db',
|
|
134
|
+
);
|
|
135
|
+
assert.ok(
|
|
136
|
+
!existsSync(join(wtGsd, 'gsd.db-shm')),
|
|
137
|
+
'#2478: orphaned gsd.db-shm must be deleted alongside gsd.db',
|
|
138
|
+
);
|
|
139
|
+
} finally {
|
|
140
|
+
cleanup(mainBase);
|
|
141
|
+
cleanup(wtBase);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── 4. Orphaned WAL/SHM cleaned up even when gsd.db already missing (#2478) ──
|
|
146
|
+
console.log('\n=== 4. orphaned WAL/SHM cleaned up even without gsd.db (#2478) ===');
|
|
147
|
+
{
|
|
148
|
+
const mainBase = createBase('main');
|
|
149
|
+
const wtBase = createBase('wt');
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
|
|
153
|
+
mkdirSync(m001Dir, { recursive: true });
|
|
154
|
+
writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
|
|
155
|
+
|
|
156
|
+
// Orphaned WAL/SHM with NO gsd.db at all — can happen from a previous
|
|
157
|
+
// partial cleanup. These must still be cleaned up.
|
|
158
|
+
const wtGsd = join(wtBase, '.gsd');
|
|
159
|
+
writeFileSync(join(wtGsd, 'gsd.db-wal'), Buffer.alloc(1024, 0xAA));
|
|
160
|
+
writeFileSync(join(wtGsd, 'gsd.db-shm'), Buffer.alloc(1024, 0xBB));
|
|
161
|
+
|
|
162
|
+
assert.ok(!existsSync(join(wtGsd, 'gsd.db')), 'gsd.db does not exist');
|
|
163
|
+
assert.ok(existsSync(join(wtGsd, 'gsd.db-wal')), 'orphaned gsd.db-wal exists');
|
|
164
|
+
assert.ok(existsSync(join(wtGsd, 'gsd.db-shm')), 'orphaned gsd.db-shm exists');
|
|
165
|
+
|
|
166
|
+
syncProjectRootToWorktree(mainBase, wtBase, 'M001');
|
|
167
|
+
|
|
168
|
+
assert.ok(
|
|
169
|
+
!existsSync(join(wtGsd, 'gsd.db-wal')),
|
|
170
|
+
'#2478: orphaned gsd.db-wal must be deleted even without main db file',
|
|
171
|
+
);
|
|
172
|
+
assert.ok(
|
|
173
|
+
!existsSync(join(wtGsd, 'gsd.db-shm')),
|
|
174
|
+
'#2478: orphaned gsd.db-shm must be deleted even without main db file',
|
|
175
|
+
);
|
|
176
|
+
} finally {
|
|
177
|
+
cleanup(mainBase);
|
|
178
|
+
cleanup(wtBase);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── 5. Milestone artifacts still synced when DB is preserved ────────
|
|
183
|
+
console.log('\n=== 5. milestone artifacts still synced even when DB preserved ===');
|
|
105
184
|
{
|
|
106
185
|
const mainBase = createBase('main');
|
|
107
186
|
const wtBase = createBase('wt');
|
|
@@ -292,13 +292,11 @@ export async function handleCompleteSlice(
|
|
|
292
292
|
// Toggle roadmap checkbox via renderer module
|
|
293
293
|
const roadmapToggled = await renderRoadmapCheckboxes(basePath, params.milestoneId);
|
|
294
294
|
if (!roadmapToggled) {
|
|
295
|
-
|
|
296
|
-
`gsd-db: complete_slice — could not find roadmap for ${params.milestoneId}, skipping checkbox toggle\n`,
|
|
297
|
-
);
|
|
295
|
+
logWarning("tool", `complete_slice — could not find roadmap for ${params.milestoneId}, skipping checkbox toggle`);
|
|
298
296
|
}
|
|
299
297
|
} catch (renderErr) {
|
|
300
298
|
// Disk render failed — roll back DB status so state stays consistent
|
|
301
|
-
logWarning("tool", `complete_slice — disk render failed, rolling back DB status:
|
|
299
|
+
logWarning("tool", `complete_slice — disk render failed for ${params.milestoneId}/${params.sliceId}, rolling back DB status`, { error: (renderErr as Error).message });
|
|
302
300
|
updateSliceStatus(params.milestoneId, params.sliceId, 'pending');
|
|
303
301
|
invalidateStateCache();
|
|
304
302
|
return { error: `disk render failed: ${(renderErr as Error).message}` };
|
|
@@ -325,7 +323,7 @@ export async function handleCompleteSlice(
|
|
|
325
323
|
trigger_reason: params.triggerReason,
|
|
326
324
|
});
|
|
327
325
|
} catch (hookErr) {
|
|
328
|
-
logWarning("tool", `complete-slice post-mutation hook
|
|
326
|
+
logWarning("tool", `complete-slice post-mutation hook failed for ${params.milestoneId}/${params.sliceId}`, { error: (hookErr as Error).message });
|
|
329
327
|
}
|
|
330
328
|
|
|
331
329
|
return {
|
|
@@ -174,17 +174,22 @@ export function summarizeLogs(): string | null {
|
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
176
|
* Format entries for display (used by auto-loop post-unit notification).
|
|
177
|
-
*
|
|
177
|
+
* Includes key context fields (file paths, commands) when present.
|
|
178
178
|
*/
|
|
179
179
|
export function formatForNotification(entries: readonly LogEntry[]): string {
|
|
180
180
|
if (entries.length === 0) return "";
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
return entries.map((e) => {
|
|
182
|
+
let line = `[${e.component}] ${e.message}`;
|
|
183
|
+
if (e.context) {
|
|
184
|
+
const ctxParts = Object.entries(e.context)
|
|
185
|
+
.filter(([k]) => k !== "error") // error is redundant with message
|
|
186
|
+
.map(([k, v]) => v.includes(",") ? `${k}: "${v}"` : `${k}: ${v}`);
|
|
187
|
+
if (ctxParts.length > 0) {
|
|
188
|
+
line += ` (${ctxParts.join(", ")})`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return line;
|
|
192
|
+
}).join("\n");
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
/**
|
|
@@ -348,7 +348,9 @@ function _reconcileWorktreeLogsInner(
|
|
|
348
348
|
if (conflicts.length > 0) {
|
|
349
349
|
// D-04: atomic all-or-nothing — block entire merge
|
|
350
350
|
writeConflictsFile(mainBasePath, conflicts, worktreeBasePath);
|
|
351
|
-
|
|
351
|
+
const conflictSummary = conflicts.slice(0, 3).map(c => `${c.entityType}:${c.entityId}`).join(", ");
|
|
352
|
+
const truncated = conflicts.length > 3 ? `... and ${conflicts.length - 3} more` : "";
|
|
353
|
+
logError("reconcile", `${conflicts.length} conflict(s) detected on ${conflictSummary}${truncated}. Details: .gsd/CONFLICTS.md`, { count: String(conflicts.length), path: join(mainBasePath, ".gsd", "CONFLICTS.md") });
|
|
352
354
|
return { autoMerged: 0, conflicts };
|
|
353
355
|
}
|
|
354
356
|
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
* triggers a follow-up free-text input prompt via ctx.ui.input().
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { describe, it } from "node:test";
|
|
13
|
+
import { describe, it, beforeEach } from "node:test";
|
|
14
14
|
import assert from "node:assert/strict";
|
|
15
15
|
|
|
16
16
|
// The ask-user-questions extension registers a tool via pi.registerTool().
|
|
17
17
|
// We capture that registration and call execute() directly with a mock context.
|
|
18
18
|
import AskUserQuestions from "../../ask-user-questions.js";
|
|
19
|
+
import { resetAskUserQuestionsCache } from "../../ask-user-questions.js";
|
|
19
20
|
|
|
20
21
|
interface CapturedTool {
|
|
21
22
|
name: string;
|
|
@@ -73,6 +74,10 @@ function makeMockCtx(opts: {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
describe("ask-user-questions RPC fallback free-text", () => {
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
resetAskUserQuestionsCache();
|
|
79
|
+
});
|
|
80
|
+
|
|
76
81
|
it("prompts for free-text input when user selects 'None of the above'", async () => {
|
|
77
82
|
const tool = captureTool();
|
|
78
83
|
const { ctx, selectCalls, inputCalls } = makeMockCtx({
|
|
File without changes
|
|
File without changes
|