devrites 6.0.0 → 6.0.2

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 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
+ ## [6.0.2](https://github.com/ViktorsBaikers/DevRites/compare/v6.0.1...v6.0.2) (2026-10-14)
6
+
7
+ ### Fixed
8
+
9
+ * **installer:** drop gh attestation requirement from installers ([ff27b58](https://github.com/ViktorsBaikers/DevRites/commit/ff27b58f569dffdc956ce4a4c759cb6427abc9bd))
10
+
11
+ ## [6.0.1](https://github.com/ViktorsBaikers/DevRites/compare/v6.0.0...v6.0.1) (2026-10-14)
12
+
13
+ ### Fixed
14
+
15
+ * **skills:** route .devrites writes to workflow artifacts ([#102](https://github.com/ViktorsBaikers/DevRites/issues/102)) ([8bb83b2](https://github.com/ViktorsBaikers/DevRites/commit/8bb83b21ef80cbcde3788e434830e38217297e31))
16
+
5
17
  ## [6.0.0](https://github.com/ViktorsBaikers/DevRites/compare/v5.14.4...v6.0.0) (2026-10-10)
6
18
 
7
19
  ### ⚠ BREAKING CHANGES
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:** [`v6.0.0`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v6.0.0): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
34
+ **Status:** [`v6.0.2`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v6.0.2): 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
 
@@ -51,11 +51,6 @@ This is the latest published release; `main` may contain unreleased work.
51
51
  install and run DevRites. Development in this repository (`npm ci`, which
52
52
  `.npmrc` runs with `engine-strict=true`) needs Node.js 22.22.2 or later within
53
53
  22.x, or 24.15 or later; Node 23.x and 24.0 through 24.14 fail with `EBADENGINE`.
54
- `gh` (GitHub CLI) must also be installed and authenticated, not merely
55
- installed, because DevRites verifies the build-provenance attestation of every
56
- release asset it downloads. Run `gh auth login` once, then `gh auth status` to
57
- confirm. See [Install, update, and remove](#install-update-and-remove) for
58
- which routes fail without it and which fall back to a local build.
59
54
 
60
55
  ```bash
61
56
  npx devrites@latest
@@ -315,8 +310,7 @@ Run `npx devrites@latest --help` for common flags and
315
310
  <br>
316
311
 
317
312
  Download the release-owned installer and its checksum before executing it. It
318
- needs `curl`, `gzip`, and `tar`, plus `gh` (GitHub CLI) installed and
319
- authenticated (`gh auth login`).
313
+ needs `curl`, `gzip`, and `tar`.
320
314
 
321
315
  ```bash
322
316
  bootstrap_dir="$(mktemp -d)"
@@ -349,8 +343,6 @@ project_dir="$PWD"
349
343
  exit 1
350
344
  fi
351
345
  [ "$got" = "$want" ] || { echo 'error: install.sh checksum mismatch' >&2; exit 1; }
352
- command -v gh >/dev/null 2>&1 || { echo 'error: gh (GitHub CLI) is required and was not found' >&2; exit 1; }
353
- gh attestation verify install.sh --repo ViktorsBaikers/DevRites --signer-workflow ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main || { if gh auth status >/dev/null 2>&1; then echo 'error: install.sh attestation verification failed: no build provenance from ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main' >&2; else echo 'error: gh is not authenticated; run gh auth login' >&2; fi; exit 1; }
354
346
 
355
347
  # Choose one Node-free operation:
356
348
  bash ./install.sh --target "$project_dir" # install into the directory you ran this from
@@ -403,59 +395,19 @@ proof. `devrites-engine migrate` owns deterministic v5 schema normalization.
403
395
  See the [CLI contract](docs/cli.md) and
404
396
  [ADR-0029](docs/adr/0029-v5-workspace-schema-and-native-migration.md).
405
397
 
406
- Every release asset is verified with `gh attestation verify` before it is
407
- decompressed, extracted, or executed, so `gh` must be installed and
408
- authenticated (`gh auth login`). Without that, the Bash bootstrap (`install.sh`
409
- fetched with `curl`) and `devrites-engine update` are fatal: they stop with no
410
- fallback. `npx devrites@latest` instead falls back to compiling the shipped
411
- `engine/` with `go build` when Go is installed, and the engine acquisition in
412
- `scripts/install-lib.sh` (used by `install.sh` from a clone or an extracted
413
- bundle) has its own fallback of the same kind.
414
-
415
- Release binaries and installers ship with SHA-256 sidecars and
416
- build-provenance attestations. Verify a download with both
417
- `shasum -a 256 -c <file>.sha256` and
398
+ Every release asset is checked against its exact-filename SHA-256 sidecar
399
+ before it is decompressed, extracted, or executed. The installers do not need
400
+ GitHub CLI and do not run `gh attestation verify`.
401
+
402
+ Release binaries and installers also ship with build-provenance attestations.
403
+ The `.sha256` sidecar is fetched from the same origin as the artifact, so it is
404
+ a corruption check, not a provenance one. To check provenance yourself, run
418
405
  `gh attestation verify <file> -R ViktorsBaikers/DevRites --signer-workflow ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`
419
- before it runs; the checksum alone is not enough (see the verified Bash
420
- bootstrap above).
421
- Pin the signer workflow, because valid provenance shows where a build ran
422
- but does not show that the intended publishing step produced it. See
406
+ on the downloaded file before it runs. This step is optional. Pin the signer
407
+ workflow, because valid provenance shows where a build ran but does not show
408
+ that the intended publishing step produced it. See
423
409
  [docs/release.md](docs/release.md).
424
410
 
425
- Read a failure as "prove provenance or stop", not as a verdict about your
426
- build. The bootstrap checks `gh` itself, exits 1 before `install.sh` runs, and
427
- prints the cause. It says `error: gh (GitHub CLI) is required and was not
428
- found` when `gh` is absent. When `gh` is present but logged out (`gh auth
429
- status` fails) it says `error: gh is not authenticated; run gh auth login`.
430
- Only when `gh` is logged in and still rejects the attestation does it say
431
- `error: install.sh attestation verification failed: no build provenance from
432
- ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`. After that,
433
- `install.sh` verifies the release bundle `devrites-<tag>.tar.gz` and reports the
434
- same three causes as `attestation verification failed: gh (GitHub CLI) is
435
- required and was not found`, `attestation verification failed: gh is not
436
- authenticated ...; run 'gh auth login' and retry`, and `attestation verification
437
- failed: no build provenance from <signer>`. The engine binary is acquired
438
- separately: the bundled installer (`scripts/install-lib.sh`) and the npm route
439
- report `attestation verification failed (gh must be installed and
440
- authenticated)` for any of these causes, discard the downloaded binary, and
441
- fall back to other sources. The bundled installer builds from the bundle's
442
- `engine/` source when `go` is available, then tries an already-installed
443
- `devrites-engine`, and only when none works does `install.sh` exit 1 with
444
- `could not acquire devrites-engine` followed by that cause. `npx devrites@latest` repeats the combined message
445
- before exiting 127 when no other engine is available. `devrites-engine update` reports `gh (GitHub CLI) is
446
- required and must be authenticated for attestation verification` when `gh` is
447
- absent, and `attestation verification failed for <asset>` whenever a present
448
- `gh` rejects the asset, logged out or not.
449
-
450
- There is no flag to skip, disable, or bypass attestation verification. The
451
- `.sha256` sidecar is fetched from the same origin as the artifact, so a
452
- substituted origin supplies a digest that matches its own payload, which is why
453
- the sidecar is a corruption check and not a provenance one. If you cannot run
454
- `gh auth login`, install Go and use `npx devrites@latest`, or clone this
455
- repository and run `bash install.sh`: both compile `engine/` locally when the
456
- download is refused, with `cd engine && go build -o devrites-engine .` as the
457
- manual equivalent.
458
-
459
411
  </details>
460
412
 
461
413
  ## Hosts
@@ -527,10 +479,9 @@ traces. DevRites detects these tools but does not install them.
527
479
 
528
480
  - Installed host artifacts stay in the project. Use `--no-binary` or
529
481
  `DEVRITES_NO_BINARY=1` to avoid keeping a shared binary outside it.
530
- - npm, Bash, and `devrites-engine update` all require checksummed release
531
- assets that also pass `gh attestation verify`, pinned to
532
- `ViktorsBaikers/DevRites` and signer workflow
533
- `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main` (see
482
+ - npm, Bash, and `devrites-engine update` all require release assets that
483
+ match their SHA-256 sidecars. Build-provenance attestations can be checked
484
+ manually with `gh attestation verify` (see
534
485
  [Install, update, and remove](#install-update-and-remove)). Remote fetches use HTTPS at every redirect hop, are bounded in size,
535
486
  and never fall back to an unchecked raw file, source archive, tag, or default
536
487
  branch.
package/SECURITY.md CHANGED
@@ -119,23 +119,20 @@ and `.codex/config.toml` without replacing unrelated user settings. The shared
119
119
  `devrites-engine` executable is the only allowed artifact outside the project.
120
120
  It is installed to `DEVRITES_BIN_DIR`, a writable `~/.local/bin`, or a writable
121
121
  `/usr/local/bin`; `--no-binary` / `DEVRITES_NO_BINARY=1` skips it. The bootstrap
122
- and direct updater may fetch the release bundle and checksummed engine assets,
123
- each of which must also pass `gh attestation verify` (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`).
122
+ and direct updater may fetch the release bundle and checksummed engine assets.
124
123
  They never invoke `sudo` or edit shell startup files.
125
124
 
126
125
  The documented Node-free boundary downloads the release-owned `install.sh` and
127
- its exact-name sidecar, then requires both the checksum and the build-provenance
128
- attestation (`gh attestation verify`, pinned to `ViktorsBaikers/DevRites` and
129
- signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`)
130
- to pass before execution, as in the README "verified Bash bootstrap"; mutable
126
+ its exact-name sidecar, then requires the checksum to pass before execution, as
127
+ in the README "verified Bash bootstrap"; mutable
131
128
  default-branch scripts are not recommended. Legacy local and extracted shim invocations remain compatible, but
132
129
  their exact-release guarantee begins at that verified asset. A piped shim never
133
130
  treats current-directory siblings as its bundle.
134
131
 
135
132
  Network acquisition accepts only an exact SemVer release asset and its mandatory
136
- exact-filename SHA-256 sidecar, and every release asset must also pass
137
- `gh attestation verify` (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`;
138
- `gh` must be installed and authenticated, with no bypass). Every redirect hop must remain HTTPS, private
133
+ exact-filename SHA-256 sidecar. Installers do not run `gh attestation verify`;
134
+ release assets carry build-provenance attestations that users may verify
135
+ manually (see [docs/release.md](docs/release.md)). Every redirect hop must remain HTTPS, private
139
136
  temporary-directory creation must succeed, and unchecked raw/source/default-
140
137
  branch fallbacks are absent. Representative in-stream ceilings are 1 MiB for
141
138
  release metadata, 4 KiB for a sidecar, and 64 MiB for an archive or binary, with
@@ -154,8 +151,7 @@ and [ADR-0028](docs/adr/0028-self-contained-engine-update.md) own the rationale.
154
151
  With `npx devrites@latest`, the CLI (`bin/devrites.mjs`) calls
155
152
  `devrites-engine` directly instead of running `install.sh`. The bundled host
156
153
  payload is pinned to the requested npm package version. The shim first tries the
157
- matching exact-release binary, which must pass its mandatory SHA-256 sidecar and
158
- `gh attestation verify` (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`),
154
+ matching exact-release binary, which must pass its mandatory SHA-256 sidecar,
159
155
  then a package-local Go build, and then an existing engine. Remote responses are
160
156
  byte-bounded and every redirect is checked before following it. It has no
161
157
  runtime npm dependencies. The same project and shared-binary boundaries apply
package/bin/devrites.mjs CHANGED
@@ -190,14 +190,6 @@ async function downloadEngine() {
190
190
  rmSync(`${out}.sha256`, { force: true });
191
191
  return null;
192
192
  }
193
- const signer = `${repo}/.github/workflows/ci.yml@refs/heads/main`;
194
- const attestation = spawnSync('gh', ['attestation', 'verify', out, '--repo', repo, '--signer-workflow', signer], { stdio: 'ignore', timeout: 120_000 });
195
- if (attestation.error || attestation.status !== 0) {
196
- acquisitionFailure = `release ${tag} asset ${name}: attestation verification failed (gh must be installed and authenticated)`;
197
- rmSync(out, { force: true });
198
- rmSync(`${out}.sha256`, { force: true });
199
- return null;
200
- }
201
193
  try {
202
194
  chmodSync(out, 0o755);
203
195
  } catch {
@@ -154,8 +154,7 @@ consume only local source, payload, and binary candidates.
154
154
 
155
155
  Direct `devrites-engine update` uses the separate `engine/internal/release`
156
156
  boundary to resolve the latest stable release and acquire its checksummed bundle
157
- and platform engine, each also checked with `gh attestation verify` (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`;
158
- `gh` must be installed and authenticated). It then invokes the downloaded engine with local paths, so
157
+ and platform engine. It then invokes the downloaded engine with local paths, so
159
158
  the candidate validates its own payload schema. `update --check` resolves only
160
159
  release metadata and downloads no assets. Engine `--to` and `--pre` selectors do
161
160
  not exist.
@@ -164,9 +163,7 @@ The shell entrypoints
164
163
  (`bin/devrites.mjs`) remain equivalent acquisition adapters. They acquire and
165
164
  verify the exact-release bundle or binary, then pass local paths and user flags
166
165
  through to the deterministic engine operation. All network paths require exact SemVer,
167
- HTTPS-only redirects, exact-filename SHA-256 sidecars, a `gh attestation verify`
168
- pass for every release asset (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`;
169
- see [Install, update, and remove](../README.md#install-update-and-remove)), private temporary
166
+ HTTPS-only redirects, exact-filename SHA-256 sidecars, private temporary
170
167
  directories, bounded transfers, and full archive preflight. They provide no
171
168
  unchecked raw, source-archive, tag, or default-branch fallback.
172
169
 
package/docs/cli.md CHANGED
@@ -76,11 +76,7 @@ Run `devrites-engine update` in an installed project to resolve the latest
76
76
  stable release and update the project pack plus shared engine binary. The
77
77
  command downloads the release bundle and platform engine, verifies both exact
78
78
  filenames against their SHA-256 sidecars, then hands the extracted candidate to
79
- the downloaded engine. Each release asset must also pass `gh attestation verify`,
80
- pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`,
81
- so `gh` must be installed and authenticated, with no bypass (see
82
- [Install, update, and remove](../README.md#install-update-and-remove)). This
83
- avoids asking an old engine to validate a newer payload schema.
79
+ the downloaded engine. This avoids asking an old engine to validate a newer payload schema.
84
80
 
85
81
  `devrites-engine update --check` resolves and compares release metadata without
86
82
  downloading assets. `--source-dir` and `--payload-dir` remain the advanced local
@@ -100,9 +96,8 @@ comparison without changing the installation. Use
100
96
  retaining the shared `devrites-engine` binary.
101
97
 
102
98
  If an older direct engine reports `missing codex/hooks.json` or asks for
103
- `--source-dir`, run `npx devrites@latest update`, or use a release `install.sh` that is both
104
- checksum-verified and attestation-verified (pinned repo and signer workflow, see
105
- the verified Bash bootstrap under
99
+ `--source-dir`, run `npx devrites@latest update`, or use a release `install.sh` that is
100
+ checksum-verified (see the verified Bash bootstrap under
106
101
  [README Install, update, and remove](../README.md#install-update-and-remove))
107
102
  and run `bash ./install.sh update` once to install a release with the
108
103
  self-contained updater.
@@ -271,9 +266,7 @@ commands refuse ambiguous, escaped, symlinked, or otherwise unsafe roots.
271
266
  Install, update application, and uninstall remain manifest-owned local
272
267
  operations. Direct engine update, shell, and npm may acquire a release candidate;
273
268
  all remote acquisition uses exact SemVer, HTTPS-only redirect hops, mandatory
274
- exact-filename SHA-256 sidecars, a `gh attestation verify` check of every release
275
- asset (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`;
276
- `gh` must be installed and authenticated, with no bypass), private temporary
269
+ exact-filename SHA-256 sidecars, private temporary
277
270
  directories, and byte/archive bounds, with no unchecked raw/source/default-branch
278
271
  fallback. Update accepts
279
272
  `--check` to compare the installed manifest version with the latest stable
@@ -74,8 +74,7 @@ Scans cap captured input at 64 MiB and entries/findings at 4,096; findings expos
74
74
  metadata only and source or limit errors fail closed.
75
75
 
76
76
  Direct `devrites-engine update` acquires the latest exact-release pack and
77
- platform binary, each verified by SHA-256 sidecar and `gh attestation verify`
78
- (`gh` installed and authenticated), then hands local paths to the downloaded engine. Shell/npm may
77
+ platform binary, each verified by SHA-256 sidecar, then hands local paths to the downloaded engine. Shell/npm may
79
78
  perform the same acquisition before invoking the local path. `--check` compares
80
79
  the installed version with latest release metadata without downloading assets;
81
80
  engine release selectors are unsupported.
package/docs/release.md CHANGED
@@ -31,9 +31,8 @@ the missing host payload before handing that local candidate to the engine. The
31
31
  engine itself only validates and copies host payloads; it never generates them.
32
32
  The npm entrypoint, verified release installer, and direct engine updater acquire
33
33
  only an exact-SemVer release bundle or platform binary with its mandatory
34
- exact-filename SHA-256 sidecar, and each such asset must also pass
35
- `gh attestation verify` (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`;
36
- `gh` must be installed and authenticated, with no bypass).
34
+ exact-filename SHA-256 sidecar. They do not run `gh attestation verify`; that
35
+ check is an optional manual step (below).
37
36
 
38
37
  Verify a downloaded engine binary from a GitHub Release:
39
38
 
@@ -45,7 +44,8 @@ curl -q -LO "https://github.com/ViktorsBaikers/DevRites/releases/download/v<vers
45
44
 
46
45
  Every release artifact also carries a GitHub artifact attestation (build
47
46
  provenance generated inside the release job, bound to the artifact digest).
48
- Verify it, pinning the signer workflow so an attestation minted from any other
47
+ The installers do not check it, and GitHub CLI is not needed to install.
48
+ To verify it manually, pin the signer workflow so an attestation minted from any other
49
49
  workflow path in this repository is rejected — provenance proves where a build
50
50
  ran, not that the publishing step was the intended one:
51
51
 
@@ -80,8 +80,7 @@ or path breach before extraction (at most 10,000 members, 4,096-byte paths, and
80
80
  256 MiB expanded files). Metadata is capped at 1 MiB, sidecars at 4 KiB,
81
81
  archives/binaries at 64 MiB, and the Node adapter follows at most five redirects.
82
82
  There is no raw, source-archive, tag, or default-branch acquisition fallback;
83
- exact-release guarantees begin at the checksummed release `install.sh` asset that
84
- also passes `gh attestation verify` (pinned to `ViktorsBaikers/DevRites` and signer workflow `ViktorsBaikers/DevRites/.github/workflows/ci.yml@refs/heads/main`).
83
+ exact-release guarantees begin at the checksummed release `install.sh` asset.
85
84
  The Go release boundary may acquire the latest stable candidate; the downloaded
86
85
  engine then supplies local paths to the manifest-owned update core. Engine
87
86
  `update --check` resolves release metadata but downloads no assets. `--to` and
@@ -16,7 +16,6 @@ import (
16
16
  "net/netip"
17
17
  "net/url"
18
18
  "os"
19
- "os/exec"
20
19
  "path"
21
20
  "path/filepath"
22
21
  "regexp"
@@ -98,7 +97,7 @@ func Acquire(ctx context.Context, repository, tag string) (Candidate, func(), er
98
97
  bundleName := "devrites-" + tag + ".tar.gz"
99
98
  bundleURL := base + "/" + bundleName
100
99
  bundlePath := filepath.Join(tmp, bundleName)
101
- if err := downloadVerified(ctx, bundleURL, bundlePath, bundleName, 0o644, repository); err != nil {
100
+ if err := downloadVerified(ctx, bundleURL, bundlePath, bundleName, 0o644); err != nil {
102
101
  return fail(fmt.Errorf("download release bundle: %w", err))
103
102
  }
104
103
  source, err := extractBundle(bundlePath, tmp, tag)
@@ -108,7 +107,7 @@ func Acquire(ctx context.Context, repository, tag string) (Candidate, func(), er
108
107
 
109
108
  engineURL := base + "/" + binaryName
110
109
  enginePath := filepath.Join(tmp, binaryName)
111
- if err := downloadVerified(ctx, engineURL, enginePath, binaryName, 0o755, repository); err != nil {
110
+ if err := downloadVerified(ctx, engineURL, enginePath, binaryName, 0o755); err != nil {
112
111
  return fail(fmt.Errorf("download engine binary: %w", err))
113
112
  }
114
113
  return Candidate{
@@ -185,7 +184,7 @@ func fetchJSON(ctx context.Context, rawURL string, out any) error {
185
184
  return nil
186
185
  }
187
186
 
188
- func downloadVerified(ctx context.Context, rawURL, destination, filename string, mode fs.FileMode, repository string) error {
187
+ func downloadVerified(ctx context.Context, rawURL, destination, filename string, mode fs.FileMode) error {
189
188
  if err := download(ctx, rawURL, destination, maxAssetBytes, mode); err != nil {
190
189
  return err
191
190
  }
@@ -223,15 +222,6 @@ func downloadVerified(ctx context.Context, rawURL, destination, filename string,
223
222
  if !strings.EqualFold(hex.EncodeToString(hash.Sum(nil)), hex.EncodeToString(want)) {
224
223
  return fmt.Errorf("checksum mismatch for %s", filename)
225
224
  }
226
- signer := repository + "/.github/workflows/ci.yml@refs/heads/main"
227
- // #nosec G204 -- fixed gh argv; repository is matched by repositoryPattern (leading alphanumeric, so never a flag) in Acquire and destination is an absolute path under the engine-created temp dir
228
- cmd := exec.CommandContext(ctx, "gh", "attestation", "verify", destination, "--repo", repository, "--signer-workflow", signer)
229
- if _, err := cmd.CombinedOutput(); err != nil {
230
- if errors.Is(err, exec.ErrNotFound) {
231
- return errors.New("gh (GitHub CLI) is required and must be authenticated for attestation verification")
232
- }
233
- return fmt.Errorf("attestation verification failed for %s: %w", filename, err)
234
- }
235
225
  return nil
236
226
  }
237
227
 
@@ -33,7 +33,9 @@ func TestLatestAndAcquireVerifiedRelease(t *testing.T) {
33
33
  bundleName: bundle,
34
34
  binaryName: engine,
35
35
  }
36
- installAttestationStandIn(t, bundle, engine)
36
+ if runtime.GOOS == "windows" {
37
+ t.Skip("positive-control engine is a shell script")
38
+ }
37
39
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
38
40
  if r.URL.Path == "/repos/owner/repo/releases/latest" {
39
41
  _, _ = fmt.Fprintf(w, `{"tag_name":%q}`, tag)
@@ -81,12 +83,12 @@ func TestLatestAndAcquireVerifiedRelease(t *testing.T) {
81
83
  cmd := exec.Command(candidate.EnginePath)
82
84
  cmd.Env = append(os.Environ(), "MARKER="+marker)
83
85
  if err := cmd.Run(); err == nil {
84
- t.Fatal("attested engine exited successfully; want positive-control exit 23")
86
+ t.Fatal("acquired engine exited successfully; want positive-control exit 23")
85
87
  } else if exit, ok := err.(*exec.ExitError); !ok || exit.ExitCode() != 23 {
86
- t.Fatalf("attested engine exit = %v, want 23", err)
88
+ t.Fatalf("acquired engine exit = %v, want 23", err)
87
89
  }
88
90
  if got, err := os.ReadFile(marker); err != nil || string(got) != "success" {
89
- t.Fatalf("attested engine marker = %q, %v", got, err)
91
+ t.Fatalf("acquired engine marker = %q, %v", got, err)
90
92
  }
91
93
  cleanup()
92
94
  if _, err := os.Stat(root); !os.IsNotExist(err) {
@@ -111,199 +113,6 @@ func TestValidTag(t *testing.T) {
111
113
  }
112
114
  }
113
115
 
114
- func TestAcquireRejectsOriginSubstitution(t *testing.T) {
115
- const tag = "v4.1.0"
116
- trusted := testBundle(t, tag, map[string]string{
117
- "package.json": `{"version":"4.1.0"}`,
118
- "pack/generated/claude/skills/rite/SKILL.md": "rite\n",
119
- })
120
- attacker := testBundle(t, tag, map[string]string{
121
- "package.json": `{"version":"4.1.0"}`,
122
- "pack/generated/claude/skills/rite/SKILL.md": "attacker\n",
123
- })
124
- binaryName, err := platformBinaryName()
125
- if err != nil {
126
- t.Skip(err)
127
- }
128
- installAttestationStandIn(t, trusted, []byte("trusted engine"))
129
- assets := map[string][]byte{"devrites-" + tag + ".tar.gz": attacker, binaryName: []byte("attacker engine")}
130
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
131
- name := filepath.Base(r.URL.Path)
132
- assetName := strings.TrimSuffix(name, ".sha256")
133
- asset, ok := assets[assetName]
134
- if !ok {
135
- http.NotFound(w, r)
136
- return
137
- }
138
- if strings.HasSuffix(name, ".sha256") {
139
- _, _ = fmt.Fprintf(w, "%x %s\n", sha256.Sum256(asset), assetName)
140
- return
141
- }
142
- _, _ = w.Write(asset)
143
- }))
144
- defer server.Close()
145
- oldWeb := webBaseURL
146
- webBaseURL = server.URL
147
- t.Cleanup(func() { webBaseURL = oldWeb })
148
-
149
- _, _, err = Acquire(context.Background(), "owner/repo", tag)
150
- if err == nil || !strings.Contains(err.Error(), "attestation verification failed") {
151
- t.Fatalf("Acquire error = %v, want attestation refusal despite a matching origin checksum", err)
152
- }
153
- }
154
-
155
- func TestAcquireRejectsSingleAssetSubstitution(t *testing.T) {
156
- const tag = "v4.1.0"
157
- files := map[string]string{
158
- "package.json": `{"version":"4.1.0"}`,
159
- "pack/generated/claude/skills/rite/SKILL.md": "rite\n",
160
- }
161
- trustedBundle := testBundle(t, tag, files)
162
- files["pack/generated/claude/skills/rite/SKILL.md"] = "attacker\n"
163
- attackerBundle := testBundle(t, tag, files)
164
- trustedEngine, attackerEngine := []byte("trusted engine"), []byte("attacker engine")
165
- binaryName, err := platformBinaryName()
166
- if err != nil {
167
- t.Skip(err)
168
- }
169
- for _, tc := range []struct {
170
- name, refusedAsset string
171
- bundle, engine []byte
172
- }{
173
- {"engine only", binaryName, trustedBundle, attackerEngine},
174
- {"bundle only", "devrites-" + tag + ".tar.gz", attackerBundle, trustedEngine},
175
- } {
176
- t.Run(tc.name, func(t *testing.T) {
177
- installAttestationStandIn(t, trustedBundle, trustedEngine)
178
- assets := map[string][]byte{"devrites-" + tag + ".tar.gz": tc.bundle, binaryName: tc.engine}
179
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
180
- name := filepath.Base(r.URL.Path)
181
- assetName := strings.TrimSuffix(name, ".sha256")
182
- asset, ok := assets[assetName]
183
- if !ok {
184
- http.NotFound(w, r)
185
- return
186
- }
187
- if strings.HasSuffix(name, ".sha256") {
188
- _, _ = fmt.Fprintf(w, "%x %s\n", sha256.Sum256(asset), assetName)
189
- return
190
- }
191
- _, _ = w.Write(asset)
192
- }))
193
- defer server.Close()
194
- oldWeb := webBaseURL
195
- webBaseURL = server.URL
196
- t.Cleanup(func() { webBaseURL = oldWeb })
197
-
198
- _, cleanup, err := Acquire(context.Background(), "owner/repo", tag)
199
- if err == nil {
200
- cleanup()
201
- t.Fatal("Acquire accepted a release with one unattested asset")
202
- }
203
- want := "attestation verification failed for " + tc.refusedAsset
204
- if !strings.Contains(err.Error(), want) {
205
- t.Fatalf("Acquire error = %v, want %q", err, want)
206
- }
207
- })
208
- }
209
- }
210
-
211
- func installAttestationStandIn(t *testing.T, trusted ...[]byte) {
212
- t.Helper()
213
- if runtime.GOOS == "windows" {
214
- t.Skip("attestation stand-in and positive-control engine are shell scripts")
215
- }
216
- dir := t.TempDir()
217
- var hashes strings.Builder
218
- for _, data := range trusted {
219
- fmt.Fprintf(&hashes, "%x\n", sha256.Sum256(data))
220
- }
221
- allowed := filepath.Join(dir, "allowed-digests")
222
- if err := os.WriteFile(allowed, []byte(hashes.String()), 0o600); err != nil {
223
- t.Fatal(err)
224
- }
225
- gh := filepath.Join(dir, "gh")
226
- script := `#!/bin/sh
227
- [ "$1" = attestation ] && [ "$2" = verify ] && [ "$4" = --repo ] && [ "$5" = owner/repo ] && [ "$6" = --signer-workflow ] && [ "$7" = owner/repo/.github/workflows/ci.yml@refs/heads/main ] || exit 64
228
- got=$(shasum -a 256 "$3" | awk '{print $1}') || exit 65
229
- grep -Fxq "$got" "$ATTESTED_DIGESTS"
230
- `
231
- if err := os.WriteFile(gh, []byte(script), 0o700); err != nil {
232
- t.Fatal(err)
233
- }
234
- t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
235
- t.Setenv("ATTESTED_DIGESTS", allowed)
236
- }
237
-
238
- func TestAttestationStandInIsStrict(t *testing.T) {
239
- trusted := []byte("trusted engine")
240
- installAttestationStandIn(t, trusted, []byte("other"))
241
- dir := t.TempDir()
242
- good := filepath.Join(dir, "good")
243
- bad := filepath.Join(dir, "bad")
244
- if err := os.WriteFile(good, trusted, 0o600); err != nil {
245
- t.Fatal(err)
246
- }
247
- if err := os.WriteFile(bad, []byte("attacker engine"), 0o600); err != nil {
248
- t.Fatal(err)
249
- }
250
- const signer = "owner/repo/.github/workflows/ci.yml@refs/heads/main"
251
- run := func(file, repo, workflow string) error {
252
- return exec.Command("gh", "attestation", "verify", file, "--repo", repo, "--signer-workflow", workflow).Run()
253
- }
254
- if err := run(good, "owner/repo", signer); err != nil {
255
- t.Fatalf("stand-in rejected the pinned request: %v", err)
256
- }
257
- if run(good, "other/repo", signer) == nil {
258
- t.Error("stand-in accepted a wrong repo")
259
- }
260
- if run(good, "owner/repo", "owner/repo/.github/workflows/other.yml@refs/heads/main") == nil {
261
- t.Error("stand-in accepted a wrong signer workflow")
262
- }
263
- if run(bad, "owner/repo", signer) == nil {
264
- t.Error("stand-in accepted an unattested digest")
265
- }
266
- }
267
-
268
- func TestAcquireRejectsMissingAttestationVerifier(t *testing.T) {
269
- const tag = "v4.1.0"
270
- binaryName, err := platformBinaryName()
271
- if err != nil {
272
- t.Skip(err)
273
- }
274
- assets := map[string][]byte{
275
- "devrites-" + tag + ".tar.gz": testBundle(t, tag, map[string]string{
276
- "package.json": `{"version":"4.1.0"}`,
277
- "pack/generated/claude/skills/rite/SKILL.md": "rite\n",
278
- }),
279
- binaryName: []byte("engine"),
280
- }
281
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
282
- name := filepath.Base(r.URL.Path)
283
- assetName := strings.TrimSuffix(name, ".sha256")
284
- asset, ok := assets[assetName]
285
- if !ok {
286
- http.NotFound(w, r)
287
- return
288
- }
289
- if strings.HasSuffix(name, ".sha256") {
290
- _, _ = fmt.Fprintf(w, "%x %s\n", sha256.Sum256(asset), assetName)
291
- return
292
- }
293
- _, _ = w.Write(asset)
294
- }))
295
- defer server.Close()
296
- oldWeb := webBaseURL
297
- webBaseURL = server.URL
298
- t.Cleanup(func() { webBaseURL = oldWeb })
299
- t.Setenv("PATH", t.TempDir())
300
-
301
- _, _, err = Acquire(context.Background(), "owner/repo", tag)
302
- if err == nil || !strings.Contains(err.Error(), "gh (GitHub CLI) is required") {
303
- t.Fatalf("Acquire error = %v, want refusal when gh is not installed", err)
304
- }
305
- }
306
-
307
116
  func TestAcquireRejectsChecksumMismatch(t *testing.T) {
308
117
  const tag = "v4.1.0"
309
118
  bundleName := "devrites-" + tag + ".tar.gz"
package/install.sh CHANGED
@@ -13,8 +13,6 @@ DEVRITES_REPO="${DEVRITES_REPO:-ViktorsBaikers/DevRites}"
13
13
  DEVRITES_REF="${DEVRITES_REF:-}"
14
14
 
15
15
  BOOTSTRAP_DIR=""
16
- RUN_JOB=""
17
- RUN_WATCH=""
18
16
  BOOTSTRAP_MAX_METADATA=1048576
19
17
  BOOTSTRAP_MAX_SIDECAR=4096
20
18
  BOOTSTRAP_MAX_ARCHIVE=67108864
@@ -140,114 +138,6 @@ verify_sha256() {
140
138
  [ -n "$want" ] && [ -n "$got" ] && [ "$got" = "$want" ]
141
139
  }
142
140
 
143
- # Runs a command for at most $1 seconds with output discarded. Past the limit it
144
- # is sent SIGTERM and, if it is still alive two seconds later, SIGKILL. A command
145
- # stopped this way sets RUN_TIMED_OUT=1 and returns 124; any other failure keeps
146
- # the command's own status, so a crash is never mistaken for a timeout. Same
147
- # watchdog idea as scripts/install-lib.sh dr_run_bounded, which is not available
148
- # until the bundle is verified and extracted. The timeout marker lives in
149
- # $BOOTSTRAP_DIR, which the EXIT trap removes, so a run without that directory
150
- # fails closed; stop_bounded ends a command and watchdog still in flight when the
151
- # bootstrap is interrupted.
152
- run_bounded() {
153
- limit="$1"; shift
154
- RUN_TIMED_OUT=0
155
- [ -n "$BOOTSTRAP_DIR" ] || return 125
156
- fired="$(mktemp "$BOOTSTRAP_DIR/bound.XXXXXX" 2>/dev/null)" || return 125
157
- "$@" >/dev/null 2>&1 </dev/null &
158
- job=$!
159
- RUN_JOB=$job
160
- {
161
- trap 'kill "$nap"; exit 0' TERM
162
- sleep "$limit" & nap=$!; wait "$nap"
163
- kill -0 "$job" || exit 0
164
- echo 1 >"$fired"
165
- kill "$job"
166
- sleep 2 & nap=$!; wait "$nap"
167
- kill -9 "$job"
168
- } >/dev/null 2>&1 </dev/null &
169
- watch=$!
170
- RUN_WATCH=$watch
171
- wait "$job" 2>/dev/null
172
- rc=$?
173
- kill "$watch" >/dev/null 2>&1
174
- wait "$watch" 2>/dev/null
175
- RUN_JOB=""; RUN_WATCH=""
176
- if [ "$rc" -ne 0 ] && [ -s "$fired" ]; then
177
- RUN_TIMED_OUT=1
178
- rc=124
179
- fi
180
- rm -f "$fired"
181
- return "$rc"
182
- }
183
-
184
- stop_bounded() {
185
- # A signal can land after a job is forked but before its PID is recorded, so
186
- # every background job is ended, not only the recorded ones. run_bounded is the
187
- # only place this script backgrounds anything.
188
- for pid in $(jobs -p); do
189
- [ "$pid" = "$RUN_WATCH" ] || kill -9 "$pid" >/dev/null 2>&1
190
- done
191
- [ -z "$RUN_WATCH" ] || kill "$RUN_WATCH" >/dev/null 2>&1
192
- RUN_JOB=""; RUN_WATCH=""
193
- }
194
-
195
- # The checksum sidecar above travels with the tarball it vouches for: it is
196
- # fetched from "$url.sha256", one suffix away from the artifact's own origin. An
197
- # attacker who substitutes the release origin therefore supplies both the
198
- # payload and a digest that matches it, and the digest check passes. The
199
- # release job already mints the proof that does not travel with the artifact --
200
- # a build-provenance attestation signed by that job's OIDC identity and bound to
201
- # the artifact digest (.github/workflows/ci.yml, 'Attest release artifacts').
202
- # Verify it here, pinned to the repository and to the signer workflow, so an
203
- # attestation minted by any other workflow in this repository is rejected too.
204
- #
205
- # There is deliberately no bypass for a missing verifier: without gh there is no
206
- # proof of origin, and the sidecar is not a substitute for one.
207
- #
208
- # The lookup is bounded: DEVRITES_ATTEST_TIMEOUT is a whole number of seconds
209
- # (1 to 99999, default 120). Anything else is ignored with a warning and the
210
- # default applies, so a bad value can never remove the bound. The follow-up
211
- # `gh auth status` is bounded by 30 seconds or that limit, whichever is shorter.
212
- # A timeout, a crash and a plain rejection each fail closed and are reported as
213
- # what they are.
214
- verify_attestation() {
215
- file="$1"
216
- signer="$DEVRITES_REPO/.github/workflows/ci.yml@refs/heads/main"
217
- ATTESTATION_FAILURE=""
218
- if ! command -v gh >/dev/null 2>&1; then
219
- ATTESTATION_FAILURE="attestation verification failed: gh (GitHub CLI) is required and was not found"
220
- return 1
221
- fi
222
- attest_limit=120
223
- case "${DEVRITES_ATTEST_TIMEOUT:-}" in
224
- "") ;;
225
- *[!0-9]* | 0* | [0-9][0-9][0-9][0-9][0-9][0-9]*)
226
- echo "warning: ignoring invalid DEVRITES_ATTEST_TIMEOUT '$DEVRITES_ATTEST_TIMEOUT' (expected whole seconds, 1 to 99999); using $attest_limit" >&2
227
- ;;
228
- *) attest_limit="$DEVRITES_ATTEST_TIMEOUT" ;;
229
- esac
230
- auth_limit=30
231
- [ "$attest_limit" -lt "$auth_limit" ] && auth_limit="$attest_limit"
232
- verify_rc=0
233
- run_bounded "$attest_limit" gh attestation verify "$file" --repo "$DEVRITES_REPO" \
234
- --signer-workflow "$signer" || verify_rc=$?
235
- if [ "$verify_rc" -ne 0 ]; then
236
- if [ "$RUN_TIMED_OUT" -eq 1 ]; then
237
- ATTESTATION_FAILURE="attestation verification timed out after ${attest_limit}s; check connectivity or raise DEVRITES_ATTEST_TIMEOUT and retry"
238
- elif [ "$verify_rc" -ge 128 ]; then
239
- ATTESTATION_FAILURE="attestation verification failed: gh exited with status $verify_rc"
240
- elif run_bounded "$auth_limit" gh auth status; then
241
- ATTESTATION_FAILURE="attestation verification failed: no build provenance from $signer"
242
- elif [ "$RUN_TIMED_OUT" -eq 1 ]; then
243
- ATTESTATION_FAILURE="attestation verification timed out: gh auth status did not answer within ${auth_limit}s; check connectivity or raise DEVRITES_ATTEST_TIMEOUT and retry"
244
- else
245
- ATTESTATION_FAILURE="attestation verification failed: gh is not authenticated (attestation lookup requires a GitHub login); run 'gh auth login' and retry"
246
- fi
247
- return 1
248
- fi
249
- }
250
-
251
141
  preflight_archive() {
252
142
  archive_path="$1"
253
143
  tag="$2"
@@ -324,7 +214,7 @@ bootstrap_bundle() {
324
214
  exit 1
325
215
  }
326
216
  umask "$old_umask"
327
- trap 'stop_bounded; exit 1' HUP INT TERM
217
+ trap 'exit 1' HUP INT TERM
328
218
  trap 'rm -rf "$BOOTSTRAP_DIR"' EXIT
329
219
  if [ -n "$DEVRITES_REF" ]; then
330
220
  tag="$(normalize_release_tag "$DEVRITES_REF")" || {
@@ -361,11 +251,6 @@ bootstrap_bundle() {
361
251
  echo "error: release $tag asset $asset: checksum failed." >&2
362
252
  exit 1
363
253
  }
364
- verify_attestation "$archive" || {
365
- rm -f "$archive" "$sidecar"
366
- echo "error: release $tag asset $asset: ${ATTESTATION_FAILURE:-attestation verification failed}." >&2
367
- exit 1
368
- }
369
254
  uncompressed="$BOOTSTRAP_DIR/devrites-$tag.tar"
370
255
  bounded_decompress "$archive" "$uncompressed" "$BOOTSTRAP_MAX_UNCOMPRESSED" || {
371
256
  rm -f "$archive" "$sidecar"
@@ -68,6 +68,8 @@ rejected result, stop, or a launch refused with no handle — run
68
68
  `devrites-engine claim release --session <id> --id <claim-id>`. Claims expire
69
69
  by TTL only as crash recovery; `claim list` shows who holds what. Claims are
70
70
  advisory coordination, never a substitute for the one-writer-per-worktree rule.
71
+ Claims cover product paths only; root-authored `.devrites/**` files use
72
+ [`workflow-artifacts.md`](workflow-artifacts.md) admission instead.
71
73
  Expiry is not proof the holder stopped: size `claim add --ttl <minutes>` to the
72
74
  expected write (engine max 240) and re-add before it lapses; before writing, also
73
75
  read `claim list --all` — a foreign `expired` claim on overlapping paths whose
@@ -75,7 +75,9 @@ Same-worktree (non-isolated) dispatch claims before writing: `devrites-engine
75
75
  claim add --session <id> <task-paths>`; retain its returned `<claim-id>`, then run
76
76
  `devrites-engine claim check --session <id> <task-paths>` — a held claim from a
77
77
  live foreign session releases the retained claim and stops the dispatch, per
78
- [`agents.md`](../../devrites-lib/reference/standards/agents.md). Ask the host
78
+ [`agents.md`](../../devrites-lib/reference/standards/agents.md). `.devrites/**`
79
+ is never a task path; see [Workflow Artifact admission](../../devrites-lib/reference/standards/workflow-artifacts.md).
80
+ Ask the host
79
81
  for the exact writer in fresh context and wait. Track it through the
80
82
  launch barrier — `devrites-engine dispatch <slug> open --phase build --wave
81
83
  <slice-id> --role slice-wright`, then `start` with the host handle, `seal`, and
@@ -68,6 +68,8 @@ rejected result, stop, or a launch refused with no handle — run
68
68
  `devrites-engine claim release --session <id> --id <claim-id>`. Claims expire
69
69
  by TTL only as crash recovery; `claim list` shows who holds what. Claims are
70
70
  advisory coordination, never a substitute for the one-writer-per-worktree rule.
71
+ Claims cover product paths only; root-authored `.devrites/**` files use
72
+ [`workflow-artifacts.md`](workflow-artifacts.md) admission instead.
71
73
  Expiry is not proof the holder stopped: size `claim add --ttl <minutes>` to the
72
74
  expected write (engine max 240) and re-add before it lapses; before writing, also
73
75
  read `claim list --all` — a foreign `expired` claim on overlapping paths whose
@@ -75,7 +75,9 @@ Same-worktree (non-isolated) dispatch claims before writing: `devrites-engine
75
75
  claim add --session <id> <task-paths>`; retain its returned `<claim-id>`, then run
76
76
  `devrites-engine claim check --session <id> <task-paths>` — a held claim from a
77
77
  live foreign session releases the retained claim and stops the dispatch, per
78
- [`agents.md`](../../devrites-lib/reference/standards/agents.md). Ask the host
78
+ [`agents.md`](../../devrites-lib/reference/standards/agents.md). `.devrites/**`
79
+ is never a task path; see [Workflow Artifact admission](../../devrites-lib/reference/standards/workflow-artifacts.md).
80
+ Ask the host
79
81
  for the exact writer in fresh context and wait. Track it through the
80
82
  launch barrier — `devrites-engine dispatch <slug> open --phase build --wave
81
83
  <slice-id> --role slice-wright`, then `start` with the host handle, `seal`, and
@@ -68,6 +68,8 @@ rejected result, stop, or a launch refused with no handle — run
68
68
  `devrites-engine claim release --session <id> --id <claim-id>`. Claims expire
69
69
  by TTL only as crash recovery; `claim list` shows who holds what. Claims are
70
70
  advisory coordination, never a substitute for the one-writer-per-worktree rule.
71
+ Claims cover product paths only; root-authored `.devrites/**` files use
72
+ [`workflow-artifacts.md`](workflow-artifacts.md) admission instead.
71
73
  Expiry is not proof the holder stopped: size `claim add --ttl <minutes>` to the
72
74
  expected write (engine max 240) and re-add before it lapses; before writing, also
73
75
  read `claim list --all` — a foreign `expired` claim on overlapping paths whose
@@ -75,7 +75,9 @@ Same-worktree (non-isolated) dispatch claims before writing: `devrites-engine
75
75
  claim add --session <id> <task-paths>`; retain its returned `<claim-id>`, then run
76
76
  `devrites-engine claim check --session <id> <task-paths>` — a held claim from a
77
77
  live foreign session releases the retained claim and stops the dispatch, per
78
- [`agents.md`](../../devrites-lib/reference/standards/agents.md). Ask the host
78
+ [`agents.md`](../../devrites-lib/reference/standards/agents.md). `.devrites/**`
79
+ is never a task path; see [Workflow Artifact admission](../../devrites-lib/reference/standards/workflow-artifacts.md).
80
+ Ask the host
79
81
  for the exact writer in fresh context and wait. Track it through the
80
82
  launch barrier — `devrites-engine dispatch <slug> open --phase build --wave
81
83
  <slice-id> --role slice-wright`, then `start` with the host handle, `seal`, and
@@ -68,6 +68,8 @@ rejected result, stop, or a launch refused with no handle — run
68
68
  `devrites-engine claim release --session <id> --id <claim-id>`. Claims expire
69
69
  by TTL only as crash recovery; `claim list` shows who holds what. Claims are
70
70
  advisory coordination, never a substitute for the one-writer-per-worktree rule.
71
+ Claims cover product paths only; root-authored `.devrites/**` files use
72
+ [`workflow-artifacts.md`](workflow-artifacts.md) admission instead.
71
73
  Expiry is not proof the holder stopped: size `claim add --ttl <minutes>` to the
72
74
  expected write (engine max 240) and re-add before it lapses; before writing, also
73
75
  read `claim list --all` — a foreign `expired` claim on overlapping paths whose
@@ -75,7 +75,9 @@ Same-worktree (non-isolated) dispatch claims before writing: `devrites-engine
75
75
  claim add --session <id> <task-paths>`; retain its returned `<claim-id>`, then run
76
76
  `devrites-engine claim check --session <id> <task-paths>` — a held claim from a
77
77
  live foreign session releases the retained claim and stops the dispatch, per
78
- [`agents.md`](../../devrites-lib/reference/standards/agents.md). Ask the host
78
+ [`agents.md`](../../devrites-lib/reference/standards/agents.md). `.devrites/**`
79
+ is never a task path; see [Workflow Artifact admission](../../devrites-lib/reference/standards/workflow-artifacts.md).
80
+ Ask the host
79
81
  for the exact writer in fresh context and wait. Track it through the
80
82
  launch barrier — `devrites-engine dispatch <slug> open --phase build --wave
81
83
  <slice-id> --role slice-wright`, then `start` with the host handle, `seal`, and
@@ -68,6 +68,8 @@ rejected result, stop, or a launch refused with no handle — run
68
68
  `devrites-engine claim release --session <id> --id <claim-id>`. Claims expire
69
69
  by TTL only as crash recovery; `claim list` shows who holds what. Claims are
70
70
  advisory coordination, never a substitute for the one-writer-per-worktree rule.
71
+ Claims cover product paths only; root-authored `.devrites/**` files use
72
+ [`workflow-artifacts.md`](workflow-artifacts.md) admission instead.
71
73
  Expiry is not proof the holder stopped: size `claim add --ttl <minutes>` to the
72
74
  expected write (engine max 240) and re-add before it lapses; before writing, also
73
75
  read `claim list --all` — a foreign `expired` claim on overlapping paths whose
@@ -75,7 +75,9 @@ Same-worktree (non-isolated) dispatch claims before writing: `devrites-engine
75
75
  claim add --session <id> <task-paths>`; retain its returned `<claim-id>`, then run
76
76
  `devrites-engine claim check --session <id> <task-paths>` — a held claim from a
77
77
  live foreign session releases the retained claim and stops the dispatch, per
78
- [`agents.md`](../../devrites-lib/reference/standards/agents.md). Ask the host
78
+ [`agents.md`](../../devrites-lib/reference/standards/agents.md). `.devrites/**`
79
+ is never a task path; see [Workflow Artifact admission](../../devrites-lib/reference/standards/workflow-artifacts.md).
80
+ Ask the host
79
81
  for the exact writer in fresh context and wait. Track it through the
80
82
  launch barrier — `devrites-engine dispatch <slug> open --phase build --wave
81
83
  <slice-id> --role slice-wright`, then `start` with the host handle, `seal`, and
@@ -68,6 +68,8 @@ rejected result, stop, or a launch refused with no handle — run
68
68
  `devrites-engine claim release --session <id> --id <claim-id>`. Claims expire
69
69
  by TTL only as crash recovery; `claim list` shows who holds what. Claims are
70
70
  advisory coordination, never a substitute for the one-writer-per-worktree rule.
71
+ Claims cover product paths only; root-authored `.devrites/**` files use
72
+ [`workflow-artifacts.md`](workflow-artifacts.md) admission instead.
71
73
  Expiry is not proof the holder stopped: size `claim add --ttl <minutes>` to the
72
74
  expected write (engine max 240) and re-add before it lapses; before writing, also
73
75
  read `claim list --all` — a foreign `expired` claim on overlapping paths whose
@@ -75,7 +75,9 @@ Same-worktree (non-isolated) dispatch claims before writing: `devrites-engine
75
75
  claim add --session <id> <task-paths>`; retain its returned `<claim-id>`, then run
76
76
  `devrites-engine claim check --session <id> <task-paths>` — a held claim from a
77
77
  live foreign session releases the retained claim and stops the dispatch, per
78
- [`agents.md`](../../devrites-lib/reference/standards/agents.md). Ask the host
78
+ [`agents.md`](../../devrites-lib/reference/standards/agents.md). `.devrites/**`
79
+ is never a task path; see [Workflow Artifact admission](../../devrites-lib/reference/standards/workflow-artifacts.md).
80
+ Ask the host
79
81
  for the exact writer in fresh context and wait. Track it through the
80
82
  launch barrier — `devrites-engine dispatch <slug> open --phase build --wave
81
83
  <slice-id> --role slice-wright`, then `start` with the host handle, `seal`, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "6.0.0",
3
+ "version": "6.0.2",
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",
@@ -100,20 +100,6 @@ dr_verify_checksum() {
100
100
  [ "$_dr_got" = "$_dr_want" ]
101
101
  }
102
102
 
103
- # Runs a command for at most $1 seconds with output discarded; a timeout is a failure.
104
- dr_run_bounded() {
105
- _dr_limit="$1"; shift
106
- "$@" >/dev/null 2>&1 </dev/null &
107
- _dr_job=$!
108
- { trap 'kill "$_dr_nap"; exit 0' TERM; sleep "$_dr_limit" & _dr_nap=$!; wait "$_dr_nap" && kill "$_dr_job"; } >/dev/null 2>&1 </dev/null &
109
- _dr_watch=$!
110
- wait "$_dr_job" 2>/dev/null
111
- _dr_rc=$?
112
- kill "$_dr_watch" >/dev/null 2>&1
113
- wait "$_dr_watch" 2>/dev/null
114
- return "$_dr_rc"
115
- }
116
-
117
103
  dr_download_engine() {
118
104
  _dr_source_dir="$1"
119
105
  _dr_repo="$2"
@@ -138,8 +124,6 @@ dr_download_engine() {
138
124
  dr_bounded_download "$_dr_url" "$_dr_engine_out" 67108864 || { DR_ACQUIRE_FAILURE="release $_dr_tag asset $_dr_asset: ${DR_DOWNLOAD_FAILURE:-download} failed"; return 1; }
139
125
  dr_bounded_download "$_dr_url.sha256" "$_dr_engine_out.sha256" 4096 || { DR_ACQUIRE_FAILURE="release $_dr_tag asset $_dr_asset.sha256: ${DR_DOWNLOAD_FAILURE:-download} failed"; rm -f "$_dr_engine_out"; return 1; }
140
126
  dr_verify_checksum "$_dr_engine_out" "$_dr_engine_out.sha256" "$_dr_asset" || { DR_ACQUIRE_FAILURE="release $_dr_tag asset $_dr_asset: checksum failed"; rm -f "$_dr_engine_out" "$_dr_engine_out.sha256"; return 1; }
141
- _dr_signer="$_dr_repo/.github/workflows/ci.yml@refs/heads/main"
142
- command -v gh >/dev/null 2>&1 && dr_run_bounded "${DR_ATTEST_TIMEOUT:-120}" gh attestation verify "$_dr_engine_out" --repo "$_dr_repo" --signer-workflow "$_dr_signer" || { DR_ACQUIRE_FAILURE="release $_dr_tag asset $_dr_asset: attestation verification failed (gh must be installed and authenticated)"; rm -f "$_dr_engine_out" "$_dr_engine_out.sha256"; return 1; }
143
127
  chmod +x "$_dr_engine_out" || { DR_ACQUIRE_FAILURE="release $_dr_tag asset $_dr_asset: executable preparation failed"; rm -f "$_dr_engine_out" "$_dr_engine_out.sha256"; return 1; }
144
128
  }
145
129