← Back to home@sunstrikes

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
GitHub repo

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"

dsh-reprise

English | 简体中文

CI Apache-2.0 status: public beta GitHub stars node >= 22 DeepSeek Harness bundle

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


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.json in the patch's config (or point DSH_REPRISE_CONFIG at it, for a launcher that writes no YAML). The file is read at mount, once: it is the base configuration and any inline config key wins over it — a shallow merge, key by key. workspace written 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_WORKSPACE names the workspace when the configuration does not; a relative value resolves against the host cwd, an absolute one is used as is. An inline workspace still 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.

FieldDefaultMeaning
workspacerequired, or DSH_REPRISE_WORKSPACEdirectory 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
domainrequireddirectory of the domain pack; no pack, no metric contract, no prompts
interval_ms5000tick cadence
executor.kindthe pack'slocal (a subprocess) or ray (Job Submission REST)
executor.commandthe pack's, required for localargv of one job, resolved inside the candidate worktree
executor.base_urlrequired for rayRay dashboard URL
executor.root_dir / metrics_required'jobs' / truewhere job dirs live; a job without metrics is a failure
candidate.kind'command'command, claude or dsh — the LAND implementer
candidate.commandrequired for commandargv; {candidate} and {worktree} are substituted
runner.repo / runner.dirrequired with candidaterepo inside the workspace; where worktrees are created (<workspace>/worktrees/<candidateId>)
runner.keeptrueretention. 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_refgit defaultthe 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.commandrequired for kind: commandthe wrapper's argv: one JSON question on stdin, one JSON answer (a JSON object) on stdout
model.timeout_ms600000how long one judgement may take before it is killed — a phase never waits for a model without a deadline
rounds.metric.name / .directionderived from the metric contractthe 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_refthe contract's namepointer to the frozen metric contract
rounds.max_rounds / .candidates_per_round3 / 2round 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_attempts60000 / 3600000 / 3backoff for a refused artifact, then a human
stop.max_rounds30 disables the rule
stop.max_no_improvement_rounds / .consecutive_empty_rounds0 (off)opt-in stop rules
stop.max_wall_hours / .max_windows0 (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_floor0 (off) / 0kill 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_limit0 (unlimited)hard cap; an uncapped remote executor is warned about, every read
budget.soft_ratio / .candidate_gpu_hours0.8 / 0.5stop dispatching at this fraction; what a dispatch reserves
budget.max_runtime_hours / .judge_tokens0 (unlimited) / falsewall-clock gate; tokens are recorded, never judged
metricsthe pack's whole contractone 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_attempts1000 / 60000 / 2job poll backoff
gate.max_attempts / .base_ms / .max_ms3 / 1000 / 60000how 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_ms3600000 / 1800000per-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

SurfaceIdentifierWhat it does
Model toolreprise_statusthe run's progress as JSON, read-only, from the same projection the page draws
Sidebar panelsidebar.panellist id reprise, order 25, plus main key reprisethe 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 routesGET /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 assetsGET /api/reprise/page, /api/reprise/dashboard.js, /api/reprise/format.jsa standalone page for a host with no client slots, with the formatting module the page and the panel share
CLInode 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
CLInode 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
CLInode packages/core/lib/cli/dump.js --overviewthe workspace page: cards and queue, the best result, and the measured/published comparison
CLInode packages/core/lib/cli/dump.js --treethe search as a lineage: who branched from whom, and the reason each branch ends
CLInode packages/core/lib/cli/dump.js --calls / --call <ref>the model-call audit, and one call with its prompt and response verbatim
CLInode packages/core/lib/cli/intents.js place <kind> / listhuman intents (proposal, grant, anchor, pause, resume, approval, experience) written into <workspace>/pending/
CLInode 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
CLInode 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.mdwhat 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.js declares nothing and reports; tick.js declares inject: ['timer'] and ticks; dashboard.js declares inject: ['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 a duplicate; the same identity with a different payload is a conflict and 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 --rebuild refuses 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 conflict and 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 readOnly SQLite 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_hours before 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, and overview().warnings, which both clients draw.

Source map

PathRole
packages/contractfour-state validation, the identity binding chain, artifact schemas, metric contract
packages/corephase machine, tick, fact store, derived views, stop policy, plugin wiring, CLI
packages/execcandidate-implementer and external-job adapters (local, ray, claude, dsh) with conformance suites
packages/memorythe injection packet, exposed as a dsh-mnemon Source
packages/uiread-only dashboard: host-side projections over routes, plus a plain-ESM client
packages/exec-pythonoptional proxy-metric bridge (needs torch) — manifest only, no code yet
examples/feed-structurea 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-recommendersa 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

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 in npm 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, versioned 0.0.0, and depends on its siblings through workspace:*. What installs is the release tarball scripts/build-release.mjs packs — 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-core entry in your profile patch supplies everything, and cordis.example.yml beside 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.js is 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-python is a manifest. No code, no torch import; 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.