devrites 7.0.2 → 7.0.3
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 +12 -0
- package/README.md +1 -1
- package/docs/adr/0027-content-bound-build-readiness.md +10 -0
- package/docs/cli.md +4 -3
- package/docs/engine/commands.md +3 -3
- package/docs/engine/workspace-schema.md +3 -1
- package/engine/go.mod +1 -1
- package/engine/help.go +1 -1
- package/engine/internal/gate/readiness_binding.go +97 -3
- package/engine/internal/lib/anchors.go +26 -0
- package/engine/internal/lib/drift.go +11 -1
- package/engine/internal/lib/packet_freeze.go +107 -11
- package/engine/internal/lib/perturb.go +39 -8
- package/engine/internal/parallel/git.go +3 -0
- package/engine/main.go +2 -2
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +5 -2
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +5 -2
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +5 -2
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +5 -2
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +5 -2
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +5 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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.0.3](https://github.com/ViktorsBaikers/DevRites/compare/v7.0.2...v7.0.3) (2026-10-21)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
* **no-release:** move the Go toolchain to go1.27.3 ([#155](https://github.com/ViktorsBaikers/DevRites/issues/155)) ([293da70](https://github.com/ViktorsBaikers/DevRites/commit/293da70c8c4b4de8ed22caababa7914594c1ff9b))
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
* **devrites:** freeze and probe a parallel sibling in its worktree ([#156](https://github.com/ViktorsBaikers/DevRites/issues/156)) ([e959804](https://github.com/ViktorsBaikers/DevRites/commit/e959804721f291db3b489eb30988792c5ab3c3fb))
|
|
14
|
+
* **no-release:** check anchors blocks prose-only blocker returns ([#154](https://github.com/ViktorsBaikers/DevRites/issues/154)) ([0807fe2](https://github.com/ViktorsBaikers/DevRites/commit/0807fe2f92100188616110beb037d81e752ee1b8))
|
|
15
|
+
* **no-release:** traceability progress cells no longer stale readiness ([#153](https://github.com/ViktorsBaikers/DevRites/issues/153)) ([7465b3e](https://github.com/ViktorsBaikers/DevRites/commit/7465b3eaaf180718015dfdab3d0af96579ab5b85))
|
|
16
|
+
|
|
5
17
|
## [7.0.2](https://github.com/ViktorsBaikers/DevRites/compare/v7.0.1...v7.0.2) (2026-10-20)
|
|
6
18
|
|
|
7
19
|
### 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.0.
|
|
34
|
+
**Status:** [`v7.0.3`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v7.0.3): 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
|
|
|
@@ -83,3 +83,13 @@ every canonical `AC-###` ID in `spec.md`'s Acceptance criteria section to appear
|
|
|
83
83
|
as a literal substring in `tasks.md`. When `test-plan.md` is required, the same
|
|
84
84
|
IDs must appear there. This is identity presence, not semantic coverage. Guard:
|
|
85
85
|
`engine/internal/state/acceptance_test.go`.
|
|
86
|
+
|
|
87
|
+
## Amendment (2026-10-21)
|
|
88
|
+
|
|
89
|
+
Build and Prove fill `traceability.md`'s Evidence and Status cells as evidence
|
|
90
|
+
lands, and those cells are Build-mutable evidence projections that Vet does not
|
|
91
|
+
approve. The binding therefore hashes `traceability.md` with every cell under an
|
|
92
|
+
`Evidence` or `Status` table header blanked; every other byte stays bound. A
|
|
93
|
+
binding line recorded over the raw bytes still verifies in `check readiness`
|
|
94
|
+
while the inputs are unchanged, and `check drift` reports a change confined to those cells as
|
|
95
|
+
`progress-only`. Guard: `engine/internal/gate/readiness_binding_test.go`.
|
package/docs/cli.md
CHANGED
|
@@ -26,7 +26,7 @@ devrites-engine check preconditions <slug> [--slice SLICE-###] [--timeout N]
|
|
|
26
26
|
devrites-engine check seal <slug>
|
|
27
27
|
devrites-engine check slice <slug> <SLICE-ID>
|
|
28
28
|
devrites-engine check diff-scope <slug> --allow <csv>|--allow-file <path> [--worktree|--staged|--base <ref>] [--snapshot|--baseline] [--verbose]
|
|
29
|
-
devrites-engine check perturb <slug> --patch <file> --expect <test-name> --gate <G-ID> [--timeout N]
|
|
29
|
+
devrites-engine check perturb <slug> --patch <file> --expect <test-name> --gate <G-ID> [--timeout N] [--worktree <path> --slice <SLICE-ID>]
|
|
30
30
|
devrites-engine check task-graph <slug>
|
|
31
31
|
devrites-engine check regression <slug> [--update]
|
|
32
32
|
devrites-engine check drift <slug> [--record]
|
|
@@ -48,7 +48,7 @@ devrites-engine context [slug] (--phase <p>|--skill <name>) [--role <r>] [--whol
|
|
|
48
48
|
devrites-engine metrics record <slug> --phase <p> --event <e>
|
|
49
49
|
devrites-engine metrics summary [slug]
|
|
50
50
|
devrites-engine dispatch <slug> <open|start|seal|return|status|abandon|attribute>
|
|
51
|
-
devrites-engine candidate freeze <slug> --wave <w> (--slice <SLICE-ID>|--paths <path>...) [--base <ref>]
|
|
51
|
+
devrites-engine candidate freeze <slug> --wave <w> (--slice <SLICE-ID>|--paths <path>...) [--base <ref>] [--worktree <path>]
|
|
52
52
|
devrites-engine packet <slug> --wave <w> --phase <p> --role <r> [--slice <SLICE-ID>] [--recheck] [--checks <G-ID,...>] [--focus <file>]
|
|
53
53
|
devrites-engine parallel <subcommand>
|
|
54
54
|
devrites-engine gates <subcommand> <slug>
|
|
@@ -160,7 +160,8 @@ self-contained updater.
|
|
|
160
160
|
ones; unresolved slots print `unresolved` rather than guessing.
|
|
161
161
|
- `check anchors <slug> <return-file>` anchors each Critical/Important row's
|
|
162
162
|
quote at its cited `file:line` and its `breaks` quote; Unverified or Deferred
|
|
163
|
-
rows exit `3` and never drive a repair round.
|
|
163
|
+
rows exit `3` and never drive a repair round. A `Counts:` head declaring more
|
|
164
|
+
Critical/Important findings than there are `Finding:` rows also exits `3`.
|
|
164
165
|
- `check dup [slug] [--all|--worktree|--staged|--base <ref>]` reports advisory
|
|
165
166
|
near-duplicate code clusters that survive comment stripping, literal
|
|
166
167
|
collapsing, and identifier renaming. Diff modes mark units overlapping
|
package/docs/engine/commands.md
CHANGED
|
@@ -24,10 +24,10 @@ workflow strategy.
|
|
|
24
24
|
| `check drift <slug> [--record]` | Attribute readiness-input changes to the exact artifact. `--record` writes the per-input `readiness-inputs.json` digest baseline under the feature lock; without it the workspace diffs current inputs against that baseline (or falls back to the aggregate binding recorded in `eng-review.md`). Advisory: exit `0` on drift; a corrupt baseline exits `3`. A `test-plan.md` change limited to exact `Workflow Artifact reviewed identity: <64hex>` lines is reported as `reviewed-identity-only` (ADR-0034). `--record` also stores each slice's proof bytes (its `tasks.md` block plus the `test-plan.md` lines naming it); both modes print `ADVISORY proportion:` for a slice whose proof grew over 2x since that baseline or carries over 3x the median proof per AC. |
|
|
25
25
|
| `check windows <slug> [--worktree\|--staged\|--base <ref>\|--since-dispatch <wave>]` | Fail on deferral and bar-lowering markers the change introduces without a waiver row in the workspace's `windows.md`: added TODO/FIXME/XXX/HACK, skipped/focused-test and stub idioms, checker suppressions, one-line empty catch; removed `deleted-test` files and net `removed-assertion` lines in remaining test files. `--since-dispatch <wave>` reports only `deleted-test`/`removed-assertion` against the work-tree snapshot (`tree` in `dispatch.json`) recorded when that build wright wave opened, untracked files included; it takes no `--cwd` and records the `windows-since` metric. |
|
|
26
26
|
| `check dup [slug] [--all\|--worktree\|--staged\|--base <ref>] [--min-lines n] [--threshold f] [--ignore-file <path>] [--limit n] [--exclude <csv>]` | Report near-duplicate code clusters that survive comment stripping, literal collapsing, and identifier renaming — token-shingle matches chained into segments, clustered transitively, ranked with bounded path/line-distance boosts. Diff modes mark units overlapping changed hunks with `*`; stable content-derived cluster hashes let `.devrites/dup-ignore` dismissals survive line moves but resurface on structural edits. Staged mode reads index blobs and excludes untracked files. Bounded heuristic: successful scans/leads exit `0`; usage, unavailable input, and partial scans exit `2`. |
|
|
27
|
-
| `check anchors <slug> <return-file> [--skills-root <dir>]` | Deterministic quote anchoring for one reviewer return (agents.md § Result admission). For each Critical/Important `Finding:` row (list markers and bold tolerated), it anchors the backticked observed quote at the cited `path:line[-line]`, looking in the workspace first, then the project, never outside either. Exact match → `anchored`. Exactly one other line → `relocated <cited> -> path:N`. Otherwise → `Unverified: <row> — anchor lost\|ambiguous`; this also covers a missing file:line or quote, or a quote under 8 characters. The `breaks "<quote>"` must appear in spec.md/decisions.md (spec ID) or in the named shipped standard under the skills root (`--skills-root` for host installs). No breaks quote → `Deferred: <row> — no spec anchor`. Prints the candidate digest and records an `anchors` metrics event. Exit `0` all anchored, `3` any Unverified/Deferred row (never drives a repair round), `2` usage or unreadable input. |
|
|
27
|
+
| `check anchors <slug> <return-file> [--skills-root <dir>]` | Deterministic quote anchoring for one reviewer return (agents.md § Result admission). For each Critical/Important `Finding:` row (list markers and bold tolerated), it anchors the backticked observed quote at the cited `path:line[-line]`, looking in the workspace first, then the project, never outside either. Exact match → `anchored`. Exactly one other line → `relocated <cited> -> path:N`. Otherwise → `Unverified: <row> — anchor lost\|ambiguous`; this also covers a missing file:line or quote, or a quote under 8 characters. The `breaks "<quote>"` must appear in spec.md/decisions.md (spec ID) or in the named shipped standard under the skills root (`--skills-root` for host installs). No breaks quote → `Deferred: <row> — no spec anchor`. Prints the candidate digest and records an `anchors` metrics event. Exit `0` all anchored, `3` any Unverified/Deferred row (never drives a repair round) or a `Counts:` head declaring more Critical/Important findings than `Finding:` rows, `2` usage or unreadable input. |
|
|
28
28
|
| `check slice <slug> <SLICE-ID>` | Pre-dispatch lint of one slice's wright contract: contract present, exact Writer allowlist paths, resolvable `Satisfies` AC ids, bounded scope. Same non-blocking size advisory as `check task-graph`, plus the `check drift` proportion advisory for that slice. |
|
|
29
29
|
| `check diff-scope <slug> --allow <csv>\|--allow-file <path> [--worktree\|--staged\|--base <ref>] [--baseline] [--verbose]`, `check diff-scope <slug> --snapshot` | Verify the changed-path set stays inside the declared allowlist; the mechanical gate run before reviewer dispatch. `--snapshot` records the mode and SHA-256 of every currently changed path in `<feature>/diff-scope-baseline.json` (an engine machine file, never reported); run it before writer dispatch. `--baseline` drops a changed path whose fingerprint still matches, printing `preexisting (unchanged)`; a baselined path whose bytes or mode changed, or a new path, still blocks; a run with only preexisting changes is an empty diff; a missing baseline exits `2`. More than 20 violations print grouped by the first two path segments (count plus 3 examples, at most 20 groups); `--verbose` lists every path. Exit codes are unchanged. |
|
|
30
|
-
| `check perturb <slug> --patch <file> --expect <test-name> --gate <G-ID> [--timeout N]` | Safe-perturbation probe (testing.md § Safe perturbation). Copies the candidate (HEAD plus every touched-files.md manifest row, at the project's prefix below the git toplevel) into a detached temporary worktree, requires it to hash to the `check candidate` digest, applies the wright-supplied mutant patch (toplevel-relative paths), and runs the gate's CHECK only when it is an approved test-plan.md preflight row. RED counts only when a runner FAIL line (go test, pytest, cargo test, jest/vitest) names `--expect`; build/setup failure, timeout, exit 0 (survivor), or a RED naming only other tests is `cannot_verify`. Reverts the patch, requires the copy back at the candidate digest and the gate GREEN (exit 0 plus EXPECT), and requires the checkout candidate unchanged. Writes `<feature>/evidence/perturb/<gate>-<digest12>-aN.log`. Never mutates the checkout. Exit `0` verified, `3` cannot_verify or refused, `2` usage. |
|
|
30
|
+
| `check perturb <slug> --patch <file> --expect <test-name> --gate <G-ID> [--timeout N] [--worktree <path> --slice <SLICE-ID>]` | Safe-perturbation probe (testing.md § Safe perturbation). Copies the candidate (HEAD plus every touched-files.md manifest row, at the project's prefix below the git toplevel) into a detached temporary worktree, requires it to hash to the `check candidate` digest, applies the wright-supplied mutant patch (toplevel-relative paths), and runs the gate's CHECK only when it is an approved test-plan.md preflight row. RED counts only when a runner FAIL line (go test, pytest, cargo test, jest/vitest) names `--expect`; build/setup failure, timeout, exit 0 (survivor), or a RED naming only other tests is `cannot_verify`. Reverts the patch, requires the copy back at the candidate digest and the gate GREEN (exit 0 plus EXPECT), and requires the checkout candidate unchanged. Writes `<feature>/evidence/perturb/<gate>-<digest12>-aN.log`. Never mutates the checkout. `--worktree <path> --slice <SLICE-ID>` (both or neither) probes a linked worktree of the same repository instead (a parallel sibling, checked as for `candidate freeze --worktree`): its HEAD plus the slice's tasks.md write set, hashed as `candidate freeze` does, with `--patch` resolved against the worktree; the transcript stays under the control workspace. Exit `0` verified, `3` cannot_verify or refused, `2` usage. |
|
|
31
31
|
| `check skill-trust <path>` | Scan one skill/agent Markdown file for structural trust violations. |
|
|
32
32
|
| `check indexes [--root <dir>]` | Report install manifest and in-repo code-index artifact presence (`.codebase-memory/`) as JSON. codebase-memory-mcp normally indexes into its central store, so its `index_status` is authoritative. |
|
|
33
33
|
| `observe summary <slug>` | Emit a sanitized workspace summary, as one compact JSON line, from one retained observation. `task_graph.ok` is true iff `task_graph.problems` is empty; `problems` lists cycle, unknown-dep, malformed-token, duplicate-id, and missing-`Dependencies` blockers. `inflight_waves` lists dispatch waves still `open` or `sealed`, with their pending roles. |
|
|
@@ -38,7 +38,7 @@ workflow strategy.
|
|
|
38
38
|
| `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. |
|
|
39
39
|
| `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. |
|
|
40
40
|
| `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 first-round build wright wave narrower than the `parallel select --from-plan` batch exits `3` (printing the batch, the command and the parallel-batch lifecycle) until evidence.md records `serial: <skipped ids> — <reason>`; repair rounds, lease siblings and HITL runs are never 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. |
|
|
41
|
-
| `candidate freeze <slug> --wave <w> (--slice <SLICE-ID> \| --paths <path>...) [--base <ref>]` | 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. |
|
|
41
|
+
| `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. |
|
|
42
42
|
| `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), 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`). |
|
|
43
43
|
| `metrics record <slug> --phase <p> --event <e> [--role r] [--bytes n] [--note s]` | Append an event to the workspace's `metrics.jsonl` ledger. |
|
|
44
44
|
| `metrics summary [slug] [--by-slice]` | Reduce `metrics.jsonl` into a per-phase roll-up. `--by-slice` reads `dispatch.json` instead: per slice, build wright rounds, slice-bound Vet rechecks, and agent-minutes (role start to `returned_at`; abandoned waves excluded). When the ledger has `resume` events it adds `resumes count=N minutes_to_dispatch=...` (each resume timed to the next `dispatch` event; `none` when another resume came first, `open` when none followed). |
|
|
@@ -69,7 +69,9 @@ identity binds raw bytes for `spec.md`, `decision-coverage.md`,
|
|
|
69
69
|
`architecture.md`, `plan.md`, `tasks.md`, `traceability.md`, and
|
|
70
70
|
`test-plan.md`; it also binds presence or absence and bytes for optional
|
|
71
71
|
`strategy.md`, `design-brief.md`, `ai-spec.md`, and project-root
|
|
72
|
-
`.devrites/principles.md`. It excludes Build-mutable ledgers and evidence.
|
|
72
|
+
`.devrites/principles.md`. It excludes Build-mutable ledgers and evidence. Cells under a `traceability.md` table's `Evidence` or `Status`
|
|
73
|
+
header are blanked before hashing, so recording progress there does not stale
|
|
74
|
+
the binding.
|
|
73
75
|
|
|
74
76
|
Each input is limited to 1 MiB and the aggregate to 8 MiB; symlinks and
|
|
75
77
|
non-regular files fail closed. Ordinary readiness verifies the binding whenever
|
package/engine/go.mod
CHANGED
package/engine/help.go
CHANGED
|
@@ -24,7 +24,7 @@ const (
|
|
|
24
24
|
check slice <slug> <SLICE-ID>
|
|
25
25
|
check diff-scope <slug> --allow <csv> | --allow-file <path> [--worktree|--staged|--base <ref>] [--allow-empty] [--baseline] [--verbose] [--cwd <dir>]
|
|
26
26
|
check diff-scope <slug> --snapshot [--worktree|--staged|--base <ref>] [--cwd <dir>]
|
|
27
|
-
check perturb <slug> --patch <file> --expect <test-name> --gate <G-ID> [--timeout N]
|
|
27
|
+
check perturb <slug> --patch <file> --expect <test-name> --gate <G-ID> [--timeout N] [--worktree <path> --slice <SLICE-ID>]
|
|
28
28
|
check skill-trust <path>
|
|
29
29
|
check indexes [--root <dir>]
|
|
30
30
|
check regression <slug> [--update]
|
|
@@ -49,6 +49,10 @@ func ReadinessBinding(root, slug string) (string, error) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
func readinessBindingFromObservation(observation *state.WorkspaceObservation) (string, error) {
|
|
52
|
+
return bindReadinessInputs(observation, true)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func bindReadinessInputs(observation *state.WorkspaceObservation, blankProgress bool) (string, error) {
|
|
52
56
|
digest := sha256.New()
|
|
53
57
|
writeReadinessField(digest, readinessBindingDomain)
|
|
54
58
|
writeReadinessField(digest, readinessBindingVersion)
|
|
@@ -58,6 +62,9 @@ func readinessBindingFromObservation(observation *state.WorkspaceObservation) (s
|
|
|
58
62
|
if err != nil {
|
|
59
63
|
return "", err
|
|
60
64
|
}
|
|
65
|
+
if blankProgress && input.logical == "traceability.md" {
|
|
66
|
+
content = withoutProgressCells(content)
|
|
67
|
+
}
|
|
61
68
|
writeReadinessField(digest, string(input.logical))
|
|
62
69
|
if present {
|
|
63
70
|
writeReadinessField(digest, "present")
|
|
@@ -110,6 +117,12 @@ func verifyReadinessBinding(observation *state.WorkspaceObservation) (string, er
|
|
|
110
117
|
if err != nil {
|
|
111
118
|
return "", err
|
|
112
119
|
}
|
|
120
|
+
// A line recorded over the raw bytes, before progress cells were blanked,
|
|
121
|
+
// still verifies while those bytes are unchanged.
|
|
122
|
+
legacy, err := bindReadinessInputs(observation, false)
|
|
123
|
+
if err != nil {
|
|
124
|
+
return "", err
|
|
125
|
+
}
|
|
113
126
|
fact, ok := observation.Fact("eng-review.md")
|
|
114
127
|
if !ok || fact.State() == state.ArtifactAbsent {
|
|
115
128
|
return expected, errors.New("readiness input eng-review.md is missing")
|
|
@@ -130,7 +143,7 @@ func verifyReadinessBinding(observation *state.WorkspaceObservation) (string, er
|
|
|
130
143
|
continue
|
|
131
144
|
}
|
|
132
145
|
attempts++
|
|
133
|
-
if line == expected {
|
|
146
|
+
if line == expected || line == legacy {
|
|
134
147
|
matches++
|
|
135
148
|
}
|
|
136
149
|
}
|
|
@@ -150,7 +163,7 @@ type ReadinessInputDigest struct {
|
|
|
150
163
|
SHA256 string `json:"sha256,omitempty"`
|
|
151
164
|
// Normalized is test-plan.md's digest without its reviewed-identity lines,
|
|
152
165
|
// so drift can prove a Workflow Artifact byte correction changed nothing
|
|
153
|
-
// else Vet approved.
|
|
166
|
+
// else Vet approved, or traceability.md's digest without progress cells.
|
|
154
167
|
Normalized string `json:"normalized,omitempty"`
|
|
155
168
|
}
|
|
156
169
|
|
|
@@ -187,9 +200,13 @@ func ReadinessInputDigests(root, slug string) ([]ReadinessInputDigest, string, e
|
|
|
187
200
|
if present {
|
|
188
201
|
sum := sha256.Sum256(content)
|
|
189
202
|
entry.SHA256 = hex.EncodeToString(sum[:])
|
|
190
|
-
|
|
203
|
+
switch input.logical {
|
|
204
|
+
case "test-plan.md":
|
|
191
205
|
normalized := sha256.Sum256(withoutReviewedIdentity(content))
|
|
192
206
|
entry.Normalized = hex.EncodeToString(normalized[:])
|
|
207
|
+
case "traceability.md":
|
|
208
|
+
normalized := sha256.Sum256(withoutProgressCells(content))
|
|
209
|
+
entry.Normalized = hex.EncodeToString(normalized[:])
|
|
193
210
|
}
|
|
194
211
|
}
|
|
195
212
|
digests = append(digests, entry)
|
|
@@ -197,6 +214,83 @@ func ReadinessInputDigests(root, slug string) ([]ReadinessInputDigest, string, e
|
|
|
197
214
|
return digests, binding, nil
|
|
198
215
|
}
|
|
199
216
|
|
|
217
|
+
// progressColumns name the traceability.md columns Build and Prove fill in as
|
|
218
|
+
// evidence lands. Vet approves the rows, not their progress, so the binding
|
|
219
|
+
// blanks these cells and keeps every other cell byte-exact.
|
|
220
|
+
var progressColumns = map[string]bool{"evidence": true, "status": true}
|
|
221
|
+
|
|
222
|
+
var delimiterCellRE = regexp.MustCompile(`^\s*:?-+:?\s*$`)
|
|
223
|
+
|
|
224
|
+
// withoutProgressCells blanks every Evidence and Status cell in the Markdown
|
|
225
|
+
// tables of content. A table is the header row, its delimiter row, and the
|
|
226
|
+
// table rows that follow.
|
|
227
|
+
func withoutProgressCells(content []byte) []byte {
|
|
228
|
+
lines := strings.Split(string(content), "\n")
|
|
229
|
+
var blank []int
|
|
230
|
+
for i, line := range lines {
|
|
231
|
+
prefix, cells, ok := tableCells(line)
|
|
232
|
+
switch {
|
|
233
|
+
case !ok:
|
|
234
|
+
blank = nil
|
|
235
|
+
case i+1 < len(lines) && delimiterRow(lines[i+1]):
|
|
236
|
+
blank = nil
|
|
237
|
+
for c, cell := range cells {
|
|
238
|
+
if progressColumns[strings.ToLower(strings.TrimSpace(cell))] {
|
|
239
|
+
blank = append(blank, c)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
case len(blank) > 0 && !delimiterRow(line):
|
|
243
|
+
for _, c := range blank {
|
|
244
|
+
if c < len(cells) {
|
|
245
|
+
cells[c] = ""
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
lines[i] = prefix + "|" + strings.Join(cells, "|") + "|"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return []byte(strings.Join(lines, "\n"))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// tableCells splits a Markdown table row on its unescaped pipes. prefix is the
|
|
255
|
+
// indentation before the first pipe.
|
|
256
|
+
func tableCells(line string) (prefix string, cells []string, ok bool) {
|
|
257
|
+
trimmed := strings.TrimRight(strings.TrimSuffix(line, "\r"), " \t")
|
|
258
|
+
start := strings.Index(trimmed, "|")
|
|
259
|
+
if start < 0 || strings.TrimSpace(trimmed[:start]) != "" {
|
|
260
|
+
return "", nil, false
|
|
261
|
+
}
|
|
262
|
+
var cell strings.Builder
|
|
263
|
+
for i := start + 1; i < len(trimmed); i++ {
|
|
264
|
+
switch {
|
|
265
|
+
case trimmed[i] == '\\' && i+1 < len(trimmed) && trimmed[i+1] == '|':
|
|
266
|
+
cell.WriteString(`\|`)
|
|
267
|
+
i++
|
|
268
|
+
case trimmed[i] == '|':
|
|
269
|
+
cells = append(cells, cell.String())
|
|
270
|
+
cell.Reset()
|
|
271
|
+
default:
|
|
272
|
+
cell.WriteByte(trimmed[i])
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if cell.Len() > 0 {
|
|
276
|
+
cells = append(cells, cell.String())
|
|
277
|
+
}
|
|
278
|
+
return trimmed[:start], cells, true
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
func delimiterRow(line string) bool {
|
|
282
|
+
_, cells, ok := tableCells(line)
|
|
283
|
+
if !ok || len(cells) == 0 {
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
for _, cell := range cells {
|
|
287
|
+
if !delimiterCellRE.MatchString(cell) {
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return true
|
|
292
|
+
}
|
|
293
|
+
|
|
200
294
|
func readinessDiagnosticError(diagnostic state.ArtifactDiagnostic) error {
|
|
201
295
|
prefix := fmt.Sprintf("readiness input %s is %s (%s); ", diagnostic.Path, diagnostic.State, diagnostic.Code)
|
|
202
296
|
repair := diagnosticRepair(diagnostic.Code)
|
|
@@ -108,6 +108,12 @@ func RunCheckAnchors(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
108
108
|
summary := fmt.Sprintf("anchored=%d relocated=%d unverified=%d deferred=%d",
|
|
109
109
|
counts["anchored"], counts["relocated"], counts["Unverified"], counts["Deferred"])
|
|
110
110
|
recordMetricNote(root, slug, cursorPhase(root, slug), "anchors", "", summary)
|
|
111
|
+
// A head that declares more blockers than there are Finding: rows means
|
|
112
|
+
// rows were written as prose; zero parsed rows must not read as clean.
|
|
113
|
+
if declared := declaredBlockers(string(raw)); declared > rows {
|
|
114
|
+
fmt.Fprintf(stdout, "check anchors: BLOCKED: Counts declares %d Critical/Important but %d are Finding: rows; rewrite each as a Finding: row and rerun\n", declared, rows)
|
|
115
|
+
return 3
|
|
116
|
+
}
|
|
111
117
|
if counts["Unverified"]+counts["Deferred"] > 0 {
|
|
112
118
|
fmt.Fprintf(stdout, "check anchors: BLOCKED: %d Critical/Important rows, %s; Unverified and Deferred rows never drive a repair round\n", rows, summary)
|
|
113
119
|
return 3
|
|
@@ -116,6 +122,26 @@ func RunCheckAnchors(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
116
122
|
return 0
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
// declaredBlockers sums the critical and important entries of the return's
|
|
126
|
+
// first Counts: line, or 0 when there is none.
|
|
127
|
+
func declaredBlockers(text string) int {
|
|
128
|
+
for _, line := range strings.Split(text, "\n") {
|
|
129
|
+
rest, ok := strings.CutPrefix(strings.TrimSpace(line), "Counts:")
|
|
130
|
+
if !ok {
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
total := 0
|
|
134
|
+
for _, field := range strings.Fields(rest) {
|
|
135
|
+
if m := countRE.FindStringSubmatch(field); m != nil && (m[1] == "critical" || m[1] == "important") {
|
|
136
|
+
n, _ := strconv.Atoi(m[2])
|
|
137
|
+
total += n
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return total
|
|
141
|
+
}
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
144
|
+
|
|
119
145
|
// row classifies one blocker row: anchored, relocated, Unverified or Deferred.
|
|
120
146
|
func (a anchorer) row(cells []string) (status, detail string) {
|
|
121
147
|
if len(cells) < 3 {
|
|
@@ -132,7 +132,14 @@ func compareDrift(workspace, baselinePath string, digests []gate.ReadinessInputD
|
|
|
132
132
|
if !hasBaseline {
|
|
133
133
|
return compareDriftFallback(workspace, binding, stdout)
|
|
134
134
|
}
|
|
135
|
-
diffs
|
|
135
|
+
var diffs []driftDiff
|
|
136
|
+
for _, d := range diffInputs(baseline.Inputs, digests) {
|
|
137
|
+
if d.Kind == "progress-only" {
|
|
138
|
+
fmt.Fprintf(stdout, "check drift: %s progress-only — only Evidence/Status cells moved; no re-vet needed\n", d.Path)
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
diffs = append(diffs, d)
|
|
142
|
+
}
|
|
136
143
|
if len(diffs) == 0 && baseline.Binding == binding {
|
|
137
144
|
fmt.Fprintf(stdout, "check drift: none (baseline %s)\n", baseline.RecordedAt)
|
|
138
145
|
return 0
|
|
@@ -215,6 +222,9 @@ func diffInputs(base, current []gate.ReadinessInputDigest) []driftDiff {
|
|
|
215
222
|
kind := "changed"
|
|
216
223
|
if b.Normalized != "" && b.Normalized == c.Normalized {
|
|
217
224
|
kind = "reviewed-identity-only"
|
|
225
|
+
if b.Path == "traceability.md" {
|
|
226
|
+
kind = "progress-only"
|
|
227
|
+
}
|
|
218
228
|
}
|
|
219
229
|
diffs = append(diffs, driftDiff{Path: b.Path, Kind: kind})
|
|
220
230
|
}
|
|
@@ -12,11 +12,12 @@ import (
|
|
|
12
12
|
"strings"
|
|
13
13
|
|
|
14
14
|
"github.com/devrites/devrites/internal/devritespaths"
|
|
15
|
+
"github.com/devrites/devrites/internal/parallel"
|
|
15
16
|
"github.com/devrites/devrites/internal/state"
|
|
16
17
|
)
|
|
17
18
|
|
|
18
19
|
// CandidateFreezeUsage is the help and usage-error text for `candidate freeze`.
|
|
19
|
-
const CandidateFreezeUsage = `usage: devrites-engine candidate freeze <slug> --wave <w> (--slice <SLICE-ID> | --paths <path>...) [--base <ref>]
|
|
20
|
+
const CandidateFreezeUsage = `usage: devrites-engine candidate freeze <slug> --wave <w> (--slice <SLICE-ID> | --paths <path>...) [--base <ref>] [--worktree <path>]
|
|
20
21
|
|
|
21
22
|
Freezes one review candidate. It writes packets/<w>.diff (the path set's
|
|
22
23
|
tracked changes against --base, default HEAD, plus each untracked file as a
|
|
@@ -24,7 +25,10 @@ tracked changes against --base, default HEAD, plus each untracked file as a
|
|
|
24
25
|
sha256, and the aggregate candidate digest under the check candidate domain
|
|
25
26
|
and bounds). --slice takes that slice's Writer allowlist (else Files likely
|
|
26
27
|
touched) from tasks.md. A wave's frozen candidate is never replaced by a
|
|
27
|
-
different one; freeze a changed candidate under a new wave.
|
|
28
|
+
different one; freeze a changed candidate under a new wave. --worktree reads
|
|
29
|
+
the path set from a linked worktree of this repository (a parallel sibling)
|
|
30
|
+
instead of the checkout, requires --base, refuses uncommitted path-set
|
|
31
|
+
changes, and records that worktree's HEAD (the transfer commit) as head.
|
|
28
32
|
`
|
|
29
33
|
|
|
30
34
|
// packetNameRE bounds a wave, role, phase, or slice used in a packets/ or
|
|
@@ -34,7 +38,7 @@ var packetNameRE = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)
|
|
|
34
38
|
// RunCandidateFreeze implements `candidate freeze`: the producer of the frozen
|
|
35
39
|
// .diff a Review or Seal reviewer packet must name.
|
|
36
40
|
func RunCandidateFreeze(root string, args []string, stdout, stderr io.Writer) int {
|
|
37
|
-
var wave, slice, base string
|
|
41
|
+
var wave, slice, base, worktree string
|
|
38
42
|
var paths, positional []string
|
|
39
43
|
for i := 0; i < len(args); i++ {
|
|
40
44
|
value := argAt(args, i+1)
|
|
@@ -45,6 +49,8 @@ func RunCandidateFreeze(root string, args []string, stdout, stderr io.Writer) in
|
|
|
45
49
|
slice = value
|
|
46
50
|
case "--base":
|
|
47
51
|
base = value
|
|
52
|
+
case "--worktree":
|
|
53
|
+
worktree = value
|
|
48
54
|
case "--paths":
|
|
49
55
|
for i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
50
56
|
i++
|
|
@@ -61,7 +67,7 @@ func RunCandidateFreeze(root string, args []string, stdout, stderr io.Writer) in
|
|
|
61
67
|
}
|
|
62
68
|
i++
|
|
63
69
|
}
|
|
64
|
-
if len(positional) != 1 || !packetNameRE.MatchString(wave) || (slice == "") == (len(paths) == 0) {
|
|
70
|
+
if len(positional) != 1 || !packetNameRE.MatchString(wave) || (slice == "") == (len(paths) == 0) || (worktree != "" && base == "") {
|
|
65
71
|
fmt.Fprint(stderr, CandidateFreezeUsage)
|
|
66
72
|
return 2
|
|
67
73
|
}
|
|
@@ -75,16 +81,16 @@ func RunCandidateFreeze(root string, args []string, stdout, stderr io.Writer) in
|
|
|
75
81
|
return fail(err)
|
|
76
82
|
}
|
|
77
83
|
if slice != "" {
|
|
78
|
-
|
|
79
|
-
if err != nil {
|
|
84
|
+
if paths, err = slicePaths(root, slug, slice); err != nil {
|
|
80
85
|
return fail(err)
|
|
81
86
|
}
|
|
82
|
-
writes, _ := sliceWritePaths(raw, ParseTaskGraph(raw))
|
|
83
|
-
if paths = writes[slice]; len(paths) == 0 {
|
|
84
|
-
return fail(fmt.Errorf("slice %s names no Writer allowlist or Files likely touched in tasks.md", slice))
|
|
85
|
-
}
|
|
86
87
|
}
|
|
87
88
|
project := projectDir(root)
|
|
89
|
+
if worktree != "" {
|
|
90
|
+
if project, err = checkedWorktree(root, worktree); err != nil {
|
|
91
|
+
return fail(err)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
88
94
|
rows, err := freezeRows(project, paths)
|
|
89
95
|
if err != nil {
|
|
90
96
|
return fail(err)
|
|
@@ -99,12 +105,24 @@ func RunCandidateFreeze(root string, args []string, stdout, stderr io.Writer) in
|
|
|
99
105
|
if len(diff) == 0 {
|
|
100
106
|
return fail(errors.New("the path set has no change against the base; there is no candidate to freeze"))
|
|
101
107
|
}
|
|
108
|
+
head := ""
|
|
109
|
+
if worktree != "" {
|
|
110
|
+
// Review binds to the transfer commit, so the frozen tree must be exactly it.
|
|
111
|
+
dirty, commit, err := freezeDiff(project, "HEAD", rows)
|
|
112
|
+
if err != nil {
|
|
113
|
+
return fail(err)
|
|
114
|
+
}
|
|
115
|
+
if len(dirty) != 0 {
|
|
116
|
+
return fail(errors.New("the worktree has uncommitted changes on the path set; the frozen candidate must be its HEAD transfer commit"))
|
|
117
|
+
}
|
|
118
|
+
head = "head " + commit + "\n"
|
|
119
|
+
}
|
|
102
120
|
digest, err := hashCandidate(project, rows)
|
|
103
121
|
if err != nil {
|
|
104
122
|
return fail(err)
|
|
105
123
|
}
|
|
106
124
|
var sums strings.Builder
|
|
107
|
-
fmt.Fprintf(&sums, "%s %s\nbase %s\
|
|
125
|
+
fmt.Fprintf(&sums, "%s %s\nbase %s\n%sdiff %x packets/%s.diff\n", candidateDigestDomain, candidateDigestVersion, baseCommit, head, sha256.Sum256(diff), wave)
|
|
108
126
|
for _, row := range rows {
|
|
109
127
|
if row.state != "present" {
|
|
110
128
|
fmt.Fprintf(&sums, "file absent %s\n", row.path)
|
|
@@ -139,6 +157,84 @@ func RunCandidateFreeze(root string, args []string, stdout, stderr io.Writer) in
|
|
|
139
157
|
return 0
|
|
140
158
|
}
|
|
141
159
|
|
|
160
|
+
// slicePaths is the slice's tasks.md write set: Writer allowlist, else Files
|
|
161
|
+
// likely touched.
|
|
162
|
+
func slicePaths(root, slug, slice string) ([]string, error) {
|
|
163
|
+
raw, err := readWorkspaceArtifact(root, slug, "tasks.md")
|
|
164
|
+
if err != nil {
|
|
165
|
+
return nil, err
|
|
166
|
+
}
|
|
167
|
+
writes, _ := sliceWritePaths(raw, ParseTaskGraph(raw))
|
|
168
|
+
if len(writes[slice]) == 0 {
|
|
169
|
+
return nil, fmt.Errorf("slice %s names no Writer allowlist or Files likely touched in tasks.md", slice)
|
|
170
|
+
}
|
|
171
|
+
return writes[slice], nil
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// checkedWorktree returns the project's directory inside path, a linked
|
|
175
|
+
// worktree of the control checkout's repository: path itself, plus the
|
|
176
|
+
// project's prefix below the git toplevel. It refuses a symlinked component, a
|
|
177
|
+
// directory that is not a worktree's top level, a submodule, another
|
|
178
|
+
// repository, a path git does not list as a worktree, and the control checkout.
|
|
179
|
+
func checkedWorktree(root, path string) (string, error) {
|
|
180
|
+
abs, err := filepath.Abs(path)
|
|
181
|
+
if err != nil {
|
|
182
|
+
return "", err
|
|
183
|
+
}
|
|
184
|
+
if err := refuseSymlinkedPath(abs); err != nil {
|
|
185
|
+
return "", err
|
|
186
|
+
}
|
|
187
|
+
out, err := runGitCommand(projectDir(root), nil, "rev-parse", "--path-format=absolute", "--git-common-dir", "--show-toplevel", "--show-prefix")
|
|
188
|
+
if err != nil {
|
|
189
|
+
return "", err
|
|
190
|
+
}
|
|
191
|
+
control := strings.Split(strings.TrimSuffix(string(out), "\n"), "\n")
|
|
192
|
+
if len(control) != 3 {
|
|
193
|
+
return "", fmt.Errorf("git rev-parse printed %q for the control checkout", out)
|
|
194
|
+
}
|
|
195
|
+
out, err = runGitCommand(abs, nil, "rev-parse", "--path-format=absolute", "--git-common-dir", "--show-toplevel", "--show-superproject-working-tree")
|
|
196
|
+
got := strings.Split(strings.TrimSpace(string(out)), "\n")
|
|
197
|
+
switch {
|
|
198
|
+
case err != nil:
|
|
199
|
+
return "", fmt.Errorf("worktree %s is not a git worktree: %w", abs, err)
|
|
200
|
+
case len(got) != 2:
|
|
201
|
+
return "", fmt.Errorf("worktree %s is inside a submodule", abs)
|
|
202
|
+
case !sameDir(got[0], control[0]):
|
|
203
|
+
return "", fmt.Errorf("worktree %s belongs to another repository", abs)
|
|
204
|
+
case !sameDir(got[1], abs):
|
|
205
|
+
return "", fmt.Errorf("worktree %s is not a worktree's top level", abs)
|
|
206
|
+
case sameDir(abs, control[1]):
|
|
207
|
+
return "", fmt.Errorf("worktree %s is the control checkout, not a linked worktree", abs)
|
|
208
|
+
}
|
|
209
|
+
registered, err := parallel.RegisteredWorktrees(control[1])
|
|
210
|
+
if err != nil {
|
|
211
|
+
return "", err
|
|
212
|
+
}
|
|
213
|
+
if !registered[abs] {
|
|
214
|
+
return "", fmt.Errorf("worktree %s is not listed by git worktree list", abs)
|
|
215
|
+
}
|
|
216
|
+
project := filepath.Join(abs, filepath.FromSlash(control[2]))
|
|
217
|
+
return project, refuseSymlinkedPath(project)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// sameDir reports whether a and b name one existing directory, whatever their
|
|
221
|
+
// spelling.
|
|
222
|
+
func sameDir(a, b string) bool {
|
|
223
|
+
ai, aerr := os.Stat(a)
|
|
224
|
+
bi, berr := os.Stat(b)
|
|
225
|
+
return aerr == nil && berr == nil && ai.IsDir() && os.SameFile(ai, bi)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// refuseSymlinkedPath refuses an absolute path any component of which is a
|
|
229
|
+
// symlink (or that does not exist).
|
|
230
|
+
func refuseSymlinkedPath(abs string) error {
|
|
231
|
+
resolved, err := filepath.EvalSymlinks(abs)
|
|
232
|
+
if err == nil && resolved != abs {
|
|
233
|
+
err = fmt.Errorf("worktree %s has a symlinked component (it resolves to %s)", abs, resolved)
|
|
234
|
+
}
|
|
235
|
+
return err
|
|
236
|
+
}
|
|
237
|
+
|
|
142
238
|
// freezeRows normalizes, deduplicates, and sorts the path set, marking each
|
|
143
239
|
// row present or deleted (absent) as hashCandidate expects.
|
|
144
240
|
func freezeRows(project string, paths []string) ([]candidateRow, error) {
|
|
@@ -22,7 +22,7 @@ import (
|
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
// PerturbUsage is the help and usage-error text for `check perturb`.
|
|
25
|
-
const PerturbUsage = `usage: devrites-engine check perturb <slug> --patch <file> --expect <test-name> --gate <G-ID> [--timeout N]
|
|
25
|
+
const PerturbUsage = `usage: devrites-engine check perturb <slug> --patch <file> --expect <test-name> --gate <G-ID> [--timeout N] [--worktree <path> --slice <SLICE-ID>]
|
|
26
26
|
|
|
27
27
|
Runs one safe-perturbation probe without touching the checkout. It copies the
|
|
28
28
|
current candidate (HEAD plus every touched-files.md manifest row) into a
|
|
@@ -34,8 +34,10 @@ test, jest/vitest) names --expect; a build or setup failure, a timeout, exit 0
|
|
|
34
34
|
(the mutant survived), or a RED naming only other tests is cannot_verify. It
|
|
35
35
|
then reverts the patch, requires the copy back at the candidate digest and the
|
|
36
36
|
gate GREEN (exit 0 plus EXPECT), and requires the checkout candidate unchanged.
|
|
37
|
-
The transcript is written under evidence/perturb/.
|
|
38
|
-
|
|
37
|
+
The transcript is written under evidence/perturb/. --worktree probes a linked
|
|
38
|
+
worktree of this repository (a parallel sibling) instead: its HEAD plus the
|
|
39
|
+
--slice write set from tasks.md, with --patch resolved against it. Exit 0
|
|
40
|
+
verified; exit 3 cannot_verify or refused.
|
|
39
41
|
`
|
|
40
42
|
|
|
41
43
|
const perturbOutputTail = 64 << 10
|
|
@@ -65,7 +67,7 @@ type perturbProbe struct {
|
|
|
65
67
|
// RunCheckPerturb implements `check perturb`: an engine-run, candidate-bound
|
|
66
68
|
// safe-perturbation probe (testing.md § Safe perturbation).
|
|
67
69
|
func RunCheckPerturb(root string, args []string, stdout, stderr io.Writer) int {
|
|
68
|
-
var patchArg, expect, gateID string
|
|
70
|
+
var patchArg, expect, gateID, worktree, slice string
|
|
69
71
|
var positional []string
|
|
70
72
|
timeout := acceptance.DefaultTimeout
|
|
71
73
|
for i := 0; i < len(args); i++ {
|
|
@@ -77,6 +79,10 @@ func RunCheckPerturb(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
77
79
|
expect = strings.TrimSpace(value)
|
|
78
80
|
case "--gate":
|
|
79
81
|
gateID = value
|
|
82
|
+
case "--worktree":
|
|
83
|
+
worktree = value
|
|
84
|
+
case "--slice":
|
|
85
|
+
slice = value
|
|
80
86
|
case "--timeout":
|
|
81
87
|
n, err := strconv.Atoi(value)
|
|
82
88
|
if err != nil || n <= 0 {
|
|
@@ -94,7 +100,7 @@ func RunCheckPerturb(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
94
100
|
}
|
|
95
101
|
i++
|
|
96
102
|
}
|
|
97
|
-
if len(positional) != 1 || patchArg == "" || expect == "" || gateID == "" {
|
|
103
|
+
if len(positional) != 1 || patchArg == "" || expect == "" || gateID == "" || (worktree == "") != (slice == "") {
|
|
98
104
|
fmt.Fprint(stderr, PerturbUsage)
|
|
99
105
|
return 2
|
|
100
106
|
}
|
|
@@ -107,7 +113,19 @@ func RunCheckPerturb(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
107
113
|
if err != nil {
|
|
108
114
|
return refuse(err)
|
|
109
115
|
}
|
|
110
|
-
|
|
116
|
+
project := projectDir(root)
|
|
117
|
+
load := func() (candidateIdentity, error) { return loadCandidateIdentity(root, slug) }
|
|
118
|
+
if worktree != "" {
|
|
119
|
+
if project, err = checkedWorktree(root, worktree); err != nil {
|
|
120
|
+
return refuse(err)
|
|
121
|
+
}
|
|
122
|
+
paths, err := slicePaths(root, slug, slice)
|
|
123
|
+
if err != nil {
|
|
124
|
+
return refuse(err)
|
|
125
|
+
}
|
|
126
|
+
load = func() (candidateIdentity, error) { return pathSetIdentity(project, paths) }
|
|
127
|
+
}
|
|
128
|
+
identity, err := load()
|
|
111
129
|
if err != nil {
|
|
112
130
|
return refuse(err)
|
|
113
131
|
}
|
|
@@ -115,7 +133,6 @@ func RunCheckPerturb(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
115
133
|
if err != nil {
|
|
116
134
|
return refuse(err)
|
|
117
135
|
}
|
|
118
|
-
project := projectDir(root)
|
|
119
136
|
patchPath := patchArg
|
|
120
137
|
if !filepath.IsAbs(patchPath) {
|
|
121
138
|
patchPath = filepath.Join(project, filepath.FromSlash(patchPath))
|
|
@@ -130,7 +147,7 @@ func RunCheckPerturb(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
130
147
|
|
|
131
148
|
probe := runPerturbProbe(project, identity, gate, patch, expect, timeout)
|
|
132
149
|
after := "unavailable"
|
|
133
|
-
if current, err :=
|
|
150
|
+
if current, err := load(); err != nil {
|
|
134
151
|
after += ": " + oneLine(err.Error())
|
|
135
152
|
} else {
|
|
136
153
|
after = current.digest
|
|
@@ -152,6 +169,20 @@ func RunCheckPerturb(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
152
169
|
return 0
|
|
153
170
|
}
|
|
154
171
|
|
|
172
|
+
// pathSetIdentity validates and hashes the path set in project, as candidate
|
|
173
|
+
// freeze does, for a candidate that has no manifest.
|
|
174
|
+
func pathSetIdentity(project string, paths []string) (candidateIdentity, error) {
|
|
175
|
+
rows, err := freezeRows(project, paths)
|
|
176
|
+
if err == nil {
|
|
177
|
+
err = validateCandidateRows(project, rows)
|
|
178
|
+
}
|
|
179
|
+
if err != nil {
|
|
180
|
+
return candidateIdentity{}, err
|
|
181
|
+
}
|
|
182
|
+
digest, err := hashCandidate(project, rows)
|
|
183
|
+
return candidateIdentity{digest: digest, rows: rows}, err
|
|
184
|
+
}
|
|
185
|
+
|
|
155
186
|
// perturbGate returns the live runnable gate id, refusing a CHECK/CWD that is
|
|
156
187
|
// not an approved test-plan.md preflight row: a probe never runs unvetted code.
|
|
157
188
|
func perturbGate(root, slug, id string) (*acceptance.Gate, error) {
|
|
@@ -236,6 +236,9 @@ func registeredWorktrees(repo string) (map[string]bool, error) {
|
|
|
236
236
|
return set, nil
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
// RegisteredWorktrees is registeredWorktrees for callers outside the package.
|
|
240
|
+
func RegisteredWorktrees(repo string) (map[string]bool, error) { return registeredWorktrees(repo) }
|
|
241
|
+
|
|
239
242
|
func resolvedPath(p string) string {
|
|
240
243
|
if r, err := filepath.EvalSymlinks(p); err == nil {
|
|
241
244
|
return r
|
package/engine/main.go
CHANGED
|
@@ -48,7 +48,7 @@ Usage:
|
|
|
48
48
|
Report near-duplicate code clusters that survive renaming (advisory)
|
|
49
49
|
devrites-engine check anchors <slug> <return-file> [--skills-root <dir>]
|
|
50
50
|
Anchor each Critical/Important row's quotes; Unverified or Deferred rows exit 3
|
|
51
|
-
devrites-engine check perturb <slug> --patch <file> --expect <test-name> --gate <G-ID> [--timeout N]
|
|
51
|
+
devrites-engine check perturb <slug> --patch <file> --expect <test-name> --gate <G-ID> [--timeout N] [--worktree <path> --slice <SLICE-ID>]
|
|
52
52
|
Run one mutant patch against a candidate copy; RED must name --expect
|
|
53
53
|
devrites-engine observe summary <slug> Emit sanitized JSON workspace summary
|
|
54
54
|
devrites-engine orient <slug> Alias for observe summary
|
|
@@ -60,7 +60,7 @@ Usage:
|
|
|
60
60
|
Emit one deduplicated read-set bundle for a phase, skill, or dispatch role
|
|
61
61
|
devrites-engine metrics record <slug> --phase <p> --event <e> [--role r] [--bytes n] [--note s]
|
|
62
62
|
devrites-engine metrics summary [slug] [--by-slice] Summarize the metrics ledger, or per-slice rounds and agent-minutes
|
|
63
|
-
devrites-engine candidate freeze <slug> --wave <w> (--slice <SLICE-ID> | --paths <path>...) [--base <ref>]
|
|
63
|
+
devrites-engine candidate freeze <slug> --wave <w> (--slice <SLICE-ID> | --paths <path>...) [--base <ref>] [--worktree <path>]
|
|
64
64
|
Freeze a review candidate to packets/<w>.diff + packets/<w>.sha256
|
|
65
65
|
devrites-engine packet <slug> --wave <w> --phase <p> --role <r> [--slice <SLICE-ID>] [--recheck] [--checks <G-ID,...>] [--focus <file>]
|
|
66
66
|
Render and lint one role's by-reference dispatch packet
|
|
@@ -105,10 +105,13 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
105
105
|
precondition is dropped from the batch and recorded as a gap, not dispatched.
|
|
106
106
|
2. Write lease; freeze `B=HEAD`; `parallel create` worktrees.
|
|
107
107
|
3. Dispatch ≤10 wrights in parallel (cwd=worktree; allowlist; prove `HEAD==B`;
|
|
108
|
-
one `--slice` per sibling on the wave).
|
|
108
|
+
one `--slice` per sibling on the wave). A linked worktree has no `.devrites`:
|
|
109
|
+
tell each wright to run `gates precheck` with `DEVRITES_ROOT=<control>/.devrites`.
|
|
109
110
|
4. Inspect each returned path list, cumulative diff from `B`, transfer identity,
|
|
110
111
|
and required result fields against the immutable contract. Reject malformed,
|
|
111
|
-
stale, or out-of-scope work.
|
|
112
|
+
stale, or out-of-scope work. Freeze and probe the sibling in its worktree:
|
|
113
|
+
`candidate freeze <slug> --wave <w> --slice <id> --base <B> --worktree <worktree_path>`,
|
|
114
|
+
`check perturb ... --worktree <worktree_path> --slice <id>`. Apply
|
|
112
115
|
[independent Build review](phase-contract.md#independent-build-review): first
|
|
113
116
|
full inventory, then bounded owning-role rechecks after repairs. Run approved
|
|
114
117
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
@@ -105,10 +105,13 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
105
105
|
precondition is dropped from the batch and recorded as a gap, not dispatched.
|
|
106
106
|
2. Write lease; freeze `B=HEAD`; `parallel create` worktrees.
|
|
107
107
|
3. Dispatch ≤10 wrights in parallel (cwd=worktree; allowlist; prove `HEAD==B`;
|
|
108
|
-
one `--slice` per sibling on the wave).
|
|
108
|
+
one `--slice` per sibling on the wave). A linked worktree has no `.devrites`:
|
|
109
|
+
tell each wright to run `gates precheck` with `DEVRITES_ROOT=<control>/.devrites`.
|
|
109
110
|
4. Inspect each returned path list, cumulative diff from `B`, transfer identity,
|
|
110
111
|
and required result fields against the immutable contract. Reject malformed,
|
|
111
|
-
stale, or out-of-scope work.
|
|
112
|
+
stale, or out-of-scope work. Freeze and probe the sibling in its worktree:
|
|
113
|
+
`candidate freeze <slug> --wave <w> --slice <id> --base <B> --worktree <worktree_path>`,
|
|
114
|
+
`check perturb ... --worktree <worktree_path> --slice <id>`. Apply
|
|
112
115
|
[independent Build review](phase-contract.md#independent-build-review): first
|
|
113
116
|
full inventory, then bounded owning-role rechecks after repairs. Run approved
|
|
114
117
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
@@ -105,10 +105,13 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
105
105
|
precondition is dropped from the batch and recorded as a gap, not dispatched.
|
|
106
106
|
2. Write lease; freeze `B=HEAD`; `parallel create` worktrees.
|
|
107
107
|
3. Dispatch ≤10 wrights in parallel (cwd=worktree; allowlist; prove `HEAD==B`;
|
|
108
|
-
one `--slice` per sibling on the wave).
|
|
108
|
+
one `--slice` per sibling on the wave). A linked worktree has no `.devrites`:
|
|
109
|
+
tell each wright to run `gates precheck` with `DEVRITES_ROOT=<control>/.devrites`.
|
|
109
110
|
4. Inspect each returned path list, cumulative diff from `B`, transfer identity,
|
|
110
111
|
and required result fields against the immutable contract. Reject malformed,
|
|
111
|
-
stale, or out-of-scope work.
|
|
112
|
+
stale, or out-of-scope work. Freeze and probe the sibling in its worktree:
|
|
113
|
+
`candidate freeze <slug> --wave <w> --slice <id> --base <B> --worktree <worktree_path>`,
|
|
114
|
+
`check perturb ... --worktree <worktree_path> --slice <id>`. Apply
|
|
112
115
|
[independent Build review](phase-contract.md#independent-build-review): first
|
|
113
116
|
full inventory, then bounded owning-role rechecks after repairs. Run approved
|
|
114
117
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
@@ -105,10 +105,13 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
105
105
|
precondition is dropped from the batch and recorded as a gap, not dispatched.
|
|
106
106
|
2. Write lease; freeze `B=HEAD`; `parallel create` worktrees.
|
|
107
107
|
3. Dispatch ≤10 wrights in parallel (cwd=worktree; allowlist; prove `HEAD==B`;
|
|
108
|
-
one `--slice` per sibling on the wave).
|
|
108
|
+
one `--slice` per sibling on the wave). A linked worktree has no `.devrites`:
|
|
109
|
+
tell each wright to run `gates precheck` with `DEVRITES_ROOT=<control>/.devrites`.
|
|
109
110
|
4. Inspect each returned path list, cumulative diff from `B`, transfer identity,
|
|
110
111
|
and required result fields against the immutable contract. Reject malformed,
|
|
111
|
-
stale, or out-of-scope work.
|
|
112
|
+
stale, or out-of-scope work. Freeze and probe the sibling in its worktree:
|
|
113
|
+
`candidate freeze <slug> --wave <w> --slice <id> --base <B> --worktree <worktree_path>`,
|
|
114
|
+
`check perturb ... --worktree <worktree_path> --slice <id>`. Apply
|
|
112
115
|
[independent Build review](phase-contract.md#independent-build-review): first
|
|
113
116
|
full inventory, then bounded owning-role rechecks after repairs. Run approved
|
|
114
117
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
@@ -105,10 +105,13 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
105
105
|
precondition is dropped from the batch and recorded as a gap, not dispatched.
|
|
106
106
|
2. Write lease; freeze `B=HEAD`; `parallel create` worktrees.
|
|
107
107
|
3. Dispatch ≤10 wrights in parallel (cwd=worktree; allowlist; prove `HEAD==B`;
|
|
108
|
-
one `--slice` per sibling on the wave).
|
|
108
|
+
one `--slice` per sibling on the wave). A linked worktree has no `.devrites`:
|
|
109
|
+
tell each wright to run `gates precheck` with `DEVRITES_ROOT=<control>/.devrites`.
|
|
109
110
|
4. Inspect each returned path list, cumulative diff from `B`, transfer identity,
|
|
110
111
|
and required result fields against the immutable contract. Reject malformed,
|
|
111
|
-
stale, or out-of-scope work.
|
|
112
|
+
stale, or out-of-scope work. Freeze and probe the sibling in its worktree:
|
|
113
|
+
`candidate freeze <slug> --wave <w> --slice <id> --base <B> --worktree <worktree_path>`,
|
|
114
|
+
`check perturb ... --worktree <worktree_path> --slice <id>`. Apply
|
|
112
115
|
[independent Build review](phase-contract.md#independent-build-review): first
|
|
113
116
|
full inventory, then bounded owning-role rechecks after repairs. Run approved
|
|
114
117
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
@@ -105,10 +105,13 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
105
105
|
precondition is dropped from the batch and recorded as a gap, not dispatched.
|
|
106
106
|
2. Write lease; freeze `B=HEAD`; `parallel create` worktrees.
|
|
107
107
|
3. Dispatch ≤10 wrights in parallel (cwd=worktree; allowlist; prove `HEAD==B`;
|
|
108
|
-
one `--slice` per sibling on the wave).
|
|
108
|
+
one `--slice` per sibling on the wave). A linked worktree has no `.devrites`:
|
|
109
|
+
tell each wright to run `gates precheck` with `DEVRITES_ROOT=<control>/.devrites`.
|
|
109
110
|
4. Inspect each returned path list, cumulative diff from `B`, transfer identity,
|
|
110
111
|
and required result fields against the immutable contract. Reject malformed,
|
|
111
|
-
stale, or out-of-scope work.
|
|
112
|
+
stale, or out-of-scope work. Freeze and probe the sibling in its worktree:
|
|
113
|
+
`candidate freeze <slug> --wave <w> --slice <id> --base <B> --worktree <worktree_path>`,
|
|
114
|
+
`check perturb ... --worktree <worktree_path> --slice <id>`. Apply
|
|
112
115
|
[independent Build review](phase-contract.md#independent-build-review): first
|
|
113
116
|
full inventory, then bounded owning-role rechecks after repairs. Run approved
|
|
114
117
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.3",
|
|
4
4
|
"description": "DevRites: a disciplined senior-engineer workflow pack for Claude Code, Codex, omp, pi, and Devin CLI",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://github.com/ViktorsBaikers/DevRites#readme",
|