gsd-pi 2.80.0-dev.e146beb20 → 2.80.0-dev.e6c48c3af
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/README.md +4 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +59 -21
- package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +17 -2
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
- package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
- package/dist/resources/extensions/gsd/auto.js +84 -5
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +75 -4
- package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
- package/dist/resources/extensions/gsd/context-budget.js +37 -2
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
- package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
- package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
- package/dist/resources/extensions/gsd/git-service.js +36 -4
- package/dist/resources/extensions/gsd/gsd-db.js +46 -13
- package/dist/resources/extensions/gsd/guided-flow.js +33 -4
- package/dist/resources/extensions/gsd/memory-store.js +69 -12
- package/dist/resources/extensions/gsd/migrate/command.js +40 -1
- package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
- package/dist/resources/extensions/gsd/pre-execution-checks.js +7 -0
- package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/dist/resources/extensions/gsd/quick.js +34 -2
- package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
- package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
- package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
- package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
- 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 +16 -16
- 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 +16 -16
- package/dist/web/standalone/.next/server/middleware-build-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/package.json +3 -3
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +22 -17
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
- package/packages/mcp-server/src/workflow-tools.ts +30 -16
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +15 -0
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +2 -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 +12 -3
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +12 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
- package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +18 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +14 -3
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
- package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
- package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +12 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +18 -8
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +20 -8
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
- package/src/resources/extensions/gsd/auto/phases.ts +85 -35
- package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +22 -2
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
- package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
- package/src/resources/extensions/gsd/auto.ts +96 -4
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +88 -4
- package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
- package/src/resources/extensions/gsd/context-budget.ts +44 -2
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
- package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
- package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
- package/src/resources/extensions/gsd/git-service.ts +46 -8
- package/src/resources/extensions/gsd/gsd-db.ts +50 -13
- package/src/resources/extensions/gsd/guided-flow.ts +49 -4
- package/src/resources/extensions/gsd/memory-store.ts +77 -12
- package/src/resources/extensions/gsd/migrate/command.ts +47 -1
- package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
- package/src/resources/extensions/gsd/pre-execution-checks.ts +7 -0
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/src/resources/extensions/gsd/quick.ts +37 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +215 -1
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +166 -4
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +14 -1
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +239 -1
- package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
- package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
- package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
- package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
- package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
- package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
- package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
- /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 4dQ9NTZJ8pEvFwBgDUX93}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 4dQ9NTZJ8pEvFwBgDUX93}/_ssgManifest.js +0 -0
|
@@ -29,7 +29,8 @@ test('buildSnapshot: renders memories, exec history, and active context', () =>
|
|
|
29
29
|
memories: [
|
|
30
30
|
{ id: 'MEM001', category: 'gotcha', content: 'FTS5 needs Porter tokenizer', confidence: 0.9,
|
|
31
31
|
source_unit_type: null, source_unit_id: null, created_at: '', updated_at: '',
|
|
32
|
-
superseded_by: null, hit_count: 0, scope: 'project', seq: 1, tags: [], structured_fields: null
|
|
32
|
+
superseded_by: null, hit_count: 0, scope: 'project', seq: 1, tags: [], structured_fields: null,
|
|
33
|
+
last_hit_at: null },
|
|
33
34
|
],
|
|
34
35
|
execHistory: [
|
|
35
36
|
{
|
|
@@ -65,6 +66,7 @@ test('buildSnapshot: enforces the byte cap with a truncation marker', () => {
|
|
|
65
66
|
seq: i,
|
|
66
67
|
tags: [] as string[],
|
|
67
68
|
structured_fields: null,
|
|
69
|
+
last_hit_at: null,
|
|
68
70
|
}));
|
|
69
71
|
const snap = buildSnapshot(
|
|
70
72
|
{ generatedAt: new Date(), memories: longMemories, execHistory: [] },
|
|
@@ -121,3 +123,14 @@ test('executeResume: reports friendly empty state when no snapshot exists', () =
|
|
|
121
123
|
cleanup(base);
|
|
122
124
|
}
|
|
123
125
|
});
|
|
126
|
+
|
|
127
|
+
test('executeResume: returns disabled error when context_mode.enabled=false', () => {
|
|
128
|
+
const base = freshBase();
|
|
129
|
+
try {
|
|
130
|
+
const result = executeResume({}, { baseDir: base, preferences: { context_mode: { enabled: false } } });
|
|
131
|
+
assert.equal(result.isError, true);
|
|
132
|
+
assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
|
|
133
|
+
} finally {
|
|
134
|
+
cleanup(base);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* No I/O, no extension context, no global state.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { describe, it } from "node:test";
|
|
7
|
+
import { describe, it, beforeEach } from "node:test";
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -16,8 +16,17 @@ import {
|
|
|
16
16
|
computeBudgets,
|
|
17
17
|
truncateAtSectionBoundary,
|
|
18
18
|
resolveExecutorContextWindow,
|
|
19
|
+
_resetEmpiricalCacheForTest,
|
|
19
20
|
} from "../context-budget.js";
|
|
20
21
|
|
|
22
|
+
// Reset the per-provider empirical chars-per-token cache before each test.
|
|
23
|
+
// The hardcoded char-ratio assertions below assume the static fallback path
|
|
24
|
+
// (3.5 / 4.0 chars/token) is used. Without this guard, a prior test that
|
|
25
|
+
// warms tiktoken would populate the cache and silently break these tests.
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
_resetEmpiricalCacheForTest();
|
|
28
|
+
});
|
|
29
|
+
|
|
21
30
|
import type { TokenProvider } from "../token-counter.js";
|
|
22
31
|
|
|
23
32
|
// ─── Test helpers ─────────────────────────────────────────────────────────────
|
|
@@ -192,7 +192,11 @@ function makeMockDeps(overrides?: Partial<LoopDeps>): LoopDeps & { callLog: stri
|
|
|
192
192
|
resolveMilestoneFile: () => null,
|
|
193
193
|
reconcileMergeState: () => "clean",
|
|
194
194
|
preflightCleanRoot: () => ({ stashPushed: false, summary: "" }),
|
|
195
|
-
postflightPopStash: () => {
|
|
195
|
+
postflightPopStash: () => ({
|
|
196
|
+
restored: true,
|
|
197
|
+
needsManualRecovery: false,
|
|
198
|
+
message: "restored",
|
|
199
|
+
}),
|
|
196
200
|
getLedger: () => null,
|
|
197
201
|
getProjectTotals: () => ({ cost: 0 }),
|
|
198
202
|
formatCost: (c: number) => `$${c.toFixed(2)}`,
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// gsd-2 / dispatch rule coverage canary test
|
|
2
|
+
//
|
|
3
|
+
// Iterates DISPATCH_RULES in order against representative GSDState stubs and
|
|
4
|
+
// asserts that the first matching rule has the expected name and unitType
|
|
5
|
+
// (mirroring auto-dispatch's first-match-wins semantics). The goal is a
|
|
6
|
+
// canary: if a future PR adds a new rule in the wrong position and steals
|
|
7
|
+
// a match from an existing one, this test fails.
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
|
|
15
|
+
import { DISPATCH_RULES } from "../auto-dispatch.ts";
|
|
16
|
+
import type { DispatchContext, DispatchAction } from "../auto-dispatch.ts";
|
|
17
|
+
import type { GSDState } from "../types.ts";
|
|
18
|
+
|
|
19
|
+
// ─── State helpers ────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function makeState(overrides: Partial<GSDState> = {}): GSDState {
|
|
22
|
+
return {
|
|
23
|
+
activeMilestone: { id: "M001", title: "Test Milestone" },
|
|
24
|
+
activeSlice: null,
|
|
25
|
+
activeTask: null,
|
|
26
|
+
phase: "pre-planning",
|
|
27
|
+
recentDecisions: [],
|
|
28
|
+
blockers: [],
|
|
29
|
+
nextAction: "",
|
|
30
|
+
registry: [],
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeCtx(basePath: string, state: GSDState, mid = "M001"): DispatchContext {
|
|
36
|
+
return {
|
|
37
|
+
basePath,
|
|
38
|
+
mid,
|
|
39
|
+
midTitle: "Test Milestone",
|
|
40
|
+
state,
|
|
41
|
+
prefs: undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Disk scaffold helpers ────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function writeMilestoneFile(basePath: string, mid: string, suffix: string, content = "stub\n"): void {
|
|
48
|
+
const dir = join(basePath, ".gsd", "milestones", mid);
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
writeFileSync(join(dir, `${mid}-${suffix}.md`), content);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeSliceFile(
|
|
54
|
+
basePath: string,
|
|
55
|
+
mid: string,
|
|
56
|
+
sid: string,
|
|
57
|
+
suffix: string,
|
|
58
|
+
content = "stub\n",
|
|
59
|
+
): void {
|
|
60
|
+
const dir = join(basePath, ".gsd", "milestones", mid, "slices", sid);
|
|
61
|
+
mkdirSync(dir, { recursive: true });
|
|
62
|
+
writeFileSync(join(dir, `${sid}-${suffix}.md`), content);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeTaskPlan(basePath: string, mid: string, sid: string, tid: string): void {
|
|
66
|
+
const dir = join(basePath, ".gsd", "milestones", mid, "slices", sid, "tasks");
|
|
67
|
+
mkdirSync(dir, { recursive: true });
|
|
68
|
+
writeFileSync(join(dir, `${tid}-PLAN.md`), `# ${tid}\n\n## Steps\n- [ ] Step\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Rule evaluation ──────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
interface MatchEntry {
|
|
74
|
+
ruleName: string;
|
|
75
|
+
result: DispatchAction;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// First-match-wins semantics: walks DISPATCH_RULES in order and stops at the
|
|
79
|
+
// first non-null result. This mirrors the production resolver and is the
|
|
80
|
+
// canary against rule reordering or shadowing.
|
|
81
|
+
async function findFirstMatch(ctx: DispatchContext): Promise<MatchEntry | null> {
|
|
82
|
+
for (const rule of DISPATCH_RULES) {
|
|
83
|
+
const result = await rule.match(ctx);
|
|
84
|
+
if (result) return { ruleName: rule.name, result };
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function assertMatch(
|
|
90
|
+
match: MatchEntry | null,
|
|
91
|
+
expected: { ruleName: string; action: DispatchAction["action"]; unitType?: string },
|
|
92
|
+
scenario: string,
|
|
93
|
+
): void {
|
|
94
|
+
assert.ok(match, `${scenario}: no rule matched`);
|
|
95
|
+
assert.equal(match.ruleName, expected.ruleName, `${scenario}: matched rule mismatch`);
|
|
96
|
+
assert.equal(match.result.action, expected.action, `${scenario}: action mismatch`);
|
|
97
|
+
if (expected.action === "dispatch" && expected.unitType) {
|
|
98
|
+
assert.ok(
|
|
99
|
+
match.result.action === "dispatch" && match.result.unitType === expected.unitType,
|
|
100
|
+
`${scenario}: unitType mismatch (got ${
|
|
101
|
+
match.result.action === "dispatch" ? match.result.unitType : match.result.action
|
|
102
|
+
})`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Tests ────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
test("dispatch-rule-coverage: escalating-task → stop (info)", async (t) => {
|
|
110
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-esc-"));
|
|
111
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
112
|
+
|
|
113
|
+
const ctx = makeCtx(
|
|
114
|
+
tmp,
|
|
115
|
+
makeState({
|
|
116
|
+
phase: "escalating-task",
|
|
117
|
+
activeSlice: { id: "S01", title: "Slice" },
|
|
118
|
+
nextAction: "Resolve escalation X",
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
const match = await findFirstMatch(ctx);
|
|
122
|
+
assertMatch(
|
|
123
|
+
match,
|
|
124
|
+
{ ruleName: "escalating-task → pause-for-escalation", action: "stop" },
|
|
125
|
+
"escalating-task",
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("dispatch-rule-coverage: pre-planning, no CONTEXT → discuss-milestone", async (t) => {
|
|
130
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-disc-"));
|
|
131
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
132
|
+
|
|
133
|
+
// Bare milestone dir, no CONTEXT/RESEARCH/ROADMAP files.
|
|
134
|
+
mkdirSync(join(tmp, ".gsd", "milestones", "M001"), { recursive: true });
|
|
135
|
+
|
|
136
|
+
const ctx = makeCtx(tmp, makeState({ phase: "pre-planning" }));
|
|
137
|
+
const match = await findFirstMatch(ctx);
|
|
138
|
+
assertMatch(
|
|
139
|
+
match,
|
|
140
|
+
{
|
|
141
|
+
ruleName: "pre-planning (no context) → discuss-milestone",
|
|
142
|
+
action: "dispatch",
|
|
143
|
+
unitType: "discuss-milestone",
|
|
144
|
+
},
|
|
145
|
+
"pre-planning no context",
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("dispatch-rule-coverage: pre-planning, has CONTEXT, no RESEARCH → research-milestone", async (t) => {
|
|
150
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-res-"));
|
|
151
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
152
|
+
|
|
153
|
+
writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n");
|
|
154
|
+
|
|
155
|
+
const ctx = makeCtx(tmp, makeState({ phase: "pre-planning" }));
|
|
156
|
+
const match = await findFirstMatch(ctx);
|
|
157
|
+
assertMatch(
|
|
158
|
+
match,
|
|
159
|
+
{
|
|
160
|
+
ruleName: "pre-planning (no research) → research-milestone",
|
|
161
|
+
action: "dispatch",
|
|
162
|
+
unitType: "research-milestone",
|
|
163
|
+
},
|
|
164
|
+
"pre-planning no research",
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("dispatch-rule-coverage: pre-planning, has CONTEXT + RESEARCH → plan-milestone", async (t) => {
|
|
169
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-plan-m-"));
|
|
170
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
171
|
+
|
|
172
|
+
writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n");
|
|
173
|
+
writeMilestoneFile(tmp, "M001", "RESEARCH", "# Research\n");
|
|
174
|
+
|
|
175
|
+
const ctx = makeCtx(tmp, makeState({ phase: "pre-planning" }));
|
|
176
|
+
const match = await findFirstMatch(ctx);
|
|
177
|
+
assertMatch(
|
|
178
|
+
match,
|
|
179
|
+
{
|
|
180
|
+
ruleName: "pre-planning (has research) → plan-milestone",
|
|
181
|
+
action: "dispatch",
|
|
182
|
+
unitType: "plan-milestone",
|
|
183
|
+
},
|
|
184
|
+
"pre-planning has research",
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("dispatch-rule-coverage: planning with active slice and skip_research → plan-slice", async (t) => {
|
|
189
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-plan-s-"));
|
|
190
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
191
|
+
|
|
192
|
+
writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n");
|
|
193
|
+
writeMilestoneFile(tmp, "M001", "ROADMAP", "# Roadmap\n");
|
|
194
|
+
|
|
195
|
+
const state = makeState({
|
|
196
|
+
phase: "planning",
|
|
197
|
+
activeSlice: { id: "S01", title: "First Slice" },
|
|
198
|
+
});
|
|
199
|
+
const ctx: DispatchContext = {
|
|
200
|
+
basePath: tmp,
|
|
201
|
+
mid: "M001",
|
|
202
|
+
midTitle: "Test Milestone",
|
|
203
|
+
state,
|
|
204
|
+
// Skip slice research so the parallel/single research rules fall through.
|
|
205
|
+
prefs: { phases: { skip_slice_research: true } } as DispatchContext["prefs"],
|
|
206
|
+
};
|
|
207
|
+
const match = await findFirstMatch(ctx);
|
|
208
|
+
assertMatch(
|
|
209
|
+
match,
|
|
210
|
+
{
|
|
211
|
+
ruleName: "planning → plan-slice",
|
|
212
|
+
action: "dispatch",
|
|
213
|
+
unitType: "plan-slice",
|
|
214
|
+
},
|
|
215
|
+
"planning → plan-slice",
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("dispatch-rule-coverage: executing with task plan present → execute-task", async (t) => {
|
|
220
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-exec-"));
|
|
221
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
222
|
+
|
|
223
|
+
writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n");
|
|
224
|
+
writeSliceFile(tmp, "M001", "S01", "PLAN", "# Plan\n");
|
|
225
|
+
writeTaskPlan(tmp, "M001", "S01", "T01");
|
|
226
|
+
|
|
227
|
+
const state = makeState({
|
|
228
|
+
phase: "executing",
|
|
229
|
+
activeSlice: { id: "S01", title: "First Slice" },
|
|
230
|
+
activeTask: { id: "T01", title: "First Task" },
|
|
231
|
+
});
|
|
232
|
+
// Disable reactive dispatch so the parallel batching rule falls through.
|
|
233
|
+
const ctx: DispatchContext = {
|
|
234
|
+
basePath: tmp,
|
|
235
|
+
mid: "M001",
|
|
236
|
+
midTitle: "Test Milestone",
|
|
237
|
+
state,
|
|
238
|
+
prefs: { reactive_execution: { enabled: false } } as DispatchContext["prefs"],
|
|
239
|
+
};
|
|
240
|
+
const match = await findFirstMatch(ctx);
|
|
241
|
+
assertMatch(
|
|
242
|
+
match,
|
|
243
|
+
{
|
|
244
|
+
ruleName: "executing → execute-task",
|
|
245
|
+
action: "dispatch",
|
|
246
|
+
unitType: "execute-task",
|
|
247
|
+
},
|
|
248
|
+
"executing → execute-task",
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("dispatch-rule-coverage: summarizing → complete-slice", async (t) => {
|
|
253
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-sum-"));
|
|
254
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
255
|
+
|
|
256
|
+
// Rule "execution-entry phase (no context)" fires for summarizing if CONTEXT
|
|
257
|
+
// is missing — write it so the summarizing rule wins.
|
|
258
|
+
writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n");
|
|
259
|
+
|
|
260
|
+
const ctx = makeCtx(
|
|
261
|
+
tmp,
|
|
262
|
+
makeState({
|
|
263
|
+
phase: "summarizing",
|
|
264
|
+
activeSlice: { id: "S01", title: "First Slice" },
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
const match = await findFirstMatch(ctx);
|
|
268
|
+
assertMatch(
|
|
269
|
+
match,
|
|
270
|
+
{
|
|
271
|
+
ruleName: "summarizing → complete-slice",
|
|
272
|
+
action: "dispatch",
|
|
273
|
+
unitType: "complete-slice",
|
|
274
|
+
},
|
|
275
|
+
"summarizing → complete-slice",
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("dispatch-rule-coverage: complete phase → stop", async (t) => {
|
|
280
|
+
const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-done-"));
|
|
281
|
+
t.after(() => rmSync(tmp, { recursive: true, force: true }));
|
|
282
|
+
|
|
283
|
+
const ctx = makeCtx(
|
|
284
|
+
tmp,
|
|
285
|
+
makeState({
|
|
286
|
+
phase: "complete",
|
|
287
|
+
activeMilestone: null,
|
|
288
|
+
lastCompletedMilestone: { id: "M001", title: "Test Milestone" },
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
const match = await findFirstMatch(ctx);
|
|
292
|
+
assertMatch(
|
|
293
|
+
match,
|
|
294
|
+
{ ruleName: "complete → stop", action: "stop" },
|
|
295
|
+
"complete → stop",
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ─── Ordering canary: every scenario above resolves to exactly one rule ────
|
|
300
|
+
|
|
301
|
+
test("dispatch-rule-coverage: rule registry has the expected size", () => {
|
|
302
|
+
// Sanity check that complements the per-state assertions: if someone adds a
|
|
303
|
+
// new rule, this number changes — prompting them to add a state stub above.
|
|
304
|
+
// Exact count is a brittle but useful canary; update when adding rules
|
|
305
|
+
// intentionally.
|
|
306
|
+
assert.equal(
|
|
307
|
+
DISPATCH_RULES.length,
|
|
308
|
+
29,
|
|
309
|
+
`DISPATCH_RULES length changed (got ${DISPATCH_RULES.length}). ` +
|
|
310
|
+
"If you added a rule, add a state stub to dispatch-rule-coverage.test.ts " +
|
|
311
|
+
"and update this expected count.",
|
|
312
|
+
);
|
|
313
|
+
});
|
|
@@ -108,6 +108,21 @@ test('executeExecSearch: returns helpful empty-state message when no matches', (
|
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
test('executeExecSearch: returns disabled error when context_mode.enabled=false', () => {
|
|
112
|
+
const base = freshBase();
|
|
113
|
+
try {
|
|
114
|
+
writeRun(base, 'should-not-surface', { stdout: 'hidden\n' });
|
|
115
|
+
const result = executeExecSearch(
|
|
116
|
+
{ query: 'hidden' },
|
|
117
|
+
{ baseDir: base, preferences: { context_mode: { enabled: false } } },
|
|
118
|
+
);
|
|
119
|
+
assert.equal(result.isError, true);
|
|
120
|
+
assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
|
|
121
|
+
} finally {
|
|
122
|
+
cleanup(base);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
111
126
|
test('executeExecSearch: includes stdout_path and preview in details', () => {
|
|
112
127
|
const base = freshBase();
|
|
113
128
|
try {
|
|
@@ -7,6 +7,7 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { EXEC_DEFAULTS, runExecSandbox, type ExecSandboxOptions } from '../exec-sandbox.ts';
|
|
8
8
|
import { buildExecOptions, executeGsdExec } from '../tools/exec-tool.ts';
|
|
9
9
|
import { isContextModeEnabled } from '../preferences-types.ts';
|
|
10
|
+
import { validatePreferences } from '../preferences-validation.ts';
|
|
10
11
|
|
|
11
12
|
function freshBase(): string {
|
|
12
13
|
return mkdtempSync(join(tmpdir(), 'gsd-exec-test-'));
|
|
@@ -174,6 +175,52 @@ test('executeGsdExec: runs when enabled explicitly set to true', async () => {
|
|
|
174
175
|
}
|
|
175
176
|
});
|
|
176
177
|
|
|
178
|
+
test('executeGsdExec: forwards custom exec_env_allowlist from preferences', async () => {
|
|
179
|
+
const base = freshBase();
|
|
180
|
+
try {
|
|
181
|
+
const result = await executeGsdExec(
|
|
182
|
+
{
|
|
183
|
+
runtime: 'bash',
|
|
184
|
+
script: 'printf "allowed=%s blocked=%s\\n" "$GSD_ALLOWED" "$GSD_BLOCKED"',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
baseDir: base,
|
|
188
|
+
preferences: {
|
|
189
|
+
context_mode: {
|
|
190
|
+
enabled: true,
|
|
191
|
+
exec_env_allowlist: ['GSD_ALLOWED'],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
env: {
|
|
195
|
+
PATH: '/usr/bin:/bin',
|
|
196
|
+
HOME: '/tmp',
|
|
197
|
+
GSD_ALLOWED: 'yes',
|
|
198
|
+
GSD_BLOCKED: 'no',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
assert.ok(!result.isError);
|
|
203
|
+
assert.match(result.content[0].text, /allowed=yes blocked=/);
|
|
204
|
+
assert.doesNotMatch(result.content[0].text, /blocked=no/);
|
|
205
|
+
} finally {
|
|
206
|
+
cleanup(base);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('executeGsdExec: enforces per-call timeout override end-to-end', async () => {
|
|
211
|
+
const base = freshBase();
|
|
212
|
+
try {
|
|
213
|
+
const result = await executeGsdExec(
|
|
214
|
+
{ runtime: 'bash', script: 'sleep 2', timeout_ms: 1 },
|
|
215
|
+
{ baseDir: base, preferences: { context_mode: { enabled: true, exec_timeout_ms: 10_000 } } },
|
|
216
|
+
);
|
|
217
|
+
assert.equal(result.details.timed_out, true);
|
|
218
|
+
assert.equal(result.isError, true);
|
|
219
|
+
} finally {
|
|
220
|
+
cleanup(base);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
177
224
|
test('executeGsdExec: rejects empty script', async () => {
|
|
178
225
|
const base = freshBase();
|
|
179
226
|
try {
|
|
@@ -188,6 +235,24 @@ test('executeGsdExec: rejects empty script', async () => {
|
|
|
188
235
|
}
|
|
189
236
|
});
|
|
190
237
|
|
|
238
|
+
test('validatePreferences: rejects invalid context_mode preference values', () => {
|
|
239
|
+
const result = validatePreferences({
|
|
240
|
+
context_mode: {
|
|
241
|
+
enabled: 'false',
|
|
242
|
+
exec_timeout_ms: 999,
|
|
243
|
+
exec_stdout_cap_bytes: 1,
|
|
244
|
+
exec_digest_chars: -1,
|
|
245
|
+
exec_env_allowlist: ['GOOD_NAME', 'bad-name'],
|
|
246
|
+
},
|
|
247
|
+
} as any);
|
|
248
|
+
assert.ok(result.errors.length > 0);
|
|
249
|
+
assert.ok(result.errors.includes('context_mode.enabled must be a boolean'));
|
|
250
|
+
assert.ok(result.errors.includes('context_mode.exec_timeout_ms must be a number between 1000 and 600000'));
|
|
251
|
+
assert.ok(result.errors.includes('context_mode.exec_stdout_cap_bytes must be a number between 4096 and 16777216'));
|
|
252
|
+
assert.ok(result.errors.includes('context_mode.exec_digest_chars must be a number between 0 and 4000'));
|
|
253
|
+
assert.ok(result.errors.includes('context_mode.exec_env_allowlist must be an array of valid env var names'));
|
|
254
|
+
});
|
|
255
|
+
|
|
191
256
|
test('isContextModeEnabled: defaults to true; only explicit false disables', () => {
|
|
192
257
|
assert.equal(isContextModeEnabled(undefined), true, 'undefined prefs → on');
|
|
193
258
|
assert.equal(isContextModeEnabled(null), true, 'null prefs → on');
|
|
@@ -542,6 +542,60 @@ describe('git-service', async () => {
|
|
|
542
542
|
rmSync(repo, { recursive: true, force: true });
|
|
543
543
|
});
|
|
544
544
|
|
|
545
|
+
// Regression: #5500. The LLM occasionally hallucinates files in
|
|
546
|
+
// task.keyFiles that were never written. Pre-existing scoped-stage code
|
|
547
|
+
// ran `git add -- <every keyFile>` and failed the entire commit on the
|
|
548
|
+
// first missing path. Verify that valid paths still commit and missing
|
|
549
|
+
// ones are dropped silently.
|
|
550
|
+
test('GitServiceImpl: scoped staging drops missing keyFiles and commits the rest', () => {
|
|
551
|
+
const repo = initTempRepo();
|
|
552
|
+
const svc = new GitServiceImpl(repo);
|
|
553
|
+
|
|
554
|
+
createFile(repo, "src/index.ts", "export const ok = true;");
|
|
555
|
+
// Note: src/commands/list.ts is intentionally NOT created — the LLM
|
|
556
|
+
// claimed it wrote this file but didn't.
|
|
557
|
+
|
|
558
|
+
const msg = svc.autoCommit("execute-task", "M001/S01/T02", [], {
|
|
559
|
+
taskId: "S01/T02",
|
|
560
|
+
taskTitle: "wire up command list",
|
|
561
|
+
oneLiner: "Added list command stub",
|
|
562
|
+
keyFiles: ["src/index.ts", "src/commands/list.ts"],
|
|
563
|
+
});
|
|
564
|
+
assert.ok(msg !== null, "autoCommit succeeds when at least one keyFile exists");
|
|
565
|
+
|
|
566
|
+
const committed = run("git show --name-only --format= HEAD", repo);
|
|
567
|
+
assert.ok(committed.includes("src/index.ts"), "existing key file is committed");
|
|
568
|
+
assert.ok(!committed.includes("src/commands/list.ts"), "missing key file is silently dropped");
|
|
569
|
+
|
|
570
|
+
rmSync(repo, { recursive: true, force: true });
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Regression: #5500. When ALL keyFiles are bogus, scopedStageTaskFiles
|
|
574
|
+
// must return false so autoCommit falls back to smartStage. The commit
|
|
575
|
+
// still goes out (using `git add -A` semantics) instead of failing.
|
|
576
|
+
test('GitServiceImpl: all missing keyFiles falls back to smartStage', () => {
|
|
577
|
+
const repo = initTempRepo();
|
|
578
|
+
const svc = new GitServiceImpl(repo);
|
|
579
|
+
|
|
580
|
+
createFile(repo, "src/actually-changed.ts", "export const real = true;");
|
|
581
|
+
|
|
582
|
+
const msg = svc.autoCommit("execute-task", "M001/S01/T03", [], {
|
|
583
|
+
taskId: "S01/T03",
|
|
584
|
+
taskTitle: "fix path handling",
|
|
585
|
+
oneLiner: "Hardened path resolution",
|
|
586
|
+
keyFiles: ["src/wrong/path-1.ts", "src/wrong/path-2.ts"],
|
|
587
|
+
});
|
|
588
|
+
assert.ok(msg !== null, "autoCommit falls back to smartStage when all keyFiles are missing");
|
|
589
|
+
|
|
590
|
+
const committed = run("git show --name-only --format= HEAD", repo);
|
|
591
|
+
assert.ok(
|
|
592
|
+
committed.includes("src/actually-changed.ts"),
|
|
593
|
+
"smartStage fallback stages real dirty files when scoped staging finds nothing",
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
rmSync(repo, { recursive: true, force: true });
|
|
597
|
+
});
|
|
598
|
+
|
|
545
599
|
// ─── GitServiceImpl: empty-after-staging guard ─────────────────────────
|
|
546
600
|
|
|
547
601
|
test('GitServiceImpl: empty-after-staging guard', () => {
|