gsd-pi 2.76.0-dev.82e249f7b → 2.76.0-dev.fe143342a
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/mcp-server.d.ts +7 -0
- package/dist/mcp-server.js +35 -1
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +2 -8
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +66 -4
- package/dist/resources/extensions/gsd/auto-start.js +27 -14
- package/dist/resources/extensions/gsd/auto.js +11 -11
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +35 -0
- package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
- package/dist/resources/extensions/gsd/error-classifier.js +10 -3
- package/dist/resources/extensions/gsd/exec-history.js +120 -0
- package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
- package/dist/resources/extensions/gsd/gsd-db.js +62 -4
- package/dist/resources/extensions/gsd/init-wizard.js +15 -1
- package/dist/resources/extensions/gsd/key-manager.js +6 -0
- package/dist/resources/extensions/gsd/pre-execution-checks.js +13 -3
- package/dist/resources/extensions/gsd/preferences-types.js +9 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
- package/dist/resources/extensions/gsd/preferences.js +17 -17
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.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/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +64 -25
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
- package/packages/mcp-server/src/workflow-tools.ts +84 -43
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
- package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
- package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
- package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
- package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -4
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +137 -2
- package/src/resources/extensions/gsd/auto-start.ts +28 -15
- package/src/resources/extensions/gsd/auto.ts +11 -11
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -0
- package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
- package/src/resources/extensions/gsd/error-classifier.ts +10 -3
- package/src/resources/extensions/gsd/exec-history.ts +153 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
- package/src/resources/extensions/gsd/gsd-db.ts +68 -4
- package/src/resources/extensions/gsd/init-wizard.ts +15 -1
- package/src/resources/extensions/gsd/key-manager.ts +6 -0
- package/src/resources/extensions/gsd/pre-execution-checks.ts +13 -3
- package/src/resources/extensions/gsd/preferences-types.ts +38 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
- package/src/resources/extensions/gsd/preferences.ts +17 -17
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
- package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
- /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → n21VtX2hZlkpdEUO_nU4z}/_ssgManifest.js +0 -0
|
@@ -68,13 +68,13 @@ export { resolveAllSkillReferences } from "./preferences-skills.js";
|
|
|
68
68
|
// These lived in preferences-skills.ts but imported loadEffectiveGSDPreferences
|
|
69
69
|
// back from this file, creating a circular dependency. Moved here since they
|
|
70
70
|
// are trivial wrappers over loadEffectiveGSDPreferences.
|
|
71
|
-
export function resolveSkillDiscoveryMode(): SkillDiscoveryMode {
|
|
72
|
-
const prefs = loadEffectiveGSDPreferences();
|
|
71
|
+
export function resolveSkillDiscoveryMode(basePath?: string): SkillDiscoveryMode {
|
|
72
|
+
const prefs = loadEffectiveGSDPreferences(basePath);
|
|
73
73
|
return prefs?.preferences.skill_discovery ?? "suggest";
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export function resolveSkillStalenessDays(): number {
|
|
77
|
-
const prefs = loadEffectiveGSDPreferences();
|
|
76
|
+
export function resolveSkillStalenessDays(basePath?: string): number {
|
|
77
|
+
const prefs = loadEffectiveGSDPreferences(basePath);
|
|
78
78
|
return prefs?.preferences.skill_staleness_days ?? 60;
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -109,16 +109,16 @@ function legacyGlobalPreferencesPath(): string {
|
|
|
109
109
|
return join(homedir(), ".pi", "agent", "gsd-preferences.md");
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function projectPreferencesPath(): string {
|
|
113
|
-
return join(gsdRoot(
|
|
112
|
+
function projectPreferencesPath(basePath: string = process.cwd()): string {
|
|
113
|
+
return join(gsdRoot(basePath), "PREFERENCES.md");
|
|
114
114
|
}
|
|
115
115
|
// Legacy lowercase files can still exist in older projects. Keep them as a
|
|
116
116
|
// compatibility-only fallback, but route new reads/writes through PREFERENCES.md.
|
|
117
117
|
function legacyGlobalPreferencesPathLowercase(): string {
|
|
118
118
|
return join(gsdHome(), "preferences.md");
|
|
119
119
|
}
|
|
120
|
-
function legacyProjectPreferencesPathLowercase(): string {
|
|
121
|
-
return join(gsdRoot(
|
|
120
|
+
function legacyProjectPreferencesPathLowercase(basePath: string = process.cwd()): string {
|
|
121
|
+
return join(gsdRoot(basePath), "preferences.md");
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
export function getGlobalGSDPreferencesPath(): string {
|
|
@@ -129,8 +129,8 @@ export function getLegacyGlobalGSDPreferencesPath(): string {
|
|
|
129
129
|
return legacyGlobalPreferencesPath();
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
export function getProjectGSDPreferencesPath(): string {
|
|
133
|
-
return projectPreferencesPath();
|
|
132
|
+
export function getProjectGSDPreferencesPath(basePath?: string): string {
|
|
133
|
+
return projectPreferencesPath(basePath);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// ─── Loading ────────────────────────────────────────────────────────────────
|
|
@@ -141,14 +141,14 @@ export function loadGlobalGSDPreferences(): LoadedGSDPreferences | null {
|
|
|
141
141
|
?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global");
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
export function loadProjectGSDPreferences(): LoadedGSDPreferences | null {
|
|
145
|
-
return loadPreferencesFile(projectPreferencesPath(), "project")
|
|
146
|
-
?? loadPreferencesFile(legacyProjectPreferencesPathLowercase(), "project");
|
|
144
|
+
export function loadProjectGSDPreferences(basePath?: string): LoadedGSDPreferences | null {
|
|
145
|
+
return loadPreferencesFile(projectPreferencesPath(basePath), "project")
|
|
146
|
+
?? loadPreferencesFile(legacyProjectPreferencesPathLowercase(basePath), "project");
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
export function loadEffectiveGSDPreferences(): LoadedGSDPreferences | null {
|
|
149
|
+
export function loadEffectiveGSDPreferences(basePath?: string): LoadedGSDPreferences | null {
|
|
150
150
|
const globalPreferences = loadGlobalGSDPreferences();
|
|
151
|
-
const projectPreferences = loadProjectGSDPreferences();
|
|
151
|
+
const projectPreferences = loadProjectGSDPreferences(basePath);
|
|
152
152
|
|
|
153
153
|
if (!globalPreferences && !projectPreferences) return null;
|
|
154
154
|
|
|
@@ -603,8 +603,8 @@ export function resolvePreDispatchHooks(): PreDispatchHookConfig[] {
|
|
|
603
603
|
* Worktree isolation requires explicit opt-in because it depends on git
|
|
604
604
|
* branch infrastructure that must be set up before use.
|
|
605
605
|
*/
|
|
606
|
-
export function getIsolationMode(): "none" | "worktree" | "branch" {
|
|
607
|
-
const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
|
|
606
|
+
export function getIsolationMode(basePath?: string): "none" | "worktree" | "branch" {
|
|
607
|
+
const prefs = loadEffectiveGSDPreferences(basePath)?.preferences?.git;
|
|
608
608
|
if (prefs?.isolation === "worktree") return "worktree";
|
|
609
609
|
if (prefs?.isolation === "branch") return "branch";
|
|
610
610
|
return "none"; // default — no isolation, work on current branch
|
|
@@ -100,7 +100,7 @@ function getChangedFilesFromLastCommit(basePath: string): string[] | null {
|
|
|
100
100
|
try {
|
|
101
101
|
const result = execFileSync(
|
|
102
102
|
"git",
|
|
103
|
-
["diff", "--
|
|
103
|
+
["diff-tree", "--root", "--no-commit-id", "-r", "--name-only", "HEAD"],
|
|
104
104
|
{ cwd: basePath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
|
|
105
105
|
).trim();
|
|
106
106
|
return result ? result.split("\n").filter(Boolean) : [];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildSnapshot,
|
|
9
|
+
readCompactionSnapshot,
|
|
10
|
+
writeCompactionSnapshot,
|
|
11
|
+
DEFAULT_SNAPSHOT_BYTES,
|
|
12
|
+
} from '../compaction-snapshot.ts';
|
|
13
|
+
import { closeDatabase, openDatabase } from '../gsd-db.ts';
|
|
14
|
+
import { createMemory } from '../memory-store.ts';
|
|
15
|
+
import { executeResume } from '../tools/resume-tool.ts';
|
|
16
|
+
|
|
17
|
+
function freshBase(): string {
|
|
18
|
+
return mkdtempSync(join(tmpdir(), 'gsd-snap-'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cleanup(dir: string): void {
|
|
22
|
+
rmSync(dir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('buildSnapshot: renders memories, exec history, and active context', () => {
|
|
26
|
+
const snap = buildSnapshot({
|
|
27
|
+
generatedAt: new Date('2026-04-20T12:00:00.000Z'),
|
|
28
|
+
activeContext: 'M001 / S01 / T01 — wire gsd_exec',
|
|
29
|
+
memories: [
|
|
30
|
+
{ id: 'MEM001', category: 'gotcha', content: 'FTS5 needs Porter tokenizer', confidence: 0.9,
|
|
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 },
|
|
33
|
+
],
|
|
34
|
+
execHistory: [
|
|
35
|
+
{
|
|
36
|
+
id: 'abc',
|
|
37
|
+
runtime: 'bash',
|
|
38
|
+
purpose: 'count TODOs',
|
|
39
|
+
started_at: '', finished_at: '', duration_ms: 10,
|
|
40
|
+
exit_code: 0, signal: null, timed_out: false,
|
|
41
|
+
stdout_bytes: 1, stderr_bytes: 0, stdout_truncated: false, stderr_truncated: false,
|
|
42
|
+
stdout_path: '/tmp/abc.stdout', stderr_path: '/tmp/abc.stderr', meta_path: '/tmp/abc.meta.json',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
assert.match(snap, /Active context/);
|
|
47
|
+
assert.match(snap, /M001 \/ S01 \/ T01/);
|
|
48
|
+
assert.match(snap, /FTS5 needs Porter tokenizer/);
|
|
49
|
+
assert.match(snap, /\[abc\] bash exit:0 — count TODOs/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('buildSnapshot: enforces the byte cap with a truncation marker', () => {
|
|
53
|
+
const longMemories = Array.from({ length: 50 }, (_v, i) => ({
|
|
54
|
+
id: `MEM${String(i).padStart(3, '0')}`,
|
|
55
|
+
category: 'gotcha',
|
|
56
|
+
content: 'x'.repeat(200),
|
|
57
|
+
confidence: 0.8,
|
|
58
|
+
source_unit_type: null,
|
|
59
|
+
source_unit_id: null,
|
|
60
|
+
created_at: '',
|
|
61
|
+
updated_at: '',
|
|
62
|
+
superseded_by: null,
|
|
63
|
+
hit_count: 0,
|
|
64
|
+
scope: 'project',
|
|
65
|
+
seq: i,
|
|
66
|
+
tags: [] as string[],
|
|
67
|
+
structured_fields: null,
|
|
68
|
+
}));
|
|
69
|
+
const snap = buildSnapshot(
|
|
70
|
+
{ generatedAt: new Date(), memories: longMemories, execHistory: [] },
|
|
71
|
+
{ maxBytes: 512, maxMemories: 50 },
|
|
72
|
+
);
|
|
73
|
+
assert.ok(Buffer.byteLength(snap, 'utf-8') <= 512, 'should respect cap');
|
|
74
|
+
assert.match(snap, /\[truncated\]/, 'should include truncation marker');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('buildSnapshot: handles empty state with an explanatory placeholder', () => {
|
|
78
|
+
const snap = buildSnapshot({ generatedAt: new Date(), memories: [], execHistory: [] });
|
|
79
|
+
assert.match(snap, /_No durable memories/);
|
|
80
|
+
assert.ok(Buffer.byteLength(snap, 'utf-8') <= DEFAULT_SNAPSHOT_BYTES);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('writeCompactionSnapshot + readCompactionSnapshot + executeResume: end-to-end', () => {
|
|
84
|
+
const base = freshBase();
|
|
85
|
+
try {
|
|
86
|
+
openDatabase(':memory:');
|
|
87
|
+
createMemory({ category: 'architecture', content: 'Single-writer DB through gsd-db.ts', confidence: 0.95 });
|
|
88
|
+
createMemory({ category: 'convention', content: 'Prefer typed helpers over raw SQL', confidence: 0.9 });
|
|
89
|
+
|
|
90
|
+
const out = writeCompactionSnapshot(base, { activeContext: 'M099 resume check' });
|
|
91
|
+
assert.ok(out.path.endsWith('last-snapshot.md'));
|
|
92
|
+
assert.ok(out.bytes > 0);
|
|
93
|
+
assert.equal(out.memories, 2);
|
|
94
|
+
|
|
95
|
+
const contents = readCompactionSnapshot(base);
|
|
96
|
+
assert.ok(contents);
|
|
97
|
+
assert.match(contents!, /Single-writer DB through gsd-db\.ts/);
|
|
98
|
+
assert.match(contents!, /M099 resume check/);
|
|
99
|
+
|
|
100
|
+
const tool = executeResume({}, { baseDir: base });
|
|
101
|
+
assert.ok(!tool.isError);
|
|
102
|
+
assert.equal(tool.details.found, true);
|
|
103
|
+
assert.match(tool.content[0].text, /Single-writer DB through gsd-db\.ts/);
|
|
104
|
+
|
|
105
|
+
// also verify the file content matches (without trailing newline)
|
|
106
|
+
const raw = readFileSync(out.path, 'utf-8');
|
|
107
|
+
assert.ok(raw.endsWith('\n'));
|
|
108
|
+
} finally {
|
|
109
|
+
closeDatabase();
|
|
110
|
+
cleanup(base);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('executeResume: reports friendly empty state when no snapshot exists', () => {
|
|
115
|
+
const base = freshBase();
|
|
116
|
+
try {
|
|
117
|
+
const result = executeResume({}, { baseDir: base });
|
|
118
|
+
assert.equal(result.details.found, false);
|
|
119
|
+
assert.match(result.content[0].text, /No snapshot found/);
|
|
120
|
+
} finally {
|
|
121
|
+
cleanup(base);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { listExecHistory, searchExecHistory } from '../exec-history.ts';
|
|
8
|
+
import { executeExecSearch } from '../tools/exec-search-tool.ts';
|
|
9
|
+
|
|
10
|
+
function freshBase(): string {
|
|
11
|
+
return mkdtempSync(join(tmpdir(), 'gsd-exec-history-'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function cleanup(dir: string): void {
|
|
15
|
+
rmSync(dir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeRun(base: string, id: string, overrides: Record<string, unknown> = {}): void {
|
|
19
|
+
const dir = join(base, '.gsd', 'exec');
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
const stdoutPath = join(dir, `${id}.stdout`);
|
|
22
|
+
const stderrPath = join(dir, `${id}.stderr`);
|
|
23
|
+
const metaPath = join(dir, `${id}.meta.json`);
|
|
24
|
+
writeFileSync(stdoutPath, (overrides.stdout as string | undefined) ?? `stdout for ${id}\n`);
|
|
25
|
+
writeFileSync(stderrPath, '');
|
|
26
|
+
writeFileSync(
|
|
27
|
+
metaPath,
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
id,
|
|
30
|
+
runtime: 'bash',
|
|
31
|
+
purpose: `purpose for ${id}`,
|
|
32
|
+
started_at: '2026-04-20T12:00:00.000Z',
|
|
33
|
+
finished_at: '2026-04-20T12:00:00.100Z',
|
|
34
|
+
duration_ms: 100,
|
|
35
|
+
exit_code: 0,
|
|
36
|
+
signal: null,
|
|
37
|
+
timed_out: false,
|
|
38
|
+
stdout_bytes: 12,
|
|
39
|
+
stderr_bytes: 0,
|
|
40
|
+
stdout_truncated: false,
|
|
41
|
+
stderr_truncated: false,
|
|
42
|
+
stdout_path: stdoutPath,
|
|
43
|
+
stderr_path: stderrPath,
|
|
44
|
+
...overrides,
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('listExecHistory: returns empty list when .gsd/exec missing', () => {
|
|
50
|
+
const base = freshBase();
|
|
51
|
+
try {
|
|
52
|
+
assert.deepEqual(listExecHistory(base), []);
|
|
53
|
+
} finally {
|
|
54
|
+
cleanup(base);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('listExecHistory: skips malformed meta files', () => {
|
|
59
|
+
const base = freshBase();
|
|
60
|
+
try {
|
|
61
|
+
const dir = join(base, '.gsd', 'exec');
|
|
62
|
+
mkdirSync(dir, { recursive: true });
|
|
63
|
+
writeFileSync(join(dir, 'bad.meta.json'), '{not-json');
|
|
64
|
+
writeRun(base, 'ok-1');
|
|
65
|
+
const list = listExecHistory(base);
|
|
66
|
+
assert.equal(list.length, 1);
|
|
67
|
+
assert.equal(list[0]!.id, 'ok-1');
|
|
68
|
+
} finally {
|
|
69
|
+
cleanup(base);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('searchExecHistory: filters by query, runtime, and failing_only', () => {
|
|
74
|
+
const base = freshBase();
|
|
75
|
+
try {
|
|
76
|
+
writeRun(base, 'playwright-run', { purpose: 'playwright snapshot' });
|
|
77
|
+
writeRun(base, 'grep-run', { purpose: 'grep TODOs' });
|
|
78
|
+
writeRun(base, 'failing-run', { exit_code: 1, purpose: 'boom' });
|
|
79
|
+
writeRun(base, 'node-run', { runtime: 'node', purpose: 'dedupe' });
|
|
80
|
+
|
|
81
|
+
const playwrightHits = searchExecHistory(base, { query: 'playwright' });
|
|
82
|
+
assert.equal(playwrightHits.length, 1);
|
|
83
|
+
assert.equal(playwrightHits[0]!.entry.id, 'playwright-run');
|
|
84
|
+
|
|
85
|
+
const failingHits = searchExecHistory(base, { failing_only: true });
|
|
86
|
+
assert.equal(failingHits.length, 1);
|
|
87
|
+
assert.equal(failingHits[0]!.entry.id, 'failing-run');
|
|
88
|
+
|
|
89
|
+
const nodeHits = searchExecHistory(base, { runtime: 'node' });
|
|
90
|
+
assert.equal(nodeHits.length, 1);
|
|
91
|
+
assert.equal(nodeHits[0]!.entry.runtime, 'node');
|
|
92
|
+
|
|
93
|
+
const unlimited = searchExecHistory(base, {});
|
|
94
|
+
assert.equal(unlimited.length, 4);
|
|
95
|
+
} finally {
|
|
96
|
+
cleanup(base);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('executeExecSearch: returns helpful empty-state message when no matches', () => {
|
|
101
|
+
const base = freshBase();
|
|
102
|
+
try {
|
|
103
|
+
const result = executeExecSearch({ query: 'missing' }, { baseDir: base });
|
|
104
|
+
assert.ok(!result.isError);
|
|
105
|
+
assert.match(result.content[0].text, /No prior gsd_exec runs/);
|
|
106
|
+
} finally {
|
|
107
|
+
cleanup(base);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('executeExecSearch: includes stdout_path and preview in details', () => {
|
|
112
|
+
const base = freshBase();
|
|
113
|
+
try {
|
|
114
|
+
writeRun(base, 'summary-run', { stdout: 'found 42 TODOs\n' });
|
|
115
|
+
const result = executeExecSearch({ query: 'summary' }, { baseDir: base });
|
|
116
|
+
const details = result.details as { results: Array<{ id: string; stdout_path: string }> };
|
|
117
|
+
assert.equal(details.results.length, 1);
|
|
118
|
+
assert.equal(details.results[0]!.id, 'summary-run');
|
|
119
|
+
assert.match(details.results[0]!.stdout_path, /summary-run\.stdout$/);
|
|
120
|
+
assert.match(result.content[0].text, /found 42 TODOs/);
|
|
121
|
+
} finally {
|
|
122
|
+
cleanup(base);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { EXEC_DEFAULTS, runExecSandbox, type ExecSandboxOptions } from '../exec-sandbox.ts';
|
|
8
|
+
import { buildExecOptions, executeGsdExec } from '../tools/exec-tool.ts';
|
|
9
|
+
import { isContextModeEnabled } from '../preferences-types.ts';
|
|
10
|
+
|
|
11
|
+
function freshBase(): string {
|
|
12
|
+
return mkdtempSync(join(tmpdir(), 'gsd-exec-test-'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cleanup(dir: string): void {
|
|
16
|
+
rmSync(dir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function baseOpts(base: string, overrides: Partial<ExecSandboxOptions> = {}): ExecSandboxOptions {
|
|
20
|
+
return {
|
|
21
|
+
baseDir: base,
|
|
22
|
+
clamp_timeout_ms: EXEC_DEFAULTS.clampTimeoutMs,
|
|
23
|
+
default_timeout_ms: 10_000,
|
|
24
|
+
stdout_cap_bytes: 1_024,
|
|
25
|
+
stderr_cap_bytes: 1_024,
|
|
26
|
+
digest_chars: 120,
|
|
27
|
+
env_allowlist: EXEC_DEFAULTS.envAllowlist,
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('runExecSandbox: captures stdout, persists artifacts, returns digest', async () => {
|
|
33
|
+
const base = freshBase();
|
|
34
|
+
try {
|
|
35
|
+
const result = await runExecSandbox(
|
|
36
|
+
{ runtime: 'bash', script: 'echo hello world' },
|
|
37
|
+
baseOpts(base),
|
|
38
|
+
);
|
|
39
|
+
assert.equal(result.exit_code, 0);
|
|
40
|
+
assert.equal(result.timed_out, false);
|
|
41
|
+
assert.ok(result.digest.includes('hello world'), `digest should contain stdout: ${result.digest}`);
|
|
42
|
+
assert.ok(result.stdout_path.startsWith(join(base, '.gsd', 'exec')), 'stdout path under .gsd/exec');
|
|
43
|
+
assert.equal(readFileSync(result.stdout_path, 'utf-8').trim(), 'hello world');
|
|
44
|
+
const meta = JSON.parse(readFileSync(result.meta_path, 'utf-8')) as Record<string, unknown>;
|
|
45
|
+
assert.equal(meta.runtime, 'bash');
|
|
46
|
+
assert.equal(meta.exit_code, 0);
|
|
47
|
+
} finally {
|
|
48
|
+
cleanup(base);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('runExecSandbox: enforces stdout cap and marks truncation', async () => {
|
|
53
|
+
const base = freshBase();
|
|
54
|
+
try {
|
|
55
|
+
const result = await runExecSandbox(
|
|
56
|
+
// Emit far more than the cap so truncation triggers.
|
|
57
|
+
{ runtime: 'bash', script: 'head -c 8000 /dev/urandom | base64' },
|
|
58
|
+
baseOpts(base, { stdout_cap_bytes: 256 }),
|
|
59
|
+
);
|
|
60
|
+
assert.equal(result.stdout_truncated, true, 'should mark stdout truncated');
|
|
61
|
+
assert.ok(result.stdout_bytes <= 256, `stdout_bytes within cap (got ${result.stdout_bytes})`);
|
|
62
|
+
const stdout = readFileSync(result.stdout_path, 'utf-8');
|
|
63
|
+
assert.ok(stdout.endsWith('[truncated: stdout cap reached]\n'), 'truncation marker appended');
|
|
64
|
+
} finally {
|
|
65
|
+
cleanup(base);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('runExecSandbox: enforces timeout and surfaces timed_out', async () => {
|
|
70
|
+
const base = freshBase();
|
|
71
|
+
try {
|
|
72
|
+
const started = Date.now();
|
|
73
|
+
const result = await runExecSandbox(
|
|
74
|
+
{ runtime: 'bash', script: 'sleep 10' },
|
|
75
|
+
baseOpts(base, { default_timeout_ms: 150, clamp_timeout_ms: 150 }),
|
|
76
|
+
);
|
|
77
|
+
const elapsed = Date.now() - started;
|
|
78
|
+
assert.equal(result.timed_out, true);
|
|
79
|
+
assert.ok(elapsed < 5_000, `should return well before 10s (took ${elapsed}ms)`);
|
|
80
|
+
} finally {
|
|
81
|
+
cleanup(base);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('runExecSandbox: forwards only allowlisted env vars', async () => {
|
|
86
|
+
const base = freshBase();
|
|
87
|
+
try {
|
|
88
|
+
const result = await runExecSandbox(
|
|
89
|
+
{ runtime: 'bash', script: 'echo PATH=$PATH SECRET=$GSD_TEST_SECRET' },
|
|
90
|
+
baseOpts(base, {
|
|
91
|
+
env_allowlist: [],
|
|
92
|
+
env: { PATH: '/usr/bin:/bin', HOME: '/tmp', GSD_TEST_SECRET: 'should-be-blocked' },
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
const stdout = readFileSync(result.stdout_path, 'utf-8');
|
|
96
|
+
assert.ok(stdout.includes('PATH=/usr/bin:/bin'), 'PATH forwarded');
|
|
97
|
+
assert.ok(!stdout.includes('should-be-blocked'), 'non-allowlisted var blocked');
|
|
98
|
+
} finally {
|
|
99
|
+
cleanup(base);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('runExecSandbox: node runtime executes JS', async () => {
|
|
104
|
+
const base = freshBase();
|
|
105
|
+
try {
|
|
106
|
+
const result = await runExecSandbox(
|
|
107
|
+
{ runtime: 'node', script: 'console.log("node-ok:" + (1+2))' },
|
|
108
|
+
baseOpts(base),
|
|
109
|
+
);
|
|
110
|
+
assert.equal(result.exit_code, 0);
|
|
111
|
+
assert.ok(result.digest.includes('node-ok:3'));
|
|
112
|
+
} finally {
|
|
113
|
+
cleanup(base);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── exec-tool executor ────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
test('executeGsdExec: runs by default when context_mode is unset', async () => {
|
|
120
|
+
const base = freshBase();
|
|
121
|
+
try {
|
|
122
|
+
const result = await executeGsdExec(
|
|
123
|
+
{ runtime: 'bash', script: 'echo default-on-run' },
|
|
124
|
+
{ baseDir: base, preferences: {} },
|
|
125
|
+
);
|
|
126
|
+
assert.ok(!result.isError, 'should succeed with no preferences');
|
|
127
|
+
assert.equal(result.details.operation, 'gsd_exec');
|
|
128
|
+
assert.equal(result.details.exit_code, 0);
|
|
129
|
+
assert.ok(result.content[0].text.includes('default-on-run'));
|
|
130
|
+
} finally {
|
|
131
|
+
cleanup(base);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('executeGsdExec: runs when preferences is null (fresh project)', async () => {
|
|
136
|
+
const base = freshBase();
|
|
137
|
+
try {
|
|
138
|
+
const result = await executeGsdExec(
|
|
139
|
+
{ runtime: 'bash', script: 'echo null-prefs-run' },
|
|
140
|
+
{ baseDir: base, preferences: null },
|
|
141
|
+
);
|
|
142
|
+
assert.ok(!result.isError, 'null preferences should not disable');
|
|
143
|
+
assert.ok(result.content[0].text.includes('null-prefs-run'));
|
|
144
|
+
} finally {
|
|
145
|
+
cleanup(base);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('executeGsdExec: blocked only when context_mode.enabled=false', async () => {
|
|
150
|
+
const base = freshBase();
|
|
151
|
+
try {
|
|
152
|
+
const result = await executeGsdExec(
|
|
153
|
+
{ runtime: 'bash', script: 'echo should-not-run' },
|
|
154
|
+
{ baseDir: base, preferences: { context_mode: { enabled: false } } },
|
|
155
|
+
);
|
|
156
|
+
assert.equal(result.isError, true);
|
|
157
|
+
assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
|
|
158
|
+
} finally {
|
|
159
|
+
cleanup(base);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('executeGsdExec: runs when enabled explicitly set to true', async () => {
|
|
164
|
+
const base = freshBase();
|
|
165
|
+
try {
|
|
166
|
+
const result = await executeGsdExec(
|
|
167
|
+
{ runtime: 'bash', script: 'echo explicit-on' },
|
|
168
|
+
{ baseDir: base, preferences: { context_mode: { enabled: true } } },
|
|
169
|
+
);
|
|
170
|
+
assert.ok(!result.isError);
|
|
171
|
+
assert.ok(result.content[0].text.includes('explicit-on'));
|
|
172
|
+
} finally {
|
|
173
|
+
cleanup(base);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('executeGsdExec: rejects empty script', async () => {
|
|
178
|
+
const base = freshBase();
|
|
179
|
+
try {
|
|
180
|
+
const result = await executeGsdExec(
|
|
181
|
+
{ runtime: 'bash', script: ' ' },
|
|
182
|
+
{ baseDir: base, preferences: { context_mode: { enabled: true } } },
|
|
183
|
+
);
|
|
184
|
+
assert.equal(result.isError, true);
|
|
185
|
+
assert.equal((result.details as { error?: string }).error, 'invalid_params');
|
|
186
|
+
} finally {
|
|
187
|
+
cleanup(base);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('isContextModeEnabled: defaults to true; only explicit false disables', () => {
|
|
192
|
+
assert.equal(isContextModeEnabled(undefined), true, 'undefined prefs → on');
|
|
193
|
+
assert.equal(isContextModeEnabled(null), true, 'null prefs → on');
|
|
194
|
+
assert.equal(isContextModeEnabled({}), true, 'empty prefs → on');
|
|
195
|
+
assert.equal(isContextModeEnabled({ context_mode: {} }), true, 'empty block → on');
|
|
196
|
+
assert.equal(isContextModeEnabled({ context_mode: { enabled: true } }), true);
|
|
197
|
+
assert.equal(isContextModeEnabled({ context_mode: { enabled: false } }), false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('buildExecOptions: clamps out-of-range values to safe defaults', () => {
|
|
201
|
+
const opts = buildExecOptions('/tmp/base', {
|
|
202
|
+
enabled: true,
|
|
203
|
+
exec_timeout_ms: 999_999_999,
|
|
204
|
+
exec_stdout_cap_bytes: 1,
|
|
205
|
+
exec_digest_chars: -20,
|
|
206
|
+
});
|
|
207
|
+
assert.equal(opts.default_timeout_ms, EXEC_DEFAULTS.clampTimeoutMs, 'timeout clamped to upper bound');
|
|
208
|
+
assert.equal(opts.stdout_cap_bytes, 4_096, 'stdout cap clamped to floor');
|
|
209
|
+
assert.equal(opts.digest_chars, 0, 'digest chars clamped to floor');
|
|
210
|
+
});
|
|
@@ -15,6 +15,26 @@ function git(cwd: string, ...args: string[]): string {
|
|
|
15
15
|
}).trim();
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
test("validateFileChanges works on repos with a single commit (no HEAD~1)", (t) => {
|
|
19
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
|
|
20
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
21
|
+
|
|
22
|
+
git(base, "init");
|
|
23
|
+
git(base, "config", "user.email", "test@example.com");
|
|
24
|
+
git(base, "config", "user.name", "Test User");
|
|
25
|
+
|
|
26
|
+
writeFileSync(join(base, "foo.ts"), "export const x = 1;\n");
|
|
27
|
+
git(base, "add", ".");
|
|
28
|
+
git(base, "commit", "-m", "initial");
|
|
29
|
+
|
|
30
|
+
// With only one commit, HEAD~1 doesn't exist — this must not throw
|
|
31
|
+
const audit = validateFileChanges(base, ["foo.ts"], []);
|
|
32
|
+
|
|
33
|
+
assert.ok(audit, "audit should be produced for single-commit repo");
|
|
34
|
+
assert.deepEqual(audit.unexpectedFiles, []);
|
|
35
|
+
assert.deepEqual(audit.missingFiles, []);
|
|
36
|
+
});
|
|
37
|
+
|
|
18
38
|
test("validateFileChanges ignores inline descriptions in expected output paths", (t) => {
|
|
19
39
|
const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
|
|
20
40
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|