@yemi33/squad 0.1.0 → 0.1.1

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 yemi33
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yemi33
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/TODO.md CHANGED
@@ -6,21 +6,21 @@ Ordered by difficulty: quick wins first, larger efforts later.
6
6
 
7
7
  ## Quick Wins (< 1 hour each)
8
8
 
9
- - [ ] **Output.log append, not overwrite** — keep all dispatch outputs, not just the last one. Rotate by dispatch ID.
10
- - [ ] **Persistent cooldowns** — save cooldown state to disk so engine restarts don't re-dispatch everything
11
- - [ ] **Worktree cleanup on merge/close** — when `pollPrStatus` detects a PR merged or abandoned, auto-remove its worktree. Currently only `runCleanup` catches old worktrees on a timer.
12
- - [ ] **Discovery skip logging** — log why items were skipped during discovery (cooldown, already dispatched, no idle agent) so users can diagnose "why isn't my work item being picked up?"
13
- - [ ] **Idle threshold alert** — if all agents are idle for >N minutes, notify via Teams/dashboard
14
- - [ ] **Config validation at startup** — verify all project paths exist, all agents defined, all playbooks exist. Fail fast with clear errors instead of silently skipping.
15
- - [ ] **macOS/Linux browser launch** — replace Windows `start` command with `open` (macOS) / `xdg-open` (Linux)
16
- - [ ] **Health check endpoint** — `/api/health` returning engine state, project reachability, agent statuses for monitoring
17
- - [ ] **Fan-out per-agent timeout** — when `@everyone` dispatches, set individual deadlines per agent instead of relying only on global `agentTimeout`
9
+ - [x] **Output.log append, not overwrite** — per-dispatch archive at `output-{id}.log` + latest copy at `output.log`
10
+ - [x] **Persistent cooldowns** — saved to `engine/cooldowns.json`, loaded at startup, 24hr auto-prune
11
+ - [x] **Worktree cleanup on merge/close** — `handlePostMerge` removes worktrees when PR merges or is abandoned
12
+ - [x] **Discovery skip logging** — PRD and work item discovery log skip counts by reason at debug level
13
+ - [x] **Idle threshold alert** — warns when all agents idle >15min (configurable via `engine.idleAlertMinutes`)
14
+ - [x] **Config validation at startup** — checks agents, project paths, playbooks, routing.md. Fatal errors exit(1).
15
+ - [x] **macOS/Linux browser launch** — already platform-aware (was done previously)
16
+ - [x] **Health check endpoint** — `GET /api/health` returning engine state, agents, project reachability, uptime
17
+ - [x] **Fan-out per-agent timeout** — fan-out items carry `meta.deadline`, configurable via `engine.fanOutTimeout`
18
18
 
19
19
  ## Small Effort (1–3 hours each)
20
20
 
21
21
  - [ ] **Auto-retry with backoff** — when an agent errors, auto-retry with exponential backoff (5min, 15min, cap at 3 attempts) instead of requiring manual dashboard retry.
22
22
  - [ ] **Auto-escalation** — if an agent errors 3 times in a row, pause their dispatch and alert via dashboard/Teams
23
- - [ ] **Post-merge hooks** — when a PR merges, trigger configurable actions: clean up worktree, update PRD item status to `done`, notify Teams, update metrics
23
+ - [x] **Post-merge hooks** — `handlePostMerge`: worktree cleanup, PRD status implemented, prsMerged metric, Teams notification
24
24
  - [ ] **Pending dispatch explanation** — show in dashboard why each pending item hasn't been dispatched (no idle agent? cooldown? max concurrency?)
25
25
  - [ ] **Work item editing** — edit title, description, type, priority, agent assignment from the dashboard UI (currently requires editing JSON)
26
26
  - [ ] **Bulk operations** — retry/delete/reassign multiple work items at once
