@event4u/agent-config 3.1.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.agent-src/commands/agent-status.md +1 -1
  2. package/.agent-src/commands/analytics/prune.md +78 -0
  3. package/.agent-src/commands/analytics/show.md +107 -0
  4. package/.agent-src/commands/analytics.md +64 -0
  5. package/.agent-src/commands/knowledge/forget.md +104 -0
  6. package/.agent-src/commands/knowledge/ingest.md +122 -0
  7. package/.agent-src/commands/knowledge/list.md +102 -0
  8. package/.agent-src/commands/knowledge.md +75 -0
  9. package/.agent-src/scripts/update_roadmap_progress.py +1 -1
  10. package/.agent-src/skills/compress-memory/SKILL.md +1 -1
  11. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  12. package/.claude-plugin/marketplace.json +8 -1
  13. package/AGENTS.md +5 -4
  14. package/CHANGELOG.md +54 -222
  15. package/README.md +12 -2
  16. package/dist/discovery/deprecation-report.md +1 -1
  17. package/dist/discovery/discovery-manifest.json +164 -10
  18. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  19. package/dist/discovery/discovery-manifest.summary.md +3 -3
  20. package/dist/discovery/orphan-report.md +1 -1
  21. package/dist/discovery/packs.json +12 -5
  22. package/dist/discovery/trust-report.md +2 -2
  23. package/dist/discovery/workspaces.json +11 -4
  24. package/dist/mcp/mcp-cloudflare-catalogue.json +2 -0
  25. package/dist/mcp/registry-manifest.json +5 -3
  26. package/docs/architecture.md +1 -1
  27. package/docs/archive/CHANGELOG-pre-3.2.0.md +268 -0
  28. package/docs/benchmarks.md +4 -4
  29. package/docs/catalog.md +9 -2
  30. package/docs/contracts/CHANGELOG-conventions.md +20 -1
  31. package/docs/contracts/adr-mcp-runtime.md +1 -1
  32. package/docs/contracts/at-rest-encryption.md +146 -0
  33. package/docs/contracts/benchmark-corpus-spec.md +3 -3
  34. package/docs/contracts/benchmark-report-schema.md +5 -5
  35. package/docs/contracts/caveman-telemetry.md +4 -4
  36. package/docs/contracts/compression-default-kill-criterion.md +5 -5
  37. package/docs/contracts/cost-enforcement.md +1 -1
  38. package/docs/contracts/daily-workspace.md +137 -0
  39. package/docs/contracts/explain-modes.md +146 -0
  40. package/docs/contracts/host-agent-protocol.md +88 -0
  41. package/docs/contracts/local-analytics.md +148 -0
  42. package/docs/contracts/local-knowledge-ingestion.md +96 -0
  43. package/docs/contracts/mcp-beta-criteria.md +1 -1
  44. package/docs/contracts/mcp-cloud-scope.md +4 -4
  45. package/docs/contracts/mcp-registry-manifest.schema.json +1 -1
  46. package/docs/contracts/mcp-tool-inventory.md +1 -1
  47. package/docs/contracts/mcp-tool-stub-envelope.md +1 -1
  48. package/docs/contracts/measurement-baseline.md +6 -6
  49. package/docs/contracts/role-experience.md +121 -0
  50. package/docs/contracts/workspace-documents.md +140 -0
  51. package/docs/decisions/ADR-022-daily-workspace-decomposition.md +140 -0
  52. package/docs/decisions/ADR-023-host-agent-protocol.md +129 -0
  53. package/docs/decisions/ADR-024-workspace-v0-feature-floor.md +126 -0
  54. package/docs/decisions/ADR-025-workspace-chrome.md +119 -0
  55. package/docs/decisions/ADR-026-explain-mode-translation.md +117 -0
  56. package/docs/decisions/ADR-027-changelog-machine-vs-manual.md +129 -0
  57. package/docs/decisions/ADR-028-root-layout.md +147 -0
  58. package/docs/decisions/ADR-029-multi-workspace-deferred.md +122 -0
  59. package/docs/decisions/INDEX.md +8 -0
  60. package/docs/deploy/small-team-recipe.md +148 -0
  61. package/docs/deploy/team-deployment-posture.md +91 -0
  62. package/docs/getting-started-by-role.md +27 -0
  63. package/docs/getting-started.md +1 -1
  64. package/docs/guides/local-analytics.md +125 -0
  65. package/docs/guides/local-knowledge.md +127 -0
  66. package/docs/mcp-server.md +1 -1
  67. package/docs/parity/bench-ruflo.json +3 -3
  68. package/docs/parity/ruflo.md +1 -1
  69. package/docs/setup/mcp-client-config.md +1 -1
  70. package/docs/setup/mcp-cloud-endpoints.md +1 -1
  71. package/docs/setup/mcp-cloud-setup.md +2 -2
  72. package/docs/setup/mcp-r2-bootstrap.md +1 -1
  73. package/package.json +4 -2
  74. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  75. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  76. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  77. package/scripts/_lib/bench_caveman.py +2 -2
  78. package/scripts/_lib/bench_caveman_report.py +1 -1
  79. package/scripts/_lib/bench_cost.py +2 -2
  80. package/scripts/_lib/bench_report.py +2 -2
  81. package/scripts/_lib/changelog_eras.py +330 -0
  82. package/scripts/audit_mcp_tools.py +1 -1
  83. package/scripts/bench_baseline_ready.py +3 -3
  84. package/scripts/bench_compress_memory.py +4 -4
  85. package/scripts/bench_drift_check.py +2 -2
  86. package/scripts/bench_per_tool.py +2 -2
  87. package/scripts/bench_run.py +4 -4
  88. package/scripts/build_mcp_registry_manifest.py +2 -2
  89. package/scripts/mcp_server/__init__.py +1 -1
  90. package/scripts/mcp_server/catalog.py +1 -1
  91. package/scripts/mcp_server/consumer_tool_catalog.json +1 -1
  92. package/scripts/mcp_server/tools.py +1 -1
  93. package/scripts/memory_lookup.py +78 -1
  94. package/scripts/pack_mcp_content.py +6 -6
  95. package/scripts/release.py +93 -3
  96. package/scripts/skill_trigger_eval.py +2 -2
