gsd-pi 2.76.0-dev.b072ebb73 → 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/phases.js +4 -1
- package/dist/resources/extensions/gsd/auto/session.js +4 -0
- package/dist/resources/extensions/gsd/auto-model-selection.js +39 -13
- package/dist/resources/extensions/gsd/auto-start.js +39 -21
- package/dist/resources/extensions/gsd/auto.js +15 -12
- package/dist/resources/extensions/gsd/blocked-models.js +68 -0
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +76 -0
- 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/complexity-classifier.js +5 -3
- package/dist/resources/extensions/gsd/error-classifier.js +31 -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/prompt-loader.js +22 -7
- 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/resources/extensions/search-the-web/command-search-provider.js +5 -4
- package/dist/resources/extensions/search-the-web/native-search.js +45 -13
- 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 +8 -8
- 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 +8 -8
- 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-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
- package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
- package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
- package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +76 -10
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +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/provider-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.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/model-discovery.test.ts +19 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +86 -10
- 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/provider-manager.ts +16 -7
- 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/scripts/link-workspace-packages.cjs +1 -0
- 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/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/phases.ts +4 -0
- package/src/resources/extensions/gsd/auto/session.ts +7 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +50 -12
- package/src/resources/extensions/gsd/auto-start.ts +40 -22
- package/src/resources/extensions/gsd/auto.ts +15 -12
- package/src/resources/extensions/gsd/blocked-models.ts +98 -0
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +97 -0
- 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/complexity-classifier.ts +5 -3
- package/src/resources/extensions/gsd/error-classifier.ts +36 -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/prompt-loader.ts +30 -7
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
- package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/blocked-models.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
- 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/prompt-loader-extension-dir.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +91 -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/src/resources/extensions/search-the-web/command-search-provider.ts +5 -4
- package/src/resources/extensions/search-the-web/native-search.ts +48 -12
- /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → 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
|
|
@@ -24,6 +24,35 @@ import { fileURLToPath } from "node:url";
|
|
|
24
24
|
import { homedir } from "node:os";
|
|
25
25
|
import { logWarning } from "./workflow-logger.js";
|
|
26
26
|
|
|
27
|
+
type ExistsFn = (path: string) => boolean;
|
|
28
|
+
|
|
29
|
+
function hasRequiredExtensionAssets(rootDir: string, exists: ExistsFn = existsSync): boolean {
|
|
30
|
+
return (
|
|
31
|
+
exists(join(rootDir, "prompts")) &&
|
|
32
|
+
exists(join(rootDir, "templates", "task-summary.md"))
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveExtensionDirFromCandidates(
|
|
37
|
+
moduleDir: string,
|
|
38
|
+
agentGsdDir: string,
|
|
39
|
+
exists: ExistsFn = existsSync,
|
|
40
|
+
): string {
|
|
41
|
+
const moduleUsable = hasRequiredExtensionAssets(moduleDir, exists);
|
|
42
|
+
const agentUsable = hasRequiredExtensionAssets(agentGsdDir, exists);
|
|
43
|
+
|
|
44
|
+
// Prefer the user-local extension tree when both are valid. This avoids
|
|
45
|
+
// leaking npm/global-install paths into prompts on Windows.
|
|
46
|
+
if (agentUsable) return agentGsdDir;
|
|
47
|
+
if (moduleUsable) return moduleDir;
|
|
48
|
+
|
|
49
|
+
// Degraded fallback: if required template is missing in both locations,
|
|
50
|
+
// keep previous behavior and prefer whichever still has prompts/.
|
|
51
|
+
if (exists(join(moduleDir, "prompts"))) return moduleDir;
|
|
52
|
+
if (exists(join(agentGsdDir, "prompts"))) return agentGsdDir;
|
|
53
|
+
return moduleDir;
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
/**
|
|
28
57
|
* Resolve the GSD extension directory.
|
|
29
58
|
*
|
|
@@ -36,15 +65,9 @@ import { logWarning } from "./workflow-logger.js";
|
|
|
36
65
|
*/
|
|
37
66
|
function resolveExtensionDir(): string {
|
|
38
67
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
39
|
-
if (existsSync(join(moduleDir, "prompts"))) return moduleDir;
|
|
40
|
-
|
|
41
|
-
// Fallback: user-local agent directory
|
|
42
68
|
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
43
69
|
const agentGsdDir = join(gsdHome, "agent", "extensions", "gsd");
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Last resort: return the module dir (warmCache will silently handle the miss)
|
|
47
|
-
return moduleDir;
|
|
70
|
+
return resolveExtensionDirFromCandidates(moduleDir, agentGsdDir);
|
|
48
71
|
}
|
|
49
72
|
|
|
50
73
|
const __extensionDir = resolveExtensionDir();
|
|
@@ -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) : [];
|
|
@@ -341,6 +341,18 @@ test("dynamic routing passes provider-qualified model keys to the router", () =>
|
|
|
341
341
|
);
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
+
test("selectAndApplyModel re-applies captured thinking level after setModel success", () => {
|
|
345
|
+
const src = readFileSync(join(__dirname, "..", "auto-model-selection.ts"), "utf-8");
|
|
346
|
+
assert.ok(
|
|
347
|
+
src.includes("autoModeStartThinkingLevel?: ReturnType<ExtensionAPI[\"getThinkingLevel\"]> | null"),
|
|
348
|
+
"selectAndApplyModel should accept an autoModeStartThinkingLevel parameter",
|
|
349
|
+
);
|
|
350
|
+
assert.ok(
|
|
351
|
+
src.includes("reapplyThinkingLevel(pi, autoModeStartThinkingLevel)"),
|
|
352
|
+
"selectAndApplyModel should re-apply captured thinking level after model changes",
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
344
356
|
test("resolveModelId: anthropic wins over claude-code when session provider is not claude-code", () => {
|
|
345
357
|
const availableModels = [
|
|
346
358
|
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
|
@@ -52,9 +52,7 @@ test("bootstrapAutoSession checks manual session override before preferences", (
|
|
|
52
52
|
"manual override and preference fallback must be resolved before building startModelSnapshot",
|
|
53
53
|
);
|
|
54
54
|
|
|
55
|
-
//
|
|
56
|
-
// sources so PREFERENCES.md continues to win over a stale settings.json
|
|
57
|
-
// default for built-in providers.
|
|
55
|
+
// Preferred model should still be part of fallback resolution.
|
|
58
56
|
const snapshotBlock = source.slice(snapshotIdx, snapshotIdx + 400);
|
|
59
57
|
assert.ok(
|
|
60
58
|
snapshotBlock.includes("validatedPreferredModel") || snapshotBlock.includes("preferredModel"),
|
|
@@ -62,6 +60,22 @@ test("bootstrapAutoSession checks manual session override before preferences", (
|
|
|
62
60
|
);
|
|
63
61
|
});
|
|
64
62
|
|
|
63
|
+
test("bootstrapAutoSession prioritizes current session model over PREFERENCES.md default", () => {
|
|
64
|
+
const snapshotIdx = source.indexOf("const startModelSnapshot = manualSessionOverride");
|
|
65
|
+
assert.ok(snapshotIdx > -1, "auto-start.ts should build startModelSnapshot");
|
|
66
|
+
|
|
67
|
+
const snapshotBlock = source.slice(snapshotIdx, snapshotIdx + 500);
|
|
68
|
+
const currentIdx = snapshotBlock.indexOf("currentSessionModel");
|
|
69
|
+
const preferredIdx = snapshotBlock.indexOf("validatedPreferredModel");
|
|
70
|
+
|
|
71
|
+
assert.ok(currentIdx > -1, "startModelSnapshot should include currentSessionModel");
|
|
72
|
+
assert.ok(preferredIdx > -1, "startModelSnapshot should include validatedPreferredModel");
|
|
73
|
+
assert.ok(
|
|
74
|
+
currentIdx < preferredIdx,
|
|
75
|
+
"startModelSnapshot should prefer currentSessionModel before validatedPreferredModel",
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
65
79
|
test("bootstrapAutoSession prefers session model over PREFERENCES.md when provider is custom (#4122)", () => {
|
|
66
80
|
// Custom providers (Ollama, vLLM, OpenAI-compatible proxies) live in
|
|
67
81
|
// ~/.gsd/agent/models.json, not PREFERENCES.md. When the user picks one
|
|
@@ -111,3 +125,19 @@ test("bootstrapAutoSession validates preferred model against live registry auth
|
|
|
111
125
|
const warningIdx = source.indexOf("is not configured; falling back to session default");
|
|
112
126
|
assert.ok(warningIdx > -1, "auto-start.ts should warn when preferred model is unconfigured");
|
|
113
127
|
});
|
|
128
|
+
|
|
129
|
+
test("bootstrapAutoSession snapshots and persists thinking level for auto-mode lifecycle", () => {
|
|
130
|
+
const captureIdx = source.indexOf("const startThinkingSnapshot = pi.getThinkingLevel()");
|
|
131
|
+
assert.ok(captureIdx > -1, "auto-start.ts should snapshot thinking level at bootstrap start");
|
|
132
|
+
|
|
133
|
+
const originalThinkingIdx = source.indexOf("s.originalThinkingLevel = startThinkingSnapshot ?? null");
|
|
134
|
+
assert.ok(originalThinkingIdx > -1, "auto-start.ts should store originalThinkingLevel from snapshot");
|
|
135
|
+
|
|
136
|
+
const autoThinkingIdx = source.indexOf("s.autoModeStartThinkingLevel = startThinkingSnapshot ?? null");
|
|
137
|
+
assert.ok(autoThinkingIdx > -1, "auto-start.ts should store autoModeStartThinkingLevel from snapshot");
|
|
138
|
+
|
|
139
|
+
assert.ok(
|
|
140
|
+
captureIdx < originalThinkingIdx && captureIdx < autoThinkingIdx,
|
|
141
|
+
"thinking snapshot must be captured before session state assignment",
|
|
142
|
+
);
|
|
143
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const autoSrc = readFileSync(join(import.meta.dirname, "..", "auto.ts"), "utf-8");
|
|
7
|
+
const phasesSrc = readFileSync(join(import.meta.dirname, "..", "auto", "phases.ts"), "utf-8");
|
|
8
|
+
|
|
9
|
+
test("stopAuto restores original thinking level", () => {
|
|
10
|
+
assert.ok(
|
|
11
|
+
autoSrc.includes("if (pi && s.originalThinkingLevel)"),
|
|
12
|
+
"auto.ts should conditionally restore original thinking level in stopAuto",
|
|
13
|
+
);
|
|
14
|
+
assert.ok(
|
|
15
|
+
autoSrc.includes("pi.setThinkingLevel(s.originalThinkingLevel)"),
|
|
16
|
+
"auto.ts should call pi.setThinkingLevel with originalThinkingLevel",
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("runUnitPhase threads captured thinking level into selectAndApplyModel", () => {
|
|
21
|
+
const callIdx = phasesSrc.indexOf("deps.selectAndApplyModel(");
|
|
22
|
+
assert.ok(callIdx > -1, "phases.ts should call selectAndApplyModel");
|
|
23
|
+
const callBlock = phasesSrc.slice(callIdx, callIdx + 600);
|
|
24
|
+
assert.ok(
|
|
25
|
+
callBlock.includes("s.autoModeStartThinkingLevel"),
|
|
26
|
+
"runUnitPhase should pass autoModeStartThinkingLevel to selectAndApplyModel",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("hook model override preserves captured thinking level", () => {
|
|
31
|
+
const hookIdx = phasesSrc.indexOf("const hookModelOverride = sidecarItem?.model ?? iterData.hookModelOverride;");
|
|
32
|
+
assert.ok(hookIdx > -1, "phases.ts should include hook model override handling");
|
|
33
|
+
const hookBlock = phasesSrc.slice(hookIdx, hookIdx + 600);
|
|
34
|
+
assert.ok(
|
|
35
|
+
hookBlock.includes("pi.setThinkingLevel(s.autoModeStartThinkingLevel)"),
|
|
36
|
+
"hook model override should re-apply captured thinking level after setModel",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// GSD — Tests for persistent blocked-models store (issue #4513)
|
|
2
|
+
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
blockModel,
|
|
11
|
+
isModelBlocked,
|
|
12
|
+
loadBlockedModels,
|
|
13
|
+
} from "../blocked-models.ts";
|
|
14
|
+
|
|
15
|
+
function mkBase(): string {
|
|
16
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-blocked-models-"));
|
|
17
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
18
|
+
return base;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("blocked-models: round-trip write and read", () => {
|
|
22
|
+
const base = mkBase();
|
|
23
|
+
try {
|
|
24
|
+
assert.equal(isModelBlocked(base, "openai-codex", "gpt-5.1-codex-max"), false);
|
|
25
|
+
blockModel(base, "openai-codex", "gpt-5.1-codex-max", "not supported for ChatGPT account");
|
|
26
|
+
assert.equal(isModelBlocked(base, "openai-codex", "gpt-5.1-codex-max"), true);
|
|
27
|
+
|
|
28
|
+
const entries = loadBlockedModels(base);
|
|
29
|
+
assert.equal(entries.length, 1);
|
|
30
|
+
assert.equal(entries[0].provider, "openai-codex");
|
|
31
|
+
assert.equal(entries[0].id, "gpt-5.1-codex-max");
|
|
32
|
+
assert.ok(entries[0].blockedAt > 0);
|
|
33
|
+
} finally {
|
|
34
|
+
rmSync(base, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("blocked-models: case-insensitive lookup", () => {
|
|
39
|
+
const base = mkBase();
|
|
40
|
+
try {
|
|
41
|
+
blockModel(base, "OpenAI-Codex", "GPT-5.1-Codex-Max", "reason");
|
|
42
|
+
assert.equal(isModelBlocked(base, "openai-codex", "gpt-5.1-codex-max"), true);
|
|
43
|
+
assert.equal(isModelBlocked(base, "OPENAI-CODEX", "GPT-5.1-CODEX-MAX"), true);
|
|
44
|
+
} finally {
|
|
45
|
+
rmSync(base, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("blocked-models: dedupes repeated blocks", () => {
|
|
50
|
+
const base = mkBase();
|
|
51
|
+
try {
|
|
52
|
+
blockModel(base, "openai-codex", "gpt-5", "first");
|
|
53
|
+
blockModel(base, "openai-codex", "gpt-5", "second");
|
|
54
|
+
blockModel(base, "openai-codex", "gpt-5", "third");
|
|
55
|
+
assert.equal(loadBlockedModels(base).length, 1);
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(base, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("blocked-models: corrupted JSON recovers to empty", () => {
|
|
62
|
+
const base = mkBase();
|
|
63
|
+
try {
|
|
64
|
+
const path = join(base, ".gsd", "runtime", "blocked-models.json");
|
|
65
|
+
mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
|
|
66
|
+
writeFileSync(path, "{not valid json", "utf-8");
|
|
67
|
+
|
|
68
|
+
assert.equal(loadBlockedModels(base).length, 0);
|
|
69
|
+
assert.equal(isModelBlocked(base, "any", "model"), false);
|
|
70
|
+
|
|
71
|
+
// A subsequent write should still succeed (overwrites the corrupt file).
|
|
72
|
+
blockModel(base, "openai-codex", "gpt-5", "reason");
|
|
73
|
+
assert.equal(loadBlockedModels(base).length, 1);
|
|
74
|
+
} finally {
|
|
75
|
+
rmSync(base, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("blocked-models: returns false for missing provider or id", () => {
|
|
80
|
+
const base = mkBase();
|
|
81
|
+
try {
|
|
82
|
+
blockModel(base, "openai-codex", "gpt-5", "reason");
|
|
83
|
+
assert.equal(isModelBlocked(base, undefined, "gpt-5"), false);
|
|
84
|
+
assert.equal(isModelBlocked(base, "openai-codex", undefined), false);
|
|
85
|
+
} finally {
|
|
86
|
+
rmSync(base, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("blocked-models: file created under .gsd/runtime/", () => {
|
|
91
|
+
const base = mkBase();
|
|
92
|
+
try {
|
|
93
|
+
blockModel(base, "openai-codex", "gpt-5", "reason");
|
|
94
|
+
assert.ok(existsSync(join(base, ".gsd", "runtime", "blocked-models.json")));
|
|
95
|
+
} finally {
|
|
96
|
+
rmSync(base, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -21,9 +21,9 @@ test("tierOrdinal returns correct ordering", () => {
|
|
|
21
21
|
|
|
22
22
|
// ─── Unit Type Classification ────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
-
test("complete-slice classifies as
|
|
24
|
+
test("complete-slice classifies as standard", () => {
|
|
25
25
|
const result = classifyUnitComplexity("complete-slice", "M001/S01", "/tmp/fake");
|
|
26
|
-
assert.equal(result.tier, "
|
|
26
|
+
assert.equal(result.tier, "standard");
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
test("run-uat classifies as light", () => {
|
|
@@ -145,7 +145,7 @@ test("budget pressure at 90% downgrades standard to light", () => {
|
|
|
145
145
|
assert.equal(result.downgraded, true);
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
test("budget pressure at 90% downgrades
|
|
148
|
+
test("budget pressure at 90% downgrades complete-slice standard to light", () => {
|
|
149
149
|
const result = classifyUnitComplexity("complete-slice", "M001/S01", "/tmp/fake", 0.95);
|
|
150
150
|
assert.equal(result.tier, "light");
|
|
151
151
|
});
|
|
@@ -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
|
+
});
|