@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
|
@@ -3,6 +3,8 @@ import { execFile } from 'node:child_process';
|
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
5
|
|
|
6
|
+
import { autoDismissPrompts } from '../runner/prompt-dismisser.mjs';
|
|
7
|
+
|
|
6
8
|
const execFileAsync = promisify(execFile);
|
|
7
9
|
const SHELL_COMMANDS = new Set(['', 'zsh', 'bash', 'sh']);
|
|
8
10
|
|
|
@@ -13,6 +15,35 @@ export class TimeoutError extends Error {
|
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
// v5.7 §4.17 (Node parity): default-No prompt detected while polling. Caller
|
|
19
|
+
// must write a BLOCKED `infra_failure` sentinel and abort — never auto-Enter,
|
|
20
|
+
// never wait silently for the human.
|
|
21
|
+
export class PromptBlockedError extends Error {
|
|
22
|
+
constructor(message, info = {}) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'PromptBlockedError';
|
|
25
|
+
this.paneId = info.paneId;
|
|
26
|
+
this.category = info.category ?? 'infra_failure';
|
|
27
|
+
this.reason = info.reason ?? message;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// v5.7 §4.22 (E2E real-claude-CLI finding): Worker process exited (back to
|
|
32
|
+
// shell prompt) but no signal/done-claim file was written. fresh-context +
|
|
33
|
+
// file-based architecture is broken — Leader has no way to know what Worker
|
|
34
|
+
// did. zsh runner has `handle_worker_exit_claude` for this; Node leader did
|
|
35
|
+
// not. Throw a specific error so the campaign loop can write BLOCKED with a
|
|
36
|
+
// descriptive reason instead of silent iter-timeout.
|
|
37
|
+
export class WorkerExitedError extends Error {
|
|
38
|
+
constructor(message, info = {}) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'WorkerExitedError';
|
|
41
|
+
this.paneId = info.paneId;
|
|
42
|
+
this.category = info.category ?? 'infra_failure';
|
|
43
|
+
this.reason = info.reason ?? message;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
16
47
|
async function defaultReadFile(filePath) {
|
|
17
48
|
return fs.readFile(filePath, 'utf8');
|
|
18
49
|
}
|
|
@@ -29,6 +60,28 @@ async function defaultGetPaneCommand(paneId) {
|
|
|
29
60
|
return stdout.trim();
|
|
30
61
|
}
|
|
31
62
|
|
|
63
|
+
async function defaultCapturePane(paneId) {
|
|
64
|
+
// v5.7 §4.21 (E2E real-claude-CLI finding): claude v2.x trust prompt is
|
|
65
|
+
// ~30+ lines tall when the pane wraps narrowly. -S -10 missed the question
|
|
66
|
+
// header ("Quick safety check / Is this a project you trust?") so PROMPT_RE
|
|
67
|
+
// never matched and the unknown-prompt fast-fail BLOCKed instead of
|
|
68
|
+
// auto-dismissing. -50 covers the full prompt with margin for typical
|
|
69
|
+
// pane heights.
|
|
70
|
+
const { stdout } = await execFileAsync('tmux', [
|
|
71
|
+
'capture-pane',
|
|
72
|
+
'-t',
|
|
73
|
+
paneId,
|
|
74
|
+
'-p',
|
|
75
|
+
'-S',
|
|
76
|
+
'-50',
|
|
77
|
+
]);
|
|
78
|
+
return stdout;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function defaultSendKeys(paneId, key) {
|
|
82
|
+
await execFileAsync('tmux', ['send-keys', '-t', paneId, key]);
|
|
83
|
+
}
|
|
84
|
+
|
|
32
85
|
function isMissingFileError(error) {
|
|
33
86
|
return error?.code === 'ENOENT';
|
|
34
87
|
}
|
|
@@ -71,11 +124,61 @@ export async function pollForSignal(
|
|
|
71
124
|
timeoutMs = 5000,
|
|
72
125
|
readFile = defaultReadFile,
|
|
73
126
|
getPaneCommand = defaultGetPaneCommand,
|
|
127
|
+
capturePane = defaultCapturePane,
|
|
128
|
+
sendKeys = defaultSendKeys,
|
|
129
|
+
log = () => {},
|
|
74
130
|
} = {},
|
|
75
131
|
) {
|
|
76
132
|
const deadline = Date.now() + timeoutMs;
|
|
133
|
+
let pendingBlock = null;
|
|
134
|
+
// v5.7 §4.22: track whether the worker process was ever observed running.
|
|
135
|
+
// Used to detect "worker started, did some work, then exited without
|
|
136
|
+
// writing signal/done-claim" — fresh-context architecture violation.
|
|
137
|
+
let seenWorkerRunning = false;
|
|
77
138
|
|
|
78
139
|
while (!deadlineExceeded(deadline)) {
|
|
140
|
+
// v5.7 §4.13.b: auto-dismiss mid-execution permission prompts before
|
|
141
|
+
// checking the signal file. Without this, Worker hangs on TUI prompts
|
|
142
|
+
// even with --dangerously-skip-permissions (Bug 4).
|
|
143
|
+
// v5.7 §4.17 (Node parity): default-No prompts must NOT be auto-Entered;
|
|
144
|
+
// they raise a PromptBlockedError so the caller writes BLOCKED and aborts.
|
|
145
|
+
if (paneId) {
|
|
146
|
+
// v0.13.0: detect Claude Code self-modification permission prompts in
|
|
147
|
+
// pane stdout BEFORE attempting auto-dismiss. These cannot be dismissed
|
|
148
|
+
// by --dangerously-skip-permissions and would otherwise hang the worker
|
|
149
|
+
// for the full pollForSignal timeout.
|
|
150
|
+
try {
|
|
151
|
+
const paneContent = await capturePane(paneId);
|
|
152
|
+
const { detectPermissionPrompt } = await import('../runner/prompt-detector.mjs');
|
|
153
|
+
if (detectPermissionPrompt(paneContent)) {
|
|
154
|
+
throw new PromptBlockedError(
|
|
155
|
+
`Permission prompt detected on pane ${paneId} (Claude Code self-modification gate)`,
|
|
156
|
+
{ paneId, category: 'permission_prompt', snippet: paneContent.split(/\r?\n/).slice(-10).join('\n') },
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (err instanceof PromptBlockedError) {
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
// capture failure is non-fatal; fall through to auto-dismiss path.
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await autoDismissPrompts(paneId, {
|
|
167
|
+
capturePane,
|
|
168
|
+
sendKeys,
|
|
169
|
+
log,
|
|
170
|
+
onDefaultNoBlock: (info) => {
|
|
171
|
+
pendingBlock = info;
|
|
172
|
+
},
|
|
173
|
+
}).catch(() => {});
|
|
174
|
+
if (pendingBlock) {
|
|
175
|
+
throw new PromptBlockedError(
|
|
176
|
+
`Default-No prompt on pane ${pendingBlock.paneId}: ${pendingBlock.reason}`,
|
|
177
|
+
pendingBlock,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
79
182
|
try {
|
|
80
183
|
const rawContent = await readFile(signalFile);
|
|
81
184
|
const parsed = JSON.parse(rawContent);
|
|
@@ -89,9 +192,42 @@ export async function pollForSignal(
|
|
|
89
192
|
}
|
|
90
193
|
|
|
91
194
|
return parsed;
|
|
92
|
-
} catch (
|
|
93
|
-
if (!isMissingFileError(
|
|
94
|
-
throw
|
|
195
|
+
} catch (signalError) {
|
|
196
|
+
if (!isMissingFileError(signalError) && !isJsonParseError(signalError)) {
|
|
197
|
+
throw signalError;
|
|
198
|
+
}
|
|
199
|
+
// Signal file missing OR partial JSON. v5.7 §4.22: parity with zsh
|
|
200
|
+
// `handle_worker_exit_claude` — if Worker pane process is back to
|
|
201
|
+
// shell, the worker exited without writing artifacts. Stop polling
|
|
202
|
+
// immediately and surface a WorkerExitedError so the campaign loop
|
|
203
|
+
// can write BLOCKED with reason `worker_exited_without_artifacts`.
|
|
204
|
+
//
|
|
205
|
+
// IMPORTANT: only run the pane-exit check on ENOENT (signal file
|
|
206
|
+
// entirely missing). A SyntaxError means the file EXISTS but the
|
|
207
|
+
// Worker is mid-write (atomic-rename race) — checking pane state
|
|
208
|
+
// here would race against the imminent successful read. Skip the
|
|
209
|
+
// check; next iteration's read will succeed.
|
|
210
|
+
if (paneId && isMissingFileError(signalError)) {
|
|
211
|
+
try {
|
|
212
|
+
const currentCommand = await getPaneCommand(paneId);
|
|
213
|
+
if (SHELL_COMMANDS.has(currentCommand)) {
|
|
214
|
+
if (seenWorkerRunning) {
|
|
215
|
+
throw new WorkerExitedError(
|
|
216
|
+
`Worker pane ${paneId} exited (now '${currentCommand || 'shell'}') without writing signal at ${signalFile} — fresh-context contract violated`,
|
|
217
|
+
{
|
|
218
|
+
paneId,
|
|
219
|
+
category: 'infra_failure',
|
|
220
|
+
reason: 'worker_exited_without_artifacts',
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
} else if (currentCommand) {
|
|
225
|
+
seenWorkerRunning = true;
|
|
226
|
+
}
|
|
227
|
+
} catch (commandError) {
|
|
228
|
+
if (commandError instanceof WorkerExitedError) throw commandError;
|
|
229
|
+
// Other tmux lookup errors: don't end the loop early.
|
|
230
|
+
}
|
|
95
231
|
}
|
|
96
232
|
}
|
|
97
233
|
|
|
@@ -3,6 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { execFile } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
|
|
6
|
+
import { resolveDeskRoot } from '../util/desk-root.mjs';
|
|
7
|
+
|
|
6
8
|
const execFileAsync = promisify(execFile);
|
|
7
9
|
const REQUIRED_ANALYTICS_FIELDS = [
|
|
8
10
|
'iter',
|
|
@@ -596,7 +598,9 @@ export async function generateSVReport({
|
|
|
596
598
|
|
|
597
599
|
export async function readStatus(slug, options = {}) {
|
|
598
600
|
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
599
|
-
const
|
|
601
|
+
const env = options.env ?? process.env;
|
|
602
|
+
const deskRoot = resolveDeskRoot(rootDir, env);
|
|
603
|
+
const statusFile = path.join(deskRoot, 'logs', slug, 'runtime', 'status.json');
|
|
600
604
|
|
|
601
605
|
if (!(await exists(statusFile))) {
|
|
602
606
|
return `No active campaign for ${slug}.`;
|
package/src/node/run.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { initCampaign } from './init/campaign-initializer.mjs';
|
|
5
5
|
import { readStatus } from './reporting/campaign-reporting.mjs';
|
|
6
6
|
import { run as runCampaignMain } from './runner/campaign-main-loop.mjs';
|
|
7
|
+
import { isClaudeEngine } from './cli/command-builder.mjs';
|
|
7
8
|
|
|
8
9
|
const RUN_DEFAULTS = {
|
|
9
10
|
mode: 'agent',
|
|
@@ -194,8 +195,9 @@ async function runInit(args, deps) {
|
|
|
194
195
|
|
|
195
196
|
const slug = args[0];
|
|
196
197
|
const objective = args.slice(1).join(' ').trim() || 'TBD - fill in the objective';
|
|
197
|
-
await deps.initCampaign(slug, objective, { rootDir: deps.cwd });
|
|
198
|
-
|
|
198
|
+
const result = await deps.initCampaign(slug, objective, { rootDir: deps.cwd });
|
|
199
|
+
const deskRoot = result?.paths?.deskRoot ?? path.join(deps.cwd, '.rlp-desk');
|
|
200
|
+
write(deps.stdout, `Initialized ${slug} in ${deskRoot}`);
|
|
199
201
|
return 0;
|
|
200
202
|
}
|
|
201
203
|
|
|
@@ -221,6 +223,33 @@ async function runRunCommand(args, deps) {
|
|
|
221
223
|
|
|
222
224
|
const slug = args[0];
|
|
223
225
|
const options = parseRunOptions(args.slice(1), deps.cwd);
|
|
226
|
+
|
|
227
|
+
// v0.13.0: warn when Claude worker runs in tmux mode. Claude Code's
|
|
228
|
+
// hardcoded sensitive policy used to hang sentinel writes inside
|
|
229
|
+
// <project>/.claude/. After v0.13.0, sentinels live in
|
|
230
|
+
// <project>/.rlp-desk/, but if the user pinned RLP_DESK_RUNTIME_DIR
|
|
231
|
+
// back inside .claude/, the hang can return — surface the warning so
|
|
232
|
+
// they can switch to gpt-5.5:* or --mode agent quickly.
|
|
233
|
+
if (
|
|
234
|
+
!process.env.RLP_DESK_QUIET_WARNINGS
|
|
235
|
+
&& process.env.NODE_ENV !== 'test'
|
|
236
|
+
&& options.mode === 'tmux'
|
|
237
|
+
&& isClaudeEngine(options.workerModel)
|
|
238
|
+
) {
|
|
239
|
+
write(
|
|
240
|
+
deps.stderr,
|
|
241
|
+
'WARNING: Claude worker in tmux mode may hang on .claude/ sentinel writes.',
|
|
242
|
+
);
|
|
243
|
+
write(
|
|
244
|
+
deps.stderr,
|
|
245
|
+
'After v0.13.0, sentinels live in <project>/.rlp-desk/ which avoids this.',
|
|
246
|
+
);
|
|
247
|
+
write(
|
|
248
|
+
deps.stderr,
|
|
249
|
+
'If hang persists, switch to --worker-model gpt-5.5:high (codex) or --mode agent.',
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
224
253
|
const result = await deps.runCampaign(slug, options);
|
|
225
254
|
// governance §1f BLOCKED Surfacing: surface the blocked reason on stderr so
|
|
226
255
|
// the operator (or wrapper script) does not have to grep memo files.
|