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.
- package/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/docs/adr/0040-early-preparation-admission.md +76 -0
- package/docs/adr/README.md +1 -0
- package/docs/engine/commands.md +48 -6
- package/engine/internal/lib/context.go +50 -2
- package/engine/internal/lib/continuation.go +2 -0
- package/engine/internal/lib/dispatch.go +75 -20
- package/engine/internal/lib/frontier.go +40 -16
- package/engine/internal/lib/frontier_controller.go +53 -38
- package/engine/internal/lib/frontier_preparation.go +210 -0
- package/engine/internal/lib/next.go +5 -2
- package/engine/internal/lib/triggers.go +9 -0
- package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +7 -5
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +55 -42
- package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +7 -5
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +55 -42
- package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +7 -5
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +55 -42
- package/pack/generated/devin/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
- package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +7 -5
- package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +55 -42
- package/pack/generated/dsh/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
- package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +7 -5
- package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +55 -42
- package/pack/generated/omp/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +7 -5
- package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +55 -42
- package/pack/generated/pi/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +12 -10
- package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +7 -5
- package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +55 -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.
|
|
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`.
|
package/docs/adr/README.md
CHANGED
|
@@ -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` |
|
package/docs/engine/commands.md
CHANGED
|
@@ -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,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
109
|
-
State
|
|
110
|
-
Reason
|
|
111
|
-
AbandonedAt
|
|
112
|
-
TerminalAt
|
|
113
|
-
Roles
|
|
114
|
-
Slices
|
|
115
|
-
ResliceReview
|
|
116
|
-
ResliceReviewed
|
|
117
|
-
Claim
|
|
118
|
-
ClaimSession
|
|
119
|
-
Tree
|
|
120
|
-
ControllerBatch
|
|
121
|
-
AdmittedPaths
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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 {
|