quadwork 0.1.2 → 1.0.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 (104) hide show
  1. package/README.md +58 -83
  2. package/bin/quadwork.js +512 -97
  3. package/out/404.html +1 -1
  4. package/out/__next.__PAGE__.txt +1 -1
  5. package/out/__next._full.txt +2 -2
  6. package/out/__next._head.txt +1 -1
  7. package/out/__next._index.txt +2 -2
  8. package/out/__next._tree.txt +2 -2
  9. package/out/_next/static/chunks/0ahp74n0wkel0.js +1 -0
  10. package/out/_next/static/chunks/{0jsosmtclw5n5.js → 0dmi9pk2bd712.js} +3 -3
  11. package/out/_next/static/chunks/0ezniz80psxr6.js +1 -0
  12. package/out/_next/static/chunks/0g-nq4.uckan-.js +1 -0
  13. package/out/_next/static/chunks/0io_y3d0p5v~b.js +2 -0
  14. package/out/_next/static/chunks/0jt42fqe6jaw6.js +1 -0
  15. package/out/_next/static/chunks/{03hi.hdp6l230.js → 0q5hwcek8vu2q.js} +12 -12
  16. package/out/_next/static/chunks/0r_tb4lmfa_yb.js +1 -0
  17. package/out/_next/static/chunks/0s8jbc4nxw6y6.css +2 -0
  18. package/out/_next/static/chunks/0z~0.4hivi.f2.js +31 -0
  19. package/out/_next/static/chunks/135rms05ismy4.js +13 -0
  20. package/out/_next/static/chunks/14kr4rvjq-2md.js +1 -0
  21. package/out/_next/static/chunks/turbopack-0sammtvunroor.js +1 -0
  22. package/out/_not-found/__next._full.txt +2 -2
  23. package/out/_not-found/__next._head.txt +1 -1
  24. package/out/_not-found/__next._index.txt +2 -2
  25. package/out/_not-found/__next._not-found.__PAGE__.txt +1 -1
  26. package/out/_not-found/__next._not-found.txt +1 -1
  27. package/out/_not-found/__next._tree.txt +2 -2
  28. package/out/_not-found.html +1 -1
  29. package/out/_not-found.txt +2 -2
  30. package/out/app-shell/__next._full.txt +18 -0
  31. package/out/app-shell/__next._head.txt +6 -0
  32. package/out/app-shell/__next._index.txt +6 -0
  33. package/out/app-shell/__next._tree.txt +3 -0
  34. package/out/app-shell/__next.app-shell.__PAGE__.txt +5 -0
  35. package/out/app-shell/__next.app-shell.txt +5 -0
  36. package/out/app-shell.html +1 -0
  37. package/out/app-shell.txt +18 -0
  38. package/out/index.html +1 -1
  39. package/out/index.txt +2 -2
  40. package/out/project/_/__next._full.txt +3 -4
  41. package/out/project/_/__next._head.txt +1 -1
  42. package/out/project/_/__next._index.txt +2 -2
  43. package/out/project/_/__next._tree.txt +2 -3
  44. package/out/project/_/__next.project.$d$id.__PAGE__.txt +2 -3
  45. package/out/project/_/__next.project.$d$id.txt +1 -1
  46. package/out/project/_/__next.project.txt +1 -1
  47. package/out/project/_/memory/__next._full.txt +3 -3
  48. package/out/project/_/memory/__next._head.txt +1 -1
  49. package/out/project/_/memory/__next._index.txt +2 -2
  50. package/out/project/_/memory/__next._tree.txt +2 -2
  51. package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +2 -2
  52. package/out/project/_/memory/__next.project.$d$id.memory.txt +1 -1
  53. package/out/project/_/memory/__next.project.$d$id.txt +1 -1
  54. package/out/project/_/memory/__next.project.txt +1 -1
  55. package/out/project/_/memory.html +1 -1
  56. package/out/project/_/memory.txt +3 -3
  57. package/out/project/_/queue/__next._full.txt +3 -3
  58. package/out/project/_/queue/__next._head.txt +1 -1
  59. package/out/project/_/queue/__next._index.txt +2 -2
  60. package/out/project/_/queue/__next._tree.txt +2 -2
  61. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +2 -2
  62. package/out/project/_/queue/__next.project.$d$id.queue.txt +1 -1
  63. package/out/project/_/queue/__next.project.$d$id.txt +1 -1
  64. package/out/project/_/queue/__next.project.txt +1 -1
  65. package/out/project/_/queue.html +1 -1
  66. package/out/project/_/queue.txt +3 -3
  67. package/out/project/_.html +1 -1
  68. package/out/project/_.txt +3 -4
  69. package/out/settings/__next._full.txt +3 -3
  70. package/out/settings/__next._head.txt +1 -1
  71. package/out/settings/__next._index.txt +2 -2
  72. package/out/settings/__next._tree.txt +2 -2
  73. package/out/settings/__next.settings.__PAGE__.txt +2 -2
  74. package/out/settings/__next.settings.txt +1 -1
  75. package/out/settings.html +1 -1
  76. package/out/settings.txt +3 -3
  77. package/out/setup/__next._full.txt +3 -3
  78. package/out/setup/__next._head.txt +1 -1
  79. package/out/setup/__next._index.txt +2 -2
  80. package/out/setup/__next._tree.txt +2 -2
  81. package/out/setup/__next.setup.__PAGE__.txt +2 -2
  82. package/out/setup/__next.setup.txt +1 -1
  83. package/out/setup.html +1 -1
  84. package/out/setup.txt +3 -3
  85. package/package.json +1 -1
  86. package/server/config.js +66 -2
  87. package/server/index.js +344 -63
  88. package/server/routes.js +195 -68
  89. package/templates/CLAUDE.md +16 -17
  90. package/templates/config.toml +12 -12
  91. package/templates/seeds/{t3.AGENTS.md → dev.AGENTS.md} +19 -19
  92. package/templates/seeds/{t1.AGENTS.md → head.AGENTS.md} +18 -18
  93. package/templates/seeds/{t2a.AGENTS.md → reviewer1.AGENTS.md} +16 -16
  94. package/templates/seeds/{t2b.AGENTS.md → reviewer2.AGENTS.md} +16 -16
  95. package/out/_next/static/chunks/0.dzh0qf9zq1l.js +0 -2
  96. package/out/_next/static/chunks/03yov._jigv17.js +0 -1
  97. package/out/_next/static/chunks/0iqqouh_3i5y5.js +0 -13
  98. package/out/_next/static/chunks/13uu.sohs74zg.js +0 -31
  99. package/out/_next/static/chunks/15kwal..m9r49.css +0 -2
  100. package/out/_next/static/chunks/17sk4qv6_d0co.js +0 -1
  101. package/out/_next/static/chunks/turbopack-06pqx~0d8czn_.js +0 -1
  102. /package/out/_next/static/{vELqtMegFMn5_6zFOkhtG → 4vrILyy2mh_Ox4JMTaqx8}/_buildManifest.js +0 -0
  103. /package/out/_next/static/{vELqtMegFMn5_6zFOkhtG → 4vrILyy2mh_Ox4JMTaqx8}/_clientMiddlewareManifest.js +0 -0
  104. /package/out/_next/static/{vELqtMegFMn5_6zFOkhtG → 4vrILyy2mh_Ox4JMTaqx8}/_ssgManifest.js +0 -0
