@mizyoel/mercury-mesh 0.9.4
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/.copilot/mcp-config.json +14 -0
- package/.copilot/skills/agent-collaboration/SKILL.md +42 -0
- package/.copilot/skills/agent-conduct/SKILL.md +24 -0
- package/.copilot/skills/architectural-proposals/SKILL.md +151 -0
- package/.copilot/skills/ci-validation-gates/SKILL.md +84 -0
- package/.copilot/skills/cli-wiring/SKILL.md +47 -0
- package/.copilot/skills/client-compatibility/SKILL.md +89 -0
- package/.copilot/skills/cross-mesh/SKILL.md +114 -0
- package/.copilot/skills/distributed-mesh/SKILL.md +287 -0
- package/.copilot/skills/distributed-mesh/mesh.json.example +30 -0
- package/.copilot/skills/distributed-mesh/sync-mesh.ps1 +111 -0
- package/.copilot/skills/distributed-mesh/sync-mesh.sh +104 -0
- package/.copilot/skills/docs-standards/SKILL.md +71 -0
- package/.copilot/skills/economy-mode/SKILL.md +101 -0
- package/.copilot/skills/external-comms/SKILL.md +331 -0
- package/.copilot/skills/gh-auth-isolation/SKILL.md +183 -0
- package/.copilot/skills/git-workflow/SKILL.md +206 -0
- package/.copilot/skills/github-multi-account/SKILL.md +95 -0
- package/.copilot/skills/history-hygiene/SKILL.md +36 -0
- package/.copilot/skills/humanizer/SKILL.md +107 -0
- package/.copilot/skills/init-mode/SKILL.md +101 -0
- package/.copilot/skills/mesh-conventions/SKILL.md +69 -0
- package/.copilot/skills/model-selection/SKILL.md +139 -0
- package/.copilot/skills/nap/SKILL.md +24 -0
- package/.copilot/skills/personal-mesh/SKILL.md +57 -0
- package/.copilot/skills/project-conventions/SKILL.md +56 -0
- package/.copilot/skills/release-process/SKILL.md +435 -0
- package/.copilot/skills/reskill/SKILL.md +92 -0
- package/.copilot/skills/reviewer-protocol/SKILL.md +79 -0
- package/.copilot/skills/secret-handling/SKILL.md +200 -0
- package/.copilot/skills/session-recovery/SKILL.md +155 -0
- package/.copilot/skills/test-discipline/SKILL.md +37 -0
- package/.copilot/skills/windows-compatibility/SKILL.md +74 -0
- package/.github/agents/mercury-mesh.agent.md +1732 -0
- package/.mesh/manifesto.md +66 -0
- package/.mesh/templates/casting/Futurama.json +10 -0
- package/.mesh/templates/casting-history.json +4 -0
- package/.mesh/templates/casting-policy.json +37 -0
- package/.mesh/templates/casting-reference.md +104 -0
- package/.mesh/templates/casting-registry.json +3 -0
- package/.mesh/templates/ceremonies.md +41 -0
- package/.mesh/templates/charter.md +56 -0
- package/.mesh/templates/constraint-tracking.md +38 -0
- package/.mesh/templates/cooperative-rate-limiting.md +229 -0
- package/.mesh/templates/copilot-instructions.md +50 -0
- package/.mesh/templates/department-backlog.md +15 -0
- package/.mesh/templates/department-charter.md +27 -0
- package/.mesh/templates/department-state.json +19 -0
- package/.mesh/templates/history.md +10 -0
- package/.mesh/templates/identity/now.md +9 -0
- package/.mesh/templates/identity/wisdom.md +15 -0
- package/.mesh/templates/interface-contract.md +26 -0
- package/.mesh/templates/issue-lifecycle.md +421 -0
- package/.mesh/templates/keda-scaler.md +166 -0
- package/.mesh/templates/machine-capabilities.md +77 -0
- package/.mesh/templates/mcp-config.md +90 -0
- package/.mesh/templates/mercury-mesh.agent.md +1732 -0
- package/.mesh/templates/multi-agent-format.md +28 -0
- package/.mesh/templates/orchestration-log.md +27 -0
- package/.mesh/templates/org-autonomy-spec.md +152 -0
- package/.mesh/templates/org-backlog-from-triage.js +199 -0
- package/.mesh/templates/org-runtime-reconcile.js +364 -0
- package/.mesh/templates/org-seed-runtime.js +238 -0
- package/.mesh/templates/org-status.js +193 -0
- package/.mesh/templates/org-structure.json +38 -0
- package/.mesh/templates/package.json +3 -0
- package/.mesh/templates/plugin-marketplace.md +49 -0
- package/.mesh/templates/ralph-circuit-breaker.md +313 -0
- package/.mesh/templates/ralph-triage.js +844 -0
- package/.mesh/templates/raw-agent-output.md +37 -0
- package/.mesh/templates/roster.md +60 -0
- package/.mesh/templates/routing.md +78 -0
- package/.mesh/templates/run-output.md +50 -0
- package/.mesh/templates/schedule.json +64 -0
- package/.mesh/templates/scribe-charter.md +119 -0
- package/.mesh/templates/skill.md +24 -0
- package/.mesh/templates/skills/agent-collaboration/SKILL.md +42 -0
- package/.mesh/templates/skills/agent-conduct/SKILL.md +24 -0
- package/.mesh/templates/skills/architectural-proposals/SKILL.md +151 -0
- package/.mesh/templates/skills/ci-validation-gates/SKILL.md +84 -0
- package/.mesh/templates/skills/cli-wiring/SKILL.md +47 -0
- package/.mesh/templates/skills/client-compatibility/SKILL.md +89 -0
- package/.mesh/templates/skills/cross-mesh/SKILL.md +114 -0
- package/.mesh/templates/skills/distributed-mesh/SKILL.md +287 -0
- package/.mesh/templates/skills/distributed-mesh/mesh.json.example +30 -0
- package/.mesh/templates/skills/distributed-mesh/sync-mesh.ps1 +111 -0
- package/.mesh/templates/skills/distributed-mesh/sync-mesh.sh +104 -0
- package/.mesh/templates/skills/docs-standards/SKILL.md +71 -0
- package/.mesh/templates/skills/economy-mode/SKILL.md +101 -0
- package/.mesh/templates/skills/external-comms/SKILL.md +331 -0
- package/.mesh/templates/skills/gh-auth-isolation/SKILL.md +183 -0
- package/.mesh/templates/skills/git-workflow/SKILL.md +204 -0
- package/.mesh/templates/skills/github-multi-account/SKILL.md +95 -0
- package/.mesh/templates/skills/history-hygiene/SKILL.md +36 -0
- package/.mesh/templates/skills/humanizer/SKILL.md +107 -0
- package/.mesh/templates/skills/init-mode/SKILL.md +101 -0
- package/.mesh/templates/skills/mesh-conventions/SKILL.md +69 -0
- package/.mesh/templates/skills/model-selection/SKILL.md +139 -0
- package/.mesh/templates/skills/nap/SKILL.md +24 -0
- package/.mesh/templates/skills/personal-mesh/SKILL.md +57 -0
- package/.mesh/templates/skills/project-conventions/SKILL.md +56 -0
- package/.mesh/templates/skills/release-process/SKILL.md +435 -0
- package/.mesh/templates/skills/reskill/SKILL.md +92 -0
- package/.mesh/templates/skills/reviewer-protocol/SKILL.md +79 -0
- package/.mesh/templates/skills/secret-handling/SKILL.md +200 -0
- package/.mesh/templates/skills/session-recovery/SKILL.md +155 -0
- package/.mesh/templates/skills/test-discipline/SKILL.md +37 -0
- package/.mesh/templates/skills/windows-compatibility/SKILL.md +74 -0
- package/.mesh/templates/workflows/mesh-ci.yml +24 -0
- package/.mesh/templates/workflows/mesh-docs.yml +54 -0
- package/.mesh/templates/workflows/mesh-heartbeat.yml +237 -0
- package/.mesh/templates/workflows/mesh-insider-release.yml +61 -0
- package/.mesh/templates/workflows/mesh-issue-assign.yml +243 -0
- package/.mesh/templates/workflows/mesh-label-enforce.yml +181 -0
- package/.mesh/templates/workflows/mesh-preview.yml +55 -0
- package/.mesh/templates/workflows/mesh-promote.yml +120 -0
- package/.mesh/templates/workflows/mesh-release.yml +77 -0
- package/.mesh/templates/workflows/mesh-triage.yml +383 -0
- package/.mesh/templates/workflows/sync-mesh-labels.yml +204 -0
- package/README.md +640 -0
- package/bin/mercury-mesh.cjs +317 -0
- package/docs/brand-language.md +287 -0
- package/docs/commander-onboarding.md +462 -0
- package/docs/mercury-mesh-runtime-rename-impact.md +148 -0
- package/docs/persona-manifesto.md +114 -0
- package/docs/scenarios/client-compatibility.md +59 -0
- package/index.cjs +41 -0
- package/package.json +43 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const RUNTIME_DIR_CANDIDATES = ['.mesh', '.mercury'];
|
|
8
|
+
|
|
9
|
+
function defaultRuntimeDir() {
|
|
10
|
+
return RUNTIME_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) || '.mesh';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function runtimeDirName(runtimeDir) {
|
|
14
|
+
return path.basename(path.resolve(runtimeDir));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const options = {
|
|
19
|
+
meshDir: defaultRuntimeDir(),
|
|
20
|
+
output: 'org-status.json',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
24
|
+
const arg = argv[index];
|
|
25
|
+
if (arg === '--mesh-dir') {
|
|
26
|
+
options.meshDir = argv[index + 1];
|
|
27
|
+
index += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === '--output') {
|
|
31
|
+
options.output = argv[index + 1];
|
|
32
|
+
index += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (arg === '--help' || arg === '-h') {
|
|
36
|
+
printUsage();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printUsage() {
|
|
46
|
+
console.log('Usage: node <runtime>/org/status.js [--mesh-dir .mesh] --output org-status.json');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readJson(filePath) {
|
|
50
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeJson(filePath, value) {
|
|
54
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
55
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseMarkdownTable(content) {
|
|
59
|
+
const lines = content.split(/\r?\n/);
|
|
60
|
+
const rows = [];
|
|
61
|
+
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) continue;
|
|
65
|
+
const cells = trimmed.slice(1, -1).split('|').map((cell) => cell.trim());
|
|
66
|
+
if (cells.every((cell) => /^-+$/.test(cell))) continue;
|
|
67
|
+
rows.push(cells);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (rows.length < 2) return null;
|
|
71
|
+
return { headers: rows[0], rows: rows.slice(1) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function summarizeBacklog(backlogPath) {
|
|
75
|
+
if (!fs.existsSync(backlogPath)) {
|
|
76
|
+
return { exists: false, counts: {}, blocked: [] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const table = parseMarkdownTable(fs.readFileSync(backlogPath, 'utf8'));
|
|
80
|
+
if (!table) {
|
|
81
|
+
return { exists: true, counts: {}, blocked: [], warning: 'Backlog table not found' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const headerIndex = new Map(table.headers.map((header, index) => [header.toLowerCase(), index]));
|
|
85
|
+
const idIndex = headerIndex.get('id');
|
|
86
|
+
const statusIndex = headerIndex.get('status');
|
|
87
|
+
const notesIndex = headerIndex.get('notes');
|
|
88
|
+
const counts = {};
|
|
89
|
+
const blocked = [];
|
|
90
|
+
|
|
91
|
+
for (const row of table.rows) {
|
|
92
|
+
const status = statusIndex >= 0 ? row[statusIndex] : 'unknown';
|
|
93
|
+
counts[status] = (counts[status] || 0) + 1;
|
|
94
|
+
if (status === 'blocked') {
|
|
95
|
+
blocked.push({
|
|
96
|
+
id: idIndex >= 0 ? row[idIndex] : 'unknown',
|
|
97
|
+
notes: notesIndex >= 0 ? row[notesIndex] : '',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { exists: true, counts, blocked };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function summarizeState(statePath, heartbeatMinutes) {
|
|
106
|
+
if (!fs.existsSync(statePath)) {
|
|
107
|
+
return { exists: false, activeClaims: 0, staleHeartbeat: false, parallelismBreach: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const state = readJson(statePath);
|
|
111
|
+
const activeClaims = Array.isArray(state.activeClaims) ? state.activeClaims : [];
|
|
112
|
+
const heartbeat = state.lastHeartbeatAt ? new Date(state.lastHeartbeatAt) : null;
|
|
113
|
+
const staleHeartbeat = !heartbeat || Number.isNaN(heartbeat.valueOf())
|
|
114
|
+
? true
|
|
115
|
+
: (Date.now() - heartbeat.valueOf()) > heartbeatMinutes * 60 * 1000;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
exists: true,
|
|
119
|
+
activeClaims: activeClaims.length,
|
|
120
|
+
staleHeartbeat,
|
|
121
|
+
lastHeartbeatAt: state.lastHeartbeatAt || null,
|
|
122
|
+
blockedCount: Array.isArray(state.blocked) ? state.blocked.length : 0,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function main() {
|
|
127
|
+
const options = parseArgs(process.argv.slice(2));
|
|
128
|
+
const meshDir = path.resolve(options.meshDir);
|
|
129
|
+
const runtimeName = runtimeDirName(meshDir);
|
|
130
|
+
const repoRoot = path.dirname(meshDir);
|
|
131
|
+
const configPath = path.join(meshDir, 'config.json');
|
|
132
|
+
const structurePath = path.join(meshDir, 'org', 'structure.json');
|
|
133
|
+
const contractsDir = path.join(meshDir, 'org', 'contracts');
|
|
134
|
+
|
|
135
|
+
if (!fs.existsSync(configPath)) {
|
|
136
|
+
throw new Error(`config.json not found: ${configPath}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const config = readJson(configPath);
|
|
140
|
+
if (!config.orgMode || !fs.existsSync(structurePath)) {
|
|
141
|
+
writeJson(path.resolve(options.output), {
|
|
142
|
+
enabled: false,
|
|
143
|
+
reason: `orgMode disabled or ${runtimeName}/org/structure.json missing`,
|
|
144
|
+
departments: [],
|
|
145
|
+
contracts: [],
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const structure = readJson(structurePath);
|
|
151
|
+
const departments = Array.isArray(structure.departments) ? structure.departments : [];
|
|
152
|
+
const heartbeatMinutes = (config.orgConfig && config.orgConfig.heartbeatMinutes) || 15;
|
|
153
|
+
const maxParallelismDefault = (config.orgConfig && config.orgConfig.maxParallelismPerDepartment) || 3;
|
|
154
|
+
|
|
155
|
+
const departmentReports = departments.map((department) => {
|
|
156
|
+
const runtime = department.runtime || {};
|
|
157
|
+
const backlogPath = path.resolve(repoRoot, runtime.backlogPath || `${runtimeName}/org/${department.id}/backlog.md`);
|
|
158
|
+
const statePath = path.resolve(repoRoot, runtime.statePath || `${runtimeName}/org/${department.id}/state.json`);
|
|
159
|
+
const backlog = summarizeBacklog(backlogPath);
|
|
160
|
+
const state = summarizeState(statePath, runtime.heartbeatMinutes || heartbeatMinutes);
|
|
161
|
+
const maxParallelism = runtime.maxParallelism || maxParallelismDefault;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
id: department.id,
|
|
165
|
+
name: department.name,
|
|
166
|
+
lead: department.lead,
|
|
167
|
+
backlogPath: path.relative(repoRoot, backlogPath),
|
|
168
|
+
statePath: path.relative(repoRoot, statePath),
|
|
169
|
+
contracts: runtime.contracts || [],
|
|
170
|
+
backlog,
|
|
171
|
+
state: {
|
|
172
|
+
...state,
|
|
173
|
+
maxParallelism,
|
|
174
|
+
parallelismBreach: state.activeClaims > maxParallelism,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const contracts = fs.existsSync(contractsDir)
|
|
180
|
+
? fs.readdirSync(contractsDir)
|
|
181
|
+
.filter((fileName) => fileName.endsWith('.md'))
|
|
182
|
+
.map((fileName) => path.posix.join(runtimeName, 'org', 'contracts', fileName))
|
|
183
|
+
: [];
|
|
184
|
+
|
|
185
|
+
writeJson(path.resolve(options.output), {
|
|
186
|
+
enabled: true,
|
|
187
|
+
generatedAt: new Date().toISOString(),
|
|
188
|
+
departments: departmentReports,
|
|
189
|
+
contracts,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"departments": [
|
|
3
|
+
{
|
|
4
|
+
"id": "frontend",
|
|
5
|
+
"name": "Frontend",
|
|
6
|
+
"lead": "{CastName}",
|
|
7
|
+
"members": ["{CastName}"],
|
|
8
|
+
"domain": ["ui", "components", "pages", "design-system"],
|
|
9
|
+
"routingKeywords": ["ui", "component", "page", "css", "react"],
|
|
10
|
+
"leadStyle": "player-coach",
|
|
11
|
+
"authority": {
|
|
12
|
+
"canDecideLocally": [
|
|
13
|
+
"component boundaries",
|
|
14
|
+
"local naming",
|
|
15
|
+
"test strategy inside department"
|
|
16
|
+
],
|
|
17
|
+
"mustEscalate": [
|
|
18
|
+
"cross-department API changes",
|
|
19
|
+
"shared architecture changes",
|
|
20
|
+
"roster changes"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"runtime": {
|
|
24
|
+
"autonomyMode": "delegated",
|
|
25
|
+
"maxParallelism": 3,
|
|
26
|
+
"claimLeaseMinutes": 30,
|
|
27
|
+
"heartbeatMinutes": 15,
|
|
28
|
+
"backlogPath": ".mesh/org/frontend/backlog.md",
|
|
29
|
+
"statePath": ".mesh/org/frontend/state.json",
|
|
30
|
+
"contracts": [".mesh/org/contracts/ui-api.md"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"crossDepartment": {
|
|
35
|
+
"strategy": "contract-first",
|
|
36
|
+
"escalation": "lead-alignment"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Plugin Marketplace
|
|
2
|
+
|
|
3
|
+
Plugins are curated agent templates, skills, instructions, and prompts shared by the community via GitHub repositories (e.g., `github/awesome-copilot`, `anthropics/skills`). They provide ready-made expertise for common domains — cloud platforms, frameworks, testing strategies, etc.
|
|
4
|
+
|
|
5
|
+
## Marketplace State
|
|
6
|
+
|
|
7
|
+
Registered marketplace sources are stored in `.mesh/plugins/marketplaces.json`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"marketplaces": [
|
|
12
|
+
{
|
|
13
|
+
"name": "awesome-copilot",
|
|
14
|
+
"source": "github/awesome-copilot",
|
|
15
|
+
"added_at": "2026-02-14T00:00:00Z"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## CLI Commands
|
|
22
|
+
|
|
23
|
+
Users manage marketplaces via the CLI:
|
|
24
|
+
- `Mercury Mesh plugin marketplace add {owner/repo}` — Register a GitHub repo as a marketplace source
|
|
25
|
+
- `Mercury Mesh plugin marketplace remove {name}` — Remove a registered marketplace
|
|
26
|
+
- `Mercury Mesh plugin marketplace list` — List registered marketplaces
|
|
27
|
+
- `Mercury Mesh plugin marketplace browse {name}` — List available plugins in a marketplace
|
|
28
|
+
|
|
29
|
+
## When to Browse
|
|
30
|
+
|
|
31
|
+
During the **Adding Team Members** flow, AFTER allocating a name but BEFORE generating the charter:
|
|
32
|
+
|
|
33
|
+
1. Read `.mesh/plugins/marketplaces.json`. If the file doesn't exist or `marketplaces` is empty, skip silently.
|
|
34
|
+
2. For each registered marketplace, search for plugins whose name or description matches the new member's role or domain keywords.
|
|
35
|
+
3. Present matching plugins to the user: *"Found '{plugin-name}' in {marketplace} marketplace — want me to install it as a skill for {CastName}?"*
|
|
36
|
+
4. If the user accepts, install the plugin (see below). If they decline or skip, proceed without it.
|
|
37
|
+
|
|
38
|
+
## How to Install a Plugin
|
|
39
|
+
|
|
40
|
+
1. Read the plugin content from the marketplace repository (the plugin's `SKILL.md` or equivalent).
|
|
41
|
+
2. Copy it into the agent's skills directory: `.mesh/skills/{plugin-name}/SKILL.md`
|
|
42
|
+
3. If the plugin includes charter-level instructions (role boundaries, tool preferences), merge those into the agent's `charter.md`.
|
|
43
|
+
4. Log the installation in the agent's `history.md`: *"📦 Plugin '{plugin-name}' installed from {marketplace}."*
|
|
44
|
+
|
|
45
|
+
## Graceful Degradation
|
|
46
|
+
|
|
47
|
+
- **No marketplaces configured:** Skip the marketplace check entirely. No warning, no prompt.
|
|
48
|
+
- **Marketplace unreachable:** Warn the user (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and proceed with team member creation normally.
|
|
49
|
+
- **No matching plugins:** Inform the user (*"No matching plugins found in configured marketplaces"*) and proceed.
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Ralph Circuit Breaker — Model Rate Limit Fallback
|
|
2
|
+
|
|
3
|
+
> Classic circuit breaker pattern (Hystrix / Polly / Resilience4j) applied to Copilot model selection.
|
|
4
|
+
> When the preferred model hits rate limits, Ralph automatically degrades to free-tier models, then self-heals.
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
When running multiple Ralph instances across repos, Copilot model rate limits cause cascading failures.
|
|
9
|
+
All Ralphs fail simultaneously when the preferred model (e.g., `claude-sonnet-4.6`) hits quota.
|
|
10
|
+
|
|
11
|
+
Premium models burn quota fast:
|
|
12
|
+
| Model | Multiplier | Risk |
|
|
13
|
+
|-------|-----------|------|
|
|
14
|
+
| `claude-sonnet-4.6` | 1x | Moderate with many Ralphs |
|
|
15
|
+
| `claude-opus-4.6` | 10x | High |
|
|
16
|
+
| `gpt-5.4` | 50x | Very high |
|
|
17
|
+
| `gpt-5.4-mini` | **0x** | **Free — unlimited** |
|
|
18
|
+
| `gpt-5-mini` | **0x** | **Free — unlimited** |
|
|
19
|
+
| `gpt-4.1` | **0x** | **Free — unlimited** |
|
|
20
|
+
|
|
21
|
+
## Circuit Breaker States
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
┌─────────┐ rate limit error ┌────────┐
|
|
25
|
+
│ CLOSED │ ───────────────────► │ OPEN │
|
|
26
|
+
│ (normal)│ │(fallback)│
|
|
27
|
+
└────┬────┘ ◄──────────────── └────┬────┘
|
|
28
|
+
│ 2 consecutive │
|
|
29
|
+
│ successes │ cooldown expires
|
|
30
|
+
│ ▼
|
|
31
|
+
│ ┌──────────┐
|
|
32
|
+
└───── success ◄──────── │HALF-OPEN │
|
|
33
|
+
(close) │ (testing) │
|
|
34
|
+
└──────────┘
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### CLOSED (normal operation)
|
|
38
|
+
- Use preferred model from config
|
|
39
|
+
- Every successful response confirms circuit stays closed
|
|
40
|
+
- On rate limit error → transition to OPEN
|
|
41
|
+
|
|
42
|
+
### OPEN (rate limited — fallback active)
|
|
43
|
+
- Fall back through the free-tier model chain:
|
|
44
|
+
1. `gpt-5.4-mini`
|
|
45
|
+
2. `gpt-5-mini`
|
|
46
|
+
3. `gpt-4.1`
|
|
47
|
+
- Start cooldown timer (default: 10 minutes)
|
|
48
|
+
- When cooldown expires → transition to HALF-OPEN
|
|
49
|
+
|
|
50
|
+
### HALF-OPEN (testing recovery)
|
|
51
|
+
- Try preferred model again
|
|
52
|
+
- If 2 consecutive successes → transition to CLOSED
|
|
53
|
+
- If rate limit error → back to OPEN, reset cooldown
|
|
54
|
+
|
|
55
|
+
## State File: `.mesh/ralph-circuit-breaker.json`
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"state": "closed",
|
|
60
|
+
"preferredModel": "claude-sonnet-4.6",
|
|
61
|
+
"fallbackChain": ["gpt-5.4-mini", "gpt-5-mini", "gpt-4.1"],
|
|
62
|
+
"currentFallbackIndex": 0,
|
|
63
|
+
"cooldownMinutes": 10,
|
|
64
|
+
"openedAt": null,
|
|
65
|
+
"halfOpenSuccesses": 0,
|
|
66
|
+
"consecutiveFailures": 0,
|
|
67
|
+
"metrics": {
|
|
68
|
+
"totalFallbacks": 0,
|
|
69
|
+
"totalRecoveries": 0,
|
|
70
|
+
"lastFallbackAt": null,
|
|
71
|
+
"lastRecoveryAt": null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## PowerShell Functions
|
|
77
|
+
|
|
78
|
+
Paste these into your `ralph-watch.ps1` or source them from a shared module.
|
|
79
|
+
|
|
80
|
+
### `Get-CircuitBreakerState`
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
function Get-CircuitBreakerState {
|
|
84
|
+
param([string]$StateFile = ".mesh/ralph-circuit-breaker.json")
|
|
85
|
+
|
|
86
|
+
if (-not (Test-Path $StateFile)) {
|
|
87
|
+
$default = @{
|
|
88
|
+
state = "closed"
|
|
89
|
+
preferredModel = "claude-sonnet-4.6"
|
|
90
|
+
fallbackChain = @("gpt-5.4-mini", "gpt-5-mini", "gpt-4.1")
|
|
91
|
+
currentFallbackIndex = 0
|
|
92
|
+
cooldownMinutes = 10
|
|
93
|
+
openedAt = $null
|
|
94
|
+
halfOpenSuccesses = 0
|
|
95
|
+
consecutiveFailures = 0
|
|
96
|
+
metrics = @{
|
|
97
|
+
totalFallbacks = 0
|
|
98
|
+
totalRecoveries = 0
|
|
99
|
+
lastFallbackAt = $null
|
|
100
|
+
lastRecoveryAt = $null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
$default | ConvertTo-Json -Depth 3 | Set-Content $StateFile
|
|
104
|
+
return $default
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (Get-Content $StateFile -Raw | ConvertFrom-Json)
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `Save-CircuitBreakerState`
|
|
112
|
+
|
|
113
|
+
```powershell
|
|
114
|
+
function Save-CircuitBreakerState {
|
|
115
|
+
param(
|
|
116
|
+
[object]$State,
|
|
117
|
+
[string]$StateFile = ".mesh/ralph-circuit-breaker.json"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
$State | ConvertTo-Json -Depth 3 | Set-Content $StateFile
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `Get-CurrentModel`
|
|
125
|
+
|
|
126
|
+
Returns the model Ralph should use right now, based on circuit state.
|
|
127
|
+
|
|
128
|
+
```powershell
|
|
129
|
+
function Get-CurrentModel {
|
|
130
|
+
param([string]$StateFile = ".mesh/ralph-circuit-breaker.json")
|
|
131
|
+
|
|
132
|
+
$cb = Get-CircuitBreakerState -StateFile $StateFile
|
|
133
|
+
|
|
134
|
+
switch ($cb.state) {
|
|
135
|
+
"closed" {
|
|
136
|
+
return $cb.preferredModel
|
|
137
|
+
}
|
|
138
|
+
"open" {
|
|
139
|
+
# Check if cooldown has expired
|
|
140
|
+
if ($cb.openedAt) {
|
|
141
|
+
$opened = [DateTime]::Parse($cb.openedAt)
|
|
142
|
+
$elapsed = (Get-Date) - $opened
|
|
143
|
+
if ($elapsed.TotalMinutes -ge $cb.cooldownMinutes) {
|
|
144
|
+
# Transition to half-open
|
|
145
|
+
$cb.state = "half-open"
|
|
146
|
+
$cb.halfOpenSuccesses = 0
|
|
147
|
+
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
|
148
|
+
Write-Host " [circuit-breaker] Cooldown expired. Testing preferred model..." -ForegroundColor Yellow
|
|
149
|
+
return $cb.preferredModel
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
# Still in cooldown — use fallback
|
|
153
|
+
$idx = [Math]::Min($cb.currentFallbackIndex, $cb.fallbackChain.Count - 1)
|
|
154
|
+
return $cb.fallbackChain[$idx]
|
|
155
|
+
}
|
|
156
|
+
"half-open" {
|
|
157
|
+
return $cb.preferredModel
|
|
158
|
+
}
|
|
159
|
+
default {
|
|
160
|
+
return $cb.preferredModel
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `Update-CircuitBreakerOnSuccess`
|
|
167
|
+
|
|
168
|
+
Call after every successful model response.
|
|
169
|
+
|
|
170
|
+
```powershell
|
|
171
|
+
function Update-CircuitBreakerOnSuccess {
|
|
172
|
+
param([string]$StateFile = ".mesh/ralph-circuit-breaker.json")
|
|
173
|
+
|
|
174
|
+
$cb = Get-CircuitBreakerState -StateFile $StateFile
|
|
175
|
+
$cb.consecutiveFailures = 0
|
|
176
|
+
|
|
177
|
+
if ($cb.state -eq "half-open") {
|
|
178
|
+
$cb.halfOpenSuccesses++
|
|
179
|
+
if ($cb.halfOpenSuccesses -ge 2) {
|
|
180
|
+
# Recovery! Close the circuit
|
|
181
|
+
$cb.state = "closed"
|
|
182
|
+
$cb.openedAt = $null
|
|
183
|
+
$cb.halfOpenSuccesses = 0
|
|
184
|
+
$cb.currentFallbackIndex = 0
|
|
185
|
+
$cb.metrics.totalRecoveries++
|
|
186
|
+
$cb.metrics.lastRecoveryAt = (Get-Date).ToString("o")
|
|
187
|
+
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
|
188
|
+
Write-Host " [circuit-breaker] RECOVERED — back to preferred model ($($cb.preferredModel))" -ForegroundColor Green
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
|
192
|
+
Write-Host " [circuit-breaker] Half-open success $($cb.halfOpenSuccesses)/2" -ForegroundColor Yellow
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# closed state — nothing to do
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### `Update-CircuitBreakerOnRateLimit`
|
|
201
|
+
|
|
202
|
+
Call when a model response indicates rate limiting (HTTP 429 or error message containing "rate limit").
|
|
203
|
+
|
|
204
|
+
```powershell
|
|
205
|
+
function Update-CircuitBreakerOnRateLimit {
|
|
206
|
+
param([string]$StateFile = ".mesh/ralph-circuit-breaker.json")
|
|
207
|
+
|
|
208
|
+
$cb = Get-CircuitBreakerState -StateFile $StateFile
|
|
209
|
+
$cb.consecutiveFailures++
|
|
210
|
+
|
|
211
|
+
if ($cb.state -eq "closed" -or $cb.state -eq "half-open") {
|
|
212
|
+
# Open the circuit
|
|
213
|
+
$cb.state = "open"
|
|
214
|
+
$cb.openedAt = (Get-Date).ToString("o")
|
|
215
|
+
$cb.halfOpenSuccesses = 0
|
|
216
|
+
$cb.currentFallbackIndex = 0
|
|
217
|
+
$cb.metrics.totalFallbacks++
|
|
218
|
+
$cb.metrics.lastFallbackAt = (Get-Date).ToString("o")
|
|
219
|
+
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
|
220
|
+
|
|
221
|
+
$fallbackModel = $cb.fallbackChain[0]
|
|
222
|
+
Write-Host " [circuit-breaker] RATE LIMITED — falling back to $fallbackModel (cooldown: $($cb.cooldownMinutes)m)" -ForegroundColor Red
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if ($cb.state -eq "open") {
|
|
227
|
+
# Already open — try next fallback in chain if current one also fails
|
|
228
|
+
if ($cb.currentFallbackIndex -lt ($cb.fallbackChain.Count - 1)) {
|
|
229
|
+
$cb.currentFallbackIndex++
|
|
230
|
+
$nextModel = $cb.fallbackChain[$cb.currentFallbackIndex]
|
|
231
|
+
Write-Host " [circuit-breaker] Fallback also limited — trying $nextModel" -ForegroundColor Red
|
|
232
|
+
}
|
|
233
|
+
# Reset cooldown timer
|
|
234
|
+
$cb.openedAt = (Get-Date).ToString("o")
|
|
235
|
+
Save-CircuitBreakerState -State $cb -StateFile $StateFile
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Integration with ralph-watch.ps1
|
|
241
|
+
|
|
242
|
+
In your Ralph polling loop, wrap the model selection:
|
|
243
|
+
|
|
244
|
+
```powershell
|
|
245
|
+
# At the top of your polling loop
|
|
246
|
+
$model = Get-CurrentModel
|
|
247
|
+
|
|
248
|
+
# When invoking copilot CLI
|
|
249
|
+
$result = copilot-cli --model $model ...
|
|
250
|
+
|
|
251
|
+
# After the call
|
|
252
|
+
if ($result -match "rate.?limit" -or $LASTEXITCODE -eq 429) {
|
|
253
|
+
Update-CircuitBreakerOnRateLimit
|
|
254
|
+
} else {
|
|
255
|
+
Update-CircuitBreakerOnSuccess
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Full integration example
|
|
260
|
+
|
|
261
|
+
```powershell
|
|
262
|
+
# Source the circuit breaker functions
|
|
263
|
+
. .mesh-templates/ralph-circuit-breaker-functions.ps1
|
|
264
|
+
|
|
265
|
+
while ($true) {
|
|
266
|
+
$model = Get-CurrentModel
|
|
267
|
+
Write-Host "Polling with model: $model"
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
# Your existing Ralph logic here, but pass $model
|
|
271
|
+
$response = Invoke-RalphCycle -Model $model
|
|
272
|
+
|
|
273
|
+
# Success path
|
|
274
|
+
Update-CircuitBreakerOnSuccess
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
if ($_.Exception.Message -match "rate.?limit|429|quota|Too Many Requests") {
|
|
278
|
+
Update-CircuitBreakerOnRateLimit
|
|
279
|
+
# Retry immediately with fallback model
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
# Other errors — handle normally
|
|
283
|
+
throw
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
Start-Sleep -Seconds $pollInterval
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Configuration
|
|
291
|
+
|
|
292
|
+
Override defaults by editing `.mesh/ralph-circuit-breaker.json`:
|
|
293
|
+
|
|
294
|
+
| Field | Default | Description |
|
|
295
|
+
|-------|---------|-------------|
|
|
296
|
+
| `preferredModel` | `claude-sonnet-4.6` | Model to use when circuit is closed |
|
|
297
|
+
| `fallbackChain` | `["gpt-5.4-mini", "gpt-5-mini", "gpt-4.1"]` | Ordered fallback models (all free-tier) |
|
|
298
|
+
| `cooldownMinutes` | `10` | How long to wait before testing recovery |
|
|
299
|
+
|
|
300
|
+
## Metrics
|
|
301
|
+
|
|
302
|
+
The state file tracks operational metrics:
|
|
303
|
+
|
|
304
|
+
- **totalFallbacks** — How many times the circuit opened
|
|
305
|
+
- **totalRecoveries** — How many times it recovered to preferred model
|
|
306
|
+
- **lastFallbackAt** — ISO timestamp of last rate limit event
|
|
307
|
+
- **lastRecoveryAt** — ISO timestamp of last successful recovery
|
|
308
|
+
|
|
309
|
+
Query metrics with:
|
|
310
|
+
```powershell
|
|
311
|
+
$cb = Get-Content .mesh/ralph-circuit-breaker.json | ConvertFrom-Json
|
|
312
|
+
Write-Host "Fallbacks: $($cb.metrics.totalFallbacks) | Recoveries: $($cb.metrics.totalRecoveries)"
|
|
313
|
+
```
|