Commit Graph
35 Commits
Author SHA1 Message Date
_Kerman fcc2b5e282 refactor agent pre-step inbox lifecycle 2026-07-31 19:21:16 +08:00
Tianyi Cui 99daef69ef fix(hooks): remove speculative regex runtime 2026-07-29 23:15:15 +08:00
ZiyaZhang c7076e15b8 fix(hooks): bound regex reuse across reloads 2026-07-28 20:33:08 -07:00
ZiyaZhang 0ef2327e34 fix(hooks): match Codex Rust regex syntax 2026-07-28 10:37:53 -07:00
ZiyaZhang 55321fe7a4 fix(hooks): ignore matcherless event fields 2026-07-28 09:55:18 -07:00
ZiyaZhang 3f71f91d5b fix(hooks): reject invalid matcher regexes 2026-07-28 02:43:51 -07:00
_Kerman 52174e32cb refactor(agent): clarify turn lifecycle event names 2026-07-27 17:38:42 +08:00
_Kerman 8117f61396 Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine
# Conflicts:
#	packages/context/workspace-context/README.md
#	packages/llm/llm-retry/README.md
#	packages/session-persistence/session-checkpoint-policy/README.md
#	scripts/type-equiv.manifest.json
2026-07-26 16:45:27 +08:00
Tianyi Cui 226dc7a249 docs: translate remaining READMEs 2026-07-26 05:06:39 +08:00
_Kerman 992cf894af refactor(agent): unify sourced message delivery 2026-07-24 22:38:50 +08:00
Tianyi Cui e8eddc7ef8 Rename RFCs to Agent Notes 2026-07-19 22:52:03 +08:00
Tianyi Cui a278374e8f docs: structure model experience fields 2026-07-19 18:08:42 +08:00
Tianyi Cui bcc920369c docs: document package KV cache effects 2026-07-19 17:39:50 +08:00
Yichen Jiang 7d85d3027d Merge remote-tracking branch 'origin/master' into codex/agent-session-jsonl-location
# Conflicts:
#	docs/config-catalog.md
#	docs/module-graph.md
#	docs/rfc/INDEX.md
#	packages/README.md
#	packages/bash/bash-local/README.md
#	packages/bash/bash/README.md
#	packages/bash/tool-bash/README.md
#	packages/bash/tool-bash/tests/tools.spec.ts
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/examples/acp-demo/src/index.ts
#	packages/examples/acp-demo/tests/acp-agent.spec.ts
#	packages/examples/agent-spine-demo/README.md
#	packages/examples/agent-spine-demo/src/index.ts
#	packages/examples/agent-spine-demo/tests/agent-core.spec.ts
#	packages/examples/stdio-demo/src/index.ts
#	packages/examples/stdio-demo/tests/stdio-agent.spec.ts
#	packages/util/README.md
#	pnpm-lock.yaml
#	tsconfig.build.json
#	tsconfig.json
2026-07-17 18:39:05 +08:00
Yichen Jiang ee111fd978 Merge remote-tracking branch 'origin/master' into codex/project-instruction-files
# Conflicts:
#	AGENTS.md
#	docs/config-catalog.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.md
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md
#	docs/rfc/implemented/feature/2026-06-15-code-mode.md
#	docs/rfc/implemented/feature/2026-06-30-hook-bridges.md
#	docs/rfc/implemented/feature/2026-06-30-interception-seams.md
#	docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md
#	docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md
#	examples/AGENTS.md
#	examples/acp-agent/cordis.yml
#	examples/acp-agent/tests/acp.snapshot.ts
#	examples/echo-agent/cordis.yml
#	examples/sandbox-acp-agent/cordis.yml
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/agent-core/README.md
#	packages/core/agent-core/src/index.ts
#	packages/core/agent-loop/README.md
#	packages/core/agent-loop/src/loop.ts
#	packages/core/agent-loop/tests/interception.spec.ts
#	packages/core/agent/src/types.ts
#	packages/core/tools/README.md
#	packages/core/tools/src/code-mode.ts
#	packages/core/tools/src/index.ts
#	packages/fs/fs-local/src/index.ts
#	packages/fs/fs/README.md
#	packages/fs/fs/src/index.ts
#	packages/guard/repeat-tool-guard/README.md
#	packages/guard/repeat-tool-guard/src/index.ts
#	packages/hooks/hooks-claude/src/index.ts
#	packages/hooks/hooks-codex/src/index.ts
#	packages/ui/acp-agent/src/index.ts
2026-07-14 19:50:25 +08:00
Yichen Jiang 0d67c16d56 Merge remote-tracking branch 'origin/master' into codex/agent-session-jsonl-location
# Conflicts:
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/bash.md
#	docs/module-graph.md
#	docs/rfc/INDEX.md
#	docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md
#	docs/rfc/implemented/feature/2026-06-30-hook-bridges.md
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.golden.md
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.golden.md
#	examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.golden.md
#	examples/acp-agent/tests/snapshots/skill-load/session.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/mode-switching/session.jsonl
#	packages/bash/bash-local/README.md
#	packages/bash/bash-local/src/run.ts
#	packages/bash/bash-local/tests/run.spec.ts
#	packages/bash/bash/README.md
#	packages/bash/tool-bash/README.md
#	packages/bash/tool-bash/src/index.ts
#	packages/bash/tool-bash/tests/tools.spec.ts
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/agent-core/src/index.ts
#	packages/session-persistence/session-persistence-jsonl/src/index.ts
#	packages/session-persistence/session-persistence-sqlite/src/index.ts
#	packages/session-persistence/session-persistence/README.md
#	packages/session-persistence/session-persistence/src/index.ts
#	packages/ui/acp-agent/README.md
#	packages/ui/stdio-agent/README.md
2026-07-14 18:04:05 +08:00
Tianyi Cui c74f17a143 docs: audit hook compatibility limits 2026-07-14 11:22:53 +08:00
Tianyi Cui d77f797d89 docs: replace model experience tables with sections 2026-07-13 22:26:33 +08:00
Tianyi Cui 646f3d2d0c docs: apply verbatim model experience guidance repo-wide 2026-07-13 21:33:23 +08:00
Yichen Jiang aa62b5109a Fix workspace context review findings 2026-07-13 16:31:03 +08:00
Yichen Jiang 768c79fd45 Fix Code Mode workspace context propagation 2026-07-13 13:56:45 +08:00
Tianyi Cui b5cd511f35 docs: address model experience review 2026-07-12 02:55:26 +08:00
Tianyi Cui a08485fc80 Merge remote-tracking branch 'origin/codex/package-readme-limitations-audit-20260712' into codex/model-experience-readmes-20260712
# Conflicts:
#	docs/cookbook/adding-a-package.md
#	docs/rfc/INDEX.md
#	package.json
#	packages/AGENTS.md
#	packages/bash/bash-sandbox/README.md
#	packages/sandbox/sandbox-local/README.md
#	packages/sandbox/sandbox/README.md
#	packages/session-persistence/session-persistence-sqlite/README.md
#	packages/support/acp-snapshot/README.md
#	packages/ui/app-boot/README.md
#	packages/ui/user-approval/README.md
#	packages/workflow/tool-workflow/README.md
2026-07-12 02:48:49 +08:00
Tianyi Cui 027970043c docs: document package model experience 2026-07-12 02:12:36 +08:00
Tianyi Cui ecb8aa5b8e Add a gated Known Limitations and Deferred Work section to every package README
Every packages/*/* README now carries a canonical '## Known Limitations and
Deferred Work' section: condensed, evidence-backed bullets for consumer-visible
gaps (unimplemented features, platform caveats, MVP cuts) and consciously
postponed work (TODO/FIXME/XXX markers, RFC deferrals still open). The ten
pre-existing ad-hoc variants ('What is NOT here (TODO)', 'Deferred',
'Limitations (MVP)', 'Known limitations (tracked TODOs)', ...) are normalized
into the canonical heading.

A new doc-sync gate, scripts/verify-readme-limitations.ts, enforces the shape:
exactly one limitations-like heading per package README, byte-equal to the
canonical h2, with at least one bullet; near-miss headings fail so variants
cannot creep back. Packages with genuinely nothing to declare (dsh-brand,
dsh-timeout, dsh-subagent-mock, dsh-app-boot) are whitelisted in the script and
must NOT carry the section; whitelist entries are validated against the scanned
package set so a rename fails loud.

Wired into the doc-sync chain (package.json) and the run-gates doc-sync leaf
set; the standing rule lands in packages/AGENTS.md and the adding-a-package
cookbook; decision record in
docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md
(RFC index regenerated).

Also fixes two stale '(deferred)' markers claiming dsh-compact-basic is
unimplemented (the dsh-compact seam README's package table and the seam's
module doc comment).
2026-07-12 01:46:34 +08:00
Yichen Jiang eea0a99985 feat: expose agent session log location 2026-07-10 20:52:27 +08:00
Tianyi Cui b59d245c7c feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.

run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).

The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00
Tianyi Cui 9ca31ab193 fix(hooks): drain detached hook runs on bridge dispose
The emit-shaped hook points (SessionStart, SubagentStart, SubagentStop)
run fire-and-forget: no seam awaits the run chain, so disposing a bridge
could strand a live hook process and let a late continuation inject into
a disposed context. The floating continuation also made the coverage
gate racy: the only coverage of the SubagentStart continuation's
no-context branch arm rode on an un-awaited .then, and on a loaded CI
runner the fork's per-file coverage snapshot beat it — master run
28798191671 failed the 100% branch gate on hooks-claude/src/index.ts at
99.03% (uncovered line 336) with the identical tree passing the PR run
three minutes earlier.

New shared primitive createDetachedRuns() in dsh-hook-protocol: a bridge
tracks each detached run chain, passes the tracker's abort signal to
runHook, and registers drain() as its effect disposer — drain aborts
still-running hook processes (a kill via the bash seam, not a wait out
to the 10-minute default hook timeout), then resolves once every chain
has settled. fiber.dispose() resolving now means the bridge's detached
work is quiescent (docs/defensive-patterns.md).

The subagent marker test disposes the bridge as its sync point, so the
formerly racy branch arm is executed deterministically before the file's
coverage snapshot; new tests pin abort-on-dispose promptness for both
bridges and the tracker's settle/drain contract in hook-protocol.
2026-07-06 23:27:54 +08:00
Tianyi Cui 19ae955009 Merge remote-tracking branch 'origin/master' into simpl-g-hook-contract
# Conflicts:
#	packages/hooks/hook-protocol/src/events.ts
#	packages/hooks/hook-protocol/tests/events.spec.ts
#	packages/hooks/hooks-claude/README.md
#	packages/hooks/hooks-claude/src/index.ts
#	packages/hooks/hooks-codex/README.md
#	packages/hooks/hooks-codex/src/index.ts
2026-07-04 21:08:50 +08:00
Tianyi Cui 774d460889 Expose audited hardcoded tunables as plugin config
The audit swept every packages/*/* plugin for the new AGENTS.md
convention (no hardcoded tunables in plugins) and exposes each finding
as a defaulted, validated Config field. Defaults are the previously
hardcoded values throughout, so no deployment or golden changes.