package/bin/quadwork.js CHANGED
@@ -11,7 +11,7 @@ const readline = require("readline");
11
11
  const CONFIG_DIR = path.join(os.homedir(), ".quadwork");
12
12
  const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
13
13
  const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
14
- const AGENTS = ["t1", "t2a", "t2b", "t3"];
14
+ const AGENTS = ["head", "reviewer1", "reviewer2", "dev"];
15
15
 
16
16
  // ─── ANSI Helpers ──────────────────────────────────────────────────────────
17
17
 
@@ -123,9 +123,33 @@ function askYN(rl, question, defaultYes = false) {
123
123
  });
124
124
  }
125
125
 
126
+ // Migration: rename old agent keys to new ones
127
+ const AGENT_KEY_MAP = { t1: "head", t2a: "reviewer1", t2b: "reviewer2", t3: "dev" };
128
+
129
+ function migrateAgentKeys(config) {
130
+ let changed = false;
131
+ if (config.projects) {
132
+ for (const project of config.projects) {
133
+ if (!project.agents) continue;
134
+ for (const [oldKey, newKey] of Object.entries(AGENT_KEY_MAP)) {
135
+ if (project.agents[oldKey] && !project.agents[newKey]) {
136
+ project.agents[newKey] = project.agents[oldKey];
137
+ delete project.agents[oldKey];
138
+ changed = true;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ if (changed) {
144
+ try { writeConfig(config); } catch {}
145
+ }
146
+ return config;
147
+ }
148
+
126
149
  function readConfig() {
127
150
  try {
128
- return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
151
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
152
+ return migrateAgentKeys(config);
129
153
  } catch {
130
154
  return { port: 8400, agentchattr_url: "http://127.0.0.1:8300", projects: [] };
131
155
  }
@@ -140,46 +164,295 @@ function writeConfig(config) {
140
164
 
141
165
  let agentChattrFound = false;
142
166
 
143
- function checkPrereqs() {
167
+ function detectPlatform() {
168
+ const p = os.platform();
169
+ if (p === "darwin") return "macos";
170
+ if (p === "linux") {
171
+ // Check for apt vs dnf vs yum
172
+ if (which("apt")) return "linux-apt";
173
+ if (which("dnf")) return "linux-dnf";
174
+ if (which("yum")) return "linux-yum";
175
+ return "linux";
176
+ }
177
+ return "other";
178
+ }
179
+
180
+ async function tryInstall(rl, name, description, commands, { platform } = {}) {
181
+ const cmd = typeof commands === "function" ? commands(platform) : commands;
182
+ if (!cmd) {
183
+ warn(`${name} cannot be auto-installed on your system.`);
184
+ return false;
185
+ }
186
+ console.log("");
187
+ log(`${description}`);
188
+ const doInstall = await askYN(rl, `Install ${name} now?`, true);
189
+ if (!doInstall) {
190
+ log("Skipped.");
191
+ return false;
192
+ }
193
+ const sp = spinner(`Installing ${name}...`);
194
+ const result = run(`${cmd} 2>&1`, { timeout: 120000 });
195
+ if (result !== null) {
196
+ sp.stop(true);
197
+ return true;
198
+ } else {
199
+ sp.stop(false);
200
+ warn(`Auto-install failed. You can install manually and try again.`);
201
+ return false;
202
+ }
203
+ }
204
+
205
+ async function checkPrereqs(rl) {
144
206
  header("Step 1: Prerequisites");
207
+ const platform = detectPlatform();
145
208
  let allOk = true;
209
+ let hasPython = false;
210
+ let hasPipx = false;
146
211
 
147
- // Node.js 20+
212
+ // ── 1. Node.js 20+ (must already exist — user ran npx) ──
148
213
  const nodeVer = run("node --version");
149
214
  if (nodeVer) {
150
215
  const major = parseInt(nodeVer.replace("v", "").split(".")[0], 10);
151
- if (major >= 20) ok(`Node.js ${nodeVer}`);
152
- else { fail(`Node.js ${nodeVer} — need 20+`); allOk = false; }
153
- } else { fail("Node.js not found"); allOk = false; }
216
+ if (major >= 20) {
217
+ ok(`Node.js ${nodeVer}`);
218
+ } else {
219
+ fail(`Node.js ${nodeVer} — version 20 or newer is required`);
220
+ log("Update from: https://nodejs.org");
221
+ allOk = false;
222
+ }
223
+ } else {
224
+ fail("Node.js not found (this shouldn't happen since you ran npx)");
225
+ allOk = false;
226
+ }
154
227
 
155
- // Python 3.10+
228
+ // ── 2. Python 3.10+ (manual install — guide only) ──
156
229
  const pyVer = run("python3 --version");
157
230
  if (pyVer) {
158
231
  const parts = pyVer.replace("Python ", "").split(".");
159
232
  const minor = parseInt(parts[1], 10);
160
- if (parseInt(parts[0], 10) >= 3 && minor >= 10) ok(`${pyVer}`);
161
- else { fail(`${pyVer} — need 3.10+`); allOk = false; }
162
- } else { fail("Python 3 not found"); allOk = false; }
233
+ if (parseInt(parts[0], 10) >= 3 && minor >= 10) {
234
+ ok(`${pyVer}`);
235
+ hasPython = true;
236
+ } else {
237
+ console.log("");
238
+ warn(`${pyVer} found, but version 3.10 or newer is required.`);
239
+ log("Python powers the agent communication layer.");
240
+ log("Download the latest version from:");
241
+ log(` → https://python.org/downloads`);
242
+ log("");
243
+ log("After installing, close and reopen your terminal, then run:");
244
+ log(" → npx quadwork init");
245
+ allOk = false;
246
+ }
247
+ } else {
248
+ console.log("");
249
+ warn("Python 3 is required but not installed on your system.");
250
+ log("");
251
+ log("Python powers the agent communication layer. Install it from:");
252
+ log(" → https://python.org/downloads (download and run the installer)");
253
+ log("");
254
+ log("After installing, close and reopen your terminal, then run:");
255
+ log(" → npx quadwork init");
256
+ allOk = false;
257
+ }
258
+
259
+ if (!hasPython) {
260
+ // Can't continue with pipx/AgentChattr without Python
261
+ console.log("");
262
+ fail("Python is required before we can set up the remaining tools.");
263
+ log("Install Python first, then re-run: npx quadwork init");
264
+ return false;
265
+ }
163
266
 
164
- // AgentChattr
267
+ // ── 3. pipx (needs Python) ──
268
+ if (which("pipx")) {
269
+ ok("pipx");
270
+ hasPipx = true;
271
+ } else {
272
+ console.log("");
273
+ warn("pipx is needed to install AgentChattr safely.");
274
+ log("(pipx keeps Python tools isolated so they don't conflict with your system)");
275
+ const installed = await tryInstall(rl, "pipx", "We can install it automatically.",
276
+ "python3 -m pip install --user pipx && python3 -m pipx ensurepath");
277
+ if (installed && which("pipx")) {
278
+ ok("pipx installed");
279
+ hasPipx = true;
280
+ } else if (installed) {
281
+ // pipx installed but not in PATH yet
282
+ warn("pipx was installed but isn't in your PATH yet.");
283
+ log("Close and reopen your terminal, then run: npx quadwork init");
284
+ return false;
285
+ } else {
286
+ warn("pipx skipped — you can install it later:");
287
+ log(" → python3 -m pip install --user pipx && pipx ensurepath");
288
+ }
289
+ }
290
+
291
+ // ── 4. AgentChattr (needs pipx) ──
165
292
  const acVer = run("agentchattr --version") || run("python3 -m agentchattr --version");
166
- if (acVer) { ok(`AgentChattr ${acVer}`); agentChattrFound = true; }
167
- else { warn("AgentChattr not found — install: pip install agentchattr"); allOk = false; }
293
+ if (acVer) {
294
+ ok(`AgentChattr ${acVer}`);
295
+ agentChattrFound = true;
296
+ } else if (hasPipx) {
297
+ console.log("");
298
+ warn("AgentChattr lets your AI agents communicate with each other.");
299
+ const installed = await tryInstall(rl, "AgentChattr",
300
+ "We can install it now using pipx.", "pipx install agentchattr");
301
+ const acVerAfter = run("agentchattr --version") || run("python3 -m agentchattr --version");
302
+ if (acVerAfter) {
303
+ ok(`AgentChattr ${acVerAfter} installed`);
304
+ agentChattrFound = true;
305
+ } else {
306
+ warn("AgentChattr not available — agents won't be able to chat until it's installed.");
307
+ log(" → Install later: pipx install agentchattr");
308
+ allOk = false;
309
+ }
310
+ } else {
311
+ warn("AgentChattr not found — install pipx first, then: pipx install agentchattr");
312
+ allOk = false;
313
+ }
168
314
 
169
- // gh CLI
170
- if (which("gh")) ok("GitHub CLI (gh)");
171
- else { fail("GitHub CLI not found — install: https://cli.github.com"); allOk = false; }
315
+ // ── 5. GitHub CLI (independent) ──
316
+ if (which("gh")) {
317
+ ok("GitHub CLI (gh)");
318
+ } else {
319
+ console.log("");
320
+ warn("GitHub CLI is required for agents to create branches, PRs, and reviews.");
321
+ const ghCmd = (p) => {
322
+ if (p === "macos") return "brew install gh";
323
+ if (p === "linux-apt") return "sudo apt install gh -y";
324
+ if (p === "linux-dnf") return "sudo dnf install gh -y";
325
+ return null;
326
+ };
327
+ const cmd = ghCmd(platform);
328
+ if (cmd) {
329
+ const installed = await tryInstall(rl, "GitHub CLI",
330
+ "We can install it now.", ghCmd, { platform });
331
+ if (installed && which("gh")) {
332
+ ok("GitHub CLI installed");
333
+ } else {
334
+ fail("GitHub CLI is required. Install from: https://cli.github.com");
335
+ allOk = false;
336
+ }
337
+ } else {
338
+ fail("GitHub CLI is required. Install from: https://cli.github.com");
339
+ allOk = false;
340
+ }
341
+ }
342
+
343
+ // ── 6. AI CLIs — at least one required (independent) ──
344
+ let hasClaude = which("claude");
345
+ let hasCodex = which("codex");
172
346
 
173
- // Claude Code or Codex
174
- const hasClaude = which("claude");
175
- const hasCodex = which("codex");
176
347
  if (hasClaude) ok("Claude Code");
177
348
  if (hasCodex) ok("Codex CLI");
349
+
350
+ if (!hasClaude && !hasCodex) {
351
+ console.log("");
352
+ warn("You need at least one AI CLI to power your agents.");
353
+ log("Choose one (or both) to install:");
354
+ console.log("");
355
+ }
356
+
357
+ // Offer to install Claude Code if missing
358
+ if (!hasClaude) {
359
+ const isRequired = !hasCodex;
360
+ log("Claude Code — Anthropic's AI coding assistant");
361
+ const installClaude = await askYN(rl, "Install Claude Code?", isRequired);
362
+ if (installClaude) {
363
+ const sp = spinner("Installing Claude Code...");
364
+ const result = run("npm install -g @anthropic-ai/claude-code 2>&1", { timeout: 120000 });
365
+ sp.stop(result !== null);
366
+ hasClaude = which("claude");
367
+ if (hasClaude) ok("Claude Code installed");
368
+ else warn("Install failed — try manually: npm install -g @anthropic-ai/claude-code");
369
+ }
370
+ }
371
+
372
+ // Offer to install Codex CLI if missing
373
+ if (!hasCodex) {
374
+ const isRequired = !hasClaude;
375
+ if (hasClaude) {
376
+ console.log("");
377
+ log("Tip: Installing Codex CLI too gives your team different AI perspectives.");
378
+ }
379
+ log("Codex CLI — OpenAI's AI coding assistant");
380
+ const installCodex = await askYN(rl, "Install Codex CLI?", isRequired);
381
+ if (installCodex) {
382
+ const sp = spinner("Installing Codex CLI...");
383
+ const result = run("npm install -g codex 2>&1", { timeout: 120000 });
384
+ sp.stop(result !== null);
385
+ hasCodex = which("codex");
386
+ if (hasCodex) ok("Codex CLI installed");
387
+ else warn("Install failed — try manually: npm install -g codex");
388
+ }
389
+ }
390
+
178
391
  if (!hasClaude && !hasCodex) {
179
- fail("No AI CLI found install Claude Code or Codex CLI");
392
+ fail("At least one AI CLI is required (Claude Code or Codex CLI).");
393
+ log("Install one and re-run: npx quadwork init");
180
394
  allOk = false;
181
395
  }
182
396
 
397
+ // ── CLI Authentication Checks ──
398
+ if (allOk) {
399
+ console.log("");
400
+ log("Checking CLI authentication...");
401
+ console.log("");
402
+
403
+ // GitHub CLI auth
404
+ const ghAuth = run("gh auth status 2>&1");
405
+ if (ghAuth && ghAuth.includes("Logged in")) {
406
+ ok("GitHub CLI — authenticated");
407
+ } else {
408
+ warn("GitHub CLI is installed but not logged in.");
409
+ log(" Run this command to log in:");
410
+ log(" → gh auth login");
411
+ log("");
412
+ const recheck = await askYN(rl, "Done? Press Y to re-check, or N to continue anyway", false);
413
+ if (recheck) {
414
+ const ghAuth2 = run("gh auth status 2>&1");
415
+ if (ghAuth2 && ghAuth2.includes("Logged in")) {
416
+ ok("GitHub CLI — authenticated");
417
+ } else {
418
+ warn("Still not authenticated — you can set this up later.");
419
+ }
420
+ }
421
+ }
422
+
423
+ // Claude Code auth
424
+ if (hasClaude) {
425
+ const claudeAuth = run("claude auth status 2>&1") || run("claude --version 2>&1");
426
+ if (claudeAuth && (claudeAuth.includes("authenticated") || claudeAuth.includes("Logged in") || claudeAuth.includes("@"))) {
427
+ ok("Claude Code — authenticated");
428
+ } else {
429
+ warn("Claude Code may need authentication.");
430
+ log(" If prompted when agents start, run: claude auth login");
431
+ }
432
+ }
433
+
434
+ // Codex CLI auth
435
+ if (hasCodex) {
436
+ const codexAuth = run("codex auth status 2>&1") || run("codex --version 2>&1");
437
+ if (codexAuth && (codexAuth.includes("authenticated") || codexAuth.includes("Logged in") || codexAuth.includes("@"))) {
438
+ ok("Codex CLI — authenticated");
439
+ } else {
440
+ warn("Codex CLI may need authentication.");
441
+ log(" If prompted when agents start, run: codex auth");
442
+ }
443
+ }
444
+ }
445
+
446
+ // ── Summary ──
447
+ console.log("");
448
+ if (allOk) {
449
+ ok("All prerequisites ready!");
450
+ } else {
451
+ console.log("");
452
+ log("Some prerequisites are missing. Fix the issues above and re-run:");
453
+ log(" → npx quadwork init");
454
+ }
455
+
183
456
  return allOk;
184
457
  }
185
458
 
@@ -223,31 +496,55 @@ async function setupGitHub(rl) {
223
496
  async function setupAgents(rl, repo) {
224
497
  header("Step 3: Agent Configuration");
225
498
 
226
- // Prompt for CLI backend
499
+ // Detect available CLIs
227
500
  const hasClaude = which("claude");
228
501
  const hasCodex = which("codex");
502
+ const bothAvailable = hasClaude && hasCodex;
503
+ const onlyOneCli = (hasClaude && !hasCodex) || (!hasClaude && hasCodex);
229
504
  let defaultBackend = hasClaude ? "claude" : "codex";
230
- log("Choose which AI CLI to run in agent terminals. Claude Code (`claude`) or OpenAI Codex (`codex`).");
231
- const backend = await ask(rl, "Default CLI backend (claude/codex)", defaultBackend);
232
- if (backend !== "claude" && backend !== "codex") {
233
- fail("Backend must be 'claude' or 'codex'");
234
- return null;
235
- }
236
505
 
237
- // Per-agent backend selection
238
506
  const backends = {};
239
- const customPerAgent = await askYN(rl, "Use same backend for all agents?", true);
240
- if (customPerAgent) {
241
- for (const agent of AGENTS) backends[agent] = backend;
242
- } else {
243
- for (const agent of AGENTS) {
244
- const agentBackend = await ask(rl, `${agent.toUpperCase()} backend (claude/codex)`, backend);
245
- backends[agent] = (agentBackend === "claude" || agentBackend === "codex") ? agentBackend : backend;
507
+
508
+ if (onlyOneCli) {
509
+ // Single-CLI mode: default all agents, no prompt needed
510
+ const cliName = hasClaude ? "Claude Code" : "Codex CLI";
511
+ const otherName = hasClaude ? "Codex CLI" : "Claude Code";
512
+ const installCmd = hasClaude ? "npm install -g codex" : "npm install -g @anthropic-ai/claude-code";
513
+ ok(`${cliName} detected all 4 agents will use ${cliName}.`);
514
+ console.log("");
515
+ log(`Tip: Installing ${otherName} too gives your team different AI perspectives,`);
516
+ log(`which can improve code review quality. You can add it anytime:`);
517
+ log(` → ${installCmd}`);
518
+ console.log("");
519
+ for (const agent of AGENTS) backends[agent] = defaultBackend;
520
+ } else if (bothAvailable) {
521
+ log("Both Claude Code and Codex CLI are available.");
522
+ log("Choose which AI CLI to run in agent terminals.");
523
+ const backend = await ask(rl, "Default CLI backend (claude/codex)", defaultBackend);
524
+ if (backend !== "claude" && backend !== "codex") {
525
+ fail("Backend must be 'claude' or 'codex'");
526
+ return null;
246
527
  }
528
+ defaultBackend = backend;
529
+
530
+ // Per-agent backend selection
531
+ const customPerAgent = await askYN(rl, "Use same backend for all agents?", true);
532
+ if (customPerAgent) {
533
+ for (const agent of AGENTS) backends[agent] = backend;
534
+ } else {
535
+ for (const agent of AGENTS) {
536
+ const agentBackend = await ask(rl, `${agent.toUpperCase()} backend (claude/codex)`, backend);
537
+ backends[agent] = (agentBackend === "claude" || agentBackend === "codex") ? agentBackend : backend;
538
+ }
539
+ }
540
+ } else {
541
+ fail("No AI CLI found — install Claude Code or Codex CLI first.");
542
+ return null;
247
543
  }
544
+ const backend = defaultBackend;
248
545
 
249
546
  log("Path to your local clone of the repo. Four worktrees will be created next to it");
250
- log("(e.g., project-t1/, project-t2a/, project-t2b/, project-t3/).");
547
+ log("(e.g., project-head/, project-reviewer1/, project-reviewer2/, project-dev/).");
251
548
  const projectDir = await ask(rl, "Project directory", process.cwd());
252
549
  const absDir = path.resolve(projectDir);
253
550
 
@@ -263,12 +560,12 @@ async function setupAgents(rl, repo) {
263
560
  }
264
561
 
265
562
  // Prompt for reviewer credentials (optional)
266
- log("A separate reviewer account lets T2a/T2b approve PRs independently. You can set this up later in Settings.");
267
- const wantReviewer = await askYN(rl, "Use a separate GitHub account for reviewers (T2a/T2b)?", false);
563
+ log("A separate reviewer account lets Reviewer1/Reviewer2 approve PRs independently. You can set this up later in Settings.");
564
+ const wantReviewer = await askYN(rl, "Use a separate GitHub account for reviewers (Reviewer1/Reviewer2)?", false);
268
565
  let reviewerUser = "";
269
566
  let reviewerTokenPath = "";
270
567
  if (wantReviewer) {
271
- log("GitHub username for the reviewer account (used in T2a/T2b seed files for PR reviews).");
568
+ log("GitHub username for the reviewer account (used in Reviewer1/Reviewer2 seed files for PR reviews).");
272
569
  reviewerUser = await ask(rl, "Reviewer GitHub username", "");
273
570
  log("Path to a file containing a GitHub PAT for the reviewer account.");
274
571
  reviewerTokenPath = await ask(rl, "Reviewer token file path", path.join(os.homedir(), ".quadwork", "reviewer-token"));
@@ -342,7 +639,7 @@ function writeAgentChattrConfig(setup, configTomlPath, { skipInstall = false } =
342
639
 
343
640
  let tomlContent = fs.readFileSync(path.join(TEMPLATES_DIR, "config.toml"), "utf-8");
344
641
  for (const agent of AGENTS) {
345
- tomlContent = tomlContent.replace(`{{${agent}_cwd}}`, setup.worktrees[agent]);
642
+ tomlContent = tomlContent.replace(new RegExp(`\\{\\{${agent}_cwd\\}\\}`, "g"), setup.worktrees[agent]);
346
643
  }
347
644
  // Replace placeholders
348
645
  tomlContent = tomlContent.replace(/\{\{project_name\}\}/g, setup.projectName);
@@ -356,6 +653,27 @@ function writeAgentChattrConfig(setup, configTomlPath, { skipInstall = false } =
356
653
  );
357
654
  }
358
655
 
656
+ // Per-project: isolated data dir and port
657
+ const dataDir = path.join(path.dirname(configTomlPath), "data");
658
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
659
+ // Read assigned port from config (set by writeQuadWorkConfig)
660
+ const existingConfig = readConfig();
661
+ const existingProject = existingConfig.projects?.find((p) => p.id === setup.projectName);
662
+ const chattrPort = existingProject?.agentchattr_url
663
+ ? new URL(existingProject.agentchattr_url).port
664
+ : "8300";
665
+ const mcpHttp = existingProject?.mcp_http_port || 8200;
666
+ const mcpSse = existingProject?.mcp_sse_port || 8201;
667
+ tomlContent = tomlContent.replace(/^port = \d+/m, `port = ${chattrPort}`);
668
+ tomlContent = tomlContent.replace(/^data_dir = .+/m, `data_dir = "${dataDir}"`);
669
+ // Add session_token to [server] section if project has one
670
+ const sessionToken = existingProject?.agentchattr_token || "";
671
+ if (sessionToken) {
672
+ tomlContent = tomlContent.replace(/^(data_dir = .+)$/m, `$1\nsession_token = "${sessionToken}"`);
673
+ }
674
+ tomlContent = tomlContent.replace(/^http_port = \d+/m, `http_port = ${mcpHttp}`);
675
+ tomlContent = tomlContent.replace(/^sse_port = \d+/m, `sse_port = ${mcpSse}`);
676
+
359
677
  // Write config.toml
360
678
  const configDir = path.dirname(configTomlPath);
361
679
  if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
@@ -366,14 +684,14 @@ function writeAgentChattrConfig(setup, configTomlPath, { skipInstall = false } =
366
684
  let acAvailable = which("agentchattr");
367
685
  if (!acAvailable && !skipInstall) {
368
686
  const acSpinner = spinner("Installing AgentChattr...");
369
- const installResult = run("pip install agentchattr 2>&1");
687
+ const installResult = run("pipx install agentchattr 2>&1");
370
688
  if (installResult !== null) {
371
689
  acSpinner.stop(true);
372
690
  acAvailable = which("agentchattr");
373
691
  if (!acAvailable) warn("agentchattr binary not found in PATH after install");
374
692
  } else {
375
693
  acSpinner.stop(false);
376
- warn("Install manually: pip install agentchattr");
694
+ warn("Install manually: pipx install agentchattr");
377
695
  }
378
696
  }
379
697
 
@@ -390,8 +708,9 @@ function writeAgentChattrConfig(setup, configTomlPath, { skipInstall = false } =
390
708
  acProc.unref();
391
709
  if (acProc.pid) {
392
710
  ok(`AgentChattr started (PID: ${acProc.pid})`);
393
- const pidFile = path.join(CONFIG_DIR, "agentchattr.pid");
711
+ // Per-project PID file
394
712
  if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
713
+ const pidFile = path.join(CONFIG_DIR, `agentchattr-${setup.projectName}.pid`);
395
714
  fs.writeFileSync(pidFile, String(acProc.pid));
396
715
  } else {
397
716
  warn("Could not start AgentChattr — start manually: agentchattr --config " + configTomlPath);
@@ -464,12 +783,17 @@ async function setupAddons(rl, setup, configTomlPath) {
464
783
  bridge_dir: telegramDir,
465
784
  };
466
785
 
786
+ // Resolve per-project AgentChattr URL
787
+ const projectCfg = readConfig();
788
+ const projectEntry = projectCfg.projects?.find((p) => p.id === setup.projectName);
789
+ const projectChattrUrl = projectEntry?.agentchattr_url || "http://127.0.0.1:8300";
790
+
467
791
  // Append telegram section to config.toml (token read from env at runtime)
468
792
  const telegramSection = `
469
793
  [telegram]
470
794
  bot_token = "env:${envKey}"
471
795
  chat_id = "${chatId}"
472
- agentchattr_url = "http://127.0.0.1:8300"
796
+ agentchattr_url = "${projectChattrUrl}"
473
797
  poll_interval = 2
474
798
  bridge_sender = "telegram-bridge"
475
799
  `;
@@ -481,7 +805,7 @@ bridge_sender = "telegram-bridge"
481
805
  if (fs.existsSync(bridgeScript)) {
482
806
  log("Starting Telegram bridge...");
483
807
  const bridgeToml = path.join(CONFIG_DIR, `telegram-${setup.projectName}.toml`);
484
- const bridgeTomlContent = `[telegram]\nbot_token = "${botToken}"\nchat_id = "${chatId}"\n\n[agentchattr]\nurl = "http://127.0.0.1:8300"\n`;
808
+ const bridgeTomlContent = `[telegram]\nbot_token = "${botToken}"\nchat_id = "${chatId}"\n\n[agentchattr]\nurl = "${projectChattrUrl}"\n`;
485
809
  fs.writeFileSync(bridgeToml, bridgeTomlContent, { mode: 0o600 });
486
810
  fs.chmodSync(bridgeToml, 0o600);
487
811
  const bridgeProc = spawn("python3", [bridgeScript, "--config", bridgeToml], {
@@ -586,9 +910,25 @@ function writeQuadWorkConfig(setup) {
586
910
  };
587
911
  }
588
912
 
913
+ // Auto-assign per-project AgentChattr and MCP ports (scan existing to avoid collisions)
914
+ const existingIdx = config.projects.findIndex((p) => p.id === setup.projectName);
915
+ const usedChattrPorts = new Set(config.projects.map((p) => {
916
+ try { return parseInt(new URL(p.agentchattr_url).port, 10); } catch { return 0; }
917
+ }).filter(Boolean));
918
+ const usedMcpPorts = new Set(config.projects.flatMap((p) => [p.mcp_http_port, p.mcp_sse_port]).filter(Boolean));
919
+ let chattrPort = 8300;
920
+ while (usedChattrPorts.has(chattrPort)) chattrPort++;
921
+ let mcp_http = 8200;
922
+ while (usedMcpPorts.has(mcp_http)) mcp_http++;
923
+ let mcp_sse = mcp_http + 1;
924
+ while (usedMcpPorts.has(mcp_sse)) mcp_sse++;
925
+ project.agentchattr_url = `http://127.0.0.1:${chattrPort}`;
926
+ project.agentchattr_token = require("crypto").randomBytes(16).toString("hex");
927
+ project.mcp_http_port = mcp_http;
928
+ project.mcp_sse_port = mcp_sse;
929
+
589
930
  // Upsert project
590
- const idx = config.projects.findIndex((p) => p.id === setup.projectName);
591
- if (idx >= 0) config.projects[idx] = project;
931
+ if (existingIdx >= 0) config.projects[existingIdx] = project;
592
932
  else config.projects.push(project);
593
933
 
594
934
  writeConfig(config);
@@ -601,53 +941,102 @@ async function cmdInit() {
601
941
  console.log("");
602
942
  console.log(` ${c.cyan}${c.bold}╔══════════════════════════════════════════╗${c.reset}`);
603
943
  console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.white}${c.bold}QuadWork Init${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
604
- console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.dim}4-agent coding team setup${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
944
+ console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.dim}Global setup projects via web UI${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
605
945
  console.log(` ${c.cyan}${c.bold}╚══════════════════════════════════════════╝${c.reset}`);
606
- console.log(`\n ${c.dim}Tip: Press Enter to accept defaults shown in [brackets].${c.reset}\n`);
946
+ console.log(`\n ${c.dim}Press Enter to accept defaults. Takes under 30 seconds.${c.reset}\n`);
607
947
 
608
948
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
609
949
 
610
950
  try {
611
- // Step 1: Prerequisites
612
- const prereqsOk = checkPrereqs();
951
+ // Step 1: Prerequisites (header printed by checkPrereqs)
952
+ const prereqsOk = await checkPrereqs(rl);
613
953
  if (!prereqsOk) {
614
954
  const proceed = await askYN(rl, "Some prerequisites missing. Continue anyway?", false);
615
955
  if (!proceed) { rl.close(); process.exit(1); }
616
956
  }
617
957
 
618
- // Step 2: GitHub
619
- const repo = await setupGitHub(rl);
620
- if (!repo) { rl.close(); process.exit(1); }
621
-
622
- // Step 3: Agents
623
- const setup = await setupAgents(rl, repo);
624
- if (!setup) { rl.close(); process.exit(1); }
625
-
626
- // Step 4: AgentChattr config (skip install if prereqs already flagged it missing)
627
- const configTomlPath = path.join(setup.absDir, "config.toml");
628
- writeAgentChattrConfig(setup, configTomlPath, { skipInstall: !agentChattrFound });
629
-
630
- // Step 5: Optional add-ons
631
- await setupAddons(rl, setup, configTomlPath);
632
-
633
- // Write QuadWork config
634
- writeQuadWorkConfig(setup);
958
+ // Step 2: Dashboard port
959
+ header("Step 2: Dashboard Port");
960
+ const port = await ask(rl, "Port for the QuadWork dashboard (Enter for default)", "8400");
961
+
962
+ // Write global config
963
+ const config = readConfig();
964
+ config.port = parseInt(port, 10) || 8400;
965
+ writeConfig(config);
966
+ ok(`Wrote ${CONFIG_PATH}`);
967
+
968
+ // Step 3: Start server
969
+ header("Step 3: Starting Dashboard");
970
+ const quadworkDir = path.join(__dirname, "..");
971
+ const serverDir = path.join(quadworkDir, "server");
972
+ let serverPid = null;
973
+ if (fs.existsSync(path.join(serverDir, "index.js"))) {
974
+ const server = spawn("node", [serverDir], {
975
+ stdio: "ignore",
976
+ detached: true,
977
+ env: { ...process.env },
978
+ });
979
+ server.unref();
980
+ if (server.pid) {
981
+ serverPid = server.pid;
982
+ ok(`Server started (PID: ${serverPid})`);
983
+ const pidFile = path.join(CONFIG_DIR, "server.pid");
984
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
985
+ fs.writeFileSync(pidFile, String(serverPid));
986
+ }
987
+ } else {
988
+ warn("Server not found — run from the quadwork directory");
989
+ }
635
990
 
636
- // Done
637
- header("Setup Complete");
638
- log(`Project: ${setup.projectName}`);
639
- log(`Repo: ${setup.repo}`);
640
- log(`Worktrees: ${AGENTS.map((a) => `${setup.projectName}-${a}/`).join(", ")}`);
641
- log(`Backends: ${AGENTS.map((a) => `${a.toUpperCase()}=${(setup.backends && setup.backends[a]) || setup.backend}`).join(", ")}`);
642
- log(`Config: ${CONFIG_PATH}`);
643
- log(`AgentChattr: ${configTomlPath}`);
644
- if (setup.telegram) log(`Telegram: configured`);
645
- if (setup.memoryDir) log(`Shared Memory: ${setup.memoryDir}`);
646
- log("");
647
- log("Next steps:");
648
- log(" npx quadwork start launch dashboard + agents");
649
- log(" npx quadwork stop stop all processes");
650
- log("");
991
+ // Done — celebratory welcome
992
+ const dashPort = parseInt(port, 10) || 8400;
993
+ const dashboardUrl = `http://127.0.0.1:${dashPort}`;
994
+
995
+ console.log("");
996
+ console.log(` ${c.cyan}${c.bold}╔══════════════════════════════════════════════════════════╗${c.reset}`);
997
+ console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
998
+ console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.green}${c.bold}Welcome to QuadWork!${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
999
+ console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
1000
+ console.log(` ${c.cyan}${c.bold}║${c.reset} Your AI-powered dev team is ready to ship. ${c.cyan}${c.bold}║${c.reset}`);
1001
+ console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
1002
+ console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.green}*${c.reset} ${c.bold}Head${c.reset} ${c.dim}— coordinates & merges${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
1003
+ console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.green}*${c.reset} ${c.bold}Dev${c.reset} ${c.dim}writes all the code${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
1004
+ console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.green}*${c.reset} ${c.bold}Reviewer1${c.reset} ${c.dim}independent code review${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
1005
+ console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.green}*${c.reset} ${c.bold}Reviewer2${c.reset} ${c.dim}— independent code review${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
1006
+ console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
1007
+ console.log(` ${c.cyan}${c.bold}║${c.reset} 4 agents. Full GitHub workflow. Runs while you sleep. ${c.cyan}${c.bold}║${c.reset}`);
1008
+ console.log(` ${c.cyan}${c.bold}║${c.reset} ${c.cyan}${c.bold}║${c.reset}`);
1009
+ console.log(` ${c.cyan}${c.bold}╚══════════════════════════════════════════════════════════╝${c.reset}`);
1010
+ console.log("");
1011
+ if (serverPid) {
1012
+ console.log(` ${c.green}*${c.reset} Server running at ${c.cyan}${dashboardUrl}${c.reset} ${c.dim}(PID: ${serverPid})${c.reset}`);
1013
+ } else {
1014
+ console.log(` ${c.yellow}*${c.reset} Server not started — run ${c.dim}npx quadwork start${c.reset} to launch`);
1015
+ }
1016
+ console.log(` ${c.green}*${c.reset} Config saved to ${c.dim}${CONFIG_PATH}${c.reset}`);
1017
+ console.log("");
1018
+ console.log(` ${c.cyan}${c.bold}--- Create Your First Project ---${c.reset}`);
1019
+ console.log("");
1020
+ console.log(` Your browser is opening now. If not, visit:`);
1021
+ console.log("");
1022
+ console.log(` ${c.cyan}${c.bold}${dashboardUrl}/setup${c.reset}`);
1023
+ console.log("");
1024
+ console.log(` ${c.dim}1.${c.reset} Connect a GitHub repo`);
1025
+ console.log(` ${c.dim}2.${c.reset} Pick models for each agent`);
1026
+ console.log(` ${c.dim}3.${c.reset} Hit Start — your team takes it from there`);
1027
+ console.log("");
1028
+ console.log(` ${c.dim}Commands:${c.reset}`);
1029
+ console.log(` ${c.dim}npx quadwork start${c.reset} — restart everything`);
1030
+ console.log(` ${c.dim}npx quadwork stop${c.reset} — shut it all down`);
1031
+ console.log("");
1032
+ console.log(` ${c.green}${c.bold}Happy shipping!${c.reset}`);
1033
+ console.log("");
1034
+
1035
+ // Open browser
1036
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1037
+ setTimeout(() => {
1038
+ try { execSync(`${openCmd} ${dashboardUrl}/setup`, { stdio: "ignore" }); } catch {}
1039
+ }, 1500);
651
1040
 
652
1041
  rl.close();
653
1042
  } catch (err) {
@@ -698,11 +1087,12 @@ function cmdStart() {
698
1087
  const pidFile = path.join(CONFIG_DIR, "server.pid");
699
1088
  fs.writeFileSync(pidFile, String(server.pid));
700
1089
 
701
- // Start AgentChattr if installed and config.toml exists for first project
702
- const firstProject = config.projects[0];
703
- if (firstProject && which("agentchattr")) {
704
- const configToml = path.join(firstProject.working_dir, "config.toml");
705
- if (fs.existsSync(configToml)) {
1090
+ // Start AgentChattr for each project that has a config.toml
1091
+ if (which("agentchattr")) {
1092
+ for (const project of config.projects) {
1093
+ if (!project.working_dir) continue;
1094
+ const configToml = path.join(project.working_dir, "agentchattr", "config.toml");
1095
+ if (!fs.existsSync(configToml)) continue;
706
1096
  const acProc = spawn("agentchattr", ["--config", configToml], {
707
1097
  stdio: "ignore",
708
1098
  detached: true,
@@ -710,8 +1100,8 @@ function cmdStart() {
710
1100
  acProc.on("error", () => {});
711
1101
  acProc.unref();
712
1102
  if (acProc.pid) {
713
- ok(`AgentChattr started (PID: ${acProc.pid})`);
714
- fs.writeFileSync(path.join(CONFIG_DIR, "agentchattr.pid"), String(acProc.pid));
1103
+ ok(`AgentChattr started for ${project.id} (PID: ${acProc.pid})`);
1104
+ fs.writeFileSync(path.join(CONFIG_DIR, `agentchattr-${project.id}.pid`), String(acProc.pid));
715
1105
  }
716
1106
  }
717
1107
  }
@@ -750,9 +1140,30 @@ function cmdStop() {
750
1140
 
751
1141
  let stopped = 0;
752
1142
  if (stopPid("Telegram bridge", "telegram-bridge.pid")) stopped++;
1143
+
1144
+ // Stop per-project AgentChattr instances
1145
+ const config = readConfig();
1146
+ for (const project of (config.projects || [])) {
1147
+ if (stopPid(`AgentChattr (${project.id})`, `agentchattr-${project.id}.pid`)) stopped++;
1148
+ }
1149
+ // Also stop legacy single-instance PID if present
753
1150
  if (stopPid("AgentChattr", "agentchattr.pid")) stopped++;
1151
+
754
1152
  if (stopPid("Server", "server.pid")) stopped++;
755
1153
 
1154
+ // Stop caffeinate via the running server's API (targets only QuadWork's instance)
1155
+ if (process.platform === "darwin") {
1156
+ const cfg = readConfig();
1157
+ const qwPort = cfg.port || 8400;
1158
+ try {
1159
+ const result = run(`curl -s -X POST http://127.0.0.1:${qwPort}/api/caffeinate/stop 2>/dev/null`);
1160
+ if (result && result.includes('"ok":true')) {
1161
+ ok("Stopped caffeinate (sleep prevention)");
1162
+ stopped++;
1163
+ }
1164
+ } catch {}
1165
+ }
1166
+
756
1167
  if (stopped === 0) warn("No running processes found");
757
1168
  else ok(`Stopped ${stopped} process(es)`);
758
1169
  log("");
@@ -772,11 +1183,11 @@ async function cmdAddProject() {
772
1183
  const setup = await setupAgents(rl, repo);
773
1184
  if (!setup) { rl.close(); process.exit(1); }
774
1185
 
775
- const configTomlPath = path.join(setup.absDir, "config.toml");
776
- writeAgentChattrConfig(setup, configTomlPath);
777
-
778
1186
  writeQuadWorkConfig(setup);
779
1187
 
1188
+ const configTomlPath = path.join(setup.absDir, "agentchattr", "config.toml");
1189
+ writeAgentChattrConfig(setup, configTomlPath);
1190
+
780
1191
  header("Project Added");
781
1192
  log(`Project: ${setup.projectName}`);
782
1193
  log(`Repo: ${setup.repo}`);
@@ -813,16 +1224,20 @@ switch (command) {
813
1224
  Usage: quadwork <command>
814
1225
 
815
1226
  Commands:
816
- init Set up a new QuadWork 4-agent environment
1227
+ init Global setup (prereqs, port, backend) then open web UI
817
1228
  start Start the QuadWork dashboard and backend
818
1229
  stop Stop all QuadWork processes
819
- add-project Add a project to an existing QuadWork setup
1230
+ add-project Add a project via CLI (alternative to web UI /setup)
1231
+
1232
+ Workflow:
1233
+ 1. npx quadwork init — one-time global setup, opens dashboard
1234
+ 2. Open /setup in browser — create projects with guided web UI
1235
+ 3. npx quadwork stop — stop everything when done
820
1236
 
821
1237
  Examples:
822
1238
  npx quadwork init
823
1239
  npx quadwork start
824
1240
  npx quadwork stop
825
- npx quadwork add-project
826
1241
  `);
827
1242
  if (command) process.exit(1);
828
1243
  }