@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.
- package/.claude-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/agents/evaluate.md +97 -0
- package/agents/ground.md +49 -0
- package/agents/implementer.md +49 -0
- package/agents/judge.md +128 -0
- package/agents/planner.md +45 -0
- package/agents/prerequisites.md +62 -0
- package/agents/reader.md +40 -0
- package/agents/reproduce.md +65 -0
- package/bin/codelife +4 -0
- package/package.json +38 -4
- package/presets/tsconfig.json +14 -0
- package/presets/vite.config.d.mts +3 -0
- package/presets/vite.config.mjs +49 -0
- package/scripts/audit/cadence.mjs +24 -0
- package/scripts/audit/close.mjs +57 -0
- package/scripts/audit/file.mjs +70 -0
- package/scripts/audit/fold.mjs +48 -0
- package/scripts/audit/lib.mjs +93 -0
- package/scripts/audit/prepare.mjs +180 -0
- package/scripts/audit/table.mjs +65 -0
- package/scripts/brainstorm/decided.mjs +6 -0
- package/scripts/brainstorm/lib.mjs +33 -0
- package/scripts/brainstorm/named.mjs +9 -0
- package/scripts/changelog.mjs +100 -0
- package/scripts/check-tag.mjs +19 -0
- package/scripts/checks.mjs +53 -0
- package/scripts/codelife.mjs +42 -0
- package/scripts/commit-msg.mjs +43 -0
- package/scripts/contract.mjs +179 -0
- package/scripts/former-form.mjs +170 -0
- package/scripts/git/branches.mjs +6 -0
- package/scripts/git/commit.mjs +40 -0
- package/scripts/git/diff.mjs +17 -0
- package/scripts/git/diff_lines.mjs +5 -0
- package/scripts/git/held.mjs +53 -0
- package/scripts/git/is_ancestor.mjs +28 -0
- package/scripts/git/lib.mjs +20 -0
- package/scripts/git/log_since.mjs +27 -0
- package/scripts/git/parent.mjs +51 -0
- package/scripts/git/show.mjs +4 -0
- package/scripts/git/status.mjs +25 -0
- package/scripts/git/submodules.mjs +5 -0
- package/scripts/init.mjs +21 -0
- package/scripts/install.mjs +32 -0
- package/scripts/lenses/A11Y.md +91 -0
- package/scripts/lenses/ARCH.md +90 -0
- package/scripts/lenses/BUG.md +52 -0
- package/scripts/lenses/BUILD.md +40 -0
- package/scripts/lenses/COMPAT.md +67 -0
- package/scripts/lenses/CONF.md +49 -0
- package/scripts/lenses/DEPS.md +43 -0
- package/scripts/lenses/DOC.md +73 -0
- package/scripts/lenses/FAIL.md +57 -0
- package/scripts/lenses/GOV.md +56 -0
- package/scripts/lenses/GUARD.md +47 -0
- package/scripts/lenses/INTENT.md +52 -0
- package/scripts/lenses/LEAN.md +49 -0
- package/scripts/lenses/LINES.md +48 -0
- package/scripts/lenses/PERF.md +61 -0
- package/scripts/lenses/PKG.md +58 -0
- package/scripts/lenses/SEC.md +49 -0
- package/scripts/lenses/STANDARDS.md +62 -0
- package/scripts/lenses/TYPE.md +56 -0
- package/scripts/lenses/VERIF.md +81 -0
- package/scripts/link.mjs +40 -0
- package/scripts/methods/cause.md +21 -0
- package/scripts/methods/comments.md +157 -0
- package/scripts/methods/description.md +67 -0
- package/scripts/methods/message.md +24 -0
- package/scripts/methods/red-green.md +28 -0
- package/scripts/methods/refusal.md +22 -0
- package/scripts/mr/open.mjs +219 -0
- package/scripts/node-floor.mjs +19 -0
- package/scripts/onboard/prep.mjs +93 -0
- package/scripts/overview/codelife-line.mjs +33 -0
- package/scripts/overview/next.mjs +76 -0
- package/scripts/overview/overview.mjs +308 -0
- package/scripts/package-manager.mjs +20 -0
- package/scripts/port/item.mjs +94 -0
- package/scripts/port/lib.mjs +194 -0
- package/scripts/port/prep.mjs +33 -0
- package/scripts/port/version.mjs +50 -0
- package/scripts/pre-commit.mjs +43 -0
- package/scripts/pre-push.mjs +53 -0
- package/scripts/prepare-lib.mjs +81 -0
- package/scripts/read-roadmap/lib.mjs +116 -0
- package/scripts/read-roadmap/moved.mjs +70 -0
- package/scripts/read-roadmap/range.mjs +15 -0
- package/scripts/release/cut.mjs +120 -0
- package/scripts/release/entry.mjs +52 -0
- package/scripts/release/lib.mjs +136 -0
- package/scripts/release/prep.mjs +58 -0
- package/scripts/review-mr/prepare.mjs +163 -0
- package/scripts/roadmap/assemble.mjs +269 -0
- package/scripts/roadmap/cut.mjs +88 -0
- package/scripts/roadmap/prepare.mjs +249 -0
- package/scripts/roadmap/roadmap.md +30 -0
- package/scripts/roadmap/write.mjs +53 -0
- package/scripts/search.mjs +29 -0
- package/scripts/setup/contract.mjs +25 -0
- package/scripts/setup/item.mjs +166 -0
- package/scripts/setup/lib.mjs +205 -0
- package/scripts/setup/prep.mjs +106 -0
- package/scripts/spec-form.mjs +444 -0
- package/scripts/spec.mjs +56 -0
- package/scripts/tags.mjs +27 -0
- package/scripts/target/target-app.md +134 -0
- package/scripts/target/target-base.md +127 -0
- package/scripts/target/target-lib.md +143 -0
- package/scripts/target/templates/audit/README.md +21 -0
- package/scripts/target/templates/characterisation/bundle.test.ts +43 -0
- package/scripts/target/templates/characterisation/declarations.test.ts +44 -0
- package/scripts/target/templates/characterisation/entries.ts +50 -0
- package/scripts/target/templates/characterisation/examples.test.ts +18 -0
- package/scripts/target/templates/characterisation/import.test.ts +22 -0
- package/scripts/target/templates/characterisation/surface.test.ts +68 -0
- package/scripts/target/templates/cliff.toml +61 -0
- package/scripts/target/templates/githooks/commit-msg +9 -0
- package/scripts/target/templates/githooks/pre-commit +9 -0
- package/scripts/target/templates/githooks/pre-push +9 -0
- package/scripts/target/templates/playground/index.html +11 -0
- package/scripts/target/templates/playground/main.ts +9 -0
- package/scripts/target/templates/spec/README.md +35 -0
- package/scripts/target/templates/spec/capability.md +36 -0
- package/scripts/target/templates/spec/glossary.md +5 -0
- package/scripts/target/templates/spec/journey.md +29 -0
- package/scripts/target/templates/spec/principles.md +15 -0
- package/scripts/target/templates/spec/users.md +15 -0
- package/scripts/target/templates/tsconfig.json +9 -0
- package/scripts/target/templates/vite.config.ts +4 -0
- package/scripts/text.mjs +6 -0
- package/scripts/tracker/account.mjs +5 -0
- package/scripts/tracker/auth.mjs +9 -0
- package/scripts/tracker/cli.version.mjs +5 -0
- package/scripts/tracker/close.mjs +20 -0
- package/scripts/tracker/create.mjs +47 -0
- package/scripts/tracker/issue.section.mjs +25 -0
- package/scripts/tracker/issue.update.mjs +13 -0
- package/scripts/tracker/issue.view.mjs +16 -0
- package/scripts/tracker/lib.mjs +130 -0
- package/scripts/tracker/list.mjs +14 -0
- package/scripts/tracker/list.whole.mjs +20 -0
- package/scripts/tracker/lot.row.mjs +42 -0
- package/scripts/tracker/mr.checkout_command.mjs +5 -0
- package/scripts/tracker/mr.comments.mjs +50 -0
- package/scripts/tracker/mr.create.mjs +12 -0
- package/scripts/tracker/mr.description.mjs +8 -0
- package/scripts/tracker/mr.fetch_head.mjs +5 -0
- package/scripts/tracker/mr.find_for_branch.mjs +9 -0
- package/scripts/tracker/mr.post.mjs +40 -0
- package/scripts/tracker/mr.update_description.mjs +8 -0
- package/scripts/tracker/mr.verdict.mjs +34 -0
- package/scripts/tracker/mr.view.mjs +20 -0
- package/scripts/tracker/none.mjs +79 -0
- package/scripts/tracker/note.mjs +12 -0
- package/scripts/tracker/reopen.mjs +8 -0
- package/scripts/tracker/search.mjs +8 -0
- package/scripts/tracker/search_id.mjs +18 -0
- package/scripts/tracker/templates/bug.md +35 -0
- package/scripts/tracker/templates/feature.md +40 -0
- package/scripts/tracker/templates/lot.md +46 -0
- package/scripts/tracker/title.join.mjs +6 -0
- package/scripts/tracker/title.leave.mjs +7 -0
- package/scripts/tracker/title.lot_of.mjs +8 -0
- package/scripts/tracker/title.search_id_filter.mjs +17 -0
- package/scripts/tracker/title.sentence_of.mjs +5 -0
- package/scripts/tracker/view.mjs +25 -0
- package/scripts/verbs.mjs +579 -0
- package/scripts/verify/BUILD.mjs +33 -0
- package/scripts/verify/DEPS.mjs +15 -0
- package/scripts/verify/GOV.mjs +34 -0
- package/scripts/verify/GUARD.mjs +34 -0
- package/scripts/verify/LINES.mjs +22 -0
- package/scripts/verify/PKG.mjs +56 -0
- package/scripts/verify/TYPE.mjs +34 -0
- package/scripts/verify/cut.mjs +73 -0
- package/scripts/verify/history.mjs +32 -0
- package/scripts/verify/lib.mjs +129 -0
- package/scripts/verify/report.mjs +104 -0
- package/scripts/verify/timing.mjs +19 -0
- package/scripts/verify.mjs +29 -0
- package/scripts/work/branch.mjs +81 -0
- package/scripts/work/close.mjs +74 -0
- package/scripts/work/member.mjs +73 -0
- package/scripts/work/prepare.mjs +116 -0
- package/scripts/work/reopen.mjs +40 -0
- package/scripts/work-feedback/prepare.mjs +96 -0
- package/skills/audit/SKILL.md +110 -0
- package/skills/audit/templates/index.md +36 -0
- package/skills/audit/templates/lens.md +44 -0
- package/skills/brainstorm/SKILL.md +144 -0
- package/skills/brainstorm/references/exit.md +57 -0
- package/skills/brainstorm/templates/note.md +79 -0
- package/skills/commit/SKILL.md +68 -0
- package/skills/mr/SKILL.md +82 -0
- package/skills/new-issue/SKILL.md +87 -0
- package/skills/onboard/SKILL.md +103 -0
- package/skills/onboard/templates/CLAUDE.md +31 -0
- package/skills/onboard/templates/CONTRIBUTING.md +34 -0
- package/skills/onboard/templates/README.md +30 -0
- package/skills/onboard/templates/SKILL.md +24 -0
- package/skills/onboard/templates/package-CLAUDE.md +15 -0
- package/skills/overview/SKILL.md +59 -0
- package/skills/port-toolchain/SKILL.md +127 -0
- package/skills/release/SKILL.md +78 -0
- package/skills/review-mr/SKILL.md +106 -0
- package/skills/review-mr/templates/pass.md +29 -0
- package/skills/roadmap/SKILL.md +137 -0
- package/skills/setup/SKILL.md +112 -0
- package/skills/work/SKILL.md +139 -0
- package/skills/work-feedback/SKILL.md +113 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// The branch a piece of work belongs on, derived once (R-192): `lot <n>` is lot #n's,
|
|
2
|
+
// an existing `lot/<n>-*` or one derived from the umbrella's sentence; `issue <n>` is that issue's,
|
|
3
|
+
// its lot's when its title names one, else its own existing one, else by where you stand. Reads only
|
|
4
|
+
// — nothing is created or switched. A summary of `- branch:`, `- from:` for an existing branch — the
|
|
5
|
+
// base, a resume's too — or a branch to cut, and `- develop:` for a cut from it; a branch that cannot be named, or a `develop`
|
|
6
|
+
// its origin does not hold whole, prints `- stop:` and exits 1. `node <this directory>/branch.mjs
|
|
7
|
+
// lot|issue <n>`, from the repository's root.
|
|
8
|
+
import { prep, said } from '../prepare-lib.mjs';
|
|
9
|
+
|
|
10
|
+
const [mode, n] = process.argv.slice(2);
|
|
11
|
+
if (!['lot', 'issue'].includes(mode) || !/^\d+$/.test(n || '')) {
|
|
12
|
+
console.error('usage: node branch.mjs lot|issue <number>');
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
const { say, stop, done, op, must, title, slug, kind } = prep('branch');
|
|
16
|
+
const tree = JSON.parse(must('git', 'status'));
|
|
17
|
+
const view = (k) => JSON.parse(must('tracker', 'view', [k]));
|
|
18
|
+
|
|
19
|
+
// A branch cut from `develop` is cut from what `origin/develop` holds whole, a forge being there to
|
|
20
|
+
// carry the difference into a merge request.
|
|
21
|
+
const held = () => {
|
|
22
|
+
if (kind() === 'none') return;
|
|
23
|
+
const r = op('git', 'is_ancestor', ['develop', 'origin/develop']);
|
|
24
|
+
const it = r.stdout.trim();
|
|
25
|
+
if (it === 'no origin') say('develop: no origin, nothing compared');
|
|
26
|
+
else if (r.status !== 0 || it !== 'contained') stop(`develop is not held whole by origin/develop (${it || said(r)}): \`git log origin/develop..develop\` to read what it holds that origin lacks, then \`git push -u origin develop\` after a pull when the two diverged`);
|
|
27
|
+
else say('develop: contained in origin/develop');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// The work branch already named for it, local or origin's, whatever its title became since; two is a stop.
|
|
31
|
+
const work = /^(feature|fix|hotfix|lot)\//;
|
|
32
|
+
const existing = (glob, what) => {
|
|
33
|
+
const names = [...new Set(must('git', 'branches', [glob]).split('\n').filter(Boolean).map((b) => b.replace(/^origin\//, '')))].filter((b) => work.test(b));
|
|
34
|
+
if (names.length > 1) stop(`two branches for ${what}: ${names.join(', ')} — which one is right is yours to say`);
|
|
35
|
+
return names[0];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// The lot's: an existing branch, or one cut from develop, its base either way; you stand on no other lot's.
|
|
39
|
+
const lot = (k) => {
|
|
40
|
+
const found = existing(`lot/${k}-*`, `lot #${k}`);
|
|
41
|
+
const branch = found ?? `lot/${k}-${slug(view(k).title)}`;
|
|
42
|
+
say(`branch: ${branch} — ${found ? 'exists' : 'does not exist'}`);
|
|
43
|
+
say('from: develop');
|
|
44
|
+
if (!found) held();
|
|
45
|
+
if (/^lot\//.test(tree.branch) && tree.branch !== branch) stop(`you stand on ${tree.branch}, another lot's branch: two lots on one branch is two merge requests that cannot be separated`);
|
|
46
|
+
done();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (mode === 'lot') lot(n);
|
|
50
|
+
const issue = view(n);
|
|
51
|
+
const named = title('title.lot_of.mjs', issue.title);
|
|
52
|
+
if (named) lot(named);
|
|
53
|
+
|
|
54
|
+
// No lot: a branch whose work is merged — its merge request, or its commits in a line — is no branch to work on — derive as if standing on develop.
|
|
55
|
+
const parent = op('git', 'parent');
|
|
56
|
+
let finished = parent.status === 0 && parent.stdout.startsWith('finished');
|
|
57
|
+
let why = parent.stdout.trim();
|
|
58
|
+
// A squash leaves no commit of its own in a line, which git.parent cannot see: the forge says it, where there is one.
|
|
59
|
+
if (!finished && kind() !== 'none' && work.test(tree.branch)) {
|
|
60
|
+
const merged = must('tracker', 'mr.find_for_branch', [tree.branch]).split('\n').find((l) => l.endsWith(' merged'));
|
|
61
|
+
if (merged) [finished, why] = [true, `merge request ${merged.split(' ')[0]} merged`];
|
|
62
|
+
}
|
|
63
|
+
const here = finished ? 'develop' : tree.branch;
|
|
64
|
+
if (finished) say(`${tree.branch} is finished (${why}): derived as if standing on develop`);
|
|
65
|
+
// Its own branch, where a resume or a delegated run's return goes, wherever you stand.
|
|
66
|
+
const own = existing(`*/${n}-*`, `#${n}`);
|
|
67
|
+
if (own) {
|
|
68
|
+
say(`branch: ${own} — exists`);
|
|
69
|
+
say(`from: ${own.startsWith('hotfix/') ? 'main' : 'develop'}`);
|
|
70
|
+
done();
|
|
71
|
+
}
|
|
72
|
+
const cut = (kind, from) => {
|
|
73
|
+
say(`branch: ${kind}/${n}-${slug(issue.title)} — does not exist`);
|
|
74
|
+
say(`from: ${from}`);
|
|
75
|
+
if (from === 'develop') held();
|
|
76
|
+
};
|
|
77
|
+
if (here === 'develop') cut(issue.labels.includes('bug') ? 'fix' : 'feature', 'develop');
|
|
78
|
+
else if (here === 'main' || here.startsWith('support/')) cut('hotfix', here);
|
|
79
|
+
else if (work.test(here)) say(`branch: ${here} — in place`);
|
|
80
|
+
else stop(`you stand on ${here}, neither a line nor a work branch: say where this work belongs`);
|
|
81
|
+
done();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// /work's close, one command (R-193): nothing reaches the forge unless every commit the closing
|
|
2
|
+
// comment cites is one HEAD contains and the commit step made for this issue — its `Closes:` footer
|
|
3
|
+
// names it, or, for `Already fixed in <hash> (#<earlier>)` — the hash with backticks on both sides or
|
|
4
|
+
// none — its `Closes:` or `Refs:` footer names that earlier issue, of any lot, and the tracker reads it
|
|
5
|
+
// closed; or, with `--wontfix`, it adds to docs/spec/ the refusal line the comment quotes. Then the comment and the close through scripts/tracker/close.mjs, and the line of the issue in its
|
|
6
|
+
// umbrella's task list ticked, with any row whose issue is already closed; the umbrella closed when
|
|
7
|
+
// the issue has its row and no member is open. A refusal prints `- stop:` and exits 1, nothing posted.
|
|
8
|
+
// `codelife issue close <issue> <comment file> <hash>… [--wontfix]`, from the repository's root.
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { closeLot, currentBranch, prep, said, tasks } from '../prepare-lib.mjs';
|
|
14
|
+
|
|
15
|
+
const [n, file, ...rest] = process.argv.slice(2);
|
|
16
|
+
const wontfix = rest.includes('--wontfix'), hashes = rest.filter((a) => a !== '--wontfix');
|
|
17
|
+
if (!/^\d+$/.test(n || '') || !file || !hashes.length || !hashes.every((h) => /^[0-9a-f]{7,40}$/.test(h))) {
|
|
18
|
+
console.error('usage: codelife issue close <issue> <comment file> <hash>… [--wontfix]');
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
const { say, stop, done, op, must, title } = prep('close');
|
|
22
|
+
|
|
23
|
+
let comment;
|
|
24
|
+
try { comment = readFileSync(file, 'utf8'); } catch (e) { stop(`the comment cannot be read — ${e.message}`); }
|
|
25
|
+
for (const h of hashes) {
|
|
26
|
+
if (!comment.includes(h)) stop(`the comment does not cite ${h}; nothing posted`);
|
|
27
|
+
// The commits from its parent to HEAD hold it only when HEAD contains it.
|
|
28
|
+
const r = op('git', 'log_since', [`${h}~1`]);
|
|
29
|
+
const c = r.status === 0 && JSON.parse(r.stdout).find((x) => x.sha.startsWith(h));
|
|
30
|
+
if (!c) stop(`${h} is not a commit HEAD contains${r.status ? ` — ${said(r)}` : ''}; nothing posted`);
|
|
31
|
+
const owner = comment.match(new RegExp(`Already fixed in (\`?)${h}[0-9a-f]*\\1 \\(#(\\d+)\\)`))?.[2] ?? n;
|
|
32
|
+
// Another issue's commit, of this lot or an earlier one, may cite it in `Refs:` alone (R-85); the issue's own must close it.
|
|
33
|
+
// A refusal another run already wrote (K-8's R-185): the commit adding to docs/spec/ the line the comment quotes.
|
|
34
|
+
const wrote = () => wontfix && owner === n && spawnSync('git', ['show', '--format=', '--unified=0', h, '--', 'docs/spec'], { encoding: 'utf8' }).stdout
|
|
35
|
+
.split('\n').some((l) => /^\+- \S/.test(l) && comment.includes(l.slice(1)));
|
|
36
|
+
const cites = c.closes.includes(`#${owner}`) ? 'closes' : owner !== n && c.refs.includes(`#${owner}`) ? 'cites' : wrote() ? 'wrote the refusal line quoted for' : '';
|
|
37
|
+
if (!cites) {
|
|
38
|
+
stop(`${h} (${c.subject}) closes ${c.closes.join(' ') || 'no issue'}${owner === n ? '' : ` and cites ${c.refs.join(' ') || 'no issue'}`}, not #${owner}: it is not the commit the commit step made; nothing posted`);
|
|
39
|
+
}
|
|
40
|
+
// `Refs:` is the footer of an issue that stays open (R-85), and a `Closes:` commit may be an abandoned one: an earlier issue must be closed.
|
|
41
|
+
if (owner !== n && !/^closed$/i.test(JSON.parse(must('tracker', 'view', [owner])).state)) {
|
|
42
|
+
stop(`${h} names #${owner}, which is not closed: the commit is not yet the fix; nothing posted`);
|
|
43
|
+
}
|
|
44
|
+
say(`commit: ${h} — ${c.subject} — ${cites} #${owner}, in HEAD`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Everything the close needs is read before anything is written: a view that fails stops the run
|
|
48
|
+
// with nothing posted, never with the issue closed and its umbrella left behind.
|
|
49
|
+
const issue = JSON.parse(must('tracker', 'view', [n]));
|
|
50
|
+
const lot = title('title.lot_of.mjs', issue.title);
|
|
51
|
+
const { body = '', state: lotState = '' } = lot ? JSON.parse(must('tracker', 'view', [lot])) : {};
|
|
52
|
+
const row = (m, box) => new RegExp(`^- \\[${box}\\] #${m}\\b`, 'm');
|
|
53
|
+
const ticked = row(n, '[xX]').test(body), unticked = row(n, ' ').test(body);
|
|
54
|
+
// Every other row's state read from its issue, never from its tick (R-193): a closed member whose
|
|
55
|
+
// line is unticked is ticked with this one — the reconciliation — and none still open closes the
|
|
56
|
+
// umbrella on its `lot-closing` comment, unless this issue has no row there to close it with.
|
|
57
|
+
const others = tasks(body).filter((t) => t.n !== n).map((t) => ({ ...t, closed: /^closed$/i.test(JSON.parse(must('tracker', 'view', [t.n])).state) }));
|
|
58
|
+
const tick = [...(unticked ? [n] : []), ...others.filter((t) => t.closed && row(t.n, ' ').test(body)).map((t) => t.n)];
|
|
59
|
+
const dir = mkdtempSync(join(tmpdir(), 'close-'));
|
|
60
|
+
let after = body;
|
|
61
|
+
for (const m of tick) {
|
|
62
|
+
after = title('lot.row.mjs', after, 'tick', m);
|
|
63
|
+
if (!after) stop(`#${lot}'s task list cannot be ticked — scripts/tracker/lot.row.mjs failed; nothing written`);
|
|
64
|
+
}
|
|
65
|
+
say(must('tracker', 'close', [n, file, ...(wontfix ? ['wontfix'] : [])]).trim());
|
|
66
|
+
if (!lot) {
|
|
67
|
+
say('umbrella: none');
|
|
68
|
+
done();
|
|
69
|
+
}
|
|
70
|
+
if (tick.length) { writeFileSync(join(dir, 'body.md'), after); must('tracker', 'issue.update', [lot, join(dir, 'body.md')]); }
|
|
71
|
+
const left = others.filter((t) => !t.closed).map((t) => `#${t.n}`);
|
|
72
|
+
say(`umbrella: #${lot} — #${n}'s line ${unticked ? 'ticked' : ticked ? 'already ticked' : 'not in its task list'}; still open: ${left.join(', ') || 'none'}${ticked || unticked ? '' : ', so the umbrella stays open'}`);
|
|
73
|
+
if (!left.length && (ticked || unticked)) say(`umbrella: ${closeLot(must, lot, lotState, tasks(body).map((t) => t.n), '/work', currentBranch())}, with its last member`);
|
|
74
|
+
done();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// `codelife lot join <umbrella> <n>[:S|M|L]…` and `codelife lot leave <umbrella> <n> <reason file>`
|
|
2
|
+
// (R-251, R-202): a member's lot written in the two places it lives, as one operation — its
|
|
3
|
+
// title's lot half, through scripts/tracker/title.*.mjs, and its row of the umbrella's `## The order`, through `lot.row.mjs`; a leave also opens the member's body on `Left lot #<umbrella> on <date>:
|
|
4
|
+
// <reason>`, and closes the umbrella when no row is left open. A join refuses a member of another
|
|
5
|
+
// lot, whose title would name two; its members' rows are appended in the order given, the umbrella
|
|
6
|
+
// read once and written once (#1136). Every issue is viewed bare, its comments unread. Every new text
|
|
7
|
+
// is computed before the first write, and none is written when one fails.
|
|
8
|
+
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { closeLot, prep, tasks } from '../prepare-lib.mjs';
|
|
12
|
+
|
|
13
|
+
const [mode, u, ...rest] = process.argv.slice(2);
|
|
14
|
+
const usage = () => {
|
|
15
|
+
console.error('usage: codelife lot join <umbrella> <n>[:S|M|L]... | codelife lot leave <umbrella> <n> <reason file>');
|
|
16
|
+
process.exit(2);
|
|
17
|
+
};
|
|
18
|
+
// A join's members in order, each `<n>[:S|M|L]` — or `<n> S|M|L`, the one-member form's effort.
|
|
19
|
+
const members = [];
|
|
20
|
+
for (const a of mode === 'join' ? rest : []) {
|
|
21
|
+
const m = a.match(/^(\d+)(?::([SML]))?$/);
|
|
22
|
+
if (m) members.push({ n: m[1], effort: m[2] });
|
|
23
|
+
else if (/^[SML]$/.test(a) && members.length && !members.at(-1).effort) members.at(-1).effort = a;
|
|
24
|
+
else usage();
|
|
25
|
+
}
|
|
26
|
+
const [n, reasonFile] = mode === 'leave' ? rest : [];
|
|
27
|
+
if (!/^\d+$/.test(u || '') || (mode === 'join' ? !members.length : mode !== 'leave' || !/^\d+$/.test(n || '') || !reasonFile)) usage();
|
|
28
|
+
const { say, stop, done, op, must, title } = prep(`lot ${mode}`);
|
|
29
|
+
const view = (k) => JSON.parse(must('tracker', 'view', [k, '--bare'])), umbrella = view(u);
|
|
30
|
+
const dir = mkdtempSync(join(tmpdir(), 'member-')), file = (name, text) => { writeFileSync(join(dir, name), text); return join(dir, name); };
|
|
31
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
32
|
+
const cannot = (k) => stop(`#${u}'s task list or #${k}'s title cannot be written — scripts/tracker/title.*.mjs failed; nothing written`);
|
|
33
|
+
|
|
34
|
+
let body = umbrella.body;
|
|
35
|
+
const issues = []; // { n, from, named, memberBody } — each member's write, in order
|
|
36
|
+
if (mode === 'join') {
|
|
37
|
+
for (const { n, effort } of members) {
|
|
38
|
+
const member = view(n), lot = title('title.lot_of.mjs', member.title);
|
|
39
|
+
if (lot && lot !== u) stop(`#${n} is a member of lot #${lot} — \`codelife lot leave ${lot} ${n}\` first; nothing written`);
|
|
40
|
+
const named = lot ? member.title : title('title.join.mjs', member.title, u);
|
|
41
|
+
body = body && title('lot.row.mjs', body, 'add', n, file(`sentence-${n}`, title('title.sentence_of.mjs', member.title)), ...(effort ? [effort] : []));
|
|
42
|
+
if (!named || !body) cannot(n);
|
|
43
|
+
issues.push({ n, from: member.title, named, memberBody: '-' });
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
let reason; // one line, wherever it is written
|
|
47
|
+
try { reason = readFileSync(reasonFile, 'utf8').trim().replace(/\s+/g, ' '); } catch (e) { stop(`the reason cannot be read — ${e.message}`); }
|
|
48
|
+
const member = view(n), inLot = title('title.lot_of.mjs', member.title) === u;
|
|
49
|
+
const named = inLot ? title('title.leave.mjs', member.title, u) : member.title;
|
|
50
|
+
body = title('lot.row.mjs', umbrella.body, 'leave', n, date, file('reason', reason));
|
|
51
|
+
if (!named || !body) cannot(n);
|
|
52
|
+
issues.push({ n, from: member.title, named, memberBody: inLot ? file('member.md', `Left lot #${u} on ${date}: ${reason}\n\n${member.body}`) : '-' });
|
|
53
|
+
}
|
|
54
|
+
// A leave that leaves no row open closes the open umbrella (R-70): otherwise a lot with nothing to work.
|
|
55
|
+
// A row whose issue cannot be viewed is not known closed, so it keeps the umbrella open and stops nothing.
|
|
56
|
+
const changed = body !== umbrella.body.trim(), rows = tasks(body).map((t) => t.n), closed = (s) => /^closed$/i.test(s);
|
|
57
|
+
const rowClosed = (m) => { const r = op('tracker', 'view', [m, '--bare']); return r.status === 0 && closed(JSON.parse(r.stdout).state); };
|
|
58
|
+
const emptied = mode === 'leave' && rows.every(rowClosed);
|
|
59
|
+
if (emptied) { // every remaining row is closed: ticked in the same body update, as `issue close` does (R-193)
|
|
60
|
+
for (const m of rows.filter((m) => new RegExp(`^- \\[ \\] #${m}\\b`, 'm').test(body))) {
|
|
61
|
+
body = title('lot.row.mjs', body, 'tick', m);
|
|
62
|
+
if (!body) stop(`#${u}'s task list cannot be ticked — scripts/tracker/lot.row.mjs failed; nothing written`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const i of issues) {
|
|
67
|
+
if (i.named !== i.from || i.memberBody !== '-') must('tracker', 'issue.update', [i.n, i.memberBody, file(`title-${i.n}`, i.named)]);
|
|
68
|
+
say(`member: #${i.n} — ${i.named}`);
|
|
69
|
+
}
|
|
70
|
+
if (body !== umbrella.body.trim()) must('tracker', 'issue.update', [u, file('body.md', body)]);
|
|
71
|
+
say(`umbrella: #${u} — ${issues.map((i) => `#${i.n}`).join(', ')}'s row${issues.length > 1 ? 's' : ''} ${mode === 'join' ? 'appended' : 'removed'}${changed ? '' : ', already so'}`);
|
|
72
|
+
if (emptied) say(`umbrella: ${closeLot(must, u, umbrella.state, rows, 'codelife lot leave', null)}, no member left open`);
|
|
73
|
+
done();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// /work's preparation, one command (R-288): the forge's authentication, then the number read as an
|
|
2
|
+
// umbrella — an issue labelled `lot` — or a lone issue. An umbrella's task list, each member's state
|
|
3
|
+
// and `plan` comment, the orphans its `debt-link` comments point at; a lone issue's evidence and its
|
|
4
|
+
// last plan or refusal; a lot's member typed alone stops, naming its umbrella (C-892); a closed one
|
|
5
|
+
// whose branch exists has only its merge request left (R-295). Then the
|
|
6
|
+
// moved-rule check, the tree and the branch — every forge and git read through the scripts of
|
|
7
|
+
// `scripts/tracker/`, `scripts/git/` and `scripts/read-roadmap/` — and a summary printed, which the brief and the table are made
|
|
8
|
+
// from. A precondition or a read that fails prints `- stop:` and exits 1, before any table; nothing
|
|
9
|
+
// reads stdin. `node <this directory>/prepare.mjs <umbrella-or-issue>`, from the repository's root.
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { prep, said, first, gist, tasks as tasksOf } from '../prepare-lib.mjs';
|
|
12
|
+
|
|
13
|
+
const n = process.argv[2];
|
|
14
|
+
if (!/^\d+$/.test(n || '')) {
|
|
15
|
+
console.error('usage: node prepare.mjs <umbrella-or-issue-number>');
|
|
16
|
+
process.exit(2);
|
|
17
|
+
}
|
|
18
|
+
const { summary, say, stop, done, include, op, must, title, kind } = prep('prepare');
|
|
19
|
+
|
|
20
|
+
// The contract and the forge: a forge that does not answer is an issue that cannot be read. Under
|
|
21
|
+
// `none` the issues are files and there is no one to authenticate with.
|
|
22
|
+
const forge = kind();
|
|
23
|
+
if (forge === 'none') say('forge: none — the issues folder, no authentication');
|
|
24
|
+
else {
|
|
25
|
+
const auth = op('tracker', 'auth');
|
|
26
|
+
if (auth.status !== 0) stop(`the forge's authentication — ${said(auth)}`);
|
|
27
|
+
say(`forge: ${forge} — ${auth.stdout.trim()}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A repository that cannot run one test is not ported (R-190): no fix without its regression test.
|
|
31
|
+
let pkg = {};
|
|
32
|
+
try { pkg = JSON.parse(readFileSync('package.json', 'utf8')); } catch {}
|
|
33
|
+
if (!pkg.scripts?.['test:one']) stop('no `test:one` script: this repository cannot run one test — `/setup`, then `/port-toolchain`');
|
|
34
|
+
|
|
35
|
+
// The tree, the branch you stand on, and the one the work belongs on, by the branch script.
|
|
36
|
+
const branch = (mode) => {
|
|
37
|
+
const tree = JSON.parse(must('git', 'status'));
|
|
38
|
+
const dirty = [...tree.staged, ...tree.modified, ...tree.untracked];
|
|
39
|
+
if (dirty.length) stop(`the tree is not clean: ${[...new Set(dirty)].join(', ')}`);
|
|
40
|
+
include(op('work', 'branch', [mode, n]));
|
|
41
|
+
say(`standing on: ${tree.branch}, the tree clean`);
|
|
42
|
+
done();
|
|
43
|
+
};
|
|
44
|
+
// A closed issue or umbrella has nothing left to work, but its branch may still hold a merge request
|
|
45
|
+
// to open: a delegated run's return, its held choices put to you there (R-295). No branch, a stop.
|
|
46
|
+
const finished = (mode) => {
|
|
47
|
+
if (!/^- branch: .* — exists$/m.test(op('work', 'branch', [mode, n]).stdout)) stop(`#${n} is closed — nothing to work`);
|
|
48
|
+
say(`closed: #${n} — nothing left to work but the merge request`);
|
|
49
|
+
branch(mode);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const issue = JSON.parse(must('tracker', 'view', [n]));
|
|
53
|
+
// What an issue's comments say of its plan: the last plan, refusal or closing — the one reader both
|
|
54
|
+
// paths call, so a refused member is named as a refused lone issue is. On an issue still open, a last
|
|
55
|
+
// closing is a delegated run's STATUS: its stop said, after the plan or refusal it stopped in.
|
|
56
|
+
const planOf = (comments) => {
|
|
57
|
+
const marks = comments.filter((c) => /^codelife: (plan|refused|closing) — /.test(first(c.body)))
|
|
58
|
+
.map((c) => { const [kind, , date = 'an undated run'] = first(c.body).slice(10).split(' — '); return { kind, date, body: c.body }; });
|
|
59
|
+
const state = (m) => m && { plan: `plan of ${m.date}`, refused: `refused on ${m.date}, after any plan` }[m.kind];
|
|
60
|
+
const last = marks.at(-1);
|
|
61
|
+
return last?.kind !== 'closing' ? state(last) ?? 'no plan'
|
|
62
|
+
: [state(marks.findLast((m) => m.kind !== 'closing')), `stopped — ${gist(last.body) ?? 'no reason given'}`].filter(Boolean).join(', ');
|
|
63
|
+
};
|
|
64
|
+
if (!issue.labels.includes('lot')) {
|
|
65
|
+
// A lone issue, a lot of one (R-199): no order, no line to tick, its branch derived (R-192).
|
|
66
|
+
const lot = title('title.lot_of.mjs', issue.title);
|
|
67
|
+
if (lot) stop(`#${n} is a member of lot #${lot}, whose order is the umbrella's — \`/work ${lot}\``);
|
|
68
|
+
if (tasksOf(issue.body).length) stop(`#${n} carries a task list but no \`lot\` label — label it, then \`/work ${n}\``);
|
|
69
|
+
if (/^closed$/i.test(issue.state)) finished('issue');
|
|
70
|
+
say(`issue: #${n}, lone — ${issue.title} — ${issue.labels.join(', ') || 'no label'} — ${issue.milestone || 'no milestone'}`);
|
|
71
|
+
// What the issue already carries: a campaign's finding, and the last of its plan, refusal or closing.
|
|
72
|
+
const finding = issue.comments.map((c) => first(c.body)).find((l) => /^codelife: finding — /.test(l));
|
|
73
|
+
say(finding ? `evidence: the finding comment (${finding.split(' — ').at(2) ?? 'undated'})` : 'evidence: the issue\'s "Where it comes from"');
|
|
74
|
+
say(`plan: ${planOf(issue.comments)}`);
|
|
75
|
+
const moved = op('read-roadmap', 'moved', [n]);
|
|
76
|
+
for (const l of moved.stdout.split('\n').filter((l) => /^- (roadmap|mark|moved #\d+): /.test(l))) summary.push(l);
|
|
77
|
+
if (moved.status !== 0) stop(/^- moved #/m.test(moved.stdout) ? 'a rule the issue cites moved, or could not be checked — `/roadmap` first' : `scripts/read-roadmap/moved.mjs failed — ${said(moved)}`);
|
|
78
|
+
branch('issue');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The umbrella, its task list in order, and each member's state and plan.
|
|
82
|
+
const umbrella = issue;
|
|
83
|
+
if (/^closed$/i.test(umbrella.state)) finished('lot');
|
|
84
|
+
const tasks = tasksOf(umbrella.body);
|
|
85
|
+
if (!tasks.length) stop(`#${n} carries no task list: a lot whose order was never decided`);
|
|
86
|
+
const members = tasks.map((t) => ({ ...t, ...JSON.parse(must('tracker', 'view', [t.n])) }));
|
|
87
|
+
const closed = members.filter((m) => /^closed$/i.test(m.state));
|
|
88
|
+
say(`umbrella: #${n} — ${umbrella.title} — ${closed.length} of ${members.length} members closed`);
|
|
89
|
+
|
|
90
|
+
// The orphans a `debt-link` on the umbrella points at: open, titled as the link says, naming no lot.
|
|
91
|
+
const joins = [];
|
|
92
|
+
for (const c of umbrella.comments.filter((c) => first(c.body).startsWith('codelife: debt-link — '))) {
|
|
93
|
+
const [, k, named] = c.body.match(/^Filed as [^#\s]*#(\d+) — (.+?)\.?$/m) ?? [];
|
|
94
|
+
if (!k || members.some((m) => m.n === k)) continue;
|
|
95
|
+
const o = JSON.parse(must('tracker', 'view', [k]));
|
|
96
|
+
if (!/^closed$/i.test(o.state) && o.title === named && !title('title.lot_of.mjs', o.title)) joins.push({ n: k, title: o.title });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Each open member's moved-rule check, in one call: a rule moved stops the run before its table.
|
|
100
|
+
const open = members.filter((m) => !closed.includes(m));
|
|
101
|
+
const moved = open.length ? op('read-roadmap', 'moved', open.map((m) => m.n)) : { status: 0, stdout: '' };
|
|
102
|
+
const verdict = Object.fromEntries([...moved.stdout.matchAll(/^- moved #(\d+): (.*)$/gm)].map((m) => [m[1], m[2]]));
|
|
103
|
+
for (const l of moved.stdout.split('\n').filter((l) => /^- (roadmap|mark): /.test(l))) summary.push(l);
|
|
104
|
+
for (const m of members) {
|
|
105
|
+
say(closed.includes(m) ? `member: #${m.n} (${m.effort || 'no effort'}) — closed, skipped — ${m.title}`
|
|
106
|
+
: `member: #${m.n} (${m.effort || 'no effort'}) — open — ${planOf(m.comments)} — moved: ${verdict[m.n] ?? 'not read'} — ${m.title}`);
|
|
107
|
+
}
|
|
108
|
+
for (const j of joins) say(`join: #${j.n}, last — ${j.title}`);
|
|
109
|
+
if (moved.status !== 0) {
|
|
110
|
+
const stopped = open.filter((m) => !verdict[m.n] || verdict[m.n].startsWith('stops'));
|
|
111
|
+
stop(stopped.length && Object.keys(verdict).length ? `a rule moved, or could not be checked, for ${stopped.map((m) => `#${m.n}`).join(', ')} — \`/roadmap\` first`
|
|
112
|
+
: `read-roadmap's moved failed — ${said(moved)}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The lot's branch: an existing `lot/<n>-*`, or one derived from the umbrella's sentence.
|
|
116
|
+
branch('lot');
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// /work's reopen, one command — the inverse of `issue close`: a held choice the user refused at /work
|
|
2
|
+
// (R-295). The comment first, so a reopen cut midway leaves the reason and never a silently reopened
|
|
3
|
+
// issue; then the issue reopened, its line unticked in its umbrella, and the umbrella reopened when
|
|
4
|
+
// its last close closed it. Everything is read before anything is written. A refusal prints `- stop:`
|
|
5
|
+
// and exits 1, nothing posted. `codelife issue reopen <issue> <comment file>`, from the repository's root.
|
|
6
|
+
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { prep } from '../prepare-lib.mjs';
|
|
10
|
+
|
|
11
|
+
const [n, file, ...rest] = process.argv.slice(2);
|
|
12
|
+
if (!/^\d+$/.test(n || '') || !file || rest.length) {
|
|
13
|
+
console.error('usage: codelife issue reopen <issue> <comment file>');
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
const { say, stop, done, must, title, kind } = prep('reopen');
|
|
17
|
+
if (kind() === 'none') stop('tracker.kind: none — a held choice is validated by its push, and there is no merge request to push it with; nothing posted');
|
|
18
|
+
try { readFileSync(file, 'utf8'); } catch (e) { stop(`the comment cannot be read — ${e.message}`); }
|
|
19
|
+
|
|
20
|
+
const closed = (s) => /^closed$/i.test(s);
|
|
21
|
+
const issue = JSON.parse(must('tracker', 'view', [n]));
|
|
22
|
+
const lot = title('title.lot_of.mjs', issue.title);
|
|
23
|
+
const umbrella = lot ? JSON.parse(must('tracker', 'view', [lot])) : null;
|
|
24
|
+
const body = umbrella ? title('lot.row.mjs', umbrella.body, 'untick', n) : '';
|
|
25
|
+
if (umbrella && !body) stop(`#${lot}'s task list cannot be unticked — scripts/tracker/lot.row.mjs failed; nothing posted`);
|
|
26
|
+
|
|
27
|
+
say(must('tracker', 'note', [n, file]).trim());
|
|
28
|
+
say(closed(issue.state) ? must('tracker', 'reopen', [n]).trim() : `#${n} already open`);
|
|
29
|
+
if (!umbrella) {
|
|
30
|
+
say('umbrella: none');
|
|
31
|
+
done();
|
|
32
|
+
}
|
|
33
|
+
if (body !== umbrella.body.trim()) {
|
|
34
|
+
const f = join(mkdtempSync(join(tmpdir(), 'reopen-')), 'body.md');
|
|
35
|
+
writeFileSync(f, body);
|
|
36
|
+
must('tracker', 'issue.update', [lot, f]);
|
|
37
|
+
say(`umbrella: #${lot} — #${n}'s line unticked`);
|
|
38
|
+
} else say(`umbrella: #${lot} — #${n}'s line already unticked, or not in its task list`);
|
|
39
|
+
if (closed(umbrella.state)) say(`umbrella: ${must('tracker', 'reopen', [lot]).trim()}`);
|
|
40
|
+
done();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// /work-feedback's preparation, `codelife feedback prep [<n>]` (K-13): the merge request — the
|
|
2
|
+
// argument, or the one open for the current branch —, the account its author, HEAD on its branch at
|
|
3
|
+
// its head or ahead of it, the tree clean; then the rows left to answer — the latest pass's numbers
|
|
4
|
+
// still open, the comments people wrote after it and the threads they opened —, less those an answer
|
|
5
|
+
// already carries, each one a commit's `Answers:` trailer names marked with that commit. A
|
|
6
|
+
// precondition that fails prints `- stop:` and exits 1. From the repository's root.
|
|
7
|
+
//
|
|
8
|
+
// The run folder, outside the tree: rows.md, one `### <number or link>` section per row, whole.
|
|
9
|
+
import '../node-floor.mjs';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const pkg = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
17
|
+
let n = process.argv[2];
|
|
18
|
+
if (n !== undefined && !/^\d+$/.test(n)) {
|
|
19
|
+
console.error('usage: codelife feedback prep [<mr-number>]');
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
const summary = ['### prepare'];
|
|
23
|
+
const say = (line) => summary.push(`- ${line}`);
|
|
24
|
+
const stop = (why) => {
|
|
25
|
+
say(`stop: ${why}`);
|
|
26
|
+
console.log(summary.join('\n'));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
};
|
|
29
|
+
// must <domain> <operation> <args> — that script's stdout, or the stop naming it and its error.
|
|
30
|
+
const must = (domain, operation, args = []) => {
|
|
31
|
+
const r = spawnSync(process.execPath, [join(pkg, 'scripts', domain, `${operation}.mjs`), ...args], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
32
|
+
if (r.status !== 0) stop(`${domain} ${operation} failed — ${(r.stderr || r.stdout).trim()}`);
|
|
33
|
+
return r.stdout;
|
|
34
|
+
};
|
|
35
|
+
const lines = (text) => text.split('\n').filter(Boolean);
|
|
36
|
+
|
|
37
|
+
// Where you are: the account, the merge request, the branch, the head and the tree.
|
|
38
|
+
const account = must('tracker', 'account').trim();
|
|
39
|
+
if (n === undefined) {
|
|
40
|
+
const branch = spawnSync('git', ['branch', '--show-current'], { encoding: 'utf8' }).stdout.trim();
|
|
41
|
+
const open = branch ? lines(must('tracker', 'mr.find_for_branch', [branch])).filter((l) => l.endsWith(' open')) : [];
|
|
42
|
+
if (open.length !== 1) stop(`no number given, and ${branch ? `${branch} has ${open.length} open merge requests` : 'HEAD is on no branch'} — \`/work-feedback <n>\``);
|
|
43
|
+
n = open[0].split(' ')[0];
|
|
44
|
+
say(`number: !${n}, the open merge request of ${branch}`);
|
|
45
|
+
}
|
|
46
|
+
const mr = JSON.parse(must('tracker', 'mr.view', [n]));
|
|
47
|
+
say(`merge request: !${n} — ${mr.source} into ${mr.target}, head ${mr.sha}, merge base ${mr.base}, author ${mr.author}, state ${mr.state}`);
|
|
48
|
+
if (!/^open/i.test(mr.state)) stop(`!${n} is ${mr.state}: there is nothing left to answer on it`);
|
|
49
|
+
if (mr.author !== account) stop(`!${n} is ${mr.author}'s, not ${account}'s — its author answers its review; /review-mr reviews another person's`);
|
|
50
|
+
const checkout = () => must('tracker', 'mr.checkout_command', [n]).trim();
|
|
51
|
+
const tree = JSON.parse(must('git', 'status'));
|
|
52
|
+
if (tree.branch !== mr.source) stop(`HEAD is on ${tree.branch}, not ${mr.source} — \`${checkout()}\` reaches it`);
|
|
53
|
+
if (spawnSync('git', ['merge-base', '--is-ancestor', mr.sha, 'HEAD']).status !== 0) stop(`HEAD ${tree.oid} does not contain the head ${mr.sha} — \`${checkout()}\` reaches it`);
|
|
54
|
+
const dirty = [...tree.staged, ...tree.modified, ...tree.untracked];
|
|
55
|
+
if (dirty.length) stop(`the tree is not clean: ${dirty.join(', ')}`);
|
|
56
|
+
const ahead = +spawnSync('git', ['rev-list', '--count', `${mr.sha}..HEAD`], { encoding: 'utf8' }).stdout.trim();
|
|
57
|
+
say(`branch: ${mr.source}, clean, ${ahead ? `${ahead} commits ahead of the head, waiting to be pushed` : 'at the head'}`);
|
|
58
|
+
|
|
59
|
+
// What was said: the latest pass, the answers to it, the comments and the open threads after it.
|
|
60
|
+
const said = must('tracker', 'mr.comments', [n]);
|
|
61
|
+
const [, k, read] = said.match(/^- latest pass: (\d+) — (\S+) — /m) || [];
|
|
62
|
+
const sections = said.split(/^#### /m).slice(1).map((s) => {
|
|
63
|
+
const [head, ...body] = s.split('\n');
|
|
64
|
+
return { head, url: head.split(' — ').at(-1), body: body.join('\n').trim() };
|
|
65
|
+
});
|
|
66
|
+
// The rows an answer already carries: the first cell of each line of its table.
|
|
67
|
+
const answered = new Set(sections.filter((s) => s.head.startsWith('answer to ')).flatMap((s) =>
|
|
68
|
+
lines(s.body).filter((l) => l.startsWith('| ')).map((l) => l.split('|')[1].trim()).flatMap((c) => c.split(/,\s*/))));
|
|
69
|
+
// The rows a commit since the merge base answers: `pass <k> <n>`, or a comment's link.
|
|
70
|
+
const trailers = new Map();
|
|
71
|
+
for (const c of JSON.parse(must('git', 'log_since', [mr.base]))) for (const a of c.answers) trailers.set(a, c.sha.slice(0, 8));
|
|
72
|
+
|
|
73
|
+
const rows = [];
|
|
74
|
+
const add = (id, key, text) => {
|
|
75
|
+
if (answered.has(id)) return;
|
|
76
|
+
rows.push({ id, text, commit: trailers.get(key) });
|
|
77
|
+
};
|
|
78
|
+
const pass = sections.find((s) => s.head.startsWith('pass '));
|
|
79
|
+
if (pass) {
|
|
80
|
+
const findings = pass.body.split(/^### /m).find((s) => s.startsWith('Findings')) ?? '';
|
|
81
|
+
for (const item of findings.split(/^(?=\d+\. )/m).filter((s) => /^\d+\. /.test(s))) {
|
|
82
|
+
const state = item.replace(/\s*\n\s*/g, ' ').match(/\*([^*]+)\*\s*$/)?.[1].trim() ?? '';
|
|
83
|
+
if (/^(new|open)\b/.test(state) || /^refused\b.*\bkept$/.test(state)) add(item.match(/^\d+/)[0], `pass ${k} ${item.match(/^\d+/)[0]}`, item.trim());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const s of sections.filter((s) => /^(comment|open thread) by /.test(s.head))) add(s.url, s.url, `${s.head}\n\n${s.body}`);
|
|
87
|
+
|
|
88
|
+
say(k ? `pass: ${k}, at ${read}` : 'pass: none — the comments people wrote are the rows');
|
|
89
|
+
const committed = rows.filter((r) => r.commit);
|
|
90
|
+
const numbers = rows.filter((r) => /^\d+$/.test(r.id)).map((r) => r.id), people = rows.length - numbers.length;
|
|
91
|
+
say(`rows: ${rows.length} — ${[numbers.length && `pass ${k}'s ${numbers.join(', ')}`, people && `${people} written by people`].filter(Boolean).join(', ') || 'none'}${committed.length ? `; already committed, to answer: ${committed.map((r) => `${r.id} in ${r.commit}`).join(', ')}` : ''}`);
|
|
92
|
+
if (!rows.length) stop(`nothing left to answer on !${n}${ahead ? `, yet ${ahead} commits wait to be pushed — \`/mr\` pushes them` : ''}`);
|
|
93
|
+
const run = mkdtempSync(join(tmpdir(), 'codelife-feedback-'));
|
|
94
|
+
writeFileSync(join(run, 'rows.md'), rows.map((r) => `### ${r.id}${r.commit ? ` — committed in ${r.commit}` : ''}\n\n${r.text}\n`).join('\n'));
|
|
95
|
+
say(`run: ${run} — rows.md holds each row whole`);
|
|
96
|
+
console.log(summary.join('\n'));
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit
|
|
3
|
+
description: Evaluate the whole tree at HEAD through the lenses, file every finding, and fold the campaign's report into the audit table at its commit.
|
|
4
|
+
argument-hint: "[full | targeted <lenses>]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /audit
|
|
8
|
+
|
|
9
|
+
Evaluates the **whole tree at `HEAD`** through the lenses and turns what it finds into **one issue
|
|
10
|
+
per finding**, one issue per root cause or contested decision, and **one umbrella per lot**. Five
|
|
11
|
+
verbs do the mechanics; one `codelife:judge` agent per part dispatches that part's lenses and writes
|
|
12
|
+
its findings; this brief asks what is yours to decide — each table, the lots — and fixes nothing. A
|
|
13
|
+
campaign runs in place, and its folder lives in the tree only until its close.
|
|
14
|
+
|
|
15
|
+
## Preconditions
|
|
16
|
+
|
|
17
|
+
None to check here: `codelife audit prep` checks the tree, the campaign under way, the lenses and
|
|
18
|
+
the push tier, and prints a `- stop:` line for each that fails. A stop ends the run — say it in your
|
|
19
|
+
reply with what to do, and run nothing else.
|
|
20
|
+
|
|
21
|
+
## Brief
|
|
22
|
+
|
|
23
|
+
**Who gave the go-ahead**, and from what, opens it: you, typing the command or answering this
|
|
24
|
+
brief; your confirmed table in a driver, which answers what it named — ask only the rest; the word
|
|
25
|
+
`delegate`; or a session that relayed your words, quoted here — relayed is not delegated. Typed or
|
|
26
|
+
tabled, this brief is an announcement; otherwise the run **stops here** for your go-ahead, after
|
|
27
|
+
nothing but a preparation that only reads.
|
|
28
|
+
|
|
29
|
+
`codelife audit prep` writes the campaign folder, so a run you did not type stops before it. Then
|
|
30
|
+
`codelife audit prep <parameters>`, its block written **in your reply**, in the user's language —
|
|
31
|
+
a tool's output is not one —, every line kept as printed. Then the questions, together, each with
|
|
32
|
+
its recommendation:
|
|
33
|
+
|
|
34
|
+
- `stop: the push tier is red` — fix it first, recommended, or audit the failure: the same command
|
|
35
|
+
with `--red`.
|
|
36
|
+
- `stop: <ID> has no reference` — a new domain lens: write `docs/audit/lenses/<ID>.md` with the user
|
|
37
|
+
from `templates/lens.md` — its id, objective, questions and probes asked —, then prep again.
|
|
38
|
+
- `campaign: … resumed` — resume, recommended, or abandon: on your word delete that folder, then
|
|
39
|
+
prep again.
|
|
40
|
+
- `tracker: not connected` — nothing can be filed: stop, recommended, and connect first.
|
|
41
|
+
|
|
42
|
+
State that findings **will** be filed, one table at a time, each confirmed by you first.
|
|
43
|
+
|
|
44
|
+
## Parameters
|
|
45
|
+
|
|
46
|
+
`full` — every lens the contract's `kind` makes applicable and every domain lens under
|
|
47
|
+
`docs/audit/lenses/` — or `targeted <LENS>…`, a domain lens like any other. Default `full`. A
|
|
48
|
+
resumed campaign keeps its own.
|
|
49
|
+
|
|
50
|
+
## Steps
|
|
51
|
+
|
|
52
|
+
1. **Judges** — in the index prep wrote from `templates/index.md`, write your model into the
|
|
53
|
+
*Environment* row. Say `cap:`'s line, then dispatch one agent per row of `judges.tsv`: the Agent
|
|
54
|
+
tool, `subagent_type: codelife:judge`, the prompt the row's folder and nothing else — as many at
|
|
55
|
+
once as `cap:` says, another as each returns. A block's `Withheld:` reproductions are shown **in your reply** and written nowhere. A
|
|
56
|
+
judge that fails, or returns no `### judge result`, stops the campaign before any table: Next is
|
|
57
|
+
`/audit`, which dispatches that part alone.
|
|
58
|
+
2. **Tables**, in the order the index lists the lenses — the hygiene lenses together, every other
|
|
59
|
+
one alone. `codelife audit table <LENS>[,<LENS>…]`; a `- stop:` line is said and followed. Write
|
|
60
|
+
the table **in your reply** with, per row, your proposed disposition — nature and severity for a
|
|
61
|
+
new one, `known`, `false`, `refused`, `merged <id>` —, the rows you doubt flagged, and the lines
|
|
62
|
+
below it. One answer per table; nothing is filed before it. A row dropped is **false** — the lens
|
|
63
|
+
was wrong — or **refused** — true and not to be fixed: write its line of `## Rejected` in the
|
|
64
|
+
capability it concerns (`codelife help method refusal`), and its address goes in the row. Then
|
|
65
|
+
the rows to a file, one per line as `codelife help audit file` reads them, and
|
|
66
|
+
`codelife audit file <file>`, its block in your reply. A point below the rows you want filed goes
|
|
67
|
+
to `/new-issue` later.
|
|
68
|
+
3. **Questions** — what sits behind several findings is a root cause, and each *Contested* line a
|
|
69
|
+
decision whose reason a fact contests: **nothing about either is decided here**. Propose them **in
|
|
70
|
+
your reply**, one issue each, written as
|
|
71
|
+
`codelife help template feature` prints one — the findings or the
|
|
72
|
+
decision's sentence and the fact as its *Context*, the question asked — and filed on your yes by
|
|
73
|
+
`codelife issue create --once feature - audit:A<n>-question-<k> <file>`. Each goes in the index's
|
|
74
|
+
*main problems* with its number.
|
|
75
|
+
4. **Plan** — lots, ordered so that what catches a regression lands first; one lot is one merge
|
|
76
|
+
request. Each answers a cause — the false claim deleted rather than guarded, one primitive its
|
|
77
|
+
sites call rather than an assertion at each —, names its dependencies, its *done when* and its
|
|
78
|
+
expected net line count. Proposed **in your reply** with your reasoning; you change the grouping,
|
|
79
|
+
the order or the sizing. Confirmed, each umbrella is written as
|
|
80
|
+
`codelife help template lot` prints one, *The order* empty, filed by
|
|
81
|
+
`codelife issue create --once lot - audit:A<n>-lot-<X> <file>`, then
|
|
82
|
+
`codelife lot join <umbrella> <n>:<effort>…`. The plan goes in the index.
|
|
83
|
+
5. **Close** — write the index's lead, then `codelife audit close`, its block in your reply. A file
|
|
84
|
+
it names as differing from the open is restored only on your word, and named in *Left open*
|
|
85
|
+
otherwise. Then `codelife commit` with `docs(audits): close campaign A<n>` on the paths it prints,
|
|
86
|
+
`codelife audit fold`, and `codelife commit` with `docs(audits): fold campaign A<n>` on its paths.
|
|
87
|
+
|
|
88
|
+
## Never
|
|
89
|
+
|
|
90
|
+
- Measure anything but `HEAD`, or check out, stash or switch.
|
|
91
|
+
- File a row, a question or a lot without your answer, or decide a root cause, a contested decision
|
|
92
|
+
or the lots yourself.
|
|
93
|
+
- Write anything outside `docs/audit/` but a `## Rejected` line you confirmed, or restore a file
|
|
94
|
+
without your word.
|
|
95
|
+
- Answer in a language the user did not write in, or write anything but English into the
|
|
96
|
+
repository or the tracker.
|
|
97
|
+
|
|
98
|
+
## Output
|
|
99
|
+
|
|
100
|
+
The run ends on this block, under this exact heading, its fields in the user's language:
|
|
101
|
+
|
|
102
|
+
```markdown
|
|
103
|
+
### audit result
|
|
104
|
+
- Produced: the issues by lens, the questions, the lot umbrellas, the two commits — the close,
|
|
105
|
+
whose sha the campaign row links, and the fold.
|
|
106
|
+
- Decided: the lenses, each table's corrections, the lots and their order, a red baseline audited.
|
|
107
|
+
- Left open: the questions, none decided; the points below the rows you kept for `/new-issue`; the
|
|
108
|
+
files the fingerprint found changed.
|
|
109
|
+
- Next: `/brainstorm #<n>` on a question; `/work <umbrella>` on the first lot.
|
|
110
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!-- generated by codelife — /audit -->
|
|
2
|
+
# Campaign {{id}} — {{type}}, {{date}}
|
|
3
|
+
|
|
4
|
+
> **⚠ IN PROGRESS.** This campaign is not closed: its findings are not all filed. Do not cite it.
|
|
5
|
+
> `/audit` resumes it; a part whose folder holds no `part.md` is not judged yet.
|
|
6
|
+
|
|
7
|
+
{{lead: one or two sentences on the state of the repository at this commit, for a developer who knows the language and the forge but not this repository}}
|
|
8
|
+
|
|
9
|
+
## The main problems
|
|
10
|
+
|
|
11
|
+
{{each root cause and each decision contested, under a ### heading in plain words: what goes wrong and why it recurs, with the findings that are its instances — or the decision's sentence and the fact against it — then its issue #n; or "None."}}
|
|
12
|
+
|
|
13
|
+
## The plan
|
|
14
|
+
|
|
15
|
+
{{each lot in order: its umbrella #n and name, its dependencies, its done when, its members}}
|
|
16
|
+
|
|
17
|
+
## What this campaign could not measure
|
|
18
|
+
|
|
19
|
+
{{measure}}. Each part's `part.md` holds every lens's *Method* and *Limits*.
|
|
20
|
+
|
|
21
|
+
## The campaign
|
|
22
|
+
|
|
23
|
+
| | |
|
|
24
|
+
|---|---|
|
|
25
|
+
| **Commit** | `{{commit}}` on `{{branch}}` |
|
|
26
|
+
| **Type** | {{type}} |
|
|
27
|
+
| **Lenses run** | {{lenses}} |
|
|
28
|
+
| **Baseline** | {{baseline}} |
|
|
29
|
+
| **Environment** | {{model: the model this campaign ran on}} |
|
|
30
|
+
| **Instrument** | {{instrument}} |
|
|
31
|
+
| **Previous campaign** | {{previous}} |
|
|
32
|
+
| **Swept** | {{open}} open issues and {{decisions}} decisions, passed to every lens as known |
|
|
33
|
+
|
|
34
|
+
## Findings by lens
|
|
35
|
+
|
|
36
|
+
{{findings}}
|