@oevery/rsp 3.1.0-beta.4 → 3.1.1

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-D7XDXBFO.mjs";
7
+ } from "./chunk-3Z4IO2TD.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,
@@ -23,6 +27,7 @@ import {
23
27
  inspectWorkTree,
24
28
  normalizeExecutableWorkRef,
25
29
  normalizeWorkRefSegment,
30
+ parseIssueRelationships,
26
31
  projectDependencyForest,
27
32
  queryArchiveHistory,
28
33
  readArchiveHistoryDetail,
@@ -30,8 +35,9 @@ import {
30
35
  resolveExecutableChange,
31
36
  resolveFocusMarkerPath,
32
37
  resolveWorkRef,
33
- selectArchiveHistoryRecord
34
- } from "./chunk-FI3SYHVH.mjs";
38
+ selectArchiveHistoryRecord,
39
+ selectArchivedGroupBrief
40
+ } from "./chunk-MUNLLVVH.mjs";
35
41
  import {
36
42
  ARCHIVES_DIR,
37
43
  CHANGES_DIR,
@@ -40,7 +46,9 @@ import {
40
46
  DEFAULT_DECISION_RECORDS_PATH,
41
47
  DocumentSectionCardinalityError,
42
48
  FOCUS_DIR,
49
+ GROUP_BRIEF_DOCUMENT_SCHEMA,
43
50
  LOCK_PATH,
51
+ ManagedPathError,
44
52
  OBSOLETE_RSP_RULES_PATH,
45
53
  PKG_ROOT,
46
54
  RSP_DIR,
@@ -61,8 +69,10 @@ import {
61
69
  generateChangeContent,
62
70
  generateDesignContent,
63
71
  generateSpecContent,
72
+ getDocumentSectionBody,
64
73
  getDocumentSectionDefinitionByHeading,
65
74
  getDocumentSections,
75
+ getDocumentTitles,
66
76
  getDurableReviewCandidateTargets,
67
77
  getVersion,
68
78
  guardRspInitialized,
@@ -91,7 +101,7 @@ import {
91
101
  validateDecisionRecordsFilesystemPath,
92
102
  walkMarkdownFiles,
93
103
  writeManagedFile
94
- } from "./chunk-FJVH7ZYU.mjs";
104
+ } from "./chunk-WJXKLLIF.mjs";
95
105
 
96
106
  // src/cli.ts
97
107
  import { resolve } from "path";
@@ -734,6 +744,17 @@ async function runCheck(options = {}) {
734
744
  }
735
745
  }
736
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
+ }
737
758
  if (!("kind" in fm)) {
738
759
  addDiagnostic({
739
760
  severity: "error",
@@ -922,15 +943,21 @@ async function createChange(name, summary = "", kind, options = {}) {
922
943
  console.error(` ${pc.red("Usage:")} rsp create <name> [summary]`);
923
944
  process.exit(1);
924
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)] : [];
925
950
  guardRspInitialized();
926
951
  try {
927
952
  return await withRspLock("create-change", async () => {
928
953
  const workRef = await resolveExecutableChange(name);
929
954
  const changePath = workRef.path;
930
955
  const existed = existsSync2(changePath);
956
+ if (existed && hasIssueOption)
957
+ throw new Error(`Change already exists; --issue cannot update ${workRef.name}`);
931
958
  if (!existed) {
932
959
  const focusEntry = resolveFocusMarkerPath(workRef);
933
- const content = generateChangeContent(workRef.name, summary, kind, { lite: Boolean(options.lite) });
960
+ const content = generateChangeContent(workRef.name, summary, kind, { lite: Boolean(options.lite), issues });
934
961
  await mkdir4(dirname2(changePath), { recursive: true });
935
962
  let changeCreated = false;
936
963
  try {
@@ -1631,7 +1658,9 @@ function exitWorkRefError(error) {
1631
1658
  }
1632
1659
 
1633
1660
  // src/commands/group.ts
1634
- 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";
1635
1664
  import { join as join7 } from "path";
1636
1665
  var ChangeGroupError = class extends Error {
1637
1666
  name = "ChangeGroupError";
@@ -1719,6 +1748,136 @@ async function closeChangeGroup(name) {
1719
1748
  throw error;
1720
1749
  }
1721
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
+ }
1722
1881
  function collectCloseReasons(group, errors) {
1723
1882
  const reasons = [...errors, ...group.warnings];
1724
1883
  const open = group.slices.filter((slice) => slice.state === "open").map((slice) => slice.name);
@@ -1926,6 +2085,11 @@ function printHistoryDetail(result) {
1926
2085
  console.log(` ${pc.dim("Kind:")} ${record.kind}`);
1927
2086
  console.log(` ${pc.dim("Path:")} ${record.path}`);
1928
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
+ }
1929
2093
  console.log(` ${pc.dim("Scenarios:")} ${record.scenarioCount}`);
1930
2094
  console.log(` ${pc.dim("Tasks:")} ${record.checkboxes.tasks.done}/${record.checkboxes.tasks.total}`);
1931
2095
  console.log(` ${pc.dim("Verify:")} ${record.checkboxes.verify.done}/${record.checkboxes.verify.total}`);
@@ -1943,8 +2107,8 @@ function printHistoryDetail(result) {
1943
2107
  }
1944
2108
 
1945
2109
  // src/commands/init.ts
1946
- import { existsSync as existsSync6 } from "fs";
1947
- 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";
1948
2112
  import { join as join8 } from "path";
1949
2113
  function generateConfigTemplate() {
1950
2114
  const fmtList = (items, indent) => items.map((i) => `${" ".repeat(indent)}# - ${i}`).join("\n");
@@ -1986,7 +2150,7 @@ function toTitle(projectName) {
1986
2150
  return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
1987
2151
  }
1988
2152
  async function initProject(args = {}) {
1989
- const isNew = !existsSync6(RSP_DIR);
2153
+ const isNew = !existsSync7(RSP_DIR);
1990
2154
  const projectName = await detectProjectName();
1991
2155
  const mode = args.agentsMode ?? "managed";
1992
2156
  if (mode !== "managed" && mode !== "print") {
@@ -2001,7 +2165,7 @@ async function initProject(args = {}) {
2001
2165
  const agentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
2002
2166
  if (agentsInspection.issue)
2003
2167
  throw agentsInspection.issue;
2004
- if (existsSync6(CONFIG_PATH)) {
2168
+ if (existsSync7(CONFIG_PATH)) {
2005
2169
  clearConfigCache();
2006
2170
  const existingConfig = await inspectRspConfig();
2007
2171
  if (existingConfig.issues.length > 0)
@@ -2022,7 +2186,7 @@ async function initProject(args = {}) {
2022
2186
  for (const directory of dirs)
2023
2187
  await mkdir8(directory.path, { recursive: true });
2024
2188
  let created = false;
2025
- 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");
2026
2190
  created = await ensureFile(RSP_RULES_PATH, bundledRules) || created;
2027
2191
  created = await ensureFile(join8(RSP_DIR, "config.yaml"), generateConfigTemplate()) || created;
2028
2192
  clearConfigCache();
@@ -2055,7 +2219,7 @@ ${upsertRspAgentsBlock("").content}
2055
2219
  created = true;
2056
2220
  agentsUpdated = true;
2057
2221
  } else {
2058
- const existing = await readFile8(agentsPath, "utf-8");
2222
+ const existing = await readFile9(agentsPath, "utf-8");
2059
2223
  const next = upsertRspAgentsBlock(existing);
2060
2224
  if (next.changed) {
2061
2225
  await writeManagedFile(agentsPath, next.content, "AGENTS.md");
@@ -2064,7 +2228,7 @@ ${upsertRspAgentsBlock("").content}
2064
2228
  }
2065
2229
  }
2066
2230
  if (mode === "print") {
2067
- const managed = await readFile8(agentsPath, "utf-8");
2231
+ const managed = await readFile9(agentsPath, "utf-8");
2068
2232
  console.log(`
2069
2233
  ${managed}
2070
2234
  `);
@@ -2103,7 +2267,7 @@ ${managed}
2103
2267
  }
2104
2268
 
2105
2269
  // src/commands/ready.ts
2106
- import { readFile as readFile9 } from "fs/promises";
2270
+ import { readFile as readFile10 } from "fs/promises";
2107
2271
  function exitReadyError(name, error, options) {
2108
2272
  if (options.json) {
2109
2273
  emitJson({
@@ -2138,7 +2302,7 @@ async function showReady(name, options = {}) {
2138
2302
  const runtime = [];
2139
2303
  let content;
2140
2304
  try {
2141
- content = await readFile9(srcPath, "utf-8");
2305
+ content = await readFile10(srcPath, "utf-8");
2142
2306
  } catch {
2143
2307
  console.error(` ${pc.red("Error:")} unable to read .rsp/changes/${name}.md`);
2144
2308
  process.exit(1);
@@ -2221,8 +2385,8 @@ function formatArchiveReady(value) {
2221
2385
  }
2222
2386
 
2223
2387
  // src/commands/reopen.ts
2224
- import { existsSync as existsSync7 } from "fs";
2225
- 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";
2226
2390
  import { dirname as dirname4 } from "path";
2227
2391
  async function reopenChange(name, options) {
2228
2392
  if (!name) {
@@ -2267,12 +2431,12 @@ async function reopenChange(name, options) {
2267
2431
  }
2268
2432
  throw error;
2269
2433
  }
2270
- if (existsSync7(workRef.path))
2434
+ if (existsSync8(workRef.path))
2271
2435
  throw new ArchiveHistoryError("open_change_exists", `open Change already exists: ${name}`);
2272
2436
  const focusEntry = resolveFocusMarkerPath(workRef);
2273
- if (existsSync7(focusEntry))
2437
+ if (existsSync8(focusEntry))
2274
2438
  throw new ArchiveHistoryError("focus_marker_exists", `focus marker already exists for non-open Change: ${name}; run rsp doctor`);
2275
- const archivedContent = await readFile10(record.sourcePath, "utf-8");
2439
+ const archivedContent = await readFile11(record.sourcePath, "utf-8");
2276
2440
  const reopenedContent = appendChecklistItem(
2277
2441
  appendChecklistItem(archivedContent, "tasks", `- [ ] Resolve reopened concern: ${reason}`),
2278
2442
  "verify",
@@ -2289,7 +2453,7 @@ async function reopenChange(name, options) {
2289
2453
  } catch (error) {
2290
2454
  if (changeCreated) {
2291
2455
  try {
2292
- await unlink8(workRef.path);
2456
+ await unlink9(workRef.path);
2293
2457
  await cleanupEmptyParentDirs(workRef.path, CHANGES_DIR);
2294
2458
  } catch {
2295
2459
  }
@@ -2301,7 +2465,7 @@ async function reopenChange(name, options) {
2301
2465
  console.log(` ${pc.dim("focused via focus.d")} \u2192 ${workRef.name}`);
2302
2466
  console.log(` ${pc.cyan("Next:")} refine the reopened Task and Verify evidence, then resolve the concern
2303
2467
  `);
2304
- if (existsSync7(".git")) {
2468
+ if (existsSync8(".git")) {
2305
2469
  console.log(` ${pc.cyan("Git delivery:")}`);
2306
2470
  console.log(` git status --short`);
2307
2471
  console.log(` Inspect the complete reopen transition; stage and commit only with separate Git authority.`);
@@ -2337,7 +2501,7 @@ function appendChecklistItem(content, sectionId, item) {
2337
2501
  }
2338
2502
 
2339
2503
  // src/commands/show.ts
2340
- import { readFile as readFile11 } from "fs/promises";
2504
+ import { readFile as readFile12 } from "fs/promises";
2341
2505
  function exitShowError(error, options) {
2342
2506
  const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
2343
2507
  if (options.json) {
@@ -2395,15 +2559,19 @@ async function showChange(nameOrFocused, options = {}) {
2395
2559
  const srcPath = workRef.path;
2396
2560
  let content;
2397
2561
  try {
2398
- content = await readFile11(srcPath, "utf-8");
2562
+ content = await readFile12(srcPath, "utf-8");
2399
2563
  } catch {
2400
2564
  exitShowError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, options);
2401
2565
  }
2402
2566
  let kind = "\u2014";
2567
+ let issues = [];
2403
2568
  try {
2404
2569
  const fm = parseFrontmatter(content);
2405
2570
  kind = fm?.kind ? String(fm.kind) : "\u2014";
2571
+ issues = parseIssueRelationships(fm);
2406
2572
  } catch (error) {
2573
+ if (error instanceof IssueRelationshipError)
2574
+ exitShowError({ code: error.code, message: error.message }, options);
2407
2575
  kind = "(invalid)";
2408
2576
  reportRuntime({
2409
2577
  code: "frontmatter_parse_failed",
@@ -2450,6 +2618,7 @@ async function showChange(nameOrFocused, options = {}) {
2450
2618
  name,
2451
2619
  path: normalizeLogicalPath(srcPath),
2452
2620
  kind,
2621
+ issues,
2453
2622
  isFocused,
2454
2623
  progress: { done: cb.done, total: cb.total },
2455
2624
  blockers,
@@ -2468,6 +2637,11 @@ async function showChange(nameOrFocused, options = {}) {
2468
2637
  console.log(` ${pc.bold("Change:")} ${pc.cyan(name)}`);
2469
2638
  console.log(` ${pc.dim("Path:")} ${normalizeLogicalPath(srcPath)}`);
2470
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
+ }
2471
2645
  console.log(` ${pc.dim("Focused:")} ${isFocused ? pc.green("yes") : pc.dim("no")}`);
2472
2646
  console.log(` ${pc.dim("Progress:")} ${cb.done}/${cb.total}`);
2473
2647
  console.log(` ${pc.dim("Blockers:")} ${blockers ? pc.yellow("yes") : pc.green("no")}`);
@@ -2872,24 +3046,36 @@ var createCommand = defineCommand({
2872
3046
  description: "Create .rsp/changes/<name>.md [summary]"
2873
3047
  },
2874
3048
  args: {
2875
- name: {
3049
+ "name": {
2876
3050
  type: "positional",
2877
3051
  description: "Change name",
2878
3052
  required: true
2879
3053
  },
2880
- kind: {
3054
+ "kind": {
2881
3055
  type: "string",
2882
3056
  description: "Optional kind for a kind-aware template (feature, fix, refactor, docs, ops, research)"
2883
3057
  },
2884
- lite: {
3058
+ "lite": {
2885
3059
  type: "boolean",
2886
3060
  description: "Use a shorter change template while keeping the required RSP sections",
2887
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"
2888
3070
  }
2889
3071
  },
2890
3072
  async run({ args }) {
2891
3073
  const summary = Array.isArray(args._) && args._.length > 1 ? args._.slice(1).join(" ") : "";
2892
- 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
+ });
2893
3079
  }
2894
3080
  });
2895
3081
  var groupCreateCommand = defineCommand({
@@ -2912,7 +3098,7 @@ var groupCreateCommand = defineCommand({
2912
3098
  var groupCommand = defineCommand({
2913
3099
  meta: {
2914
3100
  name: "group",
2915
- description: "Create and close shallow Change Groups"
3101
+ description: "Create, close, and explicitly reopen shallow Change Groups"
2916
3102
  },
2917
3103
  subCommands: {
2918
3104
  create: groupCreateCommand,
@@ -2931,6 +3117,31 @@ var groupCommand = defineCommand({
2931
3117
  async run({ args }) {
2932
3118
  await closeChangeGroup(args.name);
2933
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
+ }
2934
3145
  })
2935
3146
  }
2936
3147
  });
@@ -3396,7 +3607,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3396
3607
  if (shouldLaunchSkillsUi(rawArgs, terminalEnvironment)) {
3397
3608
  const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
3398
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);
3399
- const { runSkillsTui } = await import("./entry-5OPKUZHF.mjs");
3610
+ const { runSkillsTui } = await import("./entry-KAWA2AS4.mjs");
3400
3611
  const selection = await runSkillsTui(locale);
3401
3612
  if (selection.kind === "confirmed") {
3402
3613
  const result = await installPackagedSkills({ names: selection.names, force: selection.force });
@@ -3414,7 +3625,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
3414
3625
  } else {
3415
3626
  const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
3416
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);
3417
- const { runTui } = await import("./entry-FVUG6YW2.mjs");
3628
+ const { runTui } = await import("./entry-IPHJBI53.mjs");
3418
3629
  const exitCode = await runTui(locale);
3419
3630
  if (exitCode !== 0)
3420
3631
  process.exitCode = exitCode;
@@ -7,14 +7,14 @@ import {
7
7
  projectDependencyForest,
8
8
  queryArchiveHistory,
9
9
  readArchiveHistoryDetail
10
- } from "./chunk-FI3SYHVH.mjs";
10
+ } from "./chunk-MUNLLVVH.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-FJVH7ZYU.mjs";
17
+ import "./chunk-WJXKLLIF.mjs";
18
18
 
19
19
  // src/tui/entry.tsx
20
20
  import process from "process";
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  inspectPackagedSkillInventory
4
- } from "./chunk-D7XDXBFO.mjs";
4
+ } from "./chunk-3Z4IO2TD.mjs";
5
5
  import {
6
6
  openTerminalSession,
7
7
  truncateDisplay
8
8
  } from "./chunk-JOMFFQRG.mjs";
9
- import "./chunk-FJVH7ZYU.mjs";
9
+ import "./chunk-WJXKLLIF.mjs";
10
10
 
11
11
  // src/skills-tui/entry.tsx
12
12
  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.4",
4
+ "version": "3.1.1",
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",
@@ -37,13 +37,18 @@
37
37
  },
38
38
  "scripts": {
39
39
  "prepare": "pnpm run build",
40
- "release:check": "pnpm run release:metadata-check && pnpm run build && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run release:package-check",
40
+ "release:check": "pnpm run release:metadata-check && pnpm run docs:check && pnpm run docs:build && pnpm run build && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run release:package-check",
41
41
  "release:candidate-check": "node scripts/release-candidate-check.mjs && pnpm run release:check",
42
42
  "release:metadata-check": "node scripts/release-metadata-check.mjs",
43
43
  "release:package-check": "node scripts/clean-install-check.mjs",
44
44
  "prepublishOnly": "pnpm run release:candidate-check",
45
45
  "build": "tsup",
46
46
  "dev": "tsup --watch",
47
+ "docs:build": "vitepress build docs",
48
+ "docs:check": "node scripts/docs-check.mjs",
49
+ "docs:deploy": "wrangler deploy",
50
+ "docs:dev": "vitepress dev docs",
51
+ "docs:preview": "vitepress preview docs",
47
52
  "lint": "eslint .",
48
53
  "lint:fix": "eslint --fix .",
49
54
  "typecheck": "tsc --noEmit",
@@ -65,6 +70,8 @@
65
70
  "ink-testing-library": "4.0.0",
66
71
  "tsup": "^8.5.1",
67
72
  "typescript": "^5.9.3",
68
- "vitest": "^4.1.10"
73
+ "vitepress": "^1.6.4",
74
+ "vitest": "^4.1.10",
75
+ "wrangler": "^4.114.0"
69
76
  }
70
77
  }
@@ -34,7 +34,7 @@ This file is the minimal fallback protocol for agents that cannot load the `rsp`
34
34
  - Parallel Changes in one Group follow the Brief `Slices` declaration order in plan output; unrelated work uses stable lexical ordering.
35
35
  - A Group Brief blocker is inherited by its direct child Changes as an external blocker; it does not create inferred dependency edges.
36
36
  - Persist only `open` and `archived`; readiness, blockers, verification, and next actions are derived.
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
+ - When fresh evidence shows that archived acceptance was incomplete, restore the same owner explicitly. `rsp reopen <work-ref> --reason <text>` retains the selected archive, focuses the executable Change, and adds unfinished Task and Verify evidence. If the enclosing Group is closed, first run `rsp group reopen <group> --reason <text>` to restore only its retained Brief as unfocused work with unfinished completion evidence, then separately reopen only the incomplete child. Multiple matching Change or Group archives require exact `--from .rsp/archives/...` selection. Neither command cascades into children or dependents, and `rsp group create` never reuses archived identity. Open work wins over same-identity history in current state; genuinely new or independently delivered scope uses a new owner.
38
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.
39
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.
40
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.
@@ -59,6 +59,8 @@ This file is the minimal fallback protocol for agents that cannot load the `rsp`
59
59
 
60
60
  ## Continuation and conflicts
61
61
 
62
+ - During managed execution, treat a progress or status inquiry as an evidence update, not a stop signal, and continue authorized work while authority and blockers permit it. An explicit pause stops and confirms active workers before acknowledgement, preserves the focused owner, and permits no further mutation until resume. Only an explicit release or unfocus request clears owner selection; environment and verification blockers preserve the focused owner.
63
+ - Resume treats continuation prose only as pointers: reread authority, status, focus, owned diff, blockers, and decisive evidence, invalidate stale claims, and requalify Manage before mutation or dispatch. Never persist paused, worker, controller, or execution-chronology state.
62
64
  - When execution stops with accepted work remaining, return one compact continuation with the semantic fields `WorkRef`, `Authority`, `Current state`, `Changed artifacts`, `Fresh verification`, `Blockers`, and `Next action`. Keep this field order, but localize the human-facing title and labels to the response language without changing referenced artifact prose or canonical technical values. Reopen its authority pointers, inspect drift, and refresh verification before resuming; it is not durable truth or a second state store.
63
65
  - Write a continuation file only when explicitly authorized. Never persist hidden handoff or controller state.
64
66
  - If an active merge, rebase, or cherry-pick conflict intersects authorized implementation, inspect the exact operation and conflicted paths, then compare base/ours/theirs semantics in that operation. Resolve only evidenced in-scope content while preserving unrelated work.
@@ -69,6 +71,6 @@ This file is the minimal fallback protocol for agents that cannot load the `rsp`
69
71
 
70
72
  - Do not create RSP-managed files directly when a command owns the operation.
71
73
  - Do not create archive entries directly; use `npx -y @oevery/rsp archive <name>`.
72
- - Do not restore archived Change content directly; use `npx -y @oevery/rsp reopen <name> --reason <text>` and exact `--from` selection when required.
74
+ - Do not restore archived Change or Group Brief content directly; use `npx -y @oevery/rsp reopen <name> --reason <text>` or `npx -y @oevery/rsp group reopen <group> --reason <text>` and exact `--from` selection when required.
73
75
  - Do not modify content outside the managed RSP block in `AGENTS.md` unless explicitly requested.
74
76
  - Archive reports warnings but does not grant commit, push, publication, deletion, or external approval authority.