devrites 7.3.5 → 7.3.6

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 (42) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/docs/adr/0040-early-preparation-admission.md +76 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/engine/commands.md +48 -6
  6. package/engine/internal/lib/context.go +50 -2
  7. package/engine/internal/lib/continuation.go +2 -0
  8. package/engine/internal/lib/dispatch.go +75 -20
  9. package/engine/internal/lib/frontier.go +40 -16
  10. package/engine/internal/lib/frontier_controller.go +53 -38
  11. package/engine/internal/lib/frontier_preparation.go +210 -0
  12. package/engine/internal/lib/next.go +5 -2
  13. package/engine/internal/lib/triggers.go +9 -0
  14. package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
  15. package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
  16. package/pack/.claude/skills/rite-autocomplete/SKILL.md +7 -5
  17. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +55 -42
  18. package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
  19. package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
  20. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +7 -5
  21. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +55 -42
  22. package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
  23. package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
  24. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +7 -5
  25. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +55 -42
  26. package/pack/generated/devin/skills/devrites-lib/SKILL.md +2 -0
  27. package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
  28. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +7 -5
  29. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +55 -42
  30. package/pack/generated/dsh/skills/devrites-lib/SKILL.md +2 -0
  31. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
  32. package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +7 -5
  33. package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +55 -42
  34. package/pack/generated/omp/skills/devrites-lib/SKILL.md +2 -0
  35. package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
  36. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +7 -5
  37. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +55 -42
  38. package/pack/generated/pi/skills/devrites-lib/SKILL.md +2 -0
  39. package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
  40. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +7 -5
  41. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +55 -42
  42. 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
