panopticon-cli 0.1.3 → 0.1.6
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 +408 -9
- package/dist/chunk-RM3WGTFO.js +1058 -0
- package/dist/chunk-RM3WGTFO.js.map +1 -0
- package/dist/cli/index.js +1725 -717
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +368 -5
- package/dist/index.js +43 -1
- package/package.json +2 -1
- package/templates/traefik/README.md +106 -0
- package/templates/traefik/docker-compose.yml +40 -0
- package/templates/traefik/dynamic/panopticon.yml +51 -0
- package/templates/traefik/dynamic/workspace.yml.template +34 -0
- package/templates/traefik/traefik.yml +45 -0
- package/dist/chunk-FR2P66GU.js +0 -352
- package/dist/chunk-FR2P66GU.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
AGENTS_DIR,
|
|
4
|
+
CERTS_DIR,
|
|
4
5
|
CLAUDE_DIR,
|
|
5
6
|
CLAUDE_MD_TEMPLATES,
|
|
6
7
|
COMMANDS_DIR,
|
|
@@ -8,10 +9,16 @@ import {
|
|
|
8
9
|
INIT_DIRS,
|
|
9
10
|
PANOPTICON_HOME,
|
|
10
11
|
SKILLS_DIR,
|
|
12
|
+
SOURCE_TRAEFIK_TEMPLATES,
|
|
11
13
|
SYNC_TARGETS,
|
|
14
|
+
TRAEFIK_CERTS_DIR,
|
|
15
|
+
TRAEFIK_DIR,
|
|
16
|
+
__commonJS,
|
|
12
17
|
__require,
|
|
13
18
|
addAlias,
|
|
19
|
+
cleanOldBackups,
|
|
14
20
|
createBackup,
|
|
21
|
+
createTracker,
|
|
15
22
|
detectShell,
|
|
16
23
|
executeSync,
|
|
17
24
|
getAliasInstructions,
|
|
@@ -22,22 +29,122 @@ import {
|
|
|
22
29
|
planSync,
|
|
23
30
|
restoreBackup,
|
|
24
31
|
saveConfig
|
|
25
|
-
} from "../chunk-
|
|
32
|
+
} from "../chunk-RM3WGTFO.js";
|
|
33
|
+
|
|
34
|
+
// package.json
|
|
35
|
+
var require_package = __commonJS({
|
|
36
|
+
"package.json"(exports, module) {
|
|
37
|
+
module.exports = {
|
|
38
|
+
name: "panopticon-cli",
|
|
39
|
+
version: "0.1.6",
|
|
40
|
+
description: "Multi-agent orchestration for AI coding assistants (Claude Code, Codex, Cursor, Gemini CLI)",
|
|
41
|
+
keywords: [
|
|
42
|
+
"ai-agents",
|
|
43
|
+
"orchestration",
|
|
44
|
+
"claude-code",
|
|
45
|
+
"codex",
|
|
46
|
+
"cursor",
|
|
47
|
+
"gemini",
|
|
48
|
+
"multi-agent",
|
|
49
|
+
"devtools",
|
|
50
|
+
"linear"
|
|
51
|
+
],
|
|
52
|
+
author: "Edward Becker <edward.becker@mindyournow.com>",
|
|
53
|
+
license: "MIT",
|
|
54
|
+
repository: {
|
|
55
|
+
type: "git",
|
|
56
|
+
url: "https://github.com/eltmon/panopticon-cli.git"
|
|
57
|
+
},
|
|
58
|
+
homepage: "https://github.com/eltmon/panopticon-cli#readme",
|
|
59
|
+
bugs: {
|
|
60
|
+
url: "https://github.com/eltmon/panopticon-cli/issues"
|
|
61
|
+
},
|
|
62
|
+
type: "module",
|
|
63
|
+
bin: {
|
|
64
|
+
pan: "./dist/cli/index.js",
|
|
65
|
+
panopticon: "./dist/cli/index.js"
|
|
66
|
+
},
|
|
67
|
+
main: "./dist/index.js",
|
|
68
|
+
types: "./dist/index.d.ts",
|
|
69
|
+
files: [
|
|
70
|
+
"dist",
|
|
71
|
+
"templates",
|
|
72
|
+
"README.md",
|
|
73
|
+
"LICENSE"
|
|
74
|
+
],
|
|
75
|
+
engines: {
|
|
76
|
+
node: ">=18.0.0"
|
|
77
|
+
},
|
|
78
|
+
scripts: {
|
|
79
|
+
dev: "tsx watch src/cli/index.ts",
|
|
80
|
+
build: "tsup",
|
|
81
|
+
typecheck: "tsc --noEmit",
|
|
82
|
+
lint: "eslint src/",
|
|
83
|
+
test: "vitest",
|
|
84
|
+
prepublishOnly: "npm run build"
|
|
85
|
+
},
|
|
86
|
+
dependencies: {
|
|
87
|
+
"@iarna/toml": "^2.2.5",
|
|
88
|
+
"@linear/sdk": "^70.0.0",
|
|
89
|
+
"@octokit/rest": "^22.0.1",
|
|
90
|
+
chalk: "^5.6.2",
|
|
91
|
+
commander: "^12.1.0",
|
|
92
|
+
conf: "^12.0.0",
|
|
93
|
+
execa: "^8.0.1",
|
|
94
|
+
inquirer: "^9.3.8",
|
|
95
|
+
ora: "^8.2.0"
|
|
96
|
+
},
|
|
97
|
+
devDependencies: {
|
|
98
|
+
"@types/inquirer": "^9.0.9",
|
|
99
|
+
"@types/node": "^20.10.0",
|
|
100
|
+
eslint: "^8.55.0",
|
|
101
|
+
tsup: "^8.0.1",
|
|
102
|
+
tsx: "^4.6.2",
|
|
103
|
+
typescript: "^5.3.2",
|
|
104
|
+
vitest: "^1.0.4"
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
});
|
|
26
109
|
|
|
27
110
|
// src/cli/index.ts
|
|
28
111
|
import { Command } from "commander";
|
|
29
|
-
import
|
|
112
|
+
import chalk25 from "chalk";
|
|
30
113
|
|
|
31
114
|
// src/cli/commands/init.ts
|
|
32
|
-
import { existsSync, mkdirSync } from "fs";
|
|
115
|
+
import { existsSync, mkdirSync, readdirSync, cpSync } from "fs";
|
|
116
|
+
import { join, dirname } from "path";
|
|
117
|
+
import { fileURLToPath } from "url";
|
|
33
118
|
import chalk from "chalk";
|
|
34
119
|
import ora from "ora";
|
|
120
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
121
|
+
var __dirname = dirname(__filename);
|
|
122
|
+
var PACKAGE_ROOT = join(__dirname, "..", "..");
|
|
123
|
+
var BUNDLED_SKILLS_DIR = join(PACKAGE_ROOT, "skills");
|
|
124
|
+
function copyBundledSkills() {
|
|
125
|
+
if (!existsSync(BUNDLED_SKILLS_DIR)) {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
if (!existsSync(SKILLS_DIR)) {
|
|
129
|
+
mkdirSync(SKILLS_DIR, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
const skills = readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
132
|
+
let copied = 0;
|
|
133
|
+
for (const skill of skills) {
|
|
134
|
+
const sourcePath = join(BUNDLED_SKILLS_DIR, skill.name);
|
|
135
|
+
const targetPath = join(SKILLS_DIR, skill.name);
|
|
136
|
+
cpSync(sourcePath, targetPath, { recursive: true });
|
|
137
|
+
copied++;
|
|
138
|
+
}
|
|
139
|
+
return copied;
|
|
140
|
+
}
|
|
35
141
|
async function initCommand() {
|
|
36
142
|
const spinner = ora("Initializing Panopticon...").start();
|
|
37
143
|
if (existsSync(CONFIG_FILE)) {
|
|
38
144
|
spinner.info("Panopticon already initialized");
|
|
39
145
|
console.log(chalk.dim(` Config: ${CONFIG_FILE}`));
|
|
40
146
|
console.log(chalk.dim(` Home: ${PANOPTICON_HOME}`));
|
|
147
|
+
console.log(chalk.dim(" Run `pan sync` to update skills"));
|
|
41
148
|
return;
|
|
42
149
|
}
|
|
43
150
|
try {
|
|
@@ -50,6 +157,8 @@ async function initCommand() {
|
|
|
50
157
|
const config = getDefaultConfig();
|
|
51
158
|
saveConfig(config);
|
|
52
159
|
spinner.text = "Created config...";
|
|
160
|
+
spinner.text = "Installing bundled skills...";
|
|
161
|
+
const skillsCopied = copyBundledSkills();
|
|
53
162
|
const shell = detectShell();
|
|
54
163
|
const rcFile = getShellRcFile(shell);
|
|
55
164
|
if (rcFile && existsSync(rcFile)) {
|
|
@@ -58,19 +167,25 @@ async function initCommand() {
|
|
|
58
167
|
console.log("");
|
|
59
168
|
console.log(chalk.green("\u2713") + " Created " + chalk.cyan(PANOPTICON_HOME));
|
|
60
169
|
console.log(chalk.green("\u2713") + " Created " + chalk.cyan(CONFIG_FILE));
|
|
170
|
+
if (skillsCopied > 0) {
|
|
171
|
+
console.log(chalk.green("\u2713") + ` Installed ${skillsCopied} bundled skills`);
|
|
172
|
+
}
|
|
61
173
|
console.log(chalk.green("\u2713") + " " + getAliasInstructions(shell));
|
|
62
174
|
} else {
|
|
63
175
|
spinner.succeed("Panopticon initialized!");
|
|
64
176
|
console.log("");
|
|
65
177
|
console.log(chalk.green("\u2713") + " Created " + chalk.cyan(PANOPTICON_HOME));
|
|
66
178
|
console.log(chalk.green("\u2713") + " Created " + chalk.cyan(CONFIG_FILE));
|
|
179
|
+
if (skillsCopied > 0) {
|
|
180
|
+
console.log(chalk.green("\u2713") + ` Installed ${skillsCopied} bundled skills`);
|
|
181
|
+
}
|
|
67
182
|
console.log(chalk.yellow("!") + " Could not detect shell. Add alias manually:");
|
|
68
183
|
console.log(chalk.dim(' alias pan="panopticon"'));
|
|
69
184
|
}
|
|
70
185
|
console.log("");
|
|
71
186
|
console.log("Next steps:");
|
|
72
|
-
console.log(chalk.dim(" 1.
|
|
73
|
-
console.log(chalk.dim(" 2.
|
|
187
|
+
console.log(chalk.dim(" 1. Run: pan sync"));
|
|
188
|
+
console.log(chalk.dim(" 2. Start dashboard: pan up"));
|
|
74
189
|
} catch (error) {
|
|
75
190
|
spinner.fail("Failed to initialize");
|
|
76
191
|
console.error(chalk.red(error.message));
|
|
@@ -119,9 +234,9 @@ async function syncCommand(options) {
|
|
|
119
234
|
SYNC_TARGETS[r].skills,
|
|
120
235
|
SYNC_TARGETS[r].commands
|
|
121
236
|
]);
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
124
|
-
spinner2.succeed(`Backup created: ${
|
|
237
|
+
const backup2 = createBackup(backupDirs);
|
|
238
|
+
if (backup2.targets.length > 0) {
|
|
239
|
+
spinner2.succeed(`Backup created: ${backup2.timestamp}`);
|
|
125
240
|
} else {
|
|
126
241
|
spinner2.info("No existing content to backup");
|
|
127
242
|
}
|
|
@@ -166,8 +281,8 @@ async function restoreCommand(timestamp) {
|
|
|
166
281
|
}
|
|
167
282
|
if (!timestamp) {
|
|
168
283
|
console.log(chalk3.bold("Available backups:\n"));
|
|
169
|
-
for (const
|
|
170
|
-
console.log(` ${chalk3.cyan(
|
|
284
|
+
for (const backup2 of backups.slice(0, 10)) {
|
|
285
|
+
console.log(` ${chalk3.cyan(backup2.timestamp)} - ${backup2.targets.join(", ")}`);
|
|
171
286
|
}
|
|
172
287
|
if (backups.length > 10) {
|
|
173
288
|
console.log(chalk3.dim(` ... and ${backups.length - 10} more`));
|
|
@@ -218,10 +333,42 @@ async function restoreCommand(timestamp) {
|
|
|
218
333
|
}
|
|
219
334
|
}
|
|
220
335
|
|
|
221
|
-
// src/cli/commands/
|
|
222
|
-
import { readdirSync, readFileSync, existsSync as existsSync2 } from "fs";
|
|
223
|
-
import { join } from "path";
|
|
336
|
+
// src/cli/commands/backup.ts
|
|
224
337
|
import chalk4 from "chalk";
|
|
338
|
+
async function backupListCommand(options) {
|
|
339
|
+
const backups = listBackups();
|
|
340
|
+
if (options.json) {
|
|
341
|
+
console.log(JSON.stringify(backups, null, 2));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (backups.length === 0) {
|
|
345
|
+
console.log(chalk4.dim("No backups found."));
|
|
346
|
+
console.log(chalk4.dim("Backups are created automatically during sync."));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
console.log(chalk4.bold("Backups:\n"));
|
|
350
|
+
for (const backup2 of backups) {
|
|
351
|
+
console.log(` ${chalk4.cyan(backup2.timestamp)}`);
|
|
352
|
+
console.log(` ${chalk4.dim("Contains:")} ${backup2.targets.join(", ")}`);
|
|
353
|
+
}
|
|
354
|
+
console.log();
|
|
355
|
+
console.log(chalk4.dim(`Total: ${backups.length} backups`));
|
|
356
|
+
console.log(chalk4.dim("Use `pan restore <timestamp>` to restore a backup."));
|
|
357
|
+
}
|
|
358
|
+
async function backupCleanCommand(options) {
|
|
359
|
+
const keepCount = parseInt(options.keep || "10", 10);
|
|
360
|
+
const removed = cleanOldBackups(keepCount);
|
|
361
|
+
if (removed === 0) {
|
|
362
|
+
console.log(chalk4.dim(`No backups removed (keeping ${keepCount}).`));
|
|
363
|
+
} else {
|
|
364
|
+
console.log(chalk4.green(`Removed ${removed} old backup(s), keeping ${keepCount}.`));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/cli/commands/skills.ts
|
|
369
|
+
import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2 } from "fs";
|
|
370
|
+
import { join as join2 } from "path";
|
|
371
|
+
import chalk5 from "chalk";
|
|
225
372
|
function parseSkillFrontmatter(content) {
|
|
226
373
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
227
374
|
if (!match) return {};
|
|
@@ -233,9 +380,9 @@ function parseSkillFrontmatter(content) {
|
|
|
233
380
|
function listSkills() {
|
|
234
381
|
if (!existsSync2(SKILLS_DIR)) return [];
|
|
235
382
|
const skills = [];
|
|
236
|
-
const dirs =
|
|
383
|
+
const dirs = readdirSync2(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
237
384
|
for (const dir of dirs) {
|
|
238
|
-
const skillFile =
|
|
385
|
+
const skillFile = join2(SKILLS_DIR, dir.name, "SKILL.md");
|
|
239
386
|
if (!existsSync2(skillFile)) continue;
|
|
240
387
|
const content = readFileSync(skillFile, "utf8");
|
|
241
388
|
const { name, description } = parseSkillFrontmatter(content);
|
|
@@ -253,29 +400,32 @@ async function skillsCommand(options) {
|
|
|
253
400
|
console.log(JSON.stringify(skills, null, 2));
|
|
254
401
|
return;
|
|
255
402
|
}
|
|
256
|
-
console.log(
|
|
403
|
+
console.log(chalk5.bold(`
|
|
257
404
|
Panopticon Skills (${skills.length})
|
|
258
405
|
`));
|
|
259
406
|
if (skills.length === 0) {
|
|
260
|
-
console.log(
|
|
261
|
-
console.log(
|
|
407
|
+
console.log(chalk5.yellow("No skills found."));
|
|
408
|
+
console.log(chalk5.dim("Skills should be in ~/.panopticon/skills/<name>/SKILL.md"));
|
|
262
409
|
return;
|
|
263
410
|
}
|
|
264
411
|
for (const skill of skills) {
|
|
265
|
-
console.log(
|
|
266
|
-
console.log(
|
|
412
|
+
console.log(chalk5.cyan(skill.name));
|
|
413
|
+
console.log(chalk5.dim(` ${skill.description}`));
|
|
267
414
|
}
|
|
268
415
|
console.log(`
|
|
269
|
-
${
|
|
416
|
+
${chalk5.dim('Run "pan sync" to sync skills to Claude Code')}`);
|
|
270
417
|
}
|
|
271
418
|
|
|
272
419
|
// src/cli/commands/work/issue.ts
|
|
273
|
-
import
|
|
420
|
+
import chalk6 from "chalk";
|
|
274
421
|
import ora4 from "ora";
|
|
422
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
423
|
+
import { join as join6, dirname as dirname2 } from "path";
|
|
275
424
|
|
|
276
425
|
// src/lib/agents.ts
|
|
277
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, readFileSync as readFileSync4, readdirSync as
|
|
278
|
-
import { join as
|
|
426
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, readFileSync as readFileSync4, readdirSync as readdirSync5 } from "fs";
|
|
427
|
+
import { join as join5 } from "path";
|
|
428
|
+
import { execSync as execSync2 } from "child_process";
|
|
279
429
|
|
|
280
430
|
// src/lib/tmux.ts
|
|
281
431
|
import { execSync } from "child_process";
|
|
@@ -307,8 +457,20 @@ function sessionExists(name) {
|
|
|
307
457
|
}
|
|
308
458
|
function createSession(name, cwd, initialCommand) {
|
|
309
459
|
const escapedCwd = cwd.replace(/"/g, '\\"');
|
|
310
|
-
|
|
311
|
-
|
|
460
|
+
if (initialCommand && (initialCommand.includes("`") || initialCommand.includes("\n") || initialCommand.length > 500)) {
|
|
461
|
+
execSync(`tmux new-session -d -s ${name} -c "${escapedCwd}"`);
|
|
462
|
+
execSync("sleep 0.5");
|
|
463
|
+
const tmpFile = `/tmp/pan-cmd-${name}.sh`;
|
|
464
|
+
const fs = __require("fs");
|
|
465
|
+
fs.writeFileSync(tmpFile, initialCommand);
|
|
466
|
+
fs.chmodSync(tmpFile, "755");
|
|
467
|
+
execSync(`tmux send-keys -t ${name} "bash ${tmpFile}" Enter`);
|
|
468
|
+
} else if (initialCommand) {
|
|
469
|
+
const cmd = `tmux new-session -d -s ${name} -c "${escapedCwd}" "${initialCommand.replace(/"/g, '\\"')}"`;
|
|
470
|
+
execSync(cmd);
|
|
471
|
+
} else {
|
|
472
|
+
execSync(`tmux new-session -d -s ${name} -c "${escapedCwd}"`);
|
|
473
|
+
}
|
|
312
474
|
}
|
|
313
475
|
function killSession(name) {
|
|
314
476
|
execSync(`tmux kill-session -t ${name}`);
|
|
@@ -323,16 +485,16 @@ function getAgentSessions() {
|
|
|
323
485
|
}
|
|
324
486
|
|
|
325
487
|
// src/lib/hooks.ts
|
|
326
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync, readdirSync as
|
|
327
|
-
import { join as
|
|
488
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync, readdirSync as readdirSync3, unlinkSync } from "fs";
|
|
489
|
+
import { join as join3 } from "path";
|
|
328
490
|
function getHookDir(agentId) {
|
|
329
|
-
return
|
|
491
|
+
return join3(AGENTS_DIR, agentId);
|
|
330
492
|
}
|
|
331
493
|
function getHookFile(agentId) {
|
|
332
|
-
return
|
|
494
|
+
return join3(getHookDir(agentId), "hook.json");
|
|
333
495
|
}
|
|
334
496
|
function getMailDir(agentId) {
|
|
335
|
-
return
|
|
497
|
+
return join3(getHookDir(agentId), "mail");
|
|
336
498
|
}
|
|
337
499
|
function initHook(agentId) {
|
|
338
500
|
const hookDir = getHookDir(agentId);
|
|
@@ -377,11 +539,11 @@ function checkHook(agentId) {
|
|
|
377
539
|
if (!hook || hook.items.length === 0) {
|
|
378
540
|
const mailDir = getMailDir(agentId);
|
|
379
541
|
if (existsSync3(mailDir)) {
|
|
380
|
-
const mails =
|
|
542
|
+
const mails = readdirSync3(mailDir).filter((f) => f.endsWith(".json"));
|
|
381
543
|
if (mails.length > 0) {
|
|
382
544
|
const mailItems = mails.map((file) => {
|
|
383
545
|
try {
|
|
384
|
-
const content = readFileSync2(
|
|
546
|
+
const content = readFileSync2(join3(mailDir, file), "utf-8");
|
|
385
547
|
return JSON.parse(content);
|
|
386
548
|
} catch {
|
|
387
549
|
return null;
|
|
@@ -440,7 +602,7 @@ function sendMail(toAgentId, from, message, priority = "normal") {
|
|
|
440
602
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
441
603
|
};
|
|
442
604
|
writeFileSync(
|
|
443
|
-
|
|
605
|
+
join3(mailDir, `${mailItem.id}.json`),
|
|
444
606
|
JSON.stringify(mailItem, null, 2)
|
|
445
607
|
);
|
|
446
608
|
}
|
|
@@ -487,10 +649,10 @@ function generateGUPPPrompt(agentId) {
|
|
|
487
649
|
}
|
|
488
650
|
|
|
489
651
|
// src/lib/cv.ts
|
|
490
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as
|
|
491
|
-
import { join as
|
|
652
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync4 } from "fs";
|
|
653
|
+
import { join as join4 } from "path";
|
|
492
654
|
function getCVFile(agentId) {
|
|
493
|
-
return
|
|
655
|
+
return join4(AGENTS_DIR, agentId, "cv.json");
|
|
494
656
|
}
|
|
495
657
|
function getAgentCV(agentId) {
|
|
496
658
|
const cvFile = getCVFile(agentId);
|
|
@@ -521,7 +683,7 @@ function getAgentCV(agentId) {
|
|
|
521
683
|
return cv;
|
|
522
684
|
}
|
|
523
685
|
function saveAgentCV(cv) {
|
|
524
|
-
const dir =
|
|
686
|
+
const dir = join4(AGENTS_DIR, cv.agentId);
|
|
525
687
|
mkdirSync3(dir, { recursive: true });
|
|
526
688
|
writeFileSync2(getCVFile(cv.agentId), JSON.stringify(cv, null, 2));
|
|
527
689
|
}
|
|
@@ -551,7 +713,7 @@ function startWork(agentId, issueId, skills) {
|
|
|
551
713
|
function getAgentRankings() {
|
|
552
714
|
const rankings = [];
|
|
553
715
|
if (!existsSync4(AGENTS_DIR)) return rankings;
|
|
554
|
-
const dirs =
|
|
716
|
+
const dirs = readdirSync4(AGENTS_DIR, { withFileTypes: true }).filter(
|
|
555
717
|
(d) => d.isDirectory()
|
|
556
718
|
);
|
|
557
719
|
for (const dir of dirs) {
|
|
@@ -620,10 +782,10 @@ function formatCV(cv) {
|
|
|
620
782
|
|
|
621
783
|
// src/lib/agents.ts
|
|
622
784
|
function getAgentDir(agentId) {
|
|
623
|
-
return
|
|
785
|
+
return join5(AGENTS_DIR, agentId);
|
|
624
786
|
}
|
|
625
787
|
function getAgentState(agentId) {
|
|
626
|
-
const stateFile =
|
|
788
|
+
const stateFile = join5(getAgentDir(agentId), "state.json");
|
|
627
789
|
if (!existsSync5(stateFile)) return null;
|
|
628
790
|
const content = readFileSync4(stateFile, "utf8");
|
|
629
791
|
return JSON.parse(content);
|
|
@@ -632,7 +794,7 @@ function saveAgentState(state) {
|
|
|
632
794
|
const dir = getAgentDir(state.id);
|
|
633
795
|
mkdirSync4(dir, { recursive: true });
|
|
634
796
|
writeFileSync3(
|
|
635
|
-
|
|
797
|
+
join5(dir, "state.json"),
|
|
636
798
|
JSON.stringify(state, null, 2)
|
|
637
799
|
);
|
|
638
800
|
}
|
|
@@ -660,8 +822,34 @@ function spawnAgent(options) {
|
|
|
660
822
|
prompt = guppPrompt + "\n\n---\n\n" + prompt;
|
|
661
823
|
}
|
|
662
824
|
}
|
|
663
|
-
const
|
|
825
|
+
const promptFile = join5(getAgentDir(agentId), "initial-prompt.md");
|
|
826
|
+
if (prompt) {
|
|
827
|
+
writeFileSync3(promptFile, prompt);
|
|
828
|
+
}
|
|
829
|
+
const claudeCmd = `claude --dangerously-skip-permissions --model ${state.model}`;
|
|
664
830
|
createSession(agentId, options.workspace, claudeCmd);
|
|
831
|
+
if (prompt) {
|
|
832
|
+
let ready = false;
|
|
833
|
+
for (let i = 0; i < 15; i++) {
|
|
834
|
+
execSync2("sleep 1");
|
|
835
|
+
try {
|
|
836
|
+
const pane = execSync2(`tmux capture-pane -t ${agentId} -p`, { encoding: "utf-8" });
|
|
837
|
+
if (pane.includes("\u276F") || pane.includes(">")) {
|
|
838
|
+
ready = true;
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
} catch {
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (ready) {
|
|
845
|
+
execSync2(`tmux load-buffer "${promptFile}"`);
|
|
846
|
+
execSync2(`tmux paste-buffer -t ${agentId}`);
|
|
847
|
+
execSync2("sleep 0.5");
|
|
848
|
+
execSync2(`tmux send-keys -t ${agentId} Enter`);
|
|
849
|
+
} else {
|
|
850
|
+
console.error("Claude did not become ready in time, prompt not sent");
|
|
851
|
+
}
|
|
852
|
+
}
|
|
665
853
|
state.status = "running";
|
|
666
854
|
saveAgentState(state);
|
|
667
855
|
startWork(agentId, options.issueId);
|
|
@@ -672,7 +860,7 @@ function listRunningAgents() {
|
|
|
672
860
|
const tmuxNames = new Set(tmuxSessions.map((s) => s.name));
|
|
673
861
|
const agents = [];
|
|
674
862
|
if (!existsSync5(AGENTS_DIR)) return agents;
|
|
675
|
-
const dirs =
|
|
863
|
+
const dirs = readdirSync5(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
676
864
|
for (const dir of dirs) {
|
|
677
865
|
const state = getAgentState(dir.name);
|
|
678
866
|
if (state) {
|
|
@@ -701,11 +889,11 @@ function messageAgent(agentId, message) {
|
|
|
701
889
|
throw new Error(`Agent ${normalizedId} not running`);
|
|
702
890
|
}
|
|
703
891
|
sendKeys(normalizedId, message);
|
|
704
|
-
const mailDir =
|
|
892
|
+
const mailDir = join5(getAgentDir(normalizedId), "mail");
|
|
705
893
|
mkdirSync4(mailDir, { recursive: true });
|
|
706
894
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
707
895
|
writeFileSync3(
|
|
708
|
-
|
|
896
|
+
join5(mailDir, `${timestamp}.md`),
|
|
709
897
|
`# Message
|
|
710
898
|
|
|
711
899
|
${message}
|
|
@@ -727,7 +915,7 @@ function recoverAgent(agentId) {
|
|
|
727
915
|
if (sessionExists(normalizedId)) {
|
|
728
916
|
return state;
|
|
729
917
|
}
|
|
730
|
-
const healthFile =
|
|
918
|
+
const healthFile = join5(getAgentDir(normalizedId), "health.json");
|
|
731
919
|
let health = { consecutiveFailures: 0, killCount: 0, recoveryCount: 0 };
|
|
732
920
|
if (existsSync5(healthFile)) {
|
|
733
921
|
try {
|
|
@@ -738,7 +926,7 @@ function recoverAgent(agentId) {
|
|
|
738
926
|
health.recoveryCount = (health.recoveryCount || 0) + 1;
|
|
739
927
|
writeFileSync3(healthFile, JSON.stringify(health, null, 2));
|
|
740
928
|
const recoveryPrompt = generateRecoveryPrompt(state);
|
|
741
|
-
const claudeCmd = `claude --model ${state.model} "${recoveryPrompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
929
|
+
const claudeCmd = `claude --dangerously-skip-permissions --model ${state.model} "${recoveryPrompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
742
930
|
createSession(normalizedId, state.workspace, claudeCmd);
|
|
743
931
|
state.status = "running";
|
|
744
932
|
state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -797,37 +985,203 @@ function autoRecoverAgents() {
|
|
|
797
985
|
}
|
|
798
986
|
|
|
799
987
|
// src/cli/commands/work/issue.ts
|
|
988
|
+
function findWorkspace(issueId) {
|
|
989
|
+
const normalizedId = issueId.toLowerCase();
|
|
990
|
+
let dir = process.cwd();
|
|
991
|
+
for (let i = 0; i < 10; i++) {
|
|
992
|
+
const workspacesDir = join6(dir, "workspaces");
|
|
993
|
+
if (existsSync6(workspacesDir)) {
|
|
994
|
+
const workspaceName = `feature-${normalizedId}`;
|
|
995
|
+
const workspacePath = join6(workspacesDir, workspaceName);
|
|
996
|
+
if (existsSync6(workspacePath)) {
|
|
997
|
+
return workspacePath;
|
|
998
|
+
}
|
|
999
|
+
const altPath = join6(workspacesDir, normalizedId);
|
|
1000
|
+
if (existsSync6(altPath)) {
|
|
1001
|
+
return altPath;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const parent = dirname2(dir);
|
|
1005
|
+
if (parent === dir) break;
|
|
1006
|
+
dir = parent;
|
|
1007
|
+
}
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
function findProjectRoot() {
|
|
1011
|
+
let dir = process.cwd();
|
|
1012
|
+
for (let i = 0; i < 10; i++) {
|
|
1013
|
+
if (existsSync6(join6(dir, "workspaces")) || existsSync6(join6(dir, ".git")) || existsSync6(join6(dir, "CLAUDE.md"))) {
|
|
1014
|
+
return dir;
|
|
1015
|
+
}
|
|
1016
|
+
const parent = dirname2(dir);
|
|
1017
|
+
if (parent === dir) break;
|
|
1018
|
+
dir = parent;
|
|
1019
|
+
}
|
|
1020
|
+
return process.cwd();
|
|
1021
|
+
}
|
|
1022
|
+
function readPlanningContext(workspacePath) {
|
|
1023
|
+
const statePath = join6(workspacePath, ".planning", "STATE.md");
|
|
1024
|
+
if (existsSync6(statePath)) {
|
|
1025
|
+
return readFileSync5(statePath, "utf-8");
|
|
1026
|
+
}
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
function extractBeadsIdsFromState(stateContent) {
|
|
1030
|
+
const ids = [];
|
|
1031
|
+
const backtickMatches = stateContent.match(/`([a-z]+-[a-z0-9]+)`/g) || [];
|
|
1032
|
+
for (const match of backtickMatches) {
|
|
1033
|
+
const id = match.replace(/`/g, "");
|
|
1034
|
+
if (id.match(/^[a-z]+-[a-z0-9]{2,4}$/)) {
|
|
1035
|
+
ids.push(id);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return [...new Set(ids)];
|
|
1039
|
+
}
|
|
1040
|
+
function readBeadsTasks(workspacePath, projectRoot, issueId) {
|
|
1041
|
+
const tasks = [];
|
|
1042
|
+
const normalizedId = issueId.toLowerCase();
|
|
1043
|
+
const stateContent = readPlanningContext(workspacePath);
|
|
1044
|
+
const beadsIds = stateContent ? extractBeadsIdsFromState(stateContent) : [];
|
|
1045
|
+
const beadsPaths = [
|
|
1046
|
+
join6(workspacePath, ".beads", "issues.jsonl"),
|
|
1047
|
+
join6(projectRoot, ".beads", "issues.jsonl")
|
|
1048
|
+
];
|
|
1049
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1050
|
+
for (const beadsPath of beadsPaths) {
|
|
1051
|
+
if (!existsSync6(beadsPath)) continue;
|
|
1052
|
+
try {
|
|
1053
|
+
const content = readFileSync5(beadsPath, "utf-8");
|
|
1054
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
1055
|
+
for (const line of lines) {
|
|
1056
|
+
try {
|
|
1057
|
+
const task = JSON.parse(line);
|
|
1058
|
+
if (seenIds.has(task.id)) continue;
|
|
1059
|
+
const tags = task.tags || [];
|
|
1060
|
+
const isMatch = beadsIds.includes(task.id) || tags.some((t) => t.toLowerCase().includes(normalizedId)) || task.title?.toLowerCase().includes(normalizedId);
|
|
1061
|
+
if (isMatch) {
|
|
1062
|
+
seenIds.add(task.id);
|
|
1063
|
+
tasks.push(`- [${task.status || "open"}] ${task.title} (${task.id})`);
|
|
1064
|
+
}
|
|
1065
|
+
} catch {
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return tasks;
|
|
1072
|
+
}
|
|
1073
|
+
function buildAgentPrompt(issueId, workspacePath, projectRoot) {
|
|
1074
|
+
const lines = [
|
|
1075
|
+
`# Working on Issue: ${issueId}`,
|
|
1076
|
+
"",
|
|
1077
|
+
`**Workspace:** ${workspacePath}`,
|
|
1078
|
+
""
|
|
1079
|
+
];
|
|
1080
|
+
const hasStateFile = existsSync6(join6(workspacePath, ".planning", "STATE.md"));
|
|
1081
|
+
const hasClaudeMd = existsSync6(join6(workspacePath, "CLAUDE.md"));
|
|
1082
|
+
const hasProjectClaudeMd = existsSync6(join6(projectRoot, "CLAUDE.md"));
|
|
1083
|
+
lines.push("## IMPORTANT: Read Context Files First");
|
|
1084
|
+
lines.push("");
|
|
1085
|
+
lines.push("Before starting any work, you MUST read these files to understand the full context:");
|
|
1086
|
+
lines.push("");
|
|
1087
|
+
if (hasStateFile) {
|
|
1088
|
+
lines.push(`1. **Read \`.planning/STATE.md\`** - Contains the full planning context, decisions made, and current status for this issue.`);
|
|
1089
|
+
}
|
|
1090
|
+
if (hasClaudeMd) {
|
|
1091
|
+
lines.push(`2. **Read \`CLAUDE.md\`** (in workspace) - Contains workspace-specific instructions and warnings.`);
|
|
1092
|
+
}
|
|
1093
|
+
if (hasProjectClaudeMd && projectRoot !== workspacePath) {
|
|
1094
|
+
lines.push(`3. **Read \`${projectRoot}/CLAUDE.md\`** - Contains project-wide development guidelines.`);
|
|
1095
|
+
}
|
|
1096
|
+
lines.push("");
|
|
1097
|
+
lines.push("These files contain critical context that may have been updated since the last session.");
|
|
1098
|
+
lines.push("");
|
|
1099
|
+
const beadsTasks = readBeadsTasks(workspacePath, projectRoot, issueId);
|
|
1100
|
+
if (beadsTasks.length > 0) {
|
|
1101
|
+
lines.push("## Beads Tasks");
|
|
1102
|
+
lines.push("");
|
|
1103
|
+
lines.push("Tasks created during planning (check STATE.md for which are complete):");
|
|
1104
|
+
lines.push("");
|
|
1105
|
+
lines.push(beadsTasks.join("\n"));
|
|
1106
|
+
lines.push("");
|
|
1107
|
+
lines.push("Use `bd show <task-id>` to see task details, `bd update <task-id> --status in_progress` to start work.");
|
|
1108
|
+
lines.push("");
|
|
1109
|
+
}
|
|
1110
|
+
lines.push("## Your Task");
|
|
1111
|
+
lines.push("");
|
|
1112
|
+
lines.push("1. Read the context files listed above");
|
|
1113
|
+
lines.push("2. Check STATE.md for current status and what work remains");
|
|
1114
|
+
lines.push("3. Continue implementing the planned work");
|
|
1115
|
+
lines.push("4. Mark beads tasks as complete as you finish them: `bd update <task-id> --status closed`");
|
|
1116
|
+
lines.push("");
|
|
1117
|
+
lines.push("## CRITICAL: Keep STATE.md Updated");
|
|
1118
|
+
lines.push("");
|
|
1119
|
+
lines.push("**You may be interrupted, crash, or be stopped at any time.** To ensure the next agent can continue:");
|
|
1120
|
+
lines.push("");
|
|
1121
|
+
lines.push("1. **Update `.planning/STATE.md` frequently** as you complete work");
|
|
1122
|
+
lines.push('2. After completing each task or significant milestone, update the "Current Status" section');
|
|
1123
|
+
lines.push("3. Document any decisions made or blockers encountered");
|
|
1124
|
+
lines.push('4. Keep the "Remaining Work" section accurate');
|
|
1125
|
+
lines.push("");
|
|
1126
|
+
lines.push("The next agent will read STATE.md to know exactly where to pick up. Beads tasks track individual items,");
|
|
1127
|
+
lines.push("but STATE.md provides the narrative context and current state that beads alone cannot capture.");
|
|
1128
|
+
lines.push("");
|
|
1129
|
+
return lines.join("\n");
|
|
1130
|
+
}
|
|
800
1131
|
async function issueCommand(id, options) {
|
|
801
1132
|
const spinner = ora4(`Preparing workspace for ${id}...`).start();
|
|
802
1133
|
try {
|
|
803
1134
|
const normalizedId = id.toLowerCase();
|
|
804
|
-
const
|
|
1135
|
+
const projectRoot = findProjectRoot();
|
|
1136
|
+
let workspace = findWorkspace(id);
|
|
1137
|
+
if (!workspace) {
|
|
1138
|
+
spinner.warn(`No workspace found for ${id}, using project root`);
|
|
1139
|
+
workspace = projectRoot;
|
|
1140
|
+
} else {
|
|
1141
|
+
spinner.text = `Found workspace: ${workspace}`;
|
|
1142
|
+
}
|
|
805
1143
|
if (options.dryRun) {
|
|
806
1144
|
spinner.info("Dry run mode");
|
|
807
1145
|
console.log("");
|
|
808
|
-
console.log(
|
|
1146
|
+
console.log(chalk6.bold("Would create:"));
|
|
809
1147
|
console.log(` Agent ID: agent-${normalizedId}`);
|
|
810
1148
|
console.log(` Workspace: ${workspace}`);
|
|
811
1149
|
console.log(` Runtime: ${options.runtime}`);
|
|
812
1150
|
console.log(` Model: ${options.model}`);
|
|
1151
|
+
const planningContext2 = readPlanningContext(workspace);
|
|
1152
|
+
const beadsTasks2 = readBeadsTasks(workspace, projectRoot, id);
|
|
1153
|
+
console.log("");
|
|
1154
|
+
console.log(chalk6.bold("Context:"));
|
|
1155
|
+
console.log(` Planning: ${planningContext2 ? "Found (.planning/STATE.md)" : "None"}`);
|
|
1156
|
+
console.log(` Beads: ${beadsTasks2.length} tasks`);
|
|
813
1157
|
return;
|
|
814
1158
|
}
|
|
1159
|
+
spinner.text = "Building agent prompt with planning context...";
|
|
1160
|
+
const prompt = buildAgentPrompt(id, workspace, projectRoot);
|
|
815
1161
|
spinner.text = "Spawning agent...";
|
|
816
1162
|
const agent = spawnAgent({
|
|
817
1163
|
issueId: id,
|
|
818
1164
|
workspace,
|
|
819
1165
|
runtime: options.runtime,
|
|
820
1166
|
model: options.model,
|
|
821
|
-
prompt
|
|
1167
|
+
prompt
|
|
822
1168
|
});
|
|
823
1169
|
spinner.succeed(`Agent spawned: ${agent.id}`);
|
|
824
1170
|
console.log("");
|
|
825
|
-
console.log(
|
|
826
|
-
console.log(` Session: ${
|
|
1171
|
+
console.log(chalk6.bold("Agent Details:"));
|
|
1172
|
+
console.log(` Session: ${chalk6.cyan(agent.id)}`);
|
|
827
1173
|
console.log(` Workspace: ${workspace}`);
|
|
828
1174
|
console.log(` Runtime: ${agent.runtime} (${agent.model})`);
|
|
1175
|
+
const planningContext = readPlanningContext(workspace);
|
|
1176
|
+
const beadsTasks = readBeadsTasks(workspace, projectRoot, id);
|
|
1177
|
+
if (planningContext || beadsTasks.length > 0) {
|
|
1178
|
+
console.log("");
|
|
1179
|
+
console.log(chalk6.bold("Context Loaded:"));
|
|
1180
|
+
if (planningContext) console.log(` Planning: ${chalk6.green("\u2713")} STATE.md`);
|
|
1181
|
+
if (beadsTasks.length > 0) console.log(` Beads: ${chalk6.green("\u2713")} ${beadsTasks.length} tasks`);
|
|
1182
|
+
}
|
|
829
1183
|
console.log("");
|
|
830
|
-
console.log(
|
|
1184
|
+
console.log(chalk6.dim("Commands:"));
|
|
831
1185
|
console.log(` Attach: tmux attach -t ${agent.id}`);
|
|
832
1186
|
console.log(` Message: pan work tell ${id} "your message"`);
|
|
833
1187
|
console.log(` Kill: pan work kill ${id}`);
|
|
@@ -838,7 +1192,7 @@ async function issueCommand(id, options) {
|
|
|
838
1192
|
}
|
|
839
1193
|
|
|
840
1194
|
// src/cli/commands/work/status.ts
|
|
841
|
-
import
|
|
1195
|
+
import chalk7 from "chalk";
|
|
842
1196
|
async function statusCommand(options) {
|
|
843
1197
|
const agents = listRunningAgents();
|
|
844
1198
|
if (options.json) {
|
|
@@ -846,101 +1200,101 @@ async function statusCommand(options) {
|
|
|
846
1200
|
return;
|
|
847
1201
|
}
|
|
848
1202
|
if (agents.length === 0) {
|
|
849
|
-
console.log(
|
|
850
|
-
console.log(
|
|
1203
|
+
console.log(chalk7.dim("No running agents."));
|
|
1204
|
+
console.log(chalk7.dim('Use "pan work issue <id>" to spawn one.'));
|
|
851
1205
|
return;
|
|
852
1206
|
}
|
|
853
|
-
console.log(
|
|
1207
|
+
console.log(chalk7.bold("\nRunning Agents\n"));
|
|
854
1208
|
for (const agent of agents) {
|
|
855
|
-
const statusColor = agent.tmuxActive ?
|
|
1209
|
+
const statusColor = agent.tmuxActive ? chalk7.green : chalk7.red;
|
|
856
1210
|
const status = agent.tmuxActive ? "running" : "stopped";
|
|
857
1211
|
const startedAt = new Date(agent.startedAt);
|
|
858
1212
|
const duration = Math.floor((Date.now() - startedAt.getTime()) / 1e3 / 60);
|
|
859
|
-
console.log(`${
|
|
1213
|
+
console.log(`${chalk7.cyan(agent.id)}`);
|
|
860
1214
|
console.log(` Issue: ${agent.issueId}`);
|
|
861
1215
|
console.log(` Status: ${statusColor(status)}`);
|
|
862
1216
|
console.log(` Runtime: ${agent.runtime} (${agent.model})`);
|
|
863
1217
|
console.log(` Duration: ${duration} min`);
|
|
864
|
-
console.log(` Workspace: ${
|
|
1218
|
+
console.log(` Workspace: ${chalk7.dim(agent.workspace)}`);
|
|
865
1219
|
console.log("");
|
|
866
1220
|
}
|
|
867
1221
|
}
|
|
868
1222
|
|
|
869
1223
|
// src/cli/commands/work/tell.ts
|
|
870
|
-
import
|
|
1224
|
+
import chalk8 from "chalk";
|
|
871
1225
|
async function tellCommand(id, message) {
|
|
872
1226
|
const agentId = id.startsWith("agent-") ? id : `agent-${id.toLowerCase()}`;
|
|
873
1227
|
try {
|
|
874
1228
|
messageAgent(agentId, message);
|
|
875
|
-
console.log(
|
|
876
|
-
console.log(
|
|
1229
|
+
console.log(chalk8.green("Message sent to " + agentId));
|
|
1230
|
+
console.log(chalk8.dim(` "${message}"`));
|
|
877
1231
|
} catch (error) {
|
|
878
|
-
console.error(
|
|
1232
|
+
console.error(chalk8.red("Error: " + error.message));
|
|
879
1233
|
process.exit(1);
|
|
880
1234
|
}
|
|
881
1235
|
}
|
|
882
1236
|
|
|
883
1237
|
// src/cli/commands/work/kill.ts
|
|
884
|
-
import
|
|
1238
|
+
import chalk9 from "chalk";
|
|
885
1239
|
async function killCommand(id, options) {
|
|
886
1240
|
const agentId = id.startsWith("agent-") ? id : `agent-${id.toLowerCase()}`;
|
|
887
1241
|
const state = getAgentState(agentId);
|
|
888
1242
|
const isRunning = sessionExists(agentId);
|
|
889
1243
|
if (!state && !isRunning) {
|
|
890
|
-
console.log(
|
|
1244
|
+
console.log(chalk9.yellow(`Agent ${agentId} not found.`));
|
|
891
1245
|
return;
|
|
892
1246
|
}
|
|
893
1247
|
if (!options.force && isRunning) {
|
|
894
1248
|
}
|
|
895
1249
|
try {
|
|
896
1250
|
stopAgent(agentId);
|
|
897
|
-
console.log(
|
|
1251
|
+
console.log(chalk9.green(`Killed agent: ${agentId}`));
|
|
898
1252
|
} catch (error) {
|
|
899
|
-
console.error(
|
|
1253
|
+
console.error(chalk9.red("Error: " + error.message));
|
|
900
1254
|
process.exit(1);
|
|
901
1255
|
}
|
|
902
1256
|
}
|
|
903
1257
|
|
|
904
1258
|
// src/cli/commands/work/pending.ts
|
|
905
|
-
import
|
|
906
|
-
import { existsSync as
|
|
907
|
-
import { join as
|
|
1259
|
+
import chalk10 from "chalk";
|
|
1260
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
1261
|
+
import { join as join7 } from "path";
|
|
908
1262
|
async function pendingCommand() {
|
|
909
1263
|
const agents = listRunningAgents().filter((a) => !a.tmuxActive && a.status !== "error");
|
|
910
1264
|
if (agents.length === 0) {
|
|
911
|
-
console.log(
|
|
912
|
-
console.log(
|
|
1265
|
+
console.log(chalk10.dim("No pending reviews."));
|
|
1266
|
+
console.log(chalk10.dim("Agents will appear here when they complete work."));
|
|
913
1267
|
return;
|
|
914
1268
|
}
|
|
915
|
-
console.log(
|
|
1269
|
+
console.log(chalk10.bold("\nPending Reviews\n"));
|
|
916
1270
|
for (const agent of agents) {
|
|
917
|
-
console.log(`${
|
|
1271
|
+
console.log(`${chalk10.cyan(agent.issueId)}`);
|
|
918
1272
|
console.log(` Agent: ${agent.id}`);
|
|
919
|
-
console.log(` Workspace: ${
|
|
920
|
-
const completionFile =
|
|
921
|
-
if (
|
|
922
|
-
const content =
|
|
1273
|
+
console.log(` Workspace: ${chalk10.dim(agent.workspace)}`);
|
|
1274
|
+
const completionFile = join7(AGENTS_DIR, agent.id, "completion.md");
|
|
1275
|
+
if (existsSync7(completionFile)) {
|
|
1276
|
+
const content = readFileSync6(completionFile, "utf8");
|
|
923
1277
|
const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#"));
|
|
924
1278
|
if (firstLine) {
|
|
925
|
-
console.log(` Summary: ${
|
|
1279
|
+
console.log(` Summary: ${chalk10.dim(firstLine.trim())}`);
|
|
926
1280
|
}
|
|
927
1281
|
}
|
|
928
1282
|
console.log("");
|
|
929
1283
|
}
|
|
930
|
-
console.log(
|
|
1284
|
+
console.log(chalk10.dim('Run "pan work approve <id>" to approve and merge.'));
|
|
931
1285
|
}
|
|
932
1286
|
|
|
933
1287
|
// src/cli/commands/work/approve.ts
|
|
934
|
-
import
|
|
1288
|
+
import chalk11 from "chalk";
|
|
935
1289
|
import ora5 from "ora";
|
|
936
|
-
import { existsSync as
|
|
937
|
-
import { join as
|
|
1290
|
+
import { existsSync as existsSync8, writeFileSync as writeFileSync4, readFileSync as readFileSync7 } from "fs";
|
|
1291
|
+
import { join as join8 } from "path";
|
|
938
1292
|
import { homedir } from "os";
|
|
939
|
-
import { execSync as
|
|
1293
|
+
import { execSync as execSync3 } from "child_process";
|
|
940
1294
|
function getLinearApiKey() {
|
|
941
|
-
const envFile =
|
|
942
|
-
if (
|
|
943
|
-
const content =
|
|
1295
|
+
const envFile = join8(homedir(), ".panopticon.env");
|
|
1296
|
+
if (existsSync8(envFile)) {
|
|
1297
|
+
const content = readFileSync7(envFile, "utf-8");
|
|
944
1298
|
const match = content.match(/LINEAR_API_KEY=(.+)/);
|
|
945
1299
|
if (match) return match[1].trim();
|
|
946
1300
|
}
|
|
@@ -948,7 +1302,7 @@ function getLinearApiKey() {
|
|
|
948
1302
|
}
|
|
949
1303
|
function checkGhCli() {
|
|
950
1304
|
try {
|
|
951
|
-
|
|
1305
|
+
execSync3("which gh", { stdio: "pipe" });
|
|
952
1306
|
return true;
|
|
953
1307
|
} catch {
|
|
954
1308
|
return false;
|
|
@@ -956,12 +1310,12 @@ function checkGhCli() {
|
|
|
956
1310
|
}
|
|
957
1311
|
function findPRForBranch(workspace) {
|
|
958
1312
|
try {
|
|
959
|
-
const branch =
|
|
1313
|
+
const branch = execSync3("git rev-parse --abbrev-ref HEAD", {
|
|
960
1314
|
cwd: workspace,
|
|
961
1315
|
encoding: "utf-8",
|
|
962
1316
|
stdio: ["pipe", "pipe", "pipe"]
|
|
963
1317
|
}).trim();
|
|
964
|
-
const prJson =
|
|
1318
|
+
const prJson = execSync3(`gh pr list --head "${branch}" --json number,url --limit 1`, {
|
|
965
1319
|
cwd: workspace,
|
|
966
1320
|
encoding: "utf-8",
|
|
967
1321
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -977,7 +1331,7 @@ function findPRForBranch(workspace) {
|
|
|
977
1331
|
}
|
|
978
1332
|
function mergePR(workspace, prNumber) {
|
|
979
1333
|
try {
|
|
980
|
-
|
|
1334
|
+
execSync3(`gh pr merge ${prNumber} --squash --delete-branch`, {
|
|
981
1335
|
cwd: workspace,
|
|
982
1336
|
encoding: "utf-8",
|
|
983
1337
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1013,8 +1367,8 @@ async function approveCommand(id, options = {}) {
|
|
|
1013
1367
|
const agentId = id.startsWith("agent-") ? id : `agent-${id.toLowerCase()}`;
|
|
1014
1368
|
const state = getAgentState(agentId);
|
|
1015
1369
|
if (!state) {
|
|
1016
|
-
console.log(
|
|
1017
|
-
console.log(
|
|
1370
|
+
console.log(chalk11.yellow(`Agent ${agentId} not found.`));
|
|
1371
|
+
console.log(chalk11.dim('Run "pan work status" to see running agents.'));
|
|
1018
1372
|
return;
|
|
1019
1373
|
}
|
|
1020
1374
|
const spinner = ora5("Approving work...").start();
|
|
@@ -1025,7 +1379,7 @@ async function approveCommand(id, options = {}) {
|
|
|
1025
1379
|
if (options.merge !== false) {
|
|
1026
1380
|
if (!checkGhCli()) {
|
|
1027
1381
|
spinner.warn("gh CLI not found - skipping PR merge");
|
|
1028
|
-
console.log(
|
|
1382
|
+
console.log(chalk11.dim(" Install: https://cli.github.com/"));
|
|
1029
1383
|
} else {
|
|
1030
1384
|
spinner.text = "Looking for PR...";
|
|
1031
1385
|
const pr = findPRForBranch(workspace);
|
|
@@ -1034,13 +1388,13 @@ async function approveCommand(id, options = {}) {
|
|
|
1034
1388
|
const result = mergePR(workspace, pr.number);
|
|
1035
1389
|
if (result.success) {
|
|
1036
1390
|
prMerged = true;
|
|
1037
|
-
console.log(
|
|
1391
|
+
console.log(chalk11.green(` \u2713 Merged PR #${pr.number}`));
|
|
1038
1392
|
} else {
|
|
1039
|
-
console.log(
|
|
1040
|
-
console.log(
|
|
1393
|
+
console.log(chalk11.yellow(` \u26A0 Failed to merge: ${result.error}`));
|
|
1394
|
+
console.log(chalk11.dim(` Merge manually: gh pr merge ${pr.number} --squash`));
|
|
1041
1395
|
}
|
|
1042
1396
|
} else {
|
|
1043
|
-
console.log(
|
|
1397
|
+
console.log(chalk11.dim(" No PR found for this branch"));
|
|
1044
1398
|
}
|
|
1045
1399
|
}
|
|
1046
1400
|
}
|
|
@@ -1050,18 +1404,18 @@ async function approveCommand(id, options = {}) {
|
|
|
1050
1404
|
spinner.text = "Updating Linear status...";
|
|
1051
1405
|
linearUpdated = await updateLinearStatus(apiKey, state.issueId);
|
|
1052
1406
|
if (linearUpdated) {
|
|
1053
|
-
console.log(
|
|
1407
|
+
console.log(chalk11.green(` \u2713 Updated ${state.issueId} to Done`));
|
|
1054
1408
|
} else {
|
|
1055
|
-
console.log(
|
|
1409
|
+
console.log(chalk11.yellow(` \u26A0 Failed to update Linear status`));
|
|
1056
1410
|
}
|
|
1057
1411
|
} else {
|
|
1058
|
-
console.log(
|
|
1412
|
+
console.log(chalk11.dim(" LINEAR_API_KEY not set - skipping status update"));
|
|
1059
1413
|
}
|
|
1060
1414
|
}
|
|
1061
1415
|
state.status = "stopped";
|
|
1062
1416
|
state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1063
1417
|
saveAgentState(state);
|
|
1064
|
-
const approvedFile =
|
|
1418
|
+
const approvedFile = join8(AGENTS_DIR, agentId, "approved");
|
|
1065
1419
|
writeFileSync4(approvedFile, JSON.stringify({
|
|
1066
1420
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1067
1421
|
prMerged,
|
|
@@ -1069,13 +1423,13 @@ async function approveCommand(id, options = {}) {
|
|
|
1069
1423
|
}));
|
|
1070
1424
|
spinner.succeed(`Approved: ${state.issueId}`);
|
|
1071
1425
|
console.log("");
|
|
1072
|
-
console.log(
|
|
1073
|
-
console.log(` Issue: ${
|
|
1074
|
-
console.log(` PR: ${prMerged ?
|
|
1075
|
-
console.log(` Linear: ${linearUpdated ?
|
|
1426
|
+
console.log(chalk11.bold("Summary:"));
|
|
1427
|
+
console.log(` Issue: ${chalk11.cyan(state.issueId)}`);
|
|
1428
|
+
console.log(` PR: ${prMerged ? chalk11.green("Merged") : chalk11.dim("Not merged")}`);
|
|
1429
|
+
console.log(` Linear: ${linearUpdated ? chalk11.green("Updated to Done") : chalk11.dim("Not updated")}`);
|
|
1076
1430
|
console.log("");
|
|
1077
|
-
console.log(
|
|
1078
|
-
console.log(
|
|
1431
|
+
console.log(chalk11.dim("Workspace can be cleaned up with:"));
|
|
1432
|
+
console.log(chalk11.dim(` pan workspace destroy ${state.issueId}`));
|
|
1079
1433
|
} catch (error) {
|
|
1080
1434
|
spinner.fail(error.message);
|
|
1081
1435
|
process.exit(1);
|
|
@@ -1083,16 +1437,17 @@ async function approveCommand(id, options = {}) {
|
|
|
1083
1437
|
}
|
|
1084
1438
|
|
|
1085
1439
|
// src/cli/commands/work/plan.ts
|
|
1086
|
-
import
|
|
1440
|
+
import chalk12 from "chalk";
|
|
1087
1441
|
import ora6 from "ora";
|
|
1088
|
-
import
|
|
1089
|
-
import {
|
|
1442
|
+
import inquirer2 from "inquirer";
|
|
1443
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
|
|
1444
|
+
import { join as join9, dirname as dirname3 } from "path";
|
|
1090
1445
|
import { homedir as homedir2 } from "os";
|
|
1091
|
-
import { execSync as
|
|
1446
|
+
import { execSync as execSync4 } from "child_process";
|
|
1092
1447
|
function getLinearApiKey2() {
|
|
1093
|
-
const envFile =
|
|
1094
|
-
if (
|
|
1095
|
-
const content =
|
|
1448
|
+
const envFile = join9(homedir2(), ".panopticon.env");
|
|
1449
|
+
if (existsSync9(envFile)) {
|
|
1450
|
+
const content = readFileSync8(envFile, "utf-8");
|
|
1096
1451
|
const match = content.match(/LINEAR_API_KEY=(.+)/);
|
|
1097
1452
|
if (match) return match[1].trim();
|
|
1098
1453
|
}
|
|
@@ -1102,6 +1457,8 @@ function findPRDFiles(issueId) {
|
|
|
1102
1457
|
const found = [];
|
|
1103
1458
|
const cwd = process.cwd();
|
|
1104
1459
|
const searchPaths = [
|
|
1460
|
+
"docs/prds/active",
|
|
1461
|
+
"docs/prds/planned",
|
|
1105
1462
|
"docs/prds",
|
|
1106
1463
|
"docs/prd",
|
|
1107
1464
|
"prds",
|
|
@@ -1109,10 +1466,10 @@ function findPRDFiles(issueId) {
|
|
|
1109
1466
|
];
|
|
1110
1467
|
const issueIdLower = issueId.toLowerCase();
|
|
1111
1468
|
for (const searchPath of searchPaths) {
|
|
1112
|
-
const fullPath =
|
|
1113
|
-
if (!
|
|
1469
|
+
const fullPath = join9(cwd, searchPath);
|
|
1470
|
+
if (!existsSync9(fullPath)) continue;
|
|
1114
1471
|
try {
|
|
1115
|
-
const result =
|
|
1472
|
+
const result = execSync4(
|
|
1116
1473
|
`find "${fullPath}" -type f -name "*.md" 2>/dev/null | xargs grep -l -i "${issueIdLower}" 2>/dev/null || true`,
|
|
1117
1474
|
{ encoding: "utf-8" }
|
|
1118
1475
|
);
|
|
@@ -1123,76 +1480,352 @@ function findPRDFiles(issueId) {
|
|
|
1123
1480
|
}
|
|
1124
1481
|
return [...new Set(found)];
|
|
1125
1482
|
}
|
|
1126
|
-
function
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1483
|
+
function analyzeComplexity(issue, prdFiles) {
|
|
1484
|
+
const reasons = [];
|
|
1485
|
+
const subsystems = [];
|
|
1486
|
+
let estimatedTasks = 1;
|
|
1487
|
+
const desc = (issue.description || "").toLowerCase();
|
|
1488
|
+
const title = issue.title.toLowerCase();
|
|
1489
|
+
const combined = `${title} ${desc}`;
|
|
1490
|
+
if (combined.includes("frontend") || combined.includes("ui") || combined.includes("component")) {
|
|
1491
|
+
subsystems.push("frontend");
|
|
1492
|
+
}
|
|
1493
|
+
if (combined.includes("backend") || combined.includes("api") || combined.includes("endpoint")) {
|
|
1494
|
+
subsystems.push("backend");
|
|
1495
|
+
}
|
|
1496
|
+
if (combined.includes("database") || combined.includes("migration") || combined.includes("schema")) {
|
|
1497
|
+
subsystems.push("database");
|
|
1498
|
+
}
|
|
1499
|
+
if (combined.includes("test") || combined.includes("e2e") || combined.includes("playwright")) {
|
|
1500
|
+
subsystems.push("tests");
|
|
1501
|
+
}
|
|
1502
|
+
if (subsystems.length > 1) {
|
|
1503
|
+
reasons.push(`Multiple subsystems involved: ${subsystems.join(", ")}`);
|
|
1504
|
+
estimatedTasks += subsystems.length;
|
|
1505
|
+
}
|
|
1506
|
+
const ambiguousPatterns = [
|
|
1507
|
+
"should we",
|
|
1508
|
+
"maybe",
|
|
1509
|
+
"or",
|
|
1510
|
+
"consider",
|
|
1511
|
+
"option",
|
|
1512
|
+
"approach",
|
|
1513
|
+
"tbd",
|
|
1514
|
+
"to be determined",
|
|
1515
|
+
"needs discussion",
|
|
1516
|
+
"unclear"
|
|
1517
|
+
];
|
|
1518
|
+
for (const pattern of ambiguousPatterns) {
|
|
1519
|
+
if (combined.includes(pattern)) {
|
|
1520
|
+
reasons.push("Requirements may be ambiguous");
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
const architecturePatterns = [
|
|
1525
|
+
"refactor",
|
|
1526
|
+
"architecture",
|
|
1527
|
+
"redesign",
|
|
1528
|
+
"restructure",
|
|
1529
|
+
"migrate",
|
|
1530
|
+
"integration",
|
|
1531
|
+
"authentication",
|
|
1532
|
+
"authorization",
|
|
1533
|
+
"security"
|
|
1534
|
+
];
|
|
1535
|
+
for (const pattern of architecturePatterns) {
|
|
1536
|
+
if (combined.includes(pattern)) {
|
|
1537
|
+
reasons.push(`Architecture decision needed: ${pattern}`);
|
|
1538
|
+
estimatedTasks += 2;
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (desc.length > 500) {
|
|
1543
|
+
reasons.push("Detailed description suggests complexity");
|
|
1544
|
+
estimatedTasks += 1;
|
|
1142
1545
|
}
|
|
1143
1546
|
if (prdFiles.length > 0) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1547
|
+
reasons.push(`PRD exists - complexity already documented`);
|
|
1548
|
+
}
|
|
1549
|
+
const complexLabels = ["complex", "large", "epic", "multi-phase", "architecture"];
|
|
1550
|
+
for (const label of issue.labels || []) {
|
|
1551
|
+
if (complexLabels.some((cl) => label.name.toLowerCase().includes(cl))) {
|
|
1552
|
+
reasons.push(`Label indicates complexity: ${label.name}`);
|
|
1553
|
+
estimatedTasks += 2;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const isComplex = reasons.length >= 2 || subsystems.length > 1 || estimatedTasks >= 4;
|
|
1557
|
+
return {
|
|
1558
|
+
isComplex,
|
|
1559
|
+
reasons,
|
|
1560
|
+
subsystems,
|
|
1561
|
+
estimatedTasks: Math.max(estimatedTasks, subsystems.length + 1)
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
async function runDiscoveryPhase(issue, complexity, prdContent) {
|
|
1565
|
+
const decisions = [];
|
|
1566
|
+
const tasks = [];
|
|
1567
|
+
console.log("");
|
|
1568
|
+
console.log(chalk12.bold.cyan("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
1569
|
+
console.log(chalk12.bold.cyan(" DISCOVERY PHASE"));
|
|
1570
|
+
console.log(chalk12.bold.cyan("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
1571
|
+
console.log("");
|
|
1572
|
+
console.log(chalk12.dim("Answer questions to create a detailed execution plan."));
|
|
1573
|
+
console.log(chalk12.dim("Press Enter to skip optional questions."));
|
|
1574
|
+
console.log("");
|
|
1575
|
+
console.log(chalk12.bold("Issue:"), `${issue.identifier} - ${issue.title}`);
|
|
1576
|
+
if (complexity.subsystems.length > 0) {
|
|
1577
|
+
console.log(chalk12.bold("Detected subsystems:"), complexity.subsystems.join(", "));
|
|
1578
|
+
}
|
|
1579
|
+
console.log("");
|
|
1580
|
+
const scopeAnswer = await inquirer2.prompt([{
|
|
1581
|
+
type: "input",
|
|
1582
|
+
name: "scope",
|
|
1583
|
+
message: "What specific changes are needed? (be specific about files/components):",
|
|
1584
|
+
default: issue.description?.slice(0, 100) || ""
|
|
1585
|
+
}]);
|
|
1586
|
+
if (scopeAnswer.scope) {
|
|
1587
|
+
decisions.push({ question: "Scope", answer: scopeAnswer.scope });
|
|
1588
|
+
}
|
|
1589
|
+
const approachAnswer = await inquirer2.prompt([{
|
|
1590
|
+
type: "input",
|
|
1591
|
+
name: "approach",
|
|
1592
|
+
message: "Any specific technical approach or patterns to follow?"
|
|
1593
|
+
}]);
|
|
1594
|
+
if (approachAnswer.approach) {
|
|
1595
|
+
decisions.push({ question: "Technical approach", answer: approachAnswer.approach });
|
|
1596
|
+
}
|
|
1597
|
+
const edgeCasesAnswer = await inquirer2.prompt([{
|
|
1598
|
+
type: "input",
|
|
1599
|
+
name: "edgeCases",
|
|
1600
|
+
message: "Any edge cases or error scenarios to handle?"
|
|
1601
|
+
}]);
|
|
1602
|
+
if (edgeCasesAnswer.edgeCases) {
|
|
1603
|
+
decisions.push({ question: "Edge cases", answer: edgeCasesAnswer.edgeCases });
|
|
1604
|
+
}
|
|
1605
|
+
const testingAnswer = await inquirer2.prompt([{
|
|
1606
|
+
type: "checkbox",
|
|
1607
|
+
name: "testing",
|
|
1608
|
+
message: "What testing is required?",
|
|
1609
|
+
choices: [
|
|
1610
|
+
{ name: "Unit tests", value: "unit", checked: true },
|
|
1611
|
+
{ name: "Integration tests", value: "integration" },
|
|
1612
|
+
{ name: "E2E tests (Playwright)", value: "e2e" },
|
|
1613
|
+
{ name: "Manual testing only", value: "manual" }
|
|
1614
|
+
]
|
|
1615
|
+
}]);
|
|
1616
|
+
if (testingAnswer.testing.length > 0) {
|
|
1617
|
+
decisions.push({ question: "Testing", answer: testingAnswer.testing.join(", ") });
|
|
1618
|
+
}
|
|
1619
|
+
const outOfScopeAnswer = await inquirer2.prompt([{
|
|
1620
|
+
type: "input",
|
|
1621
|
+
name: "outOfScope",
|
|
1622
|
+
message: "Anything explicitly OUT of scope for this issue?"
|
|
1623
|
+
}]);
|
|
1624
|
+
if (outOfScopeAnswer.outOfScope) {
|
|
1625
|
+
decisions.push({ question: "Out of scope", answer: outOfScopeAnswer.outOfScope });
|
|
1626
|
+
}
|
|
1627
|
+
console.log("");
|
|
1628
|
+
console.log(chalk12.bold("Define execution tasks:"));
|
|
1629
|
+
console.log(chalk12.dim("Enter tasks in order. Empty task name to finish."));
|
|
1630
|
+
console.log("");
|
|
1631
|
+
const suggestedTasks = [
|
|
1632
|
+
{ name: "Understand requirements", description: "Review issue, PRD, and existing code" }
|
|
1633
|
+
];
|
|
1634
|
+
if (complexity.subsystems.length > 1) {
|
|
1635
|
+
suggestedTasks.push({ name: "Design approach", description: "Document architecture decisions", dependsOn: "Understand requirements" });
|
|
1636
|
+
}
|
|
1637
|
+
for (const subsystem of complexity.subsystems) {
|
|
1638
|
+
suggestedTasks.push({
|
|
1639
|
+
name: `Implement ${subsystem}`,
|
|
1640
|
+
description: `Core ${subsystem} changes`,
|
|
1641
|
+
dependsOn: complexity.subsystems.length > 1 ? "Design approach" : "Understand requirements"
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
if (suggestedTasks.length === 1) {
|
|
1645
|
+
suggestedTasks.push({ name: "Implement changes", description: "Core implementation", dependsOn: "Understand requirements" });
|
|
1646
|
+
}
|
|
1647
|
+
if (testingAnswer.testing.includes("unit") || testingAnswer.testing.includes("integration")) {
|
|
1648
|
+
suggestedTasks.push({ name: "Add tests", description: "Unit and/or integration tests", dependsOn: suggestedTasks[suggestedTasks.length - 1].name });
|
|
1649
|
+
}
|
|
1650
|
+
if (testingAnswer.testing.includes("e2e")) {
|
|
1651
|
+
suggestedTasks.push({ name: "Add E2E tests", description: "Playwright E2E tests", dependsOn: "Add tests" });
|
|
1652
|
+
}
|
|
1653
|
+
suggestedTasks.push({ name: "Verify and cleanup", description: "Lint, type check, final review", dependsOn: suggestedTasks[suggestedTasks.length - 1].name });
|
|
1654
|
+
console.log(chalk12.bold("Suggested tasks:"));
|
|
1655
|
+
for (let i = 0; i < suggestedTasks.length; i++) {
|
|
1656
|
+
const task = suggestedTasks[i];
|
|
1657
|
+
console.log(` ${i + 1}. ${task.name}${task.dependsOn ? chalk12.dim(` (after: ${task.dependsOn})`) : ""}`);
|
|
1658
|
+
}
|
|
1659
|
+
console.log("");
|
|
1660
|
+
const useDefaultAnswer = await inquirer2.prompt([{
|
|
1661
|
+
type: "confirm",
|
|
1662
|
+
name: "useDefault",
|
|
1663
|
+
message: "Use these suggested tasks?",
|
|
1664
|
+
default: true
|
|
1665
|
+
}]);
|
|
1666
|
+
if (useDefaultAnswer.useDefault) {
|
|
1667
|
+
tasks.push(...suggestedTasks);
|
|
1668
|
+
} else {
|
|
1669
|
+
let taskIndex = 1;
|
|
1670
|
+
let previousTask = "";
|
|
1671
|
+
while (true) {
|
|
1672
|
+
const taskAnswer = await inquirer2.prompt([{
|
|
1673
|
+
type: "input",
|
|
1674
|
+
name: "name",
|
|
1675
|
+
message: `Task ${taskIndex} name (empty to finish):`
|
|
1676
|
+
}]);
|
|
1677
|
+
if (!taskAnswer.name) break;
|
|
1678
|
+
const descAnswer = await inquirer2.prompt([{
|
|
1679
|
+
type: "input",
|
|
1680
|
+
name: "description",
|
|
1681
|
+
message: `Task ${taskIndex} description:`,
|
|
1682
|
+
default: taskAnswer.name
|
|
1683
|
+
}]);
|
|
1684
|
+
tasks.push({
|
|
1685
|
+
name: taskAnswer.name,
|
|
1686
|
+
description: descAnswer.description,
|
|
1687
|
+
dependsOn: previousTask || void 0
|
|
1688
|
+
});
|
|
1689
|
+
previousTask = taskAnswer.name;
|
|
1690
|
+
taskIndex++;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return { tasks, decisions };
|
|
1694
|
+
}
|
|
1695
|
+
function generateStateFile(issue, decisions, tasks) {
|
|
1696
|
+
const lines = [
|
|
1697
|
+
`# Agent State: ${issue.identifier}`,
|
|
1698
|
+
"",
|
|
1699
|
+
`**Last Updated:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1700
|
+
"",
|
|
1701
|
+
"## Current Position",
|
|
1702
|
+
"",
|
|
1703
|
+
`- **Issue:** ${issue.identifier}`,
|
|
1704
|
+
`- **Title:** ${issue.title}`,
|
|
1705
|
+
`- **Status:** Planning complete, ready for execution`,
|
|
1706
|
+
`- **Linear:** ${issue.url}`,
|
|
1707
|
+
"",
|
|
1708
|
+
"## Decisions Made During Planning",
|
|
1709
|
+
""
|
|
1710
|
+
];
|
|
1711
|
+
if (decisions.length > 0) {
|
|
1712
|
+
for (const decision of decisions) {
|
|
1713
|
+
lines.push(`- **${decision.question}:** ${decision.answer}`);
|
|
1714
|
+
}
|
|
1715
|
+
} else {
|
|
1716
|
+
lines.push("- No specific decisions recorded");
|
|
1717
|
+
}
|
|
1718
|
+
lines.push("");
|
|
1719
|
+
lines.push("## Planned Tasks");
|
|
1720
|
+
lines.push("");
|
|
1721
|
+
for (const task of tasks) {
|
|
1722
|
+
lines.push(`- [ ] ${task.name}${task.dependsOn ? ` (after: ${task.dependsOn})` : ""}`);
|
|
1723
|
+
}
|
|
1724
|
+
lines.push("");
|
|
1725
|
+
lines.push("## Blockers/Concerns");
|
|
1726
|
+
lines.push("");
|
|
1727
|
+
lines.push("- None identified during planning");
|
|
1728
|
+
lines.push("");
|
|
1729
|
+
lines.push("## Notes");
|
|
1730
|
+
lines.push("");
|
|
1731
|
+
lines.push("<!-- Add notes as work progresses -->");
|
|
1732
|
+
lines.push("");
|
|
1733
|
+
return lines.join("\n");
|
|
1734
|
+
}
|
|
1735
|
+
function generateWorkspaceFile(issue, prdFiles) {
|
|
1736
|
+
const lines = [
|
|
1737
|
+
`# Workspace: ${issue.identifier}`,
|
|
1738
|
+
"",
|
|
1739
|
+
`> ${issue.title}`,
|
|
1740
|
+
"",
|
|
1741
|
+
"## Quick Links",
|
|
1742
|
+
"",
|
|
1743
|
+
`- [Linear Issue](${issue.url})`
|
|
1744
|
+
];
|
|
1745
|
+
for (const prd of prdFiles) {
|
|
1746
|
+
const relativePath = prd.replace(process.cwd() + "/", "");
|
|
1747
|
+
lines.push(`- [PRD](${relativePath})`);
|
|
1748
|
+
}
|
|
1749
|
+
lines.push("");
|
|
1750
|
+
lines.push("## Context Files");
|
|
1751
|
+
lines.push("");
|
|
1752
|
+
lines.push("- `STATE.md` - Current progress and decisions");
|
|
1753
|
+
lines.push("- `WORKSPACE.md` - This file");
|
|
1754
|
+
lines.push("");
|
|
1755
|
+
lines.push("## Beads");
|
|
1756
|
+
lines.push("");
|
|
1757
|
+
lines.push("Check current task status:");
|
|
1758
|
+
lines.push("```bash");
|
|
1759
|
+
lines.push("bd ready # Next actionable task");
|
|
1760
|
+
lines.push(`bd list --tag ${issue.identifier} # All tasks for this issue`);
|
|
1761
|
+
lines.push("```");
|
|
1762
|
+
lines.push("");
|
|
1763
|
+
lines.push("## Agent Instructions");
|
|
1764
|
+
lines.push("");
|
|
1765
|
+
lines.push("1. Run `bd ready` to get next task");
|
|
1766
|
+
lines.push("2. Complete the task following relevant skills");
|
|
1767
|
+
lines.push('3. Run `bd close "<task name>" --reason "..."` when done');
|
|
1768
|
+
lines.push("4. Update STATE.md with progress");
|
|
1769
|
+
lines.push("5. Repeat until all tasks complete");
|
|
1770
|
+
lines.push("");
|
|
1771
|
+
return lines.join("\n");
|
|
1772
|
+
}
|
|
1773
|
+
function createBeadsTasks(issue, tasks) {
|
|
1774
|
+
const created = [];
|
|
1775
|
+
const errors = [];
|
|
1776
|
+
const taskIds = /* @__PURE__ */ new Map();
|
|
1777
|
+
try {
|
|
1778
|
+
execSync4("which bd", { encoding: "utf-8" });
|
|
1779
|
+
} catch {
|
|
1780
|
+
return { success: false, created: [], errors: ["bd (beads) CLI not found in PATH"] };
|
|
1781
|
+
}
|
|
1782
|
+
for (const task of tasks) {
|
|
1783
|
+
const fullName = `${issue.identifier}: ${task.name}`;
|
|
1784
|
+
try {
|
|
1785
|
+
const escapedName = fullName.replace(/"/g, '\\"');
|
|
1786
|
+
let cmd = `bd create "${escapedName}" --type task -l "${issue.identifier},linear"`;
|
|
1787
|
+
if (task.dependsOn) {
|
|
1788
|
+
const depName = `${issue.identifier}: ${task.dependsOn}`;
|
|
1789
|
+
const depId = taskIds.get(depName);
|
|
1790
|
+
if (depId) {
|
|
1791
|
+
cmd += ` --deps "blocks:${depId}"`;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
if (task.description) {
|
|
1795
|
+
const escapedDesc = task.description.replace(/"/g, '\\"');
|
|
1796
|
+
cmd += ` -d "${escapedDesc}"`;
|
|
1797
|
+
}
|
|
1798
|
+
const result = execSync4(cmd, { encoding: "utf-8", cwd: process.cwd() });
|
|
1799
|
+
const idMatch = result.match(/bd-[a-f0-9]+/i) || result.match(/([a-f0-9-]{8,})/i);
|
|
1800
|
+
if (idMatch) {
|
|
1801
|
+
taskIds.set(fullName, idMatch[0]);
|
|
1802
|
+
}
|
|
1803
|
+
created.push(fullName);
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
const errMsg = error.stderr?.toString() || error.message;
|
|
1806
|
+
errors.push(`Failed to create "${task.name}": ${errMsg.split("\n")[0]}`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
if (created.length > 0) {
|
|
1810
|
+
try {
|
|
1811
|
+
execSync4("bd flush", { encoding: "utf-8", cwd: process.cwd() });
|
|
1812
|
+
} catch {
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
return { success: errors.length === 0, created, errors };
|
|
1816
|
+
}
|
|
1817
|
+
function copyToPRDDirectory(issue, stateContent) {
|
|
1818
|
+
const cwd = process.cwd();
|
|
1819
|
+
const prdDir = join9(cwd, "docs", "prds", "active");
|
|
1820
|
+
try {
|
|
1821
|
+
mkdirSync5(prdDir, { recursive: true });
|
|
1822
|
+
const filename = `${issue.identifier.toLowerCase()}-plan.md`;
|
|
1823
|
+
const prdPath = join9(prdDir, filename);
|
|
1824
|
+
writeFileSync5(prdPath, stateContent);
|
|
1825
|
+
return prdPath;
|
|
1826
|
+
} catch {
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1196
1829
|
}
|
|
1197
1830
|
async function planCommand(id, options = {}) {
|
|
1198
1831
|
const spinner = ora6(`Creating execution plan for ${id}...`).start();
|
|
@@ -1201,7 +1834,7 @@ async function planCommand(id, options = {}) {
|
|
|
1201
1834
|
if (!apiKey) {
|
|
1202
1835
|
spinner.fail("LINEAR_API_KEY not found");
|
|
1203
1836
|
console.log("");
|
|
1204
|
-
console.log(
|
|
1837
|
+
console.log(chalk12.dim("Set it in ~/.panopticon.env:"));
|
|
1205
1838
|
console.log(" LINEAR_API_KEY=lin_api_xxxxx");
|
|
1206
1839
|
process.exit(1);
|
|
1207
1840
|
}
|
|
@@ -1215,9 +1848,7 @@ async function planCommand(id, options = {}) {
|
|
|
1215
1848
|
spinner.fail("No Linear team found");
|
|
1216
1849
|
process.exit(1);
|
|
1217
1850
|
}
|
|
1218
|
-
const searchResult = await team.issues({
|
|
1219
|
-
first: 100
|
|
1220
|
-
});
|
|
1851
|
+
const searchResult = await team.issues({ first: 100 });
|
|
1221
1852
|
const issue = searchResult.nodes.find(
|
|
1222
1853
|
(i) => i.identifier.toUpperCase() === id.toUpperCase()
|
|
1223
1854
|
);
|
|
@@ -1243,161 +1874,271 @@ async function planCommand(id, options = {}) {
|
|
|
1243
1874
|
};
|
|
1244
1875
|
spinner.text = "Searching for related PRDs...";
|
|
1245
1876
|
const prdFiles = findPRDFiles(id);
|
|
1246
|
-
spinner.text = "
|
|
1247
|
-
const
|
|
1877
|
+
spinner.text = "Analyzing complexity...";
|
|
1878
|
+
const complexity = analyzeComplexity(issueData, prdFiles);
|
|
1879
|
+
spinner.stop();
|
|
1880
|
+
console.log("");
|
|
1881
|
+
console.log(chalk12.bold("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
1882
|
+
console.log(chalk12.bold(` ${issueData.identifier}: ${issueData.title}`));
|
|
1883
|
+
console.log(chalk12.bold("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
1884
|
+
console.log("");
|
|
1885
|
+
console.log(chalk12.bold("Complexity Analysis:"));
|
|
1886
|
+
console.log(` Level: ${complexity.isComplex ? chalk12.yellow("COMPLEX") : chalk12.green("SIMPLE")}`);
|
|
1887
|
+
console.log(` Estimated tasks: ${complexity.estimatedTasks}`);
|
|
1888
|
+
if (complexity.subsystems.length > 0) {
|
|
1889
|
+
console.log(` Subsystems: ${complexity.subsystems.join(", ")}`);
|
|
1890
|
+
}
|
|
1891
|
+
if (complexity.reasons.length > 0) {
|
|
1892
|
+
console.log(` Reasons:`);
|
|
1893
|
+
for (const reason of complexity.reasons) {
|
|
1894
|
+
console.log(` - ${reason}`);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
console.log("");
|
|
1898
|
+
if (prdFiles.length > 0) {
|
|
1899
|
+
console.log(chalk12.bold("Related PRDs found:"));
|
|
1900
|
+
for (const prd of prdFiles) {
|
|
1901
|
+
console.log(` - ${prd.replace(process.cwd() + "/", "")}`);
|
|
1902
|
+
}
|
|
1903
|
+
console.log("");
|
|
1904
|
+
}
|
|
1905
|
+
if (!complexity.isComplex && !options.force) {
|
|
1906
|
+
const skipAnswer = await inquirer2.prompt([{
|
|
1907
|
+
type: "confirm",
|
|
1908
|
+
name: "skip",
|
|
1909
|
+
message: "This looks simple. Skip planning and go straight to /work-issue?",
|
|
1910
|
+
default: true
|
|
1911
|
+
}]);
|
|
1912
|
+
if (skipAnswer.skip) {
|
|
1913
|
+
console.log("");
|
|
1914
|
+
console.log(chalk12.cyan(`Run: pan work issue ${id}`));
|
|
1915
|
+
console.log("");
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
let tasks;
|
|
1920
|
+
let decisions;
|
|
1921
|
+
if (options.skipDiscovery) {
|
|
1922
|
+
tasks = [
|
|
1923
|
+
{ name: "Understand requirements", description: "Review issue and existing code" },
|
|
1924
|
+
{ name: "Implement changes", description: "Core implementation", dependsOn: "Understand requirements" },
|
|
1925
|
+
{ name: "Add tests", description: "Unit/integration tests", dependsOn: "Implement changes" },
|
|
1926
|
+
{ name: "Verify and cleanup", description: "Lint, type check, final review", dependsOn: "Add tests" }
|
|
1927
|
+
];
|
|
1928
|
+
decisions = [];
|
|
1929
|
+
} else {
|
|
1930
|
+
const discovery = await runDiscoveryPhase(issueData, complexity);
|
|
1931
|
+
tasks = discovery.tasks;
|
|
1932
|
+
decisions = discovery.decisions;
|
|
1933
|
+
}
|
|
1934
|
+
const spinnerCreate = ora6("Creating context files...").start();
|
|
1935
|
+
const stateContent = generateStateFile(issueData, decisions, tasks);
|
|
1936
|
+
const workspaceContent = generateWorkspaceFile(issueData, prdFiles);
|
|
1937
|
+
const outputDir = options.output ? dirname3(options.output) : process.cwd();
|
|
1938
|
+
const planningDir = join9(outputDir, ".planning");
|
|
1939
|
+
mkdirSync5(planningDir, { recursive: true });
|
|
1940
|
+
const statePath = join9(planningDir, "STATE.md");
|
|
1941
|
+
writeFileSync5(statePath, stateContent);
|
|
1942
|
+
const workspacePath = join9(planningDir, "WORKSPACE.md");
|
|
1943
|
+
writeFileSync5(workspacePath, workspaceContent);
|
|
1944
|
+
spinnerCreate.succeed("Context files created");
|
|
1945
|
+
const spinnerBeads = ora6("Creating Beads tasks...").start();
|
|
1946
|
+
const beadsResult = createBeadsTasks(issueData, tasks);
|
|
1947
|
+
if (beadsResult.success) {
|
|
1948
|
+
spinnerBeads.succeed(`Created ${beadsResult.created.length} Beads tasks`);
|
|
1949
|
+
} else {
|
|
1950
|
+
spinnerBeads.warn(`Created ${beadsResult.created.length} tasks with errors`);
|
|
1951
|
+
for (const error of beadsResult.errors) {
|
|
1952
|
+
console.log(chalk12.red(` - ${error}`));
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
const prdPath = copyToPRDDirectory(issueData, stateContent);
|
|
1956
|
+
if (prdPath) {
|
|
1957
|
+
console.log(chalk12.dim(`Plan copied to: ${prdPath.replace(process.cwd() + "/", "")}`));
|
|
1958
|
+
}
|
|
1248
1959
|
if (options.json) {
|
|
1249
|
-
spinner.stop();
|
|
1250
1960
|
console.log(JSON.stringify({
|
|
1251
1961
|
issue: issueData,
|
|
1252
|
-
|
|
1253
|
-
|
|
1962
|
+
complexity,
|
|
1963
|
+
tasks,
|
|
1964
|
+
decisions,
|
|
1965
|
+
files: {
|
|
1966
|
+
state: statePath,
|
|
1967
|
+
workspace: workspacePath,
|
|
1968
|
+
prd: prdPath
|
|
1969
|
+
},
|
|
1970
|
+
beads: beadsResult
|
|
1254
1971
|
}, null, 2));
|
|
1255
1972
|
return;
|
|
1256
1973
|
}
|
|
1257
|
-
const outputPath = options.output || `PLAN-${issue.identifier}.md`;
|
|
1258
|
-
writeFileSync5(outputPath, plan);
|
|
1259
|
-
spinner.succeed(`Execution plan created: ${outputPath}`);
|
|
1260
1974
|
console.log("");
|
|
1261
|
-
console.log(
|
|
1262
|
-
console.log(
|
|
1263
|
-
console.log(
|
|
1264
|
-
if (prdFiles.length > 0) {
|
|
1265
|
-
console.log(` PRDs found: ${chalk11.green(prdFiles.length)}`);
|
|
1266
|
-
}
|
|
1975
|
+
console.log(chalk12.bold.green("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
1976
|
+
console.log(chalk12.bold.green(" PLAN COMPLETE"));
|
|
1977
|
+
console.log(chalk12.bold.green("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
1267
1978
|
console.log("");
|
|
1268
|
-
console.log(
|
|
1269
|
-
console.log(`
|
|
1270
|
-
console.log(`
|
|
1979
|
+
console.log(chalk12.bold("Files created:"));
|
|
1980
|
+
console.log(` ${chalk12.cyan(statePath.replace(process.cwd() + "/", ""))}`);
|
|
1981
|
+
console.log(` ${chalk12.cyan(workspacePath.replace(process.cwd() + "/", ""))}`);
|
|
1271
1982
|
console.log("");
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1983
|
+
console.log(chalk12.bold("Beads tasks:"));
|
|
1984
|
+
for (const task of tasks) {
|
|
1985
|
+
console.log(` ${chalk12.dim("\u25CB")} ${issueData.identifier}: ${task.name}`);
|
|
1986
|
+
}
|
|
1987
|
+
console.log("");
|
|
1988
|
+
if (decisions.length > 0) {
|
|
1989
|
+
console.log(chalk12.bold("Decisions recorded:"));
|
|
1990
|
+
for (const decision of decisions) {
|
|
1991
|
+
console.log(` - ${decision.question}: ${chalk12.dim(decision.answer.slice(0, 50))}${decision.answer.length > 50 ? "..." : ""}`);
|
|
1276
1992
|
}
|
|
1277
1993
|
console.log("");
|
|
1278
1994
|
}
|
|
1995
|
+
console.log(chalk12.bold("Next steps:"));
|
|
1996
|
+
console.log(` 1. Review ${chalk12.cyan(".planning/STATE.md")}`);
|
|
1997
|
+
console.log(` 2. Run ${chalk12.cyan(`pan work issue ${id}`)} to spawn agent`);
|
|
1998
|
+
console.log(` 3. Agent will use ${chalk12.cyan("bd ready")} to get tasks`);
|
|
1999
|
+
console.log("");
|
|
1279
2000
|
} catch (error) {
|
|
1280
|
-
|
|
2001
|
+
console.error(chalk12.red(`Error: ${error.message}`));
|
|
1281
2002
|
process.exit(1);
|
|
1282
2003
|
}
|
|
1283
2004
|
}
|
|
1284
2005
|
|
|
1285
2006
|
// src/cli/commands/work/list.ts
|
|
1286
|
-
import
|
|
2007
|
+
import chalk13 from "chalk";
|
|
1287
2008
|
import ora7 from "ora";
|
|
1288
|
-
import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
|
|
1289
|
-
import { join as join8 } from "path";
|
|
1290
|
-
import { homedir as homedir3 } from "os";
|
|
1291
|
-
function getLinearApiKey3() {
|
|
1292
|
-
const envFile = join8(homedir3(), ".panopticon.env");
|
|
1293
|
-
if (existsSync9(envFile)) {
|
|
1294
|
-
const content = readFileSync8(envFile, "utf-8");
|
|
1295
|
-
const match = content.match(/LINEAR_API_KEY=(.+)/);
|
|
1296
|
-
if (match) return match[1].trim();
|
|
1297
|
-
}
|
|
1298
|
-
return process.env.LINEAR_API_KEY || null;
|
|
1299
|
-
}
|
|
1300
2009
|
var PRIORITY_LABELS = {
|
|
1301
|
-
0:
|
|
1302
|
-
1:
|
|
1303
|
-
2:
|
|
1304
|
-
3:
|
|
1305
|
-
4:
|
|
2010
|
+
0: chalk13.dim("None"),
|
|
2011
|
+
1: chalk13.red("Urgent"),
|
|
2012
|
+
2: chalk13.yellow("High"),
|
|
2013
|
+
3: chalk13.blue("Medium"),
|
|
2014
|
+
4: chalk13.dim("Low")
|
|
1306
2015
|
};
|
|
1307
2016
|
var STATE_COLORS = {
|
|
1308
|
-
"
|
|
1309
|
-
"
|
|
1310
|
-
"
|
|
1311
|
-
"In Review": chalk12.magenta,
|
|
1312
|
-
"Done": chalk12.green,
|
|
1313
|
-
"Canceled": chalk12.strikethrough
|
|
2017
|
+
"open": chalk13.white,
|
|
2018
|
+
"in_progress": chalk13.yellow,
|
|
2019
|
+
"closed": chalk13.green
|
|
1314
2020
|
};
|
|
2021
|
+
function getTrackerConfig(trackerType) {
|
|
2022
|
+
const config = loadConfig();
|
|
2023
|
+
const trackerConfig = config.trackers[trackerType];
|
|
2024
|
+
if (!trackerConfig) {
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
return {
|
|
2028
|
+
type: trackerType,
|
|
2029
|
+
apiKeyEnv: trackerConfig.api_key_env,
|
|
2030
|
+
team: trackerConfig.team,
|
|
2031
|
+
tokenEnv: trackerConfig.token_env,
|
|
2032
|
+
owner: trackerConfig.owner,
|
|
2033
|
+
repo: trackerConfig.repo,
|
|
2034
|
+
projectId: trackerConfig.project_id
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
function getConfiguredTrackers() {
|
|
2038
|
+
const config = loadConfig();
|
|
2039
|
+
const trackers = [];
|
|
2040
|
+
if (config.trackers.linear) trackers.push("linear");
|
|
2041
|
+
if (config.trackers.github) trackers.push("github");
|
|
2042
|
+
if (config.trackers.gitlab) trackers.push("gitlab");
|
|
2043
|
+
return trackers;
|
|
2044
|
+
}
|
|
2045
|
+
function displayIssues(issues, trackerName) {
|
|
2046
|
+
if (issues.length === 0) {
|
|
2047
|
+
console.log(chalk13.dim(` No issues found in ${trackerName}`));
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
const byState = {};
|
|
2051
|
+
for (const issue of issues) {
|
|
2052
|
+
if (!byState[issue.state]) byState[issue.state] = [];
|
|
2053
|
+
byState[issue.state].push(issue);
|
|
2054
|
+
}
|
|
2055
|
+
const stateOrder = ["in_progress", "open", "closed"];
|
|
2056
|
+
for (const state of stateOrder) {
|
|
2057
|
+
const stateIssues = byState[state];
|
|
2058
|
+
if (!stateIssues || stateIssues.length === 0) continue;
|
|
2059
|
+
const colorFn = STATE_COLORS[state] || chalk13.white;
|
|
2060
|
+
const displayState = state.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
2061
|
+
console.log(colorFn(` \u2500\u2500 ${displayState} (${stateIssues.length}) \u2500\u2500`));
|
|
2062
|
+
for (const issue of stateIssues) {
|
|
2063
|
+
const priorityLabel = issue.priority ? PRIORITY_LABELS[issue.priority] || "" : "";
|
|
2064
|
+
const assigneeStr = issue.assignee ? chalk13.dim(` @${issue.assignee.split(" ")[0]}`) : "";
|
|
2065
|
+
const priorityStr = issue.priority && issue.priority < 3 ? ` ${priorityLabel}` : "";
|
|
2066
|
+
console.log(` ${chalk13.cyan(issue.ref)} ${issue.title}${assigneeStr}${priorityStr}`);
|
|
2067
|
+
}
|
|
2068
|
+
console.log("");
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
1315
2071
|
async function listCommand(options) {
|
|
1316
|
-
const spinner = ora7("Fetching issues
|
|
2072
|
+
const spinner = ora7("Fetching issues...").start();
|
|
1317
2073
|
try {
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
2074
|
+
const config = loadConfig();
|
|
2075
|
+
const trackersToQuery = [];
|
|
2076
|
+
if (options.tracker) {
|
|
2077
|
+
const trackerType = options.tracker;
|
|
2078
|
+
if (!["linear", "github", "gitlab"].includes(trackerType)) {
|
|
2079
|
+
spinner.fail(`Unknown tracker: ${options.tracker}`);
|
|
2080
|
+
console.log(chalk13.dim("Valid trackers: linear, github, gitlab"));
|
|
2081
|
+
process.exit(1);
|
|
2082
|
+
}
|
|
2083
|
+
trackersToQuery.push(trackerType);
|
|
2084
|
+
} else if (options.allTrackers) {
|
|
2085
|
+
trackersToQuery.push(...getConfiguredTrackers());
|
|
2086
|
+
} else {
|
|
2087
|
+
trackersToQuery.push(config.trackers.primary);
|
|
1325
2088
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
const teams = await me.teams();
|
|
1330
|
-
const team = teams.nodes[0];
|
|
1331
|
-
if (!team) {
|
|
1332
|
-
spinner.fail("No Linear team found");
|
|
2089
|
+
if (trackersToQuery.length === 0) {
|
|
2090
|
+
spinner.fail("No trackers configured");
|
|
2091
|
+
console.log(chalk13.dim("Configure trackers in ~/.panopticon/config.toml"));
|
|
1333
2092
|
process.exit(1);
|
|
1334
2093
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
2094
|
+
const allIssues = [];
|
|
2095
|
+
for (const trackerType of trackersToQuery) {
|
|
2096
|
+
spinner.text = `Fetching from ${trackerType}...`;
|
|
2097
|
+
const trackerConfig = getTrackerConfig(trackerType);
|
|
2098
|
+
if (!trackerConfig) {
|
|
2099
|
+
console.log(chalk13.yellow(`
|
|
2100
|
+
Warning: ${trackerType} not configured, skipping`));
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
try {
|
|
2104
|
+
const tracker = createTracker(trackerConfig);
|
|
2105
|
+
const issues = await tracker.listIssues({
|
|
2106
|
+
includeClosed: options.all,
|
|
2107
|
+
assignee: options.mine ? "me" : void 0
|
|
2108
|
+
});
|
|
2109
|
+
allIssues.push({ tracker: trackerType, issues });
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
console.log(chalk13.yellow(`
|
|
2112
|
+
Warning: Failed to fetch from ${trackerType}: ${error.message}`));
|
|
2113
|
+
}
|
|
1349
2114
|
}
|
|
1350
2115
|
spinner.stop();
|
|
1351
2116
|
if (options.json) {
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
id: issue.id,
|
|
1357
|
-
identifier: issue.identifier,
|
|
1358
|
-
title: issue.title,
|
|
1359
|
-
state: state?.name,
|
|
1360
|
-
priority: issue.priority,
|
|
1361
|
-
assignee: assignee?.name,
|
|
1362
|
-
url: issue.url
|
|
1363
|
-
};
|
|
1364
|
-
}));
|
|
1365
|
-
console.log(JSON.stringify(formatted, null, 2));
|
|
2117
|
+
const output = allIssues.flatMap(
|
|
2118
|
+
({ tracker, issues }) => issues.map((issue) => ({ ...issue, source: tracker }))
|
|
2119
|
+
);
|
|
2120
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1366
2121
|
return;
|
|
1367
2122
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
2123
|
+
const totalIssues = allIssues.reduce((sum, { issues }) => sum + issues.length, 0);
|
|
2124
|
+
if (totalIssues === 0) {
|
|
2125
|
+
console.log(chalk13.dim("\nNo issues found."));
|
|
1370
2126
|
return;
|
|
1371
2127
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
2128
|
+
for (const { tracker, issues } of allIssues) {
|
|
2129
|
+
console.log(chalk13.bold(`
|
|
2130
|
+
${tracker.toUpperCase()} (${issues.length} issues)
|
|
1374
2131
|
`));
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
const stateIssues = byState[stateName];
|
|
1385
|
-
if (!stateIssues || stateIssues.length === 0) continue;
|
|
1386
|
-
const colorFn = STATE_COLORS[stateName] || chalk12.white;
|
|
1387
|
-
console.log(colorFn(`\u2500\u2500 ${stateName} (${stateIssues.length}) \u2500\u2500`));
|
|
1388
|
-
console.log("");
|
|
1389
|
-
for (const issue of stateIssues) {
|
|
1390
|
-
const assignee = await issue.assignee;
|
|
1391
|
-
const priorityLabel = PRIORITY_LABELS[issue.priority] || "";
|
|
1392
|
-
const assigneeStr = assignee ? chalk12.dim(` @${assignee.name.split(" ")[0]}`) : "";
|
|
1393
|
-
console.log(` ${chalk12.cyan(issue.identifier)} ${issue.title}${assigneeStr}`);
|
|
1394
|
-
if (issue.priority > 0 && issue.priority < 3) {
|
|
1395
|
-
console.log(` ${priorityLabel}`);
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
console.log("");
|
|
2132
|
+
displayIssues(issues, tracker);
|
|
2133
|
+
}
|
|
2134
|
+
const trackerNames = trackersToQuery.join(", ");
|
|
2135
|
+
console.log(chalk13.dim(`Showing ${totalIssues} issues from ${trackerNames}.`));
|
|
2136
|
+
if (!options.all) {
|
|
2137
|
+
console.log(chalk13.dim("Use --all to include closed issues."));
|
|
2138
|
+
}
|
|
2139
|
+
if (!options.allTrackers && trackersToQuery.length === 1) {
|
|
2140
|
+
console.log(chalk13.dim("Use --all-trackers to query all configured trackers."));
|
|
1399
2141
|
}
|
|
1400
|
-
console.log(chalk12.dim(`Showing ${issues.length} issues. Use --all to include completed.`));
|
|
1401
2142
|
} catch (error) {
|
|
1402
2143
|
spinner.fail(error.message);
|
|
1403
2144
|
process.exit(1);
|
|
@@ -1405,164 +2146,175 @@ ${team.name} Issues
|
|
|
1405
2146
|
}
|
|
1406
2147
|
|
|
1407
2148
|
// src/cli/commands/work/triage.ts
|
|
1408
|
-
import
|
|
2149
|
+
import chalk14 from "chalk";
|
|
1409
2150
|
import ora8 from "ora";
|
|
1410
|
-
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "fs";
|
|
1411
|
-
import { join as
|
|
1412
|
-
import { homedir as
|
|
1413
|
-
function
|
|
1414
|
-
const
|
|
1415
|
-
const
|
|
1416
|
-
if (
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
return config;
|
|
2151
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
|
|
2152
|
+
import { join as join10 } from "path";
|
|
2153
|
+
import { homedir as homedir3 } from "os";
|
|
2154
|
+
function getTrackerConfig2(trackerType) {
|
|
2155
|
+
const config = loadConfig();
|
|
2156
|
+
const trackerConfig = config.trackers[trackerType];
|
|
2157
|
+
if (!trackerConfig) {
|
|
2158
|
+
return null;
|
|
2159
|
+
}
|
|
2160
|
+
return {
|
|
2161
|
+
type: trackerType,
|
|
2162
|
+
apiKeyEnv: trackerConfig.api_key_env,
|
|
2163
|
+
team: trackerConfig.team,
|
|
2164
|
+
tokenEnv: trackerConfig.token_env,
|
|
2165
|
+
owner: trackerConfig.owner,
|
|
2166
|
+
repo: trackerConfig.repo,
|
|
2167
|
+
projectId: trackerConfig.project_id
|
|
2168
|
+
};
|
|
1429
2169
|
}
|
|
1430
2170
|
function getTriageStatePath() {
|
|
1431
|
-
return
|
|
2171
|
+
return join10(homedir3(), ".panopticon", "triage-state.json");
|
|
1432
2172
|
}
|
|
1433
2173
|
function loadTriageState() {
|
|
1434
2174
|
const path = getTriageStatePath();
|
|
1435
2175
|
if (existsSync10(path)) {
|
|
1436
|
-
|
|
2176
|
+
try {
|
|
2177
|
+
return JSON.parse(readFileSync9(path, "utf-8"));
|
|
2178
|
+
} catch {
|
|
2179
|
+
return { dismissed: [], created: {} };
|
|
2180
|
+
}
|
|
1437
2181
|
}
|
|
1438
2182
|
return { dismissed: [], created: {} };
|
|
1439
2183
|
}
|
|
1440
2184
|
function saveTriageState(state) {
|
|
2185
|
+
const dir = join10(homedir3(), ".panopticon");
|
|
2186
|
+
if (!existsSync10(dir)) {
|
|
2187
|
+
mkdirSync6(dir, { recursive: true });
|
|
2188
|
+
}
|
|
1441
2189
|
const path = getTriageStatePath();
|
|
1442
2190
|
writeFileSync6(path, JSON.stringify(state, null, 2));
|
|
1443
2191
|
}
|
|
1444
|
-
async function fetchGitHubIssues(token, repo) {
|
|
1445
|
-
const response = await fetch(`https://api.github.com/repos/${repo}/issues?state=open&per_page=50`, {
|
|
1446
|
-
headers: {
|
|
1447
|
-
Authorization: `Bearer ${token}`,
|
|
1448
|
-
Accept: "application/vnd.github.v3+json"
|
|
1449
|
-
}
|
|
1450
|
-
});
|
|
1451
|
-
if (!response.ok) {
|
|
1452
|
-
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
1453
|
-
}
|
|
1454
|
-
const issues = await response.json();
|
|
1455
|
-
return issues.filter((i) => !("pull_request" in i));
|
|
1456
|
-
}
|
|
1457
|
-
async function createLinearIssue(apiKey, title, description, githubUrl) {
|
|
1458
|
-
const { LinearClient } = await import("@linear/sdk");
|
|
1459
|
-
const client = new LinearClient({ apiKey });
|
|
1460
|
-
const me = await client.viewer;
|
|
1461
|
-
const teams = await me.teams();
|
|
1462
|
-
const team = teams.nodes[0];
|
|
1463
|
-
if (!team) {
|
|
1464
|
-
throw new Error("No Linear team found");
|
|
1465
|
-
}
|
|
1466
|
-
const fullDescription = `${description}
|
|
1467
|
-
|
|
1468
|
-
---
|
|
1469
|
-
|
|
1470
|
-
**From GitHub:** ${githubUrl}`;
|
|
1471
|
-
const result = await client.createIssue({
|
|
1472
|
-
teamId: team.id,
|
|
1473
|
-
title,
|
|
1474
|
-
description: fullDescription
|
|
1475
|
-
});
|
|
1476
|
-
const issue = await result.issue;
|
|
1477
|
-
return issue?.identifier || "unknown";
|
|
1478
|
-
}
|
|
1479
2192
|
async function triageCommand(id, options = {}) {
|
|
1480
2193
|
const spinner = ora8("Loading triage queue...").start();
|
|
1481
2194
|
try {
|
|
1482
|
-
const config =
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
2195
|
+
const config = loadConfig();
|
|
2196
|
+
const primaryType = config.trackers.primary;
|
|
2197
|
+
const secondaryType = config.trackers.secondary;
|
|
2198
|
+
if (!secondaryType) {
|
|
2199
|
+
spinner.info("No secondary tracker configured");
|
|
1487
2200
|
console.log("");
|
|
1488
|
-
console.log("
|
|
1489
|
-
console.log(chalk13.dim(" GITHUB_TOKEN=ghp_xxxxx"));
|
|
1490
|
-
console.log(chalk13.dim(" GITHUB_REPO=owner/repo"));
|
|
2201
|
+
console.log(chalk14.bold("Setup Instructions:"));
|
|
1491
2202
|
console.log("");
|
|
1492
|
-
console.log(
|
|
1493
|
-
console.log(
|
|
2203
|
+
console.log("Add secondary tracker to ~/.panopticon/config.toml:");
|
|
2204
|
+
console.log(chalk14.dim(`
|
|
2205
|
+
[trackers]
|
|
2206
|
+
primary = "linear"
|
|
2207
|
+
secondary = "github"
|
|
2208
|
+
|
|
2209
|
+
[trackers.github]
|
|
2210
|
+
type = "github"
|
|
2211
|
+
token_env = "GITHUB_TOKEN"
|
|
2212
|
+
owner = "your-org"
|
|
2213
|
+
repo = "your-repo"
|
|
2214
|
+
`));
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
const primaryConfig = getTrackerConfig2(primaryType);
|
|
2218
|
+
const secondaryConfig = getTrackerConfig2(secondaryType);
|
|
2219
|
+
if (!primaryConfig) {
|
|
2220
|
+
spinner.fail(`Primary tracker (${primaryType}) not configured`);
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
if (!secondaryConfig) {
|
|
2224
|
+
spinner.fail(`Secondary tracker (${secondaryType}) not configured`);
|
|
1494
2225
|
return;
|
|
1495
2226
|
}
|
|
1496
2227
|
const triageState = loadTriageState();
|
|
1497
2228
|
if (id) {
|
|
1498
|
-
const
|
|
2229
|
+
const issueRef = id.startsWith("#") ? id : `#${id}`;
|
|
1499
2230
|
if (options.dismiss) {
|
|
1500
|
-
if (!triageState.dismissed.includes(
|
|
1501
|
-
triageState.dismissed.push(
|
|
2231
|
+
if (!triageState.dismissed.includes(issueRef)) {
|
|
2232
|
+
triageState.dismissed.push(issueRef);
|
|
1502
2233
|
saveTriageState(triageState);
|
|
1503
2234
|
}
|
|
1504
|
-
spinner.succeed(`Dismissed
|
|
2235
|
+
spinner.succeed(`Dismissed ${issueRef}: ${options.dismiss}`);
|
|
1505
2236
|
return;
|
|
1506
2237
|
}
|
|
1507
2238
|
if (options.create) {
|
|
1508
|
-
|
|
1509
|
-
|
|
2239
|
+
spinner.text = `Fetching ${secondaryType} issue ${issueRef}...`;
|
|
2240
|
+
let secondaryTracker2;
|
|
2241
|
+
try {
|
|
2242
|
+
secondaryTracker2 = createTracker(secondaryConfig);
|
|
2243
|
+
} catch (error) {
|
|
2244
|
+
spinner.fail(`Failed to connect to ${secondaryType}: ${error.message}`);
|
|
1510
2245
|
return;
|
|
1511
2246
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
Authorization: `Bearer ${config.githubToken}`,
|
|
1518
|
-
Accept: "application/vnd.github.v3+json"
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
);
|
|
1522
|
-
if (!response.ok) {
|
|
1523
|
-
spinner.fail(`GitHub issue #${issueNumber} not found`);
|
|
2247
|
+
let sourceIssue;
|
|
2248
|
+
try {
|
|
2249
|
+
sourceIssue = await secondaryTracker2.getIssue(id);
|
|
2250
|
+
} catch (error) {
|
|
2251
|
+
spinner.fail(`Issue ${issueRef} not found in ${secondaryType}`);
|
|
1524
2252
|
return;
|
|
1525
2253
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
2254
|
+
spinner.text = `Creating ${primaryType} issue...`;
|
|
2255
|
+
let primaryTracker;
|
|
2256
|
+
try {
|
|
2257
|
+
primaryTracker = createTracker(primaryConfig);
|
|
2258
|
+
} catch (error) {
|
|
2259
|
+
spinner.fail(`Failed to connect to ${primaryType}: ${error.message}`);
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
const newIssue = await primaryTracker.createIssue({
|
|
2263
|
+
title: sourceIssue.title,
|
|
2264
|
+
description: `${sourceIssue.description}
|
|
2265
|
+
|
|
2266
|
+
---
|
|
2267
|
+
|
|
2268
|
+
**From ${secondaryType}:** ${sourceIssue.url}`,
|
|
2269
|
+
team: primaryConfig.team
|
|
2270
|
+
});
|
|
2271
|
+
try {
|
|
2272
|
+
await secondaryTracker2.addComment(
|
|
2273
|
+
id,
|
|
2274
|
+
`Internal tracking: ${newIssue.ref} (${primaryType})`
|
|
2275
|
+
);
|
|
2276
|
+
} catch {
|
|
2277
|
+
console.log(chalk14.yellow("\nNote: Could not add link comment to source issue"));
|
|
2278
|
+
}
|
|
2279
|
+
triageState.created[sourceIssue.ref] = newIssue.ref;
|
|
1535
2280
|
saveTriageState(triageState);
|
|
1536
|
-
spinner.succeed(`Created ${
|
|
2281
|
+
spinner.succeed(`Created ${newIssue.ref} from ${sourceIssue.ref}`);
|
|
1537
2282
|
console.log("");
|
|
1538
|
-
console.log(`
|
|
1539
|
-
console.log(`
|
|
2283
|
+
console.log(` ${secondaryType}: ${chalk14.dim(sourceIssue.url)}`);
|
|
2284
|
+
console.log(` ${primaryType}: ${chalk14.cyan(newIssue.ref)}`);
|
|
1540
2285
|
return;
|
|
1541
2286
|
}
|
|
1542
2287
|
}
|
|
1543
|
-
spinner.text =
|
|
1544
|
-
|
|
2288
|
+
spinner.text = `Fetching ${secondaryType} issues...`;
|
|
2289
|
+
let secondaryTracker;
|
|
2290
|
+
try {
|
|
2291
|
+
secondaryTracker = createTracker(secondaryConfig);
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
spinner.fail(`Failed to connect to ${secondaryType}: ${error.message}`);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
const issues = await secondaryTracker.listIssues({ includeClosed: false });
|
|
1545
2297
|
const pending = issues.filter(
|
|
1546
|
-
(i) => !triageState.dismissed.includes(i.
|
|
2298
|
+
(i) => !triageState.dismissed.includes(i.ref) && !triageState.created[i.ref]
|
|
1547
2299
|
);
|
|
1548
2300
|
spinner.stop();
|
|
1549
2301
|
if (pending.length === 0) {
|
|
1550
|
-
console.log(
|
|
1551
|
-
console.log(
|
|
2302
|
+
console.log(chalk14.green("No issues pending triage."));
|
|
2303
|
+
console.log(chalk14.dim(`${issues.length} total open, ${triageState.dismissed.length} dismissed, ${Object.keys(triageState.created).length} created`));
|
|
1552
2304
|
return;
|
|
1553
2305
|
}
|
|
1554
|
-
console.log(
|
|
1555
|
-
|
|
2306
|
+
console.log(chalk14.bold(`
|
|
2307
|
+
${secondaryType.toUpperCase()} Issues Pending Triage (${pending.length})
|
|
1556
2308
|
`));
|
|
1557
2309
|
for (const issue of pending) {
|
|
1558
|
-
const labels = issue.labels.map((l) =>
|
|
1559
|
-
console.log(` ${
|
|
1560
|
-
console.log(` ${
|
|
2310
|
+
const labels = issue.labels.map((l) => chalk14.dim(`[${l}]`)).join(" ");
|
|
2311
|
+
console.log(` ${chalk14.cyan(issue.ref)} ${issue.title} ${labels}`);
|
|
2312
|
+
console.log(` ${chalk14.dim(issue.url)}`);
|
|
1561
2313
|
}
|
|
1562
2314
|
console.log("");
|
|
1563
|
-
console.log(
|
|
1564
|
-
console.log(` ${
|
|
1565
|
-
console.log(` ${
|
|
2315
|
+
console.log(chalk14.bold("Commands:"));
|
|
2316
|
+
console.log(` ${chalk14.dim(`Create ${primaryType} issue:`)} pan work triage <id> --create`);
|
|
2317
|
+
console.log(` ${chalk14.dim("Dismiss from queue:")} pan work triage <id> --dismiss "reason"`);
|
|
1566
2318
|
console.log("");
|
|
1567
2319
|
} catch (error) {
|
|
1568
2320
|
spinner.fail(error.message);
|
|
@@ -1571,7 +2323,7 @@ GitHub Issues Pending Triage (${pending.length})
|
|
|
1571
2323
|
}
|
|
1572
2324
|
|
|
1573
2325
|
// src/cli/commands/work/hook.ts
|
|
1574
|
-
import
|
|
2326
|
+
import chalk15 from "chalk";
|
|
1575
2327
|
async function hookCommand(action, idOrMessage, options = {}) {
|
|
1576
2328
|
const agentId = process.env.PANOPTICON_AGENT_ID || "default";
|
|
1577
2329
|
switch (action) {
|
|
@@ -1582,20 +2334,20 @@ async function hookCommand(action, idOrMessage, options = {}) {
|
|
|
1582
2334
|
return;
|
|
1583
2335
|
}
|
|
1584
2336
|
if (!result.hasWork) {
|
|
1585
|
-
console.log(
|
|
2337
|
+
console.log(chalk15.green("\u2713 No pending work on hook"));
|
|
1586
2338
|
return;
|
|
1587
2339
|
}
|
|
1588
|
-
console.log(
|
|
2340
|
+
console.log(chalk15.yellow(`\u26A0 ${result.items.length} item(s) on hook`));
|
|
1589
2341
|
if (result.urgentCount > 0) {
|
|
1590
|
-
console.log(
|
|
2342
|
+
console.log(chalk15.red(` ${result.urgentCount} URGENT`));
|
|
1591
2343
|
}
|
|
1592
2344
|
console.log("");
|
|
1593
2345
|
for (const item of result.items) {
|
|
1594
2346
|
const priorityColor = {
|
|
1595
|
-
urgent:
|
|
1596
|
-
high:
|
|
1597
|
-
normal:
|
|
1598
|
-
low:
|
|
2347
|
+
urgent: chalk15.red,
|
|
2348
|
+
high: chalk15.yellow,
|
|
2349
|
+
normal: chalk15.white,
|
|
2350
|
+
low: chalk15.dim
|
|
1599
2351
|
}[item.priority];
|
|
1600
2352
|
console.log(`${priorityColor(`[${item.priority.toUpperCase()}]`)} ${item.id}`);
|
|
1601
2353
|
console.log(` Type: ${item.type}`);
|
|
@@ -1609,13 +2361,13 @@ async function hookCommand(action, idOrMessage, options = {}) {
|
|
|
1609
2361
|
}
|
|
1610
2362
|
case "push": {
|
|
1611
2363
|
if (!idOrMessage) {
|
|
1612
|
-
console.log(
|
|
2364
|
+
console.log(chalk15.red("Usage: pan work hook push <agent-id> <message>"));
|
|
1613
2365
|
process.exit(1);
|
|
1614
2366
|
}
|
|
1615
2367
|
const [targetAgent, ...messageParts] = idOrMessage.split(" ");
|
|
1616
2368
|
const message = messageParts.join(" ");
|
|
1617
2369
|
if (!message) {
|
|
1618
|
-
console.log(
|
|
2370
|
+
console.log(chalk15.red("Message required"));
|
|
1619
2371
|
process.exit(1);
|
|
1620
2372
|
}
|
|
1621
2373
|
const item = pushToHook(targetAgent.startsWith("agent-") ? targetAgent : `agent-${targetAgent}`, {
|
|
@@ -1624,36 +2376,36 @@ async function hookCommand(action, idOrMessage, options = {}) {
|
|
|
1624
2376
|
source: "cli",
|
|
1625
2377
|
payload: { message }
|
|
1626
2378
|
});
|
|
1627
|
-
console.log(
|
|
2379
|
+
console.log(chalk15.green(`\u2713 Pushed to hook: ${item.id}`));
|
|
1628
2380
|
break;
|
|
1629
2381
|
}
|
|
1630
2382
|
case "pop": {
|
|
1631
2383
|
if (!idOrMessage) {
|
|
1632
|
-
console.log(
|
|
2384
|
+
console.log(chalk15.red("Usage: pan work hook pop <item-id>"));
|
|
1633
2385
|
process.exit(1);
|
|
1634
2386
|
}
|
|
1635
2387
|
const success = popFromHook(agentId, idOrMessage);
|
|
1636
2388
|
if (success) {
|
|
1637
|
-
console.log(
|
|
2389
|
+
console.log(chalk15.green(`\u2713 Popped: ${idOrMessage}`));
|
|
1638
2390
|
} else {
|
|
1639
|
-
console.log(
|
|
2391
|
+
console.log(chalk15.yellow(`Item not found: ${idOrMessage}`));
|
|
1640
2392
|
}
|
|
1641
2393
|
break;
|
|
1642
2394
|
}
|
|
1643
2395
|
case "clear": {
|
|
1644
2396
|
clearHook(idOrMessage || agentId);
|
|
1645
|
-
console.log(
|
|
2397
|
+
console.log(chalk15.green("\u2713 Hook cleared"));
|
|
1646
2398
|
break;
|
|
1647
2399
|
}
|
|
1648
2400
|
case "mail": {
|
|
1649
2401
|
if (!idOrMessage) {
|
|
1650
|
-
console.log(
|
|
2402
|
+
console.log(chalk15.red("Usage: pan work hook mail <agent-id> <message>"));
|
|
1651
2403
|
process.exit(1);
|
|
1652
2404
|
}
|
|
1653
2405
|
const [targetAgent, ...messageParts] = idOrMessage.split(" ");
|
|
1654
2406
|
const message = messageParts.join(" ");
|
|
1655
2407
|
if (!message) {
|
|
1656
|
-
console.log(
|
|
2408
|
+
console.log(chalk15.red("Message required"));
|
|
1657
2409
|
process.exit(1);
|
|
1658
2410
|
}
|
|
1659
2411
|
sendMail(
|
|
@@ -1661,32 +2413,32 @@ async function hookCommand(action, idOrMessage, options = {}) {
|
|
|
1661
2413
|
"cli",
|
|
1662
2414
|
message
|
|
1663
2415
|
);
|
|
1664
|
-
console.log(
|
|
2416
|
+
console.log(chalk15.green(`\u2713 Mail sent to ${targetAgent}`));
|
|
1665
2417
|
break;
|
|
1666
2418
|
}
|
|
1667
2419
|
case "gupp": {
|
|
1668
2420
|
const prompt = generateGUPPPrompt(idOrMessage || agentId);
|
|
1669
2421
|
if (!prompt) {
|
|
1670
|
-
console.log(
|
|
2422
|
+
console.log(chalk15.green("No GUPP work found"));
|
|
1671
2423
|
return;
|
|
1672
2424
|
}
|
|
1673
2425
|
console.log(prompt);
|
|
1674
2426
|
break;
|
|
1675
2427
|
}
|
|
1676
2428
|
default:
|
|
1677
|
-
console.log(
|
|
2429
|
+
console.log(chalk15.bold("Hook Commands:"));
|
|
1678
2430
|
console.log("");
|
|
1679
|
-
console.log(` ${
|
|
1680
|
-
console.log(` ${
|
|
1681
|
-
console.log(` ${
|
|
1682
|
-
console.log(` ${
|
|
1683
|
-
console.log(` ${
|
|
1684
|
-
console.log(` ${
|
|
2431
|
+
console.log(` ${chalk15.cyan("pan work hook check [agent-id]")} - Check for pending work`);
|
|
2432
|
+
console.log(` ${chalk15.cyan("pan work hook push <agent-id> <msg>")} - Push task to hook`);
|
|
2433
|
+
console.log(` ${chalk15.cyan("pan work hook pop <item-id>")} - Remove completed item`);
|
|
2434
|
+
console.log(` ${chalk15.cyan("pan work hook clear [agent-id]")} - Clear all hook items`);
|
|
2435
|
+
console.log(` ${chalk15.cyan("pan work hook mail <agent-id> <msg>")} - Send mail to agent`);
|
|
2436
|
+
console.log(` ${chalk15.cyan("pan work hook gupp [agent-id]")} - Generate GUPP prompt`);
|
|
1685
2437
|
}
|
|
1686
2438
|
}
|
|
1687
2439
|
|
|
1688
2440
|
// src/cli/commands/work/recover.ts
|
|
1689
|
-
import
|
|
2441
|
+
import chalk16 from "chalk";
|
|
1690
2442
|
import ora9 from "ora";
|
|
1691
2443
|
async function recoverCommand(id, options = {}) {
|
|
1692
2444
|
const spinner = ora9("Checking for crashed agents...").start();
|
|
@@ -1701,7 +2453,7 @@ async function recoverCommand(id, options = {}) {
|
|
|
1701
2453
|
spinner.stop();
|
|
1702
2454
|
console.log(JSON.stringify({ crashed: crashed.map((a) => a.id) }, null, 2));
|
|
1703
2455
|
if (!options.all) {
|
|
1704
|
-
console.log(
|
|
2456
|
+
console.log(chalk16.dim("\nUse --all to auto-recover all crashed agents"));
|
|
1705
2457
|
return;
|
|
1706
2458
|
}
|
|
1707
2459
|
}
|
|
@@ -1709,12 +2461,12 @@ async function recoverCommand(id, options = {}) {
|
|
|
1709
2461
|
spinner.info(`Found ${crashed.length} crashed agent(s)`);
|
|
1710
2462
|
console.log("");
|
|
1711
2463
|
for (const agent of crashed) {
|
|
1712
|
-
console.log(` ${
|
|
2464
|
+
console.log(` ${chalk16.red("\u25CF")} ${chalk16.cyan(agent.id)}`);
|
|
1713
2465
|
console.log(` Issue: ${agent.issueId}`);
|
|
1714
2466
|
console.log(` Started: ${agent.startedAt}`);
|
|
1715
2467
|
console.log("");
|
|
1716
2468
|
}
|
|
1717
|
-
console.log(
|
|
2469
|
+
console.log(chalk16.dim("Use --all to auto-recover, or specify an agent ID"));
|
|
1718
2470
|
return;
|
|
1719
2471
|
}
|
|
1720
2472
|
spinner.text = "Auto-recovering agents...";
|
|
@@ -1725,15 +2477,15 @@ async function recoverCommand(id, options = {}) {
|
|
|
1725
2477
|
return;
|
|
1726
2478
|
}
|
|
1727
2479
|
if (result.recovered.length > 0) {
|
|
1728
|
-
console.log(
|
|
2480
|
+
console.log(chalk16.green(`\u2713 Recovered ${result.recovered.length} agent(s):`));
|
|
1729
2481
|
for (const agentId2 of result.recovered) {
|
|
1730
|
-
console.log(` ${
|
|
2482
|
+
console.log(` ${chalk16.cyan(agentId2)}`);
|
|
1731
2483
|
}
|
|
1732
2484
|
}
|
|
1733
2485
|
if (result.failed.length > 0) {
|
|
1734
|
-
console.log(
|
|
2486
|
+
console.log(chalk16.red(`\u2717 Failed to recover ${result.failed.length} agent(s):`));
|
|
1735
2487
|
for (const agentId2 of result.failed) {
|
|
1736
|
-
console.log(` ${
|
|
2488
|
+
console.log(` ${chalk16.dim(agentId2)}`);
|
|
1737
2489
|
}
|
|
1738
2490
|
}
|
|
1739
2491
|
return;
|
|
@@ -1747,12 +2499,12 @@ async function recoverCommand(id, options = {}) {
|
|
|
1747
2499
|
}
|
|
1748
2500
|
spinner.succeed(`Recovered: ${agentId}`);
|
|
1749
2501
|
console.log("");
|
|
1750
|
-
console.log(
|
|
1751
|
-
console.log(` Issue: ${
|
|
1752
|
-
console.log(` Workspace: ${
|
|
2502
|
+
console.log(chalk16.bold("Agent Details:"));
|
|
2503
|
+
console.log(` Issue: ${chalk16.cyan(state.issueId)}`);
|
|
2504
|
+
console.log(` Workspace: ${chalk16.dim(state.workspace)}`);
|
|
1753
2505
|
console.log(` Model: ${state.model}`);
|
|
1754
2506
|
console.log("");
|
|
1755
|
-
console.log(
|
|
2507
|
+
console.log(chalk16.dim("Commands:"));
|
|
1756
2508
|
console.log(` Attach: tmux attach -t ${state.id}`);
|
|
1757
2509
|
console.log(` Message: pan work tell ${state.issueId} "your message"`);
|
|
1758
2510
|
} catch (error) {
|
|
@@ -1762,7 +2514,7 @@ async function recoverCommand(id, options = {}) {
|
|
|
1762
2514
|
}
|
|
1763
2515
|
|
|
1764
2516
|
// src/cli/commands/work/cv.ts
|
|
1765
|
-
import
|
|
2517
|
+
import chalk17 from "chalk";
|
|
1766
2518
|
async function cvCommand(agentId, options = {}) {
|
|
1767
2519
|
if (options.rankings || !agentId) {
|
|
1768
2520
|
const rankings = getAgentRankings();
|
|
@@ -1771,15 +2523,15 @@ async function cvCommand(agentId, options = {}) {
|
|
|
1771
2523
|
return;
|
|
1772
2524
|
}
|
|
1773
2525
|
if (rankings.length === 0) {
|
|
1774
|
-
console.log(
|
|
1775
|
-
console.log(
|
|
2526
|
+
console.log(chalk17.dim("No agent work history yet."));
|
|
2527
|
+
console.log(chalk17.dim("CVs are created as agents complete work."));
|
|
1776
2528
|
return;
|
|
1777
2529
|
}
|
|
1778
|
-
console.log(
|
|
2530
|
+
console.log(chalk17.bold("\nAgent Rankings\n"));
|
|
1779
2531
|
console.log(
|
|
1780
2532
|
`${"Agent".padEnd(25)} ${"Success".padStart(8)} ${"Total".padStart(6)} ${"Avg Time".padStart(10)}`
|
|
1781
2533
|
);
|
|
1782
|
-
console.log(
|
|
2534
|
+
console.log(chalk17.dim("\u2500".repeat(52)));
|
|
1783
2535
|
for (let i = 0; i < rankings.length; i++) {
|
|
1784
2536
|
const r = rankings[i];
|
|
1785
2537
|
const medal = i === 0 ? "\u{1F947}" : i === 1 ? "\u{1F948}" : i === 2 ? "\u{1F949}" : " ";
|
|
@@ -1790,7 +2542,7 @@ async function cvCommand(agentId, options = {}) {
|
|
|
1790
2542
|
);
|
|
1791
2543
|
}
|
|
1792
2544
|
console.log("");
|
|
1793
|
-
console.log(
|
|
2545
|
+
console.log(chalk17.dim(`Use: pan work cv <agent-id> for details`));
|
|
1794
2546
|
return;
|
|
1795
2547
|
}
|
|
1796
2548
|
const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
|
|
@@ -1804,13 +2556,13 @@ async function cvCommand(agentId, options = {}) {
|
|
|
1804
2556
|
}
|
|
1805
2557
|
|
|
1806
2558
|
// src/cli/commands/work/context.ts
|
|
1807
|
-
import
|
|
2559
|
+
import chalk18 from "chalk";
|
|
1808
2560
|
|
|
1809
2561
|
// src/lib/context.ts
|
|
1810
|
-
import { existsSync as existsSync11, mkdirSync as
|
|
1811
|
-
import { join as
|
|
2562
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync7, appendFileSync, readdirSync as readdirSync8 } from "fs";
|
|
2563
|
+
import { join as join11 } from "path";
|
|
1812
2564
|
function getStateFile(agentId) {
|
|
1813
|
-
return
|
|
2565
|
+
return join11(AGENTS_DIR, agentId, "STATE.md");
|
|
1814
2566
|
}
|
|
1815
2567
|
function readAgentState(agentId) {
|
|
1816
2568
|
const stateFile = getStateFile(agentId);
|
|
@@ -1823,8 +2575,8 @@ function readAgentState(agentId) {
|
|
|
1823
2575
|
}
|
|
1824
2576
|
}
|
|
1825
2577
|
function writeAgentState(agentId, state) {
|
|
1826
|
-
const dir =
|
|
1827
|
-
|
|
2578
|
+
const dir = join11(AGENTS_DIR, agentId);
|
|
2579
|
+
mkdirSync7(dir, { recursive: true });
|
|
1828
2580
|
const content = generateStateMd(state);
|
|
1829
2581
|
writeFileSync7(getStateFile(agentId), content);
|
|
1830
2582
|
}
|
|
@@ -1900,11 +2652,11 @@ function parseStateMd(content) {
|
|
|
1900
2652
|
return state;
|
|
1901
2653
|
}
|
|
1902
2654
|
function getSummaryFile(agentId) {
|
|
1903
|
-
return
|
|
2655
|
+
return join11(AGENTS_DIR, agentId, "SUMMARY.md");
|
|
1904
2656
|
}
|
|
1905
2657
|
function appendSummary(agentId, summary) {
|
|
1906
|
-
const dir =
|
|
1907
|
-
|
|
2658
|
+
const dir = join11(AGENTS_DIR, agentId);
|
|
2659
|
+
mkdirSync7(dir, { recursive: true });
|
|
1908
2660
|
const summaryFile = getSummaryFile(agentId);
|
|
1909
2661
|
const content = generateSummaryEntry(summary);
|
|
1910
2662
|
if (existsSync11(summaryFile)) {
|
|
@@ -1948,14 +2700,14 @@ function generateSummaryEntry(summary) {
|
|
|
1948
2700
|
return lines.join("\n");
|
|
1949
2701
|
}
|
|
1950
2702
|
function getHistoryDir(agentId) {
|
|
1951
|
-
return
|
|
2703
|
+
return join11(AGENTS_DIR, agentId, "history");
|
|
1952
2704
|
}
|
|
1953
2705
|
function logHistory(agentId, action, details) {
|
|
1954
2706
|
const historyDir = getHistoryDir(agentId);
|
|
1955
|
-
|
|
2707
|
+
mkdirSync7(historyDir, { recursive: true });
|
|
1956
2708
|
const date = /* @__PURE__ */ new Date();
|
|
1957
2709
|
const dateStr = date.toISOString().split("T")[0];
|
|
1958
|
-
const historyFile =
|
|
2710
|
+
const historyFile = join11(historyDir, `${dateStr}.log`);
|
|
1959
2711
|
const timestamp = date.toISOString();
|
|
1960
2712
|
const detailsStr = details ? ` ${JSON.stringify(details)}` : "";
|
|
1961
2713
|
const logLine = `[${timestamp}] ${action}${detailsStr}
|
|
@@ -1967,10 +2719,10 @@ function searchHistory(agentId, pattern) {
|
|
|
1967
2719
|
if (!existsSync11(historyDir)) return [];
|
|
1968
2720
|
const results = [];
|
|
1969
2721
|
const regex = new RegExp(pattern, "i");
|
|
1970
|
-
const files =
|
|
2722
|
+
const files = readdirSync8(historyDir).filter((f) => f.endsWith(".log"));
|
|
1971
2723
|
files.sort().reverse();
|
|
1972
2724
|
for (const file of files) {
|
|
1973
|
-
const content = readFileSync10(
|
|
2725
|
+
const content = readFileSync10(join11(historyDir, file), "utf-8");
|
|
1974
2726
|
const lines = content.split("\n");
|
|
1975
2727
|
for (const line of lines) {
|
|
1976
2728
|
if (regex.test(line)) {
|
|
@@ -1984,11 +2736,11 @@ function getRecentHistory(agentId, limit = 20) {
|
|
|
1984
2736
|
const historyDir = getHistoryDir(agentId);
|
|
1985
2737
|
if (!existsSync11(historyDir)) return [];
|
|
1986
2738
|
const results = [];
|
|
1987
|
-
const files =
|
|
2739
|
+
const files = readdirSync8(historyDir).filter((f) => f.endsWith(".log"));
|
|
1988
2740
|
files.sort().reverse();
|
|
1989
2741
|
for (const file of files) {
|
|
1990
2742
|
if (results.length >= limit) break;
|
|
1991
|
-
const content = readFileSync10(
|
|
2743
|
+
const content = readFileSync10(join11(historyDir, file), "utf-8");
|
|
1992
2744
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
1993
2745
|
for (const line of lines.reverse()) {
|
|
1994
2746
|
if (results.length >= limit) break;
|
|
@@ -2001,18 +2753,18 @@ function estimateTokens(text) {
|
|
|
2001
2753
|
return Math.ceil(text.length / 4);
|
|
2002
2754
|
}
|
|
2003
2755
|
function getMaterializedDir(agentId) {
|
|
2004
|
-
return
|
|
2756
|
+
return join11(AGENTS_DIR, agentId, "materialized");
|
|
2005
2757
|
}
|
|
2006
2758
|
function listMaterialized(agentId) {
|
|
2007
2759
|
const dir = getMaterializedDir(agentId);
|
|
2008
2760
|
if (!existsSync11(dir)) return [];
|
|
2009
|
-
return
|
|
2761
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
2010
2762
|
const match = f.match(/^(.+)-(\d+)\.md$/);
|
|
2011
2763
|
if (!match) return null;
|
|
2012
2764
|
return {
|
|
2013
2765
|
tool: match[1],
|
|
2014
2766
|
timestamp: parseInt(match[2], 10),
|
|
2015
|
-
file:
|
|
2767
|
+
file: join11(dir, f)
|
|
2016
2768
|
};
|
|
2017
2769
|
}).filter(Boolean);
|
|
2018
2770
|
}
|
|
@@ -2033,34 +2785,34 @@ async function contextCommand(action, arg1, arg2, options = {}) {
|
|
|
2033
2785
|
return;
|
|
2034
2786
|
}
|
|
2035
2787
|
if (!state) {
|
|
2036
|
-
console.log(
|
|
2037
|
-
console.log(
|
|
2788
|
+
console.log(chalk18.dim("No state found for agent."));
|
|
2789
|
+
console.log(chalk18.dim("Initialize with: pan work context init <agent-id> <issue-id>"));
|
|
2038
2790
|
return;
|
|
2039
2791
|
}
|
|
2040
|
-
console.log(
|
|
2792
|
+
console.log(chalk18.bold(`
|
|
2041
2793
|
Agent State: ${state.issueId}
|
|
2042
2794
|
`));
|
|
2043
|
-
console.log(`Status: ${
|
|
2044
|
-
console.log(`Last Activity: ${
|
|
2795
|
+
console.log(`Status: ${chalk18.cyan(state.status)}`);
|
|
2796
|
+
console.log(`Last Activity: ${chalk18.dim(state.lastActivity)}`);
|
|
2045
2797
|
if (state.lastCheckpoint) {
|
|
2046
2798
|
console.log("");
|
|
2047
|
-
console.log(
|
|
2048
|
-
console.log(` Checkpoint: ${
|
|
2799
|
+
console.log(chalk18.bold("Session Continuity:"));
|
|
2800
|
+
console.log(` Checkpoint: ${chalk18.yellow(state.lastCheckpoint)}`);
|
|
2049
2801
|
if (state.resumePoint) {
|
|
2050
|
-
console.log(` Resume: ${
|
|
2802
|
+
console.log(` Resume: ${chalk18.green(state.resumePoint)}`);
|
|
2051
2803
|
}
|
|
2052
2804
|
}
|
|
2053
2805
|
if (state.contextRefs.workspace || state.contextRefs.prd) {
|
|
2054
2806
|
console.log("");
|
|
2055
|
-
console.log(
|
|
2807
|
+
console.log(chalk18.bold("Context References:"));
|
|
2056
2808
|
if (state.contextRefs.workspace) {
|
|
2057
|
-
console.log(` Workspace: ${
|
|
2809
|
+
console.log(` Workspace: ${chalk18.dim(state.contextRefs.workspace)}`);
|
|
2058
2810
|
}
|
|
2059
2811
|
if (state.contextRefs.prd) {
|
|
2060
|
-
console.log(` PRD: ${
|
|
2812
|
+
console.log(` PRD: ${chalk18.dim(state.contextRefs.prd)}`);
|
|
2061
2813
|
}
|
|
2062
2814
|
if (state.contextRefs.beads) {
|
|
2063
|
-
console.log(` Beads: ${
|
|
2815
|
+
console.log(` Beads: ${chalk18.dim(state.contextRefs.beads)}`);
|
|
2064
2816
|
}
|
|
2065
2817
|
}
|
|
2066
2818
|
console.log("");
|
|
@@ -2077,22 +2829,22 @@ Agent State: ${state.issueId}
|
|
|
2077
2829
|
};
|
|
2078
2830
|
writeAgentState(targetAgent, state);
|
|
2079
2831
|
logHistory(targetAgent, "context:init", { issueId });
|
|
2080
|
-
console.log(
|
|
2832
|
+
console.log(chalk18.green(`\u2713 Initialized state for ${targetAgent}`));
|
|
2081
2833
|
break;
|
|
2082
2834
|
}
|
|
2083
2835
|
case "checkpoint": {
|
|
2084
2836
|
const checkpoint = arg1;
|
|
2085
2837
|
const resume = arg2;
|
|
2086
2838
|
if (!checkpoint) {
|
|
2087
|
-
console.log(
|
|
2088
|
-
console.log(
|
|
2839
|
+
console.log(chalk18.red("Checkpoint message required"));
|
|
2840
|
+
console.log(chalk18.dim('Usage: pan work context checkpoint "message" ["resume point"]'));
|
|
2089
2841
|
return;
|
|
2090
2842
|
}
|
|
2091
2843
|
updateCheckpoint(agentId, checkpoint, resume);
|
|
2092
2844
|
logHistory(agentId, "context:checkpoint", { checkpoint, resume });
|
|
2093
|
-
console.log(
|
|
2845
|
+
console.log(chalk18.green(`\u2713 Checkpoint saved: "${checkpoint}"`));
|
|
2094
2846
|
if (resume) {
|
|
2095
|
-
console.log(
|
|
2847
|
+
console.log(chalk18.dim(` Resume point: "${resume}"`));
|
|
2096
2848
|
}
|
|
2097
2849
|
break;
|
|
2098
2850
|
}
|
|
@@ -2105,7 +2857,7 @@ Agent State: ${state.issueId}
|
|
|
2105
2857
|
};
|
|
2106
2858
|
appendSummary(agentId, summary);
|
|
2107
2859
|
logHistory(agentId, "context:summary", { title });
|
|
2108
|
-
console.log(
|
|
2860
|
+
console.log(chalk18.green(`\u2713 Summary added: "${title}"`));
|
|
2109
2861
|
break;
|
|
2110
2862
|
}
|
|
2111
2863
|
case "history": {
|
|
@@ -2113,10 +2865,10 @@ Agent State: ${state.issueId}
|
|
|
2113
2865
|
if (pattern) {
|
|
2114
2866
|
const results = searchHistory(agentId, pattern);
|
|
2115
2867
|
if (results.length === 0) {
|
|
2116
|
-
console.log(
|
|
2868
|
+
console.log(chalk18.dim("No matches found."));
|
|
2117
2869
|
return;
|
|
2118
2870
|
}
|
|
2119
|
-
console.log(
|
|
2871
|
+
console.log(chalk18.bold(`
|
|
2120
2872
|
History matches for "${pattern}":
|
|
2121
2873
|
`));
|
|
2122
2874
|
for (const line of results.slice(0, 50)) {
|
|
@@ -2125,10 +2877,10 @@ History matches for "${pattern}":
|
|
|
2125
2877
|
} else {
|
|
2126
2878
|
const recent = getRecentHistory(agentId, 20);
|
|
2127
2879
|
if (recent.length === 0) {
|
|
2128
|
-
console.log(
|
|
2880
|
+
console.log(chalk18.dim("No history yet."));
|
|
2129
2881
|
return;
|
|
2130
2882
|
}
|
|
2131
|
-
console.log(
|
|
2883
|
+
console.log(chalk18.bold("\nRecent History:\n"));
|
|
2132
2884
|
for (const line of recent) {
|
|
2133
2885
|
console.log(line);
|
|
2134
2886
|
}
|
|
@@ -2147,14 +2899,14 @@ History matches for "${pattern}":
|
|
|
2147
2899
|
}
|
|
2148
2900
|
const outputs = listMaterialized(agentId);
|
|
2149
2901
|
if (outputs.length === 0) {
|
|
2150
|
-
console.log(
|
|
2902
|
+
console.log(chalk18.dim("No materialized outputs."));
|
|
2151
2903
|
return;
|
|
2152
2904
|
}
|
|
2153
|
-
console.log(
|
|
2905
|
+
console.log(chalk18.bold("\nMaterialized Outputs:\n"));
|
|
2154
2906
|
for (const out of outputs) {
|
|
2155
2907
|
const date = new Date(out.timestamp).toLocaleString();
|
|
2156
|
-
console.log(` ${
|
|
2157
|
-
console.log(` ${
|
|
2908
|
+
console.log(` ${chalk18.cyan(out.tool)} ${chalk18.dim(date)}`);
|
|
2909
|
+
console.log(` ${chalk18.dim(out.file)}`);
|
|
2158
2910
|
}
|
|
2159
2911
|
console.log("");
|
|
2160
2912
|
break;
|
|
@@ -2162,7 +2914,7 @@ History matches for "${pattern}":
|
|
|
2162
2914
|
case "tokens": {
|
|
2163
2915
|
const target = arg1;
|
|
2164
2916
|
if (!target) {
|
|
2165
|
-
console.log(
|
|
2917
|
+
console.log(chalk18.dim("Usage: pan work context tokens <file-or-text>"));
|
|
2166
2918
|
return;
|
|
2167
2919
|
}
|
|
2168
2920
|
let text = target;
|
|
@@ -2170,36 +2922,36 @@ History matches for "${pattern}":
|
|
|
2170
2922
|
text = readFileSync11(target, "utf-8");
|
|
2171
2923
|
}
|
|
2172
2924
|
const tokens = estimateTokens(text);
|
|
2173
|
-
console.log(`Estimated tokens: ${
|
|
2925
|
+
console.log(`Estimated tokens: ${chalk18.cyan(tokens.toLocaleString())}`);
|
|
2174
2926
|
break;
|
|
2175
2927
|
}
|
|
2176
2928
|
default:
|
|
2177
|
-
console.log(
|
|
2929
|
+
console.log(chalk18.bold("Context Commands:"));
|
|
2178
2930
|
console.log("");
|
|
2179
|
-
console.log(` ${
|
|
2180
|
-
console.log(` ${
|
|
2181
|
-
console.log(` ${
|
|
2182
|
-
console.log(` ${
|
|
2183
|
-
console.log(` ${
|
|
2184
|
-
console.log(` ${
|
|
2185
|
-
console.log(` ${
|
|
2931
|
+
console.log(` ${chalk18.cyan("pan work context state [agent-id]")} - Show current state`);
|
|
2932
|
+
console.log(` ${chalk18.cyan("pan work context init <agent> <issue>")} - Initialize state`);
|
|
2933
|
+
console.log(` ${chalk18.cyan('pan work context checkpoint "msg"')} - Save checkpoint`);
|
|
2934
|
+
console.log(` ${chalk18.cyan("pan work context summary [title]")} - Add work summary`);
|
|
2935
|
+
console.log(` ${chalk18.cyan("pan work context history [pattern]")} - Search history`);
|
|
2936
|
+
console.log(` ${chalk18.cyan("pan work context materialize [file]")} - List/read outputs`);
|
|
2937
|
+
console.log(` ${chalk18.cyan("pan work context tokens <file>")} - Estimate token count`);
|
|
2186
2938
|
console.log("");
|
|
2187
2939
|
}
|
|
2188
2940
|
}
|
|
2189
2941
|
|
|
2190
2942
|
// src/cli/commands/work/health.ts
|
|
2191
|
-
import
|
|
2943
|
+
import chalk19 from "chalk";
|
|
2192
2944
|
|
|
2193
2945
|
// src/lib/health.ts
|
|
2194
|
-
import { existsSync as existsSync13, mkdirSync as
|
|
2195
|
-
import { join as
|
|
2196
|
-
import { execSync as
|
|
2946
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
|
|
2947
|
+
import { join as join12 } from "path";
|
|
2948
|
+
import { execSync as execSync5 } from "child_process";
|
|
2197
2949
|
var DEFAULT_PING_TIMEOUT_MS = 30 * 1e3;
|
|
2198
2950
|
var DEFAULT_CONSECUTIVE_FAILURES = 3;
|
|
2199
2951
|
var DEFAULT_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
2200
2952
|
var DEFAULT_CHECK_INTERVAL_MS = 30 * 1e3;
|
|
2201
2953
|
function getHealthFile(agentId) {
|
|
2202
|
-
return
|
|
2954
|
+
return join12(AGENTS_DIR, agentId, "health.json");
|
|
2203
2955
|
}
|
|
2204
2956
|
function getAgentHealth(agentId) {
|
|
2205
2957
|
const healthFile = getHealthFile(agentId);
|
|
@@ -2221,13 +2973,13 @@ function getAgentHealth(agentId) {
|
|
|
2221
2973
|
return defaultHealth;
|
|
2222
2974
|
}
|
|
2223
2975
|
function saveAgentHealth(health) {
|
|
2224
|
-
const dir =
|
|
2225
|
-
|
|
2976
|
+
const dir = join12(AGENTS_DIR, health.agentId);
|
|
2977
|
+
mkdirSync8(dir, { recursive: true });
|
|
2226
2978
|
writeFileSync8(getHealthFile(health.agentId), JSON.stringify(health, null, 2));
|
|
2227
2979
|
}
|
|
2228
2980
|
function isAgentAlive(agentId) {
|
|
2229
2981
|
try {
|
|
2230
|
-
|
|
2982
|
+
execSync5(`tmux has-session -t "${agentId}" 2>/dev/null`, { encoding: "utf-8" });
|
|
2231
2983
|
return true;
|
|
2232
2984
|
} catch {
|
|
2233
2985
|
return false;
|
|
@@ -2337,7 +3089,7 @@ async function runHealthCheck(config = {
|
|
|
2337
3089
|
};
|
|
2338
3090
|
let sessions = [];
|
|
2339
3091
|
try {
|
|
2340
|
-
const output =
|
|
3092
|
+
const output = execSync5(
|
|
2341
3093
|
'tmux list-sessions -F "#{session_name}" 2>/dev/null || true',
|
|
2342
3094
|
{ encoding: "utf-8" }
|
|
2343
3095
|
);
|
|
@@ -2345,8 +3097,8 @@ async function runHealthCheck(config = {
|
|
|
2345
3097
|
} catch {
|
|
2346
3098
|
}
|
|
2347
3099
|
if (existsSync13(AGENTS_DIR)) {
|
|
2348
|
-
const { readdirSync:
|
|
2349
|
-
const dirs =
|
|
3100
|
+
const { readdirSync: readdirSync13 } = await import("fs");
|
|
3101
|
+
const dirs = readdirSync13(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith("agent-")).map((d) => d.name);
|
|
2350
3102
|
for (const dir of dirs) {
|
|
2351
3103
|
if (!sessions.includes(dir)) {
|
|
2352
3104
|
sessions.push(dir);
|
|
@@ -2444,20 +3196,20 @@ async function healthCommand(action, arg, options = {}) {
|
|
|
2444
3196
|
};
|
|
2445
3197
|
switch (action) {
|
|
2446
3198
|
case "check": {
|
|
2447
|
-
console.log(
|
|
3199
|
+
console.log(chalk19.bold("Running health check...\n"));
|
|
2448
3200
|
const results = await runHealthCheck(config);
|
|
2449
3201
|
if (options.json) {
|
|
2450
3202
|
console.log(JSON.stringify(results, null, 2));
|
|
2451
3203
|
return;
|
|
2452
3204
|
}
|
|
2453
3205
|
console.log(`Checked: ${results.checked} agents`);
|
|
2454
|
-
console.log(` ${
|
|
2455
|
-
console.log(` ${
|
|
2456
|
-
console.log(` ${
|
|
2457
|
-
console.log(` ${
|
|
3206
|
+
console.log(` ${chalk19.green("\u2705 Healthy:")} ${results.healthy}`);
|
|
3207
|
+
console.log(` ${chalk19.yellow("\u26A0\uFE0F Warning:")} ${results.warning}`);
|
|
3208
|
+
console.log(` ${chalk19.hex("#FFA500")("\u{1F7E0} Stuck:")} ${results.stuck}`);
|
|
3209
|
+
console.log(` ${chalk19.red("\u274C Dead:")} ${results.dead}`);
|
|
2458
3210
|
if (results.recovered.length > 0) {
|
|
2459
3211
|
console.log("");
|
|
2460
|
-
console.log(
|
|
3212
|
+
console.log(chalk19.green("Recovered agents:"));
|
|
2461
3213
|
for (const agentId of results.recovered) {
|
|
2462
3214
|
console.log(` - ${agentId}`);
|
|
2463
3215
|
}
|
|
@@ -2467,7 +3219,7 @@ async function healthCommand(action, arg, options = {}) {
|
|
|
2467
3219
|
case "status": {
|
|
2468
3220
|
const agents = listRunningAgents();
|
|
2469
3221
|
if (agents.length === 0) {
|
|
2470
|
-
console.log(
|
|
3222
|
+
console.log(chalk19.dim("No agents found."));
|
|
2471
3223
|
return;
|
|
2472
3224
|
}
|
|
2473
3225
|
const healthData = agents.map((agent) => {
|
|
@@ -2478,7 +3230,7 @@ async function healthCommand(action, arg, options = {}) {
|
|
|
2478
3230
|
console.log(JSON.stringify(healthData.map((d) => d.health), null, 2));
|
|
2479
3231
|
return;
|
|
2480
3232
|
}
|
|
2481
|
-
console.log(
|
|
3233
|
+
console.log(chalk19.bold("Agent Health Status:\n"));
|
|
2482
3234
|
for (const { health } of healthData) {
|
|
2483
3235
|
console.log(formatHealthStatus(health));
|
|
2484
3236
|
console.log("");
|
|
@@ -2487,12 +3239,12 @@ async function healthCommand(action, arg, options = {}) {
|
|
|
2487
3239
|
}
|
|
2488
3240
|
case "ping": {
|
|
2489
3241
|
if (!arg) {
|
|
2490
|
-
console.log(
|
|
2491
|
-
console.log(
|
|
3242
|
+
console.log(chalk19.red("Agent ID required"));
|
|
3243
|
+
console.log(chalk19.dim("Usage: pan work health ping <agent-id>"));
|
|
2492
3244
|
return;
|
|
2493
3245
|
}
|
|
2494
3246
|
const agentId = arg.startsWith("agent-") ? arg : `agent-${arg.toLowerCase()}`;
|
|
2495
|
-
console.log(
|
|
3247
|
+
console.log(chalk19.dim(`Pinging ${agentId}...`));
|
|
2496
3248
|
const health = pingAgent(agentId, config);
|
|
2497
3249
|
if (options.json) {
|
|
2498
3250
|
console.log(JSON.stringify(health, null, 2));
|
|
@@ -2504,12 +3256,12 @@ async function healthCommand(action, arg, options = {}) {
|
|
|
2504
3256
|
}
|
|
2505
3257
|
case "recover": {
|
|
2506
3258
|
if (!arg) {
|
|
2507
|
-
console.log(
|
|
2508
|
-
console.log(
|
|
3259
|
+
console.log(chalk19.red("Agent ID required"));
|
|
3260
|
+
console.log(chalk19.dim("Usage: pan work health recover <agent-id>"));
|
|
2509
3261
|
return;
|
|
2510
3262
|
}
|
|
2511
3263
|
const agentId = arg.startsWith("agent-") ? arg : `agent-${arg.toLowerCase()}`;
|
|
2512
|
-
console.log(
|
|
3264
|
+
console.log(chalk19.dim(`Attempting recovery of ${agentId}...`));
|
|
2513
3265
|
const forceConfig = { ...config, consecutiveFailures: 0 };
|
|
2514
3266
|
const result = await handleStuckAgent(agentId, forceConfig);
|
|
2515
3267
|
if (options.json) {
|
|
@@ -2517,21 +3269,21 @@ async function healthCommand(action, arg, options = {}) {
|
|
|
2517
3269
|
return;
|
|
2518
3270
|
}
|
|
2519
3271
|
if (result.action === "recovered") {
|
|
2520
|
-
console.log(
|
|
3272
|
+
console.log(chalk19.green(`\u2705 ${result.reason}`));
|
|
2521
3273
|
} else if (result.action === "cooldown") {
|
|
2522
|
-
console.log(
|
|
3274
|
+
console.log(chalk19.yellow(`\u26A0\uFE0F ${result.reason}`));
|
|
2523
3275
|
} else {
|
|
2524
|
-
console.log(
|
|
3276
|
+
console.log(chalk19.dim(result.reason));
|
|
2525
3277
|
}
|
|
2526
3278
|
break;
|
|
2527
3279
|
}
|
|
2528
3280
|
case "daemon": {
|
|
2529
|
-
console.log(
|
|
2530
|
-
console.log(
|
|
2531
|
-
console.log(
|
|
2532
|
-
console.log(
|
|
3281
|
+
console.log(chalk19.bold("Starting Panopticon Health Daemon"));
|
|
3282
|
+
console.log(chalk19.dim(`Check interval: ${config.checkIntervalMs / 1e3}s`));
|
|
3283
|
+
console.log(chalk19.dim(`Failure threshold: ${config.consecutiveFailures}`));
|
|
3284
|
+
console.log(chalk19.dim(`Cooldown: ${config.cooldownMs / (1e3 * 60)}m`));
|
|
2533
3285
|
console.log("");
|
|
2534
|
-
console.log(
|
|
3286
|
+
console.log(chalk19.dim("Press Ctrl+C to stop...\n"));
|
|
2535
3287
|
const stop = startHealthDaemon(config, (results) => {
|
|
2536
3288
|
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
2537
3289
|
const statusParts = [
|
|
@@ -2542,12 +3294,12 @@ async function healthCommand(action, arg, options = {}) {
|
|
|
2542
3294
|
`\u274C${results.dead}`
|
|
2543
3295
|
];
|
|
2544
3296
|
if (results.recovered.length > 0) {
|
|
2545
|
-
statusParts.push(
|
|
3297
|
+
statusParts.push(chalk19.green(`+${results.recovered.length} recovered`));
|
|
2546
3298
|
}
|
|
2547
3299
|
console.log(statusParts.join(" "));
|
|
2548
3300
|
});
|
|
2549
3301
|
process.on("SIGINT", () => {
|
|
2550
|
-
console.log("\n" +
|
|
3302
|
+
console.log("\n" + chalk19.dim("Stopping health daemon..."));
|
|
2551
3303
|
stop();
|
|
2552
3304
|
process.exit(0);
|
|
2553
3305
|
});
|
|
@@ -2556,19 +3308,19 @@ async function healthCommand(action, arg, options = {}) {
|
|
|
2556
3308
|
break;
|
|
2557
3309
|
}
|
|
2558
3310
|
default:
|
|
2559
|
-
console.log(
|
|
3311
|
+
console.log(chalk19.bold("Health Monitoring Commands:"));
|
|
2560
3312
|
console.log("");
|
|
2561
|
-
console.log(` ${
|
|
2562
|
-
console.log(` ${
|
|
2563
|
-
console.log(` ${
|
|
2564
|
-
console.log(` ${
|
|
2565
|
-
console.log(` ${
|
|
3313
|
+
console.log(` ${chalk19.cyan("pan work health check")} - Run single health check`);
|
|
3314
|
+
console.log(` ${chalk19.cyan("pan work health status")} - Show all agent health`);
|
|
3315
|
+
console.log(` ${chalk19.cyan("pan work health ping <id>")} - Ping specific agent`);
|
|
3316
|
+
console.log(` ${chalk19.cyan("pan work health recover <id>")} - Force recover agent`);
|
|
3317
|
+
console.log(` ${chalk19.cyan("pan work health daemon")} - Start health daemon`);
|
|
2566
3318
|
console.log("");
|
|
2567
|
-
console.log(
|
|
2568
|
-
console.log(` ${
|
|
2569
|
-
console.log(` ${
|
|
3319
|
+
console.log(chalk19.bold("Options:"));
|
|
3320
|
+
console.log(` ${chalk19.dim("--json")} Output as JSON`);
|
|
3321
|
+
console.log(` ${chalk19.dim("--interval <sec>")} Check interval for daemon (default: 30)`);
|
|
2570
3322
|
console.log("");
|
|
2571
|
-
console.log(
|
|
3323
|
+
console.log(chalk19.bold("Deacon Pattern Defaults:"));
|
|
2572
3324
|
console.log(` Ping timeout: ${DEFAULT_PING_TIMEOUT_MS / 1e3}s`);
|
|
2573
3325
|
console.log(` Consecutive failures: ${DEFAULT_CONSECUTIVE_FAILURES}`);
|
|
2574
3326
|
console.log(` Cooldown after kill: ${DEFAULT_COOLDOWN_MS / (1e3 * 60)}m`);
|
|
@@ -2585,8 +3337,8 @@ function registerWorkCommands(program2) {
|
|
|
2585
3337
|
work.command("kill <id>").description("Kill an agent").option("--force", "Kill without confirmation").action(killCommand);
|
|
2586
3338
|
work.command("pending").description("Show completed work awaiting review").action(pendingCommand);
|
|
2587
3339
|
work.command("approve <id>").description("Approve agent work, merge MR, update Linear").option("--no-merge", "Skip MR merge").option("--no-linear", "Skip Linear status update").action(approveCommand);
|
|
2588
|
-
work.command("plan <id>").description("Create execution plan before spawning").option("-o, --output <path>", "Output file path").option("--json", "Output as JSON").action(planCommand);
|
|
2589
|
-
work.command("list").description("List
|
|
3340
|
+
work.command("plan <id>").description("Create execution plan before spawning").option("-o, --output <path>", "Output file path").option("--json", "Output as JSON").option("--skip-discovery", "Skip interactive discovery phase").option("--force", "Force planning even for simple issues").action(planCommand);
|
|
3341
|
+
work.command("list").description("List issues from configured trackers").option("--all", "Include closed issues").option("--mine", "Show only my assigned issues").option("--json", "Output as JSON").option("--tracker <type>", "Query specific tracker (linear/github/gitlab)").option("--all-trackers", "Query all configured trackers").action(listCommand);
|
|
2590
3342
|
work.command("triage [id]").description("Triage secondary tracker issues").option("--create", "Create primary issue from secondary").option("--dismiss <reason>", "Dismiss from triage").action(triageCommand);
|
|
2591
3343
|
work.command("hook [action] [idOrMessage...]").description("GUPP hooks: check, push, pop, clear, mail, gupp").option("--json", "Output as JSON").action((action, idOrMessage, options) => {
|
|
2592
3344
|
hookCommand(action || "help", idOrMessage?.join(" "), options);
|
|
@@ -2605,17 +3357,17 @@ function registerWorkCommands(program2) {
|
|
|
2605
3357
|
}
|
|
2606
3358
|
|
|
2607
3359
|
// src/cli/commands/workspace.ts
|
|
2608
|
-
import
|
|
3360
|
+
import chalk20 from "chalk";
|
|
2609
3361
|
import ora10 from "ora";
|
|
2610
|
-
import { existsSync as existsSync16, mkdirSync as
|
|
2611
|
-
import { join as
|
|
3362
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
3363
|
+
import { join as join15, basename as basename2 } from "path";
|
|
2612
3364
|
|
|
2613
3365
|
// src/lib/worktree.ts
|
|
2614
|
-
import { execSync as
|
|
2615
|
-
import { mkdirSync as
|
|
2616
|
-
import { dirname } from "path";
|
|
3366
|
+
import { execSync as execSync6 } from "child_process";
|
|
3367
|
+
import { mkdirSync as mkdirSync9 } from "fs";
|
|
3368
|
+
import { dirname as dirname4 } from "path";
|
|
2617
3369
|
function listWorktrees(repoPath) {
|
|
2618
|
-
const output =
|
|
3370
|
+
const output = execSync6("git worktree list --porcelain", {
|
|
2619
3371
|
cwd: repoPath,
|
|
2620
3372
|
encoding: "utf8"
|
|
2621
3373
|
});
|
|
@@ -2637,32 +3389,32 @@ function listWorktrees(repoPath) {
|
|
|
2637
3389
|
return worktrees;
|
|
2638
3390
|
}
|
|
2639
3391
|
function createWorktree(repoPath, targetPath, branchName) {
|
|
2640
|
-
|
|
3392
|
+
mkdirSync9(dirname4(targetPath), { recursive: true });
|
|
2641
3393
|
try {
|
|
2642
|
-
|
|
3394
|
+
execSync6(`git show-ref --verify --quiet refs/heads/${branchName}`, {
|
|
2643
3395
|
cwd: repoPath
|
|
2644
3396
|
});
|
|
2645
|
-
|
|
3397
|
+
execSync6(`git worktree add "${targetPath}" "${branchName}"`, {
|
|
2646
3398
|
cwd: repoPath,
|
|
2647
3399
|
stdio: "pipe"
|
|
2648
3400
|
});
|
|
2649
3401
|
} catch {
|
|
2650
|
-
|
|
3402
|
+
execSync6(`git worktree add -b "${branchName}" "${targetPath}"`, {
|
|
2651
3403
|
cwd: repoPath,
|
|
2652
3404
|
stdio: "pipe"
|
|
2653
3405
|
});
|
|
2654
3406
|
}
|
|
2655
3407
|
}
|
|
2656
3408
|
function removeWorktree(repoPath, worktreePath) {
|
|
2657
|
-
|
|
3409
|
+
execSync6(`git worktree remove "${worktreePath}" --force`, {
|
|
2658
3410
|
cwd: repoPath,
|
|
2659
3411
|
stdio: "pipe"
|
|
2660
3412
|
});
|
|
2661
3413
|
}
|
|
2662
3414
|
|
|
2663
3415
|
// src/lib/template.ts
|
|
2664
|
-
import { readFileSync as readFileSync13, existsSync as existsSync14, readdirSync as
|
|
2665
|
-
import { join as
|
|
3416
|
+
import { readFileSync as readFileSync13, existsSync as existsSync14, readdirSync as readdirSync9 } from "fs";
|
|
3417
|
+
import { join as join13 } from "path";
|
|
2666
3418
|
function loadTemplate(templatePath) {
|
|
2667
3419
|
if (!existsSync14(templatePath)) {
|
|
2668
3420
|
throw new Error(`Template not found: ${templatePath}`);
|
|
@@ -2688,17 +3440,17 @@ function generateClaudeMd(projectPath, variables) {
|
|
|
2688
3440
|
"warnings.md"
|
|
2689
3441
|
];
|
|
2690
3442
|
for (const section of defaultOrder) {
|
|
2691
|
-
const sectionPath =
|
|
3443
|
+
const sectionPath = join13(CLAUDE_MD_TEMPLATES, section);
|
|
2692
3444
|
if (existsSync14(sectionPath)) {
|
|
2693
3445
|
const content = loadTemplate(sectionPath);
|
|
2694
3446
|
sections.push(substituteVariables(content, variables));
|
|
2695
3447
|
}
|
|
2696
3448
|
}
|
|
2697
|
-
const projectSections =
|
|
3449
|
+
const projectSections = join13(projectPath, ".panopticon", "claude-md", "sections");
|
|
2698
3450
|
if (existsSync14(projectSections)) {
|
|
2699
|
-
const projectFiles =
|
|
3451
|
+
const projectFiles = readdirSync9(projectSections).filter((f) => f.endsWith(".md")).sort();
|
|
2700
3452
|
for (const file of projectFiles) {
|
|
2701
|
-
const content = loadTemplate(
|
|
3453
|
+
const content = loadTemplate(join13(projectSections, file));
|
|
2702
3454
|
sections.push(substituteVariables(content, variables));
|
|
2703
3455
|
}
|
|
2704
3456
|
}
|
|
@@ -2720,15 +3472,15 @@ This workspace was created by Panopticon. Use \`bd\` commands to track your work
|
|
|
2720
3472
|
// src/lib/skills-merge.ts
|
|
2721
3473
|
import {
|
|
2722
3474
|
existsSync as existsSync15,
|
|
2723
|
-
readdirSync as
|
|
3475
|
+
readdirSync as readdirSync10,
|
|
2724
3476
|
lstatSync,
|
|
2725
3477
|
readlinkSync,
|
|
2726
3478
|
symlinkSync,
|
|
2727
|
-
mkdirSync as
|
|
3479
|
+
mkdirSync as mkdirSync10,
|
|
2728
3480
|
appendFileSync as appendFileSync2
|
|
2729
3481
|
} from "fs";
|
|
2730
|
-
import { join as
|
|
2731
|
-
import { execSync as
|
|
3482
|
+
import { join as join14 } from "path";
|
|
3483
|
+
import { execSync as execSync7 } from "child_process";
|
|
2732
3484
|
function detectContentOrigin(path, repoPath) {
|
|
2733
3485
|
try {
|
|
2734
3486
|
const stat = lstatSync(path);
|
|
@@ -2739,7 +3491,7 @@ function detectContentOrigin(path, repoPath) {
|
|
|
2739
3491
|
}
|
|
2740
3492
|
}
|
|
2741
3493
|
try {
|
|
2742
|
-
|
|
3494
|
+
execSync7(`git ls-files --error-unmatch "${path}" 2>/dev/null`, {
|
|
2743
3495
|
cwd: repoPath,
|
|
2744
3496
|
stdio: "pipe"
|
|
2745
3497
|
});
|
|
@@ -2752,21 +3504,21 @@ function detectContentOrigin(path, repoPath) {
|
|
|
2752
3504
|
}
|
|
2753
3505
|
}
|
|
2754
3506
|
function mergeSkillsIntoWorkspace(workspacePath) {
|
|
2755
|
-
const skillsTarget =
|
|
3507
|
+
const skillsTarget = join14(workspacePath, ".claude", "skills");
|
|
2756
3508
|
const added = [];
|
|
2757
3509
|
const skipped = [];
|
|
2758
|
-
|
|
3510
|
+
mkdirSync10(skillsTarget, { recursive: true });
|
|
2759
3511
|
const existingSkills = /* @__PURE__ */ new Set();
|
|
2760
3512
|
if (existsSync15(skillsTarget)) {
|
|
2761
|
-
for (const item of
|
|
3513
|
+
for (const item of readdirSync10(skillsTarget)) {
|
|
2762
3514
|
existingSkills.add(item);
|
|
2763
3515
|
}
|
|
2764
3516
|
}
|
|
2765
3517
|
if (!existsSync15(SKILLS_DIR)) return { added, skipped };
|
|
2766
|
-
const panopticonSkills =
|
|
3518
|
+
const panopticonSkills = readdirSync10(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
2767
3519
|
for (const skill of panopticonSkills) {
|
|
2768
|
-
const targetPath =
|
|
2769
|
-
const sourcePath =
|
|
3520
|
+
const targetPath = join14(skillsTarget, skill);
|
|
3521
|
+
const sourcePath = join14(SKILLS_DIR, skill);
|
|
2770
3522
|
if (existingSkills.has(skill)) {
|
|
2771
3523
|
const origin = detectContentOrigin(targetPath, workspacePath);
|
|
2772
3524
|
if (origin === "git-tracked") {
|
|
@@ -2792,7 +3544,7 @@ function mergeSkillsIntoWorkspace(workspacePath) {
|
|
|
2792
3544
|
return { added, skipped };
|
|
2793
3545
|
}
|
|
2794
3546
|
function updateGitignore(skillsDir, skills) {
|
|
2795
|
-
const gitignorePath =
|
|
3547
|
+
const gitignorePath = join14(skillsDir, ".gitignore");
|
|
2796
3548
|
const content = `# Panopticon-managed symlinks (not committed)
|
|
2797
3549
|
${skills.join("\n")}
|
|
2798
3550
|
`;
|
|
@@ -2816,17 +3568,17 @@ async function createCommand(issueId, options) {
|
|
|
2816
3568
|
const branchName = `feature/${normalizedId}`;
|
|
2817
3569
|
const folderName = `feature-${normalizedId}`;
|
|
2818
3570
|
const projectRoot = process.cwd();
|
|
2819
|
-
const workspacesDir =
|
|
2820
|
-
const workspacePath =
|
|
3571
|
+
const workspacesDir = join15(projectRoot, "workspaces");
|
|
3572
|
+
const workspacePath = join15(workspacesDir, folderName);
|
|
2821
3573
|
if (options.dryRun) {
|
|
2822
3574
|
spinner.info("Dry run mode");
|
|
2823
3575
|
console.log("");
|
|
2824
|
-
console.log(
|
|
2825
|
-
console.log(` Workspace: ${
|
|
2826
|
-
console.log(` Branch: ${
|
|
2827
|
-
console.log(` CLAUDE.md: ${
|
|
3576
|
+
console.log(chalk20.bold("Would create:"));
|
|
3577
|
+
console.log(` Workspace: ${chalk20.cyan(workspacePath)}`);
|
|
3578
|
+
console.log(` Branch: ${chalk20.cyan(branchName)}`);
|
|
3579
|
+
console.log(` CLAUDE.md: ${chalk20.dim(join15(workspacePath, "CLAUDE.md"))}`);
|
|
2828
3580
|
if (options.skills !== false) {
|
|
2829
|
-
console.log(` Skills: ${
|
|
3581
|
+
console.log(` Skills: ${chalk20.dim(join15(workspacePath, ".claude", "skills"))}`);
|
|
2830
3582
|
}
|
|
2831
3583
|
return;
|
|
2832
3584
|
}
|
|
@@ -2834,7 +3586,7 @@ async function createCommand(issueId, options) {
|
|
|
2834
3586
|
spinner.fail(`Workspace already exists: ${workspacePath}`);
|
|
2835
3587
|
process.exit(1);
|
|
2836
3588
|
}
|
|
2837
|
-
if (!existsSync16(
|
|
3589
|
+
if (!existsSync16(join15(projectRoot, ".git"))) {
|
|
2838
3590
|
spinner.fail("Not a git repository. Run this from the project root.");
|
|
2839
3591
|
process.exit(1);
|
|
2840
3592
|
}
|
|
@@ -2850,28 +3602,28 @@ async function createCommand(issueId, options) {
|
|
|
2850
3602
|
API_URL: `https://api-${folderName}.localhost:8080`
|
|
2851
3603
|
};
|
|
2852
3604
|
const claudeMd = generateClaudeMd(projectRoot, variables);
|
|
2853
|
-
writeFileSync9(
|
|
3605
|
+
writeFileSync9(join15(workspacePath, "CLAUDE.md"), claudeMd);
|
|
2854
3606
|
let skillsResult = { added: [], skipped: [] };
|
|
2855
3607
|
if (options.skills !== false) {
|
|
2856
3608
|
spinner.text = "Merging skills...";
|
|
2857
|
-
|
|
3609
|
+
mkdirSync11(join15(workspacePath, ".claude", "skills"), { recursive: true });
|
|
2858
3610
|
skillsResult = mergeSkillsIntoWorkspace(workspacePath);
|
|
2859
3611
|
}
|
|
2860
3612
|
spinner.succeed("Workspace created!");
|
|
2861
3613
|
console.log("");
|
|
2862
|
-
console.log(
|
|
2863
|
-
console.log(` Path: ${
|
|
2864
|
-
console.log(` Branch: ${
|
|
3614
|
+
console.log(chalk20.bold("Workspace Details:"));
|
|
3615
|
+
console.log(` Path: ${chalk20.cyan(workspacePath)}`);
|
|
3616
|
+
console.log(` Branch: ${chalk20.dim(branchName)}`);
|
|
2865
3617
|
console.log("");
|
|
2866
3618
|
if (options.skills !== false) {
|
|
2867
|
-
console.log(
|
|
3619
|
+
console.log(chalk20.bold("Skills:"));
|
|
2868
3620
|
console.log(` Added: ${skillsResult.added.length} Panopticon skills`);
|
|
2869
3621
|
if (skillsResult.skipped.length > 0) {
|
|
2870
|
-
console.log(` Skipped: ${
|
|
3622
|
+
console.log(` Skipped: ${chalk20.dim(skillsResult.skipped.join(", "))}`);
|
|
2871
3623
|
}
|
|
2872
3624
|
console.log("");
|
|
2873
3625
|
}
|
|
2874
|
-
console.log(
|
|
3626
|
+
console.log(chalk20.dim(`Next: cd ${workspacePath}`));
|
|
2875
3627
|
} catch (error) {
|
|
2876
3628
|
spinner.fail(error.message);
|
|
2877
3629
|
process.exit(1);
|
|
@@ -2879,8 +3631,8 @@ async function createCommand(issueId, options) {
|
|
|
2879
3631
|
}
|
|
2880
3632
|
async function listCommand2(options) {
|
|
2881
3633
|
const projectRoot = process.cwd();
|
|
2882
|
-
if (!existsSync16(
|
|
2883
|
-
console.error(
|
|
3634
|
+
if (!existsSync16(join15(projectRoot, ".git"))) {
|
|
3635
|
+
console.error(chalk20.red("Not a git repository."));
|
|
2884
3636
|
process.exit(1);
|
|
2885
3637
|
}
|
|
2886
3638
|
const worktrees = listWorktrees(projectRoot);
|
|
@@ -2892,17 +3644,17 @@ async function listCommand2(options) {
|
|
|
2892
3644
|
return;
|
|
2893
3645
|
}
|
|
2894
3646
|
if (workspaces.length === 0) {
|
|
2895
|
-
console.log(
|
|
2896
|
-
console.log(
|
|
3647
|
+
console.log(chalk20.dim("No workspaces found."));
|
|
3648
|
+
console.log(chalk20.dim("Create one with: pan workspace create <issue-id>"));
|
|
2897
3649
|
return;
|
|
2898
3650
|
}
|
|
2899
|
-
console.log(
|
|
3651
|
+
console.log(chalk20.bold("\nWorkspaces\n"));
|
|
2900
3652
|
for (const ws of workspaces) {
|
|
2901
3653
|
const name = basename2(ws.path);
|
|
2902
|
-
const status = ws.prunable ?
|
|
2903
|
-
console.log(`${
|
|
2904
|
-
console.log(` Branch: ${ws.branch ||
|
|
2905
|
-
console.log(` Path: ${
|
|
3654
|
+
const status = ws.prunable ? chalk20.yellow(" (prunable)") : "";
|
|
3655
|
+
console.log(`${chalk20.cyan(name)}${status}`);
|
|
3656
|
+
console.log(` Branch: ${ws.branch || chalk20.dim("(detached)")}`);
|
|
3657
|
+
console.log(` Path: ${chalk20.dim(ws.path)}`);
|
|
2906
3658
|
console.log("");
|
|
2907
3659
|
}
|
|
2908
3660
|
}
|
|
@@ -2912,7 +3664,7 @@ async function destroyCommand(issueId, options) {
|
|
|
2912
3664
|
const normalizedId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
2913
3665
|
const folderName = `feature-${normalizedId}`;
|
|
2914
3666
|
const projectRoot = process.cwd();
|
|
2915
|
-
const workspacePath =
|
|
3667
|
+
const workspacePath = join15(projectRoot, "workspaces", folderName);
|
|
2916
3668
|
if (!existsSync16(workspacePath)) {
|
|
2917
3669
|
spinner.fail(`Workspace not found: ${workspacePath}`);
|
|
2918
3670
|
process.exit(1);
|
|
@@ -2923,19 +3675,19 @@ async function destroyCommand(issueId, options) {
|
|
|
2923
3675
|
} catch (error) {
|
|
2924
3676
|
spinner.fail(error.message);
|
|
2925
3677
|
if (!options.force) {
|
|
2926
|
-
console.log(
|
|
3678
|
+
console.log(chalk20.dim("Tip: Use --force to remove even with uncommitted changes"));
|
|
2927
3679
|
}
|
|
2928
3680
|
process.exit(1);
|
|
2929
3681
|
}
|
|
2930
3682
|
}
|
|
2931
3683
|
|
|
2932
3684
|
// src/cli/commands/install.ts
|
|
2933
|
-
import
|
|
3685
|
+
import chalk21 from "chalk";
|
|
2934
3686
|
import ora11 from "ora";
|
|
2935
|
-
import { execSync as
|
|
2936
|
-
import { existsSync as existsSync17, mkdirSync as
|
|
2937
|
-
import { join as
|
|
2938
|
-
import { platform } from "os";
|
|
3687
|
+
import { execSync as execSync8 } from "child_process";
|
|
3688
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10, readFileSync as readFileSync14, copyFileSync, readdirSync as readdirSync11, statSync } from "fs";
|
|
3689
|
+
import { join as join16 } from "path";
|
|
3690
|
+
import { homedir as homedir4, platform } from "os";
|
|
2939
3691
|
function registerInstallCommand(program2) {
|
|
2940
3692
|
program2.command("install").description("Install Panopticon prerequisites").option("--check", "Check prerequisites only").option("--minimal", "Skip Traefik and mkcert (use port-based routing)").option("--skip-mkcert", "Skip mkcert/HTTPS setup").option("--skip-docker", "Skip Docker network setup").action(installCommand);
|
|
2941
3693
|
}
|
|
@@ -2953,9 +3705,26 @@ function detectPlatform() {
|
|
|
2953
3705
|
}
|
|
2954
3706
|
return os;
|
|
2955
3707
|
}
|
|
3708
|
+
function copyDirectoryRecursive(source, dest) {
|
|
3709
|
+
if (!existsSync17(source)) {
|
|
3710
|
+
throw new Error(`Source directory not found: ${source}`);
|
|
3711
|
+
}
|
|
3712
|
+
mkdirSync12(dest, { recursive: true });
|
|
3713
|
+
const entries = readdirSync11(source);
|
|
3714
|
+
for (const entry of entries) {
|
|
3715
|
+
const sourcePath = join16(source, entry);
|
|
3716
|
+
const destPath = join16(dest, entry);
|
|
3717
|
+
const stat = statSync(sourcePath);
|
|
3718
|
+
if (stat.isDirectory()) {
|
|
3719
|
+
copyDirectoryRecursive(sourcePath, destPath);
|
|
3720
|
+
} else {
|
|
3721
|
+
copyFileSync(sourcePath, destPath);
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
2956
3725
|
function checkCommand(cmd) {
|
|
2957
3726
|
try {
|
|
2958
|
-
|
|
3727
|
+
execSync8(`which ${cmd}`, { stdio: "pipe" });
|
|
2959
3728
|
return true;
|
|
2960
3729
|
} catch {
|
|
2961
3730
|
return false;
|
|
@@ -2982,7 +3751,7 @@ function checkPrerequisites() {
|
|
|
2982
3751
|
let dockerRunning = false;
|
|
2983
3752
|
if (hasDocker) {
|
|
2984
3753
|
try {
|
|
2985
|
-
|
|
3754
|
+
execSync8("docker info", { stdio: "pipe" });
|
|
2986
3755
|
dockerRunning = true;
|
|
2987
3756
|
} catch {
|
|
2988
3757
|
}
|
|
@@ -3014,27 +3783,34 @@ function checkPrerequisites() {
|
|
|
3014
3783
|
message: hasBeads ? "installed" : "not found",
|
|
3015
3784
|
fix: "cargo install beads-cli"
|
|
3016
3785
|
});
|
|
3786
|
+
const hasTtyd = checkCommand("ttyd") || existsSync17(join16(homedir4(), "bin", "ttyd"));
|
|
3787
|
+
results.push({
|
|
3788
|
+
name: "ttyd",
|
|
3789
|
+
passed: hasTtyd,
|
|
3790
|
+
message: hasTtyd ? "installed" : "not found",
|
|
3791
|
+
fix: "brew install ttyd / Download from https://github.com/tsl0922/ttyd/releases"
|
|
3792
|
+
});
|
|
3017
3793
|
return {
|
|
3018
3794
|
results,
|
|
3019
|
-
allPassed: results.filter((r) => r.name !== "mkcert").every((r) => r.passed)
|
|
3795
|
+
allPassed: results.filter((r) => r.name !== "mkcert" && r.name !== "ttyd").every((r) => r.passed)
|
|
3020
3796
|
};
|
|
3021
3797
|
}
|
|
3022
3798
|
function printPrereqStatus(prereqs) {
|
|
3023
|
-
console.log(
|
|
3799
|
+
console.log(chalk21.bold("Prerequisites:\n"));
|
|
3024
3800
|
for (const result of prereqs.results) {
|
|
3025
|
-
const icon = result.passed ?
|
|
3026
|
-
const msg = result.passed ?
|
|
3801
|
+
const icon = result.passed ? chalk21.green("\u2713") : chalk21.red("\u2717");
|
|
3802
|
+
const msg = result.passed ? chalk21.dim(result.message) : chalk21.yellow(result.message);
|
|
3027
3803
|
console.log(` ${icon} ${result.name}: ${msg}`);
|
|
3028
3804
|
if (!result.passed && result.fix) {
|
|
3029
|
-
console.log(` ${
|
|
3805
|
+
console.log(` ${chalk21.dim("\u2192 " + result.fix)}`);
|
|
3030
3806
|
}
|
|
3031
3807
|
}
|
|
3032
3808
|
console.log("");
|
|
3033
3809
|
}
|
|
3034
3810
|
async function installCommand(options) {
|
|
3035
|
-
console.log(
|
|
3811
|
+
console.log(chalk21.bold("\nPanopticon Installation\n"));
|
|
3036
3812
|
const plat = detectPlatform();
|
|
3037
|
-
console.log(`Platform: ${
|
|
3813
|
+
console.log(`Platform: ${chalk21.cyan(plat)}
|
|
3038
3814
|
`);
|
|
3039
3815
|
const prereqs = checkPrerequisites();
|
|
3040
3816
|
if (options.check) {
|
|
@@ -3043,19 +3819,19 @@ async function installCommand(options) {
|
|
|
3043
3819
|
}
|
|
3044
3820
|
printPrereqStatus(prereqs);
|
|
3045
3821
|
if (!prereqs.allPassed) {
|
|
3046
|
-
console.log(
|
|
3047
|
-
console.log(
|
|
3822
|
+
console.log(chalk21.red("Fix prerequisites above before continuing."));
|
|
3823
|
+
console.log(chalk21.dim("Tip: Run with --minimal to skip optional components"));
|
|
3048
3824
|
process.exit(1);
|
|
3049
3825
|
}
|
|
3050
3826
|
const spinner = ora11("Initializing Panopticon directories...").start();
|
|
3051
3827
|
for (const dir of INIT_DIRS) {
|
|
3052
|
-
|
|
3828
|
+
mkdirSync12(dir, { recursive: true });
|
|
3053
3829
|
}
|
|
3054
3830
|
spinner.succeed("Directories initialized");
|
|
3055
3831
|
if (!options.skipDocker) {
|
|
3056
3832
|
spinner.start("Creating Docker network...");
|
|
3057
3833
|
try {
|
|
3058
|
-
|
|
3834
|
+
execSync8("docker network create panopticon 2>/dev/null || true", { stdio: "pipe" });
|
|
3059
3835
|
spinner.succeed("Docker network ready");
|
|
3060
3836
|
} catch (error) {
|
|
3061
3837
|
spinner.warn("Docker network setup failed (may already exist)");
|
|
@@ -3066,14 +3842,20 @@ async function installCommand(options) {
|
|
|
3066
3842
|
if (hasMkcert) {
|
|
3067
3843
|
spinner.start("Setting up mkcert CA...");
|
|
3068
3844
|
try {
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3845
|
+
execSync8("mkcert -install", { stdio: "pipe" });
|
|
3846
|
+
spinner.succeed("mkcert CA installed");
|
|
3847
|
+
spinner.start("Generating wildcard certificates...");
|
|
3848
|
+
const traefikCertFile = join16(TRAEFIK_CERTS_DIR, "_wildcard.pan.localhost.pem");
|
|
3849
|
+
const traefikKeyFile = join16(TRAEFIK_CERTS_DIR, "_wildcard.pan.localhost-key.pem");
|
|
3850
|
+
execSync8(
|
|
3851
|
+
`mkcert -cert-file "${traefikCertFile}" -key-file "${traefikKeyFile}" "*.pan.localhost" "*.localhost" localhost 127.0.0.1 ::1`,
|
|
3074
3852
|
{ stdio: "pipe" }
|
|
3075
3853
|
);
|
|
3076
|
-
|
|
3854
|
+
const legacyCertFile = join16(CERTS_DIR, "localhost.pem");
|
|
3855
|
+
const legacyKeyFile = join16(CERTS_DIR, "localhost-key.pem");
|
|
3856
|
+
copyFileSync(traefikCertFile, legacyCertFile);
|
|
3857
|
+
copyFileSync(traefikKeyFile, legacyKeyFile);
|
|
3858
|
+
spinner.succeed("Wildcard certificates generated (*.pan.localhost, *.localhost)");
|
|
3077
3859
|
} catch (error) {
|
|
3078
3860
|
spinner.warn("mkcert setup failed (HTTPS may not work)");
|
|
3079
3861
|
}
|
|
@@ -3081,9 +3863,60 @@ async function installCommand(options) {
|
|
|
3081
3863
|
spinner.info("Skipping mkcert (not installed)");
|
|
3082
3864
|
}
|
|
3083
3865
|
}
|
|
3084
|
-
const
|
|
3866
|
+
const hasTtyd = checkCommand("ttyd") || existsSync17(join16(homedir4(), "bin", "ttyd"));
|
|
3867
|
+
if (!hasTtyd) {
|
|
3868
|
+
spinner.start("Installing ttyd (web terminal)...");
|
|
3869
|
+
try {
|
|
3870
|
+
const binDir = join16(homedir4(), "bin");
|
|
3871
|
+
mkdirSync12(binDir, { recursive: true });
|
|
3872
|
+
const ttydPath = join16(binDir, "ttyd");
|
|
3873
|
+
const plat2 = detectPlatform();
|
|
3874
|
+
let downloadUrl = "";
|
|
3875
|
+
if (plat2 === "darwin") {
|
|
3876
|
+
try {
|
|
3877
|
+
execSync8("brew install ttyd", { stdio: "pipe" });
|
|
3878
|
+
spinner.succeed("ttyd installed via Homebrew");
|
|
3879
|
+
} catch {
|
|
3880
|
+
spinner.warn("ttyd installation failed - install manually: brew install ttyd");
|
|
3881
|
+
}
|
|
3882
|
+
} else {
|
|
3883
|
+
downloadUrl = "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.x86_64";
|
|
3884
|
+
try {
|
|
3885
|
+
execSync8(`curl -sL "${downloadUrl}" -o "${ttydPath}" && chmod +x "${ttydPath}"`, {
|
|
3886
|
+
stdio: "pipe",
|
|
3887
|
+
timeout: 6e4
|
|
3888
|
+
});
|
|
3889
|
+
spinner.succeed(`ttyd installed to ${ttydPath}`);
|
|
3890
|
+
} catch (error) {
|
|
3891
|
+
spinner.warn("ttyd download failed - install manually from https://github.com/tsl0922/ttyd/releases");
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
} catch (error) {
|
|
3895
|
+
spinner.warn("ttyd installation failed (planning sessions will not work)");
|
|
3896
|
+
}
|
|
3897
|
+
} else {
|
|
3898
|
+
spinner.info("ttyd already installed");
|
|
3899
|
+
}
|
|
3900
|
+
if (!options.minimal) {
|
|
3901
|
+
spinner.start("Setting up Traefik configuration...");
|
|
3902
|
+
try {
|
|
3903
|
+
if (!existsSync17(join16(TRAEFIK_DIR, "docker-compose.yml"))) {
|
|
3904
|
+
copyDirectoryRecursive(SOURCE_TRAEFIK_TEMPLATES, TRAEFIK_DIR);
|
|
3905
|
+
spinner.succeed("Traefik configuration created from templates");
|
|
3906
|
+
} else {
|
|
3907
|
+
spinner.info("Traefik configuration already exists (skipping)");
|
|
3908
|
+
}
|
|
3909
|
+
} catch (error) {
|
|
3910
|
+
spinner.fail(`Failed to set up Traefik configuration: ${error}`);
|
|
3911
|
+
console.log(chalk21.yellow("You can set up Traefik manually later"));
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
const configFile = join16(PANOPTICON_HOME, "config.toml");
|
|
3085
3915
|
if (!existsSync17(configFile)) {
|
|
3086
3916
|
spinner.start("Creating default config...");
|
|
3917
|
+
const traefikConfig = options.minimal ? "enabled = false" : `enabled = true
|
|
3918
|
+
dashboard_port = 8080
|
|
3919
|
+
domain = "pan.localhost"`;
|
|
3087
3920
|
writeFileSync10(
|
|
3088
3921
|
configFile,
|
|
3089
3922
|
`# Panopticon configuration
|
|
@@ -3095,6 +3928,9 @@ default_runtime = "claude"
|
|
|
3095
3928
|
port = 3001
|
|
3096
3929
|
api_port = 3002
|
|
3097
3930
|
|
|
3931
|
+
[traefik]
|
|
3932
|
+
${traefikConfig}
|
|
3933
|
+
|
|
3098
3934
|
[sync]
|
|
3099
3935
|
auto_sync = true
|
|
3100
3936
|
strategy = "symlink"
|
|
@@ -3107,20 +3943,27 @@ consecutive_failures = 3
|
|
|
3107
3943
|
spinner.succeed("Config created");
|
|
3108
3944
|
}
|
|
3109
3945
|
console.log("");
|
|
3110
|
-
console.log(
|
|
3946
|
+
console.log(chalk21.green.bold("Installation complete!"));
|
|
3111
3947
|
console.log("");
|
|
3112
|
-
console.log(
|
|
3113
|
-
console.log(` 1. Run ${
|
|
3114
|
-
|
|
3115
|
-
|
|
3948
|
+
console.log(chalk21.bold("Next steps:"));
|
|
3949
|
+
console.log(` 1. Run ${chalk21.cyan("pan sync")} to sync skills to ~/.claude/`);
|
|
3950
|
+
if (!options.minimal) {
|
|
3951
|
+
console.log(` 2. Add to ${chalk21.yellow("/etc/hosts")}: ${chalk21.cyan("127.0.0.1 pan.localhost")}`);
|
|
3952
|
+
console.log(` 3. Run ${chalk21.cyan("pan up")} to start Traefik and dashboard`);
|
|
3953
|
+
console.log(` 4. Access dashboard at ${chalk21.cyan("https://pan.localhost")}`);
|
|
3954
|
+
} else {
|
|
3955
|
+
console.log(` 2. Run ${chalk21.cyan("pan up")} to start the dashboard`);
|
|
3956
|
+
console.log(` 3. Access dashboard at ${chalk21.cyan("http://localhost:3001")}`);
|
|
3957
|
+
}
|
|
3958
|
+
console.log(` ${!options.minimal ? "5" : "4"}. Create a workspace with ${chalk21.cyan("pan workspace create <issue-id>")}`);
|
|
3116
3959
|
console.log("");
|
|
3117
3960
|
}
|
|
3118
3961
|
|
|
3119
3962
|
// src/cli/commands/project.ts
|
|
3120
|
-
import
|
|
3121
|
-
import { existsSync as existsSync18, readFileSync as readFileSync15, writeFileSync as writeFileSync11, mkdirSync as
|
|
3122
|
-
import { join as
|
|
3123
|
-
var PROJECTS_FILE =
|
|
3963
|
+
import chalk22 from "chalk";
|
|
3964
|
+
import { existsSync as existsSync18, readFileSync as readFileSync15, writeFileSync as writeFileSync11, mkdirSync as mkdirSync13 } from "fs";
|
|
3965
|
+
import { join as join17, resolve } from "path";
|
|
3966
|
+
var PROJECTS_FILE = join17(PANOPTICON_HOME, "projects.json");
|
|
3124
3967
|
function loadProjects() {
|
|
3125
3968
|
if (!existsSync18(PROJECTS_FILE)) {
|
|
3126
3969
|
return [];
|
|
@@ -3132,25 +3975,25 @@ function loadProjects() {
|
|
|
3132
3975
|
}
|
|
3133
3976
|
}
|
|
3134
3977
|
function saveProjects(projects) {
|
|
3135
|
-
|
|
3978
|
+
mkdirSync13(PANOPTICON_HOME, { recursive: true });
|
|
3136
3979
|
writeFileSync11(PROJECTS_FILE, JSON.stringify(projects, null, 2));
|
|
3137
3980
|
}
|
|
3138
3981
|
async function projectAddCommand(projectPath, options = {}) {
|
|
3139
3982
|
const fullPath = resolve(projectPath);
|
|
3140
3983
|
if (!existsSync18(fullPath)) {
|
|
3141
|
-
console.log(
|
|
3984
|
+
console.log(chalk22.red(`Path does not exist: ${fullPath}`));
|
|
3142
3985
|
return;
|
|
3143
3986
|
}
|
|
3144
3987
|
const projects = loadProjects();
|
|
3145
3988
|
const existing = projects.find((p) => p.path === fullPath);
|
|
3146
3989
|
if (existing) {
|
|
3147
|
-
console.log(
|
|
3990
|
+
console.log(chalk22.yellow(`Project already registered: ${existing.name}`));
|
|
3148
3991
|
return;
|
|
3149
3992
|
}
|
|
3150
3993
|
const name = options.name || fullPath.split("/").pop() || "unknown";
|
|
3151
3994
|
let linearTeam = options.linearTeam;
|
|
3152
3995
|
if (!linearTeam) {
|
|
3153
|
-
const projectToml =
|
|
3996
|
+
const projectToml = join17(fullPath, ".panopticon", "project.toml");
|
|
3154
3997
|
if (existsSync18(projectToml)) {
|
|
3155
3998
|
const content = readFileSync15(projectToml, "utf-8");
|
|
3156
3999
|
const match = content.match(/team\s*=\s*"([^"]+)"/);
|
|
@@ -3166,33 +4009,33 @@ async function projectAddCommand(projectPath, options = {}) {
|
|
|
3166
4009
|
};
|
|
3167
4010
|
projects.push(project2);
|
|
3168
4011
|
saveProjects(projects);
|
|
3169
|
-
console.log(
|
|
3170
|
-
console.log(
|
|
4012
|
+
console.log(chalk22.green(`\u2713 Added project: ${name}`));
|
|
4013
|
+
console.log(chalk22.dim(` Path: ${fullPath}`));
|
|
3171
4014
|
if (linearTeam) {
|
|
3172
|
-
console.log(
|
|
4015
|
+
console.log(chalk22.dim(` Linear team: ${linearTeam}`));
|
|
3173
4016
|
}
|
|
3174
4017
|
}
|
|
3175
4018
|
async function projectListCommand(options = {}) {
|
|
3176
4019
|
const projects = loadProjects();
|
|
3177
4020
|
if (projects.length === 0) {
|
|
3178
|
-
console.log(
|
|
3179
|
-
console.log(
|
|
4021
|
+
console.log(chalk22.dim("No projects registered."));
|
|
4022
|
+
console.log(chalk22.dim("Add one with: pan project add <path>"));
|
|
3180
4023
|
return;
|
|
3181
4024
|
}
|
|
3182
4025
|
if (options.json) {
|
|
3183
4026
|
console.log(JSON.stringify(projects, null, 2));
|
|
3184
4027
|
return;
|
|
3185
4028
|
}
|
|
3186
|
-
console.log(
|
|
4029
|
+
console.log(chalk22.bold("\nRegistered Projects:\n"));
|
|
3187
4030
|
for (const project2 of projects) {
|
|
3188
4031
|
const exists = existsSync18(project2.path);
|
|
3189
|
-
const statusIcon = exists ?
|
|
3190
|
-
console.log(`${statusIcon} ${
|
|
3191
|
-
console.log(` ${
|
|
4032
|
+
const statusIcon = exists ? chalk22.green("\u2713") : chalk22.red("\u2717");
|
|
4033
|
+
console.log(`${statusIcon} ${chalk22.bold(project2.name)}`);
|
|
4034
|
+
console.log(` ${chalk22.dim(project2.path)}`);
|
|
3192
4035
|
if (project2.linearTeam) {
|
|
3193
|
-
console.log(` ${
|
|
4036
|
+
console.log(` ${chalk22.cyan(`Linear: ${project2.linearTeam}`)}`);
|
|
3194
4037
|
}
|
|
3195
|
-
console.log(` ${
|
|
4038
|
+
console.log(` ${chalk22.dim(`Type: ${project2.type}`)}`);
|
|
3196
4039
|
console.log("");
|
|
3197
4040
|
}
|
|
3198
4041
|
}
|
|
@@ -3202,23 +4045,23 @@ async function projectRemoveCommand(nameOrPath) {
|
|
|
3202
4045
|
(p) => p.name === nameOrPath || p.path === resolve(nameOrPath)
|
|
3203
4046
|
);
|
|
3204
4047
|
if (index === -1) {
|
|
3205
|
-
console.log(
|
|
4048
|
+
console.log(chalk22.red(`Project not found: ${nameOrPath}`));
|
|
3206
4049
|
return;
|
|
3207
4050
|
}
|
|
3208
4051
|
const removed = projects.splice(index, 1)[0];
|
|
3209
4052
|
saveProjects(projects);
|
|
3210
|
-
console.log(
|
|
4053
|
+
console.log(chalk22.green(`\u2713 Removed project: ${removed.name}`));
|
|
3211
4054
|
}
|
|
3212
4055
|
|
|
3213
4056
|
// src/cli/commands/doctor.ts
|
|
3214
|
-
import
|
|
3215
|
-
import { existsSync as existsSync19, readdirSync as
|
|
3216
|
-
import { execSync as
|
|
3217
|
-
import { homedir as
|
|
3218
|
-
import { join as
|
|
4057
|
+
import chalk23 from "chalk";
|
|
4058
|
+
import { existsSync as existsSync19, readdirSync as readdirSync12 } from "fs";
|
|
4059
|
+
import { execSync as execSync9 } from "child_process";
|
|
4060
|
+
import { homedir as homedir5 } from "os";
|
|
4061
|
+
import { join as join18 } from "path";
|
|
3219
4062
|
function checkCommand2(cmd) {
|
|
3220
4063
|
try {
|
|
3221
|
-
|
|
4064
|
+
execSync9(`which ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
|
|
3222
4065
|
return true;
|
|
3223
4066
|
} catch {
|
|
3224
4067
|
return false;
|
|
@@ -3230,14 +4073,14 @@ function checkDirectory(path) {
|
|
|
3230
4073
|
function countItems(path) {
|
|
3231
4074
|
if (!existsSync19(path)) return 0;
|
|
3232
4075
|
try {
|
|
3233
|
-
return
|
|
4076
|
+
return readdirSync12(path).length;
|
|
3234
4077
|
} catch {
|
|
3235
4078
|
return 0;
|
|
3236
4079
|
}
|
|
3237
4080
|
}
|
|
3238
4081
|
async function doctorCommand() {
|
|
3239
|
-
console.log(
|
|
3240
|
-
console.log(
|
|
4082
|
+
console.log(chalk23.bold("\nPanopticon Doctor\n"));
|
|
4083
|
+
console.log(chalk23.dim("Checking system health...\n"));
|
|
3241
4084
|
const checks = [];
|
|
3242
4085
|
const requiredCommands = [
|
|
3243
4086
|
{ cmd: "git", name: "Git", fix: "Install git" },
|
|
@@ -3279,8 +4122,8 @@ async function doctorCommand() {
|
|
|
3279
4122
|
}
|
|
3280
4123
|
}
|
|
3281
4124
|
if (checkDirectory(CLAUDE_DIR)) {
|
|
3282
|
-
const skillsCount = countItems(
|
|
3283
|
-
const commandsCount = countItems(
|
|
4125
|
+
const skillsCount = countItems(join18(CLAUDE_DIR, "skills"));
|
|
4126
|
+
const commandsCount = countItems(join18(CLAUDE_DIR, "commands"));
|
|
3284
4127
|
checks.push({
|
|
3285
4128
|
name: "Claude Code Skills",
|
|
3286
4129
|
status: skillsCount > 0 ? "ok" : "warn",
|
|
@@ -3301,7 +4144,7 @@ async function doctorCommand() {
|
|
|
3301
4144
|
fix: "Install Claude Code first"
|
|
3302
4145
|
});
|
|
3303
4146
|
}
|
|
3304
|
-
const envFile =
|
|
4147
|
+
const envFile = join18(homedir5(), ".panopticon.env");
|
|
3305
4148
|
if (existsSync19(envFile)) {
|
|
3306
4149
|
checks.push({ name: "Config File", status: "ok", message: "~/.panopticon.env exists" });
|
|
3307
4150
|
} else {
|
|
@@ -3335,7 +4178,7 @@ async function doctorCommand() {
|
|
|
3335
4178
|
});
|
|
3336
4179
|
}
|
|
3337
4180
|
try {
|
|
3338
|
-
const sessions =
|
|
4181
|
+
const sessions = execSync9("tmux list-sessions 2>/dev/null || true", { encoding: "utf-8" });
|
|
3339
4182
|
const agentSessions = sessions.split("\n").filter((s) => s.includes("agent-")).length;
|
|
3340
4183
|
checks.push({
|
|
3341
4184
|
name: "Running Agents",
|
|
@@ -3350,52 +4193,175 @@ async function doctorCommand() {
|
|
|
3350
4193
|
});
|
|
3351
4194
|
}
|
|
3352
4195
|
const icons = {
|
|
3353
|
-
ok:
|
|
3354
|
-
warn:
|
|
3355
|
-
error:
|
|
4196
|
+
ok: chalk23.green("\u2713"),
|
|
4197
|
+
warn: chalk23.yellow("\u26A0"),
|
|
4198
|
+
error: chalk23.red("\u2717")
|
|
3356
4199
|
};
|
|
3357
4200
|
let hasErrors = false;
|
|
3358
4201
|
let hasWarnings = false;
|
|
3359
4202
|
for (const check of checks) {
|
|
3360
4203
|
const icon = icons[check.status];
|
|
3361
|
-
const message = check.status === "error" ?
|
|
4204
|
+
const message = check.status === "error" ? chalk23.red(check.message) : check.status === "warn" ? chalk23.yellow(check.message) : chalk23.dim(check.message);
|
|
3362
4205
|
console.log(`${icon} ${check.name}: ${message}`);
|
|
3363
4206
|
if (check.fix && check.status !== "ok") {
|
|
3364
|
-
console.log(
|
|
4207
|
+
console.log(chalk23.dim(` Fix: ${check.fix}`));
|
|
3365
4208
|
}
|
|
3366
4209
|
if (check.status === "error") hasErrors = true;
|
|
3367
4210
|
if (check.status === "warn") hasWarnings = true;
|
|
3368
4211
|
}
|
|
3369
4212
|
console.log("");
|
|
3370
4213
|
if (hasErrors) {
|
|
3371
|
-
console.log(
|
|
3372
|
-
console.log(
|
|
4214
|
+
console.log(chalk23.red("Some required components are missing."));
|
|
4215
|
+
console.log(chalk23.dim("Fix the errors above before using Panopticon."));
|
|
3373
4216
|
} else if (hasWarnings) {
|
|
3374
|
-
console.log(
|
|
4217
|
+
console.log(chalk23.yellow("System is functional with some optional features missing."));
|
|
3375
4218
|
} else {
|
|
3376
|
-
console.log(
|
|
4219
|
+
console.log(chalk23.green("All systems operational!"));
|
|
3377
4220
|
}
|
|
3378
4221
|
console.log("");
|
|
3379
4222
|
}
|
|
3380
4223
|
|
|
4224
|
+
// src/cli/commands/update.ts
|
|
4225
|
+
import { execSync as execSync10 } from "child_process";
|
|
4226
|
+
import chalk24 from "chalk";
|
|
4227
|
+
function getCurrentVersion() {
|
|
4228
|
+
try {
|
|
4229
|
+
const pkg = require_package();
|
|
4230
|
+
return pkg.version;
|
|
4231
|
+
} catch {
|
|
4232
|
+
return "unknown";
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
async function getLatestVersion() {
|
|
4236
|
+
try {
|
|
4237
|
+
const result = execSync10("npm view panopticon-cli version", {
|
|
4238
|
+
encoding: "utf8",
|
|
4239
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4240
|
+
});
|
|
4241
|
+
return result.trim();
|
|
4242
|
+
} catch {
|
|
4243
|
+
throw new Error("Failed to check npm for latest version");
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
function isNewer(latest, current) {
|
|
4247
|
+
const parseVersion = (v) => {
|
|
4248
|
+
const parts = v.replace(/^v/, "").split(".");
|
|
4249
|
+
return {
|
|
4250
|
+
major: parseInt(parts[0] || "0", 10),
|
|
4251
|
+
minor: parseInt(parts[1] || "0", 10),
|
|
4252
|
+
patch: parseInt(parts[2] || "0", 10)
|
|
4253
|
+
};
|
|
4254
|
+
};
|
|
4255
|
+
const l = parseVersion(latest);
|
|
4256
|
+
const c = parseVersion(current);
|
|
4257
|
+
if (l.major !== c.major) return l.major > c.major;
|
|
4258
|
+
if (l.minor !== c.minor) return l.minor > c.minor;
|
|
4259
|
+
return l.patch > c.patch;
|
|
4260
|
+
}
|
|
4261
|
+
async function updateCommand(options) {
|
|
4262
|
+
console.log(chalk24.bold("Panopticon Update\n"));
|
|
4263
|
+
const currentVersion = getCurrentVersion();
|
|
4264
|
+
console.log(`Current version: ${chalk24.cyan(currentVersion)}`);
|
|
4265
|
+
let latestVersion;
|
|
4266
|
+
try {
|
|
4267
|
+
console.log(chalk24.dim("Checking npm for latest version..."));
|
|
4268
|
+
latestVersion = await getLatestVersion();
|
|
4269
|
+
console.log(`Latest version: ${chalk24.cyan(latestVersion)}`);
|
|
4270
|
+
} catch (error) {
|
|
4271
|
+
console.error(chalk24.red("Failed to check for updates"));
|
|
4272
|
+
console.error(chalk24.dim("Make sure you have internet connectivity"));
|
|
4273
|
+
process.exit(1);
|
|
4274
|
+
}
|
|
4275
|
+
const needsUpdate = isNewer(latestVersion, currentVersion);
|
|
4276
|
+
if (!needsUpdate) {
|
|
4277
|
+
console.log(chalk24.green("\n\u2713 You are on the latest version"));
|
|
4278
|
+
return;
|
|
4279
|
+
}
|
|
4280
|
+
console.log(
|
|
4281
|
+
chalk24.yellow(`
|
|
4282
|
+
\u2191 Update available: ${currentVersion} \u2192 ${latestVersion}`)
|
|
4283
|
+
);
|
|
4284
|
+
if (options.check) {
|
|
4285
|
+
console.log(chalk24.dim("\nRun `pan update` to install"));
|
|
4286
|
+
return;
|
|
4287
|
+
}
|
|
4288
|
+
console.log(chalk24.dim("\nUpdating Panopticon..."));
|
|
4289
|
+
try {
|
|
4290
|
+
execSync10("npm install -g panopticon-cli@latest", {
|
|
4291
|
+
stdio: "inherit"
|
|
4292
|
+
});
|
|
4293
|
+
console.log(chalk24.green(`
|
|
4294
|
+
\u2713 Updated to ${latestVersion}`));
|
|
4295
|
+
const config = loadConfig();
|
|
4296
|
+
if (config.sync.auto_sync) {
|
|
4297
|
+
console.log(chalk24.dim("\nRunning auto-sync..."));
|
|
4298
|
+
await syncCommand({});
|
|
4299
|
+
}
|
|
4300
|
+
console.log(chalk24.dim("\nRestart any running agents to use the new version."));
|
|
4301
|
+
} catch (error) {
|
|
4302
|
+
console.error(chalk24.red("\nUpdate failed"));
|
|
4303
|
+
console.error(
|
|
4304
|
+
chalk24.dim("Try running with sudo: sudo npm install -g panopticon-cli@latest")
|
|
4305
|
+
);
|
|
4306
|
+
process.exit(1);
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
|
|
3381
4310
|
// src/cli/index.ts
|
|
3382
4311
|
var program = new Command();
|
|
3383
|
-
program.name("pan").description("Multi-agent orchestration for AI coding assistants").version("0.1.
|
|
4312
|
+
program.name("pan").description("Multi-agent orchestration for AI coding assistants").version("0.1.3");
|
|
3384
4313
|
program.command("init").description("Initialize Panopticon (~/.panopticon/)").action(initCommand);
|
|
3385
4314
|
program.command("sync").description("Sync skills/commands to AI tools").option("--dry-run", "Show what would be synced").option("--force", "Overwrite without prompts").option("--backup-only", "Only create backup").action(syncCommand);
|
|
3386
4315
|
program.command("restore [timestamp]").description("Restore from backup").action(restoreCommand);
|
|
4316
|
+
var backup = program.command("backup").description("Manage backups");
|
|
4317
|
+
backup.command("list").description("List all backups").option("--json", "Output as JSON").action(backupListCommand);
|
|
4318
|
+
backup.command("clean").description("Remove old backups").option("--keep <count>", "Number of backups to keep", "10").action(backupCleanCommand);
|
|
3387
4319
|
program.command("skills").description("List and manage skills").option("--json", "Output as JSON").action(skillsCommand);
|
|
3388
4320
|
registerWorkCommands(program);
|
|
3389
4321
|
registerWorkspaceCommands(program);
|
|
3390
4322
|
registerInstallCommand(program);
|
|
3391
4323
|
program.command("status").description("Show running agents (shorthand for work status)").option("--json", "Output as JSON").action(statusCommand);
|
|
3392
|
-
program.command("up").description("Start dashboard").option("--detach", "Run in background").action(async (options) => {
|
|
3393
|
-
const { spawn, execSync:
|
|
3394
|
-
const { join:
|
|
3395
|
-
const { fileURLToPath } = await import("url");
|
|
3396
|
-
const
|
|
3397
|
-
const
|
|
3398
|
-
|
|
4324
|
+
program.command("up").description("Start dashboard (and Traefik if enabled)").option("--detach", "Run in background").option("--skip-traefik", "Skip Traefik startup").action(async (options) => {
|
|
4325
|
+
const { spawn, execSync: execSync11 } = await import("child_process");
|
|
4326
|
+
const { join: join19, dirname: dirname5 } = await import("path");
|
|
4327
|
+
const { fileURLToPath: fileURLToPath2 } = await import("url");
|
|
4328
|
+
const { readFileSync: readFileSync16, existsSync: existsSync20 } = await import("fs");
|
|
4329
|
+
const { parse } = await import("@iarna/toml");
|
|
4330
|
+
const __dirname3 = dirname5(fileURLToPath2(import.meta.url));
|
|
4331
|
+
const dashboardDir = join19(__dirname3, "..", "dashboard");
|
|
4332
|
+
const configFile = join19(process.env.HOME || "", ".panopticon", "config.toml");
|
|
4333
|
+
let traefikEnabled = false;
|
|
4334
|
+
let traefikDomain = "pan.localhost";
|
|
4335
|
+
if (existsSync20(configFile)) {
|
|
4336
|
+
try {
|
|
4337
|
+
const configContent = readFileSync16(configFile, "utf-8");
|
|
4338
|
+
const config = parse(configContent);
|
|
4339
|
+
traefikEnabled = config.traefik?.enabled === true;
|
|
4340
|
+
traefikDomain = config.traefik?.domain || "pan.localhost";
|
|
4341
|
+
} catch (error) {
|
|
4342
|
+
console.log(chalk25.yellow("Warning: Could not read config.toml"));
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
console.log(chalk25.bold("Starting Panopticon...\n"));
|
|
4346
|
+
if (traefikEnabled && !options.skipTraefik) {
|
|
4347
|
+
const traefikDir = join19(process.env.HOME || "", ".panopticon", "traefik");
|
|
4348
|
+
if (existsSync20(traefikDir)) {
|
|
4349
|
+
try {
|
|
4350
|
+
console.log(chalk25.dim("Starting Traefik..."));
|
|
4351
|
+
execSync11("docker-compose up -d", {
|
|
4352
|
+
cwd: traefikDir,
|
|
4353
|
+
stdio: "pipe"
|
|
4354
|
+
});
|
|
4355
|
+
console.log(chalk25.green("\u2713 Traefik started"));
|
|
4356
|
+
console.log(chalk25.dim(` Dashboard: https://traefik.${traefikDomain}:8080
|
|
4357
|
+
`));
|
|
4358
|
+
} catch (error) {
|
|
4359
|
+
console.log(chalk25.yellow("\u26A0 Failed to start Traefik (continuing anyway)"));
|
|
4360
|
+
console.log(chalk25.dim(" Run with --skip-traefik to suppress this message\n"));
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
console.log(chalk25.dim("Starting dashboard..."));
|
|
3399
4365
|
if (options.detach) {
|
|
3400
4366
|
const child = spawn("npm", ["run", "dev"], {
|
|
3401
4367
|
cwd: dashboardDir,
|
|
@@ -3403,37 +4369,79 @@ program.command("up").description("Start dashboard").option("--detach", "Run in
|
|
|
3403
4369
|
stdio: "ignore"
|
|
3404
4370
|
});
|
|
3405
4371
|
child.unref();
|
|
3406
|
-
console.log(
|
|
3407
|
-
|
|
3408
|
-
|
|
4372
|
+
console.log(chalk25.green("\u2713 Dashboard started in background"));
|
|
4373
|
+
if (traefikEnabled) {
|
|
4374
|
+
console.log(` Frontend: ${chalk25.cyan(`https://${traefikDomain}`)}`);
|
|
4375
|
+
console.log(` API: ${chalk25.cyan(`https://${traefikDomain}/api`)}`);
|
|
4376
|
+
} else {
|
|
4377
|
+
console.log(` Frontend: ${chalk25.cyan("http://localhost:3001")}`);
|
|
4378
|
+
console.log(` API: ${chalk25.cyan("http://localhost:3002")}`);
|
|
4379
|
+
}
|
|
3409
4380
|
} else {
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
4381
|
+
if (traefikEnabled) {
|
|
4382
|
+
console.log(` Frontend: ${chalk25.cyan(`https://${traefikDomain}`)}`);
|
|
4383
|
+
console.log(` API: ${chalk25.cyan(`https://${traefikDomain}/api`)}`);
|
|
4384
|
+
} else {
|
|
4385
|
+
console.log(` Frontend: ${chalk25.cyan("http://localhost:3001")}`);
|
|
4386
|
+
console.log(` API: ${chalk25.cyan("http://localhost:3002")}`);
|
|
4387
|
+
}
|
|
4388
|
+
console.log(chalk25.dim("\nPress Ctrl+C to stop\n"));
|
|
3413
4389
|
const child = spawn("npm", ["run", "dev"], {
|
|
3414
4390
|
cwd: dashboardDir,
|
|
3415
4391
|
stdio: "inherit"
|
|
3416
4392
|
});
|
|
3417
4393
|
child.on("error", (err) => {
|
|
3418
|
-
console.error(
|
|
4394
|
+
console.error(chalk25.red("Failed to start dashboard:"), err.message);
|
|
3419
4395
|
process.exit(1);
|
|
3420
4396
|
});
|
|
3421
4397
|
}
|
|
3422
4398
|
});
|
|
3423
|
-
program.command("down").description("Stop dashboard").action(async () => {
|
|
3424
|
-
const { execSync:
|
|
4399
|
+
program.command("down").description("Stop dashboard (and Traefik if enabled)").option("--skip-traefik", "Skip Traefik shutdown").action(async (options) => {
|
|
4400
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
4401
|
+
const { join: join19 } = await import("path");
|
|
4402
|
+
const { readFileSync: readFileSync16, existsSync: existsSync20 } = await import("fs");
|
|
4403
|
+
const { parse } = await import("@iarna/toml");
|
|
4404
|
+
console.log(chalk25.bold("Stopping Panopticon...\n"));
|
|
4405
|
+
console.log(chalk25.dim("Stopping dashboard..."));
|
|
3425
4406
|
try {
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
console.log(
|
|
4407
|
+
execSync11("lsof -ti:3001 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
|
|
4408
|
+
execSync11("lsof -ti:3002 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
|
|
4409
|
+
console.log(chalk25.green("\u2713 Dashboard stopped"));
|
|
3429
4410
|
} catch {
|
|
3430
|
-
console.log(
|
|
4411
|
+
console.log(chalk25.dim(" No dashboard processes found"));
|
|
4412
|
+
}
|
|
4413
|
+
const configFile = join19(process.env.HOME || "", ".panopticon", "config.toml");
|
|
4414
|
+
let traefikEnabled = false;
|
|
4415
|
+
if (existsSync20(configFile)) {
|
|
4416
|
+
try {
|
|
4417
|
+
const configContent = readFileSync16(configFile, "utf-8");
|
|
4418
|
+
const config = parse(configContent);
|
|
4419
|
+
traefikEnabled = config.traefik?.enabled === true;
|
|
4420
|
+
} catch (error) {
|
|
4421
|
+
}
|
|
3431
4422
|
}
|
|
4423
|
+
if (traefikEnabled && !options.skipTraefik) {
|
|
4424
|
+
const traefikDir = join19(process.env.HOME || "", ".panopticon", "traefik");
|
|
4425
|
+
if (existsSync20(traefikDir)) {
|
|
4426
|
+
console.log(chalk25.dim("Stopping Traefik..."));
|
|
4427
|
+
try {
|
|
4428
|
+
execSync11("docker-compose down", {
|
|
4429
|
+
cwd: traefikDir,
|
|
4430
|
+
stdio: "pipe"
|
|
4431
|
+
});
|
|
4432
|
+
console.log(chalk25.green("\u2713 Traefik stopped"));
|
|
4433
|
+
} catch (error) {
|
|
4434
|
+
console.log(chalk25.yellow("\u26A0 Failed to stop Traefik"));
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
console.log("");
|
|
3432
4439
|
});
|
|
3433
4440
|
var project = program.command("project").description("Project management");
|
|
3434
4441
|
project.command("add <path>").description("Register a project with Panopticon").option("--name <name>", "Project name").option("--type <type>", "Project type (standalone/monorepo)", "standalone").option("--linear-team <team>", "Linear team prefix").action(projectAddCommand);
|
|
3435
4442
|
project.command("list").description("List all managed projects").option("--json", "Output as JSON").action(projectListCommand);
|
|
3436
4443
|
project.command("remove <nameOrPath>").description("Remove a project from Panopticon").action(projectRemoveCommand);
|
|
3437
4444
|
program.command("doctor").description("Check system health and dependencies").action(doctorCommand);
|
|
4445
|
+
program.command("update").description("Update Panopticon to latest version").option("--check", "Only check for updates, don't install").option("--force", "Force update even if on latest").action(updateCommand);
|
|
3438
4446
|
program.parse();
|
|
3439
4447
|
//# sourceMappingURL=index.js.map
|