@oevery/rsp 3.3.0 → 4.1.0

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.
Files changed (76) hide show
  1. package/README.md +24 -17
  2. package/README.zh-CN.md +25 -18
  3. package/dist/chunk-JO6NDJM4.mjs +360 -0
  4. package/dist/chunk-LGDCIQYL.mjs +725 -0
  5. package/dist/{chunk-7HXQNBIG.mjs → chunk-P3KO3GZW.mjs} +420 -268
  6. package/dist/chunk-U56XMIOO.mjs +66 -0
  7. package/dist/{chunk-TSUDEX5Y.mjs → chunk-VDRXFKBP.mjs} +142 -867
  8. package/dist/cli.mjs +1111 -2347
  9. package/dist/client-EHRWH5Q7.mjs +108 -0
  10. package/dist/{entry-GIDNR7CJ.mjs → entry-NDASTAI5.mjs} +128 -38
  11. package/dist/viewer/NOTICE.txt +299 -0
  12. package/dist/viewer/app.css +2 -0
  13. package/dist/viewer/app.js +670 -0
  14. package/dist/viewer-build.json +1 -0
  15. package/dist/viewer-service.mjs +551 -0
  16. package/package.json +51 -25
  17. package/skills/rsp/SKILL.md +44 -38
  18. package/skills/rsp/references/architecture.md +39 -0
  19. package/skills/rsp/references/closeout.md +38 -0
  20. package/skills/rsp/references/context-migration.md +22 -0
  21. package/skills/rsp/references/control-outcome.md +3 -29
  22. package/skills/rsp/references/coordination-recovery.md +25 -0
  23. package/skills/rsp/references/coordination-review.md +15 -0
  24. package/skills/rsp/references/coordination.md +59 -0
  25. package/skills/rsp/references/durable-review.md +60 -12
  26. package/skills/rsp/references/focus-continuation.md +12 -4
  27. package/skills/rsp/references/groups-dependencies.md +6 -4
  28. package/skills/rsp/references/response-language.md +2 -0
  29. package/skills/rsp/references/setup-repair.md +42 -7
  30. package/skills/rsp/references/work-model.md +37 -0
  31. package/skills/rsp-commit/SKILL.md +76 -31
  32. package/skills/rsp-commit/references/message.md +34 -0
  33. package/skills/rsp-doc/SKILL.md +68 -0
  34. package/skills/rsp-doc/agents/openai.yaml +3 -0
  35. package/skills/rsp-doc/references/release-communication.md +39 -0
  36. package/skills/rsp-doc/references/skill-authoring.md +36 -0
  37. package/skills/rsp-implement/SKILL.md +60 -25
  38. package/skills/rsp-implement/references/diagnosis.md +10 -0
  39. package/skills/rsp-implement/references/finding-resolution.md +21 -0
  40. package/skills/rsp-implement/references/test-first.md +7 -0
  41. package/skills/rsp-review/SKILL.md +46 -45
  42. package/skills/rsp-review/references/code-review.md +30 -9
  43. package/skills/rsp-review/references/document-review.md +24 -9
  44. package/skills/rsp-review/references/release-communication.md +21 -0
  45. package/skills/rsp-shape/SKILL.md +26 -45
  46. package/skills/rsp-shape/references/complex-shaping.md +3 -1
  47. package/skills/rsp-shape/references/deep-clarification.md +4 -4
  48. package/skills/rsp-shape/references/domain-modeling.md +13 -0
  49. package/skills/rsp-shape/references/module-seams.md +13 -0
  50. package/skills/rsp-shape/references/planning.md +42 -0
  51. package/skills/{rsp-design → rsp-shape}/references/reversible-exploration.md +2 -2
  52. package/skills/rsp-verify/SKILL.md +39 -12
  53. package/dist/chunk-JOMFFQRG.mjs +0 -99
  54. package/dist/chunk-QM2UVPQK.mjs +0 -375
  55. package/dist/entry-YAW3PYFA.mjs +0 -174
  56. package/rules/rsp-rules.md +0 -48
  57. package/skills/rsp/references/implementation-evidence.md +0 -12
  58. package/skills/rsp/references/managed-routing.md +0 -45
  59. package/skills/rsp-design/SKILL.md +0 -53
  60. package/skills/rsp-design/references/domain-modeling.md +0 -11
  61. package/skills/rsp-design/references/module-seams.md +0 -11
  62. package/skills/rsp-diagnose/SKILL.md +0 -48
  63. package/skills/rsp-manage/SKILL.md +0 -101
  64. package/skills/rsp-manage/references/closeout.md +0 -13
  65. package/skills/rsp-manage/references/delegation.md +0 -47
  66. package/skills/rsp-manage/references/interruption-recovery.md +0 -19
  67. package/skills/rsp-manage/references/review-convergence.md +0 -9
  68. package/skills/rsp-release-docs/SKILL.md +0 -77
  69. package/skills/rsp-release-docs/references/convention-discovery.md +0 -65
  70. package/skills/rsp-release-docs/references/evidence-and-surfaces.md +0 -21
  71. package/skills/rsp-release-docs/references/output-contracts.md +0 -97
  72. package/skills/rsp-release-docs/references/publication-lifecycle.md +0 -28
  73. package/skills/rsp-resolve-findings/SKILL.md +0 -61
  74. package/skills/rsp-structural-audit/SKILL.md +0 -71
  75. package/skills/rsp-structural-audit/references/structural-lenses.md +0 -41
  76. package/skills/rsp-tdd/SKILL.md +0 -56