+ ## [7.3.6](https://github.com/ViktorsBaikers/DevRites/compare/v7.3.5...v7.3.6) (2026-11-06)
6
+
7
+ ### Fixed
8
+
9
+ * **devrites:** enforce native prebuild preparation admission ([#209](https://github.com/ViktorsBaikers/DevRites/issues/209)) ([2aba0a0](https://github.com/ViktorsBaikers/DevRites/commit/2aba0a0e0828f25c90e3021ca2c29ddebb291bf9))
10
+
5
11
  ## [7.3.5](https://github.com/ViktorsBaikers/DevRites/compare/v7.3.4...v7.3.5) (2026-11-05)
6
12
 
7
13
  ### Fixed
@@ -53,6 +59,16 @@ All notable changes to DevRites are documented here. The format follows [Keep a
53
59
 
54
60
  ### Fixed
55
61
 
62
+ - Guard Autocomplete's early phase context and dispatch, not only Build writers,
63
+ against unstarted successor preparation. Keep normalization research reachable
64
+ and bound successor preparation before its first wave without recursively
65
+ requiring the rest of the roadmap.
66
+ - Keep Clarify required when decision coverage is missing or empty despite an
67
+ empty question queue.
68
+ - Preserve preparation intent and the selected host tree in suggested context
69
+ reruns; reject repeated parent flags before writes and retain settled history
70
+ without rebinding active preparation actors.
71
+
56
72
  - Give an explicitly identified Autocomplete controller structural writer-admission
57
73
  checks for actual successor preparation and source-qualified mixed reservations.
58
74
  Surface fresh global scheduling after returns and prioritize unresolved global
package/README.md CHANGED
@@ -31,7 +31,7 @@ history can't lose it. After you clear the conversation, switch hosts, or hand
31
31
  the feature to another engineer, the next session reads the same plan,
32
32
  decisions, and evidence before it continues.
33
33
 
34
- **Status:** [`v7.3.5`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v7.3.5): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
34
+ **Status:** [`v7.3.6`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v7.3.6): 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
 
@@ -0,0 +1,76 @@
1
+ # ADR-0040: Early preparation admission without recursive roadmap blocking
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-11-06
5
+
6
+ ## Context
7
+
8
+ ADR-0039 guarded Build-wright admission. A subsequent consumer trace entered
9
+ Temper and launched native review without starting its recorded successor;
10
+ it consumed a context bundle before any dispatch account. Writer-only checks
11
+ therefore left the earlier preparation path unguarded.
12
+
13
+ Requiring every source to prepare its successor immediately would also recurse
14
+ through the entire roadmap: preparing B for A would demand C before A's own
15
+ work. Sequence normalization needs a read-only bootstrap that cannot itself
16
+ be blocked by the authority it investigates.
17
+
18
+ ## Decision
19
+
20
+ Extend ADR-0039's deterministic admission boundary to ordinary local
21
+ Temper/Define/Plan/Vet context generation and dispatch opening in Autocomplete
22
+ at effective capacity greater than one. Both use one validation owner, before
23
+ workflow writes. Context does not require an open account: reviewer context
24
+ and packet construction precede wave opening. Build keeps its full admission.
25
+
26
+ - Internal `--prepare-for <parent-source>` identifies existing immediate-successor
27
+ preparation. Canonical identity, parent binding, adjacent positions and both
28
+ chains must agree. Only supported preparation phases/roles qualify; root
29
+ context may omit a role. Dispatch records optional `preparation_for`; context
30
+ records no account. Neither permits product writes or Build.
31
+ - Internal `--sequence-research` permits only exact-source, read-only
32
+ `evidence-scout` investigation in preparation phases before the authority gate.
33
+ It is mutually exclusive with `--prepare-for`. Dispatch records optional
34
+ `sequence_research`. The exception grants no scope, approval, phase advancement,
35
+ successor creation, writer admission, readiness or upstream preparation credit.
36
+ - Derive preparatory-only B from its validated immediate nonterminal parent A,
37
+ absence of ordinary forward/Build-wright admission and absence of normal handoff to B.
38
+ This applies before the first marked wave. Observe B's continuation without
39
+ making C an actionable prerequisite for A. Ordinary B admission still checks
40
+ C before publication; that admission, terminal A or normal ordered handoff
41
+ releases the routing boundary. ACTIVE selection represents native handoff,
42
+ not proof of implementation or approval.
43
+ Read-only Build accounts do not release it. Explicit descendant `next` and
44
+ `check continuation` honor inherited waiting instead of mandating a local cursor;
45
+ other actionable global work and independently admitted proof remain visible.
46
+ - Preserve completed account history and existing pending/abandoned actor and
47
+ unfinished writer integration protection. Settled purpose bindings remain
48
+ historical after authorized replanning, not current-parent authority or new
49
+ preparation credit; unsettled mismatches still fail closed.
50
+ - A resolved finite tail needs no successor. New-opening opt-out or budget
51
+ exhaustion does not erase an already-created child. Existing narrow deferrals,
52
+ human gates, claims, capacity, isolation and protected proof remain authoritative.
53
+
54
+ No new user Autocomplete flag, model client, launch bridge, receipt or queue.
55
+ Standalone/cap1 and initial Spec/Clarify/bootstrap behavior remain unchanged.
56
+ Actual native launches still require registered host handles; declared purpose
57
+ metadata does not prove profile, checkout or semantic progress.
58
+
59
+ ## Alternatives considered
60
+
61
+ | Option | Why rejected |
62
+ | --- | --- |
63
+ | Keep the gate only on Build waves | Earlier context and native planning can bypass it. |
64
+ | Require an open account before producing context | Reviewer opening requires a packet built from that context. |
65
+ | Require B's successor before admitting B's preparation for A | Recurses through the roadmap instead of pipelining one successor. |
66
+ | Use a preparation marker as the only routing boundary | The creation/context interval precedes the first marked wave. |
67
+ | Treat normalization research as ordinary planning admission | Conflicting authority blocks its own investigation. |
68
+
69
+ ## Consequences
70
+
71
+ The engine rejects the observed early CLI path, not merely its eventual writer.
72
+ Native hosts retain semantic scope, ordered handoff, exact profile/model/cwd,
73
+ result reconciliation and publication authority. Arbitrary host compliance is
74
+ not an engine guarantee.
75
+
76
+ Guard regressions: `engine/internal/lib/frontier_preparation_test.go`.
@@ -85,3 +85,4 @@ architecture; per-feature `decisions.md` files stay scoped to that feature.
85
85
  | [0037](0037-autocomplete-lands-local-checkpoints.md) | Autocomplete lands local checkpoints and self-resolves own scratch | Accepted; supersedes the ADR-0022 local-commit approval clause | `tests/authority-drift-test.sh` |
86
86
  | [0038](0038-engine-flags-unattributed-root-edits.md) | The engine flags unattributed root edits | Accepted; narrows the ADR-0018 no-replacement-enforcement clause for root-edit attribution | `engine/internal/lib/treebaseline_test.go` |
87
87
  | [0039](0039-autocomplete-structural-admission.md) | Structural admission for native Autocomplete | Accepted; narrowly amends 0022/0024 for observable writer admission, retains native dispatch and semantic authority | `engine/internal/lib/frontier_controller_test.go`, `engine/internal/lib/frontier_roadmap_test.go`, `engine/tests/autocomplete_global_test.go` |
88
+ | [0040](0040-early-preparation-admission.md) | Early preparation admission without recursive roadmap blocking | Accepted; extends 0039 to context and pre-Build native dispatch, preserves native execution and semantic authority | `engine/internal/lib/frontier_preparation_test.go` |
@@ -59,6 +59,10 @@ workflow strategy.
59
59
  | `overhaul <records\|admit\|snapshot\|score\|bench\|render> ...` | Deterministic tools behind the standalone `/overhaul` skill: run-record generations and cross-record validation, worker-receipt admission and quote anchoring, index-safe baseline snapshots and per-attempt deltas, exact-rational readiness scoring, benchmark ratio intervals, and escaped offline review/report views. Operates only on explicit run-area and repository paths. See [Overhaul tools](#overhaul-tools). |
60
60
  | `version` | Print the engine version. |
61
61
 
62
+ `next` skips zero-question Clarify only when its required artifacts are present
63
+ and nonempty. Missing or empty `decision-coverage.md` still routes to Clarify;
64
+ an empty question queue does not establish decision coverage.
65
+
62
66
  Under AFK, `next`, `handoff`, and `check continuation` include the sequence
63
67
  frontier without replacing the source-local cursor. Reporting actionable work
64
68
  does not authorize Build or Ship, and cannot guarantee native-host compliance.
@@ -71,12 +75,50 @@ archived/related owners absent from the local context read-next bundle.
71
75
  Native `/rite-autocomplete` automatically upserts AFK `mode: autocomplete`
72
76
  (no new flag), preserving other fields except an explicit `--parallel N` capacity
73
77
  update. Missing mode retains standalone AFK behavior; invalid/duplicate mode
74
- fails closed. With mode and global cap >1, Build wright `dispatch open` checks
75
- global scheduling before claims/history/round writes: prepare one immediate
76
- recorded successor per source, with actual pending planning/review handles,
77
- current native readiness, or a concrete human/dependency hold; placeholders do
78
- not count. Opt-out, exhausted opening budgets, and source human/question holds
79
- remain binding. Unresolved authority requires native normalization.
78
+ fails closed. With mode and effective global cap >1, ordinary local
79
+ Temper/Define/Plan/Vet `context` and `dispatch open` check immediate-successor
80
+ preparation before bundle/account/claim/history writes. The context check also
81
+ applies to `--skill rite-<phase>`; it does not require an already-open wave.
82
+ Build-wright admission retains its additional mixed-cohort and writer checks.
83
+ Actual pending native planning/review handles, current readiness/completion, or
84
+ a concrete human/dependency hold satisfy preparation; placeholders do not.
85
+ Valid finite tails have no successor obligation. Opt-out and exhausted opening
86
+ budgets prevent new openings, not planning an existing child. Standalone/cap1
87
+ and initial Spec/Clarify/bootstrap reads retain their existing behavior.
88
+
89
+ Both commands accept internal preparation intents, not new Autocomplete flags:
90
+
91
+ | Option | Meaning |
92
+ | --- | --- |
93
+ | `--prepare-for <parent-source>` | Prepare the existing exact immediate successor of that parent. Validates canonical identity, parent binding, adjacent positions and both chains. Only supported preparation phases/roles qualify; controlling-root context may omit a role, dispatch may not. Never Build/writers or product-write authority. |
94
+ | `--sequence-research` | Read-only normalization investigation by `evidence-scout` in Spec/Clarify/Temper/Define/Plan/Vet, bound to the exact existing source. Can inspect conflicting/malformed sequence authority; grants no normalization acceptance, scope, phase advancement, successor creation, writer admission, readiness or upstream preparation credit. |
95
+
96
+ The options are mutually exclusive; dispatch accepts them on `open` only.
97
+ Repeated `--prepare-for` is a syntax error, even with the same parent.
98
+ Syntax/combination errors exit `2`; refused preparation/authority exits `3`
99
+ before workflow writes. Successful admission uses the existing `0` convention.
100
+ Dispatch retains optional wave fields `preparation_for` (string) and
101
+ `sequence_research` (boolean); context creates no account or receipt. Old
102
+ accounts need neither field. These fields record intent, not host execution.
103
+ Keep context → packet → open → native launch → start order; register actual
104
+ host handles before resuming the parent's ordinary phase.
105
+ Suggested trigger-rerun commands preserve the preparation intent and explicitly
106
+ selected skills root, including shell-safe quoting of that path.
107
+
108
+ A newly created preparatory child B of nonterminal A does not recursively
109
+ require C before A can proceed, even before B's first marked wave. Completed
110
+ preparation history retains that routing boundary. Ordinary unmarked forward
111
+ or Build-wright admission for B still checks C; such admission, terminal A, or normal
112
+ ordered handoff selecting B restores B's own successor obligation. Pending
113
+ actors, unfinished writer integration and other independent work remain visible.
114
+ Read-only Build accounts do not release the bound. When an inherited descendant
115
+ waits, explicit `next <descendant>` emits no local phase command and
116
+ `check continuation <descendant>` emits `continuation: clear`, unless other
117
+ actionable global work takes priority. Observing a source does not change ACTIVE.
118
+ Purpose history stays immutable and does not make completed actors live again.
119
+ Settled purpose bindings remain historical after an authorized replan; they
120
+ neither bind the current parent nor supply new preparation credit. Unsettled
121
+ mismatched actors still fail closed.
80
122
 
81
123
  Source `evidence.md` may retain `frontier-blocked: <successor-slug> — <specific constraint>`
82
124
  or `frontier-blocked: sequence — <specific constraint>` as a visible scheduling
@@ -40,7 +40,7 @@ type loadsManifest struct {
40
40
 
41
41
  const loadsMarker = "<!-- loads:"
42
42
 
43
- const contextUsage = `usage: devrites-engine context [slug] (--phase <p> | --skill <name>) [--role <r>] [--slice <SLICE-###>] [--recheck <inventory>] [--trigger a,b] [--inline-contract] [--whole] [--skills-root <dir>] [--out <path>]
43
+ const contextUsage = `usage: devrites-engine context [slug] (--phase <p> | --skill <name>) [--role <r>] [--prepare-for <parent-source> | --sequence-research] [--slice <SLICE-###>] [--recheck <inventory>] [--trigger a,b] [--inline-contract] [--whole] [--skills-root <dir>] [--out <path>]
44
44
 
45
45
  Emits one deduplicated context bundle for a phase or standalone skill
46
46
  (+ optional dispatch role) into .devrites/work/<slug>/ctx/ so an agent
@@ -65,6 +65,12 @@ build or autocomplete bundle (no --role, no --slice) defaults to --slice
65
65
  bounded recheck bundle: the inventory file (relative to the workspace)
66
66
  and core.md are inlined; every other pack file becomes a by-reference
67
67
  index plus the sections the inventory cites as "<file>.md § <heading>".
68
+ Autocomplete at effective cap>1 gates Temper/Define/Plan/Vet bundles, including
69
+ --skill rite-<phase>, on immediate-successor preparation before writing.
70
+ --prepare-for binds native preparation to its exact immediate parent; roleless
71
+ controlling-root context is allowed. --sequence-research permits only exact
72
+ evidence-scout investigation of sequence authority. Neither creates an account
73
+ or grants product-write authority; research does not satisfy successor preparation.
68
74
  `
69
75
 
70
76
  // coreStandard is the universal floor every bundle inlines whole.
@@ -73,12 +79,27 @@ const coreStandard = "devrites-lib/reference/standards/core.md"
73
79
  // RunContext emits one deduplicated context bundle for a phase (+ optional
74
80
  // dispatch role) so an agent reads a single file instead of selecting files.
75
81
  //
76
- // context <slug> --phase <p> [--role <r>] [--slice <SLICE-###>] [--recheck <inventory>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
82
+ // context <slug> --phase <p> [--role <r>] [--prepare-for <parent-source> | --sequence-research] [--slice <SLICE-###>] [--recheck <inventory>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
77
83
  func RunContext(root string, args []string, stdout, stderr io.Writer) int {
78
84
  opts, code := parseContextArgs(args, stderr)
79
85
  if code != 0 {
80
86
  return code
81
87
  }
88
+ phase := opts.phase
89
+ if opts.skill != "" {
90
+ phase = ""
91
+ if strings.HasPrefix(opts.skill, "rite-") {
92
+ phase = strings.TrimPrefix(opts.skill, "rite-")
93
+ }
94
+ }
95
+ var roles []string
96
+ if opts.role != "" {
97
+ roles = []string{opts.role}
98
+ }
99
+ if err := opts.purpose.validate(phase, roles, true); err != nil {
100
+ fmt.Fprintf(stderr, "context: %v\n", err)
101
+ return 2
102
+ }
82
103
  // A Build reviewer judges one slice; an unscoped bundle inlines whole
83
104
  // plan/tasks/architecture into every dispatch.
84
105
  if opts.phase == "build" && opts.role != "" && roleBase(opts.role) != "slice-wright" && opts.slice == "" {
@@ -94,6 +115,19 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
94
115
  return 2
95
116
  }
96
117
  }
118
+ if opts.slug != "" {
119
+ notices, err := preparationAdmission(root, opts.slug, phase, opts.purpose, nil)
120
+ if err != nil {
121
+ fmt.Fprintf(stderr, "context: BLOCKED: %v\n", err)
122
+ return 3
123
+ }
124
+ for _, notice := range notices {
125
+ fmt.Fprintln(stdout, notice)
126
+ }
127
+ } else if opts.purpose.parent != "" || opts.purpose.research {
128
+ fmt.Fprintln(stderr, "context: preparation purpose requires an exact existing source")
129
+ return 2
130
+ }
97
131
  sources := contextSources{dir: featureDir}
98
132
  // One active slice per bundle, except a parallel batch: the root has to
99
133
  // see the other slices to start them. Whole tasks/plan/decisions ride
@@ -791,6 +825,7 @@ type contextOpts struct {
791
825
  recheck string
792
826
  inlineContract bool
793
827
  whole bool
828
+ purpose preparationPurpose
794
829
  }
795
830
 
796
831
  func parseContextArgs(args []string, stderr io.Writer) (contextOpts, int) {
@@ -799,6 +834,19 @@ func parseContextArgs(args []string, stderr io.Writer) (contextOpts, int) {
799
834
  for i := 0; i < len(args); i++ {
800
835
  value := argAt(args, i+1)
801
836
  switch args[i] {
837
+ case "--prepare-for":
838
+ if opts.purpose.parent != "" {
839
+ fmt.Fprintln(stderr, "context: duplicate --prepare-for")
840
+ return opts, 2
841
+ }
842
+ if value == "" || strings.HasPrefix(value, "-") {
843
+ fmt.Fprintln(stderr, "context: --prepare-for requires a parent source")
844
+ return opts, 2
845
+ }
846
+ opts.purpose.parent = value
847
+ case "--sequence-research":
848
+ opts.purpose.research = true
849
+ continue
802
850
  case "--phase":
803
851
  opts.phase = value
804
852
  case "--skill":
@@ -59,6 +59,8 @@ func RunCheckContinuation(root string, args []string, stdout, stderr io.Writer)
59
59
  }
60
60
  if mode && frontierGlobalPriority(slug, frontier) || !mode && !verdict.Required && len(frontier) > 0 && frontier[0] == "frontier: actionable" {
61
61
  verdict = continuationVerdict{Required: true, Invoke: "/rite-autocomplete", Source: slug, Reason: "global frontier requires native preparation, authority review, or selection; no Ship approval"}
62
+ } else if mode && frontierSourcePreparationWait(slug, frontier) {
63
+ verdict = continuationVerdict{Reason: frontierInheritedPreparationWait}
62
64
  }
63
65
  }
64
66
  writeContinuation(stdout, verdict)
@@ -27,9 +27,9 @@ import (
27
27
  // fan-out cannot silently degrade to serial (open -> every role started ->
28
28
  // seal -> returns -> complete). start/return also auto-record metrics events.
29
29
  //
30
- // devrites-engine dispatch <slug> <sub> [--phase p] [--wave w] [--role r] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>] [--contract <file>] [--packet <file>] [--outcome findings|no-findings|gap] [--result <file>] [--all] [--claim-session <id> --claim <path>... [--claim-ttl <minutes>]] [--release] [--path <path>...]
30
+ // devrites-engine dispatch <slug> <sub> [--phase p] [--wave w] [--role r] [--prepare-for <parent-source> | --sequence-research] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>] [--contract <file>] [--packet <file>] [--outcome findings|no-findings|gap] [--result <file>] [--all] [--claim-session <id> --claim <path>... [--claim-ttl <minutes>]] [--release] [--path <path>...]
31
31
 
32
- const dispatchUsage = `usage: devrites-engine dispatch <slug> <open|start|seal|return|status|abandon|attribute> [--phase p] [--wave w] [--role r] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>] [--contract <file>] [--packet <file>] [--outcome findings|no-findings|gap] [--result <file>] [--all] [--claim-session <id> --claim <path>... [--claim-ttl <minutes>]] [--release] [--path <path>...]
32
+ const dispatchUsage = `usage: devrites-engine dispatch <slug> <open|start|seal|return|status|abandon|attribute> [--phase p] [--wave w] [--role r] [--prepare-for <parent-source> | --sequence-research] [--handle h] [--reason s] [--slice SLICE-###] [--reslice-review <DRIFT-ID>] [--contract <file>] [--packet <file>] [--outcome findings|no-findings|gap] [--result <file>] [--all] [--claim-session <id> --claim <path>... [--claim-ttl <minutes>]] [--release] [--path <path>...]
33
33
 
34
34
  Wave barrier for parallel agent dispatch: seal fails until every declared
35
35
  role has a distinct handle; return fails before seal. The start that gives
@@ -40,6 +40,12 @@ completes the wave releases it, and abandon keeps it unless --release (any
40
40
  host-confirmed terminal path without a completing return, such as a stop or a
41
41
  refused launch). Wave IDs are single-use, including abandoned waves; retries
42
42
  need a fresh ID and never inherit or release an earlier wave's claim.
43
+ Autocomplete at effective cap>1 checks immediate-successor preparation before
44
+ ordinary Temper/Define/Plan/Vet open writes. Open-only --prepare-for binds allowed
45
+ native preparation roles to an exact immediate parent without recursively
46
+ requiring another successor. Open-only --sequence-research admits exact
47
+ evidence-scout authority investigation, never successor preparation or writers.
48
+ Both require an existing source and persist their purpose on the wave.
43
49
  Native starts, returns and abandonment retain subsecond timestamps. abandon
44
50
  --release also retains host-terminal confirmation, even without a claim or when
45
51
  its owned claim was already released; a failed release does not certify termination.
@@ -105,20 +111,22 @@ type dispatchFile struct {
105
111
  }
106
112
 
107
113
  type dispatchWave struct {
108
- Phase string `json:"phase"`
109
- State string `json:"state"` // open, sealed, abandoned, complete
110
- Reason string `json:"reason,omitempty"`
111
- AbandonedAt string `json:"abandoned_at,omitempty"`
112
- TerminalAt string `json:"terminal_at,omitempty"`
113
- Roles map[string]*dispatchRole `json:"roles"`
114
- Slices []string `json:"slices,omitempty"`
115
- ResliceReview string `json:"reslice_review,omitempty"`
116
- ResliceReviewed []string `json:"reslice_reviewed,omitempty"`
117
- Claim string `json:"claim,omitempty"`
118
- ClaimSession string `json:"claim_session,omitempty"`
119
- Tree string `json:"tree,omitempty"` // work tree at a build wright open, for check windows --since-dispatch
120
- ControllerBatch string `json:"controller_batch,omitempty"` // cohort admission identity, not evidence of actor checkout
121
- AdmittedPaths map[string][]string `json:"admitted_paths,omitempty"` // immutable normalized write sets, keyed by source-local slice
114
+ Phase string `json:"phase"`
115
+ State string `json:"state"` // open, sealed, abandoned, complete
116
+ Reason string `json:"reason,omitempty"`
117
+ AbandonedAt string `json:"abandoned_at,omitempty"`
118
+ TerminalAt string `json:"terminal_at,omitempty"`
119
+ Roles map[string]*dispatchRole `json:"roles"`
120
+ Slices []string `json:"slices,omitempty"`
121
+ ResliceReview string `json:"reslice_review,omitempty"`
122
+ ResliceReviewed []string `json:"reslice_reviewed,omitempty"`
123
+ Claim string `json:"claim,omitempty"`
124
+ ClaimSession string `json:"claim_session,omitempty"`
125
+ Tree string `json:"tree,omitempty"` // work tree at a build wright open, for check windows --since-dispatch
126
+ ControllerBatch string `json:"controller_batch,omitempty"` // cohort admission identity, not evidence of actor checkout
127
+ AdmittedPaths map[string][]string `json:"admitted_paths,omitempty"` // immutable normalized write sets, keyed by source-local slice
128
+ PreparationFor string `json:"preparation_for,omitempty"`
129
+ SequenceResearch bool `json:"sequence_research,omitempty"`
122
130
  }
123
131
 
124
132
  // dispatchClaim carries the wave-claim flags; ttl 0 means unset.
@@ -179,6 +187,16 @@ func decodeDispatch(data []byte) (*dispatchFile, error) {
179
187
  !slices.Contains([]string{"open", "sealed", "complete", "abandoned"}, w.State) {
180
188
  return nil, fmt.Errorf("wave %q is malformed (invalid phase, state or roles)", name)
181
189
  }
190
+ purpose := preparationPurpose{parent: w.PreparationFor, research: w.SequenceResearch}
191
+ if purpose.parent != "" || purpose.research {
192
+ roles := make([]string, 0, len(w.Roles))
193
+ for role := range w.Roles {
194
+ roles = append(roles, role)
195
+ }
196
+ if err := purpose.validate(w.Phase, roles, false); err != nil {
197
+ return nil, fmt.Errorf("wave %q has invalid preparation purpose: %w", name, err)
198
+ }
199
+ }
182
200
  handles := map[string]bool{}
183
201
  allStarted, allReturned := true, true
184
202
  for role, r := range w.Roles {
@@ -254,16 +272,33 @@ func saveDispatch(path string, f *dispatchFile) error {
254
272
  func nowStamp() string { return time.Now().UTC().Format(time.RFC3339Nano) }
255
273
 
256
274
  // RunDispatch implements the dispatch wave state machine.
275
+ //
276
+ // Preparation-purpose flags apply to open only; ordinary early-phase admission
277
+ // and context generation share immediate-successor validation.
257
278
  func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
258
279
  var slug, sub, phase, wave, role, handle, reason, review, contract, packet, outcome, result string
259
280
  var roles, sliceIDs []string
260
281
  var positional []string
261
282
  all := false
283
+ var purpose preparationPurpose
262
284
  var claim dispatchClaim
263
285
  var attributed []string
264
286
  for i := 0; i < len(args); i++ {
265
287
  value := argAt(args, i+1)
266
288
  switch args[i] {
289
+ case "--prepare-for":
290
+ if purpose.parent != "" {
291
+ fmt.Fprintln(stderr, "dispatch: duplicate --prepare-for")
292
+ return 2
293
+ }
294
+ if value == "" || strings.HasPrefix(value, "-") {
295
+ fmt.Fprintln(stderr, "dispatch: --prepare-for requires a parent source")
296
+ return 2
297
+ }
298
+ purpose.parent = value
299
+ case "--sequence-research":
300
+ purpose.research = true
301
+ continue
267
302
  case "--phase":
268
303
  phase = value
269
304
  case "--wave":
@@ -321,6 +356,16 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
321
356
  return 2
322
357
  }
323
358
  slug, sub = positional[0], positional[1]
359
+ if purpose.parent != "" || purpose.research {
360
+ if sub != "open" {
361
+ fmt.Fprintln(stderr, "dispatch: preparation purpose applies to open only")
362
+ return 2
363
+ }
364
+ if err := purpose.validate(phase, roles, false); err != nil {
365
+ fmt.Fprintf(stderr, "dispatch: %v\n", err)
366
+ return 2
367
+ }
368
+ }
324
369
  if sub != "open" && (claim.session != "" || len(claim.paths) > 0 || claim.ttl != 0) {
325
370
  fmt.Fprintln(stderr, "dispatch: --claim-session, --claim, and --claim-ttl apply to open only")
326
371
  return 2
@@ -348,7 +393,7 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
348
393
  return state.WithFeatureLock(root, slug, func() error {
349
394
  // Controller → feature → claims; never acquire another feature.
350
395
  return state.WithLock(claimsPath(root)+".lock", func() error {
351
- code = dispatchLocked(root, slug, sub, phase, wave, role, handle, reason, roles, sliceIDs, review, contract, packet, outcome, result, all, claim, stdout, stderr, &recordedReturn)
396
+ code = dispatchLocked(root, slug, sub, phase, wave, role, handle, reason, roles, sliceIDs, review, contract, packet, outcome, result, all, claim, purpose, stdout, stderr, &recordedReturn)
352
397
  return nil
353
398
  })
354
399
  })
@@ -359,13 +404,16 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
359
404
  controllerOpen = controllerOpen || strings.Contains(candidate, "wright")
360
405
  }
361
406
  }
407
+ if sub == "open" && (earlyPreparationPhase(phase) || purpose.parent != "" || purpose.research) {
408
+ controllerOpen = true
409
+ }
362
410
  if controllerOpen {
363
411
  mode, err := autocompleteMode(root)
364
412
  if err != nil {
365
413
  fmt.Fprintf(stderr, "dispatch: BLOCKED: %v\n", err)
366
414
  return 3
367
415
  }
368
- controllerOpen = mode
416
+ controllerOpen = mode || purpose.parent != "" || purpose.research
369
417
  }
370
418
  var lockErr error
371
419
  if controllerOpen {
@@ -397,7 +445,7 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
397
445
  return code
398
446
  }
399
447
 
400
- func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, roles, sliceIDs []string, review, contract, packet, outcome, result string, all bool, claim dispatchClaim, stdout, stderr io.Writer, recordedReturn *bool) int {
448
+ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, roles, sliceIDs []string, review, contract, packet, outcome, result string, all bool, claim dispatchClaim, purpose preparationPurpose, stdout, stderr io.Writer, recordedReturn *bool) int {
401
449
  f, path, err := loadDispatch(root, slug)
402
450
  if err != nil {
403
451
  fmt.Fprintf(stderr, "dispatch: %v\n", err)
@@ -425,7 +473,7 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
425
473
  if prev := getWave(); prev != nil {
426
474
  return fail("wave %q already exists (state %s)", wave, prev.State)
427
475
  }
428
- w := &dispatchWave{Phase: phase, State: "open", Roles: map[string]*dispatchRole{}, Slices: sliceIDs}
476
+ w := &dispatchWave{Phase: phase, State: "open", Roles: map[string]*dispatchRole{}, Slices: sliceIDs, PreparationFor: purpose.parent, SequenceResearch: purpose.research}
429
477
  for _, r := range roles {
430
478
  if w.Roles[r] != nil {
431
479
  return fail("duplicate role %q in wave", r)
@@ -435,6 +483,13 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
435
483
  if err := lintKnownRoles(root, phase, roles); err != nil {
436
484
  return fail("BLOCKED: %v", err)
437
485
  }
486
+ notices, err := preparationAdmission(root, slug, phase, purpose, f)
487
+ if err != nil {
488
+ return fail("BLOCKED: %v", err)
489
+ }
490
+ for _, notice := range notices {
491
+ fmt.Fprintln(stdout, notice)
492
+ }
438
493
  if isBuildWrightWave(w) {
439
494
  binding, notices, err := autocompleteWriterAdmission(root, slug, w, f)
440
495
  if err != nil {
@@ -301,7 +301,7 @@ func FrontierLines(root, slug string) ([]string, error) {
301
301
  return []string{
302
302
  "frontier: blocked", "frontier-source: " + slug,
303
303
  fmt.Sprintf("frontier-workspace: %s location=%s action=repair reason=%s", slug, source.location, chainErr),
304
- "frontier-review-sequence: " + artifact + " reason=native authority-normalization required: " + chainErr.Error(),
304
+ "frontier-review-sequence: reason=native authority-normalization required: " + chainErr.Error() + " artifacts=" + artifact,
305
305
  }, nil
306
306
  }
307
307
  ordered, related := frontierRelated(nodes, chain)
@@ -351,7 +351,7 @@ func FrontierLines(root, slug string) ([]string, error) {
351
351
  }
352
352
  maps, artifacts, mapProblem, invalidCanonical := frontierRoadmap(nodes, ordered)
353
353
  if invalidCanonical {
354
- return []string{"frontier: blocked", "frontier-source: " + slug, "frontier-review-sequence: " + strings.Join(artifacts, ",") + " reason=native authority-normalization required: invalid canonical authority: " + mapProblem}, nil
354
+ return []string{"frontier: blocked", "frontier-source: " + slug, "frontier-review-sequence: reason=native authority-normalization required: invalid canonical authority: " + mapProblem + " artifacts=" + strings.Join(artifacts, ",")}, nil
355
355
  }
356
356
  if len(maps) > 0 && mapProblem == "" {
357
357
  lines = append(lines, "frontier-authority: "+strings.Join(artifacts, ","))
@@ -368,12 +368,24 @@ func FrontierLines(root, slug string) ([]string, error) {
368
368
  if sequenceDeferred {
369
369
  lines = append(lines, "frontier-blocked: sequence — "+records.blocked["sequence"]+"; scheduling deferred, not normalized")
370
370
  } else {
371
- lines = append(lines, "frontier-review-sequence: "+strings.Join(artifacts, ",")+" reason=native authority-normalization required: "+mapProblem)
371
+ lines = append(lines, "frontier-review-sequence: reason=native authority-normalization required: "+mapProblem+" artifacts="+strings.Join(artifacts, ","))
372
372
  blocked = true
373
373
  }
374
374
  }
375
375
  deferred := map[string]string{}
376
376
  prepared := map[string]bool{}
377
+ var preparatory map[string]bool
378
+ var preparationAccounts map[string]*dispatchFile
379
+ if mode && capN > 1 {
380
+ preparatory = make(map[string]bool, len(ordered))
381
+ preparationAccounts = make(map[string]*dispatchFile, len(ordered))
382
+ for _, name := range ordered {
383
+ preparatory[name], preparationAccounts[name], err = frontierPreparatoryOnly(root, nodes[name], nodes, maps, mapProblem)
384
+ if err != nil {
385
+ return nil, err
386
+ }
387
+ }
388
+ }
377
389
  for _, name := range ordered {
378
390
  n := nodes[name]
379
391
  action, reason := "complete", "archived predecessor; no writer admission"
@@ -386,17 +398,21 @@ func FrontierLines(root, slug string) ([]string, error) {
386
398
  } else if gateReason != "" {
387
399
  action, reason, held, blocked = "wait", gateReason, true, true
388
400
  } else if n.location == "live" {
389
- action, reason, err = frontierLocal(root, controller, n, &candidates)
401
+ action, reason, err = frontierLocal(root, controller, n, &candidates, preparationAccounts[name])
390
402
  if err != nil {
391
403
  action, reason, n.selectionHeld = "repair", err.Error(), true
392
404
  }
393
405
  if mode && action == "prepare" && deferred[name] != "" {
394
406
  action, reason, n.selectionHeld = "wait", "frontier-blocked: "+name+" — "+deferred[name], true
395
407
  }
408
+ inheritedPreparation := preparatory[name] && preparatory[n.parent] && (n.phase == "frame" || preparationPhase(n.phase)) && action == "prepare"
409
+ if inheritedPreparation {
410
+ action, reason, n.selectionHeld = "wait", frontierInheritedPreparationWait, true
411
+ }
396
412
  if !n.selectionHeld {
397
413
  live = append(live, name)
398
414
  }
399
- held = action == "blocked" || err != nil || mode && action == "wait" && deferred[name] != ""
415
+ held = inheritedPreparation || action == "blocked" || err != nil || mode && action == "wait" && deferred[name] != ""
400
416
  switch action {
401
417
  case "inspect", "prepare", "repair":
402
418
  actionable = true
@@ -411,6 +427,9 @@ func FrontierLines(root, slug string) ([]string, error) {
411
427
  }
412
428
  }
413
429
  lines = append(lines, fmt.Sprintf("frontier-workspace: %s location=%s action=%s reason=%s", name, n.location, action, reason))
430
+ if preparatory[name] {
431
+ continue
432
+ }
414
433
  if invalid != nil || held || !allowFuture || n.hasRemaining && n.remaining == 0 || n.status == "awaiting_human" || n.status == "blocked" || n.question != "" {
415
434
  continue
416
435
  }
@@ -425,12 +444,12 @@ func FrontierLines(root, slug string) ([]string, error) {
425
444
  continue
426
445
  }
427
446
  if target != "" && frontierConflictingSuccessor(nodes, n, target) {
428
- lines = append(lines, "frontier-review-sequence: "+strings.Join(artifacts, ",")+" source="+name+" reason=conflicting successor identities; native authority-normalization required")
447
+ lines = append(lines, "frontier-review-sequence: reason=conflicting successor identities; native authority-normalization required source="+name+" artifacts="+strings.Join(artifacts, ","))
429
448
  actionable, blocked = true, true
430
449
  continue
431
450
  }
432
451
  if target != "" && n.hasPosition && maps[n.position+1] != "" && maps[n.position+1] != target {
433
- lines = append(lines, "frontier-review-sequence: "+strings.Join(artifacts, ",")+" source="+name+" reason=next Spec conflicts with recorded successor; native authority-normalization required")
452
+ lines = append(lines, "frontier-review-sequence: reason=next Spec conflicts with recorded successor; native authority-normalization required source="+name+" artifacts="+strings.Join(artifacts, ","))
434
453
  actionable, blocked = true, true
435
454
  continue
436
455
  }
@@ -459,7 +478,7 @@ func FrontierLines(root, slug string) ([]string, error) {
459
478
  lines = append(lines, fmt.Sprintf("frontier-workspace: %s location=missing action=prepare reason=recorded next workspace; native scope and independence review required", target), fmt.Sprintf("frontier-prepare: /rite-spec %s source=%s reason=read recorded sequence authority before preparation; no workspace created", frontierQuote(target), name))
460
479
  actionable = true
461
480
  } else if target == "" && n.hasRemaining && n.remaining > 0 {
462
- lines = append(lines, "frontier-review-sequence: "+strings.Join(artifacts, ",")+" source="+name+" reason=remaining work unresolved; native authority-normalization required")
481
+ lines = append(lines, "frontier-review-sequence: reason=remaining work unresolved; native authority-normalization required source="+name+" artifacts="+strings.Join(artifacts, ","))
463
482
  actionable, blocked = true, true
464
483
  }
465
484
  }
@@ -574,7 +593,7 @@ func frontierQuestionHold(n *frontierWorkspace) (string, error) {
574
593
  return "", nil
575
594
  }
576
595
 
577
- func frontierLocal(root, controller string, n *frontierWorkspace, candidates *[]parallel.SlicePaths) (string, string, error) {
596
+ func frontierLocal(root, controller string, n *frontierWorkspace, candidates *[]parallel.SlicePaths, dispatch *dispatchFile) (string, string, error) {
578
597
  n.selectionHeld = true
579
598
  if frontierTerminal(n) {
580
599
  return "complete", "terminal workspace", nil
@@ -585,15 +604,20 @@ func frontierLocal(root, controller string, n *frontierWorkspace, candidates *[]
585
604
  if n.status != "running" && n.status != "ready" {
586
605
  return "blocked", "status lacks an actionable native cursor; normalize state without inventing authority", nil
587
606
  }
588
- raw, err := readBoundedRegularFile(filepath.Join(n.dir, "dispatch.json"), workspaceArtifactLimit)
589
- if err != nil && !os.IsNotExist(err) {
590
- return "", "", err
591
- }
592
- if err == nil {
593
- dispatch, err := decodeDispatch(raw)
594
- if err != nil {
607
+ if dispatch == nil {
608
+ raw, err := readBoundedRegularFile(filepath.Join(n.dir, "dispatch.json"), workspaceArtifactLimit)
609
+ if err != nil && !os.IsNotExist(err) {
595
610
  return "", "", err
596
611
  }
612
+ if err == nil {
613
+ dispatch, err = decodeDispatch(raw)
614
+ if err != nil {
615
+ return "", "", err
616
+ }
617
+ }
618
+ }
619
+ if dispatch != nil {
620
+ var err error
597
621
  retained, pending := false, false
598
622
  var built map[string]bool
599
623
  for _, wave := range dispatch.Waves {