@ok-ggwp/git-gloss 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -4
- package/dist/ary/browse.js +80 -0
- package/dist/ary/client.js +401 -0
- package/dist/ary/index.html +13 -0
- package/dist/ary/server.js +101 -0
- package/dist/ary/style.css +115 -0
- package/dist/cli.js +3 -1
- package/dist/commands/adopt.js +3 -0
- package/dist/commands/ary.js +35 -0
- package/dist/commands/carry.js +3 -0
- package/dist/commands/html.js +1 -1
- package/dist/commands/hunks.js +4 -0
- package/dist/commands/init.js +22 -8
- package/dist/commands/review.js +3 -1
- package/dist/commands/reviews.js +70 -0
- package/dist/commands/show.js +18 -0
- package/dist/commands/sync.js +53 -0
- package/dist/git.js +2 -1
- package/dist/html/client.js +30 -1
- package/dist/html/data.js +13 -0
- package/dist/html/page.js +1 -1
- package/dist/html/style.css +9 -0
- package/dist/reviews.js +205 -0
- package/package.json +1 -1
- package/skills/gloss/SKILL.md +8 -0
package/dist/html/client.js
CHANGED
|
@@ -188,11 +188,31 @@
|
|
|
188
188
|
var allGroups = []
|
|
189
189
|
data.commits.forEach(function (c) { allGroups = allGroups.concat(c.groups) })
|
|
190
190
|
|
|
191
|
+
// Whether each commit as it is now was reviewed, per stage; each stage's
|
|
192
|
+
// reports open on demand. Findings are shown as text.
|
|
193
|
+
function reviewsBlock(c, target) {
|
|
194
|
+
var box = el('div', 'reviews')
|
|
195
|
+
if (!c.reviews.length) { add(target, add(box, el('span', 'review-none', 'No reviews recorded'))); return }
|
|
196
|
+
c.reviews.forEach(function (r) {
|
|
197
|
+
var d = el('details', 'review')
|
|
198
|
+
add(d, add(el('summary'), el('span', r.state === 'seen' && r.verdict !== 'issues' ? (r.verdict === 'clean' ? 'review-ok' : 'review-none') : 'stale', r.phrase)))
|
|
199
|
+
r.entries.forEach(function (e) {
|
|
200
|
+
add(d, el('div', 'note', r.stage + ' by ' + e.agent + (e.model ? ':' + e.model : '') + (e.mode ? ' (' + e.mode + ')' : '') +
|
|
201
|
+
', ' + e.at.slice(0, 16).replace('T', ' ') + ', of ' + e.target), el('pre', 'findings', e.findings))
|
|
202
|
+
})
|
|
203
|
+
add(box, d)
|
|
204
|
+
})
|
|
205
|
+
add(target, box)
|
|
206
|
+
}
|
|
207
|
+
|
|
191
208
|
// Header: the commit itself, or the range and its commits.
|
|
192
209
|
if (multi) {
|
|
193
210
|
var glossed = data.commits.filter(function (c) { return c.glossed }).length
|
|
211
|
+
var vetted = data.commits.filter(function (c) {
|
|
212
|
+
return c.glossed && c.reviews.some(function (r) { return r.stage === 'honesty' && r.state === 'seen' })
|
|
213
|
+
}).length
|
|
194
214
|
add(main, add(el('div', 'commit'), el('span', 'sha', data.range),
|
|
195
|
-
el('span', null, plural(data.commits.length, 'commit') + ', ' + glossed + ' glossed')))
|
|
215
|
+
el('span', null, plural(data.commits.length, 'commit') + ', ' + glossed + ' glossed' + (glossed ? ', ' + vetted + ' honesty-reviewed' : ''))))
|
|
196
216
|
add(main, el('h1', null, 'Branch ' + data.range))
|
|
197
217
|
var list = el('ol', 'commit-list')
|
|
198
218
|
data.commits.forEach(function (c) {
|
|
@@ -201,6 +221,13 @@
|
|
|
201
221
|
var li = add(el('li'), a)
|
|
202
222
|
if (!c.glossed) add(li, el('span', 'stale', 'no gloss'))
|
|
203
223
|
else if (c.warnings.length) add(li, el('span', 'stale', 'stale gloss'))
|
|
224
|
+
if (c.glossed) {
|
|
225
|
+
var honesty = c.reviews.filter(function (r) { return r.stage === 'honesty' })[0]
|
|
226
|
+
add(li, !honesty ? el('span', 'stale', 'not reviewed')
|
|
227
|
+
: honesty.state !== 'seen' ? el('span', 'stale', 'changed since review')
|
|
228
|
+
: honesty.verdict === 'issues' ? el('span', 'stale', 'honesty: issues found')
|
|
229
|
+
: el('span', 'review-ok', honesty.verdict === 'clean' ? 'honesty: no issues' : 'honesty-reviewed'))
|
|
230
|
+
}
|
|
204
231
|
list.appendChild(li)
|
|
205
232
|
})
|
|
206
233
|
data.merges.forEach(function (m) {
|
|
@@ -214,6 +241,7 @@
|
|
|
214
241
|
statement(main, 'Task', only.task)
|
|
215
242
|
statement(main, 'Summary', only.summary)
|
|
216
243
|
only.warnings.forEach(function (w) { add(main, el('div', 'warning', '⚠ ' + w)) })
|
|
244
|
+
reviewsBlock(only, main)
|
|
217
245
|
}
|
|
218
246
|
|
|
219
247
|
// Necessity overview across every commit: bar weighted by changed lines,
|
|
@@ -266,6 +294,7 @@
|
|
|
266
294
|
statement(target, 'Task', c.task)
|
|
267
295
|
statement(target, 'Summary', c.summary)
|
|
268
296
|
c.warnings.forEach(function (w) { add(target, el('div', 'warning', '⚠ ' + w)) })
|
|
297
|
+
reviewsBlock(c, target)
|
|
269
298
|
add(main, target)
|
|
270
299
|
}
|
|
271
300
|
var tocList = commitBody(c, target)
|
package/dist/html/data.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { commitDiff, commitSubject, git } from '../git.js';
|
|
2
|
+
import { reviewStatus, statusPhrase, verdict } from '../reviews.js';
|
|
2
3
|
import { parseDiff } from '../diff.js';
|
|
3
4
|
import { loadGloss } from '../loader.js';
|
|
4
5
|
import { bodyLines, changedCount, gapText, isGap, refLabel, slice, sliceStats } from '../selection.js';
|
|
5
6
|
import { NECESSITY, NECESSITY_HELP, servesOf } from '../schema.js';
|
|
7
|
+
function pageReviews(sha) {
|
|
8
|
+
return reviewStatus(sha).map((s) => ({
|
|
9
|
+
stage: s.stage,
|
|
10
|
+
state: s.code === 'changed' ? 'code-changed'
|
|
11
|
+
: (s.stage === 'honesty' || s.stage === 'improve') && s.gloss === 'changed' ? 'gloss-changed' : 'seen',
|
|
12
|
+
verdict: verdict(s),
|
|
13
|
+
phrase: statusPhrase(s),
|
|
14
|
+
entries: s.reviews.map((r) => ({ agent: r.agent, model: r.model, mode: r.mode, at: r.at, target: r.target, findings: r.findings })),
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
6
17
|
// The lines to show for a selection: the @@ header, then the slice, with
|
|
7
18
|
// changed lines that belong to other groups collapsed into gap rows.
|
|
8
19
|
export function pageLines(h, selected) {
|
|
@@ -102,6 +113,7 @@ export function commitData(gloss) {
|
|
|
102
113
|
unannotated: gloss.unannotated.map((u) => pageHunk(u.hunk, u.lines, 'unannotated', [])),
|
|
103
114
|
alternatives: a.alternatives ?? [],
|
|
104
115
|
risks: a.risks ?? [],
|
|
116
|
+
reviews: pageReviews(gloss.sha),
|
|
105
117
|
};
|
|
106
118
|
}
|
|
107
119
|
function unglossedData(sha) {
|
|
@@ -118,6 +130,7 @@ function unglossedData(sha) {
|
|
|
118
130
|
unannotated: parseDiff(commitDiff(sha)).map((h) => pageHunk(h, null, 'unannotated', [])),
|
|
119
131
|
alternatives: [],
|
|
120
132
|
risks: [],
|
|
133
|
+
reviews: pageReviews(sha),
|
|
121
134
|
};
|
|
122
135
|
}
|
|
123
136
|
export function buildPageData(shas, range = null, merges = []) {
|
package/dist/html/page.js
CHANGED
|
@@ -6,7 +6,7 @@ const asset = (name) => readFileSync(new URL(name, import.meta.url), 'utf8');
|
|
|
6
6
|
export function embedJson(data) {
|
|
7
7
|
return JSON.stringify(data).replace(/</g, '\\u003c');
|
|
8
8
|
}
|
|
9
|
-
const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
9
|
+
export const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
10
10
|
export function renderPage(data) {
|
|
11
11
|
// The page only runs our own inline script. If escaping ever failed, an
|
|
12
12
|
// injected script would lack the nonce and the CSP would block it.
|
package/dist/html/style.css
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
--surface: #ffffff;
|
|
4
4
|
--surface-2: #f1f1ee;
|
|
5
5
|
--border: #e2e2dc;
|
|
6
|
+
--ok: #1a7f37;
|
|
6
7
|
--text: #1d1d1b;
|
|
7
8
|
--muted: #6b6b66;
|
|
8
9
|
--accent: #3b5bdb;
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
--surface: #1e1e1c;
|
|
35
36
|
--surface-2: #262624;
|
|
36
37
|
--border: #34342f;
|
|
38
|
+
--ok: #57c47a;
|
|
37
39
|
--text: #e8e8e3;
|
|
38
40
|
--muted: #9a9a92;
|
|
39
41
|
--accent: #8ea2ff;
|
|
@@ -218,3 +220,10 @@ kbd { font-family: var(--mono); font-size: 11.5px; border: 1px solid var(--borde
|
|
|
218
220
|
.commit-section h2 { font-size: 20px; margin: 8px 0 12px; }
|
|
219
221
|
.commit-section .section h3 { font-size: 16px; margin: 0 0 12px; }
|
|
220
222
|
.toc h2.commit-head { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-transform: none; letter-spacing: 0; font-family: var(--mono); }
|
|
223
|
+
|
|
224
|
+
/* Reviews recorded for a commit */
|
|
225
|
+
.reviews { margin: 8px 0 12px; display: flex; flex-direction: column; gap: 4px; }
|
|
226
|
+
.review summary { cursor: pointer; }
|
|
227
|
+
.review-ok { color: var(--ok); font-weight: 600; }
|
|
228
|
+
.review-none { color: var(--muted); font-size: 0.9em; }
|
|
229
|
+
.findings { white-space: pre-wrap; font-size: 0.85em; max-height: 32em; overflow: auto; padding: 8px; border-left: 3px solid var(--border); }
|
package/dist/reviews.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { commitDiff, git, gitTry, GlossError, readNote } from './git.js';
|
|
3
|
+
import { parseDiff } from './diff.js';
|
|
4
|
+
import { bodyLines } from './selection.js';
|
|
5
|
+
// Reviews of commits (by git-gud, or anything else that records them), kept
|
|
6
|
+
// beside the glosses so a reader can see whether a gloss was checked. One JSON
|
|
7
|
+
// record per line: notes from different people and machines combine by taking
|
|
8
|
+
// every line (git's cat_sort_uniq), with no conflicts.
|
|
9
|
+
export const REVIEWS_REF = 'refs/notes/gloss-reviews';
|
|
10
|
+
// The report's VERDICT line, which has to be a line of its own among the
|
|
11
|
+
// last three, so a verdict quoted in the body can't stand in for it.
|
|
12
|
+
export function verdictOf(findings) {
|
|
13
|
+
const tail = findings.trim().split('\n').filter((l) => l.trim()).slice(-3).reverse();
|
|
14
|
+
for (const line of tail) {
|
|
15
|
+
// The whole line, give or take markdown: not a sentence that starts so.
|
|
16
|
+
const m = /^\W*VERDICT:\W*(CLEAN|ISSUES)\W*$/i.exec(line.trim());
|
|
17
|
+
if (m)
|
|
18
|
+
return m[1].toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const key = (s) => createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
23
|
+
// One key per changed line, repeated for repeated lines (a second "}" is a
|
|
24
|
+
// second key), so adding another copy of a reviewed line isn't "seen". A
|
|
25
|
+
// change with no lines (a binary file, a mode change, a pure rename) is keyed
|
|
26
|
+
// by its file and content hash.
|
|
27
|
+
export const lineKeys = (hunks) => hunks.flatMap((h) => {
|
|
28
|
+
const lines = bodyLines(h).filter((l) => l.idx !== null);
|
|
29
|
+
return lines.length ? lines.map((l) => key(`${h.file}\0${l.raw}`)) : [key(`meta\0${h.file}\0${h.hash}`)];
|
|
30
|
+
}).sort();
|
|
31
|
+
// A group's meaning, without its id or hunk references (which change when a
|
|
32
|
+
// gloss is carried or adopted) and without the notes carrying adds.
|
|
33
|
+
export function groupKey(g) {
|
|
34
|
+
const notes = (g.notes ?? '').replace(/\s*\((?:from [0-9a-f]{7,}|also served [^)]*)\)/g, '').trim();
|
|
35
|
+
const serves = (Array.isArray(g.serves) ? g.serves : [g.serves]).slice().sort();
|
|
36
|
+
return key(JSON.stringify([g.intent, g.necessity, serves, g.removal_impact, g.evidence ?? '', notes]));
|
|
37
|
+
}
|
|
38
|
+
// The gloss's parts a reviewer checks: each group, each risk and each
|
|
39
|
+
// rejected alternative (keyed one by one, so a squash that combines several
|
|
40
|
+
// glosses' risks still matches the reviews of each). The task and summary
|
|
41
|
+
// are left out: carrying joins them.
|
|
42
|
+
export function glossKeys(a) {
|
|
43
|
+
return [
|
|
44
|
+
...a.groups.map(groupKey),
|
|
45
|
+
...(a.risks ?? []).map((r) => key(`risk\0${r}`)),
|
|
46
|
+
...(a.alternatives ?? []).map((x) => key(`alternative\0${x.option}\0${x.rejected_because}`)),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
// What a reviewer of `sha` sees now: its changed lines and its gloss's groups.
|
|
50
|
+
export function stamp(sha) {
|
|
51
|
+
let groups = [];
|
|
52
|
+
try {
|
|
53
|
+
const note = readNote(sha);
|
|
54
|
+
groups = note ? glossKeys(note.annotation) : [];
|
|
55
|
+
}
|
|
56
|
+
catch { /* an unreadable gloss: nothing to stamp */ }
|
|
57
|
+
return { lines: lineKeys(parseDiff(commitDiff(sha))), groups };
|
|
58
|
+
}
|
|
59
|
+
export function readReviews(sha) {
|
|
60
|
+
const raw = gitTry(['notes', '--ref', REVIEWS_REF, 'show', sha]);
|
|
61
|
+
if (!raw)
|
|
62
|
+
return [];
|
|
63
|
+
// A rebase can join a note onto one with the same lines; each counts once.
|
|
64
|
+
return [...new Set(raw.split('\n'))].flatMap((line) => {
|
|
65
|
+
try {
|
|
66
|
+
const r = JSON.parse(line);
|
|
67
|
+
// Records arrive from other machines; skip any that aren't whole.
|
|
68
|
+
const ok = r?.review === 1 && Array.isArray(r.lines) && Array.isArray(r.groups) &&
|
|
69
|
+
['id', 'at', 'stage', 'agent', 'target', 'findings'].every((k) => typeof r[k] === 'string') &&
|
|
70
|
+
['model', 'mode'].every((k) => r[k] === undefined || typeof r[k] === 'string') &&
|
|
71
|
+
(r.verdict === undefined || r.verdict === 'clean' || r.verdict === 'issues');
|
|
72
|
+
return ok ? [r] : [];
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Adds records to a commit's reviews, keeping each line once. Several
|
|
80
|
+
// reviews can finish at once, so the write is a compare-and-swap: the new
|
|
81
|
+
// notes commit is built under a private ref, and the reviews ref moves to it
|
|
82
|
+
// only if nobody else moved it meanwhile; otherwise it starts again.
|
|
83
|
+
export function addReviews(sha, records) {
|
|
84
|
+
const mine = records.map((r) => JSON.stringify(r));
|
|
85
|
+
// git notes needs it under refs/notes; named so the refs/notes/gloss*
|
|
86
|
+
// fetch glob never matches it.
|
|
87
|
+
const tmp = `refs/notes/tmp-gloss-reviews-${process.pid}-${randomUUID().slice(0, 8)}`;
|
|
88
|
+
try {
|
|
89
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
90
|
+
const old = gitTry(['rev-parse', '-q', '--verify', REVIEWS_REF])?.trim() ?? null;
|
|
91
|
+
if (old)
|
|
92
|
+
git(['update-ref', tmp, old]);
|
|
93
|
+
else
|
|
94
|
+
gitTry(['update-ref', '-d', tmp]);
|
|
95
|
+
const existing = (gitTry(['notes', '--ref', tmp, 'show', sha]) ?? '').split('\n').filter(Boolean);
|
|
96
|
+
const lines = [...new Set([...existing, ...mine])].sort();
|
|
97
|
+
git(['notes', '--ref', tmp, 'add', '-f', '-F', '-', sha], lines.join('\n') + '\n');
|
|
98
|
+
const next = git(['rev-parse', tmp]).trim();
|
|
99
|
+
// An empty old value means "must not exist yet".
|
|
100
|
+
if (gitTry(['update-ref', REVIEWS_REF, next, old ?? '']) !== null)
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
throw new GlossError(`couldn't record the review on ${sha.slice(0, 10)}: other processes kept writing reviews at the same time`);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
gitTry(['update-ref', '-d', tmp]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Reviews follow a gloss onto commits made from the reviewed ones (adopt,
|
|
110
|
+
// carry); their keys decide what they still cover there.
|
|
111
|
+
export function copyReviews(from, to) {
|
|
112
|
+
const records = from.flatMap(readReviews);
|
|
113
|
+
if (records.length)
|
|
114
|
+
addReviews(to, records);
|
|
115
|
+
}
|
|
116
|
+
// Whether reviews saw every changed line, counting repeats: each distinct
|
|
117
|
+
// stamp (reviews of different commits, as for a squash) contributes its
|
|
118
|
+
// lines; reviews of the same stamp count once.
|
|
119
|
+
function covers(reviews, lines) {
|
|
120
|
+
const seen = new Map();
|
|
121
|
+
const stamps = new Set();
|
|
122
|
+
for (const r of reviews) {
|
|
123
|
+
const id = r.lines.join(',');
|
|
124
|
+
if (stamps.has(id))
|
|
125
|
+
continue;
|
|
126
|
+
stamps.add(id);
|
|
127
|
+
const counts = new Map();
|
|
128
|
+
for (const l of r.lines)
|
|
129
|
+
counts.set(l, (counts.get(l) ?? 0) + 1);
|
|
130
|
+
for (const [l, n] of counts)
|
|
131
|
+
seen.set(l, (seen.get(l) ?? 0) + n);
|
|
132
|
+
}
|
|
133
|
+
const need = new Map();
|
|
134
|
+
for (const l of lines)
|
|
135
|
+
need.set(l, (need.get(l) ?? 0) + 1);
|
|
136
|
+
return [...need].every(([l, n]) => (seen.get(l) ?? 0) >= n);
|
|
137
|
+
}
|
|
138
|
+
// Per stage: whether the commit as it is now was reviewed. A commit squashed
|
|
139
|
+
// from several reviewed ones is covered when their reviews together saw all
|
|
140
|
+
// of it.
|
|
141
|
+
export function reviewStatus(sha, current = stamp(sha)) {
|
|
142
|
+
const byStage = new Map();
|
|
143
|
+
for (const r of readReviews(sha))
|
|
144
|
+
byStage.set(r.stage, [...(byStage.get(r.stage) ?? []), r]);
|
|
145
|
+
return [...byStage].map(([stage, reviews]) => {
|
|
146
|
+
const groups = new Set(reviews.flatMap((r) => r.groups));
|
|
147
|
+
return {
|
|
148
|
+
stage,
|
|
149
|
+
now: current,
|
|
150
|
+
reviews: reviews.sort((a, b) => b.at.localeCompare(a.at)),
|
|
151
|
+
code: covers(reviews, current.lines) ? 'seen' : 'changed',
|
|
152
|
+
gloss: current.groups.length ? (current.groups.every((g) => groups.has(g)) ? 'seen' : 'changed') : null,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// Whether this stage's reviews still apply to the commit as it is now.
|
|
157
|
+
export const current = (s) => s.code === 'seen' && !((s.stage === 'honesty' || s.stage === 'improve') && s.gloss === 'changed');
|
|
158
|
+
// What the reviews concluded about the commit as it is now. Each part of it
|
|
159
|
+
// (a gloss group for honesty and improve, else a changed line) takes the
|
|
160
|
+
// verdict of the latest review that saw it, so fixing a gloss and having it
|
|
161
|
+
// reviewed again replaces the earlier verdict for the parts it saw. Issues
|
|
162
|
+
// if any part's latest review found some; clean if every part's said clean.
|
|
163
|
+
export function verdict(s) {
|
|
164
|
+
const v = latestVerdicts(s);
|
|
165
|
+
return v.includes('issues') ? 'issues' : v.length && v.every((x) => x === 'clean') ? 'clean' : 'unknown';
|
|
166
|
+
}
|
|
167
|
+
function latestVerdicts(s) {
|
|
168
|
+
const byGroups = (s.stage === 'honesty' || s.stage === 'improve') && s.now.groups.length;
|
|
169
|
+
const parts = byGroups ? s.now.groups : s.now.lines;
|
|
170
|
+
// s.reviews is newest first.
|
|
171
|
+
const latest = new Set();
|
|
172
|
+
for (const part of parts) {
|
|
173
|
+
// A review without a verdict line says nothing either way, so it doesn't
|
|
174
|
+
// replace an earlier one's.
|
|
175
|
+
const r = s.reviews.find((r) => r.verdict && (byGroups ? r.groups : r.lines).includes(part));
|
|
176
|
+
if (r)
|
|
177
|
+
latest.add(r);
|
|
178
|
+
}
|
|
179
|
+
return [...latest].map((r) => r.verdict);
|
|
180
|
+
}
|
|
181
|
+
// Whether a review older than the ones deciding the verdict found issues:
|
|
182
|
+
// a clean result is then shown as following an earlier one with issues.
|
|
183
|
+
export const earlierIssues = (s) => verdict(s) !== 'issues' && s.reviews.some((r) => r.verdict === 'issues');
|
|
184
|
+
// What a verdict means for each stage: improve's "issues" are suggestions.
|
|
185
|
+
const WORDS = {
|
|
186
|
+
bugs: { clean: 'no defects found', issues: 'defects found' },
|
|
187
|
+
improve: { clean: 'nothing to improve', issues: 'suggestions' },
|
|
188
|
+
default: { clean: 'no issues', issues: 'issues found' },
|
|
189
|
+
};
|
|
190
|
+
// One short phrase per stage, for summaries: "honesty: issues found (claude,
|
|
191
|
+
// 2026-10-18)". A review says the commit was checked, not that it passed.
|
|
192
|
+
export function statusPhrase(s) {
|
|
193
|
+
const latest = s.reviews[0];
|
|
194
|
+
const who = `${latest.agent}${latest.model ? `:${latest.model}` : ''}, ${latest.at.slice(0, 10)}${s.reviews.length > 1 ? `, ${s.reviews.length} reviews` : ''}`;
|
|
195
|
+
if (s.code === 'changed')
|
|
196
|
+
return `${s.stage}: code changed since review (${who})`;
|
|
197
|
+
if (!current(s)) {
|
|
198
|
+
const never = s.reviews.every((r) => !r.groups.length);
|
|
199
|
+
return `${s.stage}: ${never ? 'reviewed before it was glossed' : 'gloss changed since review'} (${who})`;
|
|
200
|
+
}
|
|
201
|
+
const v = verdict(s);
|
|
202
|
+
const words = WORDS[s.stage] ?? WORDS.default;
|
|
203
|
+
return `${s.stage}: ${v === 'clean' ? words.clean : v === 'issues' ? words.issues : 'reviewed'} (${who}` +
|
|
204
|
+
`${earlierIssues(s) ? '; an earlier review found issues' : ''})`;
|
|
205
|
+
}
|
package/package.json
CHANGED
package/skills/gloss/SKILL.md
CHANGED
|
@@ -25,6 +25,10 @@ Gloss changes **before they are committed**, unless the user asks you to commit
|
|
|
25
25
|
|
|
26
26
|
To gloss a commit instead, use its revision (default `HEAD`) wherever these steps say `wip`.
|
|
27
27
|
|
|
28
|
+
## Calling git-gloss
|
|
29
|
+
|
|
30
|
+
The commands below are written as `git gloss …`. That works only where git-gloss is on PATH. Check first with `command -v git-gloss`. If it isn't, look for a project install (`node_modules/.bin/git-gloss`, or a `package.json` that lists `@ok-ggwp/git-gloss`) and run the same commands as `pnpm exec git-gloss …`, `npx git-gloss …` or `node_modules/.bin/git-gloss …`. Use the package manager the repo uses. In a monorepo the install may sit in a subdirectory: run the commands from there, and give file paths to `why` relative to that directory. Don't install it globally just to run the skill.
|
|
31
|
+
|
|
28
32
|
## Steps
|
|
29
33
|
|
|
30
34
|
1. Run `git gloss hunks wip` to snapshot the uncommitted work and see it split into hunks with ids `h1`, `h2`, …, with each hunk's changed (`+`/`-`) lines numbered from 1. If you change files afterwards, run it again: that takes a new snapshot, and a gloss must match the snapshot it was written for.
|
|
@@ -100,3 +104,7 @@ Where you can, set `evidence` to a test that exercises the change or the line of
|
|
|
100
104
|
You don't need to look up intent before ordinary edits. Use `git gloss why` when you are genuinely unsure why existing code is the way it is, especially before removing or rewriting something that looks unnecessary, defensive or wrong. That is when an unexplained check gets deleted and a fixed bug comes back.
|
|
101
105
|
|
|
102
106
|
Ask about the block you're changing, not each line: `git gloss why <file>:<start>-<end> --json`. You get one explanation per commit that last changed those lines: the group's intent, `removal_impact`, `notes`, and the commit's `risks` and rejected `alternatives`. Check the alternatives before proposing an approach someone already tried and turned down. If the latest change to the lines was only `mechanical` (a rename, a reformat), `why` looks back past it to the real explanation; `--history` shows every change.
|
|
107
|
+
|
|
108
|
+
## Reading someone else's branch
|
|
109
|
+
|
|
110
|
+
When the user asks you to help review a branch or pull request, start from its glosses: `git fetch` (or `git gloss sync`), then `git gloss show <base>..<branch>`. You get each commit's task, groups and grades, and whether its gloss was honesty-reviewed. Lead with what the glosses grade `speculative`, `incidental` or `defensive`, and with commits that have no gloss or whose review found issues. `git gloss reviews <base>..<branch>` prints the recorded findings. A review vouches only for the gloss and code it saw: "changed since review" means it no longer does.
|