gsd-pi 2.29.0-dev.77f06e2 → 2.29.0-dev.953d788
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 +17 -24
- package/dist/resources/extensions/bg-shell/process-manager.ts +0 -13
- package/dist/resources/extensions/gsd/auto-dashboard.ts +65 -186
- package/dist/resources/extensions/gsd/auto-post-unit.ts +3 -6
- package/dist/resources/extensions/gsd/auto-recovery.ts +22 -16
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +6 -7
- package/dist/resources/extensions/gsd/auto.ts +15 -0
- package/dist/resources/extensions/gsd/commands-handlers.ts +1 -20
- package/dist/resources/extensions/gsd/commands-logs.ts +14 -13
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +14 -44
- package/dist/resources/extensions/gsd/commands.ts +22 -55
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +1 -2
- package/dist/resources/extensions/gsd/json-persistence.ts +1 -16
- package/dist/resources/extensions/gsd/queue-order.ts +11 -10
- package/dist/resources/extensions/gsd/session-status-io.ts +41 -23
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +38 -60
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/dist/resources/extensions/mcporter/index.ts +525 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +19 -8
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +17 -11
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +19 -8
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +0 -13
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +0 -13
- package/src/resources/extensions/bg-shell/process-manager.ts +0 -13
- package/src/resources/extensions/gsd/auto-dashboard.ts +65 -186
- package/src/resources/extensions/gsd/auto-post-unit.ts +3 -6
- package/src/resources/extensions/gsd/auto-recovery.ts +22 -16
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +6 -7
- package/src/resources/extensions/gsd/auto.ts +15 -0
- package/src/resources/extensions/gsd/commands-handlers.ts +1 -20
- package/src/resources/extensions/gsd/commands-logs.ts +14 -13
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -44
- package/src/resources/extensions/gsd/commands.ts +22 -55
- package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -2
- package/src/resources/extensions/gsd/json-persistence.ts +1 -16
- package/src/resources/extensions/gsd/queue-order.ts +11 -10
- package/src/resources/extensions/gsd/session-status-io.ts +41 -23
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +38 -60
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/src/resources/extensions/mcporter/index.ts +525 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +19 -8
- package/src/resources/extensions/remote-questions/slack-adapter.ts +17 -11
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +19 -8
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +0 -544
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +0 -28
- package/dist/resources/extensions/gsd/tests/workflow-templates.test.ts +0 -173
- package/dist/resources/extensions/gsd/workflow-templates/bugfix.md +0 -87
- package/dist/resources/extensions/gsd/workflow-templates/dep-upgrade.md +0 -74
- package/dist/resources/extensions/gsd/workflow-templates/full-project.md +0 -41
- package/dist/resources/extensions/gsd/workflow-templates/hotfix.md +0 -45
- package/dist/resources/extensions/gsd/workflow-templates/refactor.md +0 -83
- package/dist/resources/extensions/gsd/workflow-templates/registry.json +0 -85
- package/dist/resources/extensions/gsd/workflow-templates/security-audit.md +0 -73
- package/dist/resources/extensions/gsd/workflow-templates/small-feature.md +0 -81
- package/dist/resources/extensions/gsd/workflow-templates/spike.md +0 -69
- package/dist/resources/extensions/gsd/workflow-templates.ts +0 -241
- package/dist/resources/extensions/mcp-client/index.ts +0 -459
- package/dist/resources/extensions/remote-questions/http-client.ts +0 -76
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +0 -544
- package/src/resources/extensions/gsd/prompts/workflow-start.md +0 -28
- package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +0 -173
- package/src/resources/extensions/gsd/workflow-templates/bugfix.md +0 -87
- package/src/resources/extensions/gsd/workflow-templates/dep-upgrade.md +0 -74
- package/src/resources/extensions/gsd/workflow-templates/full-project.md +0 -41
- package/src/resources/extensions/gsd/workflow-templates/hotfix.md +0 -45
- package/src/resources/extensions/gsd/workflow-templates/refactor.md +0 -83
- package/src/resources/extensions/gsd/workflow-templates/registry.json +0 -85
- package/src/resources/extensions/gsd/workflow-templates/security-audit.md +0 -73
- package/src/resources/extensions/gsd/workflow-templates/small-feature.md +0 -81
- package/src/resources/extensions/gsd/workflow-templates/spike.md +0 -69
- package/src/resources/extensions/gsd/workflow-templates.ts +0 -241
- package/src/resources/extensions/mcp-client/index.ts +0 -459
- package/src/resources/extensions/remote-questions/http-client.ts +0 -76
|
@@ -260,57 +260,27 @@ async function configureModels(ctx: ExtensionCommandContext, prefs: Record<strin
|
|
|
260
260
|
group.push(m);
|
|
261
261
|
}
|
|
262
262
|
const providers = Array.from(byProvider.keys()).sort((a, b) => a.localeCompare(b));
|
|
263
|
-
// Sort models within each provider
|
|
264
|
-
for (const group of byProvider.values()) {
|
|
265
|
-
group.sort((a, b) => a.id.localeCompare(b.id));
|
|
266
|
-
}
|
|
267
263
|
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
264
|
+
const modelOptions: string[] = [];
|
|
265
|
+
for (const provider of providers) {
|
|
266
|
+
const group = byProvider.get(provider)!;
|
|
267
|
+
modelOptions.push(`─── ${provider} (${group.length}) ───`);
|
|
268
|
+
for (const m of group) {
|
|
269
|
+
modelOptions.push(`${m.id} · ${m.provider}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
modelOptions.push("(keep current)", "(clear)");
|
|
274
273
|
|
|
275
274
|
for (const phase of modelPhases) {
|
|
276
275
|
const current = models[phase] ?? "";
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
// Step 1: pick provider
|
|
280
|
-
const providerChoice = await ctx.ui.select(`${phaseLabel} — choose provider:`, providerOptions);
|
|
281
|
-
if (!providerChoice || typeof providerChoice !== "string" || providerChoice === "(keep current)") continue;
|
|
282
|
-
|
|
283
|
-
if (providerChoice === "(clear)") {
|
|
284
|
-
delete models[phase];
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (providerChoice === "(type manually)") {
|
|
289
|
-
const input = await ctx.ui.input(
|
|
290
|
-
`${phaseLabel} — enter model ID:`,
|
|
291
|
-
current || "e.g. claude-sonnet-4-20250514",
|
|
292
|
-
);
|
|
293
|
-
if (input !== null && input !== undefined) {
|
|
294
|
-
const val = input.trim();
|
|
295
|
-
if (val) models[phase] = val;
|
|
296
|
-
}
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Step 2: pick model within provider
|
|
301
|
-
const providerName = providerChoice.replace(/ \(\d+ models?\)$/, "");
|
|
302
|
-
const group = byProvider.get(providerName);
|
|
303
|
-
if (!group) continue;
|
|
304
|
-
|
|
305
|
-
const modelOptions = group.map(m => m.id);
|
|
306
|
-
modelOptions.push("(keep current)", "(clear)");
|
|
276
|
+
const title = `Model for ${phase} phase${current ? ` (current: ${current})` : ""}:`;
|
|
277
|
+
const choice = await ctx.ui.select(title, modelOptions);
|
|
307
278
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (modelChoice === "(clear)") {
|
|
279
|
+
if (choice && typeof choice === "string" && choice !== "(keep current)") {
|
|
280
|
+
if (choice === "(clear)") {
|
|
311
281
|
delete models[phase];
|
|
312
282
|
} else {
|
|
313
|
-
models[phase] =
|
|
283
|
+
models[phase] = choice.split(" · ")[0];
|
|
314
284
|
}
|
|
315
285
|
}
|
|
316
286
|
}
|
|
@@ -13,8 +13,7 @@ import { deriveState } from "./state.js";
|
|
|
13
13
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
14
14
|
import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
|
|
15
15
|
import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
|
|
16
|
-
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
|
|
17
|
-
import { dispatchDirectPhase } from "./auto-direct-dispatch.js";
|
|
16
|
+
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote, dispatchDirectPhase } from "./auto.js";
|
|
18
17
|
import { resolveProjectRoot } from "./worktree.js";
|
|
19
18
|
import { assertSafeDirectory } from "./validate-directory.js";
|
|
20
19
|
import {
|
|
@@ -22,6 +21,8 @@ import {
|
|
|
22
21
|
getProjectGSDPreferencesPath,
|
|
23
22
|
loadEffectiveGSDPreferences,
|
|
24
23
|
} from "./preferences.js";
|
|
24
|
+
import { loadPrompt } from "./prompt-loader.js";
|
|
25
|
+
|
|
25
26
|
import { handleRemote } from "../remote-questions/mod.js";
|
|
26
27
|
import { handleQuick } from "./quick.js";
|
|
27
28
|
import { handleHistory } from "./history.js";
|
|
@@ -43,9 +44,26 @@ import { handleInspect } from "./commands-inspect.js";
|
|
|
43
44
|
import { handleCleanupBranches, handleCleanupSnapshots, handleSkip, handleDryRun } from "./commands-maintenance.js";
|
|
44
45
|
import { handleDoctor, handleSteer, handleCapture, handleTriage, handleKnowledge, handleRunHook, handleUpdate, handleSkillHealth } from "./commands-handlers.js";
|
|
45
46
|
import { handleLogs } from "./commands-logs.js";
|
|
46
|
-
import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
|
|
50
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
51
|
+
const workflow = readFileSync(workflowPath, "utf-8");
|
|
52
|
+
const prompt = loadPrompt("doctor-heal", {
|
|
53
|
+
doctorSummary: reportText,
|
|
54
|
+
structuredIssues,
|
|
55
|
+
scopeLabel: scope ?? "active milestone / blocking scope",
|
|
56
|
+
doctorCommandSuffix: scope ? ` ${scope}` : "",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const content = `Read the following GSD workflow protocol and execute exactly.\n\n${workflow}\n\n## Your Task\n\n${prompt}`;
|
|
60
|
+
|
|
61
|
+
pi.sendMessage(
|
|
62
|
+
{ customType: "gsd-doctor-heal", content, display: false },
|
|
63
|
+
{ triggerTurn: true },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
/** Resolve the effective project root, accounting for worktree paths. */
|
|
50
68
|
export function projectRoot(): string {
|
|
51
69
|
const root = resolveProjectRoot(process.cwd());
|
|
@@ -55,7 +73,7 @@ export function projectRoot(): string {
|
|
|
55
73
|
|
|
56
74
|
export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
57
75
|
pi.registerCommand("gsd", {
|
|
58
|
-
description: "GSD — Get Shit Done: /gsd help|
|
|
76
|
+
description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel|update",
|
|
59
77
|
getArgumentCompletions: (prefix: string) => {
|
|
60
78
|
const subcommands = [
|
|
61
79
|
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
|
@@ -98,8 +116,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
98
116
|
{ cmd: "park", desc: "Park a milestone — skip without deleting" },
|
|
99
117
|
{ cmd: "unpark", desc: "Reactivate a parked milestone" },
|
|
100
118
|
{ cmd: "update", desc: "Update GSD to the latest version" },
|
|
101
|
-
{ cmd: "start", desc: "Start a workflow template (bugfix, spike, feature, etc.)" },
|
|
102
|
-
{ cmd: "templates", desc: "List available workflow templates" },
|
|
103
119
|
];
|
|
104
120
|
const parts = prefix.trim().split(/\s+/);
|
|
105
121
|
|
|
@@ -285,42 +301,6 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
285
301
|
.map((s) => ({ value: `knowledge ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
286
302
|
}
|
|
287
303
|
|
|
288
|
-
if (parts[0] === "start" && parts.length <= 2) {
|
|
289
|
-
const subPrefix = parts[1] ?? "";
|
|
290
|
-
const subs = [
|
|
291
|
-
{ cmd: "bugfix", desc: "Triage, fix, test, and ship a bug fix" },
|
|
292
|
-
{ cmd: "small-feature", desc: "Lightweight feature with optional discussion" },
|
|
293
|
-
{ cmd: "spike", desc: "Research, prototype, and document findings" },
|
|
294
|
-
{ cmd: "hotfix", desc: "Minimal: fix it, test it, ship it" },
|
|
295
|
-
{ cmd: "refactor", desc: "Inventory, plan waves, migrate, verify" },
|
|
296
|
-
{ cmd: "security-audit", desc: "Scan, triage, remediate, re-scan" },
|
|
297
|
-
{ cmd: "dep-upgrade", desc: "Assess, upgrade, fix breaks, verify" },
|
|
298
|
-
{ cmd: "full-project", desc: "Complete GSD workflow with full ceremony" },
|
|
299
|
-
{ cmd: "resume", desc: "Resume an in-progress workflow" },
|
|
300
|
-
{ cmd: "--list", desc: "List all available templates" },
|
|
301
|
-
{ cmd: "--dry-run", desc: "Preview workflow without executing" },
|
|
302
|
-
];
|
|
303
|
-
return subs
|
|
304
|
-
.filter((s) => s.cmd.startsWith(subPrefix))
|
|
305
|
-
.map((s) => ({ value: `start ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (parts[0] === "templates" && parts.length <= 2) {
|
|
309
|
-
const subPrefix = parts[1] ?? "";
|
|
310
|
-
const subs = [
|
|
311
|
-
{ cmd: "info", desc: "Show detailed template info" },
|
|
312
|
-
];
|
|
313
|
-
return subs
|
|
314
|
-
.filter((s) => s.cmd.startsWith(subPrefix))
|
|
315
|
-
.map((s) => ({ value: `templates ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (parts[0] === "templates" && parts[1] === "info" && parts.length <= 3) {
|
|
319
|
-
const namePrefix = parts[2] ?? "";
|
|
320
|
-
return getTemplateCompletions(namePrefix)
|
|
321
|
-
.map((c) => ({ value: `templates ${c.value}`, label: c.label, description: c.description }));
|
|
322
|
-
}
|
|
323
|
-
|
|
324
304
|
if (parts[0] === "doctor") {
|
|
325
305
|
const modePrefix = parts[1] ?? "";
|
|
326
306
|
const modes = [
|
|
@@ -812,17 +792,6 @@ Examples:
|
|
|
812
792
|
return;
|
|
813
793
|
}
|
|
814
794
|
|
|
815
|
-
// ─── Workflow Templates ────────────────────────────────────────
|
|
816
|
-
if (trimmed === "start" || trimmed.startsWith("start ")) {
|
|
817
|
-
await handleStart(trimmed.replace(/^start\s*/, "").trim(), ctx, pi);
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
if (trimmed === "templates" || trimmed.startsWith("templates ")) {
|
|
822
|
-
await handleTemplates(trimmed.replace(/^templates\s*/, "").trim(), ctx);
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
795
|
if (trimmed === "") {
|
|
827
796
|
// Bare /gsd defaults to step mode
|
|
828
797
|
await startAuto(ctx, pi, projectRoot(), false, { step: true });
|
|
@@ -841,8 +810,6 @@ function showHelp(ctx: ExtensionCommandContext): void {
|
|
|
841
810
|
const lines = [
|
|
842
811
|
"GSD — Get Shit Done\n",
|
|
843
812
|
"WORKFLOW",
|
|
844
|
-
" /gsd start <tpl> Start a workflow template (bugfix, spike, feature, hotfix, etc.)",
|
|
845
|
-
" /gsd templates List available workflow templates [info <name>]",
|
|
846
813
|
" /gsd Run next unit in step mode (same as /gsd next)",
|
|
847
814
|
" /gsd next Execute next task, then pause [--dry-run] [--verbose]",
|
|
848
815
|
" /gsd auto Run all queued units continuously [--verbose]",
|
|
@@ -11,8 +11,7 @@ import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
|
11
11
|
import { deriveState } from "./state.js";
|
|
12
12
|
import { loadFile, parseRoadmap, parsePlan } from "./files.js";
|
|
13
13
|
import { resolveMilestoneFile, resolveSliceFile } from "./paths.js";
|
|
14
|
-
import { getAutoDashboardData } from "./auto.js";
|
|
15
|
-
import type { AutoDashboardData } from "./auto-dashboard.js";
|
|
14
|
+
import { getAutoDashboardData, type AutoDashboardData } from "./auto.js";
|
|
16
15
|
import {
|
|
17
16
|
getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice,
|
|
18
17
|
aggregateByModel, aggregateCacheHitRate, formatCost, formatTokenCount, formatCostProjection,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -50,18 +50,3 @@ export function saveJsonFile<T>(filePath: string, data: T): void {
|
|
|
50
50
|
// Non-fatal — don't let persistence failures break operation
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Write a JSON file atomically (write to .tmp, then rename).
|
|
56
|
-
* Creates parent directories as needed. Non-fatal on error.
|
|
57
|
-
*/
|
|
58
|
-
export function writeJsonFileAtomic<T>(filePath: string, data: T): void {
|
|
59
|
-
try {
|
|
60
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
61
|
-
const tmp = filePath + ".tmp";
|
|
62
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
63
|
-
renameSync(tmp, filePath);
|
|
64
|
-
} catch {
|
|
65
|
-
// Non-fatal — don't let persistence failures break operation
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* survives branch switches and is shared across sessions.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
12
13
|
import { join } from "node:path";
|
|
13
14
|
import { gsdRoot } from "./paths.js";
|
|
14
15
|
import { milestoneIdSort } from "./milestone-ids.js";
|
|
15
|
-
import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
16
16
|
|
|
17
17
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
18
18
|
|
|
@@ -45,12 +45,6 @@ function queueOrderPath(basePath: string): string {
|
|
|
45
45
|
return join(gsdRoot(basePath), "QUEUE-ORDER.json");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// ─── Type Guards ─────────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
function isQueueOrderFile(data: unknown): data is QueueOrderFile {
|
|
51
|
-
return data !== null && typeof data === "object" && "order" in data! && Array.isArray((data as QueueOrderFile).order);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
48
|
// ─── Read / Write ────────────────────────────────────────────────────────────
|
|
55
49
|
|
|
56
50
|
/**
|
|
@@ -58,8 +52,15 @@ function isQueueOrderFile(data: unknown): data is QueueOrderFile {
|
|
|
58
52
|
* the file is corrupt/unreadable.
|
|
59
53
|
*/
|
|
60
54
|
export function loadQueueOrder(basePath: string): string[] | null {
|
|
61
|
-
const
|
|
62
|
-
|
|
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
|
+
}
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
@@ -70,7 +71,7 @@ export function saveQueueOrder(basePath: string, order: string[]): void {
|
|
|
70
71
|
order,
|
|
71
72
|
updatedAt: new Date().toISOString(),
|
|
72
73
|
};
|
|
73
|
-
|
|
74
|
+
writeFileSync(queueOrderPath(basePath), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
// ─── Sorting ─────────────────────────────────────────────────────────────────
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
+
writeFileSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
renameSync,
|
|
14
17
|
unlinkSync,
|
|
15
18
|
readdirSync,
|
|
16
19
|
mkdirSync,
|
|
@@ -18,7 +21,6 @@ import {
|
|
|
18
21
|
} from "node:fs";
|
|
19
22
|
import { join } from "node:path";
|
|
20
23
|
import { gsdRoot } from "./paths.js";
|
|
21
|
-
import { loadJsonFileOrNull, writeJsonFileAtomic } from "./json-persistence.js";
|
|
22
24
|
|
|
23
25
|
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
24
26
|
|
|
@@ -47,16 +49,9 @@ export interface SignalMessage {
|
|
|
47
49
|
const PARALLEL_DIR = "parallel";
|
|
48
50
|
const STATUS_SUFFIX = ".status.json";
|
|
49
51
|
const SIGNAL_SUFFIX = ".signal.json";
|
|
52
|
+
const TMP_SUFFIX = ".tmp";
|
|
50
53
|
const DEFAULT_STALE_TIMEOUT_MS = 30_000;
|
|
51
54
|
|
|
52
|
-
function isSessionStatus(data: unknown): data is SessionStatus {
|
|
53
|
-
return data !== null && typeof data === "object" && "milestoneId" in data && "pid" in data;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function isSignalMessage(data: unknown): data is SignalMessage {
|
|
57
|
-
return data !== null && typeof data === "object" && "signal" in data && "sentAt" in data;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
55
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
61
56
|
|
|
62
57
|
function parallelDir(basePath: string): string {
|
|
@@ -91,13 +86,25 @@ function isPidAlive(pid: number): boolean {
|
|
|
91
86
|
|
|
92
87
|
/** Write session status atomically (write to .tmp, then rename). */
|
|
93
88
|
export function writeSessionStatus(basePath: string, status: SessionStatus): void {
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
try {
|
|
90
|
+
ensureParallelDir(basePath);
|
|
91
|
+
const dest = statusPath(basePath, status.milestoneId);
|
|
92
|
+
const tmp = dest + TMP_SUFFIX;
|
|
93
|
+
writeFileSync(tmp, JSON.stringify(status, null, 2), "utf-8");
|
|
94
|
+
renameSync(tmp, dest);
|
|
95
|
+
} catch { /* non-fatal */ }
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/** Read a specific milestone's session status. */
|
|
99
99
|
export function readSessionStatus(basePath: string, milestoneId: string): SessionStatus | null {
|
|
100
|
-
|
|
100
|
+
try {
|
|
101
|
+
const p = statusPath(basePath, milestoneId);
|
|
102
|
+
if (!existsSync(p)) return null;
|
|
103
|
+
const raw = readFileSync(p, "utf-8");
|
|
104
|
+
return JSON.parse(raw) as SessionStatus;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
/** Read all session status files from .gsd/parallel/. */
|
|
@@ -107,10 +114,13 @@ export function readAllSessionStatuses(basePath: string): SessionStatus[] {
|
|
|
107
114
|
|
|
108
115
|
const results: SessionStatus[] = [];
|
|
109
116
|
try {
|
|
110
|
-
|
|
117
|
+
const entries = readdirSync(dir);
|
|
118
|
+
for (const entry of entries) {
|
|
111
119
|
if (!entry.endsWith(STATUS_SUFFIX)) continue;
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
try {
|
|
121
|
+
const raw = readFileSync(join(dir, entry), "utf-8");
|
|
122
|
+
results.push(JSON.parse(raw) as SessionStatus);
|
|
123
|
+
} catch { /* skip corrupt files */ }
|
|
114
124
|
}
|
|
115
125
|
} catch { /* non-fatal */ }
|
|
116
126
|
return results;
|
|
@@ -128,19 +138,27 @@ export function removeSessionStatus(basePath: string, milestoneId: string): void
|
|
|
128
138
|
|
|
129
139
|
/** Write a signal file for a worker to consume. */
|
|
130
140
|
export function sendSignal(basePath: string, milestoneId: string, signal: SessionSignal): void {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
try {
|
|
142
|
+
ensureParallelDir(basePath);
|
|
143
|
+
const dest = signalPath(basePath, milestoneId);
|
|
144
|
+
const tmp = dest + TMP_SUFFIX;
|
|
145
|
+
const msg: SignalMessage = { signal, sentAt: Date.now(), from: "coordinator" };
|
|
146
|
+
writeFileSync(tmp, JSON.stringify(msg, null, 2), "utf-8");
|
|
147
|
+
renameSync(tmp, dest);
|
|
148
|
+
} catch { /* non-fatal */ }
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
/** Read and delete a signal file (atomic consume). Returns null if no signal pending. */
|
|
137
152
|
export function consumeSignal(basePath: string, milestoneId: string): SignalMessage | null {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
try {
|
|
154
|
+
const p = signalPath(basePath, milestoneId);
|
|
155
|
+
if (!existsSync(p)) return null;
|
|
156
|
+
const raw = readFileSync(p, "utf-8");
|
|
157
|
+
unlinkSync(p);
|
|
158
|
+
return JSON.parse(raw) as SignalMessage;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
142
161
|
}
|
|
143
|
-
return msg;
|
|
144
162
|
}
|
|
145
163
|
|
|
146
164
|
// ─── Stale Detection ───────────────────────────────────────────────────────
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getBudgetAlertLevel,
|
|
6
6
|
getBudgetEnforcementAction,
|
|
7
7
|
getNewBudgetAlertLevel,
|
|
8
|
-
} from "../auto
|
|
8
|
+
} from "../auto.js";
|
|
9
9
|
|
|
10
10
|
test("getBudgetAlertLevel returns the expected threshold bucket", () => {
|
|
11
11
|
assert.equal(getBudgetAlertLevel(0.10), 0);
|
|
@@ -17,8 +17,8 @@ import { tmpdir } from "node:os";
|
|
|
17
17
|
import {
|
|
18
18
|
_getUnitConsecutiveSkips,
|
|
19
19
|
_resetUnitConsecutiveSkips,
|
|
20
|
+
MAX_CONSECUTIVE_SKIPS,
|
|
20
21
|
} from "../auto.ts";
|
|
21
|
-
import { MAX_CONSECUTIVE_SKIPS } from "../auto/session.ts";
|
|
22
22
|
import { persistCompletedKey, removePersistedKey, loadPersistedKeys } from "../auto-recovery.ts";
|
|
23
23
|
import { createTestContext } from "./test-helpers.ts";
|
|
24
24
|
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
// Tests for the SEPARATOR_PREFIX convention used by ExtensionSelectorComponent
|
|
2
|
-
// and the two-step provider→model picker in configureModels.
|
|
3
|
-
//
|
|
1
|
+
// Tests for the SEPARATOR_PREFIX convention used by ExtensionSelectorComponent.
|
|
4
2
|
// We cannot import the component directly in node:test because its transitive
|
|
5
3
|
// dependency (countdown-timer.ts) uses TypeScript parameter properties which
|
|
6
4
|
// are unsupported under --experimental-strip-types. Instead we duplicate the
|
|
@@ -71,17 +69,16 @@ describe("separator detection", () => {
|
|
|
71
69
|
});
|
|
72
70
|
});
|
|
73
71
|
|
|
74
|
-
describe("
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
72
|
+
describe("model grouping", () => {
|
|
73
|
+
test("groups models by provider with separator headers", () => {
|
|
74
|
+
// Simulate the grouping logic from configureModels
|
|
75
|
+
const availableModels = [
|
|
76
|
+
{ id: "claude-opus-4-6", provider: "anthropic" },
|
|
77
|
+
{ id: "gpt-4o", provider: "openai" },
|
|
78
|
+
{ id: "claude-sonnet-4-5", provider: "anthropic" },
|
|
79
|
+
{ id: "o3-mini", provider: "openai" },
|
|
80
|
+
];
|
|
83
81
|
|
|
84
|
-
function buildProviderGroups() {
|
|
85
82
|
const byProvider = new Map<string, typeof availableModels>();
|
|
86
83
|
for (const m of availableModels) {
|
|
87
84
|
let group = byProvider.get(m.provider);
|
|
@@ -92,53 +89,34 @@ describe("two-step provider→model picker", () => {
|
|
|
92
89
|
group.push(m);
|
|
93
90
|
}
|
|
94
91
|
const providers = Array.from(byProvider.keys()).sort((a, b) => a.localeCompare(b));
|
|
95
|
-
for (const group of byProvider.values()) {
|
|
96
|
-
group.sort((a, b) => a.id.localeCompare(b.id));
|
|
97
|
-
}
|
|
98
|
-
return { byProvider, providers };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
test("provider menu lists providers with model counts", () => {
|
|
102
|
-
const { providers, byProvider } = buildProviderGroups();
|
|
103
|
-
const providerOptions = providers.map(p => {
|
|
104
|
-
const count = byProvider.get(p)!.length;
|
|
105
|
-
return `${p} (${count} models)`;
|
|
106
|
-
});
|
|
107
|
-
providerOptions.push("(keep current)", "(clear)", "(type manually)");
|
|
108
|
-
|
|
109
|
-
assert.strictEqual(providerOptions[0], "anthropic (3 models)");
|
|
110
|
-
assert.strictEqual(providerOptions[1], "openai (2 models)");
|
|
111
|
-
assert.strictEqual(providerOptions[2], "(keep current)");
|
|
112
|
-
assert.strictEqual(providerOptions[3], "(clear)");
|
|
113
|
-
assert.strictEqual(providerOptions[4], "(type manually)");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("model menu for a provider is sorted alphabetically", () => {
|
|
117
|
-
const { byProvider } = buildProviderGroups();
|
|
118
|
-
const anthropicModels = byProvider.get("anthropic")!;
|
|
119
|
-
const modelOptions = anthropicModels.map(m => m.id);
|
|
120
|
-
|
|
121
|
-
assert.strictEqual(modelOptions[0], "claude-haiku-4-5");
|
|
122
|
-
assert.strictEqual(modelOptions[1], "claude-opus-4-6");
|
|
123
|
-
assert.strictEqual(modelOptions[2], "claude-sonnet-4-5");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("provider name is extracted correctly from choice string", () => {
|
|
127
|
-
const choice = "anthropic (3 models)";
|
|
128
|
-
const providerName = choice.replace(/ \(\d+ models?\)$/, "");
|
|
129
|
-
assert.strictEqual(providerName, "anthropic");
|
|
130
|
-
|
|
131
|
-
const singleChoice = "ollama (1 model)";
|
|
132
|
-
const singleProvider = singleChoice.replace(/ \(\d+ models?\)$/, "");
|
|
133
|
-
assert.strictEqual(singleProvider, "ollama");
|
|
134
|
-
});
|
|
135
92
|
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
93
|
+
const modelOptions: string[] = [];
|
|
94
|
+
for (const provider of providers) {
|
|
95
|
+
const group = byProvider.get(provider)!;
|
|
96
|
+
modelOptions.push(`${SEPARATOR_PREFIX} ${provider} (${group.length}) ${SEPARATOR_PREFIX}`);
|
|
97
|
+
for (const m of group) {
|
|
98
|
+
modelOptions.push(`${m.id} · ${m.provider}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
modelOptions.push("(keep current)", "(clear)");
|
|
102
|
+
|
|
103
|
+
// Verify structure
|
|
104
|
+
assert.strictEqual(modelOptions[0], `${SEPARATOR_PREFIX} anthropic (2) ${SEPARATOR_PREFIX}`);
|
|
105
|
+
assert.strictEqual(modelOptions[1], "claude-opus-4-6 · anthropic");
|
|
106
|
+
assert.strictEqual(modelOptions[2], "claude-sonnet-4-5 · anthropic");
|
|
107
|
+
assert.strictEqual(modelOptions[3], `${SEPARATOR_PREFIX} openai (2) ${SEPARATOR_PREFIX}`);
|
|
108
|
+
assert.strictEqual(modelOptions[4], "gpt-4o · openai");
|
|
109
|
+
assert.strictEqual(modelOptions[5], "o3-mini · openai");
|
|
110
|
+
assert.strictEqual(modelOptions[6], "(keep current)");
|
|
111
|
+
assert.strictEqual(modelOptions[7], "(clear)");
|
|
112
|
+
|
|
113
|
+
// Verify separators are correctly detected
|
|
114
|
+
assert.ok(isSeparator(modelOptions, 0));
|
|
115
|
+
assert.ok(!isSeparator(modelOptions, 1));
|
|
116
|
+
assert.ok(isSeparator(modelOptions, 3));
|
|
117
|
+
assert.ok(!isSeparator(modelOptions, 6));
|
|
118
|
+
|
|
119
|
+
// Verify first selectable is index 1, not the separator at 0
|
|
120
|
+
assert.strictEqual(nextSelectable(modelOptions, 0, 1), 1);
|
|
143
121
|
});
|
|
144
122
|
});
|