@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/dist/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  installPackagedSkills,
5
5
  printPackagedSkillInventory,
6
6
  printSkillInstallResult
7
- } from "./chunk-4K53353L.mjs";
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-EALHA3ZS.mjs";
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-OTJFL6XH.mjs";
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 unlink2, writeFile as writeFile2 } from "fs/promises";
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 { existsSync } from "fs";
161
- import { readFile as readFile2 } from "fs/promises";
162
- import { basename, isAbsolute, join, relative, sep } from "path";
163
- async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
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
- if (!quiet)
176
- console.log(` ${pc.dim("No specs directory.")}
177
- `);
178
- return false;
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
- const specFiles = inspection.files.filter((fp) => fp.endsWith(".md")).filter((fp) => {
181
- const name = basename(fp);
182
- return name !== "INDEX.md" && name !== "design.md" && !isInside(DEFAULT_DECISION_RECORDS_PATH, fp) && !isInside(decisionRecordsPath, fp);
183
- }).sort();
184
- const lines = [];
185
- lines.push("---");
186
- lines.push("title: Specs Index");
187
- lines.push("summary: Additional project-level specs beyond design.md.");
188
- lines.push("kind: generated-index");
189
- lines.push("index_type: specs");
190
- lines.push("source_dir: .rsp/specs");
191
- lines.push(`entry_count: ${specFiles.length}`);
192
- lines.push("---");
193
- lines.push("");
194
- lines.push("# Specs Index");
195
- lines.push("");
196
- lines.push("_Additional project-level specs beyond `design.md`._");
197
- lines.push("");
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("| File | Title | Summary |");
202
- lines.push("|------|-------|---------|");
203
- for (const fp of specFiles) {
204
- const rel = normalizeLogicalPath(relative(specsDir, fp));
205
- const content = await readFile2(fp, "utf-8");
206
- const fm = parseFrontmatter(content);
207
- const fmTitle = fm && typeof fm.title === "string" ? fm.title : "";
208
- const title = fmTitle || extractTitle(content) || rel.replace(/\.md$/, "");
209
- const summary = extractSummary(content, fm?.summary);
210
- lines.push(`| ${escapeCell(rel)} | ${escapeCell(title)} | ${escapeCell(summary || "\u2014")} |`);
211
- }
212
- }
213
- const indexPath = join(specsDir, "INDEX.md");
214
- const nextContent = lines.join("\n");
215
- const existingContent = existsSync(indexPath) ? await readFile2(indexPath, "utf-8") : null;
216
- const changed = existingContent !== nextContent;
217
- if (changed)
218
- await writeManagedFile(indexPath, nextContent, "Specs index");
219
- if (!quiet)
220
- console.log(` ${pc.green("INDEX.md updated:")} ${specFiles.length} spec file(s).
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 (!rawLine.startsWith("# "))
231
- continue;
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 lines) {
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 unlink2(path);
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 as existsSync3 } from "fs";
317
- import { mkdir as mkdir3, readFile as readFile4, rename, unlink as unlink3 } from "fs/promises";
318
- import { basename as basename3, join as join4 } from "path";
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 readFile4(srcPath, "utf-8");
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 = basename3(workRef.name);
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, join4(archiveSubdir, archiveName));
458
+ await rename(srcPath, join3(archiveSubdir, archiveName));
450
459
  const postArchiveWarnings = [];
451
460
  let focusCleared = false;
452
- if (existsSync3(focusEntry)) {
461
+ if (existsSync(focusEntry)) {
453
462
  try {
454
- await unlink3(focusEntry);
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 (existsSync3(".git")) {
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 (!existsSync3(join4(archiveSubdir, initialName)))
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 (!existsSync3(join4(archiveSubdir, candidate)))
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 readFile5 } from "fs/promises";
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 readFile5(entryPath, "utf-8")).trim();
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 readFile5(fp, "utf-8");
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
- if (!new RegExp(`^## ${section}$`, "m").test(content)) {
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
- const headingLine = content.split("\n").map((line) => line.trim()).find((line) => line.startsWith("# Change:"));
756
- if (headingLine) {
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 "${heading}" differs from change name`
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 |path|directory|module|subsystem|command|scenario|constraint|requirement|question|uncertainty|implementation)[^>\n]*>/i;
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 existsSync4 } from "fs";
912
- import { mkdir as mkdir4, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
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 = existsSync4(changePath);
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 unlink4(changePath);
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 existsSync6 } from "fs";
972
+ import { existsSync as existsSync4 } from "fs";
967
973
  import { readdir, readFile as readFile7, stat } from "fs/promises";
968
- import { basename as basename4, join as join6, relative as relative3 } from "path";
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 existsSync5 } from "fs";
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 (!existsSync5(RSP_DIR)) {
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 (existsSync5(OBSOLETE_RSP_RULES_PATH)) {
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
- const archiveIndexChanged = await buildArchiveIndex({ acquireLock: false, quiet: options.quiet });
1082
- if (specsIndexChanged || archiveIndexChanged) {
1083
- actions.push("generated indexes rebuilt");
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 && existsSync6(RSP_DIR)) {
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
- const specsIndexPath = join6(RSP_DIR, "specs", "INDEX.md");
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", existsSync6(designPath), "Run: rsp init");
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 checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "specs", "specs/INDEX.md has generated-index metadata", "Run: rsp update");
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 && existsSync6(RSP_DIR)) {
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 = existsSync6(OBSOLETE_RSP_RULES_PATH);
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: existsSync6(RSP_DIR) ? "Run: rsp update" : "Run: rsp init"
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 checkGeneratedIndex(checks, reportRuntime, path, expectedIndexType, label, fixHint) {
1301
- const indexFile = inspectManagedFile(path, label, { allowMissing: true });
1302
- if (indexFile.issue) {
1303
- checks.push({ status: "issue", label, message: indexFile.issue.message, hint: fixHint });
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
- if (!indexFile.exists)
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: "generated_index_frontmatter_parse_failed",
1330
- operation: "parseFrontmatter",
1331
- path,
1332
- message: toErrorMessage(error)
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
- checks.push({ status: "issue", label, hint: fixHint });
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(relative3(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename4(fp)) === false);
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 || !existsSync6(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(relative3(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
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 existsSync7 } from "fs";
1559
- import { mkdir as mkdir6, unlink as unlink5 } from "fs/promises";
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 (!existsSync7(focusEntry))
1610
+ if (!existsSync5(focusEntry))
1593
1611
  throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
1594
- await unlink5(focusEntry);
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
- if (await hasArchivedGroupBrief(name))
1633
- throw new ChangeGroupError(`archived Change Group cannot be reopened: ${name}`);
1634
- const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [name], "Change Group path");
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(name, goal), { flag: "wx" });
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: ${name}`);
1659
+ throw new ChangeGroupError(`Change Group already exists: ${groupName}`);
1641
1660
  throw error;
1642
1661
  }
