cohorte 2.10.0 → 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 -1453
  143. package/bin/cli.js +0 -812
  144. package/bin/report.js +0 -141
  145. package/core/adapter/render.js +0 -389
  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 -170
  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 -134
  166. package/core/hooks/gate.py +0 -580
  167. package/core/runtimes/claude.json +0 -74
  168. package/core/runtimes/codex.json +0 -83
  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 -115
  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 -504
  192. package/lib/runtime.js +0 -134
  193. package/lib/versions.js +0 -149
  194. package/lib/yaml.js +0 -129
  195. package/profile/PIPELINE.template.md +0 -251
  196. package/profile/SCHEMA.md +0 -698
  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 -368
  206. package/scripts/test-gate.mjs +0 -428
  207. package/scripts/test-kanban.mjs +0 -304
  208. package/scripts/test-lib.mjs +0 -310
  209. package/scripts/test-metrics.mjs +0 -156
  210. package/scripts/test-workflows.mjs +0 -758
  211. package/scripts/validate-core.mjs +0 -320
package/lib/runtime.js DELETED
@@ -1,134 +0,0 @@
1
- 'use strict';
2
- // Which coding agent(s) this project is wired for, and where each one keeps its files.
3
- //
4
- // Before 2.2.0 every path here was `.claude/…`. That is now one layout of five:
5
- // a repo driven from Cursor keeps its core in `.cohorte/cursor/`, its agents in
6
- // `.cursor/agents/` and its gate registration in `.cursor/hooks.json`. Checking the Claude
7
- // paths there reports a perfectly healthy install as broken — "no core", "no rendered agent",
8
- // "gate not registered" — which is worse than not checking at all: it sends a human fixing
9
- // something that is not wrong.
10
- //
11
- // The installer writes `<core>/pipeline/runtimes.json` (a map of every runtime installed
12
- // against that core), so discovery is a directory probe rather than a guess.
13
-
14
- const fs = require('fs');
15
- const os = require('os');
16
- const path = require('path');
17
-
18
- function readJson(file) {
19
- try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
20
- }
21
-
22
- // `<state>` — where the pipeline writes what it generates FOR THIS PROJECT (gate config,
23
- // preflight stamp, metrics). Shared by every non-Claude runtime on purpose: it describes the
24
- // project, not the agent driving it.
25
- function stateDirFor(id) {
26
- return id === 'claude' ? '.claude' : '.cohorte';
27
- }
28
-
29
- // Candidate core directories, project scope first — a bundled core wins over a global one,
30
- // same precedence the commands themselves use.
31
- function candidateCores(projectRoot, globalDir) {
32
- const home = os.homedir();
33
- const out = [
34
- { dir: path.join(projectRoot, '.claude'), scope: 'project' },
35
- { dir: globalDir || path.join(home, '.claude'), scope: 'global' },
36
- ];
37
- for (const [root, scope] of [[path.join(projectRoot, '.cohorte'), 'project'],
38
- [path.join(home, '.cohorte'), 'global']]) {
39
- let entries = [];
40
- try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { /* absent */ }
41
- for (const e of entries) {
42
- if (e.isDirectory()) out.push({ dir: path.join(root, e.name), scope });
43
- }
44
- }
45
- return out;
46
- }
47
-
48
- /**
49
- * Every runtime installed against this project, most-authoritative first.
50
- * Each layout resolves the paths a check needs, absolute where they are absolute.
51
- * Returns [] when nothing is installed — callers report that as the missing core it is.
52
- */
53
- function layouts({ projectRoot, globalDir }) {
54
- const found = [];
55
- const seen = new Set();
56
- for (const { dir, scope } of candidateCores(projectRoot, globalDir)) {
57
- const registry = readJson(path.join(dir, 'pipeline', 'runtimes.json'));
58
- // A core with no registry predates the adapter ⇒ it can only be a Claude install.
59
- const ids = registry ? Object.keys(registry) : (
60
- fs.existsSync(path.join(dir, 'pipeline', 'VERSION')) ? ['claude'] : []);
61
- for (const id of ids) {
62
- const key = `${id}@${dir}`;
63
- if (seen.has(key)) continue;
64
- seen.add(key);
65
- const rec = (registry && registry[id]) || {};
66
- const p = rec.paths || {};
67
- // runtimes.json records ABSOLUTE install-time paths. For a bundled (committed)
68
- // core, a clone or a moved checkout still carries the ORIGINAL machine's paths —
69
- // taken verbatim, every doctor check goes red on a healthy install ("no rendered
70
- // agent", "gate not registered"). Recover the install-time project root from the
71
- // recorded core path (its tail must match this core's path relative to the
72
- // current project root — '.claude', '.cohorte/<rt>') and re-root anything under
73
- // it onto the current projectRoot. Global cores sit outside the project and are
74
- // machine-local by definition — their absolute paths pass through untouched.
75
- const rel = path.relative(projectRoot, dir);
76
- let recordedRoot = null;
77
- if (p.core && rel && !rel.startsWith('..') && !path.isAbsolute(rel)
78
- && (p.core === rel || p.core.endsWith(path.sep + rel))) {
79
- recordedRoot = p.core.slice(0, p.core.length - rel.length - 1) || null;
80
- }
81
- const abs = (v, fallback) => {
82
- const raw = v || fallback;
83
- if (!raw) return null;
84
- if (!path.isAbsolute(raw)) return path.join(projectRoot, raw);
85
- if (recordedRoot && recordedRoot !== projectRoot
86
- && (raw === recordedRoot || raw.startsWith(recordedRoot + path.sep))) {
87
- return path.join(projectRoot, raw.slice(recordedRoot.length + 1) || '.');
88
- }
89
- return raw;
90
- };
91
- found.push({
92
- id,
93
- label: rec.label || id,
94
- scope: rec.scope || scope,
95
- core: dir,
96
- agents: abs(p.agents, path.join(dir, 'agents')),
97
- commands: abs(p.commands, path.join(dir, 'commands')),
98
- state: abs(null, p.state || stateDirFor(id)),
99
- // Pre-2.2.0 cores carry no registry, so nothing records where the hook is registered.
100
- // Claude's is `<core>/settings.json` and always has been — without this fallback a
101
- // legacy install reports its perfectly good registration as missing.
102
- hooksConfig: (rec.paths && rec.paths.hooks_config)
103
- ? abs(rec.paths.hooks_config)
104
- : (id === 'claude' ? path.join(dir, 'settings.json') : null),
105
- capabilities: rec.capabilities || {
106
- subagents: true, hooks: id === 'claude', workflows: id === 'claude',
107
- },
108
- legacy: !registry,
109
- });
110
- }
111
- }
112
- return found;
113
- }
114
-
115
- /**
116
- * The layout the project-level checks should use. Claude first when present — it is the
117
- * reference implementation and the layout every pre-2.2.0 repo has — else whatever is
118
- * installed. Null when nothing is.
119
- */
120
- function primary(all) {
121
- if (!all.length) return null;
122
- return all.find((l) => l.id === 'claude') || all[0];
123
- }
124
-
125
- // The state dirs actually in play. A repo driven from Claude AND Cursor has both `.claude`
126
- // and `.cohorte`; a check that looked at only one would miss a stamp or a metrics file.
127
- function stateDirs(all, projectRoot) {
128
- const dirs = [];
129
- for (const l of all) if (!dirs.includes(l.state)) dirs.push(l.state);
130
- if (!dirs.length) dirs.push(path.join(projectRoot, '.claude'));
131
- return dirs;
132
- }
133
-
134
- module.exports = { layouts, primary, stateDirs, stateDirFor };
package/lib/versions.js DELETED
@@ -1,149 +0,0 @@
1
- 'use strict';
2
- // Freshness data: which core is installed where, and how it compares to npm latest.
3
- // Dependency-free — reads VERSION files + the committed pointer, fetches the npm registry.
4
-
5
- const fs = require('fs');
6
- const path = require('path');
7
- const { spawnSync } = require('child_process');
8
- const { layouts } = require('./runtime.js');
9
-
10
- function readTrimmed(file) {
11
- try { return fs.readFileSync(file, 'utf8').trim(); } catch { return null; }
12
- }
13
-
14
- function readJson(file) {
15
- try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
16
- }
17
-
18
- // Installed core in a given root (global ~/.claude or a repo's bundled .claude).
19
- function coreAt(claudeDir) {
20
- const version = readTrimmed(path.join(claudeDir, 'pipeline', 'VERSION'));
21
- return { present: version != null, version, dir: claudeDir };
22
- }
23
-
24
- // The committed per-repo pointer (bundled mode); names the mode + core_version.
25
- function pointerAt(projectRoot) {
26
- const ptr = readJson(path.join(projectRoot, '.claude', 'pipeline.json'));
27
- if (!ptr || typeof ptr !== 'object') return { present: false };
28
- return { present: true, mode: ptr.mode || null, core_version: ptr.core_version || null };
29
- }
30
-
31
- // Latest published version — registry fetch first, `npm view` as a fallback (it uses the
32
- // user's configured registry/proxy, which works where a raw fetch may be blocked). Cached
33
- // briefly so repeated calls don't hammer the network. null only if both fail.
34
- let _cache = { value: null, at: 0 };
35
- const CACHE_MS = 5 * 60 * 1000;
36
- // Failures are cached too, briefly. Without this an offline machine paid the FULL
37
- // 5s fetch timeout + 8s `npm view` timeout on every call — and /api/fleet calls
38
- // this once per tracked project, so a batch of probes never finished.
39
- const FAIL_CACHE_MS = 60 * 1000;
40
-
41
- async function fetchRegistry(timeoutMs = 5000) {
42
- const ctrl = new AbortController();
43
- const t = setTimeout(() => ctrl.abort(), timeoutMs);
44
- try {
45
- const res = await fetch('https://registry.npmjs.org/cohorte/latest', {
46
- signal: ctrl.signal,
47
- headers: { accept: 'application/vnd.npm.install-v1+json' },
48
- });
49
- if (!res.ok) return null;
50
- const body = await res.json();
51
- return typeof body.version === 'string' ? body.version : null;
52
- } catch {
53
- return null;
54
- } finally {
55
- clearTimeout(t);
56
- }
57
- }
58
-
59
- // Fallback only — spawnSync blocks the single-threaded server for up to its
60
- // timeout, so it must stay behind latestNpm()'s cache + in-flight dedupe (at most
61
- // one call a minute, and only when the registry fetch already failed).
62
- function npmView() {
63
- try {
64
- // One static string on Windows (npm is npm.cmd — needs a shell; the
65
- // shell+args-array combination is DEP0190-deprecated). Nothing dynamic.
66
- const r = process.platform === 'win32'
67
- ? spawnSync('npm view cohorte version', { encoding: 'utf8', timeout: 8000, shell: true })
68
- : spawnSync('npm', ['view', 'cohorte', 'version'], { encoding: 'utf8', timeout: 8000 });
69
- if (r.status === 0 && r.stdout) {
70
- const v = r.stdout.trim();
71
- return /^\d+\.\d+\.\d+/.test(v) ? v : null;
72
- }
73
- } catch { /* npm absent or slow */ }
74
- return null;
75
- }
76
-
77
- let _inflight = null;
78
- // `opts` bounds what a CALLER is willing to pay for the answer. The CLI's staleness
79
- // notice runs at the tail of an install, where the default 5s fetch + 8s `npm view`
80
- // fallback would make an OFFLINE install appear to hang for 13 seconds after it had
81
- // already succeeded — so it asks for a short fetch and no fallback. The cache is
82
- // shared across callers either way: a cheap lookup still serves an expensive one.
83
- async function latestNpm({ timeoutMs = 5000, fallback = true } = {}) {
84
- const age = Date.now() - _cache.at;
85
- if (_cache.at && age < (_cache.value ? CACHE_MS : FAIL_CACHE_MS)) return _cache.value;
86
- // /api/fleet resolves N projects concurrently — without this, N lookups race and
87
- // each pays the full timeout instead of sharing one.
88
- if (_inflight) return _inflight;
89
- _inflight = (async () => {
90
- try {
91
- const v = (await fetchRegistry(timeoutMs)) || (fallback ? npmView() : null);
92
- _cache = { value: v || null, at: Date.now() };
93
- return v || null;
94
- } finally {
95
- _inflight = null;
96
- }
97
- })();
98
- return _inflight;
99
- }
100
-
101
- // Naive semver compare — returns -1|0|1 (a<b|a==b|a>b). Ignores pre-release tags.
102
- function cmpSemver(a, b) {
103
- if (!a || !b) return null;
104
- const pa = a.split('.').map(n => parseInt(n, 10) || 0);
105
- const pb = b.split('.').map(n => parseInt(n, 10) || 0);
106
- for (let i = 0; i < 3; i++) {
107
- if ((pa[i] || 0) < (pb[i] || 0)) return -1;
108
- if ((pa[i] || 0) > (pb[i] || 0)) return 1;
109
- }
110
- return 0;
111
- }
112
-
113
- async function versions({ projectRoot, globalDir, cliVersion }) {
114
- const global = coreAt(globalDir);
115
- let bundled = coreAt(path.join(projectRoot, '.claude'));
116
- const pointer = pointerAt(projectRoot);
117
- const latest = await latestNpm();
118
-
119
- // A non-Claude runtime keeps its core in `.cohorte/<id>/`, not `.claude/`. Probing only the
120
- // Claude path reported a perfectly installed Cursor/Codex/Gemini/OpenCode repo as having no
121
- // core at all — and every downstream check keys off `installMode`, so one wrong probe turned
122
- // the whole report red.
123
- if (!bundled.present) {
124
- const projectCore = layouts({ projectRoot, globalDir })
125
- .find(l => l.scope === 'project' && l.core.startsWith(projectRoot));
126
- if (projectCore) bundled = coreAt(projectCore.core);
127
- }
128
-
129
- // The core that actually serves this project: bundled wins if present, else global.
130
- const effective = bundled.present ? bundled : global.present ? global : null;
131
- const installedVersion = effective ? effective.version : null;
132
- const behind = cmpSemver(installedVersion, latest);
133
-
134
- return {
135
- cli: cliVersion,
136
- latest,
137
- global,
138
- bundled,
139
- pointer,
140
- installMode: bundled.present ? 'bundled' : global.present ? 'global' : 'none',
141
- installedVersion,
142
- // -1 behind, 0 up-to-date, 1 ahead of registry (dev), null unknown
143
- freshness: behind,
144
- // freshness of the GLOBAL core specifically (for the fleet banner, independent of project mode)
145
- globalFreshness: global.present ? cmpSemver(global.version, latest) : null,
146
- };
147
- }
148
-
149
- module.exports = { versions, latestNpm, cmpSemver };
package/lib/yaml.js DELETED
@@ -1,129 +0,0 @@
1
- 'use strict';
2
- // Minimal block-YAML parser — just enough for the `yaml pipeline-profile` block in PIPELINE.md.
3
- // Deliberately dependency-free (keeps the published package zero-dep). Handles:
4
- // key: value · nested maps (indent) · block sequences (- item / - key: val) ·
5
- // flow arrays [a, b] · flow maps { a: 1 } · quoted scalars · #-comments · bool/int coercion.
6
- // NOT a general YAML implementation — anchors, multi-line scalars, etc. are out of scope.
7
-
8
- function stripComment(line) {
9
- let q = null;
10
- for (let i = 0; i < line.length; i++) {
11
- const c = line[i];
12
- if (q) { if (c === q) q = null; continue; }
13
- if (c === '"' || c === "'") { q = c; continue; }
14
- // A '#' starts a comment only at line start or after whitespace (YAML rule).
15
- if (c === '#' && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i);
16
- }
17
- return line;
18
- }
19
-
20
- function splitTopLevel(s) {
21
- const out = [];
22
- let depth = 0, q = null, cur = '';
23
- for (let i = 0; i < s.length; i++) {
24
- const c = s[i];
25
- if (q) { cur += c; if (c === q) q = null; continue; }
26
- if (c === '"' || c === "'") { q = c; cur += c; continue; }
27
- if (c === '[' || c === '{') { depth++; cur += c; continue; }
28
- if (c === ']' || c === '}') { depth--; cur += c; continue; }
29
- if (c === ',' && depth === 0) { out.push(cur); cur = ''; continue; }
30
- cur += c;
31
- }
32
- if (cur.trim() !== '') out.push(cur);
33
- return out;
34
- }
35
-
36
- function scalar(raw) {
37
- const v = raw.trim();
38
- if (v === '') return null;
39
- if (v[0] === '[') {
40
- const inner = v.slice(1, v.lastIndexOf(']'));
41
- return inner.trim() === '' ? [] : splitTopLevel(inner).map(scalar);
42
- }
43
- if (v[0] === '{') {
44
- const inner = v.slice(1, v.lastIndexOf('}'));
45
- const obj = {};
46
- if (inner.trim() !== '') {
47
- for (const pair of splitTopLevel(inner)) {
48
- const idx = pair.indexOf(':');
49
- if (idx === -1) continue;
50
- obj[pair.slice(0, idx).trim()] = scalar(pair.slice(idx + 1));
51
- }
52
- }
53
- return obj;
54
- }
55
- if ((v[0] === '"' && v[v.length - 1] === '"') || (v[0] === "'" && v[v.length - 1] === "'")) {
56
- return v.slice(1, -1);
57
- }
58
- if (v === 'true') return true;
59
- if (v === 'false') return false;
60
- if (v === 'null' || v === '~') return null;
61
- if (/^-?\d+$/.test(v)) return parseInt(v, 10);
62
- return v;
63
- }
64
-
65
- // Split "key: rest" on the FIRST colon. A colon inside the value (a URL, a
66
- // chained shell command) is therefore kept in `rest`, which is what we want;
67
- // callers are responsible for not feeding this a flow scalar (`[a, b]` /
68
- // `{a: 1}`) — the block parser checks the first character before calling.
69
- function keyValue(text) {
70
- const idx = text.indexOf(':');
71
- if (idx === -1) return null;
72
- return { key: text.slice(0, idx).trim(), rest: text.slice(idx + 1).trim() };
73
- }
74
-
75
- function parse(text) {
76
- const lines = [];
77
- for (const raw of text.split('\n')) {
78
- const stripped = stripComment(raw).replace(/\s+$/, '');
79
- if (stripped.trim() === '') continue;
80
- lines.push({ indent: stripped.match(/^ */)[0].length, text: stripped.trim() });
81
- }
82
-
83
- let pos = 0;
84
- function block(indent) {
85
- const isSeq = lines[pos].text.startsWith('- ');
86
- const result = isSeq ? [] : {};
87
- while (pos < lines.length && lines[pos].indent >= indent) {
88
- if (lines[pos].indent > indent) { pos++; continue; } // defensive: skip stray deeper line
89
- const line = lines[pos];
90
-
91
- if (line.text.startsWith('- ')) {
92
- const remainder = line.text.slice(2).trim();
93
- const kv = keyValue(remainder);
94
- // A dash item that is `key: …` (not a flow scalar) opens a nested map.
95
- if (kv && remainder[0] !== '[' && remainder[0] !== '{' && /^[\w.-]+$/.test(kv.key)) {
96
- lines[pos] = { indent: indent + 2, text: remainder }; // realign so the item's keys share one indent
97
- result.push(block(indent + 2));
98
- } else {
99
- result.push(scalar(remainder));
100
- pos++;
101
- }
102
- continue;
103
- }
104
-
105
- const kv = keyValue(line.text);
106
- if (!kv) { pos++; continue; }
107
- if (kv.rest === '') {
108
- pos++;
109
- if (pos < lines.length && lines[pos].indent > indent) result[kv.key] = block(lines[pos].indent);
110
- else result[kv.key] = null;
111
- } else {
112
- result[kv.key] = scalar(kv.rest);
113
- pos++;
114
- }
115
- }
116
- return result;
117
- }
118
-
119
- return lines.length ? block(lines[0].indent) : {};
120
- }
121
-
122
- // Extract + parse the ```yaml pipeline-profile fenced block from a PIPELINE.md string.
123
- function parseProfileBlock(md) {
124
- const m = md.match(/```yaml pipeline-profile\r?\n([\s\S]*?)\r?\n```/);
125
- if (!m) return null;
126
- return parse(m[1]);
127
- }
128
-
129
- module.exports = { parse, parseProfileBlock };
@@ -1,251 +0,0 @@
1
- # PIPELINE.md — <PROJECT_NAME> profile
2
-
3
- > **Generated by `/cohorte-init-pipeline`.** This is the project profile: the single place the
4
- > portable multi-agent pipeline reads for everything stack-specific. The pipeline core
5
- > in the pipeline core is generic and references THIS file by section — keep project facts
6
- > here, never hardcoded into agent prompts. Kept current by `/cohorte-update-pipeline` (reconcile:
7
- > new fields are topped up, your values never overwritten). Rendered agent files are
8
- > regenerated from this profile on every reconcile — customize agents through §Conventions
9
- > here, never by editing the agent files. Free-form notes belong in `CLAUDE.md`.
10
- >
11
- > **Machine block first, prose after.** The fenced `yaml pipeline-profile` block below is
12
- > the deterministic contract (flags, surfaces, commands) — commands parse it and branch on
13
- > it. The prose sections carry conventions agents read.
14
-
15
- ```yaml pipeline-profile
16
- # ── identity ────────────────────────────────────────────────────────────────
17
- name: <PROJECT_NAME>
18
- one_liner: <one sentence: what this product is>
19
- ui_language: <English | French | …> # language of ALL user-facing copy
20
- package_manager: <pnpm | npm | yarn | bun | pip | cargo | go>
21
-
22
- # ── vcs ─────────────────────────────────────────────────────────────────────
23
- vcs:
24
- host: <github | gitlab | none>
25
- remote: <owner/repo> # e.g. acme/webapp
26
- default_branch: <main>
27
- feature_branch_prefix: feature/ # branch = <prefix><feature_id>
28
- patch_branch_prefix: fix/ # same, for /cohorte-patch specs (kind: patch)
29
-
30
- # ── repo shape ──────────────────────────────────────────────────────────────
31
- repo:
32
- layout: <monorepo | single>
33
- workspace_tool: <turborepo | nx | none>
34
-
35
- # ── code retrieval (semantic navigation — cuts agent read time) ─────────────
36
- # Agents prefer symbol/graph MCP queries over grep-and-read when a provider is
37
- # wired. serena = live LSP symbol navigation (default, no index to maintain);
38
- # graphify = persistent tree-sitter knowledge graph over code + docs (needs an
39
- # index step + rescans); none = agents fall back to Grep/Glob/Read.
40
- # Registered by /cohorte-init-pipeline as a project-scope MCP server (committed .mcp.json).
41
- retrieval:
42
- provider: serena # serena | graphify | none
43
-
44
- # ── surfaces ────────────────────────────────────────────────────────────────
45
- # One entry per independently-implemented code area. /cohorte-build dispatches ONE
46
- # implementer agent per surface, in parallel. Each surface is rendered by
47
- # /cohorte-init-pipeline into its own agent file (agent field) with the tools listed.
48
- # This list GROWS automatically: /cohorte-build §1.5 adds a surface (and renders its
49
- # agent) when a spec touches a tree no surface owns, or splits a bottleneck
50
- # surface into specialized sub-surfaces. Keep one owner per tree; shared code
51
- # (routing/state/DS) is its own single-owner surface. See SCHEMA.md §Specialization.
52
- surfaces:
53
- - key: backend # short id, used as agent name + scope
54
- path: apps/api # the ONLY tree this surface's agent may touch
55
- label: backend (AdonisJS)
56
- agent: backend # rendered as backend.md in this runtime's agents dir
57
- tools: [Read, Write, Edit, Bash, Grep, Glob, mcp__serena] # mcp__<provider> mirrors retrieval.provider
58
- model: sonnet # frontmatter model tier: sonnet | haiku | inherit
59
- # sonnet = default (applies the frozen contract — cheap
60
- # vs the Opus lead); haiku = purely mechanical scaffolding;
61
- # inherit = only for surfaces with real design decisions
62
- test_cmd: pnpm --filter api test
63
- # Bridled variants — what agents actually RUN (dot reporter / failures-only /
64
- # --quiet), so a green run costs lines, not pages. "" ⇒ callers fall back to
65
- # `<cmd> 2>&1 | tail -40`. /cohorte-init-pipeline asks for these; never store a bare
66
- # `pnpm test` as the thing agents execute.
67
- test_quiet_cmd: pnpm --filter api test --reporter=dot
68
- lint_cmd: pnpm --filter api lint
69
- lint_quiet_cmd: pnpm --filter api lint --quiet
70
- format_cmd: pnpm --filter api format
71
- typecheck_cmd: pnpm --filter api exec tsc --noEmit
72
- build_cmd: ""
73
- uses_design: false
74
- - key: frontend
75
- path: apps/web
76
- label: frontend (React/TanStack)
77
- agent: frontend
78
- tools: [Read, Write, Edit, Bash, Grep, Glob, DesignSync, mcp__serena]
79
- model: sonnet # default even for design surfaces — designs + contract are
80
- # frozen inputs the agent applies; `inherit` (bills at the
81
- # lead's tier, often Opus) ONLY if this surface must make
82
- # novel design decisions
83
- test_cmd: pnpm --filter web test
84
- test_quiet_cmd: pnpm --filter web test --reporter=dot
85
- lint_cmd: pnpm --filter web lint
86
- lint_quiet_cmd: pnpm --filter web lint --quiet
87
- format_cmd: pnpm --filter web format
88
- typecheck_cmd: pnpm check-types
89
- build_cmd: pnpm --filter web build
90
- uses_design: true
91
-
92
- # ── contract (the only cross-surface sync channel) ──────────────────────────
93
- contract:
94
- enabled: <true | false> # false ⇒ no shared contract; skip §2 of /cohorte-build
95
- mechanism: <shared-types-zod | openapi | protobuf | json-schema | none>
96
- path: packages/shared-types/src # where <feature_id>.<ext> is authored
97
- ext: ts
98
- index: packages/shared-types/src/index.ts # barrel to export from, or "" if none
99
- authored_by: lead # NEVER the implementer agents
100
-
101
- # ── release notes (optional) ────────────────────────────────────────────────
102
- # A per-feature release note the project's versioning tool consumes. When
103
- # enabled, /cohorte-ship authors it BEFORE dispatching the release agent, so it
104
- # ships inside the release commit. Repos whose CI fails a PR that lacks one
105
- # (Changesets' `changeset` job) MUST set this — otherwise every ship goes red.
106
- release_notes:
107
- enabled: <true | false> # false ⇒ /cohorte-ship §2b is a no-op
108
- tool: <changesets | none> # what consumes the file
109
- dir: .changeset # where the note is authored
110
- filename: "<feature_id>.md" # one file per feature
111
- anchor_package: shared-types # sole key in the front-matter; a lockstep/fixed group propagates the bump
112
- language: <ui_language | English> # language of the note's prose
113
- forbid_levels: [] # bumps project policy refuses, e.g. [major] while 0.x
114
- empty_cmd: "pnpm changeset --empty" # when no version should move; "" if the tool has no such escape hatch
115
- ci_job: changeset # CI job that fails when the note is missing (name it so /cohorte-ship §5 recognizes the red)
116
- guidance: | # rules the lead follows when writing the note (project policy)
117
- major = the deploy needs an operator action; minor = a user-visible capability;
118
- patch = fix/perf/refactor with no change of use. No client names, no internal
119
- paths, no exploitable attack vector.
120
-
121
- # ── repo-wide commands (root unless noted) ──────────────────────────────────
122
- commands:
123
- install: pnpm install
124
- dev: pnpm dev
125
- lint: pnpm lint
126
- lint_quiet: pnpm lint --quiet # bridled variant (see surfaces[].*_quiet_cmd)
127
- format: pnpm format
128
- typecheck: pnpm check-types
129
- test: pnpm test
130
- test_quiet: pnpm test --reporter=dot # bridled variant — what the /cohorte-review preflight runs
131
- # migration commands — omit / leave "" if the project has no DB migrations
132
- migrate: "cd apps/api && node ace migration:run"
133
- make_migration: "cd apps/api && node ace make:migration"
134
-
135
- # ── rbac (optional) ─────────────────────────────────────────────────────────
136
- rbac:
137
- enabled: <true | false>
138
- hierarchy: [super-admin, admin, editor, member] # highest → lowest, or []
139
- note: "the platform must serve EVERY role, not just admins"
140
-
141
- # ── design system (optional) ────────────────────────────────────────────────
142
- design:
143
- enabled: <true | false> # false ⇒ /cohorte-align-ds + design gates are no-ops
144
- provider: <claude-design | figma | none>
145
- design_system_project: <uuid-or-id> # the UI-kit source of truth
146
- design_project: none # legacy fallback for bare-filename design_files only; keep `none` — new specs use full links that carry their own project, so nothing here goes stale on a DS rebuild
147
- snapshot_dir: apps/web/design-reference # committed DS snapshot for /cohorte-align-ds diff
148
- direction: design-to-code # NEVER push code→design for a curated DS
149
- ui_kit_path: apps/web/src/components/ui
150
- tokens_path: apps/web/src/index.css
151
-
152
- # ── isolation / parallelism (optional) ──────────────────────────────────────
153
- isolation:
154
- enabled: <true | false> # false ⇒ features run in the main checkout
155
- unit: git-worktree
156
- db_per_worktree: <true | false>
157
- db_name_pattern: "<name>_<id>" # e.g. myapp_<id>
158
- port_base: { api: 3333, web: 5173 } # each worktree gets base+slot
159
- compose_file: docker-compose.yml
160
- registry: .worktrees/slots.tsv
161
-
162
- # ── gate (drives the pipeline gate hook + permissions) ─────────────────────
163
- # Confirm-first or hard-deny on dangerous Bash, inspecting the FULL command
164
- # string (so chained `cd x && …` forms are caught).
165
- gate:
166
- # Three tiers. `deny` = always hard-blocked. `ask` = always confirm (branch-independent risk).
167
- # `ask_on_default_branch` = confirm ONLY when the checked-out branch is `default_branch`; on
168
- # feature branches these run freely. gate.py resolves the branch at run time (git rev-parse);
169
- # unknown branch (no repo / detached) ⇒ gated, to stay safe. Keep git + docker here so agents
170
- # move fast on feature branches but main stays protected.
171
- default_branch: main # the protected branch (mirror vcs.default_branch)
172
- deny: # never allowed, on any branch
173
- - "node ace migration:fresh"
174
- - "node ace migration:reset"
175
- - "node ace migration:rollback"
176
- - "node ace db:wipe"
177
- ask: # always confirm, on any branch
178
- - "node ace migration:run"
179
- - "node ace db:"
180
- - "psql"
181
- ask_on_default_branch: # free on feature branches, confirm on default_branch
182
- - "git commit"
183
- - "git push"
184
- - "git merge"
185
- - "git rebase"
186
- - "git reset"
187
- - "docker compose"
188
- # Phase gate: review dispatches require a fresh `preflight.ok` stamp,
189
- # written by pipeline/scripts/preflight.sh when typecheck+lint+tests are green —
190
- # gate.py "ask"s the dispatch when the stamp is missing, stale, or the code changed since.
191
- # The stamp is local: keep `preflight.ok` gitignored, never committed.
192
- preflight:
193
- enabled: true
194
- agents: [review] # subagent_types the stamp gates
195
- max_age_minutes: 30
196
-
197
- ```
198
-
199
- > **Note.** The **kanban** board link is **user-scoped** — NOT configured here. Its facts live in the
200
- > consolidated user config (`~/.claude/cohorte.config.yaml`, or `~/.cohorte/cohorte.config.yaml` on
201
- > a non-Claude runtime), read at runtime (the kanban board is keyed by this
202
- > profile's `name`). This project profile only governs the dev pipeline (`/cohorte-brainstorm…/cohorte-ship`).
203
-
204
- ---
205
-
206
- ## Conventions
207
-
208
- > Read by the implementer + review agents. Keep these terse and rule-shaped; the machine
209
- > block above holds the paths/commands they enforce against.
210
-
211
- ### Shared
212
-
213
- - User-facing copy (API messages, UI text) in **<ui_language>**.
214
- - <e.g. Zod v4 syntax `z.email()`/`z.uuid()`; ESM `.js` import extensions — project-specific>
215
-
216
- ### Surface: `backend` (`apps/api`)
217
-
218
- - <one-action controllers · validator on every write · standardized response envelope · base model / UUID PK · `#`-alias imports · additive-only migrations · …>
219
-
220
- ### Surface: `frontend` (`apps/web`)
221
-
222
- - <file-based routes with loaders/guards · domain `lib/api` modules over the shared client · design-system primitives, `cn()`+CVA · **mobile-first, fully responsive** · consume contract types, never redefine · …>
223
-
224
- ### Testing — strict TDD (red → green → refactor)
225
-
226
- - <backend test runner + what a contract test must cover (status, validation, auth, payload) · DB isolation per test>
227
- - <frontend test runner + what to cover (hooks, api modules, component behavior); note layout is not unit-testable>
228
-
229
- ---
230
-
231
- ## Design brief note (feeds `/cohorte-spec` §8 and Claude Design)
232
-
233
- - <design system name + primitives to use · mobile-first · copy language · brand/theming constraints>
234
- - A feature lists its design pages in the spec front-matter `design_files` as **full links** of the form
235
- `https://claude.ai/design/p/<projectId>?file=<file>` — each carries its own project (`/p/<projectId>`)
236
- and page (`?file=`), so an agent extracts both and reads it via `DesignSync get_file(<projectId>,
237
- <file>)`. Paste the link at `/cohorte-build`'s design gate. No stored project id ⇒ a design-system rebuild
238
- (new project) just means pasting the new links. (Bare file names are legacy, resolved in `design_project`.)
239
-
240
- ---
241
-
242
- ## Personas for `/cohorte-brainstorm` (optional — the panel)
243
-
244
- <Panel members with a job AND a personality who challenge the idea from their angle and
245
- disagree with each other. Include a role-coverage guard if `rbac.enabled`. Example:>
246
-
247
- - **PM** (pragmatic, scope-cutting) — real value? what do we drop?
248
- - **Skeptical senior engineer** (blunt) — feasibility, edge cases, tech debt.
249
- - **UX/product designer** — mobile-first flows, accessibility, empty/error states.
250
- - **Security & RBAC** (paranoid) — permission leaks, cross-tenant exposure, least privilege.
251
- - <one persona per RBAC role so the feature serves every role, not just admins>