@event4u/agent-config 8.7.0 → 8.8.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.
Files changed (83) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +52 -0
  3. package/CONTRIBUTING.md +25 -0
  4. package/README.md +7 -4
  5. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +1 -1
  6. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +28 -16
  7. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +15 -0
  8. package/dist/agent-src/rules/active-remediation.md +92 -0
  9. package/dist/agent-src/rules/broken-access-control.md +106 -0
  10. package/dist/agent-src/rules/communication-through-line.md +42 -0
  11. package/dist/agent-src/rules/decision-revisit-gate.md +8 -0
  12. package/dist/agent-src/rules/minimal-safe-diff.md +18 -1
  13. package/dist/agent-src/rules/prefer-enums-over-literals.md +65 -0
  14. package/dist/agent-src/rules/senior-engineering-discipline.md +82 -0
  15. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +105 -0
  16. package/dist/agent-src/skills/ai-code-blindspots/evals/triggers.json +52 -0
  17. package/dist/agent-src/skills/authz-review/SKILL.md +21 -4
  18. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +20 -0
  19. package/dist/agent-src/skills/frontend-render-security/SKILL.md +104 -0
  20. package/dist/agent-src/skills/frontend-render-security/evals/triggers.json +52 -0
  21. package/dist/agent-src/skills/roadmap-writing/SKILL.md +7 -0
  22. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +100 -0
  23. package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +52 -0
  24. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +43 -2
  25. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  26. package/dist/agent-src/templates/roadmaps.md +24 -0
  27. package/dist/discovery/deprecation-report.md +1 -1
  28. package/dist/discovery/discovery-manifest.json +195 -15
  29. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  30. package/dist/discovery/discovery-manifest.summary.md +3 -3
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +13 -5
  33. package/dist/discovery/trust-report.md +11 -11
  34. package/dist/discovery/workspaces.json +12 -4
  35. package/dist/install/install.mjs +5 -2
  36. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  37. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  38. package/dist/mcp/registry-manifest.json +4 -4
  39. package/dist/router.json +1 -1
  40. package/dist/server/schemas/settings.js +2 -1
  41. package/dist/server/schemas/settings.js.map +1 -1
  42. package/docs/CLAIMS.md +30 -2
  43. package/docs/architecture.md +2 -2
  44. package/docs/catalog.md +14 -0
  45. package/docs/contracts/mcp-tool-inventory.md +31 -30
  46. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +6 -0
  47. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +2 -2
  48. package/docs/decisions/ADR-117-subagents-auto-default-on.md +88 -0
  49. package/docs/decisions/INDEX.md +1 -0
  50. package/docs/experiences/content_creator.md +2 -0
  51. package/docs/experiences/finance.md +2 -0
  52. package/docs/experiences/founder.md +2 -0
  53. package/docs/experiences/ops.md +2 -0
  54. package/docs/featured-skills.md +2 -2
  55. package/docs/getting-started-by-role.md +1 -1
  56. package/docs/getting-started.md +1 -1
  57. package/docs/governance-advantage.md +1 -1
  58. package/docs/guidelines/agent-infra/memory-access.md +12 -0
  59. package/docs/proof.md +45 -3
  60. package/docs/release-runbook.md +138 -0
  61. package/docs/second-brain-scope.md +73 -0
  62. package/docs/succession.md +80 -0
  63. package/package.json +2 -2
  64. package/src/config/agent-settings.template.yml +25 -6
  65. package/src/scripts/_lib/orchestration_gate.ts +12 -8
  66. package/src/scripts/_lib/orchestration_savings.ts +127 -0
  67. package/src/scripts/build_proof.ts +38 -0
  68. package/src/scripts/chat_history.ts +79 -7
  69. package/src/scripts/domain_soundness_status.ts +236 -0
  70. package/src/scripts/generate_index.ts +14 -0
  71. package/src/scripts/hot_context_hook.ts +47 -2
  72. package/src/scripts/lint_behavioural_eval_freshness.ts +140 -0
  73. package/src/scripts/mcp_server/consumer_tool_catalog.json +22 -3
  74. package/src/scripts/mcp_server/tools.ts +159 -1
  75. package/src/scripts/memory_lookup.ts +184 -6
  76. package/src/scripts/memory_replay.ts +285 -0
  77. package/src/scripts/orchestration_savings_report.ts +100 -0
  78. package/src/scripts/schemas/domain-truth.schema.json +94 -0
  79. package/src/scripts/schemas/evals.schema.json +113 -0
  80. package/src/scripts/second_brain_score.ts +186 -0
  81. package/src/scripts/session_memory_index.ts +94 -0
  82. package/src/scripts/skill_eval_coverage.ts +318 -0
  83. package/src/server/schemas/settings.ts +5 -2
