@house365/comate 0.1.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/LICENSE +21 -0
- package/README.md +78 -0
- package/bin/cli.mjs +45 -0
- package/bin/init-run.mjs +99 -0
- package/bin/init.mjs +159 -0
- package/docs/COMATE_MANUAL.md +612 -0
- package/package.json +52 -0
- package/skills/comate/SKILL.md +213 -0
- package/skills/comate/reference/decision-points.md +54 -0
- package/skills/comate/reference/dirty-worktree.md +58 -0
- package/skills/comate/reference/prerequisites.md +88 -0
- package/skills/comate/scripts/comate-lib.sh +430 -0
- package/skills/comate/scripts/comate.sh +1681 -0
- package/skills/comate/tests/comate.bats +449 -0
- package/skills/comate/tests/run.sh +101 -0
- package/skills/comate-archive/SKILL.md +63 -0
- package/skills/comate-build/SKILL.md +152 -0
- package/skills/comate-design/SKILL.md +106 -0
- package/skills/comate-hotfix/SKILL.md +115 -0
- package/skills/comate-open/SKILL.md +109 -0
- package/skills/comate-tweak/SKILL.md +109 -0
- package/skills/comate-verify/SKILL.md +130 -0
|
@@ -0,0 +1,1681 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# comate.sh — single entry dispatcher for comate.
|
|
3
|
+
#
|
|
4
|
+
# Subcommands:
|
|
5
|
+
# state <name> init <workflow> — create .comate.yaml with workflow defaults
|
|
6
|
+
# state <name> get <field> — read a field
|
|
7
|
+
# state <name> set <field> <value> — write a field (atomic, write-back checked)
|
|
8
|
+
# state <name> transition <event> — guarded state-machine transition
|
|
9
|
+
# state <name> check <phase> [--json] — entry-precondition check for a phase
|
|
10
|
+
# state <name> recover <phase> — structured compaction-resume context
|
|
11
|
+
# state <name> scale — assess and set verify_mode
|
|
12
|
+
# state <name> validate — schema + flat-yaml validation
|
|
13
|
+
# guard <name> <phase> [--apply] [--json] — exit-evidence check, optionally transition
|
|
14
|
+
# handoff <name> design --write [--full] — design->build handoff package
|
|
15
|
+
# handoff <name> build --write — build->verify handoff package
|
|
16
|
+
# sources <name> track <file...> [--note] — record knowledge-base source files
|
|
17
|
+
# sources <name> check — drift check tracked source files
|
|
18
|
+
# sources <name> list — print the source manifest
|
|
19
|
+
# sources find <file> — reverse lookup: changes citing a file
|
|
20
|
+
# archive <name> [--dry-run] — sync delta specs + annotate + move + mark archived
|
|
21
|
+
# status [path] — list active changes + next command
|
|
22
|
+
# doctor — prerequisite self-check
|
|
23
|
+
#
|
|
24
|
+
# Exit codes: 0 OK | 1 BLOCK | 2 USAGE | 3 ENV | 4 CORRUPT
|
|
25
|
+
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")" && pwd)"
|
|
29
|
+
# shellcheck source=comate-lib.sh
|
|
30
|
+
. "$SCRIPT_DIR/comate-lib.sh"
|
|
31
|
+
|
|
32
|
+
usage_top() {
|
|
33
|
+
cat >&2 <<'EOF'
|
|
34
|
+
Usage: comate.sh <command> ...
|
|
35
|
+
|
|
36
|
+
state <name> init <workflow> full | hotfix | tweak
|
|
37
|
+
state <name> get <field>
|
|
38
|
+
state <name> set <field> <value>
|
|
39
|
+
state <name> transition <event> open-complete|design-complete|build-complete|verify-pass|verify-fail|archived
|
|
40
|
+
state <name> check <phase> [--json]
|
|
41
|
+
state <name> recover <phase>
|
|
42
|
+
state <name> scale
|
|
43
|
+
state <name> validate
|
|
44
|
+
guard <name> <phase> [--apply] [--json]
|
|
45
|
+
handoff <name> design --write [--full]
|
|
46
|
+
handoff <name> build --write
|
|
47
|
+
sources <name> track <file...> [--note <text>]
|
|
48
|
+
sources <name> check
|
|
49
|
+
sources <name> list
|
|
50
|
+
sources find <file>
|
|
51
|
+
archive <name> [--dry-run]
|
|
52
|
+
status [path]
|
|
53
|
+
doctor
|
|
54
|
+
EOF
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
require_yaml() {
|
|
58
|
+
# $1 = change name; ensures .comate.yaml exists, echoes its path.
|
|
59
|
+
local name="$1" yaml
|
|
60
|
+
yaml=$(comate_yaml_for "$name")
|
|
61
|
+
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "ERROR: .comate.yaml not found at $yaml"
|
|
62
|
+
echo "$yaml"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
today() { date +%Y-%m-%d; }
|
|
66
|
+
|
|
67
|
+
# =====================================================================
|
|
68
|
+
# state
|
|
69
|
+
# =====================================================================
|
|
70
|
+
|
|
71
|
+
state_init() {
|
|
72
|
+
local name="$1" workflow="${2:-}"
|
|
73
|
+
validate_change_name "$name"
|
|
74
|
+
validate_enum workflow "$workflow"
|
|
75
|
+
[ -n "$workflow" ] || die "$COMATE_USAGE" "Usage: comate.sh state <name> init <full|hotfix|tweak>"
|
|
76
|
+
|
|
77
|
+
local dir yaml
|
|
78
|
+
dir=$(comate_change_dir "$name")
|
|
79
|
+
yaml="$dir/.comate.yaml"
|
|
80
|
+
[ -f "$yaml" ] && die "$COMATE_USAGE" "ERROR: .comate.yaml already exists at $yaml"
|
|
81
|
+
mkdir -p "$dir"
|
|
82
|
+
|
|
83
|
+
# base_ref = current HEAD.
|
|
84
|
+
local base_ref="null"
|
|
85
|
+
if git rev-parse --verify HEAD >/dev/null 2>&1; then
|
|
86
|
+
base_ref=$(git rev-parse HEAD 2>/dev/null || echo "null")
|
|
87
|
+
fi
|
|
88
|
+
# base_branch = detected default branch, fallback main/master.
|
|
89
|
+
local base_branch="null" ref
|
|
90
|
+
if ref=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null); then
|
|
91
|
+
base_branch="${ref#refs/remotes/origin/}"
|
|
92
|
+
elif git show-ref --verify --quiet refs/heads/main 2>/dev/null; then
|
|
93
|
+
base_branch="main"
|
|
94
|
+
elif git show-ref --verify --quiet refs/heads/master 2>/dev/null; then
|
|
95
|
+
base_branch="master"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Workflow defaults: full leaves decisions null; presets prefill.
|
|
99
|
+
local build_mode="null" tdd_mode="null" isolation="null" verify_mode="null"
|
|
100
|
+
case "$workflow" in
|
|
101
|
+
hotfix|tweak)
|
|
102
|
+
build_mode="direct"; tdd_mode="direct"; isolation="branch"; verify_mode="light" ;;
|
|
103
|
+
esac
|
|
104
|
+
|
|
105
|
+
# Write full canonical file directly (FIELD_ORDER kept in sync with lib).
|
|
106
|
+
cat > "$yaml" <<EOF
|
|
107
|
+
# comate state — flat key:value only, no nesting
|
|
108
|
+
schema_version: $COMATE_SCHEMA_VERSION
|
|
109
|
+
workflow: $workflow
|
|
110
|
+
phase: open
|
|
111
|
+
build_mode: $build_mode
|
|
112
|
+
tdd_mode: $tdd_mode
|
|
113
|
+
isolation: $isolation
|
|
114
|
+
verify_mode: $verify_mode
|
|
115
|
+
verify_result: pending
|
|
116
|
+
design_doc: null
|
|
117
|
+
plan: null
|
|
118
|
+
verification_report: null
|
|
119
|
+
branch_status: pending
|
|
120
|
+
handoff_context: null
|
|
121
|
+
handoff_hash: null
|
|
122
|
+
impl_context: null
|
|
123
|
+
impl_hash: null
|
|
124
|
+
direct_override: false
|
|
125
|
+
base_ref: $base_ref
|
|
126
|
+
base_branch: $base_branch
|
|
127
|
+
source_kind: conversation
|
|
128
|
+
source_manifest: null
|
|
129
|
+
build_command: null
|
|
130
|
+
verify_command: null
|
|
131
|
+
created_at: $(today)
|
|
132
|
+
verified_at: null
|
|
133
|
+
archived: false
|
|
134
|
+
EOF
|
|
135
|
+
|
|
136
|
+
validate_flat_yaml "$yaml"
|
|
137
|
+
log_pass "[INIT] $yaml (workflow=$workflow, base_branch=$base_branch)"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
state_get() {
|
|
141
|
+
local name="$1" field="${2:-}"
|
|
142
|
+
validate_change_name "$name"
|
|
143
|
+
[ -n "$field" ] || die "$COMATE_USAGE" "Usage: comate.sh state <name> get <field>"
|
|
144
|
+
local yaml; yaml=$(require_yaml "$name")
|
|
145
|
+
yaml_get "$yaml" "$field"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
state_set() {
|
|
149
|
+
local name="$1" field="${2:-}" value="${3:-}"
|
|
150
|
+
validate_change_name "$name"
|
|
151
|
+
[ -n "$field" ] || die "$COMATE_USAGE" "Usage: comate.sh state <name> set <field> <value>"
|
|
152
|
+
if [ "$field" = "phase" ]; then
|
|
153
|
+
log_warn "WARNING: setting 'phase' directly bypasses the state machine; prefer 'state transition'."
|
|
154
|
+
fi
|
|
155
|
+
local yaml; yaml=$(require_yaml "$name")
|
|
156
|
+
yaml_set "$yaml" "$field" "$value"
|
|
157
|
+
log_pass "[SET] $field=$value"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# --- transition guards (see IMPLEMENTATION.md §3 guard table) ---
|
|
161
|
+
|
|
162
|
+
_require_phase() {
|
|
163
|
+
local yaml="$1" expected="$2" actual
|
|
164
|
+
actual=$(yaml_get "$yaml" phase)
|
|
165
|
+
[ "$actual" = "$expected" ] || \
|
|
166
|
+
die "$COMATE_BLOCK" "ERROR: expected phase '$expected', got '${actual:-null}'"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_require_build_decisions() {
|
|
170
|
+
local yaml="$1"
|
|
171
|
+
local workflow build_mode tdd_mode isolation direct_override
|
|
172
|
+
workflow=$(yaml_get "$yaml" workflow)
|
|
173
|
+
build_mode=$(yaml_get "$yaml" build_mode)
|
|
174
|
+
tdd_mode=$(yaml_get "$yaml" tdd_mode)
|
|
175
|
+
isolation=$(yaml_get "$yaml" isolation)
|
|
176
|
+
direct_override=$(yaml_get "$yaml" direct_override)
|
|
177
|
+
case "$isolation" in
|
|
178
|
+
branch|worktree) ;;
|
|
179
|
+
*) die "$COMATE_BLOCK" "ERROR: isolation must be branch or worktree, got '${isolation:-null}'" ;;
|
|
180
|
+
esac
|
|
181
|
+
case "$build_mode" in
|
|
182
|
+
subagent-driven-development|executing-plans|direct) ;;
|
|
183
|
+
*) die "$COMATE_BLOCK" "ERROR: build_mode must be selected, got '${build_mode:-null}'" ;;
|
|
184
|
+
esac
|
|
185
|
+
if [ "$build_mode" = "direct" ] && [ "$workflow" != "hotfix" ] && [ "$workflow" != "tweak" ] && [ "$direct_override" != "true" ]; then
|
|
186
|
+
die "$COMATE_BLOCK" "ERROR: build_mode=direct is only allowed for hotfix/tweak unless direct_override=true"
|
|
187
|
+
fi
|
|
188
|
+
case "$tdd_mode" in
|
|
189
|
+
tdd|direct) ;;
|
|
190
|
+
*) die "$COMATE_BLOCK" "ERROR: tdd_mode must be tdd or direct, got '${tdd_mode:-null}'" ;;
|
|
191
|
+
esac
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_require_verify_evidence() {
|
|
195
|
+
local yaml="$1" report branch_status
|
|
196
|
+
report=$(yaml_get "$yaml" verification_report)
|
|
197
|
+
branch_status=$(yaml_get "$yaml" branch_status)
|
|
198
|
+
if [ -z "$report" ] || [ "$report" = "null" ] || [ ! -f "$report" ]; then
|
|
199
|
+
die "$COMATE_BLOCK" "ERROR: verification_report must point to an existing file (got '${report:-null}')"
|
|
200
|
+
fi
|
|
201
|
+
[ "$branch_status" = "handled" ] || \
|
|
202
|
+
die "$COMATE_BLOCK" "ERROR: branch_status must be 'handled', got '${branch_status:-null}'"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
state_transition() {
|
|
206
|
+
local name="$1" event="${2:-}"
|
|
207
|
+
validate_change_name "$name"
|
|
208
|
+
validate_enum_event() {
|
|
209
|
+
case "$1" in
|
|
210
|
+
open-complete|design-complete|build-complete|verify-pass|verify-fail|archived) return 0 ;;
|
|
211
|
+
*) die "$COMATE_USAGE" "ERROR: unknown event '$1'" ;;
|
|
212
|
+
esac
|
|
213
|
+
}
|
|
214
|
+
[ -n "$event" ] || die "$COMATE_USAGE" "Usage: comate.sh state <name> transition <event>"
|
|
215
|
+
validate_enum_event "$event"
|
|
216
|
+
local yaml; yaml=$(require_yaml "$name")
|
|
217
|
+
validate_flat_yaml "$yaml"
|
|
218
|
+
|
|
219
|
+
case "$event" in
|
|
220
|
+
open-complete)
|
|
221
|
+
_require_phase "$yaml" open
|
|
222
|
+
local workflow; workflow=$(yaml_get "$yaml" workflow)
|
|
223
|
+
if [ "$workflow" = "full" ]; then
|
|
224
|
+
yaml_set "$yaml" phase design
|
|
225
|
+
else
|
|
226
|
+
yaml_set "$yaml" phase build
|
|
227
|
+
fi
|
|
228
|
+
;;
|
|
229
|
+
design-complete)
|
|
230
|
+
_require_phase "$yaml" design
|
|
231
|
+
yaml_set "$yaml" phase build
|
|
232
|
+
;;
|
|
233
|
+
build-complete)
|
|
234
|
+
_require_phase "$yaml" build
|
|
235
|
+
_require_build_decisions "$yaml"
|
|
236
|
+
yaml_set "$yaml" phase verify
|
|
237
|
+
yaml_set "$yaml" verify_result pending
|
|
238
|
+
yaml_set "$yaml" verification_report null
|
|
239
|
+
yaml_set "$yaml" branch_status pending
|
|
240
|
+
;;
|
|
241
|
+
verify-pass)
|
|
242
|
+
_require_phase "$yaml" verify
|
|
243
|
+
_require_verify_evidence "$yaml"
|
|
244
|
+
yaml_set "$yaml" verify_result pass
|
|
245
|
+
yaml_set "$yaml" phase archive
|
|
246
|
+
yaml_set "$yaml" verified_at "$(today)"
|
|
247
|
+
;;
|
|
248
|
+
verify-fail)
|
|
249
|
+
_require_phase "$yaml" verify
|
|
250
|
+
yaml_set "$yaml" verify_result fail
|
|
251
|
+
yaml_set "$yaml" phase build
|
|
252
|
+
yaml_set "$yaml" branch_status pending
|
|
253
|
+
;;
|
|
254
|
+
archived)
|
|
255
|
+
_require_phase "$yaml" archive
|
|
256
|
+
yaml_set "$yaml" archived true
|
|
257
|
+
;;
|
|
258
|
+
esac
|
|
259
|
+
log_pass "[TRANSITION] $event"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# --- validate (schema + flat-yaml) ---
|
|
263
|
+
|
|
264
|
+
state_validate() {
|
|
265
|
+
local name="$1"
|
|
266
|
+
validate_change_name "$name"
|
|
267
|
+
local yaml; yaml=$(require_yaml "$name")
|
|
268
|
+
log_info "[VALIDATE] $yaml"
|
|
269
|
+
|
|
270
|
+
validate_flat_yaml "$yaml" # dies code 4 on nesting/lists/block scalars
|
|
271
|
+
|
|
272
|
+
local errors=0 warnings=0
|
|
273
|
+
# schema_version present and known.
|
|
274
|
+
local sv; sv=$(yaml_get "$yaml" schema_version)
|
|
275
|
+
if [ -z "$sv" ] || [ "$sv" = "null" ]; then
|
|
276
|
+
log_warn " WARN: schema_version missing"; warnings=$((warnings+1))
|
|
277
|
+
elif [ "$sv" != "$COMATE_SCHEMA_VERSION" ]; then
|
|
278
|
+
log_warn " WARN: schema_version=$sv (this tool writes $COMATE_SCHEMA_VERSION)"; warnings=$((warnings+1))
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# Required fields present.
|
|
282
|
+
local required="workflow phase verify_result archived"
|
|
283
|
+
local f
|
|
284
|
+
for f in $required; do
|
|
285
|
+
if ! yaml_keys "$yaml" | grep -qx "$f"; then
|
|
286
|
+
log_fail " FAIL: missing required field '$f'"; errors=$((errors+1))
|
|
287
|
+
fi
|
|
288
|
+
done
|
|
289
|
+
|
|
290
|
+
# Enum validity (null/empty allowed by validate_enum, but it dies on bad — so check inline).
|
|
291
|
+
local key legal v ok w
|
|
292
|
+
for key in "${!ENUMS[@]}"; do
|
|
293
|
+
v=$(yaml_get "$yaml" "$key")
|
|
294
|
+
[ -z "$v" ] && continue
|
|
295
|
+
[ "$v" = "null" ] && continue
|
|
296
|
+
legal="${ENUMS[$key]}"
|
|
297
|
+
ok=0
|
|
298
|
+
for w in $legal; do
|
|
299
|
+
if [ "$v" = "$w" ]; then ok=1; break; fi
|
|
300
|
+
done
|
|
301
|
+
if [ "$ok" -eq 0 ]; then
|
|
302
|
+
log_fail " FAIL: $key='$v' invalid (expected: $legal)"; errors=$((errors+1))
|
|
303
|
+
fi
|
|
304
|
+
done
|
|
305
|
+
|
|
306
|
+
# Path fields point to existing files when set.
|
|
307
|
+
for f in design_doc plan verification_report handoff_context impl_context; do
|
|
308
|
+
v=$(yaml_get "$yaml" "$f")
|
|
309
|
+
if [ -n "$v" ] && [ "$v" != "null" ] && [ ! -f "$v" ]; then
|
|
310
|
+
log_fail " FAIL: $f='$v' does not exist on disk"; errors=$((errors+1))
|
|
311
|
+
fi
|
|
312
|
+
done
|
|
313
|
+
|
|
314
|
+
# Hash fields are 64 hex when set.
|
|
315
|
+
for f in handoff_hash impl_hash; do
|
|
316
|
+
v=$(yaml_get "$yaml" "$f")
|
|
317
|
+
if [ -n "$v" ] && [ "$v" != "null" ] && [[ ! "$v" =~ ^[a-f0-9]{64}$ ]]; then
|
|
318
|
+
log_fail " FAIL: $f='$v' is not a sha256 hex digest"; errors=$((errors+1))
|
|
319
|
+
fi
|
|
320
|
+
done
|
|
321
|
+
|
|
322
|
+
# Unknown keys -> warning.
|
|
323
|
+
local known=" ${FIELD_ORDER[*]} "
|
|
324
|
+
while IFS= read -r key; do
|
|
325
|
+
[ -z "$key" ] && continue
|
|
326
|
+
case "$known" in
|
|
327
|
+
*" $key "*) ;;
|
|
328
|
+
*) log_warn " WARN: unknown field '$key'"; warnings=$((warnings+1)) ;;
|
|
329
|
+
esac
|
|
330
|
+
done < <(yaml_keys "$yaml")
|
|
331
|
+
|
|
332
|
+
if [ "$errors" -gt 0 ]; then
|
|
333
|
+
die "$COMATE_CORRUPT" "$errors error(s), $warnings warning(s) — validation FAILED"
|
|
334
|
+
fi
|
|
335
|
+
log_pass "0 errors, $warnings warning(s) — validation PASSED"
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# --- check (entry preconditions for a phase) ---
|
|
339
|
+
|
|
340
|
+
CHECK_JSON=0
|
|
341
|
+
declare -a CHECK_RESULTS=() # "name|status"
|
|
342
|
+
CHECK_BLOCK=0
|
|
343
|
+
|
|
344
|
+
_chk() {
|
|
345
|
+
# $1 = name, $2 = 0(pass)/1(fail). Always returns 0 (set -e safe).
|
|
346
|
+
local name="$1" rc="$2"
|
|
347
|
+
if [ "$rc" -eq 0 ]; then
|
|
348
|
+
CHECK_RESULTS+=("$name|pass")
|
|
349
|
+
if [ "$CHECK_JSON" -eq 0 ]; then log_pass " [PASS] $name"; fi
|
|
350
|
+
else
|
|
351
|
+
CHECK_RESULTS+=("$name|fail")
|
|
352
|
+
CHECK_BLOCK=1
|
|
353
|
+
if [ "$CHECK_JSON" -eq 0 ]; then log_fail " [FAIL] $name"; fi
|
|
354
|
+
fi
|
|
355
|
+
return 0
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_chk_yaml_is() {
|
|
359
|
+
local yaml="$1" field="$2" expected="$3" actual rc=0
|
|
360
|
+
actual=$(yaml_get "$yaml" "$field")
|
|
361
|
+
[ "$actual" = "$expected" ] || rc=1
|
|
362
|
+
_chk "$field=$expected (got ${actual:-null})" "$rc"
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
_chk_yaml_empty() {
|
|
366
|
+
local yaml="$1" field="$2" v rc=0
|
|
367
|
+
v=$(yaml_get "$yaml" "$field")
|
|
368
|
+
{ [ -z "$v" ] || [ "$v" = "null" ]; } || rc=1
|
|
369
|
+
_chk "$field empty/null" "$rc"
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_chk_nonempty() {
|
|
373
|
+
local rc=0
|
|
374
|
+
file_nonempty "$2" || rc=1
|
|
375
|
+
_chk "$1 non-empty" "$rc"
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
state_check() {
|
|
379
|
+
local name="$1" phase="${2:-}"
|
|
380
|
+
validate_change_name "$name"
|
|
381
|
+
validate_enum phase "$phase"
|
|
382
|
+
[ -n "$phase" ] || die "$COMATE_USAGE" "Usage: comate.sh state <name> check <phase> [--json]"
|
|
383
|
+
local dir yaml
|
|
384
|
+
dir=$(comate_change_dir "$name")
|
|
385
|
+
yaml="$dir/.comate.yaml"
|
|
386
|
+
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "ERROR: .comate.yaml not found at $yaml"
|
|
387
|
+
|
|
388
|
+
case "$phase" in
|
|
389
|
+
open)
|
|
390
|
+
_chk ".comate.yaml exists" 0
|
|
391
|
+
_chk_yaml_is "$yaml" phase open
|
|
392
|
+
;;
|
|
393
|
+
design)
|
|
394
|
+
_chk_yaml_is "$yaml" phase design
|
|
395
|
+
_chk_yaml_is "$yaml" workflow full
|
|
396
|
+
_chk_yaml_empty "$yaml" design_doc
|
|
397
|
+
_chk_nonempty "proposal.md" "$dir/proposal.md"
|
|
398
|
+
_chk_nonempty "design.md" "$dir/design.md"
|
|
399
|
+
_chk_nonempty "tasks.md" "$dir/tasks.md"
|
|
400
|
+
;;
|
|
401
|
+
build)
|
|
402
|
+
_chk_yaml_is "$yaml" phase build
|
|
403
|
+
local workflow; workflow=$(yaml_get "$yaml" workflow)
|
|
404
|
+
if [ "$workflow" = "full" ]; then
|
|
405
|
+
local dd; dd=$(yaml_get "$yaml" design_doc)
|
|
406
|
+
{ [ -n "$dd" ] && [ "$dd" != "null" ] && [ -f "$dd" ]; }
|
|
407
|
+
_chk "design_doc exists ($dd)" $?
|
|
408
|
+
fi
|
|
409
|
+
_chk_nonempty "proposal.md" "$dir/proposal.md"
|
|
410
|
+
_chk_nonempty "tasks.md" "$dir/tasks.md"
|
|
411
|
+
;;
|
|
412
|
+
verify)
|
|
413
|
+
_chk_yaml_is "$yaml" phase verify
|
|
414
|
+
local vr; vr=$(yaml_get "$yaml" verify_result)
|
|
415
|
+
{ [ "$vr" = "pending" ] || [ -z "$vr" ] || [ "$vr" = "null" ]; }
|
|
416
|
+
_chk "verify_result pending/null (got $vr)" $?
|
|
417
|
+
;;
|
|
418
|
+
archive)
|
|
419
|
+
_chk_yaml_is "$yaml" phase archive
|
|
420
|
+
_chk_yaml_is "$yaml" verify_result pass
|
|
421
|
+
local ar; ar=$(yaml_get "$yaml" archived)
|
|
422
|
+
[ "$ar" != "true" ]; _chk "archived not yet true (got $ar)" $?
|
|
423
|
+
;;
|
|
424
|
+
esac
|
|
425
|
+
|
|
426
|
+
if [ "$CHECK_JSON" -eq 1 ]; then
|
|
427
|
+
local out='{"phase":"'"$phase"'","checks":['
|
|
428
|
+
local first=1 r cname cstatus
|
|
429
|
+
for r in "${CHECK_RESULTS[@]}"; do
|
|
430
|
+
cname="${r%|*}"; cstatus="${r#*|}"
|
|
431
|
+
[ "$first" -eq 0 ] && out+=','
|
|
432
|
+
first=0
|
|
433
|
+
out+='{"name":"'"$(json_escape "$cname")"'","status":"'"$cstatus"'"}'
|
|
434
|
+
done
|
|
435
|
+
out+='],"block":'"$([ "$CHECK_BLOCK" -eq 1 ] && echo true || echo false)"'}'
|
|
436
|
+
printf '%s\n' "$out"
|
|
437
|
+
else
|
|
438
|
+
echo "" >&2
|
|
439
|
+
if [ "$CHECK_BLOCK" -eq 1 ]; then
|
|
440
|
+
log_fail "BLOCKED — fix failing checks before proceeding"
|
|
441
|
+
else
|
|
442
|
+
log_pass "ALL CHECKS PASSED — ready to proceed"
|
|
443
|
+
fi
|
|
444
|
+
fi
|
|
445
|
+
if [ "$CHECK_BLOCK" -eq 1 ]; then exit "$COMATE_BLOCK"; fi
|
|
446
|
+
exit "$COMATE_OK"
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
# --- recover (compaction-resume structured context) ---
|
|
450
|
+
|
|
451
|
+
_field_status() {
|
|
452
|
+
# $1 name, $2 value, [$3 path]
|
|
453
|
+
local field="$1" value="$2" path="${3:-}"
|
|
454
|
+
if [ -z "$value" ] || [ "$value" = "null" ]; then
|
|
455
|
+
echo " - $field: PENDING"
|
|
456
|
+
elif [ -n "$path" ] && [ ! -f "$path" ]; then
|
|
457
|
+
echo " - $field: BROKEN (path $value does not exist)"
|
|
458
|
+
else
|
|
459
|
+
echo " - $field: DONE ($value)"
|
|
460
|
+
fi
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
state_recover() {
|
|
464
|
+
local name="$1" phase_arg="${2:-}"
|
|
465
|
+
validate_change_name "$name"
|
|
466
|
+
local dir yaml
|
|
467
|
+
dir=$(comate_change_dir "$name")
|
|
468
|
+
yaml="$dir/.comate.yaml"
|
|
469
|
+
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "ERROR: .comate.yaml not found at $yaml"
|
|
470
|
+
|
|
471
|
+
local phase workflow
|
|
472
|
+
phase=$(yaml_get "$yaml" phase)
|
|
473
|
+
workflow=$(yaml_get "$yaml" workflow)
|
|
474
|
+
# If a phase arg was passed, honor it for reporting; else use stored phase.
|
|
475
|
+
[ -n "$phase_arg" ] && phase="$phase_arg"
|
|
476
|
+
|
|
477
|
+
echo "=== Recovery Context: $name ==="
|
|
478
|
+
echo "Phase: $phase"
|
|
479
|
+
echo "Workflow: $workflow"
|
|
480
|
+
echo ""
|
|
481
|
+
|
|
482
|
+
local design_doc plan verify_result verify_mode verification_report
|
|
483
|
+
local branch_status handoff_context handoff_hash impl_context impl_hash
|
|
484
|
+
local isolation build_mode tdd_mode direct_override
|
|
485
|
+
design_doc=$(yaml_get "$yaml" design_doc)
|
|
486
|
+
plan=$(yaml_get "$yaml" plan)
|
|
487
|
+
verify_result=$(yaml_get "$yaml" verify_result)
|
|
488
|
+
verify_mode=$(yaml_get "$yaml" verify_mode)
|
|
489
|
+
verification_report=$(yaml_get "$yaml" verification_report)
|
|
490
|
+
branch_status=$(yaml_get "$yaml" branch_status)
|
|
491
|
+
handoff_context=$(yaml_get "$yaml" handoff_context)
|
|
492
|
+
handoff_hash=$(yaml_get "$yaml" handoff_hash)
|
|
493
|
+
impl_context=$(yaml_get "$yaml" impl_context)
|
|
494
|
+
impl_hash=$(yaml_get "$yaml" impl_hash)
|
|
495
|
+
isolation=$(yaml_get "$yaml" isolation)
|
|
496
|
+
build_mode=$(yaml_get "$yaml" build_mode)
|
|
497
|
+
tdd_mode=$(yaml_get "$yaml" tdd_mode)
|
|
498
|
+
direct_override=$(yaml_get "$yaml" direct_override)
|
|
499
|
+
|
|
500
|
+
case "$phase" in
|
|
501
|
+
open)
|
|
502
|
+
echo "Artifacts:"
|
|
503
|
+
local fdone
|
|
504
|
+
for f in proposal.md design.md tasks.md; do
|
|
505
|
+
if file_nonempty "$dir/$f"; then echo " - $f: DONE"; else echo " - $f: PENDING"; fi
|
|
506
|
+
done
|
|
507
|
+
echo ""
|
|
508
|
+
echo "Recovery action: Create or complete missing artifacts, then use AskUserQuestion for user confirmation."
|
|
509
|
+
;;
|
|
510
|
+
design)
|
|
511
|
+
echo "Artifacts:"
|
|
512
|
+
for f in proposal.md design.md tasks.md; do
|
|
513
|
+
if file_nonempty "$dir/$f"; then echo " - $f: DONE"; else echo " - $f: MISSING (unexpected in design)"; fi
|
|
514
|
+
done
|
|
515
|
+
echo ""
|
|
516
|
+
echo "Design progress:"
|
|
517
|
+
_field_status "handoff_context" "$handoff_context" "$handoff_context"
|
|
518
|
+
_field_status "handoff_hash" "$handoff_hash"
|
|
519
|
+
_field_status "design_doc" "$design_doc" "$design_doc"
|
|
520
|
+
echo ""
|
|
521
|
+
if [ -n "$design_doc" ] && [ "$design_doc" != "null" ] && [ -f "$design_doc" ]; then
|
|
522
|
+
echo "Recovery action: Design Doc linked. Run guard to transition to build."
|
|
523
|
+
elif [ -n "$handoff_context" ] && [ "$handoff_context" != "null" ] && [ -f "$handoff_context" ]; then
|
|
524
|
+
echo "Recovery action: Handoff generated but Design Doc not created. Resume from brainstorming confirmation (Step 1c)."
|
|
525
|
+
else
|
|
526
|
+
echo "Recovery action: No handoff yet. Start from Step 1a (generate handoff package)."
|
|
527
|
+
fi
|
|
528
|
+
;;
|
|
529
|
+
build)
|
|
530
|
+
echo "Build decisions:"
|
|
531
|
+
_field_status "isolation" "$isolation"
|
|
532
|
+
_field_status "build_mode" "$build_mode"
|
|
533
|
+
_field_status "tdd_mode" "$tdd_mode"
|
|
534
|
+
if [ "$build_mode" = "direct" ] && [ "$workflow" != "hotfix" ] && [ "$workflow" != "tweak" ]; then
|
|
535
|
+
_field_status "direct_override" "$direct_override"
|
|
536
|
+
fi
|
|
537
|
+
echo ""
|
|
538
|
+
_field_status "plan" "$plan" "$plan"
|
|
539
|
+
_field_status "impl_context" "$impl_context" "$impl_context"
|
|
540
|
+
echo ""
|
|
541
|
+
local tasks_file="$dir/tasks.md" total=0 done=0 pending=0
|
|
542
|
+
if [ -f "$tasks_file" ]; then
|
|
543
|
+
total=$(grep -c '^- \[' "$tasks_file" 2>/dev/null || echo 0)
|
|
544
|
+
done=$(grep -c '^- \[x\]' "$tasks_file" 2>/dev/null || echo 0)
|
|
545
|
+
pending=$((total - done))
|
|
546
|
+
echo "Tasks: $done/$total done, $pending pending"
|
|
547
|
+
else
|
|
548
|
+
echo "Tasks: tasks.md MISSING"
|
|
549
|
+
fi
|
|
550
|
+
echo ""
|
|
551
|
+
if [ "$isolation" = "null" ] || [ -z "$isolation" ]; then
|
|
552
|
+
echo "Recovery action: Isolation not selected. AskUserQuestion for branch/worktree."
|
|
553
|
+
elif [ "$build_mode" = "null" ] || [ -z "$build_mode" ]; then
|
|
554
|
+
echo "Recovery action: Build mode not selected. AskUserQuestion for execution method."
|
|
555
|
+
elif [ "$tdd_mode" = "null" ] || [ -z "$tdd_mode" ]; then
|
|
556
|
+
echo "Recovery action: TDD mode not selected. AskUserQuestion for tdd/direct."
|
|
557
|
+
elif [ ! -f "$tasks_file" ]; then
|
|
558
|
+
echo "Recovery action: tasks.md missing. Verify change directory integrity."
|
|
559
|
+
elif [ "$pending" -gt 0 ]; then
|
|
560
|
+
echo "Recovery action: Read tasks.md and continue from first unchecked task."
|
|
561
|
+
else
|
|
562
|
+
echo "Recovery action: All tasks done. Run guard to transition to verify."
|
|
563
|
+
fi
|
|
564
|
+
;;
|
|
565
|
+
verify)
|
|
566
|
+
echo "Verification:"
|
|
567
|
+
_field_status "verify_result" "$verify_result"
|
|
568
|
+
_field_status "verify_mode" "$verify_mode"
|
|
569
|
+
_field_status "verification_report" "$verification_report" "$verification_report"
|
|
570
|
+
_field_status "branch_status" "$branch_status"
|
|
571
|
+
_field_status "impl_hash" "$impl_hash"
|
|
572
|
+
echo ""
|
|
573
|
+
if [ "$verify_result" = "pass" ] && [ "$branch_status" = "handled" ]; then
|
|
574
|
+
echo "Recovery action: Verification complete. Run guard to transition to archive."
|
|
575
|
+
elif [ "$verify_result" = "pass" ]; then
|
|
576
|
+
echo "Recovery action: Verified but branch not handled. Complete branch handling, set branch_status=handled."
|
|
577
|
+
elif [ "$verify_result" = "fail" ]; then
|
|
578
|
+
echo "Recovery action: Verification failed, rolled back to build. Resume from /comate-build."
|
|
579
|
+
else
|
|
580
|
+
echo "Recovery action: Verification not started. Run scale assessment then verify."
|
|
581
|
+
fi
|
|
582
|
+
;;
|
|
583
|
+
archive)
|
|
584
|
+
echo "Archive:"
|
|
585
|
+
_field_status "verify_result" "$verify_result"
|
|
586
|
+
_field_status "archived" "$(yaml_get "$yaml" archived)"
|
|
587
|
+
echo ""
|
|
588
|
+
echo "Recovery action: Run /comate-archive to complete archiving."
|
|
589
|
+
;;
|
|
590
|
+
*)
|
|
591
|
+
die "$COMATE_USAGE" "ERROR: unknown phase '$phase'"
|
|
592
|
+
;;
|
|
593
|
+
esac
|
|
594
|
+
echo ""
|
|
595
|
+
echo "=== End Recovery Context ==="
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
# --- scale (assess verify_mode, base-ref annotation) ---
|
|
599
|
+
|
|
600
|
+
state_scale() {
|
|
601
|
+
local name="$1"
|
|
602
|
+
validate_change_name "$name"
|
|
603
|
+
local dir yaml
|
|
604
|
+
dir=$(comate_change_dir "$name")
|
|
605
|
+
yaml="$dir/.comate.yaml"
|
|
606
|
+
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "ERROR: .comate.yaml not found at $yaml"
|
|
607
|
+
|
|
608
|
+
local tasks_file="$dir/tasks.md" task_count=0
|
|
609
|
+
[ -f "$tasks_file" ] && task_count=$(grep -c '^- \[' "$tasks_file" 2>/dev/null || echo 0)
|
|
610
|
+
|
|
611
|
+
local delta_spec_count=0
|
|
612
|
+
if [ -d "$dir/specs" ]; then
|
|
613
|
+
delta_spec_count=$(find "$dir/specs" -name spec.md -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
614
|
+
fi
|
|
615
|
+
|
|
616
|
+
# Changed files: plan base-ref > yaml base_ref > worktree diff. Annotate source.
|
|
617
|
+
local changed_files=0 basis="worktree-diff" base_ref=""
|
|
618
|
+
if git rev-parse --git-dir >/dev/null 2>&1; then
|
|
619
|
+
local plan_file
|
|
620
|
+
plan_file=$(yaml_get "$yaml" plan)
|
|
621
|
+
if [ -n "$plan_file" ] && [ "$plan_file" != "null" ] && [ -f "$plan_file" ]; then
|
|
622
|
+
base_ref=$(grep '^base-ref:' "$plan_file" 2>/dev/null | head -1 | sed 's/^base-ref:[[:space:]]*//')
|
|
623
|
+
[ -n "$base_ref" ] && [ "$base_ref" != "null" ] && basis="plan base-ref"
|
|
624
|
+
fi
|
|
625
|
+
if [ -z "$base_ref" ] || [ "$base_ref" = "null" ]; then
|
|
626
|
+
base_ref=$(yaml_get "$yaml" base_ref)
|
|
627
|
+
[ -n "$base_ref" ] && [ "$base_ref" != "null" ] && basis="state base_ref"
|
|
628
|
+
fi
|
|
629
|
+
if [ -n "$base_ref" ] && [ "$base_ref" != "null" ] && git rev-parse --verify "$base_ref" >/dev/null 2>&1; then
|
|
630
|
+
changed_files=$(git diff --name-only "$base_ref"...HEAD 2>/dev/null | wc -l | tr -d ' ')
|
|
631
|
+
else
|
|
632
|
+
changed_files=$(git diff --name-only HEAD 2>/dev/null | wc -l | tr -d ' ')
|
|
633
|
+
basis="worktree-diff"
|
|
634
|
+
fi
|
|
635
|
+
fi
|
|
636
|
+
|
|
637
|
+
local result="light"
|
|
638
|
+
if [ "$task_count" -gt 3 ] || [ "$delta_spec_count" -gt 1 ] || [ "$changed_files" -gt 4 ]; then
|
|
639
|
+
result="full"
|
|
640
|
+
fi
|
|
641
|
+
|
|
642
|
+
{
|
|
643
|
+
echo "=== Scale Assessment: $name ==="
|
|
644
|
+
echo " Tasks: $task_count (threshold: 3)"
|
|
645
|
+
echo " Delta specs: $delta_spec_count capabilities (threshold: 1)"
|
|
646
|
+
echo " Changed files: $changed_files (threshold: 4) [basis: $basis]"
|
|
647
|
+
echo " -> Result: $result"
|
|
648
|
+
} >&2
|
|
649
|
+
|
|
650
|
+
yaml_set "$yaml" verify_mode "$result"
|
|
651
|
+
log_pass "[SCALE] verify_mode=$result"
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
# =====================================================================
|
|
655
|
+
# guard — phase EXIT evidence checks (table in IMPLEMENTATION.md §4.3)
|
|
656
|
+
# =====================================================================
|
|
657
|
+
|
|
658
|
+
GUARD_BLOCK=0
|
|
659
|
+
declare -a GUARD_RESULTS=() # "name|status"
|
|
660
|
+
|
|
661
|
+
# _guard_check <desc> <command...> — run command; capture output; record result.
|
|
662
|
+
# Always returns 0 so a recorded FAIL never trips the caller's set -e.
|
|
663
|
+
_guard_check() {
|
|
664
|
+
local desc="$1"; shift
|
|
665
|
+
local output rc
|
|
666
|
+
if output=$("$@" 2>&1); then rc=0; else rc=1; fi
|
|
667
|
+
if [ "$rc" -eq 0 ]; then
|
|
668
|
+
GUARD_RESULTS+=("$desc|pass")
|
|
669
|
+
if [ "$GUARD_JSON" -eq 0 ]; then log_pass " [PASS] $desc"; fi
|
|
670
|
+
else
|
|
671
|
+
GUARD_RESULTS+=("$desc|fail")
|
|
672
|
+
GUARD_BLOCK=1
|
|
673
|
+
if [ "$GUARD_JSON" -eq 0 ]; then
|
|
674
|
+
log_fail " [FAIL] $desc"
|
|
675
|
+
if [ -n "$output" ]; then while IFS= read -r line; do log_fail " $line"; done <<< "$output"; fi
|
|
676
|
+
fi
|
|
677
|
+
fi
|
|
678
|
+
return 0
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
# --- evidence predicates (use $G_DIR / $G_YAML / $G_CHANGE set by guard()) ---
|
|
682
|
+
|
|
683
|
+
_g_tasks_all_done() {
|
|
684
|
+
local tasks="$G_DIR/tasks.md"
|
|
685
|
+
[ -f "$tasks" ] || { echo "tasks.md missing at $tasks"; return 1; }
|
|
686
|
+
grep -q '^- \[x\]' "$tasks" || { echo "tasks.md has no completed tasks"; return 1; }
|
|
687
|
+
if grep -q '^- \[ \]' "$tasks"; then
|
|
688
|
+
echo "Unfinished tasks:"; grep -n '^- \[ \]' "$tasks" || true; return 1
|
|
689
|
+
fi
|
|
690
|
+
return 0
|
|
691
|
+
}
|
|
692
|
+
_g_tasks_has_any() {
|
|
693
|
+
local tasks="$G_DIR/tasks.md"
|
|
694
|
+
[ -f "$tasks" ] && grep -q '^- \[' "$tasks"
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
# Resolve a project command: change yaml first, then repo-root config files.
|
|
698
|
+
_g_project_command() {
|
|
699
|
+
local field="$1" value
|
|
700
|
+
value=$(yaml_get "$G_YAML" "$field")
|
|
701
|
+
if [ -n "$value" ] && [ "$value" != "null" ]; then echo "$value"; return 0; fi
|
|
702
|
+
local cfg
|
|
703
|
+
for cfg in ".comate.yaml" "comate.yaml" ".comate.yml" "comate.yml"; do
|
|
704
|
+
if [ -f "$cfg" ]; then
|
|
705
|
+
value=$(yaml_get "$cfg" "$field")
|
|
706
|
+
if [ -n "$value" ] && [ "$value" != "null" ]; then echo "$value"; return 0; fi
|
|
707
|
+
fi
|
|
708
|
+
done
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
_g_is_windows_bash() {
|
|
712
|
+
case "$(uname -s 2>/dev/null || true)" in
|
|
713
|
+
MINGW*|MSYS*|CYGWIN*) return 0 ;;
|
|
714
|
+
*) return 1 ;;
|
|
715
|
+
esac
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
_g_run_cmd() { echo "+ $1" >&2; bash -lc "$1"; }
|
|
719
|
+
|
|
720
|
+
_g_build_passes() {
|
|
721
|
+
[ "${COMATE_SKIP_BUILD:-0}" = "1" ] && return 0
|
|
722
|
+
local cmd; cmd=$(_g_project_command build_command)
|
|
723
|
+
if [ -n "$cmd" ]; then _g_run_cmd "$cmd"; return $?; fi
|
|
724
|
+
if [ -f package.json ] && grep -q '"build"' package.json; then npm run build; return $?; fi
|
|
725
|
+
if [ -f pom.xml ]; then
|
|
726
|
+
if [ -x ./mvnw ]; then ./mvnw compile -q
|
|
727
|
+
elif _g_is_windows_bash && command -v mvn.cmd >/dev/null 2>&1; then mvn.cmd compile -q
|
|
728
|
+
else mvn compile -q; fi
|
|
729
|
+
return $?
|
|
730
|
+
fi
|
|
731
|
+
if [ -f Cargo.toml ]; then cargo build; return $?; fi
|
|
732
|
+
echo "no build_command configured and no known build tool detected"
|
|
733
|
+
echo "Next: set build_command in .comate.yaml, or skip with COMATE_SKIP_BUILD=1 (not recommended)."
|
|
734
|
+
return 1
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
_g_verify_passes() {
|
|
738
|
+
[ "${COMATE_SKIP_BUILD:-0}" = "1" ] && return 0
|
|
739
|
+
local cmd; cmd=$(_g_project_command verify_command)
|
|
740
|
+
if [ -n "$cmd" ]; then _g_run_cmd "$cmd"; return $?; fi
|
|
741
|
+
_g_build_passes
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
_g_isolation_selected() {
|
|
745
|
+
local v; v=$(yaml_get "$G_YAML" isolation)
|
|
746
|
+
case "$v" in branch|worktree) return 0 ;; esac
|
|
747
|
+
echo "isolation must be branch or worktree, got '${v:-null}'"
|
|
748
|
+
echo "Next: comate.sh state $G_CHANGE set isolation <branch|worktree>"
|
|
749
|
+
return 1
|
|
750
|
+
}
|
|
751
|
+
_g_build_mode_selected() {
|
|
752
|
+
local v; v=$(yaml_get "$G_YAML" build_mode)
|
|
753
|
+
case "$v" in subagent-driven-development|executing-plans|direct) return 0 ;; esac
|
|
754
|
+
echo "build_mode must be selected, got '${v:-null}'"
|
|
755
|
+
return 1
|
|
756
|
+
}
|
|
757
|
+
_g_build_mode_allowed() {
|
|
758
|
+
local workflow build_mode direct_override
|
|
759
|
+
workflow=$(yaml_get "$G_YAML" workflow)
|
|
760
|
+
build_mode=$(yaml_get "$G_YAML" build_mode)
|
|
761
|
+
direct_override=$(yaml_get "$G_YAML" direct_override)
|
|
762
|
+
[ "$build_mode" != "direct" ] && return 0
|
|
763
|
+
case "$workflow" in hotfix|tweak) return 0 ;; esac
|
|
764
|
+
[ "$direct_override" = "true" ] && return 0
|
|
765
|
+
echo "build_mode=direct only allowed for hotfix/tweak unless direct_override=true"
|
|
766
|
+
return 1
|
|
767
|
+
}
|
|
768
|
+
_g_tdd_mode_selected() {
|
|
769
|
+
local v; v=$(yaml_get "$G_YAML" tdd_mode)
|
|
770
|
+
case "$v" in tdd|direct) return 0 ;; esac
|
|
771
|
+
echo "tdd_mode must be tdd or direct, got '${v:-null}'"
|
|
772
|
+
echo "Next: comate.sh state $G_CHANGE set tdd_mode <tdd|direct>"
|
|
773
|
+
return 1
|
|
774
|
+
}
|
|
775
|
+
_g_verification_report_exists() {
|
|
776
|
+
local v; v=$(yaml_get "$G_YAML" verification_report)
|
|
777
|
+
[ -n "$v" ] && [ "$v" != "null" ] && [ -f "$v" ]
|
|
778
|
+
}
|
|
779
|
+
_g_branch_status_handled() {
|
|
780
|
+
[ "$(yaml_get "$G_YAML" branch_status)" = "handled" ]
|
|
781
|
+
}
|
|
782
|
+
_g_archived_is_true() {
|
|
783
|
+
[ "$(yaml_get "$G_YAML" archived)" = "true" ]
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
# source-tracking drift: knowledge-base files unchanged since they were pinned.
|
|
787
|
+
# conversation sources (no manifest) pass trivially.
|
|
788
|
+
_g_sources_not_drifted() {
|
|
789
|
+
local kind; kind=$(yaml_get "$G_YAML" source_kind)
|
|
790
|
+
[ "$kind" = "file" ] || return 0
|
|
791
|
+
local manifest; manifest=$(yaml_get "$G_YAML" source_manifest)
|
|
792
|
+
if [ -z "$manifest" ] || [ "$manifest" = "null" ]; then
|
|
793
|
+
echo "source_kind=file but source_manifest is null"; return 1
|
|
794
|
+
fi
|
|
795
|
+
local out rc
|
|
796
|
+
out=$(sources_drift "$manifest"); rc=$?
|
|
797
|
+
if [ "$rc" -ne 0 ]; then
|
|
798
|
+
[ -n "$out" ] && echo "$out"
|
|
799
|
+
echo "Next: review the knowledge-base change; rerun comate.sh sources $G_CHANGE track <file> if intended."
|
|
800
|
+
return 1
|
|
801
|
+
fi
|
|
802
|
+
return 0
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
# Handoff source set (design phase) — proposal/design/tasks + delta specs.
|
|
806
|
+
_g_handoff_sources() {
|
|
807
|
+
printf '%s\n' "$G_DIR/proposal.md" "$G_DIR/design.md" "$G_DIR/tasks.md"
|
|
808
|
+
[ -d "$G_DIR/specs" ] && find "$G_DIR/specs" -path '*/spec.md' -type f 2>/dev/null | sort
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
_g_design_handoff_valid() {
|
|
812
|
+
local context recorded actual md
|
|
813
|
+
context=$(yaml_get "$G_YAML" handoff_context)
|
|
814
|
+
recorded=$(yaml_get "$G_YAML" handoff_hash)
|
|
815
|
+
if [ -z "$context" ] || [ "$context" = "null" ]; then
|
|
816
|
+
echo "handoff_context missing; run: comate.sh handoff $G_CHANGE design --write"; return 1
|
|
817
|
+
fi
|
|
818
|
+
[ -s "$context" ] || { echo "handoff_context not a non-empty file: $context"; return 1; }
|
|
819
|
+
if [[ ! "$recorded" =~ ^[a-f0-9]{64}$ ]]; then
|
|
820
|
+
echo "handoff_hash missing or invalid: ${recorded:-null}"; return 1
|
|
821
|
+
fi
|
|
822
|
+
# Recompute over current sources; flag drift.
|
|
823
|
+
local files=()
|
|
824
|
+
while IFS= read -r f; do files+=("$f"); done < <(_g_handoff_sources)
|
|
825
|
+
actual=$(hash_files_manifest "${files[@]}")
|
|
826
|
+
if [ "$actual" != "$recorded" ]; then
|
|
827
|
+
echo "OpenSpec artifacts changed after handoff was generated."
|
|
828
|
+
echo " recorded: $recorded"
|
|
829
|
+
echo " actual: $actual"
|
|
830
|
+
echo "Next: rerun comate.sh handoff $G_CHANGE design --write"
|
|
831
|
+
return 1
|
|
832
|
+
fi
|
|
833
|
+
md="${context%.json}.md"
|
|
834
|
+
[ -s "$md" ] || { echo "design handoff markdown missing/empty: $md"; return 1; }
|
|
835
|
+
return 0
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
_g_design_handoff_traceable() {
|
|
839
|
+
local context md
|
|
840
|
+
context=$(yaml_get "$G_YAML" handoff_context)
|
|
841
|
+
[ -n "$context" ] && [ "$context" != "null" ] || { echo "handoff_context missing"; return 1; }
|
|
842
|
+
md="${context%.json}.md"
|
|
843
|
+
[ -s "$md" ] || { echo "handoff markdown missing/empty: $md"; return 1; }
|
|
844
|
+
grep -q '^Generated-by: comate$' "$md" || { echo "missing Generated-by marker"; return 1; }
|
|
845
|
+
grep -Eq '^- Mode: (compact|full)$' "$md" || { echo "missing Mode marker"; return 1; }
|
|
846
|
+
local f rc=0
|
|
847
|
+
while IFS= read -r f; do
|
|
848
|
+
[ -f "$f" ] || continue
|
|
849
|
+
grep -q "^- Source: $f$" "$md" || { echo "missing source reference: $f"; rc=1; }
|
|
850
|
+
grep -q "^- SHA256: $(hash_file "$f")$" "$md" || { echo "missing current sha256 for: $f"; rc=1; }
|
|
851
|
+
done < <(_g_handoff_sources)
|
|
852
|
+
return $rc
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
# Design Doc frontmatter helper.
|
|
856
|
+
_g_frontmatter_has() {
|
|
857
|
+
local file="$1" field="$2" expected="$3"
|
|
858
|
+
awk '
|
|
859
|
+
{ line=$0; sub(/^\357\273\277/, "", line) }
|
|
860
|
+
!in_fm && line == "---" { in_fm=1; next }
|
|
861
|
+
in_fm && line == "---" { exit }
|
|
862
|
+
in_fm { print line }
|
|
863
|
+
' "$file" | grep -Eq "^${field}: ['\"]?${expected}['\"]?[[:space:]]*$"
|
|
864
|
+
}
|
|
865
|
+
_g_design_doc_links_change() {
|
|
866
|
+
local dd; dd=$(yaml_get "$G_YAML" design_doc)
|
|
867
|
+
[ -n "$dd" ] && [ "$dd" != "null" ] && [ -s "$dd" ] || { echo "design_doc must point to an existing Design Doc"; return 1; }
|
|
868
|
+
_g_frontmatter_has "$dd" comate_change "$G_CHANGE" || { echo "Design Doc frontmatter missing comate_change: $G_CHANGE"; return 1; }
|
|
869
|
+
}
|
|
870
|
+
_g_design_doc_role() {
|
|
871
|
+
local dd; dd=$(yaml_get "$G_YAML" design_doc)
|
|
872
|
+
[ -n "$dd" ] && [ "$dd" != "null" ] && [ -s "$dd" ] && _g_frontmatter_has "$dd" role technical-design
|
|
873
|
+
}
|
|
874
|
+
_g_design_doc_canonical() {
|
|
875
|
+
local dd; dd=$(yaml_get "$G_YAML" design_doc)
|
|
876
|
+
[ -n "$dd" ] && [ "$dd" != "null" ] && [ -s "$dd" ] && _g_frontmatter_has "$dd" canonical_spec openspec
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
# impl-context drift: verify entry plan+tasks match build exit.
|
|
880
|
+
_g_impl_context_written() {
|
|
881
|
+
local ctx hash
|
|
882
|
+
ctx=$(yaml_get "$G_YAML" impl_context)
|
|
883
|
+
hash=$(yaml_get "$G_YAML" impl_hash)
|
|
884
|
+
if [ -z "$ctx" ] || [ "$ctx" = "null" ] || [ ! -f "$ctx" ]; then
|
|
885
|
+
echo "impl_context not written; run: comate.sh handoff $G_CHANGE build --write"
|
|
886
|
+
return 1
|
|
887
|
+
fi
|
|
888
|
+
if [[ ! "$hash" =~ ^[a-f0-9]{64}$ ]]; then
|
|
889
|
+
echo "impl_hash missing or invalid: ${hash:-null}"; return 1
|
|
890
|
+
fi
|
|
891
|
+
return 0
|
|
892
|
+
}
|
|
893
|
+
_g_impl_hash_matches() {
|
|
894
|
+
local recorded plan actual
|
|
895
|
+
recorded=$(yaml_get "$G_YAML" impl_hash)
|
|
896
|
+
if [ -z "$recorded" ] || [ "$recorded" = "null" ]; then
|
|
897
|
+
echo "impl_hash missing; run: comate.sh handoff $G_CHANGE build --write at end of build"; return 1
|
|
898
|
+
fi
|
|
899
|
+
plan=$(yaml_get "$G_YAML" plan)
|
|
900
|
+
local files=("$G_DIR/tasks.md")
|
|
901
|
+
[ -n "$plan" ] && [ "$plan" != "null" ] && [ -f "$plan" ] && files=("$plan" "$G_DIR/tasks.md")
|
|
902
|
+
actual=$(hash_files_manifest "${files[@]}")
|
|
903
|
+
if [ "$actual" != "$recorded" ]; then
|
|
904
|
+
echo "plan/tasks changed between build exit and verify entry."
|
|
905
|
+
echo " recorded: $recorded"
|
|
906
|
+
echo " actual: $actual"
|
|
907
|
+
echo "Next: investigate the edit; rerun build handoff only if the change is intended."
|
|
908
|
+
return 1
|
|
909
|
+
fi
|
|
910
|
+
return 0
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
guard() {
|
|
914
|
+
local name="$1" phase="${2:-}"
|
|
915
|
+
validate_change_name "$name"
|
|
916
|
+
validate_enum phase "$phase"
|
|
917
|
+
[ -n "$phase" ] || die "$COMATE_USAGE" "Usage: comate.sh guard <name> <phase> [--apply] [--json]"
|
|
918
|
+
|
|
919
|
+
G_CHANGE="$name"
|
|
920
|
+
G_DIR=$(comate_change_dir "$name")
|
|
921
|
+
G_YAML="$G_DIR/.comate.yaml"
|
|
922
|
+
|
|
923
|
+
# preflight: directory + state file + flat-yaml schema.
|
|
924
|
+
[ -d "$G_DIR" ] || die "$COMATE_CORRUPT" "FATAL: change directory not found: $G_DIR"
|
|
925
|
+
[ -f "$G_YAML" ] || die "$COMATE_CORRUPT" "FATAL: .comate.yaml not found in $G_DIR"
|
|
926
|
+
validate_flat_yaml "$G_YAML"
|
|
927
|
+
|
|
928
|
+
[ "$GUARD_JSON" -eq 0 ] && log_info "=== Guard: $phase ==="
|
|
929
|
+
|
|
930
|
+
case "$phase" in
|
|
931
|
+
open)
|
|
932
|
+
_guard_check "proposal.md non-empty" file_nonempty "$G_DIR/proposal.md"
|
|
933
|
+
_guard_check "design.md non-empty" file_nonempty "$G_DIR/design.md"
|
|
934
|
+
_guard_check "tasks.md non-empty" file_nonempty "$G_DIR/tasks.md"
|
|
935
|
+
_guard_check "tasks.md has at least one task" _g_tasks_has_any
|
|
936
|
+
;;
|
|
937
|
+
design)
|
|
938
|
+
_guard_check "proposal.md non-empty" file_nonempty "$G_DIR/proposal.md"
|
|
939
|
+
_guard_check "design.md non-empty" file_nonempty "$G_DIR/design.md"
|
|
940
|
+
_guard_check "tasks.md non-empty" file_nonempty "$G_DIR/tasks.md"
|
|
941
|
+
_guard_check "design handoff context valid (hash not drifted)" _g_design_handoff_valid
|
|
942
|
+
_guard_check "design handoff markdown traceable" _g_design_handoff_traceable
|
|
943
|
+
_guard_check "source files not drifted" _g_sources_not_drifted
|
|
944
|
+
local dd; dd=$(yaml_get "$G_YAML" design_doc)
|
|
945
|
+
if [ -n "$dd" ] && [ "$dd" != "null" ]; then
|
|
946
|
+
_guard_check "Design Doc exists ($dd)" file_nonempty "$dd"
|
|
947
|
+
_guard_check "Design Doc frontmatter links change" _g_design_doc_links_change
|
|
948
|
+
_guard_check "Design Doc declares technical-design role" _g_design_doc_role
|
|
949
|
+
_guard_check "Design Doc declares canonical_spec: openspec" _g_design_doc_canonical
|
|
950
|
+
else
|
|
951
|
+
[ "$GUARD_JSON" -eq 0 ] && log_warn " [WARN] no design_doc recorded"
|
|
952
|
+
fi
|
|
953
|
+
;;
|
|
954
|
+
build)
|
|
955
|
+
_guard_check "isolation selected" _g_isolation_selected
|
|
956
|
+
_guard_check "build_mode selected" _g_build_mode_selected
|
|
957
|
+
_guard_check "build_mode allowed for workflow" _g_build_mode_allowed
|
|
958
|
+
_guard_check "tdd_mode selected" _g_tdd_mode_selected
|
|
959
|
+
_guard_check "tasks.md all tasks checked" _g_tasks_all_done
|
|
960
|
+
_guard_check "proposal.md non-empty" file_nonempty "$G_DIR/proposal.md"
|
|
961
|
+
_guard_check "impl-context written" _g_impl_context_written
|
|
962
|
+
_guard_check "source files not drifted" _g_sources_not_drifted
|
|
963
|
+
_guard_check "build passes" _g_build_passes
|
|
964
|
+
;;
|
|
965
|
+
verify)
|
|
966
|
+
_guard_check "tasks.md all tasks checked" _g_tasks_all_done
|
|
967
|
+
_guard_check "verify command passes" _g_verify_passes
|
|
968
|
+
_guard_check "verification_report exists" _g_verification_report_exists
|
|
969
|
+
_guard_check "branch_status=handled" _g_branch_status_handled
|
|
970
|
+
_guard_check "impl_hash matches current plan+tasks" _g_impl_hash_matches
|
|
971
|
+
;;
|
|
972
|
+
archive)
|
|
973
|
+
_guard_check "archived is true" _g_archived_is_true
|
|
974
|
+
_guard_check "proposal.md non-empty" file_nonempty "$G_DIR/proposal.md"
|
|
975
|
+
_guard_check "tasks.md all tasks checked" _g_tasks_all_done
|
|
976
|
+
;;
|
|
977
|
+
esac
|
|
978
|
+
|
|
979
|
+
if [ "$GUARD_JSON" -eq 1 ]; then
|
|
980
|
+
local out='{"phase":"'"$phase"'","checks":['
|
|
981
|
+
local first=1 r cname cstatus
|
|
982
|
+
for r in "${GUARD_RESULTS[@]}"; do
|
|
983
|
+
cname="${r%|*}"; cstatus="${r#*|}"
|
|
984
|
+
[ "$first" -eq 0 ] && out+=','
|
|
985
|
+
first=0
|
|
986
|
+
out+='{"name":"'"$(json_escape "$cname")"'","status":"'"$cstatus"'"}'
|
|
987
|
+
done
|
|
988
|
+
out+='],"block":'"$([ "$GUARD_BLOCK" -eq 1 ] && echo true || echo false)"
|
|
989
|
+
out+=',"applied":false}'
|
|
990
|
+
printf '%s\n' "$out"
|
|
991
|
+
if [ "$GUARD_BLOCK" -eq 1 ]; then exit "$COMATE_BLOCK"; fi
|
|
992
|
+
# In JSON mode, still apply if requested (silent).
|
|
993
|
+
if [ "$GUARD_APPLY" -eq 1 ]; then _guard_apply "$phase" >/dev/null 2>&1 || true; fi
|
|
994
|
+
exit "$COMATE_OK"
|
|
995
|
+
fi
|
|
996
|
+
|
|
997
|
+
echo "" >&2
|
|
998
|
+
if [ "$GUARD_BLOCK" -eq 1 ]; then
|
|
999
|
+
log_fail "BLOCKED — fix failing checks before proceeding to next phase"
|
|
1000
|
+
exit "$COMATE_BLOCK"
|
|
1001
|
+
fi
|
|
1002
|
+
log_pass "ALL CHECKS PASSED — ready for next phase"
|
|
1003
|
+
if [ "$GUARD_APPLY" -eq 1 ]; then
|
|
1004
|
+
_guard_apply "$phase"
|
|
1005
|
+
fi
|
|
1006
|
+
exit "$COMATE_OK"
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
# _guard_apply <phase> — delegate to the matching transition.
|
|
1010
|
+
_guard_apply() {
|
|
1011
|
+
case "$1" in
|
|
1012
|
+
open) state_transition "$G_CHANGE" open-complete ;;
|
|
1013
|
+
design) state_transition "$G_CHANGE" design-complete ;;
|
|
1014
|
+
build) state_transition "$G_CHANGE" build-complete ;;
|
|
1015
|
+
verify) state_transition "$G_CHANGE" verify-pass ;;
|
|
1016
|
+
archive) : ;; # archive completion handled by the archive command
|
|
1017
|
+
esac
|
|
1018
|
+
local np; np=$(yaml_get "$G_YAML" phase)
|
|
1019
|
+
log_pass " [APPLY] .comate.yaml updated: phase=$np"
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
# =====================================================================
|
|
1023
|
+
# handoff — machine-owned context packages between phases
|
|
1024
|
+
# =====================================================================
|
|
1025
|
+
|
|
1026
|
+
# Shared: emit a traceable markdown excerpt for one file.
|
|
1027
|
+
_handoff_excerpt() {
|
|
1028
|
+
local file="$1" max_lines="$2" mode="$3" total
|
|
1029
|
+
total=$(wc -l < "$file" | tr -d ' ')
|
|
1030
|
+
echo "## $file"
|
|
1031
|
+
echo ""
|
|
1032
|
+
echo "- Source: $file"
|
|
1033
|
+
echo "- Lines: 1-$total"
|
|
1034
|
+
echo "- SHA256: $(hash_file "$file")"
|
|
1035
|
+
echo ""
|
|
1036
|
+
if [ "$mode" = "full" ] || [ "$total" -le "$max_lines" ]; then
|
|
1037
|
+
echo '```md'; cat "$file"; echo '```'
|
|
1038
|
+
else
|
|
1039
|
+
echo "[TRUNCATED]"; echo ""
|
|
1040
|
+
echo '```md'; sed -n "1,${max_lines}p" "$file"; echo '```'
|
|
1041
|
+
echo ""; echo "Full source: $file"
|
|
1042
|
+
fi
|
|
1043
|
+
echo ""
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
handoff() {
|
|
1047
|
+
local name="$1" phase="${2:-}" mode="${3:-}" full="${4:-}"
|
|
1048
|
+
validate_change_name "$name"
|
|
1049
|
+
local dir yaml
|
|
1050
|
+
dir=$(comate_change_dir "$name")
|
|
1051
|
+
yaml="$dir/.comate.yaml"
|
|
1052
|
+
[ -d "$dir" ] || die "$COMATE_CORRUPT" "ERROR: change directory not found: $dir"
|
|
1053
|
+
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "ERROR: .comate.yaml not found at $yaml"
|
|
1054
|
+
[ "$mode" = "--write" ] || die "$COMATE_USAGE" "Usage: comate.sh handoff <name> <design|build> --write [--full]"
|
|
1055
|
+
|
|
1056
|
+
case "$phase" in
|
|
1057
|
+
design) _handoff_design "$name" "$dir" "$yaml" "$full" ;;
|
|
1058
|
+
build) _handoff_build "$name" "$dir" "$yaml" ;;
|
|
1059
|
+
*) die "$COMATE_USAGE" "Usage: comate.sh handoff <name> <design|build> --write [--full]" ;;
|
|
1060
|
+
esac
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
_handoff_design() {
|
|
1064
|
+
local name="$1" dir="$2" yaml="$3" full="$4"
|
|
1065
|
+
local hmode="compact"
|
|
1066
|
+
case "$full" in
|
|
1067
|
+
"") hmode="compact" ;;
|
|
1068
|
+
--full) hmode="full" ;;
|
|
1069
|
+
*) die "$COMATE_USAGE" "Usage: comate.sh handoff <name> design --write [--full]" ;;
|
|
1070
|
+
esac
|
|
1071
|
+
[ "$(yaml_get "$yaml" phase)" = "design" ] || die "$COMATE_BLOCK" "ERROR: design handoff requires phase: design"
|
|
1072
|
+
|
|
1073
|
+
local f
|
|
1074
|
+
for f in proposal.md design.md tasks.md; do
|
|
1075
|
+
[ -s "$dir/$f" ] || die "$COMATE_BLOCK" "ERROR: required artifact missing/empty: $dir/$f"
|
|
1076
|
+
done
|
|
1077
|
+
|
|
1078
|
+
# Source set: proposal/design/tasks + delta specs.
|
|
1079
|
+
local files=("$dir/proposal.md" "$dir/design.md" "$dir/tasks.md")
|
|
1080
|
+
if [ -d "$dir/specs" ]; then
|
|
1081
|
+
while IFS= read -r s; do files+=("$s"); done < <(find "$dir/specs" -path '*/spec.md' -type f 2>/dev/null | sort)
|
|
1082
|
+
fi
|
|
1083
|
+
|
|
1084
|
+
local hdir="$dir/.comate/handoff"
|
|
1085
|
+
mkdir -p "$hdir"
|
|
1086
|
+
local cjson="$hdir/design-context.json" cmd="$hdir/design-context.md"
|
|
1087
|
+
local chash; chash=$(hash_files_manifest "${files[@]}")
|
|
1088
|
+
|
|
1089
|
+
# markdown
|
|
1090
|
+
{
|
|
1091
|
+
echo "# comate Design Handoff"
|
|
1092
|
+
echo ""
|
|
1093
|
+
echo "- Change: $name"
|
|
1094
|
+
echo "- Phase: design"
|
|
1095
|
+
echo "- Mode: $hmode"
|
|
1096
|
+
echo "- Context hash: $chash"
|
|
1097
|
+
echo ""
|
|
1098
|
+
echo "Generated-by: comate"
|
|
1099
|
+
echo ""
|
|
1100
|
+
echo "OpenSpec remains the canonical capability spec. This handoff is a deterministic, source-traceable context pack, not an agent-authored summary."
|
|
1101
|
+
echo ""
|
|
1102
|
+
for f in "${files[@]}"; do
|
|
1103
|
+
[ -f "$f" ] || continue
|
|
1104
|
+
_handoff_excerpt "$f" 80 "$hmode"
|
|
1105
|
+
done
|
|
1106
|
+
} > "$cmd"
|
|
1107
|
+
|
|
1108
|
+
# json
|
|
1109
|
+
{
|
|
1110
|
+
echo "{"
|
|
1111
|
+
echo " \"change\": \"$(json_escape "$name")\","
|
|
1112
|
+
echo " \"phase\": \"design\","
|
|
1113
|
+
echo " \"mode\": \"$hmode\","
|
|
1114
|
+
echo " \"canonical_spec\": \"openspec\","
|
|
1115
|
+
echo " \"generated_by\": \"comate\","
|
|
1116
|
+
echo " \"context_hash\": \"$chash\","
|
|
1117
|
+
echo " \"files\": ["
|
|
1118
|
+
local first=1
|
|
1119
|
+
for f in "${files[@]}"; do
|
|
1120
|
+
[ -f "$f" ] || continue
|
|
1121
|
+
[ "$first" -eq 0 ] && echo ","
|
|
1122
|
+
first=0
|
|
1123
|
+
printf ' { "path": "%s", "sha256": "%s" }' "$(json_escape "$f")" "$(hash_file "$f")"
|
|
1124
|
+
done
|
|
1125
|
+
echo ""
|
|
1126
|
+
echo " ]"
|
|
1127
|
+
echo "}"
|
|
1128
|
+
} > "$cjson"
|
|
1129
|
+
|
|
1130
|
+
yaml_set "$yaml" handoff_context "$cjson"
|
|
1131
|
+
yaml_set "$yaml" handoff_hash "$chash"
|
|
1132
|
+
log_pass "[HANDOFF] wrote $cjson"
|
|
1133
|
+
log_pass "[HANDOFF] wrote $cmd"
|
|
1134
|
+
log_pass "[HANDOFF] handoff_hash=$chash"
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
# build->verify handoff: hash plan + tasks.
|
|
1138
|
+
_handoff_build() {
|
|
1139
|
+
local name="$1" dir="$2" yaml="$3"
|
|
1140
|
+
[ "$(yaml_get "$yaml" phase)" = "build" ] || die "$COMATE_BLOCK" "ERROR: build handoff requires phase: build"
|
|
1141
|
+
[ -s "$dir/tasks.md" ] || die "$COMATE_BLOCK" "ERROR: tasks.md missing/empty: $dir/tasks.md"
|
|
1142
|
+
|
|
1143
|
+
local plan; plan=$(yaml_get "$yaml" plan)
|
|
1144
|
+
local files=("$dir/tasks.md")
|
|
1145
|
+
if [ -n "$plan" ] && [ "$plan" != "null" ] && [ -f "$plan" ]; then
|
|
1146
|
+
files=("$plan" "$dir/tasks.md")
|
|
1147
|
+
fi
|
|
1148
|
+
|
|
1149
|
+
local hdir="$dir/.comate/handoff"
|
|
1150
|
+
mkdir -p "$hdir"
|
|
1151
|
+
local cjson="$hdir/impl-context.json"
|
|
1152
|
+
local chash; chash=$(hash_files_manifest "${files[@]}")
|
|
1153
|
+
|
|
1154
|
+
{
|
|
1155
|
+
echo "{"
|
|
1156
|
+
echo " \"change\": \"$(json_escape "$name")\","
|
|
1157
|
+
echo " \"phase\": \"build\","
|
|
1158
|
+
echo " \"generated_by\": \"comate\","
|
|
1159
|
+
echo " \"impl_hash\": \"$chash\","
|
|
1160
|
+
echo " \"files\": ["
|
|
1161
|
+
local first=1 f
|
|
1162
|
+
for f in "${files[@]}"; do
|
|
1163
|
+
[ -f "$f" ] || continue
|
|
1164
|
+
[ "$first" -eq 0 ] && echo ","
|
|
1165
|
+
first=0
|
|
1166
|
+
printf ' { "path": "%s", "sha256": "%s" }' "$(json_escape "$f")" "$(hash_file "$f")"
|
|
1167
|
+
done
|
|
1168
|
+
echo ""
|
|
1169
|
+
echo " ]"
|
|
1170
|
+
echo "}"
|
|
1171
|
+
} > "$cjson"
|
|
1172
|
+
|
|
1173
|
+
yaml_set "$yaml" impl_context "$cjson"
|
|
1174
|
+
yaml_set "$yaml" impl_hash "$chash"
|
|
1175
|
+
log_pass "[HANDOFF] wrote $cjson"
|
|
1176
|
+
log_pass "[HANDOFF] impl_hash=$chash"
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
# =====================================================================
|
|
1180
|
+
# sources — knowledge-base provenance for a change
|
|
1181
|
+
# =====================================================================
|
|
1182
|
+
|
|
1183
|
+
# _sources_pin_ref <yaml> — echo the commit ref to pin a tracked file to:
|
|
1184
|
+
# the change's base_ref, falling back to current HEAD, else "null".
|
|
1185
|
+
_sources_pin_ref() {
|
|
1186
|
+
local yaml="$1" ref
|
|
1187
|
+
ref=$(yaml_get "$yaml" base_ref)
|
|
1188
|
+
if [ -z "$ref" ] || [ "$ref" = "null" ]; then
|
|
1189
|
+
ref=$(git rev-parse --short HEAD 2>/dev/null || echo "null")
|
|
1190
|
+
elif git rev-parse --git-dir >/dev/null 2>&1; then
|
|
1191
|
+
ref=$(git rev-parse --short "$ref" 2>/dev/null || echo "$ref")
|
|
1192
|
+
fi
|
|
1193
|
+
echo "$ref"
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
# sources <name> track <file...> [--note <text>] — record knowledge-base files
|
|
1197
|
+
# as the source of this change. Out-of-range files are skipped (caller stays on
|
|
1198
|
+
# conversation). Re-tracking a path replaces its prior record.
|
|
1199
|
+
sources_track() {
|
|
1200
|
+
local name="$1"; shift
|
|
1201
|
+
validate_change_name "$name"
|
|
1202
|
+
local yaml; yaml=$(require_yaml "$name")
|
|
1203
|
+
local manifest; manifest=$(sources_manifest_for "$name")
|
|
1204
|
+
|
|
1205
|
+
local note="" files=()
|
|
1206
|
+
while [ $# -gt 0 ]; do
|
|
1207
|
+
case "$1" in
|
|
1208
|
+
--note) note="${2:-}"; shift 2 || shift ;;
|
|
1209
|
+
*) files+=("$1"); shift ;;
|
|
1210
|
+
esac
|
|
1211
|
+
done
|
|
1212
|
+
[ "${#files[@]}" -gt 0 ] || die "$COMATE_USAGE" "Usage: comate.sh sources <name> track <file...> [--note <text>]"
|
|
1213
|
+
|
|
1214
|
+
local ref; ref=$(_sources_pin_ref "$yaml")
|
|
1215
|
+
mkdir -p "$(dirname "$manifest")"
|
|
1216
|
+
[ -f "$manifest" ] || : > "$manifest"
|
|
1217
|
+
|
|
1218
|
+
local tracked=0 skipped=0 f rel
|
|
1219
|
+
for f in "${files[@]}"; do
|
|
1220
|
+
if rel=$(validate_kb_path "$f"); then
|
|
1221
|
+
_sources_remove_path "$manifest" "$rel"
|
|
1222
|
+
{
|
|
1223
|
+
[ -s "$manifest" ] && printf -- '---\n'
|
|
1224
|
+
printf 'path:%s\n' "$rel"
|
|
1225
|
+
printf 'sha256:%s\n' "$(hash_file "$rel")"
|
|
1226
|
+
printf 'ref:%s\n' "$ref"
|
|
1227
|
+
[ -n "$note" ] && printf 'note:%s\n' "$note"
|
|
1228
|
+
} >> "$manifest"
|
|
1229
|
+
log_pass "[SOURCES] tracked $rel"
|
|
1230
|
+
tracked=$((tracked + 1))
|
|
1231
|
+
else
|
|
1232
|
+
log_warn "[SOURCES] not in $(comate_kb_root)/ or unreadable, skipped: $f"
|
|
1233
|
+
skipped=$((skipped + 1))
|
|
1234
|
+
fi
|
|
1235
|
+
done
|
|
1236
|
+
|
|
1237
|
+
if [ "$tracked" -gt 0 ]; then
|
|
1238
|
+
yaml_set "$yaml" source_kind file
|
|
1239
|
+
yaml_set "$yaml" source_manifest "$manifest"
|
|
1240
|
+
log_pass "[SOURCES] source_kind=file, source_manifest=$manifest"
|
|
1241
|
+
else
|
|
1242
|
+
log_warn "[SOURCES] no file tracked; source_kind stays conversation"
|
|
1243
|
+
fi
|
|
1244
|
+
[ "$skipped" -eq 0 ] || return "$COMATE_OK"
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
# _sources_remove_path <manifest> <path> — drop the record block for one path.
|
|
1248
|
+
_sources_remove_path() {
|
|
1249
|
+
local manifest="$1" target="$2" tmp
|
|
1250
|
+
[ -f "$manifest" ] || return 0
|
|
1251
|
+
tmp=$(mktemp "${TMPDIR:-/tmp}/comate.XXXXXX") || die "$COMATE_ENV" "ERROR: mktemp failed"
|
|
1252
|
+
awk -v target="$target" '
|
|
1253
|
+
function flush() {
|
|
1254
|
+
if (block != "" && keep) printf "%s", block
|
|
1255
|
+
block=""; keep=1
|
|
1256
|
+
}
|
|
1257
|
+
BEGIN { keep=1 }
|
|
1258
|
+
/^---$/ { flush(); next }
|
|
1259
|
+
/^path:/ { if ($0 == "path:" target) keep=0 }
|
|
1260
|
+
{ block = block $0 "\n" }
|
|
1261
|
+
END { flush() }
|
|
1262
|
+
' "$manifest" > "$tmp"
|
|
1263
|
+
# Collapse any leading blank lines, then normalise record separators.
|
|
1264
|
+
awk 'NF || NR>1 {print}' "$tmp" > "$manifest"
|
|
1265
|
+
rm -f "$tmp"
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
# sources <name> check — drift check; conversation skips. Mirrors guard codes.
|
|
1269
|
+
sources_check() {
|
|
1270
|
+
local name="$1"
|
|
1271
|
+
validate_change_name "$name"
|
|
1272
|
+
local yaml; yaml=$(require_yaml "$name")
|
|
1273
|
+
local kind; kind=$(yaml_get "$yaml" source_kind)
|
|
1274
|
+
if [ "$kind" != "file" ]; then
|
|
1275
|
+
log_info "[SOURCES] source_kind=$kind — no source files to check"
|
|
1276
|
+
exit "$COMATE_OK"
|
|
1277
|
+
fi
|
|
1278
|
+
local manifest; manifest=$(yaml_get "$yaml" source_manifest)
|
|
1279
|
+
if [ -z "$manifest" ] || [ "$manifest" = "null" ]; then
|
|
1280
|
+
die "$COMATE_CORRUPT" "ERROR: source_kind=file but source_manifest is null"
|
|
1281
|
+
fi
|
|
1282
|
+
local out rc=0
|
|
1283
|
+
out=$(sources_drift "$manifest") || rc=$?
|
|
1284
|
+
[ -n "$out" ] && printf '%s\n' "$out" >&2
|
|
1285
|
+
if [ "$rc" -eq 0 ]; then
|
|
1286
|
+
log_pass "[SOURCES] no drift ($manifest)"
|
|
1287
|
+
fi
|
|
1288
|
+
exit "$rc"
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
# sources <name> list — print the manifest.
|
|
1292
|
+
sources_list() {
|
|
1293
|
+
local name="$1"
|
|
1294
|
+
validate_change_name "$name"
|
|
1295
|
+
local manifest; manifest=$(sources_manifest_for "$name")
|
|
1296
|
+
if [ -f "$manifest" ]; then cat "$manifest"; else echo "no source manifest for $name"; fi
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
# sources find <file> — reverse lookup: which changes reference this file.
|
|
1300
|
+
sources_find() {
|
|
1301
|
+
local target="$1"
|
|
1302
|
+
[ -n "$target" ] || die "$COMATE_USAGE" "Usage: comate.sh sources find <file>"
|
|
1303
|
+
local rel="$target"
|
|
1304
|
+
if r=$(validate_kb_path "$target" 2>/dev/null); then rel="$r"; fi
|
|
1305
|
+
local any=0 m chg
|
|
1306
|
+
while IFS= read -r m; do
|
|
1307
|
+
if grep -q "^path:$rel$" "$m" 2>/dev/null; then
|
|
1308
|
+
chg=$(basename "$(dirname "$(dirname "$m")")")
|
|
1309
|
+
echo "$chg ($m)"
|
|
1310
|
+
any=1
|
|
1311
|
+
fi
|
|
1312
|
+
done < <(find openspec/changes -path '*/.comate/sources.manifest' -type f 2>/dev/null | sort)
|
|
1313
|
+
[ "$any" -eq 0 ] && echo "no change references: $rel"
|
|
1314
|
+
return 0
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
# =====================================================================
|
|
1318
|
+
# archive
|
|
1319
|
+
# =====================================================================
|
|
1320
|
+
|
|
1321
|
+
archive() {
|
|
1322
|
+
local name="$1" dry="${2:-}"
|
|
1323
|
+
validate_change_name "$name"
|
|
1324
|
+
local dry_run=0
|
|
1325
|
+
[ "$dry" = "--dry-run" ] && dry_run=1
|
|
1326
|
+
|
|
1327
|
+
local dir="openspec/changes/$name"
|
|
1328
|
+
local yaml="$dir/.comate.yaml"
|
|
1329
|
+
local today archive_name archive_dir
|
|
1330
|
+
today=$(today)
|
|
1331
|
+
archive_name="${today}-${name}"
|
|
1332
|
+
archive_dir="openspec/changes/archive/${archive_name}"
|
|
1333
|
+
|
|
1334
|
+
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "FATAL: .comate.yaml not found in $dir"
|
|
1335
|
+
validate_flat_yaml "$yaml"
|
|
1336
|
+
|
|
1337
|
+
local phase verify_result archived design_doc plan
|
|
1338
|
+
phase=$(yaml_get "$yaml" phase)
|
|
1339
|
+
verify_result=$(yaml_get "$yaml" verify_result)
|
|
1340
|
+
archived=$(yaml_get "$yaml" archived)
|
|
1341
|
+
design_doc=$(yaml_get "$yaml" design_doc)
|
|
1342
|
+
plan=$(yaml_get "$yaml" plan)
|
|
1343
|
+
|
|
1344
|
+
[ "$phase" = "archive" ] || die "$COMATE_BLOCK" "FATAL: phase is '$phase', expected 'archive'"
|
|
1345
|
+
[ "$verify_result" = "pass" ] || die "$COMATE_BLOCK" "FATAL: verify_result is '$verify_result', expected 'pass'"
|
|
1346
|
+
[ "$archived" != "true" ] || die "$COMATE_BLOCK" "FATAL: change already archived"
|
|
1347
|
+
[ ! -d "$archive_dir" ] || die "$COMATE_BLOCK" "FATAL: archive target already exists: $archive_dir"
|
|
1348
|
+
|
|
1349
|
+
local steps_ok=0 steps_total=0
|
|
1350
|
+
_step_ok() { log_pass " [OK] $1"; steps_ok=$((steps_ok+1)); steps_total=$((steps_total+1)); }
|
|
1351
|
+
_step_dry() { log_warn " [DRY-RUN] $1"; steps_ok=$((steps_ok+1)); steps_total=$((steps_total+1)); }
|
|
1352
|
+
_step_fail(){ log_fail " [FAIL] $1"; steps_total=$((steps_total+1)); }
|
|
1353
|
+
|
|
1354
|
+
log_info "=== comate Archive: $name ==="
|
|
1355
|
+
_step_ok "Entry state verified (phase=archive, verify_result=pass)"
|
|
1356
|
+
|
|
1357
|
+
# OpenSpec owns delta-spec merge semantics. Do not copy change specs over
|
|
1358
|
+
# main specs directly; delta specs may contain ADDED/MODIFIED sections rather
|
|
1359
|
+
# than a complete canonical spec.
|
|
1360
|
+
local has_delta_specs=0
|
|
1361
|
+
if [ -d "$dir/specs" ] && find "$dir/specs" -path '*/spec.md' -type f -print -quit 2>/dev/null | grep -q .; then
|
|
1362
|
+
has_delta_specs=1
|
|
1363
|
+
fi
|
|
1364
|
+
|
|
1365
|
+
# Annotate frontmatter (design doc + plan), and rewrite any body references to
|
|
1366
|
+
# the change's pre-archive OpenSpec path so they survive the move (class B).
|
|
1367
|
+
# NOTE: design_doc/plan live under docs/superpowers and do NOT move with the
|
|
1368
|
+
# change. Annotation runs AFTER openspec has moved the change, using the REAL
|
|
1369
|
+
# archive name discovered from disk — comate must not guess the dir name, as a
|
|
1370
|
+
# locally-computed date can skew vs openspec's across a UTC/local midnight.
|
|
1371
|
+
local old_prefix="openspec/changes/$name/"
|
|
1372
|
+
local new_prefix="" # set to real archive path after the move
|
|
1373
|
+
local ann_archive_name="" # set to real archive name after the move
|
|
1374
|
+
_annotate() {
|
|
1375
|
+
local file="$1" extra="$2"
|
|
1376
|
+
[ -f "$file" ] || return 0
|
|
1377
|
+
if [ "$dry_run" -eq 1 ]; then _step_dry "Would annotate: $file"; return 0; fi
|
|
1378
|
+
local tmp; tmp=$(mktemp)
|
|
1379
|
+
if head -1 "$file" | grep -q '^---'; then
|
|
1380
|
+
awk -v archive="$ann_archive_name" -v extra="$extra" \
|
|
1381
|
+
-v oldp="$old_prefix" -v newp="$new_prefix" '
|
|
1382
|
+
/^archived-with:/ { next }
|
|
1383
|
+
NR==1 && /^---/ { print; next }
|
|
1384
|
+
/^---/ && NR>1 && fm==0 { print "archived-with: " archive; if (extra != "") print extra; fm=1; print; next }
|
|
1385
|
+
fm==1 { gsub(oldp, newp) } 1
|
|
1386
|
+
' "$file" > "$tmp"
|
|
1387
|
+
else
|
|
1388
|
+
{
|
|
1389
|
+
echo "---"; echo "archived-with: $ann_archive_name"; [ -n "$extra" ] && echo "$extra"; echo "status: final"; echo "---"
|
|
1390
|
+
sed "s#$old_prefix#$new_prefix#g" "$file"
|
|
1391
|
+
} > "$tmp"
|
|
1392
|
+
fi
|
|
1393
|
+
mv "$tmp" "$file"
|
|
1394
|
+
_step_ok "Annotated: $file"
|
|
1395
|
+
}
|
|
1396
|
+
# Dry-run previews annotation up front (real name not needed for the message).
|
|
1397
|
+
if [ "$dry_run" -eq 1 ]; then
|
|
1398
|
+
[ -n "$design_doc" ] && [ "$design_doc" != "null" ] && _annotate "$design_doc" "status: final"
|
|
1399
|
+
[ -n "$plan" ] && [ "$plan" != "null" ] && _annotate "$plan" ""
|
|
1400
|
+
fi
|
|
1401
|
+
|
|
1402
|
+
# Delegate spec updates and change movement to OpenSpec.
|
|
1403
|
+
if [ "$dry_run" -eq 1 ]; then
|
|
1404
|
+
if [ "$has_delta_specs" -eq 1 ]; then
|
|
1405
|
+
_step_dry "Would run: openspec archive $name -y"
|
|
1406
|
+
else
|
|
1407
|
+
_step_dry "Would run: openspec archive $name -y --skip-specs"
|
|
1408
|
+
fi
|
|
1409
|
+
else
|
|
1410
|
+
command -v openspec >/dev/null 2>&1 || die "$COMATE_ENV" "FATAL: openspec CLI not found; install OpenSpec before archiving"
|
|
1411
|
+
if [ "$has_delta_specs" -eq 1 ]; then
|
|
1412
|
+
openspec archive "$name" -y
|
|
1413
|
+
_step_ok "OpenSpec archived change and updated main specs"
|
|
1414
|
+
else
|
|
1415
|
+
openspec archive "$name" -y --skip-specs
|
|
1416
|
+
_step_ok "OpenSpec archived change (no delta specs)"
|
|
1417
|
+
fi
|
|
1418
|
+
fi
|
|
1419
|
+
|
|
1420
|
+
# Mark archived via transition. OpenSpec normally uses YYYY-MM-DD-<name>,
|
|
1421
|
+
# but fall back to locating the moved state file if that ever changes.
|
|
1422
|
+
if [ "$dry_run" -eq 1 ]; then
|
|
1423
|
+
_step_dry "Would set archived: true"
|
|
1424
|
+
else
|
|
1425
|
+
local moved_archive_name="$archive_name"
|
|
1426
|
+
if [ ! -f "$archive_dir/.comate.yaml" ]; then
|
|
1427
|
+
local found
|
|
1428
|
+
found=$(find "openspec/changes/archive" -maxdepth 2 -path "*/.comate.yaml" -type f 2>/dev/null | sed 's#^\./##' | grep "/${name}/.comate.yaml$\|-${name}/.comate.yaml$" | sort | tail -1 || true)
|
|
1429
|
+
if [ -n "$found" ]; then
|
|
1430
|
+
moved_archive_name=$(basename "$(dirname "$found")")
|
|
1431
|
+
archive_dir=$(dirname "$found")
|
|
1432
|
+
fi
|
|
1433
|
+
fi
|
|
1434
|
+
if [ -f "$archive_dir/.comate.yaml" ]; then
|
|
1435
|
+
state_transition "$moved_archive_name" archived >/dev/null
|
|
1436
|
+
_step_ok "archived: true"
|
|
1437
|
+
# Now that the change has moved and the REAL archive name is known, annotate
|
|
1438
|
+
# design doc + plan (they stay under docs/superpowers; openspec never moved
|
|
1439
|
+
# them). Using the discovered name avoids any local-vs-openspec date skew.
|
|
1440
|
+
ann_archive_name="$moved_archive_name"
|
|
1441
|
+
new_prefix="$archive_dir/"
|
|
1442
|
+
[ -n "$design_doc" ] && [ "$design_doc" != "null" ] && _annotate "$design_doc" "status: final"
|
|
1443
|
+
[ -n "$plan" ] && [ "$plan" != "null" ] && _annotate "$plan" ""
|
|
1444
|
+
# Rewrite self-pointers that moved with the change (class A): fields under
|
|
1445
|
+
# the change's own .comate/ dir become stale after the move. design_doc /
|
|
1446
|
+
# plan live under docs/superpowers and do not move, so leave them alone.
|
|
1447
|
+
local moved_yaml="$archive_dir/.comate.yaml" k v
|
|
1448
|
+
local actual_old="openspec/changes/$name/"
|
|
1449
|
+
local actual_new="$archive_dir/"
|
|
1450
|
+
for k in source_manifest handoff_context impl_context; do
|
|
1451
|
+
v=$(yaml_get "$moved_yaml" "$k")
|
|
1452
|
+
case "$v" in
|
|
1453
|
+
"$actual_old"*)
|
|
1454
|
+
yaml_set "$moved_yaml" "$k" "$actual_new${v#"$actual_old"}"
|
|
1455
|
+
_step_ok "Repointed $k -> archive path"
|
|
1456
|
+
;;
|
|
1457
|
+
esac
|
|
1458
|
+
done
|
|
1459
|
+
else
|
|
1460
|
+
_step_fail "archived: true (.comate.yaml not found after move)"
|
|
1461
|
+
fi
|
|
1462
|
+
fi
|
|
1463
|
+
|
|
1464
|
+
echo "" >&2
|
|
1465
|
+
if [ "$dry_run" -eq 1 ]; then
|
|
1466
|
+
log_warn "Dry run complete. $steps_ok/$steps_total steps would succeed."
|
|
1467
|
+
else
|
|
1468
|
+
log_pass "Archive complete. $steps_ok/$steps_total steps succeeded."
|
|
1469
|
+
fi
|
|
1470
|
+
if [ "$steps_ok" -lt "$steps_total" ]; then exit "$COMATE_BLOCK"; fi
|
|
1471
|
+
exit "$COMATE_OK"
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
# =====================================================================
|
|
1475
|
+
# status — list active changes + next command
|
|
1476
|
+
# =====================================================================
|
|
1477
|
+
|
|
1478
|
+
_next_command() {
|
|
1479
|
+
case "$1" in
|
|
1480
|
+
open) echo "/comate-open" ;;
|
|
1481
|
+
design) echo "/comate-design" ;;
|
|
1482
|
+
build) echo "/comate-build" ;;
|
|
1483
|
+
verify) echo "/comate-verify" ;;
|
|
1484
|
+
archive) echo "/comate-archive" ;;
|
|
1485
|
+
*) echo "" ;;
|
|
1486
|
+
esac
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
status() {
|
|
1490
|
+
local base="${1:-.}"
|
|
1491
|
+
local changes_dir="$base/openspec/changes"
|
|
1492
|
+
if [ ! -d "$changes_dir" ]; then
|
|
1493
|
+
echo "No active changes."
|
|
1494
|
+
return 0
|
|
1495
|
+
fi
|
|
1496
|
+
local any=0 d name yaml phase workflow build_mode vr dd plan total done tasks_file
|
|
1497
|
+
for d in "$changes_dir"/*/; do
|
|
1498
|
+
[ -d "$d" ] || continue
|
|
1499
|
+
name=$(basename "$d")
|
|
1500
|
+
[ "$name" = "archive" ] && continue
|
|
1501
|
+
yaml="$d/.comate.yaml"
|
|
1502
|
+
[ -f "$yaml" ] || continue
|
|
1503
|
+
[ "$(yaml_get "$yaml" archived)" = "true" ] && continue
|
|
1504
|
+
any=1
|
|
1505
|
+
phase=$(yaml_get "$yaml" phase)
|
|
1506
|
+
workflow=$(yaml_get "$yaml" workflow)
|
|
1507
|
+
build_mode=$(yaml_get "$yaml" build_mode)
|
|
1508
|
+
vr=$(yaml_get "$yaml" verify_result)
|
|
1509
|
+
dd=$(yaml_get "$yaml" design_doc)
|
|
1510
|
+
plan=$(yaml_get "$yaml" plan)
|
|
1511
|
+
tasks_file="$d/tasks.md"
|
|
1512
|
+
total=0; done=0
|
|
1513
|
+
if [ -f "$tasks_file" ]; then
|
|
1514
|
+
total=$(grep -c '^- \[' "$tasks_file" 2>/dev/null || echo 0)
|
|
1515
|
+
done=$(grep -c '^- \[x\]' "$tasks_file" 2>/dev/null || echo 0)
|
|
1516
|
+
fi
|
|
1517
|
+
echo "• $name [phase: $phase, $done/$total tasks]"
|
|
1518
|
+
echo " workflow: $workflow | build_mode: $build_mode"
|
|
1519
|
+
[ -n "$dd" ] && [ "$dd" != "null" ] && echo " design: $dd"
|
|
1520
|
+
[ -n "$plan" ] && [ "$plan" != "null" ] && echo " plan: $plan"
|
|
1521
|
+
[ "$phase" = "verify" ] && echo " verify_result: $vr"
|
|
1522
|
+
echo " next: $(_next_command "$phase")"
|
|
1523
|
+
echo ""
|
|
1524
|
+
done
|
|
1525
|
+
[ "$any" -eq 0 ] && echo "No active changes."
|
|
1526
|
+
return 0
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
# =====================================================================
|
|
1530
|
+
# doctor — prerequisite self-check
|
|
1531
|
+
# =====================================================================
|
|
1532
|
+
|
|
1533
|
+
doctor() {
|
|
1534
|
+
local fail=0 warn=0
|
|
1535
|
+
_d_pass() { log_pass " ✓ $1"; }
|
|
1536
|
+
_d_warn() { log_warn " ⚠ $1"; warn=$((warn+1)); }
|
|
1537
|
+
_d_fail() { log_fail " ✗ $1"; fail=$((fail+1)); }
|
|
1538
|
+
|
|
1539
|
+
log_info "comate doctor"
|
|
1540
|
+
|
|
1541
|
+
# bash version >= 4 (need associative arrays).
|
|
1542
|
+
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
1543
|
+
_d_pass "bash ${BASH_VERSION} (>= 4)"
|
|
1544
|
+
else
|
|
1545
|
+
_d_fail "bash ${BASH_VERSION:-unknown} — comate needs bash >= 4 (associative arrays)"
|
|
1546
|
+
fi
|
|
1547
|
+
|
|
1548
|
+
# git.
|
|
1549
|
+
if command -v git >/dev/null 2>&1; then _d_pass "git present"; else _d_fail "git not found — install git"; fi
|
|
1550
|
+
|
|
1551
|
+
# sha256 tool.
|
|
1552
|
+
if command -v sha256sum >/dev/null 2>&1; then _d_pass "sha256sum present"
|
|
1553
|
+
elif command -v shasum >/dev/null 2>&1; then _d_pass "shasum present"
|
|
1554
|
+
else _d_fail "neither sha256sum nor shasum found — install coreutils or perl shasum"; fi
|
|
1555
|
+
|
|
1556
|
+
# openspec CLI.
|
|
1557
|
+
if command -v openspec >/dev/null 2>&1; then
|
|
1558
|
+
_d_pass "openspec CLI present ($(openspec --version 2>/dev/null | head -1 || echo installed))"
|
|
1559
|
+
else
|
|
1560
|
+
_d_warn "openspec CLI not found — install: npm install -g @fission-ai/openspec@latest"
|
|
1561
|
+
fi
|
|
1562
|
+
|
|
1563
|
+
# Superpowers skill directory — check known skill roots only (bounded; an
|
|
1564
|
+
# unbounded $HOME scan is too slow on large home directories).
|
|
1565
|
+
# Plugin roots matter too: Claude Code installs superpowers as a plugin under
|
|
1566
|
+
# plugins/{cache,data}/, not under skills/. maxdepth 3 keeps deeper
|
|
1567
|
+
# marketplace docs dirs (e.g. marketplaces/*/docs/superpowers) from matching.
|
|
1568
|
+
local sp_found=0 root
|
|
1569
|
+
for root in \
|
|
1570
|
+
"./.claude/skills" "./.cursor/skills" "./.codex/skills" \
|
|
1571
|
+
"$HOME/.claude/skills" "$HOME/.config/skills" "$HOME/.cursor/skills" "$HOME/.codex/skills" "$HOME/.gemini/skills" \
|
|
1572
|
+
"./.claude/plugins" "$HOME/.claude/plugins"; do
|
|
1573
|
+
if [ -d "$root" ] && find "$root" -maxdepth 3 -type d -name 'superpowers*' -print -quit 2>/dev/null | grep -q .; then
|
|
1574
|
+
sp_found=1; break
|
|
1575
|
+
fi
|
|
1576
|
+
done
|
|
1577
|
+
if [ "$sp_found" -eq 1 ]; then
|
|
1578
|
+
_d_pass "Superpowers skills found"
|
|
1579
|
+
else
|
|
1580
|
+
_d_warn "Superpowers skills not found in known roots — install manually (see reference/prerequisites.md)"
|
|
1581
|
+
fi
|
|
1582
|
+
|
|
1583
|
+
# comate's own files present.
|
|
1584
|
+
local missing=0 f
|
|
1585
|
+
for f in comate-lib.sh comate.sh; do
|
|
1586
|
+
[ -f "$SCRIPT_DIR/$f" ] || { _d_fail "missing comate file: scripts/$f"; missing=1; }
|
|
1587
|
+
done
|
|
1588
|
+
[ "$missing" -eq 0 ] && _d_pass "comate scripts complete"
|
|
1589
|
+
|
|
1590
|
+
# LF line endings on .sh (Windows-critical).
|
|
1591
|
+
local crlf=0
|
|
1592
|
+
for f in "$SCRIPT_DIR"/*.sh; do
|
|
1593
|
+
if grep -lq $'\r' "$f" 2>/dev/null; then _d_warn "CRLF line endings in $(basename "$f") — convert to LF"; crlf=1; fi
|
|
1594
|
+
done
|
|
1595
|
+
[ "$crlf" -eq 0 ] && _d_pass ".sh files use LF line endings"
|
|
1596
|
+
|
|
1597
|
+
echo "" >&2
|
|
1598
|
+
if [ "$fail" -gt 0 ]; then
|
|
1599
|
+
die "$COMATE_ENV" "$fail failure(s), $warn warning(s) — fix failures before using comate"
|
|
1600
|
+
fi
|
|
1601
|
+
log_pass "0 failures, $warn warning(s)"
|
|
1602
|
+
exit "$COMATE_OK"
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
# =====================================================================
|
|
1606
|
+
# main router
|
|
1607
|
+
# =====================================================================
|
|
1608
|
+
|
|
1609
|
+
CMD="${1:-}"
|
|
1610
|
+
shift || true
|
|
1611
|
+
|
|
1612
|
+
case "$CMD" in
|
|
1613
|
+
state)
|
|
1614
|
+
NAME="${1:-}"; SUB="${2:-}"
|
|
1615
|
+
[ -n "$NAME" ] && [ -n "$SUB" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
1616
|
+
shift 2 || true
|
|
1617
|
+
case "$SUB" in
|
|
1618
|
+
init) state_init "$NAME" "${1:-}" ;;
|
|
1619
|
+
get) state_get "$NAME" "${1:-}" ;;
|
|
1620
|
+
set) state_set "$NAME" "${1:-}" "${2:-}" ;;
|
|
1621
|
+
transition) state_transition "$NAME" "${1:-}" ;;
|
|
1622
|
+
check)
|
|
1623
|
+
CHECK_JSON=0
|
|
1624
|
+
[ "${2:-}" = "--json" ] && CHECK_JSON=1
|
|
1625
|
+
state_check "$NAME" "${1:-}"
|
|
1626
|
+
;;
|
|
1627
|
+
recover) state_recover "$NAME" "${1:-}" ;;
|
|
1628
|
+
scale) state_scale "$NAME" ;;
|
|
1629
|
+
validate) state_validate "$NAME" ;;
|
|
1630
|
+
*) die "$COMATE_USAGE" "Unknown state subcommand: $SUB" ;;
|
|
1631
|
+
esac
|
|
1632
|
+
;;
|
|
1633
|
+
guard)
|
|
1634
|
+
NAME="${1:-}"; PHASE="${2:-}"
|
|
1635
|
+
[ -n "$NAME" ] && [ -n "$PHASE" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
1636
|
+
GUARD_APPLY=0; GUARD_JSON=0
|
|
1637
|
+
for arg in "${@:3}"; do
|
|
1638
|
+
case "$arg" in
|
|
1639
|
+
--apply) GUARD_APPLY=1 ;;
|
|
1640
|
+
--json) GUARD_JSON=1 ;;
|
|
1641
|
+
esac
|
|
1642
|
+
done
|
|
1643
|
+
guard "$NAME" "$PHASE"
|
|
1644
|
+
;;
|
|
1645
|
+
handoff)
|
|
1646
|
+
handoff "${1:-}" "${2:-}" "${3:-}" "${4:-}"
|
|
1647
|
+
;;
|
|
1648
|
+
sources)
|
|
1649
|
+
NAME="${1:-}"; SUB="${2:-}"
|
|
1650
|
+
if [ "$NAME" = "find" ]; then
|
|
1651
|
+
sources_find "${2:-}"
|
|
1652
|
+
else
|
|
1653
|
+
[ -n "$NAME" ] && [ -n "$SUB" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
1654
|
+
shift 2 || true
|
|
1655
|
+
case "$SUB" in
|
|
1656
|
+
track) sources_track "$NAME" "$@" ;;
|
|
1657
|
+
check) sources_check "$NAME" ;;
|
|
1658
|
+
list) sources_list "$NAME" ;;
|
|
1659
|
+
*) die "$COMATE_USAGE" "Unknown sources subcommand: $SUB" ;;
|
|
1660
|
+
esac
|
|
1661
|
+
fi
|
|
1662
|
+
;;
|
|
1663
|
+
archive)
|
|
1664
|
+
[ -n "${1:-}" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
1665
|
+
archive "$1" "${2:-}"
|
|
1666
|
+
;;
|
|
1667
|
+
status)
|
|
1668
|
+
status "${1:-.}"
|
|
1669
|
+
;;
|
|
1670
|
+
doctor)
|
|
1671
|
+
doctor
|
|
1672
|
+
;;
|
|
1673
|
+
""|-h|--help|help)
|
|
1674
|
+
usage_top
|
|
1675
|
+
exit "$COMATE_OK"
|
|
1676
|
+
;;
|
|
1677
|
+
*)
|
|
1678
|
+
usage_top
|
|
1679
|
+
exit "$COMATE_USAGE"
|
|
1680
|
+
;;
|
|
1681
|
+
esac
|