cohorte 2.10.1 → 3.0.0-dev.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 (211) hide show
  1. package/README.md +7 -0
  2. package/assets/manifest.json +193 -0
  3. package/assets/migrations/config/README.md +12 -0
  4. package/assets/migrations/state/0001_init.sql +154 -0
  5. package/assets/prompts/README.md +11 -0
  6. package/assets/prompts/agents/fixer.md +7 -0
  7. package/assets/prompts/agents/implementer.md +12 -0
  8. package/assets/prompts/agents/reviewer.md +8 -0
  9. package/assets/prompts/agents/security-reviewer.md +7 -0
  10. package/assets/prompts/phases/build.md +5 -0
  11. package/assets/prompts/phases/review.md +5 -0
  12. package/assets/prompts/phases/test.md +5 -0
  13. package/assets/prompts/system.md +11 -0
  14. package/assets/schemas/agent-output.schema.json +225 -0
  15. package/assets/schemas/auth-status.schema.json +87 -0
  16. package/assets/schemas/command-result.schema.json +192 -0
  17. package/assets/schemas/commands.schema.json +952 -0
  18. package/assets/schemas/config.schema.json +2259 -0
  19. package/assets/schemas/doctor-report.schema.json +104 -0
  20. package/assets/schemas/events.schema.json +8514 -0
  21. package/assets/schemas/fake-script.schema.json +549 -0
  22. package/assets/schemas/inspect.schema.json +3133 -0
  23. package/assets/schemas/manifest.schema.json +79 -0
  24. package/assets/schemas/ownership.schema.json +52 -0
  25. package/assets/schemas/policy-verdict.schema.json +282 -0
  26. package/assets/schemas/project-model.schema.json +783 -0
  27. package/assets/schemas/project-status.schema.json +217 -0
  28. package/assets/schemas/reconcile-plan.schema.json +155 -0
  29. package/assets/schemas/run-diff.schema.json +177 -0
  30. package/assets/schemas/run-snapshot-manifest.schema.json +2104 -0
  31. package/assets/schemas/run-state.schema.json +1755 -0
  32. package/assets/schemas/runtime-capabilities.schema.json +1545 -0
  33. package/assets/schemas/sandbox-capabilities.schema.json +116 -0
  34. package/assets/schemas/skill.schema.json +87 -0
  35. package/assets/schemas/spec.schema.json +202 -0
  36. package/assets/schemas/tool-catalogue.schema.json +655 -0
  37. package/assets/schemas/trust-record.schema.json +38 -0
  38. package/assets/skills/README.md +9 -0
  39. package/assets/skills/inspect-surface.md +4 -0
  40. package/dist/agent-host.mjs +50 -0
  41. package/dist/agent-host.mjs.map +1 -0
  42. package/dist/bundle-manifest.json +102 -0
  43. package/dist/chunks/__host-DgGPyco9.mjs +18 -0
  44. package/dist/chunks/__host-DgGPyco9.mjs.map +1 -0
  45. package/dist/chunks/approve-Cv135UqI.mjs +19 -0
  46. package/dist/chunks/approve-Cv135UqI.mjs.map +1 -0
  47. package/dist/chunks/auth-BT5oVp76.mjs +132 -0
  48. package/dist/chunks/auth-BT5oVp76.mjs.map +1 -0
  49. package/dist/chunks/auth-D8rCVY33.mjs +84 -0
  50. package/dist/chunks/auth-D8rCVY33.mjs.map +1 -0
  51. package/dist/chunks/brainstorm-CU10T1tm.mjs +7 -0
  52. package/dist/chunks/brainstorm-CU10T1tm.mjs.map +1 -0
  53. package/dist/chunks/cancel-KV_cpsqS.mjs +15 -0
  54. package/dist/chunks/cancel-KV_cpsqS.mjs.map +1 -0
  55. package/dist/chunks/canonical-C8UxeEmB.mjs +57 -0
  56. package/dist/chunks/canonical-C8UxeEmB.mjs.map +1 -0
  57. package/dist/chunks/catalogue-BvwSrM_j.mjs +1549 -0
  58. package/dist/chunks/catalogue-BvwSrM_j.mjs.map +1 -0
  59. package/dist/chunks/compose-Cr0hP0qm.mjs +7303 -0
  60. package/dist/chunks/compose-Cr0hP0qm.mjs.map +1 -0
  61. package/dist/chunks/config-DefHA2Lw.mjs +96 -0
  62. package/dist/chunks/config-DefHA2Lw.mjs.map +1 -0
  63. package/dist/chunks/contract-B0ra2-bm.mjs +295 -0
  64. package/dist/chunks/contract-B0ra2-bm.mjs.map +1 -0
  65. package/dist/chunks/contract-vYbwYr0Y.mjs +123 -0
  66. package/dist/chunks/contract-vYbwYr0Y.mjs.map +1 -0
  67. package/dist/chunks/deny-ClTxSvHJ.mjs +15 -0
  68. package/dist/chunks/deny-ClTxSvHJ.mjs.map +1 -0
  69. package/dist/chunks/diff-Qw0II0sj.mjs +101 -0
  70. package/dist/chunks/diff-Qw0II0sj.mjs.map +1 -0
  71. package/dist/chunks/discover-30Ciya_0.mjs +20 -0
  72. package/dist/chunks/discover-30Ciya_0.mjs.map +1 -0
  73. package/dist/chunks/doctor-DfCrKpfT.mjs +222 -0
  74. package/dist/chunks/doctor-DfCrKpfT.mjs.map +1 -0
  75. package/dist/chunks/fix-D4Tj8RVA.mjs +18 -0
  76. package/dist/chunks/fix-D4Tj8RVA.mjs.map +1 -0
  77. package/dist/chunks/gc-BBXlBF5w.mjs +161 -0
  78. package/dist/chunks/gc-BBXlBF5w.mjs.map +1 -0
  79. package/dist/chunks/init-Okz4mlhW.mjs +30 -0
  80. package/dist/chunks/init-Okz4mlhW.mjs.map +1 -0
  81. package/dist/chunks/inspect-Buby4cQD.mjs +22 -0
  82. package/dist/chunks/inspect-Buby4cQD.mjs.map +1 -0
  83. package/dist/chunks/logs-Giq5sUau.mjs +25 -0
  84. package/dist/chunks/logs-Giq5sUau.mjs.map +1 -0
  85. package/dist/chunks/migrate-DRoFUg53.mjs +19 -0
  86. package/dist/chunks/migrate-DRoFUg53.mjs.map +1 -0
  87. package/dist/chunks/models-Btp6X2YZ.mjs +24 -0
  88. package/dist/chunks/models-Btp6X2YZ.mjs.map +1 -0
  89. package/dist/chunks/pause-BASBfKLO.mjs +16 -0
  90. package/dist/chunks/pause-BASBfKLO.mjs.map +1 -0
  91. package/dist/chunks/policy-DUIgzF3W.mjs +139 -0
  92. package/dist/chunks/policy-DUIgzF3W.mjs.map +1 -0
  93. package/dist/chunks/providers-Dx78imKj.mjs +19 -0
  94. package/dist/chunks/providers-Dx78imKj.mjs.map +1 -0
  95. package/dist/chunks/reconcile-BfD785gd.mjs +26 -0
  96. package/dist/chunks/reconcile-BfD785gd.mjs.map +1 -0
  97. package/dist/chunks/render-DyZr5Lgi.mjs +53 -0
  98. package/dist/chunks/render-DyZr5Lgi.mjs.map +1 -0
  99. package/dist/chunks/resume-GJQRb2Eb.mjs +15 -0
  100. package/dist/chunks/resume-GJQRb2Eb.mjs.map +1 -0
  101. package/dist/chunks/retry-CLrUem1H.mjs +15 -0
  102. package/dist/chunks/retry-CLrUem1H.mjs.map +1 -0
  103. package/dist/chunks/review-BDdjb6kB.mjs +17 -0
  104. package/dist/chunks/review-BDdjb6kB.mjs.map +1 -0
  105. package/dist/chunks/run-CgTWYlcd.mjs +82 -0
  106. package/dist/chunks/run-CgTWYlcd.mjs.map +1 -0
  107. package/dist/chunks/run-tool-C3rAv4XF.mjs +26 -0
  108. package/dist/chunks/run-tool-C3rAv4XF.mjs.map +1 -0
  109. package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
  110. package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
  111. package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
  112. package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
  113. package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
  114. package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
  115. package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
  116. package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
  117. package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
  118. package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
  119. package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
  120. package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
  121. package/dist/chunks/src-CId6kMsY.mjs +1689 -0
  122. package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
  123. package/dist/chunks/src-CfAOde7C.mjs +5403 -0
  124. package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
  125. package/dist/chunks/src-Cu3TxNob.mjs +783 -0
  126. package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
  127. package/dist/chunks/src-DcU4IXMe.mjs +117 -0
  128. package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
  129. package/dist/chunks/src-JxawBQBn.mjs +446 -0
  130. package/dist/chunks/src-JxawBQBn.mjs.map +1 -0
  131. package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
  132. package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
  133. package/dist/chunks/status-SMjiOMYy.mjs +29 -0
  134. package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
  135. package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
  136. package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
  137. package/dist/chunks/update-BXIppsbi.mjs +16 -0
  138. package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
  139. package/dist/cli.mjs +175 -0
  140. package/dist/cli.mjs.map +1 -0
  141. package/package.json +17 -32
  142. package/CHANGELOG.md +0 -1461
  143. package/bin/cli.js +0 -817
  144. package/bin/report.js +0 -141
  145. package/core/adapter/render.js +0 -415
  146. package/core/agents/implementer.template.md +0 -142
  147. package/core/agents/profile-reader.md +0 -50
  148. package/core/agents/release.md +0 -58
  149. package/core/agents/review.md +0 -179
  150. package/core/commands/cohorte-align-ds.md +0 -33
  151. package/core/commands/cohorte-audit.md +0 -58
  152. package/core/commands/cohorte-brainstorm.md +0 -70
  153. package/core/commands/cohorte-build.md +0 -219
  154. package/core/commands/cohorte-doctor.md +0 -194
  155. package/core/commands/cohorte-fix.md +0 -80
  156. package/core/commands/cohorte-fleet.md +0 -103
  157. package/core/commands/cohorte-init-pipeline.md +0 -31
  158. package/core/commands/cohorte-intake.md +0 -92
  159. package/core/commands/cohorte-patch.md +0 -118
  160. package/core/commands/cohorte-refactor.md +0 -59
  161. package/core/commands/cohorte-retro.md +0 -85
  162. package/core/commands/cohorte-review.md +0 -270
  163. package/core/commands/cohorte-ship.md +0 -122
  164. package/core/commands/cohorte-spec.md +0 -101
  165. package/core/commands/cohorte-update-pipeline.md +0 -165
  166. package/core/hooks/gate.py +0 -588
  167. package/core/runtimes/claude.json +0 -74
  168. package/core/runtimes/codex.json +0 -85
  169. package/core/runtimes/cursor.json +0 -76
  170. package/core/runtimes/gemini.json +0 -76
  171. package/core/runtimes/opencode.json +0 -73
  172. package/core/templates/agent-handoff.md +0 -34
  173. package/core/templates/brainstorm-return.md +0 -36
  174. package/core/templates/decisions.template.md +0 -42
  175. package/core/templates/design-brief.md +0 -44
  176. package/core/templates/patch.template.md +0 -86
  177. package/core/templates/pr-body.md +0 -25
  178. package/core/templates/review-feedback.md +0 -38
  179. package/core/templates/spec.template.md +0 -87
  180. package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
  181. package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
  182. package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
  183. package/core/templates/steps/init-pipeline/04-write-render.md +0 -145
  184. package/core/templates/steps/init-pipeline/05-report.md +0 -12
  185. package/core/workflows/audit.js +0 -240
  186. package/core/workflows/loop.js +0 -641
  187. package/core/workflows/refactor.js +0 -276
  188. package/core/workflows/review.js +0 -456
  189. package/install.ps1 +0 -103
  190. package/install.sh +0 -114
  191. package/lib/doctor.js +0 -534
  192. package/lib/runtime.js +0 -139
  193. package/lib/versions.js +0 -149
  194. package/lib/yaml.js +0 -129
  195. package/profile/PIPELINE.template.md +0 -259
  196. package/profile/SCHEMA.md +0 -735
  197. package/profile/cohorte.config.template.yaml +0 -43
  198. package/scripts/assert-gate-hook.mjs +0 -75
  199. package/scripts/kanban-move.sh +0 -313
  200. package/scripts/metrics/collect.mjs +0 -531
  201. package/scripts/metrics/prices.json +0 -42
  202. package/scripts/new-feature.sh.template +0 -98
  203. package/scripts/preflight.sh +0 -124
  204. package/scripts/remove-feature.sh.template +0 -56
  205. package/scripts/test-adapter.mjs +0 -437
  206. package/scripts/test-gate.mjs +0 -443
  207. package/scripts/test-kanban.mjs +0 -304
  208. package/scripts/test-lib.mjs +0 -356
  209. package/scripts/test-metrics.mjs +0 -156
  210. package/scripts/test-workflows.mjs +0 -758
  211. package/scripts/validate-core.mjs +0 -325