@@ -0,0 +1,330 @@
1
+ """Shared constants + helpers for CHANGELOG.md era discipline.
2
+
3
+ The drift gate (``tests/test_changelog_eras.py``) and the release
4
+ automation (``scripts/release.py``) both reason about the same era
5
+ shape: a single ``# Era: X.Y.x — current`` header followed by inline
6
+ entries, then ``# Era: pre-X.Y.0 — archived`` pointers to files under
7
+ ``docs/archive/``. Keeping the regex / cap / path constants in one
8
+ place prevents drift between the gate and the auto-split logic.
9
+
10
+ Normative source: ``docs/contracts/CHANGELOG-conventions.md § Era splits``.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ REPO_ROOT = Path(__file__).resolve().parent.parent.parent
19
+ CHANGELOG = REPO_ROOT / "CHANGELOG.md"
20
+ CONVENTIONS = REPO_ROOT / "docs" / "contracts" / "CHANGELOG-conventions.md"
21
+ ARCHIVE_DIR = REPO_ROOT / "docs" / "archive"
22
+
23
+ # Drift cap — entries between the current era header and the next era
24
+ # header may not exceed this many lines. Raising the cap is a contract
25
+ # change (see CHANGELOG-conventions.md § Era splits).
26
+ CURRENT_ERA_BODY_CAP = 250
27
+
28
+ ERA_HEADER_RE = re.compile(
29
+ r"^# Era: (?P<label>[^\n]+?)(?: — (?P<state>current|archived))?\s*$"
30
+ )
31
+ ARCHIVE_LINK_RE = re.compile(r"\(docs/archive/(CHANGELOG-pre-[^)\s]+\.md)\)")
32
+ VERSION_HEADING_RE = re.compile(r"^## \[?(?P<version>\d+\.\d+\.\d+)")
33
+ ERA_LABEL_RE = re.compile(r"^(?P<major>\d+)\.(?P<minor>\d+)\.x$")
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class EraSpan:
38
+ """One era header in CHANGELOG.md, with its line index."""
39
+
40
+ line_index: int
41
+ label: str
42
+ state: str # "current" | "archived" | ""
43
+
44
+
45
+ def read_changelog_lines() -> list[str]:
46
+ """Return CHANGELOG.md split into lines (no trailing newlines)."""
47
+ return CHANGELOG.read_text(encoding="utf-8").splitlines()
48
+
49
+
50
+ def era_spans(lines: list[str]) -> list[EraSpan]:
51
+ """Return every era header in line-order."""
52
+ spans: list[EraSpan] = []
53
+ for i, line in enumerate(lines):
54
+ m = ERA_HEADER_RE.match(line)
55
+ if m:
56
+ spans.append(
57
+ EraSpan(
58
+ line_index=i,
59
+ label=m.group("label"),
60
+ state=m.group("state") or "",
61
+ )
62
+ )
63
+ return spans
64
+
65
+
66
+ def current_era_index(spans: list[EraSpan]) -> int | None:
67
+ """Return the line index of the ``— current`` era header, or None."""
68
+ for span in spans:
69
+ if span.state == "current":
70
+ return span.line_index
71
+ return None
72
+
73
+
74
+ def current_era_body_size(lines: list[str] | None = None) -> int:
75
+ """Return the number of lines between the current era header and
76
+ the next era header (exclusive of both)."""
77
+ if lines is None:
78
+ lines = read_changelog_lines()
79
+ spans = era_spans(lines)
80
+ current_idx = current_era_index(spans)
81
+ if current_idx is None:
82
+ return 0
83
+ next_era_line = len(lines)
84
+ for span in spans:
85
+ if span.line_index > current_idx:
86
+ next_era_line = span.line_index
87
+ break
88
+ return next_era_line - current_idx - 1
89
+
90
+
91
+ def parse_era_label(label: str) -> tuple[int, int] | None:
92
+ """Parse ``M.N.x`` into ``(M, N)``; return None for archived labels."""
93
+ m = ERA_LABEL_RE.match(label.strip())
94
+ if not m:
95
+ return None
96
+ return int(m.group("major")), int(m.group("minor"))
97
+
98
+
99
+ def archive_path_for_boundary(boundary: str) -> Path:
100
+ """Return ``docs/archive/CHANGELOG-pre-<boundary>.md``."""
101
+ return ARCHIVE_DIR / f"CHANGELOG-pre-{boundary}.md"
102
+
103
+
104
+ def collapsed_era_block(boundary: str) -> str:
105
+ """Render the standard ``# Era: pre-<boundary> — archived`` pointer
106
+ block that replaces archived entries in CHANGELOG.md.
107
+
108
+ Mirrors the wording the manual splits already used (verified against
109
+ every existing collapsed era as of 3.2.x).
110
+ """
111
+ archive_rel = f"docs/archive/CHANGELOG-pre-{boundary}.md"
112
+ return (
113
+ f"# Era: pre-{boundary} — archived\n"
114
+ "\n"
115
+ f"> All entries before `{boundary}` live in\n"
116
+ f"> [`{archive_rel}`]({archive_rel}).\n"
117
+ "> The archive is read-only; git tags remain the canonical\n"
118
+ "> source for what shipped. Splitting them out of the main file\n"
119
+ "> keeps the active era under the 250-line drift cap enforced by\n"
120
+ "> `tests/test_changelog_eras.py`.\n"
121
+ )
122
+
123
+
124
+ def archive_file_header(boundary: str) -> str:
125
+ """Return the standard prologue for ``docs/archive/CHANGELOG-pre-<boundary>.md``."""
126
+ return (
127
+ f"# Changelog Archive — pre-{boundary}\n"
128
+ "\n"
129
+ f"> Frozen snapshot of `event4u/agent-config` changelog entries\n"
130
+ f"> released before `{boundary}`, split out of the main\n"
131
+ "> [`CHANGELOG.md`](../../CHANGELOG.md) by `scripts/release.py`\n"
132
+ "> once the active era's body crossed the drift cap enforced by\n"
133
+ "> `tests/test_changelog_eras.py`.\n"
134
+ ">\n"
135
+ "> **Read-only.** New entries land in `CHANGELOG.md`. Entries\n"
136
+ "> here are not amended — git tags remain the canonical source\n"
137
+ "> for what shipped.\n"
138
+ ">\n"
139
+ "> Entry shape follows\n"
140
+ "> [`../contracts/CHANGELOG-conventions.md`](../contracts/CHANGELOG-conventions.md).\n"
141
+ "\n"
142
+ )
143
+
144
+
145
+ # ─── split planning + execution ────────────────────────────────────────────────
146
+
147
+
148
+ _RELEASE_VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
149
+
150
+
151
+ @dataclass(frozen=True)
152
+ class SplitPlan:
153
+ """Recipe for an era split during release of ``release_version``."""
154
+
155
+ release_version: str # e.g. "3.3.0"
156
+ boundary: str # e.g. "3.3.0" — used in archive filename + pointer
157
+ new_era_label: str # e.g. "3.3.x"
158
+ old_era_label: str # e.g. "3.2.x"
159
+ archive_path: Path
160
+
161
+ @property
162
+ def commit_subject(self) -> str:
163
+ return (
164
+ f"chore(changelog): split era {self.old_era_label} "
165
+ f"→ pre-{self.boundary}"
166
+ )
167
+
168
+
169
+ def plan_split(release_version: str) -> SplitPlan | None:
170
+ """Plan an era split when releasing ``release_version``.
171
+
172
+ Returns None when no split is needed (release is a patch within the
173
+ current era, or no current era header exists). Returns a SplitPlan
174
+ when the release crosses a minor or major boundary; the caller
175
+ decides whether to invoke ``perform_split`` based on era body size.
176
+
177
+ Raises ValueError when ``release_version`` is not bare semver, or
178
+ when it would move backward relative to the current era label.
179
+ """
180
+ m = _RELEASE_VERSION_RE.match(release_version.strip())
181
+ if not m:
182
+ raise ValueError(f"not a bare semver (X.Y.Z): {release_version!r}")
183
+ rel_major, rel_minor, _rel_patch = (int(m.group(i)) for i in (1, 2, 3))
184
+
185
+ lines = read_changelog_lines()
186
+ spans = era_spans(lines)
187
+ current = next((s for s in spans if s.state == "current"), None)
188
+ if current is None:
189
+ return None
190
+
191
+ parsed = parse_era_label(current.label)
192
+ if parsed is None:
193
+ return None
194
+ era_major, era_minor = parsed
195
+
196
+ if (rel_major, rel_minor) < (era_major, era_minor):
197
+ raise ValueError(
198
+ f"release {release_version!r} is older than current era "
199
+ f"{current.label!r}; refusing to plan a backwards split"
200
+ )
201
+ if (rel_major, rel_minor) == (era_major, era_minor):
202
+ # Patch release within the current era — no era boundary crossed,
203
+ # so an auto-split would create a nonsensical archive name. The
204
+ # caller is expected to die() with the manual-intervention message.
205
+ return None
206
+
207
+ boundary = f"{rel_major}.{rel_minor}.0"
208
+ return SplitPlan(
209
+ release_version=release_version,
210
+ boundary=boundary,
211
+ new_era_label=f"{rel_major}.{rel_minor}.x",
212
+ old_era_label=current.label,
213
+ archive_path=archive_path_for_boundary(boundary),
214
+ )
215
+
216
+
217
+ def new_era_intro_block(new_era_label: str, boundary: str) -> str:
218
+ """Render the header + blockquote intro for a freshly-split current era."""
219
+ parsed = parse_era_label(new_era_label)
220
+ if parsed is None:
221
+ next_example = "# Era: <next>.x"
222
+ else:
223
+ m, n = parsed
224
+ next_example = f"# Era: {m}.{n + 1}.x"
225
+ return (
226
+ f"# Era: {new_era_label} — current\n"
227
+ "\n"
228
+ f"> Started at `{boundary}`. Full entries live inline below.\n"
229
+ "> The drift test caps this era at 250 lines of entry body; growth past\n"
230
+ f"> that forces a new era split (`{next_example}`, etc.) — see\n"
231
+ "> [`docs/contracts/CHANGELOG-conventions.md § Era splits`](docs/contracts/CHANGELOG-conventions.md).\n"
232
+ )
233
+
234
+
235
+ def _era_body_bounds(
236
+ lines: list[str], current_idx: int
237
+ ) -> tuple[int, int, int]:
238
+ """Return ``(body_start, body_end, next_era_line)`` for the era at
239
+ ``current_idx``.
240
+
241
+ * ``body_start`` — first line after the header + leading blockquote
242
+ intro + the blank line that follows.
243
+ * ``body_end`` — exclusive; one line before the next era marker (or
244
+ end of file). Trailing blank lines are NOT trimmed; the caller
245
+ reattaches them on splice.
246
+ * ``next_era_line`` — index of the next ``# Era:`` line, or
247
+ ``len(lines)`` when none follows.
248
+ """
249
+ next_era_line = len(lines)
250
+ for i in range(current_idx + 1, len(lines)):
251
+ if ERA_HEADER_RE.match(lines[i]):
252
+ next_era_line = i
253
+ break
254
+
255
+ cursor = current_idx + 1
256
+ # Skip leading blank lines between header and blockquote intro.
257
+ while cursor < next_era_line and lines[cursor].strip() == "":
258
+ cursor += 1
259
+ # Skip the leading blockquote intro (consecutive `>`-prefixed lines).
260
+ while cursor < next_era_line and lines[cursor].startswith(">"):
261
+ cursor += 1
262
+ # Skip the blank separator between intro and entries.
263
+ while cursor < next_era_line and lines[cursor].strip() == "":
264
+ cursor += 1
265
+
266
+ return cursor, next_era_line, next_era_line
267
+
268
+
269
+ def current_era_insertion_point(lines: list[str]) -> int | None:
270
+ """Return the line index at which a new release entry should be
271
+ prepended within the current era.
272
+
273
+ Strategy:
274
+ * If the current era body contains one or more ``## [X.Y.Z]``
275
+ headings, return the line of the topmost (newest) one.
276
+ * Otherwise, return the first line after the era intro blockquote.
277
+
278
+ Returns None when no current era header exists.
279
+ """
280
+ spans = era_spans(lines)
281
+ current_idx = current_era_index(spans)
282
+ if current_idx is None:
283
+ return None
284
+ body_start, body_end, _ = _era_body_bounds(lines, current_idx)
285
+ for i in range(body_start, body_end):
286
+ if VERSION_HEADING_RE.match(lines[i]):
287
+ return i
288
+ return body_start
289
+
290
+
291
+ def perform_split(plan: SplitPlan) -> None:
292
+ """Execute ``plan`` against the on-disk CHANGELOG.md.
293
+
294
+ * Refuses to overwrite an existing archive file.
295
+ * Moves every entry in the current era body into the new archive.
296
+ * Replaces the current era block with the collapsed pointer + the
297
+ freshly-labelled new current era header (empty body).
298
+ """
299
+ if plan.archive_path.exists():
300
+ raise FileExistsError(
301
+ f"archive already exists at {plan.archive_path} — "
302
+ "likely a previous --resume run; inspect manually"
303
+ )
304
+
305
+ lines = read_changelog_lines()
306
+ spans = era_spans(lines)
307
+ current_idx = current_era_index(spans)
308
+ if current_idx is None:
309
+ raise RuntimeError("no current era header found in CHANGELOG.md")
310
+
311
+ body_start, _, next_era_line = _era_body_bounds(lines, current_idx)
312
+ entries = lines[body_start:next_era_line]
313
+ # Trim trailing blank lines so the archive doesn't accumulate them.
314
+ while entries and entries[-1].strip() == "":
315
+ entries.pop()
316
+
317
+ collapsed = collapsed_era_block(plan.boundary).rstrip("\n").splitlines()
318
+ new_era = new_era_intro_block(plan.new_era_label, plan.boundary).rstrip("\n").splitlines()
319
+
320
+ head = lines[:current_idx]
321
+ tail = lines[next_era_line:]
322
+ new_lines = head + collapsed + [""] + new_era + [""] + tail
323
+ new_text = "\n".join(new_lines).rstrip() + "\n"
324
+
325
+ archive_body = "\n".join(entries).rstrip() + "\n" if entries else ""
326
+ archive_text = archive_file_header(plan.boundary) + archive_body
327
+
328
+ plan.archive_path.parent.mkdir(parents=True, exist_ok=True)
329
+ plan.archive_path.write_text(archive_text, encoding="utf-8")
330
+ CHANGELOG.write_text(new_text, encoding="utf-8")
@@ -105,7 +105,7 @@ def _render(catalog: dict, handlers: dict[str, int], cat_lines: dict[str, int])
105
105
  lines.append("## Glossary")
106
106
  lines.append("")
107
107
  lines.append("- **Side-effect** — `ro` (read-only) · `fs-write` (filesystem write) · `shell` (spawns processes).")
108
- lines.append("- **Transports** — `stdio` (`scripts/mcp_server/`) · `worker` (`workers/mcp/`). A tool may live on both.")
108
+ lines.append("- **Transports** — `stdio` (`scripts/mcp_server/`) · `worker` (`internal/workers/mcp/`). A tool may live on both.")
109
109
  lines.append("- **Stub** — catalog-listed for discovery; returns the `not_implemented` envelope from")
110
110
  lines.append(" [`mcp-tool-stub-envelope.md`](mcp-tool-stub-envelope.md) until promoted.")
111
111
  lines.append("")
@@ -2,7 +2,7 @@
2
2
  """Baseline-closure check — step-4 Phase 3 Step 4.
