@oevery/rsp 3.1.0-beta.2 → 3.1.0-beta.3

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-NX55NIC3.mjs";
8
8
  import {
9
9
  ArchiveHistoryError,
10
10
  GROUP_BRIEF_FILENAME,
@@ -29,7 +29,7 @@ import {
29
29
  resolveFocusMarkerPath,
30
30
  resolveWorkRef,
31
31
  selectArchiveHistoryRecord
32
- } from "./chunk-EALHA3ZS.mjs";
32
+ } from "./chunk-7KKAUBKN.mjs";
33
33
  import {
34
34
  ARCHIVES_DIR,
35
35
  CHANGES_DIR,
@@ -53,7 +53,6 @@ import {
53
53
  emitJson,
54
54
  ensureDecisionRecordsDirectory,
55
55
  ensureManagedFile,
56
- extractSection,
57
56
  generateChangeContent,
58
57
  generateDesignContent,
59
58
  generateSpecContent,
@@ -92,7 +91,7 @@ import { fileURLToPath } from "url";
92
91
  import { defineCommand, runMain } from "citty";
93
92
 
94
93
  // src/commands/add-spec.ts
95
- import { mkdir as mkdir2, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
94
+ import { mkdir as mkdir2, unlink as unlink3, writeFile as writeFile2 } from "fs/promises";
96
95
  import { join as join2 } from "path";
97
96
 
98
97
  // src/core/lock.ts
@@ -157,12 +156,51 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
157
156
  }
158
157
 
159
158
  // 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 } = {}) {
159
+ import { readFile as readFile2, unlink as unlink2 } from "fs/promises";
160
+ import { basename, dirname, isAbsolute, join, relative, sep } from "path";
161
+ var SPECS_INDEX_FILENAME = "00-index.md";
162
+ var LEGACY_SPECS_INDEX_FILENAME = "INDEX.md";
163
+ async function buildSpecsIndex(options = {}) {
164
+ const { acquireLock = true, quiet = false, affectedDirectory } = options;
164
165
  if (acquireLock)
165
- return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet }));
166
+ return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet, affectedDirectory }));
167
+ const plan = await planSpecsIndexes(affectedDirectory);
168
+ for (const write of plan.writes)
169
+ await writeManagedFile(write.path, write.content, "Specs index");
170
+ for (const path of plan.removals)
171
+ await unlink2(path);
172
+ const changed = plan.writes.length > 0 || plan.removals.length > 0;
173
+ if (!quiet) {
174
+ if (changed)
175
+ console.log(` ${pc.green("Specs indexes updated:")} ${plan.writes.length} written, ${plan.removals.length} removed.
176
+ `);
177
+ else
178
+ console.log(` ${pc.dim("Specs indexes already up to date.")}
179
+ `);
180
+ }
181
+ return changed;
182
+ }
183
+ async function inspectSpecsIndexHealth() {
184
+ try {
185
+ const plan = await planSpecsIndexes();
186
+ return {
187
+ ok: plan.writes.length === 0 && plan.removals.length === 0,
188
+ expectedPaths: plan.expectedPaths.map(toProjectPath),
189
+ stalePaths: plan.writes.map((write) => toProjectPath(write.path)),
190
+ obsoletePaths: plan.removals.map(toProjectPath),
191
+ issues: []
192
+ };
193
+ } catch (error) {
194
+ return {
195
+ ok: false,
196
+ expectedPaths: [],
197
+ stalePaths: [],
198
+ obsoletePaths: [],
199
+ issues: [error instanceof Error ? error.message : String(error)]
200
+ };
201
+ }
202
+ }
203
+ async function planSpecsIndexes(affectedDirectory) {
166
204
  const specsDir = join(RSP_DIR, "specs");
167
205
  const configInspection = await inspectRspConfig();
168
206
  if (configInspection.issues.length > 0)
@@ -171,91 +209,154 @@ async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
171
209
  const inspection = await inspectManagedFileTree(specsDir, "Specs", { allowMissing: true });
172
210
  if (inspection.issues.length > 0)
173
211
  throw inspection.issues[0];
174
- if (!inspection.rootExists) {
175
- if (!quiet)
176
- console.log(` ${pc.dim("No specs directory.")}
177
- `);
178
- return false;
212
+ if (!inspection.rootExists)
213
+ return { writes: [], removals: [], expectedPaths: [] };
214
+ const excludedDirectories = [DEFAULT_DECISION_RECORDS_PATH, decisionRecordsPath].filter((path) => isInside(specsDir, path));
215
+ const isExcluded = (path) => excludedDirectories.some((directory) => isInside(directory, path));
216
+ const managedIndexFiles = inspection.files.filter((path) => !isExcluded(path)).filter((path) => basename(path) === SPECS_INDEX_FILENAME || path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME));
217
+ const recognizedIndexes = /* @__PURE__ */ new Set();
218
+ for (const path of managedIndexFiles) {
219
+ let metadata;
220
+ try {
221
+ metadata = parseFrontmatter(await readFile2(path, "utf-8"));
222
+ } catch (error) {
223
+ throw new Error(`unable to inspect reserved Specs index ${toProjectPath(path)}: ${error instanceof Error ? error.message : String(error)}`);
224
+ }
225
+ const expectedSourceDirectory = path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME) ? toProjectPath(specsDir) : toProjectPath(dirname(path));
226
+ if (metadata?.kind !== "generated-index" || metadata.index_type !== "specs" || metadata.source_dir !== expectedSourceDirectory) {
227
+ throw new Error(`reserved Specs index is not recognized generated content: ${toProjectPath(path)}`);
228
+ }
229
+ recognizedIndexes.add(path);
230
+ }
231
+ 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();
232
+ const directories = /* @__PURE__ */ new Set([specsDir]);
233
+ for (const file of specFiles) {
234
+ let current = dirname(file);
235
+ while (isInside(specsDir, current)) {
236
+ directories.add(current);
237
+ if (current === specsDir)
238
+ break;
239
+ current = dirname(current);
240
+ }
241
+ }
242
+ const orderedDirectories = [...directories].sort();
243
+ const expectedPaths = orderedDirectories.map((directory) => join(directory, SPECS_INDEX_FILENAME));
244
+ const expectedSet = new Set(expectedPaths);
245
+ const writableDirectories = affectedDirectory ? new Set(ancestorDirectories(specsDir, affectedDirectory)) : directories;
246
+ const writes = [];
247
+ for (const directory of orderedDirectories) {
248
+ if (!writableDirectories.has(directory))
249
+ continue;
250
+ const directFiles = specFiles.filter((path2) => dirname(path2) === directory);
251
+ const directDirectories = orderedDirectories.filter((path2) => path2 !== directory && dirname(path2) === directory);
252
+ const content = await renderSpecsIndex(specsDir, directory, directFiles, directDirectories);
253
+ const path = join(directory, SPECS_INDEX_FILENAME);
254
+ const existingContent = recognizedIndexes.has(path) ? await readFile2(path, "utf-8") : null;
255
+ if (existingContent !== content)
256
+ writes.push({ path, content });
257
+ }
258
+ const removals = [...recognizedIndexes].filter((path) => path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME) || !expectedSet.has(path) && !affectedDirectory).sort();
259
+ return { writes, removals, expectedPaths };
260
+ }
261
+ async function renderSpecsIndex(specsDir, directory, files, directories) {
262
+ const sourceDir = toProjectPath(directory);
263
+ const scope = normalizeLogicalPath(relative(specsDir, directory));
264
+ const title = scope === "" ? "Specs Index" : `Specs: ${scope}`;
265
+ const entries = [];
266
+ for (const childDirectory of directories) {
267
+ const name = basename(childDirectory);
268
+ entries.push({
269
+ sortKey: `${name}/`,
270
+ line: `| [${escapeLabel(`${name}/`)}](${encodeLink(`${name}/${SPECS_INDEX_FILENAME}`)}) | directory | ${escapeCell(name)} | \u2014 |`
271
+ });
272
+ }
273
+ for (const path of files) {
274
+ const name = basename(path);
275
+ const content = await readFile2(path, "utf-8");
276
+ const metadata = parseFrontmatter(content);
277
+ const metadataTitle = metadata && typeof metadata.title === "string" ? metadata.title : "";
278
+ const entryTitle = metadataTitle || extractTitle(content) || name.replace(/\.md$/, "");
279
+ const summary = extractSummary(content, metadata?.summary);
280
+ entries.push({
281
+ sortKey: name,
282
+ line: `| [${escapeLabel(name)}](${encodeLink(name)}) | spec | ${escapeCell(entryTitle)} | ${escapeCell(summary || "\u2014")} |`
283
+ });
179
284
  }
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._");
285
+ entries.sort((left, right) => left.sortKey < right.sortKey ? -1 : left.sortKey > right.sortKey ? 1 : 0);
286
+ const lines = [
287
+ "---",
288
+ `title: ${JSON.stringify(title)}`,
289
+ `summary: ${JSON.stringify(`Direct Specs and child Spec directories under ${sourceDir}.`)}`,
290
+ "kind: generated-index",
291
+ "index_type: specs",
292
+ `source_dir: ${JSON.stringify(sourceDir)}`,
293
+ `entry_count: ${entries.length}`,
294
+ "---",
295
+ "",
296
+ `# ${title}`,
297
+ "",
298
+ "_Generated direct-child navigation. Edit the linked Specs, then run `rsp update`._",
299
+ ""
300
+ ];
301
+ if (entries.length === 0) {
302
+ lines.push("_No Specs or child Spec directories yet._");
200
303
  } 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);
