@oevery/rsp 3.3.0 → 4.0.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 (67) hide show
  1. package/README.md +24 -17
  2. package/README.zh-CN.md +25 -18
  3. package/dist/{chunk-TSUDEX5Y.mjs → chunk-HFSTLCCK.mjs} +1261 -319
  4. package/dist/cli.mjs +1129 -2350
  5. package/dist/{entry-GIDNR7CJ.mjs → entry-RIJQ2X46.mjs} +122 -36
  6. package/package.json +32 -24
  7. package/skills/rsp/SKILL.md +43 -38
  8. package/skills/rsp/references/closeout.md +38 -0
  9. package/skills/rsp/references/context-migration.md +22 -0
  10. package/skills/rsp/references/control-outcome.md +3 -29
  11. package/skills/rsp/references/coordination-recovery.md +25 -0
  12. package/skills/rsp/references/coordination-review.md +15 -0
  13. package/skills/rsp/references/coordination.md +59 -0
  14. package/skills/rsp/references/durable-review.md +60 -12
  15. package/skills/rsp/references/focus-continuation.md +8 -4
  16. package/skills/rsp/references/groups-dependencies.md +6 -4
  17. package/skills/rsp/references/response-language.md +2 -0
  18. package/skills/rsp/references/setup-repair.md +37 -7
  19. package/skills/rsp/references/work-model.md +27 -0
  20. package/skills/rsp-commit/SKILL.md +76 -31
  21. package/skills/rsp-commit/references/message.md +34 -0
  22. package/skills/rsp-doc/SKILL.md +65 -0
  23. package/skills/rsp-doc/agents/openai.yaml +3 -0
  24. package/skills/rsp-doc/references/artifact-lenses.md +43 -0
  25. package/skills/rsp-doc/references/release-communication.md +39 -0
  26. package/skills/rsp-doc/references/skill-authoring.md +36 -0
  27. package/skills/rsp-implement/SKILL.md +56 -25
  28. package/skills/rsp-implement/references/diagnosis.md +10 -0
  29. package/skills/rsp-implement/references/finding-resolution.md +21 -0
  30. package/skills/rsp-implement/references/test-first.md +7 -0
  31. package/skills/rsp-review/SKILL.md +46 -45
  32. package/skills/rsp-review/references/code-review.md +23 -8
  33. package/skills/rsp-review/references/document-review.md +25 -8
  34. package/skills/rsp-review/references/release-communication.md +21 -0
  35. package/skills/{rsp-structural-audit/references/structural-lenses.md → rsp-review/references/structural-review.md} +5 -5
  36. package/skills/rsp-shape/SKILL.md +24 -45
  37. package/skills/rsp-shape/references/complex-shaping.md +3 -1
  38. package/skills/rsp-shape/references/deep-clarification.md +4 -4
  39. package/skills/rsp-shape/references/domain-modeling.md +13 -0
  40. package/skills/rsp-shape/references/module-seams.md +13 -0
  41. package/skills/rsp-shape/references/planning.md +42 -0
  42. package/skills/{rsp-design → rsp-shape}/references/reversible-exploration.md +2 -2
  43. package/skills/rsp-verify/SKILL.md +44 -12
  44. package/dist/chunk-7HXQNBIG.mjs +0 -871
  45. package/dist/chunk-JOMFFQRG.mjs +0 -99
  46. package/dist/chunk-QM2UVPQK.mjs +0 -375
  47. package/dist/entry-YAW3PYFA.mjs +0 -174
  48. package/rules/rsp-rules.md +0 -48
  49. package/skills/rsp/references/implementation-evidence.md +0 -12
  50. package/skills/rsp/references/managed-routing.md +0 -45
  51. package/skills/rsp-design/SKILL.md +0 -53
  52. package/skills/rsp-design/references/domain-modeling.md +0 -11
  53. package/skills/rsp-design/references/module-seams.md +0 -11
  54. package/skills/rsp-diagnose/SKILL.md +0 -48
  55. package/skills/rsp-manage/SKILL.md +0 -101
  56. package/skills/rsp-manage/references/closeout.md +0 -13
  57. package/skills/rsp-manage/references/delegation.md +0 -47
  58. package/skills/rsp-manage/references/interruption-recovery.md +0 -19
  59. package/skills/rsp-manage/references/review-convergence.md +0 -9
  60. package/skills/rsp-release-docs/SKILL.md +0 -77
  61. package/skills/rsp-release-docs/references/convention-discovery.md +0 -65
  62. package/skills/rsp-release-docs/references/evidence-and-surfaces.md +0 -21
  63. package/skills/rsp-release-docs/references/output-contracts.md +0 -97
  64. package/skills/rsp-release-docs/references/publication-lifecycle.md +0 -28
  65. package/skills/rsp-resolve-findings/SKILL.md +0 -61
  66. package/skills/rsp-structural-audit/SKILL.md +0 -71
  67. package/skills/rsp-tdd/SKILL.md +0 -56
package/dist/cli.mjs CHANGED
@@ -1,125 +1,123 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- inspectPackagedSkillInventory,
4
- installPackagedSkills,
5
- presentPackagedSkillInventory,
6
- presentSkillInstallResult
7
- } from "./chunk-QM2UVPQK.mjs";
8
- import {
3
+ ARCHIVES_DIR,
9
4
  ArchiveHistoryError,
5
+ CHANGES_DIR,
6
+ CHANGE_DOCUMENT_SCHEMA,
7
+ CONFIG_PATH,
8
+ DEFAULT_DECISION_RECORDS_PATH,
9
+ DocumentSectionCardinalityError,
10
+ FOCUS_DIR,
11
+ GROUP_BRIEF_DOCUMENT_SCHEMA,
10
12
  GROUP_BRIEF_FILENAME,
11
13
  GROUP_REOPEN_COMPLETION_PREFIX,
12
14
  HISTORY_DEFAULT_LIMIT,
13
15
  HISTORY_MAX_LIMIT,
14
16
  IssueRelationshipError,
17
+ LOCK_PATH,
18
+ MAX_FOCUS_CAPSULE_BYTES,
19
+ ManagedPathError,
20
+ OBSOLETE_RSP_RULES_PATH,
21
+ PKG_ROOT,
22
+ RSP_DIR,
23
+ RSP_RULES_PATH,
15
24
  SPECS_DEFAULT_RESULT_LIMIT,
16
25
  SPECS_DETAIL_CONTENT_CODE_POINTS,
17
- SPECS_MAX_FILE_BYTES,
18
26
  SPECS_MAX_RESULT_LIMIT,
19
27
  SpecsQueryError,
20
28
  WorkRefError,
29
+ appendDocumentSectionItem,
21
30
  buildDurableReviewGuidance,
22
31
  cleanupEmptyParentDirs,
32
+ clearConfigCache,
23
33
  collectArchiveReadiness,
34
+ countCheckboxes,
24
35
  createIssueRelationship,
36
+ detectDeltaSections,
25
37
  detectProjectName,
38
+ emitJson,
39
+ ensureManagedFile,
40
+ generateConfigTemplate,
26
41
  generateGroupBriefContent,
42
+ getDocumentSectionDefinitionByHeading,
43
+ getDocumentSectionMarkdown,
44
+ getDocumentSections,
45
+ getDocumentTitles,
27
46
  getDurableReviewCandidateTargets,
28
47
  getGroupReopenEvidence,
48
+ getVersion,
49
+ getVisibleBodyLines,
29
50
  guardRspInitialized,
30
51
  hasArchivedGroupBrief,
52
+ hasMeaningfulBlockers,
31
53
  historyInspectionComplete,
32
54
  inspectArchiveHistory,
33
55
  inspectArchiveTree,
34
56
  inspectChangeDependencies,
35
57
  inspectChangeGroups,
36
58
  inspectFocusTree,
59
+ inspectManagedDirectory,
60
+ inspectManagedFile,
61
+ inspectManagedFileTree,
37
62
  inspectProjectStatus,
63
+ inspectRspConfig,
64
+ inspectRspInitialization,
38
65
  inspectSpecs,
39
66
  inspectUnsupportedRules,
40
67
  inspectWorkTree,
41
68
  isRecognizedGeneratedSpecsIndex,
42
69
  isValidSpecName,
70
+ loadRspConfig,
71
+ markdownProse,
72
+ markdownText,
73
+ markdownTopLevelComments,
43
74
  normalizeExecutableWorkRef,
44
75
  normalizeLogicalPath,
45
76
  normalizeWorkRefSegment,
77
+ parseFrontmatter,
46
78
  parseIssueRelationships,
79
+ parseMarkdown,
80
+ parseRspDocument,
81
+ parseScenarios,
82
+ pc,
47
83
  projectDependencyForest,
48
84
  projectSpecsDetail,
49
85
  projectSpecsSearch,
50
86
  projectSpecsTree,
51
87
  queryArchiveHistory,
52
88
  readArchiveHistoryDetail,
53
- resolveArchiveDirectory,
54
- resolveExecutableChange,
55
- resolveFocusMarkerPath,
56
- resolveWorkRef,
57
- selectArchiveHistoryRecord,
58
- selectArchivedGroupBrief,
59
- specsInspectionComplete,
60
- toArchiveReadinessOutput,
61
- walkMarkdownFiles
62
- } from "./chunk-TSUDEX5Y.mjs";
63
- import {
64
- ARCHIVES_DIR,
65
- CHANGES_DIR,
66
- CHANGE_DOCUMENT_SCHEMA,
67
- CONFIG_PATH,
68
- DEFAULT_DECISION_RECORDS_PATH,
69
- DocumentSectionCardinalityError,
70
- FOCUS_DIR,
71
- GROUP_BRIEF_DOCUMENT_SCHEMA,
72
- LOCK_PATH,
73
- MAX_FOCUS_CAPSULE_BYTES,
74
- ManagedPathError,
75
- OBSOLETE_RSP_RULES_PATH,
76
- PKG_ROOT,
77
- RSP_DIR,
78
- RSP_RULES_PATH,
79
- appendDocumentSectionItem,
80
- clearConfigCache,
81
- countCheckboxes,
82
- detectDeltaSections,
83
- emitJson,
84
- ensureDecisionRecordsDirectory,
85
- ensureManagedFile,
86
- generateConfigTemplate,
87
- getDocumentSectionBody,
88
- getDocumentSectionDefinitionByHeading,
89
- getDocumentSections,
90
- getDocumentTitles,
91
- getVersion,
92
- hasMeaningfulBlockers,
93
- inspectManagedDirectory,
94
- inspectManagedFile,
95
- inspectManagedFileTree,
96
- inspectRspConfig,
97
- loadRspConfig,
98
- parseFrontmatter,
99
- parseRspDocument,
100
- parseScenarios,
101
- pc,
102
- reconcileRspConfigDefaults,
103
89
  renderDocumentSectionHeading,
104
90
  renderDocumentTitle,
105
91
  requireManagedDirectory,
106
92
  requireManagedFile,
93
+ resolveArchiveDirectory,
107
94
  resolveDecisionRecordsPath,
95
+ resolveExecutableChange,
96
+ resolveFocusMarkerPath,
108
97
  resolveKinds,
98
+ resolveLanguagePolicy,
109
99
  resolveManagedDirectoryChain,
110
100
  resolveRequiredSections,
101
+ resolveWorkRef,
102
+ resolveWorkflowPolicy,
103
+ selectArchiveHistoryRecord,
104
+ selectArchivedGroupBrief,
105
+ specsInspectionComplete,
106
+ toArchiveReadinessOutput,
111
107
  toErrorMessage,
112
108
  validateDecisionRecordsFilesystemPath,
109
+ walkMarkdownFiles,
113
110
  writeManagedFile
114
- } from "./chunk-7HXQNBIG.mjs";
111
+ } from "./chunk-HFSTLCCK.mjs";
115
112
 
116
113
  // src/cli.ts
117
- import { resolve as resolve4 } from "path";
114
+ import { resolve as resolve2 } from "path";
118
115
  import { fileURLToPath } from "url";
119
116
  import { runMain } from "citty";
120
117
 
121
118
  // src/cli/capabilities.ts
122
119
  var CLI_COMMAND_CAPABILITIES = {
120
+ "config": { json: true, compact: true },
123
121
  "status": { json: true, compact: true },
124
122
  "show": { json: true, compact: true },
125
123
  "ready": { json: true, compact: true },
@@ -164,9 +162,10 @@ import { readFile } from "fs/promises";
164
162
  var choosePlaceholderRe = /^<choose:/i;
165
163
  function inspectChangeDocument(content, options) {
166
164
  const diagnostics = [];
165
+ const document = typeof content === "string" ? parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA) : content;
167
166
  let frontmatter = null;
168
167
  try {
169
- frontmatter = parseFrontmatter(content);
168
+ frontmatter = parseFrontmatter(document.markdown);
170
169
  } catch (error) {
171
170
  const message = error instanceof Error ? error.message : String(error);
172
171
  diagnostics.push({
@@ -182,7 +181,6 @@ function inspectChangeDocument(content, options) {
182
181
  message: "missing YAML frontmatter"
183
182
  });
184
183
  }
185
- const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
186
184
  for (const heading of options.requiredSections) {
187
185
  const definition = getDocumentSectionDefinitionByHeading(CHANGE_DOCUMENT_SCHEMA, heading);
188
186
  if (!definition)
@@ -481,9 +479,10 @@ async function runCheck(options = {}) {
481
479
  });
482
480
  continue;
483
481
  }
484
- for (const diagnostic of inspectChangeDocument(content, { name, validKinds, requiredSections }))
482
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
483
+ for (const diagnostic of inspectChangeDocument(document, { name, validKinds, requiredSections }))
485
484
  addDiagnostic({ ...diagnostic, change: name, path: fp });
486
- const deltas = detectDeltaSections(content);
485
+ const deltas = detectDeltaSections(document);
487
486
  if (deltas.added || deltas.modified || deltas.removed) {
488
487
  const parts = [];
489
488
  if (deltas.added)
@@ -500,7 +499,7 @@ async function runCheck(options = {}) {
500
499
  message: `delta markers found: ${parts.join(", ")}`
501
500
  });
502
501
  }
503
- const placeholderLines = findUnfinishedTemplateLines(content);
502
+ const placeholderLines = findUnfinishedTemplateLines(document.markdown);
504
503
  if (placeholderLines.length > 0) {
505
504
  addDiagnostic({
506
505
  severity: "warning",
@@ -512,7 +511,7 @@ async function runCheck(options = {}) {
512
511
  hint: "Replace angle-bracket template text with concrete change details before archiving."
513
512
  });
514
513
  }
515
- const clarificationLines = findUnresolvedClarificationLines(content);
514
+ const clarificationLines = findUnresolvedClarificationLines(document.markdown);
516
515
  if (clarificationLines.length > 0) {
517
516
  addDiagnostic({
518
517
  severity: "warning",
@@ -524,7 +523,7 @@ async function runCheck(options = {}) {
524
523
  hint: "Resolve or remove clarification markers once the open question is answered."
525
524
  });
526
525
  }
527
- const scenarios = parseScenarios(content);
526
+ const scenarios = parseScenarios(document.markdown);
528
527
  if (scenarios.length > 0) {
529
528
  const scenarioIssues = [];
530
529
  for (const s of scenarios) {
@@ -569,32 +568,178 @@ async function runCheck(options = {}) {
569
568
  }
570
569
  function findUnfinishedTemplateLines(content) {
571
570
  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
- });
571
+ return collectMatchingLines(content, (value) => placeholderRe.test(value));
576
572
  }
577
573
  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, "");
574
+ return collectMatchingLines(content, (value) => /\[(?:NEEDS|TODO|TBD)\s+CLARIFICATION(?::[^\]]*)?\]/i.test(value));
582
575
  }
