prloom 0.1.2 → 0.1.4

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 CHANGED
@@ -4,12 +4,12 @@
4
4
 
5
5
  You write a plan (a Markdown checklist), `prloom` turns it into a dedicated git worktree + branch, opens a draft PR, and then iterates one TODO at a time using a configurable coding agent. Review happens in GitHub: comments and review submissions are triaged into new TODOs and pushed back onto the same PR.
6
6
 
7
- `prloom` is designed to be safe to run from multiple clones: all runtime state lives in `.prloom/` (gitignored), so each developer can run their own dispatcher against the PRs they create/track in their local state.
7
+ `prloom` is designed to be safe to run from multiple clones: all runtime state lives in `prloom/.local/` (gitignored), so each developer can run their own dispatcher against the PRs they create/track in their local state.
8
8
 
9
9
  ## How It Works
10
10
 
11
- - Plans start locally in `.prloom/inbox/` (gitignored; clean `git status`).
12
- - The dispatcher ingests a plan into a new branch/worktree at `plans/<id>.md` and opens a draft PR.
11
+ - Plans start locally in `prloom/.local/inbox/` (gitignored; clean `git status`).
12
+ - The dispatcher ingests a plan into a new branch/worktree at `prloom/plans/<id>.md` and opens a draft PR.
13
13
  - The worker agent executes exactly one TODO per iteration and updates the plan in-branch.
14
14
  - PR comments/reviews trigger a triage agent which updates the plan with new TODOs and posts a reply.
15
15
  - When all TODOs are complete, the PR is marked ready; you merge when satisfied.
@@ -88,13 +88,13 @@ bun run build
88
88
  - `npx -y prloom new my-feature`
89
89
  3. Start the dispatcher:
90
90
  - `npx -y prloom start`
91
- 3. Review the draft PR in GitHub.
92
- 4. Leave PR comments or a review; `prloom` triages feedback into TODOs.
93
- 5. When the PR is ready, merge it.
91
+ 4. Review the draft PR in GitHub.
92
+ 5. Leave PR comments or a review; `prloom` triages feedback into TODOs.
93
+ 6. When the PR is ready, merge it.
94
94
 
95
95
  ## Configuration
96
96
 
97
- Create `prloom.config.json` in the repo root:
97
+ Create `prloom/config.json`:
98
98
 
99
99
  ```json
100
100
  {
@@ -102,13 +102,32 @@ Create `prloom.config.json` in the repo root:
102
102
  "default": "opencode",
103
103
  "designer": "codex"
104
104
  },
105
- "worktrees_dir": ".prloom/worktrees",
105
+ "worktrees_dir": "prloom/.local/worktrees",
106
106
  "poll_interval_ms": 60000,
107
107
  "base_branch": "main"
108
108
  }
109
109
  ```
110
110
 
111
+ ## Repository Context
112
+
113
+ You can provide repository-specific context to agents by creating markdown files in the `prloom/` directory:
114
+
115
+ ```
116
+ repo/
117
+ ├── prloom/
118
+ │ ├── config.json # Configuration
119
+ │ ├── plans/ # Committed plans (on PR branches)
120
+ │ ├── planner.md # Appended to designer prompts
121
+ │ ├── worker.md # Appended to worker prompts
122
+ │ └── .local/ # Gitignored (runtime state)
123
+ ```
124
+
125
+ - **`prloom/planner.md`**: Architecture info, coding conventions, design patterns
126
+ - **`prloom/worker.md`**: Build commands, test patterns, implementation guidelines
127
+
128
+ These files are appended to the respective agent prompts automatically.
129
+
111
130
  ## Notes
112
131
 
113
- - Runtime state is stored under `.prloom/` (gitignored).
114
- - The plan file is committed on the PR branch at `plans/<id>.md` and lands on the configured `base_branch` only when you merge the PR.
132
+ - Runtime state is stored under `prloom/.local/` (gitignored).
133
+ - The plan file is committed on the PR branch at `prloom/plans/<id>.md` and lands on the configured `base_branch` when you merge the PR.
package/dist/cli/index.js CHANGED
@@ -7238,14 +7238,46 @@ var init_execa = __esm(() => {
7238
7238
  } = getIpcExport());
7239
7239
  });
7240
7240
 
7241
+ // src/lib/adapters/tmux.ts
7242
+ async function waitForTmuxSession(sessionName) {
7243
+ while (true) {
7244
+ const { exitCode } = await execa("tmux", ["has-session", "-t", sessionName], {
7245
+ reject: false
7246
+ });
7247
+ if (exitCode !== 0) {
7248
+ return;
7249
+ }
7250
+ await new Promise((resolve5) => setTimeout(resolve5, 1000));
7251
+ }
7252
+ }
7253
+ var init_tmux = __esm(() => {
7254
+ init_execa();
7255
+ });
7256
+
7241
7257
  // src/lib/adapters/codex.ts
7242
7258
  var codexAdapter;
