@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
@@ -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 (error) {
93
- if (!isMissingFileError(error) && !isJsonParseError(error)) {
94
- throw error;
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 statusFile = path.join(rootDir, '.claude', 'ralph-desk', 'logs', slug, 'runtime', 'status.json');
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
- write(deps.stdout, `Initialized ${slug} in ${path.join(deps.cwd, '.claude', 'ralph-desk')}`);
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.