@oevery/rsp 2.0.2 → 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
@@ -19,7 +19,7 @@ import pc from "picocolors";
19
19
 
20
20
  // src/core/helpers.ts
21
21
  import { existsSync } from "fs";
22
- import { readdir, readFile, rmdir, stat } from "fs/promises";
22
+ import { readdir, readFile, rmdir } from "fs/promises";
23
23
  import { basename, dirname, join, relative, sep } from "path";
24
24
  import { parse } from "yaml";
25
25
 
@@ -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 `---
@@ -166,6 +166,7 @@ kind: ops
166
166
 
167
167
  ## Tasks
168
168
  - [ ] Review the repository structure, entrypoints, and primary outputs
169
+ - [ ] Review AGENTS.md and confirm the RSP entry points to the right project files
169
170
  - [ ] Fill .rsp/specs/design.md with durable architecture facts
170
171
  - [ ] Add .rsp/rules/project-rules.md if stable local rules or validation steps exist
171
172
 
@@ -183,6 +184,8 @@ kind: ops
183
184
  `;
184
185
  }
185
186
  const frontmatterKind = kind ?? "<choose: feature | fix | refactor | docs | ops | research>";
187
+ if (options.lite)
188
+ return generateLiteChangeContent(name, proposalSummary, frontmatterKind);
186
189
  const template = getChangeTemplateByKind(kind);
187
190
  return `---
188
191
  kind: "${frontmatterKind}"
@@ -209,12 +212,15 @@ ${template.acceptanceSection}
209
212
  - Approach:
210
213
  - ${template.approach}
211
214
  - Affected areas:
212
- - ${template.affectedAreas}
215
+ - ${template.affectedArea1}
216
+ - ${template.affectedArea2}
213
217
  - Constraints:
214
218
  - ${template.constraints}
215
219
 
216
220
  ## Tasks
221
+ - [ ] Finalize the proposal, spec, and design details for this change
217
222
  - [ ] ${template.task}
223
+ - [ ] Verify the result and update any required durable docs or rules
218
224
 
219
225
  ## Verify
220
226
  - Automated:
@@ -225,67 +231,168 @@ ${template.acceptanceSection}
225
231
  - [ ] Decide whether this change produced durable knowledge that belongs in \`.rsp/specs/\` or \`.rsp/rules/\`
226
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
227
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
+
228
286
  ## Blockers
229
287
  - none
230
288
  `;
231
289
  }
232
290
  function getChangeTemplateByKind(kind) {
233
291
  switch (kind) {
292
+ case "feature":
293
+ return {
294
+ why: "<what user need or capability gap this addresses>",
295
+ scope: "<what new behavior or capability will be delivered>",
296
+ nonGoals: "<what related capabilities are explicitly out of scope>",
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>",
298
+ acceptanceSection: "#### Scenario: user exercises the new capability\n- GIVEN <context>\n- WHEN <user action>\n- THEN <expected new behavior>",
299
+ approach: "<how the new capability will be implemented>",
300
+ affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
301
+ affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
302
+ constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
303
+ task: "<implement the new behavior and its acceptance scenario in the affected areas>",
304
+ automatedVerify: "<exact test, lint, build, or check command for the new behavior>",
305
+ manualVerify: "<exact end-to-end user scenario to validate the new capability>"
306
+ };
307
+ case "fix":
308
+ return {
309
+ why: "<what current behavior is wrong or broken>",
310
+ scope: "<what specific defect is being corrected>",
311
+ nonGoals: "<what related but separate issues are out of scope>",
312
+ specSection: "<!-- Describe expected correct behavior. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: correct behavior\n - <expected correct behavior after the fix>",
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",
314
+ approach: "<root cause analysis and fix strategy>",
315
+ affectedArea1: "<concrete file path, directory, module, or subsystem containing the defect>",
316
+ affectedArea2: "<secondary path, dependency, or test file if needed>",
317
+ constraints: "<regression, backward-compatibility, safety, or scope constraint that must hold>",
318
+ task: "<identify the root cause and apply the minimal fix>",
319
+ automatedVerify: "<exact regression test, lint, build, or check command that proves the fix>",
320
+ manualVerify: "<exact steps to reproduce the original issue and confirm it no longer occurs>"
321
+ };
322
+ case "refactor":
323
+ return {
324
+ why: "<what maintainability, readability, or structural issue motivates this>",
325
+ scope: "<what code areas are being restructured>",
326
+ nonGoals: "<no behavior change is expected or intended>",
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>",
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",
329
+ approach: "<how the code will be restructured (rename, extract, move, etc.)>",
330
+ affectedArea1: "<concrete file path, directory, module, or subsystem being refactored>",
331
+ affectedArea2: "<secondary path, dependency, or test file if needed>",
332
+ constraints: "<observable behavior must remain unchanged; compatibility, performance, and safety constraints must still hold>",
333
+ task: "<restructure the code while keeping behavior identical>",
334
+ automatedVerify: "<exact existing test, lint, build, or check command that must continue to pass>",
335
+ manualVerify: "<exact scenario to spot-check that behavior is unchanged after the refactor>"
336
+ };
234
337
  case "docs":
235
338
  return {
236
339
  why: "<why the documentation change matters>",
237
340
  scope: "<which docs, readers, or workflows will be updated>",
238
341
  nonGoals: "<what documentation behavior or audience is out of scope>",
239
- 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>",
240
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",
241
344
  approach: "<how the documentation will be updated and organized>",
242
- affectedAreas: "<doc path or documentation area>",
243
- constraints: "<durable wording, consistency, or scope constraint>",
345
+ affectedArea1: "<concrete doc path, directory, module doc, or documentation surface 1>",
346
+ affectedArea2: "<secondary doc path or reference file if needed>",
347
+ constraints: "<durable wording, consistency, scope, or audience constraint that must hold>",
244
348
  task: "<update the target documentation and supporting references>",
245
- automatedVerify: "<run markdown, docs, or link checks if applicable>",
246
- manualVerify: "<review the updated doc as a reader and confirm it is accurate>"
349
+ automatedVerify: "<exact markdown, docs, link, lint, build, or check command if applicable>",
350
+ manualVerify: "<exact reader or operator scenario to confirm the updated guidance is accurate>"
247
351
  };
248
352
  case "research":
249
353
  return {
250
354
  why: "<what question or uncertainty this research resolves>",
251
355
  scope: "<what system area, option set, or hypothesis is being examined>",
252
356
  nonGoals: "<what implementation work is explicitly out of scope>",
253
- 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>",
254
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",
255
359
  approach: "<how the investigation will be performed and what evidence will be gathered>",
256
- affectedAreas: "<code path, subsystem, or source being investigated>",
257
- constraints: "<time, evidence, or scope constraint for the investigation>",
258
- task: "<gather evidence and record the resulting recommendation or finding>",
259
- automatedVerify: "<run any command needed to confirm the observed behavior>",
260
- manualVerify: "<review the findings and confirm they answer the original question>"
360
+ affectedArea1: "<concrete file path, directory, module, subsystem, or source under investigation>",
361
+ affectedArea2: "<secondary path, dependency, environment, or evidence source if needed>",
362
+ constraints: "<time, evidence, safety, or scope constraint for the investigation>",
363
+ task: "<gather evidence, record the recommendation or finding, and note any follow-up implementation work if needed>",
364
+ automatedVerify: "<exact command, script, query, or check used to confirm the observed behavior>",
365
+ manualVerify: "<exact review of the findings to confirm they answer the original question and identify follow-up work if needed>"
261
366
  };
262
367
  case "ops":
263
368
  return {
264
369
  why: "<why this operational or environment change matters>",
265
370
  scope: "<what environment, workflow, or operational path changes>",
266
371
  nonGoals: "<what product or feature behavior will not change>",
267
- 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>",
268
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",
269
374
  approach: "<how the operational change will be applied safely>",
270
- affectedAreas: "<script, config, workflow, or environment path>",
271
- constraints: "<rollback, safety, or environment constraint>",
375
+ affectedArea1: "<concrete script, config path, workflow, environment path, or operational surface 1>",
376
+ affectedArea2: "<secondary script, config path, dependency, or environment surface if needed>",
377
+ constraints: "<rollback, safety, reliability, environment, or scope constraint that must hold>",
272
378
  task: "<apply and verify the operational change>",
273
- automatedVerify: "<run the relevant operational validation command>",
274
- manualVerify: "<exercise the target workflow and confirm the operational result>"
379
+ automatedVerify: "<exact operational validation, lint, build, deploy, or check command>",
380
+ manualVerify: "<exact operator workflow or environment scenario to confirm the operational result>"
275
381
  };
276
382
  default:
277
383
  return {
278
384
  why: "<why this change matters>",
279
385
  scope: "<what this change will do>",
280
386
  nonGoals: "<what this change will not do>",
281
- 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>",
282
388
  acceptanceSection: "#### Scenario: <name>\n- GIVEN <context>\n- WHEN <action>\n- THEN <expected outcome>",
283
389
  approach: "<how the change will be implemented>",
284
- affectedAreas: "<path or subsystem>",
285
- constraints: "<binding technical or product constraint>",
286
- task: "<implementation task>",
287
- automatedVerify: "<test command or automated check>",
288
- manualVerify: "<manual scenario to validate>"
390
+ affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
391
+ affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
392
+ constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
393
+ task: "<implement the core change in the affected areas>",
394
+ automatedVerify: "<exact test, lint, build, or check command>",
395
+ manualVerify: "<exact end-to-end scenario to validate>"
289
396
  };
290
397
  }
291
398
  }
