voku / agent-edit
Deterministic, evidence-backed source mutation for coding agents: validate, apply transactionally and verify agent-map plans.
Requires
- php: ^8.3
- helgesverre/toon: ^3.1
- voku/agent-map: ^0.18.3
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 0.2.2
- 0.2.1
- 0.2.0
- 0.1.1
- 0.1.0
- dev-feat/class-removal-plan-1
- dev-fix/snapshot-crlf-command-output
- dev-release/0.2.2-marker
- dev-release/0.2.2-fixes
- dev-ccr-6f81b111-pl491d-explain-refusals
- dev-ccr-6f81b111-pl491d-refusal-leaves-no-bundle
- dev-release/0.2.1-marker
- dev-release/0.2.1
- dev-ccr-6f81b111-pl491d-lock-two-process
- dev-ccr-6f81b111-pl491d-receipt-write-failure
- dev-ccr-6f81b111-pl491d-mark-0.2.0
- dev-ccr-6f81b111-pl491d-release-0.2.0
- dev-ccr-6f81b111-pl491d-snapshot-tests
- dev-ccr-6f81b111-pl491d-manifest
- dev-ccr-6f81b111-pl491d-mark-0.1.1
- dev-ccr-6f81b111-pl491d-release-0.1.1
- dev-test/publication-rollback-dogfood
- dev-ccr-6f81b111-pl491d
This package is auto-updated.
Last update: 2026-10-05 16:41:01 UTC
README
Deterministic, evidence-backed source mutation for coding agents.
agent-edit is the write plane next to agent-map (the read/planning plane):
agent-map = observe + plan
agent-edit = validate + mutate + verify
agent-loop = authorize + orchestrate
It consumes one already-produced, versioned agent-map plan, revalidates every source hash, inclusive byte range, expected token and plan provenance against the current source and Map, stages and syntax-checks all rewritten PHP, publishes edits and file moves in one transaction (every file is restored on any failure), and then verifies the result against a fresh Map.
It is not an LLM editor, not a workflow engine and not a refactoring recommender.
Use
vendor/bin/agent-map build --root=. --paths=src --out=.agent-map/php-symbols.json vendor/bin/agent-map method-move-plan 'App\Foo::helper' 'App\Bar' --format=json > plan.json vendor/bin/agent-edit apply plan.json --dry-run # validate everything, write nothing vendor/bin/agent-edit apply plan.json # transactional apply + receipt bundle vendor/bin/agent-map build --root=. --paths=src --out=.agent-map/php-symbols.json # refresh the map vendor/bin/agent-edit verify --bundle=.agent-edit/receipts/<label>
apply writes execution.json (the receipt) into --output-dir (default .agent-edit/receipts/<label>; --task LABEL sets the label, default plan-<sha256 prefix>). verify re-reads that receipt, the bound plan and the refreshed Map and writes verification-result.json. Each verification attempt first removes the previous result, so a failed attempt cannot leave an earlier passed verdict in the bundle.
Capabilities
vendor/bin/agent-edit capabilities --format=json
vendor/bin/agent-map plan-capabilities --format=json | vendor/bin/agent-edit capabilities --with-map=-
The second command intersects what Map can plan with what agent-edit can execute: executable, planned_not_executable (for example method_copy_plan) and executable_not_planned. A host should expose only the intersection to a coding agent.
Executable contracts (all @1.0): method|function|class|property|class_constant|parameter_rename_plan, class_move_plan, method_move_plan, method_removal_plan, property_removal_plan, class_constant_removal_plan.
Package API
voku\AgentEdit\EditEngine is the only authority; agent-edit apply|verify|capabilities are argument/print adapters over it.
$engine = new voku\AgentEdit\EditEngine(); $engine->preflight($plan, $map, $root); // validate everything, write nothing $engine->apply($plan, $map, $root); // mutation lock + transactional apply (no receipt) $receipt = $engine->applyWithReceipt(new ApplyRequest( repositoryRoot: $root, planPath: $planFile, mapIndexPath: $mapFile, mapRoot: $root, outputDirectory: $bundleDir, label: 'my-task', dryRun: false, authorizeMutation: static fn (string $label) => $host->assertMayMutate($label), // must throw to refuse )); $result = $engine->verify($root, $bundleDir, $mapFile); // after rebuilding the Map; writes verification-result.json
Plan type and contract version are routed only through CapabilityRegistry. Anything it does not list (an unknown type, or a known type with an unknown contract_version) is rejected before any source is read.
Receipt
execution.json inside the bundle is an agent-edit receipt (schema_version 1.0). It binds the plan file hash, Map digest, runner identity and Git-observed changed_files, and is what verify consumes. It is not owned by agent-loop. The names execution.json, task_id (the caller-supplied label) and runner.name are kept for compatibility with hosts that already read them; model_input_tokens/model_tool_calls are always 0.
Without Git the receipt falls back to a Map-scoped observation: before mutating, agent-edit stores map-scope-before.json (path → sha256 for every Map-indexed file, plus the observed absence of preflight-validated move destinations) in the bundle and the receipt references it only by scope_evidence (source, path, sha256) with changed_files_source: map_manifest_diff. This records both sides of a file move. verify recomputes the manifest and diffs it, so an extra changed indexed file outside the plan is an error. It proves only map_indexed_files: the result is status: incomplete with scope.status: scope_unproven (never passed, CLI exit code 3), because a file outside the Map index can change unobserved. With Git the receipt and result are unchanged.
After an authorized mutation attempt fails, applyWithReceipt() first observes the post-rollback working tree and persists a runner_failed receipt before rethrowing the original failure. A host authorization refusal still happens before the mutation attempt and writes no receipt.
A refusal that writes no receipt (dry-run preflight refusal such as a blocked plan, an unsupported plan type or contract version, a host authorization refusal) leaves no bundle directory behind: directories applyWithReceipt() created for the attempt are removed again if they are still empty; a bundle directory that already existed, or that holds any file, is never touched. An authorized mutation attempt that fails keeps its runner_failed receipt.
When the receipt itself cannot be written (unwritable bundle directory, full disk, a blocking path), the apply outcome stays the primary signal and nothing is silent:
| Situation | Behavior |
|---|---|
| Source published, receipt write fails | ReceiptNotPersistedException (changedFiles, outputDirectory, previous = the write error). The lock is already released and the transaction committed, so there is no snapshot to restore: the working tree is changed without evidence. No receipt file exists (never a half-written one), so verify refuses the bundle and a governed close cannot pass. Restore from version control and re-plan. |
| Apply failed (rolled back), failure receipt write fails | The original apply failure is rethrown unchanged; the receipt problem never replaces it. |
| Dry run, receipt write fails | The plain write error; nothing was published. |
Boundaries
agent-edit owns plan validation, exact edits/moves, transactional publication, rollback, the mutation lock, observed changed files, receipts and deterministic verification, and the executable capability list.
It does not own repository analysis or plan generation (agent-map), task approval/workflow state, LLM routing, briefing, durable task evidence or closeout (agent-loop), and it never commits or pushes.
Validation
composer ci
PHPStan-backed plans need phpstan/phpstan installed so Map can build a +phpstan index.