1643
- console.log(` ${pc.green("Created Change Group:")} ${name}`);
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 ${name}/<change>
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 group = inspection.groups.find((candidate) => candidate.name === name);
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: ${name}`);
1671
- const reasons = collectCloseReasons(group, inspection.diagnostics.filter((diagnostic) => diagnostic.change === name && diagnostic.severity === "error").map((diagnostic) => diagnostic.message));
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 "${name}" is not ready to close: ${reasons.join("; ")}`);
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
- try {
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 (input.workRef) {
1754
+ if (validation.workRef) {
1742
1755
  try {
1743
- const selected = selectArchiveHistoryRecord(inspection.records, { workRef: input.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: input.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
- if (!WORK_REF_RE.test(input.workRef) || input.workRef.endsWith("/brief") || input.workRef.endsWith("/00-brief"))
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
- if (input.limit !== void 0 || input.since !== void 0 || input.until !== void 0 || input.kind !== void 0 || input.group !== void 0)
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
- if (input.group !== void 0 && !GROUP_RE.test(input.group))
1819
- return invalid("invalid_history_group", "--group must be one lowercase kebab-case Group name");
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: input.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 existsSync8 } from "fs";
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 may authorize an eligible local commit but never push or publication.
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 = !existsSync8(RSP_DIR);
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 (existsSync8(CONFIG_PATH)) {
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
- const createdArchivesIndex = await ensureFile(join8(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
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
- if (createdSpecsIndex || createdDesign)
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 readFile10 } from "fs/promises";
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 readFile10(srcPath, "utf-8");
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 rebuild specs index"
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: "Preview archive readiness without moving the change",
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
- await archiveChange(args.name, { dryRun: Boolean(args["dry-run"]) });
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 structure after upgrade (fallback protocol, AGENTS, indices)"
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-UWUX3HM7.mjs");
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-N7WLWGTI.mjs");
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,