gsd-pi 2.17.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- package/dist/resources/extensions/gsd/auto-prompts.ts +20 -1
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +123 -10
- package/dist/resources/extensions/gsd/commands.ts +245 -22
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/dist/resources/extensions/gsd/files.ts +123 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/preferences.ts +59 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +22 -0
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -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 +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- 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 +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-prompts.ts +20 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +123 -10
- package/src/resources/extensions/gsd/commands.ts +245 -22
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/src/resources/extensions/gsd/files.ts +123 -1
- package/src/resources/extensions/gsd/guided-flow.ts +237 -4
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/preferences.ts +59 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +22 -0
- package/src/resources/extensions/shared/next-action-ui.ts +16 -1
|
@@ -28,10 +28,11 @@ import { createBashTool, createWriteTool, createReadTool, createEditTool, isTool
|
|
|
28
28
|
import { registerGSDCommand, loadToolApiKeys } from "./commands.js";
|
|
29
29
|
import { registerExitCommand } from "./exit-command.js";
|
|
30
30
|
import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
|
|
31
|
+
import { getActiveAutoWorktreeContext } from "./auto-worktree.js";
|
|
31
32
|
import { saveFile, formatContinue, loadFile, parseContinue, parseSummary, loadActiveOverrides, formatOverridesSection } from "./files.js";
|
|
32
33
|
import { loadPrompt } from "./prompt-loader.js";
|
|
33
34
|
import { deriveState } from "./state.js";
|
|
34
|
-
import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData } from "./auto.js";
|
|
35
|
+
import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData, markToolStart, markToolEnd } from "./auto.js";
|
|
35
36
|
import { saveActivityLog } from "./activity-log.js";
|
|
36
37
|
import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId } from "./guided-flow.js";
|
|
37
38
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
@@ -47,10 +48,11 @@ import {
|
|
|
47
48
|
resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTaskFiles, resolveTasksDir,
|
|
48
49
|
relSliceFile, relSlicePath, relTaskFile,
|
|
49
50
|
buildSliceFileName, buildMilestoneFileName, gsdRoot, resolveMilestonePath,
|
|
51
|
+
resolveGsdRootFile,
|
|
50
52
|
} from "./paths.js";
|
|
51
53
|
import { Key } from "@gsd/pi-tui";
|
|
52
54
|
import { join } from "node:path";
|
|
53
|
-
import { existsSync } from "node:fs";
|
|
55
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
54
56
|
import { shortcutDesc } from "../shared/terminal.js";
|
|
55
57
|
import { Text } from "@gsd/pi-tui";
|
|
56
58
|
import { pauseAutoForProviderError } from "./provider-error-pause.js";
|
|
@@ -272,6 +274,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
276
|
|
|
277
|
+
// Load project knowledge if available
|
|
278
|
+
let knowledgeBlock = "";
|
|
279
|
+
const knowledgePath = resolveGsdRootFile(process.cwd(), "KNOWLEDGE");
|
|
280
|
+
if (existsSync(knowledgePath)) {
|
|
281
|
+
try {
|
|
282
|
+
const content = readFileSync(knowledgePath, "utf-8").trim();
|
|
283
|
+
if (content) {
|
|
284
|
+
knowledgeBlock = `\n\n[PROJECT KNOWLEDGE — Rules, patterns, and lessons learned]\n\n${content}`;
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// File read error — skip knowledge injection
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
275
291
|
// Detect skills installed during this auto-mode session
|
|
276
292
|
let newSkillsBlock = "";
|
|
277
293
|
if (hasSkillSnapshot()) {
|
|
@@ -287,6 +303,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
287
303
|
let worktreeBlock = "";
|
|
288
304
|
const worktreeName = getActiveWorktreeName();
|
|
289
305
|
const worktreeMainCwd = getWorktreeOriginalCwd();
|
|
306
|
+
const autoWorktree = getActiveAutoWorktreeContext();
|
|
290
307
|
if (worktreeName && worktreeMainCwd) {
|
|
291
308
|
worktreeBlock = [
|
|
292
309
|
"",
|
|
@@ -304,10 +321,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
304
321
|
"All file operations, bash commands, and GSD state resolve against the worktree path above.",
|
|
305
322
|
"Use /worktree merge to merge changes back. Use /worktree return to switch back to the main tree.",
|
|
306
323
|
].join("\n");
|
|
324
|
+
} else if (autoWorktree) {
|
|
325
|
+
worktreeBlock = [
|
|
326
|
+
"",
|
|
327
|
+
"",
|
|
328
|
+
"[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
|
|
329
|
+
`IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
|
|
330
|
+
`The actual current working directory is: ${process.cwd()}`,
|
|
331
|
+
"",
|
|
332
|
+
"You are working inside a GSD auto-worktree.",
|
|
333
|
+
`- Milestone worktree: ${autoWorktree.worktreeName}`,
|
|
334
|
+
`- Worktree path (this is the real cwd): ${process.cwd()}`,
|
|
335
|
+
`- Main project: ${autoWorktree.originalBase}`,
|
|
336
|
+
`- Branch: ${autoWorktree.branch}`,
|
|
337
|
+
"",
|
|
338
|
+
"All file operations, bash commands, and GSD state resolve against the worktree path above.",
|
|
339
|
+
"Write every .gsd artifact in the worktree path above, never in the main project tree.",
|
|
340
|
+
].join("\n");
|
|
307
341
|
}
|
|
308
342
|
|
|
309
343
|
return {
|
|
310
|
-
systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${newSkillsBlock}${worktreeBlock}`,
|
|
344
|
+
systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${newSkillsBlock}${worktreeBlock}`,
|
|
311
345
|
...(injection
|
|
312
346
|
? {
|
|
313
347
|
message: {
|
|
@@ -542,6 +576,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
542
576
|
const existing = await loadFile(discussionPath) ?? `# ${milestoneId} Discussion Log\n\n`;
|
|
543
577
|
await saveFile(discussionPath, existing + newBlock);
|
|
544
578
|
});
|
|
579
|
+
|
|
580
|
+
// ── tool_execution_start/end: track in-flight tools for idle detection ──
|
|
581
|
+
pi.on("tool_execution_start", async (event) => {
|
|
582
|
+
if (!isAutoActive()) return;
|
|
583
|
+
markToolStart(event.toolCallId);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
pi.on("tool_execution_end", async (event) => {
|
|
587
|
+
markToolEnd(event.toolCallId);
|
|
588
|
+
});
|
|
545
589
|
}
|
|
546
590
|
|
|
547
591
|
async function buildGuidedExecuteContextInjection(prompt: string, basePath: string): Promise<string | null> {
|
|
@@ -15,6 +15,9 @@ import { nativeScanGsdTree, type GsdTreeEntry } from "./native-parser-bridge.js"
|
|
|
15
15
|
|
|
16
16
|
// ─── Directory Listing Cache ──────────────────────────────────────────────────
|
|
17
17
|
|
|
18
|
+
/** Max entries before eviction. Prevents unbounded growth in long sessions (#611). */
|
|
19
|
+
const DIR_CACHE_MAX = 200;
|
|
20
|
+
|
|
18
21
|
const dirEntryCache = new Map<string, Dirent[]>();
|
|
19
22
|
const dirListCache = new Map<string, string[]>();
|
|
20
23
|
|
|
@@ -85,6 +88,7 @@ function cachedReaddirWithTypes(dirPath: string): Dirent[] {
|
|
|
85
88
|
d.isSocket = () => false;
|
|
86
89
|
return d;
|
|
87
90
|
});
|
|
91
|
+
if (dirEntryCache.size >= DIR_CACHE_MAX) dirEntryCache.clear();
|
|
88
92
|
dirEntryCache.set(dirPath, dirents);
|
|
89
93
|
return dirents;
|
|
90
94
|
}
|
|
@@ -92,6 +96,7 @@ function cachedReaddirWithTypes(dirPath: string): Dirent[] {
|
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
99
|
+
if (dirEntryCache.size >= DIR_CACHE_MAX) dirEntryCache.clear();
|
|
95
100
|
dirEntryCache.set(dirPath, entries);
|
|
96
101
|
return entries;
|
|
97
102
|
}
|
|
@@ -107,6 +112,7 @@ function cachedReaddir(dirPath: string): string[] {
|
|
|
107
112
|
const treeEntries = nativeTreeCache.get(key);
|
|
108
113
|
if (treeEntries) {
|
|
109
114
|
const names = treeEntries.map(e => e.name);
|
|
115
|
+
if (dirListCache.size >= DIR_CACHE_MAX) dirListCache.clear();
|
|
110
116
|
dirListCache.set(dirPath, names);
|
|
111
117
|
return names;
|
|
112
118
|
}
|
|
@@ -114,6 +120,7 @@ function cachedReaddir(dirPath: string): string[] {
|
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
const entries = readdirSync(dirPath);
|
|
123
|
+
if (dirListCache.size >= DIR_CACHE_MAX) dirListCache.clear();
|
|
117
124
|
dirListCache.set(dirPath, entries);
|
|
118
125
|
return entries;
|
|
119
126
|
}
|
|
@@ -248,6 +255,7 @@ export const GSD_ROOT_FILES = {
|
|
|
248
255
|
STATE: "STATE.md",
|
|
249
256
|
REQUIREMENTS: "REQUIREMENTS.md",
|
|
250
257
|
OVERRIDES: "OVERRIDES.md",
|
|
258
|
+
KNOWLEDGE: "KNOWLEDGE.md",
|
|
251
259
|
} as const;
|
|
252
260
|
|
|
253
261
|
export type GSDRootFileKey = keyof typeof GSD_ROOT_FILES;
|
|
@@ -259,6 +267,7 @@ const LEGACY_GSD_ROOT_FILES: Record<GSDRootFileKey, string> = {
|
|
|
259
267
|
STATE: "state.md",
|
|
260
268
|
REQUIREMENTS: "requirements.md",
|
|
261
269
|
OVERRIDES: "overrides.md",
|
|
270
|
+
KNOWLEDGE: "knowledge.md",
|
|
262
271
|
};
|
|
263
272
|
|
|
264
273
|
export function gsdRoot(basePath: string): string {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
4
|
import { getAgentDir } from "@gsd/pi-coding-agent";
|
|
@@ -1252,3 +1252,61 @@ export function resolvePreDispatchHooks(): PreDispatchHookConfig[] {
|
|
|
1252
1252
|
return (prefs?.preferences.pre_dispatch_hooks ?? [])
|
|
1253
1253
|
.filter(h => h.enabled !== false);
|
|
1254
1254
|
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Validate a model ID string.
|
|
1258
|
+
* Returns true if the ID looks like a valid model identifier.
|
|
1259
|
+
*/
|
|
1260
|
+
export function validateModelId(modelId: string): boolean {
|
|
1261
|
+
if (!modelId || typeof modelId !== "string") return false;
|
|
1262
|
+
const trimmed = modelId.trim();
|
|
1263
|
+
if (trimmed.length === 0 || trimmed.length > 256) return false;
|
|
1264
|
+
// Allow alphanumeric, hyphens, underscores, dots, slashes, colons
|
|
1265
|
+
return /^[a-zA-Z0-9\-_./:]+$/.test(trimmed);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Update the models section of the global GSD preferences file.
|
|
1270
|
+
* Performs a safe read-modify-write: reads current content, updates the models
|
|
1271
|
+
* YAML block, and writes back. Creates the file if it doesn't exist.
|
|
1272
|
+
*/
|
|
1273
|
+
export function updatePreferencesModels(models: GSDModelConfigV2): void {
|
|
1274
|
+
const prefsPath = getGlobalGSDPreferencesPath();
|
|
1275
|
+
|
|
1276
|
+
let content = "";
|
|
1277
|
+
if (existsSync(prefsPath)) {
|
|
1278
|
+
content = readFileSync(prefsPath, "utf-8");
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Build the new models block
|
|
1282
|
+
const lines: string[] = ["models:"];
|
|
1283
|
+
for (const [phase, value] of Object.entries(models)) {
|
|
1284
|
+
if (typeof value === "string") {
|
|
1285
|
+
lines.push(` ${phase}: ${value}`);
|
|
1286
|
+
} else if (value && typeof value === "object") {
|
|
1287
|
+
const config = value as GSDPhaseModelConfig;
|
|
1288
|
+
lines.push(` ${phase}:`);
|
|
1289
|
+
lines.push(` model: ${config.model}`);
|
|
1290
|
+
if (config.provider) {
|
|
1291
|
+
lines.push(` provider: ${config.provider}`);
|
|
1292
|
+
}
|
|
1293
|
+
if (config.fallbacks && config.fallbacks.length > 0) {
|
|
1294
|
+
lines.push(` fallbacks:`);
|
|
1295
|
+
for (const fb of config.fallbacks) {
|
|
1296
|
+
lines.push(` - ${fb}`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
const modelsBlock = lines.join("\n");
|
|
1302
|
+
|
|
1303
|
+
// Replace existing models block or append
|
|
1304
|
+
const modelsRegex = /^models:[\s\S]*?(?=\n[a-z_]|\n*$)/m;
|
|
1305
|
+
if (modelsRegex.test(content)) {
|
|
1306
|
+
content = content.replace(modelsRegex, modelsBlock);
|
|
1307
|
+
} else {
|
|
1308
|
+
content = content.trimEnd() + "\n\n" + modelsBlock + "\n";
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
writeFileSync(prefsPath, content, "utf-8");
|
|
1312
|
+
}
|
|
@@ -54,11 +54,12 @@ Then:
|
|
|
54
54
|
- Don't fix symptoms. Understand *why* something fails before changing code. A test that passes after a change you don't understand is luck, not a fix.
|
|
55
55
|
11. **Blocker discovery:** If execution reveals that the remaining slice plan is fundamentally invalid — not just a bug or minor deviation, but a plan-invalidating finding like a wrong API, missing capability, or architectural mismatch — set `blocker_discovered: true` in the task summary frontmatter and describe the blocker clearly in the summary narrative. Do NOT set `blocker_discovered: true` for ordinary debugging, minor deviations, or issues that can be fixed within the current task or the remaining plan. This flag triggers an automatic replan of the slice.
|
|
56
56
|
12. If you made an architectural, pattern, library, or observability decision during this task that downstream work should know about, append it to `.gsd/DECISIONS.md` (use the **Decisions** output template from the inlined templates below if the file doesn't exist yet). Not every task produces decisions — only append when a meaningful choice was made.
|
|
57
|
-
13.
|
|
58
|
-
14.
|
|
59
|
-
15.
|
|
60
|
-
16.
|
|
61
|
-
17.
|
|
57
|
+
13. If you discover a non-obvious rule, recurring gotcha, or useful pattern during execution, append it to `.gsd/KNOWLEDGE.md`. Only add entries that would save future agents from repeating your investigation. Don't add obvious things.
|
|
58
|
+
14. Use the **Task Summary** output template from the inlined templates below
|
|
59
|
+
15. Write `{{taskSummaryPath}}`
|
|
60
|
+
16. Mark {{taskId}} done in `{{planPath}}` (change `[ ]` to `[x]`)
|
|
61
|
+
17. Do not commit manually — the system auto-commits your changes after this unit completes.
|
|
62
|
+
18. Update `.gsd/STATE.md`
|
|
62
63
|
|
|
63
64
|
All work stays in your working directory: `{{workingDirectory}}`.
|
|
64
65
|
|
|
@@ -65,6 +65,7 @@ Titles live inside file content (headings, frontmatter), not in file or director
|
|
|
65
65
|
PROJECT.md (living doc - what the project is right now)
|
|
66
66
|
REQUIREMENTS.md (requirement contract - tracks active/validated/deferred/out-of-scope)
|
|
67
67
|
DECISIONS.md (append-only register of architectural and pattern decisions)
|
|
68
|
+
KNOWLEDGE.md (append-only register of project-specific rules, patterns, and lessons learned)
|
|
68
69
|
OVERRIDES.md (user-issued overrides that supersede plan content via /gsd steer)
|
|
69
70
|
QUEUE.md (append-only log of queued milestones via /gsd queue)
|
|
70
71
|
STATE.md
|
|
@@ -100,6 +101,7 @@ All auto-mode work happens inside a worktree at `.gsd/worktrees/<MID>/`. This is
|
|
|
100
101
|
- **PROJECT.md** is a living document describing what the project is right now - current state only, updated at slice completion when stale
|
|
101
102
|
- **REQUIREMENTS.md** tracks the requirement contract — requirements move between Active, Validated, Deferred, Blocked, and Out of Scope as slices prove or invalidate them. Update at slice completion when evidence supports a status change.
|
|
102
103
|
- **DECISIONS.md** is an append-only register of architectural and pattern decisions - read it during planning/research, append to it during execution when a meaningful decision is made
|
|
104
|
+
- **KNOWLEDGE.md** is an append-only register of project-specific rules, patterns, and lessons learned. Read it at the start of every unit. Append to it when you discover a recurring issue, a non-obvious pattern, or a rule that future agents should follow.
|
|
103
105
|
- **CONTEXT.md** files (milestone or slice level) capture the brief — scope, goals, constraints, and key decisions from discussion. When present, they are the authoritative source for what a milestone or slice is trying to achieve. Read them before planning or executing.
|
|
104
106
|
- **Milestones** are major project phases (M001, M002, ...)
|
|
105
107
|
- **Slices** are demoable vertical increments (S01, S02, ...) ordered by risk. After each slice completes, the roadmap is reassessed before the next slice begins.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Queue Order — Custom milestone execution ordering.
|
|
3
|
+
*
|
|
4
|
+
* Stores an explicit execution order in `.gsd/QUEUE-ORDER.json`.
|
|
5
|
+
* When present, `findMilestoneIds()` uses this order instead of
|
|
6
|
+
* the default numeric sort (milestoneIdSort).
|
|
7
|
+
*
|
|
8
|
+
* The file is committed to git (not gitignored) so ordering
|
|
9
|
+
* survives branch switches and is shared across sessions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { gsdRoot } from "./paths.js";
|
|
15
|
+
import { milestoneIdSort } from "./guided-flow.js";
|
|
16
|
+
|
|
17
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
interface QueueOrderFile {
|
|
20
|
+
order: string[];
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DependencyViolation {
|
|
25
|
+
milestone: string;
|
|
26
|
+
dependsOn: string;
|
|
27
|
+
type: 'would_block' | 'circular' | 'missing_dep';
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DependencyRedundancy {
|
|
32
|
+
milestone: string;
|
|
33
|
+
dependsOn: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DependencyValidation {
|
|
37
|
+
valid: boolean;
|
|
38
|
+
violations: DependencyViolation[];
|
|
39
|
+
redundant: DependencyRedundancy[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Path ────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function queueOrderPath(basePath: string): string {
|
|
45
|
+
return join(gsdRoot(basePath), "QUEUE-ORDER.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Read / Write ────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load the custom queue order. Returns null if no file exists or if
|
|
52
|
+
* the file is corrupt/unreadable.
|
|
53
|
+
*/
|
|
54
|
+
export function loadQueueOrder(basePath: string): string[] | null {
|
|
55
|
+
const p = queueOrderPath(basePath);
|
|
56
|
+
if (!existsSync(p)) return null;
|
|
57
|
+
try {
|
|
58
|
+
const data: QueueOrderFile = JSON.parse(readFileSync(p, "utf-8"));
|
|
59
|
+
if (!Array.isArray(data.order)) return null;
|
|
60
|
+
return data.order;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Save a custom queue order to disk.
|
|
68
|
+
*/
|
|
69
|
+
export function saveQueueOrder(basePath: string, order: string[]): void {
|
|
70
|
+
const data: QueueOrderFile = {
|
|
71
|
+
order,
|
|
72
|
+
updatedAt: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
writeFileSync(queueOrderPath(basePath), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Sorting ─────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sort milestone IDs respecting a custom order.
|
|
81
|
+
*
|
|
82
|
+
* - IDs present in `customOrder` appear in that exact sequence.
|
|
83
|
+
* - IDs on disk but NOT in `customOrder` are appended at the end,
|
|
84
|
+
* sorted by the default `milestoneIdSort` (numeric).
|
|
85
|
+
* - IDs in `customOrder` but NOT on disk are silently skipped.
|
|
86
|
+
* - When `customOrder` is null, falls back to `milestoneIdSort`.
|
|
87
|
+
*/
|
|
88
|
+
export function sortByQueueOrder(ids: string[], customOrder: string[] | null): string[] {
|
|
89
|
+
if (!customOrder) return [...ids].sort(milestoneIdSort);
|
|
90
|
+
|
|
91
|
+
const idSet = new Set(ids);
|
|
92
|
+
const ordered: string[] = [];
|
|
93
|
+
|
|
94
|
+
// First: IDs from customOrder that exist on disk
|
|
95
|
+
for (const id of customOrder) {
|
|
96
|
+
if (idSet.has(id)) {
|
|
97
|
+
ordered.push(id);
|
|
98
|
+
idSet.delete(id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Then: remaining IDs not in customOrder, in default sort order
|
|
103
|
+
const remaining = [...idSet].sort(milestoneIdSort);
|
|
104
|
+
return [...ordered, ...remaining];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Pruning ─────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Remove IDs from the queue order file that are no longer valid
|
|
111
|
+
* (completed or deleted milestones). No-op if file doesn't exist.
|
|
112
|
+
*/
|
|
113
|
+
export function pruneQueueOrder(basePath: string, validIds: string[]): void {
|
|
114
|
+
const order = loadQueueOrder(basePath);
|
|
115
|
+
if (!order) return;
|
|
116
|
+
|
|
117
|
+
const validSet = new Set(validIds);
|
|
118
|
+
const pruned = order.filter(id => validSet.has(id));
|
|
119
|
+
|
|
120
|
+
if (pruned.length !== order.length) {
|
|
121
|
+
saveQueueOrder(basePath, pruned);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Validation ──────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validate a proposed queue order against dependency constraints.
|
|
129
|
+
*
|
|
130
|
+
* Checks:
|
|
131
|
+
* - would_block: A milestone is placed before one of its dependencies
|
|
132
|
+
* - circular: Two or more milestones form a dependency cycle
|
|
133
|
+
* - missing_dep: A milestone depends on an ID that doesn't exist
|
|
134
|
+
* - redundant: A dependency is satisfied by queue position (dep comes earlier)
|
|
135
|
+
*/
|
|
136
|
+
export function validateQueueOrder(
|
|
137
|
+
order: string[],
|
|
138
|
+
depsMap: Map<string, string[]>,
|
|
139
|
+
completedIds: Set<string>,
|
|
140
|
+
): DependencyValidation {
|
|
141
|
+
const violations: DependencyViolation[] = [];
|
|
142
|
+
const redundant: DependencyRedundancy[] = [];
|
|
143
|
+
|
|
144
|
+
const positionMap = new Map<string, number>();
|
|
145
|
+
for (let i = 0; i < order.length; i++) {
|
|
146
|
+
positionMap.set(order[i], i);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const allKnownIds = new Set([...order, ...completedIds]);
|
|
150
|
+
|
|
151
|
+
for (const [mid, deps] of depsMap) {
|
|
152
|
+
const midPos = positionMap.get(mid);
|
|
153
|
+
if (midPos === undefined) continue; // not in pending order
|
|
154
|
+
|
|
155
|
+
for (const dep of deps) {
|
|
156
|
+
// Dep already completed — always satisfied
|
|
157
|
+
if (completedIds.has(dep)) continue;
|
|
158
|
+
|
|
159
|
+
// Dep doesn't exist anywhere
|
|
160
|
+
if (!allKnownIds.has(dep)) {
|
|
161
|
+
violations.push({
|
|
162
|
+
milestone: mid,
|
|
163
|
+
dependsOn: dep,
|
|
164
|
+
type: 'missing_dep',
|
|
165
|
+
message: `${mid} depends on ${dep}, but ${dep} does not exist.`,
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const depPos = positionMap.get(dep);
|
|
171
|
+
if (depPos === undefined) continue; // dep not in pending order (edge case)
|
|
172
|
+
|
|
173
|
+
if (depPos > midPos) {
|
|
174
|
+
// Dep comes AFTER this milestone in the order — violation
|
|
175
|
+
violations.push({
|
|
176
|
+
milestone: mid,
|
|
177
|
+
dependsOn: dep,
|
|
178
|
+
type: 'would_block',
|
|
179
|
+
message: `${mid} cannot run before ${dep} — ${mid} depends_on: [${dep}].`,
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
// Dep comes before — satisfied by position, redundant
|
|
183
|
+
redundant.push({ milestone: mid, dependsOn: dep });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check for circular dependencies
|
|
189
|
+
const visited = new Set<string>();
|
|
190
|
+
const inStack = new Set<string>();
|
|
191
|
+
|
|
192
|
+
function hasCycle(node: string, path: string[]): string[] | null {
|
|
193
|
+
if (inStack.has(node)) return [...path, node];
|
|
194
|
+
if (visited.has(node)) return null;
|
|
195
|
+
|
|
196
|
+
visited.add(node);
|
|
197
|
+
inStack.add(node);
|
|
198
|
+
|
|
199
|
+
const deps = depsMap.get(node) ?? [];
|
|
200
|
+
for (const dep of deps) {
|
|
201
|
+
if (completedIds.has(dep)) continue;
|
|
202
|
+
const cycle = hasCycle(dep, [...path, node]);
|
|
203
|
+
if (cycle) return cycle;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
inStack.delete(node);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const mid of order) {
|
|
211
|
+
if (!visited.has(mid)) {
|
|
212
|
+
const cycle = hasCycle(mid, []);
|
|
213
|
+
if (cycle) {
|
|
214
|
+
const cycleStr = cycle.join(' → ');
|
|
215
|
+
violations.push({
|
|
216
|
+
milestone: cycle[0],
|
|
217
|
+
dependsOn: cycle[cycle.length - 2],
|
|
218
|
+
type: 'circular',
|
|
219
|
+
message: `Circular dependency: ${cycleStr}`,
|
|
220
|
+
});
|
|
221
|
+
break; // one cycle report is enough
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
valid: violations.length === 0,
|
|
228
|
+
violations,
|
|
229
|
+
redundant,
|
|
230
|
+
};
|
|
231
|
+
}
|