forge-orkes 0.68.1 → 0.70.0
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/package.json
CHANGED
|
@@ -571,7 +571,15 @@ for phase in $PHASES; do
|
|
|
571
571
|
|
|
572
572
|
# (4) REQUIRE the on-disk phase-report.yml. Absent = crash → halt now (do NOT
|
|
573
573
|
# spend the retry on a crash; the resume path re-runs it from files).
|
|
574
|
+
# This is a slice END, so it MUST ping like the other two ends (checks-failed
|
|
575
|
+
# halt below, done) — a silent phone is never success. Live gap: canvaz
|
|
576
|
+
# 2026-07-23, m-EVT01 hit error_max_turns and the slice died dark. Key is
|
|
577
|
+
# per-phase ("$phase-crash"), NOT the shared "slice" key: a crash marker on
|
|
578
|
+
# the shared key would suppress a LATER legitimate checks-failed halt after
|
|
579
|
+
# resume; a re-crash of the same phase stays deduped (append-once semantics).
|
|
574
580
|
if [ ! -f "$report" ]; then
|
|
581
|
+
slice_notify halt "$phase-crash" \
|
|
582
|
+
"slice HALTED at phase $phase ($idx/$total) — launcher crash, no phase-report.yml written (attempt $attempt); see $wdir/result.json + $wdir/launch.err"
|
|
575
583
|
printf '[runner] phase %s: no phase-report.yml written at %s — halting\n' "$phase" "$report" >&2
|
|
576
584
|
printf 'SLICE_RESULT phases=%s touches=%s last_ping=%s\n' "$COMPLETED" "$TOUCHES" "$LAST_PING"
|
|
577
585
|
exit 1
|
|
@@ -139,6 +139,8 @@ If a sibling milestone (e.g. m50) has state + requirements but is missing from `
|
|
|
139
139
|
|
|
140
140
|
**Promoted-from-backlog milestone (`milestone.origin` set).** If `state/milestone-{N}.yml` carries `milestone.origin: {R-id}` (set by `forge` Step 1.2 when promoting a Standard-tier refactor item), no extra bookkeeping is needed here — the originating backlog item is flipped to `done` automatically when the milestone completes (see `reviewing` → **Promoted Milestone Completion Hook**). Just plan it as a normal milestone.
|
|
141
141
|
|
|
142
|
+
**Roadmap integrity check (advisory, m-39).** After writing or updating `roadmap.yml` in any of the three cases above, run `.forge/checks/forge-roadmap-check.sh`. If it exits non-zero, surface every `finding=...` line to the operator as one advisory note (e.g. *"roadmap-check found {N} issue(s): {lines}"*). This NEVER blocks — Step 9 Present and the handoff proceed regardless of findings; it is a signal, not a gate (same advisory posture as `verification.commands` in FORGE.md → Verification Gates). Script absent (pre-m-39 install) → skip silently.
|
|
143
|
+
|
|
142
144
|
## Step 6: Decompose Tasks
|
|
143
145
|
|
|
144
146
|
### Step 6.1: Cross-Layer Contract Detection
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# forge-roadmap-check.sh — structural integrity check for .forge/roadmap.yml (m-39).
|
|
3
|
+
#
|
|
4
|
+
# Validates the roadmap against the drift class that caused the m-34/m-36 phase-id
|
|
5
|
+
# collision: two independent renumbering passes both picked phase id 63. A flat
|
|
6
|
+
# grep over "id: N" cannot tell a milestone's id from a phase's id from an
|
|
7
|
+
# unrelated numeral (estimated_hours: 63), and cannot catch a field that drifted
|
|
8
|
+
# to the wrong indentation depth. This check instead walks the file as a small
|
|
9
|
+
# indentation-keyed state machine over roadmap.yml's three fixed second-level
|
|
10
|
+
# blocks (milestones:/phases:/waves:) — HONEST LIMIT: it understands exactly this
|
|
11
|
+
# one fixed shape, not arbitrary YAML.
|
|
12
|
+
#
|
|
13
|
+
# forge-roadmap-check.sh [<roadmap-file>] # default: <git-root>/.forge/roadmap.yml
|
|
14
|
+
#
|
|
15
|
+
# stdout (machine-parseable, fold style):
|
|
16
|
+
# ok=clean # no findings
|
|
17
|
+
# finding=<rule>:<detail> # one line per problem
|
|
18
|
+
# rule ∈ missing-field | duplicate-phase-id | dangling-phase-ref | orphan-phase
|
|
19
|
+
# | bad-milestone-dir | dangling-wave-ref
|
|
20
|
+
# exit: 0 = ok=clean; 1 = one or more findings; 2 = bad invocation (missing/unreadable file)
|
|
21
|
+
#
|
|
22
|
+
# Required fields (confirmed against the live file — zero false positives today):
|
|
23
|
+
# milestone entry: id, name, phases (non-empty)
|
|
24
|
+
# phase entry: id, name, goal, requirements, dependencies, success_criteria,
|
|
25
|
+
# estimated_hours, status
|
|
26
|
+
# milestone_dir is DELIBERATELY not required — 17/65 live phase entries predate
|
|
27
|
+
# the nested-layout convention. Validated only when present (bad-milestone-dir).
|
|
28
|
+
#
|
|
29
|
+
# Pure POSIX sh + awk. No git-repo dependency (reads the file directly, unlike the
|
|
30
|
+
# diff-scoped forge-hold-check.sh/forge-check-lint.sh). Collects every finding in
|
|
31
|
+
# one pass before deciding exit code — a single run surfaces everything wrong.
|
|
32
|
+
set -u
|
|
33
|
+
|
|
34
|
+
FILE="${1:-}"
|
|
35
|
+
if [ -z "$FILE" ]; then
|
|
36
|
+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
37
|
+
FILE="$ROOT/.forge/roadmap.yml"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ ! -f "$FILE" ] || [ ! -r "$FILE" ]; then
|
|
41
|
+
echo "roadmap-check: $FILE: not found or unreadable" >&2
|
|
42
|
+
exit 2
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
awk '
|
|
46
|
+
function trim(s) {
|
|
47
|
+
gsub(/^[ \t]+|[ \t]+$/, "", s)
|
|
48
|
+
return s
|
|
49
|
+
}
|
|
50
|
+
function unquote(s) {
|
|
51
|
+
gsub(/"/, "", s)
|
|
52
|
+
return s
|
|
53
|
+
}
|
|
54
|
+
function bare_num(s, n) {
|
|
55
|
+
n = unquote(trim(s))
|
|
56
|
+
if (n ~ /^m-/) n = substr(n, 3)
|
|
57
|
+
return n
|
|
58
|
+
}
|
|
59
|
+
function finish_entry() {
|
|
60
|
+
if (!entry_open) return
|
|
61
|
+
if (entry_type == "milestone") {
|
|
62
|
+
ms_count++
|
|
63
|
+
ms_id[ms_count] = cur_id
|
|
64
|
+
ms_fields[ms_count] = cur_fields
|
|
65
|
+
ms_phaselist[ms_count] = cur_phaselist
|
|
66
|
+
} else if (entry_type == "phase") {
|
|
67
|
+
ph_count++
|
|
68
|
+
ph_id[ph_count] = cur_id + 0
|
|
69
|
+
ph_fields[ph_count] = cur_fields
|
|
70
|
+
ph_mdir[ph_count] = cur_mdir
|
|
71
|
+
}
|
|
72
|
+
entry_open = 0
|
|
73
|
+
cur_id = ""; cur_fields = ""; cur_mdir = ""; cur_phaselist = ""
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# 2-space block header
|
|
77
|
+
$0 ~ /^ (milestones|phases|waves):[ \t]*$/ {
|
|
78
|
+
finish_entry()
|
|
79
|
+
block = trim($0)
|
|
80
|
+
gsub(/:$/, "", block)
|
|
81
|
+
next
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# waves block: " N: [a, b, c]"
|
|
85
|
+
block == "waves" && $0 ~ /^ [0-9]+:/ {
|
|
86
|
+
line = $0
|
|
87
|
+
sub(/^ /, "", line)
|
|
88
|
+
split(line, kv, ":")
|
|
89
|
+
wnum = trim(kv[1])
|
|
90
|
+
rest = line
|
|
91
|
+
sub(/^[^:]*:/, "", rest)
|
|
92
|
+
if (match(rest, /\[[^]]*\]/)) {
|
|
93
|
+
ids = substr(rest, RSTART + 1, RLENGTH - 2)
|
|
94
|
+
} else {
|
|
95
|
+
ids = ""
|
|
96
|
+
}
|
|
97
|
+
wave_count++
|
|
98
|
+
wave_num[wave_count] = wnum
|
|
99
|
+
wave_ids[wave_count] = ids
|
|
100
|
+
next
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# entry open: " - id: 36" / " - id: \"m-33\""
|
|
104
|
+
(block == "milestones" || block == "phases") && $0 ~ /^ - id:/ {
|
|
105
|
+
finish_entry()
|
|
106
|
+
entry_open = 1
|
|
107
|
+
entry_type = (block == "milestones" ? "milestone" : "phase")
|
|
108
|
+
val = $0
|
|
109
|
+
sub(/^ - id:[ \t]*/, "", val)
|
|
110
|
+
cur_id = unquote(trim(val))
|
|
111
|
+
cur_fields = " id "
|
|
112
|
+
next
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# 6-space field line inside an open entry
|
|
116
|
+
(block == "milestones" || block == "phases") && entry_open && $0 ~ /^ [A-Za-z_]+:/ {
|
|
117
|
+
line = $0
|
|
118
|
+
sub(/^ /, "", line)
|
|
119
|
+
match(line, /^[A-Za-z_]+/)
|
|
120
|
+
field = substr(line, RSTART, RLENGTH)
|
|
121
|
+
cur_fields = cur_fields field " "
|
|
122
|
+
if (field == "milestone_dir") {
|
|
123
|
+
val = line
|
|
124
|
+
sub(/^milestone_dir:[ \t]*/, "", val)
|
|
125
|
+
cur_mdir = unquote(trim(val))
|
|
126
|
+
}
|
|
127
|
+
if (field == "phases" && entry_type == "milestone") {
|
|
128
|
+
rest = line
|
|
129
|
+
if (match(rest, /\[[^]]*\]/)) {
|
|
130
|
+
cur_phaselist = substr(rest, RSTART + 1, RLENGTH - 2)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
next
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# any other 4-space line inside milestones:/phases: (not "- id:") -> malformed
|
|
137
|
+
# dedent: closes the current entry without opening a new one.
|
|
138
|
+
(block == "milestones" || block == "phases") && $0 ~ /^ [A-Za-z_-]/ {
|
|
139
|
+
finish_entry()
|
|
140
|
+
next
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
END {
|
|
144
|
+
finish_entry()
|
|
145
|
+
|
|
146
|
+
required_ms = "name phases"
|
|
147
|
+
required_ph = "name goal requirements dependencies success_criteria estimated_hours status"
|
|
148
|
+
|
|
149
|
+
# 1. required fields
|
|
150
|
+
for (i = 1; i <= ms_count; i++) {
|
|
151
|
+
n = split(required_ms, reqs, " ")
|
|
152
|
+
for (j = 1; j <= n; j++) {
|
|
153
|
+
if (index(ms_fields[i], " " reqs[j] " ") == 0) {
|
|
154
|
+
print "finding=missing-field:milestone:" ms_id[i] ":" reqs[j]
|
|
155
|
+
found++
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (i = 1; i <= ph_count; i++) {
|
|
160
|
+
n = split(required_ph, reqs, " ")
|
|
161
|
+
for (j = 1; j <= n; j++) {
|
|
162
|
+
if (index(ph_fields[i], " " reqs[j] " ") == 0) {
|
|
163
|
+
print "finding=missing-field:phase:" ph_id[i] ":" reqs[j]
|
|
164
|
+
found++
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# phase-id set + first-seen order tracking for duplicate detection
|
|
170
|
+
for (i = 1; i <= ph_count; i++) {
|
|
171
|
+
id = ph_id[i]
|
|
172
|
+
phase_seen[id] = phase_seen[id] "" (phase_seen[id] == "" ? "" : ",")
|
|
173
|
+
dup_list[id] = (dup_list[id] == "" ? "" : dup_list[id] ",") (ph_mdir[i] == "" ? "(none)" : ph_mdir[i])
|
|
174
|
+
dup_count[id]++
|
|
175
|
+
phase_exists[id] = 1
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# 2. duplicate phase ids
|
|
179
|
+
for (id in dup_count) {
|
|
180
|
+
if (dup_count[id] > 1) {
|
|
181
|
+
print "finding=duplicate-phase-id:" id ":" dup_list[id]
|
|
182
|
+
found++
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# 3. dangling-phase-ref (milestone claims a phase id with no phase entry)
|
|
187
|
+
for (i = 1; i <= ms_count; i++) {
|
|
188
|
+
m = split(ms_phaselist[i], claimed, /[ ,]+/)
|
|
189
|
+
for (j = 1; j <= m; j++) {
|
|
190
|
+
pid = trim(claimed[j])
|
|
191
|
+
if (pid == "") continue
|
|
192
|
+
claimed_by[pid] = 1
|
|
193
|
+
if (!(pid in phase_exists)) {
|
|
194
|
+
print "finding=dangling-phase-ref:milestone:" ms_id[i] ":phase:" pid
|
|
195
|
+
found++
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# orphan-phase (phase entry claimed by zero milestones)
|
|
201
|
+
for (i = 1; i <= ph_count; i++) {
|
|
202
|
+
id = ph_id[i]
|
|
203
|
+
if (!(id in claimed_by)) {
|
|
204
|
+
print "finding=orphan-phase:" id ":" (ph_mdir[i] == "" ? "(none)" : ph_mdir[i])
|
|
205
|
+
found++
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# 4. bad-milestone-dir (milestone_dir prefix must resolve to a real milestone id)
|
|
210
|
+
for (i = 1; i <= ms_count; i++) {
|
|
211
|
+
ms_numeral[bare_num(ms_id[i])] = 1
|
|
212
|
+
}
|
|
213
|
+
for (i = 1; i <= ph_count; i++) {
|
|
214
|
+
if (ph_mdir[i] == "") continue
|
|
215
|
+
if (match(ph_mdir[i], /^milestone-[^\/]+/)) {
|
|
216
|
+
prefix = substr(ph_mdir[i], RSTART, RLENGTH)
|
|
217
|
+
numeral = prefix
|
|
218
|
+
sub(/^milestone-/, "", numeral)
|
|
219
|
+
if (!(numeral in ms_numeral)) {
|
|
220
|
+
print "finding=bad-milestone-dir:" ph_id[i] ":" ph_mdir[i]
|
|
221
|
+
found++
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# 5. dangling-wave-ref
|
|
227
|
+
for (i = 1; i <= wave_count; i++) {
|
|
228
|
+
m = split(wave_ids[i], wids, /[ ,]+/)
|
|
229
|
+
for (j = 1; j <= m; j++) {
|
|
230
|
+
pid = trim(wids[j])
|
|
231
|
+
if (pid == "") continue
|
|
232
|
+
if (!(pid in phase_exists)) {
|
|
233
|
+
print "finding=dangling-wave-ref:" wave_num[i] ":" pid
|
|
234
|
+
found++
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (found + 0 == 0) {
|
|
240
|
+
print "ok=clean"
|
|
241
|
+
exit 0
|
|
242
|
+
}
|
|
243
|
+
exit 1
|
|
244
|
+
}
|
|
245
|
+
' "$FILE"
|
|
246
|
+
exit $?
|