3
3
 
4
4
  Returns exit 0 iff the 60-day clock has elapsed since
5
- `bench/baseline-start.txt` AND `bench/reports/` contains at least
5
+ `internal/bench/baseline-start.txt` AND `internal/bench/reports/` contains at least
6
6
  `--min-reports` complete runs for the named corpus (default 30).
7
7
 
8
8
  Read by P2 enforcement roadmaps as their precondition (G1 gate in
@@ -50,8 +50,8 @@ def main(argv: list[str] | None = None) -> int:
50
50
  formatter_class=argparse.RawDescriptionHelpFormatter,
51
51
  )
52
52
  ap.add_argument("--corpus", default="dev")
53
- ap.add_argument("--reports-dir", default="bench/reports")
54
- ap.add_argument("--baseline-file", default="bench/baseline-start.txt")
53
+ ap.add_argument("--reports-dir", default="internal/bench/reports")
54
+ ap.add_argument("--baseline-file", default="internal/bench/baseline-start.txt")
55
55
  ap.add_argument("--min-days", type=int, default=60)
56
56
  ap.add_argument("--min-reports", type=int, default=30)
57
57
  ap.add_argument("--json", action="store_true")
@@ -3,10 +3,10 @@
3
3
 
4
4
  Runs `compress_memory.py` over a fixed corpus of memory-target files, records
