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.
- package/README.md +7 -0
- package/assets/manifest.json +193 -0
- package/assets/migrations/config/README.md +12 -0
- package/assets/migrations/state/0001_init.sql +154 -0
- package/assets/prompts/README.md +11 -0
- package/assets/prompts/agents/fixer.md +7 -0
- package/assets/prompts/agents/implementer.md +12 -0
- package/assets/prompts/agents/reviewer.md +8 -0
- package/assets/prompts/agents/security-reviewer.md +7 -0
- package/assets/prompts/phases/build.md +5 -0
- package/assets/prompts/phases/review.md +5 -0
- package/assets/prompts/phases/test.md +5 -0
- package/assets/prompts/system.md +11 -0
- package/assets/schemas/agent-output.schema.json +225 -0
- package/assets/schemas/auth-status.schema.json +87 -0
- package/assets/schemas/command-result.schema.json +192 -0
- package/assets/schemas/commands.schema.json +952 -0
- package/assets/schemas/config.schema.json +2259 -0
- package/assets/schemas/doctor-report.schema.json +104 -0
- package/assets/schemas/events.schema.json +8514 -0
- package/assets/schemas/fake-script.schema.json +549 -0
- package/assets/schemas/inspect.schema.json +3133 -0
- package/assets/schemas/manifest.schema.json +79 -0
- package/assets/schemas/ownership.schema.json +52 -0
- package/assets/schemas/policy-verdict.schema.json +282 -0
- package/assets/schemas/project-model.schema.json +783 -0
- package/assets/schemas/project-status.schema.json +217 -0
- package/assets/schemas/reconcile-plan.schema.json +155 -0
- package/assets/schemas/run-diff.schema.json +177 -0
- package/assets/schemas/run-snapshot-manifest.schema.json +2104 -0
- package/assets/schemas/run-state.schema.json +1755 -0
- package/assets/schemas/runtime-capabilities.schema.json +1545 -0
- package/assets/schemas/sandbox-capabilities.schema.json +116 -0
- package/assets/schemas/skill.schema.json +87 -0
- package/assets/schemas/spec.schema.json +202 -0
- package/assets/schemas/tool-catalogue.schema.json +655 -0
- package/assets/schemas/trust-record.schema.json +38 -0
- package/assets/skills/README.md +9 -0
- package/assets/skills/inspect-surface.md +4 -0
- package/dist/agent-host.mjs +50 -0
- package/dist/agent-host.mjs.map +1 -0
- package/dist/bundle-manifest.json +102 -0
- package/dist/chunks/__host-DgGPyco9.mjs +18 -0
- package/dist/chunks/__host-DgGPyco9.mjs.map +1 -0
- package/dist/chunks/approve-Cv135UqI.mjs +19 -0
- package/dist/chunks/approve-Cv135UqI.mjs.map +1 -0
- package/dist/chunks/auth-BT5oVp76.mjs +132 -0
- package/dist/chunks/auth-BT5oVp76.mjs.map +1 -0
- package/dist/chunks/auth-D8rCVY33.mjs +84 -0
- package/dist/chunks/auth-D8rCVY33.mjs.map +1 -0
- package/dist/chunks/brainstorm-CU10T1tm.mjs +7 -0
- package/dist/chunks/brainstorm-CU10T1tm.mjs.map +1 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs +15 -0
- package/dist/chunks/cancel-KV_cpsqS.mjs.map +1 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs +57 -0
- package/dist/chunks/canonical-C8UxeEmB.mjs.map +1 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs +1549 -0
- package/dist/chunks/catalogue-BvwSrM_j.mjs.map +1 -0
- package/dist/chunks/compose-Cr0hP0qm.mjs +7303 -0
- package/dist/chunks/compose-Cr0hP0qm.mjs.map +1 -0
- package/dist/chunks/config-DefHA2Lw.mjs +96 -0
- package/dist/chunks/config-DefHA2Lw.mjs.map +1 -0
- package/dist/chunks/contract-B0ra2-bm.mjs +295 -0
- package/dist/chunks/contract-B0ra2-bm.mjs.map +1 -0
- package/dist/chunks/contract-vYbwYr0Y.mjs +123 -0
- package/dist/chunks/contract-vYbwYr0Y.mjs.map +1 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs +15 -0
- package/dist/chunks/deny-ClTxSvHJ.mjs.map +1 -0
- package/dist/chunks/diff-Qw0II0sj.mjs +101 -0
- package/dist/chunks/diff-Qw0II0sj.mjs.map +1 -0
- package/dist/chunks/discover-30Ciya_0.mjs +20 -0
- package/dist/chunks/discover-30Ciya_0.mjs.map +1 -0
- package/dist/chunks/doctor-DfCrKpfT.mjs +222 -0
- package/dist/chunks/doctor-DfCrKpfT.mjs.map +1 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs +18 -0
- package/dist/chunks/fix-D4Tj8RVA.mjs.map +1 -0
- package/dist/chunks/gc-BBXlBF5w.mjs +161 -0
- package/dist/chunks/gc-BBXlBF5w.mjs.map +1 -0
- package/dist/chunks/init-Okz4mlhW.mjs +30 -0
- package/dist/chunks/init-Okz4mlhW.mjs.map +1 -0
- package/dist/chunks/inspect-Buby4cQD.mjs +22 -0
- package/dist/chunks/inspect-Buby4cQD.mjs.map +1 -0
- package/dist/chunks/logs-Giq5sUau.mjs +25 -0
- package/dist/chunks/logs-Giq5sUau.mjs.map +1 -0
- package/dist/chunks/migrate-DRoFUg53.mjs +19 -0
- package/dist/chunks/migrate-DRoFUg53.mjs.map +1 -0
- package/dist/chunks/models-Btp6X2YZ.mjs +24 -0
- package/dist/chunks/models-Btp6X2YZ.mjs.map +1 -0
- package/dist/chunks/pause-BASBfKLO.mjs +16 -0
- package/dist/chunks/pause-BASBfKLO.mjs.map +1 -0
- package/dist/chunks/policy-DUIgzF3W.mjs +139 -0
- package/dist/chunks/policy-DUIgzF3W.mjs.map +1 -0
- package/dist/chunks/providers-Dx78imKj.mjs +19 -0
- package/dist/chunks/providers-Dx78imKj.mjs.map +1 -0
- package/dist/chunks/reconcile-BfD785gd.mjs +26 -0
- package/dist/chunks/reconcile-BfD785gd.mjs.map +1 -0
- package/dist/chunks/render-DyZr5Lgi.mjs +53 -0
- package/dist/chunks/render-DyZr5Lgi.mjs.map +1 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs +15 -0
- package/dist/chunks/resume-GJQRb2Eb.mjs.map +1 -0
- package/dist/chunks/retry-CLrUem1H.mjs +15 -0
- package/dist/chunks/retry-CLrUem1H.mjs.map +1 -0
- package/dist/chunks/review-BDdjb6kB.mjs +17 -0
- package/dist/chunks/review-BDdjb6kB.mjs.map +1 -0
- package/dist/chunks/run-CgTWYlcd.mjs +82 -0
- package/dist/chunks/run-CgTWYlcd.mjs.map +1 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs +26 -0
- package/dist/chunks/run-tool-C3rAv4XF.mjs.map +1 -0
- package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
- package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
- package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
- package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
- package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
- package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
- package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
- package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
- package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
- package/dist/chunks/src-CId6kMsY.mjs +1689 -0
- package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
- package/dist/chunks/src-CfAOde7C.mjs +5403 -0
- package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
- package/dist/chunks/src-Cu3TxNob.mjs +783 -0
- package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
- package/dist/chunks/src-DcU4IXMe.mjs +117 -0
- package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
- package/dist/chunks/src-JxawBQBn.mjs +446 -0
- package/dist/chunks/src-JxawBQBn.mjs.map +1 -0
- package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
- package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
- package/dist/chunks/status-SMjiOMYy.mjs +29 -0
- package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
- package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
- package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
- package/dist/chunks/update-BXIppsbi.mjs +16 -0
- package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
- package/dist/cli.mjs +175 -0
- package/dist/cli.mjs.map +1 -0
- package/package.json +17 -32
- package/CHANGELOG.md +0 -1461
- package/bin/cli.js +0 -817
- package/bin/report.js +0 -141
- package/core/adapter/render.js +0 -415
- package/core/agents/implementer.template.md +0 -142
- package/core/agents/profile-reader.md +0 -50
- package/core/agents/release.md +0 -58
- package/core/agents/review.md +0 -179
- package/core/commands/cohorte-align-ds.md +0 -33
- package/core/commands/cohorte-audit.md +0 -58
- package/core/commands/cohorte-brainstorm.md +0 -70
- package/core/commands/cohorte-build.md +0 -219
- package/core/commands/cohorte-doctor.md +0 -194
- package/core/commands/cohorte-fix.md +0 -80
- package/core/commands/cohorte-fleet.md +0 -103
- package/core/commands/cohorte-init-pipeline.md +0 -31
- package/core/commands/cohorte-intake.md +0 -92
- package/core/commands/cohorte-patch.md +0 -118
- package/core/commands/cohorte-refactor.md +0 -59
- package/core/commands/cohorte-retro.md +0 -85
- package/core/commands/cohorte-review.md +0 -270
- package/core/commands/cohorte-ship.md +0 -122
- package/core/commands/cohorte-spec.md +0 -101
- package/core/commands/cohorte-update-pipeline.md +0 -165
- package/core/hooks/gate.py +0 -588
- package/core/runtimes/claude.json +0 -74
- package/core/runtimes/codex.json +0 -85
- package/core/runtimes/cursor.json +0 -76
- package/core/runtimes/gemini.json +0 -76
- package/core/runtimes/opencode.json +0 -73
- package/core/templates/agent-handoff.md +0 -34
- package/core/templates/brainstorm-return.md +0 -36
- package/core/templates/decisions.template.md +0 -42
- package/core/templates/design-brief.md +0 -44
- package/core/templates/patch.template.md +0 -86
- package/core/templates/pr-body.md +0 -25
- package/core/templates/review-feedback.md +0 -38
- package/core/templates/spec.template.md +0 -87
- package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
- package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
- package/core/templates/steps/init-pipeline/04-write-render.md +0 -145
- package/core/templates/steps/init-pipeline/05-report.md +0 -12
- package/core/workflows/audit.js +0 -240
- package/core/workflows/loop.js +0 -641
- package/core/workflows/refactor.js +0 -276
- package/core/workflows/review.js +0 -456
- package/install.ps1 +0 -103
- package/install.sh +0 -114
- package/lib/doctor.js +0 -534
- package/lib/runtime.js +0 -139
- package/lib/versions.js +0 -149
- package/lib/yaml.js +0 -129
- package/profile/PIPELINE.template.md +0 -259
- package/profile/SCHEMA.md +0 -735
- package/profile/cohorte.config.template.yaml +0 -43
- package/scripts/assert-gate-hook.mjs +0 -75
- package/scripts/kanban-move.sh +0 -313
- package/scripts/metrics/collect.mjs +0 -531
- package/scripts/metrics/prices.json +0 -42
- package/scripts/new-feature.sh.template +0 -98
- package/scripts/preflight.sh +0 -124
- package/scripts/remove-feature.sh.template +0 -56
- package/scripts/test-adapter.mjs +0 -437
- package/scripts/test-gate.mjs +0 -443
- package/scripts/test-kanban.mjs +0 -304
- package/scripts/test-lib.mjs +0 -356
- package/scripts/test-metrics.mjs +0 -156
- package/scripts/test-workflows.mjs +0 -758
- 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 };
|