583
576
  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()}`);
577
+ const document = parseMarkdown(content);
578
+ const sourceLines = document.source.split(/\r?\n/);
579
+ const matched = /* @__PURE__ */ new Set();
580
+ for (const fragment of markdownProse(document)) {
581
+ fragment.value.split(/\r?\n/).forEach((value, index) => {
582
+ if (predicate(value))
583
+ matched.add(fragment.line + index);
584
+ });
585
+ }
586
+ return [...matched].sort((a, b) => a - b).slice(0, 10).map((line) => `line ${line}: ${sourceLines[line - 1].trim()}`);
587
+ }
588
+
589
+ // src/commands/config.ts
590
+ async function showConfig() {
591
+ try {
592
+ const inspection = await inspectRspConfig();
593
+ if (inspection.issues.length > 0) {
594
+ return {
595
+ command: "config",
596
+ ok: false,
597
+ path: CONFIG_PATH,
598
+ summary: null,
599
+ diagnostics: [{
600
+ severity: "error",
601
+ code: "invalid_config",
602
+ path: CONFIG_PATH,
603
+ message: inspection.issues.join("; ")
604
+ }],
605
+ runtime: []
606
+ };
607
+ }
608
+ return {
609
+ command: "config",
610
+ ok: true,
611
+ path: CONFIG_PATH,
612
+ summary: {
613
+ kinds: resolveKinds(inspection.config),
614
+ decisions: { path: resolveDecisionRecordsPath(inspection.config) },
615
+ workflow: resolveWorkflowPolicy(inspection.config, { configValid: true }),
616
+ language: resolveLanguagePolicy(inspection.config, { configValid: true })
617
+ },
618
+ diagnostics: [],
619
+ runtime: []
620
+ };
621
+ } catch (error) {
622
+ return {
623
+ command: "config",
624
+ ok: false,
625
+ path: CONFIG_PATH,
626
+ summary: null,
627
+ diagnostics: [{
628
+ severity: "error",
629
+ code: "invalid_config",
630
+ path: CONFIG_PATH,
631
+ message: toErrorMessage(error)
632
+ }],
633
+ runtime: []
634
+ };
590
635
  }
591
- return matches.slice(0, 10);
592
636
  }
593
637
 
594
638
  // 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";
639
+ import { existsSync } from "fs";
640
+ import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
641
+ import { basename, join as join3, relative as relative2 } from "path";
642
+
643
+ // src/core/project-version.ts
644
+ import { readFile as readFile2, realpath } from "fs/promises";
645
+ import { join } from "path";
646
+ var PROJECT_VERSION_PATH = ".rsp/version";
647
+ var PROJECT_SOURCE_PATH = ".rsp/source";
648
+ var SOURCE_INVOCATION = "mise exec -- node dist/cli.mjs";
649
+ function isExactVersion(value) {
650
+ return /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-z-][\da-z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-z-][\da-z-]*))*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?$/i.test(value);
651
+ }
652
+ function exactInvocation(version) {
653
+ if (!isExactVersion(version))
654
+ throw new Error(`Invalid exact RSP version: ${version}`);
655
+ return `npx -y @oevery/rsp@${version}`;
656
+ }
657
+ async function readProjectVersion(projectRoot = ".") {
658
+ const directory = inspectManagedDirectory(join(projectRoot, RSP_DIR), "RSP root", { allowMissing: true });
659
+ if (directory.issue)
660
+ throw directory.issue;
661
+ if (!directory.exists)
662
+ return null;
663
+ const path = join(projectRoot, PROJECT_VERSION_PATH);
664
+ const inspection = inspectManagedFile(path, "project CLI version", { allowMissing: true });
665
+ if (inspection.issue)
666
+ throw inspection.issue;
667
+ if (!inspection.exists)
668
+ return null;
669
+ const version = (await readFile2(path, "utf8")).trim();
670
+ if (!isExactVersion(version))
671
+ throw new Error(`${PROJECT_VERSION_PATH} must contain one exact package version, not a tag or range`);
672
+ return version;
673
+ }
674
+ async function readProjectRuntime(projectRoot = ".") {
675
+ const version = await readProjectVersion(projectRoot);
676
+ const source = inspectManagedFile(join(projectRoot, PROJECT_SOURCE_PATH), "project source selection", { allowMissing: true });
677
+ if (source.issue)
678
+ throw source.issue;
679
+ if (!source.exists)
680
+ return { mode: "package", version };
681
+ if (version !== null)
682
+ throw new Error(`${PROJECT_SOURCE_PATH} and ${PROJECT_VERSION_PATH} are mutually exclusive; explicitly select one runtime.`);
683
+ if ((await readFile2(join(projectRoot, PROJECT_SOURCE_PATH), "utf8")).trim() !== ".")
684
+ throw new Error(`${PROJECT_SOURCE_PATH} must contain . to select this RSP source checkout.`);
685
+ if (await realpath(projectRoot) !== await realpath(PKG_ROOT))
686
+ throw new Error(`Source mode requires this checkout's local CLI. Build here, then use ${SOURCE_INVOCATION} <command>; a registry package or another checkout cannot substitute.`);
687
+ const sourceDirectory = inspectManagedDirectory(join(projectRoot, "src"), "RSP source directory");
688
+ const sourceEntry = inspectManagedFile(join(projectRoot, "src/cli.ts"), "RSP source entry");
689
+ if (sourceDirectory.issue || sourceEntry.issue)
690
+ throw sourceDirectory.issue ?? sourceEntry.issue;
691
+ return { mode: "source" };
692
+ }
693
+ async function selectProjectRuntime(operation = "ordinary") {
694
+ const version = await getVersion();
695
+ exactInvocation(version);
696
+ const selected = await readProjectRuntime();
697
+ if (selected.mode === "source")
698
+ return { mode: "source", version };
699
+ if (selected.version && selected.version !== version && operation !== "update") {
700
+ throw new Error(`Project pins RSP ${selected.version}; running CLI is ${version}. Use ${exactInvocation(selected.version)} <command>. To migrate deliberately, inspect the target version and run ${exactInvocation(version)} update.`);
701
+ }
702
+ if (selected.version && operation === "update" && compareVersions(version, selected.version) < 0)
703
+ throw new Error(`Downgrading RSP from ${selected.version} to ${version} is unsupported. Use ${exactInvocation(selected.version)} <command>.`);
704
+ return { mode: "package", version };
705
+ }
706
+ function compareVersions(left, right) {
707
+ const parts = (value) => {
708
+ const [core, ...pre] = value.split("+", 1)[0].split("-");
709
+ return { core: core.split(".").map(BigInt), pre: pre.length ? pre.join("-").split(".") : [] };
710
+ };
711
+ const a = parts(left);
712
+ const b = parts(right);
713
+ for (let i = 0; i < 3; i++) {
714
+ if (a.core[i] !== b.core[i])
715
+ return a.core[i] > b.core[i] ? 1 : -1;
716
+ }
717
+ if (!a.pre.length || !b.pre.length)
718
+ return Number(!a.pre.length) - Number(!b.pre.length);
719
+ for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
720
+ const x = a.pre[i];
721
+ const y = b.pre[i];
722
+ if (x === y)
723
+ continue;
724
+ if (x === void 0 || y === void 0)
725
+ return x === void 0 ? -1 : 1;
726
+ const nx = /^\d+$/.test(x);
727
+ const ny = /^\d+$/.test(y);
728
+ if (nx && ny)
729
+ return BigInt(x) > BigInt(y) ? 1 : -1;
730
+ if (nx !== ny)
731
+ return nx ? -1 : 1;
732
+ return x > y ? 1 : -1;
733
+ }
734
+ return 0;
735
+ }
736
+ async function guardProjectVersion(rawArgs) {
737
+ if (rawArgs.some((arg) => ["--help", "-h"].includes(arg)) || rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0]))
738
+ return;
739
+ if (["doctor", "config", "update"].includes(rawArgs[0] ?? ""))
740
+ return;
741
+ await selectProjectRuntime();
742
+ }
598
743
 
599
744
  // src/core/artifacts.ts
600
745
  var RSP_AGENTS_BEGIN = "<!-- rsp:begin -->";
@@ -747,62 +892,89 @@ function getChangeTemplateByKind(kind) {
747
892
  };
748
893
  }
749
894
  function changeSectionHeading(sectionId) {
750
- return renderDocumentSectionHeading(CHANGE_DOCUMENT_SCHEMA, sectionId);
895
+ const guidance = {
896
+ proposal: "State the intended outcome, why it matters, scope, and non-goals; do not narrate execution.",
897
+ spec: "Describe the contract delta and acceptance conditions, referencing affected existing Specs; do not copy the full baseline or implementation steps.",
898
+ design: "Explain the approach, responsibility boundaries, and necessary tradeoffs; keep simple changes brief and avoid repeating requirements or tasks.",
899
+ tasks: "List checkable work and mark completion only after outcomes exist; prescribe order only where correctness, safety, or migration requires it.",
900
+ verify: "Connect acceptance to verification methods; after execution retain actual results, gaps, and limits, not command transcripts.",
901
+ blockers: "Name unresolved decisions or dependencies that prevent progress; use none when there are no blockers."
902
+ };
903
+ return `${renderDocumentSectionHeading(CHANGE_DOCUMENT_SCHEMA, sectionId)}
904
+ <!-- ${guidance[sectionId]} -->`;
751
905
  }
752
- function renderRspAgentsBlock() {
906
+ function renderRspAgentsBlock(runtime) {
907
+ const { version } = runtime;
908
+ 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
909
  return `${RSP_AGENTS_BEGIN}
754
910
  ## RSP Entry
755
911
 
756
912
  RSP tracks current work, stable specs, and archives under \`.rsp/\`.
757
913
 
758
- Read in order:
914
+ Read in order using supplied or confirmed paths; discover only missing paths or applicable ancestor instructions, without overlapping whole-tree scans:
759
915
  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.
916
+ 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.
917
+ 3. Use the project \`rsp\` Skill at \`.agents/skills/rsp/SKILL.md\`; hosts may load it through Skill discovery or read it directly. Skills are the only workflow: if a required Skill is missing or unusable, stop dependent work and report the incomplete installation. Do not use retired Rules or install implicitly.
918
+ 4. \`.rsp/focus.d/\`; marker paths form the open-work candidate set, while optional bounded Markdown content is recovery guidance only. For grouped work read the sibling Group Brief, then the explicitly selected child Change.
763
919
  5. Only the relevant Specs and Decision Records under the configured authoritative path.
764
920
 
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.
921
+ If \`.rsp/focus.d/\` is empty or has several candidates, use an explicit WorkRef, the user's mentioned Change, or current status/dependency evidence to resolve the default action.
766
922
  Do not treat \`.rsp/specs/\` or \`.rsp/changes/\` as replacements for nearest \`AGENTS.md\` or \`CONTEXT.md\`.
923
+
924
+ ${selection}
767
925
  ${RSP_AGENTS_END}`;
768
926
  }
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);
927
+ function upsertRspAgentsBlock(content, runtime) {
928
+ const block = renderRspAgentsBlock(runtime);
929
+ const span = rspAgentsBlockSpan(content);
930
+ if (span) {
931
+ const next2 = content.slice(0, span.start) + block + content.slice(span.end);
774
932
  return { content: next2, changed: next2 !== content };
775
933
  }
776
- const trimmed = content.trimStart();
777
- const next = trimmed ? `${block}
934
+ const next = content ? `${block}
778
935
 
779
- ${trimmed}` : `${block}
936
+ ${content}` : `${block}
780
937
  `;
781
938
  return { content: next, changed: true };
782
939
  }
783
940
  function hasRspAgentsBlock(content) {
784
- return content.includes(RSP_AGENTS_BEGIN) && content.includes(RSP_AGENTS_END);
941
+ return rspAgentsBlockSpan(content) !== null;
942
+ }
943
+ function rspAgentsBlockSpan(content) {
944
+ let start;
945
+ for (const comment of markdownTopLevelComments(content)) {
946
+ if (comment.source === RSP_AGENTS_BEGIN && start === void 0)
947
+ start = comment.start;
948
+ if (comment.source === RSP_AGENTS_END && start !== void 0)
949
+ return { start, end: comment.end };
950
+ }
951
+ return null;
785
952
  }
786
953
  function generateDesignContent(projectName) {
787
954
  const placeholder = "<\u2026>";
788
955
  return `# Project Design: ${projectName}
789
956
 
790
957
  ## Purpose
791
- - ${placeholder}
792
-
793
- ## Stable Facts
958
+ <!-- Define the system's purpose and consumers, not the current change. -->
794
959
  - ${placeholder}
795
960
 
796
961
  ## Boundaries
962
+ <!-- Define system responsibilities, exclusions, and responsibility handoffs. -->
797
963
  - In scope:
798
964
  - ${placeholder}
799
965
  - Out of scope:
800
966
  - ${placeholder}
801
967
 
968
+ ## Contracts
969
+ <!-- State current architectural obligations and invariants that must survive implementation changes. -->
970
+ - ${placeholder}
971
+
802
972
  ## Structure
973
+ <!-- Describe stable ownership and dependency direction; leave repository navigation to CONTEXT.md. -->
803
974
  - ${placeholder}
804
975
 
805
976
  ## Constraints
977
+ <!-- Record necessary technical, security, protocol, or compatibility limits; link lasting rationale rather than duplicating it. -->
806
978
  - ${placeholder}
807
979
  `;
808
980
  }
@@ -812,1577 +984,297 @@ function generateSpecContent(name) {
812
984
  return `# ${title}
813
985
 
814
986
  ## Purpose
815
- - ${placeholder}
816
-
817
- ## Stable Facts
987
+ <!-- Define the capability, its consumers, and its value; do not describe this change. -->
818
988
  - ${placeholder}
819
989
 
820
990
  ## Boundaries
991
+ <!-- Define responsibilities, exclusions, and handoffs to other capabilities. -->
821
992
  - In scope:
822
993
  - ${placeholder}
823
994
  - Out of scope:
824
995
  - ${placeholder}
825
996
 
826
- ## Constraints
997
+ ## Contracts
998
+ <!-- State current behavior, inputs and outputs, invariants, and failure semantics by capability, not internal implementation steps. -->
827
999
  - ${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
1000
 
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
- }
1001
+ ## Scenarios
1002
+ <!-- Clarify key cross-boundary, failure, or ambiguous behavior with a few examples; omit when unnecessary and keep test execution results in the Change. -->
1003
+ - ${placeholder}
1620
1004
 
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];
1946
- }
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
- }
1005
+ ## Constraints
1006
+ <!-- Record necessary technical, protocol, security, or compatibility limits; do not repeat behavior requirements or decision rationale. -->
1007
+ - ${placeholder}
1008
+ `;
1009
+ }
1010
+
1011
+ // src/core/update-check.ts
1012
+ async function checkForUpdates(current) {
1964
1013
  try {
1965
- await writeSnapshotExclusive(destination, snapshot2, parentChain, {});
1014
+ const response = await fetch("https://registry.npmjs.org/-/package/@oevery%2Frsp/dist-tags", {
1015
+ signal: AbortSignal.timeout(2e3),
1016
+ redirect: "error"
1017
+ });
1018
+ if (!response.ok)
1019
+ throw new Error("registry unavailable");
1020
+ const body = await response.text();
1021
+ if (body.length > 16384)
1022
+ throw new Error("registry metadata exceeded bound");
1023
+ const latest = JSON.parse(body)?.latest;
1024
+ if (typeof latest !== "string" || !isExactVersion(latest) || !/^\d+\.\d+\.\d+$/.test(latest))
1025
+ throw new Error("invalid stable version");
1026
+ return { status: compareVersions(latest, current) > 0 ? "available" : "current", current, latest };
1966
1027
  } catch {
1967
- return [quarantine];
1028
+ return { status: "unavailable", current };
1968
1029
  }
1969
- return [quarantine];
1970
1030
  }
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
- );
1031
+
1032
+ // src/commands/skills.ts
1033
+ import { lstatSync, realpathSync } from "fs";
1034
+ import { chmod, copyFile, mkdir, mkdtemp, readdir, readFile as readFile3, rename, rm } from "fs/promises";
1035
+ import { dirname, join as join2, relative } from "path";
1036
+ var DEFAULT_PACKAGED_SKILL_NAMES = [
1037
+ "rsp",
1038
+ "rsp-shape",
1039
+ "rsp-implement",
1040
+ "rsp-doc",
1041
+ "rsp-verify",
1042
+ "rsp-review",
1043
+ "rsp-commit"
1044
+ ];
1045
+ var RETIRED_PACKAGED_SKILL_NAMES = [
1046
+ "rsp-address-review",
1047
+ "rsp-codebase-audit",
1048
+ "rsp-structural-audit",
1049
+ "rsp-design",
1050
+ "rsp-diagnose",
1051
+ "rsp-manage",
1052
+ "rsp-release-docs",
1053
+ "rsp-resolve-findings",
1054
+ "rsp-tdd"
1055
+ ];
1056
+ function unsupportedEntry(path, label) {
1057
+ return new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`);
1058
+ }
1059
+ async function inspectPackagedSkills(packageRoot) {
1060
+ const skillsRoot = join2(packageRoot, "skills");
1061
+ const rootInspection = inspectManagedDirectory(skillsRoot, "packaged Skills root");
1062
+ if (rootInspection.issue)
1063
+ throw rootInspection.issue;
1064
+ const entries = await readdir(skillsRoot, { withFileTypes: true });
1065
+ const skills = [];
1066
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1067
+ if (entry.name.startsWith("."))
1068
+ continue;
1069
+ const root = join2(skillsRoot, entry.name);
1070
+ if (!entry.isDirectory())
1071
+ throw unsupportedEntry(root, "packaged Skills");
1072
+ const inspection = await inspectManagedFileTree(root, `packaged Skill ${entry.name}`, { includeHidden: true });
1073
+ if (inspection.issues.length > 0)
1074
+ throw inspection.issues[0];
1075
+ skills.push({
1076
+ name: entry.name,
1077
+ root,
1078
+ files: inspection.files.map((path) => relative(root, path))
1079
+ });
1979
1080
  }
