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.
- 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 -1453
- package/bin/cli.js +0 -812
- package/bin/report.js +0 -141
- package/core/adapter/render.js +0 -389
- 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 -170
- 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 -134
- package/core/hooks/gate.py +0 -580
- package/core/runtimes/claude.json +0 -74
- package/core/runtimes/codex.json +0 -83
- 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 -115
- 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 -504
- package/lib/runtime.js +0 -134
- package/lib/versions.js +0 -149
- package/lib/yaml.js +0 -129
- package/profile/PIPELINE.template.md +0 -251
- package/profile/SCHEMA.md +0 -698
- 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 -368
- package/scripts/test-gate.mjs +0 -428
- package/scripts/test-kanban.mjs +0 -304
- package/scripts/test-lib.mjs +0 -310
- package/scripts/test-metrics.mjs +0 -156
- package/scripts/test-workflows.mjs +0 -758
- 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>
|