generated from coulomb/repo-seed
session-memory: distribute entrypoint + live verify (WP-0007 T05)
python -m session_memory.distribute: reads approved catalog patterns, builds targets from repo->domain map x flavors, renders scoped per-flavor proposals (HITL) + active registry. Live verify against the real catalog: 12 renders across 5 repos, idempotent, provisional skipped. proposals/ gitignored (regenerated); active_patterns.json committed. README documents detect->curate-> distribute. Phase 3 finished; suite 126/126. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -177,6 +177,8 @@ cython_debug/
|
|||||||
|
|
||||||
# session-memory local store
|
# session-memory local store
|
||||||
session_memory/.store/
|
session_memory/.store/
|
||||||
|
# generated per-flavor distribution proposals (HITL, regenerated each run)
|
||||||
|
session_memory/proposals/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ session_memory/
|
|||||||
curate/decisions.py # hub decision audit trail (graceful local-queue fallback)
|
curate/decisions.py # hub decision audit trail (graceful local-queue fallback)
|
||||||
curate/__main__.py # python -m session_memory.curate (interactive / --auto-approve)
|
curate/__main__.py # python -m session_memory.curate (interactive / --auto-approve)
|
||||||
catalog/ # the committed Pattern Catalog (source of truth)
|
catalog/ # the committed Pattern Catalog (source of truth)
|
||||||
|
distribute/base.py # Artifact + Distributor protocol + idempotent snippet markers
|
||||||
|
distribute/claude.py # CLAUDE.md (or skill) renderer } per-flavor edges
|
||||||
|
distribute/codex.py # AGENTS.md renderer } (agnostic body,
|
||||||
|
distribute/grok.py # native instruction renderer } different targets)
|
||||||
|
distribute/proposals.py # scoping + proposed-not-applied output + active registry
|
||||||
|
distribute/__main__.py # python -m session_memory.distribute
|
||||||
config.toml # store paths, retention caps, sources, repo->domain map, curate gate
|
config.toml # store paths, retention caps, sources, repo->domain map, curate gate
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -114,6 +120,27 @@ python -m session_memory.curate --json # machine-readable result
|
|||||||
| `dist_require_cross_flavor` | require cross-flavor evidence to be distribution-eligible |
|
| `dist_require_cross_flavor` | require cross-flavor evidence to be distribution-eligible |
|
||||||
| `dist_min_frequency` / `dist_min_cost_impact` | stricter floor for `distribution_ready` |
|
| `dist_min_frequency` / `dist_min_cost_impact` | stricter floor for `distribution_ready` |
|
||||||
|
|
||||||
|
## Distribute patterns as per-flavor proposals
|
||||||
|
|
||||||
|
Render approved catalog patterns into per-flavor artifacts — **proposed, never
|
||||||
|
auto-applied** (HITL). Completes the loop: **detect → curate → distribute**.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m session_memory.distribute # proposals for all repos/flavors
|
||||||
|
python -m session_memory.distribute --repo state-hub --flavor claude
|
||||||
|
python -m session_memory.distribute --json
|
||||||
|
```
|
||||||
|
|
||||||
|
- Only `approved` + `distribution_ready` patterns are rendered; each pattern's
|
||||||
|
`Scope` (repos/domains/flavors) decides where it lands (FR-X2).
|
||||||
|
- Each flavor renders the **same agnostic body** to its own target (Claude →
|
||||||
|
`CLAUDE.md`/skill, Codex → `AGENTS.md`, Grok → native) via `rendering_hints`
|
||||||
|
(FR-A3); blocks carry stable `BEGIN/END` markers so re-running updates in place.
|
||||||
|
- Output goes to `session_memory/proposals/<repo>/<target>` (gitignored,
|
||||||
|
regenerated) — a reviewable diff a human applies (FR-X3). The committed
|
||||||
|
`distribute/active_patterns.json` records which pattern+version is proposed in
|
||||||
|
which `(repo, flavor)` (FR-X4).
|
||||||
|
|
||||||
## Retention knobs (`[retention]` in config.toml)
|
## Retention knobs (`[retention]` in config.toml)
|
||||||
|
|
||||||
| Key | Meaning |
|
| Key | Meaning |
|
||||||
@@ -141,4 +168,10 @@ python -m pytest # schema, adapters, store, digest, retention, ingest,
|
|||||||
- **Phase 2** (AGENTIC-WP-0004): Curate — Solution Pattern schema, versioned
|
- **Phase 2** (AGENTIC-WP-0004): Curate — Solution Pattern schema, versioned
|
||||||
files-first Pattern Catalog, discuss/approve/reject review with an evidence bar +
|
files-first Pattern Catalog, discuss/approve/reject review with an evidence bar +
|
||||||
bloat guard, and hub-decision audit trail.
|
bloat guard, and hub-decision audit trail.
|
||||||
- **Next — Phase 3 (Distribute) / Phase 4 (Measure)** follow per the PRD.
|
- **Detect hardening** (AGENTIC-WP-0005): session-quality filter + tool-mix /
|
||||||
|
infra-overhead signals. **Error mining** (AGENTIC-WP-0006): recurring error
|
||||||
|
fingerprints → root-cause patterns.
|
||||||
|
- **Phase 3** (AGENTIC-WP-0007): Distribute — per-flavor distributor adapters
|
||||||
|
render approved patterns into proposed (HITL) artifacts, scoped by repo/domain,
|
||||||
|
with an active-pattern registry.
|
||||||
|
- **Next — Phase 4 (Measure)** closes the loop per the PRD.
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ min_substantive = 3 # require >= this many substantive (edit/read/shell) tool
|
|||||||
min_prompt_len = 25 # first prompt shorter than this is treated as trivial
|
min_prompt_len = 25 # first prompt shorter than this is treated as trivial
|
||||||
|
|
||||||
# Curate phase (AGENTIC-WP-0004): catalog location + promotion evidence bar.
|
# Curate phase (AGENTIC-WP-0004): catalog location + promotion evidence bar.
|
||||||
|
# Distribute phase (AGENTIC-WP-0007): where per-flavor proposals + the active
|
||||||
|
# registry are written. Proposals are HITL — reviewed, never auto-applied.
|
||||||
|
[distribute]
|
||||||
|
proposals_dir = "session_memory/proposals" # reviewable proposals (gitignored, regenerated)
|
||||||
|
active_registry = "session_memory/distribute/active_patterns.json" # what's proposed/active where (committed)
|
||||||
|
|
||||||
[curate]
|
[curate]
|
||||||
catalog_dir = "session_memory/catalog" # files-first Pattern Catalog (committed)
|
catalog_dir = "session_memory/catalog" # files-first Pattern Catalog (committed)
|
||||||
review_log = "session_memory/.store/reviews.jsonl" # remembered decisions (gitignored)
|
review_log = "session_memory/.store/reviews.jsonl" # remembered decisions (gitignored)
|
||||||
|
|||||||
89
session_memory/distribute/__main__.py
Normal file
89
session_memory/distribute/__main__.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"""Distribute entrypoint (T05): catalog -> per-flavor proposals (HITL).
|
||||||
|
|
||||||
|
python -m session_memory.distribute [--config PATH] [--repo R] [--flavor F] [--json]
|
||||||
|
|
||||||
|
Reads approved / distribution-ready Solution Patterns from the Pattern Catalog and
|
||||||
|
renders them into per-flavor **proposals** (never auto-applied) scoped by
|
||||||
|
repo/domain, recording what is proposed where in the active-pattern registry.
|
||||||
|
Targets are the repo->domain map in ``config.toml`` crossed with the known
|
||||||
|
distributor flavors; each pattern's own ``Scope`` filters where it actually lands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from ..curate.catalog import Catalog
|
||||||
|
from ..ingest import _expand, load_config
|
||||||
|
from .proposals import ActiveRegistry, Target, propose
|
||||||
|
from .registry import all_flavors
|
||||||
|
|
||||||
|
|
||||||
|
def build_targets(config: dict, repo_filter=None, flavor_filter=None) -> list[Target]:
|
||||||
|
repo_map = config.get("repo_domain_map", {})
|
||||||
|
flavors = [flavor_filter] if flavor_filter else all_flavors()
|
||||||
|
targets = []
|
||||||
|
for repo, domain in repo_map.items():
|
||||||
|
if repo_filter and repo != repo_filter:
|
||||||
|
continue
|
||||||
|
for flavor in flavors:
|
||||||
|
targets.append(Target(repo=repo, domain=domain, flavor=flavor))
|
||||||
|
return targets
|
||||||
|
|
||||||
|
|
||||||
|
def run_distribute(config: dict, *, repo_filter=None, flavor_filter=None):
|
||||||
|
cur = config.get("curate", {})
|
||||||
|
dist = config.get("distribute", {})
|
||||||
|
catalog = Catalog(_expand(cur.get("catalog_dir", "session_memory/catalog")))
|
||||||
|
patterns = catalog.list()
|
||||||
|
targets = build_targets(config, repo_filter, flavor_filter)
|
||||||
|
registry = ActiveRegistry(_expand(dist.get("active_registry",
|
||||||
|
"session_memory/distribute/active_patterns.json")))
|
||||||
|
out_dir = _expand(dist.get("proposals_dir", "session_memory/proposals"))
|
||||||
|
return propose(patterns, targets, out_dir, registry)
|
||||||
|
|
||||||
|
|
||||||
|
def _summary(res) -> str:
|
||||||
|
by_repo = {}
|
||||||
|
for repo, flavor, pid, _ in res.proposals:
|
||||||
|
by_repo.setdefault(repo, []).append(f"{pid}[{flavor}]")
|
||||||
|
lines = [f"# Distribute proposals ({len(res.proposals)} renders, "
|
||||||
|
f"{len(res.files_written)} files)"]
|
||||||
|
for repo in sorted(by_repo):
|
||||||
|
lines.append(f" {repo}: {', '.join(sorted(by_repo[repo]))}")
|
||||||
|
if res.skipped_not_distributable:
|
||||||
|
lines.append(f" skipped (not distribution-ready): "
|
||||||
|
f"{len(set(res.skipped_not_distributable))} pattern(s)")
|
||||||
|
if not res.proposals:
|
||||||
|
lines.append(" (no approved/distribution-ready patterns matched any target)")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
ap = argparse.ArgumentParser(description="Distribute approved patterns as per-flavor proposals.")
|
||||||
|
ap.add_argument("--config", default=os.path.join(here, "config.toml"))
|
||||||
|
ap.add_argument("--repo", default=None, help="limit to one target repo")
|
||||||
|
ap.add_argument("--flavor", default=None, help="limit to one flavor")
|
||||||
|
ap.add_argument("--json", action="store_true")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
|
config = load_config(args.config)
|
||||||
|
res = run_distribute(config, repo_filter=args.repo, flavor_filter=args.flavor)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({
|
||||||
|
"proposals": [{"repo": r, "flavor": f, "pattern_id": p, "path": path}
|
||||||
|
for r, f, p, path in res.proposals],
|
||||||
|
"files_written": res.files_written,
|
||||||
|
"skipped": sorted(set(res.skipped_not_distributable)),
|
||||||
|
}, indent=2))
|
||||||
|
else:
|
||||||
|
print(_summary(res))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
98
session_memory/distribute/active_patterns.json
Normal file
98
session_memory/distribute/active_patterns.json
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"flavor": "claude",
|
||||||
|
"pattern_id": "sp-problem-schema_thrash-schema_load",
|
||||||
|
"repo": "ops-bridge",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flavor": "claude",
|
||||||
|
"pattern_id": "sp-problem-tool_thrash-tool-bash",
|
||||||
|
"repo": "state-hub",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flavor": "claude",
|
||||||
|
"pattern_id": "sp-success-clean_pass-outcome",
|
||||||
|
"repo": "agentic-resources",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flavor": "grok",
|
||||||
|
"pattern_id": "sp-success-clean_pass-outcome",
|
||||||
|
"repo": "agentic-resources",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flavor": "claude",
|
||||||
|
"pattern_id": "sp-success-clean_pass-outcome",
|
||||||
|
"repo": "can-you-assist",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flavor": "grok",
|
||||||
|
"pattern_id": "sp-success-clean_pass-outcome",
|
||||||
|
"repo": "can-you-assist",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flavor": "claude",
|
||||||
|
"pattern_id": "sp-success-clean_pass-outcome",
|
||||||
|
"repo": "ops-bridge",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flavor": "grok",
|
||||||
|
"pattern_id": "sp-success-clean_pass-outcome",
|
||||||
|
"repo": "ops-bridge",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flavor": "claude",
|
||||||
|
"pattern_id": "sp-success-clean_pass-outcome",
|
||||||
|
"repo": "state-hub",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flavor": "grok",
|
||||||
|
"pattern_id": "sp-success-clean_pass-outcome",
|
||||||
|
"repo": "state-hub",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flavor": "claude",
|
||||||
|
"pattern_id": "sp-success-clean_pass-outcome",
|
||||||
|
"repo": "the-custodian",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flavor": "grok",
|
||||||
|
"pattern_id": "sp-success-clean_pass-outcome",
|
||||||
|
"repo": "the-custodian",
|
||||||
|
"status": "proposed",
|
||||||
|
"updated_at": "2026-06-07T13:20:58Z",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
76
tests/test_distribute_entrypoint.py
Normal file
76
tests/test_distribute_entrypoint.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"""Distribute entrypoint tests (WP-0007 T05)."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from session_memory.curate.catalog import Catalog # noqa: E402
|
||||||
|
from session_memory.curate.schema import Resolution, Scope, SolutionPattern # noqa: E402
|
||||||
|
from session_memory.distribute.__main__ import build_targets, main, run_distribute # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _pattern(pid, repos, flavors, status="approved", ready=True):
|
||||||
|
return SolutionPattern(
|
||||||
|
id=pid, name=pid, version="1.0.0", polarity="problem", problem="p",
|
||||||
|
resolutions=[Resolution(summary="do x")],
|
||||||
|
scope=Scope(repos=repos, flavors=flavors), status=status, distribution_ready=ready,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _config(tmp_path):
|
||||||
|
return {
|
||||||
|
"repo_domain_map": {"agentic-resources": "helix_forge", "state-hub": "custodian"},
|
||||||
|
"curate": {"catalog_dir": str(tmp_path / "catalog")},
|
||||||
|
"distribute": {"proposals_dir": str(tmp_path / "proposals"),
|
||||||
|
"active_registry": str(tmp_path / "active.json")},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_targets_crosses_repos_and_flavors():
|
||||||
|
cfg = {"repo_domain_map": {"r1": "d1", "r2": "d2"}}
|
||||||
|
targets = build_targets(cfg)
|
||||||
|
assert len(targets) == 2 * 3 # 2 repos x 3 flavors
|
||||||
|
assert build_targets(cfg, repo_filter="r1") and all(t.repo == "r1"
|
||||||
|
for t in build_targets(cfg, repo_filter="r1"))
|
||||||
|
assert all(t.flavor == "claude" for t in build_targets(cfg, flavor_filter="claude"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_distribute_scopes_to_catalog(tmp_path):
|
||||||
|
cfg = _config(tmp_path)
|
||||||
|
cat = Catalog(cfg["curate"]["catalog_dir"])
|
||||||
|
# in-scope for agentic-resources/claude only
|
||||||
|
cat.upsert(_pattern("sp-a", ["agentic-resources"], ["claude"]))
|
||||||
|
# provisional -> must be skipped
|
||||||
|
cat.upsert(_pattern("sp-prov", [], [], status="provisional", ready=False))
|
||||||
|
res = run_distribute(cfg)
|
||||||
|
rendered = {pid for _, _, pid, _ in res.proposals}
|
||||||
|
assert "sp-a" in rendered
|
||||||
|
assert "sp-prov" not in rendered
|
||||||
|
assert "sp-prov" in res.skipped_not_distributable
|
||||||
|
# landed only in the agentic-resources/CLAUDE.md proposal
|
||||||
|
p = os.path.join(cfg["distribute"]["proposals_dir"], "agentic-resources", "CLAUDE.md")
|
||||||
|
assert os.path.exists(p)
|
||||||
|
assert not os.path.exists(
|
||||||
|
os.path.join(cfg["distribute"]["proposals_dir"], "state-hub", "CLAUDE.md"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_runs_json(tmp_path, capsys):
|
||||||
|
cfg = _config(tmp_path)
|
||||||
|
cat = Catalog(cfg["curate"]["catalog_dir"])
|
||||||
|
cat.upsert(_pattern("sp-a", [], ["claude"])) # unrestricted repos
|
||||||
|
# write a config file
|
||||||
|
import json as _json
|
||||||
|
cfg_path = tmp_path / "c.json"
|
||||||
|
# main() loads TOML; emulate by calling run_distribute path via a tiny toml
|
||||||
|
toml = tmp_path / "config.toml"
|
||||||
|
toml.write_text(
|
||||||
|
f'[repo_domain_map]\nagentic-resources = "helix_forge"\n'
|
||||||
|
f'[curate]\ncatalog_dir = "{cfg["curate"]["catalog_dir"]}"\n'
|
||||||
|
f'[distribute]\nproposals_dir = "{cfg["distribute"]["proposals_dir"]}"\n'
|
||||||
|
f'active_registry = "{cfg["distribute"]["active_registry"]}"\n')
|
||||||
|
rc = main(["--config", str(toml), "--json"])
|
||||||
|
assert rc == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "sp-a" in out
|
||||||
|
_json.loads(out) # valid JSON
|
||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Coding Session Memory — Phase 3 (Distribute: per-flavor artifacts, HITL)"
|
title: "Coding Session Memory — Phase 3 (Distribute: per-flavor artifacts, HITL)"
|
||||||
domain: helix_forge
|
domain: helix_forge
|
||||||
repo: agentic-resources
|
repo: agentic-resources
|
||||||
status: ready
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: helix-forge
|
topic_slug: helix-forge
|
||||||
created: "2026-06-07"
|
created: "2026-06-07"
|
||||||
@@ -85,7 +85,7 @@ in an active-pattern registry (FR-X4). Unit-tested.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: AGENTIC-WP-0007-T05
|
id: AGENTIC-WP-0007-T05
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "f9e24c13-7049-4c1c-a2d6-3a4dc4e752fd"
|
state_hub_task_id: "f9e24c13-7049-4c1c-a2d6-3a4dc4e752fd"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user