- tool-fs (had NO Config): readLimit, readMaxLineLength, readMaxBytes,
  readStreamMinSize. The caps thread through ReadToolCaps/ReadWindow —
  read-render already documented that the consumer applies the caps, so
  they become explicit per-request fields.
- tool-web: searchMaxResults (WEB_SEARCH_MAX_RESULTS stays as the
  schemastery default). Also fixes the stale GREP_LIMIT references in
  search.ts and the web-capability-seam RFC (no such constant exists).
- bash-local: graceMs (SIGTERM->SIGKILL escalation grace). The
  RunInternals.graceMs test seam is gone: graceMs is now a required
  SpawnSpec field filled from config, so tests exercise the real
  config path and the defaults live in exactly one place.
- subagent-acp: disposeEofGraceMs / disposeGraceMs. The AcpRunSpec
  fields become required for the same one-defaulting-layer reason.
- session-persistence-sqlite: journalMode ('wal' default; the
  rollback-journal modes serve filesystems where WAL's shared-memory
  files do not work, e.g. network mounts).
- hooks-claude + hooks-codex: stderrSummaryMaxChars for the persisted
  hook/result stderr summary. The duplicated summarize() helpers merge
  into hook-protocol's summarizeStderr(stderr, maxChars), beside the
  HookResultRecord field it feeds, with the bound parameterized the
  same way runHook's defaultTimeoutMs already is.