1980
- await unlink4(quarantine);
1981
- await assertStableDirectoryChain(parentChain, "update rollback quarantine cleanup");
1081
+ return skills;
1982
1082
  }
1983
- async function writeSnapshotExclusive(path, snapshot2, parentChain, testing) {
1984
- let handle = null;
1985
- let createdIdentity = null;
1083
+ function inspectTarget(path, name) {
1986
1084
  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
- }
1085
+ const stats = lstatSync(path);
1086
+ if (stats.isDirectory())
1087
+ return "directory";
1088
+ throw unsupportedEntry(path, `installed Skill ${name}`);
2005
1089
  } 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
- }
1090
+ if (error.code === "ENOENT")
1091
+ return "missing";
2021
1092
  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
1093
  }
2030
1094
  }
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");
1095
+ function inspectInventoryTarget(path) {
2034
1096
  try {
2035
- await rename2(path, quarantine);
1097
+ return lstatSync(path).isDirectory() ? "directory" : "divergent";
2036
1098
  } catch (error) {
2037
1099
  if (error.code === "ENOENT")
2038
- return [];
1100
+ return "missing";
2039
1101
  throw error;
2040
1102
  }
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
1103
  }
2094
- function samePathState(left, right) {
2095
- if (left.kind !== right.kind)
1104
+ function isCanonicalSourceProjection(sourceRoot, targetRoot) {
1105
+ try {
1106
+ if (!lstatSync(targetRoot).isSymbolicLink())
1107
+ return false;
1108
+ return realpathSync(targetRoot) === realpathSync(sourceRoot);
1109
+ } catch {
2096
1110
  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
1111
  }
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
- }
2105
- return false;
2106
- }
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
1112
  }
2116
- function sameClaimedPathState(left, right) {
2117
- if (left.kind !== right.kind)
1113
+ async function treesEqual(source, targetRoot) {
1114
+ const target = await inspectManagedFileTree(targetRoot, `installed Skill ${source.name}`, { includeHidden: true });
1115
+ if (target.issues.length > 0)
1116
+ throw target.issues[0];
1117
+ const targetFiles = target.files.map((path) => relative(targetRoot, path));
1118
+ if (source.files.length !== targetFiles.length || source.files.some((path, index) => path !== targetFiles[index]))
2118
1119
  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;
1120
+ for (const path of source.files) {
1121
+ const sourcePath = join2(source.root, path);
1122
+ const targetPath = join2(targetRoot, path);
1123
+ const [sourceStats, targetStats] = [lstatSync(sourcePath), lstatSync(targetPath)];
1124
+ if (!sourceStats.isFile() || !targetStats.isFile())
1125
+ throw unsupportedEntry(!sourceStats.isFile() ? sourcePath : targetPath, `Skill ${source.name}`);
1126
+ if ((sourceStats.mode & 511) !== (targetStats.mode & 511))
1127
+ return false;
1128
+ const [sourceContent, targetContent] = await Promise.all([readFile3(sourcePath), readFile3(targetPath)]);
1129
+ if (!sourceContent.equals(targetContent))
1130
+ return false;
2126
1131
  }
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;
1132
+ return true;
2140
1133
  }
2141
- function toProjectPath(projectRoot, path) {
2142
- return relative3(projectRoot, path).split(sep3).join("/");
1134
+ async function inspectPackagedSkillInventory(options = {}) {
1135
+ const packageRoot = options.packageRoot ?? PKG_ROOT;
1136
+ const projectRoot = options.projectRoot ?? process.cwd();
1137
+ const packagedSkills = await inspectPackagedSkills(packageRoot);
1138
+ const packagedNames = new Set(packagedSkills.map((skill) => skill.name));
1139
+ const missingDefaults = DEFAULT_PACKAGED_SKILL_NAMES.filter((name) => !packagedNames.has(name));
1140
+ if (missingDefaults.length > 0)
1141
+ throw new Error(`packaged Skills missing default Skills: ${missingDefaults.join(", ")}`);
1142
+ const manifest = JSON.parse(await readFile3(join2(packageRoot, "package.json"), "utf8"));
1143
+ if (typeof manifest.name !== "string" || typeof manifest.version !== "string")
1144
+ throw new Error("package.json must contain string name and version fields");
1145
+ const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
1146
+ const defaults = new Set(DEFAULT_PACKAGED_SKILL_NAMES);
1147
+ const skills = [];
1148
+ for (const skill of packagedSkills) {
1149
+ const target = join2(targetRoot, skill.name);
1150
+ const targetStatus = inspectInventoryTarget(target);
1151
+ skills.push({
1152
+ name: skill.name,
1153
+ kind: defaults.has(skill.name) ? "default" : "optional",
1154
+ status: targetStatus === "missing" ? "missing" : targetStatus === "divergent" ? isCanonicalSourceProjection(skill.root, target) ? "unchanged" : "divergent" : await treesEqual(skill, target) ? "unchanged" : "divergent"
1155
+ });
1156
+ }
1157
+ const retired = RETIRED_PACKAGED_SKILL_NAMES.filter((name) => inspectInventoryTarget(join2(targetRoot, name)) !== "missing");
1158
+ return {
1159
+ retired,
1160
+ package: { name: manifest.name, version: manifest.version },
1161
+ target: relative(projectRoot, targetRoot) || ".",
1162
+ skills
1163
+ };
2143
1164
  }
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
- };
1165
+ async function copySkillTree(source, targetRoot) {
1166
+ await mkdir(targetRoot, { recursive: true });
1167
+ for (const path of source.files) {
1168
+ const sourcePath = join2(source.root, path);
1169
+ const targetPath = join2(targetRoot, path);
1170
+ const stats = lstatSync(sourcePath);
1171
+ if (!stats.isFile())
1172
+ throw unsupportedEntry(sourcePath, `packaged Skill ${source.name}`);
1173
+ await mkdir(dirname(targetPath), { recursive: true });
1174
+ await copyFile(sourcePath, targetPath);
1175
+ await chmod(targetPath, stats.mode & 511);
1176
+ }
1177
+ }
1178
+ async function updatePackagedSkills(options = {}) {
1179
+ const packageRoot = options.packageRoot ?? PKG_ROOT;
1180
+ const projectRoot = options.projectRoot ?? process.cwd();
1181
+ const renamePath = options.renamePath ?? rename;
1182
+ const packaged = await inspectPackagedSkills(packageRoot);
1183
+ const skills = DEFAULT_PACKAGED_SKILL_NAMES.map((name) => {
1184
+ const skill = packaged.find((candidate) => candidate.name === name);
1185
+ if (!skill)
1186
+ throw new Error(`Packaged Skill missing: ${name}`);
1187
+ return skill;
1188
+ }).sort((a, b) => a.name.localeCompare(b.name));
1189
+ const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
1190
+ const result = { installed: [], removed: [], unchanged: [], replaced: [] };
1191
+ for (const skill of skills) {
1192
+ const target = join2(targetRoot, skill.name);
1193
+ if (isCanonicalSourceProjection(skill.root, target))
1194
+ result.unchanged.push(skill.name);
1195
+ else if (inspectTarget(target, skill.name) === "missing")
1196
+ result.installed.push(skill.name);
1197
+ else if (await treesEqual(skill, target))
1198
+ result.unchanged.push(skill.name);
1199
+ else
1200
+ result.replaced.push(skill.name);
2165
1201
  }
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}`);
1202
+ for (const name of RETIRED_PACKAGED_SKILL_NAMES) {
1203
+ if (inspectTarget(join2(targetRoot, name), name) !== "missing")
1204
+ result.removed.push(name);
1205
+ }
1206
+ if (options.dryRun || result.installed.length + result.replaced.length + result.removed.length === 0)
1207
+ return result;
1208
+ await mkdir(targetRoot, { recursive: true });
1209
+ resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
1210
+ const staging = await mkdtemp(join2(targetRoot, ".rsp-skills-"));
1211
+ const completed = [];
1212
+ let failure2;
1213
+ try {
1214
+ for (const skill of skills) {
1215
+ if (!result.unchanged.includes(skill.name))
1216
+ await copySkillTree(skill, join2(staging, skill.name));
2187
1217
  }
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;
1218
+ for (const name of result.removed) {
1219
+ resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
1220
+ inspectTarget(join2(targetRoot, name), name);
1221
+ await rm(join2(targetRoot, name), { recursive: true, force: true });
1222
+ completed.push(`removed ${name}`);
2347
1223
  }
2348
- });
1224
+ for (const skill of skills) {
1225
+ if (result.unchanged.includes(skill.name))
1226
+ continue;
1227
+ resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
1228
+ const target = join2(targetRoot, skill.name);
1229
+ inspectTarget(target, skill.name);
1230
+ await rm(target, { recursive: true, force: true });
1231
+ await renamePath(join2(staging, skill.name), target);
1232
+ completed.push(`installed ${skill.name}`);
1233
+ }
1234
+ } catch (error) {
1235
+ failure2 = new Error(`Skill update stopped; completed: ${completed.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1236
+ }
1237
+ try {
1238
+ await rm(staging, { recursive: true });
1239
+ } catch (error) {
1240
+ failure2 = new Error(`Temporary Skill content cleanup failed: ${staging}`, { cause: failure2 ?? error });
1241
+ }
1242
+ if (failure2)
1243
+ throw failure2;
1244
+ return result;
2349
1245
  }
2350
1246
 
2351
1247
  // src/commands/doctor.ts
2352
1248
  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
1249
  const checks = [];
2370
- if (repairIssue) {
1250
+ const runtime = [];
1251
+ const reportRuntime = (diagnostic) => runtime.push(diagnostic);
1252
+ reportCheck(checks, ".rsp exists", existsSync(RSP_DIR), "Run: rsp init");
1253
+ reportRetiredProtocol(checks);
1254
+ const sourceMode = await reportProjectRuntime(checks);
1255
+ if (options.checkUpdates) {
1256
+ const notice = await checkForUpdates(await getVersion());
2371
1257
  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."
1258
+ status: "info",
1259
+ code: `update_${notice.status}`,
1260
+ label: "stable package update check",
1261
+ 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.",
1262
+ 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
1263
  });
2377
1264
  }
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
1265
  await reportUnsupportedRules(checks, reportRuntime);
2384
- reportCheck(checks, "specs/design.md exists", existsSync2(designPath), "Run: rsp init");
1266
+ const initialization = inspectRspInitialization();
1267
+ checks.push({ status: initialization ? "issue" : "ok", label: "specs/design.md exists", message: initialization });
2385
1268
  await checkAgents(checks, reportRuntime);
1269
+ if (existsSync("CONTEXT-MAP.md")) {
1270
+ checks.push({
1271
+ status: "info",
1272
+ code: "legacy_context_map",
1273
+ label: "legacy root context map detected",
1274
+ message: "CONTEXT-MAP.md remains; its semantic migration requires owner review.",
1275
+ 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."
1276
+ });
1277
+ }
2386
1278
  await checkSpecsTree(checks, reportRuntime);
2387
1279
  await checkArchiveNaming(checks);
2388
1280
  const decisionRecordsConfigValid = await checkConfigSemantics(checks, reportRuntime);
