@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.
Files changed (50) hide show
  1. package/docs/plans/spicy-booping-galaxy.md +322 -0
  2. package/docs/rlp-desk/artifact-schema.md +99 -0
  3. package/docs/rlp-desk/ci-setup.md +100 -0
  4. package/docs/rlp-desk/e2e-scenarios.md +102 -0
  5. package/docs/rlp-desk/plans/rlp-desk-tmux-flywheel-routing.md +730 -0
  6. package/install.sh +93 -20
  7. package/package.json +9 -3
  8. package/scripts/build-node-manifest.js +52 -0
  9. package/scripts/postinstall.js +162 -8
  10. package/src/commands/rlp-desk.md +73 -50
  11. package/src/governance.md +56 -7
  12. package/src/node/MANIFEST.txt +15 -0
  13. package/src/node/cli/command-builder.mjs +43 -5
  14. package/src/node/constants.mjs +19 -0
  15. package/src/node/init/campaign-initializer.mjs +100 -10
  16. package/src/node/polling/signal-poller.mjs +139 -3
  17. package/src/node/reporting/campaign-reporting.mjs +5 -1
  18. package/src/node/run.mjs +31 -2
  19. package/src/node/runner/campaign-main-loop.mjs +521 -44
  20. package/src/node/runner/leader-registry.mjs +100 -0
  21. package/src/node/runner/prompt-detector.mjs +41 -0
  22. package/src/node/runner/prompt-dismisser.mjs +200 -0
  23. package/src/node/shared/fs.mjs +38 -0
  24. package/src/node/util/debug-log.mjs +56 -0
  25. package/src/node/util/desk-root.mjs +24 -0
  26. package/src/node/util/shell-quote.mjs +12 -0
  27. package/docs/superpowers/plans/2026-04-24-gpt-5-5-default.md +0 -517
  28. package/docs/superpowers/specs/2026-04-24-gpt-5-5-default.md +0 -107
  29. /package/docs/{TODO-verification-next.md → rlp-desk/TODO-verification-next.md} +0 -0
  30. /package/docs/{architecture.md → rlp-desk/architecture.md} +0 -0
  31. /package/docs/{blueprints → rlp-desk/blueprints}/blueprint-flywheel-enhancement.md +0 -0
  32. /package/docs/{blueprints → rlp-desk/blueprints}/blueprint-pivot-step.md +0 -0
  33. /package/docs/{blueprints → rlp-desk/blueprints}/plan-flywheel-enhancement.md +0 -0
  34. /package/docs/{blueprints → rlp-desk/blueprints}/sv-architecture-rethink.md +0 -0
  35. /package/docs/{getting-started.md → rlp-desk/getting-started.md} +0 -0
  36. /package/docs/{internal → rlp-desk/internal}/verification-policy-gap-analysis.md +0 -0
  37. /package/docs/{internal → rlp-desk/internal}/verification-strategy-research.md +0 -0
  38. /package/docs/{multi-mission-orchestration.md → rlp-desk/multi-mission-orchestration.md} +0 -0
  39. /package/docs/{plans → rlp-desk/plans}/cozy-gliding-trinket.md +0 -0
  40. /package/docs/{plans → rlp-desk/plans}/frolicking-churning-honey.md +0 -0
  41. /package/docs/{plans → rlp-desk/plans}/keen-sauteeing-snowflake.md +0 -0
  42. /package/docs/{plans → rlp-desk/plans}/mutable-booping-corbato.md +0 -0
  43. /package/docs/{plans → rlp-desk/plans}/rlp-desk-0.11-handoff-7fixes.md +0 -0
  44. /package/docs/{plans → rlp-desk/plans}/rlp-desk-0.11.1-tmux-pane-disappearance.md +0 -0
  45. /package/docs/{plans → rlp-desk/plans}/rlp-desk-elegant-papert-agent-a8cd695ffca2a3ad8.md +0 -0
  46. /package/docs/{plans → rlp-desk/plans}/rlp-desk-elegant-papert.md +0 -0
  47. /package/docs/{plans → rlp-desk/plans}/toasty-whistling-diffie-agent-a6814625642e956da.md +0 -0
  48. /package/docs/{plans → rlp-desk/plans}/toasty-whistling-diffie.md +0 -0
  49. /package/docs/{plans → rlp-desk/plans}/validated-snacking-crayon.md +0 -0
  50. /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
- REPO_URL="https://raw.githubusercontent.com/ai-dev-methodologies/rlp-desk/main"
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
- curl -sSL "$REPO_URL/src/commands/rlp-desk.md" -o "$COMMANDS_DIR/rlp-desk.md"
34
- curl -sSL "$REPO_URL/src/scripts/init_ralph_desk.zsh" -o "$DESK_DIR/init_ralph_desk.zsh"
35
- curl -sSL "$REPO_URL/src/scripts/run_ralph_desk.zsh" -o "$DESK_DIR/run_ralph_desk.zsh"
36
- curl -sSL "$REPO_URL/src/scripts/lib_ralph_desk.zsh" -o "$DESK_DIR/lib_ralph_desk.zsh"
37
- curl -sSL "$REPO_URL/src/governance.md" -o "$DESK_DIR/governance.md"
38
- curl -sSL "$REPO_URL/src/model-upgrade-table.md" -o "$DESK_DIR/model-upgrade-table.md"
39
- chmod +x "$DESK_DIR/init_ralph_desk.zsh" "$DESK_DIR/run_ralph_desk.zsh" "$DESK_DIR/lib_ralph_desk.zsh"
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
- curl -sSL "$REPO_URL/README.md" -o "$DESK_DIR/README.md"
44
- curl -sSL "$REPO_URL/install.sh" -o "$DESK_DIR/install.sh"
45
- curl -sSL "$REPO_URL/docs/architecture.md" -o "$DESK_DIR/docs/architecture.md"
46
- curl -sSL "$REPO_URL/docs/getting-started.md" -o "$DESK_DIR/docs/getting-started.md"
47
- curl -sSL "$REPO_URL/docs/protocol-reference.md" -o "$DESK_DIR/docs/protocol-reference.md"
48
- curl -sSL "$REPO_URL/docs/TODO-verification-next.md" -o "$DESK_DIR/docs/TODO-verification-next.md"
49
- curl -sSL "$REPO_URL/docs/internal/verification-policy-gap-analysis.md" -o "$DESK_DIR/docs/internal/verification-policy-gap-analysis.md"
50
- curl -sSL "$REPO_URL/docs/internal/verification-strategy-research.md" -o "$DESK_DIR/docs/internal/verification-strategy-research.md"
51
- curl -sSL "$REPO_URL/docs/blueprints/blueprint-v0.4-evolution.md" -o "$DESK_DIR/docs/blueprints/blueprint-v0.4-evolution.md"
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.11.1",
4
- "description": "Fresh-context iterative loops for Claude Code autonomous task completion with independent verification",
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
+ }
@@ -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
- ["docs/architecture.md", path.join(docsDir, "architecture.md")],
23
- ["docs/getting-started.md", path.join(docsDir, "getting-started.md")],
24
- ["docs/protocol-reference.md", path.join(docsDir, "protocol-reference.md")],
25
- ["docs/TODO-verification-next.md", path.join(docsDir, "TODO-verification-next.md")],
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
- copyMarkdownDirectory("docs/internal", path.join(docsDir, "internal"));
122
- copyMarkdownDirectory("docs/blueprints", path.join(docsDir, "blueprints"));
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\"");