backpass 0.1.12 → 0.1.14

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 CHANGED
@@ -158,8 +158,9 @@ strict JSON: which instructions helped, which were violated, and what mistakes n
158
158
  instruction covers. Every negative carries a class - `harm` (following the instruction
159
159
  caused damage), `non-compliance` (the agent ignored it), or `irrelevant` - because those
160
160
  argue for opposite fates: harm argues against an instruction, non-compliance argues for
161
- reinforcing it. Every gap carries a domain - `project` for this repository's own
162
- engineering, `orchestration` for the task-management layer around the session - and the
161
+ reinforcing it. Every gap carries a domain - `orchestration` when the mistake was caused
162
+ not by this repository but by an external agent harness or tooling that orchestrated the
163
+ task, `project` for every other mistake - and the
163
164
  analysis is shown the ledger's open gaps so it can cite an existing gap id instead of
164
165
  coining a paraphrase of it.
165
166
 
@@ -193,8 +194,9 @@ consolidation call sees the full open gap set and merges entries that describe t
193
194
  mistake. That second judgment is what lets two sightings of a brand-new gap in the same
194
195
  run's parallel fan-out corroborate. A failed consolidation call degrades the run to
195
196
  lexical identity and says so; it never aborts. Orchestration-domain gaps are counted and
196
- reported but never cluster: mistakes about the task harness around a session do not
197
- become instructions in the project's memory file.
197
+ reported but never cluster: a mistake caused not by this repository but by the external
198
+ agent harness or tooling that orchestrated a session does not become an instruction in the
199
+ project's memory file.
198
200
 
199
201
  Only evidence judged against the _current_ memory-file set hash is folded into a proposal. A
200
202
  transcript that fell out of this run's sample - the time window, `maxTranscripts`, or the
@@ -311,7 +313,10 @@ not duplicate content.
311
313
 
312
314
  `backpass apply` is the only command that writes. It serves a review surface through
