open-xmen 0.1.0
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/.cerebro/.gitignore +27 -0
- package/.cerebro/cerebro-identity.md +76 -0
- package/.cerebro/docs/agent-mapping.md +54 -0
- package/.cerebro/docs/cerebro-workflow.md +115 -0
- package/.cerebro/docs/orchestration.md +28 -0
- package/.cerebro/docs/overview.md +44 -0
- package/.cerebro/docs/skill-policy.md +25 -0
- package/.cerebro/integrations/semble.md +30 -0
- package/.cerebro/opencode/model-routing.md +37 -0
- package/.cerebro/schemas/boulder.schema.json +143 -0
- package/.cerebro/schemas/team-run.schema.json +234 -0
- package/.cerebro/schemas/upgrade-manifest.schema.json +45 -0
- package/.cerebro/schemas/upgrade-state.schema.json +38 -0
- package/.cerebro/scripts/check-agent-teams-enabled.py +24 -0
- package/.cerebro/scripts/ensure-upgrade-cache-gitignored.py +27 -0
- package/.cerebro/scripts/fetch-upstream-ref.py +67 -0
- package/.cerebro/scripts/reset-runtime.py +125 -0
- package/.cerebro/scripts/setup-status.py +101 -0
- package/.cerebro/scripts/test-stop-hook.py +60 -0
- package/.cerebro/scripts/upgrade-latest-tag.py +34 -0
- package/.cerebro/scripts/validate-agent-frontmatter.py +87 -0
- package/.cerebro/scripts/validate-boulder.py +105 -0
- package/.cerebro/scripts/validate-opencode-runtime.py +94 -0
- package/.cerebro/scripts/validate-team-runs.py +310 -0
- package/.cerebro/scripts/validate-upgrade-metadata.py +104 -0
- package/.cerebro/scripts/write-upgrade-state.py +93 -0
- package/.cerebro/templates/customer-vision.md +58 -0
- package/.cerebro/templates/plan.md +35 -0
- package/.cerebro/templates/product-brief.md +110 -0
- package/.cerebro/templates/project-context.md +64 -0
- package/.cerebro/templates/requirements-brief.md +67 -0
- package/.cerebro/templates/team-run.json +22 -0
- package/.cerebro/upgrade-manifest.json +160 -0
- package/.opencode/.gitignore +5 -0
- package/.opencode/agents/beast.md +38 -0
- package/.opencode/agents/cerebro.md +22 -0
- package/.opencode/agents/cyclops.md +22 -0
- package/.opencode/agents/cypher.md +46 -0
- package/.opencode/agents/emma-frost.md +38 -0
- package/.opencode/agents/forge.md +22 -0
- package/.opencode/agents/legion.md +45 -0
- package/.opencode/agents/nightcrawler.md +22 -0
- package/.opencode/agents/professor-x.md +39 -0
- package/.opencode/agents/sage.md +22 -0
- package/.opencode/agents/storm.md +49 -0
- package/.opencode/agents/wolverine.md +22 -0
- package/.opencode/commands/cerebro-doctor.md +19 -0
- package/.opencode/commands/cerebro-index.md +22 -0
- package/.opencode/commands/cerebro-plan.md +21 -0
- package/.opencode/commands/cerebro-reset.md +20 -0
- package/.opencode/commands/cerebro-start-work.md +20 -0
- package/.opencode/commands/cerebro-upgrade.md +19 -0
- package/.opencode/commands/to-me-my-x-men.md +27 -0
- package/AGENTS.md +12 -0
- package/README.md +193 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +597 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +466 -0
- package/package.json +54 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Cerebro Team Run Manifest",
|
|
4
|
+
"description": "Coordination audit log for a single OpenCode-managed Cerebro run. Task progress lives in .cerebro/team-runs/{run_id}.tasks.json via cerebro_task_* tools.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"version",
|
|
9
|
+
"run_id",
|
|
10
|
+
"command",
|
|
11
|
+
"status",
|
|
12
|
+
"lead",
|
|
13
|
+
"team_name",
|
|
14
|
+
"objective",
|
|
15
|
+
"risk_level",
|
|
16
|
+
"started_at",
|
|
17
|
+
"updated_at",
|
|
18
|
+
"teammates",
|
|
19
|
+
"ownership",
|
|
20
|
+
"mailbox_decisions",
|
|
21
|
+
"approvals",
|
|
22
|
+
"verification",
|
|
23
|
+
"cleanup"
|
|
24
|
+
],
|
|
25
|
+
"properties": {
|
|
26
|
+
"version": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"const": 1
|
|
29
|
+
},
|
|
30
|
+
"run_id": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "YYYYMMDD-HHMMSS-{slug}",
|
|
33
|
+
"minLength": 1
|
|
34
|
+
},
|
|
35
|
+
"command": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"enum": ["/to-me-my-x-men", "/cerebro-plan", "/cerebro-start-work", "/cerebro-index", "/cerebro-doctor", "/cerebro-reset", "/cerebro-upgrade"]
|
|
38
|
+
},
|
|
39
|
+
"status": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"enum": ["planning", "running", "blocked", "completed", "cleaned_up"]
|
|
42
|
+
},
|
|
43
|
+
"lead": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"const": "cerebro"
|
|
46
|
+
},
|
|
47
|
+
"team_name": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "Stable team/session name used to group Cerebro tasks, mailbox messages, checkpoints, and pending todos",
|
|
50
|
+
"minLength": 1
|
|
51
|
+
},
|
|
52
|
+
"objective": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"minLength": 1
|
|
55
|
+
},
|
|
56
|
+
"risk_level": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"enum": ["LOW", "MEDIUM", "HIGH"]
|
|
59
|
+
},
|
|
60
|
+
"started_at": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"minLength": 1
|
|
63
|
+
},
|
|
64
|
+
"updated_at": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"minLength": 1
|
|
67
|
+
},
|
|
68
|
+
"teammates": {
|
|
69
|
+
"type": "array",
|
|
70
|
+
"items": {
|
|
71
|
+
"$ref": "#/$defs/teammate"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"ownership": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"description": "File ownership assignments — prevent two writers on the same file",
|
|
77
|
+
"items": {
|
|
78
|
+
"$ref": "#/$defs/ownership_entry"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"mailbox_decisions": {
|
|
82
|
+
"type": "array",
|
|
83
|
+
"description": "Decisions recorded via cerebro_mailbox_send that resolve cross-agent conflicts or assumptions",
|
|
84
|
+
"items": {
|
|
85
|
+
"$ref": "#/$defs/mailbox_decision"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"approvals": {
|
|
89
|
+
"type": "array",
|
|
90
|
+
"items": {
|
|
91
|
+
"$ref": "#/$defs/approval"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"verification": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"items": {
|
|
97
|
+
"$ref": "#/$defs/verification_entry"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"cleanup": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"additionalProperties": false,
|
|
103
|
+
"required": ["team_stopped", "pending_todos_clear", "notes"],
|
|
104
|
+
"properties": {
|
|
105
|
+
"team_stopped": {
|
|
106
|
+
"type": "boolean"
|
|
107
|
+
},
|
|
108
|
+
"pending_todos_clear": {
|
|
109
|
+
"type": "boolean"
|
|
110
|
+
},
|
|
111
|
+
"notes": {
|
|
112
|
+
"type": "string"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"$defs": {
|
|
118
|
+
"teammate": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"additionalProperties": false,
|
|
121
|
+
"required": ["name", "agent_type", "status", "responsibility", "last_signal"],
|
|
122
|
+
"properties": {
|
|
123
|
+
"name": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"minLength": 1
|
|
126
|
+
},
|
|
127
|
+
"agent_type": {
|
|
128
|
+
"type": "string",
|
|
129
|
+
"enum": ["cerebro", "legion", "cypher", "cyclops", "wolverine", "storm", "professor-x", "beast", "emma-frost", "nightcrawler", "sage", "forge"]
|
|
130
|
+
},
|
|
131
|
+
"status": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"enum": ["pending", "active", "idle", "done", "blocked"]
|
|
134
|
+
},
|
|
135
|
+
"responsibility": {
|
|
136
|
+
"type": "string"
|
|
137
|
+
},
|
|
138
|
+
"last_signal": {
|
|
139
|
+
"type": "string"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"ownership_entry": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"additionalProperties": false,
|
|
146
|
+
"required": ["path", "owner", "reviewer", "notes"],
|
|
147
|
+
"properties": {
|
|
148
|
+
"path": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"minLength": 1
|
|
151
|
+
},
|
|
152
|
+
"owner": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"minLength": 1
|
|
155
|
+
},
|
|
156
|
+
"reviewer": {
|
|
157
|
+
"type": "string"
|
|
158
|
+
},
|
|
159
|
+
"notes": {
|
|
160
|
+
"type": "string"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"mailbox_decision": {
|
|
165
|
+
"type": "object",
|
|
166
|
+
"additionalProperties": false,
|
|
167
|
+
"required": ["at", "from", "to", "decision", "notes"],
|
|
168
|
+
"properties": {
|
|
169
|
+
"at": {
|
|
170
|
+
"type": "string",
|
|
171
|
+
"minLength": 1
|
|
172
|
+
},
|
|
173
|
+
"from": {
|
|
174
|
+
"type": "string",
|
|
175
|
+
"minLength": 1
|
|
176
|
+
},
|
|
177
|
+
"to": {
|
|
178
|
+
"type": "string",
|
|
179
|
+
"minLength": 1
|
|
180
|
+
},
|
|
181
|
+
"decision": {
|
|
182
|
+
"type": "string",
|
|
183
|
+
"minLength": 1
|
|
184
|
+
},
|
|
185
|
+
"notes": {
|
|
186
|
+
"type": "string"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"approval": {
|
|
191
|
+
"type": "object",
|
|
192
|
+
"additionalProperties": false,
|
|
193
|
+
"required": ["gate", "status", "decided_at", "notes"],
|
|
194
|
+
"properties": {
|
|
195
|
+
"gate": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"minLength": 1
|
|
198
|
+
},
|
|
199
|
+
"status": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"enum": ["pending", "approved", "rejected"]
|
|
202
|
+
},
|
|
203
|
+
"decided_at": {
|
|
204
|
+
"type": ["string", "null"]
|
|
205
|
+
},
|
|
206
|
+
"notes": {
|
|
207
|
+
"type": "string"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
"verification_entry": {
|
|
212
|
+
"type": "object",
|
|
213
|
+
"additionalProperties": false,
|
|
214
|
+
"required": ["command", "status", "by", "notes"],
|
|
215
|
+
"properties": {
|
|
216
|
+
"command": {
|
|
217
|
+
"type": "string",
|
|
218
|
+
"minLength": 1
|
|
219
|
+
},
|
|
220
|
+
"status": {
|
|
221
|
+
"type": "string",
|
|
222
|
+
"enum": ["NOT RUN", "PASS", "FAIL", "BLOCKED"]
|
|
223
|
+
},
|
|
224
|
+
"by": {
|
|
225
|
+
"type": "string",
|
|
226
|
+
"minLength": 1
|
|
227
|
+
},
|
|
228
|
+
"notes": {
|
|
229
|
+
"type": "string"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Cerebro Upgrade Manifest",
|
|
4
|
+
"description": "Declares file ownership for /cerebro-upgrade. Controls which files are overwritten, merged, or left untouched during a template sync.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["version", "entries"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"const": 1
|
|
12
|
+
},
|
|
13
|
+
"entries": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"description": "Ordered list of ownership declarations. Earlier entries take precedence for overlapping globs.",
|
|
16
|
+
"items": {
|
|
17
|
+
"$ref": "#/$defs/entry"
|
|
18
|
+
},
|
|
19
|
+
"minItems": 1
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"$defs": {
|
|
23
|
+
"entry": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["path", "ownership"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"path": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Glob pattern or exact path relative to the project root",
|
|
31
|
+
"minLength": 1
|
|
32
|
+
},
|
|
33
|
+
"ownership": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "template: overwrite silently (Gate C fires in --strict mode); merge: show unified diff, Gate A on conflict; user: never touched",
|
|
36
|
+
"enum": ["template", "merge", "user"]
|
|
37
|
+
},
|
|
38
|
+
"notes": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Human-readable rationale for this ownership classification"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Cerebro Upgrade State",
|
|
4
|
+
"description": "Baseline record written after each successful /cerebro-upgrade run. Drives accurate change detection on the next upgrade.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["version", "applied_ref", "applied_sha", "applied_at", "hashes"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"const": 1
|
|
12
|
+
},
|
|
13
|
+
"applied_ref": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "The git tag or commit SHA that was passed to /cerebro-upgrade",
|
|
16
|
+
"minLength": 1
|
|
17
|
+
},
|
|
18
|
+
"applied_sha": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Full 40-character SHA of the upstream commit that was applied",
|
|
21
|
+
"pattern": "^[0-9a-f]{40}$"
|
|
22
|
+
},
|
|
23
|
+
"applied_at": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "ISO-8601 timestamp when the upgrade was applied",
|
|
26
|
+
"minLength": 1
|
|
27
|
+
},
|
|
28
|
+
"hashes": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"description": "Map of relative file path to SHA-256 hex digest of post-upgrade local file content. Covers all template-owned and merge-owned files present in the local tree after the upgrade.",
|
|
31
|
+
"additionalProperties": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "SHA-256 hex digest (64 lowercase hex characters)",
|
|
34
|
+
"pattern": "^[0-9a-f]{64}$"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Legacy compatibility check for Claude Code agent-team settings.
|
|
3
|
+
|
|
4
|
+
The active OpenCode runtime uses `.opencode/` agents, Cerebro custom tools,
|
|
5
|
+
and child sessions instead of Claude Code experimental agent teams. Keep this
|
|
6
|
+
script only for repositories that still validate legacy `.claude/` material.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> int:
|
|
14
|
+
settings = json.loads(Path(".claude/settings.json").read_text())
|
|
15
|
+
env = settings.get("env", {})
|
|
16
|
+
if env.get("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS") != "1":
|
|
17
|
+
print("missing CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1")
|
|
18
|
+
return 1
|
|
19
|
+
print("agent teams enabled")
|
|
20
|
+
return 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Ensure .cerebro/.gitignore ignores Cerebro upgrade cache files."""
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
LINE = "upgrade-cache/"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> int:
|
|
11
|
+
path = Path(".cerebro/.gitignore")
|
|
12
|
+
text = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
13
|
+
lines = text.splitlines()
|
|
14
|
+
if LINE in lines:
|
|
15
|
+
print("upgrade cache already ignored")
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
prefix = text
|
|
19
|
+
if prefix and not prefix.endswith("\n"):
|
|
20
|
+
prefix += "\n"
|
|
21
|
+
path.write_text(prefix + LINE + "\n", encoding="utf-8")
|
|
22
|
+
print("added upgrade-cache/ to .cerebro/.gitignore")
|
|
23
|
+
return 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fetch a Cerebro upstream ref into the local upgrade cache and print its SHA."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
REF_RE = re.compile(r"^[A-Za-z0-9._/-]+$")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> int:
|
|
14
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
15
|
+
parser.add_argument("ref")
|
|
16
|
+
parser.add_argument("--upstream", default="https://github.com/yelaco/claude-xmen.git")
|
|
17
|
+
args = parser.parse_args()
|
|
18
|
+
|
|
19
|
+
if args.ref in {"HEAD", "main", "master"}:
|
|
20
|
+
print("ref must be an explicit tag or commit SHA, not HEAD/main/master")
|
|
21
|
+
return 1
|
|
22
|
+
if not REF_RE.match(args.ref):
|
|
23
|
+
print(f"ref contains unsupported characters: {args.ref!r}")
|
|
24
|
+
return 1
|
|
25
|
+
|
|
26
|
+
cache_dir = Path(".cerebro/upgrade-cache") / args.ref
|
|
27
|
+
try:
|
|
28
|
+
if cache_dir.is_dir():
|
|
29
|
+
subprocess.run(
|
|
30
|
+
["git", "-C", str(cache_dir), "fetch", "--depth=1", "origin", "tag", args.ref],
|
|
31
|
+
check=True,
|
|
32
|
+
)
|
|
33
|
+
subprocess.run(["git", "-C", str(cache_dir), "checkout", args.ref], check=True)
|
|
34
|
+
else:
|
|
35
|
+
cache_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
subprocess.run(
|
|
37
|
+
[
|
|
38
|
+
"git",
|
|
39
|
+
"clone",
|
|
40
|
+
"--filter=blob:none",
|
|
41
|
+
"--depth=1",
|
|
42
|
+
"--branch",
|
|
43
|
+
args.ref,
|
|
44
|
+
args.upstream,
|
|
45
|
+
str(cache_dir),
|
|
46
|
+
],
|
|
47
|
+
check=True,
|
|
48
|
+
)
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
["git", "-C", str(cache_dir), "rev-parse", "HEAD"],
|
|
51
|
+
check=True,
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
)
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
print("git is required for /cerebro-upgrade - install git and retry")
|
|
57
|
+
return 1
|
|
58
|
+
except subprocess.CalledProcessError:
|
|
59
|
+
print(f"Could not fetch ref {args.ref!r} from {args.upstream}")
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
print(result.stdout.strip())
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Scan, reset, and verify Cerebro runtime state."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
RUNTIME = {
|
|
10
|
+
"boulder.json": Path(".cerebro/boulder.json"),
|
|
11
|
+
".pending-todos": Path(".cerebro/.pending-todos"),
|
|
12
|
+
"pending-todos/": Path(".cerebro/pending-todos"),
|
|
13
|
+
"plans/": Path(".cerebro/plans"),
|
|
14
|
+
"notepads/": Path(".cerebro/notepads"),
|
|
15
|
+
"team-runs/": Path(".cerebro/team-runs"),
|
|
16
|
+
}
|
|
17
|
+
TARGETS = list(RUNTIME.values())
|
|
18
|
+
STUB_DIRS = [
|
|
19
|
+
Path(".cerebro/plans"),
|
|
20
|
+
Path(".cerebro/notepads"),
|
|
21
|
+
Path(".cerebro/team-runs"),
|
|
22
|
+
]
|
|
23
|
+
EMPTY_DIRS = [
|
|
24
|
+
*STUB_DIRS,
|
|
25
|
+
Path(".cerebro/pending-todos"),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> int:
|
|
30
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
31
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
32
|
+
subparsers.add_parser("scan")
|
|
33
|
+
reset_parser = subparsers.add_parser("reset")
|
|
34
|
+
reset_parser.add_argument("--confirm", required=True)
|
|
35
|
+
subparsers.add_parser("verify")
|
|
36
|
+
args = parser.parse_args()
|
|
37
|
+
|
|
38
|
+
if args.command == "scan":
|
|
39
|
+
scan()
|
|
40
|
+
return 0
|
|
41
|
+
if args.command == "reset":
|
|
42
|
+
if args.confirm != "YES":
|
|
43
|
+
print("Reset aborted. No files were changed.")
|
|
44
|
+
return 1
|
|
45
|
+
reset()
|
|
46
|
+
return 0
|
|
47
|
+
if args.command == "verify":
|
|
48
|
+
return verify()
|
|
49
|
+
raise AssertionError(args.command)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def scan() -> None:
|
|
53
|
+
for label, path in RUNTIME.items():
|
|
54
|
+
if not path.exists():
|
|
55
|
+
print(f" {label:<20} absent - nothing to remove")
|
|
56
|
+
continue
|
|
57
|
+
if path.is_file():
|
|
58
|
+
print(f" {label:<20} FILE {path.stat().st_size} bytes")
|
|
59
|
+
continue
|
|
60
|
+
items = [item for item in path.rglob("*") if item.is_file() and item.name != ".gitkeep"]
|
|
61
|
+
print(f" {label:<20} DIR {len(items)} runtime file(s)")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def reset() -> None:
|
|
65
|
+
removed = []
|
|
66
|
+
skipped = []
|
|
67
|
+
for path in TARGETS:
|
|
68
|
+
if not path.exists():
|
|
69
|
+
skipped.append(str(path))
|
|
70
|
+
continue
|
|
71
|
+
if path.is_file():
|
|
72
|
+
path.unlink()
|
|
73
|
+
else:
|
|
74
|
+
shutil.rmtree(path)
|
|
75
|
+
removed.append(str(path))
|
|
76
|
+
|
|
77
|
+
for path in EMPTY_DIRS:
|
|
78
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
for path in STUB_DIRS:
|
|
80
|
+
(path / ".gitkeep").touch()
|
|
81
|
+
|
|
82
|
+
print("Removed:")
|
|
83
|
+
for path in removed:
|
|
84
|
+
print(f" {path}")
|
|
85
|
+
if skipped:
|
|
86
|
+
print("Skipped (absent):")
|
|
87
|
+
for path in skipped:
|
|
88
|
+
print(f" {path}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def verify() -> int:
|
|
92
|
+
checks = {
|
|
93
|
+
"boulder.json absent": not Path(".cerebro/boulder.json").exists(),
|
|
94
|
+
".pending-todos absent": not Path(".cerebro/.pending-todos").exists(),
|
|
95
|
+
"pending-todos/ absent or empty": _absent_or_has_only_gitkeep(Path(".cerebro/pending-todos")),
|
|
96
|
+
"plans/ clean": _has_only_gitkeep_or_less(Path(".cerebro/plans")),
|
|
97
|
+
"notepads/ clean": _has_only_gitkeep_or_less(Path(".cerebro/notepads")),
|
|
98
|
+
"team-runs/ clean": _has_only_gitkeep_or_less(Path(".cerebro/team-runs")),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
all_pass = True
|
|
102
|
+
for label, ok in checks.items():
|
|
103
|
+
status = "PASS" if ok else "FAIL"
|
|
104
|
+
if not ok:
|
|
105
|
+
all_pass = False
|
|
106
|
+
print(f" {status} {label}")
|
|
107
|
+
|
|
108
|
+
if not all_pass:
|
|
109
|
+
return 1
|
|
110
|
+
print("Runtime clean.")
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _has_only_gitkeep_or_less(path: Path) -> bool:
|
|
115
|
+
if not path.is_dir():
|
|
116
|
+
return False
|
|
117
|
+
return all(item.name == ".gitkeep" for item in path.iterdir())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _absent_or_has_only_gitkeep(path: Path) -> bool:
|
|
121
|
+
return not path.exists() or _has_only_gitkeep_or_less(path)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Report Cerebro OpenCode setup status."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
OPENCODE_INSTRUCTIONS = [
|
|
11
|
+
Path("AGENTS.md"),
|
|
12
|
+
Path(".cerebro/cerebro-identity.md"),
|
|
13
|
+
Path(".cerebro/opencode/model-routing.md"),
|
|
14
|
+
]
|
|
15
|
+
DEFAULT_UPSTREAM = "https://github.com/yelaco/claude-xmen.git"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> int:
|
|
19
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
20
|
+
parser.add_argument("--upstream", default=DEFAULT_UPSTREAM)
|
|
21
|
+
parser.add_argument("--skip-upstream", action="store_true")
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
instruction_status = instruction_files_status()
|
|
25
|
+
plugin_status = "PRESENT" if Path(".opencode/plugins/open-xmen.ts").is_file() else "MISSING"
|
|
26
|
+
installed = installed_version()
|
|
27
|
+
latest = "unreachable" if args.skip_upstream else latest_upstream_tag(args.upstream)
|
|
28
|
+
upgrade_needed = compare_versions(installed, latest)
|
|
29
|
+
|
|
30
|
+
missing = [str(path) for path, status in instruction_status.items() if status == "MISSING"]
|
|
31
|
+
if missing:
|
|
32
|
+
print(f"Missing OpenCode instruction files: {', '.join(missing)}")
|
|
33
|
+
print("Run `open-xmen install --reset` or `/cerebro-upgrade <latest-tag>` to restore managed runtime files.")
|
|
34
|
+
|
|
35
|
+
if latest == "unreachable":
|
|
36
|
+
print("Could not reach upstream - skipping version check. Check your connection and try again.")
|
|
37
|
+
elif upgrade_needed == "NO":
|
|
38
|
+
print(f"Cerebro installation is current at {installed}. No upgrade needed.")
|
|
39
|
+
elif upgrade_needed == "YES":
|
|
40
|
+
print(f"Upstream has {latest} - you are on {installed}. Run /cerebro-upgrade {latest} to sync.")
|
|
41
|
+
else:
|
|
42
|
+
print(
|
|
43
|
+
f"Installed version is unknown (no upgrade-state.json). Latest upstream is {latest}. "
|
|
44
|
+
f"Run /cerebro-upgrade {latest} to initialize upgrade tracking."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
print()
|
|
48
|
+
for path, status in instruction_status.items():
|
|
49
|
+
print(f"{str(path):24} - {status}")
|
|
50
|
+
print("plugin bridge - " + plugin_status)
|
|
51
|
+
print("installed version - " + ("unknown" if installed == "none" else installed))
|
|
52
|
+
print("latest upstream - " + latest)
|
|
53
|
+
print("upgrade needed - " + upgrade_needed)
|
|
54
|
+
return 0 if not missing and plugin_status == "PRESENT" else 1
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def instruction_files_status() -> dict[Path, str]:
|
|
58
|
+
return {path: ("PRESENT" if path.is_file() else "MISSING") for path in OPENCODE_INSTRUCTIONS}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def installed_version() -> str:
|
|
62
|
+
state_path = Path(".cerebro/upgrade-state.json")
|
|
63
|
+
if not state_path.exists():
|
|
64
|
+
return "none"
|
|
65
|
+
try:
|
|
66
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
67
|
+
except json.JSONDecodeError:
|
|
68
|
+
return "unknown"
|
|
69
|
+
return state.get("applied_ref", "unknown")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def latest_upstream_tag(upstream: str) -> str:
|
|
73
|
+
try:
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
["git", "ls-remote", "--tags", "--sort=-version:refname", upstream, "refs/tags/v*"],
|
|
76
|
+
check=True,
|
|
77
|
+
capture_output=True,
|
|
78
|
+
text=True,
|
|
79
|
+
timeout=20,
|
|
80
|
+
)
|
|
81
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
82
|
+
return "unreachable"
|
|
83
|
+
|
|
84
|
+
for line in result.stdout.splitlines():
|
|
85
|
+
if "refs/tags/" in line:
|
|
86
|
+
return line.rsplit("refs/tags/", 1)[1].removesuffix("^{}")
|
|
87
|
+
return "unreachable"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def compare_versions(installed: str, latest: str) -> str:
|
|
91
|
+
if latest == "unreachable":
|
|
92
|
+
return "UNKNOWN"
|
|
93
|
+
if installed in {"none", "unknown"}:
|
|
94
|
+
return "UNKNOWN"
|
|
95
|
+
if installed == latest:
|
|
96
|
+
return "NO"
|
|
97
|
+
return "YES"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
raise SystemExit(main())
|