@oevery/rsp 3.1.0-beta.2 → 3.1.0-beta.4
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 +53 -35
- package/README.zh-CN.md +46 -33
- package/dist/{chunk-4K53353L.mjs → chunk-D7XDXBFO.mjs} +41 -5
- package/dist/{chunk-EALHA3ZS.mjs → chunk-FI3SYHVH.mjs} +552 -428
- package/dist/{chunk-OTJFL6XH.mjs → chunk-FJVH7ZYU.mjs} +344 -216
- package/dist/cli.mjs +524 -339
- package/dist/{entry-UWUX3HM7.mjs → entry-5OPKUZHF.mjs} +2 -2
- package/dist/{entry-N7WLWGTI.mjs → entry-FVUG6YW2.mjs} +2 -2
- package/package.json +1 -1
- package/rules/rsp-rules.md +11 -6
- package/skills/rsp/SKILL.md +12 -6
- package/skills/rsp/references/durable-review.md +1 -1
- package/skills/rsp/references/managed-routing.md +15 -5
- package/skills/rsp/references/setup-repair.md +2 -2
- package/skills/rsp-commit/SKILL.md +2 -2
- package/skills/rsp-design/SKILL.md +2 -2
- package/skills/rsp-manage/SKILL.md +31 -12
- package/skills/rsp-release-docs/references/convention-discovery.md +2 -2
- package/skills/{rsp-address-review → rsp-resolve-findings}/SKILL.md +4 -4
- package/skills/rsp-shape/SKILL.md +2 -2
- package/skills/{rsp-codebase-audit → rsp-structural-audit}/SKILL.md +5 -3
- package/skills/{rsp-codebase-audit → rsp-structural-audit}/references/structural-lenses.md +6 -0
package/dist/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
installPackagedSkills,
|
|
5
5
|
printPackagedSkillInventory,
|
|
6
6
|
printSkillInstallResult
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-D7XDXBFO.mjs";
|
|
8
8
|
import {
|
|
9
9
|
ArchiveHistoryError,
|
|
10
10
|
GROUP_BRIEF_FILENAME,
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
inspectFocusTree,
|
|
22
22
|
inspectProjectStatus,
|
|
23
23
|
inspectWorkTree,
|
|
24
|
+
normalizeExecutableWorkRef,
|
|
25
|
+
normalizeWorkRefSegment,
|
|
24
26
|
projectDependencyForest,
|
|
25
27
|
queryArchiveHistory,
|
|
26
28
|
readArchiveHistoryDetail,
|
|
@@ -29,12 +31,14 @@ import {
|
|
|
29
31
|
resolveFocusMarkerPath,
|
|
30
32
|
resolveWorkRef,
|
|
31
33
|
selectArchiveHistoryRecord
|
|
32
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-FI3SYHVH.mjs";
|
|
33
35
|
import {
|
|
34
36
|
ARCHIVES_DIR,
|
|
35
37
|
CHANGES_DIR,
|
|
38
|
+
CHANGE_DOCUMENT_SCHEMA,
|
|
36
39
|
CONFIG_PATH,
|
|
37
40
|
DEFAULT_DECISION_RECORDS_PATH,
|
|
41
|
+
DocumentSectionCardinalityError,
|
|
38
42
|
FOCUS_DIR,
|
|
39
43
|
LOCK_PATH,
|
|
40
44
|
OBSOLETE_RSP_RULES_PATH,
|
|
@@ -42,6 +46,7 @@ import {
|
|
|
42
46
|
RSP_DIR,
|
|
43
47
|
RSP_RULES_PATH,
|
|
44
48
|
VALID_KINDS,
|
|
49
|
+
appendDocumentSectionItem,
|
|
45
50
|
buildDurableReviewGuidance,
|
|
46
51
|
cleanupEmptyParentDirs,
|
|
47
52
|
clearConfigCache,
|
|
@@ -53,10 +58,11 @@ import {
|
|
|
53
58
|
emitJson,
|
|
54
59
|
ensureDecisionRecordsDirectory,
|
|
55
60
|
ensureManagedFile,
|
|
56
|
-
extractSection,
|
|
57
61
|
generateChangeContent,
|
|
58
62
|
generateDesignContent,
|
|
59
63
|
generateSpecContent,
|
|
64
|
+
getDocumentSectionDefinitionByHeading,
|
|
65
|
+
getDocumentSections,
|
|
60
66
|
getDurableReviewCandidateTargets,
|
|
61
67
|
getVersion,
|
|
62
68
|
guardRspInitialized,
|
|
@@ -71,6 +77,7 @@ import {
|
|
|
71
77
|
loadRspConfig,
|
|
72
78
|
normalizeLogicalPath,
|
|
73
79
|
parseFrontmatter,
|
|
80
|
+
parseRspDocument,
|
|
74
81
|
parseScenarios,
|
|
75
82
|
pc,
|
|
76
83
|
recordRuntimeDiagnostic,
|
|
@@ -84,7 +91,7 @@ import {
|
|
|
84
91
|
validateDecisionRecordsFilesystemPath,
|
|
85
92
|
walkMarkdownFiles,
|
|
86
93
|
writeManagedFile
|
|
87
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-FJVH7ZYU.mjs";
|
|
88
95
|
|
|
89
96
|
// src/cli.ts
|
|
90
97
|
import { resolve } from "path";
|
|
@@ -92,7 +99,7 @@ import { fileURLToPath } from "url";
|
|
|
92
99
|
import { defineCommand, runMain } from "citty";
|
|
93
100
|
|
|
94
101
|
// src/commands/add-spec.ts
|
|
95
|
-
import { mkdir as mkdir2, unlink as
|
|
102
|
+
import { mkdir as mkdir2, unlink as unlink3, writeFile as writeFile2 } from "fs/promises";
|
|
96
103
|
import { join as join2 } from "path";
|
|
97
104
|
|
|
98
105
|
// src/core/lock.ts
|
|
@@ -157,12 +164,51 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
// src/commands/specs-index.ts
|
|
160
|
-
import {
|
|
161
|
-
import {
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
import { readFile as readFile2, unlink as unlink2 } from "fs/promises";
|
|
168
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from "path";
|
|
169
|
+
var SPECS_INDEX_FILENAME = "00-index.md";
|
|
170
|
+
var LEGACY_SPECS_INDEX_FILENAME = "INDEX.md";
|
|
171
|
+
async function buildSpecsIndex(options = {}) {
|
|
172
|
+
const { acquireLock = true, quiet = false, affectedDirectory } = options;
|
|
164
173
|
if (acquireLock)
|
|
165
|
-
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet }));
|
|
174
|
+
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet, affectedDirectory }));
|
|
175
|
+
const plan = await planSpecsIndexes(affectedDirectory);
|
|
176
|
+
for (const write of plan.writes)
|
|
177
|
+
await writeManagedFile(write.path, write.content, "Specs index");
|
|
178
|
+
for (const path of plan.removals)
|
|
179
|
+
await unlink2(path);
|
|
180
|
+
const changed = plan.writes.length > 0 || plan.removals.length > 0;
|
|
181
|
+
if (!quiet) {
|
|
182
|
+
if (changed)
|
|
183
|
+
console.log(` ${pc.green("Specs indexes updated:")} ${plan.writes.length} written, ${plan.removals.length} removed.
|
|
184
|
+
`);
|
|
185
|
+
else
|
|
186
|
+
console.log(` ${pc.dim("Specs indexes already up to date.")}
|
|
187
|
+
`);
|
|
188
|
+
}
|
|
189
|
+
return changed;
|
|
190
|
+
}
|
|
191
|
+
async function inspectSpecsIndexHealth() {
|
|
192
|
+
try {
|
|
193
|
+
const plan = await planSpecsIndexes();
|
|
194
|
+
return {
|
|
195
|
+
ok: plan.writes.length === 0 && plan.removals.length === 0,
|
|
196
|
+
expectedPaths: plan.expectedPaths.map(toProjectPath),
|
|
197
|
+
stalePaths: plan.writes.map((write) => toProjectPath(write.path)),
|
|
198
|
+
obsoletePaths: plan.removals.map(toProjectPath),
|
|
199
|
+
issues: []
|
|
200
|
+
};
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
expectedPaths: [],
|
|
205
|
+
stalePaths: [],
|
|
206
|
+
obsoletePaths: [],
|
|
207
|
+
issues: [error instanceof Error ? error.message : String(error)]
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function planSpecsIndexes(affectedDirectory) {
|
|
166
212
|
const specsDir = join(RSP_DIR, "specs");
|
|
167
213
|
const configInspection = await inspectRspConfig();
|
|
168
214
|
if (configInspection.issues.length > 0)
|
|
@@ -171,91 +217,154 @@ async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
|
|
|
171
217
|
const inspection = await inspectManagedFileTree(specsDir, "Specs", { allowMissing: true });
|
|
172
218
|
if (inspection.issues.length > 0)
|
|
173
219
|
throw inspection.issues[0];
|
|
174
|
-
if (!inspection.rootExists)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
220
|
+
if (!inspection.rootExists)
|
|
221
|
+
return { writes: [], removals: [], expectedPaths: [] };
|
|
222
|
+
const excludedDirectories = [DEFAULT_DECISION_RECORDS_PATH, decisionRecordsPath].filter((path) => isInside(specsDir, path));
|
|
223
|
+
const isExcluded = (path) => excludedDirectories.some((directory) => isInside(directory, path));
|
|
224
|
+
const managedIndexFiles = inspection.files.filter((path) => !isExcluded(path)).filter((path) => basename(path) === SPECS_INDEX_FILENAME || path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME));
|
|
225
|
+
const recognizedIndexes = /* @__PURE__ */ new Set();
|
|
226
|
+
for (const path of managedIndexFiles) {
|
|
227
|
+
let metadata;
|
|
228
|
+
try {
|
|
229
|
+
metadata = parseFrontmatter(await readFile2(path, "utf-8"));
|
|
230
|
+
} catch (error) {
|
|
231
|
+
throw new Error(`unable to inspect reserved Specs index ${toProjectPath(path)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
232
|
+
}
|
|
233
|
+
const expectedSourceDirectory = path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME) ? toProjectPath(specsDir) : toProjectPath(dirname(path));
|
|
234
|
+
if (metadata?.kind !== "generated-index" || metadata.index_type !== "specs" || metadata.source_dir !== expectedSourceDirectory) {
|
|
235
|
+
throw new Error(`reserved Specs index is not recognized generated content: ${toProjectPath(path)}`);
|
|
236
|
+
}
|
|
237
|
+
recognizedIndexes.add(path);
|
|
238
|
+
}
|
|
239
|
+
const specFiles = inspection.files.filter((path) => path.endsWith(".md")).filter((path) => !isExcluded(path)).filter((path) => basename(path) !== SPECS_INDEX_FILENAME).filter((path) => path !== join(specsDir, LEGACY_SPECS_INDEX_FILENAME)).sort();
|
|
240
|
+
const directories = /* @__PURE__ */ new Set([specsDir]);
|
|
241
|
+
for (const file of specFiles) {
|
|
242
|
+
let current = dirname(file);
|
|
243
|
+
while (isInside(specsDir, current)) {
|
|
244
|
+
directories.add(current);
|
|
245
|
+
if (current === specsDir)
|
|
246
|
+
break;
|
|
247
|
+
current = dirname(current);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const orderedDirectories = [...directories].sort();
|
|
251
|
+
const expectedPaths = orderedDirectories.map((directory) => join(directory, SPECS_INDEX_FILENAME));
|
|
252
|
+
const expectedSet = new Set(expectedPaths);
|
|
253
|
+
const writableDirectories = affectedDirectory ? new Set(ancestorDirectories(specsDir, affectedDirectory)) : directories;
|
|
254
|
+
const writes = [];
|
|
255
|
+
for (const directory of orderedDirectories) {
|
|
256
|
+
if (!writableDirectories.has(directory))
|
|
257
|
+
continue;
|
|
258
|
+
const directFiles = specFiles.filter((path2) => dirname(path2) === directory);
|
|
259
|
+
const directDirectories = orderedDirectories.filter((path2) => path2 !== directory && dirname(path2) === directory);
|
|
260
|
+
const content = await renderSpecsIndex(specsDir, directory, directFiles, directDirectories);
|
|
261
|
+
const path = join(directory, SPECS_INDEX_FILENAME);
|
|
262
|
+
const existingContent = recognizedIndexes.has(path) ? await readFile2(path, "utf-8") : null;
|
|
263
|
+
if (existingContent !== content)
|
|
264
|
+
writes.push({ path, content });
|
|
265
|
+
}
|
|
266
|
+
const removals = [...recognizedIndexes].filter((path) => path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME) || !expectedSet.has(path) && !affectedDirectory).sort();
|
|
267
|
+
return { writes, removals, expectedPaths };
|
|
268
|
+
}
|
|
269
|
+
async function renderSpecsIndex(specsDir, directory, files, directories) {
|
|
270
|
+
const sourceDir = toProjectPath(directory);
|
|
271
|
+
const scope = normalizeLogicalPath(relative(specsDir, directory));
|
|
272
|
+
const title = scope === "" ? "Specs Index" : `Specs: ${scope}`;
|
|
273
|
+
const entries = [];
|
|
274
|
+
for (const childDirectory of directories) {
|
|
275
|
+
const name = basename(childDirectory);
|
|
276
|
+
entries.push({
|
|
277
|
+
sortKey: `${name}/`,
|
|
278
|
+
line: `| [${escapeLabel(`${name}/`)}](${encodeLink(`${name}/${SPECS_INDEX_FILENAME}`)}) | directory | ${escapeCell(name)} | \u2014 |`
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
for (const path of files) {
|
|
282
|
+
const name = basename(path);
|
|
283
|
+
const content = await readFile2(path, "utf-8");
|
|
284
|
+
const metadata = parseFrontmatter(content);
|
|
285
|
+
const metadataTitle = metadata && typeof metadata.title === "string" ? metadata.title : "";
|
|
286
|
+
const entryTitle = metadataTitle || extractTitle(content) || name.replace(/\.md$/, "");
|
|
287
|
+
const summary = extractSummary(content, metadata?.summary);
|
|
288
|
+
entries.push({
|
|
289
|
+
sortKey: name,
|
|
290
|
+
line: `| [${escapeLabel(name)}](${encodeLink(name)}) | spec | ${escapeCell(entryTitle)} | ${escapeCell(summary || "\u2014")} |`
|
|
291
|
+
});
|
|
179
292
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (specFiles.length === 0) {
|
|
199
|
-
lines.push("_No additional project-level specs yet._");
|
|
293
|
+
entries.sort((left, right) => left.sortKey < right.sortKey ? -1 : left.sortKey > right.sortKey ? 1 : 0);
|
|
294
|
+
const lines = [
|
|
295
|
+
"---",
|
|
296
|
+
`title: ${JSON.stringify(title)}`,
|
|
297
|
+
`summary: ${JSON.stringify(`Direct Specs and child Spec directories under ${sourceDir}.`)}`,
|
|
298
|
+
"kind: generated-index",
|
|
299
|
+
"index_type: specs",
|
|
300
|
+
`source_dir: ${JSON.stringify(sourceDir)}`,
|
|
301
|
+
`entry_count: ${entries.length}`,
|
|
302
|
+
"---",
|
|
303
|
+
"",
|
|
304
|
+
`# ${title}`,
|
|
305
|
+
"",
|
|
306
|
+
"_Generated direct-child navigation. Edit the linked Specs, then run `rsp update`._",
|
|
307
|
+
""
|
|
308
|
+
];
|
|
309
|
+
if (entries.length === 0) {
|
|
310
|
+
lines.push("_No Specs or child Spec directories yet._");
|
|
200
311
|
} else {
|
|
201
|
-
lines.push("|
|
|
202
|
-
lines.push("
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
`);
|
|
222
|
-
return changed;
|
|
223
|
-
}
|
|
224
|
-
function isInside(directory, filePath) {
|
|
225
|
-
const rel = relative(directory, filePath);
|
|
312
|
+
lines.push("| Entry | Kind | Title | Summary |");
|
|
313
|
+
lines.push("|-------|------|-------|---------|");
|
|
314
|
+
lines.push(...entries.map((entry) => entry.line));
|
|
315
|
+
}
|
|
316
|
+
return lines.join("\n");
|
|
317
|
+
}
|
|
318
|
+
function ancestorDirectories(root, directory) {
|
|
319
|
+
if (!isInside(root, directory))
|
|
320
|
+
throw new Error(`affected Spec directory must stay inside ${toProjectPath(root)}: ${directory}`);
|
|
321
|
+
const result = [];
|
|
322
|
+
let current = directory;
|
|
323
|
+
while (true) {
|
|
324
|
+
result.push(current);
|
|
325
|
+
if (current === root)
|
|
326
|
+
return result;
|
|
327
|
+
current = dirname(current);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function isInside(directory, path) {
|
|
331
|
+
const rel = relative(directory, path);
|
|
226
332
|
return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
227
333
|
}
|
|
334
|
+
function toProjectPath(path) {
|
|
335
|
+
return normalizeLogicalPath(relative(".", path));
|
|
336
|
+
}
|
|
228
337
|
function extractTitle(content) {
|
|
229
338
|
for (const rawLine of content.split("\n")) {
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
return rawLine.slice(2).trim();
|
|
339
|
+
if (rawLine.startsWith("# "))
|
|
340
|
+
return rawLine.slice(2).trim();
|
|
233
341
|
}
|
|
234
342
|
return "";
|
|
235
343
|
}
|
|
236
344
|
function extractSummary(content, frontmatterSummary) {
|
|
237
345
|
if (typeof frontmatterSummary === "string" && frontmatterSummary.trim() !== "")
|
|
238
346
|
return frontmatterSummary.trim();
|
|
239
|
-
const lines = content.split("\n");
|
|
240
347
|
let inFrontmatter = false;
|
|
241
|
-
for (const rawLine of
|
|
348
|
+
for (const rawLine of content.split("\n")) {
|
|
242
349
|
const line = rawLine.trim();
|
|
243
350
|
if (line === "---" || line === "...") {
|
|
244
351
|
inFrontmatter = !inFrontmatter;
|
|
245
352
|
continue;
|
|
246
353
|
}
|
|
247
|
-
if (inFrontmatter || line === "")
|
|
248
|
-
continue;
|
|
249
|
-
if (line.startsWith("#"))
|
|
250
|
-
continue;
|
|
251
|
-
if (line.startsWith("##"))
|
|
354
|
+
if (inFrontmatter || line === "" || line.startsWith("#"))
|
|
252
355
|
continue;
|
|
253
356
|
return line.replace(/^[-*]\s*/, "").trim();
|
|
254
357
|
}
|
|
255
358
|
return "";
|
|
256
359
|
}
|
|
257
360
|
function escapeCell(value) {
|
|
258
|
-
return value.replace(/\|/g, "\\|");
|
|
361
|
+
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
362
|
+
}
|
|
363
|
+
function escapeLabel(value) {
|
|
364
|
+
return value.replace(/([\\[\]])/g, "\\$1");
|
|
365
|
+
}
|
|
366
|
+
function encodeLink(value) {
|
|
367
|
+
return encodeURI(value).replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
259
368
|
}
|
|
260
369
|
|
|
261
370
|
// src/commands/add-spec.ts
|
|
@@ -268,6 +377,10 @@ async function addSpec(name, projectName = "project") {
|
|
|
268
377
|
console.error(` ${pc.red("Error:")} .rsp/specs/decisions/ is reserved for Decision Records`);
|
|
269
378
|
process.exit(1);
|
|
270
379
|
}
|
|
380
|
+
if (["index", "00-index"].includes(name.split("/").at(-1) || "")) {
|
|
381
|
+
console.error(` ${pc.red("Error:")} spec name "index" is reserved for generated local navigation`);
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
271
384
|
guardRspInitialized();
|
|
272
385
|
try {
|
|
273
386
|
const configInspection = await inspectRspConfig();
|
|
@@ -294,11 +407,11 @@ async function addSpec(name, projectName = "project") {
|
|
|
294
407
|
try {
|
|
295
408
|
await writeFile2(path, content, { flag: "wx" });
|
|
296
409
|
created = true;
|
|
297
|
-
await buildSpecsIndex({ acquireLock: false });
|
|
410
|
+
await buildSpecsIndex({ acquireLock: false, affectedDirectory: parent });
|
|
298
411
|
} catch (error) {
|
|
299
412
|
if (created) {
|
|
300
413
|
try {
|
|
301
|
-
await
|
|
414
|
+
await unlink3(path);
|
|
302
415
|
await cleanupEmptyParentDirs(path, specsDir);
|
|
303
416
|
} catch {
|
|
304
417
|
}
|
|
@@ -313,129 +426,25 @@ async function addSpec(name, projectName = "project") {
|
|
|
313
426
|
}
|
|
314
427
|
|
|
315
428
|
// src/commands/archive.ts
|
|
316
|
-
import { existsSync
|
|
317
|
-
import { mkdir as mkdir3, readFile as
|
|
318
|
-
import { basename as
|
|
319
|
-
|
|
320
|
-
// src/commands/archive-index.ts
|
|
321
|
-
import { existsSync as existsSync2 } from "fs";
|
|
322
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
323
|
-
import { basename as basename2, dirname, join as join3, relative as relative2 } from "path";
|
|
324
|
-
async function buildArchiveIndex({ acquireLock = true, quiet = false } = {}) {
|
|
325
|
-
if (acquireLock)
|
|
326
|
-
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false, quiet }));
|
|
327
|
-
const inspection = await inspectArchiveTree();
|
|
328
|
-
if (inspection.diagnostics.length > 0) {
|
|
329
|
-
const diagnostic = inspection.diagnostics[0];
|
|
330
|
-
throw new WorkRefError(diagnostic.code, diagnostic.message, diagnostic.input);
|
|
331
|
-
}
|
|
332
|
-
if (!inspection.rootExists) {
|
|
333
|
-
if (!quiet)
|
|
334
|
-
console.log(` ${pc.dim("No archives directory.")}
|
|
335
|
-
`);
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
const archiveFiles = inspection.files;
|
|
339
|
-
const lines = [];
|
|
340
|
-
lines.push("---");
|
|
341
|
-
lines.push("title: Archive Index");
|
|
342
|
-
lines.push("summary: Completed RSP changes.");
|
|
343
|
-
lines.push("kind: generated-index");
|
|
344
|
-
lines.push("index_type: archives");
|
|
345
|
-
lines.push("source_dir: .rsp/archives");
|
|
346
|
-
lines.push(`entry_count: ${archiveFiles.length}`);
|
|
347
|
-
lines.push("---");
|
|
348
|
-
lines.push("");
|
|
349
|
-
lines.push("# Archive Index");
|
|
350
|
-
lines.push("");
|
|
351
|
-
if (archiveFiles.length === 0) {
|
|
352
|
-
lines.push("_No archived changes yet._");
|
|
353
|
-
} else {
|
|
354
|
-
lines.push("| Date | Change | Kind | Summary |");
|
|
355
|
-
lines.push("|------|--------|------|---------|");
|
|
356
|
-
}
|
|
357
|
-
for (const fp of archiveFiles) {
|
|
358
|
-
const base = basename2(fp);
|
|
359
|
-
const archiveRel = normalizeLogicalPath(relative2(ARCHIVES_DIR, fp));
|
|
360
|
-
const archiveDirPart = dirname(archiveRel);
|
|
361
|
-
const dateMatch = base.match(/^(\d{4}-\d{2}-\d{2})_(.+)\.md$/);
|
|
362
|
-
const date = dateMatch ? dateMatch[1] : "\u2014";
|
|
363
|
-
const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
|
|
364
|
-
const changeName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
|
|
365
|
-
let summary = "";
|
|
366
|
-
let kind = "";
|
|
367
|
-
try {
|
|
368
|
-
const content = await readFile3(fp, "utf-8");
|
|
369
|
-
const fm = parseFrontmatter(content);
|
|
370
|
-
if (typeof fm?.kind === "string")
|
|
371
|
-
kind = fm.kind;
|
|
372
|
-
if (typeof fm?.summary === "string")
|
|
373
|
-
summary = fm.summary.trim();
|
|
374
|
-
const proposal = extractSection(content, "Proposal");
|
|
375
|
-
const outcomeLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Outcome:"));
|
|
376
|
-
const legacySummaryLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Summary:"));
|
|
377
|
-
if (!summary && outcomeLine)
|
|
378
|
-
summary = outcomeLine.slice("- Outcome:".length).trim();
|
|
379
|
-
if (!summary && legacySummaryLine)
|
|
380
|
-
summary = legacySummaryLine.slice("- Summary:".length).trim();
|
|
381
|
-
} catch {
|
|
382
|
-
}
|
|
383
|
-
lines.push(`| ${date} | ${escapeCell2(changeName)} | ${escapeCell2(kind || "\u2014")} | ${escapeCell2(summary || "\u2014")} |`);
|
|
384
|
-
}
|
|
385
|
-
const indexPath = join3(ARCHIVES_DIR, "INDEX.md");
|
|
386
|
-
const nextContent = lines.join("\n");
|
|
387
|
-
const existingContent = existsSync2(indexPath) ? await readFile3(indexPath, "utf-8") : null;
|
|
388
|
-
const changed = existingContent !== nextContent;
|
|
389
|
-
if (changed)
|
|
390
|
-
await writeManagedFile(indexPath, nextContent, "archive index");
|
|
391
|
-
if (!quiet)
|
|
392
|
-
console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived change(s).
|
|
393
|
-
`);
|
|
394
|
-
return changed;
|
|
395
|
-
}
|
|
396
|
-
function escapeCell2(value) {
|
|
397
|
-
return value.replace(/\|/g, "\\|");
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// src/commands/archive.ts
|
|
401
|
-
async function archiveChange(name, options = {}) {
|
|
429
|
+
import { existsSync, readdirSync } from "fs";
|
|
430
|
+
import { mkdir as mkdir3, readFile as readFile3, rename, unlink as unlink4 } from "fs/promises";
|
|
431
|
+
import { basename as basename2, join as join3 } from "path";
|
|
432
|
+
async function archiveChange(name) {
|
|
402
433
|
if (!name) {
|
|
403
434
|
console.error(` ${pc.red("Usage:")} rsp archive <name>`);
|
|
404
435
|
process.exit(1);
|
|
405
436
|
}
|
|
406
437
|
guardRspInitialized();
|
|
407
|
-
if (options.dryRun) {
|
|
408
|
-
const workRef = await resolveArchiveWorkRefOrExit(name);
|
|
409
|
-
const content = await readFile4(workRef.path, "utf-8");
|
|
410
|
-
const dependencyInspection = await inspectChangeDependencies();
|
|
411
|
-
const checklist = collectArchiveChecklist(content, {
|
|
412
|
-
activeBlockers: dependencyInspection.activeBlockers.get(name)
|
|
413
|
-
});
|
|
414
|
-
console.log();
|
|
415
|
-
console.log(` ${pc.bold("Archive dry-run for")} ${pc.cyan(name)}`);
|
|
416
|
-
console.log();
|
|
417
|
-
if (checklist.length === 0) {
|
|
418
|
-
console.log(` ${pc.green("\u2713")} Ready to archive. No deterministic warnings found.
|
|
419
|
-
`);
|
|
420
|
-
} else {
|
|
421
|
-
for (const line of checklist)
|
|
422
|
-
console.log(` ${pc.yellow("\u26A0")} ${line}`);
|
|
423
|
-
console.log();
|
|
424
|
-
console.log(` ${pc.dim("Review the warnings above before treating this work as fully closed.")}
|
|
425
|
-
`);
|
|
426
|
-
}
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
438
|
try {
|
|
430
439
|
return await withRspLock("archive-change", async () => {
|
|
431
440
|
const workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
432
441
|
const srcPath = workRef.path;
|
|
433
442
|
const archiveSubdir = resolveArchiveDirectory(workRef);
|
|
434
443
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
435
|
-
const content = await
|
|
444
|
+
const content = await readFile3(srcPath, "utf-8");
|
|
436
445
|
const dependencyInspection = await inspectChangeDependencies();
|
|
437
446
|
const checklist = collectArchiveChecklist(content, {
|
|
438
|
-
activeBlockers: dependencyInspection.activeBlockers.get(name)
|
|
447
|
+
activeBlockers: dependencyInspection.activeBlockers.get(workRef.name)
|
|
439
448
|
});
|
|
440
449
|
for (const line of checklist)
|
|
441
450
|
console.log(` ${pc.yellow("\u26A0")} ${line}`);
|
|
@@ -443,15 +452,15 @@ async function archiveChange(name, options = {}) {
|
|
|
443
452
|
console.log(` ${pc.dim("Archive will continue. Review the warnings above before treating this work as fully closed.")}
|
|
444
453
|
`);
|
|
445
454
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
446
|
-
const base =
|
|
455
|
+
const base = basename2(workRef.name);
|
|
447
456
|
await mkdir3(archiveSubdir, { recursive: true });
|
|
448
457
|
const archiveName = resolveArchiveName(archiveSubdir, date, base);
|
|
449
|
-
await rename(srcPath,
|
|
458
|
+
await rename(srcPath, join3(archiveSubdir, archiveName));
|
|
450
459
|
const postArchiveWarnings = [];
|
|
451
460
|
let focusCleared = false;
|
|
452
|
-
if (
|
|
461
|
+
if (existsSync(focusEntry)) {
|
|
453
462
|
try {
|
|
454
|
-
await
|
|
463
|
+
await unlink4(focusEntry);
|
|
455
464
|
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
456
465
|
focusCleared = true;
|
|
457
466
|
} catch (error) {
|
|
@@ -467,17 +476,12 @@ async function archiveChange(name, options = {}) {
|
|
|
467
476
|
` : "";
|
|
468
477
|
console.log(` ${pc.green("Archived:")} ${archiveName}
|
|
469
478
|
${clearedMsg}`);
|
|
470
|
-
try {
|
|
471
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
472
|
-
} catch (error) {
|
|
473
|
-
postArchiveWarnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
|
|
474
|
-
}
|
|
475
479
|
for (const warning of postArchiveWarnings)
|
|
476
480
|
console.log(` ${pc.yellow("\u26A0")} ${warning}`);
|
|
477
481
|
if (postArchiveWarnings.length > 0)
|
|
478
482
|
console.log(` ${pc.dim("Archive completed, but follow-up cleanup was only partially successful.")}
|
|
479
483
|
`);
|
|
480
|
-
if (
|
|
484
|
+
if (existsSync(".git")) {
|
|
481
485
|
console.log(` ${pc.cyan("Git delivery:")}
|
|
482
486
|
`);
|
|
483
487
|
console.log(` git status --short`);
|
|
@@ -493,28 +497,14 @@ ${clearedMsg}`);
|
|
|
493
497
|
throw error;
|
|
494
498
|
}
|
|
495
499
|
}
|
|
496
|
-
async function resolveArchiveWorkRefOrExit(name) {
|
|
497
|
-
try {
|
|
498
|
-
const workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
499
|
-
resolveArchiveDirectory(workRef);
|
|
500
|
-
resolveFocusMarkerPath(workRef);
|
|
501
|
-
return workRef;
|
|
502
|
-
} catch (error) {
|
|
503
|
-
if (error instanceof WorkRefError) {
|
|
504
|
-
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
505
|
-
process.exit(1);
|
|
506
|
-
}
|
|
507
|
-
throw error;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
500
|
function resolveArchiveName(archiveSubdir, date, base) {
|
|
511
501
|
const initialName = `${date}_${base}.md`;
|
|
512
|
-
if (!
|
|
502
|
+
if (!archiveNameExists(archiveSubdir, initialName))
|
|
513
503
|
return initialName;
|
|
514
504
|
const MAX_SUFFIX = 9999;
|
|
515
505
|
for (let index = 2; index <= MAX_SUFFIX; index++) {
|
|
516
506
|
const candidate = `${date}_${base}-${index}.md`;
|
|
517
|
-
if (!
|
|
507
|
+
if (!archiveNameExists(archiveSubdir, candidate))
|
|
518
508
|
return candidate;
|
|
519
509
|
}
|
|
520
510
|
throw new WorkRefError(
|
|
@@ -523,9 +513,25 @@ function resolveArchiveName(archiveSubdir, date, base) {
|
|
|
523
513
|
base
|
|
524
514
|
);
|
|
525
515
|
}
|
|
516
|
+
function archiveNameExists(archiveSubdir, name) {
|
|
517
|
+
if (!existsSync(archiveSubdir))
|
|
518
|
+
return false;
|
|
519
|
+
return readdirSync(archiveSubdir).some((entry) => {
|
|
520
|
+
if (entry.normalize("NFC") !== name)
|
|
521
|
+
return false;
|
|
522
|
+
if (entry !== name) {
|
|
523
|
+
throw new WorkRefError(
|
|
524
|
+
"work_ref_collision",
|
|
525
|
+
`archive name has a Unicode normalization collision: ${entry} and ${name}`,
|
|
526
|
+
name
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
return true;
|
|
530
|
+
});
|
|
531
|
+
}
|
|
526
532
|
|
|
527
533
|
// src/commands/check.ts
|
|
528
|
-
import { readFile as
|
|
534
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
529
535
|
async function runCheck(options = {}) {
|
|
530
536
|
const diagnostics = [];
|
|
531
537
|
const runtime = [];
|
|
@@ -568,7 +574,7 @@ async function runCheck(options = {}) {
|
|
|
568
574
|
const entryContent = marker.name;
|
|
569
575
|
let markerContent = "";
|
|
570
576
|
try {
|
|
571
|
-
markerContent = (await
|
|
577
|
+
markerContent = (await readFile4(entryPath, "utf-8")).trim();
|
|
572
578
|
} catch (error) {
|
|
573
579
|
addDiagnostic({
|
|
574
580
|
severity: "error",
|
|
@@ -679,7 +685,7 @@ async function runCheck(options = {}) {
|
|
|
679
685
|
const { name, path: fp } = ref;
|
|
680
686
|
let content;
|
|
681
687
|
try {
|
|
682
|
-
content = await
|
|
688
|
+
content = await readFile4(fp, "utf-8");
|
|
683
689
|
} catch (error) {
|
|
684
690
|
addDiagnostic({
|
|
685
691
|
severity: "error",
|
|
@@ -714,8 +720,10 @@ async function runCheck(options = {}) {
|
|
|
714
720
|
message: "missing YAML frontmatter"
|
|
715
721
|
});
|
|
716
722
|
}
|
|
723
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
717
724
|
for (const section of requiredSections) {
|
|
718
|
-
|
|
725
|
+
const definition = getDocumentSectionDefinitionByHeading(CHANGE_DOCUMENT_SCHEMA, section);
|
|
726
|
+
if (!definition || !getDocumentSections(document, definition.id).some((candidate) => candidate.canonical)) {
|
|
719
727
|
addDiagnostic({
|
|
720
728
|
severity: "error",
|
|
721
729
|
code: "missing_section",
|
|
@@ -752,16 +760,14 @@ async function runCheck(options = {}) {
|
|
|
752
760
|
});
|
|
753
761
|
}
|
|
754
762
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const heading = headingLine.slice("# Change:".length).trim();
|
|
758
|
-
if (heading !== name) {
|
|
763
|
+
if (document.title !== null) {
|
|
764
|
+
if (document.title !== name) {
|
|
759
765
|
addDiagnostic({
|
|
760
766
|
severity: "error",
|
|
761
767
|
code: "heading_mismatch",
|
|
762
768
|
change: name,
|
|
763
769
|
path: fp,
|
|
764
|
-
message: `# Change: heading "${
|
|
770
|
+
message: `# Change: heading "${document.title}" differs from change name`
|
|
765
771
|
});
|
|
766
772
|
}
|
|
767
773
|
} else {
|
|
@@ -884,7 +890,7 @@ async function runCheck(options = {}) {
|
|
|
884
890
|
return result;
|
|
885
891
|
}
|
|
886
892
|
function findUnfinishedTemplateLines(content) {
|
|
887
|
-
const placeholderRe = /<[^>\n]*(?:choose:|one-line|what |why |which |who |how |new |updated |expected |actual |specific |concrete |exact |verifiable |user |capability |behavior |context |action
|
|
893
|
+
const placeholderRe = /<(?:…|[^>\n]*(?:choose:|one-line|what |why |which |who |how |new |updated |expected |actual |specific |concrete |exact |verifiable |user |capability |behavior |context |action|path|directory|module|subsystem|command|scenario|constraint|requirement|question|uncertainty|implementation)[^>\n]*)>/i;
|
|
888
894
|
return collectMatchingLines(content, (line) => {
|
|
889
895
|
const trimmed = stripInlineCodeSpans(line).trim();
|
|
890
896
|
return trimmed.startsWith("<!--") === false && placeholderRe.test(trimmed);
|
|
@@ -908,8 +914,8 @@ function collectMatchingLines(content, predicate) {
|
|
|
908
914
|
}
|
|
909
915
|
|
|
910
916
|
// src/commands/create.ts
|
|
911
|
-
import { existsSync as
|
|
912
|
-
import { mkdir as mkdir4, unlink as
|
|
917
|
+
import { existsSync as existsSync2 } from "fs";
|
|
918
|
+
import { mkdir as mkdir4, unlink as unlink5, writeFile as writeFile3 } from "fs/promises";
|
|
913
919
|
import { dirname as dirname2 } from "path";
|
|
914
920
|
async function createChange(name, summary = "", kind, options = {}) {
|
|
915
921
|
if (!name) {
|
|
@@ -921,10 +927,10 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
921
927
|
return await withRspLock("create-change", async () => {
|
|
922
928
|
const workRef = await resolveExecutableChange(name);
|
|
923
929
|
const changePath = workRef.path;
|
|
924
|
-
const existed =
|
|
930
|
+
const existed = existsSync2(changePath);
|
|
925
931
|
if (!existed) {
|
|
926
932
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
927
|
-
const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
|
|
933
|
+
const content = generateChangeContent(workRef.name, summary, kind, { lite: Boolean(options.lite) });
|
|
928
934
|
await mkdir4(dirname2(changePath), { recursive: true });
|
|
929
935
|
let changeCreated = false;
|
|
930
936
|
try {
|
|
@@ -936,7 +942,7 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
936
942
|
} catch (error) {
|
|
937
943
|
if (changeCreated) {
|
|
938
944
|
try {
|
|
939
|
-
await
|
|
945
|
+
await unlink5(changePath);
|
|
940
946
|
await cleanupEmptyParentDirs(changePath, CHANGES_DIR);
|
|
941
947
|
} catch {
|
|
942
948
|
}
|
|
@@ -947,9 +953,9 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
947
953
|
const label = existed ? "Using" : pc.green("Created");
|
|
948
954
|
console.log(` ${label}: ${changePath}`);
|
|
949
955
|
if (existed)
|
|
950
|
-
console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${name}`);
|
|
956
|
+
console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${workRef.name}`);
|
|
951
957
|
else
|
|
952
|
-
console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
|
|
958
|
+
console.log(` ${pc.dim("focused via focus.d")} \u2192 ${workRef.name}`);
|
|
953
959
|
console.log(` ${pc.cyan("Next:")} ${options.lite ? "fill the lite change details, then implement and verify" : "fill proposal/spec/design first, then implement and complete the tasks"}
|
|
954
960
|
`);
|
|
955
961
|
});
|
|
@@ -963,17 +969,39 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
963
969
|
}
|
|
964
970
|
|
|
965
971
|
// src/commands/doctor.ts
|
|
966
|
-
import { existsSync as
|
|
972
|
+
import { existsSync as existsSync4 } from "fs";
|
|
967
973
|
import { readdir, readFile as readFile7, stat } from "fs/promises";
|
|
968
|
-
import { basename as
|
|
974
|
+
import { basename as basename3, join as join6, relative as relative2 } from "path";
|
|
969
975
|
|
|
970
976
|
// src/commands/update.ts
|
|
971
|
-
import { existsSync as
|
|
977
|
+
import { existsSync as existsSync3 } from "fs";
|
|
972
978
|
import { mkdir as mkdir5, readFile as readFile6, rm, rmdir } from "fs/promises";
|
|
973
979
|
import { join as join5 } from "path";
|
|
980
|
+
|
|
981
|
+
// src/commands/archive-index-migration.ts
|
|
982
|
+
import { readFile as readFile5, unlink as unlink6 } from "fs/promises";
|
|
983
|
+
import { join as join4 } from "path";
|
|
984
|
+
async function removeLegacyArchiveIndex() {
|
|
985
|
+
const path = join4(ARCHIVES_DIR, "INDEX.md");
|
|
986
|
+
const inspection = inspectManagedFile(path, "legacy archive index", { allowMissing: true });
|
|
987
|
+
if (!inspection.exists || inspection.issue)
|
|
988
|
+
return false;
|
|
989
|
+
let frontmatter;
|
|
990
|
+
try {
|
|
991
|
+
frontmatter = parseFrontmatter(await readFile5(path, "utf-8"));
|
|
992
|
+
} catch {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
if (frontmatter?.kind !== "generated-index" || frontmatter.index_type !== "archives")
|
|
996
|
+
return false;
|
|
997
|
+
await unlink6(path);
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// src/commands/update.ts
|
|
974
1002
|
var SKILL_REFRESH_HINT = " Note: if you use package-bundled RSP Skills, refresh them too:\n rsp skills install --dry-run\n rsp skills install --force\n";
|
|
975
1003
|
async function updateProject(options = {}) {
|
|
976
|
-
if (!
|
|
1004
|
+
if (!existsSync3(RSP_DIR)) {
|
|
977
1005
|
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
978
1006
|
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
979
1007
|
process.exit(1);
|
|
@@ -1004,6 +1032,9 @@ async function updateProject(options = {}) {
|
|
|
1004
1032
|
console.log(` ${pc.green("\u2713")} changes/ directory restored`);
|
|
1005
1033
|
updated = true;
|
|
1006
1034
|
}
|
|
1035
|
+
const archivesInspection = inspectManagedDirectory(join5(RSP_DIR, "archives"), "archive root", { allowMissing: true });
|
|
1036
|
+
if (archivesInspection.issue)
|
|
1037
|
+
throw archivesInspection.issue;
|
|
1007
1038
|
const bundledRules = await readFile6(join5(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1008
1039
|
const rulesInspection = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
1009
1040
|
if (rulesInspection.issue)
|
|
@@ -1016,7 +1047,7 @@ async function updateProject(options = {}) {
|
|
|
1016
1047
|
console.log(` ${pc.green("\u2713")} rsp-rules.md updated`);
|
|
1017
1048
|
updated = true;
|
|
1018
1049
|
}
|
|
1019
|
-
if (
|
|
1050
|
+
if (existsSync3(OBSOLETE_RSP_RULES_PATH)) {
|
|
1020
1051
|
await rm(OBSOLETE_RSP_RULES_PATH);
|
|
1021
1052
|
actions.push("obsolete rules/rsp-rules.md removed");
|
|
1022
1053
|
if (!options.quiet)
|
|
@@ -1078,9 +1109,14 @@ async function updateProject(options = {}) {
|
|
|
1078
1109
|
updated = true;
|
|
1079
1110
|
}
|
|
1080
1111
|
const specsIndexChanged = await buildSpecsIndex({ acquireLock: false, quiet: options.quiet });
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1112
|
+
if (specsIndexChanged) {
|
|
1113
|
+
actions.push("generated Specs indexes reconciled");
|
|
1114
|
+
updated = true;
|
|
1115
|
+
}
|
|
1116
|
+
if (await removeLegacyArchiveIndex()) {
|
|
1117
|
+
actions.push("legacy Archive Index removed");
|
|
1118
|
+
if (!options.quiet)
|
|
1119
|
+
console.log(` ${pc.green("\u2713")} legacy Archive Index removed`);
|
|
1084
1120
|
updated = true;
|
|
1085
1121
|
}
|
|
1086
1122
|
if (!options.quiet && inspection.diagnostics.length > 0) {
|
|
@@ -1116,7 +1152,7 @@ async function updateProject(options = {}) {
|
|
|
1116
1152
|
async function runDoctor(options = {}) {
|
|
1117
1153
|
const fixed = [];
|
|
1118
1154
|
let repairIssue = null;
|
|
1119
|
-
if (options.fix &&
|
|
1155
|
+
if (options.fix && existsSync4(RSP_DIR)) {
|
|
1120
1156
|
try {
|
|
1121
1157
|
const update = await updateProject({ quiet: Boolean(options.json) });
|
|
1122
1158
|
fixed.push(...update.actions);
|
|
@@ -1136,17 +1172,12 @@ async function runDoctor(options = {}) {
|
|
|
1136
1172
|
const runtime = [];
|
|
1137
1173
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
1138
1174
|
const designPath = join6(RSP_DIR, "specs", "design.md");
|
|
1139
|
-
|
|
1140
|
-
const archivesIndexPath = join6(RSP_DIR, "archives", "INDEX.md");
|
|
1141
|
-
reportCheck(checks, ".rsp exists", existsSync6(RSP_DIR), "Run: rsp init");
|
|
1175
|
+
reportCheck(checks, ".rsp exists", existsSync4(RSP_DIR), "Run: rsp init");
|
|
1142
1176
|
reportFallbackProtocol(checks);
|
|
1143
1177
|
await reportUnsupportedRules(checks, reportRuntime);
|
|
1144
|
-
reportCheck(checks, "specs/design.md exists",
|
|
1145
|
-
reportCheck(checks, "specs/INDEX.md exists", existsSync6(specsIndexPath), "Run: rsp update");
|
|
1146
|
-
reportCheck(checks, "archives/INDEX.md exists", existsSync6(archivesIndexPath), "Run: rsp update");
|
|
1178
|
+
reportCheck(checks, "specs/design.md exists", existsSync4(designPath), "Run: rsp init");
|
|
1147
1179
|
await checkAgents(checks, reportRuntime);
|
|
1148
|
-
await
|
|
1149
|
-
await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "archives", "archives/INDEX.md has generated-index metadata", "Run: rsp update");
|
|
1180
|
+
await checkSpecsIndexes(checks, reportRuntime);
|
|
1150
1181
|
await checkArchiveNaming(checks);
|
|
1151
1182
|
const decisionRecordsConfigValid = await checkConfigSemantics(checks, reportRuntime);
|
|
1152
1183
|
if (decisionRecordsConfigValid) {
|
|
@@ -1174,7 +1205,7 @@ async function runDoctor(options = {}) {
|
|
|
1174
1205
|
if (fixed.length > 0) {
|
|
1175
1206
|
console.log(` ${pc.green("Fixed:")} ${fixed.join(", ")}`);
|
|
1176
1207
|
console.log();
|
|
1177
|
-
} else if (options.fix && !repairIssue &&
|
|
1208
|
+
} else if (options.fix && !repairIssue && existsSync4(RSP_DIR)) {
|
|
1178
1209
|
console.log(` ${pc.dim("No safe fixes needed.")}`);
|
|
1179
1210
|
console.log();
|
|
1180
1211
|
}
|
|
@@ -1192,7 +1223,7 @@ async function runDoctor(options = {}) {
|
|
|
1192
1223
|
}
|
|
1193
1224
|
function reportFallbackProtocol(checks) {
|
|
1194
1225
|
const canonical = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
1195
|
-
const obsoleteExists =
|
|
1226
|
+
const obsoleteExists = existsSync4(OBSOLETE_RSP_RULES_PATH);
|
|
1196
1227
|
if (canonical.issue) {
|
|
1197
1228
|
checks.push({
|
|
1198
1229
|
status: "issue",
|
|
@@ -1206,7 +1237,7 @@ function reportFallbackProtocol(checks) {
|
|
|
1206
1237
|
checks.push({
|
|
1207
1238
|
status: "issue",
|
|
1208
1239
|
label: "rsp-rules.md exists",
|
|
1209
|
-
hint:
|
|
1240
|
+
hint: existsSync4(RSP_DIR) ? "Run: rsp update" : "Run: rsp init"
|
|
1210
1241
|
});
|
|
1211
1242
|
}
|
|
1212
1243
|
if (obsoleteExists) {
|
|
@@ -1297,44 +1328,31 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
1297
1328
|
}
|
|
1298
1329
|
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
|
|
1299
1330
|
}
|
|
1300
|
-
async function
|
|
1301
|
-
const
|
|
1302
|
-
if (
|
|
1303
|
-
checks.push({ status: "
|
|
1331
|
+
async function checkSpecsIndexes(checks, reportRuntime) {
|
|
1332
|
+
const health = await inspectSpecsIndexHealth();
|
|
1333
|
+
if (health.ok) {
|
|
1334
|
+
checks.push({ status: "ok", label: "hierarchical Specs indexes are current generated files" });
|
|
1304
1335
|
return;
|
|
1305
1336
|
}
|
|
1306
|
-
|
|
1307
|
-
return;
|
|
1308
|
-
let content;
|
|
1309
|
-
try {
|
|
1310
|
-
content = await readFile7(path, "utf-8");
|
|
1311
|
-
} catch (error) {
|
|
1312
|
-
reportRuntime({
|
|
1313
|
-
code: "generated_index_read_failed",
|
|
1314
|
-
operation: "readFile",
|
|
1315
|
-
path,
|
|
1316
|
-
message: toErrorMessage(error)
|
|
1317
|
-
});
|
|
1318
|
-
checks.push({ status: "issue", label, message: `${label} could not be read`, hint: fixHint });
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
try {
|
|
1322
|
-
const fm = parseFrontmatter(content);
|
|
1323
|
-
if (fm?.kind === "generated-index" && fm.index_type === expectedIndexType) {
|
|
1324
|
-
checks.push({ status: "ok", label });
|
|
1325
|
-
return;
|
|
1326
|
-
}
|
|
1327
|
-
} catch (error) {
|
|
1337
|
+
for (const message of health.issues) {
|
|
1328
1338
|
reportRuntime({
|
|
1329
|
-
code: "
|
|
1330
|
-
operation: "
|
|
1331
|
-
path,
|
|
1332
|
-
message
|
|
1339
|
+
code: "specs_index_inspection_failed",
|
|
1340
|
+
operation: "inspectSpecsIndexHealth",
|
|
1341
|
+
path: ".rsp/specs",
|
|
1342
|
+
message
|
|
1333
1343
|
});
|
|
1334
|
-
checks.push({ status: "issue", label, message: `${label} could not parse metadata`, hint: fixHint });
|
|
1335
|
-
return;
|
|
1336
1344
|
}
|
|
1337
|
-
|
|
1345
|
+
const details = [
|
|
1346
|
+
...health.issues,
|
|
1347
|
+
health.stalePaths.length > 0 ? `missing or stale: ${health.stalePaths.join(", ")}` : "",
|
|
1348
|
+
health.obsoletePaths.length > 0 ? `recognized obsolete indexes: ${health.obsoletePaths.join(", ")}` : ""
|
|
1349
|
+
].filter(Boolean);
|
|
1350
|
+
checks.push({
|
|
1351
|
+
status: "issue",
|
|
1352
|
+
label: "hierarchical Specs indexes are current generated files",
|
|
1353
|
+
message: details.join("; "),
|
|
1354
|
+
hint: "Run: rsp update"
|
|
1355
|
+
});
|
|
1338
1356
|
}
|
|
1339
1357
|
async function checkArchiveNaming(checks) {
|
|
1340
1358
|
const archivesDir = join6(RSP_DIR, "archives");
|
|
@@ -1349,7 +1367,7 @@ async function checkArchiveNaming(checks) {
|
|
|
1349
1367
|
return;
|
|
1350
1368
|
}
|
|
1351
1369
|
checks.push({ status: "ok", label: "archives use supported managed paths" });
|
|
1352
|
-
const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(
|
|
1370
|
+
const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(relative2(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename3(fp)) === false);
|
|
1353
1371
|
if (invalidFiles.length === 0) {
|
|
1354
1372
|
checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
|
|
1355
1373
|
return;
|
|
@@ -1437,7 +1455,7 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
|
|
|
1437
1455
|
}
|
|
1438
1456
|
async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
1439
1457
|
const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
|
|
1440
|
-
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !
|
|
1458
|
+
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync4(DEFAULT_DECISION_RECORDS_PATH))
|
|
1441
1459
|
return;
|
|
1442
1460
|
let inspectionFailed = false;
|
|
1443
1461
|
const inactiveRecords = (await walkMarkdownFiles(DEFAULT_DECISION_RECORDS_PATH, {
|
|
@@ -1445,7 +1463,7 @@ async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
|
1445
1463
|
inspectionFailed = true;
|
|
1446
1464
|
reportRuntime(diagnostic);
|
|
1447
1465
|
}
|
|
1448
|
-
})).map((path) => normalizeLogicalPath(
|
|
1466
|
+
})).map((path) => normalizeLogicalPath(relative2(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
|
|
1449
1467
|
if (inspectionFailed) {
|
|
1450
1468
|
checks.push({
|
|
1451
1469
|
status: "issue",
|
|
@@ -1555,8 +1573,8 @@ async function checkActiveChangeConsistency(checks) {
|
|
|
1555
1573
|
}
|
|
1556
1574
|
|
|
1557
1575
|
// src/commands/focus.ts
|
|
1558
|
-
import { existsSync as
|
|
1559
|
-
import { mkdir as mkdir6, unlink as
|
|
1576
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1577
|
+
import { mkdir as mkdir6, unlink as unlink7 } from "fs/promises";
|
|
1560
1578
|
import { dirname as dirname3 } from "path";
|
|
1561
1579
|
async function focusChange(name) {
|
|
1562
1580
|
if (!name) {
|
|
@@ -1571,8 +1589,8 @@ async function focusChange(name) {
|
|
|
1571
1589
|
await mkdir6(FOCUS_DIR, { recursive: true });
|
|
1572
1590
|
await mkdir6(dirname3(focusEntry), { recursive: true });
|
|
1573
1591
|
await writeManagedFile(focusEntry, "", "focus marker");
|
|
1574
|
-
console.log(` ${pc.green("Focused:")} ${name}`);
|
|
1575
|
-
console.log(` ${pc.dim("focus.d")} \u2192 ${name}`);
|
|
1592
|
+
console.log(` ${pc.green("Focused:")} ${workRef.name}`);
|
|
1593
|
+
console.log(` ${pc.dim("focus.d")} \u2192 ${workRef.name}`);
|
|
1576
1594
|
console.log();
|
|
1577
1595
|
});
|
|
1578
1596
|
} catch (error) {
|
|
@@ -1589,12 +1607,12 @@ async function unfocusChange(name) {
|
|
|
1589
1607
|
return await withRspLock("unfocus-change", async () => {
|
|
1590
1608
|
const workRef = resolveWorkRef(name, { executable: false, mustExist: false });
|
|
1591
1609
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
1592
|
-
if (!
|
|
1610
|
+
if (!existsSync5(focusEntry))
|
|
1593
1611
|
throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
|
|
1594
|
-
await
|
|
1612
|
+
await unlink7(focusEntry);
|
|
1595
1613
|
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
1596
|
-
console.log(` ${pc.green("Unfocused:")} ${name}`);
|
|
1597
|
-
console.log(` ${pc.dim("focus.d cleared")} \u2192 ${name}`);
|
|
1614
|
+
console.log(` ${pc.green("Unfocused:")} ${workRef.name}`);
|
|
1615
|
+
console.log(` ${pc.dim("focus.d cleared")} \u2192 ${workRef.name}`);
|
|
1598
1616
|
console.log();
|
|
1599
1617
|
});
|
|
1600
1618
|
} catch (error) {
|
|
@@ -1629,20 +1647,21 @@ async function createChangeGroup(name, goal = "") {
|
|
|
1629
1647
|
const ref = resolveWorkRef(`${name}/brief`);
|
|
1630
1648
|
if (ref.kind !== "group-brief")
|
|
1631
1649
|
throw new WorkRefError("invalid_work_ref", `invalid Change Group name: ${name}`, name);
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1650
|
+
const groupName = ref.group;
|
|
1651
|
+
if (await hasArchivedGroupBrief(groupName))
|
|
1652
|
+
throw new ChangeGroupError(`archived Change Group cannot be reopened: ${groupName}`);
|
|
1653
|
+
const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [groupName], "Change Group path");
|
|
1635
1654
|
await mkdir7(groupDirectory, { recursive: true });
|
|
1636
1655
|
try {
|
|
1637
|
-
await writeFile4(ref.path, generateGroupBriefContent(
|
|
1656
|
+
await writeFile4(ref.path, generateGroupBriefContent(groupName, goal), { flag: "wx" });
|
|
1638
1657
|
} catch (error) {
|
|
1639
1658
|
if (error.code === "EEXIST")
|
|
1640
|
-
throw new ChangeGroupError(`Change Group already exists: ${
|
|
1659
|
+
throw new ChangeGroupError(`Change Group already exists: ${groupName}`);
|
|
1641
1660
|
throw error;
|
|
1642
1661
|
}
|
|
1643
|
-
console.log(` ${pc.green("Created Change Group:")} ${
|
|
1662
|
+
console.log(` ${pc.green("Created Change Group:")} ${groupName}`);
|
|
1644
1663
|
console.log(` ${pc.dim("Unfocused Group Brief:")} ${ref.path}`);
|
|
1645
|
-
console.log(` ${pc.cyan("Next:")} fill the brief, then create direct child Changes with rsp create ${
|
|
1664
|
+
console.log(` ${pc.cyan("Next:")} fill the brief, then create direct child Changes with rsp create ${groupName}/<change>
|
|
1646
1665
|
`);
|
|
1647
1666
|
});
|
|
1648
1667
|
} catch (error) {
|
|
@@ -1665,12 +1684,13 @@ async function closeChangeGroup(name) {
|
|
|
1665
1684
|
if (ref.kind !== "group-brief")
|
|
1666
1685
|
throw new ChangeGroupError(`invalid Change Group name: ${name}`);
|
|
1667
1686
|
const inspection = await inspectChangeGroups();
|
|
1668
|
-
const
|
|
1687
|
+
const groupName = ref.group;
|
|
1688
|
+
const group = inspection.groups.find((candidate) => candidate.name === groupName);
|
|
1669
1689
|
if (!group)
|
|
1670
|
-
throw new ChangeGroupError(`Change Group not found: ${
|
|
1671
|
-
const reasons = collectCloseReasons(group, inspection.diagnostics.filter((diagnostic) => diagnostic.change ===
|
|
1690
|
+
throw new ChangeGroupError(`Change Group not found: ${groupName}`);
|
|
1691
|
+
const reasons = collectCloseReasons(group, inspection.diagnostics.filter((diagnostic) => diagnostic.change === groupName && diagnostic.severity === "error").map((diagnostic) => diagnostic.message));
|
|
1672
1692
|
if (reasons.length > 0)
|
|
1673
|
-
throw new ChangeGroupError(`Change Group "${
|
|
1693
|
+
throw new ChangeGroupError(`Change Group "${groupName}" is not ready to close: ${reasons.join("; ")}`);
|
|
1674
1694
|
const archiveDirectory = resolveArchiveDirectory(ref);
|
|
1675
1695
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1676
1696
|
const archiveName = resolveArchiveName(archiveDirectory, date, "brief");
|
|
@@ -1682,12 +1702,7 @@ async function closeChangeGroup(name) {
|
|
|
1682
1702
|
} catch (error) {
|
|
1683
1703
|
warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
1684
1704
|
}
|
|
1685
|
-
|
|
1686
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
1687
|
-
} catch (error) {
|
|
1688
|
-
warnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
|
|
1689
|
-
}
|
|
1690
|
-
console.log(` ${pc.green("Closed Change Group:")} ${name}`);
|
|
1705
|
+
console.log(` ${pc.green("Closed Change Group:")} ${groupName}`);
|
|
1691
1706
|
console.log(` ${pc.dim("Archived Group Brief:")} ${join7(archiveDirectory, archiveName)}
|
|
1692
1707
|
`);
|
|
1693
1708
|
for (const warning of warnings)
|
|
@@ -1723,8 +1738,6 @@ function collectCloseReasons(group, errors) {
|
|
|
1723
1738
|
|
|
1724
1739
|
// src/commands/history.ts
|
|
1725
1740
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1726
|
-
var GROUP_RE = /^[a-z0-9-]+$/;
|
|
1727
|
-
var WORK_REF_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)?$/;
|
|
1728
1741
|
async function showHistory(input = {}, options = {}) {
|
|
1729
1742
|
guardRspInitialized();
|
|
1730
1743
|
const mode = input.workRef ? "detail" : "list";
|
|
@@ -1738,15 +1751,15 @@ async function showHistory(input = {}, options = {}) {
|
|
|
1738
1751
|
message: "archive history inspection is incomplete; resolve the reported diagnostics before querying history"
|
|
1739
1752
|
}, inspection.diagnostics, inspection.diagnosticSummary, inspection.runtime, options);
|
|
1740
1753
|
}
|
|
1741
|
-
if (
|
|
1754
|
+
if (validation.workRef) {
|
|
1742
1755
|
try {
|
|
1743
|
-
const selected = selectArchiveHistoryRecord(inspection.records, { workRef:
|
|
1756
|
+
const selected = selectArchiveHistoryRecord(inspection.records, { workRef: validation.workRef });
|
|
1744
1757
|
const record = await readArchiveHistoryDetail(selected);
|
|
1745
1758
|
const result2 = {
|
|
1746
1759
|
command: "history",
|
|
1747
1760
|
ok: true,
|
|
1748
1761
|
mode: "detail",
|
|
1749
|
-
query: { workRef:
|
|
1762
|
+
query: { workRef: validation.workRef },
|
|
1750
1763
|
record,
|
|
1751
1764
|
diagnostics: [],
|
|
1752
1765
|
diagnosticSummary: emptyCollectionSummary(),
|
|
@@ -1780,7 +1793,8 @@ async function showHistory(input = {}, options = {}) {
|
|
|
1780
1793
|
since: query.since ?? null,
|
|
1781
1794
|
until: query.until ?? null,
|
|
1782
1795
|
kind: query.kind ?? null,
|
|
1783
|
-
group: query.group ?? null
|
|
1796
|
+
group: query.group ?? null,
|
|
1797
|
+
search: query.search ?? null
|
|
1784
1798
|
},
|
|
1785
1799
|
records: listed.records,
|
|
1786
1800
|
summary: listed.summary,
|
|
@@ -1798,11 +1812,15 @@ function validateHistoryQuery(input) {
|
|
|
1798
1812
|
if ((input.positionalCount ?? (input.workRef ? 1 : 0)) > 1)
|
|
1799
1813
|
return invalid("history_positional_args_unsupported", "history accepts at most one positional WorkRef");
|
|
1800
1814
|
if (input.workRef) {
|
|
1801
|
-
|
|
1815
|
+
let workRef;
|
|
1816
|
+
try {
|
|
1817
|
+
workRef = normalizeExecutableWorkRef(input.workRef);
|
|
1818
|
+
} catch {
|
|
1802
1819
|
return invalid("invalid_history_work_ref", "history WorkRef must be a flat or one-Group-level executable Change identity");
|
|
1803
|
-
|
|
1820
|
+
}
|
|
1821
|
+
if (input.limit !== void 0 || input.since !== void 0 || input.until !== void 0 || input.kind !== void 0 || input.group !== void 0 || input.search !== void 0)
|
|
1804
1822
|
return invalid("history_detail_filters_unsupported", "list filters cannot be combined with an exact history detail lookup");
|
|
1805
|
-
return { ok: true, query: {} };
|
|
1823
|
+
return { ok: true, query: {}, workRef };
|
|
1806
1824
|
}
|
|
1807
1825
|
const limit = input.limit === void 0 ? HISTORY_DEFAULT_LIMIT : Number(input.limit);
|
|
1808
1826
|
if (input.limit !== void 0 && !/^[1-9]\d*$/.test(input.limit) || !Number.isInteger(limit) || limit < 1 || limit > HISTORY_MAX_LIMIT)
|
|
@@ -1815,8 +1833,16 @@ function validateHistoryQuery(input) {
|
|
|
1815
1833
|
return invalid("invalid_history_date_range", "--since must be on or before --until");
|
|
1816
1834
|
if (input.kind !== void 0 && input.kind.trim() === "")
|
|
1817
1835
|
return invalid("invalid_history_kind", "--kind must be a non-empty exact historical kind");
|
|
1818
|
-
|
|
1819
|
-
|
|
1836
|
+
let group = input.group;
|
|
1837
|
+
if (group !== void 0) {
|
|
1838
|
+
try {
|
|
1839
|
+
group = normalizeWorkRefSegment(group);
|
|
1840
|
+
} catch {
|
|
1841
|
+
return invalid("invalid_history_group", "--group must be one safe Unicode Group name");
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
if (input.search !== void 0 && input.search.trim() === "")
|
|
1845
|
+
return invalid("invalid_history_search", "--search must be non-empty");
|
|
1820
1846
|
return {
|
|
1821
1847
|
ok: true,
|
|
1822
1848
|
query: {
|
|
@@ -1824,7 +1850,8 @@ function validateHistoryQuery(input) {
|
|
|
1824
1850
|
since: input.since,
|
|
1825
1851
|
until: input.until,
|
|
1826
1852
|
kind: input.kind?.trim(),
|
|
1827
|
-
group
|
|
1853
|
+
group,
|
|
1854
|
+
search: input.search?.trim()
|
|
1828
1855
|
}
|
|
1829
1856
|
};
|
|
1830
1857
|
}
|
|
@@ -1916,7 +1943,7 @@ function printHistoryDetail(result) {
|
|
|
1916
1943
|
}
|
|
1917
1944
|
|
|
1918
1945
|
// src/commands/init.ts
|
|
1919
|
-
import { existsSync as
|
|
1946
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1920
1947
|
import { mkdir as mkdir8, readFile as readFile8 } from "fs/promises";
|
|
1921
1948
|
import { join as join8 } from "path";
|
|
1922
1949
|
function generateConfigTemplate() {
|
|
@@ -1932,9 +1959,16 @@ function generateConfigTemplate() {
|
|
|
1932
1959
|
# kinds:
|
|
1933
1960
|
${fmtList(VALID_KINDS, 0)}
|
|
1934
1961
|
|
|
1962
|
+
# Set one shared project default for durable artifact and commit prose.
|
|
1963
|
+
# Optional artifacts or commit values override that durable surface independently.
|
|
1964
|
+
# Response language remains user/session-owned and is never read from this project mapping.
|
|
1965
|
+
# Omit this mapping to keep durable language selection under scoped project authority.
|
|
1966
|
+
# language:
|
|
1967
|
+
# default: en
|
|
1968
|
+
|
|
1935
1969
|
# New projects default to automatic Manage routing with lifecycle-only closeout.
|
|
1936
1970
|
# Use activation: explicit to require a named managed request.
|
|
1937
|
-
# closeout accepts manual, lifecycle, or local; local
|
|
1971
|
+
# closeout accepts manual, lifecycle, or local; local routes a qualified clean terminal non-small boundary to one local commit but never push or publication.
|
|
1938
1972
|
manage:
|
|
1939
1973
|
activation: auto
|
|
1940
1974
|
closeout: lifecycle
|
|
@@ -1952,7 +1986,7 @@ function toTitle(projectName) {
|
|
|
1952
1986
|
return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
1953
1987
|
}
|
|
1954
1988
|
async function initProject(args = {}) {
|
|
1955
|
-
const isNew = !
|
|
1989
|
+
const isNew = !existsSync6(RSP_DIR);
|
|
1956
1990
|
const projectName = await detectProjectName();
|
|
1957
1991
|
const mode = args.agentsMode ?? "managed";
|
|
1958
1992
|
if (mode !== "managed" && mode !== "print") {
|
|
@@ -1967,7 +2001,7 @@ async function initProject(args = {}) {
|
|
|
1967
2001
|
const agentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
|
|
1968
2002
|
if (agentsInspection.issue)
|
|
1969
2003
|
throw agentsInspection.issue;
|
|
1970
|
-
if (
|
|
2004
|
+
if (existsSync6(CONFIG_PATH)) {
|
|
1971
2005
|
clearConfigCache();
|
|
1972
2006
|
const existingConfig = await inspectRspConfig();
|
|
1973
2007
|
if (existingConfig.issues.length > 0)
|
|
@@ -1997,12 +2031,9 @@ async function initProject(args = {}) {
|
|
|
1997
2031
|
throw new Error(configInspection.issues.join("; "));
|
|
1998
2032
|
const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
|
|
1999
2033
|
created = await ensureDecisionRecordsDirectory(decisionRecordsPath) || created;
|
|
2000
|
-
const createdSpecsIndex = await ensureFile(join8(RSP_DIR, "specs", "INDEX.md"), "# Specs Index\n\n_Additional project-level specs beyond `design.md`._\n");
|
|
2001
|
-
created = createdSpecsIndex || created;
|
|
2002
2034
|
const createdDesign = await ensureFile(join8(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
|
|
2003
2035
|
created = createdDesign || created;
|
|
2004
|
-
|
|
2005
|
-
created = createdArchivesIndex || created;
|
|
2036
|
+
created = await removeLegacyArchiveIndex() || created;
|
|
2006
2037
|
created = await ensureFile(join8(CHANGES_DIR, ".gitkeep"), "") || created;
|
|
2007
2038
|
created = await ensureFile(join8(FOCUS_DIR, ".gitkeep"), "") || created;
|
|
2008
2039
|
created = await ensureFile(join8(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
|
|
@@ -2013,10 +2044,7 @@ async function initProject(args = {}) {
|
|
|
2013
2044
|
created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
|
|
2014
2045
|
created = await ensureFile(focusMarker, "") || created;
|
|
2015
2046
|
}
|
|
2016
|
-
|
|
2017
|
-
await buildSpecsIndex({ acquireLock: false });
|
|
2018
|
-
if (createdArchivesIndex)
|
|
2019
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
2047
|
+
created = await buildSpecsIndex({ acquireLock: false }) || created;
|
|
2020
2048
|
const title = toTitle(projectName);
|
|
2021
2049
|
let agentsUpdated = false;
|
|
2022
2050
|
if (!agentsInspection.exists) {
|
|
@@ -2192,8 +2220,124 @@ function formatArchiveReady(value) {
|
|
|
2192
2220
|
}
|
|
2193
2221
|
}
|
|
2194
2222
|
|
|
2223
|
+
// src/commands/reopen.ts
|
|
2224
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2225
|
+
import { mkdir as mkdir9, readFile as readFile10, unlink as unlink8, writeFile as writeFile5 } from "fs/promises";
|
|
2226
|
+
import { dirname as dirname4 } from "path";
|
|
2227
|
+
async function reopenChange(name, options) {
|
|
2228
|
+
if (!name) {
|
|
2229
|
+
console.error(` ${pc.red("Usage:")} rsp reopen <name> --reason <text> [--from <archive-path>]`);
|
|
2230
|
+
process.exit(1);
|
|
2231
|
+
}
|
|
2232
|
+
const reason = options.reason?.trim();
|
|
2233
|
+
if (!reason || /[\r\n]/.test(reason)) {
|
|
2234
|
+
console.error(` ${pc.red("Error:")} --reason must be one non-empty line`);
|
|
2235
|
+
process.exit(1);
|
|
2236
|
+
}
|
|
2237
|
+
guardRspInitialized();
|
|
2238
|
+
try {
|
|
2239
|
+
await withRspLock("reopen-change", async () => {
|
|
2240
|
+
const normalizedName = normalizeExecutableWorkRef(name);
|
|
2241
|
+
const inspection = await inspectArchiveHistory();
|
|
2242
|
+
if (!historyInspectionComplete(inspection)) {
|
|
2243
|
+
throw new ArchiveHistoryError(
|
|
2244
|
+
"archive_inspection_incomplete",
|
|
2245
|
+
"archive history inspection is incomplete; resolve the reported diagnostics before reopening work"
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2248
|
+
const record = selectArchiveHistoryRecord(
|
|
2249
|
+
inspection.records,
|
|
2250
|
+
options.from ? { path: options.from } : { workRef: normalizedName }
|
|
2251
|
+
);
|
|
2252
|
+
if (record.workRef !== normalizedName) {
|
|
2253
|
+
throw new ArchiveHistoryError(
|
|
2254
|
+
"archive_identity_mismatch",
|
|
2255
|
+
`selected archive belongs to ${record.workRef}, not ${normalizedName}`
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
let workRef;
|
|
2259
|
+
try {
|
|
2260
|
+
workRef = resolveWorkRef(normalizedName, { executable: true });
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
if (error instanceof WorkRefError && error.code === "group_brief_missing") {
|
|
2263
|
+
throw new ArchiveHistoryError(
|
|
2264
|
+
"archived_group_closed",
|
|
2265
|
+
`cannot reopen ${name} because its Change Group is closed; reopen the Group through a separately authorized lifecycle operation first`
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
throw error;
|
|
2269
|
+
}
|
|
2270
|
+
if (existsSync7(workRef.path))
|
|
2271
|
+
throw new ArchiveHistoryError("open_change_exists", `open Change already exists: ${name}`);
|
|
2272
|
+
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
2273
|
+
if (existsSync7(focusEntry))
|
|
2274
|
+
throw new ArchiveHistoryError("focus_marker_exists", `focus marker already exists for non-open Change: ${name}; run rsp doctor`);
|
|
2275
|
+
const archivedContent = await readFile10(record.sourcePath, "utf-8");
|
|
2276
|
+
const reopenedContent = appendChecklistItem(
|
|
2277
|
+
appendChecklistItem(archivedContent, "tasks", `- [ ] Resolve reopened concern: ${reason}`),
|
|
2278
|
+
"verify",
|
|
2279
|
+
`- [ ] Verify reopened concern: ${reason}`
|
|
2280
|
+
);
|
|
2281
|
+
await mkdir9(dirname4(workRef.path), { recursive: true });
|
|
2282
|
+
let changeCreated = false;
|
|
2283
|
+
try {
|
|
2284
|
+
await writeFile5(workRef.path, reopenedContent, { flag: "wx" });
|
|
2285
|
+
changeCreated = true;
|
|
2286
|
+
await mkdir9(FOCUS_DIR, { recursive: true });
|
|
2287
|
+
await mkdir9(dirname4(focusEntry), { recursive: true });
|
|
2288
|
+
await writeManagedFile(focusEntry, "", "focus marker");
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
if (changeCreated) {
|
|
2291
|
+
try {
|
|
2292
|
+
await unlink8(workRef.path);
|
|
2293
|
+
await cleanupEmptyParentDirs(workRef.path, CHANGES_DIR);
|
|
2294
|
+
} catch {
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
throw error;
|
|
2298
|
+
}
|
|
2299
|
+
console.log(` ${pc.green("Reopened:")} ${workRef.name}`);
|
|
2300
|
+
console.log(` ${pc.dim("retained archive")} \u2192 ${record.path}`);
|
|
2301
|
+
console.log(` ${pc.dim("focused via focus.d")} \u2192 ${workRef.name}`);
|
|
2302
|
+
console.log(` ${pc.cyan("Next:")} refine the reopened Task and Verify evidence, then resolve the concern
|
|
2303
|
+
`);
|
|
2304
|
+
if (existsSync7(".git")) {
|
|
2305
|
+
console.log(` ${pc.cyan("Git delivery:")}`);
|
|
2306
|
+
console.log(` git status --short`);
|
|
2307
|
+
console.log(` Inspect the complete reopen transition; stage and commit only with separate Git authority.`);
|
|
2308
|
+
console.log();
|
|
2309
|
+
}
|
|
2310
|
+
});
|
|
2311
|
+
} catch (error) {
|
|
2312
|
+
if (error instanceof ArchiveHistoryError || error instanceof WorkRefError) {
|
|
2313
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
2314
|
+
if (error instanceof ArchiveHistoryError) {
|
|
2315
|
+
for (const candidate of error.candidates)
|
|
2316
|
+
console.error(` ${candidate}`);
|
|
2317
|
+
if (error.candidatesTruncated)
|
|
2318
|
+
console.error(` ... ${error.candidateTotal - error.candidates.length} more`);
|
|
2319
|
+
}
|
|
2320
|
+
process.exit(1);
|
|
2321
|
+
}
|
|
2322
|
+
throw error;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
function appendChecklistItem(content, sectionId, item) {
|
|
2326
|
+
try {
|
|
2327
|
+
return appendDocumentSectionItem(parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), sectionId, item);
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
if (!(error instanceof DocumentSectionCardinalityError))
|
|
2330
|
+
throw error;
|
|
2331
|
+
const heading = CHANGE_DOCUMENT_SCHEMA.sections.find((section) => section.id === sectionId).heading;
|
|
2332
|
+
throw new ArchiveHistoryError(
|
|
2333
|
+
"archive_section_invalid",
|
|
2334
|
+
`archived Change must contain exactly one required section: ${heading}`
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2195
2339
|
// src/commands/show.ts
|
|
2196
|
-
import { readFile as
|
|
2340
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2197
2341
|
function exitShowError(error, options) {
|
|
2198
2342
|
const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
|
|
2199
2343
|
if (options.json) {
|
|
@@ -2251,7 +2395,7 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
2251
2395
|
const srcPath = workRef.path;
|
|
2252
2396
|
let content;
|
|
2253
2397
|
try {
|
|
2254
|
-
content = await
|
|
2398
|
+
content = await readFile11(srcPath, "utf-8");
|
|
2255
2399
|
} catch {
|
|
2256
2400
|
exitShowError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, options);
|
|
2257
2401
|
}
|
|
@@ -2383,6 +2527,7 @@ function deriveStatusView(snapshot, options = {}) {
|
|
|
2383
2527
|
const nextActions = ok ? buildStatusNextActions(snapshot.focused.length, snapshot.records.map((record) => record.output), snapshot.groups, filtered) : ["Run: rsp doctor"];
|
|
2384
2528
|
return {
|
|
2385
2529
|
manage: snapshot.manage,
|
|
2530
|
+
language: snapshot.language,
|
|
2386
2531
|
query,
|
|
2387
2532
|
focused: snapshot.focused,
|
|
2388
2533
|
records,
|
|
@@ -2479,6 +2624,8 @@ function printStatusPlain(view) {
|
|
|
2479
2624
|
console.log(` ${pc.bold("RSP status")}`);
|
|
2480
2625
|
console.log();
|
|
2481
2626
|
console.log(` ${pc.bold("Manage:")} activation ${view.manage.activation} \xB7 closeout ${view.manage.closeout}`);
|
|
2627
|
+
if (view.language.artifacts !== null || view.language.commit !== null)
|
|
2628
|
+
console.log(` ${pc.bold("Language:")} artifacts ${view.language.artifacts ?? "unset"} \xB7 commit ${view.language.commit ?? "unset"}`);
|
|
2482
2629
|
console.log();
|
|
2483
2630
|
for (const diagnostic of view.diagnostics) {
|
|
2484
2631
|
const label = diagnostic.change ?? diagnostic.path;
|
|
@@ -2598,6 +2745,7 @@ function toStatusJson(view) {
|
|
|
2598
2745
|
command: "status",
|
|
2599
2746
|
ok: view.ok,
|
|
2600
2747
|
manage: view.manage,
|
|
2748
|
+
language: view.language,
|
|
2601
2749
|
filters: view.query,
|
|
2602
2750
|
focused: view.focused,
|
|
2603
2751
|
records: view.records.map((record) => record.output),
|
|
@@ -2615,6 +2763,7 @@ function toStatusJsonError(error, options) {
|
|
|
2615
2763
|
command: "status",
|
|
2616
2764
|
ok: false,
|
|
2617
2765
|
manage: { activation: "explicit", closeout: "manual" },
|
|
2766
|
+
language: { artifacts: null, commit: null },
|
|
2618
2767
|
filters: {
|
|
2619
2768
|
focused: options.focused,
|
|
2620
2769
|
blocked: options.blocked,
|
|
@@ -2788,7 +2937,7 @@ var groupCommand = defineCommand({
|
|
|
2788
2937
|
var addSpecCommand = defineCommand({
|
|
2789
2938
|
meta: {
|
|
2790
2939
|
name: "spec",
|
|
2791
|
-
description: "Create .rsp/specs/<name>.md and
|
|
2940
|
+
description: "Create .rsp/specs/<name>.md and refresh local Specs indexes"
|
|
2792
2941
|
},
|
|
2793
2942
|
args: {
|
|
2794
2943
|
name: {
|
|
@@ -2823,12 +2972,42 @@ var archiveCommand = defineCommand({
|
|
|
2823
2972
|
},
|
|
2824
2973
|
"dry-run": {
|
|
2825
2974
|
type: "boolean",
|
|
2826
|
-
description: "
|
|
2975
|
+
description: "Deprecated compatibility route to rsp ready; never moves the Change",
|
|
2827
2976
|
default: false
|
|
2828
2977
|
}
|
|
2829
2978
|
},
|
|
2830
2979
|
async run({ args }) {
|
|
2831
|
-
|
|
2980
|
+
if (args["dry-run"]) {
|
|
2981
|
+
console.error(" Deprecated: use `rsp ready <name>` for read-only archive readiness.");
|
|
2982
|
+
await showReady(args.name);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
await archiveChange(args.name);
|
|
2986
|
+
}
|
|
2987
|
+
});
|
|
2988
|
+
var reopenCommand = defineCommand({
|
|
2989
|
+
meta: {
|
|
2990
|
+
name: "reopen",
|
|
2991
|
+
description: "Restore one archived Change as focused open work while retaining history"
|
|
2992
|
+
},
|
|
2993
|
+
args: {
|
|
2994
|
+
name: {
|
|
2995
|
+
type: "positional",
|
|
2996
|
+
description: "Archived Change WorkRef",
|
|
2997
|
+
required: true
|
|
2998
|
+
},
|
|
2999
|
+
from: {
|
|
3000
|
+
type: "string",
|
|
3001
|
+
description: "Exact .rsp/archives/... path when the WorkRef has multiple archives"
|
|
3002
|
+
},
|
|
3003
|
+
reason: {
|
|
3004
|
+
type: "string",
|
|
3005
|
+
description: "One-line concern that makes the restored Change unfinished",
|
|
3006
|
+
required: true
|
|
3007
|
+
}
|
|
3008
|
+
},
|
|
3009
|
+
async run({ args }) {
|
|
3010
|
+
await reopenChange(args.name, { from: args.from, reason: args.reason });
|
|
2832
3011
|
}
|
|
2833
3012
|
});
|
|
2834
3013
|
var focusCommand = defineCommand({
|
|
@@ -2969,7 +3148,7 @@ var checkCommand = defineCommand({
|
|
|
2969
3148
|
var updateCommand = defineCommand({
|
|
2970
3149
|
meta: {
|
|
2971
3150
|
name: "update",
|
|
2972
|
-
description: "Refresh RSP project
|
|
3151
|
+
description: "Refresh RSP-managed project files after upgrade; does not update packaged Skills"
|
|
2973
3152
|
},
|
|
2974
3153
|
async run() {
|
|
2975
3154
|
const result = await updateProject();
|
|
@@ -3176,6 +3355,10 @@ var historyCommand = defineCommand({
|
|
|
3176
3355
|
type: "string",
|
|
3177
3356
|
description: "Match one exact Change Group"
|
|
3178
3357
|
},
|
|
3358
|
+
search: {
|
|
3359
|
+
type: "string",
|
|
3360
|
+
description: "Case-insensitive literal match over WorkRef and summary"
|
|
3361
|
+
},
|
|
3179
3362
|
json: {
|
|
3180
3363
|
type: "boolean",
|
|
3181
3364
|
description: "Print machine-readable JSON output",
|
|
@@ -3195,6 +3378,7 @@ var historyCommand = defineCommand({
|
|
|
3195
3378
|
until: args.until,
|
|
3196
3379
|
kind: args.kind,
|
|
3197
3380
|
group: args.group,
|
|
3381
|
+
search: args.search,
|
|
3198
3382
|
positionalCount: args._?.length ?? (args.name ? 1 : 0)
|
|
3199
3383
|
}, { json: Boolean(args.json), compact: Boolean(args.compact) });
|
|
3200
3384
|
if (!result.ok)
|
|
@@ -3212,7 +3396,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
3212
3396
|
if (shouldLaunchSkillsUi(rawArgs, terminalEnvironment)) {
|
|
3213
3397
|
const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
|
|
3214
3398
|
const locale = resolveUiLocale("auto", process.env.RSP_UI_LANG, process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? new Intl.DateTimeFormat().resolvedOptions().locale);
|
|
3215
|
-
const { runSkillsTui } = await import("./entry-
|
|
3399
|
+
const { runSkillsTui } = await import("./entry-5OPKUZHF.mjs");
|
|
3216
3400
|
const selection = await runSkillsTui(locale);
|
|
3217
3401
|
if (selection.kind === "confirmed") {
|
|
3218
3402
|
const result = await installPackagedSkills({ names: selection.names, force: selection.force });
|
|
@@ -3230,7 +3414,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
3230
3414
|
} else {
|
|
3231
3415
|
const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
|
|
3232
3416
|
const locale = resolveUiLocale(lang, process.env.RSP_UI_LANG, process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? new Intl.DateTimeFormat().resolvedOptions().locale);
|
|
3233
|
-
const { runTui } = await import("./entry-
|
|
3417
|
+
const { runTui } = await import("./entry-FVUG6YW2.mjs");
|
|
3234
3418
|
const exitCode = await runTui(locale);
|
|
3235
3419
|
if (exitCode !== 0)
|
|
3236
3420
|
process.exitCode = exitCode;
|
|
@@ -3253,6 +3437,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
3253
3437
|
focus: focusCommand,
|
|
3254
3438
|
unfocus: unfocusCommand,
|
|
3255
3439
|
archive: archiveCommand,
|
|
3440
|
+
reopen: reopenCommand,
|
|
3256
3441
|
ready: readyCommand,
|
|
3257
3442
|
show: showCommand,
|
|
3258
3443
|
history: historyCommand,
|