cohorte 2.1.0 → 2.2.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +41 -32
  3. package/bin/cli.js +316 -26
  4. package/core/adapter/render.js +389 -0
  5. package/core/agents/implementer.template.md +3 -3
  6. package/core/agents/release.md +1 -4
  7. package/core/agents/review.md +10 -2
  8. package/core/commands/cohorte-audit.md +2 -0
  9. package/core/commands/cohorte-brainstorm.md +3 -6
  10. package/core/commands/cohorte-build.md +14 -17
  11. package/core/commands/cohorte-doctor.md +59 -28
  12. package/core/commands/cohorte-fix.md +2 -3
  13. package/core/commands/cohorte-init-pipeline.md +7 -8
  14. package/core/commands/cohorte-refactor.md +5 -2
  15. package/core/commands/cohorte-review.md +20 -16
  16. package/core/commands/cohorte-ship.md +5 -5
  17. package/core/commands/cohorte-spec.md +3 -7
  18. package/core/commands/cohorte-update-pipeline.md +8 -8
  19. package/core/hooks/gate.py +203 -16
  20. package/core/runtimes/claude.json +73 -0
  21. package/core/runtimes/codex.json +82 -0
  22. package/core/runtimes/cursor.json +75 -0
  23. package/core/runtimes/gemini.json +75 -0
  24. package/core/runtimes/opencode.json +72 -0
  25. package/core/templates/spec.template.md +1 -3
  26. package/core/templates/steps/init-pipeline/01-detect-stack.md +1 -1
  27. package/core/templates/steps/init-pipeline/02-interview-gaps.md +2 -2
  28. package/core/templates/steps/init-pipeline/04-write-render.md +23 -17
  29. package/core/templates/steps/init-pipeline/05-report.md +1 -1
  30. package/dashboard/dist/assets/{index-P1I1JGtj.js → index-D1rsbLat.js} +1 -1
  31. package/dashboard/dist/index.html +1 -1
  32. package/dashboard/server/doctor.js +156 -69
  33. package/dashboard/server/index.js +12 -2
  34. package/dashboard/server/metrics.js +13 -6
  35. package/dashboard/server/runtime.js +115 -0
  36. package/dashboard/server/versions.js +12 -1
  37. package/install.ps1 +23 -2
  38. package/install.sh +22 -4
  39. package/package.json +6 -2
  40. package/profile/PIPELINE.template.md +7 -6
  41. package/profile/SCHEMA.md +45 -48
  42. package/scripts/kanban-move.sh +11 -1
  43. package/scripts/metrics/collect.mjs +5 -3
  44. package/scripts/preflight.sh +27 -8
  45. package/scripts/telemetry-send.sh +10 -3
  46. package/scripts/test-adapter.mjs +368 -0
  47. package/scripts/test-dashboard.mjs +70 -0
  48. package/scripts/test-gate.mjs +62 -0
  49. package/scripts/validate-core.mjs +1 -1
  50. package/core/commands/cohorte-loop.md +0 -110
  51. package/scripts/loop-detach.sh +0 -153
  52. package/scripts/loop.sh +0 -399
  53. package/scripts/test-loop.mjs +0 -330
@@ -40,6 +40,33 @@ Two extra duties beyond Bash patterns:
40
40
 
41
41
  Protocol: reads the PreToolUse payload on stdin; emits a JSON permissionDecision
42
42
  of "deny" or "ask" on a match; otherwise exits 0 silently.