package/bin/squad.js CHANGED
@@ -1,164 +1,164 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Squad CLI — Central AI dev team manager
4
- *
5
- * Usage:
6
- * squad init Bootstrap ~/.squad/ with default config and agents
7
- * squad add <project-dir> Link a project (interactive)
8
- * squad remove <project-dir> Unlink a project
9
- * squad list List linked projects
10
- * squad start Start the engine
11
- * squad stop Stop the engine
12
- * squad status Show engine status
13
- * squad pause / resume Pause/resume dispatching
14
- * squad dash Start the dashboard
15
- * squad work <title> [opts-json] Add a work item
16
- * squad spawn <agent> <prompt> Manually spawn an agent
17
- * squad dispatch Force a dispatch cycle
18
- * squad discover Dry-run work discovery
19
- * squad cleanup Run cleanup manually
20
- * squad plan <file|text> [proj] Run a plan
21
- */
22
-
23
- const fs = require('fs');
24
- const path = require('path');
25
- const { spawn, execSync } = require('child_process');
26
-
27
- const SQUAD_HOME = path.join(require('os').homedir(), '.squad');
28
- const PKG_ROOT = path.resolve(__dirname, '..');
29
-
30
- const [cmd, ...rest] = process.argv.slice(2);
31
-
32
- // ─── Init: bootstrap ~/.squad/ from package templates ───────────────────────
33
-
34
- function init() {
35
- if (fs.existsSync(path.join(SQUAD_HOME, 'engine.js'))) {
36
- console.log(`\n Squad already installed at ${SQUAD_HOME}`);
37
- console.log(' To reinitialize config: squad init --force\n');
38
- if (!rest.includes('--force')) return;
39
- }
40
-
41
- console.log(`\n Bootstrapping Squad to ${SQUAD_HOME}...\n`);
42
- fs.mkdirSync(SQUAD_HOME, { recursive: true });
43
-
44
- // Copy all package files to ~/.squad/
45
- const exclude = new Set([
46
- 'bin', 'node_modules', '.git', '.claude', 'package.json',
47
- 'package-lock.json', 'LICENSE', '.npmignore', '.gitignore',
48
- ]);
49
-
50
- copyDir(PKG_ROOT, SQUAD_HOME, exclude);
51
-
52
- // Create config from template if it doesn't exist
53
- const configPath = path.join(SQUAD_HOME, 'config.json');
54
- if (!fs.existsSync(configPath)) {
55
- const tmpl = path.join(SQUAD_HOME, 'config.template.json');
56
- if (fs.existsSync(tmpl)) {
57
- fs.copyFileSync(tmpl, configPath);
58
- }
59
- }
60
-
61
- // Ensure runtime directories exist
62
- const dirs = [
63
- 'engine', 'notes/inbox', 'notes/archive',
64
- 'identity', 'plans',
65
- ];
66
- for (const d of dirs) {
67
- fs.mkdirSync(path.join(SQUAD_HOME, d), { recursive: true });
68
- }
69
-
70
- // Run squad.js init to populate config with defaults
71
- execSync(`node "${path.join(SQUAD_HOME, 'squad.js')}" init`, { stdio: 'inherit' });
72
-
73
- console.log('\n Next steps:');
74
- console.log(' squad add ~/my-project Link your first project');
75
- console.log(' squad start Start the engine');
76
- console.log(' squad dash Open the dashboard\n');
77
- }
78
-
79
- function copyDir(src, dest, exclude) {
80
- const entries = fs.readdirSync(src, { withFileTypes: true });
81
- for (const entry of entries) {
82
- if (exclude.has(entry.name)) continue;
83
- const srcPath = path.join(src, entry.name);
84
- const destPath = path.join(dest, entry.name);
85
- if (entry.isDirectory()) {
86
- fs.mkdirSync(destPath, { recursive: true });
87
- copyDir(srcPath, destPath, new Set()); // only exclude at top level
88
- } else {
89
- // Don't overwrite user-modified files (except on --force)
90
- if (fs.existsSync(destPath) && !rest.includes('--force')) {
91
- // Always update engine code files
92
- if (!entry.name.endsWith('.js') && !entry.name.endsWith('.html')) continue;
93
- }
94
- fs.copyFileSync(srcPath, destPath);
95
- }
96
- }
97
- }
98
-
99
- // ─── Delegate: run commands against installed ~/.squad/ ─────────────────────
100
-
101
- function ensureInstalled() {
102
- if (!fs.existsSync(path.join(SQUAD_HOME, 'engine.js'))) {
103
- console.log('\n Squad is not installed. Run: squad init\n');
104
- process.exit(1);
105
- }
106
- }
107
-
108
- function delegate(script, args) {
109
- ensureInstalled();
110
- const child = spawn(process.execPath, [path.join(SQUAD_HOME, script), ...args], {
111
- stdio: 'inherit',
112
- cwd: SQUAD_HOME,
113
- });
114
- child.on('exit', code => process.exit(code || 0));
115
- }
116
-
117
- // ─── Command routing ────────────────────────────────────────────────────────
118
-
119
- const engineCmds = new Set([
120
- 'start', 'stop', 'status', 'pause', 'resume',
121
- 'queue', 'sources', 'discover', 'dispatch',
122
- 'spawn', 'work', 'cleanup', 'mcp-sync', 'plan',
123
- ]);
124
-
125
- if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
126
- console.log(`
127
- Squad — Central AI dev team manager
128
-
129
- Setup:
130
- squad init Bootstrap ~/.squad/ (first time)
131
- squad add <project-dir> Link a project (interactive)
132
- squad remove <project-dir> Unlink a project
133
- squad list List linked projects
134
-
135
- Engine:
136
- squad start Start engine daemon
137
- squad stop Stop the engine
138
- squad status Show agents, projects, queue
139
- squad pause / resume Pause/resume dispatching
140
- squad dispatch Force a dispatch cycle
141
- squad discover Dry-run work discovery
142
- squad work <title> [opts] Add a work item
143
- squad spawn <agent> <prompt> Manually spawn an agent
144
- squad plan <file|text> [proj] Run a plan
145
- squad cleanup Clean temp files, worktrees, zombies
146
-
147
- Dashboard:
148
- squad dash Start web dashboard (default :7331)
149
-
150
- Home: ${SQUAD_HOME}
151
- `);
152
- } else if (cmd === 'init') {
153
- init();
154
- } else if (cmd === 'add' || cmd === 'remove' || cmd === 'list') {
155
- delegate('squad.js', [cmd, ...rest]);
156
- } else if (cmd === 'dash' || cmd === 'dashboard') {
157
- delegate('dashboard.js', rest);
158
- } else if (engineCmds.has(cmd)) {
159
- delegate('engine.js', [cmd, ...rest]);
160
- } else {
161
- console.log(` Unknown command: ${cmd}`);
162
- console.log(' Run "squad help" for usage.\n');
163
- process.exit(1);
164
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Squad CLI — Central AI dev team manager
4
+ *
5
+ * Usage:
6
+ * squad init Bootstrap ~/.squad/ with default config and agents
7
+ * squad add <project-dir> Link a project (interactive)
8
+ * squad remove <project-dir> Unlink a project
9
+ * squad list List linked projects
10
+ * squad start Start the engine
11
+ * squad stop Stop the engine
12
+ * squad status Show engine status
13
+ * squad pause / resume Pause/resume dispatching
14
+ * squad dash Start the dashboard
15
+ * squad work <title> [opts-json] Add a work item
16
+ * squad spawn <agent> <prompt> Manually spawn an agent
17
+ * squad dispatch Force a dispatch cycle
18
+ * squad discover Dry-run work discovery
19
+ * squad cleanup Run cleanup manually
20
+ * squad plan <file|text> [proj] Run a plan
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const { spawn, execSync } = require('child_process');
26
+
27
+ const SQUAD_HOME = path.join(require('os').homedir(), '.squad');
28
+ const PKG_ROOT = path.resolve(__dirname, '..');
29
+
30
+ const [cmd, ...rest] = process.argv.slice(2);
31
+
32
+ // ─── Init: bootstrap ~/.squad/ from package templates ───────────────────────
33
+
34
+ function init() {
35
+ if (fs.existsSync(path.join(SQUAD_HOME, 'engine.js'))) {
36
+ console.log(`\n Squad already installed at ${SQUAD_HOME}`);
37
+ console.log(' To reinitialize config: squad init --force\n');
38
+ if (!rest.includes('--force')) return;
39
+ }
40
+
41
+ console.log(`\n Bootstrapping Squad to ${SQUAD_HOME}...\n`);
42
+ fs.mkdirSync(SQUAD_HOME, { recursive: true });
43
+
44
+ // Copy all package files to ~/.squad/
45
+ const exclude = new Set([
46
+ 'bin', 'node_modules', '.git', '.claude', 'package.json',
47
+ 'package-lock.json', 'LICENSE', '.npmignore', '.gitignore',
48
+ ]);
49
+
50
+ copyDir(PKG_ROOT, SQUAD_HOME, exclude);
51
+
52
+ // Create config from template if it doesn't exist
53
+ const configPath = path.join(SQUAD_HOME, 'config.json');
54
+ if (!fs.existsSync(configPath)) {
55
+ const tmpl = path.join(SQUAD_HOME, 'config.template.json');
56
+ if (fs.existsSync(tmpl)) {
57
+ fs.copyFileSync(tmpl, configPath);
58
+ }
59
+ }
60
+
61
+ // Ensure runtime directories exist
62
+ const dirs = [
63
+ 'engine', 'notes/inbox', 'notes/archive',
64
+ 'identity', 'plans',
65
+ ];
66
+ for (const d of dirs) {
67
+ fs.mkdirSync(path.join(SQUAD_HOME, d), { recursive: true });
68
+ }
69
+
70
+ // Run squad.js init to populate config with defaults
71
+ execSync(`node "${path.join(SQUAD_HOME, 'squad.js')}" init`, { stdio: 'inherit' });
72
+
73
+ console.log('\n Next steps:');
74
+ console.log(' squad add ~/my-project Link your first project');
75
+ console.log(' squad start Start the engine');
76
+ console.log(' squad dash Open the dashboard\n');
77
+ }
78
+
79
+ function copyDir(src, dest, exclude) {
80
+ const entries = fs.readdirSync(src, { withFileTypes: true });
81
+ for (const entry of entries) {
82
+ if (exclude.has(entry.name)) continue;
83
+ const srcPath = path.join(src, entry.name);
84
+ const destPath = path.join(dest, entry.name);
85
+ if (entry.isDirectory()) {
86
+ fs.mkdirSync(destPath, { recursive: true });
87
+ copyDir(srcPath, destPath, new Set()); // only exclude at top level
88
+ } else {
89
+ // Don't overwrite user-modified files (except on --force)
90
+ if (fs.existsSync(destPath) && !rest.includes('--force')) {
91
+ // Always update engine code files
92
+ if (!entry.name.endsWith('.js') && !entry.name.endsWith('.html')) continue;
93
+ }
94
+ fs.copyFileSync(srcPath, destPath);
95
+ }
96
+ }
97
+ }
98
+
99
+ // ─── Delegate: run commands against installed ~/.squad/ ─────────────────────
100
+
101
+ function ensureInstalled() {
102
+ if (!fs.existsSync(path.join(SQUAD_HOME, 'engine.js'))) {
103
+ console.log('\n Squad is not installed. Run: squad init\n');
104
+ process.exit(1);
105
+ }
106
+ }
107
+
108
+ function delegate(script, args) {
109
+ ensureInstalled();
110
+ const child = spawn(process.execPath, [path.join(SQUAD_HOME, script), ...args], {
111
+ stdio: 'inherit',
112
+ cwd: SQUAD_HOME,
113
+ });
114
+ child.on('exit', code => process.exit(code || 0));
115
+ }
116
+
117
+ // ─── Command routing ────────────────────────────────────────────────────────
118
+
119
+ const engineCmds = new Set([
120
+ 'start', 'stop', 'status', 'pause', 'resume',
121
+ 'queue', 'sources', 'discover', 'dispatch',
122
+ 'spawn', 'work', 'cleanup', 'mcp-sync', 'plan',
123
+ ]);
124
+
125
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
126
+ console.log(`
127
+ Squad — Central AI dev team manager
128
+
129
+ Setup:
130
+ squad init Bootstrap ~/.squad/ (first time)
131
+ squad add <project-dir> Link a project (interactive)
132
+ squad remove <project-dir> Unlink a project
133
+ squad list List linked projects
134
+
135
+ Engine:
136
+ squad start Start engine daemon
137
+ squad stop Stop the engine
138
+ squad status Show agents, projects, queue
139
+ squad pause / resume Pause/resume dispatching
140
+ squad dispatch Force a dispatch cycle
141
+ squad discover Dry-run work discovery
142
+ squad work <title> [opts] Add a work item
143
+ squad spawn <agent> <prompt> Manually spawn an agent
144
+ squad plan <file|text> [proj] Run a plan
145
+ squad cleanup Clean temp files, worktrees, zombies
146
+
147
+ Dashboard:
148
+ squad dash Start web dashboard (default :7331)
149
+
150
+ Home: ${SQUAD_HOME}
151
+ `);
152
+ } else if (cmd === 'init') {
153
+ init();
154
+ } else if (cmd === 'add' || cmd === 'remove' || cmd === 'list') {
155
+ delegate('squad.js', [cmd, ...rest]);
156
+ } else if (cmd === 'dash' || cmd === 'dashboard') {
157
+ delegate('dashboard.js', rest);
158
+ } else if (engineCmds.has(cmd)) {
159
+ delegate('engine.js', [cmd, ...rest]);
160
+ } else {
161
+ console.log(` Unknown command: ${cmd}`);
162
+ console.log(' Run "squad help" for usage.\n');
163
+ process.exit(1);
164
+ }