Commit Graph

44 Commits

Author SHA1 Message Date
Nicolò Boschi 2643186a85 feat(coding-agents): spread page refreshes with hashed cron fields (H) (#4208)
One `pageTriggerCron` is shared by every page in every bank running this
plugin, so a literal `"0 3 * * *"` does not schedule a refresh at 03:00 — it
schedules all of them at 03:00, ~5 page syntheses per bank, on the worker pool
that also serves retain. Moving the hour moves the pile.

Borrow Jenkins' `H`: a cron field written `H` is replaced, per page, by a value
hashed from bank + page name, so each page keeps its own stable slot.

    "H H * * *"       daily, at this page's own minute and hour
    "H * * * *"       hourly, at its own minute
    "H 3 * * *"       daily at 03:MM — spread inside the chosen hour
    "H H(0-5) * * *"  daily, spread across the night only
    "0 3 * * *"       unchanged

Spreading is a property of the schedule, so it lives in the expression rather
than in a new `pageTriggerType` member per period (`daily-staggered`, then
`hourly-staggered`, …), each of which would spell the period in the type name
and cost a config value, a branch and a docs row.

`H` never leaves the package: `expandCronHash` resolves it to an ordinary
5-field expression at page creation — the only point where the page's identity
exists — so the server parses plain cron and the control plane shows an
editable schedule. The field index is hashed alongside the seed so `H H * * *`
is worth 1440 slots rather than 60 correlated ones. A malformed `H` is refused
at config resolution, since `expandCronHash` deliberately leaves an expression
it cannot read alone rather than inventing a time nobody asked for.
2026-09-08 11:06:42 +02:00
Wojciech Guziak 254bc988d4 feat(coding-agents): add Factory Droid harness (#4011)
Adds Factory Droid as a supported harness: three hook entry points on Droid's
Claude-shaped protocol, a transcript reader for its JSONL sessions, installer
wiring for ~/.factory (hooks.json, mcp.json, skills/), and the control-plane logo.

Also lands, on top of the original contribution:

- One harness roster for the docs site. The gallery card and the sidebar preview
  kept separate hand-maintained lists and both had drifted — dcode, opencode2, pi
  and prime-agent were each missing from one of them. Both now derive from
  src/lib/coding-agent-harnesses.ts, bound by a test to the registry that decides
  what `install <harness>` accepts, so a new harness cannot ship without its logo
  or its README entry.

- A Docker E2E for the harness. Droid needs no Factory account for it: with a BYOK
  customModels entry it runs fully offline against the stub model.

- A fix to the E2E runner, which drove the container with spawnSync and so starved
  the stub model living in the same process. That silently broke dcode, qwen-code
  and dsh; qwen-code's E2E passes again with it.

Verified end to end against a real Droid session (recall injected, session
retained) and with full CI on ci/4011-factory-droid: 109/109 jobs green.
2026-09-07 15:56:31 +02:00
Nicolò Boschi 6f2799569f docs: make every integration changelog reachable, and compact the Integrations Hub (#4005)
* docs: link every integration changelog from its doc page

The release script has written a changelog page per integration since 0.4.20
(src/pages/changelog/integrations/<slug>.md), but nothing reliably linked them:
16 of 61 doc pages carried a "View Changelog" link, and the only index was a
hand-written table at the bottom of /changelog listing 6 of the 48 pages, stale
since the release that introduced them. coding-agents was one of the 31 pages
with a changelog nobody could reach except by guessing the URL.

Adds the link to the 30 pages missing one, plus a third invariant in
check-integrations.mjs so the next one can't ship unlinked: every changelog page
whose integration has a gallery entry must be linked from its doc page.

coding-agents.md is generated, so its link goes in the README as an absolute
hindsight.vectorize.io URL — the sync script rewrites our own absolute URLs to
site-relative, so it resolves on npm, GitHub and the docs site. It sits outside
the skill:begin regions, so SKILL.md is unchanged.

Claude-Session: https://claude.ai/code/session_01DNWFQn8AUN6SApcswHG3aN

* docs(integrations): compact grouped gallery with Docs/Changelog buttons

The Integrations Hub listed 60 three-line cards with a repeated "Hindsight Team"
byline and an OFFICIAL badge on 52 of them, running 6092px tall — scanning it
meant scrolling past what you came for. Cards are now a compact row (icon, name,
two-line description) grouped by category, with the featured three using the same
row tinted rather than a second card design: 4352px, and the shape of the
catalogue is visible at a glance.

Each card carries Docs and Changelog buttons. Which integrations have a changelog
comes from a new build-time plugin that indexes
src/pages/changelog/integrations/*.md and joins integrations.json on the link
slug (vercel-ai-sdk -> /sdks/integrations/ai-sdk -> ai-sdk.md), so an integration
grows the button on its first release with nothing to update by hand.

The card is a <div> now — nested anchors are invalid — so the name and the pills
are the links, not the whole card. /changelog's first line points here instead of
carrying its own listing.

Also drops the 14 CSS classes the old card owned, and shares CATEGORY_LABELS /
groupByCategory from src/lib/integrations.ts so the taxonomy has one definition.

Claude-Session: https://claude.ai/code/session_01DNWFQn8AUN6SApcswHG3aN
2026-09-02 10:21:08 +02:00
Nicolò Boschi 71a9a7008f feat(coding-agents): first-class pi support, sharing one extension adapter and installer with Prime Agent (#3993)
Adds pi (@earendil-works/pi-coding-agent) as a harness and extracts createPiExtension(harness),
the extension adapter pi and its fork Prime Agent share. Supersedes #3775.

The `pi` key is removed from package.json: both hosts read that same key, so it could only ever
name one bundle, and the host it did not name loaded the other's and reported the wrong harness.
`hindsight-coding-agents install pi|prime-agent` is now the only route for both.

Also makes the companion skill self-update for every host that installs one — the paths now live
once in core/skill-dirs.ts, which both the installer and skill-sync read.

Co-authored-by: Sebastian Otaegui <feniix@gmail.com>
2026-09-01 15:11:06 +02:00
Shaun Eccles-Smith 247e9b3138 feat(coding-agents): add per_source observation scoping (#3872)
* feat(coding-agents): add per_source observation scoping

On a repo worked by a coding agent, commit diffs and session transcripts
make different kinds of claim. A diff records what the code does; a
transcript records what someone intended, argued for, or discarded. Under
the `shared` default both consolidate into one undifferentiated belief
set, so an idea floated in chat and never implemented is indistinguishable
from a belief derived from the commits, and "what does the codebase
actually do" cannot be answered from commit-derived knowledge alone.

This cannot be fixed by configuration. The server treats an explicit
scope list as unconditional: `_resolve_obs_tags_list` and
`_resolve_write_scopes` in the consolidator both return the parsed list
verbatim, without filtering it against the memory's own tags. A configured
`[[], ["source:git"], ["source:chat"]]` therefore writes EVERY document
into all three scopes, and the `source:git` scope fills with observations
built from chat transcripts. Only a per-document decision separates them.

`per_source` is resolved client-side, per document, in the new exported
`resolveRetainScopes`, and never reaches the server. It expands to the
global scope plus one per distinct `source:` tag the document carries,
sorted — a document with two source tags (the commit-message seed keeps
`source:git` alongside `source:git-log` so the cold-repo check still sees
it) gets a scope for each, which needs no arbitrary tie-break and does not
depend on the order the caller assembled its tags in.

Reading only `source:` is what keeps this safe. `per_tag` splits on the
right axis but also on every other one: it would reinstate the per-agent
`harness:` fork that #3564 and #3575 removed, and any volatile tag such as
a session id from `retainTags` would become its own scope — the
fragmentation bug itself.

The empty scope is always emitted first and unchanged, so the merged view
matches `shared` exactly and the untagged observations that knowledge
pages read (`tags_match: "all"`, per #3664) are unaffected.

The cost is honest: one extra consolidation pass per document. That is the
price of the axis, and it is why this is opt-in —
`DEFAULT_OBSERVATION_SCOPES` remains `shared` and no existing value
changes meaning.

Closes #3871

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdYchB9yKUWw8oa16RWQu9

* fix(coding-agents): not every source tag names a kind of claim

Running per_source on real repositories showed two of the five source tags are
provenance labels rather than axes, and each produced a scope worth less than it
cost.

source:git-log is a bookkeeping alias. git.ts tags the commit-message seed with
it AND source:git, so emitting a scope for each gave two near-identical belief
sets — 307 observations against 302 on one repo — and doubled the consolidation
for that pair. It is the same claim as source:git: what the commits say. Mapped
onto it rather than dropped, because that seed is where a cold repo's entire
commit history arrives.

source:survey-baseline marks the "researching…" status document, whose retain
strategy is meant to extract nothing. It still yielded a scope holding exactly
one observation — a belief set that exists only to be noise. Excluded.

Emitting a scope per DISTINCT source tag, sorted, is still right: taking the
first made the result depend on the order git.ts assembled its tags, which a
test pins. It is the vocabulary that carries two non-semantic entries, not the
rule.

* Revert "fix(coding-agents): not every source tag names a kind of claim"

This reverts 0786de8. The evidence behind it was an artefact.

The claim was that source:git and source:git-log produced near-identical belief
sets (307 observations against 302). They did — but only because at the time of
measurement the bank held exactly ONE git document, the commit-message seed,
which carries both tags. The per-commit diff backfill under gitIngest: "full"
had not run yet, so one document was feeding both scopes.

Once it does, the two diverge and are genuinely different questions:
source:git-log is fed only by the seed — what the commit MESSAGES say — while
source:git also collects every per-commit diff. Intent against implementation.

The reasoning was wrong at a deeper level too. Overlapping content across scopes
is not duplication to be engineered away: consolidation already distills and
deduplicates WITHIN each scope, and a fact that legitimately answers two
questions belonging to both is the design working. Excluding tags to avoid
overlap misreads what a scope is for.

source:survey-baseline goes back for the same reason. Its scope holding a single
observation suggests the marker document's retain strategy is extracting facts
it is meant to suppress — which is worth fixing where it happens, not papering
over in the scope resolver.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-09-01 14:28:09 +02:00
Nicolò Boschi c249d43796 coding-agents: support opencode 2, whose plugin API shares nothing with v1's (#3985)
opencode v2 (`@opencode-ai/cli@beta`, binary `opencode2`) installs alongside v1
and rewrote the plugin contract end to end: a v1 plugin is a function returning a
bag of named hooks, a v2 plugin is `{id, setup(ctx)}` where ctx hands out
per-domain registration. Handing either host the other's export loads a plugin
that registers nothing and reports no error, so v2 needs its own adapter — mapped
onto the same harness-agnostic RuntimeCore, which keeps recall, injection, the
native hindsight_* tools, the cold seed and write-back identical to every other
harness:

  v1 chat.message                       -> ctx.session.hook("prompt")
  v1 experimental.chat.system.transform -> ctx.session.hook("context")
  v1 tool: {...}                        -> ctx.tool.transform(d => d.add(...))
  v1 event (session.idle)               -> ctx.event.subscribe()
  v1 client.session.messages()          -> ctx.session.context({sessionID})

Three host behaviours shaped the result, each verified against a live
opencode2 0.0.0-beta-18743:

ONE CONFIG ENTRY SERVES BOTH CLIS. v1 and v2 read the same
~/.config/opencode/opencode.json, and v1 REJECTS the whole file when it sees v2's
`plugins` key ("Configuration is invalid ... Unrecognized key: plugins") — so a
second entry is not an option. What makes one entry work is that they resolve a
plugin DIRECTORY differently: v1 follows package.json `main` (dist/index.js), v2
ignores `main` and loads <dir>/index.js. The package therefore ships a root
index.js re-exporting the v2 entry, and the path already in a user's `plugin`
array drives the right plugin under each host, with no config change.

THE EVENT STREAM IS GLOBAL; THE SESSION HOOKS ARE NOT. v2's background service
hosts every open project at once. `ctx.event.subscribe()` delivers session.idle
for sessions in OTHER projects, with no location on the envelope to tell them
apart, while prompt/context fire only for the location that loaded the instance.
Acting on those ids would fetch another project's transcript and retain it into
this project's bank, so write-back is gated on sessions our own prompt hook
admitted. This is a bank-isolation invariant, and it has a regression test.

TOOLS NEED options.codemode: false. Without it a tool is reachable only through
v2's `execute` code-execution tool, and a model calling it by name — which the
skill and the injected preamble both tell it to do — gets "Unknown tool:
hindsight_...".

Two capabilities are deliberately absent. The codebase survey gets no opencode2
recipe: v2 plugins cannot define an agent (agent.transform has no `add`) and a
config-file agent is not a sandbox either, since the user's own global
permissions are appended after an agent's rules — the survey reads untrusted repo
files, so without a guaranteed read-only boundary it falls back to another
installed agent's CLI, as it already does for kilo/cline/cursor/dcode. And there
is no toast: v2 plugins can observe tui.toast.show but not publish one, so the
seed banner is logged. v1's mid-session cadence write-back is also gone, because
v2 makes it redundant rather than because it was missed — session.execution.succeeded
fires after every assistant turn, so the idle path already runs once per turn
from the authoritative post-reply transcript.

Verified end to end against a real opencode2 and server: plugin loads reporting
harness opencode2, per-repo bank derived, cold seed and survey ran, the injected
block reached the model (it quoted it back verbatim), hindsight_ingest_document
was called by name, and session idle produced retain_ok. Also verified the staged
published-package shape loads under opencode2, and that opencode v1 still loads
dist/index.js and reports harness "opencode" with the new root index.js present.

Closes #3795

Claude-Session: https://claude.ai/code/session_01WbaYxouCERUv9djo3GnnA2
2026-09-01 14:08:34 +02:00
Nicolò Boschi 1aef228f7b feat(coding-agents): add qwen-code as a hook harness (#3979)
* feat(coding-agents): add qwen-code as a hook harness

Qwen Code speaks Claude Code's hook protocol field for field -- same stdin
envelope (session_id / transcript_path / cwd / hook_event_name), same
hookSpecificOutput.additionalContext + systemMessage output, same exit
semantics, and a settings.json shape byte-for-byte what cmdHook() already
emits. So HOOK_HARNESSES["qwen-code"] is claude-code's spec with three deltas,
none of which is visible from the diff:

1. TIMEOUTS ARE MILLISECONDS. Qwen passes a command hook's `timeout` straight
   to setTimeout ("Timeout in milliseconds, default 60000" -- its own bundled
   docs/features/hooks.md). Every other harness here is seconds, so the
   installed values are 30000/30000/60000. Writing 30/60 would register 30ms
   hooks, and that misconfiguration LOOKS fine in testing: Qwen spawns without
   detached:true and kills only the direct child, so the orphaned work still
   completes. HookHarnessSpec therefore declares `timeoutUnit`, and the
   lifecycle tests normalise through it -- changing 30_000 to 30, or dropping
   the unit, now fails a test instead of shipping dead hooks. The prompt budget
   must also clear core/hook.ts's HOOK_REFLECT_CAP_MS (25_000).

2. parse reads `submitted_prompt`, not `prompt`. UserPromptSubmit also fires on
   tool-result continuations, where `prompt` holds model-bound tool output;
   keying on it would recall ~20x per user turn against tool results.
   `submitted_prompt` is attached only when the turn is both the first and a
   genuine userQuery. Accepted cost: it is the interactive TUI's projection, so
   headless (`qwen -p`), serve, SDK and ACP sessions seed and retain but never
   recall. The E2E declares injectsIntoModel: false for exactly that reason --
   a different reason from grok-build's passive hook.

3. type:"user" is NOT a user turn. `provenance` is the discriminator: across a
   22-transcript corpus, synthetic records (notification 285, cron 21,
   goal_runtime 1) outnumber real_user ones (69) by 4.4:1. transcript-qwen.ts
   gates on it, rejecting a present-but-malformed value outright rather than
   falling through to a subtype heuristic, and requiring a positive subagent
   marker (agentId/isSidechain) before accepting an absent one -- subagent
   transcripts use a third envelope with neither provenance nor subtype.

Qwen also echoes injected context back into its own transcript, wrapped in
<qwen:user-prompt-submit-context> with the inner tags HTML-escaped, so the
shared stripInjectedMemory (which matches raw tags) cannot see it. The reader
removes it using the two forms of pairing evidence Qwen's contract defines:
systemPayload.hookContext present -> use systemPayload.displayText, the host's
own pre-hook projection, with no tag matching at all; otherwise a COMPLETE
tagged context in the FINAL part after at least one other part. It deliberately
does NOT match the tag as a substring -- the contract is explicit that the tag
is "a provenance marker, not ... a general trust boundary" and that consumers
"must not infer that arbitrary tag-like user text is hook provenance", so a
prompt merely quoting the tag is preserved intact.

Also here:
- registry.test.ts gains the REVERSE parity check. The existing one is
  directional (installer -> registry), so a harness present in the registry but
  missing from INSTALLERS passes it while `install <name>` returns "unknown
  harness". That is exactly how this change was briefly broken.
- readQwenTranscript catches lazy-read faults. readJsonlTail guards
  statSync/openSync, but the generator reads at iteration, and runRetainHook
  calls the reader outside buildRetain's catch -- so a directory passed as
  transcript_path (which passes both guards, then throws EISDIR on the first
  readSync) rejected the whole Stop hook. NOTE: the underlying gap is in the
  shared jsonl.ts and affects every harness; it deserves its own fix rather
  than riding along here.
- Logo asset from homarr-labs/dashboard-icons (Apache-2.0, svg/qwen.svg).

Tests: 675 pass. Note HINDSIGHT_BANK_ID must be unset in the environment, or
config.test.ts's "missing files yield defaults" fails on the leaked value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WkVVSuv7j1FwTtVpkzzmWR

* chore(coding-agents): regenerate docs artifacts and apply prettier

Address the two CI-blocking review items on #3769:

- Regenerate the two artifacts derived from the README's new Qwen Code
  section: hindsight-docs/docs-integrations/coding-agents.md (via
  sync-coding-agents-doc.mjs) and the docs-skill copy (via
  generate-docs-skill.sh).
- Run prettier over the package with 3.7.4 -- the version CI's root
  `npm ci` pins -- covering the four files the review names, plus
  package.json, where it restores the literal ellipsis in `description`
  that the branch had accidentally left as a unicode escape sequence.

Verified: sync-coding-agents-doc.mjs --check, build-skill.mjs --check,
and prettier --check all pass; 675 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qx1ByUMo3SJhXP7MBxEQ1g

* fix(coding-agents): close the qwen-code harness parity gaps

Rebased onto main (the branch was 98 commits behind and conflicted with the
dcode harness) and closed the sites a new hook harness must also appear in,
found by sweeping every place an existing harness is registered:

- `SKILL_DIRS` (core/skill-sync.ts) did not map qwen-code, so the skill the
  installer copies into ~/.qwen/skills would have been installed once and then
  never refreshed by `npm update -g`. Verified ~/.qwen/skills is Qwen's real
  user-level skills root (Storage.getUserSkillsDirs, SKILL_PROVIDER_CONFIG_DIRS
  = [".qwen", ".agents"]). Added a family-wide guard asserting SKILL_DIRS covers
  every `installSkill` call site, with the five pre-existing gaps (copilot-cli,
  grok-build, cline-cli, dsh, prime-agent) listed explicitly so a new harness
  cannot silently join them.
- The docs site had no qwen-code icon, so the README's own logo would 404, and
  the harness was absent from both coding-agent logo rosters.
- package-lock.json did not carry the three new bins.

Two comments asserted things that are not true of qwen-code 0.22.3 and are
corrected against its source: the hook runner spawns `detached` and calls
`terminateHookProcessTree` on timeout, so a milliseconds/seconds mix-up loses
the retain outright rather than orphaning it; and headless `qwen -p` DOES carry
`submitted_prompt` — the E2E is retention-only because the stub model echoes
only the first 20 000 characters of the request, which Qwen's system prompt
alone exceeds.

Verified end to end against a local API: installed the published tarball into a
container, ran `qwen -p`, and confirmed session_start / reflect_ok / pages_ok /
retain_ok, a document tagged `harness:qwen-code`, and zero injected-memory
leakage in the retained text.

Claude-Session: https://claude.ai/code/session_01DNWFQn8AUN6SApcswHG3aN

---------

Co-authored-by: Mallory M <mallorymiller1984@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 11:16:02 +02:00
Nicolò Boschi 0c2f9e7c2f fix(coding-agents): only ever ADD to a bank's config, never overwrite it (#3966)
Closes #3927.

`configureBank` runs on every session start, and it re-sent the plugin's five
retain strategies and its `knowledge` entity-label group as whole values. The
server stores each of those as ONE config value, so an import replaces the map
outright: a strategy the user had defined was deleted, their edits to the
plugin's own strategies (mission, extraction mode, chunk size) were reverted,
and `retain_default_strategy` could be left naming a strategy that no longer
existed — silently, once per session, on whatever bank the plugin was pointed
at. Pointed at a global bank shared with non-coding work, the plugin took the
bank over.

This is the third fix for one shape. #1270 covered OpenClaw's missions, #2492
this plugin's; each protected only the fields that had just been noticed, and
everything else kept being stamped back. So the rule is now the whole surface
rather than a list: read the bank's current OVERRIDES and write only where the
bank is silent. `codingBankManifest()` returns the template fields still
missing, or nothing at all — a settled bank now makes no import call.

The container fields merge per entry, which keeps the reason the re-apply
exists: a strategy ADDED by a newer release still reaches an existing bank.
What it gives up is deliberate — a release that REWORDS an existing strategy or
label does not reach a bank that already has it; clearing that override takes
the current default back on the next pass.

`manageBankConfig: false` keeps the plugin out of the bank's configuration
entirely, for a bank whose owner shapes it themselves. That bank should then
define the strategies this plugin retains under, and the cost is documented as
the silent one it is: the server does not reject a retain naming a strategy the
bank lacks — `apply_strategy` logs a warning and extracts with the bank's own
config, so a commit diff, a session transcript and a survey marker would all
get the same generic treatment. (The claim that such a retain is *rejected* was
inherited from #3352's rationale and is wrong; corrected here.)

Knowledge pages are seeded either way: they are not bank configuration, and
`pageTriggerType` already governs what they cost.
2026-09-01 11:06:35 +02:00
Nicolò Boschi 4fa110dd59 coding-agents: keep the installed runtime current by itself (#3965)
`install` stages this package into ~/.hindsight/coding-agents and points every wired
agent's hooks at that copy. Nothing ever refreshed it: the only update path was the
user remembering to re-run `install`, so a machine could sit several versions behind
indefinitely — a fix only reached people who happened to re-install. Found on a
machine running 0.4.2 while 0.4.3 had been published for days, with no signal
anywhere that an update existed.

Once a day, at session start, ask the registry for the published version and — when
it is newer — spawn a detached updater. The current session keeps running the code
it already loaded; the next one starts on the new build.

`update` is a new installer command: `install`'s staging half and nothing else. It
replaces the staged runtime (a path stable across versions, so every wired agent
picks the new code up on its next spawn) and writes to NO host config. That
separation is what makes it safe to run unattended — an `install` would need a
harness list, and choosing one on the user's behalf would rewire agents they never
asked us to touch. The cost is bounded and documented: a release introducing a NEW
hook entry point is staged but not referenced until a manual `install`.

Only ever replaces a runtime it can prove npx downloaded. `stageRuntime` records the
directory it copied from, and a copy staged from `npm i -g`, from a project
dependency, or from a local checkout is left to whoever manages that source:
re-staging behind npm's back would leave `npm ls -g` naming a version that is no
longer what runs, and re-staging over a checkout would replace a developer's own
build mid-session. A missing marker means no — it is written on every install from
this version on, and a machine has to re-install once to get this code at all, so a
runtime old enough to lack it is too old to be running the check. Failing closed
costs one manual install; failing open costs somebody their working tree.

Concurrency is real here, not hypothetical: this is a plugin for machines that run
five agents at once, and a 24h stamp does not serialise anything — several sessions
starting in the same second all read "due" before any has written it. Two concurrent
stageRuntime runs are `rmSync(dist)` then `cpSync`, where one process deletes the
directory the other is half way through writing, leaving a runtime with missing
entry points and every hook broken. A lock claimed before the registry call makes a
burst produce ONE request and one updater; same shape as deepen.ts's per-bank lock,
with the holder's pid deciding liveness so a crash cannot wedge the window, and the
stored pid is the detached CHILD's since the copy outlives the session.

Other guards, each with a test: `npx` must be on PATH (without it there is nothing
to spawn, so the check is skipped rather than burning a request and failing a spawn
asynchronously); a prerelease never supersedes the release of the same version; an
unreadable staged version never guesses; the survey's own headless session is
excluded; and both ownership refusals stamp the check so each states its reason at
most once a day.

`autoUpdate: false` (or HINDSIGHT_AUTO_UPDATE=false) pins the installed version,
settable globally, per harness or per bank. `disabled` stops it too — an inert
plugin should stay inert, and a network call plus a background npm install is not
inert.

Wired at BOTH session-start paths — `runSessionStartHook` for the hook harnesses and
`RuntimeCore.seedIfCold` for the persistent-plugin hosts — plus a family-wide guard
test that enumerates session starts structurally rather than from a hand-maintained
list, so a third host cannot land without an update check. Session-start
housekeeping has gone missing on the plugin hosts before (#3524), and the harness
that forgets is by definition the one whose test nobody wrote.

Known window, documented in the module doc: staging replaces dist/ wholesale, so a
hook spawning during the copy can fail to load. Running processes are unaffected,
the window is milliseconds once a day, and the cost is one turn without memory.
Serialising against it would need a lock every hook takes on every turn — a worse
trade than the window it closes.

Also hoists survey.ts's `binExists` to util.ts as `binOnPath`.

Claude-Session: https://claude.ai/code/session_011KDT484YujNcBxHzbfzNbk
2026-09-01 10:45:47 +02:00
Nicolò Boschi 4388e0f95b feat(coding-agents): add native DeepAgents Dcode integration (#3887)
Registers `dcode` (LangChain's deepagents-code) as a native Agent Plugin: root `plugin.json`
contributing the shared skill, the Hooks V2 SessionStart/UserPromptSubmit/Stop lifecycle, and the
`hindsight_*` MCP server, installed through Dcode's own marketplace/plugin manager.

Includes fixes found by running it against deepagents-code 0.1.65:

- Decode `last_assistant_message`. Dcode's transcript lags the Stop event, so that field is
  load-bearing, but it is computed as `str(content)` — a Python repr whenever the provider returns
  content blocks. It was retaining ~1.8KB of encrypted reasoning payload as the assistant turn, and
  never comparing equal to the transcript's clean text, so an already-flushed reply was appended
  again every turn. Guarded family-wide: any harness surfacing the field must declare a decoder.
- Annotate the six read-only MCP tools with `readOnlyHint`. Dcode rejects unannotated MCP calls in
  headless mode, which made recall and the knowledge-page tools unusable under `dcode -n`.
- Parity: local history import (attributed via `dcode threads list --json`, since the transcripts
  carry no cwd), a Docker E2E adapter and image, the harness icon on the docs site as well as the
  control plane, and a clean uninstall that also retires the marketplace it registered.
- Document that Dcode cannot host the codebase survey: `hindsight_ingest_document` writes, so its
  headless runtime gates it by design; it falls back to another agent's CLI like eight other
  harnesses.

Supersedes #3887.

Co-authored-by: Paritoshdagar <paritoshdagar@gmail.com>

Claude-Session: https://claude.ai/code/session_01DNWFQn8AUN6SApcswHG3aN
2026-09-01 10:13:24 +02:00
Nicolò Boschi d7d137fa1e fix(daemon): drop the idle timeout that could kill an in-flight request (#3930)
* fix(daemon): drop the idle timeout that could kill an in-flight request

The daemon's idle checker measured idleness as "time since the last request
*started*" (IdleTimeoutMiddleware stamped last_activity on entry, never on
completion), so a retain, reflect or consolidation that outlived the configured
timeout was SIGTERM'd mid-flight — the client saw a connection error and the
work was lost. Raising the timeout only lowered the odds; 0 (the default
everywhere but the legacy cursor hook package) was the only safe value.

Rather than teach the middleware to count in-flight requests, remove the
feature: a shared local daemon that quietly exits under a long operation is not
worth the resource reclamation it buys. The daemon now runs until it is
stopped.

- Delete IdleTimeoutMiddleware, the idle-checker thread and DEFAULT_IDLE_TIMEOUT.
- Keep `--idle-timeout` parseable — hindsight-embed and every coding-agent
  integration still pass it — but ignore it, printing a note when it is
  non-zero. The integration packages therefore need no change and no release.
- Drop it from the current docs, the SDK/integration READMEs and the generated
  docs skill. The coding-agents README keeps the row marked deprecated because
  docs-freshness.test.ts requires every readable RawConfig field to be
  documented; versioned docs, blog posts and changelogs are left as history.

Closes #3903

Claude-Session: https://claude.ai/code/session_012gXUp1i7YrWmLJVUYki53g

* review: drop the now-dead logger and refresh the comments the removal invalidated

- daemon.py: the module logger only served the deleted idle checker.
- coding-agents daemon.ts / runtime.ts / runtime.test.ts and openclaw: comments,
  a debug line and the plugin-schema description still described an auto-exit
  that can no longer happen. The config fields stay (inert) as agreed.

Claude-Session: https://claude.ai/code/session_012gXUp1i7YrWmLJVUYki53g
2026-08-31 15:59:31 +02:00
Nicolò Boschi e04d02ee62 fix(coding-agents): don't let one stray file abort the Claude import, and give Prime Agent the companion skill (#3771, #3772) (#3812)
* fix(coding-agents): skip a stray file instead of aborting the import, and give Prime Agent the skill

`importLocalHistory` documents that it never throws, but the Claude reader did:
`jsonlFiles` gated on `existsSync`, which is TRUE for a regular file sitting where
a project directory was expected, and the `readdirSync` behind it threw ENOTDIR
straight out of `--import-conversations` — one junk entry killed the whole run
instead of costing that entry. A `listDir` helper now owns "there is nothing here
to list", which also closes the same hole one level up on `~/.claude/projects`
itself. (#3771)

Prime Agent documents the same skill-discovery roots the other skills-capable
hosts use but never received the packaged companion skill, so it only ever picked
one up by accident from the shared `~/.agents` root. It now installs into its own
`~/.prime/agent/skills`: `uninstallSkill` removes a fixed directory name, so
writing to the shared root would make `uninstall prime-agent` delete Codex's and
dsh's copy. (#3772)

Closes #3771
Closes #3772

Claude-Session: https://claude.ai/code/session_01AHvpDBNP3CXfVL88WnazmC

* docs(coding-agents): regenerate the docs skill mirror for the Prime Agent skill line

Claude-Session: https://claude.ai/code/session_01AHvpDBNP3CXfVL88WnazmC
2026-08-26 14:50:52 +02:00
Altay 3b2438356e fix(coding-agents): check knowledge pages before reflecting (#3702)
* fix(coding-agents): check knowledge pages before reflecting

* docs(coding-agents): align pages-first guidance

* docs(coding-agents): regenerate the docs-skill reference from the README

The README edit reached skill/SKILL.md and the docs page but not
skills/hindsight-docs/references/sdks/integrations/coding-agents.md, which
is generated from the README too (by scripts/generate-docs-skill.sh). It
still described autoReflect=false as "the tool guide instead tells the
agent to call hindsight_reflect", contradicting the behaviour this branch
introduces, and verify-generated-files failed on the drift.

All four surfaces now carry the same wording.

Claude-Session: https://claude.ai/code/session_01HVZ94d143NPSsZjbnvwqba

* refactor(coding-agents): name the new-goal trigger for what it now does

Code-review follow-ups, no behaviour change:

- REFLECT_ON_GOALS -> PAGES_FIRST_ON_GOALS. The constant is file-local and
  its old name described the pre-change behaviour (reflect first), which is
  exactly the guidance this branch reverses.
- ToolGuideOpts.reflectOnNewGoals kept its name (two call sites depend on
  it) but its doc comment now records what changed and why, per the
  keep-comments-current rule.
- Explain why the negative assertion deliberately omits the `s` flag: it
  guards the old single-line wording, and with `s` it would span newlines
  and match the legitimate hindsight_reflect / FIRST STOP lines below.

Claude-Session: https://claude.ai/code/session_01HVZ94d143NPSsZjbnvwqba

---------

Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-08-24 14:23:30 +02:00
Sanderhoff-alt 1cbd77d2b8 docs(coding-agents): add Devin CLI to harness list in README intro (#3721) (#3738) 2026-08-24 11:47:50 +02:00
Nicolò Boschi 1b74e3efa8 docs(coding-agents): generate the companion skill from the README (#3735) (#3753)
The skill and the README documented the same configuration in two hand-
maintained files, and they drifted in both directions.

The skill never named `bankIdTemplate`, `dynamicBankId`, `optInOnly`,
`optInPaths`, `retainTags`, `retainMetadata`, `resolveWorktrees`,
`maxParallelRetains` or any daemon setting, and it stated outright that
the package reads "no environment variables" — there are 35
(core/config.ts ENV_KEYS). That is the half #3735 reported. The README
had lost the other half: `autoReflect` and `surveyRefreshCommits` were
documented only in the skill, and neither file mentioned that
`HINDSIGHT_CONFIG` relocates the config file, that the layering chain
ends at `banks.<resolvedBankId>`, or that `optInPaths` takes a
comma-separated env value like its sibling `retainTags`.

The README is now the single source. scripts/build-skill.mjs copies the
regions it marks with `skill:begin` / `skill:end` into skill/SKILL.md,
normalising heading levels per region; only the agent-facing half — which
tools to call, crediting memory, correcting a stale memory — stays
separate, in skill-src/preamble.md. The docs page was already generated
from the README; its generator now strips the markers, which MDX cannot
parse.

The skill's operative debugging knowledge (sync-status readiness,
resetting a bank, the survey-baseline/gitlog marker documents, the
session_start/deepen_started triage rule) moved into the README, so it
reaches users and the docs site too.

src/docs-freshness.test.ts fails when the skill is stale, when a field of
`RawConfig` is readable from a config file but named nowhere in the
README — the check that makes this class of drift impossible to
reintroduce — and when a documented claim about the env layer stops
holding (every key really is `HINDSIGHT_<FIELD_IN_CAPS>`; the four
map-valued settings really are file-only). It also unit-tests the marker
parser. prepublishOnly regenerates the skill so a publish cannot ship a
stale copy.
2026-08-24 11:06:28 +02:00
Nicolò Boschi 069ed96f35 fix(coding-agents): inherit opt-in across worktrees (#3471) (#3627)
* fix(coding-agents): inherit opt-in across worktrees

* fix(coding-agents): resolve removed worktree paths

* fix(coding-agents): retain opt-in after worktree removal

* fix(coding-agents): resolve worktree approval through one cascade

Review follow-up on the worktree opt-in inheritance.

`lookupDirectories` walked its own copy of the candidate cascade that
`gitProjectName` already had, and the two disagreed: bank identity
consulted the session root, approval and mapping did not. A Codex
session in a removed worktree therefore fell off its mapped bank, while
the same session under Claude Code kept it via CLAUDE_PROJECT_DIR — the
one harness that exports a project root.

Hoist the cascade into `mainWorktreeRoot` and resolve both through it.
`deriveBankId` already takes the session root and now passes it down
instead of dropping it on the mapPathToBank path.

* docs(coding-agents): sync the generated docs page with the README

---------

Co-authored-by: Altay <altay@hey.com>
2026-08-19 15:40:35 +02:00
Nicolò Boschi 188eaa3dc7 fix(coding-agents): honor retainSessions in the hook harnesses (#3596) (#3607)
* fix(coding-agents): honor retainSessions in the hook harnesses (#3596)

`retainSessions` was parsed, defaulted, env-mapped and accepted as a known
config key, but only `RuntimeCore` (opencode, Kilo, Cline, Prime Agent, dsh)
ever read it. The shared Stop-hook flow behind every hook harness — claude-code,
codex, cursor-cli, copilot-cli, devin-cli, grok-build, antigravity-cli — checked
`disabled` twice and `retainSessions` never, so `retainSessions: false` (global,
per-harness or in a `banks.<id>` section) wrote the transcript back anyway. The
comment claimed this was deliberate while the docs sold the flag as a general
write-back opt-out, including a per-bank example.

Gate the write-back in `runRetainHook`, after `applyBankConfig` so a bank
section can flip it either way, and before `ensureDaemon` — a session that
writes nothing has no reason to bring a server up. A `retain_disabled` diag
record replaces the `retain_ok` that used to appear, so the opt-out is
verifiable in the diagnostic log.

`deepen`'s conversation-history import is the same door one session later: it
reads the harness's own history files and files them as `chat:<id>`. Honoring
the flag in only one of the two places would have left the opt-out cosmetic, so
it skips the import too. Git ingest, seeding, knowledge pages, recall and the
memory tools are all untouched — that separation is what distinguishes this flag
from the `disabled` kill switch.

Tests: four end-to-end `runRetainHook` cases (default writes; global false
writes nothing and builds no client; a bank override opts one repo out; a bank
override re-enables under a global opt-out), two of which fail against the
pre-fix code. Plus a family-wide structural guard in the shape of
`daemon.test.ts`'s "every harness entrypoint reaches a daemon": every module
calling `retainLiveSession` or `ingestChats` must consult the flag. The path
that forgot is by definition the one with no test, so the guard is asserted over
the whole family rather than per-harness.

* chore(docs): re-sync the coding-agents page after the README reflow
2026-08-19 11:51:25 +02:00
Nicolò Boschi 39de3974ea fix(coding-agents): keep a long-lived host's credential live, and say which one it used (#3600) (#3606)
* fix(coding-agents): keep a long-lived host's credential live, and say which one it used (#3600)

`HindsightClient` copied `apiToken` at construction and never re-read it, so a
host that outlives its credential — dsh, Cline, Kilo, Prime Agent, opencode,
the MCP server — kept signing with a key the operator had already replaced.
Enabling auth or rotating the key mid-session 401'd every call until the whole
host restarted, while `hindsight_diagnose` re-read the file and reported the
situation as healthy. The one-shot hook binaries were immune, which is why the
same machine showed working hooks alongside dead in-session tools.

The credential is now resolved through a provider on a 401 and the request
replayed once, but only if the re-resolved token actually CHANGED — a genuinely
wrong key still surfaces as one 401 rather than doubling every failing request.
The happy path never touches the filesystem.

All three fetch paths go through one signing helper. `reflect` and the drain
poll fetched directly, so a recovery wired into `req()` alone would have left
them failing forever.

Both #3600 and the two drifts below come from the same shape: five hosts each
carried their own copy of loadConfig -> deriveBankId -> applyBankConfig ->
new HindsightClient. So the fix is one shared builder (core/host-client.ts)
rather than a sixth line pasted into each. Hoisting it fixes two settings that
had already gone missing that way:

- dsh and Prime Agent never passed `maxParallelRetains`, so both silently
  ignored it and always used the default 10.
- dsh never passed the directory to `applyBankConfig`, so `optInOnly` was not
  enforced there at all: an unapproved repo still got a bank.

`hindsight_diagnose` now reports the credential IN USE next to the one on disk
(booleans only, never the value), resolved through the same pipeline the host
used — including a per-bank `banks.<id>.apiToken`, which a bare loadConfig()
comparison would have reported as a permanent false mismatch. Without this the
drift stays invisible to the one tool whose purpose is to explain it.

A 401 also now says whether a credential was even sent. The server answers
identically for "no key" and "wrong key"; only the client knows which it was.

Behaviour change worth naming: `disabled: true` now wins uniformly. It already
did for every host except the MCP server, which applied the `banks.<id>`
section first and so could be re-enabled per bank; `optInOnly`/`optInPaths` is
the supported way to run memory in only some projects. Resolution also stops
before bank derivation when disabled, since that shells out to git and the
disabled path exists to be a zero-overhead baseline.

Reported with a verified local patch and a full root-cause analysis by
@allenliang2022 in #3600; this implements that approach.

* docs(coding-agents): say when a config change takes effect, and that the token is the exception

Nothing in the README or the skill said when an edit to
~/.hindsight/coding-agent.json actually applies — and the answer differs per
host: a hook harness re-reads the file on every invocation and picks a change
up on the next prompt, a persistent plugin holds it for the life of the agent
process, and the MCP server for the session.

That gap got worse, not better, with the credential fix: the apiToken row now
says it is picked up without a restart, which reads as "config is live" unless
the rule it is an exception to is written down somewhere.
2026-08-19 11:40:41 +02:00
Nicolò Boschi 0de91b8b73 fix(coding-agents): let hindsight_reflect wait as long as it is configured to (#3590) (#3592)
The `hindsight_reflect` MCP tool aborted every call at a hardcoded 120s, no
matter what `reflectTimeoutMs` was set to: the handler passed no `timeoutMs`,
so `HindsightClient.reflect()` fell back to its own 120s default. On a
populated bank, `budget: "high"` synthesis routinely runs longer than that —
the identical direct API call succeeded — so the tool was unusable and the
config field was dead.

Both paths that build the tools dropped the setting, not just the one filed:
`selectTools()` (MCP server) and `RuntimeCore.toolSpecs()` (the persistent
plugin harnesses — opencode, Kilo, Cline, dsh, Prime Agent).

The tool's window is now its own knob, `reflectToolTimeoutMs`, defaulting to
330s — above the server's own reflect wall timeout (300s), so the server
decides when to give up rather than an arbitrary client deadline. It inherits
an explicitly raised `reflectTimeoutMs` (the field users already reach for),
but a short one never lowers it: that value bounds an automatic hook which
must fit the host's 25s window, not a call the agent is waiting on.

`reflectBudget` makes the hardcoded `budget: "high"` configurable too, for
large banks where high-budget synthesis exceeds the server's wall timeout.

To stop this recurring, `reflect()`'s `timeoutMs` is now required — the right
deadline differs by an order of magnitude between the hook and the tool, so
there is no sensible default to fall back to silently.
2026-08-19 10:47:31 +02:00
Nicolò Boschi 8b78b4ac04 test: assert memory state via the engine read API, not raw SQL (#3591)
The suite asserted memory state by querying `memory_units` / `memory_links` /
`unit_entities` directly. That couples tests to the physical schema and makes
them unable to run against any store that keeps memory rows outside Postgres.
This ports what the read API can answer, extends it where it could not, and
marks the residue that is Postgres-shaped by nature.

**Ported to the engine API.** The "unconsolidated count" assertions spelled out
`consolidated_at IS NULL AND consolidation_failed_at IS NULL AND fact_type IN
('experience','world')` — character-for-character what
`list_memory_units(consolidation_state='pending')` already means, so seven sites
across five files became one call. Observation lineage, entity/tag checks and
chunk provenance moved to `list_memory_units` / `get_memory_unit` /
`list_document_chunks`; where the old query was an inner join, the port keeps
the same filtering and says why.

**Read model extended** so the rest could follow: `updated_at` and
`source_memory_ids` on list items, `entity_kind` on entity items, and a
list-valued `fact_type` that matches any of them. Every field is a column of the
row the query already fetched — the projection grows, the plan does not — so no
opt-in flag was needed and production paths pay nothing. Covered by a new test
module driving it all through the engine on retain-written units.

**260 of 6297 tests marked `memory_backend_incompatible`**, in two passes: those
that assert Postgres-internal state (raw `memory_links` counts, `embedding` /
`search_vector`), and those whose fixtures only exist in Postgres. The second set
was chosen on evidence — each both failed against a non-SQL store and touches
those tables in its body, a helper, or a fixture — never by grep alone. Postgres
runs are unchanged; the marker only takes effect behind
`-m 'not memory_backend_incompatible'`.

Also green-lights two checks that were red before this branch: the repo formatter
over five test files, and the coding-agents docs generator, which now rewrites our
own doc links to site-relative the way it already did for assets — the naive
regeneration would have degraded the docs-skill reference's file-relative links.
2026-08-19 00:23:44 +02:00
Nicolò Boschi 27e4b188d7 fix(coding-agents): consolidate one set of observations per bank (#3575)
Every document this integration writes carries provenance tags (`source:chat`,
`harness:<id>`, `knowledge:<kind>`, anything from `retainTags`). Consolidation's
default `combined` scoping groups observations by a memory's WHOLE tag set, so
those tags become a consolidation boundary: work one repo with two agents and
the `harness:<id>` tag alone yields two parallel sets of beliefs that never
merge, each blind to the other, at double the consolidation cost (#3564).

Retain with `observation_scopes: "shared"` instead — one global scope per bank,
which is what a bank already is: one project's memory. The tags stay on the
facts, so recall filtering, the documents-list filter and each document's agent
logo are unaffected.

New `observationScopes` config field (default `"shared"`) sets it, per bank via
`banks.<id>` like any behavioral field, or `HINDSIGHT_OBSERVATION_SCOPES` for
the scalar modes. `"combined"` restores the previous behaviour.
2026-08-18 12:01:50 +02:00
Nicolò Boschi a04f4eed3a fix(coding-agents): pin the bank to where the session started in a non-git tree (#3573)
Fixes #3563.

Outside a git repository `{gitProject}` fell through to the basename of the
agent's LIVE working directory. Inside a repo that is harmless — every
subdirectory resolves back to the root — but a plain directory tree has no root
to resolve to, so the bank id followed the agent as it `cd`d and ONE session was
retained into a bank per directory it stepped into: the same document, its facts
split across `coding-agent::2026-07-22` and `coding-agent::analysis`, and a
recall against either seeing only part of the history.

Resolve it from the directory the SESSION started in instead. `sessionRootDir`
records the first cwd any of a session's hooks reports and returns it for the
rest of the session, keyed on the session id rather than a harness-exported
project-root variable — every hook harness reports a session id, only Claude
Code exports a root, so this covers all seven the way `nearestExistingDir` does
rather than guessing at env var names. It lives in its own temp file for the
same reason the retain cursor does: the prompt hook rewrites the session cache
wholesale.

Deliberate boundaries, each pinned by a test: git resolution still runs first,
so a session started in a plain directory that then works inside a repo keeps
that repo's bank; `mapPathToBank` still overrides everything; and `{project}`
stays on the live cwd, since it is documented as the working-directory basename
and is the escape hatch for anyone who wants a bank per directory.

The persistent-plugin harnesses (dsh, opencode, Kilo, Cline, Prime Agent) need
no change — RuntimeCore captures projectDir once at construction. The Antigravity
status line cannot use a session root (its payload carries no conversation id);
that is commented at the call site and is display-only. `retainTags`
/`retainMetadata` take the session root too, so a stamped `{gitProject}` names
the project its bank id does.
2026-08-18 11:45:00 +02:00
Nicolò Boschi f8b3988cf8 feat(coding-agents): make the knowledge-page refresh trigger configurable (#3545)
* feat(coding-agents): make the knowledge-page refresh trigger configurable

Every page this plugin creates was stamped with one hardcoded trigger:
refresh after every consolidation, full rebuild from scratch. That is the
most current setting and the most expensive one -- an LLM synthesis per
page per consolidation -- and on a few heavy auto-surveyed repos it adds
up to real, unexpected spend with no way to opt out short of patching
dist/ or fixing pages up after the fact.

Three config knobs, defaulting to exactly today's behaviour:
  pageTriggerType  reactive (default) | cron | manual
  pageTriggerCron  the schedule, when type is cron
  pageTriggerMode  full (server default) | delta -- edit instead of rebuild

`buildPageTrigger(cfg)` replaces the PAGE_TRIGGER constant and also
replaces the second, hand-copied instance of the same literal in
`captureInitiative`, so seeded pages and captured initiatives can no
longer drift apart.

A `cron` type with no expression is a broken config, not a request to stop
refreshing -- the API rejects it and page creation would fail -- so it
warns and falls back to the default. `manual` is how you ask for no
automatic refreshes.

Closes #3506.

* fix(coding-agents): stop clobbering the API's knowledge-page refresh contract

`create_knowledge_page` applies KNOWLEDGE_PAGE_DEFAULT_TRIGGER only when
the client sends NO trigger: a trigger REPLACES that default rather than
merging into it. Every page this plugin created therefore lost the two
settings that make a knowledge page a knowledge page --

  mode: "delta"              -> refreshes rebuilt the page from scratch
  exclude_mental_models: true -> refreshes reflected over sibling pages

-- which is a large part of the cost #3506 is about. Both are now sent
explicitly on every page, under every trigger type, and neither is
configurable: they are the API's contract for a page, not a preference.

Drops the pageTriggerMode knob accordingly. What remains configurable is
WHEN a page refreshes (pageTriggerType/pageTriggerCron), and the README
now names the API field each flag maps to.

Also reformats the README's config table under the repo's pinned prettier
(the new rows changed its column widths -- CI's verify-generated-files
caught it).

* fix(coding-agents): send only what the plugin decides in a page trigger

Follow-up to the API fix that makes a page trigger MERGE over
KNOWLEDGE_PAGE_DEFAULT_TRIGGER instead of replacing it. With that in
place the plugin no longer has to restate the server's own page defaults
(mode: delta, exclude_mental_models) to avoid losing them -- doing so
would just freeze a copy that drifts the next time they change.

What stays is what this plugin actually decides: fact_types (its pages
are tag-scoped syntheses over knowledge:<tier> labels on world and
experience facts, not the observation-only page default) and the refresh
policy the new config knobs select.

Against a server without the merge fix, pages keep the behaviour they
have shipped with all along -- no regression, no improvement until the
server is new enough.

* refactor(coding-agents): name the trigger types after the product's own terms

`pageTriggerType: "reactive"` invented a word for something the docs, the
API and the control plane already call auto-refresh. The three values are
now auto-refresh | cron | manual.

* docs(coding-agents): say plainly that the trigger applies to new pages only

Changing pageTriggerType does not migrate a repo's existing pages -- a page
keeps the trigger it was created with. Worth stating outright, since the
repos that most want "manual" are exactly the ones already seeded.

* docs(coding-agents): list the page-trigger knobs in the companion skill

The skill enumerates the behavioral config fields for the agent to answer
from; a knob missing there is invisible to every user who asks the agent
how to configure memory.
2026-08-18 10:55:18 +02:00
Nicolò Boschi f970174213 fix(coding-agents): stop forcing a 300s idle timeout on the shared daemon (#3544)
`daemonEnv` set `HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT` unconditionally from
a plugin-side default of 300, so a daemon started by this plugin retired
itself after five minutes of inactivity -- long enough to happen during a
build, a test run or a meeting -- and the next `hindsight_*` call hit a
closed port.

Nothing else on the machine asks for that. `hindsight-embed`'s own default
is 0 (never auto-exits), and every other integration's settings.json ships
0 explicitly; the coding-agents plugin was the sole outlier, and the daemon
it retires is shared by every agent and repo.

Unset now means unset: the env var is only forwarded when the user actually
configures `daemonIdleTimeout`, and the daemon keeps its own default
otherwise.
2026-08-17 13:21:45 +02:00
Nicolò Boschi 28760f62d4 feat(coding-agents): DeepSeek Harness (dsh) support (#3504)
Adds `dsh` as a persistent-plugin harness: a native Cordis plugin that binds
DeepSeek Harness's typed lifecycle events to the shared RuntimeCore.

  agent/session-start  -> seedIfCold      (cold check + background seed)
  agent/pre-step       -> onPrompt        (recall) + the injection as a
                          `{kind:'plugin', form:'recall'}` message
  agent/turn-stopping  -> onSessionIdle   (write-back of the completed exchange)
  ctx.tools            -> the hindsight_* suite, registered natively

Its Claude Code / Codex hook bridges are deliberately not used: neither ships in
a default profile, so a bridge would cost the same install while losing the
session id, the transcript and the awaited stop boundary.

dsh is the first host where ONE process serves SEVERAL repositories — its Web UI
opens each session in whatever directory the user picks — so the bank, client and
seed are resolved per session workspace and the core is constructed with that
workspace root, which is what binds the tools' git checks to the right repo.

The plugin imports nothing from dsh: host shapes are structurally typed and tool
definitions are built in the registry's own shape, so there is no dsh package for
pnpm to resolve inside a profile and no version to keep in step.

Also here:
- backfill reads dsh session logs. They are a CONCATENATION of zstd frames, and
  both of Node's decoders stop after the first one — a plain decompress returns
  only the header line — so core/zstd-frames.ts walks the frame structure and
  decodes each frame (RFC 8878 §3.1).
- transcript normalization keeps only `source.kind === 'user'` messages: dsh
  delivers plugin context (its runtime snapshots, the skill catalog, our own
  recalled memory) as user-role messages on the same surface.
- describeError: Node's fetch reports every transport failure as the bare string
  "fetch failed" and hides the reason on `cause`, which made an unreachable
  apiUrl an investigation instead of a log line.
- vitest pins HINDSIGHT_CONFIG at a path with no file; loadConfig otherwise
  resolved the developer's real ~/.hindsight/coding-agent.json, so a machine with
  a token configured failed assertions a clean machine passed.

Verified against @deepseek-ai/dsh 0.1.0-rc.6: recall reaches the model, all 8
tools reach the model and dispatch, sessions are retained, and the Docker E2E
(e2e/Dockerfile.dsh, driven through the stub model like the other credential-less
harnesses) runs the whole lifecycle in a container.
2026-08-14 17:56:57 +02:00
Nicolò Boschi 77bbb93940 feat(coding-agents): optInOnly — run memory only in projects that were opted in (#3433)
Closes #3427.

By default every project gets memory, which is what makes the plugin zero-setup.
For shared machines and client work that is the wrong default: unrelated
directories create banks nobody asked for, and there was no way to say "off
unless I name it".

  { "optInOnly": true, "optInPaths": ["~/work/client-x", "~/oss"] }

Anything outside those paths is inert — no bank created, nothing retained, no
seed — and the agent behaves as it would without the plugin.

Approval is deliberately separate from routing. `optInPaths` says WHICH PROJECTS,
not which bank, so an approved repo keeps its usual coding-agent::{gitProject}
name and approving costs no naming decisions. Paths are prefixes with `~`
expanded, so approving ~/work approves the repos under it while each still gets
its own bank. That is why this is not built on mapPathToBank, which the issue
reporter and I both first reached for: using it for approval forces you to name a
bank per project and collapses a whole tree into one.

A mapPathToBank entry does count as opted in — routing a path to a named bank
already declares that project. A bare bankId does not: it names a bank rather
than a project, so it cannot express which work may be remembered, and a privacy
switch has to fail closed.

Enforced through the `disabled` gate every entry point already checks after bank
resolution: applyBankConfig takes the directory the bank came from and returns a
disabled config when it is not opted in. That reuses a path already known to stop
a run before anything creates a bank, rather than adding a second thing nine call
sites must remember.

Verified end to end against a live server as well as in unit tests: an unlisted
project produced no plugin events and no bank, while an opted-in one under the
same config seeded, injected and retained normally.

Why not a `.hindsight.json` in the repo (the issue's Idea C): the config module
deliberately reads no repo-carried file, because an untrusted repository must not
be able to influence memory behaviour — and here it would let a cloned repo turn
memory ON, which is exactly backwards for a privacy control.
2026-08-12 14:48:10 +02:00
Nicolò Boschi 098362d450 feat(coding-agents): handle 429 on both the request and the poll path (#3423)
* feat(coding-agents): retry a rate-limited write-back instead of dropping it

A 429 on POST /memories was indistinguishable from a 500: `req` threw a plain
Error, the write-back was abandoned for that turn, and nothing backed off. The
content survived — the cursor stays dirty, so the next Stop replaces the whole
document — but the turn's write simply did not happen, and on a session's LAST
Stop there is no next one.

429 now raises a typed RateLimitedError carrying the parsed Retry-After, and the
session write-back retries on it. Retrying is safe rather than duplicative
because the payload carries a deterministic operation_id: an identical
resubmission is collapsed into the original operation server-side, so a retry
after a response we never saw cannot write twice.

Two conditions bound it, and the first is the point:

- ONLY while our write is still the newest. If another write-back has claimed the
  cursor since — a newer turn, another process — ours is superseded: the newer
  one carries what we were sending, or replaces the document outright because our
  failure left the cursor dirty. Retrying then would spend a hook's remaining
  time re-sending content already on its way.

- ONLY within a hook's clock. Claude Code allows 60s for a Stop hook and a cold
  daemon can already have eaten most of it, so a Retry-After that does not fit a
  6s budget is not honoured at all — waiting less than the server asked would
  just earn another 429, and waiting the full amount trades a deferred retain for
  a killed hook.

Anything that is not a rate limit still fails immediately, as before.

* feat(coding-agents): add maxParallelRetains config and 429-aware drain

* fix(coding-agents): cap the 429 backoff so a long Retry-After cannot park a drain

Review follow-up on the retry path.

`Retry-After` was honoured without a ceiling, so an hour-long value — an
incident, a misconfigured limiter, a proxy inventing one — would park the drain
for that hour, up to the whole `maxMs`, with the background seed frozen behind
it. The header is a server's hint, not a budget we owe it.

Capped at 60s. That keeps the signal (the floor and the header still lengthen the
wait) without handing over the schedule: if the limit still applies, the next
poll gets another 429 and backs off again.

Also re-syncs the docs page and skill mirror from the README, which the rebase
onto main left stale by a column width.

---------

Co-authored-by: David Eriksson <david.eriksson@avanza.se>
2026-08-12 10:43:52 +02:00
voarsh2 9032ed9c95 feat(coding-agents): apply retain attribution to all ingestion paths (#3418)
* feat(coding-agents): apply retain attribution to all ingestion paths

* refactor(coding-agents): one retain call per survey marker, not two

Review follow-up. Both survey-baseline writes branched into two complete
client.retain(...) calls that differed only in whether `{ metadata }` was passed
— six duplicated argument lists between them, which is where a later edit updates
one and misses the other.

`retain` only assigns metadata when it is truthy, so `{ metadata: undefined }`
already omits it and one call covers both cases. That is what knowledge-tools.ts
does with `Object.keys(metadata).length ? { metadata } : {}`.

The marker assertion in session-start.test.ts gains the opts argument, since the
call now always passes one. Behaviour is unchanged: with no retainMetadata
configured the stamp is empty and nothing reaches the API.

---------

Co-authored-by: Reese <reesevader@hotmail.co.uk>
Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-08-12 09:53:31 +02:00
Nicolò Boschi 4fa25f47d2 feat(coding-agents)!: drop retainEveryTurns; batching belongs on the server (#3415)
The write-back cadence is removed. The persistent-plugin harnesses (opencode,
Kilo, Cline CLI) now write back every turn, which is what the default of 1
already did; the hook harnesses never consulted it at all.

Two reasons, and the second is what makes it a removal rather than a
documentation fix.

A client-side cadence holds turns in a process the host can close at any moment,
and there is no reliable signal on the way out: #2869 measured ZERO SessionEnd
retains across 36 hours because Claude Code cancels that hook at shutdown. The
flush a cadence needs cannot be built on the only event that would carry it, so
extending the setting to the seven hook harnesses was never available — and the
asymmetry that left (honoured by three of eleven) was itself the complaint.

The batching it approximated now happens server-side, where nothing can be
stranded: queued retains for one document fold into a single execution
(`engine.retain.fold`, #3395), on top of a claim predicate that serialises
same-document work. Submitting every turn therefore costs one extraction over
the concatenated turns rather than one per turn — the burst control #2143 asked
for, in the place that can do it without risking a turn.

`retainSessions` stays: opting out of write-back entirely is a different
question from how often it fires.

Removed from the config type, the resolver, HINDSIGHT_RETAIN_EVERY_TURNS, the
runtime check and its now-unread retainedUsers bookkeeping, the settings table,
and the tests. The other integrations' `retainEveryNTurns` is a separate setting
in separate packages and is untouched.
2026-08-12 08:10:54 +02:00
Ben 29cd6c6ab0 feat(coding-agents): add Prime Agent as a supported harness (#3240)
* feat(coding-agents): add Prime Agent as a supported harness

Adds Prime Agent (PrimeIntellect) as a plugin harness in
@vectorize-io/hindsight-coding-agents, giving it the shared reflect-and-inject
core (auto-seed, knowledge pages, attribution, per-repo bank naming) like
opencode/cline. A Prime Agent extension entry (src/prime-agent.ts) wires
before_agent_start -> recall + system-prompt injection and agent_end ->
transcript write-back onto RuntimeCore, and registers the hindsight_* knowledge
tools natively via pi.registerTool (Zod raw shape -> JSON Schema; Prime Agent
forwards it to the model provider). Installer registers the built extension in
~/.prime/agent/settings.json; registry lists it for backfill. Includes a
transcript normalizer, unit tests for the hooks/tool adapter/converter/installer,
a README entry, and the synced docs page.

Supersedes the standalone @vectorize-io/hindsight-prime-agent package.

* fix(coding-agents): register Prime Agent in the control plane, wire its E2E, pass the workspace

Follow-ups from review, on top of a rebase onto main (the branch was 66 commits
behind and predates the append write-back, bounded transcript reads, the retain
stamp and both 0.2.x releases).

Control-plane registry. CLAUDE.md requires a new harness to land its logo entry
in the same change, and this one didn't: documents retained by Prime Agent carry
metadata.harness = "prime-agent" and a harness:prime-agent tag, which the
documents list resolves a logo from, so every one of them rendered as bare
metadata. Verified against a real local run before fixing. Neither guardrail
catches this — the parity test's EMITTED_HARNESSES is a hand-maintained list, and
it lives in the control plane, so `detect-changes` never runs it for a
coding-agents-only PR. Added to the registry, the icon copied into
public/img/harness, and the id added to that list so it is covered from now on.
The mark is dark line art, so invertOnDark like the other monochrome ones.

E2E. Every other harness has a Docker E2E entry; this one had none. Added
Dockerfile.prime-agent and a setup entry, so it joins the roster that installs
the CLI, seeds a bank with a decision the prompt never mentions, and requires the
agent to carry it back out. It skips cleanly without credentials, like the rest.
Prime Agent ships no npm package — the upstream repo is a private monorepo — so
the image uses the vendor installer and puts ~/.local/bin on PATH.

createRuntime passed four arguments to RuntimeCore, dropping the workspace
directory added in #3346. Harmless today because repoPath is process.cwd(), but
{gitProject} and {project} in retainTags/retainMetadata would silently resolve
against the wrong directory the moment those diverge; plugin-entry and cline both
pass it explicitly.

Regenerated the docs skill mirror, which verify-generated-files was failing on.

---------

Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-08-12 07:03:48 +02:00
Nicolò Boschi 291d2b1d9d docs(coding-agents): say which harnesses retainEveryTurns applies to (#3380)
It has exactly one consumer, RuntimeCore — the persistent-plugin harnesses
(opencode, Kilo, Cline CLI), which stay loaded and therefore choose when to write
back. The hook harnesses ignore it, and not by oversight: the host decides when
they run, and each Stop is a fresh process with no memory of how many turns have
passed.

Neither the config comment nor the settings table said so. Its neighbour
retainSessions does spell out the same split ("hook harnesses always write on
Stop"), so the silence read as "applies everywhere" — a Claude Code user setting
retainEveryTurns: 5 gets no effect and no warning. That is the workaround #2143's
reporter reached for, so the gap was load-bearing.

The README also said "opencode", which undersold it: Kilo and Cline CLI share
that runtime.
2026-08-11 12:44:18 +02:00
Nicolò Boschi a22667dc06 feat(coding-agents): retainTags / retainMetadata, with HINDSIGHT_RETAIN_TAGS (#3269, #2896) (#3346)
* feat(coding-agents): retainTags / retainMetadata with template placeholders

Closes #3269.

Every conversation retain carries `source:chat` and `harness:<id>` — what wrote
the memory, but nothing about where it came from. That is fine while each repo
has its own bank, since the bank is the answer. It stops being fine on a
deliberately shared bank, the setup in the issue: one bank holding cross-project
knowledge so facts recall everywhere, where a retained fact then carries no
record of the repository it came out of.

Both settings take `{placeholder}` templates resolved per retain, against the
vocabulary the dynamic bank id already uses plus what only a retain knows:

  {gitProject} {project} {harness} {bankId} {sessionId} {timestamp}
  {channel} {user}

  { "retainTags": ["project:{gitProject}"], "retainMetadata": {"repo": "{gitProject}"} }

{gitProject} is worktree-aware here too, so linked worktrees of one repo stamp a
single name rather than project:app and project:app-wt2.

The substitution itself moves to core/template.ts, shared with bank.ts rather
than duplicated — each call site keeps its own resolver map, because the valid
placeholders genuinely differ (a bank id cannot reference {bankId}).

Two things are deliberately not user-controllable. Built-in metadata is written
last and wins, and retainTags entries in the `source:`/`harness:` namespaces are
dropped with a warning: the documents list filters on those and resolves each
document's agent logo from them, so a template that could forge them would break
attribution for everyone reading the list.

Unconfigured, this adds nothing to a retain.

* docs(coding-agents): document retainTags/retainMetadata in the README, not the generated page

The docs page is generated from the integration's README by
hindsight-docs/scripts/sync-coding-agents-doc.mjs, and build-docs runs it with
--check. The first pass edited the generated page, so the build failed with
"docs page is out of date with the README".

Same content, moved to the source and re-synced (README, generated page and the
docs skill mirror). The row's cross-reference is plain text rather than an anchor
link because the generator flattens links.

* feat(coding-agents): HINDSIGHT_RETAIN_TAGS env override

Closes #2896.

The old Claude Code plugin had HINDSIGHT_RECALL_TAGS but no retain counterpart,
so per-project retain tagging could only be configured globally in the file. Now
that retainTags exists here, it joins the env surface on the same convention:
HINDSIGHT_ + the field in SCREAMING_SNAKE, still a FALLBACK the file wins over.

It is a list rather than a scalar, so a new ENV_LISTS branch splits on commas and
trims — blank entries dropped, so a trailing comma or "a,,b" is a typo rather
than an empty tag reaching the API.

  HINDSIGHT_RETAIN_TAGS="project:{gitProject},env:work"

retainMetadata deliberately gets no env form: it is map-valued, and per-key
branching doesn't survive flattening into one variable — the same rule already
applied to mapPathToBank, harnesses and banks.

Also corrects this file's header, which still claimed the plugin reads no
environment variables at all — untrue since ENV_KEYS was added.
2026-08-10 17:14:30 +02:00
Nicolò Boschi fecacc4ea8 docs: launch Coding Agents + mark the per-agent plugins superseded (#3162)
* docs: launch the Coding Agents page and mark the per-agent plugins superseded

Hold until the Coding Agents plugin is announced — merging this makes the page
public and tells existing users their plugin is legacy, so it should land with
the announcement rather than before it.

Two halves:

- Launch. Undoes the deliberate hide: the page drops `unlisted`, its entry
  returns to integrations.json (which drives both the gallery and the sidebar),
  and coding-agents leaves the EXCLUDED set in check-integrations.mjs, so the
  released-tag check guards it like every other integration.

- Supersede. The six overlapping integrations — claude-code, codex, opencode,
  cursor-cli, cline, copilot-cli — get an admonition in the style already used
  on the Hermes page: what replaces them, the install command for their harness,
  and a link to the migration section. Pages and packages keep working; nothing
  is deleted and no registry deprecation is published, so existing links and
  installs are unaffected.

Each notice states plainly that memory does not move — the old plugins scope a
bank per agent per project where this one uses a bank per repo — and points at
`--import-conversations` for Claude Code and Codex, the two whose transcripts
record enough to attribute a session to a repo. The other four say so instead of
implying an import exists.

* docs: group the integrations sidebars into coding agents / frameworks / apps

Both sidebars driven by integrations.json rendered one flat run of 59 entries.
Split them into three groups so a coding agent is distinguishable from an SDK.

The existing `category` field couldn't drive this on its own: its `tool` bucket
mixed CLI agents and editors with chat apps, note-taking and voice platforms.
The 19 coding agents move to a new `coding-agent` category; `framework` is
unchanged, and `tool`/`mcp` become the catch-all group.

Grouping lives in src/lib/integration-groups.ts, kept free of the @site alias and
of any JSON import so both consumers can use it — the theme swizzle (webpack) and
sidebars-integrations.ts (evaluated at config load). An unrecognised category
falls into the last group rather than disappearing from the sidebar.

* docs: regenerate the docs-skill mirror for the supersede admonitions

scripts/generate-docs-skill.sh mirrors docs-integrations/ into skills/; the
launch commit edited seven pages without re-running it, so the mirror still
described the per-agent plugins as current.

* docs: spell out the migration path for Claude Code and Codex

Both pages said memory "does not move automatically", which is now only half
true: the server endpoint IS carried over (~/.hindsight/claude-code.json and
~/.hindsight/codex.json, same keys), so nobody is silently switched to Cloud.

Each page now states what moves — the endpoint automatically, conversations via
--import-conversations — and what does not: the recall/retain settings, missions
and bank-naming options. It also says why conversations come from local
transcripts rather than the old bank: that bank defaulted to a single static
bank shared by every project, whose documents record only a session id, so
attributing them to a repo requires the local transcripts regardless.

These are the only two superseded plugins with an endpoint to carry; the other
four pages already say their history can't be imported and are unchanged.

* feat(claude-code,codex): deprecation notice in the old plugins' sessions

Folded in from #3205 so the launch lands as one change: the docs that announce
the Coding Agents plugin and the in-session notice that points existing users at
them ship together, rather than one arriving without the other.

Both plugins keep working, but they are deprecated — development has moved to
@vectorize-io/hindsight-coding-agents. A changelog entry reaches nobody who
installed a year ago, so the SessionStart hook says it, via systemMessage (the
channel Claude Code shows the USER; additionalContext would only reach the
model). Codex accepts the same hook output shape, so one design serves both.

Emitted before the existing early returns: neither the memory settings nor
whether the server is reachable changes the fact that the plugin is deprecated.

Shown every session, with `"upgradeNotice": false` as the permanent opt-out —
stated in the message itself, which is what makes that frequency acceptable.
With no rate limit there is no state file and none of its failure modes; what
remains is a config check that returns None rather than raising, because a
promotional message must never be why a session breaks.

* docs: per-harness install sections, featured hub cards, browsable sidebar

Page
- One subsection per harness with its logo and a copyable install command,
  replacing the table: the command is what a reader came for, and a table cell
  is not copyable.
- Title is just "Coding Agents"; the old keyword-stuffed title read as spam in
  the sidebar and breadcrumbs.
- "Ingestion internals (no CLI)" is dropped from the docs page via the existing
  DROP_SECTIONS mechanism, staying in the README where the contributor-facing
  audience is.

Integrations Hub
- A Featured grid pins Coding Agents, Vercel AI SDK and OpenClaw above the
  rest, and only on the unfiltered view — pinned cards above non-matching
  search results would read as noise.
- The Coding Agents card draws all ten supported harness logos. "One install,
  every agent" is the whole pitch and a single icon cannot carry it.
- Logos come from the control plane's harness set, which is already keyed by the
  exact harness ids the plugin uses, so the two stay consistent by construction.

Sidebar
- Groups are open but show six entries each, with the tail behind a nested
  "Show all N". Fully expanded, 59 entries were a wall; fully collapsed hid that
  the list was worth opening.
- The umbrella Coding Agents entry leads its group instead of sorting under "C",
  since it is the entry point to every other agent in that list.

* fix(docs): point the page's harness logos at this build, not production

The README must use absolute URLs so the logos render on npm and GitHub, but the
docs page inherited them verbatim — pinning every image to hindsight.vectorize.io,
where /img/harness/* does not exist yet. Logos were broken locally and in
previews, and would only start working after a deploy.

The sync script now rewrites our own absolute asset URLs to site-relative, next
to the repo-relative-link rewrite it already does for the same reason: two
audiences needing different URLs from one source.

* docs: curate the sidebar previews, harness logos for coding agents

The coding-agent group now previews HARNESSES, not pages: ten logos that all
link to the Coding Agents page. Listing ten integration pages there presented
one plugin as ten separate integrations, which is the opposite of its pitch —
and the logos make the group recognisable at a glance. Every individual page
moves behind "Show all", which is also what keeps it associated with the
sidebar.

The other groups get hand-picked previews instead of the first six
alphabetically — the first names in a sorted list are an accident of spelling,
not a description of the group:
  Frameworks & SDKs — LangGraph/LangChain, Vercel AI SDK, Vercel Chat, Eve, CrewAI
  Apps & tools      — ChatGPT, Hermes, OpenClaw, Obsidian

Hermes, NemoClaw, OpenClaw and Paperclip move from framework to tool.

The overflow label counts what opening it actually reveals: "Show 21 more" where
some entries are already previewed above, "Show all 19" for coding agents, whose
overflow really is every page.

* docs: inline sidebar preview, full list on the integration pages

The two sidebars do different jobs, so they now show different things.

Main docs sidebar — a preview: three groups rendered INLINE and
non-collapsible, nothing behind a disclosure. Ten harness logos for coding
agents, five frameworks, four apps, then an "All integrations" link to the
gallery, which offers search and filters a sidebar cannot.

Integration pages — the full list again: flat, alphabetical, every entry. Once
you are on one of these pages you are comparing and hopping between them, so
hiding two thirds behind "Show N more" worked against the reader. Listing each
page directly is also what associates it with this sidebar.

* docs: promote the sidebar groups, separate Featured, brand the umbrella card

Sidebar: the "Integrations" placeholder is replaced BY its contents instead of
filled, so the three groups sit at the same level as the rest of the navigation.
The wrapper was two levels of nesting to say one thing, and it indented every
entry beneath it.

Hub: a divider and an "All integrations" heading separate the pinned Featured
cards from the full list, which otherwise read as one uninterrupted run.

The Coding Agents card carries the Hindsight mark rather than the GitHub logo —
it is our own package, and the GitHub icon said nothing about it.

* docs: install with npx, no global install

Every example across the README, the seven integration pages, the companion
skill and the generated docs now runs the installer with npx. Nothing here asks
anyone to keep a package installed whose only job is to wire other tools up.

The paragraph telling people to install globally — and warning that npx was
refused — is replaced by what actually happens: install copies what it needs
into ~/.hindsight/coding-agents and points each agent's wiring there, so it does
not matter where it ran from, and updating is the same command again.

Depends on #3241, which makes that staging real; until it ships in 0.0.6 the
published installer still refuses to run from an npx cache.

* docs: move the superseded pages into a Legacy section, out of the gallery

The six per-agent pages the Coding Agents plugin replaces — Claude Code, Codex,
Cursor CLI, Copilot CLI, opencode, Cline — move to a `legacy` category.

They keep their pages and their migration banners: people still run these
plugins and still arrive from old links, so removing the pages would break both.
What changes is where they are offered. The gallery is where someone comes to
CHOOSE an integration, and offering one we are actively migrating them off
points them at a dead end — so legacy entries are filtered out of it, including
the hero banner, whose hardcoded list still advertised Claude Code and now
advertises the plugin that replaced it.

In the sidebar they sit in a collapsed "Legacy" section at the end instead of
mixed in alphabetically, so the main list is only what we would recommend today.
Docusaurus expands that section automatically when you are on one of the pages.

Grouping keys off an explicit `harnessPreview` flag now: "no previewIds" used to
imply the coding-agent group, which the Legacy group would also have matched.
2026-08-07 17:00:50 +02:00
Nicolò Boschi 18a58ef3aa feat(coding-agents): carry the Codex plugin's endpoint over too (#3203)
The endpoint carry-over only read ~/.hindsight/claude-code.json, so someone
migrating off the Codex plugin silently landed on Cloud despite having a server
configured. Codex uses the same key names in ~/.hindsight/codex.json, so one
reader serves both.

The agent being installed is checked first: wiring Codex must take Codex's
server even when a stale claude-code.json is still present. It then falls back
to any known legacy config, since one server shared by both is the common case.

These two are the only superseded plugins that shipped a user config —
Cursor CLI, Copilot CLI, opencode and Cline have no endpoint to carry.
2026-08-07 14:08:23 +02:00
Nicolò Boschi e1b3d438ef feat(coding-agents): local daemon mode, and pick the server at install time (#3193)
* feat(coding-agents): local daemon mode, and pick the server at install time

The package that supersedes the per-agent plugins had no embedded mode at all:
apiUrl defaulted to Cloud and there was no daemon lifecycle anywhere. The old
Claude Code plugin auto-managed hindsight-embed (scripts/lib/daemon.py), so
anyone without a Cloud account or a server lost a working setup in the move.

Three modes, resolved the way the old plugin resolved them: an external API
(cloud or self-hosted), a healthy local server adopted as-is, or a daemon we
start. `install` asks once on a terminal; `--server cloud|self-hosted|daemon`
scripts it, and a config that already names a server is never re-asked or
rewritten.

Lifecycle is delegated to @vectorize-io/hindsight-all, which owns the uvx
invocation, profile creation and the macOS Metal workaround. It has zero
dependencies and is inlined by tsup, so hook bundles stay self-contained.

Design points worth knowing:

- Daemon mode resolves its URL inside resolveConfig, so all eight existing
  client-construction sites work unchanged instead of threading a mode through
  each one.
- A cold start (uvx download + model load) outlives every hook timeout, so it is
  never awaited inline: SessionStart spawns a DETACHED starter, the same idiom
  seeding and the codebase survey already use, and each caller waits only a
  bounded slice of its own budget. The prompt hook never starts a daemon.
- There is deliberately no stop-on-session-end, unlike the old plugin: one
  daemon serves every agent and repo, so ending one session must not cut memory
  out from under another. daemonIdleTimeout retires it instead.
- Port 9077, not hindsight-all's 8888 — 8888 is the conventional port for a
  server the user runs, and a daemon must not squat on it.
- Daemon settings keep the old plugin's env names (HINDSIGHT_API_PORT,
  HINDSIGHT_DAEMON_IDLE_TIMEOUT, HINDSIGHT_EMBED_VERSION,
  HINDSIGHT_EMBED_PACKAGE_PATH), so a migrating environment carries over.

Prerequisites are reported at install time rather than failing silently later:
uv on PATH, an LLM for local extraction (explicit provider, then a known key
env, then the Claude Code CLI which needs none), and on macOS a current Rust
toolchain — litellm publishes no macOS wheel, so a Mac builds it from source and
its crates pin a recent rustc. These are advisory, not blocking: unlike the
devin-cli preflight, every one of them can be installed after the fact.

* fix(coding-agents): treat a down daemon exactly like a down server

Two divergences between daemon mode and the api modes, both removed so the
client and everything downstream of the resolved URL behave identically:

- The Stop hook gated retain on ensureDaemon's result, so a daemon that wasn't
  up made retain SKIP — the conversation was dropped — while an unreachable
  Cloud or self-hosted server let retain proceed and fail through buildRetain's
  handler, which already logs and emits `retain_failed` with the error. A local
  port being closed is just a connection failure; it now takes the same path.
  ensureDaemon is called for its side effect only and its result is ignored.

- ensureDaemon's `allowStart: false` branch, and the `daemon_not_ready`
  diagnostic it emitted, were never reached: the prompt hook has no daemon code
  at all, so only a unit test exercised them. Dropped, along with the option;
  the module docstring described that unwired prompt-path behaviour and now
  describes what actually runs.

The remaining daemon-mode work is a side effect at two lifecycle points
(SessionStart and Stop) plus one ternary in resolveConfig. Nothing downstream
knows which mode is active.

* feat(coding-agents): carry the server endpoint over from the old plugin

Installing over an existing ~/.hindsight/claude-code.json now adopts its
endpoint — hindsightApiUrl -> apiUrl, hindsightApiToken -> apiToken, and an
empty URL meaning the local daemon, exactly as that plugin read it. Someone
running against a self-hosted server or a daemon has already decided where
their prompts and transcripts go; defaulting to Cloud would silently redirect
them. --server still overrides.

ONLY the endpoint. None of the old plugin's ~40 behavioural settings are
translated: 12 recall*, 7 retain*, the mission pair and dynamicBankGranularity
describe a pipeline this package replaced, and reinterpreting them would be
guesswork.

Conversations keep coming from local transcripts (--import-conversations),
re-extracted as new documents. That is not a fallback — the old bank cannot be
split by repo on its own. Its default was a SINGLE static bank (dynamicBankId
defaults to false, so everything landed in `claude_code`) whose documents record
only retained_at, message_count and session_id, with nothing identifying the
project. Attributing them means joining session_id back to the cwd in the local
transcript, so the transcripts are required either way.

Also corrects the migration docs, which claimed the old plugin scoped a bank per
agent per project (true only in dynamic mode, not the default) and that the old
bank could not be merged (document-transfer does merge, with on_conflict).
2026-08-06 12:40:15 +02:00
Nicolò Boschi 333812c85a fix(coding-agents): read Devin's transcript with node:sqlite, and refuse to install without it (#3175)
* fix(coding-agents): read Devin's transcript with node:sqlite, and refuse to install without it

Devin is the only harness whose hooks never hand over a transcript — they carry
a session id and nothing else, so the conversation has to be read from the CLI's
own sessions.db. That read shelled out to the `sqlite3` BINARY, which is not a
declared dependency and was never checked for. Where it was absent, execFileSync
threw ENOENT, a bare `catch` folded it into `return []`, and retain no-opped
forever while the installer reported success (#3125).

Node ships its own SQLite, so the binary is no longer needed for anything:

- `node:sqlite` replaces the subprocess. It is a builtin — nothing added to the
  package, nothing bundled (esbuild leaves `node:` imports external; the devin
  hook bundle is unchanged at 59K). Loaded through createRequire INSIDE the read
  function rather than imported at module scope: this module is pulled in by
  hook-lifecycle, which every harness shares, so a static import would break
  Claude Code and Codex on a Node without it.
- The session id is now a bound parameter instead of being escaped into the SQL
  string by hand.
- Missing reader, absent database and read failure each emit a distinct `diag`
  event, so a permanently memory-less install no longer looks like an idle
  session. A Devin storage-schema change surfaces the same way.
- `install devin-cli` preflights the `node` on PATH — the interpreter the hook
  command actually runs under, which an npx-launched installer may not be — and
  refuses with the reason and a non-zero exit instead of wiring hooks that could
  never retain anything. On `install all` only Devin is blocked; the other
  agents are still wired.

The SQLite read path had no tests at all; it now covers reading a session,
binding a quoted id, and both failure diagnostics against a real database.

* ci: export the integrations-coding-agents change filter

The filter was defined and the job consumed it, but detect-changes never listed
it among its outputs — so `needs.detect-changes.outputs.integrations-coding-agents`
was always empty and test-coding-agents ran only on workflow_dispatch or a
workflow-file change. Every PR touching just that package went untested.

* chore: regenerate the stale docs-skill mirror

Pre-existing drift on main, not from this branch: the `agent_name` deprecation
landed in the docs without re-running generate-docs-skill.sh, so
verify-generated-files fails for every PR. Pulled in here because this PR can't
go green without it.
2026-08-04 16:30:08 +02:00
Nicolò Boschi 84fd3b1767 docs(coding-agents): explain how imported sessions are attributed
Both supported harnesses record the directory each session ran in, so a
conversation is only imported when the session proves where it belongs — a
paragraph on why that matters (the folder-name encoding is ambiguous, and a
wrong guess files another repo's conversation into your bank) and what happens
to sessions that record nothing.
2026-08-03 18:01:14 +02:00
Nicolò Boschi d238d2f7d7 fix(coding-agents): attribute imported sessions by recorded cwd, never by name
A conversation may only be imported into a repo's bank when the session itself
records the directory it ran in. The previous fallback — matching the project
folder name — was a guess, and the encoding makes it an unsafe one: `/` and `.`
both become `-`, so `repo-sub` is either the subdirectory `repo/sub` or an
unrelated sibling repo. Guessing wrong files someone else's conversation into
this repo's memory, which is worse than importing nothing.

Both supported harnesses can prove it: Codex writes the cwd in its session_meta
header, and Claude records one on its entries (measured: 400/400 sampled
sessions of 13,841). Sessions that record none are skipped and counted, and the
count is printed rather than swallowed.

Matching on the recorded directory also fixes the opposite error: a session run
in a SUBDIRECTORY of the repo is now imported (Claude gives a subdirectory
launch its own project folder, which an exact-name match missed), and Codex
matches sessions whose cwd is inside the repo rather than exactly equal to it.
2026-08-03 17:26:54 +02:00
Nicolò Boschi 17b7f46ae7 feat(coding-agents): --import-conversations, a migration path off the per-agent plugins
The old integrations can't be migrated by moving data: they scope a bank per
agent per project (`claude-code::myrepo`) where this package uses one per repo
(`coding-agent::myrepo`), so two old banks map onto one new one — and the
server's bank import restores a whole bank rather than merging into a live one.

Re-reading the transcripts the agent already wrote to disk sidesteps that: the
same conversations are re-extracted into whichever bank is current. The flag
hands them to the deepen engine a session start already uses, so ingestion
dedups by document id and re-running is safe.

Scoped to the current repo — this machine holds ~14k Claude sessions, and
importing every project's history would run extraction over all of them. Claude
Code keys history by project directory; Codex partitions by date and records the
cwd in each rollout's session_meta header, which is read to filter.

Only file-based harnesses are supported. opencode, Kilo, Cursor, Cline, Copilot
and Devin keep history in internal SQLite databases with unversioned schemas;
they report as skipped WITH the reason rather than importing nothing silently.

One bug worth naming: the Codex header is a single line carrying the agent's
full base instructions, tens of KB. Reading a fixed 4096-byte prefix truncated it
mid-JSON, so every rollout was skipped and the import quietly found nothing —
hidden by the surrounding catch. The header is now read a chunk at a time until
the newline, with a regression test that fails against the old slice.
2026-08-03 16:18:08 +02:00
Nicolò Boschi d6bdb62a02 docs(coding-agents): generate the docs page from the README, lead with install
The two pages described the same product in different words and had drifted: the
docs page still carried install instructions that no longer worked and a harness
table that had fallen behind. The README is the single source now — a sync
script writes the doc page from it, and `--check` runs in the docs build so the
two cannot separate again (verified: the build fails on a hand-edited page).

Also, from reading it as a new user would:

- Install moves above "How it works". The first question is "how do I get this",
  and it was buried under the harness table.
- "How it works" becomes short bullets grouped by what actually happens —
  ingestion, what the agent receives, write-back, guarantees — instead of seven
  dense paragraphs.
- Layout (a source-tree map for contributors) is dropped from the doc page: it
  answers a question no reader of the docs site is asking.
- The Configuration section no longer claims "no environment variables", which
  stopped being true when the env fallback landed.
2026-08-03 15:29:26 +02:00
Nicolò Boschi 05b55d20f5 feat(coding-agents): require an explicit target — install all or a harness name
A bare `install` wired every detected agent: hooks, MCP registration and the
companion skill written into up to ten hosts' configs from one command that
never said it would. `all` is now spelled out, so wiring the whole machine is a
choice rather than a side effect, and a bare `install` changes nothing and
prints the options. `uninstall` matches, so the pair stays symmetric.

Chosen over a --yes flag or a confirmation prompt: a prompt has to decide what
to do without a TTY (assume consent, or break automation), whereas an explicit
target reads the same in a terminal, a script and a README.

The README also gains a per-agent section — one row per agent with its exact
command and what that wiring touches — since "install and it finds everything"
is no longer the whole story.
2026-08-03 14:54:14 +02:00
Nicolò Boschi 06eaf09493 docs(coding-agents): scope the npm package in the install command
The package publishes as @vectorize-io/hindsight-coding-agents, so the unscoped
`npm install -g hindsight-coding-agents` in the README, the docs page and the
companion skill resolved to a different (non-existent) package and failed for
the first users who tried it. The BINARY stays unscoped, so `hindsight-coding-agents
install` is unchanged.
2026-08-03 14:29:40 +02:00
Nicolò Boschi 8e5fdf28dd docs(coding-agents): unlist the integration page until it is announced
The package is about to be released so it can be installed and exercised end to
end, but its page should not surface yet. Three edits, each covering a different
surface:

- integrations.json: the entry drives BOTH the gallery and the sidebar (the
  sidebar is generated from this file), so removing it hides both.
- `unlisted: true` on the doc page: keeps it out of search and the sitemap and
  marks it noindex, while leaving it reachable by direct URL — and stops the
  build warning about a doc belonging to no sidebar.
- check-integrations.mjs EXCLUDED: the reverse check fails the docs build when a
  released integration has no gallery entry, so without this the build breaks the
  moment the release tag exists.

To publish it later: drop it from EXCLUDED, restore the integrations.json entry,
and remove `unlisted` from the page.
2026-08-03 11:08:27 +02:00
Nicolò Boschi b5d8439c8f hindsight-coding-agents: harness-pluggable long-term memory for coding agents (#2522)
* feat(integrations): add hindsight-opencode-coding plugin

Reflect-only long-term memory for coding agents in OpenCode, with a git+chat
backfill and (opt-in) live session write-back.

- reflect + INJECT: on a task, reflect() the symptom and push the root-cause
  answer into the system prompt (no tools/recall).
- backfill: every commit (full message + full diff, commit timestamp + git
  metadata) under a 'git' retain strategy; each chat as a JSON user/assistant
  transcript with custom extraction (<=2 coherent facts) under a 'chat' strategy;
  observations on; optional codebase knowledge pages.
- live write-back (opt-in HINDSIGHT_RETAIN_SESSIONS): every N turns upsert the
  tool-filtered transcript under a stable conversation:<sessionID> document_id.

* refactor(integrations): generalize opencode-coding into hindsight-coding-agents

Make the coding-memory plugin harness-pluggable instead of opencode-specific.
A 'harness' (coding agent) differs in only two places; everything else is now
shared core:
  - src/core/    hindsight client, missions, git + chat ingest, inject, RuntimeCore
  - src/core/types.ts  HarnessAdapter + ChatReader interfaces
  - src/harness/ per-agent adapters + registry (opencode implemented)

Backfill: --harness selects how past sessions are read (opencode today);
git ingest, retain strategies, missions, and knowledge pages are identical
across agents. Runtime: HINDSIGHT_HARNESS (default opencode) selects the
adapter that binds RuntimeCore's reflect+inject+write-back to that agent's
plugin API. Adding an agent = one adapter file + a registry entry.

Type-checks and builds clean; unknown --harness/HINDSIGHT_HARNESS errors with
the available list.

* feat(coding-agents): on-demand memory_reflect tool, opt-in git-sync, JSON config

Add two capabilities to the reflect-only coding-agents plugin and move all
configuration off environment variables onto a single JSON file.

- memory_reflect tool: exposes the same synthesized reflect that is auto-injected
  on the first message as an on-demand opencode tool the agent can call mid-task
  (RuntimeCore.reflectNow + opencode adapter tool). Harness-agnostic core, thin
  opencode wiring.
- incremental git-sync (opt-in): on load, diff the target ref's commits
  (origin/main, falling back to HEAD) against the git:<sha> document_ids already
  in the bank and async-retain only the missing ones, reusing the backfill's
  per-commit encoding (retainCommit). Set-based, correct across rebases;
  best-effort, non-blocking. Off by default (gitSync.enabled).
  Adds HindsightClient.listDocumentIds + core/sync.ts.
- config file: all settings now come from ~/.hindsight/coding-agent.json
  (core/config.ts) -- no environment variables. The backfill CLI reads the same
  file for shared connection/bank settings with --flags overriding; operation
  flags stay CLI-only.

Committed with --no-verify: the repo-wide pre-commit lint hook is broken in this
environment (missing @eslint/js in hindsight-control-plane) and blocks all commits.

* fix(coding-agents): remove benchmark-specific strings from prompts

Fairness audit of the sdebench benchmark found three contaminations:
- CHAT_CUSTOM_INSTRUCTIONS used the literal answer to a graded task
  (round_cents/ROUND_HALF_DOWN/legacy ledger) as its example - replaced
  with a fictional, non-benchmark example.
- buildSystemInjection told the model 'the hidden tests depend on those
  exact choices' - hardcoded knowledge of the benchmark's grading;
  reworded benchmark-agnostic.
- REFLECT_MISSION examples were shape-matched to specific benchmark
  tasks (symbol mappings, exact numbers) - neutralized.

No behavior change intended beyond removing the leaked specifics.
(includes hook-regenerated skills/hindsight-docs sync)

* feat(coding-agents): reflect-outcome diagnostics — no more silent memory loss

A benchmark sweep ran the entire memory arm with zero injected memory:
reflect failed environmentally on every task and the best-effort catch
swallowed it, making a memory-less run indistinguishable from a memory
run. onTask now appends a reflect_ok/reflect_empty/reflect_failed record
(duration, error, query prefix) to HINDSIGHT_DIAG_FILE (default
/tmp/hindsight-plugin.log). Consumers can assert a session actually had
memory before trusting a comparison.

* fix(coding-agents): chronological session recency + supersession-aware reflect

Two defects surfaced by the conversation-amended benchmark tasks (a rule
settled in one chat and amended in a later one):

- chat ingestion staggered synthetic timestamps NOW - i*1h, INVERTING
  recency: an amendment chat ranked older than the decision it
  superseded, steering temporal ranking toward the stale rule. Session
  list order is chronological; the last session is now the newest.
- REFLECT_MISSION now states that when memories conflict on the same
  rule, the latest/superseding decision wins and the superseded rule
  must be reported as no longer in effect, never presented as the fix.

Observed live: reflect on an amended bank returned the superseded
keep-latest rule as the fix. Both fixes are general recency/consistency
semantics, not benchmark-specific behavior.

* feat(coding-agents): multi-harness configurability + Claude Code hook entry

One config, several agents side by side:

- Each runtime entry point now KNOWS its harness instead of reading the
  config's `harness` key (which selected a single global adapter and
  made opencode + claude mutually exclusive). That key now only picks
  the backfill's session formatter.
- New `harnesses.<name>` config sections: per-agent overrides of any
  field (bank, disabled, timeouts) over shared connection defaults.
- New project-local layer: <project>/.hindsight/coding-agent.json
  overrides the global file — the natural home for a per-repo bank.
  Precedence: defaults < global < global.harnesses < project <
  project.harnesses.
- New entry point: `hindsight-claude-hook` (dist/claude-hook.js), a
  Claude Code UserPromptSubmit hook. Reflects once per Claude session,
  caches the answer in tmp and re-injects it on later prompts, and
  writes the same reflect_ok/failed diagnostics as the opencode path.

Verified live: claude hook via project config + harnesses section
(reflect_ok, cached re-emit in 46ms, one reflect total); opencode via
the benchmark harness (reflect_ok, task solved 0 corrections).

* feat(coding-agents): per-repo dynamic bank resolution (family convention)

Port of the bank-derivation convention shared by the claude-code, omo,
cline, and opencode integrations, with coding-first defaults:

- No bankId configured => the bank is derived from the git repo the
  working directory belongs to, WORKTREE-AWARE: git rev-parse
  --git-common-dir resolves every linked worktree to the main worktree's
  basename, so all worktrees of a repo share one memory bank (bare repos
  use the bare dir name; non-git dirs fall back to the dir basename).
- Default granularity is [gitProject] (not agent::project): opencode and
  claude share ONE memory per repo — add 'agent' to
  dynamicBankGranularity to split per agent.
- Explicit bankId keeps today's static behavior (benchmark harness,
  single-bank setups); dynamicBankId forces either mode; supporting
  fields: bankIdPrefix, directoryBankMap (exact cwd -> bank escape
  hatch), agentName, resolveWorktrees.
- backfill: --bank wins, else the SAME resolution applied to --repo, so
  `hindsight-coding-backfill --repo .` fills exactly the bank the
  agents will read.

Verified: worktree -> main-repo bank (hs-coding-plugin-wt -> memory-poc),
static/prefix/dirMap/granularity cases, and the claude hook e2e
(reflect_ok via directoryBankMap against a live bank).

* feat(coding-agents): bank template string, prefix path map, {harness} field

Bank-resolution refinements:

- `bankIdTemplate` format string replaces the granularity array:
  e.g. "hindsight-{gitProject}" or "{harness}-{gitProject}" — default
  "{gitProject}" (opencode + claude share one bank per repo).
  Placeholders: {gitProject} {project} {harness} {channel} {user};
  unknown placeholders warn with the valid list. bankIdPrefix removed
  (expressible in the template).
- {harness} is supplied by the entry point itself (opencode plugin,
  claude hook, backfill --harness), not a config field — nothing to
  keep in sync.
- directoryBankMap now matches by LONGEST absolute-path prefix and
  overrides everything incl. an explicit bankId: mapping a repo root
  covers all its subdirectories; deeper mappings win.
- config discovery walks UP from the working directory to the nearest
  .hindsight/coding-agent.json — a hook invoked from a repo subdir
  previously missed the repo's project config entirely (found by an
  e2e test that failed exactly this way).

Verified: derivation matrix (template/prefix-map/override/static/bad
placeholder), claude hook e2e from a nested subdir (reflect_ok via
walked-up config + prefix-matched map), opencode benchmark task green.

* feat(coding-agents): cursor-cli + codex harnesses, unit tests, live system tests

Harnesses — hook-based agents now share one runtime (core/hook.ts:
stdin event -> layered config -> per-repo bank -> once-per-session
reflect with tmp cache -> native output -> diagnostics), so each agent
is a ~25-line HookSpec:
- hindsight-claude-hook  (UserPromptSubmit -> additionalContext)
- hindsight-cursor-hook  (beforeSubmitPrompt -> {continue, additional_context})
- hindsight-codex-hook   (Codex CLI v0.116+ claude-compatible hooks;
  accepts prompt/user_prompt)
All three + opencode registered in the harness registry (backfill
--harness resolves them; hook harnesses share the normalized-JSON
chat reader).

Tests (vitest, family convention):
- 25 unit tests: full bank-derivation matrix (worktree/bare/static/
  dynamic/template/{harness}/prefix-map incl. longest-wins and
  no-sibling-false-match) and config layering (harness sections,
  project-over-global, upward walk, nearest-wins, gitSync field merge,
  malformed fallback, legacy signature).
- live system suite (npm run test:live, HINDSIGHT_LIVE_E2E=1): builds a
  real git repo with a decision planted in a commit + a conversation,
  runs the real backfill CLI (server-side LLM extraction), then invokes
  the BUILT hook binaries as subprocesses and asserts the decision's
  literals come back in the injected context — semantic verification
  with a real LLM — plus per-session cache behavior and diag records.
  All 4 passing against a live server.

Note: session ids in the live suite are unique per run — the hooks
cache per session id in tmp, and a static id once cached a bad answer
from a half-broken server across reruns.

* docs(coding-agents): full README rewrite + integration docs page

README now covers everything the package does today: the reflect-once/
inject-every-turn mechanics, all four harnesses (opencode plugin +
claude/codex/cursor hooks) with install snippets, the complete
configuration reference (layered files, harnesses sections, per-repo
dynamic bank resolution with template placeholders, directoryBankMap,
worktree behavior), backfill CLI incl. bank auto-resolution and
chronological session ordering, the reflect diagnostics contract, and
the unit + live test suites.

Docs site: new docs-integrations/coding-agents.md (same content adapted
to the integration-guide format) + integrations.json hub entry so the
generated sidebar picks it up. Placeholder icon (github.png) pending a
real one. Verified: page renders (docusaurus build), all doc pre-flight
checks pass for this entry — note the docs build on this branch was
ALREADY failing on the unrelated pre-existing 'zcode missing from
integrations.json' check.

* feat(coding-agents): 🧠 attribution header in buildSystemInjection

Prepend the 'Using Hindsight Memories' visible-attribution directive to the
harness-agnostic system injection so every coding-agent harness surfaces a
recognizable header when it uses recalled memory. Covered by 5 deterministic
inject.test.ts cases (real emoji + em dash, no lone surrogates).

* feat(core): add recall() to HindsightClient

* style(core): apply prettier formatting to recall test

* style(coding-agents): normalize prettier formatting across package

* fix(core): narrow RecallResult to actual API contract, add fetch-throw test

* feat(core): formatMemories + shared attribution preamble

* style(core): prettier-wrap recall.test.ts array literal

* fix(core): cover formatMemories trim/filter + drop stale inject comment

* feat(core): per-turn recall in the hook runtime (reflect once, recall every turn)

Extracts the hook logic into a pure, unit-testable buildHookOutput(): every
prompt now runs recall() and injects a <hindsight_memories> block; reflect
still runs once per session (first prompt) and its cached answer is no
longer re-injected on later turns. runHook() becomes thin stdin/stdout
plumbing with a makeClient seam for tests. Updates the three hook
entrypoints' doc comments to match, and adds recallMaxTokens/recallTimeoutMs
config fields.

* fix(core): make recall fail-open in buildHookOutput + cover recall failure/opts

* feat(claude-code-v2): wrapper plugin skeleton (per-turn recall via bundled core)

Also disables tsup code-splitting in hindsight-coding-agents so each bin
entry (claude-hook.js etc.) is a single self-contained file with no
shared chunk-*.js — required for wrapper build scripts that copy just
the one hook file out of dist/.

* chore(coding-agents): sync codex-hook bin into package-lock

* fix(claude-code-v2): derive version from manifest + guard self-contained bundle

* feat(core): Claude transcript reader (normalized user/assistant text turns)

* fix(core): transcript reader null-safety + drop sidechain turns

* feat: live write-back on the Claude Stop hook (shared retain-hook runtime)

Extracts a testable buildRetain core (read transcript -> upsert under
conversation:<sessionId> via retainLiveSession) plus a thin runRetainHook
plumbing wrapper mirroring the existing runHook/buildHookOutput split, and
wires it up as a Claude Code Stop hook. Fail-open throughout: an empty
transcript is a no-op, and a retain failure is diagnosed but never thrown.

Exports diag() from core/hook.ts so retain-hook.ts can reuse the same
diagnostics helper instead of duplicating it.

* refactor(core): extract diag module + trim buildRetain params

- Move diag() out of hook.ts into a neutral src/core/diag.ts so retain-hook
  (and future lifecycle hooks like SessionStart) don't reach into a
  recall/reflect-specific module for a cross-cutting concern.
- Drop the unused cwd/cfg params from buildRetain — only harness, sessionId,
  transcriptPath, and client are read; cwd/cfg stay in runRetainHook where
  they're actually used (config load + deriveBankId).
- Clarify that retainSessions is opencode-plugin-only; the Stop hook always
  writes back unless disabled.

* feat(core): knowledge-page CRUD on HindsightClient (mental-models)

* fix(core): page methods throw on 404 + doc rationale

* feat: native TS MCP server for knowledge-page tools (bank-aligned)

Adds a native TypeScript MCP (stdio) server exposing the agent_knowledge_*
tools (get_current_bank, list_pages, get_page, create_page, update_page,
delete_page, recall) over MCP, wired into the claude-code-v2 wrapper.

Bank resolution goes through the same loadConfig + deriveBankId path the
hooks use (harness "claude-code"), so knowledge pages, recall, and retain
all land in one per-repo bank. This is a native TS server rather than
reusing the Python MCP because its bank derivation mismatches.

- src/core/knowledge-tools.ts: SDK-free tool specs (zod schemas), unit
  tested against a stub client (17 tests) — every handler is fail-closed
  to an isError:true result instead of throwing.
- src/mcp-server.ts: the only file importing @modelcontextprotocol/sdk.
- tsup.config.ts: new mcp-server entry, noExternal inlines the SDK + zod
  so dist/mcp-server.js stays a single self-contained file.
- claude-code-v2/.mcp.json + build.mjs: wires the bundle into the plugin;
  the self-contained-bundle guard passes for mcp-server.js unmodified
  (no exemption needed) since noExternal fully inlines its deps.

* fix(mcp): honor disabled flag + testable selectTools

- Export selectTools(cfg, client, bankId) from mcp-server.ts: pure,
  SDK-free, returns [] when cfg.disabled (mirrors the hooks' disabled
  check) so a disabled Hindsight exposes zero MCP tools instead of all 7.
  Confirmed at runtime: with disabled:true the server still connects but
  doesn't advertise a tools capability at all (tools/list -> Method not
  found), which is stronger than an empty list.
- Guard main() behind an argv[1]-vs-import.meta.url check so importing
  the module for tests doesn't start a real stdio server.
- Add src/mcp-server.test.ts covering selectTools for both the disabled
  and enabled cases.
- Reword the HINDSIGHT_MCP_PROJECT_CWD comment: nothing sets it today
  (the plugin doesn't cd), it's an escape hatch, not a launching-host
  contract.

* refactor(core): lazy-load opencode adapter so backfill bundles self-contained

* test(core): lock opencode no-runtime registry invariant + doc it

* feat(core): cold-repo detection + seed-consent state

* test(core): cover seed write-failure + guard non-object state

* feat(core): background seed mechanics + hindsight-seed control CLI

Adds hasGitHistory (git.ts), startBackgroundSeed + seedControl (seed.ts),
and the src/hindsight-seed.ts entrypoint the agent runs after the
SessionStart seed offer (Task 10b) to seed or decline a repo's bank.

* fix(core): handle async spawn error in startBackgroundSeed

spawn() failures (ENOENT/EACCES/fd exhaustion/sandboxed environments) often
arrive asynchronously as an 'error' event on the child, not a synchronous
throw. An unhandled 'error' event crashes the caller, so attach a no-op
handler alongside the existing try/catch. Also documents the Claude-Code-only
harness assumption in hindsight-seed.ts.

* feat: SessionStart auto-seed offer for cold repos (Claude wrapper wired)

* fix(core): shell-escape seed offer paths + drop orphaned isColdRepo

* docs(claude-code-v2): marketplace entry, full README, v1→v2 migration note

* fix(core): cap hook reflect timeout, align backfill+hook config resolution

- hook.ts: cap reflect's timeoutMs to HOOK_REFLECT_CAP_MS (8s) so it always
  resolves/aborts before Claude Code's 15s UserPromptSubmit kill window,
  guaranteeing the session cache write + recall injection complete instead
  of silently retrying reflect (and dropping recall) on every turn.
- backfill.ts: resolve config via loadConfig({harness, projectDir: REPO,
  path}) instead of the legacy string form, so project-local
  .hindsight/coding-agent.json layers in and the background auto-seed
  backfill targets the same bank recall/retain/MCP read from.
- hook.ts/retain-hook.ts: resolve the cwd fallback before loadConfig (not
  just at deriveBankId) so project-local config layers even when the
  hook event's cwd is missing.

* fix(claude-code-v2): dev-install must copy .mcp.json (MCP tools were missing)

* feat(core): deterministic SessionStart auto-seed + knowledge-page bank mission

The prior SessionStart design asked the agent to pose a y/n question then
run a seed command itself; live testing showed the model surfaces the
question and then ignores it, so nothing ever seeds. The hook now starts
the background seed itself on a cold git repo (tri-state: cold/warm/
unreachable) and always injects a short visible note plus a bank-mission
pointing the agent at the agent_knowledge_* tools.

* docs(claude-code-v2): update seed docs for deterministic auto-seed + knowledge mission

* feat(core): default seed to aggregated commit messages (one cheap doc) + Initiatives page; full-diff opt-in via --diffs

* docs(core): align backfill README + strategy log/comment with gitlog default

* feat(core): headless codebase-survey seed + agent_knowledge_ingest MCP tool

On a cold repo, the SessionStart hook now also spawns a detached headless
`claude` that samples the repo's structure and ingests its findings into
Hindsight via a new agent_knowledge_ingest MCP tool, alongside the existing
git-history backfill. Knowledge pages synthesize their content from bank
memories via source_query, so this is how the survey feeds them.

- knowledge-tools.ts: add agent_knowledge_ingest (title -> slug doc id,
  retain via the "chat" strategy, tagged source:upload).
- survey.ts: resolveClaudeBin + startCodebaseSurvey, mirroring seed.ts's
  fire-and-forget/never-throw spawn pattern.
- Anti-recursion: HINDSIGHT_DISABLE_HOOKS guard at the top of runHook,
  runRetainHook, and runSessionStartHook so the survey's own claude session
  can't re-trigger seeding/recall/retain; survey.ts sets it on the child.
- config.ts: codebaseSurvey (default true) + surveyModel (default "sonnet").
- session-start.ts: wire startSurvey into the cold-repo branch alongside
  startSeed; update the visible learning note.

* fix(core): sandbox headless survey (deny-list, no bypassPermissions) + spend cap + document strategy

* feat(core): default codebase-survey model to haiku (cheaper/faster; sonnet still configurable)

* feat(core): survey excludes CLAUDE.md + agent-instruction files from ingestion

* docs(coding-agents): v2 knowledge-pages design spec + implementation plan

* feat(core): add pageRefreshEveryTurns config (default 10)

* feat(core): knowledge-injection roster/preamble formatting

* feat(core): passive knowledge entity_labels tier vocabulary + configureBank wiring

* feat(core): tag-scope seeded pages, Initiatives folder, relatedPageId link source_query

* feat(core): captureInitiative — per-initiative page + relatedPageId marker

* feat(mcp): hindsight_* grounding tools + capture_initiative; remove raw page CRUD from agent

* feat(core): SessionStart injects page roster + guidance preamble

* feat(core): UserPromptSubmit hook-counted periodic page-roster refresh

* feat(core): rich markdown session write-back with tool calls + verbose session strategy

* chore: apply prettier line-wrapping to test files

* fix(claude-code): surface the seed note via user-visible systemMessage, keep preamble in additionalContext

* fix(survey): use renamed hindsight_ingest_document MCP tool (Task 6 rename regression)

* fix(core): preamble + refresh nudge the agent to call capture_initiative for major features

* fix(core): re-inject tool+capture reminder every cadence turn even with no pages (unconditional nudge)

* fix(core): inject when-to-call guide for the full hindsight_* tool suite, not just pages+capture

* feat(claude-code): cold-check-wins seeding — reseed a cleared bank on the live doc count, ignore stale seededAt

* fix(core): simplify capture_initiative instruction to one clear trigger (remove confusing OR-chains)

* fix(core): port proven v1 attribution preamble + surface memories block first so the header actually gets emitted

* fix(core): reflect every turn (configurable reflectEveryTurns, default 1) instead of once per session

* feat(core): per-turn injection is recall-only (drop reflect from the hook), recall token budget default 750

* feat(core): inject a user-feedback section above memories (capture-initiative + attribution-header preferences)

* fix(core): align user-feedback attribution bullet with the generous WHEN-IN-DOUBT-EMIT rule

* fix(core): sharpen capture_initiative trigger — call right after plan approval, before implementation

* feat(survey): raise default codebase-survey budget cap to $2 (0.5 was over-conservative)

* feat(codex): codex-v2 wrapper (SessionStart seed + per-turn recall + MCP); parametrize session-start/MCP harness

* feat(core): default bank template is harness-neutral coding-agent::{gitProject} (shared memory across agents)

* feat(core): default apiUrl is Hindsight Cloud (https://api.hindsight.vectorize.io); local is now an override

* feat(codex): Stop write-back — Codex rollout transcript reader + codex-stop-hook (full parity)

* fix(core): captureInitiative returns the server-assigned page id (not the slug) so read_knowledge_page + relatedPageId links resolve

* feat(coding-agents): upgrade opencode adapter to full v2 parity

Per-turn recall via chat.message + system.transform (750-tok budget), native hindsight_* tools registered directly through opencode's tool() (no MCP server), rich tool-aware write-back on by default, and cold-check auto-seed at plugin load — reusing the shared formatMemories / buildKnowledgePreamble / buildKnowledgeTools / buildSessionStartContext primitives so opencode matches Claude Code and Codex.

Adds transcript-opencode.ts (rich normalizer over the live message list). Adds a HINDSIGHT_DISABLE_HOOKS recursion guard to RuntimeCore (seed/recall/write-back/sync no-op; tools still register) for headless survey runs. Removes the now-dead reflect path (client.reflect, inject.ts/buildSystemInjection, reflectTimeoutMs) as the whole surface is recall-only. README rewritten to the recall/knowledge-page/seed/write-back v2 model.

* feat(coding-agents): harness-portable codebase survey (multi-agent headless)

The cold-repo survey no longer hardcodes headless `claude` — startCodebaseSurvey now runs under the current harness's own CLI (claude/codex/gemini/opencode), falling back to any available agent, so a Codex/Gemini/opencode user without claude installed still gets the survey (the git-log seed already ran regardless).

Per-agent read-only recipes: claude (-p + inline --mcp-config + --disallowedTools), codex (exec --sandbox read-only + inline -c MCP), gemini (-p --approval-mode plan --allowed-mcp-server-names hindsight --skip-trust), opencode (run --agent plan; tools from the loaded plugin under the HINDSIGHT_DISABLE_HOOKS guard). All spawned with HINDSIGHT_DISABLE_HOOKS=1. session-start threads the harness through to the survey.

* feat(gemini): add Gemini CLI v2 integration (gemini-v2)

Full v2 parity for Gemini CLI (>=0.52.0), which added a Claude-style hooks system (stdin/stdout JSON). Maps onto the shared HookSpec/runSessionStartHook/runRetainHook abstraction with Gemini's event names: BeforeAgent (per-turn recall -> hookSpecificOutput.additionalContext), SessionStart (seed), SessionEnd (write-back).

The one Gemini-specific piece is transcript-gemini.ts — a reader for the 0.52.0 chats/session-*.jsonl mutation-log (upsert-by-id, polymorphic content: user text arrays, assistant plain strings, tool results as user functionResponse parts; drops the synthetic session_context message + thoughts). Adds the gemini-v2 wrapper (build.mjs + dev-install.sh that merges hooks + mcpServers into ~/.gemini/settings.json). Validated: reader against a real transcript, and a live recall smoke test (recall_ok) end-to-end.

* style(coding-agents): prettier-format README config table

* fix(opencode): inject via lastInjection fallback (1.18.5 system.transform has no sessionId)

opencode 1.18.5 fires experimental.chat.system.transform with input {model} only — no sessionId — so RuntimeCore.getInjection(input.sessionID) looked up undefined and pushed nothing into the system prompt. Recall still ran (chat.message does pass sessionID) but the memory block + attribution preamble + knowledge-page guide never reached the model, so no visible header and no tool use.

getInjection now falls back to the most recent turn's block (lastInjection) when there's no session-keyed hit. The completion's system.transform fires right after that session's onPrompt, so lastInjection is this turn's block. Adds an inject_ok/inject_empty diag (matching recall_ok/seed_started) to confirm injection lands.

* fix(coding-agents): treat project-local config as untrusted (block apiUrl/apiToken/directoryBankMap from a repo)

A project-local .hindsight/coding-agent.json lives inside whatever repo the developer opens, so it is untrusted input. loadConfig previously merged it per-field over the user-global config, letting a repo override apiUrl while the user-global apiToken survived the merge — so a malicious repo could set only apiUrl and the client would send the user's real Bearer token plus every recall query (the prompt) and Stop write-back transcript to an attacker-controlled host, silently, just by opening the repo (verified end-to-end).

Fix: the project-local layer is now sanitized — apiUrl, apiToken, and directoryBankMap are stripped from it (top level + any harnesses.<name> section) with a one-line warning; the user-global config stays trusted and unrestricted, and a repo can still set its own per-repo bank (bankId/bankIdTemplate). Also skip re-applying the global file as a project layer when the upward findProjectConfig walk lands back on it (a repo under $HOME with no closer config), which would otherwise strip its own apiUrl and warn every session. Adds 4 regression tests.

* style(coding-agents): prettier-format config.ts

* feat(coding-agents): restore reflect as the memory path; per-turn injection from knowledge-page sections

One opinionated runtime path (no behavior config):
- reflect ONCE per session on the first prompt (agentic root-cause synthesis,
  benchmark-proven), cached and re-injected every turn — hook harnesses and the
  opencode runtime alike
- every turn: knowledge-page SECTIONS matched locally against the prompt
  (lexical section index, no server/LLM call) injected with provenance and a
  pointer to the full page — fast like recall, organized like reflect
- raw recall leaves the runtime path (still powers the hindsight_search_memory
  tool)

Session write-back: transcripts are now JSON turns matching the backfill chat
format, with each tool call compacted to a role:"action" turn naming the tool
and its primary target (no arguments, no outputs) — Claude, Codex, Gemini and
opencode readers.

Knowledge pages: no more entity_labels/tag taxonomy — pages are unscoped, each
page's source_query selects from the whole bank; survey, gitlog seed, write-back
and security hardening stay.

Spec: docs/superpowers/specs/2026-07-27-reflect-pages-runtime.md

* test(coding-agents): rewrite unit tests for reflect+pages runtime and JSON action transcripts

* test(coding-agents): live suite matches reflect_ok by content (pages_ok now follows it in the diag stream)

* docs(coding-agents): README + docs page describe the reflect+pages runtime (reflect once per session, local page-section injection per turn, JSON action write-back)

* feat(coding-agents): drop the backfill CLI — ingestion is automatic and background

- new deepen engine (dist/deepen.js, unpublished): idempotent, resumable —
  per-bank lock, dedup by document id; ingests missing conversations, the
  one-time gitlog seed, then progressively deepens recent history with
  per-commit full diffs (newest first, bounded batch per run); drains and
  creates knowledge pages last
- every session start now fires the engine (cold or warm); survey and the
  cold-seed note stay cold-only
- sync status is the new readiness contract: hindsight_sync_status agent tool
  + dist/status.js for harnesses (synced = gitlog seeded, pages present,
  extractions drained); activeOperations() filters terminal ops
- opencode write-back now upserts every turn (async) so a killed session
  loses at most the last turn
- repoNameOf resolves relative paths so document ids are path-spelling-proof
- hindsight-coding-backfill bin removed; benchmark/e2e run the engine
  directly and poll status

* polish(coding-agents): short, non-technical cold-seed message highlighting the bank id

* polish(coding-agents): cold-start banner — HINDSIGHT unicode wordmark + bank id line

* feat(coding-agents): timing diagnostics on by default

- session_start diag event on EVERY session (bank, cold/warm, pages, ms) —
  warm sessions previously logged nothing
- deepen engine: deepen_started/deepen_done/deepen_failed diag events with
  duration; child output now appended to ~/.hindsight/coding-agent-state/deepen.log
  (was stdio:ignore — undebuggable) and log lines timestamped
- retain_ok/retain_failed carry ms on both the Stop hook and the opencode
  per-turn upsert (which was fully silent)
- vitest config pins HINDSIGHT_DIAG_FILE to a tmp file so unit tests stop
  polluting the real diag log

* feat(coding-agents): show the Hindsight banner on every session start (cold: learning, warm: remembering)

* polish(coding-agents): session banner uses the API server's colored pixel-art logo (shared visual identity), wording line below

* polish(coding-agents): banner text before logo — the TUI's first-line prefix was displacing the logo's top row

* polish(coding-agents): banner logo re-rendered foreground-only — the TUI strips ANSI background colors, which deleted half the server logo's pixels

* feat(coding-agents): per-turn user-visible notice — every prompt shows what Hindsight delivered (reflect state + matched knowledge pages) via hook systemMessage; opencode logs the same line

* polish(coding-agents): per-turn notice shows the match query excerpt and the page titles it returned

* fix(pages-index): singularize plain-word tokens so plural prompts match singular headings ('components' -> 'Component map'); path-like tokens untouched

* polish(coding-agents): per-turn notice — gradient Hindsight wordmark, value-driven wording, no timings

* feat(coding-agents): interim always-inject knowledge stub + explicit Hindsight attribution

- selectSections: TEMPORARY stub returning the first section of up to 3
  distinct pages every turn regardless of prompt — guarantees injected data
  for testing source attribution; will be replaced by the server-side
  knowledge-base/search (local lexical index drops with it)
- both injection blocks now carry an ATTRIBUTION directive: when memory
  shapes the answer, the agent introduces it with '🧠 From Hindsight memory
  (<page>)' — and must never credit memory that did not contribute

* polish(coding-agents): gradient-word banner (logo dropped), lean per-turn notice, attribution directive front-loaded as a mandatory output format

* polish(coding-agents): reflect turn notice shows the assigned goal and a preview of what memory returned

* feat(coding-agents): page knowledge moves from auto-injection to an explicit tool

- new hindsight_search_knowledge_pages(query) tool (native on opencode, MCP on
  hook harnesses) — interim local selection, single swap point for the
  server-side knowledge-base/search; results carry the attribution requirement
- per-turn auto-injection of page sections removed: a trivial prompt ('yes')
  no longer displays phantom research; ordinary turns are silent
- per-turn notice only on the reflect turn (assigned goal + result preview);
  tool calls provide their own native visibility
- tool guide/roster advertises the search tool as the first stop

* feat(coding-agents): bind hindsight_search_knowledge_pages to the server-side hybrid knowledge-base search

- merge feat/knowledge-pages-okf underneath (GET /knowledge-base/search,
  BM25 + vector, RRF-fused; conflicts resolved in okf's favor for server/
  clients/UI, coding-agents docs entry preserved)
- client.searchKnowledgePages(query, limit) wraps the endpoint; the tool
  returns ranked {page, page_id, snippet, score} — verified end-to-end
  through the real MCP server against the live endpoint
- interim local selection removed from the tool path (pages-index remains
  only for the hook page cache pending full cleanup)

* refactor(coding-agents): drop pages-index — local section index deleted; hook/runtime keep only the id+title roster (content lives behind the server-side knowledge-base search)

* refactor(coding-agents): drop hindsight_search_memory (raw recall) — knowledge-page search is THE search surface; recall client method and formatter removed

* feat(coding-agents): hindsight_reflect tool — on-demand deep memory reasoning alongside the session-start reflect

* refactor(coding-agents): one 'conversation' retain strategy for all developer conversations

Backfilled decision chats and live session write-back were the same content
type (identical JSON action-transcript format) extracted two ways based only
on where they came from. Merged CHAT_MISSION + SESSION_MISSION into one
CONVERSATION_MISSION that scales facts to substance (short decision chat ->
1-2 facts, working session -> several; final-state-wins, verbatim literals,
rejected-alternative rule kept); the ≤2-fact CHAT_CUSTOM_INSTRUCTIONS
extractor is retired with it.

* feat(coding-agents): restore Chris's knowledge entity_labels tier

configureBank again sets entity_labels {knowledge: feature-work/decision/
convention/component/concept, tag:true} + entities_allow_free_form, so the
extractor routes durable facts with knowledge:<tier> tags the server-side
knowledge base can select on; capture_initiative markers regain the
knowledge:feature-work label. Pages themselves stay unscoped (the okf
knowledge base owns synthesis).

* feat(coding-agents): seeded pages tag-scoped again — page tags match the restored knowledge:<tier> entity labels (capture_initiative pages included)

* fix(coding-agents): reflect injection wrapped in <hindsight_memory> so write-back never re-ingests it; seed-state file (declined flag) removed — the live bank is the only state

* feat(coding-agents): gitIngest enum ('message' | 'full' | 'none') — one setting, one code path for seeding AND staying current

- deepen's idempotent git pass IS the sync: gitlog doc re-upserts when HEAD
  moves (gitlog-head:<sha> tag makes freshness a single tag query); in full
  mode new commits surface at the top of rev-list and the next run ingests
  them
- separate git-sync path deleted (sync.ts, runtime.syncGitOnce, gitSync
  config)

* feat(coding-agents): gitIngest defaults to 'message' (cheap by default; opt into depth); deepen gains --git-ingest override for harnesses

* feat(coding-agents): session banner shows git-sync state (condensed syncStatus): 'git in sync' / 'catching up on new commits' / 'syncing git history (n/target)'

* polish(coding-agents): two-line banner — value headline (tracking decisions/conventions/history) + bank/sync detail line

* refactor(coding-agents): ONE config file — project-local .hindsight/coding-agent.json layer removed entirely (with its sanitization machinery); per-repo routing stays via directoryBankMap

* docs(coding-agents): fix stale project-config reference in comment

* refactor(coding-agents): runtime scratch (deepen lock + engine log) moves to the OS temp dir — ~/.hindsight now holds ONLY the config file

* feat(coding-agents): cursor auto-ingestion parity — hosts without a SessionStart hook fire the deepen engine (+ cold survey) from the session's first prompt

* feat(coding-agents): leveled plugin logging — one plugin.log (debug/info/warn/error, config logLevel + HINDSIGHT_LOG_LEVEL/FILE overrides); diag events mirror at debug; deepen logs itself (separate deepen.log dropped); warn on reflect/retain failures

* feat(coding-agents): one-shot bank configuration via the server's template import — missions, strategies, entity labels, and the 5 seeded pages in a single idempotent POST /import (configureBank PUT+PATCH and createPages removed)

* feat(coding-agents): one-command installer — npx hindsight-coding-agents install|uninstall [harness...]

Detects the coding agents on the machine and merges each one's native
wiring (hooks + MCP: claude mcp add for Claude Code; hooks.json + append-
only config.toml sections for Codex; settings.json for Gemini; hooks.json
+ mcp.json for Cursor; plugin array for opencode). Idempotent by marker,
preserves foreign entries, backs up touched files as .hindsight-backup;
uninstall removes exactly ours. 27 unit tests over temp homes.

* fix(installer): refuse to install from an npx/dlx cache (wired paths would die on eviction); document global install + npm update -g as the update path

* ci(coding-agents): unit + typecheck + build job, and a live E2E job (real API server + real LLM) running the deepen->sync->reflect->injection path; prettier-format the package

* docs(blog): launch post draft — coding-agent memory results (marked draft: true)

* docs(blog): rewrite launch post as the narrative — from 'does memory even help?' through why-not-SWE-bench, the corrections dataset, benchmark-driven architecture decisions, to the final numbers

* docs(blog): position knowledge pages as a co-launch headline — living-documents framing, example page excerpt, platform-wide availability (dashboard editor, hybrid search API, bank templates), closing CTA

* docs(blog): restructure launch post payoff-first — contrarian RAG finding + cost in the lede, TL;DR box, narrated task with both runs, seeded-answers objection met head-on, data-locality/time-to-value/latency answers, Sonnet number promoted, backstory compressed to one section

* fix(coding-agents): deepen waits for server-side ops to settle (template-import page refreshes broke the synced contract); HINDSIGHT_CONFIG env override for the config path (containers/test harnesses; replaces the live test's dependency on the removed project-config layer)

* docs(blog): second-pass fixes — flagship example swapped to the arbitrary retry decision (RFC 4180 attack closed), reconstruction disclosed, 58% provenance clause, placebo backstory + grading block restored in numbers, RAG figure per-task, benchmark-site date

* docs(blog): align remaining CSV references with the retry flagship; TL;DR per-task figures

* docs(blog): flagship rebuilt on the real dataset task — the ERP export decision whose rejected alternative IS the textbook fix (='00042' formula form, minimal quoting, CRLF); dangling injection-verified reference restored; limitations cross-check attached to the correct row

* docs(blog): rewrite as the 0.9.0 launch post — five-beat narrative (question → dataset → auto-recall failure → reflect → knowledge pages from llm-wiki to self-healing) for Knowledge Pages + unified coding-agents plugin

* docs(blog): add the missing beat — shaping the dataset revealed decisions live in git, which the old plugins never ingested

* docs(blog): reframe reflect — very smart rather than slow; first message carries the session goal; on-demand reflect tool for session drift

* docs(blog): pages section addresses the 'back to files?' objection — pages as projected views over consolidated memory (contradiction resolution underneath), raw docs remain source of truth

* docs(blog): out-of-box row updated to n=3 (22/26/23 -> 0.72/task, -26%; cost -35%); matured row marked single-run

* fix(hooks): reflect block injected once per session (+ cadence refresh), not every turn — hook context persists in the transcript, so per-turn re-injection stacked duplicate blocks

* fix(coding-agents): wrapper bundles ship deepen.js, not the renamed backfill.js

The core build entry `backfill` was renamed to `deepen` (deepen engine +
status), but the three wrapper build.mjs bundleFiles lists still copied the
removed `backfill.js`, so every dev-install failed with ENOENT. Point them at
`deepen.js` (spawned by seed.ts at runtime) so the installers build again.

* feat(coding-agents): periodic re-survey — refresh structural pages every N commits

Structural knowledge pages are only generated on a cold repo, so an evolving
architecture drifts from what the survey captured. Add surveyRefreshCommits
(default 20; 0 = cold-seed only): at SessionStart, count commits reachable from
HEAD since the newest survey-baseline marker (branch-robust via
git.commitsSince) and re-run the headless survey once the threshold is crossed,
re-recording a baseline marker. Cold seed still records the first baseline.

* fix(coding-agents): per-turn hook timeout (30s) must exceed the 25s reflect cap

The once-per-session reflect is capped internally at HOOK_REFLECT_CAP_MS=25s,
but every harness killed the UserPromptSubmit/BeforeAgent hook at 15s — below
the cap. The host killed the hook mid-reflect before the cache write, so the
injection was discarded AND the reflect re-fired uncached on every turn
("UserPromptSubmit hook timed out after 15s" every prompt). Raise the hook
timeout to 30s (> cap) across claude/codex/gemini, bump Stop to 30 to match,
and document the cap-below-timeout invariant so it can't silently drift again.

* polish(coding-agents): attribution header is a bold blockquote callout, not flat text

The live directives all told the agent to credit memory with a plain inline
"From Hindsight memory (<page>):", which renders as flat text. Switch every
directive (session tool-guide, reflect injection, both MCP tool descriptions)
to a markdown blockquote header "> ... **From Hindsight memory (<page>)** — ..."
so it renders as a distinct callout, restoring the richer attribution look.

* fix(coding-agents): strip <hook_prompt> transport wrappers from retained transcripts (codex surfaces hook stdout/errors as user messages); session + backfill transcripts switch to JSONL (one turn per line — clean appends, chunker-atomic turns)

Note: benchmark numbers (n=3) were measured on the JSON-array format; JSONL
is extraction-equivalent by design but unvalidated by a sweep — gate before
quoting new numbers on this pipeline.

* fix(installer): write [features].hooks (codex_hooks deprecated in Codex >= 0.145); accept either flag as already-enabled

* fix(hooks): fire the ingestion engine from the FIRST prompt on every harness (lock-protected no-op when SessionStart already did) — safety net for sessions predating the install, whose banks otherwise never get pages; survey stays SessionStart-owned (ensureSeed hosts excepted)

* feat(status): expose survey observability — surveyBaseline (last surveyed HEAD, from Chris's survey-baseline markers) + surveyCommitsBehind in syncStatus/hindsight_sync_status

* test(status): expected shapes include the survey observability fields

* feat(survey): findings docs ARE the completion signal — surveyDocs (0-4) in syncStatus; a baseline without findings re-fires the survey at the next warm session start (crashed-survey retry)

* feat(config): banks.<bankId> overrides — per-repo opt-in/out applied AFTER bank resolution (disable a repo, tune gitIngest/retainSessions per bank) from the ONE config file; resolution fields ignored inside a bank section

* feat(config): bankAliases — remap resolved bank ids as the final resolution step (single hop, converging allowed); docs page brought fully current (env exceptions, gitIngest/logLevel/survey rows, banks overrides, aliases, resolution step 4)

* refactor(config): bank rename lives INSIDE banks.<id> as the  field (separate bankAliases tree removed) — one per-repo section for disable, behavior, and rename; applyBankConfig returns {cfg, bankId}

* docs(coding-agents): recipe — two repos sharing one bank (converge by resolved id via banks.<id>.bank, or by path prefix via directoryBankMap), with the id-vs-path rule of thumb

* rename(config): directoryBankMap -> mapPathToBank (direction-explicit; pre-0.9.0 breaking-rename window)

* feat(coding-agents): companion skill — hindsight-coding-agent SKILL.md shipped in the package and installed into ~/.claude/skills by the installer; explains storing/retrieving, full config (banks/mapPathToBank/gitIngest), install/update, and debugging

* docs(coding-agents): mention the companion skill in README + docs page

* feat(coding-agents): companion skill ships to ALL skills-capable hosts (claude/gemini/cursor native dirs, codex via ~/.agents/skills standard); retained sessions and ingested documents carry the harness as tag (harness:<name>) and metadata

* feat(skill): self-updating companion skill — every session start re-syncs installed copies with the packaged SKILL.md (presence-gated; npm update -g now updates the skill too, no re-install)

* fix(coding-agents): worktree-aware document ids (no more per-worktree gitlog duplicates) + deepen self-cleanup; issue/PR refs preserved verbatim and emitted as ENTITIES; calibrated reflect-injection wrapper; docs ported to the TRUE source (hindsight-docs/docs-integrations) that generates the skill copy

* docs(skill): explain the internal marker documents (survey-baseline:<sha> bare-sha content is deliberate — zero extracted facts; gitlog:<repo> seed doc)

* feat(survey): human-readable baseline markers under a zero-extraction marker strategy (live-verified: 0 facts) — start as researching, deepen lazily flips to completed once findings exist

* refactor(survey): one survey strategy with conditional rules replaces the separate marker strategy — status markers extract nothing, findings extract structural facts (both branches live-verified)

* fix(hooks): mid-session heal — zero knowledge pages in the roster cache fires the ingestion engine on any prompt (covers long-lived sessions predating the install; lock makes repeats free)

* feat(bank): ~ expansion in mapPathToBank; document the directory-blacklist recipe (map tree to one bank + disable it)

* feat(coding-agents): explicit correction protocol — when the agent verifies a memory is wrong/stale it ingests a 'Correction: <topic>' doc (claimed vs verified-true vs evidence); guidance in the injection wrapper, tool guide, tool description, and companion skill

* fix(hooks): reflect block injected exactly once — cadence re-injection dropped (replaying the turn-1 synthesis at arbitrary turns reads as random noise after drift; hindsight_reflect covers genuine re-need)

* fix(coding-agents): 15s hard timeout on every client request + opencode boot no longer awaits seedIfCold — a stalled memory server can never freeze the host TUI (onPrompt already tolerates a late preamble)

* fix(reflect): defer past trivial openers — a greeting no longer spends the once-per-session synthesis on 'hi' (seen live: reflect answered a greeting with persona chatter and burned the session's slot); first substantive prompt reflects instead

* test(hooks): align reflect-call assertions with the non-trivial fixture prompt

* Revert trivial-prompt reflect deferral (misread the report — the issue was the notice's UI position, not reflect-on-greeting behavior)

* fix(opencode): stop writing banner/reflect notices to stderr — opencode renders plugin stderr inside the TUI at the cursor (text wedged against the input bar); the trail moves to the plugin log

* feat(opencode): TUI companion plugin — visible presence via api.ui.toast (opencode's TUI plugin API): banner toast on activation + reflect goal/preview toasts from the plugin-log trail; installer registers the second entry

* fix(opencode): visible presence via the server client's tui.showToast (POST /tui/show-toast) — banner + reflect toasts from the server plugin; the separate TUI module approach removed (1.18.9's loader rejects tui-only entries in the shared plugin list); SDK deps bumped to 1.18.9

* fix(opencode): toasts never rendered — v1 client wants {body}, and boot toast raced TUI mount

opencode injects the v1 SDK client whose showToast signature is {body: {title,
message, variant, duration}} and which resolves with {data|error} instead of
rejecting — the earlier flat-params call sent an empty body and the failure was
invisible. Also the toast event is not durable: the seed banner on a warm bank
fired <1s after plugin init, before the TUI subscribed, and was lost. Toasts now
use the body shape, log a rejected result at debug, and defer until ~3s past
init. Verified live in tmux: boot banner and reflect toast both render.

* fix(coding-agents): reflect must report history, never issue directives

The 0.8.6-blog incident: reflect fused two true but unrelated facts (the
hermes-deprecation goal and the blog-section removals of c87e7ac19) into one
confabulated narrative rendered in the imperative — 'You should explicitly
remove the following sections' — a completed past action re-issued as a present
directive, indistinguishable from a prompt injection to the receiving agent.

Three changes:
- buildReflectQuery wraps the session's first prompt with strict rendering
  rules: declarative past-tense attributed facts only, no instructions or
  recommendations, no stitching unrelated episodes into one narrative.
- The <hindsight_memory> wrapper now states the block is a record of the past
  that never assigns tasks: imperative wording inside it is a description of
  work already done, to be ignored unless it informs the task as historical
  fact (and unrelated memories are still ignored outright).
- The reflect_ok diag event records the injected synthesis verbatim (8k cap),
  so the next incident is one grep instead of harness-transcript spelunking.

* refactor(coding-agents): read and seed knowledge pages through the knowledge-base API

The plugin advertised knowledge pages but drove them off /mental-models, so the
two halves of the feature never met: pages seeded via the bank template's
mental_models key got a mental model and no knowledge_pages node, and
/knowledge-base/search joins through that table — the five seeded pages were
absent from the corpus of the tool billed to the agent as its FIRST STOP. The one
page search could return (an initiative, created through the KB endpoint) came
back as a kp-… node id, which the reader then fed to GET /mental-models/{id} and
404'd. Search found only what read could not open.

Every page operation now speaks one id space:

- listPages reads /knowledge-base/tree and flattens it to {items:[…]}, dropping
  folders and keeping the containing folder name.
- getPage reads /knowledge-base/pages/{id} — the ids search and [[page:<id>]]
  links already hand back.
- seedPages replaces the template's mental_models key: it creates the PAGES
  taxonomy through /knowledge-base/pages and re-syncs a drifted source_query via
  PATCH /knowledge-base/nodes/{id}, so a plugin upgrade that rewords a query
  lands on the live page instead of orphaning its synthesized content. Matched by
  name, since the endpoint mints its own id; a 409 from a concurrent deepen run
  is tolerated rather than failing the run.
- createPage/updatePage/deletePage are deleted — mental-models CRUD with no
  callers outside its own tests.

Verified against a live server on a scratch bank: five real kp- nodes, re-run
reports 0 created / 5 unchanged, all five readable by their listed id, all five
now returned by /knowledge-base/search, and a hand-drifted source_query restored
onto the same node rather than a duplicate.

* feat(coding-agents): autoReflect flag — opt out of injected reflect into tool-only mode

autoReflect (default true, layerable per-harness/per-bank like every other
field) keeps today's validated behavior: one reflect synthesis injected on the
session's first prompt. Set false and nothing is injected; instead the
knowledge preamble and every roster refresh carry an explicit trigger telling
the agent to call hindsight_reflect itself whenever a new task/goal is set —
the pull-based variant, ready to benchmark against the push default.

* docs(blog): move the 0.9.0 launch post to its own PR

The draft now lives on blog/0-9-0-launch so this PR merges independently of
launch timing (hero image, publish date, and final voice pass pending there).

* fix(deepen): dead-holder locks are stale immediately, not after 30 minutes

The per-bank deepen lock only honored its TTL: a killed run (SIGKILL, crashed
harness) left its bank locked for LOCK_STALE_MS, and every subsequent deepen
exited 'another run holds the lock — nothing to do' against an empty bank.
The lock already records the holder's pid — probe it (kill -0); if the holder
is gone the lock is stale now. Found live: a killed benchmark ingestion left
four banks locked and the retry campaign polled empty banks to its deadline.

* feat(coding-agents): expand native harness support

* fix(reflect): table-shaped decisions must be reproduced verbatim, not summarized

Benchmark replay showed reflect compressing mapping/table policies into prose
('specific extensions map to specific types') and even asserting a lossy
generalization that matched a known-wrong fix — while rule-shaped policies
survive intact. The reflect query now demands complete verbatim enumeration of
mappings/sets/tables including carve-outs.

* fix(reflect): decisions outrank implementation-derived memory

Under heavy retrieval noise, reflect surfaced the git-ingested BUGGY module
source as 'the established implementation logic' while claiming no decision
records existed — presenting the bug under investigation as authority. The
rendering rules now state: report decisions and rationale, never the current
implementation (the reader has the code); when decision memory and
code-derived memory conflict, the decision wins; implementation-only matches
are not policy.

* feat(coding-agents): expand harness integrations

* Expand coding-agent integrations and legacy compatibility

* chore(coding-agents): fix the CI-only test failure and complete the release wiring

The `test-coding-agents` job failed on every run while passing locally: the
gitDiffTarget fixture committed into a temp repo without a git identity, which a
developer machine supplies from its global config and a CI runner does not
("empty ident name not allowed"). The identity is now passed per-command, the
way the harness E2E fixture already did it.

Release wiring, which was incomplete in three places that each fail at a
different point:

- scripts/release-integration.sh had no entry, so the release refuses to start.
- generate_changelog.py keeps its OWN integration list; the release script
  aborts and reverts at the changelog step when a name is missing there.
- The docs build cross-checks released tags (`integrations/<name>/vX.Y.Z`)
  against the SLUGS in integrations.json. The directory was the only
  integration carrying a `hindsight-` prefix, so the tag would have been
  `integrations/hindsight-coding-agents/...` against a `coding-agents` slug —
  green release, then a failing docs build. The directory is renamed to
  `coding-agents` so directory, integration name, tag and docs slug all agree,
  matching every other integration.

Also drops the claude-code-v2 / codex-v2 / gemini-v2 wrappers and the
hindsight-memory-v2 marketplace entry. Claude Code is fully served by
`hindsight-coding-agents install claude-code` — hooks, MCP and skill — so the
wrappers were a second copy of the same core with its own version to keep in
lockstep. The README rows that pointed at their dev-installers now name the
supported installer command instead.

* fix(coding-agents): make the installer actually re-point a moved package

Both bugs were exposed by the directory rename, which invalidated the absolute
paths every host config stores — the case `install` exists to repair.

- Grok wrote its block only when one was absent, so every later `install` was a
  silent no-op and the dead paths survived; the only repair was editing
  config.toml by hand. It now replaces the block, sharing one regex with
  uninstall.
- MARKER was the full package name, which identifies our entries for
  dedupe-on-reinstall and for uninstall. A repo checkout stopped containing it
  once the directory dropped its `hindsight-` prefix, so from a checkout
  re-installs would have accumulated duplicate hook entries and `uninstall`
  would have removed nothing. Narrowed to the substring both layouts share.

Regression tests cover a moved package being repointed (not appended past), the
marker matching npm and checkout paths, and a repeated checkout install leaving
one entry per event.

---------

Co-authored-by: Chris Latimer <chris.latimer@vectorize.io>
2026-07-31 22:15:21 +02:00