package/dist/cli.mjs CHANGED
@@ -1,10 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- inspectPackagedSkillInventory,
4
- installPackagedSkills,
5
- presentPackagedSkillInventory,
6
- presentSkillInstallResult
7
- } from "./chunk-QM2UVPQK.mjs";
3
+ ARCHITECTURE_FILE,
4
+ PROJECT_VERSION_PATH,
5
+ SOURCE_INVOCATION,
6
+ compareVersions,
7
+ exactInvocation,
8
+ guardProjectVersion,
9
+ isExactVersion,
10
+ queryArchitecture,
11
+ readProjectRuntime,
12
+ readProjectVersion,
13
+ selectProjectRuntime
14
+ } from "./chunk-JO6NDJM4.mjs";
8
15
  import {
9
16
  ArchiveHistoryError,
10
17
  GROUP_BRIEF_FILENAME,
@@ -12,21 +19,13 @@ import {
12
19
  HISTORY_DEFAULT_LIMIT,
13
20
  HISTORY_MAX_LIMIT,
14
21
  IssueRelationshipError,
15
- SPECS_DEFAULT_RESULT_LIMIT,
16
- SPECS_DETAIL_CONTENT_CODE_POINTS,
17
- SPECS_MAX_FILE_BYTES,
18
- SPECS_MAX_RESULT_LIMIT,
19
- SpecsQueryError,
20
22
  WorkRefError,
21
23
  buildDurableReviewGuidance,
22
- cleanupEmptyParentDirs,
23
24
  collectArchiveReadiness,
24
25
  createIssueRelationship,
25
- detectProjectName,
26
26
  generateGroupBriefContent,
27
27
  getDurableReviewCandidateTargets,
28
28
  getGroupReopenEvidence,
29
- guardRspInitialized,
30
29
  hasArchivedGroupBrief,
31
30
  historyInspectionComplete,
32
31
  inspectArchiveHistory,
@@ -35,13 +34,8 @@ import {
35
34
  inspectChangeGroups,
36
35
  inspectFocusTree,
37
36
  inspectProjectStatus,
38
- inspectSpecs,
39
- inspectUnsupportedRules,
40
37
  inspectWorkTree,
41
- isRecognizedGeneratedSpecsIndex,
42
- isValidSpecName,
43
38
  normalizeExecutableWorkRef,
44
- normalizeLogicalPath,
45
39
  normalizeWorkRefSegment,
46
40
  parseIssueRelationships,
47
41
  projectDependencyForest,
@@ -56,10 +50,27 @@ import {
56
50
  resolveWorkRef,
57
51
  selectArchiveHistoryRecord,
58
52
  selectArchivedGroupBrief,
53
+ toArchiveReadinessOutput
54
+ } from "./chunk-VDRXFKBP.mjs";
55
+ import {
56
+ SPECS_DEFAULT_RESULT_LIMIT,
57
+ SPECS_DETAIL_CONTENT_CODE_POINTS,
58
+ SPECS_MAX_RESULT_LIMIT,
59
+ SpecsQueryError,
60
+ cleanupEmptyParentDirs,
61
+ detectProjectName,
62
+ emitJson,
63
+ guardRspInitialized,
64
+ inspectRspInitialization,
65
+ inspectSpecs,
66
+ inspectUnsupportedRules,
67
+ isRecognizedGeneratedSpecsIndex,
68
+ isValidSpecName,
69
+ normalizeLogicalPath,
59
70
  specsInspectionComplete,
60
- toArchiveReadinessOutput,
71
+ toErrorMessage,
61
72
  walkMarkdownFiles
62
- } from "./chunk-TSUDEX5Y.mjs";
73
+ } from "./chunk-LGDCIQYL.mjs";
63
74
  import {
64
75
  ARCHIVES_DIR,
65
76
  CHANGES_DIR,
@@ -80,46 +91,51 @@ import {
80
91
  clearConfigCache,
81
92
  countCheckboxes,
82
93
  detectDeltaSections,
83
- emitJson,
84
- ensureDecisionRecordsDirectory,
85
94
  ensureManagedFile,
86
95
  generateConfigTemplate,
87
- getDocumentSectionBody,
88
96
  getDocumentSectionDefinitionByHeading,
97
+ getDocumentSectionMarkdown,
89
98
  getDocumentSections,
90
99
  getDocumentTitles,
91
100
  getVersion,
101
+ getVisibleBodyLines,
92
102
  hasMeaningfulBlockers,
93
103
  inspectManagedDirectory,
94
104
  inspectManagedFile,
95
105
  inspectManagedFileTree,
96
106
  inspectRspConfig,
97
107
  loadRspConfig,
108
+ markdownProse,
109
+ markdownText,
110
+ markdownTopLevelComments,
98
111
  parseFrontmatter,
112
+ parseMarkdown,
99
113
  parseRspDocument,
100
114
  parseScenarios,
101
115
  pc,
102
- reconcileRspConfigDefaults,
103
116
  renderDocumentSectionHeading,
104
117
  renderDocumentTitle,
105
118
  requireManagedDirectory,
106
119
  requireManagedFile,
107
120
  resolveDecisionRecordsPath,
108
121
  resolveKinds,
122
+ resolveLanguagePolicy,
109
123
  resolveManagedDirectoryChain,
110
124
  resolveRequiredSections,
111
- toErrorMessage,
125
+ resolveWorkflowPolicy,
112
126
  validateDecisionRecordsFilesystemPath,
113
127
  writeManagedFile
114
- } from "./chunk-7HXQNBIG.mjs";
128
+ } from "./chunk-P3KO3GZW.mjs";
115
129
 
116
130
  // src/cli.ts
117
- import { resolve as resolve4 } from "path";
131
+ import { resolve as resolve2 } from "path";
118
132
  import { fileURLToPath } from "url";
119
133
  import { runMain } from "citty";
120
134
 
121
135
  // src/cli/capabilities.ts
122
136
  var CLI_COMMAND_CAPABILITIES = {
137
+ "architecture": { json: true, compact: true },
138
+ "config": { json: true, compact: true },
123
139
  "status": { json: true, compact: true },
124
140
  "show": { json: true, compact: true },
125
141
  "ready": { json: true, compact: true },
@@ -152,10 +168,49 @@ function supportsCompact(command) {
152
168
  }
153
169
 
154
170
  // src/cli/registry.ts
155
- import { defineCommand as defineCommand5 } from "citty";
171
+ import { defineCommand as defineCommand6 } from "citty";
156
172
 
157
- // src/cli/commands/inspection.ts
173
+ // src/cli/commands/architecture.ts
158
174
  import { defineCommand } from "citty";
175
+ var architectureCommand = defineCommand({
176
+ meta: { name: "architecture", description: "Read full or selected architecture structure without document bodies" },
177
+ args: { node: { type: "string", description: "Node and descendants" }, view: { type: "string", description: "Explicit view selection" }, ...compactJsonArgs },
178
+ async run({ args }) {
179
+ const unknown = Object.keys(args).filter((key) => !["node", "view", "json", "compact", "_"].includes(key));
180
+ if (unknown.length || args._.length)
181
+ throw new Error(`Unsupported architecture options/arguments: ${[...unknown, ...args._].join(", ")}`);
182
+ const result = await queryArchitecture({ node: args.node, view: args.view });
183
+ if (args.json) {
184
+ console.log(JSON.stringify(result, null, args.compact ? void 0 : 2));
185
+ } else {
186
+ console.log(`Architecture: ${result.configured ? result.source.root : "not configured"}
187
+ Selection: ${result.selection.kind}${result.selection.id ? ` ${result.selection.id}` : ""}`);
188
+ for (const node of result.nodes) {
189
+ console.log(`${node.boundary ? "[boundary] " : ""}${node.id}: ${node.title} \u2014 ${node.description}${node.parent ? ` (parent: ${node.parent})` : ""}`);
190
+ for (const binding of node.bindings) console.log(` ${binding.file}${binding.headings ? ` > ${binding.headings.join(" > ")}` : ""} (scope: ${binding.scope}; ${binding.inherited ? `inherited from ${binding.origin}` : `direct on ${binding.origin}`})`);
191
+ }
192
+ for (const edge of result.relations) console.log(`Relation ${edge.id}: ${edge.source} -> ${edge.target} [${edge.type}]: ${edge.description}`);
193
+ for (const view of result.views) console.log(`View ${view.id}: ${view.title} (owner: ${view.node}; include: ${view.include.join(", ")})`);
194
+ for (const diagnostic of result.diagnostics) console.log(`${diagnostic.code}: ${diagnostic.message}`);
195
+ if (result.configured)
196
+ console.log(`Source: ${result.source.file} @ ${result.source.revision}`);
197
+ }
198
+ }
199
+ });
200
+ var viewCommand = defineCommand({
201
+ meta: { name: "view", description: "Open the shared local architecture viewer, inspect or stop its service" },
202
+ args: { action: { type: "positional", required: false, description: "status or stop; omit to open" }, open: { type: "boolean", default: true, description: "Open browser (use --no-open to print URL only)" } },
203
+ async run({ args }) {
204
+ const unknown = Object.keys(args).filter((key) => !["action", "open", "_"].includes(key));
205
+ if (unknown.length || args._.length > 1 || args.action && !args.open)
206
+ throw new Error("rsp view accepts only an optional status/stop action or --no-open");
207
+ const { runView } = await import("./client-EHRWH5Q7.mjs");
208
+ await runView(args.action, args.open);
209
+ }
210
+ });
211
+
212
+ // src/cli/commands/inspection.ts
213
+ import { defineCommand as defineCommand2 } from "citty";
159
214
 
160
215
  // src/commands/check.ts
161
216
  import { readFile } from "fs/promises";
@@ -164,9 +219,10 @@ import { readFile } from "fs/promises";
164
219
  var choosePlaceholderRe = /^<choose:/i;
165
220
  function inspectChangeDocument(content, options) {
166
221
  const diagnostics = [];
222
+ const document = typeof content === "string" ? parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA) : content;
167
223
  let frontmatter = null;
168
224
  try {
169
- frontmatter = parseFrontmatter(content);
225
+ frontmatter = parseFrontmatter(document.markdown);
170
226
  } catch (error) {
171
227
  const message = error instanceof Error ? error.message : String(error);
172
228
  diagnostics.push({
@@ -182,7 +238,6 @@ function inspectChangeDocument(content, options) {
182
238
  message: "missing YAML frontmatter"
183
239
  });
184
240
  }
185
- const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
186
241
  for (const heading of options.requiredSections) {
187
242
  const definition = getDocumentSectionDefinitionByHeading(CHANGE_DOCUMENT_SCHEMA, heading);
188
243
  if (!definition)
@@ -257,7 +312,6 @@ function inspectChangeDocument(content, options) {
257
312
  }
258
313
 
259
314
  // src/core/focus-capsule.ts
260
- import { Buffer } from "buffer";
261
315
  import { TextDecoder } from "util";
262
316
  var FOCUS_CAPSULE_VERSION = "<!-- rsp-focus:v1 -->";
263
317
  var LEGACY_WARNING = "legacy focus capsule has no structured recovery projection";
@@ -275,15 +329,6 @@ function inspectFocusCapsuleBytes(bytes) {
275
329
  } catch {
276
330
  return { kind: "invalid", code: "focus_capsule_invalid_utf8", message: "focus capsule must contain valid UTF-8" };
277
331
  }
278
- if (Buffer.byteLength(content, "utf-8") > MAX_FOCUS_CAPSULE_BYTES) {
279
- return {
280
- kind: "invalid",
281
- code: "focus_capsule_too_large",
282
- message: `focus capsule exceeds ${MAX_FOCUS_CAPSULE_BYTES} UTF-8 bytes`
283
- };
284
- }
285
- if (content.length === 0)
286
- return { kind: "empty", content: "" };
287
332
  return parseFocusCapsule(content);
288
333
  }
289
334
  function parseFocusCapsule(content) {
@@ -481,9 +526,10 @@ async function runCheck(options = {}) {
481
526
  });
482
527
  continue;
483
528
  }
484
- for (const diagnostic of inspectChangeDocument(content, { name, validKinds, requiredSections }))
529
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
530
+ for (const diagnostic of inspectChangeDocument(document, { name, validKinds, requiredSections }))
485
531
  addDiagnostic({ ...diagnostic, change: name, path: fp });
486
- const deltas = detectDeltaSections(content);
532
+ const deltas = detectDeltaSections(document);
487
533
  if (deltas.added || deltas.modified || deltas.removed) {
488
534
  const parts = [];
489
535
  if (deltas.added)
@@ -500,7 +546,7 @@ async function runCheck(options = {}) {
500
546
  message: `delta markers found: ${parts.join(", ")}`
501
547
  });
502
548
  }
503
- const placeholderLines = findUnfinishedTemplateLines(content);
549
+ const placeholderLines = findUnfinishedTemplateLines(document.markdown);
504
550
  if (placeholderLines.length > 0) {
505
551
  addDiagnostic({
506
552
  severity: "warning",
@@ -512,7 +558,7 @@ async function runCheck(options = {}) {
512
558
  hint: "Replace angle-bracket template text with concrete change details before archiving."
513
559
  });
514
560
  }
515
- const clarificationLines = findUnresolvedClarificationLines(content);
561
+ const clarificationLines = findUnresolvedClarificationLines(document.markdown);
516
562
  if (clarificationLines.length > 0) {
517
563
  addDiagnostic({
518
564
  severity: "warning",
@@ -524,7 +570,7 @@ async function runCheck(options = {}) {
524
570
  hint: "Resolve or remove clarification markers once the open question is answered."
525
571
  });
526
572
  }
527
- const scenarios = parseScenarios(content);
573
+ const scenarios = parseScenarios(document.markdown);
528
574
  if (scenarios.length > 0) {
529
575
  const scenarioIssues = [];
530
576
  for (const s of scenarios) {
@@ -569,32 +615,77 @@ async function runCheck(options = {}) {
569
615
  }
570
616
  function findUnfinishedTemplateLines(content) {
571
617
  const placeholderRe = /<(?:…|[^>\n]*(?:choose:|one-line|what |why |which |who |how |new |updated |expected |actual |specific |concrete |exact |verifiable |user |capability |behavior |context |action|path|directory|module|subsystem|command|scenario|constraint|requirement|question|uncertainty|implementation)[^>\n]*)>/i;
572
- return collectMatchingLines(content, (line) => {
573
- const trimmed = stripInlineCodeSpans(line).trim();
574
- return trimmed.startsWith("<!--") === false && placeholderRe.test(trimmed);
575
- });
618
+ return collectMatchingLines(content, (value) => placeholderRe.test(value));
576
619
  }
577
620
  function findUnresolvedClarificationLines(content) {
578
- return collectMatchingLines(content, (line) => /\[(?:NEEDS|TODO|TBD)\s+CLARIFICATION(?::[^\]]*)?\]/i.test(stripInlineCodeSpans(line)));
579
- }
580
- function stripInlineCodeSpans(line) {
581
- return line.replace(/`[^`]*`/g, "");
621
+ return collectMatchingLines(content, (value) => /\[(?:NEEDS|TODO|TBD)\s+CLARIFICATION(?::[^\]]*)?\]/i.test(value));
582
622
  }
583
623
  function collectMatchingLines(content, predicate) {
584
- const matches = [];
585
- const lines = content.split("\n");
586
- for (let index = 0; index < lines.length; index++) {
587
- const line = lines[index];
588
- if (predicate(line))
589
- matches.push(`line ${index + 1}: ${line.trim()}`);
624
+ const document = parseMarkdown(content);
625
+ const sourceLines = document.source.split(/\r?\n/);
626
+ const matched = /* @__PURE__ */ new Set();
627
+ for (const fragment of markdownProse(document)) {
628
+ fragment.value.split(/\r?\n/).forEach((value, index) => {
629
+ if (predicate(value))
630
+ matched.add(fragment.line + index);
631
+ });
632
+ }
633
+ return [...matched].sort((a, b) => a - b).slice(0, 10).map((line) => `line ${line}: ${sourceLines[line - 1].trim()}`);
634
+ }
635
+
636
+ // src/commands/config.ts
637
+ async function showConfig() {
638
+ try {
639
+ const inspection = await inspectRspConfig();
640
+ if (inspection.issues.length > 0) {
641
+ return {
642
+ command: "config",
643
+ ok: false,
644
+ path: CONFIG_PATH,
645
+ summary: null,
646
+ diagnostics: [{
647
+ severity: "error",
648
+ code: "invalid_config",
649
+ path: CONFIG_PATH,
650
+ message: inspection.issues.join("; ")
651
+ }],
652
+ runtime: []
653
+ };
654
+ }
655
+ return {
656
+ command: "config",
657
+ ok: true,
658
+ path: CONFIG_PATH,
659
+ summary: {
660
+ kinds: resolveKinds(inspection.config),
661
+ decisions: { path: resolveDecisionRecordsPath(inspection.config) },
662
+ workflow: resolveWorkflowPolicy(inspection.config, { configValid: true }),
663
+ language: resolveLanguagePolicy(inspection.config, { configValid: true })
664
+ },
665
+ diagnostics: [],
666
+ runtime: []
667
+ };
668
+ } catch (error) {
669
+ return {
670
+ command: "config",
671
+ ok: false,
672
+ path: CONFIG_PATH,
673
+ summary: null,
674
+ diagnostics: [{
675
+ severity: "error",
676
+ code: "invalid_config",
677
+ path: CONFIG_PATH,
678
+ message: toErrorMessage(error)
679
+ }],
680
+ runtime: []
681
+ };
590
682
  }
591
- return matches.slice(0, 10);
592
683
  }
593
684
 
594
685
  // src/commands/doctor.ts
595
- import { existsSync as existsSync2 } from "fs";
596
- import { readdir as readdir2, readFile as readFile5, stat } from "fs/promises";
597
- import { basename, join as join4, relative as relative4 } from "path";
686
+ import { existsSync } from "fs";
687
+ import { readdir as readdir2, readFile as readFile3, stat } from "fs/promises";
688
+ import { basename, join as join2, relative as relative2 } from "path";
598
689
 
599
690
  // src/core/artifacts.ts
600
691
  var RSP_AGENTS_BEGIN = "<!-- rsp:begin -->";
@@ -747,62 +838,88 @@ function getChangeTemplateByKind(kind) {
747
838
  };
748
839
  }
749
840
  function changeSectionHeading(sectionId) {
750
- return renderDocumentSectionHeading(CHANGE_DOCUMENT_SCHEMA, sectionId);
841
+ const guidance = {
842
+ proposal: "State the intended outcome, why it matters, scope, and non-goals; do not narrate execution.",
843
+ spec: "Describe the contract delta and acceptance conditions, referencing affected existing Specs; do not copy the full baseline or implementation steps.",
844
+ design: "Explain the current approach, boundaries, and necessary tradeoffs; replace obsolete plans while preserving decision reasons and useful outcome comparisons.",
845
+ tasks: "Keep current checkable work and actual completion; revise or reopen invalidated tasks within authorized scope. Do not append routine attempts, fixes, retries, or check runs.",
846
+ verify: "Keep required/optional checks and only details needed to judge acceptance, evidence applicability, or locate necessary evidence. Do not repeat check results or retain commands, counts, or review steps merely because they occurred. Preserve applicable coverage, gaps, and distinct review/human acceptance; no run history.",
847
+ blockers: "Name unresolved decisions or dependencies that prevent progress; use none when there are no blockers."
848
+ };
849
+ return `${renderDocumentSectionHeading(CHANGE_DOCUMENT_SCHEMA, sectionId)}
850
+ <!-- ${guidance[sectionId]} -->`;
751
851
  }
752
- function renderRspAgentsBlock() {
852
+ function renderRspAgentsBlock(runtime) {
853
+ const { version } = runtime;
854
+ const selection = runtime.mode === "source" ? `This RSP development checkout explicitly selects source mode through \`.rsp/source\`. Build the current source with \`mise exec -- pnpm run build\`, then use \`${SOURCE_INVOCATION} <command>\` from this checkout for every CLI operation. Skill examples use \`rsp\` as shorthand for this local invocation. Do not substitute a registry package, global CLI or another checkout, even when its version matches. Package version metadata is not source identity; retain the commit, uncommitted diff and build inputs for candidate evidence. Authorized \`update\` reconciles this checkout's managed environment from its local source and keeps source mode; it does not select a published version.` : `This project pins RSP \`${version}\` in \`.rsp/version\`. For every CLI operation use \`${exactInvocation(version)} <command>\` (or a verified executable of that exact version). Skill examples use \`rsp\` as shorthand for this selected invocation. Never substitute \`@latest\`, a version range, or an unverified global CLI. Package fetching requires authority; if unavailable, report the gap. Only an authorized upgrade uses a deliberately selected target CLI with \`update\`; it updates the complete managed environment. Notices grant no upgrade authority.`;
753
855
  return `${RSP_AGENTS_BEGIN}
754
856
  ## RSP Entry
755
857
 
756
858
  RSP tracks current work, stable specs, and archives under \`.rsp/\`.
757
859
 
758
- Read in order:
860
+ Use supplied or confirmed paths; discover only missing paths or applicable ancestor instructions, without overlapping whole-tree scans:
759
861
  1. Nearest \`AGENTS.md\` for project or module instructions.
760
- 2. Root \`CONTEXT-MAP.md\` if present, then the relevant nearest \`CONTEXT.md\`.
761
- 3. Use the project \`rsp\` Skill at \`.agents/skills/rsp/SKILL.md\`; hosts may load it through Skill discovery or read it directly. Only when it is absent or cannot be used, read \`.rsp/rsp-rules.md\` as the fallback protocol.
762
- 4. \`.rsp/focus.d/\`; marker paths select work, while optional bounded Markdown content is recovery guidance only. For grouped work read the sibling Group Brief, then the explicitly selected focused Change.
763
- 5. Only the relevant Specs and Decision Records under the configured authoritative path.
862
+ 2. Root \`CONTEXT.md\` if present, then the relevant nearest \`CONTEXT.md\` for domain language, relationships, and navigation. If a legacy \`CONTEXT-MAP.md\` remains, retain its relevant context and use Core's context-migration branch; discovery alone does not authorize migration.
863
+ 3. Establish the request's scope, authority and existing owner. A clear bounded request selecting one capability with sufficient owner facts loads that project Skill directly from \`.agents/skills/\`, without a Core or Focus preflight. For tracked work read the selected Change; for grouped work read its sibling Group Brief first. Read only relevant Specs and Decision Records under the configured authoritative path.
864
+ 4. Load \`rsp\` at \`.agents/skills/rsp/SKILL.md\` for unresolved routing or ownership, actual cross-capability coordination, or lifecycle operations. Read \`.rsp/focus.d/\` only when needed to resolve or recover the owner; marker paths are open-work candidates and optional bounded Markdown is recovery guidance, not authority. An explicit WorkRef or the user's mentioned Change selects first; otherwise use current status and dependency evidence.
764
865
 
765
- If \`.rsp/focus.d/\` is empty and the user has not provided a concrete task, ask what to work on or suggest \`npx -y @oevery/rsp create <name>\` for tracked work.
866
+ Direct capability invocation does not make tracked work untracked. Preserve existing Changes, required tracking prerequisites, Group dependencies, independent review and permission boundaries; missing prerequisites stop dependent work. Skills are the only workflow: hosts may discover them or read them directly, but a missing or unusable required Skill stops dependent work and must be reported as an incomplete installation. Do not use retired Rules or install implicitly.
766
867
  Do not treat \`.rsp/specs/\` or \`.rsp/changes/\` as replacements for nearest \`AGENTS.md\` or \`CONTEXT.md\`.
868
+
869
+ ${selection}
767
870
  ${RSP_AGENTS_END}`;
768
871
  }
769
- function upsertRspAgentsBlock(content) {
770
- const block = renderRspAgentsBlock();
771
- const managedRe = /<!-- rsp:begin -->[\s\S]*?<!-- rsp:end -->/;
772
- if (managedRe.test(content)) {
773
- const next2 = content.replace(managedRe, block);
872
+ function upsertRspAgentsBlock(content, runtime) {
873
+ const block = renderRspAgentsBlock(runtime);
874
+ const span = rspAgentsBlockSpan(content);
875
+ if (span) {
876
+ const next2 = content.slice(0, span.start) + block + content.slice(span.end);
774
877
  return { content: next2, changed: next2 !== content };
775
878
  }
776
- const trimmed = content.trimStart();
777
- const next = trimmed ? `${block}
879
+ const next = content ? `${block}
778
880
 
779
- ${trimmed}` : `${block}
881
+ ${content}` : `${block}
780
882
  `;
781
883
  return { content: next, changed: true };
782
884
  }
783
885
  function hasRspAgentsBlock(content) {
784
- return content.includes(RSP_AGENTS_BEGIN) && content.includes(RSP_AGENTS_END);
886
+ return rspAgentsBlockSpan(content) !== null;
887
+ }
888
+ function rspAgentsBlockSpan(content) {
889
+ let start;
890
+ for (const comment of markdownTopLevelComments(content)) {
891
+ if (comment.source === RSP_AGENTS_BEGIN && start === void 0)
892
+ start = comment.start;
893
+ if (comment.source === RSP_AGENTS_END && start !== void 0)
894
+ return { start, end: comment.end };
895
+ }
896
+ return null;
785
897
  }
786
898
  function generateDesignContent(projectName) {
787
899
  const placeholder = "<\u2026>";
788
900
  return `# Project Design: ${projectName}
789
901
 
790
902
  ## Purpose
791
- - ${placeholder}
792
-
793
- ## Stable Facts
903
+ <!-- Define the system's purpose and consumers, not the current change. -->
794
904
  - ${placeholder}
795
905
 
796
906
  ## Boundaries
907
+ <!-- Define system responsibilities, exclusions, and responsibility handoffs. -->
797
908
  - In scope:
798
909
  - ${placeholder}
799
910
  - Out of scope:
800
911
  - ${placeholder}
801
912
 
913
+ ## Contracts
914
+ <!-- State current architectural obligations and invariants that must survive implementation changes. -->
915
+ - ${placeholder}
916
+
802
917
  ## Structure
918
+ <!-- Describe stable ownership and dependency direction; leave repository navigation to CONTEXT.md. -->
803
919
  - ${placeholder}
804
920
 
805
921
  ## Constraints
922
+ <!-- Record necessary technical, security, protocol, or compatibility limits; link lasting rationale rather than duplicating it. -->
806
923
  - ${placeholder}
807
924
  `;
808
925
  }
@@ -812,1589 +929,310 @@ function generateSpecContent(name) {
812
929
  return `# ${title}
813
930
 
814
931
  ## Purpose
815
- - ${placeholder}
816
-
817
- ## Stable Facts
932
+ <!-- Define the capability, its consumers, and its value; do not describe this change. -->
818
933
  - ${placeholder}
819
934
 
820
935
  ## Boundaries
936
+ <!-- Define responsibilities, exclusions, and handoffs to other capabilities. -->
821
937
  - In scope:
822
938
  - ${placeholder}
823
939
  - Out of scope:
824
940
  - ${placeholder}
825
941
 
826
- ## Constraints
942
+ ## Contracts
943
+ <!-- State current behavior, inputs and outputs, invariants, and failure semantics by capability, not internal implementation steps. -->
827
944
  - ${placeholder}
828
- `;
829
- }
830
-
831
- // src/commands/update.ts
832
- import { existsSync } from "fs";
833
- import { mkdir as mkdir3, readFile as readFile4, rm, rmdir as rmdir2 } from "fs/promises";
834
- import { join as join3 } from "path";
835
-
836
- // src/core/lock.ts
837
- import { mkdir, readFile as readFile2, unlink, writeFile } from "fs/promises";
838
- async function withRspLock(operation, fn) {
839
- requireManagedDirectory(RSP_DIR, "RSP root", { allowMissing: true });
840
- await mkdir(RSP_DIR, { recursive: true });
841
- try {
842
- await writeFile(LOCK_PATH, `${process.pid}
843
- ${operation}
844
- ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
845
- } catch (err) {
846
- if (err.code === "EEXIST")
847
- await handleStaleLock(operation);
848
- else
849
- throw err;
850
- }
851
- try {
852
- return await fn();
853
- } finally {
854
- try {
855
- await unlink(LOCK_PATH);
856
- } catch {
857
- }
858
- }
859
- }
860
- async function handleStaleLock(operation) {
861
- try {
862
- const content = await readFile2(LOCK_PATH, "utf-8");
863
- const lines = content.trim().split("\n");
864
- const pid = Number(lines[0]);
865
- if (pid && Number.isFinite(pid)) {
866
- try {
867
- process.kill(pid, 0);
868
- const op = lines[1] || "unknown";
869
- const ts = lines[2] || "unknown";
870
- throw new Error(`RSP locked by pid ${pid} (${op}) since ${ts}`);
871
- } catch (e) {
872
- if (e instanceof Error && e.message.startsWith("RSP locked"))
873
- throw e;
874
- const code = e.code;
875
- if (code === "ESRCH") {
876
- await unlink(LOCK_PATH);
877
- await writeFile(LOCK_PATH, `${process.pid}
878
- ${operation}
879
- ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
880
- return;
881
- }
882
- throw e;
883
- }
884
- } else {
885
- await unlink(LOCK_PATH);
886
- await writeFile(LOCK_PATH, `${process.pid}
887
- ${operation}
888
- ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
889
- }
890
- } catch (e) {
891
- if (e instanceof Error && e.message.startsWith("RSP locked"))
892
- throw e;
893
- throw new Error(`RSP is locked. Remove ${LOCK_PATH} manually if no other process is running.`);
894
- }
895
- }
896
-
897
- // src/commands/archive-index-migration.ts
898
- import { readFile as readFile3, unlink as unlink2 } from "fs/promises";
899
- import { join } from "path";
900
- async function removeLegacyArchiveIndex() {
901
- const path = join(ARCHIVES_DIR, "INDEX.md");
902
- const inspection = inspectManagedFile(path, "legacy archive index", { allowMissing: true });
903
- if (!inspection.exists || inspection.issue)
904
- return false;
905
- let frontmatter;
906
- try {
907
- frontmatter = parseFrontmatter(await readFile3(path, "utf-8"));
908
- } catch {
909
- return false;
910
- }
911
- if (frontmatter?.kind !== "generated-index" || frontmatter.index_type !== "archives")
912
- return false;
913
- await unlink2(path);
914
- return true;
915
- }
916
-
917
- // src/commands/specs-index-migration.ts
918
- import { Buffer as Buffer2 } from "buffer";
919
- import { createHash, randomUUID } from "crypto";
920
- import { constants } from "fs";
921
- import { link, lstat as lstat2, open, readdir, realpath as realpath2, rename, unlink as unlink3 } from "fs/promises";
922
- import { dirname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
923
-
924
- // src/core/path-identity.ts
925
- import { lstat, realpath } from "fs/promises";
926
- import { isAbsolute, relative, resolve, sep } from "path";
927
- var StablePathIdentityError = class extends Error {
928
- constructor(code, message) {
929
- super(message);
930
- this.code = code;
931
- this.name = "StablePathIdentityError";
932
- }
933
- code;
934
- };
935
- async function captureStableDirectoryChain(options) {
936
- const rootPath = resolve(options.rootPath);
937
- const targetPath = resolve(options.targetPath);
938
- const requiredPath = options.requiredPath === void 0 ? null : resolve(options.requiredPath);
939
- const targetRelative = containedRelative(rootPath, targetPath);
940
- if (targetRelative === null) {
941
- throw new StablePathIdentityError(
942
- "stable_path_outside_root",
943
- `${options.label} target must stay inside ${rootPath}: ${targetPath}`
944
- );
945
- }
946
- if (requiredPath !== null) {
947
- if (containedRelative(rootPath, requiredPath) === null || containedRelative(requiredPath, targetPath) === null) {
948
- throw new StablePathIdentityError(
949
- "stable_path_outside_required_root",
950
- `${options.label} target must stay inside ${requiredPath}: ${targetPath}`
951
- );
952
- }
953
- }
954
- const logicalPaths = [rootPath];
955
- if (targetRelative !== "") {
956
- let current = rootPath;
957
- for (const segment of targetRelative.split(sep)) {
958
- current = resolve(current, segment);
959
- logicalPaths.push(current);
960
- }
961
- }
962
- const entries = [];
963
- let rootRealPath = null;
964
- let requiredRealPath = null;
965
- for (const path of logicalPaths) {
966
- let value;
967
- let resolvedPath;
968
- try {
969
- value = await lstat(path, { bigint: true });
970
- resolvedPath = await realpath(path);
971
- } catch (error) {
972
- throw new StablePathIdentityError(
973
- "stable_path_inspection_failed",
974
- `${options.label} parent chain could not be inspected at ${path}: ${errorMessage(error)}`
975
- );
976
- }
977
- if (!value.isDirectory() || value.isSymbolicLink()) {
978
- throw new StablePathIdentityError(
979
- "stable_path_not_real_directory",
980
- `${options.label} parent chain must contain only real directories: ${path}`
981
- );
982
- }
983
- rootRealPath ??= resolvedPath;
984
- if (containedRelative(rootRealPath, resolvedPath) === null) {
985
- throw new StablePathIdentityError(
986
- "stable_path_real_containment_failed",
987
- `${options.label} parent chain resolves outside ${rootRealPath}: ${path}`
988
- );
989
- }
990
- if (requiredPath === path)
991
- requiredRealPath = resolvedPath;
992
- entries.push({
993
- path,
994
- realPath: resolvedPath,
995
- device: value.dev,
996
- inode: value.ino
997
- });
998
- }
999
- if (requiredPath !== null) {
1000
- const target = entries.at(-1);
1001
- if (requiredRealPath === null || containedRelative(requiredRealPath, target.realPath) === null) {
1002
- throw new StablePathIdentityError(
1003
- "stable_path_required_containment_failed",
1004
- `${options.label} parent chain no longer resolves inside ${requiredPath}`
1005
- );
1006
- }
1007
- }
1008
- return {
1009
- rootPath,
1010
- targetPath,
1011
- requiredPath,
1012
- entries
1013
- };
1014
- }
1015
- async function assertStableDirectoryChain(expected, label) {
1016
- const current = await captureStableDirectoryChain({
1017
- rootPath: expected.rootPath,
1018
- targetPath: expected.targetPath,
1019
- requiredPath: expected.requiredPath ?? void 0,
1020
- label
1021
- });
1022
- if (current.entries.length !== expected.entries.length)
1023
- throw changed(label, expected.targetPath);
1024
- for (let index = 0; index < expected.entries.length; index += 1) {
1025
- const before = expected.entries[index];
1026
- const after = current.entries[index];
1027
- if (before.path !== after.path || before.realPath !== after.realPath || before.device !== after.device || before.inode !== after.inode) {
1028
- throw changed(label, before.path);
1029
- }
1030
- }
1031
- }
1032
- function containedRelative(rootPath, candidatePath) {
1033
- const candidate = relative(rootPath, candidatePath);
1034
- if (candidate === "" || candidate !== ".." && !candidate.startsWith(`..${sep}`) && !isAbsolute(candidate)) {
1035
- return candidate;
1036
- }
1037
- return null;
1038
- }
1039
- function changed(label, path) {
1040
- return new StablePathIdentityError(
1041
- "stable_path_identity_changed",
1042
- `${label} parent identity changed before the filesystem operation: ${path}`
1043
- );
1044
- }
1045
- function errorMessage(error) {
1046
- return error instanceof Error ? error.message : String(error);
1047
- }
1048
-
1049
- // src/commands/specs-index-migration.ts
1050
- var READ_NO_FOLLOW_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
1051
- var WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
1052
- var DEFAULT_FILE_ADAPTER = { link };
1053
- var GeneratedSpecsIndexMigrationError = class extends Error {
1054
- constructor(code, message, recoveryPaths = [], retainedMutations = []) {
1055
- super(message);
1056
- this.code = code;
1057
- this.recoveryPaths = recoveryPaths;
1058
- this.retainedMutations = retainedMutations;
1059
- this.name = "GeneratedSpecsIndexMigrationError";
1060
- }
1061
- code;
1062
- recoveryPaths;
1063
- retainedMutations;
1064
- };
1065
- async function inspectGeneratedSpecsIndexMigration(cwd = process.cwd()) {
1066
- const inspection = await inspectSpecs({
1067
- cwd,
1068
- allowMissingDecisionRecords: true
1069
- });
1070
- const removable = inspection.generatedIndexes.filter((index) => index.classification === "safe-removal").map((index) => index.path).sort();
1071
- const ownerControlled = inspection.generatedIndexes.filter((index) => index.classification === "owner-controlled").map((index) => index.path).sort();
1072
- return {
1073
- inspectionComplete: specsInspectionComplete(inspection),
1074
- removable,
1075
- ownerControlled,
1076
- diagnostics: inspection.diagnostics.map((diagnostic) => `${diagnostic.path ?? ".rsp/specs"}: ${diagnostic.message}`)
1077
- };
1078
- }
1079
- async function removeRecognizedGeneratedSpecsIndexes(options = {}) {
1080
- const cwd = await realpath2(resolve2(options.cwd ?? process.cwd()));
1081
- const fileAdapter = options.fileAdapter ?? DEFAULT_FILE_ADAPTER;
1082
- const plan = await inspectGeneratedSpecsIndexMigration(cwd);
1083
- if (!plan.inspectionComplete || plan.ownerControlled.length > 0) {
1084
- const ownerMessage = plan.ownerControlled.length > 0 ? ` Owner-controlled reserved path(s): ${plan.ownerControlled.join(", ")}.` : "";
1085
- const diagnosticMessage = plan.diagnostics.length > 0 ? ` ${plan.diagnostics.join("; ")}` : "";
1086
- throw new GeneratedSpecsIndexMigrationError(
1087
- "generated_specs_index_owner_review_required",
1088
- `Generated Specs-index migration stopped before mutation.${ownerMessage}${diagnosticMessage}`
1089
- );
1090
- }
1091
- if (plan.removable.length === 0)
1092
- return [];
1093
- const snapshots = [];
1094
- for (const projectPath of plan.removable)
1095
- snapshots.push(await inspectRecognizedGeneratedIndex(cwd, projectPath));
1096
- const quarantined = [];
1097
- let removalCommitted = false;
1098
- try {
1099
- for (const snapshot2 of snapshots) {
1100
- const quarantinePath = `${snapshot2.absolutePath}.${process.pid}.${randomUUID()}.rsp-migration`;
1101
- await assertMigrationParent(snapshot2, "quarantine");
1102
- await rename(snapshot2.absolutePath, quarantinePath);
1103
- await assertMigrationParent(snapshot2, "quarantine");
1104
- const quarantinedItem = {
1105
- ...snapshot2,
1106
- quarantinePath,
1107
- recoveryPath: null
1108
- };
1109
- quarantined.push(quarantinedItem);
1110
- const quarantinedSnapshot = await inspectRecognizedGeneratedIndexAt(
1111
- cwd,
1112
- snapshot2.projectPath,
1113
- quarantinePath,
1114
- snapshot2.parentChain
1115
- );
1116
- if (!sameRenamedSnapshot(snapshot2.identity, quarantinedSnapshot.identity) || snapshot2.contentHash !== quarantinedSnapshot.contentHash) {
1117
- throw new GeneratedSpecsIndexMigrationError(
1118
- "generated_specs_index_changed",
1119
- `Generated Specs index changed before migration: ${snapshot2.projectPath}`
1120
- );
1121
- }
1122
- quarantinedItem.identity = quarantinedSnapshot.identity;
1123
- await options.afterQuarantine?.(snapshot2.projectPath);
1124
- await assertMigrationParent(quarantinedItem, "post-quarantine hook");
1125
- }
1126
- for (const item of quarantined)
1127
- await assertMigrationParent(item, "direct Specs postcheck");
1128
- const after = await inspectSpecs({
1129
- cwd,
1130
- allowMissingDecisionRecords: true
1131
- });
1132
- if (!specsInspectionComplete(after) || after.generatedIndexes.length > 0) {
1133
- throw new GeneratedSpecsIndexMigrationError(
1134
- "generated_specs_index_postcheck_failed",
1135
- "Specs tree changed while generated indexes were being migrated; no quarantined index was deleted."
1136
- );
1137
- }
1138
- for (const item of quarantined)
1139
- await assertMigrationParent(item, "direct Specs postcheck");
1140
- for (const item of quarantined) {
1141
- item.recoveryPath = `${item.quarantinePath}.recovery`;
1142
- await materializeVerifiedSnapshot(
1143
- item,
1144
- item.quarantinePath,
1145
- item.recoveryPath,
1146
- fileAdapter,
1147
- options.testing
1148
- );
1149
- }
1150
- for (const item of quarantined) {
1151
- await assertMigrationParent(item, "quarantine removal");
1152
- await unlink3(item.quarantinePath);
1153
- await assertMigrationParent(item, "quarantine removal");
1154
- }
1155
- removalCommitted = true;
1156
- const cleanupPaths = [];
1157
- for (const item of quarantined) {
1158
- try {
1159
- await assertMigrationParent(item, "recovery cleanup");
1160
- await unlink3(item.recoveryPath);
1161
- await assertMigrationParent(item, "recovery cleanup");
1162
- item.recoveryPath = null;
1163
- } catch {
1164
- cleanupPaths.push(item.recoveryPath);
1165
- }
1166
- }
1167
- if (cleanupPaths.length > 0) {
1168
- throw new GeneratedSpecsIndexMigrationError(
1169
- "generated_specs_index_cleanup_required",
1170
- `Generated Specs indexes were removed, but recovery-link cleanup requires owner action: ${cleanupPaths.join(", ")}`,
1171
- cleanupPaths
1172
- );
1173
- }
1174
- return plan.removable;
1175
- } catch (error) {
1176
- if (removalCommitted)
1177
- throw error;
1178
- const recovery = await restoreQuarantinedIndexes(
1179
- quarantined,
1180
- fileAdapter,
1181
- options.testing
1182
- );
1183
- if (recovery.recoveryPaths.length > 0 || recovery.retainedMutations.length > 0) {
1184
- const retainedMessage = recovery.retainedMutations.length > 0 ? ` Retained mutation evidence: ${recovery.retainedMutations.map((item) => `${item.projectPath} (${item.quarantineName}, sha256 ${item.contentHash})`).join(", ")}.` : "";
1185
- throw new GeneratedSpecsIndexMigrationError(
1186
- "generated_specs_index_recovery_required",
1187
- `Generated Specs-index migration stopped and preserved recovery evidence.${recovery.recoveryPaths.length > 0 ? ` Accessible recovery copies: ${recovery.recoveryPaths.join(", ")}.` : ""}${retainedMessage}`,
1188
- recovery.recoveryPaths,
1189
- recovery.retainedMutations
1190
- );
1191
- }
1192
- throw error;
1193
- }
1194
- }
1195
- async function inspectRecognizedGeneratedIndex(projectRoot, projectPath) {
1196
- const absolutePath = resolveProjectPath(projectRoot, projectPath);
1197
- const parentChain = await captureStableDirectoryChain({
1198
- rootPath: projectRoot,
1199
- requiredPath: resolve2(projectRoot, ".rsp", "specs"),
1200
- targetPath: dirname(absolutePath),
1201
- label: `generated Specs index ${projectPath}`
1202
- });
1203
- return inspectRecognizedGeneratedIndexAt(
1204
- projectRoot,
1205
- projectPath,
1206
- absolutePath,
1207
- parentChain
1208
- );
1209
- }
1210
- async function inspectRecognizedGeneratedIndexAt(projectRoot, projectPath, absolutePath, parentChain) {
1211
- await assertStableDirectoryChain(parentChain, `generated Specs index ${projectPath}`);
1212
- const before = await lstat2(absolutePath, { bigint: true });
1213
- if (!before.isFile() || before.isSymbolicLink())
1214
- throw changed2(projectPath);
1215
- const handle = await open(absolutePath, READ_NO_FOLLOW_FLAGS);
1216
- try {
1217
- const opened = await handle.stat({ bigint: true });
1218
- if (!opened.isFile() || !sameSnapshot(before, opened))
1219
- throw changed2(projectPath);
1220
- const content = await readBounded(handle, projectPath);
1221
- const after = await handle.stat({ bigint: true });
1222
- if (!sameSnapshot(opened, after))
1223
- throw changed2(projectPath);
1224
- const finalPath = await lstat2(absolutePath, { bigint: true });
1225
- if (!sameSnapshot(after, finalPath))
1226
- throw changed2(projectPath);
1227
- await assertStableDirectoryChain(parentChain, `generated Specs index ${projectPath}`);
1228
- if (!isRecognizedGeneratedSpecsIndex(content, projectPath)) {
1229
- throw new GeneratedSpecsIndexMigrationError(
1230
- "generated_specs_index_owner_review_required",
1231
- `Reserved Specs index is not recognized generated content: ${projectPath}`
1232
- );
1233
- }
1234
- return {
1235
- projectPath,
1236
- absolutePath,
1237
- identity: snapshot(after),
1238
- contentHash: createHash("sha256").update(content).digest("hex"),
1239
- parentChain
1240
- };
1241
- } finally {
1242
- await handle.close();
1243
- }
1244
- }
1245
- async function readBounded(handle, projectPath) {
1246
- const buffer = Buffer2.allocUnsafe(SPECS_MAX_FILE_BYTES + 1);
1247
- let bytes = 0;
1248
- while (bytes < buffer.length) {
1249
- const result = await handle.read(buffer, bytes, buffer.length - bytes, bytes);
1250
- if (result.bytesRead === 0)
1251
- break;
1252
- bytes += result.bytesRead;
1253
- }
1254
- if (bytes > SPECS_MAX_FILE_BYTES) {
1255
- throw new GeneratedSpecsIndexMigrationError(
1256
- "generated_specs_index_too_large",
1257
- `Reserved Specs index exceeds the ${SPECS_MAX_FILE_BYTES}-byte migration limit: ${projectPath}`
1258
- );
1259
- }
1260
- return buffer.subarray(0, bytes).toString("utf8");
1261
- }
1262
- async function restoreQuarantinedIndexes(quarantined, fileAdapter, testing) {
1263
- const recoveryPaths = [];
1264
- const retainedMutations = [];
1265
- for (const item of [...quarantined].reverse()) {
1266
- try {
1267
- await assertMigrationParent(item, "rollback source inspection");
1268
- const sourcePath = await firstExistingPath(item);
1269
- if (!sourcePath) {
1270
- recoveryPaths.push(item.recoveryPath ?? item.quarantinePath);
1271
- continue;
1272
- }
1273
- await materializeVerifiedSnapshot(
1274
- item,
1275
- sourcePath,
1276
- item.absolutePath,
1277
- fileAdapter,
1278
- testing
1279
- );
1280
- await assertMigrationParent(item, "rollback cleanup");
1281
- await unlink3(item.quarantinePath).catch(ignoreMissing);
1282
- if (item.recoveryPath) {
1283
- await assertMigrationParent(item, "rollback cleanup");
1284
- await unlink3(item.recoveryPath).catch(ignoreMissing);
1285
- }
1286
- } catch {
1287
- try {
1288
- await assertMigrationParent(item, "rollback recovery reporting");
1289
- const recoveryPath = await firstExistingPath(item);
1290
- if (recoveryPath)
1291
- recoveryPaths.push(recoveryPath);
1292
- else
1293
- retainedMutations.push(retainedMutationEvidence(item));
1294
- } catch {
1295
- const relocated = await findRelocatedRetainedMutation(item).catch(() => null);
1296
- if (relocated)
1297
- recoveryPaths.push(relocated);
1298
- else
1299
- retainedMutations.push(retainedMutationEvidence(item));
1300
- }
1301
- }
1302
- }
1303
- return { recoveryPaths, retainedMutations };
1304
- }
1305
- async function findRelocatedRetainedMutation(item) {
1306
- const targetName = item.quarantinePath.slice(dirname(item.quarantinePath).length + 1);
1307
- const pending = [item.parentChain.rootPath];
1308
- let inspected = 0;
1309
- while (pending.length > 0 && inspected < 4096) {
1310
- const directory = pending.shift();
1311
- for (const entry of await readdir(directory, { withFileTypes: true })) {
1312
- inspected += 1;
1313
- if (inspected > 4096)
1314
- return null;
1315
- const path = resolve2(directory, entry.name);
1316
- if (entry.isSymbolicLink())
1317
- continue;
1318
- if (entry.isDirectory()) {
1319
- pending.push(path);
1320
- continue;
1321
- }
1322
- if (!entry.isFile() || entry.name !== targetName)
1323
- continue;
1324
- const value = await lstat2(path, { bigint: true });
1325
- if (value.isFile() && !value.isSymbolicLink() && value.dev === item.identity.dev && value.ino === item.identity.ino) {
1326
- return path;
1327
- }
1328
- }
1329
- }
1330
- return null;
1331
- }
1332
- async function firstExistingPath(item) {
1333
- for (const path of [item.quarantinePath, item.recoveryPath]) {
1334
- if (!path) {
1335
- continue;
1336
- }
1337
- try {
1338
- await assertMigrationParent(item, "recovery source inspection");
1339
- await lstat2(path);
1340
- return path;
1341
- } catch (error) {
1342
- if (error.code !== "ENOENT")
1343
- throw error;
1344
- }
1345
- }
1346
- return null;
1347
- }
1348
- function ignoreMissing(error) {
1349
- if (error.code !== "ENOENT")
1350
- throw error;
1351
- }
1352
- async function materializeVerifiedSnapshot(item, sourcePath, destinationPath, fileAdapter, testing) {
1353
- await assertMigrationParent(item, "snapshot materialization");
1354
- const source = await inspectRecognizedGeneratedIndexAt(
1355
- item.parentChain.rootPath,
1356
- item.projectPath,
1357
- sourcePath,
1358
- item.parentChain
1359
- );
1360
- if (source.contentHash !== item.contentHash || permissionMode(source.identity.mode) !== permissionMode(item.identity.mode)) {
1361
- throw changed2(item.projectPath);
1362
- }
1363
- try {
1364
- await fileAdapter.link(sourcePath, destinationPath);
1365
- } catch (error) {
1366
- if (!hardLinkUnsupported(error))
1367
- throw error;
1368
- await copySnapshotExclusive(
1369
- source,
1370
- destinationPath,
1371
- fileAdapter,
1372
- testing
1373
- );
1374
- }
1375
- const destination = await inspectRecognizedGeneratedIndexAt(
1376
- item.parentChain.rootPath,
1377
- item.projectPath,
1378
- destinationPath,
1379
- item.parentChain
1380
- );
1381
- const sourceAfter = await inspectRecognizedGeneratedIndexAt(
1382
- item.parentChain.rootPath,
1383
- item.projectPath,
1384
- sourcePath,
1385
- item.parentChain
1386
- );
1387
- if (destination.contentHash !== item.contentHash || sourceAfter.contentHash !== item.contentHash || permissionMode(destination.identity.mode) !== permissionMode(item.identity.mode) || permissionMode(sourceAfter.identity.mode) !== permissionMode(item.identity.mode) || !sameRenamedSnapshot(source.identity, sourceAfter.identity)) {
1388
- throw changed2(item.projectPath);
1389
- }
1390
- await assertMigrationParent(item, "snapshot materialization");
1391
- }
1392
- async function copySnapshotExclusive(source, destinationPath, fileAdapter, testing) {
1393
- const sourceHandle = await open(source.absolutePath, READ_NO_FOLLOW_FLAGS);
1394
- let destinationHandle = null;
1395
- let destinationIdentity = null;
1396
- try {
1397
- const sourceOpened = await sourceHandle.stat({ bigint: true });
1398
- if (!sameSnapshot(source.identity, sourceOpened))
1399
- throw changed2(source.projectPath);
1400
- destinationHandle = await open(
1401
- destinationPath,
1402
- WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS,
1403
- Number(permissionMode(source.identity.mode))
1404
- );
1405
- const destinationOpened = await destinationHandle.stat({ bigint: true });
1406
- if (!destinationOpened.isFile() || destinationOpened.isSymbolicLink())
1407
- throw changed2(source.projectPath);
1408
- destinationIdentity = snapshot(destinationOpened);
1409
- await testing?.afterCopyDestinationOpen?.(destinationPath);
1410
- const hash = createHash("sha256");
1411
- const buffer = Buffer2.allocUnsafe(64 * 1024);
1412
- let bytes = 0;
1413
- while (bytes <= SPECS_MAX_FILE_BYTES) {
1414
- const read = await sourceHandle.read(buffer, 0, buffer.length, null);
1415
- if (read.bytesRead === 0)
1416
- break;
1417
- bytes += read.bytesRead;
1418
- if (bytes > SPECS_MAX_FILE_BYTES)
1419
- throw changed2(source.projectPath);
1420
- const chunk = buffer.subarray(0, read.bytesRead);
1421
- hash.update(chunk);
1422
- let written = 0;
1423
- while (written < chunk.length) {
1424
- const result = await destinationHandle.write(
1425
- chunk,
1426
- written,
1427
- chunk.length - written,
1428
- null
1429
- );
1430
- written += result.bytesWritten;
1431
- }
1432
- }
1433
- if (BigInt(bytes) !== source.identity.size || hash.digest("hex") !== source.contentHash) {
1434
- throw changed2(source.projectPath);
1435
- }
1436
- await destinationHandle.chmod(Number(permissionMode(source.identity.mode)));
1437
- await destinationHandle.sync();
1438
- const destinationAfter = await destinationHandle.stat({ bigint: true });
1439
- const sourceAfter = await sourceHandle.stat({ bigint: true });
1440
- if (!sameSnapshot(sourceOpened, sourceAfter) || !destinationAfter.isFile() || destinationAfter.isSymbolicLink() || destinationAfter.size !== source.identity.size || permissionMode(destinationAfter.mode) !== permissionMode(source.identity.mode)) {
1441
- throw changed2(source.projectPath);
1442
- }
1443
- } catch (error) {
1444
- await destinationHandle?.close().catch(() => void 0);
1445
- destinationHandle = null;
1446
- if (destinationIdentity) {
1447
- await cleanupExclusiveDestination(
1448
- source,
1449
- destinationPath,
1450
- destinationIdentity,
1451
- fileAdapter
1452
- );
1453
- }
1454
- throw error;
1455
- } finally {
1456
- await destinationHandle?.close();
1457
- await sourceHandle.close();
1458
- }
1459
- }
1460
- async function cleanupExclusiveDestination(source, destinationPath, expected, fileAdapter) {
1461
- const quarantinePath = `${destinationPath}.${process.pid}.${randomUUID()}.rsp-copy-cleanup`;
1462
- await assertMigrationParent(source, "exclusive-copy cleanup");
1463
- try {
1464
- await rename(destinationPath, quarantinePath);
1465
- } catch (error) {
1466
- if (error.code === "ENOENT")
1467
- return;
1468
- throw error;
1469
- }
1470
- await assertMigrationParent(source, "exclusive-copy cleanup");
1471
- const quarantined = await lstat2(quarantinePath, { bigint: true });
1472
- if (quarantined.isFile() && !quarantined.isSymbolicLink() && quarantined.dev === expected.dev && quarantined.ino === expected.ino) {
1473
- const finalIdentity = await lstat2(quarantinePath, { bigint: true });
1474
- if (!finalIdentity.isFile() || finalIdentity.isSymbolicLink() || finalIdentity.dev !== expected.dev || finalIdentity.ino !== expected.ino) {
1475
- throw new GeneratedSpecsIndexMigrationError(
1476
- "generated_specs_index_copy_cleanup_required",
1477
- `Exclusive-copy cleanup retained a changed file: ${quarantinePath}`,
1478
- [quarantinePath]
1479
- );
1480
- }
1481
- await unlink3(quarantinePath);
1482
- return;
1483
- }
1484
- try {
1485
- await fileAdapter.link(quarantinePath, destinationPath);
1486
- await unlink3(quarantinePath);
1487
- throw new GeneratedSpecsIndexMigrationError(
1488
- "generated_specs_index_copy_replaced",
1489
- `Exclusive-copy cleanup preserved a concurrent replacement at ${destinationPath}`
1490
- );
1491
- } catch (error) {
1492
- if (error instanceof GeneratedSpecsIndexMigrationError)
1493
- throw error;
1494
- if (!hardLinkUnsupported(error) && error.code !== "EEXIST") {
1495
- throw error;
1496
- }
1497
- }
1498
- if (await lstat2(destinationPath).catch(() => null) === null)
1499
- await copyConcurrentReplacementExclusive(quarantinePath, destinationPath);
1500
- throw new GeneratedSpecsIndexMigrationError(
1501
- "generated_specs_index_copy_cleanup_required",
1502
- `Exclusive-copy cleanup preserved a concurrent replacement and retained its original inode at ${quarantinePath}`,
1503
- [quarantinePath]
1504
- );
1505
- }
1506
- async function copyConcurrentReplacementExclusive(sourcePath, destinationPath) {
1507
- const sourceHandle = await open(sourcePath, READ_NO_FOLLOW_FLAGS);
1508
- let destinationHandle = null;
1509
- try {
1510
- const sourceBefore = await sourceHandle.stat({ bigint: true });
1511
- if (!sourceBefore.isFile() || sourceBefore.isSymbolicLink() || sourceBefore.size > BigInt(SPECS_MAX_FILE_BYTES)) {
1512
- return;
1513
- }
1514
- destinationHandle = await open(
1515
- destinationPath,
1516
- WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS,
1517
- Number(permissionMode(sourceBefore.mode))
1518
- );
1519
- const buffer = Buffer2.allocUnsafe(64 * 1024);
1520
- let bytes = 0;
1521
- while (bytes <= SPECS_MAX_FILE_BYTES) {
1522
- const read = await sourceHandle.read(buffer, 0, buffer.length, null);
1523
- if (read.bytesRead === 0)
1524
- break;
1525
- bytes += read.bytesRead;
1526
- if (bytes > SPECS_MAX_FILE_BYTES)
1527
- return;
1528
- let written = 0;
1529
- while (written < read.bytesRead) {
1530
- const result = await destinationHandle.write(
1531
- buffer,
1532
- written,
1533
- read.bytesRead - written,
1534
- null
1535
- );
1536
- written += result.bytesWritten;
1537
- }
1538
- }
1539
- await destinationHandle.chmod(Number(permissionMode(sourceBefore.mode)));
1540
- await destinationHandle.sync();
1541
- const sourceAfter = await sourceHandle.stat({ bigint: true });
1542
- const destinationAfter = await destinationHandle.stat({ bigint: true });
1543
- if (!sameSnapshot(sourceBefore, sourceAfter) || destinationAfter.size !== sourceBefore.size || permissionMode(destinationAfter.mode) !== permissionMode(sourceBefore.mode)) {
1544
- throw new GeneratedSpecsIndexMigrationError(
1545
- "generated_specs_index_copy_cleanup_required",
1546
- `Concurrent replacement could not be restored byte-for-byte: ${sourcePath}`,
1547
- [sourcePath]
1548
- );
1549
- }
1550
- } finally {
1551
- await destinationHandle?.close();
1552
- await sourceHandle.close();
1553
- }
1554
- }
1555
- function retainedMutationEvidence(item) {
1556
- return {
1557
- projectPath: item.projectPath,
1558
- quarantineName: item.quarantinePath.slice(dirname(item.quarantinePath).length + 1),
1559
- contentHash: item.contentHash,
1560
- device: String(item.identity.dev),
1561
- inode: String(item.identity.ino),
1562
- size: String(item.identity.size),
1563
- mode: permissionMode(item.identity.mode).toString(8),
1564
- lastKnownParentRealPath: item.parentChain.entries.at(-1).realPath
1565
- };
1566
- }
1567
- async function assertMigrationParent(item, operation) {
1568
- try {
1569
- await assertStableDirectoryChain(
1570
- item.parentChain,
1571
- `generated Specs index ${item.projectPath} ${operation}`
1572
- );
1573
- } catch (error) {
1574
- throw new GeneratedSpecsIndexMigrationError(
1575
- "generated_specs_index_parent_changed",
1576
- `Generated Specs-index migration stopped because a managed parent changed before ${operation}: ${item.projectPath}. ${error instanceof Error ? error.message : String(error)}`
1577
- );
1578
- }
1579
- }
1580
- function hardLinkUnsupported(error) {
1581
- return ["EXDEV", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EPERM"].includes(error.code ?? "");
1582
- }
1583
- function permissionMode(mode) {
1584
- return mode & 0o777n;
1585
- }
1586
- function resolveProjectPath(projectRoot, projectPath) {
1587
- const normalized = normalizeLogicalPath(projectPath);
1588
- const absolutePath = resolve2(projectRoot, ...normalized.split("/"));
1589
- const candidate = relative2(projectRoot, absolutePath);
1590
- if (normalized === "" || isAbsolute2(projectPath) || candidate === "" || candidate === ".." || candidate.startsWith(`..${sep2}`) || isAbsolute2(candidate) || dirname(normalized) === ".") {
1591
- throw new GeneratedSpecsIndexMigrationError(
1592
- "generated_specs_index_path_invalid",
1593
- `Generated Specs-index path is outside the managed Specs tree: ${projectPath}`
1594
- );
1595
- }
1596
- return absolutePath;
1597
- }
1598
- function changed2(projectPath) {
1599
- return new GeneratedSpecsIndexMigrationError(
1600
- "generated_specs_index_changed",
1601
- `Generated Specs index changed before migration: ${projectPath}`
1602
- );
1603
- }
1604
- function snapshot(value) {
1605
- return {
1606
- dev: value.dev,
1607
- ino: value.ino,
1608
- size: value.size,
1609
- mode: value.mode,
1610
- mtimeNs: value.mtimeNs,
1611
- ctimeNs: value.ctimeNs
1612
- };
1613
- }
1614
- function sameSnapshot(left, right) {
1615
- return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mode === right.mode && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
1616
- }
1617
- function sameRenamedSnapshot(left, right) {
1618
- return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mode === right.mode && left.mtimeNs === right.mtimeNs;
1619
- }
1620
-
1621
- // src/commands/update-transaction.ts
1622
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
1623
- import { constants as constants2 } from "fs";
1624
- import {
1625
- chmod,
1626
- link as link2,
1627
- lstat as lstat3,
1628
- mkdir as mkdir2,
1629
- mkdtemp,
1630
- open as open2,
1631
- rename as rename2,
1632
- rmdir,
1633
- unlink as unlink4
1634
- } from "fs/promises";
1635
- import { tmpdir } from "os";
1636
- import { dirname as dirname2, join as join2, relative as relative3, resolve as resolve3, sep as sep3 } from "path";
1637
- var READ_NO_FOLLOW_FLAGS2 = constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0);
1638
- var WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS2 = constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0);
1639
- var UpdateRollbackMutationError = class extends Error {
1640
- constructor(message, recoveryPaths = []) {
1641
- super(message);
1642
- this.recoveryPaths = recoveryPaths;
1643
- this.name = "UpdateRollbackMutationError";
1644
- }
1645
- recoveryPaths;
1646
- };
1647
- var UpdateRollbackJournal = class {
1648
- constructor(projectRoot = resolve3("."), testing = {}) {
1649
- this.projectRoot = projectRoot;
1650
- this.testing = testing;
1651
- }
1652
- projectRoot;
1653
- testing;
1654
- entries = /* @__PURE__ */ new Map();
1655
- mutationOrder = [];
1656
- async capture(path) {
1657
- const absolutePath = this.resolveProjectPath(path);
1658
- if (this.entries.has(absolutePath))
1659
- return;
1660
- this.entries.set(absolutePath, {
1661
- path: absolutePath,
1662
- original: await inspectPathSnapshot(absolutePath),
1663
- published: null,
1664
- parentChain: await captureExistingParentChain(
1665
- this.projectRoot,
1666
- absolutePath
1667
- )
1668
- });
1669
- }
1670
- async captureDirectoryChain(path) {
1671
- const absolutePath = this.resolveProjectPath(path);
1672
- const rel = relative3(this.projectRoot, absolutePath);
1673
- const captured = [];
1674
- let current = this.projectRoot;
1675
- for (const segment of rel === "" ? [] : rel.split(sep3)) {
1676
- current = resolve3(current, segment);
1677
- await this.capture(current);
1678
- captured.push(current);
1679
- }
1680
- return captured;
1681
- }
1682
- async mark(path) {
1683
- const absolutePath = this.resolveProjectPath(path);
1684
- const entry = this.entries.get(absolutePath);
1685
- if (!entry)
1686
- throw new Error(`Update rollback journal did not capture ${absolutePath}`);
1687
- entry.published = await inspectPathSnapshot(absolutePath);
1688
- if (!samePathState(entry.original, entry.published) && !this.mutationOrder.includes(absolutePath)) {
1689
- this.mutationOrder.push(absolutePath);
1690
- }
1691
- }
1692
- async markAll(paths) {
1693
- for (const path of paths)
1694
- await this.mark(path);
1695
- }
1696
- async rollback() {
1697
- const restored = [];
1698
- const retainedMutations = [];
1699
- const recoveryPaths = [];
1700
- for (const [path, entry] of this.entries) {
1701
- if (entry.published)
1702
- continue;
1703
- const current = await inspectPathSnapshot(path).catch(() => null);
1704
- if (current && !samePathState(entry.original, current)) {
1705
- entry.published = current;
1706
- if (!this.mutationOrder.includes(path))
1707
- this.mutationOrder.push(path);
1708
- }
1709
- }
1710
- const ordered = [...this.mutationOrder].sort((left, right) => compareRollbackOrder(
1711
- this.entries.get(left),
1712
- this.entries.get(right)
1713
- ));
1714
- for (const path of ordered) {
1715
- const entry = this.entries.get(path);
1716
- const published = entry.published;
1717
- if (!published)
1718
- continue;
1719
- let current;
1720
- try {
1721
- current = await inspectPathSnapshot(path);
1722
- } catch {
1723
- await recordIncompleteRollback(
1724
- this.projectRoot,
1725
- entry,
1726
- retainedMutations,
1727
- recoveryPaths
1728
- );
1729
- continue;
1730
- }
1731
- if (!samePathState(current, published)) {
1732
- await recordIncompleteRollback(
1733
- this.projectRoot,
1734
- entry,
1735
- retainedMutations,
1736
- recoveryPaths
1737
- );
1738
- continue;
1739
- }
1740
- try {
1741
- recoveryPaths.push(...await restorePath(entry, this.testing));
1742
- restored.push(toProjectPath(this.projectRoot, path));
1743
- if (entry.original.kind === "directory" && published.kind === "missing") {
1744
- await this.refreshDescendantParentChains(path);
1745
- }
1746
- } catch (error) {
1747
- if (error instanceof UpdateRollbackMutationError)
1748
- recoveryPaths.push(...error.recoveryPaths);
1749
- await recordIncompleteRollback(
1750
- this.projectRoot,
1751
- entry,
1752
- retainedMutations,
1753
- recoveryPaths
1754
- );
1755
- }
1756
- }
1757
- return {
1758
- restored,
1759
- retainedMutations: [...new Set(retainedMutations)].sort(),
1760
- recoveryPaths: [...new Set(recoveryPaths)].sort()
1761
- };
1762
- }
1763
- resolveProjectPath(path) {
1764
- const absolutePath = resolve3(path);
1765
- const rel = relative3(this.projectRoot, absolutePath);
1766
- if (rel === ".." || rel.startsWith(`..${sep3}`))
1767
- throw new Error(`Update transaction path escapes the project root: ${path}`);
1768
- return absolutePath;
1769
- }
1770
- async refreshDescendantParentChains(restoredDirectory) {
1771
- for (const entry of this.entries.values()) {
1772
- const rel = relative3(restoredDirectory, entry.path);
1773
- if (rel === "" || rel === ".." || rel.startsWith(`..${sep3}`))
1774
- continue;
1775
- entry.parentChain = await captureExistingParentChain(
1776
- this.projectRoot,
1777
- entry.path
1778
- );
1779
- }
1780
- }
1781
- };
1782
- async function recordIncompleteRollback(projectRoot, entry, retainedMutations, recoveryPaths) {
1783
- retainedMutations.push(toProjectPath(projectRoot, entry.path));
1784
- const recovery = await persistOriginalRecovery(entry.original).catch(() => null);
1785
- if (recovery)
1786
- recoveryPaths.push(recovery);
1787
- }
1788
- async function inspectPathSnapshot(path) {
1789
- let before;
1790
- try {
1791
- before = await lstat3(path, { bigint: true });
1792
- } catch (error) {
1793
- if (error.code === "ENOENT")
1794
- return { kind: "missing" };
1795
- throw error;
1796
- }
1797
- if (before.isSymbolicLink())
1798
- throw new Error(`Update transaction refuses a symlinked path: ${path}`);
1799
- if (before.isDirectory()) {
1800
- return {
1801
- kind: "directory",
1802
- device: before.dev,
1803
- inode: before.ino,
1804
- mode: before.mode
1805
- };
1806
- }
1807
- if (!before.isFile())
1808
- throw new Error(`Update transaction requires a regular file or directory: ${path}`);
1809
- const handle = await open2(path, READ_NO_FOLLOW_FLAGS2);
1810
- try {
1811
- const opened = await handle.stat({ bigint: true });
1812
- if (!sameFileIdentity(before, opened))
1813
- throw new Error(`Update transaction path changed while it was captured: ${path}`);
1814
- const content = await handle.readFile();
1815
- const after = await handle.stat({ bigint: true });
1816
- const finalPath = await lstat3(path, { bigint: true });
1817
- if (!sameFileIdentity(opened, after) || !sameFileIdentity(after, finalPath) || BigInt(content.length) !== after.size) {
1818
- throw new Error(`Update transaction path changed while it was captured: ${path}`);
1819
- }
1820
- return {
1821
- kind: "file",
1822
- content,
1823
- device: after.dev,
1824
- inode: after.ino,
1825
- mode: after.mode,
1826
- size: after.size,
1827
- mtimeNs: after.mtimeNs,
1828
- ctimeNs: after.ctimeNs,
1829
- contentHash: createHash2("sha256").update(content).digest("hex")
1830
- };
1831
- } finally {
1832
- await handle.close();
1833
- }
1834
- }
1835
- async function restorePath(entry, testing) {
1836
- const { path, original, published, parentChain } = entry;
1837
- if (!published)
1838
- return [];
1839
- if (original.kind === "missing") {
1840
- if (published.kind === "file") {
1841
- const quarantine2 = await quarantineExpectedFile(entry, published, testing);
1842
- await unlinkVerifiedQuarantine(quarantine2, published, parentChain);
1843
- return [];
1844
- }
1845
- if (published.kind === "directory") {
1846
- const quarantine2 = await quarantineExpectedDirectory(entry, published, testing);
1847
- await assertStableDirectoryChain(parentChain, "update rollback directory removal");
1848
- await rmdir(quarantine2);
1849
- await assertStableDirectoryChain(parentChain, "update rollback directory removal");
1850
- return [];
1851
- }
1852
- return [];
1853
- }
1854
- if (original.kind === "directory") {
1855
- if (published.kind === "missing") {
1856
- await assertStableDirectoryChain(parentChain, "update rollback directory restore");
1857
- await mkdir2(path, { mode: Number(permissionMode2(original.mode)) });
1858
- if (process.platform !== "win32")
1859
- await chmod(path, Number(permissionMode2(original.mode)));
1860
- return [];
1861
- }
1862
- if (published.kind !== "directory")
1863
- throw new Error(`Update rollback path type changed: ${path}`);
1864
- const current = await inspectPathSnapshot(path);
1865
- if (!samePathState(current, published))
1866
- throw new UpdateRollbackMutationError(`Update rollback directory changed: ${path}`);
1867
- if (process.platform !== "win32" && permissionMode2(published.mode) !== permissionMode2(original.mode)) {
1868
- await assertStableDirectoryChain(parentChain, "update rollback directory mode restore");
1869
- await chmod(path, Number(permissionMode2(original.mode)));
1870
- }
1871
- return [];
1872
- }
1873
- if (published.kind === "missing") {
1874
- await writeSnapshotExclusive(path, original, parentChain, testing);
1875
- return [];
1876
- }
1877
- if (published.kind !== "file")
1878
- throw new Error(`Update rollback path type changed: ${path}`);
1879
- const quarantine = await quarantineExpectedFile(entry, published, testing);
1880
- try {
1881
- await writeSnapshotExclusive(path, original, parentChain, testing);
1882
- } catch (error) {
1883
- const recovery = await restoreQuarantinedFile(
1884
- quarantine,
1885
- path,
1886
- published,
1887
- parentChain
1888
- );
1889
- if (recovery.length > 0) {
1890
- throw new UpdateRollbackMutationError(
1891
- `Update rollback preserved the current file after restore failure: ${path}`,
1892
- recovery
1893
- );
1894
- }
1895
- throw error;
1896
- }
1897
- await unlinkVerifiedQuarantine(quarantine, published, parentChain);
1898
- return [];
1899
- }
1900
- async function quarantineExpectedFile(entry, expected, testing) {
1901
- await testing.afterPublishedValidation?.(entry.path);
1902
- await assertStableDirectoryChain(entry.parentChain, "update rollback file quarantine");
1903
- const quarantine = `${entry.path}.${process.pid}.${randomUUID2()}.rsp-update-current`;
1904
- await rename2(entry.path, quarantine);
1905
- await assertStableDirectoryChain(entry.parentChain, "update rollback file quarantine");
1906
- const quarantined = await inspectPathSnapshot(quarantine);
1907
- if (!sameClaimedPathState(quarantined, expected)) {
1908
- const recovery = await restoreQuarantinedPath(
1909
- quarantine,
1910
- entry.path,
1911
- quarantined,
1912
- entry.parentChain
1913
- );
1914
- throw new UpdateRollbackMutationError(
1915
- `Update rollback refused a concurrently replaced file: ${entry.path}`,
1916
- recovery
1917
- );
1918
- }
1919
- return quarantine;
1920
- }
1921
- async function quarantineExpectedDirectory(entry, expected, testing) {
1922
- await testing.afterPublishedValidation?.(entry.path);
1923
- await assertStableDirectoryChain(entry.parentChain, "update rollback directory quarantine");
1924
- const quarantine = `${entry.path}.${process.pid}.${randomUUID2()}.rsp-update-current`;
1925
- await rename2(entry.path, quarantine);
1926
- await assertStableDirectoryChain(entry.parentChain, "update rollback directory quarantine");
1927
- const quarantined = await inspectPathSnapshot(quarantine);
1928
- if (!sameClaimedPathState(quarantined, expected)) {
1929
- const recovery = await restoreQuarantinedPath(
1930
- quarantine,
1931
- entry.path,
1932
- quarantined,
1933
- entry.parentChain
1934
- );
1935
- throw new UpdateRollbackMutationError(
1936
- `Update rollback refused a concurrently replaced directory: ${entry.path}`,
1937
- recovery
1938
- );
1939
- }
1940
- return quarantine;
1941
- }
1942
- async function restoreQuarantinedPath(quarantine, destination, snapshot2, parentChain) {
1943
- if (snapshot2.kind === "file")
1944
- return restoreQuarantinedFile(quarantine, destination, snapshot2, parentChain);
1945
- return [quarantine];
945
+
946
+ ## Scenarios
947
+ <!-- Clarify key cross-boundary, failure, or ambiguous behavior with a few examples; omit when unnecessary and keep test execution results in the Change. -->
948
+ - ${placeholder}
949
+
950
+ ## Constraints
951
+ <!-- Record necessary technical, protocol, security, or compatibility limits; do not repeat behavior requirements or decision rationale. -->
952
+ - ${placeholder}
953
+ `;
1946
954
  }
1947
- async function restoreQuarantinedFile(quarantine, destination, snapshot2, parentChain) {
1948
- await assertStableDirectoryChain(parentChain, "update rollback concurrent-file restore");
1949
- try {
1950
- await link2(quarantine, destination);
1951
- const restored = await inspectPathSnapshot(destination);
1952
- const retained = await inspectPathSnapshot(quarantine);
1953
- if (!sameClaimedPathState(restored, snapshot2) || !sameClaimedPathState(retained, snapshot2)) {
1954
- return [quarantine];
1955
- }
1956
- await unlinkVerifiedQuarantine(quarantine, snapshot2, parentChain);
1957
- return [];
1958
- } catch (error) {
1959
- if (error.code === "EEXIST")
1960
- return [quarantine];
1961
- if (!hardLinkUnsupported2(error))
1962
- throw error;
1963
- }
955
+
956
+ // src/core/update-check.ts
957
+ async function checkForUpdates(current) {
1964
958
  try {
1965
- await writeSnapshotExclusive(destination, snapshot2, parentChain, {});
959
+ const response = await fetch("https://registry.npmjs.org/-/package/@oevery%2Frsp/dist-tags", {
960
+ signal: AbortSignal.timeout(2e3),
961
+ redirect: "error"
962
+ });
963
+ if (!response.ok)
964
+ throw new Error("registry unavailable");
965
+ const body = await response.text();
966
+ if (body.length > 16384)
967
+ throw new Error("registry metadata exceeded bound");
968
+ const latest = JSON.parse(body)?.latest;
969
+ if (typeof latest !== "string" || !isExactVersion(latest) || !/^\d+\.\d+\.\d+$/.test(latest))
970
+ throw new Error("invalid stable version");
971
+ return { status: compareVersions(latest, current) > 0 ? "available" : "current", current, latest };
1966
972
  } catch {
1967
- return [quarantine];
973
+ return { status: "unavailable", current };
1968
974
  }
1969
- return [quarantine];
1970
975
  }
1971
- async function unlinkVerifiedQuarantine(quarantine, expected, parentChain) {
1972
- await assertStableDirectoryChain(parentChain, "update rollback quarantine cleanup");
1973
- const current = await inspectPathSnapshot(quarantine);
1974
- if (!sameClaimedPathState(current, expected)) {
1975
- throw new UpdateRollbackMutationError(
1976
- `Update rollback cleanup retained a changed quarantine: ${quarantine}`,
1977
- [quarantine]
1978
- );
976
+
977
+ // src/commands/skills.ts
978
+ import { lstatSync, realpathSync } from "fs";
979
+ import { chmod, copyFile, mkdir, mkdtemp, readdir, readFile as readFile2, rename, rm } from "fs/promises";
980
+ import { dirname, join, relative } from "path";
981
+ var DEFAULT_PACKAGED_SKILL_NAMES = [
982
+ "rsp",
983
+ "rsp-shape",
984
+ "rsp-implement",
985
+ "rsp-doc",
986
+ "rsp-verify",
987
+ "rsp-review",
988
+ "rsp-commit"
989
+ ];
990
+ var RETIRED_PACKAGED_SKILL_NAMES = [
991
+ "rsp-address-review",
992
+ "rsp-codebase-audit",
993
+ "rsp-structural-audit",
994
+ "rsp-design",
995
+ "rsp-diagnose",
996
+ "rsp-manage",
997
+ "rsp-release-docs",
998
+ "rsp-resolve-findings",
999
+ "rsp-tdd"
1000
+ ];
1001
+ function unsupportedEntry(path, label) {
1002
+ return new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`);
1003
+ }
1004
+ async function inspectPackagedSkills(packageRoot) {
1005
+ const skillsRoot = join(packageRoot, "skills");
1006
+ const rootInspection = inspectManagedDirectory(skillsRoot, "packaged Skills root");
1007
+ if (rootInspection.issue)
1008
+ throw rootInspection.issue;
1009
+ const entries = await readdir(skillsRoot, { withFileTypes: true });
1010
+ const skills = [];
1011
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1012
+ if (entry.name.startsWith("."))
1013
+ continue;
1014
+ const root = join(skillsRoot, entry.name);
1015
+ if (!entry.isDirectory())
1016
+ throw unsupportedEntry(root, "packaged Skills");
1017
+ const inspection = await inspectManagedFileTree(root, `packaged Skill ${entry.name}`, { includeHidden: true });
1018
+ if (inspection.issues.length > 0)
1019
+ throw inspection.issues[0];
1020
+ skills.push({
1021
+ name: entry.name,
1022
+ root,
1023
+ files: inspection.files.map((path) => relative(root, path))
1024
+ });
1979
1025
  }
1980
- await unlink4(quarantine);
1981
- await assertStableDirectoryChain(parentChain, "update rollback quarantine cleanup");
1026
+ return skills;
1982
1027
  }
1983
- async function writeSnapshotExclusive(path, snapshot2, parentChain, testing) {
1984
- let handle = null;
1985
- let createdIdentity = null;
1028
+ function inspectTarget(path, name) {
1986
1029
  try {
1987
- await assertStableDirectoryChain(parentChain, "update rollback exclusive restore");
1988
- handle = await open2(
1989
- path,
1990
- WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS2,
1991
- Number(permissionMode2(snapshot2.mode))
1992
- );
1993
- const opened = await handle.stat({ bigint: true });
1994
- if (!opened.isFile() || opened.isSymbolicLink())
1995
- throw new Error(`Update rollback could not open a regular restore file: ${path}`);
1996
- createdIdentity = { device: opened.dev, inode: opened.ino };
1997
- await testing.afterRestoreDestinationOpen?.(path);
1998
- await handle.writeFile(snapshot2.content);
1999
- await handle.chmod(Number(permissionMode2(snapshot2.mode)));
2000
- await handle.sync();
2001
- const value = await handle.stat({ bigint: true });
2002
- if (!value.isFile() || value.isSymbolicLink() || value.dev !== createdIdentity.device || value.ino !== createdIdentity.inode || value.size !== snapshot2.size || permissionMode2(value.mode) !== permissionMode2(snapshot2.mode)) {
2003
- throw new Error(`Update rollback could not validate restored file: ${path}`);
2004
- }
1030
+ const stats = lstatSync(path);
1031
+ if (stats.isDirectory())
1032
+ return "directory";
1033
+ throw unsupportedEntry(path, `installed Skill ${name}`);
2005
1034
  } catch (error) {
2006
- await handle?.close().catch(() => void 0);
2007
- handle = null;
2008
- if (createdIdentity) {
2009
- const recovery = await cleanupCreatedFile(
2010
- path,
2011
- createdIdentity,
2012
- parentChain
2013
- );
2014
- if (recovery.length > 0) {
2015
- throw new UpdateRollbackMutationError(
2016
- `Update rollback exclusive restore preserved a concurrent replacement: ${path}`,
2017
- recovery
2018
- );
2019
- }
2020
- }
1035
+ if (error.code === "ENOENT")
1036
+ return "missing";
2021
1037
  throw error;
2022
- } finally {
2023
- await handle?.close();
2024
- }
2025
- await assertStableDirectoryChain(parentChain, "update rollback exclusive restore");
2026
- const restored = await inspectPathSnapshot(path);
2027
- if (restored.kind !== "file" || restored.contentHash !== snapshot2.contentHash || permissionMode2(restored.mode) !== permissionMode2(snapshot2.mode)) {
2028
- throw new Error(`Update rollback restored different bytes or mode: ${path}`);
2029
1038
  }
2030
1039
  }
2031
- async function cleanupCreatedFile(path, expected, parentChain) {
2032
- const quarantine = `${path}.${process.pid}.${randomUUID2()}.rsp-update-created`;
2033
- await assertStableDirectoryChain(parentChain, "update rollback exclusive cleanup");
1040
+ function inspectInventoryTarget(path) {
2034
1041
  try {
2035
- await rename2(path, quarantine);
1042
+ return lstatSync(path).isDirectory() ? "directory" : "divergent";
2036
1043
  } catch (error) {
2037
1044
  if (error.code === "ENOENT")
2038
- return [];
1045
+ return "missing";
2039
1046
  throw error;
2040
1047
  }
2041
- await assertStableDirectoryChain(parentChain, "update rollback exclusive cleanup");
2042
- const current = await lstat3(quarantine, { bigint: true });
2043
- if (current.isFile() && !current.isSymbolicLink() && current.dev === expected.device && current.ino === expected.inode) {
2044
- const finalIdentity = await lstat3(quarantine, { bigint: true });
2045
- if (!finalIdentity.isFile() || finalIdentity.isSymbolicLink() || finalIdentity.dev !== expected.device || finalIdentity.ino !== expected.inode) {
2046
- return [quarantine];
2047
- }
2048
- await unlink4(quarantine);
2049
- return [];
2050
- }
2051
- const snapshot2 = await inspectPathSnapshot(quarantine);
2052
- return restoreQuarantinedPath(quarantine, path, snapshot2, parentChain);
2053
- }
2054
- async function persistOriginalRecovery(original) {
2055
- if (original.kind !== "file")
2056
- return null;
2057
- const recoveryRoot = await mkdtemp(join2(tmpdir(), "rsp-update-recovery-"));
2058
- if (process.platform !== "win32")
2059
- await chmod(recoveryRoot, 448);
2060
- const recovery = join2(recoveryRoot, "snapshot");
2061
- const parentChain = await captureStableDirectoryChain({
2062
- rootPath: recoveryRoot,
2063
- targetPath: recoveryRoot,
2064
- label: "update rollback recovery root"
2065
- });
2066
- await writeSnapshotExclusive(recovery, original, parentChain, {});
2067
- return recovery;
2068
- }
2069
- async function captureExistingParentChain(projectRoot, path) {
2070
- let parent = dirname2(path);
2071
- while (true) {
2072
- const rel = relative3(projectRoot, parent);
2073
- if (rel === ".." || rel.startsWith(`..${sep3}`))
2074
- throw new Error(`Update transaction parent escapes the project root: ${path}`);
2075
- try {
2076
- const value = await lstat3(parent);
2077
- if (!value.isDirectory() || value.isSymbolicLink())
2078
- throw new Error(`Update transaction parent must be a real directory: ${parent}`);
2079
- break;
2080
- } catch (error) {
2081
- if (error.code !== "ENOENT")
2082
- throw error;
2083
- if (parent === projectRoot)
2084
- throw error;
2085
- parent = dirname2(parent);
2086
- }
2087
- }
2088
- return captureStableDirectoryChain({
2089
- rootPath: projectRoot,
2090
- targetPath: parent,
2091
- label: `update transaction ${path}`
2092
- });
2093
1048
  }
2094
- function samePathState(left, right) {
2095
- if (left.kind !== right.kind)
1049
+ function isCanonicalSourceProjection(sourceRoot, targetRoot) {
1050
+ try {
1051
+ if (!lstatSync(targetRoot).isSymbolicLink())
1052
+ return false;
1053
+ return realpathSync(targetRoot) === realpathSync(sourceRoot);
1054
+ } catch {
2096
1055
  return false;
2097
- if (left.kind === "missing")
2098
- return true;
2099
- if (left.kind === "directory" && right.kind === "directory") {
2100
- return left.device === right.device && left.inode === right.inode && permissionMode2(left.mode) === permissionMode2(right.mode);
2101
- }
2102
- if (left.kind === "file" && right.kind === "file") {
2103
- return left.device === right.device && left.inode === right.inode && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs && permissionMode2(left.mode) === permissionMode2(right.mode) && left.contentHash === right.contentHash;
2104
1056
  }
2105
- return false;
2106
1057
  }
2107
- function compareRollbackOrder(left, right) {
2108
- const leftRestoresDeleted = left.original.kind !== "missing" && left.published?.kind === "missing";
2109
- const rightRestoresDeleted = right.original.kind !== "missing" && right.published?.kind === "missing";
2110
- if (leftRestoresDeleted !== rightRestoresDeleted)
2111
- return leftRestoresDeleted ? -1 : 1;
2112
- if (leftRestoresDeleted)
2113
- return pathDepth(left.path) - pathDepth(right.path);
2114
- return pathDepth(right.path) - pathDepth(left.path);
2115
- }
2116
- function sameClaimedPathState(left, right) {
2117
- if (left.kind !== right.kind)
1058
+ async function treesEqual(source, targetRoot) {
1059
+ const target = await inspectManagedFileTree(targetRoot, `installed Skill ${source.name}`, { includeHidden: true });
1060
+ if (target.issues.length > 0)
1061
+ throw target.issues[0];
1062
+ const targetFiles = target.files.map((path) => relative(targetRoot, path));
1063
+ if (source.files.length !== targetFiles.length || source.files.some((path, index) => path !== targetFiles[index]))
2118
1064
  return false;
2119
- if (left.kind === "missing")
2120
- return true;
2121
- if (left.kind === "directory" && right.kind === "directory") {
2122
- return left.device === right.device && left.inode === right.inode && permissionMode2(left.mode) === permissionMode2(right.mode);
2123
- }
2124
- if (left.kind === "file" && right.kind === "file") {
2125
- return left.device === right.device && left.inode === right.inode && left.size === right.size && permissionMode2(left.mode) === permissionMode2(right.mode) && left.contentHash === right.contentHash;
1065
+ for (const path of source.files) {
1066
+ const sourcePath = join(source.root, path);
1067
+ const targetPath = join(targetRoot, path);
1068
+ const [sourceStats, targetStats] = [lstatSync(sourcePath), lstatSync(targetPath)];
1069
+ if (!sourceStats.isFile() || !targetStats.isFile())
1070
+ throw unsupportedEntry(!sourceStats.isFile() ? sourcePath : targetPath, `Skill ${source.name}`);
1071
+ if ((sourceStats.mode & 511) !== (targetStats.mode & 511))
1072
+ return false;
1073
+ const [sourceContent, targetContent] = await Promise.all([readFile2(sourcePath), readFile2(targetPath)]);
1074
+ if (!sourceContent.equals(targetContent))
1075
+ return false;
2126
1076
  }
2127
- return false;
2128
- }
2129
- function sameFileIdentity(left, right) {
2130
- return left.isFile() && right.isFile() && !left.isSymbolicLink() && !right.isSymbolicLink() && left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
2131
- }
2132
- function hardLinkUnsupported2(error) {
2133
- return ["EXDEV", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EPERM"].includes(error.code ?? "");
2134
- }
2135
- function permissionMode2(mode) {
2136
- return mode & 0o777n;
2137
- }
2138
- function pathDepth(path) {
2139
- return path.split(sep3).length;
1077
+ return true;
2140
1078
  }
2141
- function toProjectPath(projectRoot, path) {
2142
- return relative3(projectRoot, path).split(sep3).join("/");
1079
+ async function inspectPackagedSkillInventory(options = {}) {
1080
+ const packageRoot = options.packageRoot ?? PKG_ROOT;
1081
+ const projectRoot = options.projectRoot ?? process.cwd();
1082
+ const packagedSkills = await inspectPackagedSkills(packageRoot);
1083
+ const packagedNames = new Set(packagedSkills.map((skill) => skill.name));
1084
+ const missingDefaults = DEFAULT_PACKAGED_SKILL_NAMES.filter((name) => !packagedNames.has(name));
1085
+ if (missingDefaults.length > 0)
1086
+ throw new Error(`packaged Skills missing default Skills: ${missingDefaults.join(", ")}`);
1087
+ const manifest = JSON.parse(await readFile2(join(packageRoot, "package.json"), "utf8"));
1088
+ if (typeof manifest.name !== "string" || typeof manifest.version !== "string")
1089
+ throw new Error("package.json must contain string name and version fields");
1090
+ const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
1091
+ const defaults = new Set(DEFAULT_PACKAGED_SKILL_NAMES);
1092
+ const skills = [];
1093
+ for (const skill of packagedSkills) {
1094
+ const target = join(targetRoot, skill.name);
1095
+ const targetStatus = inspectInventoryTarget(target);
1096
+ skills.push({
1097
+ name: skill.name,
1098
+ kind: defaults.has(skill.name) ? "default" : "optional",
1099
+ status: targetStatus === "missing" ? "missing" : targetStatus === "divergent" ? isCanonicalSourceProjection(skill.root, target) ? "unchanged" : "divergent" : await treesEqual(skill, target) ? "unchanged" : "divergent"
1100
+ });
1101
+ }
1102
+ const retired = RETIRED_PACKAGED_SKILL_NAMES.filter((name) => inspectInventoryTarget(join(targetRoot, name)) !== "missing");
1103
+ return {
1104
+ retired,
1105
+ package: { name: manifest.name, version: manifest.version },
1106
+ target: relative(projectRoot, targetRoot) || ".",
1107
+ skills
1108
+ };
2143
1109
  }
2144
-
2145
- // src/commands/update.ts
2146
- var UpdateTransactionError = class extends Error {
2147
- constructor(message, attemptedActions, rollback, options = {}) {
2148
- super(message, options);
2149
- this.attemptedActions = attemptedActions;
2150
- this.rollback = rollback;
2151
- this.name = "UpdateTransactionError";
2152
- }
2153
- attemptedActions;
2154
- rollback;
2155
- };
2156
- async function updateProject(options = {}) {
2157
- if (!existsSync(RSP_DIR)) {
2158
- return {
2159
- ok: false,
2160
- actions: [],
2161
- updated: false,
2162
- events: [{ type: "not-initialized" }],
2163
- migration: { inspectionComplete: false, manualMigrationRequired: false, residualEntries: [], diagnostics: [] }
2164
- };
1110
+ async function copySkillTree(source, targetRoot) {
1111
+ await mkdir(targetRoot, { recursive: true });
1112
+ for (const path of source.files) {
1113
+ const sourcePath = join(source.root, path);
1114
+ const targetPath = join(targetRoot, path);
1115
+ const stats = lstatSync(sourcePath);
1116
+ if (!stats.isFile())
1117
+ throw unsupportedEntry(sourcePath, `packaged Skill ${source.name}`);
1118
+ await mkdir(dirname(targetPath), { recursive: true });
1119
+ await copyFile(sourcePath, targetPath);
1120
+ await chmod(targetPath, stats.mode & 511);
1121
+ }
1122
+ }
1123
+ async function updatePackagedSkills(options = {}) {
1124
+ const packageRoot = options.packageRoot ?? PKG_ROOT;
1125
+ const projectRoot = options.projectRoot ?? process.cwd();
1126
+ const renamePath = options.renamePath ?? rename;
1127
+ const packaged = await inspectPackagedSkills(packageRoot);
1128
+ const skills = DEFAULT_PACKAGED_SKILL_NAMES.map((name) => {
1129
+ const skill = packaged.find((candidate) => candidate.name === name);
1130
+ if (!skill)
1131
+ throw new Error(`Packaged Skill missing: ${name}`);
1132
+ return skill;
1133
+ }).sort((a, b) => a.name.localeCompare(b.name));
1134
+ const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
1135
+ const result = { installed: [], removed: [], unchanged: [], replaced: [] };
1136
+ for (const skill of skills) {
1137
+ const target = join(targetRoot, skill.name);
1138
+ if (isCanonicalSourceProjection(skill.root, target))
1139
+ result.unchanged.push(skill.name);
1140
+ else if (inspectTarget(target, skill.name) === "missing")
1141
+ result.installed.push(skill.name);
1142
+ else if (await treesEqual(skill, target))
1143
+ result.unchanged.push(skill.name);
1144
+ else
1145
+ result.replaced.push(skill.name);
2165
1146
  }
2166
- return withRspLock("update", async () => {
2167
- const agentsPath = "AGENTS.md";
2168
- const agentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
2169
- if (agentsInspection.issue)
2170
- throw agentsInspection.issue;
2171
- let updated = false;
2172
- const actions = [];
2173
- const events = [];
2174
- clearConfigCache();
2175
- const configInspection = await inspectRspConfig();
2176
- if (configInspection.issues.length > 0)
2177
- throw new Error(configInspection.issues.join("; "));
2178
- const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
2179
- const decisionRecordsFilesystemIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath);
2180
- if (decisionRecordsFilesystemIssue)
2181
- throw new Error(decisionRecordsFilesystemIssue);
2182
- const specsMigrationPlan = await inspectGeneratedSpecsIndexMigration();
2183
- if (!specsMigrationPlan.inspectionComplete || specsMigrationPlan.ownerControlled.length > 0) {
2184
- const ownerMessage = specsMigrationPlan.ownerControlled.length > 0 ? ` Owner-controlled reserved path(s): ${specsMigrationPlan.ownerControlled.join(", ")}.` : "";
2185
- const diagnosticMessage = specsMigrationPlan.diagnostics.length > 0 ? ` ${specsMigrationPlan.diagnostics.join("; ")}` : "";
2186
- throw new Error(`Generated Specs-index migration requires owner review before update.${ownerMessage}${diagnosticMessage}`);
1147
+ for (const name of RETIRED_PACKAGED_SKILL_NAMES) {
1148
+ if (inspectTarget(join(targetRoot, name), name) !== "missing")
1149
+ result.removed.push(name);
1150
+ }
1151
+ if (options.dryRun || result.installed.length + result.replaced.length + result.removed.length === 0)
1152
+ return result;
1153
+ await mkdir(targetRoot, { recursive: true });
1154
+ resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
1155
+ const staging = await mkdtemp(join(targetRoot, ".rsp-skills-"));
1156
+ const completed = [];
1157
+ let failure2;
1158
+ try {
1159
+ for (const skill of skills) {
1160
+ if (!result.unchanged.includes(skill.name))
1161
+ await copySkillTree(skill, join(staging, skill.name));
2187
1162
  }
2188
- const rollbackJournal = new UpdateRollbackJournal(
2189
- process.cwd(),
2190
- options.testing?.rollback
2191
- );
2192
- try {
2193
- const configFileInspection = inspectManagedFile(CONFIG_PATH, "config file", { allowMissing: true });
2194
- if (configFileInspection.issue)
2195
- throw configFileInspection.issue;
2196
- const configContent = configFileInspection.exists ? await readFile4(CONFIG_PATH, "utf-8") : null;
2197
- const reconciledConfig = configContent === null ? { content: generateConfigTemplate(), added: ["all defaults"], changed: true } : reconcileRspConfigDefaults(configContent);
2198
- if (reconciledConfig.changed) {
2199
- await rollbackJournal.capture(CONFIG_PATH);
2200
- await writeManagedFile(CONFIG_PATH, reconciledConfig.content, "config file");
2201
- await rollbackJournal.mark(CONFIG_PATH);
2202
- const detail = reconciledConfig.added.length > 0 ? `defaults added: ${reconciledConfig.added.join(", ")}` : "generated layout normalized";
2203
- actions.push(`config.yaml ${detail}`);
2204
- events.push({ type: "updated", message: `config.yaml ${detail}` });
2205
- updated = true;
2206
- }
2207
- const changesInspection = inspectManagedDirectory(CHANGES_DIR, "open work root", { allowMissing: true });
2208
- if (changesInspection.issue)
2209
- throw changesInspection.issue;
2210
- if (!changesInspection.exists) {
2211
- const changesDirectoryChain = await rollbackJournal.captureDirectoryChain(CHANGES_DIR);
2212
- const changesPlaceholder = join3(CHANGES_DIR, ".gitkeep");
2213
- await rollbackJournal.capture(changesPlaceholder);
2214
- await mkdir3(CHANGES_DIR, { recursive: true });
2215
- await ensureManagedFile(changesPlaceholder, "", "changes placeholder");
2216
- await rollbackJournal.mark(changesPlaceholder);
2217
- await rollbackJournal.markAll(changesDirectoryChain);
2218
- actions.push("changes/ directory restored");
2219
- events.push({ type: "updated", message: "changes/ directory restored" });
2220
- updated = true;
2221
- }
2222
- const archivesInspection = inspectManagedDirectory(join3(RSP_DIR, "archives"), "archive root", { allowMissing: true });
2223
- if (archivesInspection.issue)
2224
- throw archivesInspection.issue;
2225
- const bundledRules = await readFile4(join3(options.testing?.packageRoot ?? PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
2226
- const rulesInspection = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
2227
- if (rulesInspection.issue)
2228
- throw rulesInspection.issue;
2229
- const existingRules = rulesInspection.exists ? await readFile4(RSP_RULES_PATH, "utf-8") : null;
2230
- if (existingRules !== bundledRules) {
2231
- await rollbackJournal.capture(RSP_RULES_PATH);
2232
- await writeManagedFile(RSP_RULES_PATH, bundledRules, "fallback protocol");
2233
- await rollbackJournal.mark(RSP_RULES_PATH);
2234
- actions.push("rsp-rules.md updated");
2235
- events.push({ type: "updated", message: "rsp-rules.md updated" });
2236
- updated = true;
2237
- }
2238
- if (existsSync(OBSOLETE_RSP_RULES_PATH)) {
2239
- await rollbackJournal.capture(OBSOLETE_RSP_RULES_PATH);
2240
- await rm(OBSOLETE_RSP_RULES_PATH);
2241
- await rollbackJournal.mark(OBSOLETE_RSP_RULES_PATH);
2242
- actions.push("obsolete rules/rsp-rules.md removed");
2243
- events.push({ type: "updated", message: "obsolete rules/rsp-rules.md removed" });
2244
- updated = true;
2245
- }
2246
- try {
2247
- const rulesRoot = join3(RSP_DIR, "rules");
2248
- await rollbackJournal.capture(rulesRoot);
2249
- await rmdir2(rulesRoot);
2250
- await rollbackJournal.mark(rulesRoot);
2251
- actions.push("empty rules directory removed");
2252
- updated = true;
2253
- } catch (error) {
2254
- if (!["ENOENT", "ENOTEMPTY"].includes(error.code || ""))
2255
- throw error;
2256
- }
2257
- let inspection = await inspectUnsupportedRules();
2258
- if (inspection.diagnostics.length === 0 && inspection.directoryRemaining && inspection.entries.every((entry) => entry.kind === "directory")) {
2259
- const directories = inspection.entries.map((entry) => entry.path).sort((left, right) => right.split("/").length - left.split("/").length);
2260
- for (const path of directories) {
2261
- const directoryPath = join3(RSP_DIR, "rules", path);
2262
- await rollbackJournal.capture(directoryPath);
2263
- await rmdir2(directoryPath);
2264
- await rollbackJournal.mark(directoryPath);
2265
- }
2266
- const rulesRoot = join3(RSP_DIR, "rules");
2267
- await rollbackJournal.capture(rulesRoot);
2268
- await rmdir2(rulesRoot);
2269
- await rollbackJournal.mark(rulesRoot);
2270
- actions.push("empty rules directory tree removed");
2271
- events.push({ type: "updated", message: "empty rules directory tree removed" });
2272
- updated = true;
2273
- inspection = await inspectUnsupportedRules();
2274
- }
2275
- const residualEntries = inspection.entries.map((entry) => entry.path);
2276
- if (inspection.diagnostics.length > 0)
2277
- events.push({ type: "migration-diagnostics", diagnostics: inspection.diagnostics });
2278
- if (residualEntries.length > 0)
2279
- events.push({ type: "residual-rules", entries: residualEntries });
2280
- const projectName = await detectProjectName();
2281
- const baseAgents = agentsInspection.exists ? await readFile4(agentsPath, "utf-8") : `# ${projectName}
2282
-
2283
- `;
2284
- const nextAgents = upsertRspAgentsBlock(baseAgents);
2285
- if (!agentsInspection.exists || nextAgents.changed) {
2286
- await rollbackJournal.capture(agentsPath);
2287
- await writeManagedFile(agentsPath, nextAgents.content, "AGENTS.md");
2288
- await rollbackJournal.mark(agentsPath);
2289
- actions.push("AGENTS.md managed block refreshed");
2290
- events.push({ type: "updated", message: "AGENTS.md managed block refreshed" });
2291
- updated = true;
2292
- }
2293
- const decisionRecordsDirectoryChain = await rollbackJournal.captureDirectoryChain(decisionRecordsPath);
2294
- const decisionRecordsPlaceholder = join3(decisionRecordsPath, ".gitkeep");
2295
- await rollbackJournal.capture(decisionRecordsPlaceholder);
2296
- if (await ensureDecisionRecordsDirectory(decisionRecordsPath)) {
2297
- await rollbackJournal.mark(decisionRecordsPlaceholder);
2298
- await rollbackJournal.markAll(decisionRecordsDirectoryChain);
2299
- actions.push(`Decision Record directory ensured: ${decisionRecordsPath}`);
2300
- events.push({ type: "updated", message: `Decision Record directory ensured: ${decisionRecordsPath}` });
2301
- updated = true;
2302
- }
2303
- const legacyArchiveIndexPath = join3(RSP_DIR, "archives", "INDEX.md");
2304
- await rollbackJournal.capture(legacyArchiveIndexPath);
2305
- if (await removeLegacyArchiveIndex()) {
2306
- await rollbackJournal.mark(legacyArchiveIndexPath);
2307
- actions.push("legacy Archive Index removed");
2308
- events.push({ type: "updated", message: "legacy Archive Index removed" });
2309
- updated = true;
2310
- }
2311
- for (const path of specsMigrationPlan.removable)
2312
- await rollbackJournal.capture(path);
2313
- await options.testing?.afterPreMutations?.();
2314
- const removedSpecsIndexes = await removeRecognizedGeneratedSpecsIndexes();
2315
- for (const path of specsMigrationPlan.removable)
2316
- await rollbackJournal.mark(path);
2317
- await options.testing?.afterSpecsMigration?.();
2318
- if (removedSpecsIndexes.length > 0) {
2319
- actions.push(`generated Specs indexes removed: ${removedSpecsIndexes.join(", ")}`);
2320
- events.push({ type: "specs-indexes-removed", paths: removedSpecsIndexes });
2321
- updated = true;
2322
- }
2323
- return {
2324
- ok: inspection.diagnostics.length === 0,
2325
- actions,
2326
- updated,
2327
- events,
2328
- migration: {
2329
- inspectionComplete: inspection.diagnostics.length === 0,
2330
- manualMigrationRequired: inspection.directoryRemaining,
2331
- residualEntries,
2332
- diagnostics: inspection.diagnostics
2333
- }
2334
- };
2335
- } catch (error) {
2336
- const rollback = await rollbackJournal.rollback();
2337
- clearConfigCache();
2338
- if (rollback.retainedMutations.length > 0 || rollback.recoveryPaths.length > 0) {
2339
- throw new UpdateTransactionError(
2340
- `RSP update failed and could not fully restore its command-scoped mutations: ${error instanceof Error ? error.message : String(error)}`,
2341
- actions,
2342
- rollback,
2343
- { cause: error }
2344
- );
2345
- }
2346
- throw error;
1163
+ for (const name of result.removed) {
1164
+ resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
1165
+ inspectTarget(join(targetRoot, name), name);
1166
+ await rm(join(targetRoot, name), { recursive: true, force: true });
1167
+ completed.push(`removed ${name}`);
2347
1168
  }
2348
- });
1169
+ for (const skill of skills) {
1170
+ if (result.unchanged.includes(skill.name))
1171
+ continue;
1172
+ resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
1173
+ const target = join(targetRoot, skill.name);
1174
+ inspectTarget(target, skill.name);
1175
+ await rm(target, { recursive: true, force: true });
1176
+ await renamePath(join(staging, skill.name), target);
1177
+ completed.push(`installed ${skill.name}`);
1178
+ }
1179
+ } catch (error) {
1180
+ failure2 = new Error(`Skill update stopped; completed: ${completed.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1181
+ }
1182
+ try {
1183
+ await rm(staging, { recursive: true });
1184
+ } catch (error) {
1185
+ failure2 = new Error(`Temporary Skill content cleanup failed: ${staging}`, { cause: failure2 ?? error });
1186
+ }
1187
+ if (failure2)
1188
+ throw failure2;
1189
+ return result;
2349
1190
  }
2350
1191
 
2351
1192
  // src/commands/doctor.ts
2352
1193
  async function runDoctor(options = {}) {
2353
- const fixed = [];
2354
- let repairIssue = null;
2355
- if (options.fix && existsSync2(RSP_DIR)) {
2356
- try {
2357
- const update = await updateProject({ testing: options.testing });
2358
- fixed.push(...update.actions);
2359
- } catch (error) {
2360
- if (error instanceof UpdateTransactionError) {
2361
- fixed.push(
2362
- ...error.rollback.retainedMutations.map((path) => `partial update mutation retained: ${path}`),
2363
- ...error.rollback.recoveryPaths.map((path) => `update recovery copy retained: ${path}`)
2364
- );
2365
- }
2366
- repairIssue = `safe deterministic repair could not run: ${toErrorMessage(error)}`;
2367
- }
2368
- }
2369
1194
  const checks = [];
2370
- if (repairIssue) {
1195
+ const runtime = [];
1196
+ const reportRuntime = (diagnostic) => runtime.push(diagnostic);
1197
+ reportCheck(checks, ".rsp exists", existsSync(RSP_DIR), "Run: rsp init");
1198
+ reportRetiredProtocol(checks);
1199
+ const sourceMode = await reportProjectRuntime(checks);
1200
+ if (options.checkUpdates) {
1201
+ const notice = await checkForUpdates(await getVersion());
2371
1202
  checks.push({
2372
- status: "issue",
2373
- label: "safe deterministic repairs completed",
2374
- message: repairIssue,
2375
- hint: "Resolve the reported configuration or filesystem issue, then run rsp doctor --fix again."
1203
+ status: "info",
1204
+ code: `update_${notice.status}`,
1205
+ label: "stable package update check",
1206
+ message: notice.status === "available" ? `RSP ${notice.latest} is available; running ${notice.current}. Project selection is unchanged.` : notice.status === "current" ? `No newer stable package found for ${notice.current}.` : "Update metadata is unavailable; local checks are unaffected.",
1207
+ hint: sourceMode ? "Stable package metadata does not establish source freshness. Keep using the local source CLI; this notice does not select a package runtime." : notice.status === "available" ? "Inspect migration guidance and choose an exact target. Upgrade notices authorize no update." : void 0
2376
1208
  });
2377
1209
  }
2378
- const runtime = [];
2379
- const reportRuntime = (diagnostic) => runtime.push(diagnostic);
2380
- const designPath = join4(RSP_DIR, "specs", "design.md");
2381
- reportCheck(checks, ".rsp exists", existsSync2(RSP_DIR), "Run: rsp init");
2382
- reportFallbackProtocol(checks);
2383
1210
  await reportUnsupportedRules(checks, reportRuntime);
2384
- reportCheck(checks, "specs/design.md exists", existsSync2(designPath), "Run: rsp init");
1211
+ const initialization = inspectRspInitialization();
1212
+ checks.push({ status: initialization ? "issue" : "ok", label: "specs/design.md exists", message: initialization });
2385
1213
  await checkAgents(checks, reportRuntime);
2386
- await checkSpecsTree(checks, reportRuntime);
1214
+ if (existsSync("CONTEXT-MAP.md")) {
1215
+ checks.push({
1216
+ status: "info",
1217
+ code: "legacy_context_map",
1218
+ label: "legacy root context map detected",
1219
+ message: "CONTEXT-MAP.md remains; its semantic migration requires owner review.",
1220
+ hint: "Use Core context migration. Retain relevant legacy context until an authorized merge updates CONTEXT.md and active references; update does not migrate or delete it."
1221
+ });
1222
+ }
2387
1223
  await checkArchiveNaming(checks);
2388
- const decisionRecordsConfigValid = await checkConfigSemantics(checks, reportRuntime);
2389
- if (decisionRecordsConfigValid) {
1224
+ const configValid = await checkConfigSemantics(checks, reportRuntime);
1225
+ if (configValid) {
1226
+ await checkSpecsTree(checks, reportRuntime);
2390
1227
  await checkDecisionRecordsDirectory(checks, reportRuntime);
2391
1228
  await checkInactiveDefaultDecisionRecords(checks, reportRuntime);
2392
1229
  }
2393
1230
  await checkActiveChangeConsistency(checks);
1231
+ if (configValid)
1232
+ await checkArchitecture(checks);
2394
1233
  const result = {
2395
1234
  command: "doctor",
2396
1235
  ok: checks.every((check) => check.status !== "issue"),
2397
- fixed,
2398
1236
  checks,
2399
1237
  runtime,
2400
1238
  summary: {
@@ -2403,32 +1241,88 @@ async function runDoctor(options = {}) {
2403
1241
  };
2404
1242
  return result;
2405
1243
  }
2406
- function reportFallbackProtocol(checks) {
2407
- const canonical = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
2408
- const obsoleteExists = existsSync2(OBSOLETE_RSP_RULES_PATH);
2409
- if (canonical.issue) {
1244
+ async function checkArchitecture(checks) {
1245
+ try {
1246
+ const architecture = await queryArchitecture();
1247
+ if (!architecture.configured) {
1248
+ checks.push({
1249
+ status: "info",
1250
+ code: "architecture_not_configured",
1251
+ label: ARCHITECTURE_FILE,
1252
+ message: "Optional architecture model is not configured."
1253
+ });
1254
+ } else if (architecture.diagnostics.length) {
1255
+ for (const diagnostic of architecture.diagnostics) {
1256
+ checks.push({
1257
+ status: "issue",
1258
+ code: `architecture_${diagnostic.code}`,
1259
+ label: `architecture binding: ${diagnostic.file ?? ARCHITECTURE_FILE}`,
1260
+ message: `${diagnostic.file ?? ARCHITECTURE_FILE}: ${diagnostic.message}`
1261
+ });
1262
+ }
1263
+ } else {
1264
+ checks.push({
1265
+ status: "ok",
1266
+ code: "architecture_valid",
1267
+ label: "architecture structure and document bindings",
1268
+ message: `${architecture.nodes.length} nodes, ${architecture.relations.length} relations, ${architecture.views.length} views. Structural and reference validity does not establish semantic agreement with implementation.`
1269
+ });
1270
+ }
1271
+ } catch (error) {
2410
1272
  checks.push({
2411
1273
  status: "issue",
2412
- label: "rsp-rules.md is a regular managed file",
2413
- message: canonical.issue.message,
2414
- hint: "Replace the unsupported entry with a project-local regular file, then run: rsp update"
1274
+ code: "architecture_inspection_failed",
1275
+ label: ARCHITECTURE_FILE,
1276
+ message: `${ARCHITECTURE_FILE}: ${toErrorMessage(error)}`
2415
1277
  });
2416
- } else if (canonical.exists) {
2417
- checks.push({ status: "ok", label: "rsp-rules.md exists" });
2418
- } else if (!obsoleteExists) {
1278
+ }
1279
+ }
1280
+ async function reportProjectRuntime(checks) {
1281
+ let sourceMode = false;
1282
+ try {
1283
+ const selected = await readProjectRuntime();
1284
+ sourceMode = selected.mode === "source";
1285
+ const running = await getVersion();
2419
1286
  checks.push({
2420
- status: "issue",
2421
- label: "rsp-rules.md exists",
2422
- hint: existsSync2(RSP_DIR) ? "Run: rsp update" : "Run: rsp init"
1287
+ status: selected.mode === "source" || selected.version === running ? "ok" : "issue",
1288
+ code: "project_cli_version",
1289
+ label: "project CLI runtime",
1290
+ message: selected.mode === "source" ? `Source mode: this checkout's local CLI; package metadata ${running} is not source identity.` : selected.version ? `Project pins ${selected.version}; running ${running}.` : "Project has no exact CLI pin.",
1291
+ hint: selected.mode === "source" ? `Build current source, then use ${SOURCE_INVOCATION} <command>. Retain commit, uncommitted diff and build inputs for candidate evidence.` : selected.version ? `Use ${exactInvocation(selected.version)} <command>; only an authorized migration uses update.` : "Choose an exact CLI version, then run its update command."
2423
1292
  });
1293
+ } catch (error) {
1294
+ checks.push({ status: "issue", code: "project_cli_version", label: "project CLI runtime", message: toErrorMessage(error) });
2424
1295
  }
2425
- if (obsoleteExists) {
1296
+ try {
1297
+ const inventory = await inspectPackagedSkillInventory();
1298
+ const drift = [
1299
+ ...inventory.skills.filter((skill) => skill.status !== "unchanged").map((skill) => `${skill.name}: ${skill.status}`),
1300
+ ...inventory.retired.map((name) => `${name}: retired`)
1301
+ ];
2426
1302
  checks.push({
2427
- status: "issue",
2428
- label: "obsolete fallback protocol path detected",
2429
- message: "obsolete fallback protocol path detected",
2430
- hint: "Run: rsp update"
1303
+ status: drift.length === 0 ? "ok" : "issue",
1304
+ code: "project_skill_suite",
1305
+ label: "project Skill suite matches running package",
1306
+ message: drift.length > 0 ? drift.join(", ") : void 0,
1307
+ hint: drift.length > 0 ? "Preserve needed custom content in managed Skills, then run the selected exact CLI: update." : void 0
2431
1308
  });
1309
+ } catch (error) {
1310
+ checks.push({ status: "issue", code: "project_skill_suite", label: "project Skill suite is inspectable", message: toErrorMessage(error) });
1311
+ }
1312
+ return sourceMode;
1313
+ }
1314
+ function reportRetiredProtocol(checks) {
1315
+ for (const path of [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH]) {
1316
+ const inspection = inspectManagedFile(path, "retired Rules file", { allowMissing: true });
1317
+ if (inspection.issue || inspection.exists) {
1318
+ checks.push({
1319
+ status: "issue",
1320
+ code: "retired_rules",
1321
+ label: `retired Rules path: ${path}`,
1322
+ message: inspection.issue?.message ?? "Rules are no longer read or generated.",
1323
+ hint: "Preserve needed custom content, then run update. Symlinks and special files require owner review."
1324
+ });
1325
+ }
2432
1326
  }
2433
1327
  }
2434
1328
  async function reportUnsupportedRules(checks, reportRuntime) {
@@ -2468,7 +1362,7 @@ async function checkAgents(checks, reportRuntime) {
2468
1362
  status: "issue",
2469
1363
  label: "AGENTS.md is a regular managed file",
2470
1364
  message: inspection.issue.message,
2471
- hint: "Replace the unsupported entry with a project-local regular file, then run: rsp update"
1365
+ hint: "Check AGENTS.md path type and permissions; preserve user content before correcting the entry. Then run: rsp update"
2472
1366
  });
2473
1367
  return;
2474
1368
  }
@@ -2478,7 +1372,7 @@ async function checkAgents(checks, reportRuntime) {
2478
1372
  }
2479
1373
  let agents;
2480
1374
  try {
2481
- agents = await readFile5("AGENTS.md", "utf-8");
1375
+ agents = await readFile3("AGENTS.md", "utf-8");
2482
1376
  } catch (error) {
2483
1377
  reportRuntime({
2484
1378
  code: "agents_read_failed",
@@ -2486,7 +1380,7 @@ async function checkAgents(checks, reportRuntime) {
2486
1380
  path: "AGENTS.md",
2487
1381
  message: toErrorMessage(error)
2488
1382
  });
2489
- checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md could not be read", hint: "Check AGENTS.md permissions or recreate it with rsp init" });
1383
+ checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md could not be read", hint: "Check AGENTS.md permissions and filesystem access; init/update do not repair unreadable user content." });
2490
1384
  return;
2491
1385
  }
2492
1386
  if (hasRspAgentsBlock(agents)) {
@@ -2537,7 +1431,7 @@ async function checkSpecsTree(checks, reportRuntime) {
2537
1431
  }
2538
1432
  }
2539
1433
  async function checkArchiveNaming(checks) {
2540
- const archivesDir = join4(RSP_DIR, "archives");
1434
+ const archivesDir = join2(RSP_DIR, "archives");
2541
1435
  const inspection = await inspectArchiveTree({ archivesDir });
2542
1436
  if (inspection.diagnostics.length > 0) {
2543
1437
  checks.push({
@@ -2549,7 +1443,7 @@ async function checkArchiveNaming(checks) {
2549
1443
  return;
2550
1444
  }
2551
1445
  checks.push({ status: "ok", label: "archives use supported managed paths" });
2552
- const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(relative4(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename(fp)) === false);
1446
+ const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(relative2(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename(fp)) === false);
2553
1447
  if (invalidFiles.length === 0) {
2554
1448
  checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
2555
1449
  return;
@@ -2558,7 +1452,7 @@ async function checkArchiveNaming(checks) {
2558
1452
  status: "issue",
2559
1453
  label: "archived change files follow the date_name.md convention",
2560
1454
  message: `archived change files with invalid names: ${invalidFiles.join(", ")}`,
2561
- hint: "Rename archive files to YYYY-MM-DD_name.md and run: rsp update"
1455
+ hint: "Rename archive files to YYYY-MM-DD_name.md, then rerun rsp doctor"
2562
1456
  });
2563
1457
  }
2564
1458
  async function checkConfigSemantics(checks, reportRuntime) {
@@ -2605,7 +1499,7 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
2605
1499
  status: "issue",
2606
1500
  label: `${decisionRecordsPath} is the authoritative Decision Record directory`,
2607
1501
  message: filesystemError,
2608
- hint: "Choose a project-relative directory that does not escape through a symlink."
1502
+ hint: "Correct decisions.path or its filesystem entry; it must resolve to a project-local directory. Preserve existing user content."
2609
1503
  });
2610
1504
  return;
2611
1505
  }
@@ -2616,10 +1510,12 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
2616
1510
  checks,
2617
1511
  `${decisionRecordsPath} is the authoritative Decision Record directory`,
2618
1512
  info.isDirectory(),
2619
- "Run: rsp update"
1513
+ "Correct the path type; init/update do not replace existing user content."
2620
1514
  );
2621
1515
  } catch (error) {
2622
- if (error.code !== "ENOENT") {
1516
+ const code = error.code;
1517
+ const missing = code === "ENOENT";
1518
+ if (!missing) {
2623
1519
  reportRuntime({
2624
1520
  code: "decision_records_path_stat_failed",
2625
1521
  operation: "stat",
@@ -2630,14 +1526,14 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
2630
1526
  checks.push({
2631
1527
  status: "issue",
2632
1528
  label: `${decisionRecordsPath} is the authoritative Decision Record directory`,
2633
- message: `${decisionRecordsPath} Decision Record directory is missing or unreadable`,
2634
- hint: "Run: rsp update"
1529
+ message: missing ? `${decisionRecordsPath} Decision Record directory is missing` : `Unable to inspect Decision Record directory ${decisionRecordsPath}: ${toErrorMessage(error)}`,
1530
+ hint: missing ? "Create the directory yourself, or run rsp init to create missing scaffolding. Update does not create user directories." : code === "EACCES" || code === "EPERM" ? "Check directory and parent permissions; init/update do not repair filesystem access." : "Resolve the filesystem error or correct decisions.path, then rerun rsp doctor."
2635
1531
  });
2636
1532
  }
2637
1533
  }
2638
1534
  async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
2639
1535
  const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
2640
- if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync2(DEFAULT_DECISION_RECORDS_PATH))
1536
+ if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync(DEFAULT_DECISION_RECORDS_PATH))
2641
1537
  return;
2642
1538
  let inspectionFailed = false;
2643
1539
  const inactiveRecords = (await walkMarkdownFiles(DEFAULT_DECISION_RECORDS_PATH, {
@@ -2645,7 +1541,7 @@ async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
2645
1541
  inspectionFailed = true;
2646
1542
  reportRuntime(diagnostic);
2647
1543
  }
2648
- })).map((path) => normalizeLogicalPath(relative4(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
1544
+ })).map((path) => normalizeLogicalPath(relative2(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
2649
1545
  if (inspectionFailed) {
2650
1546
  checks.push({
2651
1547
  status: "issue",
@@ -2899,7 +1795,7 @@ function emptyCollectionSummary() {
2899
1795
  }
2900
1796
 
2901
1797
  // src/commands/ready.ts
2902
- import { readFile as readFile6 } from "fs/promises";
1798
+ import { readFile as readFile4 } from "fs/promises";
2903
1799
  function readyError(name, error, kind) {
2904
1800
  return { command: "ready", ok: false, change: name || null, path: null, warnings: [], runtime: [], error, kind };
2905
1801
  }
@@ -2919,7 +1815,7 @@ async function showReady(name) {
2919
1815
  const runtime = [];
2920
1816
  let content;
2921
1817
  try {
2922
- content = await readFile6(srcPath, "utf-8");
1818
+ content = await readFile4(srcPath, "utf-8");
2923
1819
  } catch {
2924
1820
  return readyError(name, { code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, "read");
2925
1821
  }
@@ -2937,9 +1833,10 @@ async function showReady(name) {
2937
1833
  return readyError(name, { code: "invalid_config", message: `.rsp/config.yaml could not be parsed: ${toErrorMessage(error)}` }, "config");
2938
1834
  }
2939
1835
  const dependencyInspection = await inspectChangeDependencies();
2940
- const readinessDetails = collectArchiveReadiness(content, {
1836
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
1837
+ const readinessDetails = collectArchiveReadiness(document, {
2941
1838
  activeBlockers: dependencyInspection.activeBlockers.get(name),
2942
- documentDiagnostics: inspectChangeDocument(content, {
1839
+ documentDiagnostics: inspectChangeDocument(document, {
2943
1840
  name,
2944
1841
  validKinds: resolveKinds(configInspection.config),
2945
1842
  requiredSections: resolveRequiredSections(configInspection.config)
@@ -2962,7 +1859,7 @@ async function showReady(name) {
2962
1859
  }
2963
1860
 
2964
1861
  // src/commands/show.ts
2965
- import { readFile as readFile7 } from "fs/promises";
1862
+ import { readFile as readFile5 } from "fs/promises";
2966
1863
  function showError(error, runtime = []) {
2967
1864
  const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
2968
1865
  return { command: "show", ok: false, change: null, contextPaths: [], runtime, nextActions, error };
@@ -3006,14 +1903,15 @@ async function showChange(nameOrFocused, options = {}) {
3006
1903
  const srcPath = workRef.path;
3007
1904
  let content;
3008
1905
  try {
3009
- content = await readFile7(srcPath, "utf-8");
1906
+ content = await readFile5(srcPath, "utf-8");
3010
1907
  } catch {
3011
1908
  return showError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, runtime);
3012
1909
  }
3013
1910
  let kind = "\u2014";
1911
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
3014
1912
  let issues = [];
3015
1913
  try {
3016
- const fm = parseFrontmatter(content);
1914
+ const fm = parseFrontmatter(document.markdown);
3017
1915
  kind = fm?.kind ? String(fm.kind) : "\u2014";
3018
1916
  issues = parseIssueRelationships(fm);
3019
1917
  } catch (error) {
@@ -3034,7 +1932,7 @@ async function showChange(nameOrFocused, options = {}) {
3034
1932
  const markerPath = focusMarkers.get(name);
3035
1933
  let markerContent;
3036
1934
  try {
3037
- markerContent = await readFile7(markerPath);
1935
+ markerContent = await readFile5(markerPath);
3038
1936
  } catch (error) {
3039
1937
  reportRuntime({
3040
1938
  code: "focus_marker_read_failed",
@@ -3060,11 +1958,10 @@ async function showChange(nameOrFocused, options = {}) {
3060
1958
  recovery = capsule.recovery;
3061
1959
  }
3062
1960
  const dependencyInspection = await inspectChangeDependencies();
3063
- const blockers = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(content);
3064
- const scenarios = parseScenarios(content);
3065
- const readinessDetails = collectArchiveReadiness(content, { activeBlockers: blockers });
3066
- const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
3067
- const taskCheckboxes = countCheckboxes(getDocumentSectionBody(document, "tasks"));
1961
+ const blockers = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(document);
1962
+ const scenarios = parseScenarios(document.markdown);
1963
+ const readinessDetails = collectArchiveReadiness(document, { activeBlockers: blockers });
1964
+ const taskCheckboxes = countCheckboxes(getDocumentSectionMarkdown(document, "tasks"));
3068
1965
  const progress = {
3069
1966
  done: taskCheckboxes.done + readinessDetails.verifyCriticality.required.done,
3070
1967
  total: taskCheckboxes.total + readinessDetails.verifyCriticality.required.total
@@ -3221,13 +2118,13 @@ function publicQuery(query) {
3221
2118
  }
3222
2119
 
3223
2120
  // src/status/derive.ts
3224
- function deriveStatusView(snapshot2, options = {}) {
2121
+ function deriveStatusView(snapshot, options = {}) {
3225
2122
  const query = {
3226
2123
  focused: Boolean(options.focused),
3227
2124
  blocked: Boolean(options.blocked),
3228
2125
  stale: typeof options.stale === "number" ? options.stale : null
3229
2126
  };
3230
- const records = snapshot2.records.filter(({ output: record }) => {
2127
+ const records = snapshot.records.filter(({ output: record }) => {
3231
2128
  if (query.focused && !record.isFocused)
3232
2129
  return false;
3233
2130
  if (query.blocked && !record.isBlocked)
@@ -3237,26 +2134,26 @@ function deriveStatusView(snapshot2, options = {}) {
3237
2134
  return true;
3238
2135
  });
3239
2136
  const filtered = query.focused || query.blocked || query.stale !== null;
3240
- const ok = snapshot2.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
3241
- const nextActions = ok ? buildStatusNextActions(snapshot2.focused.length, snapshot2.records.map((record) => record.output), snapshot2.groups, filtered) : ["Run: rsp doctor"];
2137
+ const ok = snapshot.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
2138
+ const nextActions = ok ? buildStatusNextActions(snapshot.focused.length, snapshot.records.map((record) => record.output), snapshot.groups, filtered) : ["Run: rsp doctor"];
3242
2139
  return {
3243
- manage: snapshot2.manage,
3244
- language: snapshot2.language,
2140
+ workflow: snapshot.workflow,
2141
+ language: snapshot.language,
3245
2142
  query,
3246
- focused: snapshot2.focused,
2143
+ focused: snapshot.focused,
3247
2144
  records,
3248
- groups: snapshot2.groups,
3249
- plan: filterDependencyPlan(snapshot2.plan, new Set(records.map((record) => record.output.name))),
2145
+ groups: snapshot.groups,
2146
+ plan: filterDependencyPlan(snapshot.plan, new Set(records.map((record) => record.output.name))),
3250
2147
  summary: {
3251
2148
  total: records.length,
3252
2149
  focused: records.filter((record) => record.output.isFocused).length,
3253
2150
  blocked: records.filter((record) => record.output.isBlocked).length
3254
2151
  },
3255
2152
  nextActions,
3256
- archiveTrend: snapshot2.archiveTrend,
3257
- diagnostics: snapshot2.diagnostics,
3258
- runtime: snapshot2.runtime,
3259
- hasExecutableChanges: snapshot2.records.length > 0,
2153
+ archiveTrend: snapshot.archiveTrend,
2154
+ diagnostics: snapshot.diagnostics,
2155
+ runtime: snapshot.runtime,
2156
+ hasExecutableChanges: snapshot.records.length > 0,
3260
2157
  ok
3261
2158
  };
3262
2159
  }
@@ -3304,13 +2201,13 @@ function recommendOpenChange(records, groups) {
3304
2201
  }
3305
2202
  function filterDependencyPlan(plan, names) {
3306
2203
  const included = new Set(names);
3307
- let changed3 = true;
3308
- while (changed3) {
3309
- changed3 = false;
2204
+ let changed = true;
2205
+ while (changed) {
2206
+ changed = false;
3310
2207
  for (const edge of plan.edges) {
3311
2208
  if (included.has(edge.change) && !included.has(edge.requires)) {
3312
2209
  included.add(edge.requires);
3313
- changed3 = true;
2210
+ changed = true;
3314
2211
  }
3315
2212
  }
3316
2213
  }
@@ -3326,46 +2223,49 @@ function filterDependencyPlan(plan, names) {
3326
2223
 
3327
2224
  // src/commands/status.ts
3328
2225
  async function showStatus(options = {}) {
3329
- const snapshot2 = await inspectProjectStatus();
3330
- return deriveStatusView(snapshot2, options);
2226
+ const snapshot = await inspectProjectStatus();
2227
+ return deriveStatusView(snapshot, options);
3331
2228
  }
3332
2229
 
3333
2230
  // src/status/v3-json.ts
3334
- function toStatusJson(view) {
3335
- return {
2231
+ function hasActiveFilters(query) {
2232
+ return query.focused || query.blocked || query.stale !== null;
2233
+ }
2234
+ function toStatusJson(view, options = {}) {
2235
+ const output = {
3336
2236
  command: "status",
3337
2237
  ok: view.ok,
3338
- manage: view.manage,
3339
- language: view.language,
3340
- filters: view.query,
3341
2238
  focused: view.focused,
3342
2239
  records: view.records.map((record) => record.output),
3343
2240
  groups: view.groups,
3344
- plan: view.plan,
2241
+ plan: {
2242
+ nodes: view.plan.nodes,
2243
+ edges: view.plan.edges,
2244
+ blocked: view.plan.blocked,
2245
+ waves: view.plan.waves
2246
+ },
3345
2247
  summary: view.summary,
3346
- nextActions: view.nextActions,
3347
- archiveTrend: view.archiveTrend,
3348
- diagnostics: view.diagnostics,
3349
- runtime: view.runtime
2248
+ diagnostics: view.diagnostics
3350
2249
  };
2250
+ if (options.verbose || hasActiveFilters(view.query)) {
2251
+ output.filters = view.query;
2252
+ }
2253
+ if (options.verbose) {
2254
+ output.nextActions = view.nextActions;
2255
+ output.archiveTrend = view.archiveTrend;
2256
+ output.runtime = view.runtime;
2257
+ }
2258
+ return output;
3351
2259
  }
3352
2260
  function toStatusJsonError(error, options) {
3353
- return {
2261
+ const output = {
3354
2262
  command: "status",
3355
2263
  ok: false,
3356
- manage: { activation: "explicit", closeout: "manual" },
3357
- language: { artifacts: null, commit: null },
3358
- filters: {
3359
- focused: options.focused,
3360
- blocked: options.blocked,
3361
- stale: null
3362
- },
3363
2264
  focused: [],
3364
2265
  records: [],
3365
2266
  groups: [],
3366
2267
  plan: {
3367
2268
  nodes: [],
3368
- ready: [],
3369
2269
  edges: [],
3370
2270
  blocked: [],
3371
2271
  waves: []
@@ -3375,12 +2275,22 @@ function toStatusJsonError(error, options) {
3375
2275
  focused: 0,
3376
2276
  blocked: 0
3377
2277
  },
3378
- archiveTrend: [],
3379
- nextActions: [],
3380
2278
  diagnostics: [],
3381
- runtime: [],
3382
2279
  error
3383
2280
  };
2281
+ if (options.verbose || options.focused || options.blocked) {
2282
+ output.filters = {
2283
+ focused: options.focused,
2284
+ blocked: options.blocked,
2285
+ stale: null
2286
+ };
2287
+ }
2288
+ if (options.verbose) {
2289
+ output.archiveTrend = [];
2290
+ output.nextActions = [];
2291
+ output.runtime = [];
2292
+ }
2293
+ return output;
3384
2294
  }
3385
2295
 
3386
2296
  // src/cli/adapter.ts
@@ -3393,9 +2303,6 @@ async function executeCliCommand(adapter, args) {
3393
2303
  return result;
3394
2304
  }
3395
2305
 
3396
- // src/cli/presenters/inspection.ts
3397
- import { existsSync as existsSync3 } from "fs";
3398
-
3399
2306
  // src/status/plain.ts
3400
2307
  var COL_CHANGE = 32;
3401
2308
  var COL_KIND = 10;
@@ -3409,10 +2316,6 @@ function printStatusPlain(view, options = {}) {
3409
2316
  console.log();
3410
2317
  console.log(` ${pc.bold("RSP status")}`);
3411
2318
  console.log();
3412
- if (options.verbose)
3413
- console.log(` ${pc.bold("Manage:")} activation ${view.manage.activation} \xB7 closeout ${view.manage.closeout}`);
3414
- if (options.verbose && (view.language.artifacts !== null || view.language.commit !== null))
3415
- console.log(` ${pc.bold("Language:")} artifacts ${view.language.artifacts ?? "unset"} \xB7 commit ${view.language.commit ?? "unset"}`);
3416
2319
  if (options.verbose)
3417
2320
  console.log();
3418
2321
  for (const diagnostic of view.diagnostics) {
@@ -3542,6 +2445,25 @@ function printArchiveTrend(trend) {
3542
2445
  }
3543
2446
 
3544
2447
  // src/cli/presenters/inspection.ts
2448
+ function presentConfig(result, options) {
2449
+ if (options.json) {
2450
+ emitJson(result.ok ? result.summary : result, options);
2451
+ return;
2452
+ }
2453
+ if (!result.ok) {
2454
+ console.error(` ${pc.red("Error:")} ${result.diagnostics.map((diagnostic) => diagnostic.message).join("; ")}`);
2455
+ return;
2456
+ }
2457
+ console.log();
2458
+ console.log(` ${pc.bold("RSP config")}`);
2459
+ console.log(` ${pc.dim("Path:")} ${result.path}`);
2460
+ console.log(` ${pc.dim("Valid:")} ${pc.green("yes")}`);
2461
+ console.log(` ${pc.dim("Kinds:")} ${result.summary.kinds.join(", ")}`);
2462
+ console.log(` ${pc.dim("Decision Records:")} ${result.summary.decisions.path}`);
2463
+ console.log(` ${pc.dim("Workflow:")} delegation ${result.summary.workflow.delegation} \xB7 closeout ${result.summary.workflow.closeout}`);
2464
+ console.log(` ${pc.dim("Language:")} artifacts ${result.summary.language.artifacts ?? "unset"} \xB7 commit ${result.summary.language.commit ?? "unset"}`);
2465
+ console.log();
2466
+ }
3545
2467
  function presentStatus(result, options) {
3546
2468
  if (result.kind === "error") {
3547
2469
  if (options.json)
@@ -3553,7 +2475,7 @@ function presentStatus(result, options) {
3553
2475
  if (options.verbose && !options.json)
3554
2476
  printStatusRuntimeDiagnostics(result.view.runtime);
3555
2477
  if (options.json)
3556
- emitJson(toStatusJson(result.view), options);
2478
+ emitJson(toStatusJson(result.view, { verbose: options.verbose }), options);
3557
2479
  else
3558
2480
  printStatusPlain(result.view, { verbose: options.verbose });
3559
2481
  }
@@ -3694,13 +2616,6 @@ function presentDoctor(result, options) {
3694
2616
  console.log();
3695
2617
  console.log(` ${pc.bold("RSP doctor")}`);
3696
2618
  console.log();
3697
- if (result.fixed.length > 0) {
3698
- console.log(` ${pc.green("Fixed:")} ${result.fixed.join(", ")}`);
3699
- console.log();
3700
- } else if (options.fix && result.checks.every((check) => check.message?.startsWith("safe deterministic repair could not run:") !== true) && existsSync3(RSP_DIR)) {
3701
- console.log(` ${pc.dim("No safe fixes needed.")}`);
3702
- console.log();
3703
- }
3704
2619
  for (const check of result.checks)
3705
2620
  printDoctorCheck(check);
3706
2621
  if (result.summary.issues === 0)
@@ -3918,7 +2833,23 @@ function formatArchiveReady(value) {
3918
2833
  }
3919
2834
 
3920
2835
  // src/cli/commands/inspection.ts
3921
- var specsCommand = defineCommand({
2836
+ var configCommand = defineCommand2({
2837
+ meta: {
2838
+ name: "config",
2839
+ description: "Inspect effective project configuration without scanning work state"
2840
+ },
2841
+ args: {
2842
+ ...compactJsonArgs
2843
+ },
2844
+ async run({ args }) {
2845
+ await executeCliCommand({
2846
+ execute: () => showConfig(),
2847
+ present: (result) => presentConfig(result, args),
2848
+ exitCode: (result) => result.ok ? void 0 : 1
2849
+ }, args);
2850
+ }
2851
+ });
2852
+ var specsCommand = defineCommand2({
3922
2853
  meta: {
3923
2854
  name: "specs",
3924
2855
  description: "Inspect current Specs and Decision Records or run bounded literal search"
@@ -3957,7 +2888,7 @@ var specsCommand = defineCommand({
3957
2888
  }, args);
3958
2889
  }
3959
2890
  });
3960
- var statusCommand = defineCommand({
2891
+ var statusCommand = defineCommand2({
3961
2892
  meta: {
3962
2893
  name: "status",
3963
2894
  description: "Show project status summary with current focus information"
@@ -3980,7 +2911,7 @@ var statusCommand = defineCommand({
3980
2911
  ...compactJsonArgs,
3981
2912
  verbose: {
3982
2913
  type: "boolean",
3983
- description: "Print runtime diagnostics for suppressed I/O issues",
2914
+ description: "Show auxiliary status details and runtime diagnostics",
3984
2915
  default: false
3985
2916
  }
3986
2917
  },
@@ -3993,7 +2924,8 @@ var statusCommand = defineCommand({
3993
2924
  kind: "error",
3994
2925
  output: toStatusJsonError({ code: "invalid_stale_filter", message: "--stale must be a non-negative integer number of days" }, {
3995
2926
  focused: Boolean(args.focused),
3996
- blocked: Boolean(args.blocked)
2927
+ blocked: Boolean(args.blocked),
2928
+ verbose: Boolean(args.verbose)
3997
2929
  })
3998
2930
  };
3999
2931
  }
@@ -4004,7 +2936,7 @@ var statusCommand = defineCommand({
4004
2936
  }, args);
4005
2937
  }
4006
2938
  });
4007
- var checkCommand = defineCommand({
2939
+ var checkCommand = defineCommand2({
4008
2940
  meta: {
4009
2941
  name: "check",
4010
2942
  description: "Validate changes (frontmatter, sections, template text, deltas, scenarios)"
@@ -4030,33 +2962,35 @@ var checkCommand = defineCommand({
4030
2962
  }, args);
4031
2963
  }
4032
2964
  });
4033
- var doctorCommand = defineCommand({
2965
+ var doctorCommand = defineCommand2({
4034
2966
  meta: {
4035
2967
  name: "doctor",
4036
2968
  description: "Check RSP setup health and common integration issues"
4037
2969
  },
4038
2970
  args: {
4039
2971
  ...compactJsonArgs,
4040
- verbose: {
2972
+ "verbose": {
4041
2973
  type: "boolean",
4042
2974
  description: "Print runtime diagnostics for suppressed I/O issues",
4043
2975
  default: false
4044
2976
  },
4045
- fix: {
2977
+ "check-updates": {
4046
2978
  type: "boolean",
4047
- description: "Run safe deterministic repairs before reporting diagnostics",
2979
+ description: "Explicitly check stable registry metadata without upgrading; timeout is non-blocking",
4048
2980
  default: false
4049
2981
  }
4050
2982
  },
4051
- async run({ args }) {
2983
+ async run({ args, rawArgs }) {
2984
+ if (rawArgs.some((arg) => arg === "--fix" || arg.startsWith("--fix=") || arg === "--no-fix"))
2985
+ throw new Error("doctor is read-only; use update to reconcile managed assets");
4052
2986
  await executeCliCommand({
4053
- execute: () => runDoctor({ fix: Boolean(args.fix) }),
2987
+ execute: () => runDoctor({ checkUpdates: Boolean(args["check-updates"]) }),
4054
2988
  present: (result) => presentDoctor(result, args),
4055
2989
  exitCode: (result) => result.ok ? void 0 : 1
4056
2990
  }, args);
4057
2991
  }
4058
2992
  });
4059
- var readyCommand = defineCommand({
2993
+ var readyCommand = defineCommand2({
4060
2994
  meta: {
4061
2995
  name: "ready",
4062
2996
  description: "Preview archive readiness for a change without moving it"
@@ -4082,7 +3016,7 @@ var readyCommand = defineCommand({
4082
3016
  }, args);
4083
3017
  }
4084
3018
  });
4085
- var showCommand = defineCommand({
3019
+ var showCommand = defineCommand2({
4086
3020
  meta: {
4087
3021
  name: "show",
4088
3022
  description: "Show change context with readiness signals and relevant paths"
@@ -4113,7 +3047,7 @@ var showCommand = defineCommand({
4113
3047
  }, args);
4114
3048
  }
4115
3049
  });
4116
- var historyCommand = defineCommand({
3050
+ var historyCommand = defineCommand2({
4117
3051
  meta: {
4118
3052
  name: "history",
4119
3053
  description: "Query bounded archived Change summaries or one exact archived WorkRef"
@@ -4168,6 +3102,7 @@ var historyCommand = defineCommand({
4168
3102
  }
4169
3103
  });
4170
3104
  var inspectionCommands = {
3105
+ config: configCommand,
4171
3106
  specs: specsCommand,
4172
3107
  ready: readyCommand,
4173
3108
  show: showCommand,
@@ -4178,12 +3113,74 @@ var inspectionCommands = {
4178
3113
  };
4179
3114
 
4180
3115
  // src/cli/commands/setup.ts
4181
- import { defineCommand as defineCommand2 } from "citty";
3116
+ import { defineCommand as defineCommand3 } from "citty";
3117
+
3118
+ // src/commands/add-spec.ts
3119
+ import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
3120
+ import { join as join3 } from "path";
3121
+
3122
+ // src/core/lock.ts
3123
+ import { mkdir as mkdir2, readFile as readFile6, unlink, writeFile } from "fs/promises";
3124
+ async function withRspLock(operation, fn) {
3125
+ requireManagedDirectory(RSP_DIR, "RSP root", { allowMissing: true });
3126
+ await mkdir2(RSP_DIR, { recursive: true });
3127
+ try {
3128
+ await writeFile(LOCK_PATH, `${process.pid}
3129
+ ${operation}
3130
+ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
3131
+ } catch (err) {
3132
+ if (err.code === "EEXIST")
3133
+ await handleStaleLock(operation);
3134
+ else
3135
+ throw err;
3136
+ }
3137
+ try {
3138
+ return await fn();
3139
+ } finally {
3140
+ try {
3141
+ await unlink(LOCK_PATH);
3142
+ } catch {
3143
+ }
3144
+ }
3145
+ }
3146
+ async function handleStaleLock(operation) {
3147
+ try {
3148
+ const content = await readFile6(LOCK_PATH, "utf-8");
3149
+ const lines = content.trim().split("\n");
3150
+ const pid = Number(lines[0]);
3151
+ if (pid && Number.isFinite(pid)) {
3152
+ try {
3153
+ process.kill(pid, 0);
3154
+ const op = lines[1] || "unknown";
3155
+ const ts = lines[2] || "unknown";
3156
+ throw new Error(`RSP locked by pid ${pid} (${op}) since ${ts}`);
3157
+ } catch (e) {
3158
+ if (e instanceof Error && e.message.startsWith("RSP locked"))
3159
+ throw e;
3160
+ const code = e.code;
3161
+ if (code === "ESRCH") {
3162
+ await unlink(LOCK_PATH);
3163
+ await writeFile(LOCK_PATH, `${process.pid}
3164
+ ${operation}
3165
+ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
3166
+ return;
3167
+ }
3168
+ throw e;
3169
+ }
3170
+ } else {
3171
+ await unlink(LOCK_PATH);
3172
+ await writeFile(LOCK_PATH, `${process.pid}
3173
+ ${operation}
3174
+ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
3175
+ }
3176
+ } catch (e) {
3177
+ if (e instanceof Error && e.message.startsWith("RSP locked"))
3178
+ throw e;
3179
+ throw new Error(`RSP is locked. Remove ${LOCK_PATH} manually if no other process is running.`);
3180
+ }
3181
+ }
4182
3182
 
4183
3183
  // src/commands/add-spec.ts
4184
- import { existsSync as existsSync4 } from "fs";
4185
- import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
4186
- import { join as join5 } from "path";
4187
3184
  async function addSpec(name, projectName = "project") {
4188
3185
  if (!name || !isValidSpecName(name)) {
4189
3186
  return { ok: false, message: "spec name must be kebab-case with optional subdirectory" };
@@ -4194,13 +3191,9 @@ async function addSpec(name, projectName = "project") {
4194
3191
  if (["index", "00-index"].includes(name.split("/").at(-1) || "")) {
4195
3192
  return { ok: false, message: 'spec name "index" is reserved for Specs migration compatibility' };
4196
3193
  }
4197
- const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
4198
- const design = inspectManagedFile(join5(RSP_DIR, "specs", "design.md"), "design Spec", { allowMissing: true });
4199
- if (rules.issue || design.issue || !rules.exists || !design.exists) {
4200
- const initialized = existsSync4(RSP_DIR);
4201
- return { ok: false, message: `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
4202
- ${initialized ? "Run: rsp update" : "Run: rsp init"}` };
4203
- }
3194
+ const initialization = inspectRspInitialization();
3195
+ if (initialization)
3196
+ return { ok: false, message: initialization };
4204
3197
  try {
4205
3198
  const configInspection = await inspectRspConfig();
4206
3199
  if (configInspection.issues.length > 0) {
@@ -4210,14 +3203,14 @@ async function addSpec(name, projectName = "project") {
4210
3203
  return { ok: false, message: `.rsp/config.yaml could not be parsed: ${toErrorMessage(error)}` };
4211
3204
  }
4212
3205
  return withRspLock("add-spec", async () => {
4213
- const specsDir = join5(RSP_DIR, "specs");
3206
+ const specsDir = join3(RSP_DIR, "specs");
4214
3207
  const segments = name.split("/");
4215
3208
  const parent = resolveManagedDirectoryChain(specsDir, segments.slice(0, -1), "spec path");
4216
- const path = join5(parent, `${segments.at(-1)}.md`);
3209
+ const path = join3(parent, `${segments.at(-1)}.md`);
4217
3210
  const inspection = await inspectManagedFileTree(specsDir, "Specs");
4218
3211
  if (inspection.issues.length > 0)
4219
3212
  throw inspection.issues[0];
4220
- await mkdir4(parent, { recursive: true });
3213
+ await mkdir3(parent, { recursive: true });
4221
3214
  const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
4222
3215
  const content = name === "design" ? generateDesignContent(resolvedProjectName) : generateSpecContent(name);
4223
3216
  try {
@@ -4232,114 +3225,245 @@ async function addSpec(name, projectName = "project") {
4232
3225
  }
4233
3226
 
4234
3227
  // src/commands/init.ts
4235
- import { existsSync as existsSync5 } from "fs";
4236
- import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
4237
- import { join as join6 } from "path";
4238
- async function ensureFile(path, content) {
4239
- return ensureManagedFile(path, content, "managed file");
4240
- }
4241
- function toTitle(projectName) {
4242
- return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
3228
+ import { existsSync as existsSync2 } from "fs";
3229
+ import { mkdir as mkdir4, readdir as readdir3, readFile as readFile8 } from "fs/promises";
3230
+ import { join as join5 } from "path";
3231
+
3232
+ // src/core/setup-files.ts
3233
+ import { constants } from "fs";
3234
+ import { access, chmod as chmod2, link, mkdtemp as mkdtemp2, readFile as readFile7, rename as rename2, rm as rm2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
3235
+ import { dirname as dirname2, join as join4 } from "path";
3236
+ async function updateSetupFile(path, render) {
3237
+ const exists = requireManagedFile(path, "setup file", { allowMissing: true });
3238
+ const current = exists ? await readFile7(path, "utf8") : null;
3239
+ const next = render(current);
3240
+ if (next === current)
3241
+ return false;
3242
+ if (exists)
3243
+ await access(path, constants.W_OK);
3244
+ const temporary = await mkdtemp2(join4(dirname2(path), ".rsp-write-"));
3245
+ const prepared = join4(temporary, "next");
3246
+ let failure2;
3247
+ try {
3248
+ await writeFile3(prepared, next, { flag: "wx" });
3249
+ if (exists)
3250
+ await chmod2(prepared, (await stat2(path)).mode & 511);
3251
+ const stillExists = requireManagedFile(path, "setup file", { allowMissing: true });
3252
+ if (stillExists !== exists || stillExists && await readFile7(path, "utf8") !== current)
3253
+ throw new Error(`Setup file changed during update: ${path}`);
3254
+ if (exists)
3255
+ await rename2(prepared, path);
3256
+ else
3257
+ await link(prepared, path);
3258
+ } catch (error) {
3259
+ failure2 = error instanceof Error ? error : new Error(String(error));
3260
+ }
3261
+ try {
3262
+ await rm2(temporary, { recursive: true });
3263
+ } catch (error) {
3264
+ failure2 = new Error(`Temporary content cleanup failed: ${temporary}`, { cause: failure2 ?? error });
3265
+ }
3266
+ if (failure2)
3267
+ throw failure2;
3268
+ return true;
4243
3269
  }
3270
+
3271
+ // src/commands/init.ts
4244
3272
  async function initProject(args = {}) {
4245
- const isNew = !existsSync5(RSP_DIR);
3273
+ const isNew = !existsSync2(RSP_DIR);
4246
3274
  const projectName = await detectProjectName();
4247
3275
  const mode = args.agentsMode ?? "managed";
4248
- if (mode !== "managed" && mode !== "print") {
4249
- return { ok: false, message: `unsupported --agents-mode "${mode}". Use: managed or print` };
4250
- }
4251
- const agentsPath = "AGENTS.md";
4252
- const initialAgentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
4253
- if (initialAgentsInspection.issue)
4254
- throw initialAgentsInspection.issue;
3276
+ if (mode !== "managed" && mode !== "print")
3277
+ return { ok: false, message: "unsupported --agents-mode; use managed or print" };
4255
3278
  return withRspLock("init", async () => {
4256
- const agentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
4257
- if (agentsInspection.issue)
4258
- throw agentsInspection.issue;
4259
- if (existsSync5(CONFIG_PATH)) {
4260
- clearConfigCache();
4261
- const existingConfig = await inspectRspConfig();
4262
- if (existingConfig.issues.length > 0)
4263
- throw new Error(existingConfig.issues.join("; "));
4264
- const existingDecisionRecordsPath = resolveDecisionRecordsPath(existingConfig.config);
4265
- const filesystemIssue = await validateDecisionRecordsFilesystemPath(existingDecisionRecordsPath);
4266
- if (filesystemIssue)
4267
- throw new Error(filesystemIssue);
3279
+ const runtime = await selectProjectRuntime();
3280
+ const { version } = runtime;
3281
+ const retiredRules = [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH].map((path) => requireManagedFile(path, "retired Rules", { allowMissing: true })).some(Boolean);
3282
+ if (retiredRules)
3283
+ return { ok: false, message: "Retired RSP assets require migration; use the selected target CLI: rsp update" };
3284
+ requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true });
3285
+ clearConfigCache();
3286
+ const inspection = await inspectRspConfig();
3287
+ if (inspection.issues.length)
3288
+ throw new Error(inspection.issues.join("; "));
3289
+ const decisionRecordsPath = resolveDecisionRecordsPath(inspection.config);
3290
+ const pathIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath);
3291
+ if (pathIssue)
3292
+ throw new Error(pathIssue);
3293
+ const dirs = [join5(RSP_DIR, "specs"), CHANGES_DIR, FOCUS_DIR, join5(RSP_DIR, "archives"), decisionRecordsPath];
3294
+ for (const path of dirs)
3295
+ resolveManagedDirectoryChain(process.cwd(), path.split("/"), "project directory");
3296
+ const completed = [];
3297
+ async function ensureFile(path, content) {
3298
+ if (await updateSetupFile(path, (current) => current ?? content))
3299
+ completed.push(`created ${path}`);
4268
3300
  }
4269
- const dirs = [
4270
- { path: join6(RSP_DIR, "specs"), label: "specs root" },
4271
- { path: CHANGES_DIR, label: "open work root" },
4272
- { path: FOCUS_DIR, label: "focus root" },
4273
- { path: join6(RSP_DIR, "archives"), label: "archive root" }
4274
- ];
4275
- for (const directory of dirs)
4276
- requireManagedDirectory(directory.path, directory.label, { allowMissing: true });
4277
- for (const directory of dirs)
4278
- await mkdir5(directory.path, { recursive: true });
4279
- let created = false;
4280
- const bundledRules = await readFile8(join6(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
4281
- created = await ensureFile(RSP_RULES_PATH, bundledRules) || created;
4282
- if (existsSync5(CONFIG_PATH)) {
4283
- const existing = await readFile8(CONFIG_PATH, "utf-8");
4284
- const reconciled = reconcileRspConfigDefaults(existing);
4285
- if (reconciled.changed) {
4286
- await writeManagedFile(CONFIG_PATH, reconciled.content, "config file");
4287
- created = true;
3301
+ try {
3302
+ const skills = await updatePackagedSkills();
3303
+ completed.push(...skills.installed.map((name) => `installed ${name}`), ...skills.replaced.map((name) => `updated ${name}`), ...skills.removed.map((name) => `removed ${name}`));
3304
+ for (const path of dirs) {
3305
+ if (!requireManagedDirectory(path, "project directory", { allowMissing: true })) {
3306
+ await mkdir4(path, { recursive: true });
3307
+ requireManagedDirectory(path, "project directory");
3308
+ completed.push(`created ${path}`);
3309
+ }
4288
3310
  }
4289
- } else {
4290
- created = await ensureFile(join6(RSP_DIR, "config.yaml"), generateConfigTemplate()) || created;
3311
+ await ensureFile(CONFIG_PATH, generateConfigTemplate());
3312
+ clearConfigCache();
3313
+ await ensureFile(join5(RSP_DIR, "specs/design.md"), generateDesignContent(projectName));
3314
+ if ((await readdir3(decisionRecordsPath)).length === 0)
3315
+ await ensureFile(join5(decisionRecordsPath, ".gitkeep"), "");
3316
+ await ensureFile(join5(CHANGES_DIR, ".gitkeep"), "");
3317
+ await ensureFile(join5(FOCUS_DIR, ".gitkeep"), "");
3318
+ await ensureFile(join5(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n");
3319
+ if (args.withProjectSetup) {
3320
+ const work = resolveWorkRef("project-setup", { executable: true });
3321
+ await ensureFile(work.path, generateChangeContent("project-setup"));
3322
+ await ensureFile(resolveFocusMarkerPath(work), "");
3323
+ }
3324
+ const title = projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
3325
+ const agentsUpdated = await updateSetupFile("AGENTS.md", (current) => upsertRspAgentsBlock(current ?? `# ${title}
3326
+
3327
+ `, runtime).content);
3328
+ if (agentsUpdated)
3329
+ completed.push("updated AGENTS.md managed block");
3330
+ if (runtime.mode === "package" && await updateSetupFile(PROJECT_VERSION_PATH, () => `${version}
3331
+ `))
3332
+ completed.push(`pinned ${version}`);
3333
+ return { ok: true, isNew, created: completed.length > 0, decisionRecordsPath, withProjectSetup: Boolean(args.withProjectSetup), agentsUpdated, skills, managedAgents: mode === "print" ? await readFile8("AGENTS.md", "utf8") : void 0 };
3334
+ } catch (error) {
3335
+ throw new Error(`Init stopped; completed: ${completed.join(", ") || "none"}. No rollback was attempted. Fix the cause and rerun init with the same target CLI. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
4291
3336
  }
3337
+ });
3338
+ }
3339
+
3340
+ // src/commands/update.ts
3341
+ import { existsSync as existsSync3 } from "fs";
3342
+ import { readFile as readFile11, unlink as unlink3 } from "fs/promises";
3343
+ import { join as join7 } from "path";
3344
+
3345
+ // src/commands/archive-index-migration.ts
3346
+ import { readFile as readFile9 } from "fs/promises";
3347
+ import { join as join6 } from "path";
3348
+ async function isLegacyArchiveIndex() {
3349
+ const path = join6(ARCHIVES_DIR, "INDEX.md");
3350
+ if (!requireManagedFile(path, "legacy archive index", { allowMissing: true }))
3351
+ return false;
3352
+ return isLegacyArchiveIndexContent(await readFile9(path, "utf-8"));
3353
+ }
3354
+ function isLegacyArchiveIndexContent(content) {
3355
+ try {
3356
+ const frontmatter = parseFrontmatter(content);
3357
+ return frontmatter?.kind === "generated-index" && frontmatter.index_type === "archives";
3358
+ } catch {
3359
+ return false;
3360
+ }
3361
+ }
3362
+
3363
+ // src/commands/specs-index-migration.ts
3364
+ import { readFile as readFile10, unlink as unlink2 } from "fs/promises";
3365
+ import { dirname as dirname3, resolve } from "path";
3366
+ async function inspectGeneratedSpecsIndexMigration(cwd = process.cwd()) {
3367
+ const inspection = await inspectSpecs({ cwd, allowMissingDecisionRecords: true });
3368
+ return {
3369
+ inspectionComplete: specsInspectionComplete(inspection),
3370
+ removable: inspection.generatedIndexes.filter((index) => index.classification === "safe-removal").map((index) => index.path).sort(),
3371
+ ownerControlled: inspection.generatedIndexes.filter((index) => index.classification === "owner-controlled").map((index) => index.path).sort(),
3372
+ diagnostics: inspection.diagnostics.map((diagnostic) => `${diagnostic.path}: ${diagnostic.message}`)
3373
+ };
3374
+ }
3375
+ async function removeGeneratedSpecsIndex(path) {
3376
+ resolveManagedDirectoryChain(process.cwd(), dirname3(path).split("/"), "Specs parent");
3377
+ if (!requireManagedFile(path, "generated Specs index", { allowMissing: true }))
3378
+ return false;
3379
+ if (!isRecognizedGeneratedSpecsIndex(await readFile10(resolve(path), "utf8"), path))
3380
+ throw new Error(`Specs index is no longer generated; left untouched: ${path}`);
3381
+ await unlink2(path);
3382
+ return true;
3383
+ }
3384
+
3385
+ // src/commands/update.ts
3386
+ async function updateProject(options = {}) {
3387
+ if (!existsSync3(RSP_DIR))
3388
+ return { ok: false, actions: [], updated: false, events: [{ type: "not-initialized" }], migration: { inspectionComplete: false, manualMigrationRequired: false, residualEntries: [], diagnostics: [] } };
3389
+ const execute = async () => {
3390
+ const runtime = await selectProjectRuntime("update");
3391
+ const { version } = runtime;
4292
3392
  clearConfigCache();
4293
- const configInspection = await inspectRspConfig();
4294
- if (configInspection.issues.length > 0)
4295
- throw new Error(configInspection.issues.join("; "));
4296
- const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
4297
- created = await ensureDecisionRecordsDirectory(decisionRecordsPath) || created;
4298
- const createdDesign = await ensureFile(join6(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
4299
- created = createdDesign || created;
4300
- created = await removeLegacyArchiveIndex() || created;
4301
- created = await ensureFile(join6(CHANGES_DIR, ".gitkeep"), "") || created;
4302
- created = await ensureFile(join6(FOCUS_DIR, ".gitkeep"), "") || created;
4303
- created = await ensureFile(join6(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
4304
- if (args.withProjectSetup) {
4305
- const projectSetupRef = resolveWorkRef("project-setup", { executable: true });
4306
- const changePath = projectSetupRef.path;
4307
- const focusMarker = resolveFocusMarkerPath(projectSetupRef);
4308
- created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
4309
- created = await ensureFile(focusMarker, "") || created;
4310
- }
4311
- const title = toTitle(projectName);
4312
- let agentsUpdated = false;
4313
- if (!agentsInspection.exists) {
4314
- await writeManagedFile(agentsPath, `# ${title}
3393
+ const config = await inspectRspConfig();
3394
+ if (config.issues.length)
3395
+ throw new Error(config.issues.join("; "));
3396
+ resolveManagedDirectoryChain(process.cwd(), [RSP_DIR, "rules"], "retired Rules parent");
3397
+ const retired = [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH].filter((path) => requireManagedFile(path, "retired Rules", { allowMissing: true }));
3398
+ const rules = await inspectUnsupportedRules();
3399
+ const migration = { inspectionComplete: rules.diagnostics.length === 0, manualMigrationRequired: rules.entries.length > 0, residualEntries: rules.entries.map((entry) => entry.path), diagnostics: rules.diagnostics };
3400
+ if (!migration.inspectionComplete)
3401
+ return { ok: false, actions: [], updated: false, events: [{ type: "migration-diagnostics", diagnostics: rules.diagnostics }], migration };
3402
+ requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true });
3403
+ resolveManagedDirectoryChain(process.cwd(), [RSP_DIR, "archives"], "archive parent");
3404
+ requireManagedFile(join7(RSP_DIR, "archives/INDEX.md"), "archive index", { allowMissing: true });
3405
+ const plan = await inspectGeneratedSpecsIndexMigration();
3406
+ if (!plan.inspectionComplete || plan.ownerControlled.length)
3407
+ throw new Error(`Generated Specs-index migration requires owner review before update: ${[...plan.ownerControlled, ...plan.diagnostics].join("; ")}`);
3408
+ const projectName = await detectProjectName();
3409
+ const agents = requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true }) ? await readFile11("AGENTS.md", "utf8") : null;
3410
+ const refreshAgents = (current) => upsertRspAgentsBlock(current ?? `# ${projectName}
4315
3411
 
4316
- ${upsertRspAgentsBlock("").content}
4317
- `, "AGENTS.md");
4318
- created = true;
4319
- agentsUpdated = true;
4320
- } else {
4321
- const existing = await readFile8(agentsPath, "utf-8");
4322
- const next = upsertRspAgentsBlock(existing);
4323
- if (next.changed) {
4324
- await writeManagedFile(agentsPath, next.content, "AGENTS.md");
4325
- created = true;
4326
- agentsUpdated = true;
3412
+ `, runtime).content;
3413
+ const removeArchive = await isLegacyArchiveIndex();
3414
+ const actions = [];
3415
+ const events = [];
3416
+ if (existsSync3("CONTEXT-MAP.md"))
3417
+ events.push({ type: "legacy-context-map" });
3418
+ if (migration.residualEntries.length)
3419
+ events.push({ type: "residual-rules", entries: migration.residualEntries });
3420
+ const record = (message) => {
3421
+ actions.push(message);
3422
+ events.push({ type: "updated", message });
3423
+ };
3424
+ try {
3425
+ const skills = await updatePackagedSkills({ dryRun: options.dryRun });
3426
+ for (const name of skills.installed) record(`Skill installed: ${name}`);
3427
+ for (const name of skills.replaced) record(`Skill updated: ${name}`);
3428
+ for (const name of skills.removed) record(`retired Skill removed: ${name}`);
3429
+ if (options.dryRun) {
3430
+ actions.push(...retired.map((path) => `retired Rules file removed: ${path}`), ...plan.removable.map((path) => `generated Specs index removed: ${path}`));
3431
+ if (removeArchive)
3432
+ actions.push("legacy Archive Index removed");
3433
+ if (refreshAgents(agents) !== agents)
3434
+ actions.push("AGENTS.md managed block refreshed");
3435
+ if (runtime.mode === "package" && await readProjectVersion() !== version)
3436
+ actions.push(`project CLI pinned to ${version}`);
3437
+ return { ok: true, actions, updated: false, skills, events: [], migration };
3438
+ }
3439
+ for (const path of retired) {
3440
+ if (requireManagedFile(path, "retired Rules", { allowMissing: true })) {
3441
+ await unlink3(path);
3442
+ record(`retired Rules file removed: ${path}`);
3443
+ }
4327
3444
  }
3445
+ for (const path of plan.removable) {
3446
+ if (await removeGeneratedSpecsIndex(path))
3447
+ record(`generated Specs index removed: ${path}`);
3448
+ }
3449
+ if (await isLegacyArchiveIndex()) {
3450
+ await unlink3(join7(RSP_DIR, "archives/INDEX.md"));
3451
+ record("legacy Archive Index removed");
3452
+ }
3453
+ if (await updateSetupFile("AGENTS.md", refreshAgents))
3454
+ record("AGENTS.md managed block refreshed");
3455
+ if (runtime.mode === "package" && await updateSetupFile(PROJECT_VERSION_PATH, () => `${version}
3456
+ `))
3457
+ record(`project CLI pinned to ${version}`);
3458
+ return { ok: true, actions, updated: actions.length > 0, skills, events, migration };
3459
+ } catch (error) {
3460
+ throw new Error(`Update stopped; completed: ${actions.join(", ") || "none"}. No rollback was attempted. Fix the cause and rerun update with the same target CLI. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
4328
3461
  }
4329
- return {
4330
- ok: true,
4331
- isNew,
4332
- created,
4333
- decisionRecordsPath,
4334
- withProjectSetup: Boolean(args.withProjectSetup),
4335
- agentsUpdated,
4336
- managedAgents: mode === "print" ? await readFile8(agentsPath, "utf-8") : void 0
4337
- };
4338
- });
3462
+ };
3463
+ return options.dryRun ? execute() : withRspLock("update", execute);
4339
3464
  }
4340
3465
 
4341
3466
  // src/cli/presenters/setup.ts
4342
- 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";
4343
3467
  function presentAddSpec(result) {
4344
3468
  if (!result.ok) {
4345
3469
  console.error(` ${pc.red("Error:")} ${result.message}`);
@@ -4360,11 +3484,12 @@ ${result.managedAgents}
4360
3484
  if (result.isNew) {
4361
3485
  const createdProjectSetup = result.withProjectSetup ? "\n .rsp/changes/project-setup.md\n .rsp/focus.d/project-setup" : "";
4362
3486
  const createdAgents = result.agentsUpdated ? "\n AGENTS.md" : "";
4363
- const next = result.withProjectSetup ? "fill .rsp/changes/project-setup.md" : "rsp create project-setup";
3487
+ const next = result.withProjectSetup ? "fill .rsp/changes/project-setup.md" : "rsp status";
4364
3488
  console.log(`
4365
3489
  ${pc.green("RSP scaffolded.")}
4366
3490
  `);
4367
- console.log(` Created: .rsp/rsp-rules.md
3491
+ console.log(` Created: complete core Skill suite
3492
+ .rsp/version
4368
3493
  .rsp/specs/
4369
3494
  ${result.decisionRecordsPath}/
4370
3495
  .rsp/changes/
@@ -4388,7 +3513,13 @@ ${result.managedAgents}
4388
3513
  `);
4389
3514
  }
4390
3515
  }
4391
- function presentUpdate(result) {
3516
+ function presentUpdate(result, dryRun = false) {
3517
+ if (dryRun && result.ok) {
3518
+ console.log(" Update preview (no files changed):");
3519
+ for (const action of result.actions)
3520
+ console.log(` ${action}`);
3521
+ return;
3522
+ }
4392
3523
  if (!result.ok && result.events.some((event) => event.type === "not-initialized")) {
4393
3524
  console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
4394
3525
  console.error(` ${pc.dim("Run: rsp init")}`);
@@ -4410,10 +3541,9 @@ function presentUpdate(result) {
4410
3541
  console.log(` ${pc.dim("Move stable scoped instructions to the nearest project-owned AGENTS.md, then remove the old files.")}`);
4411
3542
  console.log(` ${pc.dim("Run: rsp doctor")}`);
4412
3543
  console.log();
4413
- } else if (event.type === "specs-indexes-removed") {
4414
- console.log(` ${pc.green("\u2713")} generated Specs indexes removed`);
4415
- for (const path of event.paths)
4416
- console.log(` ${path}`);
3544
+ } else if (event.type === "legacy-context-map") {
3545
+ console.log(` ${pc.yellow("Note:")} CONTEXT-MAP.md remains; context migration needs owner review.`);
3546
+ console.log(` ${pc.dim("Use Core context migration. Keep relevant legacy context until an authorized merge updates CONTEXT.md and active references; update does not migrate or delete it.")}`);
4417
3547
  }
4418
3548
  }
4419
3549
  if (!result.migration.inspectionComplete)
@@ -4428,11 +3558,10 @@ function presentUpdate(result) {
4428
3558
  else
4429
3559
  console.log(` ${pc.green("Update complete.")}
4430
3560
  `);
4431
- console.log(SKILL_REFRESH_HINT);
4432
3561
  }
4433
3562
 
4434
3563
  // src/cli/commands/setup.ts
4435
- var initCommand = defineCommand2({
3564
+ var initCommand = defineCommand3({
4436
3565
  meta: {
4437
3566
  name: "init",
4438
3567
  description: "Scaffold .rsp/ and ensure AGENTS.md contains the RSP entry block"
@@ -4460,7 +3589,7 @@ var initCommand = defineCommand2({
4460
3589
  }, args);
4461
3590
  }
4462
3591
  });
4463
- var addSpecCommand = defineCommand2({
3592
+ var addSpecCommand = defineCommand3({
4464
3593
  meta: {
4465
3594
  name: "spec",
4466
3595
  description: "Create .rsp/specs/<name>.md for direct Specs queries"
@@ -4480,7 +3609,7 @@ var addSpecCommand = defineCommand2({
4480
3609
  }, args);
4481
3610
  }
4482
3611
  });
4483
- var addCommand = defineCommand2({
3612
+ var addCommand = defineCommand3({
4484
3613
  meta: {
4485
3614
  name: "add",
4486
3615
  description: "Add an optional durable spec file"
@@ -4489,17 +3618,22 @@ var addCommand = defineCommand2({
4489
3618
  spec: addSpecCommand
4490
3619
  }
4491
3620
  });
4492
- var updateCommand = defineCommand2({
3621
+ var updateCommand = defineCommand3({
4493
3622
  meta: {
4494
3623
  name: "update",
4495
- description: "Refresh RSP-managed project files after upgrade; does not update packaged Skills"
3624
+ description: "Update the complete pinned environment and remove retired managed assets"
3625
+ },
3626
+ args: {
3627
+ "dry-run": { type: "boolean", description: "Preview the complete update without changing files", default: false }
4496
3628
  },
4497
- async run() {
3629
+ async run({ args, rawArgs }) {
3630
+ if (args._.length || rawArgs.some((arg) => !["--dry-run", "--dry-run=true", "--dry-run=false", "--no-dry-run"].includes(arg)))
3631
+ throw new Error("rsp update accepts only --dry-run; use the selected target CLI to update all managed assets");
4498
3632
  await executeCliCommand({
4499
- execute: () => updateProject(),
4500
- present: presentUpdate,
3633
+ execute: () => updateProject({ dryRun: Boolean(args["dry-run"]) }),
3634
+ present: (result) => presentUpdate(result, Boolean(args["dry-run"])),
4501
3635
  exitCode: (result) => result.migration.inspectionComplete ? void 0 : 1
4502
- }, {});
3636
+ }, args);
4503
3637
  }
4504
3638
  });
4505
3639
  var setupCommands = {
@@ -4509,50 +3643,27 @@ var setupCommands = {
4509
3643
  };
4510
3644
 
4511
3645
  // src/cli/commands/skills.ts
4512
- import { defineCommand as defineCommand3 } from "citty";
4513
- var skillsInstallCommand = defineCommand3({
4514
- meta: {
4515
- name: "install",
4516
- description: "Install this package's bundled Skills into .agents/skills"
4517
- },
4518
- args: {
4519
- "name": {
4520
- type: "positional",
4521
- description: "Exact packaged Skill name (default: lifecycle suite)",
4522
- required: false
4523
- },
4524
- "dry-run": {
4525
- type: "boolean",
4526
- description: "Preflight selected packaged Skills without changing files",
4527
- default: false
4528
- },
4529
- "force": {
4530
- type: "boolean",
4531
- description: "Replace divergent package-owned Skill directories",
4532
- default: false
4533
- }
4534
- },
4535
- async run({ args }) {
4536
- if (args._.length > 1)
4537
- throw new Error("rsp skills install accepts at most one Skill name");
4538
- await executeCliCommand({
4539
- execute: () => installPackagedSkills({
4540
- dryRun: Boolean(args["dry-run"]),
4541
- force: Boolean(args.force),
4542
- names: args.name ? [args.name] : void 0
4543
- }),
4544
- present: (result) => presentSkillInstallResult(result, Boolean(args["dry-run"]))
4545
- }, args);
3646
+ import { defineCommand as defineCommand4 } from "citty";
3647
+
3648
+ // src/cli/presenters/skills.ts
3649
+ function presentPackagedSkillInventory(inventory, json = false) {
3650
+ if (json) {
3651
+ emitJson(inventory);
3652
+ return;
4546
3653
  }
4547
- });
4548
- var skillsListCommand = defineCommand3({
4549
- meta: {
4550
- name: "list",
4551
- description: "List package-bundled Skills and their project status"
4552
- },
4553
- args: {
4554
- ...jsonArgs
4555
- },
3654
+ console.log(` ${inventory.package.name}@${inventory.package.version}`);
3655
+ console.log(` target: ${inventory.target}`);
3656
+ console.log(" Core suite (managed by init/update)");
3657
+ for (const skill of inventory.skills)
3658
+ console.log(` ${skill.name} ${skill.status}`);
3659
+ for (const name of inventory.retired)
3660
+ console.log(` ${name} retired`);
3661
+ }
3662
+
3663
+ // src/cli/commands/skills.ts
3664
+ var skillsListCommand = defineCommand4({
3665
+ meta: { name: "list", description: "Inspect the complete package-owned Skill inventory" },
3666
+ args: { ...jsonArgs },
4556
3667
  async run({ args }) {
4557
3668
  await executeCliCommand({
4558
3669
  execute: () => inspectPackagedSkillInventory(),
@@ -4560,32 +3671,25 @@ var skillsListCommand = defineCommand3({
4560
3671
  }, args);
4561
3672
  }
4562
3673
  });
4563
- var skillsCommand = defineCommand3({
4564
- meta: {
4565
- name: "skills",
4566
- description: "Manage package-bundled project Skills"
4567
- },
4568
- subCommands: {
4569
- install: skillsInstallCommand,
4570
- list: skillsListCommand
4571
- }
4572
- });
4573
3674
  var skillsCommands = {
4574
- skills: skillsCommand
3675
+ skills: defineCommand4({
3676
+ meta: { name: "skills", description: "Inspect Skills; use init or update to manage the complete environment" },
3677
+ subCommands: { list: skillsListCommand }
3678
+ })
4575
3679
  };
4576
3680
 
4577
3681
  // src/cli/commands/work.ts
4578
- import { defineCommand as defineCommand4 } from "citty";
3682
+ import { defineCommand as defineCommand5 } from "citty";
4579
3683
 
4580
3684
  // src/commands/archive.ts
4581
- import { existsSync as existsSync6, readdirSync } from "fs";
4582
- import { mkdir as mkdir6, readFile as readFile9, rename as rename3, unlink as unlink5 } from "fs/promises";
4583
- import { basename as basename2, join as join7 } from "path";
3685
+ import { existsSync as existsSync4, readdirSync } from "fs";
3686
+ import { mkdir as mkdir5, readFile as readFile12, rename as rename3, unlink as unlink4 } from "fs/promises";
3687
+ import { basename as basename2, join as join8 } from "path";
4584
3688
  async function archiveChange(name) {
4585
3689
  if (!name) {
4586
3690
  return { ok: false, kind: "usage", message: "rsp archive <name>" };
4587
3691
  }
4588
- const initialization = inspectInitialization();
3692
+ const initialization = inspectRspInitialization();
4589
3693
  if (initialization)
4590
3694
  return { ok: false, kind: "error", message: initialization };
4591
3695
  try {
@@ -4597,7 +3701,7 @@ async function archiveChange(name) {
4597
3701
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4598
3702
  const base = basename2(workRef.name);
4599
3703
  const archiveName = resolveArchiveName(archiveSubdir, date, base);
4600
- const content = await readFile9(srcPath, "utf-8");
3704
+ const content = await readFile12(srcPath, "utf-8");
4601
3705
  const configInspection = await inspectRspConfig();
4602
3706
  if (configInspection.issues.length > 0) {
4603
3707
  return {
@@ -4607,9 +3711,10 @@ async function archiveChange(name) {
4607
3711
  };
4608
3712
  }
4609
3713
  const dependencyInspection = await inspectChangeDependencies();
4610
- const readiness = collectArchiveReadiness(content, {
3714
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
3715
+ const readiness = collectArchiveReadiness(document, {
4611
3716
  activeBlockers: dependencyInspection.activeBlockers.get(workRef.name),
4612
- documentDiagnostics: inspectChangeDocument(content, {
3717
+ documentDiagnostics: inspectChangeDocument(document, {
4613
3718
  name: workRef.name,
4614
3719
  validKinds: resolveKinds(configInspection.config),
4615
3720
  requiredSections: resolveRequiredSections(configInspection.config)
@@ -4624,13 +3729,13 @@ async function archiveChange(name) {
4624
3729
  warnings: checklist
4625
3730
  };
4626
3731
  }
4627
- await mkdir6(archiveSubdir, { recursive: true });
4628
- await rename3(srcPath, join7(archiveSubdir, archiveName));
3732
+ await mkdir5(archiveSubdir, { recursive: true });
3733
+ await rename3(srcPath, join8(archiveSubdir, archiveName));
4629
3734
  const postArchiveWarnings = [];
4630
3735
  let focusCleared = false;
4631
- if (existsSync6(focusEntry)) {
3736
+ if (existsSync4(focusEntry)) {
4632
3737
  try {
4633
- await unlink5(focusEntry);
3738
+ await unlink4(focusEntry);
4634
3739
  await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
4635
3740
  focusCleared = true;
4636
3741
  } catch (error) {
@@ -4642,7 +3747,7 @@ async function archiveChange(name) {
4642
3747
  } catch (error) {
4643
3748
  postArchiveWarnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
4644
3749
  }
4645
- return { ok: true, archiveName, focusCleared, readinessWarnings: checklist, cleanupWarnings: postArchiveWarnings, git: existsSync6(".git") };
3750
+ return { ok: true, archiveName, focusCleared, readinessWarnings: checklist, cleanupWarnings: postArchiveWarnings, git: existsSync4(".git") };
4646
3751
  });
4647
3752
  } catch (error) {
4648
3753
  if (error instanceof WorkRefError) {
@@ -4651,15 +3756,6 @@ async function archiveChange(name) {
4651
3756
  throw error;
4652
3757
  }
4653
3758
  }
4654
- function inspectInitialization() {
4655
- const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
4656
- const design = inspectManagedFile(join7(RSP_DIR, "specs", "design.md"), "design Spec", { allowMissing: true });
4657
- if (!rules.issue && !design.issue && rules.exists && design.exists)
4658
- return void 0;
4659
- const initialized = existsSync6(RSP_DIR);
4660
- return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
4661
- ${initialized ? "Run: rsp update" : "Run: rsp init"}`;
4662
- }
4663
3759
  function resolveArchiveName(archiveSubdir, date, base) {
4664
3760
  const initialName = `${date}_${base}.md`;
4665
3761
  if (!archiveNameExists(archiveSubdir, initialName))
@@ -4677,7 +3773,7 @@ function resolveArchiveName(archiveSubdir, date, base) {
4677
3773
  );
4678
3774
  }
4679
3775
  function archiveNameExists(archiveSubdir, name) {
4680
- if (!existsSync6(archiveSubdir))
3776
+ if (!existsSync4(archiveSubdir))
4681
3777
  return false;
4682
3778
  return readdirSync(archiveSubdir).some((entry) => {
4683
3779
  if (entry.normalize("NFC") !== name)
@@ -4693,252 +3789,10 @@ function archiveNameExists(archiveSubdir, name) {
4693
3789
  });
4694
3790
  }
4695
3791
 
4696
- // src/commands/commit.ts
4697
- import { execFile, spawn } from "child_process";
4698
- import { lstat as lstat4, readFile as readFile10 } from "fs/promises";
4699
- import { promisify } from "util";
4700
- var execFileAsync = promisify(execFile);
4701
- function messagesMatch(prepared, observed) {
4702
- if (prepared === observed)
4703
- return true;
4704
- if (prepared.endsWith("\n") && prepared.slice(0, -1) === observed)
4705
- return true;
4706
- return observed.endsWith("\n") && observed.slice(0, -1) === prepared;
4707
- }
4708
- function extractCommittedMessage(output) {
4709
- const separator = output.indexOf("\0");
4710
- return separator === -1 ? output : output.slice(0, separator);
4711
- }
4712
- async function readStagedPaths() {
4713
- const { stdout } = await execFileAsync("git", ["diff", "--cached", "--name-only", "--no-renames", "--diff-filter=ACDMRTUXB"], {
4714
- encoding: "utf8"
4715
- });
4716
- return stdout.split("\n").filter(Boolean);
4717
- }
4718
- async function readHead() {
4719
- try {
4720
- const { stdout } = await execFileAsync("git", ["rev-parse", "--verify", "HEAD"], { encoding: "utf8" });
4721
- return stdout.trim();
4722
- } catch {
4723
- return null;
4724
- }
4725
- }
4726
- async function readPathLines(args) {
4727
- const { stdout } = await execFileAsync("git", args, { encoding: "utf8" });
4728
- return stdout.split("\n").filter(Boolean);
4729
- }
4730
- async function readCommittedPaths(commit) {
4731
- return readPathLines(["diff-tree", "--root", "--no-commit-id", "--name-only", "--no-renames", "-r", commit]);
4732
- }
4733
- async function readRemainingWorktreePaths() {
4734
- const [unstaged, staged, untracked] = await Promise.all([
4735
- readPathLines(["diff", "--name-only", "--no-renames", "--diff-filter=ACDMRTUXB"]),
4736
- readPathLines(["diff", "--cached", "--name-only", "--no-renames", "--diff-filter=ACDMRTUXB"]),
4737
- readPathLines(["ls-files", "--others", "--exclude-standard"])
4738
- ]);
4739
- return [.../* @__PURE__ */ new Set([...unstaged, ...staged, ...untracked])].sort();
4740
- }
4741
- async function gitPathExists(path) {
4742
- const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", path], { encoding: "utf8" });
4743
- try {
4744
- await lstat4(stdout.trim());
4745
- return true;
4746
- } catch (error) {
4747
- if (error.code === "ENOENT")
4748
- return false;
4749
- throw error;
4750
- }
4751
- }
4752
- async function detectGitOperation() {
4753
- const operations = [
4754
- ["merge", "MERGE_HEAD"],
4755
- ["cherry-pick", "CHERRY_PICK_HEAD"],
4756
- ["revert", "REVERT_HEAD"],
4757
- ["rebase", "rebase-merge"],
4758
- ["rebase-or-am", "rebase-apply"],
4759
- ["sequencer", "sequencer"]
4760
- ];
4761
- for (const [operation, path] of operations) {
4762
- if (await gitPathExists(path))
4763
- return operation;
4764
- }
4765
- return null;
4766
- }
4767
- function samePaths(expected, observed) {
4768
- const left = [...expected].sort();
4769
- const right = [...observed].sort();
4770
- return left.length === right.length && left.every((path, index) => path === right[index]);
4771
- }
4772
- async function runGitCommit(message) {
4773
- return await new Promise((resolve5) => {
4774
- const child = spawn("git", ["commit", "--cleanup=verbatim", "-F", "-"], {
4775
- stdio: ["pipe", "pipe", "pipe"]
4776
- });
4777
- let stdout = "";
4778
- let stderr = "";
4779
- let settled = false;
4780
- child.stdout.setEncoding("utf8");
4781
- child.stderr.setEncoding("utf8");
4782
- child.stdout.on("data", (chunk) => stdout += chunk);
4783
- child.stderr.on("data", (chunk) => stderr += chunk);
4784
- child.on("error", (error) => {
4785
- if (!settled) {
4786
- settled = true;
4787
- resolve5({ ok: false, message: toErrorMessage(error) });
4788
- }
4789
- });
4790
- child.on("close", (code) => {
4791
- if (settled)
4792
- return;
4793
- settled = true;
4794
- if (code === 0)
4795
- resolve5({ ok: true, stdout });
4796
- else
4797
- resolve5({ ok: false, message: stderr.trim() || stdout.trim() || `git commit exited with code ${code ?? "unknown"}` });
4798
- });
4799
- child.stdin.end(message);
4800
- });
4801
- }
4802
- async function commitFromMessageFile(messageFile) {
4803
- let preparedMessage;
4804
- try {
4805
- preparedMessage = await readFile10(messageFile, "utf8");
4806
- } catch (error) {
4807
- return {
4808
- ok: false,
4809
- command: "commit",
4810
- code: "message_file_read_failed",
4811
- message: `unable to read message file: ${toErrorMessage(error)}`
4812
- };
4813
- }
4814
- if (preparedMessage.includes("\\n")) {
4815
- return {
4816
- ok: false,
4817
- command: "commit",
4818
- code: "literal_newline_escape",
4819
- message: "message file contains literal \\\\n; use actual line breaks"
4820
- };
4821
- }
4822
- let operation;
4823
- try {
4824
- operation = await detectGitOperation();
4825
- } catch (error) {
4826
- return {
4827
- ok: false,
4828
- command: "commit",
4829
- code: "git_operation_in_progress",
4830
- message: `unable to verify current Git operation state: ${toErrorMessage(error)}`
4831
- };
4832
- }
4833
- if (operation) {
4834
- return {
4835
- ok: false,
4836
- command: "commit",
4837
- code: "git_operation_in_progress",
4838
- operation,
4839
- message: `refusing to commit while Git operation is in progress: ${operation}`
4840
- };
4841
- }
4842
- let stagedPaths;
4843
- try {
4844
- stagedPaths = await readStagedPaths();
4845
- } catch (error) {
4846
- return {
4847
- ok: false,
4848
- command: "commit",
4849
- code: "no_staged_boundary",
4850
- message: `unable to inspect staged boundary: ${toErrorMessage(error)}`
4851
- };
4852
- }
4853
- if (stagedPaths.length === 0) {
4854
- return {
4855
- ok: false,
4856
- command: "commit",
4857
- code: "no_staged_boundary",
4858
- message: "no staged boundary exists"
4859
- };
4860
- }
4861
- const headBefore = await readHead();
4862
- const commitResult = await runGitCommit(preparedMessage);
4863
- if (!commitResult.ok) {
4864
- return {
4865
- ok: false,
4866
- command: "commit",
4867
- code: "git_commit_failed",
4868
- message: commitResult.message
4869
- };
4870
- }
4871
- let headAfter;
4872
- let observedMessage;
4873
- let committedPaths;
4874
- let remainingWorktreePaths;
4875
- try {
4876
- const [head, observed] = await Promise.all([
4877
- execFileAsync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }),
4878
- execFileAsync("git", ["show", "-s", "--format=%B%x00", "HEAD"], { encoding: "utf8" })
4879
- ]);
4880
- headAfter = head.stdout.trim();
4881
- observedMessage = extractCommittedMessage(observed.stdout);
4882
- [committedPaths, remainingWorktreePaths] = await Promise.all([
4883
- readCommittedPaths(headAfter),
4884
- readRemainingWorktreePaths()
4885
- ]);
4886
- } catch (error) {
4887
- return {
4888
- ok: false,
4889
- command: "commit",
4890
- code: "receipt_observation_failed",
4891
- message: `commit created but its complete receipt could not be observed: ${toErrorMessage(error)}`,
4892
- headBefore,
4893
- preparedLength: preparedMessage.length
4894
- };
4895
- }
4896
- if (!messagesMatch(preparedMessage, observedMessage)) {
4897
- return {
4898
- ok: false,
4899
- command: "commit",
4900
- code: "message_mismatch",
4901
- message: "committed message differs from the prepared message; stopped without amend or a second commit",
4902
- commit: headAfter,
4903
- headBefore,
4904
- headAfter,
4905
- storedMessage: observedMessage,
4906
- committedPaths,
4907
- remainingWorktreePaths,
4908
- preparedLength: preparedMessage.length,
4909
- observedLength: observedMessage.length
4910
- };
4911
- }
4912
- if (!samePaths(stagedPaths, committedPaths)) {
4913
- return {
4914
- ok: false,
4915
- command: "commit",
4916
- code: "commit_boundary_mismatch",
4917
- message: "committed paths differ from the reviewed staged boundary; stopped without amend or a second commit",
4918
- commit: headAfter,
4919
- headBefore,
4920
- headAfter,
4921
- storedMessage: observedMessage,
4922
- committedPaths,
4923
- remainingWorktreePaths
4924
- };
4925
- }
4926
- return {
4927
- ok: true,
4928
- command: "commit",
4929
- commit: headAfter,
4930
- headBefore,
4931
- headAfter,
4932
- storedMessage: observedMessage,
4933
- committedPaths,
4934
- remainingWorktreePaths
4935
- };
4936
- }
4937
-
4938
3792
  // src/commands/create.ts
4939
- import { existsSync as existsSync7 } from "fs";
4940
- import { mkdir as mkdir7, unlink as unlink6, writeFile as writeFile3 } from "fs/promises";
4941
- import { dirname as dirname3 } from "path";
3793
+ import { existsSync as existsSync5 } from "fs";
3794
+ import { mkdir as mkdir6, unlink as unlink5, writeFile as writeFile4 } from "fs/promises";
3795
+ import { dirname as dirname4 } from "path";
4942
3796
  async function createChange(name, summary = "", kind, options = {}) {
4943
3797
  if (!name) {
4944
3798
  return { ok: false, kind: "usage", message: "rsp create <name> [summary]" };
@@ -4948,31 +3802,31 @@ async function createChange(name, summary = "", kind, options = {}) {
4948
3802
  if (options.issueRelation && !hasIssueOption)
4949
3803
  throw new Error("--issue-relation requires --issue");
4950
3804
  const issues = options.issue !== void 0 ? [createIssueRelationship(options.issue, options.issueRelation)] : [];
4951
- const initialization = inspectInitialization2();
3805
+ const initialization = inspectRspInitialization();
4952
3806
  if (initialization)
4953
3807
  return { ok: false, kind: "error", message: initialization, warning };
4954
3808
  try {
4955
3809
  return await withRspLock("create-change", async () => {
4956
3810
  const workRef = await resolveExecutableChange(name);
4957
3811
  const changePath = workRef.path;
4958
- const existed = existsSync7(changePath);
3812
+ const existed = existsSync5(changePath);
4959
3813
  if (existed && hasIssueOption)
4960
3814
  throw new Error(`Change already exists; --issue cannot update ${workRef.name}`);
4961
3815
  if (!existed) {
4962
3816
  const focusEntry = resolveFocusMarkerPath(workRef);
4963
3817
  const content = generateChangeContent(workRef.name, summary, kind, { issues });
4964
- await mkdir7(dirname3(changePath), { recursive: true });
3818
+ await mkdir6(dirname4(changePath), { recursive: true });
4965
3819
  let changeCreated = false;
4966
3820
  try {
4967
- await writeFile3(changePath, content, { flag: "wx" });
3821
+ await writeFile4(changePath, content, { flag: "wx" });
4968
3822
  changeCreated = true;
4969
- await mkdir7(FOCUS_DIR, { recursive: true });
4970
- await mkdir7(dirname3(focusEntry), { recursive: true });
3823
+ await mkdir6(FOCUS_DIR, { recursive: true });
3824
+ await mkdir6(dirname4(focusEntry), { recursive: true });
4971
3825
  await writeManagedFile(focusEntry, "", "focus marker");
4972
3826
  } catch (error) {
4973
3827
  if (changeCreated) {
4974
3828
  try {
4975
- await unlink6(changePath);
3829
+ await unlink5(changePath);
4976
3830
  await cleanupEmptyParentDirs(changePath, CHANGES_DIR);
4977
3831
  } catch {
4978
3832
  }
@@ -4989,35 +3843,26 @@ async function createChange(name, summary = "", kind, options = {}) {
4989
3843
  throw error;
4990
3844
  }
4991
3845
  }
4992
- function inspectInitialization2() {
4993
- const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
4994
- const design = inspectManagedFile(`${RSP_DIR}/specs/design.md`, "design Spec", { allowMissing: true });
4995
- if (!rules.issue && !design.issue && rules.exists && design.exists)
4996
- return void 0;
4997
- const initialized = existsSync7(RSP_DIR);
4998
- return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
4999
- ${initialized ? "Run: rsp update" : "Run: rsp init"}`;
5000
- }
5001
3846
 
5002
3847
  // src/commands/focus.ts
5003
- import { Buffer as Buffer3 } from "buffer";
5004
- import { randomUUID as randomUUID3 } from "crypto";
5005
- import { existsSync as existsSync8 } from "fs";
5006
- import { mkdir as mkdir8, open as open3, rename as rename4, unlink as unlink7 } from "fs/promises";
5007
- import { basename as basename3, dirname as dirname4, join as join8 } from "path";
3848
+ import { Buffer } from "buffer";
3849
+ import { randomUUID } from "crypto";
3850
+ import { existsSync as existsSync6 } from "fs";
3851
+ import { mkdir as mkdir7, open, rename as rename4, unlink as unlink6 } from "fs/promises";
3852
+ import { basename as basename3, dirname as dirname5, join as join9 } from "path";
5008
3853
  async function focusChange(name, options = {}) {
5009
3854
  if (!name) {
5010
3855
  return { ok: false, kind: "usage", message: "rsp focus <name>" };
5011
3856
  }
5012
- const initialization = inspectInitialization3();
3857
+ const initialization = inspectRspInitialization();
5013
3858
  if (initialization)
5014
3859
  return { ok: false, kind: "error", message: initialization };
5015
3860
  try {
5016
3861
  return await withRspLock("focus-change", async () => {
5017
3862
  const workRef = await resolveExecutableChange(name, { mustExist: true });
5018
3863
  const focusEntry = resolveFocusMarkerPath(workRef);
5019
- await mkdir8(FOCUS_DIR, { recursive: true });
5020
- await mkdir8(dirname4(focusEntry), { recursive: true });
3864
+ await mkdir7(FOCUS_DIR, { recursive: true });
3865
+ await mkdir7(dirname5(focusEntry), { recursive: true });
5021
3866
  if (options.capsuleFile === void 0)
5022
3867
  await ensureManagedFile(focusEntry, "", "focus marker");
5023
3868
  else
@@ -5039,10 +3884,10 @@ async function readCapsule(source) {
5039
3884
  }
5040
3885
  async function replaceFocusCapsule(path, content) {
5041
3886
  requireManagedFile(path, "focus marker", { allowMissing: true });
5042
- const temporaryPath = join8(dirname4(path), `.${basename3(path)}.${randomUUID3()}.tmp`);
3887
+ const temporaryPath = join9(dirname5(path), `.${basename3(path)}.${randomUUID()}.tmp`);
5043
3888
  let handle;
5044
3889
  try {
5045
- handle = await open3(temporaryPath, "wx");
3890
+ handle = await open(temporaryPath, "wx");
5046
3891
  await handle.writeFile(content, "utf-8");
5047
3892
  await handle.close();
5048
3893
  handle = void 0;
@@ -5051,7 +3896,7 @@ async function replaceFocusCapsule(path, content) {
5051
3896
  } finally {
5052
3897
  await handle?.close().catch(() => {
5053
3898
  });
5054
- await unlink7(temporaryPath).catch((error) => {
3899
+ await unlink6(temporaryPath).catch((error) => {
5055
3900
  if (error.code !== "ENOENT")
5056
3901
  throw error;
5057
3902
  });
@@ -5059,7 +3904,7 @@ async function replaceFocusCapsule(path, content) {
5059
3904
  }
5060
3905
  async function readRegularCapsuleFile(path) {
5061
3906
  requireManagedFile(path, "focus capsule input");
5062
- const handle = await open3(path, "r");
3907
+ const handle = await open(path, "r");
5063
3908
  try {
5064
3909
  if (!(await handle.stat()).isFile())
5065
3910
  throw new Error(`focus capsule input must be a regular file: ${path}`);
@@ -5072,28 +3917,28 @@ async function readBoundedStdin() {
5072
3917
  const chunks = [];
5073
3918
  let size = 0;
5074
3919
  for await (const chunk of process.stdin) {
5075
- const buffer = Buffer3.isBuffer(chunk) ? chunk : Buffer3.from(chunk);
3920
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5076
3921
  size += buffer.byteLength;
5077
3922
  if (size > MAX_FOCUS_CAPSULE_BYTES)
5078
3923
  throw new Error(`focus capsule exceeds ${MAX_FOCUS_CAPSULE_BYTES} UTF-8 bytes`);
5079
3924
  chunks.push(buffer);
5080
3925
  }
5081
- return Buffer3.concat(chunks, size);
3926
+ return Buffer.concat(chunks, size);
5082
3927
  }
5083
3928
  async function unfocusChange(name) {
5084
3929
  if (!name) {
5085
3930
  return { ok: false, kind: "usage", message: "rsp unfocus <name>" };
5086
3931
  }
5087
- const initialization = inspectInitialization3();
3932
+ const initialization = inspectRspInitialization();
5088
3933
  if (initialization)
5089
3934
  return { ok: false, kind: "error", message: initialization };
5090
3935
  try {
5091
3936
  return await withRspLock("unfocus-change", async () => {
5092
3937
  const workRef = resolveWorkRef(name, { executable: false, mustExist: false });
5093
3938
  const focusEntry = resolveFocusMarkerPath(workRef);
5094
- if (!existsSync8(focusEntry))
3939
+ if (!existsSync6(focusEntry))
5095
3940
  throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
5096
- await unlink7(focusEntry);
3941
+ await unlink6(focusEntry);
5097
3942
  await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
5098
3943
  return { ok: true, action: "unfocus", workRef: workRef.name };
5099
3944
  });
@@ -5109,21 +3954,12 @@ function workRefFailure(error) {
5109
3954
  }
5110
3955
  throw error;
5111
3956
  }
5112
- function inspectInitialization3() {
5113
- const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
5114
- const design = inspectManagedFile(`${RSP_DIR}/specs/design.md`, "design Spec", { allowMissing: true });
5115
- if (!rules.issue && !design.issue && rules.exists && design.exists)
5116
- return void 0;
5117
- const initialized = existsSync8(RSP_DIR);
5118
- return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
5119
- ${initialized ? "Run: rsp update" : "Run: rsp init"}`;
5120
- }
5121
3957
 
5122
3958
  // src/commands/group.ts
5123
- import { randomUUID as randomUUID4 } from "crypto";
5124
- import { existsSync as existsSync9 } from "fs";
5125
- import { link as link3, mkdir as mkdir9, readdir as readdir3, readFile as readFile11, rename as rename5, unlink as unlink8, writeFile as writeFile4 } from "fs/promises";
5126
- import { join as join9 } from "path";
3959
+ import { randomUUID as randomUUID2 } from "crypto";
3960
+ import { existsSync as existsSync7 } from "fs";
3961
+ import { link as link2, mkdir as mkdir8, readdir as readdir4, readFile as readFile13, rename as rename5, unlink as unlink7, writeFile as writeFile5 } from "fs/promises";
3962
+ import { join as join10 } from "path";
5127
3963
  var ChangeGroupError = class extends Error {
5128
3964
  name = "ChangeGroupError";
5129
3965
  };
@@ -5131,7 +3967,7 @@ async function createChangeGroup(name, goal = "") {
5131
3967
  if (!name) {
5132
3968
  return failure("usage", "rsp group create <name> [goal]");
5133
3969
  }
5134
- const initialization = inspectInitialization4();
3970
+ const initialization = inspectRspInitialization();
5135
3971
  if (initialization)
5136
3972
  return failure("error", initialization);
5137
3973
  try {
@@ -5143,9 +3979,9 @@ async function createChangeGroup(name, goal = "") {
5143
3979
  if (await hasArchivedGroupBrief(groupName))
5144
3980
  throw new ChangeGroupError(`archived Change Group cannot be reopened: ${groupName}`);
5145
3981
  const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [groupName], "Change Group path");
5146
- await mkdir9(groupDirectory, { recursive: true });
3982
+ await mkdir8(groupDirectory, { recursive: true });
5147
3983
  try {
5148
- await writeFile4(ref.path, generateGroupBriefContent(groupName, goal), { flag: "wx" });
3984
+ await writeFile5(ref.path, generateGroupBriefContent(groupName, goal), { flag: "wx" });
5149
3985
  } catch (error) {
5150
3986
  if (error.code === "EEXIST")
5151
3987
  throw new ChangeGroupError(`Change Group already exists: ${groupName}`);
@@ -5164,7 +4000,7 @@ async function closeChangeGroup(name) {
5164
4000
  if (!name) {
5165
4001
  return failure("usage", "rsp group close <name>");
5166
4002
  }
5167
- const initialization = inspectInitialization4();
4003
+ const initialization = inspectRspInitialization();
5168
4004
  if (initialization)
5169
4005
  return failure("error", initialization);
5170
4006
  try {
@@ -5183,15 +4019,15 @@ async function closeChangeGroup(name) {
5183
4019
  const archiveDirectory = resolveArchiveDirectory(ref);
5184
4020
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5185
4021
  const archiveName = resolveArchiveName(archiveDirectory, date, "brief");
5186
- await mkdir9(archiveDirectory, { recursive: true });
5187
- await rename5(ref.path, join9(archiveDirectory, archiveName));
4022
+ await mkdir8(archiveDirectory, { recursive: true });
4023
+ await rename5(ref.path, join10(archiveDirectory, archiveName));
5188
4024
  const warnings = [];
5189
4025
  try {
5190
4026
  await cleanupEmptyParentDirs(ref.path, CHANGES_DIR);
5191
4027
  } catch (error) {
5192
4028
  warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
5193
4029
  }
5194
- return { ok: true, action: "close", groupName, archivePath: join9(archiveDirectory, archiveName), warnings };
4030
+ return { ok: true, action: "close", groupName, archivePath: join10(archiveDirectory, archiveName), warnings };
5195
4031
  });
5196
4032
  } catch (error) {
5197
4033
  if (error instanceof WorkRefError || error instanceof ChangeGroupError) {
@@ -5205,18 +4041,18 @@ async function reopenChangeGroup(name, options) {
5205
4041
  return failure("usage", "rsp group reopen <name> --reason <text> [--from <archive-path>]");
5206
4042
  }
5207
4043
  const reason = options.reason?.trim();
5208
- if (!reason || /[\r\n]/.test(reason)) {
4044
+ if (!reason || /[\r\n]/.test(reason) || !markdownText(parseMarkdown(reason).root).trim()) {
5209
4045
  return failure("error", "--reason must be one non-empty line");
5210
4046
  }
5211
- const initialization = inspectInitialization4();
4047
+ const initialization = inspectRspInitialization();
5212
4048
  if (initialization)
5213
4049
  return failure("error", initialization);
5214
4050
  try {
5215
4051
  return await withRspLock("reopen-change-group", async () => {
5216
4052
  const warnings = [];
5217
4053
  const groupName = normalizeWorkRefSegment(name);
5218
- await requireEmptyGroupRecoverySubtree(join9(CHANGES_DIR, groupName), `Change Group work subtree for ${groupName}`);
5219
- await requireEmptyGroupRecoverySubtree(join9(FOCUS_DIR, groupName), `Change Group focus subtree for ${groupName}`);
4054
+ await requireEmptyGroupRecoverySubtree(join10(CHANGES_DIR, groupName), `Change Group work subtree for ${groupName}`);
4055
+ await requireEmptyGroupRecoverySubtree(join10(FOCUS_DIR, groupName), `Change Group focus subtree for ${groupName}`);
5220
4056
  const ref = resolveWorkRef(`${groupName}/brief`);
5221
4057
  if (ref.kind !== "group-brief")
5222
4058
  throw new ChangeGroupError(`invalid Change Group name: ${name}`);
@@ -5237,16 +4073,16 @@ async function reopenChangeGroup(name, options) {
5237
4073
  `selected archived Change Group belongs to ${record.group}, not ${groupName}`
5238
4074
  );
5239
4075
  }
5240
- if (existsSync9(ref.path))
4076
+ if (existsSync7(ref.path))
5241
4077
  throw new ArchiveHistoryError("open_group_exists", `open Change Group already exists: ${groupName}`);
5242
- const archivedContent = await readFile11(record.sourcePath, "utf-8");
4078
+ const archivedContent = await readFile13(record.sourcePath, "utf-8");
5243
4079
  const document = parseRspDocument(archivedContent, GROUP_BRIEF_DOCUMENT_SCHEMA);
5244
4080
  validateArchivedGroupBrief(document, groupName);
5245
4081
  const evidenceKey = `${GROUP_REOPEN_COMPLETION_PREFIX} \`${record.path}\`: ${reason}`;
5246
4082
  const retainedEvidence = /* @__PURE__ */ new Set();
5247
4083
  for (const retained of inspection.groupBriefs.filter((candidate) => candidate.group === groupName)) {
5248
- const content = retained.sourcePath === record.sourcePath ? archivedContent : await readFile11(retained.sourcePath, "utf-8");
5249
- for (const item of getGroupReopenEvidence(content))
4084
+ const retainedDocument = retained.sourcePath === record.sourcePath ? document : parseRspDocument(await readFile13(retained.sourcePath, "utf-8"), GROUP_BRIEF_DOCUMENT_SCHEMA);
4085
+ for (const item of getGroupReopenEvidence(retainedDocument))
5250
4086
  retainedEvidence.add(item.key);
5251
4087
  }
5252
4088
  if (retainedEvidence.has(evidenceKey)) {
@@ -5261,17 +4097,17 @@ async function reopenChangeGroup(name, options) {
5261
4097
  `- [ ] ${evidenceKey}`
5262
4098
  );
5263
4099
  const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [groupName], "Change Group path");
5264
- await mkdir9(groupDirectory, { recursive: true });
5265
- const temporaryPath = join9(RSP_DIR, `.group-reopen-${process.pid}-${randomUUID4()}.tmp`);
4100
+ await mkdir8(groupDirectory, { recursive: true });
4101
+ const temporaryPath = join10(RSP_DIR, `.group-reopen-${process.pid}-${randomUUID2()}.tmp`);
5266
4102
  let mutationError;
5267
4103
  try {
5268
- await writeFile4(temporaryPath, reopenedContent, { flag: "wx" });
5269
- await link3(temporaryPath, ref.path);
4104
+ await writeFile5(temporaryPath, reopenedContent, { flag: "wx" });
4105
+ await link2(temporaryPath, ref.path);
5270
4106
  } catch (error) {
5271
4107
  mutationError = error;
5272
4108
  }
5273
4109
  try {
5274
- await unlink8(temporaryPath);
4110
+ await unlink7(temporaryPath);
5275
4111
  } catch (error) {
5276
4112
  if (!mutationError)
5277
4113
  warnings.push(`temporary reopen file cleanup failed: ${toErrorMessage(error)}`);
@@ -5302,21 +4138,12 @@ async function reopenChangeGroup(name, options) {
5302
4138
  function failure(kind, message, candidates = [], remainingCandidates = 0) {
5303
4139
  return { ok: false, kind, message, candidates, remainingCandidates };
5304
4140
  }
5305
- function inspectInitialization4() {
5306
- const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
5307
- const design = inspectManagedFile(join9(RSP_DIR, "specs", "design.md"), "design Spec", { allowMissing: true });
5308
- if (!rules.issue && !design.issue && rules.exists && design.exists)
5309
- return void 0;
5310
- const initialized = existsSync9(RSP_DIR);
5311
- return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
5312
- ${initialized ? "Run: rsp update" : "Run: rsp init"}`;
5313
- }
5314
4141
  async function requireEmptyGroupRecoverySubtree(path, label) {
5315
4142
  if (!requireManagedDirectory(path, label, { allowMissing: true }))
5316
4143
  return;
5317
4144
  let entries;
5318
4145
  try {
5319
- entries = await readdir3(path);
4146
+ entries = await readdir4(path);
5320
4147
  } catch (error) {
5321
4148
  throw new ChangeGroupError(`unable to inspect ${label}: ${toErrorMessage(error)}`);
5322
4149
  }
@@ -5329,7 +4156,7 @@ function validateArchivedGroupBrief(document, group) {
5329
4156
  throw new ArchiveHistoryError("archive_heading_invalid", `archived Change Group must contain exactly one canonical identity heading for ${group}`);
5330
4157
  for (const section of GROUP_BRIEF_DOCUMENT_SCHEMA.sections) {
5331
4158
  const matches = getDocumentSections(document, section.id);
5332
- if (matches.length !== 1 || !matches[0].canonical || !getDocumentSectionBody(document, section.id)) {
4159
+ if (matches.length !== 1 || !matches[0].canonical || getVisibleBodyLines(getDocumentSectionMarkdown(document, section.id)).length === 0) {
5333
4160
  throw new ArchiveHistoryError(
5334
4161
  "archive_section_invalid",
5335
4162
  `archived Change Group must contain exactly one canonical required section: ${section.heading}`
@@ -5339,10 +4166,10 @@ function validateArchivedGroupBrief(document, group) {
5339
4166
  }
5340
4167
  function collectCloseReasons(group, errors) {
5341
4168
  const reasons = [...errors, ...group.warnings];
5342
- const open4 = group.slices.filter((slice) => slice.state === "open").map((slice) => slice.name);
4169
+ const open2 = group.slices.filter((slice) => slice.state === "open").map((slice) => slice.name);
5343
4170
  const missing = group.slices.filter((slice) => slice.state === "missing").map((slice) => slice.name);
5344
- if (open4.length > 0)
5345
- reasons.push(`open slices: ${open4.join(", ")}`);
4171
+ if (open2.length > 0)
4172
+ reasons.push(`open slices: ${open2.join(", ")}`);
5346
4173
  if (missing.length > 0)
5347
4174
  reasons.push(`missing slices: ${missing.join(", ")}`);
5348
4175
  if (group.completion.total === 0)
@@ -5355,18 +4182,18 @@ function collectCloseReasons(group, errors) {
5355
4182
  }
5356
4183
 
5357
4184
  // src/commands/reopen.ts
5358
- import { existsSync as existsSync10 } from "fs";
5359
- import { mkdir as mkdir10, readFile as readFile12, unlink as unlink9, writeFile as writeFile5 } from "fs/promises";
5360
- import { dirname as dirname5 } from "path";
4185
+ import { existsSync as existsSync8 } from "fs";
4186
+ import { mkdir as mkdir9, readFile as readFile14, unlink as unlink8, writeFile as writeFile6 } from "fs/promises";
4187
+ import { dirname as dirname6 } from "path";
5361
4188
  async function reopenChange(name, options) {
5362
4189
  if (!name) {
5363
4190
  return { ok: false, kind: "usage", message: "rsp reopen <name> --reason <text> [--from <archive-path>]", candidates: [], remainingCandidates: 0 };
5364
4191
  }
5365
4192
  const reason = options.reason?.trim();
5366
- if (!reason || /[\r\n]/.test(reason)) {
4193
+ if (!reason || /[\r\n]/.test(reason) || !markdownText(parseMarkdown(reason).root).trim()) {
5367
4194
  return { ok: false, kind: "error", message: "--reason must be one non-empty line", candidates: [], remainingCandidates: 0 };
5368
4195
  }
5369
- const initialization = inspectInitialization5();
4196
+ const initialization = inspectRspInitialization();
5370
4197
  if (initialization)
5371
4198
  return { ok: false, kind: "error", message: initialization, candidates: [], remainingCandidates: 0 };
5372
4199
  try {
@@ -5401,36 +4228,36 @@ async function reopenChange(name, options) {
5401
4228
  }
5402
4229
  throw error;
5403
4230
  }
5404
- if (existsSync10(workRef.path))
4231
+ if (existsSync8(workRef.path))
5405
4232
  throw new ArchiveHistoryError("open_change_exists", `open Change already exists: ${name}`);
5406
4233
  const focusEntry = resolveFocusMarkerPath(workRef);
5407
- if (existsSync10(focusEntry))
4234
+ if (existsSync8(focusEntry))
5408
4235
  throw new ArchiveHistoryError("focus_marker_exists", `focus marker already exists for non-open Change: ${name}; run rsp doctor`);
5409
- const archivedContent = await readFile12(record.sourcePath, "utf-8");
4236
+ const archivedContent = await readFile14(record.sourcePath, "utf-8");
5410
4237
  const reopenedContent = appendChecklistItem(
5411
4238
  appendChecklistItem(archivedContent, "tasks", `- [ ] Resolve reopened concern: ${reason}`),
5412
4239
  "verify",
5413
4240
  `- [ ] Verify reopened concern: ${reason}`
5414
4241
  );
5415
- await mkdir10(dirname5(workRef.path), { recursive: true });
4242
+ await mkdir9(dirname6(workRef.path), { recursive: true });
5416
4243
  let changeCreated = false;
5417
4244
  try {
5418
- await writeFile5(workRef.path, reopenedContent, { flag: "wx" });
4245
+ await writeFile6(workRef.path, reopenedContent, { flag: "wx" });
5419
4246
  changeCreated = true;
5420
- await mkdir10(FOCUS_DIR, { recursive: true });
5421
- await mkdir10(dirname5(focusEntry), { recursive: true });
4247
+ await mkdir9(FOCUS_DIR, { recursive: true });
4248
+ await mkdir9(dirname6(focusEntry), { recursive: true });
5422
4249
  await writeManagedFile(focusEntry, "", "focus marker");
5423
4250
  } catch (error) {
5424
4251
  if (changeCreated) {
5425
4252
  try {
5426
- await unlink9(workRef.path);
4253
+ await unlink8(workRef.path);
5427
4254
  await cleanupEmptyParentDirs(workRef.path, CHANGES_DIR);
5428
4255
  } catch {
5429
4256
  }
5430
4257
  }
5431
4258
  throw error;
5432
4259
  }
5433
- return { ok: true, workRef: workRef.name, archivePath: record.path, git: existsSync10(".git") };
4260
+ return { ok: true, workRef: workRef.name, archivePath: record.path, git: existsSync8(".git") };
5434
4261
  });
5435
4262
  } catch (error) {
5436
4263
  if (error instanceof ArchiveHistoryError || error instanceof WorkRefError) {
@@ -5445,15 +4272,6 @@ async function reopenChange(name, options) {
5445
4272
  throw error;
5446
4273
  }
5447
4274
  }
5448
- function inspectInitialization5() {
5449
- const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
5450
- const design = inspectManagedFile(`${RSP_DIR}/specs/design.md`, "design Spec", { allowMissing: true });
5451
- if (!rules.issue && !design.issue && rules.exists && design.exists)
5452
- return void 0;
5453
- const initialized = existsSync10(RSP_DIR);
5454
- return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
5455
- ${initialized ? "Run: rsp update" : "Run: rsp init"}`;
5456
- }
5457
4275
  function appendChecklistItem(content, sectionId, item) {
5458
4276
  try {
5459
4277
  return appendDocumentSectionItem(parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), sectionId, item);
@@ -5468,25 +4286,6 @@ function appendChecklistItem(content, sectionId, item) {
5468
4286
  }
5469
4287
  }
5470
4288
 
5471
- // src/cli/presenters/commit.ts
5472
- function presentCommit(result, json) {
5473
- if (json) {
5474
- emitJson(result);
5475
- return;
5476
- }
5477
- if (!result.ok) {
5478
- console.error(` Commit stopped: ${result.message}`);
5479
- return;
5480
- }
5481
- console.log(` Committed: ${result.commit}`);
5482
- console.log(` HEAD: ${result.headBefore ?? "(unborn)"} -> ${result.headAfter}`);
5483
- console.log(` committed paths: ${result.committedPaths?.join(", ") || "none"}`);
5484
- console.log(` remaining worktree paths: ${result.remainingWorktreePaths?.join(", ") || "none"}`);
5485
- console.log(" stored message:");
5486
- for (const line of (result.storedMessage ?? "").split("\n"))
5487
- console.log(` ${line}`);
5488
- }
5489
-
5490
4289
  // src/cli/presenters/work.ts
5491
4290
  function presentFailure(result) {
5492
4291
  const label = result.kind === "usage" ? "Usage:" : "Error:";
@@ -5604,7 +4403,7 @@ function presentReopen(result) {
5604
4403
 
5605
4404
  // src/cli/commands/work.ts
5606
4405
  function createCreateCommand(deprecatedLite) {
5607
- return defineCommand4({
4406
+ return defineCommand5({
5608
4407
  meta: {
5609
4408
  name: "create",
5610
4409
  description: "Create .rsp/changes/<name>.md [summary]"
@@ -5629,7 +4428,7 @@ function createCreateCommand(deprecatedLite) {
5629
4428
  }
5630
4429
  },
5631
4430
  async run({ args }) {
5632
- const summary = Array.isArray(args._) && args._.length > 1 ? args._.slice(1).join(" ") : "";
4431
+ const summary = args._.slice(1).join(" ");
5633
4432
  await executeCliCommand({
5634
4433
  execute: () => createChange(args.name, summary, args.kind, {
5635
4434
  issue: args.issue,
@@ -5642,7 +4441,7 @@ function createCreateCommand(deprecatedLite) {
5642
4441
  }
5643
4442
  });
5644
4443
  }
5645
- var groupCreateCommand = defineCommand4({
4444
+ var groupCreateCommand = defineCommand5({
5646
4445
  meta: {
5647
4446
  name: "create",
5648
4447
  description: "Create an unfocused .rsp/changes/<group>/00-brief.md"
@@ -5655,18 +4454,18 @@ var groupCreateCommand = defineCommand4({
5655
4454
  }
5656
4455
  },
5657
4456
  async run({ args }) {
5658
- const goal = Array.isArray(args._) && args._.length > 1 ? args._.slice(1).join(" ") : "";
4457
+ const goal = args._.slice(1).join(" ");
5659
4458
  await executeCliCommand({ execute: () => createChangeGroup(args.name, goal), present: presentGroup, exitCode: (result) => result.ok ? void 0 : 1 }, args);
5660
4459
  }
5661
4460
  });
5662
- var groupCommand = defineCommand4({
4461
+ var groupCommand = defineCommand5({
5663
4462
  meta: {
5664
4463
  name: "group",
5665
4464
  description: "Create, close, and explicitly reopen shallow Change Groups"
5666
4465
  },
5667
4466
  subCommands: {
5668
4467
  create: groupCreateCommand,
5669
- close: defineCommand4({
4468
+ close: defineCommand5({
5670
4469
  meta: {
5671
4470
  name: "close",
5672
4471
  description: "Archive a completed Group Brief after all child Changes are archived"
@@ -5682,7 +4481,7 @@ var groupCommand = defineCommand4({
5682
4481
  await executeCliCommand({ execute: () => closeChangeGroup(args.name), present: presentGroup, exitCode: (result) => result.ok ? void 0 : 1 }, args);
5683
4482
  }
5684
4483
  }),
5685
- reopen: defineCommand4({
4484
+ reopen: defineCommand5({
5686
4485
  meta: {
5687
4486
  name: "reopen",
5688
4487
  description: "Restore one archived Group Brief while retaining history"
@@ -5709,7 +4508,7 @@ var groupCommand = defineCommand4({
5709
4508
  })
5710
4509
  }
5711
4510
  });
5712
- var archiveCommand = defineCommand4({
4511
+ var archiveCommand = defineCommand5({
5713
4512
  meta: {
5714
4513
  name: "archive",
5715
4514
  description: "Archive an open change into .rsp/archives/"
@@ -5733,7 +4532,7 @@ var archiveCommand = defineCommand4({
5733
4532
  await executeCliCommand({ execute: () => archiveChange(args.name), present: presentArchive, exitCode: (result) => result.ok ? void 0 : 1 }, args);
5734
4533
  }
5735
4534
  });
5736
- var reopenCommand = defineCommand4({
4535
+ var reopenCommand = defineCommand5({
5737
4536
  meta: {
5738
4537
  name: "reopen",
5739
4538
  description: "Restore one archived Change as focused open work while retaining history"
@@ -5758,7 +4557,7 @@ var reopenCommand = defineCommand4({
5758
4557
  await executeCliCommand({ execute: () => reopenChange(args.name, { from: args.from, reason: args.reason }), present: presentReopen, exitCode: (result) => result.ok ? void 0 : 1 }, args);
5759
4558
  }
5760
4559
  });
5761
- var focusCommand = defineCommand4({
4560
+ var focusCommand = defineCommand5({
5762
4561
  meta: {
5763
4562
  name: "focus",
5764
4563
  description: "Mark an existing change as currently focused in .rsp/focus.d/"
@@ -5778,7 +4577,7 @@ var focusCommand = defineCommand4({
5778
4577
  await executeCliCommand({ execute: () => focusChange(args.name, { capsuleFile: args["capsule-file"] }), present: presentFocus, exitCode: (result) => result.ok ? void 0 : 1 }, args);
5779
4578
  }
5780
4579
  });
5781
- var unfocusCommand = defineCommand4({
4580
+ var unfocusCommand = defineCommand5({
5782
4581
  meta: {
5783
4582
  name: "unfocus",
5784
4583
  description: "Remove a change from the current focus set in .rsp/focus.d/"
@@ -5794,30 +4593,8 @@ var unfocusCommand = defineCommand4({
5794
4593
  await executeCliCommand({ execute: () => unfocusChange(args.name), present: presentFocus, exitCode: (result) => result.ok ? void 0 : 1 }, args);
5795
4594
  }
5796
4595
  });
5797
- var commitCommand = defineCommand4({
5798
- meta: {
5799
- name: "commit",
5800
- description: "Create one local commit from a message file on the existing staged boundary"
5801
- },
5802
- args: {
5803
- "message-file": {
5804
- type: "string",
5805
- description: "Path to the prepared commit message file",
5806
- required: true
5807
- },
5808
- ...jsonArgs
5809
- },
5810
- async run({ args }) {
5811
- await executeCliCommand({
5812
- execute: () => commitFromMessageFile(args["message-file"]),
5813
- present: (result) => presentCommit(result, Boolean(args.json)),
5814
- exitCode: (result) => result.ok ? void 0 : 1
5815
- }, args);
5816
- }
5817
- });
5818
4596
  function createWorkCommands(deprecatedLite) {
5819
4597
  return {
5820
- commit: commitCommand,
5821
4598
  create: createCreateCommand(deprecatedLite),
5822
4599
  group: groupCommand,
5823
4600
  focus: focusCommand,
@@ -5830,17 +4607,19 @@ function createWorkCommands(deprecatedLite) {
5830
4607
  // src/cli/registry.ts
5831
4608
  function createRootCommand(options) {
5832
4609
  const workCommands = createWorkCommands(options.deprecatedLite);
5833
- return defineCommand5({
4610
+ return defineCommand6({
5834
4611
  meta: {
5835
4612
  name: "rsp",
5836
4613
  version: options.version,
5837
4614
  description: "RSP (Reliable Software Practice), a repository-native engineering workflow for humans and AI agents"
5838
4615
  },
5839
4616
  subCommands: {
4617
+ architecture: architectureCommand,
4618
+ view: viewCommand,
5840
4619
  init: setupCommands.init,
5841
4620
  add: setupCommands.add,
5842
4621
  specs: inspectionCommands.specs,
5843
- commit: workCommands.commit,
4622
+ config: inspectionCommands.config,
5844
4623
  create: workCommands.create,
5845
4624
  group: workCommands.group,
5846
4625
  focus: workCommands.focus,
@@ -5866,9 +4645,6 @@ function isInteractiveTerminal(environment) {
5866
4645
  function shouldAutoLaunchUi(args, environment) {
5867
4646
  return args.length === 0 && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
5868
4647
  }
5869
- function shouldLaunchSkillsUi(args, environment) {
5870
- return args.length === 1 && args[0] === "skills" && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
5871
- }
5872
4648
  function validateUiArgs(args) {
5873
4649
  let lang = "auto";
5874
4650
  for (let index = 0; index < args.length; index += 1) {
@@ -5908,6 +4684,7 @@ function normalizeLegacyCreateOptions(rawArgs) {
5908
4684
  };
5909
4685
  }
5910
4686
  async function runCli(rawArgs = process.argv.slice(2)) {
4687
+ await guardProjectVersion(rawArgs);
5911
4688
  const terminalEnvironment = {
5912
4689
  stdinTty: Boolean(process.stdin.isTTY),
5913
4690
  stdoutTty: Boolean(process.stdout.isTTY),
@@ -5915,19 +4692,6 @@ async function runCli(rawArgs = process.argv.slice(2)) {
5915
4692
  ci: process.env.CI
5916
4693
  };
5917
4694
  const explicitUi = rawArgs[0] === "ui";
5918
- if (shouldLaunchSkillsUi(rawArgs, terminalEnvironment)) {
5919
- const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
5920
- 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);
5921
- const { runSkillsTui } = await import("./entry-YAW3PYFA.mjs");
5922
- const selection = await runSkillsTui(locale);
5923
- if (selection.kind === "confirmed") {
5924
- const result = await installPackagedSkills({ names: selection.names, force: selection.force });
5925
- presentSkillInstallResult(result);
5926
- } else if (selection.kind === "error") {
5927
- process.exitCode = 1;
5928
- }
5929
- return;
5930
- }
5931
4695
  if (explicitUi || shouldAutoLaunchUi(rawArgs, terminalEnvironment)) {
5932
4696
  const { lang } = validateUiArgs(explicitUi ? rawArgs.slice(1) : []);
5933
4697
  if (!isInteractiveTerminal(terminalEnvironment)) {
@@ -5936,7 +4700,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
5936
4700
  } else {
5937
4701
  const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
5938
4702
  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);
5939
- const { runTui } = await import("./entry-GIDNR7CJ.mjs");
4703
+ const { runTui } = await import("./entry-NDASTAI5.mjs");
5940
4704
  const exitCode = await runTui(locale);
5941
4705
  if (exitCode !== 0)
5942
4706
  process.exitCode = exitCode;
@@ -5961,7 +4725,7 @@ async function runCliMain(rawArgs = process.argv.slice(2)) {
5961
4725
  process.exitCode = 1;
5962
4726
  }
5963
4727
  }
5964
- var isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve4(process.argv[1]);
4728
+ var isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve2(process.argv[1]);
5965
4729
  if (isMain)
5966
4730
  await runCliMain();
5967
4731
  export {