devrites 7.2.7 → 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 +8 -0
- package/README.md +6 -1
- package/docs/command-map.md +4 -0
- package/docs/engine/commands.md +22 -0
- 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 +3 -2
- package/engine/internal/lib/dispatch.go +4 -0
- package/engine/internal/lib/resolve.go +10 -5
- package/engine/internal/lib/state_edit.go +9 -2
- package/engine/internal/lib/triggers.go +2 -2
- package/engine/internal/state/cursor.go +5 -5
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +3 -3
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +3 -3
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +3 -3
- package/pack/generated/devin/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +3 -3
- package/pack/generated/dsh/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +3 -3
- package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +10 -2
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +3 -3
- package/pack/generated/pi/skills/devrites-lib/reference/standards/agents.md +6 -2
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +3 -3
- package/package.json +1 -1
- package/scripts/build-host-artifacts.sh +5 -6
- package/scripts/omp-generate.sh +24 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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.2.8](https://github.com/ViktorsBaikers/DevRites/compare/v7.2.7...v7.2.8) (2026-10-31)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **no-release:** close installer evidence and recovery integrity gaps ([#200](https://github.com/ViktorsBaikers/DevRites/issues/200)) ([edb8754](https://github.com/ViktorsBaikers/DevRites/commit/edb8754ecf80d4a0dd83590c5b346787c3647244))
|
|
10
|
+
* **no-release:** select exact OMP specialists and preserve reviewer roles ([#199](https://github.com/ViktorsBaikers/DevRites/issues/199)) ([b31814a](https://github.com/ViktorsBaikers/DevRites/commit/b31814a37d80c45a27d5470ab5464cd93c91f76a))
|
|
11
|
+
* **release:** publish pending installer fixes ([dfd2192](https://github.com/ViktorsBaikers/DevRites/commit/dfd2192d1b7d9da1b0111603760c5eb8f246b9a4))
|
|
12
|
+
|
|
5
13
|
## [7.2.7](https://github.com/ViktorsBaikers/DevRites/compare/v7.2.6...v7.2.7) (2026-10-31)
|
|
6
14
|
|
|
7
15
|
### Fixed
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ history can't lose it. After you clear the conversation, switch hosts, or hand
|
|
|
31
31
|
the feature to another engineer, the next session reads the same plan,
|
|
32
32
|
decisions, and evidence before it continues.
|
|
33
33
|
|
|
34
|
-
**Status:** [`v7.2.
|
|
34
|
+
**Status:** [`v7.2.8`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v7.2.8): 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
|
|
|
@@ -382,6 +382,11 @@ with `--force`; legacy manifests without hashes also require `--force`.
|
|
|
382
382
|
`--force --dry-run` lists the exact destructive actions. Every settings and
|
|
383
383
|
marker merge is checked before the first write, so a rejected merge (such as a
|
|
384
384
|
Codex config that already sets top-level `default_permissions`) leaves the project untouched.
|
|
385
|
+
Unmerges receive the same preflight: malformed settings or legacy hooks JSON
|
|
386
|
+
blocks uninstall and dropped-host pruning, including dry runs, before writes.
|
|
387
|
+
Existing files retain their permission bits. Claude permission ownership is
|
|
388
|
+
recorded before settings replacement, so interrupted merges retain the records
|
|
389
|
+
needed to remove installer-added grants on retry and uninstall.
|
|
385
390
|
Marker-merged files keep user content outside the DevRites block, and
|
|
386
391
|
`.devrites/` runtime state stays in place. `update` reuses the install flags
|
|
387
392
|
recorded in the manifest; flags you pass explicitly win and are recorded. The installer refuses symlinks, junctions, and paths that
|
package/docs/command-map.md
CHANGED
|
@@ -50,6 +50,10 @@ The Go surface is intentionally closed and deterministic:
|
|
|
50
50
|
| Install/operator | `install`, `update`, `uninstall`, `version` | Direct update acquires latest; npm/bootstrap may supply local candidates; the engine performs manifest-owned local changes. |
|
|
51
51
|
| Native policy | no engine command | Skills/root own spec grammar re-read, qid allocation, Clarify cursor edits, AFK/recovery accounting, and read-only `/rite-doctor`. |
|
|
52
52
|
|
|
53
|
+
`state apply-bundle` merges workspace-relative path aliases into one target:
|
|
54
|
+
`plan.md`, `./plan.md`, and `refs/../plan.md` share base pins and patch hunks.
|
|
55
|
+
Conflicting replacements or base hashes block the entire bundle without writes.
|
|
56
|
+
|
|
53
57
|
`devrites-engine help` exhaustively lists the operational engine commands.
|
|
54
58
|
Standard `help`, `-h`, `--help`, `version`, and `--version` forms remain
|
|
55
59
|
available; each operational command and subcommand also accepts `-h` / `--help`.
|
package/docs/engine/commands.md
CHANGED
|
@@ -58,6 +58,13 @@ workflow strategy.
|
|
|
58
58
|
| `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). |
|
|
59
59
|
| `version` | Print the engine version. |
|
|
60
60
|
|
|
61
|
+
For `context --role`, optional `devrites-` prefixes and `.lane` suffixes resolve
|
|
62
|
+
the base specialist's contract, workspace read-set, and role-specific rules.
|
|
63
|
+
The full label remains in bundle filenames and attribution. Reviewer
|
|
64
|
+
`dispatch return` requires `--result <file>` or an explicit
|
|
65
|
+
`--outcome findings|no-findings|gap`; omitting both refuses the return without
|
|
66
|
+
changing the wave or releasing its claim. Non-reviewer returns are unchanged.
|
|
67
|
+
|
|
61
68
|
`help`, `-h`, and `--help` print this operational inventory. Each operational
|
|
62
69
|
command and subcommand also accepts `-h` / `--help` and prints that command's
|
|
63
70
|
usage to stdout without requiring a workspace. `version` and `--version` print
|
|
@@ -66,6 +73,21 @@ engine has no compatibility aliases or tombstones. The `add`/`upgrade`/`remove`
|
|
|
66
73
|
conveniences belong only to the `npx devrites` adapter, not to the engine
|
|
67
74
|
command namespace.
|
|
68
75
|
|
|
76
|
+
## State and evidence safety
|
|
77
|
+
|
|
78
|
+
- `state set` resolves known cursor aliases before checking protected fields
|
|
79
|
+
and duplicate keys. Underscore variants cannot bypass phase, schema, or
|
|
80
|
+
saved-return-cursor protections; extension keys retain their spelling.
|
|
81
|
+
- `state resolve` ignores fenced examples when matching and removing an
|
|
82
|
+
Awaiting-human section. Resolving another question leaves the live wait intact.
|
|
83
|
+
- `state close` reads the physical `ACTIVE` pointer under the feature lock;
|
|
84
|
+
`DEVRITES_WORKSPACE` cannot make it clear another feature's pointer.
|
|
85
|
+
- `gates run` and `reverify` snapshot every declared `INPUTS` dependency around
|
|
86
|
+
the actual shared CHECK execution, recheck during locked writeback, and
|
|
87
|
+
invalidate earlier passes if later checks move their inputs.
|
|
88
|
+
Missing or changed inputs cannot certify a pass. Execution, discarded-result,
|
|
89
|
+
or evidence-write failures exit `3`, even if an older ledger still reads met.
|
|
90
|
+
|
|
69
91
|
## Check boundary
|
|
70
92
|
|
|
71
93
|
The candidate gate validates and hashes path/state/type/mode/content identity;
|
package/docs/orchestration.md
CHANGED
|
@@ -37,6 +37,15 @@ Skills name the exact role and bounded task in normal language. The host owns
|
|
|
37
37
|
internal spawn fields, scheduling, waiting, follow-ups, and result delivery.
|
|
38
38
|
DevRites does not parse rollout logs or maintain dispatch receipts.
|
|
39
39
|
|
|
40
|
+
On OMP, each `task` item selects its specialist through `agent`, including every
|
|
41
|
+
item in a `tasks[]` batch: `devrites-code-reviewer`, `devrites-test-analyst`, or
|
|
42
|
+
`devrites-slice-wright` for those roles. Writing “you are…” in a task or naming it
|
|
43
|
+
after a specialist does not select that profile. Lane suffixes stay on engine
|
|
44
|
+
role labels, not native profile names. Check the actual host selector before
|
|
45
|
+
admitting results: the engine records declared roles and host handles, not
|
|
46
|
+
authenticated native profile identity. A missing profile remains a gap, not
|
|
47
|
+
permission to select a generic/default child.
|
|
48
|
+
|
|
40
49
|
Quick, Standard, and Full select evidence/review depth only; they are not agent
|
|
41
50
|
API versions or execution tiers. The shared contract lives in
|
|
42
51
|
[`orchestration-profiles.md`](../pack/.claude/skills/devrites-lib/reference/orchestration-profiles.md).
|
|
@@ -160,12 +160,12 @@ func cmdStatus(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
160
160
|
fmt.Fprintf(stderr, "gates status: %v\n", err)
|
|
161
161
|
return ExitBlocked
|
|
162
162
|
}
|
|
163
|
-
return reportStatus(slug, ledgerPath, testPlanPath, stdout, stderr)
|
|
163
|
+
return reportStatus(slug, ledgerPath, testPlanPath, false, stdout, stderr)
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// reportStatus prints the stable greppable reduction. It returns ExitOK only
|
|
167
|
-
// for a parse-clean ledger with every gate met and
|
|
168
|
-
func reportStatus(slug, ledgerPath, testPlanPath string, stdout, stderr io.Writer) int {
|
|
167
|
+
// for a parse-clean ledger with every gate met, none abandoned and no failed attempt.
|
|
168
|
+
func reportStatus(slug, ledgerPath, testPlanPath string, attemptFailed bool, stdout, stderr io.Writer) int {
|
|
169
169
|
text, err := ReadLedgerFile(ledgerPath)
|
|
170
170
|
if err != nil {
|
|
171
171
|
fmt.Fprintf(stderr, "gates status: %v\n", err)
|
|
@@ -191,7 +191,7 @@ func reportStatus(slug, ledgerPath, testPlanPath string, stdout, stderr io.Write
|
|
|
191
191
|
case reduction.HandoffRequired():
|
|
192
192
|
fmt.Fprintln(stdout, "result: handoff")
|
|
193
193
|
return ExitBlocked
|
|
194
|
-
case !reduction.AllMet():
|
|
194
|
+
case attemptFailed || !reduction.AllMet():
|
|
195
195
|
fmt.Fprintln(stdout, "result: not-met")
|
|
196
196
|
return ExitBlocked
|
|
197
197
|
default:
|
|
@@ -269,6 +269,7 @@ func cmdRun(root string, args []string, reverify bool, stdout, stderr io.Writer)
|
|
|
269
269
|
}
|
|
270
270
|
var ran []GateResult
|
|
271
271
|
var ranDoc *Document
|
|
272
|
+
attemptFailed := false
|
|
272
273
|
|
|
273
274
|
// The gates lock serializes check execution; the feature lock is held only
|
|
274
275
|
// to read inputs and per writeback, so dispatch and other workspace writers
|
|
@@ -294,6 +295,9 @@ func cmdRun(root string, args []string, reverify bool, stdout, stderr io.Writer)
|
|
|
294
295
|
}
|
|
295
296
|
ran, ranDoc = RunGates(context.Background(), project, ledgerPath, doc, approved, reverify, timeout, featureLock), doc
|
|
296
297
|
for _, result := range ran {
|
|
298
|
+
if result.Ran && !result.Passed || result.Discarded || result.WriteErr != nil {
|
|
299
|
+
attemptFailed = true
|
|
300
|
+
}
|
|
297
301
|
switch {
|
|
298
302
|
case !result.Ran:
|
|
299
303
|
fmt.Fprintf(stdout, "gate %s: skipped (%s)\n", result.ID, result.Detail)
|
|
@@ -320,7 +324,7 @@ func cmdRun(root string, args []string, reverify bool, stdout, stderr io.Writer)
|
|
|
320
324
|
}
|
|
321
325
|
}
|
|
322
326
|
fmt.Fprintln(stdout, "---")
|
|
323
|
-
return reportStatus(slug, ledgerPath, testPlanPath, stdout, stderr)
|
|
327
|
+
return reportStatus(slug, ledgerPath, testPlanPath, attemptFailed, stdout, stderr)
|
|
324
328
|
}
|
|
325
329
|
|
|
326
330
|
func cmdLint(root string, args []string, stdout, stderr io.Writer) int {
|
|
@@ -28,7 +28,7 @@ type GateResult struct {
|
|
|
28
28
|
Ran bool
|
|
29
29
|
Passed bool
|
|
30
30
|
Detail string // exit/match/timeout/approval diagnostic
|
|
31
|
-
Discarded bool // result dropped:
|
|
31
|
+
Discarded bool // result dropped: bound oracle or inputs changed during the run
|
|
32
32
|
WriteErr error // evidence writeback failed for a reason other than drift
|
|
33
33
|
Reused bool // met gate kept because its INPUTS are unchanged
|
|
34
34
|
Inputs string // INPUTS content digest at run time, when declared
|
|
@@ -77,13 +77,24 @@ func ReadLedgerFile(ledgerPath string) (string, error) {
|
|
|
77
77
|
// execute; writeResult re-verifies the gate's oracle before it writes.
|
|
78
78
|
func RunGates(ctx context.Context, root, ledgerPath string, doc *Document, approved []ApprovedCommand, reverify bool, timeout time.Duration, writeLock func(func() error) error) []GateResult {
|
|
79
79
|
var results []GateResult
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
executions := map[string]struct {
|
|
81
|
+
ran bool
|
|
82
|
+
outcome RunOutcome
|
|
83
|
+
span [2]time.Time
|
|
84
|
+
inputs map[string]string
|
|
85
|
+
}{}
|
|
86
|
+
// Collect every dependent INPUTS spec before any shared command can run.
|
|
87
|
+
for _, gate := range doc.Gates {
|
|
88
|
+
if gate.Check == "" || gate.Inputs == "" {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
key := gate.Check + "\x00" + filepath.Join(root, filepath.FromSlash(gate.Cwd))
|
|
92
|
+
execution := executions[key]
|
|
93
|
+
if execution.inputs == nil {
|
|
94
|
+
execution.inputs = map[string]string{}
|
|
85
95
|
}
|
|
86
|
-
|
|
96
|
+
execution.inputs[gate.Inputs] = ""
|
|
97
|
+
executions[key] = execution
|
|
87
98
|
}
|
|
88
99
|
for _, gate := range doc.Gates {
|
|
89
100
|
if _, abandoned := doc.Abandoned[gate.ID]; abandoned {
|
|
@@ -95,15 +106,15 @@ func RunGates(ctx context.Context, root, ledgerPath string, doc *Document, appro
|
|
|
95
106
|
continue
|
|
96
107
|
}
|
|
97
108
|
inputs := ""
|
|
98
|
-
if gate.Inputs != "" {
|
|
99
|
-
// An unreadable or unmatched spec yields "", which never matches, so the gate re-executes.
|
|
100
|
-
inputs, _ = InputsDigest(root, gate.Inputs)
|
|
101
|
-
}
|
|
102
109
|
if GateState(gate, doc) == StateMet {
|
|
103
110
|
if gate.Inputs == "" && !reverify {
|
|
104
111
|
results = append(results, GateResult{ID: gate.ID, Passed: true, Detail: "already met"})
|
|
105
112
|
continue
|
|
106
113
|
}
|
|
114
|
+
if gate.Inputs != "" {
|
|
115
|
+
// An unreadable or unmatched spec never matches recorded evidence.
|
|
116
|
+
inputs, _ = InputsDigest(root, gate.Inputs)
|
|
117
|
+
}
|
|
107
118
|
// A gate that declares INPUTS is reused, under run and reverify alike,
|
|
108
119
|
// exactly while the files it depends on are byte-identical; any change re-executes it.
|
|
109
120
|
if inputs != "" && inputs == recordedInputs(gate) {
|
|
@@ -120,36 +131,84 @@ func RunGates(ctx context.Context, root, ledgerPath string, doc *Document, appro
|
|
|
120
131
|
results = append(results, GateResult{ID: gate.ID, Detail: err.Error()})
|
|
121
132
|
continue
|
|
122
133
|
}
|
|
123
|
-
cwd := root
|
|
124
|
-
if gate.Cwd != "" {
|
|
125
|
-
cwd = filepath.Join(root, filepath.FromSlash(gate.Cwd))
|
|
126
|
-
}
|
|
134
|
+
cwd := filepath.Join(root, filepath.FromSlash(gate.Cwd))
|
|
127
135
|
key := gate.Check + "\x00" + cwd
|
|
128
|
-
|
|
129
|
-
if !ran {
|
|
136
|
+
execution := executions[key]
|
|
137
|
+
if !execution.ran {
|
|
138
|
+
for spec := range execution.inputs {
|
|
139
|
+
execution.inputs[spec], _ = InputsDigest(root, spec)
|
|
140
|
+
}
|
|
130
141
|
started := time.Now().UTC()
|
|
131
|
-
outcome = Execute(ctx, gate.Check, cwd, timeout)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
execution.outcome = Execute(ctx, gate.Check, cwd, timeout)
|
|
143
|
+
execution.span = [2]time.Time{started, time.Now().UTC()}
|
|
144
|
+
for spec, before := range execution.inputs {
|
|
145
|
+
after, _ := InputsDigest(root, spec)
|
|
146
|
+
if after != before {
|
|
147
|
+
execution.inputs[spec] = ""
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
execution.ran = true
|
|
151
|
+
executions[key] = execution
|
|
152
|
+
}
|
|
153
|
+
outcome := execution.outcome
|
|
154
|
+
inputs = execution.inputs[gate.Inputs]
|
|
155
|
+
passed := outcome.Err == nil && outcome.ExitCode == 0 && expectation.Matches(outcome.Output)
|
|
156
|
+
detail := "pass"
|
|
157
|
+
if !passed {
|
|
158
|
+
detail = failDetail(outcome, expectation)
|
|
159
|
+
}
|
|
160
|
+
write := func() error {
|
|
161
|
+
if passed && gate.Inputs != "" {
|
|
162
|
+
// Earlier writebacks may have moved files since execution.
|
|
163
|
+
current, _ := InputsDigest(root, gate.Inputs)
|
|
164
|
+
if inputs == "" || current != inputs {
|
|
165
|
+
passed = false
|
|
166
|
+
detail = "INPUTS changed or could not be read during the run"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
evidence := "pending"
|
|
170
|
+
if passed {
|
|
171
|
+
evidence = withInputs(FormatEvidence(gate, OutputDigest(outcome.Output), outcome.OutputBytes, time.Now().UTC().Format(time.RFC3339)), inputs)
|
|
172
|
+
}
|
|
173
|
+
return writeResult(ledgerPath, gate, evidence, passed)
|
|
174
|
+
}
|
|
175
|
+
var writeErr error
|
|
176
|
+
if writeLock == nil {
|
|
177
|
+
writeErr = write()
|
|
178
|
+
} else {
|
|
179
|
+
writeErr = writeLock(write)
|
|
180
|
+
}
|
|
181
|
+
results = append(results, GateResult{
|
|
182
|
+
ID: gate.ID, Ran: true, Passed: passed,
|
|
183
|
+
Discarded: errors.Is(writeErr, ErrOracleMoved), WriteErr: writeErr,
|
|
184
|
+
Detail: resultDetail(writeErr, detail),
|
|
185
|
+
Inputs: inputs, Outcome: outcome, Started: execution.span[0], Ended: execution.span[1],
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
// Later checks may invalidate an earlier execution or reused pass.
|
|
189
|
+
for i, gate := range doc.Gates {
|
|
190
|
+
result := &results[i]
|
|
191
|
+
if !result.Passed || result.Discarded || result.WriteErr != nil || gate.Inputs == "" {
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
validate := func() error {
|
|
195
|
+
current, _ := InputsDigest(root, gate.Inputs)
|
|
196
|
+
if current != "" && current == result.Inputs {
|
|
197
|
+
return nil
|
|
198
|
+
}
|
|
199
|
+
result.Passed, result.Reused, result.Discarded = false, false, true
|
|
200
|
+
result.Detail = "INPUTS changed or could not be read after a later check"
|
|
201
|
+
return writeResult(ledgerPath, gate, "pending", false)
|
|
202
|
+
}
|
|
203
|
+
var err error
|
|
204
|
+
if writeLock == nil {
|
|
205
|
+
err = validate()
|
|
145
206
|
} else {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Inputs: inputs, Outcome: outcome, Started: span[0], Ended: span[1],
|
|
152
|
-
})
|
|
207
|
+
err = writeLock(validate)
|
|
208
|
+
}
|
|
209
|
+
if err != nil {
|
|
210
|
+
result.WriteErr = err
|
|
211
|
+
result.Detail = resultDetail(err, result.Detail)
|
|
153
212
|
}
|
|
154
213
|
}
|
|
155
214
|
return results
|
|
@@ -194,14 +194,19 @@ func checkedCanonicalDir(root, candidate, parent, slug string) (string, error) {
|
|
|
194
194
|
return filepath.Clean(candidate), nil
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// ActiveSlug
|
|
198
|
-
// is not an error; it yields the empty slug.
|
|
197
|
+
// ActiveSlug resolves the workspace override, or reads the active-feature pointer.
|
|
198
|
+
// A missing pointer file is not an error; it yields the empty slug.
|
|
199
199
|
func ActiveSlug(root string) (string, error) {
|
|
200
200
|
if ws, err := WorkspaceOverrideChecked(root, ""); err != nil {
|
|
201
201
|
return "", err
|
|
202
202
|
} else if ws != "" {
|
|
203
203
|
return filepath.Base(ws), nil
|
|
204
204
|
}
|
|
205
|
+
return PhysicalActiveSlug(root)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// PhysicalActiveSlug reads the validated ACTIVE pointer without workspace overrides.
|
|
209
|
+
func PhysicalActiveSlug(root string) (string, error) {
|
|
205
210
|
activePath := filepath.Join(root, "ACTIVE")
|
|
206
211
|
if info, err := os.Lstat(activePath); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
|
207
212
|
return "", fmt.Errorf("DRV-ACTIVE-SYMLINK: ACTIVE is a symlink; run `rm -f %q`", activePath)
|
|
@@ -231,6 +231,11 @@ func (r *runner) writeConfined(dest string, data []byte, perm fs.FileMode) error
|
|
|
231
231
|
return err
|
|
232
232
|
}
|
|
233
233
|
defer func() { _ = root.Close() }()
|
|
234
|
+
if info, err := root.Stat(rel); err == nil {
|
|
235
|
+
perm = info.Mode().Perm()
|
|
236
|
+
} else if !os.IsNotExist(err) {
|
|
237
|
+
return err
|
|
238
|
+
}
|
|
234
239
|
return fsutil.WriteFileAtomicIn(root, rel, data, perm)
|
|
235
240
|
}
|
|
236
241
|
|
|
@@ -17,11 +17,18 @@ import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
func (r *runner) installClaudeSettingsMarker(state claudeSettingsMerge) error {
|
|
20
|
+
return r.installMarker(hostpack.ClaudeSettingsMerge.MarkerRel, claudeSettingsMarkerText(state))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func claudeSettingsMarkerText(state claudeSettingsMerge) string {
|
|
20
24
|
var b strings.Builder
|
|
21
25
|
b.WriteString(hostpack.ClaudeSettingsMerge.MarkerText + "\n" + claudeRulesAddedLine)
|
|
22
26
|
if state.created {
|
|
23
27
|
b.WriteString("\n" + claudeSettingsCreatedLine)
|
|
24
28
|
}
|
|
29
|
+
if state.defaultModeOwned {
|
|
30
|
+
b.WriteString("\ndefault-mode=added")
|
|
31
|
+
}
|
|
25
32
|
rules := make([]string, 0, len(state.added))
|
|
26
33
|
for rule := range state.added {
|
|
27
34
|
rules = append(rules, rule)
|
|
@@ -30,7 +37,7 @@ func (r *runner) installClaudeSettingsMarker(state claudeSettingsMerge) error {
|
|
|
30
37
|
for _, rule := range rules {
|
|
31
38
|
b.WriteString("\nrule:" + rule)
|
|
32
39
|
}
|
|
33
|
-
return
|
|
40
|
+
return b.String()
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
const (
|
|
@@ -364,16 +371,45 @@ func (r *runner) mergeClaudeSettings(merge hostpack.JSONMerge) error {
|
|
|
364
371
|
return fmt.Errorf("encode merged Claude settings: %w", err)
|
|
365
372
|
}
|
|
366
373
|
data = append(data, '\n')
|
|
374
|
+
// Publish ownership before grants; keep retired ownership until settings land.
|
|
375
|
+
pending := r.pendingClaudeSettingsOwnership(state)
|
|
376
|
+
if err := r.installClaudeSettingsMarker(pending); err != nil {
|
|
377
|
+
return err
|
|
378
|
+
}
|
|
379
|
+
if r.records[merge.MarkerRel] == "" {
|
|
380
|
+
return fmt.Errorf("cannot merge %s without owning %s; use --force to replace the marker", merge.TargetRel, merge.MarkerRel)
|
|
381
|
+
}
|
|
382
|
+
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
383
|
+
return err
|
|
384
|
+
}
|
|
367
385
|
if err := r.writeConfined(dest, data, 0o644); err != nil {
|
|
368
386
|
return fmt.Errorf("cannot write %s: %w", merge.TargetRel, err)
|
|
369
387
|
}
|
|
370
|
-
|
|
388
|
+
if pending.defaultModeOwned || len(pending.added) != len(state.added) {
|
|
389
|
+
if err := r.refreshPreflight(merge.MarkerRel); err != nil {
|
|
390
|
+
return err
|
|
391
|
+
}
|
|
392
|
+
return r.installClaudeSettingsMarker(state)
|
|
393
|
+
}
|
|
394
|
+
return nil
|
|
371
395
|
}
|
|
372
396
|
|
|
373
397
|
// claudeSettingsMerge is what the settings marker records about a merge.
|
|
374
398
|
type claudeSettingsMerge struct {
|
|
375
|
-
created
|
|
376
|
-
added
|
|
399
|
+
created bool
|
|
400
|
+
added map[string]bool
|
|
401
|
+
defaultModeOwned bool
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
func (r *runner) pendingClaudeSettingsOwnership(next claudeSettingsMerge) claudeSettingsMerge {
|
|
405
|
+
pending := claudeSettingsMerge{
|
|
406
|
+
created: next.created, added: r.managedClaudePermissionRules(),
|
|
407
|
+
defaultModeOwned: r.claudeDefaultModeOwned(),
|
|
408
|
+
}
|
|
409
|
+
for rule := range next.added {
|
|
410
|
+
pending.added[rule] = true
|
|
411
|
+
}
|
|
412
|
+
return pending
|
|
377
413
|
}
|
|
378
414
|
|
|
379
415
|
// claudeSettingsNext returns the merged .claude/settings.json without
|
|
@@ -433,6 +469,12 @@ func stripHooksWith(path string, write func([]byte) error, remove func() error)
|
|
|
433
469
|
}
|
|
434
470
|
|
|
435
471
|
func (r *runner) stripClaudeSettings(path string) error {
|
|
472
|
+
return r.stripClaudeSettingsWith(path, func(data []byte) error {
|
|
473
|
+
return r.writeConfined(path, data, 0o644)
|
|
474
|
+
}, func() error { return r.removeConfined(path) })
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
func (r *runner) stripClaudeSettingsWith(path string, write func([]byte) error, remove func() error) error {
|
|
436
478
|
current, err := readJSON(path)
|
|
437
479
|
if err != nil {
|
|
438
480
|
if errors.Is(err, fs.ErrNotExist) {
|
|
@@ -442,14 +484,14 @@ func (r *runner) stripClaudeSettings(path string) error {
|
|
|
442
484
|
}
|
|
443
485
|
next := stripDevritesSettings(current, r.claudeDefaultModeOwned(), r.managedClaudePermissionRules())
|
|
444
486
|
if len(next) == 0 && r.claudeSettingsCreated() {
|
|
445
|
-
return
|
|
487
|
+
return remove()
|
|
446
488
|
}
|
|
447
489
|
data, err := json.MarshalIndent(next, "", " ")
|
|
448
490
|
if err != nil {
|
|
449
491
|
return fmt.Errorf("encode Claude settings: %w", err)
|
|
450
492
|
}
|
|
451
493
|
data = append(data, '\n')
|
|
452
|
-
return
|
|
494
|
+
return write(data)
|
|
453
495
|
}
|
|
454
496
|
|
|
455
497
|
func (r *runner) stripMarkerConfined(path, begin, end string) error {
|
|
@@ -6,6 +6,7 @@ import (
|
|
|
6
6
|
"fmt"
|
|
7
7
|
"io"
|
|
8
8
|
"io/fs"
|
|
9
|
+
"maps"
|
|
9
10
|
"os"
|
|
10
11
|
"path/filepath"
|
|
11
12
|
"slices"
|
|
@@ -22,6 +23,7 @@ func (r *runner) preflightInstall() error {
|
|
|
22
23
|
return fmt.Errorf("preflight install paths: %w", err)
|
|
23
24
|
}
|
|
24
25
|
var conflicts []string
|
|
26
|
+
dryTree := map[string][]byte{}
|
|
25
27
|
for rel := range desired {
|
|
26
28
|
snapshot, err := r.rememberPath(rel)
|
|
27
29
|
if err != nil {
|
|
@@ -34,7 +36,8 @@ func (r *runner) preflightInstall() error {
|
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
|
-
for
|
|
39
|
+
for _, rel := range slices.Sorted(maps.Keys(r.prev)) {
|
|
40
|
+
record := r.prev[rel]
|
|
38
41
|
if desired[rel] || hostpack.PreserveOnPrune(rel) {
|
|
39
42
|
continue
|
|
40
43
|
}
|
|
@@ -58,7 +61,7 @@ func (r *runner) preflightInstall() error {
|
|
|
58
61
|
if _, err := r.rememberPath(merge.TargetRel); err != nil {
|
|
59
62
|
return err
|
|
60
63
|
}
|
|
61
|
-
if err := dryRunStripMarker(
|
|
64
|
+
if err := r.dryRunStripMarker(dryTree, merge); err != nil {
|
|
62
65
|
return err
|
|
63
66
|
}
|
|
64
67
|
}
|
|
@@ -104,19 +107,34 @@ func (r *runner) preflightMerges() error {
|
|
|
104
107
|
return fmt.Errorf("merge %s: %w", hostpack.CodexConfigMerge.TargetRel, err)
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
|
-
|
|
110
|
+
_, settings, err := r.claudeSettingsNext()
|
|
111
|
+
if err != nil {
|
|
108
112
|
return fmt.Errorf("seed claude settings: %w", err)
|
|
109
113
|
}
|
|
114
|
+
marker := hostpack.ClaudeSettingsMerge.MarkerRel
|
|
115
|
+
if snapshot := r.preflight[marker]; !snapshot.missing && !r.opts.Force {
|
|
116
|
+
if _, managed := r.prev[marker]; !managed {
|
|
117
|
+
text := claudeSettingsMarkerText(r.pendingClaudeSettingsOwnership(settings)) + "\n"
|
|
118
|
+
sum := sha256.Sum256([]byte(text))
|
|
119
|
+
if snapshot.hash != fmt.Sprintf("sha256:%x", sum[:]) {
|
|
120
|
+
return fmt.Errorf("cannot merge %s without owning %s; use --force to replace the marker", hostpack.ClaudeSettingsMerge.TargetRel, marker)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
110
124
|
return nil
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
func (r *runner) preflightUninstall(entries []string) error {
|
|
114
128
|
var conflicts []string
|
|
129
|
+
dryTree := map[string][]byte{}
|
|
115
130
|
for _, rel := range entries {
|
|
116
131
|
if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
|
|
117
132
|
if _, err := r.rememberPath(merge.TargetRel); err != nil {
|
|
118
133
|
return err
|
|
119
134
|
}
|
|
135
|
+
if err := r.dryRunStripMarker(dryTree, merge); err != nil {
|
|
136
|
+
return err
|
|
137
|
+
}
|
|
120
138
|
}
|
|
121
139
|
if !hostpack.ShouldRemoveOnUninstall(rel, entries) {
|
|
122
140
|
continue
|
|
@@ -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 {
|
|
@@ -80,7 +80,7 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
80
80
|
}
|
|
81
81
|
// A Build reviewer judges one slice; an unscoped bundle inlines whole
|
|
82
82
|
// plan/tasks/architecture into every dispatch.
|
|
83
|
-
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 == "" {
|
|
84
84
|
fmt.Fprintf(stderr, "context: BLOCKED: unscoped build reviewer bundle: --role %s in phase build needs --slice <SLICE-ID>\n", opts.role)
|
|
85
85
|
return 3
|
|
86
86
|
}
|
|
@@ -455,7 +455,7 @@ func workspaceReadSet(manifest loadsManifest, role string) ([]string, bool) {
|
|
|
455
455
|
if role == "" {
|
|
456
456
|
return manifest.Workspace, false
|
|
457
457
|
}
|
|
458
|
-
if list, ok := manifest.WorkspaceByRole[role]; ok {
|
|
458
|
+
if list, ok := manifest.WorkspaceByRole[roleBase(role)]; ok {
|
|
459
459
|
return list, true
|
|
460
460
|
}
|
|
461
461
|
return manifest.Workspace, false
|
|
@@ -915,6 +915,7 @@ func parseLoadsManifest(text string) (loadsManifest, bool, error) {
|
|
|
915
915
|
}
|
|
916
916
|
|
|
917
917
|
func roleAgentPath(manifest loadsManifest, role string) string {
|
|
918
|
+
role = roleBase(role)
|
|
918
919
|
if manifest.Agents != nil {
|
|
919
920
|
if rel, ok := manifest.Agents[role]; ok {
|
|
920
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),
|
|
@@ -455,6 +456,9 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
|
|
|
455
456
|
if r.Returned {
|
|
456
457
|
return fail("role %q already returned", role)
|
|
457
458
|
}
|
|
459
|
+
if isReviewerRole(role) && result == "" && outcome == "" {
|
|
460
|
+
return fail("reviewer %q needs --result <file> or --outcome <findings|no-findings|gap>", role)
|
|
461
|
+
}
|
|
458
462
|
if outcome != "" && !slices.Contains([]string{"findings", "no-findings", "gap"}, outcome) {
|
|
459
463
|
return fail("--outcome %q is not findings, no-findings, or gap", outcome)
|
|
460
464
|
}
|
|
@@ -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)
|
|
@@ -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,
|
|
@@ -45,8 +45,12 @@ Files own briefs; [`parallel-dispatch.md`](../parallel-dispatch.md) owns rosters
|
|
|
45
45
|
<!-- root-only -->
|
|
46
46
|
## Native invocation
|
|
47
47
|
|
|
48
|
-
Skills name exact fresh roles
|
|
49
|
-
|
|
48
|
+
Skills name exact fresh roles; hosts select matching native profiles and
|
|
49
|
+
spawn/wait/deliver. Task prose and display names do not select profiles.
|
|
50
|
+
Dispatch labels may add `devrites-` or `.lane`; the native profile stays
|
|
51
|
+
`devrites-<base-role>`. Verify the host selector before recording a start or
|
|
52
|
+
admitting a result; a ledger role and handle alone do not prove profile identity.
|
|
53
|
+
Root MUST NOT advance/claim completion before admitting required results;
|
|
50
54
|
running/orphaned/unavailable = `gap` — no root/generic substitute.
|
|
51
55
|
Where a feature workspace exists, every fresh dispatch to a named `devrites-*`
|
|
52
56
|
role — wave or solo — builds its read-set with `devrites-engine context <slug>
|
|
@@ -128,9 +128,9 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
128
128
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
129
129
|
else proof → `green|red|gap`.
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
Group recheck findings by owning role and changed surface. Each exact
|
|
132
|
+
specialist rechecks its own fingerprints and the correction diff; code, test,
|
|
133
|
+
and doubt remain separate dispatches, even on the same diff.
|
|
134
134
|
Reviewer fan-out does not shrink the writer batch. Reviewers queue at the
|
|
135
135
|
same cap as writers (10; see
|
|
136
136
|
[`parallel-dispatch.md`](../../devrites-lib/reference/parallel-dispatch.md#scale-limits)).
|
|
@@ -45,8 +45,12 @@ Files own briefs; [`parallel-dispatch.md`](../parallel-dispatch.md) owns rosters
|
|
|
45
45
|
<!-- root-only -->
|
|
46
46
|
## Native invocation
|
|
47
47
|
|
|
48
|
-
Skills name exact fresh roles
|
|
49
|
-
|
|
48
|
+
Skills name exact fresh roles; hosts select matching native profiles and
|
|
49
|
+
spawn/wait/deliver. Task prose and display names do not select profiles.
|
|
50
|
+
Dispatch labels may add `devrites-` or `.lane`; the native profile stays
|
|
51
|
+
`devrites-<base-role>`. Verify the host selector before recording a start or
|
|
52
|
+
admitting a result; a ledger role and handle alone do not prove profile identity.
|
|
53
|
+
Root MUST NOT advance/claim completion before admitting required results;
|
|
50
54
|
running/orphaned/unavailable = `gap` — no root/generic substitute.
|
|
51
55
|
Where a feature workspace exists, every fresh dispatch to a named `devrites-*`
|
|
52
56
|
role — wave or solo — builds its read-set with `devrites-engine context <slug>
|
|
@@ -128,9 +128,9 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
128
128
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
129
129
|
else proof → `green|red|gap`.
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
Group recheck findings by owning role and changed surface. Each exact
|
|
132
|
+
specialist rechecks its own fingerprints and the correction diff; code, test,
|
|
133
|
+
and doubt remain separate dispatches, even on the same diff.
|
|
134
134
|
Reviewer fan-out does not shrink the writer batch. Reviewers queue at the
|
|
135
135
|
same cap as writers (10; see
|
|
136
136
|
[`parallel-dispatch.md`](../../devrites-lib/reference/parallel-dispatch.md#scale-limits)).
|
|
@@ -45,8 +45,12 @@ Files own briefs; [`parallel-dispatch.md`](../parallel-dispatch.md) owns rosters
|
|
|
45
45
|
<!-- root-only -->
|
|
46
46
|
## Native invocation
|
|
47
47
|
|
|
48
|
-
Skills name exact fresh roles
|
|
49
|
-
|
|
48
|
+
Skills name exact fresh roles; hosts select matching native profiles and
|
|
49
|
+
spawn/wait/deliver. spawn_agent prose and display names do not select profiles.
|
|
50
|
+
Dispatch labels may add `devrites-` or `.lane`; the native profile stays
|
|
51
|
+
`devrites-<base-role>`. Verify the host selector before recording a start or
|
|
52
|
+
admitting a result; a ledger role and handle alone do not prove profile identity.
|
|
53
|
+
Root MUST NOT advance/claim completion before admitting required results;
|
|
50
54
|
running/orphaned/unavailable = `gap` — no root/generic substitute.
|
|
51
55
|
Where a feature workspace exists, every fresh dispatch to a named `devrites-*`
|
|
52
56
|
role — wave or solo — builds its read-set with `devrites-engine context --skills-root .agents/skills <slug>
|
|
@@ -128,9 +128,9 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
128
128
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
129
129
|
else proof → `green|red|gap`.
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
Group recheck findings by owning role and changed surface. Each exact
|
|
132
|
+
specialist rechecks its own fingerprints and the correction diff; code, test,
|
|
133
|
+
and doubt remain separate dispatches, even on the same diff.
|
|
134
134
|
Reviewer fan-out does not shrink the writer batch. Reviewers queue at the
|
|
135
135
|
same cap as writers (10; see
|
|
136
136
|
[`parallel-dispatch.md`](../../devrites-lib/reference/parallel-dispatch.md#scale-limits)).
|
|
@@ -45,8 +45,12 @@ Files own briefs; [`parallel-dispatch.md`](../parallel-dispatch.md) owns rosters
|
|
|
45
45
|
<!-- root-only -->
|
|
46
46
|
## Native invocation
|
|
47
47
|
|
|
48
|
-
Skills name exact fresh roles
|
|
49
|
-
|
|
48
|
+
Skills name exact fresh roles; hosts select matching native profiles and
|
|
49
|
+
spawn/wait/deliver. Task prose and display names do not select profiles.
|
|
50
|
+
Dispatch labels may add `devrites-` or `.lane`; the native profile stays
|
|
51
|
+
`devrites-<base-role>`. Verify the host selector before recording a start or
|
|
52
|
+
admitting a result; a ledger role and handle alone do not prove profile identity.
|
|
53
|
+
Root MUST NOT advance/claim completion before admitting required results;
|
|
50
54
|
running/orphaned/unavailable = `gap` — no root/generic substitute.
|
|
51
55
|
Where a feature workspace exists, every fresh dispatch to a named `devrites-*`
|
|
52
56
|
role — wave or solo — builds its read-set with `devrites-engine context --skills-root .devin/skills <slug>
|
|
@@ -128,9 +128,9 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
128
128
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
129
129
|
else proof → `green|red|gap`.
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
Group recheck findings by owning role and changed surface. Each exact
|
|
132
|
+
specialist rechecks its own fingerprints and the correction diff; code, test,
|
|
133
|
+
and doubt remain separate dispatches, even on the same diff.
|
|
134
134
|
Reviewer fan-out does not shrink the writer batch. Reviewers queue at the
|
|
135
135
|
same cap as writers (10; see
|
|
136
136
|
[`parallel-dispatch.md`](../../devrites-lib/reference/parallel-dispatch.md#scale-limits)).
|
|
@@ -45,8 +45,12 @@ Files own briefs; [`parallel-dispatch.md`](../parallel-dispatch.md) owns rosters
|
|
|
45
45
|
<!-- root-only -->
|
|
46
46
|
## Native invocation
|
|
47
47
|
|
|
48
|
-
Skills name exact fresh roles
|
|
49
|
-
|
|
48
|
+
Skills name exact fresh roles; hosts select matching native profiles and
|
|
49
|
+
spawn/wait/deliver. Task prose and display names do not select profiles.
|
|
50
|
+
Dispatch labels may add `devrites-` or `.lane`; the native profile stays
|
|
51
|
+
`devrites-<base-role>`. Verify the host selector before recording a start or
|
|
52
|
+
admitting a result; a ledger role and handle alone do not prove profile identity.
|
|
53
|
+
Root MUST NOT advance/claim completion before admitting required results;
|
|
50
54
|
running/orphaned/unavailable = `gap` — no root/generic substitute.
|
|
51
55
|
Where a feature workspace exists, every fresh dispatch to a named `devrites-*`
|
|
52
56
|
role — wave or solo — builds its read-set with `devrites-engine context --skills-root .dsh/skills <slug>
|
|
@@ -128,9 +128,9 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
128
128
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
129
129
|
else proof → `green|red|gap`.
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
Group recheck findings by owning role and changed surface. Each exact
|
|
132
|
+
specialist rechecks its own fingerprints and the correction diff; code, test,
|
|
133
|
+
and doubt remain separate dispatches, even on the same diff.
|
|
134
134
|
Reviewer fan-out does not shrink the writer batch. Reviewers queue at the
|
|
135
135
|
same cap as writers (10; see
|
|
136
136
|
[`parallel-dispatch.md`](../../devrites-lib/reference/parallel-dispatch.md#scale-limits)).
|
|
@@ -45,8 +45,16 @@ Files own briefs; [`parallel-dispatch.md`](skill://devrites-lib/reference/parall
|
|
|
45
45
|
<!-- root-only -->
|
|
46
46
|
## Native invocation
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
On omp, every `task` item (each `tasks[]` member) sets `agent` to the exact native profile;
|
|
49
|
+
use `agent: "devrites-code-reviewer"`, `agent: "devrites-test-analyst"`, or
|
|
50
|
+
`agent: "devrites-slice-wright"` for those roles, including rechecks and repairs.
|
|
51
|
+
Keep `.frontend`, `.backend`, and `.SLICE-001` on dispatch labels, not `agent`.
|
|
52
|
+
An omitted `agent` selects the default worker; `agent: "reviewer"` is generic. Hosts
|
|
53
|
+
spawn/wait/deliver. Task prose and display names do not select profiles.
|
|
54
|
+
Dispatch labels may add `devrites-` or `.lane`; the native profile stays
|
|
55
|
+
`devrites-<base-role>`. Verify the host selector before recording a start or
|
|
56
|
+
admitting a result; a ledger role and handle alone do not prove profile identity.
|
|
57
|
+
Root MUST NOT advance/claim completion before admitting required results;
|
|
50
58
|
running/orphaned/unavailable = `gap` — no root/generic substitute.
|
|
51
59
|
Where a feature workspace exists, every fresh dispatch to a named `devrites-*`
|
|
52
60
|
role — wave or solo — builds its read-set with `devrites-engine context --skills-root .omp/skills <slug>
|
|
@@ -128,9 +128,9 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
128
128
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
129
129
|
else proof → `green|red|gap`.
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
Group recheck findings by owning role and changed surface. Each exact
|
|
132
|
+
specialist rechecks its own fingerprints and the correction diff; code, test,
|
|
133
|
+
and doubt remain separate dispatches, even on the same diff.
|
|
134
134
|
Reviewer fan-out does not shrink the writer batch. Reviewers queue at the
|
|
135
135
|
same cap as writers (10; see
|
|
136
136
|
[`parallel-dispatch.md`](skill://devrites-lib/reference/parallel-dispatch.md#scale-limits)).
|
|
@@ -45,8 +45,12 @@ Files own briefs; [`parallel-dispatch.md`](../parallel-dispatch.md) owns rosters
|
|
|
45
45
|
<!-- root-only -->
|
|
46
46
|
## Native invocation
|
|
47
47
|
|
|
48
|
-
Skills name exact fresh roles
|
|
49
|
-
|
|
48
|
+
Skills name exact fresh roles; hosts select matching native profiles and
|
|
49
|
+
spawn/wait/deliver. Task prose and display names do not select profiles.
|
|
50
|
+
Dispatch labels may add `devrites-` or `.lane`; the native profile stays
|
|
51
|
+
`devrites-<base-role>`. Verify the host selector before recording a start or
|
|
52
|
+
admitting a result; a ledger role and handle alone do not prove profile identity.
|
|
53
|
+
Root MUST NOT advance/claim completion before admitting required results;
|
|
50
54
|
running/orphaned/unavailable = `gap` — no root/generic substitute.
|
|
51
55
|
Where a feature workspace exists, every fresh dispatch to a named `devrites-*`
|
|
52
56
|
role — wave or solo — builds its read-set with `devrites-engine context --skills-root .pi/skills <slug>
|
|
@@ -128,9 +128,9 @@ Lease: `batch_id`, `created_at`, `base_sha`, `n`,
|
|
|
128
128
|
proof in the same window as that review. Gap or Critical/Important → `gap`;
|
|
129
129
|
else proof → `green|red|gap`.
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
Group recheck findings by owning role and changed surface. Each exact
|
|
132
|
+
specialist rechecks its own fingerprints and the correction diff; code, test,
|
|
133
|
+
and doubt remain separate dispatches, even on the same diff.
|
|
134
134
|
Reviewer fan-out does not shrink the writer batch. Reviewers queue at the
|
|
135
135
|
same cap as writers (10; see
|
|
136
136
|
[`parallel-dispatch.md`](../../devrites-lib/reference/parallel-dispatch.md#scale-limits)).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.8",
|
|
4
4
|
"description": "DevRites: a disciplined senior-engineer workflow pack for Claude Code, Codex, omp, pi, and Devin CLI",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://github.com/ViktorsBaikers/DevRites#readme",
|
|
@@ -100,7 +100,6 @@ render_codex_skill_tree() {
|
|
|
100
100
|
;;
|
|
101
101
|
esac
|
|
102
102
|
done < <(find "$_sd" -type f)
|
|
103
|
-
pin_context_skills_root "$_dest" .agents/skills
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
render_omp_skill_tree() {
|
|
@@ -114,18 +113,15 @@ render_omp_skill_tree() {
|
|
|
114
113
|
case "$_r" in
|
|
115
114
|
SKILL.md)
|
|
116
115
|
gen_omp_skill_file "$f" "$_out"
|
|
117
|
-
omp_rewrite_skill_refs "$_out" "$(basename "$_dest")/$_r"
|
|
118
116
|
;;
|
|
119
117
|
*.md)
|
|
120
118
|
gen_omp_markdown_file "$f" "$_out"
|
|
121
|
-
omp_rewrite_skill_refs "$_out" "$(basename "$_dest")/$_r"
|
|
122
119
|
;;
|
|
123
120
|
*)
|
|
124
121
|
cp "$f" "$_out"
|
|
125
122
|
;;
|
|
126
123
|
esac
|
|
127
124
|
done < <(find "$_sd" -type f)
|
|
128
|
-
pin_context_skills_root "$_dest" .omp/skills
|
|
129
125
|
}
|
|
130
126
|
|
|
131
127
|
render_pi_skill_tree() {
|
|
@@ -148,7 +144,6 @@ render_pi_skill_tree() {
|
|
|
148
144
|
;;
|
|
149
145
|
esac
|
|
150
146
|
done < <(find "$_sd" -type f)
|
|
151
|
-
pin_context_skills_root "$_dest" .pi/skills
|
|
152
147
|
}
|
|
153
148
|
|
|
154
149
|
render_devin_skill_tree() {
|
|
@@ -171,7 +166,6 @@ render_devin_skill_tree() {
|
|
|
171
166
|
;;
|
|
172
167
|
esac
|
|
173
168
|
done < <(find "$_sd" -type f)
|
|
174
|
-
pin_context_skills_root "$_dest" .devin/skills
|
|
175
169
|
}
|
|
176
170
|
|
|
177
171
|
mkdir -p "$OUT_ROOT/claude" "$OUT_ROOT/codex" "$OUT_ROOT/omp" "$OUT_ROOT/pi" "$OUT_ROOT/devin"
|
|
@@ -204,6 +198,7 @@ while IFS= read -r d; do
|
|
|
204
198
|
[ "$name" = ".impeccable" ] && continue
|
|
205
199
|
render_codex_skill_tree "$d" "$OUT_ROOT/codex/skills/$name"
|
|
206
200
|
done < <(find "$PACK_SRC/skills" -mindepth 1 -maxdepth 1 -type d)
|
|
201
|
+
pin_context_skills_root "$OUT_ROOT/codex/skills" .agents/skills
|
|
207
202
|
|
|
208
203
|
while IFS= read -r f; do
|
|
209
204
|
name="$(basename "$f" .md)"
|
|
@@ -225,6 +220,8 @@ while IFS= read -r d; do
|
|
|
225
220
|
;;
|
|
226
221
|
esac
|
|
227
222
|
done < <(find "$PACK_SRC/skills" -mindepth 1 -maxdepth 1 -type d)
|
|
223
|
+
omp_rewrite_skill_refs "$OUT_ROOT/omp/skills"
|
|
224
|
+
pin_context_skills_root "$OUT_ROOT/omp/skills" .omp/skills
|
|
228
225
|
|
|
229
226
|
while IFS= read -r f; do
|
|
230
227
|
name="$(basename "$f" .md)"
|
|
@@ -248,6 +245,7 @@ while IFS= read -r d; do
|
|
|
248
245
|
;;
|
|
249
246
|
esac
|
|
250
247
|
done < <(find "$PACK_SRC/skills" -mindepth 1 -maxdepth 1 -type d)
|
|
248
|
+
pin_context_skills_root "$OUT_ROOT/pi/skills" .pi/skills
|
|
251
249
|
|
|
252
250
|
while IFS= read -r f; do
|
|
253
251
|
name="$(basename "$f" .md)"
|
|
@@ -265,6 +263,7 @@ while IFS= read -r d; do
|
|
|
265
263
|
[ "$name" = ".impeccable" ] && continue
|
|
266
264
|
render_devin_skill_tree "$d" "$OUT_ROOT/devin/skills/$name"
|
|
267
265
|
done < <(find "$PACK_SRC/skills" -mindepth 1 -maxdepth 1 -type d)
|
|
266
|
+
pin_context_skills_root "$OUT_ROOT/devin/skills" .devin/skills
|
|
268
267
|
|
|
269
268
|
while IFS= read -r f; do
|
|
270
269
|
name="$(basename "$f" .md)"
|
package/scripts/omp-generate.sh
CHANGED
|
@@ -44,6 +44,7 @@ gen_omp_markdown_file() {
|
|
|
44
44
|
-e '/Rules in scope/{N;s#;\n[[:space:]]*`[^`]+` mirror on Codex##;}' \
|
|
45
45
|
-e '/On Codex, use the/{N;s# On Codex, use the mirrors under\n`[^`]+`\.##;s# On Codex, use the\nmirrors under `[^`]+`\.##;s# On Codex, use the\nmirror under `[^`]+`\.##;}' \
|
|
46
46
|
-e 's#Host mapping: Claude Code uses `Agent` \(`Task` is its legacy alias\); Codex uses#Host mapping: omp fresh-context dispatch uses the `task` tool (`tasks[]` batch); hosts use#g' \
|
|
47
|
+
-e 's#Skills name exact fresh roles; hosts select matching native profiles and#On omp, every `task` item (each `tasks[]` member) sets `agent` to the exact native profile;\nuse `agent: "devrites-code-reviewer"`, `agent: "devrites-test-analyst"`, or\n`agent: "devrites-slice-wright"` for those roles, including rechecks and repairs.\nKeep `.frontend`, `.backend`, and `.SLICE-001` on dispatch labels, not `agent`.\nAn omitted `agent` selects the default worker; `agent: "reviewer"` is generic. Hosts#' \
|
|
47
48
|
-e 's#`Agent` call#`task` call#g' \
|
|
48
49
|
-e 's#Claude: N concurrent Task wrights \(`acceptEdits`, cwd=worktree\)\.#omp: one `tasks[]` batch of N wrights; `task` has no `cwd` or `acceptEdits`, so each task names its absolute worktree path and the wright works only there.#g' \
|
|
49
50
|
-e 's#^Claude grants only the exact wright `acceptEdits`;#omp has no permission modes: only the wright'"'"'s `tools` allowlist has `edit`/`write`, and the root stays source-read-only by instruction. Claude grants only the exact wright `acceptEdits`;#' \
|
|
@@ -62,24 +63,33 @@ gen_omp_markdown_file() {
|
|
|
62
63
|
"$_src" > "$_out"
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
# Rewrite
|
|
66
|
-
# root (<skill>/<path>).
|
|
66
|
+
# Rewrite installed omp skill Markdown in place. $1 is the complete skills root.
|
|
67
67
|
# - omp loads skill files as skill://<skill>/<path> and cannot climb above a
|
|
68
68
|
# skill root, so each `../` link target or backtick path becomes an absolute
|
|
69
69
|
# skill:// URL.
|
|
70
70
|
omp_rewrite_skill_refs() {
|
|
71
|
-
python3 - "$1"
|
|
72
|
-
import posixpath, re, sys
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
python3 - "$1" <<'PY'
|
|
72
|
+
import os, posixpath, re, sys
|
|
73
|
+
root = sys.argv[1]
|
|
74
|
+
def fail(error):
|
|
75
|
+
raise error
|
|
76
|
+
for directory, _, names in os.walk(root, onerror=fail):
|
|
77
|
+
for name in names:
|
|
78
|
+
if not name.endswith(".md"):
|
|
79
|
+
continue
|
|
80
|
+
path = os.path.join(directory, name)
|
|
81
|
+
if os.path.islink(path):
|
|
82
|
+
continue
|
|
83
|
+
rel = os.path.relpath(path, root).replace(os.sep, "/")
|
|
84
|
+
here = posixpath.dirname(rel)
|
|
85
|
+
def fix(m):
|
|
86
|
+
target = posixpath.normpath(posixpath.join(here, m.group(2)))
|
|
87
|
+
return m.group(0) if target.startswith("..") else m.group(1) + "skill://" + target
|
|
88
|
+
with open(path, encoding="utf-8") as fh:
|
|
89
|
+
text = fh.read()
|
|
90
|
+
text = re.sub(r"(\]\(|`)((?:\.\./)+[A-Za-z0-9._/-]+)", fix, text)
|
|
91
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
92
|
+
fh.write(text)
|
|
83
93
|
PY
|
|
84
94
|
}
|
|
85
95
|
|