cohorte 2.10.1 → 3.0.0-dev.1

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 (213) 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 +154 -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 +2187 -0
  41. package/dist/agent-host.mjs.map +1 -0
  42. package/dist/bundle-manifest.json +104 -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-CYOh60KG.mjs +93 -0
  50. package/dist/chunks/auth-CYOh60KG.mjs.map +1 -0
  51. package/dist/chunks/brainstorm-BwKA6yZP.mjs +7 -0
  52. package/dist/chunks/brainstorm-BwKA6yZP.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-Dvxunpto.mjs +7307 -0
  60. package/dist/chunks/compose-Dvxunpto.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-BkMjU1kR.mjs +295 -0
  64. package/dist/chunks/contract-BkMjU1kR.mjs.map +1 -0
  65. package/dist/chunks/contract-bQ9FJl4N.mjs +123 -0
  66. package/dist/chunks/contract-bQ9FJl4N.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-eS01-me1.mjs +21 -0
  72. package/dist/chunks/discover-eS01-me1.mjs.map +1 -0
  73. package/dist/chunks/doctor-B-H2UK5e.mjs +222 -0
  74. package/dist/chunks/doctor-B-H2UK5e.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-BUnzIkeE.mjs +63 -0
  80. package/dist/chunks/init-BUnzIkeE.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-DybihKzZ.mjs +33 -0
  86. package/dist/chunks/migrate-DybihKzZ.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-Bhy3IIYo.mjs +50 -0
  96. package/dist/chunks/reconcile-Bhy3IIYo.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/scan-8Gy_B6kI.mjs +122 -0
  110. package/dist/chunks/scan-8Gy_B6kI.mjs.map +1 -0
  111. package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
  112. package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
  113. package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
  114. package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
  115. package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
  116. package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
  117. package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
  118. package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
  119. package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
  120. package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
  121. package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
  122. package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
  123. package/dist/chunks/src-CId6kMsY.mjs +1689 -0
  124. package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
  125. package/dist/chunks/src-CfAOde7C.mjs +5403 -0
  126. package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
  127. package/dist/chunks/src-CmFjh4M0.mjs +928 -0
  128. package/dist/chunks/src-CmFjh4M0.mjs.map +1 -0
  129. package/dist/chunks/src-Cu3TxNob.mjs +783 -0
  130. package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
  131. package/dist/chunks/src-DcU4IXMe.mjs +117 -0
  132. package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
  133. package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
  134. package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
  135. package/dist/chunks/status-SMjiOMYy.mjs +29 -0
  136. package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
  137. package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
  138. package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
  139. package/dist/chunks/update-BXIppsbi.mjs +16 -0
  140. package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
  141. package/dist/cli.mjs +175 -0
  142. package/dist/cli.mjs.map +1 -0
  143. package/package.json +17 -32
  144. package/CHANGELOG.md +0 -1461
  145. package/bin/cli.js +0 -817
  146. package/bin/report.js +0 -141
  147. package/core/adapter/render.js +0 -415
  148. package/core/agents/implementer.template.md +0 -142
  149. package/core/agents/profile-reader.md +0 -50
  150. package/core/agents/release.md +0 -58
  151. package/core/agents/review.md +0 -179
  152. package/core/commands/cohorte-align-ds.md +0 -33
  153. package/core/commands/cohorte-audit.md +0 -58
  154. package/core/commands/cohorte-brainstorm.md +0 -70
  155. package/core/commands/cohorte-build.md +0 -219
  156. package/core/commands/cohorte-doctor.md +0 -194
  157. package/core/commands/cohorte-fix.md +0 -80
  158. package/core/commands/cohorte-fleet.md +0 -103
  159. package/core/commands/cohorte-init-pipeline.md +0 -31
  160. package/core/commands/cohorte-intake.md +0 -92
  161. package/core/commands/cohorte-patch.md +0 -118
  162. package/core/commands/cohorte-refactor.md +0 -59
  163. package/core/commands/cohorte-retro.md +0 -85
  164. package/core/commands/cohorte-review.md +0 -270
  165. package/core/commands/cohorte-ship.md +0 -122
  166. package/core/commands/cohorte-spec.md +0 -101
  167. package/core/commands/cohorte-update-pipeline.md +0 -165
  168. package/core/hooks/gate.py +0 -588
  169. package/core/runtimes/claude.json +0 -74
  170. package/core/runtimes/codex.json +0 -85
  171. package/core/runtimes/cursor.json +0 -76
  172. package/core/runtimes/gemini.json +0 -76
  173. package/core/runtimes/opencode.json +0 -73
  174. package/core/templates/agent-handoff.md +0 -34
  175. package/core/templates/brainstorm-return.md +0 -36
  176. package/core/templates/decisions.template.md +0 -42
  177. package/core/templates/design-brief.md +0 -44
  178. package/core/templates/patch.template.md +0 -86
  179. package/core/templates/pr-body.md +0 -25
  180. package/core/templates/review-feedback.md +0 -38
  181. package/core/templates/spec.template.md +0 -87
  182. package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
  183. package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
  184. package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
  185. package/core/templates/steps/init-pipeline/04-write-render.md +0 -145
  186. package/core/templates/steps/init-pipeline/05-report.md +0 -12
  187. package/core/workflows/audit.js +0 -240
  188. package/core/workflows/loop.js +0 -641
  189. package/core/workflows/refactor.js +0 -276
  190. package/core/workflows/review.js +0 -456
  191. package/install.ps1 +0 -103
  192. package/install.sh +0 -114
  193. package/lib/doctor.js +0 -534
  194. package/lib/runtime.js +0 -139
  195. package/lib/versions.js +0 -149
  196. package/lib/yaml.js +0 -129
  197. package/profile/PIPELINE.template.md +0 -259
  198. package/profile/SCHEMA.md +0 -735
  199. package/profile/cohorte.config.template.yaml +0 -43
  200. package/scripts/assert-gate-hook.mjs +0 -75
  201. package/scripts/kanban-move.sh +0 -313
  202. package/scripts/metrics/collect.mjs +0 -531
  203. package/scripts/metrics/prices.json +0 -42
  204. package/scripts/new-feature.sh.template +0 -98
  205. package/scripts/preflight.sh +0 -124
  206. package/scripts/remove-feature.sh.template +0 -56
  207. package/scripts/test-adapter.mjs +0 -437
  208. package/scripts/test-gate.mjs +0 -443
  209. package/scripts/test-kanban.mjs +0 -304
  210. package/scripts/test-lib.mjs +0 -356
  211. package/scripts/test-metrics.mjs +0 -156
  212. package/scripts/test-workflows.mjs +0 -758
  213. package/scripts/validate-core.mjs +0 -325
