gsd-pi 2.28.0-dev.853dfc5 → 2.28.0-dev.a8d3050
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/resource-loader.js +80 -8
- package/dist/resources/extensions/gsd/auto-verification.ts +41 -7
- package/dist/resources/extensions/gsd/auto.ts +2 -2
- package/dist/resources/extensions/gsd/commands-handlers.ts +1 -9
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +14 -22
- package/dist/resources/extensions/gsd/commands.ts +1 -7
- package/dist/resources/extensions/gsd/index.ts +2 -1
- package/dist/resources/extensions/gsd/json-persistence.ts +52 -0
- package/dist/resources/extensions/gsd/metrics.ts +17 -31
- package/dist/resources/extensions/gsd/paths.ts +0 -8
- package/dist/resources/extensions/gsd/routing-history.ts +13 -17
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
- package/dist/resources/extensions/gsd/types.ts +1 -0
- package/dist/resources/extensions/gsd/unit-runtime.ts +16 -13
- package/dist/resources/extensions/gsd/verification-evidence.ts +2 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +13 -2
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -2
- package/dist/resources/extensions/remote-questions/notify.ts +1 -2
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +1 -2
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +1 -2
- package/dist/resources/extensions/remote-questions/types.ts +3 -0
- package/dist/resources/extensions/shared/mod.ts +3 -0
- package/package.json +4 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -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 +8 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +10 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- 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 +4 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/scripts/copy-assets.cjs +39 -8
- package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +11 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -1
- package/packages/pi-tui/dist/autocomplete.d.ts +3 -0
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +14 -0
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/src/autocomplete.ts +19 -1
- package/src/resources/extensions/gsd/auto-verification.ts +41 -7
- package/src/resources/extensions/gsd/auto.ts +2 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +1 -9
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -22
- package/src/resources/extensions/gsd/commands.ts +1 -7
- package/src/resources/extensions/gsd/index.ts +2 -1
- package/src/resources/extensions/gsd/json-persistence.ts +52 -0
- package/src/resources/extensions/gsd/metrics.ts +17 -31
- package/src/resources/extensions/gsd/paths.ts +0 -8
- package/src/resources/extensions/gsd/routing-history.ts +13 -17
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
- package/src/resources/extensions/gsd/types.ts +1 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +16 -13
- package/src/resources/extensions/gsd/verification-evidence.ts +2 -0
- package/src/resources/extensions/gsd/verification-gate.ts +13 -2
- package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -2
- package/src/resources/extensions/remote-questions/notify.ts +1 -2
- package/src/resources/extensions/remote-questions/slack-adapter.ts +1 -2
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +1 -2
- package/src/resources/extensions/remote-questions/types.ts +3 -0
- package/src/resources/extensions/shared/mod.ts +3 -0
- package/dist/resources/extensions/gsd/preferences-hooks.ts +0 -10
- package/dist/resources/extensions/shared/progress-widget.ts +0 -282
- package/dist/resources/extensions/shared/thinking-widget.ts +0 -107
- package/src/resources/extensions/gsd/preferences-hooks.ts +0 -10
- package/src/resources/extensions/shared/progress-widget.ts +0 -282
- package/src/resources/extensions/shared/thinking-widget.ts +0 -107
package/dist/resource-loader.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DefaultResourceLoader } from '@gsd/pi-coding-agent';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
|
-
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
5
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { compareSemver } from './update-check.js';
|
|
@@ -41,7 +42,11 @@ function getBundledGsdVersion() {
|
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
function writeManagedResourceManifest(agentDir) {
|
|
44
|
-
const manifest = {
|
|
45
|
+
const manifest = {
|
|
46
|
+
gsdVersion: getBundledGsdVersion(),
|
|
47
|
+
syncedAt: Date.now(),
|
|
48
|
+
contentHash: computeResourceFingerprint(),
|
|
49
|
+
};
|
|
45
50
|
writeFileSync(getManagedResourceManifestPath(agentDir), JSON.stringify(manifest));
|
|
46
51
|
}
|
|
47
52
|
export function readManagedResourceVersion(agentDir) {
|
|
@@ -53,6 +58,44 @@ export function readManagedResourceVersion(agentDir) {
|
|
|
53
58
|
return null;
|
|
54
59
|
}
|
|
55
60
|
}
|
|
61
|
+
function readManagedResourceManifest(agentDir) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(readFileSync(getManagedResourceManifestPath(agentDir), 'utf-8'));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Computes a lightweight content fingerprint of the bundled resources directory.
|
|
71
|
+
*
|
|
72
|
+
* Walks all files under resourcesDir and hashes their relative paths + sizes.
|
|
73
|
+
* This catches same-version content changes (npm link dev workflow, hotfixes
|
|
74
|
+
* within a release) without the cost of reading every file's contents.
|
|
75
|
+
*
|
|
76
|
+
* ~1ms for a typical resources tree (~100 files) — just stat calls, no reads.
|
|
77
|
+
*/
|
|
78
|
+
function computeResourceFingerprint() {
|
|
79
|
+
const entries = [];
|
|
80
|
+
collectFileEntries(resourcesDir, resourcesDir, entries);
|
|
81
|
+
entries.sort();
|
|
82
|
+
return createHash('sha256').update(entries.join('\n')).digest('hex').slice(0, 16);
|
|
83
|
+
}
|
|
84
|
+
function collectFileEntries(dir, root, out) {
|
|
85
|
+
if (!existsSync(dir))
|
|
86
|
+
return;
|
|
87
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
88
|
+
const fullPath = join(dir, entry.name);
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
collectFileEntries(fullPath, root, out);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const rel = relative(root, fullPath);
|
|
94
|
+
const size = statSync(fullPath).size;
|
|
95
|
+
out.push(`${rel}:${size}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
56
99
|
export function getNewerManagedResourceVersion(agentDir, currentVersion) {
|
|
57
100
|
const managedVersion = readManagedResourceVersion(agentDir);
|
|
58
101
|
if (!managedVersion) {
|
|
@@ -111,10 +154,34 @@ function syncResourceDir(srcDir, destDir) {
|
|
|
111
154
|
rmSync(target, { recursive: true, force: true });
|
|
112
155
|
}
|
|
113
156
|
}
|
|
114
|
-
|
|
157
|
+
try {
|
|
158
|
+
cpSync(srcDir, destDir, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Fallback for Windows paths with non-ASCII characters where cpSync
|
|
162
|
+
// fails with the \\?\ extended-length prefix (#1178).
|
|
163
|
+
copyDirRecursive(srcDir, destDir);
|
|
164
|
+
}
|
|
115
165
|
makeTreeWritable(destDir);
|
|
116
166
|
}
|
|
117
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Recursive directory copy using copyFileSync — workaround for cpSync failures
|
|
170
|
+
* on Windows paths containing non-ASCII characters (#1178).
|
|
171
|
+
*/
|
|
172
|
+
function copyDirRecursive(src, dest) {
|
|
173
|
+
mkdirSync(dest, { recursive: true });
|
|
174
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
175
|
+
const srcPath = join(src, entry.name);
|
|
176
|
+
const destPath = join(dest, entry.name);
|
|
177
|
+
if (entry.isDirectory()) {
|
|
178
|
+
copyDirRecursive(srcPath, destPath);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
copyFileSync(srcPath, destPath);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
118
185
|
/**
|
|
119
186
|
* Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
|
|
120
187
|
*
|
|
@@ -132,12 +199,17 @@ function syncResourceDir(srcDir, destDir) {
|
|
|
132
199
|
*/
|
|
133
200
|
export function initResources(agentDir) {
|
|
134
201
|
mkdirSync(agentDir, { recursive: true });
|
|
135
|
-
// Skip the full copy when
|
|
136
|
-
//
|
|
202
|
+
// Skip the full copy when both version AND content fingerprint match.
|
|
203
|
+
// Version-only checks miss same-version content changes (npm link dev workflow,
|
|
204
|
+
// hotfixes within a release). The content hash catches those at ~1ms cost.
|
|
137
205
|
const currentVersion = getBundledGsdVersion();
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
|
|
206
|
+
const manifest = readManagedResourceManifest(agentDir);
|
|
207
|
+
if (manifest && manifest.gsdVersion === currentVersion) {
|
|
208
|
+
// Version matches — check content fingerprint for same-version staleness.
|
|
209
|
+
const currentHash = computeResourceFingerprint();
|
|
210
|
+
if (manifest.contentHash && manifest.contentHash === currentHash) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
141
213
|
}
|
|
142
214
|
syncResourceDir(bundledExtensionsDir, join(agentDir, 'extensions'));
|
|
143
215
|
syncResourceDir(join(resourcesDir, 'agents'), join(agentDir, 'agents'));
|
|
@@ -105,19 +105,39 @@ export async function runPostUnitVerification(
|
|
|
105
105
|
const completionKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
|
|
106
106
|
|
|
107
107
|
if (result.checks.length > 0) {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
108
|
+
const blockingChecks = result.checks.filter(c => c.blocking);
|
|
109
|
+
const advisoryChecks = result.checks.filter(c => !c.blocking);
|
|
110
|
+
const blockingPassCount = blockingChecks.filter(c => c.exitCode === 0).length;
|
|
111
|
+
const advisoryFailCount = advisoryChecks.filter(c => c.exitCode !== 0).length;
|
|
112
|
+
|
|
110
113
|
if (result.passed) {
|
|
111
|
-
|
|
114
|
+
let msg = blockingChecks.length > 0
|
|
115
|
+
? `Verification gate: ${blockingPassCount}/${blockingChecks.length} blocking checks passed`
|
|
116
|
+
: `Verification gate: passed (no blocking checks)`;
|
|
117
|
+
if (advisoryFailCount > 0) {
|
|
118
|
+
msg += ` (${advisoryFailCount} advisory warning${advisoryFailCount > 1 ? "s" : ""})`;
|
|
119
|
+
}
|
|
120
|
+
ctx.ui.notify(msg);
|
|
121
|
+
// Log advisory warnings to stderr for visibility
|
|
122
|
+
if (advisoryFailCount > 0) {
|
|
123
|
+
const advisoryFailures = advisoryChecks.filter(c => c.exitCode !== 0);
|
|
124
|
+
process.stderr.write(`verification-gate: ${advisoryFailCount} advisory (non-blocking) failure(s)\n`);
|
|
125
|
+
for (const f of advisoryFailures) {
|
|
126
|
+
process.stderr.write(` [advisory] ${f.command} exited ${f.exitCode}\n`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
112
129
|
} else {
|
|
113
|
-
const
|
|
114
|
-
const failNames =
|
|
130
|
+
const blockingFailures = blockingChecks.filter(c => c.exitCode !== 0);
|
|
131
|
+
const failNames = blockingFailures.map(f => f.command).join(", ");
|
|
115
132
|
ctx.ui.notify(`Verification gate: FAILED — ${failNames}`);
|
|
116
|
-
process.stderr.write(`verification-gate: ${
|
|
117
|
-
for (const f of
|
|
133
|
+
process.stderr.write(`verification-gate: ${blockingFailures.length}/${blockingChecks.length} blocking checks failed\n`);
|
|
134
|
+
for (const f of blockingFailures) {
|
|
118
135
|
process.stderr.write(` ${f.command} exited ${f.exitCode}\n`);
|
|
119
136
|
if (f.stderr) process.stderr.write(` stderr: ${f.stderr.slice(0, 500)}\n`);
|
|
120
137
|
}
|
|
138
|
+
if (advisoryFailCount > 0) {
|
|
139
|
+
process.stderr.write(`verification-gate: ${advisoryFailCount} additional advisory (non-blocking) failure(s)\n`);
|
|
140
|
+
}
|
|
121
141
|
}
|
|
122
142
|
}
|
|
123
143
|
|
|
@@ -155,6 +175,20 @@ export async function runPostUnitVerification(
|
|
|
155
175
|
s.verificationRetryCount.delete(s.currentUnit.id);
|
|
156
176
|
s.pendingVerificationRetry = null;
|
|
157
177
|
return "continue";
|
|
178
|
+
} else if (result.discoverySource === "package-json") {
|
|
179
|
+
// Auto-discovered checks from package.json may fail on pre-existing errors
|
|
180
|
+
// that the current task didn't introduce. Don't trigger the retry loop —
|
|
181
|
+
// log a warning and let the task proceed (#1186).
|
|
182
|
+
process.stderr.write(
|
|
183
|
+
`verification-gate: auto-discovered checks failed (source: package-json) — treating as advisory, not blocking\n`,
|
|
184
|
+
);
|
|
185
|
+
ctx.ui.notify(
|
|
186
|
+
`Verification: auto-discovered checks failed (pre-existing errors likely). Continuing without retry.`,
|
|
187
|
+
"warning",
|
|
188
|
+
);
|
|
189
|
+
s.verificationRetryCount.delete(s.currentUnit.id);
|
|
190
|
+
s.pendingVerificationRetry = null;
|
|
191
|
+
return "continue";
|
|
158
192
|
} else if (autoFixEnabled && attempt + 1 <= maxRetries) {
|
|
159
193
|
const nextAttempt = attempt + 1;
|
|
160
194
|
s.verificationRetryCount.set(s.currentUnit.id, nextAttempt);
|
|
@@ -1212,7 +1212,7 @@ async function dispatchNextUnit(
|
|
|
1212
1212
|
try { process.chdir(s.basePath); } catch { /* best-effort */ }
|
|
1213
1213
|
}
|
|
1214
1214
|
}
|
|
1215
|
-
} else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode()
|
|
1215
|
+
} else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode() === "branch") {
|
|
1216
1216
|
try {
|
|
1217
1217
|
const currentBranch = getCurrentBranch(s.basePath);
|
|
1218
1218
|
const milestoneBranch = autoWorktreeBranch(s.currentMilestoneId);
|
|
@@ -1314,7 +1314,7 @@ async function dispatchNextUnit(
|
|
|
1314
1314
|
try { process.chdir(s.basePath); } catch { /* best-effort */ }
|
|
1315
1315
|
}
|
|
1316
1316
|
}
|
|
1317
|
-
} else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode()
|
|
1317
|
+
} else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode() === "branch") {
|
|
1318
1318
|
try {
|
|
1319
1319
|
const currentBranch = getCurrentBranch(s.basePath);
|
|
1320
1320
|
const milestoneBranch = autoWorktreeBranch(s.currentMilestoneId);
|
|
@@ -20,15 +20,7 @@ import {
|
|
|
20
20
|
} from "./doctor.js";
|
|
21
21
|
import { loadPrompt } from "./prompt-loader.js";
|
|
22
22
|
import { isAutoActive } from "./auto.js";
|
|
23
|
-
import {
|
|
24
|
-
import { assertSafeDirectory } from "./validate-directory.js";
|
|
25
|
-
|
|
26
|
-
/** Resolve the effective project root, accounting for worktree paths. */
|
|
27
|
-
function projectRoot(): string {
|
|
28
|
-
const root = resolveProjectRoot(process.cwd());
|
|
29
|
-
assertSafeDirectory(root);
|
|
30
|
-
return root;
|
|
31
|
-
}
|
|
23
|
+
import { projectRoot } from "./commands.js";
|
|
32
24
|
|
|
33
25
|
function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
|
|
34
26
|
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
@@ -22,6 +22,14 @@ import {
|
|
|
22
22
|
import { loadFile, saveFile, splitFrontmatter, parseFrontmatterMap } from "./files.js";
|
|
23
23
|
import { runClaudeImportFlow } from "./claude-import.js";
|
|
24
24
|
|
|
25
|
+
/** Extract body content after frontmatter closing delimiter, or null if none. */
|
|
26
|
+
function extractBodyAfterFrontmatter(content: string): string | null {
|
|
27
|
+
const closingIdx = content.indexOf("\n---", content.indexOf("---"));
|
|
28
|
+
if (closingIdx === -1) return null;
|
|
29
|
+
const afterFrontmatter = content.slice(closingIdx + 4);
|
|
30
|
+
return afterFrontmatter.trim() ? afterFrontmatter : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
export async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
26
34
|
const trimmed = args.trim();
|
|
27
35
|
|
|
@@ -98,12 +106,8 @@ export async function handleImportClaude(ctx: ExtensionCommandContext, scope: "g
|
|
|
98
106
|
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
99
107
|
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
100
108
|
if (existsSync(path)) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
if (closingIdx !== -1) {
|
|
104
|
-
const afterFrontmatter = existingContent.slice(closingIdx + 4);
|
|
105
|
-
if (afterFrontmatter.trim()) body = afterFrontmatter;
|
|
106
|
-
}
|
|
109
|
+
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
110
|
+
if (preserved) body = preserved;
|
|
107
111
|
}
|
|
108
112
|
await saveFile(path, `---\n${frontmatter}---${body}`);
|
|
109
113
|
};
|
|
@@ -124,14 +128,8 @@ export async function handlePrefsMode(ctx: ExtensionCommandContext, scope: "glob
|
|
|
124
128
|
|
|
125
129
|
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
126
130
|
if (existsSync(path)) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
if (closingIdx !== -1) {
|
|
130
|
-
const afterFrontmatter = existingContent.slice(closingIdx + 4);
|
|
131
|
-
if (afterFrontmatter.trim()) {
|
|
132
|
-
body = afterFrontmatter;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
131
|
+
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
132
|
+
if (preserved) body = preserved;
|
|
135
133
|
}
|
|
136
134
|
|
|
137
135
|
const content = `---\n${frontmatter}---${body}`;
|
|
@@ -622,14 +620,8 @@ export async function handlePrefsWizard(
|
|
|
622
620
|
// Preserve existing body content (everything after closing ---)
|
|
623
621
|
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
624
622
|
if (existsSync(path)) {
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
if (closingIdx !== -1) {
|
|
628
|
-
const afterFrontmatter = existingContent.slice(closingIdx + 4); // skip past "\n---"
|
|
629
|
-
if (afterFrontmatter.trim()) {
|
|
630
|
-
body = afterFrontmatter;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
623
|
+
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
624
|
+
if (preserved) body = preserved;
|
|
633
625
|
}
|
|
634
626
|
|
|
635
627
|
const content = `---\n${frontmatter}---${body}`;
|
|
@@ -45,12 +45,6 @@ import { handleCleanupBranches, handleCleanupSnapshots, handleSkip, handleDryRun
|
|
|
45
45
|
import { handleDoctor, handleSteer, handleCapture, handleTriage, handleKnowledge, handleRunHook, handleUpdate, handleSkillHealth } from "./commands-handlers.js";
|
|
46
46
|
import { handleLogs } from "./commands-logs.js";
|
|
47
47
|
|
|
48
|
-
// ─── Re-exports (preserve public API surface) ───────────────────────────────
|
|
49
|
-
export { handlePrefs, handlePrefsMode, handlePrefsWizard, ensurePreferencesFile, handleImportClaude, buildCategorySummaries, serializePreferencesToFrontmatter, yamlSafeString, configureMode } from "./commands-prefs-wizard.js";
|
|
50
|
-
export { TOOL_KEYS, loadToolApiKeys, getConfigAuthStorage, handleConfig } from "./commands-config.js";
|
|
51
|
-
export { type InspectData, formatInspectOutput, handleInspect } from "./commands-inspect.js";
|
|
52
|
-
export { handleCleanupBranches, handleCleanupSnapshots, handleSkip, handleDryRun } from "./commands-maintenance.js";
|
|
53
|
-
export { handleDoctor, handleSteer, handleCapture, handleTriage, handleKnowledge, handleRunHook, handleUpdate, handleSkillHealth } from "./commands-handlers.js";
|
|
54
48
|
|
|
55
49
|
export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
|
|
56
50
|
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
@@ -846,7 +840,7 @@ function showHelp(ctx: ExtensionCommandContext): void {
|
|
|
846
840
|
" /gsd init Project init wizard — detect, configure, bootstrap .gsd/",
|
|
847
841
|
" /gsd setup Global setup status [llm|search|remote|keys|prefs]",
|
|
848
842
|
" /gsd mode Set workflow mode (solo/team) [global|project]",
|
|
849
|
-
" /gsd prefs Manage preferences [global|project|status|wizard|setup]",
|
|
843
|
+
" /gsd prefs Manage preferences [global|project|status|wizard|setup|import-claude]",
|
|
850
844
|
" /gsd config Set API keys for external tools",
|
|
851
845
|
" /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
|
|
852
846
|
" /gsd hooks Show post-unit hook configuration",
|
|
@@ -27,7 +27,8 @@ import { createBashTool, createWriteTool, createReadTool, createEditTool, isTool
|
|
|
27
27
|
import { Type } from "@sinclair/typebox";
|
|
28
28
|
|
|
29
29
|
import { debugLog, debugTime } from "./debug-logger.js";
|
|
30
|
-
import { registerGSDCommand
|
|
30
|
+
import { registerGSDCommand } from "./commands.js";
|
|
31
|
+
import { loadToolApiKeys } from "./commands-config.js";
|
|
31
32
|
import { registerExitCommand } from "./exit-command.js";
|
|
32
33
|
import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
|
|
33
34
|
import { getActiveAutoWorktreeContext } from "./auto-worktree.js";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Load a JSON file with validation, returning a default on failure.
|
|
6
|
+
* Handles missing files, corrupt JSON, and schema mismatches uniformly.
|
|
7
|
+
*/
|
|
8
|
+
export function loadJsonFile<T>(
|
|
9
|
+
filePath: string,
|
|
10
|
+
validate: (data: unknown) => data is T,
|
|
11
|
+
defaultFactory: () => T,
|
|
12
|
+
): T {
|
|
13
|
+
try {
|
|
14
|
+
if (!existsSync(filePath)) return defaultFactory();
|
|
15
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
return validate(parsed) ? parsed : defaultFactory();
|
|
18
|
+
} catch {
|
|
19
|
+
return defaultFactory();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load a JSON file with validation, returning null on failure.
|
|
25
|
+
* For callers that distinguish "no data" from "default data".
|
|
26
|
+
*/
|
|
27
|
+
export function loadJsonFileOrNull<T>(
|
|
28
|
+
filePath: string,
|
|
29
|
+
validate: (data: unknown) => data is T,
|
|
30
|
+
): T | null {
|
|
31
|
+
try {
|
|
32
|
+
if (!existsSync(filePath)) return null;
|
|
33
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
return validate(parsed) ? parsed : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Save a JSON file, creating parent directories as needed.
|
|
43
|
+
* Non-fatal — swallows errors to prevent persistence from breaking operations.
|
|
44
|
+
*/
|
|
45
|
+
export function saveJsonFile<T>(filePath: string, data: T): void {
|
|
46
|
+
try {
|
|
47
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
48
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
49
|
+
} catch {
|
|
50
|
+
// Non-fatal — don't let persistence failures break operation
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* 4. On crash recovery or fresh start, the ledger is loaded from disk
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
17
16
|
import { join } from "node:path";
|
|
18
17
|
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
19
18
|
import { gsdRoot } from "./paths.js";
|
|
20
19
|
import { getAndClearSkills } from "./skill-telemetry.js";
|
|
20
|
+
import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
21
21
|
|
|
22
22
|
// Re-export from shared — canonical implementation lives in format-utils.
|
|
23
23
|
export { formatTokenCount } from "../shared/mod.js";
|
|
@@ -502,45 +502,31 @@ function metricsPath(base: string): string {
|
|
|
502
502
|
return join(gsdRoot(base), "metrics.json");
|
|
503
503
|
}
|
|
504
504
|
|
|
505
|
+
function isMetricsLedger(data: unknown): data is MetricsLedger {
|
|
506
|
+
return (
|
|
507
|
+
typeof data === "object" &&
|
|
508
|
+
data !== null &&
|
|
509
|
+
(data as MetricsLedger).version === 1 &&
|
|
510
|
+
Array.isArray((data as MetricsLedger).units)
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function defaultLedger(): MetricsLedger {
|
|
515
|
+
return { version: 1, projectStartedAt: Date.now(), units: [] };
|
|
516
|
+
}
|
|
517
|
+
|
|
505
518
|
/**
|
|
506
519
|
* Load ledger from disk without initializing in-memory state.
|
|
507
520
|
* Used by history/export commands outside of auto-mode.
|
|
508
521
|
*/
|
|
509
522
|
export function loadLedgerFromDisk(base: string): MetricsLedger | null {
|
|
510
|
-
|
|
511
|
-
const raw = readFileSync(metricsPath(base), "utf-8");
|
|
512
|
-
const parsed = JSON.parse(raw);
|
|
513
|
-
if (parsed.version === 1 && Array.isArray(parsed.units)) {
|
|
514
|
-
return parsed as MetricsLedger;
|
|
515
|
-
}
|
|
516
|
-
} catch {
|
|
517
|
-
// File doesn't exist or is corrupt
|
|
518
|
-
}
|
|
519
|
-
return null;
|
|
523
|
+
return loadJsonFileOrNull(metricsPath(base), isMetricsLedger);
|
|
520
524
|
}
|
|
521
525
|
|
|
522
526
|
function loadLedger(base: string): MetricsLedger {
|
|
523
|
-
|
|
524
|
-
const raw = readFileSync(metricsPath(base), "utf-8");
|
|
525
|
-
const parsed = JSON.parse(raw);
|
|
526
|
-
if (parsed.version === 1 && Array.isArray(parsed.units)) {
|
|
527
|
-
return parsed as MetricsLedger;
|
|
528
|
-
}
|
|
529
|
-
} catch {
|
|
530
|
-
// File doesn't exist or is corrupt — start fresh
|
|
531
|
-
}
|
|
532
|
-
return {
|
|
533
|
-
version: 1,
|
|
534
|
-
projectStartedAt: Date.now(),
|
|
535
|
-
units: [],
|
|
536
|
-
};
|
|
527
|
+
return loadJsonFile(metricsPath(base), isMetricsLedger, defaultLedger);
|
|
537
528
|
}
|
|
538
529
|
|
|
539
530
|
function saveLedger(base: string, data: MetricsLedger): void {
|
|
540
|
-
|
|
541
|
-
mkdirSync(gsdRoot(base), { recursive: true });
|
|
542
|
-
writeFileSync(metricsPath(base), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
543
|
-
} catch {
|
|
544
|
-
// Don't let metrics failures break auto-mode
|
|
545
|
-
}
|
|
531
|
+
saveJsonFile(metricsPath(base), data);
|
|
546
532
|
}
|
|
@@ -137,14 +137,6 @@ export function clearPathCache(): void {
|
|
|
137
137
|
|
|
138
138
|
// ─── Name Builders ─────────────────────────────────────────────────────────
|
|
139
139
|
|
|
140
|
-
/**
|
|
141
|
-
* Build a directory name from an ID.
|
|
142
|
-
* ("M001") → "M001"
|
|
143
|
-
*/
|
|
144
|
-
export function buildDirName(id: string): string {
|
|
145
|
-
return id;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
140
|
/**
|
|
149
141
|
* Build a milestone-level file name.
|
|
150
142
|
* ("M001", "CONTEXT") → "M001-CONTEXT.md"
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// Tracks success/failure per tier per unit-type pattern to improve
|
|
3
3
|
// classification accuracy over time.
|
|
4
4
|
|
|
5
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
6
5
|
import { join } from "node:path";
|
|
7
6
|
import { gsdRoot } from "./paths.js";
|
|
8
7
|
import type { ComplexityTier } from "./types.js";
|
|
8
|
+
import { loadJsonFile, saveJsonFile } from "./json-persistence.js";
|
|
9
9
|
|
|
10
10
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
11
11
|
|
|
@@ -267,24 +267,20 @@ function historyPath(base: string): string {
|
|
|
267
267
|
return join(gsdRoot(base), HISTORY_FILE);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
function isRoutingHistoryData(data: unknown): data is RoutingHistoryData {
|
|
271
|
+
return (
|
|
272
|
+
typeof data === "object" &&
|
|
273
|
+
data !== null &&
|
|
274
|
+
(data as RoutingHistoryData).version === 1 &&
|
|
275
|
+
typeof (data as RoutingHistoryData).patterns === "object" &&
|
|
276
|
+
(data as RoutingHistoryData).patterns !== null
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
270
280
|
function loadHistory(base: string): RoutingHistoryData {
|
|
271
|
-
|
|
272
|
-
const raw = readFileSync(historyPath(base), "utf-8");
|
|
273
|
-
const parsed = JSON.parse(raw);
|
|
274
|
-
if (parsed.version === 1 && parsed.patterns) {
|
|
275
|
-
return parsed as RoutingHistoryData;
|
|
276
|
-
}
|
|
277
|
-
} catch {
|
|
278
|
-
// File doesn't exist or is corrupt — start fresh
|
|
279
|
-
}
|
|
280
|
-
return createEmptyHistory();
|
|
281
|
+
return loadJsonFile(historyPath(base), isRoutingHistoryData, createEmptyHistory);
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
function saveHistory(base: string, data: RoutingHistoryData): void {
|
|
284
|
-
|
|
285
|
-
mkdirSync(gsdRoot(base), { recursive: true });
|
|
286
|
-
writeFileSync(historyPath(base), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
287
|
-
} catch {
|
|
288
|
-
// Non-fatal — don't let history failures break auto-mode
|
|
289
|
-
}
|
|
285
|
+
saveJsonFile(historyPath(base), data);
|
|
290
286
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Tests the pure formatInspectOutput function with known data.
|
|
4
4
|
|
|
5
5
|
import { createTestContext } from './test-helpers.ts';
|
|
6
|
-
import { formatInspectOutput, type InspectData } from '../commands.ts';
|
|
6
|
+
import { formatInspectOutput, type InspectData } from '../commands-inspect.ts';
|
|
7
7
|
|
|
8
8
|
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
9
9
|
|