@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/README.md +28 -16
- package/README.zh-CN.md +20 -13
- package/dist/{chunk-NX55NIC3.mjs → chunk-3X2V6IEW.mjs} +1 -1
- package/dist/{chunk-OTJFL6XH.mjs → chunk-UVYEXKYS.mjs} +358 -221
- package/dist/{chunk-7KKAUBKN.mjs → chunk-ZKZVWJQ7.mjs} +687 -421
- package/dist/cli.mjs +343 -88
- package/dist/{entry-WZDVKDUM.mjs → entry-BKEJDNOQ.mjs} +2 -2
- package/dist/{entry-X23GPCVS.mjs → entry-ETZGIFKP.mjs} +2 -2
- package/package.json +1 -1
- package/rules/rsp-rules.md +8 -5
- package/skills/rsp/SKILL.md +8 -18
- package/skills/rsp/references/groups-dependencies.md +1 -0
- package/skills/rsp/references/managed-routing.md +12 -23
- package/skills/rsp/references/release-operations.md +9 -0
- package/skills/rsp/references/reopen-recovery.md +9 -0
- package/skills/rsp-commit/SKILL.md +4 -2
- package/skills/rsp-implement/SKILL.md +5 -5
- package/skills/rsp-manage/SKILL.md +17 -7
- package/skills/rsp-release-docs/references/convention-discovery.md +2 -2
- package/skills/rsp-shape/SKILL.md +2 -0
- package/skills/rsp-shape/references/external-issue-input.md +9 -0
package/dist/cli.mjs
CHANGED
|
@@ -4,14 +4,18 @@ import {
|
|
|
4
4
|
installPackagedSkills,
|
|
5
5
|
printPackagedSkillInventory,
|
|
6
6
|
printSkillInstallResult
|
|
7
|
-
} from "./chunk-
|
|
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
|
-
|
|
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-
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
738
|
-
|
|
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 "${
|
|
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
|
|
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 {
|
|
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
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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(
|
|
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: ${
|
|
1688
|
+
throw new ChangeGroupError(`Change Group already exists: ${groupName}`);
|
|
1635
1689
|
throw error;
|
|
1636
1690
|
}
|
|
1637
|
-
console.log(` ${pc.green("Created Change Group:")} ${
|
|
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 ${
|
|
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
|
|
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: ${
|
|
1665
|
-
const reasons = collectCloseReasons(group, inspection.diagnostics.filter((diagnostic) => diagnostic.change ===
|
|
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 "${
|
|
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:")} ${
|
|
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 (
|
|
1913
|
+
if (validation.workRef) {
|
|
1731
1914
|
try {
|
|
1732
|
-
const selected = selectArchiveHistoryRecord(inspection.records, { 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:
|
|
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
|
-
|
|
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
|
-
|
|
1809
|
-
|
|
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
|
|
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
|
|
1913
|
-
import { mkdir as mkdir8, readFile as
|
|
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 = !
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2184
|
-
import { mkdir as mkdir9, readFile as
|
|
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:
|
|
2414
|
+
options.from ? { path: options.from } : { workRef: normalizedName }
|
|
2209
2415
|
);
|
|
2210
|
-
if (record.workRef !==
|
|
2416
|
+
if (record.workRef !== normalizedName) {
|
|
2211
2417
|
throw new ArchiveHistoryError(
|
|
2212
2418
|
"archive_identity_mismatch",
|
|
2213
|
-
`selected archive belongs to ${record.workRef}, not ${
|
|
2419
|
+
`selected archive belongs to ${record.workRef}, not ${normalizedName}`
|
|
2214
2420
|
);
|
|
2215
2421
|
}
|
|
2216
2422
|
let workRef;
|
|
2217
2423
|
try {
|
|
2218
|
-
workRef = resolveWorkRef(
|
|
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 (
|
|
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 (
|
|
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
|
|
2439
|
+
const archivedContent = await readFile11(record.sourcePath, "utf-8");
|
|
2234
2440
|
const reopenedContent = appendChecklistItem(
|
|
2235
|
-
appendChecklistItem(archivedContent, "
|
|
2236
|
-
"
|
|
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
|
|
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 (
|
|
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,
|
|
2284
|
-
|
|
2285
|
-
|
|
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
|
|
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
|
|
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, {
|
|
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
|
|
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-
|
|
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-
|
|
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;
|