@bradygaster/squad-cli 0.9.1 → 0.9.2-insider.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/README.md +329 -329
  2. package/dist/cli/commands/build.d.ts.map +1 -1
  3. package/dist/cli/commands/build.js +10 -10
  4. package/dist/cli/commands/build.js.map +1 -1
  5. package/dist/cli/commands/config.d.ts +12 -0
  6. package/dist/cli/commands/config.d.ts.map +1 -0
  7. package/dist/cli/commands/config.js +157 -0
  8. package/dist/cli/commands/config.js.map +1 -0
  9. package/dist/cli/commands/consult.d.ts.map +1 -1
  10. package/dist/cli/commands/consult.js +9 -4
  11. package/dist/cli/commands/consult.js.map +1 -1
  12. package/dist/cli/commands/copilot.d.ts.map +1 -1
  13. package/dist/cli/commands/copilot.js +8 -7
  14. package/dist/cli/commands/copilot.js.map +1 -1
  15. package/dist/cli/commands/doctor.d.ts.map +1 -1
  16. package/dist/cli/commands/doctor.js +50 -17
  17. package/dist/cli/commands/doctor.js.map +1 -1
  18. package/dist/cli/commands/economy.d.ts.map +1 -1
  19. package/dist/cli/commands/economy.js +3 -2
  20. package/dist/cli/commands/economy.js.map +1 -1
  21. package/dist/cli/commands/export.d.ts.map +1 -1
  22. package/dist/cli/commands/export.js +22 -16
  23. package/dist/cli/commands/export.js.map +1 -1
  24. package/dist/cli/commands/extract.d.ts.map +1 -1
  25. package/dist/cli/commands/extract.js +14 -10
  26. package/dist/cli/commands/extract.js.map +1 -1
  27. package/dist/cli/commands/import.d.ts.map +1 -1
  28. package/dist/cli/commands/import.js +21 -18
  29. package/dist/cli/commands/import.js.map +1 -1
  30. package/dist/cli/commands/init-remote.d.ts.map +1 -1
  31. package/dist/cli/commands/init-remote.js +7 -6
  32. package/dist/cli/commands/init-remote.js.map +1 -1
  33. package/dist/cli/commands/link.d.ts.map +1 -1
  34. package/dist/cli/commands/link.js +11 -10
  35. package/dist/cli/commands/link.js.map +1 -1
  36. package/dist/cli/commands/migrate.d.ts.map +1 -1
  37. package/dist/cli/commands/migrate.js +19 -18
  38. package/dist/cli/commands/migrate.js.map +1 -1
  39. package/dist/cli/commands/personal.d.ts.map +1 -1
  40. package/dist/cli/commands/personal.js +57 -65
  41. package/dist/cli/commands/personal.js.map +1 -1
  42. package/dist/cli/commands/plugin.d.ts.map +1 -1
  43. package/dist/cli/commands/plugin.js +8 -7
  44. package/dist/cli/commands/plugin.js.map +1 -1
  45. package/dist/cli/commands/rc.d.ts.map +1 -1
  46. package/dist/cli/commands/rc.js +19 -12
  47. package/dist/cli/commands/rc.js.map +1 -1
  48. package/dist/cli/commands/schedule.d.ts.map +1 -1
  49. package/dist/cli/commands/schedule.js +6 -5
  50. package/dist/cli/commands/schedule.js.map +1 -1
  51. package/dist/cli/commands/start.d.ts.map +1 -1
  52. package/dist/cli/commands/start.js +18 -11
  53. package/dist/cli/commands/start.js.map +1 -1
  54. package/dist/cli/commands/streams.d.ts.map +1 -1
  55. package/dist/cli/commands/streams.js +3 -2
  56. package/dist/cli/commands/streams.js.map +1 -1
  57. package/dist/cli/commands/upstream.d.ts.map +1 -1
  58. package/dist/cli/commands/upstream.js +23 -19
  59. package/dist/cli/commands/upstream.js.map +1 -1
  60. package/dist/cli/commands/watch/capabilities/board.d.ts +22 -0
  61. package/dist/cli/commands/watch/capabilities/board.d.ts.map +1 -0
  62. package/dist/cli/commands/watch/capabilities/board.js +121 -0
  63. package/dist/cli/commands/watch/capabilities/board.js.map +1 -0
  64. package/dist/cli/commands/watch/capabilities/decision-hygiene.d.ts +14 -0
  65. package/dist/cli/commands/watch/capabilities/decision-hygiene.d.ts.map +1 -0
  66. package/dist/cli/commands/watch/capabilities/decision-hygiene.js +72 -0
  67. package/dist/cli/commands/watch/capabilities/decision-hygiene.js.map +1 -0
  68. package/dist/cli/commands/watch/capabilities/execute.d.ts +33 -0
  69. package/dist/cli/commands/watch/capabilities/execute.d.ts.map +1 -0
  70. package/dist/cli/commands/watch/capabilities/execute.js +119 -0
  71. package/dist/cli/commands/watch/capabilities/execute.js.map +1 -0
  72. package/dist/cli/commands/watch/capabilities/index.d.ts +7 -0
  73. package/dist/cli/commands/watch/capabilities/index.d.ts.map +1 -0
  74. package/dist/cli/commands/watch/capabilities/index.js +28 -0
  75. package/dist/cli/commands/watch/capabilities/index.js.map +1 -0
  76. package/dist/cli/commands/watch/capabilities/monitor-email.d.ts +14 -0
  77. package/dist/cli/commands/watch/capabilities/monitor-email.d.ts.map +1 -0
  78. package/dist/cli/commands/watch/capabilities/monitor-email.js +54 -0
  79. package/dist/cli/commands/watch/capabilities/monitor-email.js.map +1 -0
  80. package/dist/cli/commands/watch/capabilities/monitor-teams.d.ts +14 -0
  81. package/dist/cli/commands/watch/capabilities/monitor-teams.d.ts.map +1 -0
  82. package/dist/cli/commands/watch/capabilities/monitor-teams.js +55 -0
  83. package/dist/cli/commands/watch/capabilities/monitor-teams.js.map +1 -0
  84. package/dist/cli/commands/watch/capabilities/retro.d.ts +14 -0
  85. package/dist/cli/commands/watch/capabilities/retro.d.ts.map +1 -0
  86. package/dist/cli/commands/watch/capabilities/retro.js +81 -0
  87. package/dist/cli/commands/watch/capabilities/retro.js.map +1 -0
  88. package/dist/cli/commands/watch/capabilities/self-pull.d.ts +14 -0
  89. package/dist/cli/commands/watch/capabilities/self-pull.d.ts.map +1 -0
  90. package/dist/cli/commands/watch/capabilities/self-pull.js +33 -0
  91. package/dist/cli/commands/watch/capabilities/self-pull.js.map +1 -0
  92. package/dist/cli/commands/watch/capabilities/two-pass.d.ts +14 -0
  93. package/dist/cli/commands/watch/capabilities/two-pass.d.ts.map +1 -0
  94. package/dist/cli/commands/watch/capabilities/two-pass.js +66 -0
  95. package/dist/cli/commands/watch/capabilities/two-pass.js.map +1 -0
  96. package/dist/cli/commands/watch/capabilities/wave-dispatch.d.ts +14 -0
  97. package/dist/cli/commands/watch/capabilities/wave-dispatch.d.ts.map +1 -0
  98. package/dist/cli/commands/watch/capabilities/wave-dispatch.js +117 -0
  99. package/dist/cli/commands/watch/capabilities/wave-dispatch.js.map +1 -0
  100. package/dist/cli/commands/watch/config.d.ts +25 -0
  101. package/dist/cli/commands/watch/config.d.ts.map +1 -0
  102. package/dist/cli/commands/watch/config.js +82 -0
  103. package/dist/cli/commands/watch/config.js.map +1 -0
  104. package/dist/cli/commands/watch/index.d.ts +95 -0
  105. package/dist/cli/commands/watch/index.d.ts.map +1 -0
  106. package/dist/cli/commands/watch/index.js +704 -0
  107. package/dist/cli/commands/watch/index.js.map +1 -0
  108. package/dist/cli/commands/watch/registry.d.ts +19 -0
  109. package/dist/cli/commands/watch/registry.d.ts.map +1 -0
  110. package/dist/cli/commands/watch/registry.js +28 -0
  111. package/dist/cli/commands/watch/registry.js.map +1 -0
  112. package/dist/cli/commands/watch/types.d.ts +57 -0
  113. package/dist/cli/commands/watch/types.d.ts.map +1 -0
  114. package/dist/cli/commands/watch/types.js +8 -0
  115. package/dist/cli/commands/watch/types.js.map +1 -0
  116. package/dist/cli/core/cast.d.ts.map +1 -1
  117. package/dist/cli/core/cast.js +15 -19
  118. package/dist/cli/core/cast.js.map +1 -1
  119. package/dist/cli/core/detect-squad-dir.d.ts.map +1 -1
  120. package/dist/cli/core/detect-squad-dir.js +12 -10
  121. package/dist/cli/core/detect-squad-dir.js.map +1 -1
  122. package/dist/cli/core/email-scrub.d.ts.map +1 -1
  123. package/dist/cli/core/email-scrub.js +12 -11
  124. package/dist/cli/core/email-scrub.js.map +1 -1
  125. package/dist/cli/core/gh-cli.d.ts +13 -0
  126. package/dist/cli/core/gh-cli.d.ts.map +1 -1
  127. package/dist/cli/core/gh-cli.js +24 -0
  128. package/dist/cli/core/gh-cli.js.map +1 -1
  129. package/dist/cli/core/init.d.ts +2 -0
  130. package/dist/cli/core/init.d.ts.map +1 -1
  131. package/dist/cli/core/init.js +22 -5
  132. package/dist/cli/core/init.js.map +1 -1
  133. package/dist/cli/core/migrate-directory.d.ts.map +1 -1
  134. package/dist/cli/core/migrate-directory.js +14 -13
  135. package/dist/cli/core/migrate-directory.js.map +1 -1
  136. package/dist/cli/core/migrations.d.ts.map +1 -1
  137. package/dist/cli/core/migrations.js +22 -8
  138. package/dist/cli/core/migrations.js.map +1 -1
  139. package/dist/cli/core/nap.d.ts.map +1 -1
  140. package/dist/cli/core/nap.js +111 -49
  141. package/dist/cli/core/nap.js.map +1 -1
  142. package/dist/cli/core/project-type.d.ts.map +1 -1
  143. package/dist/cli/core/project-type.js +11 -10
  144. package/dist/cli/core/project-type.js.map +1 -1
  145. package/dist/cli/core/team-md.d.ts.map +1 -1
  146. package/dist/cli/core/team-md.js +43 -38
  147. package/dist/cli/core/team-md.js.map +1 -1
  148. package/dist/cli/core/templates.d.ts.map +1 -1
  149. package/dist/cli/core/templates.js +4 -3
  150. package/dist/cli/core/templates.js.map +1 -1
  151. package/dist/cli/core/upgrade.d.ts.map +1 -1
  152. package/dist/cli/core/upgrade.js +68 -55
  153. package/dist/cli/core/upgrade.js.map +1 -1
  154. package/dist/cli/core/version.d.ts.map +1 -1
  155. package/dist/cli/core/version.js +8 -7
  156. package/dist/cli/core/version.js.map +1 -1
  157. package/dist/cli/index.d.ts +1 -1
  158. package/dist/cli/index.d.ts.map +1 -1
  159. package/dist/cli/index.js +1 -1
  160. package/dist/cli/index.js.map +1 -1
  161. package/dist/cli/self-update.d.ts.map +1 -1
  162. package/dist/cli/self-update.js +7 -4
  163. package/dist/cli/self-update.js.map +1 -1
  164. package/dist/cli/shell/agent-name-parser.d.ts +16 -0
  165. package/dist/cli/shell/agent-name-parser.d.ts.map +1 -0
  166. package/dist/cli/shell/agent-name-parser.js +54 -0
  167. package/dist/cli/shell/agent-name-parser.js.map +1 -0
  168. package/dist/cli/shell/commands.d.ts.map +1 -1
  169. package/dist/cli/shell/commands.js +4 -3
  170. package/dist/cli/shell/commands.js.map +1 -1
  171. package/dist/cli/shell/coordinator.d.ts +4 -1
  172. package/dist/cli/shell/coordinator.d.ts.map +1 -1
  173. package/dist/cli/shell/coordinator.js +29 -26
  174. package/dist/cli/shell/coordinator.js.map +1 -1
  175. package/dist/cli/shell/index.d.ts.map +1 -1
  176. package/dist/cli/shell/index.js +33 -35
  177. package/dist/cli/shell/index.js.map +1 -1
  178. package/dist/cli/shell/lifecycle.d.ts +13 -2
  179. package/dist/cli/shell/lifecycle.d.ts.map +1 -1
  180. package/dist/cli/shell/lifecycle.js +26 -13
  181. package/dist/cli/shell/lifecycle.js.map +1 -1
  182. package/dist/cli/shell/session-store.d.ts.map +1 -1
  183. package/dist/cli/shell/session-store.js +16 -12
  184. package/dist/cli/shell/session-store.js.map +1 -1
  185. package/dist/cli/shell/spawn.d.ts +4 -1
  186. package/dist/cli/shell/spawn.d.ts.map +1 -1
  187. package/dist/cli/shell/spawn.js +28 -10
  188. package/dist/cli/shell/spawn.js.map +1 -1
  189. package/dist/cli-entry.js +83 -12
  190. package/dist/cli-entry.js.map +1 -1
  191. package/package.json +8 -4
  192. package/scripts/patch-esm-imports.mjs +105 -105
  193. package/scripts/patch-ink-rendering.mjs +115 -115
  194. package/templates/casting/Futurama.json +9 -9
  195. package/templates/casting-history.json +4 -4
  196. package/templates/casting-policy.json +37 -37
  197. package/templates/casting-reference.md +104 -104
  198. package/templates/casting-registry.json +3 -3
  199. package/templates/ceremonies.md +41 -41
  200. package/templates/charter.md +53 -53
  201. package/templates/constraint-tracking.md +38 -38
  202. package/templates/cooperative-rate-limiting.md +229 -229
  203. package/templates/copilot-instructions.md +46 -46
  204. package/templates/history.md +10 -10
  205. package/templates/identity/now.md +9 -9
  206. package/templates/identity/wisdom.md +15 -15
  207. package/templates/issue-lifecycle.md +412 -412
  208. package/templates/keda-scaler.md +164 -164
  209. package/templates/machine-capabilities.md +74 -74
  210. package/templates/mcp-config.md +90 -90
  211. package/templates/multi-agent-format.md +28 -28
  212. package/templates/orchestration-log.md +27 -27
  213. package/templates/plugin-marketplace.md +49 -49
  214. package/templates/ralph-circuit-breaker.md +313 -313
  215. package/templates/raw-agent-output.md +37 -37
  216. package/templates/roster.md +60 -60
  217. package/templates/routing.md +39 -39
  218. package/templates/run-output.md +50 -50
  219. package/templates/scribe-charter.md +123 -119
  220. package/templates/skill.md +24 -24
  221. package/templates/skills/agent-collaboration/SKILL.md +42 -42
  222. package/templates/skills/agent-conduct/SKILL.md +24 -24
  223. package/templates/skills/architectural-proposals/SKILL.md +151 -151
  224. package/templates/skills/ci-validation-gates/SKILL.md +84 -84
  225. package/templates/skills/cli-wiring/SKILL.md +47 -47
  226. package/templates/skills/client-compatibility/SKILL.md +89 -89
  227. package/templates/skills/cross-machine-coordination/SKILL.md +434 -0
  228. package/templates/skills/cross-squad/SKILL.md +114 -114
  229. package/templates/skills/distributed-mesh/SKILL.md +287 -287
  230. package/templates/skills/distributed-mesh/mesh.json.example +30 -30
  231. package/templates/skills/distributed-mesh/sync-mesh.ps1 +111 -111
  232. package/templates/skills/distributed-mesh/sync-mesh.sh +104 -104
  233. package/templates/skills/docs-standards/SKILL.md +71 -71
  234. package/templates/skills/economy-mode/SKILL.md +114 -114
  235. package/templates/skills/error-recovery/SKILL.md +99 -0
  236. package/templates/skills/external-comms/SKILL.md +329 -329
  237. package/templates/skills/gh-auth-isolation/SKILL.md +183 -183
  238. package/templates/skills/git-workflow/SKILL.md +204 -204
  239. package/templates/skills/github-multi-account/SKILL.md +95 -95
  240. package/templates/skills/history-hygiene/SKILL.md +36 -36
  241. package/templates/skills/humanizer/SKILL.md +105 -105
  242. package/templates/skills/init-mode/SKILL.md +102 -102
  243. package/templates/skills/iterative-retrieval/SKILL.md +165 -0
  244. package/templates/skills/model-selection/SKILL.md +117 -117
  245. package/templates/skills/nap/SKILL.md +24 -24
  246. package/templates/skills/notification-routing/SKILL.md +105 -0
  247. package/templates/skills/personal-squad/SKILL.md +57 -57
  248. package/templates/skills/pr-screenshots/SKILL.md +149 -0
  249. package/templates/skills/ralph-two-pass-scan/SKILL.md +35 -0
  250. package/templates/skills/reflect/SKILL.md +229 -0
  251. package/templates/skills/release-process/SKILL.md +131 -423
  252. package/templates/skills/reskill/SKILL.md +92 -92
  253. package/templates/skills/retro-enforcement/SKILL.md +148 -0
  254. package/templates/skills/reviewer-protocol/SKILL.md +79 -79
  255. package/templates/skills/secret-handling/SKILL.md +200 -200
  256. package/templates/skills/session-recovery/SKILL.md +155 -155
  257. package/templates/skills/squad-conventions/SKILL.md +69 -69
  258. package/templates/skills/test-discipline/SKILL.md +37 -37
  259. package/templates/skills/tiered-memory/SKILL.md +234 -0
  260. package/templates/skills/windows-compatibility/SKILL.md +98 -74
  261. package/templates/{squad.agent.md → squad.agent.md.template} +1316 -1287
  262. package/templates/workflows/squad-ci.yml +24 -24
  263. package/templates/workflows/squad-docs.yml +54 -54
  264. package/templates/workflows/squad-heartbeat.yml +0 -4
  265. package/templates/workflows/squad-insider-release.yml +61 -61
  266. package/templates/workflows/squad-issue-assign.yml +161 -161
  267. package/templates/workflows/squad-label-enforce.yml +181 -181
  268. package/templates/workflows/squad-preview.yml +55 -55
  269. package/templates/workflows/squad-promote.yml +120 -120
  270. package/templates/workflows/squad-release.yml +77 -77
  271. package/templates/workflows/squad-triage.yml +260 -260
  272. package/templates/workflows/sync-squad-labels.yml +169 -169
  273. package/dist/cli/commands/watch.d.ts +0 -18
  274. package/dist/cli/commands/watch.d.ts.map +0 -1
  275. package/dist/cli/commands/watch.js +0 -306
  276. package/dist/cli/commands/watch.js.map +0 -1
