gsd-pi 2.74.0-dev.703eabc → 2.74.0-dev.b2838e6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/gsd/auto/phases.js +51 -6
- package/dist/resources/extensions/gsd/auto-model-selection.js +3 -3
- package/dist/resources/extensions/gsd/auto-recovery.js +24 -10
- package/dist/resources/extensions/gsd/auto-worktree.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +5 -3
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +16 -5
- package/dist/resources/extensions/gsd/cache.js +16 -5
- package/dist/resources/extensions/gsd/commands/catalog.js +6 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +5 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +50 -3
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -0
- package/dist/resources/extensions/gsd/guided-flow.js +8 -6
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +10 -0
- package/dist/resources/extensions/gsd/preferences.js +5 -0
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +15 -30
- package/dist/resources/extensions/gsd/templates/PREFERENCES.md +1 -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 +13 -13
- 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 +13 -13
- 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/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +88 -6
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.ts +95 -10
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.js +61 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.js.map +1 -0
- 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 +9 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.d.ts +8 -5
- package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js +27 -13
- package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +8 -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 +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- 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 +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.ts +92 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +12 -4
- package/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +36 -15
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +17 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +19 -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 +9 -2
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +9 -1
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +70 -6
- package/src/resources/extensions/gsd/auto-model-selection.ts +3 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +29 -9
- package/src/resources/extensions/gsd/auto-worktree.ts +1 -0
- package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +5 -3
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +16 -5
- package/src/resources/extensions/gsd/cache.ts +16 -5
- package/src/resources/extensions/gsd/commands/catalog.ts +6 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +5 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +57 -3
- package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -0
- package/src/resources/extensions/gsd/guided-flow.ts +4 -2
- package/src/resources/extensions/gsd/preferences-types.ts +6 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +10 -0
- package/src/resources/extensions/gsd/preferences.ts +6 -0
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +15 -31
- package/src/resources/extensions/gsd/templates/PREFERENCES.md +1 -0
- package/src/resources/extensions/gsd/tests/artifacts-table-preserved-on-cache-invalidate.test.ts +177 -0
- package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +272 -0
- package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/preferences.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +57 -2
- /package/dist/web/standalone/.next/static/{3U-oZ5FT59BM7sm2GInic → wuiYdNtJdo9ISED55DAkz}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{3U-oZ5FT59BM7sm2GInic → wuiYdNtJdo9ISED55DAkz}/_ssgManifest.js +0 -0
|
@@ -4,7 +4,7 @@ import type { GSDState } from "../../types.js";
|
|
|
4
4
|
|
|
5
5
|
import { computeProgressScore, formatProgressLine } from "../../progress-score.js";
|
|
6
6
|
import { loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath, getProjectGSDPreferencesPath } from "../../preferences.js";
|
|
7
|
-
import { ensurePreferencesFile, handlePrefs, handlePrefsMode, handlePrefsWizard } from "../../commands-prefs-wizard.js";
|
|
7
|
+
import { ensurePreferencesFile, handlePrefs, handlePrefsMode, handlePrefsWizard, handleLanguage } from "../../commands-prefs-wizard.js";
|
|
8
8
|
import { runEnvironmentChecks } from "../../doctor-environment.js";
|
|
9
9
|
import { deriveState } from "../../state.js";
|
|
10
10
|
import { handleCmux } from "../../commands-cmux.js";
|
|
@@ -395,6 +395,10 @@ export async function handleCoreCommand(
|
|
|
395
395
|
await handlePrefs(trimmed.replace(/^prefs\s*/, "").trim(), ctx);
|
|
396
396
|
return true;
|
|
397
397
|
}
|
|
398
|
+
if (trimmed === "language" || trimmed.startsWith("language ")) {
|
|
399
|
+
await handleLanguage(trimmed.replace(/^language\s*/, "").trim(), ctx);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
398
402
|
if (trimmed === "cmux" || trimmed.startsWith("cmux ")) {
|
|
399
403
|
await handleCmux(trimmed.replace(/^cmux\s*/, "").trim(), ctx);
|
|
400
404
|
return true;
|
|
@@ -756,8 +756,8 @@ export async function handlePrefsWizard(
|
|
|
756
756
|
/** Wrap a YAML value in double quotes if it contains special characters. */
|
|
757
757
|
export function yamlSafeString(val: unknown): string {
|
|
758
758
|
if (typeof val !== "string") return String(val);
|
|
759
|
-
if (/[:#{\[\]'"
|
|
760
|
-
return `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
759
|
+
if (/[:#{\[\]'"`,|>&*!?@%\r\n]/.test(val) || val.trim() !== val || val === "") {
|
|
760
|
+
return `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n")}"`;
|
|
761
761
|
}
|
|
762
762
|
return val;
|
|
763
763
|
}
|
|
@@ -825,7 +825,7 @@ export function serializePreferencesToFrontmatter(prefs: Record<string, unknown>
|
|
|
825
825
|
"dynamic_routing", "uok", "token_profile", "phases", "parallel",
|
|
826
826
|
"auto_visualize", "auto_report",
|
|
827
827
|
"verification_commands", "verification_auto_fix", "verification_max_retries",
|
|
828
|
-
"search_provider", "context_selection",
|
|
828
|
+
"search_provider", "context_selection", "language",
|
|
829
829
|
];
|
|
830
830
|
|
|
831
831
|
const seen = new Set<string>();
|
|
@@ -862,3 +862,57 @@ export async function ensurePreferencesFile(
|
|
|
862
862
|
ctx.ui.notify(`Using existing ${scope} GSD skill preferences at ${path}`, "info");
|
|
863
863
|
}
|
|
864
864
|
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Handle `/gsd language [code]` — set or clear the global language preference.
|
|
868
|
+
* Without an argument, shows the current setting.
|
|
869
|
+
* Project-level override can be set by editing `.gsd/preferences.md` directly
|
|
870
|
+
* (project language overrides global when both are set).
|
|
871
|
+
*/
|
|
872
|
+
export async function handleLanguage(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
873
|
+
const path = getGlobalGSDPreferencesPath();
|
|
874
|
+
const lang = args.trim();
|
|
875
|
+
|
|
876
|
+
// Show current setting when called without argument
|
|
877
|
+
if (!lang) {
|
|
878
|
+
const loaded = loadGlobalGSDPreferences();
|
|
879
|
+
const current = loaded?.preferences.language;
|
|
880
|
+
if (current) {
|
|
881
|
+
ctx.ui.notify(`Current language preference: ${current}\nUse /gsd language <name> to change, or /gsd language off to clear.`, "info");
|
|
882
|
+
} else {
|
|
883
|
+
ctx.ui.notify("No language preference set. Use /gsd language <name> to set one (e.g. /gsd language Chinese).", "info");
|
|
884
|
+
}
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Ensure preferences file exists with the canonical template
|
|
889
|
+
await ensurePreferencesFile(path, ctx, "global");
|
|
890
|
+
|
|
891
|
+
// Read via the same validated path as other handlers
|
|
892
|
+
const existing = loadGlobalGSDPreferences();
|
|
893
|
+
const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : { version: 1 };
|
|
894
|
+
|
|
895
|
+
if (lang === "off" || lang === "none" || lang === "clear") {
|
|
896
|
+
delete prefs.language;
|
|
897
|
+
ctx.ui.notify("Language preference cleared. GSD will use the default language.", "info");
|
|
898
|
+
} else {
|
|
899
|
+
// Validate before writing — reject values that would fail on next load
|
|
900
|
+
if (lang.length > 50 || /[\r\n]/.test(lang)) {
|
|
901
|
+
ctx.ui.notify(
|
|
902
|
+
"Language value must be 50 characters or fewer with no newlines (e.g. /gsd language Chinese).",
|
|
903
|
+
"warning",
|
|
904
|
+
);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
prefs.language = lang;
|
|
908
|
+
ctx.ui.notify(`Language preference set to: ${lang}\nGSD will now respond in ${lang} across all sessions.`, "info");
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const rawContent = existsSync(path) ? readFileSync(path, "utf-8") : `---\nversion: 1\n---\n`;
|
|
912
|
+
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
913
|
+
const body = extractBodyAfterFrontmatter(rawContent)
|
|
914
|
+
?? "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
915
|
+
await saveFile(path, `---\n${frontmatter}---${body}`);
|
|
916
|
+
await ctx.waitForIdle();
|
|
917
|
+
await ctx.reload();
|
|
918
|
+
}
|
|
@@ -102,6 +102,8 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
102
102
|
|
|
103
103
|
- `custom_instructions`: extra durable instructions related to skill use. For operational project knowledge (recurring rules, gotchas, patterns), use `.gsd/KNOWLEDGE.md` instead — it's injected into every agent prompt automatically and agents can append to it during execution.
|
|
104
104
|
|
|
105
|
+
- `language`: preferred response language for all GSD interactions. Accepts any language name or code — `"Chinese"`, `"zh"`, `"German"`, `"de"`, `"日本語"`, etc. When set, GSD injects "Always respond in \<language\>" into every agent's system prompt, including after `/clear`. Quickest way to set it: `/gsd language <name>`. To clear: `/gsd language off`.
|
|
106
|
+
|
|
105
107
|
- `models`: per-stage model selection (applies to both auto-mode and guided-flow dispatches). Keys: `research`, `planning`, `discuss`, `execution`, `execution_simple`, `completion`, `validation`, `subagent`. Values can be:
|
|
106
108
|
- Simple string: `"claude-sonnet-4-6"` — single model, no fallbacks
|
|
107
109
|
- Provider-qualified string: `"bedrock/claude-sonnet-4-6"` — targets a specific provider when the same model ID exists across multiple providers
|
|
@@ -269,10 +269,12 @@ export function checkAutoStartAfterDiscuss(): boolean {
|
|
|
269
269
|
} catch (e) { logWarning("guided", `CONTEXT-DRAFT.md unlink failed: ${(e as Error).message}`); }
|
|
270
270
|
|
|
271
271
|
// Cleanup: remove discussion manifest after auto-start (only needed during discussion)
|
|
272
|
-
|
|
272
|
+
if (existsSync(manifestPath)) {
|
|
273
|
+
try { unlinkSync(manifestPath); } catch (e) { logWarning("guided", `manifest unlink failed: ${(e as Error).message}`); }
|
|
274
|
+
}
|
|
273
275
|
|
|
274
276
|
pendingAutoStartMap.delete(basePath);
|
|
275
|
-
ctx.ui.notify(`Milestone ${milestoneId} ready.`, "
|
|
277
|
+
ctx.ui.notify(`Milestone ${milestoneId} ready.`, "success");
|
|
276
278
|
startAutoDetached(ctx, pi, basePath, false, { step });
|
|
277
279
|
return true;
|
|
278
280
|
}
|
|
@@ -115,6 +115,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
115
115
|
"discuss_web_research",
|
|
116
116
|
"discuss_depth",
|
|
117
117
|
"flat_rate_providers",
|
|
118
|
+
"language",
|
|
118
119
|
]);
|
|
119
120
|
|
|
120
121
|
/** Canonical list of all dispatch unit types. */
|
|
@@ -403,6 +404,11 @@ export interface GSDPreferences {
|
|
|
403
404
|
* same regardless of model. Case-insensitive.
|
|
404
405
|
*/
|
|
405
406
|
flat_rate_providers?: string[];
|
|
407
|
+
/**
|
|
408
|
+
* Language preference for GSD responses. Accepts any language name or code
|
|
409
|
+
* (e.g. "Chinese", "zh", "German", "de", "日本語"). Persists across /clear.
|
|
410
|
+
*/
|
|
411
|
+
language?: string;
|
|
406
412
|
}
|
|
407
413
|
|
|
408
414
|
export interface LoadedGSDPreferences {
|
|
@@ -1107,5 +1107,15 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
1107
1107
|
}
|
|
1108
1108
|
}
|
|
1109
1109
|
|
|
1110
|
+
// ─── Language ────────────────────────────────────────────────────────
|
|
1111
|
+
if (preferences.language !== undefined) {
|
|
1112
|
+
const trimmed = typeof preferences.language === "string" ? preferences.language.trim() : undefined;
|
|
1113
|
+
if (trimmed && trimmed.length <= 50 && !/[\r\n]/.test(trimmed)) {
|
|
1114
|
+
validated.language = trimmed;
|
|
1115
|
+
} else {
|
|
1116
|
+
errors.push(`language must be a non-empty string up to 50 characters with no newlines (e.g. "Chinese", "de", "日本語")`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1110
1120
|
return { preferences: validated, errors, warnings };
|
|
1111
1121
|
}
|
|
@@ -447,6 +447,7 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
|
|
|
447
447
|
slice_parallel: (base.slice_parallel || override.slice_parallel)
|
|
448
448
|
? { ...(base.slice_parallel ?? {}), ...(override.slice_parallel ?? {}) }
|
|
449
449
|
: undefined,
|
|
450
|
+
language: override.language ?? base.language,
|
|
450
451
|
};
|
|
451
452
|
}
|
|
452
453
|
|
|
@@ -562,6 +563,11 @@ export function renderPreferencesForSystemPrompt(preferences: GSDPreferences, re
|
|
|
562
563
|
}
|
|
563
564
|
}
|
|
564
565
|
|
|
566
|
+
if (preferences.language) {
|
|
567
|
+
const safeLang = preferences.language.replace(/[\r\n]/g, " ").slice(0, 50);
|
|
568
|
+
lines.push(`- Language: Always respond in ${safeLang}.`);
|
|
569
|
+
}
|
|
570
|
+
|
|
565
571
|
return lines.join("\n");
|
|
566
572
|
}
|
|
567
573
|
|
|
@@ -68,11 +68,11 @@ export function getFilePaths(): string[] {
|
|
|
68
68
|
* Record a tool call at dispatch time (before execution).
|
|
69
69
|
* Exit codes and output are filled in by recordToolResult after execution.
|
|
70
70
|
*/
|
|
71
|
-
export function recordToolCall(toolName: string, input: Record<string, unknown>): void {
|
|
71
|
+
export function recordToolCall(toolCallId: string, toolName: string, input: Record<string, unknown>): void {
|
|
72
72
|
if (toolName === "bash" || toolName === "Bash") {
|
|
73
73
|
unitEvidence.push({
|
|
74
74
|
kind: "bash",
|
|
75
|
-
toolCallId
|
|
75
|
+
toolCallId,
|
|
76
76
|
command: String(input.command ?? ""),
|
|
77
77
|
exitCode: -1,
|
|
78
78
|
outputSnippet: "",
|
|
@@ -81,14 +81,14 @@ export function recordToolCall(toolName: string, input: Record<string, unknown>)
|
|
|
81
81
|
} else if (toolName === "write" || toolName === "Write") {
|
|
82
82
|
unitEvidence.push({
|
|
83
83
|
kind: "write",
|
|
84
|
-
toolCallId
|
|
84
|
+
toolCallId,
|
|
85
85
|
path: String(input.file_path ?? input.path ?? ""),
|
|
86
86
|
timestamp: Date.now(),
|
|
87
87
|
});
|
|
88
88
|
} else if (toolName === "edit" || toolName === "Edit") {
|
|
89
89
|
unitEvidence.push({
|
|
90
90
|
kind: "edit",
|
|
91
|
-
toolCallId
|
|
91
|
+
toolCallId,
|
|
92
92
|
path: String(input.file_path ?? input.path ?? ""),
|
|
93
93
|
timestamp: Date.now(),
|
|
94
94
|
});
|
|
@@ -96,8 +96,9 @@ export function recordToolCall(toolName: string, input: Record<string, unknown>)
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* Record a tool execution result. Matches the
|
|
100
|
-
*
|
|
99
|
+
* Record a tool execution result. Matches the entry by toolCallId (assigned
|
|
100
|
+
* at dispatch time) and fills in exit code + output. Prior versions matched
|
|
101
|
+
* by `kind + empty-string` which corrupted parallel tool calls.
|
|
101
102
|
*/
|
|
102
103
|
export function recordToolResult(
|
|
103
104
|
toolCallId: string,
|
|
@@ -105,36 +106,19 @@ export function recordToolResult(
|
|
|
105
106
|
result: unknown,
|
|
106
107
|
isError: boolean,
|
|
107
108
|
): void {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const exitMatch = text.match(/Command exited with code (\d+)/);
|
|
117
|
-
entry.exitCode = exitMatch ? Number(exitMatch[1]) : (isError ? 1 : 0);
|
|
118
|
-
}
|
|
119
|
-
} else if (normalizedName === "write" || normalizedName === "edit") {
|
|
120
|
-
const entry = findLastUnresolved(normalizedName as "write" | "edit");
|
|
121
|
-
if (entry) {
|
|
122
|
-
entry.toolCallId = toolCallId;
|
|
123
|
-
}
|
|
109
|
+
const entry = unitEvidence.find(e => e.toolCallId === toolCallId);
|
|
110
|
+
if (!entry) return;
|
|
111
|
+
|
|
112
|
+
if (entry.kind === "bash") {
|
|
113
|
+
const text = extractResultText(result);
|
|
114
|
+
entry.outputSnippet = text.slice(0, 500);
|
|
115
|
+
const exitMatch = text.match(/Command exited with code (\d+)/);
|
|
116
|
+
entry.exitCode = exitMatch ? Number(exitMatch[1]) : (isError ? 1 : 0);
|
|
124
117
|
}
|
|
125
118
|
}
|
|
126
119
|
|
|
127
120
|
// ─── Internals ──────────────────────────────────────────────────────────────
|
|
128
121
|
|
|
129
|
-
function findLastUnresolved(kind: string): EvidenceEntry | undefined {
|
|
130
|
-
for (let i = unitEvidence.length - 1; i >= 0; i--) {
|
|
131
|
-
if (unitEvidence[i].kind === kind && unitEvidence[i].toolCallId === "") {
|
|
132
|
-
return unitEvidence[i];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
122
|
function extractResultText(result: unknown): string {
|
|
139
123
|
if (typeof result === "string") return result;
|
|
140
124
|
if (result && typeof result === "object") {
|
package/src/resources/extensions/gsd/tests/artifacts-table-preserved-on-cache-invalidate.test.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test: invalidateAllCaches() must NOT wipe the artifacts table.
|
|
3
|
+
*
|
|
4
|
+
* Prior to this fix, `cache.ts` bundled `clearArtifacts()` (which runs
|
|
5
|
+
* `DELETE FROM artifacts`) into `invalidateAllCaches()`. That helper fires
|
|
6
|
+
* on every post-unit pass, so rows written by `saveArtifactToDb` and
|
|
7
|
+
* `writeAndStore` (RESEARCH, CONTEXT, VALIDATION, ASSESSMENT, PLAN,
|
|
8
|
+
* ROADMAP, task PLAN, task SUMMARY) got deleted within seconds of being
|
|
9
|
+
* written. The milestone completed on disk, but `SELECT COUNT(*) FROM
|
|
10
|
+
* artifacts` returned 0, and the agent fell into a "file exists but DB
|
|
11
|
+
* record missing" recovery loop.
|
|
12
|
+
*
|
|
13
|
+
* The artifacts table is a write-through store, not a read cache. Routine
|
|
14
|
+
* cache invalidation must preserve its contents.
|
|
15
|
+
*
|
|
16
|
+
* Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, test, afterEach } from "node:test";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
|
|
24
|
+
import { invalidateAllCaches } from "../cache.ts";
|
|
25
|
+
import {
|
|
26
|
+
openDatabase,
|
|
27
|
+
closeDatabase,
|
|
28
|
+
insertArtifact,
|
|
29
|
+
isDbAvailable,
|
|
30
|
+
_getAdapter,
|
|
31
|
+
} from "../gsd-db.ts";
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
if (isDbAvailable()) {
|
|
35
|
+
try {
|
|
36
|
+
closeDatabase();
|
|
37
|
+
} catch {
|
|
38
|
+
/* best-effort teardown */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("invalidateAllCaches() must preserve the artifacts table", () => {
|
|
44
|
+
test("rows survive a single invalidate call", () => {
|
|
45
|
+
const opened = openDatabase(":memory:");
|
|
46
|
+
assert.equal(opened, true, "in-memory DB must open");
|
|
47
|
+
|
|
48
|
+
insertArtifact({
|
|
49
|
+
path: "milestones/M001/slices/S01/S01-RESEARCH.md",
|
|
50
|
+
artifact_type: "RESEARCH",
|
|
51
|
+
milestone_id: "M001",
|
|
52
|
+
slice_id: "S01",
|
|
53
|
+
task_id: null,
|
|
54
|
+
full_content: "# Research\n\nFindings go here.\n",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
invalidateAllCaches();
|
|
58
|
+
|
|
59
|
+
const adapter = _getAdapter();
|
|
60
|
+
assert.ok(adapter, "adapter should be available");
|
|
61
|
+
const row = adapter!
|
|
62
|
+
.prepare(
|
|
63
|
+
"SELECT path, artifact_type, length(full_content) AS len FROM artifacts WHERE path = :path",
|
|
64
|
+
)
|
|
65
|
+
.get({ ":path": "milestones/M001/slices/S01/S01-RESEARCH.md" }) as
|
|
66
|
+
| { path: string; artifact_type: string; len: number }
|
|
67
|
+
| undefined;
|
|
68
|
+
|
|
69
|
+
assert.ok(
|
|
70
|
+
row,
|
|
71
|
+
"artifact row must still exist after invalidateAllCaches — this is the Phase B bug",
|
|
72
|
+
);
|
|
73
|
+
assert.equal(row!.artifact_type, "RESEARCH");
|
|
74
|
+
assert.ok((row!.len ?? 0) > 0, "full_content must not be truncated");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("multiple rows for a full milestone survive repeated invalidates", () => {
|
|
78
|
+
openDatabase(":memory:");
|
|
79
|
+
|
|
80
|
+
const inserts = [
|
|
81
|
+
{
|
|
82
|
+
path: "milestones/M001/M001-ROADMAP.md",
|
|
83
|
+
artifact_type: "ROADMAP",
|
|
84
|
+
milestone_id: "M001",
|
|
85
|
+
slice_id: null,
|
|
86
|
+
task_id: null,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
path: "milestones/M001/slices/S01/S01-RESEARCH.md",
|
|
90
|
+
artifact_type: "RESEARCH",
|
|
91
|
+
milestone_id: "M001",
|
|
92
|
+
slice_id: "S01",
|
|
93
|
+
task_id: null,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
path: "milestones/M001/slices/S01/S01-PLAN.md",
|
|
97
|
+
artifact_type: "PLAN",
|
|
98
|
+
milestone_id: "M001",
|
|
99
|
+
slice_id: "S01",
|
|
100
|
+
task_id: null,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
path: "milestones/M001/slices/S01/tasks/T01-PLAN.md",
|
|
104
|
+
artifact_type: "PLAN",
|
|
105
|
+
milestone_id: "M001",
|
|
106
|
+
slice_id: "S01",
|
|
107
|
+
task_id: "T01",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
path: "milestones/M001/slices/S01/tasks/T01-SUMMARY.md",
|
|
111
|
+
artifact_type: "SUMMARY",
|
|
112
|
+
milestone_id: "M001",
|
|
113
|
+
slice_id: "S01",
|
|
114
|
+
task_id: "T01",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
path: "milestones/M001/M001-SUMMARY.md",
|
|
118
|
+
artifact_type: "SUMMARY",
|
|
119
|
+
milestone_id: "M001",
|
|
120
|
+
slice_id: null,
|
|
121
|
+
task_id: null,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
for (const i of inserts) {
|
|
126
|
+
insertArtifact({ ...i, full_content: `# ${i.artifact_type} content\n` });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Simulate a full milestone's worth of post-unit cycles.
|
|
130
|
+
for (let i = 0; i < 10; i++) {
|
|
131
|
+
invalidateAllCaches();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const adapter = _getAdapter()!;
|
|
135
|
+
const count = (
|
|
136
|
+
adapter.prepare("SELECT COUNT(*) AS n FROM artifacts").get() as { n: number }
|
|
137
|
+
).n;
|
|
138
|
+
|
|
139
|
+
assert.equal(
|
|
140
|
+
count,
|
|
141
|
+
inserts.length,
|
|
142
|
+
`all ${inserts.length} artifact rows must survive repeated invalidate calls; got ${count}`,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("cache.ts must not re-import clearArtifacts into invalidateAllCaches", () => {
|
|
148
|
+
const src = readFileSync(
|
|
149
|
+
resolve(process.cwd(), "src", "resources", "extensions", "gsd", "cache.ts"),
|
|
150
|
+
"utf-8",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
test("clearArtifacts is not imported from gsd-db", () => {
|
|
154
|
+
assert.ok(
|
|
155
|
+
!/import\s*\{[^}]*clearArtifacts[^}]*\}\s*from\s*['"]\.\/gsd-db/.test(src),
|
|
156
|
+
"cache.ts must not import clearArtifacts — it causes the artifacts-table-wipe regression",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("invalidateAllCaches does not call clearArtifacts", () => {
|
|
161
|
+
const fnIdx = src.indexOf("function invalidateAllCaches");
|
|
162
|
+
assert.ok(fnIdx !== -1);
|
|
163
|
+
const body = src.slice(fnIdx, fnIdx + 1000);
|
|
164
|
+
assert.ok(
|
|
165
|
+
!/\bclearArtifacts\s*\(/.test(body),
|
|
166
|
+
"invalidateAllCaches must not call clearArtifacts() — it wipes the write-through store",
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("cache.ts documents why clearArtifacts is not bundled here", () => {
|
|
171
|
+
// Future reviewers need to see the rationale or they'll re-add it.
|
|
172
|
+
assert.ok(
|
|
173
|
+
/artifacts.*NOT included|write-through store/i.test(src),
|
|
174
|
+
"cache.ts must explain why the artifacts table is NOT invalidated here",
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
});
|