package/bin/report.js DELETED
@@ -1,141 +0,0 @@
1
- 'use strict';
2
- // Machine-readable reports for `cohorte specs` and `cohorte doctor`.
3
- //
4
- // Both commands read what lib/doctor.js computes (`scanSpecs` and `state`) and
5
- // render it four ways:
6
- //
7
- // default a human table on a terminal
8
- // --porcelain one record per line, fields separated by U+001F (the ASCII unit
9
- // separator) — stable, greppable, and immune to a spec title that
10
- // contains a space, a tab or a pipe
11
- // --json the native document (the full doctor state, the spec list)
12
- // --panel the payload shape a Francois extension panel validates against
13
- // (`{rows:[{key,value,tone}]}` / `{rows:[{id,cells,tone}]}`). This is
14
- // the ONLY Francois-aware surface in cohorte; everything else here is
15
- // generic. See github.com/TheBidouilleAgency/francois-plugin-cohorte.
16
- //
17
- // Dependency-free, and it never writes anything.
18
-
19
- const US = String.fromCharCode(0x1f); // ASCII unit separator (U+001F)
20
-
21
- // A spec's status → a display tone. The tones are the ones a status column can carry
22
- // anywhere (terminal colour, panel row tone); `busy` marks the two statuses that mean
23
- // "a command is mid-flight on this spec".
24
- const STATUS_TONE = {
25
- draft: 'neutral',
26
- frozen: 'neutral',
27
- 'in-progress': 'busy',
28
- 'in-review': 'busy',
29
- shipped: 'ok',
30
- blocked: 'error',
31
- };
32
-
33
- // doctor's four check states → the same tone vocabulary. `skip` is not a failure:
34
- // a check that does not apply to this project (no design system, no worktrees) reads
35
- // neutral, never warn.
36
- const CHECK_TONE = { ok: 'ok', warn: 'warn', bad: 'error', skip: 'neutral' };
37
-
38
- // Collapse anything that would break the line format or a single-line cell. A spec
39
- // title is free text written by a human in YAML frontmatter — it can contain a newline
40
- // continuation, and it must never split one record into two. A stray separator inside a
41
- // field goes the same way, and so does the padding a doctor `fix` carries for terminal
42
- // alignment — in a one-line panel cell that padding reads as a hole.
43
- function flat(value) {
44
- return String(value == null ? '' : value)
45
- .replace(new RegExp(`[\\s${US}]+`, 'g'), ' ')
46
- .trim();
47
- }
48
-
49
- function specRecords(projectRoot, scanSpecs) {
50
- return scanSpecs(projectRoot).map((s) => ({
51
- id: s.id,
52
- title: s.title || '',
53
- status: s.status || 'unknown',
54
- branch: s.branch || '',
55
- file: s.file,
56
- tone: STATUS_TONE[s.status] || 'warn', // an unknown status IS the warning
57
- }));
58
- }
59
-
60
- function specsPorcelain(records) {
61
- return records
62
- .map((r) => [r.id, r.title, r.status, r.branch, r.tone].map(flat).join(US))
63
- .join('\n');
64
- }
65
-
66
- function specsPanel(records) {
67
- return JSON.stringify({
68
- rows: records.map((r) => ({
69
- id: flat(r.id),
70
- tone: r.tone,
71
- cells: { id: flat(r.id), title: flat(r.title), status: flat(r.status), branch: flat(r.branch) },
72
- })),
73
- });
74
- }
75
-
76
- function specsHuman(records) {
77
- if (!records.length) return 'no specs in ./specs — /cohorte-spec writes the first one';
78
- const w = (k) => Math.max(...records.map((r) => flat(r[k]).length), k.length);
79
- const [wi, ws, wb] = [w('id'), w('status'), w('branch')];
80
- const head = `${'id'.padEnd(wi)} ${'status'.padEnd(ws)} ${'branch'.padEnd(wb)} title`;
81
- const rows = records.map(
82
- (r) => `${flat(r.id).padEnd(wi)} ${flat(r.status).padEnd(ws)} ${flat(r.branch).padEnd(wb)} ${flat(r.title)}`,
83
- );
84
- return [head, '-'.repeat(head.length), ...rows].join('\n');
85
- }
86
-
87
- function checkRecords(state) {
88
- return state.checks.map((c) => ({
89
- id: c.id,
90
- label: c.label,
91
- status: c.status,
92
- detail: c.detail,
93
- fix: c.fix || '',
94
- tone: CHECK_TONE[c.status] || 'neutral',
95
- }));
96
- }
97
-
98
- function doctorPorcelain(records) {
99
- return records
100
- .map((r) => [r.id, r.label, r.status, r.detail, r.fix].map(flat).join(US))
101
- .join('\n');
102
- }
103
-
104
- // key-value, one row per check: the check's label keyed against what it found. The fix
105
- // rides in the value for a failing check — a panel row has nowhere else to put it, and
106
- // a health report that says "broken" without saying "run this" is half a report.
107
- function doctorPanel(records) {
108
- return JSON.stringify({
109
- rows: records.map((r) => ({
110
- key: flat(r.label),
111
- value: flat(r.fix && r.tone !== 'ok' ? `${r.detail} → ${r.fix}` : r.detail),
112
- tone: r.tone,
113
- })),
114
- });
115
- }
116
-
117
- function doctorHuman(state, records) {
118
- const mark = { ok: 'ok ', warn: 'warn', bad: 'BAD ', skip: 'skip' };
119
- const lines = [`cohorte doctor — ${state.project}`, ''];
120
- for (const r of records) {
121
- lines.push(`${mark[r.status] || '? '} ${r.label} — ${flat(r.detail)}`);
122
- if (r.fix && r.status !== 'ok') lines.push(` fix: ${flat(r.fix)}`);
123
- }
124
- const s = state.summary;
125
- lines.push('', `${s.ok} ok · ${s.warn} warn · ${s.bad} bad · ${s.skip} skipped`);
126
- return lines.join('\n');
127
- }
128
-
129
- module.exports = {
130
- US,
131
- STATUS_TONE,
132
- CHECK_TONE,
133
- specRecords,
134
- specsPorcelain,
135
- specsPanel,
136
- specsHuman,
137
- checkRecords,
138
- doctorPorcelain,
139
- doctorPanel,
140
- doctorHuman,
141
- };
@@ -1,415 +0,0 @@
1
- #!/usr/bin/env node
2
- // cohorte — runtime adapter.
3
- //
4
- // The core prompts (core/commands/*.md, core/agents/*.md) are the SINGLE source of
5
- // truth and are written runtime-neutral. This module transpiles them into whatever
6
- // the target coding agent actually reads:
7
- //
8
- // commands agents gate hook
9
- // claude .claude/commands/<n>.md md, $ARGUMENTS md PreToolUse (deny+ask)
10
- // codex .agents/skills/<n>/SKILL.md md, no substitution toml PreToolUse (deny only)
11
- // cursor .cursor/commands/<n>.md md, no substitution md beforeShellExecution
12
- // gemini .gemini/commands/<n>.toml toml, {{args}} md BeforeTool (deny only)
13
- // opencode .opencode/commands/<n>.md md, $ARGUMENTS md none — advisory --check
14
- //
15
- // Three transforms, in order:
16
- //
17
- // 1. capability conditionals — `<!-- cohorte:if hooks -->…<!-- cohorte:else -->…
18
- // <!-- cohorte:endif -->` keeps exactly one branch, so one file can state both "the gate
19
- // fires whether or not you cooperate" and "you must call the gate yourself".
20
- // 2. runtime preamble — one block resolving `<core>`, `<agents>`, the dispatch verb and
21
- // the gate mechanism, so the prose below never hardcodes a runtime's layout.
22
- // 3. surface encoding — frontmatter filtered to the keys that runtime understands (an
23
- // unknown key is prose the model reads as an instruction, so dropping is not cosmetic),
24
- // arg placeholder swapped, and the whole thing re-emitted as md, toml or a skill dir.
25
- //
26
- // Subagents are NOT one of the conditionals: they are a precondition (see assertSupported).
27
- //
28
- // Claude output is byte-identical to the pre-adapter core when a file has no conditionals:
29
- // that is the regression test (scripts/test-adapter.mjs).
30
-
31
- 'use strict';
32
-
33
- const fs = require('fs');
34
- const os = require('os');
35
- const path = require('path');
36
-
37
- const RUNTIME_DIR = path.join(__dirname, '..', 'runtimes');
38
-
39
- // --- registry ----------------------------------------------------------------
40
-
41
- function listRuntimes() {
42
- return fs.readdirSync(RUNTIME_DIR)
43
- .filter((f) => f.endsWith('.json'))
44
- .map((f) => f.slice(0, -5))
45
- .sort();
46
- }
47
-
48
- function loadRuntime(id) {
49
- const file = path.join(RUNTIME_DIR, `${id}.json`);
50
- if (!fs.existsSync(file)) {
51
- throw new Error(`unknown runtime "${id}" (known: ${listRuntimes().join(', ')})`);
52
- }
53
- const rt = JSON.parse(fs.readFileSync(file, 'utf8'));
54
- assertSupported(rt);
55
- return rt;
56
- }
57
-
58
- // Subagents are a HARD requirement, not a capability to branch on. The pipeline's premise is
59
- // that each surface is built by someone who can only see the frozen contract; without real
60
- // subagents the lead does every surface in one context and that isolation is simply gone.
61
- // A sequential-persona fallback existed briefly and was removed: it asked the lead to simulate
62
- // the boundary by discipline, which is not the same guarantee, and no supported runtime ever
63
- // took that branch. Refuse loudly rather than render a pipeline whose central promise is absent.
64
- function assertSupported(rt) {
65
- if (rt.capabilities && rt.capabilities.subagents === false) {
66
- throw new Error(`runtime "${rt.id}" declares no subagents — cohorte requires them `
67
- + '(see docs/reference/runtimes.md §Requirements)');
68
- }
69
- return rt;
70
- }
71
-
72
- function expandHome(p) {
73
- if (!p) return p;
74
- return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;
75
- }
76
-
77
- // Which runtimes are actually installed on this machine. Detection is by config
78
- // directory, not by binary on PATH: Cursor ships no CLI, and a runtime the human
79
- // has configured but not opened this shell for is still a legitimate target.
80
- function detectRuntimes() {
81
- return listRuntimes().filter((id) => {
82
- const rt = loadRuntime(id);
83
- return (rt.detect || []).some((d) => fs.existsSync(expandHome(d)));
84
- });
85
- }
86
-
87
- // Absolute destinations for one (runtime, scope) pair. `projectRoot` anchors every
88
- // relative path; `~`-rooted ones ignore it. A scope entry may pin an individual
89
- // surface to another scope (Codex reads prompts ONLY from ~/.codex, even when the
90
- // neutral core is bundled in the repo) via `<key>_scope`.
91
- function resolvePaths(runtime, scope, projectRoot, { overrides } = {}) {
92
- const spec = runtime.scopes[scope];
93
- if (!spec) throw new Error(`runtime ${runtime.id} has no "${scope}" scope`);
94
- // `overrides` re-roots a declared prefix — the one caller is CLAUDE_CONFIG_DIR, which moves
95
- // Claude Code's whole `~/.claude` tree elsewhere (a desktop host points it at
96
- // `~/Library/Application Support/…`). Ignoring it split the install in half: the core was
97
- // written to `~/.claude` while the hook was registered in the overridden dir, so a scratch
98
- // install silently wrote into the user's real global core.
99
- const reroot = (p) => {
100
- for (const [from, to] of Object.entries(overrides || {})) {
101
- if (p === from) return to;
102
- if (p.startsWith(from + '/')) return to + p.slice(from.length);
103
- }
104
- return p;
105
- };
106
- const abs = (p) => {
107
- if (!p) return null;
108
- const e = expandHome(reroot(p));
109
- return path.isAbsolute(e) ? e : path.join(projectRoot, e);
110
- };
111
- const out = { scope, effective: {} };
112
- for (const key of ['root', 'core', 'commands', 'agents', 'hooks', 'hooks_config',
113
- 'workflows', 'settings']) {
114
- out[key] = abs(spec[key]);
115
- out.effective[key] = spec[`${key}_scope`] || scope;
116
- }
117
- return out;
118
- }
119
-
120
- // --- 1. capability conditionals ----------------------------------------------
121
-
122
- const IF = /^\s*<!--\s*cohorte:if\s+([^>]+?)\s*-->\s*$/;
123
- const ELSE = /^\s*<!--\s*cohorte:else\s*-->\s*$/;
124
- const ENDIF = /^\s*<!--\s*cohorte:endif\s*-->\s*$/;
125
-
126
- // A condition is a space-separated OR of terms; a term is `cap`, `!cap`, `runtime:<id>`
127
- // or `!runtime:<id>`. OR (not AND) because every real use is "this family of runtimes".
128
- function testCondition(expr, runtime) {
129
- return expr.split(/\s+/).filter(Boolean).some((term) => {
130
- const neg = term.startsWith('!');
131
- const name = neg ? term.slice(1) : term;
132
- let value;
133
- if (name.startsWith('runtime:')) value = runtime.id === name.slice(8);
134
- else if (name in (runtime.capabilities || {})) value = !!runtime.capabilities[name];
135
- else throw new Error(`unknown cohorte:if term "${name}"`);
136
- return neg ? !value : value;
137
- });
138
- }
139
-
140
- function applyConditionals(text, runtime) {
141
- const lines = text.split('\n');
142
- const out = [];
143
- // Each frame: {keep} — whether the branch currently being read survives. Nesting is
144
- // supported so a hooks-branch can carry a subagents-branch inside it.
145
- const stack = [];
146
- const emitting = () => stack.every((f) => f.keep);
147
- for (const line of lines) {
148
- let m;
149
- if ((m = line.match(IF))) {
150
- const taken = testCondition(m[1], runtime);
151
- stack.push({ taken, keep: taken, seenElse: false });
152
- continue;
153
- }
154
- if (ELSE.test(line)) {
155
- const frame = stack[stack.length - 1];
156
- if (!frame) throw new Error('cohorte:else without a matching cohorte:if');
157
- if (frame.seenElse) throw new Error('two cohorte:else in one cohorte:if');
158
- frame.seenElse = true;
159
- frame.keep = !frame.taken;
160
- continue;
161
- }
162
- if (ENDIF.test(line)) {
163
- if (!stack.pop()) throw new Error('cohorte:endif without a matching cohorte:if');
164
- continue;
165
- }
166
- if (emitting()) out.push(line);
167
- }
168
- if (stack.length) throw new Error('unclosed cohorte:if');
169
- return out.join('\n');
170
- }
171
-
172
- // --- 2. runtime preamble ------------------------------------------------------
173
-
174
- // Path as the human/model should see it: `~/…` reads better than an expanded homedir,
175
- // and a project-relative path must stay relative (the agent's cwd is the repo).
176
- function displayPath(p, projectRoot) {
177
- if (!p) return null;
178
- const home = os.homedir();
179
- if (p.startsWith(home + path.sep)) return '~/' + path.relative(home, p).split(path.sep).join('/');
180
- if (p.startsWith(projectRoot + path.sep)) return path.relative(projectRoot, p).split(path.sep).join('/');
181
- return p;
182
- }
183
-
184
- // The per-project directory holding everything the pipeline GENERATES for this repo
185
- // (gate-config.json, preflight.ok, pipeline-metrics.jsonl, pipeline.json). Distinct from
186
- // `<core>`: the core can be global while this is always in the repo, next to the code it
187
- // describes. `.claude` on a Claude install — unchanged, so existing repos keep working.
188
- //
189
- // Deliberately SHARED across non-Claude runtimes (`.cohorte`), while each of their cores is
190
- // its own subdirectory (`.cohorte/<id>`). The state describes the PROJECT — one gate config,
191
- // one preflight stamp, one metrics log — and duplicating it per runtime would let a repo
192
- // driven from two agents disagree with itself about what is gated and what is verified.
193
- // The core, by contrast, is rendered per runtime and cannot be shared: the same template
194
- // resolves differently depending on whether that agent has subagents or hooks.
195
- function stateDir(runtime) {
196
- return runtime.id === 'claude' ? '.claude' : '.cohorte';
197
- }
198
-
199
- // The user-level config (kanban boards, shared vault). One file per
200
- // human, not per project or per runtime; the shipped scripts probe the same two paths.
201
- function configPath(runtime) {
202
- return runtime.id === 'claude' ? '~/.claude/cohorte.config.yaml' : '~/.cohorte/cohorte.config.yaml';
203
- }
204
-
205
- function preamble(runtime, paths, projectRoot, { kind = 'command' } = {}) {
206
- const caps = runtime.capabilities || {};
207
- const core = displayPath(paths.core, projectRoot);
208
- const fixedAgentsDir = paths.agents ? displayPath(paths.agents, projectRoot) : `${core}/agents`;
209
- const agentsDir = runtime.id === 'codex' ? '.codex/agents' : fixedAgentsDir;
210
- const L = [];
211
- L.push(`> **Runtime: ${runtime.label}.** Generated by the cohorte adapter — do not edit this file;`);
212
- L.push(`> edit \`core/${kind === 'agent' ? 'agents' : 'commands'}/\` in the cohorte source and re-install.`);
213
- L.push('>');
214
- L.push(`> - \`<core>\` = \`${core}\` — the pipeline's shared assets (\`pipeline/scripts/\`, \`pipeline/SCHEMA.md\`, \`templates/\`). Every \`<core>/…\` path below resolves there, and nowhere else.`);
215
- L.push(`> - \`<state>\` = \`${stateDir(runtime)}/\` in **this repo** — what the pipeline generates for this project (\`gate-config.json\`, \`preflight.ok\`, \`pipeline-metrics.jsonl\`, \`pipeline.json\`). Always project-relative, even when \`<core>\` is global.`);
216
- L.push(`> - \`<memory>\` = \`${runtime.memory}\` — this runtime's project-instructions file at the repo root, loaded into every session here. Where the doctrine says to reference or extend it, that is the file.`);
217
- L.push(`> - \`<config>\` = \`${configPath(runtime)}\` — your user-level config (kanban boards, shared vault). One per human, never committed.`);
218
-
219
- L.push(`> - \`<agents>\` = \`${agentsDir}\` — real subagents. Dispatch: ${runtime.agent.dispatch}.`);
220
- L.push(`> - \`<fixed-agents>\` = \`${fixedAgentsDir}\` — shipped review, release and profile-reader agents. Agent extension: \`${runtime.agent.ext}\`.`);
221
- if (runtime.id === 'codex') {
222
- L.push('> - Surface agents are always project-local `.codex/agents/*.toml`, even with a global core. Keep the normal user `CODEX_HOME`; no project launcher or authentication symlink is needed. Never overwrite another project\'s global agents.');
223
- L.push('> - Agent files use TOML `name`, `description`, `developer_instructions`; optional `model` and `model_reasoning_effort` must be Codex-compatible. Omitted model settings inherit; never write Anthropic aliases. MCP registration lives in `.codex/config.toml`, not a standalone `.mcp.json`.');
224
- }
225
-
226
- if (caps.hooks) {
227
- const cfg = displayPath(paths.hooks_config, projectRoot);
228
- const ask = runtime.hook.supports_ask
229
- ? 'It can deny outright or ask you to confirm.'
230
- : 'This runtime has **no confirmation tier**, so a command that would merely be queried elsewhere is **denied** here — re-run it yourself if you meant it.';
231
- L.push(`> - **Gate** — \`<core>/hooks/gate.py\` is registered as a blocking \`${runtime.hook.event}\` hook in \`${cfg}\`, reading \`<state>/gate-config.json\`. It fires whether or not you cooperate. ${ask}`);
232
- }
233
-
234
- if (!caps.hooks) {
235
- L.push('> - **Gate** — this runtime has no blocking hook, so the gate is not automatic. Before any command listed in the profile\'s `gate` block, and before every phase dispatch, run `<core>/hooks/gate.py --check <command>` yourself and obey its verdict (`deny` ⇒ stop, `ask` ⇒ get the human\'s explicit go-ahead). Skipping this is the one deviation that silently removes a safety property.');
236
- }
237
-
238
- if (!caps.workflows) {
239
- L.push('> - **Workflows** — unavailable on this runtime. The conversational path below is the only path; ignore any mention of a workflow variant.');
240
- }
241
-
242
- if (!runtime.command.args) {
243
- L.push(`> - \`$ARGUMENTS\` — this runtime does not substitute placeholders. It means ${runtime.command.args_note || 'the text typed after the command name'}; expand it yourself everywhere it appears below.`);
244
- }
245
-
246
- return L.join('\n') + '\n';
247
- }
248
-
249
- // Apply to commands AND the instruction templates they consume. Filenames here refer to
250
- // rendered agents, not to the Markdown source templates shipped inside the core.
251
- function adaptInstructions(text, runtime) {
252
- const resolved = applyConditionals(text, runtime);
253
- if (runtime.id !== 'codex') return resolved;
254
- return resolved.replace(/(<(?:agents|fixed-agents)>\/[^\s`]+)\.md\b/g, '$1.toml');
255
- }
256
-
257
- // --- 3. surface encoding ------------------------------------------------------
258
-
259
- function parseFrontmatter(text) {
260
- if (!text.startsWith('---\n')) return { keys: [], body: text };
261
- const end = text.indexOf('\n---\n', 3);
262
- if (end === -1) return { keys: [], body: text };
263
- const block = text.slice(4, end + 1);
264
- const body = text.slice(end + 5);
265
- const keys = [];
266
- for (const line of block.split('\n')) {
267
- const m = line.match(/^([A-Za-z][\w-]*):\s?(.*)$/);
268
- // Continuation lines (a wrapped value) belong to the previous key.
269
- if (!m) {
270
- if (line.trim() && keys.length) keys[keys.length - 1][1] += '\n' + line;
271
- continue;
272
- }
273
- keys.push([m[1], m[2]]);
274
- }
275
- return { keys, body };
276
- }
277
-
278
- function emitFrontmatter(keys) {
279
- if (!keys.length) return '';
280
- return '---\n' + keys.map(([k, v]) => `${k}: ${v}`).join('\n') + '\n---\n\n';
281
- }
282
-
283
- function substituteArgs(text, runtime) {
284
- const token = runtime.command.args;
285
- if (!token || token === '$ARGUMENTS') return text;
286
- return text.split('$ARGUMENTS').join(token);
287
- }
288
-
289
- // Prefer readable multi-line literal TOML; fall back to basic strings for delimiter collisions.
290
- function tomlMultiline(s) {
291
- // Literal TOML strings cannot escape their delimiter. Use a basic string when the
292
- // prompt itself contains triple apostrophes, preserving the content exactly.
293
- return s.includes("'''") ? JSON.stringify(s) : "'''\n" + s + "\n'''";
294
- }
295
- function tomlBasic(s) {
296
- return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ') + '"';
297
- }
298
-
299
- /**
300
- * Render one core command for one runtime.
301
- * @returns {{filename: string, content: string, dir: 'commands'}}
302
- */
303
- function renderCommand({ source, name, runtime, paths, projectRoot }) {
304
- const { keys, body } = parseFrontmatter(source);
305
- let out = adaptInstructions(body, runtime);
306
- out = preamble(runtime, paths, projectRoot) + '\n' + out.replace(/^\n+/, '');
307
- out = substituteArgs(out, runtime);
308
-
309
- const kept = keys.filter(([k]) => runtime.command.frontmatter.includes(k));
310
- const dropped = keys.filter(([k]) => !runtime.command.frontmatter.includes(k)).map(([k]) => k);
311
-
312
- // A skill is a DIRECTORY — `<name>/SKILL.md` — whose frontmatter must carry `name`, since
313
- // that is what the runtime matches on (both for `$<name>` and for implicit selection from
314
- // `description`). Command sources have no `name` key, so synthesise it from the filename.
315
- if (runtime.command.format === 'skill') {
316
- if (!kept.some(([k]) => k === 'name')) kept.unshift(['name', name]);
317
- return { filename: `${name}${runtime.command.ext}`, content: emitFrontmatter(kept) + out, dropped };
318
- }
319
-
320
- if (runtime.command.format === 'toml') {
321
- const desc = (keys.find(([k]) => k === 'description') || [null, ''])[1];
322
- const lines = [`# cohorte — generated for ${runtime.label}. Do not edit; edit core/commands/${name}.md.`];
323
- if (dropped.length) lines.push(`# frontmatter not supported here, dropped: ${dropped.join(', ')}`);
324
- if (desc) lines.push(`description = ${tomlBasic(desc)}`);
325
- lines.push(`prompt = ${tomlMultiline(substituteArgs(out, runtime))}`);
326
- return { filename: `${name}${runtime.command.ext}`, content: lines.join('\n') + '\n', dropped };
327
- }
328
-
329
- const header = emitFrontmatter(kept);
330
- return { filename: `${name}${runtime.command.ext}`, content: header + out, dropped };
331
- }
332
-
333
- // Is this agent read-only? Derived from the source's Claude-style `tools:` list rather than
334
- // declared twice: an agent that was never given Write, Edit or Bash is read-only by intent,
335
- // and each runtime spells that its own way (`readonly: true`, `sandbox_mode = "read-only"`).
336
- // Losing it silently would hand the reviewer the ability to fix what it is meant to report.
337
- function isReadOnly(keys) {
338
- const tools = (keys.find(([k]) => k === 'tools') || [])[1];
339
- if (!tools) return false;
340
- // The implementer template is rendered before its surface tool list is known.
341
- // A placeholder is not evidence that the eventual worker must be read-only.
342
- if (tools.includes('<SURFACE_TOOLS>')) return false;
343
- return !/\b(Write|Edit|MultiEdit|Bash|NotebookEdit)\b/.test(tools);
344
- }
345
-
346
- /**
347
- * Render one core agent for one runtime.
348
- *
349
- * Every runtime cohorte targets has real subagents, but they disagree on the file: markdown +
350
- * frontmatter for Claude Code, Cursor, Gemini CLI and OpenCode; TOML with the body under
351
- * `developer_instructions` for Codex. `format: "persona"` remains for a runtime with no
352
- * subagents at all — the file is then read and adopted by the lead in sequence.
353
- */
354
- function renderAgent({ source, name, runtime, paths, projectRoot }) {
355
- const { keys, body } = parseFrontmatter(source);
356
- const out = preamble(runtime, paths, projectRoot, { kind: 'agent' }) + '\n'
357
- + adaptInstructions(body, runtime).replace(/^\n+/, '');
358
- const spec = runtime.agent;
359
- const readonly = isReadOnly(keys);
360
-
361
- if (spec.format === 'toml') {
362
- const get = (k) => (keys.find(([kk]) => kk === k) || [null, ''])[1];
363
- const lines = [`# cohorte — generated for ${runtime.label}. Do not edit; edit core/agents/${name}.md.`];
364
- lines.push(`name = ${tomlBasic(get('name') || name)}`);
365
- if (get('description')) lines.push(`description = ${tomlBasic(get('description'))}`);
366
- const model = get('model');
367
- if (spec.frontmatter.includes('model') && model
368
- && !['inherit', 'sonnet', 'haiku', 'opus'].includes(model) && !model.startsWith('<')) {
369
- lines.push(`model = ${tomlBasic(model)}`);
370
- }
371
- if (spec.frontmatter.includes('model_reasoning_effort') && get('model_reasoning_effort')) {
372
- lines.push(`model_reasoning_effort = ${tomlBasic(get('model_reasoning_effort'))}`);
373
- }
374
- if (readonly && spec.readonly_key) {
375
- lines.push(`${spec.readonly_key} = ${tomlBasic(spec.readonly_value)}`);
376
- }
377
- lines.push(`${spec.body_key} = ${tomlMultiline(out)}`);
378
- return { filename: `${name}${spec.ext}`, content: lines.join('\n') + '\n', native: true, readonly };
379
- }
380
-
381
- if (spec.format !== 'md') {
382
- return { filename: `${name}.md`, content: out, native: false, dropped: keys.map(([k]) => k) };
383
- }
384
-
385
- const allowed = spec.frontmatter;
386
- const kept = keys.filter(([k]) => allowed.includes(k));
387
- for (const [k, v] of Object.entries(spec.defaults || {})) {
388
- if (!kept.some(([kk]) => kk === k)) kept.push([k, v]);
389
- }
390
- if (readonly && spec.readonly_key && !kept.some(([k]) => k === spec.readonly_key)) {
391
- kept.push([spec.readonly_key, spec.readonly_value]);
392
- }
393
- const dropped = keys.filter(([k]) => !allowed.includes(k)).map(([k]) => k);
394
- return { filename: `${name}${spec.ext || '.md'}`, content: emitFrontmatter(kept) + out, native: true, dropped, readonly };
395
- }
396
-
397
- module.exports = {
398
- assertSupported,
399
- stateDir,
400
- configPath,
401
- listRuntimes,
402
- loadRuntime,
403
- detectRuntimes,
404
- resolvePaths,
405
- expandHome,
406
- displayPath,
407
- applyConditionals,
408
- adaptInstructions,
409
- testCondition,
410
- parseFrontmatter,
411
- emitFrontmatter,
412
- preamble,
413
- renderCommand,
414
- renderAgent,
415
- };