package/lib/runtime.js DELETED
@@ -1,139 +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
- // A global Codex core must never bind surface agents to its installing project.
98
- // Also repair discovery for registries written before this distinction existed.
99
- surfaceAgents: id === 'codex' ? path.join(projectRoot, '.codex', 'agents')
100
- : abs(p.surface_agents || p.agents, path.join(dir, 'agents')),
101
- agentExt: p.agent_ext || (id === 'codex' ? '.toml' : '.md'),
102
- commands: abs(p.commands, path.join(dir, 'commands')),
103
- state: abs(null, p.state || stateDirFor(id)),
104
- // Pre-2.2.0 cores carry no registry, so nothing records where the hook is registered.
105
- // Claude's is `<core>/settings.json` and always has been — without this fallback a
106
- // legacy install reports its perfectly good registration as missing.
107
- hooksConfig: (rec.paths && rec.paths.hooks_config)
108
- ? abs(rec.paths.hooks_config)
109
- : (id === 'claude' ? path.join(dir, 'settings.json') : null),
110
- capabilities: rec.capabilities || {
111
- subagents: true, hooks: id === 'claude', workflows: id === 'claude',
112
- },
113
- legacy: !registry,
114
- });
115
- }
116
- }
117
- return found;
118
- }
119
-
120
- /**
121
- * The layout the project-level checks should use. Claude first when present — it is the
122
- * reference implementation and the layout every pre-2.2.0 repo has — else whatever is
123
- * installed. Null when nothing is.
124
- */
125
- function primary(all) {
126
- if (!all.length) return null;
127
- return all.find((l) => l.id === 'claude') || all[0];
128
- }
129
-
130
- // The state dirs actually in play. A repo driven from Claude AND Cursor has both `.claude`
131
- // and `.cohorte`; a check that looked at only one would miss a stamp or a metrics file.
132
- function stateDirs(all, projectRoot) {
133
- const dirs = [];
134
- for (const l of all) if (!dirs.includes(l.state)) dirs.push(l.state);
135
- if (!dirs.length) dirs.push(path.join(projectRoot, '.claude'));
136
- return dirs;
137
- }
138
-
139
- 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 };