@@ -0,0 +1,80 @@
1
+ # Succession — the minimal knowledge to take over
2
+
3
+ > **Bus-factor doc (road-to-maintainer-bus-factor Phase 3).** If the maintainer
4
+ > is unavailable, this is what a successor needs to keep `main` healthy and cut
5
+ > releases. **No secret values live here — pointers only.** Companion:
6
+ > [`release-runbook.md`](release-runbook.md) (the how-to-release steps).
7
+
8
+ ## Honest bus-factor (tracked, not inflated)
9
+
10
+ - **Distinct humans who have reviewed/merged in the trailing 90 days: 1** (the
11
+ maintainer, `@matze4u`). The project is a single-maintainer repo; an AI
12
+ self-review gate (deferred — see the roadmap Phase 1) raises the floor but is
13
+ **not** independent human review.
14
+ - Recompute this number honestly at any time:
15
+
16
+ ```bash
17
+ # merged PRs in the last 90 days + who reviewed them
18
+ gh pr list --state merged --search "merged:>=$(date -v-90d +%Y-%m-%d 2>/dev/null || date -d '90 days ago' +%Y-%m-%d)" \
19
+ --json number,author,reviews \
20
+ --jq '[.[] | .reviews[]?.author.login] | unique'
21
+ ```
22
+
23
+ Report the count as-is. A bus-factor of 1 stated plainly beats a bus-factor of
24
+ 1 implied to be more.
25
+
26
+ ## Secrets / tokens — where they live, what they gate
27
+
28
+ Configured in **GitHub → Settings → Secrets and variables → Actions** (repo
29
+ secrets), never in the tree. None is required for the *core* release (version
30
+ bump + tag + GitHub Release); they gate the **downstream** publish/deploy legs.
31
+
32
+ | Secret | Gates | Missing → |
33
+ |---|---|---|
34
+ | `RELEASE_PR_TOKEN` (optional PAT / App token, `contents:write` + `pull-requests:write`) | Unattended release PR checks in [`release.yml`](../.github/workflows/release.yml) | One manual **"Approve workflows to run"** click per release (deliberate checkpoint, not a failure). |
35
+ | — (none: **npm OIDC Trusted Publishing**) | `publish-npm.yml` — uses `id-token: write`, no stored npm token | If OIDC trust is misconfigured on npmjs, npm publish fails; fix the trusted-publisher config on the npm package, not a secret. |
36
+ | `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_WORKER_SUBDOMAIN`, `MCP_SMOKE_TOKEN` | `deploy-mcp-worker.yml` (MCP worker deploy + smoke) | MCP worker deploy skips/fails; core release unaffected. |
37
+ | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY` | `cross-model-canary.yml` + any AI-council / self-review automation | Canary + council automation skip; core release unaffected. |
38
+ | `GITHUB_TOKEN` (auto-provided) | all workflows' default auth | n/a — GitHub injects it. |
39
+
40
+ The live source of truth for which workflow needs which secret is the
41
+ `secrets.*` references in [`.github/workflows/`](../.github/workflows/) — grep
42
+ there if this table drifts:
43
+
44
+ ```bash
45
+ grep -rhoE "secrets\.[A-Z_]+" .github/workflows/ | sort -u
46
+ ```
47
+
48
+ ## Operator-gated steps (need a human with credentials)
49
+
50
+ - **Release-PR workflow approval** — the "Approve workflows to run" click when
51
+ `RELEASE_PR_TOKEN` is absent (see the runbook § 3.A).
52
+ - **Branch-protection ruleset** — applied in GitHub → Settings → Rules UI;
53
+ source of truth mirrored in [`branch-protection-policy.md`](contracts/branch-protection-policy.md).
54
+ Not in code; a successor edits it in the UI.
55
+ - **Repo administration** (visibility / rename / delete) — the
56
+ `production-visibility` environment gates manual approval; see
57
+ [`CONTRIBUTING.md`](../CONTRIBUTING.md).
58
+ - **npm trusted-publisher config** — set on npmjs.org for the package, not in
59
+ this repo.
60
+
61
+ ## "Healthy main" — what good looks like
62
+
63
+ - Latest `main` commit is green on all required checks for its PR shape
64
+ (see [`branch-protection-policy.md`](contracts/branch-protection-policy.md),
65
+ [`ci-green-floor.md`](contracts/ci-green-floor.md)).
66
+ - No drift gate red: `release-drift.yml`, `release-guard.yml`,
67
+ `check-visibility-drift.yml`, the consistency + claims + proof gates.
68
+ - `docs/CLAIMS.md` resolves (`task check-claims`); `docs/proof.md` is in sync
69
+ (`task build-proof-check`).
70
+ - No completed roadmap left unarchived in `agents/roadmaps/` (the
71
+ `roadmap:progress-check` backstop).
72
+
73
+ ## Minimal takeover checklist
74
+
75
+ 1. Get `gh` authenticated as a repo admin; confirm `gh auth status`.
76
+ 2. Read [`release-runbook.md`](release-runbook.md) end-to-end.
77
+ 3. Confirm the secrets above are present for whichever downstream legs matter.
78
+ 4. Cut a `--dry-run` release (`task release -- --dry-run`) to see the pipeline
79
+ with zero mutations.
80
+ 5. When ready, follow the runbook for a real release.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "8.7.0",
3
+ "version": "8.8.0",
4
4
  "description": "Universal AI Agent OS — audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -12,7 +12,7 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/event4u-app/agent-config/issues"
14
14
  },
15
- "homepage": "https://github.com/event4u-app/agent-config#readme",
15
+ "homepage": "https://event4u-app.github.io/agent-config",
16
16
  "keywords": [
17
17
  "ai-agent",
18
18
  "mcp",
@@ -524,13 +524,21 @@ subagents:
524
524
  # on = auto-dispatch delegable tasks without asking (surface the choice
525
525
  # in one line).
526
526
  #
527
- # Initial SHIPPED default is `ask` on hosts whose capability manifest reports
527
+ # SHIPPED default is `on` on hosts whose capability manifest reports
528
528
  # `subagent_spawn: true`, and `off` on hosts without a subagent primitive.
529
- # The shipped default flips toward `on` (host-gated) only behind the Phase-6
530
- # benchmark gate of road-to-auto-subagent-orchestration.md — never on an
531
- # unmeasured claim. Auto-dispatch is also a no-op wherever the host manifest
532
- # reports `subagent_spawn: false`, regardless of this value.
533
- auto: ask
529
+ # Auto-dispatch stays a no-op wherever the manifest reports
530
+ # `subagent_spawn: false`, regardless of this value.
531
+ #
532
+ # The default was `ask` until 2026-07-09, when it was flipped to `on` after a
533
+ # re-evaluation (agents/settings/contexts/orchestration-default-flip-verdict.md
534
+ # § 2026-07-09; ADR-117). The flip rests on a bounded-downside argument, NOT a
535
+ # passed benchmark: delegable slices are already cost-routed to the cheapest
536
+ # capable tier, classification fires only on real structural signals (never
537
+ # unstructured or tiny work), and every return is verified — so `on` cannot
538
+ # delegate frontier-priced or speculative work. `resolveShippedDefault()`
539
+ # remains the telemetry-driven DEMOTION gate: a measured regression demotes
540
+ # back to `ask`.
541
+ auto: on
534
542
 
535
543
  # Route delegable sub-tasks to the lowest-capable model tier (cost + speed
536
544
  # via model downshift). true (default) = downshift per declared model_tier;
@@ -695,6 +703,17 @@ memory:
695
703
  # Example: ["api[_-]?key", "/Users/[a-z]+/Library"]
696
704
  redact_patterns: []
697
705
 
706
+ # Opt-in compact memory index at session start
707
+ # (road-to-memory-retrieval-economy Phase 5). When `on`, session_start
708
+ # injects a compact index of curated entries — id + title + ~tokens,
709
+ # hard cap 30 rows — through the hot-context hook surface. Bodies are
710
+ # never injected; the model fetches full entries via memory_get on
711
+ # demand. Default OFF: the ship-criterion (measured hit-rate gain at
712
+ # bounded fixed cost) is unproven — off unless proven.
713
+ # off = (default) no session-start index.
714
+ # on = inject the compact index (spotlighted as DATA).
715
+ session_index: "off"
716
+
698
717
  # --- Knowledge cards: global cross-project sharing (ADR-100) ---
699
718
  #
700
719
  # Structure-grounding v2. Promote *expensive* (remote) knowledge cards from
@@ -1,15 +1,19 @@
1
1
  /**
2
- * Default-flip decision gate (Phase 6).
2
+ * Default-flip / demotion decision gate (Phase 6 → telemetry-demotion).
3
3
  *
4
- * Pure, no-I/O. Encodes the Phase-6 falsification gate: the SHIPPED default for
5
- * `subagents.auto` flips toward `on` ONLY when a real benchmark shows a net
6
- * token-or-time win at held quality, and only on hosts with a subagent
7
- * primitive. Honest-null exit (no win, or quality regressed) keeps the
8
- * conservative default. Contract:
4
+ * Pure, no-I/O. Originally the Phase-6 falsification gate: the SHIPPED default
5
+ * for `subagents.auto` was to flip toward `on` ONLY on a passed benchmark.
6
+ *
7
+ * As of 2026-07-09 the shipped default IS `on` on subagent-capable hosts
8
+ * (ADR-117; agents/settings/contexts/orchestration-default-flip-verdict.md
9
+ * § 2026-07-09) — flipped on a bounded-downside re-evaluation, not a passed
10
+ * benchmark. This helper is therefore now the telemetry-driven DEMOTION gate:
11
+ * a `fail` verdict from accumulated real-world orchestration telemetry demotes
12
+ * the default back to `ask` (conservative). Contract:
9
13
  * `src/agent-src/contexts/execution/orchestration-benchmark-gate.md`.
10
14
  *
11
- * This helper decides; it never runs the benchmark. The measurement is the
12
- * empirical bench:ab step, authorised + run out of band.
15
+ * This helper decides; it never runs the measurement. The signal is realized
16
+ * orchestration telemetry, aggregated + evaluated out of band.
13
17
  */
14
18
 
15
19
  export interface BenchmarkResult {
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Orchestration savings aggregator (pure, no-I/O).
3
+ *
4
+ * Aggregates realized orchestration telemetry into a token-savings report.
5
+ * Consumes the `orchestration` sub-object of audit-log-v1 JSONL lines
6
+ * (contract: `src/agent-src/contexts/execution/orchestration-telemetry.md`).
7
+ *
8
+ * HONEST LIMIT — the telemetry records `token_delta` (net dispatch cost vs the
9
+ * in-session baseline; negative = saved) but NOT the absolute baseline. A clean
10
+ * "% of session saved" is therefore NOT derivable from this data alone — only
11
+ * ABSOLUTE net tokens saved, plus the measured-vs-estimated provenance split.
12
+ * A percentage would need an absolute-baseline field on the telemetry (see the
13
+ * report `notes` and the PR's follow-up section).
14
+ */
15
+
16
+ export type TokenDeltaProvenance = 'measured' | 'estimated';
17
+
18
+ /** The `orchestration` sub-object shape we read (subset — additive fields only). */
19
+ export interface OrchestrationRecord {
20
+ spawn_count?: number;
21
+ token_delta?: number;
22
+ token_delta_provenance?: TokenDeltaProvenance;
23
+ tier_chosen?: string | null;
24
+ task_class?: string | null;
25
+ outcome?: string;
26
+ }
27
+
28
+ /** A parsed audit-log-v1 line (only the fields this aggregator touches). */
29
+ export interface AuditLine {
30
+ input_kind?: string;
31
+ orchestration?: OrchestrationRecord;
32
+ }
33
+
34
+ export interface ProvenanceStat {
35
+ dispatches: number;
36
+ /** Σ token_delta for this provenance (negative = net saved). */
37
+ net_token_delta: number;
38
+ }
39
+
40
+ export interface SavingsReport {
41
+ /** Lines carrying an orchestration object with spawn_count > 0. */
42
+ dispatches: number;
43
+ total_spawns: number;
44
+ /** Σ token_delta across all dispatches (negative = net saved). */
45
+ net_token_delta: number;
46
+ /** Magnitude of tokens saved (sum of negative deltas, as a positive number). */
47
+ tokens_saved: number;
48
+ /** Sum of positive deltas (orchestration that cost MORE than baseline). */
49
+ tokens_added: number;
50
+ by_provenance: { measured: ProvenanceStat; estimated: ProvenanceStat };
51
+ /** tier_chosen → Σ token_delta (negative = net saved on that tier). */
52
+ by_tier: Record<string, number>;
53
+ /** task_class → Σ token_delta. */
54
+ by_task_class: Record<string, number>;
55
+ /** measured dispatches / dispatches, 0..1 (0 when no dispatches). */
56
+ measured_share: number;
57
+ /** Honest caveats about what the number does and does not mean. */
58
+ notes: string[];
59
+ }
60
+
61
+ function emptyProvenance(): ProvenanceStat {
62
+ return { dispatches: 0, net_token_delta: 0 };
63
+ }
64
+
65
+ /**
66
+ * Aggregate orchestration telemetry lines into a savings report.
67
+ *
68
+ * A line counts as a dispatch only when it carries an `orchestration` object
69
+ * with `spawn_count > 0` (spawn_count 0 / absent = handled in-session, not a
70
+ * dispatch). Unknown/absent fields default safely (token_delta → 0, provenance
71
+ * → estimated) so a pre-extension line never throws.
72
+ */
73
+ export function aggregateOrchestrationSavings(lines: AuditLine[]): SavingsReport {
74
+ const report: SavingsReport = {
75
+ dispatches: 0,
76
+ total_spawns: 0,
77
+ net_token_delta: 0,
78
+ tokens_saved: 0,
79
+ tokens_added: 0,
80
+ by_provenance: { measured: emptyProvenance(), estimated: emptyProvenance() },
81
+ by_tier: {},
82
+ by_task_class: {},
83
+ measured_share: 0,
84
+ notes: [],
85
+ };
86
+
87
+ for (const line of lines) {
88
+ const o = line.orchestration;
89
+ if (!o) continue;
90
+ const spawns = o.spawn_count ?? 0;
91
+ if (!(spawns > 0)) continue; // in-session, not a dispatch
92
+
93
+ report.dispatches += 1;
94
+ report.total_spawns += spawns;
95
+
96
+ const delta = o.token_delta ?? 0;
97
+ report.net_token_delta += delta;
98
+ if (delta < 0) report.tokens_saved += -delta;
99
+ else report.tokens_added += delta;
100
+
101
+ // Provenance defaults to `estimated` when unstated (matches the
102
+ // telemetry contract: estimated is the lossy fallback).
103
+ const prov: TokenDeltaProvenance = o.token_delta_provenance === 'measured' ? 'measured' : 'estimated';
104
+ report.by_provenance[prov].dispatches += 1;
105
+ report.by_provenance[prov].net_token_delta += delta;
106
+
107
+ const tier = o.tier_chosen ?? 'unknown';
108
+ report.by_tier[tier] = (report.by_tier[tier] ?? 0) + delta;
109
+
110
+ const cls = o.task_class ?? 'unclassified';
111
+ report.by_task_class[cls] = (report.by_task_class[cls] ?? 0) + delta;
112
+ }
113
+
114
+ report.measured_share = report.dispatches === 0 ? 0 : report.by_provenance.measured.dispatches / report.dispatches;
115
+
116
+ // Honest caveats — always attached so a consumer never over-reads the number.
117
+ if (report.dispatches === 0) {
118
+ report.notes.push('No orchestration telemetry yet (0 dispatches). The number accrues as `subagents.auto: on` runs delegate real slices.');
119
+ } else {
120
+ report.notes.push('Reports ABSOLUTE net token_delta (negative = saved). A "% of session saved" is NOT derivable — the telemetry records net delta, not the absolute baseline. A percentage needs an absolute-baseline field on the telemetry.');
121
+ if (report.by_provenance.estimated.dispatches > 0) {
122
+ report.notes.push(`${report.by_provenance.estimated.dispatches}/${report.dispatches} dispatch(es) use ESTIMATED token_delta (chars/4, lossy). Prefer measured (host usage metadata).`);
123
+ }
124
+ }
125
+
126
+ return report;
127
+ }
@@ -107,6 +107,42 @@ function render(): string {
107
107
  L.push('Benchmark results are published under `docs/` including measured nulls.');
108
108
  }
109
109
  L.push('');
110
+ L.push('**Behavioural-eval coverage — the honest baseline.** Skill *quality* is only');
111
+ L.push('as good as its measurement. Today **2 of 264** skills carry a behavioural');
112
+ L.push('`evals.json`, and the highest-traffic / highest-cost tiers (default-surface +');
113
+ L.push('`rich` + routers, **0 of 35**) are not yet covered. We publish that gap rather');
114
+ L.push('than imply "264 evaluated skills": coverage is measured per tier');
115
+ L.push('(`./scripts-run src/scripts/skill_eval_coverage`) and **CI-ratcheted so it can');
116
+ L.push('only rise** — a merged change can never lower it. Authoring the priority-tier');
117
+ L.push('evals is gated on per-case human ratification (a generated assertion that');
118
+ L.push('checks the wrong property is worse than none), so the number grows');
119
+ L.push('deliberately, not overnight.');
120
+ L.push('');
121
+ L.push('**Non-coding domain soundness — scoped, not proven.** The `finance` /');
122
+ L.push('`founder` / `ops` / `content` profiles sell concrete domain value (DCF,');
123
+ L.push('runway, RICE, incident command, messaging), but the skills are forged on');
124
+ L.push('TS/PHP — "promising, not proven" off those stacks. A disclaimer floor');
125
+ L.push('bounds *liability*, not *correctness*: a skill can be format-correct,');
126
+ L.push('disclaimered, and still embed a wrong domain assumption. Today **0 of 20**');
127
+ L.push('default-surface domain skills carry a sourced `domain-truth` fixture');
128
+ L.push('(`./scripts-run src/scripts/domain_soundness_status`); all are labeled');
129
+ L.push('`unvalidated` and the validated count is CI-ratcheted. Authoring the');
130
+ L.push('answer keys needs domain competence (a cited method, not the skill\'s own');
131
+ L.push('output), so validation lands deliberately — the gap is published, never');
132
+ L.push('implied away.');
133
+ L.push('');
134
+ L.push('**Second-brain substrate — continuity convenience, task-lift unmeasured.**');
135
+ L.push('The memory substrate is built (working-memory continuity across');
136
+ L.push('compaction, promotable knowledge cards, contradiction surfacing), but');
137
+ L.push('whether it beats a no-memory baseline on a reproducible task is **not yet');
138
+ L.push('measured**. The measurement rig exists — a deterministic multi-session');
139
+ L.push('recall corpus + scorer (`./scripts-run src/scripts/second_brain_score');
140
+ L.push('--dry-run`, no model-in-the-loop grading) — but the paired');
141
+ L.push('`memory-on`/`off`/`placebo` run is spend-bearing and pending. So no');
142
+ L.push('"second brain" capability claim is made anywhere; the substrate ships as');
143
+ L.push('continuity convenience until a paired run backs a lift. Boundary vs a');
144
+ L.push('human PKM is stated in [`docs/second-brain-scope.md`](second-brain-scope.md).');
145
+ L.push('');
110
146
  L.push('## 3. Known limits (published, witness-tested)');