43
+
44
+ Four runtimes can run this as a real blocking hook, and they disagree on the envelope —
45
+ `--runtime <id>` (passed by the installer when it registers the hook) selects the dialect:
46
+
47
+ claude PreToolUse {"hookSpecificOutput": {"permissionDecision": "deny"|"ask", …}}
48
+ codex PreToolUse same envelope, but `ask` is parsed and NOT honoured
49
+ cursor beforeShellExecution {"permission": "deny"|"ask", "user_message", "agent_message"}
50
+ gemini BeforeTool {"decision": "deny", "reason": …} (no ask tier)
51
+
52
+ Where a runtime has no `ask`, an `ask` verdict is escalated to `deny` with the reason
53
+ attached rather than downgraded to allow: the whole point of the tier is that a human sees
54
+ the command before it runs, and a runtime that cannot ask cannot deliver that — so the safe
55
+ translation is to refuse and let the human re-run it deliberately. Same rule the
56
+ bypassPermissions escalation has always used.
57
+
58
+ Explicit mode (`--check`) — for OpenCode, whose extension point is plugins rather than a
59
+ blocking hook. Same config, same patterns, same verdicts; the difference is who calls it.
60
+ There the rendered commands instruct the agent to run the check itself before a gated
61
+ command or a phase dispatch:
62
+
63
+ gate.py --check "git push --force" # a Bash command
64
+ gate.py --check-dispatch review # the preflight phase gate
65
+
66
+ It prints ONE line (`gate: allow` / `gate: ask — <reason>` / `gate: deny — <reason>`)
67
+ and exits 0 / 1 / 2 so a shell can branch on it. This is advisory by construction — an
68
+ agent can decline to call it, which a hook makes impossible — so /cohorte-doctor reports
69
+ the gate as "advisory" on those runtimes rather than pretending parity.
43
70
  """
44
71
 
45
72
  import json
@@ -54,13 +81,37 @@ import time
54
81
  SPLIT = re.compile(r"&&|\|\||[;|\n]")
55
82
  WS = re.compile(r"\s+")
56
83
 
84
+ # How far to backdate the throwaway index in tree_digest(), in seconds. Anything modified
85
+ # within this window of the digest is re-hashed by content instead of trusted from its cached
86
+ # stat data. Must stay ≥ the filesystem's mtime granularity (1 s on ext4/HFS+); 5 s covers a
87
+ # clock that ticks backwards a little without costing anything on a quiet tree.
88
+ RACY_WINDOW_S = 5
89
+
57
90
 
58
91
  def project_root() -> str:
59
- return os.environ.get("CLAUDE_PROJECT_DIR", ".")
92
+ return os.environ.get("COHORTE_PROJECT_DIR") or os.environ.get("CLAUDE_PROJECT_DIR", ".")
93
+
94
+
95
+ # The generated per-project files (gate config + preflight stamp) live under `.claude/`
96
+ # on a Claude install and `.cohorte/` on every other runtime. Probe both, newest layout
97
+ # first, so one gate serves a repo whichever agent is driving it — and so a repo that
98
+ # adds a second runtime doesn't end up with two configs, one of them silently unread.
99
+ STATE_DIRS = (".cohorte", ".claude")
100
+
101
+
102
+ def state_path(name: str) -> str:
103
+ """Path to a per-project pipeline file: the first layout that has it, else `.claude`
104
+ (the historical default, so a fresh write lands where existing tooling looks)."""
105
+ root = project_root()
106
+ for d in STATE_DIRS:
107
+ p = os.path.join(root, d, name)
108
+ if os.path.exists(p):
109
+ return p
110
+ return os.path.join(root, ".claude", name)
60
111
 
61
112
 
62
113
  def load_config() -> dict:
63
- path = os.path.join(project_root(), ".claude", "gate-config.json")
114
+ path = state_path("gate-config.json")
64
115
  empty = {"deny": [], "ask": [], "ask_on_default_branch": [], "default_branch": "main",
65
116
  "preflight": {}}
66
117
  try:
@@ -187,7 +238,7 @@ def worktree_dirs(cwd: str):
187
238
 
188
239
  def tree_digest(cwd: str):
189
240
  """The digest pipeline/scripts/preflight.sh stamps for the checkout at `cwd`: the git
