@ai-dev-methodologies/rlp-desk 0.7.5 → 0.9.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/README.md +58 -0
- package/docs/blueprints/blueprint-pivot-step.md +137 -0
- package/docs/plans/validated-snacking-crayon.md +407 -0
- package/package.json +5 -2
- package/scripts/postinstall.js +91 -51
- package/scripts/uninstall.js +18 -9
- package/src/commands/rlp-desk.md +10 -3
- package/src/governance.md +2 -1
- package/src/node/cli/command-builder.mjs +96 -0
- package/src/node/init/campaign-initializer.mjs +235 -0
- package/src/node/polling/signal-poller.mjs +106 -0
- package/src/node/prompts/prompt-assembler.mjs +213 -0
- package/src/node/reporting/campaign-reporting.mjs +257 -0
- package/src/node/run.mjs +234 -0
- package/src/node/runner/campaign-main-loop.mjs +624 -0
- package/src/node/shared/fs.mjs +23 -0
- package/src/node/shared/paths.mjs +28 -0
- package/src/node/tmux/pane-manager.mjs +77 -0
- package/docs/blueprints/blueprint-v0.4-evolution.md +0 -347
- package/docs/prompts/ralplan-codex-review.md +0 -55
- package/docs/superpowers/plans/2026-04-06-worker-verifier-prompt-restructure.md +0 -179
- package/src/scripts/init_ralph_desk.zsh +0 -885
- package/src/scripts/lib_ralph_desk.zsh +0 -904
- package/src/scripts/run_ralph_desk.zsh +0 -2750
package/scripts/postinstall.js
CHANGED
|
@@ -4,41 +4,19 @@
|
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const os = require("os");
|
|
7
|
-
const
|
|
7
|
+
const pkg = require(path.join(__dirname, "..", "package.json"));
|
|
8
8
|
|
|
9
9
|
const home = os.homedir();
|
|
10
10
|
const claudeDir = path.join(home, ".claude");
|
|
11
11
|
const commandsDir = path.join(claudeDir, "commands");
|
|
12
12
|
const deskDir = path.join(claudeDir, "ralph-desk");
|
|
13
|
-
const pkgDir = path.join(__dirname, "..");
|
|
14
|
-
const pkg = require(path.join(pkgDir, "package.json"));
|
|
15
|
-
|
|
16
|
-
console.log("");
|
|
17
|
-
console.log(" RLP Desk v" + pkg.version);
|
|
18
|
-
console.log(" ================");
|
|
19
|
-
console.log("");
|
|
20
|
-
|
|
21
|
-
// Create directories
|
|
22
|
-
fs.mkdirSync(commandsDir, { recursive: true });
|
|
23
|
-
fs.mkdirSync(deskDir, { recursive: true });
|
|
24
|
-
|
|
25
|
-
// Create docs subdirectories
|
|
26
13
|
const docsDir = path.join(deskDir, "docs");
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
fs.mkdirSync(docsBlueprintsDir, { recursive: true });
|
|
31
|
-
|
|
32
|
-
// Copy files — must match CLAUDE.md "Local File Sync" section exactly
|
|
33
|
-
const copies = [
|
|
34
|
-
// Runtime files
|
|
14
|
+
const nodeDir = path.join(deskDir, "node");
|
|
15
|
+
const pkgDir = path.join(__dirname, "..");
|
|
16
|
+
const runtimeSources = [
|
|
35
17
|
["src/commands/rlp-desk.md", path.join(commandsDir, "rlp-desk.md")],
|
|
36
18
|
["src/governance.md", path.join(deskDir, "governance.md")],
|
|
37
19
|
["src/model-upgrade-table.md", path.join(deskDir, "model-upgrade-table.md")],
|
|
38
|
-
["src/scripts/init_ralph_desk.zsh", path.join(deskDir, "init_ralph_desk.zsh")],
|
|
39
|
-
["src/scripts/run_ralph_desk.zsh", path.join(deskDir, "run_ralph_desk.zsh")],
|
|
40
|
-
["src/scripts/lib_ralph_desk.zsh", path.join(deskDir, "lib_ralph_desk.zsh")],
|
|
41
|
-
// Reference docs
|
|
42
20
|
["README.md", path.join(deskDir, "README.md")],
|
|
43
21
|
["install.sh", path.join(deskDir, "install.sh")],
|
|
44
22
|
["docs/architecture.md", path.join(docsDir, "architecture.md")],
|
|
@@ -46,42 +24,104 @@ const copies = [
|
|
|
46
24
|
["docs/protocol-reference.md", path.join(docsDir, "protocol-reference.md")],
|
|
47
25
|
["docs/TODO-verification-next.md", path.join(docsDir, "TODO-verification-next.md")],
|
|
48
26
|
];
|
|
27
|
+
const legacyFiles = [
|
|
28
|
+
path.join(deskDir, "init_ralph_desk.zsh"),
|
|
29
|
+
path.join(deskDir, "run_ralph_desk.zsh"),
|
|
30
|
+
path.join(deskDir, "lib_ralph_desk.zsh"),
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function getNodeVersion() {
|
|
34
|
+
return process.env.RLP_DESK_NODE_VERSION_OVERRIDE || process.version;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isSupportedNodeVersion(version) {
|
|
38
|
+
const match = /^v(\d+)/.exec(version || "");
|
|
39
|
+
return Boolean(match) && Number(match[1]) >= 16;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureDir(dirPath) {
|
|
43
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function removePath(targetPath) {
|
|
47
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function copyFile(sourceRelativePath, targetPath) {
|
|
51
|
+
ensureDir(path.dirname(targetPath));
|
|
52
|
+
fs.copyFileSync(path.join(pkgDir, sourceRelativePath), targetPath);
|
|
53
|
+
console.log(" + " + targetPath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function copyMarkdownDirectory(sourceRelativeDir, targetDir) {
|
|
57
|
+
const sourceDir = path.join(pkgDir, sourceRelativeDir);
|
|
58
|
+
if (!fs.existsSync(sourceDir)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
for (const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
ensureDir(targetDir);
|
|
63
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
64
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
65
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
copyMarkdownDirectory(path.join(sourceRelativeDir, entry.name), targetPath);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
71
|
+
ensureDir(path.dirname(targetPath));
|
|
72
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
73
|
+
console.log(" + " + targetPath);
|
|
58
74
|
}
|
|
59
75
|
}
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
78
|
+
function copyNodeRuntime(sourceDir, targetDir) {
|
|
79
|
+
removePath(targetDir);
|
|
80
|
+
ensureDir(targetDir);
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
83
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
84
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
copyNodeRuntime(sourcePath, targetPath);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (entry.isFile()) {
|
|
90
|
+
ensureDir(path.dirname(targetPath));
|
|
91
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
92
|
+
console.log(" + " + targetPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
74
95
|
}
|
|
75
96
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
console.log("");
|
|
98
|
+
console.log(" RLP Desk v" + pkg.version);
|
|
99
|
+
console.log(" ================");
|
|
100
|
+
console.log("");
|
|
101
|
+
|
|
102
|
+
if (!isSupportedNodeVersion(getNodeVersion())) {
|
|
103
|
+
console.log(" [warn] RLP Desk requires Node.js >= 16 for the Node rewrite runtime.");
|
|
104
|
+
console.log(" Existing zsh installation was left unchanged.");
|
|
82
105
|
console.log("");
|
|
106
|
+
process.exit(0);
|
|
83
107
|
}
|
|
84
108
|
|
|
109
|
+
ensureDir(commandsDir);
|
|
110
|
+
ensureDir(deskDir);
|
|
111
|
+
ensureDir(docsDir);
|
|
112
|
+
|
|
113
|
+
for (const legacyFile of legacyFiles) {
|
|
114
|
+
removePath(legacyFile);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const [sourcePath, targetPath] of runtimeSources) {
|
|
118
|
+
copyFile(sourcePath, targetPath);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
copyMarkdownDirectory("docs/internal", path.join(docsDir, "internal"));
|
|
122
|
+
copyMarkdownDirectory("docs/blueprints", path.join(docsDir, "blueprints"));
|
|
123
|
+
copyNodeRuntime(path.join(pkgDir, "src", "node"), nodeDir);
|
|
124
|
+
|
|
85
125
|
console.log("");
|
|
86
126
|
console.log(" Done! Open Claude Code and run:");
|
|
87
127
|
console.log(" /rlp-desk brainstorm \"your task description\"");
|
package/scripts/uninstall.js
CHANGED
|
@@ -16,22 +16,31 @@ console.log("");
|
|
|
16
16
|
|
|
17
17
|
const files = [
|
|
18
18
|
path.join(commandsDir, "rlp-desk.md"),
|
|
19
|
-
path.join(deskDir, "init_ralph_desk.zsh"),
|
|
20
|
-
path.join(deskDir, "run_ralph_desk.zsh"),
|
|
21
|
-
path.join(deskDir, "lib_ralph_desk.zsh"),
|
|
22
19
|
path.join(deskDir, "governance.md"),
|
|
20
|
+
path.join(deskDir, "model-upgrade-table.md"),
|
|
21
|
+
path.join(deskDir, "README.md"),
|
|
22
|
+
path.join(deskDir, "install.sh"),
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
-
for (const
|
|
25
|
+
for (const targetPath of files) {
|
|
26
26
|
try {
|
|
27
|
-
fs.
|
|
28
|
-
console.log(" - " +
|
|
27
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
28
|
+
console.log(" - " + targetPath);
|
|
29
29
|
} catch (_) {
|
|
30
|
-
//
|
|
30
|
+
// Ignore missing files.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const subdir of ["docs", "node"]) {
|
|
35
|
+
const targetPath = path.join(deskDir, subdir);
|
|
36
|
+
try {
|
|
37
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
38
|
+
console.log(" - " + targetPath);
|
|
39
|
+
} catch (_) {
|
|
40
|
+
// Ignore missing directories.
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
// Remove ralph-desk dir if empty
|
|
35
44
|
try {
|
|
36
45
|
const remaining = fs.readdirSync(deskDir);
|
|
37
46
|
if (remaining.length === 0) {
|
|
@@ -39,7 +48,7 @@ try {
|
|
|
39
48
|
console.log(" - " + deskDir);
|
|
40
49
|
}
|
|
41
50
|
} catch (_) {
|
|
42
|
-
// Directory may not exist
|
|
51
|
+
// Directory may not exist.
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
console.log("");
|
package/src/commands/rlp-desk.md
CHANGED
|
@@ -23,6 +23,12 @@ Present your suggestion, then wait for the user's confirmation or change.
|
|
|
23
23
|
Ask about these items one by one (or in small groups):
|
|
24
24
|
1. **Slug** — short identifier (e.g., `auth-refactor`). Suggest one, ask if OK.
|
|
25
25
|
2. **Objective** — what the loop achieves
|
|
26
|
+
2.5. **Codebase Exploration** — Before proposing user stories, examine the project:
|
|
27
|
+
- Read the project's entry points, key modules, and test structure
|
|
28
|
+
- Identify architectural patterns in use (frameworks, conventions, test setup)
|
|
29
|
+
- Note constraints the Worker will encounter (dependencies, build system, existing code style)
|
|
30
|
+
- Present findings: "I explored the codebase and found: [patterns], [constraints], [existing tests]. This informs the US breakdown below."
|
|
31
|
+
- If the project is new/empty, skip this step and note "greenfield project."
|
|
26
32
|
3. **User Stories** — discrete units with testable acceptance criteria. Propose a breakdown, ask the user to confirm/modify.
|
|
27
33
|
- Apply INVEST criteria: each US must be Independent, Negotiable, Valuable, Estimable, Small, Testable.
|
|
28
34
|
- **Task Sizing (governance §1c)**: Size each US within the Worker's comfortable zone — smaller than what the Worker can handle, not at its ceiling. Max 3-4 ACs, max 2 files. If a US feels "just barely doable" for the target model, split it further.
|
|
@@ -128,7 +134,10 @@ Do NOT auto-decide iteration unit — the user MUST explicitly choose.
|
|
|
128
134
|
## `init <slug> [objective]`
|
|
129
135
|
|
|
130
136
|
Run: `~/.claude/ralph-desk/init_ralph_desk.zsh <slug> "<objective>" [--mode fresh|improve]`
|
|
131
|
-
If brainstorm was done, auto-fill
|
|
137
|
+
If brainstorm was done, auto-fill:
|
|
138
|
+
- PRD and test-spec with the brainstorm results
|
|
139
|
+
- Campaign memory "Key Decisions" with architectural decisions from brainstorm
|
|
140
|
+
- Campaign memory "Patterns Discovered" with codebase exploration findings (from step 2.5)
|
|
132
141
|
|
|
133
142
|
**After init completes, STOP. Do NOT auto-run the loop.**
|
|
134
143
|
|
|
@@ -372,7 +381,6 @@ If claude engine (default):
|
|
|
372
381
|
```
|
|
373
382
|
Agent(
|
|
374
383
|
description="rlp-desk worker iter-NNN",
|
|
375
|
-
subagent_type="executor",
|
|
376
384
|
model=<worker_model>,
|
|
377
385
|
mode="bypassPermissions",
|
|
378
386
|
prompt=<full worker prompt text>
|
|
@@ -433,7 +441,6 @@ If claude engine (default):
|
|
|
433
441
|
```
|
|
434
442
|
Agent(
|
|
435
443
|
description="rlp-desk verifier iter-NNN (us_id)",
|
|
436
|
-
subagent_type="executor",
|
|
437
444
|
model=<selected_verifier_model>,
|
|
438
445
|
mode="bypassPermissions",
|
|
439
446
|
prompt=<full verifier prompt text with US scope>
|
package/src/governance.md
CHANGED
|
@@ -214,7 +214,7 @@ This is the default behavior, not an optional flag. Without it, IL-1 (Evidence M
|
|
|
214
214
|
### Worker: execution_steps in done-claim.json
|
|
215
215
|
Worker records what was done, in what order, with command evidence in `done-claim.json`:
|
|
216
216
|
- Each step includes: what action, which AC, command executed, exit code, summary
|
|
217
|
-
- Step types: `write_test`, `verify_red`, `implement`, `verify_green`, `refactor`, `commit`, `verify`, `verify_existing`
|
|
217
|
+
- Step types: `plan`, `write_test`, `verify_red`, `implement`, `verify_green`, `refactor`, `commit`, `verify`, `verify_existing`
|
|
218
218
|
- This proves the Worker followed test-first approach and did not skip steps
|
|
219
219
|
- **Existing implementation rule**: When code already exists from a prior iteration/campaign, Worker MAY use `verify_existing` instead of `write_test → verify_red → implement → verify_green`. `verify_existing` requires: run all existing tests, record exit codes, confirm all AC are covered by passing tests. Worker MUST NOT skip recording evidence — `verify_existing` is evidence that existing code satisfies AC, not a shortcut to skip verification.
|
|
220
220
|
|
|
@@ -267,6 +267,7 @@ Verifier records WHY each judgment was made in `verify-verdict.json`:
|
|
|
267
267
|
- Runs commands directly to collect fresh evidence
|
|
268
268
|
- Campaign Memory is for orientation only — not the source of truth
|
|
269
269
|
- Writes verdict (`pass` | `fail` | `request_info`) — if uncertain, use `request_info` with specific questions; Leader decides
|
|
270
|
+
- **Verdict output rule**: MUST write verdict JSON as a FILE (not stdout). Leader polls the file path — terminal output is lost. Evidence strings: include key metrics and exit codes only, do NOT quote full command output or logs verbatim.
|
|
270
271
|
- Delegates deterministic checks (type hints, linting, security) to tools defined in test-spec
|
|
271
272
|
- Focuses on AC verification, semantic review, and smoke tests
|
|
272
273
|
- **Must NEVER modify code or write sentinel files**
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const CLAUDE_BIN = 'claude';
|
|
2
|
+
const CODEX_BIN = 'codex';
|
|
3
|
+
const CLAUDE_MODELS = new Set(['haiku', 'sonnet', 'opus']);
|
|
4
|
+
|
|
5
|
+
function assertTuiMode(mode, builderName) {
|
|
6
|
+
if (mode !== 'tui') {
|
|
7
|
+
throw new Error(`${builderName} unknown mode '${mode}'`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildClaudeCmd(mode, model, options = {}) {
|
|
12
|
+
assertTuiMode(mode, 'buildClaudeCmd');
|
|
13
|
+
|
|
14
|
+
const parts = [
|
|
15
|
+
'DISABLE_OMC=1',
|
|
16
|
+
CLAUDE_BIN,
|
|
17
|
+
'--model',
|
|
18
|
+
model,
|
|
19
|
+
'--mcp-config',
|
|
20
|
+
'\'{"mcpServers":{}}\'',
|
|
21
|
+
'--strict-mcp-config',
|
|
22
|
+
'--dangerously-skip-permissions',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
if (options.effort !== undefined && options.effort !== '') {
|
|
26
|
+
parts.push('--effort', options.effort);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return parts.join(' ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildCodexCmd(mode, model, options = {}) {
|
|
33
|
+
assertTuiMode(mode, 'buildCodexCmd');
|
|
34
|
+
|
|
35
|
+
const parts = [
|
|
36
|
+
CODEX_BIN,
|
|
37
|
+
'-m',
|
|
38
|
+
model,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
if (options.reasoning !== undefined) {
|
|
42
|
+
parts.push('-c', `model_reasoning_effort="${options.reasoning}"`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
parts.push('--disable', 'plugins', '--dangerously-bypass-approvals-and-sandbox');
|
|
46
|
+
|
|
47
|
+
return parts.join(' ');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parseModelFlag(value, role = 'worker') {
|
|
51
|
+
const colonCount = [...value].filter((character) => character === ':').length;
|
|
52
|
+
|
|
53
|
+
if (colonCount > 1) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`invalid format for --${role}-model '${value}'. Use 'model:effort' (claude) or 'model:reasoning' (codex).`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (colonCount === 0) {
|
|
60
|
+
if (!value) {
|
|
61
|
+
throw new Error(`--${role}-model model is required`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
engine: 'claude',
|
|
66
|
+
model: value,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const [model, level] = value.split(':');
|
|
71
|
+
if (!model) {
|
|
72
|
+
throw new Error(`--${role}-model model is required`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (CLAUDE_MODELS.has(model)) {
|
|
76
|
+
return {
|
|
77
|
+
engine: 'claude',
|
|
78
|
+
model,
|
|
79
|
+
effort: level,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (model === 'spark') {
|
|
84
|
+
return {
|
|
85
|
+
engine: 'codex',
|
|
86
|
+
model: 'gpt-5.3-codex-spark',
|
|
87
|
+
reasoning: level,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
engine: 'codex',
|
|
93
|
+
model,
|
|
94
|
+
reasoning: level,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const GITIGNORE_MARKER = '# RLP Desk runtime artifacts';
|
|
5
|
+
const GITIGNORE_RULE = '.claude/ralph-desk/';
|
|
6
|
+
|
|
7
|
+
export async function initCampaign(slug, objective, options = {}) {
|
|
8
|
+
const normalizedSlug = normalizeSlug(slug);
|
|
9
|
+
const normalizedObjective = objective?.trim() || 'TBD - fill in the objective';
|
|
10
|
+
const mode = options.mode ?? 'agent';
|
|
11
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
12
|
+
const tmuxEnv = options.tmuxEnv ?? process.env.TMUX ?? '';
|
|
13
|
+
const deskRoot = path.join(rootDir, '.claude', 'ralph-desk');
|
|
14
|
+
|
|
15
|
+
if (mode === 'tmux' && !tmuxEnv) {
|
|
16
|
+
throw new Error('tmux required');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (mode === 'fresh') {
|
|
20
|
+
await fs.rm(deskRoot, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const paths = buildPaths(rootDir, normalizedSlug);
|
|
24
|
+
await ensureDirectories(paths);
|
|
25
|
+
await ensureGitignore(rootDir);
|
|
26
|
+
|
|
27
|
+
await writeIfMissing(paths.workerPrompt, buildWorkerPrompt(normalizedSlug, normalizedObjective));
|
|
28
|
+
await writeIfMissing(paths.verifierPrompt, buildVerifierPrompt(normalizedSlug));
|
|
29
|
+
await writeIfMissing(paths.contextFile, buildContext(normalizedSlug));
|
|
30
|
+
await writeIfMissing(paths.memoryFile, buildMemory(normalizedSlug, normalizedObjective));
|
|
31
|
+
|
|
32
|
+
const prdContent = options.prdContent ?? buildPrd(normalizedSlug, normalizedObjective);
|
|
33
|
+
await fs.writeFile(paths.prdFile, prdContent, 'utf8');
|
|
34
|
+
await writeIfMissing(paths.testSpecFile, buildTestSpec(normalizedSlug));
|
|
35
|
+
await splitPrdByUs(paths.plansDir, normalizedSlug, prdContent, normalizedObjective);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
slug: normalizedSlug,
|
|
39
|
+
paths,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const init = initCampaign;
|
|
44
|
+
|
|
45
|
+
function normalizeSlug(value) {
|
|
46
|
+
const slug = (value ?? '')
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
49
|
+
.replace(/^-+|-+$/g, '');
|
|
50
|
+
|
|
51
|
+
if (!slug) {
|
|
52
|
+
throw new Error('slug is required');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return slug;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildPaths(rootDir, slug) {
|
|
59
|
+
const deskRoot = path.join(rootDir, '.claude', 'ralph-desk');
|
|
60
|
+
const promptsDir = path.join(deskRoot, 'prompts');
|
|
61
|
+
const plansDir = path.join(deskRoot, 'plans');
|
|
62
|
+
const memosDir = path.join(deskRoot, 'memos');
|
|
63
|
+
const logsDir = path.join(deskRoot, 'logs');
|
|
64
|
+
const contextDir = path.join(deskRoot, 'context');
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
deskRoot,
|
|
68
|
+
promptsDir,
|
|
69
|
+
plansDir,
|
|
70
|
+
memosDir,
|
|
71
|
+
logsDir,
|
|
72
|
+
contextDir,
|
|
73
|
+
workerPrompt: path.join(promptsDir, `${slug}.worker.prompt.md`),
|
|
74
|
+
verifierPrompt: path.join(promptsDir, `${slug}.verifier.prompt.md`),
|
|
75
|
+
contextFile: path.join(contextDir, `${slug}-latest.md`),
|
|
76
|
+
memoryFile: path.join(memosDir, `${slug}-memory.md`),
|
|
77
|
+
prdFile: path.join(plansDir, `prd-${slug}.md`),
|
|
78
|
+
testSpecFile: path.join(plansDir, `test-spec-${slug}.md`),
|
|
79
|
+
campaignLogDir: path.join(logsDir, slug),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function ensureDirectories(paths) {
|
|
84
|
+
await Promise.all(
|
|
85
|
+
[
|
|
86
|
+
paths.promptsDir,
|
|
87
|
+
paths.plansDir,
|
|
88
|
+
paths.memosDir,
|
|
89
|
+
paths.logsDir,
|
|
90
|
+
paths.contextDir,
|
|
91
|
+
paths.campaignLogDir,
|
|
92
|
+
].map((directory) => fs.mkdir(directory, { recursive: true })),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function ensureGitignore(rootDir) {
|
|
97
|
+
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
98
|
+
let content = '';
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
content = await fs.readFile(gitignorePath, 'utf8');
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error.code !== 'ENOENT') {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (content.includes(GITIGNORE_MARKER) && content.includes(GITIGNORE_RULE)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const prefix = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
|
|
113
|
+
const block = `${prefix}${GITIGNORE_MARKER}\n${GITIGNORE_RULE}\n`;
|
|
114
|
+
await fs.writeFile(gitignorePath, `${content}${block}`, 'utf8');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function writeIfMissing(targetPath, content) {
|
|
118
|
+
try {
|
|
119
|
+
await fs.access(targetPath);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error.code !== 'ENOENT') {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
await fs.writeFile(targetPath, content, 'utf8');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildWorkerPrompt(slug, objective) {
|
|
129
|
+
return `Execute the plan for ${slug}.\n\n## Objective\n${objective}\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildVerifierPrompt(slug) {
|
|
133
|
+
return `Independent verifier for Ralph Desk: ${slug}\n`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildContext(slug) {
|
|
137
|
+
return `# ${slug} - Latest Context\n`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildMemory(slug, objective) {
|
|
141
|
+
return `# ${slug} - Campaign Memory\n\n## Stop Status\ncontinue\n\n## Objective\n${objective}\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildPrd(slug, objective) {
|
|
145
|
+
return `# PRD: ${slug}\n\n## Objective\n${objective}\n`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildTestSpec(slug) {
|
|
149
|
+
return `# Test Specification: ${slug}\n`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function splitPrdByUs(plansDir, slug, prdContent, fallbackObjective) {
|
|
153
|
+
const matches = extractUsSections(prdContent);
|
|
154
|
+
const objectiveBlock = extractObjectiveBlock(prdContent, fallbackObjective);
|
|
155
|
+
|
|
156
|
+
await removeExistingSplitFiles(plansDir, slug);
|
|
157
|
+
|
|
158
|
+
await Promise.all(
|
|
159
|
+
matches.map((section) => {
|
|
160
|
+
const usId = section.match(/^## (US-\d{3}):/m)?.[1];
|
|
161
|
+
if (!usId) {
|
|
162
|
+
return Promise.resolve();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const content = `# PRD: ${slug}\n\n${objectiveBlock}\n\n${section}\n`;
|
|
166
|
+
return fs.writeFile(path.join(plansDir, `prd-${slug}-${usId}.md`), content, 'utf8');
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function removeExistingSplitFiles(plansDir, slug) {
|
|
172
|
+
const entries = await fs.readdir(plansDir, { withFileTypes: true });
|
|
173
|
+
const prefix = `prd-${slug}-US-`;
|
|
174
|
+
|
|
175
|
+
await Promise.all(
|
|
176
|
+
entries
|
|
177
|
+
.filter((entry) => entry.isFile() && entry.name.startsWith(prefix) && entry.name.endsWith('.md'))
|
|
178
|
+
.map((entry) => fs.rm(path.join(plansDir, entry.name), { force: true })),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function extractObjectiveBlock(prdContent, fallbackObjective) {
|
|
183
|
+
const lines = prdContent.split(/\r?\n/);
|
|
184
|
+
const collected = [];
|
|
185
|
+
let collecting = false;
|
|
186
|
+
|
|
187
|
+
for (const line of lines) {
|
|
188
|
+
if (/^## Objective\s*$/.test(line)) {
|
|
189
|
+
collecting = true;
|
|
190
|
+
collected.push('## Objective');
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (collecting && /^## US-\d{3}:/.test(line)) {
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (collecting) {
|
|
199
|
+
collected.push(line);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const content = collected.join('\n').trim();
|
|
204
|
+
if (content) {
|
|
205
|
+
return content;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return `## Objective\n${fallbackObjective}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function extractUsSections(prdContent) {
|
|
212
|
+
const lines = prdContent.split(/\r?\n/);
|
|
213
|
+
const sections = [];
|
|
214
|
+
let current = [];
|
|
215
|
+
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
if (/^## US-\d{3}:/.test(line)) {
|
|
218
|
+
if (current.length > 0) {
|
|
219
|
+
sections.push(current.join('\n').trim());
|
|
220
|
+
}
|
|
221
|
+
current = [line];
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (current.length > 0) {
|
|
226
|
+
current.push(line);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (current.length > 0) {
|
|
231
|
+
sections.push(current.join('\n').trim());
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return sections;
|
|
235
|
+
}
|