5
5
  pre/post char counts, approximates input-token savings (chars / 4 — the
6
- GPT-4 / Claude rule of thumb), and emits `bench/reports/caveman-v2.{json,md}`.
6
+ GPT-4 / Claude rule of thumb), and emits `internal/bench/reports/caveman-v2.{json,md}`.
7
7
 
8
8
  Offline (no API calls). Cadence-aligned with `docs/benchmarks.md`. Citation
9
- in `bench/reports/caveman-v2.md` notes the chars→tokens approximation and
9
+ in `internal/bench/reports/caveman-v2.md` notes the chars→tokens approximation and
10
10
  points at upstream tiktoken / claude-tokenizer if a calibrated number is
11
11
  later needed.
12
12
  """
@@ -23,8 +23,8 @@ from pathlib import Path
23
23
 
24
24
  REPO_ROOT = Path(__file__).resolve().parent.parent
25
25
  COMPRESS_SCRIPT = REPO_ROOT / "scripts" / "compress_memory.py"
26
- REPORT_JSON = REPO_ROOT / "bench" / "reports" / "caveman-v2.json"
27
- REPORT_MD = REPO_ROOT / "bench" / "reports" / "caveman-v2.md"
26
+ REPORT_JSON = REPO_ROOT / "internal" / "bench" / "reports" / "caveman-v2.json"
27
+ REPORT_MD = REPO_ROOT / "internal" / "bench" / "reports" / "caveman-v2.md"
28
28
 
29
29
  CORPUS: list[tuple[str, str]] = [
30
30
  ("AGENTS.md", "thin-root-package"),
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """Drift detector for the bench corpus — step-4 Phase 3 Step 2.
3
3
 
