@oevery/rsp 3.1.0-beta.1 → 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
@@ -1,22 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- ARCHIVES_DIR,
3
+ inspectPackagedSkillInventory,
4
+ installPackagedSkills,
5
+ printPackagedSkillInventory,
6
+ printSkillInstallResult
7
+ } from "./chunk-NX55NIC3.mjs";
8
+ import {
4
9
  ArchiveHistoryError,
10
+ GROUP_BRIEF_FILENAME,
11
+ HISTORY_DEFAULT_LIMIT,
12
+ HISTORY_MAX_LIMIT,
13
+ WorkRefError,
14
+ generateGroupBriefContent,
15
+ hasArchivedGroupBrief,
16
+ historyInspectionComplete,
17
+ inspectArchiveHistory,
18
+ inspectArchiveTree,
19
+ inspectChangeDependencies,
20
+ inspectChangeGroups,
21
+ inspectFocusTree,
22
+ inspectProjectStatus,
23
+ inspectWorkTree,
24
+ projectDependencyForest,
25
+ queryArchiveHistory,
26
+ readArchiveHistoryDetail,
27
+ resolveArchiveDirectory,
28
+ resolveExecutableChange,
29
+ resolveFocusMarkerPath,
30
+ resolveWorkRef,
31
+ selectArchiveHistoryRecord
32
+ } from "./chunk-7KKAUBKN.mjs";
33
+ import {
34
+ ARCHIVES_DIR,
5
35
  CHANGES_DIR,
6
36
  CONFIG_PATH,
7
37
  DEFAULT_DECISION_RECORDS_PATH,
8
38
  FOCUS_DIR,
9
- GROUP_BRIEF_FILENAME,
10
- HISTORY_DEFAULT_LIMIT,
11
- HISTORY_MAX_LIMIT,
12
39
  LOCK_PATH,
13
- ManagedPathError,
14
40
  OBSOLETE_RSP_RULES_PATH,
15
41
  PKG_ROOT,
16
42
  RSP_DIR,
17
43
  RSP_RULES_PATH,
18
44
  VALID_KINDS,
19
- WorkRefError,
20
45
  buildDurableReviewGuidance,
21
46
  cleanupEmptyParentDirs,
22
47
  clearConfigCache,
@@ -28,56 +53,37 @@ import {
28
53
  emitJson,
29
54
  ensureDecisionRecordsDirectory,
30
55
  ensureManagedFile,
31
- extractSection,
32
56
  generateChangeContent,
33
57
  generateDesignContent,
34
- generateGroupBriefContent,
35
58
  generateSpecContent,
36
59
  getDurableReviewCandidateTargets,
37
60
  getVersion,
38
61
  guardRspInitialized,
39
- hasArchivedGroupBrief,
40
62
  hasMeaningfulBlockers,
41
63
  hasRspAgentsBlock,
42
- historyInspectionComplete,
43
- inspectArchiveHistory,
44
- inspectArchiveTree,
45
- inspectChangeDependencies,
46
- inspectChangeGroups,
47
- inspectFocusTree,
48
64
  inspectManagedDirectory,
49
65
  inspectManagedFile,
50
66
  inspectManagedFileTree,
51
- inspectProjectStatus,
52
67
  inspectRspConfig,
53
68
  inspectUnsupportedRules,
54
- inspectWorkTree,
55
69
  isValidSpecName,
56
70
  loadRspConfig,
57
71
  normalizeLogicalPath,
58
72
  parseFrontmatter,
59
73
  parseScenarios,
60
74
  pc,
61
- projectDependencyForest,
62
- queryArchiveHistory,
63
- readArchiveHistoryDetail,
64
75
  recordRuntimeDiagnostic,
65
76
  requireManagedDirectory,
66
- resolveArchiveDirectory,
67
77
  resolveDecisionRecordsPath,
68
- resolveExecutableChange,
69
- resolveFocusMarkerPath,
70
78
  resolveKinds,
71
79
  resolveManagedDirectoryChain,
72
80
  resolveRequiredSections,
73
- resolveWorkRef,
74
- selectArchiveHistoryRecord,
75
81
  toErrorMessage,
76
82
  upsertRspAgentsBlock,
77
83
  validateDecisionRecordsFilesystemPath,
78
84
  walkMarkdownFiles,
79
85
  writeManagedFile
80
- } from "./chunk-QLXIDVKK.mjs";
86
+ } from "./chunk-OTJFL6XH.mjs";
81
87
 
82
88
  // src/cli.ts
83
89
  import { resolve } from "path";
@@ -85,7 +91,7 @@ import { fileURLToPath } from "url";
85
91
  import { defineCommand, runMain } from "citty";
86
92
 
87
93
  // src/commands/add-spec.ts
88
- 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";
89
95
  import { join as join2 } from "path";
90
96
 
91
97
  // src/core/lock.ts
@@ -150,12 +156,51 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
150
156
  }
151
157
 
152
158
  // src/commands/specs-index.ts
153
- import { existsSync } from "fs";
154
- import { readFile as readFile2 } from "fs/promises";
155
- import { basename, isAbsolute, join, relative, sep } from "path";
156
- 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;
157
165
  if (acquireLock)
158
- 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) {
159
204
  const specsDir = join(RSP_DIR, "specs");
160
205
  const configInspection = await inspectRspConfig();
161
206
  if (configInspection.issues.length > 0)
@@ -164,91 +209,154 @@ async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
164
209
  const inspection = await inspectManagedFileTree(specsDir, "Specs", { allowMissing: true });
165
210
  if (inspection.issues.length > 0)
166
211
  throw inspection.issues[0];
167
- if (!inspection.rootExists) {
168
- if (!quiet)
169
- console.log(` ${pc.dim("No specs directory.")}
170
- `);
171
- 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
+ });
172
272
  }
173
- const specFiles = inspection.files.filter((fp) => fp.endsWith(".md")).filter((fp) => {
174
- const name = basename(fp);
175
- return name !== "INDEX.md" && name !== "design.md" && !isInside(DEFAULT_DECISION_RECORDS_PATH, fp) && !isInside(decisionRecordsPath, fp);
176
- }).sort();
177
- const lines = [];
178
- lines.push("---");
179
- lines.push("title: Specs Index");
180
- lines.push("summary: Additional project-level specs beyond design.md.");
181
- lines.push("kind: generated-index");
182
- lines.push("index_type: specs");
183
- lines.push("source_dir: .rsp/specs");
184
- lines.push(`entry_count: ${specFiles.length}`);
185
- lines.push("---");
186
- lines.push("");
187
- lines.push("# Specs Index");
188
- lines.push("");
189
- lines.push("_Additional project-level specs beyond `design.md`._");
190
- lines.push("");
191
- if (specFiles.length === 0) {
192
- lines.push("_No additional project-level specs yet._");
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
+ });
284
+ }
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._");
193
303
  } else {
194
- lines.push("| File | Title | Summary |");
195
- lines.push("|------|-------|---------|");
196
- for (const fp of specFiles) {
197
- const rel = normalizeLogicalPath(relative(specsDir, fp));
198
- const content = await readFile2(fp, "utf-8");
199
- const fm = parseFrontmatter(content);
200
- const fmTitle = fm && typeof fm.title === "string" ? fm.title : "";
201
- const title = fmTitle || extractTitle(content) || rel.replace(/\.md$/, "");
202
- const summary = extractSummary(content, fm?.summary);
203
- lines.push(`| ${escapeCell(rel)} | ${escapeCell(title)} | ${escapeCell(summary || "\u2014")} |`);
204
- }
205
- }
206
- const indexPath = join(specsDir, "INDEX.md");
207
- const nextContent = lines.join("\n");
208
- const existingContent = existsSync(indexPath) ? await readFile2(indexPath, "utf-8") : null;
209
- const changed = existingContent !== nextContent;
210
- if (changed)
211
- await writeManagedFile(indexPath, nextContent, "Specs index");
212
- if (!quiet)
213
- console.log(` ${pc.green("INDEX.md updated:")} ${specFiles.length} spec file(s).
214
- `);
215
- return changed;
216
- }
217
- function isInside(directory, filePath) {
218
- 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);
219
324
  return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