@@ -293,6 +400,9 @@ function renderRspAgentsBlock() {
293
400
  return `${RSP_AGENTS_BEGIN}
294
401
  ## RSP Entry
295
402
 
403
+ RSP keeps durable rules, specs, and current work under \`.rsp/\`.
404
+ Treat AGENTS.md as navigation only; keep durable rules and design in \`.rsp/\`.
405
+
296
406
  Read in order:
297
407
  1. .rsp/rules/rsp-rules.md
298
408
  2. .rsp/focus.d/
@@ -300,6 +410,9 @@ Read in order:
300
410
  4. .rsp/specs/design.md
301
411
  5. .rsp/specs/INDEX.md
302
412
  6. only the relevant additional .rsp/rules/*.md and .rsp/specs/*.md files
413
+
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.
415
+ If your agent supports Agent Skills, load \`rsp\` for setup, repair, and durable-decision tasks.
303
416
  ${RSP_AGENTS_END}`;
304
417
  }
305
418
  function upsertRspAgentsBlock(content) {
@@ -332,6 +445,16 @@ function guardRspInitialized() {
332
445
  process.exit(1);
333
446
  }
334
447
  }
448
+ async function getFocusedChangeNames(options = {}) {
449
+ const names = /* @__PURE__ */ new Set();
450
+ const focusDir = join(RSP_DIR, "focus.d");
451
+ if (!existsSync(focusDir))
452
+ return names;
453
+ const entries = await walkFiles(focusDir, options);
454
+ for (const entryPath of entries)
455
+ names.add(normalizeLogicalPath(relative(focusDir, entryPath)));
456
+ return names;
457
+ }
335
458
  function getOpenCheckboxes(sectionText) {
336
459
  return sectionText.split("\n").map((line) => line.trim()).filter((line) => /^- \[[ /]\]/.test(line));
337
460
  }
@@ -478,6 +601,57 @@ function detectDeltaSections(content) {
478
601
  removed: /^###\s*REMOVED/im.test(body)
479
602
  };
480
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
+ }
481
655
  function parseScenarios(content) {
482
656
  const scenarios = [];
483
657
  const regex = /^#{3,4}\s*Scenario:\s*(.+)$/gim;
@@ -623,20 +797,30 @@ import { dirname as dirname4, join as join5 } from "path";
623
797
  import { existsSync as existsSync4 } from "fs";
624
798
  import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
625
799
  import { basename as basename2, join as join4, relative as relative2 } from "path";
626
- async function buildSpecsIndex({ acquireLock = true } = {}) {
800
+ async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
627
801
  if (acquireLock)
628
- return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false }));
802
+ return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet }));
629
803
  const specsDir = join4(RSP_DIR, "specs");
