@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.
- package/README.md +329 -329
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +10 -10
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/config.d.ts +12 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +157 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/consult.d.ts.map +1 -1
- package/dist/cli/commands/consult.js +9 -4
- package/dist/cli/commands/consult.js.map +1 -1
- package/dist/cli/commands/copilot.d.ts.map +1 -1
- package/dist/cli/commands/copilot.js +8 -7
- package/dist/cli/commands/copilot.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +50 -17
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/economy.d.ts.map +1 -1
- package/dist/cli/commands/economy.js +3 -2
- package/dist/cli/commands/economy.js.map +1 -1
- package/dist/cli/commands/export.d.ts.map +1 -1
- package/dist/cli/commands/export.js +22 -16
- package/dist/cli/commands/export.js.map +1 -1
- package/dist/cli/commands/extract.d.ts.map +1 -1
- package/dist/cli/commands/extract.js +14 -10
- package/dist/cli/commands/extract.js.map +1 -1
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +21 -18
- package/dist/cli/commands/import.js.map +1 -1
- package/dist/cli/commands/init-remote.d.ts.map +1 -1
- package/dist/cli/commands/init-remote.js +7 -6
- package/dist/cli/commands/init-remote.js.map +1 -1
- package/dist/cli/commands/link.d.ts.map +1 -1
- package/dist/cli/commands/link.js +11 -10
- package/dist/cli/commands/link.js.map +1 -1
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +19 -18
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/personal.d.ts.map +1 -1
- package/dist/cli/commands/personal.js +57 -65
- package/dist/cli/commands/personal.js.map +1 -1
- package/dist/cli/commands/plugin.d.ts.map +1 -1
- package/dist/cli/commands/plugin.js +8 -7
- package/dist/cli/commands/plugin.js.map +1 -1
- package/dist/cli/commands/rc.d.ts.map +1 -1
- package/dist/cli/commands/rc.js +19 -12
- package/dist/cli/commands/rc.js.map +1 -1
- package/dist/cli/commands/schedule.d.ts.map +1 -1
- package/dist/cli/commands/schedule.js +6 -5
- package/dist/cli/commands/schedule.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +18 -11
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/streams.d.ts.map +1 -1
- package/dist/cli/commands/streams.js +3 -2
- package/dist/cli/commands/streams.js.map +1 -1
- package/dist/cli/commands/upstream.d.ts.map +1 -1
- package/dist/cli/commands/upstream.js +23 -19
- package/dist/cli/commands/upstream.js.map +1 -1
- package/dist/cli/commands/watch/capabilities/board.d.ts +22 -0
- package/dist/cli/commands/watch/capabilities/board.d.ts.map +1 -0
- package/dist/cli/commands/watch/capabilities/board.js +121 -0
- package/dist/cli/commands/watch/capabilities/board.js.map +1 -0
- package/dist/cli/commands/watch/capabilities/decision-hygiene.d.ts +14 -0
- package/dist/cli/commands/watch/capabilities/decision-hygiene.d.ts.map +1 -0
- package/dist/cli/commands/watch/capabilities/decision-hygiene.js +72 -0
- package/dist/cli/commands/watch/capabilities/decision-hygiene.js.map +1 -0
- package/dist/cli/commands/watch/capabilities/execute.d.ts +33 -0
- package/dist/cli/commands/watch/capabilities/execute.d.ts.map +1 -0
- package/dist/cli/commands/watch/capabilities/execute.js +119 -0
- package/dist/cli/commands/watch/capabilities/execute.js.map +1 -0
- package/dist/cli/commands/watch/capabilities/index.d.ts +7 -0
- package/dist/cli/commands/watch/capabilities/index.d.ts.map +1 -0
- package/dist/cli/commands/watch/capabilities/index.js +28 -0
- package/dist/cli/commands/watch/capabilities/index.js.map +1 -0
- package/dist/cli/commands/watch/capabilities/monitor-email.d.ts +14 -0
- package/dist/cli/commands/watch/capabilities/monitor-email.d.ts.map +1 -0
- package/dist/cli/commands/watch/capabilities/monitor-email.js +54 -0
- package/dist/cli/commands/watch/capabilities/monitor-email.js.map +1 -0
- package/dist/cli/commands/watch/capabilities/monitor-teams.d.ts +14 -0
- package/dist/cli/commands/watch/capabilities/monitor-teams.d.ts.map +1 -0
- package/dist/cli/commands/watch/capabilities/monitor-teams.js +55 -0
- package/dist/cli/commands/watch/capabilities/monitor-teams.js.map +1 -0
- package/dist/cli/commands/watch/capabilities/retro.d.ts +14 -0
- package/dist/cli/commands/watch/capabilities/retro.d.ts.map +1 -0
- package/dist/cli/commands/watch/capabilities/retro.js +81 -0
- package/dist/cli/commands/watch/capabilities/retro.js.map +1 -0
- package/dist/cli/commands/watch/capabilities/self-pull.d.ts +14 -0
- package/dist/cli/commands/watch/capabilities/self-pull.d.ts.map +1 -0
- package/dist/cli/commands/watch/capabilities/self-pull.js +33 -0
- package/dist/cli/commands/watch/capabilities/self-pull.js.map +1 -0
- package/dist/cli/commands/watch/capabilities/two-pass.d.ts +14 -0
- package/dist/cli/commands/watch/capabilities/two-pass.d.ts.map +1 -0
- package/dist/cli/commands/watch/capabilities/two-pass.js +66 -0
- package/dist/cli/commands/watch/capabilities/two-pass.js.map +1 -0
- package/dist/cli/commands/watch/capabilities/wave-dispatch.d.ts +14 -0
- package/dist/cli/commands/watch/capabilities/wave-dispatch.d.ts.map +1 -0
- package/dist/cli/commands/watch/capabilities/wave-dispatch.js +117 -0
- package/dist/cli/commands/watch/capabilities/wave-dispatch.js.map +1 -0
- package/dist/cli/commands/watch/config.d.ts +25 -0
- package/dist/cli/commands/watch/config.d.ts.map +1 -0
- package/dist/cli/commands/watch/config.js +82 -0
- package/dist/cli/commands/watch/config.js.map +1 -0
- package/dist/cli/commands/watch/index.d.ts +95 -0
- package/dist/cli/commands/watch/index.d.ts.map +1 -0
- package/dist/cli/commands/watch/index.js +704 -0
- package/dist/cli/commands/watch/index.js.map +1 -0
- package/dist/cli/commands/watch/registry.d.ts +19 -0
- package/dist/cli/commands/watch/registry.d.ts.map +1 -0
- package/dist/cli/commands/watch/registry.js +28 -0
- package/dist/cli/commands/watch/registry.js.map +1 -0
- package/dist/cli/commands/watch/types.d.ts +57 -0
- package/dist/cli/commands/watch/types.d.ts.map +1 -0
- package/dist/cli/commands/watch/types.js +8 -0
- package/dist/cli/commands/watch/types.js.map +1 -0
- package/dist/cli/core/cast.d.ts.map +1 -1
- package/dist/cli/core/cast.js +15 -19
- package/dist/cli/core/cast.js.map +1 -1
- package/dist/cli/core/detect-squad-dir.d.ts.map +1 -1
- package/dist/cli/core/detect-squad-dir.js +12 -10
- package/dist/cli/core/detect-squad-dir.js.map +1 -1
- package/dist/cli/core/email-scrub.d.ts.map +1 -1
- package/dist/cli/core/email-scrub.js +12 -11
- package/dist/cli/core/email-scrub.js.map +1 -1
- package/dist/cli/core/gh-cli.d.ts +13 -0
- package/dist/cli/core/gh-cli.d.ts.map +1 -1
- package/dist/cli/core/gh-cli.js +24 -0
- package/dist/cli/core/gh-cli.js.map +1 -1
- package/dist/cli/core/init.d.ts +2 -0
- package/dist/cli/core/init.d.ts.map +1 -1
- package/dist/cli/core/init.js +22 -5
- package/dist/cli/core/init.js.map +1 -1
- package/dist/cli/core/migrate-directory.d.ts.map +1 -1
- package/dist/cli/core/migrate-directory.js +14 -13
- package/dist/cli/core/migrate-directory.js.map +1 -1
- package/dist/cli/core/migrations.d.ts.map +1 -1
- package/dist/cli/core/migrations.js +22 -8
- package/dist/cli/core/migrations.js.map +1 -1
- package/dist/cli/core/nap.d.ts.map +1 -1
- package/dist/cli/core/nap.js +111 -49
- package/dist/cli/core/nap.js.map +1 -1
- package/dist/cli/core/project-type.d.ts.map +1 -1
- package/dist/cli/core/project-type.js +11 -10
- package/dist/cli/core/project-type.js.map +1 -1
- package/dist/cli/core/team-md.d.ts.map +1 -1
- package/dist/cli/core/team-md.js +43 -38
- package/dist/cli/core/team-md.js.map +1 -1
- package/dist/cli/core/templates.d.ts.map +1 -1
- package/dist/cli/core/templates.js +4 -3
- package/dist/cli/core/templates.js.map +1 -1
- package/dist/cli/core/upgrade.d.ts.map +1 -1
- package/dist/cli/core/upgrade.js +68 -55
- package/dist/cli/core/upgrade.js.map +1 -1
- package/dist/cli/core/version.d.ts.map +1 -1
- package/dist/cli/core/version.js +8 -7
- package/dist/cli/core/version.js.map +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/self-update.d.ts.map +1 -1
- package/dist/cli/self-update.js +7 -4
- package/dist/cli/self-update.js.map +1 -1
- package/dist/cli/shell/agent-name-parser.d.ts +16 -0
- package/dist/cli/shell/agent-name-parser.d.ts.map +1 -0
- package/dist/cli/shell/agent-name-parser.js +54 -0
- package/dist/cli/shell/agent-name-parser.js.map +1 -0
- package/dist/cli/shell/commands.d.ts.map +1 -1
- package/dist/cli/shell/commands.js +4 -3
- package/dist/cli/shell/commands.js.map +1 -1
- package/dist/cli/shell/coordinator.d.ts +4 -1
- package/dist/cli/shell/coordinator.d.ts.map +1 -1
- package/dist/cli/shell/coordinator.js +29 -26
- package/dist/cli/shell/coordinator.js.map +1 -1
- package/dist/cli/shell/index.d.ts.map +1 -1
- package/dist/cli/shell/index.js +33 -35
- package/dist/cli/shell/index.js.map +1 -1
- package/dist/cli/shell/lifecycle.d.ts +13 -2
- package/dist/cli/shell/lifecycle.d.ts.map +1 -1
- package/dist/cli/shell/lifecycle.js +26 -13
- package/dist/cli/shell/lifecycle.js.map +1 -1
- package/dist/cli/shell/session-store.d.ts.map +1 -1
- package/dist/cli/shell/session-store.js +16 -12
- package/dist/cli/shell/session-store.js.map +1 -1
- package/dist/cli/shell/spawn.d.ts +4 -1
- package/dist/cli/shell/spawn.d.ts.map +1 -1
- package/dist/cli/shell/spawn.js +28 -10
- package/dist/cli/shell/spawn.js.map +1 -1
- package/dist/cli-entry.js +83 -12
- package/dist/cli-entry.js.map +1 -1
- package/package.json +8 -4
- package/scripts/patch-esm-imports.mjs +105 -105
- package/scripts/patch-ink-rendering.mjs +115 -115
- package/templates/casting/Futurama.json +9 -9
- package/templates/casting-history.json +4 -4
- package/templates/casting-policy.json +37 -37
- package/templates/casting-reference.md +104 -104
- package/templates/casting-registry.json +3 -3
- package/templates/ceremonies.md +41 -41
- package/templates/charter.md +53 -53
- package/templates/constraint-tracking.md +38 -38
- package/templates/cooperative-rate-limiting.md +229 -229
- package/templates/copilot-instructions.md +46 -46
- package/templates/history.md +10 -10
- package/templates/identity/now.md +9 -9
- package/templates/identity/wisdom.md +15 -15
- package/templates/issue-lifecycle.md +412 -412
- package/templates/keda-scaler.md +164 -164
- package/templates/machine-capabilities.md +74 -74
- package/templates/mcp-config.md +90 -90
- package/templates/multi-agent-format.md +28 -28
- package/templates/orchestration-log.md +27 -27
- package/templates/plugin-marketplace.md +49 -49
- package/templates/ralph-circuit-breaker.md +313 -313
- package/templates/raw-agent-output.md +37 -37
- package/templates/roster.md +60 -60
- package/templates/routing.md +39 -39
- package/templates/run-output.md +50 -50
- package/templates/scribe-charter.md +123 -119
- package/templates/skill.md +24 -24
- package/templates/skills/agent-collaboration/SKILL.md +42 -42
- package/templates/skills/agent-conduct/SKILL.md +24 -24
- package/templates/skills/architectural-proposals/SKILL.md +151 -151
- package/templates/skills/ci-validation-gates/SKILL.md +84 -84
- package/templates/skills/cli-wiring/SKILL.md +47 -47
- package/templates/skills/client-compatibility/SKILL.md +89 -89
- package/templates/skills/cross-machine-coordination/SKILL.md +434 -0
- package/templates/skills/cross-squad/SKILL.md +114 -114
- package/templates/skills/distributed-mesh/SKILL.md +287 -287
- package/templates/skills/distributed-mesh/mesh.json.example +30 -30
- package/templates/skills/distributed-mesh/sync-mesh.ps1 +111 -111
- package/templates/skills/distributed-mesh/sync-mesh.sh +104 -104
- package/templates/skills/docs-standards/SKILL.md +71 -71
- package/templates/skills/economy-mode/SKILL.md +114 -114
- package/templates/skills/error-recovery/SKILL.md +99 -0
- package/templates/skills/external-comms/SKILL.md +329 -329
- package/templates/skills/gh-auth-isolation/SKILL.md +183 -183
- package/templates/skills/git-workflow/SKILL.md +204 -204
- package/templates/skills/github-multi-account/SKILL.md +95 -95
- package/templates/skills/history-hygiene/SKILL.md +36 -36
- package/templates/skills/humanizer/SKILL.md +105 -105
- package/templates/skills/init-mode/SKILL.md +102 -102
- package/templates/skills/iterative-retrieval/SKILL.md +165 -0
- package/templates/skills/model-selection/SKILL.md +117 -117
- package/templates/skills/nap/SKILL.md +24 -24
- package/templates/skills/notification-routing/SKILL.md +105 -0
- package/templates/skills/personal-squad/SKILL.md +57 -57
- package/templates/skills/pr-screenshots/SKILL.md +149 -0
- package/templates/skills/ralph-two-pass-scan/SKILL.md +35 -0
- package/templates/skills/reflect/SKILL.md +229 -0
- package/templates/skills/release-process/SKILL.md +131 -423
- package/templates/skills/reskill/SKILL.md +92 -92
- package/templates/skills/retro-enforcement/SKILL.md +148 -0
- package/templates/skills/reviewer-protocol/SKILL.md +79 -79
- package/templates/skills/secret-handling/SKILL.md +200 -200
- package/templates/skills/session-recovery/SKILL.md +155 -155
- package/templates/skills/squad-conventions/SKILL.md +69 -69
- package/templates/skills/test-discipline/SKILL.md +37 -37
- package/templates/skills/tiered-memory/SKILL.md +234 -0
- package/templates/skills/windows-compatibility/SKILL.md +98 -74
- package/templates/{squad.agent.md → squad.agent.md.template} +1316 -1287
- package/templates/workflows/squad-ci.yml +24 -24
- package/templates/workflows/squad-docs.yml +54 -54
- package/templates/workflows/squad-heartbeat.yml +0 -4
- package/templates/workflows/squad-insider-release.yml +61 -61
- package/templates/workflows/squad-issue-assign.yml +161 -161
- package/templates/workflows/squad-label-enforce.yml +181 -181
- package/templates/workflows/squad-preview.yml +55 -55
- package/templates/workflows/squad-promote.yml +120 -120
- package/templates/workflows/squad-release.yml +77 -77
- package/templates/workflows/squad-triage.yml +260 -260
- package/templates/workflows/sync-squad-labels.yml +169 -169
- package/dist/cli/commands/watch.d.ts +0 -18
- package/dist/cli/commands/watch.d.ts.map +0 -1
- package/dist/cli/commands/watch.js +0 -306
- 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
|