devrites 7.3.0 → 7.3.2
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 +32 -0
- package/README.md +19 -3
- package/bin/devrites.mjs +31 -5
- package/docs/engine/commands.md +41 -3
- package/docs/release.md +5 -2
- package/engine/Makefile +2 -2
- package/engine/internal/acceptance/newfinal_origins.go +39 -0
- package/engine/internal/acceptance/preconditions.go +17 -11
- package/engine/internal/acceptance/transcript.go +23 -4
- package/engine/internal/gate/gate.go +8 -6
- package/engine/internal/gate/readiness_binding.go +54 -24
- package/engine/internal/install/apply.go +36 -21
- package/engine/internal/install/binary.go +67 -9
- package/engine/internal/install/install.go +63 -21
- package/engine/internal/install/manifest.go +4 -5
- package/engine/internal/install/merge.go +89 -33
- package/engine/internal/install/preflight.go +60 -19
- package/engine/internal/install/uninstall.go +19 -9
- package/engine/internal/install/update.go +0 -4
- package/engine/internal/lib/claims.go +170 -4
- package/engine/internal/lib/claims_holder.go +13 -16
- package/engine/internal/lib/closeout.go +52 -28
- package/engine/internal/lib/context_io.go +88 -4
- package/engine/internal/lib/continuation.go +111 -24
- package/engine/internal/lib/dispatch.go +157 -24
- package/engine/internal/lib/final_prerequisites.go +137 -0
- package/engine/internal/lib/manifest_union.go +62 -25
- package/engine/internal/lib/next.go +10 -10
- package/engine/internal/lib/packet.go +32 -8
- package/engine/internal/lib/packet_integrity.go +194 -2
- package/engine/internal/lib/resolve.go +128 -142
- package/engine/internal/lib/treebaseline.go +0 -1
- package/engine/internal/markdowntext/markdowntext.go +110 -31
- package/engine/internal/overhaul/records/check.go +45 -5
- package/engine/internal/overhaul/records/consistency.go +34 -8
- package/engine/internal/overhaul/records/run.go +55 -11
- package/engine/internal/parallel/landing.go +17 -0
- package/engine/internal/state/acceptance.go +35 -5
- package/engine/internal/state/feature.go +3 -2
- package/engine/internal/state/observation.go +50 -3
- package/engine/internal/state/write.go +20 -44
- package/engine/internal/workflow/commands.go +25 -3
- package/engine/main.go +19 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/.claude/skills/overhaul/SKILL.md +8 -4
- package/pack/.claude/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/.claude/skills/overhaul/references/orchestration.md +32 -21
- package/pack/.claude/skills/overhaul/references/records.md +23 -8
- package/pack/.claude/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/.claude/skills/overhaul/references/scoring.md +7 -0
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/.claude/skills/rite-clarify/SKILL.md +25 -25
- package/pack/.claude/skills/rite-prove/SKILL.md +15 -7
- package/pack/.claude/skills/rite-review/SKILL.md +7 -0
- package/pack/.claude/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/claude/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/claude/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/claude/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/claude/skills/overhaul/references/records.md +23 -8
- package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/claude/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/claude/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/claude/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/claude/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/claude/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/codex/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/codex/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/codex/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/codex/skills/overhaul/references/records.md +23 -8
- package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/codex/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/codex/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/codex/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/codex/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/codex/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/devin/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/devin/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/devin/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/devin/skills/overhaul/references/records.md +23 -8
- package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/devin/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/devin/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/devin/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/devin/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/devin/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/dsh/runtime/cordis.patch.yml +28 -14
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/dsh/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/dsh/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/dsh/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/dsh/skills/overhaul/references/records.md +23 -8
- package/pack/generated/dsh/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/dsh/skills/overhaul/references/scoring.md +6 -0
- package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/dsh/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/dsh/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/dsh/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/dsh/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/dsh/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/omp/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/omp/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/omp/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/omp/skills/overhaul/references/records.md +23 -8
- package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/omp/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/omp/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/omp/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/omp/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/omp/skills/rite-vet/SKILL.md +10 -1
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
- package/pack/generated/pi/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/pi/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/pi/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/pi/skills/overhaul/references/records.md +23 -8
- package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/pi/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +9 -10
- package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +8 -1
- package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/pi/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/pi/skills/rite-prove/SKILL.md +15 -7
- package/pack/generated/pi/skills/rite-review/SKILL.md +7 -0
- package/pack/generated/pi/skills/rite-vet/SKILL.md +10 -1
- package/package.json +1 -1
- package/scripts/dsh-generate.py +1 -1
- package/scripts/install-lib.sh +29 -0
- package/uninstall.sh +1 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package lib
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"bytes"
|
|
4
5
|
"fmt"
|
|
5
6
|
"io"
|
|
6
7
|
"os"
|
|
@@ -67,7 +68,7 @@ func Resolve(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
67
68
|
mode = "answer"
|
|
68
69
|
qid = first
|
|
69
70
|
payload = argAt(args, 1)
|
|
70
|
-
if payload == "" {
|
|
71
|
+
if strings.TrimSpace(payload) == "" {
|
|
71
72
|
return fail(stderr, "Answer text required for "+qid, 5)
|
|
72
73
|
}
|
|
73
74
|
}
|
|
@@ -79,7 +80,7 @@ func Resolve(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
79
80
|
|
|
80
81
|
code := 0
|
|
81
82
|
if err := state.WithFeatureLock(root, slug, func() error {
|
|
82
|
-
code = resolveMutation(mode, qid, payload, qfile, sfile, human, stdout, stderr)
|
|
83
|
+
code = resolveMutation(slug, mode, qid, payload, qfile, sfile, human, stdout, stderr)
|
|
83
84
|
return nil
|
|
84
85
|
}); err != nil {
|
|
85
86
|
return fail(stderr, err.Error(), 1)
|
|
@@ -87,38 +88,20 @@ func Resolve(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
87
88
|
return code
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
func resolveMutation(mode, qid, payload, qfile, sfile string, human bool, stdout, stderr io.Writer) int {
|
|
91
|
+
func resolveMutation(slug, mode, qid, payload, qfile, sfile string, human bool, stdout, stderr io.Writer) int {
|
|
92
|
+
type resolution struct{ qid, status, text string }
|
|
93
|
+
var entries []resolution
|
|
91
94
|
switch mode {
|
|
92
95
|
case "answer":
|
|
93
|
-
|
|
94
|
-
return code
|
|
95
|
-
}
|
|
96
|
-
if err := clearAwaiting(sfile, qid); err != nil {
|
|
97
|
-
return fail(stderr, err.Error(), 1)
|
|
98
|
-
}
|
|
99
|
-
fmt.Fprintf(stdout, "Resolved: %s\n", qid)
|
|
100
|
-
fmt.Fprintf(stdout, "Status: answered\n")
|
|
101
|
-
fmt.Fprintf(stdout, "Workspace: questions.md + state.md updated.\n")
|
|
96
|
+
entries = append(entries, resolution{qid, "answered", payload})
|
|
102
97
|
case "drop":
|
|
103
|
-
|
|
104
|
-
return code
|
|
105
|
-
}
|
|
106
|
-
if err := clearAwaiting(sfile, qid); err != nil {
|
|
107
|
-
return fail(stderr, err.Error(), 1)
|
|
108
|
-
}
|
|
109
|
-
fmt.Fprintf(stdout, "Dropped: %s\n", qid)
|
|
110
|
-
fmt.Fprintf(stdout, "Reason: %s\n", payload)
|
|
111
|
-
fmt.Fprintf(stdout, "Workspace: questions.md + state.md updated.\n")
|
|
98
|
+
entries = append(entries, resolution{qid, "dropped", payload})
|
|
112
99
|
case "batch":
|
|
113
100
|
// #nosec G304 -- operator-passed --batch file argument
|
|
114
101
|
data, err := os.ReadFile(payload)
|
|
115
102
|
if err != nil {
|
|
116
103
|
return fail(stderr, "Batch file not found: "+payload, 5)
|
|
117
104
|
}
|
|
118
|
-
// Every line is parsed and applied in memory first, so a bad line
|
|
119
|
-
// leaves questions.md untouched instead of half-resolved.
|
|
120
|
-
type batchEntry struct{ qid, status, text string }
|
|
121
|
-
var entries []batchEntry
|
|
122
105
|
for _, line := range batchLines(data) {
|
|
123
106
|
line = strings.TrimSpace(line)
|
|
124
107
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
@@ -133,27 +116,56 @@ func resolveMutation(mode, qid, payload, qfile, sfile string, human bool, stdout
|
|
|
133
116
|
if !ok || bid == "" || strings.HasPrefix(bid, "-") {
|
|
134
117
|
return fail(stderr, "Malformed batch line (want `<qid>: <answer>` or `--drop <qid>: <reason>`): "+line, 5)
|
|
135
118
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// #nosec G304 -- questions.md path inside the feature workspace
|
|
139
|
-
qdata, err := os.ReadFile(qfile)
|
|
140
|
-
if err != nil {
|
|
141
|
-
return fail(stderr, "questions.md missing at "+qfile, 2)
|
|
142
|
-
}
|
|
143
|
-
lines := splitLinesNoTrailing(qdata)
|
|
144
|
-
for _, entry := range entries {
|
|
145
|
-
var code int
|
|
146
|
-
if lines, code = applyResolution(lines, entry.qid, entry.status, entry.text, human, stderr); code != 0 {
|
|
147
|
-
return code
|
|
119
|
+
if status == "answered" && strings.TrimSpace(text) == "" {
|
|
120
|
+
return fail(stderr, "Answer text required for "+bid, 5)
|
|
148
121
|
}
|
|
122
|
+
entries = append(entries, resolution{bid, status, strings.TrimPrefix(text, " ")})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if len(entries) == 0 {
|
|
126
|
+
return 0
|
|
127
|
+
}
|
|
128
|
+
// #nosec G304 -- questions.md path inside the feature workspace
|
|
129
|
+
qdata, err := os.ReadFile(qfile)
|
|
130
|
+
if err != nil {
|
|
131
|
+
return fail(stderr, "questions.md missing at "+qfile, 2)
|
|
132
|
+
}
|
|
133
|
+
// #nosec G304 -- state.md path inside the feature workspace
|
|
134
|
+
sdata, err := os.ReadFile(sfile)
|
|
135
|
+
if err != nil {
|
|
136
|
+
return fail(stderr, fmt.Sprintf("read state %s: %v", sfile, err), 1)
|
|
137
|
+
}
|
|
138
|
+
lines := splitLinesNoTrailing(qdata)
|
|
139
|
+
nextState := sdata
|
|
140
|
+
// Prepare both artifacts for every entry before publishing either file.
|
|
141
|
+
for _, entry := range entries {
|
|
142
|
+
var code int
|
|
143
|
+
if lines, code = applyResolution(lines, entry.qid, entry.status, entry.text, human, stderr); code != 0 {
|
|
144
|
+
return code
|
|
149
145
|
}
|
|
150
|
-
if err
|
|
146
|
+
if nextState, err = clearAwaiting(nextState, entry.qid, slug); err != nil {
|
|
151
147
|
return fail(stderr, err.Error(), 1)
|
|
152
148
|
}
|
|
149
|
+
}
|
|
150
|
+
if err := fsutil.WriteFileAtomic(qfile, joinRecords(lines), 0o644); err != nil {
|
|
151
|
+
return fail(stderr, err.Error(), 1)
|
|
152
|
+
}
|
|
153
|
+
if !bytes.Equal(nextState, sdata) {
|
|
154
|
+
if err := fsutil.WriteFileAtomic(sfile, nextState, 0o644); err != nil {
|
|
155
|
+
return fail(stderr, fmt.Sprintf("update state %s: %v", sfile, err), 1)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
switch mode {
|
|
159
|
+
case "answer":
|
|
160
|
+
fmt.Fprintf(stdout, "Resolved: %s\n", qid)
|
|
161
|
+
fmt.Fprintf(stdout, "Status: answered\n")
|
|
162
|
+
fmt.Fprintf(stdout, "Workspace: questions.md + state.md updated.\n")
|
|
163
|
+
case "drop":
|
|
164
|
+
fmt.Fprintf(stdout, "Dropped: %s\n", qid)
|
|
165
|
+
fmt.Fprintf(stdout, "Reason: %s\n", payload)
|
|
166
|
+
fmt.Fprintf(stdout, "Workspace: questions.md + state.md updated.\n")
|
|
167
|
+
case "batch":
|
|
153
168
|
for _, entry := range entries {
|
|
154
|
-
if err := clearAwaiting(sfile, entry.qid); err != nil {
|
|
155
|
-
return fail(stderr, err.Error(), 1)
|
|
156
|
-
}
|
|
157
169
|
if entry.status == "dropped" {
|
|
158
170
|
fmt.Fprintf(stdout, "Dropped: %s: %s\n", entry.qid, entry.text)
|
|
159
171
|
} else {
|
|
@@ -214,29 +226,6 @@ func clockNow() time.Time {
|
|
|
214
226
|
// nowUTC returns the current time as an ISO-8601 UTC timestamp.
|
|
215
227
|
func nowUTC() string { return clockNow().UTC().Format("2006-01-02T15:04:05Z") }
|
|
216
228
|
|
|
217
|
-
// resolveQuestion rewrites the <qid> block in questions.md so it records the new
|
|
218
|
-
// status, answer, and answered-at time in one pass. With human set it also records
|
|
219
|
-
// answered_by: human; without it any answered_by line in the block, whatever its
|
|
220
|
-
// indentation or case, is removed, so
|
|
221
|
-
// an identity written before the resolve cannot survive it. Returns 0, or 3 (qid
|
|
222
|
-
// not found) / 4 (qid not open), writing the message on failure.
|
|
223
|
-
func resolveQuestion(qfile, qid, status, answer string, human bool, stderr io.Writer) int {
|
|
224
|
-
// #nosec G304 -- questions.md path inside the feature workspace
|
|
225
|
-
data, err := os.ReadFile(qfile)
|
|
226
|
-
if err != nil {
|
|
227
|
-
return fail(stderr, "questions.md missing at "+qfile, 2)
|
|
228
|
-
}
|
|
229
|
-
updated, code := applyResolution(splitLinesNoTrailing(data), qid, status, answer, human, stderr)
|
|
230
|
-
if code != 0 {
|
|
231
|
-
return code
|
|
232
|
-
}
|
|
233
|
-
if err := fsutil.WriteFileAtomic(qfile, joinRecords(updated), 0o644); err != nil {
|
|
234
|
-
return fail(stderr, err.Error(), 1)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return 0
|
|
238
|
-
}
|
|
239
|
-
|
|
240
229
|
// applyResolution rewrites the <qid> block in the questions.md lines in memory.
|
|
241
230
|
// Returns 0, or 3 (qid not found) / 4 (qid not open), writing the message on
|
|
242
231
|
// failure.
|
|
@@ -350,100 +339,97 @@ var (
|
|
|
350
339
|
logHdrRe = regexp.MustCompile(`^## Log`)
|
|
351
340
|
)
|
|
352
341
|
|
|
353
|
-
// clearAwaiting
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
// workspace is not waiting on this question.
|
|
357
|
-
func clearAwaiting(sfile, qid string) error {
|
|
358
|
-
// #nosec G304 -- state.md path inside the feature workspace
|
|
359
|
-
data, err := os.ReadFile(sfile)
|
|
360
|
-
if err != nil {
|
|
361
|
-
return fmt.Errorf("read state %s: %w", sfile, err)
|
|
362
|
-
}
|
|
342
|
+
// clearAwaiting removes only matching wait representations. Another question
|
|
343
|
+
// keeps the workspace paused; otherwise the canonical phase resumes on SOURCE.
|
|
344
|
+
func clearAwaiting(data []byte, qid, slug string) ([]byte, error) {
|
|
363
345
|
lines := splitLinesNoTrailing(data)
|
|
364
346
|
visible, err := structuralLines(data)
|
|
365
347
|
if err != nil {
|
|
366
|
-
return fmt.Errorf("read state
|
|
348
|
+
return nil, fmt.Errorf("read state: %w", err)
|
|
367
349
|
}
|
|
368
|
-
resumePolicy, _ := state.PolicyFor(state.PhaseBuild)
|
|
369
|
-
if rawPhase, ok := state.CursorField(lines, state.CursorPhase); ok {
|
|
370
|
-
if fields := strings.Fields(strings.ToLower(rawPhase)); len(fields) > 0 {
|
|
371
|
-
if policy, known := state.PolicyFor(state.Phase(fields[0])); known && policy.ResumeVerb != "" {
|
|
372
|
-
resumePolicy = policy
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
resumeCommand := workflow.ForVerb(resumePolicy.ResumeVerb)
|
|
377
|
-
// First check whether the awaiting block references this question at all.
|
|
378
|
-
inAw := false
|
|
379
|
-
var awaitingLines []string
|
|
380
|
-
for _, line := range visible {
|
|
381
|
-
switch {
|
|
382
|
-
case awaitingRe.MatchString(line):
|
|
383
|
-
inAw = true
|
|
384
|
-
continue
|
|
385
|
-
case inAw && hdrSpaceRe.MatchString(line):
|
|
386
|
-
inAw = false
|
|
387
|
-
}
|
|
388
|
-
if inAw {
|
|
389
|
-
awaitingLines = append(awaitingLines, line)
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
waitingOn, _ := state.CursorField(awaitingLines, state.CursorQuestionID)
|
|
393
|
-
cursorWaits := false
|
|
394
|
-
if waitingOn != qid {
|
|
395
|
-
// `state set question_id=<qid>` records the wait in the cursor table
|
|
396
|
-
// instead of an Awaiting human block.
|
|
397
|
-
if cursorQID, _ := state.CursorField(lines, state.CursorQuestionID); cursorQID != qid {
|
|
398
|
-
return nil
|
|
399
|
-
}
|
|
400
|
-
cursorWaits = true
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Rewrite: drop the awaiting block, reset Status/Next step, log the resolution.
|
|
404
|
-
ts := nowUTC()
|
|
405
350
|
var out []string
|
|
406
|
-
|
|
407
|
-
|
|
351
|
+
matched, otherWait := false, false
|
|
352
|
+
inLog, logAt := false, -1
|
|
353
|
+
for i := 0; i < len(lines); i++ {
|
|
408
354
|
line := visible[i]
|
|
409
|
-
if
|
|
410
|
-
|
|
411
|
-
|
|
355
|
+
if inLog && hdrSpaceRe.MatchString(line) {
|
|
356
|
+
if logAt < 0 {
|
|
357
|
+
logAt = len(out)
|
|
358
|
+
}
|
|
359
|
+
inLog = false
|
|
412
360
|
}
|
|
413
|
-
if
|
|
414
|
-
|
|
361
|
+
if logHdrRe.MatchString(line) {
|
|
362
|
+
inLog = true
|
|
415
363
|
}
|
|
416
|
-
if
|
|
364
|
+
if awaitingRe.MatchString(line) {
|
|
365
|
+
end := i + 1
|
|
366
|
+
for end < len(lines) && !hdrSpaceRe.MatchString(visible[end]) {
|
|
367
|
+
end++
|
|
368
|
+
}
|
|
369
|
+
blockMatches, blockConflicts := false, false
|
|
370
|
+
for j := i + 1; j < end; j++ {
|
|
371
|
+
if waitingOn, ok := state.CursorField(visible[j:j+1], state.CursorQuestionID); ok && waitingOn != "" {
|
|
372
|
+
if waitingOn == qid {
|
|
373
|
+
blockMatches = true
|
|
374
|
+
} else {
|
|
375
|
+
blockConflicts = true
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if blockMatches && blockConflicts {
|
|
380
|
+
return nil, fmt.Errorf("state has conflicting question IDs in Awaiting human")
|
|
381
|
+
}
|
|
382
|
+
if blockMatches {
|
|
383
|
+
matched = true
|
|
384
|
+
} else {
|
|
385
|
+
otherWait = true
|
|
386
|
+
out = append(out, lines[i:end]...)
|
|
387
|
+
}
|
|
388
|
+
i = end - 1
|
|
417
389
|
continue
|
|
418
390
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
continue
|
|
424
|
-
case inLog && hdrSpaceRe.MatchString(line):
|
|
425
|
-
if !logAppended {
|
|
426
|
-
out = append(out, fmt.Sprintf("- %s %s: resolved %s", ts, resumePolicy.Target, qid))
|
|
427
|
-
logAppended = true
|
|
391
|
+
if waitingOn, ok := state.CursorField(visible[i:i+1], state.CursorQuestionID); ok && waitingOn != "" {
|
|
392
|
+
if waitingOn == qid {
|
|
393
|
+
matched = true
|
|
394
|
+
continue
|
|
428
395
|
}
|
|
429
|
-
|
|
430
|
-
out = append(out, original)
|
|
431
|
-
continue
|
|
396
|
+
otherWait = true
|
|
432
397
|
}
|
|
433
|
-
out = append(out,
|
|
398
|
+
out = append(out, lines[i])
|
|
399
|
+
}
|
|
400
|
+
if !matched {
|
|
401
|
+
return data, nil
|
|
402
|
+
}
|
|
403
|
+
rawPhase, _ := state.CursorField(lines, state.CursorPhase)
|
|
404
|
+
fields := strings.Fields(strings.ToLower(rawPhase))
|
|
405
|
+
var resumePolicy state.PhasePolicy
|
|
406
|
+
known := false
|
|
407
|
+
if len(fields) > 0 {
|
|
408
|
+
resumePolicy, known = state.PolicyFor(state.Phase(fields[0]))
|
|
434
409
|
}
|
|
435
|
-
if
|
|
436
|
-
|
|
410
|
+
if !known || resumePolicy.ResumeVerb == "" {
|
|
411
|
+
return nil, fmt.Errorf("cannot resume unknown or terminal phase %q", rawPhase)
|
|
437
412
|
}
|
|
438
|
-
if
|
|
439
|
-
|
|
413
|
+
if inLog && logAt < 0 {
|
|
414
|
+
logAt = len(out)
|
|
440
415
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
416
|
+
if logAt >= 0 {
|
|
417
|
+
out = append(out, "")
|
|
418
|
+
copy(out[logAt+1:], out[logAt:])
|
|
419
|
+
out[logAt] = fmt.Sprintf("- %s %s: resolved %s", nowUTC(), resumePolicy.Target, qid)
|
|
420
|
+
}
|
|
421
|
+
if !otherWait {
|
|
422
|
+
action := workflow.ForVerb(resumePolicy.ResumeVerb).SourceInvocation(slug)
|
|
423
|
+
out = state.UpsertCursorField(out, state.CursorStatus, "running")
|
|
424
|
+
out = state.UpsertCursorField(out, state.CursorNextAction, action)
|
|
425
|
+
if got, _ := state.CursorField(out, state.CursorStatus); got != "running" {
|
|
426
|
+
return nil, fmt.Errorf("cannot update state status")
|
|
427
|
+
}
|
|
428
|
+
if got, _ := state.CursorField(out, state.CursorNextAction); got != action {
|
|
429
|
+
return nil, fmt.Errorf("cannot update state next_action")
|
|
430
|
+
}
|
|
445
431
|
}
|
|
446
|
-
return nil
|
|
432
|
+
return joinRecords(out), nil
|
|
447
433
|
}
|
|
448
434
|
|
|
449
435
|
// joinRecords joins lines with newlines and terminates the last one, so a rewritten
|
|
@@ -249,7 +249,6 @@ func CheckTreeBaseline(root, slug string) error {
|
|
|
249
249
|
return fmt.Errorf("tree baseline: %w", err)
|
|
250
250
|
}
|
|
251
251
|
if len(flagged) > 0 {
|
|
252
|
-
recordUnattributed(root, slug, cursorPhase(root, slug), flagged)
|
|
253
252
|
return errors.New(unattributedMessage(slug, flagged))
|
|
254
253
|
}
|
|
255
254
|
return nil
|
|
@@ -9,6 +9,16 @@ import (
|
|
|
9
9
|
// Structural returns Markdown with fenced blocks replaced by spaces. Byte
|
|
10
10
|
// offsets and line breaks stay aligned with the original input.
|
|
11
11
|
func Structural(input []byte) ([]byte, error) {
|
|
12
|
+
return structural(input, false)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Prose also masks HTML comment blocks, while Structural retains comments used
|
|
16
|
+
// by machine-readable markers. Both preserve byte offsets and line breaks.
|
|
17
|
+
func Prose(input []byte) ([]byte, error) {
|
|
18
|
+
return structural(input, true)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func structural(input []byte, comments bool) ([]byte, error) {
|
|
12
22
|
if !utf8.Valid(input) {
|
|
13
23
|
return nil, errors.New("markdown text contains malformed UTF-8")
|
|
14
24
|
}
|
|
@@ -21,6 +31,8 @@ func Structural(input []byte) ([]byte, error) {
|
|
|
21
31
|
fenceWidth := 0
|
|
22
32
|
anchor, anchorStart := -1, 0
|
|
23
33
|
afterBlank := true
|
|
34
|
+
inComment := false
|
|
35
|
+
lines:
|
|
24
36
|
for start := 0; start < len(input); {
|
|
25
37
|
lineEnd := bytes.IndexByte(input[start:], '\n')
|
|
26
38
|
if lineEnd < 0 {
|
|
@@ -35,43 +47,110 @@ func Structural(input []byte) ([]byte, error) {
|
|
|
35
47
|
line := input[start:contentEnd]
|
|
36
48
|
|
|
37
49
|
if marker == 0 {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
commentsPending := comments
|
|
51
|
+
// Recognize fences before their literal info strings can open comments.
|
|
52
|
+
for pass := 0; ; pass++ {
|
|
53
|
+
if commentsPending && (inComment || pass > 0) {
|
|
54
|
+
commentsPending = false
|
|
55
|
+
for pos := 0; pos < len(line); {
|
|
56
|
+
commentStart := pos
|
|
57
|
+
if !inComment {
|
|
58
|
+
if line[pos] == '`' {
|
|
59
|
+
end := pos + 1
|
|
60
|
+
for end < len(line) && line[end] == '`' {
|
|
61
|
+
end++
|
|
62
|
+
}
|
|
63
|
+
width := end - pos
|
|
64
|
+
for next := end; next < len(line); {
|
|
65
|
+
tick := bytes.IndexByte(line[next:], '`')
|
|
66
|
+
if tick < 0 {
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
next += tick
|
|
70
|
+
close := next + 1
|
|
71
|
+
for close < len(line) && line[close] == '`' {
|
|
72
|
+
close++
|
|
73
|
+
}
|
|
74
|
+
if close-next == width {
|
|
75
|
+
end = close
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
next = close
|
|
79
|
+
}
|
|
80
|
+
pos = end
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
if !bytes.HasPrefix(line[pos:], []byte("<!--")) {
|
|
84
|
+
pos++
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
pos += len("<!--")
|
|
88
|
+
inComment = true
|
|
89
|
+
}
|
|
90
|
+
end := bytes.Index(line[pos:], []byte("-->"))
|
|
91
|
+
if end < 0 {
|
|
92
|
+
mask(out[start+commentStart : contentEnd])
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
pos += end + len("-->")
|
|
96
|
+
mask(out[start+commentStart : start+pos])
|
|
97
|
+
inComment = false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if comments && !bytes.Equal(line, out[start:contentEnd]) && blank(out[start:contentEnd]) {
|
|
101
|
+
anchor, afterBlank = -1, false
|
|
102
|
+
if lineEnd == len(input) {
|
|
103
|
+
break lines
|
|
104
|
+
}
|
|
105
|
+
start = lineEnd + 1
|
|
106
|
+
continue lines
|
|
107
|
+
}
|
|
108
|
+
line = out[start:contentEnd]
|
|
109
|
+
if blank(line) {
|
|
110
|
+
afterBlank = true
|
|
111
|
+
} else if nextMarker, width, ok := openingFence(line, 3); ok {
|
|
112
|
+
marker, fenceWidth = nextMarker, width
|
|
113
|
+
mask(out[start:lineEnd])
|
|
114
|
+
anchor, afterBlank = -1, false
|
|
115
|
+
} else if nextMarker, width, ok := openingFence(line, anchor+3); ok && anchor >= 0 && indentColumns(line) >= anchor {
|
|
116
|
+
anchored := anchor
|
|
117
|
+
anchor, afterBlank = -1, false
|
|
118
|
+
if closeEnd := listFenceEnd(input, lineEnd, nextMarker, width, indentColumns(line), anchored); closeEnd >= 0 {
|
|
119
|
+
if !loneCR(input[anchorStart:closeEnd]) {
|
|
120
|
+
mask(out[start:closeEnd])
|
|
121
|
+
}
|
|
122
|
+
if closeEnd == len(input) {
|
|
123
|
+
break lines
|
|
124
|
+
}
|
|
125
|
+
start = closeEnd + 1
|
|
126
|
+
continue lines
|
|
127
|
+
}
|
|
128
|
+
if commentsPending {
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
} else if fence, closeEnd := markerLineFence(input, line, lineEnd, afterBlank || anchor >= 0); closeEnd >= 0 {
|
|
132
|
+
anchor, afterBlank = -1, false
|
|
133
|
+
if !loneCR(input[start:closeEnd]) {
|
|
134
|
+
mask(out[start+fence : closeEnd])
|
|
50
135
|
}
|
|
51
136
|
if closeEnd == len(input) {
|
|
52
|
-
break
|
|
137
|
+
break lines
|
|
53
138
|
}
|
|
54
139
|
start = closeEnd + 1
|
|
55
|
-
continue
|
|
56
|
-
}
|
|
57
|
-
} else if fence, closeEnd := markerLineFence(input, line, lineEnd, afterBlank || anchor >= 0); closeEnd >= 0 {
|
|
58
|
-
anchor, afterBlank = -1, false
|
|
59
|
-
if !loneCR(input[start:closeEnd]) {
|
|
60
|
-
mask(out[start+fence : closeEnd])
|
|
61
|
-
}
|
|
62
|
-
if closeEnd == len(input) {
|
|
63
|
-
break
|
|
64
|
-
}
|
|
65
|
-
start = closeEnd + 1
|
|
66
|
-
continue
|
|
67
|
-
} else {
|
|
68
|
-
column, ok := topLevelItem(line)
|
|
69
|
-
if ok && (afterBlank || anchor >= 0) {
|
|
70
|
-
anchor, anchorStart = column, start
|
|
140
|
+
continue lines
|
|
71
141
|
} else {
|
|
72
|
-
|
|
142
|
+
if commentsPending {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
column, ok := topLevelItem(line)
|
|
146
|
+
if ok && (afterBlank || anchor >= 0) {
|
|
147
|
+
anchor, anchorStart = column, start
|
|
148
|
+
} else {
|
|
149
|
+
anchor = -1
|
|
150
|
+
}
|
|
151
|
+
afterBlank = false
|
|
73
152
|
}
|
|
74
|
-
|
|
153
|
+
break
|
|
75
154
|
}
|
|
76
155
|
} else {
|
|
77
156
|
mask(out[start:lineEnd])
|
|
@@ -575,6 +575,9 @@ func Check(run, genDir, prevDir string) ([]string, error) {
|
|
|
575
575
|
}
|
|
576
576
|
}
|
|
577
577
|
}
|
|
578
|
+
if paths, ok := t["allowed_paths"].([]any); !ok || len(paths) == 0 {
|
|
579
|
+
add("task %s: allowed_paths must be a non-empty array of exact paths", ts)
|
|
580
|
+
}
|
|
578
581
|
for _, p := range ovio.List(t["allowed_paths"]) {
|
|
579
582
|
if why := BadPath(p, repo); why != "" {
|
|
580
583
|
add("task %s: allowed path %s %s", ts, Repr(p), why)
|
|
@@ -724,11 +727,30 @@ func Check(run, genDir, prevDir string) ([]string, error) {
|
|
|
724
727
|
for _, it := range att {
|
|
725
728
|
x := ovio.Obj(it)
|
|
726
729
|
xid, status := PyStr(x["task_id"])+"#"+PyStr(x["attempt_id"]), str(x, "status")
|
|
730
|
+
for _, k := range []string{"task_id", "attempt_id", "role"} {
|
|
731
|
+
if str(x, k) == "" {
|
|
732
|
+
add("attempt %s: %s must be a non-empty string", xid, k)
|
|
733
|
+
}
|
|
734
|
+
}
|
|
727
735
|
if !attemptState[status] {
|
|
728
736
|
add("attempt %s: bad status", xid)
|
|
729
737
|
}
|
|
730
|
-
if set("returned", "admitted", "rejected")[status] && !(str(x, "receipt") != "" && isFile(join(run, str(x, "receipt")))) {
|
|
731
|
-
add("attempt %s: receipt file missing", xid)
|
|
738
|
+
if set("returned", "admitted", "rejected")[status] && !(str(x, "receipt") != "" && inside(run, str(x, "receipt")) && isFile(join(run, str(x, "receipt")))) {
|
|
739
|
+
add("attempt %s: receipt file missing or outside run dir", xid)
|
|
740
|
+
}
|
|
741
|
+
if status == "admitted" && inside(run, str(x, "receipt")) && isFile(join(run, str(x, "receipt"))) {
|
|
742
|
+
rc, err := ovio.LoadObject(join(run, str(x, "receipt")))
|
|
743
|
+
if err != nil {
|
|
744
|
+
return nil, err
|
|
745
|
+
}
|
|
746
|
+
if !eq(rc["schema"], "overhaul.receipt/1") || str(rj, "run_id") == "" || !eq(rc["run_id"], rj["run_id"]) {
|
|
747
|
+
add("attempt %s: admitted receipt has wrong schema or run", xid)
|
|
748
|
+
}
|
|
749
|
+
for _, k := range []string{"task_id", "attempt_id", "role", "lane", "snapshot"} {
|
|
750
|
+
if str(x, k) == "" || !eq(rc[k], x[k]) {
|
|
751
|
+
add("attempt %s: admitted receipt %s differs from dispatch or is missing", xid, k)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
732
754
|
}
|
|
733
755
|
if !set("admitted", "rejected", "cancelled")[status] {
|
|
734
756
|
unsettled = append(unsettled, xid)
|
|
@@ -739,7 +761,7 @@ func Check(run, genDir, prevDir string) ([]string, error) {
|
|
|
739
761
|
if assessmentOnly {
|
|
740
762
|
add("attempt %s: writer dispatched in an assessment-only run", xid)
|
|
741
763
|
}
|
|
742
|
-
if
|
|
764
|
+
if live[status] && !approved.has(x["task_id"]) {
|
|
743
765
|
add("attempt %s: writer task is not in the active approval", xid)
|
|
744
766
|
}
|
|
745
767
|
if !granted[Repr(x["authorized_by"])].has(x["task_id"]) {
|
|
@@ -756,13 +778,31 @@ func Check(run, genDir, prevDir string) ([]string, error) {
|
|
|
756
778
|
add("attempt %s: admitted changes outside allowed paths %s", xid, Repr(extra.sorted()))
|
|
757
779
|
}
|
|
758
780
|
if live[status] {
|
|
759
|
-
|
|
781
|
+
paths, ok := tasks[Repr(x["task_id"])]["allowed_paths"].([]any)
|
|
782
|
+
if !ok || len(paths) == 0 {
|
|
783
|
+
add("attempt %s: live writer needs an authoritative task scope", xid)
|
|
784
|
+
}
|
|
785
|
+
// Reserve the approved task's whole scope, never a worker's narrower claim.
|
|
786
|
+
for _, p := range paths {
|
|
760
787
|
k := strings.ToLower(realpath(join(repo, ovio.Str(p))))
|
|
761
|
-
if prev, ok := liveWrites[k]; ok {
|
|
788
|
+
if prev, ok := liveWrites[k]; ok && prev != xid {
|
|
762
789
|
add("single writer: %s and %s both hold %s", prev, xid, PyStr(p))
|
|
763
790
|
}
|
|
764
791
|
liveWrites[k] = xid
|
|
765
792
|
}
|
|
793
|
+
if declared, exists := x["write_paths"]; exists {
|
|
794
|
+
declaredPaths, ok := declared.([]any)
|
|
795
|
+
if !ok {
|
|
796
|
+
add("attempt %s: write_paths must be an array of exact paths", xid)
|
|
797
|
+
}
|
|
798
|
+
for _, p := range declaredPaths {
|
|
799
|
+
if why := BadPath(p, repo); why != "" {
|
|
800
|
+
add("attempt %s: write path %s %s", xid, Repr(p), why)
|
|
801
|
+
} else if liveWrites[strings.ToLower(realpath(join(repo, ovio.Str(p))))] != xid {
|
|
802
|
+
add("attempt %s: write path %s is outside the authoritative task scope", xid, Repr(p))
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
766
806
|
}
|
|
767
807
|
}
|
|
768
808
|
if outcome != "RUNNING" && len(unsettled) > 0 {
|