630
804
  if (!existsSync4(specsDir)) {
631
- console.log(` ${pc.dim("No specs directory.")}
805
+ if (!quiet)
806
+ console.log(` ${pc.dim("No specs directory.")}
632
807
  `);
633
- return;
808
+ return false;
634
809
  }
635
810
  const specFiles = (await walkMarkdownFiles(specsDir)).filter((fp) => {
636
811
  const name = basename2(fp);
637
812
  return name !== "INDEX.md" && name !== "design.md";
638
813
  }).sort();
639
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("");
640
824
  lines.push("# Specs Index");
641
825
  lines.push("");
642
826
  lines.push("_Additional project-level specs beyond `design.md`._");
@@ -656,11 +840,16 @@ async function buildSpecsIndex({ acquireLock = true } = {}) {
656
840
  lines.push(`| ${escapeCell(rel)} | ${escapeCell(title)} | ${escapeCell(summary || "\u2014")} |`);
657
841
  }
658
842
  }
659
- lines.push("");
660
- lines.push("_Auto-generated by `rsp update`._");
661
- await writeFile3(join4(specsDir, "INDEX.md"), lines.join("\n"));
662
- 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).
663
851
  `);
852
+ return changed;
664
853
  }
665
854
  function extractTitle(content) {
666
855
  for (const rawLine of content.split("\n")) {
@@ -727,25 +916,33 @@ import { basename as basename4, dirname as dirname6, join as join7 } from "path"
727
916
  import { existsSync as existsSync6 } from "fs";
728
917
  import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
729
918
  import { basename as basename3, dirname as dirname5, join as join6, relative as relative3 } from "path";
730
- async function buildArchiveIndex({ acquireLock = true } = {}) {
919
+ async function buildArchiveIndex({ acquireLock = true, quiet = false } = {}) {
731
920
  if (acquireLock)
732
- return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false }));
921
+ return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false, quiet }));
733
922
  if (!existsSync6(ARCHIVES_DIR)) {
734
- console.log(` ${pc.dim("No archives directory.")}
923
+ if (!quiet)
924
+ console.log(` ${pc.dim("No archives directory.")}
735
925
  `);
736
- return;
926
+ return false;
737
927
  }
738
928
  const archiveFiles = (await walkMarkdownFiles(ARCHIVES_DIR)).filter((fp) => basename3(fp) !== "INDEX.md").sort();
739
929
  const lines = [];
740
- 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("---");
741
938
  lines.push("");
742
- lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
939
+ lines.push("# Archive Index");
743
940
  lines.push("");
744
941
  if (archiveFiles.length === 0) {
745
942
  lines.push("_No archived changes yet._");
746
943
  } else {
747
- lines.push("| Date | Change | Summary |");
748
- lines.push("|------|--------|---------|");
944
+ lines.push("| Date | Change | Kind | Summary |");
945
+ lines.push("|------|--------|------|---------|");
749
946
  }
750
947
  for (const fp of archiveFiles) {
751
948
  const base = basename3(fp);
@@ -756,27 +953,39 @@ async function buildArchiveIndex({ acquireLock = true } = {}) {
756
953
  const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
757
954
  const changeName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
758
955
  let summary = "";
956
+ let kind = "";
759
957
  try {
760
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();
761
964
  const proposal = extractSection(content, "Proposal");
762
965
  const summaryLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Summary:"));
763
- if (summaryLine)
966
+ if (!summary && summaryLine)
764
967
  summary = summaryLine.slice("- Summary:".length).trim();
765
968
  } catch {
766
969
  }
767
- const summaryCell = summary ? summary.replace(/\|/g, "\\|") : "\u2014";
768
- lines.push(`| ${date} | ${changeName} | ${summaryCell} |`);
970
+ lines.push(`| ${date} | ${escapeCell2(changeName)} | ${escapeCell2(kind || "\u2014")} | ${escapeCell2(summary || "\u2014")} |`);
769
971
  }
770
- lines.push("");
771
- lines.push("_Auto-generated by `rsp update`._");
772
972
  const indexPath = join6(ARCHIVES_DIR, "INDEX.md");
773
- await writeFile5(indexPath, lines.join("\n"));
774
- 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).
775
980
  `);
981
+ return changed;
982
+ }
983
+ function escapeCell2(value) {
984
+ return value.replace(/\|/g, "\\|");
776
985
  }
777
986
 
778
987
  // src/commands/archive.ts
