Compare commits
5 commits
9ec83bf7f2
...
971b389014
| Author | SHA1 | Date | |
|---|---|---|---|
| 971b389014 | |||
| a5bb337a9f | |||
| 88e9a39503 | |||
| 67a65e7f16 | |||
| 1d0ecb8bd9 |
5 changed files with 324 additions and 6 deletions
|
|
@ -6,6 +6,7 @@ import socket
|
||||||
import sys
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -20,6 +21,7 @@ from m2_market.ledger import InsufficientFunds, LedgerClient
|
||||||
from m2_market.output import emit_json, format_price, render_kv, render_table, search_results_table
|
from m2_market.output import emit_json, format_price, render_kv, render_table, search_results_table
|
||||||
from m2_market.publish import PublishValidationError, publish_bundle, validate_bundle
|
from m2_market.publish import PublishValidationError, publish_bundle, validate_bundle
|
||||||
from m2_market.registry import RegistryClient, RegistryError
|
from m2_market.registry import RegistryClient, RegistryError
|
||||||
|
from m2_market.telemetry import post_install_outcome
|
||||||
|
|
||||||
# Exit-code map (contracts/cli.md).
|
# Exit-code map (contracts/cli.md).
|
||||||
EXIT_OK = 0 # ok / no-op
|
EXIT_OK = 0 # ok / no-op
|
||||||
|
|
@ -201,6 +203,7 @@ def install(ctx: click.Context, listing_id: str, yes: bool, adapter_name: str |
|
||||||
ref = f"{listing_id}:{socket.gethostname()}:{uuid.uuid4()}"
|
ref = f"{listing_id}:{socket.gethostname()}:{uuid.uuid4()}"
|
||||||
seller = listing.get("seller") or solution.get("seller")
|
seller = listing.get("seller") or solution.get("seller")
|
||||||
amount = price.get("amount")
|
amount = price.get("amount")
|
||||||
|
started = time.monotonic()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
grant = ledger.install(buyer=config.operator_id, seller=seller, amount=amount, ref=ref)
|
grant = ledger.install(buyer=config.operator_id, seller=seller, amount=amount, ref=ref)
|
||||||
|
|
@ -221,6 +224,15 @@ def install(ctx: click.Context, listing_id: str, yes: bool, adapter_name: str |
|
||||||
bundle_root = _extract_bundle(bundle_path, tmp_dir / "extracted")
|
bundle_root = _extract_bundle(bundle_path, tmp_dir / "extracted")
|
||||||
except (RegistryError, OSError, tarfile.TarError) as exc:
|
except (RegistryError, OSError, tarfile.TarError) as exc:
|
||||||
ledger.refund(ref)
|
ledger.refund(ref)
|
||||||
|
post_install_outcome(
|
||||||
|
config,
|
||||||
|
listing_id,
|
||||||
|
seller,
|
||||||
|
amount,
|
||||||
|
ref,
|
||||||
|
"fetch_failed_refunded",
|
||||||
|
time.monotonic() - started,
|
||||||
|
)
|
||||||
click.echo(
|
click.echo(
|
||||||
f"m2-market install: bundle fetch/verify failed, refunded: {exc}",
|
f"m2-market install: bundle fetch/verify failed, refunded: {exc}",
|
||||||
err=True,
|
err=True,
|
||||||
|
|
@ -244,6 +256,15 @@ def install(ctx: click.Context, listing_id: str, yes: bool, adapter_name: str |
|
||||||
return
|
return
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
ledger.refund(ref)
|
ledger.refund(ref)
|
||||||
|
post_install_outcome(
|
||||||
|
config,
|
||||||
|
listing_id,
|
||||||
|
seller,
|
||||||
|
amount,
|
||||||
|
ref,
|
||||||
|
"apply_failed_refunded",
|
||||||
|
time.monotonic() - started,
|
||||||
|
)
|
||||||
click.echo(
|
click.echo(
|
||||||
f"m2-market install: apply failed, refunded: {exc}\n"
|
f"m2-market install: apply failed, refunded: {exc}\n"
|
||||||
"Remediation: inspect the recipe's post-install checks, fix the "
|
"Remediation: inspect the recipe's post-install checks, fix the "
|
||||||
|
|
@ -260,6 +281,16 @@ def install(ctx: click.Context, listing_id: str, yes: bool, adapter_name: str |
|
||||||
changeset.changeset_hash,
|
changeset.changeset_hash,
|
||||||
"applied",
|
"applied",
|
||||||
)
|
)
|
||||||
|
post_install_outcome(
|
||||||
|
config,
|
||||||
|
listing_id,
|
||||||
|
seller,
|
||||||
|
amount,
|
||||||
|
ref,
|
||||||
|
"applied",
|
||||||
|
time.monotonic() - started,
|
||||||
|
grant_id=grant.get("grant_id"),
|
||||||
|
)
|
||||||
|
|
||||||
_print_entrypoint_hint(json_output, solution, {"listing_id": listing_id, "noop": False})
|
_print_entrypoint_hint(json_output, solution, {"listing_id": listing_id, "noop": False})
|
||||||
|
|
||||||
|
|
|
||||||
58
cli/src/m2_market/telemetry.py
Normal file
58
cli/src/m2_market/telemetry.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""Install outcome telemetry write-back (contracts/catalog-index.md §Telemetry write-back).
|
||||||
|
|
||||||
|
Fire-and-forget: writes an episodic memory into the `market:evidence` partition so
|
||||||
|
listings accrue install evidence. Telemetry must never break an install — any
|
||||||
|
failure is swallowed after printing one warning line to stderr.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from m2_market.config import Config
|
||||||
|
|
||||||
|
PARTITION = "market:evidence"
|
||||||
|
|
||||||
|
|
||||||
|
def post_install_outcome(
|
||||||
|
config: Config,
|
||||||
|
listing_id: str,
|
||||||
|
seller: str | None,
|
||||||
|
amount,
|
||||||
|
ref: str,
|
||||||
|
outcome: str,
|
||||||
|
wall_seconds: float,
|
||||||
|
grant_id: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Best-effort write of an install outcome to memory-api. Never raises."""
|
||||||
|
try:
|
||||||
|
with httpx.Client(
|
||||||
|
base_url=config.memory_api_url,
|
||||||
|
headers={"X-API-Key": config.memory_api_key},
|
||||||
|
timeout=10.0,
|
||||||
|
) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/memory/store",
|
||||||
|
json={
|
||||||
|
"content": f"install {outcome}: {listing_id}",
|
||||||
|
"agent_id": PARTITION,
|
||||||
|
"memory_type": "episodic",
|
||||||
|
"importance": 0.8,
|
||||||
|
"source": "m2-market-cli",
|
||||||
|
"metadata": {
|
||||||
|
"listing_id": listing_id,
|
||||||
|
"buyer": config.operator_id,
|
||||||
|
"seller": seller,
|
||||||
|
"amount": amount,
|
||||||
|
"outcome": outcome,
|
||||||
|
"ref": ref,
|
||||||
|
"wall_seconds": wall_seconds,
|
||||||
|
"grant_id": grant_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
except Exception as exc: # noqa: BLE001 - telemetry must never break an install
|
||||||
|
print(f"m2-market: telemetry write-back failed (ignored): {exc}", file=sys.stderr)
|
||||||
|
|
@ -85,6 +85,7 @@ class FakeState:
|
||||||
self.refund_calls = 0
|
self.refund_calls = 0
|
||||||
self.license_exists = False
|
self.license_exists = False
|
||||||
self.insufficient_funds = False
|
self.insufficient_funds = False
|
||||||
|
self.telemetry_calls = []
|
||||||
|
|
||||||
|
|
||||||
def _b64_json(payload: dict) -> str:
|
def _b64_json(payload: dict) -> str:
|
||||||
|
|
@ -164,6 +165,12 @@ def make_handler(state: FakeState):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if host == "catalog.test" and path == "/memory/store":
|
||||||
|
body = json.loads(request.content)
|
||||||
|
assert body["agent_id"] == "market:evidence", body
|
||||||
|
state.telemetry_calls.append(body)
|
||||||
|
return httpx.Response(200, json={"id": "mem-telemetry-1"})
|
||||||
|
|
||||||
raise AssertionError(f"unexpected request: {request.method} {request.url}")
|
raise AssertionError(f"unexpected request: {request.method} {request.url}")
|
||||||
|
|
||||||
return handler
|
return handler
|
||||||
|
|
@ -255,6 +262,10 @@ def test_funded_install_happy_path(fake_backend, config_env, monkeypatch):
|
||||||
assert calls.plan_calls == 1
|
assert calls.plan_calls == 1
|
||||||
assert calls.apply_calls == 1
|
assert calls.apply_calls == 1
|
||||||
assert calls.verify_calls == 1
|
assert calls.verify_calls == 1
|
||||||
|
assert len(fake_backend.telemetry_calls) == 1
|
||||||
|
telemetry = fake_backend.telemetry_calls[0]
|
||||||
|
assert telemetry["metadata"]["outcome"] == "applied"
|
||||||
|
assert telemetry["metadata"]["listing_id"] == LISTING_ID
|
||||||
|
|
||||||
|
|
||||||
def test_insufficient_funds_no_adapter_call(fake_backend, config_env, monkeypatch):
|
def test_insufficient_funds_no_adapter_call(fake_backend, config_env, monkeypatch):
|
||||||
|
|
|
||||||
218
scout/SPIKE.md
Normal file
218
scout/SPIKE.md
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
# Scout Host Spike (T041)
|
||||||
|
|
||||||
|
Time-boxed, read-mostly investigation of this host (m2, the machine this repo lives on)
|
||||||
|
to pick where the Solution Scout (Story 5 / FR-011) should live. Evaluates the three
|
||||||
|
options named in research.md §R6: (A) standalone supervised watcher, (B) Hermes plugin,
|
||||||
|
(C) herdr plugin/hook — against (a) access to structured summaries without raw
|
||||||
|
keystrokes, (b) opt-in enforcement point, (c) on-box summarization cost, (d) notification
|
||||||
|
surface, (e) update/rollout story across the `primus` and `agent-latest` image classes.
|
||||||
|
|
||||||
|
Note: `specs/S4-solution-scout.md` §5 already drafted a recommendation (Option A) before
|
||||||
|
this spike ran. This report re-derives the decision from concrete file/path evidence
|
||||||
|
gathered on the live host, and **overrides that draft** — see §5 for why.
|
||||||
|
|
||||||
|
## 0. Fleet context confirmed on this host
|
||||||
|
|
||||||
|
- Two desktop image classes exist, and they are **not** interchangeable for rollout:
|
||||||
|
- `primus` — built from `/home/m2/m2o/desktop/Dockerfile`, this repo's own supervisord
|
||||||
|
config, canary machine `chris-m2o`. System/image changes here mean re-baking primus
|
||||||
|
(`/home/m2/m2o/desktop/README.md:74`, `:95-104`).
|
||||||
|
- `agent-latest` — `ghcr.io/machine-machine/m2-desktop:agent-latest`, described
|
||||||
|
repeatedly in `/home/m2/m2o/.planning/federated-learning/*` as "legacy/Coolify",
|
||||||
|
a **different image lineage** this repo does not build
|
||||||
|
(`m2o/.planning/federated-learning/proposals/A4-propagation-rollout.md:5,17`).
|
||||||
|
- The federated-learning propagation ADR is explicit: *"image/system changes are baked
|
||||||
|
into `m2-desktop:primus`... This avoids pretending that `primus` and legacy/Coolify
|
||||||
|
`agent-latest` machines can be updated the same way"* (A4:5). Constitution VI says the
|
||||||
|
same: "runtime-sync is the only universal install path across mixed desktop images."
|
||||||
|
- Concretely on this host, every desktop-level supervised program (`hermes-gateway`,
|
||||||
|
`herdr-integrations`, `ttyd`) is wired in by `Dockerfile` `COPY ... >> supervisord.conf`
|
||||||
|
steps at **build time** (`m2o/desktop/Dockerfile:73-126`) into
|
||||||
|
`/etc/supervisor/conf.d/supervisord.conf`. That file is not part of any volume that
|
||||||
|
runtime-sync touches — it only exists post-bake, inside the image.
|
||||||
|
|
||||||
|
This single fact is the crux of the spike: **adding a new supervised process is an
|
||||||
|
image-bake operation**, reachable on `primus` (we own the Dockerfile) but not on
|
||||||
|
`agent-latest` (we don't own that image's build) without a separate, per-machine,
|
||||||
|
non-idempotent `docker exec` patch to a running container's supervisord config — exactly
|
||||||
|
the kind of fleet-wide-blast / non-reversible operation constitution VI rules out.
|
||||||
|
|
||||||
|
## 1. Option A — Standalone supervised watcher
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
|
||||||
|
- Pattern to copy: `hermes-gateway.supervisor.conf` (`m2o/desktop/hermes/hermes-gateway.supervisor.conf`)
|
||||||
|
— `[program:hermes-gateway]`, `autorestart=true`, `priority=45`, `sleep 12` boot stagger,
|
||||||
|
appended into the active `supervisord.conf` via `Dockerfile:79-82`.
|
||||||
|
- Adding `m2-solution-scout` the same way means a third `COPY ... >> supervisord.conf`
|
||||||
|
block in `m2o/desktop/Dockerfile`, i.e. a `primus` image cut (`VERSION=vN ./build.sh`,
|
||||||
|
README.md:99).
|
||||||
|
- **Rollout gap (the finding S4 missed):** there is no equivalent Dockerfile for
|
||||||
|
`agent-latest` in this repo — it's pulled pre-built from `ghcr.io`. Reaching it means
|
||||||
|
either waiting for whoever owns that image to add the program, or `docker exec`-patching
|
||||||
|
each running `agent-latest` container's `/etc/supervisor/conf.d/supervisord.conf` and
|
||||||
|
`supervisorctl update` by hand per machine. That is not idempotent/reversible/runtime-sync
|
||||||
|
in the sense constitution VI and the "Inherited Hard Constraints" section require.
|
||||||
|
|
||||||
|
Scoring: strongest privacy isolation (a dedicated process only reads what it's told to),
|
||||||
|
but the update/rollout story (e) is the weakest of the three on THIS fleet, not the
|
||||||
|
strongest as S4 assumed.
|
||||||
|
|
||||||
|
## 2. Option B — Hermes plugin
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
|
||||||
|
- Hermes already runs a real plugin on this host:
|
||||||
|
`/home/m2/.hermes/plugins/herdr-agent-state/{__init__.py,plugin.yaml}`, installed by
|
||||||
|
`herdr integration install hermes` (confirmed via `herdr integration` subcommand list;
|
||||||
|
this exact plugin ships with `# HERDR_INTEGRATION_ID=hermes` header). It registers
|
||||||
|
hooks via `ctx.register_hook(...)` for `on_session_start`, `pre_llm_call`,
|
||||||
|
`pre_tool_call`, `post_tool_call`, `pre_approval_request`, `post_approval_response`,
|
||||||
|
`post_llm_call`, `on_session_end`, `on_session_finalize` — reporting only lifecycle
|
||||||
|
state (`working|blocked|idle`) over the herdr Unix socket, never message content.
|
||||||
|
- Plugin hook docs (`/home/m2/.hermes/hermes-agent/website/docs/user-guide/features/hooks.md`)
|
||||||
|
show the two hooks a Scout would actually want:
|
||||||
|
- `post_llm_call(session_id, user_message, assistant_response, conversation_history,
|
||||||
|
model, platform, **kwargs)` — fires once per turn, **already gives structured,
|
||||||
|
per-turn intent content** (not raw keystrokes — it's the resolved message/response
|
||||||
|
text, the same thing Hermes itself just processed).
|
||||||
|
- `pre_llm_call(...) -> {"context": str}` — could be reused later to inject a
|
||||||
|
"here's a matching Solution" hint directly into the next turn instead of only toasting.
|
||||||
|
- Deployment is a pure file drop: `~/.hermes/plugins/m2-solution-scout/{__init__.py,plugin.yaml}`.
|
||||||
|
No supervisord, no image bake — this is exactly the "runtime fleet-sync agent into
|
||||||
|
persisted home volumes" path the federated-learning ADR (A4) calls out as the one
|
||||||
|
path that reaches BOTH image classes, **because Hermes itself is already supervised
|
||||||
|
on both** (hermes-gateway is the canonical per-desktop supervision pattern referenced
|
||||||
|
by this very task, and its plugin directory lives under `$HERMES_HOME` regardless of
|
||||||
|
which image booted it).
|
||||||
|
- Opt-in enforcement point: hooks are in-process Python: a `pull-policy.toml` read at
|
||||||
|
plugin `register()` time can make every hook a no-op when `scout.enabled=false`,
|
||||||
|
exactly like the CLAUDE.md-documented pattern for other opt-in config.
|
||||||
|
- On-box summarization cost: cheapest of the three — the plugin receives already-clean
|
||||||
|
turn text; no separate log-tailing/parsing/redaction pass over external files is needed
|
||||||
|
before summarizing, only redaction of the text it's handed directly (same
|
||||||
|
`redact_secrets`/`redact_client_identifiers` obligations from S4's `pull-policy.toml` draft).
|
||||||
|
- Notification surface: none built in — the plugin still needs to shell out to
|
||||||
|
`notify-send` (XFCE) or the herdr toast socket (see Option C) for the actual popup.
|
||||||
|
This is a small, one-file bridge either way.
|
||||||
|
|
||||||
|
Cons confirmed from the doc: plugin hooks run **in-process** with Hermes (same trust
|
||||||
|
boundary — a crashing hook is caught and logged per the docs, "never crashing the agent",
|
||||||
|
but a slow hook still adds latency to every turn). And it only sees Hermes sessions —
|
||||||
|
OpenClaw-only or bare-herdr (Claude Code/Codex panes with no Hermes chat active) sessions
|
||||||
|
are invisible to it.
|
||||||
|
|
||||||
|
## 3. Option C — herdr plugin/hook
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
|
||||||
|
- `herdr agent list` (run once, per task instructions) returns a live, structured JSON
|
||||||
|
lifecycle feed today: `{agent, agent_status: working|idle|done, cwd, pane_id,
|
||||||
|
workspace_id, ...}` for every pane across every workspace on the box — genuinely
|
||||||
|
"structured run/lifecycle summary," zero raw keystrokes, already running, no code to write.
|
||||||
|
- `~/.herdr/runs/*.md` (e.g. `2026-07-02-m2-market-first-wedge-gate.md`) are rich,
|
||||||
|
human/agent-authored narrative summaries — but they are a **manual convention** written
|
||||||
|
by an orchestrating session at the end of a herd run (see this repo's own recent runs),
|
||||||
|
not an automatic per-session artifact. Most single-agent sessions never produce one.
|
||||||
|
This makes Option C's "structured summary" source sparse and bursty rather than
|
||||||
|
continuous — a poor fit for "an operator is mid-session, starts building something... an
|
||||||
|
agent proposes the link right then" (CONCEPT.md §5).
|
||||||
|
- `~/.claude/hooks/herdr-agent-state.sh` (installed by `herdr integration install claude`)
|
||||||
|
shows the wire protocol: a Claude Code hook posts `pane.report_agent_session` over
|
||||||
|
`$HERDR_SOCKET_PATH` — again lifecycle-only, no content.
|
||||||
|
- `~/.config/herdr/config.toml` already has `[ui.toast] delivery = "herdr"` — confirms
|
||||||
|
herdr owns a native toast/notification surface today (this is also directly reusable
|
||||||
|
by Options A/B as the notification backend, independent of which host wins).
|
||||||
|
- `herdr integration` subcommands (`install/uninstall {pi,omp,claude,codex,copilot,
|
||||||
|
opencode,hermes,qodercli}`, `status`) show integrations are themselves file-drops under
|
||||||
|
`~/.claude/hooks/`, `~/.hermes/plugins/`, etc. — same runtime-sync-friendly deploy story
|
||||||
|
as Option B.
|
||||||
|
|
||||||
|
Scoring: best-in-class for (b) opt-in enforcement (a single socket message gate) and (d)
|
||||||
|
notification, but weakest for (a) — the only *continuous* signal herdr exposes natively is
|
||||||
|
coarse lifecycle state (working/idle/done + cwd), not intent. Getting real intent out of
|
||||||
|
herdr alone means depending on the sparse `runs/*.md` convention, which most sessions
|
||||||
|
don't produce.
|
||||||
|
|
||||||
|
## 4. Scored comparison
|
||||||
|
|
||||||
|
| Criterion | A: Standalone watcher | B: Hermes plugin | C: herdr plugin/hook |
|
||||||
|
|---|---|---|---|
|
||||||
|
| (a) structured intent w/o raw keystrokes | Depends entirely on tap sources it's given — none exist continuously today | **Best** — `post_llm_call` gives per-turn content already, every turn | Weak — only continuous signal is lifecycle state; real intent needs sparse `runs/*.md` |
|
||||||
|
| (b) opt-in enforcement point | Own config file, own gate — clean but net-new | Clean — gate inside `register()` before any hook does work | **Best** — one socket message, herdr already gates its own integrations |
|
||||||
|
| (c) on-box summarization cost | Highest — must tail/parse/redact multiple external files first | **Lowest** — handed clean text directly | Low for lifecycle, but high if it also has to parse `runs/*.md` |
|
||||||
|
| (d) notification surface | Must shell out (herdr toast or XFCE) | Must shell out (herdr toast or XFCE) | **Native** — herdr toast is its own surface, `[ui.toast]` already configured |
|
||||||
|
| (e) update/rollout, both image classes | **Weakest** — new supervisor program = image bake; no equivalent Dockerfile for `agent-latest` | **Best** — pure file drop under `$HERMES_HOME`, works wherever Hermes already runs (both classes) | Good — file drop under `~/.claude/hooks`/`~/.hermes/plugins` via `herdr integration install`, but only reaches herdr-managed panes |
|
||||||
|
|
||||||
|
## 5. Recommendation
|
||||||
|
|
||||||
|
**DECISION: Hermes plugin (Option B)**
|
||||||
|
|
||||||
|
Rationale: FR-011 and Story 5's hardest constraints are (1) real per-turn intent
|
||||||
|
without raw keystrokes, and (5) a rollout story that doesn't require rebuilding either
|
||||||
|
image class. Concrete evidence on this host shows Option A fails (5) outright — the only
|
||||||
|
mechanism this repo has for registering a new supervised program is a `Dockerfile` COPY
|
||||||
|
into `supervisord.conf`, which has no `agent-latest` equivalent, making that option
|
||||||
|
image-bake-only on one of the two live classes. Option C is native for opt-in/notification
|
||||||
|
but its only continuous, low-cost signal is coarse lifecycle state — it would need Option
|
||||||
|
B's or Option A's tap to get real intent anyway. Option B satisfies (1) directly
|
||||||
|
(`post_llm_call` hands over already-resolved turn text, cheapest to summarize-on-box), and
|
||||||
|
(5) matches proven fact: this host already runtime-installs a real Hermes plugin
|
||||||
|
(`herdr-agent-state`) as a pure file drop, no image rebuild, and Hermes is the one
|
||||||
|
component already supervised identically across both `primus` and `agent-latest`
|
||||||
|
(that's the same "hermes-gateway pattern" this task was pointed at as reference).
|
||||||
|
|
||||||
|
Caveat worth carrying into T042: a Hermes-hosted Scout is blind to bare-herdr sessions
|
||||||
|
that never touch Hermes chat (a Claude Code/Codex pane with no Hermes turn). This does
|
||||||
|
**not** violate the "Hermes first, never Hermes-only" inherited constraint — that
|
||||||
|
constraint is about marketplace touchpoints in general (Store/CLI/catalog remain
|
||||||
|
Hermes-independent), and Story 5's own acceptance criteria only require discovery "via
|
||||||
|
Scout **or** Hermes/CLI." It is, however, a real coverage gap: pure-herdr desktops get no
|
||||||
|
push-side discovery in v0. Cheap mitigation for a later phase, not blocking: also register
|
||||||
|
`herdr integration install hermes`'s existing socket report as a secondary opt-in trigger
|
||||||
|
so the same plugin can request the herdr toast surface (`[ui.toast] delivery = "herdr"`)
|
||||||
|
instead of shelling to `notify-send`, without adding a second host.
|
||||||
|
|
||||||
|
## 6. v0 implementation sketch
|
||||||
|
|
||||||
|
Components:
|
||||||
|
|
||||||
|
- `~/.hermes/plugins/m2-solution-scout/`
|
||||||
|
- `plugin.yaml` — `name: m2-solution-scout`, `description: Solution Scout intent watch`.
|
||||||
|
- `__init__.py` — `register(ctx)` wires `on_session_start` (load `pull-policy.toml`,
|
||||||
|
no-op immediately if `scout.enabled=false` or `tenant_id` missing — fail closed, per
|
||||||
|
S4's policy rules) and `post_llm_call` (the intent tap).
|
||||||
|
- Config: reuse S4's existing `pull-policy.toml` schema verbatim
|
||||||
|
(`~/.config/m2-market/pull-policy.toml`, `[scout]` block, `raw_keystrokes`/
|
||||||
|
`raw_client_data` reserved-deny keys, `max_proposals_per_day`, cooldowns). No new
|
||||||
|
config format — the plugin is a new *host* for the same contract S4 already specified.
|
||||||
|
- State/telemetry: same paths as S4 §3 (`~/.local/share/m2-market/scout/state.json`,
|
||||||
|
`outbox/YYYY-MM-DD.jsonl`) — the plugin process just runs inside Hermes instead of as
|
||||||
|
its own daemon.
|
||||||
|
|
||||||
|
Event flow (per turn, inside `post_llm_call`):
|
||||||
|
|
||||||
|
1. Gate: `scout.enabled` true, tenant_id present, rate cap and per-session cap not yet
|
||||||
|
hit, cooldown not active for the last dismissed listing → else return immediately.
|
||||||
|
2. Take `user_message` + `assistant_response` (bounded to `summary.max_source_chars`),
|
||||||
|
redact secrets/client identifiers per `pull-policy.toml`, produce
|
||||||
|
`m2.scout.intent_summary.v1` (S4 schema, unchanged) — deterministic extractive summary
|
||||||
|
in v0, no extra LLM call needed since the plugin already has clean turn text.
|
||||||
|
3. `POST /memory/search` to memory-api, `agent_id=market:catalog`, tenant-filtered (S4 §3
|
||||||
|
catalog query, unchanged).
|
||||||
|
4. Score/coverage threshold check (S4's `m2.scout.match.v1`) → if it clears, build
|
||||||
|
`m2.scout.proposal.v1` and fire the toast (`notify-send` primary, herdr socket toast as
|
||||||
|
fallback/secondary surface) with the M2 Store deep link.
|
||||||
|
5. Write `proposal_shown` telemetry to local outbox (batched flush to `market:evidence`,
|
||||||
|
same as S4 §3/§4) — Scout never calls `m2-market install` itself; Store/CLI own
|
||||||
|
debit→grant→apply and emit the accept/dismiss events this plugin later folds back in.
|
||||||
|
6. All of steps 2-5 run off the hot path where possible (background thread, mirroring the
|
||||||
|
`boot-md` hook tutorial's pattern of not blocking the turn on non-essential work) so a
|
||||||
|
slow catalog query never adds latency to the operator's actual turn.
|
||||||
|
|
||||||
|
Guardrails carried over unchanged from FR-011 / S4: default `enabled=false`; opt-in is
|
||||||
|
per-desktop `pull-policy.toml`; `raw_keystrokes`/`raw_client_data` keys hard-refuse start
|
||||||
|
if true; rate cap default 5/day; propose-only — install always requires the human's click
|
||||||
|
through M2 Store.
|
||||||
|
|
||||||
|
DECISION: Hermes plugin
|
||||||
|
|
@ -113,9 +113,9 @@ branch `m2-store`; contracts referenced from this repo)
|
||||||
|
|
||||||
**Independent test**: quickstart.md Scenario F on chris-m2o
|
**Independent test**: quickstart.md Scenario F on chris-m2o
|
||||||
|
|
||||||
- [ ] T037 [US4] Fork/branch `m2-store` on github.com/machine-machine/cargstore; swap catalog source from Flatpak JSON to `market:catalog` via memory-api HTTP (desktop-provisioned creds per research.md R7) in cargstore `src/` catalog service
|
- [x] T037 [US4] Fork/branch `m2-store` on github.com/machine-machine/cargstore; swap catalog source from Flatpak JSON to `market:catalog` via memory-api HTTP (desktop-provisioned creds per research.md R7) in cargstore `src/` catalog service
|
||||||
- [ ] T038 [US4] Add Solution install backend to cargstore that shells `m2-market install --json` and renders progress/result truthfully (incl. refund-on-failure path) in cargstore `src/` install manager
|
- [x] T038 [US4] Add Solution install backend to cargstore that shells `m2-market install --json` and renders progress/result truthfully (incl. refund-on-failure path) in cargstore `src/` install manager
|
||||||
- [ ] T039 [US4] Solution detail page: evidence, price, permissions diff (from `m2-market show --json`) in cargstore `src/` UI
|
- [x] T039 [US4] Solution detail page: evidence, price, permissions diff (from `m2-market show --json`) in cargstore `src/` UI
|
||||||
- [ ] T040 [US4] Rebrand Clawdbot→M2 Store and deploy to canaries chris-m2o + gunnar-m2o only; run Scenario F
|
- [ ] T040 [US4] Rebrand Clawdbot→M2 Store and deploy to canaries chris-m2o + gunnar-m2o only; run Scenario F
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -126,7 +126,7 @@ branch `m2-store`; contracts referenced from this repo)
|
||||||
|
|
||||||
**Independent test**: quickstart.md Scenario G
|
**Independent test**: quickstart.md Scenario G
|
||||||
|
|
||||||
- [ ] T041 [US5] Scout host exploration spike → `scout/SPIKE.md`: compare standalone supervised watcher vs Hermes plugin vs herdr plugin/hook on: summary access, opt-in enforcement point, on-box summarization cost, notification surface, rollout story per image class (research.md R6; operator fork §14.4 "explore") — time-boxed, ends with a decision
|
- [x] T041 [US5] Scout host exploration spike → `scout/SPIKE.md`: compare standalone supervised watcher vs Hermes plugin vs herdr plugin/hook on: summary access, opt-in enforcement point, on-box summarization cost, notification surface, rollout story per image class (research.md R6; operator fork §14.4 "explore") — time-boxed, ends with a decision
|
||||||
- [ ] T042 [US5] Implement Scout v0 per SPIKE.md winner in `scout/` (or plugin location the spike picks): summary ingestion → embed → `market:catalog` match (tenant-scoped) → threshold → toast + deep link; opt-in via pull-policy config; rate cap (default 5/day)
|
- [ ] T042 [US5] Implement Scout v0 per SPIKE.md winner in `scout/` (or plugin location the spike picks): summary ingestion → embed → `market:catalog` match (tenant-scoped) → threshold → toast + deep link; opt-in via pull-policy config; rate cap (default 5/day)
|
||||||
- [ ] T043 [US5] Proposal outcome recording (shown/dismissed/accepted/suppressed) locally + summarized to `market:evidence` per data-model.md Proposal; conversion counts folded to listing stats via indexer telemetry pass
|
- [ ] T043 [US5] Proposal outcome recording (shown/dismissed/accepted/suppressed) locally + summarized to `market:evidence` per data-model.md Proposal; conversion counts folded to listing stats via indexer telemetry pass
|
||||||
- [ ] T044 [US5] Deploy Scout v0 to ONE canary opt-in desktop; run Scenario G (incl. opt-out and rate-cap negative checks)
|
- [ ] T044 [US5] Deploy Scout v0 to ONE canary opt-in desktop; run Scenario G (incl. opt-out and rate-cap negative checks)
|
||||||
|
|
@ -135,8 +135,8 @@ branch `m2-store`; contracts referenced from this repo)
|
||||||
|
|
||||||
## Phase 8: Polish & Cross-Cutting
|
## Phase 8: Polish & Cross-Cutting
|
||||||
|
|
||||||
- [ ] T045 [P] Install telemetry write-back (FR-014): CLI posts install outcome records to `market:evidence`; indexer folds into listing `stats.installs` — `cli/src/m2_market/telemetry.py` + indexer pass
|
- [x] T045 [P] Install telemetry write-back (FR-014): CLI posts install outcome records to `market:evidence`; indexer folds into listing `stats.installs` — `cli/src/m2_market/telemetry.py` + indexer pass
|
||||||
- [ ] T046 [P] `reindex` operational check task: run Scenario E, document recovery runbook in `docs/runbook.md` (incl. memory-api split-brain caveat from research.md R4)
|
- [x] T046 [P] `reindex` operational check task: run Scenario E, document recovery runbook in `docs/runbook.md` (incl. memory-api split-brain caveat from research.md R4)
|
||||||
- [ ] T047 Run full quickstart.md scenario sweep A–E on canaries; fix gaps; tick spec.md acceptance boxes in a verification note `specs/001-market-first-wedge/VERIFICATION.md`
|
- [ ] T047 Run full quickstart.md scenario sweep A–E on canaries; fix gaps; tick spec.md acceptance boxes in a verification note `specs/001-market-first-wedge/VERIFICATION.md`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue