Commit Graph

3195 Commits

Author SHA1 Message Date
liuhao1024 00a68a8768 fix(browser): bypass proxies for loopback hosts in browser child envs
websockets>=14 defaults to proxy=True and resolves proxies via
urllib.request.getproxies(), which reads the macOS/Windows system
proxy config even with no *_proxy env vars set. Local CDP endpoints
(ws://127.0.0.1:<port>/devtools/...) were therefore dialed through
the system proxy and the handshake failed with "did not receive a
valid HTTP response" (#110565).

Append 127.0.0.1/localhost/::1 to NO_PROXY/no_proxy (both casings)
in _build_browser_env so every browser subprocess (Browser Use CLI,
agent-browser, Chromium, Lightpanda) bypasses proxies for loopback.
Operator-provided NO_PROXY entries are preserved.

Fixes #110565
2026-09-15 06:16:38 -07:00
Kevin Rajan 7d292af875 fix(journey): show foreground-created skills in /journey via learn provenance marker
record_created now stamps created_by="learn" on foreground creates
(e.g. /learn) instead of leaving it unset, and the learning-graph filter
honors "learn" alongside "agent"/used. "learn" is a learning-signal
marker only: curator management stays keyed strictly on "agent"
(_is_curator_managed_record), so user-taught skills appear in /journey
without becoming eligible for autonomous curation.

Fixes #111317.

---
authored with AI assistance (Muse, Meta's Muse Spark) under the contributor's direction; the contributor reviewed the diff and ran the tests.
2026-09-15 05:38:31 -07:00
teknium1 8ecc3dd71e fix(stt): treat a partial whisper cache as a cache miss
An interrupted first download leaves refs/main plus a snapshot folder
without model.bin. snapshot_download(local_files_only=True) returns that
folder rather than raising LocalEntryNotFoundError, so ctranslate2 fails
with RuntimeError "Unable to open file 'model.bin'" and the online path
never ran, leaving STT permanently broken with a misleading error. Fall
through to the download when the local attempt fails that way. The
loading tests are also gated on faster_whisper being installed, so the
module collects when the voice extra is absent.

Review finding: partial-cache RuntimeError bypassed the online fallback.
2026-09-15 05:36:37 -07:00
teknium1 0bb460b488 fix(stt): tolerate a missing huggingface_hub when checking the whisper cache
The cache-first loader imported LocalEntryNotFoundError unconditionally; in
environments without the optional huggingface_hub dependency every local
transcription raised ModuleNotFoundError. Resolve the cache-miss exception
lazily and fall back to its OSError base class when the package is absent.
2026-09-15 05:36:37 -07:00
teknium1 489ad83907 fix(stt): only rewrap Hub/network failures on the whisper download path
The salvaged fallback caught bare Exception around the online load, which
would relabel a CUDA runtime error or an invalid model size as a download
problem and defeat the CUDA → CPU fallback above it. huggingface_hub raises
every network/Hub failure as an OSError subclass (LocalEntryNotFoundError,
HfHubHTTPError), so catch that class only. The negative test now feeds the
real LocalEntryNotFoundError('Got: ConnectTimeout ...') shape the reporter
saw instead of a synthetic RuntimeError.
2026-09-15 05:36:37 -07:00
fangliquan ebb6dc6e70 fix(stt): prefer cached local whisper models 2026-09-15 05:36:37 -07:00
teknium1 bae9f8ab85 fix(file-sync): sweep stale sync-back dirs too and tighten the stale window to 6 h
The salvaged commit only swept `hermes-sync-back-*.tar`. The extraction
staging dir (`tempfile.TemporaryDirectory(prefix="hermes-sync-back-")`) is
leaked by the same hard kill, so the sweep now reclaims both shapes and
returns the count. The window drops from 24 h to 6 h (the reporter's
value): archives appear every few minutes on a busy gateway and a live
transfer is never hours old. Tests trimmed to two invariants: the sweep
touches only stale prefixed entries, and a real sync_back reclaims a
leaked archive while writing its own tar under the identifiable prefix.
2026-09-15 05:35:34 -07:00
KoNit-K 6a03d5a94c fix(file-sync): clean stale sync-back archives 2026-09-15 05:35:34 -07:00
teknium1 6f87575009 test(approval): keep two invariants for unattended allowlist keys; document rule keys
Drop the session-only negative (session grants were never consulted on the unattended
path, so it pins pre-existing behaviour rather than the fix). Keep the permanent-key
positive and the Tirith-not-bypassed negative. Docs: command_allowlist rule keys are
honored in cron/-q/unattended sessions.
2026-09-15 05:33:41 -07:00
Hukla 05e7e89175 fix(approval): honor pattern-key allowlists when unattended 2026-09-15 05:33:41 -07:00
fangliquan 729dbc825b test(skills): cover package-owned markdown lookup 2026-09-15 05:32:14 -07:00
fangliquan 7851d1d3ea fix(skills): ignore package-owned legacy markdown 2026-09-15 05:32:14 -07:00
teknium1 5117e3b3a0 fix: key the check_fn cache by the same served-profile predicate as the MCP registry scope
_mcp_registry_scope() became profile-keyed for served profiles with the
multiplex flag off, but check_fn_cache_scope() still returned None in that
mode, so the process-wide availability cache stayed keyed (fn, None) across
profiles. A served profile whose mcp__x__* check_fn now correctly resolves to
its own (absent) connection cached False for the TTL window and the launch
profile that owns the live connection lost its tools for that window.

Both sites now call one helper, agent.secret_scope.serves_routed_profile()
(multiplex on, or a HERMES_HOME override naming a home other than the
process home), so the registry scope and the cache key can no longer drift.

Review finding: served profile B's check_fn verdict shadowed the launch profile's live mcp tools via the unscoped check_fn cache.
2026-09-15 04:56:40 -07:00
teknium1 57c4e1d963 fix(mcp): served profiles keep their own MCP connections without the multiplex flag
A dashboard/desktop backend (and the per-profile cron ticker) serves sessions of
several profiles through the HERMES_HOME contextvar override while
gateway.multiplex_profiles stays off. _mcp_registry_scope() keyed every MCP
connection by the bare server name in that mode, so the first profile to
discover `zernio` owned the only connection and every later served profile —
including one whose config carries a different Authorization header — called
the server through it and got the other account's data back (#111151).

The registry scope now follows the served home: a routed profile (an override
naming a home other than the process home) gets the same per-profile overlay
the multiplexer uses, so a same-named server with other credentials is a
separate connection, discovery for profile B is a connect candidate instead of
"already connected", and status/tool views stay per profile. Single-profile
processes (no override) keep bare keys, byte-identical to before.

Fixes #111151

Credit: #111158 by @KoNit-K located the inert flag on hermes_cli surfaces; its
fix (activating fail-closed multiplex secret scoping from config.yaml on the
dashboard) is not taken — the connection-key seam, not the secret-scope mode,
is what leaks the connection, and flipping the process-wide mode from the
dashboard would change credential resolution for every code path in it.
2026-09-15 04:56:40 -07:00
liuhao1024 5fff41e52e fix(vault): bind manager logins to every saved web origin
1Password/Bitwarden items can carry several websites, but both backends
collapsed the item's urls[]/uris[] to the first origin that normalizes,
so browser_vault_fill refused every other explicitly saved origin with
origin_mismatch. Reordering the URLs in the manager just moved which
single origin worked.

VaultItemMeta now carries allowed_origins (every normalized, deduped
web origin; origin stays the first/primary one). Fill matching stays
exact-origin against that list — no wildcard, parent-domain or subdomain
inference — and the in-page synchronous check pins the origin actually
matched via build_fill_js(expected_origin=page_origin). App URIs such as
androidapp:// never widen the fill set.
2026-09-15 04:56:01 -07:00
liuhao1024 ddd084a24c test(mcp): cover a properties-map entry literally named required (#110530) 2026-09-15 04:55:14 -07:00
liuhao1024 798cc60f4c fix(mcp): repair schema-map keywords per-entry in _repair_object_shape
_repair_object_shape recursed over every dict value as a schema node,
including the properties map itself. When one of the map's keys was
literally named properties/required, the missing-type heuristic fired on
the map and injected a bogus "type": "object" string as a parameter,
400ing the whole tool array on strict providers.

Recurse into map values only for the mapping-valued keywords
(properties, patternProperties, $defs, definitions, dependentSchemas),
matching the in-tree precedent in _rewrite_local_refs.

Fixes #110530
2026-09-15 04:55:14 -07:00
KoNit-K 20d80bb36f fix(mcp): refresh expired cold-loaded OAuth tokens 2026-09-15 04:54:30 -07:00
teknium1 d94f13fe35 test(vision): keep two invariant aggregator-veto tests (tool-result downgrade + fast-path veto) 2026-09-15 04:48:20 -07:00
KoNit-K 04060eae2a fix(vision): honor routed tool message veto 2026-09-15 04:48:20 -07:00
Teknium a654de291a Merge pull request #111844 from NousResearch/fix/cron-script-not-found-hint
Cron 'Script not found' explains per-profile lookup and the fix (#94821, salvage #105775)
2026-09-15 04:26:33 -07:00
teknium1 0ea0c53e89 fix: record a known exit status when the reader's wait() raises
The early-EOF reaper made _finish_reader return without publishing when
wait() raises, so the session stays tracked for later reconciliation.
That is right for the pipe path (_reconcile_local_exit can still reap via
session.process), but PTY sessions have no session.process: when
ptyprocess.wait raises (waitpid ECHILD after isalive() already reaped the
child) the exitstatus is known, yet poll() reported "running" forever.

Only leave the session tracked when exit_code() is still None; otherwise
record the known status and finish as before.

Review finding: PTY session whose pty.wait raises stays in _running forever (fail-open regression vs main)
2026-09-15 04:23:32 -07:00
teknium1 dc4a2aeb10 test(process): trim early-EOF reaper test to its two invariants
Drop the wait-timeout call-shape assertion; the invariants are that the
session records the real exit code and that a failed reap does not publish a
false completion.
2026-09-15 04:23:32 -07:00
fangliquan 1bfbeff4e5 fix(process): reap children after early stdout EOF 2026-09-15 04:23:32 -07:00
teknium1 9efa50071a fix: match find/read-tool dynamic words only as unquoted command arguments
The dynamic-shell-word rules fired on any `-del*`/`-exec*` substring after a
`find` anywhere in the segment, so quoted predicate arguments the shell never
expands were flagged: `find . -name 'log-del*'`, `find . -name 'pre-exec*.sh'`,
`find src -path '*-exec[0-9]*'`, and `echo find . -{delete,print}`.

Three changes close that:
- `find` must be the command word (_CMDPOS anchored, same as mkfs/rm/dd) and
  the dynamic word must start a whitespace-delimited token (`(?<!\S)`), for
  both the find rule and the rg/sort/ag/man program-option rule.
- Both rules now scan the quote-masked variant (_QUOTE_MASKED_DANGEROUS_
  DESCRIPTIONS, same _mask_quoted_prose used by the positionless hardline
  rules) so glob characters inside quotes are data, not expansion.
- _iter_shell_command_starts no longer treats the `{` inside a brace-expansion
  word (`-{delete,print}`) as a brace-group opener; it split the word across a
  marked start so `echo x; find . -{delete,print}` matched nothing. A brace
  group opener is `{` as its own word (after whitespace or a separator).

The quoted-name cases join the inert parametrized test; the separator case
joins the dangerous one. Still two parametrized functions.
2026-09-15 04:17:42 -07:00
Teknium 06a3a98751 fix: gate dynamic shell words in approval checks
Port from openai/codex#39159: require approval when shell expansion could synthesize destructive find flags or program-executing read-tool options.
2026-09-15 04:17:42 -07:00
chelsealong 7ecbe50d98 fix(cron): validate script existence and fix profile-aware path in error messages
_validate_cron_script_path only checked that a no_agent/monitor script path
was relative and contained within scripts_dir, never that the file actually
existed — so a misplaced script registered fine and only failed at every
fire with a generic "Script not found" from scheduler_script.py, dropping
once-only jobs from jobs.json on failure.

The error messages also hardcoded "~/.hermes/scripts/" even though
resolution goes through get_hermes_home(), which is per-profile — actively
misleading users running non-default profiles into placing scripts in the
wrong directory.

Now the validator resolves the file and returns a "Script file not found"
error naming the actual resolved scripts_dir, surfacing the mistake at
creation time instead of at every scheduled fire.
2026-09-15 04:13:47 -07:00
teknium1 ef44c1b73f fix: restore TestBridgeDispatch, trim #39797 tests, drop dead execution_guidance_text param
Why: the rebase conflict resolution in tests/tools/test_model_tools.py
deleted the unrelated TestBridgeDispatch class (3 tests from 73163e3);
it is restored verbatim from main with TestBrowserRetrievalHints after it.

The fix had five tests for one invariant: the OPENAI_MODEL_EXECUTION_GUIDANCE
check duplicated test_phantom_tool_references, and the two static-schema
"toolset-neutral" checks are now folded into test_silent_without_web_tools,
which runs _apply_dynamic_schemas over the real browser_navigate/browser_cdp
schemas so the rendered descriptions are what is asserted.

execution_guidance_text() no longer takes valid_tool_names: the guidance is
toolset-neutral, so the parameter was ignored; the single caller in
agent/system_prompt.py and its test are updated.
2026-09-15 04:13:13 -07:00
teknium1 ac63d0eea5 fix(agent): execution guidance and browser hints drop the web_search stripper; tests assert the invariant
The rebased guidance text no longer names web_search anywhere, so
execution_guidance_text()'s replace() calls (3733e4aff5) matched
nothing and were dead; the function now returns the neutral text for
every toolset and its phantom-tool test asserts "no web tool named"
instead of the removed sentence. model_tools ports the PR's hint layer
into main's _DYNAMIC_SCHEMA_REWRITERS table (browser_navigate +
browser_cdp) rather than a second pass after it.

Tests: the two browser_cdp registry tests were re-added by the PR but
main pruned them in 39975613b13b4; replaced with one schema-neutrality
invariant. Exact-wording assertions ("lightweight retrieval tool",
"appropriate permitted retrieval/search tool") were change detectors and
are dropped. tools-reference.md row updated to the new schema text.
2026-09-15 04:13:13 -07:00
Kevin Yin ccf380f634 fix(agent): respect permitted web retrieval guidance 2026-09-15 04:13:13 -07:00
teknium1 6dc6c92ea2 fix: release the profile MCP stderr handle before rename too
rename_profile moves the profile directory while this process may still
hold the cached per-profile mcp-stderr.log handle (left behind by a
completed probe or a running server). On Windows a directory containing
an open file cannot be renamed, the same WinError class delete_profile
now avoids. On other platforms the stale handle stayed cached under the
old home key, so a later probe on the renamed profile opened a second
handle and a new profile re-created under the old name wrote its MCP
stderr into the renamed profile's log. Release the scoped handle next to
the multiplexer unroute, mirroring delete_profile.

Review finding: rename_profile missed the sibling surface of the
delete_profile handle release.
2026-09-15 04:12:41 -07:00
tarkilhk 68dea35e0e fix(mcp): release profile stderr handles before deletion 2026-09-15 04:12:41 -07:00
teknium1 23036e20a6 fix(ux): plain-language, actionable user-facing messages (core)
Squashed integration of the user-facing message audit for this surface set.
Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts).
2026-09-15 04:12:13 -07:00
teknium1 d0bdf39dad test: rewind-exclusion assertion inspects the real results key
The discovery payload exposes hits under `results`, not `matches`, so the
loop over `result_rewind.get("matches", [])` never iterated and the
invariant (the rewound row never surfaces, even via the OR-relaxed retry)
was not being checked. Assert on the anchor message id and snippet of each
returned result instead.
2026-09-15 04:01:17 -07:00
Teknium 2a2c3aa588 feat(session-search): OR-relaxed retry recovers paraphrased recall
Port from nearai/ironclaw#7553 (Filter::FtsRanked): FTS5's implicit AND
between terms means a paraphrased multi-word query misses a stored
sentence that lacks even one of the words. When the exact-match search
and the substring fallbacks all return zero rows, retry the same
unicode61 FTS index with the terms OR-joined, ranked by bm25 so rows
covering more terms surface first.

Strictly additive: gated on a zero-result miss, so successful searches
keep exact-match semantics and ordering. Queries with explicit OR/NOT,
single-term queries, and CJK-routed queries are left untouched. Quoted
phrases relax as whole units.

Adapted for hermes-agent: implemented inside SessionSearchMixin's
zero-result fallback chain (after the CJK-bigram/trigram substring
retries) rather than as a separate filter variant, reusing the already-
built SQL/params so all source/role/sort filters apply to the retry.
2026-09-15 04:01:17 -07:00
teknium1 681a28b06a test(session_search): fold the temporal-narrowing tests to four invariants
Parse-unit cases (relative units, ISO, empty, invalid) become one parametrized test; one
end-to-end per parameter: ISO window enforced in SQL (survives a truncated FTS scan),
relative after/before + exclude_session_ids driven through INLINE_TOOL_EXECUTORS so the
production dispatch is what is tested (red on the executor before this fix), and lineage
exclusion. Drops the schema-membership and unbounded-equals-None change-detectors.
2026-09-15 03:58:05 -07:00
Teknium e819846b10 Inspired by Amp: relative time bounds (7d/24h/2w) + wrapper forwarding for session_search after/before
Amp's thread feed supports relative time filters (`after:7d`,
`updated_before:7d`) alongside ISO dates. Extend the salvaged
after/before bounds (PR #86067 by @Moodtuner997) the same way:

- `_parse_iso_bound()` now accepts relative durations `Nh`/`Nd`/`Nw`
  (case-insensitive) meaning "now minus N", alongside ISO
  dates/datetimes. Clearer error message names both accepted forms.
- Forward after/before/exclude_session_ids through the public
  `session_search()` wrapper (the PR predates the wrapper/impl split;
  without this the SQL bounds were unreachable from the registry
  handler — same class as the earlier `detail` forwarding fix).
  Appended after `detail` to preserve positional compatibility.
- Tool schema descriptions teach both forms.
- Tests: relative after/before against the discovery shape, unit
  checks for h/d/w math, case-insensitivity, and bad-unit rejection.
- Docs: tools-reference row mentions time bounds + exclude_session_ids.
2026-09-15 03:58:05 -07:00
Théo 6f793ddbdc fix(session_search): apply after/before in SQL WHERE
Push the session-start bounds into search_messages so FTS LIMIT
cannot be filled by out-of-range hits. Covers FTS5, CJK, trigram,
LIKE fallback, and the unindexed-gap supplement.

Refs #86021.
2026-09-15 03:58:05 -07:00
Théo 5655920f9a feat(session_search): add after/before bounds and exclude_session_ids
Discovery-only filters for issue #86021. sort remains a ranking
bias. Date-only before is an exclusive midnight UTC bound.
exclude_session_ids drops the named session and its lineage (cap 20).
2026-09-15 03:58:05 -07:00
teknium1 dddefaefae fix: paged, extracted and post-compaction reads count as a write_file baseline
The stale-overwrite refusal made write_file permanently unusable for any
existing file it could not show in one read_file page: every >2000-line
(or >100K-char) page was recorded as partial, no full baseline ever
existed, and the refusal told the model to "re-read the whole file", which
the tool cannot do. Track the line ranges each task pages through per path
at one mtime; contiguous pages from line 1 to total_lines are a full read
(a new mtime between pages restarts the coverage). The same gap hit two
siblings: the extracted-document branch (.ipynb, text-authorable) returned
before any read bookkeeping, so an existing notebook could never be
overwritten; and reset_file_dedup dropped every baseline on compaction
while keeping read_timestamps, so every write after compaction was refused
even for files unchanged on disk. Baselines now survive compaction exactly
like the dedup mtime map does — only while the recorded mtime still matches.

Refusal texts no longer embed the pre-PR "Warning: … Consider re-reading"
copy inside "Refusing to overwrite", and every refusal names a recovery the
model can perform: read the remaining pages, or use patch.
2026-09-15 03:57:24 -07:00
teknium1 4613f895ab test: give the rewrite-hint fixtures a read baseline; align docs row with schema
The stale-write guard now refuses write_file on an existing file the task
never read in full, so test_write_file_rewrite_hint's overwrite-without-read
fixtures were refused before the hint could be computed. Reading first is
the exact read->whole-file-rewrite pattern the hint exists for.

tools-reference.md's write_file row now mirrors the WRITE_FILE_SCHEMA
description (one-sentence contract + the recovery step) instead of a
longer paraphrase.
2026-09-15 03:57:24 -07:00
Teknium 6569651b87 fix: harden salvage of #65605 — redaction-gated test, sibling test baseline, docs
- test_file_staleness redacted-read case now force-enables redaction
  (matches tests/agent/test_redact.py convention) so it exercises the
  sentinel path in hermetic CI where security.redact_secrets is unset.
- test_write_verification CRLF case establishes a read baseline first
  (the new guard refuses unread existing-file overwrites by design).
- tools-reference.md documents the read-before-overwrite contract.
- contributors/emails mapping for DanSpicyTaco.
2026-09-15 03:57:24 -07:00
DanSpicyTaco a8af57fcd2 fix: block stale write_file overwrites
Require an explicit full-file baseline before replacing existing host-visible files with write_file, and fail closed when that baseline is stale. This prevents stale conversation context from clobbering manual or external edits.\n\nRefs #65604
2026-09-15 03:57:24 -07:00
teknium1 04fcf9159c fix: keep api_server approval bridge and cron self-scheduling after presence strip
Widening the _presence() clearing from single-query to every unattended
context also cleared is_ask for platform=api_server. That surface answers
approvals through the /v1/runs bridge (approval.request ->
POST /v1/runs/{id}/approval), so a dangerous command that used to park in
waiting_for_approval became an instant BLOCK with no approval.request.
Restrict the clearing to single-query + cron, where nobody can answer.

Stripping HERMES_INTERACTIVE/HERMES_GATEWAY_SESSION/HERMES_EXEC_ASK from
the external worker env also made check_cronjob_requirements() False, so
the cronjob toolset vanished for every job on a managed-systemd gateway
even with cron.allow_agent_scheduling: true. Accept the existing
HERMES_CRON_SESSION marker (set by run_one_job's context) as well.

Review finding: _presence() over-widening broke the /v1/runs approval bridge; env strip hid the cronjob toolset in external workers.
2026-09-15 03:56:17 -07:00
teknium1 b6b7802447 fix(approval): every unattended context clears leaked presence vars
Widen the cron-only clearing to `_unattended_contexts()`: a webhook /
api_server session running inside a gateway inherits HERMES_EXEC_ASK=1
exactly like an external cron worker does, and `_presence()` returning
is_ask=True sent it to the gateway-decision branch with no notifier — a
pending card nobody can answer — instead of `approvals.unattended_mode`.
Same class as #110932, one predicate.

Test trimmed to two invariants (cron / webhook leak → cleared; interactive
keeps presence); the launch-path comment in cron/scheduler.py names the
env-fallback consumers instead of an internal incident log.
2026-09-15 03:56:17 -07:00
fabiantax 2a630671d7 fix(approval): cron context is never interactive, even with leaked presence env
_presence() cleared is_cli/is_gateway/is_ask for single-query sessions but
not for cron, so a cron worker that inherited HERMES_INTERACTIVE /
HERMES_EXEC_ASK from its launching gateway resolved as an interactive CLI
and blocked on an approval card nobody could answer (measured: 31
pending_approval hangs/hour, 6 stranded claims — #110932).

Mirror the single-query clearing for _is_cron_approval_context(), matching
the cron exclusion already inside _is_gateway_approval_context(). Layer 1
(#110942) strips the vars at the launch path; this makes the gate robust
to any other leak route.
2026-09-15 03:56:17 -07:00
teknium1 39fbfbe4e6 test: drop prose change-detector for terminal description
The test asserted three verbatim substrings of TERMINAL_TOOL_DESCRIPTION,
so any future rewording of the guidance would fail it without a behavior
change. The PR's change is prose-only guidance; the description text is
not a stable interface worth pinning.
2026-09-15 03:52:24 -07:00
Teknium 325d73ce07 docs(tools): clarify terminal background waits
Port from Kilo-Org/kilocode#13224: fixed waits belong in foreground terminal calls, while background mode is reserved for independently running processes.
2026-09-15 03:52:24 -07:00
teknium1 45ab3ad57f fix(delegate_task): return a schema-invalid child's raw text instead of failing the task
When a child's final answer still missed its output_schema after the one
bounded retry, the result entry flipped to status=failed with the error
"Final answer does not satisfy the declared output_schema" — the completion
line printed ✗ and orchestrators read a finished audit as a failure. Five
audits of 413-4103 s were lost this way in the Sep 10-14 retrospective and
the parent had to mine the live transcripts; in four of them the "violation"
was a ```json fence around a valid array, which the candidate extractor
sliced to its first..last object.

Now: status stays completed, `summary` is the child's raw final text,
`schema_valid: false` + `schema_errors` carry the verdict and a `schema_note`
says the text is unvalidated; the sync completion line shows ⚠ with the
reason. The extractor tries the earliest-opening bracket span and keeps the
first that parses (fenced arrays validate). The OUTPUT CONTRACT the child
sees now says "ONLY the JSON value — no prose, no code fence" and what a miss
costs. One bounded retry is unchanged.
2026-09-15 03:45:41 -07:00
teknium1 8a8c3634e8 fix(kanban): scope the delegated-child write fence to the lineage's board root
HERMES_DELEGATED_CHILD_CONTEXT=1 is deliberately carried into every shell/
execute_code subprocess a delegate_task child spawns (the fence must survive
exec so a grandchild `hermes kanban complete` cannot promote itself). But the
readers treated the bare flag as "fence every Kanban DB": kanban_db_connect
opened ANY board ?mode=ro and write_txn refused ANY mutation. A subagent
running a Kanban reproduction against a scratch HERMES_HOME therefore got a
silently read-only board with a misleading "descendants require an
initialized board" error; only one lane in the retrospective ever discovered
why (deleg_15dac332), every earlier kanban repro ran degraded.

The marker's value is now the fenced board ROOT (kanban_home() at spawn) and
readers deny only paths under that root or the dispatcher-pinned
HERMES_KANBAN_DB (kanban_path_is_fenced). In-process children and a legacy
"1" marker still fence everything; an inherited path marker is never
re-derived, so a grandchild that moved HERMES_HOME cannot unfence the real
board. Owner-gate tests (test_kanban_descendant_scope, cron env isolation,
kanban CLI exit status) are unchanged and green.
2026-09-15 03:45:41 -07:00