devrites 6.1.1 → 6.1.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 +6 -0
- package/README.md +1 -1
- package/docs/cli.md +4 -1
- package/docs/engine/commands.md +4 -4
- package/docs/orchestration.md +4 -3
- package/docs/usage.md +1 -1
- package/engine/internal/lib/claims.go +12 -2
- package/engine/internal/lib/claims_holder.go +124 -0
- package/engine/internal/lib/cli_observe.go +14 -0
- package/engine/internal/lib/taskgraph.go +124 -0
- package/engine/internal/parallel/cli.go +111 -9
- package/engine/internal/parallel/lease.go +58 -4
- package/engine/internal/parallel/ops.go +244 -75
- package/engine/internal/parallel/select.go +31 -6
- package/pack/.claude/agents/devrites-plan-drafter.md +2 -1
- package/pack/.claude/agents/devrites-slice-wright.md +2 -1
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/.claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +2 -2
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +1 -1
- package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
- package/pack/.claude/skills/rite-build/SKILL.md +7 -7
- package/pack/.claude/skills/rite-build/reference/afk-discipline.md +2 -2
- package/pack/.claude/skills/rite-build/reference/checkpoint.md +1 -1
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +1 -1
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +28 -21
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +10 -1
- package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +3 -2
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +2 -2
- package/pack/.claude/skills/rite-define/SKILL.md +1 -1
- package/pack/.claude/skills/rite-handoff/SKILL.md +1 -1
- package/pack/.claude/skills/rite-plan/reference/slicing.md +5 -0
- package/pack/.claude/skills/rite-vet/reference/artifacts.md +8 -0
- package/pack/.claude/skills/rite-vet/reference/review-axes.md +1 -1
- package/pack/generated/claude/agents/devrites-plan-drafter.md +2 -1
- package/pack/generated/claude/agents/devrites-slice-wright.md +2 -1
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/generated/claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +2 -2
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +1 -1
- package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
- package/pack/generated/claude/skills/rite-build/SKILL.md +7 -7
- package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +2 -2
- package/pack/generated/claude/skills/rite-build/reference/checkpoint.md +1 -1
- package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +1 -1
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +28 -21
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +10 -1
- package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +3 -2
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +2 -2
- package/pack/generated/claude/skills/rite-define/SKILL.md +1 -1
- package/pack/generated/claude/skills/rite-handoff/SKILL.md +1 -1
- package/pack/generated/claude/skills/rite-plan/reference/slicing.md +5 -0
- package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +8 -0
- package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +1 -1
- package/pack/generated/codex/agents/devrites-plan-drafter.toml +2 -1
- package/pack/generated/codex/agents/devrites-slice-wright.toml +2 -1
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/generated/codex/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +2 -2
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +1 -1
- package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
- package/pack/generated/codex/skills/rite-build/SKILL.md +7 -7
- package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +2 -2
- package/pack/generated/codex/skills/rite-build/reference/checkpoint.md +1 -1
- package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +1 -1
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +28 -21
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +10 -1
- package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +3 -2
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +2 -2
- package/pack/generated/codex/skills/rite-define/SKILL.md +1 -1
- package/pack/generated/codex/skills/rite-handoff/SKILL.md +1 -1
- package/pack/generated/codex/skills/rite-plan/reference/slicing.md +5 -0
- package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +8 -0
- package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +1 -1
- package/pack/generated/devin/agents/devrites-plan-drafter.md +2 -1
- package/pack/generated/devin/agents/devrites-slice-wright.md +2 -1
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
- package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
- package/pack/generated/devin/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/generated/devin/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
- package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
- package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +2 -2
- package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +1 -1
- package/pack/generated/devin/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
- package/pack/generated/devin/skills/rite-build/SKILL.md +7 -7
- package/pack/generated/devin/skills/rite-build/reference/afk-discipline.md +2 -2
- package/pack/generated/devin/skills/rite-build/reference/checkpoint.md +1 -1
- package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +1 -1
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +28 -21
- package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +10 -1
- package/pack/generated/devin/skills/rite-build/reference/spec-drift-guard.md +3 -2
- package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +2 -2
- package/pack/generated/devin/skills/rite-define/SKILL.md +1 -1
- package/pack/generated/devin/skills/rite-handoff/SKILL.md +1 -1
- package/pack/generated/devin/skills/rite-plan/reference/slicing.md +5 -0
- package/pack/generated/devin/skills/rite-vet/reference/artifacts.md +8 -0
- package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +1 -1
- package/pack/generated/omp/agents/devrites-plan-drafter.md +2 -1
- package/pack/generated/omp/agents/devrites-slice-wright.md +2 -1
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
- package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
- package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/generated/omp/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
- package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +2 -2
- package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +1 -1
- package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
- package/pack/generated/omp/skills/rite-build/SKILL.md +7 -7
- package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +2 -2
- package/pack/generated/omp/skills/rite-build/reference/checkpoint.md +1 -1
- package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +1 -1
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +28 -21
- package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +10 -1
- package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +3 -2
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +2 -2
- package/pack/generated/omp/skills/rite-define/SKILL.md +1 -1
- package/pack/generated/omp/skills/rite-handoff/SKILL.md +1 -1
- package/pack/generated/omp/skills/rite-plan/reference/slicing.md +5 -0
- package/pack/generated/omp/skills/rite-vet/reference/artifacts.md +8 -0
- package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +1 -1
- package/pack/generated/pi/agents/devrites-plan-drafter.md +2 -1
- package/pack/generated/pi/agents/devrites-slice-wright.md +2 -1
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
- package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
- package/pack/generated/pi/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/generated/pi/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
- package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
- package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +2 -2
- package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +1 -1
- package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
- package/pack/generated/pi/skills/rite-build/SKILL.md +7 -7
- package/pack/generated/pi/skills/rite-build/reference/afk-discipline.md +2 -2
- package/pack/generated/pi/skills/rite-build/reference/checkpoint.md +1 -1
- package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +1 -1
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +28 -21
- package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +10 -1
- package/pack/generated/pi/skills/rite-build/reference/spec-drift-guard.md +3 -2
- package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +2 -2
- package/pack/generated/pi/skills/rite-define/SKILL.md +1 -1
- package/pack/generated/pi/skills/rite-handoff/SKILL.md +1 -1
- package/pack/generated/pi/skills/rite-plan/reference/slicing.md +5 -0
- package/pack/generated/pi/skills/rite-vet/reference/artifacts.md +8 -0
- package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
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.1.2](https://github.com/ViktorsBaikers/DevRites/compare/v6.1.1...v6.1.2) (2026-10-16)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **devrites:** close autocomplete dead ends and orphaned writer claims ([#108](https://github.com/ViktorsBaikers/DevRites/issues/108)) ([a680203](https://github.com/ViktorsBaikers/DevRites/commit/a680203d226160f9dae3bf5a386914567bd25da5))
|
|
10
|
+
|
|
5
11
|
## [6.1.1](https://github.com/ViktorsBaikers/DevRites/compare/v6.1.0...v6.1.1) (2026-10-16)
|
|
6
12
|
|
|
7
13
|
### 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:** [`v6.1.
|
|
34
|
+
**Status:** [`v6.1.2`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v6.1.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
|
|
package/docs/cli.md
CHANGED
|
@@ -165,7 +165,10 @@ self-contained updater.
|
|
|
165
165
|
and exits `3` naming the holder when a live claim by another session covers a
|
|
166
166
|
path; `release --session <id> --id <claim>` frees it; `list [--all]` shows
|
|
167
167
|
live (or all) claims; `check [--session <id>] <path>...` is the read-only
|
|
168
|
-
preflight.
|
|
168
|
+
preflight. Each claim records its host and the claiming session's long-lived
|
|
169
|
+
processes; when all of them have exited on this host, `list` shows the claim
|
|
170
|
+
`dead` and it no longer conflicts. Other hosts or an unavailable `ps` never
|
|
171
|
+
prove exit. Claims coordinate concurrent same-tree sessions; they never
|
|
169
172
|
replace the one-writer-per-worktree rule in `standards/agents.md`.
|
|
170
173
|
- `note <add|list|check|rm>` manages `notes.md` anchored notes: `add <slug>
|
|
171
174
|
<subject> <quote> <title> [body]` binds a note to a verbatim quote in one
|
package/docs/engine/commands.md
CHANGED
|
@@ -16,9 +16,9 @@ workflow strategy.
|
|
|
16
16
|
| `check readiness --emit-binding <slug>` | Render the exact stable Build-input binding for Vet to record after review. |
|
|
17
17
|
| `check seal <slug>` | Check files required by target Phase `seal`, open human gates, the `tasks.md` slice graph, canonical `AC-###` presence when `tasks.md`/`test-plan.md` are required, the stable Build-input binding, and exact candidate bindings. |
|
|
18
18
|
| `check path-disjoint [--root <dir>] [<json-file> | -]` | Verify slice path sets are pairwise disjoint. |
|
|
19
|
-
| `parallel select --cap <1-10> [--root <dir>] [<json-file> | -]` | Choose the greedy path-disjoint subset ≤ cap from supplied ready slices. |
|
|
20
|
-
| `parallel <create\|integrate\|cleanup\|status\|abort\|lease-read\|lease-write\|lease-clear\|record-green>` | Deterministic parallel worktree leases, integration, and cleanup for parallel writer batches. |
|
|
21
|
-
| `check task-graph <slug>` | Validate `tasks.md` slice dependency graph for cycles, unknown deps, malformed tokens, duplicate IDs, missing `Dependencies`, and `depends_on` mismatch. Prints a non-blocking `ADVISORY size:` line (and records a `size-advisory` metric) for each slice with 12+ Writer allowlist files and no `Size rationale:`. A `Dependencies` ID may carry a `(via <interface>)` suffix (empty or unclosed is a malformed token); prints a non-blocking `ADVISORY edge:` line (and records an `edge-advisory` metric) for each edge with no via, or whose via text is not a case-insensitive substring of the dependency's `Consumes / Produces:`. Advisories print only when the graph is valid. |
|
|
19
|
+
| `parallel select --cap <1-10> [--root <dir>] [--slug <slug>] [<json-file> | -]` | Choose the greedy path-disjoint subset ≤ cap from supplied ready slices. With `--slug` and a running lease, the lease's not-yet-landed slices hold cap slots and paths, and only new picks print. |
|
|
20
|
+
| `parallel <create\|add\|integrate\|cleanup\|status\|abort\|lease-read\|lease-write\|lease-clear\|record-green>` | Deterministic parallel worktree leases, integration, and cleanup for parallel writer batches. A lease is a pipeline: `integrate --slice <id> --apply-to-control` lands one green sibling onto control (refusing if anything but this lease moved control), and `add` starts new path-disjoint siblings from the current control tip while others still run. |
|
|
21
|
+
| `check task-graph <slug>` | Validate `tasks.md` slice dependency graph for cycles, unknown deps, malformed tokens, duplicate IDs, missing `Dependencies`, and `depends_on` mismatch. Prints a non-blocking `ADVISORY size:` line (and records a `size-advisory` metric) for each slice with 12+ Writer allowlist files and no `Size rationale:`. A `Dependencies` ID may carry a `(via <interface>)` suffix (empty or unclosed is a malformed token); prints a non-blocking `ADVISORY edge:` line (and records an `edge-advisory` metric) for each edge with no via, or whose via text is not a case-insensitive substring of the dependency's `Consumes / Produces:`. Prints `schedule: <n> slices in <r> build rounds at cap 10` (the `/rite-build` batch selection replayed over each slice's Writer allowlist, else Files likely touched; omitted when a slice has no path) and a non-blocking `ADVISORY hub:` line (top five; records a `hub-advisory` metric) for each path, compared case-folded, that three or more slices without `Serial rationale:` write. Advisories print only when the graph is valid. |
|
|
22
22
|
| `check regression <slug> [--update]` | Compare the workspace's structural progress facts (phase ordinal, checked ACs, met/abandoned gates, done slices, resolved questions, present artifacts) against the recorded `regression-baseline.json` high-water mark; `--update` ratchets the baseline forward under the feature lock and prints each blessed regression. |
|
|
23
23
|
| `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). |
|
|
24
24
|
| `check windows <slug> [--worktree\|--staged\|--base <ref>]` | 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. |
|
|
@@ -37,7 +37,7 @@ workflow strategy.
|
|
|
37
37
|
| `dispatch <slug> <open\|start\|seal\|return\|status\|abandon>` | Launch-wave barrier for parallel dispatch: seal needs a handle per role, return needs seal; start/return auto-record metrics. A build wave with a wright role needs `--slice <SLICE-ID>` per slice, and a slice-bound vet wave takes it too; 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. |
|
|
38
38
|
| `metrics record <slug> --phase <p> --event <e> [--role r] [--bytes n] [--note s]` | Append an event to the workspace's `metrics.jsonl` ledger. |
|
|
39
39
|
| `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). |
|
|
40
|
-
| `claim <add\|release\|list\|check>` | Advisory session-scoped file claims in `.devrites/claims.jsonl` (append-only). `add` records intent to write a path set with a TTL (1–240 min, default 30); a live claim by another session exits `3` and names the holder. `release` appends a release event owned by the claiming session. `list` shows live claims (`--all` includes expired/released). `check` is the read-only preflight for a path set. Coordination aid only — never a substitute for one-writer-per-worktree. |
|
|
40
|
+
| `claim <add\|release\|list\|check>` | Advisory session-scoped file claims in `.devrites/claims.jsonl` (append-only). `add` records intent to write a path set with a TTL (1–240 min, default 30); a live claim by another session exits `3` and names the holder. `release` appends a release event owned by the claiming session. `list` shows live claims (`--all` includes expired/released); a claim whose holding session has provably exited on this host shows `dead` and no longer conflicts. `check` is the read-only preflight for a path set. Coordination aid only — never a substitute for one-writer-per-worktree. |
|
|
41
41
|
| `note <add\|list\|check\|rm> <slug> ...` | Anchored workspace notes in `notes.md`: `add <slug> <subject> <quote> <title> [body]` binds a note to a verbatim quote in one repository file and assigns `NOTE-###`; `list` and `check` regrade each anchor `exact`/`moved`/`stale`/`ambiguous`/`lost` by searching the repo for the quote (dependency and workspace trees excluded); `check --repair` rewrites a `moved` subject to its new file; `rm` deletes by ID. `check seal` refuses a malformed `notes.md` and any non-`exact` anchor. |
|
|
42
42
|
| `gates <scaffold\|status\|run\|reverify\|lint\|attest\|abandon> <slug>` | Operate the `gates.md` acceptance ledger: seed per-AC gates, execute runnable gates bound to `test-plan.md` preflight rows, attest manual gates, and reduce to `all-met`/`not-met`/`handoff`/`malformed`. |
|
|
43
43
|
| `state resolve <qid> "<answer>"` | Resolve an open question and update `questions.md` plus `state.md` atomically. |
|
package/docs/orchestration.md
CHANGED
|
@@ -138,9 +138,10 @@ and a control `parallel-lease.md` apply. `--parallel N` on `/rite-autocomplete`
|
|
|
138
138
|
or `/rite-build` is the batch cap for that run and wins over leftover sentinel
|
|
139
139
|
`max_parallel`; `/rite-autocomplete --parallel N` also writes or replaces only
|
|
140
140
|
that field. Absent the flag, the sentinel (default cap 10 under
|
|
141
|
-
`/rite-autocomplete`) is a **cap**:
|
|
142
|
-
the largest eligible set,
|
|
143
|
-
|
|
141
|
+
`/rite-autocomplete`) is a **cap**: the run fills it
|
|
142
|
+
with the largest eligible set, lands each sibling as soon as its review and proof are
|
|
143
|
+
green, refills the freed slot (`parallel add`), and repeats until no pending slice
|
|
144
|
+
remains; a one-slice round is serial for that round only (see
|
|
144
145
|
[`parallel-batch.md`](../pack/.claude/skills/rite-build/reference/parallel-batch.md)).
|
|
145
146
|
Same-worktree multi-writer and root-emulated worktrees remain forbidden; default
|
|
146
147
|
`/rite-build` stays one writer across linked worktrees.
|
package/docs/usage.md
CHANGED
|
@@ -71,7 +71,7 @@ Project-root sentinel (outside the workspace):
|
|
|
71
71
|
| `.devrites/AFK` | you (presence = AFK mode active) | optional YAML: `max_slices`, `notify`, `allow_gates`. Empty file = AFK with defaults. See [`pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md`](../pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md). |
|
|
72
72
|
| `.devrites/CHECKPOINT` | unused as a gate | reserved path (not a candidate). After Independent Build review and fail-on-red proof are green (not when a wright returns), `/rite-build` / `/rite-prove` / `/rite-polish` / `/rite-review` land a local `WIP(<slug>): <imperative summary>` commit (serial: checkpoint git recipe or no-op if nothing staged; parallel: `devrites-engine parallel integrate`); `/rite-ship` later collapses an eligible WIP run. See [`pack/.claude/skills/rite-build/reference/checkpoint.md`](../pack/.claude/skills/rite-build/reference/checkpoint.md). |
|
|
73
73
|
| `.devrites/dup-ignore` | you (optional) | One `check dup` cluster hash per line with a `#` reason — the dismissal ledger. Content-derived hashes survive line moves but resurface on structural edits. See [`pack/.claude/skills/devrites-lib/reference/standards/duplicate-code.md`](../pack/.claude/skills/devrites-lib/reference/standards/duplicate-code.md). |
|
|
74
|
-
| `.devrites/claims.jsonl` | `devrites-engine claim add` | Append-only advisory file claims: `{id, session, paths, reason, claimed_at, expires_at}` plus release events. Session-scoped and TTL-bounded — per-checkout state, so gitignore it (a committed ledger exports stale claims to every clone). See [`standards/agents.md`](../pack/.claude/skills/devrites-lib/reference/standards/agents.md). |
|
|
74
|
+
| `.devrites/claims.jsonl` | `devrites-engine claim add` | Append-only advisory file claims: `{id, session, paths, reason, claimed_at, expires_at, host, anchors}` plus release events. Session-scoped and TTL-bounded — per-checkout state, so gitignore it (a committed ledger exports stale claims to every clone). See [`standards/agents.md`](../pack/.claude/skills/devrites-lib/reference/standards/agents.md). |
|
|
75
75
|
|
|
76
76
|
The shape of this directory is also documented in
|
|
77
77
|
[`flow.md` § Workspace state model](flow.md#7-workspace-state-model).
|
|
@@ -51,6 +51,10 @@ type claimRecord struct {
|
|
|
51
51
|
ClaimedAt string `json:"claimed_at"`
|
|
52
52
|
ExpiresAt string `json:"expires_at"`
|
|
53
53
|
Released bool `json:"released,omitempty"`
|
|
54
|
+
// Host and Anchors identify the claiming session's long-lived processes,
|
|
55
|
+
// so a later session can prove that holder exited (see holderProbe).
|
|
56
|
+
Host string `json:"host,omitempty"`
|
|
57
|
+
Anchors []claimProc `json:"anchors,omitempty"`
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
// RunClaim manages the advisory file-claim ledger.
|
|
@@ -299,7 +303,8 @@ func claimAddLocked(path, session, reason string, ttl int, paths []string, stdou
|
|
|
299
303
|
return 2
|
|
300
304
|
}
|
|
301
305
|
now := time.Now().UTC()
|
|
302
|
-
|
|
306
|
+
probe := newHolderProbe()
|
|
307
|
+
conflicts := claimConflicts(probe.withoutGone(liveClaims(latest, order, now)), session, paths)
|
|
303
308
|
if len(conflicts) > 0 {
|
|
304
309
|
printConflicts(stderr, conflicts)
|
|
305
310
|
return 3
|
|
@@ -311,6 +316,8 @@ func claimAddLocked(path, session, reason string, ttl int, paths []string, stdou
|
|
|
311
316
|
Reason: reason,
|
|
312
317
|
ClaimedAt: now.Format(time.RFC3339),
|
|
313
318
|
ExpiresAt: now.Add(time.Duration(ttl) * time.Minute).Format(time.RFC3339),
|
|
319
|
+
Host: probe.host,
|
|
320
|
+
Anchors: probe.anchors(now),
|
|
314
321
|
}
|
|
315
322
|
if err := appendClaim(path, rec); err != nil {
|
|
316
323
|
fmt.Fprintf(stderr, "claim: %v\n", err)
|
|
@@ -389,6 +396,7 @@ func claimList(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
389
396
|
return 2
|
|
390
397
|
}
|
|
391
398
|
now := time.Now().UTC()
|
|
399
|
+
probe := newHolderProbe()
|
|
392
400
|
rows := liveClaims(latest, order, now)
|
|
393
401
|
if all {
|
|
394
402
|
rows = rows[:0]
|
|
@@ -404,6 +412,8 @@ func claimList(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
404
412
|
state := "live"
|
|
405
413
|
if rec.Released {
|
|
406
414
|
state = "released"
|
|
415
|
+
} else if probe.gone(rec) {
|
|
416
|
+
state = "dead"
|
|
407
417
|
} else if exp, err := time.Parse(time.RFC3339, rec.ExpiresAt); err != nil || !exp.After(now) {
|
|
408
418
|
state = "expired"
|
|
409
419
|
}
|
|
@@ -453,7 +463,7 @@ func claimCheck(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
453
463
|
fmt.Fprintf(stderr, unparsedLedgerMsg, bad)
|
|
454
464
|
return 2
|
|
455
465
|
}
|
|
456
|
-
conflicts := claimConflicts(liveClaims(latest, order, time.Now().UTC()), session, paths)
|
|
466
|
+
conflicts := claimConflicts(newHolderProbe().withoutGone(liveClaims(latest, order, time.Now().UTC())), session, paths)
|
|
457
467
|
if len(conflicts) > 0 {
|
|
458
468
|
printConflicts(stdout, conflicts)
|
|
459
469
|
return 3
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
package lib
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"os/exec"
|
|
6
|
+
"slices"
|
|
7
|
+
"strconv"
|
|
8
|
+
"strings"
|
|
9
|
+
"time"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// claimAnchorAge separates a claiming session's long-lived host processes from
|
|
13
|
+
// the per-command shells and launchers that start moments before `claim add`.
|
|
14
|
+
const claimAnchorAge = time.Minute
|
|
15
|
+
|
|
16
|
+
// claimProc names one process by pid and start time, so a reused pid cannot
|
|
17
|
+
// impersonate it.
|
|
18
|
+
type claimProc struct {
|
|
19
|
+
PID int `json:"pid"`
|
|
20
|
+
Start string `json:"start"`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type procInfo struct {
|
|
24
|
+
ppid int
|
|
25
|
+
start string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// processTable lists running processes keyed by pid, or nil when the host
|
|
29
|
+
// cannot say. Tests replace it.
|
|
30
|
+
var processTable = psProcessTable
|
|
31
|
+
|
|
32
|
+
func psProcessTable() map[int]procInfo {
|
|
33
|
+
cmd := exec.Command("ps", "-A", "-o", "pid=,ppid=,lstart=")
|
|
34
|
+
cmd.Env = append(os.Environ(), "LC_ALL=C")
|
|
35
|
+
out, err := cmd.Output()
|
|
36
|
+
if err != nil {
|
|
37
|
+
return nil
|
|
38
|
+
}
|
|
39
|
+
table := map[int]procInfo{}
|
|
40
|
+
for _, line := range strings.Split(string(out), "\n") {
|
|
41
|
+
f := strings.Fields(line)
|
|
42
|
+
if len(f) != 7 {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
pid, perr := strconv.Atoi(f[0])
|
|
46
|
+
ppid, pperr := strconv.Atoi(f[1])
|
|
47
|
+
start, serr := time.ParseInLocation("Mon Jan 2 15:04:05 2006", strings.Join(f[2:], " "), time.Local)
|
|
48
|
+
if perr != nil || pperr != nil || serr != nil {
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
table[pid] = procInfo{ppid: ppid, start: start.UTC().Format(time.RFC3339)}
|
|
52
|
+
}
|
|
53
|
+
if len(table) == 0 {
|
|
54
|
+
return nil
|
|
55
|
+
}
|
|
56
|
+
return table
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// lineage walks from pid up to, but not including, pid 1.
|
|
60
|
+
func lineage(table map[int]procInfo, pid int) []claimProc {
|
|
61
|
+
var out []claimProc
|
|
62
|
+
seen := map[int]bool{}
|
|
63
|
+
for pid > 1 && !seen[pid] {
|
|
64
|
+
seen[pid] = true
|
|
65
|
+
p, ok := table[pid]
|
|
66
|
+
if !ok {
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
out = append(out, claimProc{PID: pid, Start: p.start})
|
|
70
|
+
pid = p.ppid
|
|
71
|
+
}
|
|
72
|
+
return out
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// holderProbe answers whether a claim's holding session has exited. It only
|
|
76
|
+
// ever proves exit on this host; anything it cannot see counts as alive.
|
|
77
|
+
type holderProbe struct {
|
|
78
|
+
table map[int]procInfo
|
|
79
|
+
host string
|
|
80
|
+
self []claimProc
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func newHolderProbe() holderProbe {
|
|
84
|
+
table := processTable()
|
|
85
|
+
host, _ := os.Hostname()
|
|
86
|
+
return holderProbe{table: table, host: host, self: lineage(table, os.Getppid())}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// anchors returns the caller's ancestors that started more than
|
|
90
|
+
// claimAnchorAge before now: the session's host processes.
|
|
91
|
+
func (p holderProbe) anchors(now time.Time) []claimProc {
|
|
92
|
+
var out []claimProc
|
|
93
|
+
for _, proc := range p.self {
|
|
94
|
+
start, err := time.Parse(time.RFC3339, proc.Start)
|
|
95
|
+
if err == nil && now.Sub(start) > claimAnchorAge {
|
|
96
|
+
out = append(out, proc)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// gone reports whether rec's holder is proven exited: the claim was taken on
|
|
103
|
+
// this host and every anchor it does not share with the caller has exited.
|
|
104
|
+
func (p holderProbe) gone(rec *claimRecord) bool {
|
|
105
|
+
if p.table == nil || rec.Host == "" || rec.Host != p.host {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
unique := 0
|
|
109
|
+
for _, a := range rec.Anchors {
|
|
110
|
+
if slices.Contains(p.self, a) {
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
unique++
|
|
114
|
+
if proc, ok := p.table[a.PID]; ok && proc.start == a.Start {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return unique > 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// withoutGone drops claims whose holder session has exited.
|
|
122
|
+
func (p holderProbe) withoutGone(claims []*claimRecord) []*claimRecord {
|
|
123
|
+
return slices.DeleteFunc(claims, p.gone)
|
|
124
|
+
}
|
|
@@ -5,6 +5,7 @@ import (
|
|
|
5
5
|
"io"
|
|
6
6
|
|
|
7
7
|
"github.com/devrites/devrites/internal/devritespaths"
|
|
8
|
+
"github.com/devrites/devrites/internal/parallel"
|
|
8
9
|
"github.com/devrites/devrites/internal/state"
|
|
9
10
|
)
|
|
10
11
|
|
|
@@ -36,6 +37,19 @@ func RunTaskGraphCheck(root, slug string, stdout, stderr io.Writer) int {
|
|
|
36
37
|
if over > 0 {
|
|
37
38
|
recordMetric(root, slug, "define", "size-advisory", "", int64(over))
|
|
38
39
|
}
|
|
40
|
+
paths, serial := sliceWritePaths(raw, graph)
|
|
41
|
+
if rounds := scheduleRounds(graph, paths, root); rounds > 0 {
|
|
42
|
+
fmt.Fprintf(stdout, "task-graph: schedule: %d slices in %d build rounds at cap %d\n", len(graph.Slices), rounds, parallel.MaxParallelSlices)
|
|
43
|
+
}
|
|
44
|
+
if hubs := hubAdvisories(graph, paths, serial); len(hubs) > 0 {
|
|
45
|
+
for _, line := range hubs[:min(len(hubs), hubAdvisoryShown)] {
|
|
46
|
+
fmt.Fprintf(stdout, "task-graph: %s\n", line)
|
|
47
|
+
}
|
|
48
|
+
if extra := len(hubs) - hubAdvisoryShown; extra > 0 {
|
|
49
|
+
fmt.Fprintf(stdout, "task-graph: ADVISORY hub: %d more shared paths; fix the ones above first\n", extra)
|
|
50
|
+
}
|
|
51
|
+
recordMetric(root, slug, "define", "hub-advisory", "", int64(len(hubs)))
|
|
52
|
+
}
|
|
39
53
|
}
|
|
40
54
|
if edges := edgeAdvisories(graph); len(edges) > 0 {
|
|
41
55
|
for _, line := range edges {
|
|
@@ -2,8 +2,11 @@ package lib
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"fmt"
|
|
5
|
+
"path"
|
|
6
|
+
"sort"
|
|
5
7
|
"strings"
|
|
6
8
|
|
|
9
|
+
"github.com/devrites/devrites/internal/parallel"
|
|
7
10
|
"github.com/devrites/devrites/internal/state"
|
|
8
11
|
)
|
|
9
12
|
|
|
@@ -27,6 +30,127 @@ func CheckTaskGraph(root, slug string) (TaskGraphResult, error) {
|
|
|
27
30
|
return ParseTaskGraph(raw), nil
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
// hubAdvisoryMin is how many slices must write one path before it is flagged:
|
|
34
|
+
// slices sharing a write path never share a parallel build round.
|
|
35
|
+
const hubAdvisoryMin = 3
|
|
36
|
+
|
|
37
|
+
// hubAdvisoryShown caps printed hub lines; the metric still counts every hub.
|
|
38
|
+
const hubAdvisoryShown = 5
|
|
39
|
+
|
|
40
|
+
// sliceWritePaths returns each slice's write set (Writer allowlist, else Files
|
|
41
|
+
// likely touched) and whether it records a `Serial rationale:`.
|
|
42
|
+
func sliceWritePaths(tasksMarkdown []byte, graph TaskGraphResult) (map[string][]string, map[string]bool) {
|
|
43
|
+
paths := make(map[string][]string, len(graph.Slices))
|
|
44
|
+
serial := make(map[string]bool)
|
|
45
|
+
for _, s := range graph.Slices {
|
|
46
|
+
block, _ := state.ExtractTaskSlice(tasksMarkdown, s.ID)
|
|
47
|
+
fields := sliceFields(block)
|
|
48
|
+
allow, _ := fieldValue(fields, "writer allowlist")
|
|
49
|
+
paths[s.ID] = pathTokens(allow)
|
|
50
|
+
if len(paths[s.ID]) == 0 {
|
|
51
|
+
touched, _ := fieldValue(fields, "files likely touched")
|
|
52
|
+
paths[s.ID] = pathTokens(touched)
|
|
53
|
+
}
|
|
54
|
+
if v, _ := fieldValue(fields, "serial rationale"); v != "" {
|
|
55
|
+
serial[s.ID] = true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return paths, serial
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// pathTokens keeps the list entries that can be paths; prose and Markdown links
|
|
62
|
+
// (anything with whitespace) are not write paths.
|
|
63
|
+
func pathTokens(raw string) []string {
|
|
64
|
+
var out []string
|
|
65
|
+
for _, p := range parsePathList(raw) {
|
|
66
|
+
if !strings.ContainsAny(p, " \t") {
|
|
67
|
+
out = append(out, p)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// scheduleRounds replays /rite-build batch selection at the maximum cap: each
|
|
74
|
+
// round runs the plan-order path-disjoint subset of dependency-ready slices.
|
|
75
|
+
// It returns 0 when a slice has no write paths or parallel select rejects them.
|
|
76
|
+
func scheduleRounds(graph TaskGraphResult, paths map[string][]string, root string) int {
|
|
77
|
+
done := make(map[string]bool, len(graph.Slices))
|
|
78
|
+
rounds := 0
|
|
79
|
+
for len(done) < len(graph.Slices) {
|
|
80
|
+
var ready []parallel.SlicePaths
|
|
81
|
+
for _, s := range graph.Slices {
|
|
82
|
+
if done[s.ID] || !allDone(s.Dependencies, done) {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if len(paths[s.ID]) == 0 {
|
|
86
|
+
return 0
|
|
87
|
+
}
|
|
88
|
+
ready = append(ready, parallel.SlicePaths{ID: s.ID, Paths: paths[s.ID]})
|
|
89
|
+
}
|
|
90
|
+
if len(ready) == 0 {
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
picked, err := parallel.SelectGreedy(parallel.MaxParallelSlices, ready, root)
|
|
94
|
+
if err != nil || len(picked) == 0 {
|
|
95
|
+
return 0
|
|
96
|
+
}
|
|
97
|
+
for _, p := range picked {
|
|
98
|
+
done[p.ID] = true
|
|
99
|
+
}
|
|
100
|
+
rounds++
|
|
101
|
+
}
|
|
102
|
+
return rounds
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func allDone(ids []string, done map[string]bool) bool {
|
|
106
|
+
for _, id := range ids {
|
|
107
|
+
if !done[id] {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// hubAdvisories names each path written by hubAdvisoryMin+ slices that carry
|
|
115
|
+
// no `Serial rationale:`, most-shared first.
|
|
116
|
+
func hubAdvisories(graph TaskGraphResult, paths map[string][]string, serial map[string]bool) []string {
|
|
117
|
+
writers := map[string][]string{}
|
|
118
|
+
shown := map[string]string{} // first spelling seen, for display
|
|
119
|
+
for _, s := range graph.Slices {
|
|
120
|
+
if serial[s.ID] {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
seen := map[string]bool{}
|
|
124
|
+
for _, p := range paths[s.ID] {
|
|
125
|
+
key := strings.ToLower(path.Clean(strings.ReplaceAll(p, `\`, "/")))
|
|
126
|
+
if !seen[key] {
|
|
127
|
+
seen[key] = true
|
|
128
|
+
writers[key] = append(writers[key], s.ID)
|
|
129
|
+
if _, ok := shown[key]; !ok {
|
|
130
|
+
shown[key] = p
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
hubs := make([]string, 0, len(writers))
|
|
136
|
+
for p, ids := range writers {
|
|
137
|
+
if len(ids) >= hubAdvisoryMin {
|
|
138
|
+
hubs = append(hubs, p)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
sort.Slice(hubs, func(i, j int) bool {
|
|
142
|
+
if len(writers[hubs[i]]) != len(writers[hubs[j]]) {
|
|
143
|
+
return len(writers[hubs[i]]) > len(writers[hubs[j]])
|
|
144
|
+
}
|
|
145
|
+
return hubs[i] < hubs[j]
|
|
146
|
+
})
|
|
147
|
+
lines := make([]string, len(hubs))
|
|
148
|
+
for i, p := range hubs {
|
|
149
|
+
lines[i] = fmt.Sprintf("ADVISORY hub: %s is written by %d slices (%s), so they build one at a time; move the shared edit into one seam slice they depend on, or record `Serial rationale:`", shown[p], len(writers[p]), strings.Join(writers[p], ", "))
|
|
150
|
+
}
|
|
151
|
+
return lines
|
|
152
|
+
}
|
|
153
|
+
|
|
30
154
|
// edgeAdvisories names each dependency edge that does not say what crosses it:
|
|
31
155
|
// no `(via <interface>)`, or a via the dependency's Consumes / Produces lacks.
|
|
32
156
|
// An edge the dependent reads nothing across only serializes work.
|
|
@@ -74,18 +74,23 @@ func runPathDisjoint(args []string, stdin io.Reader, stdout, stderr io.Writer) i
|
|
|
74
74
|
|
|
75
75
|
func cmdSelect(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
76
76
|
root := ""
|
|
77
|
+
slug := ""
|
|
77
78
|
jsonPath := "-"
|
|
78
79
|
capN := 0
|
|
79
80
|
sawCap := false
|
|
80
81
|
for i := 0; i < len(args); i++ {
|
|
81
82
|
switch args[i] {
|
|
82
|
-
case "--root":
|
|
83
|
+
case "--root", "--slug":
|
|
83
84
|
if i+1 >= len(args) {
|
|
84
85
|
fmt.Fprintln(stderr, usageSelect)
|
|
85
86
|
return ExitUsage
|
|
86
87
|
}
|
|
87
88
|
i++
|
|
88
|
-
|
|
89
|
+
if args[i-1] == "--root" {
|
|
90
|
+
root = args[i]
|
|
91
|
+
} else {
|
|
92
|
+
slug = args[i]
|
|
93
|
+
}
|
|
89
94
|
case "--cap":
|
|
90
95
|
if i+1 >= len(args) {
|
|
91
96
|
fmt.Fprintln(stderr, usageSelect)
|
|
@@ -124,7 +129,33 @@ func cmdSelect(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
124
129
|
fmt.Fprintf(stderr, "parallel select: %v\n", err)
|
|
125
130
|
return ExitBlocked
|
|
126
131
|
}
|
|
127
|
-
|
|
132
|
+
var seed []SlicePaths
|
|
133
|
+
running := -1
|
|
134
|
+
if slug != "" {
|
|
135
|
+
lease, err := runningLease(root, slug)
|
|
136
|
+
if err != nil {
|
|
137
|
+
fmt.Fprintf(stderr, "parallel select: %v\n", err)
|
|
138
|
+
return ExitBlocked
|
|
139
|
+
}
|
|
140
|
+
if lease != nil {
|
|
141
|
+
// The lease's active slices keep their slots and paths; nothing it
|
|
142
|
+
// already names (active or landed) is picked again.
|
|
143
|
+
seed = activeSlices(lease)
|
|
144
|
+
running = len(seed)
|
|
145
|
+
named := make(map[string]bool, len(lease.Slices))
|
|
146
|
+
for _, sl := range lease.Slices {
|
|
147
|
+
named[sl.ID] = true
|
|
148
|
+
}
|
|
149
|
+
var fresh []SlicePaths
|
|
150
|
+
for _, s := range slices {
|
|
151
|
+
if !named[s.ID] {
|
|
152
|
+
fresh = append(fresh, s)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
slices = fresh
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
selected, err := SelectGreedySeeded(capN, seed, slices, root)
|
|
128
159
|
if err != nil {
|
|
129
160
|
fmt.Fprintf(stderr, "parallel select: %v\n", err)
|
|
130
161
|
if strings.Contains(err.Error(), "cap must be") {
|
|
@@ -132,10 +163,30 @@ func cmdSelect(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
132
163
|
}
|
|
133
164
|
return ExitBlocked
|
|
134
165
|
}
|
|
135
|
-
fmt.Fprintln(stdout, formatSelect(selected))
|
|
166
|
+
fmt.Fprintln(stdout, formatSelect(selected, running))
|
|
136
167
|
return ExitOK
|
|
137
168
|
}
|
|
138
169
|
|
|
170
|
+
// runningLease returns slug's lease when it is still in flight
|
|
171
|
+
// (running|integrate-failed), or nil when there is none to seed from.
|
|
172
|
+
func runningLease(root, slug string) (*Lease, error) {
|
|
173
|
+
path, err := LeasePath(root, slug)
|
|
174
|
+
if err != nil {
|
|
175
|
+
return nil, err
|
|
176
|
+
}
|
|
177
|
+
lease, err := ReadLease(path)
|
|
178
|
+
if os.IsNotExist(err) {
|
|
179
|
+
return nil, nil
|
|
180
|
+
}
|
|
181
|
+
if err != nil {
|
|
182
|
+
return nil, err
|
|
183
|
+
}
|
|
184
|
+
if lease.Status != StatusRunning && lease.Status != StatusIntegrateFailed {
|
|
185
|
+
return nil, nil
|
|
186
|
+
}
|
|
187
|
+
return lease, nil
|
|
188
|
+
}
|
|
189
|
+
|
|
139
190
|
func runParallel(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
140
191
|
if len(args) == 0 {
|
|
141
192
|
fmt.Fprintln(stderr, parallelUsage())
|
|
@@ -145,6 +196,8 @@ func runParallel(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
145
196
|
switch sub {
|
|
146
197
|
case "create":
|
|
147
198
|
return cmdCreate(rest, stdin, stdout, stderr)
|
|
199
|
+
case "add":
|
|
200
|
+
return cmdAdd(rest, stdin, stdout, stderr)
|
|
148
201
|
case "record-green":
|
|
149
202
|
return cmdRecordGreen(rest, stdout, stderr)
|
|
150
203
|
case "abort":
|
|
@@ -174,16 +227,17 @@ func runParallel(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
174
227
|
|
|
175
228
|
const (
|
|
176
229
|
usageCreate = "usage: parallel create --root --slug --batch --base --json"
|
|
230
|
+
usageAdd = "usage: parallel add --root --slug --json <json-file>|-"
|
|
177
231
|
usageRecordGreen = "usage: parallel record-green --root --slug --slice --commit"
|
|
178
232
|
usageAbort = "usage: parallel abort --root --slug"
|
|
179
|
-
usageIntegrate = "usage: parallel integrate --root --slug [--apply-to-control]"
|
|
233
|
+
usageIntegrate = "usage: parallel integrate --root --slug [--apply-to-control [--slice <id>]]"
|
|
180
234
|
usageCleanup = "usage: parallel cleanup --root --slug [--force]"
|
|
181
235
|
usageStatus = "usage: parallel status --root --slug"
|
|
182
236
|
usageLeaseWrite = "usage: parallel lease-write --root --slug --json"
|
|
183
237
|
usageLeaseRead = "usage: parallel lease-read --root --slug [--field name]"
|
|
184
238
|
usageLeaseClear = "usage: parallel lease-clear --root --slug"
|
|
185
239
|
usagePathDisjoint = "usage: check path-disjoint [--root <dir>] [<json-file>|-]"
|
|
186
|
-
usageSelect = "usage: parallel select --cap <1-10> [--root <dir>] [<json-file>|-]"
|
|
240
|
+
usageSelect = "usage: parallel select --cap <1-10> [--root <dir>] [--slug <slug>] [<json-file>|-]"
|
|
187
241
|
)
|
|
188
242
|
|
|
189
243
|
func parallelUsage() string {
|
|
@@ -191,15 +245,16 @@ func parallelUsage() string {
|
|
|
191
245
|
|
|
192
246
|
Subcommands:
|
|
193
247
|
create --root --slug --batch --base --json
|
|
248
|
+
add --root --slug --json
|
|
194
249
|
record-green --root --slug --slice --commit
|
|
195
250
|
abort --root --slug
|
|
196
|
-
integrate --root --slug [--apply-to-control]
|
|
251
|
+
integrate --root --slug [--apply-to-control [--slice <id>]]
|
|
197
252
|
cleanup --root --slug [--force]
|
|
198
253
|
status --root --slug
|
|
199
254
|
lease-write --root --slug --json
|
|
200
255
|
lease-read --root --slug [--field name]
|
|
201
256
|
lease-clear --root --slug
|
|
202
|
-
select --cap <1-10> [--root] [<json-file>|-]
|
|
257
|
+
select --cap <1-10> [--root] [--slug] [<json-file>|-]
|
|
203
258
|
|
|
204
259
|
Exit codes: 0 ok, 2 usage, 3 blocked`)
|
|
205
260
|
}
|
|
@@ -210,6 +265,8 @@ func CommandUsage(name string) string {
|
|
|
210
265
|
switch name {
|
|
211
266
|
case "create":
|
|
212
267
|
return usageCreate
|
|
268
|
+
case "add":
|
|
269
|
+
return usageAdd
|
|
213
270
|
case "record-green":
|
|
214
271
|
return usageRecordGreen
|
|
215
272
|
case "abort":
|
|
@@ -414,6 +471,41 @@ func cmdCreate(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
414
471
|
return ExitOK
|
|
415
472
|
}
|
|
416
473
|
|
|
474
|
+
func cmdAdd(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
475
|
+
f, code, done := parseFlagsOrHelp(args, stdout, stderr, usageAdd)
|
|
476
|
+
if done {
|
|
477
|
+
return code
|
|
478
|
+
}
|
|
479
|
+
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
480
|
+
return code
|
|
481
|
+
}
|
|
482
|
+
if code := rejectUnsupportedFlags(f, stderr, false, false); code != ExitOK {
|
|
483
|
+
return code
|
|
484
|
+
}
|
|
485
|
+
if f.JSON == "" {
|
|
486
|
+
fmt.Fprintln(stderr, usageAdd)
|
|
487
|
+
return ExitUsage
|
|
488
|
+
}
|
|
489
|
+
data, err := readJSONInput(f.JSON, stdin)
|
|
490
|
+
if err != nil {
|
|
491
|
+
fmt.Fprintf(stderr, "parallel add: %v\n", err)
|
|
492
|
+
return ExitBlocked
|
|
493
|
+
}
|
|
494
|
+
slices, err := ParseSlicesJSON(data)
|
|
495
|
+
if err != nil {
|
|
496
|
+
fmt.Fprintf(stderr, "parallel add: %v\n", err)
|
|
497
|
+
return ExitBlocked
|
|
498
|
+
}
|
|
499
|
+
lease, err := Add(AddOpts{Root: f.Root, Slug: f.Slug, Slices: slices})
|
|
500
|
+
if err != nil {
|
|
501
|
+
fmt.Fprintf(stderr, "parallel add: %v\n", err)
|
|
502
|
+
return ExitBlocked
|
|
503
|
+
}
|
|
504
|
+
fmt.Fprintf(stdout, "added: %d worktrees; batch=%s active=%d base=%s\n",
|
|
505
|
+
len(slices), lease.BatchID, len(activeSlices(lease)), lease.Slices[len(lease.Slices)-1].BaseSHA)
|
|
506
|
+
return ExitOK
|
|
507
|
+
}
|
|
508
|
+
|
|
417
509
|
// rejectUnsupportedFlags reports flags a subcommand parses but must not
|
|
418
510
|
// silently ignore: --force only exists for cleanup, --apply-to-control only
|
|
419
511
|
// for integrate.
|
|
@@ -484,16 +576,26 @@ func cmdIntegrate(args []string, stdout, stderr io.Writer) int {
|
|
|
484
576
|
if code := rejectUnsupportedFlags(f, stderr, false, true); code != ExitOK {
|
|
485
577
|
return code
|
|
486
578
|
}
|
|
579
|
+
if f.Slice != "" && !f.ApplyToControl {
|
|
580
|
+
fmt.Fprintln(stderr, "parallel integrate: --slice requires --apply-to-control")
|
|
581
|
+
fmt.Fprintln(stderr, usageIntegrate)
|
|
582
|
+
return ExitUsage
|
|
583
|
+
}
|
|
487
584
|
tip, lease, err := Integrate(IntegrateOpts{
|
|
488
585
|
Root: f.Root,
|
|
489
586
|
Slug: f.Slug,
|
|
587
|
+
Slice: f.Slice,
|
|
490
588
|
ApplyToControl: f.ApplyToControl,
|
|
491
589
|
})
|
|
492
590
|
if err != nil {
|
|
493
591
|
fmt.Fprintf(stderr, "parallel integrate: %v\n", err)
|
|
494
592
|
return ExitBlocked
|
|
495
593
|
}
|
|
496
|
-
fmt.Fprintf(stdout, "integrate-ok: tip=%s status=%s apply_to_control=%v
|
|
594
|
+
fmt.Fprintf(stdout, "integrate-ok: tip=%s status=%s apply_to_control=%v", tip, lease.Status, f.ApplyToControl)
|
|
595
|
+
if f.Slice != "" {
|
|
596
|
+
fmt.Fprintf(stdout, " slice=%s", f.Slice)
|
|
597
|
+
}
|
|
598
|
+
fmt.Fprintln(stdout)
|
|
497
599
|
return ExitOK
|
|
498
600
|
}
|
|
499
601
|
|