@oevery/rsp 3.3.0 → 4.0.0
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 +24 -17
- package/README.zh-CN.md +25 -18
- package/dist/{chunk-TSUDEX5Y.mjs → chunk-HFSTLCCK.mjs} +1261 -319
- package/dist/cli.mjs +1129 -2350
- package/dist/{entry-GIDNR7CJ.mjs → entry-RIJQ2X46.mjs} +122 -36
- package/package.json +32 -24
- package/skills/rsp/SKILL.md +43 -38
- package/skills/rsp/references/closeout.md +38 -0
- package/skills/rsp/references/context-migration.md +22 -0
- package/skills/rsp/references/control-outcome.md +3 -29
- package/skills/rsp/references/coordination-recovery.md +25 -0
- package/skills/rsp/references/coordination-review.md +15 -0
- package/skills/rsp/references/coordination.md +59 -0
- package/skills/rsp/references/durable-review.md +60 -12
- package/skills/rsp/references/focus-continuation.md +8 -4
- package/skills/rsp/references/groups-dependencies.md +6 -4
- package/skills/rsp/references/response-language.md +2 -0
- package/skills/rsp/references/setup-repair.md +37 -7
- package/skills/rsp/references/work-model.md +27 -0
- package/skills/rsp-commit/SKILL.md +76 -31
- package/skills/rsp-commit/references/message.md +34 -0
- package/skills/rsp-doc/SKILL.md +65 -0
- package/skills/rsp-doc/agents/openai.yaml +3 -0
- package/skills/rsp-doc/references/artifact-lenses.md +43 -0
- package/skills/rsp-doc/references/release-communication.md +39 -0
- package/skills/rsp-doc/references/skill-authoring.md +36 -0
- package/skills/rsp-implement/SKILL.md +56 -25
- package/skills/rsp-implement/references/diagnosis.md +10 -0
- package/skills/rsp-implement/references/finding-resolution.md +21 -0
- package/skills/rsp-implement/references/test-first.md +7 -0
- package/skills/rsp-review/SKILL.md +46 -45
- package/skills/rsp-review/references/code-review.md +23 -8
- package/skills/rsp-review/references/document-review.md +25 -8
- package/skills/rsp-review/references/release-communication.md +21 -0
- package/skills/{rsp-structural-audit/references/structural-lenses.md → rsp-review/references/structural-review.md} +5 -5
- package/skills/rsp-shape/SKILL.md +24 -45
- package/skills/rsp-shape/references/complex-shaping.md +3 -1
- package/skills/rsp-shape/references/deep-clarification.md +4 -4
- package/skills/rsp-shape/references/domain-modeling.md +13 -0
- package/skills/rsp-shape/references/module-seams.md +13 -0
- package/skills/rsp-shape/references/planning.md +42 -0
- package/skills/{rsp-design → rsp-shape}/references/reversible-exploration.md +2 -2
- package/skills/rsp-verify/SKILL.md +44 -12
- package/dist/chunk-7HXQNBIG.mjs +0 -871
- package/dist/chunk-JOMFFQRG.mjs +0 -99
- package/dist/chunk-QM2UVPQK.mjs +0 -375
- package/dist/entry-YAW3PYFA.mjs +0 -174
- package/rules/rsp-rules.md +0 -48
- package/skills/rsp/references/implementation-evidence.md +0 -12
- package/skills/rsp/references/managed-routing.md +0 -45
- package/skills/rsp-design/SKILL.md +0 -53
- package/skills/rsp-design/references/domain-modeling.md +0 -11
- package/skills/rsp-design/references/module-seams.md +0 -11
- package/skills/rsp-diagnose/SKILL.md +0 -48
- package/skills/rsp-manage/SKILL.md +0 -101
- package/skills/rsp-manage/references/closeout.md +0 -13
- package/skills/rsp-manage/references/delegation.md +0 -47
- package/skills/rsp-manage/references/interruption-recovery.md +0 -19
- package/skills/rsp-manage/references/review-convergence.md +0 -9
- package/skills/rsp-release-docs/SKILL.md +0 -77
- package/skills/rsp-release-docs/references/convention-discovery.md +0 -65
- package/skills/rsp-release-docs/references/evidence-and-surfaces.md +0 -21
- package/skills/rsp-release-docs/references/output-contracts.md +0 -97
- package/skills/rsp-release-docs/references/publication-lifecycle.md +0 -28
- package/skills/rsp-resolve-findings/SKILL.md +0 -61
- package/skills/rsp-structural-audit/SKILL.md +0 -71
- package/skills/rsp-tdd/SKILL.md +0 -56
package/dist/cli.mjs
CHANGED
|
@@ -1,125 +1,123 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
installPackagedSkills,
|
|
5
|
-
presentPackagedSkillInventory,
|
|
6
|
-
presentSkillInstallResult
|
|
7
|
-
} from "./chunk-QM2UVPQK.mjs";
|
|
8
|
-
import {
|
|
3
|
+
ARCHIVES_DIR,
|
|
9
4
|
ArchiveHistoryError,
|
|
5
|
+
CHANGES_DIR,
|
|
6
|
+
CHANGE_DOCUMENT_SCHEMA,
|
|
7
|
+
CONFIG_PATH,
|
|
8
|
+
DEFAULT_DECISION_RECORDS_PATH,
|
|
9
|
+
DocumentSectionCardinalityError,
|
|
10
|
+
FOCUS_DIR,
|
|
11
|
+
GROUP_BRIEF_DOCUMENT_SCHEMA,
|
|
10
12
|
GROUP_BRIEF_FILENAME,
|
|
11
13
|
GROUP_REOPEN_COMPLETION_PREFIX,
|
|
12
14
|
HISTORY_DEFAULT_LIMIT,
|
|
13
15
|
HISTORY_MAX_LIMIT,
|
|
14
16
|
IssueRelationshipError,
|
|
17
|
+
LOCK_PATH,
|
|
18
|
+
MAX_FOCUS_CAPSULE_BYTES,
|
|
19
|
+
ManagedPathError,
|
|
20
|
+
OBSOLETE_RSP_RULES_PATH,
|
|
21
|
+
PKG_ROOT,
|
|
22
|
+
RSP_DIR,
|
|
23
|
+
RSP_RULES_PATH,
|
|
15
24
|
SPECS_DEFAULT_RESULT_LIMIT,
|
|
16
25
|
SPECS_DETAIL_CONTENT_CODE_POINTS,
|
|
17
|
-
SPECS_MAX_FILE_BYTES,
|
|
18
26
|
SPECS_MAX_RESULT_LIMIT,
|
|
19
27
|
SpecsQueryError,
|
|
20
28
|
WorkRefError,
|
|
29
|
+
appendDocumentSectionItem,
|
|
21
30
|
buildDurableReviewGuidance,
|
|
22
31
|
cleanupEmptyParentDirs,
|
|
32
|
+
clearConfigCache,
|
|
23
33
|
collectArchiveReadiness,
|
|
34
|
+
countCheckboxes,
|
|
24
35
|
createIssueRelationship,
|
|
36
|
+
detectDeltaSections,
|
|
25
37
|
detectProjectName,
|
|
38
|
+
emitJson,
|
|
39
|
+
ensureManagedFile,
|
|
40
|
+
generateConfigTemplate,
|
|
26
41
|
generateGroupBriefContent,
|
|
42
|
+
getDocumentSectionDefinitionByHeading,
|
|
43
|
+
getDocumentSectionMarkdown,
|
|
44
|
+
getDocumentSections,
|
|
45
|
+
getDocumentTitles,
|
|
27
46
|
getDurableReviewCandidateTargets,
|
|
28
47
|
getGroupReopenEvidence,
|
|
48
|
+
getVersion,
|
|
49
|
+
getVisibleBodyLines,
|
|
29
50
|
guardRspInitialized,
|
|
30
51
|
hasArchivedGroupBrief,
|
|
52
|
+
hasMeaningfulBlockers,
|
|
31
53
|
historyInspectionComplete,
|
|
32
54
|
inspectArchiveHistory,
|
|
33
55
|
inspectArchiveTree,
|
|
34
56
|
inspectChangeDependencies,
|
|
35
57
|
inspectChangeGroups,
|
|
36
58
|
inspectFocusTree,
|
|
59
|
+
inspectManagedDirectory,
|
|
60
|
+
inspectManagedFile,
|
|
61
|
+
inspectManagedFileTree,
|
|
37
62
|
inspectProjectStatus,
|
|
63
|
+
inspectRspConfig,
|
|
64
|
+
inspectRspInitialization,
|
|
38
65
|
inspectSpecs,
|
|
39
66
|
inspectUnsupportedRules,
|
|
40
67
|
inspectWorkTree,
|
|
41
68
|
isRecognizedGeneratedSpecsIndex,
|
|
42
69
|
isValidSpecName,
|
|
70
|
+
loadRspConfig,
|
|
71
|
+
markdownProse,
|
|
72
|
+
markdownText,
|
|
73
|
+
markdownTopLevelComments,
|
|
43
74
|
normalizeExecutableWorkRef,
|
|
44
75
|
normalizeLogicalPath,
|
|
45
76
|
normalizeWorkRefSegment,
|
|
77
|
+
parseFrontmatter,
|
|
46
78
|
parseIssueRelationships,
|
|
79
|
+
parseMarkdown,
|
|
80
|
+
parseRspDocument,
|
|
81
|
+
parseScenarios,
|
|
82
|
+
pc,
|
|
47
83
|
projectDependencyForest,
|
|
48
84
|
projectSpecsDetail,
|
|
49
85
|
projectSpecsSearch,
|
|
50
86
|
projectSpecsTree,
|
|
51
87
|
queryArchiveHistory,
|
|
52
88
|
readArchiveHistoryDetail,
|
|
53
|
-
resolveArchiveDirectory,
|
|
54
|
-
resolveExecutableChange,
|
|
55
|
-
resolveFocusMarkerPath,
|
|
56
|
-
resolveWorkRef,
|
|
57
|
-
selectArchiveHistoryRecord,
|
|
58
|
-
selectArchivedGroupBrief,
|
|
59
|
-
specsInspectionComplete,
|
|
60
|
-
toArchiveReadinessOutput,
|
|
61
|
-
walkMarkdownFiles
|
|
62
|
-
} from "./chunk-TSUDEX5Y.mjs";
|
|
63
|
-
import {
|
|
64
|
-
ARCHIVES_DIR,
|
|
65
|
-
CHANGES_DIR,
|
|
66
|
-
CHANGE_DOCUMENT_SCHEMA,
|
|
67
|
-
CONFIG_PATH,
|
|
68
|
-
DEFAULT_DECISION_RECORDS_PATH,
|
|
69
|
-
DocumentSectionCardinalityError,
|
|
70
|
-
FOCUS_DIR,
|
|
71
|
-
GROUP_BRIEF_DOCUMENT_SCHEMA,
|
|
72
|
-
LOCK_PATH,
|
|
73
|
-
MAX_FOCUS_CAPSULE_BYTES,
|
|
74
|
-
ManagedPathError,
|
|
75
|
-
OBSOLETE_RSP_RULES_PATH,
|
|
76
|
-
PKG_ROOT,
|
|
77
|
-
RSP_DIR,
|
|
78
|
-
RSP_RULES_PATH,
|
|
79
|
-
appendDocumentSectionItem,
|
|
80
|
-
clearConfigCache,
|
|
81
|
-
countCheckboxes,
|
|
82
|
-
detectDeltaSections,
|
|
83
|
-
emitJson,
|
|
84
|
-
ensureDecisionRecordsDirectory,
|
|
85
|
-
ensureManagedFile,
|
|
86
|
-
generateConfigTemplate,
|
|
87
|
-
getDocumentSectionBody,
|
|
88
|
-
getDocumentSectionDefinitionByHeading,
|
|
89
|
-
getDocumentSections,
|
|
90
|
-
getDocumentTitles,
|
|
91
|
-
getVersion,
|
|
92
|
-
hasMeaningfulBlockers,
|
|
93
|
-
inspectManagedDirectory,
|
|
94
|
-
inspectManagedFile,
|
|
95
|
-
inspectManagedFileTree,
|
|
96
|
-
inspectRspConfig,
|
|
97
|
-
loadRspConfig,
|
|
98
|
-
parseFrontmatter,
|
|
99
|
-
parseRspDocument,
|
|
100
|
-
parseScenarios,
|
|
101
|
-
pc,
|
|
102
|
-
reconcileRspConfigDefaults,
|
|
103
89
|
renderDocumentSectionHeading,
|
|
104
90
|
renderDocumentTitle,
|
|
105
91
|
requireManagedDirectory,
|
|
106
92
|
requireManagedFile,
|
|
93
|
+
resolveArchiveDirectory,
|
|
107
94
|
resolveDecisionRecordsPath,
|
|
95
|
+
resolveExecutableChange,
|
|
96
|
+
resolveFocusMarkerPath,
|
|
108
97
|
resolveKinds,
|
|
98
|
+
resolveLanguagePolicy,
|
|
109
99
|
resolveManagedDirectoryChain,
|
|
110
100
|
resolveRequiredSections,
|
|
101
|
+
resolveWorkRef,
|
|
102
|
+
resolveWorkflowPolicy,
|
|
103
|
+
selectArchiveHistoryRecord,
|
|
104
|
+
selectArchivedGroupBrief,
|
|
105
|
+
specsInspectionComplete,
|
|
106
|
+
toArchiveReadinessOutput,
|
|
111
107
|
toErrorMessage,
|
|
112
108
|
validateDecisionRecordsFilesystemPath,
|
|
109
|
+
walkMarkdownFiles,
|
|
113
110
|
writeManagedFile
|
|
114
|
-
} from "./chunk-
|
|
111
|
+
} from "./chunk-HFSTLCCK.mjs";
|
|
115
112
|
|
|
116
113
|
// src/cli.ts
|
|
117
|
-
import { resolve as
|
|
114
|
+
import { resolve as resolve2 } from "path";
|
|
118
115
|
import { fileURLToPath } from "url";
|
|
119
116
|
import { runMain } from "citty";
|
|
120
117
|
|
|
121
118
|
// src/cli/capabilities.ts
|
|
122
119
|
var CLI_COMMAND_CAPABILITIES = {
|
|
120
|
+
"config": { json: true, compact: true },
|
|
123
121
|
"status": { json: true, compact: true },
|
|
124
122
|
"show": { json: true, compact: true },
|
|
125
123
|
"ready": { json: true, compact: true },
|
|
@@ -164,9 +162,10 @@ import { readFile } from "fs/promises";
|
|
|
164
162
|
var choosePlaceholderRe = /^<choose:/i;
|
|
165
163
|
function inspectChangeDocument(content, options) {
|
|
166
164
|
const diagnostics = [];
|
|
165
|
+
const document = typeof content === "string" ? parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA) : content;
|
|
167
166
|
let frontmatter = null;
|
|
168
167
|
try {
|
|
169
|
-
frontmatter = parseFrontmatter(
|
|
168
|
+
frontmatter = parseFrontmatter(document.markdown);
|
|
170
169
|
} catch (error) {
|
|
171
170
|
const message = error instanceof Error ? error.message : String(error);
|
|
172
171
|
diagnostics.push({
|
|
@@ -182,7 +181,6 @@ function inspectChangeDocument(content, options) {
|
|
|
182
181
|
message: "missing YAML frontmatter"
|
|
183
182
|
});
|
|
184
183
|
}
|
|
185
|
-
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
186
184
|
for (const heading of options.requiredSections) {
|
|
187
185
|
const definition = getDocumentSectionDefinitionByHeading(CHANGE_DOCUMENT_SCHEMA, heading);
|
|
188
186
|
if (!definition)
|
|
@@ -481,9 +479,10 @@ async function runCheck(options = {}) {
|
|
|
481
479
|
});
|
|
482
480
|
continue;
|
|
483
481
|
}
|
|
484
|
-
|
|
482
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
483
|
+
for (const diagnostic of inspectChangeDocument(document, { name, validKinds, requiredSections }))
|
|
485
484
|
addDiagnostic({ ...diagnostic, change: name, path: fp });
|
|
486
|
-
const deltas = detectDeltaSections(
|
|
485
|
+
const deltas = detectDeltaSections(document);
|
|
487
486
|
if (deltas.added || deltas.modified || deltas.removed) {
|
|
488
487
|
const parts = [];
|
|
489
488
|
if (deltas.added)
|
|
@@ -500,7 +499,7 @@ async function runCheck(options = {}) {
|
|
|
500
499
|
message: `delta markers found: ${parts.join(", ")}`
|
|
501
500
|
});
|
|
502
501
|
}
|
|
503
|
-
const placeholderLines = findUnfinishedTemplateLines(
|
|
502
|
+
const placeholderLines = findUnfinishedTemplateLines(document.markdown);
|
|
504
503
|
if (placeholderLines.length > 0) {
|
|
505
504
|
addDiagnostic({
|
|
506
505
|
severity: "warning",
|
|
@@ -512,7 +511,7 @@ async function runCheck(options = {}) {
|
|
|
512
511
|
hint: "Replace angle-bracket template text with concrete change details before archiving."
|
|
513
512
|
});
|
|
514
513
|
}
|
|
515
|
-
const clarificationLines = findUnresolvedClarificationLines(
|
|
514
|
+
const clarificationLines = findUnresolvedClarificationLines(document.markdown);
|
|
516
515
|
if (clarificationLines.length > 0) {
|
|
517
516
|
addDiagnostic({
|
|
518
517
|
severity: "warning",
|
|
@@ -524,7 +523,7 @@ async function runCheck(options = {}) {
|
|
|
524
523
|
hint: "Resolve or remove clarification markers once the open question is answered."
|
|
525
524
|
});
|
|
526
525
|
}
|
|
527
|
-
const scenarios = parseScenarios(
|
|
526
|
+
const scenarios = parseScenarios(document.markdown);
|
|
528
527
|
if (scenarios.length > 0) {
|
|
529
528
|
const scenarioIssues = [];
|
|
530
529
|
for (const s of scenarios) {
|
|
@@ -569,32 +568,178 @@ async function runCheck(options = {}) {
|
|
|
569
568
|
}
|
|
570
569
|
function findUnfinishedTemplateLines(content) {
|
|
571
570
|
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;
|
|
572
|
-
return collectMatchingLines(content, (
|
|
573
|
-
const trimmed = stripInlineCodeSpans(line).trim();
|
|
574
|
-
return trimmed.startsWith("<!--") === false && placeholderRe.test(trimmed);
|
|
575
|
-
});
|
|
571
|
+
return collectMatchingLines(content, (value) => placeholderRe.test(value));
|
|
576
572
|
}
|
|
577
573
|
function findUnresolvedClarificationLines(content) {
|
|
578
|
-
return collectMatchingLines(content, (
|
|
579
|
-
}
|
|
580
|
-
function stripInlineCodeSpans(line) {
|
|
581
|
-
return line.replace(/`[^`]*`/g, "");
|
|
574
|
+
return collectMatchingLines(content, (value) => /\[(?:NEEDS|TODO|TBD)\s+CLARIFICATION(?::[^\]]*)?\]/i.test(value));
|
|
582
575
|
}
|
|
583
576
|
function collectMatchingLines(content, predicate) {
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
577
|
+
const document = parseMarkdown(content);
|
|
578
|
+
const sourceLines = document.source.split(/\r?\n/);
|
|
579
|
+
const matched = /* @__PURE__ */ new Set();
|
|
580
|
+
for (const fragment of markdownProse(document)) {
|
|
581
|
+
fragment.value.split(/\r?\n/).forEach((value, index) => {
|
|
582
|
+
if (predicate(value))
|
|
583
|
+
matched.add(fragment.line + index);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return [...matched].sort((a, b) => a - b).slice(0, 10).map((line) => `line ${line}: ${sourceLines[line - 1].trim()}`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/commands/config.ts
|
|
590
|
+
async function showConfig() {
|
|
591
|
+
try {
|
|
592
|
+
const inspection = await inspectRspConfig();
|
|
593
|
+
if (inspection.issues.length > 0) {
|
|
594
|
+
return {
|
|
595
|
+
command: "config",
|
|
596
|
+
ok: false,
|
|
597
|
+
path: CONFIG_PATH,
|
|
598
|
+
summary: null,
|
|
599
|
+
diagnostics: [{
|
|
600
|
+
severity: "error",
|
|
601
|
+
code: "invalid_config",
|
|
602
|
+
path: CONFIG_PATH,
|
|
603
|
+
message: inspection.issues.join("; ")
|
|
604
|
+
}],
|
|
605
|
+
runtime: []
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
command: "config",
|
|
610
|
+
ok: true,
|
|
611
|
+
path: CONFIG_PATH,
|
|
612
|
+
summary: {
|
|
613
|
+
kinds: resolveKinds(inspection.config),
|
|
614
|
+
decisions: { path: resolveDecisionRecordsPath(inspection.config) },
|
|
615
|
+
workflow: resolveWorkflowPolicy(inspection.config, { configValid: true }),
|
|
616
|
+
language: resolveLanguagePolicy(inspection.config, { configValid: true })
|
|
617
|
+
},
|
|
618
|
+
diagnostics: [],
|
|
619
|
+
runtime: []
|
|
620
|
+
};
|
|
621
|
+
} catch (error) {
|
|
622
|
+
return {
|
|
623
|
+
command: "config",
|
|
624
|
+
ok: false,
|
|
625
|
+
path: CONFIG_PATH,
|
|
626
|
+
summary: null,
|
|
627
|
+
diagnostics: [{
|
|
628
|
+
severity: "error",
|
|
629
|
+
code: "invalid_config",
|
|
630
|
+
path: CONFIG_PATH,
|
|
631
|
+
message: toErrorMessage(error)
|
|
632
|
+
}],
|
|
633
|
+
runtime: []
|
|
634
|
+
};
|
|
590
635
|
}
|
|
591
|
-
return matches.slice(0, 10);
|
|
592
636
|
}
|
|
593
637
|
|
|
594
638
|
// src/commands/doctor.ts
|
|
595
|
-
import { existsSync
|
|
596
|
-
import { readdir as readdir2, readFile as
|
|
597
|
-
import { basename, join as
|
|
639
|
+
import { existsSync } from "fs";
|
|
640
|
+
import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
|
|
641
|
+
import { basename, join as join3, relative as relative2 } from "path";
|
|
642
|
+
|
|
643
|
+
// src/core/project-version.ts
|
|
644
|
+
import { readFile as readFile2, realpath } from "fs/promises";
|
|
645
|
+
import { join } from "path";
|
|
646
|
+
var PROJECT_VERSION_PATH = ".rsp/version";
|
|
647
|
+
var PROJECT_SOURCE_PATH = ".rsp/source";
|
|
648
|
+
var SOURCE_INVOCATION = "mise exec -- node dist/cli.mjs";
|
|
649
|
+
function isExactVersion(value) {
|
|
650
|
+
return /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-z-][\da-z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-z-][\da-z-]*))*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?$/i.test(value);
|
|
651
|
+
}
|
|
652
|
+
function exactInvocation(version) {
|
|
653
|
+
if (!isExactVersion(version))
|
|
654
|
+
throw new Error(`Invalid exact RSP version: ${version}`);
|
|
655
|
+
return `npx -y @oevery/rsp@${version}`;
|
|
656
|
+
}
|
|
657
|
+
async function readProjectVersion(projectRoot = ".") {
|
|
658
|
+
const directory = inspectManagedDirectory(join(projectRoot, RSP_DIR), "RSP root", { allowMissing: true });
|
|
659
|
+
if (directory.issue)
|
|
660
|
+
throw directory.issue;
|
|
661
|
+
if (!directory.exists)
|
|
662
|
+
return null;
|
|
663
|
+
const path = join(projectRoot, PROJECT_VERSION_PATH);
|
|
664
|
+
const inspection = inspectManagedFile(path, "project CLI version", { allowMissing: true });
|
|
665
|
+
if (inspection.issue)
|
|
666
|
+
throw inspection.issue;
|
|
667
|
+
if (!inspection.exists)
|
|
668
|
+
return null;
|
|
669
|
+
const version = (await readFile2(path, "utf8")).trim();
|
|
670
|
+
if (!isExactVersion(version))
|
|
671
|
+
throw new Error(`${PROJECT_VERSION_PATH} must contain one exact package version, not a tag or range`);
|
|
672
|
+
return version;
|
|
673
|
+
}
|
|
674
|
+
async function readProjectRuntime(projectRoot = ".") {
|
|
675
|
+
const version = await readProjectVersion(projectRoot);
|
|
676
|
+
const source = inspectManagedFile(join(projectRoot, PROJECT_SOURCE_PATH), "project source selection", { allowMissing: true });
|
|
677
|
+
if (source.issue)
|
|
678
|
+
throw source.issue;
|
|
679
|
+
if (!source.exists)
|
|
680
|
+
return { mode: "package", version };
|
|
681
|
+
if (version !== null)
|
|
682
|
+
throw new Error(`${PROJECT_SOURCE_PATH} and ${PROJECT_VERSION_PATH} are mutually exclusive; explicitly select one runtime.`);
|
|
683
|
+
if ((await readFile2(join(projectRoot, PROJECT_SOURCE_PATH), "utf8")).trim() !== ".")
|
|
684
|
+
throw new Error(`${PROJECT_SOURCE_PATH} must contain . to select this RSP source checkout.`);
|
|
685
|
+
if (await realpath(projectRoot) !== await realpath(PKG_ROOT))
|
|
686
|
+
throw new Error(`Source mode requires this checkout's local CLI. Build here, then use ${SOURCE_INVOCATION} <command>; a registry package or another checkout cannot substitute.`);
|
|
687
|
+
const sourceDirectory = inspectManagedDirectory(join(projectRoot, "src"), "RSP source directory");
|
|
688
|
+
const sourceEntry = inspectManagedFile(join(projectRoot, "src/cli.ts"), "RSP source entry");
|
|
689
|
+
if (sourceDirectory.issue || sourceEntry.issue)
|
|
690
|
+
throw sourceDirectory.issue ?? sourceEntry.issue;
|
|
691
|
+
return { mode: "source" };
|
|
692
|
+
}
|
|
693
|
+
async function selectProjectRuntime(operation = "ordinary") {
|
|
694
|
+
const version = await getVersion();
|
|
695
|
+
exactInvocation(version);
|
|
696
|
+
const selected = await readProjectRuntime();
|
|
697
|
+
if (selected.mode === "source")
|
|
698
|
+
return { mode: "source", version };
|
|
699
|
+
if (selected.version && selected.version !== version && operation !== "update") {
|
|
700
|
+
throw new Error(`Project pins RSP ${selected.version}; running CLI is ${version}. Use ${exactInvocation(selected.version)} <command>. To migrate deliberately, inspect the target version and run ${exactInvocation(version)} update.`);
|
|
701
|
+
}
|
|
702
|
+
if (selected.version && operation === "update" && compareVersions(version, selected.version) < 0)
|
|
703
|
+
throw new Error(`Downgrading RSP from ${selected.version} to ${version} is unsupported. Use ${exactInvocation(selected.version)} <command>.`);
|
|
704
|
+
return { mode: "package", version };
|
|
705
|
+
}
|
|
706
|
+
function compareVersions(left, right) {
|
|
707
|
+
const parts = (value) => {
|
|
708
|
+
const [core, ...pre] = value.split("+", 1)[0].split("-");
|
|
709
|
+
return { core: core.split(".").map(BigInt), pre: pre.length ? pre.join("-").split(".") : [] };
|
|
710
|
+
};
|
|
711
|
+
const a = parts(left);
|
|
712
|
+
const b = parts(right);
|
|
713
|
+
for (let i = 0; i < 3; i++) {
|
|
714
|
+
if (a.core[i] !== b.core[i])
|
|
715
|
+
return a.core[i] > b.core[i] ? 1 : -1;
|
|
716
|
+
}
|
|
717
|
+
if (!a.pre.length || !b.pre.length)
|
|
718
|
+
return Number(!a.pre.length) - Number(!b.pre.length);
|
|
719
|
+
for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
|
|
720
|
+
const x = a.pre[i];
|
|
721
|
+
const y = b.pre[i];
|
|
722
|
+
if (x === y)
|
|
723
|
+
continue;
|
|
724
|
+
if (x === void 0 || y === void 0)
|
|
725
|
+
return x === void 0 ? -1 : 1;
|
|
726
|
+
const nx = /^\d+$/.test(x);
|
|
727
|
+
const ny = /^\d+$/.test(y);
|
|
728
|
+
if (nx && ny)
|
|
729
|
+
return BigInt(x) > BigInt(y) ? 1 : -1;
|
|
730
|
+
if (nx !== ny)
|
|
731
|
+
return nx ? -1 : 1;
|
|
732
|
+
return x > y ? 1 : -1;
|
|
733
|
+
}
|
|
734
|
+
return 0;
|
|
735
|
+
}
|
|
736
|
+
async function guardProjectVersion(rawArgs) {
|
|
737
|
+
if (rawArgs.some((arg) => ["--help", "-h"].includes(arg)) || rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0]))
|
|
738
|
+
return;
|
|
739
|
+
if (["doctor", "config", "update"].includes(rawArgs[0] ?? ""))
|
|
740
|
+
return;
|
|
741
|
+
await selectProjectRuntime();
|
|
742
|
+
}
|
|
598
743
|
|
|
599
744
|
// src/core/artifacts.ts
|
|
600
745
|
var RSP_AGENTS_BEGIN = "<!-- rsp:begin -->";
|
|
@@ -747,62 +892,89 @@ function getChangeTemplateByKind(kind) {
|
|
|
747
892
|
};
|
|
748
893
|
}
|
|
749
894
|
function changeSectionHeading(sectionId) {
|
|
750
|
-
|
|
895
|
+
const guidance = {
|
|
896
|
+
proposal: "State the intended outcome, why it matters, scope, and non-goals; do not narrate execution.",
|
|
897
|
+
spec: "Describe the contract delta and acceptance conditions, referencing affected existing Specs; do not copy the full baseline or implementation steps.",
|
|
898
|
+
design: "Explain the approach, responsibility boundaries, and necessary tradeoffs; keep simple changes brief and avoid repeating requirements or tasks.",
|
|
899
|
+
tasks: "List checkable work and mark completion only after outcomes exist; prescribe order only where correctness, safety, or migration requires it.",
|
|
900
|
+
verify: "Connect acceptance to verification methods; after execution retain actual results, gaps, and limits, not command transcripts.",
|
|
901
|
+
blockers: "Name unresolved decisions or dependencies that prevent progress; use none when there are no blockers."
|
|
902
|
+
};
|
|
903
|
+
return `${renderDocumentSectionHeading(CHANGE_DOCUMENT_SCHEMA, sectionId)}
|
|
904
|
+
<!-- ${guidance[sectionId]} -->`;
|
|
751
905
|
}
|
|
752
|
-
function renderRspAgentsBlock() {
|
|
906
|
+
function renderRspAgentsBlock(runtime) {
|
|
907
|
+
const { version } = runtime;
|
|
908
|
+
const selection = runtime.mode === "source" ? `This RSP development checkout explicitly selects source mode through \`.rsp/source\`. Build the current source with \`mise exec -- pnpm run build\`, then use \`${SOURCE_INVOCATION} <command>\` from this checkout for every CLI operation. Skill examples use \`rsp\` as shorthand for this local invocation. Do not substitute a registry package, global CLI or another checkout, even when its version matches. Package version metadata is not source identity; retain the commit, uncommitted diff and build inputs for candidate evidence. Authorized \`update\` reconciles this checkout's managed environment from its local source and keeps source mode; it does not select a published version.` : `This project pins RSP \`${version}\` in \`.rsp/version\`. For every CLI operation use \`${exactInvocation(version)} <command>\` (or a verified executable of that exact version). Skill examples use \`rsp\` as shorthand for this selected invocation. Never substitute \`@latest\`, a version range, or an unverified global CLI. Package fetching requires authority; if unavailable, report the gap. Only an authorized upgrade uses a deliberately selected target CLI with \`update\`; it updates the complete managed environment. Notices grant no upgrade authority.`;
|
|
753
909
|
return `${RSP_AGENTS_BEGIN}
|
|
754
910
|
## RSP Entry
|
|
755
911
|
|
|
756
912
|
RSP tracks current work, stable specs, and archives under \`.rsp/\`.
|
|
757
913
|
|
|
758
|
-
Read in order:
|
|
914
|
+
Read in order using supplied or confirmed paths; discover only missing paths or applicable ancestor instructions, without overlapping whole-tree scans:
|
|
759
915
|
1. Nearest \`AGENTS.md\` for project or module instructions.
|
|
760
|
-
2. Root \`CONTEXT
|
|
761
|
-
3. Use the project \`rsp\` Skill at \`.agents/skills/rsp/SKILL.md\`; hosts may load it through Skill discovery or read it directly.
|
|
762
|
-
4. \`.rsp/focus.d/\`; marker paths
|
|
916
|
+
2. Root \`CONTEXT.md\` if present, then the relevant nearest \`CONTEXT.md\` for domain language, relationships, and navigation. If a legacy \`CONTEXT-MAP.md\` remains, retain its relevant context and use Core's context-migration branch; discovery alone does not authorize migration.
|
|
917
|
+
3. Use the project \`rsp\` Skill at \`.agents/skills/rsp/SKILL.md\`; hosts may load it through Skill discovery or read it directly. Skills are the only workflow: if a required Skill is missing or unusable, stop dependent work and report the incomplete installation. Do not use retired Rules or install implicitly.
|
|
918
|
+
4. \`.rsp/focus.d/\`; marker paths form the open-work candidate set, while optional bounded Markdown content is recovery guidance only. For grouped work read the sibling Group Brief, then the explicitly selected child Change.
|
|
763
919
|
5. Only the relevant Specs and Decision Records under the configured authoritative path.
|
|
764
920
|
|
|
765
|
-
If \`.rsp/focus.d/\` is empty
|
|
921
|
+
If \`.rsp/focus.d/\` is empty or has several candidates, use an explicit WorkRef, the user's mentioned Change, or current status/dependency evidence to resolve the default action.
|
|
766
922
|
Do not treat \`.rsp/specs/\` or \`.rsp/changes/\` as replacements for nearest \`AGENTS.md\` or \`CONTEXT.md\`.
|
|
923
|
+
|
|
924
|
+
${selection}
|
|
767
925
|
${RSP_AGENTS_END}`;
|
|
768
926
|
}
|
|
769
|
-
function upsertRspAgentsBlock(content) {
|
|
770
|
-
const block = renderRspAgentsBlock();
|
|
771
|
-
const
|
|
772
|
-
if (
|
|
773
|
-
const next2 = content.
|
|
927
|
+
function upsertRspAgentsBlock(content, runtime) {
|
|
928
|
+
const block = renderRspAgentsBlock(runtime);
|
|
929
|
+
const span = rspAgentsBlockSpan(content);
|
|
930
|
+
if (span) {
|
|
931
|
+
const next2 = content.slice(0, span.start) + block + content.slice(span.end);
|
|
774
932
|
return { content: next2, changed: next2 !== content };
|
|
775
933
|
}
|
|
776
|
-
const
|
|
777
|
-
const next = trimmed ? `${block}
|
|
934
|
+
const next = content ? `${block}
|
|
778
935
|
|
|
779
|
-
${
|
|
936
|
+
${content}` : `${block}
|
|
780
937
|
`;
|
|
781
938
|
return { content: next, changed: true };
|
|
782
939
|
}
|
|
783
940
|
function hasRspAgentsBlock(content) {
|
|
784
|
-
return content
|
|
941
|
+
return rspAgentsBlockSpan(content) !== null;
|
|
942
|
+
}
|
|
943
|
+
function rspAgentsBlockSpan(content) {
|
|
944
|
+
let start;
|
|
945
|
+
for (const comment of markdownTopLevelComments(content)) {
|
|
946
|
+
if (comment.source === RSP_AGENTS_BEGIN && start === void 0)
|
|
947
|
+
start = comment.start;
|
|
948
|
+
if (comment.source === RSP_AGENTS_END && start !== void 0)
|
|
949
|
+
return { start, end: comment.end };
|
|
950
|
+
}
|
|
951
|
+
return null;
|
|
785
952
|
}
|
|
786
953
|
function generateDesignContent(projectName) {
|
|
787
954
|
const placeholder = "<\u2026>";
|
|
788
955
|
return `# Project Design: ${projectName}
|
|
789
956
|
|
|
790
957
|
## Purpose
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
## Stable Facts
|
|
958
|
+
<!-- Define the system's purpose and consumers, not the current change. -->
|
|
794
959
|
- ${placeholder}
|
|
795
960
|
|
|
796
961
|
## Boundaries
|
|
962
|
+
<!-- Define system responsibilities, exclusions, and responsibility handoffs. -->
|
|
797
963
|
- In scope:
|
|
798
964
|
- ${placeholder}
|
|
799
965
|
- Out of scope:
|
|
800
966
|
- ${placeholder}
|
|
801
967
|
|
|
968
|
+
## Contracts
|
|
969
|
+
<!-- State current architectural obligations and invariants that must survive implementation changes. -->
|
|
970
|
+
- ${placeholder}
|
|
971
|
+
|
|
802
972
|
## Structure
|
|
973
|
+
<!-- Describe stable ownership and dependency direction; leave repository navigation to CONTEXT.md. -->
|
|
803
974
|
- ${placeholder}
|
|
804
975
|
|
|
805
976
|
## Constraints
|
|
977
|
+
<!-- Record necessary technical, security, protocol, or compatibility limits; link lasting rationale rather than duplicating it. -->
|
|
806
978
|
- ${placeholder}
|
|
807
979
|
`;
|
|
808
980
|
}
|
|
@@ -812,1577 +984,297 @@ function generateSpecContent(name) {
|
|
|
812
984
|
return `# ${title}
|
|
813
985
|
|
|
814
986
|
## Purpose
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
## Stable Facts
|
|
987
|
+
<!-- Define the capability, its consumers, and its value; do not describe this change. -->
|
|
818
988
|
- ${placeholder}
|
|
819
989
|
|
|
820
990
|
## Boundaries
|
|
991
|
+
<!-- Define responsibilities, exclusions, and handoffs to other capabilities. -->
|
|
821
992
|
- In scope:
|
|
822
993
|
- ${placeholder}
|
|
823
994
|
- Out of scope:
|
|
824
995
|
- ${placeholder}
|
|
825
996
|
|
|
826
|
-
##
|
|
997
|
+
## Contracts
|
|
998
|
+
<!-- State current behavior, inputs and outputs, invariants, and failure semantics by capability, not internal implementation steps. -->
|
|
827
999
|
- ${placeholder}
|
|
828
|
-
`;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// src/commands/update.ts
|
|
832
|
-
import { existsSync } from "fs";
|
|
833
|
-
import { mkdir as mkdir3, readFile as readFile4, rm, rmdir as rmdir2 } from "fs/promises";
|
|
834
|
-
import { join as join3 } from "path";
|
|
835
|
-
|
|
836
|
-
// src/core/lock.ts
|
|
837
|
-
import { mkdir, readFile as readFile2, unlink, writeFile } from "fs/promises";
|
|
838
|
-
async function withRspLock(operation, fn) {
|
|
839
|
-
requireManagedDirectory(RSP_DIR, "RSP root", { allowMissing: true });
|
|
840
|
-
await mkdir(RSP_DIR, { recursive: true });
|
|
841
|
-
try {
|
|
842
|
-
await writeFile(LOCK_PATH, `${process.pid}
|
|
843
|
-
${operation}
|
|
844
|
-
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
845
|
-
} catch (err) {
|
|
846
|
-
if (err.code === "EEXIST")
|
|
847
|
-
await handleStaleLock(operation);
|
|
848
|
-
else
|
|
849
|
-
throw err;
|
|
850
|
-
}
|
|
851
|
-
try {
|
|
852
|
-
return await fn();
|
|
853
|
-
} finally {
|
|
854
|
-
try {
|
|
855
|
-
await unlink(LOCK_PATH);
|
|
856
|
-
} catch {
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
async function handleStaleLock(operation) {
|
|
861
|
-
try {
|
|
862
|
-
const content = await readFile2(LOCK_PATH, "utf-8");
|
|
863
|
-
const lines = content.trim().split("\n");
|
|
864
|
-
const pid = Number(lines[0]);
|
|
865
|
-
if (pid && Number.isFinite(pid)) {
|
|
866
|
-
try {
|
|
867
|
-
process.kill(pid, 0);
|
|
868
|
-
const op = lines[1] || "unknown";
|
|
869
|
-
const ts = lines[2] || "unknown";
|
|
870
|
-
throw new Error(`RSP locked by pid ${pid} (${op}) since ${ts}`);
|
|
871
|
-
} catch (e) {
|
|
872
|
-
if (e instanceof Error && e.message.startsWith("RSP locked"))
|
|
873
|
-
throw e;
|
|
874
|
-
const code = e.code;
|
|
875
|
-
if (code === "ESRCH") {
|
|
876
|
-
await unlink(LOCK_PATH);
|
|
877
|
-
await writeFile(LOCK_PATH, `${process.pid}
|
|
878
|
-
${operation}
|
|
879
|
-
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
throw e;
|
|
883
|
-
}
|
|
884
|
-
} else {
|
|
885
|
-
await unlink(LOCK_PATH);
|
|
886
|
-
await writeFile(LOCK_PATH, `${process.pid}
|
|
887
|
-
${operation}
|
|
888
|
-
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
889
|
-
}
|
|
890
|
-
} catch (e) {
|
|
891
|
-
if (e instanceof Error && e.message.startsWith("RSP locked"))
|
|
892
|
-
throw e;
|
|
893
|
-
throw new Error(`RSP is locked. Remove ${LOCK_PATH} manually if no other process is running.`);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
1000
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
async function removeLegacyArchiveIndex() {
|
|
901
|
-
const path = join(ARCHIVES_DIR, "INDEX.md");
|
|
902
|
-
const inspection = inspectManagedFile(path, "legacy archive index", { allowMissing: true });
|
|
903
|
-
if (!inspection.exists || inspection.issue)
|
|
904
|
-
return false;
|
|
905
|
-
let frontmatter;
|
|
906
|
-
try {
|
|
907
|
-
frontmatter = parseFrontmatter(await readFile3(path, "utf-8"));
|
|
908
|
-
} catch {
|
|
909
|
-
return false;
|
|
910
|
-
}
|
|
911
|
-
if (frontmatter?.kind !== "generated-index" || frontmatter.index_type !== "archives")
|
|
912
|
-
return false;
|
|
913
|
-
await unlink2(path);
|
|
914
|
-
return true;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// src/commands/specs-index-migration.ts
|
|
918
|
-
import { Buffer as Buffer2 } from "buffer";
|
|
919
|
-
import { createHash, randomUUID } from "crypto";
|
|
920
|
-
import { constants } from "fs";
|
|
921
|
-
import { link, lstat as lstat2, open, readdir, realpath as realpath2, rename, unlink as unlink3 } from "fs/promises";
|
|
922
|
-
import { dirname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
923
|
-
|
|
924
|
-
// src/core/path-identity.ts
|
|
925
|
-
import { lstat, realpath } from "fs/promises";
|
|
926
|
-
import { isAbsolute, relative, resolve, sep } from "path";
|
|
927
|
-
var StablePathIdentityError = class extends Error {
|
|
928
|
-
constructor(code, message) {
|
|
929
|
-
super(message);
|
|
930
|
-
this.code = code;
|
|
931
|
-
this.name = "StablePathIdentityError";
|
|
932
|
-
}
|
|
933
|
-
code;
|
|
934
|
-
};
|
|
935
|
-
async function captureStableDirectoryChain(options) {
|
|
936
|
-
const rootPath = resolve(options.rootPath);
|
|
937
|
-
const targetPath = resolve(options.targetPath);
|
|
938
|
-
const requiredPath = options.requiredPath === void 0 ? null : resolve(options.requiredPath);
|
|
939
|
-
const targetRelative = containedRelative(rootPath, targetPath);
|
|
940
|
-
if (targetRelative === null) {
|
|
941
|
-
throw new StablePathIdentityError(
|
|
942
|
-
"stable_path_outside_root",
|
|
943
|
-
`${options.label} target must stay inside ${rootPath}: ${targetPath}`
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
if (requiredPath !== null) {
|
|
947
|
-
if (containedRelative(rootPath, requiredPath) === null || containedRelative(requiredPath, targetPath) === null) {
|
|
948
|
-
throw new StablePathIdentityError(
|
|
949
|
-
"stable_path_outside_required_root",
|
|
950
|
-
`${options.label} target must stay inside ${requiredPath}: ${targetPath}`
|
|
951
|
-
);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
const logicalPaths = [rootPath];
|
|
955
|
-
if (targetRelative !== "") {
|
|
956
|
-
let current = rootPath;
|
|
957
|
-
for (const segment of targetRelative.split(sep)) {
|
|
958
|
-
current = resolve(current, segment);
|
|
959
|
-
logicalPaths.push(current);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
const entries = [];
|
|
963
|
-
let rootRealPath = null;
|
|
964
|
-
let requiredRealPath = null;
|
|
965
|
-
for (const path of logicalPaths) {
|
|
966
|
-
let value;
|
|
967
|
-
let resolvedPath;
|
|
968
|
-
try {
|
|
969
|
-
value = await lstat(path, { bigint: true });
|
|
970
|
-
resolvedPath = await realpath(path);
|
|
971
|
-
} catch (error) {
|
|
972
|
-
throw new StablePathIdentityError(
|
|
973
|
-
"stable_path_inspection_failed",
|
|
974
|
-
`${options.label} parent chain could not be inspected at ${path}: ${errorMessage(error)}`
|
|
975
|
-
);
|
|
976
|
-
}
|
|
977
|
-
if (!value.isDirectory() || value.isSymbolicLink()) {
|
|
978
|
-
throw new StablePathIdentityError(
|
|
979
|
-
"stable_path_not_real_directory",
|
|
980
|
-
`${options.label} parent chain must contain only real directories: ${path}`
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
rootRealPath ??= resolvedPath;
|
|
984
|
-
if (containedRelative(rootRealPath, resolvedPath) === null) {
|
|
985
|
-
throw new StablePathIdentityError(
|
|
986
|
-
"stable_path_real_containment_failed",
|
|
987
|
-
`${options.label} parent chain resolves outside ${rootRealPath}: ${path}`
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
if (requiredPath === path)
|
|
991
|
-
requiredRealPath = resolvedPath;
|
|
992
|
-
entries.push({
|
|
993
|
-
path,
|
|
994
|
-
realPath: resolvedPath,
|
|
995
|
-
device: value.dev,
|
|
996
|
-
inode: value.ino
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
if (requiredPath !== null) {
|
|
1000
|
-
const target = entries.at(-1);
|
|
1001
|
-
if (requiredRealPath === null || containedRelative(requiredRealPath, target.realPath) === null) {
|
|
1002
|
-
throw new StablePathIdentityError(
|
|
1003
|
-
"stable_path_required_containment_failed",
|
|
1004
|
-
`${options.label} parent chain no longer resolves inside ${requiredPath}`
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
return {
|
|
1009
|
-
rootPath,
|
|
1010
|
-
targetPath,
|
|
1011
|
-
requiredPath,
|
|
1012
|
-
entries
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
async function assertStableDirectoryChain(expected, label) {
|
|
1016
|
-
const current = await captureStableDirectoryChain({
|
|
1017
|
-
rootPath: expected.rootPath,
|
|
1018
|
-
targetPath: expected.targetPath,
|
|
1019
|
-
requiredPath: expected.requiredPath ?? void 0,
|
|
1020
|
-
label
|
|
1021
|
-
});
|
|
1022
|
-
if (current.entries.length !== expected.entries.length)
|
|
1023
|
-
throw changed(label, expected.targetPath);
|
|
1024
|
-
for (let index = 0; index < expected.entries.length; index += 1) {
|
|
1025
|
-
const before = expected.entries[index];
|
|
1026
|
-
const after = current.entries[index];
|
|
1027
|
-
if (before.path !== after.path || before.realPath !== after.realPath || before.device !== after.device || before.inode !== after.inode) {
|
|
1028
|
-
throw changed(label, before.path);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
function containedRelative(rootPath, candidatePath) {
|
|
1033
|
-
const candidate = relative(rootPath, candidatePath);
|
|
1034
|
-
if (candidate === "" || candidate !== ".." && !candidate.startsWith(`..${sep}`) && !isAbsolute(candidate)) {
|
|
1035
|
-
return candidate;
|
|
1036
|
-
}
|
|
1037
|
-
return null;
|
|
1038
|
-
}
|
|
1039
|
-
function changed(label, path) {
|
|
1040
|
-
return new StablePathIdentityError(
|
|
1041
|
-
"stable_path_identity_changed",
|
|
1042
|
-
`${label} parent identity changed before the filesystem operation: ${path}`
|
|
1043
|
-
);
|
|
1044
|
-
}
|
|
1045
|
-
function errorMessage(error) {
|
|
1046
|
-
return error instanceof Error ? error.message : String(error);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
// src/commands/specs-index-migration.ts
|
|
1050
|
-
var READ_NO_FOLLOW_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
|
|
1051
|
-
var WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
1052
|
-
var DEFAULT_FILE_ADAPTER = { link };
|
|
1053
|
-
var GeneratedSpecsIndexMigrationError = class extends Error {
|
|
1054
|
-
constructor(code, message, recoveryPaths = [], retainedMutations = []) {
|
|
1055
|
-
super(message);
|
|
1056
|
-
this.code = code;
|
|
1057
|
-
this.recoveryPaths = recoveryPaths;
|
|
1058
|
-
this.retainedMutations = retainedMutations;
|
|
1059
|
-
this.name = "GeneratedSpecsIndexMigrationError";
|
|
1060
|
-
}
|
|
1061
|
-
code;
|
|
1062
|
-
recoveryPaths;
|
|
1063
|
-
retainedMutations;
|
|
1064
|
-
};
|
|
1065
|
-
async function inspectGeneratedSpecsIndexMigration(cwd = process.cwd()) {
|
|
1066
|
-
const inspection = await inspectSpecs({
|
|
1067
|
-
cwd,
|
|
1068
|
-
allowMissingDecisionRecords: true
|
|
1069
|
-
});
|
|
1070
|
-
const removable = inspection.generatedIndexes.filter((index) => index.classification === "safe-removal").map((index) => index.path).sort();
|
|
1071
|
-
const ownerControlled = inspection.generatedIndexes.filter((index) => index.classification === "owner-controlled").map((index) => index.path).sort();
|
|
1072
|
-
return {
|
|
1073
|
-
inspectionComplete: specsInspectionComplete(inspection),
|
|
1074
|
-
removable,
|
|
1075
|
-
ownerControlled,
|
|
1076
|
-
diagnostics: inspection.diagnostics.map((diagnostic) => `${diagnostic.path ?? ".rsp/specs"}: ${diagnostic.message}`)
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
async function removeRecognizedGeneratedSpecsIndexes(options = {}) {
|
|
1080
|
-
const cwd = await realpath2(resolve2(options.cwd ?? process.cwd()));
|
|
1081
|
-
const fileAdapter = options.fileAdapter ?? DEFAULT_FILE_ADAPTER;
|
|
1082
|
-
const plan = await inspectGeneratedSpecsIndexMigration(cwd);
|
|
1083
|
-
if (!plan.inspectionComplete || plan.ownerControlled.length > 0) {
|
|
1084
|
-
const ownerMessage = plan.ownerControlled.length > 0 ? ` Owner-controlled reserved path(s): ${plan.ownerControlled.join(", ")}.` : "";
|
|
1085
|
-
const diagnosticMessage = plan.diagnostics.length > 0 ? ` ${plan.diagnostics.join("; ")}` : "";
|
|
1086
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1087
|
-
"generated_specs_index_owner_review_required",
|
|
1088
|
-
`Generated Specs-index migration stopped before mutation.${ownerMessage}${diagnosticMessage}`
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
if (plan.removable.length === 0)
|
|
1092
|
-
return [];
|
|
1093
|
-
const snapshots = [];
|
|
1094
|
-
for (const projectPath of plan.removable)
|
|
1095
|
-
snapshots.push(await inspectRecognizedGeneratedIndex(cwd, projectPath));
|
|
1096
|
-
const quarantined = [];
|
|
1097
|
-
let removalCommitted = false;
|
|
1098
|
-
try {
|
|
1099
|
-
for (const snapshot2 of snapshots) {
|
|
1100
|
-
const quarantinePath = `${snapshot2.absolutePath}.${process.pid}.${randomUUID()}.rsp-migration`;
|
|
1101
|
-
await assertMigrationParent(snapshot2, "quarantine");
|
|
1102
|
-
await rename(snapshot2.absolutePath, quarantinePath);
|
|
1103
|
-
await assertMigrationParent(snapshot2, "quarantine");
|
|
1104
|
-
const quarantinedItem = {
|
|
1105
|
-
...snapshot2,
|
|
1106
|
-
quarantinePath,
|
|
1107
|
-
recoveryPath: null
|
|
1108
|
-
};
|
|
1109
|
-
quarantined.push(quarantinedItem);
|
|
1110
|
-
const quarantinedSnapshot = await inspectRecognizedGeneratedIndexAt(
|
|
1111
|
-
cwd,
|
|
1112
|
-
snapshot2.projectPath,
|
|
1113
|
-
quarantinePath,
|
|
1114
|
-
snapshot2.parentChain
|
|
1115
|
-
);
|
|
1116
|
-
if (!sameRenamedSnapshot(snapshot2.identity, quarantinedSnapshot.identity) || snapshot2.contentHash !== quarantinedSnapshot.contentHash) {
|
|
1117
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1118
|
-
"generated_specs_index_changed",
|
|
1119
|
-
`Generated Specs index changed before migration: ${snapshot2.projectPath}`
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
|
-
quarantinedItem.identity = quarantinedSnapshot.identity;
|
|
1123
|
-
await options.afterQuarantine?.(snapshot2.projectPath);
|
|
1124
|
-
await assertMigrationParent(quarantinedItem, "post-quarantine hook");
|
|
1125
|
-
}
|
|
1126
|
-
for (const item of quarantined)
|
|
1127
|
-
await assertMigrationParent(item, "direct Specs postcheck");
|
|
1128
|
-
const after = await inspectSpecs({
|
|
1129
|
-
cwd,
|
|
1130
|
-
allowMissingDecisionRecords: true
|
|
1131
|
-
});
|
|
1132
|
-
if (!specsInspectionComplete(after) || after.generatedIndexes.length > 0) {
|
|
1133
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1134
|
-
"generated_specs_index_postcheck_failed",
|
|
1135
|
-
"Specs tree changed while generated indexes were being migrated; no quarantined index was deleted."
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
for (const item of quarantined)
|
|
1139
|
-
await assertMigrationParent(item, "direct Specs postcheck");
|
|
1140
|
-
for (const item of quarantined) {
|
|
1141
|
-
item.recoveryPath = `${item.quarantinePath}.recovery`;
|
|
1142
|
-
await materializeVerifiedSnapshot(
|
|
1143
|
-
item,
|
|
1144
|
-
item.quarantinePath,
|
|
1145
|
-
item.recoveryPath,
|
|
1146
|
-
fileAdapter,
|
|
1147
|
-
options.testing
|
|
1148
|
-
);
|
|
1149
|
-
}
|
|
1150
|
-
for (const item of quarantined) {
|
|
1151
|
-
await assertMigrationParent(item, "quarantine removal");
|
|
1152
|
-
await unlink3(item.quarantinePath);
|
|
1153
|
-
await assertMigrationParent(item, "quarantine removal");
|
|
1154
|
-
}
|
|
1155
|
-
removalCommitted = true;
|
|
1156
|
-
const cleanupPaths = [];
|
|
1157
|
-
for (const item of quarantined) {
|
|
1158
|
-
try {
|
|
1159
|
-
await assertMigrationParent(item, "recovery cleanup");
|
|
1160
|
-
await unlink3(item.recoveryPath);
|
|
1161
|
-
await assertMigrationParent(item, "recovery cleanup");
|
|
1162
|
-
item.recoveryPath = null;
|
|
1163
|
-
} catch {
|
|
1164
|
-
cleanupPaths.push(item.recoveryPath);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
if (cleanupPaths.length > 0) {
|
|
1168
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1169
|
-
"generated_specs_index_cleanup_required",
|
|
1170
|
-
`Generated Specs indexes were removed, but recovery-link cleanup requires owner action: ${cleanupPaths.join(", ")}`,
|
|
1171
|
-
cleanupPaths
|
|
1172
|
-
);
|
|
1173
|
-
}
|
|
1174
|
-
return plan.removable;
|
|
1175
|
-
} catch (error) {
|
|
1176
|
-
if (removalCommitted)
|
|
1177
|
-
throw error;
|
|
1178
|
-
const recovery = await restoreQuarantinedIndexes(
|
|
1179
|
-
quarantined,
|
|
1180
|
-
fileAdapter,
|
|
1181
|
-
options.testing
|
|
1182
|
-
);
|
|
1183
|
-
if (recovery.recoveryPaths.length > 0 || recovery.retainedMutations.length > 0) {
|
|
1184
|
-
const retainedMessage = recovery.retainedMutations.length > 0 ? ` Retained mutation evidence: ${recovery.retainedMutations.map((item) => `${item.projectPath} (${item.quarantineName}, sha256 ${item.contentHash})`).join(", ")}.` : "";
|
|
1185
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1186
|
-
"generated_specs_index_recovery_required",
|
|
1187
|
-
`Generated Specs-index migration stopped and preserved recovery evidence.${recovery.recoveryPaths.length > 0 ? ` Accessible recovery copies: ${recovery.recoveryPaths.join(", ")}.` : ""}${retainedMessage}`,
|
|
1188
|
-
recovery.recoveryPaths,
|
|
1189
|
-
recovery.retainedMutations
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
throw error;
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
async function inspectRecognizedGeneratedIndex(projectRoot, projectPath) {
|
|
1196
|
-
const absolutePath = resolveProjectPath(projectRoot, projectPath);
|
|
1197
|
-
const parentChain = await captureStableDirectoryChain({
|
|
1198
|
-
rootPath: projectRoot,
|
|
1199
|
-
requiredPath: resolve2(projectRoot, ".rsp", "specs"),
|
|
1200
|
-
targetPath: dirname(absolutePath),
|
|
1201
|
-
label: `generated Specs index ${projectPath}`
|
|
1202
|
-
});
|
|
1203
|
-
return inspectRecognizedGeneratedIndexAt(
|
|
1204
|
-
projectRoot,
|
|
1205
|
-
projectPath,
|
|
1206
|
-
absolutePath,
|
|
1207
|
-
parentChain
|
|
1208
|
-
);
|
|
1209
|
-
}
|
|
1210
|
-
async function inspectRecognizedGeneratedIndexAt(projectRoot, projectPath, absolutePath, parentChain) {
|
|
1211
|
-
await assertStableDirectoryChain(parentChain, `generated Specs index ${projectPath}`);
|
|
1212
|
-
const before = await lstat2(absolutePath, { bigint: true });
|
|
1213
|
-
if (!before.isFile() || before.isSymbolicLink())
|
|
1214
|
-
throw changed2(projectPath);
|
|
1215
|
-
const handle = await open(absolutePath, READ_NO_FOLLOW_FLAGS);
|
|
1216
|
-
try {
|
|
1217
|
-
const opened = await handle.stat({ bigint: true });
|
|
1218
|
-
if (!opened.isFile() || !sameSnapshot(before, opened))
|
|
1219
|
-
throw changed2(projectPath);
|
|
1220
|
-
const content = await readBounded(handle, projectPath);
|
|
1221
|
-
const after = await handle.stat({ bigint: true });
|
|
1222
|
-
if (!sameSnapshot(opened, after))
|
|
1223
|
-
throw changed2(projectPath);
|
|
1224
|
-
const finalPath = await lstat2(absolutePath, { bigint: true });
|
|
1225
|
-
if (!sameSnapshot(after, finalPath))
|
|
1226
|
-
throw changed2(projectPath);
|
|
1227
|
-
await assertStableDirectoryChain(parentChain, `generated Specs index ${projectPath}`);
|
|
1228
|
-
if (!isRecognizedGeneratedSpecsIndex(content, projectPath)) {
|
|
1229
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1230
|
-
"generated_specs_index_owner_review_required",
|
|
1231
|
-
`Reserved Specs index is not recognized generated content: ${projectPath}`
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
return {
|
|
1235
|
-
projectPath,
|
|
1236
|
-
absolutePath,
|
|
1237
|
-
identity: snapshot(after),
|
|
1238
|
-
contentHash: createHash("sha256").update(content).digest("hex"),
|
|
1239
|
-
parentChain
|
|
1240
|
-
};
|
|
1241
|
-
} finally {
|
|
1242
|
-
await handle.close();
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
async function readBounded(handle, projectPath) {
|
|
1246
|
-
const buffer = Buffer2.allocUnsafe(SPECS_MAX_FILE_BYTES + 1);
|
|
1247
|
-
let bytes = 0;
|
|
1248
|
-
while (bytes < buffer.length) {
|
|
1249
|
-
const result = await handle.read(buffer, bytes, buffer.length - bytes, bytes);
|
|
1250
|
-
if (result.bytesRead === 0)
|
|
1251
|
-
break;
|
|
1252
|
-
bytes += result.bytesRead;
|
|
1253
|
-
}
|
|
1254
|
-
if (bytes > SPECS_MAX_FILE_BYTES) {
|
|
1255
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1256
|
-
"generated_specs_index_too_large",
|
|
1257
|
-
`Reserved Specs index exceeds the ${SPECS_MAX_FILE_BYTES}-byte migration limit: ${projectPath}`
|
|
1258
|
-
);
|
|
1259
|
-
}
|
|
1260
|
-
return buffer.subarray(0, bytes).toString("utf8");
|
|
1261
|
-
}
|
|
1262
|
-
async function restoreQuarantinedIndexes(quarantined, fileAdapter, testing) {
|
|
1263
|
-
const recoveryPaths = [];
|
|
1264
|
-
const retainedMutations = [];
|
|
1265
|
-
for (const item of [...quarantined].reverse()) {
|
|
1266
|
-
try {
|
|
1267
|
-
await assertMigrationParent(item, "rollback source inspection");
|
|
1268
|
-
const sourcePath = await firstExistingPath(item);
|
|
1269
|
-
if (!sourcePath) {
|
|
1270
|
-
recoveryPaths.push(item.recoveryPath ?? item.quarantinePath);
|
|
1271
|
-
continue;
|
|
1272
|
-
}
|
|
1273
|
-
await materializeVerifiedSnapshot(
|
|
1274
|
-
item,
|
|
1275
|
-
sourcePath,
|
|
1276
|
-
item.absolutePath,
|
|
1277
|
-
fileAdapter,
|
|
1278
|
-
testing
|
|
1279
|
-
);
|
|
1280
|
-
await assertMigrationParent(item, "rollback cleanup");
|
|
1281
|
-
await unlink3(item.quarantinePath).catch(ignoreMissing);
|
|
1282
|
-
if (item.recoveryPath) {
|
|
1283
|
-
await assertMigrationParent(item, "rollback cleanup");
|
|
1284
|
-
await unlink3(item.recoveryPath).catch(ignoreMissing);
|
|
1285
|
-
}
|
|
1286
|
-
} catch {
|
|
1287
|
-
try {
|
|
1288
|
-
await assertMigrationParent(item, "rollback recovery reporting");
|
|
1289
|
-
const recoveryPath = await firstExistingPath(item);
|
|
1290
|
-
if (recoveryPath)
|
|
1291
|
-
recoveryPaths.push(recoveryPath);
|
|
1292
|
-
else
|
|
1293
|
-
retainedMutations.push(retainedMutationEvidence(item));
|
|
1294
|
-
} catch {
|
|
1295
|
-
const relocated = await findRelocatedRetainedMutation(item).catch(() => null);
|
|
1296
|
-
if (relocated)
|
|
1297
|
-
recoveryPaths.push(relocated);
|
|
1298
|
-
else
|
|
1299
|
-
retainedMutations.push(retainedMutationEvidence(item));
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
return { recoveryPaths, retainedMutations };
|
|
1304
|
-
}
|
|
1305
|
-
async function findRelocatedRetainedMutation(item) {
|
|
1306
|
-
const targetName = item.quarantinePath.slice(dirname(item.quarantinePath).length + 1);
|
|
1307
|
-
const pending = [item.parentChain.rootPath];
|
|
1308
|
-
let inspected = 0;
|
|
1309
|
-
while (pending.length > 0 && inspected < 4096) {
|
|
1310
|
-
const directory = pending.shift();
|
|
1311
|
-
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
1312
|
-
inspected += 1;
|
|
1313
|
-
if (inspected > 4096)
|
|
1314
|
-
return null;
|
|
1315
|
-
const path = resolve2(directory, entry.name);
|
|
1316
|
-
if (entry.isSymbolicLink())
|
|
1317
|
-
continue;
|
|
1318
|
-
if (entry.isDirectory()) {
|
|
1319
|
-
pending.push(path);
|
|
1320
|
-
continue;
|
|
1321
|
-
}
|
|
1322
|
-
if (!entry.isFile() || entry.name !== targetName)
|
|
1323
|
-
continue;
|
|
1324
|
-
const value = await lstat2(path, { bigint: true });
|
|
1325
|
-
if (value.isFile() && !value.isSymbolicLink() && value.dev === item.identity.dev && value.ino === item.identity.ino) {
|
|
1326
|
-
return path;
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
return null;
|
|
1331
|
-
}
|
|
1332
|
-
async function firstExistingPath(item) {
|
|
1333
|
-
for (const path of [item.quarantinePath, item.recoveryPath]) {
|
|
1334
|
-
if (!path) {
|
|
1335
|
-
continue;
|
|
1336
|
-
}
|
|
1337
|
-
try {
|
|
1338
|
-
await assertMigrationParent(item, "recovery source inspection");
|
|
1339
|
-
await lstat2(path);
|
|
1340
|
-
return path;
|
|
1341
|
-
} catch (error) {
|
|
1342
|
-
if (error.code !== "ENOENT")
|
|
1343
|
-
throw error;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
return null;
|
|
1347
|
-
}
|
|
1348
|
-
function ignoreMissing(error) {
|
|
1349
|
-
if (error.code !== "ENOENT")
|
|
1350
|
-
throw error;
|
|
1351
|
-
}
|
|
1352
|
-
async function materializeVerifiedSnapshot(item, sourcePath, destinationPath, fileAdapter, testing) {
|
|
1353
|
-
await assertMigrationParent(item, "snapshot materialization");
|
|
1354
|
-
const source = await inspectRecognizedGeneratedIndexAt(
|
|
1355
|
-
item.parentChain.rootPath,
|
|
1356
|
-
item.projectPath,
|
|
1357
|
-
sourcePath,
|
|
1358
|
-
item.parentChain
|
|
1359
|
-
);
|
|
1360
|
-
if (source.contentHash !== item.contentHash || permissionMode(source.identity.mode) !== permissionMode(item.identity.mode)) {
|
|
1361
|
-
throw changed2(item.projectPath);
|
|
1362
|
-
}
|
|
1363
|
-
try {
|
|
1364
|
-
await fileAdapter.link(sourcePath, destinationPath);
|
|
1365
|
-
} catch (error) {
|
|
1366
|
-
if (!hardLinkUnsupported(error))
|
|
1367
|
-
throw error;
|
|
1368
|
-
await copySnapshotExclusive(
|
|
1369
|
-
source,
|
|
1370
|
-
destinationPath,
|
|
1371
|
-
fileAdapter,
|
|
1372
|
-
testing
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
const destination = await inspectRecognizedGeneratedIndexAt(
|
|
1376
|
-
item.parentChain.rootPath,
|
|
1377
|
-
item.projectPath,
|
|
1378
|
-
destinationPath,
|
|
1379
|
-
item.parentChain
|
|
1380
|
-
);
|
|
1381
|
-
const sourceAfter = await inspectRecognizedGeneratedIndexAt(
|
|
1382
|
-
item.parentChain.rootPath,
|
|
1383
|
-
item.projectPath,
|
|
1384
|
-
sourcePath,
|
|
1385
|
-
item.parentChain
|
|
1386
|
-
);
|
|
1387
|
-
if (destination.contentHash !== item.contentHash || sourceAfter.contentHash !== item.contentHash || permissionMode(destination.identity.mode) !== permissionMode(item.identity.mode) || permissionMode(sourceAfter.identity.mode) !== permissionMode(item.identity.mode) || !sameRenamedSnapshot(source.identity, sourceAfter.identity)) {
|
|
1388
|
-
throw changed2(item.projectPath);
|
|
1389
|
-
}
|
|
1390
|
-
await assertMigrationParent(item, "snapshot materialization");
|
|
1391
|
-
}
|
|
1392
|
-
async function copySnapshotExclusive(source, destinationPath, fileAdapter, testing) {
|
|
1393
|
-
const sourceHandle = await open(source.absolutePath, READ_NO_FOLLOW_FLAGS);
|
|
1394
|
-
let destinationHandle = null;
|
|
1395
|
-
let destinationIdentity = null;
|
|
1396
|
-
try {
|
|
1397
|
-
const sourceOpened = await sourceHandle.stat({ bigint: true });
|
|
1398
|
-
if (!sameSnapshot(source.identity, sourceOpened))
|
|
1399
|
-
throw changed2(source.projectPath);
|
|
1400
|
-
destinationHandle = await open(
|
|
1401
|
-
destinationPath,
|
|
1402
|
-
WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS,
|
|
1403
|
-
Number(permissionMode(source.identity.mode))
|
|
1404
|
-
);
|
|
1405
|
-
const destinationOpened = await destinationHandle.stat({ bigint: true });
|
|
1406
|
-
if (!destinationOpened.isFile() || destinationOpened.isSymbolicLink())
|
|
1407
|
-
throw changed2(source.projectPath);
|
|
1408
|
-
destinationIdentity = snapshot(destinationOpened);
|
|
1409
|
-
await testing?.afterCopyDestinationOpen?.(destinationPath);
|
|
1410
|
-
const hash = createHash("sha256");
|
|
1411
|
-
const buffer = Buffer2.allocUnsafe(64 * 1024);
|
|
1412
|
-
let bytes = 0;
|
|
1413
|
-
while (bytes <= SPECS_MAX_FILE_BYTES) {
|
|
1414
|
-
const read = await sourceHandle.read(buffer, 0, buffer.length, null);
|
|
1415
|
-
if (read.bytesRead === 0)
|
|
1416
|
-
break;
|
|
1417
|
-
bytes += read.bytesRead;
|
|
1418
|
-
if (bytes > SPECS_MAX_FILE_BYTES)
|
|
1419
|
-
throw changed2(source.projectPath);
|
|
1420
|
-
const chunk = buffer.subarray(0, read.bytesRead);
|
|
1421
|
-
hash.update(chunk);
|
|
1422
|
-
let written = 0;
|
|
1423
|
-
while (written < chunk.length) {
|
|
1424
|
-
const result = await destinationHandle.write(
|
|
1425
|
-
chunk,
|
|
1426
|
-
written,
|
|
1427
|
-
chunk.length - written,
|
|
1428
|
-
null
|
|
1429
|
-
);
|
|
1430
|
-
written += result.bytesWritten;
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
if (BigInt(bytes) !== source.identity.size || hash.digest("hex") !== source.contentHash) {
|
|
1434
|
-
throw changed2(source.projectPath);
|
|
1435
|
-
}
|
|
1436
|
-
await destinationHandle.chmod(Number(permissionMode(source.identity.mode)));
|
|
1437
|
-
await destinationHandle.sync();
|
|
1438
|
-
const destinationAfter = await destinationHandle.stat({ bigint: true });
|
|
1439
|
-
const sourceAfter = await sourceHandle.stat({ bigint: true });
|
|
1440
|
-
if (!sameSnapshot(sourceOpened, sourceAfter) || !destinationAfter.isFile() || destinationAfter.isSymbolicLink() || destinationAfter.size !== source.identity.size || permissionMode(destinationAfter.mode) !== permissionMode(source.identity.mode)) {
|
|
1441
|
-
throw changed2(source.projectPath);
|
|
1442
|
-
}
|
|
1443
|
-
} catch (error) {
|
|
1444
|
-
await destinationHandle?.close().catch(() => void 0);
|
|
1445
|
-
destinationHandle = null;
|
|
1446
|
-
if (destinationIdentity) {
|
|
1447
|
-
await cleanupExclusiveDestination(
|
|
1448
|
-
source,
|
|
1449
|
-
destinationPath,
|
|
1450
|
-
destinationIdentity,
|
|
1451
|
-
fileAdapter
|
|
1452
|
-
);
|
|
1453
|
-
}
|
|
1454
|
-
throw error;
|
|
1455
|
-
} finally {
|
|
1456
|
-
await destinationHandle?.close();
|
|
1457
|
-
await sourceHandle.close();
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
async function cleanupExclusiveDestination(source, destinationPath, expected, fileAdapter) {
|
|
1461
|
-
const quarantinePath = `${destinationPath}.${process.pid}.${randomUUID()}.rsp-copy-cleanup`;
|
|
1462
|
-
await assertMigrationParent(source, "exclusive-copy cleanup");
|
|
1463
|
-
try {
|
|
1464
|
-
await rename(destinationPath, quarantinePath);
|
|
1465
|
-
} catch (error) {
|
|
1466
|
-
if (error.code === "ENOENT")
|
|
1467
|
-
return;
|
|
1468
|
-
throw error;
|
|
1469
|
-
}
|
|
1470
|
-
await assertMigrationParent(source, "exclusive-copy cleanup");
|
|
1471
|
-
const quarantined = await lstat2(quarantinePath, { bigint: true });
|
|
1472
|
-
if (quarantined.isFile() && !quarantined.isSymbolicLink() && quarantined.dev === expected.dev && quarantined.ino === expected.ino) {
|
|
1473
|
-
const finalIdentity = await lstat2(quarantinePath, { bigint: true });
|
|
1474
|
-
if (!finalIdentity.isFile() || finalIdentity.isSymbolicLink() || finalIdentity.dev !== expected.dev || finalIdentity.ino !== expected.ino) {
|
|
1475
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1476
|
-
"generated_specs_index_copy_cleanup_required",
|
|
1477
|
-
`Exclusive-copy cleanup retained a changed file: ${quarantinePath}`,
|
|
1478
|
-
[quarantinePath]
|
|
1479
|
-
);
|
|
1480
|
-
}
|
|
1481
|
-
await unlink3(quarantinePath);
|
|
1482
|
-
return;
|
|
1483
|
-
}
|
|
1484
|
-
try {
|
|
1485
|
-
await fileAdapter.link(quarantinePath, destinationPath);
|
|
1486
|
-
await unlink3(quarantinePath);
|
|
1487
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1488
|
-
"generated_specs_index_copy_replaced",
|
|
1489
|
-
`Exclusive-copy cleanup preserved a concurrent replacement at ${destinationPath}`
|
|
1490
|
-
);
|
|
1491
|
-
} catch (error) {
|
|
1492
|
-
if (error instanceof GeneratedSpecsIndexMigrationError)
|
|
1493
|
-
throw error;
|
|
1494
|
-
if (!hardLinkUnsupported(error) && error.code !== "EEXIST") {
|
|
1495
|
-
throw error;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
if (await lstat2(destinationPath).catch(() => null) === null)
|
|
1499
|
-
await copyConcurrentReplacementExclusive(quarantinePath, destinationPath);
|
|
1500
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1501
|
-
"generated_specs_index_copy_cleanup_required",
|
|
1502
|
-
`Exclusive-copy cleanup preserved a concurrent replacement and retained its original inode at ${quarantinePath}`,
|
|
1503
|
-
[quarantinePath]
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1506
|
-
async function copyConcurrentReplacementExclusive(sourcePath, destinationPath) {
|
|
1507
|
-
const sourceHandle = await open(sourcePath, READ_NO_FOLLOW_FLAGS);
|
|
1508
|
-
let destinationHandle = null;
|
|
1509
|
-
try {
|
|
1510
|
-
const sourceBefore = await sourceHandle.stat({ bigint: true });
|
|
1511
|
-
if (!sourceBefore.isFile() || sourceBefore.isSymbolicLink() || sourceBefore.size > BigInt(SPECS_MAX_FILE_BYTES)) {
|
|
1512
|
-
return;
|
|
1513
|
-
}
|
|
1514
|
-
destinationHandle = await open(
|
|
1515
|
-
destinationPath,
|
|
1516
|
-
WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS,
|
|
1517
|
-
Number(permissionMode(sourceBefore.mode))
|
|
1518
|
-
);
|
|
1519
|
-
const buffer = Buffer2.allocUnsafe(64 * 1024);
|
|
1520
|
-
let bytes = 0;
|
|
1521
|
-
while (bytes <= SPECS_MAX_FILE_BYTES) {
|
|
1522
|
-
const read = await sourceHandle.read(buffer, 0, buffer.length, null);
|
|
1523
|
-
if (read.bytesRead === 0)
|
|
1524
|
-
break;
|
|
1525
|
-
bytes += read.bytesRead;
|
|
1526
|
-
if (bytes > SPECS_MAX_FILE_BYTES)
|
|
1527
|
-
return;
|
|
1528
|
-
let written = 0;
|
|
1529
|
-
while (written < read.bytesRead) {
|
|
1530
|
-
const result = await destinationHandle.write(
|
|
1531
|
-
buffer,
|
|
1532
|
-
written,
|
|
1533
|
-
read.bytesRead - written,
|
|
1534
|
-
null
|
|
1535
|
-
);
|
|
1536
|
-
written += result.bytesWritten;
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
await destinationHandle.chmod(Number(permissionMode(sourceBefore.mode)));
|
|
1540
|
-
await destinationHandle.sync();
|
|
1541
|
-
const sourceAfter = await sourceHandle.stat({ bigint: true });
|
|
1542
|
-
const destinationAfter = await destinationHandle.stat({ bigint: true });
|
|
1543
|
-
if (!sameSnapshot(sourceBefore, sourceAfter) || destinationAfter.size !== sourceBefore.size || permissionMode(destinationAfter.mode) !== permissionMode(sourceBefore.mode)) {
|
|
1544
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1545
|
-
"generated_specs_index_copy_cleanup_required",
|
|
1546
|
-
`Concurrent replacement could not be restored byte-for-byte: ${sourcePath}`,
|
|
1547
|
-
[sourcePath]
|
|
1548
|
-
);
|
|
1549
|
-
}
|
|
1550
|
-
} finally {
|
|
1551
|
-
await destinationHandle?.close();
|
|
1552
|
-
await sourceHandle.close();
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
function retainedMutationEvidence(item) {
|
|
1556
|
-
return {
|
|
1557
|
-
projectPath: item.projectPath,
|
|
1558
|
-
quarantineName: item.quarantinePath.slice(dirname(item.quarantinePath).length + 1),
|
|
1559
|
-
contentHash: item.contentHash,
|
|
1560
|
-
device: String(item.identity.dev),
|
|
1561
|
-
inode: String(item.identity.ino),
|
|
1562
|
-
size: String(item.identity.size),
|
|
1563
|
-
mode: permissionMode(item.identity.mode).toString(8),
|
|
1564
|
-
lastKnownParentRealPath: item.parentChain.entries.at(-1).realPath
|
|
1565
|
-
};
|
|
1566
|
-
}
|
|
1567
|
-
async function assertMigrationParent(item, operation) {
|
|
1568
|
-
try {
|
|
1569
|
-
await assertStableDirectoryChain(
|
|
1570
|
-
item.parentChain,
|
|
1571
|
-
`generated Specs index ${item.projectPath} ${operation}`
|
|
1572
|
-
);
|
|
1573
|
-
} catch (error) {
|
|
1574
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1575
|
-
"generated_specs_index_parent_changed",
|
|
1576
|
-
`Generated Specs-index migration stopped because a managed parent changed before ${operation}: ${item.projectPath}. ${error instanceof Error ? error.message : String(error)}`
|
|
1577
|
-
);
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
function hardLinkUnsupported(error) {
|
|
1581
|
-
return ["EXDEV", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EPERM"].includes(error.code ?? "");
|
|
1582
|
-
}
|
|
1583
|
-
function permissionMode(mode) {
|
|
1584
|
-
return mode & 0o777n;
|
|
1585
|
-
}
|
|
1586
|
-
function resolveProjectPath(projectRoot, projectPath) {
|
|
1587
|
-
const normalized = normalizeLogicalPath(projectPath);
|
|
1588
|
-
const absolutePath = resolve2(projectRoot, ...normalized.split("/"));
|
|
1589
|
-
const candidate = relative2(projectRoot, absolutePath);
|
|
1590
|
-
if (normalized === "" || isAbsolute2(projectPath) || candidate === "" || candidate === ".." || candidate.startsWith(`..${sep2}`) || isAbsolute2(candidate) || dirname(normalized) === ".") {
|
|
1591
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1592
|
-
"generated_specs_index_path_invalid",
|
|
1593
|
-
`Generated Specs-index path is outside the managed Specs tree: ${projectPath}`
|
|
1594
|
-
);
|
|
1595
|
-
}
|
|
1596
|
-
return absolutePath;
|
|
1597
|
-
}
|
|
1598
|
-
function changed2(projectPath) {
|
|
1599
|
-
return new GeneratedSpecsIndexMigrationError(
|
|
1600
|
-
"generated_specs_index_changed",
|
|
1601
|
-
`Generated Specs index changed before migration: ${projectPath}`
|
|
1602
|
-
);
|
|
1603
|
-
}
|
|
1604
|
-
function snapshot(value) {
|
|
1605
|
-
return {
|
|
1606
|
-
dev: value.dev,
|
|
1607
|
-
ino: value.ino,
|
|
1608
|
-
size: value.size,
|
|
1609
|
-
mode: value.mode,
|
|
1610
|
-
mtimeNs: value.mtimeNs,
|
|
1611
|
-
ctimeNs: value.ctimeNs
|
|
1612
|
-
};
|
|
1613
|
-
}
|
|
1614
|
-
function sameSnapshot(left, right) {
|
|
1615
|
-
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mode === right.mode && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
1616
|
-
}
|
|
1617
|
-
function sameRenamedSnapshot(left, right) {
|
|
1618
|
-
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mode === right.mode && left.mtimeNs === right.mtimeNs;
|
|
1619
|
-
}
|
|
1001
|
+
## Scenarios
|
|
1002
|
+
<!-- Clarify key cross-boundary, failure, or ambiguous behavior with a few examples; omit when unnecessary and keep test execution results in the Change. -->
|
|
1003
|
+
- ${placeholder}
|
|
1620
1004
|
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
mkdtemp,
|
|
1630
|
-
open as open2,
|
|
1631
|
-
rename as rename2,
|
|
1632
|
-
rmdir,
|
|
1633
|
-
unlink as unlink4
|
|
1634
|
-
} from "fs/promises";
|
|
1635
|
-
import { tmpdir } from "os";
|
|
1636
|
-
import { dirname as dirname2, join as join2, relative as relative3, resolve as resolve3, sep as sep3 } from "path";
|
|
1637
|
-
var READ_NO_FOLLOW_FLAGS2 = constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0);
|
|
1638
|
-
var WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS2 = constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0);
|
|
1639
|
-
var UpdateRollbackMutationError = class extends Error {
|
|
1640
|
-
constructor(message, recoveryPaths = []) {
|
|
1641
|
-
super(message);
|
|
1642
|
-
this.recoveryPaths = recoveryPaths;
|
|
1643
|
-
this.name = "UpdateRollbackMutationError";
|
|
1644
|
-
}
|
|
1645
|
-
recoveryPaths;
|
|
1646
|
-
};
|
|
1647
|
-
var UpdateRollbackJournal = class {
|
|
1648
|
-
constructor(projectRoot = resolve3("."), testing = {}) {
|
|
1649
|
-
this.projectRoot = projectRoot;
|
|
1650
|
-
this.testing = testing;
|
|
1651
|
-
}
|
|
1652
|
-
projectRoot;
|
|
1653
|
-
testing;
|
|
1654
|
-
entries = /* @__PURE__ */ new Map();
|
|
1655
|
-
mutationOrder = [];
|
|
1656
|
-
async capture(path) {
|
|
1657
|
-
const absolutePath = this.resolveProjectPath(path);
|
|
1658
|
-
if (this.entries.has(absolutePath))
|
|
1659
|
-
return;
|
|
1660
|
-
this.entries.set(absolutePath, {
|
|
1661
|
-
path: absolutePath,
|
|
1662
|
-
original: await inspectPathSnapshot(absolutePath),
|
|
1663
|
-
published: null,
|
|
1664
|
-
parentChain: await captureExistingParentChain(
|
|
1665
|
-
this.projectRoot,
|
|
1666
|
-
absolutePath
|
|
1667
|
-
)
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1670
|
-
async captureDirectoryChain(path) {
|
|
1671
|
-
const absolutePath = this.resolveProjectPath(path);
|
|
1672
|
-
const rel = relative3(this.projectRoot, absolutePath);
|
|
1673
|
-
const captured = [];
|
|
1674
|
-
let current = this.projectRoot;
|
|
1675
|
-
for (const segment of rel === "" ? [] : rel.split(sep3)) {
|
|
1676
|
-
current = resolve3(current, segment);
|
|
1677
|
-
await this.capture(current);
|
|
1678
|
-
captured.push(current);
|
|
1679
|
-
}
|
|
1680
|
-
return captured;
|
|
1681
|
-
}
|
|
1682
|
-
async mark(path) {
|
|
1683
|
-
const absolutePath = this.resolveProjectPath(path);
|
|
1684
|
-
const entry = this.entries.get(absolutePath);
|
|
1685
|
-
if (!entry)
|
|
1686
|
-
throw new Error(`Update rollback journal did not capture ${absolutePath}`);
|
|
1687
|
-
entry.published = await inspectPathSnapshot(absolutePath);
|
|
1688
|
-
if (!samePathState(entry.original, entry.published) && !this.mutationOrder.includes(absolutePath)) {
|
|
1689
|
-
this.mutationOrder.push(absolutePath);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
async markAll(paths) {
|
|
1693
|
-
for (const path of paths)
|
|
1694
|
-
await this.mark(path);
|
|
1695
|
-
}
|
|
1696
|
-
async rollback() {
|
|
1697
|
-
const restored = [];
|
|
1698
|
-
const retainedMutations = [];
|
|
1699
|
-
const recoveryPaths = [];
|
|
1700
|
-
for (const [path, entry] of this.entries) {
|
|
1701
|
-
if (entry.published)
|
|
1702
|
-
continue;
|
|
1703
|
-
const current = await inspectPathSnapshot(path).catch(() => null);
|
|
1704
|
-
if (current && !samePathState(entry.original, current)) {
|
|
1705
|
-
entry.published = current;
|
|
1706
|
-
if (!this.mutationOrder.includes(path))
|
|
1707
|
-
this.mutationOrder.push(path);
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
const ordered = [...this.mutationOrder].sort((left, right) => compareRollbackOrder(
|
|
1711
|
-
this.entries.get(left),
|
|
1712
|
-
this.entries.get(right)
|
|
1713
|
-
));
|
|
1714
|
-
for (const path of ordered) {
|
|
1715
|
-
const entry = this.entries.get(path);
|
|
1716
|
-
const published = entry.published;
|
|
1717
|
-
if (!published)
|
|
1718
|
-
continue;
|
|
1719
|
-
let current;
|
|
1720
|
-
try {
|
|
1721
|
-
current = await inspectPathSnapshot(path);
|
|
1722
|
-
} catch {
|
|
1723
|
-
await recordIncompleteRollback(
|
|
1724
|
-
this.projectRoot,
|
|
1725
|
-
entry,
|
|
1726
|
-
retainedMutations,
|
|
1727
|
-
recoveryPaths
|
|
1728
|
-
);
|
|
1729
|
-
continue;
|
|
1730
|
-
}
|
|
1731
|
-
if (!samePathState(current, published)) {
|
|
1732
|
-
await recordIncompleteRollback(
|
|
1733
|
-
this.projectRoot,
|
|
1734
|
-
entry,
|
|
1735
|
-
retainedMutations,
|
|
1736
|
-
recoveryPaths
|
|
1737
|
-
);
|
|
1738
|
-
continue;
|
|
1739
|
-
}
|
|
1740
|
-
try {
|
|
1741
|
-
recoveryPaths.push(...await restorePath(entry, this.testing));
|
|
1742
|
-
restored.push(toProjectPath(this.projectRoot, path));
|
|
1743
|
-
if (entry.original.kind === "directory" && published.kind === "missing") {
|
|
1744
|
-
await this.refreshDescendantParentChains(path);
|
|
1745
|
-
}
|
|
1746
|
-
} catch (error) {
|
|
1747
|
-
if (error instanceof UpdateRollbackMutationError)
|
|
1748
|
-
recoveryPaths.push(...error.recoveryPaths);
|
|
1749
|
-
await recordIncompleteRollback(
|
|
1750
|
-
this.projectRoot,
|
|
1751
|
-
entry,
|
|
1752
|
-
retainedMutations,
|
|
1753
|
-
recoveryPaths
|
|
1754
|
-
);
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
return {
|
|
1758
|
-
restored,
|
|
1759
|
-
retainedMutations: [...new Set(retainedMutations)].sort(),
|
|
1760
|
-
recoveryPaths: [...new Set(recoveryPaths)].sort()
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
resolveProjectPath(path) {
|
|
1764
|
-
const absolutePath = resolve3(path);
|
|
1765
|
-
const rel = relative3(this.projectRoot, absolutePath);
|
|
1766
|
-
if (rel === ".." || rel.startsWith(`..${sep3}`))
|
|
1767
|
-
throw new Error(`Update transaction path escapes the project root: ${path}`);
|
|
1768
|
-
return absolutePath;
|
|
1769
|
-
}
|
|
1770
|
-
async refreshDescendantParentChains(restoredDirectory) {
|
|
1771
|
-
for (const entry of this.entries.values()) {
|
|
1772
|
-
const rel = relative3(restoredDirectory, entry.path);
|
|
1773
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${sep3}`))
|
|
1774
|
-
continue;
|
|
1775
|
-
entry.parentChain = await captureExistingParentChain(
|
|
1776
|
-
this.projectRoot,
|
|
1777
|
-
entry.path
|
|
1778
|
-
);
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
};
|
|
1782
|
-
async function recordIncompleteRollback(projectRoot, entry, retainedMutations, recoveryPaths) {
|
|
1783
|
-
retainedMutations.push(toProjectPath(projectRoot, entry.path));
|
|
1784
|
-
const recovery = await persistOriginalRecovery(entry.original).catch(() => null);
|
|
1785
|
-
if (recovery)
|
|
1786
|
-
recoveryPaths.push(recovery);
|
|
1787
|
-
}
|
|
1788
|
-
async function inspectPathSnapshot(path) {
|
|
1789
|
-
let before;
|
|
1790
|
-
try {
|
|
1791
|
-
before = await lstat3(path, { bigint: true });
|
|
1792
|
-
} catch (error) {
|
|
1793
|
-
if (error.code === "ENOENT")
|
|
1794
|
-
return { kind: "missing" };
|
|
1795
|
-
throw error;
|
|
1796
|
-
}
|
|
1797
|
-
if (before.isSymbolicLink())
|
|
1798
|
-
throw new Error(`Update transaction refuses a symlinked path: ${path}`);
|
|
1799
|
-
if (before.isDirectory()) {
|
|
1800
|
-
return {
|
|
1801
|
-
kind: "directory",
|
|
1802
|
-
device: before.dev,
|
|
1803
|
-
inode: before.ino,
|
|
1804
|
-
mode: before.mode
|
|
1805
|
-
};
|
|
1806
|
-
}
|
|
1807
|
-
if (!before.isFile())
|
|
1808
|
-
throw new Error(`Update transaction requires a regular file or directory: ${path}`);
|
|
1809
|
-
const handle = await open2(path, READ_NO_FOLLOW_FLAGS2);
|
|
1810
|
-
try {
|
|
1811
|
-
const opened = await handle.stat({ bigint: true });
|
|
1812
|
-
if (!sameFileIdentity(before, opened))
|
|
1813
|
-
throw new Error(`Update transaction path changed while it was captured: ${path}`);
|
|
1814
|
-
const content = await handle.readFile();
|
|
1815
|
-
const after = await handle.stat({ bigint: true });
|
|
1816
|
-
const finalPath = await lstat3(path, { bigint: true });
|
|
1817
|
-
if (!sameFileIdentity(opened, after) || !sameFileIdentity(after, finalPath) || BigInt(content.length) !== after.size) {
|
|
1818
|
-
throw new Error(`Update transaction path changed while it was captured: ${path}`);
|
|
1819
|
-
}
|
|
1820
|
-
return {
|
|
1821
|
-
kind: "file",
|
|
1822
|
-
content,
|
|
1823
|
-
device: after.dev,
|
|
1824
|
-
inode: after.ino,
|
|
1825
|
-
mode: after.mode,
|
|
1826
|
-
size: after.size,
|
|
1827
|
-
mtimeNs: after.mtimeNs,
|
|
1828
|
-
ctimeNs: after.ctimeNs,
|
|
1829
|
-
contentHash: createHash2("sha256").update(content).digest("hex")
|
|
1830
|
-
};
|
|
1831
|
-
} finally {
|
|
1832
|
-
await handle.close();
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
async function restorePath(entry, testing) {
|
|
1836
|
-
const { path, original, published, parentChain } = entry;
|
|
1837
|
-
if (!published)
|
|
1838
|
-
return [];
|
|
1839
|
-
if (original.kind === "missing") {
|
|
1840
|
-
if (published.kind === "file") {
|
|
1841
|
-
const quarantine2 = await quarantineExpectedFile(entry, published, testing);
|
|
1842
|
-
await unlinkVerifiedQuarantine(quarantine2, published, parentChain);
|
|
1843
|
-
return [];
|
|
1844
|
-
}
|
|
1845
|
-
if (published.kind === "directory") {
|
|
1846
|
-
const quarantine2 = await quarantineExpectedDirectory(entry, published, testing);
|
|
1847
|
-
await assertStableDirectoryChain(parentChain, "update rollback directory removal");
|
|
1848
|
-
await rmdir(quarantine2);
|
|
1849
|
-
await assertStableDirectoryChain(parentChain, "update rollback directory removal");
|
|
1850
|
-
return [];
|
|
1851
|
-
}
|
|
1852
|
-
return [];
|
|
1853
|
-
}
|
|
1854
|
-
if (original.kind === "directory") {
|
|
1855
|
-
if (published.kind === "missing") {
|
|
1856
|
-
await assertStableDirectoryChain(parentChain, "update rollback directory restore");
|
|
1857
|
-
await mkdir2(path, { mode: Number(permissionMode2(original.mode)) });
|
|
1858
|
-
if (process.platform !== "win32")
|
|
1859
|
-
await chmod(path, Number(permissionMode2(original.mode)));
|
|
1860
|
-
return [];
|
|
1861
|
-
}
|
|
1862
|
-
if (published.kind !== "directory")
|
|
1863
|
-
throw new Error(`Update rollback path type changed: ${path}`);
|
|
1864
|
-
const current = await inspectPathSnapshot(path);
|
|
1865
|
-
if (!samePathState(current, published))
|
|
1866
|
-
throw new UpdateRollbackMutationError(`Update rollback directory changed: ${path}`);
|
|
1867
|
-
if (process.platform !== "win32" && permissionMode2(published.mode) !== permissionMode2(original.mode)) {
|
|
1868
|
-
await assertStableDirectoryChain(parentChain, "update rollback directory mode restore");
|
|
1869
|
-
await chmod(path, Number(permissionMode2(original.mode)));
|
|
1870
|
-
}
|
|
1871
|
-
return [];
|
|
1872
|
-
}
|
|
1873
|
-
if (published.kind === "missing") {
|
|
1874
|
-
await writeSnapshotExclusive(path, original, parentChain, testing);
|
|
1875
|
-
return [];
|
|
1876
|
-
}
|
|
1877
|
-
if (published.kind !== "file")
|
|
1878
|
-
throw new Error(`Update rollback path type changed: ${path}`);
|
|
1879
|
-
const quarantine = await quarantineExpectedFile(entry, published, testing);
|
|
1880
|
-
try {
|
|
1881
|
-
await writeSnapshotExclusive(path, original, parentChain, testing);
|
|
1882
|
-
} catch (error) {
|
|
1883
|
-
const recovery = await restoreQuarantinedFile(
|
|
1884
|
-
quarantine,
|
|
1885
|
-
path,
|
|
1886
|
-
published,
|
|
1887
|
-
parentChain
|
|
1888
|
-
);
|
|
1889
|
-
if (recovery.length > 0) {
|
|
1890
|
-
throw new UpdateRollbackMutationError(
|
|
1891
|
-
`Update rollback preserved the current file after restore failure: ${path}`,
|
|
1892
|
-
recovery
|
|
1893
|
-
);
|
|
1894
|
-
}
|
|
1895
|
-
throw error;
|
|
1896
|
-
}
|
|
1897
|
-
await unlinkVerifiedQuarantine(quarantine, published, parentChain);
|
|
1898
|
-
return [];
|
|
1899
|
-
}
|
|
1900
|
-
async function quarantineExpectedFile(entry, expected, testing) {
|
|
1901
|
-
await testing.afterPublishedValidation?.(entry.path);
|
|
1902
|
-
await assertStableDirectoryChain(entry.parentChain, "update rollback file quarantine");
|
|
1903
|
-
const quarantine = `${entry.path}.${process.pid}.${randomUUID2()}.rsp-update-current`;
|
|
1904
|
-
await rename2(entry.path, quarantine);
|
|
1905
|
-
await assertStableDirectoryChain(entry.parentChain, "update rollback file quarantine");
|
|
1906
|
-
const quarantined = await inspectPathSnapshot(quarantine);
|
|
1907
|
-
if (!sameClaimedPathState(quarantined, expected)) {
|
|
1908
|
-
const recovery = await restoreQuarantinedPath(
|
|
1909
|
-
quarantine,
|
|
1910
|
-
entry.path,
|
|
1911
|
-
quarantined,
|
|
1912
|
-
entry.parentChain
|
|
1913
|
-
);
|
|
1914
|
-
throw new UpdateRollbackMutationError(
|
|
1915
|
-
`Update rollback refused a concurrently replaced file: ${entry.path}`,
|
|
1916
|
-
recovery
|
|
1917
|
-
);
|
|
1918
|
-
}
|
|
1919
|
-
return quarantine;
|
|
1920
|
-
}
|
|
1921
|
-
async function quarantineExpectedDirectory(entry, expected, testing) {
|
|
1922
|
-
await testing.afterPublishedValidation?.(entry.path);
|
|
1923
|
-
await assertStableDirectoryChain(entry.parentChain, "update rollback directory quarantine");
|
|
1924
|
-
const quarantine = `${entry.path}.${process.pid}.${randomUUID2()}.rsp-update-current`;
|
|
1925
|
-
await rename2(entry.path, quarantine);
|
|
1926
|
-
await assertStableDirectoryChain(entry.parentChain, "update rollback directory quarantine");
|
|
1927
|
-
const quarantined = await inspectPathSnapshot(quarantine);
|
|
1928
|
-
if (!sameClaimedPathState(quarantined, expected)) {
|
|
1929
|
-
const recovery = await restoreQuarantinedPath(
|
|
1930
|
-
quarantine,
|
|
1931
|
-
entry.path,
|
|
1932
|
-
quarantined,
|
|
1933
|
-
entry.parentChain
|
|
1934
|
-
);
|
|
1935
|
-
throw new UpdateRollbackMutationError(
|
|
1936
|
-
`Update rollback refused a concurrently replaced directory: ${entry.path}`,
|
|
1937
|
-
recovery
|
|
1938
|
-
);
|
|
1939
|
-
}
|
|
1940
|
-
return quarantine;
|
|
1941
|
-
}
|
|
1942
|
-
async function restoreQuarantinedPath(quarantine, destination, snapshot2, parentChain) {
|
|
1943
|
-
if (snapshot2.kind === "file")
|
|
1944
|
-
return restoreQuarantinedFile(quarantine, destination, snapshot2, parentChain);
|
|
1945
|
-
return [quarantine];
|
|
1946
|
-
}
|
|
1947
|
-
async function restoreQuarantinedFile(quarantine, destination, snapshot2, parentChain) {
|
|
1948
|
-
await assertStableDirectoryChain(parentChain, "update rollback concurrent-file restore");
|
|
1949
|
-
try {
|
|
1950
|
-
await link2(quarantine, destination);
|
|
1951
|
-
const restored = await inspectPathSnapshot(destination);
|
|
1952
|
-
const retained = await inspectPathSnapshot(quarantine);
|
|
1953
|
-
if (!sameClaimedPathState(restored, snapshot2) || !sameClaimedPathState(retained, snapshot2)) {
|
|
1954
|
-
return [quarantine];
|
|
1955
|
-
}
|
|
1956
|
-
await unlinkVerifiedQuarantine(quarantine, snapshot2, parentChain);
|
|
1957
|
-
return [];
|
|
1958
|
-
} catch (error) {
|
|
1959
|
-
if (error.code === "EEXIST")
|
|
1960
|
-
return [quarantine];
|
|
1961
|
-
if (!hardLinkUnsupported2(error))
|
|
1962
|
-
throw error;
|
|
1963
|
-
}
|
|
1005
|
+
## Constraints
|
|
1006
|
+
<!-- Record necessary technical, protocol, security, or compatibility limits; do not repeat behavior requirements or decision rationale. -->
|
|
1007
|
+
- ${placeholder}
|
|
1008
|
+
`;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/core/update-check.ts
|
|
1012
|
+
async function checkForUpdates(current) {
|
|
1964
1013
|
try {
|
|
1965
|
-
await
|
|
1014
|
+
const response = await fetch("https://registry.npmjs.org/-/package/@oevery%2Frsp/dist-tags", {
|
|
1015
|
+
signal: AbortSignal.timeout(2e3),
|
|
1016
|
+
redirect: "error"
|
|
1017
|
+
});
|
|
1018
|
+
if (!response.ok)
|
|
1019
|
+
throw new Error("registry unavailable");
|
|
1020
|
+
const body = await response.text();
|
|
1021
|
+
if (body.length > 16384)
|
|
1022
|
+
throw new Error("registry metadata exceeded bound");
|
|
1023
|
+
const latest = JSON.parse(body)?.latest;
|
|
1024
|
+
if (typeof latest !== "string" || !isExactVersion(latest) || !/^\d+\.\d+\.\d+$/.test(latest))
|
|
1025
|
+
throw new Error("invalid stable version");
|
|
1026
|
+
return { status: compareVersions(latest, current) > 0 ? "available" : "current", current, latest };
|
|
1966
1027
|
} catch {
|
|
1967
|
-
return
|
|
1028
|
+
return { status: "unavailable", current };
|
|
1968
1029
|
}
|
|
1969
|
-
return [quarantine];
|
|
1970
1030
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1031
|
+
|
|
1032
|
+
// src/commands/skills.ts
|
|
1033
|
+
import { lstatSync, realpathSync } from "fs";
|
|
1034
|
+
import { chmod, copyFile, mkdir, mkdtemp, readdir, readFile as readFile3, rename, rm } from "fs/promises";
|
|
1035
|
+
import { dirname, join as join2, relative } from "path";
|
|
1036
|
+
var DEFAULT_PACKAGED_SKILL_NAMES = [
|
|
1037
|
+
"rsp",
|
|
1038
|
+
"rsp-shape",
|
|
1039
|
+
"rsp-implement",
|
|
1040
|
+
"rsp-doc",
|
|
1041
|
+
"rsp-verify",
|
|
1042
|
+
"rsp-review",
|
|
1043
|
+
"rsp-commit"
|
|
1044
|
+
];
|
|
1045
|
+
var RETIRED_PACKAGED_SKILL_NAMES = [
|
|
1046
|
+
"rsp-address-review",
|
|
1047
|
+
"rsp-codebase-audit",
|
|
1048
|
+
"rsp-structural-audit",
|
|
1049
|
+
"rsp-design",
|
|
1050
|
+
"rsp-diagnose",
|
|
1051
|
+
"rsp-manage",
|
|
1052
|
+
"rsp-release-docs",
|
|
1053
|
+
"rsp-resolve-findings",
|
|
1054
|
+
"rsp-tdd"
|
|
1055
|
+
];
|
|
1056
|
+
function unsupportedEntry(path, label) {
|
|
1057
|
+
return new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`);
|
|
1058
|
+
}
|
|
1059
|
+
async function inspectPackagedSkills(packageRoot) {
|
|
1060
|
+
const skillsRoot = join2(packageRoot, "skills");
|
|
1061
|
+
const rootInspection = inspectManagedDirectory(skillsRoot, "packaged Skills root");
|
|
1062
|
+
if (rootInspection.issue)
|
|
1063
|
+
throw rootInspection.issue;
|
|
1064
|
+
const entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
1065
|
+
const skills = [];
|
|
1066
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1067
|
+
if (entry.name.startsWith("."))
|
|
1068
|
+
continue;
|
|
1069
|
+
const root = join2(skillsRoot, entry.name);
|
|
1070
|
+
if (!entry.isDirectory())
|
|
1071
|
+
throw unsupportedEntry(root, "packaged Skills");
|
|
1072
|
+
const inspection = await inspectManagedFileTree(root, `packaged Skill ${entry.name}`, { includeHidden: true });
|
|
1073
|
+
if (inspection.issues.length > 0)
|
|
1074
|
+
throw inspection.issues[0];
|
|
1075
|
+
skills.push({
|
|
1076
|
+
name: entry.name,
|
|
1077
|
+
root,
|
|
1078
|
+
files: inspection.files.map((path) => relative(root, path))
|
|
1079
|
+
});
|
|
1979
1080
|
}
|
|
1980
|
-
|
|
1981
|
-
await assertStableDirectoryChain(parentChain, "update rollback quarantine cleanup");
|
|
1081
|
+
return skills;
|
|
1982
1082
|
}
|
|
1983
|
-
|
|
1984
|
-
let handle = null;
|
|
1985
|
-
let createdIdentity = null;
|
|
1083
|
+
function inspectTarget(path, name) {
|
|
1986
1084
|
try {
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
Number(permissionMode2(snapshot2.mode))
|
|
1992
|
-
);
|
|
1993
|
-
const opened = await handle.stat({ bigint: true });
|
|
1994
|
-
if (!opened.isFile() || opened.isSymbolicLink())
|
|
1995
|
-
throw new Error(`Update rollback could not open a regular restore file: ${path}`);
|
|
1996
|
-
createdIdentity = { device: opened.dev, inode: opened.ino };
|
|
1997
|
-
await testing.afterRestoreDestinationOpen?.(path);
|
|
1998
|
-
await handle.writeFile(snapshot2.content);
|
|
1999
|
-
await handle.chmod(Number(permissionMode2(snapshot2.mode)));
|
|
2000
|
-
await handle.sync();
|
|
2001
|
-
const value = await handle.stat({ bigint: true });
|
|
2002
|
-
if (!value.isFile() || value.isSymbolicLink() || value.dev !== createdIdentity.device || value.ino !== createdIdentity.inode || value.size !== snapshot2.size || permissionMode2(value.mode) !== permissionMode2(snapshot2.mode)) {
|
|
2003
|
-
throw new Error(`Update rollback could not validate restored file: ${path}`);
|
|
2004
|
-
}
|
|
1085
|
+
const stats = lstatSync(path);
|
|
1086
|
+
if (stats.isDirectory())
|
|
1087
|
+
return "directory";
|
|
1088
|
+
throw unsupportedEntry(path, `installed Skill ${name}`);
|
|
2005
1089
|
} catch (error) {
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
if (createdIdentity) {
|
|
2009
|
-
const recovery = await cleanupCreatedFile(
|
|
2010
|
-
path,
|
|
2011
|
-
createdIdentity,
|
|
2012
|
-
parentChain
|
|
2013
|
-
);
|
|
2014
|
-
if (recovery.length > 0) {
|
|
2015
|
-
throw new UpdateRollbackMutationError(
|
|
2016
|
-
`Update rollback exclusive restore preserved a concurrent replacement: ${path}`,
|
|
2017
|
-
recovery
|
|
2018
|
-
);
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
1090
|
+
if (error.code === "ENOENT")
|
|
1091
|
+
return "missing";
|
|
2021
1092
|
throw error;
|
|
2022
|
-
} finally {
|
|
2023
|
-
await handle?.close();
|
|
2024
|
-
}
|
|
2025
|
-
await assertStableDirectoryChain(parentChain, "update rollback exclusive restore");
|
|
2026
|
-
const restored = await inspectPathSnapshot(path);
|
|
2027
|
-
if (restored.kind !== "file" || restored.contentHash !== snapshot2.contentHash || permissionMode2(restored.mode) !== permissionMode2(snapshot2.mode)) {
|
|
2028
|
-
throw new Error(`Update rollback restored different bytes or mode: ${path}`);
|
|
2029
1093
|
}
|
|
2030
1094
|
}
|
|
2031
|
-
|
|
2032
|
-
const quarantine = `${path}.${process.pid}.${randomUUID2()}.rsp-update-created`;
|
|
2033
|
-
await assertStableDirectoryChain(parentChain, "update rollback exclusive cleanup");
|
|
1095
|
+
function inspectInventoryTarget(path) {
|
|
2034
1096
|
try {
|
|
2035
|
-
|
|
1097
|
+
return lstatSync(path).isDirectory() ? "directory" : "divergent";
|
|
2036
1098
|
} catch (error) {
|
|
2037
1099
|
if (error.code === "ENOENT")
|
|
2038
|
-
return
|
|
1100
|
+
return "missing";
|
|
2039
1101
|
throw error;
|
|
2040
1102
|
}
|
|
2041
|
-
await assertStableDirectoryChain(parentChain, "update rollback exclusive cleanup");
|
|
2042
|
-
const current = await lstat3(quarantine, { bigint: true });
|
|
2043
|
-
if (current.isFile() && !current.isSymbolicLink() && current.dev === expected.device && current.ino === expected.inode) {
|
|
2044
|
-
const finalIdentity = await lstat3(quarantine, { bigint: true });
|
|
2045
|
-
if (!finalIdentity.isFile() || finalIdentity.isSymbolicLink() || finalIdentity.dev !== expected.device || finalIdentity.ino !== expected.inode) {
|
|
2046
|
-
return [quarantine];
|
|
2047
|
-
}
|
|
2048
|
-
await unlink4(quarantine);
|
|
2049
|
-
return [];
|
|
2050
|
-
}
|
|
2051
|
-
const snapshot2 = await inspectPathSnapshot(quarantine);
|
|
2052
|
-
return restoreQuarantinedPath(quarantine, path, snapshot2, parentChain);
|
|
2053
|
-
}
|
|
2054
|
-
async function persistOriginalRecovery(original) {
|
|
2055
|
-
if (original.kind !== "file")
|
|
2056
|
-
return null;
|
|
2057
|
-
const recoveryRoot = await mkdtemp(join2(tmpdir(), "rsp-update-recovery-"));
|
|
2058
|
-
if (process.platform !== "win32")
|
|
2059
|
-
await chmod(recoveryRoot, 448);
|
|
2060
|
-
const recovery = join2(recoveryRoot, "snapshot");
|
|
2061
|
-
const parentChain = await captureStableDirectoryChain({
|
|
2062
|
-
rootPath: recoveryRoot,
|
|
2063
|
-
targetPath: recoveryRoot,
|
|
2064
|
-
label: "update rollback recovery root"
|
|
2065
|
-
});
|
|
2066
|
-
await writeSnapshotExclusive(recovery, original, parentChain, {});
|
|
2067
|
-
return recovery;
|
|
2068
|
-
}
|
|
2069
|
-
async function captureExistingParentChain(projectRoot, path) {
|
|
2070
|
-
let parent = dirname2(path);
|
|
2071
|
-
while (true) {
|
|
2072
|
-
const rel = relative3(projectRoot, parent);
|
|
2073
|
-
if (rel === ".." || rel.startsWith(`..${sep3}`))
|
|
2074
|
-
throw new Error(`Update transaction parent escapes the project root: ${path}`);
|
|
2075
|
-
try {
|
|
2076
|
-
const value = await lstat3(parent);
|
|
2077
|
-
if (!value.isDirectory() || value.isSymbolicLink())
|
|
2078
|
-
throw new Error(`Update transaction parent must be a real directory: ${parent}`);
|
|
2079
|
-
break;
|
|
2080
|
-
} catch (error) {
|
|
2081
|
-
if (error.code !== "ENOENT")
|
|
2082
|
-
throw error;
|
|
2083
|
-
if (parent === projectRoot)
|
|
2084
|
-
throw error;
|
|
2085
|
-
parent = dirname2(parent);
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
return captureStableDirectoryChain({
|
|
2089
|
-
rootPath: projectRoot,
|
|
2090
|
-
targetPath: parent,
|
|
2091
|
-
label: `update transaction ${path}`
|
|
2092
|
-
});
|
|
2093
1103
|
}
|
|
2094
|
-
function
|
|
2095
|
-
|
|
1104
|
+
function isCanonicalSourceProjection(sourceRoot, targetRoot) {
|
|
1105
|
+
try {
|
|
1106
|
+
if (!lstatSync(targetRoot).isSymbolicLink())
|
|
1107
|
+
return false;
|
|
1108
|
+
return realpathSync(targetRoot) === realpathSync(sourceRoot);
|
|
1109
|
+
} catch {
|
|
2096
1110
|
return false;
|
|
2097
|
-
if (left.kind === "missing")
|
|
2098
|
-
return true;
|
|
2099
|
-
if (left.kind === "directory" && right.kind === "directory") {
|
|
2100
|
-
return left.device === right.device && left.inode === right.inode && permissionMode2(left.mode) === permissionMode2(right.mode);
|
|
2101
1111
|
}
|
|
2102
|
-
if (left.kind === "file" && right.kind === "file") {
|
|
2103
|
-
return left.device === right.device && left.inode === right.inode && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs && permissionMode2(left.mode) === permissionMode2(right.mode) && left.contentHash === right.contentHash;
|
|
2104
|
-
}
|
|
2105
|
-
return false;
|
|
2106
|
-
}
|
|
2107
|
-
function compareRollbackOrder(left, right) {
|
|
2108
|
-
const leftRestoresDeleted = left.original.kind !== "missing" && left.published?.kind === "missing";
|
|
2109
|
-
const rightRestoresDeleted = right.original.kind !== "missing" && right.published?.kind === "missing";
|
|
2110
|
-
if (leftRestoresDeleted !== rightRestoresDeleted)
|
|
2111
|
-
return leftRestoresDeleted ? -1 : 1;
|
|
2112
|
-
if (leftRestoresDeleted)
|
|
2113
|
-
return pathDepth(left.path) - pathDepth(right.path);
|
|
2114
|
-
return pathDepth(right.path) - pathDepth(left.path);
|
|
2115
1112
|
}
|
|
2116
|
-
function
|
|
2117
|
-
|
|
1113
|
+
async function treesEqual(source, targetRoot) {
|
|
1114
|
+
const target = await inspectManagedFileTree(targetRoot, `installed Skill ${source.name}`, { includeHidden: true });
|
|
1115
|
+
if (target.issues.length > 0)
|
|
1116
|
+
throw target.issues[0];
|
|
1117
|
+
const targetFiles = target.files.map((path) => relative(targetRoot, path));
|
|
1118
|
+
if (source.files.length !== targetFiles.length || source.files.some((path, index) => path !== targetFiles[index]))
|
|
2118
1119
|
return false;
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
1120
|
+
for (const path of source.files) {
|
|
1121
|
+
const sourcePath = join2(source.root, path);
|
|
1122
|
+
const targetPath = join2(targetRoot, path);
|
|
1123
|
+
const [sourceStats, targetStats] = [lstatSync(sourcePath), lstatSync(targetPath)];
|
|
1124
|
+
if (!sourceStats.isFile() || !targetStats.isFile())
|
|
1125
|
+
throw unsupportedEntry(!sourceStats.isFile() ? sourcePath : targetPath, `Skill ${source.name}`);
|
|
1126
|
+
if ((sourceStats.mode & 511) !== (targetStats.mode & 511))
|
|
1127
|
+
return false;
|
|
1128
|
+
const [sourceContent, targetContent] = await Promise.all([readFile3(sourcePath), readFile3(targetPath)]);
|
|
1129
|
+
if (!sourceContent.equals(targetContent))
|
|
1130
|
+
return false;
|
|
2126
1131
|
}
|
|
2127
|
-
return
|
|
2128
|
-
}
|
|
2129
|
-
function sameFileIdentity(left, right) {
|
|
2130
|
-
return left.isFile() && right.isFile() && !left.isSymbolicLink() && !right.isSymbolicLink() && left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
2131
|
-
}
|
|
2132
|
-
function hardLinkUnsupported2(error) {
|
|
2133
|
-
return ["EXDEV", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EPERM"].includes(error.code ?? "");
|
|
2134
|
-
}
|
|
2135
|
-
function permissionMode2(mode) {
|
|
2136
|
-
return mode & 0o777n;
|
|
2137
|
-
}
|
|
2138
|
-
function pathDepth(path) {
|
|
2139
|
-
return path.split(sep3).length;
|
|
1132
|
+
return true;
|
|
2140
1133
|
}
|
|
2141
|
-
function
|
|
2142
|
-
|
|
1134
|
+
async function inspectPackagedSkillInventory(options = {}) {
|
|
1135
|
+
const packageRoot = options.packageRoot ?? PKG_ROOT;
|
|
1136
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
1137
|
+
const packagedSkills = await inspectPackagedSkills(packageRoot);
|
|
1138
|
+
const packagedNames = new Set(packagedSkills.map((skill) => skill.name));
|
|
1139
|
+
const missingDefaults = DEFAULT_PACKAGED_SKILL_NAMES.filter((name) => !packagedNames.has(name));
|
|
1140
|
+
if (missingDefaults.length > 0)
|
|
1141
|
+
throw new Error(`packaged Skills missing default Skills: ${missingDefaults.join(", ")}`);
|
|
1142
|
+
const manifest = JSON.parse(await readFile3(join2(packageRoot, "package.json"), "utf8"));
|
|
1143
|
+
if (typeof manifest.name !== "string" || typeof manifest.version !== "string")
|
|
1144
|
+
throw new Error("package.json must contain string name and version fields");
|
|
1145
|
+
const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
1146
|
+
const defaults = new Set(DEFAULT_PACKAGED_SKILL_NAMES);
|
|
1147
|
+
const skills = [];
|
|
1148
|
+
for (const skill of packagedSkills) {
|
|
1149
|
+
const target = join2(targetRoot, skill.name);
|
|
1150
|
+
const targetStatus = inspectInventoryTarget(target);
|
|
1151
|
+
skills.push({
|
|
1152
|
+
name: skill.name,
|
|
1153
|
+
kind: defaults.has(skill.name) ? "default" : "optional",
|
|
1154
|
+
status: targetStatus === "missing" ? "missing" : targetStatus === "divergent" ? isCanonicalSourceProjection(skill.root, target) ? "unchanged" : "divergent" : await treesEqual(skill, target) ? "unchanged" : "divergent"
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
const retired = RETIRED_PACKAGED_SKILL_NAMES.filter((name) => inspectInventoryTarget(join2(targetRoot, name)) !== "missing");
|
|
1158
|
+
return {
|
|
1159
|
+
retired,
|
|
1160
|
+
package: { name: manifest.name, version: manifest.version },
|
|
1161
|
+
target: relative(projectRoot, targetRoot) || ".",
|
|
1162
|
+
skills
|
|
1163
|
+
};
|
|
2143
1164
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
1165
|
+
async function copySkillTree(source, targetRoot) {
|
|
1166
|
+
await mkdir(targetRoot, { recursive: true });
|
|
1167
|
+
for (const path of source.files) {
|
|
1168
|
+
const sourcePath = join2(source.root, path);
|
|
1169
|
+
const targetPath = join2(targetRoot, path);
|
|
1170
|
+
const stats = lstatSync(sourcePath);
|
|
1171
|
+
if (!stats.isFile())
|
|
1172
|
+
throw unsupportedEntry(sourcePath, `packaged Skill ${source.name}`);
|
|
1173
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
1174
|
+
await copyFile(sourcePath, targetPath);
|
|
1175
|
+
await chmod(targetPath, stats.mode & 511);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
async function updatePackagedSkills(options = {}) {
|
|
1179
|
+
const packageRoot = options.packageRoot ?? PKG_ROOT;
|
|
1180
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
1181
|
+
const renamePath = options.renamePath ?? rename;
|
|
1182
|
+
const packaged = await inspectPackagedSkills(packageRoot);
|
|
1183
|
+
const skills = DEFAULT_PACKAGED_SKILL_NAMES.map((name) => {
|
|
1184
|
+
const skill = packaged.find((candidate) => candidate.name === name);
|
|
1185
|
+
if (!skill)
|
|
1186
|
+
throw new Error(`Packaged Skill missing: ${name}`);
|
|
1187
|
+
return skill;
|
|
1188
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
1189
|
+
const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
1190
|
+
const result = { installed: [], removed: [], unchanged: [], replaced: [] };
|
|
1191
|
+
for (const skill of skills) {
|
|
1192
|
+
const target = join2(targetRoot, skill.name);
|
|
1193
|
+
if (isCanonicalSourceProjection(skill.root, target))
|
|
1194
|
+
result.unchanged.push(skill.name);
|
|
1195
|
+
else if (inspectTarget(target, skill.name) === "missing")
|
|
1196
|
+
result.installed.push(skill.name);
|
|
1197
|
+
else if (await treesEqual(skill, target))
|
|
1198
|
+
result.unchanged.push(skill.name);
|
|
1199
|
+
else
|
|
1200
|
+
result.replaced.push(skill.name);
|
|
2165
1201
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
throw new Error(decisionRecordsFilesystemIssue);
|
|
2182
|
-
const specsMigrationPlan = await inspectGeneratedSpecsIndexMigration();
|
|
2183
|
-
if (!specsMigrationPlan.inspectionComplete || specsMigrationPlan.ownerControlled.length > 0) {
|
|
2184
|
-
const ownerMessage = specsMigrationPlan.ownerControlled.length > 0 ? ` Owner-controlled reserved path(s): ${specsMigrationPlan.ownerControlled.join(", ")}.` : "";
|
|
2185
|
-
const diagnosticMessage = specsMigrationPlan.diagnostics.length > 0 ? ` ${specsMigrationPlan.diagnostics.join("; ")}` : "";
|
|
2186
|
-
throw new Error(`Generated Specs-index migration requires owner review before update.${ownerMessage}${diagnosticMessage}`);
|
|
1202
|
+
for (const name of RETIRED_PACKAGED_SKILL_NAMES) {
|
|
1203
|
+
if (inspectTarget(join2(targetRoot, name), name) !== "missing")
|
|
1204
|
+
result.removed.push(name);
|
|
1205
|
+
}
|
|
1206
|
+
if (options.dryRun || result.installed.length + result.replaced.length + result.removed.length === 0)
|
|
1207
|
+
return result;
|
|
1208
|
+
await mkdir(targetRoot, { recursive: true });
|
|
1209
|
+
resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
1210
|
+
const staging = await mkdtemp(join2(targetRoot, ".rsp-skills-"));
|
|
1211
|
+
const completed = [];
|
|
1212
|
+
let failure2;
|
|
1213
|
+
try {
|
|
1214
|
+
for (const skill of skills) {
|
|
1215
|
+
if (!result.unchanged.includes(skill.name))
|
|
1216
|
+
await copySkillTree(skill, join2(staging, skill.name));
|
|
2187
1217
|
}
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
const configFileInspection = inspectManagedFile(CONFIG_PATH, "config file", { allowMissing: true });
|
|
2194
|
-
if (configFileInspection.issue)
|
|
2195
|
-
throw configFileInspection.issue;
|
|
2196
|
-
const configContent = configFileInspection.exists ? await readFile4(CONFIG_PATH, "utf-8") : null;
|
|
2197
|
-
const reconciledConfig = configContent === null ? { content: generateConfigTemplate(), added: ["all defaults"], changed: true } : reconcileRspConfigDefaults(configContent);
|
|
2198
|
-
if (reconciledConfig.changed) {
|
|
2199
|
-
await rollbackJournal.capture(CONFIG_PATH);
|
|
2200
|
-
await writeManagedFile(CONFIG_PATH, reconciledConfig.content, "config file");
|
|
2201
|
-
await rollbackJournal.mark(CONFIG_PATH);
|
|
2202
|
-
const detail = reconciledConfig.added.length > 0 ? `defaults added: ${reconciledConfig.added.join(", ")}` : "generated layout normalized";
|
|
2203
|
-
actions.push(`config.yaml ${detail}`);
|
|
2204
|
-
events.push({ type: "updated", message: `config.yaml ${detail}` });
|
|
2205
|
-
updated = true;
|
|
2206
|
-
}
|
|
2207
|
-
const changesInspection = inspectManagedDirectory(CHANGES_DIR, "open work root", { allowMissing: true });
|
|
2208
|
-
if (changesInspection.issue)
|
|
2209
|
-
throw changesInspection.issue;
|
|
2210
|
-
if (!changesInspection.exists) {
|
|
2211
|
-
const changesDirectoryChain = await rollbackJournal.captureDirectoryChain(CHANGES_DIR);
|
|
2212
|
-
const changesPlaceholder = join3(CHANGES_DIR, ".gitkeep");
|
|
2213
|
-
await rollbackJournal.capture(changesPlaceholder);
|
|
2214
|
-
await mkdir3(CHANGES_DIR, { recursive: true });
|
|
2215
|
-
await ensureManagedFile(changesPlaceholder, "", "changes placeholder");
|
|
2216
|
-
await rollbackJournal.mark(changesPlaceholder);
|
|
2217
|
-
await rollbackJournal.markAll(changesDirectoryChain);
|
|
2218
|
-
actions.push("changes/ directory restored");
|
|
2219
|
-
events.push({ type: "updated", message: "changes/ directory restored" });
|
|
2220
|
-
updated = true;
|
|
2221
|
-
}
|
|
2222
|
-
const archivesInspection = inspectManagedDirectory(join3(RSP_DIR, "archives"), "archive root", { allowMissing: true });
|
|
2223
|
-
if (archivesInspection.issue)
|
|
2224
|
-
throw archivesInspection.issue;
|
|
2225
|
-
const bundledRules = await readFile4(join3(options.testing?.packageRoot ?? PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
2226
|
-
const rulesInspection = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
2227
|
-
if (rulesInspection.issue)
|
|
2228
|
-
throw rulesInspection.issue;
|
|
2229
|
-
const existingRules = rulesInspection.exists ? await readFile4(RSP_RULES_PATH, "utf-8") : null;
|
|
2230
|
-
if (existingRules !== bundledRules) {
|
|
2231
|
-
await rollbackJournal.capture(RSP_RULES_PATH);
|
|
2232
|
-
await writeManagedFile(RSP_RULES_PATH, bundledRules, "fallback protocol");
|
|
2233
|
-
await rollbackJournal.mark(RSP_RULES_PATH);
|
|
2234
|
-
actions.push("rsp-rules.md updated");
|
|
2235
|
-
events.push({ type: "updated", message: "rsp-rules.md updated" });
|
|
2236
|
-
updated = true;
|
|
2237
|
-
}
|
|
2238
|
-
if (existsSync(OBSOLETE_RSP_RULES_PATH)) {
|
|
2239
|
-
await rollbackJournal.capture(OBSOLETE_RSP_RULES_PATH);
|
|
2240
|
-
await rm(OBSOLETE_RSP_RULES_PATH);
|
|
2241
|
-
await rollbackJournal.mark(OBSOLETE_RSP_RULES_PATH);
|
|
2242
|
-
actions.push("obsolete rules/rsp-rules.md removed");
|
|
2243
|
-
events.push({ type: "updated", message: "obsolete rules/rsp-rules.md removed" });
|
|
2244
|
-
updated = true;
|
|
2245
|
-
}
|
|
2246
|
-
try {
|
|
2247
|
-
const rulesRoot = join3(RSP_DIR, "rules");
|
|
2248
|
-
await rollbackJournal.capture(rulesRoot);
|
|
2249
|
-
await rmdir2(rulesRoot);
|
|
2250
|
-
await rollbackJournal.mark(rulesRoot);
|
|
2251
|
-
actions.push("empty rules directory removed");
|
|
2252
|
-
updated = true;
|
|
2253
|
-
} catch (error) {
|
|
2254
|
-
if (!["ENOENT", "ENOTEMPTY"].includes(error.code || ""))
|
|
2255
|
-
throw error;
|
|
2256
|
-
}
|
|
2257
|
-
let inspection = await inspectUnsupportedRules();
|
|
2258
|
-
if (inspection.diagnostics.length === 0 && inspection.directoryRemaining && inspection.entries.every((entry) => entry.kind === "directory")) {
|
|
2259
|
-
const directories = inspection.entries.map((entry) => entry.path).sort((left, right) => right.split("/").length - left.split("/").length);
|
|
2260
|
-
for (const path of directories) {
|
|
2261
|
-
const directoryPath = join3(RSP_DIR, "rules", path);
|
|
2262
|
-
await rollbackJournal.capture(directoryPath);
|
|
2263
|
-
await rmdir2(directoryPath);
|
|
2264
|
-
await rollbackJournal.mark(directoryPath);
|
|
2265
|
-
}
|
|
2266
|
-
const rulesRoot = join3(RSP_DIR, "rules");
|
|
2267
|
-
await rollbackJournal.capture(rulesRoot);
|
|
2268
|
-
await rmdir2(rulesRoot);
|
|
2269
|
-
await rollbackJournal.mark(rulesRoot);
|
|
2270
|
-
actions.push("empty rules directory tree removed");
|
|
2271
|
-
events.push({ type: "updated", message: "empty rules directory tree removed" });
|
|
2272
|
-
updated = true;
|
|
2273
|
-
inspection = await inspectUnsupportedRules();
|
|
2274
|
-
}
|
|
2275
|
-
const residualEntries = inspection.entries.map((entry) => entry.path);
|
|
2276
|
-
if (inspection.diagnostics.length > 0)
|
|
2277
|
-
events.push({ type: "migration-diagnostics", diagnostics: inspection.diagnostics });
|
|
2278
|
-
if (residualEntries.length > 0)
|
|
2279
|
-
events.push({ type: "residual-rules", entries: residualEntries });
|
|
2280
|
-
const projectName = await detectProjectName();
|
|
2281
|
-
const baseAgents = agentsInspection.exists ? await readFile4(agentsPath, "utf-8") : `# ${projectName}
|
|
2282
|
-
|
|
2283
|
-
`;
|
|
2284
|
-
const nextAgents = upsertRspAgentsBlock(baseAgents);
|
|
2285
|
-
if (!agentsInspection.exists || nextAgents.changed) {
|
|
2286
|
-
await rollbackJournal.capture(agentsPath);
|
|
2287
|
-
await writeManagedFile(agentsPath, nextAgents.content, "AGENTS.md");
|
|
2288
|
-
await rollbackJournal.mark(agentsPath);
|
|
2289
|
-
actions.push("AGENTS.md managed block refreshed");
|
|
2290
|
-
events.push({ type: "updated", message: "AGENTS.md managed block refreshed" });
|
|
2291
|
-
updated = true;
|
|
2292
|
-
}
|
|
2293
|
-
const decisionRecordsDirectoryChain = await rollbackJournal.captureDirectoryChain(decisionRecordsPath);
|
|
2294
|
-
const decisionRecordsPlaceholder = join3(decisionRecordsPath, ".gitkeep");
|
|
2295
|
-
await rollbackJournal.capture(decisionRecordsPlaceholder);
|
|
2296
|
-
if (await ensureDecisionRecordsDirectory(decisionRecordsPath)) {
|
|
2297
|
-
await rollbackJournal.mark(decisionRecordsPlaceholder);
|
|
2298
|
-
await rollbackJournal.markAll(decisionRecordsDirectoryChain);
|
|
2299
|
-
actions.push(`Decision Record directory ensured: ${decisionRecordsPath}`);
|
|
2300
|
-
events.push({ type: "updated", message: `Decision Record directory ensured: ${decisionRecordsPath}` });
|
|
2301
|
-
updated = true;
|
|
2302
|
-
}
|
|
2303
|
-
const legacyArchiveIndexPath = join3(RSP_DIR, "archives", "INDEX.md");
|
|
2304
|
-
await rollbackJournal.capture(legacyArchiveIndexPath);
|
|
2305
|
-
if (await removeLegacyArchiveIndex()) {
|
|
2306
|
-
await rollbackJournal.mark(legacyArchiveIndexPath);
|
|
2307
|
-
actions.push("legacy Archive Index removed");
|
|
2308
|
-
events.push({ type: "updated", message: "legacy Archive Index removed" });
|
|
2309
|
-
updated = true;
|
|
2310
|
-
}
|
|
2311
|
-
for (const path of specsMigrationPlan.removable)
|
|
2312
|
-
await rollbackJournal.capture(path);
|
|
2313
|
-
await options.testing?.afterPreMutations?.();
|
|
2314
|
-
const removedSpecsIndexes = await removeRecognizedGeneratedSpecsIndexes();
|
|
2315
|
-
for (const path of specsMigrationPlan.removable)
|
|
2316
|
-
await rollbackJournal.mark(path);
|
|
2317
|
-
await options.testing?.afterSpecsMigration?.();
|
|
2318
|
-
if (removedSpecsIndexes.length > 0) {
|
|
2319
|
-
actions.push(`generated Specs indexes removed: ${removedSpecsIndexes.join(", ")}`);
|
|
2320
|
-
events.push({ type: "specs-indexes-removed", paths: removedSpecsIndexes });
|
|
2321
|
-
updated = true;
|
|
2322
|
-
}
|
|
2323
|
-
return {
|
|
2324
|
-
ok: inspection.diagnostics.length === 0,
|
|
2325
|
-
actions,
|
|
2326
|
-
updated,
|
|
2327
|
-
events,
|
|
2328
|
-
migration: {
|
|
2329
|
-
inspectionComplete: inspection.diagnostics.length === 0,
|
|
2330
|
-
manualMigrationRequired: inspection.directoryRemaining,
|
|
2331
|
-
residualEntries,
|
|
2332
|
-
diagnostics: inspection.diagnostics
|
|
2333
|
-
}
|
|
2334
|
-
};
|
|
2335
|
-
} catch (error) {
|
|
2336
|
-
const rollback = await rollbackJournal.rollback();
|
|
2337
|
-
clearConfigCache();
|
|
2338
|
-
if (rollback.retainedMutations.length > 0 || rollback.recoveryPaths.length > 0) {
|
|
2339
|
-
throw new UpdateTransactionError(
|
|
2340
|
-
`RSP update failed and could not fully restore its command-scoped mutations: ${error instanceof Error ? error.message : String(error)}`,
|
|
2341
|
-
actions,
|
|
2342
|
-
rollback,
|
|
2343
|
-
{ cause: error }
|
|
2344
|
-
);
|
|
2345
|
-
}
|
|
2346
|
-
throw error;
|
|
1218
|
+
for (const name of result.removed) {
|
|
1219
|
+
resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
1220
|
+
inspectTarget(join2(targetRoot, name), name);
|
|
1221
|
+
await rm(join2(targetRoot, name), { recursive: true, force: true });
|
|
1222
|
+
completed.push(`removed ${name}`);
|
|
2347
1223
|
}
|
|
2348
|
-
|
|
1224
|
+
for (const skill of skills) {
|
|
1225
|
+
if (result.unchanged.includes(skill.name))
|
|
1226
|
+
continue;
|
|
1227
|
+
resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
1228
|
+
const target = join2(targetRoot, skill.name);
|
|
1229
|
+
inspectTarget(target, skill.name);
|
|
1230
|
+
await rm(target, { recursive: true, force: true });
|
|
1231
|
+
await renamePath(join2(staging, skill.name), target);
|
|
1232
|
+
completed.push(`installed ${skill.name}`);
|
|
1233
|
+
}
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
failure2 = new Error(`Skill update stopped; completed: ${completed.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
await rm(staging, { recursive: true });
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
failure2 = new Error(`Temporary Skill content cleanup failed: ${staging}`, { cause: failure2 ?? error });
|
|
1241
|
+
}
|
|
1242
|
+
if (failure2)
|
|
1243
|
+
throw failure2;
|
|
1244
|
+
return result;
|
|
2349
1245
|
}
|
|
2350
1246
|
|
|
2351
1247
|
// src/commands/doctor.ts
|
|
2352
1248
|
async function runDoctor(options = {}) {
|
|
2353
|
-
const fixed = [];
|
|
2354
|
-
let repairIssue = null;
|
|
2355
|
-
if (options.fix && existsSync2(RSP_DIR)) {
|
|
2356
|
-
try {
|
|
2357
|
-
const update = await updateProject({ testing: options.testing });
|
|
2358
|
-
fixed.push(...update.actions);
|
|
2359
|
-
} catch (error) {
|
|
2360
|
-
if (error instanceof UpdateTransactionError) {
|
|
2361
|
-
fixed.push(
|
|
2362
|
-
...error.rollback.retainedMutations.map((path) => `partial update mutation retained: ${path}`),
|
|
2363
|
-
...error.rollback.recoveryPaths.map((path) => `update recovery copy retained: ${path}`)
|
|
2364
|
-
);
|
|
2365
|
-
}
|
|
2366
|
-
repairIssue = `safe deterministic repair could not run: ${toErrorMessage(error)}`;
|
|
2367
|
-
}
|
|
2368
|
-
}
|
|
2369
1249
|
const checks = [];
|
|
2370
|
-
|
|
1250
|
+
const runtime = [];
|
|
1251
|
+
const reportRuntime = (diagnostic) => runtime.push(diagnostic);
|
|
1252
|
+
reportCheck(checks, ".rsp exists", existsSync(RSP_DIR), "Run: rsp init");
|
|
1253
|
+
reportRetiredProtocol(checks);
|
|
1254
|
+
const sourceMode = await reportProjectRuntime(checks);
|
|
1255
|
+
if (options.checkUpdates) {
|
|
1256
|
+
const notice = await checkForUpdates(await getVersion());
|
|
2371
1257
|
checks.push({
|
|
2372
|
-
status: "
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
1258
|
+
status: "info",
|
|
1259
|
+
code: `update_${notice.status}`,
|
|
1260
|
+
label: "stable package update check",
|
|
1261
|
+
message: notice.status === "available" ? `RSP ${notice.latest} is available; running ${notice.current}. Project selection is unchanged.` : notice.status === "current" ? `No newer stable package found for ${notice.current}.` : "Update metadata is unavailable; local checks are unaffected.",
|
|
1262
|
+
hint: sourceMode ? "Stable package metadata does not establish source freshness. Keep using the local source CLI; this notice does not select a package runtime." : notice.status === "available" ? "Inspect migration guidance and choose an exact target. Upgrade notices authorize no update." : void 0
|
|
2376
1263
|
});
|
|
2377
1264
|
}
|
|
2378
|
-
const runtime = [];
|
|
2379
|
-
const reportRuntime = (diagnostic) => runtime.push(diagnostic);
|
|
2380
|
-
const designPath = join4(RSP_DIR, "specs", "design.md");
|
|
2381
|
-
reportCheck(checks, ".rsp exists", existsSync2(RSP_DIR), "Run: rsp init");
|
|
2382
|
-
reportFallbackProtocol(checks);
|
|
2383
1265
|
await reportUnsupportedRules(checks, reportRuntime);
|
|
2384
|
-
|
|
1266
|
+
const initialization = inspectRspInitialization();
|
|
1267
|
+
checks.push({ status: initialization ? "issue" : "ok", label: "specs/design.md exists", message: initialization });
|
|
2385
1268
|
await checkAgents(checks, reportRuntime);
|
|
1269
|
+
if (existsSync("CONTEXT-MAP.md")) {
|
|
1270
|
+
checks.push({
|
|
1271
|
+
status: "info",
|
|
1272
|
+
code: "legacy_context_map",
|
|
1273
|
+
label: "legacy root context map detected",
|
|
1274
|
+
message: "CONTEXT-MAP.md remains; its semantic migration requires owner review.",
|
|
1275
|
+
hint: "Use Core context migration. Retain relevant legacy context until an authorized merge updates CONTEXT.md and active references; update does not migrate or delete it."
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
2386
1278
|
await checkSpecsTree(checks, reportRuntime);
|
|
2387
1279
|
await checkArchiveNaming(checks);
|
|
2388
1280
|
const decisionRecordsConfigValid = await checkConfigSemantics(checks, reportRuntime);
|
|
@@ -2394,7 +1286,6 @@ async function runDoctor(options = {}) {
|
|
|
2394
1286
|
const result = {
|
|
2395
1287
|
command: "doctor",
|
|
2396
1288
|
ok: checks.every((check) => check.status !== "issue"),
|
|
2397
|
-
fixed,
|
|
2398
1289
|
checks,
|
|
2399
1290
|
runtime,
|
|
2400
1291
|
summary: {
|
|
@@ -2403,32 +1294,52 @@ async function runDoctor(options = {}) {
|
|
|
2403
1294
|
};
|
|
2404
1295
|
return result;
|
|
2405
1296
|
}
|
|
2406
|
-
function
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
label: "rsp-rules.md is a regular managed file",
|
|
2413
|
-
message: canonical.issue.message,
|
|
2414
|
-
hint: "Replace the unsupported entry with a project-local regular file, then run: rsp update"
|
|
2415
|
-
});
|
|
2416
|
-
} else if (canonical.exists) {
|
|
2417
|
-
checks.push({ status: "ok", label: "rsp-rules.md exists" });
|
|
2418
|
-
} else if (!obsoleteExists) {
|
|
1297
|
+
async function reportProjectRuntime(checks) {
|
|
1298
|
+
let sourceMode = false;
|
|
1299
|
+
try {
|
|
1300
|
+
const selected = await readProjectRuntime();
|
|
1301
|
+
sourceMode = selected.mode === "source";
|
|
1302
|
+
const running = await getVersion();
|
|
2419
1303
|
checks.push({
|
|
2420
|
-
status: "issue",
|
|
2421
|
-
|
|
2422
|
-
|
|
1304
|
+
status: selected.mode === "source" || selected.version === running ? "ok" : "issue",
|
|
1305
|
+
code: "project_cli_version",
|
|
1306
|
+
label: "project CLI runtime",
|
|
1307
|
+
message: selected.mode === "source" ? `Source mode: this checkout's local CLI; package metadata ${running} is not source identity.` : selected.version ? `Project pins ${selected.version}; running ${running}.` : "Project has no exact CLI pin.",
|
|
1308
|
+
hint: selected.mode === "source" ? `Build current source, then use ${SOURCE_INVOCATION} <command>. Retain commit, uncommitted diff and build inputs for candidate evidence.` : selected.version ? `Use ${exactInvocation(selected.version)} <command>; only an authorized migration uses update.` : "Choose an exact CLI version, then run its update command."
|
|
2423
1309
|
});
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
checks.push({ status: "issue", code: "project_cli_version", label: "project CLI runtime", message: toErrorMessage(error) });
|
|
2424
1312
|
}
|
|
2425
|
-
|
|
1313
|
+
try {
|
|
1314
|
+
const inventory = await inspectPackagedSkillInventory();
|
|
1315
|
+
const drift = [
|
|
1316
|
+
...inventory.skills.filter((skill) => skill.status !== "unchanged").map((skill) => `${skill.name}: ${skill.status}`),
|
|
1317
|
+
...inventory.retired.map((name) => `${name}: retired`)
|
|
1318
|
+
];
|
|
2426
1319
|
checks.push({
|
|
2427
|
-
status: "issue",
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
1320
|
+
status: drift.length === 0 ? "ok" : "issue",
|
|
1321
|
+
code: "project_skill_suite",
|
|
1322
|
+
label: "project Skill suite matches running package",
|
|
1323
|
+
message: drift.length > 0 ? drift.join(", ") : void 0,
|
|
1324
|
+
hint: drift.length > 0 ? "Preserve needed custom content in managed Skills, then run the selected exact CLI: update." : void 0
|
|
2431
1325
|
});
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
checks.push({ status: "issue", code: "project_skill_suite", label: "project Skill suite is inspectable", message: toErrorMessage(error) });
|
|
1328
|
+
}
|
|
1329
|
+
return sourceMode;
|
|
1330
|
+
}
|
|
1331
|
+
function reportRetiredProtocol(checks) {
|
|
1332
|
+
for (const path of [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH]) {
|
|
1333
|
+
const inspection = inspectManagedFile(path, "retired Rules file", { allowMissing: true });
|
|
1334
|
+
if (inspection.issue || inspection.exists) {
|
|
1335
|
+
checks.push({
|
|
1336
|
+
status: "issue",
|
|
1337
|
+
code: "retired_rules",
|
|
1338
|
+
label: `retired Rules path: ${path}`,
|
|
1339
|
+
message: inspection.issue?.message ?? "Rules are no longer read or generated.",
|
|
1340
|
+
hint: "Preserve needed custom content, then run update. Symlinks and special files require owner review."
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
2432
1343
|
}
|
|
2433
1344
|
}
|
|
2434
1345
|
async function reportUnsupportedRules(checks, reportRuntime) {
|
|
@@ -2468,7 +1379,7 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
2468
1379
|
status: "issue",
|
|
2469
1380
|
label: "AGENTS.md is a regular managed file",
|
|
2470
1381
|
message: inspection.issue.message,
|
|
2471
|
-
hint: "
|
|
1382
|
+
hint: "Check AGENTS.md path type and permissions; preserve user content before correcting the entry. Then run: rsp update"
|
|
2472
1383
|
});
|
|
2473
1384
|
return;
|
|
2474
1385
|
}
|
|
@@ -2478,7 +1389,7 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
2478
1389
|
}
|
|
2479
1390
|
let agents;
|
|
2480
1391
|
try {
|
|
2481
|
-
agents = await
|
|
1392
|
+
agents = await readFile4("AGENTS.md", "utf-8");
|
|
2482
1393
|
} catch (error) {
|
|
2483
1394
|
reportRuntime({
|
|
2484
1395
|
code: "agents_read_failed",
|
|
@@ -2486,7 +1397,7 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
2486
1397
|
path: "AGENTS.md",
|
|
2487
1398
|
message: toErrorMessage(error)
|
|
2488
1399
|
});
|
|
2489
|
-
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md could not be read", hint: "Check AGENTS.md permissions
|
|
1400
|
+
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md could not be read", hint: "Check AGENTS.md permissions and filesystem access; init/update do not repair unreadable user content." });
|
|
2490
1401
|
return;
|
|
2491
1402
|
}
|
|
2492
1403
|
if (hasRspAgentsBlock(agents)) {
|
|
@@ -2537,7 +1448,7 @@ async function checkSpecsTree(checks, reportRuntime) {
|
|
|
2537
1448
|
}
|
|
2538
1449
|
}
|
|
2539
1450
|
async function checkArchiveNaming(checks) {
|
|
2540
|
-
const archivesDir =
|
|
1451
|
+
const archivesDir = join3(RSP_DIR, "archives");
|
|
2541
1452
|
const inspection = await inspectArchiveTree({ archivesDir });
|
|
2542
1453
|
if (inspection.diagnostics.length > 0) {
|
|
2543
1454
|
checks.push({
|
|
@@ -2549,7 +1460,7 @@ async function checkArchiveNaming(checks) {
|
|
|
2549
1460
|
return;
|
|
2550
1461
|
}
|
|
2551
1462
|
checks.push({ status: "ok", label: "archives use supported managed paths" });
|
|
2552
|
-
const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(
|
|
1463
|
+
const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(relative2(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename(fp)) === false);
|
|
2553
1464
|
if (invalidFiles.length === 0) {
|
|
2554
1465
|
checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
|
|
2555
1466
|
return;
|
|
@@ -2558,7 +1469,7 @@ async function checkArchiveNaming(checks) {
|
|
|
2558
1469
|
status: "issue",
|
|
2559
1470
|
label: "archived change files follow the date_name.md convention",
|
|
2560
1471
|
message: `archived change files with invalid names: ${invalidFiles.join(", ")}`,
|
|
2561
|
-
hint: "Rename archive files to YYYY-MM-DD_name.md
|
|
1472
|
+
hint: "Rename archive files to YYYY-MM-DD_name.md, then rerun rsp doctor"
|
|
2562
1473
|
});
|
|
2563
1474
|
}
|
|
2564
1475
|
async function checkConfigSemantics(checks, reportRuntime) {
|
|
@@ -2605,7 +1516,7 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
|
|
|
2605
1516
|
status: "issue",
|
|
2606
1517
|
label: `${decisionRecordsPath} is the authoritative Decision Record directory`,
|
|
2607
1518
|
message: filesystemError,
|
|
2608
|
-
hint: "
|
|
1519
|
+
hint: "Correct decisions.path or its filesystem entry; it must resolve to a project-local directory. Preserve existing user content."
|
|
2609
1520
|
});
|
|
2610
1521
|
return;
|
|
2611
1522
|
}
|
|
@@ -2616,10 +1527,12 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
|
|
|
2616
1527
|
checks,
|
|
2617
1528
|
`${decisionRecordsPath} is the authoritative Decision Record directory`,
|
|
2618
1529
|
info.isDirectory(),
|
|
2619
|
-
"
|
|
1530
|
+
"Correct the path type; init/update do not replace existing user content."
|
|
2620
1531
|
);
|
|
2621
1532
|
} catch (error) {
|
|
2622
|
-
|
|
1533
|
+
const code = error.code;
|
|
1534
|
+
const missing = code === "ENOENT";
|
|
1535
|
+
if (!missing) {
|
|
2623
1536
|
reportRuntime({
|
|
2624
1537
|
code: "decision_records_path_stat_failed",
|
|
2625
1538
|
operation: "stat",
|
|
@@ -2630,14 +1543,14 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
|
|
|
2630
1543
|
checks.push({
|
|
2631
1544
|
status: "issue",
|
|
2632
1545
|
label: `${decisionRecordsPath} is the authoritative Decision Record directory`,
|
|
2633
|
-
message: `${decisionRecordsPath} Decision Record directory is missing
|
|
2634
|
-
hint: "
|
|
1546
|
+
message: missing ? `${decisionRecordsPath} Decision Record directory is missing` : `Unable to inspect Decision Record directory ${decisionRecordsPath}: ${toErrorMessage(error)}`,
|
|
1547
|
+
hint: missing ? "Create the directory yourself, or run rsp init to create missing scaffolding. Update does not create user directories." : code === "EACCES" || code === "EPERM" ? "Check directory and parent permissions; init/update do not repair filesystem access." : "Resolve the filesystem error or correct decisions.path, then rerun rsp doctor."
|
|
2635
1548
|
});
|
|
2636
1549
|
}
|
|
2637
1550
|
}
|
|
2638
1551
|
async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
2639
1552
|
const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
|
|
2640
|
-
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !
|
|
1553
|
+
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync(DEFAULT_DECISION_RECORDS_PATH))
|
|
2641
1554
|
return;
|
|
2642
1555
|
let inspectionFailed = false;
|
|
2643
1556
|
const inactiveRecords = (await walkMarkdownFiles(DEFAULT_DECISION_RECORDS_PATH, {
|
|
@@ -2645,7 +1558,7 @@ async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
|
2645
1558
|
inspectionFailed = true;
|
|
2646
1559
|
reportRuntime(diagnostic);
|
|
2647
1560
|
}
|
|
2648
|
-
})).map((path) => normalizeLogicalPath(
|
|
1561
|
+
})).map((path) => normalizeLogicalPath(relative2(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
|
|
2649
1562
|
if (inspectionFailed) {
|
|
2650
1563
|
checks.push({
|
|
2651
1564
|
status: "issue",
|
|
@@ -2899,7 +1812,7 @@ function emptyCollectionSummary() {
|
|
|
2899
1812
|
}
|
|
2900
1813
|
|
|
2901
1814
|
// src/commands/ready.ts
|
|
2902
|
-
import { readFile as
|
|
1815
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
2903
1816
|
function readyError(name, error, kind) {
|
|
2904
1817
|
return { command: "ready", ok: false, change: name || null, path: null, warnings: [], runtime: [], error, kind };
|
|
2905
1818
|
}
|
|
@@ -2919,7 +1832,7 @@ async function showReady(name) {
|
|
|
2919
1832
|
const runtime = [];
|
|
2920
1833
|
let content;
|
|
2921
1834
|
try {
|
|
2922
|
-
content = await
|
|
1835
|
+
content = await readFile5(srcPath, "utf-8");
|
|
2923
1836
|
} catch {
|
|
2924
1837
|
return readyError(name, { code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, "read");
|
|
2925
1838
|
}
|
|
@@ -2937,9 +1850,10 @@ async function showReady(name) {
|
|
|
2937
1850
|
return readyError(name, { code: "invalid_config", message: `.rsp/config.yaml could not be parsed: ${toErrorMessage(error)}` }, "config");
|
|
2938
1851
|
}
|
|
2939
1852
|
const dependencyInspection = await inspectChangeDependencies();
|
|
2940
|
-
const
|
|
1853
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
1854
|
+
const readinessDetails = collectArchiveReadiness(document, {
|
|
2941
1855
|
activeBlockers: dependencyInspection.activeBlockers.get(name),
|
|
2942
|
-
documentDiagnostics: inspectChangeDocument(
|
|
1856
|
+
documentDiagnostics: inspectChangeDocument(document, {
|
|
2943
1857
|
name,
|
|
2944
1858
|
validKinds: resolveKinds(configInspection.config),
|
|
2945
1859
|
requiredSections: resolveRequiredSections(configInspection.config)
|
|
@@ -2962,7 +1876,7 @@ async function showReady(name) {
|
|
|
2962
1876
|
}
|
|
2963
1877
|
|
|
2964
1878
|
// src/commands/show.ts
|
|
2965
|
-
import { readFile as
|
|
1879
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2966
1880
|
function showError(error, runtime = []) {
|
|
2967
1881
|
const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
|
|
2968
1882
|
return { command: "show", ok: false, change: null, contextPaths: [], runtime, nextActions, error };
|
|
@@ -3006,14 +1920,15 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
3006
1920
|
const srcPath = workRef.path;
|
|
3007
1921
|
let content;
|
|
3008
1922
|
try {
|
|
3009
|
-
content = await
|
|
1923
|
+
content = await readFile6(srcPath, "utf-8");
|
|
3010
1924
|
} catch {
|
|
3011
1925
|
return showError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, runtime);
|
|
3012
1926
|
}
|
|
3013
1927
|
let kind = "\u2014";
|
|
1928
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
3014
1929
|
let issues = [];
|
|
3015
1930
|
try {
|
|
3016
|
-
const fm = parseFrontmatter(
|
|
1931
|
+
const fm = parseFrontmatter(document.markdown);
|
|
3017
1932
|
kind = fm?.kind ? String(fm.kind) : "\u2014";
|
|
3018
1933
|
issues = parseIssueRelationships(fm);
|
|
3019
1934
|
} catch (error) {
|
|
@@ -3034,7 +1949,7 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
3034
1949
|
const markerPath = focusMarkers.get(name);
|
|
3035
1950
|
let markerContent;
|
|
3036
1951
|
try {
|
|
3037
|
-
markerContent = await
|
|
1952
|
+
markerContent = await readFile6(markerPath);
|
|
3038
1953
|
} catch (error) {
|
|
3039
1954
|
reportRuntime({
|
|
3040
1955
|
code: "focus_marker_read_failed",
|
|
@@ -3060,11 +1975,10 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
3060
1975
|
recovery = capsule.recovery;
|
|
3061
1976
|
}
|
|
3062
1977
|
const dependencyInspection = await inspectChangeDependencies();
|
|
3063
|
-
const blockers = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(
|
|
3064
|
-
const scenarios = parseScenarios(
|
|
3065
|
-
const readinessDetails = collectArchiveReadiness(
|
|
3066
|
-
const
|
|
3067
|
-
const taskCheckboxes = countCheckboxes(getDocumentSectionBody(document, "tasks"));
|
|
1978
|
+
const blockers = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(document);
|
|
1979
|
+
const scenarios = parseScenarios(document.markdown);
|
|
1980
|
+
const readinessDetails = collectArchiveReadiness(document, { activeBlockers: blockers });
|
|
1981
|
+
const taskCheckboxes = countCheckboxes(getDocumentSectionMarkdown(document, "tasks"));
|
|
3068
1982
|
const progress = {
|
|
3069
1983
|
done: taskCheckboxes.done + readinessDetails.verifyCriticality.required.done,
|
|
3070
1984
|
total: taskCheckboxes.total + readinessDetails.verifyCriticality.required.total
|
|
@@ -3221,13 +2135,13 @@ function publicQuery(query) {
|
|
|
3221
2135
|
}
|
|
3222
2136
|
|
|
3223
2137
|
// src/status/derive.ts
|
|
3224
|
-
function deriveStatusView(
|
|
2138
|
+
function deriveStatusView(snapshot, options = {}) {
|
|
3225
2139
|
const query = {
|
|
3226
2140
|
focused: Boolean(options.focused),
|
|
3227
2141
|
blocked: Boolean(options.blocked),
|
|
3228
2142
|
stale: typeof options.stale === "number" ? options.stale : null
|
|
3229
2143
|
};
|
|
3230
|
-
const records =
|
|
2144
|
+
const records = snapshot.records.filter(({ output: record }) => {
|
|
3231
2145
|
if (query.focused && !record.isFocused)
|
|
3232
2146
|
return false;
|
|
3233
2147
|
if (query.blocked && !record.isBlocked)
|
|
@@ -3237,26 +2151,26 @@ function deriveStatusView(snapshot2, options = {}) {
|
|
|
3237
2151
|
return true;
|
|
3238
2152
|
});
|
|
3239
2153
|
const filtered = query.focused || query.blocked || query.stale !== null;
|
|
3240
|
-
const ok =
|
|
3241
|
-
const nextActions = ok ? buildStatusNextActions(
|
|
2154
|
+
const ok = snapshot.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
|
|
2155
|
+
const nextActions = ok ? buildStatusNextActions(snapshot.focused.length, snapshot.records.map((record) => record.output), snapshot.groups, filtered) : ["Run: rsp doctor"];
|
|
3242
2156
|
return {
|
|
3243
|
-
|
|
3244
|
-
language:
|
|
2157
|
+
workflow: snapshot.workflow,
|
|
2158
|
+
language: snapshot.language,
|
|
3245
2159
|
query,
|
|
3246
|
-
focused:
|
|
2160
|
+
focused: snapshot.focused,
|
|
3247
2161
|
records,
|
|
3248
|
-
groups:
|
|
3249
|
-
plan: filterDependencyPlan(
|
|
2162
|
+
groups: snapshot.groups,
|
|
2163
|
+
plan: filterDependencyPlan(snapshot.plan, new Set(records.map((record) => record.output.name))),
|
|
3250
2164
|
summary: {
|
|
3251
2165
|
total: records.length,
|
|
3252
2166
|
focused: records.filter((record) => record.output.isFocused).length,
|
|
3253
2167
|
blocked: records.filter((record) => record.output.isBlocked).length
|
|
3254
2168
|
},
|
|
3255
2169
|
nextActions,
|
|
3256
|
-
archiveTrend:
|
|
3257
|
-
diagnostics:
|
|
3258
|
-
runtime:
|
|
3259
|
-
hasExecutableChanges:
|
|
2170
|
+
archiveTrend: snapshot.archiveTrend,
|
|
2171
|
+
diagnostics: snapshot.diagnostics,
|
|
2172
|
+
runtime: snapshot.runtime,
|
|
2173
|
+
hasExecutableChanges: snapshot.records.length > 0,
|
|
3260
2174
|
ok
|
|
3261
2175
|
};
|
|
3262
2176
|
}
|
|
@@ -3304,13 +2218,13 @@ function recommendOpenChange(records, groups) {
|
|
|
3304
2218
|
}
|
|
3305
2219
|
function filterDependencyPlan(plan, names) {
|
|
3306
2220
|
const included = new Set(names);
|
|
3307
|
-
let
|
|
3308
|
-
while (
|
|
3309
|
-
|
|
2221
|
+
let changed = true;
|
|
2222
|
+
while (changed) {
|
|
2223
|
+
changed = false;
|
|
3310
2224
|
for (const edge of plan.edges) {
|
|
3311
2225
|
if (included.has(edge.change) && !included.has(edge.requires)) {
|
|
3312
2226
|
included.add(edge.requires);
|
|
3313
|
-
|
|
2227
|
+
changed = true;
|
|
3314
2228
|
}
|
|
3315
2229
|
}
|
|
3316
2230
|
}
|
|
@@ -3326,46 +2240,49 @@ function filterDependencyPlan(plan, names) {
|
|
|
3326
2240
|
|
|
3327
2241
|
// src/commands/status.ts
|
|
3328
2242
|
async function showStatus(options = {}) {
|
|
3329
|
-
const
|
|
3330
|
-
return deriveStatusView(
|
|
2243
|
+
const snapshot = await inspectProjectStatus();
|
|
2244
|
+
return deriveStatusView(snapshot, options);
|
|
3331
2245
|
}
|
|
3332
2246
|
|
|
3333
2247
|
// src/status/v3-json.ts
|
|
3334
|
-
function
|
|
3335
|
-
return
|
|
2248
|
+
function hasActiveFilters(query) {
|
|
2249
|
+
return query.focused || query.blocked || query.stale !== null;
|
|
2250
|
+
}
|
|
2251
|
+
function toStatusJson(view, options = {}) {
|
|
2252
|
+
const output = {
|
|
3336
2253
|
command: "status",
|
|
3337
2254
|
ok: view.ok,
|
|
3338
|
-
manage: view.manage,
|
|
3339
|
-
language: view.language,
|
|
3340
|
-
filters: view.query,
|
|
3341
2255
|
focused: view.focused,
|
|
3342
2256
|
records: view.records.map((record) => record.output),
|
|
3343
2257
|
groups: view.groups,
|
|
3344
|
-
plan:
|
|
2258
|
+
plan: {
|
|
2259
|
+
nodes: view.plan.nodes,
|
|
2260
|
+
edges: view.plan.edges,
|
|
2261
|
+
blocked: view.plan.blocked,
|
|
2262
|
+
waves: view.plan.waves
|
|
2263
|
+
},
|
|
3345
2264
|
summary: view.summary,
|
|
3346
|
-
|
|
3347
|
-
archiveTrend: view.archiveTrend,
|
|
3348
|
-
diagnostics: view.diagnostics,
|
|
3349
|
-
runtime: view.runtime
|
|
2265
|
+
diagnostics: view.diagnostics
|
|
3350
2266
|
};
|
|
2267
|
+
if (options.verbose || hasActiveFilters(view.query)) {
|
|
2268
|
+
output.filters = view.query;
|
|
2269
|
+
}
|
|
2270
|
+
if (options.verbose) {
|
|
2271
|
+
output.nextActions = view.nextActions;
|
|
2272
|
+
output.archiveTrend = view.archiveTrend;
|
|
2273
|
+
output.runtime = view.runtime;
|
|
2274
|
+
}
|
|
2275
|
+
return output;
|
|
3351
2276
|
}
|
|
3352
2277
|
function toStatusJsonError(error, options) {
|
|
3353
|
-
|
|
2278
|
+
const output = {
|
|
3354
2279
|
command: "status",
|
|
3355
2280
|
ok: false,
|
|
3356
|
-
manage: { activation: "explicit", closeout: "manual" },
|
|
3357
|
-
language: { artifacts: null, commit: null },
|
|
3358
|
-
filters: {
|
|
3359
|
-
focused: options.focused,
|
|
3360
|
-
blocked: options.blocked,
|
|
3361
|
-
stale: null
|
|
3362
|
-
},
|
|
3363
2281
|
focused: [],
|
|
3364
2282
|
records: [],
|
|
3365
2283
|
groups: [],
|
|
3366
2284
|
plan: {
|
|
3367
2285
|
nodes: [],
|
|
3368
|
-
ready: [],
|
|
3369
2286
|
edges: [],
|
|
3370
2287
|
blocked: [],
|
|
3371
2288
|
waves: []
|
|
@@ -3375,12 +2292,22 @@ function toStatusJsonError(error, options) {
|
|
|
3375
2292
|
focused: 0,
|
|
3376
2293
|
blocked: 0
|
|
3377
2294
|
},
|
|
3378
|
-
archiveTrend: [],
|
|
3379
|
-
nextActions: [],
|
|
3380
2295
|
diagnostics: [],
|
|
3381
|
-
runtime: [],
|
|
3382
2296
|
error
|
|
3383
2297
|
};
|
|
2298
|
+
if (options.verbose || options.focused || options.blocked) {
|
|
2299
|
+
output.filters = {
|
|
2300
|
+
focused: options.focused,
|
|
2301
|
+
blocked: options.blocked,
|
|
2302
|
+
stale: null
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
if (options.verbose) {
|
|
2306
|
+
output.archiveTrend = [];
|
|
2307
|
+
output.nextActions = [];
|
|
2308
|
+
output.runtime = [];
|
|
2309
|
+
}
|
|
2310
|
+
return output;
|
|
3384
2311
|
}
|
|
3385
2312
|
|
|
3386
2313
|
// src/cli/adapter.ts
|
|
@@ -3393,9 +2320,6 @@ async function executeCliCommand(adapter, args) {
|
|
|
3393
2320
|
return result;
|
|
3394
2321
|
}
|
|
3395
2322
|
|
|
3396
|
-
// src/cli/presenters/inspection.ts
|
|
3397
|
-
import { existsSync as existsSync3 } from "fs";
|
|
3398
|
-
|
|
3399
2323
|
// src/status/plain.ts
|
|
3400
2324
|
var COL_CHANGE = 32;
|
|
3401
2325
|
var COL_KIND = 10;
|
|
@@ -3409,10 +2333,6 @@ function printStatusPlain(view, options = {}) {
|
|
|
3409
2333
|
console.log();
|
|
3410
2334
|
console.log(` ${pc.bold("RSP status")}`);
|
|
3411
2335
|
console.log();
|
|
3412
|
-
if (options.verbose)
|
|
3413
|
-
console.log(` ${pc.bold("Manage:")} activation ${view.manage.activation} \xB7 closeout ${view.manage.closeout}`);
|
|
3414
|
-
if (options.verbose && (view.language.artifacts !== null || view.language.commit !== null))
|
|
3415
|
-
console.log(` ${pc.bold("Language:")} artifacts ${view.language.artifacts ?? "unset"} \xB7 commit ${view.language.commit ?? "unset"}`);
|
|
3416
2336
|
if (options.verbose)
|
|
3417
2337
|
console.log();
|
|
3418
2338
|
for (const diagnostic of view.diagnostics) {
|
|
@@ -3542,6 +2462,25 @@ function printArchiveTrend(trend) {
|
|
|
3542
2462
|
}
|
|
3543
2463
|
|
|
3544
2464
|
// src/cli/presenters/inspection.ts
|
|
2465
|
+
function presentConfig(result, options) {
|
|
2466
|
+
if (options.json) {
|
|
2467
|
+
emitJson(result.ok ? result.summary : result, options);
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
if (!result.ok) {
|
|
2471
|
+
console.error(` ${pc.red("Error:")} ${result.diagnostics.map((diagnostic) => diagnostic.message).join("; ")}`);
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
console.log();
|
|
2475
|
+
console.log(` ${pc.bold("RSP config")}`);
|
|
2476
|
+
console.log(` ${pc.dim("Path:")} ${result.path}`);
|
|
2477
|
+
console.log(` ${pc.dim("Valid:")} ${pc.green("yes")}`);
|
|
2478
|
+
console.log(` ${pc.dim("Kinds:")} ${result.summary.kinds.join(", ")}`);
|
|
2479
|
+
console.log(` ${pc.dim("Decision Records:")} ${result.summary.decisions.path}`);
|
|
2480
|
+
console.log(` ${pc.dim("Workflow:")} delegation ${result.summary.workflow.delegation} \xB7 closeout ${result.summary.workflow.closeout}`);
|
|
2481
|
+
console.log(` ${pc.dim("Language:")} artifacts ${result.summary.language.artifacts ?? "unset"} \xB7 commit ${result.summary.language.commit ?? "unset"}`);
|
|
2482
|
+
console.log();
|
|
2483
|
+
}
|
|
3545
2484
|
function presentStatus(result, options) {
|
|
3546
2485
|
if (result.kind === "error") {
|
|
3547
2486
|
if (options.json)
|
|
@@ -3553,7 +2492,7 @@ function presentStatus(result, options) {
|
|
|
3553
2492
|
if (options.verbose && !options.json)
|
|
3554
2493
|
printStatusRuntimeDiagnostics(result.view.runtime);
|
|
3555
2494
|
if (options.json)
|
|
3556
|
-
emitJson(toStatusJson(result.view), options);
|
|
2495
|
+
emitJson(toStatusJson(result.view, { verbose: options.verbose }), options);
|
|
3557
2496
|
else
|
|
3558
2497
|
printStatusPlain(result.view, { verbose: options.verbose });
|
|
3559
2498
|
}
|
|
@@ -3694,13 +2633,6 @@ function presentDoctor(result, options) {
|
|
|
3694
2633
|
console.log();
|
|
3695
2634
|
console.log(` ${pc.bold("RSP doctor")}`);
|
|
3696
2635
|
console.log();
|
|
3697
|
-
if (result.fixed.length > 0) {
|
|
3698
|
-
console.log(` ${pc.green("Fixed:")} ${result.fixed.join(", ")}`);
|
|
3699
|
-
console.log();
|
|
3700
|
-
} else if (options.fix && result.checks.every((check) => check.message?.startsWith("safe deterministic repair could not run:") !== true) && existsSync3(RSP_DIR)) {
|
|
3701
|
-
console.log(` ${pc.dim("No safe fixes needed.")}`);
|
|
3702
|
-
console.log();
|
|
3703
|
-
}
|
|
3704
2636
|
for (const check of result.checks)
|
|
3705
2637
|
printDoctorCheck(check);
|
|
3706
2638
|
if (result.summary.issues === 0)
|
|
@@ -3918,6 +2850,22 @@ function formatArchiveReady(value) {
|
|
|
3918
2850
|
}
|
|
3919
2851
|
|
|
3920
2852
|
// src/cli/commands/inspection.ts
|
|
2853
|
+
var configCommand = defineCommand({
|
|
2854
|
+
meta: {
|
|
2855
|
+
name: "config",
|
|
2856
|
+
description: "Inspect effective project configuration without scanning work state"
|
|
2857
|
+
},
|
|
2858
|
+
args: {
|
|
2859
|
+
...compactJsonArgs
|
|
2860
|
+
},
|
|
2861
|
+
async run({ args }) {
|
|
2862
|
+
await executeCliCommand({
|
|
2863
|
+
execute: () => showConfig(),
|
|
2864
|
+
present: (result) => presentConfig(result, args),
|
|
2865
|
+
exitCode: (result) => result.ok ? void 0 : 1
|
|
2866
|
+
}, args);
|
|
2867
|
+
}
|
|
2868
|
+
});
|
|
3921
2869
|
var specsCommand = defineCommand({
|
|
3922
2870
|
meta: {
|
|
3923
2871
|
name: "specs",
|
|
@@ -3980,7 +2928,7 @@ var statusCommand = defineCommand({
|
|
|
3980
2928
|
...compactJsonArgs,
|
|
3981
2929
|
verbose: {
|
|
3982
2930
|
type: "boolean",
|
|
3983
|
-
description: "
|
|
2931
|
+
description: "Show auxiliary status details and runtime diagnostics",
|
|
3984
2932
|
default: false
|
|
3985
2933
|
}
|
|
3986
2934
|
},
|
|
@@ -3993,7 +2941,8 @@ var statusCommand = defineCommand({
|
|
|
3993
2941
|
kind: "error",
|
|
3994
2942
|
output: toStatusJsonError({ code: "invalid_stale_filter", message: "--stale must be a non-negative integer number of days" }, {
|
|
3995
2943
|
focused: Boolean(args.focused),
|
|
3996
|
-
blocked: Boolean(args.blocked)
|
|
2944
|
+
blocked: Boolean(args.blocked),
|
|
2945
|
+
verbose: Boolean(args.verbose)
|
|
3997
2946
|
})
|
|
3998
2947
|
};
|
|
3999
2948
|
}
|
|
@@ -4037,20 +2986,22 @@ var doctorCommand = defineCommand({
|
|
|
4037
2986
|
},
|
|
4038
2987
|
args: {
|
|
4039
2988
|
...compactJsonArgs,
|
|
4040
|
-
verbose: {
|
|
2989
|
+
"verbose": {
|
|
4041
2990
|
type: "boolean",
|
|
4042
2991
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
4043
2992
|
default: false
|
|
4044
2993
|
},
|
|
4045
|
-
|
|
2994
|
+
"check-updates": {
|
|
4046
2995
|
type: "boolean",
|
|
4047
|
-
description: "
|
|
2996
|
+
description: "Explicitly check stable registry metadata without upgrading; timeout is non-blocking",
|
|
4048
2997
|
default: false
|
|
4049
2998
|
}
|
|
4050
2999
|
},
|
|
4051
|
-
async run({ args }) {
|
|
3000
|
+
async run({ args, rawArgs }) {
|
|
3001
|
+
if (rawArgs.some((arg) => arg === "--fix" || arg.startsWith("--fix=") || arg === "--no-fix"))
|
|
3002
|
+
throw new Error("doctor is read-only; use update to reconcile managed assets");
|
|
4052
3003
|
await executeCliCommand({
|
|
4053
|
-
execute: () => runDoctor({
|
|
3004
|
+
execute: () => runDoctor({ checkUpdates: Boolean(args["check-updates"]) }),
|
|
4054
3005
|
present: (result) => presentDoctor(result, args),
|
|
4055
3006
|
exitCode: (result) => result.ok ? void 0 : 1
|
|
4056
3007
|
}, args);
|
|
@@ -4166,24 +3117,87 @@ var historyCommand = defineCommand({
|
|
|
4166
3117
|
exitCode: (result) => result.ok ? void 0 : 1
|
|
4167
3118
|
}, args);
|
|
4168
3119
|
}
|
|
4169
|
-
});
|
|
4170
|
-
var inspectionCommands = {
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
3120
|
+
});
|
|
3121
|
+
var inspectionCommands = {
|
|
3122
|
+
config: configCommand,
|
|
3123
|
+
specs: specsCommand,
|
|
3124
|
+
ready: readyCommand,
|
|
3125
|
+
show: showCommand,
|
|
3126
|
+
history: historyCommand,
|
|
3127
|
+
status: statusCommand,
|
|
3128
|
+
check: checkCommand,
|
|
3129
|
+
doctor: doctorCommand
|
|
3130
|
+
};
|
|
3131
|
+
|
|
3132
|
+
// src/cli/commands/setup.ts
|
|
3133
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
3134
|
+
|
|
3135
|
+
// src/commands/add-spec.ts
|
|
3136
|
+
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
3137
|
+
import { join as join4 } from "path";
|
|
3138
|
+
|
|
3139
|
+
// src/core/lock.ts
|
|
3140
|
+
import { mkdir as mkdir2, readFile as readFile7, unlink, writeFile } from "fs/promises";
|
|
3141
|
+
async function withRspLock(operation, fn) {
|
|
3142
|
+
requireManagedDirectory(RSP_DIR, "RSP root", { allowMissing: true });
|
|
3143
|
+
await mkdir2(RSP_DIR, { recursive: true });
|
|
3144
|
+
try {
|
|
3145
|
+
await writeFile(LOCK_PATH, `${process.pid}
|
|
3146
|
+
${operation}
|
|
3147
|
+
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
3148
|
+
} catch (err) {
|
|
3149
|
+
if (err.code === "EEXIST")
|
|
3150
|
+
await handleStaleLock(operation);
|
|
3151
|
+
else
|
|
3152
|
+
throw err;
|
|
3153
|
+
}
|
|
3154
|
+
try {
|
|
3155
|
+
return await fn();
|
|
3156
|
+
} finally {
|
|
3157
|
+
try {
|
|
3158
|
+
await unlink(LOCK_PATH);
|
|
3159
|
+
} catch {
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
async function handleStaleLock(operation) {
|
|
3164
|
+
try {
|
|
3165
|
+
const content = await readFile7(LOCK_PATH, "utf-8");
|
|
3166
|
+
const lines = content.trim().split("\n");
|
|
3167
|
+
const pid = Number(lines[0]);
|
|
3168
|
+
if (pid && Number.isFinite(pid)) {
|
|
3169
|
+
try {
|
|
3170
|
+
process.kill(pid, 0);
|
|
3171
|
+
const op = lines[1] || "unknown";
|
|
3172
|
+
const ts = lines[2] || "unknown";
|
|
3173
|
+
throw new Error(`RSP locked by pid ${pid} (${op}) since ${ts}`);
|
|
3174
|
+
} catch (e) {
|
|
3175
|
+
if (e instanceof Error && e.message.startsWith("RSP locked"))
|
|
3176
|
+
throw e;
|
|
3177
|
+
const code = e.code;
|
|
3178
|
+
if (code === "ESRCH") {
|
|
3179
|
+
await unlink(LOCK_PATH);
|
|
3180
|
+
await writeFile(LOCK_PATH, `${process.pid}
|
|
3181
|
+
${operation}
|
|
3182
|
+
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
throw e;
|
|
3186
|
+
}
|
|
3187
|
+
} else {
|
|
3188
|
+
await unlink(LOCK_PATH);
|
|
3189
|
+
await writeFile(LOCK_PATH, `${process.pid}
|
|
3190
|
+
${operation}
|
|
3191
|
+
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
3192
|
+
}
|
|
3193
|
+
} catch (e) {
|
|
3194
|
+
if (e instanceof Error && e.message.startsWith("RSP locked"))
|
|
3195
|
+
throw e;
|
|
3196
|
+
throw new Error(`RSP is locked. Remove ${LOCK_PATH} manually if no other process is running.`);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
4182
3199
|
|
|
4183
3200
|
// src/commands/add-spec.ts
|
|
4184
|
-
import { existsSync as existsSync4 } from "fs";
|
|
4185
|
-
import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
|
|
4186
|
-
import { join as join5 } from "path";
|
|
4187
3201
|
async function addSpec(name, projectName = "project") {
|
|
4188
3202
|
if (!name || !isValidSpecName(name)) {
|
|
4189
3203
|
return { ok: false, message: "spec name must be kebab-case with optional subdirectory" };
|
|
@@ -4194,13 +3208,9 @@ async function addSpec(name, projectName = "project") {
|
|
|
4194
3208
|
if (["index", "00-index"].includes(name.split("/").at(-1) || "")) {
|
|
4195
3209
|
return { ok: false, message: 'spec name "index" is reserved for Specs migration compatibility' };
|
|
4196
3210
|
}
|
|
4197
|
-
const
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
const initialized = existsSync4(RSP_DIR);
|
|
4201
|
-
return { ok: false, message: `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
4202
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}` };
|
|
4203
|
-
}
|
|
3211
|
+
const initialization = inspectRspInitialization();
|
|
3212
|
+
if (initialization)
|
|
3213
|
+
return { ok: false, message: initialization };
|
|
4204
3214
|
try {
|
|
4205
3215
|
const configInspection = await inspectRspConfig();
|
|
4206
3216
|
if (configInspection.issues.length > 0) {
|
|
@@ -4210,14 +3220,14 @@ async function addSpec(name, projectName = "project") {
|
|
|
4210
3220
|
return { ok: false, message: `.rsp/config.yaml could not be parsed: ${toErrorMessage(error)}` };
|
|
4211
3221
|
}
|
|
4212
3222
|
return withRspLock("add-spec", async () => {
|
|
4213
|
-
const specsDir =
|
|
3223
|
+
const specsDir = join4(RSP_DIR, "specs");
|
|
4214
3224
|
const segments = name.split("/");
|
|
4215
3225
|
const parent = resolveManagedDirectoryChain(specsDir, segments.slice(0, -1), "spec path");
|
|
4216
|
-
const path =
|
|
3226
|
+
const path = join4(parent, `${segments.at(-1)}.md`);
|
|
4217
3227
|
const inspection = await inspectManagedFileTree(specsDir, "Specs");
|
|
4218
3228
|
if (inspection.issues.length > 0)
|
|
4219
3229
|
throw inspection.issues[0];
|
|
4220
|
-
await
|
|
3230
|
+
await mkdir3(parent, { recursive: true });
|
|
4221
3231
|
const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
|
|
4222
3232
|
const content = name === "design" ? generateDesignContent(resolvedProjectName) : generateSpecContent(name);
|
|
4223
3233
|
try {
|
|
@@ -4232,114 +3242,245 @@ async function addSpec(name, projectName = "project") {
|
|
|
4232
3242
|
}
|
|
4233
3243
|
|
|
4234
3244
|
// src/commands/init.ts
|
|
4235
|
-
import { existsSync as
|
|
4236
|
-
import { mkdir as
|
|
3245
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3246
|
+
import { mkdir as mkdir4, readdir as readdir3, readFile as readFile9 } from "fs/promises";
|
|
4237
3247
|
import { join as join6 } from "path";
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
}
|
|
4241
|
-
|
|
4242
|
-
|
|
3248
|
+
|
|
3249
|
+
// src/core/setup-files.ts
|
|
3250
|
+
import { constants } from "fs";
|
|
3251
|
+
import { access, chmod as chmod2, link, mkdtemp as mkdtemp2, readFile as readFile8, rename as rename2, rm as rm2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
|
|
3252
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
3253
|
+
async function updateSetupFile(path, render) {
|
|
3254
|
+
const exists = requireManagedFile(path, "setup file", { allowMissing: true });
|
|
3255
|
+
const current = exists ? await readFile8(path, "utf8") : null;
|
|
3256
|
+
const next = render(current);
|
|
3257
|
+
if (next === current)
|
|
3258
|
+
return false;
|
|
3259
|
+
if (exists)
|
|
3260
|
+
await access(path, constants.W_OK);
|
|
3261
|
+
const temporary = await mkdtemp2(join5(dirname2(path), ".rsp-write-"));
|
|
3262
|
+
const prepared = join5(temporary, "next");
|
|
3263
|
+
let failure2;
|
|
3264
|
+
try {
|
|
3265
|
+
await writeFile3(prepared, next, { flag: "wx" });
|
|
3266
|
+
if (exists)
|
|
3267
|
+
await chmod2(prepared, (await stat2(path)).mode & 511);
|
|
3268
|
+
const stillExists = requireManagedFile(path, "setup file", { allowMissing: true });
|
|
3269
|
+
if (stillExists !== exists || stillExists && await readFile8(path, "utf8") !== current)
|
|
3270
|
+
throw new Error(`Setup file changed during update: ${path}`);
|
|
3271
|
+
if (exists)
|
|
3272
|
+
await rename2(prepared, path);
|
|
3273
|
+
else
|
|
3274
|
+
await link(prepared, path);
|
|
3275
|
+
} catch (error) {
|
|
3276
|
+
failure2 = error instanceof Error ? error : new Error(String(error));
|
|
3277
|
+
}
|
|
3278
|
+
try {
|
|
3279
|
+
await rm2(temporary, { recursive: true });
|
|
3280
|
+
} catch (error) {
|
|
3281
|
+
failure2 = new Error(`Temporary content cleanup failed: ${temporary}`, { cause: failure2 ?? error });
|
|
3282
|
+
}
|
|
3283
|
+
if (failure2)
|
|
3284
|
+
throw failure2;
|
|
3285
|
+
return true;
|
|
4243
3286
|
}
|
|
3287
|
+
|
|
3288
|
+
// src/commands/init.ts
|
|
4244
3289
|
async function initProject(args = {}) {
|
|
4245
|
-
const isNew = !
|
|
3290
|
+
const isNew = !existsSync2(RSP_DIR);
|
|
4246
3291
|
const projectName = await detectProjectName();
|
|
4247
3292
|
const mode = args.agentsMode ?? "managed";
|
|
4248
|
-
if (mode !== "managed" && mode !== "print")
|
|
4249
|
-
return { ok: false, message:
|
|
4250
|
-
}
|
|
4251
|
-
const agentsPath = "AGENTS.md";
|
|
4252
|
-
const initialAgentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
|
|
4253
|
-
if (initialAgentsInspection.issue)
|
|
4254
|
-
throw initialAgentsInspection.issue;
|
|
3293
|
+
if (mode !== "managed" && mode !== "print")
|
|
3294
|
+
return { ok: false, message: "unsupported --agents-mode; use managed or print" };
|
|
4255
3295
|
return withRspLock("init", async () => {
|
|
4256
|
-
const
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
if (
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
3296
|
+
const runtime = await selectProjectRuntime();
|
|
3297
|
+
const { version } = runtime;
|
|
3298
|
+
const retiredRules = [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH].map((path) => requireManagedFile(path, "retired Rules", { allowMissing: true })).some(Boolean);
|
|
3299
|
+
if (retiredRules)
|
|
3300
|
+
return { ok: false, message: "Retired RSP assets require migration; use the selected target CLI: rsp update" };
|
|
3301
|
+
requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true });
|
|
3302
|
+
clearConfigCache();
|
|
3303
|
+
const inspection = await inspectRspConfig();
|
|
3304
|
+
if (inspection.issues.length)
|
|
3305
|
+
throw new Error(inspection.issues.join("; "));
|
|
3306
|
+
const decisionRecordsPath = resolveDecisionRecordsPath(inspection.config);
|
|
3307
|
+
const pathIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath);
|
|
3308
|
+
if (pathIssue)
|
|
3309
|
+
throw new Error(pathIssue);
|
|
3310
|
+
const dirs = [join6(RSP_DIR, "specs"), CHANGES_DIR, FOCUS_DIR, join6(RSP_DIR, "archives"), decisionRecordsPath];
|
|
3311
|
+
for (const path of dirs)
|
|
3312
|
+
resolveManagedDirectoryChain(process.cwd(), path.split("/"), "project directory");
|
|
3313
|
+
const completed = [];
|
|
3314
|
+
async function ensureFile(path, content) {
|
|
3315
|
+
if (await updateSetupFile(path, (current) => current ?? content))
|
|
3316
|
+
completed.push(`created ${path}`);
|
|
4268
3317
|
}
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
{
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
await mkdir5(directory.path, { recursive: true });
|
|
4279
|
-
let created = false;
|
|
4280
|
-
const bundledRules = await readFile8(join6(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
4281
|
-
created = await ensureFile(RSP_RULES_PATH, bundledRules) || created;
|
|
4282
|
-
if (existsSync5(CONFIG_PATH)) {
|
|
4283
|
-
const existing = await readFile8(CONFIG_PATH, "utf-8");
|
|
4284
|
-
const reconciled = reconcileRspConfigDefaults(existing);
|
|
4285
|
-
if (reconciled.changed) {
|
|
4286
|
-
await writeManagedFile(CONFIG_PATH, reconciled.content, "config file");
|
|
4287
|
-
created = true;
|
|
3318
|
+
try {
|
|
3319
|
+
const skills = await updatePackagedSkills();
|
|
3320
|
+
completed.push(...skills.installed.map((name) => `installed ${name}`), ...skills.replaced.map((name) => `updated ${name}`), ...skills.removed.map((name) => `removed ${name}`));
|
|
3321
|
+
for (const path of dirs) {
|
|
3322
|
+
if (!requireManagedDirectory(path, "project directory", { allowMissing: true })) {
|
|
3323
|
+
await mkdir4(path, { recursive: true });
|
|
3324
|
+
requireManagedDirectory(path, "project directory");
|
|
3325
|
+
completed.push(`created ${path}`);
|
|
3326
|
+
}
|
|
4288
3327
|
}
|
|
4289
|
-
|
|
4290
|
-
|
|
3328
|
+
await ensureFile(CONFIG_PATH, generateConfigTemplate());
|
|
3329
|
+
clearConfigCache();
|
|
3330
|
+
await ensureFile(join6(RSP_DIR, "specs/design.md"), generateDesignContent(projectName));
|
|
3331
|
+
if ((await readdir3(decisionRecordsPath)).length === 0)
|
|
3332
|
+
await ensureFile(join6(decisionRecordsPath, ".gitkeep"), "");
|
|
3333
|
+
await ensureFile(join6(CHANGES_DIR, ".gitkeep"), "");
|
|
3334
|
+
await ensureFile(join6(FOCUS_DIR, ".gitkeep"), "");
|
|
3335
|
+
await ensureFile(join6(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n");
|
|
3336
|
+
if (args.withProjectSetup) {
|
|
3337
|
+
const work = resolveWorkRef("project-setup", { executable: true });
|
|
3338
|
+
await ensureFile(work.path, generateChangeContent("project-setup"));
|
|
3339
|
+
await ensureFile(resolveFocusMarkerPath(work), "");
|
|
3340
|
+
}
|
|
3341
|
+
const title = projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
3342
|
+
const agentsUpdated = await updateSetupFile("AGENTS.md", (current) => upsertRspAgentsBlock(current ?? `# ${title}
|
|
3343
|
+
|
|
3344
|
+
`, runtime).content);
|
|
3345
|
+
if (agentsUpdated)
|
|
3346
|
+
completed.push("updated AGENTS.md managed block");
|
|
3347
|
+
if (runtime.mode === "package" && await updateSetupFile(PROJECT_VERSION_PATH, () => `${version}
|
|
3348
|
+
`))
|
|
3349
|
+
completed.push(`pinned ${version}`);
|
|
3350
|
+
return { ok: true, isNew, created: completed.length > 0, decisionRecordsPath, withProjectSetup: Boolean(args.withProjectSetup), agentsUpdated, skills, managedAgents: mode === "print" ? await readFile9("AGENTS.md", "utf8") : void 0 };
|
|
3351
|
+
} catch (error) {
|
|
3352
|
+
throw new Error(`Init stopped; completed: ${completed.join(", ") || "none"}. No rollback was attempted. Fix the cause and rerun init with the same target CLI. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
4291
3353
|
}
|
|
3354
|
+
});
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
// src/commands/update.ts
|
|
3358
|
+
import { existsSync as existsSync3 } from "fs";
|
|
3359
|
+
import { readFile as readFile12, unlink as unlink3 } from "fs/promises";
|
|
3360
|
+
import { join as join8 } from "path";
|
|
3361
|
+
|
|
3362
|
+
// src/commands/archive-index-migration.ts
|
|
3363
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3364
|
+
import { join as join7 } from "path";
|
|
3365
|
+
async function isLegacyArchiveIndex() {
|
|
3366
|
+
const path = join7(ARCHIVES_DIR, "INDEX.md");
|
|
3367
|
+
if (!requireManagedFile(path, "legacy archive index", { allowMissing: true }))
|
|
3368
|
+
return false;
|
|
3369
|
+
return isLegacyArchiveIndexContent(await readFile10(path, "utf-8"));
|
|
3370
|
+
}
|
|
3371
|
+
function isLegacyArchiveIndexContent(content) {
|
|
3372
|
+
try {
|
|
3373
|
+
const frontmatter = parseFrontmatter(content);
|
|
3374
|
+
return frontmatter?.kind === "generated-index" && frontmatter.index_type === "archives";
|
|
3375
|
+
} catch {
|
|
3376
|
+
return false;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
// src/commands/specs-index-migration.ts
|
|
3381
|
+
import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
|
|
3382
|
+
import { dirname as dirname3, resolve } from "path";
|
|
3383
|
+
async function inspectGeneratedSpecsIndexMigration(cwd = process.cwd()) {
|
|
3384
|
+
const inspection = await inspectSpecs({ cwd, allowMissingDecisionRecords: true });
|
|
3385
|
+
return {
|
|
3386
|
+
inspectionComplete: specsInspectionComplete(inspection),
|
|
3387
|
+
removable: inspection.generatedIndexes.filter((index) => index.classification === "safe-removal").map((index) => index.path).sort(),
|
|
3388
|
+
ownerControlled: inspection.generatedIndexes.filter((index) => index.classification === "owner-controlled").map((index) => index.path).sort(),
|
|
3389
|
+
diagnostics: inspection.diagnostics.map((diagnostic) => `${diagnostic.path}: ${diagnostic.message}`)
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
async function removeGeneratedSpecsIndex(path) {
|
|
3393
|
+
resolveManagedDirectoryChain(process.cwd(), dirname3(path).split("/"), "Specs parent");
|
|
3394
|
+
if (!requireManagedFile(path, "generated Specs index", { allowMissing: true }))
|
|
3395
|
+
return false;
|
|
3396
|
+
if (!isRecognizedGeneratedSpecsIndex(await readFile11(resolve(path), "utf8"), path))
|
|
3397
|
+
throw new Error(`Specs index is no longer generated; left untouched: ${path}`);
|
|
3398
|
+
await unlink2(path);
|
|
3399
|
+
return true;
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
// src/commands/update.ts
|
|
3403
|
+
async function updateProject(options = {}) {
|
|
3404
|
+
if (!existsSync3(RSP_DIR))
|
|
3405
|
+
return { ok: false, actions: [], updated: false, events: [{ type: "not-initialized" }], migration: { inspectionComplete: false, manualMigrationRequired: false, residualEntries: [], diagnostics: [] } };
|
|
3406
|
+
const execute = async () => {
|
|
3407
|
+
const runtime = await selectProjectRuntime("update");
|
|
3408
|
+
const { version } = runtime;
|
|
4292
3409
|
clearConfigCache();
|
|
4293
|
-
const
|
|
4294
|
-
if (
|
|
4295
|
-
throw new Error(
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
const
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
}
|
|
4311
|
-
const title = toTitle(projectName);
|
|
4312
|
-
let agentsUpdated = false;
|
|
4313
|
-
if (!agentsInspection.exists) {
|
|
4314
|
-
await writeManagedFile(agentsPath, `# ${title}
|
|
3410
|
+
const config = await inspectRspConfig();
|
|
3411
|
+
if (config.issues.length)
|
|
3412
|
+
throw new Error(config.issues.join("; "));
|
|
3413
|
+
resolveManagedDirectoryChain(process.cwd(), [RSP_DIR, "rules"], "retired Rules parent");
|
|
3414
|
+
const retired = [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH].filter((path) => requireManagedFile(path, "retired Rules", { allowMissing: true }));
|
|
3415
|
+
const rules = await inspectUnsupportedRules();
|
|
3416
|
+
const migration = { inspectionComplete: rules.diagnostics.length === 0, manualMigrationRequired: rules.entries.length > 0, residualEntries: rules.entries.map((entry) => entry.path), diagnostics: rules.diagnostics };
|
|
3417
|
+
if (!migration.inspectionComplete)
|
|
3418
|
+
return { ok: false, actions: [], updated: false, events: [{ type: "migration-diagnostics", diagnostics: rules.diagnostics }], migration };
|
|
3419
|
+
requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true });
|
|
3420
|
+
resolveManagedDirectoryChain(process.cwd(), [RSP_DIR, "archives"], "archive parent");
|
|
3421
|
+
requireManagedFile(join8(RSP_DIR, "archives/INDEX.md"), "archive index", { allowMissing: true });
|
|
3422
|
+
const plan = await inspectGeneratedSpecsIndexMigration();
|
|
3423
|
+
if (!plan.inspectionComplete || plan.ownerControlled.length)
|
|
3424
|
+
throw new Error(`Generated Specs-index migration requires owner review before update: ${[...plan.ownerControlled, ...plan.diagnostics].join("; ")}`);
|
|
3425
|
+
const projectName = await detectProjectName();
|
|
3426
|
+
const agents = requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true }) ? await readFile12("AGENTS.md", "utf8") : null;
|
|
3427
|
+
const refreshAgents = (current) => upsertRspAgentsBlock(current ?? `# ${projectName}
|
|
4315
3428
|
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
3429
|
+
`, runtime).content;
|
|
3430
|
+
const removeArchive = await isLegacyArchiveIndex();
|
|
3431
|
+
const actions = [];
|
|
3432
|
+
const events = [];
|
|
3433
|
+
if (existsSync3("CONTEXT-MAP.md"))
|
|
3434
|
+
events.push({ type: "legacy-context-map" });
|
|
3435
|
+
if (migration.residualEntries.length)
|
|
3436
|
+
events.push({ type: "residual-rules", entries: migration.residualEntries });
|
|
3437
|
+
const record = (message) => {
|
|
3438
|
+
actions.push(message);
|
|
3439
|
+
events.push({ type: "updated", message });
|
|
3440
|
+
};
|
|
3441
|
+
try {
|
|
3442
|
+
const skills = await updatePackagedSkills({ dryRun: options.dryRun });
|
|
3443
|
+
for (const name of skills.installed) record(`Skill installed: ${name}`);
|
|
3444
|
+
for (const name of skills.replaced) record(`Skill updated: ${name}`);
|
|
3445
|
+
for (const name of skills.removed) record(`retired Skill removed: ${name}`);
|
|
3446
|
+
if (options.dryRun) {
|
|
3447
|
+
actions.push(...retired.map((path) => `retired Rules file removed: ${path}`), ...plan.removable.map((path) => `generated Specs index removed: ${path}`));
|
|
3448
|
+
if (removeArchive)
|
|
3449
|
+
actions.push("legacy Archive Index removed");
|
|
3450
|
+
if (refreshAgents(agents) !== agents)
|
|
3451
|
+
actions.push("AGENTS.md managed block refreshed");
|
|
3452
|
+
if (runtime.mode === "package" && await readProjectVersion() !== version)
|
|
3453
|
+
actions.push(`project CLI pinned to ${version}`);
|
|
3454
|
+
return { ok: true, actions, updated: false, skills, events: [], migration };
|
|
3455
|
+
}
|
|
3456
|
+
for (const path of retired) {
|
|
3457
|
+
if (requireManagedFile(path, "retired Rules", { allowMissing: true })) {
|
|
3458
|
+
await unlink3(path);
|
|
3459
|
+
record(`retired Rules file removed: ${path}`);
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
for (const path of plan.removable) {
|
|
3463
|
+
if (await removeGeneratedSpecsIndex(path))
|
|
3464
|
+
record(`generated Specs index removed: ${path}`);
|
|
4327
3465
|
}
|
|
3466
|
+
if (await isLegacyArchiveIndex()) {
|
|
3467
|
+
await unlink3(join8(RSP_DIR, "archives/INDEX.md"));
|
|
3468
|
+
record("legacy Archive Index removed");
|
|
3469
|
+
}
|
|
3470
|
+
if (await updateSetupFile("AGENTS.md", refreshAgents))
|
|
3471
|
+
record("AGENTS.md managed block refreshed");
|
|
3472
|
+
if (runtime.mode === "package" && await updateSetupFile(PROJECT_VERSION_PATH, () => `${version}
|
|
3473
|
+
`))
|
|
3474
|
+
record(`project CLI pinned to ${version}`);
|
|
3475
|
+
return { ok: true, actions, updated: actions.length > 0, skills, events, migration };
|
|
3476
|
+
} catch (error) {
|
|
3477
|
+
throw new Error(`Update stopped; completed: ${actions.join(", ") || "none"}. No rollback was attempted. Fix the cause and rerun update with the same target CLI. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
4328
3478
|
}
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
isNew,
|
|
4332
|
-
created,
|
|
4333
|
-
decisionRecordsPath,
|
|
4334
|
-
withProjectSetup: Boolean(args.withProjectSetup),
|
|
4335
|
-
agentsUpdated,
|
|
4336
|
-
managedAgents: mode === "print" ? await readFile8(agentsPath, "utf-8") : void 0
|
|
4337
|
-
};
|
|
4338
|
-
});
|
|
3479
|
+
};
|
|
3480
|
+
return options.dryRun ? execute() : withRspLock("update", execute);
|
|
4339
3481
|
}
|
|
4340
3482
|
|
|
4341
3483
|
// src/cli/presenters/setup.ts
|
|
4342
|
-
var SKILL_REFRESH_HINT = " Note: if you use package-bundled RSP Skills, refresh them too:\n rsp skills install --dry-run\n rsp skills install --force\n";
|
|
4343
3484
|
function presentAddSpec(result) {
|
|
4344
3485
|
if (!result.ok) {
|
|
4345
3486
|
console.error(` ${pc.red("Error:")} ${result.message}`);
|
|
@@ -4360,11 +3501,12 @@ ${result.managedAgents}
|
|
|
4360
3501
|
if (result.isNew) {
|
|
4361
3502
|
const createdProjectSetup = result.withProjectSetup ? "\n .rsp/changes/project-setup.md\n .rsp/focus.d/project-setup" : "";
|
|
4362
3503
|
const createdAgents = result.agentsUpdated ? "\n AGENTS.md" : "";
|
|
4363
|
-
const next = result.withProjectSetup ? "fill .rsp/changes/project-setup.md" : "rsp
|
|
3504
|
+
const next = result.withProjectSetup ? "fill .rsp/changes/project-setup.md" : "rsp status";
|
|
4364
3505
|
console.log(`
|
|
4365
3506
|
${pc.green("RSP scaffolded.")}
|
|
4366
3507
|
`);
|
|
4367
|
-
console.log(` Created:
|
|
3508
|
+
console.log(` Created: complete core Skill suite
|
|
3509
|
+
.rsp/version
|
|
4368
3510
|
.rsp/specs/
|
|
4369
3511
|
${result.decisionRecordsPath}/
|
|
4370
3512
|
.rsp/changes/
|
|
@@ -4388,7 +3530,13 @@ ${result.managedAgents}
|
|
|
4388
3530
|
`);
|
|
4389
3531
|
}
|
|
4390
3532
|
}
|
|
4391
|
-
function presentUpdate(result) {
|
|
3533
|
+
function presentUpdate(result, dryRun = false) {
|
|
3534
|
+
if (dryRun && result.ok) {
|
|
3535
|
+
console.log(" Update preview (no files changed):");
|
|
3536
|
+
for (const action of result.actions)
|
|
3537
|
+
console.log(` ${action}`);
|
|
3538
|
+
return;
|
|
3539
|
+
}
|
|
4392
3540
|
if (!result.ok && result.events.some((event) => event.type === "not-initialized")) {
|
|
4393
3541
|
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
4394
3542
|
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
@@ -4410,10 +3558,9 @@ function presentUpdate(result) {
|
|
|
4410
3558
|
console.log(` ${pc.dim("Move stable scoped instructions to the nearest project-owned AGENTS.md, then remove the old files.")}`);
|
|
4411
3559
|
console.log(` ${pc.dim("Run: rsp doctor")}`);
|
|
4412
3560
|
console.log();
|
|
4413
|
-
} else if (event.type === "
|
|
4414
|
-
console.log(` ${pc.
|
|
4415
|
-
|
|
4416
|
-
console.log(` ${path}`);
|
|
3561
|
+
} else if (event.type === "legacy-context-map") {
|
|
3562
|
+
console.log(` ${pc.yellow("Note:")} CONTEXT-MAP.md remains; context migration needs owner review.`);
|
|
3563
|
+
console.log(` ${pc.dim("Use Core context migration. Keep relevant legacy context until an authorized merge updates CONTEXT.md and active references; update does not migrate or delete it.")}`);
|
|
4417
3564
|
}
|
|
4418
3565
|
}
|
|
4419
3566
|
if (!result.migration.inspectionComplete)
|
|
@@ -4428,7 +3575,6 @@ function presentUpdate(result) {
|
|
|
4428
3575
|
else
|
|
4429
3576
|
console.log(` ${pc.green("Update complete.")}
|
|
4430
3577
|
`);
|
|
4431
|
-
console.log(SKILL_REFRESH_HINT);
|
|
4432
3578
|
}
|
|
4433
3579
|
|
|
4434
3580
|
// src/cli/commands/setup.ts
|
|
@@ -4492,14 +3638,19 @@ var addCommand = defineCommand2({
|
|
|
4492
3638
|
var updateCommand = defineCommand2({
|
|
4493
3639
|
meta: {
|
|
4494
3640
|
name: "update",
|
|
4495
|
-
description: "
|
|
3641
|
+
description: "Update the complete pinned environment and remove retired managed assets"
|
|
3642
|
+
},
|
|
3643
|
+
args: {
|
|
3644
|
+
"dry-run": { type: "boolean", description: "Preview the complete update without changing files", default: false }
|
|
4496
3645
|
},
|
|
4497
|
-
async run() {
|
|
3646
|
+
async run({ args, rawArgs }) {
|
|
3647
|
+
if (args._.length || rawArgs.some((arg) => !["--dry-run", "--dry-run=true", "--dry-run=false", "--no-dry-run"].includes(arg)))
|
|
3648
|
+
throw new Error("rsp update accepts only --dry-run; use the selected target CLI to update all managed assets");
|
|
4498
3649
|
await executeCliCommand({
|
|
4499
|
-
execute: () => updateProject(),
|
|
4500
|
-
present: presentUpdate,
|
|
3650
|
+
execute: () => updateProject({ dryRun: Boolean(args["dry-run"]) }),
|
|
3651
|
+
present: (result) => presentUpdate(result, Boolean(args["dry-run"])),
|
|
4501
3652
|
exitCode: (result) => result.migration.inspectionComplete ? void 0 : 1
|
|
4502
|
-
},
|
|
3653
|
+
}, args);
|
|
4503
3654
|
}
|
|
4504
3655
|
});
|
|
4505
3656
|
var setupCommands = {
|
|
@@ -4510,49 +3661,26 @@ var setupCommands = {
|
|
|
4510
3661
|
|
|
4511
3662
|
// src/cli/commands/skills.ts
|
|
4512
3663
|
import { defineCommand as defineCommand3 } from "citty";
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
"name": {
|
|
4520
|
-
type: "positional",
|
|
4521
|
-
description: "Exact packaged Skill name (default: lifecycle suite)",
|
|
4522
|
-
required: false
|
|
4523
|
-
},
|
|
4524
|
-
"dry-run": {
|
|
4525
|
-
type: "boolean",
|
|
4526
|
-
description: "Preflight selected packaged Skills without changing files",
|
|
4527
|
-
default: false
|
|
4528
|
-
},
|
|
4529
|
-
"force": {
|
|
4530
|
-
type: "boolean",
|
|
4531
|
-
description: "Replace divergent package-owned Skill directories",
|
|
4532
|
-
default: false
|
|
4533
|
-
}
|
|
4534
|
-
},
|
|
4535
|
-
async run({ args }) {
|
|
4536
|
-
if (args._.length > 1)
|
|
4537
|
-
throw new Error("rsp skills install accepts at most one Skill name");
|
|
4538
|
-
await executeCliCommand({
|
|
4539
|
-
execute: () => installPackagedSkills({
|
|
4540
|
-
dryRun: Boolean(args["dry-run"]),
|
|
4541
|
-
force: Boolean(args.force),
|
|
4542
|
-
names: args.name ? [args.name] : void 0
|
|
4543
|
-
}),
|
|
4544
|
-
present: (result) => presentSkillInstallResult(result, Boolean(args["dry-run"]))
|
|
4545
|
-
}, args);
|
|
3664
|
+
|
|
3665
|
+
// src/cli/presenters/skills.ts
|
|
3666
|
+
function presentPackagedSkillInventory(inventory, json = false) {
|
|
3667
|
+
if (json) {
|
|
3668
|
+
emitJson(inventory);
|
|
3669
|
+
return;
|
|
4546
3670
|
}
|
|
4547
|
-
});
|
|
3671
|
+
console.log(` ${inventory.package.name}@${inventory.package.version}`);
|
|
3672
|
+
console.log(` target: ${inventory.target}`);
|
|
3673
|
+
console.log(" Core suite (managed by init/update)");
|
|
3674
|
+
for (const skill of inventory.skills)
|
|
3675
|
+
console.log(` ${skill.name} ${skill.status}`);
|
|
3676
|
+
for (const name of inventory.retired)
|
|
3677
|
+
console.log(` ${name} retired`);
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
// src/cli/commands/skills.ts
|
|
4548
3681
|
var skillsListCommand = defineCommand3({
|
|
4549
|
-
meta: {
|
|
4550
|
-
|
|
4551
|
-
description: "List package-bundled Skills and their project status"
|
|
4552
|
-
},
|
|
4553
|
-
args: {
|
|
4554
|
-
...jsonArgs
|
|
4555
|
-
},
|
|
3682
|
+
meta: { name: "list", description: "Inspect the complete package-owned Skill inventory" },
|
|
3683
|
+
args: { ...jsonArgs },
|
|
4556
3684
|
async run({ args }) {
|
|
4557
3685
|
await executeCliCommand({
|
|
4558
3686
|
execute: () => inspectPackagedSkillInventory(),
|
|
@@ -4560,32 +3688,25 @@ var skillsListCommand = defineCommand3({
|
|
|
4560
3688
|
}, args);
|
|
4561
3689
|
}
|
|
4562
3690
|
});
|
|
4563
|
-
var skillsCommand = defineCommand3({
|
|
4564
|
-
meta: {
|
|
4565
|
-
name: "skills",
|
|
4566
|
-
description: "Manage package-bundled project Skills"
|
|
4567
|
-
},
|
|
4568
|
-
subCommands: {
|
|
4569
|
-
install: skillsInstallCommand,
|
|
4570
|
-
list: skillsListCommand
|
|
4571
|
-
}
|
|
4572
|
-
});
|
|
4573
3691
|
var skillsCommands = {
|
|
4574
|
-
skills:
|
|
3692
|
+
skills: defineCommand3({
|
|
3693
|
+
meta: { name: "skills", description: "Inspect Skills; use init or update to manage the complete environment" },
|
|
3694
|
+
subCommands: { list: skillsListCommand }
|
|
3695
|
+
})
|
|
4575
3696
|
};
|
|
4576
3697
|
|
|
4577
3698
|
// src/cli/commands/work.ts
|
|
4578
3699
|
import { defineCommand as defineCommand4 } from "citty";
|
|
4579
3700
|
|
|
4580
3701
|
// src/commands/archive.ts
|
|
4581
|
-
import { existsSync as
|
|
4582
|
-
import { mkdir as
|
|
4583
|
-
import { basename as basename2, join as
|
|
3702
|
+
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
3703
|
+
import { mkdir as mkdir5, readFile as readFile13, rename as rename3, unlink as unlink4 } from "fs/promises";
|
|
3704
|
+
import { basename as basename2, join as join9 } from "path";
|
|
4584
3705
|
async function archiveChange(name) {
|
|
4585
3706
|
if (!name) {
|
|
4586
3707
|
return { ok: false, kind: "usage", message: "rsp archive <name>" };
|
|
4587
3708
|
}
|
|
4588
|
-
const initialization =
|
|
3709
|
+
const initialization = inspectRspInitialization();
|
|
4589
3710
|
if (initialization)
|
|
4590
3711
|
return { ok: false, kind: "error", message: initialization };
|
|
4591
3712
|
try {
|
|
@@ -4597,7 +3718,7 @@ async function archiveChange(name) {
|
|
|
4597
3718
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4598
3719
|
const base = basename2(workRef.name);
|
|
4599
3720
|
const archiveName = resolveArchiveName(archiveSubdir, date, base);
|
|
4600
|
-
const content = await
|
|
3721
|
+
const content = await readFile13(srcPath, "utf-8");
|
|
4601
3722
|
const configInspection = await inspectRspConfig();
|
|
4602
3723
|
if (configInspection.issues.length > 0) {
|
|
4603
3724
|
return {
|
|
@@ -4607,9 +3728,10 @@ async function archiveChange(name) {
|
|
|
4607
3728
|
};
|
|
4608
3729
|
}
|
|
4609
3730
|
const dependencyInspection = await inspectChangeDependencies();
|
|
4610
|
-
const
|
|
3731
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
3732
|
+
const readiness = collectArchiveReadiness(document, {
|
|
4611
3733
|
activeBlockers: dependencyInspection.activeBlockers.get(workRef.name),
|
|
4612
|
-
documentDiagnostics: inspectChangeDocument(
|
|
3734
|
+
documentDiagnostics: inspectChangeDocument(document, {
|
|
4613
3735
|
name: workRef.name,
|
|
4614
3736
|
validKinds: resolveKinds(configInspection.config),
|
|
4615
3737
|
requiredSections: resolveRequiredSections(configInspection.config)
|
|
@@ -4624,13 +3746,13 @@ async function archiveChange(name) {
|
|
|
4624
3746
|
warnings: checklist
|
|
4625
3747
|
};
|
|
4626
3748
|
}
|
|
4627
|
-
await
|
|
4628
|
-
await rename3(srcPath,
|
|
3749
|
+
await mkdir5(archiveSubdir, { recursive: true });
|
|
3750
|
+
await rename3(srcPath, join9(archiveSubdir, archiveName));
|
|
4629
3751
|
const postArchiveWarnings = [];
|
|
4630
3752
|
let focusCleared = false;
|
|
4631
|
-
if (
|
|
3753
|
+
if (existsSync4(focusEntry)) {
|
|
4632
3754
|
try {
|
|
4633
|
-
await
|
|
3755
|
+
await unlink4(focusEntry);
|
|
4634
3756
|
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
4635
3757
|
focusCleared = true;
|
|
4636
3758
|
} catch (error) {
|
|
@@ -4642,7 +3764,7 @@ async function archiveChange(name) {
|
|
|
4642
3764
|
} catch (error) {
|
|
4643
3765
|
postArchiveWarnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
4644
3766
|
}
|
|
4645
|
-
return { ok: true, archiveName, focusCleared, readinessWarnings: checklist, cleanupWarnings: postArchiveWarnings, git:
|
|
3767
|
+
return { ok: true, archiveName, focusCleared, readinessWarnings: checklist, cleanupWarnings: postArchiveWarnings, git: existsSync4(".git") };
|
|
4646
3768
|
});
|
|
4647
3769
|
} catch (error) {
|
|
4648
3770
|
if (error instanceof WorkRefError) {
|
|
@@ -4651,15 +3773,6 @@ async function archiveChange(name) {
|
|
|
4651
3773
|
throw error;
|
|
4652
3774
|
}
|
|
4653
3775
|
}
|
|
4654
|
-
function inspectInitialization() {
|
|
4655
|
-
const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
4656
|
-
const design = inspectManagedFile(join7(RSP_DIR, "specs", "design.md"), "design Spec", { allowMissing: true });
|
|
4657
|
-
if (!rules.issue && !design.issue && rules.exists && design.exists)
|
|
4658
|
-
return void 0;
|
|
4659
|
-
const initialized = existsSync6(RSP_DIR);
|
|
4660
|
-
return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
4661
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}`;
|
|
4662
|
-
}
|
|
4663
3776
|
function resolveArchiveName(archiveSubdir, date, base) {
|
|
4664
3777
|
const initialName = `${date}_${base}.md`;
|
|
4665
3778
|
if (!archiveNameExists(archiveSubdir, initialName))
|
|
@@ -4677,7 +3790,7 @@ function resolveArchiveName(archiveSubdir, date, base) {
|
|
|
4677
3790
|
);
|
|
4678
3791
|
}
|
|
4679
3792
|
function archiveNameExists(archiveSubdir, name) {
|
|
4680
|
-
if (!
|
|
3793
|
+
if (!existsSync4(archiveSubdir))
|
|
4681
3794
|
return false;
|
|
4682
3795
|
return readdirSync(archiveSubdir).some((entry) => {
|
|
4683
3796
|
if (entry.normalize("NFC") !== name)
|
|
@@ -4693,252 +3806,10 @@ function archiveNameExists(archiveSubdir, name) {
|
|
|
4693
3806
|
});
|
|
4694
3807
|
}
|
|
4695
3808
|
|
|
4696
|
-
// src/commands/commit.ts
|
|
4697
|
-
import { execFile, spawn } from "child_process";
|
|
4698
|
-
import { lstat as lstat4, readFile as readFile10 } from "fs/promises";
|
|
4699
|
-
import { promisify } from "util";
|
|
4700
|
-
var execFileAsync = promisify(execFile);
|
|
4701
|
-
function messagesMatch(prepared, observed) {
|
|
4702
|
-
if (prepared === observed)
|
|
4703
|
-
return true;
|
|
4704
|
-
if (prepared.endsWith("\n") && prepared.slice(0, -1) === observed)
|
|
4705
|
-
return true;
|
|
4706
|
-
return observed.endsWith("\n") && observed.slice(0, -1) === prepared;
|
|
4707
|
-
}
|
|
4708
|
-
function extractCommittedMessage(output) {
|
|
4709
|
-
const separator = output.indexOf("\0");
|
|
4710
|
-
return separator === -1 ? output : output.slice(0, separator);
|
|
4711
|
-
}
|
|
4712
|
-
async function readStagedPaths() {
|
|
4713
|
-
const { stdout } = await execFileAsync("git", ["diff", "--cached", "--name-only", "--no-renames", "--diff-filter=ACDMRTUXB"], {
|
|
4714
|
-
encoding: "utf8"
|
|
4715
|
-
});
|
|
4716
|
-
return stdout.split("\n").filter(Boolean);
|
|
4717
|
-
}
|
|
4718
|
-
async function readHead() {
|
|
4719
|
-
try {
|
|
4720
|
-
const { stdout } = await execFileAsync("git", ["rev-parse", "--verify", "HEAD"], { encoding: "utf8" });
|
|
4721
|
-
return stdout.trim();
|
|
4722
|
-
} catch {
|
|
4723
|
-
return null;
|
|
4724
|
-
}
|
|
4725
|
-
}
|
|
4726
|
-
async function readPathLines(args) {
|
|
4727
|
-
const { stdout } = await execFileAsync("git", args, { encoding: "utf8" });
|
|
4728
|
-
return stdout.split("\n").filter(Boolean);
|
|
4729
|
-
}
|
|
4730
|
-
async function readCommittedPaths(commit) {
|
|
4731
|
-
return readPathLines(["diff-tree", "--root", "--no-commit-id", "--name-only", "--no-renames", "-r", commit]);
|
|
4732
|
-
}
|
|
4733
|
-
async function readRemainingWorktreePaths() {
|
|
4734
|
-
const [unstaged, staged, untracked] = await Promise.all([
|
|
4735
|
-
readPathLines(["diff", "--name-only", "--no-renames", "--diff-filter=ACDMRTUXB"]),
|
|
4736
|
-
readPathLines(["diff", "--cached", "--name-only", "--no-renames", "--diff-filter=ACDMRTUXB"]),
|
|
4737
|
-
readPathLines(["ls-files", "--others", "--exclude-standard"])
|
|
4738
|
-
]);
|
|
4739
|
-
return [.../* @__PURE__ */ new Set([...unstaged, ...staged, ...untracked])].sort();
|
|
4740
|
-
}
|
|
4741
|
-
async function gitPathExists(path) {
|
|
4742
|
-
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", path], { encoding: "utf8" });
|
|
4743
|
-
try {
|
|
4744
|
-
await lstat4(stdout.trim());
|
|
4745
|
-
return true;
|
|
4746
|
-
} catch (error) {
|
|
4747
|
-
if (error.code === "ENOENT")
|
|
4748
|
-
return false;
|
|
4749
|
-
throw error;
|
|
4750
|
-
}
|
|
4751
|
-
}
|
|
4752
|
-
async function detectGitOperation() {
|
|
4753
|
-
const operations = [
|
|
4754
|
-
["merge", "MERGE_HEAD"],
|
|
4755
|
-
["cherry-pick", "CHERRY_PICK_HEAD"],
|
|
4756
|
-
["revert", "REVERT_HEAD"],
|
|
4757
|
-
["rebase", "rebase-merge"],
|
|
4758
|
-
["rebase-or-am", "rebase-apply"],
|
|
4759
|
-
["sequencer", "sequencer"]
|
|
4760
|
-
];
|
|
4761
|
-
for (const [operation, path] of operations) {
|
|
4762
|
-
if (await gitPathExists(path))
|
|
4763
|
-
return operation;
|
|
4764
|
-
}
|
|
4765
|
-
return null;
|
|
4766
|
-
}
|
|
4767
|
-
function samePaths(expected, observed) {
|
|
4768
|
-
const left = [...expected].sort();
|
|
4769
|
-
const right = [...observed].sort();
|
|
4770
|
-
return left.length === right.length && left.every((path, index) => path === right[index]);
|
|
4771
|
-
}
|
|
4772
|
-
async function runGitCommit(message) {
|
|
4773
|
-
return await new Promise((resolve5) => {
|
|
4774
|
-
const child = spawn("git", ["commit", "--cleanup=verbatim", "-F", "-"], {
|
|
4775
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4776
|
-
});
|
|
4777
|
-
let stdout = "";
|
|
4778
|
-
let stderr = "";
|
|
4779
|
-
let settled = false;
|
|
4780
|
-
child.stdout.setEncoding("utf8");
|
|
4781
|
-
child.stderr.setEncoding("utf8");
|
|
4782
|
-
child.stdout.on("data", (chunk) => stdout += chunk);
|
|
4783
|
-
child.stderr.on("data", (chunk) => stderr += chunk);
|
|
4784
|
-
child.on("error", (error) => {
|
|
4785
|
-
if (!settled) {
|
|
4786
|
-
settled = true;
|
|
4787
|
-
resolve5({ ok: false, message: toErrorMessage(error) });
|
|
4788
|
-
}
|
|
4789
|
-
});
|
|
4790
|
-
child.on("close", (code) => {
|
|
4791
|
-
if (settled)
|
|
4792
|
-
return;
|
|
4793
|
-
settled = true;
|
|
4794
|
-
if (code === 0)
|
|
4795
|
-
resolve5({ ok: true, stdout });
|
|
4796
|
-
else
|
|
4797
|
-
resolve5({ ok: false, message: stderr.trim() || stdout.trim() || `git commit exited with code ${code ?? "unknown"}` });
|
|
4798
|
-
});
|
|
4799
|
-
child.stdin.end(message);
|
|
4800
|
-
});
|
|
4801
|
-
}
|
|
4802
|
-
async function commitFromMessageFile(messageFile) {
|
|
4803
|
-
let preparedMessage;
|
|
4804
|
-
try {
|
|
4805
|
-
preparedMessage = await readFile10(messageFile, "utf8");
|
|
4806
|
-
} catch (error) {
|
|
4807
|
-
return {
|
|
4808
|
-
ok: false,
|
|
4809
|
-
command: "commit",
|
|
4810
|
-
code: "message_file_read_failed",
|
|
4811
|
-
message: `unable to read message file: ${toErrorMessage(error)}`
|
|
4812
|
-
};
|
|
4813
|
-
}
|
|
4814
|
-
if (preparedMessage.includes("\\n")) {
|
|
4815
|
-
return {
|
|
4816
|
-
ok: false,
|
|
4817
|
-
command: "commit",
|
|
4818
|
-
code: "literal_newline_escape",
|
|
4819
|
-
message: "message file contains literal \\\\n; use actual line breaks"
|
|
4820
|
-
};
|
|
4821
|
-
}
|
|
4822
|
-
let operation;
|
|
4823
|
-
try {
|
|
4824
|
-
operation = await detectGitOperation();
|
|
4825
|
-
} catch (error) {
|
|
4826
|
-
return {
|
|
4827
|
-
ok: false,
|
|
4828
|
-
command: "commit",
|
|
4829
|
-
code: "git_operation_in_progress",
|
|
4830
|
-
message: `unable to verify current Git operation state: ${toErrorMessage(error)}`
|
|
4831
|
-
};
|
|
4832
|
-
}
|
|
4833
|
-
if (operation) {
|
|
4834
|
-
return {
|
|
4835
|
-
ok: false,
|
|
4836
|
-
command: "commit",
|
|
4837
|
-
code: "git_operation_in_progress",
|
|
4838
|
-
operation,
|
|
4839
|
-
message: `refusing to commit while Git operation is in progress: ${operation}`
|
|
4840
|
-
};
|
|
4841
|
-
}
|
|
4842
|
-
let stagedPaths;
|
|
4843
|
-
try {
|
|
4844
|
-
stagedPaths = await readStagedPaths();
|
|
4845
|
-
} catch (error) {
|
|
4846
|
-
return {
|
|
4847
|
-
ok: false,
|
|
4848
|
-
command: "commit",
|
|
4849
|
-
code: "no_staged_boundary",
|
|
4850
|
-
message: `unable to inspect staged boundary: ${toErrorMessage(error)}`
|
|
4851
|
-
};
|
|
4852
|
-
}
|
|
4853
|
-
if (stagedPaths.length === 0) {
|
|
4854
|
-
return {
|
|
4855
|
-
ok: false,
|
|
4856
|
-
command: "commit",
|
|
4857
|
-
code: "no_staged_boundary",
|
|
4858
|
-
message: "no staged boundary exists"
|
|
4859
|
-
};
|
|
4860
|
-
}
|
|
4861
|
-
const headBefore = await readHead();
|
|
4862
|
-
const commitResult = await runGitCommit(preparedMessage);
|
|
4863
|
-
if (!commitResult.ok) {
|
|
4864
|
-
return {
|
|
4865
|
-
ok: false,
|
|
4866
|
-
command: "commit",
|
|
4867
|
-
code: "git_commit_failed",
|
|
4868
|
-
message: commitResult.message
|
|
4869
|
-
};
|
|
4870
|
-
}
|
|
4871
|
-
let headAfter;
|
|
4872
|
-
let observedMessage;
|
|
4873
|
-
let committedPaths;
|
|
4874
|
-
let remainingWorktreePaths;
|
|
4875
|
-
try {
|
|
4876
|
-
const [head, observed] = await Promise.all([
|
|
4877
|
-
execFileAsync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }),
|
|
4878
|
-
execFileAsync("git", ["show", "-s", "--format=%B%x00", "HEAD"], { encoding: "utf8" })
|
|
4879
|
-
]);
|
|
4880
|
-
headAfter = head.stdout.trim();
|
|
4881
|
-
observedMessage = extractCommittedMessage(observed.stdout);
|
|
4882
|
-
[committedPaths, remainingWorktreePaths] = await Promise.all([
|
|
4883
|
-
readCommittedPaths(headAfter),
|
|
4884
|
-
readRemainingWorktreePaths()
|
|
4885
|
-
]);
|
|
4886
|
-
} catch (error) {
|
|
4887
|
-
return {
|
|
4888
|
-
ok: false,
|
|
4889
|
-
command: "commit",
|
|
4890
|
-
code: "receipt_observation_failed",
|
|
4891
|
-
message: `commit created but its complete receipt could not be observed: ${toErrorMessage(error)}`,
|
|
4892
|
-
headBefore,
|
|
4893
|
-
preparedLength: preparedMessage.length
|
|
4894
|
-
};
|
|
4895
|
-
}
|
|
4896
|
-
if (!messagesMatch(preparedMessage, observedMessage)) {
|
|
4897
|
-
return {
|
|
4898
|
-
ok: false,
|
|
4899
|
-
command: "commit",
|
|
4900
|
-
code: "message_mismatch",
|
|
4901
|
-
message: "committed message differs from the prepared message; stopped without amend or a second commit",
|
|
4902
|
-
commit: headAfter,
|
|
4903
|
-
headBefore,
|
|
4904
|
-
headAfter,
|
|
4905
|
-
storedMessage: observedMessage,
|
|
4906
|
-
committedPaths,
|
|
4907
|
-
remainingWorktreePaths,
|
|
4908
|
-
preparedLength: preparedMessage.length,
|
|
4909
|
-
observedLength: observedMessage.length
|
|
4910
|
-
};
|
|
4911
|
-
}
|
|
4912
|
-
if (!samePaths(stagedPaths, committedPaths)) {
|
|
4913
|
-
return {
|
|
4914
|
-
ok: false,
|
|
4915
|
-
command: "commit",
|
|
4916
|
-
code: "commit_boundary_mismatch",
|
|
4917
|
-
message: "committed paths differ from the reviewed staged boundary; stopped without amend or a second commit",
|
|
4918
|
-
commit: headAfter,
|
|
4919
|
-
headBefore,
|
|
4920
|
-
headAfter,
|
|
4921
|
-
storedMessage: observedMessage,
|
|
4922
|
-
committedPaths,
|
|
4923
|
-
remainingWorktreePaths
|
|
4924
|
-
};
|
|
4925
|
-
}
|
|
4926
|
-
return {
|
|
4927
|
-
ok: true,
|
|
4928
|
-
command: "commit",
|
|
4929
|
-
commit: headAfter,
|
|
4930
|
-
headBefore,
|
|
4931
|
-
headAfter,
|
|
4932
|
-
storedMessage: observedMessage,
|
|
4933
|
-
committedPaths,
|
|
4934
|
-
remainingWorktreePaths
|
|
4935
|
-
};
|
|
4936
|
-
}
|
|
4937
|
-
|
|
4938
3809
|
// src/commands/create.ts
|
|
4939
|
-
import { existsSync as
|
|
4940
|
-
import { mkdir as
|
|
4941
|
-
import { dirname as
|
|
3810
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3811
|
+
import { mkdir as mkdir6, unlink as unlink5, writeFile as writeFile4 } from "fs/promises";
|
|
3812
|
+
import { dirname as dirname4 } from "path";
|
|
4942
3813
|
async function createChange(name, summary = "", kind, options = {}) {
|
|
4943
3814
|
if (!name) {
|
|
4944
3815
|
return { ok: false, kind: "usage", message: "rsp create <name> [summary]" };
|
|
@@ -4948,31 +3819,31 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
4948
3819
|
if (options.issueRelation && !hasIssueOption)
|
|
4949
3820
|
throw new Error("--issue-relation requires --issue");
|
|
4950
3821
|
const issues = options.issue !== void 0 ? [createIssueRelationship(options.issue, options.issueRelation)] : [];
|
|
4951
|
-
const initialization =
|
|
3822
|
+
const initialization = inspectRspInitialization();
|
|
4952
3823
|
if (initialization)
|
|
4953
3824
|
return { ok: false, kind: "error", message: initialization, warning };
|
|
4954
3825
|
try {
|
|
4955
3826
|
return await withRspLock("create-change", async () => {
|
|
4956
3827
|
const workRef = await resolveExecutableChange(name);
|
|
4957
3828
|
const changePath = workRef.path;
|
|
4958
|
-
const existed =
|
|
3829
|
+
const existed = existsSync5(changePath);
|
|
4959
3830
|
if (existed && hasIssueOption)
|
|
4960
3831
|
throw new Error(`Change already exists; --issue cannot update ${workRef.name}`);
|
|
4961
3832
|
if (!existed) {
|
|
4962
3833
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
4963
3834
|
const content = generateChangeContent(workRef.name, summary, kind, { issues });
|
|
4964
|
-
await
|
|
3835
|
+
await mkdir6(dirname4(changePath), { recursive: true });
|
|
4965
3836
|
let changeCreated = false;
|
|
4966
3837
|
try {
|
|
4967
|
-
await
|
|
3838
|
+
await writeFile4(changePath, content, { flag: "wx" });
|
|
4968
3839
|
changeCreated = true;
|
|
4969
|
-
await
|
|
4970
|
-
await
|
|
3840
|
+
await mkdir6(FOCUS_DIR, { recursive: true });
|
|
3841
|
+
await mkdir6(dirname4(focusEntry), { recursive: true });
|
|
4971
3842
|
await writeManagedFile(focusEntry, "", "focus marker");
|
|
4972
3843
|
} catch (error) {
|
|
4973
3844
|
if (changeCreated) {
|
|
4974
3845
|
try {
|
|
4975
|
-
await
|
|
3846
|
+
await unlink5(changePath);
|
|
4976
3847
|
await cleanupEmptyParentDirs(changePath, CHANGES_DIR);
|
|
4977
3848
|
} catch {
|
|
4978
3849
|
}
|
|
@@ -4989,35 +3860,26 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
4989
3860
|
throw error;
|
|
4990
3861
|
}
|
|
4991
3862
|
}
|
|
4992
|
-
function inspectInitialization2() {
|
|
4993
|
-
const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
4994
|
-
const design = inspectManagedFile(`${RSP_DIR}/specs/design.md`, "design Spec", { allowMissing: true });
|
|
4995
|
-
if (!rules.issue && !design.issue && rules.exists && design.exists)
|
|
4996
|
-
return void 0;
|
|
4997
|
-
const initialized = existsSync7(RSP_DIR);
|
|
4998
|
-
return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
4999
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}`;
|
|
5000
|
-
}
|
|
5001
3863
|
|
|
5002
3864
|
// src/commands/focus.ts
|
|
5003
|
-
import { Buffer as
|
|
5004
|
-
import { randomUUID
|
|
5005
|
-
import { existsSync as
|
|
5006
|
-
import { mkdir as
|
|
5007
|
-
import { basename as basename3, dirname as
|
|
3865
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
3866
|
+
import { randomUUID } from "crypto";
|
|
3867
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3868
|
+
import { mkdir as mkdir7, open, rename as rename4, unlink as unlink6 } from "fs/promises";
|
|
3869
|
+
import { basename as basename3, dirname as dirname5, join as join10 } from "path";
|
|
5008
3870
|
async function focusChange(name, options = {}) {
|
|
5009
3871
|
if (!name) {
|
|
5010
3872
|
return { ok: false, kind: "usage", message: "rsp focus <name>" };
|
|
5011
3873
|
}
|
|
5012
|
-
const initialization =
|
|
3874
|
+
const initialization = inspectRspInitialization();
|
|
5013
3875
|
if (initialization)
|
|
5014
3876
|
return { ok: false, kind: "error", message: initialization };
|
|
5015
3877
|
try {
|
|
5016
3878
|
return await withRspLock("focus-change", async () => {
|
|
5017
3879
|
const workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
5018
3880
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
5019
|
-
await
|
|
5020
|
-
await
|
|
3881
|
+
await mkdir7(FOCUS_DIR, { recursive: true });
|
|
3882
|
+
await mkdir7(dirname5(focusEntry), { recursive: true });
|
|
5021
3883
|
if (options.capsuleFile === void 0)
|
|
5022
3884
|
await ensureManagedFile(focusEntry, "", "focus marker");
|
|
5023
3885
|
else
|
|
@@ -5039,10 +3901,10 @@ async function readCapsule(source) {
|
|
|
5039
3901
|
}
|
|
5040
3902
|
async function replaceFocusCapsule(path, content) {
|
|
5041
3903
|
requireManagedFile(path, "focus marker", { allowMissing: true });
|
|
5042
|
-
const temporaryPath =
|
|
3904
|
+
const temporaryPath = join10(dirname5(path), `.${basename3(path)}.${randomUUID()}.tmp`);
|
|
5043
3905
|
let handle;
|
|
5044
3906
|
try {
|
|
5045
|
-
handle = await
|
|
3907
|
+
handle = await open(temporaryPath, "wx");
|
|
5046
3908
|
await handle.writeFile(content, "utf-8");
|
|
5047
3909
|
await handle.close();
|
|
5048
3910
|
handle = void 0;
|
|
@@ -5051,7 +3913,7 @@ async function replaceFocusCapsule(path, content) {
|
|
|
5051
3913
|
} finally {
|
|
5052
3914
|
await handle?.close().catch(() => {
|
|
5053
3915
|
});
|
|
5054
|
-
await
|
|
3916
|
+
await unlink6(temporaryPath).catch((error) => {
|
|
5055
3917
|
if (error.code !== "ENOENT")
|
|
5056
3918
|
throw error;
|
|
5057
3919
|
});
|
|
@@ -5059,7 +3921,7 @@ async function replaceFocusCapsule(path, content) {
|
|
|
5059
3921
|
}
|
|
5060
3922
|
async function readRegularCapsuleFile(path) {
|
|
5061
3923
|
requireManagedFile(path, "focus capsule input");
|
|
5062
|
-
const handle = await
|
|
3924
|
+
const handle = await open(path, "r");
|
|
5063
3925
|
try {
|
|
5064
3926
|
if (!(await handle.stat()).isFile())
|
|
5065
3927
|
throw new Error(`focus capsule input must be a regular file: ${path}`);
|
|
@@ -5072,28 +3934,28 @@ async function readBoundedStdin() {
|
|
|
5072
3934
|
const chunks = [];
|
|
5073
3935
|
let size = 0;
|
|
5074
3936
|
for await (const chunk of process.stdin) {
|
|
5075
|
-
const buffer =
|
|
3937
|
+
const buffer = Buffer2.isBuffer(chunk) ? chunk : Buffer2.from(chunk);
|
|
5076
3938
|
size += buffer.byteLength;
|
|
5077
3939
|
if (size > MAX_FOCUS_CAPSULE_BYTES)
|
|
5078
3940
|
throw new Error(`focus capsule exceeds ${MAX_FOCUS_CAPSULE_BYTES} UTF-8 bytes`);
|
|
5079
3941
|
chunks.push(buffer);
|
|
5080
3942
|
}
|
|
5081
|
-
return
|
|
3943
|
+
return Buffer2.concat(chunks, size);
|
|
5082
3944
|
}
|
|
5083
3945
|
async function unfocusChange(name) {
|
|
5084
3946
|
if (!name) {
|
|
5085
3947
|
return { ok: false, kind: "usage", message: "rsp unfocus <name>" };
|
|
5086
3948
|
}
|
|
5087
|
-
const initialization =
|
|
3949
|
+
const initialization = inspectRspInitialization();
|
|
5088
3950
|
if (initialization)
|
|
5089
3951
|
return { ok: false, kind: "error", message: initialization };
|
|
5090
3952
|
try {
|
|
5091
3953
|
return await withRspLock("unfocus-change", async () => {
|
|
5092
3954
|
const workRef = resolveWorkRef(name, { executable: false, mustExist: false });
|
|
5093
3955
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
5094
|
-
if (!
|
|
3956
|
+
if (!existsSync6(focusEntry))
|
|
5095
3957
|
throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
|
|
5096
|
-
await
|
|
3958
|
+
await unlink6(focusEntry);
|
|
5097
3959
|
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
5098
3960
|
return { ok: true, action: "unfocus", workRef: workRef.name };
|
|
5099
3961
|
});
|
|
@@ -5109,21 +3971,12 @@ function workRefFailure(error) {
|
|
|
5109
3971
|
}
|
|
5110
3972
|
throw error;
|
|
5111
3973
|
}
|
|
5112
|
-
function inspectInitialization3() {
|
|
5113
|
-
const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
5114
|
-
const design = inspectManagedFile(`${RSP_DIR}/specs/design.md`, "design Spec", { allowMissing: true });
|
|
5115
|
-
if (!rules.issue && !design.issue && rules.exists && design.exists)
|
|
5116
|
-
return void 0;
|
|
5117
|
-
const initialized = existsSync8(RSP_DIR);
|
|
5118
|
-
return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
5119
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}`;
|
|
5120
|
-
}
|
|
5121
3974
|
|
|
5122
3975
|
// src/commands/group.ts
|
|
5123
|
-
import { randomUUID as
|
|
5124
|
-
import { existsSync as
|
|
5125
|
-
import { link as
|
|
5126
|
-
import { join as
|
|
3976
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3977
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3978
|
+
import { link as link2, mkdir as mkdir8, readdir as readdir4, readFile as readFile14, rename as rename5, unlink as unlink7, writeFile as writeFile5 } from "fs/promises";
|
|
3979
|
+
import { join as join11 } from "path";
|
|
5127
3980
|
var ChangeGroupError = class extends Error {
|
|
5128
3981
|
name = "ChangeGroupError";
|
|
5129
3982
|
};
|
|
@@ -5131,7 +3984,7 @@ async function createChangeGroup(name, goal = "") {
|
|
|
5131
3984
|
if (!name) {
|
|
5132
3985
|
return failure("usage", "rsp group create <name> [goal]");
|
|
5133
3986
|
}
|
|
5134
|
-
const initialization =
|
|
3987
|
+
const initialization = inspectRspInitialization();
|
|
5135
3988
|
if (initialization)
|
|
5136
3989
|
return failure("error", initialization);
|
|
5137
3990
|
try {
|
|
@@ -5143,9 +3996,9 @@ async function createChangeGroup(name, goal = "") {
|
|
|
5143
3996
|
if (await hasArchivedGroupBrief(groupName))
|
|
5144
3997
|
throw new ChangeGroupError(`archived Change Group cannot be reopened: ${groupName}`);
|
|
5145
3998
|
const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [groupName], "Change Group path");
|
|
5146
|
-
await
|
|
3999
|
+
await mkdir8(groupDirectory, { recursive: true });
|
|
5147
4000
|
try {
|
|
5148
|
-
await
|
|
4001
|
+
await writeFile5(ref.path, generateGroupBriefContent(groupName, goal), { flag: "wx" });
|
|
5149
4002
|
} catch (error) {
|
|
5150
4003
|
if (error.code === "EEXIST")
|
|
5151
4004
|
throw new ChangeGroupError(`Change Group already exists: ${groupName}`);
|
|
@@ -5164,7 +4017,7 @@ async function closeChangeGroup(name) {
|
|
|
5164
4017
|
if (!name) {
|
|
5165
4018
|
return failure("usage", "rsp group close <name>");
|
|
5166
4019
|
}
|
|
5167
|
-
const initialization =
|
|
4020
|
+
const initialization = inspectRspInitialization();
|
|
5168
4021
|
if (initialization)
|
|
5169
4022
|
return failure("error", initialization);
|
|
5170
4023
|
try {
|
|
@@ -5183,15 +4036,15 @@ async function closeChangeGroup(name) {
|
|
|
5183
4036
|
const archiveDirectory = resolveArchiveDirectory(ref);
|
|
5184
4037
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5185
4038
|
const archiveName = resolveArchiveName(archiveDirectory, date, "brief");
|
|
5186
|
-
await
|
|
5187
|
-
await rename5(ref.path,
|
|
4039
|
+
await mkdir8(archiveDirectory, { recursive: true });
|
|
4040
|
+
await rename5(ref.path, join11(archiveDirectory, archiveName));
|
|
5188
4041
|
const warnings = [];
|
|
5189
4042
|
try {
|
|
5190
4043
|
await cleanupEmptyParentDirs(ref.path, CHANGES_DIR);
|
|
5191
4044
|
} catch (error) {
|
|
5192
4045
|
warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
5193
4046
|
}
|
|
5194
|
-
return { ok: true, action: "close", groupName, archivePath:
|
|
4047
|
+
return { ok: true, action: "close", groupName, archivePath: join11(archiveDirectory, archiveName), warnings };
|
|
5195
4048
|
});
|
|
5196
4049
|
} catch (error) {
|
|
5197
4050
|
if (error instanceof WorkRefError || error instanceof ChangeGroupError) {
|
|
@@ -5205,18 +4058,18 @@ async function reopenChangeGroup(name, options) {
|
|
|
5205
4058
|
return failure("usage", "rsp group reopen <name> --reason <text> [--from <archive-path>]");
|
|
5206
4059
|
}
|
|
5207
4060
|
const reason = options.reason?.trim();
|
|
5208
|
-
if (!reason || /[\r\n]/.test(reason)) {
|
|
4061
|
+
if (!reason || /[\r\n]/.test(reason) || !markdownText(parseMarkdown(reason).root).trim()) {
|
|
5209
4062
|
return failure("error", "--reason must be one non-empty line");
|
|
5210
4063
|
}
|
|
5211
|
-
const initialization =
|
|
4064
|
+
const initialization = inspectRspInitialization();
|
|
5212
4065
|
if (initialization)
|
|
5213
4066
|
return failure("error", initialization);
|
|
5214
4067
|
try {
|
|
5215
4068
|
return await withRspLock("reopen-change-group", async () => {
|
|
5216
4069
|
const warnings = [];
|
|
5217
4070
|
const groupName = normalizeWorkRefSegment(name);
|
|
5218
|
-
await requireEmptyGroupRecoverySubtree(
|
|
5219
|
-
await requireEmptyGroupRecoverySubtree(
|
|
4071
|
+
await requireEmptyGroupRecoverySubtree(join11(CHANGES_DIR, groupName), `Change Group work subtree for ${groupName}`);
|
|
4072
|
+
await requireEmptyGroupRecoverySubtree(join11(FOCUS_DIR, groupName), `Change Group focus subtree for ${groupName}`);
|
|
5220
4073
|
const ref = resolveWorkRef(`${groupName}/brief`);
|
|
5221
4074
|
if (ref.kind !== "group-brief")
|
|
5222
4075
|
throw new ChangeGroupError(`invalid Change Group name: ${name}`);
|
|
@@ -5237,16 +4090,16 @@ async function reopenChangeGroup(name, options) {
|
|
|
5237
4090
|
`selected archived Change Group belongs to ${record.group}, not ${groupName}`
|
|
5238
4091
|
);
|
|
5239
4092
|
}
|
|
5240
|
-
if (
|
|
4093
|
+
if (existsSync7(ref.path))
|
|
5241
4094
|
throw new ArchiveHistoryError("open_group_exists", `open Change Group already exists: ${groupName}`);
|
|
5242
|
-
const archivedContent = await
|
|
4095
|
+
const archivedContent = await readFile14(record.sourcePath, "utf-8");
|
|
5243
4096
|
const document = parseRspDocument(archivedContent, GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
5244
4097
|
validateArchivedGroupBrief(document, groupName);
|
|
5245
4098
|
const evidenceKey = `${GROUP_REOPEN_COMPLETION_PREFIX} \`${record.path}\`: ${reason}`;
|
|
5246
4099
|
const retainedEvidence = /* @__PURE__ */ new Set();
|
|
5247
4100
|
for (const retained of inspection.groupBriefs.filter((candidate) => candidate.group === groupName)) {
|
|
5248
|
-
const
|
|
5249
|
-
for (const item of getGroupReopenEvidence(
|
|
4101
|
+
const retainedDocument = retained.sourcePath === record.sourcePath ? document : parseRspDocument(await readFile14(retained.sourcePath, "utf-8"), GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
4102
|
+
for (const item of getGroupReopenEvidence(retainedDocument))
|
|
5250
4103
|
retainedEvidence.add(item.key);
|
|
5251
4104
|
}
|
|
5252
4105
|
if (retainedEvidence.has(evidenceKey)) {
|
|
@@ -5261,17 +4114,17 @@ async function reopenChangeGroup(name, options) {
|
|
|
5261
4114
|
`- [ ] ${evidenceKey}`
|
|
5262
4115
|
);
|
|
5263
4116
|
const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [groupName], "Change Group path");
|
|
5264
|
-
await
|
|
5265
|
-
const temporaryPath =
|
|
4117
|
+
await mkdir8(groupDirectory, { recursive: true });
|
|
4118
|
+
const temporaryPath = join11(RSP_DIR, `.group-reopen-${process.pid}-${randomUUID2()}.tmp`);
|
|
5266
4119
|
let mutationError;
|
|
5267
4120
|
try {
|
|
5268
|
-
await
|
|
5269
|
-
await
|
|
4121
|
+
await writeFile5(temporaryPath, reopenedContent, { flag: "wx" });
|
|
4122
|
+
await link2(temporaryPath, ref.path);
|
|
5270
4123
|
} catch (error) {
|
|
5271
4124
|
mutationError = error;
|
|
5272
4125
|
}
|
|
5273
4126
|
try {
|
|
5274
|
-
await
|
|
4127
|
+
await unlink7(temporaryPath);
|
|
5275
4128
|
} catch (error) {
|
|
5276
4129
|
if (!mutationError)
|
|
5277
4130
|
warnings.push(`temporary reopen file cleanup failed: ${toErrorMessage(error)}`);
|
|
@@ -5302,21 +4155,12 @@ async function reopenChangeGroup(name, options) {
|
|
|
5302
4155
|
function failure(kind, message, candidates = [], remainingCandidates = 0) {
|
|
5303
4156
|
return { ok: false, kind, message, candidates, remainingCandidates };
|
|
5304
4157
|
}
|
|
5305
|
-
function inspectInitialization4() {
|
|
5306
|
-
const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
5307
|
-
const design = inspectManagedFile(join9(RSP_DIR, "specs", "design.md"), "design Spec", { allowMissing: true });
|
|
5308
|
-
if (!rules.issue && !design.issue && rules.exists && design.exists)
|
|
5309
|
-
return void 0;
|
|
5310
|
-
const initialized = existsSync9(RSP_DIR);
|
|
5311
|
-
return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
5312
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}`;
|
|
5313
|
-
}
|
|
5314
4158
|
async function requireEmptyGroupRecoverySubtree(path, label) {
|
|
5315
4159
|
if (!requireManagedDirectory(path, label, { allowMissing: true }))
|
|
5316
4160
|
return;
|
|
5317
4161
|
let entries;
|
|
5318
4162
|
try {
|
|
5319
|
-
entries = await
|
|
4163
|
+
entries = await readdir4(path);
|
|
5320
4164
|
} catch (error) {
|
|
5321
4165
|
throw new ChangeGroupError(`unable to inspect ${label}: ${toErrorMessage(error)}`);
|
|
5322
4166
|
}
|
|
@@ -5329,7 +4173,7 @@ function validateArchivedGroupBrief(document, group) {
|
|
|
5329
4173
|
throw new ArchiveHistoryError("archive_heading_invalid", `archived Change Group must contain exactly one canonical identity heading for ${group}`);
|
|
5330
4174
|
for (const section of GROUP_BRIEF_DOCUMENT_SCHEMA.sections) {
|
|
5331
4175
|
const matches = getDocumentSections(document, section.id);
|
|
5332
|
-
if (matches.length !== 1 || !matches[0].canonical ||
|
|
4176
|
+
if (matches.length !== 1 || !matches[0].canonical || getVisibleBodyLines(getDocumentSectionMarkdown(document, section.id)).length === 0) {
|
|
5333
4177
|
throw new ArchiveHistoryError(
|
|
5334
4178
|
"archive_section_invalid",
|
|
5335
4179
|
`archived Change Group must contain exactly one canonical required section: ${section.heading}`
|
|
@@ -5339,10 +4183,10 @@ function validateArchivedGroupBrief(document, group) {
|
|
|
5339
4183
|
}
|
|
5340
4184
|
function collectCloseReasons(group, errors) {
|
|
5341
4185
|
const reasons = [...errors, ...group.warnings];
|
|
5342
|
-
const
|
|
4186
|
+
const open2 = group.slices.filter((slice) => slice.state === "open").map((slice) => slice.name);
|
|
5343
4187
|
const missing = group.slices.filter((slice) => slice.state === "missing").map((slice) => slice.name);
|
|
5344
|
-
if (
|
|
5345
|
-
reasons.push(`open slices: ${
|
|
4188
|
+
if (open2.length > 0)
|
|
4189
|
+
reasons.push(`open slices: ${open2.join(", ")}`);
|
|
5346
4190
|
if (missing.length > 0)
|
|
5347
4191
|
reasons.push(`missing slices: ${missing.join(", ")}`);
|
|
5348
4192
|
if (group.completion.total === 0)
|
|
@@ -5355,18 +4199,18 @@ function collectCloseReasons(group, errors) {
|
|
|
5355
4199
|
}
|
|
5356
4200
|
|
|
5357
4201
|
// src/commands/reopen.ts
|
|
5358
|
-
import { existsSync as
|
|
5359
|
-
import { mkdir as
|
|
5360
|
-
import { dirname as
|
|
4202
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4203
|
+
import { mkdir as mkdir9, readFile as readFile15, unlink as unlink8, writeFile as writeFile6 } from "fs/promises";
|
|
4204
|
+
import { dirname as dirname6 } from "path";
|
|
5361
4205
|
async function reopenChange(name, options) {
|
|
5362
4206
|
if (!name) {
|
|
5363
4207
|
return { ok: false, kind: "usage", message: "rsp reopen <name> --reason <text> [--from <archive-path>]", candidates: [], remainingCandidates: 0 };
|
|
5364
4208
|
}
|
|
5365
4209
|
const reason = options.reason?.trim();
|
|
5366
|
-
if (!reason || /[\r\n]/.test(reason)) {
|
|
4210
|
+
if (!reason || /[\r\n]/.test(reason) || !markdownText(parseMarkdown(reason).root).trim()) {
|
|
5367
4211
|
return { ok: false, kind: "error", message: "--reason must be one non-empty line", candidates: [], remainingCandidates: 0 };
|
|
5368
4212
|
}
|
|
5369
|
-
const initialization =
|
|
4213
|
+
const initialization = inspectRspInitialization();
|
|
5370
4214
|
if (initialization)
|
|
5371
4215
|
return { ok: false, kind: "error", message: initialization, candidates: [], remainingCandidates: 0 };
|
|
5372
4216
|
try {
|
|
@@ -5401,36 +4245,36 @@ async function reopenChange(name, options) {
|
|
|
5401
4245
|
}
|
|
5402
4246
|
throw error;
|
|
5403
4247
|
}
|
|
5404
|
-
if (
|
|
4248
|
+
if (existsSync8(workRef.path))
|
|
5405
4249
|
throw new ArchiveHistoryError("open_change_exists", `open Change already exists: ${name}`);
|
|
5406
4250
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
5407
|
-
if (
|
|
4251
|
+
if (existsSync8(focusEntry))
|
|
5408
4252
|
throw new ArchiveHistoryError("focus_marker_exists", `focus marker already exists for non-open Change: ${name}; run rsp doctor`);
|
|
5409
|
-
const archivedContent = await
|
|
4253
|
+
const archivedContent = await readFile15(record.sourcePath, "utf-8");
|
|
5410
4254
|
const reopenedContent = appendChecklistItem(
|
|
5411
4255
|
appendChecklistItem(archivedContent, "tasks", `- [ ] Resolve reopened concern: ${reason}`),
|
|
5412
4256
|
"verify",
|
|
5413
4257
|
`- [ ] Verify reopened concern: ${reason}`
|
|
5414
4258
|
);
|
|
5415
|
-
await
|
|
4259
|
+
await mkdir9(dirname6(workRef.path), { recursive: true });
|
|
5416
4260
|
let changeCreated = false;
|
|
5417
4261
|
try {
|
|
5418
|
-
await
|
|
4262
|
+
await writeFile6(workRef.path, reopenedContent, { flag: "wx" });
|
|
5419
4263
|
changeCreated = true;
|
|
5420
|
-
await
|
|
5421
|
-
await
|
|
4264
|
+
await mkdir9(FOCUS_DIR, { recursive: true });
|
|
4265
|
+
await mkdir9(dirname6(focusEntry), { recursive: true });
|
|
5422
4266
|
await writeManagedFile(focusEntry, "", "focus marker");
|
|
5423
4267
|
} catch (error) {
|
|
5424
4268
|
if (changeCreated) {
|
|
5425
4269
|
try {
|
|
5426
|
-
await
|
|
4270
|
+
await unlink8(workRef.path);
|
|
5427
4271
|
await cleanupEmptyParentDirs(workRef.path, CHANGES_DIR);
|
|
5428
4272
|
} catch {
|
|
5429
4273
|
}
|
|
5430
4274
|
}
|
|
5431
4275
|
throw error;
|
|
5432
4276
|
}
|
|
5433
|
-
return { ok: true, workRef: workRef.name, archivePath: record.path, git:
|
|
4277
|
+
return { ok: true, workRef: workRef.name, archivePath: record.path, git: existsSync8(".git") };
|
|
5434
4278
|
});
|
|
5435
4279
|
} catch (error) {
|
|
5436
4280
|
if (error instanceof ArchiveHistoryError || error instanceof WorkRefError) {
|
|
@@ -5445,15 +4289,6 @@ async function reopenChange(name, options) {
|
|
|
5445
4289
|
throw error;
|
|
5446
4290
|
}
|
|
5447
4291
|
}
|
|
5448
|
-
function inspectInitialization5() {
|
|
5449
|
-
const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
5450
|
-
const design = inspectManagedFile(`${RSP_DIR}/specs/design.md`, "design Spec", { allowMissing: true });
|
|
5451
|
-
if (!rules.issue && !design.issue && rules.exists && design.exists)
|
|
5452
|
-
return void 0;
|
|
5453
|
-
const initialized = existsSync10(RSP_DIR);
|
|
5454
|
-
return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
5455
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}`;
|
|
5456
|
-
}
|
|
5457
4292
|
function appendChecklistItem(content, sectionId, item) {
|
|
5458
4293
|
try {
|
|
5459
4294
|
return appendDocumentSectionItem(parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), sectionId, item);
|
|
@@ -5468,25 +4303,6 @@ function appendChecklistItem(content, sectionId, item) {
|
|
|
5468
4303
|
}
|
|
5469
4304
|
}
|
|
5470
4305
|
|
|
5471
|
-
// src/cli/presenters/commit.ts
|
|
5472
|
-
function presentCommit(result, json) {
|
|
5473
|
-
if (json) {
|
|
5474
|
-
emitJson(result);
|
|
5475
|
-
return;
|
|
5476
|
-
}
|
|
5477
|
-
if (!result.ok) {
|
|
5478
|
-
console.error(` Commit stopped: ${result.message}`);
|
|
5479
|
-
return;
|
|
5480
|
-
}
|
|
5481
|
-
console.log(` Committed: ${result.commit}`);
|
|
5482
|
-
console.log(` HEAD: ${result.headBefore ?? "(unborn)"} -> ${result.headAfter}`);
|
|
5483
|
-
console.log(` committed paths: ${result.committedPaths?.join(", ") || "none"}`);
|
|
5484
|
-
console.log(` remaining worktree paths: ${result.remainingWorktreePaths?.join(", ") || "none"}`);
|
|
5485
|
-
console.log(" stored message:");
|
|
5486
|
-
for (const line of (result.storedMessage ?? "").split("\n"))
|
|
5487
|
-
console.log(` ${line}`);
|
|
5488
|
-
}
|
|
5489
|
-
|
|
5490
4306
|
// src/cli/presenters/work.ts
|
|
5491
4307
|
function presentFailure(result) {
|
|
5492
4308
|
const label = result.kind === "usage" ? "Usage:" : "Error:";
|
|
@@ -5794,30 +4610,8 @@ var unfocusCommand = defineCommand4({
|
|
|
5794
4610
|
await executeCliCommand({ execute: () => unfocusChange(args.name), present: presentFocus, exitCode: (result) => result.ok ? void 0 : 1 }, args);
|
|
5795
4611
|
}
|
|
5796
4612
|
});
|
|
5797
|
-
var commitCommand = defineCommand4({
|
|
5798
|
-
meta: {
|
|
5799
|
-
name: "commit",
|
|
5800
|
-
description: "Create one local commit from a message file on the existing staged boundary"
|
|
5801
|
-
},
|
|
5802
|
-
args: {
|
|
5803
|
-
"message-file": {
|
|
5804
|
-
type: "string",
|
|
5805
|
-
description: "Path to the prepared commit message file",
|
|
5806
|
-
required: true
|
|
5807
|
-
},
|
|
5808
|
-
...jsonArgs
|
|
5809
|
-
},
|
|
5810
|
-
async run({ args }) {
|
|
5811
|
-
await executeCliCommand({
|
|
5812
|
-
execute: () => commitFromMessageFile(args["message-file"]),
|
|
5813
|
-
present: (result) => presentCommit(result, Boolean(args.json)),
|
|
5814
|
-
exitCode: (result) => result.ok ? void 0 : 1
|
|
5815
|
-
}, args);
|
|
5816
|
-
}
|
|
5817
|
-
});
|
|
5818
4613
|
function createWorkCommands(deprecatedLite) {
|
|
5819
4614
|
return {
|
|
5820
|
-
commit: commitCommand,
|
|
5821
4615
|
create: createCreateCommand(deprecatedLite),
|
|
5822
4616
|
group: groupCommand,
|
|
5823
4617
|
focus: focusCommand,
|
|
@@ -5840,7 +4634,7 @@ function createRootCommand(options) {
|
|
|
5840
4634
|
init: setupCommands.init,
|
|
5841
4635
|
add: setupCommands.add,
|
|
5842
4636
|
specs: inspectionCommands.specs,
|
|
5843
|
-
|
|
4637
|
+
config: inspectionCommands.config,
|
|
5844
4638
|
create: workCommands.create,
|
|
5845
4639
|
group: workCommands.group,
|
|
5846
4640
|
focus: workCommands.focus,
|
|
@@ -5866,9 +4660,6 @@ function isInteractiveTerminal(environment) {
|
|
|
5866
4660
|
function shouldAutoLaunchUi(args, environment) {
|
|
5867
4661
|
return args.length === 0 && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
|
|
5868
4662
|
}
|
|
5869
|
-
function shouldLaunchSkillsUi(args, environment) {
|
|
5870
|
-
return args.length === 1 && args[0] === "skills" && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
|
|
5871
|
-
}
|
|
5872
4663
|
function validateUiArgs(args) {
|
|
5873
4664
|
let lang = "auto";
|
|
5874
4665
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -5908,6 +4699,7 @@ function normalizeLegacyCreateOptions(rawArgs) {
|
|
|
5908
4699
|
};
|
|
5909
4700
|
}
|
|
5910
4701
|
async function runCli(rawArgs = process.argv.slice(2)) {
|
|
4702
|
+
await guardProjectVersion(rawArgs);
|
|
5911
4703
|
const terminalEnvironment = {
|
|
5912
4704
|
stdinTty: Boolean(process.stdin.isTTY),
|
|
5913
4705
|
stdoutTty: Boolean(process.stdout.isTTY),
|
|
@@ -5915,19 +4707,6 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
5915
4707
|
ci: process.env.CI
|
|
5916
4708
|
};
|
|
5917
4709
|
const explicitUi = rawArgs[0] === "ui";
|
|
5918
|
-
if (shouldLaunchSkillsUi(rawArgs, terminalEnvironment)) {
|
|
5919
|
-
const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
|
|
5920
|
-
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);
|
|
5921
|
-
const { runSkillsTui } = await import("./entry-YAW3PYFA.mjs");
|
|
5922
|
-
const selection = await runSkillsTui(locale);
|
|
5923
|
-
if (selection.kind === "confirmed") {
|
|
5924
|
-
const result = await installPackagedSkills({ names: selection.names, force: selection.force });
|
|
5925
|
-
presentSkillInstallResult(result);
|
|
5926
|
-
} else if (selection.kind === "error") {
|
|
5927
|
-
process.exitCode = 1;
|
|
5928
|
-
}
|
|
5929
|
-
return;
|
|
5930
|
-
}
|
|
5931
4710
|
if (explicitUi || shouldAutoLaunchUi(rawArgs, terminalEnvironment)) {
|
|
5932
4711
|
const { lang } = validateUiArgs(explicitUi ? rawArgs.slice(1) : []);
|
|
5933
4712
|
if (!isInteractiveTerminal(terminalEnvironment)) {
|
|
@@ -5936,7 +4715,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
5936
4715
|
} else {
|
|
5937
4716
|
const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
|
|
5938
4717
|
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);
|
|
5939
|
-
const { runTui } = await import("./entry-
|
|
4718
|
+
const { runTui } = await import("./entry-RIJQ2X46.mjs");
|
|
5940
4719
|
const exitCode = await runTui(locale);
|
|
5941
4720
|
if (exitCode !== 0)
|
|
5942
4721
|
process.exitCode = exitCode;
|
|
@@ -5961,7 +4740,7 @@ async function runCliMain(rawArgs = process.argv.slice(2)) {
|
|
|
5961
4740
|
process.exitCode = 1;
|
|
5962
4741
|
}
|
|
5963
4742
|
}
|
|
5964
|
-
var isMain = process.argv[1] && fileURLToPath(import.meta.url) ===
|
|
4743
|
+
var isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve2(process.argv[1]);
|
|
5965
4744
|
if (isMain)
|
|
5966
4745
|
await runCliMain();
|
|
5967
4746
|
export {
|