@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/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"
@@ -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.12.0",
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
  }
@@ -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 ;;
@@ -1,21 +1,28 @@
1
1
  #!/usr/bin/env python3
2
- """File-based retrieval for the `absent` path.
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
- The returned shape is identical to the `present`-path adapter over the
10
- `@event4u/agent-memory` API, so skills stay backend-agnostic.
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(types=["ownership"], keys=["app/Http"], limit=3)
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, with_shadows=args.with_shadows)
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
@@ -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 _probe_health(cli_path: str) -> tuple[bool, str]:
68
- """Returns (healthy, reason)."""
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
- return True, "ok"
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
- result = Result("present", "package", reason, elapsed, cli)
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
- # When the package is present, report its version from `health()`
159
- # output; until we parse that, keep the file-fallback marker so the
160
- # envelope never lies about what backed the response.
161
- backend_version = _FILE_BACKEND_VERSION
162
- features = list(_FILE_BACKEND_FEATURES)
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,
@@ -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