@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.
Files changed (47) hide show
  1. package/.agent-src/commands/memory/load.md +69 -0
  2. package/.agent-src/commands/memory/mine-session.md +151 -0
  3. package/.agent-src/commands/memory/promote.md +35 -0
  4. package/.agent-src/commands/memory/propose.md +10 -1
  5. package/.agent-src/commands/memory.md +5 -3
  6. package/.agent-src/commands/roadmap/process-full.md +20 -15
  7. package/.agent-src/contexts/authority/scope-mechanics.md +36 -0
  8. package/.agent-src/contexts/execution/autonomy-detection.md +7 -7
  9. package/.agent-src/contexts/execution/roadmap-process-loop.md +16 -10
  10. package/.agent-src/personas/discovery-lead.md +99 -0
  11. package/.agent-src/personas/product-owner.md +71 -52
  12. package/.agent-src/personas/revops-maintainer.md +100 -0
  13. package/.agent-src/personas/tech-writer.md +99 -0
  14. package/.agent-src/rules/autonomous-execution.md +25 -0
  15. package/.agent-src/rules/scope-control.md +12 -5
  16. package/.agent-src/skills/competitive-positioning/SKILL.md +152 -0
  17. package/.agent-src/skills/customer-research/SKILL.md +116 -0
  18. package/.agent-src/skills/decision-record/SKILL.md +78 -3
  19. package/.agent-src/skills/discovery-interview/SKILL.md +152 -0
  20. package/.agent-src/skills/launch-readiness/SKILL.md +156 -0
  21. package/.agent-src/skills/memory-consolidation/SKILL.md +216 -0
  22. package/.agent-src/skills/release-comms/SKILL.md +123 -0
  23. package/.agent-src/skills/roadmap-writing/SKILL.md +1 -1
  24. package/.agent-src/skills/stakeholder-tradeoff/SKILL.md +91 -3
  25. package/.agent-src/skills/voc-extract/SKILL.md +164 -0
  26. package/.agent-src/templates/roadmaps.md +14 -0
  27. package/.claude-plugin/marketplace.json +9 -1
  28. package/CHANGELOG.md +64 -0
  29. package/README.md +3 -3
  30. package/config/agent-settings.template.yml +35 -0
  31. package/docs/architecture.md +3 -3
  32. package/docs/catalog.md +14 -5
  33. package/docs/contracts/agent-memory-contract.md +15 -1
  34. package/docs/contracts/command-clusters.md +1 -1
  35. package/docs/contracts/context-spine.md +133 -0
  36. package/docs/contracts/file-ownership-matrix.json +388 -0
  37. package/docs/contracts/mental-models.md +336 -0
  38. package/docs/getting-started.md +1 -1
  39. package/docs/guidelines/agent-infra/engineering-memory-data-format.md +52 -0
  40. package/docs/guidelines/cross-role-handoff.md +127 -0
  41. package/package.json +1 -1
  42. package/scripts/check_memory.py +106 -4
  43. package/scripts/check_references.py +1 -0
  44. package/scripts/lint_context_spine_usage.py +133 -0
  45. package/scripts/lint_roadmap_complexity.py +87 -3
  46. package/scripts/mine_session.py +279 -0
  47. 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,