@oevery/rsp 2.0.3 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -119,7 +119,7 @@ function changeNameFromPath(changesDir, filePath) {
119
119
  function normalizeLogicalPath(pathValue) {
120
120
  return pathValue.split(sep).join("/").replace(/\\/g, "/");
121
121
  }
122
- function generateChangeContent(name, summary = "", kind) {
122
+ function generateChangeContent(name, summary = "", kind, options = {}) {
123
123
  const proposalSummary = summary || (name === "project-setup" ? "Capture the project model, boundaries, and stable local constraints" : "<one-line summary>");
124
124
  if (name === "project-setup") {
125
125
  return `---
@@ -184,6 +184,8 @@ kind: ops
184
184
  `;
185
185
  }
186
186
  const frontmatterKind = kind ?? "<choose: feature | fix | refactor | docs | ops | research>";
187
+ if (options.lite)
188
+ return generateLiteChangeContent(name, proposalSummary, frontmatterKind);
187
189
  const template = getChangeTemplateByKind(kind);
188
190
  return `---
189
191
  kind: "${frontmatterKind}"
@@ -229,6 +231,58 @@ ${template.acceptanceSection}
229
231
  - [ ] Decide whether this change produced durable knowledge that belongs in \`.rsp/specs/\` or \`.rsp/rules/\`
230
232
  - [ ] If yes, write only stable facts to the smallest correct target file before archive; do not promote task history, debugging notes, or one-off implementation context
231
233
 
234
+ ## Blockers
235
+ - none
236
+ `;
237
+ }
238
+ function generateLiteChangeContent(name, proposalSummary, frontmatterKind) {
239
+ return `---
240
+ kind: "${frontmatterKind}"
241
+ ---
242
+
243
+ # Change: ${name}
244
+
245
+ ## Proposal
246
+ - Summary: ${proposalSummary}
247
+ - Why:
248
+ - <why this small change matters>
249
+ - Scope:
250
+ - <what will change>
251
+ - Non-goals:
252
+ - none
253
+
254
+ ## Spec
255
+ ### MODIFIED
256
+ - Requirement: <observable outcome>
257
+ - <what should be true after this change>
258
+
259
+ ### Acceptance
260
+ #### Scenario: change is complete
261
+ - GIVEN <current context>
262
+ - WHEN <the change is applied>
263
+ - THEN <expected outcome>
264
+
265
+ ## Design
266
+ - Approach:
267
+ - <minimal implementation approach>
268
+ - Affected areas:
269
+ - <primary file, directory, or doc path>
270
+ - Constraints:
271
+ - <important constraint, or none>
272
+
273
+ ## Tasks
274
+ - [ ] Implement the small change
275
+ - [ ] Verify the result
276
+
277
+ ## Verify
278
+ - Automated:
279
+ - [ ] <exact command, or not needed: reason>
280
+ - Manual:
281
+ - [ ] <exact scenario, or not needed: reason>
282
+ - Durable updates:
283
+ - [ ] Decide whether this change produced durable knowledge that belongs in ".rsp/specs/" or ".rsp/rules/"
284
+ - [ ] If yes, write only stable facts to the smallest correct target file before archive
285
+
232
286
  ## Blockers
233
287
  - none
234
288
  `;
@@ -240,7 +294,7 @@ function getChangeTemplateByKind(kind) {
240
294
  why: "<what user need or capability gap this addresses>",
241
295
  scope: "<what new behavior or capability will be delivered>",
242
296
  nonGoals: "<what related capabilities are explicitly out of scope>",
243
- specSection: "### ADDED\n- Requirement: <new user-facing behavior>\n - <verifiable requirement for the new capability>",
297
+ specSection: "<!-- Describe observable behavior and requirements. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: <new user-facing behavior>\n - <verifiable requirement for the new capability>",
244
298
  acceptanceSection: "#### Scenario: user exercises the new capability\n- GIVEN <context>\n- WHEN <user action>\n- THEN <expected new behavior>",
245
299
  approach: "<how the new capability will be implemented>",
246
300
  affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
@@ -255,7 +309,7 @@ function getChangeTemplateByKind(kind) {
255
309
  why: "<what current behavior is wrong or broken>",
256
310
  scope: "<what specific defect is being corrected>",
257
311
  nonGoals: "<what related but separate issues are out of scope>",
258
- specSection: "### MODIFIED\n- Requirement: correct behavior\n - <expected correct behavior after the fix>",
312
+ specSection: "<!-- Describe expected correct behavior. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: correct behavior\n - <expected correct behavior after the fix>",
259
313
  acceptanceSection: "#### Scenario: defect is resolved\n- GIVEN <conditions that trigger the defect>\n- WHEN <action that previously failed>\n- THEN <expected correct outcome>\n- AND the original broken outcome no longer occurs",
260
314
  approach: "<root cause analysis and fix strategy>",
261
315
  affectedArea1: "<concrete file path, directory, module, or subsystem containing the defect>",
@@ -270,7 +324,7 @@ function getChangeTemplateByKind(kind) {
270
324
  why: "<what maintainability, readability, or structural issue motivates this>",
271
325
  scope: "<what code areas are being restructured>",
272
326
  nonGoals: "<no behavior change is expected or intended>",
273
- specSection: "### MODIFIED\n- Requirement: internal structure improvement\n - <desired structural outcome without observable behavior change>",
327
+ specSection: "<!-- Describe the desired structural outcome. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: internal structure improvement\n - <desired structural outcome without observable behavior change>",
274
328
  acceptanceSection: "#### Scenario: behavior is preserved after restructuring\n- GIVEN the existing codebase before the refactor\n- WHEN the refactored code runs against existing tests\n- THEN all existing tests pass unchanged",
275
329
  approach: "<how the code will be restructured (rename, extract, move, etc.)>",
276
330
  affectedArea1: "<concrete file path, directory, module, or subsystem being refactored>",
@@ -285,7 +339,7 @@ function getChangeTemplateByKind(kind) {
285
339
  why: "<why the documentation change matters>",
286
340
  scope: "<which docs, readers, or workflows will be updated>",
287
341
  nonGoals: "<what documentation behavior or audience is out of scope>",
288
- specSection: "### MODIFIED\n- Requirement: documentation accuracy\n - <what durable reader-facing behavior or explanation changes>",
342
+ specSection: "<!-- Describe what the reader should see or experience. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: documentation accuracy\n - <what durable reader-facing behavior or explanation changes>",
289
343
  acceptanceSection: "#### Scenario: reader follows the updated guidance\n- GIVEN the updated documentation\n- WHEN a reader follows the documented workflow\n- THEN the steps are accurate and sufficient",
290
344
  approach: "<how the documentation will be updated and organized>",
291
345
  affectedArea1: "<concrete doc path, directory, module doc, or documentation surface 1>",
@@ -300,7 +354,7 @@ function getChangeTemplateByKind(kind) {
300
354
  why: "<what question or uncertainty this research resolves>",
301
355
  scope: "<what system area, option set, or hypothesis is being examined>",
302
356
  nonGoals: "<what implementation work is explicitly out of scope>",
303
- specSection: "### ADDED\n- Requirement: research outcome recording\n - <what finding, option, or decision must be captured clearly>",
357
+ specSection: "<!-- Describe what finding or decision must be captured. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: research outcome recording\n - <what finding, option, or decision must be captured clearly>",
304
358
  acceptanceSection: "#### Scenario: research question is resolved\n- GIVEN the current uncertainty or open question\n- WHEN the investigation is completed\n- THEN the result is recorded clearly enough to guide follow-up work",
305
359
  approach: "<how the investigation will be performed and what evidence will be gathered>",
306
360
  affectedArea1: "<concrete file path, directory, module, subsystem, or source under investigation>",
@@ -315,7 +369,7 @@ function getChangeTemplateByKind(kind) {
315
369
  why: "<why this operational or environment change matters>",
316
370
  scope: "<what environment, workflow, or operational path changes>",
317
371
  nonGoals: "<what product or feature behavior will not change>",
318
- specSection: "### MODIFIED\n- Requirement: operational behavior\n - <what reliable operational outcome should change or stay true>",
372
+ specSection: "<!-- Describe the reliable operational outcome. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: operational behavior\n - <what reliable operational outcome should change or stay true>",
319
373
  acceptanceSection: "#### Scenario: operational path succeeds\n- GIVEN the target environment or workflow\n- WHEN the operational change is applied\n- THEN the expected operational outcome is reliable",
320
374
  approach: "<how the operational change will be applied safely>",
321
375
  affectedArea1: "<concrete script, config path, workflow, environment path, or operational surface 1>",
@@ -330,7 +384,7 @@ function getChangeTemplateByKind(kind) {
330
384
  why: "<why this change matters>",
331
385
  scope: "<what this change will do>",
332
386
  nonGoals: "<what this change will not do>",
333
- specSection: "### ADDED\n- Requirement: <new or updated behavior>\n - <verifiable requirement>",
387
+ specSection: "<!-- Describe observable behavior and requirements. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: <new or updated behavior>\n - <verifiable requirement>",
334
388
  acceptanceSection: "#### Scenario: <name>\n- GIVEN <context>\n- WHEN <action>\n- THEN <expected outcome>",
335
389
  approach: "<how the change will be implemented>",
336
390
  affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
@@ -357,7 +411,7 @@ Read in order:
357
411
  5. .rsp/specs/INDEX.md
358
412
  6. only the relevant additional .rsp/rules/*.md and .rsp/specs/*.md files
359
413
 
360
- If \`.rsp/focus.d/\` is empty, ask what to work on or suggest \`npx -y @oevery/rsp create <name>\`.
414
+ 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.
361
415
  If your agent supports Agent Skills, load \`rsp\` for setup, repair, and durable-decision tasks.
362
416
  ${RSP_AGENTS_END}`;
363
417
  }
@@ -547,6 +601,57 @@ function detectDeltaSections(content) {
547
601
  removed: /^###\s*REMOVED/im.test(body)
548
602
  };
549
603
  }
604
+ function collectArchiveReadiness(content) {
605
+ const warnings = [];
606
+ const tasksSection = extractSection(content, "Tasks");
607
+ const verifySection = extractSection(content, "Verify");
608
+ const taskTodos = getOpenCheckboxes(tasksSection);
609
+ if (taskTodos.length > 0)
610
+ warnings.push(`${taskTodos.length} task item(s) still incomplete`);
611
+ const verifyTodos = getOpenCheckboxes(verifySection);
612
+ if (verifyTodos.length > 0)
613
+ warnings.push(`${verifyTodos.length} Verify checklist item(s) are still incomplete`);
614
+ const activeBlockers = hasMeaningfulBlockers(content);
615
+ if (activeBlockers)
616
+ warnings.push("active blockers are present in the change file");
617
+ const scenarios = parseScenarios(content);
618
+ const missingScenarios = scenarios.length === 0;
619
+ if (missingScenarios)
620
+ warnings.push("no Scenario blocks found (some changes do not need them)");
621
+ const archiveReady = activeBlockers ? "no" : warnings.length === 0 ? "yes" : "judgment";
622
+ return {
623
+ taskTodos,
624
+ verifyTodos,
625
+ activeBlockers,
626
+ scenarioCount: scenarios.length,
627
+ missingScenarios,
628
+ deterministic: warnings.length === 0 ? "pass" : "warnings",
629
+ semantic: "needs-review",
630
+ archiveReady,
631
+ warnings
632
+ };
633
+ }
634
+ function buildDurableReviewGuidance(candidateTargets) {
635
+ return {
636
+ required: true,
637
+ decisions: [
638
+ "No durable update needed",
639
+ "Update existing spec or rule",
640
+ "Create a new durable spec"
641
+ ],
642
+ candidateTargets,
643
+ note: "Semantic review decides whether stable facts belong in specs or rules; the CLI never merges delta specs automatically."
644
+ };
645
+ }
646
+ function getDurableReviewCandidateTargets(options = {}) {
647
+ const targets = [".rsp/specs/design.md"];
648
+ if (options.projectRulesExists)
649
+ targets.push(".rsp/rules/project-rules.md");
650
+ return targets;
651
+ }
652
+ function collectArchiveChecklist(content) {
653
+ return collectArchiveReadiness(content).warnings;
654
+ }
550
655
  function parseScenarios(content) {
551
656
  const scenarios = [];
552
657
  const regex = /^#{3,4}\s*Scenario:\s*(.+)$/gim;
@@ -692,20 +797,30 @@ import { dirname as dirname4, join as join5 } from "path";
692
797
  import { existsSync as existsSync4 } from "fs";
693
798
  import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
694
799
  import { basename as basename2, join as join4, relative as relative2 } from "path";
695
- async function buildSpecsIndex({ acquireLock = true } = {}) {
800
+ async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
696
801
  if (acquireLock)
697
- return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false }));
802
+ return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet }));
698
803
  const specsDir = join4(RSP_DIR, "specs");
699
804
  if (!existsSync4(specsDir)) {
700
- console.log(` ${pc.dim("No specs directory.")}
805
+ if (!quiet)
806
+ console.log(` ${pc.dim("No specs directory.")}
701
807
  `);
702
- return;
808
+ return false;
703
809
  }
704
810
  const specFiles = (await walkMarkdownFiles(specsDir)).filter((fp) => {
705
811
  const name = basename2(fp);
706
812
  return name !== "INDEX.md" && name !== "design.md";
707
813
  }).sort();
708
814
  const lines = [];
815
+ lines.push("---");
816
+ lines.push("title: Specs Index");
817
+ lines.push("summary: Additional project-level specs beyond design.md.");
818
+ lines.push("kind: generated-index");
819
+ lines.push("index_type: specs");
820
+ lines.push("source_dir: .rsp/specs");
821
+ lines.push(`entry_count: ${specFiles.length}`);
822
+ lines.push("---");
823
+ lines.push("");
709
824
  lines.push("# Specs Index");
710
825
  lines.push("");
711
826
  lines.push("_Additional project-level specs beyond `design.md`._");
@@ -725,11 +840,16 @@ async function buildSpecsIndex({ acquireLock = true } = {}) {
725
840
  lines.push(`| ${escapeCell(rel)} | ${escapeCell(title)} | ${escapeCell(summary || "\u2014")} |`);
726
841
  }
727
842
  }
728
- lines.push("");
729
- lines.push("_Auto-generated by `rsp update`._");
730
- await writeFile3(join4(specsDir, "INDEX.md"), lines.join("\n"));
731
- console.log(` ${pc.green("INDEX.md updated:")} ${specFiles.length} spec file(s).
843
+ const indexPath = join4(specsDir, "INDEX.md");
844
+ const nextContent = lines.join("\n");
845
+ const existingContent = existsSync4(indexPath) ? await readFile4(indexPath, "utf-8") : null;
846
+ const changed = existingContent !== nextContent;
847
+ if (changed)
848
+ await writeFile3(indexPath, nextContent);
849
+ if (!quiet)
850
+ console.log(` ${pc.green("INDEX.md updated:")} ${specFiles.length} spec file(s).
732
851
  `);
852
+ return changed;
733
853
  }
734
854
  function extractTitle(content) {
735
855
  for (const rawLine of content.split("\n")) {
@@ -796,25 +916,33 @@ import { basename as basename4, dirname as dirname6, join as join7 } from "path"
796
916
  import { existsSync as existsSync6 } from "fs";
797
917
  import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
798
918
  import { basename as basename3, dirname as dirname5, join as join6, relative as relative3 } from "path";
799
- async function buildArchiveIndex({ acquireLock = true } = {}) {
919
+ async function buildArchiveIndex({ acquireLock = true, quiet = false } = {}) {
800
920
  if (acquireLock)
801
- return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false }));
921
+ return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false, quiet }));
802
922
  if (!existsSync6(ARCHIVES_DIR)) {
803
- console.log(` ${pc.dim("No archives directory.")}
923
+ if (!quiet)
924
+ console.log(` ${pc.dim("No archives directory.")}
804
925
  `);
805
- return;
926
+ return false;
806
927
  }
807
928
  const archiveFiles = (await walkMarkdownFiles(ARCHIVES_DIR)).filter((fp) => basename3(fp) !== "INDEX.md").sort();
808
929
  const lines = [];
809
- lines.push("# Archive Index");
930
+ lines.push("---");
931
+ lines.push("title: Archive Index");
932
+ lines.push("summary: Completed RSP changes.");
933
+ lines.push("kind: generated-index");
934
+ lines.push("index_type: archives");
935
+ lines.push("source_dir: .rsp/archives");
936
+ lines.push(`entry_count: ${archiveFiles.length}`);
937
+ lines.push("---");
810
938
  lines.push("");
811
- lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
939
+ lines.push("# Archive Index");
812
940
  lines.push("");
813
941
  if (archiveFiles.length === 0) {
814
942
  lines.push("_No archived changes yet._");
815
943
  } else {
816
- lines.push("| Date | Change | Summary |");
817
- lines.push("|------|--------|---------|");
944
+ lines.push("| Date | Change | Kind | Summary |");
945
+ lines.push("|------|--------|------|---------|");
818
946
  }
819
947
  for (const fp of archiveFiles) {
820
948
  const base = basename3(fp);
@@ -825,27 +953,39 @@ async function buildArchiveIndex({ acquireLock = true } = {}) {
825
953
  const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
826
954
  const changeName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
827
955
  let summary = "";
956
+ let kind = "";
828
957
  try {
829
958
  const content = await readFile5(fp, "utf-8");
959
+ const fm = parseFrontmatter(content);
960
+ if (typeof fm?.kind === "string")
961
+ kind = fm.kind;
962
+ if (typeof fm?.summary === "string")
963
+ summary = fm.summary.trim();
830
964
  const proposal = extractSection(content, "Proposal");
831
965
  const summaryLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Summary:"));
832
- if (summaryLine)
966
+ if (!summary && summaryLine)
833
967
  summary = summaryLine.slice("- Summary:".length).trim();
834
968
  } catch {
835
969
  }
836
- const summaryCell = summary ? summary.replace(/\|/g, "\\|") : "\u2014";
837
- lines.push(`| ${date} | ${changeName} | ${summaryCell} |`);
970
+ lines.push(`| ${date} | ${escapeCell2(changeName)} | ${escapeCell2(kind || "\u2014")} | ${escapeCell2(summary || "\u2014")} |`);
838
971
  }
839
- lines.push("");
840
- lines.push("_Auto-generated by `rsp update`._");
841
972
  const indexPath = join6(ARCHIVES_DIR, "INDEX.md");
842
- await writeFile5(indexPath, lines.join("\n"));
843
- console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived change(s).
973
+ const nextContent = lines.join("\n");
974
+ const existingContent = existsSync6(indexPath) ? await readFile5(indexPath, "utf-8") : null;
975
+ const changed = existingContent !== nextContent;
976
+ if (changed)
977
+ await writeFile5(indexPath, nextContent);
978
+ if (!quiet)
979
+ console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived change(s).
844
980
  `);
981
+ return changed;
982
+ }
983
+ function escapeCell2(value) {
984
+ return value.replace(/\|/g, "\\|");
845
985
  }
846
986
 
847
987
  // src/commands/archive.ts
848
- async function archiveChange(name) {
988
+ async function archiveChange(name, options = {}) {
849
989
  if (!name) {
850
990
  console.error(` ${pc.red("Usage:")} rsp archive <name>`);
851
991
  process.exit(1);
@@ -860,6 +1000,24 @@ async function archiveChange(name) {
860
1000
  console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
861
1001
  process.exit(1);
862
1002
  }
1003
+ if (options.dryRun) {
1004
+ const content = await readFile6(srcPath, "utf-8");
1005
+ const checklist = collectArchiveChecklist(content);
1006
+ console.log();
1007
+ console.log(` ${pc.bold("Archive dry-run for")} ${pc.cyan(name)}`);
1008
+ console.log();
1009
+ if (checklist.length === 0) {
1010
+ console.log(` ${pc.green("\u2713")} Ready to archive. No deterministic warnings found.
1011
+ `);
1012
+ } else {
1013
+ for (const line of checklist)
1014
+ console.log(` ${pc.yellow("\u26A0")} ${line}`);
1015
+ console.log();
1016
+ console.log(` ${pc.dim("Review the warnings above before treating this work as fully closed.")}
1017
+ `);
1018
+ }
1019
+ return;
1020
+ }
863
1021
  return withRspLock("archive-change", async () => {
864
1022
  const content = await readFile6(srcPath, "utf-8");
865
1023
  const checklist = collectArchiveChecklist(content);
@@ -929,23 +1087,6 @@ function resolveArchiveName(archiveSubdir, date, base) {
929
1087
  console.error(` ${pc.red("Error:")} archive name collision exceeded ${MAX_SUFFIX} attempts for ${base}`);
930
1088
  process.exit(1);
931
1089
  }
932
- function collectArchiveChecklist(content) {
933
- const warnings = [];
934
- const tasksSection = extractSection(content, "Tasks");
935
- const verifySection = extractSection(content, "Verify");
936
- const taskTodos = getOpenCheckboxes(tasksSection);
937
- if (taskTodos.length > 0)
938
- warnings.push(`${taskTodos.length} task item(s) still incomplete`);
939
- const verifyTodos = getOpenCheckboxes(verifySection);
940
- if (verifyTodos.length > 0)
941
- warnings.push(`${verifyTodos.length} Verify checklist item(s) are still incomplete`);
942
- if (hasMeaningfulBlockers(content))
943
- warnings.push("active blockers are present in the change file");
944
- const scenarios = parseScenarios(content);
945
- if (scenarios.length === 0)
946
- warnings.push("no Scenario blocks found (some changes do not need them)");
947
- return warnings;
948
- }
949
1090
 
950
1091
  // src/commands/check.ts
951
1092
  import { existsSync as existsSync8 } from "fs";
@@ -960,6 +1101,10 @@ async function runCheck(options = {}) {
960
1101
  const runtime = [];
961
1102
  const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
962
1103
  const addDiagnostic = (diagnostic) => diagnostics.push(diagnostic);
1104
+ let focusedSet = /* @__PURE__ */ new Set();
1105
+ if (options.focused) {
1106
+ focusedSet = await getFocusedChangeNames({ onError: reportRuntime });
1107
+ }
963
1108
  if (existsSync8(FOCUS_DIR)) {
964
1109
  const entries = await walkFiles(FOCUS_DIR, { onError: reportRuntime });
965
1110
  for (const entryPath of entries) {
@@ -1004,11 +1149,13 @@ async function runCheck(options = {}) {
1004
1149
  }
1005
1150
  }
1006
1151
  }
1007
- const changeFiles = existsSync8(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
1152
+ const allChangeFiles = existsSync8(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
1153
+ const changeFiles = options.focused ? allChangeFiles.filter((fp) => focusedSet.has(changeNameFromPath(CHANGES_DIR, fp))) : allChangeFiles;
1008
1154
  if (changeFiles.length === 0) {
1009
1155
  const result2 = {
1010
1156
  command: "check",
1011
1157
  ok: diagnostics.every((d) => d.severity !== "error"),
1158
+ focused: Boolean(options.focused),
1012
1159
  diagnostics,
1013
1160
  runtime,
1014
1161
  summary: {
@@ -1023,6 +1170,8 @@ async function runCheck(options = {}) {
1023
1170
  }
1024
1171
  console.log();
1025
1172
  console.log(` ${pc.bold("RSP check")}`);
1173
+ if (options.focused)
1174
+ console.log(` ${pc.dim("(focused only)")}`);
1026
1175
  console.log();
1027
1176
  if (diagnostics.length === 0) {
1028
1177
  console.log(` ${pc.dim("No change files to check.")}
@@ -1159,6 +1308,30 @@ async function runCheck(options = {}) {
1159
1308
  message: `delta markers found: ${parts.join(", ")}`
1160
1309
  });
1161
1310
  }
1311
+ const placeholderLines = findUnfinishedTemplateLines(content);
1312
+ if (placeholderLines.length > 0) {
1313
+ addDiagnostic({
1314
+ severity: "warning",
1315
+ code: "unfinished_template_placeholders",
1316
+ change: name,
1317
+ path: fp,
1318
+ message: "unfinished template placeholders found",
1319
+ details: placeholderLines,
1320
+ hint: "Replace angle-bracket template text with concrete change details before archiving."
1321
+ });
1322
+ }
1323
+ const clarificationLines = findUnresolvedClarificationLines(content);
1324
+ if (clarificationLines.length > 0) {
1325
+ addDiagnostic({
1326
+ severity: "warning",
1327
+ code: "unresolved_clarifications",
1328
+ change: name,
1329
+ path: fp,
1330
+ message: "unresolved clarification markers found",
1331
+ details: clarificationLines,
1332
+ hint: "Resolve or remove clarification markers once the open question is answered."
1333
+ });
1334
+ }
1162
1335
  const scenarios = parseScenarios(content);
1163
1336
  if (scenarios.length > 0) {
1164
1337
  const scenarioIssues = [];
@@ -1191,6 +1364,7 @@ async function runCheck(options = {}) {
1191
1364
  const result = {
1192
1365
  command: "check",
1193
1366
  ok: diagnostics.every((d) => d.severity !== "error"),
1367
+ focused: Boolean(options.focused),
1194
1368
  diagnostics,
1195
1369
  runtime,
1196
1370
  summary: {
@@ -1205,6 +1379,8 @@ async function runCheck(options = {}) {
1205
1379
  }
1206
1380
  console.log();
1207
1381
  console.log(` ${pc.bold("RSP check")}`);
1382
+ if (options.focused)
1383
+ console.log(` ${pc.dim("(focused only)")}`);
1208
1384
  console.log();
1209
1385
  for (const diagnostic of diagnostics) {
1210
1386
  const icon = diagnostic.severity === "error" ? pc.red("\u2717") : diagnostic.severity === "warning" ? pc.yellow("\u26A0") : pc.dim("\u2139");
@@ -1225,12 +1401,35 @@ async function runCheck(options = {}) {
1225
1401
  `);
1226
1402
  return result;
1227
1403
  }
1404
+ function findUnfinishedTemplateLines(content) {
1405
+ 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;
1406
+ return collectMatchingLines(content, (line) => {
1407
+ const trimmed = stripInlineCodeSpans(line).trim();
1408
+ return trimmed.startsWith("<!--") === false && placeholderRe.test(trimmed);
1409
+ });
1410
+ }
1411
+ function findUnresolvedClarificationLines(content) {
1412
+ return collectMatchingLines(content, (line) => /\[(?:NEEDS|TODO|TBD)\s+CLARIFICATION(?::[^\]]*)?\]/i.test(stripInlineCodeSpans(line)));
1413
+ }
1414
+ function stripInlineCodeSpans(line) {
1415
+ return line.replace(/`[^`]*`/g, "");
1416
+ }
1417
+ function collectMatchingLines(content, predicate) {
1418
+ const matches = [];
1419
+ const lines = content.split("\n");
1420
+ for (let index = 0; index < lines.length; index++) {
1421
+ const line = lines[index];
1422
+ if (predicate(line))
1423
+ matches.push(`line ${index + 1}: ${line.trim()}`);
1424
+ }
1425
+ return matches.slice(0, 10);
1426
+ }
1228
1427
 
1229
1428
  // src/commands/create.ts
1230
1429
  import { existsSync as existsSync9 } from "fs";
1231
1430
  import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
1232
1431
  import { dirname as dirname7, join as join9 } from "path";
1233
- async function createChange(name, summary = "", kind) {
1432
+ async function createChange(name, summary = "", kind, options = {}) {
1234
1433
  if (!name) {
1235
1434
  console.error(` ${pc.red("Usage:")} rsp create <name> [summary]`);
1236
1435
  process.exit(1);
@@ -1245,7 +1444,7 @@ async function createChange(name, summary = "", kind) {
1245
1444
  await mkdir5(dirname7(changePath), { recursive: true });
1246
1445
  const existed = existsSync9(changePath);
1247
1446
  if (!existed) {
1248
- const content = generateChangeContent(name, summary, kind);
1447
+ const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
1249
1448
  await writeFile6(changePath, content);
1250
1449
  await mkdir5(FOCUS_DIR, { recursive: true });
1251
1450
  const focusEntry = join9(FOCUS_DIR, name);
@@ -1258,37 +1457,101 @@ async function createChange(name, summary = "", kind) {
1258
1457
  console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${name}`);
1259
1458
  else
1260
1459
  console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
1261
- console.log(` ${pc.cyan("Next:")} fill proposal/spec/design first, then implement and complete the tasks
1460
+ console.log(` ${pc.cyan("Next:")} ${options.lite ? "fill the lite change details, then implement and verify" : "fill proposal/spec/design first, then implement and complete the tasks"}
1262
1461
  `);
1263
1462
  });
1264
1463
  }
1265
1464
 
1266
1465
  // src/commands/doctor.ts
1466
+ import { existsSync as existsSync11 } from "fs";
1467
+ import { readFile as readFile9 } from "fs/promises";
1468
+ import { basename as basename5, join as join11, relative as relative5 } from "path";
1469
+
1470
+ // src/commands/update.ts
1267
1471
  import { existsSync as existsSync10 } from "fs";
1268
- import { readFile as readFile8 } from "fs/promises";
1269
- import { basename as basename5, join as join10, relative as relative5 } from "path";
1472
+ import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
1473
+ import { join as join10 } from "path";
1474
+ var SKILL_REFRESH_HINT = " Note: if you use the published RSP skill, refresh it too:\n npx skills add oevery/rsp\n";
1475
+ async function updateProject(options = {}) {
1476
+ if (!existsSync10(RSP_DIR)) {
1477
+ console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
1478
+ console.error(` ${pc.dim("Run: rsp init")}`);
1479
+ process.exit(1);
1480
+ }
1481
+ return withRspLock("update", async () => {
1482
+ let updated = false;
1483
+ const actions = [];
1484
+ const bundledRules = await readFile8(join10(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
1485
+ const rulesPath = join10(RSP_DIR, "rules", "rsp-rules.md");
1486
+ await mkdir6(join10(RSP_DIR, "rules"), { recursive: true });
1487
+ const existingRules = existsSync10(rulesPath) ? await readFile8(rulesPath, "utf-8") : null;
1488
+ if (existingRules !== bundledRules) {
1489
+ await writeFile7(rulesPath, bundledRules);
1490
+ actions.push("rules/rsp-rules.md updated");
1491
+ if (!options.quiet)
1492
+ console.log(` ${pc.green("\u2713")} rules/rsp-rules.md updated`);
1493
+ updated = true;
1494
+ }
1495
+ const agentsPath = "AGENTS.md";
1496
+ const projectName = await detectProjectName();
1497
+ const baseAgents = existsSync10(agentsPath) ? await readFile8(agentsPath, "utf-8") : `# ${projectName}
1498
+
1499
+ `;
1500
+ const nextAgents = upsertRspAgentsBlock(baseAgents);
1501
+ if (!existsSync10(agentsPath) || nextAgents.changed) {
1502
+ await writeFile7(agentsPath, nextAgents.content);
1503
+ actions.push("AGENTS.md managed block refreshed");
1504
+ if (!options.quiet)
1505
+ console.log(` ${pc.green("\u2713")} AGENTS.md managed block refreshed`);
1506
+ updated = true;
1507
+ }
1508
+ const specsIndexChanged = await buildSpecsIndex({ acquireLock: false, quiet: options.quiet });
1509
+ const archiveIndexChanged = await buildArchiveIndex({ acquireLock: false, quiet: options.quiet });
1510
+ if (specsIndexChanged || archiveIndexChanged) {
1511
+ actions.push("generated indexes rebuilt");
1512
+ updated = true;
1513
+ }
1514
+ if (!options.quiet && !updated) {
1515
+ console.log(` ${pc.dim("Already up to date.")}
1516
+ `);
1517
+ console.log(SKILL_REFRESH_HINT);
1518
+ } else if (!options.quiet) {
1519
+ console.log(` ${pc.green("Update complete.")}
1520
+ `);
1521
+ console.log(SKILL_REFRESH_HINT);
1522
+ }
1523
+ return actions;
1524
+ });
1525
+ }
1526
+
1527
+ // src/commands/doctor.ts
1270
1528
  async function runDoctor(options = {}) {
1529
+ const fixed = [];
1530
+ if (options.fix && existsSync11(RSP_DIR)) {
1531
+ fixed.push(...await updateProject({ quiet: Boolean(options.json) }));
1532
+ }
1271
1533
  const checks = [];
1272
1534
  const runtime = [];
1273
1535
  const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
1274
- const rspRulesPath = join10(RSP_DIR, "rules", "rsp-rules.md");
1275
- const designPath = join10(RSP_DIR, "specs", "design.md");
1276
- const specsIndexPath = join10(RSP_DIR, "specs", "INDEX.md");
1277
- const archivesIndexPath = join10(RSP_DIR, "archives", "INDEX.md");
1278
- reportCheck(checks, ".rsp exists", existsSync10(RSP_DIR), "Run: rsp init");
1279
- reportCheck(checks, "rules/rsp-rules.md exists", existsSync10(rspRulesPath), "Run: rsp init");
1280
- reportCheck(checks, "specs/design.md exists", existsSync10(designPath), "Run: rsp init");
1281
- reportCheck(checks, "specs/INDEX.md exists", existsSync10(specsIndexPath), "Run: rsp update");
1282
- reportCheck(checks, "archives/INDEX.md exists", existsSync10(archivesIndexPath), "Run: rsp update");
1536
+ const rspRulesPath = join11(RSP_DIR, "rules", "rsp-rules.md");
1537
+ const designPath = join11(RSP_DIR, "specs", "design.md");
1538
+ const specsIndexPath = join11(RSP_DIR, "specs", "INDEX.md");
1539
+ const archivesIndexPath = join11(RSP_DIR, "archives", "INDEX.md");
1540
+ reportCheck(checks, ".rsp exists", existsSync11(RSP_DIR), "Run: rsp init");
1541
+ reportCheck(checks, "rules/rsp-rules.md exists", existsSync11(rspRulesPath), "Run: rsp init");
1542
+ reportCheck(checks, "specs/design.md exists", existsSync11(designPath), "Run: rsp init");
1543
+ reportCheck(checks, "specs/INDEX.md exists", existsSync11(specsIndexPath), "Run: rsp update");
1544
+ reportCheck(checks, "archives/INDEX.md exists", existsSync11(archivesIndexPath), "Run: rsp update");
1283
1545
  await checkAgents(checks, reportRuntime);
1284
- await checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "_Auto-generated by `rsp update`._", "specs/INDEX.md has generated signature", "Run: rsp update");
1285
- await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "_Auto-generated by `rsp update`._", "archives/INDEX.md has generated signature", "Run: rsp update");
1546
+ await checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "specs", "specs/INDEX.md has generated-index metadata", "Run: rsp update");
1547
+ await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "archives", "archives/INDEX.md has generated-index metadata", "Run: rsp update");
1286
1548
  await checkArchiveNaming(checks, reportRuntime);
1287
1549
  await checkConfigSemantics(checks, reportRuntime);
1288
1550
  await checkActiveChangeConsistency(checks, reportRuntime);
1289
1551
  const result = {
1290
1552
  command: "doctor",
1291
1553
  ok: checks.every((check) => check.status !== "issue"),
1554
+ fixed,
1292
1555
  checks,
1293
1556
  runtime,
1294
1557
  summary: {
@@ -1302,6 +1565,13 @@ async function runDoctor(options = {}) {
1302
1565
  console.log();
1303
1566
  console.log(` ${pc.bold("RSP doctor")}`);
1304
1567
  console.log();
1568
+ if (fixed.length > 0) {
1569
+ console.log(` ${pc.green("Fixed:")} ${fixed.join(", ")}`);
1570
+ console.log();
1571
+ } else if (options.fix) {
1572
+ console.log(` ${pc.dim("No safe fixes needed.")}`);
1573
+ console.log();
1574
+ }
1305
1575
  for (const check of checks)
1306
1576
  printDoctorCheck(check);
1307
1577
  if (result.summary.issues === 0)
@@ -1337,13 +1607,13 @@ function printDoctorCheck(check) {
1337
1607
  console.log(` ${pc.dim(check.hint)}`);
1338
1608
  }
1339
1609
  async function checkAgents(checks, reportRuntime) {
1340
- if (!existsSync10("AGENTS.md")) {
1610
+ if (!existsSync11("AGENTS.md")) {
1341
1611
  checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing", hint: "Run: rsp init" });
1342
1612
  return;
1343
1613
  }
1344
1614
  let agents;
1345
1615
  try {
1346
- agents = await readFile8("AGENTS.md", "utf-8");
1616
+ agents = await readFile9("AGENTS.md", "utf-8");
1347
1617
  } catch (error) {
1348
1618
  reportRuntime({
1349
1619
  code: "agents_read_failed",
@@ -1360,12 +1630,12 @@ async function checkAgents(checks, reportRuntime) {
1360
1630
  }
1361
1631
  checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
1362
1632
  }
1363
- async function checkGeneratedIndex(checks, reportRuntime, path, signature, label, fixHint) {
1364
- if (!existsSync10(path))
1633
+ async function checkGeneratedIndex(checks, reportRuntime, path, expectedIndexType, label, fixHint) {
1634
+ if (!existsSync11(path))
1365
1635
  return;
1366
1636
  let content;
1367
1637
  try {
1368
- content = await readFile8(path, "utf-8");
1638
+ content = await readFile9(path, "utf-8");
1369
1639
  } catch (error) {
1370
1640
  reportRuntime({
1371
1641
  code: "generated_index_read_failed",
@@ -1376,15 +1646,27 @@ async function checkGeneratedIndex(checks, reportRuntime, path, signature, label
1376
1646
  checks.push({ status: "issue", label, message: `${label} could not be read`, hint: fixHint });
1377
1647
  return;
1378
1648
  }
1379
- if (content.includes(signature)) {
1380
- checks.push({ status: "ok", label });
1649
+ try {
1650
+ const fm = parseFrontmatter(content);
1651
+ if (fm?.kind === "generated-index" && fm.index_type === expectedIndexType) {
1652
+ checks.push({ status: "ok", label });
1653
+ return;
1654
+ }
1655
+ } catch (error) {
1656
+ reportRuntime({
1657
+ code: "generated_index_frontmatter_parse_failed",
1658
+ operation: "parseFrontmatter",
1659
+ path,
1660
+ message: toErrorMessage(error)
1661
+ });
1662
+ checks.push({ status: "issue", label, message: `${label} could not parse metadata`, hint: fixHint });
1381
1663
  return;
1382
1664
  }
1383
1665
  checks.push({ status: "issue", label, hint: fixHint });
1384
1666
  }
1385
1667
  async function checkArchiveNaming(checks, reportRuntime) {
1386
- const archivesDir = join10(RSP_DIR, "archives");
1387
- const archiveFiles = existsSync10(archivesDir) ? (await walkMarkdownFiles(archivesDir, { onError: reportRuntime })).filter((fp) => relative5(archivesDir, fp) !== "INDEX.md") : [];
1668
+ const archivesDir = join11(RSP_DIR, "archives");
1669
+ const archiveFiles = existsSync11(archivesDir) ? (await walkMarkdownFiles(archivesDir, { onError: reportRuntime })).filter((fp) => relative5(archivesDir, fp) !== "INDEX.md") : [];
1388
1670
  const invalidFiles = archiveFiles.map((fp) => normalizeLogicalPath(relative5(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename5(fp)) === false);
1389
1671
  if (invalidFiles.length === 0) {
1390
1672
  checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
@@ -1398,11 +1680,11 @@ async function checkArchiveNaming(checks, reportRuntime) {
1398
1680
  });
1399
1681
  }
1400
1682
  async function checkConfigSemantics(checks, reportRuntime) {
1401
- if (!existsSync10(CONFIG_PATH))
1683
+ if (!existsSync11(CONFIG_PATH))
1402
1684
  return;
1403
1685
  let content;
1404
1686
  try {
1405
- content = await readFile8(CONFIG_PATH, "utf-8");
1687
+ content = await readFile9(CONFIG_PATH, "utf-8");
1406
1688
  } catch (error) {
1407
1689
  reportRuntime({
1408
1690
  code: "config_read_failed",
@@ -1458,7 +1740,7 @@ function reportConfigListField(checks, parsed, key) {
1458
1740
  });
1459
1741
  }
1460
1742
  async function checkActiveChangeConsistency(checks, reportRuntime) {
1461
- const changeFiles = existsSync10(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
1743
+ const changeFiles = existsSync11(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
1462
1744
  const changeNames = new Set(changeFiles.map((fp) => changeNameFromPath(CHANGES_DIR, fp)));
1463
1745
  const focusNames = await getFocusedChangeNames({ onError: reportRuntime });
1464
1746
  const missingChangeFiles = [...focusNames].filter((name) => !changeNames.has(name));
@@ -1488,9 +1770,9 @@ async function checkActiveChangeConsistency(checks, reportRuntime) {
1488
1770
  }
1489
1771
 
1490
1772
  // src/commands/focus.ts
1491
- import { existsSync as existsSync11 } from "fs";
1492
- import { mkdir as mkdir6, unlink as unlink3, writeFile as writeFile7 } from "fs/promises";
1493
- import { dirname as dirname8, join as join11 } from "path";
1773
+ import { existsSync as existsSync12 } from "fs";
1774
+ import { mkdir as mkdir7, unlink as unlink3, writeFile as writeFile8 } from "fs/promises";
1775
+ import { dirname as dirname8, join as join12 } from "path";
1494
1776
  async function focusChange(name) {
1495
1777
  if (!name) {
1496
1778
  console.error(` ${pc.red("Usage:")} rsp focus <name>`);
@@ -1501,16 +1783,16 @@ async function focusChange(name) {
1501
1783
  process.exit(1);
1502
1784
  }
1503
1785
  guardRspInitialized();
1504
- const changePath = join11(CHANGES_DIR, `${name}.md`);
1505
- if (!existsSync11(changePath)) {
1786
+ const changePath = join12(CHANGES_DIR, `${name}.md`);
1787
+ if (!existsSync12(changePath)) {
1506
1788
  console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
1507
1789
  process.exit(1);
1508
1790
  }
1509
1791
  return withRspLock("focus-change", async () => {
1510
- await mkdir6(FOCUS_DIR, { recursive: true });
1511
- const focusEntry = join11(FOCUS_DIR, name);
1512
- await mkdir6(dirname8(focusEntry), { recursive: true });
1513
- await writeFile7(focusEntry, "");
1792
+ await mkdir7(FOCUS_DIR, { recursive: true });
1793
+ const focusEntry = join12(FOCUS_DIR, name);
1794
+ await mkdir7(dirname8(focusEntry), { recursive: true });
1795
+ await writeFile8(focusEntry, "");
1514
1796
  console.log(` ${pc.green("Focused:")} ${name}`);
1515
1797
  console.log(` ${pc.dim("focus.d")} \u2192 ${name}`);
1516
1798
  console.log();
@@ -1526,8 +1808,8 @@ async function unfocusChange(name) {
1526
1808
  process.exit(1);
1527
1809
  }
1528
1810
  guardRspInitialized();
1529
- const focusEntry = join11(FOCUS_DIR, name);
1530
- if (!existsSync11(focusEntry)) {
1811
+ const focusEntry = join12(FOCUS_DIR, name);
1812
+ if (!existsSync12(focusEntry)) {
1531
1813
  console.error(` ${pc.red("Focus marker not found:")} .rsp/focus.d/${name}`);
1532
1814
  process.exit(1);
1533
1815
  }
@@ -1541,9 +1823,9 @@ async function unfocusChange(name) {
1541
1823
  }
1542
1824
 
1543
1825
  // src/commands/init.ts
1544
- import { existsSync as existsSync12 } from "fs";
1545
- import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
1546
- import { join as join12 } from "path";
1826
+ import { existsSync as existsSync13 } from "fs";
1827
+ import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile9 } from "fs/promises";
1828
+ import { join as join13 } from "path";
1547
1829
  function generateConfigTemplate() {
1548
1830
  const fmtList = (items, indent) => items.map((i) => `${" ".repeat(indent)}# - ${i}`).join("\n");
1549
1831
  return `# RSP project configuration
@@ -1557,16 +1839,16 @@ ${fmtList(VALID_KINDS, 2)}
1557
1839
  `;
1558
1840
  }
1559
1841
  async function ensureFile(path, content) {
1560
- if (existsSync12(path))
1842
+ if (existsSync13(path))
1561
1843
  return false;
1562
- await writeFile8(path, content);
1844
+ await writeFile9(path, content);
1563
1845
  return true;
1564
1846
  }
1565
1847
  function toTitle(projectName) {
1566
1848
  return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
1567
1849
  }
1568
1850
  async function initProject(args = {}) {
1569
- const isNew = !existsSync12(RSP_DIR);
1851
+ const isNew = !existsSync13(RSP_DIR);
1570
1852
  const projectName = await detectProjectName();
1571
1853
  const mode = args.agentsMode ?? "managed";
1572
1854
  if (mode !== "managed" && mode !== "print") {
@@ -1575,32 +1857,32 @@ async function initProject(args = {}) {
1575
1857
  }
1576
1858
  return withRspLock("init", async () => {
1577
1859
  const dirs = [
1578
- join12(RSP_DIR, "rules"),
1579
- join12(RSP_DIR, "specs"),
1860
+ join13(RSP_DIR, "rules"),
1861
+ join13(RSP_DIR, "specs"),
1580
1862
  CHANGES_DIR,
1581
1863
  FOCUS_DIR,
1582
- join12(RSP_DIR, "archives")
1864
+ join13(RSP_DIR, "archives")
1583
1865
  ];
1584
1866
  for (const d of dirs)
1585
- await mkdir7(d, { recursive: true });
1867
+ await mkdir8(d, { recursive: true });
1586
1868
  let created = false;
1587
- const bundledRules = await readFile9(join12(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
1588
- created = await ensureFile(join12(RSP_DIR, "rules", "rsp-rules.md"), bundledRules) || created;
1589
- created = await ensureFile(join12(RSP_DIR, "config.yaml"), generateConfigTemplate()) || created;
1590
- const createdSpecsIndex = await ensureFile(join12(RSP_DIR, "specs", "INDEX.md"), "# Specs Index\n\n_Additional project-level specs beyond `design.md`._\n");
1869
+ const bundledRules = await readFile10(join13(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
1870
+ created = await ensureFile(join13(RSP_DIR, "rules", "rsp-rules.md"), bundledRules) || created;
1871
+ created = await ensureFile(join13(RSP_DIR, "config.yaml"), generateConfigTemplate()) || created;
1872
+ const createdSpecsIndex = await ensureFile(join13(RSP_DIR, "specs", "INDEX.md"), "# Specs Index\n\n_Additional project-level specs beyond `design.md`._\n");
1591
1873
  created = createdSpecsIndex || created;
1592
- const createdDesign = await ensureFile(join12(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
1874
+ const createdDesign = await ensureFile(join13(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
1593
1875
  created = createdDesign || created;
1594
- const createdArchivesIndex = await ensureFile(join12(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
1876
+ const createdArchivesIndex = await ensureFile(join13(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
1595
1877
  created = createdArchivesIndex || created;
1596
- created = await ensureFile(join12(CHANGES_DIR, ".gitkeep"), "") || created;
1597
- created = await ensureFile(join12(FOCUS_DIR, ".gitkeep"), "") || created;
1598
- created = await ensureFile(join12(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
1878
+ created = await ensureFile(join13(CHANGES_DIR, ".gitkeep"), "") || created;
1879
+ created = await ensureFile(join13(FOCUS_DIR, ".gitkeep"), "") || created;
1880
+ created = await ensureFile(join13(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
1599
1881
  if (args.withProjectRules)
1600
- created = await ensureFile(join12(RSP_DIR, "rules", "project-rules.md"), generateProjectRulesContent(projectName)) || created;
1882
+ created = await ensureFile(join13(RSP_DIR, "rules", "project-rules.md"), generateProjectRulesContent(projectName)) || created;
1601
1883
  if (args.withProjectSetup) {
1602
- const changePath = join12(CHANGES_DIR, "project-setup.md");
1603
- const focusMarker = join12(FOCUS_DIR, "project-setup");
1884
+ const changePath = join13(CHANGES_DIR, "project-setup.md");
1885
+ const focusMarker = join13(FOCUS_DIR, "project-setup");
1604
1886
  created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
1605
1887
  created = await ensureFile(focusMarker, "") || created;
1606
1888
  }
@@ -1611,24 +1893,24 @@ async function initProject(args = {}) {
1611
1893
  const agentsPath = "AGENTS.md";
1612
1894
  const title = toTitle(projectName);
1613
1895
  let agentsUpdated = false;
1614
- if (!existsSync12(agentsPath)) {
1615
- await writeFile8(agentsPath, `# ${title}
1896
+ if (!existsSync13(agentsPath)) {
1897
+ await writeFile9(agentsPath, `# ${title}
1616
1898
 
1617
1899
  ${upsertRspAgentsBlock("").content}
1618
1900
  `);
1619
1901
  created = true;
1620
1902
  agentsUpdated = true;
1621
1903
  } else {
1622
- const existing = await readFile9(agentsPath, "utf-8");
1904
+ const existing = await readFile10(agentsPath, "utf-8");
1623
1905
  const next = upsertRspAgentsBlock(existing);
1624
1906
  if (next.changed) {
1625
- await writeFile8(agentsPath, next.content);
1907
+ await writeFile9(agentsPath, next.content);
1626
1908
  created = true;
1627
1909
  agentsUpdated = true;
1628
1910
  }
1629
1911
  }
1630
1912
  if (mode === "print") {
1631
- const managed = await readFile9(agentsPath, "utf-8");
1913
+ const managed = await readFile10(agentsPath, "utf-8");
1632
1914
  console.log(`
1633
1915
  ${managed}
1634
1916
  `);
@@ -1665,10 +1947,258 @@ ${managed}
1665
1947
  });
1666
1948
  }
1667
1949
 
1950
+ // src/commands/ready.ts
1951
+ import { existsSync as existsSync14 } from "fs";
1952
+ import { readFile as readFile11 } from "fs/promises";
1953
+ import { join as join14 } from "path";
1954
+ async function showReady(name, options = {}) {
1955
+ if (!name) {
1956
+ console.error(` ${pc.red("Usage:")} rsp ready <name>`);
1957
+ process.exit(1);
1958
+ }
1959
+ if (!isValidChangeName(name)) {
1960
+ console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
1961
+ process.exit(1);
1962
+ }
1963
+ guardRspInitialized();
1964
+ const srcPath = join14(CHANGES_DIR, `${name}.md`);
1965
+ if (!existsSync14(srcPath)) {
1966
+ console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
1967
+ process.exit(1);
1968
+ }
1969
+ const runtime = [];
1970
+ let content;
1971
+ try {
1972
+ content = await readFile11(srcPath, "utf-8");
1973
+ } catch {
1974
+ console.error(` ${pc.red("Error:")} unable to read .rsp/changes/${name}.md`);
1975
+ process.exit(1);
1976
+ }
1977
+ const readinessDetails = collectArchiveReadiness(content);
1978
+ const checklist = readinessDetails.warnings;
1979
+ const readiness = {
1980
+ incompleteTasks: readinessDetails.taskTodos.length,
1981
+ incompleteVerify: readinessDetails.verifyTodos.length,
1982
+ activeBlockers: readinessDetails.activeBlockers,
1983
+ missingScenarios: readinessDetails.missingScenarios,
1984
+ deterministic: readinessDetails.deterministic,
1985
+ semantic: readinessDetails.semantic,
1986
+ archiveReady: readinessDetails.archiveReady
1987
+ };
1988
+ const durableReview = buildDurableReviewGuidance(getReadyDurableReviewCandidateTargets());
1989
+ const result = {
1990
+ command: "ready",
1991
+ ok: true,
1992
+ change: name,
1993
+ path: normalizeLogicalPath(srcPath),
1994
+ readiness,
1995
+ durableReview,
1996
+ warnings: checklist,
1997
+ runtime
1998
+ };
1999
+ if (options.json) {
2000
+ emitJson(result);
2001
+ return result;
2002
+ }
2003
+ console.log();
2004
+ console.log(` ${pc.bold("Archive readiness for")} ${pc.cyan(name)}`);
2005
+ console.log();
2006
+ if (checklist.length === 0) {
2007
+ console.log(` ${pc.green("\u2713")} Ready to archive. No deterministic warnings found.
2008
+ `);
2009
+ } else {
2010
+ for (const line of checklist)
2011
+ console.log(` ${pc.yellow("\u26A0")} ${line}`);
2012
+ console.log();
2013
+ console.log(` ${pc.dim("Review the warnings above before treating this work as fully closed.")}`);
2014
+ console.log(` ${pc.dim("Run:")} rsp archive ${name}
2015
+ `);
2016
+ }
2017
+ console.log(` ${pc.dim("Deterministic readiness:")} ${readiness.deterministic === "pass" ? pc.green("pass") : pc.yellow("warnings")}`);
2018
+ console.log(` ${pc.dim("Semantic review:")} ${pc.yellow("needed")}`);
2019
+ console.log(` ${pc.dim("Archive ready:")} ${formatArchiveReady(readiness.archiveReady)}
2020
+ `);
2021
+ console.log(` ${pc.bold("Durable review:")}`);
2022
+ console.log(` ${pc.dim("Decision options:")} ${durableReview.decisions.join(" | ")}`);
2023
+ console.log(` ${pc.dim("Candidate targets:")} ${durableReview.candidateTargets.join(", ")}`);
2024
+ console.log(` ${pc.dim(durableReview.note)}
2025
+ `);
2026
+ return result;
2027
+ }
2028
+ function getReadyDurableReviewCandidateTargets() {
2029
+ return getDurableReviewCandidateTargets({
2030
+ projectRulesExists: existsSync14(join14(RSP_DIR, "rules", "project-rules.md"))
2031
+ });
2032
+ }
2033
+ function formatArchiveReady(value) {
2034
+ switch (value) {
2035
+ case "yes":
2036
+ return pc.green("yes");
2037
+ case "no":
2038
+ return pc.yellow("no");
2039
+ case "judgment":
2040
+ return pc.yellow("judgment");
2041
+ }
2042
+ }
2043
+
2044
+ // src/commands/show.ts
2045
+ import { existsSync as existsSync15 } from "fs";
2046
+ import { readFile as readFile12 } from "fs/promises";
2047
+ import { join as join15 } from "path";
2048
+ function exitShowError(error, options) {
2049
+ const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
2050
+ if (options.json) {
2051
+ emitJson({
2052
+ command: "show",
2053
+ ok: false,
2054
+ change: null,
2055
+ contextPaths: [],
2056
+ runtime: [],
2057
+ nextActions,
2058
+ error
2059
+ });
2060
+ } else {
2061
+ console.error(` ${pc.red("Error:")} ${error.message}`);
2062
+ for (const action of nextActions)
2063
+ console.error(` ${pc.dim(action)}`);
2064
+ }
2065
+ process.exit(1);
2066
+ }
2067
+ async function showChange(nameOrFocused, options = {}) {
2068
+ const runtime = [];
2069
+ const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
2070
+ guardRspInitialized();
2071
+ let name;
2072
+ if (options.focused) {
2073
+ const focused = await getFocusedChangeNames({ onError: reportRuntime });
2074
+ if (focused.size === 0) {
2075
+ exitShowError({ code: "no_focused_change", message: "no focused change exists" }, options);
2076
+ }
2077
+ if (focused.size > 1) {
2078
+ exitShowError({
2079
+ code: "multiple_focused_changes",
2080
+ message: `multiple focused changes exist (${[...focused].join(", ")}). Specify a name or focus exactly one change.`
2081
+ }, options);
2082
+ }
2083
+ name = [...focused][0];
2084
+ } else if (nameOrFocused) {
2085
+ if (!isValidChangeName(nameOrFocused)) {
2086
+ exitShowError({
2087
+ code: "invalid_change_name",
2088
+ message: "change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)"
2089
+ }, options);
2090
+ }
2091
+ name = nameOrFocused;
2092
+ } else {
2093
+ exitShowError({ code: "missing_change_name", message: "Usage: rsp show <name|--focused> [--json] [--verbose]" }, options);
2094
+ }
2095
+ const srcPath = join15(CHANGES_DIR, `${name}.md`);
2096
+ if (!existsSync15(srcPath)) {
2097
+ exitShowError({ code: "change_not_found", message: `.rsp/changes/${name}.md not found` }, options);
2098
+ }
2099
+ let content;
2100
+ try {
2101
+ content = await readFile12(srcPath, "utf-8");
2102
+ } catch {
2103
+ exitShowError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, options);
2104
+ }
2105
+ let kind = "\u2014";
2106
+ try {
2107
+ const fm = parseFrontmatter(content);
2108
+ kind = fm?.kind ? String(fm.kind) : "\u2014";
2109
+ } catch (error) {
2110
+ kind = "(invalid)";
2111
+ reportRuntime({
2112
+ code: "frontmatter_parse_failed",
2113
+ operation: "parseFrontmatter",
2114
+ path: srcPath,
2115
+ message: toErrorMessage(error)
2116
+ });
2117
+ }
2118
+ const focusedSet = await getFocusedChangeNames({ onError: reportRuntime });
2119
+ const isFocused = focusedSet.has(name);
2120
+ const cb = countCheckboxes(content);
2121
+ const blockers = hasMeaningfulBlockers(content);
2122
+ const scenarios = parseScenarios(content);
2123
+ const readinessDetails = collectArchiveReadiness(content);
2124
+ const readiness = {
2125
+ incompleteTasks: readinessDetails.taskTodos.length,
2126
+ incompleteVerify: readinessDetails.verifyTodos.length,
2127
+ activeBlockers: readinessDetails.activeBlockers,
2128
+ missingScenarios: readinessDetails.missingScenarios,
2129
+ deterministic: readinessDetails.deterministic,
2130
+ semantic: readinessDetails.semantic,
2131
+ archiveReady: readinessDetails.archiveReady
2132
+ };
2133
+ const contextPaths = getDurableReviewCandidateTargets({
2134
+ projectRulesExists: existsSync15(join15(RSP_DIR, "rules", "project-rules.md"))
2135
+ });
2136
+ const durableReview = buildDurableReviewGuidance(contextPaths);
2137
+ const result = {
2138
+ command: "show",
2139
+ ok: true,
2140
+ change: {
2141
+ name,
2142
+ path: normalizeLogicalPath(srcPath),
2143
+ kind,
2144
+ isFocused,
2145
+ progress: { done: cb.done, total: cb.total },
2146
+ blockers,
2147
+ scenarioCount: scenarios.length,
2148
+ readiness
2149
+ },
2150
+ contextPaths,
2151
+ durableReview,
2152
+ runtime
2153
+ };
2154
+ if (options.json) {
2155
+ emitJson(result);
2156
+ return result;
2157
+ }
2158
+ console.log();
2159
+ console.log(` ${pc.bold("Change:")} ${pc.cyan(name)}`);
2160
+ console.log(` ${pc.dim("Path:")} ${normalizeLogicalPath(srcPath)}`);
2161
+ console.log(` ${pc.dim("Kind:")} ${kind}`);
2162
+ console.log(` ${pc.dim("Focused:")} ${isFocused ? pc.green("yes") : pc.dim("no")}`);
2163
+ console.log(` ${pc.dim("Progress:")} ${cb.done}/${cb.total}`);
2164
+ console.log(` ${pc.dim("Blockers:")} ${blockers ? pc.yellow("yes") : pc.green("no")}`);
2165
+ console.log(` ${pc.dim("Scenarios:")} ${scenarios.length}`);
2166
+ console.log();
2167
+ console.log(` ${pc.bold("Readiness:")}`);
2168
+ console.log(` ${pc.dim("Incomplete tasks:")} ${readiness.incompleteTasks > 0 ? pc.yellow(String(readiness.incompleteTasks)) : pc.green("0")}`);
2169
+ console.log(` ${pc.dim("Incomplete verify:")} ${readiness.incompleteVerify > 0 ? pc.yellow(String(readiness.incompleteVerify)) : pc.green("0")}`);
2170
+ console.log(` ${pc.dim("Active blockers:")} ${readiness.activeBlockers ? pc.yellow("yes") : pc.green("no")}`);
2171
+ console.log(` ${pc.dim("Missing scenarios:")} ${readiness.missingScenarios ? pc.yellow("yes") : pc.green("no")}`);
2172
+ console.log(` ${pc.dim("Deterministic:")} ${readiness.deterministic === "pass" ? pc.green("pass") : pc.yellow("warnings")}`);
2173
+ console.log(` ${pc.dim("Semantic review:")} ${pc.yellow("needed")}`);
2174
+ console.log(` ${pc.dim("Archive ready:")} ${formatArchiveReady2(readiness.archiveReady)}`);
2175
+ console.log();
2176
+ console.log(` ${pc.bold("Context paths:")}`);
2177
+ for (const cp of contextPaths)
2178
+ console.log(` ${pc.dim(cp)}`);
2179
+ console.log();
2180
+ console.log(` ${pc.bold("Durable review:")}`);
2181
+ console.log(` ${pc.dim("Decision options:")} ${durableReview.decisions.join(" | ")}`);
2182
+ console.log(` ${pc.dim("Candidate targets:")} ${durableReview.candidateTargets.join(", ")}`);
2183
+ console.log(` ${pc.dim(durableReview.note)}`);
2184
+ console.log();
2185
+ return result;
2186
+ }
2187
+ function formatArchiveReady2(value) {
2188
+ switch (value) {
2189
+ case "yes":
2190
+ return pc.green("yes");
2191
+ case "no":
2192
+ return pc.yellow("no");
2193
+ case "judgment":
2194
+ return pc.yellow("judgment");
2195
+ }
2196
+ }
2197
+
1668
2198
  // src/commands/status.ts
1669
- import { existsSync as existsSync13 } from "fs";
1670
- import { readFile as readFile10, stat } from "fs/promises";
1671
- import { join as join13 } from "path";
2199
+ import { existsSync as existsSync16 } from "fs";
2200
+ import { readFile as readFile13, stat } from "fs/promises";
2201
+ import { join as join16 } from "path";
1672
2202
  var COL_CHANGE = 32;
1673
2203
  var COL_KIND = 10;
1674
2204
  var COL_AGE = 7;
@@ -1677,7 +2207,7 @@ async function showStatus(options = {}, runOptions = {}) {
1677
2207
  const runtime = [];
1678
2208
  const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(runOptions.verbose) && !runOptions.json);
1679
2209
  const focusedSet = await getFocusedChangeNames({ onError: reportRuntime });
1680
- const changeFiles = existsSync13(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
2210
+ const changeFiles = existsSync16(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
1681
2211
  const changeMap = {};
1682
2212
  for (const fp of changeFiles)
1683
2213
  changeMap[changeNameFromPath(CHANGES_DIR, fp)] = fp;
@@ -1695,7 +2225,7 @@ async function showStatus(options = {}, runOptions = {}) {
1695
2225
  let total = 0;
1696
2226
  if (fp) {
1697
2227
  try {
1698
- const content = await readFile10(fp, "utf-8");
2228
+ const content = await readFile13(fp, "utf-8");
1699
2229
  try {
1700
2230
  const fm = parseFrontmatter(content);
1701
2231
  kind = fm?.kind ? String(fm.kind) : "\u2014";
@@ -1737,7 +2267,7 @@ async function showStatus(options = {}, runOptions = {}) {
1737
2267
  reportRuntime({
1738
2268
  code: "focused_change_missing",
1739
2269
  operation: "resolveChange",
1740
- path: join13(CHANGES_DIR, `${name}.md`),
2270
+ path: join16(CHANGES_DIR, `${name}.md`),
1741
2271
  message: "focus marker points to a missing change file"
1742
2272
  });
1743
2273
  }
@@ -1777,6 +2307,7 @@ async function showStatus(options = {}, runOptions = {}) {
1777
2307
  return true;
1778
2308
  });
1779
2309
  const blockedCount = filteredOutputRecords.filter((r) => r.isBlocked).length;
2310
+ const nextActions = buildStatusNextActions(focusedSet.size, Object.keys(changeMap).sort(), Boolean(options.focused || options.blocked || typeof options.stale === "number"));
1780
2311
  const statusResult = {
1781
2312
  command: "status",
1782
2313
  ok: true,
@@ -1792,6 +2323,7 @@ async function showStatus(options = {}, runOptions = {}) {
1792
2323
  focused: filteredOutputRecords.filter((r) => r.isFocused).length,
1793
2324
  blocked: blockedCount
1794
2325
  },
2326
+ nextActions,
1795
2327
  archiveTrend: await readArchiveTrend(runtime, runOptions),
1796
2328
  runtime
1797
2329
  };
@@ -1806,7 +2338,9 @@ async function showStatus(options = {}, runOptions = {}) {
1806
2338
  console.log(` ${pc.cyan("Focused:")} ${[...focusedSet].join(", ")}`);
1807
2339
  console.log();
1808
2340
  } else {
1809
- console.log(` ${pc.dim("No focused change.")} Run: rsp create <name> or rsp focus <name>`);
2341
+ console.log(` ${pc.dim("No focused change.")}`);
2342
+ for (const action of nextActions)
2343
+ console.log(` ${pc.dim(action)}`);
1810
2344
  console.log();
1811
2345
  }
1812
2346
  if (allNames.length === 0) {
@@ -1843,12 +2377,32 @@ async function showStatus(options = {}, runOptions = {}) {
1843
2377
  }
1844
2378
  return statusResult;
1845
2379
  }
2380
+ function buildStatusNextActions(focusedCount, openChanges, filtered) {
2381
+ if (focusedCount > 0)
2382
+ return [];
2383
+ if (openChanges.length === 0)
2384
+ return ["Run: rsp create <name>"];
2385
+ if (filtered) {
2386
+ return [
2387
+ "Run: rsp status",
2388
+ "Run: rsp focus <name>",
2389
+ "Or run: rsp create <name>"
2390
+ ];
2391
+ }
2392
+ const firstChanges = openChanges.slice(0, 3).join(", ");
2393
+ const extra = openChanges.length > 3 ? ` (+${openChanges.length - 3} more)` : "";
2394
+ return [
2395
+ `Open changes: ${firstChanges}${extra}`,
2396
+ `Run: rsp focus ${openChanges[0]}`,
2397
+ "Or run: rsp create <name>"
2398
+ ];
2399
+ }
1846
2400
  async function readArchiveTrend(runtime, runOptions) {
1847
- const indexPath = join13(ARCHIVES_DIR, "INDEX.md");
1848
- if (!existsSync13(indexPath))
2401
+ const indexPath = join16(ARCHIVES_DIR, "INDEX.md");
2402
+ if (!existsSync16(indexPath))
1849
2403
  return [];
1850
2404
  try {
1851
- const content = await readFile10(indexPath, "utf-8");
2405
+ const content = await readFile13(indexPath, "utf-8");
1852
2406
  const dateRe = /^\| (\d{4}-\d{2})-\d{2} \|/gm;
1853
2407
  const counts = {};
1854
2408
  let match;
@@ -1879,53 +2433,6 @@ function printArchiveTrend(trend) {
1879
2433
  console.log();
1880
2434
  }
1881
2435
 
1882
- // src/commands/update.ts
1883
- import { existsSync as existsSync14 } from "fs";
1884
- import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
1885
- import { join as join14 } from "path";
1886
- var SKILL_REFRESH_HINT = " Note: if you use the published RSP skill, refresh it too:\n npx skills add oevery/rsp\n";
1887
- async function updateProject() {
1888
- if (!existsSync14(RSP_DIR)) {
1889
- console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
1890
- console.error(` ${pc.dim("Run: rsp init")}`);
1891
- process.exit(1);
1892
- }
1893
- return withRspLock("update", async () => {
1894
- let updated = false;
1895
- const bundledRules = await readFile11(join14(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
1896
- const rulesPath = join14(RSP_DIR, "rules", "rsp-rules.md");
1897
- await mkdir8(join14(RSP_DIR, "rules"), { recursive: true });
1898
- const existingRules = existsSync14(rulesPath) ? await readFile11(rulesPath, "utf-8") : null;
1899
- if (existingRules !== bundledRules) {
1900
- await writeFile9(rulesPath, bundledRules);
1901
- console.log(` ${pc.green("\u2713")} rules/rsp-rules.md updated`);
1902
- updated = true;
1903
- }
1904
- const agentsPath = "AGENTS.md";
1905
- const projectName = await detectProjectName();
1906
- const baseAgents = existsSync14(agentsPath) ? await readFile11(agentsPath, "utf-8") : `# ${projectName}
1907
-
1908
- `;
1909
- const nextAgents = upsertRspAgentsBlock(baseAgents);
1910
- if (!existsSync14(agentsPath) || nextAgents.changed) {
1911
- await writeFile9(agentsPath, nextAgents.content);
1912
- console.log(` ${pc.green("\u2713")} AGENTS.md managed block refreshed`);
1913
- updated = true;
1914
- }
1915
- await buildSpecsIndex({ acquireLock: false });
1916
- await buildArchiveIndex({ acquireLock: false });
1917
- if (!updated) {
1918
- console.log(` ${pc.dim("Already up to date.")}
1919
- `);
1920
- console.log(SKILL_REFRESH_HINT);
1921
- } else {
1922
- console.log(` ${pc.green("Update complete.")}
1923
- `);
1924
- console.log(SKILL_REFRESH_HINT);
1925
- }
1926
- });
1927
- }
1928
-
1929
2436
  // src/cli.ts
1930
2437
  var initCommand = defineCommand({
1931
2438
  meta: {
@@ -1971,11 +2478,16 @@ var createCommand = defineCommand({
1971
2478
  kind: {
1972
2479
  type: "string",
1973
2480
  description: "Optional kind for a kind-aware template (feature, fix, refactor, docs, ops, research)"
2481
+ },
2482
+ lite: {
2483
+ type: "boolean",
2484
+ description: "Use a shorter change template while keeping the required RSP sections",
2485
+ default: false
1974
2486
  }
1975
2487
  },
1976
2488
  async run({ args }) {
1977
2489
  const summary = Array.isArray(args._) && args._.length > 1 ? args._.slice(1).join(" ") : "";
1978
- await createChange(args.name, summary, args.kind);
2490
+ await createChange(args.name, summary, args.kind, { lite: Boolean(args.lite) });
1979
2491
  }
1980
2492
  });
1981
2493
  var addRulesCommand = defineCommand({
@@ -2026,14 +2538,19 @@ var archiveCommand = defineCommand({
2026
2538
  description: "Archive an open change into .rsp/archives/"
2027
2539
  },
2028
2540
  args: {
2029
- name: {
2541
+ "name": {
2030
2542
  type: "positional",
2031
2543
  description: "Change name",
2032
2544
  required: true
2545
+ },
2546
+ "dry-run": {
2547
+ type: "boolean",
2548
+ description: "Preview archive readiness without moving the change",
2549
+ default: false
2033
2550
  }
2034
2551
  },
2035
2552
  async run({ args }) {
2036
- await archiveChange(args.name);
2553
+ await archiveChange(args.name, { dryRun: Boolean(args["dry-run"]) });
2037
2554
  }
2038
2555
  });
2039
2556
  var focusCommand = defineCommand({
@@ -2128,9 +2645,14 @@ var statusCommand = defineCommand({
2128
2645
  var checkCommand = defineCommand({
2129
2646
  meta: {
2130
2647
  name: "check",
2131
- description: "Validate changes (frontmatter, sections, deltas, scenarios)"
2648
+ description: "Validate changes (frontmatter, sections, template text, deltas, scenarios)"
2132
2649
  },
2133
2650
  args: {
2651
+ focused: {
2652
+ type: "boolean",
2653
+ description: "Only validate currently focused changes",
2654
+ default: false
2655
+ },
2134
2656
  json: {
2135
2657
  type: "boolean",
2136
2658
  description: "Print machine-readable JSON output",
@@ -2143,7 +2665,11 @@ var checkCommand = defineCommand({
2143
2665
  }
2144
2666
  },
2145
2667
  async run({ args }) {
2146
- const result = await runCheck({ json: Boolean(args.json), verbose: Boolean(args.verbose) });
2668
+ const result = await runCheck({
2669
+ focused: Boolean(args.focused),
2670
+ json: Boolean(args.json),
2671
+ verbose: Boolean(args.verbose)
2672
+ });
2147
2673
  if (!result.ok)
2148
2674
  process.exit(1);
2149
2675
  }
@@ -2172,14 +2698,80 @@ var doctorCommand = defineCommand({
2172
2698
  type: "boolean",
2173
2699
  description: "Print runtime diagnostics for suppressed I/O issues",
2174
2700
  default: false
2701
+ },
2702
+ fix: {
2703
+ type: "boolean",
2704
+ description: "Run safe deterministic repairs before reporting diagnostics",
2705
+ default: false
2175
2706
  }
2176
2707
  },
2177
2708
  async run({ args }) {
2178
- const result = await runDoctor({ json: Boolean(args.json), verbose: Boolean(args.verbose) });
2709
+ const result = await runDoctor({ json: Boolean(args.json), verbose: Boolean(args.verbose), fix: Boolean(args.fix) });
2179
2710
  if (!result.ok)
2180
2711
  process.exit(1);
2181
2712
  }
2182
2713
  });
2714
+ var readyCommand = defineCommand({
2715
+ meta: {
2716
+ name: "ready",
2717
+ description: "Preview archive readiness for a change without moving it"
2718
+ },
2719
+ args: {
2720
+ name: {
2721
+ type: "positional",
2722
+ description: "Change name",
2723
+ required: true
2724
+ },
2725
+ json: {
2726
+ type: "boolean",
2727
+ description: "Print machine-readable JSON output",
2728
+ default: false
2729
+ },
2730
+ verbose: {
2731
+ type: "boolean",
2732
+ description: "Print runtime diagnostics for suppressed I/O issues",
2733
+ default: false
2734
+ }
2735
+ },
2736
+ async run({ args }) {
2737
+ await showReady(args.name, { json: Boolean(args.json), verbose: Boolean(args.verbose) });
2738
+ }
2739
+ });
2740
+ var showCommand = defineCommand({
2741
+ meta: {
2742
+ name: "show",
2743
+ description: "Show change context with readiness signals and relevant paths"
2744
+ },
2745
+ args: {
2746
+ name: {
2747
+ type: "positional",
2748
+ description: "Change name (use --focused instead to show the currently focused change)",
2749
+ required: false
2750
+ },
2751
+ focused: {
2752
+ type: "boolean",
2753
+ description: "Show the currently focused change",
2754
+ default: false
2755
+ },
2756
+ json: {
2757
+ type: "boolean",
2758
+ description: "Print machine-readable JSON output",
2759
+ default: false
2760
+ },
2761
+ verbose: {
2762
+ type: "boolean",
2763
+ description: "Print runtime diagnostics for suppressed I/O issues",
2764
+ default: false
2765
+ }
2766
+ },
2767
+ async run({ args }) {
2768
+ await showChange(args.name, {
2769
+ focused: Boolean(args.focused),
2770
+ json: Boolean(args.json),
2771
+ verbose: Boolean(args.verbose)
2772
+ });
2773
+ }
2774
+ });
2183
2775
  async function runCli(rawArgs = process.argv.slice(2)) {
2184
2776
  const version = await getVersion();
2185
2777
  const main = defineCommand({
@@ -2195,6 +2787,8 @@ async function runCli(rawArgs = process.argv.slice(2)) {
2195
2787
  focus: focusCommand,
2196
2788
  unfocus: unfocusCommand,
2197
2789
  archive: archiveCommand,
2790
+ ready: readyCommand,
2791
+ show: showCommand,
2198
2792
  status: statusCommand,
2199
2793
  check: checkCommand,
2200
2794
  update: updateCommand,