cohorte 2.10.0 → 3.0.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/README.md +7 -0
  2. package/assets/manifest.json +193 -0
  3. package/assets/migrations/config/README.md +12 -0
  4. package/assets/migrations/state/0001_init.sql +154 -0
  5. package/assets/prompts/README.md +11 -0
  6. package/assets/prompts/agents/fixer.md +7 -0
  7. package/assets/prompts/agents/implementer.md +12 -0
  8. package/assets/prompts/agents/reviewer.md +8 -0
  9. package/assets/prompts/agents/security-reviewer.md +7 -0
  10. package/assets/prompts/phases/build.md +5 -0
  11. package/assets/prompts/phases/review.md +5 -0
  12. package/assets/prompts/phases/test.md +5 -0
  13. package/assets/prompts/system.md +11 -0
  14. package/assets/schemas/agent-output.schema.json +225 -0
  15. package/assets/schemas/auth-status.schema.json +87 -0
  16. package/assets/schemas/command-result.schema.json +192 -0
  17. package/assets/schemas/commands.schema.json +952 -0
  18. package/assets/schemas/config.schema.json +2259 -0
  19. package/assets/schemas/doctor-report.schema.json +104 -0
  20. package/assets/schemas/events.schema.json +8514 -0
  21. package/assets/schemas/fake-script.schema.json +549 -0
  22. package/assets/schemas/inspect.schema.json +3133 -0
  23. package/assets/schemas/manifest.schema.json +79 -0
  24. package/assets/schemas/ownership.schema.json +52 -0
  25. package/assets/schemas/policy-verdict.schema.json +282 -0
  26. package/assets/schemas/project-model.schema.json +783 -0
  27. package/assets/schemas/project-status.schema.json +217 -0
  28. package/assets/schemas/reconcile-plan.schema.json +155 -0
  29. package/assets/schemas/run-diff.schema.json +177 -0
  30. package/assets/schemas/run-snapshot-manifest.schema.json +2104 -0
  31. package/assets/schemas/run-state.schema.json +1755 -0
  32. package/assets/schemas/runtime-capabilities.schema.json +1545 -0
  33. package/assets/schemas/sandbox-capabilities.schema.json +116 -0
  34. package/assets/schemas/skill.schema.json +87 -0
  35. package/assets/schemas/spec.schema.json +202 -0
  36. package/assets/schemas/tool-catalogue.schema.json +655 -0
  37. package/assets/schemas/trust-record.schema.json +38 -0
  38. package/assets/skills/README.md +9 -0
  39. package/assets/skills/inspect-surface.md +4 -0
  40. package/dist/agent-host.mjs +50 -0
  41. package/dist/agent-host.mjs.map +1 -0
  42. package/dist/bundle-manifest.json +102 -0
  43. package/dist/chunks/__host-DgGPyco9.mjs +18 -0
  44. package/dist/chunks/__host-DgGPyco9.mjs.map +1 -0
  45. package/dist/chunks/approve-Cv135UqI.mjs +19 -0
  46. package/dist/chunks/approve-Cv135UqI.mjs.map +1 -0
  47. package/dist/chunks/auth-BT5oVp76.mjs +132 -0
  48. package/dist/chunks/auth-BT5oVp76.mjs.map +1 -0
  49. package/dist/chunks/auth-D8rCVY33.mjs +84 -0
  50. package/dist/chunks/auth-D8rCVY33.mjs.map +1 -0
  51. package/dist/chunks/brainstorm-CU10T1tm.mjs +7 -0
  52. package/dist/chunks/brainstorm-CU10T1tm.mjs.map +1 -0
  53. package/dist/chunks/cancel-KV_cpsqS.mjs +15 -0
  54. package/dist/chunks/cancel-KV_cpsqS.mjs.map +1 -0
  55. package/dist/chunks/canonical-C8UxeEmB.mjs +57 -0
  56. package/dist/chunks/canonical-C8UxeEmB.mjs.map +1 -0
  57. package/dist/chunks/catalogue-BvwSrM_j.mjs +1549 -0
  58. package/dist/chunks/catalogue-BvwSrM_j.mjs.map +1 -0
  59. package/dist/chunks/compose-Cr0hP0qm.mjs +7303 -0
  60. package/dist/chunks/compose-Cr0hP0qm.mjs.map +1 -0
  61. package/dist/chunks/config-DefHA2Lw.mjs +96 -0
  62. package/dist/chunks/config-DefHA2Lw.mjs.map +1 -0
  63. package/dist/chunks/contract-B0ra2-bm.mjs +295 -0
  64. package/dist/chunks/contract-B0ra2-bm.mjs.map +1 -0
  65. package/dist/chunks/contract-vYbwYr0Y.mjs +123 -0
  66. package/dist/chunks/contract-vYbwYr0Y.mjs.map +1 -0
  67. package/dist/chunks/deny-ClTxSvHJ.mjs +15 -0
  68. package/dist/chunks/deny-ClTxSvHJ.mjs.map +1 -0
  69. package/dist/chunks/diff-Qw0II0sj.mjs +101 -0
  70. package/dist/chunks/diff-Qw0II0sj.mjs.map +1 -0
  71. package/dist/chunks/discover-30Ciya_0.mjs +20 -0
  72. package/dist/chunks/discover-30Ciya_0.mjs.map +1 -0
  73. package/dist/chunks/doctor-DfCrKpfT.mjs +222 -0
  74. package/dist/chunks/doctor-DfCrKpfT.mjs.map +1 -0
  75. package/dist/chunks/fix-D4Tj8RVA.mjs +18 -0
  76. package/dist/chunks/fix-D4Tj8RVA.mjs.map +1 -0
  77. package/dist/chunks/gc-BBXlBF5w.mjs +161 -0
  78. package/dist/chunks/gc-BBXlBF5w.mjs.map +1 -0
  79. package/dist/chunks/init-Okz4mlhW.mjs +30 -0
  80. package/dist/chunks/init-Okz4mlhW.mjs.map +1 -0
  81. package/dist/chunks/inspect-Buby4cQD.mjs +22 -0
  82. package/dist/chunks/inspect-Buby4cQD.mjs.map +1 -0
  83. package/dist/chunks/logs-Giq5sUau.mjs +25 -0
  84. package/dist/chunks/logs-Giq5sUau.mjs.map +1 -0
  85. package/dist/chunks/migrate-DRoFUg53.mjs +19 -0
  86. package/dist/chunks/migrate-DRoFUg53.mjs.map +1 -0
  87. package/dist/chunks/models-Btp6X2YZ.mjs +24 -0
  88. package/dist/chunks/models-Btp6X2YZ.mjs.map +1 -0
  89. package/dist/chunks/pause-BASBfKLO.mjs +16 -0
  90. package/dist/chunks/pause-BASBfKLO.mjs.map +1 -0
  91. package/dist/chunks/policy-DUIgzF3W.mjs +139 -0
  92. package/dist/chunks/policy-DUIgzF3W.mjs.map +1 -0
  93. package/dist/chunks/providers-Dx78imKj.mjs +19 -0
  94. package/dist/chunks/providers-Dx78imKj.mjs.map +1 -0
  95. package/dist/chunks/reconcile-BfD785gd.mjs +26 -0
  96. package/dist/chunks/reconcile-BfD785gd.mjs.map +1 -0
  97. package/dist/chunks/render-DyZr5Lgi.mjs +53 -0
  98. package/dist/chunks/render-DyZr5Lgi.mjs.map +1 -0
  99. package/dist/chunks/resume-GJQRb2Eb.mjs +15 -0
  100. package/dist/chunks/resume-GJQRb2Eb.mjs.map +1 -0
  101. package/dist/chunks/retry-CLrUem1H.mjs +15 -0
  102. package/dist/chunks/retry-CLrUem1H.mjs.map +1 -0
  103. package/dist/chunks/review-BDdjb6kB.mjs +17 -0
  104. package/dist/chunks/review-BDdjb6kB.mjs.map +1 -0
  105. package/dist/chunks/run-CgTWYlcd.mjs +82 -0
  106. package/dist/chunks/run-CgTWYlcd.mjs.map +1 -0
  107. package/dist/chunks/run-tool-C3rAv4XF.mjs +26 -0
  108. package/dist/chunks/run-tool-C3rAv4XF.mjs.map +1 -0
  109. package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
  110. package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
  111. package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
  112. package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
  113. package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
  114. package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
  115. package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
  116. package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
  117. package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
  118. package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
  119. package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
  120. package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
  121. package/dist/chunks/src-CId6kMsY.mjs +1689 -0
  122. package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
  123. package/dist/chunks/src-CfAOde7C.mjs +5403 -0
  124. package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
  125. package/dist/chunks/src-Cu3TxNob.mjs +783 -0
  126. package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
  127. package/dist/chunks/src-DcU4IXMe.mjs +117 -0
  128. package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
  129. package/dist/chunks/src-JxawBQBn.mjs +446 -0
  130. package/dist/chunks/src-JxawBQBn.mjs.map +1 -0
  131. package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
  132. package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
  133. package/dist/chunks/status-SMjiOMYy.mjs +29 -0
  134. package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
  135. package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
  136. package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
  137. package/dist/chunks/update-BXIppsbi.mjs +16 -0
  138. package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
  139. package/dist/cli.mjs +175 -0
  140. package/dist/cli.mjs.map +1 -0
  141. package/package.json +17 -32
  142. package/CHANGELOG.md +0 -1453
  143. package/bin/cli.js +0 -812
  144. package/bin/report.js +0 -141
  145. package/core/adapter/render.js +0 -389
  146. package/core/agents/implementer.template.md +0 -142
  147. package/core/agents/profile-reader.md +0 -50
  148. package/core/agents/release.md +0 -58
  149. package/core/agents/review.md +0 -179
  150. package/core/commands/cohorte-align-ds.md +0 -33
  151. package/core/commands/cohorte-audit.md +0 -58
  152. package/core/commands/cohorte-brainstorm.md +0 -70
  153. package/core/commands/cohorte-build.md +0 -219
  154. package/core/commands/cohorte-doctor.md +0 -170
  155. package/core/commands/cohorte-fix.md +0 -80
  156. package/core/commands/cohorte-fleet.md +0 -103
  157. package/core/commands/cohorte-init-pipeline.md +0 -31
  158. package/core/commands/cohorte-intake.md +0 -92
  159. package/core/commands/cohorte-patch.md +0 -118
  160. package/core/commands/cohorte-refactor.md +0 -59
  161. package/core/commands/cohorte-retro.md +0 -85
  162. package/core/commands/cohorte-review.md +0 -270
  163. package/core/commands/cohorte-ship.md +0 -122
  164. package/core/commands/cohorte-spec.md +0 -101
  165. package/core/commands/cohorte-update-pipeline.md +0 -134
  166. package/core/hooks/gate.py +0 -580
  167. package/core/runtimes/claude.json +0 -74
  168. package/core/runtimes/codex.json +0 -83
  169. package/core/runtimes/cursor.json +0 -76
  170. package/core/runtimes/gemini.json +0 -76
  171. package/core/runtimes/opencode.json +0 -73
  172. package/core/templates/agent-handoff.md +0 -34
  173. package/core/templates/brainstorm-return.md +0 -36
  174. package/core/templates/decisions.template.md +0 -42
  175. package/core/templates/design-brief.md +0 -44
  176. package/core/templates/patch.template.md +0 -86
  177. package/core/templates/pr-body.md +0 -25
  178. package/core/templates/review-feedback.md +0 -38
  179. package/core/templates/spec.template.md +0 -87
  180. package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
  181. package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
  182. package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
  183. package/core/templates/steps/init-pipeline/04-write-render.md +0 -115
  184. package/core/templates/steps/init-pipeline/05-report.md +0 -12
  185. package/core/workflows/audit.js +0 -240
  186. package/core/workflows/loop.js +0 -641
  187. package/core/workflows/refactor.js +0 -276
  188. package/core/workflows/review.js +0 -456
  189. package/install.ps1 +0 -103
  190. package/install.sh +0 -114
  191. package/lib/doctor.js +0 -504
  192. package/lib/runtime.js +0 -134
  193. package/lib/versions.js +0 -149
  194. package/lib/yaml.js +0 -129
  195. package/profile/PIPELINE.template.md +0 -251
  196. package/profile/SCHEMA.md +0 -698
  197. package/profile/cohorte.config.template.yaml +0 -43
  198. package/scripts/assert-gate-hook.mjs +0 -75
  199. package/scripts/kanban-move.sh +0 -313
  200. package/scripts/metrics/collect.mjs +0 -531
  201. package/scripts/metrics/prices.json +0 -42
  202. package/scripts/new-feature.sh.template +0 -98
  203. package/scripts/preflight.sh +0 -124
  204. package/scripts/remove-feature.sh.template +0 -56
  205. package/scripts/test-adapter.mjs +0 -368
  206. package/scripts/test-gate.mjs +0 -428
  207. package/scripts/test-kanban.mjs +0 -304
  208. package/scripts/test-lib.mjs +0 -310
  209. package/scripts/test-metrics.mjs +0 -156
  210. package/scripts/test-workflows.mjs +0 -758
  211. package/scripts/validate-core.mjs +0 -320
package/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
- };
@@ -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
- ```