@@ -2394,7 +1286,6 @@ async function runDoctor(options = {}) {
2394
1286
  const result = {
2395
1287
  command: "doctor",
2396
1288
  ok: checks.every((check) => check.status !== "issue"),
2397
- fixed,
2398
1289
  checks,
2399
1290
  runtime,
2400
1291
  summary: {
@@ -2403,32 +1294,52 @@ async function runDoctor(options = {}) {
2403
1294
  };
2404
1295
  return result;
2405
1296
  }
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) {
2410
- checks.push({
2411
- 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"
2415
- });
2416
- } else if (canonical.exists) {
2417
- checks.push({ status: "ok", label: "rsp-rules.md exists" });
2418
- } else if (!obsoleteExists) {
1297
+ async function reportProjectRuntime(checks) {
1298
+ let sourceMode = false;
1299
+ try {
1300
+ const selected = await readProjectRuntime();
1301
+ sourceMode = selected.mode === "source";
1302
+ const running = await getVersion();
2419
1303
  checks.push({
2420
- status: "issue",
2421
- label: "rsp-rules.md exists",
2422
- hint: existsSync2(RSP_DIR) ? "Run: rsp update" : "Run: rsp init"
1304
+ status: selected.mode === "source" || selected.version === running ? "ok" : "issue",
1305
+ code: "project_cli_version",
1306
+ label: "project CLI runtime",
1307
+ 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.",
1308
+ 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
1309
  });
1310
+ } catch (error) {
1311
+ checks.push({ status: "issue", code: "project_cli_version", label: "project CLI runtime", message: toErrorMessage(error) });
2424
1312
  }
2425
- if (obsoleteExists) {
1313
+ try {
1314
+ const inventory = await inspectPackagedSkillInventory();
1315
+ const drift = [
1316
+ ...inventory.skills.filter((skill) => skill.status !== "unchanged").map((skill) => `${skill.name}: ${skill.status}`),
1317
+ ...inventory.retired.map((name) => `${name}: retired`)
1318
+ ];
2426
1319
  checks.push({
2427
- status: "issue",
2428
- label: "obsolete fallback protocol path detected",
2429
- message: "obsolete fallback protocol path detected",
2430
- hint: "Run: rsp update"
1320
+ status: drift.length === 0 ? "ok" : "issue",
1321
+ code: "project_skill_suite",
1322
+ label: "project Skill suite matches running package",
1323
+ message: drift.length > 0 ? drift.join(", ") : void 0,
1324
+ hint: drift.length > 0 ? "Preserve needed custom content in managed Skills, then run the selected exact CLI: update." : void 0
2431
1325
  });
1326
+ } catch (error) {
1327
+ checks.push({ status: "issue", code: "project_skill_suite", label: "project Skill suite is inspectable", message: toErrorMessage(error) });
1328
+ }
1329
+ return sourceMode;
1330
+ }
1331
+ function reportRetiredProtocol(checks) {
1332
+ for (const path of [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH]) {
1333
+ const inspection = inspectManagedFile(path, "retired Rules file", { allowMissing: true });
1334
+ if (inspection.issue || inspection.exists) {
1335
+ checks.push({
1336
+ status: "issue",
1337
+ code: "retired_rules",
1338
+ label: `retired Rules path: ${path}`,
1339
+ message: inspection.issue?.message ?? "Rules are no longer read or generated.",
1340
+ hint: "Preserve needed custom content, then run update. Symlinks and special files require owner review."
1341
+ });
1342
+ }
2432
1343
  }
2433
1344
  }
2434
1345
  async function reportUnsupportedRules(checks, reportRuntime) {
@@ -2468,7 +1379,7 @@ async function checkAgents(checks, reportRuntime) {
2468
1379
  status: "issue",
2469
1380
  label: "AGENTS.md is a regular managed file",
2470
1381
  message: inspection.issue.message,
2471
- hint: "Replace the unsupported entry with a project-local regular file, then run: rsp update"
1382
+ hint: "Check AGENTS.md path type and permissions; preserve user content before correcting the entry. Then run: rsp update"
2472
1383
  });
2473
1384
  return;
2474
1385
  }
@@ -2478,7 +1389,7 @@ async function checkAgents(checks, reportRuntime) {
2478
1389
  }
2479
1390
  let agents;
2480
1391
  try {
2481
- agents = await readFile5("AGENTS.md", "utf-8");
1392
+ agents = await readFile4("AGENTS.md", "utf-8");
2482
1393
  } catch (error) {
2483
1394
  reportRuntime({
2484
1395
  code: "agents_read_failed",
@@ -2486,7 +1397,7 @@ async function checkAgents(checks, reportRuntime) {
2486
1397
  path: "AGENTS.md",
2487
1398
  message: toErrorMessage(error)
2488
1399
  });
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" });
1400
+ 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
1401
  return;
2491
1402
  }
2492
1403
  if (hasRspAgentsBlock(agents)) {
@@ -2537,7 +1448,7 @@ async function checkSpecsTree(checks, reportRuntime) {
2537
1448
  }
2538
1449
  }
2539
1450
  async function checkArchiveNaming(checks) {
2540
- const archivesDir = join4(RSP_DIR, "archives");
1451
+ const archivesDir = join3(RSP_DIR, "archives");
2541
1452
  const inspection = await inspectArchiveTree({ archivesDir });
2542
1453
  if (inspection.diagnostics.length > 0) {
2543
1454
  checks.push({
@@ -2549,7 +1460,7 @@ async function checkArchiveNaming(checks) {
2549
1460
  return;
2550
1461
  }
2551
1462
  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);
1463
+ const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(relative2(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename(fp)) === false);
2553
1464
  if (invalidFiles.length === 0) {
2554
1465
  checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
2555
1466
  return;
@@ -2558,7 +1469,7 @@ async function checkArchiveNaming(checks) {
2558
1469
  status: "issue",
2559
1470
  label: "archived change files follow the date_name.md convention",
2560
1471
  message: `archived change files with invalid names: ${invalidFiles.join(", ")}`,
2561
- hint: "Rename archive files to YYYY-MM-DD_name.md and run: rsp update"
1472
+ hint: "Rename archive files to YYYY-MM-DD_name.md, then rerun rsp doctor"
2562
1473
  });
2563
1474
  }
2564
1475
  async function checkConfigSemantics(checks, reportRuntime) {
@@ -2605,7 +1516,7 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
2605
1516
  status: "issue",
2606
1517
  label: `${decisionRecordsPath} is the authoritative Decision Record directory`,
2607
1518
  message: filesystemError,
2608
- hint: "Choose a project-relative directory that does not escape through a symlink."
1519
+ hint: "Correct decisions.path or its filesystem entry; it must resolve to a project-local directory. Preserve existing user content."
2609
1520
  });
2610
1521
  return;
2611
1522
  }
@@ -2616,10 +1527,12 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
2616
1527
  checks,
2617
1528
  `${decisionRecordsPath} is the authoritative Decision Record directory`,
2618
1529
  info.isDirectory(),
2619
- "Run: rsp update"
1530
+ "Correct the path type; init/update do not replace existing user content."
2620
1531
  );
2621
1532
  } catch (error) {
2622
- if (error.code !== "ENOENT") {
1533
+ const code = error.code;
1534
+ const missing = code === "ENOENT";
1535
+ if (!missing) {
2623
1536
  reportRuntime({
2624
1537
  code: "decision_records_path_stat_failed",
2625
1538
  operation: "stat",
@@ -2630,14 +1543,14 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
2630
1543
  checks.push({
2631
1544
  status: "issue",
2632
1545
  label: `${decisionRecordsPath} is the authoritative Decision Record directory`,
2633
- message: `${decisionRecordsPath} Decision Record directory is missing or unreadable`,
2634
- hint: "Run: rsp update"
1546
+ message: missing ? `${decisionRecordsPath} Decision Record directory is missing` : `Unable to inspect Decision Record directory ${decisionRecordsPath}: ${toErrorMessage(error)}`,
1547
+ 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
1548
  });
2636
1549
  }
2637
1550
  }
2638
1551
  async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
2639
1552
  const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
2640
- if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync2(DEFAULT_DECISION_RECORDS_PATH))
1553
+ if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync(DEFAULT_DECISION_RECORDS_PATH))
2641
1554
  return;
2642
1555
  let inspectionFailed = false;
2643
1556
  const inactiveRecords = (await walkMarkdownFiles(DEFAULT_DECISION_RECORDS_PATH, {
@@ -2645,7 +1558,7 @@ async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
2645
1558
  inspectionFailed = true;
2646
1559
  reportRuntime(diagnostic);
2647
1560
  }
2648
- })).map((path) => normalizeLogicalPath(relative4(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
1561
+ })).map((path) => normalizeLogicalPath(relative2(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
2649
1562
  if (inspectionFailed) {
2650
1563
  checks.push({
2651
1564
  status: "issue",
@@ -2899,7 +1812,7 @@ function emptyCollectionSummary() {
2899
1812
  }
2900
1813
 
2901
1814
  // src/commands/ready.ts
2902
- import { readFile as readFile6 } from "fs/promises";
1815
+ import { readFile as readFile5 } from "fs/promises";
2903
1816
  function readyError(name, error, kind) {
2904
1817
  return { command: "ready", ok: false, change: name || null, path: null, warnings: [], runtime: [], error, kind };
2905
1818
  }
@@ -2919,7 +1832,7 @@ async function showReady(name) {
2919
1832
  const runtime = [];
2920
1833
  let content;
2921
1834
  try {
2922
- content = await readFile6(srcPath, "utf-8");
1835
+ content = await readFile5(srcPath, "utf-8");
2923
1836
  } catch {
2924
1837
  return readyError(name, { code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, "read");
2925
1838
  }
@@ -2937,9 +1850,10 @@ async function showReady(name) {
2937
1850
  return readyError(name, { code: "invalid_config", message: `.rsp/config.yaml could not be parsed: ${toErrorMessage(error)}` }, "config");
2938
1851
  }
2939
1852
  const dependencyInspection = await inspectChangeDependencies();
2940
- const readinessDetails = collectArchiveReadiness(content, {
1853
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
1854
+ const readinessDetails = collectArchiveReadiness(document, {
2941
1855
  activeBlockers: dependencyInspection.activeBlockers.get(name),
2942
- documentDiagnostics: inspectChangeDocument(content, {
1856
+ documentDiagnostics: inspectChangeDocument(document, {
2943
1857
  name,
2944
1858
  validKinds: resolveKinds(configInspection.config),
2945
1859
  requiredSections: resolveRequiredSections(configInspection.config)
@@ -2962,7 +1876,7 @@ async function showReady(name) {
2962
1876
  }
2963
1877
 
2964
1878
  // src/commands/show.ts
2965
- import { readFile as readFile7 } from "fs/promises";
1879
+ import { readFile as readFile6 } from "fs/promises";
2966
1880
  function showError(error, runtime = []) {
2967
1881
  const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
2968
1882
  return { command: "show", ok: false, change: null, contextPaths: [], runtime, nextActions, error };
@@ -3006,14 +1920,15 @@ async function showChange(nameOrFocused, options = {}) {
3006
1920
  const srcPath = workRef.path;
3007
1921
  let content;
3008
1922
  try {
3009
- content = await readFile7(srcPath, "utf-8");
1923
+ content = await readFile6(srcPath, "utf-8");
3010
1924
  } catch {
3011
1925
  return showError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, runtime);
3012
1926
  }
3013
1927
  let kind = "\u2014";
1928
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
3014
1929
  let issues = [];
3015
1930
  try {
3016
- const fm = parseFrontmatter(content);
1931
+ const fm = parseFrontmatter(document.markdown);
3017
1932
  kind = fm?.kind ? String(fm.kind) : "\u2014";
3018
1933
  issues = parseIssueRelationships(fm);
3019
1934
  } catch (error) {
@@ -3034,7 +1949,7 @@ async function showChange(nameOrFocused, options = {}) {
3034
1949
  const markerPath = focusMarkers.get(name);
3035
1950
  let markerContent;
3036
1951
  try {
3037
- markerContent = await readFile7(markerPath);
1952
+ markerContent = await readFile6(markerPath);
3038
1953
  } catch (error) {
3039
1954
  reportRuntime({
3040
1955
  code: "focus_marker_read_failed",
@@ -3060,11 +1975,10 @@ async function showChange(nameOrFocused, options = {}) {
3060
1975
  recovery = capsule.recovery;
3061
1976
  }
3062
1977
  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"));
1978
+ const blockers = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(document);
1979
+ const scenarios = parseScenarios(document.markdown);
1980
+ const readinessDetails = collectArchiveReadiness(document, { activeBlockers: blockers });
1981
+ const taskCheckboxes = countCheckboxes(getDocumentSectionMarkdown(document, "tasks"));
3068
1982
  const progress = {
3069
1983
  done: taskCheckboxes.done + readinessDetails.verifyCriticality.required.done,
3070
1984
  total: taskCheckboxes.total + readinessDetails.verifyCriticality.required.total
@@ -3221,13 +2135,13 @@ function publicQuery(query) {
3221
2135
  }
3222
2136
 
3223
2137
  // src/status/derive.ts
3224
- function deriveStatusView(snapshot2, options = {}) {
2138
+ function deriveStatusView(snapshot, options = {}) {
3225
2139
  const query = {
3226
2140
  focused: Boolean(options.focused),
3227
2141
  blocked: Boolean(options.blocked),
3228
2142
  stale: typeof options.stale === "number" ? options.stale : null
3229
2143
  };
3230
- const records = snapshot2.records.filter(({ output: record }) => {
2144
+ const records = snapshot.records.filter(({ output: record }) => {
3231
2145
  if (query.focused && !record.isFocused)
3232
2146
  return false;
3233
2147
  if (query.blocked && !record.isBlocked)
@@ -3237,26 +2151,26 @@ function deriveStatusView(snapshot2, options = {}) {
3237
2151
  return true;
3238
2152
  });
3239
2153
  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"];
2154
+ const ok = snapshot.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
2155
+ const nextActions = ok ? buildStatusNextActions(snapshot.focused.length, snapshot.records.map((record) => record.output), snapshot.groups, filtered) : ["Run: rsp doctor"];
3242
2156
  return {
3243
- manage: snapshot2.manage,
3244
- language: snapshot2.language,
2157
+ workflow: snapshot.workflow,
2158
+ language: snapshot.language,
3245
2159
  query,
3246
- focused: snapshot2.focused,
2160
+ focused: snapshot.focused,
3247
2161
  records,
3248
- groups: snapshot2.groups,
3249
- plan: filterDependencyPlan(snapshot2.plan, new Set(records.map((record) => record.output.name))),
2162
+ groups: snapshot.groups,
2163
+ plan: filterDependencyPlan(snapshot.plan, new Set(records.map((record) => record.output.name))),
3250
2164
  summary: {
3251
2165
  total: records.length,
3252
2166
  focused: records.filter((record) => record.output.isFocused).length,
3253
2167
  blocked: records.filter((record) => record.output.isBlocked).length
3254
2168
  },
3255
2169
  nextActions,
3256
- archiveTrend: snapshot2.archiveTrend,
3257
- diagnostics: snapshot2.diagnostics,
3258
- runtime: snapshot2.runtime,
3259
- hasExecutableChanges: snapshot2.records.length > 0,
2170
+ archiveTrend: snapshot.archiveTrend,
2171
+ diagnostics: snapshot.diagnostics,
2172
+ runtime: snapshot.runtime,
2173
+ hasExecutableChanges: snapshot.records.length > 0,
3260
2174
  ok
3261
2175
  };
3262
2176
  }
@@ -3304,13 +2218,13 @@ function recommendOpenChange(records, groups) {
3304
2218
  }
3305
2219
  function filterDependencyPlan(plan, names) {
3306
2220
  const included = new Set(names);
3307
- let changed3 = true;
3308
- while (changed3) {
3309
- changed3 = false;
2221
+ let changed = true;
2222
+ while (changed) {
2223
+ changed = false;
3310
2224
  for (const edge of plan.edges) {
3311
2225
  if (included.has(edge.change) && !included.has(edge.requires)) {
3312
2226
  included.add(edge.requires);
3313
- changed3 = true;
2227
+ changed = true;
3314
2228
  }
3315
2229
  }
3316
2230
  }
@@ -3326,46 +2240,49 @@ function filterDependencyPlan(plan, names) {
3326
2240
 
3327
2241
  // src/commands/status.ts
3328
2242
  async function showStatus(options = {}) {
3329
- const snapshot2 = await inspectProjectStatus();
3330
- return deriveStatusView(snapshot2, options);
2243
+ const snapshot = await inspectProjectStatus();
2244
+ return deriveStatusView(snapshot, options);
3331
2245
  }
3332
2246
 
3333
2247
  // src/status/v3-json.ts
3334
- function toStatusJson(view) {
3335
- return {
2248
+ function hasActiveFilters(query) {
2249
+ return query.focused || query.blocked || query.stale !== null;
2250
+ }
2251
+ function toStatusJson(view, options = {}) {
2252
+ const output = {
3336
2253
  command: "status",
3337
2254
  ok: view.ok,
3338
- manage: view.manage,
3339
- language: view.language,
3340
- filters: view.query,
3341
2255
  focused: view.focused,
3342
2256
  records: view.records.map((record) => record.output),
3343
2257
  groups: view.groups,
3344
- plan: view.plan,
2258
+ plan: {
2259
+ nodes: view.plan.nodes,
2260
+ edges: view.plan.edges,
2261
+ blocked: view.plan.blocked,
2262
+ waves: view.plan.waves
2263
+ },
3345
2264
  summary: view.summary,
3346
- nextActions: view.nextActions,
3347
- archiveTrend: view.archiveTrend,
3348
- diagnostics: view.diagnostics,
3349
- runtime: view.runtime
2265
+ diagnostics: view.diagnostics
3350
2266
  };
2267
+ if (options.verbose || hasActiveFilters(view.query)) {
2268
+ output.filters = view.query;
2269
+ }
2270
+ if (options.verbose) {
2271
+ output.nextActions = view.nextActions;
2272
+ output.archiveTrend = view.archiveTrend;
2273
+ output.runtime = view.runtime;
2274
+ }
2275
+ return output;
3351
2276
  }
3352
2277
  function toStatusJsonError(error, options) {
3353
- return {
2278
+ const output = {
3354
2279
  command: "status",
3355
2280
  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
2281
  focused: [],
3364
2282
  records: [],
3365
2283
  groups: [],
3366
2284
  plan: {
3367
2285
  nodes: [],
3368
- ready: [],
3369
2286
  edges: [],
3370
2287
  blocked: [],
3371
2288
  waves: []
@@ -3375,12 +2292,22 @@ function toStatusJsonError(error, options) {
3375
2292
  focused: 0,
3376
2293
  blocked: 0
3377
2294
  },
3378
- archiveTrend: [],
3379
- nextActions: [],
3380
2295
  diagnostics: [],
3381
- runtime: [],
3382
2296
  error
3383
2297
  };
2298
+ if (options.verbose || options.focused || options.blocked) {
2299
+ output.filters = {
2300
+ focused: options.focused,
2301
+ blocked: options.blocked,
2302
+ stale: null
2303
+ };
2304
+ }
2305
+ if (options.verbose) {
2306
+ output.archiveTrend = [];
2307
+ output.nextActions = [];
2308
+ output.runtime = [];
2309
+ }
2310
+ return output;
3384
2311
  }
3385
2312
 
3386
2313
  // src/cli/adapter.ts
@@ -3393,9 +2320,6 @@ async function executeCliCommand(adapter, args) {
3393
2320
  return result;
3394
2321
  }
3395
2322
 
3396
- // src/cli/presenters/inspection.ts
3397
- import { existsSync as existsSync3 } from "fs";
3398
-
3399
2323
  // src/status/plain.ts
3400
2324
  var COL_CHANGE = 32;
3401
2325
  var COL_KIND = 10;
@@ -3409,10 +2333,6 @@ function printStatusPlain(view, options = {}) {
3409
2333
  console.log();
3410
2334
  console.log(` ${pc.bold("RSP status")}`);
3411
2335
  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
2336
  if (options.verbose)
3417
2337
  console.log();
3418
2338
  for (const diagnostic of view.diagnostics) {
@@ -3542,6 +2462,25 @@ function printArchiveTrend(trend) {
3542
2462
  }
3543
2463
 
3544
2464
  // src/cli/presenters/inspection.ts
2465
+ function presentConfig(result, options) {
2466
+ if (options.json) {
2467
+ emitJson(result.ok ? result.summary : result, options);
2468
+ return;
2469
+ }
2470
+ if (!result.ok) {
2471
+ console.error(` ${pc.red("Error:")} ${result.diagnostics.map((diagnostic) => diagnostic.message).join("; ")}`);
2472
+ return;
2473
+ }
2474
+ console.log();
2475
+ console.log(` ${pc.bold("RSP config")}`);
2476
+ console.log(` ${pc.dim("Path:")} ${result.path}`);
2477
+ console.log(` ${pc.dim("Valid:")} ${pc.green("yes")}`);
2478
+ console.log(` ${pc.dim("Kinds:")} ${result.summary.kinds.join(", ")}`);
2479
+ console.log(` ${pc.dim("Decision Records:")} ${result.summary.decisions.path}`);
2480
+ console.log(` ${pc.dim("Workflow:")} delegation ${result.summary.workflow.delegation} \xB7 closeout ${result.summary.workflow.closeout}`);
2481
+ console.log(` ${pc.dim("Language:")} artifacts ${result.summary.language.artifacts ?? "unset"} \xB7 commit ${result.summary.language.commit ?? "unset"}`);
2482
+ console.log();
2483
+ }
3545
2484
  function presentStatus(result, options) {
3546
2485
  if (result.kind === "error") {
3547
2486
  if (options.json)
@@ -3553,7 +2492,7 @@ function presentStatus(result, options) {
3553
2492
  if (options.verbose && !options.json)
3554
2493
  printStatusRuntimeDiagnostics(result.view.runtime);
3555
2494
  if (options.json)
3556
- emitJson(toStatusJson(result.view), options);
2495
+ emitJson(toStatusJson(result.view, { verbose: options.verbose }), options);
3557
2496
  else
3558
2497
  printStatusPlain(result.view, { verbose: options.verbose });
3559
2498
  }
@@ -3694,13 +2633,6 @@ function presentDoctor(result, options) {
3694
2633
  console.log();
3695
2634
  console.log(` ${pc.bold("RSP doctor")}`);
3696
2635
  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
2636
  for (const check of result.checks)
3705
2637
  printDoctorCheck(check);
3706
2638
  if (result.summary.issues === 0)
@@ -3918,6 +2850,22 @@ function formatArchiveReady(value) {
3918
2850
  }
3919
2851
 
3920
2852
  // src/cli/commands/inspection.ts
2853
+ var configCommand = defineCommand({
2854
+ meta: {
2855
+ name: "config",
2856
+ description: "Inspect effective project configuration without scanning work state"
2857
+ },
2858
+ args: {
2859
+ ...compactJsonArgs
2860
+ },
2861
+ async run({ args }) {
2862
+ await executeCliCommand({
2863
+ execute: () => showConfig(),
2864
+ present: (result) => presentConfig(result, args),
2865
+ exitCode: (result) => result.ok ? void 0 : 1
2866
+ }, args);
2867
+ }
2868
+ });
3921
2869
  var specsCommand = defineCommand({
3922
2870
  meta: {
3923
2871
  name: "specs",
@@ -3980,7 +2928,7 @@ var statusCommand = defineCommand({
3980
2928
  ...compactJsonArgs,
3981
2929
  verbose: {
3982
2930
  type: "boolean",
3983
- description: "Print runtime diagnostics for suppressed I/O issues",
2931
+ description: "Show auxiliary status details and runtime diagnostics",
3984
2932
  default: false
3985
2933
  }
3986
2934
  },
@@ -3993,7 +2941,8 @@ var statusCommand = defineCommand({
3993
2941
  kind: "error",
3994
2942
  output: toStatusJsonError({ code: "invalid_stale_filter", message: "--stale must be a non-negative integer number of days" }, {
3995
2943
  focused: Boolean(args.focused),
3996
- blocked: Boolean(args.blocked)
2944
+ blocked: Boolean(args.blocked),
2945
+ verbose: Boolean(args.verbose)
3997
2946
  })
3998
2947
  };
3999
2948
  }
@@ -4037,20 +2986,22 @@ var doctorCommand = defineCommand({
4037
2986
  },
4038
2987
  args: {
4039
2988
  ...compactJsonArgs,
4040
- verbose: {
2989
+ "verbose": {
4041
2990
  type: "boolean",
4042
2991
  description: "Print runtime diagnostics for suppressed I/O issues",
4043
2992
  default: false
4044
2993
  },
4045
- fix: {
2994
+ "check-updates": {
4046
2995
  type: "boolean",
4047
- description: "Run safe deterministic repairs before reporting diagnostics",
2996
+ description: "Explicitly check stable registry metadata without upgrading; timeout is non-blocking",
4048
2997
  default: false
4049
2998
  }
4050
2999
  },
4051
- async run({ args }) {
3000
+ async run({ args, rawArgs }) {
3001
+ if (rawArgs.some((arg) => arg === "--fix" || arg.startsWith("--fix=") || arg === "--no-fix"))
3002
+ throw new Error("doctor is read-only; use update to reconcile managed assets");
4052
3003
  await executeCliCommand({
4053
- execute: () => runDoctor({ fix: Boolean(args.fix) }),
3004
+ execute: () => runDoctor({ checkUpdates: Boolean(args["check-updates"]) }),
4054
3005
  present: (result) => presentDoctor(result, args),
4055
3006
  exitCode: (result) => result.ok ? void 0 : 1
4056
3007
  }, args);
@@ -4166,24 +3117,87 @@ var historyCommand = defineCommand({
4166
3117
  exitCode: (result) => result.ok ? void 0 : 1
4167
3118
  }, args);
4168
3119
  }
4169
- });
4170
- var inspectionCommands = {
4171
- specs: specsCommand,
4172
- ready: readyCommand,
4173
- show: showCommand,
4174
- history: historyCommand,
4175
- status: statusCommand,
4176
- check: checkCommand,
4177
- doctor: doctorCommand
4178
- };
4179
-
4180
- // src/cli/commands/setup.ts
4181
- import { defineCommand as defineCommand2 } from "citty";
3120
+ });
3121
+ var inspectionCommands = {
3122
+ config: configCommand,
3123
+ specs: specsCommand,
3124
+ ready: readyCommand,
3125
+ show: showCommand,
3126
+ history: historyCommand,
3127
+ status: statusCommand,
3128
+ check: checkCommand,
3129
+ doctor: doctorCommand
3130
+ };
3131
+
3132
+ // src/cli/commands/setup.ts
3133
+ import { defineCommand as defineCommand2 } from "citty";
3134
+
3135
+ // src/commands/add-spec.ts
3136
+ import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
3137
+ import { join as join4 } from "path";
3138
+
3139
+ // src/core/lock.ts
3140
+ import { mkdir as mkdir2, readFile as readFile7, unlink, writeFile } from "fs/promises";
3141
+ async function withRspLock(operation, fn) {
3142
+ requireManagedDirectory(RSP_DIR, "RSP root", { allowMissing: true });
3143
+ await mkdir2(RSP_DIR, { recursive: true });
3144
+ try {
3145
+ await writeFile(LOCK_PATH, `${process.pid}
3146
+ ${operation}
3147
+ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
3148
+ } catch (err) {
3149
+ if (err.code === "EEXIST")
3150
+ await handleStaleLock(operation);
3151
+ else
3152
+ throw err;
3153
+ }
3154
+ try {
3155
+ return await fn();
3156
+ } finally {
3157
+ try {
3158
+ await unlink(LOCK_PATH);
3159
+ } catch {
3160
+ }
3161
+ }
3162
+ }
3163
+ async function handleStaleLock(operation) {
3164
+ try {
3165
+ const content = await readFile7(LOCK_PATH, "utf-8");
3166
+ const lines = content.trim().split("\n");
3167
+ const pid = Number(lines[0]);
3168
+ if (pid && Number.isFinite(pid)) {
3169
+ try {
3170
+ process.kill(pid, 0);
3171
+ const op = lines[1] || "unknown";
3172
+ const ts = lines[2] || "unknown";
3173
+ throw new Error(`RSP locked by pid ${pid} (${op}) since ${ts}`);
3174
+ } catch (e) {
3175
+ if (e instanceof Error && e.message.startsWith("RSP locked"))
3176
+ throw e;
3177
+ const code = e.code;
3178
+ if (code === "ESRCH") {
3179
+ await unlink(LOCK_PATH);
3180
+ await writeFile(LOCK_PATH, `${process.pid}
3181
+ ${operation}
3182
+ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
3183
+ return;
3184
+ }
3185
+ throw e;
3186
+ }
3187
+ } else {
3188
+ await unlink(LOCK_PATH);
3189
+ await writeFile(LOCK_PATH, `${process.pid}
3190
+ ${operation}
3191
+ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
3192
+ }
3193
+ } catch (e) {
3194
+ if (e instanceof Error && e.message.startsWith("RSP locked"))
3195
+ throw e;
3196
+ throw new Error(`RSP is locked. Remove ${LOCK_PATH} manually if no other process is running.`);
3197
+ }
3198
+ }
4182
3199
 
4183
3200
  // 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
3201
  async function addSpec(name, projectName = "project") {
4188
3202
  if (!name || !isValidSpecName(name)) {
4189
3203
  return { ok: false, message: "spec name must be kebab-case with optional subdirectory" };
@@ -4194,13 +3208,9 @@ async function addSpec(name, projectName = "project") {
4194
3208
  if (["index", "00-index"].includes(name.split("/").at(-1) || "")) {
4195
3209
  return { ok: false, message: 'spec name "index" is reserved for Specs migration compatibility' };
4196
3210
  }
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
- }
3211
+ const initialization = inspectRspInitialization();
3212
+ if (initialization)
3213
+ return { ok: false, message: initialization };
4204
3214
  try {
4205
3215
  const configInspection = await inspectRspConfig();
4206
3216
  if (configInspection.issues.length > 0) {
@@ -4210,14 +3220,14 @@ async function addSpec(name, projectName = "project") {
4210
3220
  return { ok: false, message: `.rsp/config.yaml could not be parsed: ${toErrorMessage(error)}` };
4211
3221
  }
4212
3222
  return withRspLock("add-spec", async () => {
4213
- const specsDir = join5(RSP_DIR, "specs");
3223
+ const specsDir = join4(RSP_DIR, "specs");
4214
3224
  const segments = name.split("/");
4215
3225
  const parent = resolveManagedDirectoryChain(specsDir, segments.slice(0, -1), "spec path");
4216
- const path = join5(parent, `${segments.at(-1)}.md`);
3226
+ const path = join4(parent, `${segments.at(-1)}.md`);
4217
3227
  const inspection = await inspectManagedFileTree(specsDir, "Specs");
4218
3228
  if (inspection.issues.length > 0)
4219
3229
  throw inspection.issues[0];
4220
- await mkdir4(parent, { recursive: true });
3230
+ await mkdir3(parent, { recursive: true });
4221
3231
  const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
4222
3232
  const content = name === "design" ? generateDesignContent(resolvedProjectName) : generateSpecContent(name);
4223
3233
  try {
@@ -4232,114 +3242,245 @@ async function addSpec(name, projectName = "project") {
4232
3242
  }
4233
3243
 
4234
3244
  // src/commands/init.ts
4235
- import { existsSync as existsSync5 } from "fs";
4236
- import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
3245
+ import { existsSync as existsSync2 } from "fs";
3246
+ import { mkdir as mkdir4, readdir as readdir3, readFile as readFile9 } from "fs/promises";
4237
3247
  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;
3248
+
3249
+ // src/core/setup-files.ts
3250
+ import { constants } from "fs";
3251
+ import { access, chmod as chmod2, link, mkdtemp as mkdtemp2, readFile as readFile8, rename as rename2, rm as rm2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
3252
+ import { dirname as dirname2, join as join5 } from "path";
3253
+ async function updateSetupFile(path, render) {
3254
+ const exists = requireManagedFile(path, "setup file", { allowMissing: true });
3255
+ const current = exists ? await readFile8(path, "utf8") : null;
3256
+ const next = render(current);
3257
+ if (next === current)
3258
+ return false;
3259
+ if (exists)
3260
+ await access(path, constants.W_OK);
3261
+ const temporary = await mkdtemp2(join5(dirname2(path), ".rsp-write-"));
3262
+ const prepared = join5(temporary, "next");
3263
+ let failure2;
3264
+ try {
3265
+ await writeFile3(prepared, next, { flag: "wx" });
3266
+ if (exists)
3267
+ await chmod2(prepared, (await stat2(path)).mode & 511);
3268
+ const stillExists = requireManagedFile(path, "setup file", { allowMissing: true });
3269
+ if (stillExists !== exists || stillExists && await readFile8(path, "utf8") !== current)
3270
+ throw new Error(`Setup file changed during update: ${path}`);
3271
+ if (exists)
3272
+ await rename2(prepared, path);
3273
+ else
3274
+ await link(prepared, path);
3275
+ } catch (error) {
3276
+ failure2 = error instanceof Error ? error : new Error(String(error));
3277
+ }
3278
+ try {
3279
+ await rm2(temporary, { recursive: true });
3280
+ } catch (error) {
3281
+ failure2 = new Error(`Temporary content cleanup failed: ${temporary}`, { cause: failure2 ?? error });
3282
+ }
3283
+ if (failure2)
3284
+ throw failure2;
3285
+ return true;
4243
3286
  }
3287
+
3288
+ // src/commands/init.ts
4244
3289
  async function initProject(args = {}) {
4245
- const isNew = !existsSync5(RSP_DIR);
3290
+ const isNew = !existsSync2(RSP_DIR);
4246
3291
  const projectName = await detectProjectName();
4247
3292
  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;
3293
+ if (mode !== "managed" && mode !== "print")
3294
+ return { ok: false, message: "unsupported --agents-mode; use managed or print" };
4255
3295
  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);
3296
+ const runtime = await selectProjectRuntime();
3297
+ const { version } = runtime;
3298
+ const retiredRules = [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH].map((path) => requireManagedFile(path, "retired Rules", { allowMissing: true })).some(Boolean);
3299
+ if (retiredRules)
3300
+ return { ok: false, message: "Retired RSP assets require migration; use the selected target CLI: rsp update" };
3301
+ requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true });
3302
+ clearConfigCache();
3303
+ const inspection = await inspectRspConfig();
3304
+ if (inspection.issues.length)
3305
+ throw new Error(inspection.issues.join("; "));
3306
+ const decisionRecordsPath = resolveDecisionRecordsPath(inspection.config);
3307
+ const pathIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath);
3308
+ if (pathIssue)
3309
+ throw new Error(pathIssue);
3310
+ const dirs = [join6(RSP_DIR, "specs"), CHANGES_DIR, FOCUS_DIR, join6(RSP_DIR, "archives"), decisionRecordsPath];
3311
+ for (const path of dirs)
3312
+ resolveManagedDirectoryChain(process.cwd(), path.split("/"), "project directory");
3313
+ const completed = [];
3314
+ async function ensureFile(path, content) {
3315
+ if (await updateSetupFile(path, (current) => current ?? content))
3316
+ completed.push(`created ${path}`);
4268
3317
  }
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;
3318
+ try {
3319
+ const skills = await updatePackagedSkills();
3320
+ completed.push(...skills.installed.map((name) => `installed ${name}`), ...skills.replaced.map((name) => `updated ${name}`), ...skills.removed.map((name) => `removed ${name}`));
3321
+ for (const path of dirs) {
3322
+ if (!requireManagedDirectory(path, "project directory", { allowMissing: true })) {
3323
+ await mkdir4(path, { recursive: true });
3324
+ requireManagedDirectory(path, "project directory");
3325
+ completed.push(`created ${path}`);
3326
+ }
4288
3327
  }
4289
- } else {
4290
- created = await ensureFile(join6(RSP_DIR, "config.yaml"), generateConfigTemplate()) || created;
3328
+ await ensureFile(CONFIG_PATH, generateConfigTemplate());
3329
+ clearConfigCache();
3330
+ await ensureFile(join6(RSP_DIR, "specs/design.md"), generateDesignContent(projectName));
3331
+ if ((await readdir3(decisionRecordsPath)).length === 0)
3332
+ await ensureFile(join6(decisionRecordsPath, ".gitkeep"), "");
3333
+ await ensureFile(join6(CHANGES_DIR, ".gitkeep"), "");
3334
+ await ensureFile(join6(FOCUS_DIR, ".gitkeep"), "");
3335
+ await ensureFile(join6(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n");
3336
+ if (args.withProjectSetup) {
3337
+ const work = resolveWorkRef("project-setup", { executable: true });
3338
+ await ensureFile(work.path, generateChangeContent("project-setup"));
3339
+ await ensureFile(resolveFocusMarkerPath(work), "");
3340
+ }
3341
+ const title = projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
3342
+ const agentsUpdated = await updateSetupFile("AGENTS.md", (current) => upsertRspAgentsBlock(current ?? `# ${title}
3343
+
3344
+ `, runtime).content);
3345
+ if (agentsUpdated)
3346
+ completed.push("updated AGENTS.md managed block");
3347
+ if (runtime.mode === "package" && await updateSetupFile(PROJECT_VERSION_PATH, () => `${version}
3348
+ `))
3349
+ completed.push(`pinned ${version}`);
3350
+ return { ok: true, isNew, created: completed.length > 0, decisionRecordsPath, withProjectSetup: Boolean(args.withProjectSetup), agentsUpdated, skills, managedAgents: mode === "print" ? await readFile9("AGENTS.md", "utf8") : void 0 };
3351
+ } catch (error) {
3352
+ 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
3353
  }
3354
+ });
3355
+ }
3356
+
3357
+ // src/commands/update.ts
3358
+ import { existsSync as existsSync3 } from "fs";
3359
+ import { readFile as readFile12, unlink as unlink3 } from "fs/promises";
3360
+ import { join as join8 } from "path";
3361
+
3362
+ // src/commands/archive-index-migration.ts
3363
+ import { readFile as readFile10 } from "fs/promises";
3364
+ import { join as join7 } from "path";
3365
+ async function isLegacyArchiveIndex() {
3366
+ const path = join7(ARCHIVES_DIR, "INDEX.md");
3367
+ if (!requireManagedFile(path, "legacy archive index", { allowMissing: true }))
3368
+ return false;
3369
+ return isLegacyArchiveIndexContent(await readFile10(path, "utf-8"));
3370
+ }
3371
+ function isLegacyArchiveIndexContent(content) {
3372
+ try {
3373
+ const frontmatter = parseFrontmatter(content);
3374
+ return frontmatter?.kind === "generated-index" && frontmatter.index_type === "archives";
3375
+ } catch {
3376
+ return false;
3377
+ }
3378
+ }
3379
+
3380
+ // src/commands/specs-index-migration.ts
3381
+ import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
3382
+ import { dirname as dirname3, resolve } from "path";
3383
+ async function inspectGeneratedSpecsIndexMigration(cwd = process.cwd()) {
3384
+ const inspection = await inspectSpecs({ cwd, allowMissingDecisionRecords: true });
3385
+ return {
3386
+ inspectionComplete: specsInspectionComplete(inspection),
3387
+ removable: inspection.generatedIndexes.filter((index) => index.classification === "safe-removal").map((index) => index.path).sort(),
3388
+ ownerControlled: inspection.generatedIndexes.filter((index) => index.classification === "owner-controlled").map((index) => index.path).sort(),
3389
+ diagnostics: inspection.diagnostics.map((diagnostic) => `${diagnostic.path}: ${diagnostic.message}`)
3390
+ };
3391
+ }
3392
+ async function removeGeneratedSpecsIndex(path) {
3393
+ resolveManagedDirectoryChain(process.cwd(), dirname3(path).split("/"), "Specs parent");
3394
+ if (!requireManagedFile(path, "generated Specs index", { allowMissing: true }))
3395
+ return false;
3396
+ if (!isRecognizedGeneratedSpecsIndex(await readFile11(resolve(path), "utf8"), path))
3397
+ throw new Error(`Specs index is no longer generated; left untouched: ${path}`);
3398
+ await unlink2(path);
3399
+ return true;
3400
+ }
3401
+
3402
+ // src/commands/update.ts
3403
+ async function updateProject(options = {}) {
3404
+ if (!existsSync3(RSP_DIR))
3405
+ return { ok: false, actions: [], updated: false, events: [{ type: "not-initialized" }], migration: { inspectionComplete: false, manualMigrationRequired: false, residualEntries: [], diagnostics: [] } };
3406
+ const execute = async () => {
3407
+ const runtime = await selectProjectRuntime("update");
3408
+ const { version } = runtime;
4292
3409
  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}
3410
+ const config = await inspectRspConfig();
3411
+ if (config.issues.length)
3412
+ throw new Error(config.issues.join("; "));
3413
+ resolveManagedDirectoryChain(process.cwd(), [RSP_DIR, "rules"], "retired Rules parent");
3414
+ const retired = [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH].filter((path) => requireManagedFile(path, "retired Rules", { allowMissing: true }));
3415
+ const rules = await inspectUnsupportedRules();
3416
+ const migration = { inspectionComplete: rules.diagnostics.length === 0, manualMigrationRequired: rules.entries.length > 0, residualEntries: rules.entries.map((entry) => entry.path), diagnostics: rules.diagnostics };
3417
+ if (!migration.inspectionComplete)
3418
+ return { ok: false, actions: [], updated: false, events: [{ type: "migration-diagnostics", diagnostics: rules.diagnostics }], migration };
3419
+ requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true });
3420
+ resolveManagedDirectoryChain(process.cwd(), [RSP_DIR, "archives"], "archive parent");
3421
+ requireManagedFile(join8(RSP_DIR, "archives/INDEX.md"), "archive index", { allowMissing: true });
3422
+ const plan = await inspectGeneratedSpecsIndexMigration();
3423
+ if (!plan.inspectionComplete || plan.ownerControlled.length)
3424
+ throw new Error(`Generated Specs-index migration requires owner review before update: ${[...plan.ownerControlled, ...plan.diagnostics].join("; ")}`);
3425
+ const projectName = await detectProjectName();
3426
+ const agents = requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true }) ? await readFile12("AGENTS.md", "utf8") : null;
3427
+ const refreshAgents = (current) => upsertRspAgentsBlock(current ?? `# ${projectName}
4315
3428
 
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;
3429
+ `, runtime).content;
3430
+ const removeArchive = await isLegacyArchiveIndex();
3431
+ const actions = [];
3432
+ const events = [];
3433
+ if (existsSync3("CONTEXT-MAP.md"))
3434
+ events.push({ type: "legacy-context-map" });
3435
+ if (migration.residualEntries.length)
3436
+ events.push({ type: "residual-rules", entries: migration.residualEntries });
3437
+ const record = (message) => {
3438
+ actions.push(message);
3439
+ events.push({ type: "updated", message });
3440
+ };
3441
+ try {
3442
+ const skills = await updatePackagedSkills({ dryRun: options.dryRun });
3443
+ for (const name of skills.installed) record(`Skill installed: ${name}`);
3444
+ for (const name of skills.replaced) record(`Skill updated: ${name}`);
3445
+ for (const name of skills.removed) record(`retired Skill removed: ${name}`);
3446
+ if (options.dryRun) {
3447
+ actions.push(...retired.map((path) => `retired Rules file removed: ${path}`), ...plan.removable.map((path) => `generated Specs index removed: ${path}`));
3448
+ if (removeArchive)
3449
+ actions.push("legacy Archive Index removed");
3450
+ if (refreshAgents(agents) !== agents)
3451
+ actions.push("AGENTS.md managed block refreshed");
3452
+ if (runtime.mode === "package" && await readProjectVersion() !== version)
3453
+ actions.push(`project CLI pinned to ${version}`);
3454
+ return { ok: true, actions, updated: false, skills, events: [], migration };
3455
+ }
3456
+ for (const path of retired) {
3457
+ if (requireManagedFile(path, "retired Rules", { allowMissing: true })) {
3458
+ await unlink3(path);
3459
+ record(`retired Rules file removed: ${path}`);
3460
+ }
3461
+ }
3462
+ for (const path of plan.removable) {
3463
+ if (await removeGeneratedSpecsIndex(path))
3464
+ record(`generated Specs index removed: ${path}`);
4327
3465
  }
3466
+ if (await isLegacyArchiveIndex()) {
3467
+ await unlink3(join8(RSP_DIR, "archives/INDEX.md"));
3468
+ record("legacy Archive Index removed");
3469
+ }
3470
+ if (await updateSetupFile("AGENTS.md", refreshAgents))
3471
+ record("AGENTS.md managed block refreshed");
3472
+ if (runtime.mode === "package" && await updateSetupFile(PROJECT_VERSION_PATH, () => `${version}
3473
+ `))
3474
+ record(`project CLI pinned to ${version}`);
3475
+ return { ok: true, actions, updated: actions.length > 0, skills, events, migration };
3476
+ } catch (error) {
3477
+ 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
3478
  }
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
- });
3479
+ };
3480
+ return options.dryRun ? execute() : withRspLock("update", execute);
4339
3481
  }
4340
3482
 
4341
3483
  // 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
3484
  function presentAddSpec(result) {
4344
3485
  if (!result.ok) {
4345
3486
  console.error(` ${pc.red("Error:")} ${result.message}`);
@@ -4360,11 +3501,12 @@ ${result.managedAgents}
4360
3501
  if (result.isNew) {
4361
3502
  const createdProjectSetup = result.withProjectSetup ? "\n .rsp/changes/project-setup.md\n .rsp/focus.d/project-setup" : "";
4362
3503
  const createdAgents = result.agentsUpdated ? "\n AGENTS.md" : "";
4363
- const next = result.withProjectSetup ? "fill .rsp/changes/project-setup.md" : "rsp create project-setup";
3504
+ const next = result.withProjectSetup ? "fill .rsp/changes/project-setup.md" : "rsp status";
4364
3505
  console.log(`
4365
3506
  ${pc.green("RSP scaffolded.")}
4366
3507
  `);
4367
- console.log(` Created: .rsp/rsp-rules.md
3508
+ console.log(` Created: complete core Skill suite
3509
+ .rsp/version
4368
3510
  .rsp/specs/
4369
3511
  ${result.decisionRecordsPath}/
4370
3512
  .rsp/changes/
@@ -4388,7 +3530,13 @@ ${result.managedAgents}
4388
3530
  `);
4389
3531
  }
4390
3532
  }
4391
- function presentUpdate(result) {
3533
+ function presentUpdate(result, dryRun = false) {
3534
+ if (dryRun && result.ok) {
3535
+ console.log(" Update preview (no files changed):");
3536
+ for (const action of result.actions)
3537
+ console.log(` ${action}`);
3538
+ return;
3539
+ }
4392
3540
  if (!result.ok && result.events.some((event) => event.type === "not-initialized")) {
4393
3541
  console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
4394
3542
  console.error(` ${pc.dim("Run: rsp init")}`);
@@ -4410,10 +3558,9 @@ function presentUpdate(result) {
4410
3558
  console.log(` ${pc.dim("Move stable scoped instructions to the nearest project-owned AGENTS.md, then remove the old files.")}`);
4411
3559
  console.log(` ${pc.dim("Run: rsp doctor")}`);
4412
3560
  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}`);
3561
+ } else if (event.type === "legacy-context-map") {
3562
+ console.log(` ${pc.yellow("Note:")} CONTEXT-MAP.md remains; context migration needs owner review.`);
3563
+ 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
3564
  }
4418
3565
  }
4419
3566
  if (!result.migration.inspectionComplete)
@@ -4428,7 +3575,6 @@ function presentUpdate(result) {
4428
3575
  else
4429
3576
  console.log(` ${pc.green("Update complete.")}
4430
3577
  `);
4431
- console.log(SKILL_REFRESH_HINT);
4432
3578
  }
4433
3579
 
4434
3580
  // src/cli/commands/setup.ts
@@ -4492,14 +3638,19 @@ var addCommand = defineCommand2({
4492
3638
  var updateCommand = defineCommand2({
4493
3639
  meta: {
4494
3640
  name: "update",
4495
- description: "Refresh RSP-managed project files after upgrade; does not update packaged Skills"
3641
+ description: "Update the complete pinned environment and remove retired managed assets"
3642
+ },
3643
+ args: {
3644
+ "dry-run": { type: "boolean", description: "Preview the complete update without changing files", default: false }
4496
3645
  },
4497
- async run() {
3646
+ async run({ args, rawArgs }) {
3647
+ if (args._.length || rawArgs.some((arg) => !["--dry-run", "--dry-run=true", "--dry-run=false", "--no-dry-run"].includes(arg)))
3648
+ throw new Error("rsp update accepts only --dry-run; use the selected target CLI to update all managed assets");
4498
3649
  await executeCliCommand({
4499
- execute: () => updateProject(),
4500
- present: presentUpdate,
3650
+ execute: () => updateProject({ dryRun: Boolean(args["dry-run"]) }),
3651
+ present: (result) => presentUpdate(result, Boolean(args["dry-run"])),
4501
3652
  exitCode: (result) => result.migration.inspectionComplete ? void 0 : 1
4502
- }, {});
3653
+ }, args);
4503
3654
  }
4504
3655
  });
4505
3656
  var setupCommands = {
@@ -4510,49 +3661,26 @@ var setupCommands = {
4510
3661
 
4511
3662
  // src/cli/commands/skills.ts
4512
3663
  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);
3664
+
3665
+ // src/cli/presenters/skills.ts
3666
+ function presentPackagedSkillInventory(inventory, json = false) {
3667
+ if (json) {
3668
+ emitJson(inventory);
3669
+ return;
4546
3670
  }
4547
- });
3671
+ console.log(` ${inventory.package.name}@${inventory.package.version}`);
3672
+ console.log(` target: ${inventory.target}`);
3673
+ console.log(" Core suite (managed by init/update)");
3674
+ for (const skill of inventory.skills)
3675
+ console.log(` ${skill.name} ${skill.status}`);
3676
+ for (const name of inventory.retired)
3677
+ console.log(` ${name} retired`);
3678
+ }
3679
+
3680
+ // src/cli/commands/skills.ts
4548
3681
  var skillsListCommand = defineCommand3({
4549
- meta: {
4550
- name: "list",
4551
- description: "List package-bundled Skills and their project status"
4552
- },
4553
- args: {
4554
- ...jsonArgs
4555
- },
3682
+ meta: { name: "list", description: "Inspect the complete package-owned Skill inventory" },
3683
+ args: { ...jsonArgs },
4556
3684
  async run({ args }) {
4557
3685
  await executeCliCommand({
4558
3686
  execute: () => inspectPackagedSkillInventory(),
@@ -4560,32 +3688,25 @@ var skillsListCommand = defineCommand3({
4560
3688
  }, args);
4561
3689
  }
4562
3690
  });
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
3691
  var skillsCommands = {
4574
- skills: skillsCommand
3692
+ skills: defineCommand3({
3693
+ meta: { name: "skills", description: "Inspect Skills; use init or update to manage the complete environment" },
3694
+ subCommands: { list: skillsListCommand }
3695
+ })
4575
3696
  };
4576
3697
 
4577
3698
  // src/cli/commands/work.ts
4578
3699
  import { defineCommand as defineCommand4 } from "citty";
4579
3700
 
4580
3701
  // 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";
3702
+ import { existsSync as existsSync4, readdirSync } from "fs";
3703
+ import { mkdir as mkdir5, readFile as readFile13, rename as rename3, unlink as unlink4 } from "fs/promises";
3704
+ import { basename as basename2, join as join9 } from "path";
4584
3705
  async function archiveChange(name) {
4585
3706
  if (!name) {
4586
3707
  return { ok: false, kind: "usage", message: "rsp archive <name>" };
4587
3708
  }
4588
- const initialization = inspectInitialization();
3709
+ const initialization = inspectRspInitialization();
4589
3710
  if (initialization)
4590
3711
  return { ok: false, kind: "error", message: initialization };
4591
3712
  try {
@@ -4597,7 +3718,7 @@ async function archiveChange(name) {
4597
3718
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4598
3719
  const base = basename2(workRef.name);
4599
3720
  const archiveName = resolveArchiveName(archiveSubdir, date, base);
4600
- const content = await readFile9(srcPath, "utf-8");
3721
+ const content = await readFile13(srcPath, "utf-8");
4601
3722
  const configInspection = await inspectRspConfig();
4602
3723
  if (configInspection.issues.length > 0) {
4603
3724
  return {
@@ -4607,9 +3728,10 @@ async function archiveChange(name) {
4607
3728
  };
4608
3729
  }
4609
3730
  const dependencyInspection = await inspectChangeDependencies();
4610
- const readiness = collectArchiveReadiness(content, {
3731
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
3732
+ const readiness = collectArchiveReadiness(document, {
4611
3733
  activeBlockers: dependencyInspection.activeBlockers.get(workRef.name),
4612
- documentDiagnostics: inspectChangeDocument(content, {
3734
+ documentDiagnostics: inspectChangeDocument(document, {
4613
3735
  name: workRef.name,
4614
3736
  validKinds: resolveKinds(configInspection.config),
4615
3737
  requiredSections: resolveRequiredSections(configInspection.config)
@@ -4624,13 +3746,13 @@ async function archiveChange(name) {
4624
3746
  warnings: checklist
4625
3747
  };
4626
3748
  }
4627
- await mkdir6(archiveSubdir, { recursive: true });
4628
- await rename3(srcPath, join7(archiveSubdir, archiveName));
3749
+ await mkdir5(archiveSubdir, { recursive: true });
3750
+ await rename3(srcPath, join9(archiveSubdir, archiveName));
4629
3751
  const postArchiveWarnings = [];
4630
3752
  let focusCleared = false;
4631
- if (existsSync6(focusEntry)) {
3753
+ if (existsSync4(focusEntry)) {
4632
3754
  try {
4633
- await unlink5(focusEntry);
3755
+ await unlink4(focusEntry);
4634
3756
  await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
4635
3757
  focusCleared = true;
4636
3758
  } catch (error) {
@@ -4642,7 +3764,7 @@ async function archiveChange(name) {
4642
3764
  } catch (error) {
4643
3765
  postArchiveWarnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
4644
3766
  }
4645
- return { ok: true, archiveName, focusCleared, readinessWarnings: checklist, cleanupWarnings: postArchiveWarnings, git: existsSync6(".git") };
3767
+ return { ok: true, archiveName, focusCleared, readinessWarnings: checklist, cleanupWarnings: postArchiveWarnings, git: existsSync4(".git") };
4646
3768
  });
4647
3769
  } catch (error) {
4648
3770
  if (error instanceof WorkRefError) {
@@ -4651,15 +3773,6 @@ async function archiveChange(name) {
4651
3773
  throw error;
4652
3774
  }
4653
3775
  }
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
3776
  function resolveArchiveName(archiveSubdir, date, base) {
4664
3777
  const initialName = `${date}_${base}.md`;
4665
3778
  if (!archiveNameExists(archiveSubdir, initialName))
@@ -4677,7 +3790,7 @@ function resolveArchiveName(archiveSubdir, date, base) {
4677
3790
  );
4678
3791
  }
4679
3792
  function archiveNameExists(archiveSubdir, name) {
4680
- if (!existsSync6(archiveSubdir))
3793
+ if (!existsSync4(archiveSubdir))
4681
3794
  return false;
4682
3795
  return readdirSync(archiveSubdir).some((entry) => {
4683
3796
  if (entry.normalize("NFC") !== name)
@@ -4693,252 +3806,10 @@ function archiveNameExists(archiveSubdir, name) {
4693
3806
  });
4694
3807
  }
4695
3808
 
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
3809
  // 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";
3810
+ import { existsSync as existsSync5 } from "fs";
3811
+ import { mkdir as mkdir6, unlink as unlink5, writeFile as writeFile4 } from "fs/promises";
3812
+ import { dirname as dirname4 } from "path";
4942
3813
  async function createChange(name, summary = "", kind, options = {}) {
4943
3814
  if (!name) {
4944
3815
  return { ok: false, kind: "usage", message: "rsp create <name> [summary]" };
@@ -4948,31 +3819,31 @@ async function createChange(name, summary = "", kind, options = {}) {
4948
3819
  if (options.issueRelation && !hasIssueOption)
4949
3820
  throw new Error("--issue-relation requires --issue");
4950
3821
  const issues = options.issue !== void 0 ? [createIssueRelationship(options.issue, options.issueRelation)] : [];
4951
- const initialization = inspectInitialization2();
3822
+ const initialization = inspectRspInitialization();
4952
3823
  if (initialization)
4953
3824
  return { ok: false, kind: "error", message: initialization, warning };
4954
3825
  try {
4955
3826
  return await withRspLock("create-change", async () => {
4956
3827
  const workRef = await resolveExecutableChange(name);
4957
3828
  const changePath = workRef.path;
4958
- const existed = existsSync7(changePath);
3829
+ const existed = existsSync5(changePath);
4959
3830
  if (existed && hasIssueOption)
4960
3831
  throw new Error(`Change already exists; --issue cannot update ${workRef.name}`);
4961
3832
  if (!existed) {
4962
3833
  const focusEntry = resolveFocusMarkerPath(workRef);
4963
3834
  const content = generateChangeContent(workRef.name, summary, kind, { issues });
4964
- await mkdir7(dirname3(changePath), { recursive: true });
3835
+ await mkdir6(dirname4(changePath), { recursive: true });
4965
3836
  let changeCreated = false;
4966
3837
  try {
4967
- await writeFile3(changePath, content, { flag: "wx" });
3838
+ await writeFile4(changePath, content, { flag: "wx" });
4968
3839
  changeCreated = true;
4969
- await mkdir7(FOCUS_DIR, { recursive: true });
4970
- await mkdir7(dirname3(focusEntry), { recursive: true });
3840
+ await mkdir6(FOCUS_DIR, { recursive: true });
3841
+ await mkdir6(dirname4(focusEntry), { recursive: true });
4971
3842
  await writeManagedFile(focusEntry, "", "focus marker");
4972
3843
  } catch (error) {
4973
3844
  if (changeCreated) {
4974
3845
  try {
4975
- await unlink6(changePath);
3846
+ await unlink5(changePath);
4976
3847
  await cleanupEmptyParentDirs(changePath, CHANGES_DIR);
4977
3848
  } catch {
4978
3849
  }
@@ -4989,35 +3860,26 @@ async function createChange(name, summary = "", kind, options = {}) {
4989
3860
  throw error;
4990
3861
  }
4991
3862
  }
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
3863
 
5002
3864
  // 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";
3865
+ import { Buffer as Buffer2 } from "buffer";
3866
+ import { randomUUID } from "crypto";
3867
+ import { existsSync as existsSync6 } from "fs";
3868
+ import { mkdir as mkdir7, open, rename as rename4, unlink as unlink6 } from "fs/promises";
3869
+ import { basename as basename3, dirname as dirname5, join as join10 } from "path";
5008
3870
  async function focusChange(name, options = {}) {
5009
3871
  if (!name) {
5010
3872
  return { ok: false, kind: "usage", message: "rsp focus <name>" };
5011
3873
  }
5012
- const initialization = inspectInitialization3();
3874
+ const initialization = inspectRspInitialization();
5013
3875
  if (initialization)
5014
3876
  return { ok: false, kind: "error", message: initialization };
5015
3877
  try {
5016
3878
  return await withRspLock("focus-change", async () => {
5017
3879
  const workRef = await resolveExecutableChange(name, { mustExist: true });
5018
3880
  const focusEntry = resolveFocusMarkerPath(workRef);
5019
- await mkdir8(FOCUS_DIR, { recursive: true });
5020
- await mkdir8(dirname4(focusEntry), { recursive: true });
3881
+ await mkdir7(FOCUS_DIR, { recursive: true });
3882
+ await mkdir7(dirname5(focusEntry), { recursive: true });
5021
3883
  if (options.capsuleFile === void 0)
5022
3884
  await ensureManagedFile(focusEntry, "", "focus marker");
5023
3885
  else
@@ -5039,10 +3901,10 @@ async function readCapsule(source) {
5039
3901
  }
5040
3902
  async function replaceFocusCapsule(path, content) {
5041
3903
  requireManagedFile(path, "focus marker", { allowMissing: true });
5042
- const temporaryPath = join8(dirname4(path), `.${basename3(path)}.${randomUUID3()}.tmp`);
3904
+ const temporaryPath = join10(dirname5(path), `.${basename3(path)}.${randomUUID()}.tmp`);
5043
3905
  let handle;
5044
3906
  try {
5045
- handle = await open3(temporaryPath, "wx");
3907
+ handle = await open(temporaryPath, "wx");
5046
3908
  await handle.writeFile(content, "utf-8");
5047
3909
  await handle.close();
5048
3910
  handle = void 0;
@@ -5051,7 +3913,7 @@ async function replaceFocusCapsule(path, content) {
5051
3913
  } finally {
5052
3914
  await handle?.close().catch(() => {
5053
3915
  });
5054
- await unlink7(temporaryPath).catch((error) => {
3916
+ await unlink6(temporaryPath).catch((error) => {
5055
3917
  if (error.code !== "ENOENT")
5056
3918
  throw error;
5057
3919
  });
@@ -5059,7 +3921,7 @@ async function replaceFocusCapsule(path, content) {
5059
3921
  }
5060
3922
  async function readRegularCapsuleFile(path) {
5061
3923
  requireManagedFile(path, "focus capsule input");
5062
- const handle = await open3(path, "r");
3924
+ const handle = await open(path, "r");
5063
3925
  try {
5064
3926
  if (!(await handle.stat()).isFile())
5065
3927
  throw new Error(`focus capsule input must be a regular file: ${path}`);
@@ -5072,28 +3934,28 @@ async function readBoundedStdin() {
5072
3934
  const chunks = [];
5073
3935
  let size = 0;
5074
3936
  for await (const chunk of process.stdin) {
5075
- const buffer = Buffer3.isBuffer(chunk) ? chunk : Buffer3.from(chunk);
3937
+ const buffer = Buffer2.isBuffer(chunk) ? chunk : Buffer2.from(chunk);
5076
3938
  size += buffer.byteLength;
5077
3939
  if (size > MAX_FOCUS_CAPSULE_BYTES)
5078
3940
  throw new Error(`focus capsule exceeds ${MAX_FOCUS_CAPSULE_BYTES} UTF-8 bytes`);
5079
3941
  chunks.push(buffer);
5080
3942
  }
5081
- return Buffer3.concat(chunks, size);
3943
+ return Buffer2.concat(chunks, size);
5082
3944
  }
5083
3945
  async function unfocusChange(name) {
5084
3946
  if (!name) {
5085
3947
  return { ok: false, kind: "usage", message: "rsp unfocus <name>" };
5086
3948
  }
5087
- const initialization = inspectInitialization3();
3949
+ const initialization = inspectRspInitialization();
5088
3950
  if (initialization)
5089
3951
  return { ok: false, kind: "error", message: initialization };
5090
3952
  try {
5091
3953
  return await withRspLock("unfocus-change", async () => {
5092
3954
  const workRef = resolveWorkRef(name, { executable: false, mustExist: false });
5093
3955
  const focusEntry = resolveFocusMarkerPath(workRef);
5094
- if (!existsSync8(focusEntry))
3956
+ if (!existsSync6(focusEntry))
5095
3957
  throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
5096
- await unlink7(focusEntry);
3958
+ await unlink6(focusEntry);
5097
3959
  await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
5098
3960
  return { ok: true, action: "unfocus", workRef: workRef.name };
5099
3961
  });
@@ -5109,21 +3971,12 @@ function workRefFailure(error) {
5109
3971
  }
5110
3972
  throw error;
5111
3973
  }
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
3974
 
5122
3975
  // 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";
3976
+ import { randomUUID as randomUUID2 } from "crypto";
3977
+ import { existsSync as existsSync7 } from "fs";
3978
+ import { link as link2, mkdir as mkdir8, readdir as readdir4, readFile as readFile14, rename as rename5, unlink as unlink7, writeFile as writeFile5 } from "fs/promises";
3979
+ import { join as join11 } from "path";
5127
3980
  var ChangeGroupError = class extends Error {
5128
3981
  name = "ChangeGroupError";
5129
3982
  };
@@ -5131,7 +3984,7 @@ async function createChangeGroup(name, goal = "") {
5131
3984
  if (!name) {
5132
3985
  return failure("usage", "rsp group create <name> [goal]");
5133
3986
  }
5134
- const initialization = inspectInitialization4();
3987
+ const initialization = inspectRspInitialization();
5135
3988
  if (initialization)
5136
3989
  return failure("error", initialization);
5137
3990
  try {
@@ -5143,9 +3996,9 @@ async function createChangeGroup(name, goal = "") {
5143
3996
  if (await hasArchivedGroupBrief(groupName))
5144
3997
  throw new ChangeGroupError(`archived Change Group cannot be reopened: ${groupName}`);
5145
3998
  const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [groupName], "Change Group path");
5146
- await mkdir9(groupDirectory, { recursive: true });
3999
+ await mkdir8(groupDirectory, { recursive: true });
5147
4000
  try {
5148
- await writeFile4(ref.path, generateGroupBriefContent(groupName, goal), { flag: "wx" });
4001
+ await writeFile5(ref.path, generateGroupBriefContent(groupName, goal), { flag: "wx" });
5149
4002
  } catch (error) {
5150
4003
  if (error.code === "EEXIST")
5151
4004
  throw new ChangeGroupError(`Change Group already exists: ${groupName}`);
@@ -5164,7 +4017,7 @@ async function closeChangeGroup(name) {
5164
4017
  if (!name) {
5165
4018
  return failure("usage", "rsp group close <name>");
5166
4019
  }
5167
- const initialization = inspectInitialization4();
4020
+ const initialization = inspectRspInitialization();
5168
4021
  if (initialization)
5169
4022
  return failure("error", initialization);
5170
4023
  try {
@@ -5183,15 +4036,15 @@ async function closeChangeGroup(name) {
5183
4036
  const archiveDirectory = resolveArchiveDirectory(ref);
5184
4037
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5185
4038
  const archiveName = resolveArchiveName(archiveDirectory, date, "brief");
5186
- await mkdir9(archiveDirectory, { recursive: true });
5187
- await rename5(ref.path, join9(archiveDirectory, archiveName));
4039
+ await mkdir8(archiveDirectory, { recursive: true });
4040
+ await rename5(ref.path, join11(archiveDirectory, archiveName));
5188
4041
  const warnings = [];
5189
4042
  try {
5190
4043
  await cleanupEmptyParentDirs(ref.path, CHANGES_DIR);
5191
4044
  } catch (error) {
5192
4045
  warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
5193
4046
  }
5194
- return { ok: true, action: "close", groupName, archivePath: join9(archiveDirectory, archiveName), warnings };
4047
+ return { ok: true, action: "close", groupName, archivePath: join11(archiveDirectory, archiveName), warnings };
5195
4048
  });
5196
4049
  } catch (error) {
5197
4050
  if (error instanceof WorkRefError || error instanceof ChangeGroupError) {
@@ -5205,18 +4058,18 @@ async function reopenChangeGroup(name, options) {
5205
4058
  return failure("usage", "rsp group reopen <name> --reason <text> [--from <archive-path>]");
5206
4059
  }
5207
4060
  const reason = options.reason?.trim();
5208
- if (!reason || /[\r\n]/.test(reason)) {
4061
+ if (!reason || /[\r\n]/.test(reason) || !markdownText(parseMarkdown(reason).root).trim()) {
5209
4062
  return failure("error", "--reason must be one non-empty line");
5210
4063
  }
5211
- const initialization = inspectInitialization4();
4064
+ const initialization = inspectRspInitialization();
5212
4065
  if (initialization)
5213
4066
  return failure("error", initialization);
5214
4067
  try {
5215
4068
  return await withRspLock("reopen-change-group", async () => {
5216
4069
  const warnings = [];
5217
4070
  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}`);
4071
+ await requireEmptyGroupRecoverySubtree(join11(CHANGES_DIR, groupName), `Change Group work subtree for ${groupName}`);
4072
+ await requireEmptyGroupRecoverySubtree(join11(FOCUS_DIR, groupName), `Change Group focus subtree for ${groupName}`);
5220
4073
  const ref = resolveWorkRef(`${groupName}/brief`);
5221
4074
  if (ref.kind !== "group-brief")
5222
4075
  throw new ChangeGroupError(`invalid Change Group name: ${name}`);
@@ -5237,16 +4090,16 @@ async function reopenChangeGroup(name, options) {
5237
4090
  `selected archived Change Group belongs to ${record.group}, not ${groupName}`
5238
4091
  );
5239
4092
  }
5240
- if (existsSync9(ref.path))
4093
+ if (existsSync7(ref.path))
5241
4094
  throw new ArchiveHistoryError("open_group_exists", `open Change Group already exists: ${groupName}`);
5242
- const archivedContent = await readFile11(record.sourcePath, "utf-8");
4095
+ const archivedContent = await readFile14(record.sourcePath, "utf-8");
5243
4096
  const document = parseRspDocument(archivedContent, GROUP_BRIEF_DOCUMENT_SCHEMA);
5244
4097
  validateArchivedGroupBrief(document, groupName);
5245
4098
  const evidenceKey = `${GROUP_REOPEN_COMPLETION_PREFIX} \`${record.path}\`: ${reason}`;
5246
4099
  const retainedEvidence = /* @__PURE__ */ new Set();
5247
4100
  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))
4101
+ const retainedDocument = retained.sourcePath === record.sourcePath ? document : parseRspDocument(await readFile14(retained.sourcePath, "utf-8"), GROUP_BRIEF_DOCUMENT_SCHEMA);
4102
+ for (const item of getGroupReopenEvidence(retainedDocument))
5250
4103
  retainedEvidence.add(item.key);
5251
4104
  }
5252
4105
  if (retainedEvidence.has(evidenceKey)) {
@@ -5261,17 +4114,17 @@ async function reopenChangeGroup(name, options) {
5261
4114
  `- [ ] ${evidenceKey}`
5262
4115
  );
5263
4116
  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`);
4117
+ await mkdir8(groupDirectory, { recursive: true });
4118
+ const temporaryPath = join11(RSP_DIR, `.group-reopen-${process.pid}-${randomUUID2()}.tmp`);
5266
4119
  let mutationError;
5267
4120
  try {
5268
- await writeFile4(temporaryPath, reopenedContent, { flag: "wx" });
5269
- await link3(temporaryPath, ref.path);
4121
+ await writeFile5(temporaryPath, reopenedContent, { flag: "wx" });
4122
+ await link2(temporaryPath, ref.path);
5270
4123
  } catch (error) {
5271
4124
  mutationError = error;
5272
4125
  }
5273
4126
  try {
5274
- await unlink8(temporaryPath);
4127
+ await unlink7(temporaryPath);
5275
4128
  } catch (error) {
5276
4129
  if (!mutationError)
5277
4130
  warnings.push(`temporary reopen file cleanup failed: ${toErrorMessage(error)}`);
@@ -5302,21 +4155,12 @@ async function reopenChangeGroup(name, options) {
5302
4155
  function failure(kind, message, candidates = [], remainingCandidates = 0) {
5303
4156
  return { ok: false, kind, message, candidates, remainingCandidates };
5304
4157
  }
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
4158
  async function requireEmptyGroupRecoverySubtree(path, label) {
5315
4159
  if (!requireManagedDirectory(path, label, { allowMissing: true }))
5316
4160
  return;
5317
4161
  let entries;
5318
4162
  try {
5319
- entries = await readdir3(path);
4163
+ entries = await readdir4(path);
5320
4164
  } catch (error) {
5321
4165
  throw new ChangeGroupError(`unable to inspect ${label}: ${toErrorMessage(error)}`);
5322
4166
  }
@@ -5329,7 +4173,7 @@ function validateArchivedGroupBrief(document, group) {
5329
4173
  throw new ArchiveHistoryError("archive_heading_invalid", `archived Change Group must contain exactly one canonical identity heading for ${group}`);
5330
4174
  for (const section of GROUP_BRIEF_DOCUMENT_SCHEMA.sections) {
5331
4175
  const matches = getDocumentSections(document, section.id);
5332
- if (matches.length !== 1 || !matches[0].canonical || !getDocumentSectionBody(document, section.id)) {
4176
+ if (matches.length !== 1 || !matches[0].canonical || getVisibleBodyLines(getDocumentSectionMarkdown(document, section.id)).length === 0) {
5333
4177
  throw new ArchiveHistoryError(
5334
4178
  "archive_section_invalid",
5335
4179
  `archived Change Group must contain exactly one canonical required section: ${section.heading}`
@@ -5339,10 +4183,10 @@ function validateArchivedGroupBrief(document, group) {
5339
4183
  }
5340
4184
  function collectCloseReasons(group, errors) {
5341
4185
  const reasons = [...errors, ...group.warnings];
5342
- const open4 = group.slices.filter((slice) => slice.state === "open").map((slice) => slice.name);
4186
+ const open2 = group.slices.filter((slice) => slice.state === "open").map((slice) => slice.name);
5343
4187
  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(", ")}`);
4188
+ if (open2.length > 0)
4189
+ reasons.push(`open slices: ${open2.join(", ")}`);
5346
4190
  if (missing.length > 0)
5347
4191
  reasons.push(`missing slices: ${missing.join(", ")}`);
5348
4192
  if (group.completion.total === 0)
@@ -5355,18 +4199,18 @@ function collectCloseReasons(group, errors) {
5355
4199
  }
5356
4200
 
5357
4201
  // 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";
4202
+ import { existsSync as existsSync8 } from "fs";
4203
+ import { mkdir as mkdir9, readFile as readFile15, unlink as unlink8, writeFile as writeFile6 } from "fs/promises";
4204
+ import { dirname as dirname6 } from "path";
5361
4205
  async function reopenChange(name, options) {
5362
4206
  if (!name) {
5363
4207
  return { ok: false, kind: "usage", message: "rsp reopen <name> --reason <text> [--from <archive-path>]", candidates: [], remainingCandidates: 0 };
5364
4208
  }
5365
4209
  const reason = options.reason?.trim();
5366
- if (!reason || /[\r\n]/.test(reason)) {
4210
+ if (!reason || /[\r\n]/.test(reason) || !markdownText(parseMarkdown(reason).root).trim()) {
5367
4211
  return { ok: false, kind: "error", message: "--reason must be one non-empty line", candidates: [], remainingCandidates: 0 };
5368
4212
  }
5369
- const initialization = inspectInitialization5();
4213
+ const initialization = inspectRspInitialization();
5370
4214
  if (initialization)
5371
4215
  return { ok: false, kind: "error", message: initialization, candidates: [], remainingCandidates: 0 };
5372
4216
  try {
@@ -5401,36 +4245,36 @@ async function reopenChange(name, options) {
5401
4245
  }
5402
4246
  throw error;
5403
4247
  }
5404
- if (existsSync10(workRef.path))
4248
+ if (existsSync8(workRef.path))
5405
4249
  throw new ArchiveHistoryError("open_change_exists", `open Change already exists: ${name}`);
5406
4250
  const focusEntry = resolveFocusMarkerPath(workRef);
5407
- if (existsSync10(focusEntry))
4251
+ if (existsSync8(focusEntry))
5408
4252
  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");
4253
+ const archivedContent = await readFile15(record.sourcePath, "utf-8");
5410
4254
  const reopenedContent = appendChecklistItem(
5411
4255
  appendChecklistItem(archivedContent, "tasks", `- [ ] Resolve reopened concern: ${reason}`),
5412
4256
  "verify",
5413
4257
  `- [ ] Verify reopened concern: ${reason}`
5414
4258
  );
5415
- await mkdir10(dirname5(workRef.path), { recursive: true });
4259
+ await mkdir9(dirname6(workRef.path), { recursive: true });
5416
4260
  let changeCreated = false;
5417
4261
  try {
5418
- await writeFile5(workRef.path, reopenedContent, { flag: "wx" });
4262
+ await writeFile6(workRef.path, reopenedContent, { flag: "wx" });
5419
4263
  changeCreated = true;
5420
- await mkdir10(FOCUS_DIR, { recursive: true });
5421
- await mkdir10(dirname5(focusEntry), { recursive: true });
4264
+ await mkdir9(FOCUS_DIR, { recursive: true });
4265
+ await mkdir9(dirname6(focusEntry), { recursive: true });
5422
4266
  await writeManagedFile(focusEntry, "", "focus marker");
5423
4267
  } catch (error) {
5424
4268
  if (changeCreated) {
5425
4269
  try {
5426
- await unlink9(workRef.path);
4270
+ await unlink8(workRef.path);
5427
4271
  await cleanupEmptyParentDirs(workRef.path, CHANGES_DIR);
5428
4272
  } catch {
5429
4273
  }
5430
4274
  }
5431
4275
  throw error;
5432
4276
  }
5433
- return { ok: true, workRef: workRef.name, archivePath: record.path, git: existsSync10(".git") };
4277
+ return { ok: true, workRef: workRef.name, archivePath: record.path, git: existsSync8(".git") };
5434
4278
  });
5435
4279
  } catch (error) {
5436
4280
  if (error instanceof ArchiveHistoryError || error instanceof WorkRefError) {
@@ -5445,15 +4289,6 @@ async function reopenChange(name, options) {
5445
4289
  throw error;
5446
4290
  }
5447
4291
  }
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
4292
  function appendChecklistItem(content, sectionId, item) {
5458
4293
  try {
5459
4294
  return appendDocumentSectionItem(parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), sectionId, item);
@@ -5468,25 +4303,6 @@ function appendChecklistItem(content, sectionId, item) {
5468
4303
  }
5469
4304
  }
5470
4305
 
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
4306
  // src/cli/presenters/work.ts
5491
4307
  function presentFailure(result) {
5492
4308
  const label = result.kind === "usage" ? "Usage:" : "Error:";
@@ -5794,30 +4610,8 @@ var unfocusCommand = defineCommand4({
5794
4610
  await executeCliCommand({ execute: () => unfocusChange(args.name), present: presentFocus, exitCode: (result) => result.ok ? void 0 : 1 }, args);
5795
4611
  }
5796
4612
  });
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
4613
  function createWorkCommands(deprecatedLite) {
5819
4614
  return {
5820
- commit: commitCommand,
5821
4615
  create: createCreateCommand(deprecatedLite),
5822
4616
  group: groupCommand,
5823
4617
  focus: focusCommand,
@@ -5840,7 +4634,7 @@ function createRootCommand(options) {
5840
4634
  init: setupCommands.init,
5841
4635
  add: setupCommands.add,
5842
4636
  specs: inspectionCommands.specs,
5843
- commit: workCommands.commit,
4637
+ config: inspectionCommands.config,
5844
4638
  create: workCommands.create,
5845
4639
  group: workCommands.group,
5846
4640
  focus: workCommands.focus,
@@ -5866,9 +4660,6 @@ function isInteractiveTerminal(environment) {
5866
4660
  function shouldAutoLaunchUi(args, environment) {
5867
4661
  return args.length === 0 && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
5868
4662
  }
5869
- function shouldLaunchSkillsUi(args, environment) {
5870
- return args.length === 1 && args[0] === "skills" && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
5871
- }
5872
4663
  function validateUiArgs(args) {
5873
4664
  let lang = "auto";
5874
4665
  for (let index = 0; index < args.length; index += 1) {
@@ -5908,6 +4699,7 @@ function normalizeLegacyCreateOptions(rawArgs) {
5908
4699
  };
5909
4700
  }
5910
4701
  async function runCli(rawArgs = process.argv.slice(2)) {
4702
+ await guardProjectVersion(rawArgs);
5911
4703
  const terminalEnvironment = {
5912
4704
  stdinTty: Boolean(process.stdin.isTTY),
5913
4705
  stdoutTty: Boolean(process.stdout.isTTY),
@@ -5915,19 +4707,6 @@ async function runCli(rawArgs = process.argv.slice(2)) {
5915
4707
  ci: process.env.CI
5916
4708
  };
5917
4709
  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
4710
  if (explicitUi || shouldAutoLaunchUi(rawArgs, terminalEnvironment)) {
5932
4711
  const { lang } = validateUiArgs(explicitUi ? rawArgs.slice(1) : []);
5933
4712
  if (!isInteractiveTerminal(terminalEnvironment)) {
@@ -5936,7 +4715,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
5936
4715
  } else {
5937
4716
  const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
5938
4717
  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");
4718
+ const { runTui } = await import("./entry-RIJQ2X46.mjs");
5940
4719
  const exitCode = await runTui(locale);
5941
4720
  if (exitCode !== 0)
5942
4721
  process.exitCode = exitCode;
@@ -5961,7 +4740,7 @@ async function runCliMain(rawArgs = process.argv.slice(2)) {
5961
4740
  process.exitCode = 1;
5962
4741
  }
5963
4742
  }
5964
- var isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve4(process.argv[1]);
4743
+ var isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve2(process.argv[1]);
5965
4744
  if (isMain)
5966
4745
  await runCliMain();
5967
4746
  export {