# DACW — Defensive Agent Coding Workflow
Let AI write the code, let machines verify it, and let humans make the final call.
[简体中文](README.md) | English | [Safety](SAFETY_EN.md)
DACW is a **DSH Host plugin**: a writer subagent emits code changes under an incremental protocol, a deterministic runner executes the project's own tests, an independent reviewer returns a structured verdict, and the workflow stops at a human gate. Every failure path converges to `NEED_HUMAN` — **nothing is ever merged automatically**.
## Features
- **Separation of powers**: the writer only emits a JSON changeset, the runner only executes whitelisted commands, the reviewer only emits a verdict — no single party can merge code.
- **Zero-tool child isolation**: writer/reviewer run on persona-only presets; on `agent/created` the plugin uses `ctx.tools.restrict({ allow: [] })` to strip every inherited tool and verifies the child sees zero tools. If isolation fails, the child is never created.
- **Output protocol v2**: the writer emits only deltas against the baseline, untouched files are preserved byte-for-byte, which cuts tokens and prevents "the code that passed last round got broken" regressions.
- **Deterministic runner**: command whitelist (npm / pnpm / yarn / npx / node / python / python3 / pytest / cargo / go), environment whitelist, timeout tree-kill, output truncation.
- **Structured review**: the verdict must be exact JSON; unparseable output is treated as REJECTED; PASS containing suspicious wording goes to the human gate.
- **Configurable models**: writer/reviewer can each inherit the parent session route or select from the DSH configured-model directory; the startup log and the result object show the models actually in effect.
- **Workspace safety gate**: accepts only an empty directory or a valid `.dacw-workspace` marker written by this plugin; refuses filesystem roots, the user home directory, and `DSH_HOME`/profile directories; writes are checked with realpath plus per-segment symlink checks.
- **Audit record**: the state file records the task, file manifest summary, test output and review text, signed with HMAC-SHA256.
## How it works
```
Writer (JSON changeset) → Orchestrator (validate / merge / write) → Runner (project tests)
│ failure → rewrite (up to maxRounds)
Reviewer (JSON verdict) → human gate NEED_HUMAN
```
1. The writer emits deltas under protocol v2.
2. The orchestrator escape-checks paths, merges the baseline, and resets the workspace to the merged set.
3. The runner executes tests by project type (re-sniffed every round).
4. The reviewer audits the full file set plus foundation files (lockfiles as SHA256 digests).
5. On PASS the tool returns `success: true` and `status: "NEED_HUMAN"`; every other path also converges to the human gate.
## Install
Prerequisites: a DSH providing `ctx.subagents`, `agentPresets` and the `spawn` provider (`@deepseek-ai/dsh-subagent-spawn-in-process`); Node ≥ 22.13 for the build; the tested project's dependencies must be installed manually.
```bash
node build.mjs # build dist/ (Node built-in type stripping, no typescript)
node selftest.mjs # 60 regression vectors
npm pack --pack-destination ../dacw-pack # produce the install artifact
```
In the DSH plugin manager run `install_bundle` with the tarball's absolute path prefixed by `file:` (for example `file:/absolute/path/to/stmcw1-dacw-1.0.0.tgz`). Installation inserts Host row `dacw` (bundle package `@stmcw1/dacw`) into the profile. **Do not install with a directory path** — a directory install bypasses the `files` whitelist and ships sources, tests and build scripts into the profile.
Upgrade: edit sources → `node build.mjs` → `npm pack` → `remove_bundle` → `install_bundle` the same tarball → restart the Harness.
## Usage
A model calls the `dacw_run` tool:
| Argument | Required | Description |
|---|---|---|
| `task` | yes | Natural-language requirement (the more specific, the better) |
| `workspace` | no | Dedicated workspace; absolute or relative to the session working directory; omitted means the configured value / session working directory |
| `task_id` | no | Suffix for the audit record filename; auto-generated when omitted |
On success:
| Field | Description |
|---|---|
| `success` / `status` | Machine-gate result; `status` is always `NEED_HUMAN` |
| `message` | Human-readable conclusion |
| `workspace` / `files` / `round` | Workspace, produced files, terminating round |
| `models` | `{ writer, reviewer }`, the models actually in effect |
| `logs` / `elapsed_ms` | Log tail and elapsed time |
Example startup log line:
```
🧠 模型配置 | ✍️ 写手: deepseek-account/deepseek-flash(插件配置) | 🔍 审查者: 继承父会话路由(DSH 默认: deepseek-account/deepseek-flash)
```
On workflow failure the tool throws (including workspace, status, round and log tail) instead of returning a normal failure result; no path commits or merges anything.
## Model configuration
`writerModel` / `reviewerModel` accept four forms:
| Value | Behavior |
|---|---|
| `null` / `"inherit"` | Inherit the parent session route (default) |
| `"provider/model"` | Explicit; the provider must be registered in DSH |
| `"model"` | Resolve the unique provider from the DSH configured-model directory |
| `{ provider?, model, reasoningEffort? }` | Object form; `reasoningEffort` is forwarded to the child |
```yaml
config:
writerModel: deepseek-account/deepseek-flash
reviewerModel: { model: deepseek-flash, reasoningEffort: high }
```
Resolution happens at the start of every tool call, so DSH settings changes apply immediately; a resolution failure fails closed and never silently switches models.
## Safety
DACW executes model-generated code and commands and is **not a container-grade sandbox**. Run it in a disposable or dedicated environment under a least-privilege account, and always keep the human gate. See [SAFETY_EN.md](SAFETY_EN.md) for the full boundaries and disclaimer.
## Project layout
| Path | Responsibility |
|---|---|
| `plugin.ts` | DSH Host half: tool registration, child presets, runtime tool isolation, model resolution, workspace gate |
| `index.ts` | Orchestrator: rounds, baseline merge, runner dispatch, verdict, human gate |
| `runner.ts` | Runner: command resolution, timeout tree-kill, output truncation |
| `state_manager.ts` | State: atomic writes, HMAC, serial queue |
| `build.mjs` / `selftest.mjs` | Build (TS → `dist/`) and regression self-test |
| `cordis.patch.yml` / `locale/` / `icon.png` | Plugin manifest and display resources |
## Development
- `dist/` is produced by `build.mjs` with Node built-in type stripping; it needs no `typescript` and is not committed.
- Run `node build.mjs && node selftest.mjs` before submitting changes.
- Every config field and its default lives in `DEFAULT_PLUGIN_CONFIG` in `plugin.ts` (each can be overridden in the `config` block of `cordis.patch.yml`).
- Experimental project; interfaces and behavior may change.
## Status
Experimental developer preview (current version `v1.0.2`, bundle `@stmcw1/dacw`): the build smoke check and regression self-test pass, and a live loop has been verified on the DSH `desktop` profile (writer produced files → `npm test` passed → independent reviewer PASS → `NEED_HUMAN`), with child tool isolation measured in effect. Issues and PRs are welcome.
## License
Released under the [MIT](LICENSE) license.
npm: @stmcw1/dacw · GitHub