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/bin/report.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
// Machine-readable reports for `cohorte specs` and `cohorte doctor`.
|
|
3
|
-
//
|
|
4
|
-
// Both commands read what lib/doctor.js computes (`scanSpecs` and `state`) and
|
|
5
|
-
// render it four ways:
|
|
6
|
-
//
|
|
7
|
-
// default a human table on a terminal
|
|
8
|
-
// --porcelain one record per line, fields separated by U+001F (the ASCII unit
|
|
9
|
-
// separator) — stable, greppable, and immune to a spec title that
|
|
10
|
-
// contains a space, a tab or a pipe
|
|
11
|
-
// --json the native document (the full doctor state, the spec list)
|
|
12
|
-
// --panel the payload shape a Francois extension panel validates against
|
|
13
|
-
// (`{rows:[{key,value,tone}]}` / `{rows:[{id,cells,tone}]}`). This is
|
|
14
|
-
// the ONLY Francois-aware surface in cohorte; everything else here is
|
|
15
|
-
// generic. See github.com/TheBidouilleAgency/francois-plugin-cohorte.
|
|
16
|
-
//
|
|
17
|
-
// Dependency-free, and it never writes anything.
|
|
18
|
-
|
|
19
|
-
const US = String.fromCharCode(0x1f); // ASCII unit separator (U+001F)
|
|
20
|
-
|
|
21
|
-
// A spec's status → a display tone. The tones are the ones a status column can carry
|
|
22
|
-
// anywhere (terminal colour, panel row tone); `busy` marks the two statuses that mean
|
|
23
|
-
// "a command is mid-flight on this spec".
|
|
24
|
-
const STATUS_TONE = {
|
|
25
|
-
draft: 'neutral',
|
|
26
|
-
frozen: 'neutral',
|
|
27
|
-
'in-progress': 'busy',
|
|
28
|
-
'in-review': 'busy',
|
|
29
|
-
shipped: 'ok',
|
|
30
|
-
blocked: 'error',
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// doctor's four check states → the same tone vocabulary. `skip` is not a failure:
|
|
34
|
-
// a check that does not apply to this project (no design system, no worktrees) reads
|
|
35
|
-
// neutral, never warn.
|
|
36
|
-
const CHECK_TONE = { ok: 'ok', warn: 'warn', bad: 'error', skip: 'neutral' };
|
|
37
|
-
|
|
38
|
-
// Collapse anything that would break the line format or a single-line cell. A spec
|
|
39
|
-
// title is free text written by a human in YAML frontmatter — it can contain a newline
|
|
40
|
-
// continuation, and it must never split one record into two. A stray separator inside a
|
|
41
|
-
// field goes the same way, and so does the padding a doctor `fix` carries for terminal
|
|
42
|
-
// alignment — in a one-line panel cell that padding reads as a hole.
|
|
43
|
-
function flat(value) {
|
|
44
|
-
return String(value == null ? '' : value)
|
|
45
|
-
.replace(new RegExp(`[\\s${US}]+`, 'g'), ' ')
|
|
46
|
-
.trim();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function specRecords(projectRoot, scanSpecs) {
|
|
50
|
-
return scanSpecs(projectRoot).map((s) => ({
|
|
51
|
-
id: s.id,
|
|
52
|
-
title: s.title || '',
|
|
53
|
-
status: s.status || 'unknown',
|
|
54
|
-
branch: s.branch || '',
|
|
55
|
-
file: s.file,
|
|
56
|
-
tone: STATUS_TONE[s.status] || 'warn', // an unknown status IS the warning
|
|
57
|
-
}));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function specsPorcelain(records) {
|
|
61
|
-
return records
|
|
62
|
-
.map((r) => [r.id, r.title, r.status, r.branch, r.tone].map(flat).join(US))
|
|
63
|
-
.join('\n');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function specsPanel(records) {
|
|
67
|
-
return JSON.stringify({
|
|
68
|
-
rows: records.map((r) => ({
|
|
69
|
-
id: flat(r.id),
|
|
70
|
-
tone: r.tone,
|
|
71
|
-
cells: { id: flat(r.id), title: flat(r.title), status: flat(r.status), branch: flat(r.branch) },
|
|
72
|
-
})),
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function specsHuman(records) {
|
|
77
|
-
if (!records.length) return 'no specs in ./specs — /cohorte-spec writes the first one';
|
|
78
|
-
const w = (k) => Math.max(...records.map((r) => flat(r[k]).length), k.length);
|
|
79
|
-
const [wi, ws, wb] = [w('id'), w('status'), w('branch')];
|
|
80
|
-
const head = `${'id'.padEnd(wi)} ${'status'.padEnd(ws)} ${'branch'.padEnd(wb)} title`;
|
|
81
|
-
const rows = records.map(
|
|
82
|
-
(r) => `${flat(r.id).padEnd(wi)} ${flat(r.status).padEnd(ws)} ${flat(r.branch).padEnd(wb)} ${flat(r.title)}`,
|
|
83
|
-
);
|
|
84
|
-
return [head, '-'.repeat(head.length), ...rows].join('\n');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function checkRecords(state) {
|
|
88
|
-
return state.checks.map((c) => ({
|
|
89
|
-
id: c.id,
|
|
90
|
-
label: c.label,
|
|
91
|
-
status: c.status,
|
|
92
|
-
detail: c.detail,
|
|
93
|
-
fix: c.fix || '',
|
|
94
|
-
tone: CHECK_TONE[c.status] || 'neutral',
|
|
95
|
-
}));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function doctorPorcelain(records) {
|
|
99
|
-
return records
|
|
100
|
-
.map((r) => [r.id, r.label, r.status, r.detail, r.fix].map(flat).join(US))
|
|
101
|
-
.join('\n');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// key-value, one row per check: the check's label keyed against what it found. The fix
|
|
105
|
-
// rides in the value for a failing check — a panel row has nowhere else to put it, and
|
|
106
|
-
// a health report that says "broken" without saying "run this" is half a report.
|
|
107
|
-
function doctorPanel(records) {
|
|
108
|
-
return JSON.stringify({
|
|
109
|
-
rows: records.map((r) => ({
|
|
110
|
-
key: flat(r.label),
|
|
111
|
-
value: flat(r.fix && r.tone !== 'ok' ? `${r.detail} → ${r.fix}` : r.detail),
|
|
112
|
-
tone: r.tone,
|
|
113
|
-
})),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function doctorHuman(state, records) {
|
|
118
|
-
const mark = { ok: 'ok ', warn: 'warn', bad: 'BAD ', skip: 'skip' };
|
|
119
|
-
const lines = [`cohorte doctor — ${state.project}`, ''];
|
|
120
|
-
for (const r of records) {
|
|
121
|
-
lines.push(`${mark[r.status] || '? '} ${r.label} — ${flat(r.detail)}`);
|
|
122
|
-
if (r.fix && r.status !== 'ok') lines.push(` fix: ${flat(r.fix)}`);
|
|
123
|
-
}
|
|
124
|
-
const s = state.summary;
|
|
125
|
-
lines.push('', `${s.ok} ok · ${s.warn} warn · ${s.bad} bad · ${s.skip} skipped`);
|
|
126
|
-
return lines.join('\n');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
module.exports = {
|
|
130
|
-
US,
|
|
131
|
-
STATUS_TONE,
|
|
132
|
-
CHECK_TONE,
|
|
133
|
-
specRecords,
|
|
134
|
-
specsPorcelain,
|
|
135
|
-
specsPanel,
|
|
136
|
-
specsHuman,
|
|
137
|
-
checkRecords,
|
|
138
|
-
doctorPorcelain,
|
|
139
|
-
doctorPanel,
|
|
140
|
-
doctorHuman,
|
|
141
|
-
};
|
package/core/adapter/render.js
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// cohorte — runtime adapter.
|
|
3
|
-
//
|
|
4
|
-
// The core prompts (core/commands/*.md, core/agents/*.md) are the SINGLE source of
|
|
5
|
-
// truth and are written runtime-neutral. This module transpiles them into whatever
|
|
6
|
-
// the target coding agent actually reads:
|
|
7
|
-
//
|
|
8
|
-
// commands agents gate hook
|
|
9
|
-
// claude .claude/commands/<n>.md md, $ARGUMENTS md PreToolUse (deny+ask)
|
|
10
|
-
// codex .agents/skills/<n>/SKILL.md md, no substitution toml PreToolUse (deny only)
|
|
11
|
-
// cursor .cursor/commands/<n>.md md, no substitution md beforeShellExecution
|
|
12
|
-
// gemini .gemini/commands/<n>.toml toml, {{args}} md BeforeTool (deny only)
|
|
13
|
-
// opencode .opencode/commands/<n>.md md, $ARGUMENTS md none — advisory --check
|
|
14
|
-
//
|
|
15
|
-
// Three transforms, in order:
|
|
16
|
-
//
|
|
17
|
-
// 1. capability conditionals — `<!-- cohorte:if hooks -->…<!-- cohorte:else -->…
|
|
18
|
-
// <!-- cohorte:endif -->` keeps exactly one branch, so one file can state both "the gate
|
|
19
|
-
// fires whether or not you cooperate" and "you must call the gate yourself".
|
|
20
|
-
// 2. runtime preamble — one block resolving `<core>`, `<agents>`, the dispatch verb and
|
|
21
|
-
// the gate mechanism, so the prose below never hardcodes a runtime's layout.
|
|
22
|
-
// 3. surface encoding — frontmatter filtered to the keys that runtime understands (an
|
|
23
|
-
// unknown key is prose the model reads as an instruction, so dropping is not cosmetic),
|
|
24
|
-
// arg placeholder swapped, and the whole thing re-emitted as md, toml or a skill dir.
|
|
25
|
-
//
|
|
26
|
-
// Subagents are NOT one of the conditionals: they are a precondition (see assertSupported).
|
|
27
|
-
//
|
|
28
|
-
// Claude output is byte-identical to the pre-adapter core when a file has no conditionals:
|
|
29
|
-
// that is the regression test (scripts/test-adapter.mjs).
|
|
30
|
-
|
|
31
|
-
'use strict';
|
|
32
|
-
|
|
33
|
-
const fs = require('fs');
|
|
34
|
-
const os = require('os');
|
|
35
|
-
const path = require('path');
|
|
36
|
-
|
|
37
|
-
const RUNTIME_DIR = path.join(__dirname, '..', 'runtimes');
|
|
38
|
-
|
|
39
|
-
// --- registry ----------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
function listRuntimes() {
|
|
42
|
-
return fs.readdirSync(RUNTIME_DIR)
|
|
43
|
-
.filter((f) => f.endsWith('.json'))
|
|
44
|
-
.map((f) => f.slice(0, -5))
|
|
45
|
-
.sort();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function loadRuntime(id) {
|
|
49
|
-
const file = path.join(RUNTIME_DIR, `${id}.json`);
|
|
50
|
-
if (!fs.existsSync(file)) {
|
|
51
|
-
throw new Error(`unknown runtime "${id}" (known: ${listRuntimes().join(', ')})`);
|
|
52
|
-
}
|
|
53
|
-
const rt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
54
|
-
assertSupported(rt);
|
|
55
|
-
return rt;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Subagents are a HARD requirement, not a capability to branch on. The pipeline's premise is
|
|
59
|
-
// that each surface is built by someone who can only see the frozen contract; without real
|
|
60
|
-
// subagents the lead does every surface in one context and that isolation is simply gone.
|
|
61
|
-
// A sequential-persona fallback existed briefly and was removed: it asked the lead to simulate
|
|
62
|
-
// the boundary by discipline, which is not the same guarantee, and no supported runtime ever
|
|
63
|
-
// took that branch. Refuse loudly rather than render a pipeline whose central promise is absent.
|
|
64
|
-
function assertSupported(rt) {
|
|
65
|
-
if (rt.capabilities && rt.capabilities.subagents === false) {
|
|
66
|
-
throw new Error(`runtime "${rt.id}" declares no subagents — cohorte requires them `
|
|
67
|
-
+ '(see docs/reference/runtimes.md §Requirements)');
|
|
68
|
-
}
|
|
69
|
-
return rt;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function expandHome(p) {
|
|
73
|
-
if (!p) return p;
|
|
74
|
-
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Which runtimes are actually installed on this machine. Detection is by config
|
|
78
|
-
// directory, not by binary on PATH: Cursor ships no CLI, and a runtime the human
|
|
79
|
-
// has configured but not opened this shell for is still a legitimate target.
|
|
80
|
-
function detectRuntimes() {
|
|
81
|
-
return listRuntimes().filter((id) => {
|
|
82
|
-
const rt = loadRuntime(id);
|
|
83
|
-
return (rt.detect || []).some((d) => fs.existsSync(expandHome(d)));
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Absolute destinations for one (runtime, scope) pair. `projectRoot` anchors every
|
|
88
|
-
// relative path; `~`-rooted ones ignore it. A scope entry may pin an individual
|
|
89
|
-
// surface to another scope (Codex reads prompts ONLY from ~/.codex, even when the
|
|
90
|
-
// neutral core is bundled in the repo) via `<key>_scope`.
|
|
91
|
-
function resolvePaths(runtime, scope, projectRoot, { overrides } = {}) {
|
|
92
|
-
const spec = runtime.scopes[scope];
|
|
93
|
-
if (!spec) throw new Error(`runtime ${runtime.id} has no "${scope}" scope`);
|
|
94
|
-
// `overrides` re-roots a declared prefix — the one caller is CLAUDE_CONFIG_DIR, which moves
|
|
95
|
-
// Claude Code's whole `~/.claude` tree elsewhere (a desktop host points it at
|
|
96
|
-
// `~/Library/Application Support/…`). Ignoring it split the install in half: the core was
|
|
97
|
-
// written to `~/.claude` while the hook was registered in the overridden dir, so a scratch
|
|
98
|
-
// install silently wrote into the user's real global core.
|
|
99
|
-
const reroot = (p) => {
|
|
100
|
-
for (const [from, to] of Object.entries(overrides || {})) {
|
|
101
|
-
if (p === from) return to;
|
|
102
|
-
if (p.startsWith(from + '/')) return to + p.slice(from.length);
|
|
103
|
-
}
|
|
104
|
-
return p;
|
|
105
|
-
};
|
|
106
|
-
const abs = (p) => {
|
|
107
|
-
if (!p) return null;
|
|
108
|
-
const e = expandHome(reroot(p));
|
|
109
|
-
return path.isAbsolute(e) ? e : path.join(projectRoot, e);
|
|
110
|
-
};
|
|
111
|
-
const out = { scope, effective: {} };
|
|
112
|
-
for (const key of ['root', 'core', 'commands', 'agents', 'hooks', 'hooks_config',
|
|
113
|
-
'workflows', 'settings']) {
|
|
114
|
-
out[key] = abs(spec[key]);
|
|
115
|
-
out.effective[key] = spec[`${key}_scope`] || scope;
|
|
116
|
-
}
|
|
117
|
-
return out;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// --- 1. capability conditionals ----------------------------------------------
|
|
121
|
-
|
|
122
|
-
const IF = /^\s*<!--\s*cohorte:if\s+([^>]+?)\s*-->\s*$/;
|
|
123
|
-
const ELSE = /^\s*<!--\s*cohorte:else\s*-->\s*$/;
|
|
124
|
-
const ENDIF = /^\s*<!--\s*cohorte:endif\s*-->\s*$/;
|
|
125
|
-
|
|
126
|
-
// A condition is a space-separated OR of terms; a term is `cap`, `!cap`, `runtime:<id>`
|
|
127
|
-
// or `!runtime:<id>`. OR (not AND) because every real use is "this family of runtimes".
|
|
128
|
-
function testCondition(expr, runtime) {
|
|
129
|
-
return expr.split(/\s+/).filter(Boolean).some((term) => {
|
|
130
|
-
const neg = term.startsWith('!');
|
|
131
|
-
const name = neg ? term.slice(1) : term;
|
|
132
|
-
let value;
|
|
133
|
-
if (name.startsWith('runtime:')) value = runtime.id === name.slice(8);
|
|
134
|
-
else if (name in (runtime.capabilities || {})) value = !!runtime.capabilities[name];
|
|
135
|
-
else throw new Error(`unknown cohorte:if term "${name}"`);
|
|
136
|
-
return neg ? !value : value;
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function applyConditionals(text, runtime) {
|
|
141
|
-
const lines = text.split('\n');
|
|
142
|
-
const out = [];
|
|
143
|
-
// Each frame: {keep} — whether the branch currently being read survives. Nesting is
|
|
144
|
-
// supported so a hooks-branch can carry a subagents-branch inside it.
|
|
145
|
-
const stack = [];
|
|
146
|
-
const emitting = () => stack.every((f) => f.keep);
|
|
147
|
-
for (const line of lines) {
|
|
148
|
-
let m;
|
|
149
|
-
if ((m = line.match(IF))) {
|
|
150
|
-
const taken = testCondition(m[1], runtime);
|
|
151
|
-
stack.push({ taken, keep: taken, seenElse: false });
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (ELSE.test(line)) {
|
|
155
|
-
const frame = stack[stack.length - 1];
|
|
156
|
-
if (!frame) throw new Error('cohorte:else without a matching cohorte:if');
|
|
157
|
-
if (frame.seenElse) throw new Error('two cohorte:else in one cohorte:if');
|
|
158
|
-
frame.seenElse = true;
|
|
159
|
-
frame.keep = !frame.taken;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
if (ENDIF.test(line)) {
|
|
163
|
-
if (!stack.pop()) throw new Error('cohorte:endif without a matching cohorte:if');
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (emitting()) out.push(line);
|
|
167
|
-
}
|
|
168
|
-
if (stack.length) throw new Error('unclosed cohorte:if');
|
|
169
|
-
return out.join('\n');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// --- 2. runtime preamble ------------------------------------------------------
|
|
173
|
-
|
|
174
|
-
// Path as the human/model should see it: `~/…` reads better than an expanded homedir,
|
|
175
|
-
// and a project-relative path must stay relative (the agent's cwd is the repo).
|
|
176
|
-
function displayPath(p, projectRoot) {
|
|
177
|
-
if (!p) return null;
|
|
178
|
-
const home = os.homedir();
|
|
179
|
-
if (p.startsWith(home + path.sep)) return '~/' + path.relative(home, p).split(path.sep).join('/');
|
|
180
|
-
if (p.startsWith(projectRoot + path.sep)) return path.relative(projectRoot, p).split(path.sep).join('/');
|
|
181
|
-
return p;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// The per-project directory holding everything the pipeline GENERATES for this repo
|
|
185
|
-
// (gate-config.json, preflight.ok, pipeline-metrics.jsonl, pipeline.json). Distinct from
|
|
186
|
-
// `<core>`: the core can be global while this is always in the repo, next to the code it
|
|
187
|
-
// describes. `.claude` on a Claude install — unchanged, so existing repos keep working.
|
|
188
|
-
//
|
|
189
|
-
// Deliberately SHARED across non-Claude runtimes (`.cohorte`), while each of their cores is
|
|
190
|
-
// its own subdirectory (`.cohorte/<id>`). The state describes the PROJECT — one gate config,
|
|
191
|
-
// one preflight stamp, one metrics log — and duplicating it per runtime would let a repo
|
|
192
|
-
// driven from two agents disagree with itself about what is gated and what is verified.
|
|
193
|
-
// The core, by contrast, is rendered per runtime and cannot be shared: the same template
|
|
194
|
-
// resolves differently depending on whether that agent has subagents or hooks.
|
|
195
|
-
function stateDir(runtime) {
|
|
196
|
-
return runtime.id === 'claude' ? '.claude' : '.cohorte';
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// The user-level config (kanban boards, shared vault). One file per
|
|
200
|
-
// human, not per project or per runtime; the shipped scripts probe the same two paths.
|
|
201
|
-
function configPath(runtime) {
|
|
202
|
-
return runtime.id === 'claude' ? '~/.claude/cohorte.config.yaml' : '~/.cohorte/cohorte.config.yaml';
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function preamble(runtime, paths, projectRoot, { kind = 'command' } = {}) {
|
|
206
|
-
const caps = runtime.capabilities || {};
|
|
207
|
-
const core = displayPath(paths.core, projectRoot);
|
|
208
|
-
const agentsDir = paths.agents ? displayPath(paths.agents, projectRoot) : `${core}/agents`;
|
|
209
|
-
const L = [];
|
|
210
|
-
L.push(`> **Runtime: ${runtime.label}.** Generated by the cohorte adapter — do not edit this file;`);
|
|
211
|
-
L.push(`> edit \`core/${kind === 'agent' ? 'agents' : 'commands'}/\` in the cohorte source and re-install.`);
|
|
212
|
-
L.push('>');
|
|
213
|
-
L.push(`> - \`<core>\` = \`${core}\` — the pipeline's shared assets (\`pipeline/scripts/\`, \`pipeline/SCHEMA.md\`, \`templates/\`). Every \`<core>/…\` path below resolves there, and nowhere else.`);
|
|
214
|
-
L.push(`> - \`<state>\` = \`${stateDir(runtime)}/\` in **this repo** — what the pipeline generates for this project (\`gate-config.json\`, \`preflight.ok\`, \`pipeline-metrics.jsonl\`, \`pipeline.json\`). Always project-relative, even when \`<core>\` is global.`);
|
|
215
|
-
L.push(`> - \`<memory>\` = \`${runtime.memory}\` — this runtime's project-instructions file at the repo root, loaded into every session here. Where the doctrine says to reference or extend it, that is the file.`);
|
|
216
|
-
L.push(`> - \`<config>\` = \`${configPath(runtime)}\` — your user-level config (kanban boards, shared vault). One per human, never committed.`);
|
|
217
|
-
|
|
218
|
-
L.push(`> - \`<agents>\` = \`${agentsDir}\` — real subagents. Dispatch: ${runtime.agent.dispatch}.`);
|
|
219
|
-
|
|
220
|
-
if (caps.hooks) {
|
|
221
|
-
const cfg = displayPath(paths.hooks_config, projectRoot);
|
|
222
|
-
const ask = runtime.hook.supports_ask
|
|
223
|
-
? 'It can deny outright or ask you to confirm.'
|
|
224
|
-
: 'This runtime has **no confirmation tier**, so a command that would merely be queried elsewhere is **denied** here — re-run it yourself if you meant it.';
|
|
225
|
-
L.push(`> - **Gate** — \`<core>/hooks/gate.py\` is registered as a blocking \`${runtime.hook.event}\` hook in \`${cfg}\`, reading \`<state>/gate-config.json\`. It fires whether or not you cooperate. ${ask}`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (!caps.hooks) {
|
|
229
|
-
L.push('> - **Gate** — this runtime has no blocking hook, so the gate is not automatic. Before any command listed in the profile\'s `gate` block, and before every phase dispatch, run `<core>/hooks/gate.py --check <command>` yourself and obey its verdict (`deny` ⇒ stop, `ask` ⇒ get the human\'s explicit go-ahead). Skipping this is the one deviation that silently removes a safety property.');
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (!caps.workflows) {
|
|
233
|
-
L.push('> - **Workflows** — unavailable on this runtime. The conversational path below is the only path; ignore any mention of a workflow variant.');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (!runtime.command.args) {
|
|
237
|
-
L.push(`> - \`$ARGUMENTS\` — this runtime does not substitute placeholders. It means ${runtime.command.args_note || 'the text typed after the command name'}; expand it yourself everywhere it appears below.`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return L.join('\n') + '\n';
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// --- 3. surface encoding ------------------------------------------------------
|
|
244
|
-
|
|
245
|
-
function parseFrontmatter(text) {
|
|
246
|
-
if (!text.startsWith('---\n')) return { keys: [], body: text };
|
|
247
|
-
const end = text.indexOf('\n---\n', 3);
|
|
248
|
-
if (end === -1) return { keys: [], body: text };
|
|
249
|
-
const block = text.slice(4, end + 1);
|
|
250
|
-
const body = text.slice(end + 5);
|
|
251
|
-
const keys = [];
|
|
252
|
-
for (const line of block.split('\n')) {
|
|
253
|
-
const m = line.match(/^([A-Za-z][\w-]*):\s?(.*)$/);
|
|
254
|
-
// Continuation lines (a wrapped value) belong to the previous key.
|
|
255
|
-
if (!m) {
|
|
256
|
-
if (line.trim() && keys.length) keys[keys.length - 1][1] += '\n' + line;
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
keys.push([m[1], m[2]]);
|
|
260
|
-
}
|
|
261
|
-
return { keys, body };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function emitFrontmatter(keys) {
|
|
265
|
-
if (!keys.length) return '';
|
|
266
|
-
return '---\n' + keys.map(([k, v]) => `${k}: ${v}`).join('\n') + '\n---\n\n';
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function substituteArgs(text, runtime) {
|
|
270
|
-
const token = runtime.command.args;
|
|
271
|
-
if (!token || token === '$ARGUMENTS') return text;
|
|
272
|
-
return text.split('$ARGUMENTS').join(token);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// TOML basic-string escaping for the multi-line ''' form Gemini expects. A literal
|
|
276
|
-
// ''' inside a prompt would close the string early, so it is the one sequence broken up.
|
|
277
|
-
function tomlMultiline(s) {
|
|
278
|
-
return "'''\n" + s.replace(/'''/g, "''\\'") + "\n'''";
|
|
279
|
-
}
|
|
280
|
-
function tomlBasic(s) {
|
|
281
|
-
return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ') + '"';
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Render one core command for one runtime.
|
|
286
|
-
* @returns {{filename: string, content: string, dir: 'commands'}}
|
|
287
|
-
*/
|
|
288
|
-
function renderCommand({ source, name, runtime, paths, projectRoot }) {
|
|
289
|
-
const { keys, body } = parseFrontmatter(source);
|
|
290
|
-
let out = applyConditionals(body, runtime);
|
|
291
|
-
out = preamble(runtime, paths, projectRoot) + '\n' + out.replace(/^\n+/, '');
|
|
292
|
-
out = substituteArgs(out, runtime);
|
|
293
|
-
|
|
294
|
-
const kept = keys.filter(([k]) => runtime.command.frontmatter.includes(k));
|
|
295
|
-
const dropped = keys.filter(([k]) => !runtime.command.frontmatter.includes(k)).map(([k]) => k);
|
|
296
|
-
|
|
297
|
-
// A skill is a DIRECTORY — `<name>/SKILL.md` — whose frontmatter must carry `name`, since
|
|
298
|
-
// that is what the runtime matches on (both for `$<name>` and for implicit selection from
|
|
299
|
-
// `description`). Command sources have no `name` key, so synthesise it from the filename.
|
|
300
|
-
if (runtime.command.format === 'skill') {
|
|
301
|
-
if (!kept.some(([k]) => k === 'name')) kept.unshift(['name', name]);
|
|
302
|
-
return { filename: `${name}${runtime.command.ext}`, content: emitFrontmatter(kept) + out, dropped };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (runtime.command.format === 'toml') {
|
|
306
|
-
const desc = (keys.find(([k]) => k === 'description') || [null, ''])[1];
|
|
307
|
-
const lines = [`# cohorte — generated for ${runtime.label}. Do not edit; edit core/commands/${name}.md.`];
|
|
308
|
-
if (dropped.length) lines.push(`# frontmatter not supported here, dropped: ${dropped.join(', ')}`);
|
|
309
|
-
if (desc) lines.push(`description = ${tomlBasic(desc)}`);
|
|
310
|
-
lines.push(`prompt = ${tomlMultiline(substituteArgs(out, runtime))}`);
|
|
311
|
-
return { filename: `${name}${runtime.command.ext}`, content: lines.join('\n') + '\n', dropped };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const header = emitFrontmatter(kept);
|
|
315
|
-
return { filename: `${name}${runtime.command.ext}`, content: header + out, dropped };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Is this agent read-only? Derived from the source's Claude-style `tools:` list rather than
|
|
319
|
-
// declared twice: an agent that was never given Write, Edit or Bash is read-only by intent,
|
|
320
|
-
// and each runtime spells that its own way (`readonly: true`, `sandbox_mode = "read-only"`).
|
|
321
|
-
// Losing it silently would hand the reviewer the ability to fix what it is meant to report.
|
|
322
|
-
function isReadOnly(keys) {
|
|
323
|
-
const tools = (keys.find(([k]) => k === 'tools') || [])[1];
|
|
324
|
-
if (!tools) return false;
|
|
325
|
-
return !/\b(Write|Edit|MultiEdit|Bash|NotebookEdit)\b/.test(tools);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Render one core agent for one runtime.
|
|
330
|
-
*
|
|
331
|
-
* Every runtime cohorte targets has real subagents, but they disagree on the file: markdown +
|
|
332
|
-
* frontmatter for Claude Code, Cursor, Gemini CLI and OpenCode; TOML with the body under
|
|
333
|
-
* `developer_instructions` for Codex. `format: "persona"` remains for a runtime with no
|
|
334
|
-
* subagents at all — the file is then read and adopted by the lead in sequence.
|
|
335
|
-
*/
|
|
336
|
-
function renderAgent({ source, name, runtime, paths, projectRoot }) {
|
|
337
|
-
const { keys, body } = parseFrontmatter(source);
|
|
338
|
-
const out = preamble(runtime, paths, projectRoot, { kind: 'agent' }) + '\n'
|
|
339
|
-
+ applyConditionals(body, runtime).replace(/^\n+/, '');
|
|
340
|
-
const spec = runtime.agent;
|
|
341
|
-
const readonly = isReadOnly(keys);
|
|
342
|
-
|
|
343
|
-
if (spec.format === 'toml') {
|
|
344
|
-
const get = (k) => (keys.find(([kk]) => kk === k) || [null, ''])[1];
|
|
345
|
-
const lines = [`# cohorte — generated for ${runtime.label}. Do not edit; edit core/agents/${name}.md.`];
|
|
346
|
-
lines.push(`name = ${tomlBasic(get('name') || name)}`);
|
|
347
|
-
if (get('description')) lines.push(`description = ${tomlBasic(get('description'))}`);
|
|
348
|
-
if (spec.frontmatter.includes('model') && get('model')) lines.push(`model = ${tomlBasic(get('model'))}`);
|
|
349
|
-
if (readonly && spec.readonly_key) {
|
|
350
|
-
lines.push(`${spec.readonly_key} = ${tomlBasic(spec.readonly_value)}`);
|
|
351
|
-
}
|
|
352
|
-
lines.push(`${spec.body_key} = ${tomlMultiline(out)}`);
|
|
353
|
-
return { filename: `${name}${spec.ext}`, content: lines.join('\n') + '\n', native: true, readonly };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (spec.format !== 'md') {
|
|
357
|
-
return { filename: `${name}.md`, content: out, native: false, dropped: keys.map(([k]) => k) };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const allowed = spec.frontmatter;
|
|
361
|
-
const kept = keys.filter(([k]) => allowed.includes(k));
|
|
362
|
-
for (const [k, v] of Object.entries(spec.defaults || {})) {
|
|
363
|
-
if (!kept.some(([kk]) => kk === k)) kept.push([k, v]);
|
|
364
|
-
}
|
|
365
|
-
if (readonly && spec.readonly_key && !kept.some(([k]) => k === spec.readonly_key)) {
|
|
366
|
-
kept.push([spec.readonly_key, spec.readonly_value]);
|
|
367
|
-
}
|
|
368
|
-
const dropped = keys.filter(([k]) => !allowed.includes(k)).map(([k]) => k);
|
|
369
|
-
return { filename: `${name}${spec.ext || '.md'}`, content: emitFrontmatter(kept) + out, native: true, dropped, readonly };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
module.exports = {
|
|
373
|
-
assertSupported,
|
|
374
|
-
stateDir,
|
|
375
|
-
configPath,
|
|
376
|
-
listRuntimes,
|
|
377
|
-
loadRuntime,
|
|
378
|
-
detectRuntimes,
|
|
379
|
-
resolvePaths,
|
|
380
|
-
expandHome,
|
|
381
|
-
displayPath,
|
|
382
|
-
applyConditionals,
|
|
383
|
-
testCondition,
|
|
384
|
-
parseFrontmatter,
|
|
385
|
-
emitFrontmatter,
|
|
386
|
-
preamble,
|
|
387
|
-
renderCommand,
|
|
388
|
-
renderAgent,
|
|
389
|
-
};
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: <SURFACE_AGENT>
|
|
3
|
-
description: Implements the <SURFACE_LABEL> surface (<SURFACE_PATH>) for one feature, strictly from the frozen spec + contract, test-first TDD. Dispatched by /cohorte-build. Touches only its own surface.
|
|
4
|
-
tools: <SURFACE_TOOLS>
|
|
5
|
-
model: <SURFACE_MODEL>
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are the **<SURFACE_AGENT>** engineer for one feature of **<PROJECT_NAME>**. You work alone,
|
|
9
|
-
statelessly, from the spec you are given. You cannot talk to the other surface agents — your only
|
|
10
|
-
shared surface is the frozen contract and the spec.
|
|
11
|
-
|
|
12
|
-
> **First action, always:** read `PIPELINE.md`'s fenced `yaml pipeline-profile` block ONLY — the
|
|
13
|
-
> machine contract (surfaces, contract, commands, gate). Do **not** read the prose sections
|
|
14
|
-
> (§Conventions/§Testing): your slice of them is baked into this file below (§Your conventions),
|
|
15
|
-
> rendered from the profile — re-reading the prose every dispatch is exactly the cost the bake
|
|
16
|
-
> removes. If the baked slice visibly contradicts `PIPELINE.md`, say so in your handoff: the profile
|
|
17
|
-
> wins, and this agent file needs a re-render (`/cohorte-update-pipeline`).
|
|
18
|
-
|
|
19
|
-
## You own
|
|
20
|
-
|
|
21
|
-
`<SURFACE_PATH>/**` only. Everything under it — and nothing outside it.
|
|
22
|
-
|
|
23
|
-
## Your conventions (baked from `PIPELINE.md` at render time)
|
|
24
|
-
|
|
25
|
-
<!-- Rendered by /cohorte-init-pipeline (and refreshed by /cohorte-update-pipeline's reconcile) from
|
|
26
|
-
§Conventions `### Shared` + `### Surface: <your key>` + your §Testing lines.
|
|
27
|
-
Edit conventions in PIPELINE.md, never here — this block is regenerated. -->
|
|
28
|
-
|
|
29
|
-
<SURFACE_CONVENTIONS>
|
|
30
|
-
|
|
31
|
-
## You must NEVER
|
|
32
|
-
|
|
33
|
-
- Touch any other surface's tree (see the `surfaces` list in `PIPELINE.md`). That's another agent's.
|
|
34
|
-
- Edit the frozen **contract** (`contract.path` in `PIPELINE.md`). It is authored by the lead; import
|
|
35
|
-
from it read-only. If you believe the contract is wrong, **stop and report it** in your handoff — do
|
|
36
|
-
not change it.
|
|
37
|
-
- Run any command in `PIPELINE.md` §`gate.deny` (destructive DB / history rewrites). Migrations (if any)
|
|
38
|
-
are **append-only** — never `fresh`/`reset`/`rollback`. The DB and ports may be shared across worktrees.
|
|
39
|
-
<SURFACE_EXTRA_NEVER>
|
|
40
|
-
|
|
41
|
-
## Your inputs (supplied at dispatch — you have no memory)
|
|
42
|
-
|
|
43
|
-
1. The spec path `specs/<id>.md` — on a **first build** (your dispatch's Remediation slot says
|
|
44
|
-
`none`), read it fully (contract §5, your surface's tasks, acceptance §9). On a **fix loop**, do
|
|
45
|
-
NOT re-read the spec: your dispatch carries your open Remediation items verbatim, and the contract
|
|
46
|
-
file (input 2) is your only source of shapes — open the spec only if a finding explicitly cites a
|
|
47
|
-
spec section, or if `contract.enabled` is false in `PIPELINE.md` (then spec §5 prose IS the contract).
|
|
48
|
-
2. The frozen contract for this feature (`<contract.path>/<id>.<contract.ext>`) — the shapes you build against.
|
|
49
|
-
3. On a fix loop: the findings in your dispatch are **self-contained** (`file:line` · concrete fix).
|
|
50
|
-
Read only the files they name — don't re-explore your whole tree. Need the current state of your
|
|
51
|
-
work? Compute it yourself: `git diff <default_branch> -- <your surface path>` (never expect a diff
|
|
52
|
-
in your dispatch). Fix exactly what's flagged.
|
|
53
|
-
<SURFACE_DESIGN_INPUT>
|
|
54
|
-
|
|
55
|
-
## How you read code — retrieval first
|
|
56
|
-
|
|
57
|
-
If `retrieval.provider` in `PIPELINE.md` is not `none`, its MCP tools are in your toolset — **prefer
|
|
58
|
-
them over Grep/Glob + whole-file Reads**: locate code by symbol, read only the definitions you need,
|
|
59
|
-
and trace references before changing any shared shape. Fall back to Grep/Read only when the retrieval
|
|
60
|
-
tools are unavailable or come up empty.
|
|
61
|
-
|
|
62
|
-
## How you choose what to write — the minimality ladder
|
|
63
|
-
|
|
64
|
-
The spec froze the **what**; this ladder governs only the **how**. It never licenses you to skip a
|
|
65
|
-
contract field, an acceptance criterion, a test, a validation, an authz check or an accessibility
|
|
66
|
-
attribute — those are the *what*, and they are not yours to trim.
|
|
67
|
-
|
|
68
|
-
Before writing any helper, utility, wrapper, abstraction or new dependency, walk down and stop at the
|
|
69
|
-
first hit:
|
|
70
|
-
|
|
71
|
-
1. **Does it need to exist at all?** An abstraction with one implementation, a config nobody sets, a
|
|
72
|
-
layer with one caller — don't write it. The second caller is when it earns its keep.
|
|
73
|
-
2. **Is it already in this repo?** One retrieval/Grep lookup by symbol name, not a survey — you are
|
|
74
|
-
checking, not exploring. Reuse beats re-implementing, and it keeps the convention.
|
|
75
|
-
3. **Is it in the standard library / framework?** Name it and use it.
|
|
76
|
-
4. **Is it a native platform feature?** (CSS, the HTTP layer, the DB, the runtime.) Prefer it over code.
|
|
77
|
-
5. **Is it in a dependency already installed?** Use that one. Adding a dependency for what tiers 3–5
|
|
78
|
-
already ship is a finding at review.
|
|
79
|
-
6. **Can it be a few lines inline?** Then it doesn't need a file, a class, or a name.
|
|
80
|
-
7. Only then: the **minimum implementation that satisfies the contract** — no speculative options, no
|
|
81
|
-
"we'll probably need" parameters, no premature generalisation.
|
|
82
|
-
|
|
83
|
-
Bound the cost: this is at most **one lookup per candidate**, and it applies to code you are inventing —
|
|
84
|
-
never to code the contract dictates. If a step would cost more searching than writing, write it.
|
|
85
|
-
|
|
86
|
-
Something you deliberately kept simple with a known ceiling goes in your handoff `## TODO / not done`
|
|
87
|
-
with its limit and what would trigger the upgrade — not in a comment, and not silently.
|
|
88
|
-
|
|
89
|
-
## How you work — strict TDD (red → green → refactor)
|
|
90
|
-
|
|
91
|
-
<!-- <SURFACE_TDD_STEP1> is a LEAD-IN paragraph, not a numbered item: it is filled only for a
|
|
92
|
-
`uses_design` surface (the design-pull step) and renders as nothing otherwise. As a numbered
|
|
93
|
-
item it left every non-design agent with a blank "1." above the real first step. -->
|
|
94
|
-
|
|
95
|
-
<SURFACE_TDD_STEP1>
|
|
96
|
-
|
|
97
|
-
1. **Write the failing test(s) first** from the frozen contract. Cover exactly what your baked
|
|
98
|
-
Testing rules (§Your conventions) prescribe. Run the test command and watch it fail (red).
|
|
99
|
-
2. Implement until green, following your baked conventions.
|
|
100
|
-
3. Refactor to the conventions. Keep tests green.
|
|
101
|
-
4. **Lint + format before handoff:** run your surface's lint and fix every issue. If the project
|
|
102
|
-
registers a format-on-write hook (Claude Code: `PostToolUse` in `settings.json`), your files are
|
|
103
|
-
already formatted on every write — skip `format_cmd`; otherwise run it too. Code you hand off must
|
|
104
|
-
be lint-clean and formatted.
|
|
105
|
-
|
|
106
|
-
**Run commands bridled — always.** Your surface's `test_quiet_cmd`/`lint_quiet_cmd` in `PIPELINE.md`
|
|
107
|
-
are the forms you execute (dot reporter / failures-only); when a quiet variant is empty or absent,
|
|
108
|
-
run `<full cmd> 2>&1 | tail -40`. Never print a full runner log into your context — redirect to a
|
|
109
|
-
file and grep it if you need more than the tail.
|
|
110
|
-
|
|
111
|
-
## Definition of done
|
|
112
|
-
|
|
113
|
-
Your surface's `test_cmd` green, `lint_cmd` clean, `typecheck_cmd` clean for your code, and every part
|
|
114
|
-
of the contract your surface implements matches the spec exactly. User-facing copy in `ui_language`.
|
|
115
|
-
|
|
116
|
-
## Your return — the HANDOFF, exactly this shape
|
|
117
|
-
|
|
118
|
-
Your final message **is** the handoff (read by the lead, not a human chat). Keep it tight — the lead
|
|
119
|
-
only acts on mismatches, test failures, remediation ticks, and TODOs; never list files one by one
|
|
120
|
-
(the lead has `git diff --stat`), never paste code excerpts (the code is on disk):
|
|
121
|
-
|
|
122
|
-
```
|
|
123
|
-
# HANDOFF — <surface> · <feature_id>
|
|
124
|
-
|
|
125
|
-
## Summary
|
|
126
|
-
<2–4 lines: what you built and the approach>
|
|
127
|
-
|
|
128
|
-
## Migrations / schema (only if any)
|
|
129
|
-
- <name> — <additive change>
|
|
130
|
-
|
|
131
|
-
## Tests
|
|
132
|
-
- Run: <your test_cmd> · result: <pass/fail + counts>
|
|
133
|
-
|
|
134
|
-
## Contract mismatches / assumptions
|
|
135
|
-
<none, or describe — NEVER edit the contract; report here instead>
|
|
136
|
-
|
|
137
|
-
## Remediation addressed (fix loops only)
|
|
138
|
-
- <items fixed, by file:line>
|
|
139
|
-
|
|
140
|
-
## TODO / not done
|
|
141
|
-
- <deferred, blocked, or out of scope — or "none">
|
|
142
|
-
```
|