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.
Files changed (153) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/docs/cli.md +4 -1
  4. package/docs/engine/commands.md +4 -4
  5. package/docs/orchestration.md +4 -3
  6. package/docs/usage.md +1 -1
  7. package/engine/internal/lib/claims.go +12 -2
  8. package/engine/internal/lib/claims_holder.go +124 -0
  9. package/engine/internal/lib/cli_observe.go +14 -0
  10. package/engine/internal/lib/taskgraph.go +124 -0
  11. package/engine/internal/parallel/cli.go +111 -9
  12. package/engine/internal/parallel/lease.go +58 -4
  13. package/engine/internal/parallel/ops.go +244 -75
  14. package/engine/internal/parallel/select.go +31 -6
  15. package/pack/.claude/agents/devrites-plan-drafter.md +2 -1
  16. package/pack/.claude/agents/devrites-slice-wright.md +2 -1
  17. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  18. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  19. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +6 -2
  20. package/pack/.claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  21. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  22. package/pack/.claude/skills/rite-autocomplete/SKILL.md +2 -2
  23. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +1 -1
  24. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  25. package/pack/.claude/skills/rite-build/SKILL.md +7 -7
  26. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +2 -2
  27. package/pack/.claude/skills/rite-build/reference/checkpoint.md +1 -1
  28. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +1 -1
  29. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +28 -21
  30. package/pack/.claude/skills/rite-build/reference/phase-contract.md +10 -1
  31. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +3 -2
  32. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +2 -2
  33. package/pack/.claude/skills/rite-define/SKILL.md +1 -1
  34. package/pack/.claude/skills/rite-handoff/SKILL.md +1 -1
  35. package/pack/.claude/skills/rite-plan/reference/slicing.md +5 -0
  36. package/pack/.claude/skills/rite-vet/reference/artifacts.md +8 -0
  37. package/pack/.claude/skills/rite-vet/reference/review-axes.md +1 -1
  38. package/pack/generated/claude/agents/devrites-plan-drafter.md +2 -1
  39. package/pack/generated/claude/agents/devrites-slice-wright.md +2 -1
  40. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  41. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  42. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +6 -2
  43. package/pack/generated/claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  44. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  45. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +2 -2
  46. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +1 -1
  47. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  48. package/pack/generated/claude/skills/rite-build/SKILL.md +7 -7
  49. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +2 -2
  50. package/pack/generated/claude/skills/rite-build/reference/checkpoint.md +1 -1
  51. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +1 -1
  52. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +28 -21
  53. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +10 -1
  54. package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +3 -2
  55. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +2 -2
  56. package/pack/generated/claude/skills/rite-define/SKILL.md +1 -1
  57. package/pack/generated/claude/skills/rite-handoff/SKILL.md +1 -1
  58. package/pack/generated/claude/skills/rite-plan/reference/slicing.md +5 -0
  59. package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +8 -0
  60. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +1 -1
  61. package/pack/generated/codex/agents/devrites-plan-drafter.toml +2 -1
  62. package/pack/generated/codex/agents/devrites-slice-wright.toml +2 -1
  63. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  64. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  65. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +6 -2
  66. package/pack/generated/codex/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  67. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  68. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +2 -2
  69. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +1 -1
  70. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  71. package/pack/generated/codex/skills/rite-build/SKILL.md +7 -7
  72. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +2 -2
  73. package/pack/generated/codex/skills/rite-build/reference/checkpoint.md +1 -1
  74. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +1 -1
  75. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +28 -21
  76. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +10 -1
  77. package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +3 -2
  78. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +2 -2
  79. package/pack/generated/codex/skills/rite-define/SKILL.md +1 -1
  80. package/pack/generated/codex/skills/rite-handoff/SKILL.md +1 -1
  81. package/pack/generated/codex/skills/rite-plan/reference/slicing.md +5 -0
  82. package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +8 -0
  83. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +1 -1
  84. package/pack/generated/devin/agents/devrites-plan-drafter.md +2 -1
  85. package/pack/generated/devin/agents/devrites-slice-wright.md +2 -1
  86. package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  87. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  88. package/pack/generated/devin/skills/devrites-lib/reference/standards/agents.md +6 -2
  89. package/pack/generated/devin/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  90. package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  91. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +2 -2
  92. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +1 -1
  93. package/pack/generated/devin/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  94. package/pack/generated/devin/skills/rite-build/SKILL.md +7 -7
  95. package/pack/generated/devin/skills/rite-build/reference/afk-discipline.md +2 -2
  96. package/pack/generated/devin/skills/rite-build/reference/checkpoint.md +1 -1
  97. package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +1 -1
  98. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +28 -21
  99. package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +10 -1
  100. package/pack/generated/devin/skills/rite-build/reference/spec-drift-guard.md +3 -2
  101. package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +2 -2
  102. package/pack/generated/devin/skills/rite-define/SKILL.md +1 -1
  103. package/pack/generated/devin/skills/rite-handoff/SKILL.md +1 -1
  104. package/pack/generated/devin/skills/rite-plan/reference/slicing.md +5 -0
  105. package/pack/generated/devin/skills/rite-vet/reference/artifacts.md +8 -0
  106. package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +1 -1
  107. package/pack/generated/omp/agents/devrites-plan-drafter.md +2 -1
  108. package/pack/generated/omp/agents/devrites-slice-wright.md +2 -1
  109. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  110. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  111. package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +6 -2
  112. package/pack/generated/omp/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  113. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  114. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +2 -2
  115. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +1 -1
  116. package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  117. package/pack/generated/omp/skills/rite-build/SKILL.md +7 -7
  118. package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +2 -2
  119. package/pack/generated/omp/skills/rite-build/reference/checkpoint.md +1 -1
  120. package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +1 -1
  121. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +28 -21
  122. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +10 -1
  123. package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +3 -2
  124. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +2 -2
  125. package/pack/generated/omp/skills/rite-define/SKILL.md +1 -1
  126. package/pack/generated/omp/skills/rite-handoff/SKILL.md +1 -1
  127. package/pack/generated/omp/skills/rite-plan/reference/slicing.md +5 -0
  128. package/pack/generated/omp/skills/rite-vet/reference/artifacts.md +8 -0
  129. package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +1 -1
  130. package/pack/generated/pi/agents/devrites-plan-drafter.md +2 -1
  131. package/pack/generated/pi/agents/devrites-slice-wright.md +2 -1
  132. package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +7 -2
  133. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +3 -2
  134. package/pack/generated/pi/skills/devrites-lib/reference/standards/agents.md +6 -2
  135. package/pack/generated/pi/skills/devrites-lib/reference/standards/workflow-artifacts.md +10 -6
  136. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +1 -1
  137. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +2 -2
  138. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +1 -1
  139. package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +5 -1
  140. package/pack/generated/pi/skills/rite-build/SKILL.md +7 -7
  141. package/pack/generated/pi/skills/rite-build/reference/afk-discipline.md +2 -2
  142. package/pack/generated/pi/skills/rite-build/reference/checkpoint.md +1 -1
  143. package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +1 -1
  144. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +28 -21
  145. package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +10 -1
  146. package/pack/generated/pi/skills/rite-build/reference/spec-drift-guard.md +3 -2
  147. package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +2 -2
  148. package/pack/generated/pi/skills/rite-define/SKILL.md +1 -1
  149. package/pack/generated/pi/skills/rite-handoff/SKILL.md +1 -1
  150. package/pack/generated/pi/skills/rite-plan/reference/slicing.md +5 -0
  151. package/pack/generated/pi/skills/rite-vet/reference/artifacts.md +8 -0
  152. package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +1 -1
  153. 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.1`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v6.1.1): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
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. Claims coordinate concurrent same-tree sessions; they never
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
@@ -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. |
@@ -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**: each round runs
142
- the largest eligible set, recomputes after every completed round, and repeats until no
143
- pending slice remains; a one-slice round is serial for that round only (see
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
- conflicts := claimConflicts(liveClaims(latest, order, now), session, paths)
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
- root = args[i]
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
- selected, err := SelectGreedy(capN, slices, root)
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\n", tip, lease.Status, f.ApplyToControl)
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