190
- tree id of the working tree, computed in a throwaway index (`.claude` and `specs`
241
+ tree id of the working tree, computed in a throwaway index (`.claude`, `.cohorte` and `specs`
191
242
  excluded — the pipeline writes those itself). Content-addressed, so a commit of the
192
243
  same code keeps the stamp valid and any real edit invalidates it. Must stay identical
193
244
  to the shell side. Returns None when git can't answer, and the caller falls back."""
@@ -206,17 +257,29 @@ def tree_digest(cwd: str):
206
257
  # what changed instead of the whole tree on every dispatch.
207
258
  if os.path.exists(real_index):
208
259
  shutil.copyfile(real_index, tmp)
260
+ # Age the copy by a few seconds. Git trusts an entry's cached stat data only when
261
+ # the entry's mtime is OLDER than the index file's; entries at or after it are
262
+ # "racily clean" and get re-hashed by content. copyfile stamps the temp index with
263
+ # `now`, so a file edited in the same second as the preflight — with an unchanged
264
+ # size — looked clean here and the gate greened code that had changed. Backdating
265
+ # forces a content check for anything touched inside that window, and leaves the
266
+ # stat-cache fast path (the whole point of seeding from the real index) intact for
267
+ # every older file.
268
+ old = time.time() - RACY_WINDOW_S
269
+ os.utime(tmp, (old, old))
209
270
  else:
210
271
  os.unlink(tmp) # a 0-byte index is a corrupt index — let git create it
211
272
  env = dict(os.environ, GIT_INDEX_FILE=tmp)
212
273
  # Drop the excluded paths outright — an `add` exclude only stops them being
213
274
  # *updated*, so already-tracked ones would still shift the tree id.
214
275
  subprocess.run(
215
- ["git", "rm", "--cached", "-r", "-q", "--ignore-unmatch", "--", ".claude", "specs"],
276
+ ["git", "rm", "--cached", "-r", "-q", "--ignore-unmatch", "--",
277
+ ".claude", ".cohorte", "specs"],
216
278
  cwd=cwd, capture_output=True, timeout=10, env=env,
217
279
  )
218
280
  add = subprocess.run(
219
- ["git", "add", "-A", "--", ".", ":(exclude).claude", ":(exclude)specs"],
281
+ ["git", "add", "-A", "--", ".",
282
+ ":(exclude).claude", ":(exclude).cohorte", ":(exclude)specs"],
220
283
  cwd=cwd, capture_output=True, timeout=30, env=env,
221
284
  )
222
285
  if add.returncode != 0:
@@ -250,11 +313,17 @@ def check_preflight(payload: dict, cfg: dict) -> int:
250
313
  if not pf.get("enabled"):
251
314
  return 0
252
315
  agents = pf.get("agents") or ["review"]
316
+ # Runtimes name a subagent dispatch differently. Claude Code and Cursor send a `Task` tool
317
+ # carrying `subagent_type`; Gemini exposes each subagent as a tool of the SAME NAME, so the
318
+ # dispatch arrives as `tool_name: review`. Accept both rather than gating only the shape
319
+ # one vendor happens to use — a phase gate that silently never fires is the 1.3.0 bug.
253
320
  subagent = (payload.get("tool_input") or {}).get("subagent_type", "") or ""
321
+ if not subagent and payload.get("tool_name") in agents:
322
+ subagent = payload.get("tool_name")
254
323
  if subagent not in agents:
255
324
  return 0
256
325
 
257
- stamp = os.path.join(project_root(), ".claude", "preflight.ok")
326
+ stamp = state_path("preflight.ok")
258
327
  why = None
259
328
  try:
260
329
  with open(stamp, "r", encoding="utf-8") as fh:
@@ -300,20 +369,65 @@ def check_preflight(payload: dict, cfg: dict) -> int:
300
369
  )
301
370
 
302
371
 
