@event4u/agent-config 1.12.0 → 1.13.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/rules/user-interaction.md +53 -7
- package/.agent-src/scripts/update_roadmap_progress.py +31 -1
- package/.agent-src/templates/github-workflows/roadmap-progress-check.yml +63 -0
- package/.agent-src/templates/hooks/pre-commit-roadmap-progress +60 -0
- package/.agent-src/templates/scripts/memory_lookup.py +382 -21
- package/.agent-src/templates/scripts/memory_status.py +110 -9
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +21 -0
- package/composer.json +3 -0
- package/docs/getting-started.md +1 -0
- package/package.json +9 -1
- package/scripts/agent-config +75 -0
- package/scripts/memory_lookup.py +143 -7
- package/scripts/memory_status.py +76 -14
- package/scripts/postinstall.sh +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ versioning policy is documented in [CONTRIBUTING.md](CONTRIBUTING.md#versioning-
|
|
|
7
7
|
> Entries before 1.3.3 were reconstructed from git history after the fact.
|
|
8
8
|
> Early releases did not maintain release notes.
|
|
9
9
|
|
|
10
|
+
## [1.13.0](https://github.com/event4u-app/agent-config/compare/1.12.0...1.13.0) (2026-04-27)
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **postinstall:** hint about optional @event4u/agent-memory backend ([395cff1](https://github.com/event4u-app/agent-config/commit/395cff164770da4a18d4287effd9ce06b2cee8b9))
|
|
15
|
+
* **npm:** declare @event4u/agent-memory as optional peer dependency ([cef7715](https://github.com/event4u-app/agent-config/commit/cef77159d2d7cd0ba29c78c9c2115f1d08f0e649))
|
|
16
|
+
* **composer:** suggest @event4u/agent-memory as optional memory backend ([6585c32](https://github.com/event4u-app/agent-config/commit/6585c324fcc65ad08f1d50f0e54a7f56b2018d03))
|
|
17
|
+
* **scripts:** fail check mode on unarchived complete roadmaps ([f017979](https://github.com/event4u-app/agent-config/commit/f0179792a9b15588182815a17e4ac7366dad1db0))
|
|
18
|
+
* **scripts:** add hooks:install and pre-commit roadmap-progress hook ([cab9048](https://github.com/event4u-app/agent-config/commit/cab90482ad2bf70fa08f9494236eb19b72e5d58b))
|
|
19
|
+
* **templates:** ship roadmap-progress-check GitHub Actions workflow ([a16c560](https://github.com/event4u-app/agent-config/commit/a16c560d57f3cefd0b99aeaadd0946c3a8865866))
|
|
20
|
+
* **memory:** real backend health envelope ([145bd13](https://github.com/event4u-app/agent-config/commit/145bd13ec6027d48a90cdacc3622ef9cca7d8c05))
|
|
21
|
+
* **memory:** package-backed operational provider (Drift #2) ([284be4c](https://github.com/event4u-app/agent-config/commit/284be4c4addca37490b727a2aec9d45c1fa9b274))
|
|
22
|
+
* **rules:** require recommendations on every numbered-option question ([ed9f5c9](https://github.com/event4u-app/agent-config/commit/ed9f5c9271c486a920fed3fbbea10fc16e75f685))
|
|
23
|
+
* **memory:** wire agent-memory MCP server + recognize 'memory' binary ([e24168b](https://github.com/event4u-app/agent-config/commit/e24168b12bd8f5711ec02f6511c3afa952e595a8))
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
|
|
27
|
+
* **readme:** document @event4u/agent-memory as optional companion ([350930f](https://github.com/event4u-app/agent-config/commit/350930fcee3134275bbed26a6783d54837eba568))
|
|
28
|
+
* **agent-memory:** align contract with reality — CLI surface + drift status ([6cdf19e](https://github.com/event4u-app/agent-config/commit/6cdf19ee256b52aa7602419fde730477d2a904de))
|
|
29
|
+
|
|
30
|
+
### CI
|
|
31
|
+
|
|
32
|
+
* wire roadmap-progress-check into task ci ([2022396](https://github.com/event4u-app/agent-config/commit/20223964f2c391598efca5b9e76fd5ca1365f05e))
|
|
33
|
+
|
|
10
34
|
## [1.12.0](https://github.com/event4u-app/agent-config/compare/1.10.0...1.12.0) (2026-04-25)
|
|
11
35
|
|
|
12
36
|
### Features
|
package/README.md
CHANGED
|
@@ -71,6 +71,27 @@ Install directly in your agent for global, cross-project use:
|
|
|
71
71
|
|
|
72
72
|
→ [Full getting started guide](docs/getting-started.md)
|
|
73
73
|
|
|
74
|
+
### Optional: persistent agent memory
|
|
75
|
+
|
|
76
|
+
`agent-config` integrates with [`@event4u/agent-memory`](https://www.npmjs.com/package/@event4u/agent-memory)
|
|
77
|
+
— an MCP-based memory backend that gives agents persistent learnings
|
|
78
|
+
across sessions. It is **strictly optional**:
|
|
79
|
+
|
|
80
|
+
- Not a required dependency (declared as `suggest` in Composer and as an
|
|
81
|
+
optional peer in npm). `agent-config` itself never imports it.
|
|
82
|
+
- Without it, agent skills fall back to **file-based memory** under
|
|
83
|
+
`agents/memory/` and continue to work normally.
|
|
84
|
+
- Recommended for teams that want learnings to survive across machines,
|
|
85
|
+
branches, and chat sessions.
|
|
86
|
+
|
|
87
|
+
Install in the same project (dev-only):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm install --save-dev @event4u/agent-memory
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
→ [Memory contract & retrieval API](agents/contexts/agent-memory-contract.md)
|
|
94
|
+
|
|
74
95
|
---
|
|
75
96
|
|
|
76
97
|
## 2-minute demo: `/implement-ticket`
|
package/composer.json
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
"require": {
|
|
7
7
|
"php": ">=8.0"
|
|
8
8
|
},
|
|
9
|
+
"suggest": {
|
|
10
|
+
"@event4u/agent-memory": "Optional MCP-based memory backend (npm: @event4u/agent-memory ^1.1.0). Adds persistent agent learnings across sessions. Install with `npm install --save-dev @event4u/agent-memory` if your team wants the memory layer; otherwise agent-config falls back to file-based memory."
|
|
11
|
+
},
|
|
9
12
|
"bin": [
|
|
10
13
|
"bin/install.php",
|
|
11
14
|
"scripts/agent-config"
|
package/docs/getting-started.md
CHANGED
|
@@ -28,6 +28,7 @@ so you can run a few package scripts without installing `go-task`,
|
|
|
28
28
|
```bash
|
|
29
29
|
./agent-config mcp:render # sync MCP server config into .cursor/ and .windsurf/
|
|
30
30
|
./agent-config roadmap:progress # regenerate agents/roadmaps-progress.md
|
|
31
|
+
./agent-config hooks:install # install pre-commit roadmap-progress hook (opt-in)
|
|
31
32
|
./agent-config first-run # guided setup
|
|
32
33
|
./agent-config help # full command list
|
|
33
34
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "Shared agent configuration \u2014 skills, rules, commands, guidelines, and templates for AI coding tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -38,5 +38,13 @@
|
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public",
|
|
40
40
|
"provenance": true
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@event4u/agent-memory": "^1.1.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"@event4u/agent-memory": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
}
|
package/scripts/agent-config
CHANGED
|
@@ -46,6 +46,8 @@ Commands:
|
|
|
46
46
|
mcp:check Dry-run mcp:render; exit non-zero if targets are stale
|
|
47
47
|
roadmap:progress Regenerate agents/roadmaps-progress.md from open roadmaps
|
|
48
48
|
roadmap:progress-check Fail if agents/roadmaps-progress.md is stale (for CI)
|
|
49
|
+
hooks:install Install the pre-commit roadmap-progress hook
|
|
50
|
+
(use --print to dump it, --force to overwrite an existing hook)
|
|
49
51
|
first-run Guided first-run setup — cost profile, settings, tooling
|
|
50
52
|
help Show this help
|
|
51
53
|
--version, -V Print package version
|
|
@@ -55,6 +57,7 @@ Examples:
|
|
|
55
57
|
./agent-config mcp:render --claude-desktop
|
|
56
58
|
./agent-config mcp:check
|
|
57
59
|
./agent-config roadmap:progress
|
|
60
|
+
./agent-config hooks:install
|
|
58
61
|
./agent-config first-run
|
|
59
62
|
|
|
60
63
|
All commands operate on the CURRENT DIRECTORY (your project root).
|
|
@@ -132,6 +135,77 @@ cmd_first_run() {
|
|
|
132
135
|
exec bash "$script" "$@"
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
cmd_hooks_install() {
|
|
139
|
+
local force=false
|
|
140
|
+
local print_only=false
|
|
141
|
+
for arg in "$@"; do
|
|
142
|
+
case "$arg" in
|
|
143
|
+
--force) force=true ;;
|
|
144
|
+
--print) print_only=true ;;
|
|
145
|
+
-h|--help)
|
|
146
|
+
cat <<'HELP'
|
|
147
|
+
agent-config hooks:install — install the pre-commit roadmap-progress hook.
|
|
148
|
+
|
|
149
|
+
Usage:
|
|
150
|
+
./agent-config hooks:install [--force] [--print]
|
|
151
|
+
|
|
152
|
+
Without flags: copies the hook to .git/hooks/pre-commit. Refuses to
|
|
153
|
+
overwrite an existing pre-commit hook unless --force is given (the
|
|
154
|
+
existing hook may already chain other tooling).
|
|
155
|
+
|
|
156
|
+
--print dump the hook script to stdout (for manual chaining into an
|
|
157
|
+
existing pre-commit script, husky, lefthook, etc.)
|
|
158
|
+
--force overwrite an existing .git/hooks/pre-commit (DESTRUCTIVE)
|
|
159
|
+
HELP
|
|
160
|
+
return 0 ;;
|
|
161
|
+
*)
|
|
162
|
+
echo "❌ hooks:install: unknown argument: $arg" >&2
|
|
163
|
+
echo " Run \`./agent-config hooks:install --help\` for usage." >&2
|
|
164
|
+
return 2 ;;
|
|
165
|
+
esac
|
|
166
|
+
done
|
|
167
|
+
|
|
168
|
+
local hook_src
|
|
169
|
+
hook_src="$(resolve_script ".agent-src/templates/hooks/pre-commit-roadmap-progress" ".augment/templates/hooks/pre-commit-roadmap-progress")" || return 1
|
|
170
|
+
|
|
171
|
+
if $print_only; then
|
|
172
|
+
cat "$hook_src"
|
|
173
|
+
return 0
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
local git_dir
|
|
177
|
+
git_dir="$(git -C "$CONSUMER_ROOT" rev-parse --git-dir 2>/dev/null || true)"
|
|
178
|
+
if [[ -z "$git_dir" ]]; then
|
|
179
|
+
echo "❌ hooks:install: $CONSUMER_ROOT is not a git repository." >&2
|
|
180
|
+
return 1
|
|
181
|
+
fi
|
|
182
|
+
# Resolve relative git-dir paths (worktrees, submodules) against CONSUMER_ROOT.
|
|
183
|
+
[[ "$git_dir" != /* ]] && git_dir="$CONSUMER_ROOT/$git_dir"
|
|
184
|
+
|
|
185
|
+
local hook_dir="$git_dir/hooks"
|
|
186
|
+
local target="$hook_dir/pre-commit"
|
|
187
|
+
mkdir -p "$hook_dir"
|
|
188
|
+
|
|
189
|
+
if [[ -f "$target" ]] && ! $force; then
|
|
190
|
+
if grep -q "pre-commit-roadmap-progress" "$target" 2>/dev/null; then
|
|
191
|
+
echo "✅ hooks:install: already installed at $target"
|
|
192
|
+
return 0
|
|
193
|
+
fi
|
|
194
|
+
echo "⚠️ hooks:install: $target already exists and looks unrelated." >&2
|
|
195
|
+
echo " Options:" >&2
|
|
196
|
+
echo " 1. Inspect it and append the snippet manually:" >&2
|
|
197
|
+
echo " ./agent-config hooks:install --print >> $target" >&2
|
|
198
|
+
echo " 2. Replace it (destructive):" >&2
|
|
199
|
+
echo " ./agent-config hooks:install --force" >&2
|
|
200
|
+
return 1
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
cp "$hook_src" "$target"
|
|
204
|
+
chmod +x "$target"
|
|
205
|
+
echo "✅ hooks:install: pre-commit hook installed at $target"
|
|
206
|
+
echo " To uninstall: rm $target"
|
|
207
|
+
}
|
|
208
|
+
|
|
135
209
|
main() {
|
|
136
210
|
local cmd="${1-}"
|
|
137
211
|
[[ $# -gt 0 ]] && shift || true
|
|
@@ -141,6 +215,7 @@ main() {
|
|
|
141
215
|
mcp:check) cmd_mcp_check "$@" ;;
|
|
142
216
|
roadmap:progress) cmd_roadmap_progress "$@" ;;
|
|
143
217
|
roadmap:progress-check) cmd_roadmap_progress_check "$@" ;;
|
|
218
|
+
hooks:install) cmd_hooks_install "$@" ;;
|
|
144
219
|
first-run) cmd_first_run "$@" ;;
|
|
145
220
|
help|--help|-h|"") usage ;;
|
|
146
221
|
--version|-V) print_version ;;
|
package/scripts/memory_lookup.py
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Hybrid retrieval — file-first with optional package augmentation.
|
|
3
3
|
|
|
4
4
|
Implements the shared `retrieve(types, keys, limit)` abstraction used
|
|
5
5
|
by skills. Reads YAML under `agents/memory/<type>/` (curated, hand-
|
|
6
6
|
reviewed) and JSONL under `agents/memory/intake/*.jsonl` (agent-written,
|
|
7
7
|
append-only, supersede-chain aware).
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
When the `@event4u/agent-memory` package is present (see
|
|
10
|
+
`scripts/memory_status.py`), callers can pass the result of
|
|
11
|
+
:func:`package_operational_provider` to route additional retrieval
|
|
12
|
+
through the package's semantic CLI. Repo entries always win on
|
|
13
|
+
conflict — see `_apply_conflict_rule`.
|
|
11
14
|
|
|
12
15
|
Usage:
|
|
13
16
|
python3 scripts/memory_lookup.py --types domain-invariants,ownership \\
|
|
14
17
|
--key "app/Http/Controllers/Foo" --limit 5
|
|
15
18
|
python3 scripts/memory_lookup.py --types incident-learnings --format json
|
|
19
|
+
python3 scripts/memory_lookup.py --types ownership --key billing --auto
|
|
16
20
|
|
|
17
|
-
from scripts.memory_lookup import retrieve
|
|
18
|
-
hits = retrieve(
|
|
21
|
+
from scripts.memory_lookup import retrieve, package_operational_provider
|
|
22
|
+
hits = retrieve(
|
|
23
|
+
types=["ownership"], keys=["app/Http"], limit=3,
|
|
24
|
+
operational_provider=package_operational_provider(),
|
|
25
|
+
)
|
|
19
26
|
"""
|
|
20
27
|
|
|
21
28
|
from __future__ import annotations
|
|
@@ -23,6 +30,8 @@ from __future__ import annotations
|
|
|
23
30
|
import argparse
|
|
24
31
|
import fnmatch
|
|
25
32
|
import json
|
|
33
|
+
import os
|
|
34
|
+
import subprocess
|
|
26
35
|
import sys
|
|
27
36
|
from dataclasses import dataclass, asdict, field
|
|
28
37
|
from pathlib import Path
|
|
@@ -229,6 +238,125 @@ def _apply_conflict_rule(
|
|
|
229
238
|
return merged, shadows
|
|
230
239
|
|
|
231
240
|
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Package-backed operational provider (the `present` path)
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
#
|
|
245
|
+
# When `memory_status.status() == "present"` the consumer-facing contract
|
|
246
|
+
# says retrieval should route through `@event4u/agent-memory`. The package
|
|
247
|
+
# CLI is purely **semantic** (`memory retrieve <query> --type T …`); the
|
|
248
|
+
# shared `retrieve(types, keys, …)` API is **key-based**. The hybrid
|
|
249
|
+
# resolution agreed in `agents/contexts/agent-memory-contract.md` synthesises
|
|
250
|
+
# `keys` into a single natural-language query for the package call, while
|
|
251
|
+
# the file fallback continues to do glob/substring matching on the same
|
|
252
|
+
# keys. Both legs land in the same `Hit` shape so the conflict rule can
|
|
253
|
+
# merge them transparently.
|
|
254
|
+
|
|
255
|
+
_CLI_TIMEOUT_SECONDS = 5.0
|
|
256
|
+
_CLI_RETRIEVE_LIMIT_DEFAULT = 20
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _synthesize_query(keys: list[str]) -> str:
|
|
260
|
+
"""Turn a list of retrieval keys into one natural-language query.
|
|
261
|
+
|
|
262
|
+
Keys are typically file paths (`app/Http/Controllers/Foo`), feature
|
|
263
|
+
names (`billing`), or short identifiers — joining them with spaces
|
|
264
|
+
gives the package's semantic search enough surface to score against
|
|
265
|
+
without inventing structure. Empty or whitespace-only keys are
|
|
266
|
+
dropped; if nothing remains the caller falls back to the file path.
|
|
267
|
+
"""
|
|
268
|
+
cleaned = [k.strip() for k in keys if isinstance(k, str) and k.strip()]
|
|
269
|
+
return " ".join(cleaned)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _cli_operational_provider(
|
|
273
|
+
types: list[str],
|
|
274
|
+
keys: list[str],
|
|
275
|
+
*,
|
|
276
|
+
cli_path: str = "memory",
|
|
277
|
+
timeout: float = _CLI_TIMEOUT_SECONDS,
|
|
278
|
+
limit: int = _CLI_RETRIEVE_LIMIT_DEFAULT,
|
|
279
|
+
) -> Iterable[Hit]:
|
|
280
|
+
"""Run `memory retrieve` and yield operational `Hit` objects.
|
|
281
|
+
|
|
282
|
+
Pino structured logs from the package go to stderr; stdout is a
|
|
283
|
+
clean v1 retrieval envelope. Any non-zero exit, timeout, or parse
|
|
284
|
+
failure degrades to "no operational hits" — `retrieve()` already
|
|
285
|
+
treats provider exceptions as a soft warning, so the caller still
|
|
286
|
+
gets the file-fallback result.
|
|
287
|
+
"""
|
|
288
|
+
query = _synthesize_query(keys)
|
|
289
|
+
if not query:
|
|
290
|
+
return
|
|
291
|
+
cmd: list[str] = [cli_path, "retrieve", query, "--limit", str(limit)]
|
|
292
|
+
for t in types:
|
|
293
|
+
cmd.extend(["--type", t])
|
|
294
|
+
try:
|
|
295
|
+
out = subprocess.run(
|
|
296
|
+
cmd,
|
|
297
|
+
capture_output=True, text=True, timeout=timeout,
|
|
298
|
+
)
|
|
299
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
300
|
+
return
|
|
301
|
+
if out.returncode != 0:
|
|
302
|
+
return
|
|
303
|
+
try:
|
|
304
|
+
envelope = json.loads(out.stdout)
|
|
305
|
+
except (ValueError, TypeError):
|
|
306
|
+
return
|
|
307
|
+
entries = envelope.get("entries") if isinstance(envelope, dict) else None
|
|
308
|
+
if not isinstance(entries, list):
|
|
309
|
+
return
|
|
310
|
+
for e in entries:
|
|
311
|
+
if not isinstance(e, dict):
|
|
312
|
+
continue
|
|
313
|
+
eid = e.get("id")
|
|
314
|
+
etype = e.get("type")
|
|
315
|
+
if not isinstance(eid, str) or not isinstance(etype, str):
|
|
316
|
+
continue
|
|
317
|
+
# The package returns `confidence` (0..1) per the v1 envelope;
|
|
318
|
+
# map it onto our internal `score` field so the conflict rule
|
|
319
|
+
# and ranking work uniformly across providers.
|
|
320
|
+
try:
|
|
321
|
+
score = float(e.get("confidence", 0.0))
|
|
322
|
+
except (TypeError, ValueError):
|
|
323
|
+
score = 0.0
|
|
324
|
+
body = e.get("body") if isinstance(e.get("body"), dict) else {}
|
|
325
|
+
yield Hit(
|
|
326
|
+
id=eid,
|
|
327
|
+
type=etype,
|
|
328
|
+
source="operational",
|
|
329
|
+
path=f"agent-memory:{eid}",
|
|
330
|
+
score=score,
|
|
331
|
+
entry=body,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def package_operational_provider() -> Optional[OperationalProvider]:
|
|
336
|
+
"""Return a CLI-backed provider when the package is `present`, else None.
|
|
337
|
+
|
|
338
|
+
Callers who want automatic backend routing pass the result directly
|
|
339
|
+
to :func:`retrieve` — `None` is a safe value that yields file-only
|
|
340
|
+
retrieval, so this is the recommended one-liner for skills:
|
|
341
|
+
|
|
342
|
+
retrieve(types, keys, operational_provider=package_operational_provider())
|
|
343
|
+
|
|
344
|
+
The status probe is bounded (≤ 2s, cached per process) — see
|
|
345
|
+
`scripts/memory_status.py`. We import lazily so pure file-fallback
|
|
346
|
+
callers never pay for the probe.
|
|
347
|
+
"""
|
|
348
|
+
# Late import: keeps `memory_lookup` importable even when
|
|
349
|
+
# `memory_status` is missing in stripped consumer installs.
|
|
350
|
+
try:
|
|
351
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
352
|
+
import memory_status # type: ignore[import-not-found]
|
|
353
|
+
except ImportError:
|
|
354
|
+
return None
|
|
355
|
+
if memory_status.status().status != "present":
|
|
356
|
+
return None
|
|
357
|
+
return _cli_operational_provider
|
|
358
|
+
|
|
359
|
+
|
|
232
360
|
def retrieve(
|
|
233
361
|
types: list[str],
|
|
234
362
|
keys: list[str],
|
|
@@ -402,16 +530,24 @@ def main() -> int:
|
|
|
402
530
|
ap.add_argument("--with-shadows", action="store_true",
|
|
403
531
|
help="Include shadowed-operational entries in the output "
|
|
404
532
|
"(no-op until an operational backend is wired)")
|
|
533
|
+
ap.add_argument("--auto", action="store_true",
|
|
534
|
+
help="Auto-route to the @event4u/agent-memory package "
|
|
535
|
+
"when memory_status.status() == 'present'; "
|
|
536
|
+
"falls through to file-only retrieval otherwise")
|
|
405
537
|
args = ap.parse_args()
|
|
406
538
|
types = [t.strip() for t in args.types.split(",") if t.strip()]
|
|
407
539
|
if not types:
|
|
408
540
|
print("error: --types is required", file=sys.stderr)
|
|
409
541
|
return 2
|
|
542
|
+
op_provider = package_operational_provider() if args.auto else None
|
|
410
543
|
if args.envelope == "v1":
|
|
411
|
-
envelope = retrieve_v1(types, args.key, args.limit
|
|
544
|
+
envelope = retrieve_v1(types, args.key, args.limit,
|
|
545
|
+
operational_provider=op_provider)
|
|
412
546
|
print(json.dumps(envelope, indent=2, default=str))
|
|
413
547
|
return 0
|
|
414
|
-
result = retrieve(types, args.key, args.limit,
|
|
548
|
+
result = retrieve(types, args.key, args.limit,
|
|
549
|
+
operational_provider=op_provider,
|
|
550
|
+
with_shadows=args.with_shadows)
|
|
415
551
|
if args.with_shadows:
|
|
416
552
|
assert isinstance(result, RetrievalResult)
|
|
417
553
|
hits, shadows = result.hits, result.shadows
|
package/scripts/memory_status.py
CHANGED
|
@@ -35,7 +35,7 @@ from typing import Literal
|
|
|
35
35
|
|
|
36
36
|
Status = Literal["absent", "misconfigured", "present"]
|
|
37
37
|
|
|
38
|
-
_CLI_CANDIDATES = ("agent-memory", "agentmem")
|
|
38
|
+
_CLI_CANDIDATES = ("memory", "agent-memory", "agentmem")
|
|
39
39
|
_HEALTH_TIMEOUT_SECONDS = 2.0
|
|
40
40
|
_CACHE_ENV = "AGENT_MEMORY_STATUS"
|
|
41
41
|
_CACHE_FILE = Path(".agent-memory") / "status.cache"
|
|
@@ -54,6 +54,11 @@ class Result:
|
|
|
54
54
|
reason: str # short explanation
|
|
55
55
|
elapsed_ms: int # time spent probing (0 if cached)
|
|
56
56
|
cli_path: str = "" # resolved CLI path, if any
|
|
57
|
+
# Populated only when status == "present" — sourced from the
|
|
58
|
+
# `health` CLI envelope so the v1 health() reports real package
|
|
59
|
+
# capabilities instead of file-fallback placeholders.
|
|
60
|
+
backend_version: str = ""
|
|
61
|
+
features: tuple = ()
|
|
57
62
|
|
|
58
63
|
|
|
59
64
|
def _find_cli() -> str:
|
|
@@ -64,8 +69,45 @@ def _find_cli() -> str:
|
|
|
64
69
|
return ""
|
|
65
70
|
|
|
66
71
|
|
|
67
|
-
def
|
|
68
|
-
"""
|
|
72
|
+
def _parse_health_envelope(stdout: str) -> dict | None:
|
|
73
|
+
"""Extract the v1 health envelope from `memory health` stdout.
|
|
74
|
+
|
|
75
|
+
The package emits a single JSON object on stdout (pino structured
|
|
76
|
+
logs go to stderr). We tolerate older builds that may have leaked
|
|
77
|
+
log lines into stdout by scanning for the first top-level object
|
|
78
|
+
that carries ``contract_version``.
|
|
79
|
+
"""
|
|
80
|
+
text = (stdout or "").strip()
|
|
81
|
+
if not text:
|
|
82
|
+
return None
|
|
83
|
+
try:
|
|
84
|
+
obj = json.loads(text)
|
|
85
|
+
except ValueError:
|
|
86
|
+
obj = None
|
|
87
|
+
if isinstance(obj, dict) and obj.get("contract_version"):
|
|
88
|
+
return obj
|
|
89
|
+
# Fallback: line-by-line scan for an envelope-shaped object — covers
|
|
90
|
+
# the case where structured logs accidentally share stdout.
|
|
91
|
+
for line in text.splitlines():
|
|
92
|
+
line = line.strip()
|
|
93
|
+
if not line.startswith("{"):
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
cand = json.loads(line)
|
|
97
|
+
except ValueError:
|
|
98
|
+
continue
|
|
99
|
+
if isinstance(cand, dict) and cand.get("contract_version"):
|
|
100
|
+
return cand
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _probe_health(cli_path: str) -> tuple[bool, str, dict | None]:
|
|
105
|
+
"""Returns (healthy, reason, envelope).
|
|
106
|
+
|
|
107
|
+
On success ``envelope`` is the parsed v1 health envelope (may still
|
|
108
|
+
be ``None`` for very old CLIs that don't emit one). On failure it
|
|
109
|
+
is always ``None``.
|
|
110
|
+
"""
|
|
69
111
|
try:
|
|
70
112
|
out = subprocess.run(
|
|
71
113
|
[cli_path, "health"],
|
|
@@ -73,15 +115,16 @@ def _probe_health(cli_path: str) -> tuple[bool, str]:
|
|
|
73
115
|
timeout=_HEALTH_TIMEOUT_SECONDS,
|
|
74
116
|
)
|
|
75
117
|
except subprocess.TimeoutExpired:
|
|
76
|
-
return False, f"health() timed out after {_HEALTH_TIMEOUT_SECONDS}s"
|
|
118
|
+
return False, f"health() timed out after {_HEALTH_TIMEOUT_SECONDS}s", None
|
|
77
119
|
except FileNotFoundError:
|
|
78
|
-
return False, "CLI vanished between which() and invoke"
|
|
120
|
+
return False, "CLI vanished between which() and invoke", None
|
|
79
121
|
if out.returncode != 0:
|
|
80
122
|
# First line of combined output, capped, for the reason field.
|
|
81
123
|
msg = (out.stderr or out.stdout or "exit != 0").strip().splitlines()
|
|
82
124
|
head = msg[0][:120] if msg else "exit != 0"
|
|
83
|
-
return False, f"health() returned {out.returncode}: {head}"
|
|
84
|
-
|
|
125
|
+
return False, f"health() returned {out.returncode}: {head}", None
|
|
126
|
+
envelope = _parse_health_envelope(out.stdout)
|
|
127
|
+
return True, "ok", envelope
|
|
85
128
|
|
|
86
129
|
|
|
87
130
|
def _read_cache() -> Result | None:
|
|
@@ -131,10 +174,23 @@ def status(refresh: bool = False) -> Result:
|
|
|
131
174
|
result = Result("absent", "file",
|
|
132
175
|
"agent-memory CLI not on PATH", 0)
|
|
133
176
|
else:
|
|
134
|
-
healthy, reason = _probe_health(cli)
|
|
177
|
+
healthy, reason, envelope = _probe_health(cli)
|
|
135
178
|
elapsed = int((time.monotonic() - t0) * 1000)
|
|
136
179
|
if healthy:
|
|
137
|
-
|
|
180
|
+
backend_version = ""
|
|
181
|
+
features: tuple = ()
|
|
182
|
+
if isinstance(envelope, dict):
|
|
183
|
+
bv = envelope.get("backend_version")
|
|
184
|
+
if isinstance(bv, str):
|
|
185
|
+
backend_version = bv
|
|
186
|
+
feats = envelope.get("features")
|
|
187
|
+
if isinstance(feats, list) and all(
|
|
188
|
+
isinstance(f, str) for f in feats
|
|
189
|
+
):
|
|
190
|
+
features = tuple(feats)
|
|
191
|
+
result = Result("present", "package", reason, elapsed, cli,
|
|
192
|
+
backend_version=backend_version,
|
|
193
|
+
features=features)
|
|
138
194
|
else:
|
|
139
195
|
result = Result("misconfigured", "file", reason, elapsed, cli)
|
|
140
196
|
_write_cache(result)
|
|
@@ -148,6 +204,11 @@ def health(refresh: bool = False) -> dict:
|
|
|
148
204
|
Maps the three-state :func:`status` result onto the contract's
|
|
149
205
|
``ok | degraded | error`` so consumers can read
|
|
150
206
|
``contract_version`` without caring about the file-vs-package split.
|
|
207
|
+
|
|
208
|
+
When the package backs the call (``status == "present"``), the
|
|
209
|
+
envelope reports the package's own ``backend_version`` and
|
|
210
|
+
``features`` so consumers can feature-detect against real
|
|
211
|
+
capabilities. Otherwise the file-fallback markers are returned.
|
|
151
212
|
"""
|
|
152
213
|
r = status(refresh=refresh)
|
|
153
214
|
envelope_status = {
|
|
@@ -155,11 +216,12 @@ def health(refresh: bool = False) -> dict:
|
|
|
155
216
|
"misconfigured": "degraded",
|
|
156
217
|
"absent": "ok",
|
|
157
218
|
}[r.status]
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
219
|
+
if r.status == "present" and (r.backend_version or r.features):
|
|
220
|
+
backend_version = r.backend_version or _FILE_BACKEND_VERSION
|
|
221
|
+
features = list(r.features) if r.features else list(_FILE_BACKEND_FEATURES)
|
|
222
|
+
else:
|
|
223
|
+
backend_version = _FILE_BACKEND_VERSION
|
|
224
|
+
features = list(_FILE_BACKEND_FEATURES)
|
|
163
225
|
return {
|
|
164
226
|
"contract_version": CONTRACT_VERSION,
|
|
165
227
|
"status": envelope_status,
|
package/scripts/postinstall.sh
CHANGED
|
@@ -33,6 +33,22 @@ trap 'rm -f "$LOG"' EXIT
|
|
|
33
33
|
bash "$INSTALLER" --quiet >"$LOG" 2>&1
|
|
34
34
|
CODE=$?
|
|
35
35
|
if [[ $CODE -eq 0 ]]; then
|
|
36
|
+
# Optional companion: @event4u/agent-memory. Suggest it once, only if
|
|
37
|
+
# the consumer hasn't already installed it (locally or on PATH). The
|
|
38
|
+
# hint is purely informational; agent-config falls back to file-based
|
|
39
|
+
# memory when the backend is absent.
|
|
40
|
+
if ! command -v memory >/dev/null 2>&1 \
|
|
41
|
+
&& ! command -v agent-memory >/dev/null 2>&1 \
|
|
42
|
+
&& [[ ! -d "$SCRIPT_DIR/../../@event4u/agent-memory" ]]; then
|
|
43
|
+
cat >&2 <<'HINT'
|
|
44
|
+
💡 agent-config tip: install @event4u/agent-memory for persistent agent
|
|
45
|
+
learnings across sessions (optional, dev-only):
|
|
46
|
+
|
|
47
|
+
npm install --save-dev @event4u/agent-memory
|
|
48
|
+
|
|
49
|
+
Skip if you don't need it — agent-config falls back to file-based memory.
|
|
50
|
+
HINT
|
|
51
|
+
fi
|
|
36
52
|
exit 0
|
|
37
53
|
fi
|
|
38
54
|
|