@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.
- package/.agent-src/commands/agent-status.md +1 -1
- package/.agent-src/commands/analytics/prune.md +78 -0
- package/.agent-src/commands/analytics/show.md +107 -0
- package/.agent-src/commands/analytics.md +64 -0
- package/.agent-src/commands/knowledge/forget.md +104 -0
- package/.agent-src/commands/knowledge/ingest.md +122 -0
- package/.agent-src/commands/knowledge/list.md +102 -0
- package/.agent-src/commands/knowledge.md +75 -0
- package/.agent-src/scripts/update_roadmap_progress.py +1 -1
- package/.agent-src/skills/compress-memory/SKILL.md +1 -1
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.claude-plugin/marketplace.json +8 -1
- package/AGENTS.md +5 -4
- package/CHANGELOG.md +54 -222
- package/README.md +12 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +164 -10
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +12 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +11 -4
- package/dist/mcp/mcp-cloudflare-catalogue.json +2 -0
- package/dist/mcp/registry-manifest.json +5 -3
- package/docs/architecture.md +1 -1
- package/docs/archive/CHANGELOG-pre-3.2.0.md +268 -0
- package/docs/benchmarks.md +4 -4
- package/docs/catalog.md +9 -2
- package/docs/contracts/CHANGELOG-conventions.md +20 -1
- package/docs/contracts/adr-mcp-runtime.md +1 -1
- package/docs/contracts/at-rest-encryption.md +146 -0
- package/docs/contracts/benchmark-corpus-spec.md +3 -3
- package/docs/contracts/benchmark-report-schema.md +5 -5
- package/docs/contracts/caveman-telemetry.md +4 -4
- package/docs/contracts/compression-default-kill-criterion.md +5 -5
- package/docs/contracts/cost-enforcement.md +1 -1
- package/docs/contracts/daily-workspace.md +137 -0
- package/docs/contracts/explain-modes.md +146 -0
- package/docs/contracts/host-agent-protocol.md +88 -0
- package/docs/contracts/local-analytics.md +148 -0
- package/docs/contracts/local-knowledge-ingestion.md +96 -0
- package/docs/contracts/mcp-beta-criteria.md +1 -1
- package/docs/contracts/mcp-cloud-scope.md +4 -4
- package/docs/contracts/mcp-registry-manifest.schema.json +1 -1
- package/docs/contracts/mcp-tool-inventory.md +1 -1
- package/docs/contracts/mcp-tool-stub-envelope.md +1 -1
- package/docs/contracts/measurement-baseline.md +6 -6
- package/docs/contracts/role-experience.md +121 -0
- package/docs/contracts/workspace-documents.md +140 -0
- package/docs/decisions/ADR-022-daily-workspace-decomposition.md +140 -0
- package/docs/decisions/ADR-023-host-agent-protocol.md +129 -0
- package/docs/decisions/ADR-024-workspace-v0-feature-floor.md +126 -0
- package/docs/decisions/ADR-025-workspace-chrome.md +119 -0
- package/docs/decisions/ADR-026-explain-mode-translation.md +117 -0
- package/docs/decisions/ADR-027-changelog-machine-vs-manual.md +129 -0
- package/docs/decisions/ADR-028-root-layout.md +147 -0
- package/docs/decisions/ADR-029-multi-workspace-deferred.md +122 -0
- package/docs/decisions/INDEX.md +8 -0
- package/docs/deploy/small-team-recipe.md +148 -0
- package/docs/deploy/team-deployment-posture.md +91 -0
- package/docs/getting-started-by-role.md +27 -0
- package/docs/getting-started.md +1 -1
- package/docs/guides/local-analytics.md +125 -0
- package/docs/guides/local-knowledge.md +127 -0
- package/docs/mcp-server.md +1 -1
- package/docs/parity/bench-ruflo.json +3 -3
- package/docs/parity/ruflo.md +1 -1
- package/docs/setup/mcp-client-config.md +1 -1
- package/docs/setup/mcp-cloud-endpoints.md +1 -1
- package/docs/setup/mcp-cloud-setup.md +2 -2
- package/docs/setup/mcp-r2-bootstrap.md +1 -1
- package/package.json +4 -2
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/scripts/_lib/bench_caveman.py +2 -2
- package/scripts/_lib/bench_caveman_report.py +1 -1
- package/scripts/_lib/bench_cost.py +2 -2
- package/scripts/_lib/bench_report.py +2 -2
- package/scripts/_lib/changelog_eras.py +330 -0
- package/scripts/audit_mcp_tools.py +1 -1
- package/scripts/bench_baseline_ready.py +3 -3
- package/scripts/bench_compress_memory.py +4 -4
- package/scripts/bench_drift_check.py +2 -2
- package/scripts/bench_per_tool.py +2 -2
- package/scripts/bench_run.py +4 -4
- package/scripts/build_mcp_registry_manifest.py +2 -2
- package/scripts/mcp_server/__init__.py +1 -1
- package/scripts/mcp_server/catalog.py +1 -1
- package/scripts/mcp_server/consumer_tool_catalog.json +1 -1
- package/scripts/mcp_server/tools.py +1 -1
- package/scripts/memory_lookup.py +78 -1
- package/scripts/pack_mcp_content.py +6 -6
- package/scripts/release.py +93 -3
- 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"
|
package/scripts/bench_run.py
CHANGED
|
@@ -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`
|
|
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
|
package/scripts/memory_lookup.py
CHANGED
|
@@ -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:
|