dsh-reprise
An AutoResearch DSH plugin for reproducing papers in generative recommendation, enabling controllable experiment iteration and reproduction of training code.
- Stars
- 1
- Language
- JavaScript
- Created
- Oct 23, 2026
- Updated
- Oct 26, 2026
Introduction
description: "A model-structure exploration loop as a DeepSeek Harness bundle: deterministic phases, a frozen baseline, a spend ledger, and a stop decision that only reads recorded facts." kind: "package-bundle"
English | 简体中文
dsh-reprise
An exploration loop whose conclusions you can check.
A DeepSeek Harness bundle: reproduce a published number, then iterate on the reproduction under control — deterministic phases, a baseline the run measures itself and then freezes, a spend ledger that reserves before it spends, and a stop decision computed from recorded facts rather than remembered intent.
Summary
dsh-reprise reproduces a published experiment and then iterates on that reproduction under control:
deterministic phases, a baseline the run measures with the same executor path a candidate takes and
then freezes, a budget that reserves before it spends, and a stop decision computed from recorded facts
instead of remembered intent.
A candidate is a structural hypothesis plus the batch of code changes it needs to land — not a hyperparameter value. The loop is ordinary code; the model is consulted in exactly five places, and what it says enters the fact plane only as an artifact a validator has accepted.
Mounting the bundle adds three surfaces and nothing else: a tick that advances the phase machine in a
workspace nobody has open, a reprise_status tool that reads the same projection from inside a
conversation, and a Reprise panel in the sidebar (backed by the read-only GET routes below). It does
not touch your repository, does not open a port, and does not call a model unless you name one in
model.command.
Do not reach for it if what you want is prompt search, a hyperparameter sweep, or a dashboard you can drive: v1 has no write endpoint at all, the browser half is read-only by construction, and the loop optimises one declared metric per round rather than a population of prompts.
Table of Contents
- When to use it
- Install and mount
- Configure it
- What it adds
- The five-minute path
- Checks and local development
- Design
- Source map
- Further Exploration
- Model Experience
- Known Limitations and Deferred Work
Use this package
When to use it
You have a metric that can be measured twice, a baseline you are willing to freeze, and a belief that the structure of a model — not its learning rate — is what is wrong with it. You want the answer to "why did this candidate stop?" to come from a file rather than from whoever was watching the terminal.
dsh-reprise fits when the run must survive being unattended: a crash mid-phase is a detectable lag
in the fact plane, a paused budget is a recorded decision, and every number on the page is derivable
from facts alone (dump --rebuild proves it). It is a poor fit when the metric is a human judgement,
when the baseline cannot be made reproducible, or when you need several concurrent runs against one
workspace — one workspace belongs to one run.
Install and mount
Install the release tarball. It carries its sibling packages inside itself — vendored under
vendor/packages/, with every @dsh-reprise/* specifier rewritten to a relative path — so an install
needs no checkout, no workspace to resolve and no build step:
dsh plugin --profile web add ./dsh-reprise-<version>.tgz # the asset on the newest release
node scripts/build-release.mjs writes that file into dist/ from a checkout, and installing it by
path works the same way. A git install is not the shortcut it looks like:
github:sunstrikes/dsh-reprise#path:/packages/core fails three ways at once — the packages depend on
each other through workspace:*, which only the monorepo resolves; pnpm refuses any git dependency
below the root (ERR_PNPM_EXOTIC_SUBDEP); and packages/ui imports packages/core by relative path,
which no package manager's layout preserves. The tarball is the artifact a stranger can install.
To develop against it, clone and link — the profile links that directory instead of copying it, so edits are live and the sibling packages are the ones beside it:
git clone https://github.com/sunstrikes/dsh-reprise.git
cd dsh-reprise
node scripts/link-workspace.mjs # Node 22.5+; makes packages/* importable, idempotent
Use the host's own tooling rather than hand-editing the profile — install_bundle performs the
dependency install, the link and the bundle selection in one step, with target set to the absolute
path of <clone>/packages/core:
plugin_manager { action: "install_bundle", target: "/absolute/path/to/dsh-reprise/packages/core" }
Then supply your own configuration. The shipped patch has no config block at all: a run cannot
start without a workspace and a domain pack, and every path a real config needs belongs to the
machine that owns the run. Add an entry that targets reprise-core by id and restate the whole config — a matching override
replaces config wholesale rather than deep-merging it. Put it in your profile's
cordis.patch.yml (or $DSH_HOME/cordis.patch.yml, which outranks the profile layer);
packages/core/cordis.example.yml is the filled-in version of the
shape below, including the two path rules that cost a debugging round each.
- id: reprise-core
name: dsh-reprise
config:
workspace: /absolute/path/to/my-run # required; created facts live here
domain: /absolute/path/to/my-domain-pack # required; the metric contract, priors, surface, prompts, executor
interval_ms: 5000
candidate:
kind: command
command: ['node', '/absolute/path/to/apply-change.mjs', '{candidate}']
runner: { repo: model-repo, dir: worktrees }
model: { command: ['node', '/absolute/path/to/my-model-port.mjs'] }
rounds: { metric: { name: auc, direction: higher }, max_rounds: 3 }
metrics: { primary: auc, objectives: { auc: up } }
budget: { candidate_gpu_hours: 0.5, gpu_hours_limit: 0 }
An operator who starts one run per task should not have to edit that patch at all. Both of these work
without it, and they compose — the precedence is inline config → config_file →
environment:
- One file per run. Put
config_file: /path/to/run.jsonin the patch'sconfig(or pointDSH_REPRISE_CONFIGat it, for a launcher that writes no YAML). The file is read at mount, once: it is the base configuration and any inlineconfigkey wins over it — a shallow merge, key by key.workspacewritten inside that file resolves against the file's own directory, so a file can sit next to the run it describes; every other path keeps the host-cwd rule below. A missing, unreadable or non-JSON file is a named refusal on the boot log (path and parse error), never a silent fallback to defaults. - One environment variable.
DSH_REPRISE_WORKSPACEnames the workspace when the configuration does not; a relative value resolves against the host cwd, an absolute one is used as is. An inlineworkspacestill outranks it.
Either way, the boot log's run: line answers what is this host running? in one line: the resolved
workspace (and where it came from), the repository candidates branch from, where their worktrees are
created, whether they are kept, and which file the configuration was read from.
Restart the host (bundle lists are read at startup; with HMR a patch edit alone applies live). You are
mounted when the log carries the boot half's applied line, then the tick half's tick-applied, the
sidebar shows Reprise, and reprise_status is available to the model.
Three absences degrade instead of failing, by design: no timer service means the ticking half is
deferred, not failed (cordis reports the two identically, which is why the package ships two entry
points); no connection service means no dashboard; a workspace that cannot be opened or a config that
cannot work is one log line and a host that keeps booting. None of these three appear as an error.
A refused run configuration is not one of those absences, and it no longer takes the page with it.
The dashboard mounts on the workspace the configuration still names and says why the loop is silent:
the reason rides the overview's notes, which the strip on both surfaces paints in every state, the
empty page included. So the page shows the facts already recorded and never pretends the loop is
running.
A configuration that cannot name a workspace at all is the one case that still leaves nothing to mount.
The @dsh-reprise/memory bundle is optional and separate (row reprise-memory); it adds the injection
packet as a dsh-mnemon Source. Nothing in packages/core imports it, and it has no dependencies —
so that one installs straight from the repository subdirectory, no tarball involved:
dsh plugin --profile web add github:sunstrikes/dsh-reprise#path:/packages/memory.
Configure it
Everything domain-specific is data, and it lives in a domain pack — a directory whose
domain.json names its metric contract, its priors, its adjustable-surface whitelist, its five
prompts and its executor entry (examples/feed-structure is a complete
one). The kernel does not know a metric name, a model repository or a prompt; it reads them from the
pack, and a pack missing a part is refused by name at boot instead of running half-configured.
| Field | Default | Meaning |
|---|---|---|
workspace | required, or DSH_REPRISE_WORKSPACE | directory the run owns; the fact plane is workspace.db inside it. Relative resolves against the host cwd; an absolute path is used as is |
config_file | — (or DSH_REPRISE_CONFIG) | one JSON run configuration file, read once at mount: the base configuration, with every inline key winning over it (a shallow, key-by-key merge). workspace inside the file resolves against the file's directory. A missing/malformed file is a named boot refusal |
domain | required | directory of the domain pack; no pack, no metric contract, no prompts |
interval_ms | 5000 | tick cadence |
executor.kind | the pack's | local (a subprocess) or ray (Job Submission REST) |
executor.command | the pack's, required for local | argv of one job, resolved inside the candidate worktree |
executor.base_url | required for ray | Ray dashboard URL |
executor.root_dir / metrics_required | 'jobs' / true | where job dirs live; a job without metrics is a failure |
candidate.kind | 'command' | command, claude or dsh — the LAND implementer |
candidate.command | required for command | argv; {candidate} and {worktree} are substituted |
runner.repo / runner.dir | required with candidate | repo inside the workspace; where worktrees are created (<workspace>/worktrees/<candidateId>) |
runner.keep | true | retention. With true nothing in the kernel removes a candidate worktree; false removes nothing by itself either — it only allows the explicit worktrees prune |
runner.base_ref | git default | the commit candidates branch from, until the run rolls its code base (see rounds.anchor) |
model.kind | 'command' | how a judgement reaches a model: dsh (dsh --profile headless --json -), claude (claude -p --output-format stream-json --verbose), or command for a wrapper of your own |
model.command | required for kind: command | the wrapper's argv: one JSON question on stdin, one JSON answer (a JSON object) on stdout |
model.timeout_ms | 600000 | how long one judgement may take before it is killed — a phase never waits for a model without a deadline |
rounds.metric.name / .direction | derived from the metric contract | the one metric the round gate optimises; restating it is how a run points at a different objective, and a restatement that disagrees with metrics is warned about |
rounds.metric.contract_ref | the contract's name | pointer to the frozen metric contract |
rounds.max_rounds / .candidates_per_round | 3 / 2 | round workflow shape |
rounds.anchor | 'automatic' | who may roll the code base forward: automatic applies the double-domination rule between rounds (a candidate must dominate both the current anchor and the frozen baseline); human files a proposal under anchor-pending/ and waits for dsh-reprise intents anchor --candidate-id … --commit …. Neither mode moves the differential denominator |
rounds.retry.base_ms / .max_ms / .max_attempts | 60000 / 3600000 / 3 | backoff for a refused artifact, then a human |
stop.max_rounds | 3 | 0 disables the rule |
stop.max_no_improvement_rounds / .consecutive_empty_rounds | 0 (off) | opt-in stop rules |
stop.max_wall_hours / .max_windows | 0 (off) | workspace-level ceilings: the run ends when it has been open this long, or has measured this many windows — a conclusion on _workspace, unlike a candidate rejection |
stop.regression_windows / .regression_floor | 0 (off) / 0 | kill a candidate worse than the baseline in each of the trailing N windows; the floor may raise a tolerance, never lower one |
budget.gpu_hours_limit | 0 (unlimited) | hard cap; an uncapped remote executor is warned about, every read |
budget.soft_ratio / .candidate_gpu_hours | 0.8 / 0.5 | stop dispatching at this fraction; what a dispatch reserves |
budget.max_runtime_hours / .judge_tokens | 0 (unlimited) / false | wall-clock gate; tokens are recorded, never judged |
metrics | the pack's whole contract | one account of "better" — primary, objectives, guardrail, minimum_effective_delta, window_mode — resolved by packages/core/lib/metric-contract.js for the ranking, the stop rules and the gate; a partial or self-contradictory restatement is refused at boot |
poll.base_ms / .max_ms / .grace_attempts | 1000 / 60000 / 2 | job poll backoff |
gate.max_attempts / .base_ms / .max_ms | 3 / 1000 / 60000 | how many times gate two may run before a candidate stops, and the backoff between attempts; 1 restores a single shot |
limits.max_wall_ms / .max_idle_ms | 3600000 / 1800000 | per-job wall-clock and silence deadlines |
One of model.kind and model.command must be there when rounds is set; the five call sites are the
only places a model is consulted, and they all go through this one adapter —
packages/exec/lib/model.js. It sends one structured question
({protocol, call_site, subject, prompt, facts, answer_schema, question_id}), accepts one JSON object
back (a bare object, a stream's final message, or a fenced block inside prose), and reports one
failure class — model_unavailable — for all four ways of getting no answer: a missing binary, a
non-zero exit, a timeout, an unreadable reply. A judgement never becomes "the review said reject" and
a phase never sits waiting: the outage is recorded once in the ops log
(store.ops() → model_unavailable, with the cause beside it) and the phase is retried on a backoff
(1 min doubling to 1 h).
Every call is also filed where a reader can see the originals — including LAND's briefing of the
implementer, which is model_call/land/<candidate>.json: <workspace>/artifacts/model_call/<call site>/<subject>[/<ref>].json, holding prompt, facts, answer_schema, answer (the parsed
object), response (the words the model actually returned), and status / error_class / cause /
message when no answer arrived. The record has no timestamp and no counter — its identity is the
question's own question_id — so asking the same question twice rewrites the same bytes instead of
appending a second history. That is the record the audit panel (ticket 29) reads.
Validation is packages/core/lib/config.js and it runs once at boot. The package exports no Config
object, so the host holds no schema for the row: a bad config is a log line, never a boot failure. Two
refusals are worth knowing before you hit them — rounds without model.command, and candidate
without runner — because the alternative is a run that stalls at the first round instead of at boot.
metrics, stop, budget, executor and runner are copied into the workspace at INIT and read
from there afterwards, so editing the patch mid-run changes nothing the dashboard reports (and dump
can still say where the worktrees went, from the workspace directory alone).
Candidate worktrees are kept, on purpose. A landed candidate's tree lives at
<workspace>/<runner.dir>/<candidateId> — the diff it was judged on is the files in that directory —
so no phase, recovery path or cleanup routine removes one. runner.keep (default true) is the
contract rather than the accident; dump --candidate <id> prints the path and whether it is still on
disk, worktrees list is the inventory for the whole run, and worktrees prune --candidate <id> is
the only thing that may remove a tree — it refuses a candidate that has not finished, an unknown id,
and every candidate of a run that declared keep: true.
What it adds
| Surface | Identifier | What it does |
|---|---|---|
| Model tool | reprise_status | the run's progress as JSON, read-only, from the same projection the page draws |
| Sidebar panel | sidebar.panellist id reprise, order 25, plus main key reprise | the Reprise button (one name in both languages — the product name is not translated) and the page it opens; labels follow the active language (en, zh) |
| HTTP routes | GET /api/reprise/{overview,rounds,candidate,tree,conclusion,calls,call} | the four views, the search tree and the model-call audit, each read through its own read-only handle |
| Static assets | GET /api/reprise/page, /api/reprise/dashboard.js, /api/reprise/format.js | a standalone page for a host with no client slots, with the formatting module the page and the panel share |
| CLI | node packages/core/lib/cli/run.js --workspace <dir> --config <file> | the same wiring the host ticks, driven by a loop; exit 0 conclusion, 1 no comparable metric, 2 usage, 3 timeout |
| CLI | node packages/core/lib/cli/dump.js --candidate <id> | timeline, every gate verdict, the gate chain step by step, the candidate's worktree path and whether it is still on disk, identity bindings, frontier position, and why the run stopped |
| CLI | node packages/core/lib/cli/dump.js --overview | the workspace page: cards and queue, the best result, and the measured/published comparison |
| CLI | node packages/core/lib/cli/dump.js --tree | the search as a lineage: who branched from whom, and the reason each branch ends |
| CLI | node packages/core/lib/cli/dump.js --calls / --call <ref> | the model-call audit, and one call with its prompt and response verbatim |
| CLI | node packages/core/lib/cli/intents.js place <kind> / list | human intents (proposal, grant, anchor, pause, resume, approval, experience) written into <workspace>/pending/ |
| CLI | node packages/core/lib/cli/jobs.js list / reap --job <id> | the cluster job ledger: our jobs as the cluster lists them, the orphans (our marker, no ledger row), and the only way an orphan is ever stopped |
| CLI | node packages/core/lib/cli/worktrees.js list / prune --candidate <id> | list: one row per candidate — round, phase, files landed, whether the tree is on disk, its path. prune: the only way a worktree is ever removed (git worktree remove plus a worktree_pruned fact), and only when the run declared runner.keep: false |
| Skills | <domain>/prompts/{land,verify,cand-analyze,round-plan,round-reflect}/SKILL.md | what the model is told at each of the five call sites; markdown on disk, part of the domain pack, read fresh on every call — not DSH skills and not registered as such |
The bundle provides no service and emits no event: it consumes timer, connection and tools.
Every route is GET; there is no write endpoint to reach for, including one that would let the page
change the run it is reporting on.
The five-minute path
A laptop, a clone, and nothing else: no GPU, no cluster, no model provider. The whole path is exercised
by packages/core/test/five-minute-path.test.js in a temporary directory — it runs the commands this
section prints, so these five lines are a contract, not an illustration.
Step 1 — make the workspace packages importable
node scripts/link-workspace.mjs
It checks the Node version, links packages/* so the repository root can import them, and finishes by
actually importing the kernel: a clone that cannot import the kernel cannot do anything else either.
No network, no package manager, idempotent.
Step 2 — build a workspace
node examples/feed-structure/init.mjs .workspace/feed
A real setup, not a fixture: it copies the toy model repository, commits it (candidates branch from that
commit), freezes a sha256 manifest of what the run starts from, and measures the baseline by running
metric.mjs through the actual local executor. That number is a cross-check: the run measures its own
baseline at the BASELINE phase — submit, wait, collect — and freezes it as a fact, so the denominator
comes from the same executor run as everything compared against it.
Step 3 — look at the domain pack you own
node examples/feed-structure/inspect.mjs
The pack manifest, the metric contract (direction and role), the adjustable-surface whitelist, the priors and hypothesis classes, this round's structural hypotheses, the executor entry and the wiring. Changing any of them is a configuration change; the kernel knows nothing about AUC, clicks, or your model.
Step 4 — run the loop
node packages/core/lib/cli/run.js --workspace .workspace/feed --config examples/feed-structure/config.json
One line per phase step — including the baseline job it submits and the windowed samples it freezes —
then the winner, its metric, the baseline it beat, and the rule that ended the run. This is the same wiring the host plugin uses — workspace.js and tick() — driven by a loop
instead of by the host's timer, so what you just watched is the plugin's behaviour.
Step 5 — ask why
node packages/core/lib/cli/dump.js --candidate r1-h2 --workspace .workspace/feed
Timeline, every gate conclusion, the identity bindings, the frontier position, and why the run stopped —
from the fact plane alone. r1-h2 is the winner the shipped example produces; use whatever step 4
printed. No --config is needed: the run records the metric contract and the stop rules into the
workspace at INIT, so a reader with only the directory gets the same answer. Add --rebuild to replay
the whole event stream and check the materialised view still follows from the facts.
What the example does not exercise: a live model (recorded/judgement.json replays four answers) and a
real coding harness (apply-change.mjs applies the hypothesis mechanically). Both are configuration —
model.command and candidate.command. examples/feed-structure/README.md
says what to change and what stays unverified.
Checks and local development
npm test # scripts/check.mjs, then the full suite
npm run check # manifests, export targets, and a parse of every module under lib/
scripts/check.mjs covers what the suite cannot see: export targets that do not exist, exports
pointing at TypeScript the host cannot import, declared dependencies nothing imports, imports nothing
declares, and modules no test happens to load.
The packages resolve cordis from the host profile rather than installing a second copy, so link the
profile's module tree instead of running pnpm install:
ln -s ~/.dsh/profiles/node_modules node_modules
Five tests in packages/core/test/host-plugin.test.js drive cordis's real lifecycle. cordis is the
plugin host (MIT, shipped with the Harness profile), not a dependency of this repository, so on a
machine without it those five report themselves skipped instead of failing to load. Wire the
pre-commit hook once per clone: git config core.hooksPath .githooks.
Nothing here may be imported as TypeScript by the host: every bundle exposes main: lib/*.js, and the
shipped code is plain ESM with no build step.
Understand the implementation
Implementation internals — click to expand
Design
- Two entry points, because a missing dependency defers a plugin rather than failing it.
boot.jsdeclares nothing and reports;tick.jsdeclaresinject: ['timer']and ticks;dashboard.jsdeclaresinject: ['connection']. A merged plugin would make "the package never loaded" and "the timer never appeared" produce the same empty log. - Facts before state, always. A fact is an append-only row identified by
(candidate_id, local_seq), written in its own transaction before the state it justifies, so a crash leaves a detectable lag instead of a silent divergence. Replaying a fact with the same payload is aduplicate; the same identity with a different payload is aconflictand is refused. - Views are derivable or they are wrong. The frontier, the round history and the dashboard's four
pages are all computed from facts.
dump --rebuildrefuses to agree with a view that no longer follows from them. - The model gets five call sites and no other.
land,verify,cand_analyze,round_plan,round_reflect. Each call is a separate process with a prompt rendered from a markdown skill plus facts as JSON; the answer shape lives in code next to its parser, so a markdown example cannot drift away from what the judge accepts. - Gate two consumes a structured verdict, not an opinion. The candidate's diff is checked by the
kernel — syntax, the domain pack's adjustable-surface whitelist, residual conflict markers — and the
adversarial review must answer accept or reject; an answer that does not parse is a failure. When the
review accepts over a check that failed the record is a
conflictand the candidate stops. Each attempt is a numbered record carrying the sha256 of the bytes it checked, so it can be recomputed rather than trusted, and a plain "no" is retried on a backoff instead of killing the candidate. - Read-only is structural, not a promise. Every read opens its own
readOnlySQLite handle and closes it: the tool physically cannot record a fact, two browser windows cannot interfere, and the page works with every session closed — it needs the workspace directory and nothing else. - Reserve, then settle. A dispatch reserves
budget.candidate_gpu_hoursbefore the job starts and settles the measured cost after, so a crash mid-job is charged rather than forgotten. Tokens are recorded and never judged, because harnesses report them too unevenly to use as a gate. - Say the dangerous default out loud. An uncapped remote executor is legal, is the shipped default,
and is warned about on every read — boot log,
run's banner, andoverview().warnings, which both clients draw.
Source map
| Path | Role |
|---|---|
packages/contract | four-state validation, the identity binding chain, artifact schemas, metric contract |
packages/core | phase machine, tick, fact store, derived views, stop policy, plugin wiring, CLI |
packages/exec | candidate-implementer and external-job adapters (local, ray, claude, dsh) with conformance suites |
packages/memory | the injection packet, exposed as a dsh-mnemon Source |
packages/ui | read-only dashboard: host-side projections over routes, plus a plain-ESM client |
packages/exec-python | optional proxy-metric bridge (needs torch) — manifest only, no code yet |
examples/feed-structure | a complete domain pack and a worked scenario: domain.json plus the metric contract, priors, adjustable surface, five prompts, executor entry, structural hypotheses and a local metric |
examples/generative-recommenders | a domain pack for someone else's repository (HSTU in meta-recsys/generative-recommenders): metric contract with a tolerance and a guardrail, cited priors, a verified surface whitelist, five prompts and the job's metric reader — data and prompts only, no kernel change |
Dependency direction is acyclic by package name: contract depends on nothing here; exec and
memory depend on contract; core depends on all three. One deliberate exception: ui reads
core's store and projection modules by relative path, because the dashboard must not be able to open
a write handle to the fact plane, and a relative import of the read-only readers says exactly that.
Further Exploration
CONTEXT.md— the ten words the code depends on, with the precise meaning it assumesdocs/extending.md— add an executor; add an artifact kinddocs/windowed-samples.md— the one-row-per-window sample shape, its two adapters, and the window intersection ruledocs/license-audit.md— every dependency and borrowed idea, against Apache-2.0docs/adr/0001-acceptance-is-a-reproduced-public-experiment.md— what "done" means for this projectexamples/feed-structure/README.md— the worked scenario in detailexamples/generative-recommenders/README.md— the second pack, and the human-review checklist it ships withCODING_STANDARDS.md— how to review a diff here
Model Experience
What the model sees
Two ways, both read-only and neither on a critical path.
reprise_status is the only tool the bundle registers. Its input is candidate (an optional id, for
that candidate's page instead of the overview) and events (0–20, default 6). It answers with the
envelope the HTTP routes serve — view, workspace, cursor, data, an incremental.events list —
trimmed to six facts and ten frontier members; when a workspace has no facts yet it says so with
data: null and a reason instead of failing. Refusals carry ok: false and a reason. The whole fact
stream is never a tool result.
Inside the loop, the model is a subprocess: at each of the five call sites it reads one SKILL.md from
the domain pack plus JSON facts (candidate identity, L1 check results, the metrics the monitor reported), and
answers in a wire format the judge parses. It is never handed the fact plane, a session, or another
candidate's outcome.
Token effect
Small and bounded. A status answer is a single projection page trimmed to six events and ten frontier
members; the tool is called when someone asks how the run is doing, not on every turn. The five
judgement calls carry one markdown prompt each (the pack's prompts/ are 246 lines in total) plus JSON,
and each is a fresh process, so nothing accumulates across a run.
KV Cache effect
None inside the conversation: the tool call sits in the normal tool-call sequence and adds no system prefix. None across judgement calls either — they are separate processes started per call site, so there is no shared prefix to cache and no cache to invalidate. Nothing in the plugin mutates the system prompt.
Known Limitations and Deferred Work
These limits define when this is a poor fit — they are not a task list.
- Verified offline only. The phase machine, validators, identity chain, fact plane, ledger,
frontier, stop rules, executor and harness ports,
dump, and the five-command path all run innpm test. Not verified end to end here: a live model, a real coding harness, a real cluster, and a browser actually loading the page against a real host. - Not on npm. Every package is
private, versioned0.0.0, and depends on its siblings throughworkspace:*. What installs is the release tarballscripts/build-release.mjspacks — not a registry package, and not the git URL. - The shipped bundle patch carries no config, on purpose. The package cannot know where your
workspace is, so it ships the row and nothing else; a
reprise-coreentry in your profile patch supplies everything, andcordis.example.ymlbeside the patch is a filled-in worked example. A run with no config is one log line, not a mount. - No host-side config schema. The package exports no
Config, so the Plugin Manager cannot render or validate the row;config.jsis the only validator, and its verdict is a log line. - No write surface. Pause, resume, grant and approvals arrive as files under
<workspace>/pending/(intents.js), not as HTTP calls. - One workspace, one run. Two runs in one directory are not a supported configuration; two workspaces are two independent dashboards.
packages/exec-pythonis a manifest. No code, notorchimport; the proxy-metric bridge is deferred.
Dev Note
Non-authoritative maintainer context — click to expand
The repository is a pnpm workspace of six packages, tested on Node 24 (Node 22 works and warns, because
node:sqlite is experimental there). npm test is the whole gate; the pre-commit hook runs exactly it
with git's environment stripped, because the tests create their own temporary repositories and an
inherited GIT_DIR points them at this one.
Distinctive machinery worth not re-deriving during a review: phase-level artifact contracts, the
four-state verdict vocabulary (ok / conflict / missing / invalid), the identity binding chain,
and frontier and stop rules that are code rather than policy configuration. The code is the chain of
authority for a decision, and it is the record of what was accepted and what was refused.
Comments still cite things like spec: 相位机 or issue 06. That is the shorthand this codebase was
built with, naming a design record kept outside the repository as the author's working notes. Every
claim in such a comment stands on its own — read the sentence, not the citation.
License
Apache-2.0 — see LICENSE.