@xhulz/nina 0.37.0 → 0.38.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/package.json +1 -1
- package/releases/0.38.0/core/GAPS.md +125 -0
- package/releases/0.38.0/core/answered.json +32 -0
- package/releases/0.38.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.38.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.38.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.38.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.38.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.38.0/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.38.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.38.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.38.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.38.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.38.0/core/tree/.claude/agents/architect.md +127 -0
- package/releases/0.38.0/core/tree/.claude/agents/dba.md +110 -0
- package/releases/0.38.0/core/tree/.claude/agents/devops.md +114 -0
- package/releases/0.38.0/core/tree/.claude/agents/implementer.md +156 -0
- package/releases/0.38.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.38.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.38.0/core/tree/.claude/agents/qa.md +152 -0
- package/releases/0.38.0/core/tree/.claude/agents/reviewer.md +183 -0
- package/releases/0.38.0/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.38.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.38.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.38.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.38.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.38.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.38.0/core/tree/.claude/patterns.md +213 -0
- package/releases/0.38.0/core/tree/.claude/pills/README.md +147 -0
- package/releases/0.38.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.38.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.38.0/core/tree/.claude/router.md +198 -0
- package/releases/0.38.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.38.0/core/tree/CLAUDE.md +161 -0
- package/releases/0.38.0/core/tree/scripts/cost-watch.mjs +18 -0
- package/releases/0.38.0/core/tree/scripts/edit-guard.mjs +16 -0
- package/releases/0.38.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.38.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.38.0/core/vocabulary.json +13 -0
- package/releases/0.38.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.38.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.38.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.38.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.38.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.38.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.38.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.38.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.38.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/patterns.md +19 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.38.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.38.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.38.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.38.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.38.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
- package/releases/0.38.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.38.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.38.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.38.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.38.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.38.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.38.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.38.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.38.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.38.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.38.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.38.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.38.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.38.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.38.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.38.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.38.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.38.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.38.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.38.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.38.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.38.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.38.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.38.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.38.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.38.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.38.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.38.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.38.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
- package/releases/0.38.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.38.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.38.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.38.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.38.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.38.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/src/commands/learn.mjs +226 -71
- package/src/commands/pills.mjs +32 -9
- package/src/commands/stats.mjs +4 -4
- package/src/deep.mjs +26 -4
- package/src/gate.mjs +10 -0
- package/src/lessons.mjs +55 -0
- package/src/wiring.mjs +2 -1
package/src/commands/learn.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import { HARNESS, legacyHint, slugFor, snapshotsDir } from '../paths.mjs';
|
|
|
29
29
|
import { localDay, projectRecords } from '../store.mjs';
|
|
30
30
|
import { isLoopBack, runOf } from '../transcripts.mjs';
|
|
31
31
|
import { REQUIRES, byVersion, layerRootFor } from './compose.mjs';
|
|
32
|
-
import { GRADUATION_AT, frontmatter, graduationTarget, list, pillFiles, roleGates, tidyRetired } from './pills.mjs';
|
|
32
|
+
import { GRADUATION_AT, RETIRED, frontmatter, graduationTarget, list, pillFiles, roleGates, tidyRetired } from './pills.mjs';
|
|
33
33
|
import { decodeProjectDir } from './stats.mjs';
|
|
34
34
|
import { snapshot } from './snapshot.mjs';
|
|
35
35
|
import { loadProject, projectGateDir, readLedger, replay } from '../gate.mjs';
|
|
@@ -106,6 +106,38 @@ export async function lessons(target) {
|
|
|
106
106
|
return out;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/** How long after a loop-back's report a dispatch can still be its fix. */
|
|
110
|
+
const SENT_BACK_HOURS = 24;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The stage each loop-back sent work back to: the next one dispatched in its session, after it reported, among
|
|
114
|
+
* the stages the project's graph routes that verdict of that stage to — or the only one it routes it to.
|
|
115
|
+
*
|
|
116
|
+
* @param {object[]} records - Recorded dispatches.
|
|
117
|
+
* @param {Map<string, Map<string, Map<string, number>>>|null} loops - The graph's capped loop-back edges.
|
|
118
|
+
* @returns {Map<object, string>} Loop-back record → the stage it sent work back to, where one can be told.
|
|
119
|
+
*/
|
|
120
|
+
export function sentBackTo(records, loops) {
|
|
121
|
+
const out = new Map();
|
|
122
|
+
if (!loops) return out;
|
|
123
|
+
const bySession = new Map();
|
|
124
|
+
for (const r of records) if (r.session) bySession.set(r.session, [...(bySession.get(r.session) ?? []), r]);
|
|
125
|
+
for (const rs of bySession.values()) rs.sort((a, b) => String(a.ts).localeCompare(String(b.ts)));
|
|
126
|
+
for (const r of records) {
|
|
127
|
+
if (!readable(r.verdict) || !isLoopBack(r.verdict)) continue;
|
|
128
|
+
const targets = loops.get(r.role)?.get(r.verdict);
|
|
129
|
+
if (!targets || targets.size === 0) continue;
|
|
130
|
+
const back = String(r.result_ts ?? r.ts);
|
|
131
|
+
// Within a day of the report: one project ran six weeks in one session, and the next architect there could be
|
|
132
|
+
// five days on, with no telling it was this loop-back's fix.
|
|
133
|
+
const within = (x) => Date.parse(x.ts) - Date.parse(back) <= SENT_BACK_HOURS * 3_600_000;
|
|
134
|
+
const next = (bySession.get(r.session) ?? []).find((x) => String(x.ts) > back && within(x) && targets.has(x.role));
|
|
135
|
+
const to = next?.role ?? (targets.size === 1 ? [...targets.keys()][0] : null);
|
|
136
|
+
if (to) out.set(r, to);
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
109
141
|
/**
|
|
110
142
|
* Roles that looped back often enough, since their newest lesson, that one is overdue.
|
|
111
143
|
*
|
|
@@ -118,9 +150,13 @@ export async function lessons(target) {
|
|
|
118
150
|
* lesson: a count alone sends them digging through weeks of transcripts, which is how a warning
|
|
119
151
|
* becomes one more thing to ignore.
|
|
120
152
|
*/
|
|
121
|
-
export function overdue(records, known, now = new Date(), days = CAPTURE_DAYS) {
|
|
153
|
+
export function overdue(records, known, now = new Date(), days = CAPTURE_DAYS, owners = new Map()) {
|
|
122
154
|
const cutoff = day(new Date(now.getTime() - days * 86_400_000).toISOString());
|
|
123
|
-
|
|
155
|
+
// A loop-back is the lesson of the stage it sent work back to, where that is known (`sentBackTo`): charged
|
|
156
|
+
// to the stage that gave the verdict, the reviewer owed the implementer's lessons, and only a pill naming
|
|
157
|
+
// every checker in its applies_to ever settled the count.
|
|
158
|
+
const ownerOf = (r) => owners.get(r) ?? r.role;
|
|
159
|
+
const roles = [...new Set(records.map(ownerOf).filter(Boolean))];
|
|
124
160
|
const out = [];
|
|
125
161
|
for (const role of roles) {
|
|
126
162
|
// A retired lesson was still written down: it retired because its rule moved into the harness,
|
|
@@ -138,7 +174,7 @@ export function overdue(records, known, now = new Date(), days = CAPTURE_DAYS) {
|
|
|
138
174
|
// thing capture exists to catch.
|
|
139
175
|
const at = floor === newest ? taught?.at ?? null : null;
|
|
140
176
|
const after = (r) => day(r.ts) > floor || (at !== null && day(r.ts) === floor && String(r.ts) > at);
|
|
141
|
-
const loops = records.filter((r) => r
|
|
177
|
+
const loops = records.filter((r) => ownerOf(r) === role && readable(r.verdict) && isLoopBack(r.verdict) && after(r));
|
|
142
178
|
if (loops.length >= CAPTURE_AT) {
|
|
143
179
|
const recent = loops
|
|
144
180
|
.sort((a, b) => String(b.ts).localeCompare(String(a.ts)))
|
|
@@ -177,50 +213,129 @@ export function applied(records, known) {
|
|
|
177
213
|
};
|
|
178
214
|
}
|
|
179
215
|
|
|
216
|
+
/** How many days either side of a lesson's date its role is measured over: the same on both sides. */
|
|
217
|
+
const VERIFY_DAYS = 14;
|
|
218
|
+
|
|
219
|
+
/** A day as `YYYY-MM-DD`, moved by some days. */
|
|
220
|
+
const shift = (date, days) => new Date(Date.parse(`${date}T12:00:00Z`) + days * 86_400_000).toISOString().slice(0, 10);
|
|
221
|
+
|
|
180
222
|
/**
|
|
181
|
-
* The
|
|
223
|
+
* The 95% Wilson interval of a share.
|
|
182
224
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
225
|
+
* @param {number} k - How many.
|
|
226
|
+
* @param {number} n - Out of how many.
|
|
227
|
+
* @returns {[number, number]}
|
|
228
|
+
*/
|
|
229
|
+
export function wilson(k, n) {
|
|
230
|
+
if (n === 0) return [0, 1];
|
|
231
|
+
const z = 1.96;
|
|
232
|
+
const p = k / n;
|
|
233
|
+
const d = 1 + (z * z) / n;
|
|
234
|
+
const c = p + (z * z) / (2 * n);
|
|
235
|
+
const m = z * Math.sqrt((p * (1 - p)) / n + (z * z) / (4 * n * n));
|
|
236
|
+
return [(c - m) / d, (c + m) / d];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* How often a lesson's stage had its work sent back before the lesson was written and after, stage by stage.
|
|
241
|
+
*
|
|
242
|
+
* It measured the rate of the stage's own verdicts. For a lesson about the implementer that was its BLOCKED
|
|
243
|
+
* rate, mostly unreadable, while the reviewer's rejections of its work — what the lesson was about — went to
|
|
244
|
+
* the reviewer's account; a lesson for several stages pooled them and read one model; and "before" and
|
|
245
|
+
* "after" were all of history either side, so a lesson written in a project's first days had nothing to
|
|
246
|
+
* compare. Now a stage is read on its own, over the same fourteen days either side, as the share of its
|
|
247
|
+
* rounds that a later stage sent back to it (`sentBackTo`), beside the same share for every other stage the
|
|
248
|
+
* graph sends work back to. A change is called only where the two intervals do not overlap. Not proof — the
|
|
249
|
+
* work changes too — but a number where one can be had, and the reason where it cannot.
|
|
185
250
|
*
|
|
186
251
|
* @param {object[]} records - Recorded dispatches.
|
|
187
252
|
* @param {Awaited<ReturnType<typeof lessons>>} known - The project's pills.
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* @returns {{rel: string, date: string, on: string|null, comparable: boolean, was?: string, before?: {n: number, loops: number}, after?: {n: number, loops: number}, control?: {before: {n: number, loops: number}, after: {n: number, loops: number}}}[]}
|
|
253
|
+
* @param {{owners?: Map<object, string>, targets?: Set<string>|null}} [graph] - Who each loop-back sent work
|
|
254
|
+
* back to, and the stages the graph sends work back to at all.
|
|
255
|
+
* @returns {{rel: string, date: string, role: string, comparable: boolean, why?: string, on?: string|null,
|
|
256
|
+
* was?: string, before?: {n: number, loops: number}, after?: {n: number, loops: number},
|
|
257
|
+
* change?: 'lower'|'higher'|'unclear', control?: {before: {n: number, loops: number}, after: {n: number, loops: number}}}[]}
|
|
194
258
|
*/
|
|
195
|
-
export function verified(records, known) {
|
|
259
|
+
export function verified(records, known, { owners = new Map(), targets = null } = {}) {
|
|
196
260
|
const out = [];
|
|
261
|
+
const ownerOf = (r) => owners.get(r) ?? r.role;
|
|
262
|
+
const sentBack = records.filter((r) => readable(r.verdict) && isLoopBack(r.verdict));
|
|
263
|
+
const ranOn = (r) => (r.usage_model ? `${r.usage_model}${r.effort ? ` · ${r.effort}` : ''}` : null);
|
|
197
264
|
for (const lesson of known.filter((l) => l.date)) {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
265
|
+
const from = shift(lesson.date, -VERIFY_DAYS);
|
|
266
|
+
const until = shift(lesson.date, VERIFY_DAYS);
|
|
267
|
+
const pre = (r) => day(r.ts) >= from && day(r.ts) < lesson.date;
|
|
268
|
+
const post = (r) => day(r.ts) > lesson.date && day(r.ts) <= until;
|
|
269
|
+
for (const role of lesson.roles) {
|
|
270
|
+
const base = { rel: lesson.rel, date: lesson.date, role };
|
|
271
|
+
if (targets && !targets.has(role)) {
|
|
272
|
+
out.push({ ...base, comparable: false, why: `nothing in the graph sends work back to ${role}` });
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const rounds = records.filter((r) => r.role === role && readable(r.verdict));
|
|
276
|
+
const backs = sentBack.filter((r) => ownerOf(r) === role);
|
|
277
|
+
const side = (pick, mine) => {
|
|
278
|
+
const n = mine.filter(pick).length;
|
|
279
|
+
return { n, loops: Math.min(n, backs.filter(pick).length) };
|
|
280
|
+
};
|
|
281
|
+
if (side(pre, rounds).n < VERIFY_MIN || side(post, rounds).n < VERIFY_MIN) {
|
|
282
|
+
const [b, a] = [side(pre, rounds).n, side(post, rounds).n];
|
|
283
|
+
out.push({ ...base, comparable: false, why: `too few rounds of ${role} either side (${b} before, ${a} after, in ${VERIFY_DAYS} days each)` });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
// Read on the model and effort the stage ran on most after the lesson: one written the week its stage moved
|
|
287
|
+
// to a stricter model read as making it worse — one reviewer went from 2% to 29%, and from Sonnet to Opus.
|
|
288
|
+
const tally = new Map();
|
|
289
|
+
for (const r of rounds.filter(post)) if (ranOn(r)) tally.set(ranOn(r), (tally.get(ranOn(r)) ?? 0) + 1);
|
|
290
|
+
const on = [...tally].sort((x, y) => y[1] - x[1])[0]?.[0] ?? null;
|
|
291
|
+
const mine = on ? rounds.filter((r) => ranOn(r) === on) : rounds;
|
|
292
|
+
const before = side(pre, mine);
|
|
293
|
+
const after = side(post, mine);
|
|
294
|
+
if (before.n < VERIFY_MIN || after.n < VERIFY_MIN) {
|
|
295
|
+
const was = [...new Set(rounds.filter(pre).map(ranOn).filter(Boolean))].join(', ') || 'no recorded model';
|
|
296
|
+
out.push({ ...base, on, was, comparable: false, why: `${role} ran on ${on} after it, and before it on ${was}` });
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const [bLo, bHi] = wilson(before.loops, before.n);
|
|
300
|
+
const [aLo, aHi] = wilson(after.loops, after.n);
|
|
301
|
+
const change = aHi < bLo ? 'lower' : aLo > bHi ? 'higher' : 'unclear';
|
|
302
|
+
const others = [...(targets ?? new Set(sentBack.map(ownerOf)))].filter((x) => x !== role);
|
|
303
|
+
const theirs = records.filter((r) => others.includes(r.role) && readable(r.verdict));
|
|
304
|
+
const otherBacks = sentBack.filter((r) => others.includes(ownerOf(r)));
|
|
305
|
+
const control = (pick) => ({ n: theirs.filter(pick).length, loops: Math.min(theirs.filter(pick).length, otherBacks.filter(pick).length) });
|
|
306
|
+
out.push({ ...base, comparable: true, on, before, after, change, control: { before: control(pre), after: control(post) } });
|
|
222
307
|
}
|
|
223
|
-
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Lessons that came back after the rule they graduated into reached this project: their newest request is
|
|
314
|
+
* closed, and the pill has been counted more times since it was filed. The pill is retired by then, and
|
|
315
|
+
* nothing read a retired pill's count — one project's lesson graduated twice and was learned a third time,
|
|
316
|
+
* while two retired pills went from 3 to 8 and from 5 to 11 without a word to anyone.
|
|
317
|
+
*
|
|
318
|
+
* @param {Awaited<ReturnType<typeof lessons>>} known - The project's pills, retired ones included.
|
|
319
|
+
* @param {Awaited<ReturnType<typeof filedRequests>>} filed - Its requests.
|
|
320
|
+
* @returns {{lesson: object, request: object, recurred: number}[]}
|
|
321
|
+
*/
|
|
322
|
+
export function relapsed(known, filed) {
|
|
323
|
+
// A pill with a request still open is waiting on the harness, whatever the dates say. Of the closed ones, the
|
|
324
|
+
// newest by date — and on one date the one that counted most, since a reopening is filed after what it reopens;
|
|
325
|
+
// by the order a directory lists them, a reopening filed the same day read as older, and was filed again.
|
|
326
|
+
const open = new Set(filed.filter((r) => r.status === 'open').map((r) => r.pill));
|
|
327
|
+
const newest = new Map();
|
|
328
|
+
for (const r of filed) {
|
|
329
|
+
if (!r.pill || r.status !== 'closed' || open.has(r.pill)) continue;
|
|
330
|
+
const had = newest.get(r.pill);
|
|
331
|
+
if (!had || r.date > had.date || (r.date === had.date && Number(r.occurrences) > Number(had.occurrences))) newest.set(r.pill, r);
|
|
332
|
+
}
|
|
333
|
+
const out = [];
|
|
334
|
+
for (const [pill, request] of newest) {
|
|
335
|
+
const shelved = pill.replace(/^\.claude\/pills\//, `.claude/pills/${RETIRED}/`);
|
|
336
|
+
const lesson = known.find((l) => l.rel === pill || l.rel === shelved);
|
|
337
|
+
const then = Number(request.occurrences);
|
|
338
|
+
if (lesson && Number.isFinite(then) && lesson.occurrences > then) out.push({ lesson, request, recurred: lesson.occurrences - then });
|
|
224
339
|
}
|
|
225
340
|
return out;
|
|
226
341
|
}
|
|
@@ -250,7 +365,17 @@ export async function filedRequests(target) {
|
|
|
250
365
|
const out = [];
|
|
251
366
|
for (const f of (await readdir(dir).catch(() => [])).filter((f) => f.endsWith('.md'))) {
|
|
252
367
|
const fields = frontmatter(await readFile(join(dir, f), 'utf8')) ?? {};
|
|
253
|
-
out.push({
|
|
368
|
+
out.push({
|
|
369
|
+
file: f,
|
|
370
|
+
id: f.replace(/\.md$/, ''),
|
|
371
|
+
status: fields.status ?? '',
|
|
372
|
+
pill: fields.pill ?? '',
|
|
373
|
+
target: fields.target ?? '',
|
|
374
|
+
date: fields.date ?? '',
|
|
375
|
+
occurrences: fields.occurrences ?? '',
|
|
376
|
+
rule_in: fields.rule_in ?? '',
|
|
377
|
+
answered_in: fields.answered_in ?? '',
|
|
378
|
+
});
|
|
254
379
|
}
|
|
255
380
|
return out;
|
|
256
381
|
}
|
|
@@ -474,15 +599,23 @@ async function deepReport(target, records, known, argv) {
|
|
|
474
599
|
* @param {Awaited<ReturnType<typeof lessons>>[number]} lesson - A lesson that names its roles.
|
|
475
600
|
* @returns {Promise<{file: string, layer: string, why: string}>}
|
|
476
601
|
*/
|
|
477
|
-
async function fileRequest(target, profile, layerRoot, lesson) {
|
|
602
|
+
async function fileRequest(target, profile, layerRoot, lesson, { reopens = null, recurred = 0 } = {}) {
|
|
478
603
|
const target_ = graduationTarget(lesson.roles, await roleGates(layerRoot));
|
|
479
604
|
const date = localDay(new Date().toISOString());
|
|
605
|
+
// Asked about the pill where it was learned, a retired one included: its request names that path.
|
|
606
|
+
const pill = reopens?.pill ?? lesson.rel;
|
|
480
607
|
// The directory separator becomes `__`, so `reviewer/a.b.md` and `reviewer-a/b.md` stay two
|
|
481
608
|
// names — flattening both `/` and `.` to `-` gave them the same one.
|
|
482
|
-
const name =
|
|
609
|
+
const name = pill.replace(/^\.claude\/pills\//, '').replace(/\.md$/, '').replace(/\//g, '__');
|
|
483
610
|
const dir = join(target, HARNESS, 'requests');
|
|
484
|
-
|
|
611
|
+
// Never over a request already written: the same lesson can be sent more than once on one day.
|
|
612
|
+
let file = `${date}-${name}.md`;
|
|
613
|
+
for (let n = 1; existsSync(join(dir, file)); n += 1) file = `${date}-${name}-again${n > 1 ? `-${n}` : ''}.md`;
|
|
485
614
|
await mkdir(dir, { recursive: true });
|
|
615
|
+
const heading = reopens
|
|
616
|
+
? `**A lesson that graduated into \`${reopens.rule_in || reopens.target}\` in ${reopens.answered_in || 'a release'} came back ${recurred} time(s) since** — ` +
|
|
617
|
+
`the rule did not hold here. It reopens ${HARNESS}/requests/${reopens.file}.`
|
|
618
|
+
: `**A lesson this project has learned ${lesson.occurrences} time(s), proposed as a rule in \`${target_.layer}\`** — ${target_.why}.`;
|
|
486
619
|
await writeFile(
|
|
487
620
|
join(dir, file),
|
|
488
621
|
[
|
|
@@ -491,12 +624,13 @@ async function fileRequest(target, profile, layerRoot, lesson) {
|
|
|
491
624
|
'status: open',
|
|
492
625
|
`date: ${date}`,
|
|
493
626
|
`core: ${profile.core}`,
|
|
494
|
-
`target: ${target_.layer}`,
|
|
495
|
-
`pill: ${
|
|
627
|
+
`target: ${reopens?.target || target_.layer}`,
|
|
628
|
+
`pill: ${pill}`,
|
|
496
629
|
`occurrences: ${lesson.occurrences}`,
|
|
630
|
+
...(reopens ? [`reopens: ${reopens.file}`, `rule_in: ${oneLine(reopens.rule_in)}`] : []),
|
|
497
631
|
'---',
|
|
498
632
|
'',
|
|
499
|
-
|
|
633
|
+
heading,
|
|
500
634
|
'',
|
|
501
635
|
`Learned against core ${profile.core}. The rule lands in a release of the harness, and the`,
|
|
502
636
|
'`nina upgrade` that installs that release closes this request and retires the pill — not before.',
|
|
@@ -504,6 +638,7 @@ async function fileRequest(target, profile, layerRoot, lesson) {
|
|
|
504
638
|
lesson.body,
|
|
505
639
|
'',
|
|
506
640
|
].join('\n'),
|
|
641
|
+
{ flag: 'wx' },
|
|
507
642
|
);
|
|
508
643
|
return { file, layer: target_.layer, why: target_.why };
|
|
509
644
|
}
|
|
@@ -589,7 +724,11 @@ export async function learn(argv, ctx) {
|
|
|
589
724
|
}
|
|
590
725
|
if (argv.includes('--close')) return close(target, argv[argv.indexOf('--close') + 1] ?? '');
|
|
591
726
|
|
|
592
|
-
|
|
727
|
+
// Who each loop-back sent work back to, read off the project's own graph.
|
|
728
|
+
const loops = loadProject(target)?.loops ?? null;
|
|
729
|
+
const owners = sentBackTo(records, loops);
|
|
730
|
+
const targets = loops ? new Set([...loops.values()].flatMap((byToken) => [...byToken.values()].flatMap((to) => [...to.keys()]))) : null;
|
|
731
|
+
const late = overdue(records, known, new Date(), days, owners);
|
|
593
732
|
const filed = await filedRequests(target);
|
|
594
733
|
const owed = unfiled(known, filed);
|
|
595
734
|
|
|
@@ -614,20 +753,22 @@ export async function learn(argv, ctx) {
|
|
|
614
753
|
// once. The per-line actions matter too: one hint for the whole detector told a project to
|
|
615
754
|
// "write the lesson" when the lesson was written three times over and only needed sending.
|
|
616
755
|
const sent = [];
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
756
|
+
// A lesson that came back after its rule shipped goes back to the harness the way it went the first time.
|
|
757
|
+
const outgoing = [...owed.map((lesson) => ({ lesson })), ...relapsed(known, filed).map(({ lesson, request, recurred }) => ({ lesson, reopens: request, recurred }))];
|
|
758
|
+
const stuck = outgoing
|
|
759
|
+
.filter((o) => o.lesson.roles.length === 0)
|
|
760
|
+
.map((o) => ({ ...o, why: 'names no role in applies_to, so no layer can take it', fix: 'fix its frontmatter — `nina pills` says what is wrong' }));
|
|
620
761
|
// Sent from the Stop hook, a request was news only the person heard: its one line said the model would
|
|
621
762
|
// be told, and by the next message the lesson was no longer owed, so nothing told it. It is sent by the
|
|
622
763
|
// run that hands its findings to the model, or by hand; the Stop hook says it is about to go.
|
|
623
|
-
const due = beforeModel() ?
|
|
624
|
-
for (const
|
|
764
|
+
const due = beforeModel() ? outgoing.filter((o) => o.lesson.roles.length > 0) : [];
|
|
765
|
+
for (const o of beforeModel() ? [] : outgoing.filter((o) => o.lesson.roles.length > 0)) {
|
|
625
766
|
// A detector that throws is reported as one that could not run, every turn, and takes the
|
|
626
767
|
// rest of its findings with it — so a request that cannot be written is a finding instead.
|
|
627
768
|
try {
|
|
628
|
-
sent.push({
|
|
769
|
+
sent.push({ ...o, ...(await fileRequest(target, profile, resolved.dir, o.lesson, o)) });
|
|
629
770
|
} catch (error) {
|
|
630
|
-
stuck.push({
|
|
771
|
+
stuck.push({ ...o, why: `could not be written to ${HARNESS}/requests/ — ${error.message}`, fix: `make it writable, or send it by hand: \`nina learn --graduate ${o.lesson.rel}\`` });
|
|
631
772
|
}
|
|
632
773
|
}
|
|
633
774
|
if (late.length === 0 && sent.length === 0 && due.length === 0 && stuck.length === 0 && !exported) {
|
|
@@ -643,15 +784,19 @@ export async function learn(argv, ctx) {
|
|
|
643
784
|
for (const r of o.recent) console.log(` ${r.ts} ${r.desc || '(no description)'} session ${r.session}`);
|
|
644
785
|
console.log(' → ask of each whether it would happen again; if so, write the pill, or bump `occurrences` and `last_seen` on the one that already says it');
|
|
645
786
|
}
|
|
787
|
+
const what = (o) =>
|
|
788
|
+
o.reopens
|
|
789
|
+
? `${o.lesson.rel} came back ${o.recurred} time(s) after its rule shipped in ${o.reopens.answered_in || 'a release'} (${o.reopens.rule_in || o.reopens.target})`
|
|
790
|
+
: `${o.lesson.rel} has recurred ${o.lesson.occurrences} times`;
|
|
646
791
|
for (const s of sent) {
|
|
647
|
-
console.log(` ${s
|
|
792
|
+
console.log(` ${what(s)} — sent to the harness as ${HARNESS}/requests/${s.file}${s.reopens ? ', reopening the rule' : `, proposing a rule in ${s.layer}`}`);
|
|
648
793
|
console.log(' → commit it with the pill; nothing else is owed here. `nina upgrade` to the release that answers it closes it and retires the pill');
|
|
649
794
|
}
|
|
650
|
-
for (const
|
|
651
|
-
console.log(` ${
|
|
795
|
+
for (const o of due) {
|
|
796
|
+
console.log(` ${what(o)} — it goes to the harness as a request before your next message`);
|
|
652
797
|
}
|
|
653
798
|
for (const s of stuck) {
|
|
654
|
-
console.log(` ${s
|
|
799
|
+
console.log(` ${what(s)} and ${s.why}`);
|
|
655
800
|
console.log(` → ${s.fix}`);
|
|
656
801
|
}
|
|
657
802
|
if (exported) console.log(` ${exported}`);
|
|
@@ -673,7 +818,7 @@ export async function learn(argv, ctx) {
|
|
|
673
818
|
const ready = active.filter((l) => l.occurrences >= GRADUATION_AT);
|
|
674
819
|
const open = filed.filter((r) => r.status === 'open');
|
|
675
820
|
const use = applied(records, known);
|
|
676
|
-
const checks = verified(records, known);
|
|
821
|
+
const checks = verified(records, known, { owners, targets });
|
|
677
822
|
const span = records.length ? `${day(records[0].ts)} → ${day(records.at(-1).ts)}` : '';
|
|
678
823
|
|
|
679
824
|
console.log(`\n learning — core ${profile.core}\n`);
|
|
@@ -703,19 +848,28 @@ export async function learn(argv, ctx) {
|
|
|
703
848
|
);
|
|
704
849
|
for (const l of ready.filter((l) => owed.includes(l))) console.log(` not sent yet: ${l.rel} — the next \`harness:check\` sends it`);
|
|
705
850
|
for (const r of open) console.log(` open since ${r.date}: ${r.pill} → ${r.target}. \`nina upgrade\` to the release that answers it closes it`);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
851
|
+
for (const r of relapsed(known, filed)) {
|
|
852
|
+
console.log(` ✗ ${r.lesson.rel} came back ${r.recurred} time(s) after its rule shipped in ${r.request.answered_in || 'a release'} — the rule did not hold; \`harness:check\` reopens it`);
|
|
853
|
+
}
|
|
854
|
+
const measured = checks.filter((c) => c.comparable);
|
|
855
|
+
const unmeasured = checks.filter((c) => !c.comparable);
|
|
856
|
+
if (measured.length === 0) console.log(` verify no lesson could be measured yet — ${VERIFY_MIN}+ rounds of its stage on the same model are needed in the ${VERIFY_DAYS} days either side of its date`);
|
|
857
|
+
measured.forEach((c, i) => {
|
|
858
|
+
const said = { lower: 'sent back less since', higher: 'sent back more since', unclear: 'no clear change' }[c.change];
|
|
713
859
|
console.log(
|
|
714
|
-
|
|
715
|
-
`${pct(c.before.loops, c.before.n)} → ${pct(c.after.loops, c.after.n)}
|
|
716
|
-
`
|
|
860
|
+
` ${i === 0 ? 'verify ' : ' '} ${c.rel.replace(/^\.claude\/pills\//, '')} (${c.date}) · ${c.role}${c.on ? `, on ${c.on}` : ''}: ` +
|
|
861
|
+
`${pct(c.before.loops, c.before.n)} → ${pct(c.after.loops, c.after.n)} of its rounds sent back (n ${c.before.n} → ${c.after.n}) — ${said}; ` +
|
|
862
|
+
`the other stages ${pct(c.control.before.loops, c.control.before.n)} → ${pct(c.control.after.loops, c.control.after.n)}`,
|
|
717
863
|
);
|
|
718
864
|
});
|
|
865
|
+
if (unmeasured.length > 0) {
|
|
866
|
+
const reasons = new Map();
|
|
867
|
+
for (const c of unmeasured) {
|
|
868
|
+
const reason = c.why.startsWith('too few') ? 'too few rounds either side' : c.why.startsWith('nothing') ? 'a stage nothing sends work back to' : 'a stage that changed model across its date';
|
|
869
|
+
reasons.set(reason, (reasons.get(reason) ?? 0) + 1);
|
|
870
|
+
}
|
|
871
|
+
console.log(` ${unmeasured.length} more could not be measured: ${[...reasons].map(([why, n]) => `${n} ${why}`).join(', ')}`);
|
|
872
|
+
}
|
|
719
873
|
// The loops the lessons are about, as the gate counted them — beside the lessons, because a loop the
|
|
720
874
|
// gate held is a lesson waiting to be asked for.
|
|
721
875
|
const since = new Date(Date.now() - days * 86_400_000).toISOString();
|
|
@@ -858,7 +1012,8 @@ export async function requests(argv = [], ctx = { root: '.' }) {
|
|
|
858
1012
|
}
|
|
859
1013
|
// The maintainer's side of graduation used to be "run nina requests and remember to" — the one
|
|
860
1014
|
// step with nothing to prompt it. This repo's own harness:check runs this every turn.
|
|
861
|
-
|
|
1015
|
+
const reopened = (r) => (r.fields.reopens ? ` — reopens a rule that did not hold (${r.fields.rule_in || r.fields.target})` : '');
|
|
1016
|
+
for (const r of unanswered) console.log(` ${r.fields.date} ${r.fields.pill} → ${r.fields.target} (core ${r.fields.core})${reopened(r)} ${where(r)}`);
|
|
862
1017
|
for (const r of unreleased) console.log(` answered, not released: ${r.id}`);
|
|
863
1018
|
console.log(
|
|
864
1019
|
`requests: ${[
|
|
@@ -881,7 +1036,7 @@ export async function requests(argv = [], ctx = { root: '.' }) {
|
|
|
881
1036
|
: working[r.id]
|
|
882
1037
|
? 'answered in the working core — cut a release to carry it'
|
|
883
1038
|
: `waiting — nina requests --answer ${r.id} --in <layer file>, or --decline … --why …`;
|
|
884
|
-
console.log(` ${r.fields.date} ${r.fields.target.padEnd(20)} core ${r.fields.core} ${r.fields.pill}`);
|
|
1039
|
+
console.log(` ${r.fields.date} ${r.fields.target.padEnd(20)} core ${r.fields.core} ${r.fields.pill}${r.fields.reopens ? ` — reopens a rule that did not hold (${r.fields.rule_in || r.fields.target})` : ''}`);
|
|
885
1040
|
console.log(` ${where(r)}`);
|
|
886
1041
|
console.log(` ${state}`);
|
|
887
1042
|
}
|
package/src/commands/pills.mjs
CHANGED
|
@@ -81,14 +81,17 @@ const CITATION = /^[^\s:]+(:\d+)?$/;
|
|
|
81
81
|
* @param {string} text - The whole file.
|
|
82
82
|
* @returns {Record<string, string> | null} The fields, or `null` when there is no frontmatter.
|
|
83
83
|
*/
|
|
84
|
-
export function frontmatter(
|
|
84
|
+
export function frontmatter(raw) {
|
|
85
|
+
// Saved on Windows, a pill read as having no frontmatter at all; and `occurrences: 4 # seen in 5.1` read as
|
|
86
|
+
// 1 — a comment after a value is YAML's, and ends at the value.
|
|
87
|
+
const text = String(raw ?? '').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
|
|
85
88
|
if (!text.startsWith('---\n')) return null;
|
|
86
89
|
const end = text.indexOf('\n---', 3);
|
|
87
90
|
if (end === -1) return null;
|
|
88
91
|
const fields = {};
|
|
89
92
|
for (const line of text.slice(4, end + 1).split('\n')) {
|
|
90
93
|
const field = /^([a-z_]+):\s*(.*)$/.exec(line);
|
|
91
|
-
if (field) fields[field[1]] = field[2].trim();
|
|
94
|
+
if (field) fields[field[1]] = field[2].replace(/\s+#.*$/, '').trim();
|
|
92
95
|
}
|
|
93
96
|
return fields;
|
|
94
97
|
}
|
|
@@ -308,6 +311,9 @@ export function validate(pill, text, roles, today) {
|
|
|
308
311
|
problems.push(`${label} has last_seen "${fields.last_seen}" — expected YYYY-MM-DD`);
|
|
309
312
|
} else if (fields.last_seen && fields.date && fields.last_seen < fields.date) {
|
|
310
313
|
problems.push(`${label} was last seen ${fields.last_seen}, before it was first learned (${fields.date})`);
|
|
314
|
+
} else if (fields.last_seen && fields.last_seen > today) {
|
|
315
|
+
// The capture detector counts loop-backs after a stage's newest lesson: one seen in the future silenced it.
|
|
316
|
+
problems.push(`${label} was last seen ${fields.last_seen}, which is in the future — no loop-back of its stage would be counted until then`);
|
|
311
317
|
}
|
|
312
318
|
if (fields.occurrences !== undefined && !/^[1-9]\d*$/.test(fields.occurrences)) {
|
|
313
319
|
problems.push(`${label} has occurrences "${fields.occurrences}" — expected a positive whole number`);
|
|
@@ -384,6 +390,15 @@ export async function pills(argv, ctx) {
|
|
|
384
390
|
/* without a profile the candidate is still worth naming; only its target is unknown */
|
|
385
391
|
}
|
|
386
392
|
|
|
393
|
+
// What this project has already asked the harness for, by pill: a request is how a lesson leaves a project.
|
|
394
|
+
const requested = new Map();
|
|
395
|
+
for (const f of (await readdir(join(target, HARNESS, 'requests')).catch(() => [])).filter((f) => f.endsWith('.md'))) {
|
|
396
|
+
const fields = frontmatter(await readFile(join(target, HARNESS, 'requests', f), 'utf8').catch(() => ''));
|
|
397
|
+
if (fields?.pill) requested.set(fields.pill, fields);
|
|
398
|
+
}
|
|
399
|
+
/** Every stage a pill was ever written for, by its directory or its applies_to, retired ones included. */
|
|
400
|
+
const taught = new Set();
|
|
401
|
+
|
|
387
402
|
const changedCache = new Map();
|
|
388
403
|
const candidates = [];
|
|
389
404
|
let resolved = 0;
|
|
@@ -393,7 +408,9 @@ export async function pills(argv, ctx) {
|
|
|
393
408
|
const result = validate(pill, await readFile(pill.path, 'utf8'), roles, today);
|
|
394
409
|
problems.push(...result.problems);
|
|
395
410
|
notes.push(...result.notes);
|
|
396
|
-
if (
|
|
411
|
+
if (pill.dir && pill.dir !== 'shared' && pill.dir !== RETIRED) taught.add(pill.dir);
|
|
412
|
+
for (const role of list(result.fields?.applies_to)) taught.add(role);
|
|
413
|
+
if (result.uncited && result.fields?.status !== 'retired') uncited += 1;
|
|
397
414
|
if (result.fields?.status === 'retired') retired += 1;
|
|
398
415
|
else active.push({ ...pill, date: result.fields?.date });
|
|
399
416
|
|
|
@@ -429,23 +446,29 @@ export async function pills(argv, ctx) {
|
|
|
429
446
|
if (cited > 0) notes.push(`${resolved} of ${cited} citation(s) still resolve`);
|
|
430
447
|
|
|
431
448
|
// A lesson learned three times is not a correction any more; it is a rule nobody wrote down.
|
|
449
|
+
// A project cannot write the harness, and a pill retired before the release that carries its rule arrives
|
|
450
|
+
// leaves the lesson nowhere: it goes as a request, and waits.
|
|
432
451
|
for (const c of candidates) {
|
|
433
|
-
const
|
|
452
|
+
const home = gates && c.applies.length > 0 ? graduationTarget(c.applies, gates) : null;
|
|
453
|
+
const asked = requested.get(`.claude/pills/${c.label}`);
|
|
434
454
|
notes.push(
|
|
435
455
|
`${c.label} has recurred ${c.seen} times — that is a rule, not a pill.` +
|
|
436
|
-
(
|
|
437
|
-
|
|
456
|
+
(home ? ` Its home is ${home.layer}, since ${home.why}.` : '') +
|
|
457
|
+
(asked
|
|
458
|
+
? ` It went to the harness as a request on ${asked.date} (${asked.status}); the \`nina upgrade\` to the release that answers it retires this pill.`
|
|
459
|
+
: ' `harness:check` sends it to the harness as a request, or `nina learn --graduate` does now; keep it active until the release that answers it retires it.'),
|
|
438
460
|
);
|
|
439
461
|
}
|
|
440
462
|
|
|
441
463
|
// One line, not one per pill: what matters is how much of the corpus a staleness check
|
|
442
464
|
// could ever reach, and naming eleven files says that worse than counting them.
|
|
443
465
|
if (uncited > 0) {
|
|
444
|
-
notes.push(`${uncited} of ${
|
|
466
|
+
notes.push(`${uncited} of ${active.length} active pill(s) cite no code, so staleness cannot be checked mechanically`);
|
|
445
467
|
}
|
|
446
468
|
|
|
447
|
-
|
|
448
|
-
|
|
469
|
+
// Read off each pill's applies_to as well as its directory: counted by directory, a project whose lessons were
|
|
470
|
+
// all shared was told none had ever been written for the stages they named.
|
|
471
|
+
const uncovered = roles.filter((r) => !taught.has(r));
|
|
449
472
|
if (uncovered.length > 0 && files.length > 0) {
|
|
450
473
|
notes.push(`no pill has ever been written for ${uncovered.join(', ')}`);
|
|
451
474
|
}
|
package/src/commands/stats.mjs
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* can be judged by whether it ever stops anything rather than by whether it is in the
|
|
6
6
|
* table.
|
|
7
7
|
*
|
|
8
|
-
* It also reports the one rule that governs the harness's own improvement
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* It also reports the one rule that governs the harness's own improvement: every loop-back is looked
|
|
9
|
+
* at for a lesson. Nothing ever checked whether that happened — the rule ran at a fraction of its
|
|
10
|
+
* declared rate for months, invisibly, while the loop-backs it was supposed to harvest were being
|
|
11
|
+
* counted right here.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { amber, bar, bold, dim, heading, note, pink, stacked } from '../look.mjs';
|
package/src/deep.mjs
CHANGED
|
@@ -152,6 +152,28 @@ export function reducePrompt(causes, known) {
|
|
|
152
152
|
].join('\n');
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The causes a batch's answer adds: one per report the batch held, the first the answer gave for it. Checked
|
|
157
|
+
* against the causes already taken, a ref an answer repeated was taken twice, and the count of reports given
|
|
158
|
+
* no cause came out short by as many.
|
|
159
|
+
*
|
|
160
|
+
* @param {{ref: string}[]} taken - Causes from earlier batches.
|
|
161
|
+
* @param {{ref: string}[]} batch - The reports this answer is about.
|
|
162
|
+
* @param {unknown[]} answered - What the answer gave.
|
|
163
|
+
* @returns {{ref: string, cause: string}[]}
|
|
164
|
+
*/
|
|
165
|
+
export function newCauses(taken, batch, answered) {
|
|
166
|
+
const refs = new Set(batch.map((r) => r.ref));
|
|
167
|
+
const seen = new Set(taken.map((c) => c.ref));
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const c of Array.isArray(answered) ? answered : []) {
|
|
170
|
+
if (!c || !refs.has(c.ref) || seen.has(c.ref) || typeof c.cause !== 'string') continue;
|
|
171
|
+
seen.add(c.ref);
|
|
172
|
+
out.push(c);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
155
177
|
/**
|
|
156
178
|
* Reads the loop-backs and says what they were about.
|
|
157
179
|
*
|
|
@@ -178,9 +200,7 @@ export async function deepLearn({ records, known, projectDir, since, model, api,
|
|
|
178
200
|
made += 1;
|
|
179
201
|
if (failed || !Array.isArray(answer?.causes)) return stop(failed ?? 'the map answer was not the shape asked for');
|
|
180
202
|
cost += spent;
|
|
181
|
-
|
|
182
|
-
const seen = new Set(causes.map((c) => c.ref));
|
|
183
|
-
causes.push(...answer.causes.filter((c) => refs.has(c.ref) && !seen.has(c.ref) && typeof c.cause === 'string'));
|
|
203
|
+
causes.push(...newCauses(causes, batch, answer.causes));
|
|
184
204
|
}
|
|
185
205
|
progress('reduce');
|
|
186
206
|
const { answer, cost: spent, failed } = ask(reducePrompt(causes, known), REDUCE_SCHEMA, options);
|
|
@@ -189,7 +209,9 @@ export async function deepLearn({ records, known, projectDir, since, model, api,
|
|
|
189
209
|
cost += spent;
|
|
190
210
|
// Believed only as far as it can be checked: refs that were read, and pills that exist.
|
|
191
211
|
const read = new Set(causes.map((c) => c.ref));
|
|
192
|
-
|
|
212
|
+
// Only an active pill covers a cause here: the grouping is shown no retired one, and one named is a lesson that
|
|
213
|
+
// left this project for the harness, not one this project still holds.
|
|
214
|
+
const pills = new Set(known.filter((p) => !p.retired).map((p) => p.rel));
|
|
193
215
|
const clusters = answer.clusters
|
|
194
216
|
.map((c) => ({
|
|
195
217
|
...c,
|
package/src/gate.mjs
CHANGED
|
@@ -60,6 +60,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
60
60
|
import { TERMINALS, declaredTokens, parseGraph } from './graph.mjs';
|
|
61
61
|
import { gateDir, slugFor } from './paths.mjs';
|
|
62
62
|
import { declaredIssues, isLoopBack, saidToAgent } from './transcripts.mjs';
|
|
63
|
+
import { lessonIndex } from './lessons.mjs';
|
|
63
64
|
|
|
64
65
|
/** The package this runs from: its releases say whether a pinned version ships the gate at all. */
|
|
65
66
|
const PACKAGE = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -718,6 +719,15 @@ export function handle(input, { root, now = new Date(), pkg = PACKAGE }) {
|
|
|
718
719
|
return null;
|
|
719
720
|
case 'SubagentStop':
|
|
720
721
|
return onStop(input, project, path, at);
|
|
722
|
+
case 'SubagentStart': {
|
|
723
|
+
// Not the loop's business, but the one hook every stage passes through as it begins: its lessons. Claude
|
|
724
|
+
// Code reads `additionalContext` from this event's `hookSpecificOutput` into the subagent's context as it
|
|
725
|
+
// starts — its hook schema and handler say so in 2.1.283, and the handler in 2.1.158 — though the help text
|
|
726
|
+
// it prints for a malformed hook output does not list the event.
|
|
727
|
+
const role = input.agent_type;
|
|
728
|
+
const text = role && project.graph.stages.has(role) ? lessonIndex(root, role) : null;
|
|
729
|
+
return text ? { hookSpecificOutput: { hookEventName: 'SubagentStart', additionalContext: text } } : null;
|
|
730
|
+
}
|
|
721
731
|
case 'PostToolUse':
|
|
722
732
|
return onPost(input, project, path, at);
|
|
723
733
|
case 'PreToolUse':
|