@oevery/rsp 3.1.0-beta.3 → 3.1.0-beta.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
@@ -4,7 +4,7 @@ import {
4
4
  installPackagedSkills,
5
5
  printPackagedSkillInventory,
6
6
  printSkillInstallResult
7
- } from "./chunk-NX55NIC3.mjs";
7
+ } from "./chunk-D7XDXBFO.mjs";
8
8
  import {
9
9
  ArchiveHistoryError,
10
10
  GROUP_BRIEF_FILENAME,
@@ -21,6 +21,8 @@ import {
21
21
  inspectFocusTree,
22
22
  inspectProjectStatus,
23
23
  inspectWorkTree,
24
+ normalizeExecutableWorkRef,
25
+ normalizeWorkRefSegment,
24
26
  projectDependencyForest,
25
27
  queryArchiveHistory,
26
28
  readArchiveHistoryDetail,
@@ -29,12 +31,14 @@ import {
29
31
  resolveFocusMarkerPath,
30
32
  resolveWorkRef,
31
33
  selectArchiveHistoryRecord
32
- } from "./chunk-7KKAUBKN.mjs";
34
+ } from "./chunk-FI3SYHVH.mjs";
33
35
  import {
34
36
  ARCHIVES_DIR,
35
37
  CHANGES_DIR,
38
+ CHANGE_DOCUMENT_SCHEMA,
36
39
  CONFIG_PATH,
37
40
  DEFAULT_DECISION_RECORDS_PATH,
41
+ DocumentSectionCardinalityError,
38
42
  FOCUS_DIR,
39
43
  LOCK_PATH,
40
44
  OBSOLETE_RSP_RULES_PATH,
@@ -42,6 +46,7 @@ import {
42
46
  RSP_DIR,
43
47
  RSP_RULES_PATH,
44
48
  VALID_KINDS,
49
+ appendDocumentSectionItem,
45
50
  buildDurableReviewGuidance,
46
51
  cleanupEmptyParentDirs,
47
52
  clearConfigCache,
@@ -56,6 +61,8 @@ import {
56
61
  generateChangeContent,
57
62
  generateDesignContent,
58
63
  generateSpecContent,
64
+ getDocumentSectionDefinitionByHeading,
65
+ getDocumentSections,
59
66
  getDurableReviewCandidateTargets,
60
67
  getVersion,
61
68
  guardRspInitialized,
@@ -70,6 +77,7 @@ import {
70
77
  loadRspConfig,
71
78
  normalizeLogicalPath,
72
79
  parseFrontmatter,
80
+ parseRspDocument,
73
81
  parseScenarios,
74
82
  pc,
75
83
  recordRuntimeDiagnostic,
@@ -83,7 +91,7 @@ import {
83
91
  validateDecisionRecordsFilesystemPath,
84
92
  walkMarkdownFiles,
85
93
  writeManagedFile
86
- } from "./chunk-OTJFL6XH.mjs";
94
+ } from "./chunk-FJVH7ZYU.mjs";
87
95
 
88
96
  // src/cli.ts
89
97
  import { resolve } from "path";
@@ -418,7 +426,7 @@ async function addSpec(name, projectName = "project") {
418
426
  }
419
427
 
420
428
  // src/commands/archive.ts
421
- import { existsSync } from "fs";
429
+ import { existsSync, readdirSync } from "fs";
422
430
  import { mkdir as mkdir3, readFile as readFile3, rename, unlink as unlink4 } from "fs/promises";
423
431
  import { basename as basename2, join as join3 } from "path";
424
432
  async function archiveChange(name) {
@@ -436,7 +444,7 @@ async function archiveChange(name) {
436
444
  const content = await readFile3(srcPath, "utf-8");
437
445
  const dependencyInspection = await inspectChangeDependencies();
438
446
  const checklist = collectArchiveChecklist(content, {
439
- activeBlockers: dependencyInspection.activeBlockers.get(name)
447
+ activeBlockers: dependencyInspection.activeBlockers.get(workRef.name)
440
448
  });
441
449
  for (const line of checklist)
442
450
  console.log(` ${pc.yellow("\u26A0")} ${line}`);
@@ -491,12 +499,12 @@ ${clearedMsg}`);
491
499
  }
492
500
  function resolveArchiveName(archiveSubdir, date, base) {
493
501
  const initialName = `${date}_${base}.md`;
494
- if (!existsSync(join3(archiveSubdir, initialName)))
502
+ if (!archiveNameExists(archiveSubdir, initialName))
495
503
  return initialName;
496
504
  const MAX_SUFFIX = 9999;
497
505
  for (let index = 2; index <= MAX_SUFFIX; index++) {
498
506
  const candidate = `${date}_${base}-${index}.md`;
499
- if (!existsSync(join3(archiveSubdir, candidate)))
507
+ if (!archiveNameExists(archiveSubdir, candidate))
500
508
  return candidate;
501
509
  }
502
510
  throw new WorkRefError(
@@ -505,6 +513,22 @@ function resolveArchiveName(archiveSubdir, date, base) {
505
513
  base
506
514
  );
507
515
  }
516
+ function archiveNameExists(archiveSubdir, name) {
517
+ if (!existsSync(archiveSubdir))
518
+ return false;
519
+ return readdirSync(archiveSubdir).some((entry) => {
520
+ if (entry.normalize("NFC") !== name)
521
+ return false;
522
+ if (entry !== name) {
523
+ throw new WorkRefError(
524
+ "work_ref_collision",
525
+ `archive name has a Unicode normalization collision: ${entry} and ${name}`,
526
+ name
527
+ );
528
+ }
529
+ return true;
530
+ });
531
+ }
508
532
 
509
533
  // src/commands/check.ts
510
534
  import { readFile as readFile4 } from "fs/promises";
@@ -696,8 +720,10 @@ async function runCheck(options = {}) {
696
720
  message: "missing YAML frontmatter"
697
721
  });
698
722
  }
723
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
699
724
  for (const section of requiredSections) {
700
- if (!new RegExp(`^## ${section}$`, "m").test(content)) {
725
+ const definition = getDocumentSectionDefinitionByHeading(CHANGE_DOCUMENT_SCHEMA, section);
726
+ if (!definition || !getDocumentSections(document, definition.id).some((candidate) => candidate.canonical)) {
701
727
  addDiagnostic({
702
728
  severity: "error",
703
729
  code: "missing_section",
@@ -734,16 +760,14 @@ async function runCheck(options = {}) {
734
760
  });
735
761
  }
736
762
  }
737
- const headingLine = content.split("\n").map((line) => line.trim()).find((line) => line.startsWith("# Change:"));
738
- if (headingLine) {
739
- const heading = headingLine.slice("# Change:".length).trim();
740
- if (heading !== name) {
763
+ if (document.title !== null) {
764
+ if (document.title !== name) {
741
765
  addDiagnostic({
742
766
  severity: "error",
743
767
  code: "heading_mismatch",
744
768
  change: name,
745
769
  path: fp,
746
- message: `# Change: heading "${heading}" differs from change name`
770
+ message: `# Change: heading "${document.title}" differs from change name`
747
771
  });
748
772
  }
749
773
  } else {
@@ -866,7 +890,7 @@ async function runCheck(options = {}) {
866
890
  return result;
867
891
  }
868
892
  function findUnfinishedTemplateLines(content) {
869
- 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;
893
+ 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;
870
894
  return collectMatchingLines(content, (line) => {
871
895
  const trimmed = stripInlineCodeSpans(line).trim();
872
896
  return trimmed.startsWith("<!--") === false && placeholderRe.test(trimmed);
@@ -906,7 +930,7 @@ async function createChange(name, summary = "", kind, options = {}) {
906
930
  const existed = existsSync2(changePath);
907
931
  if (!existed) {
908
932
  const focusEntry = resolveFocusMarkerPath(workRef);
909
- const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
933
+ const content = generateChangeContent(workRef.name, summary, kind, { lite: Boolean(options.lite) });
910
934
  await mkdir4(dirname2(changePath), { recursive: true });
911
935
  let changeCreated = false;
912
936
  try {
@@ -929,9 +953,9 @@ async function createChange(name, summary = "", kind, options = {}) {
929
953
  const label = existed ? "Using" : pc.green("Created");
930
954
  console.log(` ${label}: ${changePath}`);
931
955
  if (existed)
932
- console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${name}`);
956
+ console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${workRef.name}`);
933
957
  else
934
- console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
958
+ console.log(` ${pc.dim("focused via focus.d")} \u2192 ${workRef.name}`);
935
959
  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"}
936
960
  `);
937
961
  });
@@ -1565,8 +1589,8 @@ async function focusChange(name) {
1565
1589
  await mkdir6(FOCUS_DIR, { recursive: true });
1566
1590
  await mkdir6(dirname3(focusEntry), { recursive: true });
1567
1591
  await writeManagedFile(focusEntry, "", "focus marker");
1568
- console.log(` ${pc.green("Focused:")} ${name}`);
1569
- console.log(` ${pc.dim("focus.d")} \u2192 ${name}`);
1592
+ console.log(` ${pc.green("Focused:")} ${workRef.name}`);
1593
+ console.log(` ${pc.dim("focus.d")} \u2192 ${workRef.name}`);
1570
1594
  console.log();
1571
1595
  });
1572
1596
  } catch (error) {
@@ -1587,8 +1611,8 @@ async function unfocusChange(name) {
1587
1611
  throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
1588
1612
  await unlink7(focusEntry);
1589
1613
  await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
1590
- console.log(` ${pc.green("Unfocused:")} ${name}`);
1591
- console.log(` ${pc.dim("focus.d cleared")} \u2192 ${name}`);
1614
+ console.log(` ${pc.green("Unfocused:")} ${workRef.name}`);
1615
+ console.log(` ${pc.dim("focus.d cleared")} \u2192 ${workRef.name}`);
1592
1616
  console.log();
1593
1617
  });
1594
1618
  } catch (error) {
@@ -1623,20 +1647,21 @@ async function createChangeGroup(name, goal = "") {
1623
1647
  const ref = resolveWorkRef(`${name}/brief`);
1624
1648
  if (ref.kind !== "group-brief")
1625
1649
  throw new WorkRefError("invalid_work_ref", `invalid Change Group name: ${name}`, name);
1626
- if (await hasArchivedGroupBrief(name))
1627
- throw new ChangeGroupError(`archived Change Group cannot be reopened: ${name}`);
1628
- const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [name], "Change Group path");
1650
+ const groupName = ref.group;
1651
+ if (await hasArchivedGroupBrief(groupName))
1652
+ throw new ChangeGroupError(`archived Change Group cannot be reopened: ${groupName}`);
1653
+ const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [groupName], "Change Group path");
1629
1654
  await mkdir7(groupDirectory, { recursive: true });
1630
1655
  try {
1631
- await writeFile4(ref.path, generateGroupBriefContent(name, goal), { flag: "wx" });
1656
+ await writeFile4(ref.path, generateGroupBriefContent(groupName, goal), { flag: "wx" });
1632
1657
  } catch (error) {
1633
1658
  if (error.code === "EEXIST")
1634
- throw new ChangeGroupError(`Change Group already exists: ${name}`);
1659
+ throw new ChangeGroupError(`Change Group already exists: ${groupName}`);
1635
1660
  throw error;
1636
1661
  }
1637
- console.log(` ${pc.green("Created Change Group:")} ${name}`);
1662
+ console.log(` ${pc.green("Created Change Group:")} ${groupName}`);
1638
1663
  console.log(` ${pc.dim("Unfocused Group Brief:")} ${ref.path}`);
1639
- console.log(` ${pc.cyan("Next:")} fill the brief, then create direct child Changes with rsp create ${name}/<change>
1664
+ console.log(` ${pc.cyan("Next:")} fill the brief, then create direct child Changes with rsp create ${groupName}/<change>
1640
1665
  `);
1641
1666
  });
1642
1667
  } catch (error) {
@@ -1659,12 +1684,13 @@ async function closeChangeGroup(name) {
1659
1684
  if (ref.kind !== "group-brief")
1660
1685
  throw new ChangeGroupError(`invalid Change Group name: ${name}`);
1661
1686
  const inspection = await inspectChangeGroups();
1662
- const group = inspection.groups.find((candidate) => candidate.name === name);
1687
+ const groupName = ref.group;
1688
+ const group = inspection.groups.find((candidate) => candidate.name === groupName);
1663
1689
  if (!group)
1664
- throw new ChangeGroupError(`Change Group not found: ${name}`);
1665
- const reasons = collectCloseReasons(group, inspection.diagnostics.filter((diagnostic) => diagnostic.change === name && diagnostic.severity === "error").map((diagnostic) => diagnostic.message));
1690
+ throw new ChangeGroupError(`Change Group not found: ${groupName}`);
1691
+ const reasons = collectCloseReasons(group, inspection.diagnostics.filter((diagnostic) => diagnostic.change === groupName && diagnostic.severity === "error").map((diagnostic) => diagnostic.message));
1666
1692
  if (reasons.length > 0)
1667
- throw new ChangeGroupError(`Change Group "${name}" is not ready to close: ${reasons.join("; ")}`);
1693
+ throw new ChangeGroupError(`Change Group "${groupName}" is not ready to close: ${reasons.join("; ")}`);
1668
1694
  const archiveDirectory = resolveArchiveDirectory(ref);
1669
1695
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1670
1696
  const archiveName = resolveArchiveName(archiveDirectory, date, "brief");
@@ -1676,7 +1702,7 @@ async function closeChangeGroup(name) {
1676
1702
  } catch (error) {
1677
1703
  warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
1678
1704
  }
1679
- console.log(` ${pc.green("Closed Change Group:")} ${name}`);
1705
+ console.log(` ${pc.green("Closed Change Group:")} ${groupName}`);
1680
1706
  console.log(` ${pc.dim("Archived Group Brief:")} ${join7(archiveDirectory, archiveName)}
1681
1707
  `);
1682
1708
  for (const warning of warnings)
@@ -1712,8 +1738,6 @@ function collectCloseReasons(group, errors) {
1712
1738
 
1713
1739
  // src/commands/history.ts
1714
1740
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1715
- var GROUP_RE = /^[a-z0-9-]+$/;
1716
- var WORK_REF_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)?$/;
1717
1741
  async function showHistory(input = {}, options = {}) {
1718
1742
  guardRspInitialized();
1719
1743
  const mode = input.workRef ? "detail" : "list";
@@ -1727,15 +1751,15 @@ async function showHistory(input = {}, options = {}) {
1727
1751
  message: "archive history inspection is incomplete; resolve the reported diagnostics before querying history"
1728
1752
  }, inspection.diagnostics, inspection.diagnosticSummary, inspection.runtime, options);
1729
1753
  }
1730
- if (input.workRef) {
1754
+ if (validation.workRef) {
1731
1755
  try {
1732
- const selected = selectArchiveHistoryRecord(inspection.records, { workRef: input.workRef });
1756
+ const selected = selectArchiveHistoryRecord(inspection.records, { workRef: validation.workRef });
1733
1757
  const record = await readArchiveHistoryDetail(selected);
1734
1758
  const result2 = {
1735
1759
  command: "history",
1736
1760
  ok: true,
1737
1761
  mode: "detail",
1738
- query: { workRef: input.workRef },
1762
+ query: { workRef: validation.workRef },
1739
1763
  record,
1740
1764
  diagnostics: [],
1741
1765
  diagnosticSummary: emptyCollectionSummary(),
@@ -1788,11 +1812,15 @@ function validateHistoryQuery(input) {
1788
1812
  if ((input.positionalCount ?? (input.workRef ? 1 : 0)) > 1)
1789
1813
  return invalid("history_positional_args_unsupported", "history accepts at most one positional WorkRef");
1790
1814
  if (input.workRef) {
1791
- if (!WORK_REF_RE.test(input.workRef) || input.workRef.endsWith("/brief") || input.workRef.endsWith("/00-brief"))
1815
+ let workRef;
1816
+ try {
1817
+ workRef = normalizeExecutableWorkRef(input.workRef);
1818
+ } catch {
1792
1819
  return invalid("invalid_history_work_ref", "history WorkRef must be a flat or one-Group-level executable Change identity");
1820
+ }
1793
1821
  if (input.limit !== void 0 || input.since !== void 0 || input.until !== void 0 || input.kind !== void 0 || input.group !== void 0 || input.search !== void 0)
1794
1822
  return invalid("history_detail_filters_unsupported", "list filters cannot be combined with an exact history detail lookup");
1795
- return { ok: true, query: {} };
1823
+ return { ok: true, query: {}, workRef };
1796
1824
  }
1797
1825
  const limit = input.limit === void 0 ? HISTORY_DEFAULT_LIMIT : Number(input.limit);
1798
1826
  if (input.limit !== void 0 && !/^[1-9]\d*$/.test(input.limit) || !Number.isInteger(limit) || limit < 1 || limit > HISTORY_MAX_LIMIT)
@@ -1805,8 +1833,14 @@ function validateHistoryQuery(input) {
1805
1833
  return invalid("invalid_history_date_range", "--since must be on or before --until");
1806
1834
  if (input.kind !== void 0 && input.kind.trim() === "")
1807
1835
  return invalid("invalid_history_kind", "--kind must be a non-empty exact historical kind");
1808
- if (input.group !== void 0 && !GROUP_RE.test(input.group))
1809
- return invalid("invalid_history_group", "--group must be one lowercase kebab-case Group name");
1836
+ let group = input.group;
1837
+ if (group !== void 0) {
1838
+ try {
1839
+ group = normalizeWorkRefSegment(group);
1840
+ } catch {
1841
+ return invalid("invalid_history_group", "--group must be one safe Unicode Group name");
1842
+ }
1843
+ }
1810
1844
  if (input.search !== void 0 && input.search.trim() === "")
1811
1845
  return invalid("invalid_history_search", "--search must be non-empty");
1812
1846
  return {
@@ -1816,7 +1850,7 @@ function validateHistoryQuery(input) {
1816
1850
  since: input.since,
1817
1851
  until: input.until,
1818
1852
  kind: input.kind?.trim(),
1819
- group: input.group,
1853
+ group,
1820
1854
  search: input.search?.trim()
1821
1855
  }
1822
1856
  };
@@ -1925,6 +1959,13 @@ function generateConfigTemplate() {
1925
1959
  # kinds:
1926
1960
  ${fmtList(VALID_KINDS, 0)}
1927
1961
 
1962
+ # Set one shared project default for durable artifact and commit prose.
1963
+ # Optional artifacts or commit values override that durable surface independently.
1964
+ # Response language remains user/session-owned and is never read from this project mapping.
1965
+ # Omit this mapping to keep durable language selection under scoped project authority.
1966
+ # language:
1967
+ # default: en
1968
+
1928
1969
  # New projects default to automatic Manage routing with lifecycle-only closeout.
1929
1970
  # Use activation: explicit to require a named managed request.
1930
1971
  # closeout accepts manual, lifecycle, or local; local routes a qualified clean terminal non-small boundary to one local commit but never push or publication.
@@ -2196,6 +2237,7 @@ async function reopenChange(name, options) {
2196
2237
  guardRspInitialized();
2197
2238
  try {
2198
2239
  await withRspLock("reopen-change", async () => {
2240
+ const normalizedName = normalizeExecutableWorkRef(name);
2199
2241
  const inspection = await inspectArchiveHistory();
2200
2242
  if (!historyInspectionComplete(inspection)) {
2201
2243
  throw new ArchiveHistoryError(
@@ -2205,17 +2247,17 @@ async function reopenChange(name, options) {
2205
2247
  }
2206
2248
  const record = selectArchiveHistoryRecord(
2207
2249
  inspection.records,
2208
- options.from ? { path: options.from } : { workRef: name }
2250
+ options.from ? { path: options.from } : { workRef: normalizedName }
2209
2251
  );
2210
- if (record.workRef !== name) {
2252
+ if (record.workRef !== normalizedName) {
2211
2253
  throw new ArchiveHistoryError(
2212
2254
  "archive_identity_mismatch",
2213
- `selected archive belongs to ${record.workRef}, not ${name}`
2255
+ `selected archive belongs to ${record.workRef}, not ${normalizedName}`
2214
2256
  );
2215
2257
  }
2216
2258
  let workRef;
2217
2259
  try {
2218
- workRef = resolveWorkRef(name, { executable: true });
2260
+ workRef = resolveWorkRef(normalizedName, { executable: true });
2219
2261
  } catch (error) {
2220
2262
  if (error instanceof WorkRefError && error.code === "group_brief_missing") {
2221
2263
  throw new ArchiveHistoryError(
@@ -2232,8 +2274,8 @@ async function reopenChange(name, options) {
2232
2274
  throw new ArchiveHistoryError("focus_marker_exists", `focus marker already exists for non-open Change: ${name}; run rsp doctor`);
2233
2275
  const archivedContent = await readFile10(record.sourcePath, "utf-8");
2234
2276
  const reopenedContent = appendChecklistItem(
2235
- appendChecklistItem(archivedContent, "Tasks", `- [ ] Resolve reopened concern: ${reason}`),
2236
- "Verify",
2277
+ appendChecklistItem(archivedContent, "tasks", `- [ ] Resolve reopened concern: ${reason}`),
2278
+ "verify",
2237
2279
  `- [ ] Verify reopened concern: ${reason}`
2238
2280
  );
2239
2281
  await mkdir9(dirname4(workRef.path), { recursive: true });
@@ -2254,9 +2296,9 @@ async function reopenChange(name, options) {
2254
2296
  }
2255
2297
  throw error;
2256
2298
  }
2257
- console.log(` ${pc.green("Reopened:")} ${name}`);
2299
+ console.log(` ${pc.green("Reopened:")} ${workRef.name}`);
2258
2300
  console.log(` ${pc.dim("retained archive")} \u2192 ${record.path}`);
2259
- console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
2301
+ console.log(` ${pc.dim("focused via focus.d")} \u2192 ${workRef.name}`);
2260
2302
  console.log(` ${pc.cyan("Next:")} refine the reopened Task and Verify evidence, then resolve the concern
2261
2303
  `);
2262
2304
  if (existsSync7(".git")) {
@@ -2280,21 +2322,18 @@ async function reopenChange(name, options) {
2280
2322
  throw error;
2281
2323
  }
2282
2324
  }
2283
- function appendChecklistItem(content, heading, item) {
2284
- const headings = [...content.matchAll(new RegExp(`^## ${heading}[ \\t]*\\r?$`, "gm"))];
2285
- if (headings.length !== 1) {
2325
+ function appendChecklistItem(content, sectionId, item) {
2326
+ try {
2327
+ return appendDocumentSectionItem(parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), sectionId, item);
2328
+ } catch (error) {
2329
+ if (!(error instanceof DocumentSectionCardinalityError))
2330
+ throw error;
2331
+ const heading = CHANGE_DOCUMENT_SCHEMA.sections.find((section) => section.id === sectionId).heading;
2286
2332
  throw new ArchiveHistoryError(
2287
2333
  "archive_section_invalid",
2288
2334
  `archived Change must contain exactly one required section: ${heading}`
2289
2335
  );
2290
2336
  }
2291
- const sectionBodyStart = headings[0].index + headings[0][0].length;
2292
- const nextHeading = content.indexOf("\n## ", sectionBodyStart);
2293
- const insertAt = nextHeading < 0 ? content.length : nextHeading;
2294
- const before = content.slice(0, insertAt);
2295
- const separator = before.endsWith("\n\n") ? "" : before.endsWith("\n") ? "\n" : "\n\n";
2296
- return `${before}${separator}${item}
2297
- ${content.slice(insertAt)}`;
2298
2337
  }
2299
2338
 
2300
2339
  // src/commands/show.ts
@@ -2488,6 +2527,7 @@ function deriveStatusView(snapshot, options = {}) {
2488
2527
  const nextActions = ok ? buildStatusNextActions(snapshot.focused.length, snapshot.records.map((record) => record.output), snapshot.groups, filtered) : ["Run: rsp doctor"];
2489
2528
  return {
2490
2529
  manage: snapshot.manage,
2530
+ language: snapshot.language,
2491
2531
  query,
2492
2532
  focused: snapshot.focused,
2493
2533
  records,
@@ -2584,6 +2624,8 @@ function printStatusPlain(view) {
2584
2624
  console.log(` ${pc.bold("RSP status")}`);
2585
2625
  console.log();
2586
2626
  console.log(` ${pc.bold("Manage:")} activation ${view.manage.activation} \xB7 closeout ${view.manage.closeout}`);
2627
+ if (view.language.artifacts !== null || view.language.commit !== null)
2628
+ console.log(` ${pc.bold("Language:")} artifacts ${view.language.artifacts ?? "unset"} \xB7 commit ${view.language.commit ?? "unset"}`);
2587
2629
  console.log();
2588
2630
  for (const diagnostic of view.diagnostics) {
2589
2631
  const label = diagnostic.change ?? diagnostic.path;
@@ -2703,6 +2745,7 @@ function toStatusJson(view) {
2703
2745
  command: "status",
2704
2746
  ok: view.ok,
2705
2747
  manage: view.manage,
2748
+ language: view.language,
2706
2749
  filters: view.query,
2707
2750
  focused: view.focused,
2708
2751
  records: view.records.map((record) => record.output),
@@ -2720,6 +2763,7 @@ function toStatusJsonError(error, options) {
2720
2763
  command: "status",
2721
2764
  ok: false,
2722
2765
  manage: { activation: "explicit", closeout: "manual" },
2766
+ language: { artifacts: null, commit: null },
2723
2767
  filters: {
2724
2768
  focused: options.focused,
2725
2769
  blocked: options.blocked,
@@ -3352,7 +3396,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3352
3396
  if (shouldLaunchSkillsUi(rawArgs, terminalEnvironment)) {
3353
3397
  const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
3354
3398
  const locale = resolveUiLocale("auto", process.env.RSP_UI_LANG, process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? new Intl.DateTimeFormat().resolvedOptions().locale);
3355
- const { runSkillsTui } = await import("./entry-WZDVKDUM.mjs");
3399
+ const { runSkillsTui } = await import("./entry-5OPKUZHF.mjs");
3356
3400
  const selection = await runSkillsTui(locale);
3357
3401
  if (selection.kind === "confirmed") {
3358
3402
  const result = await installPackagedSkills({ names: selection.names, force: selection.force });
@@ -3370,7 +3414,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3370
3414
  } else {
3371
3415
  const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
3372
3416
  const locale = resolveUiLocale(lang, process.env.RSP_UI_LANG, process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? new Intl.DateTimeFormat().resolvedOptions().locale);
3373
- const { runTui } = await import("./entry-X23GPCVS.mjs");
3417
+ const { runTui } = await import("./entry-FVUG6YW2.mjs");
3374
3418
  const exitCode = await runTui(locale);
3375
3419
  if (exitCode !== 0)
3376
3420
  process.exitCode = exitCode;
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  inspectPackagedSkillInventory
4
- } from "./chunk-NX55NIC3.mjs";
4
+ } from "./chunk-D7XDXBFO.mjs";
5
5
  import {
6
6
  openTerminalSession,
7
7
  truncateDisplay
8
8
  } from "./chunk-JOMFFQRG.mjs";
9
- import "./chunk-OTJFL6XH.mjs";
9
+ import "./chunk-FJVH7ZYU.mjs";
10
10
 
11
11
  // src/skills-tui/entry.tsx
12
12
  import process from "process";
@@ -7,14 +7,14 @@ import {
7
7
  projectDependencyForest,
8
8
  queryArchiveHistory,
9
9
  readArchiveHistoryDetail
10
- } from "./chunk-7KKAUBKN.mjs";
10
+ } from "./chunk-FI3SYHVH.mjs";
11
11
  import {
12
12
  displayWidth,
13
13
  openTerminalSession,
14
14
  truncateDisplay,
15
15
  wrapDisplay
16
16
  } from "./chunk-JOMFFQRG.mjs";
17
- import "./chunk-OTJFL6XH.mjs";
17
+ import "./chunk-FJVH7ZYU.mjs";
18
18
 
19
19
  // src/tui/entry.tsx
20
20
  import process from "process";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oevery/rsp",
3
3
  "type": "module",
4
- "version": "3.1.0-beta.3",
4
+ "version": "3.1.0-beta.4",
5
5
  "description": "RSP (Reliable Software Practice), a repository-native engineering workflow for humans and AI agents",
6
6
  "author": "oevery",
7
7
  "license": "MIT",
@@ -18,7 +18,8 @@ This file is the minimal fallback protocol for agents that cannot load the `rsp`
18
18
 
19
19
  ## Core
20
20
 
21
- - Choose response language and artifact language independently. Localize response prose from the explicit request, scoped instructions, then conversation language; write authorized artifacts from the explicit request, scoped instructions, existing artifact language, then conversation language. Preserve canonical headings, WorkRefs, paths, commands, identifiers, and machine values.
21
+ - Keep response language user/session-owned and durable project language repository-owned. For response prose, use an explicit current response language, user/host personal instructions, then conversation language; project `.rsp/config.yaml` never selects it. For a new authorized artifact, use an explicit artifact language, then the configured effective artifact language, artifact-scoped project instructions, and conversation language. Preserve an existing artifact's established language unless translation is explicitly authorized; changing configuration never rewrites it. Commit-message prose follows an explicit current commit-language instruction, configured effective commit language, nearest repository commit authority, then clear recent non-merge history. Preserve canonical headings, paths, commands, identifiers, Conventional Commit types/scopes, trailers, and machine values. WorkRefs follow the Unicode identity contract rather than the natural-language fallback chain.
22
+ - Treat CLI-generated artifact scaffolds as deterministic structure, not natural-language generation: keep canonical structural tokens, neutral `<…>` placeholders, and verbatim user input; fill authorized prose from the effective artifact-language policy without a locale catalog or implicit English fallback.
22
23
  - Write persistent artifact prose in domain, system, user, or operator language. Mention AI or agents only when they are real product actors, consumers, interface participants, or constraints; agent authorship, execution narration, or convenience for a hypothetical future agent is not durable content.
23
24
  - `changes/` stores open work; `archives/` stores completed history; `specs/` stores stable current facts; the configured Decision Record path stores lasting rationale.
24
25
  - Every Change is one Markdown file with explicit `kind` and the fixed `Proposal`, `Spec`, `Design`, `Tasks`, `Verify`, and `Blockers` sections.
@@ -35,9 +36,9 @@ This file is the minimal fallback protocol for agents that cannot load the `rsp`
35
36
  - Persist only `open` and `archived`; readiness, blockers, verification, and next actions are derived.
36
37
  - When fresh evidence shows that an archived executable Change did not meet its original acceptance, explicitly use `rsp reopen <work-ref> --reason <text>` to restore the same WorkRef. Reopen retains the selected archive, focuses the open Change, and adds unfinished Task and Verify evidence. Multiple archives require exact `--from .rsp/archives/...` selection; open work wins over same-WorkRef history in current dependency state. Use a new corrective Change for genuinely new scope or an independently delivered correction. Do not reopen a closed Change Group implicitly.
37
38
  - Ordinary implementation is the default after unexplained failures are ruled out. Use test-first only when authority requires it or a concrete risk makes pre-mutation failure materially safer. Retain a new test only when it protects observable behavior or a real boundary with distinct proportionate value; otherwise remove the temporary probe and use decisive final verification.
38
- - Before later-turn mutation under a direct report, tiny, or small route, rederive from the newly authorized objective and prospective work. Cross-module implementation, multiple acceptance surfaces, repeated production-path correction, real-host validation, or lifecycle delivery requires the smallest sufficient WorkRef before mutation; unchanged tiny/small follow-ups stay direct. Elapsed time and message count alone never escalate work.
39
+ - Before later-turn mutation under a direct report, design, tiny, or small route, rederive from the newly authorized objective and prospective work. Cross-module implementation, multiple acceptance surfaces, repeated production-path correction, real-host validation, bounded review convergence, lifecycle delivery, or a clear ready successor requires the smallest sufficient WorkRef and fresh qualification before mutation; unchanged tiny/small follow-ups stay direct. Elapsed time and message count alone never escalate work.
39
40
  - Before focusing or mutating a different WorkRef, compare dirty product or durable-truth paths with the prior owner's declared or observed paths. Overlap requires continuation of the same open WorkRef, an authorized reopen, an explicitly authorized integration owner, or a stop for boundary resolution. Disjoint authorized work may proceed without staging or forcing a commit; insufficient ownership evidence stops the transition.
40
- - This fallback does not emulate `rsp-manage`, even when project configuration selects automatic activation or local closeout. Invalid configuration grants nothing and remains visible. Every `manage.closeout` preset stays advisory because fallback Core never selects or qualifies Manage. For an explicit managed request or an automatically eligible completion/continuation, resolve the smallest owner using ordinary Core rules: reuse one unambiguous selected ready Change, send tiny settled work direct without a synthetic Change, and shape only independently authorized planning artifacts for clear non-trivial work. Configuration grants no planning or product-mutation authority. Stop for material product, acceptance, interface, scope, mutation, or external-action decisions. Perform at most one ordinary Core or Discipline action, return its WorkRef and next action, and never dispatch, auto-continue successors, loop review corrections, archive, stage, commit, push, or persist controller state. Load the `rsp` and `rsp-manage` Skills when controller behavior is required.
41
+ - This fallback does not emulate `rsp-manage`, even when project configuration selects automatic activation or local closeout. Invalid configuration grants nothing and remains visible. Every `manage.closeout` preset stays advisory because fallback Core never selects or qualifies Manage. For an explicit managed request or an automatically eligible completion/continuation, resolve the smallest owner using ordinary Core rules: reuse one unambiguous selected ready Change, send tiny settled work direct without a synthetic Change, and shape only independently authorized planning artifacts for clear non-trivial work. Automatic eligibility includes independent slices, interruption recovery, or prospective execution spanning implementation plus integration/review/lifecycle, cross-module or cross-process mutation, real-host/provider/hardware verification, bounded finding convergence, or a clear ready successor. Decline as direct one-step work only when one owner, one local seam, one mutation pass, one decisive check, no managed lifecycle coordination, and no ready successor all hold; a selected ready completion or continuation that fails any one condition is non-small and requires loaded Manage rather than an unclassified fallback. Report that Manage is unavailable rather than selected, the decisive eligibility or exclusion, the exact direct action, and any applicable serial/parallel overlap reason; do not persist that reasoning. Configuration grants no planning or product-mutation authority. Stop for material product, acceptance, interface, scope, mutation, or external-action decisions. Perform at most one ordinary Core or Discipline action, return its WorkRef and next action, and never dispatch, auto-continue successors, loop review corrections, archive, stage, commit, push, or persist controller state. Load the `rsp` and `rsp-manage` Skills when controller behavior is required.
41
42
  - Route one explicit isolated domain, module/seam, or evidence-seeking question to installed `rsp-design`; use a compact manual design pass only when that Skill is unavailable. Without a selected Change, use report-only Pre-Change Design without inventing a WorkRef only when the question is already bounded; when outcome, scope, non-goals, acceptance, or decomposition remains materially unclear, route to Shape. With a selected Change, return evidence, alternatives, owner decisions, and any authorized planned-design update to the same WorkRef. Do not implement or write durable current truth from either mode.
42
43
  - Route release documentation only for an explicit release operation with a confirmed identity or range and unfinished surfaces; no Release Change is required. Confirm identity only from explicit instruction or authoritative configuration. Until then stay version-neutral and do not mutate versioned surfaces. Keep ledgers transient; use an optional Release Change only for material decisions, coordination, recovery, blockers, or acceptance. This route grants no Git, publication, deployment, or approval authority.
43
44
  - Do not infer current work from open Changes or filenames.
@@ -4,14 +4,14 @@ description: Use this skill when initializing RSP, operating an existing .rsp pr
4
4
  license: MIT
5
5
  metadata:
6
6
  author: oevery
7
- version: "2026.07.28.1"
7
+ version: "2026.07.28.4"
8
8
  ---
9
9
 
10
10
  # RSP Skill
11
11
 
12
12
  Use RSP to derive one current action from project authority and durable work artifacts. This Skill is the preferred operational guide; `.rsp/rsp-rules.md` is the runtime fallback when the Skill is unavailable.
13
13
 
14
- Nearest project instructions and relevant `CONTEXT.md` remain authoritative. Choose response and artifact languages independently: localize human-facing response prose, but preserve canonical RSP headings, WorkRefs, paths, commands, identifiers, and machine values. Write persistent artifacts in domain, system, user, or operator language; mention AI or agents only when they are actual product actors or constraints.
14
+ Nearest project instructions and relevant `CONTEXT.md` remain authoritative. Keep response language user/session-owned and durable project language repository-owned. Response prose follows an explicit current response language, user/host personal instructions, then conversation language; project `.rsp/config.yaml` never selects it. A new authorized artifact follows an explicit artifact language, then the configured effective artifact language, artifact-scoped project instructions, and conversation language; an existing artifact keeps its established language unless translation is explicitly authorized. Configuration changes never rewrite existing artifacts. Commit-message prose follows an explicit current commit-language instruction, configured effective commit language, nearest repository commit authority, then clear recent non-merge history. Preserve canonical headings, paths, commands, identifiers, Conventional Commit types/scopes, trailers, and machine values; WorkRefs follow the Unicode identity contract rather than the natural-language fallback chain. Write persistent artifacts in domain, system, user, or operator language; mention AI or agents only when they are actual product actors or constraints.
15
15
 
16
16
  ## Scope
17
17
 
@@ -23,7 +23,7 @@ Read user intent, nearest authority, `rsp status --json`, the selected Change an
23
23
 
24
24
  Apply these routes in order:
25
25
 
26
- Before later-turn mutation under a direct report, tiny, or small route, rederive routing from the now-authorized objective and prospective work. If it materially expands into cross-module implementation, multiple acceptance surfaces, repeated production-path correction, real-host validation, or lifecycle delivery, establish or reuse the smallest sufficient WorkRef, then apply managed preflight and fresh qualification before mutation. Follow-ups that remain tiny/small stay direct. Elapsed time and message count are never escalation evidence.
26
+ Before later-turn mutation under a direct report, design, tiny, or small route, rederive routing from the now-authorized objective and prospective work. If it materially expands into cross-module implementation, multiple acceptance surfaces, repeated production-path correction, real-host validation, bounded review convergence, lifecycle delivery, or a clear ready successor, establish or reuse the smallest sufficient WorkRef, then apply managed preflight and fresh qualification before mutation. Follow-ups that remain tiny/small stay direct. Elapsed time and message count are never escalation evidence.
27
27
 
28
28
  1. Stop for ambiguous authority or selection and return the one required input and its owner. For a material owner decision inside an explicit managed request, continue to the Shape preflight in route 2 so repository inspection precedes the single owner question; otherwise stop here.
29
29
  2. For a requested completion or continuation that is not a report-only review or release operation, read [managed routing](references/managed-routing.md) when the request is explicitly managed or the effective status policy has `manage.activation: auto`. It owns owner preflight, qualification, successor discovery, bounded review convergence, and lifecycle/Git separation. Select `rsp-manage` only after that preflight for an eligible selected ready Change or shallow Group; otherwise return the ordinary Core or Discipline action. Automatic activation grants controller selection only; planning, product mutation, lifecycle, Git, and external authority remain separately derived.
@@ -35,7 +35,7 @@ Before later-turn mutation under a direct report, tiny, or small route, rederive
35
35
  8. For incomplete implementation or stale, missing, or failed verification, use [Implementation evidence](#implementation-evidence).
36
36
  9. When Tasks and required verification pass without blockers, perform the durable decision. Do not infer a release operation from Change completion.
37
37
 
38
- State the derived stage, one next action, required input, returned owner, and decisive evidence. Name at most one optional capability, only when it is the next action and appears in the host's loaded Skill inventory. Missing capabilities never invalidate RSP: give the manual fallback against the same owner. When Core or qualified Manage has derived one authorized RSP-owned local commit boundary, route exact staging, structured message construction, local commit execution, and post-commit observation to `rsp-commit`; if unavailable, use the bounded manual action in durable review against the same owner. Do not preload, enumerate, or recursively invoke optional capabilities.
38
+ State the derived stage, one next action, required input, returned owner, and decisive evidence. When managed routing is evaluated, state `selected` or `declined` with the decisive qualification signal or complete direct-work exclusion; when dispatch applies, also state why it is parallel or sequential. Name at most one optional capability, only when it is the next action and appears in the host's loaded Skill inventory. Missing capabilities never invalidate RSP: give the manual fallback against the same owner. When Core or qualified Manage has derived one authorized RSP-owned local commit boundary, route exact staging, structured message construction, local commit execution, and post-commit observation to `rsp-commit`; if unavailable, use the bounded manual action in durable review against the same owner. Do not preload, enumerate, or recursively invoke optional capabilities.
39
39
 
40
40
  ### Implementation evidence
41
41
 
@@ -13,13 +13,17 @@ Before focusing, dispatching, or mutating a different WorkRef, inspect dirty pat
13
13
  - For clear non-trivial work with no sufficient ready owner, an explicit managed request authorizes only in-scope RSP planning artifacts unless the user requests no edits. Under automatic activation, route to Shape only when the current request or nearer authority independently permits those artifacts; configuration alone never does. Then re-read status/readiness and re-evaluate without another authorization round.
14
14
  - If repository evidence leaves a material product, acceptance, public-interface, scope, mutation-authority, external-action, or human choice, Shape returns the single highest-impact owner decision and creates no implementation or controller artifact.
15
15
 
16
- A prior direct report, tiny, or small route is not sticky. Before later-turn mutation, rederive from the now-authorized objective and prospective work. Material expansion into cross-module implementation, multiple acceptance surfaces, repeated production-path correction, real-host validation, or lifecycle delivery requires Core to establish or reuse the smallest sufficient WorkRef and rerun this preflight plus fresh Manage qualification before mutation. Unchanged tiny/small follow-ups remain direct; elapsed time and message count alone never trigger escalation.
16
+ A prior direct report, design, tiny, or small route is not sticky. Before later-turn mutation, rederive from the now-authorized objective and prospective work. Material expansion into cross-module implementation, multiple acceptance surfaces, repeated production-path correction, real-host validation, bounded review convergence, lifecycle delivery, or a clear ready successor requires Core to establish or reuse the smallest sufficient WorkRef and rerun this preflight plus fresh Manage qualification before mutation. Unchanged tiny/small follow-ups remain direct; elapsed time and message count alone never trigger escalation.
17
17
 
18
18
  PREFLIGHT is complete only when one selected ready Change or shallow Group owns the requested outcome and no material decision remains.
19
19
 
20
20
  ## QUALIFY — select or decline Manage
21
21
 
22
- Select `rsp-manage` only for one selected ready Change or shallow Group that qualifies through at least one independent path: genuinely independent slices, prospectively long authorized continuation, or interruption recovery. A Change with long/recovery work does not also need independent or parallelizable slices. Derive “long” before dispatch from the authorized objective and its expected phases; elapsed wall-clock minutes are never qualification evidence. Small coupled one-step work and worker-only work remain ineligible and return to ordinary Core or Discipline action. Automatic routing is policy-selected, bounded by the requested goal, and not inferred from readiness alone.
22
+ Select `rsp-manage` only for one selected ready Change or shallow Group that qualifies through at least one independent path: genuinely independent slices, interruption recovery, or prospective execution signals showing more than one bounded phase or authority surface. Prospective signals are implementation followed by integration verification, managed review, or lifecycle work; cross-module or cross-process mutation; real-host, provider, or hardware verification; bounded finding convergence; or a clear ready successor. A Change with prospective or recovery work does not also need independent or parallelizable slices. Derive these signals before dispatch from the authorized objective and expected phases; elapsed wall-clock minutes are never qualification evidence.
23
+
24
+ Under automatic activation, bias non-small continuation toward Manage. Decline as direct one-step work only when all of these are true: one owner, one local seam, one mutation pass, one decisive check, no managed lifecycle coordination, and no ready successor. A selected ready completion or continuation that fails any one of these conditions qualifies as non-small through this automatic path even when no separate prospective signal is obvious; do not leave the middle case unclassified. Worker-only work remains ineligible. Readiness alone does not qualify work without a requested completion or continuation, and automatic routing remains bounded by the requested goal.
25
+
26
+ Make the route observable: report `selected` with the decisive qualification signal, or `declined` with the complete direct-work exclusion and exact Core or Discipline action. If dispatch applies, report the concrete overlap/isolation evidence that makes it sequential or parallel. Selection, decline, and dispatch reasoning remain transient and create no controller state.
23
27
 
24
28
  ## CONTINUE — rederive from evidence
25
29
 
@@ -4,7 +4,7 @@ description: Create one authorized, exact-scope local commit for an RSP-owned Ch
4
4
  license: MIT
5
5
  metadata:
6
6
  author: oevery
7
- version: "2026.07.26.1"
7
+ version: "2026.07.28.2"
8
8
  ---
9
9
 
10
10
  # RSP Commit
@@ -19,7 +19,7 @@ Stop without staging when the owner, allowed paths, commit authority, verificati
19
19
 
20
20
  ## Derive the message
21
21
 
22
- Choose subject language from explicit current instruction, then nearest repository authority, then the clear style of recent non-merge commits. Response language, RSP artifact language, and preferences remembered from another repository do not select it. When recent history is materially mixed and no nearer rule resolves it, return the single language decision to its owner.
22
+ Choose subject and body prose language from explicit current commit-language instruction, then the configured effective commit language, nearest repository commit authority, and finally the clear style of recent non-merge commits. Response language and preferences remembered from another repository do not select it. Preserve Conventional Commit types/scopes and trailers as technical values. When recent history is materially mixed and no nearer rule resolves it, return the single language decision to its owner.
23
23
 
24
24
  Use the repository's established Conventional Commit form when present. Derive type and scope from the owned outcome and repository history, not from the conversation. Keep the subject concise and imperative or otherwise repository-consistent.
25
25