devrites 7.3.1 → 7.3.3
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 +38 -0
- package/README.md +19 -3
- package/bin/devrites.mjs +31 -5
- package/docs/cli.md +15 -7
- package/docs/engine/commands.md +37 -5
- package/docs/engine/workspace-schema.md +4 -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 +50 -21
- package/engine/internal/hostpack/hostpack.go +1 -1
- 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/apply_bundle.go +25 -8
- 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/regression.go +0 -5
- package/engine/internal/lib/resolve.go +148 -145
- 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/rootfacts/facts.go +11 -0
- package/engine/internal/state/acceptance.go +35 -5
- package/engine/internal/state/feature.go +3 -2
- package/engine/internal/state/lock_other.go +4 -0
- package/engine/internal/state/observation.go +1 -1
- package/engine/internal/state/write.go +56 -39
- package/engine/internal/workflow/commands.go +25 -3
- package/engine/main.go +21 -2
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- 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-build/reference/wright-dispatch.md +17 -17
- package/pack/.claude/skills/rite-clarify/SKILL.md +25 -25
- package/pack/.claude/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/README.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- 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-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/claude/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/claude/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- 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-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/codex/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/codex/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- 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-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/devin/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/devin/skills/rite-doctor/SKILL.md +70 -68
- 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/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-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/dsh/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/dsh/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +1 -1
- 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-build/reference/wright-dispatch.md +18 -18
- package/pack/generated/omp/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/omp/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/pi/AGENTS.md +1 -1
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- 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-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/pi/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/pi/skills/rite-doctor/SKILL.md +70 -68
- package/package.json +1 -1
- package/scripts/build-host-artifacts.sh +1 -1
- package/scripts/dsh-generate.py +6 -2
- package/scripts/install-lib.sh +29 -0
- package/scripts/omp-generate.sh +4 -4
- package/scripts/pi-generate.sh +3 -3
- 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"
|
|
@@ -10,7 +11,7 @@ import (
|
|
|
10
11
|
"strings"
|
|
11
12
|
"time"
|
|
12
13
|
|
|
13
|
-
"github.com/devrites/devrites/internal/
|
|
14
|
+
"github.com/devrites/devrites/internal/devritespaths"
|
|
14
15
|
"github.com/devrites/devrites/internal/markdowntext"
|
|
15
16
|
"github.com/devrites/devrites/internal/state"
|
|
16
17
|
"github.com/devrites/devrites/internal/workflow"
|
|
@@ -24,11 +25,24 @@ import (
|
|
|
24
25
|
// 0 resolved 2 no active workspace 3 qid not found
|
|
25
26
|
// 4 qid not open 5 bad arguments
|
|
26
27
|
func Resolve(root string, args []string, stdout, stderr io.Writer) int {
|
|
28
|
+
return resolveWithLock(root, args, stdout, stderr, state.WithFeatureLockIn)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func resolveWithLock(root string, args []string, stdout, stderr io.Writer, withLock func(*os.Root, string, func() error) error) int {
|
|
27
32
|
slug := activeSlug(root)
|
|
28
33
|
if slug == "" {
|
|
29
34
|
return fail(stderr, "No active workspace. Run "+workflow.ForVerb("spec").Both()+" <feature> first.", 2)
|
|
30
35
|
}
|
|
31
|
-
work :=
|
|
36
|
+
work, err := devritespaths.ExistingFeatureDirChecked(root, slug)
|
|
37
|
+
if err != nil {
|
|
38
|
+
return fail(stderr, err.Error(), 2)
|
|
39
|
+
}
|
|
40
|
+
// Pin the selected root before waiting; workspace paths may change meanwhile.
|
|
41
|
+
selectedRoot, err := os.OpenRoot(root)
|
|
42
|
+
if err != nil {
|
|
43
|
+
return fail(stderr, err.Error(), 2)
|
|
44
|
+
}
|
|
45
|
+
defer func() { _ = selectedRoot.Close() }()
|
|
32
46
|
qfile := filepath.Join(work, "questions.md")
|
|
33
47
|
sfile := filepath.Join(work, "state.md")
|
|
34
48
|
if !isFile(qfile) {
|
|
@@ -67,7 +81,7 @@ func Resolve(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
67
81
|
mode = "answer"
|
|
68
82
|
qid = first
|
|
69
83
|
payload = argAt(args, 1)
|
|
70
|
-
if payload == "" {
|
|
84
|
+
if strings.TrimSpace(payload) == "" {
|
|
71
85
|
return fail(stderr, "Answer text required for "+qid, 5)
|
|
72
86
|
}
|
|
73
87
|
}
|
|
@@ -78,8 +92,13 @@ func Resolve(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
code := 0
|
|
81
|
-
if err :=
|
|
82
|
-
|
|
95
|
+
if err := withLock(selectedRoot, slug, func() error {
|
|
96
|
+
ws, err := selectedRoot.OpenRoot(filepath.Join("work", slug))
|
|
97
|
+
if err != nil {
|
|
98
|
+
return err
|
|
99
|
+
}
|
|
100
|
+
defer func() { _ = ws.Close() }()
|
|
101
|
+
code = resolveMutation(ws, slug, mode, qid, payload, human, stdout, stderr, writeBundleTargets)
|
|
83
102
|
return nil
|
|
84
103
|
}); err != nil {
|
|
85
104
|
return fail(stderr, err.Error(), 1)
|
|
@@ -87,38 +106,20 @@ func Resolve(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
87
106
|
return code
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
func resolveMutation(
|
|
109
|
+
func resolveMutation(ws *os.Root, slug, mode, qid, payload string, human bool, stdout, stderr io.Writer, publish func(*os.Root, []*bundleTarget) error) int {
|
|
110
|
+
type resolution struct{ qid, status, text string }
|
|
111
|
+
var entries []resolution
|
|
91
112
|
switch mode {
|
|
92
113
|
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")
|
|
114
|
+
entries = append(entries, resolution{qid, "answered", payload})
|
|
102
115
|
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")
|
|
116
|
+
entries = append(entries, resolution{qid, "dropped", payload})
|
|
112
117
|
case "batch":
|
|
113
118
|
// #nosec G304 -- operator-passed --batch file argument
|
|
114
119
|
data, err := os.ReadFile(payload)
|
|
115
120
|
if err != nil {
|
|
116
121
|
return fail(stderr, "Batch file not found: "+payload, 5)
|
|
117
122
|
}
|
|
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
123
|
for _, line := range batchLines(data) {
|
|
123
124
|
line = strings.TrimSpace(line)
|
|
124
125
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
@@ -133,27 +134,55 @@ func resolveMutation(mode, qid, payload, qfile, sfile string, human bool, stdout
|
|
|
133
134
|
if !ok || bid == "" || strings.HasPrefix(bid, "-") {
|
|
134
135
|
return fail(stderr, "Malformed batch line (want `<qid>: <answer>` or `--drop <qid>: <reason>`): "+line, 5)
|
|
135
136
|
}
|
|
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
|
|
137
|
+
if status == "answered" && strings.TrimSpace(text) == "" {
|
|
138
|
+
return fail(stderr, "Answer text required for "+bid, 5)
|
|
148
139
|
}
|
|
140
|
+
entries = append(entries, resolution{bid, status, strings.TrimPrefix(text, " ")})
|
|
149
141
|
}
|
|
150
|
-
|
|
142
|
+
}
|
|
143
|
+
if len(entries) == 0 {
|
|
144
|
+
return 0
|
|
145
|
+
}
|
|
146
|
+
qdata, err := ws.ReadFile("questions.md")
|
|
147
|
+
if err != nil {
|
|
148
|
+
return fail(stderr, fmt.Sprintf("read questions.md: %v", err), 2)
|
|
149
|
+
}
|
|
150
|
+
sdata, err := ws.ReadFile("state.md")
|
|
151
|
+
if err != nil {
|
|
152
|
+
return fail(stderr, fmt.Sprintf("read state.md: %v", err), 1)
|
|
153
|
+
}
|
|
154
|
+
lines := splitLinesNoTrailing(qdata)
|
|
155
|
+
nextState := sdata
|
|
156
|
+
// Prepare both artifacts for every entry before publishing either file.
|
|
157
|
+
for _, entry := range entries {
|
|
158
|
+
var code int
|
|
159
|
+
if lines, code = applyResolution(lines, entry.qid, entry.status, entry.text, human, stderr); code != 0 {
|
|
160
|
+
return code
|
|
161
|
+
}
|
|
162
|
+
if nextState, err = clearAwaiting(nextState, entry.qid, slug); err != nil {
|
|
151
163
|
return fail(stderr, err.Error(), 1)
|
|
152
164
|
}
|
|
165
|
+
}
|
|
166
|
+
targets := []*bundleTarget{
|
|
167
|
+
{rel: "questions.md", before: qdata, existed: true, after: joinRecords(lines)},
|
|
168
|
+
}
|
|
169
|
+
if !bytes.Equal(nextState, sdata) {
|
|
170
|
+
targets = append(targets, &bundleTarget{rel: "state.md", before: sdata, existed: true, after: nextState})
|
|
171
|
+
}
|
|
172
|
+
if err := publish(ws, targets); err != nil {
|
|
173
|
+
return fail(stderr, err.Error(), 1)
|
|
174
|
+
}
|
|
175
|
+
switch mode {
|
|
176
|
+
case "answer":
|
|
177
|
+
fmt.Fprintf(stdout, "Resolved: %s\n", qid)
|
|
178
|
+
fmt.Fprintf(stdout, "Status: answered\n")
|
|
179
|
+
fmt.Fprintf(stdout, "Workspace: questions.md + state.md updated.\n")
|
|
180
|
+
case "drop":
|
|
181
|
+
fmt.Fprintf(stdout, "Dropped: %s\n", qid)
|
|
182
|
+
fmt.Fprintf(stdout, "Reason: %s\n", payload)
|
|
183
|
+
fmt.Fprintf(stdout, "Workspace: questions.md + state.md updated.\n")
|
|
184
|
+
case "batch":
|
|
153
185
|
for _, entry := range entries {
|
|
154
|
-
if err := clearAwaiting(sfile, entry.qid); err != nil {
|
|
155
|
-
return fail(stderr, err.Error(), 1)
|
|
156
|
-
}
|
|
157
186
|
if entry.status == "dropped" {
|
|
158
187
|
fmt.Fprintf(stdout, "Dropped: %s: %s\n", entry.qid, entry.text)
|
|
159
188
|
} else {
|
|
@@ -214,29 +243,6 @@ func clockNow() time.Time {
|
|
|
214
243
|
// nowUTC returns the current time as an ISO-8601 UTC timestamp.
|
|
215
244
|
func nowUTC() string { return clockNow().UTC().Format("2006-01-02T15:04:05Z") }
|
|
216
245
|
|
|
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
246
|
// applyResolution rewrites the <qid> block in the questions.md lines in memory.
|
|
241
247
|
// Returns 0, or 3 (qid not found) / 4 (qid not open), writing the message on
|
|
242
248
|
// failure.
|
|
@@ -350,100 +356,97 @@ var (
|
|
|
350
356
|
logHdrRe = regexp.MustCompile(`^## Log`)
|
|
351
357
|
)
|
|
352
358
|
|
|
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
|
-
}
|
|
359
|
+
// clearAwaiting removes only matching wait representations. Another question
|
|
360
|
+
// keeps the workspace paused; otherwise the canonical phase resumes on SOURCE.
|
|
361
|
+
func clearAwaiting(data []byte, qid, slug string) ([]byte, error) {
|
|
363
362
|
lines := splitLinesNoTrailing(data)
|
|
364
363
|
visible, err := structuralLines(data)
|
|
365
364
|
if err != nil {
|
|
366
|
-
return fmt.Errorf("read state
|
|
365
|
+
return nil, fmt.Errorf("read state: %w", err)
|
|
367
366
|
}
|
|
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
367
|
var out []string
|
|
406
|
-
|
|
407
|
-
|
|
368
|
+
matched, otherWait := false, false
|
|
369
|
+
inLog, logAt := false, -1
|
|
370
|
+
for i := 0; i < len(lines); i++ {
|
|
408
371
|
line := visible[i]
|
|
409
|
-
if
|
|
410
|
-
|
|
411
|
-
|
|
372
|
+
if inLog && hdrSpaceRe.MatchString(line) {
|
|
373
|
+
if logAt < 0 {
|
|
374
|
+
logAt = len(out)
|
|
375
|
+
}
|
|
376
|
+
inLog = false
|
|
412
377
|
}
|
|
413
|
-
if
|
|
414
|
-
|
|
378
|
+
if logHdrRe.MatchString(line) {
|
|
379
|
+
inLog = true
|
|
415
380
|
}
|
|
416
|
-
if
|
|
381
|
+
if awaitingRe.MatchString(line) {
|
|
382
|
+
end := i + 1
|
|
383
|
+
for end < len(lines) && !hdrSpaceRe.MatchString(visible[end]) {
|
|
384
|
+
end++
|
|
385
|
+
}
|
|
386
|
+
blockMatches, blockConflicts := false, false
|
|
387
|
+
for j := i + 1; j < end; j++ {
|
|
388
|
+
if waitingOn, ok := state.CursorField(visible[j:j+1], state.CursorQuestionID); ok && waitingOn != "" {
|
|
389
|
+
if waitingOn == qid {
|
|
390
|
+
blockMatches = true
|
|
391
|
+
} else {
|
|
392
|
+
blockConflicts = true
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if blockMatches && blockConflicts {
|
|
397
|
+
return nil, fmt.Errorf("state has conflicting question IDs in Awaiting human")
|
|
398
|
+
}
|
|
399
|
+
if blockMatches {
|
|
400
|
+
matched = true
|
|
401
|
+
} else {
|
|
402
|
+
otherWait = true
|
|
403
|
+
out = append(out, lines[i:end]...)
|
|
404
|
+
}
|
|
405
|
+
i = end - 1
|
|
417
406
|
continue
|
|
418
407
|
}
|
|
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
|
|
408
|
+
if waitingOn, ok := state.CursorField(visible[i:i+1], state.CursorQuestionID); ok && waitingOn != "" {
|
|
409
|
+
if waitingOn == qid {
|
|
410
|
+
matched = true
|
|
411
|
+
continue
|
|
428
412
|
}
|
|
429
|
-
|
|
430
|
-
out = append(out, original)
|
|
431
|
-
continue
|
|
413
|
+
otherWait = true
|
|
432
414
|
}
|
|
433
|
-
out = append(out,
|
|
415
|
+
out = append(out, lines[i])
|
|
416
|
+
}
|
|
417
|
+
if !matched {
|
|
418
|
+
return data, nil
|
|
419
|
+
}
|
|
420
|
+
rawPhase, _ := state.CursorField(lines, state.CursorPhase)
|
|
421
|
+
fields := strings.Fields(strings.ToLower(rawPhase))
|
|
422
|
+
var resumePolicy state.PhasePolicy
|
|
423
|
+
known := false
|
|
424
|
+
if len(fields) > 0 {
|
|
425
|
+
resumePolicy, known = state.PolicyFor(state.Phase(fields[0]))
|
|
434
426
|
}
|
|
435
|
-
if
|
|
436
|
-
|
|
427
|
+
if !known || resumePolicy.ResumeVerb == "" {
|
|
428
|
+
return nil, fmt.Errorf("cannot resume unknown or terminal phase %q", rawPhase)
|
|
437
429
|
}
|
|
438
|
-
if
|
|
439
|
-
|
|
430
|
+
if inLog && logAt < 0 {
|
|
431
|
+
logAt = len(out)
|
|
440
432
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
433
|
+
if logAt >= 0 {
|
|
434
|
+
out = append(out, "")
|
|
435
|
+
copy(out[logAt+1:], out[logAt:])
|
|
436
|
+
out[logAt] = fmt.Sprintf("- %s %s: resolved %s", nowUTC(), resumePolicy.Target, qid)
|
|
437
|
+
}
|
|
438
|
+
if !otherWait {
|
|
439
|
+
action := workflow.ForVerb(resumePolicy.ResumeVerb).SourceInvocation(slug)
|
|
440
|
+
out = state.UpsertCursorField(out, state.CursorStatus, "running")
|
|
441
|
+
out = state.UpsertCursorField(out, state.CursorNextAction, action)
|
|
442
|
+
if got, _ := state.CursorField(out, state.CursorStatus); got != "running" {
|
|
443
|
+
return nil, fmt.Errorf("cannot update state status")
|
|
444
|
+
}
|
|
445
|
+
if got, _ := state.CursorField(out, state.CursorNextAction); got != action {
|
|
446
|
+
return nil, fmt.Errorf("cannot update state next_action")
|
|
447
|
+
}
|
|
445
448
|
}
|
|
446
|
-
return nil
|
|
449
|
+
return joinRecords(out), nil
|
|
447
450
|
}
|
|
448
451
|
|
|
449
452
|
// 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 {
|