@ersbeth/codelife 0.0.0-bootstrap.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +66 -0
  4. package/agents/evaluate.md +97 -0
  5. package/agents/ground.md +49 -0
  6. package/agents/implementer.md +49 -0
  7. package/agents/judge.md +128 -0
  8. package/agents/planner.md +45 -0
  9. package/agents/prerequisites.md +62 -0
  10. package/agents/reader.md +40 -0
  11. package/agents/reproduce.md +65 -0
  12. package/bin/codelife +4 -0
  13. package/package.json +40 -1
  14. package/presets/tsconfig.json +14 -0
  15. package/presets/vite.config.d.mts +3 -0
  16. package/presets/vite.config.mjs +49 -0
  17. package/scripts/audit/cadence.mjs +24 -0
  18. package/scripts/audit/close.mjs +57 -0
  19. package/scripts/audit/file.mjs +70 -0
  20. package/scripts/audit/fold.mjs +48 -0
  21. package/scripts/audit/lib.mjs +93 -0
  22. package/scripts/audit/prepare.mjs +180 -0
  23. package/scripts/audit/table.mjs +65 -0
  24. package/scripts/brainstorm/decided.mjs +6 -0
  25. package/scripts/brainstorm/lib.mjs +33 -0
  26. package/scripts/brainstorm/named.mjs +9 -0
  27. package/scripts/changelog.mjs +100 -0
  28. package/scripts/check-tag.mjs +19 -0
  29. package/scripts/checks.mjs +53 -0
  30. package/scripts/codelife.mjs +42 -0
  31. package/scripts/commit-msg.mjs +43 -0
  32. package/scripts/contract.mjs +179 -0
  33. package/scripts/former-form.mjs +170 -0
  34. package/scripts/git/branches.mjs +6 -0
  35. package/scripts/git/commit.mjs +40 -0
  36. package/scripts/git/diff.mjs +17 -0
  37. package/scripts/git/diff_lines.mjs +5 -0
  38. package/scripts/git/held.mjs +53 -0
  39. package/scripts/git/is_ancestor.mjs +28 -0
  40. package/scripts/git/lib.mjs +20 -0
  41. package/scripts/git/log_since.mjs +27 -0
  42. package/scripts/git/parent.mjs +51 -0
  43. package/scripts/git/show.mjs +4 -0
  44. package/scripts/git/status.mjs +25 -0
  45. package/scripts/git/submodules.mjs +5 -0
  46. package/scripts/init.mjs +21 -0
  47. package/scripts/install.mjs +32 -0
  48. package/scripts/lenses/A11Y.md +91 -0
  49. package/scripts/lenses/ARCH.md +90 -0
  50. package/scripts/lenses/BUG.md +52 -0
  51. package/scripts/lenses/BUILD.md +40 -0
  52. package/scripts/lenses/COMPAT.md +67 -0
  53. package/scripts/lenses/CONF.md +49 -0
  54. package/scripts/lenses/DEPS.md +43 -0
  55. package/scripts/lenses/DOC.md +73 -0
  56. package/scripts/lenses/FAIL.md +57 -0
  57. package/scripts/lenses/GOV.md +56 -0
  58. package/scripts/lenses/GUARD.md +47 -0
  59. package/scripts/lenses/INTENT.md +52 -0
  60. package/scripts/lenses/LEAN.md +49 -0
  61. package/scripts/lenses/LINES.md +48 -0
  62. package/scripts/lenses/PERF.md +61 -0
  63. package/scripts/lenses/PKG.md +58 -0
  64. package/scripts/lenses/SEC.md +49 -0
  65. package/scripts/lenses/STANDARDS.md +62 -0
  66. package/scripts/lenses/TYPE.md +56 -0
  67. package/scripts/lenses/VERIF.md +81 -0
  68. package/scripts/link.mjs +40 -0
  69. package/scripts/methods/cause.md +21 -0
  70. package/scripts/methods/comments.md +157 -0
  71. package/scripts/methods/description.md +67 -0
  72. package/scripts/methods/message.md +24 -0
  73. package/scripts/methods/red-green.md +28 -0
  74. package/scripts/methods/refusal.md +22 -0
  75. package/scripts/mr/open.mjs +219 -0
  76. package/scripts/node-floor.mjs +19 -0
  77. package/scripts/onboard/prep.mjs +93 -0
  78. package/scripts/overview/codelife-line.mjs +33 -0
  79. package/scripts/overview/next.mjs +76 -0
  80. package/scripts/overview/overview.mjs +308 -0
  81. package/scripts/package-manager.mjs +20 -0
  82. package/scripts/port/item.mjs +94 -0
  83. package/scripts/port/lib.mjs +194 -0
  84. package/scripts/port/prep.mjs +33 -0
  85. package/scripts/port/version.mjs +50 -0
  86. package/scripts/pre-commit.mjs +43 -0
  87. package/scripts/pre-push.mjs +53 -0
  88. package/scripts/prepare-lib.mjs +81 -0
  89. package/scripts/read-roadmap/lib.mjs +116 -0
  90. package/scripts/read-roadmap/moved.mjs +70 -0
  91. package/scripts/read-roadmap/range.mjs +15 -0
  92. package/scripts/release/cut.mjs +120 -0
  93. package/scripts/release/entry.mjs +52 -0
  94. package/scripts/release/lib.mjs +136 -0
  95. package/scripts/release/prep.mjs +58 -0
  96. package/scripts/review-mr/prepare.mjs +163 -0
  97. package/scripts/roadmap/assemble.mjs +269 -0
  98. package/scripts/roadmap/cut.mjs +88 -0
  99. package/scripts/roadmap/prepare.mjs +249 -0
  100. package/scripts/roadmap/roadmap.md +30 -0
  101. package/scripts/roadmap/write.mjs +53 -0
  102. package/scripts/search.mjs +29 -0
  103. package/scripts/setup/contract.mjs +25 -0
  104. package/scripts/setup/item.mjs +166 -0
  105. package/scripts/setup/lib.mjs +205 -0
  106. package/scripts/setup/prep.mjs +106 -0
  107. package/scripts/spec-form.mjs +444 -0
  108. package/scripts/spec.mjs +56 -0
  109. package/scripts/tags.mjs +27 -0
  110. package/scripts/target/target-app.md +134 -0
  111. package/scripts/target/target-base.md +127 -0
  112. package/scripts/target/target-lib.md +143 -0
  113. package/scripts/target/templates/audit/README.md +21 -0
  114. package/scripts/target/templates/characterisation/bundle.test.ts +43 -0
  115. package/scripts/target/templates/characterisation/declarations.test.ts +44 -0
  116. package/scripts/target/templates/characterisation/entries.ts +50 -0
  117. package/scripts/target/templates/characterisation/examples.test.ts +18 -0
  118. package/scripts/target/templates/characterisation/import.test.ts +22 -0
  119. package/scripts/target/templates/characterisation/surface.test.ts +68 -0
  120. package/scripts/target/templates/cliff.toml +61 -0
  121. package/scripts/target/templates/githooks/commit-msg +9 -0
  122. package/scripts/target/templates/githooks/pre-commit +9 -0
  123. package/scripts/target/templates/githooks/pre-push +9 -0
  124. package/scripts/target/templates/playground/index.html +11 -0
  125. package/scripts/target/templates/playground/main.ts +9 -0
  126. package/scripts/target/templates/spec/README.md +35 -0
  127. package/scripts/target/templates/spec/capability.md +36 -0
  128. package/scripts/target/templates/spec/glossary.md +5 -0
  129. package/scripts/target/templates/spec/journey.md +29 -0
  130. package/scripts/target/templates/spec/principles.md +15 -0
  131. package/scripts/target/templates/spec/users.md +15 -0
  132. package/scripts/target/templates/tsconfig.json +9 -0
  133. package/scripts/target/templates/vite.config.ts +4 -0
  134. package/scripts/text.mjs +6 -0
  135. package/scripts/tracker/account.mjs +5 -0
  136. package/scripts/tracker/auth.mjs +9 -0
  137. package/scripts/tracker/cli.version.mjs +5 -0
  138. package/scripts/tracker/close.mjs +20 -0
  139. package/scripts/tracker/create.mjs +47 -0
  140. package/scripts/tracker/issue.section.mjs +25 -0
  141. package/scripts/tracker/issue.update.mjs +13 -0
  142. package/scripts/tracker/issue.view.mjs +16 -0
  143. package/scripts/tracker/lib.mjs +130 -0
  144. package/scripts/tracker/list.mjs +14 -0
  145. package/scripts/tracker/list.whole.mjs +20 -0
  146. package/scripts/tracker/lot.row.mjs +42 -0
  147. package/scripts/tracker/mr.checkout_command.mjs +5 -0
  148. package/scripts/tracker/mr.comments.mjs +50 -0
  149. package/scripts/tracker/mr.create.mjs +12 -0
  150. package/scripts/tracker/mr.description.mjs +8 -0
  151. package/scripts/tracker/mr.fetch_head.mjs +5 -0
  152. package/scripts/tracker/mr.find_for_branch.mjs +9 -0
  153. package/scripts/tracker/mr.post.mjs +40 -0
  154. package/scripts/tracker/mr.update_description.mjs +8 -0
  155. package/scripts/tracker/mr.verdict.mjs +34 -0
  156. package/scripts/tracker/mr.view.mjs +20 -0
  157. package/scripts/tracker/none.mjs +79 -0
  158. package/scripts/tracker/note.mjs +12 -0
  159. package/scripts/tracker/reopen.mjs +8 -0
  160. package/scripts/tracker/search.mjs +8 -0
  161. package/scripts/tracker/search_id.mjs +18 -0
  162. package/scripts/tracker/templates/bug.md +35 -0
  163. package/scripts/tracker/templates/feature.md +40 -0
  164. package/scripts/tracker/templates/lot.md +46 -0
  165. package/scripts/tracker/title.join.mjs +6 -0
  166. package/scripts/tracker/title.leave.mjs +7 -0
  167. package/scripts/tracker/title.lot_of.mjs +8 -0
  168. package/scripts/tracker/title.search_id_filter.mjs +17 -0
  169. package/scripts/tracker/title.sentence_of.mjs +5 -0
  170. package/scripts/tracker/view.mjs +25 -0
  171. package/scripts/verbs.mjs +579 -0
  172. package/scripts/verify/BUILD.mjs +33 -0
  173. package/scripts/verify/DEPS.mjs +15 -0
  174. package/scripts/verify/GOV.mjs +34 -0
  175. package/scripts/verify/GUARD.mjs +34 -0
  176. package/scripts/verify/LINES.mjs +22 -0
  177. package/scripts/verify/PKG.mjs +56 -0
  178. package/scripts/verify/TYPE.mjs +34 -0
  179. package/scripts/verify/cut.mjs +73 -0
  180. package/scripts/verify/history.mjs +32 -0
  181. package/scripts/verify/lib.mjs +129 -0
  182. package/scripts/verify/report.mjs +104 -0
  183. package/scripts/verify/timing.mjs +19 -0
  184. package/scripts/verify.mjs +29 -0
  185. package/scripts/work/branch.mjs +81 -0
  186. package/scripts/work/close.mjs +74 -0
  187. package/scripts/work/member.mjs +73 -0
  188. package/scripts/work/prepare.mjs +116 -0
  189. package/scripts/work/reopen.mjs +40 -0
  190. package/scripts/work-feedback/prepare.mjs +96 -0
  191. package/skills/audit/SKILL.md +110 -0
  192. package/skills/audit/templates/index.md +36 -0
  193. package/skills/audit/templates/lens.md +44 -0
  194. package/skills/brainstorm/SKILL.md +144 -0
  195. package/skills/brainstorm/references/exit.md +57 -0
  196. package/skills/brainstorm/templates/note.md +79 -0
  197. package/skills/commit/SKILL.md +68 -0
  198. package/skills/mr/SKILL.md +82 -0
  199. package/skills/new-issue/SKILL.md +87 -0
  200. package/skills/onboard/SKILL.md +103 -0
  201. package/skills/onboard/templates/CLAUDE.md +31 -0
  202. package/skills/onboard/templates/CONTRIBUTING.md +34 -0
  203. package/skills/onboard/templates/README.md +30 -0
  204. package/skills/onboard/templates/SKILL.md +24 -0
  205. package/skills/onboard/templates/package-CLAUDE.md +15 -0
  206. package/skills/overview/SKILL.md +59 -0
  207. package/skills/port-toolchain/SKILL.md +127 -0
  208. package/skills/release/SKILL.md +78 -0
  209. package/skills/review-mr/SKILL.md +106 -0
  210. package/skills/review-mr/templates/pass.md +29 -0
  211. package/skills/roadmap/SKILL.md +137 -0
  212. package/skills/setup/SKILL.md +112 -0
  213. package/skills/work/SKILL.md +139 -0
  214. package/skills/work-feedback/SKILL.md +113 -0
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ // `codelife release entry` (K-16, R-247): once the tag is on origin, the forge's release entry for
3
+ // it, its notes the version's changelog section — an entry the forge already has, or refuses, is a
4
+ // line, never a failure —, then the hand-off: the pipeline's URL, the registry page of a package
5
+ // that is not private, and the failure modes a person watching recognises. Publishes nothing and
6
+ // watches nothing. Exit 1 when the tag is missing, here or on origin.
7
+ import '../node-floor.mjs';
8
+ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { tagFor } from '../tags.mjs';
12
+ import { cli as forgeCli, env, parseOrigin } from '../tracker/lib.mjs';
13
+ import { hasOrigin, manifest, said, say, trackerKind, versionFile } from './lib.mjs';
14
+ import { git, sh } from '../verify/lib.mjs';
15
+
16
+ if (process.argv.length > 2) { console.error('usage: codelife release entry'); process.exit(2); }
17
+ console.log('### release entry');
18
+ const stop = (why) => { say('stop', why); process.exit(1); };
19
+ const pkg = manifest(versionFile()) ?? {};
20
+ const v = pkg.version, tag = v && tagFor(v);
21
+ if (!tag || !git('tag', '-l', tag).out) stop(`no tag ${tag ?? 'for the version file'} — \`codelife release cut\` first`);
22
+ if (!hasOrigin()) { say('entry', 'none — no origin: the changelog section is the entry'); process.exit(0); }
23
+ const ls = git('ls-remote', '--exit-code', '--tags', 'origin', `refs/tags/${tag}`);
24
+ if (ls.status === 2) stop(`origin has no ${tag} — \`git push origin refs/tags/${tag}\` first`);
25
+ if (ls.status) stop(`origin could not tell — ${said(ls)}`);
26
+
27
+ const kind = trackerKind();
28
+ let notes = '';
29
+ try {
30
+ const lines = readFileSync('CHANGELOG.md', 'utf8').split('\n');
31
+ const at = lines.findIndex((l) => l.startsWith(`## [${v}]`));
32
+ if (at >= 0) { const end = lines.findIndex((l, i) => i > at && (l.startsWith('## ') || l.startsWith('<!--'))); notes = lines.slice(at + 1, end < 0 ? undefined : end).join('\n').trim(); }
33
+ } catch { /* no changelog: an entry with no notes */ }
34
+ if (kind === 'glab' || kind === 'gh') {
35
+ const file = join(mkdtempSync(join(tmpdir(), 'codelife-release-')), 'notes.md');
36
+ writeFileSync(file, `${notes}\n`);
37
+ const cli = (...a) => sh(forgeCli(), a, { env: env() });
38
+ if (cli('release', 'view', tag).status === 0) say('entry', `exists already on ${tag} — left as it is`);
39
+ else {
40
+ const r = kind === 'glab' ? cli('release', 'create', tag, '--name', v, '--notes-file', file) : cli('release', 'create', tag, '--title', v, '--notes-file', file, '--verify-tag');
41
+ say('entry', r.status ? `degraded — ${said(r)}` : `created on ${tag}${r.out ? ` — ${r.out.split('\n').at(-1)}` : ''}`);
42
+ }
43
+ } else say('entry', `none — tracker.kind ${kind}: the changelog section is the entry`);
44
+
45
+ const url = git('remote', 'get-url', 'origin').out, forge = parseOrigin(url);
46
+ if (!forge) say('pipeline', `none — origin ${url} is no forge`);
47
+ else if (kind === 'gh' || forge.host === 'github.com') say('pipeline', `https://${forge.host}/${forge.project}/actions`);
48
+ else say('pipeline', `https://${forge.host}/${forge.project}/-/pipelines?ref=${tag}`);
49
+ say('registry', pkg.private || !pkg.name ? 'none — the package is private or unnamed' : `https://www.npmjs.com/package/${pkg.name}`);
50
+ say('watch for', 'no pipeline at all — the tag matches nothing the pipeline\'s rules watch for');
51
+ say('watch for', 'an authentication error at publish time (EOTP and its kin) — an account authenticated where a trusted publisher was expected');
52
+ say('watch for', 'a green pipeline that published nothing — the job\'s condition did not match the tag');
@@ -0,0 +1,136 @@
1
+ // What `codelife release prep`, `cut` and `entry` share (K-16), from the repository's root: where a
2
+ // release may run, the line's last tag and the commits since it, the bump the scheme gives, and a
3
+ // version already prepared. The rules are release.md's; each function says which.
4
+ import '../node-floor.mjs';
5
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { PATTERNS, spellings, versionOf } from '../tags.mjs';
8
+ import { git, guards, sh } from '../verify/lib.mjs';
9
+ import { field } from '../contract.mjs';
10
+
11
+ export const said = (r) => (r.err || r.out).split('\n').at(-1);
12
+ export const say = (key, value) => console.log(`- ${key}: ${value}`);
13
+
14
+ export const versionFile = () => field('release.version_file') ?? '';
15
+ export const trackerKind = () => field('tracker.kind') ?? 'none';
16
+ export const manifest = (path, ref) => {
17
+ const text = ref ? git('show', `${ref}:${path}`) : { status: existsSync(path) ? 0 : 1, out: existsSync(path) ? readFileSync(path, 'utf8') : '' };
18
+ try { return text.status ? null : JSON.parse(text.out); } catch { return null; }
19
+ };
20
+ export const isTagged = (version, name) => !!git('tag', '-l', ...spellings(version, name)).out;
21
+ // The paths the tree changed, staged or not, untracked included; read untrimmed, NUL-separated.
22
+ // A rename's or a copy's entry is followed by its origin's, skipped. A git status that fails is
23
+ // `fail`'s, a `- stop:` and exit 1 unless the caller gives its own: never a clean tree.
24
+ export const dirtyPaths = (fail = (why) => { say('stop', why); process.exit(1); }) => {
25
+ const r = sh('git', ['status', '--porcelain', '-z', '-uall'], { trim: false });
26
+ if (r.status) return fail(`git status could not tell what the tree changed — ${r.err}`);
27
+ const entries = r.out.split('\0');
28
+ const paths = [];
29
+ for (let i = 0; i < entries.length; i++) if (entries[i]) { paths.push(entries[i].slice(3)); if (/^[RC]/.test(entries[i])) i++; }
30
+ return paths;
31
+ };
32
+ export const hasOrigin = () => git('remote', 'get-url', 'origin').status === 0;
33
+
34
+ // R-241: `develop` cuts a release, `main` a hotfix's patch, `support/` a patch on an older line;
35
+ // `--prepare` runs on any branch, the merge and the tag left to a later run on one of those three.
36
+ export function where(prepare) {
37
+ const branch = git('branch', '--show-current').out;
38
+ const kind = { develop: 'release', main: 'hotfix' }[branch] ?? (branch.startsWith('support/') ? 'support' : null);
39
+ const stops = [];
40
+ if (!branch) stops.push('HEAD is detached — check out develop, main or a support/ branch');
41
+ else if (!kind && !prepare) stops.push(`${branch} is a work branch — merge it, then run /release on develop (C-355), or prepare it with --prepare`);
42
+ const dirty = dirtyPaths((why) => { stops.push(why); return []; });
43
+ if (dirty.length) stops.push(`the tree is not clean — a release commits what it writes and nothing else: ${dirty.slice(0, 5).join(', ')}${dirty.length > 5 ? ', …' : ''}`);
44
+ return { branch, kind, stops };
45
+ }
46
+
47
+ // R-241: level with origin as the forge has it, not as the last fetch left it. Behind or diverged
48
+ // stops; ahead is what the pushes carry; no origin keeps the release local.
49
+ export function level(branch) {
50
+ if (!hasOrigin()) return { line: 'none — the release stays local: nothing compared, pushed or handed off' };
51
+ const ls = git('ls-remote', '--exit-code', '--heads', 'origin', branch);
52
+ if (ls.status === 2) return { line: `has no ${branch} yet — ahead, the push carries it` };
53
+ const f = ls.status ? ls : git('fetch', 'origin', `+refs/heads/${branch}:refs/remotes/origin/${branch}`);
54
+ if (f.status) return { stop: `origin could not tell — ${said(f)}` };
55
+ const [behind, ahead] = git('rev-list', '--left-right', '--count', `origin/${branch}...HEAD`).out.split(/\s+/).map(Number);
56
+ if (behind && ahead) return { stop: `${branch} diverged from origin/${branch}, ${ahead} ahead and ${behind} behind — reconcile first` };
57
+ if (behind) return { stop: `${branch} is ${behind} behind origin/${branch} — \`git pull --ff-only\`, then /release again` };
58
+ return { line: ahead ? `${ahead} ahead of origin/${branch} — the push carries it` : `level with origin/${branch}` };
59
+ }
60
+
61
+ // R-242: the line's last tag, whichever spelling it has, and the commits since it as Conventional
62
+ // Commits; with no tag, the whole history.
63
+ export const lastTag = () => {
64
+ const r = git('describe', '--tags', '--abbrev=0', ...PATTERNS.flatMap((p) => ['--match', p]), 'HEAD');
65
+ return r.status ? null : r.out;
66
+ };
67
+ export const commits = (from) => git('log', '--no-merges', '--format=%H%x1f%s%x1f%b%x1e', ...(from ? [`${from}..HEAD`] : ['HEAD'])).out
68
+ .split('\x1e').map((x) => x.replace(/^\n/, '')).filter(Boolean).map((record) => {
69
+ const [sha, subject, body = ''] = record.split('\x1f');
70
+ const m = subject.match(/^(\w+)(?:\([^)]*\))?(!)?: \S/);
71
+ return { sha, subject, type: m?.[1] ?? null, breaking: !!m?.[2] || /^BREAKING[ -]CHANGE: /m.test(body) };
72
+ });
73
+
74
+ // R-242: the scheme, derived from the manifest at the last tag — `major.peer.patch` when its version
75
+ // reads `<n>.<peer>.<p>` and a peerDependencies entry has minor `<peer>` —, then the bump.
76
+ const numbers = (v) => v.split('-')[0].split('.').map(Number);
77
+ const peerMinor = (range) => { const m = String(range ?? '').match(/(\d+)\.(\d+)/); return m ? Number(m[2]) : null; };
78
+ export function bump(from, list, before, now) {
79
+ const [M, m, p] = numbers(from);
80
+ const peer = Object.entries(before?.peerDependencies ?? {}).find(([, r]) => peerMinor(r) === m)?.[0];
81
+ const breaking = list.some((c) => c.breaking), feat = list.some((c) => c.type === 'feat');
82
+ const scheme = peer ? `major.peer.patch — peer ${peer}` : 'semver';
83
+ if (from.includes('-')) return { scheme, version: `${M}.${m}.${p}`, why: `the final release of the pre-release ${from}` };
84
+ if (peer) {
85
+ const moved = peerMinor(now?.peerDependencies?.[peer]);
86
+ if (moved !== null && moved !== m) return { scheme, version: `${M + 1}.${moved}.0`, why: `the peer ${peer} moved from minor ${m} to ${moved}: a new line`, support: `support/${M}.${m}` };
87
+ return breaking ? { scheme, version: `${M + 1}.${m}.0`, why: 'a breaking change bumps the own major, at the same peer' }
88
+ : { scheme, version: `${M}.${m}.${p + 1}`, why: 'anything else bumps the patch under major.peer.patch' };
89
+ }
90
+ if (M === 0) return breaking || feat ? { scheme, version: `0.${m + 1}.0`, why: `${breaking ? 'a breaking change' : 'a feat'} bumps the middle number on a 0.x line` }
91
+ : { scheme, version: `0.${m}.${p + 1}`, why: 'no feat and no breaking change' };
92
+ if (breaking) return { scheme, version: `${M + 1}.0.0`, why: 'a breaking change' };
93
+ return feat ? { scheme, version: `${M}.${m + 1}.0`, why: 'a feat, no breaking change' } : { scheme, version: `${M}.${m}.${p + 1}`, why: 'no feat and no breaking change' };
94
+ }
95
+
96
+ // R-241: a version file ahead of every tag is a release prepared — by `--prepare`, or by a run
97
+ // interrupted after its commit — and is resumed at the merge only when nothing landed after its
98
+ // release commit; otherwise the bump is redone from the last tag.
99
+ export function prepared(version, name, from) {
100
+ if (!version || version === '0.0.0' || isTagged(version, name)) return null;
101
+ const commit = git('log', '-1', '--format=%H', '-F', `--grep=chore(release): ${version}`, ...(from ? [`${from}..HEAD`] : ['HEAD'])).out;
102
+ if (!commit) return { version, commit: null, after: [] };
103
+ return { version, commit, after: git('rev-list', '--no-merges', `${commit}..HEAD`).out.split('\n').filter(Boolean) };
104
+ }
105
+
106
+ // R-244: the version file, and every published package.json the workspace declares.
107
+ export function packages(vf) {
108
+ const root = manifest('package.json') ?? {};
109
+ let globs = Array.isArray(root.workspaces) ? root.workspaces : root.workspaces?.packages ?? [];
110
+ if (existsSync('pnpm-workspace.yaml')) {
111
+ const block = readFileSync('pnpm-workspace.yaml', 'utf8').split(/^packages:\s*$/m)[1]?.split(/^\S/m)[0] ?? '';
112
+ globs = [...block.matchAll(/^\s+-\s*['"]?([^'"\s#]+)/gm)].map((m) => m[1]);
113
+ }
114
+ const dirs = globs.filter((g) => !g.startsWith('!')).flatMap((g) => g.endsWith('/*')
115
+ ? (existsSync(g.slice(0, -2)) ? readdirSync(g.slice(0, -2), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join(g.slice(0, -2), d.name)) : [])
116
+ : [g.replace(/\/$/, '')]);
117
+ const files = dirs.map((d) => join(d, 'package.json')).filter((f) => { const j = manifest(f); return j && !j.private && j.version; });
118
+ return [...new Set([vf, ...files])];
119
+ }
120
+
121
+ // R-243: the generated[] entry that writes CHANGELOG.md, its command one line of shell.
122
+ export const changelog = () => guards('generated').find((g) => (g.path ?? []).includes('CHANGELOG.md'))?.command?.join(' && ') ?? null;
123
+
124
+ // The facts prep prints and cut acts on, read once.
125
+ export function read(prepare = false) {
126
+ const w = where(prepare);
127
+ const vf = versionFile();
128
+ const now = vf ? manifest(vf) : null;
129
+ const tag = lastTag();
130
+ const list = commits(tag);
131
+ const from = tag ? versionOf(tag) : null;
132
+ const p = now ? prepared(now.version, now.name, tag) : null;
133
+ const resume = p?.commit && !p.after.length ? p : null;
134
+ const proposed = from && list.length ? bump(from, list, manifest(vf, tag), now) : null;
135
+ return { ...w, vf, now, tag, from, list, prepared: p, resume, proposed };
136
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // `codelife release prep [--prepare]` (K-16): everything /release states before it asks for the
3
+ // version — the branch, origin, the last tag, the commits since it, the bump, a release already
4
+ // prepared, the audit the cadence proposes, the changelog generator. Reads only; one `### release
5
+ // prep` block; a `- stop:` line per thing that stops the run, and exit 1.
6
+ import '../node-floor.mjs';
7
+ import { cadence } from '../audit/cadence.mjs';
8
+ import { changelog, level, read, say, trackerKind } from './lib.mjs';
9
+ import { git, sh } from '../verify/lib.mjs';
10
+
11
+ const args = process.argv.slice(2);
12
+ if (args.some((a) => a !== '--prepare')) { console.error('usage: codelife release prep [--prepare]'); process.exit(2); }
13
+ const prepare = args.includes('--prepare');
14
+ const f = read(prepare);
15
+ const stops = [...f.stops];
16
+
17
+ console.log('### release prep');
18
+ say('branch', `${f.branch || 'detached'} — ${{ release: 'a release, merged into main and tagged there', hotfix: 'a patch after a hotfix, tagged on main and merged back into develop', support: 'a patch on an older line, tagged here, merged nowhere' }[f.kind] ?? 'a work branch'}${prepare ? ' — --prepare: up to the release commit' : ''}`);
19
+ if (f.branch) {
20
+ const l = level(f.branch);
21
+ if (l.stop) stops.push(l.stop); else say('origin', l.line);
22
+ }
23
+ if (!f.vf || !f.now?.version) stops.push(`no version read from release.version_file ('${f.vf}') — /setup`);
24
+ else say('version file', `${f.vf} reads ${f.now.version}`);
25
+ say('last tag', f.tag ? `${f.tag} (${f.from})` : 'none — the whole history is read, and the first version is yours to give');
26
+
27
+ const types = Object.entries(f.list.reduce((n, c) => ({ ...n, [c.type ?? 'unparsed']: (n[c.type ?? 'unparsed'] ?? 0) + 1 }), {})).sort((a, b) => b[1] - a[1]);
28
+ say('commits', f.list.length ? `${f.list.length} since ${f.tag ?? 'the first'} — ${types.map(([t, n]) => `${t} ${n}`).join(', ')}` : 'none');
29
+ for (const c of f.list.filter((x) => x.breaking)) say('breaking', `${c.sha.slice(0, 8)} ${c.subject}`);
30
+
31
+ if (f.resume) say('resume', `${f.resume.version} is prepared by ${f.resume.commit.slice(0, 8)} chore(release): ${f.resume.version} and carries no tag — \`codelife release cut\` goes straight to the merge and the tag`);
32
+ else {
33
+ if (f.prepared?.commit) say('prepared', `${f.prepared.version} by ${f.prepared.commit.slice(0, 8)}, but ${f.prepared.after.length} commit${f.prepared.after.length === 1 ? '' : 's'} landed after it — the bump is redone from ${f.tag ?? 'the first commit'}`);
34
+ else if (f.prepared) say('prepared', `${f.prepared.version} carries no tag and no release commit — the bump is redone from ${f.tag ?? 'the first commit'}`);
35
+ if (f.tag && !f.list.length) stops.push(`nothing since ${f.tag} — no release to cut`);
36
+ if (f.proposed) {
37
+ say('scheme', f.proposed.scheme);
38
+ say('bump', `${f.proposed.version} — ${f.proposed.why}`);
39
+ if (f.proposed.support) say('support', `${f.proposed.support} at ${f.tag}, opened with the bump so the old line stays patchable`);
40
+ const [M, m] = f.from.split('.').map(Number), [nM, nm] = f.proposed.version.split('.').map(Number);
41
+ // A campaign since the last tag already answers the bump's proposal (R-248, C-368); both days in UTC (#1137).
42
+ const c = cadence(), since = new Date(git('log', '-1', '--format=%ct', f.tag).out * 1000).toISOString().slice(0, 10);
43
+ const after = (row) => row && row[1] >= since;
44
+ const audit = c.due ? `full — ${c.due}` : nM > M && !after(c.full) ? 'full — before a major' : nm > m && !after(c.last) ? 'targeted — before a minor' : null;
45
+ say('audit', audit ? `${audit}: /audit proposed first` : 'none due');
46
+ } else if (!f.tag) say('bump', 'none — no tag: give the first version');
47
+ }
48
+
49
+ const cl = changelog();
50
+ if (!cl) stops.push('no generated[] entry writes CHANGELOG.md — /setup');
51
+ else {
52
+ const r = sh(`${cl} --check`, [], { shell: true });
53
+ if (r.status) stops.push(`the changelog generator refuses — ${r.err.split('\n')[0] || `exit ${r.status}`}`);
54
+ else say('changelog', `\`${cl}\` renders`);
55
+ }
56
+ say('tracker', trackerKind());
57
+ for (const s of stops) say('stop', s);
58
+ process.exit(stops.length ? 1 : 0);
@@ -0,0 +1,163 @@
1
+ // /review-mr's preparation, `codelife review prep [<n>]` (C-762): the merge request — the argument,
2
+ // or the one open for the current branch —, the account, the forge CLI against its floor, the head
3
+ // asserted and the tree clean, the diff with each file's changed lines, *How to test* split by
4
+ // shape, the known list, the reader's case and the earlier pass — every file in a run folder
5
+ // outside the tree, and a summary printed, which the brief is made from. Its forge and git reads run
6
+ // scripts/tracker/ and scripts/git/, each named after its operation. A precondition that fails, or
7
+ // a read that does, prints `- stop:` and exits 1, before any agent. From the repository's root.
8
+ //
9
+ // The run folder: mr.json, comments.md, diff.diff, lines.tsv (`<path>\t<new|old>\t<a>\t<b>`),
10
+ // commands.txt and manual.md (*How to test*'s commands, one a line, and its manual steps), known.txt,
11
+ // case.md — what the reader is handed: the range, the issues the commits close with their umbrella
12
+ // and what each cites, or the description when none is closed.
13
+ import '../node-floor.mjs';
14
+ import { spawnSync } from 'node:child_process';
15
+ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { dirname, join } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { field } from '../contract.mjs';
20
+
21
+ const pkg = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
22
+ let n = process.argv[2];
23
+ if (n !== undefined && !/^\d+$/.test(n)) {
24
+ console.error('usage: codelife review prep [<mr-number>]');
25
+ process.exit(2);
26
+ }
27
+ const summary = ['### prepare'];
28
+ const say = (line) => summary.push(`- ${line}`);
29
+ const stop = (why) => {
30
+ say(`stop: ${why}`);
31
+ console.log(summary.join('\n'));
32
+ process.exit(1);
33
+ };
34
+ const spawn = (file, args, input) => spawnSync(file, args, { encoding: 'utf8', input, maxBuffer: 256 * 1024 * 1024 });
35
+ // op <skill> <operation> <args> [stdin] — that skill's script for the operation: { status, stdout, stderr }.
36
+ const op = (skill, operation, args = [], input) => spawn(process.execPath, [join(pkg, 'scripts', skill, `${operation}.mjs`), ...args], input);
37
+ // must — the same, its stdout, or the stop naming the operation and its error.
38
+ const must = (skill, operation, args, input) => {
39
+ const r = op(skill, operation, args, input);
40
+ if (r.status !== 0) stop(`scripts/${skill}/${operation}.mjs failed — ${(r.stderr || r.stdout).trim()}`);
41
+ return r.stdout;
42
+ };
43
+ const nul = (...paths) => paths.filter(Boolean).join('\0');
44
+ const lines = (text) => text.split('\n').filter(Boolean);
45
+ const section = (text, heading) => text.split(new RegExp(`^## ${heading}.*$`, 'm'))[1]?.split(/^## |^<!-- codelife:end/m)[0] ?? '';
46
+
47
+ // Where you are: the CLI and its floor, the account, the merge request, the head and the tree.
48
+ const [cli, version] = must('tracker', 'cli.version').trim().split(' ');
49
+ const row = lines(readFileSync(join(pkg, 'scripts/target/target-base.md'), 'utf8'))
50
+ .find((l) => l.startsWith('| ') && l.split('|')[1].includes(`\`${cli}\``))?.split('|')[3] || '';
51
+ const floor = row.match(new RegExp(`\`${cli}\` (\\S+)`))?.[1] || row.trim().split(' ')[0];
52
+ const below = (have, want) => {
53
+ const [a, b] = [have, want].map((v) => v.split('.').map(Number));
54
+ for (let i = 0; i < 3; i++) if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) < (b[i] || 0);
55
+ return false;
56
+ };
57
+ const account = must('tracker', 'account').trim();
58
+ if (n === undefined) {
59
+ const branch = spawn('git', ['branch', '--show-current']).stdout.trim();
60
+ const open = branch ? lines(must('tracker', 'mr.find_for_branch', [branch])).filter((l) => l.endsWith(' open')) : [];
61
+ if (open.length !== 1) stop(`no number given, and ${branch ? `${branch} has ${open.length} open merge requests` : 'HEAD is on no branch'} — \`/review-mr <n>\``);
62
+ n = open[0].split(' ')[0];
63
+ say(`number: !${n}, the open merge request of ${branch}`);
64
+ }
65
+ must('tracker', 'mr.fetch_head', [n]);
66
+ const mr = JSON.parse(must('tracker', 'mr.view', [n]));
67
+ say(`merge request: !${n} — head ${mr.sha}, target ${mr.target}, merge base ${mr.base}, author ${mr.author}, draft ${mr.draft ? 'yes' : 'no'}, state ${mr.state}`);
68
+ if (mr.author === account) stop(`!${n} is ${account}'s own merge request — reviewed inside its unit, before it opened (work C-861); /review-mr reviews another person's`);
69
+ say(`account: ${account} — a reviewer`);
70
+ say(`forge CLI: ${cli} ${version}, floor ${floor || 'unread'}`);
71
+ if (floor && floor !== 'none' && below(version, floor)) stop(`${cli} ${version} is below the floor ${floor} the review's posting needs`);
72
+ const tree = JSON.parse(must('git', 'status'));
73
+ if (tree.oid !== mr.sha) stop(`HEAD is ${tree.oid}, not the head ${mr.sha} — \`${must('tracker', 'mr.checkout_command', [n]).trim()}\` reaches it`);
74
+ const dirty = [...tree.staged, ...tree.modified, ...tree.untracked];
75
+ if (dirty.length) stop(`the tree is not clean: ${dirty.join(', ')}`);
76
+ say('tree: at the head, clean');
77
+ const run = mkdtempSync(join(tmpdir(), 'codelife-review-'));
78
+ say(`run: ${run}`);
79
+ writeFileSync(join(run, 'mr.json'), JSON.stringify(mr, null, 2));
80
+
81
+ // The earlier pass, whoever ran it: its number and the sha it read, and the comments people wrote
82
+ // after it, which this pass carries (C-865).
83
+ const comments = must('tracker', 'mr.comments', [n]);
84
+ writeFileSync(join(run, 'comments.md'), comments);
85
+ const [, k, reviewed] = comments.match(/^- latest pass: (\d+) — (\S+) — /m) || [];
86
+ say(k ? `review: pass ${+k + 1}, later, since pass ${k} at ${reviewed} — comments.md carries it and what people wrote since` : 'review: pass 1, first');
87
+
88
+ // The diff, file by file in git's order, and the lines each changes: a finding outside them is not
89
+ // the merge request's.
90
+ const files = JSON.parse(must('git', 'diff', [mr.base, mr.sha]));
91
+ let diff = '';
92
+ const tsv = [];
93
+ for (const f of files) {
94
+ const text = must('git', 'diff_lines', [mr.base, mr.sha], nul(f.old, f.path));
95
+ diff += text;
96
+ for (const [, a, b = '1', c, d = '1'] of text.matchAll(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/gm)) {
97
+ if (+d) tsv.push(`${f.path}\tnew\t${c}\t${+c + +d - 1}`);
98
+ if (+b) tsv.push(`${f.path}\told\t${a}\t${+a + +b - 1}`);
99
+ }
100
+ }
101
+ writeFileSync(join(run, 'diff.diff'), diff);
102
+ writeFileSync(join(run, 'lines.tsv'), tsv.map((l) => `${l}\n`).join(''));
103
+ say(`diff: ${files.length} files in diff.diff, their changed lines in lines.tsv`);
104
+ // The submodules at the head: a file under one is another repository's, where no skill files (C-919).
105
+ const subs = JSON.parse(must('git', 'submodules', [mr.sha]));
106
+ if (subs.length) say(`submodules: ${subs.join(', ')} — a debt in a file under one is named, not filed (C-919)`);
107
+
108
+ // *How to test*, from the description's tagged region, by shape: a bullet opening on a backticked
109
+ // command is a command; a *manual* bullet with its indented lines is one step, a command inside it
110
+ // one of its actions; any other bullet is not runnable as written.
111
+ const region = mr.description.split('<!-- codelife:begin -->')[1]?.split('<!-- codelife:end -->')[0] ?? '';
112
+ const how = /^## How to test/m.test(region) ? section(region, 'How to test') : null;
113
+ const bullets = (how ?? '').split(/^(?=- )/m).filter((b) => b.startsWith('- '));
114
+ const commands = bullets.map((b) => b.match(/^- `([^`]+)`/)?.[1]).filter(Boolean);
115
+ const manual = bullets.filter((b) => /^- \*manual\*/.test(b));
116
+ const prose = bullets.filter((b) => !/^- (`|\*manual\*)/.test(b)).map((b) => b.split('\n')[0].slice(2).trim());
117
+ writeFileSync(join(run, 'commands.txt'), commands.map((c) => `${c}\n`).join(''));
118
+ writeFileSync(join(run, 'manual.md'), manual.map((b) => b.trimEnd()).join('\n\n') + (manual.length ? '\n' : ''));
119
+ say(how === null ? 'how to test: none in the description — `/mr` writes one'
120
+ : `how to test: ${commands.length} commands in commands.txt, ${manual.length} manual steps in manual.md${prose.length ? `; not runnable as written: ${prose.join(' | ')}` : ''}`);
121
+ // The running application a manual step's browser reaches — the contract's `app` block, which the
122
+ // session hands each reproduce agent with its step (reproduce.md R-143).
123
+ if (manual.length && field('kind') === 'app') say(field('app.url') ? `app: ${field('app.url')}, started by \`${field('app.start') || 'no command in the contract'}\` — never started by the review` : 'app: no `app` block in the contract — a browser step cannot reach the application');
124
+
125
+ // The known list: the open issues and the debt the description names — a defect one of them
126
+ // already carries is not filed again.
127
+ const open = lines(must('tracker', 'list'));
128
+ const debt = lines(section(mr.description, 'Debt noticed')).filter((l) => /^- #\d+/.test(l));
129
+ writeFileSync(join(run, 'known.txt'), [['## Open issues', open], ['## Debt noticed', debt.map((l) => l.slice(2))]]
130
+ .map(([h, l]) => [h, ...l].join('\n')).join('\n\n') + '\n');
131
+ say(`known list: known.txt — ${open.length} open issues, debt noticed ${debt.map((l) => l.match(/#\d+/)[0]).join(', ') || 'none'}`);
132
+
133
+ // The reader's case: the range, and what the merge request was meant to do — the issues its commits
134
+ // close, their umbrella and the identifiers each cites, each checked to resolve; the description,
135
+ // outside *How to test*, when it closes none (C-499).
136
+ const words = JSON.parse(must('git', 'log_since', [mr.base])).flatMap((c) => c.closes), number = /^#?\d+$/;
137
+ const closes = [...new Set(words.filter((w) => number.test(w)).map((w) => w.replace('#', '')))];
138
+ const said = [], unresolved = [...new Set(words.filter((w) => !number.test(w)))].map((w) => `Closes: ${w}`);
139
+ const umbrellas = new Set();
140
+ // known <id> — where it is defined under docs/spec/, empty when nowhere; a scan that fails stops.
141
+ const known = (id) => {
142
+ const r = spawn(process.execPath, [join(pkg, 'scripts/brainstorm/named.mjs'), id]);
143
+ if (r.status !== 0) stop(`scripts/brainstorm/named.mjs failed — ${(r.stderr || r.error?.message || '').trim()}`);
144
+ return r.stdout.trim();
145
+ };
146
+ for (const k of closes) {
147
+ const issue = JSON.parse(must('tracker', 'view', [k]));
148
+ const lot = issue.title.match(/\blot #(\d+)/)?.[1];
149
+ if (lot) umbrellas.add(lot);
150
+ const ids = [...new Set([...(issue.title.match(/\bspec:([A-Z]-\d+)/)?.slice(1) || []),
151
+ ...[...section(issue.body, 'Where it comes from').matchAll(/^(?:- )?([RC]-\d+) — /gm)].map((m) => m[1])])];
152
+ for (const id of ids) if (!known(id)) unresolved.push(`#${k} ${id}`);
153
+ said.push(`- #${k} — ${issue.title}${ids.length ? ` — cites ${ids.join(', ')}` : ' — cites nothing: its text is what was asked'}`);
154
+ }
155
+ const kase = [`Range: ${mr.base}...${mr.sha} — \`git diff ${mr.base}...${mr.sha}\` shows the change.`,
156
+ 'Command line: codelife — on your PATH: run each `codelife <verb>` as written.', ''];
157
+ if (closes.length) kase.push('The issues it closes — read each by section, its plan among them:', '', ...said,
158
+ ...(umbrellas.size ? ['', `The lot: ${[...umbrellas].map((u) => `#${u}`).join(', ')} — read its \`## Done when\`.`] : []));
159
+ else kase.push(`!${n} closes no issue: its description, outside *How to test*, is what was asked.`, '',
160
+ (region || mr.description).replace(/^## How to test[\s\S]*?(?=^## |(?![\s\S]))/m, '').trim());
161
+ writeFileSync(join(run, 'case.md'), `${kase.join('\n')}\n`);
162
+ say(`case: case.md — ${closes.length ? closes.map((k) => `#${k}`).join(', ') : 'no issue closed — the description is what was asked'}${unresolved.length ? `; resolves to nothing: ${unresolved.join(', ')}` : ''}`);
163
+ console.log(summary.join('\n'));