devrites 7.2.6 → 7.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +6 -1
- package/SECURITY.md +11 -7
- package/docs/command-map.md +4 -0
- package/docs/engine/commands.md +25 -3
- package/docs/orchestration.md +9 -0
- package/engine/internal/acceptance/cli.go +9 -5
- package/engine/internal/acceptance/run.go +97 -38
- package/engine/internal/devritespaths/paths.go +7 -2
- package/engine/internal/install/apply.go +5 -0
- package/engine/internal/install/merge.go +48 -6
- package/engine/internal/install/preflight.go +21 -3
- package/engine/internal/install/uninstall.go +9 -10
- package/engine/internal/lib/apply_bundle.go +7 -1
- package/engine/internal/lib/closeout.go +7 -7
- package/engine/internal/lib/context.go +10 -6
- package/engine/internal/lib/dispatch.go +81 -21
- package/engine/internal/lib/resolve.go +10 -5
- package/engine/internal/lib/state_edit.go +9 -2
- package/engine/internal/lib/taskgraph.go +94 -21
- package/engine/internal/lib/triggers.go +2 -2
- package/engine/internal/state/cursor.go +5 -5
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/.claude/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/.claude/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/.claude/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/.claude/skills/rite-spec/SKILL.md +2 -2
- package/pack/.claude/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/generated/claude/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/claude/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/claude/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/claude/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/claude/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/generated/codex/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/codex/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/codex/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/codex/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/codex/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/devin/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/generated/devin/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/devin/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/devin/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/devin/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/devin/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/devin/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/dsh/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/generated/dsh/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/dsh/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/dsh/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/dsh/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/dsh/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/dsh/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/dsh/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +13 -8
- package/pack/generated/omp/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/omp/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/omp/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/omp/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/omp/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/pi/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/generated/pi/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/pi/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/pi/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/pi/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/pi/skills/rite-temper/SKILL.md +4 -5
- package/package.json +3 -3
- package/scripts/build-host-artifacts.sh +5 -6
- package/scripts/omp-generate.sh +24 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [7.2.8](https://github.com/ViktorsBaikers/DevRites/compare/v7.2.7...v7.2.8) (2026-10-31)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **no-release:** close installer evidence and recovery integrity gaps ([#200](https://github.com/ViktorsBaikers/DevRites/issues/200)) ([edb8754](https://github.com/ViktorsBaikers/DevRites/commit/edb8754ecf80d4a0dd83590c5b346787c3647244))
|
|
10
|
+
* **no-release:** select exact OMP specialists and preserve reviewer roles ([#199](https://github.com/ViktorsBaikers/DevRites/issues/199)) ([b31814a](https://github.com/ViktorsBaikers/DevRites/commit/b31814a37d80c45a27d5470ab5464cd93c91f76a))
|
|
11
|
+
* **release:** publish pending installer fixes ([dfd2192](https://github.com/ViktorsBaikers/DevRites/commit/dfd2192d1b7d9da1b0111603760c5eb8f246b9a4))
|
|
12
|
+
|
|
13
|
+
## [7.2.7](https://github.com/ViktorsBaikers/DevRites/compare/v7.2.6...v7.2.7) (2026-10-31)
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* **skills:** keep lookahead planning through false stops ([#192](https://github.com/ViktorsBaikers/DevRites/issues/192)) ([1185459](https://github.com/ViktorsBaikers/DevRites/commit/1185459e0477fe7fc9ee6b8e32e3ef315100ae15))
|
|
18
|
+
|
|
5
19
|
## [7.2.6](https://github.com/ViktorsBaikers/DevRites/compare/v7.2.5...v7.2.6) (2026-10-31)
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ history can't lose it. After you clear the conversation, switch hosts, or hand
|
|
|
31
31
|
the feature to another engineer, the next session reads the same plan,
|
|
32
32
|
decisions, and evidence before it continues.
|
|
33
33
|
|
|
34
|
-
**Status:** [`v7.2.
|
|
34
|
+
**Status:** [`v7.2.8`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v7.2.8): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
35
35
|
|
|
36
36
|
This is the latest published release; `main` may contain unreleased work.
|
|
37
37
|
|
|
@@ -382,6 +382,11 @@ with `--force`; legacy manifests without hashes also require `--force`.
|
|
|
382
382
|
`--force --dry-run` lists the exact destructive actions. Every settings and
|
|
383
383
|
marker merge is checked before the first write, so a rejected merge (such as a
|
|
384
384
|
Codex config that already sets top-level `default_permissions`) leaves the project untouched.
|
|
385
|
+
Unmerges receive the same preflight: malformed settings or legacy hooks JSON
|
|
386
|
+
blocks uninstall and dropped-host pruning, including dry runs, before writes.
|
|
387
|
+
Existing files retain their permission bits. Claude permission ownership is
|
|
388
|
+
recorded before settings replacement, so interrupted merges retain the records
|
|
389
|
+
needed to remove installer-added grants on retry and uninstall.
|
|
385
390
|
Marker-merged files keep user content outside the DevRites block, and
|
|
386
391
|
`.devrites/` runtime state stays in place. `update` reuses the install flags
|
|
387
392
|
recorded in the manifest; flags you pass explicitly win and are recorded. The installer refuses symlinks, junctions, and paths that
|
package/SECURITY.md
CHANGED
|
@@ -218,13 +218,17 @@ hosts or their makers. codebase-memory-mcp, ego-lite, browser-harness, and
|
|
|
218
218
|
Playwright MCP are optional user-selected tools that DevRites calls through
|
|
219
219
|
their documented interfaces rather than bundling them.
|
|
220
220
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
221
|
+
Advisories whose only installed copy is inside the npm CLI tarball
|
|
222
|
+
(`node_modules/npm/node_modules/`) are not blocking. Overrides cannot replace
|
|
223
|
+
those copies, and the publish plugin cannot take a newer npm that still bundles
|
|
224
|
+
them. The same advisory installed anywhere else still blocks. Any other
|
|
225
|
+
npm-audit exception is a temporary trust record, not a claim that an upstream
|
|
226
|
+
issue is fixed. Each entry in `scripts/npm-audit-exceptions.json` is restricted
|
|
227
|
+
to an exact advisory, package range, installed node path, owner, reason, source,
|
|
228
|
+
and near-term expiry; validation re-audits the live dependency graph and fails
|
|
229
|
+
stale, broadened, unmatched, expired, or inside-the-7-day-refresh-horizon
|
|
230
|
+
entries. Prefer pinning a patched ancestor (see `package.json` `overrides`)
|
|
231
|
+
over extending an expiry.
|
|
228
232
|
|
|
229
233
|
### Agentic trust boundaries
|
|
230
234
|
|
package/docs/command-map.md
CHANGED
|
@@ -50,6 +50,10 @@ The Go surface is intentionally closed and deterministic:
|
|
|
50
50
|
| Install/operator | `install`, `update`, `uninstall`, `version` | Direct update acquires latest; npm/bootstrap may supply local candidates; the engine performs manifest-owned local changes. |
|
|
51
51
|
| Native policy | no engine command | Skills/root own spec grammar re-read, qid allocation, Clarify cursor edits, AFK/recovery accounting, and read-only `/rite-doctor`. |
|
|
52
52
|
|
|
53
|
+
`state apply-bundle` merges workspace-relative path aliases into one target:
|
|
54
|
+
`plan.md`, `./plan.md`, and `refs/../plan.md` share base pins and patch hunks.
|
|
55
|
+
Conflicting replacements or base hashes block the entire bundle without writes.
|
|
56
|
+
|
|
53
57
|
`devrites-engine help` exhaustively lists the operational engine commands.
|
|
54
58
|
Standard `help`, `-h`, `--help`, `version`, and `--version` forms remain
|
|
55
59
|
available; each operational command and subcommand also accepts `-h` / `--help`.
|
package/docs/engine/commands.md
CHANGED
|
@@ -17,7 +17,7 @@ workflow strategy.
|
|
|
17
17
|
| `check preconditions <slug> [--slice SLICE-###] [--timeout N]` | Run the `test-plan.md` Build-entry preflight rows whose optional `Run at` cell is `vet` (with `--slice`, `pre-dispatch <SLICE>`); `action` rows and rows without the cell never run. Writes one candidate-bound transcript per row under `references/gates/preflight-*/`; a row passes on exit 0. A `pre-dispatch` row whose newest transcript passed is kept, not re-run: it is a pre-edit baseline, so a re-dispatch never runs it against the slice's own edits; changing its command or cwd retires the pass. Exit `3` on any failure or an invalid `Run at` value. |
|
|
18
18
|
| `check seal <slug>` | Check files required by target Phase `seal`, open human gates, the `tasks.md` slice graph, canonical `AC-###` presence when `tasks.md`/`test-plan.md` are required, the stable Build-input binding, and exact candidate bindings. `check readiness` and `check seal` also block a `scripts/` test driver that a tasks.md Writer allowlist plans or touched-files.md lists when no gates.md `CHECK` runs it and no `Driver waiver: <path> — DEC-### <reason>` line cites a recorded decision; waived drivers print as `note: unexecuted driver (waived by DEC-###): <path>` lines, on pass or block. |
|
|
19
19
|
| `check path-disjoint [--root <dir>] [<json-file> | -]` | Verify slice path sets are pairwise disjoint. |
|
|
20
|
-
| `parallel select --cap <1-10> [--root <dir>] [--slug <slug>] [--from-plan \| <json-file> \| -]` | Choose the greedy path-disjoint subset ≤ cap from supplied ready slices, or with `--from-plan` (needs `--slug`) from the plan: tasks.md slices whose dependencies state.md
|
|
20
|
+
| `parallel select --cap <1-10> [--root <dir>] [--slug <slug>] [--from-plan \| <json-file> \| -]` | Choose the greedy path-disjoint subset ≤ cap from supplied ready slices, or with `--from-plan` (needs `--slug`) from the plan: tasks.md slices whose dependencies are built (state.md checkbox, or an evidence.md heading that says that slice was checkpointed plus a `Checkpoint: <sha>` line), with Writer allowlist (else Files likely touched) paths plus the nearest manifest/lockfile when `External dependencies:` names a package (module path, npm scope, backticked name, or a non-toolchain version). With `--slug` and a running lease, the lease's not-yet-landed slices hold cap slots and paths, slices it names are never picked, and only new picks print. |
|
|
21
21
|
| `parallel <create\|add\|integrate\|cleanup\|status\|abort\|lease-read\|lease-write\|lease-clear\|record-green>` | Deterministic parallel worktree leases, integration, and cleanup for parallel writer batches. A lease is a pipeline: `integrate --slice <id> --apply-to-control` lands one green sibling onto control (refusing if anything but this lease moved control), and `add` starts new path-disjoint siblings from the current control tip while others still run. |
|
|
22
22
|
| `check task-graph <slug>` | Validate `tasks.md` slice dependency graph for cycles, unknown deps, malformed tokens, duplicate IDs, missing `Dependencies`, and `depends_on` mismatch. Prints a non-blocking `ADVISORY size:` line (and records a `size-advisory` metric) for each slice with 12+ Writer allowlist files and no `Size rationale:`. A `Dependencies` ID may carry a `(via <interface>)` suffix (empty or unclosed is a malformed token); prints a non-blocking `ADVISORY edge:` line (and records an `edge-advisory` metric) for each edge with no via, or whose via text is not a case-insensitive substring of the dependency's `Consumes / Produces:`. Prints `schedule: <n> slices in <r> build rounds at cap 10` (the `/rite-build` batch selection replayed over each slice's Writer allowlist, else Files likely touched, plus the manifest parallel select reserves; omitted when a slice has no path) and a non-blocking `ADVISORY hub:` line (top five; records a `hub-advisory` metric) for each path, compared case-folded, that three or more slices without `Serial rationale:` write. Advisories print only when the graph is valid. |
|
|
23
23
|
| `check regression <slug> [--update]` | Compare the workspace's structural progress facts (phase ordinal, checked ACs, met/abandoned gates, done slices, resolved questions, present artifacts) against the recorded `regression-baseline.json` high-water mark; `--update` ratchets the baseline forward under the feature lock and prints each blessed regression. |
|
|
@@ -37,8 +37,8 @@ workflow strategy.
|
|
|
37
37
|
| `next [slug]` | Print the minimal remaining lifecycle path plus advisory skips; in Build also `eligible_now:`, `batch:` and the `select-command:` (`parallel select --root <project> --slug <slug> --cap N --from-plan`, N = AFK max_parallel else 10) that reproduces the batch. Prints `blocked: unattributed root edit: ...` after `open-questions:` and exits `3` while the ADR-0038 tripwire flags a product path; the rest of the output still prints. |
|
|
38
38
|
| `handoff [slug] [--resume]` | Emit the deterministic resume record: cursor fields, `awaiting_human`, open `questions.md` gate kinds, the `gates.md` reduction with unmet/stale/abandoned ids, `decisions.md` dead ends, in-flight dispatch waves (`inflight_wave:` lines: phase, state, pending roles, slices), missing required files, the canonical read-next order, and the same `continuation:` lines as `check continuation`. Read-only unless `--resume`, which appends one `resume` event (role root) to `metrics.jsonl`; `metrics summary` then reports minutes from each resume to the next dispatch. The `/rite-handoff` spine and the compaction-fallback entry point. |
|
|
39
39
|
| `recall --paths <p1,p2,...> [--limit n]` | Read-time query over `.devrites/archive/`: list archived features whose `touched-files.md` candidate manifest covers a queried project-relative path (exact or directory prefix, case-folded), ranked by overlap count, then newest archive-directory mtime, then slug (default 10 features). Each feature prints up to 8 `archive/<slug>/<file>:<line>` pointers, then `+N more`: `decisions.md` `DEC-###` entries and `## Carry-forward constraints` bullets whose `Files:` overlap (or a constraint naming no files), plus `review.md`/`seal.md`/`drift.md` lines that mention a queried path; fenced examples are ignored. Never writes, caches, or indexes (no metric either); symlinked archive entries are refused. An unreadable archive file prints `unreadable:` and the scan continues. Exit `0` (including `recall: no archive`), `1` invalid archive directory, `2` usage. |
|
|
40
|
-
| `context [slug] (--phase <p>\|--skill <name>) [--role <r>] [--slice <SLICE-ID>] [--recheck <inventory>] [--trigger a,b] [--inline-contract] [--whole]` | Emit one deduplicated read-set bundle per phase/role/skill from each skill's `loads:` manifest; `--skill` works without a workspace when `--out` is given. A root `--phase build\|autocomplete` bundle (no `--role`, no `--slice`) defaults to `--slice <active_slice>` while the state.md cursor phase is `build` and active_slice names a slice present in tasks.md (prints `scope: slice SLICE-### (auto from state.md) ...`); `--whole` opts out. Every bundle opens, after its header comments, with a table of contents: one `L<start>-<end> <section label>` line per `===== label =====` section, giving its exact line range in the bundle file. A bundle without `--role` (the root's own read) gives over-budget artifacts as by-reference indexes; a `--role` bundle leads with the role's agent contract and names the manifest's `rootOnly` paths instead of inlining them. When the contract resolves under `.claude/agents`, `.omp/agents`, or `.codex/agents` (the host loads it as the agent's system prompt), the bundle writes one header line naming its path, size, and SHA-256 instead of the body; other layouts (pi, devin, unknown) and `--inline-contract` (only with `--role`) inline the body, and a missing contract still exits `3`. Inside inlined files, a `--role` bundle replaces each `<!-- root-only -->`…`<!-- /root-only -->` block with a one-line stub naming the omitted heading, and does the same for each `<!-- model-surface -->` block unless the role is `security-auditor`, the feature has an `ai-spec.md`, or `spec.md`/`plan.md`/`tasks.md`/`design-brief.md` are absent or (outside the spec template's `AI-SPEC annex`) name a model, agent, RAG, or tool surface; an unbalanced or headingless marker exits `2`. `--slice` inlines that slice's `tasks.md` block and turns each over-budget artifact, and each artifact over 16 KiB except `spec.md`, into a by-reference index (path, SHA-256, headings, line hits for every ID the block cites); repeat `--slice` for several slices; an unknown slice exits `3`. In phase `build`, a `--role` bundle other than `slice-wright` without `--slice` exits `3` (`unscoped build reviewer bundle`). `--recheck <inventory>` (phase `build`, with `--role` and `--slice`; path relative to the workspace) writes `<phase>-<role>.<slice>.recheck.bundle.md`: the inventory file and `core.md` inline whole, every other non-`rootOnly` pack file becomes a by-reference index (path, SHA-256, headings with line numbers) followed by each section the inventory cites as `<file>.md § <heading>`; workspace handling matches `--slice`. A missing or empty inventory exits `2`. First reviews, full-inventory rechecks, Vet, `/rite-review`, and Seal do not use it. |
|
|
41
|
-
| `dispatch <slug> <open\|start\|seal\|return\|status\|abandon\|attribute>` | Launch-wave barrier for parallel dispatch: seal needs a handle per role, return needs seal; the start that gives the last role its handle seals the wave, and an explicit seal on a sealed wave is a no-op; `open --claim-session <id> --claim <path>... [--claim-ttl <minutes>]` records one file claim for the wave in claims.jsonl and exits `3`, creating no wave and no claim, while a live foreign session holds an overlapping path; the return that completes the wave releases that claim; `abandon --release` releases it on any other host-confirmed terminal path (a stop or a refused launch); re-opening an abandoned wave inherits its kept claim or, with new claim flags, replaces and releases it; claim flags apply to open only and `--release` to abandon only; start/return auto-record metrics, `return --outcome findings|no-findings|gap` records the role's Outcome line, and `abandon` records its reason, so `metrics summary` shows per-role yield and lost waves. Bare `status` prints every open, sealed, or abandoned wave and folds complete waves into one `N complete wave(s) (use --all)` line; `--all` lists every wave; it still exits nonzero while any wave is open or sealed. For a reviewer, auditor, or test-analyst role, `return --result <file>` stores the return verbatim as `packets/<wave>-<role>.return.md` (exit `3` if that file exists), derives the outcome from its `Outcome:`/`Counts:` head (a malformed or inconsistent head records `gap` with a warning, never `no-findings`), exits `3` when an explicit `--outcome` disagrees, and records each `Finding:` row's `role|slice|file|section|severity` key, so `metrics summary` adds per-role `resolution: resolved re-raised open late` counts. A `--result` file that already is that `packets/` path is kept as the stored return. A build wave with a wright role needs `--slice <SLICE-ID>` per slice, and a slice-bound vet wave takes it too; a reviewer-only wave (for example a Build slice review) may take it, uncapped, so its `Finding:` keys name that slice; a reviewer, auditor, or test-analyst wave needs `--packet <file>` and `open` exits `3` on a packet that seeds a conclusion (gate `*-PASS` token, `no-findings`, an `Outcome:`/`Counts:` line, an "already passed" claim, or zero Critical/Important counts) or tells the agent to read an installed `agents/devrites-*.md`/`.toml` role contract (on the line or under a "Read …" heading or label; canonical `pack/` paths excepted), and a wright wave's `--contract` gets the same role-contract check; a Review or Seal reviewer wave also exits `3` unless the packet names an existing non-empty `.diff`/`.patch` file and the manifest covers every checkpointed path; every 5 rounds of either kind on one slice, `open` exits `3` until `--reslice-review <DRIFT-ID>` names an unused `drift.md` `## <DRIFT-ID>` heading for that slice. `open` also warns when an earlier wave for the same slice never returned. Under `.devrites/AFK` max_parallel >= 2, a
|
|
40
|
+
| `context [slug] (--phase <p>\|--skill <name>) [--role <r>] [--slice <SLICE-ID>] [--recheck <inventory>] [--trigger a,b] [--inline-contract] [--whole]` | Emit one deduplicated read-set bundle per phase/role/skill from each skill's `loads:` manifest; `--skill` works without a workspace when `--out` is given. A root `--phase build\|autocomplete` bundle (no `--role`, no `--slice`) defaults to `--slice <active_slice>` while the state.md cursor phase is `build` and active_slice names a slice present in tasks.md (prints `scope: slice SLICE-### (auto from state.md) ...`), unless `.devrites/AFK` `max_parallel` is 2 or more or a parallel lease is `running` (the bundle stays whole); `--whole` opts out. Every bundle opens, after its header comments, with a table of contents: one `L<start>-<end> <section label>` line per `===== label =====` section, giving its exact line range in the bundle file. A bundle without `--role` (the root's own read) gives over-budget artifacts as by-reference indexes; a `--role` bundle leads with the role's agent contract and names the manifest's `rootOnly` paths instead of inlining them. When the contract resolves under `.claude/agents`, `.omp/agents`, or `.codex/agents` (the host loads it as the agent's system prompt), the bundle writes one header line naming its path, size, and SHA-256 instead of the body; other layouts (pi, devin, unknown) and `--inline-contract` (only with `--role`) inline the body, and a missing contract still exits `3`. Inside inlined files, a `--role` bundle replaces each `<!-- root-only -->`…`<!-- /root-only -->` block with a one-line stub naming the omitted heading, and does the same for each `<!-- model-surface -->` block unless the role is `security-auditor`, the feature has an `ai-spec.md`, or `spec.md`/`plan.md`/`tasks.md`/`design-brief.md` are absent or (outside the spec template's `AI-SPEC annex`) name a model, agent, RAG, or tool surface; an unbalanced or headingless marker exits `2`. `--slice` inlines that slice's `tasks.md` block and turns each over-budget artifact, and each artifact over 16 KiB except `spec.md`, into a by-reference index (path, SHA-256, headings, line hits for every ID the block cites); repeat `--slice` for several slices; an unknown slice exits `3`. In phase `build`, a `--role` bundle other than `slice-wright` without `--slice` exits `3` (`unscoped build reviewer bundle`). `--recheck <inventory>` (phase `build`, with `--role` and `--slice`; path relative to the workspace) writes `<phase>-<role>.<slice>.recheck.bundle.md`: the inventory file and `core.md` inline whole, every other non-`rootOnly` pack file becomes a by-reference index (path, SHA-256, headings with line numbers) followed by each section the inventory cites as `<file>.md § <heading>`; workspace handling matches `--slice`. A missing or empty inventory exits `2`. First reviews, full-inventory rechecks, Vet, `/rite-review`, and Seal do not use it. |
|
|
41
|
+
| `dispatch <slug> <open\|start\|seal\|return\|status\|abandon\|attribute>` | Launch-wave barrier for parallel dispatch: seal needs a handle per role, return needs seal; the start that gives the last role its handle seals the wave, and an explicit seal on a sealed wave is a no-op; `open --claim-session <id> --claim <path>... [--claim-ttl <minutes>]` records one file claim for the wave in claims.jsonl and exits `3`, creating no wave and no claim, while a live foreign session holds an overlapping path; the return that completes the wave releases that claim; `abandon --release` releases it on any other host-confirmed terminal path (a stop or a refused launch); re-opening an abandoned wave inherits its kept claim or, with new claim flags, replaces and releases it; claim flags apply to open only and `--release` to abandon only; start/return auto-record metrics, `return --outcome findings|no-findings|gap` records the role's Outcome line, and `abandon` records its reason, so `metrics summary` shows per-role yield and lost waves. Bare `status` prints every open, sealed, or abandoned wave and folds complete waves into one `N complete wave(s) (use --all)` line; `--all` lists every wave; it still exits nonzero while any wave is open or sealed. For a reviewer, auditor, or test-analyst role, `return --result <file>` stores the return verbatim as `packets/<wave>-<role>.return.md` (exit `3` if that file exists), derives the outcome from its `Outcome:`/`Counts:` head (a malformed or inconsistent head records `gap` with a warning, never `no-findings`), exits `3` when an explicit `--outcome` disagrees, and records each `Finding:` row's `role|slice|file|section|severity` key, so `metrics summary` adds per-role `resolution: resolved re-raised open late` counts. A `--result` file that already is that `packets/` path is kept as the stored return. A build wave with a wright role needs `--slice <SLICE-ID>` per slice, and a slice-bound vet wave takes it too; a reviewer-only wave (for example a Build slice review) may take it, uncapped, so its `Finding:` keys name that slice; a reviewer, auditor, or test-analyst wave needs `--packet <file>` and `open` exits `3` on a packet that seeds a conclusion (gate `*-PASS` token, `no-findings`, an `Outcome:`/`Counts:` line, an "already passed" claim, or zero Critical/Important counts) or tells the agent to read an installed `agents/devrites-*.md`/`.toml` role contract (on the line or under a "Read …" heading or label; canonical `pack/` paths excepted), and a wright wave's `--contract` gets the same role-contract check; a Review or Seal reviewer wave also exits `3` unless the packet names an existing non-empty `.diff`/`.patch` file and the manifest covers every checkpointed path; every 5 rounds of either kind on one slice, `open` exits `3` until `--reslice-review <DRIFT-ID>` names an unused `drift.md` `## <DRIFT-ID>` heading for that slice. `open` also warns when an earlier wave for the same slice never returned. Under `.devrites/AFK` max_parallel >= 2, a build wright wave that leaves `parallel select --from-plan` picks unstarted exits `3` (printing the batch, the command and the parallel-batch lifecycle) until evidence.md records `serial: <skipped ids> — <reason>`; a repair round or a running lease slice does not waive those picks, and a reason that only says the skipped slice is repairing, already started, or in a wave does not count. A wave naming two or more slices needs one `--role slice-wright.<SLICE-ID>` per slice; one shared role exits `3`. HITL runs are never width-refused. A build wright `--contract` must name `gates precheck` under `Writer-safe checks:` when gates.md has a `RUN: writer` gate. `open` exits `3` when a `--role` names no pack agent in a project-level install (a `.<lane>` suffix is allowed), and lints a reviewer packet against the wave's roles, so a recheck may name its own role's fingerprint. Root-edit tripwire (ADR-0038): the first `open` records `<feature>/tree-baseline.json`, which maps dirty product paths outside `.devrites/` and `.scratch/` to their mode plus SHA-256, accepted as user work. The return that completes a wright wave (any phase) adds its slice write sets and claim paths. `open` exits `3` with `unattributed root edit: <paths>` while a dirty product path differs from that baseline and no live claim or in-flight wright wave covers it, unless the new wright wave covers the path through `--claim` or a slice write set. `attribute --reason <evidence> --path <path>...` records owner work, gate output, a standalone wright, or a sanctioned root write for flagged paths only. Each finding writes an `unattributed-edit` metrics row. The engine never reverts a file, and a workspace without the baseline is never blocked. |
|
|
42
42
|
| `candidate freeze <slug> --wave <w> (--slice <SLICE-ID> \| --paths <path>...) [--base <ref>] [--worktree <path>]` | Writes packets/<w>.diff (tracked changes vs base, default HEAD, plus untracked files as /dev/null diffs) and packets/<w>.sha256 (base, diff digest, per-file sha256, aggregate check-candidate digest); never replaces a wave's frozen candidate with a different one. `--worktree <path>` (requires `--base`) reads the same path set from a parallel sibling's linked worktree before it lands: the path must be a symlink-free worktree top level that `git worktree list` names, shares the control repository, and is neither a submodule nor the control checkout. It exits `3` on uncommitted path-set changes there and adds a `head <sha>` row naming the worktree HEAD (the transfer commit); packets stay in the control workspace. |
|
|
43
43
|
| `packet <slug> --wave <w> --phase <p> --role <r> [--slice <SLICE-ID>] [--recheck] [--checks <G-ID,...>] [--focus <file>]` | Renders one reviewer/auditor/test-analyst or same-worktree slice-wright by-reference packet (packets/<w>.<role>.md, or packets/<w>.contract.md for a wright) naming the context bundle (the .recheck bundle with `--recheck`) and frozen diff. It copies packets/<w>.sha256 (header rows only when large), lists a wright slice's tasks.md `Tests/proof` as its first writer-safe check, resolves `--checks` through gates.md and approved preflight rows (adding `gates precheck` for `RUN: writer` gates), and accepts only anchored `--focus` rows (a doubt-reviewer's 1-3 row claim is required). It runs the dispatch open lints on a draft and never replaces a prior packet with a refused one (exit `3`). |
|
|
44
44
|
| `metrics record <slug> --phase <p> --event <e> [--role r] [--bytes n] [--note s]` | Append an event to the workspace's `metrics.jsonl` ledger. |
|
|
@@ -58,6 +58,13 @@ workflow strategy.
|
|
|
58
58
|
| `overhaul <records\|admit\|snapshot\|score\|bench\|render> ...` | Deterministic tools behind the standalone `/overhaul` skill: run-record generations and cross-record validation, worker-receipt admission and quote anchoring, index-safe baseline snapshots and per-attempt deltas, exact-rational readiness scoring, benchmark ratio intervals, and escaped offline review/report views. Operates only on explicit run-area and repository paths. See [Overhaul tools](#overhaul-tools). |
|
|
59
59
|
| `version` | Print the engine version. |
|
|
60
60
|
|
|
61
|
+
For `context --role`, optional `devrites-` prefixes and `.lane` suffixes resolve
|
|
62
|
+
the base specialist's contract, workspace read-set, and role-specific rules.
|
|
63
|
+
The full label remains in bundle filenames and attribution. Reviewer
|
|
64
|
+
`dispatch return` requires `--result <file>` or an explicit
|
|
65
|
+
`--outcome findings|no-findings|gap`; omitting both refuses the return without
|
|
66
|
+
changing the wave or releasing its claim. Non-reviewer returns are unchanged.
|
|
67
|
+
|
|
61
68
|
`help`, `-h`, and `--help` print this operational inventory. Each operational
|
|
62
69
|
command and subcommand also accepts `-h` / `--help` and prints that command's
|
|
63
70
|
usage to stdout without requiring a workspace. `version` and `--version` print
|
|
@@ -66,6 +73,21 @@ engine has no compatibility aliases or tombstones. The `add`/`upgrade`/`remove`
|
|
|
66
73
|
conveniences belong only to the `npx devrites` adapter, not to the engine
|
|
67
74
|
command namespace.
|
|
68
75
|
|
|
76
|
+
## State and evidence safety
|
|
77
|
+
|
|
78
|
+
- `state set` resolves known cursor aliases before checking protected fields
|
|
79
|
+
and duplicate keys. Underscore variants cannot bypass phase, schema, or
|
|
80
|
+
saved-return-cursor protections; extension keys retain their spelling.
|
|
81
|
+
- `state resolve` ignores fenced examples when matching and removing an
|
|
82
|
+
Awaiting-human section. Resolving another question leaves the live wait intact.
|
|
83
|
+
- `state close` reads the physical `ACTIVE` pointer under the feature lock;
|
|
84
|
+
`DEVRITES_WORKSPACE` cannot make it clear another feature's pointer.
|
|
85
|
+
- `gates run` and `reverify` snapshot every declared `INPUTS` dependency around
|
|
86
|
+
the actual shared CHECK execution, recheck during locked writeback, and
|
|
87
|
+
invalidate earlier passes if later checks move their inputs.
|
|
88
|
+
Missing or changed inputs cannot certify a pass. Execution, discarded-result,
|
|
89
|
+
or evidence-write failures exit `3`, even if an older ledger still reads met.
|
|
90
|
+
|
|
69
91
|
## Check boundary
|
|
70
92
|
|
|
71
93
|
The candidate gate validates and hashes path/state/type/mode/content identity;
|
package/docs/orchestration.md
CHANGED
|
@@ -37,6 +37,15 @@ Skills name the exact role and bounded task in normal language. The host owns
|
|
|
37
37
|
internal spawn fields, scheduling, waiting, follow-ups, and result delivery.
|
|
38
38
|
DevRites does not parse rollout logs or maintain dispatch receipts.
|
|
39
39
|
|
|
40
|
+
On OMP, each `task` item selects its specialist through `agent`, including every
|
|
41
|
+
item in a `tasks[]` batch: `devrites-code-reviewer`, `devrites-test-analyst`, or
|
|
42
|
+
`devrites-slice-wright` for those roles. Writing “you are…” in a task or naming it
|
|
43
|
+
after a specialist does not select that profile. Lane suffixes stay on engine
|
|
44
|
+
role labels, not native profile names. Check the actual host selector before
|
|
45
|
+
admitting results: the engine records declared roles and host handles, not
|
|
46
|
+
authenticated native profile identity. A missing profile remains a gap, not
|
|
47
|
+
permission to select a generic/default child.
|
|
48
|
+
|
|
40
49
|
Quick, Standard, and Full select evidence/review depth only; they are not agent
|
|
41
50
|
API versions or execution tiers. The shared contract lives in
|
|
42
51
|
[`orchestration-profiles.md`](../pack/.claude/skills/devrites-lib/reference/orchestration-profiles.md).
|
|
@@ -160,12 +160,12 @@ func cmdStatus(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
160
160
|
fmt.Fprintf(stderr, "gates status: %v\n", err)
|
|
161
161
|
return ExitBlocked
|
|
162
162
|
}
|
|
163
|
-
return reportStatus(slug, ledgerPath, testPlanPath, stdout, stderr)
|
|
163
|
+
return reportStatus(slug, ledgerPath, testPlanPath, false, stdout, stderr)
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// reportStatus prints the stable greppable reduction. It returns ExitOK only
|
|
167
|
-
// for a parse-clean ledger with every gate met and
|
|
168
|
-
func reportStatus(slug, ledgerPath, testPlanPath string, stdout, stderr io.Writer) int {
|
|
167
|
+
// for a parse-clean ledger with every gate met, none abandoned and no failed attempt.
|
|
168
|
+
func reportStatus(slug, ledgerPath, testPlanPath string, attemptFailed bool, stdout, stderr io.Writer) int {
|
|
169
169
|
text, err := ReadLedgerFile(ledgerPath)
|
|
170
170
|
if err != nil {
|
|
171
171
|
fmt.Fprintf(stderr, "gates status: %v\n", err)
|
|
@@ -191,7 +191,7 @@ func reportStatus(slug, ledgerPath, testPlanPath string, stdout, stderr io.Write
|
|
|
191
191
|
case reduction.HandoffRequired():
|
|
192
192
|
fmt.Fprintln(stdout, "result: handoff")
|
|
193
193
|
return ExitBlocked
|
|
194
|
-
case !reduction.AllMet():
|
|
194
|
+
case attemptFailed || !reduction.AllMet():
|
|
195
195
|
fmt.Fprintln(stdout, "result: not-met")
|
|
196
196
|
return ExitBlocked
|
|
197
197
|
default:
|
|
@@ -269,6 +269,7 @@ func cmdRun(root string, args []string, reverify bool, stdout, stderr io.Writer)
|
|
|
269
269
|
}
|
|
270
270
|
var ran []GateResult
|
|
271
271
|
var ranDoc *Document
|
|
272
|
+
attemptFailed := false
|
|
272
273
|
|
|
273
274
|
// The gates lock serializes check execution; the feature lock is held only
|
|
274
275
|
// to read inputs and per writeback, so dispatch and other workspace writers
|
|
@@ -294,6 +295,9 @@ func cmdRun(root string, args []string, reverify bool, stdout, stderr io.Writer)
|
|
|
294
295
|
}
|
|
295
296
|
ran, ranDoc = RunGates(context.Background(), project, ledgerPath, doc, approved, reverify, timeout, featureLock), doc
|
|
296
297
|
for _, result := range ran {
|
|
298
|
+
if result.Ran && !result.Passed || result.Discarded || result.WriteErr != nil {
|
|
299
|
+
attemptFailed = true
|
|
300
|
+
}
|
|
297
301
|
switch {
|
|
298
302
|
case !result.Ran:
|
|
299
303
|
fmt.Fprintf(stdout, "gate %s: skipped (%s)\n", result.ID, result.Detail)
|
|
@@ -320,7 +324,7 @@ func cmdRun(root string, args []string, reverify bool, stdout, stderr io.Writer)
|
|
|
320
324
|
}
|
|
321
325
|
}
|
|
322
326
|
fmt.Fprintln(stdout, "---")
|
|
323
|
-
return reportStatus(slug, ledgerPath, testPlanPath, stdout, stderr)
|
|
327
|
+
return reportStatus(slug, ledgerPath, testPlanPath, attemptFailed, stdout, stderr)
|
|
324
328
|
}
|
|
325
329
|
|
|
326
330
|
func cmdLint(root string, args []string, stdout, stderr io.Writer) int {
|
|
@@ -28,7 +28,7 @@ type GateResult struct {
|
|
|
28
28
|
Ran bool
|
|
29
29
|
Passed bool
|
|
30
30
|
Detail string // exit/match/timeout/approval diagnostic
|
|
31
|
-
Discarded bool // result dropped:
|
|
31
|
+
Discarded bool // result dropped: bound oracle or inputs changed during the run
|
|
32
32
|
WriteErr error // evidence writeback failed for a reason other than drift
|
|
33
33
|
Reused bool // met gate kept because its INPUTS are unchanged
|
|
34
34
|
Inputs string // INPUTS content digest at run time, when declared
|
|
@@ -77,13 +77,24 @@ func ReadLedgerFile(ledgerPath string) (string, error) {
|
|
|
77
77
|
// execute; writeResult re-verifies the gate's oracle before it writes.
|
|
78
78
|
func RunGates(ctx context.Context, root, ledgerPath string, doc *Document, approved []ApprovedCommand, reverify bool, timeout time.Duration, writeLock func(func() error) error) []GateResult {
|
|
79
79
|
var results []GateResult
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
executions := map[string]struct {
|
|
81
|
+
ran bool
|
|
82
|
+
outcome RunOutcome
|
|
83
|
+
span [2]time.Time
|
|
84
|
+
inputs map[string]string
|
|
85
|
+
}{}
|
|
86
|
+
// Collect every dependent INPUTS spec before any shared command can run.
|
|
87
|
+
for _, gate := range doc.Gates {
|
|
88
|
+
if gate.Check == "" || gate.Inputs == "" {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
key := gate.Check + "\x00" + filepath.Join(root, filepath.FromSlash(gate.Cwd))
|
|
92
|
+
execution := executions[key]
|
|
93
|
+
if execution.inputs == nil {
|
|
94
|
+
execution.inputs = map[string]string{}
|
|
85
95
|
}
|
|
86
|
-
|
|
96
|
+
execution.inputs[gate.Inputs] = ""
|
|
97
|
+
executions[key] = execution
|
|
87
98
|
}
|
|
88
99
|
for _, gate := range doc.Gates {
|
|
89
100
|
if _, abandoned := doc.Abandoned[gate.ID]; abandoned {
|
|
@@ -95,15 +106,15 @@ func RunGates(ctx context.Context, root, ledgerPath string, doc *Document, appro
|
|
|
95
106
|
continue
|
|
96
107
|
}
|
|
97
108
|
inputs := ""
|
|
98
|
-
if gate.Inputs != "" {
|
|
99
|
-
// An unreadable or unmatched spec yields "", which never matches, so the gate re-executes.
|
|
100
|
-
inputs, _ = InputsDigest(root, gate.Inputs)
|
|
101
|
-
}
|
|
102
109
|
if GateState(gate, doc) == StateMet {
|
|
103
110
|
if gate.Inputs == "" && !reverify {
|
|
104
111
|
results = append(results, GateResult{ID: gate.ID, Passed: true, Detail: "already met"})
|
|
105
112
|
continue
|
|
106
113
|
}
|
|
114
|
+
if gate.Inputs != "" {
|
|
115
|
+
// An unreadable or unmatched spec never matches recorded evidence.
|
|
116
|
+
inputs, _ = InputsDigest(root, gate.Inputs)
|
|
117
|
+
}
|
|
107
118
|
// A gate that declares INPUTS is reused, under run and reverify alike,
|
|
108
119
|
// exactly while the files it depends on are byte-identical; any change re-executes it.
|
|
109
120
|
if inputs != "" && inputs == recordedInputs(gate) {
|
|
@@ -120,36 +131,84 @@ func RunGates(ctx context.Context, root, ledgerPath string, doc *Document, appro
|
|
|
120
131
|
results = append(results, GateResult{ID: gate.ID, Detail: err.Error()})
|
|
121
132
|
continue
|
|
122
133
|
}
|
|
123
|
-
cwd := root
|
|
124
|
-
if gate.Cwd != "" {
|
|
125
|
-
cwd = filepath.Join(root, filepath.FromSlash(gate.Cwd))
|
|
126
|
-
}
|
|
134
|
+
cwd := filepath.Join(root, filepath.FromSlash(gate.Cwd))
|
|
127
135
|
key := gate.Check + "\x00" + cwd
|
|
128
|
-
|
|
129
|
-
if !ran {
|
|
136
|
+
execution := executions[key]
|
|
137
|
+
if !execution.ran {
|
|
138
|
+
for spec := range execution.inputs {
|
|
139
|
+
execution.inputs[spec], _ = InputsDigest(root, spec)
|
|
140
|
+
}
|
|
130
141
|
started := time.Now().UTC()
|
|
131
|
-
outcome = Execute(ctx, gate.Check, cwd, timeout)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
execution.outcome = Execute(ctx, gate.Check, cwd, timeout)
|
|
143
|
+
execution.span = [2]time.Time{started, time.Now().UTC()}
|
|
144
|
+
for spec, before := range execution.inputs {
|
|
145
|
+
after, _ := InputsDigest(root, spec)
|
|
146
|
+
if after != before {
|
|
147
|
+
execution.inputs[spec] = ""
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
execution.ran = true
|
|
151
|
+
executions[key] = execution
|
|
152
|
+
}
|
|
153
|
+
outcome := execution.outcome
|
|
154
|
+
inputs = execution.inputs[gate.Inputs]
|
|
155
|
+
passed := outcome.Err == nil && outcome.ExitCode == 0 && expectation.Matches(outcome.Output)
|
|
156
|
+
detail := "pass"
|
|
157
|
+
if !passed {
|
|
158
|
+
detail = failDetail(outcome, expectation)
|
|
159
|
+
}
|
|
160
|
+
write := func() error {
|
|
161
|
+
if passed && gate.Inputs != "" {
|
|
162
|
+
// Earlier writebacks may have moved files since execution.
|
|
163
|
+
current, _ := InputsDigest(root, gate.Inputs)
|
|
164
|
+
if inputs == "" || current != inputs {
|
|
165
|
+
passed = false
|
|
166
|
+
detail = "INPUTS changed or could not be read during the run"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
evidence := "pending"
|
|
170
|
+
if passed {
|
|
171
|
+
evidence = withInputs(FormatEvidence(gate, OutputDigest(outcome.Output), outcome.OutputBytes, time.Now().UTC().Format(time.RFC3339)), inputs)
|
|
172
|
+
}
|
|
173
|
+
return writeResult(ledgerPath, gate, evidence, passed)
|
|
174
|
+
}
|
|
175
|
+
var writeErr error
|
|
176
|
+
if writeLock == nil {
|
|
177
|
+
writeErr = write()
|
|
178
|
+
} else {
|
|
179
|
+
writeErr = writeLock(write)
|
|
180
|
+
}
|
|
181
|
+
results = append(results, GateResult{
|
|
182
|
+
ID: gate.ID, Ran: true, Passed: passed,
|
|
183
|
+
Discarded: errors.Is(writeErr, ErrOracleMoved), WriteErr: writeErr,
|
|
184
|
+
Detail: resultDetail(writeErr, detail),
|
|
185
|
+
Inputs: inputs, Outcome: outcome, Started: execution.span[0], Ended: execution.span[1],
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
// Later checks may invalidate an earlier execution or reused pass.
|
|
189
|
+
for i, gate := range doc.Gates {
|
|
190
|
+
result := &results[i]
|
|
191
|
+
if !result.Passed || result.Discarded || result.WriteErr != nil || gate.Inputs == "" {
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
validate := func() error {
|
|
195
|
+
current, _ := InputsDigest(root, gate.Inputs)
|
|
196
|
+
if current != "" && current == result.Inputs {
|
|
197
|
+
return nil
|
|
198
|
+
}
|
|
199
|
+
result.Passed, result.Reused, result.Discarded = false, false, true
|
|
200
|
+
result.Detail = "INPUTS changed or could not be read after a later check"
|
|
201
|
+
return writeResult(ledgerPath, gate, "pending", false)
|
|
202
|
+
}
|
|
203
|
+
var err error
|
|
204
|
+
if writeLock == nil {
|
|
205
|
+
err = validate()
|
|
145
206
|
} else {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Inputs: inputs, Outcome: outcome, Started: span[0], Ended: span[1],
|
|
152
|
-
})
|
|
207
|
+
err = writeLock(validate)
|
|
208
|
+
}
|
|
209
|
+
if err != nil {
|
|
210
|
+
result.WriteErr = err
|
|
211
|
+
result.Detail = resultDetail(err, result.Detail)
|
|
153
212
|
}
|
|
154
213
|
}
|
|
155
214
|
return results
|
|
@@ -194,14 +194,19 @@ func checkedCanonicalDir(root, candidate, parent, slug string) (string, error) {
|
|
|
194
194
|
return filepath.Clean(candidate), nil
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// ActiveSlug
|
|
198
|
-
// is not an error; it yields the empty slug.
|
|
197
|
+
// ActiveSlug resolves the workspace override, or reads the active-feature pointer.
|
|
198
|
+
// A missing pointer file is not an error; it yields the empty slug.
|
|
199
199
|
func ActiveSlug(root string) (string, error) {
|
|
200
200
|
if ws, err := WorkspaceOverrideChecked(root, ""); err != nil {
|
|
201
201
|
return "", err
|
|
202
202
|
} else if ws != "" {
|
|
203
203
|
return filepath.Base(ws), nil
|
|
204
204
|
}
|
|
205
|
+
return PhysicalActiveSlug(root)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// PhysicalActiveSlug reads the validated ACTIVE pointer without workspace overrides.
|
|
209
|
+
func PhysicalActiveSlug(root string) (string, error) {
|
|
205
210
|
activePath := filepath.Join(root, "ACTIVE")
|
|
206
211
|
if info, err := os.Lstat(activePath); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
|
207
212
|
return "", fmt.Errorf("DRV-ACTIVE-SYMLINK: ACTIVE is a symlink; run `rm -f %q`", activePath)
|
|
@@ -231,6 +231,11 @@ func (r *runner) writeConfined(dest string, data []byte, perm fs.FileMode) error
|
|
|
231
231
|
return err
|
|
232
232
|
}
|
|
233
233
|
defer func() { _ = root.Close() }()
|
|
234
|
+
if info, err := root.Stat(rel); err == nil {
|
|
235
|
+
perm = info.Mode().Perm()
|
|
236
|
+
} else if !os.IsNotExist(err) {
|
|
237
|
+
return err
|
|
238
|
+
}
|
|
234
239
|
return fsutil.WriteFileAtomicIn(root, rel, data, perm)
|
|
235
240
|
}
|
|
236
241
|
|
|
@@ -17,11 +17,18 @@ import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
func (r *runner) installClaudeSettingsMarker(state claudeSettingsMerge) error {
|
|
20
|
+
return r.installMarker(hostpack.ClaudeSettingsMerge.MarkerRel, claudeSettingsMarkerText(state))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func claudeSettingsMarkerText(state claudeSettingsMerge) string {
|
|
20
24
|
var b strings.Builder
|
|
21
25
|
b.WriteString(hostpack.ClaudeSettingsMerge.MarkerText + "\n" + claudeRulesAddedLine)
|
|
22
26
|
if state.created {
|
|
23
27
|
b.WriteString("\n" + claudeSettingsCreatedLine)
|
|
24
28
|
}
|
|
29
|
+
if state.defaultModeOwned {
|
|
30
|
+
b.WriteString("\ndefault-mode=added")
|
|
31
|
+
}
|
|
25
32
|
rules := make([]string, 0, len(state.added))
|
|
26
33
|
for rule := range state.added {
|
|
27
34
|
rules = append(rules, rule)
|
|
@@ -30,7 +37,7 @@ func (r *runner) installClaudeSettingsMarker(state claudeSettingsMerge) error {
|
|
|
30
37
|
for _, rule := range rules {
|
|
31
38
|
b.WriteString("\nrule:" + rule)
|
|
32
39
|
}
|
|
33
|
-
return
|
|
40
|
+
return b.String()
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
const (
|
|
@@ -364,16 +371,45 @@ func (r *runner) mergeClaudeSettings(merge hostpack.JSONMerge) error {
|
|
|
364
371
|
return fmt.Errorf("encode merged Claude settings: %w", err)
|
|
365
372
|
}
|
|
366
373
|
data = append(data, '\n')
|
|
374
|
+
// Publish ownership before grants; keep retired ownership until settings land.
|
|
375
|
+
pending := r.pendingClaudeSettingsOwnership(state)
|
|
376
|
+
if err := r.installClaudeSettingsMarker(pending); err != nil {
|
|
377
|
+
return err
|
|
378
|
+
}
|
|
379
|
+
if r.records[merge.MarkerRel] == "" {
|
|
380
|
+
return fmt.Errorf("cannot merge %s without owning %s; use --force to replace the marker", merge.TargetRel, merge.MarkerRel)
|
|
381
|
+
}
|
|
382
|
+
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
383
|
+
return err
|
|
384
|
+
}
|
|
367
385
|
if err := r.writeConfined(dest, data, 0o644); err != nil {
|
|
368
386
|
return fmt.Errorf("cannot write %s: %w", merge.TargetRel, err)
|
|
369
387
|
}
|
|
370
|
-
|
|
388
|
+
if pending.defaultModeOwned || len(pending.added) != len(state.added) {
|
|
389
|
+
if err := r.refreshPreflight(merge.MarkerRel); err != nil {
|
|
390
|
+
return err
|
|
391
|
+
}
|
|
392
|
+
return r.installClaudeSettingsMarker(state)
|
|
393
|
+
}
|
|
394
|
+
return nil
|
|
371
395
|
}
|
|
372
396
|
|
|
373
397
|
// claudeSettingsMerge is what the settings marker records about a merge.
|
|
374
398
|
type claudeSettingsMerge struct {
|
|
375
|
-
created
|
|
376
|
-
added
|
|
399
|
+
created bool
|
|
400
|
+
added map[string]bool
|
|
401
|
+
defaultModeOwned bool
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
func (r *runner) pendingClaudeSettingsOwnership(next claudeSettingsMerge) claudeSettingsMerge {
|
|
405
|
+
pending := claudeSettingsMerge{
|
|
406
|
+
created: next.created, added: r.managedClaudePermissionRules(),
|
|
407
|
+
defaultModeOwned: r.claudeDefaultModeOwned(),
|
|
408
|
+
}
|
|
409
|
+
for rule := range next.added {
|
|
410
|
+
pending.added[rule] = true
|
|
411
|
+
}
|
|
412
|
+
return pending
|
|
377
413
|
}
|
|
378
414
|
|
|
379
415
|
// claudeSettingsNext returns the merged .claude/settings.json without
|
|
@@ -433,6 +469,12 @@ func stripHooksWith(path string, write func([]byte) error, remove func() error)
|
|
|
433
469
|
}
|
|
434
470
|
|
|
435
471
|
func (r *runner) stripClaudeSettings(path string) error {
|
|
472
|
+
return r.stripClaudeSettingsWith(path, func(data []byte) error {
|
|
473
|
+
return r.writeConfined(path, data, 0o644)
|
|
474
|
+
}, func() error { return r.removeConfined(path) })
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
func (r *runner) stripClaudeSettingsWith(path string, write func([]byte) error, remove func() error) error {
|
|
436
478
|
current, err := readJSON(path)
|
|
437
479
|
if err != nil {
|
|
438
480
|
if errors.Is(err, fs.ErrNotExist) {
|
|
@@ -442,14 +484,14 @@ func (r *runner) stripClaudeSettings(path string) error {
|
|
|
442
484
|
}
|
|
443
485
|
next := stripDevritesSettings(current, r.claudeDefaultModeOwned(), r.managedClaudePermissionRules())
|
|
444
486
|
if len(next) == 0 && r.claudeSettingsCreated() {
|
|
445
|
-
return
|
|
487
|
+
return remove()
|
|
446
488
|
}
|
|
447
489
|
data, err := json.MarshalIndent(next, "", " ")
|
|
448
490
|
if err != nil {
|
|
449
491
|
return fmt.Errorf("encode Claude settings: %w", err)
|
|
450
492
|
}
|
|
451
493
|
data = append(data, '\n')
|
|
452
|
-
return
|
|
494
|
+
return write(data)
|
|
453
495
|
}
|
|
454
496
|
|
|
455
497
|
func (r *runner) stripMarkerConfined(path, begin, end string) error {
|
|
@@ -6,6 +6,7 @@ import (
|
|
|
6
6
|
"fmt"
|
|
7
7
|
"io"
|
|
8
8
|
"io/fs"
|
|
9
|
+
"maps"
|
|
9
10
|
"os"
|
|
10
11
|
"path/filepath"
|
|
11
12
|
"slices"
|
|
@@ -22,6 +23,7 @@ func (r *runner) preflightInstall() error {
|
|
|
22
23
|
return fmt.Errorf("preflight install paths: %w", err)
|
|
23
24
|
}
|
|
24
25
|
var conflicts []string
|
|
26
|
+
dryTree := map[string][]byte{}
|
|
25
27
|
for rel := range desired {
|
|
26
28
|
snapshot, err := r.rememberPath(rel)
|
|
27
29
|
if err != nil {
|
|
@@ -34,7 +36,8 @@ func (r *runner) preflightInstall() error {
|
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
|
-
for
|
|
39
|
+
for _, rel := range slices.Sorted(maps.Keys(r.prev)) {
|
|
40
|
+
record := r.prev[rel]
|
|
38
41
|
if desired[rel] || hostpack.PreserveOnPrune(rel) {
|
|
39
42
|
continue
|
|
40
43
|
}
|
|
@@ -58,7 +61,7 @@ func (r *runner) preflightInstall() error {
|
|
|
58
61
|
if _, err := r.rememberPath(merge.TargetRel); err != nil {
|
|
59
62
|
return err
|
|
60
63
|
}
|
|
61
|
-
if err := dryRunStripMarker(
|
|
64
|
+
if err := r.dryRunStripMarker(dryTree, merge); err != nil {
|
|
62
65
|
return err
|
|
63
66
|
}
|
|
64
67
|
}
|
|
@@ -104,19 +107,34 @@ func (r *runner) preflightMerges() error {
|
|
|
104
107
|
return fmt.Errorf("merge %s: %w", hostpack.CodexConfigMerge.TargetRel, err)
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
|
-
|
|
110
|
+
_, settings, err := r.claudeSettingsNext()
|
|
111
|
+
if err != nil {
|
|
108
112
|
return fmt.Errorf("seed claude settings: %w", err)
|
|
109
113
|
}
|
|
114
|
+
marker := hostpack.ClaudeSettingsMerge.MarkerRel
|
|
115
|
+
if snapshot := r.preflight[marker]; !snapshot.missing && !r.opts.Force {
|
|
116
|
+
if _, managed := r.prev[marker]; !managed {
|
|
117
|
+
text := claudeSettingsMarkerText(r.pendingClaudeSettingsOwnership(settings)) + "\n"
|
|
118
|
+
sum := sha256.Sum256([]byte(text))
|
|
119
|
+
if snapshot.hash != fmt.Sprintf("sha256:%x", sum[:]) {
|
|
120
|
+
return fmt.Errorf("cannot merge %s without owning %s; use --force to replace the marker", hostpack.ClaudeSettingsMerge.TargetRel, marker)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
110
124
|
return nil
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
func (r *runner) preflightUninstall(entries []string) error {
|
|
114
128
|
var conflicts []string
|
|
129
|
+
dryTree := map[string][]byte{}
|
|
115
130
|
for _, rel := range entries {
|
|
116
131
|
if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
|
|
117
132
|
if _, err := r.rememberPath(merge.TargetRel); err != nil {
|
|
118
133
|
return err
|
|
119
134
|
}
|
|
135
|
+
if err := r.dryRunStripMarker(dryTree, merge); err != nil {
|
|
136
|
+
return err
|
|
137
|
+
}
|
|
120
138
|
}
|
|
121
139
|
if !hostpack.ShouldRemoveOnUninstall(rel, entries) {
|
|
122
140
|
continue
|