merge: support-hermes branch (Hermes agent support modernization)
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
# 🧭 Agent Backend Evaluation — Local LLM (Ollama / GLM-5.2) CLI Integration
|
||||||
|
|
||||||
|
- **Planner**: `planner-reviewer-claude-01`
|
||||||
|
- **Job ID**: `50c8456e` (Rev.1) → refined under `8aee5fdb` (Rev.2)
|
||||||
|
- **Scope**: Evaluate `hermes`, `pi`, and `opencode` as candidates for official Multi-Agent Mux (MAM) integration, specifically to support a **local LLM backend** (Ollama-hosted, target model **GLM-5.2**).
|
||||||
|
- **Status**: Rev.2 — refined per `creator-agy-01`'s architectural challenge (job `f98023cb`). Still pending a live `[VERDICT: PASS]` before adapter implementation begins (see §6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Changelog — Rev.2 (response to `creator-agy-01` challenge)
|
||||||
|
|
||||||
|
`creator-agy-01` filed a formal architectural challenge against Rev.1 (full text preserved in `.mam/jobs/8aee5fdb/brief.md`), raising four points. **All four are accepted as valid — no `[REBUT:]` is filed.** Each is grounded in evidence already present in this repo (the permission-bypass flags actually shipped in `claude.py`/`agy.py`/`grok.py`, and the Session ID Lifecycle protocol in `MULTI_AGENT_RULES.md` §2) or in well-established Ollama operational behavior, and Rev.1 did not address any of them. Changes made:
|
||||||
|
|
||||||
|
| # | Challenge | Disposition | Where addressed in Rev.2 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | Unverified unattended permission-bypass flag for OpenCode | **Accepted** | New §3.2 "Unattended Execution Compatibility" subsection; new Phase 0 checklist item; new comparison-matrix row |
|
||||||
|
| 2 | Unverified `--session-id` pre-assignment support for OpenCode (breaks P0 assigned-UUID protocol → `C-ambiguous` risk) | **Accepted** | New §3.2 subsection; new adapter-contract fallback requirement in Phase 2 §8; new comparison-matrix row |
|
||||||
|
| 3 | Ollama default `num_ctx: 2048` causes tool-call/context truncation unless raised (~32768) | **Accepted** | New §4.3; new Phase 0/1 documentation deliverable |
|
||||||
|
| 4 | Local-inference cold-start/latency vs. MAM's 120s `idle_timeout_sec` watchdog | **Accepted** | New §4.4; new Phase 0/1 config recommendation (`MAM_IDLE_TIMEOUT` relaxation) |
|
||||||
|
|
||||||
|
Net effect: OpenCode's status is downgraded from "Low-Medium, well-precedented" to **"Low-Medium, contingent on two unverified CLI capabilities that must be confirmed before adapter work starts"** — this changes the Phase 0 gate from optional-best-practice to **hard blocking prerequisite** for Phase 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Executive Summary & Recommendation
|
||||||
|
|
||||||
|
**Recommended path: two-track rollout, OpenCode as primary target, Hermes as immediate interim path, `pi` deferred pending scope clarification.**
|
||||||
|
|
||||||
|
| Rank | Candidate | Verdict | Rationale (1 line) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **OpenCode** | ✅ **Primary integration target** | Purpose-built multi-provider harness with first-class local-model/Ollama support and a provider-agnostic tool-calling layer — best long-term fit for GLM-5.2 stability. |
|
||||||
|
| 2 | **Hermes** | ✅ **Interim / parallel-track** | Adapter already exists in MAM (`hermes.py`, registered) with **zero net-new integration cost**; `auth_ok()` has no hard-coded SaaS credential gate, which is favorable for pointing it at a local endpoint — but its tool-calling behavior against GLM-5.2 via Ollama is unverified and must be smoke-tested before it's trusted for unattended Creator/Reviewer roles. |
|
||||||
|
| 3 | **`pi`** | ⚠️ **Insufficient information — do not integrate yet** | No adapter, no roadmap mention, and no reliable architectural grounding was found in this repo or in prior MAM reports. See §4.3 and §7 for what must be clarified before this candidate can be scored. |
|
||||||
|
|
||||||
|
This is a **Planner-level architectural recommendation**, not a unilateral final decision — per `MULTI_AGENT_RULES.md` §3, it should be routed to the currently running Reviewer/Creator sessions (`reviewer-creator-grok-01`, `creator-agy-01`) for a `[VERDICT: PASS]` / `[VERDICT: NOT PASS]` pass before any adapter code is written. See §6.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Evaluation Criteria
|
||||||
|
|
||||||
|
Weighted against MAM's existing 5-layer adapter contract (`BaseAgentAdapter`, see `new_agent_types_roadmap.md` §2–3) and the stated goal (local LLM support):
|
||||||
|
|
||||||
|
1. **Local-model / Ollama compatibility** — Can the CLI point at a local OpenAI-compatible or native Ollama endpoint without vendor lock-in?
|
||||||
|
2. **Tool-calling stability** — Does the CLI enforce its own structured function-calling schema/validation layer independent of the backing model, or does it trust raw model output? This matters more for GLM-5.2 than for frontier hosted models, since open-weight tool-calling adherence varies by quantization/serving stack.
|
||||||
|
3. **MAM adapter-contract fit** — Session artifact format, `ready_tokens` predictability, auth model, resume/purge semantics (i.e., cost to implement `BaseAgentAdapter`).
|
||||||
|
4. **Integration cost** — Net-new engineering effort across the 5 layers (adapter, registry, `lib.sh` dispatch, herdr kind support, tests).
|
||||||
|
5. **Operational maturity** — Update cadence, community/maintainer signal, TUI stability under herdr's raw-terminal automation (`send_keys_safe`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Candidate Profiles
|
||||||
|
|
||||||
|
### 3.1 Hermes — *already integrated at Tier 1*
|
||||||
|
|
||||||
|
- **Repo evidence**: `.agents/skills/lib_py/agents/adapters/hermes.py` exists and is registered in `registry.py`. This is the only one of the three candidates with a working, tested MAM adapter today.
|
||||||
|
- **Session storage**: SQLite (`~/.hermes/state.db`, `sessions`/`messages` tables) keyed by `cwd` — more robust than flat-file JSON candidates (grok, opencode) for concurrent-workspace disambiguation, and purge semantics are already implemented (`purge_artifacts` deletes both the on-disk JSON and DB rows).
|
||||||
|
- **Auth model**: `auth_ok()` unconditionally returns `True` — there is no hard-coded credential/token-file check the way `grok.py` checks `~/.grok/auth.json` or `XAI_API_KEY`. This is a **positive signal** for local-LLM use: it implies Hermes's own config (outside MAM's purview) is what selects the backend, so pointing it at an Ollama endpoint hosting GLM-5.2 should not trip any MAM-level auth gate.
|
||||||
|
- **Readiness detection**: `ready_tokens = 'Hermes'` — a single, simple banner token. Low risk of TUI-readiness false-negatives, but also under-specified compared to grok's multi-token pattern (`'Grok|xAI|Assistant|❯|>>>'`); should be hardened if Hermes's banner changes across backend configurations (e.g., does it print the active model name?).
|
||||||
|
- **Gap**: Nothing in the adapter or this repo verifies **which** backend/model Hermes is actually running against at spawn time. `spawn_spec()` takes no model/backend argument — model selection is presumably out-of-band (Hermes's own config file/env). This must be confirmed empirically, not assumed.
|
||||||
|
- **Integration cost**: **Zero** (already done). Remaining cost is *validation*, not *engineering*.
|
||||||
|
|
||||||
|
### 3.2 OpenCode — *roadmapped, not yet implemented*
|
||||||
|
|
||||||
|
- **Repo evidence**: Appears only in `new_agent_types_roadmap.md` §4 as a **Tier 1 (Low)** complexity candidate (~0.5 day estimated effort), session storage as JSON files under `~/.opencode/sessions/`, proposed `ready_tokens: 'OpenCode|Chat|Welcome'`.
|
||||||
|
- **Ecosystem knowledge**: OpenCode is designed from the ground up as a **provider-agnostic** terminal coding agent — its core value proposition (distinct from single-vendor CLIs like Claude Code) is a pluggable model-provider layer that explicitly supports local/self-hosted backends (Ollama, LM Studio, and any OpenAI-compatible endpoint) alongside hosted providers, with per-project/per-agent model configuration.
|
||||||
|
- **Tool-calling stability**: Because OpenCode's edit/bash/read tool surface is enforced by its own harness rather than assumed from the model, it is materially more resilient to a local model's imperfect native function-calling than a thin CLI that passes tool schemas straight through to the model API. This is the strongest differentiator in GLM-5.2's favor, since open-weight tool-calling reliability is known to vary by quantization and serving backend (Ollama's tool-calling support itself is still evolving).
|
||||||
|
- **Gap**: Requires **net-new** engineering across all 5 layers per the roadmap blueprint (adapter class, registry entry, `lib.sh` kind mapping + binary-name recognition, herdr `--kind` compatibility check, contract + lifecycle tests). Estimated ~0.5–1.5 days per the existing complexity matrix, consistent with grok's actual delivered effort (grok is now fully integrated, confirming the roadmap's Tier 1 estimates are realistic).
|
||||||
|
- **Integration cost**: **Low-Medium**, well-precedented — the grok adapter (`grok.py`, fully shipped) is a directly reusable template (glob-based session discovery, JSONL artifact verification, `permission-mode`-style CLI flags). **Revised in Rev.2: this cost estimate is now contingent on the two verification gates below.**
|
||||||
|
|
||||||
|
- **⚠️ Unattended Execution Compatibility (new in Rev.2, per `creator-agy-01` challenge §2.1)**: MAM's herdr-background execution model requires the target CLI to run with **zero interactive confirmation prompts** — every existing adapter enforces this by construction: `claude.py`/`agy.py` force `--dangerously-skip-permissions`, `grok.py` forces `--permission-mode bypassPermissions`. **Rev.1 did not verify that OpenCode ships an equivalent flag** (e.g. `--auto-approve`, `--yes`, a `--permission-mode` analog). If it doesn't, the first bash/file-edit tool call will emit an interactive `[y/N]`-style prompt that blocks on stdin inside a headless herdr pane — `send_keys_safe` cannot answer a prompt it wasn't told to expect, and the job stalls until `idle_timeout_sec` (120s default) force-kills it. **This is now a hard go/no-go gate, not a nice-to-have**: OpenCode cannot be adapted for Creator/Reviewer roles at all if no such flag exists, regardless of its tool-calling or local-model strengths.
|
||||||
|
|
||||||
|
- **⚠️ Session-ID Pre-Assignment Compatibility (new in Rev.2, per `creator-agy-01` challenge §2.2)**: `MULTI_AGENT_RULES.md` §2 requires new sessions to receive an externally-generated UUID at spawn time (`--session-id <uuid>`, recorded as `session_id_source: assigned`) specifically to prevent `C-ambiguous` race conditions when multiple sessions of the same agent type are created concurrently in one workspace. **Rev.1's proposed `spawn_spec()` (mirroring grok's `--session-id {session_uuid}` pattern) assumed OpenCode accepts an externally-supplied session ID without checking it.** If OpenCode instead only generates its own internal session identifier (e.g., a hash or timestamp-derived directory name under `~/.opencode/sessions/`), the P0 pre-assignment protocol cannot be used, and `discover()` must fall back to timestamp/PID/cwd-based matching — which is exactly the race-prone pattern §2 of `MULTI_AGENT_RULES.md` was designed to eliminate. Rev.2's Phase 2 plan (§8) now specifies the required fallback contract for this case.
|
||||||
|
|
||||||
|
### 3.3 `pi` — *unresolved candidate, insufficient grounding*
|
||||||
|
|
||||||
|
- **Repo evidence**: **None.** No adapter, no mention in `new_agent_types_roadmap.md`'s candidate list (`codex`, `grok-build`, `opencode`, `kimi`, `cursor`), no reference anywhere under `.agents/` or `.mam/` prior to this job's own brief.
|
||||||
|
- **Ambiguity risk**: "pi" is a generic, collision-prone identifier — it could refer to several unrelated products (a lightweight personal-assistant chat CLI, an internal/codenamed tool, or a coding-agent CLI not yet in this evaluator's confirmed knowledge). Fabricating an architecture/auth/session-format profile for it would produce a plausible-sounding but unverifiable comparison, which is worse than flagging the gap — a wrong `ready_tokens` regex or session-artifact assumption baked into a plan would silently break `wait_for_tui_ready()` and session discovery at implementation time.
|
||||||
|
- **Recommendation**: **Do not score `pi` in this pass.** Before it can be evaluated on equal footing with Hermes/OpenCode, the requester must confirm: (a) the exact binary/package name and install source, (b) whether it exposes a scriptable non-interactive mode or only a raw TUI, (c) its session/transcript storage format, and (d) whether it supports pointing at an arbitrary OpenAI-compatible/Ollama endpoint at all. See §7 open questions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Tool-Calling Stability Against GLM-5.2 (Ollama)
|
||||||
|
|
||||||
|
Regardless of which CLI is chosen, two risks are backend-specific (not MAM-specific) and apply to whichever candidate is selected:
|
||||||
|
|
||||||
|
1. **Function-calling schema adherence**: GLM-family models served through Ollama depend on the Modelfile's chat template correctly implementing tool-call token formatting. A CLI that validates/repairs malformed tool-call JSON client-side (OpenCode's model) degrades more gracefully than one that trusts raw output (unverified for Hermes; unknown for `pi`).
|
||||||
|
2. **Context window / quantization tradeoffs**: Local GGUF/Ollama-served quantizations of GLM-5.2 may have reduced effective context vs. the reference weights, which stresses MAM's existing long-running-session assumptions (`idle_timeout_sec`, `SUB_IDLE_TIMEOUT` in `MULTI_AGENT_RULES.md` §4). This should be smoke-tested with a real multi-turn MAM job before either candidate is trusted for unattended Creator work.
|
||||||
|
|
||||||
|
**Action item**: before committing engineering time to OpenCode's adapter, run a manual (non-MAM) smoke test of both Hermes-against-Ollama-GLM-5.2 and OpenCode-against-Ollama-GLM-5.2 on a representative multi-file edit task, and record actual tool-call success/repair rates. This evaluation is architectural; it cannot substitute for an empirical tool-calling benchmark.
|
||||||
|
|
||||||
|
### 4.3 Ollama Default Context Window (new in Rev.2, per `creator-agy-01` challenge §3.1)
|
||||||
|
|
||||||
|
Ollama's default `num_ctx` is **2048 tokens** unless explicitly overridden. MAM's job briefs, `README.md`/`MULTI_AGENT_RULES.md` reference material injected into agent context, and especially multi-turn tool-calling exchanges routinely exceed this — well below the 8k–32k range typical agentic coding workloads need. Left at the default, this produces **silent early context truncation**, which surfaces as malformed or dropped tool calls that look like a model-quality problem but are actually a serving-configuration problem. This is not specific to OpenCode or Hermes; it applies to **any** CLI pointed at an Ollama-served GLM-5.2. **Requirement**: the Modelfile (or per-request client parameter, if the CLI exposes one) must explicitly set `PARAMETER num_ctx 32768` (or the CLI's equivalent override) before any tool-calling stability conclusions from the Phase 0 smoke test can be trusted — a smoke test run against the 2048-token default would understate both candidates' true tool-calling reliability.
|
||||||
|
|
||||||
|
### 4.4 Local Inference Latency vs. MAM Watchdog Timeouts (new in Rev.2, per `creator-agy-01` challenge §3.2)
|
||||||
|
|
||||||
|
Local GLM-5.2 inference (VRAM load + generation) can plausibly take 30–60s to first token and run at single-digit-to-low-teens tokens/sec, versus the sub-second-to-few-second response latency MAM's timeout defaults were tuned against for hosted-API backends. `lib.sh::wait_for_tui_ready` and the default `idle_timeout_sec` (120s per `MULTI_AGENT_RULES.md` §4) risk false-positive stalls/kills against a working-but-slow local backend, which would misclassify healthy local inference as a hung job. **Requirement**: any local-LLM-backed session (Hermes or OpenCode) must run under a relaxed idle timeout — the challenge suggests `MAM_IDLE_TIMEOUT=300` as a starting point — set via job-level `idle_timeout_sec` override or a documented `.mam.env` convention specifically for local backends, not the global default (which should stay tuned for hosted-API sessions to avoid masking genuinely hung jobs elsewhere).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Comparison Matrix
|
||||||
|
|
||||||
|
| Criterion | Hermes | OpenCode | `pi` |
|
||||||
|
|---|---|---|---|
|
||||||
|
| MAM adapter status | ✅ Shipped | ❌ Roadmapped only | ❌ None |
|
||||||
|
| Local/Ollama support | Likely (out-of-band config, unverified) | ✅ Native, first-class | Unknown |
|
||||||
|
| Tool-calling validation layer | Unknown / unverified | ✅ Harness-enforced | Unknown |
|
||||||
|
| Session storage | SQLite (`state.db`) | JSON files | Unknown |
|
||||||
|
| Auth gate in MAM | None (`auth_ok` always `True`) | N/A (not yet implemented) | Unknown |
|
||||||
|
| Unattended permission-bypass flag confirmed | ⚠️ Not yet verified (needs Phase 0) | ⚠️ **Not yet verified — hard gate (Rev.2)** | Unknown |
|
||||||
|
| External `--session-id` acceptance confirmed | N/A (adapter already ships without it, uses discovery) | ⚠️ **Not yet verified — hard gate (Rev.2)** | Unknown |
|
||||||
|
| Integration cost | **Zero** (done) | Low-Medium (~0.5–1.5 days, **contingent on above two gates — Rev.2**) | Cannot estimate |
|
||||||
|
| Confidence in this evaluation | Medium (grounded in repo code) | Medium-High (grounded in repo roadmap + ecosystem knowledge) | **Low** (no grounding) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Team Consensus Process Note
|
||||||
|
|
||||||
|
Per `MULTI_AGENT_RULES.md` §3, a Planner recommendation is not a final decision — it is meant to be routed through the Developer/Reviewer objection loop. At the time of writing, `.mam/agent-sessions.yaml` shows two other live sessions in this workspace:
|
||||||
|
|
||||||
|
- `reviewer-creator-grok-01` (role: `reviewer,creator`)
|
||||||
|
- `creator-agy-01` (role: `creator`; registry shows a stale `resume dry-run failed` status — its live availability should be re-confirmed by the General Manager before delegating a review job to it)
|
||||||
|
|
||||||
|
Rev.1 was produced by the Planner alone (no live cross-session review round-trip was executed as part of job `50c8456e`). **Update (Rev.2)**: that gap was subsequently closed — `creator-agy-01` did review Rev.1 and filed a formal architectural challenge (job `f98023cb`), which this Rev.2 fully incorporates (see §0). No `[REBUT:]` was needed since every point was valid. **Remaining next step**: this Rev.2 still needs an explicit `[VERDICT: PASS]` / `[VERDICT: NOT PASS]` from a Reviewer session (`reviewer-creator-grok-01`, and `creator-agy-01` re-reviewing its own incorporated feedback) before Phase 2 adapter implementation work begins — consistent with the standard Workflow Loop and the Rebuttal & Adjudication Protocol in `MULTI_AGENT_RULES.md` §3/§3.1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Open Questions (block full consensus until resolved)
|
||||||
|
|
||||||
|
1. What exactly is `pi` — binary name, source, and whether it supports non-interactive/scriptable invocation compatible with herdr's `send_keys_safe` automation?
|
||||||
|
2. Is GLM-5.2 to be served via Ollama's native tool-calling API or an OpenAI-compatible shim? This affects which CLIs are even eligible (some CLIs only support OpenAI-schema tool calls).
|
||||||
|
3. Does Hermes actually expose a way to target a specific Ollama model/endpoint (env var, config file, CLI flag)? This repo's adapter is silent on model selection — needs to be confirmed against Hermes's own docs/CLI help, not assumed.
|
||||||
|
4. What quantization/context-length of GLM-5.2 will be locally hosted? This determines whether the empirical smoke test in §4 is representative of production behavior.
|
||||||
|
5. **(New, Rev.2)** Does OpenCode CLI expose a documented non-interactive/auto-approve flag equivalent to `claude`'s `--dangerously-skip-permissions` or `grok`'s `--permission-mode bypassPermissions`? **Blocking** — see §3.2.
|
||||||
|
6. **(New, Rev.2)** Does OpenCode CLI accept an externally-generated session UUID at spawn time (`--session-id`-style), or only self-assign session identifiers? **Blocking** — determines whether P0 pre-assignment or a PID/cwd-based `discover()` fallback must be used. See §3.2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Integration Roadmap (pending consensus sign-off)
|
||||||
|
|
||||||
|
**Phase 0 (immediate, no code — expanded in Rev.2, now the hard gate before Phase 2)**: Manual smoke test — run Hermes and OpenCode (standalone, outside MAM) against the target Ollama/GLM-5.2 endpoint (served with `num_ctx 32768`, per §4.3) on a representative multi-file coding task. Record:
|
||||||
|
1. Tool-call success rate and malformed-call repair behavior, and session banner/readiness output for adapter tuning (Rev.1 scope).
|
||||||
|
2. **(Rev.2)** Whether each CLI can complete the task with **zero interactive confirmation prompts** using a documented flag — this determines OpenCode's Phase 2 eligibility outright (§3.2).
|
||||||
|
3. **(Rev.2)** Whether OpenCode accepts an externally-supplied `--session-id`-equivalent, or only self-assigns — this determines whether Phase 2 step 1 below uses the P0 pre-assignment pattern or the PID/cwd-based `discover()` fallback (§3.2).
|
||||||
|
4. **(Rev.2)** Observed time-to-first-token and end-to-end task latency, to calibrate the `idle_timeout_sec` override in Phase 1/2 (§4.4).
|
||||||
|
|
||||||
|
**Phase 1 (Hermes validation track, ~0 eng. days)**: No adapter work needed. Confirm Hermes's model-selection mechanism (open question #3) and, if satisfactory, promote it from "Tier 1 present but unvalidated" to "MAM-supported local-LLM backend" in `.mam.env.example` documentation. **(Rev.2)** Document the required Ollama `num_ctx 32768` Modelfile setting and the recommended local-backend `idle_timeout_sec`/`MAM_IDLE_TIMEOUT` override (starting point: 300s, to be tuned against actual Phase 0 latency data) in the same documentation pass — for both Hermes and (if greenlit) OpenCode.
|
||||||
|
|
||||||
|
**Phase 2 (OpenCode adapter track, ~0.5–1.5 eng. days, grok.py as template) — gated on Phase 0 items 2–3 passing**:
|
||||||
|
1. `.agents/skills/lib_py/agents/adapters/opencode.py` — implement `OpenCodeAgentAdapter(BaseAgentAdapter)` per the Step 1 contract in `new_agent_types_roadmap.md`. `spawn_spec()`/`resume_spec()` must include the confirmed unattended-execution flag (§3.2) unconditionally, matching the `claude.py`/`agy.py`/`grok.py` pattern.
|
||||||
|
2. **(Rev.2)** If Phase 0 confirms OpenCode accepts an external session UUID: use the standard `--session-id {session_uuid}` pattern (as Rev.1 assumed). **If not**: `discover()` must implement a strict cwd + process-liveness (PID/`lstart`) matching rule — analogous to the "Identity Verification" guard already used elsewhere in this framework (`MULTI_AGENT_RULES.md` §4) — to avoid `C-ambiguous` collisions between concurrently-spawned OpenCode sessions in the same workspace. This fallback must be explicitly covered by a `C-ambiguous`-scenario unit test (new test, not in Rev.1's plan).
|
||||||
|
3. `.agents/skills/lib_py/agents/registry.py` — import + register `'opencode': OpenCodeAgentAdapter()`.
|
||||||
|
4. `lib.sh` — herdr kind mapping (`*-creator-opencode|*-planner-opencode|*-reviewer-opencode`), binary-name recognition tuple, `send_keys_safe` input-region delimiters.
|
||||||
|
5. Confirm `herdr agent start --kind opencode` compatibility (or fall back to `--kind generic`).
|
||||||
|
6. `tests/test_a4_adapter_contract.py` + `tests/test_tier1_unit.py` — registry, property-contract, and lifecycle coverage, mirroring the grok adapter's test additions, **plus (Rev.2)** the `C-ambiguous` fallback test from step 2 if applicable.
|
||||||
|
7. Update the 8 skill `SKILL.md` files' supported-agent lists (same set enumerated in `plan-b8872c34.md` §2.3 for the grok rollout), **plus (Rev.2)** the `num_ctx`/`MAM_IDLE_TIMEOUT` local-backend prerequisites in `BOOTSTRAP.md`/`.mam.env.example`.
|
||||||
|
|
||||||
|
**Phase 3 (defer)**: Revisit `pi` only after Open Questions §7.1 is answered by the requester; do not schedule engineering time against it in this cycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Definition of Done
|
||||||
|
|
||||||
|
- [ ] Phase 0 smoke-test results recorded (tool-call success rate for Hermes and OpenCode against GLM-5.2/Ollama, run with `num_ctx 32768`).
|
||||||
|
- [ ] **(Rev.2)** Phase 0 confirms (or rules out) an unattended/no-prompt execution flag for OpenCode.
|
||||||
|
- [ ] **(Rev.2)** Phase 0 confirms (or rules out) external `--session-id` acceptance for OpenCode; `discover()` fallback design selected accordingly.
|
||||||
|
- [ ] **(Rev.2)** Local-backend `idle_timeout_sec`/`MAM_IDLE_TIMEOUT` override value chosen from observed Phase 0 latency data and documented.
|
||||||
|
- [ ] This document (Rev.2) reviewed by at least one live Reviewer session with an explicit `[VERDICT: PASS]`.
|
||||||
|
- [ ] `pi` open questions resolved or candidate formally dropped from scope.
|
||||||
|
- [ ] If OpenCode is greenlit: `OpenCodeAgentAdapter` passes all contract tests, including the `C-ambiguous` fallback test if applicable; `pytest tests/` shows 0 regressions.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Report: Job a7a2da9f — Refined Plan (Rev.2) per `creator-agy-01` Challenge
|
||||||
|
|
||||||
|
**Durable output (updated in place)**: [.agents/reports/planner-reviewer-claude-01/plan-28f9b565.md](../../../.agents/reports/planner-reviewer-claude-01/plan-28f9b565.md)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`creator-agy-01` filed a formal architectural challenge (job `cd64ae0b`) against Rev.1's hermes audit plan, identifying that **Rev.1's own proposed §2.4 fix for the `C-ambiguous` bug was itself regressive**: it widened `reconcile.sh`'s candidate query but still relied on `adapters/hermes.py::verify_artifact()`, which checks the shared `~/.hermes/state.db` file's mtime rather than the individual session row's `started_at`. Since all hermes sessions across all workspaces share one `state.db` file, any recent write makes the file-level epoch check pass for essentially every historical session row — meaning Rev.1's fix would have turned "never detects `C-ambiguous`" into "permanently false-positives `C-ambiguous` in any workspace with prior hermes history." A second, lower-severity point noted that `hermes --resume` auto-`cd`s into its recorded `cwd`, risking divergence from MAM's symlink-canonicalized path model, and recommended adding `--no-restore-cwd`.
|
||||||
|
|
||||||
|
**Disposition**: I independently re-verified both claims against the actual code (`hermes.py::verify_artifact`'s file-mtime check; `verify_session.py`'s `epoch = row.get("herdr_session_epoch", 0)`) before accepting — both are correct. **Both accepted — no `[REBUT:]` filed.**
|
||||||
|
|
||||||
|
## What changed in Rev.2
|
||||||
|
|
||||||
|
- Added §0 changelog cross-referencing each challenge point.
|
||||||
|
- §2.4 replaced (not appended) with a two-layer fix: `verify_artifact()` now checks the session row's own `started_at` instead of the shared file's mtime, and `reconcile.sh`'s query adds a `started_at >= ?` SQL-level filter alongside the existing candidate-widening + sibling-exclusion logic from Rev.1.
|
||||||
|
- §2.1's canonical `resume_spec()` and §2.6 updated to add `--no-restore-cwd` alongside `--yolo --accept-hooks`.
|
||||||
|
- Comparison table and Definition of Done updated; the regression test requirement now explicitly covers **both** directions (under-detection and the Rev.1 over-detection regression), not just the original under-detection case.
|
||||||
|
|
||||||
|
Full Rev.2 text, including both corrected code diffs, is in the durable report linked above.
|
||||||
|
|
||||||
|
**Process note**: Per `MULTI_AGENT_RULES.md` §3.1, this Rev.2 still requires a fresh `[VERDICT: PASS]` before the §2.4 fix — the highest-risk change in this plan — is merged.
|
||||||
|
|
||||||
|
[VERDICT: N/A — planning artifact, review pending]
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# 🔍 Cross-Code Review — Hermes Agent Support Implementation, Follow-Up (Job fcb16ed6)
|
||||||
|
|
||||||
|
- **Reviewer**: `planner-reviewer-claude-01`
|
||||||
|
- **Target diff**: revised implementation of `plan-28f9b565.md`, superseding the diff reviewed in job `685bb381` (`[VERDICT: PASS]`, with 2 non-blocking findings). This diff addresses both of those findings plus adds two proactive fixes not previously flagged.
|
||||||
|
- **Method**: same as the prior review — read every changed file's live post-diff state directly (branch `support-hermes`), traced call chains, ran `bash -n` syntax checks, and ran the actual test suite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Disposition of my Previous Review's Findings (job 685bb381)
|
||||||
|
|
||||||
|
| Finding | Status this round |
|
||||||
|
|---|---|
|
||||||
|
| §4.1: `multi-agent-mux-resume/SKILL.md` doc example was stale (didn't include `--no-restore-cwd --yolo --accept-hooks`) | ✅ **Fixed.** Now reads `hermes) CMD_FULL="hermes --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;;` — verified it matches `resume_session.sh`'s real fallback string exactly (modulo the illustrative literal binary name, consistent with how the doc renders every other agent's row). |
|
||||||
|
| §4.2: `_sibling_claimed_uuids` set on the hermes row in `reconcile.sh` but never read by `hermes.py::verify_artifact()` (unlike `agy`'s adapter-level pattern) | **Not touched directly** — `hermes.py::verify_artifact()` still doesn't read `ctx.row`. This is unchanged from the prior diff and remains a minor architectural-parity note, not a live bug (the explicit loop-level `if uuid in sibling_claimed: continue` in `reconcile.sh` still enforces it correctly on its own, confirmed again this round — see §3). Still non-blocking. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. New Changes Beyond the Prior Diff (not requested by my previous review — found and verified independently)
|
||||||
|
|
||||||
|
### 2.1 `hermes.py::discover()` rewritten to return multiple candidates (previously `LIMIT 1`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def discover(self, ctx: DiscoveryContext) -> list:
|
||||||
|
...
|
||||||
|
if ctx.epoch:
|
||||||
|
rows = conn.execute("SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20", ...)
|
||||||
|
else:
|
||||||
|
rows = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 20", ...)
|
||||||
|
...
|
||||||
|
candidates = []
|
||||||
|
for (cand,) in rows:
|
||||||
|
if cand and self.verify_artifact(cand, ctx):
|
||||||
|
candidates.append(cand)
|
||||||
|
return candidates
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a genuine, previously-unflagged fix: the old `discover()` used `fetchone()`/`LIMIT 1`, making hermes the only adapter whose `discover()` could never surface more than one candidate — inconsistent with `grok.py`/`agy.py`/`cline.py`, whose `discover()` methods already return every verified candidate. **Checked for regression risk**: grepped every call site of `.discover(` across the repo — the only real caller is `workspace_uuid.py:81` (`for cand in adapter.discover(ctx):`), which already iterates rather than indexing, so it handles 0/1/many candidates identically regardless of agent. No caller assumes a single-element list. This change makes hermes consistent with the rest of the framework rather than introducing risk.
|
||||||
|
|
||||||
|
The new `test_hermes_verify_artifact_spawn_epoch_timing_f1` test specifically exercises `discover()` with a mix of old (`started_at < epoch`) and fresh rows and asserts only the fresh one survives — I re-ran it directly, passes.
|
||||||
|
|
||||||
|
### 2.2 `create_session.sh`: `HERDR_EPOCH` capture moved earlier, before `spawn()`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
+HERDR_EPOCH=$(date +%s)
|
||||||
|
+NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
+
|
||||||
|
spawn
|
||||||
|
...
|
||||||
|
-HERDR_EPOCH=$(date +%s)
|
||||||
|
-NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
```
|
||||||
|
|
||||||
|
This closes a real timing edge case in Rev.2's own epoch-based fix that neither my review nor the plan itself had caught: previously `HERDR_EPOCH` was captured **after** `spawn()`, `wait_for_tui_ready`, and pane-metadata resolution — all of which can take multiple seconds. Since `HERDR_EPOCH` becomes `herdr_session_epoch` in the registry and flows into `ctx.epoch` for the new `started_at >= ctx.epoch` check, a late-captured epoch could end up **later** than the freshly-spawned session's own `started_at`, causing the epoch guard to falsely reject the very session it was meant to validate — self-defeating. Capturing it immediately before `spawn()` guarantees `HERDR_EPOCH <= started_at` for the session about to be created.
|
||||||
|
|
||||||
|
**Verified this is safe for every agent, not just hermes**: `HERDR_EPOCH` is a single shared line (not gated by `$AGENT`), consumed generically by `verify_session_uuid`'s `epoch = row.get("herdr_session_epoch", 0)` for all agents. For claude/agy/grok, whose `verify_artifact()` implementations use `os.path.getmtime(path) < ctx.epoch`, an earlier epoch can only make that check *more* lenient (smaller epoch → less likely to be `>` a file's mtime), never *more* restrictive — so this is a strictly safe, general robustness improvement, not a hermes-only special case. `grep -rn "HERDR_EPOCH"` confirms its only consumer (`create_session.sh:346`, feeding `lib.sh`'s YAML-write step) is unaffected by the earlier capture point — it only needs the variable to exist by the time it's read, which it still does.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Re-Verification of Everything from the Prior (Already-PASSed) Review
|
||||||
|
|
||||||
|
Re-traced and re-confirmed unchanged/still-correct (identical diff hash `974a367` for `reconcile.sh` vs. the prior review — no regression risk here, but re-verified functionally with the fuller test run below):
|
||||||
|
- Core `C-ambiguous` epoch fix (`verify_artifact` row-level `started_at` check + `reconcile.sh`'s `started_at >= ?` SQL filter + sibling-exclusion loop) — still correct, still passes its dual-direction regression test.
|
||||||
|
- `--yolo --accept-hooks` / `--no-restore-cwd` wiring across `spawn_spec`/`resume_spec`/`create_session.sh`/`resume_session.sh`/both `SKILL.md` files — consistent everywhere, no drift.
|
||||||
|
- `input_prompt`/`input_placeholder`/`input_rule_pattern`/`ready_tokens` — unchanged, still correct.
|
||||||
|
|
||||||
|
`bash -n` on `create_session.sh` (re-checked after the epoch-timing edit) — valid.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Test Suite — Functional Verification
|
||||||
|
|
||||||
|
```
|
||||||
|
.venv/bin/python -m pytest tests/test_a4_adapter_contract.py -q -v
|
||||||
|
→ 19 passed (17 from before + 2 new: test_hermes_verify_artifact_spawn_epoch_timing_f1, test_hermes_reconcile_full_block_integration)
|
||||||
|
|
||||||
|
.venv/bin/python -m pytest tests/ -q
|
||||||
|
→ 441 passed in 625.89s (0:10:25), exit code 0
|
||||||
|
```
|
||||||
|
|
||||||
|
I ran the full suite myself (not the diff's own claims) rather than sampling only the adapter-contract file, given this diff touches session-creation timing (`create_session.sh`) which is broader-surface than the previous diff. **441 passed vs. 439 in the previous review — the +2 matches exactly the two new hermes regression tests added in this diff, and there are zero failures.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Findings Carried Forward (non-blocking, unchanged from job 685bb381)
|
||||||
|
|
||||||
|
- `_sibling_claimed_uuids` is still set-but-unread at the `hermes.py` adapter level (§1 above). Still recommend either removing the unused field or moving the check into `verify_artifact()` for architectural parity with `agy`'s pattern — purely a consistency/future-proofing item, not a live defect.
|
||||||
|
|
||||||
|
No new findings beyond that one. No design rework is warranted — this round demonstrably improved on the already-PASSed implementation rather than introducing regressions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Verdict
|
||||||
|
|
||||||
|
Both items from my previous review are now closed (one fully fixed, one confirmed still non-blocking and unchanged), and the two additional changes in this diff (`discover()` multi-candidate parity, `HERDR_EPOCH` pre-spawn capture) are correct, well-targeted fixes to real edge cases in the underlying epoch-based design — verified independently via call-site tracing and the full test suite, not accepted on the diff's own say-so. No correctness defects found; no escalation warranted.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Re-review: Hermes Agent Support Audit & Modernization
|
||||||
|
|
||||||
|
- **Reviewer**: `reviewer-creator-grok-01` (role: reviewer)
|
||||||
|
- **Job**: `e46a3590` (follow-up to `53ad6a4b` `[VERDICT: NOT PASS]`)
|
||||||
|
- **Branch**: `support-hermes`
|
||||||
|
- **Prior blocking issue**: F1 — `started_at >= herdr_session_epoch` rejected the session just created, because epoch was stamped after `wait_for_tui_ready`.
|
||||||
|
|
||||||
|
## Disposition of prior findings
|
||||||
|
|
||||||
|
| ID | Prior severity | This round |
|
||||||
|
|---|---|---|
|
||||||
|
| **F1** epoch stamped after TUI ready | **Blocking** | **Fixed.** `create_session.sh` now captures `HERDR_EPOCH=$(date +%s)` immediately before `spawn()`. Verified: single assignment, `spawn` follows epoch (65 chars later), after the dry-run early-exit. |
|
||||||
|
| **F2** `discover()` still `LIMIT 1` | Non-blocking leftover | **Fixed.** Epoch-filtered `LIMIT 20` (or unfiltered `LIMIT 20` when `ctx.epoch` is 0), then `verify_artifact` per row. |
|
||||||
|
| **F3** C-ambiguous test inlines SQL, does not run `reconcile.sh` | Non-blocking quality | **Partially addressed.** New `test_hermes_reconcile_full_block_integration` covers sibling exclusion with the same gather loop. Still does not exec the embedded Python in `reconcile.sh`. Acceptable residual. |
|
||||||
|
| **F4** resume `SKILL.md` missing flags | Docs nit | **Fixed.** Example now matches `resume_session.sh`. |
|
||||||
|
|
||||||
|
New regression test `test_hermes_verify_artifact_spawn_epoch_timing_f1` encodes the intended contract: pre-spawn epoch + `started_at = epoch + 0.1` verifies; `started_at = epoch - 3600` does not; `discover()` returns only the fresh id.
|
||||||
|
|
||||||
|
## What remains correct (unchanged from first pass)
|
||||||
|
|
||||||
|
- `--yolo --accept-hooks` on spawn; `--no-restore-cwd --yolo --accept-hooks` on materialized resume — matches installed hermes CLI.
|
||||||
|
- TUI facts: `ready_tokens`, `input_prompt='❯'`, `input_rule_pattern='─{10,}'`.
|
||||||
|
- `verify_artifact` uses per-row `started_at REAL` (confirmed on live `~/.hermes/state.db`).
|
||||||
|
- `reconcile.sh` hermes drift-C: `LIMIT 20` + `started_at >= ?` + sibling-claim skip — C-ambiguous is reachable.
|
||||||
|
- Create fallback `CMD_FULL` and create `SKILL.md` `hermes)` spawn example.
|
||||||
|
|
||||||
|
## Residual nits (do not block PASS)
|
||||||
|
|
||||||
|
1. **Create SKILL.md illustrative workflow** still shows `HERDR_EPOCH=$(date +%s)` after `sleep 6`. The real script is correct; the example would reintroduce F1 if copied. Follow-up docs only.
|
||||||
|
2. **`cmd_full` table** in create SKILL.md still lists only claude/agy.
|
||||||
|
3. **F3** does not drive `reconcile.sh --once --dry-run` against a fixture. The adapter + gather-loop tests are enough for this change.
|
||||||
|
4. **`find_workspace_uuid`** still constructs `DiscoveryContext` with `epoch=0`, so the resume on-disk fallback can return multiple cwd matches and emit the newest unclaimed. Running-session pinning goes through reconcile (epoch set). Not a create-path pin failure.
|
||||||
|
|
||||||
|
None of these reintroduce silent mis-pin or unattended-flag gaps.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```
|
||||||
|
pytest tests/test_a4_adapter_contract.py tests/test_tier1_unit.py \
|
||||||
|
tests/test_c1_tui_readiness.py tests/test_orc_onboard.py
|
||||||
|
→ 140 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
Earlier-epoch capture for all agents is the right semantic for transcript mtime guards (artifacts appear after spawn). No fail-closed regression observed.
|
||||||
|
|
||||||
|
No `[ESCALATE: PLANNER]`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# 📋 Code Review Report — Hermes Support Modernization Rev.3 (Job ca4539e8)
|
||||||
|
|
||||||
|
- **Reviewer**: `reviewer-hermes-01` (role: reviewer)
|
||||||
|
- **Reviewed diff**: branch `support-hermes` — 7 files, +231/−34 (working tree, unstaged) plus 2 untracked report/plan documents
|
||||||
|
- **Delta vs. previous review (job b45fb1d4, verdict PASS)**: this revision adds (1) a multi-candidate `discover()` rewrite in `hermes.py`, (2) the `HERDR_EPOCH` capture moved from post-TUI-wait to pre-spawn in `create_session.sh` (fix "F1"), (3) `multi-agent-mux-resume/SKILL.md` alignment, and (4) two new tests — `test_hermes_verify_artifact_spawn_epoch_timing_f1` and `test_hermes_reconcile_full_block_integration` ("F3").
|
||||||
|
- **Method**: static audit of every changed file, cross-layer consistency checks, full test-suite execution, shell/python syntax checks. No project files modified.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Verification Evidence (all checked, not assumed)
|
||||||
|
|
||||||
|
| # | Check | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Full test suite (`pytest tests/`) | ✅ **441 passed, 0 failures** (584.8 s) — includes the 5 hermes tests from Rev.2 plus the 2 new F1/F3 tests |
|
||||||
|
| 2 | `bash -n` on create/reconcile/resume scripts; `py_compile` on hermes.py + tests | ✅ clean |
|
||||||
|
| 3 | `HERDR_EPOCH` capture timing | ✅ now at create_session.sh:222, immediately before `spawn` (line 225). Previously captured at line ~290 (post `wait_for_tui_ready`) — i.e. potentially **after** the agent process had already inserted its `started_at` row into `state.db`, which would make the recorded `herdr_session_epoch` ≥ `started_at` and cause `verify_artifact`'s epoch filter to reject the session's own row. The move eliminates that race; the F1 test (`test_hermes_verify_artifact_spawn_epoch_timing_f1`) encodes exactly this scenario (fresh row at T0+0.1 verifies; old row at T0−3600 rejected; `discover(ctx_spawn) == ['u-fresh']`). |
|
||||||
|
| 4 | `HERDR_EPOCH` consumer consistency | ✅ only consumers are the yaml registration (line 346, via `atomic_dump_yaml`) and SKILL.md's documented workflow (updated comment matches the new placement: "stamp the epoch locally … we just spawned this session ourselves"); no other script reads `HERDR_EPOCH`, so the earlier capture cannot break downstream consumers |
|
||||||
|
| 5 | `discover()` rewrite vs. reconcile.sh drift-C | ✅ now structurally aligned: epoch-filtered multi-row query (`started_at >= ? LIMIT 20`) when `ctx.epoch` is set, unfiltered fallback when 0, per-candidate `verify_artifact()`, returns **list** (multiple candidates flow to `find_workspace_uuid`'s `emit()` which prints the first and exits — same first-match semantics as before; ambiguity surfaced by reconcile, which is the component responsible for C-ambiguous). Closes the §3.1 follow-up item from my previous review (b45fb1d4). |
|
||||||
|
| 6 | `discover()` caller contract | ✅ sole production caller is `workspace_uuid.py:81` (`adapter.discover(ctx)` → iterate/emit) — list return type matches; `DiscoveryContext.epoch` is populated by `verify_session_uuid` in discover mode, and by `find_workspace_uuid_main` the ctx carries epoch=0 (unfiltered branch), which is correct there because tier-1/running-id exclusion already disambiguates |
|
||||||
|
| 7 | Resume command three-way consistency | ✅ adapter `resume_spec()`, `resume_session.sh` fallback (line 110), **and now `multi-agent-mux-resume/SKILL.md`** (line 90) all emit `hermes --resume $UUID --no-restore-cwd --yolo --accept-hooks` |
|
||||||
|
| 8 | Spawn command three-way consistency | ✅ `spawn_spec()` / `create_session.sh:188` / `multi-agent-mux-create/SKILL.md:157` all `hermes --yolo --accept-hooks` |
|
||||||
|
| 9 | F3 sibling-exclusion test vs. reconcile.sh block | ✅ `test_hermes_reconcile_full_block_integration` replicates the block's candidate-gathering (SQL epoch filter + `_sibling_claimed_uuids` skip + `verify_session_uuid`) and asserts exactly `['u-target']` with a claimed sibling present — matches reconcile.sh:740–767 line-for-line semantics |
|
||||||
|
| 10 | Previously verified facts (unchanged in this revision) | ✅ carried over from b45fb1d4: CLI flags exist on installed hermes; `state.db` schema (`started_at REAL NOT NULL` + index); facts-emitter readiness plumbing; reconcile block = agy pattern; `verify_session_uuid` ↔ `verify_artifact` epoch-mode agreement |
|
||||||
|
|
||||||
|
## 2. Plan DoD Cross-Check (plan §5 → diff)
|
||||||
|
|
||||||
|
All b45fb1d4 DoD items remain satisfied (no regression between revisions — re-verified the adapter file end-to-end). New in this revision:
|
||||||
|
|
||||||
|
| Item | Status |
|
||||||
|
|---|---|
|
||||||
|
| `discover()` aligned with reconcile pattern (my prior review's follow-up recommendation) | ✅ adopted — multi-candidate + epoch filter, exception-safe (`except: return []`) |
|
||||||
|
| F1 spawn-epoch race fixed and regression-tested | ✅ capture moved pre-spawn; dedicated test with realistic timing (T0+0.1s fresh row) |
|
||||||
|
| F3 sibling-exclusion behavior regression-tested | ✅ dedicated test simulating the reconcile block |
|
||||||
|
| Resume SKILL.md docs gap (hermes resume line lacked flags) | ✅ updated, matches script and adapter |
|
||||||
|
|
||||||
|
## 3. Findings
|
||||||
|
|
||||||
|
No blocking defects. Non-blocking observations:
|
||||||
|
|
||||||
|
1. **[Low — edge case, fail-open]** In `verify_artifact`, a row with `started_at` NULL/0 bypasses the epoch check (`if ctx.epoch and started_at and …`). The production schema declares `started_at REAL NOT NULL`, so this is unreachable in practice (same as prior review §3.3). Behavior is fail-open by design; acceptable.
|
||||||
|
2. **[Low — cosmetic]** In `discover()`, when `ctx.epoch` is 0 the unfiltered query can return historical sessions for the same cwd; however the sole caller (`find_workspace_uuid`) already excludes running/orchestrator-claimed UUIDs and prefers per-row own-id, so mis-pinning risk is bounded. The epoch-0 branch exists precisely to serve that caller. Acceptable.
|
||||||
|
3. **[Info]** `test_hermes_reconcile_full_block_integration` re-implements the reconcile block's gathering logic in Python rather than executing reconcile.sh itself. This is a reasonable unit-level compromise (the shell integration path is covered by the tier-2/3 suites); noted for future hardening if drift between test and block is a concern.
|
||||||
|
4. **[Info]** Trailing blank lines at end of the new test functions (cosmetic only).
|
||||||
|
|
||||||
|
## 4. Regression Risk Assessment
|
||||||
|
|
||||||
|
- The `HERDR_EPOCH` move is the only behavioral change to a shared creation path, and it is strictly in the safer direction: an epoch stamped *earlier* widens (never narrows) the acceptable-candidate window, and the C-ambiguous guard remains the backstop for over-wide windows. Consumers verified — no reader expects the post-spawn value.
|
||||||
|
- `discover()`'s list-return is contract-compatible with its only caller; the epoch-0 branch preserves prior behavior for that path.
|
||||||
|
- Live corroboration: this review session itself is running on the reviewed adapter stack (spawn → onboarding → event publication → job execution) with zero stalls.
|
||||||
|
|
||||||
|
## 5. Verdict
|
||||||
|
|
||||||
|
Both previously-identified follow-ups (discover() alignment, epoch-timing race) are now fixed, the fixes carry dedicated regression tests, the full suite passes with zero regressions (441 tests), and all three layers (adapter / scripts / SKILL docs) remain mutually consistent. No new defect found.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -14,7 +14,7 @@ class HermesAgentAdapter(BaseAgentAdapter):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def ready_tokens(self) -> str:
|
def ready_tokens(self) -> str:
|
||||||
return 'Hermes'
|
return 'Hermes|Welcome to Hermes Agent|NOUS HERMES'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def exit_key(self) -> str:
|
def exit_key(self) -> str:
|
||||||
@@ -28,6 +28,18 @@ class HermesAgentAdapter(BaseAgentAdapter):
|
|||||||
def identity_cache_fields(self) -> tuple:
|
def identity_cache_fields(self) -> tuple:
|
||||||
return ('session_id',)
|
return ('session_id',)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def input_prompt(self) -> str:
|
||||||
|
return '❯'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def input_placeholder(self) -> str:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def input_rule_pattern(self) -> str:
|
||||||
|
return '─{10,}'
|
||||||
|
|
||||||
def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str:
|
def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str:
|
||||||
return f"{ctx.home_dir}/.hermes/sessions/session_{uuid}.json"
|
return f"{ctx.home_dir}/.hermes/sessions/session_{uuid}.json"
|
||||||
|
|
||||||
@@ -35,15 +47,15 @@ class HermesAgentAdapter(BaseAgentAdapter):
|
|||||||
hdb = f"{ctx.home_dir}/.hermes/state.db"
|
hdb = f"{ctx.home_dir}/.hermes/state.db"
|
||||||
if not os.path.exists(hdb):
|
if not os.path.exists(hdb):
|
||||||
return False
|
return False
|
||||||
if ctx.epoch and os.path.getmtime(hdb) < ctx.epoch:
|
|
||||||
return False
|
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(hdb)
|
conn = sqlite3.connect(hdb)
|
||||||
r = conn.execute("SELECT cwd FROM sessions WHERE id=?", (uuid,)).fetchone()
|
r = conn.execute("SELECT cwd, started_at FROM sessions WHERE id=?", (uuid,)).fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not r:
|
if not r:
|
||||||
return False
|
return False
|
||||||
found_cwd = r[0]
|
found_cwd, started_at = r[0], r[1]
|
||||||
|
if ctx.epoch and started_at and float(started_at) < float(ctx.epoch):
|
||||||
|
return False
|
||||||
if found_cwd and workspace_key(found_cwd) != workspace_key(ctx.cwd):
|
if found_cwd and workspace_key(found_cwd) != workspace_key(ctx.cwd):
|
||||||
return False
|
return False
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -70,27 +82,37 @@ class HermesAgentAdapter(BaseAgentAdapter):
|
|||||||
return purged
|
return purged
|
||||||
|
|
||||||
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
|
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
|
||||||
return binary
|
return f"{binary} --yolo --accept-hooks"
|
||||||
|
|
||||||
def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str:
|
def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str:
|
||||||
if materialized and session_uuid:
|
if materialized and session_uuid:
|
||||||
return f"{binary} --resume {session_uuid}"
|
return f"{binary} --resume {session_uuid} --no-restore-cwd --yolo --accept-hooks"
|
||||||
return binary
|
return f"{binary} --yolo --accept-hooks"
|
||||||
|
|
||||||
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
|
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def discover(self, ctx: DiscoveryContext) -> list:
|
def discover(self, ctx: DiscoveryContext) -> list:
|
||||||
hdb = f"{ctx.home_dir}/.hermes/state.db"
|
hdb = f"{ctx.home_dir}/.hermes/state.db"
|
||||||
if os.path.exists(hdb):
|
if not os.path.exists(hdb):
|
||||||
try:
|
return []
|
||||||
conn = sqlite3.connect(hdb)
|
try:
|
||||||
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (ctx.workspace,)).fetchone()
|
conn = sqlite3.connect(hdb)
|
||||||
conn.close()
|
if ctx.epoch:
|
||||||
if r:
|
rows = conn.execute(
|
||||||
cand = r[0]
|
"SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20",
|
||||||
if cand and self.verify_artifact(cand, ctx):
|
(ctx.workspace, ctx.epoch)
|
||||||
return [cand]
|
).fetchall()
|
||||||
except Exception:
|
else:
|
||||||
pass
|
rows = conn.execute(
|
||||||
return []
|
"SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 20",
|
||||||
|
(ctx.workspace,)
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
candidates = []
|
||||||
|
for (cand,) in rows:
|
||||||
|
if cand and self.verify_artifact(cand, ctx):
|
||||||
|
candidates.append(cand)
|
||||||
|
return candidates
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|||||||
@@ -154,6 +154,9 @@ case "$AGENT" in
|
|||||||
agy)
|
agy)
|
||||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "agy --dangerously-skip-permissions"
|
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "agy --dangerously-skip-permissions"
|
||||||
;;
|
;;
|
||||||
|
hermes)
|
||||||
|
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "hermes --yolo --accept-hooks"
|
||||||
|
;;
|
||||||
grok)
|
grok)
|
||||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "grok --permission-mode bypassPermissions"
|
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "grok --permission-mode bypassPermissions"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ if [ -z "$CMD_FULL" ]; then
|
|||||||
case "$AGENT" in
|
case "$AGENT" in
|
||||||
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --session-id ${SESSION_UUID}" ;;
|
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --session-id ${SESSION_UUID}" ;;
|
||||||
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
|
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
|
||||||
hermes) CMD_FULL="${RESOLVED_BIN}" ;;
|
hermes) CMD_FULL="${RESOLVED_BIN} --yolo --accept-hooks" ;;
|
||||||
cline) CMD_FULL="${RESOLVED_BIN} -i" ;;
|
cline) CMD_FULL="${RESOLVED_BIN} -i" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
@@ -219,6 +219,9 @@ if [ "$DRY_RUN" = "1" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
HERDR_EPOCH=$(date +%s)
|
||||||
|
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
|
||||||
spawn
|
spawn
|
||||||
|
|
||||||
# Trap for rolling back/cleaning up herdr session if script exits due to error
|
# Trap for rolling back/cleaning up herdr session if script exits due to error
|
||||||
@@ -287,8 +290,6 @@ fi
|
|||||||
PANE_PID=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "")
|
PANE_PID=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "")
|
||||||
PANE_CWD=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_path}' 2>/dev/null || echo "$WORKSPACE")
|
PANE_CWD=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_path}' 2>/dev/null || echo "$WORKSPACE")
|
||||||
PANE_CMD=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_command}' 2>/dev/null || echo "$AGENT")
|
PANE_CMD=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_command}' 2>/dev/null || echo "$AGENT")
|
||||||
HERDR_EPOCH=$(date +%s)
|
|
||||||
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
|
|
||||||
# 시작 명령
|
# 시작 명령
|
||||||
# NOTE: this must match what `spawn()` actually ran above — env-var-driven
|
# NOTE: this must match what `spawn()` actually ran above — env-var-driven
|
||||||
|
|||||||
@@ -737,14 +737,31 @@ for s in d.get('herdr_sessions', []):
|
|||||||
if not os.path.exists(hdb):
|
if not os.path.exists(hdb):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
epoch_threshold = s.get('herdr_session_epoch', 0)
|
||||||
|
|
||||||
|
sibling_claimed = [
|
||||||
|
other.get('hermes_conversation_id_own')
|
||||||
|
for other in d.get('herdr_sessions', [])
|
||||||
|
if other is not s
|
||||||
|
and (other.get('pane') or {}).get('cwd') == cwd
|
||||||
|
and other.get('status') not in ('stopped', 'terminated')
|
||||||
|
and other.get('hermes_conversation_id_own')
|
||||||
|
]
|
||||||
|
s_eval = dict(s)
|
||||||
|
s_eval['_sibling_claimed_uuids'] = sibling_claimed
|
||||||
|
|
||||||
valid_candidates = []
|
valid_candidates = []
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(hdb)
|
conn = sqlite3.connect(hdb)
|
||||||
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (cwd,)).fetchone()
|
rows = conn.execute(
|
||||||
|
"SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20",
|
||||||
|
(cwd, epoch_threshold)
|
||||||
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
if r:
|
for (uuid,) in rows:
|
||||||
uuid = r[0]
|
if uuid in (sibling_claimed or []):
|
||||||
if verify_session_uuid(cwd, 'hermes', uuid, s, mode="discover"):
|
continue
|
||||||
|
if verify_session_uuid(cwd, 'hermes', uuid, s_eval, mode="discover"):
|
||||||
valid_candidates.append(uuid)
|
valid_candidates.append(uuid)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ fi
|
|||||||
case "$AGENT" in
|
case "$AGENT" in
|
||||||
claude) CMD_FULL="claude --dangerously-skip-permissions $CLAUDE_ID_FLAG $UUID" ;;
|
claude) CMD_FULL="claude --dangerously-skip-permissions $CLAUDE_ID_FLAG $UUID" ;;
|
||||||
agy) CMD_FULL="agy --dangerously-skip-permissions --conversation $UUID" ;;
|
agy) CMD_FULL="agy --dangerously-skip-permissions --conversation $UUID" ;;
|
||||||
hermes) CMD_FULL="hermes --resume $UUID" ;;
|
hermes) CMD_FULL="hermes --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;;
|
||||||
cline) CMD_FULL="cline -i --id $UUID" ;;
|
cline) CMD_FULL="cline -i --id $UUID" ;;
|
||||||
grok) CMD_FULL="grok --resume $UUID --permission-mode bypassPermissions" ;;
|
grok) CMD_FULL="grok --resume $UUID --permission-mode bypassPermissions" ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ if [ -z "$CMD_FULL" ]; then
|
|||||||
case "$AGENT" in
|
case "$AGENT" in
|
||||||
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions -r $UUID" ;;
|
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions -r $UUID" ;;
|
||||||
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --conversation $UUID" ;;
|
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --conversation $UUID" ;;
|
||||||
hermes) CMD_FULL="${RESOLVED_BIN} --resume $UUID" ;;
|
hermes) CMD_FULL="${RESOLVED_BIN} --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;;
|
||||||
cline) CMD_FULL="${RESOLVED_BIN} -i --id $UUID" ;;
|
cline) CMD_FULL="${RESOLVED_BIN} -i --id $UUID" ;;
|
||||||
grok) CMD_FULL="${RESOLVED_BIN} --resume $UUID --permission-mode bypassPermissions" ;;
|
grok) CMD_FULL="${RESOLVED_BIN} --resume $UUID --permission-mode bypassPermissions" ;;
|
||||||
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
|
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ def test_adapter_required_properties():
|
|||||||
expected = {
|
expected = {
|
||||||
'claude': ('Anthropic|Assistant|Chat|Welcome|Claude Code|Opus|Sonnet|Haiku', '/exit', 'claude-code', ('session_id', 'session_jsonl', 'session_size_bytes', 'session_lines')),
|
'claude': ('Anthropic|Assistant|Chat|Welcome|Claude Code|Opus|Sonnet|Haiku', '/exit', 'claude-code', ('session_id', 'session_jsonl', 'session_size_bytes', 'session_lines')),
|
||||||
'agy': ('Antigravity', 'Exit', 'antigravity-cli', ('conversation_id', 'conversation_db', 'conversation_brain_dir')),
|
'agy': ('Antigravity', 'Exit', 'antigravity-cli', ('conversation_id', 'conversation_db', 'conversation_brain_dir')),
|
||||||
'hermes': ('Hermes', '/exit', 'hermes-agent', ('session_id',)),
|
'hermes': ('Hermes|Welcome to Hermes Agent|NOUS HERMES', '/exit', 'hermes-agent', ('session_id',)),
|
||||||
'cline': ('Cline|history|Chat|What can I do|slash commands', '/exit', 'cline-agent', ('session_id',)),
|
'cline': ('Cline|history|Chat|What can I do|slash commands', '/exit', 'cline-agent', ('session_id',)),
|
||||||
'grok': ('Grok|xAI|Assistant|❯|>>>', '/exit', 'grok-build', ('session_id', 'session_jsonl')),
|
'grok': ('Grok|xAI|Assistant|❯|>>>', '/exit', 'grok-build', ('session_id', 'session_jsonl')),
|
||||||
}
|
}
|
||||||
@@ -223,8 +223,9 @@ def test_adapter_spawn_and_resume_specs():
|
|||||||
assert agy.resume_spec('agy', 'u1', materialized=True) == 'agy --dangerously-skip-permissions --conversation u1'
|
assert agy.resume_spec('agy', 'u1', materialized=True) == 'agy --dangerously-skip-permissions --conversation u1'
|
||||||
|
|
||||||
hermes = get_adapter('hermes')
|
hermes = get_adapter('hermes')
|
||||||
assert hermes.spawn_spec('hermes', 'u1') == 'hermes'
|
assert hermes.spawn_spec('hermes', 'u1') == 'hermes --yolo --accept-hooks'
|
||||||
assert hermes.resume_spec('hermes', 'u1', materialized=True) == 'hermes --resume u1'
|
assert hermes.resume_spec('hermes', 'u1', materialized=True) == 'hermes --resume u1 --no-restore-cwd --yolo --accept-hooks'
|
||||||
|
assert hermes.resume_spec('hermes', 'u1', materialized=False) == 'hermes --yolo --accept-hooks'
|
||||||
|
|
||||||
cline = get_adapter('cline')
|
cline = get_adapter('cline')
|
||||||
assert cline.spawn_spec('cline', 'u1') == 'cline -i'
|
assert cline.spawn_spec('cline', 'u1') == 'cline -i'
|
||||||
@@ -397,3 +398,156 @@ def test_wait_for_tui_ready_missing_tokens_diagnostic(mam_sandbox):
|
|||||||
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
|
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
|
||||||
assert res.returncode != 0
|
assert res.returncode != 0
|
||||||
assert "no ready tokens for agent 'bogus-agent'" in res.stderr
|
assert "no ready tokens for agent 'bogus-agent'" in res.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_hermes_input_region_properties():
|
||||||
|
adapter = get_adapter('hermes')
|
||||||
|
assert adapter.input_prompt == '❯'
|
||||||
|
assert adapter.input_placeholder == ''
|
||||||
|
assert adapter.input_rule_pattern == '─{10,}'
|
||||||
|
|
||||||
|
|
||||||
|
def test_hermes_verify_artifact_started_at_epoch_filtering(tmp_path):
|
||||||
|
import sqlite3
|
||||||
|
from lib_py.agents.base import DiscoveryContext
|
||||||
|
ws = str(tmp_path / "ws")
|
||||||
|
home = str(tmp_path / "home")
|
||||||
|
os.makedirs(ws, exist_ok=True)
|
||||||
|
os.makedirs(home, exist_ok=True)
|
||||||
|
|
||||||
|
hdb = f"{home}/.hermes/state.db"
|
||||||
|
os.makedirs(os.path.dirname(hdb), exist_ok=True)
|
||||||
|
conn = sqlite3.connect(hdb)
|
||||||
|
conn.execute("CREATE TABLE sessions (id TEXT, cwd TEXT, started_at REAL)")
|
||||||
|
conn.execute("INSERT INTO sessions VALUES ('u-old', ?, 1000.0)", (ws,))
|
||||||
|
conn.execute("INSERT INTO sessions VALUES ('u-new', ?, 2000.0)", (ws,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
adapter = get_adapter('hermes')
|
||||||
|
ctx_old = DiscoveryContext(workspace=ws, agent_name='hermes', home_dir=home, epoch=1500)
|
||||||
|
assert adapter.verify_artifact('u-old', ctx_old) is False
|
||||||
|
assert adapter.verify_artifact('u-new', ctx_old) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_hermes_reconcile_dual_direction_epoch_guards(tmp_path):
|
||||||
|
import sqlite3
|
||||||
|
from lib_py.agents.base import DiscoveryContext
|
||||||
|
from lib_py.verify_session import verify_session_uuid
|
||||||
|
ws = str(tmp_path / "ws")
|
||||||
|
home = str(tmp_path / "home")
|
||||||
|
os.makedirs(ws, exist_ok=True)
|
||||||
|
os.makedirs(home, exist_ok=True)
|
||||||
|
|
||||||
|
hdb = f"{home}/.hermes/state.db"
|
||||||
|
os.makedirs(os.path.dirname(hdb), exist_ok=True)
|
||||||
|
conn = sqlite3.connect(hdb)
|
||||||
|
conn.execute("CREATE TABLE sessions (id TEXT, cwd TEXT, started_at REAL)")
|
||||||
|
# Direction 1: Old historical session (started_at=1000) + 1 new session (started_at=2000)
|
||||||
|
conn.execute("INSERT INTO sessions VALUES ('u-hist', ?, 1000.0)", (ws,))
|
||||||
|
conn.execute("INSERT INTO sessions VALUES ('u-fresh1', ?, 2000.0)", (ws,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
epoch = 1500
|
||||||
|
s_row = {'name': 'sess1', 'pane': {'cwd': ws}, 'herdr_session_epoch': epoch}
|
||||||
|
|
||||||
|
# Query with epoch filter (Rev.2 pattern)
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20",
|
||||||
|
(ws, epoch)
|
||||||
|
).fetchall()
|
||||||
|
valid = [u for (u,) in rows if verify_session_uuid(ws, 'hermes', u, s_row, home_dir=home, mode="discover")]
|
||||||
|
assert valid == ['u-fresh1']
|
||||||
|
assert len(valid) == 1 # Exactly 1, no false C-ambiguous!
|
||||||
|
|
||||||
|
# Direction 2: Add a second concurrent fresh session (started_at=2100) -> must detect C-ambiguous
|
||||||
|
conn.execute("INSERT INTO sessions VALUES ('u-fresh2', ?, 2100.0)", (ws,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
rows2 = sqlite3.connect(hdb).execute(
|
||||||
|
"SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20",
|
||||||
|
(ws, epoch)
|
||||||
|
).fetchall()
|
||||||
|
valid2 = [u for (u,) in rows2 if verify_session_uuid(ws, 'hermes', u, s_row, home_dir=home, mode="discover")]
|
||||||
|
assert len(valid2) == 2 # Ambiguous detected correctly!
|
||||||
|
|
||||||
|
|
||||||
|
def test_hermes_verify_artifact_spawn_epoch_timing_f1(tmp_path):
|
||||||
|
"""F1 Regression: Ensure spawn-time epoch capture allows freshly started session to be verified."""
|
||||||
|
import sqlite3, time
|
||||||
|
from lib_py.agents.base import DiscoveryContext
|
||||||
|
ws = str(tmp_path / "ws")
|
||||||
|
home = str(tmp_path / "home")
|
||||||
|
os.makedirs(ws, exist_ok=True)
|
||||||
|
os.makedirs(home, exist_ok=True)
|
||||||
|
|
||||||
|
# 1. Pre-spawn epoch capture (T0)
|
||||||
|
spawn_epoch = int(time.time())
|
||||||
|
|
||||||
|
# 2. Process spawns and writes session row (T0 + 0.1s)
|
||||||
|
session_started_at = spawn_epoch + 0.1
|
||||||
|
hdb = f"{home}/.hermes/state.db"
|
||||||
|
os.makedirs(os.path.dirname(hdb), exist_ok=True)
|
||||||
|
conn = sqlite3.connect(hdb)
|
||||||
|
conn.execute("CREATE TABLE sessions (id TEXT, cwd TEXT, started_at REAL)")
|
||||||
|
conn.execute("INSERT INTO sessions VALUES ('u-fresh', ?, ?)", (ws, session_started_at))
|
||||||
|
conn.execute("INSERT INTO sessions VALUES ('u-old', ?, ?)", (ws, spawn_epoch - 3600.0))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
adapter = get_adapter('hermes')
|
||||||
|
# Context with pre-spawn epoch
|
||||||
|
ctx_spawn = DiscoveryContext(workspace=ws, agent_name='hermes', home_dir=home, epoch=spawn_epoch)
|
||||||
|
assert adapter.verify_artifact('u-fresh', ctx_spawn) is True
|
||||||
|
assert adapter.verify_artifact('u-old', ctx_spawn) is False
|
||||||
|
|
||||||
|
# Discover with pre-spawn epoch returns the fresh candidate only
|
||||||
|
assert adapter.discover(ctx_spawn) == ['u-fresh']
|
||||||
|
|
||||||
|
|
||||||
|
def test_hermes_reconcile_full_block_integration(tmp_path):
|
||||||
|
"""F3: Verify hermes drift-C block behavior with sibling_claimed exclusion."""
|
||||||
|
import sqlite3
|
||||||
|
from lib_py.agents.base import DiscoveryContext
|
||||||
|
from lib_py.verify_session import verify_session_uuid
|
||||||
|
ws = str(tmp_path / "ws")
|
||||||
|
home = str(tmp_path / "home")
|
||||||
|
os.makedirs(ws, exist_ok=True)
|
||||||
|
os.makedirs(home, exist_ok=True)
|
||||||
|
|
||||||
|
hdb = f"{home}/.hermes/state.db"
|
||||||
|
os.makedirs(os.path.dirname(hdb), exist_ok=True)
|
||||||
|
conn = sqlite3.connect(hdb)
|
||||||
|
conn.execute("CREATE TABLE sessions (id TEXT, cwd TEXT, started_at REAL)")
|
||||||
|
conn.execute("INSERT INTO sessions VALUES ('u-claimed', ?, 2000.0)", (ws,))
|
||||||
|
conn.execute("INSERT INTO sessions VALUES ('u-target', ?, 2001.0)", (ws,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
epoch = 1900
|
||||||
|
s_eval = {
|
||||||
|
'name': 'hermes-target-01',
|
||||||
|
'pane': {'cwd': ws},
|
||||||
|
'herdr_session_epoch': epoch,
|
||||||
|
'_sibling_claimed_uuids': ['u-claimed']
|
||||||
|
}
|
||||||
|
|
||||||
|
# Simulate reconcile.sh candidate gathering
|
||||||
|
conn = sqlite3.connect(hdb)
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20",
|
||||||
|
(ws, epoch)
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
valid_candidates = []
|
||||||
|
for (uuid,) in rows:
|
||||||
|
if uuid in s_eval['_sibling_claimed_uuids']:
|
||||||
|
continue
|
||||||
|
if verify_session_uuid(ws, 'hermes', uuid, s_eval, home_dir=home, mode="discover"):
|
||||||
|
valid_candidates.append(uuid)
|
||||||
|
|
||||||
|
assert valid_candidates == ['u-target']
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user