303
- def main() -> int:
304
- try:
305
- payload = json.load(sys.stdin)
306
- except Exception:
307
- return 0 # malformed input don't block
308
-
309
- tool = payload.get("tool_name")
372
+ def check_cli(argv) -> int:
373
+ """Explicit gate for runtimes with no PreToolUse hook.
374
+
375
+ Builds the same payload the hook would have received and runs the same evaluation,
376
+ then translates the verdict into a line + exit code a shell (or an agent reading the
377
+ output) can act on. Exit 0 = allow, 1 = ask, 2 = deny — chosen so `&&` naturally
378
+ stops on anything that isn't a clean allow."""
379
+ mode = argv[0]
380
+ unattended = False
381
+ cwd = os.getcwd()
382
+ words = []
383
+ i = 1
384
+ while i < len(argv):
385
+ a = argv[i]
386
+ if a == "--unattended":
387
+ unattended = True
388
+ elif a == "--cwd" and i + 1 < len(argv):
389
+ cwd = argv[i + 1]
390
+ i += 1
391
+ elif a.startswith("--cwd="):
392
+ cwd = a[6:]
393
+ else:
394
+ words.append(a)
395
+ i += 1
396
+ subject = " ".join(words).strip()
397
+ if not subject:
398
+ print(f"gate: usage — gate.py {mode} <{'command' if mode == '--check' else 'agent'}>",
399
+ file=sys.stderr)
400
+ return 2
401
+
402
+ payload = {"cwd": cwd, "permission_mode": "bypassPermissions" if unattended else "default"}
403
+ if mode == "--check":
404
+ payload["tool_name"] = "Bash"
405
+ payload["tool_input"] = {"command": subject}
406
+ else:
407
+ payload["tool_name"] = "Task"
408
+ payload["tool_input"] = {"subagent_type": subject}
409
+
410
+ # Reuse the hook path verbatim — one evaluator, so the two modes can never drift into
411
+ # different verdicts on the same config. `decide()` records instead of printing while
412
+ # DECISIONS is armed, so the hook's JSON never leaks into the human-readable output.
413
+ global DECISIONS
414
+ DECISIONS = []
310
415
  cfg = load_config()
416
+ if payload["tool_name"] == "Task":
417
+ check_preflight(payload, cfg)
418
+ else:
419
+ check_bash(payload, cfg)
311
420
 
312
- if tool == "Task":
313
- return check_preflight(payload, cfg)
314
- if tool != "Bash":
421
+ if not DECISIONS:
422
+ print("gate: allow")
315
423
  return 0
424
+ decision, reason = DECISIONS[0]
425
+ print(f"gate: {decision} — {reason}")
426
+ return 2 if decision == "deny" else 1
427
+
316
428
 
429
+ def check_bash(payload: dict, cfg: dict) -> int:
430
+ """Pattern gate on a Bash command. Split out of main() so --check reuses it."""
317
431
  command = (payload.get("tool_input") or {}).get("command", "") or ""
318
432
  deny, ask, branch_gated = cfg["deny"], cfg["ask"], cfg["ask_on_default_branch"]
319
433
  default = cfg["default_branch"]
@@ -368,7 +482,80 @@ def main() -> int:
368
482
  return 0
369
483
 
370
484
 
485
+ def main() -> int:
486
+ argv = sys.argv[1:]
487
+ if argv and argv[0] in ("--check", "--check-dispatch"):
488
+ return check_cli(argv)
489
+
490
+ global RUNTIME
491
+ for i, a in enumerate(argv):
492
+ if a == "--runtime" and i + 1 < len(argv):
493
+ RUNTIME = argv[i + 1]
494
+ elif a.startswith("--runtime="):
495
+ RUNTIME = a[10:]
496
+
497
+ try:
498
+ payload = json.load(sys.stdin)
499
+ except Exception:
500
+ return 0 # malformed input → don't block
501
+
502
+ tool = payload.get("tool_name")
503
+ # Cursor's shell hook carries the command at the top level rather than in tool_input, and
504
+ # names no tool. Normalise once here so every check below stays runtime-agnostic.
505
+ if RUNTIME == "cursor" and not tool:
506
+ tool = "Bash"
507
+ payload = dict(payload, tool_name="Bash",
508
+ tool_input=payload.get("tool_input") or {"command": payload.get("command", "")})
509
+ cfg = load_config()
510
+
511
+ # A dispatch, in whichever shape this runtime sends it (see check_preflight).
512
+ if tool == "Task" or tool in ((cfg.get("preflight") or {}).get("agents") or ["review"]):
513
+ return check_preflight(payload, cfg)
514
+ # Bash is Claude's/Codex's name for the shell tool; Gemini calls it run_shell_command, and
515
+ # Cursor's beforeShellExecution was normalised to "Bash" above. Anything else is not a
516
+ # command we gate.
517
+ if tool not in ("Bash", "shell", "run_shell_command"):
518
+ return 0
519
+ return check_bash(payload, cfg)
520
+
521
+
522
+ # Armed by --check: while it is a list, decide() records the verdict instead of emitting
523
+ # a hook envelope, which only a hook host understands.
524
+ DECISIONS = None
525
+
526
+ # Which runtime is hosting this hook — set from `--runtime <id>`. Defaults to Claude Code:
527
+ # it is the only host that ran this file before the flag existed, so an old registration
528
+ # that predates the adapter keeps behaving exactly as it did.
529
+ RUNTIME = "claude"
530
+
531
+ # Runtimes whose hook contract has no "ask" tier. Codex parses `permissionDecision: ask` and
532
+ # ignores it — which would let a gated command through — and Gemini's BeforeTool only
533
+ # allows or denies. Escalate rather than downgrade: see the module docstring.
534
+ NO_ASK = {"codex", "gemini"}
535
+
536
+
371
537
  def decide(decision: str, reason: str) -> int:
