gsd-pi 2.29.0-dev.2ccf3fb → 2.29.0-dev.4c155ee
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/headless.js +4 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +31 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +32 -3
- package/dist/resources/extensions/gsd/auto-post-unit.ts +39 -10
- package/dist/resources/extensions/gsd/auto-prompts.ts +40 -17
- package/dist/resources/extensions/gsd/auto-recovery.ts +2 -1
- package/dist/resources/extensions/gsd/auto-start.ts +18 -32
- package/dist/resources/extensions/gsd/auto-worktree.ts +21 -182
- package/dist/resources/extensions/gsd/auto.ts +2 -9
- package/dist/resources/extensions/gsd/captures.ts +4 -10
- package/dist/resources/extensions/gsd/commands-handlers.ts +2 -1
- package/dist/resources/extensions/gsd/commands.ts +2 -1
- package/dist/resources/extensions/gsd/detection.ts +2 -1
- package/dist/resources/extensions/gsd/doctor-checks.ts +49 -1
- package/dist/resources/extensions/gsd/doctor-types.ts +3 -1
- package/dist/resources/extensions/gsd/forensics.ts +2 -2
- package/dist/resources/extensions/gsd/git-service.ts +3 -2
- package/dist/resources/extensions/gsd/gitignore.ts +9 -63
- package/dist/resources/extensions/gsd/gsd-db.ts +1 -165
- package/dist/resources/extensions/gsd/guided-flow.ts +8 -5
- package/dist/resources/extensions/gsd/index.ts +3 -3
- package/dist/resources/extensions/gsd/md-importer.ts +3 -2
- package/dist/resources/extensions/gsd/mechanical-completion.ts +430 -0
- package/dist/resources/extensions/gsd/migrate/command.ts +3 -2
- package/dist/resources/extensions/gsd/migrate/writer.ts +2 -1
- package/dist/resources/extensions/gsd/migrate-external.ts +123 -0
- package/dist/resources/extensions/gsd/paths.ts +24 -2
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +6 -5
- package/dist/resources/extensions/gsd/preferences-models.ts +7 -1
- package/dist/resources/extensions/gsd/preferences-validation.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +10 -5
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +4 -2
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +26 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +15 -1
- package/dist/resources/extensions/gsd/repo-identity.ts +148 -0
- package/dist/resources/extensions/gsd/resource-version.ts +99 -0
- package/dist/resources/extensions/gsd/session-forensics.ts +4 -3
- package/dist/resources/extensions/gsd/tests/activity-log.test.ts +2 -2
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +3 -3
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +0 -58
- package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +3 -4
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +5 -18
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +10 -37
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +4 -4
- package/dist/resources/extensions/gsd/tests/mechanical-completion.test.ts +356 -0
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/token-profile.test.ts +14 -16
- package/dist/resources/extensions/gsd/triage-resolution.ts +2 -1
- package/dist/resources/extensions/gsd/types.ts +2 -0
- package/dist/resources/extensions/gsd/worktree-command.ts +1 -11
- package/dist/resources/extensions/gsd/worktree-manager.ts +3 -2
- package/dist/resources/extensions/gsd/worktree.ts +42 -5
- package/dist/resources/skills/react-best-practices/SKILL.md +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +3 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +3 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +31 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +32 -3
- package/src/resources/extensions/gsd/auto-post-unit.ts +39 -10
- package/src/resources/extensions/gsd/auto-prompts.ts +40 -17
- package/src/resources/extensions/gsd/auto-recovery.ts +2 -1
- package/src/resources/extensions/gsd/auto-start.ts +18 -32
- package/src/resources/extensions/gsd/auto-worktree.ts +21 -182
- package/src/resources/extensions/gsd/auto.ts +2 -9
- package/src/resources/extensions/gsd/captures.ts +4 -10
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -1
- package/src/resources/extensions/gsd/commands.ts +2 -1
- package/src/resources/extensions/gsd/detection.ts +2 -1
- package/src/resources/extensions/gsd/doctor-checks.ts +49 -1
- package/src/resources/extensions/gsd/doctor-types.ts +3 -1
- package/src/resources/extensions/gsd/forensics.ts +2 -2
- package/src/resources/extensions/gsd/git-service.ts +3 -2
- package/src/resources/extensions/gsd/gitignore.ts +9 -63
- package/src/resources/extensions/gsd/gsd-db.ts +1 -165
- package/src/resources/extensions/gsd/guided-flow.ts +8 -5
- package/src/resources/extensions/gsd/index.ts +3 -3
- package/src/resources/extensions/gsd/md-importer.ts +3 -2
- package/src/resources/extensions/gsd/mechanical-completion.ts +430 -0
- package/src/resources/extensions/gsd/migrate/command.ts +3 -2
- package/src/resources/extensions/gsd/migrate/writer.ts +2 -1
- package/src/resources/extensions/gsd/migrate-external.ts +123 -0
- package/src/resources/extensions/gsd/paths.ts +24 -2
- package/src/resources/extensions/gsd/post-unit-hooks.ts +6 -5
- package/src/resources/extensions/gsd/preferences-models.ts +7 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +10 -5
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +4 -2
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +26 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +15 -1
- package/src/resources/extensions/gsd/repo-identity.ts +148 -0
- package/src/resources/extensions/gsd/resource-version.ts +99 -0
- package/src/resources/extensions/gsd/session-forensics.ts +4 -3
- package/src/resources/extensions/gsd/tests/activity-log.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +0 -58
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +3 -4
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +5 -18
- package/src/resources/extensions/gsd/tests/git-service.test.ts +10 -37
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +356 -0
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +14 -16
- package/src/resources/extensions/gsd/triage-resolution.ts +2 -1
- package/src/resources/extensions/gsd/types.ts +2 -0
- package/src/resources/extensions/gsd/worktree-command.ts +1 -11
- package/src/resources/extensions/gsd/worktree-manager.ts +3 -2
- package/src/resources/extensions/gsd/worktree.ts +42 -5
- package/src/resources/skills/react-best-practices/SKILL.md +1 -1
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +0 -199
- package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +0 -205
- package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +0 -442
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +0 -199
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +0 -205
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +0 -442
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mechanical Completion — deterministic post-verification artifact generation.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that aggregate task-level outputs into slice/milestone summaries,
|
|
5
|
+
* UAT stubs, roadmap checkbox updates, and validation reports. Zero orchestration
|
|
6
|
+
* dependencies — operates on filesystem paths and parsed structures only.
|
|
7
|
+
*
|
|
8
|
+
* ADR-003: replaces LLM-driven complete-slice and validate-milestone units with
|
|
9
|
+
* mechanical aggregation when the data is sufficient.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
15
|
+
import { loadFile, parseSummary } from "./files.js";
|
|
16
|
+
import { extractMarkdownSection } from "./auto-prompts.js";
|
|
17
|
+
import {
|
|
18
|
+
resolveTaskFiles,
|
|
19
|
+
resolveTaskJsonFiles,
|
|
20
|
+
resolveTasksDir,
|
|
21
|
+
resolveSliceFile,
|
|
22
|
+
resolveSlicePath,
|
|
23
|
+
resolveMilestoneFile,
|
|
24
|
+
resolveMilestonePath,
|
|
25
|
+
resolveGsdRootFile,
|
|
26
|
+
} from "./paths.js";
|
|
27
|
+
import type { Summary, SummaryFrontmatter } from "./types.js";
|
|
28
|
+
import type { EvidenceJSON } from "./verification-evidence.js";
|
|
29
|
+
|
|
30
|
+
// ─── Slice Completion ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Mechanically complete a slice by aggregating task summaries into:
|
|
34
|
+
* - S##-SUMMARY.md (aggregated frontmatter + task one-liners)
|
|
35
|
+
* - S##-UAT.md (extracted from plan Verification section)
|
|
36
|
+
* - Roadmap checkbox [x] update
|
|
37
|
+
*
|
|
38
|
+
* Returns true if completion succeeded, false if data is insufficient
|
|
39
|
+
* (serves as quality gate — caller falls back to LLM completion).
|
|
40
|
+
*/
|
|
41
|
+
export async function mechanicalSliceCompletion(
|
|
42
|
+
base: string, mid: string, sid: string,
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
45
|
+
if (!tDir) return false;
|
|
46
|
+
|
|
47
|
+
// Read all task summaries
|
|
48
|
+
const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
|
|
49
|
+
if (summaryFiles.length === 0) return false;
|
|
50
|
+
|
|
51
|
+
const taskSummaries: Array<{ taskId: string; summary: Summary }> = [];
|
|
52
|
+
for (const file of summaryFiles) {
|
|
53
|
+
const content = readFileSync(join(tDir, file), "utf-8");
|
|
54
|
+
if (!content.trim()) continue;
|
|
55
|
+
const summary = parseSummary(content);
|
|
56
|
+
const taskId = file.match(/^(T\d+)/)?.[1] ?? file;
|
|
57
|
+
taskSummaries.push({ taskId, summary });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (taskSummaries.length === 0) return false;
|
|
61
|
+
|
|
62
|
+
// Quality gate: multi-task slices need substantive summaries
|
|
63
|
+
if (taskSummaries.length > 1) {
|
|
64
|
+
const totalContent = taskSummaries
|
|
65
|
+
.map(ts => ts.summary.whatHappened || ts.summary.oneLiner || "")
|
|
66
|
+
.join("");
|
|
67
|
+
if (totalContent.length < 200) return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Aggregate frontmatter
|
|
71
|
+
const aggregated = aggregateFrontmatter(taskSummaries.map(ts => ts.summary.frontmatter));
|
|
72
|
+
|
|
73
|
+
// Build SUMMARY.md
|
|
74
|
+
const summaryLines: string[] = [
|
|
75
|
+
"---",
|
|
76
|
+
`id: ${sid}`,
|
|
77
|
+
`parent: ${mid}`,
|
|
78
|
+
`milestone: ${mid}`,
|
|
79
|
+
];
|
|
80
|
+
if (aggregated.provides.length > 0)
|
|
81
|
+
summaryLines.push(`provides:\n${aggregated.provides.map(p => ` - ${p}`).join("\n")}`);
|
|
82
|
+
if (aggregated.key_files.length > 0)
|
|
83
|
+
summaryLines.push(`key_files:\n${aggregated.key_files.map(f => ` - ${f}`).join("\n")}`);
|
|
84
|
+
if (aggregated.key_decisions.length > 0)
|
|
85
|
+
summaryLines.push(`key_decisions:\n${aggregated.key_decisions.map(d => ` - ${d}`).join("\n")}`);
|
|
86
|
+
if (aggregated.patterns_established.length > 0)
|
|
87
|
+
summaryLines.push(`patterns_established:\n${aggregated.patterns_established.map(p => ` - ${p}`).join("\n")}`);
|
|
88
|
+
if (aggregated.affects.length > 0)
|
|
89
|
+
summaryLines.push(`affects:\n${aggregated.affects.map(a => ` - ${a}`).join("\n")}`);
|
|
90
|
+
if (aggregated.observability_surfaces.length > 0)
|
|
91
|
+
summaryLines.push(`observability_surfaces:\n${aggregated.observability_surfaces.map(o => ` - ${o}`).join("\n")}`);
|
|
92
|
+
const allPassed = taskSummaries.every(ts => ts.summary.frontmatter.verification_result === "passed");
|
|
93
|
+
summaryLines.push(`verification_result: ${allPassed ? "passed" : "mixed"}`);
|
|
94
|
+
summaryLines.push(`completed_at: ${new Date().toISOString()}`);
|
|
95
|
+
summaryLines.push("---");
|
|
96
|
+
summaryLines.push("");
|
|
97
|
+
summaryLines.push(`# ${sid}: Slice Summary`);
|
|
98
|
+
summaryLines.push("");
|
|
99
|
+
|
|
100
|
+
// Task one-liners
|
|
101
|
+
for (const { taskId, summary } of taskSummaries) {
|
|
102
|
+
const line = summary.oneLiner || summary.title || taskId;
|
|
103
|
+
summaryLines.push(`- **${taskId}**: ${line}`);
|
|
104
|
+
}
|
|
105
|
+
summaryLines.push("");
|
|
106
|
+
|
|
107
|
+
const sDir = resolveSlicePath(base, mid, sid);
|
|
108
|
+
if (!sDir) return false;
|
|
109
|
+
|
|
110
|
+
const summaryPath = join(sDir, `${sid}-SUMMARY.md`);
|
|
111
|
+
atomicWriteSync(summaryPath, summaryLines.join("\n"));
|
|
112
|
+
process.stderr.write(`gsd-mechanical: wrote ${summaryPath}\n`);
|
|
113
|
+
|
|
114
|
+
// Build UAT.md from plan's Verification section
|
|
115
|
+
const planPath = resolveSliceFile(base, mid, sid, "PLAN");
|
|
116
|
+
if (planPath) {
|
|
117
|
+
const planContent = readFileSync(planPath, "utf-8");
|
|
118
|
+
const verification = extractMarkdownSection(planContent, "Verification");
|
|
119
|
+
if (verification) {
|
|
120
|
+
const uatContent = [
|
|
121
|
+
"---",
|
|
122
|
+
`id: ${sid}`,
|
|
123
|
+
`parent: ${mid}`,
|
|
124
|
+
"type: artifact-driven",
|
|
125
|
+
"---",
|
|
126
|
+
"",
|
|
127
|
+
`# ${sid}: UAT`,
|
|
128
|
+
"",
|
|
129
|
+
verification,
|
|
130
|
+
"",
|
|
131
|
+
].join("\n");
|
|
132
|
+
const uatPath = join(sDir, `${sid}-UAT.md`);
|
|
133
|
+
atomicWriteSync(uatPath, uatContent);
|
|
134
|
+
process.stderr.write(`gsd-mechanical: wrote ${uatPath}\n`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Mark slice [x] in ROADMAP
|
|
139
|
+
await markSliceInRoadmap(base, mid, sid);
|
|
140
|
+
|
|
141
|
+
// Append new decisions if any
|
|
142
|
+
await appendNewDecisions(base, taskSummaries.map(ts => ts.summary));
|
|
143
|
+
|
|
144
|
+
// Update requirements if all passed
|
|
145
|
+
if (allPassed) {
|
|
146
|
+
await mechanicalRequirementsUpdate(base, mid, sid, taskSummaries.map(ts => ts.summary));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── Requirements Update ─────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Conservative requirements update: mark requirements Validated only if
|
|
156
|
+
* all tasks' verification passed.
|
|
157
|
+
*/
|
|
158
|
+
export async function mechanicalRequirementsUpdate(
|
|
159
|
+
_base: string, _mid: string, _sid: string, _taskSummaries: Summary[],
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
// Conservative: requirements validation requires human or LLM judgment
|
|
162
|
+
// about whether the requirement is truly met. Mechanical completion only
|
|
163
|
+
// marks the slice done — requirement status updates are left to the
|
|
164
|
+
// existing validation pipeline.
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Decision Aggregation ────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Collect key_decisions from task summaries, deduplicate against existing
|
|
171
|
+
* DECISIONS.md, and append new ones.
|
|
172
|
+
*/
|
|
173
|
+
export async function appendNewDecisions(
|
|
174
|
+
base: string, taskSummaries: Summary[],
|
|
175
|
+
): Promise<void> {
|
|
176
|
+
const allDecisions = taskSummaries.flatMap(s => s.frontmatter.key_decisions);
|
|
177
|
+
if (allDecisions.length === 0) return;
|
|
178
|
+
|
|
179
|
+
const decisionsPath = resolveGsdRootFile(base, "DECISIONS");
|
|
180
|
+
const existing = existsSync(decisionsPath)
|
|
181
|
+
? readFileSync(decisionsPath, "utf-8")
|
|
182
|
+
: "";
|
|
183
|
+
|
|
184
|
+
// Deduplicate — skip decisions whose text already appears in the file
|
|
185
|
+
const newDecisions = allDecisions.filter(d =>
|
|
186
|
+
d.trim() && !existing.includes(d.trim()),
|
|
187
|
+
);
|
|
188
|
+
if (newDecisions.length === 0) return;
|
|
189
|
+
|
|
190
|
+
const entries = newDecisions
|
|
191
|
+
.map(d => `- ${d} _(auto-aggregated from task summaries)_`)
|
|
192
|
+
.join("\n");
|
|
193
|
+
|
|
194
|
+
const updated = existing.trimEnd() + "\n\n### Auto-aggregated Decisions\n\n" + entries + "\n";
|
|
195
|
+
atomicWriteSync(decisionsPath, updated);
|
|
196
|
+
process.stderr.write(`gsd-mechanical: appended ${newDecisions.length} decision(s) to DECISIONS.md\n`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Milestone Verification ──────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
export interface MilestoneVerificationResult {
|
|
202
|
+
verdict: "passed" | "failed" | "mixed";
|
|
203
|
+
checks: EvidenceJSON[];
|
|
204
|
+
uatResults: string[];
|
|
205
|
+
markdown: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Aggregate T##-VERIFY.json files and S##-UAT-RESULT.md files across all
|
|
210
|
+
* slices in a milestone to produce VALIDATION.md.
|
|
211
|
+
*/
|
|
212
|
+
export async function aggregateMilestoneVerification(
|
|
213
|
+
base: string, mid: string,
|
|
214
|
+
): Promise<MilestoneVerificationResult> {
|
|
215
|
+
const mDir = resolveMilestonePath(base, mid);
|
|
216
|
+
if (!mDir) return { verdict: "failed", checks: [], uatResults: [], markdown: "" };
|
|
217
|
+
|
|
218
|
+
const allChecks: EvidenceJSON[] = [];
|
|
219
|
+
const allUatResults: string[] = [];
|
|
220
|
+
|
|
221
|
+
// Scan all slices
|
|
222
|
+
const slicesDir = join(mDir, "slices");
|
|
223
|
+
if (!existsSync(slicesDir)) return { verdict: "failed", checks: [], uatResults: [], markdown: "" };
|
|
224
|
+
|
|
225
|
+
const sliceDirs = readdirSyncSafe(slicesDir).filter(name => /^S\d+/i.test(name)).sort();
|
|
226
|
+
|
|
227
|
+
for (const sliceName of sliceDirs) {
|
|
228
|
+
const sid = sliceName.match(/^(S\d+)/i)?.[1] ?? sliceName;
|
|
229
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
230
|
+
if (tDir) {
|
|
231
|
+
const verifyFiles = resolveTaskJsonFiles(tDir, "VERIFY");
|
|
232
|
+
for (const vf of verifyFiles) {
|
|
233
|
+
try {
|
|
234
|
+
const content = readFileSync(join(tDir, vf), "utf-8");
|
|
235
|
+
const evidence = JSON.parse(content) as EvidenceJSON;
|
|
236
|
+
allChecks.push(evidence);
|
|
237
|
+
} catch {
|
|
238
|
+
// Skip malformed JSON
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check for UAT result
|
|
244
|
+
const uatResultPath = resolveSliceFile(base, mid, sid, "UAT-RESULT");
|
|
245
|
+
if (uatResultPath) {
|
|
246
|
+
try {
|
|
247
|
+
const uatContent = readFileSync(uatResultPath, "utf-8");
|
|
248
|
+
allUatResults.push(`### ${sid}\n\n${uatContent}`);
|
|
249
|
+
} catch {
|
|
250
|
+
// Non-fatal
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Determine verdict
|
|
256
|
+
const allPassed = allChecks.length > 0 && allChecks.every(c => c.passed);
|
|
257
|
+
const anyFailed = allChecks.some(c => !c.passed);
|
|
258
|
+
const verdict: "passed" | "failed" | "mixed" = allPassed
|
|
259
|
+
? "passed"
|
|
260
|
+
: anyFailed
|
|
261
|
+
? (allChecks.some(c => c.passed) ? "mixed" : "failed")
|
|
262
|
+
: "passed"; // No checks = vacuously passed
|
|
263
|
+
|
|
264
|
+
// Build VALIDATION.md
|
|
265
|
+
const mdLines: string[] = [
|
|
266
|
+
"---",
|
|
267
|
+
`milestone: ${mid}`,
|
|
268
|
+
`verdict: ${verdict}`,
|
|
269
|
+
"remediation_round: 0",
|
|
270
|
+
`validated_at: ${new Date().toISOString()}`,
|
|
271
|
+
"---",
|
|
272
|
+
"",
|
|
273
|
+
`# ${mid}: Milestone Validation`,
|
|
274
|
+
"",
|
|
275
|
+
`**Verdict:** ${verdict}`,
|
|
276
|
+
"",
|
|
277
|
+
"## Verification Results",
|
|
278
|
+
"",
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
if (allChecks.length === 0) {
|
|
282
|
+
mdLines.push("_No verification evidence found._");
|
|
283
|
+
} else {
|
|
284
|
+
mdLines.push("| Task | Passed | Checks | Failed |");
|
|
285
|
+
mdLines.push("|------|--------|--------|--------|");
|
|
286
|
+
for (const check of allChecks) {
|
|
287
|
+
const failedCount = check.checks.filter(c => c.verdict === "fail").length;
|
|
288
|
+
mdLines.push(
|
|
289
|
+
`| ${check.taskId} | ${check.passed ? "yes" : "no"} | ${check.checks.length} | ${failedCount} |`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (allUatResults.length > 0) {
|
|
295
|
+
mdLines.push("");
|
|
296
|
+
mdLines.push("## UAT Results");
|
|
297
|
+
mdLines.push("");
|
|
298
|
+
mdLines.push(...allUatResults);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
mdLines.push("");
|
|
302
|
+
|
|
303
|
+
const markdown = mdLines.join("\n");
|
|
304
|
+
|
|
305
|
+
// Write VALIDATION.md
|
|
306
|
+
const validationPath = join(mDir, `${mid}-VALIDATION.md`);
|
|
307
|
+
atomicWriteSync(validationPath, markdown);
|
|
308
|
+
process.stderr.write(`gsd-mechanical: wrote ${validationPath}\n`);
|
|
309
|
+
|
|
310
|
+
return { verdict, checks: allChecks, uatResults: allUatResults, markdown };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── Milestone Summary ──────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Read all S##-SUMMARY.md files and produce M##-SUMMARY.md.
|
|
317
|
+
*/
|
|
318
|
+
export async function generateMilestoneSummary(
|
|
319
|
+
base: string, mid: string,
|
|
320
|
+
): Promise<string> {
|
|
321
|
+
const mDir = resolveMilestonePath(base, mid);
|
|
322
|
+
if (!mDir) return "";
|
|
323
|
+
|
|
324
|
+
const slicesDir = join(mDir, "slices");
|
|
325
|
+
if (!existsSync(slicesDir)) return "";
|
|
326
|
+
|
|
327
|
+
const sliceDirs = readdirSyncSafe(slicesDir).filter(name => /^S\d+/i.test(name)).sort();
|
|
328
|
+
|
|
329
|
+
const aggregatedProvides: string[] = [];
|
|
330
|
+
const aggregatedKeyFiles: string[] = [];
|
|
331
|
+
const aggregatedKeyDecisions: string[] = [];
|
|
332
|
+
const aggregatedPatterns: string[] = [];
|
|
333
|
+
const sliceOneLinerList: string[] = [];
|
|
334
|
+
|
|
335
|
+
for (const sliceName of sliceDirs) {
|
|
336
|
+
const sid = sliceName.match(/^(S\d+)/i)?.[1] ?? sliceName;
|
|
337
|
+
const summaryPath = resolveSliceFile(base, mid, sid, "SUMMARY");
|
|
338
|
+
if (!summaryPath) continue;
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const content = readFileSync(summaryPath, "utf-8");
|
|
342
|
+
const summary = parseSummary(content);
|
|
343
|
+
aggregatedProvides.push(...summary.frontmatter.provides);
|
|
344
|
+
aggregatedKeyFiles.push(...summary.frontmatter.key_files);
|
|
345
|
+
aggregatedKeyDecisions.push(...summary.frontmatter.key_decisions);
|
|
346
|
+
aggregatedPatterns.push(...summary.frontmatter.patterns_established);
|
|
347
|
+
sliceOneLinerList.push(`- **${sid}**: ${summary.oneLiner || summary.title || sid}`);
|
|
348
|
+
} catch {
|
|
349
|
+
sliceOneLinerList.push(`- **${sid}**: _(summary unavailable)_`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const mdLines: string[] = [
|
|
354
|
+
"---",
|
|
355
|
+
`id: ${mid}`,
|
|
356
|
+
];
|
|
357
|
+
if (dedup(aggregatedProvides).length > 0)
|
|
358
|
+
mdLines.push(`provides:\n${dedup(aggregatedProvides).map(p => ` - ${p}`).join("\n")}`);
|
|
359
|
+
if (dedup(aggregatedKeyFiles).length > 0)
|
|
360
|
+
mdLines.push(`key_files:\n${dedup(aggregatedKeyFiles).map(f => ` - ${f}`).join("\n")}`);
|
|
361
|
+
if (dedup(aggregatedKeyDecisions).length > 0)
|
|
362
|
+
mdLines.push(`key_decisions:\n${dedup(aggregatedKeyDecisions).map(d => ` - ${d}`).join("\n")}`);
|
|
363
|
+
if (dedup(aggregatedPatterns).length > 0)
|
|
364
|
+
mdLines.push(`patterns_established:\n${dedup(aggregatedPatterns).map(p => ` - ${p}`).join("\n")}`);
|
|
365
|
+
mdLines.push(`completed_at: ${new Date().toISOString()}`);
|
|
366
|
+
mdLines.push("---");
|
|
367
|
+
mdLines.push("");
|
|
368
|
+
mdLines.push(`# ${mid}: Milestone Summary`);
|
|
369
|
+
mdLines.push("");
|
|
370
|
+
mdLines.push("## Slices");
|
|
371
|
+
mdLines.push("");
|
|
372
|
+
mdLines.push(...sliceOneLinerList);
|
|
373
|
+
mdLines.push("");
|
|
374
|
+
|
|
375
|
+
const content = mdLines.join("\n");
|
|
376
|
+
|
|
377
|
+
// Write M##-SUMMARY.md
|
|
378
|
+
const summaryPath = join(mDir, `${mid}-SUMMARY.md`);
|
|
379
|
+
atomicWriteSync(summaryPath, content);
|
|
380
|
+
process.stderr.write(`gsd-mechanical: wrote ${summaryPath}\n`);
|
|
381
|
+
|
|
382
|
+
return content;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
function aggregateFrontmatter(fms: SummaryFrontmatter[]): {
|
|
388
|
+
provides: string[];
|
|
389
|
+
key_files: string[];
|
|
390
|
+
key_decisions: string[];
|
|
391
|
+
patterns_established: string[];
|
|
392
|
+
affects: string[];
|
|
393
|
+
observability_surfaces: string[];
|
|
394
|
+
} {
|
|
395
|
+
return {
|
|
396
|
+
provides: dedup(fms.flatMap(f => f.provides)),
|
|
397
|
+
key_files: dedup(fms.flatMap(f => f.key_files)),
|
|
398
|
+
key_decisions: dedup(fms.flatMap(f => f.key_decisions)),
|
|
399
|
+
patterns_established: dedup(fms.flatMap(f => f.patterns_established)),
|
|
400
|
+
affects: dedup(fms.flatMap(f => f.affects)),
|
|
401
|
+
observability_surfaces: dedup(fms.flatMap(f => f.observability_surfaces)),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function dedup(arr: string[]): string[] {
|
|
406
|
+
return [...new Set(arr.filter(s => s.trim()))];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function markSliceInRoadmap(base: string, mid: string, sid: string): Promise<void> {
|
|
410
|
+
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
411
|
+
if (!roadmapPath) return;
|
|
412
|
+
const content = await loadFile(roadmapPath);
|
|
413
|
+
if (!content) return;
|
|
414
|
+
const updated = content.replace(
|
|
415
|
+
new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${sid}:`, "m"),
|
|
416
|
+
`$1[x] **${sid}:`,
|
|
417
|
+
);
|
|
418
|
+
if (updated !== content) {
|
|
419
|
+
atomicWriteSync(roadmapPath, updated);
|
|
420
|
+
process.stderr.write(`gsd-mechanical: marked ${sid} done in ROADMAP\n`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function readdirSyncSafe(dir: string): string[] {
|
|
425
|
+
try {
|
|
426
|
+
return readdirSync(dir);
|
|
427
|
+
} catch {
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
13
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
14
|
import { resolve, join, dirname } from "node:path";
|
|
15
|
+
import { gsdRoot } from "../paths.js";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
16
17
|
import { showNextAction } from "../../shared/mod.js";
|
|
17
18
|
import {
|
|
@@ -144,7 +145,7 @@ export async function handleMigrate(
|
|
|
144
145
|
);
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
const targetGsdExists = existsSync(
|
|
148
|
+
const targetGsdExists = existsSync(gsdRoot(process.cwd()));
|
|
148
149
|
if (targetGsdExists) {
|
|
149
150
|
lines.push("");
|
|
150
151
|
lines.push("⚠ A .gsd directory already exists in the current working directory — it will be overwritten.");
|
|
@@ -179,7 +180,7 @@ export async function handleMigrate(
|
|
|
179
180
|
ctx.ui.notify("Writing .gsd directory…", "info");
|
|
180
181
|
|
|
181
182
|
const result = await writeGSDDirectory(project, process.cwd());
|
|
182
|
-
const gsdPath =
|
|
183
|
+
const gsdPath = gsdRoot(process.cwd());
|
|
183
184
|
|
|
184
185
|
ctx.ui.notify(
|
|
185
186
|
`✓ Migration complete — ${result.paths.length} file(s) written to .gsd/`,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { saveFile } from '../files.js';
|
|
8
|
+
import { gsdRoot } from '../paths.js';
|
|
8
9
|
|
|
9
10
|
import type {
|
|
10
11
|
GSDMilestone,
|
|
@@ -421,7 +422,7 @@ export async function writeGSDDirectory(
|
|
|
421
422
|
project: GSDProject,
|
|
422
423
|
targetPath: string,
|
|
423
424
|
): Promise<WrittenFiles> {
|
|
424
|
-
const gsdDir =
|
|
425
|
+
const gsdDir = gsdRoot(targetPath);
|
|
425
426
|
const milestonesBase = join(gsdDir, 'milestones');
|
|
426
427
|
const paths: string[] = [];
|
|
427
428
|
const counts: WrittenFiles['counts'] = {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD External State Migration
|
|
3
|
+
*
|
|
4
|
+
* Migrates legacy in-project `.gsd/` directories to the external
|
|
5
|
+
* `~/.gsd/projects/<hash>/` state directory. After migration, a
|
|
6
|
+
* symlink replaces the original directory so all paths remain valid.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { externalGsdRoot } from "./repo-identity.js";
|
|
12
|
+
|
|
13
|
+
export interface MigrationResult {
|
|
14
|
+
migrated: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Migrate a legacy in-project `.gsd/` directory to external storage.
|
|
20
|
+
*
|
|
21
|
+
* Algorithm:
|
|
22
|
+
* 1. If `<project>/.gsd` is a symlink or doesn't exist -> skip
|
|
23
|
+
* 2. If `<project>/.gsd` is a real directory:
|
|
24
|
+
* a. Compute external path from repoIdentity
|
|
25
|
+
* b. mkdir -p external dir
|
|
26
|
+
* c. Rename `.gsd` -> `.gsd.migrating` (atomic on same FS, acts as lock)
|
|
27
|
+
* d. Copy contents to external dir (skip `worktrees/` subdirectory)
|
|
28
|
+
* e. Create symlink `.gsd -> external path`
|
|
29
|
+
* f. Remove `.gsd.migrating`
|
|
30
|
+
* 3. On failure: rename `.gsd.migrating` back to `.gsd` (rollback)
|
|
31
|
+
*/
|
|
32
|
+
export function migrateToExternalState(basePath: string): MigrationResult {
|
|
33
|
+
const localGsd = join(basePath, ".gsd");
|
|
34
|
+
|
|
35
|
+
// Skip if doesn't exist
|
|
36
|
+
if (!existsSync(localGsd)) {
|
|
37
|
+
return { migrated: false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Skip if already a symlink
|
|
41
|
+
try {
|
|
42
|
+
const stat = lstatSync(localGsd);
|
|
43
|
+
if (stat.isSymbolicLink()) {
|
|
44
|
+
return { migrated: false };
|
|
45
|
+
}
|
|
46
|
+
if (!stat.isDirectory()) {
|
|
47
|
+
return { migrated: false, error: ".gsd exists but is not a directory or symlink" };
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return { migrated: false, error: `Cannot stat .gsd: ${err instanceof Error ? err.message : String(err)}` };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const externalPath = externalGsdRoot(basePath);
|
|
54
|
+
const migratingPath = join(basePath, ".gsd.migrating");
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// mkdir -p the external dir
|
|
58
|
+
mkdirSync(externalPath, { recursive: true });
|
|
59
|
+
|
|
60
|
+
// Rename .gsd -> .gsd.migrating (atomic lock)
|
|
61
|
+
renameSync(localGsd, migratingPath);
|
|
62
|
+
|
|
63
|
+
// Copy contents to external dir, skipping worktrees/
|
|
64
|
+
const entries = readdirSync(migratingPath, { withFileTypes: true });
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.name === "worktrees") continue; // worktrees stay local
|
|
67
|
+
|
|
68
|
+
const src = join(migratingPath, entry.name);
|
|
69
|
+
const dst = join(externalPath, entry.name);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
cpSync(src, dst, { recursive: true, force: true });
|
|
74
|
+
} else {
|
|
75
|
+
cpSync(src, dst, { force: true });
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Non-fatal: continue with other files
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Create symlink .gsd -> external path
|
|
83
|
+
symlinkSync(externalPath, localGsd, "junction");
|
|
84
|
+
|
|
85
|
+
// Remove .gsd.migrating
|
|
86
|
+
rmSync(migratingPath, { recursive: true, force: true });
|
|
87
|
+
|
|
88
|
+
return { migrated: true };
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// Rollback: rename .gsd.migrating back to .gsd
|
|
91
|
+
try {
|
|
92
|
+
if (existsSync(migratingPath) && !existsSync(localGsd)) {
|
|
93
|
+
renameSync(migratingPath, localGsd);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Rollback failed -- leave .gsd.migrating for doctor to detect
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
migrated: false,
|
|
101
|
+
error: `Migration failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Recover from a failed migration (`.gsd.migrating` exists).
|
|
108
|
+
* Moves `.gsd.migrating` back to `.gsd` if `.gsd` doesn't exist.
|
|
109
|
+
*/
|
|
110
|
+
export function recoverFailedMigration(basePath: string): boolean {
|
|
111
|
+
const localGsd = join(basePath, ".gsd");
|
|
112
|
+
const migratingPath = join(basePath, ".gsd.migrating");
|
|
113
|
+
|
|
114
|
+
if (!existsSync(migratingPath)) return false;
|
|
115
|
+
if (existsSync(localGsd)) return false; // both exist -- ambiguous, don't touch
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
renameSync(migratingPath, localGsd);
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* via prefix matching, so existing projects work without migration.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { readdirSync, existsSync, Dirent } from "node:fs";
|
|
12
|
+
import { readdirSync, existsSync, realpathSync, Dirent } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { nativeScanGsdTree, type GsdTreeEntry } from "./native-parser-bridge.js";
|
|
15
15
|
import { DIR_CACHE_MAX } from "./constants.js";
|
|
@@ -236,6 +236,23 @@ export function resolveTaskFiles(tasksDir: string, suffix: string): string[] {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Find all task JSON files matching a pattern in a tasks directory.
|
|
241
|
+
* Returns sorted file names matching T##-SUFFIX.json or legacy T##-*-SUFFIX.json
|
|
242
|
+
*/
|
|
243
|
+
export function resolveTaskJsonFiles(tasksDir: string, suffix: string): string[] {
|
|
244
|
+
if (!existsSync(tasksDir)) return [];
|
|
245
|
+
try {
|
|
246
|
+
const currentPattern = new RegExp(`^T\\d+-${suffix}\\.json$`, "i");
|
|
247
|
+
const legacyPattern = new RegExp(`^T\\d+-.*-${suffix}\\.json$`, "i");
|
|
248
|
+
return cachedReaddir(tasksDir)
|
|
249
|
+
.filter(f => currentPattern.test(f) || legacyPattern.test(f))
|
|
250
|
+
.sort();
|
|
251
|
+
} catch {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
239
256
|
// ─── Full Path Builders ────────────────────────────────────────────────────
|
|
240
257
|
|
|
241
258
|
export const GSD_ROOT_FILES = {
|
|
@@ -261,7 +278,12 @@ const LEGACY_GSD_ROOT_FILES: Record<GSDRootFileKey, string> = {
|
|
|
261
278
|
};
|
|
262
279
|
|
|
263
280
|
export function gsdRoot(basePath: string): string {
|
|
264
|
-
|
|
281
|
+
const local = join(basePath, ".gsd");
|
|
282
|
+
try {
|
|
283
|
+
const resolved = realpathSync(local);
|
|
284
|
+
if (resolved !== local) return resolved; // symlink resolved
|
|
285
|
+
} catch { /* doesn't exist yet — fall through */ }
|
|
286
|
+
return local; // backwards compat: unmigrated projects
|
|
265
287
|
}
|
|
266
288
|
|
|
267
289
|
export function milestonesDir(basePath: string): string {
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
import { resolvePostUnitHooks, resolvePreDispatchHooks } from "./preferences.js";
|
|
15
15
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
+
import { gsdRoot } from "./paths.js";
|
|
17
18
|
|
|
18
19
|
// ─── Hook Queue State ──────────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -210,13 +211,13 @@ export function resolveHookArtifactPath(basePath: string, unitId: string, artifa
|
|
|
210
211
|
const parts = unitId.split("/");
|
|
211
212
|
if (parts.length === 3) {
|
|
212
213
|
const [mid, sid, tid] = parts;
|
|
213
|
-
return join(basePath,
|
|
214
|
+
return join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-${artifactName}`);
|
|
214
215
|
}
|
|
215
216
|
if (parts.length === 2) {
|
|
216
217
|
const [mid, sid] = parts;
|
|
217
|
-
return join(basePath,
|
|
218
|
+
return join(gsdRoot(basePath), mid, "slices", sid, artifactName);
|
|
218
219
|
}
|
|
219
|
-
return join(basePath,
|
|
220
|
+
return join(gsdRoot(basePath), parts[0], artifactName);
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -310,7 +311,7 @@ export function runPreDispatchHooks(
|
|
|
310
311
|
const HOOK_STATE_FILE = "hook-state.json";
|
|
311
312
|
|
|
312
313
|
function hookStatePath(basePath: string): string {
|
|
313
|
-
return join(basePath,
|
|
314
|
+
return join(gsdRoot(basePath), HOOK_STATE_FILE);
|
|
314
315
|
}
|
|
315
316
|
|
|
316
317
|
/**
|
|
@@ -323,7 +324,7 @@ export function persistHookState(basePath: string): void {
|
|
|
323
324
|
savedAt: new Date().toISOString(),
|
|
324
325
|
};
|
|
325
326
|
try {
|
|
326
|
-
const dir =
|
|
327
|
+
const dir = gsdRoot(basePath);
|
|
327
328
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
328
329
|
writeFileSync(hookStatePath(basePath), JSON.stringify(state, null, 2), "utf-8");
|
|
329
330
|
} catch {
|