304
+ lines.push("| Entry | Kind | Title | Summary |");
305
+ lines.push("|-------|------|-------|---------|");
306
+ lines.push(...entries.map((entry) => entry.line));
307
+ }
308
+ return lines.join("\n");
309
+ }
310
+ function ancestorDirectories(root, directory) {
311
+ if (!isInside(root, directory))
312
+ throw new Error(`affected Spec directory must stay inside ${toProjectPath(root)}: ${directory}`);
313
+ const result = [];
314
+ let current = directory;
315
+ while (true) {
316
+ result.push(current);
317
+ if (current === root)
318
+ return result;
319
+ current = dirname(current);
320
+ }
321
+ }
322
+ function isInside(directory, path) {
323
+ const rel = relative(directory, path);
226
324
  return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
227
325
  }
326
+ function toProjectPath(path) {
327
+ return normalizeLogicalPath(relative(".", path));
328
+ }
228
329
  function extractTitle(content) {
229
330
  for (const rawLine of content.split("\n")) {
230
- if (!rawLine.startsWith("# "))
231
- continue;
232
- return rawLine.slice(2).trim();
331
+ if (rawLine.startsWith("# "))
332
+ return rawLine.slice(2).trim();
233
333
  }
234
334
  return "";
235
335
  }
236
336
  function extractSummary(content, frontmatterSummary) {
237
337
  if (typeof frontmatterSummary === "string" && frontmatterSummary.trim() !== "")
238
338
  return frontmatterSummary.trim();
239
- const lines = content.split("\n");
240
339
  let inFrontmatter = false;
241
- for (const rawLine of lines) {
340
+ for (const rawLine of content.split("\n")) {
242
341
  const line = rawLine.trim();
243
342
  if (line === "---" || line === "...") {
244
343
  inFrontmatter = !inFrontmatter;
245
344
  continue;
246
345
  }
247
- if (inFrontmatter || line === "")
248
- continue;
249
- if (line.startsWith("#"))
250
- continue;
251
- if (line.startsWith("##"))
346
+ if (inFrontmatter || line === "" || line.startsWith("#"))
252
347
  continue;
253
348
  return line.replace(/^[-*]\s*/, "").trim();
254
349
  }
255
350
  return "";
256
351
  }
257
352
  function escapeCell(value) {
258
- return value.replace(/\|/g, "\\|");
353
+ return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
354
+ }
355
+ function escapeLabel(value) {
356
+ return value.replace(/([\\[\]])/g, "\\$1");
357
+ }
358
+ function encodeLink(value) {
359
+ return encodeURI(value).replace(/\(/g, "%28").replace(/\)/g, "%29");
259
360
  }
260
361
 
261
362
  // src/commands/add-spec.ts
@@ -268,6 +369,10 @@ async function addSpec(name, projectName = "project") {
268
369
  console.error(` ${pc.red("Error:")} .rsp/specs/decisions/ is reserved for Decision Records`);
269
370
  process.exit(1);
270
371
  }
372
+ if (["index", "00-index"].includes(name.split("/").at(-1) || "")) {
373
+ console.error(` ${pc.red("Error:")} spec name "index" is reserved for generated local navigation`);
374
+ process.exit(1);
375
+ }
271
376
  guardRspInitialized();
272
377
  try {
273
378
  const configInspection = await inspectRspConfig();
@@ -294,11 +399,11 @@ async function addSpec(name, projectName = "project") {
294
399
  try {
295
400
  await writeFile2(path, content, { flag: "wx" });
296
401
  created = true;
297
- await buildSpecsIndex({ acquireLock: false });
402
+ await buildSpecsIndex({ acquireLock: false, affectedDirectory: parent });
298
403
  } catch (error) {
299
404
  if (created) {
300
405
  try {
301
- await unlink2(path);
406
+ await unlink3(path);
302
407
  await cleanupEmptyParentDirs(path, specsDir);
303
408
  } catch {
304
409
  }
@@ -313,126 +418,22 @@ async function addSpec(name, projectName = "project") {
313
418
  }
314
419
 
315
420
  // 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 = {}) {
421
+ import { existsSync } from "fs";
422
+ import { mkdir as mkdir3, readFile as readFile3, rename, unlink as unlink4 } from "fs/promises";
423
+ import { basename as basename2, join as join3 } from "path";
424
+ async function archiveChange(name) {
402
425
  if (!name) {
403
426
  console.error(` ${pc.red("Usage:")} rsp archive <name>`);
404
427
  process.exit(1);
405
428
  }
406
429
  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
430
  try {
430
431
  return await withRspLock("archive-change", async () => {
431
432
  const workRef = await resolveExecutableChange(name, { mustExist: true });
432
433
  const srcPath = workRef.path;
433
434
  const archiveSubdir = resolveArchiveDirectory(workRef);
434
435
  const focusEntry = resolveFocusMarkerPath(workRef);
435
- const content = await readFile4(srcPath, "utf-8");
436
+ const content = await readFile3(srcPath, "utf-8");
436
437
  const dependencyInspection = await inspectChangeDependencies();
437
438
  const checklist = collectArchiveChecklist(content, {
438
439
  activeBlockers: dependencyInspection.activeBlockers.get(name)
@@ -443,15 +444,15 @@ async function archiveChange(name, options = {}) {
443
444
  console.log(` ${pc.dim("Archive will continue. Review the warnings above before treating this work as fully closed.")}
444
445
  `);
445
446
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
446
- const base = basename3(workRef.name);
447
+ const base = basename2(workRef.name);
447
448
  await mkdir3(archiveSubdir, { recursive: true });
448
449
  const archiveName = resolveArchiveName(archiveSubdir, date, base);
449
- await rename(srcPath, join4(archiveSubdir, archiveName));
450
+ await rename(srcPath, join3(archiveSubdir, archiveName));
450
451
  const postArchiveWarnings = [];
451
452
  let focusCleared = false;
452
- if (existsSync3(focusEntry)) {
453
+ if (existsSync(focusEntry)) {
453
454
  try {
454
- await unlink3(focusEntry);
455
+ await unlink4(focusEntry);
455
456
  await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
456
457
  focusCleared = true;
457
458
  } catch (error) {
@@ -467,17 +468,12 @@ async function archiveChange(name, options = {}) {
467
468
  ` : "";
468
469
  console.log(` ${pc.green("Archived:")} ${archiveName}
469
470
  ${clearedMsg}`);
470
- try {
471
- await buildArchiveIndex({ acquireLock: false });
472
- } catch (error) {
473
- postArchiveWarnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
474
- }
475
471
  for (const warning of postArchiveWarnings)
476
472
  console.log(` ${pc.yellow("\u26A0")} ${warning}`);
477
473
  if (postArchiveWarnings.length > 0)
478
474
  console.log(` ${pc.dim("Archive completed, but follow-up cleanup was only partially successful.")}
479
475
  `);
480
- if (existsSync3(".git")) {
476
+ if (existsSync(".git")) {
481
477
  console.log(` ${pc.cyan("Git delivery:")}
482
478
  `);
483
479
  console.log(` git status --short`);
@@ -493,28 +489,14 @@ ${clearedMsg}`);
493
489
  throw error;
494
490
  }
495
491
  }
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
492
  function resolveArchiveName(archiveSubdir, date, base) {
511
493
  const initialName = `${date}_${base}.md`;
512
- if (!existsSync3(join4(archiveSubdir, initialName)))
494
+ if (!existsSync(join3(archiveSubdir, initialName)))
513
495
  return initialName;
514
496
  const MAX_SUFFIX = 9999;
515
497
  for (let index = 2; index <= MAX_SUFFIX; index++) {
516
498
  const candidate = `${date}_${base}-${index}.md`;
517
- if (!existsSync3(join4(archiveSubdir, candidate)))
499
+ if (!existsSync(join3(archiveSubdir, candidate)))
518
500
  return candidate;
519
501
  }
520
502
  throw new WorkRefError(
@@ -525,7 +507,7 @@ function resolveArchiveName(archiveSubdir, date, base) {
525
507
  }
526
508
 
527
509
  // src/commands/check.ts
528
- import { readFile as readFile5 } from "fs/promises";
510
+ import { readFile as readFile4 } from "fs/promises";
529
511
  async function runCheck(options = {}) {
530
512
  const diagnostics = [];
531
513
  const runtime = [];
@@ -568,7 +550,7 @@ async function runCheck(options = {}) {
568
550
  const entryContent = marker.name;
569
551
  let markerContent = "";
570
552
  try {
571
- markerContent = (await readFile5(entryPath, "utf-8")).trim();
553
+ markerContent = (await readFile4(entryPath, "utf-8")).trim();
572
554
  } catch (error) {
573
555
  addDiagnostic({
574
556
  severity: "error",
@@ -679,7 +661,7 @@ async function runCheck(options = {}) {
679
661
  const { name, path: fp } = ref;
680
662
  let content;
681
663
  try {
682
- content = await readFile5(fp, "utf-8");
664
+ content = await readFile4(fp, "utf-8");
683
665
  } catch (error) {
684
666
  addDiagnostic({
685
667
  severity: "error",
@@ -908,8 +890,8 @@ function collectMatchingLines(content, predicate) {
908
890
  }
909
891
 
910
892
  // 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";
893
+ import { existsSync as existsSync2 } from "fs";
894
+ import { mkdir as mkdir4, unlink as unlink5, writeFile as writeFile3 } from "fs/promises";
913
895
  import { dirname as dirname2 } from "path";
914
896
  async function createChange(name, summary = "", kind, options = {}) {
915
897
  if (!name) {
@@ -921,7 +903,7 @@ async function createChange(name, summary = "", kind, options = {}) {
921
903
  return await withRspLock("create-change", async () => {
922
904
  const workRef = await resolveExecutableChange(name);
923
905
  const changePath = workRef.path;
924
- const existed = existsSync4(changePath);
906
+ const existed = existsSync2(changePath);
925
907
  if (!existed) {
926
908
  const focusEntry = resolveFocusMarkerPath(workRef);
927
909
  const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
@@ -936,7 +918,7 @@ async function createChange(name, summary = "", kind, options = {}) {
936
918
  } catch (error) {
937
919
  if (changeCreated) {
938
920
  try {
939
- await unlink4(changePath);
921
+ await unlink5(changePath);
940
922
  await cleanupEmptyParentDirs(changePath, CHANGES_DIR);
941
923
  } catch {
942
924
  }
@@ -963,17 +945,39 @@ async function createChange(name, summary = "", kind, options = {}) {
963
945
  }
964
946
 
965
947
  // src/commands/doctor.ts
966
- import { existsSync as existsSync6 } from "fs";
948
+ import { existsSync as existsSync4 } from "fs";
967
949
  import { readdir, readFile as readFile7, stat } from "fs/promises";
968
- import { basename as basename4, join as join6, relative as relative3 } from "path";
950
+ import { basename as basename3, join as join6, relative as relative2 } from "path";
969
951
 
970
952
  // src/commands/update.ts
971
- import { existsSync as existsSync5 } from "fs";
953
+ import { existsSync as existsSync3 } from "fs";
972
954
  import { mkdir as mkdir5, readFile as readFile6, rm, rmdir } from "fs/promises";
973
955
  import { join as join5 } from "path";
956
+
957
+ // src/commands/archive-index-migration.ts
958
+ import { readFile as readFile5, unlink as unlink6 } from "fs/promises";
959
+ import { join as join4 } from "path";
960
+ async function removeLegacyArchiveIndex() {
961
+ const path = join4(ARCHIVES_DIR, "INDEX.md");
962
+ const inspection = inspectManagedFile(path, "legacy archive index", { allowMissing: true });
963
+ if (!inspection.exists || inspection.issue)
964
+ return false;
965
+ let frontmatter;
966
+ try {
967
+ frontmatter = parseFrontmatter(await readFile5(path, "utf-8"));
968
+ } catch {
969
+ return false;
970
+ }
971
+ if (frontmatter?.kind !== "generated-index" || frontmatter.index_type !== "archives")
972
+ return false;
973
+ await unlink6(path);
974
+ return true;
975
+ }
976
+
977
+ // src/commands/update.ts
974
978
  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
979
  async function updateProject(options = {}) {
976
- if (!existsSync5(RSP_DIR)) {
980
+ if (!existsSync3(RSP_DIR)) {
977
981
  console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
978
982
  console.error(` ${pc.dim("Run: rsp init")}`);
979
983
  process.exit(1);
@@ -1004,6 +1008,9 @@ async function updateProject(options = {}) {
1004
1008
  console.log(` ${pc.green("\u2713")} changes/ directory restored`);
1005
1009
  updated = true;
1006
1010
  }
1011
+ const archivesInspection = inspectManagedDirectory(join5(RSP_DIR, "archives"), "archive root", { allowMissing: true });
1012
+ if (archivesInspection.issue)
1013
+ throw archivesInspection.issue;
1007
1014
  const bundledRules = await readFile6(join5(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
1008
1015
  const rulesInspection = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
1009
1016
  if (rulesInspection.issue)
@@ -1016,7 +1023,7 @@ async function updateProject(options = {}) {
1016
1023
  console.log(` ${pc.green("\u2713")} rsp-rules.md updated`);
1017
1024
  updated = true;
1018
1025
  }
1019
- if (existsSync5(OBSOLETE_RSP_RULES_PATH)) {
1026
+ if (existsSync3(OBSOLETE_RSP_RULES_PATH)) {
1020
1027
  await rm(OBSOLETE_RSP_RULES_PATH);
1021
1028
  actions.push("obsolete rules/rsp-rules.md removed");
1022
1029
  if (!options.quiet)
@@ -1078,9 +1085,14 @@ async function updateProject(options = {}) {
1078
1085
  updated = true;
1079
1086
  }
1080
1087
  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");
1088
+ if (specsIndexChanged) {
1089
+ actions.push("generated Specs indexes reconciled");
1090
+ updated = true;
1091
+ }
1092
+ if (await removeLegacyArchiveIndex()) {
1093
+ actions.push("legacy Archive Index removed");
1094
+ if (!options.quiet)
1095
+ console.log(` ${pc.green("\u2713")} legacy Archive Index removed`);
1084
1096
  updated = true;
1085
1097
  }
1086
1098
  if (!options.quiet && inspection.diagnostics.length > 0) {
@@ -1116,7 +1128,7 @@ async function updateProject(options = {}) {
1116
1128
  async function runDoctor(options = {}) {
1117
1129
  const fixed = [];
1118
1130
  let repairIssue = null;
1119
- if (options.fix && existsSync6(RSP_DIR)) {
1131
+ if (options.fix && existsSync4(RSP_DIR)) {
1120
1132
  try {
1121
1133
  const update = await updateProject({ quiet: Boolean(options.json) });
1122
1134
  fixed.push(...update.actions);
@@ -1136,17 +1148,12 @@ async function runDoctor(options = {}) {
1136
1148
  const runtime = [];
1137
1149
  const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
1138
1150
  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");
1151
+ reportCheck(checks, ".rsp exists", existsSync4(RSP_DIR), "Run: rsp init");
1142
1152
  reportFallbackProtocol(checks);
1143
1153
  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");
1154
+ reportCheck(checks, "specs/design.md exists", existsSync4(designPath), "Run: rsp init");
1147
1155
  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");
1156
+ await checkSpecsIndexes(checks, reportRuntime);
1150
1157
  await checkArchiveNaming(checks);
1151
1158
  const decisionRecordsConfigValid = await checkConfigSemantics(checks, reportRuntime);
1152
1159
  if (decisionRecordsConfigValid) {
@@ -1174,7 +1181,7 @@ async function runDoctor(options = {}) {
1174
1181
  if (fixed.length > 0) {
1175
1182
  console.log(` ${pc.green("Fixed:")} ${fixed.join(", ")}`);
1176
1183
  console.log();
1177
- } else if (options.fix && !repairIssue && existsSync6(RSP_DIR)) {
1184
+ } else if (options.fix && !repairIssue && existsSync4(RSP_DIR)) {
1178
1185
  console.log(` ${pc.dim("No safe fixes needed.")}`);
1179
1186
  console.log();
1180
1187
  }
@@ -1192,7 +1199,7 @@ async function runDoctor(options = {}) {
1192
1199
  }
1193
1200
  function reportFallbackProtocol(checks) {
1194
1201
  const canonical = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
1195
- const obsoleteExists = existsSync6(OBSOLETE_RSP_RULES_PATH);
1202
+ const obsoleteExists = existsSync4(OBSOLETE_RSP_RULES_PATH);
1196
1203
  if (canonical.issue) {
1197
1204
  checks.push({
1198
1205
  status: "issue",
@@ -1206,7 +1213,7 @@ function reportFallbackProtocol(checks) {
1206
1213
  checks.push({
1207
1214
  status: "issue",
1208
1215
  label: "rsp-rules.md exists",
1209
- hint: existsSync6(RSP_DIR) ? "Run: rsp update" : "Run: rsp init"
1216
+ hint: existsSync4(RSP_DIR) ? "Run: rsp update" : "Run: rsp init"
1210
1217
  });
1211
1218
  }
1212
1219
  if (obsoleteExists) {
@@ -1297,44 +1304,31 @@ async function checkAgents(checks, reportRuntime) {
1297
1304
  }
1298
1305
  checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
1299
1306
  }
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 });
1304
- return;
1305
- }
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 });
1307
+ async function checkSpecsIndexes(checks, reportRuntime) {
1308
+ const health = await inspectSpecsIndexHealth();
1309
+ if (health.ok) {
1310
+ checks.push({ status: "ok", label: "hierarchical Specs indexes are current generated files" });
1319
1311
  return;
1320
1312
  }
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) {
1313
+ for (const message of health.issues) {
1328
1314
  reportRuntime({
1329
- code: "generated_index_frontmatter_parse_failed",
1330
- operation: "parseFrontmatter",
1331
- path,
1332
- message: toErrorMessage(error)
1315
+ code: "specs_index_inspection_failed",
1316
+ operation: "inspectSpecsIndexHealth",
1317
+ path: ".rsp/specs",
1318
+ message
1333
1319
  });
1334
- checks.push({ status: "issue", label, message: `${label} could not parse metadata`, hint: fixHint });
1335
- return;
1336
1320
  }
1337
- checks.push({ status: "issue", label, hint: fixHint });
1321
+ const details = [
1322
+ ...health.issues,
1323
+ health.stalePaths.length > 0 ? `missing or stale: ${health.stalePaths.join(", ")}` : "",
1324
+ health.obsoletePaths.length > 0 ? `recognized obsolete indexes: ${health.obsoletePaths.join(", ")}` : ""
1325
+ ].filter(Boolean);
1326
+ checks.push({
1327
+ status: "issue",
1328
+ label: "hierarchical Specs indexes are current generated files",
1329
+ message: details.join("; "),
1330
+ hint: "Run: rsp update"
1331
+ });
1338
1332
  }
1339
1333
  async function checkArchiveNaming(checks) {
1340
1334
  const archivesDir = join6(RSP_DIR, "archives");
@@ -1349,7 +1343,7 @@ async function checkArchiveNaming(checks) {
1349
1343
  return;
1350
1344
  }
1351
1345
  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);
1346
+ const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(relative2(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename3(fp)) === false);
1353
1347
  if (invalidFiles.length === 0) {
1354
1348
  checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
1355
1349
  return;
@@ -1437,7 +1431,7 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
1437
1431
  }
1438
1432
  async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
1439
1433
  const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
1440
- if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync6(DEFAULT_DECISION_RECORDS_PATH))
1434
+ if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync4(DEFAULT_DECISION_RECORDS_PATH))
1441
1435
  return;
1442
1436
  let inspectionFailed = false;
1443
1437
  const inactiveRecords = (await walkMarkdownFiles(DEFAULT_DECISION_RECORDS_PATH, {
@@ -1445,7 +1439,7 @@ async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
1445
1439
  inspectionFailed = true;
1446
1440
  reportRuntime(diagnostic);
1447
1441
  }
1448
- })).map((path) => normalizeLogicalPath(relative3(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
1442
+ })).map((path) => normalizeLogicalPath(relative2(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
1449
1443
  if (inspectionFailed) {
1450
1444
  checks.push({
1451
1445
  status: "issue",
@@ -1555,8 +1549,8 @@ async function checkActiveChangeConsistency(checks) {
1555
1549
  }
1556
1550
 
1557
1551
  // src/commands/focus.ts
1558
- import { existsSync as existsSync7 } from "fs";
1559
- import { mkdir as mkdir6, unlink as unlink5 } from "fs/promises";
1552
+ import { existsSync as existsSync5 } from "fs";
1553
+ import { mkdir as mkdir6, unlink as unlink7 } from "fs/promises";
1560
1554
  import { dirname as dirname3 } from "path";
1561
1555
  async function focusChange(name) {
1562
1556
  if (!name) {
@@ -1589,9 +1583,9 @@ async function unfocusChange(name) {
1589
1583
  return await withRspLock("unfocus-change", async () => {
1590
1584
  const workRef = resolveWorkRef(name, { executable: false, mustExist: false });
1591
1585
  const focusEntry = resolveFocusMarkerPath(workRef);
1592
- if (!existsSync7(focusEntry))
1586
+ if (!existsSync5(focusEntry))
1593
1587
  throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
1594
- await unlink5(focusEntry);
1588
+ await unlink7(focusEntry);
1595
1589
  await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
1596
1590
  console.log(` ${pc.green("Unfocused:")} ${name}`);
1597
1591
  console.log(` ${pc.dim("focus.d cleared")} \u2192 ${name}`);
@@ -1682,11 +1676,6 @@ async function closeChangeGroup(name) {
1682
1676
  } catch (error) {
1683
1677
  warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
1684
1678
  }
1685
- try {
1686
- await buildArchiveIndex({ acquireLock: false });
1687
- } catch (error) {
1688
- warnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
1689
- }
1690
1679
  console.log(` ${pc.green("Closed Change Group:")} ${name}`);
1691
1680
  console.log(` ${pc.dim("Archived Group Brief:")} ${join7(archiveDirectory, archiveName)}
1692
1681
  `);
@@ -1780,7 +1769,8 @@ async function showHistory(input = {}, options = {}) {
1780
1769
  since: query.since ?? null,
1781
1770
  until: query.until ?? null,
1782
1771
  kind: query.kind ?? null,
1783
- group: query.group ?? null
1772
+ group: query.group ?? null,
1773
+ search: query.search ?? null
1784
1774
  },
1785
1775
  records: listed.records,
1786
1776
  summary: listed.summary,
@@ -1800,7 +1790,7 @@ function validateHistoryQuery(input) {
1800
1790
  if (input.workRef) {
1801
1791
  if (!WORK_REF_RE.test(input.workRef) || input.workRef.endsWith("/brief") || input.workRef.endsWith("/00-brief"))
1802
1792
  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)
1793
+ 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
1794
  return invalid("history_detail_filters_unsupported", "list filters cannot be combined with an exact history detail lookup");
1805
1795
  return { ok: true, query: {} };
1806
1796
  }
@@ -1817,6 +1807,8 @@ function validateHistoryQuery(input) {
1817
1807
  return invalid("invalid_history_kind", "--kind must be a non-empty exact historical kind");
1818
1808
  if (input.group !== void 0 && !GROUP_RE.test(input.group))
1819
1809
  return invalid("invalid_history_group", "--group must be one lowercase kebab-case Group name");
1810
+ if (input.search !== void 0 && input.search.trim() === "")
1811
+ return invalid("invalid_history_search", "--search must be non-empty");
1820
1812
  return {
1821
1813
  ok: true,
1822
1814
  query: {
@@ -1824,7 +1816,8 @@ function validateHistoryQuery(input) {
1824
1816
  since: input.since,
1825
1817
  until: input.until,
1826
1818
  kind: input.kind?.trim(),
1827
- group: input.group
1819
+ group: input.group,
1820
+ search: input.search?.trim()
1828
1821
  }
1829
1822
  };
1830
1823
  }
@@ -1916,7 +1909,7 @@ function printHistoryDetail(result) {
1916
1909
  }
1917
1910
 
1918
1911
  // src/commands/init.ts
1919
- import { existsSync as existsSync8 } from "fs";
1912
+ import { existsSync as existsSync6 } from "fs";
1920
1913
  import { mkdir as mkdir8, readFile as readFile8 } from "fs/promises";
1921
1914
  import { join as join8 } from "path";
1922
1915
  function generateConfigTemplate() {
@@ -1934,7 +1927,7 @@ ${fmtList(VALID_KINDS, 0)}
1934
1927
 
1935
1928
  # New projects default to automatic Manage routing with lifecycle-only closeout.
1936
1929
  # 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.
1930
+ # 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
1931
  manage:
1939
1932
  activation: auto
1940
1933
  closeout: lifecycle
@@ -1952,7 +1945,7 @@ function toTitle(projectName) {
1952
1945
  return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
1953
1946
  }
1954
1947
  async function initProject(args = {}) {
1955
- const isNew = !existsSync8(RSP_DIR);
1948
+ const isNew = !existsSync6(RSP_DIR);
1956
1949
  const projectName = await detectProjectName();
1957
1950
  const mode = args.agentsMode ?? "managed";
1958
1951
  if (mode !== "managed" && mode !== "print") {
@@ -1967,7 +1960,7 @@ async function initProject(args = {}) {
1967
1960
  const agentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
1968
1961
  if (agentsInspection.issue)
1969
1962
  throw agentsInspection.issue;
1970
- if (existsSync8(CONFIG_PATH)) {
1963
+ if (existsSync6(CONFIG_PATH)) {
1971
1964
  clearConfigCache();
1972
1965
  const existingConfig = await inspectRspConfig();
1973
1966
  if (existingConfig.issues.length > 0)
@@ -1997,12 +1990,9 @@ async function initProject(args = {}) {
1997
1990
  throw new Error(configInspection.issues.join("; "));
1998
1991
  const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
1999
1992
  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
1993
  const createdDesign = await ensureFile(join8(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
2003
1994
  created = createdDesign || created;
2004
- const createdArchivesIndex = await ensureFile(join8(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
2005
- created = createdArchivesIndex || created;
1995
+ created = await removeLegacyArchiveIndex() || created;
2006
1996
  created = await ensureFile(join8(CHANGES_DIR, ".gitkeep"), "") || created;
2007
1997
  created = await ensureFile(join8(FOCUS_DIR, ".gitkeep"), "") || created;
2008
1998
  created = await ensureFile(join8(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
@@ -2013,10 +2003,7 @@ async function initProject(args = {}) {
2013
2003
  created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
2014
2004
  created = await ensureFile(focusMarker, "") || created;
2015
2005
  }
2016
- if (createdSpecsIndex || createdDesign)
2017
- await buildSpecsIndex({ acquireLock: false });
2018
- if (createdArchivesIndex)
2019
- await buildArchiveIndex({ acquireLock: false });
2006
+ created = await buildSpecsIndex({ acquireLock: false }) || created;
2020
2007
  const title = toTitle(projectName);
2021
2008
  let agentsUpdated = false;
2022
2009
  if (!agentsInspection.exists) {
@@ -2192,8 +2179,126 @@ function formatArchiveReady(value) {
2192
2179
  }
2193
2180
  }
2194
2181
 
2182
+ // src/commands/reopen.ts
2183
+ import { existsSync as existsSync7 } from "fs";
2184
+ import { mkdir as mkdir9, readFile as readFile10, unlink as unlink8, writeFile as writeFile5 } from "fs/promises";
2185
+ import { dirname as dirname4 } from "path";
2186
+ async function reopenChange(name, options) {
2187
+ if (!name) {
2188
+ console.error(` ${pc.red("Usage:")} rsp reopen <name> --reason <text> [--from <archive-path>]`);
2189
+ process.exit(1);
2190
+ }
2191
+ const reason = options.reason?.trim();
2192
+ if (!reason || /[\r\n]/.test(reason)) {
2193
+ console.error(` ${pc.red("Error:")} --reason must be one non-empty line`);
2194
+ process.exit(1);
2195
+ }
2196
+ guardRspInitialized();
2197
+ try {
2198
+ await withRspLock("reopen-change", async () => {
2199
+ const inspection = await inspectArchiveHistory();
2200
+ if (!historyInspectionComplete(inspection)) {
2201
+ throw new ArchiveHistoryError(
2202
+ "archive_inspection_incomplete",
2203
+ "archive history inspection is incomplete; resolve the reported diagnostics before reopening work"
2204
+ );
2205
+ }
2206
+ const record = selectArchiveHistoryRecord(
2207
+ inspection.records,
2208
+ options.from ? { path: options.from } : { workRef: name }
2209
+ );
2210
+ if (record.workRef !== name) {
2211
+ throw new ArchiveHistoryError(
2212
+ "archive_identity_mismatch",
2213
+ `selected archive belongs to ${record.workRef}, not ${name}`
2214
+ );
2215
+ }
2216
+ let workRef;
2217
+ try {
2218
+ workRef = resolveWorkRef(name, { executable: true });
2219
+ } catch (error) {
2220
+ if (error instanceof WorkRefError && error.code === "group_brief_missing") {
2221
+ throw new ArchiveHistoryError(
2222
+ "archived_group_closed",
2223
+ `cannot reopen ${name} because its Change Group is closed; reopen the Group through a separately authorized lifecycle operation first`
2224
+ );
2225
+ }
2226
+ throw error;
2227
+ }
2228
+ if (existsSync7(workRef.path))
2229
+ throw new ArchiveHistoryError("open_change_exists", `open Change already exists: ${name}`);
2230
+ const focusEntry = resolveFocusMarkerPath(workRef);
2231
+ if (existsSync7(focusEntry))
2232
+ throw new ArchiveHistoryError("focus_marker_exists", `focus marker already exists for non-open Change: ${name}; run rsp doctor`);
2233
+ const archivedContent = await readFile10(record.sourcePath, "utf-8");
2234
+ const reopenedContent = appendChecklistItem(
2235
+ appendChecklistItem(archivedContent, "Tasks", `- [ ] Resolve reopened concern: ${reason}`),
2236
+ "Verify",
2237
+ `- [ ] Verify reopened concern: ${reason}`
2238
+ );
2239
+ await mkdir9(dirname4(workRef.path), { recursive: true });
2240
+ let changeCreated = false;
2241
+ try {
2242
+ await writeFile5(workRef.path, reopenedContent, { flag: "wx" });
2243
+ changeCreated = true;
2244
+ await mkdir9(FOCUS_DIR, { recursive: true });
2245
+ await mkdir9(dirname4(focusEntry), { recursive: true });
2246
+ await writeManagedFile(focusEntry, "", "focus marker");
2247
+ } catch (error) {
2248
+ if (changeCreated) {
2249
+ try {
2250
+ await unlink8(workRef.path);
2251
+ await cleanupEmptyParentDirs(workRef.path, CHANGES_DIR);
2252
+ } catch {
2253
+ }
2254
+ }
2255
+ throw error;
2256
+ }
2257
+ console.log(` ${pc.green("Reopened:")} ${name}`);
2258
+ console.log(` ${pc.dim("retained archive")} \u2192 ${record.path}`);
2259
+ console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
2260
+ console.log(` ${pc.cyan("Next:")} refine the reopened Task and Verify evidence, then resolve the concern
2261
+ `);
2262
+ if (existsSync7(".git")) {
2263
+ console.log(` ${pc.cyan("Git delivery:")}`);
2264
+ console.log(` git status --short`);
2265
+ console.log(` Inspect the complete reopen transition; stage and commit only with separate Git authority.`);
2266
+ console.log();
2267
+ }
2268
+ });
2269
+ } catch (error) {
2270
+ if (error instanceof ArchiveHistoryError || error instanceof WorkRefError) {
2271
+ console.error(` ${pc.red("Error:")} ${error.message}`);
2272
+ if (error instanceof ArchiveHistoryError) {
2273
+ for (const candidate of error.candidates)
2274
+ console.error(` ${candidate}`);
2275
+ if (error.candidatesTruncated)
2276
+ console.error(` ... ${error.candidateTotal - error.candidates.length} more`);
2277
+ }
2278
+ process.exit(1);
2279
+ }
2280
+ throw error;
2281
+ }
2282
+ }
2283
+ function appendChecklistItem(content, heading, item) {
2284
+ const headings = [...content.matchAll(new RegExp(`^## ${heading}[ \\t]*\\r?$`, "gm"))];
2285
+ if (headings.length !== 1) {
2286
+ throw new ArchiveHistoryError(
2287
+ "archive_section_invalid",
2288
+ `archived Change must contain exactly one required section: ${heading}`
2289
+ );
2290
+ }
2291
+ const sectionBodyStart = headings[0].index + headings[0][0].length;
2292
+ const nextHeading = content.indexOf("\n## ", sectionBodyStart);
2293
+ const insertAt = nextHeading < 0 ? content.length : nextHeading;
2294
+ const before = content.slice(0, insertAt);
2295
+ const separator = before.endsWith("\n\n") ? "" : before.endsWith("\n") ? "\n" : "\n\n";
2296
+ return `${before}${separator}${item}
2297
+ ${content.slice(insertAt)}`;
2298
+ }
2299
+
2195
2300
  // src/commands/show.ts
2196
- import { readFile as readFile10 } from "fs/promises";
2301
+ import { readFile as readFile11 } from "fs/promises";
2197
2302
  function exitShowError(error, options) {
2198
2303
  const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
2199
2304
  if (options.json) {
@@ -2251,7 +2356,7 @@ async function showChange(nameOrFocused, options = {}) {
2251
2356
  const srcPath = workRef.path;
2252
2357
  let content;
2253
2358
  try {
2254
- content = await readFile10(srcPath, "utf-8");
2359
+ content = await readFile11(srcPath, "utf-8");
2255
2360
  } catch {
2256
2361
  exitShowError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, options);
2257
2362
  }
@@ -2788,7 +2893,7 @@ var groupCommand = defineCommand({
2788
2893
  var addSpecCommand = defineCommand({
2789
2894
  meta: {
2790
2895
  name: "spec",
2791
- description: "Create .rsp/specs/<name>.md and rebuild specs index"
2896
+ description: "Create .rsp/specs/<name>.md and refresh local Specs indexes"
2792
2897
  },
2793
2898
  args: {
2794
2899
  name: {
@@ -2823,12 +2928,42 @@ var archiveCommand = defineCommand({
2823
2928
  },
2824
2929
  "dry-run": {
2825
2930
  type: "boolean",
2826
- description: "Preview archive readiness without moving the change",
2931
+ description: "Deprecated compatibility route to rsp ready; never moves the Change",
2827
2932
  default: false
2828
2933
  }
2829
2934
  },
2830
2935
  async run({ args }) {
2831
- await archiveChange(args.name, { dryRun: Boolean(args["dry-run"]) });
2936
+ if (args["dry-run"]) {
2937
+ console.error(" Deprecated: use `rsp ready <name>` for read-only archive readiness.");
2938
+ await showReady(args.name);
2939
+ return;
2940
+ }
2941
+ await archiveChange(args.name);
2942
+ }
2943
+ });
2944
+ var reopenCommand = defineCommand({
2945
+ meta: {
2946
+ name: "reopen",
2947
+ description: "Restore one archived Change as focused open work while retaining history"
2948
+ },
2949
+ args: {
2950
+ name: {
2951
+ type: "positional",
2952
+ description: "Archived Change WorkRef",
2953
+ required: true
2954
+ },
2955
+ from: {
2956
+ type: "string",
2957
+ description: "Exact .rsp/archives/... path when the WorkRef has multiple archives"
2958
+ },
2959
+ reason: {
2960
+ type: "string",
2961
+ description: "One-line concern that makes the restored Change unfinished",
2962
+ required: true
2963
+ }
2964
+ },
2965
+ async run({ args }) {
2966
+ await reopenChange(args.name, { from: args.from, reason: args.reason });
2832
2967
  }
2833
2968
  });
2834
2969
  var focusCommand = defineCommand({
@@ -2969,7 +3104,7 @@ var checkCommand = defineCommand({
2969
3104
  var updateCommand = defineCommand({
2970
3105
  meta: {
2971
3106
  name: "update",
2972
- description: "Refresh RSP project structure after upgrade (fallback protocol, AGENTS, indices)"
3107
+ description: "Refresh RSP-managed project files after upgrade; does not update packaged Skills"
2973
3108
  },
2974
3109
  async run() {
2975
3110
  const result = await updateProject();
@@ -3176,6 +3311,10 @@ var historyCommand = defineCommand({
3176
3311
  type: "string",
3177
3312
  description: "Match one exact Change Group"
3178
3313
  },
3314
+ search: {
3315
+ type: "string",
3316
+ description: "Case-insensitive literal match over WorkRef and summary"
3317
+ },
3179
3318
  json: {
3180
3319
  type: "boolean",
3181
3320
  description: "Print machine-readable JSON output",
@@ -3195,6 +3334,7 @@ var historyCommand = defineCommand({
3195
3334
  until: args.until,
3196
3335
  kind: args.kind,
3197
3336
  group: args.group,
3337
+ search: args.search,
3198
3338
  positionalCount: args._?.length ?? (args.name ? 1 : 0)
3199
3339
  }, { json: Boolean(args.json), compact: Boolean(args.compact) });
3200
3340
  if (!result.ok)
@@ -3212,7 +3352,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3212
3352
  if (shouldLaunchSkillsUi(rawArgs, terminalEnvironment)) {
3213
3353
  const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
3214
3354
  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");
3355
+ const { runSkillsTui } = await import("./entry-WZDVKDUM.mjs");
3216
3356
  const selection = await runSkillsTui(locale);
3217
3357
  if (selection.kind === "confirmed") {
3218
3358
  const result = await installPackagedSkills({ names: selection.names, force: selection.force });
@@ -3230,7 +3370,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3230
3370
  } else {
3231
3371
  const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
3232
3372
  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");
3373
+ const { runTui } = await import("./entry-X23GPCVS.mjs");
3234
3374
  const exitCode = await runTui(locale);
3235
3375
  if (exitCode !== 0)
3236
3376
  process.exitCode = exitCode;
@@ -3253,6 +3393,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3253
3393
  focus: focusCommand,
3254
3394
  unfocus: unfocusCommand,
3255
3395
  archive: archiveCommand,
3396
+ reopen: reopenCommand,
3256
3397
  ready: readyCommand,
3257
3398
  show: showCommand,
3258
3399
  history: historyCommand,