538
+ if DECISIONS is not None:
539
+ DECISIONS.append((decision, reason))
540
+ return 0
541
+
542
+ if decision == "ask" and RUNTIME in NO_ASK:
543
+ decision = "deny"
544
+ reason += (f" (denied outright: {RUNTIME} has no confirmation tier, so an \"ask\" here "
545
+ f"would silently run. Re-run it yourself if you meant to.)")
546
+
547
+ if RUNTIME == "cursor":
548
+ # Cursor's own envelope. `agent_message` is what the model is told, `user_message`
549
+ # what the human sees — the reason serves both.
550
+ print(json.dumps({"permission": decision, "user_message": reason, "agent_message": reason}))
551
+ return 2 if decision == "deny" else 0
552
+
553
+ if RUNTIME == "gemini":
554
+ # BeforeTool speaks {"decision", "reason"}; anything else is read as allow.
555
+ print(json.dumps({"decision": decision, "reason": reason}))
556
+ return 0
557
+
558
+ # Claude Code and Codex share the PreToolUse envelope.
372
559
  print(
373
560
  json.dumps(
374
561
  {
@@ -0,0 +1,73 @@
1
+ {
2
+ "id": "claude",
3
+ "label": "Claude Code",
4
+ "docs": "https://docs.claude.com/en/docs/claude-code/slash-commands",
5
+ "detect": [
6
+ "~/.claude",
7
+ "~/.config/claude"
8
+ ],
9
+ "scopes": {
10
+ "global": {
11
+ "root": "~/.claude",
12
+ "core": "~/.claude",
13
+ "commands": "~/.claude/commands",
14
+ "agents": "~/.claude/agents",
15
+ "hooks": "~/.claude/hooks",
16
+ "workflows": "~/.claude/workflows",
17
+ "settings": "~/.claude/settings.json",
18
+ "hooks_config": "~/.claude/settings.json"
19
+ },
20
+ "project": {
21
+ "root": ".claude",
22
+ "core": ".claude",
23
+ "commands": ".claude/commands",
24
+ "agents": ".claude/agents",
25
+ "hooks": ".claude/hooks",
26
+ "workflows": ".claude/workflows",
27
+ "settings": ".claude/settings.json",
28
+ "hooks_config": ".claude/settings.json"
29
+ }
30
+ },
31
+ "exclude_commands": [],
32
+ "command": {
33
+ "format": "md",
34
+ "ext": ".md",
35
+ "frontmatter": [
36
+ "model",
37
+ "description",
38
+ "argument-hint",
39
+ "allowed-tools",
40
+ "disable-model-invocation"
41
+ ],
42
+ "args": "$ARGUMENTS",
43
+ "invoke": "/<name>"
44
+ },
45
+ "agent": {
46
+ "format": "md",
47
+ "frontmatter": [
48
+ "name",
49
+ "description",
50
+ "tools",
51
+ "model"
52
+ ],
53
+ "dispatch": "Task tool, subagent_type: <name>",
54
+ "address": "<name>",
55
+ "ext": ".md"
56
+ },
57
+ "capabilities": {
58
+ "subagents": true,
59
+ "hooks": true,
60
+ "workflows": true,
61
+ "mcp": true,
62
+ "tool_restriction": true
63
+ },
64
+ "memory": "CLAUDE.md",
65
+ "headless": "claude -p",
66
+ "hook": {
67
+ "format": "claude",
68
+ "event": "PreToolUse",
69
+ "matcher": "Bash|Task",
70
+ "supports_ask": true,
71
+ "config_shape": "settings"
72
+ }
73
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "id": "codex",
3
+ "label": "Codex CLI (OpenAI)",
4
+ "docs": "https://learn.chatgpt.com/docs/build-skills",
5
+ "detect": [
6
+ "~/.codex"
7
+ ],
8
+ "notes": [
9
+ "Commands ship as SKILLS (.agents/skills/<name>/SKILL.md), not as custom prompts. Prompts are",
10
+ "deprecated, and they are user-scoped only (~/.codex/prompts) \u2014 so a teammate cloning the repo",
11
+ "would get the profile but none of the commands. Skills are discovered from `.agents/skills` in",
12
+ "the repo, which means they travel with it. Invoked as $<name>, or implicitly from `description`.",
13
+ "Skills do NOT substitute arguments, so the preamble explains $ARGUMENTS rather than rewriting it.",
14
+ "Subagents are TOML, one file per agent, and the body lives in `developer_instructions`.",
15
+ "Hooks use the SAME PreToolUse envelope as Claude Code, with one difference that matters:",
16
+ "`permissionDecision: ask` is parsed but not honoured, so gate.py escalates ask to deny here.",
17
+ "Model pins are NOT carried across: the profile names Anthropic aliases (sonnet/haiku), which are meaningless here and would either error or be silently ignored. Agents and commands inherit this runtime's own model selection instead."
18
+ ],
19
+ "scopes": {
20
+ "global": {
21
+ "root": "~/.codex",
22
+ "core": "~/.cohorte/codex",
23
+ "commands": "~/.agents/skills",
24
+ "agents": "~/.codex/agents",
25
+ "hooks": "~/.cohorte/codex/hooks",
26
+ "hooks_config": "~/.codex/hooks.json",
27
+ "workflows": null,
28
+ "settings": null
29
+ },
30
+ "project": {
31
+ "root": ".codex",
32
+ "core": ".cohorte/codex",
33
+ "commands": ".agents/skills",
34
+ "agents": ".codex/agents",
35
+ "hooks": ".cohorte/codex/hooks",
36
+ "hooks_config": ".codex/hooks.json",
37
+ "workflows": null,
38
+ "settings": null
39
+ }
40
+ },
41
+ "exclude_commands": [],
42
+ "command": {
43
+ "format": "skill",
44
+ "ext": "/SKILL.md",
45
+ "frontmatter": [
46
+ "name",
47
+ "description"
48
+ ],
49
+ "args": null,
50
+ "args_note": "the text you typed after the skill name",
51
+ "invoke": "$<name>"
52
+ },
53
+ "agent": {
54
+ "format": "toml",
55
+ "ext": ".toml",
56
+ "body_key": "developer_instructions",
57
+ "frontmatter": [
58
+ "name",
59
+ "description"
60
+ ],
61
+ "readonly_key": "sandbox_mode",
62
+ "readonly_value": "read-only",
63
+ "dispatch": "spawn the named agent; several run in parallel and Codex returns a consolidated result",
64
+ "address": "<name>"
65
+ },
66
+ "hook": {
67
+ "format": "claude",
68
+ "event": "PreToolUse",
69
+ "matcher": "Bash|shell",
70
+ "supports_ask": false,
71
+ "config_shape": "json"
72
+ },
73
+ "capabilities": {
74
+ "subagents": true,
75
+ "hooks": true,
76
+ "workflows": false,
77
+ "mcp": true,
78
+ "tool_restriction": true
79
+ },
80
+ "memory": "AGENTS.md",
81
+ "headless": "codex exec"
82
+ }
@@ -0,0 +1,75 @@
1
+ {
2
+ "id": "cursor",
3
+ "label": "Cursor",
4
+ "docs": "https://cursor.com/docs/cli/overview",
5
+ "detect": [
6
+ "~/.cursor"
7
+ ],
8
+ "notes": [
9
+ "Cursor commands are plain markdown with NO placeholder substitution: whatever the human",
10
+ "types after the command name is appended to the prompt. The adapter therefore explains the",
11
+ "argument placeholder in the preamble rather than rewriting it to a token.",
12
+ "Subagents are markdown + frontmatter, and `readonly: true` is a real tool restriction \u2014",
13
+ "the review agent gets it. Cursor also reads .claude/agents and .codex/agents.",
14
+ "Hooks use Cursor's own envelope ({\"permission\": \u2026}), not Claude's.",
15
+ "Model pins are NOT carried across: the profile names Anthropic aliases (sonnet/haiku), which are meaningless here and would either error or be silently ignored. Agents and commands inherit this runtime's own model selection instead."
16
+ ],
17
+ "scopes": {
18
+ "global": {
19
+ "root": "~/.cursor",
20
+ "core": "~/.cohorte/cursor",
21
+ "commands": "~/.cursor/commands",
22
+ "agents": "~/.cursor/agents",
23
+ "hooks": "~/.cohorte/cursor/hooks",
24
+ "hooks_config": "~/.cursor/hooks.json",
25
+ "workflows": null,
26
+ "settings": null
27
+ },
28
+ "project": {
29
+ "root": ".cursor",
30
+ "core": ".cohorte/cursor",
31
+ "commands": ".cursor/commands",
32
+ "agents": ".cursor/agents",
33
+ "hooks": ".cohorte/cursor/hooks",
34
+ "hooks_config": ".cursor/hooks.json",
35
+ "workflows": null,
36
+ "settings": null
37
+ }
38
+ },
39
+ "exclude_commands": [],
40
+ "command": {
41
+ "format": "md",
42
+ "ext": ".md",
43
+ "frontmatter": [],
44
+ "args": null,
45
+ "args_note": "the text you typed after the command name",
46
+ "invoke": "/<name>"
47
+ },
48
+ "agent": {
49
+ "format": "md",
50
+ "ext": ".md",
51
+ "frontmatter": [
52
+ "name",
53
+ "description"
54
+ ],
55
+ "readonly_key": "readonly",
56
+ "readonly_value": "true",
57
+ "dispatch": "invoke it as /<name> <brief>, or name it in the prompt; several run in parallel",
58
+ "address": "/<name>"
59
+ },
60
+ "hook": {
61
+ "format": "cursor",
62
+ "event": "beforeShellExecution",
63
+ "supports_ask": true,
64
+ "config_shape": "json"
65
+ },
66
+ "capabilities": {
67
+ "subagents": true,
68
+ "hooks": true,
69
+ "workflows": false,
70
+ "mcp": true,
71
+ "tool_restriction": true
72
+ },
73
+ "memory": "AGENTS.md",
74
+ "headless": "cursor-agent -p"
75
+ }
@@ -0,0 +1,75 @@
1
+ {
2
+ "id": "gemini",
3
+ "label": "Gemini CLI",
4
+ "docs": "https://geminicli.com/docs/",
5
+ "detect": [
6
+ "~/.gemini"
7
+ ],
8
+ "notes": [
9
+ "Commands are TOML, not markdown: the whole prompt body becomes the `prompt` key, and the",
10
+ "argument placeholder is {{args}}. Sub-directories namespace with ':'.",
11
+ "Subagents ARE markdown + frontmatter (.gemini/agents), and are exposed to the main agent as",
12
+ "a tool of the same name; @agent-name forces one. They cannot call other subagents.",
13
+ "Hooks live in settings.json under `hooks`, event `BeforeTool`, matched on the tool name",
14
+ "(run_shell_command). The envelope is {\"decision\": \"deny\", \"reason\": \u2026} with no ask tier.",
15
+ "Model pins are NOT carried across: the profile names Anthropic aliases (sonnet/haiku), which are meaningless here and would either error or be silently ignored. Agents and commands inherit this runtime's own model selection instead."
16
+ ],
17
+ "scopes": {
18
+ "global": {
19
+ "root": "~/.gemini",
20
+ "core": "~/.cohorte/gemini",
21
+ "commands": "~/.gemini/commands",
22
+ "agents": "~/.gemini/agents",
23
+ "hooks": "~/.cohorte/gemini/hooks",
24
+ "hooks_config": "~/.gemini/settings.json",
25
+ "workflows": null,
26
+ "settings": "~/.gemini/settings.json"
27
+ },
28
+ "project": {
29
+ "root": ".gemini",
30
+ "core": ".cohorte/gemini",
31
+ "commands": ".gemini/commands",
32
+ "agents": ".gemini/agents",
33
+ "hooks": ".cohorte/gemini/hooks",
34
+ "hooks_config": ".gemini/settings.json",
35
+ "workflows": null,
36
+ "settings": ".gemini/settings.json"
37
+ }
38
+ },
39
+ "exclude_commands": [],
40
+ "command": {
41
+ "format": "toml",
42
+ "ext": ".toml",
43
+ "frontmatter": [
44
+ "description"
45
+ ],
46
+ "args": "{{args}}",
47
+ "invoke": "/<name>"
48
+ },
49
+ "agent": {
50
+ "format": "md",
51
+ "ext": ".md",
52
+ "frontmatter": [
53
+ "name",
54
+ "description"
55
+ ],
56
+ "dispatch": "the subagent is exposed as a tool of the same name; @<name> forces it",
57
+ "address": "@<name>"
58
+ },
59
+ "hook": {
60
+ "format": "gemini",
61
+ "event": "BeforeTool",
62
+ "matcher": "run_shell_command",
63
+ "supports_ask": false,
64
+ "config_shape": "settings"
65
+ },
66
+ "capabilities": {
67
+ "subagents": true,
68
+ "hooks": true,
69
+ "workflows": false,
70
+ "mcp": true,
71
+ "tool_restriction": false
72
+ },
73
+ "memory": "GEMINI.md",
74
+ "headless": "gemini -p"
75
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "id": "opencode",
3
+ "label": "OpenCode",
4
+ "docs": "https://opencode.ai/docs/cli/",
5
+ "detect": [
6
+ "~/.config/opencode",
7
+ "~/.opencode"
8
+ ],
9
+ "notes": [
10
+ "Markdown agents in agents/ with `mode: subagent`, addressed with @<name>.",
11
+ "The one runtime here with NO hook mechanism \u2014 extension is via plugins (.opencode/plugin/*.js),",
12
+ "which is a different contract from a blocking PreToolUse hook. The gate therefore runs in",
13
+ "explicit --check mode, which is advisory by construction.",
14
+ "`opencode run` is the headless primitive; --agent selects an agent.",
15
+ "Model pins are NOT carried across: the profile names Anthropic aliases (sonnet/haiku), which are meaningless here and would either error or be silently ignored. Agents and commands inherit this runtime's own model selection instead."
16
+ ],
17
+ "scopes": {
18
+ "global": {
19
+ "root": "~/.config/opencode",
20
+ "core": "~/.cohorte/opencode",
21
+ "commands": "~/.config/opencode/commands",
22
+ "agents": "~/.config/opencode/agents",
23
+ "hooks": null,
24
+ "workflows": null,
25
+ "settings": null
26
+ },
27
+ "project": {
28
+ "root": ".opencode",
29
+ "core": ".cohorte/opencode",
30
+ "commands": ".opencode/commands",
31
+ "agents": ".opencode/agents",
32
+ "hooks": null,
33
+ "workflows": null,
34
+ "settings": null
35
+ }
36
+ },
37
+ "exclude_commands": [],
38
+ "command": {
39
+ "format": "md",
40
+ "ext": ".md",
41
+ "frontmatter": [
42
+ "description",
43
+ "agent",
44
+ "subtask"
45
+ ],
46
+ "args": "$ARGUMENTS",
47
+ "invoke": "/<name>"
48
+ },
49
+ "agent": {
50
+ "format": "md",
51
+ "frontmatter": [
52
+ "description",
53
+ "mode"
54
+ ],
55
+ "defaults": {
56
+ "mode": "subagent"
57
+ },
58
+ "dispatch": "address the subagent with @<name> and give it the full brief in one message",
59
+ "address": "@<name>",
60
+ "ext": ".md"
61
+ },
62
+ "capabilities": {
63
+ "subagents": true,
64
+ "hooks": false,
65
+ "workflows": false,
66
+ "mcp": true,
67
+ "tool_restriction": false
68
+ },
69
+ "memory": "AGENTS.md",
70
+ "headless": "opencode run",
71
+ "hook": null
72
+ }