779
- async function archiveChange(name) {
988
+ async function archiveChange(name, options = {}) {
780
989
  if (!name) {
781
990
  console.error(` ${pc.red("Usage:")} rsp archive <name>`);
782
991
  process.exit(1);
@@ -785,11 +994,30 @@ async function archiveChange(name) {
785
994
  console.error(` ${pc.red("Error:")} change name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
786
995
  process.exit(1);
787
996
  }
997
+ guardRspInitialized();
788
998
  const srcPath = join7(CHANGES_DIR, `${name}.md`);
789
999
  if (!existsSync7(srcPath)) {
790
1000
  console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
791
1001
  process.exit(1);
792
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
+ }
793
1021
  return withRspLock("archive-change", async () => {
794
1022
  const content = await readFile6(srcPath, "utf-8");
795
1023
  const checklist = collectArchiveChecklist(content);
@@ -859,23 +1087,6 @@ function resolveArchiveName(archiveSubdir, date, base) {
859
1087
  console.error(` ${pc.red("Error:")} archive name collision exceeded ${MAX_SUFFIX} attempts for ${base}`);
860
1088
  process.exit(1);
861
1089
  }
862
- function collectArchiveChecklist(content) {
863
- const warnings = [];
864
- const tasksSection = extractSection(content, "Tasks");
865
- const verifySection = extractSection(content, "Verify");
866
- const taskTodos = getOpenCheckboxes(tasksSection);
867
- if (taskTodos.length > 0)
868
- warnings.push(`${taskTodos.length} task item(s) still incomplete`);
869
- const verifyTodos = getOpenCheckboxes(verifySection);
870
- if (verifyTodos.length > 0)
871
- warnings.push(`${verifyTodos.length} Verify checklist item(s) are still incomplete`);
872
- if (hasMeaningfulBlockers(content))
873
- warnings.push("active blockers are present in the change file");
874
- const scenarios = parseScenarios(content);
875
- if (scenarios.length === 0)
876
- warnings.push("no Scenario blocks found (some changes do not need them)");
877
- return warnings;
878
- }
879
1090
 
880
1091
  // src/commands/check.ts
881
1092
  import { existsSync as existsSync8 } from "fs";
@@ -890,6 +1101,10 @@ async function runCheck(options = {}) {
890
1101
  const runtime = [];
891
1102
  const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
892
1103
  const addDiagnostic = (diagnostic) => diagnostics.push(diagnostic);
1104
+ let focusedSet = /* @__PURE__ */ new Set();
1105
+ if (options.focused) {
1106
+ focusedSet = await getFocusedChangeNames({ onError: reportRuntime });
1107
+ }
893
1108
  if (existsSync8(FOCUS_DIR)) {
894
1109
  const entries = await walkFiles(FOCUS_DIR, { onError: reportRuntime });
895
1110
  for (const entryPath of entries) {
@@ -934,11 +1149,13 @@ async function runCheck(options = {}) {
934
1149
  }
935
1150
  }
936
1151
  }
937
- 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;
938
1154
  if (changeFiles.length === 0) {
939
1155
  const result2 = {
940
1156
  command: "check",
941
1157
  ok: diagnostics.every((d) => d.severity !== "error"),
1158
+ focused: Boolean(options.focused),
942
1159
  diagnostics,
943
1160
  runtime,
944
1161
  summary: {
@@ -953,6 +1170,8 @@ async function runCheck(options = {}) {
953
1170
  }
954
1171
  console.log();
955
1172
  console.log(` ${pc.bold("RSP check")}`);
1173
+ if (options.focused)
1174
+ console.log(` ${pc.dim("(focused only)")}`);
956
1175
  console.log();
957
1176
  if (diagnostics.length === 0) {
958
1177
  console.log(` ${pc.dim("No change files to check.")}
@@ -1089,6 +1308,30 @@ async function runCheck(options = {}) {
1089
1308
  message: `delta markers found: ${parts.join(", ")}`
1090
1309
  });
1091
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
+ }
1092
1335
  const scenarios = parseScenarios(content);
1093
1336
  if (scenarios.length > 0) {
1094
1337
  const scenarioIssues = [];
@@ -1121,6 +1364,7 @@ async function runCheck(options = {}) {
1121
1364
  const result = {
1122
1365
  command: "check",
1123
1366
  ok: diagnostics.every((d) => d.severity !== "error"),
1367
+ focused: Boolean(options.focused),
1124
1368
  diagnostics,
1125
1369
  runtime,
1126
1370
  summary: {
@@ -1135,6 +1379,8 @@ async function runCheck(options = {}) {
1135
1379
  }
1136
1380
  console.log();
1137
1381
  console.log(` ${pc.bold("RSP check")}`);
1382
+ if (options.focused)
1383
+ console.log(` ${pc.dim("(focused only)")}`);
1138
1384
  console.log();
1139
1385
  for (const diagnostic of diagnostics) {
1140
1386
  const icon = diagnostic.severity === "error" ? pc.red("\u2717") : diagnostic.severity === "warning" ? pc.yellow("\u26A0") : pc.dim("\u2139");
@@ -1155,12 +1401,35 @@ async function runCheck(options = {}) {
1155
1401
  `);
1156
1402
  return result;
1157
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
+ }
1158
1427
 
1159
1428
  // src/commands/create.ts
1160
1429
  import { existsSync as existsSync9 } from "fs";
1161
1430
  import { mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
1162
1431
  import { dirname as dirname7, join as join9 } from "path";
1163
- async function createChange(name, summary = "", kind) {
1432
+ async function createChange(name, summary = "", kind, options = {}) {
1164
1433
  if (!name) {
1165
1434
  console.error(` ${pc.red("Usage:")} rsp create <name> [summary]`);
1166
1435
  process.exit(1);
@@ -1175,7 +1444,7 @@ async function createChange(name, summary = "", kind) {
1175
1444
  await mkdir5(dirname7(changePath), { recursive: true });
1176
1445
  const existed = existsSync9(changePath);
1177
1446
  if (!existed) {
1178
- const content = generateChangeContent(name, summary, kind);
1447
+ const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
1179
1448
  await writeFile6(changePath, content);
1180
1449
  await mkdir5(FOCUS_DIR, { recursive: true });
1181
1450
  const focusEntry = join9(FOCUS_DIR, name);
@@ -1188,37 +1457,101 @@ async function createChange(name, summary = "", kind) {
1188
1457
  console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${name}`);
1189
1458
  else
1190
1459
  console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
1191
- console.log(` ${pc.cyan("Next:")} edit the change file 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"}
1192
1461
  `);
1193
1462
  });
1194
1463
  }
1195
1464
 
1196
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
1197
1471
  import { existsSync as existsSync10 } from "fs";
1198
- import { readFile as readFile8 } from "fs/promises";
1199
- 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
1200
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
+ }
1201
1533
  const checks = [];
1202
1534
  const runtime = [];
1203
1535
  const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
1204
- const rspRulesPath = join10(RSP_DIR, "rules", "rsp-rules.md");
1205
- const designPath = join10(RSP_DIR, "specs", "design.md");
1206
- const specsIndexPath = join10(RSP_DIR, "specs", "INDEX.md");
1207
- const archivesIndexPath = join10(RSP_DIR, "archives", "INDEX.md");
1208
- reportCheck(checks, ".rsp exists", existsSync10(RSP_DIR), "Run: rsp init");
1209
- reportCheck(checks, "rules/rsp-rules.md exists", existsSync10(rspRulesPath), "Run: rsp init");
1210
- reportCheck(checks, "specs/design.md exists", existsSync10(designPath), "Run: rsp init");
1211
- reportCheck(checks, "specs/INDEX.md exists", existsSync10(specsIndexPath), "Run: rsp update");
1212
- 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");
1213
1545
  await checkAgents(checks, reportRuntime);
1214
- await checkGeneratedIndex(checks, reportRuntime, specsIndexPath, "_Auto-generated by `rsp update`._", "specs/INDEX.md has generated signature", "Run: rsp update");
1215
- 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");
1216
1548
  await checkArchiveNaming(checks, reportRuntime);
1217
1549
  await checkConfigSemantics(checks, reportRuntime);
1218
1550
  await checkActiveChangeConsistency(checks, reportRuntime);
1219
1551
  const result = {
1220
1552
  command: "doctor",
1221
1553
  ok: checks.every((check) => check.status !== "issue"),
1554
+ fixed,
1222
1555
  checks,
1223
1556
  runtime,
1224
1557
  summary: {
@@ -1232,6 +1565,13 @@ async function runDoctor(options = {}) {
1232
1565
  console.log();
1233
1566
  console.log(` ${pc.bold("RSP doctor")}`);
1234
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
+ }
1235
1575
  for (const check of checks)
1236
1576
  printDoctorCheck(check);
1237
1577
  if (result.summary.issues === 0)
@@ -1267,13 +1607,13 @@ function printDoctorCheck(check) {
1267
1607
  console.log(` ${pc.dim(check.hint)}`);
1268
1608
  }
1269
1609
  async function checkAgents(checks, reportRuntime) {
1270
- if (!existsSync10("AGENTS.md")) {
1610
+ if (!existsSync11("AGENTS.md")) {
1271
1611
  checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing", hint: "Run: rsp init" });
1272
1612
  return;
1273
1613
  }
1274
1614
  let agents;
1275
1615
  try {
1276
- agents = await readFile8("AGENTS.md", "utf-8");
1616
+ agents = await readFile9("AGENTS.md", "utf-8");
1277
1617
  } catch (error) {
1278
1618
  reportRuntime({
1279
1619
  code: "agents_read_failed",
@@ -1290,12 +1630,12 @@ async function checkAgents(checks, reportRuntime) {
1290
1630
  }
1291
1631
  checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
1292
1632
  }
1293
- async function checkGeneratedIndex(checks, reportRuntime, path, signature, label, fixHint) {
1294
- if (!existsSync10(path))
1633
+ async function checkGeneratedIndex(checks, reportRuntime, path, expectedIndexType, label, fixHint) {
1634
+ if (!existsSync11(path))
1295
1635
  return;
1296
1636
  let content;
1297
1637
  try {
1298
- content = await readFile8(path, "utf-8");
1638
+ content = await readFile9(path, "utf-8");
1299
1639
  } catch (error) {
1300
1640
  reportRuntime({
1301
1641
  code: "generated_index_read_failed",
@@ -1306,15 +1646,27 @@ async function checkGeneratedIndex(checks, reportRuntime, path, signature, label
1306
1646
  checks.push({ status: "issue", label, message: `${label} could not be read`, hint: fixHint });
1307
1647
  return;
1308
1648
  }
1309
- if (content.includes(signature)) {
1310
- 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 });
1311
1663
  return;
1312
1664
  }
1313
1665
  checks.push({ status: "issue", label, hint: fixHint });
1314
1666
  }
1315
1667
  async function checkArchiveNaming(checks, reportRuntime) {
1316
- const archivesDir = join10(RSP_DIR, "archives");
1317
- 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") : [];
1318
1670
  const invalidFiles = archiveFiles.map((fp) => normalizeLogicalPath(relative5(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename5(fp)) === false);
1319
1671
  if (invalidFiles.length === 0) {
1320
1672
  checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
@@ -1328,11 +1680,11 @@ async function checkArchiveNaming(checks, reportRuntime) {
1328
1680
  });
1329
1681
  }
1330
1682
  async function checkConfigSemantics(checks, reportRuntime) {
1331
- if (!existsSync10(CONFIG_PATH))
1683
+ if (!existsSync11(CONFIG_PATH))
1332
1684
  return;
1333
1685
  let content;
1334
1686
  try {
1335
- content = await readFile8(CONFIG_PATH, "utf-8");
1687
+ content = await readFile9(CONFIG_PATH, "utf-8");
1336
1688
  } catch (error) {
1337
1689
  reportRuntime({
1338
1690
  code: "config_read_failed",
@@ -1388,10 +1740,9 @@ function reportConfigListField(checks, parsed, key) {
1388
1740
  });
1389
1741
  }
1390
1742
  async function checkActiveChangeConsistency(checks, reportRuntime) {
1391
- const changeFiles = existsSync10(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
1743
+ const changeFiles = existsSync11(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
1392
1744
  const changeNames = new Set(changeFiles.map((fp) => changeNameFromPath(CHANGES_DIR, fp)));
1393
- const focusEntries = existsSync10(FOCUS_DIR) ? await walkFiles(FOCUS_DIR, { onError: reportRuntime }) : [];
1394
- const focusNames = new Set(focusEntries.map((fp) => normalizeLogicalPath(relative5(FOCUS_DIR, fp))));
1745
+ const focusNames = await getFocusedChangeNames({ onError: reportRuntime });
1395
1746
  const missingChangeFiles = [...focusNames].filter((name) => !changeNames.has(name));
1396
1747
  const unfocusedChangeFiles = [...changeNames].filter((name) => !focusNames.has(name));
1397
1748
  if (missingChangeFiles.length === 0) {
@@ -1419,9 +1770,9 @@ async function checkActiveChangeConsistency(checks, reportRuntime) {
1419
1770
  }
1420
1771
 
1421
1772
  // src/commands/focus.ts
1422
- import { existsSync as existsSync11 } from "fs";
1423
- import { mkdir as mkdir6, unlink as unlink3, writeFile as writeFile7 } from "fs/promises";
1424
- 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";
1425
1776
  async function focusChange(name) {
1426
1777
  if (!name) {
1427
1778
  console.error(` ${pc.red("Usage:")} rsp focus <name>`);
@@ -1432,16 +1783,16 @@ async function focusChange(name) {
1432
1783
  process.exit(1);
1433
1784
  }
1434
1785
  guardRspInitialized();
1435
- const changePath = join11(CHANGES_DIR, `${name}.md`);
1436
- if (!existsSync11(changePath)) {
1786
+ const changePath = join12(CHANGES_DIR, `${name}.md`);
1787
+ if (!existsSync12(changePath)) {
1437
1788
  console.error(` ${pc.red("Change not found:")} .rsp/changes/${name}.md`);
1438
1789
  process.exit(1);
1439
1790
  }
1440
1791
  return withRspLock("focus-change", async () => {
1441
- await mkdir6(FOCUS_DIR, { recursive: true });
1442
- const focusEntry = join11(FOCUS_DIR, name);
1443
- await mkdir6(dirname8(focusEntry), { recursive: true });
1444
- 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, "");
1445
1796
  console.log(` ${pc.green("Focused:")} ${name}`);
1446
1797
  console.log(` ${pc.dim("focus.d")} \u2192 ${name}`);
1447
1798
  console.log();
@@ -1457,8 +1808,8 @@ async function unfocusChange(name) {
1457
1808
  process.exit(1);
1458
1809
  }
1459
1810
  guardRspInitialized();
1460
- const focusEntry = join11(FOCUS_DIR, name);
1461
- if (!existsSync11(focusEntry)) {
1811
+ const focusEntry = join12(FOCUS_DIR, name);
1812
+ if (!existsSync12(focusEntry)) {
1462
1813
  console.error(` ${pc.red("Focus marker not found:")} .rsp/focus.d/${name}`);
1463
1814
  process.exit(1);
1464
1815
  }
@@ -1472,9 +1823,9 @@ async function unfocusChange(name) {
1472
1823
  }
1473
1824
 
1474
1825
  // src/commands/init.ts
1475
- import { existsSync as existsSync12 } from "fs";
1476
- import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
1477
- 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";
1478
1829
  function generateConfigTemplate() {
1479
1830
  const fmtList = (items, indent) => items.map((i) => `${" ".repeat(indent)}# - ${i}`).join("\n");
1480
1831
  return `# RSP project configuration
@@ -1488,16 +1839,16 @@ ${fmtList(VALID_KINDS, 2)}
1488
1839
  `;
1489
1840
  }
1490
1841
  async function ensureFile(path, content) {
1491
- if (existsSync12(path))
1842
+ if (existsSync13(path))
1492
1843
  return false;
1493
- await writeFile8(path, content);
1844
+ await writeFile9(path, content);
1494
1845
  return true;
1495
1846
  }
1496
1847
  function toTitle(projectName) {
1497
1848
  return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
1498
1849
  }
1499
1850
  async function initProject(args = {}) {
1500
- const isNew = !existsSync12(RSP_DIR);
1851
+ const isNew = !existsSync13(RSP_DIR);
1501
1852
  const projectName = await detectProjectName();
1502
1853
  const mode = args.agentsMode ?? "managed";
1503
1854
  if (mode !== "managed" && mode !== "print") {
@@ -1506,32 +1857,32 @@ async function initProject(args = {}) {
1506
1857
  }
1507
1858
  return withRspLock("init", async () => {
1508
1859
  const dirs = [
1509
- join12(RSP_DIR, "rules"),
1510
- join12(RSP_DIR, "specs"),
1860
+ join13(RSP_DIR, "rules"),
1861
+ join13(RSP_DIR, "specs"),
1511
1862
  CHANGES_DIR,
1512
1863
  FOCUS_DIR,
1513
- join12(RSP_DIR, "archives")
1864
+ join13(RSP_DIR, "archives")
1514
1865
  ];
1515
1866
  for (const d of dirs)
1516
- await mkdir7(d, { recursive: true });
1867
+ await mkdir8(d, { recursive: true });
1517
1868
  let created = false;
1518
- const bundledRules = await readFile9(join12(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
1519
- created = await ensureFile(join12(RSP_DIR, "rules", "rsp-rules.md"), bundledRules) || created;
1520
- created = await ensureFile(join12(RSP_DIR, "config.yaml"), generateConfigTemplate()) || created;
1521
- 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");
1522
1873
  created = createdSpecsIndex || created;
1523
- 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));
1524
1875
  created = createdDesign || created;
1525
- 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");
1526
1877
  created = createdArchivesIndex || created;
1527
- created = await ensureFile(join12(CHANGES_DIR, ".gitkeep"), "") || created;
1528
- created = await ensureFile(join12(FOCUS_DIR, ".gitkeep"), "") || created;
1529
- 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;
1530
1881
  if (args.withProjectRules)
1531
- 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;
1532
1883
  if (args.withProjectSetup) {
1533
- const changePath = join12(CHANGES_DIR, "project-setup.md");
1534
- const focusMarker = join12(FOCUS_DIR, "project-setup");
1884
+ const changePath = join13(CHANGES_DIR, "project-setup.md");
1885
+ const focusMarker = join13(FOCUS_DIR, "project-setup");
1535
1886
  created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
1536
1887
  created = await ensureFile(focusMarker, "") || created;
1537
1888
  }
@@ -1542,24 +1893,24 @@ async function initProject(args = {}) {
1542
1893
  const agentsPath = "AGENTS.md";
1543
1894
  const title = toTitle(projectName);
1544
1895
  let agentsUpdated = false;
1545
- if (!existsSync12(agentsPath)) {
1546
- await writeFile8(agentsPath, `# ${title}
1896
+ if (!existsSync13(agentsPath)) {
1897
+ await writeFile9(agentsPath, `# ${title}
1547
1898
 
1548
1899
  ${upsertRspAgentsBlock("").content}
1549
1900
  `);
1550
1901
  created = true;
1551
1902
  agentsUpdated = true;
1552
1903
  } else {
1553
- const existing = await readFile9(agentsPath, "utf-8");
1904
+ const existing = await readFile10(agentsPath, "utf-8");
1554
1905
  const next = upsertRspAgentsBlock(existing);
1555
1906
  if (next.changed) {
1556
- await writeFile8(agentsPath, next.content);
1907
+ await writeFile9(agentsPath, next.content);
1557
1908
  created = true;
1558
1909
  agentsUpdated = true;
1559
1910
  }
1560
1911
  }
1561
1912
  if (mode === "print") {
1562
- const managed = await readFile9(agentsPath, "utf-8");
1913
+ const managed = await readFile10(agentsPath, "utf-8");
1563
1914
  console.log(`
1564
1915
  ${managed}
1565
1916
  `);
@@ -1596,24 +1947,267 @@ ${managed}
1596
1947
  });
1597
1948
  }
1598
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
+
1599
2198
  // src/commands/status.ts
1600
- import { existsSync as existsSync13 } from "fs";
1601
- import { readFile as readFile10, stat as stat2 } from "fs/promises";
1602
- import { join as join13, relative as relative6 } 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";
1603
2202
  var COL_CHANGE = 32;
1604
2203
  var COL_KIND = 10;
1605
2204
  var COL_AGE = 7;
1606
2205
  var COL_PROGRESS = 10;
1607
2206
  async function showStatus(options = {}, runOptions = {}) {
1608
- const focusedSet = /* @__PURE__ */ new Set();
1609
2207
  const runtime = [];
1610
2208
  const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(runOptions.verbose) && !runOptions.json);
1611
- if (existsSync13(FOCUS_DIR)) {
1612
- const entries = await walkFiles(FOCUS_DIR, { onError: reportRuntime });
1613
- for (const entryPath of entries)
1614
- focusedSet.add(normalizeLogicalPath(relative6(FOCUS_DIR, entryPath)));
1615
- }
1616
- const changeFiles = existsSync13(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
2209
+ const focusedSet = await getFocusedChangeNames({ onError: reportRuntime });
2210
+ const changeFiles = existsSync16(CHANGES_DIR) ? await walkMarkdownFiles(CHANGES_DIR, { onError: reportRuntime }) : [];
1617
2211
  const changeMap = {};
1618
2212
  for (const fp of changeFiles)
1619
2213
  changeMap[changeNameFromPath(CHANGES_DIR, fp)] = fp;
@@ -1631,7 +2225,7 @@ async function showStatus(options = {}, runOptions = {}) {
1631
2225
  let total = 0;
1632
2226
  if (fp) {
1633
2227
  try {
1634
- const content = await readFile10(fp, "utf-8");
2228
+ const content = await readFile13(fp, "utf-8");
1635
2229
  try {
1636
2230
  const fm = parseFrontmatter(content);
1637
2231
  kind = fm?.kind ? String(fm.kind) : "\u2014";
@@ -1658,7 +2252,7 @@ async function showStatus(options = {}, runOptions = {}) {
1658
2252
  });
1659
2253
  }
1660
2254
  try {
1661
- const s = await stat2(fp);
2255
+ const s = await stat(fp);
1662
2256
  ageDays = Math.floor((Date.now() - s.mtime.getTime()) / (1e3 * 60 * 60 * 24));
1663
2257
  } catch (error) {
1664
2258
  reportRuntime({
@@ -1673,7 +2267,7 @@ async function showStatus(options = {}, runOptions = {}) {
1673
2267
  reportRuntime({
1674
2268
  code: "focused_change_missing",
1675
2269
  operation: "resolveChange",
1676
- path: join13(CHANGES_DIR, `${name}.md`),
2270
+ path: join16(CHANGES_DIR, `${name}.md`),
1677
2271
  message: "focus marker points to a missing change file"
1678
2272
  });
1679
2273
  }
@@ -1713,6 +2307,7 @@ async function showStatus(options = {}, runOptions = {}) {
1713
2307
  return true;
1714
2308
  });
1715
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"));
1716
2311
  const statusResult = {
1717
2312
  command: "status",
1718
2313
  ok: true,
@@ -1728,6 +2323,7 @@ async function showStatus(options = {}, runOptions = {}) {
1728
2323
  focused: filteredOutputRecords.filter((r) => r.isFocused).length,
1729
2324
  blocked: blockedCount
1730
2325
  },
2326
+ nextActions,
1731
2327
  archiveTrend: await readArchiveTrend(runtime, runOptions),
1732
2328
  runtime
1733
2329
  };
@@ -1742,7 +2338,9 @@ async function showStatus(options = {}, runOptions = {}) {
1742
2338
  console.log(` ${pc.cyan("Focused:")} ${[...focusedSet].join(", ")}`);
1743
2339
  console.log();
1744
2340
  } else {
1745
- 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)}`);
1746
2344
  console.log();
1747
2345
  }
1748
2346
  if (allNames.length === 0) {
@@ -1779,12 +2377,32 @@ async function showStatus(options = {}, runOptions = {}) {
1779
2377
  }
1780
2378
  return statusResult;
1781
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
+ }
1782
2400
  async function readArchiveTrend(runtime, runOptions) {
1783
- const indexPath = join13(ARCHIVES_DIR, "INDEX.md");
1784
- if (!existsSync13(indexPath))
2401
+ const indexPath = join16(ARCHIVES_DIR, "INDEX.md");
2402
+ if (!existsSync16(indexPath))
1785
2403
  return [];
1786
2404
  try {
1787
- const content = await readFile10(indexPath, "utf-8");
2405
+ const content = await readFile13(indexPath, "utf-8");
1788
2406
  const dateRe = /^\| (\d{4}-\d{2})-\d{2} \|/gm;
1789
2407
  const counts = {};
1790
2408
  let match;
@@ -1815,52 +2433,6 @@ function printArchiveTrend(trend) {
1815
2433
  console.log();
1816
2434
  }
1817
2435
 
1818
- // src/commands/update.ts
1819
- import { existsSync as existsSync14 } from "fs";
1820
- import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
1821
- import { join as join14 } from "path";
1822
- var SKILL_REFRESH_HINT = " Note: if you use the published RSP skill, refresh it too:\n npx skills add oevery/rsp\n";
1823
- async function updateProject() {
1824
- if (!existsSync14(RSP_DIR)) {
1825
- console.log(` ${pc.red("Error:")} RSP is not initialized. Run: rsp init`);
1826
- process.exit(1);
1827
- }
1828
- return withRspLock("update", async () => {
1829
- let updated = false;
1830
- const bundledRules = await readFile11(join14(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
1831
- const rulesPath = join14(RSP_DIR, "rules", "rsp-rules.md");
1832
- await mkdir8(join14(RSP_DIR, "rules"), { recursive: true });
1833
- const existingRules = existsSync14(rulesPath) ? await readFile11(rulesPath, "utf-8") : null;
1834
- if (existingRules !== bundledRules) {
1835
- await writeFile9(rulesPath, bundledRules);
1836
- console.log(` ${pc.green("\u2713")} rules/rsp-rules.md updated`);
1837
- updated = true;
1838
- }
1839
- const agentsPath = "AGENTS.md";
1840
- const projectName = await detectProjectName();
1841
- const baseAgents = existsSync14(agentsPath) ? await readFile11(agentsPath, "utf-8") : `# ${projectName}
1842
-
1843
- `;
1844
- const nextAgents = upsertRspAgentsBlock(baseAgents);
1845
- if (!existsSync14(agentsPath) || nextAgents.changed) {
1846
- await writeFile9(agentsPath, nextAgents.content);
1847
- console.log(` ${pc.green("\u2713")} AGENTS.md managed block refreshed`);
1848
- updated = true;
1849
- }
1850
- await buildSpecsIndex({ acquireLock: false });
1851
- await buildArchiveIndex({ acquireLock: false });
1852
- if (!updated) {
1853
- console.log(` ${pc.dim("Already up to date.")}
1854
- `);
1855
- console.log(SKILL_REFRESH_HINT);
1856
- } else {
1857
- console.log(` ${pc.green("Update complete.")}
1858
- `);
1859
- console.log(SKILL_REFRESH_HINT);
1860
- }
1861
- });
1862
- }
1863
-
1864
2436
  // src/cli.ts
1865
2437
  var initCommand = defineCommand({
1866
2438
  meta: {
@@ -1906,11 +2478,16 @@ var createCommand = defineCommand({
1906
2478
  kind: {
1907
2479
  type: "string",
1908
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
1909
2486
  }
1910
2487
  },
1911
2488
  async run({ args }) {
1912
2489
  const summary = Array.isArray(args._) && args._.length > 1 ? args._.slice(1).join(" ") : "";
1913
- await createChange(args.name, summary, args.kind);
2490
+ await createChange(args.name, summary, args.kind, { lite: Boolean(args.lite) });
1914
2491
  }
1915
2492
  });
1916
2493
  var addRulesCommand = defineCommand({
@@ -1961,14 +2538,19 @@ var archiveCommand = defineCommand({
1961
2538
  description: "Archive an open change into .rsp/archives/"
1962
2539
  },
1963
2540
  args: {
1964
- name: {
2541
+ "name": {
1965
2542
  type: "positional",
1966
2543
  description: "Change name",
1967
2544
  required: true
2545
+ },
2546
+ "dry-run": {
2547
+ type: "boolean",
2548
+ description: "Preview archive readiness without moving the change",
2549
+ default: false
1968
2550
  }
1969
2551
  },
1970
2552
  async run({ args }) {
1971
- await archiveChange(args.name);
2553
+ await archiveChange(args.name, { dryRun: Boolean(args["dry-run"]) });
1972
2554
  }
1973
2555
  });
1974
2556
  var focusCommand = defineCommand({
@@ -2063,9 +2645,14 @@ var statusCommand = defineCommand({
2063
2645
  var checkCommand = defineCommand({
2064
2646
  meta: {
2065
2647
  name: "check",
2066
- description: "Validate changes (frontmatter, sections, deltas, scenarios)"
2648
+ description: "Validate changes (frontmatter, sections, template text, deltas, scenarios)"
2067
2649
  },
2068
2650
  args: {
2651
+ focused: {
2652
+ type: "boolean",
2653
+ description: "Only validate currently focused changes",
2654
+ default: false
2655
+ },
2069
2656
  json: {
2070
2657
  type: "boolean",
2071
2658
  description: "Print machine-readable JSON output",
@@ -2078,7 +2665,11 @@ var checkCommand = defineCommand({
2078
2665
  }
2079
2666
  },
2080
2667
  async run({ args }) {
2081
- 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
+ });
2082
2673
  if (!result.ok)
2083
2674
  process.exit(1);
2084
2675
  }
@@ -2107,14 +2698,80 @@ var doctorCommand = defineCommand({
2107
2698
  type: "boolean",
2108
2699
  description: "Print runtime diagnostics for suppressed I/O issues",
2109
2700
  default: false
2701
+ },
2702
+ fix: {
2703
+ type: "boolean",
2704
+ description: "Run safe deterministic repairs before reporting diagnostics",
2705
+ default: false
2110
2706
  }
2111
2707
  },
2112
2708
  async run({ args }) {
2113
- 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) });
2114
2710
  if (!result.ok)
2115
2711
  process.exit(1);
2116
2712
  }
2117
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
+ });
2118
2775
  async function runCli(rawArgs = process.argv.slice(2)) {
2119
2776
  const version = await getVersion();
2120
2777
  const main = defineCommand({
@@ -2130,6 +2787,8 @@ async function runCli(rawArgs = process.argv.slice(2)) {
2130
2787
  focus: focusCommand,
2131
2788
  unfocus: unfocusCommand,
2132
2789
  archive: archiveCommand,
2790
+ ready: readyCommand,
2791
+ show: showCommand,
2133
2792
  status: statusCommand,
2134
2793
  check: checkCommand,
2135
2794
  update: updateCommand,