devrites 7.3.4 → 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 +34 -0
- package/README.md +1 -1
- package/docs/adr/0039-autocomplete-structural-admission.md +87 -0
- package/docs/adr/0040-early-preparation-admission.md +76 -0
- package/docs/adr/README.md +2 -0
- package/docs/cli.md +103 -5
- package/docs/engine/commands.md +88 -1
- package/engine/internal/lib/claims.go +65 -8
- package/engine/internal/lib/context.go +50 -2
- package/engine/internal/lib/continuation.go +8 -1
- package/engine/internal/lib/dispatch.go +147 -46
- package/engine/internal/lib/frontier.go +139 -59
- package/engine/internal/lib/frontier_controller.go +667 -0
- package/engine/internal/lib/frontier_preparation.go +210 -0
- package/engine/internal/lib/frontier_roadmap.go +168 -0
- package/engine/internal/lib/next.go +14 -2
- package/engine/internal/lib/triggers.go +9 -0
- package/engine/internal/parallel/repository.go +10 -0
- package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +35 -38
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +66 -59
- package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +35 -38
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +66 -59
- package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +35 -38
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +66 -59
- package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/devin/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
- package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +35 -38
- package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +66 -59
- package/pack/generated/devin/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/dsh/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
- package/pack/generated/dsh/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/dsh/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +35 -38
- package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +66 -59
- package/pack/generated/dsh/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/omp/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
- package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +35 -38
- package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +66 -59
- package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/pi/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
- package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +35 -38
- package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +66 -59
- package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +17 -15
- package/package.json +1 -1
|
@@ -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":
|
|
@@ -40,6 +40,11 @@ func RunCheckContinuation(root string, args []string, stdout, stderr io.Writer)
|
|
|
40
40
|
}
|
|
41
41
|
return code
|
|
42
42
|
}
|
|
43
|
+
mode, err := autocompleteMode(root)
|
|
44
|
+
if err != nil {
|
|
45
|
+
fmt.Fprintf(stderr, "check continuation: %v\n", err)
|
|
46
|
+
return 2
|
|
47
|
+
}
|
|
43
48
|
verdict, err := classifyContinuation(root, slug)
|
|
44
49
|
if err != nil {
|
|
45
50
|
fmt.Fprintf(stderr, "check continuation: %v\n", err)
|
|
@@ -52,8 +57,10 @@ func RunCheckContinuation(root string, args []string, stdout, stderr io.Writer)
|
|
|
52
57
|
fmt.Fprintf(stderr, "check continuation: frontier: %v\n", err)
|
|
53
58
|
return 2
|
|
54
59
|
}
|
|
55
|
-
if !verdict.Required && len(frontier) > 0 && frontier[0] == "frontier: actionable" {
|
|
60
|
+
if mode && frontierGlobalPriority(slug, frontier) || !mode && !verdict.Required && len(frontier) > 0 && frontier[0] == "frontier: actionable" {
|
|
56
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}
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
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,18 +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
|
|
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"`
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
// dispatchClaim carries the wave-claim flags; ttl 0 means unset.
|
|
@@ -177,6 +187,16 @@ func decodeDispatch(data []byte) (*dispatchFile, error) {
|
|
|
177
187
|
!slices.Contains([]string{"open", "sealed", "complete", "abandoned"}, w.State) {
|
|
178
188
|
return nil, fmt.Errorf("wave %q is malformed (invalid phase, state or roles)", name)
|
|
179
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
|
+
}
|
|
180
200
|
handles := map[string]bool{}
|
|
181
201
|
allStarted, allReturned := true, true
|
|
182
202
|
for role, r := range w.Roles {
|
|
@@ -252,16 +272,33 @@ func saveDispatch(path string, f *dispatchFile) error {
|
|
|
252
272
|
func nowStamp() string { return time.Now().UTC().Format(time.RFC3339Nano) }
|
|
253
273
|
|
|
254
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.
|
|
255
278
|
func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
|
|
256
279
|
var slug, sub, phase, wave, role, handle, reason, review, contract, packet, outcome, result string
|
|
257
280
|
var roles, sliceIDs []string
|
|
258
281
|
var positional []string
|
|
259
282
|
all := false
|
|
283
|
+
var purpose preparationPurpose
|
|
260
284
|
var claim dispatchClaim
|
|
261
285
|
var attributed []string
|
|
262
286
|
for i := 0; i < len(args); i++ {
|
|
263
287
|
value := argAt(args, i+1)
|
|
264
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
|
|
265
302
|
case "--phase":
|
|
266
303
|
phase = value
|
|
267
304
|
case "--wave":
|
|
@@ -319,6 +356,16 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
319
356
|
return 2
|
|
320
357
|
}
|
|
321
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
|
+
}
|
|
322
369
|
if sub != "open" && (claim.session != "" || len(claim.paths) > 0 || claim.ttl != 0) {
|
|
323
370
|
fmt.Fprintln(stderr, "dispatch: --claim-session, --claim, and --claim-ttl apply to open only")
|
|
324
371
|
return 2
|
|
@@ -341,20 +388,64 @@ func RunDispatch(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
341
388
|
// Serialize load-modify-save so concurrent start/return calls never lose
|
|
342
389
|
// an update and strand the wave short of complete.
|
|
343
390
|
code := 0
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return state.
|
|
347
|
-
|
|
348
|
-
return
|
|
391
|
+
recordedReturn := false
|
|
392
|
+
mutate := func() error {
|
|
393
|
+
return state.WithFeatureLock(root, slug, func() error {
|
|
394
|
+
// Controller → feature → claims; never acquire another feature.
|
|
395
|
+
return state.WithLock(claimsPath(root)+".lock", func() error {
|
|
396
|
+
code = dispatchLocked(root, slug, sub, phase, wave, role, handle, reason, roles, sliceIDs, review, contract, packet, outcome, result, all, claim, purpose, stdout, stderr, &recordedReturn)
|
|
397
|
+
return nil
|
|
398
|
+
})
|
|
349
399
|
})
|
|
350
|
-
}
|
|
400
|
+
}
|
|
401
|
+
controllerOpen := false
|
|
402
|
+
if sub == "open" && strings.EqualFold(phase, "build") {
|
|
403
|
+
for _, candidate := range roles {
|
|
404
|
+
controllerOpen = controllerOpen || strings.Contains(candidate, "wright")
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if sub == "open" && (earlyPreparationPhase(phase) || purpose.parent != "" || purpose.research) {
|
|
408
|
+
controllerOpen = true
|
|
409
|
+
}
|
|
410
|
+
if controllerOpen {
|
|
411
|
+
mode, err := autocompleteMode(root)
|
|
412
|
+
if err != nil {
|
|
413
|
+
fmt.Fprintf(stderr, "dispatch: BLOCKED: %v\n", err)
|
|
414
|
+
return 3
|
|
415
|
+
}
|
|
416
|
+
controllerOpen = mode || purpose.parent != "" || purpose.research
|
|
417
|
+
}
|
|
418
|
+
var lockErr error
|
|
419
|
+
if controllerOpen {
|
|
420
|
+
lockErr = parallel.WithControlLock(root, mutate)
|
|
421
|
+
} else {
|
|
422
|
+
lockErr = mutate()
|
|
423
|
+
}
|
|
424
|
+
if err := lockErr; err != nil {
|
|
351
425
|
fmt.Fprintf(stderr, "dispatch: %v\n", err)
|
|
352
426
|
return 3
|
|
353
427
|
}
|
|
428
|
+
if recordedReturn {
|
|
429
|
+
mode, err := autocompleteMode(root)
|
|
430
|
+
if err != nil {
|
|
431
|
+
fmt.Fprintf(stderr, "dispatch: return recorded; global scheduling: %v\n", err)
|
|
432
|
+
return 3
|
|
433
|
+
}
|
|
434
|
+
if mode {
|
|
435
|
+
lines, err := FrontierLines(root, slug)
|
|
436
|
+
if err != nil {
|
|
437
|
+
fmt.Fprintf(stderr, "dispatch: return recorded; global scheduling: %v\n", err)
|
|
438
|
+
return 3
|
|
439
|
+
}
|
|
440
|
+
for _, line := range lines {
|
|
441
|
+
fmt.Fprintln(stdout, line)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
354
445
|
return code
|
|
355
446
|
}
|
|
356
447
|
|
|
357
|
-
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) 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 {
|
|
358
449
|
f, path, err := loadDispatch(root, slug)
|
|
359
450
|
if err != nil {
|
|
360
451
|
fmt.Fprintf(stderr, "dispatch: %v\n", err)
|
|
@@ -382,7 +473,7 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
|
|
|
382
473
|
if prev := getWave(); prev != nil {
|
|
383
474
|
return fail("wave %q already exists (state %s)", wave, prev.State)
|
|
384
475
|
}
|
|
385
|
-
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}
|
|
386
477
|
for _, r := range roles {
|
|
387
478
|
if w.Roles[r] != nil {
|
|
388
479
|
return fail("duplicate role %q in wave", r)
|
|
@@ -392,6 +483,23 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
|
|
|
392
483
|
if err := lintKnownRoles(root, phase, roles); err != nil {
|
|
393
484
|
return fail("BLOCKED: %v", err)
|
|
394
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
|
+
}
|
|
493
|
+
if isBuildWrightWave(w) {
|
|
494
|
+
binding, notices, err := autocompleteWriterAdmission(root, slug, w, f)
|
|
495
|
+
if err != nil {
|
|
496
|
+
return fail("BLOCKED: %v", err)
|
|
497
|
+
}
|
|
498
|
+
w.ControllerBatch = binding
|
|
499
|
+
for _, notice := range notices {
|
|
500
|
+
fmt.Fprintln(stdout, notice)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
395
503
|
rounds, code := admitWrightRounds(root, slug, f, w, review, stderr)
|
|
396
504
|
if code != 0 {
|
|
397
505
|
return code
|
|
@@ -428,10 +536,16 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
|
|
|
428
536
|
}
|
|
429
537
|
admissionPaths := slices.Clone(claimPaths)
|
|
430
538
|
if isWrightWave(w) {
|
|
431
|
-
|
|
539
|
+
if len(w.AdmittedPaths) > 0 {
|
|
540
|
+
for _, id := range w.Slices {
|
|
541
|
+
admissionPaths = append(admissionPaths, w.AdmittedPaths[id]...)
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
admissionPaths = append(admissionPaths, sliceWriteSet(root, slug, w.Slices)...)
|
|
545
|
+
}
|
|
432
546
|
}
|
|
433
547
|
if len(claimPaths) == 0 {
|
|
434
|
-
if err :=
|
|
548
|
+
if err := checkWriterClaimsLocked(root, claim.session, admissionPaths); err != nil {
|
|
435
549
|
return fail("BLOCKED: %v", err)
|
|
436
550
|
}
|
|
437
551
|
}
|
|
@@ -566,6 +680,7 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
|
|
|
566
680
|
if !releaseWaveClaim(root, wave, w, stdout, stderr) {
|
|
567
681
|
return 3
|
|
568
682
|
}
|
|
683
|
+
*recordedReturn = true
|
|
569
684
|
return 0
|
|
570
685
|
}
|
|
571
686
|
if r.Returned {
|
|
@@ -604,6 +719,7 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
|
|
|
604
719
|
}
|
|
605
720
|
return fail("%v", err)
|
|
606
721
|
}
|
|
722
|
+
*recordedReturn = true
|
|
607
723
|
recordMetricNote(root, slug, w.Phase, "return", role, note)
|
|
608
724
|
fmt.Fprintf(stdout, "dispatch: %s returned (wave %s %s)\n", role, wave, w.State)
|
|
609
725
|
if all {
|
|
@@ -714,28 +830,6 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
|
|
|
714
830
|
return 2
|
|
715
831
|
}
|
|
716
832
|
|
|
717
|
-
// Caller holds the repository claims lock through new-wave publication.
|
|
718
|
-
func checkAbandonedWriterClaims(root string, paths []string) error {
|
|
719
|
-
if len(paths) == 0 {
|
|
720
|
-
return nil
|
|
721
|
-
}
|
|
722
|
-
latest, _, bad, err := readClaims(claimsPath(root))
|
|
723
|
-
if err != nil {
|
|
724
|
-
return err
|
|
725
|
-
}
|
|
726
|
-
if bad > 0 {
|
|
727
|
-
return fmt.Errorf("%d unparsed ledger line(s); cannot rule out an abandoned writer claim", bad)
|
|
728
|
-
}
|
|
729
|
-
conflicts, err := retainedWriterConflicts(root, latest, paths, newHolderProbe())
|
|
730
|
-
if err != nil {
|
|
731
|
-
return err
|
|
732
|
-
}
|
|
733
|
-
if len(conflicts) > 0 {
|
|
734
|
-
return fmt.Errorf("abandoned writer still holds claim %s on %s; confirm host termination and use dispatch abandon --release before retrying", conflicts[0].ID, strings.Join(conflicts[0].Paths, ","))
|
|
735
|
-
}
|
|
736
|
-
return nil
|
|
737
|
-
}
|
|
738
|
-
|
|
739
833
|
// releaseWaveClaim releases w's claim or reconciles a verified prior release.
|
|
740
834
|
// Callers surface failure without undoing the recorded return or abandonment.
|
|
741
835
|
func releaseWaveClaim(root, wave string, w *dispatchWave, stdout, stderr io.Writer) bool {
|
|
@@ -1039,6 +1133,13 @@ func wrightLane(role string) (string, bool) {
|
|
|
1039
1133
|
// errors are never refused: the check widens the batch, it never stops a
|
|
1040
1134
|
// wave that already starts every remaining pick.
|
|
1041
1135
|
func widerBatchRefusal(root, slug string, wave []string) string {
|
|
1136
|
+
mode, modeErr := autocompleteMode(root)
|
|
1137
|
+
if modeErr != nil {
|
|
1138
|
+
return fmt.Sprintf("dispatch: BLOCKED: %v", modeErr)
|
|
1139
|
+
}
|
|
1140
|
+
if mode {
|
|
1141
|
+
return ""
|
|
1142
|
+
}
|
|
1042
1143
|
capN := min(afkMaxParallel(root), parallel.MaxParallelSlices)
|
|
1043
1144
|
if capN < 2 {
|
|
1044
1145
|
return ""
|