@event4u/agent-config 1.35.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 +6 -3
- 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 +7 -2
- package/.agent-src/rules/autonomous-execution.md +25 -0
- package/.agent-src/rules/scope-control.md +12 -5
- package/.agent-src/skills/memory-consolidation/SKILL.md +216 -0
- package/.agent-src/templates/roadmaps.md +14 -9
- package/.claude-plugin/marketplace.json +3 -1
- package/CHANGELOG.md +30 -0
- package/README.md +3 -3
- package/config/agent-settings.template.yml +35 -0
- package/docs/architecture.md +2 -2
- package/docs/catalog.md +10 -4
- package/docs/contracts/agent-memory-contract.md +15 -1
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/file-ownership-matrix.json +278 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/engineering-memory-data-format.md +52 -0
- package/package.json +1 -1
- package/scripts/check_memory.py +106 -4
- package/scripts/check_references.py +1 -0
- package/scripts/lint_roadmap_complexity.py +53 -6
- package/scripts/mine_session.py +279 -0
|
@@ -10,7 +10,10 @@ Enforces the measurable subset of
|
|
|
10
10
|
headings, and contain no `## Council Round N` / `### Verdict`
|
|
11
11
|
sections;
|
|
12
12
|
- structural roadmaps have no upper cap, but the tag must be
|
|
13
|
-
declared
|
|
13
|
+
declared;
|
|
14
|
+
- plate / horizon framing is forbidden when
|
|
15
|
+
`roadmap.horizon_weeks` in `.agent-settings.yml` is 0 (default)
|
|
16
|
+
and allowed when it is a positive integer.
|
|
14
17
|
|
|
15
18
|
Cap: ≤ 150 LOC, stdlib only. Hooked into `task ci` via
|
|
16
19
|
`task lint-roadmap-complexity`.
|
|
@@ -27,6 +30,10 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
27
30
|
ROADMAP_GLOB = "agents/roadmaps/*.md"
|
|
28
31
|
LIGHTWEIGHT_LINE_CAP = 600
|
|
29
32
|
LIGHTWEIGHT_PHASE_CAP = 6
|
|
33
|
+
SETTINGS_FILE = REPO_ROOT / ".agent-settings.yml"
|
|
34
|
+
HORIZON_WEEKS_PAT = re.compile(
|
|
35
|
+
r"^\s*horizon_weeks:\s*(\d+)\s*(?:#.*)?$", re.MULTILINE
|
|
36
|
+
)
|
|
30
37
|
|
|
31
38
|
PHASE_PAT = re.compile(r"^## Phase \d+\b", re.MULTILINE)
|
|
32
39
|
COUNCIL_PAT = re.compile(r"^## Council Round \d+\b", re.MULTILINE)
|
|
@@ -66,6 +73,38 @@ def _frontmatter(text: str) -> str:
|
|
|
66
73
|
return text[4:end] if end != -1 else ""
|
|
67
74
|
|
|
68
75
|
|
|
76
|
+
def _read_horizon_weeks() -> int:
|
|
77
|
+
"""Read roadmap.horizon_weeks from .agent-settings.yml.
|
|
78
|
+
|
|
79
|
+
Default 0 (off) when file or key is missing or unparseable.
|
|
80
|
+
Positive integer = horizon framing allowed.
|
|
81
|
+
"""
|
|
82
|
+
if not SETTINGS_FILE.is_file():
|
|
83
|
+
return 0
|
|
84
|
+
try:
|
|
85
|
+
text = SETTINGS_FILE.read_text(encoding="utf-8")
|
|
86
|
+
except OSError:
|
|
87
|
+
return 0
|
|
88
|
+
in_roadmap = False
|
|
89
|
+
for raw in text.splitlines():
|
|
90
|
+
if not raw.strip() or raw.lstrip().startswith("#"):
|
|
91
|
+
continue
|
|
92
|
+
if raw.startswith("roadmap:"):
|
|
93
|
+
in_roadmap = True
|
|
94
|
+
continue
|
|
95
|
+
if in_roadmap and raw and not raw.startswith((" ", "\t")):
|
|
96
|
+
in_roadmap = False
|
|
97
|
+
continue
|
|
98
|
+
if in_roadmap:
|
|
99
|
+
m = HORIZON_WEEKS_PAT.match(raw)
|
|
100
|
+
if m:
|
|
101
|
+
try:
|
|
102
|
+
return max(0, int(m.group(1)))
|
|
103
|
+
except ValueError:
|
|
104
|
+
return 0
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
69
108
|
def _read_complexity(fm: str) -> str | None:
|
|
70
109
|
m = COMPLEXITY_PAT.search(fm)
|
|
71
110
|
return m.group(1) if m else None
|
|
@@ -97,7 +136,11 @@ def _check_lightweight(text: str, line_count: int, problems: list[str]) -> None:
|
|
|
97
136
|
|
|
98
137
|
|
|
99
138
|
def _check_no_plate(text: str, problems: list[str]) -> None:
|
|
100
|
-
"""Detect time-boxed plate / horizon framing
|
|
139
|
+
"""Detect time-boxed plate / horizon framing.
|
|
140
|
+
|
|
141
|
+
Forbidden by template rule 16 when `roadmap.horizon_weeks` is 0
|
|
142
|
+
(default). Allowed when the setting is a positive integer.
|
|
143
|
+
"""
|
|
101
144
|
for pat, label in PLATE_PATS:
|
|
102
145
|
m = pat.search(text)
|
|
103
146
|
if m is None:
|
|
@@ -105,11 +148,13 @@ def _check_no_plate(text: str, problems: list[str]) -> None:
|
|
|
105
148
|
line = text.count("\n", 0, m.start()) + 1
|
|
106
149
|
problems.append(
|
|
107
150
|
f"plate/horizon convention detected ({label}) at line {line} — "
|
|
108
|
-
f"forbidden by templates/roadmaps.md rule 16"
|
|
151
|
+
f"forbidden by templates/roadmaps.md rule 16 when "
|
|
152
|
+
f"`roadmap.horizon_weeks` is 0; set a positive integer in "
|
|
153
|
+
f".agent-settings.yml to opt in"
|
|
109
154
|
)
|
|
110
155
|
|
|
111
156
|
|
|
112
|
-
def lint_roadmap(path: Path) -> list[str]:
|
|
157
|
+
def lint_roadmap(path: Path, horizon_weeks: int) -> list[str]:
|
|
113
158
|
text = path.read_text(encoding="utf-8")
|
|
114
159
|
line_count = text.count("\n") + (1 if text and not text.endswith("\n") else 0)
|
|
115
160
|
problems: list[str] = []
|
|
@@ -123,12 +168,14 @@ def lint_roadmap(path: Path) -> list[str]:
|
|
|
123
168
|
return problems
|
|
124
169
|
if complexity == "lightweight":
|
|
125
170
|
_check_lightweight(text, line_count, problems)
|
|
126
|
-
|
|
171
|
+
if horizon_weeks <= 0:
|
|
172
|
+
_check_no_plate(text, problems)
|
|
127
173
|
return problems
|
|
128
174
|
|
|
129
175
|
|
|
130
176
|
def main() -> int:
|
|
131
177
|
roadmaps = sorted(REPO_ROOT.glob(ROADMAP_GLOB))
|
|
178
|
+
horizon_weeks = _read_horizon_weeks()
|
|
132
179
|
if not roadmaps:
|
|
133
180
|
print(f"❌ no roadmaps matched {ROADMAP_GLOB}", file=sys.stderr)
|
|
134
181
|
return 1
|
|
@@ -136,7 +183,7 @@ def main() -> int:
|
|
|
136
183
|
summary: list[tuple[str, str]] = []
|
|
137
184
|
for roadmap in roadmaps:
|
|
138
185
|
rel = roadmap.relative_to(REPO_ROOT)
|
|
139
|
-
problems = lint_roadmap(roadmap)
|
|
186
|
+
problems = lint_roadmap(roadmap, horizon_weeks)
|
|
140
187
|
text = roadmap.read_text(encoding="utf-8")
|
|
141
188
|
complexity = _read_complexity(_frontmatter(text)) or "untagged"
|
|
142
189
|
summary.append((str(rel), complexity))
|
|
@@ -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())
|