220
325
  }
326
+ function toProjectPath(path) {
327
+ return normalizeLogicalPath(relative(".", path));
328
+ }
221
329
  function extractTitle(content) {
222
330
  for (const rawLine of content.split("\n")) {
223
- if (!rawLine.startsWith("# "))
224
- continue;
225
- return rawLine.slice(2).trim();
331
+ if (rawLine.startsWith("# "))
332
+ return rawLine.slice(2).trim();
226
333
  }
227
334
  return "";
228
335
  }
229
336
  function extractSummary(content, frontmatterSummary) {
230
337
  if (typeof frontmatterSummary === "string" && frontmatterSummary.trim() !== "")
231
338
  return frontmatterSummary.trim();
232
- const lines = content.split("\n");
233
339
  let inFrontmatter = false;
234
- for (const rawLine of lines) {
340
+ for (const rawLine of content.split("\n")) {
235
341
  const line = rawLine.trim();
236
342
  if (line === "---" || line === "...") {
237
343
  inFrontmatter = !inFrontmatter;
238
344
  continue;
239
345
  }
240
- if (inFrontmatter || line === "")
241
- continue;
242
- if (line.startsWith("#"))
243
- continue;
244
- if (line.startsWith("##"))
346
+ if (inFrontmatter || line === "" || line.startsWith("#"))
245
347
  continue;
246
348
  return line.replace(/^[-*]\s*/, "").trim();
247
349
  }
248
350
  return "";
249
351
  }
250
352
  function escapeCell(value) {
251
- 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");
252
360
  }
253
361
 
254
362
  // src/commands/add-spec.ts
@@ -261,6 +369,10 @@ async function addSpec(name, projectName = "project") {
261
369
  console.error(` ${pc.red("Error:")} .rsp/specs/decisions/ is reserved for Decision Records`);
262
370
  process.exit(1);
263
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
+ }
264
376
  guardRspInitialized();
265
377
  try {
266
378
  const configInspection = await inspectRspConfig();
@@ -287,11 +399,11 @@ async function addSpec(name, projectName = "project") {
287
399
  try {
288
400
  await writeFile2(path, content, { flag: "wx" });
289
401
  created = true;
290
- await buildSpecsIndex({ acquireLock: false });
402
+ await buildSpecsIndex({ acquireLock: false, affectedDirectory: parent });
291
403
  } catch (error) {
292
404
  if (created) {
293
405
  try {
294
- await unlink2(path);
406
+ await unlink3(path);
295
407
  await cleanupEmptyParentDirs(path, specsDir);
296
408
  } catch {
297
409
  }
@@ -306,126 +418,22 @@ async function addSpec(name, projectName = "project") {
306
418
  }
307
419
 
308
420
  // src/commands/archive.ts
309
- import { existsSync as existsSync3 } from "fs";
310
- import { mkdir as mkdir3, readFile as readFile4, rename, unlink as unlink3 } from "fs/promises";
311
- import { basename as basename3, join as join4 } from "path";
312
-
313
- // src/commands/archive-index.ts
314
- import { existsSync as existsSync2 } from "fs";
315
- import { readFile as readFile3 } from "fs/promises";
316
- import { basename as basename2, dirname, join as join3, relative as relative2 } from "path";
317
- async function buildArchiveIndex({ acquireLock = true, quiet = false } = {}) {
318
- if (acquireLock)
319
- return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false, quiet }));
320
- const inspection = await inspectArchiveTree();
321
- if (inspection.diagnostics.length > 0) {
322
- const diagnostic = inspection.diagnostics[0];
323
- throw new WorkRefError(diagnostic.code, diagnostic.message, diagnostic.input);
324
- }
325
- if (!inspection.rootExists) {
326
- if (!quiet)
327
- console.log(` ${pc.dim("No archives directory.")}
328
- `);
329
- return false;
330
- }
331
- const archiveFiles = inspection.files;
332
- const lines = [];
333
- lines.push("---");
334
- lines.push("title: Archive Index");
335
- lines.push("summary: Completed RSP changes.");
336
- lines.push("kind: generated-index");
337
- lines.push("index_type: archives");
338
- lines.push("source_dir: .rsp/archives");
339
- lines.push(`entry_count: ${archiveFiles.length}`);
340
- lines.push("---");
341
- lines.push("");
342
- lines.push("# Archive Index");
343
- lines.push("");
344
- if (archiveFiles.length === 0) {
345
- lines.push("_No archived changes yet._");
346
- } else {
347
- lines.push("| Date | Change | Kind | Summary |");
348
- lines.push("|------|--------|------|---------|");
349
- }
350
- for (const fp of archiveFiles) {
351
- const base = basename2(fp);
352
- const archiveRel = normalizeLogicalPath(relative2(ARCHIVES_DIR, fp));
353
- const archiveDirPart = dirname(archiveRel);
354
- const dateMatch = base.match(/^(\d{4}-\d{2}-\d{2})_(.+)\.md$/);
355
- const date = dateMatch ? dateMatch[1] : "\u2014";
356
- const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
357
- const changeName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
358
- let summary = "";
359
- let kind = "";
360
- try {
361
- const content = await readFile3(fp, "utf-8");
362
- const fm = parseFrontmatter(content);
363
- if (typeof fm?.kind === "string")
364
- kind = fm.kind;
365
- if (typeof fm?.summary === "string")
366
- summary = fm.summary.trim();
367
- const proposal = extractSection(content, "Proposal");
368
- const outcomeLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Outcome:"));
369
- const legacySummaryLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Summary:"));
370
- if (!summary && outcomeLine)
371
- summary = outcomeLine.slice("- Outcome:".length).trim();
372
- if (!summary && legacySummaryLine)
373
- summary = legacySummaryLine.slice("- Summary:".length).trim();
374
- } catch {
375
- }
376
- lines.push(`| ${date} | ${escapeCell2(changeName)} | ${escapeCell2(kind || "\u2014")} | ${escapeCell2(summary || "\u2014")} |`);
377
- }
378
- const indexPath = join3(ARCHIVES_DIR, "INDEX.md");
379
- const nextContent = lines.join("\n");
380
- const existingContent = existsSync2(indexPath) ? await readFile3(indexPath, "utf-8") : null;
381
- const changed = existingContent !== nextContent;
382
- if (changed)
383
- await writeManagedFile(indexPath, nextContent, "archive index");
384
- if (!quiet)
385
- console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived change(s).
386
- `);
387
- return changed;
388
- }
389
- function escapeCell2(value) {
390
- return value.replace(/\|/g, "\\|");
391
- }
392
-
393
- // src/commands/archive.ts
394
- 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) {
395
425
  if (!name) {
396
426
  console.error(` ${pc.red("Usage:")} rsp archive <name>`);
397
427
  process.exit(1);
398
428
  }
399
429
  guardRspInitialized();
400
- if (options.dryRun) {
401
- const workRef = await resolveArchiveWorkRefOrExit(name);
402
- const content = await readFile4(workRef.path, "utf-8");
403
- const dependencyInspection = await inspectChangeDependencies();
404
- const checklist = collectArchiveChecklist(content, {
405
- activeBlockers: dependencyInspection.activeBlockers.get(name)
406
- });
407
- console.log();
408
- console.log(` ${pc.bold("Archive dry-run for")} ${pc.cyan(name)}`);
409
- console.log();
410
- if (checklist.length === 0) {
411
- console.log(` ${pc.green("\u2713")} Ready to archive. No deterministic warnings found.
412
- `);
413
- } else {
414
- for (const line of checklist)
415
- console.log(` ${pc.yellow("\u26A0")} ${line}`);
416
- console.log();
417
- console.log(` ${pc.dim("Review the warnings above before treating this work as fully closed.")}
418
- `);
419
- }
420
- return;
421
- }
422
430
  try {
423
431
  return await withRspLock("archive-change", async () => {
424
432
  const workRef = await resolveExecutableChange(name, { mustExist: true });
425
433
  const srcPath = workRef.path;
426
434
  const archiveSubdir = resolveArchiveDirectory(workRef);
427
435
  const focusEntry = resolveFocusMarkerPath(workRef);
428
- const content = await readFile4(srcPath, "utf-8");
436
+ const content = await readFile3(srcPath, "utf-8");
429
437
  const dependencyInspection = await inspectChangeDependencies();
430
438
  const checklist = collectArchiveChecklist(content, {
431
439
  activeBlockers: dependencyInspection.activeBlockers.get(name)
@@ -436,15 +444,15 @@ async function archiveChange(name, options = {}) {
436
444
  console.log(` ${pc.dim("Archive will continue. Review the warnings above before treating this work as fully closed.")}
437
445
  `);
438
446
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
439
- const base = basename3(workRef.name);
447
+ const base = basename2(workRef.name);
440
448
  await mkdir3(archiveSubdir, { recursive: true });
441
449
  const archiveName = resolveArchiveName(archiveSubdir, date, base);
442
- await rename(srcPath, join4(archiveSubdir, archiveName));
450
+ await rename(srcPath, join3(archiveSubdir, archiveName));
443
451
  const postArchiveWarnings = [];
444
452
  let focusCleared = false;
445
- if (existsSync3(focusEntry)) {
453
+ if (existsSync(focusEntry)) {
446
454
  try {
447
- await unlink3(focusEntry);
455
+ await unlink4(focusEntry);
448
456
  await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
449
457
  focusCleared = true;
450
458
  } catch (error) {
@@ -460,17 +468,12 @@ async function archiveChange(name, options = {}) {
460
468
  ` : "";
461
469
  console.log(` ${pc.green("Archived:")} ${archiveName}
462
470
  ${clearedMsg}`);
463
- try {
464
- await buildArchiveIndex({ acquireLock: false });
465
- } catch (error) {
466
- postArchiveWarnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
467
- }
468
471
  for (const warning of postArchiveWarnings)
469
472
  console.log(` ${pc.yellow("\u26A0")} ${warning}`);
470
473
  if (postArchiveWarnings.length > 0)
471
474
  console.log(` ${pc.dim("Archive completed, but follow-up cleanup was only partially successful.")}
472
475
  `);
473
- if (existsSync3(".git")) {
476
+ if (existsSync(".git")) {
474
477
  console.log(` ${pc.cyan("Git delivery:")}
475
478
  `);
476
479
  console.log(` git status --short`);
@@ -486,28 +489,14 @@ ${clearedMsg}`);
486
489
  throw error;
487
490
  }
488
491
  }
489
- async function resolveArchiveWorkRefOrExit(name) {
490
- try {
491
- const workRef = await resolveExecutableChange(name, { mustExist: true });
492
- resolveArchiveDirectory(workRef);
493
- resolveFocusMarkerPath(workRef);
494
- return workRef;
495
- } catch (error) {
496
- if (error instanceof WorkRefError) {
497
- console.error(` ${pc.red("Error:")} ${error.message}`);
498
- process.exit(1);
499
- }
500
- throw error;
501
- }
502
- }
503
492
  function resolveArchiveName(archiveSubdir, date, base) {
504
493
  const initialName = `${date}_${base}.md`;
505
- if (!existsSync3(join4(archiveSubdir, initialName)))
494
+ if (!existsSync(join3(archiveSubdir, initialName)))
506
495
  return initialName;
507
496
  const MAX_SUFFIX = 9999;
508
497
  for (let index = 2; index <= MAX_SUFFIX; index++) {
509
498
  const candidate = `${date}_${base}-${index}.md`;
510
- if (!existsSync3(join4(archiveSubdir, candidate)))
499
+ if (!existsSync(join3(archiveSubdir, candidate)))
511
500
  return candidate;
512
501
  }
513
502
  throw new WorkRefError(
@@ -518,7 +507,7 @@ function resolveArchiveName(archiveSubdir, date, base) {
518
507
  }
519
508
 
520
509
  // src/commands/check.ts
521
- import { readFile as readFile5 } from "fs/promises";
510
+ import { readFile as readFile4 } from "fs/promises";
522
511
  async function runCheck(options = {}) {
523
512
  const diagnostics = [];
524
513
  const runtime = [];
@@ -561,7 +550,7 @@ async function runCheck(options = {}) {
561
550
  const entryContent = marker.name;
562
551
  let markerContent = "";
563
552
  try {
564
- markerContent = (await readFile5(entryPath, "utf-8")).trim();
553
+ markerContent = (await readFile4(entryPath, "utf-8")).trim();
565
554
  } catch (error) {
566
555
  addDiagnostic({
567
556
  severity: "error",
@@ -672,7 +661,7 @@ async function runCheck(options = {}) {
672
661
  const { name, path: fp } = ref;
673
662
  let content;
674
663
  try {
675
- content = await readFile5(fp, "utf-8");
664
+ content = await readFile4(fp, "utf-8");
676
665
  } catch (error) {
677
666
  addDiagnostic({
678
667
  severity: "error",
@@ -901,8 +890,8 @@ function collectMatchingLines(content, predicate) {
901
890
  }
902
891
 
903
892
  // src/commands/create.ts
904
- import { existsSync as existsSync4 } from "fs";
905
- 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";
906
895
  import { dirname as dirname2 } from "path";
907
896
  async function createChange(name, summary = "", kind, options = {}) {
908
897
  if (!name) {
@@ -914,7 +903,7 @@ async function createChange(name, summary = "", kind, options = {}) {
914
903
  return await withRspLock("create-change", async () => {
915
904
  const workRef = await resolveExecutableChange(name);
916
905
  const changePath = workRef.path;
917
- const existed = existsSync4(changePath);
906
+ const existed = existsSync2(changePath);
918
907
  if (!existed) {
919
908
  const focusEntry = resolveFocusMarkerPath(workRef);
920
909
  const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
@@ -929,7 +918,7 @@ async function createChange(name, summary = "", kind, options = {}) {
929
918
  } catch (error) {
930
919
  if (changeCreated) {
931
920
  try {
932
- await unlink4(changePath);
921
+ await unlink5(changePath);
933
922
  await cleanupEmptyParentDirs(changePath, CHANGES_DIR);
934
923
  } catch {
935
924
  }
@@ -956,17 +945,39 @@ async function createChange(name, summary = "", kind, options = {}) {
956
945
  }
957
946
 
958
947
  // src/commands/doctor.ts
959
- import { existsSync as existsSync6 } from "fs";
948
+ import { existsSync as existsSync4 } from "fs";
960
949
  import { readdir, readFile as readFile7, stat } from "fs/promises";
961
- 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";
962
951
 
963
952
  // src/commands/update.ts
964
- import { existsSync as existsSync5 } from "fs";
953
+ import { existsSync as existsSync3 } from "fs";
965
954
  import { mkdir as mkdir5, readFile as readFile6, rm, rmdir } from "fs/promises";
966
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
967
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";
968
979
  async function updateProject(options = {}) {
969
- if (!existsSync5(RSP_DIR)) {
980
+ if (!existsSync3(RSP_DIR)) {
970
981
  console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
971
982
  console.error(` ${pc.dim("Run: rsp init")}`);
972
983
  process.exit(1);
@@ -997,6 +1008,9 @@ async function updateProject(options = {}) {
997
1008
  console.log(` ${pc.green("\u2713")} changes/ directory restored`);
998
1009
  updated = true;
999
1010
  }
1011
+ const archivesInspection = inspectManagedDirectory(join5(RSP_DIR, "archives"), "archive root", { allowMissing: true });
1012
+ if (archivesInspection.issue)
1013
+ throw archivesInspection.issue;
1000
1014
  const bundledRules = await readFile6(join5(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
1001
1015
  const rulesInspection = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
1002
1016
  if (rulesInspection.issue)
@@ -1009,7 +1023,7 @@ async function updateProject(options = {}) {
1009
1023
  console.log(` ${pc.green("\u2713")} rsp-rules.md updated`);
1010
1024
  updated = true;
1011
1025
  }
1012
- if (existsSync5(OBSOLETE_RSP_RULES_PATH)) {
1026
+ if (existsSync3(OBSOLETE_RSP_RULES_PATH)) {
1013
1027
  await rm(OBSOLETE_RSP_RULES_PATH);
1014
1028
  actions.push("obsolete rules/rsp-rules.md removed");
1015
1029
  if (!options.quiet)
@@ -1071,9 +1085,14 @@ async function updateProject(options = {}) {
1071
1085
  updated = true;
1072
1086
  }
1073
1087
  const specsIndexChanged = await buildSpecsIndex({ acquireLock: false, quiet: options.quiet });
1074
- const archiveIndexChanged = await buildArchiveIndex({ acquireLock: false, quiet: options.quiet });
1075
- if (specsIndexChanged || archiveIndexChanged) {
1076
- 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`);
1077
1096
  updated = true;
1078
1097
  }
1079
1098
  if (!options.quiet && inspection.diagnostics.length > 0) {
@@ -1109,7 +1128,7 @@ async function updateProject(options = {}) {
1109
1128
  async function runDoctor(options = {}) {
1110
1129
  const fixed = [];
1111
1130
  let repairIssue = null;
1112
- if (options.fix && existsSync6(RSP_DIR)) {
1131
+ if (options.fix && existsSync4(RSP_DIR)) {
1113
1132
  try {
1114
1133
  const update = await updateProject({ quiet: Boolean(options.json) });
1115
1134
  fixed.push(...update.actions);
@@ -1129,17 +1148,12 @@ async function runDoctor(options = {}) {
1129
1148
  const runtime = [];
1130
1149
  const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
1131
1150
  const designPath = join6(RSP_DIR, "specs", "design.md");
1132
- const specsIndexPath = join6(RSP_DIR, "specs", "INDEX.md");
1133
- const archivesIndexPath = join6(RSP_DIR, "archives", "INDEX.md");
1134
- reportCheck(checks, ".rsp exists", existsSync6(RSP_DIR), "Run: rsp init");
1151
+ reportCheck(checks, ".rsp exists", existsSync4(RSP_DIR), "Run: rsp init");
1135
1152
  reportFallbackProtocol(checks);
1136
1153
  await reportUnsupportedRules(checks, reportRuntime);
1137
- reportCheck(checks, "specs/design.md exists", existsSync6(designPath), "Run: rsp init");
1138
- reportCheck(checks, "specs/INDEX.md exists", existsSync6(specsIndexPath), "Run: rsp update");
1139
- reportCheck(checks, "archives/INDEX.md exists", existsSync6(archivesIndexPath), "Run: rsp update");
1154
+ reportCheck(checks, "specs/design.md exists", existsSync4(designPath), "Run: rsp init");
1140
1155
  await checkAgents(checks, reportRuntime);
1141
- await checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "specs", "specs/INDEX.md has generated-index metadata", "Run: rsp update");
1142
- await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "archives", "archives/INDEX.md has generated-index metadata", "Run: rsp update");
1156
+ await checkSpecsIndexes(checks, reportRuntime);
1143
1157
  await checkArchiveNaming(checks);
1144
1158
  const decisionRecordsConfigValid = await checkConfigSemantics(checks, reportRuntime);
1145
1159
  if (decisionRecordsConfigValid) {
@@ -1167,7 +1181,7 @@ async function runDoctor(options = {}) {
1167
1181
  if (fixed.length > 0) {
1168
1182
  console.log(` ${pc.green("Fixed:")} ${fixed.join(", ")}`);
1169
1183
  console.log();
1170
- } else if (options.fix && !repairIssue && existsSync6(RSP_DIR)) {
1184
+ } else if (options.fix && !repairIssue && existsSync4(RSP_DIR)) {
1171
1185
  console.log(` ${pc.dim("No safe fixes needed.")}`);
1172
1186
  console.log();
1173
1187
  }
@@ -1185,7 +1199,7 @@ async function runDoctor(options = {}) {
1185
1199
  }
1186
1200
  function reportFallbackProtocol(checks) {
1187
1201
  const canonical = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
1188
- const obsoleteExists = existsSync6(OBSOLETE_RSP_RULES_PATH);
1202
+ const obsoleteExists = existsSync4(OBSOLETE_RSP_RULES_PATH);
1189
1203
  if (canonical.issue) {
1190
1204
  checks.push({
1191
1205
  status: "issue",
@@ -1199,7 +1213,7 @@ function reportFallbackProtocol(checks) {
1199
1213
  checks.push({
1200
1214
  status: "issue",
1201
1215
  label: "rsp-rules.md exists",
1202
- hint: existsSync6(RSP_DIR) ? "Run: rsp update" : "Run: rsp init"
1216
+ hint: existsSync4(RSP_DIR) ? "Run: rsp update" : "Run: rsp init"
1203
1217
  });
1204
1218
  }
1205
1219
  if (obsoleteExists) {
@@ -1290,44 +1304,31 @@ async function checkAgents(checks, reportRuntime) {
1290
1304
  }
1291
1305
  checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
1292
1306
  }
1293
- async function checkGeneratedIndex(checks, reportRuntime, path, expectedIndexType, label, fixHint) {
1294
- const indexFile = inspectManagedFile(path, label, { allowMissing: true });
1295
- if (indexFile.issue) {
1296
- checks.push({ status: "issue", label, message: indexFile.issue.message, hint: fixHint });
1297
- return;
1298
- }
1299
- if (!indexFile.exists)
1300
- return;
1301
- let content;
1302
- try {
1303
- content = await readFile7(path, "utf-8");
1304
- } catch (error) {
1305
- reportRuntime({
1306
- code: "generated_index_read_failed",
1307
- operation: "readFile",
1308
- path,
1309
- message: toErrorMessage(error)
1310
- });
1311
- 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" });
1312
1311
  return;
1313
1312
  }
1314
- try {
1315
- const fm = parseFrontmatter(content);
1316
- if (fm?.kind === "generated-index" && fm.index_type === expectedIndexType) {
1317
- checks.push({ status: "ok", label });
1318
- return;
1319
- }
1320
- } catch (error) {
1313
+ for (const message of health.issues) {
1321
1314
  reportRuntime({
1322
- code: "generated_index_frontmatter_parse_failed",
1323
- operation: "parseFrontmatter",
1324
- path,
1325
- message: toErrorMessage(error)
1315
+ code: "specs_index_inspection_failed",
1316
+ operation: "inspectSpecsIndexHealth",
1317
+ path: ".rsp/specs",
1318
+ message
1326
1319
  });
1327
- checks.push({ status: "issue", label, message: `${label} could not parse metadata`, hint: fixHint });
1328
- return;
1329
1320
  }
1330
- 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
+ });
1331
1332
  }
1332
1333
  async function checkArchiveNaming(checks) {
1333
1334
  const archivesDir = join6(RSP_DIR, "archives");
@@ -1342,7 +1343,7 @@ async function checkArchiveNaming(checks) {
1342
1343
  return;
1343
1344
  }
1344
1345
  checks.push({ status: "ok", label: "archives use supported managed paths" });
1345
- 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);
1346
1347
  if (invalidFiles.length === 0) {
1347
1348
  checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
1348
1349
  return;
@@ -1430,7 +1431,7 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
1430
1431
  }
1431
1432
  async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
1432
1433
  const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
1433
- if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync6(DEFAULT_DECISION_RECORDS_PATH))
1434
+ if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync4(DEFAULT_DECISION_RECORDS_PATH))
1434
1435
  return;
1435
1436
  let inspectionFailed = false;
1436
1437
  const inactiveRecords = (await walkMarkdownFiles(DEFAULT_DECISION_RECORDS_PATH, {
@@ -1438,7 +1439,7 @@ async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
1438
1439
  inspectionFailed = true;
1439
1440
  reportRuntime(diagnostic);
1440
1441
  }
1441
- })).map((path) => normalizeLogicalPath(relative3(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
1442
+ })).map((path) => normalizeLogicalPath(relative2(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
1442
1443
  if (inspectionFailed) {
1443
1444
  checks.push({
1444
1445
  status: "issue",
@@ -1548,8 +1549,8 @@ async function checkActiveChangeConsistency(checks) {
1548
1549
  }
1549
1550
 
1550
1551
  // src/commands/focus.ts
1551
- import { existsSync as existsSync7 } from "fs";
1552
- 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";
1553
1554
  import { dirname as dirname3 } from "path";
1554
1555
  async function focusChange(name) {
1555
1556
  if (!name) {
@@ -1582,9 +1583,9 @@ async function unfocusChange(name) {
1582
1583
  return await withRspLock("unfocus-change", async () => {
1583
1584
  const workRef = resolveWorkRef(name, { executable: false, mustExist: false });
1584
1585
  const focusEntry = resolveFocusMarkerPath(workRef);
1585
- if (!existsSync7(focusEntry))
1586
+ if (!existsSync5(focusEntry))
1586
1587
  throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
1587
- await unlink5(focusEntry);
1588
+ await unlink7(focusEntry);
1588
1589
  await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
1589
1590
  console.log(` ${pc.green("Unfocused:")} ${name}`);
1590
1591
  console.log(` ${pc.dim("focus.d cleared")} \u2192 ${name}`);
@@ -1675,11 +1676,6 @@ async function closeChangeGroup(name) {
1675
1676
  } catch (error) {
1676
1677
  warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
1677
1678
  }
1678
- try {
1679
- await buildArchiveIndex({ acquireLock: false });
1680
- } catch (error) {
1681
- warnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
1682
- }
1683
1679
  console.log(` ${pc.green("Closed Change Group:")} ${name}`);
1684
1680
  console.log(` ${pc.dim("Archived Group Brief:")} ${join7(archiveDirectory, archiveName)}
1685
1681
  `);
@@ -1773,7 +1769,8 @@ async function showHistory(input = {}, options = {}) {
1773
1769
  since: query.since ?? null,
1774
1770
  until: query.until ?? null,
1775
1771
  kind: query.kind ?? null,
1776
- group: query.group ?? null
1772
+ group: query.group ?? null,
1773
+ search: query.search ?? null
1777
1774
  },
1778
1775
  records: listed.records,
1779
1776
  summary: listed.summary,
@@ -1793,7 +1790,7 @@ function validateHistoryQuery(input) {
1793
1790
  if (input.workRef) {
1794
1791
  if (!WORK_REF_RE.test(input.workRef) || input.workRef.endsWith("/brief") || input.workRef.endsWith("/00-brief"))
1795
1792
  return invalid("invalid_history_work_ref", "history WorkRef must be a flat or one-Group-level executable Change identity");
1796
- 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)
1797
1794
  return invalid("history_detail_filters_unsupported", "list filters cannot be combined with an exact history detail lookup");
1798
1795
  return { ok: true, query: {} };
1799
1796
  }
@@ -1810,6 +1807,8 @@ function validateHistoryQuery(input) {
1810
1807
  return invalid("invalid_history_kind", "--kind must be a non-empty exact historical kind");
1811
1808
  if (input.group !== void 0 && !GROUP_RE.test(input.group))
1812
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");
1813
1812
  return {
1814
1813
  ok: true,
1815
1814
  query: {
@@ -1817,7 +1816,8 @@ function validateHistoryQuery(input) {
1817
1816
  since: input.since,
1818
1817
  until: input.until,
1819
1818
  kind: input.kind?.trim(),
1820
- group: input.group
1819
+ group: input.group,
1820
+ search: input.search?.trim()
1821
1821
  }
1822
1822
  };
1823
1823
  }
@@ -1909,7 +1909,7 @@ function printHistoryDetail(result) {
1909
1909
  }
1910
1910
 
1911
1911
  // src/commands/init.ts
1912
- import { existsSync as existsSync8 } from "fs";
1912
+ import { existsSync as existsSync6 } from "fs";
1913
1913
  import { mkdir as mkdir8, readFile as readFile8 } from "fs/promises";
1914
1914
  import { join as join8 } from "path";
1915
1915
  function generateConfigTemplate() {
@@ -1923,7 +1923,14 @@ function generateConfigTemplate() {
1923
1923
  # kinds: ${VALID_KINDS.join(", ")}
1924
1924
  #
1925
1925
  # kinds:
1926
- ${fmtList(VALID_KINDS, 2)}
1926
+ ${fmtList(VALID_KINDS, 0)}
1927
+
1928
+ # New projects default to automatic Manage routing with lifecycle-only closeout.
1929
+ # Use activation: explicit to require a named managed request.
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.
1931
+ manage:
1932
+ activation: auto
1933
+ closeout: lifecycle
1927
1934
 
1928
1935
  # Decision Records default to .rsp/specs/decisions.
1929
1936
  # Set exactly one project-relative authoritative directory when the Host Project already owns Decision Records elsewhere.
@@ -1938,7 +1945,7 @@ function toTitle(projectName) {
1938
1945
  return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
1939
1946
  }
1940
1947
  async function initProject(args = {}) {
1941
- const isNew = !existsSync8(RSP_DIR);
1948
+ const isNew = !existsSync6(RSP_DIR);
1942
1949
  const projectName = await detectProjectName();
1943
1950
  const mode = args.agentsMode ?? "managed";
1944
1951
  if (mode !== "managed" && mode !== "print") {
@@ -1953,7 +1960,7 @@ async function initProject(args = {}) {
1953
1960
  const agentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
1954
1961
  if (agentsInspection.issue)
1955
1962
  throw agentsInspection.issue;
1956
- if (existsSync8(CONFIG_PATH)) {
1963
+ if (existsSync6(CONFIG_PATH)) {
1957
1964
  clearConfigCache();
1958
1965
  const existingConfig = await inspectRspConfig();
1959
1966
  if (existingConfig.issues.length > 0)
@@ -1983,12 +1990,9 @@ async function initProject(args = {}) {
1983
1990
  throw new Error(configInspection.issues.join("; "));
1984
1991
  const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
1985
1992
  created = await ensureDecisionRecordsDirectory(decisionRecordsPath) || created;
1986
- const createdSpecsIndex = await ensureFile(join8(RSP_DIR, "specs", "INDEX.md"), "# Specs Index\n\n_Additional project-level specs beyond `design.md`._\n");
1987
- created = createdSpecsIndex || created;
1988
1993
  const createdDesign = await ensureFile(join8(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
1989
1994
  created = createdDesign || created;
1990
- const createdArchivesIndex = await ensureFile(join8(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
1991
- created = createdArchivesIndex || created;
1995
+ created = await removeLegacyArchiveIndex() || created;
1992
1996
  created = await ensureFile(join8(CHANGES_DIR, ".gitkeep"), "") || created;
1993
1997
  created = await ensureFile(join8(FOCUS_DIR, ".gitkeep"), "") || created;
1994
1998
  created = await ensureFile(join8(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
@@ -1999,10 +2003,7 @@ async function initProject(args = {}) {
1999
2003
  created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
2000
2004
  created = await ensureFile(focusMarker, "") || created;
2001
2005
  }
2002
- if (createdSpecsIndex || createdDesign)
2003
- await buildSpecsIndex({ acquireLock: false });
2004
- if (createdArchivesIndex)
2005
- await buildArchiveIndex({ acquireLock: false });
2006
+ created = await buildSpecsIndex({ acquireLock: false }) || created;
2006
2007
  const title = toTitle(projectName);
2007
2008
  let agentsUpdated = false;
2008
2009
  if (!agentsInspection.exists) {
@@ -2178,8 +2179,126 @@ function formatArchiveReady(value) {
2178
2179
  }
2179
2180
  }
2180
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
+
2181
2300
  // src/commands/show.ts
2182
- import { readFile as readFile10 } from "fs/promises";
2301
+ import { readFile as readFile11 } from "fs/promises";
2183
2302
  function exitShowError(error, options) {
2184
2303
  const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
2185
2304
  if (options.json) {
@@ -2237,7 +2356,7 @@ async function showChange(nameOrFocused, options = {}) {
2237
2356
  const srcPath = workRef.path;
2238
2357
  let content;
2239
2358
  try {
2240
- content = await readFile10(srcPath, "utf-8");
2359
+ content = await readFile11(srcPath, "utf-8");
2241
2360
  } catch {
2242
2361
  exitShowError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, options);
2243
2362
  }
@@ -2348,239 +2467,6 @@ function formatArchiveReady2(value) {
2348
2467
  }
2349
2468
  }
2350
2469
 
2351
- // src/commands/skills.ts
2352
- import { randomUUID } from "crypto";
2353
- import { lstatSync } from "fs";
2354
- import { chmod, copyFile, mkdir as mkdir9, readdir as readdir2, readFile as readFile11, rename as rename3, rm as rm2 } from "fs/promises";
2355
- import { dirname as dirname4, join as join9, relative as relative4 } from "path";
2356
- function unsupportedEntry(path, label) {
2357
- return new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`);
2358
- }
2359
- function inspectDirectoryIdentity(path, label) {
2360
- const inspection = inspectManagedDirectory(path, label);
2361
- if (inspection.issue)
2362
- throw inspection.issue;
2363
- const stats = lstatSync(path);
2364
- return { path, dev: stats.dev, ino: stats.ino };
2365
- }
2366
- function assertDirectoryIdentities(identities, label) {
2367
- for (const identity of identities) {
2368
- const current = inspectDirectoryIdentity(identity.path, label);
2369
- if (current.dev !== identity.dev || current.ino !== identity.ino)
2370
- throw new ManagedPathError(identity.path, `${label} changed after validation: ${identity.path}`);
2371
- }
2372
- }
2373
- async function prepareTargetRoot(projectRoot, targetRoot) {
2374
- const projectIdentity = inspectDirectoryIdentity(projectRoot, "project Skills root");
2375
- const agentsRoot = join9(projectRoot, ".agents");
2376
- const identities = [projectIdentity];
2377
- assertDirectoryIdentities(identities, "project Skills root");
2378
- await mkdir9(agentsRoot).catch((error) => {
2379
- if (error.code !== "EEXIST")
2380
- throw error;
2381
- });
2382
- assertDirectoryIdentities(identities, "project Skills root");
2383
- identities.push(inspectDirectoryIdentity(agentsRoot, "project Skills root"));
2384
- assertDirectoryIdentities(identities, "project Skills root");
2385
- await mkdir9(targetRoot).catch((error) => {
2386
- if (error.code !== "EEXIST")
2387
- throw error;
2388
- });
2389
- assertDirectoryIdentities(identities, "project Skills root");
2390
- identities.push(inspectDirectoryIdentity(targetRoot, "project Skills root"));
2391
- return identities;
2392
- }
2393
- async function inspectPackagedSkills(packageRoot) {
2394
- const skillsRoot = join9(packageRoot, "skills");
2395
- const rootInspection = inspectManagedDirectory(skillsRoot, "packaged Skills root");
2396
- if (rootInspection.issue)
2397
- throw rootInspection.issue;
2398
- const entries = await readdir2(skillsRoot, { withFileTypes: true });
2399
- const skills = [];
2400
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
2401
- if (entry.name.startsWith("."))
2402
- continue;
2403
- const root = join9(skillsRoot, entry.name);
2404
- if (!entry.isDirectory())
2405
- throw unsupportedEntry(root, "packaged Skills");
2406
- const inspection = await inspectManagedFileTree(root, `packaged Skill ${entry.name}`);
2407
- if (inspection.issues.length > 0)
2408
- throw inspection.issues[0];
2409
- skills.push({
2410
- name: entry.name,
2411
- root,
2412
- files: inspection.files.map((path) => relative4(root, path))
2413
- });
2414
- }
2415
- return skills;
2416
- }
2417
- function inspectTarget(path, name) {
2418
- try {
2419
- const stats = lstatSync(path);
2420
- if (stats.isDirectory())
2421
- return "directory";
2422
- throw unsupportedEntry(path, `installed Skill ${name}`);
2423
- } catch (error) {
2424
- if (error.code === "ENOENT")
2425
- return "missing";
2426
- throw error;
2427
- }
2428
- }
2429
- async function treesEqual(source, targetRoot) {
2430
- const target = await inspectManagedFileTree(targetRoot, `installed Skill ${source.name}`);
2431
- if (target.issues.length > 0)
2432
- throw target.issues[0];
2433
- const targetFiles = target.files.map((path) => relative4(targetRoot, path));
2434
- if (source.files.length !== targetFiles.length || source.files.some((path, index) => path !== targetFiles[index]))
2435
- return false;
2436
- for (const path of source.files) {
2437
- const sourcePath = join9(source.root, path);
2438
- const targetPath = join9(targetRoot, path);
2439
- const [sourceStats, targetStats] = [lstatSync(sourcePath), lstatSync(targetPath)];
2440
- if (!sourceStats.isFile() || !targetStats.isFile())
2441
- throw unsupportedEntry(!sourceStats.isFile() ? sourcePath : targetPath, `Skill ${source.name}`);
2442
- if ((sourceStats.mode & 511) !== (targetStats.mode & 511))
2443
- return false;
2444
- const [sourceContent, targetContent] = await Promise.all([readFile11(sourcePath), readFile11(targetPath)]);
2445
- if (!sourceContent.equals(targetContent))
2446
- return false;
2447
- }
2448
- return true;
2449
- }
2450
- async function copySkillTree(source, targetRoot) {
2451
- await mkdir9(targetRoot, { recursive: true });
2452
- for (const path of source.files) {
2453
- const sourcePath = join9(source.root, path);
2454
- const targetPath = join9(targetRoot, path);
2455
- const stats = lstatSync(sourcePath);
2456
- if (!stats.isFile())
2457
- throw unsupportedEntry(sourcePath, `packaged Skill ${source.name}`);
2458
- await mkdir9(dirname4(targetPath), { recursive: true });
2459
- await copyFile(sourcePath, targetPath);
2460
- await chmod(targetPath, stats.mode & 511);
2461
- }
2462
- }
2463
- async function installPackagedSkills(args = {}, options = {}) {
2464
- const packageRoot = options.packageRoot ?? PKG_ROOT;
2465
- const projectRoot = options.projectRoot ?? process.cwd();
2466
- const renamePath = options.renamePath ?? rename3;
2467
- const skills = await inspectPackagedSkills(packageRoot);
2468
- const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
2469
- const result = { installed: [], unchanged: [], replaced: [] };
2470
- const conflicts = [];
2471
- const targetTreeIdentities = /* @__PURE__ */ new Map();
2472
- for (const skill of skills) {
2473
- const target = join9(targetRoot, skill.name);
2474
- if (inspectTarget(target, skill.name) === "missing") {
2475
- result.installed.push(skill.name);
2476
- continue;
2477
- }
2478
- const targetIdentity = inspectDirectoryIdentity(target, `installed Skill ${skill.name}`);
2479
- if (await treesEqual(skill, target)) {
2480
- result.unchanged.push(skill.name);
2481
- continue;
2482
- }
2483
- assertDirectoryIdentities([targetIdentity], `installed Skill ${skill.name}`);
2484
- if (args.force) {
2485
- result.replaced.push(skill.name);
2486
- targetTreeIdentities.set(skill.name, targetIdentity);
2487
- } else {
2488
- conflicts.push(skill.name);
2489
- }
2490
- }
2491
- if (conflicts.length > 0)
2492
- throw new Error(`conflicting packaged Skills: ${conflicts.join(", ")}; rerun with --force to replace only these package-owned directories`);
2493
- if (args.dryRun)
2494
- return result;
2495
- await options.onMutationStep?.({ phase: "before-target-root-mutation", targetRoot });
2496
- const targetIdentities = await prepareTargetRoot(projectRoot, targetRoot);
2497
- const stagingRoot = join9(targetRoot, `.rsp-skills-install-${randomUUID()}`);
2498
- const nextRoot = join9(stagingRoot, "next");
2499
- const previousRoot = join9(stagingRoot, "previous");
2500
- const failedRoot = join9(stagingRoot, "failed");
2501
- const movedPrevious = [];
2502
- const activated = [];
2503
- let removeStaging = true;
2504
- try {
2505
- assertDirectoryIdentities(targetIdentities, "project Skills root");
2506
- await mkdir9(nextRoot, { recursive: true });
2507
- await mkdir9(previousRoot);
2508
- await mkdir9(failedRoot);
2509
- for (const skill of skills) {
2510
- if (result.unchanged.includes(skill.name))
2511
- continue;
2512
- await copySkillTree(skill, join9(nextRoot, skill.name));
2513
- }
2514
- for (const name of result.installed) {
2515
- const target = join9(targetRoot, name);
2516
- if (inspectTarget(target, name) !== "missing")
2517
- throw new ManagedPathError(target, `installed Skill changed after preflight: ${target}`);
2518
- }
2519
- for (const name of result.replaced) {
2520
- assertDirectoryIdentities([targetTreeIdentities.get(name)], `installed Skill ${name}`);
2521
- }
2522
- for (const name of result.replaced) {
2523
- assertDirectoryIdentities(targetIdentities, "project Skills root");
2524
- assertDirectoryIdentities([targetTreeIdentities.get(name)], `installed Skill ${name}`);
2525
- await renamePath(join9(targetRoot, name), join9(previousRoot, name));
2526
- movedPrevious.push(name);
2527
- }
2528
- for (const name of [...result.installed, ...result.replaced].sort()) {
2529
- const target = join9(targetRoot, name);
2530
- assertDirectoryIdentities(targetIdentities, "project Skills root");
2531
- await options.onMutationStep?.({ phase: "before-activate", name, target });
2532
- assertDirectoryIdentities(targetIdentities, "project Skills root");
2533
- if (inspectTarget(target, name) !== "missing")
2534
- throw new ManagedPathError(target, `installed Skill changed after preflight: ${target}`);
2535
- await renamePath(join9(nextRoot, name), target);
2536
- activated.push(name);
2537
- }
2538
- } catch (error) {
2539
- const rollbackErrors = [];
2540
- for (const name of activated.reverse()) {
2541
- const target = join9(targetRoot, name);
2542
- try {
2543
- assertDirectoryIdentities(targetIdentities, "project Skills root");
2544
- if (inspectTarget(target, name) === "directory")
2545
- await renamePath(target, join9(failedRoot, name));
2546
- } catch (rollbackError) {
2547
- rollbackErrors.push(rollbackError instanceof Error ? rollbackError.message : String(rollbackError));
2548
- }
2549
- }
2550
- for (const name of movedPrevious.reverse()) {
2551
- const target = join9(targetRoot, name);
2552
- const previous = join9(previousRoot, name);
2553
- try {
2554
- assertDirectoryIdentities(targetIdentities, "project Skills root");
2555
- await options.onMutationStep?.({ phase: "before-rollback-restore", name, target, previous });
2556
- assertDirectoryIdentities(targetIdentities, "project Skills root");
2557
- if (inspectTarget(target, name) !== "missing")
2558
- throw new ManagedPathError(target, `cannot restore installed Skill because the target is occupied: ${target}`);
2559
- await renamePath(previous, target);
2560
- } catch (rollbackError) {
2561
- rollbackErrors.push(rollbackError instanceof Error ? rollbackError.message : String(rollbackError));
2562
- }
2563
- }
2564
- if (rollbackErrors.length > 0) {
2565
- removeStaging = false;
2566
- const message = error instanceof Error ? error.message : String(error);
2567
- throw new Error(`${message}; rollback incomplete; recover original Skills from ${previousRoot}; ${rollbackErrors.join("; ")}`);
2568
- }
2569
- throw error;
2570
- } finally {
2571
- if (removeStaging)
2572
- await rm2(stagingRoot, { recursive: true, force: true });
2573
- }
2574
- return result;
2575
- }
2576
- function printSkillInstallResult(result, dryRun = false) {
2577
- const prefix = dryRun ? "would be " : "";
2578
- for (const status of ["installed", "unchanged", "replaced"]) {
2579
- if (result[status].length > 0)
2580
- console.log(` ${prefix}${status}: ${result[status].join(", ")}`);
2581
- }
2582
- }
2583
-
2584
2470
  // src/status/derive.ts
2585
2471
  function deriveStatusView(snapshot, options = {}) {
2586
2472
  const query = {
@@ -2601,6 +2487,7 @@ function deriveStatusView(snapshot, options = {}) {
2601
2487
  const ok = snapshot.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
2602
2488
  const nextActions = ok ? buildStatusNextActions(snapshot.focused.length, snapshot.records.map((record) => record.output), snapshot.groups, filtered) : ["Run: rsp doctor"];
2603
2489
  return {
2490
+ manage: snapshot.manage,
2604
2491
  query,
2605
2492
  focused: snapshot.focused,
2606
2493
  records,
@@ -2696,6 +2583,8 @@ function printStatusPlain(view) {
2696
2583
  console.log();
2697
2584
  console.log(` ${pc.bold("RSP status")}`);
2698
2585
  console.log();
2586
+ console.log(` ${pc.bold("Manage:")} activation ${view.manage.activation} \xB7 closeout ${view.manage.closeout}`);
2587
+ console.log();
2699
2588
  for (const diagnostic of view.diagnostics) {
2700
2589
  const label = diagnostic.change ?? diagnostic.path;
2701
2590
  console.log(` ${pc.red("\u2717")} ${label ? `${label} \u2014 ` : ""}${diagnostic.message}`);
@@ -2813,6 +2702,7 @@ function toStatusJson(view) {
2813
2702
  return {
2814
2703
  command: "status",
2815
2704
  ok: view.ok,
2705
+ manage: view.manage,
2816
2706
  filters: view.query,
2817
2707
  focused: view.focused,
2818
2708
  records: view.records.map((record) => record.output),
@@ -2829,6 +2719,7 @@ function toStatusJsonError(error, options) {
2829
2719
  return {
2830
2720
  command: "status",
2831
2721
  ok: false,
2722
+ manage: { activation: "explicit", closeout: "manual" },
2832
2723
  filters: {
2833
2724
  focused: options.focused,
2834
2725
  blocked: options.blocked,
@@ -2878,6 +2769,9 @@ function isInteractiveTerminal(environment) {
2878
2769
  function shouldAutoLaunchUi(args, environment) {
2879
2770
  return args.length === 0 && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
2880
2771
  }
2772
+ function shouldLaunchSkillsUi(args, environment) {
2773
+ return args.length === 1 && args[0] === "skills" && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
2774
+ }
2881
2775
  function validateUiArgs(args) {
2882
2776
  let lang = "auto";
2883
2777
  for (let index = 0; index < args.length; index += 1) {
@@ -2999,7 +2893,7 @@ var groupCommand = defineCommand({
2999
2893
  var addSpecCommand = defineCommand({
3000
2894
  meta: {
3001
2895
  name: "spec",
3002
- description: "Create .rsp/specs/<name>.md and rebuild specs index"
2896
+ description: "Create .rsp/specs/<name>.md and refresh local Specs indexes"
3003
2897
  },
3004
2898
  args: {
3005
2899
  name: {
@@ -3034,12 +2928,42 @@ var archiveCommand = defineCommand({
3034
2928
  },
3035
2929
  "dry-run": {
3036
2930
  type: "boolean",
3037
- description: "Preview archive readiness without moving the change",
2931
+ description: "Deprecated compatibility route to rsp ready; never moves the Change",
3038
2932
  default: false
3039
2933
  }
3040
2934
  },
3041
2935
  async run({ args }) {
3042
- 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 });
3043
2967
  }
3044
2968
  });
3045
2969
  var focusCommand = defineCommand({
@@ -3180,7 +3104,7 @@ var checkCommand = defineCommand({
3180
3104
  var updateCommand = defineCommand({
3181
3105
  meta: {
3182
3106
  name: "update",
3183
- 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"
3184
3108
  },
3185
3109
  async run() {
3186
3110
  const result = await updateProject();
@@ -3194,9 +3118,14 @@ var skillsInstallCommand = defineCommand({
3194
3118
  description: "Install this package's bundled Skills into .agents/skills"
3195
3119
  },
3196
3120
  args: {
3121
+ "name": {
3122
+ type: "positional",
3123
+ description: "Exact packaged Skill name (default: lifecycle suite)",
3124
+ required: false
3125
+ },
3197
3126
  "dry-run": {
3198
3127
  type: "boolean",
3199
- description: "Preflight all packaged Skills without changing files",
3128
+ description: "Preflight selected packaged Skills without changing files",
3200
3129
  default: false
3201
3130
  },
3202
3131
  "force": {
@@ -3206,20 +3135,44 @@ var skillsInstallCommand = defineCommand({
3206
3135
  }
3207
3136
  },
3208
3137
  async run({ args }) {
3138
+ if (args._.length > 1)
3139
+ throw new Error("rsp skills install accepts at most one Skill name");
3209
3140
  const result = await installPackagedSkills({
3210
3141
  dryRun: Boolean(args["dry-run"]),
3211
- force: Boolean(args.force)
3142
+ force: Boolean(args.force),
3143
+ names: args.name ? [args.name] : void 0
3212
3144
  });
3213
3145
  printSkillInstallResult(result, Boolean(args["dry-run"]));
3214
3146
  }
3215
3147
  });
3148
+ var skillsListCommand = defineCommand({
3149
+ meta: {
3150
+ name: "list",
3151
+ description: "List package-bundled Skills and their project status"
3152
+ },
3153
+ args: {
3154
+ json: {
3155
+ type: "boolean",
3156
+ description: "Print machine-readable JSON output",
3157
+ default: false
3158
+ }
3159
+ },
3160
+ async run({ args }) {
3161
+ const inventory = await inspectPackagedSkillInventory();
3162
+ if (args.json)
3163
+ emitJson(inventory);
3164
+ else
3165
+ printPackagedSkillInventory(inventory);
3166
+ }
3167
+ });
3216
3168
  var skillsCommand = defineCommand({
3217
3169
  meta: {
3218
3170
  name: "skills",
3219
3171
  description: "Manage package-bundled project Skills"
3220
3172
  },
3221
3173
  subCommands: {
3222
- install: skillsInstallCommand
3174
+ install: skillsInstallCommand,
3175
+ list: skillsListCommand
3223
3176
  }
3224
3177
  });
3225
3178
  var doctorCommand = defineCommand({
@@ -3358,6 +3311,10 @@ var historyCommand = defineCommand({
3358
3311
  type: "string",
3359
3312
  description: "Match one exact Change Group"
3360
3313
  },
3314
+ search: {
3315
+ type: "string",
3316
+ description: "Case-insensitive literal match over WorkRef and summary"
3317
+ },
3361
3318
  json: {
3362
3319
  type: "boolean",
3363
3320
  description: "Print machine-readable JSON output",
@@ -3377,6 +3334,7 @@ var historyCommand = defineCommand({
3377
3334
  until: args.until,
3378
3335
  kind: args.kind,
3379
3336
  group: args.group,
3337
+ search: args.search,
3380
3338
  positionalCount: args._?.length ?? (args.name ? 1 : 0)
3381
3339
  }, { json: Boolean(args.json), compact: Boolean(args.compact) });
3382
3340
  if (!result.ok)
@@ -3391,6 +3349,19 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3391
3349
  ci: process.env.CI
3392
3350
  };
3393
3351
  const explicitUi = rawArgs[0] === "ui";
3352
+ if (shouldLaunchSkillsUi(rawArgs, terminalEnvironment)) {
3353
+ const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
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);
3355
+ const { runSkillsTui } = await import("./entry-WZDVKDUM.mjs");
3356
+ const selection = await runSkillsTui(locale);
3357
+ if (selection.kind === "confirmed") {
3358
+ const result = await installPackagedSkills({ names: selection.names, force: selection.force });
3359
+ printSkillInstallResult(result);
3360
+ } else if (selection.kind === "error") {
3361
+ process.exitCode = 1;
3362
+ }
3363
+ return;
3364
+ }
3394
3365
  if (explicitUi || shouldAutoLaunchUi(rawArgs, terminalEnvironment)) {
3395
3366
  const { lang } = validateUiArgs(explicitUi ? rawArgs.slice(1) : []);
3396
3367
  if (!isInteractiveTerminal(terminalEnvironment)) {
@@ -3398,8 +3369,8 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3398
3369
  throw new Error("rsp ui requires an interactive terminal; use rsp status or rsp status --json instead");
3399
3370
  } else {
3400
3371
  const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
3401
- const locale = resolveUiLocale(lang, process.env.RSP_UI_LANG, process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? Intl.DateTimeFormat().resolvedOptions().locale);
3402
- const { runTui } = await import("./entry-LAPR3TNI.mjs");
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);
3373
+ const { runTui } = await import("./entry-X23GPCVS.mjs");
3403
3374
  const exitCode = await runTui(locale);
3404
3375
  if (exitCode !== 0)
3405
3376
  process.exitCode = exitCode;
@@ -3422,6 +3393,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3422
3393
  focus: focusCommand,
3423
3394
  unfocus: unfocusCommand,
3424
3395
  archive: archiveCommand,
3396
+ reopen: reopenCommand,
3425
3397
  ready: readyCommand,
3426
3398
  show: showCommand,
3427
3399
  history: historyCommand,