111
147
  L.push('');
112
148
  const skillGaps = collectSkillGaps();
@@ -164,6 +200,8 @@ function render(): string {
164
200
  L.push('task check-refs # no broken internal references');
165
201
  L.push('task check-skill-gaps # every logged known-limit cites a real witness test');
166
202
  L.push('task check-comparison # every comparison-table "our evidence" pointer resolves');
203
+ L.push('./scripts-run src/scripts/skill_eval_coverage # behavioural-eval coverage, per tier');
204
+ L.push('./scripts-run src/scripts/skill_eval_coverage --check # the ratchet: coverage may not drop');
167
205
  L.push('task build-proof-check # this page is in sync with its sources');
168
206
  L.push('```');
169
207
  L.push('');
@@ -43,6 +43,8 @@ import * as fs from 'node:fs';
43
43
  import * as path from 'node:path';
44
44
  import { fileURLToPath, pathToFileURL } from 'node:url';
45
45
 
46
+ import { _entry_tokens_estimate } from './memory_lookup.js';
47
+
46
48
  import { load_agent_settings } from './_lib/agent_settings.js';
47
49
 
48
50
  export const DEFAULT_FILE = 'agents/runtime/.agent-chat-history';
@@ -708,6 +710,26 @@ export function read_entries(
708
710
  agent?: string | null | undefined;
709
711
  } = {},
710
712
  ): Entry[] {
713
+ return read_entries_with_refs(options).map((p) => p.entry);
714
+ }
715
+
716
+ /**
717
+ * Like {@link read_entries} but each entry carries its `ref` — the 0-based
718
+ * ordinal in the file's FULL chronological entry list (header excluded,
719
+ * counted BEFORE any session/agent/last filter), so a ref from an index row
720
+ * stays valid regardless of the filters a later call applies. Refs are
721
+ * stable for the append-only log; file rotation invalidates them (documented
722
+ * — a ref is a within-file locator, not a durable id).
723
+ * (road-to-memory-retrieval-economy Phase 3.)
724
+ */
725
+ export function read_entries_with_refs(
726
+ options: {
727
+ last?: number | null | undefined;
728
+ path?: string | null | undefined;
729
+ session?: string | null | undefined;
730
+ agent?: string | null | undefined;
731
+ } = {},
732
+ ): Array<{ entry: Entry; ref: number }> {
711
733
  const target = options.path ?? file_path();
712
734
  const last = options.last ?? null;
713
735
  const session = options.session ?? null;
@@ -715,8 +737,9 @@ export function read_entries(
715
737
  if (!_isFile(target)) {
716
738
  return [];
717
739
  }
718
- let entries: Entry[] = [];
740
+ let pairs: Array<{ entry: Entry; ref: number }> = [];
719
741
  const lines = _readLines(target);
742
+ let ref = 0;
720
743
  for (let i = 0; i < lines.length; i++) {
721
744
  const line = _strip(lines[i] ?? '');
722
745
  if (!line) {
@@ -732,23 +755,72 @@ export function read_entries(
732
755
  continue;
733
756
  }
734
757
  if (_isPlainObject(obj)) {
735
- entries.push(obj);
758
+ pairs.push({ entry: obj, ref });
759
+ ref += 1;
736
760
  }
737
761
  }
738
762
  if (session !== null) {
739
- entries = entries.filter((e) => e.s === session);
763
+ pairs = pairs.filter((p) => p.entry.s === session);
740
764
  }
741
765
  if (agent !== null) {
742
766
  if (agent === AGENT_UNKNOWN) {
743
- entries = entries.filter((e) => !e.agent);
767
+ pairs = pairs.filter((p) => !p.entry.agent);
744
768
  } else {
745
- entries = entries.filter((e) => e.agent === agent);
769
+ pairs = pairs.filter((p) => p.entry.agent === agent);
746
770
  }
747
771
  }
748
772
  if (last !== null && last >= 0) {
749
- entries = entries.slice(entries.length - last);
773
+ pairs = pairs.slice(pairs.length - last);
774
+ }
775
+ return pairs;
776
+ }
777
+
778
+ /**
779
+ * Timeline anchor: the entries around `ref` in the FULL chronological list —
780
+ * `before`/`after` neighbours plus the anchor itself. JSONL is chronological,
781
+ * so this is slicing, not indexing (road-to-memory-retrieval-economy P3).
782
+ * An out-of-range ref returns [].
783
+ */
784
+ export function slice_around(
785
+ path_: string | null,
786
+ ref: number,
787
+ before = 3,
788
+ after = 3,
789
+ ): Array<{ entry: Entry; ref: number }> {
790
+ const all = read_entries_with_refs({ path: path_ });
791
+ if (ref < 0 || ref >= all.length) {
792
+ return [];
793
+ }
794
+ const start = Math.max(0, ref - Math.max(0, before));
795
+ const end = Math.min(all.length, ref + Math.max(0, after) + 1);
796
+ return all.slice(start, end);
797
+ }
798
+
799
+ /**
800
+ * Compact index row for a history entry: ref + type tag + timestamp-ish
801
+ * fields when present + a ~100-char preview + the real-tokenizer cost of
802
+ * fetching the full entry (same lazy-tokenizer contract as memory_lookup's
803
+ * index rows).
804
+ */
805
+ export function history_index_row(pair: { entry: Entry; ref: number }): Record<string, unknown> {
806
+ const e = pair.entry;
807
+ const text = typeof e.text === 'string' ? e.text : '';
808
+ const row: Record<string, unknown> = {
809
+ ref: pair.ref,
810
+ t: e.t ?? '',
811
+ preview: text.slice(0, 100),
812
+ tokens_estimate: _entry_tokens_estimate(e as Record<string, unknown>),
813
+ };
814
+ if (typeof e.s === 'string') {
815
+ row.s = e.s;
816
+ }
817
+ for (const tsField of ['ts', 'time', 'started']) {
818
+ if (typeof (e as Record<string, unknown>)[tsField] === 'string') {
819
+ row[tsField] = (e as Record<string, unknown>)[tsField];
820
+ break;
821
+ }
750
822
  }
751
- return entries;
823
+ return row;
752
824
  }
753
825
 
754
826
  export function read_entries_for_current(