@event4u/agent-config 1.34.0 → 1.36.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/memory/load.md +69 -0
- package/.agent-src/commands/memory/mine-session.md +151 -0
- package/.agent-src/commands/memory/promote.md +35 -0
- package/.agent-src/commands/memory/propose.md +10 -1
- package/.agent-src/commands/memory.md +5 -3
- package/.agent-src/commands/roadmap/process-full.md +20 -15
- package/.agent-src/contexts/authority/scope-mechanics.md +36 -0
- package/.agent-src/contexts/execution/autonomy-detection.md +7 -7
- package/.agent-src/contexts/execution/roadmap-process-loop.md +16 -10
- package/.agent-src/personas/discovery-lead.md +99 -0
- package/.agent-src/personas/product-owner.md +71 -52
- package/.agent-src/personas/revops-maintainer.md +100 -0
- package/.agent-src/personas/tech-writer.md +99 -0
- package/.agent-src/rules/autonomous-execution.md +25 -0
- package/.agent-src/rules/scope-control.md +12 -5
- package/.agent-src/skills/competitive-positioning/SKILL.md +152 -0
- package/.agent-src/skills/customer-research/SKILL.md +116 -0
- package/.agent-src/skills/decision-record/SKILL.md +78 -3
- package/.agent-src/skills/discovery-interview/SKILL.md +152 -0
- package/.agent-src/skills/launch-readiness/SKILL.md +156 -0
- package/.agent-src/skills/memory-consolidation/SKILL.md +216 -0
- package/.agent-src/skills/release-comms/SKILL.md +123 -0
- package/.agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/.agent-src/skills/stakeholder-tradeoff/SKILL.md +91 -3
- package/.agent-src/skills/voc-extract/SKILL.md +164 -0
- package/.agent-src/templates/roadmaps.md +14 -0
- package/.claude-plugin/marketplace.json +9 -1
- package/CHANGELOG.md +64 -0
- package/README.md +3 -3
- package/config/agent-settings.template.yml +35 -0
- package/docs/architecture.md +3 -3
- package/docs/catalog.md +14 -5
- package/docs/contracts/agent-memory-contract.md +15 -1
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/context-spine.md +133 -0
- package/docs/contracts/file-ownership-matrix.json +388 -0
- package/docs/contracts/mental-models.md +336 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/engineering-memory-data-format.md +52 -0
- package/docs/guidelines/cross-role-handoff.md +127 -0
- package/package.json +1 -1
- package/scripts/check_memory.py +106 -4
- package/scripts/check_references.py +1 -0
- package/scripts/lint_context_spine_usage.py +133 -0
- package/scripts/lint_roadmap_complexity.py +87 -3
- package/scripts/mine_session.py +279 -0
- package/scripts/schemas/skill.schema.json +9 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Mine session transcripts for memory signals — Phase-1 single-host.
|
|
3
|
+
|
|
4
|
+
Implements the GATHER SIGNAL phase of the `memory-consolidation` skill
|
|
5
|
+
against Claude-Code-format JSONL transcripts. Default behaviour is
|
|
6
|
+
``--preview`` (stdout only). ``--commit-intake`` appends one JSONL line
|
|
7
|
+
per fact to ``agents/memory/intake/<primary-tag>.jsonl`` per the
|
|
8
|
+
agent-memory contract.
|
|
9
|
+
|
|
10
|
+
Strict gates: opt-in transcript access (``--confirm-transcript-access``
|
|
11
|
+
required per invocation), ≤ 5 normalised facts per cycle, redaction
|
|
12
|
+
applied to every yielded text. See
|
|
13
|
+
``.agent-src.uncompressed/commands/memory/mine-session.md`` for the
|
|
14
|
+
authored spec.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import datetime as dt
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Iterable
|
|
28
|
+
|
|
29
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
30
|
+
INTAKE_ROOT = Path("agents/memory/intake")
|
|
31
|
+
DEFAULT_WINDOW_DAYS = 14
|
|
32
|
+
MAX_FACTS = 5
|
|
33
|
+
|
|
34
|
+
SIGNAL_FAMILIES: dict[str, re.Pattern[str]] = {
|
|
35
|
+
# Correction first — explicit redirects beat ambient preference matches.
|
|
36
|
+
"gotcha": re.compile(
|
|
37
|
+
r"(?i)\b(actually|wrong|stop doing|don't do|that's not what|nicht so)\b"),
|
|
38
|
+
# Decision next — narrowest family.
|
|
39
|
+
"invariant": re.compile(
|
|
40
|
+
r"(?i)\b(let's go with|decided|we'll use|entschieden)\b"),
|
|
41
|
+
# Preference last — widest, must not eat correction/decision turns.
|
|
42
|
+
"convention": re.compile(
|
|
43
|
+
r"(?i)\b(prefer|always|never|standard|i want|ich will)\b"),
|
|
44
|
+
}
|
|
45
|
+
PATTERN_MIN_REPEATS = 3
|
|
46
|
+
PATTERN_WINDOW_HOURS = 24
|
|
47
|
+
|
|
48
|
+
NAME_REDACT = re.compile(r"\b(Matze|Mathias)\b")
|
|
49
|
+
PRONOUN_STRIP = re.compile(r"(?i)\b(I|me|my|mein|ich)\b\s*")
|
|
50
|
+
PATH_TOKEN = re.compile(r"\b[a-zA-Z][\w/.-]*/[\w./-]+\b")
|
|
51
|
+
SYMBOL_TOKEN = re.compile(r"\b[A-Z][a-zA-Z0-9]+(?:::|\.)[a-zA-Z_][\w]*\b")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _redact(text: str, extra_patterns: list[re.Pattern[str]]) -> str:
|
|
55
|
+
out = NAME_REDACT.sub("<user>", text)
|
|
56
|
+
for p in extra_patterns:
|
|
57
|
+
out = p.sub("<redacted>", out)
|
|
58
|
+
return out.strip()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _normalise(text: str, extra_patterns: list[re.Pattern[str]]) -> str | None:
|
|
62
|
+
"""Strip pronouns and chrome; require a project-scoped key token."""
|
|
63
|
+
cleaned = _redact(text, extra_patterns)
|
|
64
|
+
cleaned = PRONOUN_STRIP.sub("", cleaned).strip()
|
|
65
|
+
if not (PATH_TOKEN.search(cleaned) or SYMBOL_TOKEN.search(cleaned)):
|
|
66
|
+
return None # user-scoped, drop
|
|
67
|
+
return re.sub(r"\s+", " ", cleaned)[:240]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _key_of(text: str) -> str:
|
|
71
|
+
m = PATH_TOKEN.search(text) or SYMBOL_TOKEN.search(text)
|
|
72
|
+
return m.group(0) if m else "unknown"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _iter_claude_code_jsonl(path: Path) -> Iterable[dict[str, Any]]:
|
|
76
|
+
with path.open(encoding="utf-8") as f:
|
|
77
|
+
for line in f:
|
|
78
|
+
line = line.strip()
|
|
79
|
+
if not line:
|
|
80
|
+
continue
|
|
81
|
+
try:
|
|
82
|
+
yield json.loads(line)
|
|
83
|
+
except json.JSONDecodeError:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _turn_text(turn: dict[str, Any]) -> str:
|
|
88
|
+
msg = turn.get("message") or {}
|
|
89
|
+
content = msg.get("content")
|
|
90
|
+
if isinstance(content, str):
|
|
91
|
+
return content
|
|
92
|
+
if isinstance(content, list):
|
|
93
|
+
return " ".join(c.get("text", "") for c in content
|
|
94
|
+
if isinstance(c, dict) and c.get("type") == "text")
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _turn_ts(turn: dict[str, Any]) -> str:
|
|
99
|
+
return turn.get("timestamp") or turn.get("ts") or ""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _within_window(ts_str: str, since: dt.datetime) -> bool:
|
|
103
|
+
if not ts_str:
|
|
104
|
+
return True
|
|
105
|
+
try:
|
|
106
|
+
ts = dt.datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
107
|
+
except ValueError:
|
|
108
|
+
return True
|
|
109
|
+
if ts.tzinfo is None:
|
|
110
|
+
ts = ts.replace(tzinfo=dt.timezone.utc)
|
|
111
|
+
return ts >= since
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _detect_pattern(turns: list[dict[str, Any]]) -> list[tuple[str, str, str]]:
|
|
115
|
+
"""Return [(key, observation, ts)] for paths/symbols seen ≥ 3× / 24h."""
|
|
116
|
+
seen: dict[str, list[tuple[dt.datetime, str]]] = {}
|
|
117
|
+
for t in turns:
|
|
118
|
+
text = _turn_text(t)
|
|
119
|
+
ts_str = _turn_ts(t)
|
|
120
|
+
try:
|
|
121
|
+
ts = dt.datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
|
122
|
+
except ValueError:
|
|
123
|
+
continue
|
|
124
|
+
if ts.tzinfo is None:
|
|
125
|
+
ts = ts.replace(tzinfo=dt.timezone.utc)
|
|
126
|
+
for m in PATH_TOKEN.findall(text) + SYMBOL_TOKEN.findall(text):
|
|
127
|
+
seen.setdefault(m, []).append((ts, ts_str))
|
|
128
|
+
out: list[tuple[str, str, str]] = []
|
|
129
|
+
window = dt.timedelta(hours=PATTERN_WINDOW_HOURS)
|
|
130
|
+
for key, hits in seen.items():
|
|
131
|
+
hits.sort()
|
|
132
|
+
for i in range(len(hits) - PATTERN_MIN_REPEATS + 1):
|
|
133
|
+
if hits[i + PATTERN_MIN_REPEATS - 1][0] - hits[i][0] <= window:
|
|
134
|
+
out.append((key, f"recurring reference to {key}",
|
|
135
|
+
hits[i + PATTERN_MIN_REPEATS - 1][1]))
|
|
136
|
+
break
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _session_id(transcript: Path) -> str:
|
|
141
|
+
h = hashlib.sha256(str(transcript.resolve()).encode()).hexdigest()
|
|
142
|
+
return h[:16]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def mine(transcript: Path, since: dt.datetime,
|
|
146
|
+
extra_patterns: list[re.Pattern[str]]) -> list[dict[str, Any]]:
|
|
147
|
+
"""Return up to MAX_FACTS normalised facts (preview shape)."""
|
|
148
|
+
turns_in_window = [t for t in _iter_claude_code_jsonl(transcript)
|
|
149
|
+
if _within_window(_turn_ts(t), since)]
|
|
150
|
+
facts: list[dict[str, Any]] = []
|
|
151
|
+
session_id = _session_id(transcript)
|
|
152
|
+
for turn in turns_in_window:
|
|
153
|
+
text = _turn_text(turn)
|
|
154
|
+
if not text:
|
|
155
|
+
continue
|
|
156
|
+
for tag, family in SIGNAL_FAMILIES.items():
|
|
157
|
+
if not family.search(text):
|
|
158
|
+
continue
|
|
159
|
+
obs = _normalise(text, extra_patterns)
|
|
160
|
+
if obs is None:
|
|
161
|
+
continue
|
|
162
|
+
facts.append({
|
|
163
|
+
"ts": _turn_ts(turn) or dt.datetime.now(
|
|
164
|
+
dt.timezone.utc).isoformat(timespec="seconds"),
|
|
165
|
+
"type": tag,
|
|
166
|
+
"key": _key_of(text),
|
|
167
|
+
"observation": obs,
|
|
168
|
+
"source": "agent",
|
|
169
|
+
"session_id": session_id,
|
|
170
|
+
"tags": [tag],
|
|
171
|
+
})
|
|
172
|
+
break
|
|
173
|
+
for key, obs, ts in _detect_pattern(turns_in_window):
|
|
174
|
+
facts.append({
|
|
175
|
+
"ts": ts, "type": "pattern", "key": key,
|
|
176
|
+
"observation": obs, "source": "agent",
|
|
177
|
+
"session_id": session_id, "tags": ["pattern"],
|
|
178
|
+
})
|
|
179
|
+
return facts[:MAX_FACTS]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def render_preview(facts: list[dict[str, Any]],
|
|
183
|
+
project: str, window: str, host: str) -> str:
|
|
184
|
+
if not facts:
|
|
185
|
+
return (f"## Mining preview — {project} · {window} · host={host}\n\n"
|
|
186
|
+
"_No signals matched. Tighten patterns or widen --since._\n")
|
|
187
|
+
lines = [f"## Mining preview — {project} · {window} · host={host}", "",
|
|
188
|
+
"| # | Tag | Key | Observation | Source turn |",
|
|
189
|
+
"|---|---|---|---|---|"]
|
|
190
|
+
for i, f in enumerate(facts, 1):
|
|
191
|
+
lines.append(f"| {i} | {f['type']} | {f['key']} | "
|
|
192
|
+
f"{f['observation']} | {f['ts']} |")
|
|
193
|
+
schemas = sorted({f["type"] for f in facts})
|
|
194
|
+
lines.append("")
|
|
195
|
+
lines.append(f"Schemas touched: {', '.join(schemas)}")
|
|
196
|
+
return "\n".join(lines) + "\n"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def commit_intake(facts: list[dict[str, Any]], intake_root: Path) -> int:
|
|
200
|
+
intake_root.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
written = 0
|
|
202
|
+
for f in facts:
|
|
203
|
+
dest = intake_root / f"{f['type']}.jsonl"
|
|
204
|
+
with dest.open("a", encoding="utf-8") as fh:
|
|
205
|
+
fh.write(json.dumps(f, ensure_ascii=False) + "\n")
|
|
206
|
+
written += 1
|
|
207
|
+
return written
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _resolve_transcript(host: str, override: str | None) -> Path | None:
|
|
211
|
+
if override:
|
|
212
|
+
return Path(override)
|
|
213
|
+
if host != "claude-code":
|
|
214
|
+
return None
|
|
215
|
+
home = Path(os.path.expanduser("~/.claude/projects"))
|
|
216
|
+
if not home.exists():
|
|
217
|
+
return None
|
|
218
|
+
candidates = sorted(home.rglob("*.jsonl"),
|
|
219
|
+
key=lambda p: p.stat().st_mtime, reverse=True)
|
|
220
|
+
return candidates[0] if candidates else None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def main(argv: list[str] | None = None) -> int:
|
|
224
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
225
|
+
ap.add_argument("--since", default=None,
|
|
226
|
+
help="ISO date; default 14 days ago")
|
|
227
|
+
ap.add_argument("--confirm-transcript-access", action="store_true")
|
|
228
|
+
ap.add_argument("--preview", action="store_true", default=True)
|
|
229
|
+
ap.add_argument("--commit-intake", action="store_true")
|
|
230
|
+
ap.add_argument("--host", default="claude-code")
|
|
231
|
+
ap.add_argument("--transcript", default=None,
|
|
232
|
+
help="Override transcript path (testing)")
|
|
233
|
+
ap.add_argument("--intake-root", default=str(INTAKE_ROOT))
|
|
234
|
+
ap.add_argument("--project", default=Path.cwd().name)
|
|
235
|
+
ns = ap.parse_args(argv)
|
|
236
|
+
|
|
237
|
+
if ns.commit_intake and not ns.preview:
|
|
238
|
+
ns.preview = False
|
|
239
|
+
if ns.commit_intake and ns.preview:
|
|
240
|
+
ns.preview = False # commit-intake wins
|
|
241
|
+
|
|
242
|
+
if not ns.confirm_transcript_access:
|
|
243
|
+
print("> Mining reads your session transcript files. Re-run with\n"
|
|
244
|
+
"> --confirm-transcript-access to proceed. The flag is "
|
|
245
|
+
"per-invocation\n> and not persisted.")
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
if ns.host != "claude-code":
|
|
249
|
+
print(f"> No TranscriptAdapter for host={ns.host}. Phase 1 supports: "
|
|
250
|
+
"claude-code.\n> Use /memory propose to record signals "
|
|
251
|
+
"manually.")
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
transcript = _resolve_transcript(ns.host, ns.transcript)
|
|
255
|
+
if transcript is None or not transcript.exists():
|
|
256
|
+
print("> No transcript found for host=claude-code. "
|
|
257
|
+
"Use /memory propose.")
|
|
258
|
+
return 0
|
|
259
|
+
|
|
260
|
+
since = (dt.datetime.fromisoformat(ns.since)
|
|
261
|
+
.replace(tzinfo=dt.timezone.utc) if ns.since
|
|
262
|
+
else dt.datetime.now(dt.timezone.utc)
|
|
263
|
+
- dt.timedelta(days=DEFAULT_WINDOW_DAYS))
|
|
264
|
+
facts = mine(transcript, since, extra_patterns=[])
|
|
265
|
+
window = f"since {since.date().isoformat()}"
|
|
266
|
+
|
|
267
|
+
if not ns.commit_intake:
|
|
268
|
+
print(render_preview(facts, ns.project, window, ns.host), end="")
|
|
269
|
+
return 0
|
|
270
|
+
|
|
271
|
+
written = commit_intake(facts, Path(ns.intake_root))
|
|
272
|
+
files_touched = len({f["type"] for f in facts})
|
|
273
|
+
print(f"✅ Appended {written} intake lines across {files_touched} files.\n"
|
|
274
|
+
" Next: /memory promote to lift validated lines into curated YAML.")
|
|
275
|
+
return 0
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
sys.exit(main())
|
|
@@ -67,6 +67,15 @@
|
|
|
67
67
|
"enum": ["deep"],
|
|
68
68
|
"description": "Optional reasoning-depth marker for AI Council invocations triggered by this skill. The only accepted value is 'deep'; omit the key for default depth (setting 'standard' is rejected — every frontmatter byte counts against the context window, and 'standard' is the implicit default). 'deep' instructs the host agent to pass --depth deep to council_cli, which floors rounds at max(ai_council.deep_min_rounds, ai_council.min_rounds). Use for architecture, refactoring, or bug-diagnosis skills. See .agent-src.uncompressed/skills/ai-council/SKILL.md."
|
|
69
69
|
},
|
|
70
|
+
"context_spine": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"uniqueItems": true,
|
|
73
|
+
"items": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"enum": ["product", "team", "repo"]
|
|
76
|
+
},
|
|
77
|
+
"description": "Senior-skill opt-in for the tri-slot context spine. Declares which slots under agents/context-spine/ the skill expects to read (product, team, repo). Council Q1 (KEEP-3) locks the slot count at 3; additions require ≥ 2 citing skills + ADR per docs/contracts/context-spine.md § 5."
|
|
78
|
+
},
|
|
70
79
|
"execution": {
|
|
71
80
|
"type": "object",
|
|
72
81
|
"additionalProperties": false,
|