@oevery/rsp 3.1.0-beta.3 → 3.1.0-beta.5

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,14 +4,18 @@ import {
4
4
  installPackagedSkills,
5
5
  printPackagedSkillInventory,
6
6
  printSkillInstallResult
7
- } from "./chunk-NX55NIC3.mjs";
7
+ } from "./chunk-3X2V6IEW.mjs";
8
8
  import {
9
9
  ArchiveHistoryError,
10
10
  GROUP_BRIEF_FILENAME,
11
+ GROUP_REOPEN_COMPLETION_PREFIX,
11
12
  HISTORY_DEFAULT_LIMIT,
12
13
  HISTORY_MAX_LIMIT,
14
+ IssueRelationshipError,
13
15
  WorkRefError,
16
+ createIssueRelationship,
14
17
  generateGroupBriefContent,
18
+ getGroupReopenEvidence,
15
19
  hasArchivedGroupBrief,
16
20
  historyInspectionComplete,
17
21
  inspectArchiveHistory,
@@ -21,6 +25,9 @@ import {
21
25
  inspectFocusTree,
22
26
  inspectProjectStatus,
23
27
  inspectWorkTree,
28
+ normalizeExecutableWorkRef,
29
+ normalizeWorkRefSegment,
30
+ parseIssueRelationships,
24
31
  projectDependencyForest,
25
32
  queryArchiveHistory,
26
33
  readArchiveHistoryDetail,
@@ -28,20 +35,26 @@ import {
28
35
  resolveExecutableChange,
29
36
  resolveFocusMarkerPath,
30
37
  resolveWorkRef,
31
- selectArchiveHistoryRecord
32
- } from "./chunk-7KKAUBKN.mjs";
38
+ selectArchiveHistoryRecord,
39
+ selectArchivedGroupBrief
40
+ } from "./chunk-ZKZVWJQ7.mjs";
33
41
  import {
34
42
  ARCHIVES_DIR,
35
43
  CHANGES_DIR,
44
+ CHANGE_DOCUMENT_SCHEMA,
36
45
  CONFIG_PATH,
37
46
  DEFAULT_DECISION_RECORDS_PATH,
47
+ DocumentSectionCardinalityError,
38
48
  FOCUS_DIR,
49
+ GROUP_BRIEF_DOCUMENT_SCHEMA,
39
50
  LOCK_PATH,
51
+ ManagedPathError,
40
52
  OBSOLETE_RSP_RULES_PATH,
41
53
  PKG_ROOT,
42
54
  RSP_DIR,
43
55
  RSP_RULES_PATH,
44
56
  VALID_KINDS,
57
+ appendDocumentSectionItem,
45
58
  buildDurableReviewGuidance,
46
59
  cleanupEmptyParentDirs,
47
60
  clearConfigCache,
@@ -56,6 +69,10 @@ import {
56
69
  generateChangeContent,
57
70
  generateDesignContent,
58
71
  generateSpecContent,
72
+ getDocumentSectionBody,
73
+ getDocumentSectionDefinitionByHeading,
74
+ getDocumentSections,
75
+ getDocumentTitles,
59
76
  getDurableReviewCandidateTargets,
60
77
  getVersion,
61
78
  guardRspInitialized,
@@ -70,6 +87,7 @@ import {
70
87
  loadRspConfig,
71
88
  normalizeLogicalPath,
72
89
  parseFrontmatter,
90
+ parseRspDocument,
73
91
  parseScenarios,
74
92
  pc,
75
93
  recordRuntimeDiagnostic,
@@ -83,7 +101,7 @@ import {
83
101
  validateDecisionRecordsFilesystemPath,
84
102
  walkMarkdownFiles,
85
103
  writeManagedFile
86
- } from "./chunk-OTJFL6XH.mjs";
104
+ } from "./chunk-UVYEXKYS.mjs";
87
105
 
88
106
  // src/cli.ts
89
107
  import { resolve } from "path";
@@ -418,7 +436,7 @@ async function addSpec(name, projectName = "project") {
418
436
  }
419
437
 
420
438
  // src/commands/archive.ts
421
- import { existsSync } from "fs";
439
+ import { existsSync, readdirSync } from "fs";
422
440
  import { mkdir as mkdir3, readFile as readFile3, rename, unlink as unlink4 } from "fs/promises";
423
441
  import { basename as basename2, join as join3 } from "path";
424
442
  async function archiveChange(name) {
@@ -436,7 +454,7 @@ async function archiveChange(name) {
436
454
  const content = await readFile3(srcPath, "utf-8");
437
455
  const dependencyInspection = await inspectChangeDependencies();
438
456
  const checklist = collectArchiveChecklist(content, {
439
- activeBlockers: dependencyInspection.activeBlockers.get(name)
457
+ activeBlockers: dependencyInspection.activeBlockers.get(workRef.name)
440
458
  });
441
459
  for (const line of checklist)
442
460
  console.log(` ${pc.yellow("\u26A0")} ${line}`);
@@ -491,12 +509,12 @@ ${clearedMsg}`);
491
509
  }
492
510
  function resolveArchiveName(archiveSubdir, date, base) {
493
511
  const initialName = `${date}_${base}.md`;
494
- if (!existsSync(join3(archiveSubdir, initialName)))
512
+ if (!archiveNameExists(archiveSubdir, initialName))
495
513
  return initialName;
496
514
  const MAX_SUFFIX = 9999;
497
515
  for (let index = 2; index <= MAX_SUFFIX; index++) {
498
516
  const candidate = `${date}_${base}-${index}.md`;
499
- if (!existsSync(join3(archiveSubdir, candidate)))
517
+ if (!archiveNameExists(archiveSubdir, candidate))
500
518
  return candidate;
501
519
  }
502
520
  throw new WorkRefError(
@@ -505,6 +523,22 @@ function resolveArchiveName(archiveSubdir, date, base) {
505
523
  base
506
524
  );
507
525
  }
526
+ function archiveNameExists(archiveSubdir, name) {
527
+ if (!existsSync(archiveSubdir))
528
+ return false;
529
+ return readdirSync(archiveSubdir).some((entry) => {
530
+ if (entry.normalize("NFC") !== name)
531
+ return false;
532
+ if (entry !== name) {
533
+ throw new WorkRefError(
534
+ "work_ref_collision",
535
+ `archive name has a Unicode normalization collision: ${entry} and ${name}`,
536
+ name
537
+ );
538
+ }
539
+ return true;
540
+ });
541
+ }
508
542
 
509
543
  // src/commands/check.ts
510
544
  import { readFile as readFile4 } from "fs/promises";
@@ -696,8 +730,10 @@ async function runCheck(options = {}) {
696
730
  message: "missing YAML frontmatter"
697
731
  });
698
732
  }
733
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
699
734
  for (const section of requiredSections) {
700
- if (!new RegExp(`^## ${section}$`, "m").test(content)) {
735
+ const definition = getDocumentSectionDefinitionByHeading(CHANGE_DOCUMENT_SCHEMA, section);
736
+ if (!definition || !getDocumentSections(document, definition.id).some((candidate) => candidate.canonical)) {
701
737
  addDiagnostic({
702
738
  severity: "error",
703
739
  code: "missing_section",
@@ -708,6 +744,17 @@ async function runCheck(options = {}) {
708
744
  }
709
745
  }
710
746
  if (fm) {
747
+ try {
748
+ parseIssueRelationships(fm);
749
+ } catch (error) {
750
+ addDiagnostic({
751
+ severity: "error",
752
+ code: error instanceof IssueRelationshipError ? error.code : "invalid_issue_metadata",
753
+ change: name,
754
+ path: fp,
755
+ message: error instanceof Error ? error.message : String(error)
756
+ });
757
+ }
711
758
  if (!("kind" in fm)) {
712
759
  addDiagnostic({
713
760
  severity: "error",
@@ -734,16 +781,14 @@ async function runCheck(options = {}) {
734
781
  });
735
782
  }
736
783
  }
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) {
784
+ if (document.title !== null) {
785
+ if (document.title !== name) {
741
786
  addDiagnostic({
742
787
  severity: "error",
743
788
  code: "heading_mismatch",
744
789
  change: name,
745
790
  path: fp,
746
- message: `# Change: heading "${heading}" differs from change name`
791
+ message: `# Change: heading "${document.title}" differs from change name`
747
792
  });
748
793
  }
749
794
  } else {
@@ -866,7 +911,7 @@ async function runCheck(options = {}) {
866
911
  return result;
867
912
  }
868
913
  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;
914
+ 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
915
  return collectMatchingLines(content, (line) => {
871
916
  const trimmed = stripInlineCodeSpans(line).trim();
872
917
  return trimmed.startsWith("<!--") === false && placeholderRe.test(trimmed);
@@ -898,15 +943,21 @@ async function createChange(name, summary = "", kind, options = {}) {
898
943
  console.error(` ${pc.red("Usage:")} rsp create <name> [summary]`);
899
944
  process.exit(1);
900
945
  }
946
+ const hasIssueOption = options.issue !== void 0;
947
+ if (options.issueRelation && !hasIssueOption)
948
+ throw new Error("--issue-relation requires --issue");
949
+ const issues = options.issue !== void 0 ? [createIssueRelationship(options.issue, options.issueRelation)] : [];
901
950
  guardRspInitialized();
902
951
  try {
903
952
  return await withRspLock("create-change", async () => {
904
953
  const workRef = await resolveExecutableChange(name);
905
954
  const changePath = workRef.path;
906
955
  const existed = existsSync2(changePath);
956
+ if (existed && hasIssueOption)
957
+ throw new Error(`Change already exists; --issue cannot update ${workRef.name}`);
907
958
  if (!existed) {
908
959
  const focusEntry = resolveFocusMarkerPath(workRef);
909
- const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
960
+ const content = generateChangeContent(workRef.name, summary, kind, { lite: Boolean(options.lite), issues });
910
961
  await mkdir4(dirname2(changePath), { recursive: true });
911
962
  let changeCreated = false;
912
963
  try {
@@ -929,9 +980,9 @@ async function createChange(name, summary = "", kind, options = {}) {
929
980
  const label = existed ? "Using" : pc.green("Created");
930
981
  console.log(` ${label}: ${changePath}`);
931
982
  if (existed)
932
- console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${name}`);
983
+ console.log(` ${pc.dim("Unchanged focus.")} Run: rsp focus ${workRef.name}`);
933
984
  else
934
- console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
985
+ console.log(` ${pc.dim("focused via focus.d")} \u2192 ${workRef.name}`);
935
986
  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
987
  `);
937
988
  });
@@ -1565,8 +1616,8 @@ async function focusChange(name) {
1565
1616
  await mkdir6(FOCUS_DIR, { recursive: true });
1566
1617
  await mkdir6(dirname3(focusEntry), { recursive: true });
1567
1618
  await writeManagedFile(focusEntry, "", "focus marker");
1568
- console.log(` ${pc.green("Focused:")} ${name}`);
1569
- console.log(` ${pc.dim("focus.d")} \u2192 ${name}`);
1619
+ console.log(` ${pc.green("Focused:")} ${workRef.name}`);
1620
+ console.log(` ${pc.dim("focus.d")} \u2192 ${workRef.name}`);
1570
1621
  console.log();
1571
1622
  });
1572
1623
  } catch (error) {
@@ -1587,8 +1638,8 @@ async function unfocusChange(name) {
1587
1638
  throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
1588
1639
  await unlink7(focusEntry);
1589
1640
  await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
1590
- console.log(` ${pc.green("Unfocused:")} ${name}`);
1591
- console.log(` ${pc.dim("focus.d cleared")} \u2192 ${name}`);
1641
+ console.log(` ${pc.green("Unfocused:")} ${workRef.name}`);
1642
+ console.log(` ${pc.dim("focus.d cleared")} \u2192 ${workRef.name}`);
1592
1643
  console.log();
1593
1644
  });
1594
1645
  } catch (error) {
@@ -1607,7 +1658,9 @@ function exitWorkRefError(error) {
1607
1658
  }
1608
1659
 
1609
1660
  // src/commands/group.ts
1610
- import { mkdir as mkdir7, rename as rename2, writeFile as writeFile4 } from "fs/promises";
1661
+ import { randomUUID } from "crypto";
1662
+ import { existsSync as existsSync6 } from "fs";
1663
+ import { link, mkdir as mkdir7, readdir as readdir2, readFile as readFile8, rename as rename2, unlink as unlink8, writeFile as writeFile4 } from "fs/promises";
1611
1664
  import { join as join7 } from "path";
1612
1665
  var ChangeGroupError = class extends Error {
1613
1666
  name = "ChangeGroupError";
@@ -1623,20 +1676,21 @@ async function createChangeGroup(name, goal = "") {
1623
1676
  const ref = resolveWorkRef(`${name}/brief`);
1624
1677
  if (ref.kind !== "group-brief")
1625
1678
  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");
1679
+ const groupName = ref.group;
1680
+ if (await hasArchivedGroupBrief(groupName))
1681
+ throw new ChangeGroupError(`archived Change Group cannot be reopened: ${groupName}`);
1682
+ const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [groupName], "Change Group path");
1629
1683
  await mkdir7(groupDirectory, { recursive: true });
1630
1684
  try {
1631
- await writeFile4(ref.path, generateGroupBriefContent(name, goal), { flag: "wx" });
1685
+ await writeFile4(ref.path, generateGroupBriefContent(groupName, goal), { flag: "wx" });
1632
1686
  } catch (error) {
1633
1687
  if (error.code === "EEXIST")
1634
- throw new ChangeGroupError(`Change Group already exists: ${name}`);
1688
+ throw new ChangeGroupError(`Change Group already exists: ${groupName}`);
1635
1689
  throw error;
1636
1690
  }
1637
- console.log(` ${pc.green("Created Change Group:")} ${name}`);
1691
+ console.log(` ${pc.green("Created Change Group:")} ${groupName}`);
1638
1692
  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>
1693
+ console.log(` ${pc.cyan("Next:")} fill the brief, then create direct child Changes with rsp create ${groupName}/<change>
1640
1694
  `);
1641
1695
  });
1642
1696
  } catch (error) {
@@ -1659,12 +1713,13 @@ async function closeChangeGroup(name) {
1659
1713
  if (ref.kind !== "group-brief")
1660
1714
  throw new ChangeGroupError(`invalid Change Group name: ${name}`);
1661
1715
  const inspection = await inspectChangeGroups();
1662
- const group = inspection.groups.find((candidate) => candidate.name === name);
1716
+ const groupName = ref.group;
1717
+ const group = inspection.groups.find((candidate) => candidate.name === groupName);
1663
1718
  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));
1719
+ throw new ChangeGroupError(`Change Group not found: ${groupName}`);
1720
+ const reasons = collectCloseReasons(group, inspection.diagnostics.filter((diagnostic) => diagnostic.change === groupName && diagnostic.severity === "error").map((diagnostic) => diagnostic.message));
1666
1721
  if (reasons.length > 0)
1667
- throw new ChangeGroupError(`Change Group "${name}" is not ready to close: ${reasons.join("; ")}`);
1722
+ throw new ChangeGroupError(`Change Group "${groupName}" is not ready to close: ${reasons.join("; ")}`);
1668
1723
  const archiveDirectory = resolveArchiveDirectory(ref);
1669
1724
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1670
1725
  const archiveName = resolveArchiveName(archiveDirectory, date, "brief");
@@ -1676,7 +1731,7 @@ async function closeChangeGroup(name) {
1676
1731
  } catch (error) {
1677
1732
  warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
1678
1733
  }
1679
- console.log(` ${pc.green("Closed Change Group:")} ${name}`);
1734
+ console.log(` ${pc.green("Closed Change Group:")} ${groupName}`);
1680
1735
  console.log(` ${pc.dim("Archived Group Brief:")} ${join7(archiveDirectory, archiveName)}
1681
1736
  `);
1682
1737
  for (const warning of warnings)
@@ -1693,6 +1748,136 @@ async function closeChangeGroup(name) {
1693
1748
  throw error;
1694
1749
  }
1695
1750
  }
1751
+ async function reopenChangeGroup(name, options) {
1752
+ if (!name) {
1753
+ console.error(` ${pc.red("Usage:")} rsp group reopen <name> --reason <text> [--from <archive-path>]`);
1754
+ process.exit(1);
1755
+ }
1756
+ const reason = options.reason?.trim();
1757
+ if (!reason || /[\r\n]/.test(reason)) {
1758
+ console.error(` ${pc.red("Error:")} --reason must be one non-empty line`);
1759
+ process.exit(1);
1760
+ }
1761
+ guardRspInitialized();
1762
+ try {
1763
+ await withRspLock("reopen-change-group", async () => {
1764
+ const groupName = normalizeWorkRefSegment(name);
1765
+ await requireEmptyGroupRecoverySubtree(join7(CHANGES_DIR, groupName), `Change Group work subtree for ${groupName}`);
1766
+ await requireEmptyGroupRecoverySubtree(join7(FOCUS_DIR, groupName), `Change Group focus subtree for ${groupName}`);
1767
+ const ref = resolveWorkRef(`${groupName}/brief`);
1768
+ if (ref.kind !== "group-brief")
1769
+ throw new ChangeGroupError(`invalid Change Group name: ${name}`);
1770
+ const inspection = await inspectArchiveHistory();
1771
+ if (!historyInspectionComplete(inspection)) {
1772
+ throw new ArchiveHistoryError(
1773
+ "archive_inspection_incomplete",
1774
+ "archive history inspection is incomplete; resolve the reported diagnostics before reopening a Change Group"
1775
+ );
1776
+ }
1777
+ const record = selectArchivedGroupBrief(
1778
+ inspection.groupBriefs,
1779
+ options.from ? { path: options.from } : { group: groupName }
1780
+ );
1781
+ if (record.group !== groupName) {
1782
+ throw new ArchiveHistoryError(
1783
+ "archive_identity_mismatch",
1784
+ `selected archived Change Group belongs to ${record.group}, not ${groupName}`
1785
+ );
1786
+ }
1787
+ if (existsSync6(ref.path))
1788
+ throw new ArchiveHistoryError("open_group_exists", `open Change Group already exists: ${groupName}`);
1789
+ const archivedContent = await readFile8(record.sourcePath, "utf-8");
1790
+ const document = parseRspDocument(archivedContent, GROUP_BRIEF_DOCUMENT_SCHEMA);
1791
+ validateArchivedGroupBrief(document, groupName);
1792
+ const evidenceKey = `${GROUP_REOPEN_COMPLETION_PREFIX} \`${record.path}\`: ${reason}`;
1793
+ const retainedEvidence = /* @__PURE__ */ new Set();
1794
+ for (const retained of inspection.groupBriefs.filter((candidate) => candidate.group === groupName)) {
1795
+ const content = retained.sourcePath === record.sourcePath ? archivedContent : await readFile8(retained.sourcePath, "utf-8");
1796
+ for (const item of getGroupReopenEvidence(content))
1797
+ retainedEvidence.add(item.key);
1798
+ }
1799
+ if (retainedEvidence.has(evidenceKey)) {
1800
+ throw new ArchiveHistoryError(
1801
+ "archive_reopen_evidence_replayed",
1802
+ `reopen evidence already exists in retained Change Group history; choose a fresh reason or a different exact archive: ${record.path}`
1803
+ );
1804
+ }
1805
+ const reopenedContent = appendDocumentSectionItem(
1806
+ document,
1807
+ "completionConditions",
1808
+ `- [ ] ${evidenceKey}`
1809
+ );
1810
+ const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [groupName], "Change Group path");
1811
+ await mkdir7(groupDirectory, { recursive: true });
1812
+ const temporaryPath = join7(RSP_DIR, `.group-reopen-${process.pid}-${randomUUID()}.tmp`);
1813
+ let mutationError;
1814
+ try {
1815
+ await writeFile4(temporaryPath, reopenedContent, { flag: "wx" });
1816
+ await link(temporaryPath, ref.path);
1817
+ } catch (error) {
1818
+ mutationError = error;
1819
+ }
1820
+ try {
1821
+ await unlink8(temporaryPath);
1822
+ } catch (error) {
1823
+ if (!mutationError)
1824
+ console.log(` ${pc.yellow("\u26A0")} temporary reopen file cleanup failed: ${toErrorMessage(error)}`);
1825
+ }
1826
+ if (mutationError) {
1827
+ try {
1828
+ await cleanupEmptyParentDirs(ref.path, CHANGES_DIR);
1829
+ } catch {
1830
+ }
1831
+ if (mutationError.code === "EEXIST")
1832
+ throw new ArchiveHistoryError("open_group_exists", `open Change Group already exists: ${groupName}`);
1833
+ throw mutationError;
1834
+ }
1835
+ console.log(` ${pc.green("Reopened Change Group:")} ${groupName}`);
1836
+ console.log(` ${pc.dim("retained archive")} \u2192 ${record.path}`);
1837
+ console.log(` ${pc.dim("unfocused Group Brief")} \u2192 ${ref.path}`);
1838
+ console.log(` ${pc.cyan("Next:")} run rsp reopen ${groupName}/<change> --reason <text> only for an incomplete archived child
1839
+ `);
1840
+ });
1841
+ } catch (error) {
1842
+ if (error instanceof ArchiveHistoryError || error instanceof WorkRefError || error instanceof ChangeGroupError || error instanceof ManagedPathError) {
1843
+ console.error(` ${pc.red("Error:")} ${error.message}`);
1844
+ if (error instanceof ArchiveHistoryError) {
1845
+ for (const candidate of error.candidates)
1846
+ console.error(` ${candidate}`);
1847
+ if (error.candidatesTruncated)
1848
+ console.error(` ... ${error.candidateTotal - error.candidates.length} more`);
1849
+ }
1850
+ process.exit(1);
1851
+ }
1852
+ throw error;
1853
+ }
1854
+ }
1855
+ async function requireEmptyGroupRecoverySubtree(path, label) {
1856
+ if (!requireManagedDirectory(path, label, { allowMissing: true }))
1857
+ return;
1858
+ let entries;
1859
+ try {
1860
+ entries = await readdir2(path);
1861
+ } catch (error) {
1862
+ throw new ChangeGroupError(`unable to inspect ${label}: ${toErrorMessage(error)}`);
1863
+ }
1864
+ if (entries.length > 0)
1865
+ throw new ChangeGroupError(`${label} must be absent or empty before reopen; found: ${entries.sort().join(", ")}`);
1866
+ }
1867
+ function validateArchivedGroupBrief(document, group) {
1868
+ const titles = getDocumentTitles(document, "identity");
1869
+ if (titles.length !== 1 || titles[0] !== group)
1870
+ throw new ArchiveHistoryError("archive_heading_invalid", `archived Change Group must contain exactly one canonical identity heading for ${group}`);
1871
+ for (const section of GROUP_BRIEF_DOCUMENT_SCHEMA.sections) {
1872
+ const matches = getDocumentSections(document, section.id);
1873
+ if (matches.length !== 1 || !matches[0].canonical || !getDocumentSectionBody(document, section.id)) {
1874
+ throw new ArchiveHistoryError(
1875
+ "archive_section_invalid",
1876
+ `archived Change Group must contain exactly one canonical required section: ${section.heading}`
1877
+ );
1878
+ }
1879
+ }
1880
+ }
1696
1881
  function collectCloseReasons(group, errors) {
1697
1882
  const reasons = [...errors, ...group.warnings];
1698
1883
  const open = group.slices.filter((slice) => slice.state === "open").map((slice) => slice.name);
@@ -1712,8 +1897,6 @@ function collectCloseReasons(group, errors) {
1712
1897
 
1713
1898
  // src/commands/history.ts
1714
1899
  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
1900
  async function showHistory(input = {}, options = {}) {
1718
1901
  guardRspInitialized();
1719
1902
  const mode = input.workRef ? "detail" : "list";
@@ -1727,15 +1910,15 @@ async function showHistory(input = {}, options = {}) {
1727
1910
  message: "archive history inspection is incomplete; resolve the reported diagnostics before querying history"
1728
1911
  }, inspection.diagnostics, inspection.diagnosticSummary, inspection.runtime, options);
1729
1912
  }
1730
- if (input.workRef) {
1913
+ if (validation.workRef) {
1731
1914
  try {
1732
- const selected = selectArchiveHistoryRecord(inspection.records, { workRef: input.workRef });
1915
+ const selected = selectArchiveHistoryRecord(inspection.records, { workRef: validation.workRef });
1733
1916
  const record = await readArchiveHistoryDetail(selected);
1734
1917
  const result2 = {
1735
1918
  command: "history",
1736
1919
  ok: true,
1737
1920
  mode: "detail",
1738
- query: { workRef: input.workRef },
1921
+ query: { workRef: validation.workRef },
1739
1922
  record,
1740
1923
  diagnostics: [],
1741
1924
  diagnosticSummary: emptyCollectionSummary(),
@@ -1788,11 +1971,15 @@ function validateHistoryQuery(input) {
1788
1971
  if ((input.positionalCount ?? (input.workRef ? 1 : 0)) > 1)
1789
1972
  return invalid("history_positional_args_unsupported", "history accepts at most one positional WorkRef");
1790
1973
  if (input.workRef) {
1791
- if (!WORK_REF_RE.test(input.workRef) || input.workRef.endsWith("/brief") || input.workRef.endsWith("/00-brief"))
1974
+ let workRef;
1975
+ try {
1976
+ workRef = normalizeExecutableWorkRef(input.workRef);
1977
+ } catch {
1792
1978
  return invalid("invalid_history_work_ref", "history WorkRef must be a flat or one-Group-level executable Change identity");
1979
+ }
1793
1980
  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
1981
  return invalid("history_detail_filters_unsupported", "list filters cannot be combined with an exact history detail lookup");
1795
- return { ok: true, query: {} };
1982
+ return { ok: true, query: {}, workRef };
1796
1983
  }
1797
1984
  const limit = input.limit === void 0 ? HISTORY_DEFAULT_LIMIT : Number(input.limit);
1798
1985
  if (input.limit !== void 0 && !/^[1-9]\d*$/.test(input.limit) || !Number.isInteger(limit) || limit < 1 || limit > HISTORY_MAX_LIMIT)
@@ -1805,8 +1992,14 @@ function validateHistoryQuery(input) {
1805
1992
  return invalid("invalid_history_date_range", "--since must be on or before --until");
1806
1993
  if (input.kind !== void 0 && input.kind.trim() === "")
1807
1994
  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");
1995
+ let group = input.group;
1996
+ if (group !== void 0) {
1997
+ try {
1998
+ group = normalizeWorkRefSegment(group);
1999
+ } catch {
2000
+ return invalid("invalid_history_group", "--group must be one safe Unicode Group name");
2001
+ }
2002
+ }
1810
2003
  if (input.search !== void 0 && input.search.trim() === "")
1811
2004
  return invalid("invalid_history_search", "--search must be non-empty");
1812
2005
  return {
@@ -1816,7 +2009,7 @@ function validateHistoryQuery(input) {
1816
2009
  since: input.since,
1817
2010
  until: input.until,
1818
2011
  kind: input.kind?.trim(),
1819
- group: input.group,
2012
+ group,
1820
2013
  search: input.search?.trim()
1821
2014
  }
1822
2015
  };
@@ -1892,6 +2085,11 @@ function printHistoryDetail(result) {
1892
2085
  console.log(` ${pc.dim("Kind:")} ${record.kind}`);
1893
2086
  console.log(` ${pc.dim("Path:")} ${record.path}`);
1894
2087
  console.log(` ${pc.dim("Summary:")} ${record.summary}${record.summaryTruncated ? "\u2026" : ""}`);
2088
+ if ((record.issues?.length ?? 0) > 0) {
2089
+ console.log(` ${pc.dim("Issues:")}`);
2090
+ for (const issue of record.issues ?? [])
2091
+ console.log(` ${issue.relation} ${issue.url}`);
2092
+ }
1895
2093
  console.log(` ${pc.dim("Scenarios:")} ${record.scenarioCount}`);
1896
2094
  console.log(` ${pc.dim("Tasks:")} ${record.checkboxes.tasks.done}/${record.checkboxes.tasks.total}`);
1897
2095
  console.log(` ${pc.dim("Verify:")} ${record.checkboxes.verify.done}/${record.checkboxes.verify.total}`);
@@ -1909,8 +2107,8 @@ function printHistoryDetail(result) {
1909
2107
  }
1910
2108
 
1911
2109
  // src/commands/init.ts
1912
- import { existsSync as existsSync6 } from "fs";
1913
- import { mkdir as mkdir8, readFile as readFile8 } from "fs/promises";
2110
+ import { existsSync as existsSync7 } from "fs";
2111
+ import { mkdir as mkdir8, readFile as readFile9 } from "fs/promises";
1914
2112
  import { join as join8 } from "path";
1915
2113
  function generateConfigTemplate() {
1916
2114
  const fmtList = (items, indent) => items.map((i) => `${" ".repeat(indent)}# - ${i}`).join("\n");
@@ -1925,6 +2123,13 @@ function generateConfigTemplate() {
1925
2123
  # kinds:
1926
2124
  ${fmtList(VALID_KINDS, 0)}
1927
2125
 
2126
+ # Set one shared project default for durable artifact and commit prose.
2127
+ # Optional artifacts or commit values override that durable surface independently.
2128
+ # Response language remains user/session-owned and is never read from this project mapping.
2129
+ # Omit this mapping to keep durable language selection under scoped project authority.
2130
+ # language:
2131
+ # default: en
2132
+
1928
2133
  # New projects default to automatic Manage routing with lifecycle-only closeout.
1929
2134
  # Use activation: explicit to require a named managed request.
1930
2135
  # closeout accepts manual, lifecycle, or local; local routes a qualified clean terminal non-small boundary to one local commit but never push or publication.
@@ -1945,7 +2150,7 @@ function toTitle(projectName) {
1945
2150
  return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
1946
2151
  }
1947
2152
  async function initProject(args = {}) {
1948
- const isNew = !existsSync6(RSP_DIR);
2153
+ const isNew = !existsSync7(RSP_DIR);
1949
2154
  const projectName = await detectProjectName();
1950
2155
  const mode = args.agentsMode ?? "managed";
1951
2156
  if (mode !== "managed" && mode !== "print") {
@@ -1960,7 +2165,7 @@ async function initProject(args = {}) {
1960
2165
  const agentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
1961
2166
  if (agentsInspection.issue)
1962
2167
  throw agentsInspection.issue;
1963
- if (existsSync6(CONFIG_PATH)) {
2168
+ if (existsSync7(CONFIG_PATH)) {
1964
2169
  clearConfigCache();
1965
2170
  const existingConfig = await inspectRspConfig();
1966
2171
  if (existingConfig.issues.length > 0)
@@ -1981,7 +2186,7 @@ async function initProject(args = {}) {
1981
2186
  for (const directory of dirs)
1982
2187
  await mkdir8(directory.path, { recursive: true });
1983
2188
  let created = false;
1984
- const bundledRules = await readFile8(join8(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
2189
+ const bundledRules = await readFile9(join8(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
1985
2190
  created = await ensureFile(RSP_RULES_PATH, bundledRules) || created;
1986
2191
  created = await ensureFile(join8(RSP_DIR, "config.yaml"), generateConfigTemplate()) || created;
1987
2192
  clearConfigCache();
@@ -2014,7 +2219,7 @@ ${upsertRspAgentsBlock("").content}
2014
2219
  created = true;
2015
2220
  agentsUpdated = true;
2016
2221
  } else {
2017
- const existing = await readFile8(agentsPath, "utf-8");
2222
+ const existing = await readFile9(agentsPath, "utf-8");
2018
2223
  const next = upsertRspAgentsBlock(existing);
2019
2224
  if (next.changed) {
2020
2225
  await writeManagedFile(agentsPath, next.content, "AGENTS.md");
@@ -2023,7 +2228,7 @@ ${upsertRspAgentsBlock("").content}
2023
2228
  }
2024
2229
  }
2025
2230
  if (mode === "print") {
2026
- const managed = await readFile8(agentsPath, "utf-8");
2231
+ const managed = await readFile9(agentsPath, "utf-8");
2027
2232
  console.log(`
2028
2233
  ${managed}
2029
2234
  `);
@@ -2062,7 +2267,7 @@ ${managed}
2062
2267
  }
2063
2268
 
2064
2269
  // src/commands/ready.ts
2065
- import { readFile as readFile9 } from "fs/promises";
2270
+ import { readFile as readFile10 } from "fs/promises";
2066
2271
  function exitReadyError(name, error, options) {
2067
2272
  if (options.json) {
2068
2273
  emitJson({
@@ -2097,7 +2302,7 @@ async function showReady(name, options = {}) {
2097
2302
  const runtime = [];
2098
2303
  let content;
2099
2304
  try {
2100
- content = await readFile9(srcPath, "utf-8");
2305
+ content = await readFile10(srcPath, "utf-8");
2101
2306
  } catch {
2102
2307
  console.error(` ${pc.red("Error:")} unable to read .rsp/changes/${name}.md`);
2103
2308
  process.exit(1);
@@ -2180,8 +2385,8 @@ function formatArchiveReady(value) {
2180
2385
  }
2181
2386
 
2182
2387
  // src/commands/reopen.ts
2183
- import { existsSync as existsSync7 } from "fs";
2184
- import { mkdir as mkdir9, readFile as readFile10, unlink as unlink8, writeFile as writeFile5 } from "fs/promises";
2388
+ import { existsSync as existsSync8 } from "fs";
2389
+ import { mkdir as mkdir9, readFile as readFile11, unlink as unlink9, writeFile as writeFile5 } from "fs/promises";
2185
2390
  import { dirname as dirname4 } from "path";
2186
2391
  async function reopenChange(name, options) {
2187
2392
  if (!name) {
@@ -2196,6 +2401,7 @@ async function reopenChange(name, options) {
2196
2401
  guardRspInitialized();
2197
2402
  try {
2198
2403
  await withRspLock("reopen-change", async () => {
2404
+ const normalizedName = normalizeExecutableWorkRef(name);
2199
2405
  const inspection = await inspectArchiveHistory();
2200
2406
  if (!historyInspectionComplete(inspection)) {
2201
2407
  throw new ArchiveHistoryError(
@@ -2205,17 +2411,17 @@ async function reopenChange(name, options) {
2205
2411
  }
2206
2412
  const record = selectArchiveHistoryRecord(
2207
2413
  inspection.records,
2208
- options.from ? { path: options.from } : { workRef: name }
2414
+ options.from ? { path: options.from } : { workRef: normalizedName }
2209
2415
  );
2210
- if (record.workRef !== name) {
2416
+ if (record.workRef !== normalizedName) {
2211
2417
  throw new ArchiveHistoryError(
2212
2418
  "archive_identity_mismatch",
2213
- `selected archive belongs to ${record.workRef}, not ${name}`
2419
+ `selected archive belongs to ${record.workRef}, not ${normalizedName}`
2214
2420
  );
2215
2421
  }
2216
2422
  let workRef;
2217
2423
  try {
2218
- workRef = resolveWorkRef(name, { executable: true });
2424
+ workRef = resolveWorkRef(normalizedName, { executable: true });
2219
2425
  } catch (error) {
2220
2426
  if (error instanceof WorkRefError && error.code === "group_brief_missing") {
2221
2427
  throw new ArchiveHistoryError(
@@ -2225,15 +2431,15 @@ async function reopenChange(name, options) {
2225
2431
  }
2226
2432
  throw error;
2227
2433
  }
2228
- if (existsSync7(workRef.path))
2434
+ if (existsSync8(workRef.path))
2229
2435
  throw new ArchiveHistoryError("open_change_exists", `open Change already exists: ${name}`);
2230
2436
  const focusEntry = resolveFocusMarkerPath(workRef);
2231
- if (existsSync7(focusEntry))
2437
+ if (existsSync8(focusEntry))
2232
2438
  throw new ArchiveHistoryError("focus_marker_exists", `focus marker already exists for non-open Change: ${name}; run rsp doctor`);
2233
- const archivedContent = await readFile10(record.sourcePath, "utf-8");
2439
+ const archivedContent = await readFile11(record.sourcePath, "utf-8");
2234
2440
  const reopenedContent = appendChecklistItem(
2235
- appendChecklistItem(archivedContent, "Tasks", `- [ ] Resolve reopened concern: ${reason}`),
2236
- "Verify",
2441
+ appendChecklistItem(archivedContent, "tasks", `- [ ] Resolve reopened concern: ${reason}`),
2442
+ "verify",
2237
2443
  `- [ ] Verify reopened concern: ${reason}`
2238
2444
  );
2239
2445
  await mkdir9(dirname4(workRef.path), { recursive: true });
@@ -2247,19 +2453,19 @@ async function reopenChange(name, options) {
2247
2453
  } catch (error) {
2248
2454
  if (changeCreated) {
2249
2455
  try {
2250
- await unlink8(workRef.path);
2456
+ await unlink9(workRef.path);
2251
2457
  await cleanupEmptyParentDirs(workRef.path, CHANGES_DIR);
2252
2458
  } catch {
2253
2459
  }
2254
2460
  }
2255
2461
  throw error;
2256
2462
  }
2257
- console.log(` ${pc.green("Reopened:")} ${name}`);
2463
+ console.log(` ${pc.green("Reopened:")} ${workRef.name}`);
2258
2464
  console.log(` ${pc.dim("retained archive")} \u2192 ${record.path}`);
2259
- console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
2465
+ console.log(` ${pc.dim("focused via focus.d")} \u2192 ${workRef.name}`);
2260
2466
  console.log(` ${pc.cyan("Next:")} refine the reopened Task and Verify evidence, then resolve the concern
2261
2467
  `);
2262
- if (existsSync7(".git")) {
2468
+ if (existsSync8(".git")) {
2263
2469
  console.log(` ${pc.cyan("Git delivery:")}`);
2264
2470
  console.log(` git status --short`);
2265
2471
  console.log(` Inspect the complete reopen transition; stage and commit only with separate Git authority.`);
@@ -2280,25 +2486,22 @@ async function reopenChange(name, options) {
2280
2486
  throw error;
2281
2487
  }
2282
2488
  }
2283
- function appendChecklistItem(content, heading, item) {
2284
- const headings = [...content.matchAll(new RegExp(`^## ${heading}[ \\t]*\\r?$`, "gm"))];
2285
- if (headings.length !== 1) {
2489
+ function appendChecklistItem(content, sectionId, item) {
2490
+ try {
2491
+ return appendDocumentSectionItem(parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), sectionId, item);
2492
+ } catch (error) {
2493
+ if (!(error instanceof DocumentSectionCardinalityError))
2494
+ throw error;
2495
+ const heading = CHANGE_DOCUMENT_SCHEMA.sections.find((section) => section.id === sectionId).heading;
2286
2496
  throw new ArchiveHistoryError(
2287
2497
  "archive_section_invalid",
2288
2498
  `archived Change must contain exactly one required section: ${heading}`
2289
2499
  );
2290
2500
  }
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
2501
  }
2299
2502
 
2300
2503
  // src/commands/show.ts
2301
- import { readFile as readFile11 } from "fs/promises";
2504
+ import { readFile as readFile12 } from "fs/promises";
2302
2505
  function exitShowError(error, options) {
2303
2506
  const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
2304
2507
  if (options.json) {
@@ -2356,15 +2559,19 @@ async function showChange(nameOrFocused, options = {}) {
2356
2559
  const srcPath = workRef.path;
2357
2560
  let content;
2358
2561
  try {
2359
- content = await readFile11(srcPath, "utf-8");
2562
+ content = await readFile12(srcPath, "utf-8");
2360
2563
  } catch {
2361
2564
  exitShowError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, options);
2362
2565
  }
2363
2566
  let kind = "\u2014";
2567
+ let issues = [];
2364
2568
  try {
2365
2569
  const fm = parseFrontmatter(content);
2366
2570
  kind = fm?.kind ? String(fm.kind) : "\u2014";
2571
+ issues = parseIssueRelationships(fm);
2367
2572
  } catch (error) {
2573
+ if (error instanceof IssueRelationshipError)
2574
+ exitShowError({ code: error.code, message: error.message }, options);
2368
2575
  kind = "(invalid)";
2369
2576
  reportRuntime({
2370
2577
  code: "frontmatter_parse_failed",
@@ -2411,6 +2618,7 @@ async function showChange(nameOrFocused, options = {}) {
2411
2618
  name,
2412
2619
  path: normalizeLogicalPath(srcPath),
2413
2620
  kind,
2621
+ issues,
2414
2622
  isFocused,
2415
2623
  progress: { done: cb.done, total: cb.total },
2416
2624
  blockers,
@@ -2429,6 +2637,11 @@ async function showChange(nameOrFocused, options = {}) {
2429
2637
  console.log(` ${pc.bold("Change:")} ${pc.cyan(name)}`);
2430
2638
  console.log(` ${pc.dim("Path:")} ${normalizeLogicalPath(srcPath)}`);
2431
2639
  console.log(` ${pc.dim("Kind:")} ${kind}`);
2640
+ if (issues.length > 0) {
2641
+ console.log(` ${pc.dim("Issues:")}`);
2642
+ for (const issue of issues)
2643
+ console.log(` ${issue.relation} ${issue.url}`);
2644
+ }
2432
2645
  console.log(` ${pc.dim("Focused:")} ${isFocused ? pc.green("yes") : pc.dim("no")}`);
2433
2646
  console.log(` ${pc.dim("Progress:")} ${cb.done}/${cb.total}`);
2434
2647
  console.log(` ${pc.dim("Blockers:")} ${blockers ? pc.yellow("yes") : pc.green("no")}`);
@@ -2488,6 +2701,7 @@ function deriveStatusView(snapshot, options = {}) {
2488
2701
  const nextActions = ok ? buildStatusNextActions(snapshot.focused.length, snapshot.records.map((record) => record.output), snapshot.groups, filtered) : ["Run: rsp doctor"];
2489
2702
  return {
2490
2703
  manage: snapshot.manage,
2704
+ language: snapshot.language,
2491
2705
  query,
2492
2706
  focused: snapshot.focused,
2493
2707
  records,
@@ -2584,6 +2798,8 @@ function printStatusPlain(view) {
2584
2798
  console.log(` ${pc.bold("RSP status")}`);
2585
2799
  console.log();
2586
2800
  console.log(` ${pc.bold("Manage:")} activation ${view.manage.activation} \xB7 closeout ${view.manage.closeout}`);
2801
+ if (view.language.artifacts !== null || view.language.commit !== null)
2802
+ console.log(` ${pc.bold("Language:")} artifacts ${view.language.artifacts ?? "unset"} \xB7 commit ${view.language.commit ?? "unset"}`);
2587
2803
  console.log();
2588
2804
  for (const diagnostic of view.diagnostics) {
2589
2805
  const label = diagnostic.change ?? diagnostic.path;
@@ -2703,6 +2919,7 @@ function toStatusJson(view) {
2703
2919
  command: "status",
2704
2920
  ok: view.ok,
2705
2921
  manage: view.manage,
2922
+ language: view.language,
2706
2923
  filters: view.query,
2707
2924
  focused: view.focused,
2708
2925
  records: view.records.map((record) => record.output),
@@ -2720,6 +2937,7 @@ function toStatusJsonError(error, options) {
2720
2937
  command: "status",
2721
2938
  ok: false,
2722
2939
  manage: { activation: "explicit", closeout: "manual" },
2940
+ language: { artifacts: null, commit: null },
2723
2941
  filters: {
2724
2942
  focused: options.focused,
2725
2943
  blocked: options.blocked,
@@ -2828,24 +3046,36 @@ var createCommand = defineCommand({
2828
3046
  description: "Create .rsp/changes/<name>.md [summary]"
2829
3047
  },
2830
3048
  args: {
2831
- name: {
3049
+ "name": {
2832
3050
  type: "positional",
2833
3051
  description: "Change name",
2834
3052
  required: true
2835
3053
  },
2836
- kind: {
3054
+ "kind": {
2837
3055
  type: "string",
2838
3056
  description: "Optional kind for a kind-aware template (feature, fix, refactor, docs, ops, research)"
2839
3057
  },
2840
- lite: {
3058
+ "lite": {
2841
3059
  type: "boolean",
2842
3060
  description: "Use a shorter change template while keeping the required RSP sections",
2843
3061
  default: false
3062
+ },
3063
+ "issue": {
3064
+ type: "string",
3065
+ description: "Attach one external issue URL without fetching it"
3066
+ },
3067
+ "issue-relation": {
3068
+ type: "string",
3069
+ description: "Issue relationship: relates (default) or closes; requires --issue"
2844
3070
  }
2845
3071
  },
2846
3072
  async run({ args }) {
2847
3073
  const summary = Array.isArray(args._) && args._.length > 1 ? args._.slice(1).join(" ") : "";
2848
- await createChange(args.name, summary, args.kind, { lite: Boolean(args.lite) });
3074
+ await createChange(args.name, summary, args.kind, {
3075
+ lite: Boolean(args.lite),
3076
+ issue: args.issue,
3077
+ issueRelation: args.issueRelation
3078
+ });
2849
3079
  }
2850
3080
  });
2851
3081
  var groupCreateCommand = defineCommand({
@@ -2868,7 +3098,7 @@ var groupCreateCommand = defineCommand({
2868
3098
  var groupCommand = defineCommand({
2869
3099
  meta: {
2870
3100
  name: "group",
2871
- description: "Create and close shallow Change Groups"
3101
+ description: "Create, close, and explicitly reopen shallow Change Groups"
2872
3102
  },
2873
3103
  subCommands: {
2874
3104
  create: groupCreateCommand,
@@ -2887,6 +3117,31 @@ var groupCommand = defineCommand({
2887
3117
  async run({ args }) {
2888
3118
  await closeChangeGroup(args.name);
2889
3119
  }
3120
+ }),
3121
+ reopen: defineCommand({
3122
+ meta: {
3123
+ name: "reopen",
3124
+ description: "Restore one archived Group Brief while retaining history"
3125
+ },
3126
+ args: {
3127
+ name: {
3128
+ type: "positional",
3129
+ description: "Archived Change Group name",
3130
+ required: true
3131
+ },
3132
+ from: {
3133
+ type: "string",
3134
+ description: "Exact .rsp/archives/... path when the Group has multiple archives"
3135
+ },
3136
+ reason: {
3137
+ type: "string",
3138
+ description: "One-line concern that makes Group completion unfinished",
3139
+ required: true
3140
+ }
3141
+ },
3142
+ async run({ args }) {
3143
+ await reopenChangeGroup(args.name, { from: args.from, reason: args.reason });
3144
+ }
2890
3145
  })
2891
3146
  }
2892
3147
  });
@@ -3352,7 +3607,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3352
3607
  if (shouldLaunchSkillsUi(rawArgs, terminalEnvironment)) {
3353
3608
  const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
3354
3609
  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");
3610
+ const { runSkillsTui } = await import("./entry-BKEJDNOQ.mjs");
3356
3611
  const selection = await runSkillsTui(locale);
3357
3612
  if (selection.kind === "confirmed") {
3358
3613
  const result = await installPackagedSkills({ names: selection.names, force: selection.force });
@@ -3370,7 +3625,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3370
3625
  } else {
3371
3626
  const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
3372
3627
  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");
3628
+ const { runTui } = await import("./entry-ETZGIFKP.mjs");
3374
3629
  const exitCode = await runTui(locale);
3375
3630
  if (exitCode !== 0)
3376
3631
  process.exitCode = exitCode;