leos-agent 6.3.0 → 7.0.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/LICENSE +21 -0
- package/README.md +14 -7
- package/adapters/cursor/agents/executor.md +1 -1
- package/adapters/cursor/agents/implementer.md +2 -2
- package/adapters/cursor/agents/review-lens.md +22 -0
- package/adapters/cursor/agents/reviewer.md +2 -2
- package/adapters/opencode/agents.json +43 -4
- package/adapters/opencode/plugin.js +325 -37
- package/config/MCP_PINS.md +17 -0
- package/config/models.json +276 -8
- package/hooks/bash-guard.py +51 -9
- package/package.json +3 -6
- package/roles/executor.md +1 -1
- package/roles/implementer.md +2 -2
- package/roles/review-lens.md +20 -0
- package/roles/reviewer.md +2 -2
- package/scripts/doctor.py +267 -31
- package/scripts/ghreview.py +7 -3
- package/scripts/jsonc_bridge.cjs +23 -0
- package/scripts/memory.py +74 -35
- package/scripts/render_adapters.py +57 -22
- package/scripts/resolve_attach_target.py +45 -13
- package/scripts/setup.py +1594 -2
- package/skills/brainstorming/SKILL.md +3 -1
- package/skills/debugging/SKILL.md +4 -2
- package/skills/delegation/SKILL.md +10 -8
- package/skills/doctor/SKILL.md +33 -14
- package/skills/executing-plans/SKILL.md +2 -1
- package/skills/finishing-a-branch/SKILL.md +4 -2
- package/skills/freshness/SKILL.md +23 -10
- package/skills/memory/SKILL.md +12 -2
- package/skills/resolve-ticket/SKILL.md +15 -9
- package/skills/review-pr/SKILL.md +26 -16
- package/skills/setup/SKILL.md +123 -9
- package/skills/setup/agents/openai.yaml +5 -0
- package/skills/test-first/SKILL.md +3 -1
- package/skills/using-leo/SKILL.md +11 -6
- package/skills/using-leo/references/claude-mapping.md +2 -1
- package/skills/using-leo/references/codex-mapping.md +4 -5
- package/skills/using-leo/references/cursor-mapping.md +2 -1
- package/skills/using-leo/references/hermes-mapping.md +2 -1
- package/skills/using-leo/references/opencode-mapping.md +6 -3
- package/skills/verification/SKILL.md +2 -1
- package/skills/visual-verification/SKILL.md +2 -1
- package/skills/watch-review/SKILL.md +17 -14
- package/skills/watch-review/agents/openai.yaml +5 -0
- package/skills/worktrees/SKILL.md +3 -1
- package/skills/writing-plans/SKILL.md +2 -1
- package/skills/writing-skills/SKILL.md +9 -2
- package/vendor/jsonc-parser-3.3.1/LICENSE.md +21 -0
- package/vendor/jsonc-parser-3.3.1/README.md +26 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/edit.js +201 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/format.js +275 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/parser.js +682 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/scanner.js +456 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/string-intern.js +42 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/main.d.ts +351 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/main.js +194 -0
- package/vendor/jsonc-parser-3.3.1/package.json +37 -0
- package/workflows/cost-tiered-fix.js +32 -4
package/scripts/doctor.py
CHANGED
|
@@ -7,25 +7,40 @@ and correctly listed, and still have failed open this session. Only the running
|
|
|
7
7
|
agent can see its own context, so leo:doctor pairs this output with three
|
|
8
8
|
questions the model answers itself.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
verdict about this session.
|
|
12
|
-
|
|
13
|
-
own.
|
|
10
|
+
Most breadcrumb logs are reported as history with unknown provenance, never as
|
|
11
|
+
a verdict about this session. Some legacy entries carry no timestamps and the
|
|
12
|
+
test suite drives failure paths deliberately, so "the log has errors" usually
|
|
13
|
+
proves nothing on its own. `opencode-skills.log` is the capability exception:
|
|
14
|
+
its presence records a namespaced-skill registration failure and is reported
|
|
15
|
+
as degraded until it is intentionally cleared after repair.
|
|
14
16
|
|
|
15
17
|
doctor.py human-readable report
|
|
16
18
|
doctor.py --json the same facts as JSON
|
|
17
19
|
doctor.py --harness <name> state the harness instead of detecting it
|
|
18
20
|
|
|
19
|
-
Exit code is
|
|
21
|
+
Exit code is nonzero when required payload/bootstrap artifacts are missing or
|
|
22
|
+
invalid, or the running Python is unsupported. Degraded historical warnings
|
|
23
|
+
remain report-only.
|
|
20
24
|
"""
|
|
21
25
|
import importlib.util
|
|
22
26
|
import json
|
|
23
27
|
import os
|
|
28
|
+
import re
|
|
24
29
|
import sys
|
|
25
30
|
|
|
26
31
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
27
32
|
PAYLOAD = os.path.dirname(_HERE)
|
|
28
33
|
TIERS = ("fable", "opus", "sonnet", "haiku")
|
|
34
|
+
MIN_PYTHON = (3, 9)
|
|
35
|
+
EXPECTED_HARNESSES = {"claude", "codex", "cursor", "hermes", "opencode"}
|
|
36
|
+
SEMVER_RE = re.compile(
|
|
37
|
+
r"^(0|[1-9]\d*)\."
|
|
38
|
+
r"(0|[1-9]\d*)\."
|
|
39
|
+
r"(0|[1-9]\d*)"
|
|
40
|
+
r"(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\."
|
|
41
|
+
r"(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?"
|
|
42
|
+
r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$"
|
|
43
|
+
)
|
|
29
44
|
|
|
30
45
|
|
|
31
46
|
HARNESS_ENV = ("CURSOR_PLUGIN_ROOT", "CURSOR_VERSION", "PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT")
|
|
@@ -33,7 +48,8 @@ HARNESS_ENV = ("CURSOR_PLUGIN_ROOT", "CURSOR_VERSION", "PLUGIN_ROOT", "CLAUDE_PL
|
|
|
33
48
|
|
|
34
49
|
def _known_harnesses():
|
|
35
50
|
models = _read_json(os.path.join(PAYLOAD, "config", "models.json")) or {}
|
|
36
|
-
|
|
51
|
+
harnesses = models.get("harnesses") if isinstance(models, dict) else None
|
|
52
|
+
return set(harnesses) if isinstance(harnesses, dict) else set()
|
|
37
53
|
|
|
38
54
|
|
|
39
55
|
def _detect_harness(argv=()):
|
|
@@ -101,6 +117,78 @@ def _read_json(path):
|
|
|
101
117
|
return None
|
|
102
118
|
|
|
103
119
|
|
|
120
|
+
def _manifest_valid(manifest):
|
|
121
|
+
"""Validate the required portable manifest fields doctor reports."""
|
|
122
|
+
if not isinstance(manifest, dict):
|
|
123
|
+
return False
|
|
124
|
+
for key in ("name", "description", "homepage", "repository", "license"):
|
|
125
|
+
if not isinstance(manifest.get(key), str) or not manifest[key].strip():
|
|
126
|
+
return False
|
|
127
|
+
version = manifest.get("version")
|
|
128
|
+
if not isinstance(version, str) or SEMVER_RE.fullmatch(version) is None:
|
|
129
|
+
return False
|
|
130
|
+
author = manifest.get("author")
|
|
131
|
+
if not isinstance(author, dict) or not isinstance(author.get("name"), str) \
|
|
132
|
+
or not author["name"].strip():
|
|
133
|
+
return False
|
|
134
|
+
skills = manifest.get("skills")
|
|
135
|
+
return (isinstance(skills, list) and bool(skills) and
|
|
136
|
+
all(isinstance(item, str) and item.strip() for item in skills))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _models_valid(models):
|
|
140
|
+
"""Apply structural checks and the canonical renderer validation.
|
|
141
|
+
|
|
142
|
+
The renderer owns cross-field coherence, while these checks reject
|
|
143
|
+
skeletal documents that happen not to exercise one of its relationships.
|
|
144
|
+
Doctor must never call either shape healthy merely because it contains a
|
|
145
|
+
``harnesses`` object.
|
|
146
|
+
"""
|
|
147
|
+
if not isinstance(models, dict) or models.get("schemaVersion") != 4:
|
|
148
|
+
return False
|
|
149
|
+
for key in ("roles", "harnesses", "skills", "mcp", "memoryTarget", "visual"):
|
|
150
|
+
if not isinstance(models.get(key), dict) or not models[key]:
|
|
151
|
+
return False
|
|
152
|
+
capabilities = models.get("capabilities")
|
|
153
|
+
if not isinstance(capabilities, list) or not capabilities:
|
|
154
|
+
return False
|
|
155
|
+
harnesses = models["harnesses"]
|
|
156
|
+
if set(harnesses) != EXPECTED_HARNESSES:
|
|
157
|
+
return False
|
|
158
|
+
for rows in harnesses.values():
|
|
159
|
+
if not isinstance(rows, dict):
|
|
160
|
+
return False
|
|
161
|
+
for tier in TIERS:
|
|
162
|
+
mapping = rows.get(tier)
|
|
163
|
+
if (not isinstance(mapping, dict) or
|
|
164
|
+
not isinstance(mapping.get("model"), str) or
|
|
165
|
+
not mapping["model"].strip()):
|
|
166
|
+
return False
|
|
167
|
+
if "effort" in mapping and (not isinstance(mapping["effort"], str) or
|
|
168
|
+
not mapping["effort"].strip()):
|
|
169
|
+
return False
|
|
170
|
+
for spec in models["roles"].values():
|
|
171
|
+
if (not isinstance(spec, dict) or spec.get("tier") not in TIERS or
|
|
172
|
+
spec.get("access") not in ("read-only", "write")):
|
|
173
|
+
return False
|
|
174
|
+
for row in capabilities:
|
|
175
|
+
if (not isinstance(row, dict) or not isinstance(row.get("key"), str) or
|
|
176
|
+
not isinstance(row.get("modes"), list) or not row["modes"] or
|
|
177
|
+
not isinstance(row.get("values"), dict)):
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
validator_path = os.path.join(PAYLOAD, "scripts", "render_adapters.py")
|
|
181
|
+
try:
|
|
182
|
+
spec = importlib.util.spec_from_file_location("leo_doctor_models_validator",
|
|
183
|
+
validator_path)
|
|
184
|
+
validator = importlib.util.module_from_spec(spec)
|
|
185
|
+
spec.loader.exec_module(validator)
|
|
186
|
+
validator._validate(models)
|
|
187
|
+
except Exception:
|
|
188
|
+
return False
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
|
|
104
192
|
def _local_root():
|
|
105
193
|
return os.environ.get("LEOS_AGENT_LOCAL_PATH") or os.path.join(
|
|
106
194
|
os.path.expanduser("~"), ".leos-agent-local"
|
|
@@ -113,8 +201,21 @@ def _memory_report():
|
|
|
113
201
|
import memory
|
|
114
202
|
|
|
115
203
|
root = memory.memory_root()
|
|
204
|
+
hermes_enabled = memory.hermes_enabled()
|
|
205
|
+
hermes_target = os.path.join(memory.hermes_home(), "SOUL.md")
|
|
206
|
+
hermes_projection = {
|
|
207
|
+
"enabled": hermes_enabled,
|
|
208
|
+
"path": hermes_target,
|
|
209
|
+
"status": (
|
|
210
|
+
"disabled" if not hermes_enabled else
|
|
211
|
+
"projected" if (os.path.isfile(hermes_target) and
|
|
212
|
+
memory.BEGIN in _slurp(hermes_target)) else
|
|
213
|
+
"not projected"
|
|
214
|
+
),
|
|
215
|
+
}
|
|
116
216
|
if not os.path.isdir(root):
|
|
117
|
-
return {"store": root, "facts": 0, "present": False, "targets": []
|
|
217
|
+
return {"store": root, "facts": 0, "present": False, "targets": [],
|
|
218
|
+
"hermes_projection": hermes_projection}
|
|
118
219
|
index = memory._load_index() or {"facts": []}
|
|
119
220
|
targets = [
|
|
120
221
|
{"harness": h, "path": f, "present": os.path.exists(f),
|
|
@@ -123,7 +224,7 @@ def _memory_report():
|
|
|
123
224
|
if os.path.isdir(gate)
|
|
124
225
|
]
|
|
125
226
|
return {"store": root, "facts": len(index["facts"]), "present": True,
|
|
126
|
-
"targets": targets}
|
|
227
|
+
"targets": targets, "hermes_projection": hermes_projection}
|
|
127
228
|
except Exception as exc:
|
|
128
229
|
return {"store": _local_root(), "error": f"{type(exc).__name__}: {exc}"}
|
|
129
230
|
|
|
@@ -138,7 +239,8 @@ def _slurp(path):
|
|
|
138
239
|
|
|
139
240
|
def _breadcrumbs():
|
|
140
241
|
out = {}
|
|
141
|
-
for name in ("session-start.log", "hermes-policy.log", "opencode-guard.log"
|
|
242
|
+
for name in ("session-start.log", "hermes-policy.log", "opencode-guard.log",
|
|
243
|
+
"opencode-skills.log"):
|
|
142
244
|
path = os.path.join(_local_root(), name)
|
|
143
245
|
if not os.path.exists(path):
|
|
144
246
|
continue
|
|
@@ -148,6 +250,78 @@ def _breadcrumbs():
|
|
|
148
250
|
return out
|
|
149
251
|
|
|
150
252
|
|
|
253
|
+
def _contains(path, needle):
|
|
254
|
+
return needle in _slurp(path)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _hook_manifest_valid(data, harness):
|
|
258
|
+
"""Validate the session-start registration, not a stray filename mention."""
|
|
259
|
+
if not isinstance(data, dict) or not isinstance(data.get("hooks"), dict):
|
|
260
|
+
return False
|
|
261
|
+
event = "sessionStart" if harness == "cursor" else "SessionStart"
|
|
262
|
+
groups = data["hooks"].get(event)
|
|
263
|
+
if not isinstance(groups, list):
|
|
264
|
+
return False
|
|
265
|
+
hooks = groups if harness == "cursor" else [
|
|
266
|
+
hook
|
|
267
|
+
for group in groups if isinstance(group, dict)
|
|
268
|
+
for hook in (group.get("hooks") or []) if isinstance(hook, dict)
|
|
269
|
+
]
|
|
270
|
+
return any(
|
|
271
|
+
isinstance(hook, dict)
|
|
272
|
+
and isinstance(hook.get("command"), str)
|
|
273
|
+
and "hooks/session-start.py" in hook["command"]
|
|
274
|
+
for hook in hooks
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _bootstrap(harness):
|
|
279
|
+
"""Describe the harness's actual policy delivery mechanism.
|
|
280
|
+
|
|
281
|
+
Presence is intentionally a payload check, not a claim that the host ran
|
|
282
|
+
it. Codex hook trust is visible only in its `/hooks` UI, and neither
|
|
283
|
+
Hermes nor OpenCode exposes a host-side registration ledger to inspect.
|
|
284
|
+
"""
|
|
285
|
+
hook = os.path.join(PAYLOAD, "hooks", "session-start.py")
|
|
286
|
+
if harness in ("claude", "codex", "cursor"):
|
|
287
|
+
manifest = os.path.join(
|
|
288
|
+
PAYLOAD, "hooks", "hooks-cursor.json" if harness == "cursor" else "hooks.json"
|
|
289
|
+
)
|
|
290
|
+
manifest_data = _read_json(manifest)
|
|
291
|
+
data = {"kind": "session hook", "hook": hook,
|
|
292
|
+
"present": os.path.isfile(hook),
|
|
293
|
+
"executable": os.access(hook, os.X_OK),
|
|
294
|
+
"manifest": manifest,
|
|
295
|
+
"manifest_valid": _hook_manifest_valid(manifest_data, harness),
|
|
296
|
+
"required": ("present", "executable", "manifest_valid")}
|
|
297
|
+
if harness == "codex":
|
|
298
|
+
data["trust_instruction"] = (
|
|
299
|
+
"Codex cannot be verified from disk; run /hooks and confirm this plugin is trusted."
|
|
300
|
+
)
|
|
301
|
+
return data
|
|
302
|
+
if harness == "opencode":
|
|
303
|
+
plugin = os.path.join(PAYLOAD, "adapters", "opencode", "plugin.js")
|
|
304
|
+
return {"kind": "config.instructions", "plugin": plugin,
|
|
305
|
+
"present": os.path.isfile(plugin),
|
|
306
|
+
"instructions": _contains(plugin, "config.instructions"),
|
|
307
|
+
"required": ("present", "instructions")}
|
|
308
|
+
if harness == "hermes":
|
|
309
|
+
entry = os.path.join(os.path.dirname(PAYLOAD), "..", "__init__.py")
|
|
310
|
+
entry = os.path.abspath(entry)
|
|
311
|
+
return {"kind": "registration + first-tool-result fallback", "entrypoint": entry,
|
|
312
|
+
"present": os.path.isfile(entry),
|
|
313
|
+
"registration": _contains(entry, "def register(ctx)"),
|
|
314
|
+
"fallback": _contains(
|
|
315
|
+
entry, 'register_hook("transform_tool_result", _on_transform_tool_result)'
|
|
316
|
+
),
|
|
317
|
+
"required": ("present", "registration", "fallback")}
|
|
318
|
+
return {"kind": "unknown", "present": True, "required": ()}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _bootstrap_valid(bootstrap):
|
|
322
|
+
return all(bootstrap.get(key) for key in bootstrap.get("required", ("present",)))
|
|
323
|
+
|
|
324
|
+
|
|
151
325
|
def _skills():
|
|
152
326
|
shipped = {}
|
|
153
327
|
for root in ("skills", "skills-claude"):
|
|
@@ -161,32 +335,74 @@ def _skills():
|
|
|
161
335
|
|
|
162
336
|
def collect(argv=()):
|
|
163
337
|
harness, source = _detect_harness(argv)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
338
|
+
manifest_path = os.path.join(PAYLOAD, ".claude-plugin", "plugin.json")
|
|
339
|
+
models_path = os.path.join(PAYLOAD, "config", "models.json")
|
|
340
|
+
manifest_raw = _read_json(manifest_path)
|
|
341
|
+
models_raw = _read_json(models_path)
|
|
342
|
+
manifest = manifest_raw if isinstance(manifest_raw, dict) else {}
|
|
343
|
+
models = models_raw if isinstance(models_raw, dict) else {}
|
|
344
|
+
harnesses = models.get("harnesses")
|
|
345
|
+
harnesses = harnesses if isinstance(harnesses, dict) else {}
|
|
346
|
+
config = harnesses.get(harness)
|
|
347
|
+
config = config if isinstance(config, dict) else {}
|
|
348
|
+
bootstrap = _bootstrap(harness)
|
|
168
349
|
local = _local_root()
|
|
169
350
|
skills = _skills()
|
|
170
|
-
|
|
171
|
-
|
|
351
|
+
skill_config = models.get("skills")
|
|
352
|
+
skill_config = skill_config if isinstance(skill_config, dict) else {}
|
|
353
|
+
claude_only_raw = skill_config.get("claudeOnly")
|
|
354
|
+
claude_only = ({item for item in claude_only_raw if isinstance(item, str)}
|
|
355
|
+
if isinstance(claude_only_raw, list) else set())
|
|
356
|
+
exclusions = skill_config.get("exclude")
|
|
357
|
+
exclusions = exclusions if isinstance(exclusions, dict) else {}
|
|
358
|
+
excluded_raw = exclusions.get(harness)
|
|
359
|
+
excluded = ({item for item in excluded_raw if isinstance(item, str)}
|
|
360
|
+
if isinstance(excluded_raw, list) else set())
|
|
172
361
|
|
|
173
362
|
registered = [n for n in skills["skills"] if n not in excluded]
|
|
174
363
|
if harness == "claude":
|
|
175
364
|
registered += skills["skills-claude"]
|
|
365
|
+
tiers = {}
|
|
366
|
+
for tier in TIERS:
|
|
367
|
+
mapping = config.get(tier)
|
|
368
|
+
mapping = mapping if isinstance(mapping, dict) else {}
|
|
369
|
+
tiers[tier] = {"model": mapping.get("model"), "effort": mapping.get("effort")}
|
|
370
|
+
manifest_valid = _manifest_valid(manifest)
|
|
371
|
+
models_valid = _models_valid(models)
|
|
372
|
+
tier_mapping_valid = harness == "unknown" or all(
|
|
373
|
+
isinstance(value["model"], str) and bool(value["model"])
|
|
374
|
+
for value in tiers.values()
|
|
375
|
+
)
|
|
376
|
+
python_report = {
|
|
377
|
+
"running": ".".join(str(v) for v in sys.version_info[:3]),
|
|
378
|
+
"minimum": ".".join(str(v) for v in MIN_PYTHON),
|
|
379
|
+
"supported": sys.version_info[:2] >= MIN_PYTHON,
|
|
380
|
+
}
|
|
381
|
+
breadcrumbs = _breadcrumbs()
|
|
382
|
+
degraded_reasons = []
|
|
383
|
+
if harness == "unknown":
|
|
384
|
+
degraded_reasons.append("unknown harness")
|
|
385
|
+
if "opencode-skills.log" in breadcrumbs:
|
|
386
|
+
degraded_reasons.append("OpenCode namespaced skills previously failed to register")
|
|
387
|
+
if not python_report["supported"]:
|
|
388
|
+
degraded_reasons.append("unsupported Python")
|
|
389
|
+
if not (manifest_valid and models_valid and tier_mapping_valid):
|
|
390
|
+
degraded_reasons.append("invalid required payload")
|
|
391
|
+
if harness != "unknown" and not _bootstrap_valid(bootstrap):
|
|
392
|
+
degraded_reasons.append("missing or invalid required bootstrap")
|
|
176
393
|
|
|
177
394
|
return {
|
|
178
395
|
"harness": {"value": harness, "source": source},
|
|
179
|
-
"payload": {
|
|
180
|
-
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
|
|
185
|
-
"tiers": {
|
|
186
|
-
tier: {"model": (config.get(tier) or {}).get("model"),
|
|
187
|
-
"effort": (config.get(tier) or {}).get("effort")}
|
|
188
|
-
for tier in TIERS
|
|
396
|
+
"payload": {
|
|
397
|
+
"path": PAYLOAD,
|
|
398
|
+
"version": manifest.get("version"),
|
|
399
|
+
"manifest": manifest_path,
|
|
400
|
+
"models": models_path,
|
|
401
|
+
"valid": manifest_valid and models_valid and tier_mapping_valid,
|
|
189
402
|
},
|
|
403
|
+
"bootstrap": bootstrap,
|
|
404
|
+
"python": python_report,
|
|
405
|
+
"tiers": tiers,
|
|
190
406
|
"local_state": {
|
|
191
407
|
"path": local,
|
|
192
408
|
"present": os.path.isdir(local),
|
|
@@ -199,7 +415,13 @@ def collect(argv=()):
|
|
|
199
415
|
"expected_here": sorted(registered),
|
|
200
416
|
"excluded_here": sorted(excluded | (set() if harness == "claude" else claude_only)),
|
|
201
417
|
},
|
|
202
|
-
"breadcrumbs":
|
|
418
|
+
"breadcrumbs": breadcrumbs,
|
|
419
|
+
# Most breadcrumbs are deliberately untrusted history. The OpenCode
|
|
420
|
+
# shadow-skills log is different: it means this payload could not
|
|
421
|
+
# register the namespaced skill tree, so its presence is a durable
|
|
422
|
+
# degraded capability signal even though it cannot timestamp a turn.
|
|
423
|
+
"status": "degraded" if degraded_reasons else "healthy",
|
|
424
|
+
"degraded_reasons": degraded_reasons,
|
|
203
425
|
}
|
|
204
426
|
|
|
205
427
|
|
|
@@ -222,10 +444,13 @@ def _render(data):
|
|
|
222
444
|
row("version", data["payload"]["version"] or "unknown", "disk")
|
|
223
445
|
|
|
224
446
|
boot = data["bootstrap"]
|
|
225
|
-
state = "present" if boot
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
447
|
+
state = "present" if _bootstrap_valid(boot) else "MISSING or invalid"
|
|
448
|
+
row("bootstrap", f"{boot['kind']}: {state}", "disk")
|
|
449
|
+
if boot.get("trust_instruction"):
|
|
450
|
+
row("Codex hooks", boot["trust_instruction"], "manual check")
|
|
451
|
+
python = data["python"]
|
|
452
|
+
row("python", f"{python['running']} (minimum {python['minimum']})",
|
|
453
|
+
"supported" if python["supported"] else "UNSUPPORTED")
|
|
229
454
|
|
|
230
455
|
tiers = " · ".join(
|
|
231
456
|
f"{t.capitalize()} {v['model']}" + (f"/{v['effort']}" if v["effort"] else "")
|
|
@@ -248,6 +473,9 @@ def _render(data):
|
|
|
248
473
|
for target in mem["targets"]:
|
|
249
474
|
mark = "projected" if target["projected"] else "not projected"
|
|
250
475
|
row("", f" {target['harness']}: {mark}", target["path"])
|
|
476
|
+
hermes = mem.get("hermes_projection")
|
|
477
|
+
if hermes:
|
|
478
|
+
row("Hermes memory", hermes["status"], hermes["path"])
|
|
251
479
|
|
|
252
480
|
skills = data["skills"]
|
|
253
481
|
row("skills shipped",
|
|
@@ -271,13 +499,21 @@ def _render(data):
|
|
|
271
499
|
return "\n".join(lines)
|
|
272
500
|
|
|
273
501
|
|
|
502
|
+
def _exit_code(data):
|
|
503
|
+
if not data["payload"].get("valid") or not data["python"].get("supported"):
|
|
504
|
+
return 1
|
|
505
|
+
if data["harness"]["value"] != "unknown" and not _bootstrap_valid(data["bootstrap"]):
|
|
506
|
+
return 1
|
|
507
|
+
return 0
|
|
508
|
+
|
|
509
|
+
|
|
274
510
|
def main(argv):
|
|
275
511
|
data = collect(argv)
|
|
276
512
|
if "--json" in argv:
|
|
277
513
|
print(json.dumps(data, indent=1, sort_keys=True))
|
|
278
514
|
else:
|
|
279
515
|
print(_render(data))
|
|
280
|
-
return
|
|
516
|
+
return _exit_code(data)
|
|
281
517
|
|
|
282
518
|
|
|
283
519
|
if __name__ == "__main__":
|
package/scripts/ghreview.py
CHANGED
|
@@ -265,7 +265,7 @@ query($owner: String!, $name: String!, $number: Int!, $cursor: String) {
|
|
|
265
265
|
reviewThreads(first: 50, after: $cursor) {
|
|
266
266
|
pageInfo { hasNextPage endCursor }
|
|
267
267
|
nodes {
|
|
268
|
-
id isResolved isOutdated path line
|
|
268
|
+
id isResolved isOutdated path line originalLine
|
|
269
269
|
comments(first: 100) {
|
|
270
270
|
# 100 covers the overwhelming majority of threads without inner
|
|
271
271
|
# pagination; a thread with >100 comments truncates here, so
|
|
@@ -387,6 +387,7 @@ def cmd_threads(a):
|
|
|
387
387
|
"thread_id": node["id"],
|
|
388
388
|
"path": node["path"],
|
|
389
389
|
"line": node["line"],
|
|
390
|
+
"original_line": node.get("originalLine"),
|
|
390
391
|
"is_resolved": node["isResolved"],
|
|
391
392
|
"is_outdated": node["isOutdated"],
|
|
392
393
|
"replies_after_mine": (last.get("author") or {}).get("login") != login,
|
|
@@ -483,8 +484,11 @@ def cmd_stage(a):
|
|
|
483
484
|
# reflects the first pass's snapped output) so the reported snap is
|
|
484
485
|
# the real one-hop mapping, not a fictitious second hop.
|
|
485
486
|
staged, snapped2, dropped2 = validate_comments(comments, build_maps(files))
|
|
486
|
-
|
|
487
|
-
|
|
487
|
+
# The retry replaces the first attempt's outcome. Reporting both
|
|
488
|
+
# attempts as one result makes a comment appear both snapped and
|
|
489
|
+
# dropped even though only the final revalidation is stageable.
|
|
490
|
+
report["snapped"] = snapped2
|
|
491
|
+
report["dropped"] = dropped2
|
|
488
492
|
if not staged:
|
|
489
493
|
print(json.dumps({**report, "note": "nothing left to stage after revalidation"}))
|
|
490
494
|
sys.exit(1)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const parser = require(process.argv[2]);
|
|
4
|
+
try {
|
|
5
|
+
const request = JSON.parse(require("fs").readFileSync(0, "utf8"));
|
|
6
|
+
const errors = [];
|
|
7
|
+
const value = parser.parse(request.text, errors, { allowTrailingComma: true, disallowComments: false });
|
|
8
|
+
if (errors.length) throw new Error("invalid JSONC (parse error " + errors[0].error + ")");
|
|
9
|
+
if (request.mode === "parse") process.stdout.write(JSON.stringify({ value }));
|
|
10
|
+
else {
|
|
11
|
+
let text = request.text;
|
|
12
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
13
|
+
const indentation = text.match(/(?:\r?\n)([ \t]+)"/);
|
|
14
|
+
const indent = indentation ? indentation[1] : " ";
|
|
15
|
+
for (const edit of request.edits || []) {
|
|
16
|
+
text = parser.applyEdits(text, parser.modify(text, edit.path, edit.value, {
|
|
17
|
+
isArrayInsertion: false,
|
|
18
|
+
formattingOptions: { insertSpaces: !indent.includes("\t"), tabSize: indent.includes("\t") ? 1 : Math.max(1, indent.length), eol, keepLines: true },
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
process.stdout.write(JSON.stringify({ text }));
|
|
22
|
+
}
|
|
23
|
+
} catch (error) { process.stderr.write(String(error && error.message || error)); process.exit(2); }
|