@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
|
@@ -16,6 +16,7 @@ import { execSync } from "node:child_process";
|
|
|
16
16
|
|
|
17
17
|
import { captureIntegrationBranch, getCurrentBranch } from "../../worktree.ts";
|
|
18
18
|
import { readIntegrationBranch, QUICK_BRANCH_RE } from "../../git-service.ts";
|
|
19
|
+
import { disableDebug, enableDebug, getDebugCounters } from "../../debug-logger.ts";
|
|
19
20
|
|
|
20
21
|
function run(command: string, cwd: string): string {
|
|
21
22
|
return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
@@ -205,16 +206,25 @@ test('cleanupQuickBranch: recovers from disk state (cross-session)', async () =>
|
|
|
205
206
|
test('cleanupQuickBranch: no-op without pending state', async () => {
|
|
206
207
|
const repo = createTestRepo();
|
|
207
208
|
const origCwd = process.cwd();
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const result = cleanupQuickBranch();
|
|
212
|
-
|
|
213
|
-
assert.ok(!result, "returns false when no pending state");
|
|
214
|
-
assert.deepStrictEqual(getCurrentBranch(repo), "main", "stays on main");
|
|
209
|
+
try {
|
|
210
|
+
process.chdir(repo);
|
|
211
|
+
enableDebug(repo);
|
|
215
212
|
|
|
216
|
-
|
|
217
|
-
|
|
213
|
+
const { cleanupQuickBranch } = await import("../../quick.ts");
|
|
214
|
+
const result = cleanupQuickBranch();
|
|
215
|
+
const firstGitInvocations = getDebugCounters().gitInvocations;
|
|
216
|
+
const secondResult = cleanupQuickBranch();
|
|
217
|
+
|
|
218
|
+
assert.ok(!result, "returns false when no pending state");
|
|
219
|
+
assert.ok(!secondResult, "still returns false when no pending state");
|
|
220
|
+
assert.deepStrictEqual(getDebugCounters().gitInvocations, firstGitInvocations,
|
|
221
|
+
"cached no-state cleanup does not re-run git branch inference");
|
|
222
|
+
assert.deepStrictEqual(getCurrentBranch(repo), "main", "stays on main");
|
|
223
|
+
} finally {
|
|
224
|
+
disableDebug();
|
|
225
|
+
process.chdir(origCwd);
|
|
226
|
+
rmSync(repo, { recursive: true, force: true });
|
|
227
|
+
}
|
|
218
228
|
});
|
|
219
229
|
|
|
220
230
|
test('cleanupQuickBranch: infers return state from current gsd/quick branch', async () => {
|
|
@@ -240,6 +250,43 @@ test('cleanupQuickBranch: infers return state from current gsd/quick branch', as
|
|
|
240
250
|
}
|
|
241
251
|
});
|
|
242
252
|
|
|
253
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
254
|
+
// cleanupQuickBranch: stale miss invalidated after mid-session branch switch
|
|
255
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
256
|
+
test('cleanupQuickBranch: clears stale miss when branch switches to gsd/quick mid-session', async () => {
|
|
257
|
+
const repo = createTestRepo();
|
|
258
|
+
const origCwd = process.cwd();
|
|
259
|
+
try {
|
|
260
|
+
// Create a quick branch with real product work (so inference finds a diff)
|
|
261
|
+
run("git checkout -b gsd/quick/3-stale-miss", repo);
|
|
262
|
+
writeFileSync(join(repo, "stale.txt"), "stale miss test\n");
|
|
263
|
+
run("git add stale.txt", repo);
|
|
264
|
+
run('git commit -m "test: stale miss"', repo);
|
|
265
|
+
// Return to main so the first cleanupQuickBranch call records a miss
|
|
266
|
+
run("git checkout main", repo);
|
|
267
|
+
|
|
268
|
+
process.chdir(repo);
|
|
269
|
+
const { cleanupQuickBranch } = await import("../../quick.ts");
|
|
270
|
+
|
|
271
|
+
// First call: on main with no disk state → miss recorded (keyed to "main")
|
|
272
|
+
const result1 = cleanupQuickBranch();
|
|
273
|
+
assert.ok(!result1, "first call (on main) returns false — miss recorded");
|
|
274
|
+
|
|
275
|
+
// Simulate mid-session external branch switch to the stranded quick branch
|
|
276
|
+
run("git checkout gsd/quick/3-stale-miss", repo);
|
|
277
|
+
|
|
278
|
+
// Second call: branch changed from the recorded miss, so cache is invalidated
|
|
279
|
+
// and inferQuickReturnFromBranch runs — cleanup must succeed
|
|
280
|
+
const result2 = cleanupQuickBranch();
|
|
281
|
+
assert.ok(result2, "second call returns true after mid-session switch to quick branch");
|
|
282
|
+
assert.deepStrictEqual(getCurrentBranch(repo), "main",
|
|
283
|
+
"cleanup merged back to main after stale-miss invalidation");
|
|
284
|
+
} finally {
|
|
285
|
+
process.chdir(origCwd);
|
|
286
|
+
rmSync(repo, { recursive: true, force: true });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
243
290
|
// ═══════════════════════════════════════════════════════════════════════
|
|
244
291
|
// End-to-end: quick branch does NOT contaminate integration branch
|
|
245
292
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -136,8 +136,10 @@ test("#896 startup maintenance skips repeated sentinel work in one session", asy
|
|
|
136
136
|
closeDatabase();
|
|
137
137
|
assert.equal(isDbAvailable(), false);
|
|
138
138
|
|
|
139
|
+
let sessionId = "session-one";
|
|
139
140
|
const ctx = {
|
|
140
141
|
projectRoot: base,
|
|
142
|
+
sessionManager: { getSessionId: () => sessionId },
|
|
141
143
|
ui: { notify: () => undefined },
|
|
142
144
|
} as unknown as ExtensionContext;
|
|
143
145
|
|
|
@@ -190,6 +192,18 @@ test("#896 startup maintenance skips repeated sentinel work in one session", asy
|
|
|
190
192
|
.prepare("SELECT COUNT(*) AS count FROM memories WHERE structured_fields LIKE '%\"sourceKnowledgeId\":\"P002\"%'")
|
|
191
193
|
.get() as { count: number };
|
|
192
194
|
assert.equal(secondPass.count, 0, "second startup in same session should not re-run KNOWLEDGE.md sentinel backfill");
|
|
195
|
+
|
|
196
|
+
sessionId = "session-two";
|
|
197
|
+
await buildBeforeAgentStartResult(
|
|
198
|
+
{ prompt: "Inspect project knowledge in a new session", systemPrompt: "base system prompt" },
|
|
199
|
+
ctx,
|
|
200
|
+
);
|
|
201
|
+
await _flushDeferredContextMaintenanceForTest(base);
|
|
202
|
+
|
|
203
|
+
const nextSessionPass = adapter
|
|
204
|
+
.prepare("SELECT COUNT(*) AS count FROM memories WHERE structured_fields LIKE '%\"sourceKnowledgeId\":\"P002\"%'")
|
|
205
|
+
.get() as { count: number };
|
|
206
|
+
assert.equal(nextSessionPass.count, 1, "startup maintenance should run once for a later session");
|
|
193
207
|
});
|
|
194
208
|
|
|
195
209
|
test("#896 later turns reopen the project DB after startup maintenance is complete", async (t) => {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
openDatabase,
|
|
17
17
|
closeDatabase,
|
|
18
18
|
insertDecision,
|
|
19
|
+
_getAdapter,
|
|
19
20
|
} from "../gsd-db.ts";
|
|
20
21
|
import { createMemory } from "../memory-store.ts";
|
|
21
22
|
import {
|
|
@@ -271,6 +272,83 @@ test("scanConsolidationGaps combines decisions and KNOWLEDGE.md gaps in summary"
|
|
|
271
272
|
}
|
|
272
273
|
});
|
|
273
274
|
|
|
275
|
+
test("scanConsolidationGaps collects memory source markers with one memories scan", () => {
|
|
276
|
+
const base = makeTmpBase();
|
|
277
|
+
try {
|
|
278
|
+
insertDecision({
|
|
279
|
+
id: "D001",
|
|
280
|
+
when_context: "2026-01-01",
|
|
281
|
+
scope: "M001",
|
|
282
|
+
decision: "Unmigrated decision",
|
|
283
|
+
choice: "A",
|
|
284
|
+
rationale: "x",
|
|
285
|
+
revisable: "yes",
|
|
286
|
+
made_by: "agent",
|
|
287
|
+
superseded_by: null,
|
|
288
|
+
});
|
|
289
|
+
insertDecision({
|
|
290
|
+
id: "D002",
|
|
291
|
+
when_context: "2026-01-02",
|
|
292
|
+
scope: "M001",
|
|
293
|
+
decision: "Migrated decision",
|
|
294
|
+
choice: "B",
|
|
295
|
+
rationale: "y",
|
|
296
|
+
revisable: "yes",
|
|
297
|
+
made_by: "agent",
|
|
298
|
+
superseded_by: null,
|
|
299
|
+
});
|
|
300
|
+
createMemory({
|
|
301
|
+
category: "architecture",
|
|
302
|
+
content: "Migrated decision Chose: B. Rationale: y.",
|
|
303
|
+
scope: "M001",
|
|
304
|
+
structuredFields: { sourceDecisionId: "D002" },
|
|
305
|
+
});
|
|
306
|
+
createMemory({
|
|
307
|
+
category: "pattern",
|
|
308
|
+
content: "Migrated knowledge pattern",
|
|
309
|
+
scope: "project",
|
|
310
|
+
structuredFields: { sourceKnowledgeId: "P001" },
|
|
311
|
+
});
|
|
312
|
+
writeKnowledgeMd(
|
|
313
|
+
base,
|
|
314
|
+
`## Patterns
|
|
315
|
+
|
|
316
|
+
| # | Pattern | Where | Notes |
|
|
317
|
+
|---|---------|-------|-------|
|
|
318
|
+
| P001 | Migrated knowledge pattern | scanner | covered |
|
|
319
|
+
| P002 | Unmigrated knowledge pattern | scanner | gap |
|
|
320
|
+
`,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const adapter = _getAdapter();
|
|
324
|
+
assert.ok(adapter);
|
|
325
|
+
const originalPrepare = adapter.prepare.bind(adapter);
|
|
326
|
+
const memorySql: string[] = [];
|
|
327
|
+
adapter.prepare = (sql: string) => {
|
|
328
|
+
if (sql.includes("FROM memories")) memorySql.push(sql);
|
|
329
|
+
return originalPrepare(sql);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const report = scanConsolidationGaps(base);
|
|
333
|
+
assert.equal(report.decisions.migrated, 1);
|
|
334
|
+
assert.equal(report.decisions.unmigrated, 1);
|
|
335
|
+
assert.equal(report.knowledge.migrated, 1);
|
|
336
|
+
assert.equal(report.knowledge.unmigrated, 1);
|
|
337
|
+
assert.equal(
|
|
338
|
+
memorySql.filter((sql) => sql.includes("SELECT structured_fields FROM memories")).length,
|
|
339
|
+
1,
|
|
340
|
+
"source marker detection should read memories once",
|
|
341
|
+
);
|
|
342
|
+
assert.equal(
|
|
343
|
+
memorySql.some((sql) => sql.includes("LIKE :pattern")),
|
|
344
|
+
false,
|
|
345
|
+
"scanner must not reintroduce per-row LIKE marker probes",
|
|
346
|
+
);
|
|
347
|
+
} finally {
|
|
348
|
+
cleanup(base);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
274
352
|
// ─── reportConsolidationGaps ───────────────────────────────────────────────
|
|
275
353
|
|
|
276
354
|
test("reportConsolidationGaps emits a notification + warning when gaps exist", () => {
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import { resolveExpectedArtifactPath } from "../auto-artifact-paths.ts";
|
|
37
37
|
import { AutoSession } from "../auto/session.ts";
|
|
38
38
|
import { acquireSessionLock, releaseSessionLock } from "../session-lock.ts";
|
|
39
|
+
import { clearGSDPreferencesCache } from "../preferences.ts";
|
|
39
40
|
import { invalidateAllCaches } from "../cache.ts";
|
|
40
41
|
import { invalidateStateCache } from "../state.ts";
|
|
41
42
|
import {
|
|
@@ -63,12 +64,15 @@ interface FixtureOptions {
|
|
|
63
64
|
dispatch?: UnifiedRule["where"];
|
|
64
65
|
/** Drop the gate_runs table after seeding so emitUokGate's insertGateRun throws (:538 path). */
|
|
65
66
|
dropGateRuns?: boolean;
|
|
67
|
+
/** Write project preferences that disable UOK gate telemetry. */
|
|
68
|
+
disableUokGates?: boolean;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
interface Fixture {
|
|
69
72
|
base: string;
|
|
70
73
|
session: AutoSession;
|
|
71
74
|
orchestrator: ReturnType<typeof createAutoOrchestrator>;
|
|
75
|
+
getAvailableCalls(): number;
|
|
72
76
|
cleanup(): void;
|
|
73
77
|
}
|
|
74
78
|
|
|
@@ -82,6 +86,7 @@ function makeFixture(opts: FixtureOptions = {}): Fixture {
|
|
|
82
86
|
|
|
83
87
|
invalidateAllCaches();
|
|
84
88
|
invalidateStateCache();
|
|
89
|
+
clearGSDPreferencesCache();
|
|
85
90
|
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
86
91
|
insertMilestone({ id: "M001", title: "Milestone", status: "active" });
|
|
87
92
|
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "active", risk: "low", depends: [], demo: "", sequence: 1 });
|
|
@@ -100,6 +105,13 @@ function makeFixture(opts: FixtureOptions = {}): Fixture {
|
|
|
100
105
|
join(sliceDir, "S01-PLAN.md"),
|
|
101
106
|
["# S01: Slice", "", "**Goal:** g", "**Demo:** d", "", "## Tasks", "", "- [ ] **T01: Task** `est:1h`", ""].join("\n"),
|
|
102
107
|
);
|
|
108
|
+
if (opts.disableUokGates) {
|
|
109
|
+
writeFileSync(
|
|
110
|
+
join(base, ".gsd", "PREFERENCES.md"),
|
|
111
|
+
"---\nuok:\n gates:\n enabled: false\n---\n",
|
|
112
|
+
);
|
|
113
|
+
clearGSDPreferencesCache();
|
|
114
|
+
}
|
|
103
115
|
|
|
104
116
|
acquireSessionLock(base);
|
|
105
117
|
|
|
@@ -112,8 +124,19 @@ function makeFixture(opts: FixtureOptions = {}): Fixture {
|
|
|
112
124
|
session.currentMilestoneId = "M001";
|
|
113
125
|
session.resourceVersionOnStart = null;
|
|
114
126
|
|
|
127
|
+
let getAvailableCalls = 0;
|
|
115
128
|
const ctx: OrchestratorContext = {
|
|
116
|
-
ctx: {
|
|
129
|
+
ctx: {
|
|
130
|
+
model: {},
|
|
131
|
+
modelRegistry: {
|
|
132
|
+
getAll: () => [],
|
|
133
|
+
getAvailable: () => {
|
|
134
|
+
getAvailableCalls += 1;
|
|
135
|
+
return [];
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
ui: { notify() {} },
|
|
139
|
+
} as never,
|
|
117
140
|
pi: { getActiveTools: () => [] } as never,
|
|
118
141
|
dispatchBasePath: base,
|
|
119
142
|
runtimeBasePath: base,
|
|
@@ -140,8 +163,10 @@ function makeFixture(opts: FixtureOptions = {}): Fixture {
|
|
|
140
163
|
base,
|
|
141
164
|
session,
|
|
142
165
|
orchestrator,
|
|
166
|
+
getAvailableCalls: () => getAvailableCalls,
|
|
143
167
|
cleanup() {
|
|
144
168
|
resetRegistry();
|
|
169
|
+
clearGSDPreferencesCache();
|
|
145
170
|
try { releaseSessionLock(base); } catch { /* */ }
|
|
146
171
|
try { closeDatabase(); } catch { /* */ }
|
|
147
172
|
try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
@@ -210,6 +235,23 @@ test("advance() logs an engine warning when the uok gate emit fails (orchestrato
|
|
|
210
235
|
assert.ok(uokFail!.context?.gateId, "the failing gate id must be captured in context");
|
|
211
236
|
});
|
|
212
237
|
|
|
238
|
+
test("advance() resolves disabled uok gate flags once before gate emission", async (t) => {
|
|
239
|
+
const f = makeFixture({ dropGateRuns: true, disableUokGates: true });
|
|
240
|
+
t.after(() => f.cleanup());
|
|
241
|
+
|
|
242
|
+
const { logs } = await captureLogs(() => f.orchestrator.advance());
|
|
243
|
+
|
|
244
|
+
assert.equal(
|
|
245
|
+
logs.some((e) => e.component === "engine" && /uok gate emit failed/u.test(e.message)),
|
|
246
|
+
false,
|
|
247
|
+
"disabled uok gates must not construct the runner or write gate rows",
|
|
248
|
+
);
|
|
249
|
+
assert.ok(
|
|
250
|
+
f.getAvailableCalls() <= 2,
|
|
251
|
+
`uok gate preferences should be resolved once per advance, not once per emitted gate (getAvailable calls: ${f.getAvailableCalls()})`,
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
213
255
|
// orchestrator.ts:637 — mergePendingCompleteMilestone catches a
|
|
214
256
|
// rebuildMarkdownProjectionsFromDb failure after the system-owned milestone
|
|
215
257
|
// merge and logs `markdown projection rebuild after settlement merge failed`.
|
|
@@ -145,6 +145,32 @@ describe("parallel-research-slices dispatch rule", () => {
|
|
|
145
145
|
}
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
test("dispatches parallel research for S01 even when milestone research exists", async () => {
|
|
149
|
+
writeRoadmap(base, "M001", [
|
|
150
|
+
{ id: "S01", title: "Alpha" },
|
|
151
|
+
{ id: "S02", title: "Beta" },
|
|
152
|
+
]);
|
|
153
|
+
writeFileSync(
|
|
154
|
+
join(base, ".gsd", "milestones", "M001", "M001-RESEARCH.md"),
|
|
155
|
+
"# Milestone Research\n",
|
|
156
|
+
"utf-8",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const action = await resolveDispatch({
|
|
160
|
+
basePath: base,
|
|
161
|
+
mid: "M001",
|
|
162
|
+
midTitle: "Parallel Research Milestone",
|
|
163
|
+
state: baseState(),
|
|
164
|
+
prefs: undefined,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
assert.equal(action.action, "dispatch");
|
|
168
|
+
if (action.action === "dispatch") {
|
|
169
|
+
assert.equal(action.unitType, "research-slice");
|
|
170
|
+
assert.equal(action.unitId, "M001/parallel-research");
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
148
174
|
test("does not dispatch parallel research with only one ready slice", async () => {
|
|
149
175
|
writeRoadmap(base, "M001", [{ id: "S01", title: "Alpha" }]);
|
|
150
176
|
|
|
@@ -22,7 +22,7 @@ import { invalidateAllCaches } from "../cache.ts";
|
|
|
22
22
|
import type { GSDState } from "../types.ts";
|
|
23
23
|
|
|
24
24
|
const PARALLEL_RESEARCH_RULE = "planning (multiple slices need research) → parallel-research-slices";
|
|
25
|
-
const SINGLE_RESEARCH_RULE = "planning (no research
|
|
25
|
+
const SINGLE_RESEARCH_RULE = "planning (no research) → research-slice";
|
|
26
26
|
const VALIDATE_RULE = "validating-milestone → validate-milestone";
|
|
27
27
|
|
|
28
28
|
// ─── Fixture helpers ──────────────────────────────────────────────────────
|
|
@@ -6,7 +6,12 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
buildCategorySummaries,
|
|
11
|
+
handlePrefsWizard,
|
|
12
|
+
serializePreferencesToFrontmatter,
|
|
13
|
+
} from "../commands-prefs-wizard.ts";
|
|
14
|
+
import { parsePreferencesMarkdown, validatePreferences } from "../preferences.ts";
|
|
10
15
|
import { KNOWN_PREFERENCE_KEYS } from "../preferences-types.ts";
|
|
11
16
|
|
|
12
17
|
const PREF_SAMPLE_VALUES: Record<string, unknown> = {
|
|
@@ -103,6 +108,54 @@ const PREF_SAMPLE_VALUES: Record<string, unknown> = {
|
|
|
103
108
|
},
|
|
104
109
|
};
|
|
105
110
|
|
|
111
|
+
test("prefs serializer preserves nested hook on_block objects", () => {
|
|
112
|
+
const frontmatter = serializePreferencesToFrontmatter({
|
|
113
|
+
post_unit_hooks: [{
|
|
114
|
+
name: "plan-review",
|
|
115
|
+
after: ["execute-task"],
|
|
116
|
+
prompt: "write PLAN-REVIEW.md",
|
|
117
|
+
artifact: "PLAN-REVIEW.md",
|
|
118
|
+
criticality: "blocking",
|
|
119
|
+
on_block: { action: "pause" },
|
|
120
|
+
}],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
assert.doesNotMatch(frontmatter, /\[object Object\]/);
|
|
124
|
+
assert.ok(frontmatter.includes(" on_block:\n action: pause\n"));
|
|
125
|
+
|
|
126
|
+
const parsed = parsePreferencesMarkdown(`---\n${frontmatter}---\n`);
|
|
127
|
+
assert.notEqual(parsed, null);
|
|
128
|
+
|
|
129
|
+
const { errors, preferences } = validatePreferences(parsed!);
|
|
130
|
+
assert.deepEqual(errors, []);
|
|
131
|
+
assert.deepEqual(preferences.post_unit_hooks?.[0]?.on_block, { action: "pause" });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("prefs serializer correctly indents nested object when it is the first array-item property", () => {
|
|
135
|
+
// on_block is the FIRST property — exercises the first-key-is-object branch
|
|
136
|
+
const frontmatter = serializePreferencesToFrontmatter({
|
|
137
|
+
post_unit_hooks: [{
|
|
138
|
+
on_block: { action: "pause" }, // first property — exercises the first-key-is-object branch
|
|
139
|
+
name: "gate", // required fields come after so on_block stays first
|
|
140
|
+
after: ["execute-task"],
|
|
141
|
+
prompt: "review output",
|
|
142
|
+
}],
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
assert.doesNotMatch(frontmatter, /\[object Object\]/);
|
|
146
|
+
// Children of on_block must be indented deeper than on_block itself (not siblings)
|
|
147
|
+
assert.ok(
|
|
148
|
+
frontmatter.includes(" - on_block:\n action: pause\n"),
|
|
149
|
+
`Expected on_block children indented as nested YAML, got:\n${frontmatter}`,
|
|
150
|
+
);
|
|
151
|
+
// Sanity: parse + validate round-trip
|
|
152
|
+
const parsed = parsePreferencesMarkdown(`---\n${frontmatter}---\n`);
|
|
153
|
+
assert.notEqual(parsed, null);
|
|
154
|
+
const { errors, preferences } = validatePreferences(parsed!);
|
|
155
|
+
assert.deepEqual(errors, []);
|
|
156
|
+
assert.deepEqual(preferences.post_unit_hooks?.[0]?.on_block, { action: "pause" });
|
|
157
|
+
});
|
|
158
|
+
|
|
106
159
|
test("prefs wizard save path preserves every known preference key", async () => {
|
|
107
160
|
const missingSamples = [...KNOWN_PREFERENCE_KEYS].filter((key) => !(key in PREF_SAMPLE_VALUES));
|
|
108
161
|
assert.deepEqual(missingSamples, [], "test fixture must cover every known preference key");
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import test from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { classifyError, isTransient, isTransientNetworkError } from "../error-classifier.ts";
|
|
@@ -26,6 +26,10 @@ import { _buildCancelledUnitStopReason } from "../auto/phase-helpers.ts";
|
|
|
26
26
|
import { _classifyZeroToolProviderMessageForTest } from "../auto/unit-phase.ts";
|
|
27
27
|
import { autoSession } from "../auto-runtime-state.ts";
|
|
28
28
|
import { getNextFallbackModel } from "../preferences.ts";
|
|
29
|
+
import { clearGuidedUnitContext, getGuidedUnitContext, setGuidedUnitContext } from "../guided-unit-context.ts";
|
|
30
|
+
import { initNotificationStore, readNotifications, _resetNotificationStore } from "../notification-store.ts";
|
|
31
|
+
import { installNotifyInterceptor } from "../bootstrap/notify-interceptor.ts";
|
|
32
|
+
import { extractTrace } from "../session-forensics.ts";
|
|
29
33
|
// Zero-import module — imported by path rather than through the package
|
|
30
34
|
// barrel to avoid pulling the full AgentSession / @gsd/pi-ai dep graph into
|
|
31
35
|
// this unit test (see #4837).
|
|
@@ -488,6 +492,63 @@ test("pauseAutoForProviderError falls back to indefinite pause when not rate lim
|
|
|
488
492
|
]);
|
|
489
493
|
});
|
|
490
494
|
|
|
495
|
+
test("agent_end retries when empty errorMessage has stream failure in content (#956)", async () => {
|
|
496
|
+
const originalSetTimeout = globalThis.setTimeout;
|
|
497
|
+
const notifications: Array<{ message: string; level?: string }> = [];
|
|
498
|
+
const sendMessageCalls: unknown[][] = [];
|
|
499
|
+
const timers: Array<{ fn: () => void; delay: number }> = [];
|
|
500
|
+
|
|
501
|
+
resetTransientRetryState();
|
|
502
|
+
autoSession.reset();
|
|
503
|
+
// handleAgentEnd returns at the isAutoActive() guard unless auto-mode is
|
|
504
|
+
// active. Set the minimum fields needed to reach the stopReason === "error"
|
|
505
|
+
// branch without requiring a real DB or worktree.
|
|
506
|
+
autoSession.active = true;
|
|
507
|
+
autoSession.currentUnit = { type: "execute-task", id: "M001/S01/T01", startedAt: Date.now() };
|
|
508
|
+
|
|
509
|
+
globalThis.setTimeout = ((fn: () => void, delay?: number) => {
|
|
510
|
+
timers.push({ fn, delay: delay ?? 0 });
|
|
511
|
+
return 0 as unknown as ReturnType<typeof setTimeout>;
|
|
512
|
+
}) as typeof setTimeout;
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
await handleAgentEnd({
|
|
516
|
+
sendMessage: (...args: unknown[]) => {
|
|
517
|
+
sendMessageCalls.push(args);
|
|
518
|
+
},
|
|
519
|
+
} as any, {
|
|
520
|
+
messages: [{
|
|
521
|
+
role: "assistant",
|
|
522
|
+
stopReason: "error",
|
|
523
|
+
errorMessage: "",
|
|
524
|
+
content: [{ type: "text", text: "API Error: stream idle timeout - partial response received" }],
|
|
525
|
+
}],
|
|
526
|
+
} as any, {
|
|
527
|
+
model: { provider: "openai-codex", id: "gpt-5.5" },
|
|
528
|
+
ui: {
|
|
529
|
+
notify(message: string, level?: "info" | "warning" | "error" | "success") {
|
|
530
|
+
notifications.push({ message, level });
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
} as any);
|
|
534
|
+
|
|
535
|
+
assert.equal(timers.length, 1, "empty errorMessage stream failures should use the network retry path");
|
|
536
|
+
assert.equal(timers[0].delay, 3_000);
|
|
537
|
+
assert.deepEqual(notifications[0], {
|
|
538
|
+
message: "Network error on gpt-5.5: API Error: stream idle timeout - partial response received. Retry 1/2 in 3s...",
|
|
539
|
+
level: "warning",
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
timers[0].fn();
|
|
543
|
+
assert.equal(sendMessageCalls.length, 1);
|
|
544
|
+
assert.deepEqual(sendMessageCalls[0][1], { triggerTurn: true });
|
|
545
|
+
} finally {
|
|
546
|
+
globalThis.setTimeout = originalSetTimeout;
|
|
547
|
+
resetTransientRetryState();
|
|
548
|
+
autoSession.reset();
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
491
552
|
test("rate-limit agent_end walks past unavailable fallback models before pausing (#716 follow-up)", async () => {
|
|
492
553
|
const originalCwd = process.cwd();
|
|
493
554
|
const originalSetTimeout = globalThis.setTimeout;
|
|
@@ -648,6 +709,139 @@ test("does not suppress deleted-worktree provider errors outside terminal comple
|
|
|
648
709
|
assert.equal(event.message.stopReason, "error");
|
|
649
710
|
});
|
|
650
711
|
|
|
712
|
+
test("manual guided discuss provider error records warning and activity marker (#944)", async () => {
|
|
713
|
+
const originalCwd = process.cwd();
|
|
714
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-manual-discuss-error-"));
|
|
715
|
+
const notifications: Array<{ message: string; level?: string }> = [];
|
|
716
|
+
const sendMessageCalls: unknown[][] = [];
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
autoSession.reset();
|
|
720
|
+
mkdirSync(join(base, ".git"), { recursive: true });
|
|
721
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
722
|
+
process.chdir(base);
|
|
723
|
+
initNotificationStore(base);
|
|
724
|
+
|
|
725
|
+
// Use base as the guided context path so gsdRoot(base) hits the fast path
|
|
726
|
+
// (.gsd exists at base directly) and doesn't need git to walk up from a
|
|
727
|
+
// subdirectory — the empty .git folder is not a real repo and git resolution
|
|
728
|
+
// from a child directory would return the wrong .gsd path.
|
|
729
|
+
setGuidedUnitContext(base, "discuss-slice");
|
|
730
|
+
|
|
731
|
+
const ctx = {
|
|
732
|
+
model: { provider: "openai-codex", id: "gpt-5.1-codex" },
|
|
733
|
+
modelRegistry: { getAvailable: () => [] },
|
|
734
|
+
ui: {
|
|
735
|
+
notify(message: string, level?: "info" | "warning" | "error" | "success") {
|
|
736
|
+
notifications.push({ message, level });
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
} as any;
|
|
740
|
+
installNotifyInterceptor(ctx);
|
|
741
|
+
|
|
742
|
+
const pi = {
|
|
743
|
+
sendMessage: (...args: unknown[]) => {
|
|
744
|
+
sendMessageCalls.push(args);
|
|
745
|
+
},
|
|
746
|
+
} as any;
|
|
747
|
+
|
|
748
|
+
await handleAgentEnd(pi, {
|
|
749
|
+
messages: [{
|
|
750
|
+
role: "assistant",
|
|
751
|
+
stopReason: "error",
|
|
752
|
+
errorMessage: "",
|
|
753
|
+
content: [{ type: "text", text: "stream idle timeout while saving summary" }],
|
|
754
|
+
}],
|
|
755
|
+
} as any, ctx);
|
|
756
|
+
|
|
757
|
+
assert.deepEqual(sendMessageCalls, [], "manual discuss terminal errors must not auto-retry or redispatch");
|
|
758
|
+
assert.equal(getGuidedUnitContext(base), null, "guided unit context must still be cleared after the turn");
|
|
759
|
+
assert.equal(notifications.length, 1);
|
|
760
|
+
assert.equal(notifications[0]?.level, "warning");
|
|
761
|
+
assert.match(notifications[0]?.message ?? "", /Manual \/gsd discuss discuss-slice ended with a provider error/);
|
|
762
|
+
assert.match(notifications[0]?.message ?? "", /openai-codex\/gpt-5\.1-codex/);
|
|
763
|
+
assert.match(notifications[0]?.message ?? "", /stream idle timeout/);
|
|
764
|
+
|
|
765
|
+
const persisted = readNotifications(base);
|
|
766
|
+
assert.equal(persisted.length, 1, "wrapped notify should persist exactly one warning notification");
|
|
767
|
+
assert.equal(persisted[0]?.severity, "warning");
|
|
768
|
+
assert.equal(persisted[0]?.source, "notify");
|
|
769
|
+
|
|
770
|
+
const activityDir = join(base, ".gsd", "activity");
|
|
771
|
+
const files = readdirSync(activityDir).filter((file) => file.endsWith(".jsonl"));
|
|
772
|
+
assert.equal(files.length, 1, "manual guided provider error should write one activity marker");
|
|
773
|
+
const entries = readFileSync(join(activityDir, files[0]!), "utf-8")
|
|
774
|
+
.split("\n")
|
|
775
|
+
.filter(Boolean)
|
|
776
|
+
.map((line) => JSON.parse(line));
|
|
777
|
+
const trace = extractTrace(entries);
|
|
778
|
+
assert.equal(trace.errors.length, 1);
|
|
779
|
+
assert.match(trace.errors[0] ?? "", /discuss-slice/);
|
|
780
|
+
assert.match(trace.errors[0] ?? "", /openai-codex\/gpt-5\.1-codex/);
|
|
781
|
+
assert.match(trace.errors[0] ?? "", /stream idle timeout/);
|
|
782
|
+
} finally {
|
|
783
|
+
clearGuidedUnitContext();
|
|
784
|
+
_resetNotificationStore();
|
|
785
|
+
autoSession.reset();
|
|
786
|
+
process.chdir(originalCwd);
|
|
787
|
+
rmSync(base, { recursive: true, force: true });
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
test("manual guided discuss user-cancel is not treated as a provider error (#944)", async () => {
|
|
792
|
+
const originalCwd = process.cwd();
|
|
793
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-manual-discuss-cancel-"));
|
|
794
|
+
const guidedBase = join(base, "slice-work");
|
|
795
|
+
const notifications: Array<{ message: string; level?: string }> = [];
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
autoSession.reset();
|
|
799
|
+
mkdirSync(join(base, ".git"), { recursive: true });
|
|
800
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
801
|
+
mkdirSync(guidedBase, { recursive: true });
|
|
802
|
+
process.chdir(base);
|
|
803
|
+
initNotificationStore(base);
|
|
804
|
+
|
|
805
|
+
setGuidedUnitContext(guidedBase, "discuss-slice");
|
|
806
|
+
|
|
807
|
+
const ctx = {
|
|
808
|
+
model: { provider: "anthropic", id: "claude-opus-4" },
|
|
809
|
+
modelRegistry: { getAvailable: () => [] },
|
|
810
|
+
ui: {
|
|
811
|
+
notify(message: string, level?: "info" | "warning" | "error" | "success") {
|
|
812
|
+
notifications.push({ message, level });
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
} as any;
|
|
816
|
+
installNotifyInterceptor(ctx);
|
|
817
|
+
|
|
818
|
+
const pi = { sendMessage: () => {} } as any;
|
|
819
|
+
|
|
820
|
+
await handleAgentEnd(pi, {
|
|
821
|
+
messages: [{
|
|
822
|
+
role: "assistant",
|
|
823
|
+
stopReason: "error",
|
|
824
|
+
errorMessage: "Request aborted by user",
|
|
825
|
+
content: [],
|
|
826
|
+
}],
|
|
827
|
+
} as any, ctx);
|
|
828
|
+
|
|
829
|
+
assert.deepEqual(notifications, [], "user-cancel stopReason=error must not emit a provider-error warning");
|
|
830
|
+
assert.equal(getGuidedUnitContext(guidedBase), null, "context must be cleared even for user-cancel");
|
|
831
|
+
|
|
832
|
+
// No activity directory should have been created
|
|
833
|
+
let activityExists = false;
|
|
834
|
+
try { readdirSync(join(base, ".gsd", "activity")); activityExists = true; } catch { /* expected */ }
|
|
835
|
+
assert.equal(activityExists, false, "user-cancel must not write an activity error marker");
|
|
836
|
+
} finally {
|
|
837
|
+
clearGuidedUnitContext();
|
|
838
|
+
_resetNotificationStore();
|
|
839
|
+
autoSession.reset();
|
|
840
|
+
process.chdir(originalCwd);
|
|
841
|
+
rmSync(base, { recursive: true, force: true });
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
|
|
651
845
|
// ── resumeAutoAfterProviderDelay ────────────────────────────────────────────
|
|
652
846
|
|
|
653
847
|
test("resumeAutoAfterProviderDelay restarts paused auto-mode from the recorded base path", async () => {
|