313
315
  [`lavish-axi`](https://github.com/kunchenguid/lavish-axi): one card per edit with the diff,
314
- the evidence quotes and their sources, a live budget gauge, and ACCEPT / REJECT.
316
+ the evidence quotes and their sources, a live budget gauge, and ACCEPT / REJECT. A compact
317
+ gap funnel shows how accumulated sightings narrowed through domain filtering, clustering,
318
+ and the corroboration floor to the gaps eligible for a proposal; older proposals without
319
+ recorded funnel counts omit it.
315
320
 
316
321
  The surface is a static template shipped in the package - the CLI injects one JSON payload,
317
322
  so it is instant, deterministic, and identical every run. Nothing there is model-generated.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backpass",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "description": "Gradient descent for your agent memory - analyzes past agent session transcripts and proposes evidence-backed edits to AGENTS.md / CLAUDE.md",
6
6
  "type": "module",
package/src/fold.js CHANGED
@@ -91,9 +91,9 @@ export function foldEvidence(evidenceRecords, { minGapEvidence = 2, memoryFile =
91
91
  }
92
92
  }
93
93
 
94
- // Orchestration-domain sightings are about the task-management layer around the
95
- // session, not this repository's engineering; they are counted for legibility but
96
- // never cluster, so they can never corroborate into a project proposal.
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
97
  const allObservations = gapObservations ?? recordObservations;
98
98
  const projectObservations = allObservations.filter((obs) => obs?.domain !== "orchestration");
99
99
  const orchestrationGapSightings = allObservations.length - projectObservations.length;
@@ -143,6 +143,10 @@ export function foldEvidence(evidenceRecords, { minGapEvidence = 2, memoryFile =
143
143
  totals: {
144
144
  positive: positiveCount,
145
145
  negative: negativeCount,
146
+ // The gap funnel's top: every sighting this fold clustered over (the pruned ledger
147
+ // when one is passed, this run's records otherwise). Orchestration sightings are the
148
+ // slice excluded before clustering; project-domain is the difference.
149
+ gapSightings: allObservations.length,
146
150
  gapClusters: gaps.length,
147
151
  droppedGapSingletons,
148
152
  orchestrationGapSightings,
@@ -230,8 +234,8 @@ export function renderEvidenceForPrompt(summary) {
230
234
  lines.push("### Gap clusters (mistakes no current instruction covers)");
231
235
  if (summary.totals.orchestrationGapSightings) {
232
236
  lines.push(
233
- `- ${summary.totals.orchestrationGapSightings} orchestration-domain sighting(s) about the ` +
234
- `task-management layer were excluded; they never enter this repository's memory file`,
237
+ `- ${summary.totals.orchestrationGapSightings} orchestration-domain sighting(s) caused by the ` +
238
+ `orchestrating harness or tooling were excluded; they never enter this repository's memory file`,
235
239
  );
236
240
  }
237
241
  if (!summary.gaps.length) {
package/src/gap-ledger.js CHANGED
@@ -26,9 +26,9 @@ import { sha256 } from "./state.js";
26
26
  * merged by the pre-synthesis consolidation pass (`mergeGapEntries`, driven by
27
27
  * `src/consolidate.js`), which is what lets two same-run parallel sightings - neither
28
28
  * of which could cite the other - still corroborate.
29
- * - Every observation carries the `domain` the analysis judged: `project` for mistakes
30
- * about this repository's own engineering, `orchestration` for mistakes about the
31
- * task-management layer around it (briefs, scout scope, status records, approvals).
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
32
  * Orchestration sightings are recorded for legibility but never counted toward
33
33
  * corroboration and never surface in a proposal; a missing domain counts as project,
34
34
  * so evidence from before the field existed keeps its old behavior.
@@ -53,12 +53,12 @@ Rules, in order of importance:
53
53
  or violated the instruction - evidence the instruction failed to steer, which argues
54
54
  for reinforcing it, never for deleting it. `irrelevant`: on inspection the moment
55
55
  does not actually bear on this instruction. Never report a skipped rule as `harm`.
56
- 4. **`domain` states whose mistake a gap is.** `project`: about this repository's own
57
- engineering - its code, tests, build, docs, releases, conventions. `orchestration`:
58
- about the task-management layer around the session - task briefs and their scope
59
- (scout/read-only rules), status reporting to a supervisor, approval and authorization
60
- flows, delivery-lifecycle process imposed from outside the repository. Orchestration
61
- gaps are counted but never proposed into this repository's memory file.
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
62
  5. **Do not confabulate influence.** Only call something positive when the trace shows
63
63
  the agent doing the specific thing the instruction asks for. An outcome that would
64
64
  have happened anyway is not evidence.
@@ -81,7 +81,7 @@ analyzed sessions in which an instruction drew any evidence at all.
81
81
  5. **Every edit must be backed by at least one verbatim quote** from the evidence. You
82
82
  will attach the quotes in the next step, so only make changes you can back.
83
83
  6. **Budget:** {{BUDGET_RULE}}
84
- 7. Prefer extracting a long, narrow, crisply-triggered section over deleting anything:
84
+ 7. You can extract a long, narrow, crisply-triggered section instead of deleting it:
85
85
  extraction frees the same always-loaded tokens and loses nothing.
86
86
  8. Change only `./{{MEMORY_PATH}}` and files under `./{{SKILLS_DIR}}/`. Never delete a
87
87
  file. Do not create notes, scripts, or scratch files.
@@ -94,7 +94,7 @@ analyzed sessions in which an instruction drew any evidence at all.
94
94
  | Conditional / narrow | **skill** (the description is the condition) | deletion candidate |
95
95
 
96
96
  A skill's description is always loaded and its body is free until triggered, so moving a
97
- long, narrow, crisply-triggered section into a skill is nearly pure budget profit.
97
+ section into a skill trades its always-loaded cost for that one description line.
98
98
 
99
99
  **Skill descriptions are weights too.** If the evidence shows an agent lacked knowledge
100
100
  an existing skill already contains, that is a failed trigger: rewrite that skill's
package/src/proposal.js CHANGED
@@ -489,6 +489,13 @@ export function buildProposal(rawResult, context) {
489
489
  positive: summary?.totals?.positive ?? 0,
490
490
  negative: summary?.totals?.negative ?? 0,
491
491
  gapClusters: summary?.totals?.gapClusters ?? 0,
492
+ // The gap funnel, for the apply surface: sightings -> project-domain (sightings
493
+ // minus orchestration) -> distinct gaps (clusters + dropped) -> eligible
494
+ // (clusters). `null`, never 0, when the summary predates a count - the surface
495
+ // hides what was not recorded rather than showing an invented zero.
496
+ gapSightings: summary?.totals?.gapSightings ?? null,
497
+ orchestrationGapSightings: summary?.totals?.orchestrationGapSightings ?? null,
498
+ droppedGapSingletons: summary?.totals?.droppedGapSingletons ?? null,
492
499
  skillExtractions: accepted.reduce((n, e) => n + editSkills(e).length, 0),
493
500
  },
494
501
  edits: accepted,
@@ -126,6 +126,124 @@
126
126
  margin-top: 3px;
127
127
  }
128
128
 
129
+ /* ---------- gap funnel ---------- */
130
+ /* The template's own display rules (.statrow's grid) would beat the UA
131
+ stylesheet's [hidden]; this keeps the hidden attribute authoritative. */
132
+ [hidden] {
133
+ display: none !important;
134
+ }
135
+ .run-context {
136
+ border: 1px solid var(--line);
137
+ background: var(--line);
138
+ display: grid;
139
+ grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr);
140
+ gap: 1px;
141
+ margin-bottom: 36px;
142
+ }
143
+ .ctx-funnel {
144
+ background: var(--panel);
145
+ padding: 13px 18px 14px;
146
+ min-width: 0;
147
+ }
148
+ .ctx-stats {
149
+ display: grid;
150
+ grid-template-columns: 1fr 1fr;
151
+ grid-auto-rows: 1fr;
152
+ gap: 1px;
153
+ min-width: 0;
154
+ }
155
+ .funnel-head {
156
+ display: flex;
157
+ justify-content: space-between;
158
+ align-items: baseline;
159
+ flex-wrap: wrap;
160
+ gap: 8px;
161
+ margin-bottom: 10px;
162
+ }
163
+ .funnel-head .t {
164
+ font-size: 13px;
165
+ color: var(--dim);
166
+ }
167
+ .funnel-head .facts {
168
+ font-family: var(--mono);
169
+ font-size: 11px;
170
+ color: var(--faint);
171
+ }
172
+ .funnel-head .facts b {
173
+ color: var(--text);
174
+ font-weight: 500;
175
+ }
176
+ .frow {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: 12px;
180
+ margin: 4px 0;
181
+ }
182
+ .frow .fl {
183
+ width: 148px;
184
+ flex: none;
185
+ font-family: var(--mono);
186
+ font-size: 10px;
187
+ letter-spacing: 0.11em;
188
+ text-transform: uppercase;
189
+ color: var(--faint);
190
+ white-space: nowrap;
191
+ overflow: hidden;
192
+ text-overflow: ellipsis;
193
+ }
194
+ .frow .fb {
195
+ flex: 1;
196
+ min-width: 0;
197
+ display: flex;
198
+ align-items: center;
199
+ height: 16px;
200
+ background: var(--panel-2);
201
+ border: 1px solid var(--line-soft);
202
+ }
203
+ .frow .fb i {
204
+ display: block;
205
+ height: 100%;
206
+ background: rgba(110, 168, 255, 0.38);
207
+ border-right: 1px solid rgba(110, 168, 255, 0.75);
208
+ }
209
+ .frow.lit .fb i {
210
+ background: rgba(79, 227, 193, 0.4);
211
+ border-right-color: var(--accent);
212
+ }
213
+ .frow.zero .fb i {
214
+ border-right: none;
215
+ }
216
+ .frow .fv {
217
+ flex: none;
218
+ width: 34px;
219
+ font-family: var(--mono);
220
+ font-size: 12.5px;
221
+ color: var(--text);
222
+ }
223
+ .frow.zero .fv {
224
+ color: var(--faint);
225
+ }
226
+ .frow.lit .fv {
227
+ color: var(--accent);
228
+ }
229
+ .fdrop {
230
+ margin: 1px 0 6px 160px;
231
+ font-size: 12px;
232
+ color: var(--dim);
233
+ }
234
+ .fdrop b {
235
+ font-family: var(--mono);
236
+ font-weight: 500;
237
+ color: var(--faint);
238
+ }
239
+ .funnel-note {
240
+ margin-top: 10px;
241
+ padding-top: 9px;
242
+ border-top: 1px dashed var(--line-soft);
243
+ font-size: 12.5px;
244
+ color: var(--dim);
245
+ }
246
+
129
247
  /* ---------- budget gauge ---------- */
130
248
  .gauge-block {
131
249
  border: 1px solid var(--line);
@@ -564,7 +682,7 @@
564
682
  cursor: not-allowed;
565
683
  }
566
684
 
567
- @media (max-width: 640px) {
685
+ @media (max-width: 760px) {
568
686
  .wrap {
569
687
  padding: 28px 16px 170px;
570
688
  }
@@ -572,6 +690,15 @@
572
690
  padding: 12px 16px;
573
691
  gap: 12px;
574
692
  }
693
+ .run-context {
694
+ grid-template-columns: minmax(0, 1fr);
695
+ }
696
+ .frow .fl {
697
+ width: 92px;
698
+ }
699
+ .fdrop {
700
+ margin-left: 0;
701
+ }
575
702
  }
576
703
  </style>
577
704
  </head>
@@ -584,7 +711,19 @@
584
711
  </div>
585
712
  <div class="runline" id="runline"></div>
586
713
 
587
- <div class="statrow" id="statrow"></div>
714
+ <div class="statrow" id="statrow" hidden></div>
715
+
716
+ <div class="run-context" id="ctx" hidden>
717
+ <div class="ctx-funnel">
718
+ <div class="funnel-head">
719
+ <span class="t">Gap funnel · counted across runs</span>
720
+ <span class="facts num" id="funnel-floor"></span>
721
+ </div>
722
+ <div id="funnel-bars"></div>
723
+ <div class="funnel-note" id="funnel-note" hidden></div>
724
+ </div>
725
+ <div class="ctx-stats" id="ctx-stats"></div>
726
+ </div>
588
727
 
589
728
  <div class="gauge-block">
590
729
  <div class="gauge-head">
@@ -668,26 +807,142 @@
668
807
  document.createTextNode(" transcripts" + (harnessBits.length ? " (" + harnessBits.join(" · ") + ")" : "")),
669
808
  );
670
809
 
671
- // ---- stats ----
672
- var stats = [
810
+ // ---- run context: one frame, the gap funnel beside the run stats ----
811
+ // A single bordered band. The left pane tells the gap story as proportional
812
+ // bars - sightings recorded by analysis -> project-domain (orchestration
813
+ // excluded) -> distinct gaps after clustering -> eligible to propose - with
814
+ // each drop-off explained in plain words. The right pane holds the run facts
815
+ // (edits, evidence, skills) as stat cells; a separate stat row would duplicate
816
+ // the funnel's last stage. Every count is recorded by the fold (`totals` in
817
+ // evidence-summary.json); a proposal saved before the counts existed falls
818
+ // back to the classic stat row rather than showing zeros nobody measured.
819
+ function funnelCounts() {
820
+ if (typeof (P.stats || {}).gapSightings !== "number") return null;
821
+ var eligible = P.stats.gapClusters || 0;
822
+ var dropped = P.stats.droppedGapSingletons || 0;
823
+ var orch = P.stats.orchestrationGapSightings || 0;
824
+ var project = Math.max(0, P.stats.gapSightings - orch);
825
+ return {
826
+ floor: Number((P.config || {}).minGapEvidence) || 2,
827
+ sighted: P.stats.gapSightings,
828
+ orch: orch,
829
+ project: project,
830
+ distinct: eligible + dropped,
831
+ merged: Math.max(0, project - (eligible + dropped)),
832
+ dropped: dropped,
833
+ eligible: eligible,
834
+ };
835
+ }
836
+ function funnelBar(host, value, label, max, lit) {
837
+ var row = el("div", "frow" + (value === 0 ? " zero" : lit ? " lit" : ""));
838
+ row.appendChild(el("span", "fl", label));
839
+ var track = el("div", "fb");
840
+ var fill = el("i");
841
+ fill.style.width = max > 0 ? Math.max((value / max) * 100, value > 0 ? 2 : 0) + "%" : "0%";
842
+ track.appendChild(fill);
843
+ row.appendChild(track);
844
+ row.appendChild(el("span", "fv num", fmt(value)));
845
+ host.appendChild(row);
846
+ }
847
+ function funnelDrop(host, n, text) {
848
+ if (!n) return;
849
+ var line = el("div", "fdrop");
850
+ line.appendChild(el("b", null, "− " + fmt(n) + " "));
851
+ line.appendChild(document.createTextNode(text));
852
+ host.appendChild(line);
853
+ }
854
+ var funnelNote = null;
855
+ var funnel = funnelCounts();
856
+ if (funnel) {
857
+ var bars = document.getElementById("funnel-bars");
858
+ funnelBar(bars, funnel.sighted, "sightings", funnel.sighted);
859
+ funnelDrop(
860
+ bars,
861
+ funnel.orch,
862
+ "orchestration: mistakes in how the task was run, not in this project - excluded from proposals by design",
863
+ );
864
+ funnelBar(bars, funnel.project, "project-domain", funnel.sighted);
865
+ funnelDrop(
866
+ bars,
867
+ funnel.merged,
868
+ "merged: repeat sightings of a gap already counted - they corroborate it rather than add a new one",
869
+ );
870
+ funnelBar(bars, funnel.distinct, "distinct gaps", funnel.sighted);
871
+ funnelDrop(
872
+ bars,
873
+ funnel.dropped,
874
+ "below the corroboration floor: seen in fewer than " +
875
+ funnel.floor +
876
+ " sessions so far - more transcripts can corroborate them",
877
+ );
878
+ funnelBar(bars, funnel.eligible, "eligible to propose", funnel.sighted, true);
879
+
880
+ document.getElementById("funnel-floor").textContent = "corroboration floor · " + funnel.floor + " sessions";
881
+ var statHost = document.getElementById("ctx-stats");
673
882
  [
674
- String(edits.length) + "/" + String((P.config || {}).maxEditsPerRun || edits.length),
675
- "edits proposed / cap",
676
- null,
677
- ],
678
- [fmt(P.stats.positive), "positive evidence", "accent"],
679
- [fmt(P.stats.negative), "negative evidence", "reject"],
680
- [fmt(P.stats.gapClusters), "gap clusters", "defer"],
681
- [fmt(P.stats.skillExtractions), "skill extractions", null],
682
- ];
683
- var statrow = document.getElementById("statrow");
684
- stats.forEach(function (s) {
685
- var box = el("div", "stat");
686
- var v = el("div", "v num", s[0]);
687
- box.appendChild(v);
688
- box.appendChild(el("div", "k", s[1]));
689
- statrow.appendChild(box);
690
- });
883
+ [
884
+ String(edits.length) + "/" + String((P.config || {}).maxEditsPerRun || edits.length),
885
+ "edits proposed / cap",
886
+ ],
887
+ [fmt(P.stats.positive), "positive evidence"],
888
+ [fmt(P.stats.negative), "negative evidence"],
889
+ [fmt(P.stats.skillExtractions), "skill extractions"],
890
+ ].forEach(function (s) {
891
+ var box = el("div", "stat");
892
+ box.appendChild(el("div", "v num", s[0]));
893
+ box.appendChild(el("div", "k", s[1]));
894
+ statHost.appendChild(box);
895
+ });
896
+
897
+ // The drop lines carry the takeaway; the visible note only speaks when there
898
+ // are no bars to read. funnelNote itself still feeds the zero-edit empty state.
899
+ if (!funnel.sighted) {
900
+ funnelNote = "Analysis reported no uncovered gaps.";
901
+ } else if (!funnel.eligible && funnel.dropped) {
902
+ funnelNote =
903
+ funnel.dropped +
904
+ " distinct gap" +
905
+ (funnel.dropped === 1 ? " is" : "s are") +
906
+ " still below the " +
907
+ funnel.floor +
908
+ "-session corroboration floor - more transcripts may corroborate " +
909
+ (funnel.dropped === 1 ? "it." : "them.");
910
+ } else if (!funnel.eligible && funnel.orch >= funnel.sighted) {
911
+ funnelNote =
912
+ "All " +
913
+ funnel.sighted +
914
+ " sighting" +
915
+ (funnel.sighted === 1 ? " was" : "s were") +
916
+ " orchestration-domain (caused by the orchestrating harness, not this repository) and " +
917
+ (funnel.sighted === 1 ? "is" : "are") +
918
+ " excluded by design.";
919
+ }
920
+ if (funnelNote && !funnel.sighted) {
921
+ var noteEl = document.getElementById("funnel-note");
922
+ noteEl.textContent = funnelNote;
923
+ noteEl.hidden = false;
924
+ }
925
+ document.getElementById("ctx").hidden = false;
926
+ } else {
927
+ // A proposal from before the funnel counts existed: the classic stat row.
928
+ var statrow = document.getElementById("statrow");
929
+ statrow.hidden = false;
930
+ [
931
+ [
932
+ String(edits.length) + "/" + String((P.config || {}).maxEditsPerRun || edits.length),
933
+ "edits proposed / cap",
934
+ ],
935
+ [fmt(P.stats.positive), "positive evidence"],
936
+ [fmt(P.stats.negative), "negative evidence"],
937
+ [fmt(P.stats.gapClusters), "eligible gaps"],
938
+ [fmt(P.stats.skillExtractions), "skill extractions"],
939
+ ].forEach(function (s) {
940
+ var box = el("div", "stat");
941
+ box.appendChild(el("div", "v num", s[0]));
942
+ box.appendChild(el("div", "k", s[1]));
943
+ statrow.appendChild(box);
944
+ });
945
+ }
691
946
 
692
947
  // ---- budget gauge ----
693
948
  document.getElementById("gauge-title").textContent = "Always-loaded budget · " + (P.memoryFile.path || "");
@@ -700,8 +955,21 @@
700
955
  edits.length + " of " + ((P.config || {}).maxEditsPerRun || edits.length) + " (learning-rate cap)";
701
956
 
702
957
  if (!edits.length) {
958
+ var eligibleWithoutEdits = Number((P.stats || {}).gapClusters) || 0;
959
+ var emptyReason = funnelNote;
960
+ if (!emptyReason && eligibleWithoutEdits > 0) {
961
+ emptyReason =
962
+ fmt(eligibleWithoutEdits) +
963
+ " eligible gap" +
964
+ (eligibleWithoutEdits === 1 ? "" : "s") +
965
+ " reached synthesis, but no edit was proposed this run.";
966
+ }
703
967
  host.appendChild(
704
- el("div", "empty", "No edits proposed. The evidence did not clear the thresholds this run."),
968
+ el(
969
+ "div",
970
+ "empty",
971
+ "No edits proposed. " + (emptyReason || "The evidence did not clear the thresholds this run."),
972
+ ),
705
973
  );
706
974
  }
707
975