- compact-basic: charsPerToken for the token estimator (default 4, the
  English-text heuristic; CJK-heavy deployments need ~1-2 or compaction
  fires far too late). Also corrects the BasicCompactService class doc,
  which claimed defaults the required-field config never had.
- fs-local: deletes the dead STREAM_MIN_SIZE constant and the dead
  FsIoInternals.streamMinSize seam — the read-routing bound lives in
  the consumer (tool-fs), where it is now config. This is item 1 of
  the proposed prune-write-only-fs-surface RFC, annotated accordingly.

Every new field gets range validation (following the existing
assertPositiveFinite pattern), a README row, and tests covering the
configured behavior, the schema default, and load-time rejection.
2026-07-04 17:37:23 +08:00
Tianyi Cui cd49670f4e refactor(hooks): tighten the hook-protocol contract surface
Implement the tighten-hook-protocol-contract RFC (moved to implemented/):

- HookDialect narrows to 'claude' | 'codex': the 'native' variant had zero
  producers (native plugins on the seams write no hook/* provenance), and the
  dialect is defined as the bridge that ran the hook.
- HookOutput.suppressOutput is gone: the codec parsed it and every path
  discarded it with no warn and no deferral — hook stdout never enters a
  transcript, so there is nothing to suppress.
- hook/result.durationMs is gone: durable timing telemetry with no reader
  that the snapshot normalizer had to scrub as replay noise. With no duration
  to measure, runHook loses its injected now clock and the single-field
  RunHookResult wrapper — it returns the HookOutput directly. The committed
  hook fixtures had the field stripped mechanically (field-only diff); the
  stdout goldens never carried it.
- The bridges' double-defaulted defaultTimeoutMs config knob is replaced by
  one reference-default constant, DEFAULT_HOOK_TIMEOUT_MS, exported from the
  lib's runner and applied inside runHook; per-hook timeoutSec stays the
  override surface.
- The hook/result semantics move into the lib that declares the event:
  HookResultRecord now carries the decoded HookOutput and appendHookResult
  derives the decision string (decision ?? stop-on-continue:false ?? pass)
  and the 500-char stderrSummary truncation; both bridges delete their
  byte-identical private copies. The snapshot suite passes against the
  existing goldens, proving the derived values are unchanged.
- Rider: BLOCKING_EXIT_CODE is codec-internal again (zero importers).

Amend the hook-protocol-lib and hook-snapshot-matrix RFCs to the new facts,
update the lib/bridge READMEs and the session.md event tables, and retarget
the affected unit tests (including new lib-level coverage of the derivation
rules).
2026-07-04 15:44:26 +08:00
Tianyi Cui 9bc4df28c1 fix(hooks): delegate context-only hooks + default CLAUDE_PROJECT_DIR
Address review on the hook-bridges PR — two composability/compatibility bugs
in both the CC and Codex bridges:

1. A hook that only attaches additionalContext (no block/deny) returned
   `allow`/`accept` WITHOUT calling next(), short-circuiting every later
   agent/prompt-submit / tools/post-execute listener. A policy/sandbox plugin
   registered after the bridge never saw the prompt. Now the context-only path
   delegates via next() and folds its context onto the downstream decision
   (concatContext): a downstream block/deny still wins and carries the bridge
   context; a downstream allow/accept keeps its own content rewrite and gains
   the context. Only a real hook deny/block short-circuits.

2. CLAUDE_PROJECT_DIR was empty in the default ACP wiring (no projectDir
   configured), breaking common unmodified hooks that reference
   $CLAUDE_PROJECT_DIR. It now defaults per-run to the agent's session
   workspace (the same cwd the hook runs in); an explicit config.projectDir
   still wins.

Regression tests per bridge: a later listener blocks a prompt a context-only
hook allowed; both contexts survive when the downstream also adds one; the
default CLAUDE_PROJECT_DIR reaches the hook. Each proven red on the pre-fix
code.
2026-07-02 18:35:53 +08:00
Tianyi Cui 09c8e549b0 fix(hooks): run hooks in the session cwd; honest process-level config + best-effort session-start; surface systemMessage drop
Address review on the bridges:

- Hook cwd (blocking): the bridges never passed a workdir to runHook, so hooks
  ran in the executor default (the ACP server launch dir), not the session
  cwd — a hook doing `pwd`/relative reads/marker writes operated in the wrong
  tree. Both bridges now thread the agent's session `header.cwd` (the
  session/new.cwd) as the hook workdir for agent-scoped points. Regression per
  bridge: server cwd ≠ session cwd, a `pwd` hook proves it ran in the session
  workspace (proven red without the workdir).
- Example config honesty (blocking): `configPath: ./hooks.json` is read ONCE at
  load against the PROCESS cwd, not per-session — the comment/README now say so
  explicitly (a project-local per-session hooks.json is not discovered;
  TODO(per-session-hook-config)). The hooks-run-in-session-cwd fix above is the
  distinct, separately-documented half.
- Session-start timing (blocking): agent/session-start is a synchronous emit and
  the hook runs on a detached .then, so injected context is BEST-EFFORT — not
  guaranteed before the first request. Downgrade the contract in code comments +
  README + RFC (TODO(session-start-gating)) rather than implying "first request
  sees it", and add a no-wait regression that asserts the safe properties
  without pre-waiting for the inject.
- systemMessage (non-blocking): the merge collects merged.systemMessages but no
  bridge surfaced it. Warn per hook (like updatedInput) and document it as
  deferred in both READMEs + the RFC; tests assert the warn + non-surfacing.
2026-07-01 16:34:28 +08:00
Tianyi Cui 8870da4313 fix(hooks): address Codex review — Stop force-continue, Codex tool_name + plain-stdout context, defer continue:false
Round-1 Codex review findings on the bridges:

- Stop force-continue (both bridges): a blocking Stop hook with EMPTY stderr
  yielded decision 'deny' + reason undefined, and the `&& reason !== undefined`
  guard let the turn STOP — the opposite of a blocking Stop hook. Force-continue
  on any deny; fall back to a generic steering line when there is no reason.
- Codex payload tool_name: hardcoded "Bash" disagreed with the exec.name matcher
  subject, so a real Codex `matcher:"Bash"` never fired against the harness's
  lowercase `bash` tool. Use exec.name in both payload builders (matches the
  matcher subject and the sibling CC bridge). Doc/RFC updated.
- Codex plain-stdout context: SessionStart/UserPromptSubmit are documented to
  treat a clean hook's PLAIN (non-JSON) stdout as additionalContext, but nothing
  folded it. runPoint now folds plain stdout into context for those two events,
  gated on the codec's JSON gate so structured stdout is never dumped as prose.
- continue:false is deferred, not honored: the seams have no hard-halt primitive
  yet. TODO(hook-continue-false) at both bridges + an RFC deferred note; the two
  tests now assert the LOG records the halt request AND that the run is NOT
  actually halted (no longer misleading).
- README concurrency wording: hooks run SERIALLY (deliberate — adjacent
  invoked/result log pairs, order-independent fold), not concurrently. Fixed the
  CC README claim + an RFC note.

Regression guards proven red on the unfixed code, then reverted. The mismatched-
hookEventName discard (also flagged) is fixed in dsh-hook-protocol and merged down.
2026-07-01 10:48:23 +08:00
Tianyi Cui 8adcbceeed feat(hooks): dsh-hooks-claude + dsh-hooks-codex bridges (hooks stack PR-F)
The two bridge plugins that run a user's existing Claude Code / Codex hook
config on the harness's typed interception seams, built on the shared
dsh-hook-protocol library. A bridge is a faithfulness adapter, not a power
tool: anything it does a native cordis plugin does more powerfully — the
bridge exists only to run UNMODIFIED external hooks.

- dsh-hooks-claude: CC dialect. Seven hook points (SessionStart,
  UserPromptSubmit, PreToolUse, PostToolUse, Stop, SubagentStart,
  SubagentStop), CC per-event stdin payloads, env + ${CLAUDE_PLUGIN_ROOT}/
  ${CLAUDE_PROJECT_DIR} substitution, literal-or-regex matcher.
- dsh-hooks-codex: Codex dialect — a deliberate subset. Five hook points,
  always-regex matcher, snake_case payloads (turn_id/model, no trailing
  newline), no env/substitution, block-only decisions.

Both map the neutral merged outcome onto the seam's typed Decision and stamp
an explicit {kind:'plugin'} source on injected context (so it is never
mislabeled as a user prompt). Config parse-failure is contained; only command
hooks run. updatedInput is logged+warned (input rewrite deferred); the Stop
loop-guard is deferred (TODO).

Tests: per-file 100% — config-parse unit branches + per-seam mappings
end-to-end through the REAL loop + REAL bash + REAL shell scripts (scripted
mock model only) + a real-Loader export-shape guard. A keyless ACP snapshot
scenario (hook-prompt-block) proves a UserPromptSubmit hook blocks a prompt
end-to-end (rejected turn -> ACP cancelled, hook/* events in the log); a
with-key e2e (hooks.e2e.ts) proves a PreToolUse hook blocks real bash
(verified on disk). The snapshot normalizer now scrubs hook/result.durationMs.

RFC: docs/rfc/implemented/feature/2026-06-30-hook-bridges.md
2026-07-01 04:23:49 +08:00