devrites 7.2.6 → 7.2.8
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 +14 -0
- package/README.md +6 -1
- package/SECURITY.md +11 -7
- package/docs/command-map.md +4 -0
- package/docs/engine/commands.md +25 -3
- package/docs/orchestration.md +9 -0
- package/engine/internal/acceptance/cli.go +9 -5
- package/engine/internal/acceptance/run.go +97 -38
- package/engine/internal/devritespaths/paths.go +7 -2
- package/engine/internal/install/apply.go +5 -0
- package/engine/internal/install/merge.go +48 -6
- package/engine/internal/install/preflight.go +21 -3
- package/engine/internal/install/uninstall.go +9 -10
- package/engine/internal/lib/apply_bundle.go +7 -1
- package/engine/internal/lib/closeout.go +7 -7
- package/engine/internal/lib/context.go +10 -6
- package/engine/internal/lib/dispatch.go +81 -21
- package/engine/internal/lib/resolve.go +10 -5
- package/engine/internal/lib/state_edit.go +9 -2
- package/engine/internal/lib/taskgraph.go +94 -21
- package/engine/internal/lib/triggers.go +2 -2
- package/engine/internal/state/cursor.go +5 -5
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/.claude/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/.claude/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/.claude/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/.claude/skills/rite-spec/SKILL.md +2 -2
- package/pack/.claude/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/generated/claude/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/claude/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/claude/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/claude/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/claude/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/generated/codex/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/codex/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/codex/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/codex/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/codex/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/devin/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/generated/devin/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/devin/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/devin/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/devin/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/devin/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/devin/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/dsh/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/generated/dsh/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/dsh/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/dsh/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/dsh/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/dsh/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/dsh/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/dsh/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +13 -8
- package/pack/generated/omp/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/omp/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/omp/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/omp/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/omp/skills/rite-temper/SKILL.md +4 -5
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +3 -3
- package/pack/generated/pi/skills/devrites-lib/reference/standards/agents.md +9 -8
- package/pack/generated/pi/skills/devrites-lib/reference/standards/principles.md +5 -6
- package/pack/generated/pi/skills/rite-autocomplete/reference/decision-policy.md +3 -1
- package/pack/generated/pi/skills/rite-autocomplete/reference/lookahead.md +18 -35
- package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +7 -8
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +26 -16
- package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +5 -5
- package/pack/generated/pi/skills/rite-spec/SKILL.md +2 -2
- package/pack/generated/pi/skills/rite-temper/SKILL.md +4 -5
- package/package.json +3 -3
- package/scripts/build-host-artifacts.sh +5 -6
- package/scripts/omp-generate.sh +24 -14
|
@@ -35,18 +35,12 @@ func (r *runner) uninstall() error {
|
|
|
35
35
|
return err
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Per-path would-be content for a dry run; a nil value means the file
|
|
39
|
-
// would be removed.
|
|
40
|
-
dryTree := map[string][]byte{}
|
|
41
38
|
for _, rel := range entries {
|
|
42
39
|
merge, ok := hostpack.ManagedMergeForMarker(rel)
|
|
43
40
|
if !ok {
|
|
44
41
|
continue
|
|
45
42
|
}
|
|
46
43
|
if r.opts.DryRun {
|
|
47
|
-
if err := dryRunStripMarker(dryTree, r.target, merge); err != nil {
|
|
48
|
-
return err
|
|
49
|
-
}
|
|
50
44
|
fmt.Fprintf(r.opts.Stdout, " [merge-remove] %s\n", merge.DryRun)
|
|
51
45
|
continue
|
|
52
46
|
}
|
|
@@ -142,11 +136,16 @@ func (r *runner) uninstall() error {
|
|
|
142
136
|
// dryRunStripMarker runs the real marker strip against an in-memory view of
|
|
143
137
|
// the target (tree), so a dry run follows the same sequence as the real
|
|
144
138
|
// uninstall and reports the same errors without writing anything.
|
|
145
|
-
func dryRunStripMarker(tree map[string][]byte,
|
|
146
|
-
|
|
147
|
-
|
|
139
|
+
func (r *runner) dryRunStripMarker(tree map[string][]byte, merge hostpack.ManagedMerge) error {
|
|
140
|
+
path := filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))
|
|
141
|
+
write := func(data []byte) error { tree[path] = data; return nil }
|
|
142
|
+
remove := func() error { tree[path] = nil; return nil }
|
|
143
|
+
if merge.MarkerRel == hostpack.LegacyCodexHooksMerge.MarkerRel {
|
|
144
|
+
return stripHooksWith(path, write, remove)
|
|
145
|
+
}
|
|
146
|
+
if merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel {
|
|
147
|
+
return r.stripClaudeSettingsWith(path, write, remove)
|
|
148
148
|
}
|
|
149
|
-
path := filepath.Join(target, filepath.FromSlash(merge.TargetRel))
|
|
150
149
|
data, seen := tree[path]
|
|
151
150
|
if !seen {
|
|
152
151
|
var err error
|
|
@@ -210,7 +210,12 @@ func collectBundleTargets(slug string, bundles []candidateBundle) ([]*bundleTarg
|
|
|
210
210
|
}
|
|
211
211
|
pins := map[string]string{}
|
|
212
212
|
for path, hash := range bundle.BaseHashes {
|
|
213
|
-
|
|
213
|
+
rel := filepath.ToSlash(filepath.Clean(strings.TrimPrefix(filepath.ToSlash(path), prefix)))
|
|
214
|
+
pin := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(hash), "sha256:"))
|
|
215
|
+
if previous, ok := pins[rel]; ok && previous != pin {
|
|
216
|
+
problems = append(problems, fmt.Sprintf("bundle %d %s: conflicting base hashes for the same file", bi+1, path))
|
|
217
|
+
}
|
|
218
|
+
pins[rel] = pin
|
|
214
219
|
}
|
|
215
220
|
for _, file := range bundle.CandidateFiles {
|
|
216
221
|
rel := strings.TrimPrefix(filepath.ToSlash(file.Path), prefix)
|
|
@@ -219,6 +224,7 @@ func collectBundleTargets(slug string, bundles []candidateBundle) ([]*bundleTarg
|
|
|
219
224
|
problems = append(problems, where+": path must be a Markdown file inside the "+slug+" workspace")
|
|
220
225
|
continue
|
|
221
226
|
}
|
|
227
|
+
rel = filepath.ToSlash(filepath.Clean(rel))
|
|
222
228
|
op := strings.ToLower(strings.TrimSpace(file.Operation))
|
|
223
229
|
switch op {
|
|
224
230
|
case "create", "replace":
|
|
@@ -62,11 +62,6 @@ func CloseOut(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
62
62
|
archDisplay := filepath.ToSlash(filepath.Join("archive", slug))
|
|
63
63
|
active := filepath.Join(dv, "ACTIVE")
|
|
64
64
|
|
|
65
|
-
activeSlug, err := devritespaths.ActiveSlug(dv)
|
|
66
|
-
if err != nil {
|
|
67
|
-
fmt.Fprintf(stderr, "state close: invalid ACTIVE cursor: %v\n", err)
|
|
68
|
-
return 1
|
|
69
|
-
}
|
|
70
65
|
// A broken symlink at the destination still counts as "already there", so use
|
|
71
66
|
// Lstat rather than Stat to avoid silently overwriting it.
|
|
72
67
|
if _, err := os.Lstat(arch); err == nil {
|
|
@@ -84,7 +79,7 @@ func CloseOut(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
84
79
|
// open file, so there the lock is released just before the rename.
|
|
85
80
|
code := 1
|
|
86
81
|
if err := state.WithFeatureLockRelease(dv, slug, func(release func()) error {
|
|
87
|
-
code = closeOutLocked(release, work, arch, archiveDir, archDisplay, active,
|
|
82
|
+
code = closeOutLocked(release, work, arch, archiveDir, archDisplay, active, slug, stdout, stderr)
|
|
88
83
|
return nil
|
|
89
84
|
}); err != nil {
|
|
90
85
|
fmt.Fprintf(stderr, "state close: cannot lock feature %s: %v\n", slug, err)
|
|
@@ -96,7 +91,12 @@ func CloseOut(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
96
91
|
// closeOutLocked performs the rename and the conditional ACTIVE clear. It runs
|
|
97
92
|
// under the feature lock held by CloseOut and reports an exit code rather than
|
|
98
93
|
// an error, preserving CloseOut's documented exit codes.
|
|
99
|
-
func closeOutLocked(release func(), work, arch, archiveDir, archDisplay, active,
|
|
94
|
+
func closeOutLocked(release func(), work, arch, archiveDir, archDisplay, active, slug string, stdout, stderr io.Writer) int {
|
|
95
|
+
activeSlug, err := devritespaths.PhysicalActiveSlug(filepath.Dir(active))
|
|
96
|
+
if err != nil {
|
|
97
|
+
fmt.Fprintf(stderr, "state close: invalid ACTIVE cursor: %v\n", err)
|
|
98
|
+
return 1
|
|
99
|
+
}
|
|
100
100
|
// Fail closed: if the move does not happen, leave the cursor alone and do not
|
|
101
101
|
// claim success: a lost audit trail must never look like a clean close-out.
|
|
102
102
|
if err := os.MkdirAll(archiveDir, 0o755); err != nil {
|
|
@@ -57,7 +57,9 @@ every ID the block cites).
|
|
|
57
57
|
Repeat --slice for a recheck that spans several slices. In phase build a
|
|
58
58
|
--role bundle other than slice-wright requires --slice (exit 3). A root
|
|
59
59
|
build or autocomplete bundle (no --role, no --slice) defaults to --slice
|
|
60
|
-
<active_slice> when state.md names one present in tasks.md
|
|
60
|
+
<active_slice> when state.md names one present in tasks.md, unless
|
|
61
|
+
.devrites/AFK max_parallel is 2 or more or a parallel lease is running
|
|
62
|
+
(the bundle stays whole so the other slices stay visible); --whole opts out.
|
|
61
63
|
--recheck <inventory> (phase build, with --role and --slice) builds a
|
|
62
64
|
bounded recheck bundle: the inventory file (relative to the workspace)
|
|
63
65
|
and core.md are inlined; every other pack file becomes a by-reference
|
|
@@ -78,7 +80,7 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
78
80
|
}
|
|
79
81
|
// A Build reviewer judges one slice; an unscoped bundle inlines whole
|
|
80
82
|
// plan/tasks/architecture into every dispatch.
|
|
81
|
-
if opts.phase == "build" && opts.role != "" && opts.role != "slice-wright" && opts.slice == "" {
|
|
83
|
+
if opts.phase == "build" && opts.role != "" && roleBase(opts.role) != "slice-wright" && opts.slice == "" {
|
|
82
84
|
fmt.Fprintf(stderr, "context: BLOCKED: unscoped build reviewer bundle: --role %s in phase build needs --slice <SLICE-ID>\n", opts.role)
|
|
83
85
|
return 3
|
|
84
86
|
}
|
|
@@ -91,10 +93,11 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
91
93
|
return 2
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
+
// One active slice per bundle, except a parallel batch: the root has to
|
|
97
|
+
// see the other slices to start them. Whole tasks/plan/decisions ride
|
|
98
|
+
// that batch's turns; serial runs stay scoped.
|
|
96
99
|
autoScope := false
|
|
97
|
-
if featureDir != "" && opts.role == "" && opts.slice == "" && !opts.whole && (opts.phase == "build" || opts.phase == "autocomplete") {
|
|
100
|
+
if featureDir != "" && opts.role == "" && opts.slice == "" && !opts.whole && (opts.phase == "build" || opts.phase == "autocomplete") && !parallelPlanVisible(root, opts.slug) {
|
|
98
101
|
if id := activeSliceScope(featureDir); id != "" {
|
|
99
102
|
opts.slices, opts.slice, autoScope = []string{id}, id, true
|
|
100
103
|
}
|
|
@@ -452,7 +455,7 @@ func workspaceReadSet(manifest loadsManifest, role string) ([]string, bool) {
|
|
|
452
455
|
if role == "" {
|
|
453
456
|
return manifest.Workspace, false
|
|
454
457
|
}
|
|
455
|
-
if list, ok := manifest.WorkspaceByRole[role]; ok {
|
|
458
|
+
if list, ok := manifest.WorkspaceByRole[roleBase(role)]; ok {
|
|
456
459
|
return list, true
|
|
457
460
|
}
|
|
458
461
|
return manifest.Workspace, false
|
|
@@ -912,6 +915,7 @@ func parseLoadsManifest(text string) (loadsManifest, bool, error) {
|
|
|
912
915
|
}
|
|
913
916
|
|
|
914
917
|
func roleAgentPath(manifest loadsManifest, role string) string {
|
|
918
|
+
role = roleBase(role)
|
|
915
919
|
if manifest.Agents != nil {
|
|
916
920
|
if rel, ok := manifest.Agents[role]; ok {
|
|
917
921
|
return rel
|
|
@@ -50,6 +50,7 @@ For a reviewer role, return --result <file> stores the return verbatim as
|
|
|
50
50
|
packets/<wave>-<role>.return.md (never overwritten), derives the outcome
|
|
51
51
|
from its Outcome/Counts head (malformed records gap), refuses an --outcome
|
|
52
52
|
that disagrees, and records its Finding: keys for metrics summary.
|
|
53
|
+
Reviewer returns require --result or --outcome; omitting both leaves the role pending.
|
|
53
54
|
|
|
54
55
|
A build wave with a wright role must name its slice(s) with --slice; a vet
|
|
55
56
|
wave may (slice-bound recheck), and so may a reviewer-only wave (uncapped),
|
|
@@ -57,9 +58,11 @@ which keys its --result findings on that slice. A --result file may already
|
|
|
57
58
|
be the packets/ path; it is then kept as the stored return. Every 5 rounds of one kind on one slice, the
|
|
58
59
|
next open is blocked until --reslice-review names a drift.md
|
|
59
60
|
"## <DRIFT-ID>" heading for that slice. Under .devrites/AFK max_parallel >= 2,
|
|
60
|
-
a
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
a build wright wave that leaves parallel select --from-plan picks unstarted is
|
|
62
|
+
blocked until evidence.md records "serial: <skipped ids> — <reason>". A repair
|
|
63
|
+
round or a running lease slice does not waive those picks. A wave naming two
|
|
64
|
+
or more slices needs one slice-wright.<SLICE-ID> role per slice; one shared
|
|
65
|
+
role has one handle, so the other slices never start. HITL is never width-blocked.
|
|
63
66
|
|
|
64
67
|
Root-edit tripwire (ADR-0038): the first open records tree-baseline.json
|
|
65
68
|
(dirty product paths outside .devrites/ and .scratch/, accepted as user work);
|
|
@@ -453,6 +456,9 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
|
|
|
453
456
|
if r.Returned {
|
|
454
457
|
return fail("role %q already returned", role)
|
|
455
458
|
}
|
|
459
|
+
if isReviewerRole(role) && result == "" && outcome == "" {
|
|
460
|
+
return fail("reviewer %q needs --result <file> or --outcome <findings|no-findings|gap>", role)
|
|
461
|
+
}
|
|
456
462
|
if outcome != "" && !slices.Contains([]string{"findings", "no-findings", "gap"}, outcome) {
|
|
457
463
|
return fail("--outcome %q is not findings, no-findings, or gap", outcome)
|
|
458
464
|
}
|
|
@@ -742,7 +748,6 @@ func admitWrightRounds(root, slug string, f *dispatchFile, w *dispatchWave, revi
|
|
|
742
748
|
}
|
|
743
749
|
}
|
|
744
750
|
var lines, overCap []string
|
|
745
|
-
firstRound := true
|
|
746
751
|
for _, id := range w.Slices {
|
|
747
752
|
prior, reviews := 0, 0
|
|
748
753
|
var unreturned []string
|
|
@@ -762,7 +767,6 @@ func admitWrightRounds(root, slug string, f *dispatchFile, w *dispatchWave, revi
|
|
|
762
767
|
}
|
|
763
768
|
}
|
|
764
769
|
round := prior + 1
|
|
765
|
-
firstRound = firstRound && prior == 0
|
|
766
770
|
if round > wrightRoundsPerReview*(1+reviews) {
|
|
767
771
|
overCap = append(overCap, id)
|
|
768
772
|
}
|
|
@@ -780,7 +784,11 @@ func admitWrightRounds(root, slug string, f *dispatchFile, w *dispatchWave, revi
|
|
|
780
784
|
fmt.Fprintf(stderr, "dispatch: --reslice-review given but no slice has reached its %d-round cap\n", wrightRoundsPerReview)
|
|
781
785
|
return nil, 2
|
|
782
786
|
}
|
|
783
|
-
if kind == "wright"
|
|
787
|
+
if kind == "wright" {
|
|
788
|
+
if refusal := wrightFanoutRefusal(w); refusal != "" {
|
|
789
|
+
fmt.Fprintln(stderr, refusal)
|
|
790
|
+
return nil, 3
|
|
791
|
+
}
|
|
784
792
|
if refusal := widerBatchRefusal(root, slug, w.Slices); refusal != "" {
|
|
785
793
|
fmt.Fprintln(stderr, refusal)
|
|
786
794
|
return nil, 3
|
|
@@ -816,36 +824,88 @@ func admitWrightRounds(root, slug string, f *dispatchFile, w *dispatchWave, revi
|
|
|
816
824
|
}
|
|
817
825
|
|
|
818
826
|
// serialRecordRe matches an evidence.md `serial: <ids> — <reason>` record
|
|
819
|
-
// (the reason may open with a code span, never be empty)
|
|
820
|
-
|
|
827
|
+
// (the reason may open with a code span, never be empty). Group 1 holds the
|
|
828
|
+
// ids; group 2 holds the reason.
|
|
829
|
+
var serialRecordRe = regexp.MustCompile("(?mi)^[ \\t]*(?:[-*][ \\t]+)?`?serial:([^\\n]*?)(?:[ \\t]*[—–]|[ \\t]+--?[ \\t])[ \\t]*([^\\s`][^\\n]*|`[^\\s`][^\\n]*)")
|
|
830
|
+
|
|
831
|
+
// A repair, an open wave, or "already started" is not a shared surface.
|
|
832
|
+
// Those reasons do not waive a pick unless they also name a path.
|
|
833
|
+
var transientSerialReasonRe = regexp.MustCompile(`(?i)\b(?:repairs?|repairing|repaired|already started|in flight|open wave|sealed wave|running lease|in the lease)\b`)
|
|
834
|
+
var serialSurfaceRe = regexp.MustCompile("`[^`\\n]*\\.[^`\\n]+`|(?i)\\b[\\w./-]+\\.[a-z0-9]+\\b")
|
|
835
|
+
|
|
836
|
+
func serialReasonWaives(reason string) bool {
|
|
837
|
+
if transientSerialReasonRe.MatchString(reason) && !serialSurfaceRe.MatchString(reason) {
|
|
838
|
+
return false
|
|
839
|
+
}
|
|
840
|
+
return true
|
|
841
|
+
}
|
|
821
842
|
|
|
822
843
|
var sliceTokenRe = regexp.MustCompile(`(?i)\bSLICE-\d+\b`)
|
|
823
844
|
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
845
|
+
// wrightFanoutRefusal refuses a build wright wave that names several slices
|
|
846
|
+
// but not one slice-wright role per slice. One role has one host handle, so
|
|
847
|
+
// the other slices would never start.
|
|
848
|
+
func wrightFanoutRefusal(w *dispatchWave) string {
|
|
849
|
+
if len(w.Slices) < 2 {
|
|
850
|
+
return ""
|
|
851
|
+
}
|
|
852
|
+
lanes := map[string]bool{}
|
|
853
|
+
bare := 0
|
|
854
|
+
for role := range w.Roles {
|
|
855
|
+
if roleBase(role) != "slice-wright" {
|
|
856
|
+
continue
|
|
857
|
+
}
|
|
858
|
+
lane, ok := wrightLane(role)
|
|
859
|
+
if !ok {
|
|
860
|
+
bare++
|
|
861
|
+
continue
|
|
862
|
+
}
|
|
863
|
+
lanes[sliceNumber(lane)] = true
|
|
864
|
+
}
|
|
865
|
+
if bare > 0 || len(lanes) != len(w.Slices) {
|
|
866
|
+
return fmt.Sprintf("dispatch: BLOCKED: %d slices need one `--role slice-wright.<SLICE-ID>` each so every writer gets a handle; one shared slice-wright role starts only one agent", len(w.Slices))
|
|
867
|
+
}
|
|
868
|
+
for _, id := range w.Slices {
|
|
869
|
+
if !lanes[sliceNumber(id)] {
|
|
870
|
+
return fmt.Sprintf("dispatch: BLOCKED: %s has no `--role slice-wright.%s`", id, id)
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return ""
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// wrightLane is the SLICE-### suffix of a slice-wright.<SLICE-ID> role.
|
|
877
|
+
func wrightLane(role string) (string, bool) {
|
|
878
|
+
base, lane, ok := strings.Cut(strings.TrimPrefix(role, "devrites-"), ".")
|
|
879
|
+
if !ok || base != "slice-wright" || !state.ValidSliceID(lane) {
|
|
880
|
+
return "", false
|
|
881
|
+
}
|
|
882
|
+
return lane, true
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// widerBatchRefusal refuses an AFK build wright wave when
|
|
886
|
+
// `parallel select --from-plan` still picks slices this wave does not start
|
|
887
|
+
// and evidence.md has no `serial:` record for each. A repair round or a
|
|
888
|
+
// running lease slice does not waive those picks. HITL runs and selection
|
|
889
|
+
// errors are never refused: the check widens the batch, it never stops a
|
|
890
|
+
// wave that already starts every remaining pick.
|
|
829
891
|
func widerBatchRefusal(root, slug string, wave []string) string {
|
|
830
892
|
capN := min(afkMaxParallel(root), parallel.MaxParallelSlices)
|
|
831
893
|
if capN < 2 {
|
|
832
894
|
return ""
|
|
833
895
|
}
|
|
834
|
-
|
|
835
|
-
if err != nil || len(batch)
|
|
896
|
+
_, batch, err := planBatch(root, slug, capN)
|
|
897
|
+
if err != nil || len(batch) == 0 {
|
|
836
898
|
return ""
|
|
837
899
|
}
|
|
838
|
-
for _, id := range wave {
|
|
839
|
-
if !containsSlice(eligible, id) {
|
|
840
|
-
return ""
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
900
|
var recorded []string
|
|
844
901
|
if raw, err := readWorkspaceArtifact(root, slug, "evidence.md"); err == nil {
|
|
845
902
|
if visible, err := markdowntext.Structural(raw); err == nil {
|
|
846
903
|
raw = visible
|
|
847
904
|
}
|
|
848
905
|
for _, m := range serialRecordRe.FindAllStringSubmatch(string(raw), -1) {
|
|
906
|
+
if len(m) < 3 || !serialReasonWaives(m[2]) {
|
|
907
|
+
continue
|
|
908
|
+
}
|
|
849
909
|
recorded = append(recorded, sliceTokenRe.FindAllString(m[1], -1)...)
|
|
850
910
|
}
|
|
851
911
|
}
|
|
@@ -858,7 +918,7 @@ func widerBatchRefusal(root, slug string, wave []string) string {
|
|
|
858
918
|
if len(missing) == 0 {
|
|
859
919
|
return ""
|
|
860
920
|
}
|
|
861
|
-
return fmt.Sprintf("dispatch: BLOCKED: AFK max_parallel %d: `%s` picks %s, wider than this wave (%s);
|
|
921
|
+
return fmt.Sprintf("dispatch: BLOCKED: AFK max_parallel %d: `%s` picks %s, wider than this wave (%s); start those picks (`parallel create`, or `parallel add` while a lease runs) and dispatch them, or record `serial: %s — <shared surface or reason>` in evidence.md",
|
|
862
922
|
capN, planSelectCommand(filepath.Dir(root), slug, capN), strings.Join(batch, ","), strings.Join(wave, ","), strings.Join(missing, ","))
|
|
863
923
|
}
|
|
864
924
|
|
|
@@ -361,6 +361,10 @@ func clearAwaiting(sfile, qid string) error {
|
|
|
361
361
|
return fmt.Errorf("read state %s: %w", sfile, err)
|
|
362
362
|
}
|
|
363
363
|
lines := splitLinesNoTrailing(data)
|
|
364
|
+
visible, err := structuralLines(data)
|
|
365
|
+
if err != nil {
|
|
366
|
+
return fmt.Errorf("read state %s: %w", sfile, err)
|
|
367
|
+
}
|
|
364
368
|
resumePolicy, _ := state.PolicyFor(state.PhaseBuild)
|
|
365
369
|
if rawPhase, ok := state.CursorField(lines, state.CursorPhase); ok {
|
|
366
370
|
if fields := strings.Fields(strings.ToLower(rawPhase)); len(fields) > 0 {
|
|
@@ -373,7 +377,7 @@ func clearAwaiting(sfile, qid string) error {
|
|
|
373
377
|
// First check whether the awaiting block references this question at all.
|
|
374
378
|
inAw := false
|
|
375
379
|
var awaitingLines []string
|
|
376
|
-
for _, line := range
|
|
380
|
+
for _, line := range visible {
|
|
377
381
|
switch {
|
|
378
382
|
case awaitingRe.MatchString(line):
|
|
379
383
|
inAw = true
|
|
@@ -400,7 +404,8 @@ func clearAwaiting(sfile, qid string) error {
|
|
|
400
404
|
ts := nowUTC()
|
|
401
405
|
var out []string
|
|
402
406
|
inAw, inLog, logAppended := false, false, false
|
|
403
|
-
for
|
|
407
|
+
for i, original := range lines {
|
|
408
|
+
line := visible[i]
|
|
404
409
|
if awaitingRe.MatchString(line) {
|
|
405
410
|
inAw = true
|
|
406
411
|
continue
|
|
@@ -413,7 +418,7 @@ func clearAwaiting(sfile, qid string) error {
|
|
|
413
418
|
}
|
|
414
419
|
switch {
|
|
415
420
|
case logHdrRe.MatchString(line):
|
|
416
|
-
out = append(out,
|
|
421
|
+
out = append(out, original)
|
|
417
422
|
inLog = true
|
|
418
423
|
continue
|
|
419
424
|
case inLog && hdrSpaceRe.MatchString(line):
|
|
@@ -422,10 +427,10 @@ func clearAwaiting(sfile, qid string) error {
|
|
|
422
427
|
logAppended = true
|
|
423
428
|
}
|
|
424
429
|
inLog = false
|
|
425
|
-
out = append(out,
|
|
430
|
+
out = append(out, original)
|
|
426
431
|
continue
|
|
427
432
|
}
|
|
428
|
-
out = append(out,
|
|
433
|
+
out = append(out, original)
|
|
429
434
|
}
|
|
430
435
|
if inLog && !logAppended {
|
|
431
436
|
out = append(out, fmt.Sprintf("- %s %s: resolved %s", ts, resumePolicy.Target, qid))
|
|
@@ -58,10 +58,17 @@ func RunStateSet(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
58
58
|
return fail(stderr, stateSetUsage, 2)
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
if !cursorKeyPattern.MatchString(key)
|
|
61
|
+
if !cursorKeyPattern.MatchString(key) {
|
|
62
62
|
return fail(stderr, fmt.Sprintf("state set: key %q is not a snake_case cursor key or repeats", key), 2)
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
if canonical, ok := state.CanonicalCursorKey(key); ok {
|
|
65
|
+
key = canonical
|
|
66
|
+
}
|
|
67
|
+
identity := strings.ReplaceAll(key, "_", "")
|
|
68
|
+
if seen[identity] {
|
|
69
|
+
return fail(stderr, fmt.Sprintf("state set: key %q is not a snake_case cursor key or repeats", key), 2)
|
|
70
|
+
}
|
|
71
|
+
seen[identity] = true
|
|
65
72
|
changes = append(changes, change{key, value})
|
|
66
73
|
}
|
|
67
74
|
sfile, code := stateEditWorkspace(root, slug, "state.md", stderr)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package lib
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"errors"
|
|
4
5
|
"fmt"
|
|
5
6
|
"os"
|
|
6
7
|
"path"
|
|
@@ -195,10 +196,10 @@ func PrecheckSkips(root, slug, slice string, gateIDs []string) (map[string]strin
|
|
|
195
196
|
return skips, nil
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
// planReady lists, in plan order, the tasks.md slices
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
199
|
+
// planReady lists, in plan order, the tasks.md slices that are not built whose
|
|
200
|
+
// dependencies are all built, each with its sliceWritePaths plus its dependency
|
|
201
|
+
// manifest and lockfile when it declares external dependencies. root is the
|
|
202
|
+
// .devrites directory.
|
|
202
203
|
func planReady(root, slug string) ([]parallel.SlicePaths, error) {
|
|
203
204
|
raw, err := readWorkspaceArtifact(root, slug, "tasks.md")
|
|
204
205
|
if err != nil {
|
|
@@ -233,8 +234,11 @@ func planReady(root, slug string) ([]parallel.SlicePaths, error) {
|
|
|
233
234
|
// builtWordRe marks a checked state.md item as a built checkpoint.
|
|
234
235
|
var builtWordRe = regexp.MustCompile(`(?i)\bbuilt\b`)
|
|
235
236
|
|
|
236
|
-
// builtSlices returns the slice numbers
|
|
237
|
-
// built — <sha>`, or `- [x] Slice 4:
|
|
237
|
+
// builtSlices returns the slice numbers that are built, outside fenced examples.
|
|
238
|
+
// A state.md checkbox (`- [x] SLICE-004 built — <sha>`, or `- [x] Slice 4:
|
|
239
|
+
// <name> — built`) counts. So does an evidence.md heading that says that slice
|
|
240
|
+
// was checkpointed and whose section has a `Checkpoint: <sha>` line — a missed
|
|
241
|
+
// checkbox must not keep dependents out of the ready set.
|
|
238
242
|
func builtSlices(root, slug string) (map[string]bool, error) {
|
|
239
243
|
raw, err := readWorkspaceArtifact(root, slug, "state.md")
|
|
240
244
|
if err != nil {
|
|
@@ -253,35 +257,86 @@ func builtSlices(root, slug string) (map[string]bool, error) {
|
|
|
253
257
|
built[strings.TrimLeft(m[1], "0")] = true
|
|
254
258
|
}
|
|
255
259
|
}
|
|
260
|
+
extra, err := evidenceBuilt(root, slug)
|
|
261
|
+
if err != nil {
|
|
262
|
+
return nil, err
|
|
263
|
+
}
|
|
264
|
+
for id := range extra {
|
|
265
|
+
built[id] = true
|
|
266
|
+
}
|
|
256
267
|
return built, nil
|
|
257
268
|
}
|
|
258
269
|
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
var (
|
|
271
|
+
checkpointedHeadingRe = regexp.MustCompile(`(?i)\bcheckpointed\b`)
|
|
272
|
+
notCheckpointedRe = regexp.MustCompile(`(?i)\bnot checkpointed\b|\bcheckpoint (?:refused|reverted|failed)\b`)
|
|
273
|
+
checkpointSHARe = regexp.MustCompile(`(?m)^[ \t]*Checkpoint: [0-9a-fA-F]{7,40}\b`)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
// evidenceBuilt reads evidence.md checkpoint sections. A missing file is none.
|
|
277
|
+
func evidenceBuilt(root, slug string) (map[string]bool, error) {
|
|
278
|
+
raw, err := readWorkspaceArtifact(root, slug, "evidence.md")
|
|
279
|
+
if err != nil {
|
|
280
|
+
if errors.Is(err, os.ErrNotExist) {
|
|
281
|
+
return map[string]bool{}, nil
|
|
282
|
+
}
|
|
283
|
+
return nil, err
|
|
284
|
+
}
|
|
285
|
+
masked, err := markdowntext.Structural(raw)
|
|
286
|
+
if err != nil {
|
|
287
|
+
return nil, fmt.Errorf("evidence.md: %w", err)
|
|
288
|
+
}
|
|
289
|
+
text := string(masked)
|
|
290
|
+
if strings.HasPrefix(text, "## ") {
|
|
291
|
+
text = "\n" + text
|
|
292
|
+
}
|
|
293
|
+
built := map[string]bool{}
|
|
294
|
+
for part := range strings.SplitSeq(text, "\n## ") {
|
|
295
|
+
heading, body, _ := strings.Cut(part, "\n")
|
|
296
|
+
var ids []string
|
|
297
|
+
for _, id := range sliceTokenRe.FindAllString(heading, -1) {
|
|
298
|
+
ids = append(ids, sliceNumber(id))
|
|
299
|
+
}
|
|
300
|
+
if len(ids) == 0 {
|
|
301
|
+
continue
|
|
302
|
+
}
|
|
303
|
+
if notCheckpointedRe.MatchString(heading) {
|
|
304
|
+
for _, id := range ids {
|
|
305
|
+
delete(built, id)
|
|
306
|
+
}
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
309
|
+
if checkpointedHeadingRe.MatchString(heading) && checkpointSHARe.MatchString(body) {
|
|
310
|
+
for _, id := range ids {
|
|
311
|
+
built[id] = true
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return built, nil
|
|
316
|
+
}
|
|
261
317
|
|
|
262
318
|
// A backtick name, module path, npm scope, or x.y.z pin. go.mod/go.sum is not a module path.
|
|
319
|
+
// A toolchain version (Go 1.22.0) is not a package add.
|
|
263
320
|
var (
|
|
264
|
-
backtickPkgRe
|
|
265
|
-
modulePathRe
|
|
266
|
-
npmScopeRe
|
|
267
|
-
versionPinRe
|
|
268
|
-
|
|
321
|
+
backtickPkgRe = regexp.MustCompile("`([^`]+)`")
|
|
322
|
+
modulePathRe = regexp.MustCompile(`(?i)(?:[a-z0-9-]+\.)+[a-z]{2,}/[a-z0-9._@~+-]+`)
|
|
323
|
+
npmScopeRe = regexp.MustCompile(`(?i)@[a-z0-9._-]+/[a-z0-9._-]+`)
|
|
324
|
+
versionPinRe = regexp.MustCompile(`(?i)\bv?\d+\.\d+\.\d+\b`)
|
|
325
|
+
toolchainVersionRe = regexp.MustCompile(`(?i)\b(?:go|golang|node|bun|npm|pnpm|yarn|rustc|python|ruby|java)\s+v?\d+\.\d+\.\d+\b`)
|
|
326
|
+
manifestHostRe = regexp.MustCompile(`(?i)\.(?:mod|sum|json|toml|yaml|yml|lock|lockb)$`)
|
|
269
327
|
)
|
|
270
328
|
|
|
271
329
|
// externalClaimsManifest reports whether External dependencies names a package
|
|
272
330
|
// add, so parallel select reserves the nearest manifest. A none prefix does
|
|
273
|
-
// not.
|
|
274
|
-
//
|
|
331
|
+
// not. Prose with no module path, npm scope, `package`, or non-toolchain
|
|
332
|
+
// version pin does not either. shortcut: name the package when the slice
|
|
333
|
+
// edits a manifest; "Go 1.22.0" or "standard library" is not a package add.
|
|
275
334
|
func externalClaimsManifest(external string) bool {
|
|
276
335
|
ext := strings.ToLower(strings.Trim(strings.TrimSpace(external), "`"))
|
|
277
336
|
if ext == "" || ext == "-" || ext == "n/a" || strings.HasPrefix(ext, "none") {
|
|
278
337
|
return false
|
|
279
338
|
}
|
|
280
|
-
|
|
281
|
-
if manifestDenialRe.MatchString(raw) && !namesPackage(raw) {
|
|
282
|
-
return false
|
|
283
|
-
}
|
|
284
|
-
return true
|
|
339
|
+
return namesPackage(strings.TrimSpace(external))
|
|
285
340
|
}
|
|
286
341
|
|
|
287
342
|
// namesPackage reports a module path, version pin, npm scope, or `package`
|
|
@@ -293,7 +348,7 @@ func namesPackage(raw string) bool {
|
|
|
293
348
|
return true
|
|
294
349
|
}
|
|
295
350
|
}
|
|
296
|
-
if npmScopeRe.MatchString(raw) || versionPinRe.MatchString(raw) {
|
|
351
|
+
if npmScopeRe.MatchString(raw) || versionPinRe.MatchString(toolchainVersionRe.ReplaceAllString(raw, " ")) {
|
|
297
352
|
return true
|
|
298
353
|
}
|
|
299
354
|
for _, loc := range modulePathRe.FindAllStringIndex(raw, -1) {
|
|
@@ -396,6 +451,24 @@ func afkMaxParallel(root string) int {
|
|
|
396
451
|
return n
|
|
397
452
|
}
|
|
398
453
|
|
|
454
|
+
// parallelPlanVisible reports that the root must see every slice. An
|
|
455
|
+
// unattended cap above 1, or a running parallel lease, is a batch: scoping
|
|
456
|
+
// the bundle to active_slice hides the siblings that batch is supposed to start.
|
|
457
|
+
func parallelPlanVisible(devritesRoot, slug string) bool {
|
|
458
|
+
if afkMaxParallel(devritesRoot) >= 2 {
|
|
459
|
+
return true
|
|
460
|
+
}
|
|
461
|
+
if slug == "" {
|
|
462
|
+
return false
|
|
463
|
+
}
|
|
464
|
+
path, err := parallel.LeasePath(filepath.Dir(devritesRoot), slug)
|
|
465
|
+
if err != nil {
|
|
466
|
+
return false
|
|
467
|
+
}
|
|
468
|
+
lease, err := parallel.ReadLease(path)
|
|
469
|
+
return err == nil && lease.Status == parallel.StatusRunning
|
|
470
|
+
}
|
|
471
|
+
|
|
399
472
|
func allDone(ids []string, done map[string]bool) bool {
|
|
400
473
|
for _, id := range ids {
|
|
401
474
|
if !done[id] {
|
|
@@ -52,7 +52,7 @@ var (
|
|
|
52
52
|
// corpus (minus the template's AI-SPEC annex) names a model, agent, RAG, or
|
|
53
53
|
// tool surface.
|
|
54
54
|
func keepModelSurface(role string, facts triggerFacts) bool {
|
|
55
|
-
return role == "security-auditor" || facts.aiSpec || strings.TrimSpace(facts.corpus) == "" ||
|
|
55
|
+
return roleBase(role) == "security-auditor" || facts.aiSpec || strings.TrimSpace(facts.corpus) == "" ||
|
|
56
56
|
aiSignalRe.MatchString(aiAnnexRe.ReplaceAllString(facts.corpus, "$1"))
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -102,7 +102,7 @@ func suggestTriggers(manifest loadsManifest, root, featureDir string, opts conte
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
func gatherTriggerFacts(root, featureDir, role string) triggerFacts {
|
|
105
|
-
facts := triggerFacts{role: role}
|
|
105
|
+
facts := triggerFacts{role: roleBase(role)}
|
|
106
106
|
if root != "" {
|
|
107
107
|
afkPath := filepath.Join(root, "AFK")
|
|
108
108
|
if info, err := os.Stat(afkPath); err == nil && info.Mode().IsRegular() {
|
|
@@ -265,7 +265,7 @@ func ConvertCursorToTable(lines []string) ([]string, bool) {
|
|
|
265
265
|
if strings.Contains(value, "|") {
|
|
266
266
|
continue
|
|
267
267
|
}
|
|
268
|
-
spelling, ok :=
|
|
268
|
+
spelling, ok := CanonicalCursorKey(key)
|
|
269
269
|
if !ok {
|
|
270
270
|
continue
|
|
271
271
|
}
|
|
@@ -278,14 +278,14 @@ func ConvertCursorToTable(lines []string) ([]string, bool) {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
func isMigratableCursorKey(key string) bool {
|
|
281
|
-
_, ok :=
|
|
281
|
+
_, ok := CanonicalCursorKey(key)
|
|
282
282
|
return ok
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
//
|
|
285
|
+
// CanonicalCursorKey maps a cursor key (including aliases) to the
|
|
286
286
|
// canonical table-row spelling. Matching is normalization-based, so
|
|
287
|
-
// "Next step" resolves to the "next_action" spelling.
|
|
288
|
-
func
|
|
287
|
+
// "Next step" resolves to the "next_action" spelling. Unknown extensions return false.
|
|
288
|
+
func CanonicalCursorKey(key string) (string, bool) {
|
|
289
289
|
normalized := normalizeCursorKey(key)
|
|
290
290
|
canonicals := []string{CursorPhase, CursorStatus, CursorNextAction, CursorQuestionID,
|
|
291
291
|
CursorActiveSlice, CursorAFKSlicesRemaining, CursorSequenceParent,
|
|
@@ -169,9 +169,9 @@ named in the re-dispatch contract; changes outside task paths stop for the human
|
|
|
169
169
|
|
|
170
170
|
## Scale limits
|
|
171
171
|
|
|
172
|
-
The role table bounds rosters. Use
|
|
173
|
-
counting children any leaf spawned
|
|
174
|
-
Refuse
|
|
172
|
+
The role table bounds rosters. Use up to ten concurrent read-only reviewers,
|
|
173
|
+
the same cap as a writer batch, counting children any leaf spawned.
|
|
174
|
+
Refuse a larger swarm and role substitution: sharpen scope or use a bounded second pass when coverage is missing.
|
|
175
175
|
Overlapping reviewers increase duplicate findings and reconciliation work.
|
|
176
176
|
|
|
177
177
|
## Children are tools, not peers
|