@ai-dev-methodologies/rlp-desk 0.11.0 → 0.12.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/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-0.11.1-tmux-pane-disappearance.md +260 -0
- package/docs/rlp-desk/plans/rlp-desk-tmux-flywheel-routing.md +730 -0
- package/install.sh +93 -20
- package/package.json +8 -2
- package/scripts/build-node-manifest.js +52 -0
- package/scripts/postinstall.js +162 -8
- package/src/commands/rlp-desk.md +48 -25
- package/src/governance.md +68 -6
- package/src/node/MANIFEST.txt +15 -0
- package/src/node/cli/command-builder.mjs +25 -5
- package/src/node/constants.mjs +19 -0
- package/src/node/polling/signal-poller.mjs +119 -3
- package/src/node/runner/campaign-main-loop.mjs +470 -41
- package/src/node/runner/leader-registry.mjs +100 -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/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-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.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Fresh-context iterative loops for Claude Code — 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\"");
|
package/src/commands/rlp-desk.md
CHANGED
|
@@ -280,40 +280,51 @@ Parse the `--mode` flag. If absent or `agent`, use the Agent() path below. If `t
|
|
|
280
280
|
|
|
281
281
|
#### Tmux Mode (`--mode tmux`)
|
|
282
282
|
|
|
283
|
-
When `--mode tmux` is specified:
|
|
283
|
+
When `--mode tmux` is specified (v0.12.0+ — v5.7 §4.1 routes to Node leader for flywheel + SV support):
|
|
284
284
|
|
|
285
285
|
1. **Validate scaffold** — same as Agent() mode: check `.claude/ralph-desk/prompts/<slug>.worker.prompt.md` etc.
|
|
286
286
|
2. **Check sentinels** — same as Agent() mode.
|
|
287
|
-
3. **Check prerequisites** — verify `tmux` and `
|
|
288
|
-
4. **Locate
|
|
289
|
-
5. **Launch** — shell out to the
|
|
287
|
+
3. **Check prerequisites** — verify `tmux`, `jq`, and `node` (>= 16) are installed. If not, report what is missing and stop.
|
|
288
|
+
4. **Locate Node leader** — find `~/.claude/ralph-desk/node/run.mjs`. If not found, tell the user to reinstall (`npm install` or `bash install.sh`).
|
|
289
|
+
5. **Launch** — shell out to the Node leader. **All dynamic args (slug + model values) MUST be passed through shell single-quote escaping** (v5.7 §4.12 G11) so bracketed model ids like `claude-opus-4-7[1m]` survive zsh parsing:
|
|
290
|
+
|
|
290
291
|
```bash
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
292
|
+
node ~/.claude/ralph-desk/node/run.mjs run '<slug>' \
|
|
293
|
+
--mode tmux \
|
|
294
|
+
--max-iter <N> \
|
|
295
|
+
--worker-model '<value>' \
|
|
296
|
+
[--lock-worker-model] \
|
|
297
|
+
--verifier-model '<value>' \
|
|
298
|
+
--final-verifier-model '<value>' \
|
|
299
|
+
--consensus <off|all|final-only> \
|
|
300
|
+
--consensus-model '<value>' \
|
|
301
|
+
--final-consensus-model '<value>' \
|
|
302
|
+
--verify-mode <per-us|batch> \
|
|
303
|
+
--cb-threshold <N> \
|
|
304
|
+
--iter-timeout <N> \
|
|
305
|
+
[--debug] [--autonomous] \
|
|
306
|
+
[--lane-strict] # was env LANE_MODE=strict \
|
|
307
|
+
[--test-density-strict] # was env TEST_DENSITY_MODE=strict \
|
|
308
|
+
[--with-self-verification] \
|
|
309
|
+
[--flywheel on-fail --flywheel-model '<value>'] \
|
|
310
|
+
[--flywheel-guard on --flywheel-guard-model '<value>']
|
|
307
311
|
```
|
|
308
|
-
|
|
309
|
-
7.
|
|
312
|
+
|
|
313
|
+
**Quoting contract (v5.7 §4.1)**: every `'<value>'` placeholder above must be replaced with the user's flag value wrapped in single quotes via the equivalent of `shellQuote(value)` — `"'" + value.replace(/'/g, "'\\''") + "'"` for POSIX correctness. The slug, all model values, and any future dynamic flag must follow this rule. A slug or model containing brackets / spaces / single quotes / dollar signs / backticks must NOT break the leader invocation.
|
|
314
|
+
|
|
315
|
+
**Env-var translation (v5.7 §4.1)**: the slash command historically built `LANE_MODE=strict zsh ...` and `TEST_DENSITY_MODE=strict zsh ...` from CLI flags. The Node leader uses CLI flags instead — translate `--lane-strict` and `--test-density-strict` into the corresponding flags. Direct env-var users (running zsh directly) are unaffected.
|
|
316
|
+
|
|
317
|
+
6. **If the Node leader exits with error** — report the error to the user and STOP. Do NOT attempt to work around it. Do NOT create tmux sessions yourself. Do NOT re-launch in a different way. Tell the user what went wrong and suggest `--mode agent` as alternative.
|
|
318
|
+
7. **If successful** — tell the user the tmux session has been started. The Node leader takes over as the deterministic Leader. No Agent() calls are made in tmux mode.
|
|
310
319
|
|
|
311
320
|
**IMPORTANT RULES:**
|
|
312
|
-
- Tmux mode requires the user to already be inside a tmux session. If the
|
|
313
|
-
- MUST launch
|
|
321
|
+
- Tmux mode requires the user to already be inside a tmux session. If the leader rejects because $TMUX is not set, do NOT try to create a tmux session yourself. Tell the user: "Start tmux first, then retry."
|
|
322
|
+
- MUST launch with `run_in_background: true` so `/rlp-desk` returns control immediately while preserving live tmux visibility.
|
|
314
323
|
- Run-in-background is used so the shell can keep the command visible and keep the pane layout stable for status checks and completion flow.
|
|
315
324
|
- Do NOT kill panes after completion. Panes stay alive for inspection. User cleans up with `/rlp-desk clean <slug> --kill-session`.
|
|
316
|
-
- `--with-self-verification` is
|
|
325
|
+
- `--with-self-verification` is fully supported in tmux mode (v5.7 §4.7). The Node leader's `generateSVReport()` writes `self-verification-report.md` + `self-verification-data.json` under `<project>/.claude/ralph-desk/analytics/<slug>/` (project-local, v5.7 §4.11.b).
|
|
326
|
+
- `--flywheel on-fail` and `--flywheel-guard on` are fully supported in tmux mode (v5.7 §4.1). The Node leader handles pane creation, sendKeys dispatch, signal polling, and Guard retry semantics identically to agent mode.
|
|
327
|
+
- Legacy `zsh ~/.claude/ralph-desk/run_ralph_desk.zsh` (deprecated in 0.12.0) still runs for non-flywheel/non-SV invocations but emits a deprecation `[notice]`. Calling it with `FLYWHEEL` or `WITH_SELF_VERIFICATION` env vars exits 2 with a migration banner pointing to the Node leader.
|
|
317
328
|
|
|
318
329
|
**tmux UX model (5 items):**
|
|
319
330
|
- The session returns immediately after launch (`run_in_background: true`) so the command returns control to the parent CLI.
|
|
@@ -324,6 +335,18 @@ WITH_SELF_VERIFICATION=<1 if --with-self-verification, else 0> \
|
|
|
324
335
|
|
|
325
336
|
#### Agent Mode (`--mode agent` or default)
|
|
326
337
|
|
|
338
|
+
**Why Agent mode is structurally immune to Bug 4/5 (mid-execution prompt hang
|
|
339
|
+
& A4 premature dispatch):** Worker/Verifier are dispatched as `Agent(...,
|
|
340
|
+
mode="bypassPermissions", ...)`. The subagent runs non-interactively under
|
|
341
|
+
the platform's bypass — it has no tmux pane, no TUI surface, and cannot
|
|
342
|
+
surface a `[y/N]` prompt to the parent Leader. The auto-dismiss /
|
|
343
|
+
prompt-stall / no-progress timeouts in `run_ralph_desk.zsh` (v5.7 §4.13.b /
|
|
344
|
+
§4.16 / §4.17) are therefore tmux-only by design. **Tradeoff**: because
|
|
345
|
+
`Agent()` has no timeout API, agent-mode iterations are not bounded — if
|
|
346
|
+
the platform's `bypassPermissions` ever fails to suppress an interactive
|
|
347
|
+
prompt at the SDK level, the call hangs indefinitely with no rlp-desk-side
|
|
348
|
+
watchdog. Use `--mode tmux` if you need bounded execution time.
|
|
349
|
+
|
|
327
350
|
### Preparation
|
|
328
351
|
1. Validate scaffold: `.claude/ralph-desk/prompts/<slug>.worker.prompt.md` etc.
|
|
329
352
|
2. **Codex CLI pre-validation**: If `--consensus` is not `off` OR `--worker-model` uses codex format (contains `:`) OR `--verifier-model` / `--final-verifier-model` / `--consensus-model` / `--final-consensus-model` uses codex format, check that `codex` CLI exists in PATH. If codex CLI not found → STOP immediately, print install instructions (`npm install -g @openai/codex`), do not start the loop.
|
package/src/governance.md
CHANGED
|
@@ -297,13 +297,54 @@ BLOCKED writes a JSON sidecar (`<slug>-blocked.json`) alongside the markdown sen
|
|
|
297
297
|
- English: `depends on US-`, `blocking US-`, `awaits US-`, `post-iter US-`, `requires US-N`, `cross-US`
|
|
298
298
|
- Korean: `US-N 산출물`, `신규 US-`, `post-iter`
|
|
299
299
|
|
|
300
|
-
**Write Order Contract (atomicity invariant)
|
|
301
|
-
1.
|
|
302
|
-
2.
|
|
303
|
-
3. Invariant: **markdown exists ⇒ JSON exists** (
|
|
304
|
-
4. Wrappers SHOULD watch markdown sentinel, then read JSON sidecar. If JSON not yet visible (rare), retry up to 5 × 50ms before failing.
|
|
300
|
+
**Write Order Contract (atomicity invariant)** — v5.7 §4.24 reversed:
|
|
301
|
+
1. **markdown sentinel written FIRST** via `writeSentinelExclusive` (`fs.open(path, 'wx')` — O_EXCL first-writer-wins). The md acts as the race lock.
|
|
302
|
+
2. **JSON sidecar written SECOND**, only by the winning writer.
|
|
303
|
+
3. Invariant: **markdown exists ⇒ JSON exists** (winner writes both; losers see EEXIST and return without touching JSON, preserving the winner's content).
|
|
304
|
+
4. Wrappers SHOULD watch markdown sentinel, then read JSON sidecar. If JSON not yet visible (rare ≤50ms), retry up to 5 × 50ms before failing.
|
|
305
305
|
|
|
306
|
-
`
|
|
306
|
+
`writeSentinelExclusive` (in `src/node/shared/fs.mjs`) provides per-file first-writer-wins; cross-file ordering is enforced by the explicit md-then-JSON sequence inside `writeSentinel`.
|
|
307
|
+
|
|
308
|
+
## 1g. Sentinel Guarantee Invariant (file-guarantee contract)
|
|
309
|
+
|
|
310
|
+
**Every terminal exit of `runCampaign()` MUST leave exactly one sentinel on disk: `<slug>-blocked.md` XOR `<slug>-complete.md`.**
|
|
311
|
+
|
|
312
|
+
This invariant is the foundation of the fresh-context architecture. If a campaign exits without any sentinel, future iterations cannot determine campaign state — Worker/Verifier are dispatched into a campaign whose history they cannot reconstruct.
|
|
313
|
+
|
|
314
|
+
### Enforcement (3-layer defense)
|
|
315
|
+
|
|
316
|
+
1. **Per-poll-site sentinel write** (`_handlePollFailure` helper at `src/node/runner/campaign-main-loop.mjs`). Every `pollForSignal` call site (Worker, VerifierPerUS, VerifierFinal, Flywheel, Guard) is wrapped in `try { … } catch (error) { return _handlePollFailure(error, { role, … }); }`. The helper classifies via `BLOCK_TAGS` typed enum, calls `writeSentinel` (idempotent via O_EXCL), and returns `{status:'blocked', …}` so the caller exits the loop cleanly.
|
|
317
|
+
|
|
318
|
+
2. **Run-level try/finally backstop** (`_ensureTerminalSentinel`). After the campaign body executes, a `finally` block checks `exists(blockedSentinel) XOR exists(completeSentinel)`. If neither (paused state `continue` excepted), writes a synthetic BLOCKED `infra_failure/leader_exited_without_terminal_state` so even unhandled exceptions cannot escape silently.
|
|
319
|
+
|
|
320
|
+
3. **Schema validator at READ boundary** (`validateArtifact`). After every `pollForSignal` returns parsed JSON, validates `(slug, iteration ≥ floor, signal_type matches read context, us_id ∈ usList ∪ {ALL})`. Throws `MalformedArtifactError({field, expected, got})` → caught by same `_handlePollFailure` → BLOCKED `contract_violation/malformed_artifact` (recoverable).
|
|
321
|
+
|
|
322
|
+
### Per-role failure-category enum
|
|
323
|
+
|
|
324
|
+
`_classifyBlock` (in `campaign-main-loop.mjs`) maps each `BLOCK_TAGS` value to one of the locked taxonomy categories:
|
|
325
|
+
|
|
326
|
+
| Tag | reason_category | recoverable | Example trigger |
|
|
327
|
+
|-----|----------------|-------------|-----------------|
|
|
328
|
+
| `WORKER_EXITED` | `infra_failure` | false | Worker pane returned to shell without writing signal |
|
|
329
|
+
| `VERIFIER_EXITED` | `infra_failure` | false | Per-US Verifier exited without writing verdict |
|
|
330
|
+
| `FINAL_VERIFIER_EXITED` | `infra_failure` | false | Final ALL-verifier exited without writing verdict |
|
|
331
|
+
| `FLYWHEEL_EXITED` | `infra_failure` | false | Flywheel pane crashed |
|
|
332
|
+
| `GUARD_EXITED` | `infra_failure` | false | Guard pane crashed |
|
|
333
|
+
| `PROMPT_BLOCKED` | `infra_failure` | false | Default-No prompt — auto-Enter would CANCEL |
|
|
334
|
+
| `<role>_TIMEOUT` | `infra_failure` | false | pollForSignal timed out without exit detected |
|
|
335
|
+
| `MALFORMED_ARTIFACT` | `contract_violation` | true | Worker/Verifier wrote schema-violating JSON |
|
|
336
|
+
| `LEADER_EXITED_WITHOUT_TERMINAL_STATE` | `infra_failure` | false | Backstop fired (uncaught exception or paths outside controlled scope) |
|
|
337
|
+
|
|
338
|
+
### Auditing
|
|
339
|
+
|
|
340
|
+
Operators can verify the invariant for any campaign by running:
|
|
341
|
+
|
|
342
|
+
```sh
|
|
343
|
+
zsh tests/sv-gate-fast.sh # 30s mechanical check (greps + units)
|
|
344
|
+
zsh tests/sv-gate-full.sh # 5min including REAL tmux + REAL campaign E2E
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The fast gate fails immediately if any pollForSignal call site lacks a `_handlePollFailure` wiring or the writeSentinelExclusive primitive is bypassed.
|
|
307
348
|
|
|
308
349
|
## 2. Roles
|
|
309
350
|
|
|
@@ -553,6 +594,14 @@ for iteration in 1..max_iter:
|
|
|
553
594
|
• fail + retries exhausted → BLOCKED
|
|
554
595
|
• inconclusive → BLOCKED (escalate to user)
|
|
555
596
|
- Guard count tracked per-US in status.json
|
|
597
|
+
- **Mode support (v0.12.0+, v5.7 §4.3)**: flywheel runs identically in
|
|
598
|
+
--mode agent and --mode tmux when routed through the Node leader
|
|
599
|
+
(`node ~/.claude/ralph-desk/node/run.mjs run --mode tmux`). The legacy
|
|
600
|
+
`run_ralph_desk.zsh` runner rejects --flywheel/--flywheel-guard with
|
|
601
|
+
exit 2 + migration banner; users must use the Node entry. Same applies
|
|
602
|
+
to --with-self-verification: SV report generation is supported in
|
|
603
|
+
tmux mode via the Node leader's generateSVReport() (no longer
|
|
604
|
+
agent-mode-only).
|
|
556
605
|
|
|
557
606
|
⑦ Execute Verifier (see §7a for per-US and §7b for consensus details)
|
|
558
607
|
- Build prompt (scoped to us_id if per-us mode) → log
|
|
@@ -774,6 +823,19 @@ The base signal vocabulary (`continue | verify | blocked`) is binary at the iter
|
|
|
774
823
|
|
|
775
824
|
The downgrade is intentionally recoverable: the malformed signal is a worker-side prompt regression, not an environment failure, and the operator can fix it in-place.
|
|
776
825
|
|
|
826
|
+
## 7h. Tmux Session Lifecycle Resilience (US-024/025/026 R12+R13+R14 P0)
|
|
827
|
+
|
|
828
|
+
Multi-mission queue/daemon (`RLP_BACKGROUND=1`) workflows can lose their tmux session between missions — terminal close, manual `tmux kill-session`, or tmux server restart all drop the session and every pane in it. Three independent guards now compose:
|
|
829
|
+
|
|
830
|
+
### R12 — Pane lifecycle monitor (5s authoritative budget)
|
|
831
|
+
`_verify_pane_alive` and `_verify_session_alive` (lib_ralph_desk.zsh) check `#{pane_dead}` and `tmux has-session`. The runner invokes `_r12_check_lifecycle` at three sites: (1) immediately after `create_session()`, (2) at the top of every iteration, (3) right after worker dispatch and before the wait-loop. The check polls 5 attempts with 1-second sleep (5-second hard budget). On expiry it writes a BLOCKED sentinel with `reason_category=infra_failure`, `recoverable=true`, `suggested_action=restart` and exits 1 — never an infinite loop.
|
|
832
|
+
|
|
833
|
+
### R13 — Detached session protection (RLP_BACKGROUND only)
|
|
834
|
+
When `tmux new-session -d` collides with an existing session and `RLP_BACKGROUND=1`, the runner appends `-bg-<epoch>-<pid>` to `SESSION_NAME` and runs a `tmux has-session` loop with random 4-digit suffixes until the name is unique. The new session also sets `destroy-unattached off` so the session survives every attached client disconnecting. **Limits**: this option is best-effort; it does NOT survive a manual `tmux kill-session` or a tmux server restart. R12 will detect those events at the next checkpoint.
|
|
835
|
+
|
|
836
|
+
### R14 — Project-scoped runner lockfile (mkdir atomic)
|
|
837
|
+
`RUNNER_LOCKFILE_PATH` keys on `ROOT_HASH` (`shasum || sha1sum || cksum` of the repo root), so two different projects can run runners in parallel while the same project root is single-runner. `RUNNER_LOCKDIR` (`${RUNNER_LOCKFILE_PATH}.d`) is acquired by `mkdir` for true filesystem-level atomicity — no check-then-write race. Stale pids (no longer responding to `kill -0`) are reaped automatically; live duplicates exit 1 with a recovery hint.
|
|
838
|
+
|
|
777
839
|
## 8. Circuit Breaker
|
|
778
840
|
|
|
779
841
|
| Condition | Verdict |
|