@@ -0,0 +1,704 @@
1
+ /**
2
+ * Watch command — Ralph's standalone polling process.
3
+ *
4
+ * Thin orchestrator that delegates opt-in features to capabilities.
5
+ * Core triage logic (runCheck, checkPRs) remains inline because it
6
+ * always runs — it is not an opt-in capability.
7
+ */
8
+ import path from 'node:path';
9
+ import { execFile, execFileSync } from 'node:child_process';
10
+ import { promisify } from 'node:util';
11
+ import { FSStorageProvider } from '@bradygaster/squad-sdk';
12
+ const storage = new FSStorageProvider();
13
+ const execFileAsync = promisify(execFile);
14
+ import { detectSquadDir } from '../../core/detect-squad-dir.js';
15
+ import { fatal } from '../../core/errors.js';
16
+ import { GREEN, RED, DIM, BOLD, RESET, YELLOW } from '../../core/output.js';
17
+ import { parseRoutingRules, parseModuleOwnership, parseRoster, triageIssue, } from '@bradygaster/squad-sdk/ralph/triage';
18
+ import { RalphMonitor } from '@bradygaster/squad-sdk/ralph';
19
+ import { EventBus } from '@bradygaster/squad-sdk/runtime/event-bus';
20
+ import { ghAvailable, ghAuthenticated, ghRateLimitCheck, isRateLimitError } from '../../core/gh-cli.js';
21
+ import { PredictiveCircuitBreaker, getTrafficLight, } from '@bradygaster/squad-sdk/ralph/rate-limiting';
22
+ import { createPlatformAdapter } from '@bradygaster/squad-sdk/platform';
23
+ import { createDefaultRegistry } from './capabilities/index.js';
24
+ export { loadWatchConfig } from './config.js';
25
+ export { CapabilityRegistry } from './registry.js';
26
+ export { createDefaultRegistry } from './capabilities/index.js';
27
+ // ── SDK Mapping Helpers ──────────────────────────────────────────
28
+ function toWatchWorkItem(wi) {
29
+ return {
30
+ number: wi.id,
31
+ title: wi.title,
32
+ labels: wi.tags.map(t => ({ name: t })),
33
+ assignees: wi.assignedTo ? [{ login: wi.assignedTo }] : [],
34
+ };
35
+ }
36
+ function toWatchPullRequest(pr) {
37
+ return {
38
+ number: pr.id,
39
+ title: pr.title,
40
+ author: { login: pr.author },
41
+ labels: [],
42
+ isDraft: pr.status === 'draft',
43
+ reviewDecision: pr.reviewStatus === 'approved' ? 'APPROVED'
44
+ : pr.reviewStatus === 'changes-requested' ? 'CHANGES_REQUESTED'
45
+ : pr.reviewStatus === 'pending' ? 'REVIEW_REQUIRED' : '',
46
+ state: pr.status === 'active' ? 'OPEN'
47
+ : pr.status === 'completed' ? 'MERGED'
48
+ : pr.status === 'abandoned' ? 'CLOSED' : 'OPEN',
49
+ headRefName: pr.sourceBranch,
50
+ statusCheckRollup: [],
51
+ };
52
+ }
53
+ async function listWatchWorkItems(adapter, options) {
54
+ const tags = options.label ? [options.label] : undefined;
55
+ const items = await adapter.listWorkItems({ tags, state: options.state, limit: options.limit });
56
+ return items.map(toWatchWorkItem);
57
+ }
58
+ async function listWatchPullRequests(adapter, options) {
59
+ let status;
60
+ if (options.state === 'open')
61
+ status = 'active';
62
+ else if (options.state === 'closed')
63
+ status = 'abandoned';
64
+ else if (options.state === 'merged')
65
+ status = 'completed';
66
+ else
67
+ status = options.state;
68
+ const prs = await adapter.listPullRequests({ status, limit: options.limit });
69
+ return prs.map(toWatchPullRequest);
70
+ }
71
+ async function editWorkItem(adapter, id, options) {
72
+ if (options.addLabel)
73
+ await adapter.addTag(id, options.addLabel);
74
+ if (options.removeLabel)
75
+ await adapter.removeTag(id, options.removeLabel);
76
+ if (options.addAssignee) {
77
+ if (adapter.type === 'github') {
78
+ try {
79
+ await execFileAsync('gh', ['issue', 'edit', String(id), '--add-assignee', options.addAssignee]);
80
+ }
81
+ catch { /* best-effort */ }
82
+ }
83
+ else if (adapter.type === 'azure-devops') {
84
+ const assignee = options.addAssignee === '@me' ? '' : options.addAssignee;
85
+ if (assignee) {
86
+ try {
87
+ execFileSync('az', [
88
+ 'boards', 'work-item', 'update',
89
+ '--id', String(id),
90
+ '--fields', `System.AssignedTo=${assignee}`,
91
+ '--output', 'json',
92
+ ], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
93
+ }
94
+ catch { /* best-effort */ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ export function reportBoard(state, round) {
100
+ const total = Object.values(state).reduce((a, b) => a + b, 0);
101
+ if (total === 0) {
102
+ console.log(`${DIM}📋 Board is clear — Ralph is idling${RESET}`);
103
+ return;
104
+ }
105
+ console.log(`\n${BOLD}🔄 Ralph — Round ${round}${RESET}`);
106
+ console.log('━'.repeat(30));
107
+ if (state.untriaged > 0)
108
+ console.log(` 🔴 Untriaged: ${state.untriaged}`);
109
+ if (state.assigned > 0)
110
+ console.log(` 🟡 Assigned: ${state.assigned}`);
111
+ if (state.drafts > 0)
112
+ console.log(` 🟡 Draft PRs: ${state.drafts}`);
113
+ if (state.changesRequested > 0)
114
+ console.log(` ⚠️ Changes requested: ${state.changesRequested}`);
115
+ if (state.ciFailures > 0)
116
+ console.log(` ❌ CI failures: ${state.ciFailures}`);
117
+ if (state.needsReview > 0)
118
+ console.log(` 🔵 Needs review: ${state.needsReview}`);
119
+ if (state.readyToMerge > 0)
120
+ console.log(` 🟢 Ready to merge: ${state.readyToMerge}`);
121
+ if (state.executed > 0)
122
+ console.log(` 🚀 Executed: ${state.executed}`);
123
+ console.log();
124
+ }
125
+ function emptyBoardState() {
126
+ return { untriaged: 0, assigned: 0, drafts: 0, needsReview: 0, changesRequested: 0, ciFailures: 0, readyToMerge: 0, executed: 0 };
127
+ }
128
+ async function checkPRs(roster, adapter) {
129
+ const timestamp = new Date().toLocaleTimeString();
130
+ const prs = await listWatchPullRequests(adapter, { state: 'open', limit: 20 });
131
+ const squadPRs = prs.filter(pr => pr.labels.some(l => l.name.startsWith('squad')) || pr.headRefName.startsWith('squad/'));
132
+ if (squadPRs.length === 0) {
133
+ return { drafts: 0, needsReview: 0, changesRequested: 0, ciFailures: 0, readyToMerge: 0, totalOpen: 0 };
134
+ }
135
+ const drafts = squadPRs.filter(pr => pr.isDraft);
136
+ const changesRequested = squadPRs.filter(pr => pr.reviewDecision === 'CHANGES_REQUESTED');
137
+ const approved = squadPRs.filter(pr => pr.reviewDecision === 'APPROVED' && !pr.isDraft);
138
+ const ciFailures = squadPRs.filter(pr => pr.statusCheckRollup?.some(check => check.state === 'FAILURE' || check.state === 'ERROR'));
139
+ const readyToMerge = approved.filter(pr => !pr.statusCheckRollup?.some(c => c.state === 'FAILURE' || c.state === 'ERROR' || c.state === 'PENDING'));
140
+ const changesRequestedSet = new Set(changesRequested.map(pr => pr.number));
141
+ const ciFailureSet = new Set(ciFailures.map(pr => pr.number));
142
+ const readyToMergeSet = new Set(readyToMerge.map(pr => pr.number));
143
+ const needsReview = squadPRs.filter(pr => !pr.isDraft && !changesRequestedSet.has(pr.number) && !ciFailureSet.has(pr.number) && !readyToMergeSet.has(pr.number));
144
+ const memberNames = new Set(roster.map(m => m.name.toLowerCase()));
145
+ if (drafts.length > 0) {
146
+ console.log(`${DIM}[${timestamp}]${RESET} 🟡 ${drafts.length} draft PR(s) in progress`);
147
+ for (const pr of drafts)
148
+ console.log(` ${DIM}PR #${pr.number}: ${pr.title} (${pr.author.login})${RESET}`);
149
+ }
150
+ if (changesRequested.length > 0) {
151
+ console.log(`${YELLOW}[${timestamp}]${RESET} ⚠️ ${changesRequested.length} PR(s) need revision`);
152
+ for (const pr of changesRequested) {
153
+ const owner = memberNames.has(pr.author.login.toLowerCase()) ? ` — ${pr.author.login}` : '';
154
+ console.log(` PR #${pr.number}: ${pr.title} — changes requested${owner}`);
155
+ }
156
+ }
157
+ if (ciFailures.length > 0) {
158
+ console.log(`${RED}[${timestamp}]${RESET} ❌ ${ciFailures.length} PR(s) with CI failures`);
159
+ for (const pr of ciFailures) {
160
+ const failedChecks = pr.statusCheckRollup?.filter(c => c.state === 'FAILURE' || c.state === 'ERROR') || [];
161
+ const owner = memberNames.has(pr.author.login.toLowerCase()) ? ` — ${pr.author.login}` : '';
162
+ console.log(` PR #${pr.number}: ${pr.title}${owner} — ${failedChecks.map(c => c.name).join(', ')}`);
163
+ }
164
+ }
165
+ if (readyToMerge.length > 0) {
166
+ console.log(`${GREEN}[${timestamp}]${RESET} 🟢 ${readyToMerge.length} PR(s) ready to merge`);
167
+ for (const pr of readyToMerge)
168
+ console.log(` PR #${pr.number}: ${pr.title} — approved, CI green`);
169
+ }
170
+ return {
171
+ drafts: drafts.length,
172
+ needsReview: needsReview.length,
173
+ changesRequested: changesRequestedSet.size,
174
+ ciFailures: ciFailureSet.size,
175
+ readyToMerge: readyToMergeSet.size,
176
+ totalOpen: squadPRs.length,
177
+ };
178
+ }
179
+ // ── Core triage (always runs) ────────────────────────────────────
180
+ const BLOCKED_LABELS = new Set([
181
+ 'status:blocked', 'status:waiting-external', 'status:postponed',
182
+ 'status:scheduled', 'status:needs-action', 'status:needs-decision',
183
+ 'status:needs-review', 'pending-user', 'do-not-merge',
184
+ ]);
185
+ async function runCheck(rules, modules, roster, hasCopilot, autoAssign, capabilities, adapter) {
186
+ const timestamp = new Date().toLocaleTimeString();
187
+ try {
188
+ const issues = await listWatchWorkItems(adapter, { label: 'squad', state: 'open', limit: 20 });
189
+ const { filterByCapabilities } = await import('@bradygaster/squad-sdk/ralph/capabilities');
190
+ const { handled: capableIssues, skipped: incapableIssues } = filterByCapabilities(issues, capabilities);
191
+ for (const { issue, missing } of incapableIssues) {
192
+ console.log(`${DIM}[${timestamp}] ⏭️ Skipping #${issue.number} "${issue.title}" — missing: ${missing.join(', ')}${RESET}`);
193
+ }
194
+ const memberLabels = roster.map(m => m.label);
195
+ const untriaged = capableIssues.filter(issue => {
196
+ const issueLabels = issue.labels.map(l => l.name);
197
+ return !memberLabels.some(ml => issueLabels.includes(ml));
198
+ });
199
+ const assignedIssues = capableIssues.filter(issue => {
200
+ const issueLabels = issue.labels.map(l => l.name);
201
+ return memberLabels.some(ml => issueLabels.includes(ml));
202
+ });
203
+ let unassignedCopilot = [];
204
+ if (hasCopilot && autoAssign) {
205
+ try {
206
+ const copilotIssues = await listWatchWorkItems(adapter, { label: 'squad:copilot', state: 'open', limit: 10 });
207
+ unassignedCopilot = copilotIssues.filter(i => !i.assignees || i.assignees.length === 0);
208
+ }
209
+ catch { /* label may not exist */ }
210
+ }
211
+ for (const issue of untriaged) {
212
+ const triageInput = {
213
+ number: issue.number,
214
+ title: issue.title,
215
+ body: issue.body,
216
+ labels: issue.labels.map(l => l.name),
217
+ };
218
+ const triage = triageIssue(triageInput, rules, modules, roster);
219
+ if (triage) {
220
+ try {
221
+ await editWorkItem(adapter, issue.number, { addLabel: triage.agent.label });
222
+ console.log(`${GREEN}✓${RESET} [${timestamp}] Triaged #${issue.number} "${issue.title}" → ${triage.agent.name} (${triage.reason})`);
223
+ }
224
+ catch (e) {
225
+ console.error(`${RED}✗${RESET} [${timestamp}] Failed to label #${issue.number}: ${e.message}`);
226
+ }
227
+ }
228
+ }
229
+ for (const issue of unassignedCopilot) {
230
+ try {
231
+ await editWorkItem(adapter, issue.number, { addAssignee: 'copilot-swe-agent' });
232
+ console.log(`${GREEN}✓${RESET} [${timestamp}] Assigned @copilot to #${issue.number} "${issue.title}"`);
233
+ }
234
+ catch (e) {
235
+ console.error(`${RED}✗${RESET} [${timestamp}] Failed to assign @copilot to #${issue.number}: ${e.message}`);
236
+ }
237
+ }
238
+ const prState = await checkPRs(roster, adapter);
239
+ return { untriaged: untriaged.length, assigned: assignedIssues.length, executed: 0, ...prState };
240
+ }
241
+ catch (e) {
242
+ console.error(`${RED}✗${RESET} [${timestamp}] Check failed: ${e.message}`);
243
+ return emptyBoardState();
244
+ }
245
+ }
246
+ function discoverSubSquads(teamRoot) {
247
+ const subsquadDir = path.join(teamRoot, '.squad', 'subsquads');
248
+ if (!storage.existsSync(subsquadDir))
249
+ return [];
250
+ try {
251
+ const entries = storage.listSync?.(subsquadDir) ?? [];
252
+ const dirs = Array.isArray(entries) ? entries : [];
253
+ const squads = [];
254
+ for (const entry of dirs) {
255
+ const entryPath = path.join(subsquadDir, entry);
256
+ const teamMdPath = path.join(entryPath, 'team.md');
257
+ if (!storage.existsSync(teamMdPath))
258
+ continue;
259
+ const routingPath = path.join(entryPath, 'routing.md');
260
+ let labels = [];
261
+ if (storage.existsSync(routingPath)) {
262
+ try {
263
+ const content = storage.readSync(routingPath) ?? '';
264
+ const labelMatches = content.match(/label[s]?:\s*([^\n]+)/gi);
265
+ if (labelMatches) {
266
+ labels = labelMatches
267
+ .flatMap((m) => m.replace(/labels?:\s*/i, '').split(','))
268
+ .map((l) => l.trim())
269
+ .filter(Boolean);
270
+ }
271
+ }
272
+ catch { /* best-effort */ }
273
+ }
274
+ squads.push({ name: entry, dir: entryPath, labels });
275
+ }
276
+ return squads;
277
+ }
278
+ catch {
279
+ return [];
280
+ }
281
+ }
282
+ function defaultCBState() {
283
+ return {
284
+ status: 'closed', openedAt: null, cooldownMinutes: 2,
285
+ consecutiveFailures: 0, consecutiveSuccesses: 0,
286
+ lastRateLimitRemaining: null, lastRateLimitTotal: null,
287
+ };
288
+ }
289
+ function loadCBState(squadDir) {
290
+ const filePath = path.join(squadDir, 'ralph-circuit-breaker.json');
291
+ try {
292
+ const raw = storage.readSync(filePath);
293
+ if (!raw)
294
+ return defaultCBState();
295
+ return JSON.parse(raw);
296
+ }
297
+ catch {
298
+ return defaultCBState();
299
+ }
300
+ }
301
+ function saveCBState(squadDir, state) {
302
+ storage.writeSync(path.join(squadDir, 'ralph-circuit-breaker.json'), JSON.stringify(state, null, 2));
303
+ }
304
+ // ── Capability Phase Runner ──────────────────────────────────────
305
+ async function runPhase(phase, enabled, context, config) {
306
+ const results = new Map();
307
+ const phaseCapabilities = enabled.filter(c => c.phase === phase);
308
+ const ts = new Date().toLocaleTimeString();
309
+ for (const cap of phaseCapabilities) {
310
+ try {
311
+ const capConfig = config.capabilities[cap.name];
312
+ const capContext = {
313
+ ...context,
314
+ config: typeof capConfig === 'object' && capConfig !== null
315
+ ? capConfig
316
+ : { enabled: !!capConfig, maxConcurrent: config.maxConcurrent, timeout: config.timeout },
317
+ };
318
+ const result = await cap.execute(capContext);
319
+ results.set(cap.name, result);
320
+ if (!result.success) {
321
+ console.log(`${YELLOW}⚠${RESET} [${ts}] ${cap.name}: ${result.summary}`);
322
+ }
323
+ }
324
+ catch (e) {
325
+ const result = { success: false, summary: `${cap.name} crashed: ${e.message}` };
326
+ results.set(cap.name, result);
327
+ console.log(`${YELLOW}⚠${RESET} [${ts}] ${cap.name}: ${result.summary}`);
328
+ }
329
+ }
330
+ return results;
331
+ }
332
+ /** Preflight all capabilities, return only those that pass. */
333
+ async function preflightCapabilities(registry, config, context) {
334
+ const enabled = [];
335
+ const skipped = [];
336
+ for (const cap of registry.all()) {
337
+ // Check if this capability is enabled in config
338
+ const capConfig = config.capabilities[cap.name];
339
+ if (!capConfig)
340
+ continue;
341
+ const capContext = {
342
+ ...context,
343
+ config: typeof capConfig === 'object' && capConfig !== null
344
+ ? capConfig
345
+ : {},
346
+ };
347
+ try {
348
+ const result = await cap.preflight(capContext);
349
+ if (result.ok) {
350
+ enabled.push(cap);
351
+ }
352
+ else {
353
+ skipped.push({ name: cap.name, reason: result.reason ?? 'preflight failed' });
354
+ }
355
+ }
356
+ catch (e) {
357
+ skipped.push({ name: cap.name, reason: e.message });
358
+ }
359
+ }
360
+ // Print startup banner
361
+ if (enabled.length > 0) {
362
+ console.log(`${GREEN}✅${RESET} Capabilities: ${enabled.map(c => c.name).join(', ')}`);
363
+ }
364
+ if (skipped.length > 0) {
365
+ for (const s of skipped) {
366
+ console.log(`${YELLOW}⚠️${RESET} ${s.name} skipped: ${s.reason}`);
367
+ }
368
+ }
369
+ return enabled;
370
+ }
371
+ /** Convert legacy WatchOptions to WatchConfig. */
372
+ function legacyToConfig(options) {
373
+ const capabilities = {};
374
+ if (options.execute)
375
+ capabilities['execute'] = true;
376
+ if (options.monitorTeams)
377
+ capabilities['monitor-teams'] = true;
378
+ if (options.monitorEmail)
379
+ capabilities['monitor-email'] = true;
380
+ if (options.board)
381
+ capabilities['board'] = { projectNumber: options.boardProject ?? 1 };
382
+ if (options.twoPass)
383
+ capabilities['two-pass'] = true;
384
+ if (options.waveDispatch)
385
+ capabilities['wave-dispatch'] = true;
386
+ if (options.retro)
387
+ capabilities['retro'] = true;
388
+ if (options.decisionHygiene)
389
+ capabilities['decision-hygiene'] = true;
390
+ return {
391
+ interval: options.intervalMinutes,
392
+ execute: options.execute ?? false,
393
+ maxConcurrent: options.maxConcurrent ?? 1,
394
+ timeout: options.issueTimeoutMinutes ?? 30,
395
+ copilotFlags: options.copilotFlags,
396
+ agentCmd: options.agentCmd,
397
+ capabilities,
398
+ };
399
+ }
400
+ // ── Exported helpers (backward compat) ───────────────────────────
401
+ export { findExecutableIssues } from './capabilities/execute.js';
402
+ export function buildAgentCommand(issue, teamRoot, options) {
403
+ const prompt = `Work on issue #${issue.number}: ${issue.title}. Read the issue body for full details.`;
404
+ if (options.agentCmd) {
405
+ const parts = options.agentCmd.trim().split(/\s+/);
406
+ return { cmd: parts[0], args: [...parts.slice(1), '--message', prompt] };
407
+ }
408
+ const args = ['copilot', '--message', prompt];
409
+ if (options.copilotFlags)
410
+ args.push(...options.copilotFlags.trim().split(/\s+/));
411
+ return { cmd: 'gh', args };
412
+ }
413
+ export async function selfPull(teamRoot) {
414
+ try {
415
+ await new Promise((resolve, reject) => {
416
+ execFile('git', ['fetch', '--quiet'], { cwd: teamRoot }, (err) => (err ? reject(err) : resolve()));
417
+ });
418
+ await new Promise((resolve, reject) => {
419
+ execFile('git', ['pull', '--ff-only', '--quiet'], { cwd: teamRoot }, (err) => (err ? reject(err) : resolve()));
420
+ });
421
+ }
422
+ catch {
423
+ console.log(`${DIM}⚠ selfPull: git pull skipped (not on a tracking branch or conflicts)${RESET}`);
424
+ }
425
+ }
426
+ export async function executeIssue(issue, teamRoot, options, adapter) {
427
+ const ts = new Date().toLocaleTimeString();
428
+ const timeoutMs = (options.issueTimeoutMinutes ?? 30) * 60_000;
429
+ try {
430
+ await editWorkItem(adapter, issue.number, { addAssignee: '@me' });
431
+ }
432
+ catch { /* best-effort */ }
433
+ try {
434
+ await adapter.addComment(issue.number, '🤖 Ralph: starting autonomous work on this issue.');
435
+ }
436
+ catch { /* best-effort */ }
437
+ const { cmd, args } = buildAgentCommand(issue, teamRoot, options);
438
+ console.log(`${GREEN}▶${RESET} [${ts}] Executing #${issue.number} "${issue.title}" → ${cmd} ${args.join(' ')}`);
439
+ return new Promise((resolve) => {
440
+ execFile(cmd, args, { cwd: teamRoot, timeout: timeoutMs, maxBuffer: 50 * 1024 * 1024 }, (err) => {
441
+ if (err) {
442
+ const execErr = err;
443
+ const msg = execErr.killed ? `Timed out after ${options.issueTimeoutMinutes ?? 30}m` : execErr.message;
444
+ console.error(`${RED}✗${RESET} [${new Date().toLocaleTimeString()}] #${issue.number} failed: ${msg}`);
445
+ resolve({ success: false, error: msg });
446
+ }
447
+ else {
448
+ console.log(`${GREEN}✓${RESET} [${new Date().toLocaleTimeString()}] #${issue.number} completed`);
449
+ resolve({ success: true });
450
+ }
451
+ });
452
+ });
453
+ }
454
+ // ── Main Entry Point ─────────────────────────────────────────────
455
+ /**
456
+ * Run watch command — Ralph's local polling process.
457
+ *
458
+ * Accepts either the new {@link WatchConfig} or the legacy
459
+ * {@link WatchOptions} bag for backward compatibility.
460
+ */
461
+ export async function runWatch(dest, options) {
462
+ // Normalize to WatchConfig
463
+ const config = 'intervalMinutes' in options
464
+ ? legacyToConfig(options)
465
+ : options;
466
+ const { interval } = config;
467
+ if (isNaN(interval) || interval < 1) {
468
+ fatal('--interval must be a positive number of minutes');
469
+ }
470
+ // Detect squad directory
471
+ const squadDirInfo = detectSquadDir(dest);
472
+ const teamMd = path.join(squadDirInfo.path, 'team.md');
473
+ const routingMdPath = path.join(squadDirInfo.path, 'routing.md');
474
+ const teamRoot = path.dirname(squadDirInfo.path);
475
+ if (!storage.existsSync(teamMd)) {
476
+ fatal('No squad found — run init first.');
477
+ }
478
+ // Create platform adapter
479
+ let adapter;
480
+ try {
481
+ adapter = createPlatformAdapter(teamRoot);
482
+ console.log(`${DIM}Platform: ${adapter.type}${RESET}`);
483
+ }
484
+ catch (err) {
485
+ return fatal(`Could not detect platform: ${err.message}`);
486
+ }
487
+ // Verify platform CLI availability
488
+ if (adapter.type === 'github') {
489
+ if (!(await ghAvailable()))
490
+ fatal('gh CLI not found — install from https://cli.github.com');
491
+ if (!(await ghAuthenticated()))
492
+ fatal('gh CLI not authenticated — run: gh auth login');
493
+ }
494
+ else if (adapter.type === 'azure-devops') {
495
+ try {
496
+ await execFileAsync('az', ['devops', '-h']);
497
+ }
498
+ catch {
499
+ fatal('az CLI not found');
500
+ }
501
+ try {
502
+ await execFileAsync('az', ['account', 'show']);
503
+ }
504
+ catch {
505
+ fatal('az CLI not authenticated — run: az login');
506
+ }
507
+ }
508
+ // Parse team.md
509
+ const content = storage.readSync(teamMd) ?? '';
510
+ const roster = parseRoster(content);
511
+ const routingContent = storage.existsSync(routingMdPath) ? (storage.readSync(routingMdPath) ?? '') : '';
512
+ const rules = parseRoutingRules(routingContent);
513
+ const modules = parseModuleOwnership(routingContent);
514
+ // Load machine capabilities (#514)
515
+ const { loadCapabilities } = await import('@bradygaster/squad-sdk/ralph/capabilities');
516
+ const capabilities = await loadCapabilities(teamRoot);
517
+ if (capabilities) {
518
+ console.log(`${DIM}📦 Machine: ${capabilities.machine} — ${capabilities.capabilities.length} capabilities loaded${RESET}`);
519
+ }
520
+ if (roster.length === 0) {
521
+ fatal('No squad members found in team.md');
522
+ }
523
+ const hasCopilot = content.includes('🤖 Coding Agent') || content.includes('@copilot');
524
+ const autoAssign = content.includes('<!-- copilot-auto-assign: true -->');
525
+ const monitorSessionId = 'ralph-watch';
526
+ const eventBus = new EventBus();
527
+ const monitor = new RalphMonitor({
528
+ teamRoot,
529
+ healthCheckInterval: interval * 60 * 1000,
530
+ staleSessionThreshold: interval * 60 * 1000 * 3,
531
+ statePath: path.join(squadDirInfo.path, '.ralph-state.json'),
532
+ });
533
+ await monitor.start(eventBus);
534
+ await eventBus.emit({
535
+ type: 'session:created', sessionId: monitorSessionId,
536
+ agentName: 'Ralph', payload: { interval }, timestamp: new Date(),
537
+ });
538
+ // ── Capability system setup ────────────────────────────────────
539
+ const registry = createDefaultRegistry();
540
+ const baseContext = {
541
+ teamRoot,
542
+ adapter,
543
+ round: 0,
544
+ roster: roster.map(r => ({ name: r.name, label: r.label, expertise: [] })),
545
+ config: {},
546
+ agentCmd: config.agentCmd,
547
+ copilotFlags: config.copilotFlags,
548
+ };
549
+ const enabledCapabilities = await preflightCapabilities(registry, config, baseContext);
550
+ // Print startup banner
551
+ const modeTag = config.execute ? ` ${BOLD}(Execute)${RESET}` : '';
552
+ const platformTag = ` [${adapter.type}]`;
553
+ console.log(`\n${BOLD}🔄 Ralph — Watch Mode${RESET}${modeTag}${platformTag}`);
554
+ console.log(`${DIM}Polling every ${interval} minute(s) for squad work. Ctrl+C to stop.${RESET}`);
555
+ if (config.execute && config.copilotFlags) {
556
+ console.log(`${DIM}Copilot flags: ${config.copilotFlags}${RESET}`);
557
+ }
558
+ if (config.execute) {
559
+ console.log(`${DIM}Max concurrent: ${config.maxConcurrent} | Timeout: ${config.timeout}m${RESET}`);
560
+ }
561
+ console.log();
562
+ // Initialize circuit breaker (#515)
563
+ const circuitBreaker = new PredictiveCircuitBreaker();
564
+ let cbState = loadCBState(squadDirInfo.path);
565
+ let round = 0;
566
+ let roundInProgress = false;
567
+ async function executeRound() {
568
+ const ts = new Date().toLocaleTimeString();
569
+ // Circuit breaker gate
570
+ if (cbState.status === 'open') {
571
+ const elapsed = Date.now() - new Date(cbState.openedAt).getTime();
572
+ if (elapsed < cbState.cooldownMinutes * 60_000) {
573
+ const left = Math.ceil((cbState.cooldownMinutes * 60_000 - elapsed) / 1000);
574
+ console.log(`${YELLOW}⏸${RESET} [${ts}] Circuit open — cooling down (${left}s left)`);
575
+ return;
576
+ }
577
+ cbState.status = 'half-open';
578
+ console.log(`${DIM}[${ts}] Circuit half-open — probing...${RESET}`);
579
+ saveCBState(squadDirInfo.path, cbState);
580
+ }
581
+ // Rate limit check (GitHub only)
582
+ if (adapter.type === 'github') {
583
+ try {
584
+ const rl = await ghRateLimitCheck();
585
+ if (rl) {
586
+ cbState.lastRateLimitRemaining = rl.remaining;
587
+ cbState.lastRateLimitTotal = rl.limit;
588
+ circuitBreaker.addSample(rl.remaining, rl.limit);
589
+ const light = getTrafficLight(rl.remaining, rl.limit);
590
+ if (light === 'red' || circuitBreaker.shouldOpen()) {
591
+ cbState.status = 'open';
592
+ cbState.openedAt = new Date().toISOString();
593
+ cbState.consecutiveFailures++;
594
+ cbState.consecutiveSuccesses = 0;
595
+ cbState.cooldownMinutes = Math.min(cbState.cooldownMinutes * 2, 30);
596
+ saveCBState(squadDirInfo.path, cbState);
597
+ console.log(`${RED}🛑${RESET} [${ts}] Circuit opened — quota ${light === 'red' ? 'critical' : 'predicted low'} (${rl.remaining}/${rl.limit})`);
598
+ return;
599
+ }
600
+ if (light === 'amber') {
601
+ console.log(`${YELLOW}⚠️${RESET} [${ts}] Quota amber (${rl.remaining}/${rl.limit}) — proceeding cautiously`);
602
+ }
603
+ }
604
+ }
605
+ catch { /* proceed anyway */ }
606
+ }
607
+ round++;
608
+ const roundContext = { ...baseContext, round };
609
+ // Phase 1: pre-scan (self-pull, subsquad discovery)
610
+ await runPhase('pre-scan', enabledCapabilities, roundContext, config);
611
+ // SubSquad discovery (informational, not a capability)
612
+ const subSquads = discoverSubSquads(teamRoot);
613
+ if (subSquads.length > 0 && round === 1) {
614
+ console.log(`${DIM}📂 Discovered ${subSquads.length} subsquad(s): ${subSquads.map(s => s.name).join(', ')}${RESET}`);
615
+ }
616
+ // Core: triage (always runs — not a capability)
617
+ const roundState = await runCheck(rules, modules, roster, hasCopilot, autoAssign, capabilities, adapter);
618
+ // Phase 2: post-triage (two-pass hydration)
619
+ await runPhase('post-triage', enabledCapabilities, roundContext, config);
620
+ // Phase 3: post-execute (execute issues, wave dispatch, board updates)
621
+ const execResults = await runPhase('post-execute', enabledCapabilities, roundContext, config);
622
+ // Update executed count from execute capability
623
+ const execResult = execResults.get('execute');
624
+ if (execResult?.data?.['executed']) {
625
+ roundState.executed = execResult.data['executed'];
626
+ }
627
+ // Phase 4: housekeeping (monitoring, retro, decision hygiene)
628
+ await runPhase('housekeeping', enabledCapabilities, roundContext, config);
629
+ await eventBus.emit({
630
+ type: 'agent:milestone', sessionId: monitorSessionId,
631
+ agentName: 'Ralph',
632
+ payload: { milestone: `Completed watch round ${round}`, task: 'watch cycle' },
633
+ timestamp: new Date(),
634
+ });
635
+ await monitor.healthCheck();
636
+ reportBoard(roundState, round);
637
+ // Post-round: update circuit breaker on success
638
+ if (cbState.status === 'half-open') {
639
+ cbState.consecutiveSuccesses++;
640
+ if (cbState.consecutiveSuccesses >= 2) {
641
+ cbState.status = 'closed';
642
+ cbState.cooldownMinutes = 2;
643
+ cbState.consecutiveFailures = 0;
644
+ console.log(`${GREEN}✓${RESET} [${new Date().toLocaleTimeString()}] Circuit closed — quota recovered`);
645
+ }
646
+ }
647
+ else {
648
+ cbState.consecutiveSuccesses = 0;
649
+ cbState.consecutiveFailures = 0;
650
+ }
651
+ saveCBState(squadDirInfo.path, cbState);
652
+ }
653
+ // Run immediately, then on interval
654
+ await executeRound();
655
+ return new Promise((resolve) => {
656
+ const intervalId = setInterval(async () => {
657
+ if (roundInProgress)
658
+ return;
659
+ roundInProgress = true;
660
+ try {
661
+ await executeRound();
662
+ }
663
+ catch (e) {
664
+ const err = e;
665
+ if (adapter.type === 'github' && isRateLimitError(err)) {
666
+ cbState.status = 'open';
667
+ cbState.openedAt = new Date().toISOString();
668
+ cbState.consecutiveFailures++;
669
+ cbState.consecutiveSuccesses = 0;
670
+ cbState.cooldownMinutes = Math.min(cbState.cooldownMinutes * 2, 30);
671
+ saveCBState(squadDirInfo.path, cbState);
672
+ console.log(`${RED}🛑${RESET} Rate limited — circuit opened, cooldown ${cbState.cooldownMinutes}m`);
673
+ }
674
+ else {
675
+ console.error(`${RED}✗${RESET} Round error: ${err.message}`);
676
+ }
677
+ }
678
+ finally {
679
+ roundInProgress = false;
680
+ }
681
+ }, interval * 60 * 1000);
682
+ // Graceful shutdown
683
+ let isShuttingDown = false;
684
+ const shutdown = async () => {
685
+ if (isShuttingDown)
686
+ return;
687
+ isShuttingDown = true;
688
+ clearInterval(intervalId);
689
+ process.off('SIGINT', shutdown);
690
+ process.off('SIGTERM', shutdown);
691
+ await eventBus.emit({
692
+ type: 'session:destroyed', sessionId: monitorSessionId,
693
+ agentName: 'Ralph', payload: null, timestamp: new Date(),
694
+ });
695
+ await monitor.stop();
696
+ saveCBState(squadDirInfo.path, cbState);
697
+ console.log(`\n${DIM}🔄 Ralph — Watch stopped${RESET}`);
698
+ resolve();
699
+ };
700
+ process.on('SIGINT', shutdown);
701
+ process.on('SIGTERM', shutdown);
702
+ });
703
+ }
704
+ //# sourceMappingURL=index.js.map