gsd-pi 2.29.0-dev.7612840 → 2.29.0-dev.77f06e2
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 +24 -17
- package/dist/resources/extensions/bg-shell/process-manager.ts +13 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +186 -65
- package/dist/resources/extensions/gsd/auto-post-unit.ts +6 -3
- package/dist/resources/extensions/gsd/auto-recovery.ts +16 -22
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +7 -6
- package/dist/resources/extensions/gsd/commands-handlers.ts +20 -1
- package/dist/resources/extensions/gsd/commands-logs.ts +13 -14
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +44 -14
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
- package/dist/resources/extensions/gsd/commands.ts +53 -21
- package/dist/resources/extensions/gsd/json-persistence.ts +16 -1
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +28 -0
- package/dist/resources/extensions/gsd/queue-order.ts +10 -11
- package/dist/resources/extensions/gsd/session-status-io.ts +23 -41
- package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
- package/dist/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
- package/dist/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
- package/dist/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
- package/dist/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
- package/dist/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
- package/dist/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
- package/dist/resources/extensions/gsd/workflow-templates/registry.json +85 -0
- package/dist/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
- package/dist/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
- package/dist/resources/extensions/gsd/workflow-templates/spike.md +69 -0
- package/dist/resources/extensions/gsd/workflow-templates.ts +241 -0
- package/dist/resources/extensions/mcp-client/index.ts +459 -0
- 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 +13 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -0
- package/src/resources/extensions/bg-shell/process-manager.ts +13 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +186 -65
- package/src/resources/extensions/gsd/auto-post-unit.ts +6 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +16 -22
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +7 -6
- package/src/resources/extensions/gsd/commands-handlers.ts +20 -1
- package/src/resources/extensions/gsd/commands-logs.ts +13 -14
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +44 -14
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
- package/src/resources/extensions/gsd/commands.ts +53 -21
- package/src/resources/extensions/gsd/json-persistence.ts +16 -1
- package/src/resources/extensions/gsd/prompts/workflow-start.md +28 -0
- package/src/resources/extensions/gsd/queue-order.ts +10 -11
- package/src/resources/extensions/gsd/session-status-io.ts +23 -41
- package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
- package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
- package/src/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
- package/src/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
- package/src/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
- package/src/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
- package/src/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
- package/src/resources/extensions/gsd/workflow-templates/registry.json +85 -0
- package/src/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
- package/src/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
- package/src/resources/extensions/gsd/workflow-templates/spike.md +69 -0
- package/src/resources/extensions/gsd/workflow-templates.ts +241 -0
- package/src/resources/extensions/mcp-client/index.ts +459 -0
- package/dist/resources/extensions/mcporter/index.ts +0 -525
- package/src/resources/extensions/mcporter/index.ts +0 -525
|
@@ -176,7 +176,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
176
176
|
);
|
|
177
177
|
try {
|
|
178
178
|
const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
|
|
179
|
-
const { dispatchDoctorHeal } = await import("./commands.js");
|
|
179
|
+
const { dispatchDoctorHeal } = await import("./commands-handlers.js");
|
|
180
180
|
const actionable = report.issues.filter(i => i.severity === "error");
|
|
181
181
|
const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
|
|
182
182
|
const structuredIssues = formatDoctorIssuesForPrompt(actionable);
|
|
@@ -202,10 +202,13 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
// Prune dead bg-shell processes
|
|
205
|
+
// Prune dead bg-shell processes and kill non-persistent live ones.
|
|
206
|
+
// Without killing live processes between units, dev servers spawned during
|
|
207
|
+
// one task keep ports bound, causing conflicts in subsequent tasks (#1209).
|
|
206
208
|
try {
|
|
207
|
-
const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
|
|
209
|
+
const { pruneDeadProcesses, killSessionProcesses } = await import("../bg-shell/process-manager.js");
|
|
208
210
|
pruneDeadProcesses();
|
|
211
|
+
killSessionProcesses();
|
|
209
212
|
} catch {
|
|
210
213
|
// Non-fatal
|
|
211
214
|
}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import { isValidationTerminal } from "./state.js";
|
|
40
40
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
41
41
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
42
|
+
import { loadJsonFileOrNull } from "./json-persistence.js";
|
|
42
43
|
import { dirname, join } from "node:path";
|
|
43
44
|
|
|
44
45
|
// ─── Artifact Resolution & Verification ───────────────────────────────────────
|
|
@@ -354,6 +355,10 @@ export function skipExecuteTask(
|
|
|
354
355
|
|
|
355
356
|
// ─── Disk-backed completed-unit helpers ───────────────────────────────────────
|
|
356
357
|
|
|
358
|
+
function isStringArray(data: unknown): data is string[] {
|
|
359
|
+
return Array.isArray(data) && data.every(item => typeof item === "string");
|
|
360
|
+
}
|
|
361
|
+
|
|
357
362
|
/** Path to the persisted completed-unit keys file. */
|
|
358
363
|
export function completedKeysPath(base: string): string {
|
|
359
364
|
return join(base, ".gsd", "completed-units.json");
|
|
@@ -362,12 +367,7 @@ export function completedKeysPath(base: string): string {
|
|
|
362
367
|
/** Write a completed unit key to disk (read-modify-write append to set). */
|
|
363
368
|
export function persistCompletedKey(base: string, key: string): void {
|
|
364
369
|
const file = completedKeysPath(base);
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
if (existsSync(file)) {
|
|
368
|
-
keys = JSON.parse(readFileSync(file, "utf-8"));
|
|
369
|
-
}
|
|
370
|
-
} catch (e) { /* corrupt file — start fresh */ void e; }
|
|
370
|
+
const keys = loadJsonFileOrNull(file, isStringArray) ?? [];
|
|
371
371
|
const keySet = new Set(keys);
|
|
372
372
|
if (!keySet.has(key)) {
|
|
373
373
|
keys.push(key);
|
|
@@ -378,27 +378,21 @@ export function persistCompletedKey(base: string, key: string): void {
|
|
|
378
378
|
/** Remove a stale completed unit key from disk. */
|
|
379
379
|
export function removePersistedKey(base: string, key: string): void {
|
|
380
380
|
const file = completedKeysPath(base);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
atomicWriteSync(file, JSON.stringify(filtered));
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
} catch (e) { /* non-fatal: removePersistedKey failure */ void e; }
|
|
381
|
+
const keys = loadJsonFileOrNull(file, isStringArray);
|
|
382
|
+
if (!keys) return;
|
|
383
|
+
const filtered = keys.filter(k => k !== key);
|
|
384
|
+
if (filtered.length !== keys.length) {
|
|
385
|
+
atomicWriteSync(file, JSON.stringify(filtered));
|
|
386
|
+
}
|
|
391
387
|
}
|
|
392
388
|
|
|
393
389
|
/** Load all completed unit keys from disk into the in-memory set. */
|
|
394
390
|
export function loadPersistedKeys(base: string, target: Set<string>): void {
|
|
395
391
|
const file = completedKeysPath(base);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
} catch (e) { /* non-fatal: loadPersistedKeys failure */ void e; }
|
|
392
|
+
const keys = loadJsonFileOrNull(file, isStringArray);
|
|
393
|
+
if (keys) {
|
|
394
|
+
for (const k of keys) target.add(k);
|
|
395
|
+
}
|
|
402
396
|
}
|
|
403
397
|
|
|
404
398
|
// ─── Merge State Reconciliation ───────────────────────────────────────────────
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, cpSync, unlinkSync, readdirSync } from "node:fs";
|
|
14
|
+
import { loadJsonFileOrNull } from "./json-persistence.js";
|
|
14
15
|
import { join, sep as pathSep } from "node:path";
|
|
15
16
|
import { homedir } from "node:os";
|
|
16
17
|
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
|
|
@@ -112,15 +113,15 @@ export function syncStateToProjectRoot(worktreePath: string, projectRoot: string
|
|
|
112
113
|
* Uses gsdVersion instead of syncedAt so that launching a second session
|
|
113
114
|
* doesn't falsely trigger staleness (#804).
|
|
114
115
|
*/
|
|
116
|
+
function isManifestWithVersion(data: unknown): data is { gsdVersion: string } {
|
|
117
|
+
return data !== null && typeof data === "object" && "gsdVersion" in data! && typeof (data as Record<string, unknown>).gsdVersion === "string";
|
|
118
|
+
}
|
|
119
|
+
|
|
115
120
|
export function readResourceVersion(): string | null {
|
|
116
121
|
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
|
|
117
122
|
const manifestPath = join(agentDir, "managed-resources.json");
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return typeof manifest?.gsdVersion === "string" ? manifest.gsdVersion : null;
|
|
121
|
-
} catch {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
123
|
+
const manifest = loadJsonFileOrNull(manifestPath, isManifestWithVersion);
|
|
124
|
+
return manifest?.gsdVersion ?? null;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
/**
|
|
@@ -19,7 +19,26 @@ import {
|
|
|
19
19
|
filterDoctorIssues,
|
|
20
20
|
} from "./doctor.js";
|
|
21
21
|
import { isAutoActive } from "./auto.js";
|
|
22
|
-
import { projectRoot
|
|
22
|
+
import { projectRoot } from "./commands.js";
|
|
23
|
+
import { loadPrompt } from "./prompt-loader.js";
|
|
24
|
+
|
|
25
|
+
export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
|
|
26
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
27
|
+
const workflow = readFileSync(workflowPath, "utf-8");
|
|
28
|
+
const prompt = loadPrompt("doctor-heal", {
|
|
29
|
+
doctorSummary: reportText,
|
|
30
|
+
structuredIssues,
|
|
31
|
+
scopeLabel: scope ?? "active milestone / blocking scope",
|
|
32
|
+
doctorCommandSuffix: scope ? ` ${scope}` : "",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const content = `Read the following GSD workflow protocol and execute exactly.\n\n${workflow}\n\n## Your Task\n\n${prompt}`;
|
|
36
|
+
|
|
37
|
+
pi.sendMessage(
|
|
38
|
+
{ customType: "gsd-doctor-heal", content, display: false },
|
|
39
|
+
{ triggerTurn: true },
|
|
40
|
+
);
|
|
41
|
+
}
|
|
23
42
|
|
|
24
43
|
export async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
25
44
|
const trimmed = args.trim();
|
|
@@ -14,6 +14,7 @@ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
|
14
14
|
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { gsdRoot } from "./paths.js";
|
|
17
|
+
import { loadJsonFileOrNull } from "./json-persistence.js";
|
|
17
18
|
|
|
18
19
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -331,20 +332,18 @@ async function handleLogsList(basePath: string, ctx: ExtensionCommandContext): P
|
|
|
331
332
|
|
|
332
333
|
// Metrics summary
|
|
333
334
|
const metricsPath = join(gsdRoot(basePath), "metrics.json");
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
} catch { /* ignore */ }
|
|
335
|
+
const isMetrics = (d: unknown): d is { units: Array<Record<string, unknown>> } =>
|
|
336
|
+
d !== null && typeof d === "object" && "units" in d! && Array.isArray((d as Record<string, unknown>).units);
|
|
337
|
+
const metrics = loadJsonFileOrNull(metricsPath, isMetrics);
|
|
338
|
+
if (metrics && metrics.units.length > 0) {
|
|
339
|
+
const units = metrics.units;
|
|
340
|
+
const totalCost = units.reduce((sum: number, u) => sum + ((u.cost as number) ?? 0), 0);
|
|
341
|
+
const totalTokens = units.reduce((sum: number, u) => {
|
|
342
|
+
const t = u.tokens as Record<string, number> | undefined;
|
|
343
|
+
return sum + (t?.total ?? 0);
|
|
344
|
+
}, 0);
|
|
345
|
+
lines.push("");
|
|
346
|
+
lines.push(`Metrics: ${units.length} units tracked · $${totalCost.toFixed(2)} · ${(totalTokens / 1000).toFixed(0)}K tokens`);
|
|
348
347
|
}
|
|
349
348
|
|
|
350
349
|
lines.push("");
|
|
@@ -260,27 +260,57 @@ 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
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
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
|
-
}
|
|
263
|
+
// Sort models within each provider
|
|
264
|
+
for (const group of byProvider.values()) {
|
|
265
|
+
group.sort((a, b) => a.id.localeCompare(b.id));
|
|
271
266
|
}
|
|
272
|
-
|
|
267
|
+
|
|
268
|
+
// Build provider menu with model counts
|
|
269
|
+
const providerOptions = providers.map(p => {
|
|
270
|
+
const count = byProvider.get(p)!.length;
|
|
271
|
+
return `${p} (${count} models)`;
|
|
272
|
+
});
|
|
273
|
+
providerOptions.push("(keep current)", "(clear)", "(type manually)");
|
|
273
274
|
|
|
274
275
|
for (const phase of modelPhases) {
|
|
275
276
|
const current = models[phase] ?? "";
|
|
276
|
-
const
|
|
277
|
-
|
|
277
|
+
const phaseLabel = `Model for ${phase} phase${current ? ` (current: ${current})` : ""}`;
|
|
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)");
|
|
278
307
|
|
|
279
|
-
|
|
280
|
-
|
|
308
|
+
const modelChoice = await ctx.ui.select(`${phaseLabel} — ${providerName}:`, modelOptions);
|
|
309
|
+
if (modelChoice && typeof modelChoice === "string" && modelChoice !== "(keep current)") {
|
|
310
|
+
if (modelChoice === "(clear)") {
|
|
281
311
|
delete models[phase];
|
|
282
312
|
} else {
|
|
283
|
-
models[phase] =
|
|
313
|
+
models[phase] = modelChoice;
|
|
284
314
|
}
|
|
285
315
|
}
|
|
286
316
|
}
|