4
- Compares the latest `bench/reports/<stamp>-<corpus>.json` against the
4
+ Compares the latest `internal/bench/reports/<stamp>-<corpus>.json` against the
5
5
  previous N reports (default 5) for the same corpus. Drift defined as:
6
6
 
7
7
  - selection-accuracy: latest is more than `accuracy_drop_pp` below
@@ -99,7 +99,7 @@ def _check(latest: dict[str, Any], baseline: list[dict[str, Any]],
99
99
  def main(argv: list[str] | None = None) -> int:
100
100
  ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
101
101
  ap.add_argument("--corpus", default="dev")
102
- ap.add_argument("--reports-dir", default="bench/reports")
102
+ ap.add_argument("--reports-dir", default="internal/bench/reports")
103
103
  ap.add_argument("--window", type=int, default=5, help="rolling window size (default 5)")
104
104
  ap.add_argument("--accuracy-drop-pp", type=float, default=5.0)
105
105
  ap.add_argument("--cost-increase-pct", type=float, default=20.0)
@@ -43,7 +43,7 @@ from bench_runner import rank_skills # type: ignore # noqa: E402
43
43
 
44
44
  REPO_ROOT = Path(__file__).resolve().parent.parent
45
45
  CORPUS_DIR = REPO_ROOT / "tests" / "eval"
46
- REPORTS_DIR = REPO_ROOT / "bench" / "reports"
46
+ REPORTS_DIR = REPO_ROOT / "internal" / "bench" / "reports"
47
47
 
48
48
  # tool_id -> (skills_root, kind). kind = "skills" | "rules_only" | "single_file".
49
49
  SURFACES: dict[str, tuple[Path, str]] = {
@@ -185,7 +185,7 @@ def main(argv=None) -> int:
185
185
  ap.add_argument("--threshold", type=float, default=0.85)
186
186
  ap.add_argument("--json", action="store_true")
187
187
  ap.add_argument("--write-report", action="store_true",
188
- help="emit bench/reports/<ts>-<corpus>-projection.{json,md}")
188
+ help="emit internal/bench/reports/<ts>-<corpus>-projection.{json,md}")
189
189
  args = ap.parse_args(argv)
190
190
 
191
191
  corpus_path = CORPUS_DIR / f"corpus-{args.corpus}.yaml"
@@ -5,7 +5,7 @@ Wraps the selection-accuracy baseline collector (`scripts/bench_runner.py`),
5
5
  captures token / cost data from `agents/cost-tracking/sessions.jsonl` if
6
6
  present (per ruflo pattern, external-findings § 2), runs structural
7
7
  quality assertions per prompt, and emits a versioned JSON + Markdown
8
- report under `bench/reports/` per
8
+ report under `internal/bench/reports/` per
9
9
  `docs/contracts/benchmark-report-schema.md`.
10
10
 
11
11
  Usage:
@@ -46,11 +46,11 @@ except ImportError:
46
46
  sys.exit(2)
47
47
 
48
48
  BENCH_RUN_VERSION = "0.2.0"
49
- PRICING_PATH = REPO_ROOT / "bench" / "pricing.yaml"
49
+ PRICING_PATH = REPO_ROOT / "internal" / "bench" / "pricing.yaml"
50
50
  SESSIONS_JSONL = REPO_ROOT / "agents" / "cost-tracking" / "sessions.jsonl"
51
- REPORTS_DIR = REPO_ROOT / "bench" / "reports"
51
+ REPORTS_DIR = REPO_ROOT / "internal" / "bench" / "reports"
52
52
  CORPUS_DIR = REPO_ROOT / "tests" / "eval"
53
- CAVEMAN_CORPUS = REPO_ROOT / "bench" / "corpora" / "caveman" / "prompts.yaml"
53
+ CAVEMAN_CORPUS = REPO_ROOT / "internal" / "bench" / "corpora" / "caveman" / "prompts.yaml"
54
54
  BASELINE_COLLECTOR = REPO_ROOT / "scripts" / "bench_runner.py"
55
55
 
56
56
 
@@ -4,7 +4,7 @@
4
4
  Reads three on-disk sources:
5
5
  * `package.json` — name, version, description, homepage, repository
6
6
  * `.github/topics.yml` — topics list (for registries that accept tags)
7
- * `workers/mcp/content.json` — `tool_catalog` (tools_count, install_hint_stdio)
7
+ * `internal/workers/mcp/content.json` — `tool_catalog` (tools_count, install_hint_stdio)
8
8
  * `dist/discovery/discovery-manifest.json` — artefact_count + scanner_version (HARD prereq per AI-Council R5)
9
9
 
10
10
  Emits:
@@ -37,7 +37,7 @@ import yaml
37
37
  ROOT = Path(__file__).resolve().parents[1]
38
38
  PKG_FILE = ROOT / "package.json"
39
39
  TOPICS_FILE = ROOT / ".github" / "topics.yml"
40
- CONTENT_FILE = ROOT / "workers" / "mcp" / "content.json"
40
+ CONTENT_FILE = ROOT / "internal" / "workers" / "mcp" / "content.json"
41
41
  DISCOVERY_FILE = ROOT / "dist" / "discovery" / "discovery-manifest.json"
42
42
  OUT_DIR = ROOT / "dist" / "mcp"
43
43
  OUT_MANIFEST = OUT_DIR / "registry-manifest.json"
@@ -2,7 +2,7 @@
2
2
 
3
3
  mcp_scope: full — local stdio access can be extended to tool execution
4
4
  under the Phase 7 wake-up triggers in `docs/contracts/mcp-cloud-scope.md`.
5
- The hosted Worker (`workers/mcp/`) is `mcp_scope: lite` and is
5
+ The hosted Worker (`internal/workers/mcp/`) is `mcp_scope: lite` and is
6
6
  intentionally narrower.
7
7
 
8
8
  Exposes a hand-picked subset of `.agent-src/skills/` as MCP `prompts`
@@ -108,7 +108,7 @@ def not_implemented_envelope(
108
108
  ) -> dict[str, Any]:
109
109
  """Wire-shape error envelope used when a stub is invoked.
110
110
 
111
- Mirrored verbatim by the Cloud Worker (`workers/mcp/src/stubs.ts`).
111
+ Mirrored verbatim by the Cloud Worker (`internal/workers/mcp/src/stubs.ts`).
112
112
  """
113
113
  return {
114
114
  "code": NOT_IMPLEMENTED_CODE,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "description": "Source-of-truth catalog of consumer-relevant MCP tools. Read by the stdio server (scripts/mcp_server/) and packed into the Cloud Worker bundle (workers/mcp/). Phase 1 of road-to-mcp-full-coverage: tools without 'implemented' transports return the 'not_implemented' envelope defined in docs/contracts/mcp-tool-stub-envelope.md. The 'implemented_on' field lists transports where the real handler is wired; everything else is a discovery stub. See agents/roadmaps/archive/road-to-mcp-full-coverage.md.",
3
+ "description": "Source-of-truth catalog of consumer-relevant MCP tools. Read by the stdio server (scripts/mcp_server/) and packed into the Cloud Worker bundle (internal/workers/mcp/). Phase 1 of road-to-mcp-full-coverage: tools without 'implemented' transports return the 'not_implemented' envelope defined in docs/contracts/mcp-tool-stub-envelope.md. The 'implemented_on' field lists transports where the real handler is wired; everything else is a discovery stub. See agents/roadmaps/archive/road-to-mcp-full-coverage.md.",
4
4
  "install_hint_stdio": "pip install agent-config[mcp] && ./agent-config mcp:run",
5
5
  "tools": [
6
6
  {
@@ -43,7 +43,7 @@ from .catalog import (
43
43
  from .telemetry import Outcome, record_call
44
44
 
45
45
  # Stable transport tag for the stub envelope. Mirrored verbatim by
46
- # `workers/mcp/src/stubs.ts` with ``"worker"``.
46
+ # `internal/workers/mcp/src/stubs.ts` with ``"worker"``.
47
47
  STDIO_TRANSPORT = "stdio"
48
48
 
49
49
  # Allowlisted directories (relative to consumer_root) where tool writes
@@ -39,6 +39,7 @@ from typing import Any, Callable, Iterable, Optional, Union
39
39
 
40
40
  MEMORY_ROOT = Path("agents/memory")
41
41
  INTAKE_ROOT = MEMORY_ROOT / "intake"
42
+ KNOWLEDGE_ROOT = MEMORY_ROOT / "knowledge"
42
43
 
43
44
  CURATED_TYPES = {
44
45
  "ownership",
@@ -49,6 +50,12 @@ CURATED_TYPES = {
49
50
  "product-rules",
50
51
  }
51
52
 
53
+ # `knowledge` is its own type: user-ingested local documents that live
54
+ # under `agents/memory/knowledge/<ingest-id>/chunks/*.md`. They are
55
+ # repo-side (file-backed) but not "curated" and not intake — the
56
+ # conflict rule still treats them as repo entries against operational.
57
+ KNOWLEDGE_TYPE = "knowledge"
58
+
52
59
 
53
60
  @dataclass
54
61
  class Hit:
@@ -167,6 +174,58 @@ def _iter_intake_entries(mtype: str) -> Iterable[tuple[Path, dict]]:
167
174
  yield jsonl, obj
168
175
 
169
176
 
177
+ def _iter_knowledge_entries() -> Iterable[tuple[Path, dict]]:
178
+ """Yield (chunk-file, entry) pairs from `agents/memory/knowledge/`.
179
+
180
+ Layout (frozen in `docs/contracts/local-knowledge-ingestion.md`):
181
+
182
+ agents/memory/knowledge/<ingest-id>/
183
+ manifest.json
184
+ chunks/<n>.md
185
+
186
+ Each chunk becomes one retrieval entry. The chunk body, the
187
+ manifest source path, and pinned flag are surfaced into the entry
188
+ so `_score()` can match on either the source path or the chunk
189
+ text. The entry id is ``<ingest-id>:<chunk-stem>`` so callers can
190
+ locate the exact file on disk.
191
+ """
192
+ if not KNOWLEDGE_ROOT.is_dir():
193
+ return
194
+ for ingest_dir in sorted(KNOWLEDGE_ROOT.iterdir()):
195
+ if not ingest_dir.is_dir():
196
+ continue
197
+ manifest_path = ingest_dir / "manifest.json"
198
+ manifest: dict = {}
199
+ if manifest_path.is_file():
200
+ try:
201
+ manifest = json.loads(
202
+ manifest_path.read_text(encoding="utf-8")
203
+ )
204
+ except (ValueError, OSError):
205
+ manifest = {}
206
+ ingest_id = str(manifest.get("ingest_id") or ingest_dir.name)
207
+ source = str(manifest.get("source") or "")
208
+ pinned = bool(manifest.get("pinned", False))
209
+ chunks_dir = ingest_dir / "chunks"
210
+ if not chunks_dir.is_dir():
211
+ continue
212
+ for chunk in sorted(chunks_dir.glob("*.md")):
213
+ try:
214
+ body = chunk.read_text(encoding="utf-8")
215
+ except OSError:
216
+ continue
217
+ entry = {
218
+ "id": f"{ingest_id}:{chunk.stem}",
219
+ "ingest_id": ingest_id,
220
+ "source": source,
221
+ "path": source,
222
+ "body": body,
223
+ "pinned": pinned,
224
+ "source_kind": "knowledge",
225
+ }
226
+ yield chunk, entry
227
+
228
+
170
229
  def _score(entry: dict, keys: list[str]) -> float:
171
230
  """Naive relevance score: max over keys of (glob-match | substring).
172
231
 
@@ -378,6 +437,24 @@ def retrieve(
378
437
  """
379
438
  repo_hits: list[Hit] = []
380
439
  for mtype in types:
440
+ if mtype == KNOWLEDGE_TYPE:
441
+ for path, entry in _iter_knowledge_entries():
442
+ base = _score(entry, keys)
443
+ # Pinned entries get a slight ranking boost so the
444
+ # `/knowledge:list --pin` flag has retrieval effect.
445
+ if entry.get("pinned"):
446
+ base = min(1.0, base + 0.05)
447
+ repo_hits.append(Hit(
448
+ id=str(entry.get("id", "")),
449
+ type=KNOWLEDGE_TYPE,
450
+ source="knowledge",
451
+ path=str(path),
452
+ # Discount vs curated/intake so hand-reviewed repo
453
+ # entries still win on equal relevance.
454
+ score=base * 0.85,
455
+ entry=entry,
456
+ ))
457
+ continue
381
458
  if mtype not in CURATED_TYPES:
382
459
  continue
383
460
  for path, entry in _iter_curated_entries(mtype):
@@ -426,7 +503,7 @@ CONTRACT_VERSION = 1
426
503
 
427
504
  # Memory types this file-backed backend can answer. Types outside this
428
505
  # set map to `unknown_type` per the retrieval contract.
429
- _KNOWN_TYPES = CURATED_TYPES
506
+ _KNOWN_TYPES = CURATED_TYPES | {KNOWLEDGE_TYPE}
430
507
 
431
508
 
432
509
  def retrieve_v1(
@@ -3,12 +3,12 @@
3
3
  Walks `.agent-src/skills/`, `.agent-src/commands/`, `.agent-src/rules/`,
4
4
  `docs/guidelines/`, `.agent-src/contexts/` via the same Python loaders
5
5
  that drive the local stdio kernel, emits one JSON blob and a sidecar
6
- manifest for `workers/mcp/`.
6
+ manifest for `internal/workers/mcp/`.
7
7
 
8
8
  Outputs (relative to repo root):
9
- - `workers/mcp/content.json` — uncompressed, bundled by `wrangler deploy`.
10
- - `workers/mcp/content.json.gz` — gzipped archival copy for R2.
11
- - `workers/mcp/manifest.json` — manifest only (RCA / R2 sidecar).
9
+ - `internal/workers/mcp/content.json` — uncompressed, bundled by `wrangler deploy`.
10
+ - `internal/workers/mcp/content.json.gz` — gzipped archival copy for R2.
11
+ - `internal/workers/mcp/manifest.json` — manifest only (RCA / R2 sidecar).
12
12
 
13
13
  Hard-fail thresholds (Phase 2-5 council verdict D2):
14
14
  - Uncompressed JSON > 2 MB → SystemExit(1).
@@ -262,14 +262,14 @@ def main(argv: list[str] | None = None) -> int:
262
262
  "--out",
263
263
  type=Path,
264
264
  default=None,
265
- help="Output directory (defaults to <root>/workers/mcp).",
265
+ help="Output directory (defaults to <root>/internal/workers/mcp).",
266
266
  )
267
267
  parser.add_argument(
268
268
  "--quiet", action="store_true", help="Suppress success summary."
269
269
  )
270
270
  args = parser.parse_args(argv)
271
271
 
272
- out_dir = args.out or (args.root / "workers" / "mcp")
272
+ out_dir = args.out or (args.root / "internal" / "workers" / "mcp")
273
273
  manifest = pack(args.root, out_dir)
274
274
 
275
275
  if not args.quiet: