backpass 0.1.11 → 0.1.13
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 -12
- package/package.json +1 -1
- package/src/analyze.js +32 -11
- package/src/cli.js +3 -3
- package/src/commands/bootstrap.js +4 -2
- package/src/commands/propose.js +21 -1
- package/src/config.js +4 -3
- package/src/consolidate.js +110 -0
- package/src/diff.js +2 -2
- package/src/discovery/index.js +13 -4
- package/src/fold.js +50 -9
- package/src/gap-ledger.js +137 -20
- package/src/prompts/analysis.md +28 -7
- package/src/prompts/annotate.md +14 -8
- package/src/prompts/consolidate.md +29 -0
- package/src/prompts/synthesis.md +15 -6
- package/src/proposal.js +64 -0
- package/src/sample.js +39 -17
- package/src/state.js +57 -12
- package/src/synthesize.js +6 -4
- package/src/transcript.js +15 -0
- package/src/workspace.js +129 -1
package/src/fold.js
CHANGED
|
@@ -12,7 +12,10 @@ import { GAP_SIMILARITY_THRESHOLD, gapSource } from "./gap-ledger.js";
|
|
|
12
12
|
* evidence / sessions analyzed. That ratio is what decides memory-file vs skill
|
|
13
13
|
* placement in section 7.
|
|
14
14
|
* 2. Near-duplicate gaps from different sessions are clustered, so "three sessions
|
|
15
|
-
* re-derived the db schema" arrives as one item with three quotes.
|
|
15
|
+
* re-derived the db schema" arrives as one item with three quotes. Judged identity
|
|
16
|
+
* happens upstream (analysis citations and the consolidation pass reshape the
|
|
17
|
+
* ledger); the clustering here stays deterministic. Orchestration-domain sightings
|
|
18
|
+
* are excluded before clustering and surfaced only as a count.
|
|
16
19
|
* 3. Gap clusters below `minGapEvidence` are dropped. Batch size > 1: one bad session
|
|
17
20
|
* never rewrites the weights. Sessions are counted across runs, not per run: when the
|
|
18
21
|
* caller passes `gapObservations` (the pruned gap ledger, `src/gap-ledger.js`) the
|
|
@@ -40,6 +43,7 @@ export function foldEvidence(evidenceRecords, { minGapEvidence = 2, memoryFile =
|
|
|
40
43
|
positive: 0,
|
|
41
44
|
negative: 0,
|
|
42
45
|
sessions: new Set(),
|
|
46
|
+
harmSessions: new Set(),
|
|
43
47
|
quotes: [],
|
|
44
48
|
});
|
|
45
49
|
}
|
|
@@ -55,8 +59,20 @@ export function foldEvidence(evidenceRecords, { minGapEvidence = 2, memoryFile =
|
|
|
55
59
|
for (const item of record[polarity] || []) {
|
|
56
60
|
const entry = touch(item.instruction);
|
|
57
61
|
entry[polarity] += 1;
|
|
58
|
-
|
|
59
|
-
entry.
|
|
62
|
+
const sessionIdentity = record.transcript.identity || record.transcript.id;
|
|
63
|
+
entry.sessions.add(sessionIdentity);
|
|
64
|
+
// `class` is what a negative means (harm vs non-compliance vs irrelevant);
|
|
65
|
+
// `harmSessions` is what the removal-evidence floor counts. A record from
|
|
66
|
+
// before the class existed carries none and never counts as harm.
|
|
67
|
+
if (polarity === "negative" && item.class === "harm") entry.harmSessions.add(sessionIdentity);
|
|
68
|
+
entry.quotes.push({
|
|
69
|
+
polarity,
|
|
70
|
+
text: item.quote,
|
|
71
|
+
effect: item.effect,
|
|
72
|
+
moment: item.moment,
|
|
73
|
+
class: polarity === "negative" ? (item.class ?? null) : undefined,
|
|
74
|
+
source,
|
|
75
|
+
});
|
|
60
76
|
if (polarity === "positive") positiveCount += 1;
|
|
61
77
|
else negativeCount += 1;
|
|
62
78
|
}
|
|
@@ -69,12 +85,20 @@ export function foldEvidence(evidenceRecords, { minGapEvidence = 2, memoryFile =
|
|
|
69
85
|
quote: gap.quote,
|
|
70
86
|
recurrenceRisk: gap.recurrenceRisk,
|
|
71
87
|
source,
|
|
72
|
-
sessionId: record.transcript.id,
|
|
88
|
+
sessionId: record.transcript.identity || record.transcript.id,
|
|
89
|
+
domain: gap.domain === "orchestration" ? "orchestration" : "project",
|
|
73
90
|
});
|
|
74
91
|
}
|
|
75
92
|
}
|
|
76
93
|
|
|
77
|
-
|
|
94
|
+
// Orchestration-domain sightings are mistakes caused not by this repository but by the
|
|
95
|
+
// external agent harness or tooling that orchestrated the session; they are counted for
|
|
96
|
+
// legibility but never cluster, so they can never corroborate into a project proposal.
|
|
97
|
+
const allObservations = gapObservations ?? recordObservations;
|
|
98
|
+
const projectObservations = allObservations.filter((obs) => obs?.domain !== "orchestration");
|
|
99
|
+
const orchestrationGapSightings = allObservations.length - projectObservations.length;
|
|
100
|
+
|
|
101
|
+
const gapClusters = clusterGapObservations(projectObservations);
|
|
78
102
|
|
|
79
103
|
// Instructions that exist in the file but drew no evidence at all are the strongest
|
|
80
104
|
// removal / extraction candidates, so they must appear in the summary too.
|
|
@@ -89,6 +113,7 @@ export function foldEvidence(evidenceRecords, { minGapEvidence = 2, memoryFile =
|
|
|
89
113
|
instruction: entry.instruction,
|
|
90
114
|
positive: entry.positive,
|
|
91
115
|
negative: entry.negative,
|
|
116
|
+
harmSessions: entry.harmSessions.size,
|
|
92
117
|
sessions: entry.sessions.size,
|
|
93
118
|
relevance: analyzedSessions ? entry.sessions.size / analyzedSessions : 0,
|
|
94
119
|
tokens: unit?.tokens ?? null,
|
|
@@ -120,6 +145,7 @@ export function foldEvidence(evidenceRecords, { minGapEvidence = 2, memoryFile =
|
|
|
120
145
|
negative: negativeCount,
|
|
121
146
|
gapClusters: gaps.length,
|
|
122
147
|
droppedGapSingletons,
|
|
148
|
+
orchestrationGapSightings,
|
|
123
149
|
usedRawTranscript: usedRawCount,
|
|
124
150
|
},
|
|
125
151
|
instructions: instructionRows,
|
|
@@ -179,20 +205,35 @@ export function renderEvidenceForPrompt(summary) {
|
|
|
179
205
|
);
|
|
180
206
|
lines.push("");
|
|
181
207
|
lines.push("### Per-instruction evidence");
|
|
208
|
+
lines.push(
|
|
209
|
+
"A negative's class is what it means: `harm` = following the instruction caused damage " +
|
|
210
|
+
"(evidence against it); `non-compliance` = the agent ignored it (evidence it failed to " +
|
|
211
|
+
"steer - argues for reinforcement, never deletion); `irrelevant` = no real bearing.",
|
|
212
|
+
);
|
|
182
213
|
for (const row of summary.instructions) {
|
|
183
214
|
const relevance = `${(row.relevance * 100).toFixed(1)}%`;
|
|
184
215
|
const cost = row.tokens === null ? "" : ` cost=${row.tokens}tok`;
|
|
216
|
+
const harm = row.negative > 0 ? ` harm-sessions=${row.harmSessions ?? 0}` : "";
|
|
185
217
|
lines.push(
|
|
186
|
-
`- [${row.instruction}] +${row.positive} -${row.negative} sessions=${row.sessions} relevance=${relevance}${cost}` +
|
|
218
|
+
`- [${row.instruction}] +${row.positive} -${row.negative}${harm} sessions=${row.sessions} relevance=${relevance}${cost}` +
|
|
187
219
|
(row.known ? "" : " (id not found in current file - stale reference)"),
|
|
188
220
|
);
|
|
189
221
|
for (const quote of row.quotes.slice(0, 3)) {
|
|
190
|
-
|
|
222
|
+
const sign = quote.polarity === "negative" ? "-" : "+";
|
|
223
|
+
const cls = quote.polarity === "negative" ? ` [${quote.class ?? "unclassified"}]` : "";
|
|
224
|
+
const effect = quote.effect ? ` :: ${oneLine(quote.effect, 200)}` : "";
|
|
225
|
+
lines.push(` ${sign}${cls} "${oneLine(quote.text)}"${effect} (${quote.source})`);
|
|
191
226
|
}
|
|
192
227
|
}
|
|
193
228
|
|
|
194
229
|
lines.push("");
|
|
195
230
|
lines.push("### Gap clusters (mistakes no current instruction covers)");
|
|
231
|
+
if (summary.totals.orchestrationGapSightings) {
|
|
232
|
+
lines.push(
|
|
233
|
+
`- ${summary.totals.orchestrationGapSightings} orchestration-domain sighting(s) caused by the ` +
|
|
234
|
+
`orchestrating harness or tooling were excluded; they never enter this repository's memory file`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
196
237
|
if (!summary.gaps.length) {
|
|
197
238
|
lines.push("- none above the evidence threshold");
|
|
198
239
|
}
|
|
@@ -206,9 +247,9 @@ export function renderEvidenceForPrompt(summary) {
|
|
|
206
247
|
return lines.join("\n");
|
|
207
248
|
}
|
|
208
249
|
|
|
209
|
-
function oneLine(text) {
|
|
250
|
+
function oneLine(text, max = 240) {
|
|
210
251
|
const flat = String(text || "")
|
|
211
252
|
.replace(/\s+/g, " ")
|
|
212
253
|
.trim();
|
|
213
|
-
return flat.length >
|
|
254
|
+
return flat.length > max ? `${flat.slice(0, max)}...` : flat;
|
|
214
255
|
}
|
package/src/gap-ledger.js
CHANGED
|
@@ -15,10 +15,23 @@ import { sha256 } from "./state.js";
|
|
|
15
15
|
*
|
|
16
16
|
* Identity and freshness rules:
|
|
17
17
|
*
|
|
18
|
-
* - A gap's identity is
|
|
19
|
-
* the
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* - A gap's identity is judged first and matched lexically second. The analysis turn is
|
|
19
|
+
* shown the ledger's open entries (`renderOpenGapIndex`) and cites an entry id
|
|
20
|
+
* (`matchesGap`) when its gap is one already on the books; a valid citation wins
|
|
21
|
+
* outright, because word overlap cannot recognize a paraphrase and the analysis model
|
|
22
|
+
* has both sentences in front of it. Without a citation, bigram similarity
|
|
23
|
+
* (`GAP_SIMILARITY_THRESHOLD`) against the canonical phrasing (the shortest seen) is
|
|
24
|
+
* the fallback. The entry id is a hash of the first phrasing and never changes, so
|
|
25
|
+
* rephrasing does not split an entry. Two entries later judged to be one gap are
|
|
26
|
+
* merged by the pre-synthesis consolidation pass (`mergeGapEntries`, driven by
|
|
27
|
+
* `src/consolidate.js`), which is what lets two same-run parallel sightings - neither
|
|
28
|
+
* of which could cite the other - still corroborate.
|
|
29
|
+
* - Every observation carries the `domain` the analysis judged: `orchestration` when the
|
|
30
|
+
* mistake was not caused by this repository but by an external agent harness or tooling
|
|
31
|
+
* that orchestrated the task, `project` for every other mistake.
|
|
32
|
+
* Orchestration sightings are recorded for legibility but never counted toward
|
|
33
|
+
* corroboration and never surface in a proposal; a missing domain counts as project,
|
|
34
|
+
* so evidence from before the field existed keeps its old behavior.
|
|
22
35
|
* - Sessions are keyed by transcript id (harness + native session id), so re-analyzing
|
|
23
36
|
* or re-sampling the same session overwrites its observation and never adds a count.
|
|
24
37
|
* - A gap is a fact about its session: re-analysis that no longer mentions it is model
|
|
@@ -61,13 +74,20 @@ export function gapEntryId(memoryPath, proposedInstruction) {
|
|
|
61
74
|
return sha256(`${memoryPath}\n${normalize(proposedInstruction)}`).slice(0, 16);
|
|
62
75
|
}
|
|
63
76
|
|
|
77
|
+
function gapEntryById(ledger, id) {
|
|
78
|
+
const direct = ledger.entries[id];
|
|
79
|
+
if (direct) return direct;
|
|
80
|
+
return Object.values(ledger.entries).find((entry) => (entry.aliases || []).includes(id)) || null;
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
/** The ledger entry a proposed instruction belongs to, or null. */
|
|
65
84
|
export function findGapEntry(ledger, memoryPath, proposedInstruction) {
|
|
66
85
|
let best = null;
|
|
67
86
|
let bestScore = 0;
|
|
68
87
|
for (const entry of Object.values(ledger.entries)) {
|
|
69
88
|
if (entry.memoryPath !== memoryPath) continue;
|
|
70
|
-
const
|
|
89
|
+
const phrasings = [...new Set([entry.proposedInstruction, ...(entry.phrasings || [])])];
|
|
90
|
+
const score = Math.max(...phrasings.map((phrasing) => similarity(phrasing, proposedInstruction)));
|
|
71
91
|
if (score >= GAP_SIMILARITY_THRESHOLD && score > bestScore) {
|
|
72
92
|
best = entry;
|
|
73
93
|
bestScore = score;
|
|
@@ -86,32 +106,56 @@ export function recordGapObservations(ledger, evidenceRecords, { now = new Date(
|
|
|
86
106
|
for (const record of evidenceRecords) {
|
|
87
107
|
if (!record || record.status !== "ok" || !record.memoryPath) continue;
|
|
88
108
|
const transcript = record.transcript || {};
|
|
89
|
-
|
|
109
|
+
const sessionIdentity = transcript.identity || transcript.id;
|
|
110
|
+
if (!sessionIdentity) continue;
|
|
90
111
|
for (const gap of record.gaps || []) {
|
|
91
112
|
if (!gap || !gap.proposedInstruction) continue;
|
|
92
|
-
|
|
113
|
+
// A citation from the analysis turn wins over word overlap: the model saw both
|
|
114
|
+
// sentences and judged them the same gap. An id that names nothing (stale index,
|
|
115
|
+
// typo) falls back to the lexical match rather than failing the record.
|
|
116
|
+
const cited = gap.matchesGap ? gapEntryById(ledger, gap.matchesGap) : null;
|
|
117
|
+
const deterministicId = gapEntryId(record.memoryPath, gap.proposedInstruction);
|
|
118
|
+
const deterministic = gapEntryById(ledger, deterministicId);
|
|
119
|
+
let entry =
|
|
120
|
+
(cited && cited.memoryPath === record.memoryPath ? cited : null) ||
|
|
121
|
+
findGapEntry(ledger, record.memoryPath, gap.proposedInstruction) ||
|
|
122
|
+
(deterministic && deterministic.memoryPath === record.memoryPath ? deterministic : null);
|
|
93
123
|
if (!entry) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
id,
|
|
124
|
+
entry = ledger.entries[deterministicId] = {
|
|
125
|
+
id: deterministicId,
|
|
97
126
|
memoryPath: record.memoryPath,
|
|
98
127
|
proposedInstruction: gap.proposedInstruction,
|
|
128
|
+
phrasings: [gap.proposedInstruction],
|
|
99
129
|
sessions: {},
|
|
100
130
|
};
|
|
101
|
-
} else
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
} else {
|
|
132
|
+
entry.phrasings = [
|
|
133
|
+
...new Set([entry.proposedInstruction, ...(entry.phrasings || []), gap.proposedInstruction]),
|
|
134
|
+
];
|
|
135
|
+
if (gap.proposedInstruction.length < entry.proposedInstruction.length) {
|
|
136
|
+
// Keep the shortest phrasing: it generalizes best (same rule as the in-run fold).
|
|
137
|
+
entry.proposedInstruction = gap.proposedInstruction;
|
|
138
|
+
}
|
|
104
139
|
}
|
|
105
|
-
const
|
|
106
|
-
entry.sessions[transcript.id]
|
|
107
|
-
|
|
140
|
+
const identityPrior = entry.sessions[sessionIdentity];
|
|
141
|
+
const aliasPrior = transcript.id && transcript.id !== sessionIdentity ? entry.sessions[transcript.id] : null;
|
|
142
|
+
const priors = [identityPrior, aliasPrior].filter(Boolean);
|
|
143
|
+
const firstObservedAt = priors
|
|
144
|
+
.map((observation) => observation.firstObservedAt || observation.observedAt)
|
|
145
|
+
.filter((value) => Number.isFinite(Date.parse(value)))
|
|
146
|
+
.sort((a, b) => Date.parse(a) - Date.parse(b))[0];
|
|
147
|
+
if (aliasPrior) delete entry.sessions[transcript.id];
|
|
148
|
+
entry.sessions[sessionIdentity] = {
|
|
149
|
+
firstObservedAt: firstObservedAt || observedAt,
|
|
108
150
|
observedAt,
|
|
109
|
-
sessionStartedAt:
|
|
151
|
+
sessionStartedAt:
|
|
152
|
+
transcript.startedAt ?? identityPrior?.sessionStartedAt ?? aliasPrior?.sessionStartedAt ?? null,
|
|
110
153
|
memoryHash: record.memoryHash || null,
|
|
111
154
|
source: gapSource(transcript),
|
|
112
155
|
mistake: gap.mistake,
|
|
113
156
|
quote: gap.quote,
|
|
114
157
|
recurrenceRisk: gap.recurrenceRisk,
|
|
158
|
+
domain: gap.domain === "orchestration" ? "orchestration" : "project",
|
|
115
159
|
};
|
|
116
160
|
recorded += 1;
|
|
117
161
|
}
|
|
@@ -133,7 +177,7 @@ export function pruneGapLedger(
|
|
|
133
177
|
|
|
134
178
|
for (const [id, entry] of Object.entries(ledger.entries)) {
|
|
135
179
|
const applies = memoryPath === null || entry.memoryPath === memoryPath;
|
|
136
|
-
if (applies && memoryFile && isCovered(memoryFile, entry
|
|
180
|
+
if (applies && memoryFile && isCovered(memoryFile, entry)) {
|
|
137
181
|
stats.covered += Object.keys(entry.sessions).length;
|
|
138
182
|
delete ledger.entries[id];
|
|
139
183
|
continue;
|
|
@@ -150,8 +194,11 @@ export function pruneGapLedger(
|
|
|
150
194
|
return stats;
|
|
151
195
|
}
|
|
152
196
|
|
|
153
|
-
function isCovered(memoryFile,
|
|
154
|
-
|
|
197
|
+
function isCovered(memoryFile, entry) {
|
|
198
|
+
const phrasings = [...new Set([entry.proposedInstruction, ...(entry.phrasings || [])])];
|
|
199
|
+
return (memoryFile.units || []).some((unit) =>
|
|
200
|
+
phrasings.some((phrasing) => similarity(unit.text, phrasing) >= GAP_COVERED_THRESHOLD),
|
|
201
|
+
);
|
|
155
202
|
}
|
|
156
203
|
|
|
157
204
|
/** Flatten the ledger into the observation list `foldEvidence` clusters over. */
|
|
@@ -167,8 +214,78 @@ export function ledgerGapObservations(ledger, memoryPath) {
|
|
|
167
214
|
mistake: obs.mistake,
|
|
168
215
|
quote: obs.quote,
|
|
169
216
|
recurrenceRisk: obs.recurrenceRisk,
|
|
217
|
+
domain: obs.domain === "orchestration" ? "orchestration" : "project",
|
|
170
218
|
});
|
|
171
219
|
}
|
|
172
220
|
}
|
|
173
221
|
return observations;
|
|
174
222
|
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The ledger's open entries for one memory path, rendered for the analysis prompt so the
|
|
226
|
+
* model can cite an existing gap instead of coining a paraphrase of it. An accumulator,
|
|
227
|
+
* not a detector: a gap nobody has reported yet is simply absent, and the analysis
|
|
228
|
+
* reports it fresh.
|
|
229
|
+
*/
|
|
230
|
+
export function renderOpenGapIndex(ledger, memoryPath, { max = 200 } = {}) {
|
|
231
|
+
const entries = Object.values(ledger.entries).filter((e) => e.memoryPath === memoryPath);
|
|
232
|
+
if (!entries.length) return "(none yet)";
|
|
233
|
+
const lines = entries.slice(0, max).map((e) => `[gap:${e.id}] ${e.proposedInstruction}`);
|
|
234
|
+
if (entries.length > max) lines.push(`... ${entries.length - max} more`);
|
|
235
|
+
return lines.join("\n");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Merge groups of ledger entries the consolidation pass judged to be one gap. Each group
|
|
240
|
+
* keeps the entry with the most sessions (its id stays citable), unions the session maps
|
|
241
|
+
* without ever double-counting a session (an observation already present keeps its
|
|
242
|
+
* earliest firstObservedAt), and keeps the shortest phrasing as canonical - the same rule
|
|
243
|
+
* recording uses. Unknown ids, cross-path groups, and groups that shrink below two known
|
|
244
|
+
* entries are dropped rather than guessed at. Returns how many entries were absorbed.
|
|
245
|
+
*/
|
|
246
|
+
export function mergeGapEntries(ledger, groups) {
|
|
247
|
+
let absorbed = 0;
|
|
248
|
+
const claimed = new Set();
|
|
249
|
+
for (const group of Array.isArray(groups) ? groups : []) {
|
|
250
|
+
const ids = [...new Set((Array.isArray(group) ? group : []).map(String))].filter(
|
|
251
|
+
(id) => ledger.entries[id] && !claimed.has(id),
|
|
252
|
+
);
|
|
253
|
+
if (ids.length < 2) continue;
|
|
254
|
+
const paths = new Set(ids.map((id) => ledger.entries[id].memoryPath));
|
|
255
|
+
if (paths.size !== 1) continue;
|
|
256
|
+
for (const id of ids) claimed.add(id);
|
|
257
|
+
|
|
258
|
+
const entries = ids.map((id) => ledger.entries[id]);
|
|
259
|
+
const target = entries.reduce((best, e) =>
|
|
260
|
+
Object.keys(e.sessions).length > Object.keys(best.sessions).length ? e : best,
|
|
261
|
+
);
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
if (entry === target) continue;
|
|
264
|
+
for (const [sessionId, obs] of Object.entries(entry.sessions)) {
|
|
265
|
+
const prior = target.sessions[sessionId];
|
|
266
|
+
if (!prior) {
|
|
267
|
+
target.sessions[sessionId] = obs;
|
|
268
|
+
} else {
|
|
269
|
+
const earlier =
|
|
270
|
+
Date.parse(obs.firstObservedAt || obs.observedAt) < Date.parse(prior.firstObservedAt || prior.observedAt);
|
|
271
|
+
if (earlier) prior.firstObservedAt = obs.firstObservedAt || obs.observedAt;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
target.aliases = [...new Set([...(target.aliases || []), entry.id, ...(entry.aliases || [])])];
|
|
275
|
+
target.phrasings = [
|
|
276
|
+
...new Set([
|
|
277
|
+
target.proposedInstruction,
|
|
278
|
+
...(target.phrasings || []),
|
|
279
|
+
entry.proposedInstruction,
|
|
280
|
+
...(entry.phrasings || []),
|
|
281
|
+
]),
|
|
282
|
+
];
|
|
283
|
+
if (entry.proposedInstruction.length < target.proposedInstruction.length) {
|
|
284
|
+
target.proposedInstruction = entry.proposedInstruction;
|
|
285
|
+
}
|
|
286
|
+
delete ledger.entries[entry.id];
|
|
287
|
+
absorbed += 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return absorbed;
|
|
291
|
+
}
|
package/src/prompts/analysis.md
CHANGED
|
@@ -10,6 +10,15 @@ Each instruction has a stable id in [brackets]. Refer to instructions ONLY by th
|
|
|
10
10
|
|
|
11
11
|
{{INSTRUCTION_INDEX}}
|
|
12
12
|
|
|
13
|
+
## Gaps already on the books
|
|
14
|
+
|
|
15
|
+
Earlier sessions reported these gaps; each has a stable id. If a gap you found is the
|
|
16
|
+
same underlying gap as one below - one instruction would prevent both - cite that id in
|
|
17
|
+
`matchesGap` and still describe what you saw. A gap that matches nothing here is new:
|
|
18
|
+
omit `matchesGap`.
|
|
19
|
+
|
|
20
|
+
{{OPEN_GAPS}}
|
|
21
|
+
|
|
13
22
|
## The distilled session trace
|
|
14
23
|
|
|
15
24
|
Tool calls are one-line summaries and tool output is truncated. The raw transcript path
|
|
@@ -26,8 +35,8 @@ Return ONE JSON object and nothing else. No prose before or after, no markdown f
|
|
|
26
35
|
```
|
|
27
36
|
{
|
|
28
37
|
"positive": [{"instruction": "AG-042", "moment": "turn 12", "effect": "what following it achieved", "quote": "verbatim text from the trace"}],
|
|
29
|
-
"negative": [{"instruction": "AG-017", "moment": "turn 3", "effect": "what
|
|
30
|
-
"gaps": [{"mistake": "what went wrong", "proposedInstruction": "one sentence that would have prevented it", "recurrenceRisk": "high|medium|low", "quote": "verbatim text from the trace"}],
|
|
38
|
+
"negative": [{"instruction": "AG-017", "moment": "turn 3", "effect": "what happened and what it cost", "class": "harm|non-compliance|irrelevant", "quote": "verbatim text from the trace"}],
|
|
39
|
+
"gaps": [{"mistake": "what went wrong", "proposedInstruction": "one sentence that would have prevented it", "recurrenceRisk": "high|medium|low", "domain": "project|orchestration", "matchesGap": "<id from the list above, omit when new>", "quote": "verbatim text from the trace"}],
|
|
31
40
|
"usedRawTranscript": false
|
|
32
41
|
}
|
|
33
42
|
```
|
|
@@ -38,11 +47,23 @@ Rules, in order of importance:
|
|
|
38
47
|
a real quote are discarded downstream, so an unquotable claim is wasted work.
|
|
39
48
|
2. **Negative evidence is the most valuable.** A visible violation, misreading, or
|
|
40
49
|
ignored instruction outranks a dozen "it went fine" observations.
|
|
41
|
-
3.
|
|
50
|
+
3. **`class` states what a negative means, and the difference decides the instruction's
|
|
51
|
+
fate.** `harm`: the agent FOLLOWED the instruction and following it caused damage or
|
|
52
|
+
cost - evidence against the instruction itself. `non-compliance`: the agent ignored
|
|
53
|
+
or violated the instruction - evidence the instruction failed to steer, which argues
|
|
54
|
+
for reinforcing it, never for deleting it. `irrelevant`: on inspection the moment
|
|
55
|
+
does not actually bear on this instruction. Never report a skipped rule as `harm`.
|
|
56
|
+
4. **`domain` states what caused a gap.** A gap is `orchestration` when the mistake was
|
|
57
|
+
not caused by this repository, but by an external agent harness or tooling that
|
|
58
|
+
orchestrated the task (a task brief, a supervisor's process, the harness itself - by
|
|
59
|
+
way of illustration only, not a list to match against); every other gap is `project`.
|
|
60
|
+
Ask the causal question, not which category the wording resembles. Orchestration gaps
|
|
61
|
+
are counted but never proposed into this repository's memory file.
|
|
62
|
+
5. **Do not confabulate influence.** Only call something positive when the trace shows
|
|
42
63
|
the agent doing the specific thing the instruction asks for. An outcome that would
|
|
43
64
|
have happened anyway is not evidence.
|
|
44
|
-
|
|
45
|
-
and was ignored, that is `negative`, not a gap.
|
|
46
|
-
|
|
65
|
+
6. `gaps` are mistakes NOT covered by any current instruction. If an instruction exists
|
|
66
|
+
and was ignored, that is `negative` with `class: "non-compliance"`, not a gap.
|
|
67
|
+
7. `proposedInstruction` must be one imperative sentence, specific enough to act on and
|
|
47
68
|
general enough to apply beyond this one session.
|
|
48
|
-
|
|
69
|
+
8. An empty array is a valid and useful answer. Report nothing rather than something weak.
|
package/src/prompts/annotate.md
CHANGED
|
@@ -39,14 +39,20 @@ Hard rules - a violation fails the whole proposal:
|
|
|
39
39
|
4. **New instructions need evidence from at least {{MIN_GAP_EVIDENCE}} distinct
|
|
40
40
|
sessions.** `transcripts` is how many distinct sessions back the edit; an edit that
|
|
41
41
|
only adds text is a new instruction whatever its `kind` says.
|
|
42
|
-
5.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
5. **Removing an instruction outright needs harm evidence from at least
|
|
43
|
+
{{MIN_GAP_EVIDENCE}} distinct sessions** (`harm-sessions` in the evidence). Only
|
|
44
|
+
`harm` negatives argue against an instruction; `non-compliance` never justifies a
|
|
45
|
+
deletion. A change that only deletes text and is not part of an extract is a removal
|
|
46
|
+
whatever its `kind` says - if it lacks the evidence, revert it in the file first.
|
|
47
|
+
6. `kind: "extract"` is an edit whose changes are one or more created `SKILL.md` files
|
|
48
|
+
plus the change(s) to {{MEMORY_PATH}} that pay for them. **The skills must carry every
|
|
49
|
+
line those changes remove** - a deletion is never part of an extract; give it its own
|
|
50
|
+
`remove` edit. One skill per extract is the normal shape. Several skills belong in ONE
|
|
51
|
+
extract exactly when their removals landed in a **single** measured change: adjacent
|
|
52
|
+
removals are merged into one change, and a merged change cannot be accepted in halves.
|
|
53
|
+
If each skill has its own measured change, give each its own extract. Any other kind
|
|
54
|
+
must not include a created file, and an edit changes one file only.
|
|
55
|
+
7. **Budget:** {{BUDGET_RULE}}
|
|
50
56
|
|
|
51
57
|
If you still need to change the files, do that first and then answer; backpass
|
|
52
58
|
re-measures after this reply and shows you the new ids if anything moved. Re-measuring
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
You are consolidating the gap ledger of a backward pass over a repository's agent
|
|
2
|
+
memory file. Each entry below is a mistake some past agent session hit that no current
|
|
3
|
+
instruction covers, phrased as the instruction that would have prevented it. Different
|
|
4
|
+
sessions phrase the same underlying gap differently, and a gap only graduates into a
|
|
5
|
+
proposal once enough DISTINCT sessions have hit it - so paraphrases of one gap must be
|
|
6
|
+
recognized as one entry, or real recurrence stays invisible.
|
|
7
|
+
|
|
8
|
+
## Open gap entries
|
|
9
|
+
|
|
10
|
+
{{GAP_ENTRIES}}
|
|
11
|
+
|
|
12
|
+
## What to return
|
|
13
|
+
|
|
14
|
+
Return ONE JSON object and nothing else. No prose, no markdown fence.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
{"merges": [["<id>", "<id>", ...], ...]}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Each inner array lists two or more entry ids that are the SAME underlying gap. Rules:
|
|
21
|
+
|
|
22
|
+
1. Merge only when one instruction would prevent every entry in the group - the same
|
|
23
|
+
mistake, not merely the same topic. Two gaps about the same subsystem that call for
|
|
24
|
+
different instructions stay separate.
|
|
25
|
+
2. When unsure, do not merge. A wrong merge fabricates corroboration and can write a
|
|
26
|
+
weakly-supported instruction into the memory file; a missed merge only waits for
|
|
27
|
+
another sighting.
|
|
28
|
+
3. An id may appear in at most one group. Ids not listed stay untouched.
|
|
29
|
+
4. `{"merges": []}` is a valid and common answer.
|
package/src/prompts/synthesis.md
CHANGED
|
@@ -69,12 +69,21 @@ analyzed sessions in which an instruction drew any evidence at all.
|
|
|
69
69
|
speculative one.
|
|
70
70
|
2. **New instructions need evidence from at least {{MIN_GAP_EVIDENCE}} distinct
|
|
71
71
|
sessions.** One bad session never rewrites the weights.
|
|
72
|
-
3. **
|
|
72
|
+
3. **Removing an instruction outright needs harm evidence from at least
|
|
73
|
+
{{MIN_GAP_EVIDENCE}} distinct sessions** (`harm-sessions` in the evidence rows).
|
|
74
|
+
Only `harm` negatives - following the instruction caused damage - argue against an
|
|
75
|
+
instruction. `non-compliance` means it failed to steer: reinforce it, reposition it,
|
|
76
|
+
or improve its trigger, never delete it for being ignored. Text you delete that does
|
|
77
|
+
not land in a created skill is a removal, whatever the edit is called.
|
|
78
|
+
4. **An extraction preserves every line it removes** in the SKILL.md it creates. A
|
|
79
|
+
deletion is never part of an extract: it is its own `remove` edit, decided on its
|
|
80
|
+
own evidence.
|
|
81
|
+
5. **Every edit must be backed by at least one verbatim quote** from the evidence. You
|
|
73
82
|
will attach the quotes in the next step, so only make changes you can back.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
6. **Budget:** {{BUDGET_RULE}}
|
|
84
|
+
7. You can extract a long, narrow, crisply-triggered section instead of deleting it:
|
|
85
|
+
extraction frees the same always-loaded tokens and loses nothing.
|
|
86
|
+
8. Change only `./{{MEMORY_PATH}}` and files under `./{{SKILLS_DIR}}/`. Never delete a
|
|
78
87
|
file. Do not create notes, scripts, or scratch files.
|
|
79
88
|
|
|
80
89
|
## Where an instruction belongs
|
|
@@ -85,7 +94,7 @@ analyzed sessions in which an instruction drew any evidence at all.
|
|
|
85
94
|
| Conditional / narrow | **skill** (the description is the condition) | deletion candidate |
|
|
86
95
|
|
|
87
96
|
A skill's description is always loaded and its body is free until triggered, so moving a
|
|
88
|
-
|
|
97
|
+
section into a skill trades its always-loaded cost for that one description line.
|
|
89
98
|
|
|
90
99
|
**Skill descriptions are weights too.** If the evidence shows an agent lacked knowledge
|
|
91
100
|
an existing skill already contains, that is a failed trigger: rewrite that skill's
|
package/src/proposal.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { renderHunkLines } from "./diff.js";
|
|
2
2
|
import { editSkills } from "./skills.js";
|
|
3
3
|
import { budgetGateKind, budgetStatus, estimateTokens } from "./tokens.js";
|
|
4
|
+
import { normalizeRecoveryLine, recoveredLineCounts } from "./workspace.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The proposal model: what a synthesis pass is allowed to produce, and the mechanical
|
|
@@ -103,6 +104,28 @@ function countSources(evidence) {
|
|
|
103
104
|
return new Set(evidence.map((e) => e?.source).filter(Boolean)).size;
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
/** The del-line texts of a hunk that are not carried by `lineCounts` (blank lines ignored). */
|
|
108
|
+
function unrecoveredRemovedLines(hunk, lineCounts) {
|
|
109
|
+
const missing = [];
|
|
110
|
+
for (const line of hunk.lines || []) {
|
|
111
|
+
if (line.type !== "del") continue;
|
|
112
|
+
const normalized = normalizeRecoveryLine(line.text);
|
|
113
|
+
if (!normalized) continue;
|
|
114
|
+
const remaining = lineCounts.get(normalized) || 0;
|
|
115
|
+
if (remaining > 0) lineCounts.set(normalized, remaining - 1);
|
|
116
|
+
else missing.push(line.text);
|
|
117
|
+
}
|
|
118
|
+
return missing;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The memory units a pure-removal hunk deletes. For a pure removal every file line in
|
|
123
|
+
* [oldStart, oldEnd] is removed, so this is a plain range intersection with unit lines.
|
|
124
|
+
*/
|
|
125
|
+
function unitsRemovedBy(hunk, memoryFile) {
|
|
126
|
+
return memoryFile.units.filter((unit) => unit.startLine <= hunk.oldEnd && unit.endLine >= hunk.oldStart);
|
|
127
|
+
}
|
|
128
|
+
|
|
106
129
|
/** Overlapping count: a run of identical lines must not pass as unique. */
|
|
107
130
|
function occurrences(haystack, needle) {
|
|
108
131
|
if (!needle) return 0;
|
|
@@ -308,6 +331,19 @@ export function buildProposal(rawResult, context) {
|
|
|
308
331
|
violations.push(`edit ${edit.id}: ${unusable.file} needs YAML frontmatter with \`name:\` and \`description:\``);
|
|
309
332
|
continue;
|
|
310
333
|
}
|
|
334
|
+
// An extraction moves text; it never doubles as a deletion. Every line its hunks
|
|
335
|
+
// remove must land in the skills it creates - a real deletion goes in its own
|
|
336
|
+
// remove edit, where the removal-evidence floor below can judge it on its own.
|
|
337
|
+
const carried = recoveredLineCounts(created.map((c) => c.text));
|
|
338
|
+
const missing = hunks.flatMap((h) => unrecoveredRemovedLines(h, carried));
|
|
339
|
+
if (missing.length) {
|
|
340
|
+
violations.push(
|
|
341
|
+
`edit ${edit.id} ("${edit.title}") removes text its created skill(s) do not carry ` +
|
|
342
|
+
`(first: "${missing[0].trim().slice(0, 80)}"); an extraction preserves every line it removes - ` +
|
|
343
|
+
`revert that text, or make its deletion a separate "remove" edit`,
|
|
344
|
+
);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
311
347
|
} else if (created.length) {
|
|
312
348
|
violations.push(`edit ${edit.id}: only kind "extract" may include a created file (${created[0].id})`);
|
|
313
349
|
continue;
|
|
@@ -323,6 +359,34 @@ export function buildProposal(rawResult, context) {
|
|
|
323
359
|
continue;
|
|
324
360
|
}
|
|
325
361
|
|
|
362
|
+
// A removal is measured the same way: a hunk that only deletes text, outside an
|
|
363
|
+
// extraction, deletes instructions - whatever the edit's kind says. Deleting an
|
|
364
|
+
// instruction needs the same corroboration adding one does, and only negatives the
|
|
365
|
+
// analysis classified as `harm` (following the rule caused damage) count toward it.
|
|
366
|
+
// Non-compliance is the rule failing to steer; it never justifies deletion. This is
|
|
367
|
+
// the removal-evidence floor; the >= 20%-relevance placement table stays guidance.
|
|
368
|
+
if (edit.kind !== "extract" && files[0] === memoryFile.path) {
|
|
369
|
+
const rows = new Map((summary?.instructions ?? []).map((row) => [row.instruction, row]));
|
|
370
|
+
const unsupported = [];
|
|
371
|
+
for (const hunk of hunks) {
|
|
372
|
+
if (!hunk.removed || hunk.added) continue;
|
|
373
|
+
for (const unit of unitsRemovedBy(hunk, memoryFile)) {
|
|
374
|
+
const harm = rows.get(unit.id)?.harmSessions ?? 0;
|
|
375
|
+
if (harm < config.minGapEvidence) unsupported.push({ unit, harm });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (unsupported.length) {
|
|
379
|
+
const worst = unsupported[0];
|
|
380
|
+
violations.push(
|
|
381
|
+
`edit ${edit.id} ("${edit.title}") deletes [${worst.unit.id}] "${worst.unit.text.slice(0, 80)}" backed by ` +
|
|
382
|
+
`${worst.harm} session(s) of harm-class negative evidence; removing an instruction needs ` +
|
|
383
|
+
`${config.minGapEvidence}, and non-compliance never counts - revert the deletion` +
|
|
384
|
+
(unsupported.length > 1 ? ` (${unsupported.length} unit(s) affected)` : ""),
|
|
385
|
+
);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
326
390
|
const file = files[0];
|
|
327
391
|
const proposed = {
|
|
328
392
|
id: edit.id,
|