@ai-dev-methodologies/rlp-desk 0.11.1 → 0.13.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/docs/plans/spicy-booping-galaxy.md +322 -0
- package/docs/rlp-desk/artifact-schema.md +99 -0
- package/docs/rlp-desk/ci-setup.md +100 -0
- package/docs/rlp-desk/e2e-scenarios.md +102 -0
- package/docs/rlp-desk/plans/rlp-desk-tmux-flywheel-routing.md +730 -0
- package/install.sh +93 -20
- package/package.json +9 -3
- package/scripts/build-node-manifest.js +52 -0
- package/scripts/postinstall.js +162 -8
- package/src/commands/rlp-desk.md +73 -50
- package/src/governance.md +56 -7
- package/src/node/MANIFEST.txt +15 -0
- package/src/node/cli/command-builder.mjs +43 -5
- package/src/node/constants.mjs +19 -0
- package/src/node/init/campaign-initializer.mjs +100 -10
- package/src/node/polling/signal-poller.mjs +139 -3
- package/src/node/reporting/campaign-reporting.mjs +5 -1
- package/src/node/run.mjs +31 -2
- package/src/node/runner/campaign-main-loop.mjs +521 -44
- package/src/node/runner/leader-registry.mjs +100 -0
- package/src/node/runner/prompt-detector.mjs +41 -0
- package/src/node/runner/prompt-dismisser.mjs +200 -0
- package/src/node/shared/fs.mjs +38 -0
- package/src/node/util/debug-log.mjs +56 -0
- package/src/node/util/desk-root.mjs +24 -0
- package/src/node/util/shell-quote.mjs +12 -0
- package/docs/superpowers/plans/2026-04-24-gpt-5-5-default.md +0 -517
- package/docs/superpowers/specs/2026-04-24-gpt-5-5-default.md +0 -107
- /package/docs/{TODO-verification-next.md → rlp-desk/TODO-verification-next.md} +0 -0
- /package/docs/{architecture.md → rlp-desk/architecture.md} +0 -0
- /package/docs/{blueprints → rlp-desk/blueprints}/blueprint-flywheel-enhancement.md +0 -0
- /package/docs/{blueprints → rlp-desk/blueprints}/blueprint-pivot-step.md +0 -0
- /package/docs/{blueprints → rlp-desk/blueprints}/plan-flywheel-enhancement.md +0 -0
- /package/docs/{blueprints → rlp-desk/blueprints}/sv-architecture-rethink.md +0 -0
- /package/docs/{getting-started.md → rlp-desk/getting-started.md} +0 -0
- /package/docs/{internal → rlp-desk/internal}/verification-policy-gap-analysis.md +0 -0
- /package/docs/{internal → rlp-desk/internal}/verification-strategy-research.md +0 -0
- /package/docs/{multi-mission-orchestration.md → rlp-desk/multi-mission-orchestration.md} +0 -0
- /package/docs/{plans → rlp-desk/plans}/cozy-gliding-trinket.md +0 -0
- /package/docs/{plans → rlp-desk/plans}/frolicking-churning-honey.md +0 -0
- /package/docs/{plans → rlp-desk/plans}/keen-sauteeing-snowflake.md +0 -0
- /package/docs/{plans → rlp-desk/plans}/mutable-booping-corbato.md +0 -0
- /package/docs/{plans → rlp-desk/plans}/rlp-desk-0.11-handoff-7fixes.md +0 -0
- /package/docs/{plans → rlp-desk/plans}/rlp-desk-0.11.1-tmux-pane-disappearance.md +0 -0
- /package/docs/{plans → rlp-desk/plans}/rlp-desk-elegant-papert-agent-a8cd695ffca2a3ad8.md +0 -0
- /package/docs/{plans → rlp-desk/plans}/rlp-desk-elegant-papert.md +0 -0
- /package/docs/{plans → rlp-desk/plans}/toasty-whistling-diffie-agent-a6814625642e956da.md +0 -0
- /package/docs/{plans → rlp-desk/plans}/toasty-whistling-diffie.md +0 -0
- /package/docs/{plans → rlp-desk/plans}/validated-snacking-crayon.md +0 -0
- /package/docs/{protocol-reference.md → rlp-desk/protocol-reference.md} +0 -0
package/install.sh
CHANGED
|
@@ -12,10 +12,24 @@ set -euo pipefail
|
|
|
12
12
|
# Safe to run multiple times (idempotent).
|
|
13
13
|
# =============================================================================
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
# v5.7 §4.4: REPO_URL overridable for offline/local testing (test fixture).
|
|
16
|
+
REPO_URL="${REPO_URL:-https://raw.githubusercontent.com/ai-dev-methodologies/rlp-desk/main}"
|
|
16
17
|
CLAUDE_DIR="$HOME/.claude"
|
|
17
18
|
COMMANDS_DIR="$CLAUDE_DIR/commands"
|
|
18
19
|
DESK_DIR="$CLAUDE_DIR/ralph-desk"
|
|
20
|
+
NODE_DIR="$DESK_DIR/node"
|
|
21
|
+
|
|
22
|
+
# v5.7 §4.4 / Q3: Node ≥16 preflight (matches scripts/postinstall.js policy).
|
|
23
|
+
if command -v node &>/dev/null; then
|
|
24
|
+
NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]" 2>/dev/null || echo 0)
|
|
25
|
+
if [[ "$NODE_MAJOR" -lt 16 ]]; then
|
|
26
|
+
echo " [warn] Node.js >= 16 required for the Node leader (--mode tmux flywheel/SV)."
|
|
27
|
+
echo " Found Node $NODE_MAJOR. Continuing zsh install; --mode tmux features will be unavailable."
|
|
28
|
+
fi
|
|
29
|
+
else
|
|
30
|
+
echo " [warn] node not found in PATH. Node leader features (--flywheel, --with-self-verification in tmux) unavailable."
|
|
31
|
+
echo " Install Node.js >= 16: https://nodejs.org/"
|
|
32
|
+
fi
|
|
19
33
|
|
|
20
34
|
echo ""
|
|
21
35
|
echo " RLP Desk Installer"
|
|
@@ -25,30 +39,89 @@ echo ""
|
|
|
25
39
|
# Create directories
|
|
26
40
|
mkdir -p "$COMMANDS_DIR"
|
|
27
41
|
mkdir -p "$DESK_DIR"
|
|
28
|
-
mkdir -p "$DESK_DIR/docs/internal"
|
|
29
|
-
mkdir -p "$DESK_DIR/docs/blueprints"
|
|
42
|
+
mkdir -p "$DESK_DIR/docs/rlp-desk/internal"
|
|
43
|
+
mkdir -p "$DESK_DIR/docs/rlp-desk/blueprints"
|
|
44
|
+
mkdir -p "$DESK_DIR/docs/rlp-desk/plans"
|
|
45
|
+
mkdir -p "$NODE_DIR"
|
|
46
|
+
|
|
47
|
+
# v5.7 §4.10 helpers — chmod-before-curl (unlock prior install) + chmod a-w
|
|
48
|
+
# (lock down). Hard-fail per Architect (no `2>/dev/null || true` swallowing).
|
|
49
|
+
unlock_target() {
|
|
50
|
+
local target="$1"
|
|
51
|
+
if [[ -e "$target" ]]; then
|
|
52
|
+
chmod u+w "$target" || {
|
|
53
|
+
echo " [install] FATAL: cannot unlock existing $target. Filesystem may be read-only."
|
|
54
|
+
exit 1
|
|
55
|
+
}
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
lock_target() {
|
|
59
|
+
local target="$1"
|
|
60
|
+
if ! chmod a-w "$target" 2>/dev/null; then
|
|
61
|
+
echo " [install] WARNING: chmod a-w failed on $target. Filesystem may not honor POSIX mode bits (WSL1/NTFS); cross-session edit protection unavailable."
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
fetch() {
|
|
65
|
+
local url="$1" target="$2"
|
|
66
|
+
unlock_target "$target"
|
|
67
|
+
curl -fsSL "$url" -o "$target" || {
|
|
68
|
+
echo " [install] FATAL: download failed for $url"
|
|
69
|
+
exit 1
|
|
70
|
+
}
|
|
71
|
+
lock_target "$target"
|
|
72
|
+
}
|
|
30
73
|
|
|
31
74
|
# Runtime files
|
|
32
75
|
echo " Downloading runtime files..."
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
76
|
+
fetch "$REPO_URL/src/commands/rlp-desk.md" "$COMMANDS_DIR/rlp-desk.md"
|
|
77
|
+
fetch "$REPO_URL/src/scripts/init_ralph_desk.zsh" "$DESK_DIR/init_ralph_desk.zsh"
|
|
78
|
+
fetch "$REPO_URL/src/scripts/run_ralph_desk.zsh" "$DESK_DIR/run_ralph_desk.zsh"
|
|
79
|
+
fetch "$REPO_URL/src/scripts/lib_ralph_desk.zsh" "$DESK_DIR/lib_ralph_desk.zsh"
|
|
80
|
+
fetch "$REPO_URL/src/governance.md" "$DESK_DIR/governance.md"
|
|
81
|
+
fetch "$REPO_URL/src/model-upgrade-table.md" "$DESK_DIR/model-upgrade-table.md"
|
|
82
|
+
|
|
83
|
+
# v5.7 §4.4 — Node leader files (manifest-driven, prevents drift).
|
|
84
|
+
echo " Downloading Node leader runtime via MANIFEST.txt..."
|
|
85
|
+
MANIFEST_TMP=$(mktemp)
|
|
86
|
+
unlock_target "$NODE_DIR/MANIFEST.txt"
|
|
87
|
+
curl -fsSL "$REPO_URL/src/node/MANIFEST.txt" -o "$MANIFEST_TMP" || {
|
|
88
|
+
echo " [install] WARNING: src/node/MANIFEST.txt unavailable. Node leader features will be missing."
|
|
89
|
+
echo " Update install.sh from a 0.12.0+ source if upgrading."
|
|
90
|
+
MANIFEST_TMP=""
|
|
91
|
+
}
|
|
92
|
+
if [[ -n "$MANIFEST_TMP" && -s "$MANIFEST_TMP" ]]; then
|
|
93
|
+
cp "$MANIFEST_TMP" "$NODE_DIR/MANIFEST.txt"
|
|
94
|
+
while IFS= read -r relpath; do
|
|
95
|
+
[[ -z "$relpath" ]] && continue
|
|
96
|
+
target="$NODE_DIR/$relpath"
|
|
97
|
+
mkdir -p "$(dirname "$target")"
|
|
98
|
+
fetch "$REPO_URL/src/node/$relpath" "$target"
|
|
99
|
+
done < "$MANIFEST_TMP"
|
|
100
|
+
lock_target "$NODE_DIR/MANIFEST.txt"
|
|
101
|
+
rm -f "$MANIFEST_TMP"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# Note: chmod +x is NOT needed — runtime files are invoked via `zsh script.zsh`
|
|
105
|
+
# or `node script.mjs`, never directly. lock_target above already chmod a-w'd
|
|
106
|
+
# them; an explicit chmod +x would re-add write to other.
|
|
40
107
|
|
|
41
|
-
# Reference docs
|
|
108
|
+
# Reference docs (v5.7 §4.4 follow-up: same fetch() helper handles unlock+lock
|
|
109
|
+
# so reference-doc upgrade-over-installed-and-locked file does not silently fail).
|
|
42
110
|
echo " Downloading reference docs..."
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
111
|
+
fetch "$REPO_URL/README.md" "$DESK_DIR/README.md"
|
|
112
|
+
fetch "$REPO_URL/install.sh" "$DESK_DIR/install.sh"
|
|
113
|
+
fetch "$REPO_URL/docs/rlp-desk/architecture.md" "$DESK_DIR/docs/rlp-desk/architecture.md"
|
|
114
|
+
fetch "$REPO_URL/docs/rlp-desk/getting-started.md" "$DESK_DIR/docs/rlp-desk/getting-started.md"
|
|
115
|
+
fetch "$REPO_URL/docs/rlp-desk/protocol-reference.md" "$DESK_DIR/docs/rlp-desk/protocol-reference.md"
|
|
116
|
+
fetch "$REPO_URL/docs/rlp-desk/TODO-verification-next.md" "$DESK_DIR/docs/rlp-desk/TODO-verification-next.md"
|
|
117
|
+
fetch "$REPO_URL/docs/rlp-desk/multi-mission-orchestration.md" "$DESK_DIR/docs/rlp-desk/multi-mission-orchestration.md"
|
|
118
|
+
# Dev meta docs (v5.7 §4.15: under docs/rlp-desk/ to avoid mixing with user docs)
|
|
119
|
+
fetch "$REPO_URL/docs/rlp-desk/internal/verification-policy-gap-analysis.md" "$DESK_DIR/docs/rlp-desk/internal/verification-policy-gap-analysis.md"
|
|
120
|
+
fetch "$REPO_URL/docs/rlp-desk/internal/verification-strategy-research.md" "$DESK_DIR/docs/rlp-desk/internal/verification-strategy-research.md"
|
|
121
|
+
fetch "$REPO_URL/docs/rlp-desk/blueprints/blueprint-flywheel-enhancement.md" "$DESK_DIR/docs/rlp-desk/blueprints/blueprint-flywheel-enhancement.md"
|
|
122
|
+
fetch "$REPO_URL/docs/rlp-desk/blueprints/blueprint-pivot-step.md" "$DESK_DIR/docs/rlp-desk/blueprints/blueprint-pivot-step.md"
|
|
123
|
+
fetch "$REPO_URL/docs/rlp-desk/blueprints/plan-flywheel-enhancement.md" "$DESK_DIR/docs/rlp-desk/blueprints/plan-flywheel-enhancement.md"
|
|
124
|
+
fetch "$REPO_URL/docs/rlp-desk/blueprints/sv-architecture-rethink.md" "$DESK_DIR/docs/rlp-desk/blueprints/sv-architecture-rethink.md"
|
|
52
125
|
|
|
53
126
|
# Check tmux availability
|
|
54
127
|
if ! command -v tmux &>/dev/null; then
|
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-dev-methodologies/rlp-desk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Fresh-context iterative loops for Claude Code
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Fresh-context iterative loops for Claude Code \u2014 autonomous task completion with independent verification",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"postinstall": "node scripts/postinstall.js",
|
|
7
|
-
"uninstall": "node scripts/uninstall.js"
|
|
7
|
+
"uninstall": "node scripts/uninstall.js",
|
|
8
|
+
"test:node": "node --test 'tests/node/*.mjs' 'tests/node/*.test.mjs'",
|
|
9
|
+
"test:zsh": "for f in tests/test_*.sh; do echo \"=== $f ===\"; zsh \"$f\" || exit 1; done",
|
|
10
|
+
"test:fast": "npm run test:node",
|
|
11
|
+
"test:full": "npm run test:fast && npm run test:zsh",
|
|
12
|
+
"sv-gate:fast": "zsh tests/sv-gate-fast.sh",
|
|
13
|
+
"sv-gate:full": "zsh tests/sv-gate-full.sh"
|
|
8
14
|
},
|
|
9
15
|
"files": [
|
|
10
16
|
"src/commands/",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// v5.7 §4.4 — generate src/node/MANIFEST.txt listing every Node runtime file.
|
|
5
|
+
// install.sh reads this manifest line-by-line and curls each file. Without
|
|
6
|
+
// this, the curl-pipe-shell install path has no Node leader (release-blocker).
|
|
7
|
+
//
|
|
8
|
+
// Run as `prepublishOnly` AND on every CI build to keep the manifest in sync.
|
|
9
|
+
// CI drift check: `node scripts/build-node-manifest.js --check` returns
|
|
10
|
+
// non-zero exit if the on-disk manifest does not match the regenerated form.
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
|
|
15
|
+
const repoRoot = path.join(__dirname, "..");
|
|
16
|
+
const nodeDir = path.join(repoRoot, "src", "node");
|
|
17
|
+
const manifestPath = path.join(nodeDir, "MANIFEST.txt");
|
|
18
|
+
|
|
19
|
+
function walk(dir, base) {
|
|
20
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
21
|
+
const files = [];
|
|
22
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
23
|
+
const sourcePath = path.join(dir, entry.name);
|
|
24
|
+
const relPath = path.posix.join(base, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
files.push(...walk(sourcePath, relPath));
|
|
27
|
+
} else if (entry.isFile() && entry.name.endsWith(".mjs")) {
|
|
28
|
+
files.push(relPath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const generated = walk(nodeDir, "").join("\n") + "\n";
|
|
35
|
+
|
|
36
|
+
const isCheck = process.argv.includes("--check");
|
|
37
|
+
|
|
38
|
+
if (isCheck) {
|
|
39
|
+
const onDisk = fs.existsSync(manifestPath) ? fs.readFileSync(manifestPath, "utf8") : "";
|
|
40
|
+
if (onDisk !== generated) {
|
|
41
|
+
console.error("MANIFEST.txt drift detected. Run: node scripts/build-node-manifest.js");
|
|
42
|
+
console.error("--- ON-DISK ---");
|
|
43
|
+
console.error(onDisk);
|
|
44
|
+
console.error("--- GENERATED ---");
|
|
45
|
+
console.error(generated);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
console.log("MANIFEST.txt in sync (" + generated.split("\n").filter(Boolean).length + " entries).");
|
|
49
|
+
} else {
|
|
50
|
+
fs.writeFileSync(manifestPath, generated);
|
|
51
|
+
console.log("Wrote " + manifestPath + " (" + generated.split("\n").filter(Boolean).length + " entries).");
|
|
52
|
+
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -19,10 +19,12 @@ const runtimeSources = [
|
|
|
19
19
|
["src/model-upgrade-table.md", path.join(deskDir, "model-upgrade-table.md")],
|
|
20
20
|
["README.md", path.join(deskDir, "README.md")],
|
|
21
21
|
["install.sh", path.join(deskDir, "install.sh")],
|
|
22
|
-
|
|
23
|
-
["docs/
|
|
24
|
-
["docs/
|
|
25
|
-
["docs/
|
|
22
|
+
// v5.7 §4.15: all rlp-desk docs (user-facing + dev meta) under docs/rlp-desk/.
|
|
23
|
+
["docs/rlp-desk/architecture.md", path.join(docsDir, "rlp-desk", "architecture.md")],
|
|
24
|
+
["docs/rlp-desk/getting-started.md", path.join(docsDir, "rlp-desk", "getting-started.md")],
|
|
25
|
+
["docs/rlp-desk/protocol-reference.md", path.join(docsDir, "rlp-desk", "protocol-reference.md")],
|
|
26
|
+
["docs/rlp-desk/TODO-verification-next.md", path.join(docsDir, "rlp-desk", "TODO-verification-next.md")],
|
|
27
|
+
["docs/rlp-desk/multi-mission-orchestration.md", path.join(docsDir, "rlp-desk", "multi-mission-orchestration.md")],
|
|
26
28
|
];
|
|
27
29
|
const legacyFiles = [
|
|
28
30
|
path.join(deskDir, "init_ralph_desk.zsh"),
|
|
@@ -43,13 +45,117 @@ function ensureDir(dirPath) {
|
|
|
43
45
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function unlockTree(targetPath) {
|
|
49
|
+
// v5.7 §4.10: walk and chmod u+w every entry so rmSync(recursive) does not
|
|
50
|
+
// ENOTEMPTY on a directory full of 0o444 children. Idempotent on missing paths.
|
|
51
|
+
// Security review v5.7 follow-up: lstatSync first; SKIP symlinks entirely so
|
|
52
|
+
// a hostile symlink (e.g., ~/.claude/ralph-desk/foo -> /etc/passwd) cannot
|
|
53
|
+
// be chmod'd via unlockTree's chmodSync (which follows symlinks).
|
|
54
|
+
if (!fs.existsSync(targetPath)) return;
|
|
55
|
+
const stat = fs.lstatSync(targetPath);
|
|
56
|
+
if (stat.isSymbolicLink()) {
|
|
57
|
+
// Don't chmod the symlink target. lchmod is unsupported on Linux; safest
|
|
58
|
+
// action is to leave symlinks alone — they're not part of our install set.
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try { fs.chmodSync(targetPath, stat.isDirectory() ? 0o755 : 0o644); } catch {}
|
|
62
|
+
if (stat.isDirectory()) {
|
|
63
|
+
for (const entry of fs.readdirSync(targetPath, { withFileTypes: true })) {
|
|
64
|
+
unlockTree(path.join(targetPath, entry.name));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
function removePath(targetPath) {
|
|
70
|
+
// v5.7 §4.10: existing target tree may contain 0o444 files. Walk and unlock
|
|
71
|
+
// before rmSync so EACCES/ENOTEMPTY don't break the upgrade path.
|
|
72
|
+
unlockTree(targetPath);
|
|
47
73
|
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
48
74
|
}
|
|
49
75
|
|
|
76
|
+
// v5.7 §4.10: per-extension banner format. `# DO NOT EDIT` text leaks into
|
|
77
|
+
// rendered Markdown, so .md uses HTML comment; .mjs/.js uses //; shell uses #.
|
|
78
|
+
function bannerFor(extension, sourceRelativePath) {
|
|
79
|
+
const msg = `DO NOT EDIT — generated from ${sourceRelativePath}. Edit source and re-sync. See ~/.claude/ralph-desk/UNLOCK.md for debug unlock.`;
|
|
80
|
+
switch (extension) {
|
|
81
|
+
case ".md":
|
|
82
|
+
return `<!-- ${msg} -->\n`;
|
|
83
|
+
case ".mjs":
|
|
84
|
+
case ".js":
|
|
85
|
+
return `// ${msg}\n`;
|
|
86
|
+
case ".zsh":
|
|
87
|
+
case ".sh":
|
|
88
|
+
return `# ${msg}\n`;
|
|
89
|
+
default:
|
|
90
|
+
return null; // .json and unknown types: rely on chmod alone
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let _chmodWarningEmitted = false;
|
|
95
|
+
function tryLockFile(targetPath) {
|
|
96
|
+
// Best-effort write-protect. Some filesystems (WSL1/NTFS, tmpfs noexec, certain
|
|
97
|
+
// bind mounts) silently no-op chmod. R-V5-5: emit ONE warning per install run.
|
|
98
|
+
try {
|
|
99
|
+
fs.chmodSync(targetPath, 0o444);
|
|
100
|
+
const stat = fs.statSync(targetPath);
|
|
101
|
+
if ((stat.mode & 0o222) !== 0 && !_chmodWarningEmitted) {
|
|
102
|
+
console.log(" [install] WARNING: filesystem does not honor chmod a-w; cross-session edit protection unavailable.");
|
|
103
|
+
_chmodWarningEmitted = true;
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (!_chmodWarningEmitted) {
|
|
107
|
+
console.log(" [install] WARNING: chmod a-w failed (" + err.code + "); cross-session edit protection unavailable.");
|
|
108
|
+
_chmodWarningEmitted = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function injectBannerAndLock(targetPath, sourceRelativePath) {
|
|
114
|
+
const ext = path.extname(targetPath).toLowerCase();
|
|
115
|
+
const banner = bannerFor(ext, sourceRelativePath);
|
|
116
|
+
if (banner) {
|
|
117
|
+
const original = fs.readFileSync(targetPath);
|
|
118
|
+
// Idempotency guard (code-review v5.7 follow-up): the source file in the
|
|
119
|
+
// package tarball already contains an injected banner from a prior install
|
|
120
|
+
// ONLY if a developer ran sync from an installed copy back to the source —
|
|
121
|
+
// which is forbidden by CLAUDE.md. But re-running install over an existing
|
|
122
|
+
// installed file (e.g., npm i again) does NOT need re-injection because
|
|
123
|
+
// copyFileSync replaced the file with the source contents. The check below
|
|
124
|
+
// is defensive — only inject when the file does not already start with a
|
|
125
|
+
// DO NOT EDIT marker.
|
|
126
|
+
const head = original.subarray(0, 200).toString('utf8');
|
|
127
|
+
if (head.includes('DO NOT EDIT — generated from')) {
|
|
128
|
+
// Already banner-headed (rare: source somehow shipped with banner). Skip
|
|
129
|
+
// injection but still apply chmod for consistency.
|
|
130
|
+
tryLockFile(targetPath);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Shebang preservation: if first line starts with `#!`, banner goes on line 2.
|
|
134
|
+
if (original.length >= 2 && original[0] === 0x23 && original[1] === 0x21) {
|
|
135
|
+
const newlineIdx = original.indexOf(0x0a);
|
|
136
|
+
if (newlineIdx >= 0) {
|
|
137
|
+
const headBuf = original.subarray(0, newlineIdx + 1);
|
|
138
|
+
const tailBuf = original.subarray(newlineIdx + 1);
|
|
139
|
+
fs.writeFileSync(targetPath, Buffer.concat([headBuf, Buffer.from(banner), tailBuf]));
|
|
140
|
+
} else {
|
|
141
|
+
fs.writeFileSync(targetPath, Buffer.concat([original, Buffer.from("\n" + banner)]));
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
fs.writeFileSync(targetPath, Buffer.concat([Buffer.from(banner), original]));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
tryLockFile(targetPath);
|
|
148
|
+
}
|
|
149
|
+
|
|
50
150
|
function copyFile(sourceRelativePath, targetPath) {
|
|
51
151
|
ensureDir(path.dirname(targetPath));
|
|
152
|
+
// v5.7 §4.10: unlock target if it exists and is write-protected from a prior
|
|
153
|
+
// install (R-V5-1: copyFileSync over 0o444 fails EACCES on upgrade).
|
|
154
|
+
if (fs.existsSync(targetPath)) {
|
|
155
|
+
try { fs.chmodSync(targetPath, 0o644); } catch { /* may be already writable */ }
|
|
156
|
+
}
|
|
52
157
|
fs.copyFileSync(path.join(pkgDir, sourceRelativePath), targetPath);
|
|
158
|
+
injectBannerAndLock(targetPath, sourceRelativePath);
|
|
53
159
|
console.log(" + " + targetPath);
|
|
54
160
|
}
|
|
55
161
|
|
|
@@ -69,31 +175,73 @@ function copyMarkdownDirectory(sourceRelativeDir, targetDir) {
|
|
|
69
175
|
}
|
|
70
176
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
71
177
|
ensureDir(path.dirname(targetPath));
|
|
178
|
+
// v5.7 §4.10: unlock prior-install write-protected target before copy.
|
|
179
|
+
if (fs.existsSync(targetPath)) {
|
|
180
|
+
try { fs.chmodSync(targetPath, 0o644); } catch {}
|
|
181
|
+
}
|
|
72
182
|
fs.copyFileSync(sourcePath, targetPath);
|
|
183
|
+
injectBannerAndLock(targetPath, path.join(sourceRelativeDir, entry.name));
|
|
73
184
|
console.log(" + " + targetPath);
|
|
74
185
|
}
|
|
75
186
|
}
|
|
76
187
|
}
|
|
77
188
|
|
|
78
|
-
function copyNodeRuntime(sourceDir, targetDir) {
|
|
189
|
+
function copyNodeRuntime(sourceDir, targetDir, sourceRelativeBase) {
|
|
190
|
+
// removePath already handles 0o444 unlock per v5.7 §4.10.
|
|
79
191
|
removePath(targetDir);
|
|
80
192
|
ensureDir(targetDir);
|
|
193
|
+
const baseRel = sourceRelativeBase || "src/node";
|
|
81
194
|
|
|
82
195
|
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
83
196
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
84
197
|
const targetPath = path.join(targetDir, entry.name);
|
|
198
|
+
const childRel = path.join(baseRel, entry.name);
|
|
85
199
|
if (entry.isDirectory()) {
|
|
86
|
-
copyNodeRuntime(sourcePath, targetPath);
|
|
200
|
+
copyNodeRuntime(sourcePath, targetPath, childRel);
|
|
87
201
|
continue;
|
|
88
202
|
}
|
|
89
203
|
if (entry.isFile()) {
|
|
90
204
|
ensureDir(path.dirname(targetPath));
|
|
91
205
|
fs.copyFileSync(sourcePath, targetPath);
|
|
206
|
+
injectBannerAndLock(targetPath, childRel);
|
|
92
207
|
console.log(" + " + targetPath);
|
|
93
208
|
}
|
|
94
209
|
}
|
|
95
210
|
}
|
|
96
211
|
|
|
212
|
+
// v5.7 §4.10: Documented escape hatch for debug sessions.
|
|
213
|
+
function writeUnlockDoc() {
|
|
214
|
+
const unlockPath = path.join(deskDir, "UNLOCK.md");
|
|
215
|
+
const content = `# UNLOCK — Debug edit escape hatch
|
|
216
|
+
|
|
217
|
+
Files in \`~/.claude/ralph-desk/\` and \`~/.claude/commands/rlp-desk.md\` are
|
|
218
|
+
installed read-only (\`chmod a-w\`) so cross-session AI agents cannot silently
|
|
219
|
+
corrupt them. Source of truth: the rlp-desk source repository.
|
|
220
|
+
|
|
221
|
+
If you need to edit an installed file for **temporary debug** (e.g., add a
|
|
222
|
+
\`set -x\` line, insert a \`print\` statement):
|
|
223
|
+
|
|
224
|
+
\`\`\`bash
|
|
225
|
+
chmod -R u+w ~/.claude/ralph-desk
|
|
226
|
+
chmod u+w ~/.claude/commands/rlp-desk.md
|
|
227
|
+
# ... edit, test, then revert ...
|
|
228
|
+
\`\`\`
|
|
229
|
+
|
|
230
|
+
To re-apply protection without a full reinstall, run npm install rlp-desk
|
|
231
|
+
from the source repo or rerun \`scripts/postinstall.js\`.
|
|
232
|
+
|
|
233
|
+
**For permanent fixes**, edit the source repo and re-publish — never edit
|
|
234
|
+
installed files directly. The banner at the top of every installed file
|
|
235
|
+
points back to its source path.
|
|
236
|
+
`;
|
|
237
|
+
if (fs.existsSync(unlockPath)) {
|
|
238
|
+
try { fs.chmodSync(unlockPath, 0o644); } catch {}
|
|
239
|
+
}
|
|
240
|
+
fs.writeFileSync(unlockPath, content);
|
|
241
|
+
console.log(" + " + unlockPath);
|
|
242
|
+
// UNLOCK.md is itself NOT locked — users may want to add their own notes.
|
|
243
|
+
}
|
|
244
|
+
|
|
97
245
|
console.log("");
|
|
98
246
|
console.log(" RLP Desk v" + pkg.version);
|
|
99
247
|
console.log(" ================");
|
|
@@ -118,10 +266,16 @@ for (const [sourcePath, targetPath] of runtimeSources) {
|
|
|
118
266
|
copyFile(sourcePath, targetPath);
|
|
119
267
|
}
|
|
120
268
|
|
|
121
|
-
|
|
122
|
-
|
|
269
|
+
// v5.7 §4.15: dev meta docs live under docs/rlp-desk/ to avoid mixing with
|
|
270
|
+
// user-facing operational docs (per user feedback).
|
|
271
|
+
copyMarkdownDirectory("docs/rlp-desk/internal", path.join(docsDir, "rlp-desk", "internal"));
|
|
272
|
+
copyMarkdownDirectory("docs/rlp-desk/blueprints", path.join(docsDir, "rlp-desk", "blueprints"));
|
|
273
|
+
copyMarkdownDirectory("docs/rlp-desk/plans", path.join(docsDir, "rlp-desk", "plans"));
|
|
123
274
|
copyNodeRuntime(path.join(pkgDir, "src", "node"), nodeDir);
|
|
124
275
|
|
|
276
|
+
// v5.7 §4.10: write the UNLOCK.md escape-hatch doc for debug sessions.
|
|
277
|
+
writeUnlockDoc();
|
|
278
|
+
|
|
125
279
|
console.log("");
|
|
126
280
|
console.log(" Done! Open Claude Code and run:");
|
|
127
281
|
console.log(" /rlp-desk brainstorm \"your task description\"");
|