7243
7259
  var init_codex = __esm(() => {
7244
7260
  init_execa();
7261
+ init_tmux();
7245
7262
  codexAdapter = {
7246
7263
  name: "codex",
7247
- async execute({ cwd, prompt }) {
7248
- const result = await execa("codex", ["exec", prompt, "--full-auto"], {
7264
+ async execute({ cwd, prompt, tmux }) {
7265
+ const args = ["exec", prompt, "--full-auto"];
7266
+ if (tmux) {
7267
+ await execa("tmux", [
7268
+ "new-session",
7269
+ "-d",
7270
+ "-s",
7271
+ tmux.sessionName,
7272
+ "-c",
7273
+ cwd,
7274
+ "codex",
7275
+ ...args
7276
+ ], { reject: false });
7277
+ await waitForTmuxSession(tmux.sessionName);
7278
+ return { exitCode: 0 };
7279
+ }
7280
+ const result = await execa("codex", args, {
7249
7281
  cwd,
7250
7282
  timeout: 0,
7251
7283
  reject: false
@@ -7266,9 +7298,25 @@ var init_codex = __esm(() => {
7266
7298
  var opencodeAdapter;
7267
7299
  var init_opencode = __esm(() => {
7268
7300
  init_execa();
7301
+ init_tmux();
7269
7302
  opencodeAdapter = {
7270
7303
  name: "opencode",
7271
- async execute({ cwd, prompt }) {
7304
+ async execute({ cwd, prompt, tmux }) {
7305
+ if (tmux) {
7306
+ await execa("tmux", [
7307
+ "new-session",
7308
+ "-d",
7309
+ "-s",
7310
+ tmux.sessionName,
7311
+ "-c",
7312
+ cwd,
7313
+ "opencode",
7314
+ "run",
7315
+ prompt
7316
+ ], { reject: false });
7317
+ await waitForTmuxSession(tmux.sessionName);
7318
+ return { exitCode: 0 };
7319
+ }
7272
7320
  const result = await execa("opencode", ["run", prompt], {
7273
7321
  cwd,
7274
7322
  timeout: 0,
@@ -7290,10 +7338,26 @@ var init_opencode = __esm(() => {
7290
7338
  var claudeAdapter;
7291
7339
  var init_claude = __esm(() => {
7292
7340
  init_execa();
7341
+ init_tmux();
7293
7342
  claudeAdapter = {
7294
7343
  name: "claude",
7295
- async execute({ cwd, prompt }) {
7296
- const result = await execa("claude", ["-p", prompt, "--dangerously-skip-permissions"], {
7344
+ async execute({ cwd, prompt, tmux }) {
7345
+ const args = ["-p", prompt, "--dangerously-skip-permissions"];
7346
+ if (tmux) {
7347
+ await execa("tmux", [
7348
+ "new-session",
7349
+ "-d",
7350
+ "-s",
7351
+ tmux.sessionName,
7352
+ "-c",
7353
+ cwd,
7354
+ "claude",
7355
+ ...args
7356
+ ], { reject: false });
7357
+ await waitForTmuxSession(tmux.sessionName);
7358
+ return { exitCode: 0 };
7359
+ }
7360
+ const result = await execa("claude", args, {
7297
7361
  cwd,
7298
7362
  timeout: 0,
7299
7363
  reject: false
@@ -7314,7 +7378,7 @@ var manualAdapter;
7314
7378
  var init_manual = __esm(() => {
7315
7379
  manualAdapter = {
7316
7380
  name: "manual",
7317
- async execute({ cwd, prompt }) {
7381
+ async execute({ cwd, prompt, tmux }) {
7318
7382
  console.log("⚠️ Manual agent: execute() called but should be skipped by dispatcher.");
7319
7383
  console.log(" This plan is intended for IDE-driven execution.");
7320
7384
  return { exitCode: 0 };
@@ -7357,7 +7421,7 @@ var init_adapters = __esm(() => {
7357
7421
  import { join as join2, resolve as resolve5 } from "path";
7358
7422
  import { existsSync, readFileSync as readFileSync6 } from "fs";
7359
7423
  function loadConfig(repoRoot) {
7360
- const configPath = join2(repoRoot, "prloom.config.json");
7424
+ const configPath = join2(repoRoot, "prloom", "config.json");
7361
7425
  if (!existsSync(configPath)) {
7362
7426
  return { ...DEFAULTS };
7363
7427
  }
@@ -7394,7 +7458,7 @@ var init_config = __esm(() => {
7394
7458
  agents: {
7395
7459
  default: "opencode"
7396
7460
  },
7397
- worktrees_dir: ".prloom/worktrees",
7461
+ worktrees_dir: "prloom/.local/worktrees",
7398
7462
  poll_interval_ms: 60000,
7399
7463
  base_branch: "main"
7400
7464
  };
@@ -7415,7 +7479,9 @@ async function runInit(cwd, opts = {}) {
7415
7479
  await ensureGhAuthed();
7416
7480
  await ensureGhRepoResolvable(repoRoot);
7417
7481
  const defaultBranch = await detectDefaultBranch(repoRoot);
7418
- const configPath = join3(repoRoot, "prloom.config.json");
7482
+ const configPath = join3(repoRoot, "prloom", "config.json");
7483
+ const prloomDir = join3(repoRoot, "prloom");
7484
+ mkdirSync(prloomDir, { recursive: true });
7419
7485
  if (!existsSync2(configPath) || opts.force) {
7420
7486
  const existing = loadConfig(repoRoot);
7421
7487
  const config = {
@@ -7430,12 +7496,14 @@ async function runInit(cwd, opts = {}) {
7430
7496
  } else {
7431
7497
  console.log(`Found existing ${configPath} (leaving unchanged)`);
7432
7498
  }
7433
- const prloomDir = join3(repoRoot, ".prloom");
7434
- const inboxDir = join3(prloomDir, "inbox");
7435
- const plansDir = join3(prloomDir, "plans");
7499
+ const localDir = join3(prloomDir, ".local");
7500
+ const inboxDir = join3(localDir, "inbox");
7501
+ const plansStateDir = join3(localDir, "plans");
7436
7502
  mkdirSync(inboxDir, { recursive: true });
7503
+ mkdirSync(plansStateDir, { recursive: true });
7504
+ const plansDir = join3(prloomDir, "plans");
7437
7505
  mkdirSync(plansDir, { recursive: true });
7438
- await ensureGitignoreEntry(repoRoot, ".prloom/");
7506
+ await ensureGitignoreEntry(repoRoot, "prloom/.local/");
7439
7507
  console.log("✅ prloom initialized");
7440
7508
  console.log(`Base branch: ${defaultBranch}`);
7441
7509
  if (!opts.yes) {
@@ -11138,7 +11206,7 @@ ${plan.progressLog}`;
11138
11206
  function generatePlanSkeleton(id, agent, baseBranch) {
11139
11207
  const frontmatter = {
11140
11208
  id,
11141
- status: "queued"
11209
+ status: "draft"
11142
11210
  };
11143
11211
  if (agent) {
11144
11212
  frontmatter.agent = agent;
@@ -16834,11 +16902,10 @@ You are implementing exactly ONE task from this plan.
16834
16902
 
16835
16903
  You are processing PR feedback for an active plan. Your job is to:
16836
16904
 
16837
- 1. Analyze all review feedback (comments, reviews, inline comments)
16838
- 2. Create specific, actionable TODO items for the plan
16839
- 3. Group related feedback into fewer tasks where appropriate
16905
+ 1. **Classify** each feedback item by type
16906
+ 2. **Respond** appropriately based on the type
16907
+ 3. **Update** the plan if changes are needed
16840
16908
  4. Detect if a rebase was requested
16841
- 5. Compose a reply message for the reviewer(s)
16842
16909
 
16843
16910
  ## Feedback to Process
16844
16911
 
@@ -16848,9 +16915,36 @@ You are processing PR feedback for an active plan. Your job is to:
16848
16915
 
16849
16916
  {{plan}}
16850
16917
 
16851
- ## Instructions
16918
+ ---
16919
+
16920
+ ## Step 1: Classify Each Feedback Item
16921
+
16922
+ Before acting, identify the type of each feedback item:
16923
+
16924
+ | Type | Description | Action |
16925
+ | ------------------- | --------------------------------------------- | ------------------------ |
16926
+ | **Question** | Asking "why", "how", or seeking clarification | Answer in reply, NO TODO |
16927
+ | **Change Request** | Asking for code modifications | Create specific TODO |
16928
+ | **Approval/Praise** | Positive feedback, LGTM, approval | Acknowledge briefly |
16929
+ | **Process Request** | Rebase, update branch, etc. | Set flag, acknowledge |
16930
+
16931
+ ## Step 2: Handle Each Type
16932
+
16933
+ ### Questions → Answer Directly
16934
+
16935
+ For questions, **answer them substantively in your reply**:
16936
+
16937
+ - Explore the codebase to find the answer if needed
16938
+ - Explain the reasoning behind implementation decisions
16939
+ - Reference specific code locations or commits if helpful
16940
+ - Do NOT create a TODO like "answer the question" - just answer it
16852
16941
 
16853
- ### Adding TODOs
16942
+ Example question: "Why did you use a polling approach instead of webhooks?"
16943
+ → Reply with the actual reasoning, don't create a TODO.
16944
+
16945
+ ### Change Requests → Create TODOs
16946
+
16947
+ For explicit requests to modify code:
16854
16948
 
16855
16949
  - Edit the plan file directly to add actionable TODOs
16856
16950
  - Add them at the end of the ## TODO section
@@ -16860,13 +16954,23 @@ You are processing PR feedback for an active plan. Your job is to:
16860
16954
  - "Update function X to handle null input"
16861
16955
  - "Add test for Y edge case"
16862
16956
  - "Rename Z for clarity"
16863
- - "Remove deprecated code in file A"
16864
- - Group related comments into single tasks when logical
16957
+ - Group related requests into single tasks when logical
16865
16958
  - If the plan status is \`done\` and you add TODOs, change status to \`active\`
16866
16959
 
16867
- ### Writing the Result File
16960
+ ### Approval/Praise Acknowledge
16961
+
16962
+ Simply thank the reviewer in your reply. No TODO needed.
16963
+
16964
+ ### Process Requests → Set Flag
16965
+
16966
+ If any comment mentions rebase, update branch, or similar:
16967
+
16968
+ - Set \`rebase_requested: true\` in the result file
16969
+ - Acknowledge in your reply
16868
16970
 
16869
- After editing the plan, you MUST write \`.prloom/triage-result.json\` with:
16971
+ ## Step 3: Write the Result File
16972
+
16973
+ After processing, you MUST write \`prloom/.local/triage-result.json\`:
16870
16974
 
16871
16975
  \`\`\`json
16872
16976
  {
@@ -16877,22 +16981,23 @@ After editing the plan, you MUST write \`.prloom/triage-result.json\` with:
16877
16981
 
16878
16982
  **Required fields:**
16879
16983
 
16880
- - \`reply_markdown\`: Your response to post on the PR (ALWAYS required, even for no-ops)
16881
- - \`rebase_requested\`: Set to \`true\` if any comment mentions rebase, update branch, or similar
16984
+ - \`reply_markdown\`: Your response to post on the PR (ALWAYS required)
16985
+ - \`rebase_requested\`: Set to \`true\` if rebase was requested
16882
16986
 
16883
16987
  ### Reply Guidelines
16884
16988
 
16885
16989
  - Be polite and professional
16886
- - Acknowledge specific feedback points
16887
- - Explain what TODOs were created
16888
- - If no changes needed, explain why
16889
- - Keep it concise
16990
+ - **Answer questions directly** - this is the most important part
16991
+ - Explain what TODOs were created (if any)
16992
+ - If a request doesn't require changes, explain why
16993
+ - Keep it concise but complete
16890
16994
 
16891
16995
  ## Critical Rules
16892
16996
 
16893
- 1. You MUST write \`.prloom/triage-result.json\` even if you add no TODOs
16997
+ 1. You MUST write \`prloom/.local/triage-result.json\` even if you add no TODOs
16894
16998
  2. The result file must contain valid JSON only, no markdown wrapper
16895
16999
  3. Failure to write the result file will mark the plan as blocked
17000
+ 4. **Questions should be answered, not converted to TODOs**
16896
17001
  `
16897
17002
  };
16898
17003
  });
@@ -16903,32 +17008,72 @@ import { join as join4 } from "path";
16903
17008
  function loadTemplate(_repoRoot, name) {
16904
17009
  return BUILTIN_PROMPTS[name];
16905
17010
  }
17011
+ function loadAgentContext(repoRoot, agentType) {
17012
+ const contextPath = join4(repoRoot, "prloom", `${agentType}.md`);
17013
+ if (!existsSync3(contextPath)) {
17014
+ return "";
17015
+ }
17016
+ return readFileSync9(contextPath, "utf-8");
17017
+ }
16906
17018
  function renderWorkerPrompt(repoRoot, plan, todo) {
16907
17019
  const template = loadTemplate(repoRoot, "worker");
16908
17020
  const compiled = import_handlebars.default.compile(template);
16909
- return compiled({
17021
+ let prompt = compiled({
16910
17022
  current_todo: `TODO #${todo.index}: ${todo.text}`,
16911
17023
  plan: plan.raw
16912
17024
  });
17025
+ const context = loadAgentContext(repoRoot, "worker");
17026
+ if (context) {
17027
+ prompt += `
17028
+
17029
+ ---
17030
+
17031
+ # Repository Context
17032
+
17033
+ ${context}`;
17034
+ }
17035
+ return prompt;
16913
17036
  }
16914
17037
  function renderDesignerNewPrompt(repoPath, planPath, baseBranch, workerAgent, userDescription) {
16915
17038
  const template = BUILTIN_PROMPTS["designer_new"];
16916
17039
  const compiled = import_handlebars.default.compile(template);
16917
- return compiled({
17040
+ let prompt = compiled({
16918
17041
  repo_path: repoPath,
16919
17042
  plan_path: planPath,
16920
17043
  base_branch: baseBranch,
16921
17044
  worker_agent: workerAgent,
16922
17045
  user_description: userDescription ?? ""
16923
17046
  });
17047
+ const context = loadAgentContext(repoPath, "planner");
17048
+ if (context) {
17049
+ prompt += `
17050
+
17051
+ ---
17052
+
17053
+ # Repository Context
17054
+
17055
+ ${context}`;
17056
+ }
17057
+ return prompt;
16924
17058
  }
16925
- function renderDesignerEditPrompt(planPath, existingPlan) {
17059
+ function renderDesignerEditPrompt(repoPath, planPath, existingPlan) {
16926
17060
  const template = BUILTIN_PROMPTS["designer_edit"];
16927
17061
  const compiled = import_handlebars.default.compile(template);
16928
- return compiled({
17062
+ let prompt = compiled({
16929
17063
  plan_path: planPath,
16930
17064
  existing_plan: existingPlan
16931
17065
  });
17066
+ const context = loadAgentContext(repoPath, "planner");
17067
+ if (context) {
17068
+ prompt += `
17069
+
17070
+ ---
17071
+
17072
+ # Repository Context
17073
+
17074
+ ${context}`;
17075
+ }
17076
+ return prompt;
16932
17077
  }
16933
17078
  function renderTriagePrompt(repoRoot, plan, feedback) {
16934
17079
  const template = loadTemplate(repoRoot, "review_triage");
@@ -16944,6 +17089,10 @@ ${f.body}`;
16944
17089
  entry += `
16945
17090
 
16946
17091
  *Review: ${f.reviewState}*`;
17092
+ if (f.inReplyToId)
17093
+ entry += `
17094
+
17095
+ *In reply to comment #${f.inReplyToId}*`;
16947
17096
  return entry;
16948
17097
  }).join(`
16949
17098
 
@@ -16975,7 +17124,7 @@ function readTriageResultFile(worktreePath) {
16975
17124
  rebase_requested: result.rebase_requested
16976
17125
  };
16977
17126
  }
16978
- var import_handlebars, TRIAGE_RESULT_FILE = ".prloom/triage-result.json";
17127
+ var import_handlebars, TRIAGE_RESULT_FILE = "prloom/.local/triage-result.json";
16979
17128
  var init_template2 = __esm(() => {
16980
17129
  init_prompt_sources();
16981
17130
  import_handlebars = __toESM(require_lib(), 1);
@@ -17110,7 +17259,7 @@ function deleteInboxPlan(repoRoot, planId) {
17110
17259
  unlinkSync2(inboxPath);
17111
17260
  }
17112
17261
  }
17113
- var SWARM_DIR = ".prloom", STATE_FILE = "state.json", LOCK_FILE = "lock", PLANS_DIR = "plans", INBOX_DIR = "inbox";
17262
+ var SWARM_DIR = "prloom/.local", STATE_FILE = "state.json", LOCK_FILE = "lock", PLANS_DIR = "plans", INBOX_DIR = "inbox";
17114
17263
  var init_state = () => {};
17115
17264
 
17116
17265
  // node_modules/nanoid/url-alphabet/index.js
@@ -17237,7 +17386,7 @@ function copyFileToWorktree(srcPath, worktreePath, destRelPath) {
17237
17386
  copyFileSync(srcPath, destPath);
17238
17387
  }
17239
17388
  function ensureWorktreePrloomDir(worktreePath) {
17240
- const prloomDir = join6(worktreePath, ".prloom");
17389
+ const prloomDir = join6(worktreePath, "prloom", ".local");
17241
17390
  if (!existsSync5(prloomDir)) {
17242
17391
  mkdirSync3(prloomDir, { recursive: true });
17243
17392
  }
@@ -17247,6 +17396,23 @@ var init_git = __esm(() => {
17247
17396
  init_nanoid();
17248
17397
  });
17249
17398
 
17399
+ // src/cli/prompt.ts
17400
+ import * as readline from "readline";
17401
+ function confirm(message) {
17402
+ const rl = readline.createInterface({
17403
+ input: process.stdin,
17404
+ output: process.stdout
17405
+ });
17406
+ return new Promise((resolve6) => {
17407
+ rl.question(`${message} (y/N) `, (answer) => {
17408
+ rl.close();
17409
+ const normalized = answer.trim().toLowerCase();
17410
+ resolve6(normalized === "y" || normalized === "yes");
17411
+ });
17412
+ });
17413
+ }
17414
+ var init_prompt = () => {};
17415
+
17250
17416
  // src/cli/new.ts
17251
17417
  var exports_new = {};
17252
17418
  __export(exports_new, {
@@ -17284,8 +17450,8 @@ async function runNew(repoRoot, planId, agentOverride, noDesigner) {
17284
17450
  console.log(`Worker agent: ${workerAgent}`);
17285
17451
  if (noDesigner) {
17286
17452
  console.log("");
17287
- console.log("Plan skeleton created. Edit manually or use your IDE.");
17288
- console.log("Run 'prloom start' to dispatch when ready.");
17453
+ console.log("Plan skeleton created (status: draft).");
17454
+ console.log("Use 'prloom edit' to design, then queue for dispatch.");
17289
17455
  return;
17290
17456
  }
17291
17457
  const adapter = getAdapter(designerAgent);
@@ -17294,8 +17460,15 @@ async function runNew(repoRoot, planId, agentOverride, noDesigner) {
17294
17460
  console.log("Starting Designer session to fill in the plan...");
17295
17461
  const prompt = renderDesignerNewPrompt(repoRoot, planPath, baseBranch, workerAgent);
17296
17462
  await adapter.interactive({ cwd: repoRoot, prompt });
17463
+ console.log("");
17297
17464
  console.log("Designer session ended.");
17298
- console.log("Plan is now in inbox. Run 'prloom start' to dispatch.");
17465
+ const shouldQueue = await confirm("Queue this plan for the dispatcher?");
17466
+ if (shouldQueue) {
17467
+ setStatus(planPath, "queued");
17468
+ console.log("Plan queued. Run 'prloom start' to dispatch.");
17469
+ } else {
17470
+ console.log("Plan left as draft. Use 'prloom edit' to continue later.");
17471
+ }
17299
17472
  }
17300
17473
  var init_new = __esm(() => {
17301
17474
  init_config();
@@ -17304,6 +17477,7 @@ var init_new = __esm(() => {
17304
17477
  init_template2();
17305
17478
  init_state();
17306
17479
  init_git();
17480
+ init_prompt();
17307
17481
  });
17308
17482
 
17309
17483
  // src/cli/edit.ts
@@ -17319,9 +17493,11 @@ async function runEdit(repoRoot, planId, agentOverride, noDesigner) {
17319
17493
  const inboxPath = getInboxPath(repoRoot, planId);
17320
17494
  let planPath;
17321
17495
  let cwd;
17496
+ let isInbox = false;
17322
17497
  if (existsSync7(inboxPath)) {
17323
17498
  planPath = inboxPath;
17324
17499
  cwd = repoRoot;
17500
+ isInbox = true;
17325
17501
  console.log(`Editing inbox plan: ${planId}`);
17326
17502
  } else {
17327
17503
  const ps = state.plans[planId];
@@ -17349,15 +17525,30 @@ async function runEdit(repoRoot, planId, agentOverride, noDesigner) {
17349
17525
  const agentName = agentOverride ?? config.agents.designer ?? config.agents.default;
17350
17526
  const adapter = getAdapter(agentName);
17351
17527
  console.log(`Agent: ${agentName}`);
17352
- const prompt = renderDesignerEditPrompt(planPath, existingPlan);
17528
+ const prompt = renderDesignerEditPrompt(cwd, planPath, existingPlan);
17353
17529
  await adapter.interactive({ cwd, prompt });
17530
+ console.log("");
17354
17531
  console.log("Designer session ended.");
17532
+ if (isInbox) {
17533
+ const plan = parsePlan(planPath);
17534
+ if (plan.frontmatter.status === "draft") {
17535
+ const shouldQueue = await confirm("Queue this plan for the dispatcher?");
17536
+ if (shouldQueue) {
17537
+ setStatus(planPath, "queued");
17538
+ console.log("Plan queued. Run 'prloom start' to dispatch.");
17539
+ } else {
17540
+ console.log("Plan left as draft.");
17541
+ }
17542
+ }
17543
+ }
17355
17544
  }
17356
17545
  var init_edit = __esm(() => {
17357
17546
  init_config();
17358
17547
  init_adapters();
17359
17548
  init_template2();
17360
17549
  init_state();
17550
+ init_plan();
17551
+ init_prompt();
17361
17552
  });
17362
17553
 
17363
17554
  // src/lib/ipc.ts
@@ -17375,10 +17566,10 @@ import {
17375
17566
  mkdirSync as mkdirSync4
17376
17567
  } from "fs";
17377
17568
  function getControlPath(repoRoot) {
17378
- return join8(repoRoot, ".prloom", CONTROL_FILE);
17569
+ return join8(repoRoot, "prloom", ".local", CONTROL_FILE);
17379
17570
  }
17380
17571
  function enqueue(repoRoot, cmd) {
17381
- const prloomDir = join8(repoRoot, ".prloom");
17572
+ const prloomDir = join8(repoRoot, "prloom", ".local");
17382
17573
  if (!existsSync8(prloomDir)) {
17383
17574
  mkdirSync4(prloomDir, { recursive: true });
17384
17575
  }
@@ -17508,7 +17699,7 @@ async function getPRReviewComments(repoRoot, prNumber) {
17508
17699
  "api",
17509
17700
  `repos/{owner}/{repo}/pulls/${prNumber}/comments`,
17510
17701
  "--jq",
17511
- ".[] | {id: .id, author: .user.login, body: .body, path: .path, line: .line, createdAt: .created_at}"
17702
+ ".[] | {id: .id, author: .user.login, body: .body, path: .path, line: .line, createdAt: .created_at, inReplyToId: .in_reply_to_id}"
17512
17703
  ], { cwd: repoRoot });
17513
17704
  if (!stdout.trim())
17514
17705
  return [];
@@ -17522,7 +17713,8 @@ async function getPRReviewComments(repoRoot, prNumber) {
17522
17713
  body: obj.body,
17523
17714
  path: obj.path,
17524
17715
  line: obj.line,
17525
- createdAt: obj.createdAt
17716
+ createdAt: obj.createdAt,
17717
+ inReplyToId: obj.inReplyToId || undefined
17526
17718
  };
17527
17719
  });
17528
17720
  }
@@ -17582,7 +17774,7 @@ __export(exports_dispatcher, {
17582
17774
  });
17583
17775
  import { join as join9 } from "path";
17584
17776
  import { existsSync as existsSync9, statSync as statSync5 } from "fs";
17585
- async function runDispatcher(repoRoot) {
17777
+ async function runDispatcher(repoRoot, options2 = {}) {
17586
17778
  const config = loadConfig(repoRoot);
17587
17779
  const worktreesDir = resolveWorktreesDir(repoRoot, config);
17588
17780
  acquireLock(repoRoot);
@@ -17610,7 +17802,7 @@ async function runDispatcher(repoRoot) {
17610
17802
  await handleCommand2(state, cmd);
17611
17803
  }
17612
17804
  await ingestInboxPlans(repoRoot, worktreesDir, config, state);
17613
- await processActivePlans(repoRoot, config, state, botLogin);
17805
+ await processActivePlans(repoRoot, config, state, botLogin, options2);
17614
17806
  saveState(repoRoot, state);
17615
17807
  await sleepUntilIpcOrTimeout(repoRoot, state.control_cursor, config.poll_interval_ms);
17616
17808
  } catch (error) {
@@ -17629,11 +17821,14 @@ async function ingestInboxPlans(repoRoot, worktreesDir, config, state) {
17629
17821
  console.error(`ID mismatch: file ${planId}.md has id: ${plan.frontmatter.id}, skipping`);
17630
17822
  continue;
17631
17823
  }
17824
+ if (plan.frontmatter.status === "draft") {
17825
+ continue;
17826
+ }
17632
17827
  console.log(`\uD83D\uDCE5 Ingesting inbox plan: ${planId}`);
17633
17828
  const baseBranch = plan.frontmatter.base_branch ?? config.base_branch;
17634
17829
  const branch = await createBranchName(planId);
17635
17830
  const worktreePath = await createWorktree(repoRoot, worktreesDir, branch, baseBranch);
17636
- const planRelpath = `plans/${planId}.md`;
17831
+ const planRelpath = `prloom/plans/${planId}.md`;
17637
17832
  copyFileToWorktree(inboxPath, worktreePath, planRelpath);
17638
17833
  const worktreePlanPath = join9(worktreePath, planRelpath);
17639
17834
  setStatus(worktreePlanPath, "active");
@@ -17667,7 +17862,7 @@ function getFeedbackPollDecision(opts) {
17667
17862
  shouldUpdateLastPolledAt: !pollOnce && shouldPoll
17668
17863
  };
17669
17864
  }
17670
- async function processActivePlans(repoRoot, config, state, botLogin) {
17865
+ async function processActivePlans(repoRoot, config, state, botLogin, options2 = {}) {
17671
17866
  for (const [planId, ps] of Object.entries(state.plans)) {
17672
17867
  try {
17673
17868
  const planPath = join9(ps.worktree, ps.planRelpath);
@@ -17702,7 +17897,7 @@ async function processActivePlans(repoRoot, config, state, botLogin) {
17702
17897
  if (newFeedback.length > 0) {
17703
17898
  console.log(`\uD83D\uDCAC ${newFeedback.length} new feedback for ${planId}`);
17704
17899
  if (!isManualAgent) {
17705
- await runTriage(repoRoot, config, ps, plan, newFeedback);
17900
+ await runTriage(repoRoot, config, ps, plan, newFeedback, options2);
17706
17901
  }
17707
17902
  plan = parsePlan(planPath);
17708
17903
  const maxIds = getMaxFeedbackIds(newFeedback);
@@ -17725,7 +17920,15 @@ async function processActivePlans(repoRoot, config, state, botLogin) {
17725
17920
  const prompt = renderWorkerPrompt(repoRoot, plan, todo);
17726
17921
  const agentName = plan.frontmatter.agent ?? config.agents.default;
17727
17922
  const adapter = getAdapter(agentName);
17728
- await adapter.execute({ cwd: ps.worktree, prompt });
17923
+ const tmuxConfig = options2.tmux ? { sessionName: `prloom-${planId}` } : undefined;
17924
+ if (tmuxConfig) {
17925
+ ps.tmuxSession = tmuxConfig.sessionName;
17926
+ console.log(` [spawned in tmux session: ${tmuxConfig.sessionName}]`);
17927
+ }
17928
+ await adapter.execute({ cwd: ps.worktree, prompt, tmux: tmuxConfig });
17929
+ if (tmuxConfig) {
17930
+ ps.tmuxSession = undefined;
17931
+ }
17729
17932
  const committed = await commitAll(ps.worktree, `[prloom] ${planId}: TODO #${todo.index}`);
17730
17933
  if (committed) {
17731
17934
  await push(ps.worktree, ps.branch);
@@ -17773,13 +17976,14 @@ async function pollNewFeedback(repoRoot, ps, botLogin) {
17773
17976
  };
17774
17977
  return filterNewFeedback(allFeedback, cursors, botLogin);
17775
17978
  }
17776
- async function runTriage(repoRoot, config, ps, plan, feedback) {
17979
+ async function runTriage(repoRoot, config, ps, plan, feedback, options2 = {}) {
17777
17980
  ensureWorktreePrloomDir(ps.worktree);
17778
17981
  const triageAgent = config.agents.designer ?? config.agents.default;
17779
17982
  const adapter = getAdapter(triageAgent);
17780
17983
  const prompt = renderTriagePrompt(repoRoot, plan, feedback);
17781
17984
  console.log(`\uD83D\uDD0D Running triage for ${plan.frontmatter.id}...`);
17782
- await adapter.execute({ cwd: ps.worktree, prompt });
17985
+ const tmuxConfig = options2.tmux ? { sessionName: `prloom-triage-${plan.frontmatter.id}` } : undefined;
17986
+ await adapter.execute({ cwd: ps.worktree, prompt, tmux: tmuxConfig });
17783
17987
  try {
17784
17988
  const result = readTriageResultFile(ps.worktree);
17785
17989
  if (result.rebase_requested) {
@@ -17789,12 +17993,44 @@ async function runTriage(repoRoot, config, ps, plan, feedback) {
17789
17993
  const planPath = join9(ps.worktree, ps.planRelpath);
17790
17994
  setStatus(planPath, "blocked");
17791
17995
  ps.lastError = `Rebase conflict: ${rebaseResult.conflictFiles?.join(", ")}`;
17792
- await postPRComment(repoRoot, ps.pr, `⚠️ Rebase conflict detected:
17996
+ await postPRComment(repoRoot, ps.pr, `⚠️ **Rebase conflict detected**
17997
+
17998
+ The following files have conflicts:
17793
17999
  \`\`\`
17794
18000
  ${rebaseResult.conflictFiles?.join(`
17795
18001
  `)}
17796
18002
  \`\`\`
17797
- Please resolve manually.`);
18003
+
18004
+ **To resolve:**
18005
+
18006
+ 1. Navigate to the worktree:
18007
+ \`\`\`
18008
+ cd ${ps.worktree}
18009
+ \`\`\`
18010
+
18011
+ 2. Fetch and rebase manually:
18012
+ \`\`\`
18013
+ git fetch origin ${ps.baseBranch}
18014
+ git rebase origin/${ps.baseBranch}
18015
+ \`\`\`
18016
+
18017
+ 3. Resolve conflicts in your editor, then:
18018
+ \`\`\`
18019
+ git add .
18020
+ git rebase --continue
18021
+ \`\`\`
18022
+
18023
+ 4. Force push the resolved branch:
18024
+ \`\`\`
18025
+ git push --force-with-lease
18026
+ \`\`\`
18027
+
18028
+ 5. Unblock the plan:
18029
+ \`\`\`
18030
+ prloom unpause ${plan.frontmatter.id}
18031
+ \`\`\`
18032
+
18033
+ The plan is now **blocked** until conflicts are resolved.`);
17798
18034
  } else if (rebaseResult.success) {
17799
18035
  await forcePush(ps.worktree, ps.branch);
17800
18036
  console.log(` Rebased and force-pushed`);
@@ -17993,6 +18229,35 @@ var init_open = __esm(() => {
17993
18229
  init_adapters();
17994
18230
  });
17995
18231
 
18232
+ // src/cli/watch.ts
18233
+ var exports_watch = {};
18234
+ __export(exports_watch, {
18235
+ runWatch: () => runWatch
18236
+ });
18237
+ async function runWatch(repoRoot, planId) {
18238
+ const state = loadState(repoRoot);
18239
+ const ps = state.plans[planId];
18240
+ if (!ps) {
18241
+ console.error(`Plan not found in state: ${planId}`);
18242
+ console.error("Make sure the plan has been dispatched at least once.");
18243
+ process.exit(1);
18244
+ }
18245
+ if (!ps.tmuxSession) {
18246
+ console.error(`No active tmux session for ${planId}`);
18247
+ console.error("This plan may not be running, or dispatcher wasn't started with --tmux");
18248
+ process.exit(1);
18249
+ }
18250
+ console.log(`Attaching to ${ps.tmuxSession} (read-only)...`);
18251
+ console.log("Press Ctrl+B D to detach without interrupting the worker.");
18252
+ await execa("tmux", ["attach", "-t", ps.tmuxSession, "-r"], {
18253
+ stdio: "inherit"
18254
+ });
18255
+ }
18256
+ var init_watch = __esm(() => {
18257
+ init_execa();
18258
+ init_state();
18259
+ });
18260
+
17996
18261
  // src/cli/logs.ts
17997
18262
  var exports_logs = {};
17998
18263
  __export(exports_logs, {
@@ -18139,8 +18404,8 @@ async function runClean(repoRoot) {
18139
18404
  console.log("");
18140
18405
  console.log(`Found ${planIds.length} plan(s) in inbox.`);
18141
18406
  console.log("");
18142
- const readline = await import("readline");
18143
- const rl = readline.createInterface({
18407
+ const readline2 = await import("readline");
18408
+ const rl = readline2.createInterface({
18144
18409
  input: process.stdin,
18145
18410
  output: process.stdout
18146
18411
  });
@@ -23508,9 +23773,13 @@ yargs_default(hideBin(process.argv)).scriptName("prloom").usage("$0 <command> [o
23508
23773
  }), async (argv) => {
23509
23774
  const { runEdit: runEdit2 } = await Promise.resolve().then(() => (init_edit(), exports_edit));
23510
23775
  await runEdit2(await getRepoRoot(), argv["plan-id"], argv.agent, argv["no-designer"]);
23511
- }).command("start", "Start the dispatcher", () => {}, async () => {
23776
+ }).command("start", "Start the dispatcher", (yargs) => yargs.option("tmux", {
23777
+ type: "boolean",
23778
+ describe: "Run workers in tmux sessions for observation",
23779
+ default: false
23780
+ }), async (argv) => {
23512
23781
  const { runDispatcher: runDispatcher2 } = await Promise.resolve().then(() => (init_dispatcher(), exports_dispatcher));
23513
- await runDispatcher2(await getRepoRoot());
23782
+ await runDispatcher2(await getRepoRoot(), { tmux: argv.tmux });
23514
23783
  }).command("status", "Show plan states", () => {}, async () => {
23515
23784
  const { runStatus: runStatus2 } = await Promise.resolve().then(() => (init_status(), exports_status));
23516
23785
  await runStatus2(await getRepoRoot());
@@ -23523,6 +23792,9 @@ yargs_default(hideBin(process.argv)).scriptName("prloom").usage("$0 <command> [o
23523
23792
  }).command("open <plan-id>", "Open TUI for manual work (requires paused)", (yargs) => yargs.positional("plan-id", { type: "string", demandOption: true }), async (argv) => {
23524
23793
  const { runOpen: runOpen2 } = await Promise.resolve().then(() => (init_open(), exports_open));
23525
23794
  await runOpen2(await getRepoRoot(), argv["plan-id"]);
23795
+ }).command("watch <plan-id>", "Observe a running worker (requires --tmux mode)", (yargs) => yargs.positional("plan-id", { type: "string", demandOption: true }), async (argv) => {
23796
+ const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_watch(), exports_watch));
23797
+ await runWatch2(await getRepoRoot(), argv["plan-id"]);
23526
23798
  }).command("logs <plan-id>", "Show session ID for a plan", (yargs) => yargs.positional("plan-id", { type: "string", demandOption: true }), async (argv) => {
23527
23799
  const { runLogs: runLogs2 } = await Promise.resolve().then(() => (init_logs(), exports_logs));
23528
23800
  await runLogs2(await getRepoRoot(), argv["plan-id"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prloom",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "prloom": "./dist/cli/index.js"
@@ -2,11 +2,10 @@
2
2
 
3
3
  You are processing PR feedback for an active plan. Your job is to:
4
4
 
5
- 1. Analyze all review feedback (comments, reviews, inline comments)
6
- 2. Create specific, actionable TODO items for the plan
7
- 3. Group related feedback into fewer tasks where appropriate
5
+ 1. **Classify** each feedback item by type
6
+ 2. **Respond** appropriately based on the type
7
+ 3. **Update** the plan if changes are needed
8
8
  4. Detect if a rebase was requested
9
- 5. Compose a reply message for the reviewer(s)
10
9
 
11
10
  ## Feedback to Process
12
11
 
@@ -16,9 +15,36 @@ You are processing PR feedback for an active plan. Your job is to:
16
15
 
17
16
  {{plan}}
18
17
 
19
- ## Instructions
18
+ ---
20
19
 
21
- ### Adding TODOs
20
+ ## Step 1: Classify Each Feedback Item
21
+
22
+ Before acting, identify the type of each feedback item:
23
+
24
+ | Type | Description | Action |
25
+ | ------------------- | --------------------------------------------- | ------------------------ |
26
+ | **Question** | Asking "why", "how", or seeking clarification | Answer in reply, NO TODO |
27
+ | **Change Request** | Asking for code modifications | Create specific TODO |
28
+ | **Approval/Praise** | Positive feedback, LGTM, approval | Acknowledge briefly |
29
+ | **Process Request** | Rebase, update branch, etc. | Set flag, acknowledge |
30
+
31
+ ## Step 2: Handle Each Type
32
+
33
+ ### Questions → Answer Directly
34
+
35
+ For questions, **answer them substantively in your reply**:
36
+
37
+ - Explore the codebase to find the answer if needed
38
+ - Explain the reasoning behind implementation decisions
39
+ - Reference specific code locations or commits if helpful
40
+ - Do NOT create a TODO like "answer the question" - just answer it
41
+
42
+ Example question: "Why did you use a polling approach instead of webhooks?"
43
+ → Reply with the actual reasoning, don't create a TODO.
44
+
45
+ ### Change Requests → Create TODOs
46
+
47
+ For explicit requests to modify code:
22
48
 
23
49
  - Edit the plan file directly to add actionable TODOs
24
50
  - Add them at the end of the ## TODO section
@@ -28,13 +54,23 @@ You are processing PR feedback for an active plan. Your job is to:
28
54
  - "Update function X to handle null input"
29
55
  - "Add test for Y edge case"
30
56
  - "Rename Z for clarity"
31
- - "Remove deprecated code in file A"
32
- - Group related comments into single tasks when logical
57
+ - Group related requests into single tasks when logical
33
58
  - If the plan status is `done` and you add TODOs, change status to `active`
34
59
 
35
- ### Writing the Result File
60
+ ### Approval/Praise Acknowledge
61
+
62
+ Simply thank the reviewer in your reply. No TODO needed.
63
+
64
+ ### Process Requests → Set Flag
65
+
66
+ If any comment mentions rebase, update branch, or similar:
67
+
68
+ - Set `rebase_requested: true` in the result file
69
+ - Acknowledge in your reply
70
+
71
+ ## Step 3: Write the Result File
36
72
 
37
- After editing the plan, you MUST write `.prloom/triage-result.json` with:
73
+ After processing, you MUST write `prloom/.local/triage-result.json`:
38
74
 
39
75
  ```json
40
76
  {
@@ -45,19 +81,20 @@ After editing the plan, you MUST write `.prloom/triage-result.json` with:
45
81
 
46
82
  **Required fields:**
47
83
 
48
- - `reply_markdown`: Your response to post on the PR (ALWAYS required, even for no-ops)
49
- - `rebase_requested`: Set to `true` if any comment mentions rebase, update branch, or similar
84
+ - `reply_markdown`: Your response to post on the PR (ALWAYS required)
85
+ - `rebase_requested`: Set to `true` if rebase was requested
50
86
 
51
87
  ### Reply Guidelines
52
88
 
53
89
  - Be polite and professional
54
- - Acknowledge specific feedback points
55
- - Explain what TODOs were created
56
- - If no changes needed, explain why
57
- - Keep it concise
90
+ - **Answer questions directly** - this is the most important part
91
+ - Explain what TODOs were created (if any)
92
+ - If a request doesn't require changes, explain why
93
+ - Keep it concise but complete
58
94
 
59
95
  ## Critical Rules
60
96
 
61
- 1. You MUST write `.prloom/triage-result.json` even if you add no TODOs
97
+ 1. You MUST write `prloom/.local/triage-result.json` even if you add no TODOs
62
98
  2. The result file must contain valid JSON only, no markdown wrapper
63
99
  3. Failure to write the result file will mark the plan as blocked
100
+ 4. **Questions should be answered, not converted to TODOs**