@ersbeth/codelife 0.0.0-stage → 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 +65 -2
  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 +38 -4
  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,8 @@
1
+ // tracker.mr.description(n): the merge request's description as written, nothing else — read where
2
+ // `mr.view`'s merge base is not needed, since on gh it costs a `mr.fetch_head` first.
3
+ // `node mr.description.mjs <n>`.
4
+ import { arg, cli, run } from './lib.mjs';
5
+
6
+ const n = arg(0, '<n>');
7
+ process.stdout.write(cli() === 'glab' ? JSON.parse(run(['mr', 'view', n, '-F', 'json'])).description || ''
8
+ : JSON.parse(run(['pr', 'view', n, '--json', 'body'])).body || '');
@@ -0,0 +1,5 @@
1
+ // tracker.mr.fetch_head(n): the merge request's head fetched — refs only, never a checkout.
2
+ import { arg, cli, git } from './lib.mjs';
3
+
4
+ const n = arg(0, '<n>');
5
+ git('fetch', '-q', 'origin', cli() === 'glab' ? `refs/merge-requests/${n}/head` : `refs/pull/${n}/head`);
@@ -0,0 +1,9 @@
1
+ // tracker.mr.find_for_branch(b): the merge requests whose source is that branch, in any state, one
2
+ // `<number> <open|merged|closed>` per line — none when there is none.
3
+ import { arg, cli, origin, run } from './lib.mjs';
4
+
5
+ const b = arg(0, '<branch>');
6
+ const STATE = { opened: 'open', open: 'open', merged: 'merged', closed: 'closed', locked: 'closed' };
7
+ const rows = cli() === 'glab' ? JSON.parse(run(['mr', 'list', '--source-branch', b, '--all', '-F', 'json'])).map((m) => [m.iid, m.state])
8
+ : JSON.parse(run(['pr', 'list', '--head', b, '--state', 'all', '--json', 'number,state'])).map((p) => [p.number, p.state]);
9
+ for (const [n, s] of rows) console.log(`${n} ${STATE[String(s).toLowerCase()] ?? s}`);
@@ -0,0 +1,40 @@
1
+ // `codelife mr post <n> <file>` (C-864, C-867): one new general comment on the merge request, its
2
+ // body the file as written — never edited afterwards, never typed. A body whose first line opens on
3
+ // `codelife:` and ends on a sha is about that head: when the merge request's head is another,
4
+ // nothing is posted, since a pass's numbers and an answer's hashes name lines of the head they read;
5
+ // and when that very comment is already there, a re-run's, nothing is posted either. On GitLab the
6
+ // comment is then resolved, a record being no thread. Prints the comment's link, read back by its body.
7
+ import { arg, body, cli, die, first, origin, run } from './lib.mjs';
8
+ import { lf } from '../text.mjs';
9
+
10
+ const n = arg(0, '<n> <file>');
11
+ const comment = body(arg(1, '<n> <file>'));
12
+ const glab = cli() === 'glab';
13
+ const line = first(comment).trimEnd();
14
+ // posted — that comment, as the forge stores it, latest first: its link, and on glab its note.
15
+ const same = (b) => lf(b).trim() === lf(comment).trim();
16
+ const posted = () => {
17
+ if (!glab) return { url: JSON.parse(run(['pr', 'view', n, '--json', 'comments'])).comments.filter((c) => same(c.body)).at(-1)?.url };
18
+ const { host, project } = origin();
19
+ const mine = JSON.parse(run(['mr', 'note', 'list', n, '-t', 'general', '-F', 'json']) || '[]')
20
+ .flatMap((d) => d.notes).filter((x) => !x.system && same(x.body)).at(-1);
21
+ return { url: mine && `https://${host}/${project}/-/merge_requests/${n}#note_${mine.id}`, note: mine };
22
+ };
23
+ const sha = line.match(/^codelife:.* — ([0-9a-f]{7,40})$/)?.[1];
24
+ if (sha) {
25
+ const head = glab ? JSON.parse(run(['mr', 'view', n, '-F', 'json'])).sha : JSON.parse(run(['pr', 'view', n, '--json', 'headRefOid'])).headRefOid;
26
+ if (!head.startsWith(sha)) die(`the merge request moved: its head is ${head}, the comment reads ${sha}; nothing posted`);
27
+ }
28
+ let found = sha ? posted() : {};
29
+ if (!found.url) {
30
+ if (glab) run(['mr', 'note', 'create', n, '-m', comment]);
31
+ else run(['pr', 'comment', n, '--body-file', '-'], comment);
32
+ found = posted();
33
+ }
34
+ if (!found.url) die('the comment was sent and does not read back');
35
+ // A record is no question: on GitLab a general note is a resolvable discussion, counted among the
36
+ // merge request's unresolved threads until someone resolves it, so its poster does (#1047) — and a
37
+ // reply under it then reads as a comment after the pass (#1097). The merge request first: the
38
+ // order glab 1.113.0 accepts, whatever its usage line says.
39
+ if (found.note?.resolvable && !found.note.resolved) run(['mr', 'note', 'resolve', n, String(found.note.id)]);
40
+ console.log(found.url);
@@ -0,0 +1,8 @@
1
+ // tracker.mr.update_description(n, file): the merge request's description replaced by the file as
2
+ // written; prints its link when the CLI answers one. `node mr.update_description.mjs <n> <file>`.
3
+ import { arg, body, cli, run } from './lib.mjs';
4
+
5
+ const n = arg(0, '<n> <file>'), file = arg(1, '<n> <file>');
6
+ const description = body(file);
7
+ const out = cli() === 'glab' ? run(['mr', 'update', n, '-d', description]) : run(['pr', 'edit', n, '--body-file', file]);
8
+ console.log(out.match(/https?:\/\/\S+/g)?.at(-1) ?? '');
@@ -0,0 +1,34 @@
1
+ // `codelife mr verdict <n> approve|request [<pass link>]` (C-866): the forge's own verdict — on GitLab
2
+ // the approval, or the reviewer state *requested changes*, which glab has no verb for and is read
3
+ // back, since the API answers 204 to an account that is no reviewer and changes nothing; on GitHub a
4
+ // review in the APPROVE or REQUEST_CHANGES state, its body the pass's link. An approval of one's own
5
+ // merge request is refused: it would count toward the forge's approval rules with nobody else having read it (R-292). /review-mr's
6
+ // preparation stops on one's own merge request first (work C-861); this is the verb's own
7
+ // refusal, for any other caller.
8
+ import { arg, call, cli, die, origin, run } from './lib.mjs';
9
+
10
+ const n = arg(0, '<n> approve|request [<pass link>]');
11
+ const verdict = arg(1, '<n> approve|request [<pass link>]');
12
+ if (!['approve', 'request'].includes(verdict)) die(`usage: codelife mr verdict <n> approve|request [<pass link>] — not '${verdict}'`);
13
+ const link = process.argv[4] || '';
14
+ if (cli() === 'glab') {
15
+ const me = JSON.parse(run(['api', 'user', '--hostname', origin().host]));
16
+ const m = JSON.parse(run(['mr', 'view', n, '-F', 'json']));
17
+ if (verdict === 'approve') {
18
+ if (m.author?.username === me.username) die(`!${n} is ${me.username}'s own: no approval is sent`);
19
+ run(['mr', 'approve', n]);
20
+ } else {
21
+ call(['mr', 'revoke', n]); // an approval of ours an earlier pass sent; none is no failure
22
+ const ids = (m.reviewers || []).map((r) => r.id);
23
+ if (!ids.includes(me.id)) run(['api', '-X', 'PUT', `projects/:id/merge_requests/${n}`, '-f', `reviewer_ids=${[...ids, me.id].join(',')}`]);
24
+ run(['api', '-X', 'POST', `projects/:id/merge_requests/${n}/draft_notes/bulk_publish`, '-f', 'reviewer_state=requested_changes']);
25
+ const state = JSON.parse(run(['api', `projects/:id/merge_requests/${n}/reviewers`])).find((r) => r.user?.id === me.id)?.state;
26
+ if (state !== 'requested_changes') die(`the request for changes did not hold: the account's reviewer state reads ${state || 'nothing'}`);
27
+ }
28
+ } else {
29
+ const me = run(['api', 'user', '--jq', '.login']).trim();
30
+ const author = JSON.parse(run(['pr', 'view', n, '--json', 'author'])).author?.login;
31
+ if (verdict === 'approve' && author === me) die(`#${n} is ${me}'s own: no approval is sent`);
32
+ run(['pr', 'review', n, verdict === 'approve' ? '--approve' : '--request-changes', '--body-file', '-'], link || (verdict === 'approve' ? 'Approved.' : 'Changes requested.'));
33
+ }
34
+ console.log(`${verdict === 'approve' ? 'approved' : 'changes requested'}: !${n}`);
@@ -0,0 +1,20 @@
1
+ // tracker.mr.view(n): the merge request as JSON — sha, base, source, target, author, reviewers, draft, state,
2
+ // description. On gh there is no base sha: the merge base is computed from the target the forge
3
+ // named, fetched from it first — the clone's own copy may be stale or missing —, after
4
+ // tracker.mr.fetch_head.
5
+ import { arg, cli, git, run } from './lib.mjs';
6
+
7
+ const n = arg(0, '<n>');
8
+ let view;
9
+ if (cli() === 'glab') {
10
+ const m = JSON.parse(run(['mr', 'view', n, '-F', 'json']));
11
+ view = { sha: m.sha, base: m.diff_refs?.base_sha, source: m.source_branch, target: m.target_branch, author: m.author?.username,
12
+ reviewers: (m.reviewers || []).map((r) => r.username), draft: !!m.draft, state: m.state, description: m.description || '' };
13
+ } else {
14
+ const m = JSON.parse(run(['pr', 'view', n, '--json', 'headRefOid,headRefName,baseRefName,author,isDraft,state,body,reviewRequests']));
15
+ git('fetch', '-q', 'origin', `+refs/heads/${m.baseRefName}:refs/remotes/origin/${m.baseRefName}`);
16
+ view = { sha: m.headRefOid, base: git('merge-base', `origin/${m.baseRefName}`, m.headRefOid).trim(), source: m.headRefName, target: m.baseRefName,
17
+ author: m.author?.login, reviewers: (m.reviewRequests || []).map((r) => r.login), draft: !!m.isDraft, state: m.state,
18
+ description: m.body || '' };
19
+ }
20
+ console.log(JSON.stringify(view));
@@ -0,0 +1,79 @@
1
+ // What `tracker.kind: none` reads: `issues/NNN-<slug>.md`, one issue per file (none-fallback.md),
2
+ // as the JSON a forge's view gives — title, body, state, labels, milestone, its notes as comments in
3
+ // order, and no history of its state; and the two writes a close makes, each changing its part of
4
+ // the file and nothing else.
5
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { die, word } from './lib.mjs';
7
+ import { lf } from '../text.mjs';
8
+
9
+ // issues — every file of issues/; none before the first issue, which creates the folder (#1067).
10
+ const issues = () => {
11
+ if (!existsSync('issues')) return [];
12
+ return readdirSync('issues').filter((f) => /^\d+-.*\.md$/.test(f)).sort().map((f) => {
13
+ const [, head = '', rest = ''] = lf(readFileSync(`issues/${f}`, 'utf8')).match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/) ?? [];
14
+ const meta = Object.fromEntries([...head.matchAll(/^(\w+):[ \t]*(.*?)[ \t]*(?:#.*)?$/gm)].map((m) => [m[1], m[2]]));
15
+ const [, title = '', after = ''] = rest.match(/^\n*# (.*)\n([\s\S]*)$/) ?? [];
16
+ const [body, notes = ''] = after.split(/^## Notes[ \t]*$/m);
17
+ return { id: Number(meta.id), path: `issues/${f}`, meta, title, body: body.replace(/^\n+|\s+$/g, ''), notes };
18
+ });
19
+ };
20
+
21
+ // open — `#<id>\t<title>` for each issue still open, or every one when `all`.
22
+ export const lines = (all) => issues().filter((i) => all || i.meta.status === 'open').map((i) => `#${i.id}\t${i.title}`);
23
+
24
+ // matching — `#<id>\t<title>` for each open issue whose file holds the term as a whole word.
25
+ export const matching = (term) => {
26
+ const holds = word(term);
27
+ return issues().filter((i) => i.meta.status === 'open' && holds(readFileSync(i.path, 'utf8'))).map((i) => `#${i.id}\t${i.title}`);
28
+ };
29
+
30
+ const one = (n) => issues().find((x) => x.id === Number(n)) ?? die(`no issue ${n} under issues/`);
31
+
32
+ export const view = (n) => {
33
+ const i = one(n);
34
+ const labels = [i.meta.nature, i.meta.severity && `severity-${i.meta.severity}`, i.meta.lot === 'true' && 'lot', i.meta.status === 'wontfix' && 'wontfix'].filter(Boolean);
35
+ const comments = i.notes.split(/^### /m).slice(1).map((s) => {
36
+ const [first, ...text] = s.split('\n');
37
+ const [, date = '', line = first] = first.match(/^(\d{4}-\d{2}-\d{2}) — (.*)$/) ?? [];
38
+ return { author: '', date, body: [line, ...text].join('\n').trim() };
39
+ });
40
+ return { title: i.title, body: i.body, state: i.meta.status === 'open' ? 'opened' : 'closed', labels, milestone: i.meta.milestone || '', comments, events: [] };
41
+ };
42
+
43
+ // close — the comment appended as a dated section under `## Notes`, then the status set, in that order.
44
+ // note — one dated section appended under `## Notes`, opened when the file has none.
45
+ export const note = (n, comment) => {
46
+ const i = one(n);
47
+ appendFileSync(i.path, `${/^## Notes[ \t]*$/m.test(readFileSync(i.path, 'utf8')) ? '' : '\n## Notes\n'}\n### ${new Date().toISOString().slice(0, 10)} — ${comment}\n`);
48
+ return i;
49
+ };
50
+ export const close = (n, comment, wontfix) => {
51
+ const i = note(n, comment);
52
+ writeFileSync(i.path, readFileSync(i.path, 'utf8').replace(/^status:[ \t]*\w+/m, `status: ${wontfix ? 'wontfix' : 'closed'}`));
53
+ };
54
+
55
+ // create — a new file after the highest id, `status: open`, created today; returns its path.
56
+ export const create = (title, body, nature, severity) => {
57
+ const id = Math.max(0, ...issues().map((i) => i.id)) + 1;
58
+ const slug = title.replace(/^\[[^\]]*\]\s*/, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50).replace(/-$/, '');
59
+ const path = `issues/${String(id).padStart(3, '0')}-${slug}.md`;
60
+ mkdirSync('issues', { recursive: true });
61
+ writeFileSync(path, `---\nid: ${id}\nstatus: open\nnature: ${nature}\n${severity ? `severity: ${severity}\n` : ''}created: ${new Date().toISOString().slice(0, 10)}\n---\n# ${title}\n\n${body}\n`);
62
+ return path;
63
+ };
64
+
65
+ // retitle — the `# ` line after the frontmatter replaced, nothing else.
66
+ export const retitle = (n, title) => {
67
+ const i = one(n), text = readFileSync(i.path, 'utf8'), at = text.indexOf(`\n# ${i.title}\n`, text.indexOf('\n---\n') + 4);
68
+ if (at < 0) die(`${i.path}: no title to replace`);
69
+ writeFileSync(i.path, `${text.slice(0, at)}\n# ${title}\n${text.slice(at + i.title.length + 4)}`);
70
+ };
71
+
72
+ // update — the body replaced where it stands, after the title, so a frontmatter value or a title the
73
+ // body repeats is never what is replaced (#1093); the notes untouched.
74
+ export const update = (n, body) => {
75
+ const i = one(n), text = readFileSync(i.path, 'utf8');
76
+ const at = i.body ? text.indexOf(i.body, text.indexOf(`\n# ${i.title}\n`, text.indexOf('\n---\n') + 4) + i.title.length + 3) : -1;
77
+ if (at < 0) die(`${i.path}: no body to replace`);
78
+ writeFileSync(i.path, `${text.slice(0, at)}${body}${text.slice(at + i.body.length)}`);
79
+ };
@@ -0,0 +1,12 @@
1
+ // tracker.note(n, file): one new comment on the issue, read from its file — the plan posted before
2
+ // the first edit, or a STATUS at a stop — never a subagent's (R-290). `codelife issue note <n> <comment file>`.
3
+ import { arg, body, cli, kind, run } from './lib.mjs';
4
+ import { note } from './none.mjs';
5
+
6
+ const usage = '<n> <comment file>';
7
+ const n = arg(0, usage), file = arg(1, usage);
8
+ const comment = body(file);
9
+ if (kind() === 'none') note(n, comment);
10
+ else if (cli() === 'glab') run(['issue', 'note', n, '-m', comment]);
11
+ else run(['issue', 'comment', n, '--body-file', file]);
12
+ console.log(`noted #${n}`);
@@ -0,0 +1,8 @@
1
+ // tracker.reopen(n): the issue reopened — a held choice the user refused, after its `refused` note,
2
+ // never a `wontfix`. Refused under `tracker.kind: none`, as every write `none.mjs` does not carry: a
3
+ // held choice is validated by its push, and there is no merge request to push it with. `node reopen.mjs <n>`.
4
+ import { arg, run } from './lib.mjs';
5
+
6
+ const n = arg(0, '<n>');
7
+ run(['issue', 'reopen', n]);
8
+ console.log(`reopened #${n}`);
@@ -0,0 +1,8 @@
1
+ // tracker.search, one term at a time — the issues that match it, one line each, every page:
2
+ // `node search.mjs <term file> [--all]`. The term arrives by file and goes to the CLI as one argument,
3
+ // with no shell to run a backtick span or a `$(…)` it quotes; `--all` adds the closed issues, as
4
+ // `tracker.search_id` wants them. The ranking across terms is `rank`'s (lib.mjs).
5
+ import { arg, matching, text } from './lib.mjs';
6
+
7
+ const term = text(arg(0, '<term file> [--all]'));
8
+ process.stdout.write(matching(term, process.argv[3] === '--all').map((l) => `${l}\n`).join(''));
@@ -0,0 +1,18 @@
1
+ // tracker.search_id(<key>, all?): the open issues — every one with `--all` — whose title's tag holds
2
+ // the key whole, one line each: search.mjs (under `none`, issues/ titles) piped into title.search_id_filter.mjs. `node search_id.mjs <key file> [--all]`; a failure of either is the operation's.
3
+ import { spawnSync } from 'node:child_process';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { arg, die, kind } from './lib.mjs';
7
+ import { lines } from './none.mjs';
8
+
9
+ const key = arg(0, '<key file> [--all]');
10
+ const step = (script, args, input = '') => {
11
+ const r = spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), script), ...args], { encoding: 'utf8', input, maxBuffer: 256 * 1024 * 1024 });
12
+ if (r.status !== 0) die((r.stderr || r.stdout).trim());
13
+ return r.stdout;
14
+ };
15
+ const all = process.argv[3] === '--all';
16
+ // Under `none` the listing is issues/'s own titles; on a forge, search.mjs's.
17
+ const listing = kind() === 'none' ? lines(all).join('\n') : step('search.mjs', process.argv.slice(2));
18
+ process.stdout.write(step('title.search_id_filter.mjs', [key], listing));
@@ -0,0 +1,35 @@
1
+ <!-- generated by codelife — edit the plugin, not this file -->
2
+ <!-- codelife: name="Bug" about="Something behaves differently from what it promises" labels="bug" -->
3
+ <!-- Title: the symptom as someone meets it, in one short sentence that fits a line of the issue list. The cause and the mechanism go in the body. -->
4
+ <!-- Body: written for a reader who knows the language and the forge, not this repository. A term the project coined is explained in a few words where it first appears, and linked to the document that defines it; a decision is restated in a sentence, never cited by its number alone. Every link is a full URL: a relative one resolves against the issue's page, not the repository. The length is what that reader needs, and no more. -->
5
+
6
+ One or two sentences that stand on their own: what goes wrong, for whom. A reader who stops here
7
+ knows what the issue is about.
8
+
9
+ ## Context
10
+
11
+ The part of the project involved, what it is for, and what it promises — enough for someone new to
12
+ the repository to follow the next section.
13
+
14
+ ## What goes wrong
15
+
16
+ The behaviour observed against the behaviour promised, with a concrete example. Name the promise —
17
+ a documented API, a test, a stated invariant — rather than an expectation.
18
+
19
+ ## How to see it
20
+
21
+ The shortest path to the behaviour: a command, a snippet, a test that fails. If the reproduction
22
+ would be an exploitation path on a public repository, say so here and leave it out — it belongs
23
+ in the conversation with the maintainer, not in a public issue.
24
+
25
+ ## Where it comes from
26
+
27
+ One sentence saying what the title's origin points at, linked: the audit finding, the merge
28
+ request, the issue it was noticed on — or the module, the commit or the release where it appears.
29
+
30
+ From the specification, it cites the rule and, under it, one line per case the issue covers —
31
+ their headings as on the day it is filed, a snapshot nobody corrects, since the identifier still
32
+ resolves — then the link to the document, there for the click and no part of the address:
33
+
34
+ R-<n> — <heading>
35
+ - C-<n> — <heading>
@@ -0,0 +1,40 @@
1
+ <!-- generated by codelife — edit the plugin, not this file -->
2
+ <!-- codelife: name="Feature" about="Something the repository should do and does not yet" labels="feature" -->
3
+ <!-- Title: the symptom as someone meets it, in one short sentence that fits a line of the issue list. The cause and the mechanism go in the body. -->
4
+ <!-- Body: written for a reader who knows the language and the forge, not this repository. A term the project coined is explained in a few words where it first appears, and linked to the document that defines it; a decision is restated in a sentence, never cited by its number alone. Every link is a full URL: a relative one resolves against the issue's page, not the repository. The length is what that reader needs, and no more. -->
5
+
6
+ One or two sentences that stand on their own: what is missing, for whom. A reader who stops here
7
+ knows what the issue is about.
8
+
9
+ ## Context
10
+
11
+ The part of the project involved, what it is for, and what it promises — enough for someone new to
12
+ the repository to follow the next sections.
13
+
14
+ ## What is missing
15
+
16
+ The capability, from the point of view of whoever needs it — a consumer of the library, a
17
+ contributor, an agent working the repository — and what it makes possible that is not possible
18
+ today.
19
+
20
+ ## What it would change
21
+
22
+ The public surface, the configuration, the documentation — anything a consumer would notice, and
23
+ whether it breaks one.
24
+
25
+ ## What it is not
26
+
27
+ The nearest thing this issue deliberately does not cover, so that scope creep has a line to cross
28
+ visibly. A rejected alternative and its reason belong here too.
29
+
30
+ ## Where it comes from
31
+
32
+ One sentence saying what the title's origin points at, linked: the audit finding, the feature
33
+ document, the merge request or the issue it was noticed on.
34
+
35
+ From the specification, it cites the rule and, under it, one line per case the issue covers —
36
+ their headings as on the day it is filed, a snapshot nobody corrects, since the identifier still
37
+ resolves — then the link to the document, there for the click and no part of the address:
38
+
39
+ R-<n> — <heading>
40
+ - C-<n> — <heading>
@@ -0,0 +1,46 @@
1
+ <!-- generated by codelife — edit the plugin, not this file -->
2
+ <!-- codelife: name="Lot" about="An umbrella issue carrying the remediation order of a lot" labels="lot" -->
3
+
4
+ <!--
5
+ The umbrella of a lot. Title: [lot · <origin>] <name> — audit:A<n>-lot-<X> for an audit's lot,
6
+ spec:C-<n> for a specification's, its first case — an older one's spec:J-<n> or spec:K-<n> —,
7
+ citation for the citation lot, triage:<slug> for a triage's. `codelife issue create` writes
8
+ the tag itself: the `# ` line of the file it is handed carries the name alone.
9
+ The name is the symptom the members share, in one short sentence that fits a line of the
10
+ issue list; the cause goes in the body.
11
+ One lot is one merge request on one lot/ branch, so a lot that no longer fits one
12
+ merge request is two lots.
13
+ -->
14
+ <!-- Body: written for a reader who knows the language and the forge, not this repository. A term the project coined is explained in a few words where it first appears, and linked to the document that defines it; a decision is restated in a sentence, never cited by its number alone. Every link is a full URL: a relative one resolves against the issue's page, not the repository. The length is what that reader needs, and no more. -->
15
+
16
+ One or two sentences that stand on their own: the symptom these issues share, and what landing
17
+ them restores. A reader who stops here knows what the lot is about.
18
+
19
+ ## Context
20
+
21
+ The common cause, explained with one concrete example — a root cause, a subsystem, a promise to
22
+ restore. Then one sentence per member saying how it is an instance of that cause. A lot whose
23
+ members share only their severity is not a lot.
24
+
25
+ ## The order
26
+
27
+ The remediation order, and it is an order: each line is ticked by `codelife issue close` as it closes.
28
+ Leave it empty here — every row is appended by `codelife lot join`, the only writer of this list,
29
+ so a number is read from the member it joined rather than typed before that member exists.
30
+
31
+ Effort is `S` / `M` / `L`, proposed by the audit's lens subagent and corrected by hand here. It moves as the work is understood, which is why it is a
32
+ line in this list and not a label.
33
+
34
+ ## Dependencies
35
+
36
+ What must land before this lot starts, inside or outside the repository. *None* is an answer.
37
+
38
+ ## Done when
39
+
40
+ The observable state that closes this umbrella — a suite green, a promise restored, a drift
41
+ measured back to zero. Not "all the issues are closed": that is the task list's job.
42
+
43
+ ## Where it comes from
44
+
45
+ One sentence saying what the title's origin points at, linked: the audit's plan, the feature
46
+ document, or the triage that grouped these issues.
@@ -0,0 +1,6 @@
1
+ // A member's title once it joins lot #n: `node title.join.mjs <n> < <title file>`.
2
+ import { arg, text } from './lib.mjs';
3
+
4
+ const [n, title] = [arg(0, '<n> < <title file>'), text(0)];
5
+ const close = title.indexOf(']');
6
+ console.log(title.startsWith('[') && title.startsWith('] ', close) ? `${title.slice(0, close)} · lot #${n}]${title.slice(close + 1)}` : `[lot #${n}] ${title}`);
@@ -0,0 +1,7 @@
1
+ // A member's title once it leaves lot #n, only the half `title.join.mjs` wrote:
2
+ // `node title.leave.mjs <n> < <title file>`.
3
+ import { arg, text } from './lib.mjs';
4
+
5
+ const [n, title] = [arg(0, '<n> < <title file>'), text(0)];
6
+ const bare = title.replace(` · lot #${n}]`, () => ']');
7
+ console.log(bare.startsWith(`[lot #${n}] `) ? bare.slice(`[lot #${n}] `.length) : bare);
@@ -0,0 +1,8 @@
1
+ // The lot a member's title names — nothing for an umbrella or an orphan:
2
+ // `node title.lot_of.mjs < <title file>`.
3
+ import { text } from './lib.mjs';
4
+
5
+ const title = text(0);
6
+ const tag = title.startsWith('[') && title.includes(']') ? title.slice(0, title.indexOf(']')) : '';
7
+ if (tag.startsWith('[lot #')) console.log(tag.slice('[lot #'.length));
8
+ else if (tag.includes(' · lot #')) console.log(tag.slice(tag.lastIndexOf(' · lot #') + ' · lot #'.length));
@@ -0,0 +1,17 @@
1
+ // The titles of a list, one per line on stdin, whose leading tag holds the key whole — `[`, `:` or a
2
+ // space before it, `]` or a space after — so `A3-PKG-2` is not `A3-PKG-21` and a key cited in the
3
+ // sentence is no hit: `node title.search_id_filter.mjs <key file> < <list>`.
4
+ import { arg, text } from './lib.mjs';
5
+
6
+ const key = text(arg(0, '<key file> < <list>'));
7
+ const whole = (tag) => {
8
+ for (let i = tag.indexOf(key); key && i >= 0; i = tag.indexOf(key, i + 1)) {
9
+ if (i > 0 && '[: '.includes(tag[i - 1]) && '] '.includes(tag[i + key.length] ?? '\0')) return true;
10
+ }
11
+ return false;
12
+ };
13
+ for (const line of text(0).split('\n')) {
14
+ const b = line.indexOf('[');
15
+ const e = b < 0 ? -1 : line.indexOf(']', b);
16
+ if (e >= 0 && whole(line.slice(b, e + 1))) console.log(line);
17
+ }
@@ -0,0 +1,5 @@
1
+ // A title without its tag: `node title.sentence_of.mjs < <title file>`.
2
+ import { text } from './lib.mjs';
3
+
4
+ const title = text(0);
5
+ console.log(title.startsWith('[') && title.includes('] ') ? title.slice(title.indexOf('] ') + 2) : title);
@@ -0,0 +1,25 @@
1
+ // The issue as JSON — title, body, state, labels, milestone, its comments oldest first (lib.mjs's
2
+ // `comments`) and its state events, each close and reopening with its date: on glab from their own
3
+ // endpoint, a close being no note; on gh the events API. Under `none`, the file, with no history of
4
+ // its state. `--bare` reads neither comments nor events, one forge call: what a lot's join and leave read.
5
+ import { arg, cli, comments, commentsOf, kind, ndjson, origin, run } from './lib.mjs';
6
+ import { view } from './none.mjs';
7
+ import { lf } from '../text.mjs';
8
+
9
+ const n = arg(0, '<n> [--bare]'), bare = process.argv[3] === '--bare';
10
+ let issue;
11
+ if (kind() === 'none') issue = view(n);
12
+ else if (cli() === 'glab') {
13
+ const i = JSON.parse(run(['issue', 'view', n, '-F', 'json']));
14
+ issue = { title: i.title, body: i.description || '', state: i.state, labels: i.labels || [], milestone: i.milestone?.title || '' };
15
+ if (!bare) Object.assign(issue, { comments: commentsOf(run(comments(n))),
16
+ events: ndjson(run(['api', `projects/:id/issues/${n}/resource_state_events`, '--paginate', '--output', 'ndjson']))
17
+ .map((e) => ({ state: e.state, date: e.created_at })) });
18
+ } else {
19
+ const out = run(['issue', 'view', n, '--json', `title,body,state,labels,milestone${bare ? '' : ',comments'}`]), i = JSON.parse(out);
20
+ issue = { title: i.title, body: i.body || '', state: i.state, labels: (i.labels || []).map((l) => l.name), milestone: i.milestone?.title || '' };
21
+ if (!bare) Object.assign(issue, { comments: commentsOf(out),
22
+ events: JSON.parse(run(['api', `repos/${origin().project}/issues/${n}/events`, '--paginate', '--slurp'])).flat()
23
+ .filter((e) => e.event === 'closed' || e.event === 'reopened').map((e) => ({ state: e.event, date: e.created_at })) });
24
+ }
25
+ console.log(JSON.stringify(issue, (_, v) => (typeof v === 'string' ? lf(v) : v)));