@house365/comate 0.10.3 → 1.0.2-next.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 -21
- package/README.md +104 -64
- package/bin/cli.mjs +197 -190
- package/bin/doctor.mjs +29 -41
- package/bin/init-run.mjs +310 -368
- package/bin/init.mjs +53 -71
- package/bin/postinstall.mjs +56 -56
- package/docs/CHANGELOG-fix-simplification.md +145 -0
- package/docs/COMATE_BETA.md +90 -0
- package/docs/COMATE_MANUAL.md +239 -665
- package/docs/superpowers/plans/2026-10-14-review-fixes.md +48 -0
- package/docs/superpowers/plans/2026-10-14-workflow-quality.md +48 -0
- package/package.json +54 -55
- package/skills/comate/SKILL.md +42 -325
- package/skills/comate/reference/archive.md +50 -0
- package/skills/comate/reference/build.md +67 -0
- package/skills/comate/reference/fix.md +150 -0
- package/skills/comate/reference/open.md +162 -0
- package/skills/comate/reference/verify.md +123 -0
- package/skills/comate/scaffold/comate/README.md +18 -0
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/architecture/README.md +5 -6
- package/skills/comate/scaffold/comate/comate/config.json +10 -0
- package/skills/comate/scaffold/comate/knowledge-base/README.md +42 -0
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/integrations/_TEMPLATE/README.md +2 -2
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/integrations/_TEMPLATE/api-snapshot.md +47 -47
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/requirements/_templates/PRD.md +4 -4
- package/skills/comate/scripts/comate.mjs +147 -0
- package/skills/comate/scripts/lib/acceptance.mjs +112 -0
- package/skills/comate/scripts/lib/brief.mjs +50 -0
- package/skills/comate/scripts/lib/checks.mjs +178 -0
- package/skills/comate/scripts/lib/cmd-archive.mjs +152 -0
- package/skills/comate/scripts/lib/cmd-decide.mjs +116 -0
- package/skills/comate/scripts/lib/cmd-init.mjs +75 -0
- package/skills/comate/scripts/lib/cmd-loop.mjs +273 -0
- package/skills/comate/scripts/lib/cmd-open.mjs +161 -0
- package/skills/comate/scripts/lib/cmd-status.mjs +121 -0
- package/skills/comate/scripts/lib/common.mjs +85 -0
- package/skills/comate/scripts/lib/docs.mjs +45 -0
- package/skills/comate/scripts/lib/finish.mjs +108 -0
- package/skills/comate/scripts/lib/git.mjs +112 -0
- package/skills/comate/scripts/lib/md.mjs +85 -0
- package/skills/comate/scripts/lib/next.mjs +217 -0
- package/skills/comate/scripts/lib/paths.mjs +96 -0
- package/skills/comate/scripts/lib/render.mjs +46 -0
- package/skills/comate/scripts/lib/spec.mjs +106 -0
- package/skills/comate/scripts/lib/state.mjs +128 -0
- package/skills/comate/scripts/lib/tasks.mjs +53 -0
- package/skills/comate/scripts/lib/templates.mjs +58 -0
- package/skills/comate/scripts/lib/util.mjs +108 -0
- package/skills/comate-kb-integration/SKILL.md +128 -128
- package/skills/comate-kb-integration/scripts/fetch_rendered_doc.py +84 -84
- package/bin/cleanup-duplicate-superpowers.mjs +0 -119
- package/docs/COMATE_BEGINNER_GUIDE.md +0 -188
- package/docs/COMATE_INIT_DIRECTORY_GUIDE.md +0 -201
- package/docs/superpowers/plans/2026-09-13-fast-workflow.md +0 -62
- package/skills/comate/reference/decision-points.md +0 -103
- package/skills/comate/reference/dirty-worktree.md +0 -58
- package/skills/comate/reference/prerequisites.md +0 -89
- package/skills/comate/scripts/comate-lib.sh +0 -559
- package/skills/comate/scripts/comate.sh +0 -2147
- package/skills/comate/tests/comate.bats +0 -1019
- package/skills/comate/tests/run.sh +0 -101
- package/skills/comate-archive/SKILL.md +0 -63
- package/skills/comate-build/SKILL.md +0 -278
- package/skills/comate-design/SKILL.md +0 -178
- package/skills/comate-fix/SKILL.md +0 -195
- package/skills/comate-gate/SKILL.md +0 -319
- package/skills/comate-gate/scaffold/docs/README.md +0 -36
- package/skills/comate-gate/scaffold/docs/knowledge-base/README.md +0 -47
- package/skills/comate-grill/SKILL.md +0 -185
- package/skills/comate-open/SKILL.md +0 -240
- package/skills/comate-verify/SKILL.md +0 -212
- /package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/integrations/README.md +0 -0
|
@@ -1,2147 +0,0 @@
|
|
|
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
|
-
# gate <prd> review — create/refresh gate.yaml next to the PRD, pin hash
|
|
21
|
-
# gate <prd> set <field> <value> — write score | blocking_open | gate_status | …
|
|
22
|
-
# gate <prd> pass [--tbd] — pass clean gate or explicitly accepted TBD blockers
|
|
23
|
-
# gate <prd> spawned <change> — record a change fanned out from this PRD
|
|
24
|
-
# gate <prd> fanout-status [--json] — report mapped/spawned fanout progress
|
|
25
|
-
# gate <prd> status [--json] — print gate status (computes stale on hash drift)
|
|
26
|
-
# gate <prd> check — precondition guard before spawning a change
|
|
27
|
-
# archive <name> [--dry-run] — sync delta specs + annotate + move + mark archived
|
|
28
|
-
# status [path] — list active changes + next command
|
|
29
|
-
# doctor — prerequisite self-check
|
|
30
|
-
#
|
|
31
|
-
# Exit codes: 0 OK | 1 BLOCK | 2 USAGE | 3 ENV | 4 CORRUPT
|
|
32
|
-
|
|
33
|
-
set -euo pipefail
|
|
34
|
-
|
|
35
|
-
# The scripts intentionally stay compatible with macOS's system Bash 3.2.
|
|
36
|
-
# Do not add Bash 4-only features (notably associative arrays) here.
|
|
37
|
-
|
|
38
|
-
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")" && pwd)"
|
|
39
|
-
# shellcheck source=comate-lib.sh
|
|
40
|
-
. "$SCRIPT_DIR/comate-lib.sh"
|
|
41
|
-
|
|
42
|
-
usage_top() {
|
|
43
|
-
cat >&2 <<'EOF'
|
|
44
|
-
Usage: comate.sh <command> ...
|
|
45
|
-
|
|
46
|
-
state <name> init <workflow> full | fix
|
|
47
|
-
state <name> get <field>
|
|
48
|
-
state <name> set <field> <value>
|
|
49
|
-
state <name> transition <event> open-complete|design-complete|build-complete|verify-pass|verify-fail|fix-upgrade|archived
|
|
50
|
-
state <name> check <phase> [--json]
|
|
51
|
-
state <name> recover <phase>
|
|
52
|
-
state <name> scale
|
|
53
|
-
state <name> validate
|
|
54
|
-
guard <name> <phase> [--apply] [--json]
|
|
55
|
-
handoff <name> design --write [--full]
|
|
56
|
-
handoff <name> build --write
|
|
57
|
-
sources <name> track <file...> [--note <text>]
|
|
58
|
-
sources <name> check
|
|
59
|
-
sources <name> list
|
|
60
|
-
sources find <file>
|
|
61
|
-
dependency <name> archived verify an exact archived change exists
|
|
62
|
-
gate <prd> review create/refresh gate.yaml next to PRD, pin hash
|
|
63
|
-
gate <prd> set <field> <value> score | blocking_open | gate_status | …
|
|
64
|
-
gate <prd> pass [--tbd] pass: zero blockers; --tbd: accepted blockers
|
|
65
|
-
gate <prd> spawned <change> record a change fanned out from this PRD
|
|
66
|
-
gate <prd> fanout-status [--json] report mapped/spawned fanout progress
|
|
67
|
-
gate <prd> status [--json]
|
|
68
|
-
gate <prd> check precondition guard before spawning a change
|
|
69
|
-
archive <name> [--dry-run]
|
|
70
|
-
status [path]
|
|
71
|
-
doctor
|
|
72
|
-
EOF
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
require_yaml() {
|
|
76
|
-
# $1 = change name; ensures .comate.yaml exists, echoes its path.
|
|
77
|
-
local name="$1" yaml
|
|
78
|
-
yaml=$(comate_yaml_for "$name")
|
|
79
|
-
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "ERROR: .comate.yaml not found at $yaml"
|
|
80
|
-
echo "$yaml"
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
today() { date +%Y-%m-%d; }
|
|
84
|
-
|
|
85
|
-
# =====================================================================
|
|
86
|
-
# state
|
|
87
|
-
# =====================================================================
|
|
88
|
-
|
|
89
|
-
state_init() {
|
|
90
|
-
local name="$1" workflow="${2:-}"
|
|
91
|
-
validate_change_name "$name"
|
|
92
|
-
validate_enum workflow "$workflow"
|
|
93
|
-
[ -n "$workflow" ] || die "$COMATE_USAGE" "Usage: comate.sh state <name> init <full|fix>"
|
|
94
|
-
|
|
95
|
-
local dir yaml
|
|
96
|
-
dir=$(comate_change_dir "$name")
|
|
97
|
-
yaml="$dir/.comate.yaml"
|
|
98
|
-
[ -f "$yaml" ] && die "$COMATE_USAGE" "ERROR: .comate.yaml already exists at $yaml"
|
|
99
|
-
mkdir -p "$dir"
|
|
100
|
-
|
|
101
|
-
# base_ref = current HEAD.
|
|
102
|
-
local base_ref="null"
|
|
103
|
-
if git rev-parse --verify HEAD >/dev/null 2>&1; then
|
|
104
|
-
base_ref=$(git rev-parse HEAD 2>/dev/null || echo "null")
|
|
105
|
-
fi
|
|
106
|
-
# base_branch = detected default branch, fallback main/master.
|
|
107
|
-
local base_branch="null" ref
|
|
108
|
-
if ref=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null); then
|
|
109
|
-
base_branch="${ref#refs/remotes/origin/}"
|
|
110
|
-
elif git show-ref --verify --quiet refs/heads/main 2>/dev/null; then
|
|
111
|
-
base_branch="main"
|
|
112
|
-
elif git show-ref --verify --quiet refs/heads/master 2>/dev/null; then
|
|
113
|
-
base_branch="master"
|
|
114
|
-
fi
|
|
115
|
-
|
|
116
|
-
# Workflow defaults: full leaves decisions null; presets prefill.
|
|
117
|
-
local build_mode="null" tdd_mode="null" isolation="null" verify_mode="null"
|
|
118
|
-
case "$workflow" in
|
|
119
|
-
fix)
|
|
120
|
-
build_mode="direct"; tdd_mode="direct"; isolation="branch"; verify_mode="light" ;;
|
|
121
|
-
esac
|
|
122
|
-
|
|
123
|
-
# Write full canonical file directly (FIELD_ORDER kept in sync with lib).
|
|
124
|
-
cat > "$yaml" <<EOF
|
|
125
|
-
# comate state — flat key:value only, no nesting
|
|
126
|
-
schema_version: $COMATE_SCHEMA_VERSION
|
|
127
|
-
workflow: $workflow
|
|
128
|
-
phase: open
|
|
129
|
-
phase_step: null
|
|
130
|
-
phase_step_data: null
|
|
131
|
-
build_mode: $build_mode
|
|
132
|
-
tdd_mode: $tdd_mode
|
|
133
|
-
isolation: $isolation
|
|
134
|
-
verify_mode: $verify_mode
|
|
135
|
-
verify_result: pending
|
|
136
|
-
design_doc: null
|
|
137
|
-
plan: null
|
|
138
|
-
verification_report: null
|
|
139
|
-
branch_status: pending
|
|
140
|
-
handoff_context: null
|
|
141
|
-
handoff_hash: null
|
|
142
|
-
impl_context: null
|
|
143
|
-
impl_hash: null
|
|
144
|
-
direct_override: false
|
|
145
|
-
base_ref: $base_ref
|
|
146
|
-
base_branch: $base_branch
|
|
147
|
-
source_kind: conversation
|
|
148
|
-
source_manifest: null
|
|
149
|
-
build_command: null
|
|
150
|
-
verify_command: null
|
|
151
|
-
created_at: $(today)
|
|
152
|
-
verified_at: null
|
|
153
|
-
archived: false
|
|
154
|
-
EOF
|
|
155
|
-
|
|
156
|
-
validate_flat_yaml "$yaml"
|
|
157
|
-
log_pass "[INIT] $yaml (workflow=$workflow, base_branch=$base_branch)"
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
state_get() {
|
|
161
|
-
local name="$1" field="${2:-}"
|
|
162
|
-
validate_change_name "$name"
|
|
163
|
-
[ -n "$field" ] || die "$COMATE_USAGE" "Usage: comate.sh state <name> get <field>"
|
|
164
|
-
local yaml; yaml=$(require_yaml "$name")
|
|
165
|
-
yaml_get "$yaml" "$field"
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
state_set() {
|
|
169
|
-
local name="$1" field="${2:-}" value="${3:-}"
|
|
170
|
-
validate_change_name "$name"
|
|
171
|
-
[ -n "$field" ] || die "$COMATE_USAGE" "Usage: comate.sh state <name> set <field> <value>"
|
|
172
|
-
if [ "$field" = "phase" ]; then
|
|
173
|
-
log_warn "WARNING: setting 'phase' directly bypasses the state machine; prefer 'state transition'."
|
|
174
|
-
fi
|
|
175
|
-
local yaml; yaml=$(require_yaml "$name")
|
|
176
|
-
yaml_set "$yaml" "$field" "$value"
|
|
177
|
-
log_pass "[SET] $field=$value"
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
# --- transition guards (see IMPLEMENTATION.md §3 guard table) ---
|
|
181
|
-
|
|
182
|
-
_require_phase() {
|
|
183
|
-
local yaml="$1" expected="$2" actual
|
|
184
|
-
actual=$(yaml_get "$yaml" phase)
|
|
185
|
-
[ "$actual" = "$expected" ] || \
|
|
186
|
-
die "$COMATE_BLOCK" "ERROR: expected phase '$expected', got '${actual:-null}'"
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
_require_build_decisions() {
|
|
190
|
-
local yaml="$1"
|
|
191
|
-
local workflow build_mode tdd_mode isolation direct_override
|
|
192
|
-
workflow=$(yaml_get "$yaml" workflow)
|
|
193
|
-
build_mode=$(yaml_get "$yaml" build_mode)
|
|
194
|
-
tdd_mode=$(yaml_get "$yaml" tdd_mode)
|
|
195
|
-
isolation=$(yaml_get "$yaml" isolation)
|
|
196
|
-
direct_override=$(yaml_get "$yaml" direct_override)
|
|
197
|
-
case "$isolation" in
|
|
198
|
-
branch|worktree) ;;
|
|
199
|
-
*) die "$COMATE_BLOCK" "ERROR: isolation must be branch or worktree, got '${isolation:-null}'" ;;
|
|
200
|
-
esac
|
|
201
|
-
case "$build_mode" in
|
|
202
|
-
subagent-driven-development|executing-plans|direct) ;;
|
|
203
|
-
*) die "$COMATE_BLOCK" "ERROR: build_mode must be selected, got '${build_mode:-null}'" ;;
|
|
204
|
-
esac
|
|
205
|
-
if [ "$build_mode" = "direct" ] && [ "$workflow" != "fix" ] && [ "$direct_override" != "true" ]; then
|
|
206
|
-
die "$COMATE_BLOCK" "ERROR: build_mode=direct is only allowed for fix unless direct_override=true"
|
|
207
|
-
fi
|
|
208
|
-
case "$tdd_mode" in
|
|
209
|
-
tdd|direct) ;;
|
|
210
|
-
*) die "$COMATE_BLOCK" "ERROR: tdd_mode must be tdd or direct, got '${tdd_mode:-null}'" ;;
|
|
211
|
-
esac
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
_require_verify_evidence() {
|
|
215
|
-
local yaml="$1" report branch_status
|
|
216
|
-
report=$(yaml_get "$yaml" verification_report)
|
|
217
|
-
branch_status=$(yaml_get "$yaml" branch_status)
|
|
218
|
-
if [ -z "$report" ] || [ "$report" = "null" ] || [ ! -f "$report" ]; then
|
|
219
|
-
die "$COMATE_BLOCK" "ERROR: verification_report must point to an existing file (got '${report:-null}')"
|
|
220
|
-
fi
|
|
221
|
-
[ "$branch_status" = "handled" ] || \
|
|
222
|
-
die "$COMATE_BLOCK" "ERROR: branch_status must be 'handled', got '${branch_status:-null}'"
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
state_transition() {
|
|
226
|
-
local name="$1" event="${2:-}"
|
|
227
|
-
validate_change_name "$name"
|
|
228
|
-
validate_enum_event() {
|
|
229
|
-
case "$1" in
|
|
230
|
-
open-complete|design-complete|build-complete|verify-pass|verify-fail|fix-upgrade|archived) return 0 ;;
|
|
231
|
-
*) die "$COMATE_USAGE" "ERROR: unknown event '$1'" ;;
|
|
232
|
-
esac
|
|
233
|
-
}
|
|
234
|
-
[ -n "$event" ] || die "$COMATE_USAGE" "Usage: comate.sh state <name> transition <event>"
|
|
235
|
-
validate_enum_event "$event"
|
|
236
|
-
local yaml; yaml=$(require_yaml "$name")
|
|
237
|
-
validate_flat_yaml "$yaml"
|
|
238
|
-
|
|
239
|
-
# phase_step is local to the current phase. Carrying it into the next phase
|
|
240
|
-
# makes identical step numbers ambiguous and can skip mandatory work.
|
|
241
|
-
_clear_phase_progress() {
|
|
242
|
-
yaml_set "$yaml" phase_step null
|
|
243
|
-
yaml_set "$yaml" phase_step_data null
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
case "$event" in
|
|
247
|
-
open-complete)
|
|
248
|
-
_require_phase "$yaml" open
|
|
249
|
-
local workflow; workflow=$(yaml_get "$yaml" workflow)
|
|
250
|
-
if [ "$workflow" = "full" ]; then
|
|
251
|
-
yaml_set "$yaml" phase design
|
|
252
|
-
else
|
|
253
|
-
yaml_set "$yaml" phase build
|
|
254
|
-
fi
|
|
255
|
-
_clear_phase_progress
|
|
256
|
-
;;
|
|
257
|
-
design-complete)
|
|
258
|
-
_require_phase "$yaml" design
|
|
259
|
-
yaml_set "$yaml" phase build
|
|
260
|
-
_clear_phase_progress
|
|
261
|
-
;;
|
|
262
|
-
build-complete)
|
|
263
|
-
_require_phase "$yaml" build
|
|
264
|
-
_require_build_decisions "$yaml"
|
|
265
|
-
yaml_set "$yaml" phase verify
|
|
266
|
-
yaml_set "$yaml" verify_result pending
|
|
267
|
-
yaml_set "$yaml" verification_report null
|
|
268
|
-
yaml_set "$yaml" branch_status pending
|
|
269
|
-
_clear_phase_progress
|
|
270
|
-
;;
|
|
271
|
-
verify-pass)
|
|
272
|
-
_require_phase "$yaml" verify
|
|
273
|
-
_require_verify_evidence "$yaml"
|
|
274
|
-
yaml_set "$yaml" verify_result pass
|
|
275
|
-
yaml_set "$yaml" phase archive
|
|
276
|
-
yaml_set "$yaml" verified_at "$(today)"
|
|
277
|
-
_clear_phase_progress
|
|
278
|
-
;;
|
|
279
|
-
verify-fail)
|
|
280
|
-
_require_phase "$yaml" verify
|
|
281
|
-
# The failure is captured in the report; once routed back to build the
|
|
282
|
-
# active verification state must be pending or the orchestrator loops.
|
|
283
|
-
yaml_set "$yaml" verify_result pending
|
|
284
|
-
yaml_set "$yaml" phase build
|
|
285
|
-
yaml_set "$yaml" verification_report null
|
|
286
|
-
yaml_set "$yaml" branch_status pending
|
|
287
|
-
_clear_phase_progress
|
|
288
|
-
;;
|
|
289
|
-
fix-upgrade)
|
|
290
|
-
[ "$(yaml_get "$yaml" workflow)" = "fix" ] || \
|
|
291
|
-
die "$COMATE_BLOCK" "ERROR: fix-upgrade requires workflow=fix"
|
|
292
|
-
case "$(yaml_get "$yaml" phase)" in
|
|
293
|
-
open|build) ;;
|
|
294
|
-
*) die "$COMATE_BLOCK" "ERROR: fix-upgrade is only valid from open or build" ;;
|
|
295
|
-
esac
|
|
296
|
-
yaml_set "$yaml" workflow full
|
|
297
|
-
yaml_set "$yaml" phase design
|
|
298
|
-
yaml_set "$yaml" build_mode null
|
|
299
|
-
yaml_set "$yaml" tdd_mode null
|
|
300
|
-
yaml_set "$yaml" isolation null
|
|
301
|
-
yaml_set "$yaml" verify_mode null
|
|
302
|
-
yaml_set "$yaml" direct_override false
|
|
303
|
-
yaml_set "$yaml" verify_result pending
|
|
304
|
-
yaml_set "$yaml" verification_report null
|
|
305
|
-
yaml_set "$yaml" branch_status pending
|
|
306
|
-
_clear_phase_progress
|
|
307
|
-
;;
|
|
308
|
-
archived)
|
|
309
|
-
_require_phase "$yaml" archive
|
|
310
|
-
yaml_set "$yaml" archived true
|
|
311
|
-
;;
|
|
312
|
-
esac
|
|
313
|
-
log_pass "[TRANSITION] $event"
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
# --- validate (schema + flat-yaml) ---
|
|
317
|
-
|
|
318
|
-
state_validate() {
|
|
319
|
-
local name="$1"
|
|
320
|
-
validate_change_name "$name"
|
|
321
|
-
local yaml; yaml=$(require_yaml "$name")
|
|
322
|
-
log_info "[VALIDATE] $yaml"
|
|
323
|
-
|
|
324
|
-
validate_flat_yaml "$yaml" # dies code 4 on nesting/lists/block scalars
|
|
325
|
-
|
|
326
|
-
local errors=0 warnings=0
|
|
327
|
-
# schema_version present and known.
|
|
328
|
-
local sv; sv=$(yaml_get "$yaml" schema_version)
|
|
329
|
-
if [ -z "$sv" ] || [ "$sv" = "null" ]; then
|
|
330
|
-
log_warn " WARN: schema_version missing"; warnings=$((warnings+1))
|
|
331
|
-
elif [ "$sv" != "$COMATE_SCHEMA_VERSION" ]; then
|
|
332
|
-
log_warn " WARN: schema_version=$sv (this tool writes $COMATE_SCHEMA_VERSION)"; warnings=$((warnings+1))
|
|
333
|
-
fi
|
|
334
|
-
|
|
335
|
-
# Required fields present.
|
|
336
|
-
local required="workflow phase verify_result archived"
|
|
337
|
-
local f
|
|
338
|
-
for f in $required; do
|
|
339
|
-
if ! yaml_keys "$yaml" | grep -qx "$f"; then
|
|
340
|
-
log_fail " FAIL: missing required field '$f'"; errors=$((errors+1))
|
|
341
|
-
fi
|
|
342
|
-
done
|
|
343
|
-
|
|
344
|
-
# Enum validity (null/empty allowed by validate_enum, but it dies on bad — so check inline).
|
|
345
|
-
local key legal v ok w
|
|
346
|
-
for key in $(enum_keys); do
|
|
347
|
-
v=$(yaml_get "$yaml" "$key")
|
|
348
|
-
[ -z "$v" ] && continue
|
|
349
|
-
[ "$v" = "null" ] && continue
|
|
350
|
-
legal=$(enum_values "$key")
|
|
351
|
-
ok=0
|
|
352
|
-
for w in $legal; do
|
|
353
|
-
if [ "$v" = "$w" ]; then ok=1; break; fi
|
|
354
|
-
done
|
|
355
|
-
if [ "$ok" -eq 0 ]; then
|
|
356
|
-
log_fail " FAIL: $key='$v' invalid (expected: $legal)"; errors=$((errors+1))
|
|
357
|
-
fi
|
|
358
|
-
done
|
|
359
|
-
|
|
360
|
-
# Path fields point to existing files when set.
|
|
361
|
-
for f in design_doc plan verification_report handoff_context impl_context; do
|
|
362
|
-
v=$(yaml_get "$yaml" "$f")
|
|
363
|
-
if [ -n "$v" ] && [ "$v" != "null" ] && [ ! -f "$v" ]; then
|
|
364
|
-
log_fail " FAIL: $f='$v' does not exist on disk"; errors=$((errors+1))
|
|
365
|
-
fi
|
|
366
|
-
done
|
|
367
|
-
|
|
368
|
-
# Hash fields are 64 hex when set.
|
|
369
|
-
for f in handoff_hash impl_hash; do
|
|
370
|
-
v=$(yaml_get "$yaml" "$f")
|
|
371
|
-
if [ -n "$v" ] && [ "$v" != "null" ] && [[ ! "$v" =~ ^[a-f0-9]{64}$ ]]; then
|
|
372
|
-
log_fail " FAIL: $f='$v' is not a sha256 hex digest"; errors=$((errors+1))
|
|
373
|
-
fi
|
|
374
|
-
done
|
|
375
|
-
|
|
376
|
-
# Unknown keys -> warning.
|
|
377
|
-
local known=" ${FIELD_ORDER[*]} "
|
|
378
|
-
while IFS= read -r key; do
|
|
379
|
-
[ -z "$key" ] && continue
|
|
380
|
-
case "$known" in
|
|
381
|
-
*" $key "*) ;;
|
|
382
|
-
*) log_warn " WARN: unknown field '$key'"; warnings=$((warnings+1)) ;;
|
|
383
|
-
esac
|
|
384
|
-
done < <(yaml_keys "$yaml")
|
|
385
|
-
|
|
386
|
-
if [ "$errors" -gt 0 ]; then
|
|
387
|
-
die "$COMATE_CORRUPT" "$errors error(s), $warnings warning(s) — validation FAILED"
|
|
388
|
-
fi
|
|
389
|
-
log_pass "0 errors, $warnings warning(s) — validation PASSED"
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
# --- check (entry preconditions for a phase) ---
|
|
393
|
-
|
|
394
|
-
CHECK_JSON=0
|
|
395
|
-
declare -a CHECK_RESULTS=() # "name|status"
|
|
396
|
-
CHECK_BLOCK=0
|
|
397
|
-
|
|
398
|
-
_chk() {
|
|
399
|
-
# $1 = name, $2 = 0(pass)/1(fail). Always returns 0 (set -e safe).
|
|
400
|
-
local name="$1" rc="$2"
|
|
401
|
-
if [ "$rc" -eq 0 ]; then
|
|
402
|
-
CHECK_RESULTS+=("$name|pass")
|
|
403
|
-
if [ "$CHECK_JSON" -eq 0 ]; then log_pass " [PASS] $name"; fi
|
|
404
|
-
else
|
|
405
|
-
CHECK_RESULTS+=("$name|fail")
|
|
406
|
-
CHECK_BLOCK=1
|
|
407
|
-
if [ "$CHECK_JSON" -eq 0 ]; then log_fail " [FAIL] $name"; fi
|
|
408
|
-
fi
|
|
409
|
-
return 0
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
_chk_yaml_is() {
|
|
413
|
-
local yaml="$1" field="$2" expected="$3" actual rc=0
|
|
414
|
-
actual=$(yaml_get "$yaml" "$field")
|
|
415
|
-
[ "$actual" = "$expected" ] || rc=1
|
|
416
|
-
_chk "$field=$expected (got ${actual:-null})" "$rc"
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
_chk_yaml_empty() {
|
|
420
|
-
local yaml="$1" field="$2" v rc=0
|
|
421
|
-
v=$(yaml_get "$yaml" "$field")
|
|
422
|
-
{ [ -z "$v" ] || [ "$v" = "null" ]; } || rc=1
|
|
423
|
-
_chk "$field empty/null" "$rc"
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
_chk_nonempty() {
|
|
427
|
-
local rc=0
|
|
428
|
-
file_nonempty "$2" || rc=1
|
|
429
|
-
_chk "$1 non-empty" "$rc"
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
state_check() {
|
|
433
|
-
local name="$1" phase="${2:-}"
|
|
434
|
-
validate_change_name "$name"
|
|
435
|
-
validate_enum phase "$phase"
|
|
436
|
-
[ -n "$phase" ] || die "$COMATE_USAGE" "Usage: comate.sh state <name> check <phase> [--json]"
|
|
437
|
-
local dir yaml
|
|
438
|
-
dir=$(comate_change_dir "$name")
|
|
439
|
-
yaml="$dir/.comate.yaml"
|
|
440
|
-
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "ERROR: .comate.yaml not found at $yaml"
|
|
441
|
-
|
|
442
|
-
case "$phase" in
|
|
443
|
-
open)
|
|
444
|
-
_chk ".comate.yaml exists" 0
|
|
445
|
-
_chk_yaml_is "$yaml" phase open
|
|
446
|
-
;;
|
|
447
|
-
design)
|
|
448
|
-
_chk_yaml_is "$yaml" phase design
|
|
449
|
-
_chk_yaml_is "$yaml" workflow full
|
|
450
|
-
_chk_yaml_empty "$yaml" design_doc
|
|
451
|
-
_chk_nonempty "proposal.md" "$dir/proposal.md"
|
|
452
|
-
_chk_nonempty "design.md" "$dir/design.md"
|
|
453
|
-
_chk_nonempty "tasks.md" "$dir/tasks.md"
|
|
454
|
-
;;
|
|
455
|
-
build)
|
|
456
|
-
_chk_yaml_is "$yaml" phase build
|
|
457
|
-
local workflow; workflow=$(yaml_get "$yaml" workflow)
|
|
458
|
-
if [ "$workflow" = "full" ]; then
|
|
459
|
-
local dd; dd=$(yaml_get "$yaml" design_doc)
|
|
460
|
-
{ [ -n "$dd" ] && [ "$dd" != "null" ] && [ -f "$dd" ]; }
|
|
461
|
-
_chk "design_doc exists ($dd)" $?
|
|
462
|
-
fi
|
|
463
|
-
_chk_nonempty "proposal.md" "$dir/proposal.md"
|
|
464
|
-
_chk_nonempty "tasks.md" "$dir/tasks.md"
|
|
465
|
-
;;
|
|
466
|
-
verify)
|
|
467
|
-
_chk_yaml_is "$yaml" phase verify
|
|
468
|
-
local vr; vr=$(yaml_get "$yaml" verify_result)
|
|
469
|
-
{ [ "$vr" = "pending" ] || [ -z "$vr" ] || [ "$vr" = "null" ]; }
|
|
470
|
-
_chk "verify_result pending/null (got $vr)" $?
|
|
471
|
-
;;
|
|
472
|
-
archive)
|
|
473
|
-
_chk_yaml_is "$yaml" phase archive
|
|
474
|
-
_chk_yaml_is "$yaml" verify_result pass
|
|
475
|
-
local ar; ar=$(yaml_get "$yaml" archived)
|
|
476
|
-
[ "$ar" != "true" ]; _chk "archived not yet true (got $ar)" $?
|
|
477
|
-
;;
|
|
478
|
-
esac
|
|
479
|
-
|
|
480
|
-
if [ "$CHECK_JSON" -eq 1 ]; then
|
|
481
|
-
local out='{"phase":"'"$phase"'","checks":['
|
|
482
|
-
local first=1 r cname cstatus
|
|
483
|
-
for r in "${CHECK_RESULTS[@]}"; do
|
|
484
|
-
cname="${r%|*}"; cstatus="${r#*|}"
|
|
485
|
-
[ "$first" -eq 0 ] && out+=','
|
|
486
|
-
first=0
|
|
487
|
-
out+='{"name":"'"$(json_escape "$cname")"'","status":"'"$cstatus"'"}'
|
|
488
|
-
done
|
|
489
|
-
out+='],"block":'"$([ "$CHECK_BLOCK" -eq 1 ] && echo true || echo false)"'}'
|
|
490
|
-
printf '%s\n' "$out"
|
|
491
|
-
else
|
|
492
|
-
echo "" >&2
|
|
493
|
-
if [ "$CHECK_BLOCK" -eq 1 ]; then
|
|
494
|
-
log_fail "BLOCKED — fix failing checks before proceeding"
|
|
495
|
-
else
|
|
496
|
-
log_pass "ALL CHECKS PASSED — ready to proceed"
|
|
497
|
-
fi
|
|
498
|
-
fi
|
|
499
|
-
if [ "$CHECK_BLOCK" -eq 1 ]; then exit "$COMATE_BLOCK"; fi
|
|
500
|
-
exit "$COMATE_OK"
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
# --- recover (compaction-resume structured context) ---
|
|
504
|
-
|
|
505
|
-
_field_status() {
|
|
506
|
-
# $1 name, $2 value, [$3 path]
|
|
507
|
-
local field="$1" value="$2" path="${3:-}"
|
|
508
|
-
if [ -z "$value" ] || [ "$value" = "null" ]; then
|
|
509
|
-
echo " - $field: PENDING"
|
|
510
|
-
elif [ -n "$path" ] && [ ! -f "$path" ]; then
|
|
511
|
-
echo " - $field: BROKEN (path $value does not exist)"
|
|
512
|
-
else
|
|
513
|
-
echo " - $field: DONE ($value)"
|
|
514
|
-
fi
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
state_recover() {
|
|
518
|
-
local name="$1" phase_arg="${2:-}"
|
|
519
|
-
validate_change_name "$name"
|
|
520
|
-
local dir yaml
|
|
521
|
-
dir=$(comate_change_dir "$name")
|
|
522
|
-
yaml="$dir/.comate.yaml"
|
|
523
|
-
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "ERROR: .comate.yaml not found at $yaml"
|
|
524
|
-
|
|
525
|
-
local phase workflow phase_step phase_step_data
|
|
526
|
-
phase=$(yaml_get "$yaml" phase)
|
|
527
|
-
workflow=$(yaml_get "$yaml" workflow)
|
|
528
|
-
phase_step=$(yaml_get "$yaml" phase_step)
|
|
529
|
-
phase_step_data=$(yaml_get "$yaml" phase_step_data)
|
|
530
|
-
# If a phase arg was passed, honor it for reporting; else use stored phase.
|
|
531
|
-
[ -n "$phase_arg" ] && phase="$phase_arg"
|
|
532
|
-
|
|
533
|
-
echo "=== Recovery Context: $name ==="
|
|
534
|
-
echo "Phase: $phase"
|
|
535
|
-
echo "Workflow: $workflow"
|
|
536
|
-
echo "Phase step: ${phase_step:-null}"
|
|
537
|
-
echo "Phase step data: ${phase_step_data:-null}"
|
|
538
|
-
echo ""
|
|
539
|
-
|
|
540
|
-
local design_doc plan verify_result verify_mode verification_report
|
|
541
|
-
local branch_status handoff_context handoff_hash impl_context impl_hash
|
|
542
|
-
local isolation build_mode tdd_mode direct_override
|
|
543
|
-
design_doc=$(yaml_get "$yaml" design_doc)
|
|
544
|
-
plan=$(yaml_get "$yaml" plan)
|
|
545
|
-
verify_result=$(yaml_get "$yaml" verify_result)
|
|
546
|
-
verify_mode=$(yaml_get "$yaml" verify_mode)
|
|
547
|
-
verification_report=$(yaml_get "$yaml" verification_report)
|
|
548
|
-
branch_status=$(yaml_get "$yaml" branch_status)
|
|
549
|
-
handoff_context=$(yaml_get "$yaml" handoff_context)
|
|
550
|
-
handoff_hash=$(yaml_get "$yaml" handoff_hash)
|
|
551
|
-
impl_context=$(yaml_get "$yaml" impl_context)
|
|
552
|
-
impl_hash=$(yaml_get "$yaml" impl_hash)
|
|
553
|
-
isolation=$(yaml_get "$yaml" isolation)
|
|
554
|
-
build_mode=$(yaml_get "$yaml" build_mode)
|
|
555
|
-
tdd_mode=$(yaml_get "$yaml" tdd_mode)
|
|
556
|
-
direct_override=$(yaml_get "$yaml" direct_override)
|
|
557
|
-
|
|
558
|
-
case "$phase" in
|
|
559
|
-
open)
|
|
560
|
-
echo "Artifacts:"
|
|
561
|
-
local fdone
|
|
562
|
-
for f in proposal.md design.md tasks.md; do
|
|
563
|
-
if file_nonempty "$dir/$f"; then echo " - $f: DONE"; else echo " - $f: PENDING"; fi
|
|
564
|
-
done
|
|
565
|
-
echo ""
|
|
566
|
-
echo "Recovery action: Create or complete missing artifacts, then use AskUserQuestion for user confirmation."
|
|
567
|
-
;;
|
|
568
|
-
design)
|
|
569
|
-
echo "Artifacts:"
|
|
570
|
-
for f in proposal.md design.md tasks.md; do
|
|
571
|
-
if file_nonempty "$dir/$f"; then echo " - $f: DONE"; else echo " - $f: MISSING (unexpected in design)"; fi
|
|
572
|
-
done
|
|
573
|
-
echo ""
|
|
574
|
-
echo "Design progress:"
|
|
575
|
-
_field_status "handoff_context" "$handoff_context" "$handoff_context"
|
|
576
|
-
_field_status "handoff_hash" "$handoff_hash"
|
|
577
|
-
_field_status "design_doc" "$design_doc" "$design_doc"
|
|
578
|
-
echo ""
|
|
579
|
-
if [ -n "$design_doc" ] && [ "$design_doc" != "null" ] && [ -f "$design_doc" ]; then
|
|
580
|
-
echo "Recovery action: Design Doc linked. Run guard to transition to build."
|
|
581
|
-
elif [ -n "$handoff_context" ] && [ "$handoff_context" != "null" ] && [ -f "$handoff_context" ]; then
|
|
582
|
-
echo "Recovery action: Handoff generated but Design Doc not created. Resume from brainstorming confirmation (Step 1c)."
|
|
583
|
-
else
|
|
584
|
-
echo "Recovery action: No handoff yet. Start from Step 1a (generate handoff package)."
|
|
585
|
-
fi
|
|
586
|
-
;;
|
|
587
|
-
build)
|
|
588
|
-
echo "Build decisions:"
|
|
589
|
-
_field_status "isolation" "$isolation"
|
|
590
|
-
_field_status "build_mode" "$build_mode"
|
|
591
|
-
_field_status "tdd_mode" "$tdd_mode"
|
|
592
|
-
if [ "$build_mode" = "direct" ] && [ "$workflow" != "fix" ]; then
|
|
593
|
-
_field_status "direct_override" "$direct_override"
|
|
594
|
-
fi
|
|
595
|
-
echo ""
|
|
596
|
-
_field_status "plan" "$plan" "$plan"
|
|
597
|
-
_field_status "impl_context" "$impl_context" "$impl_context"
|
|
598
|
-
echo ""
|
|
599
|
-
local tasks_file="$dir/tasks.md" total=0 done=0 pending=0
|
|
600
|
-
if [ -f "$tasks_file" ]; then
|
|
601
|
-
total=$(grep -c '^- \[' "$tasks_file" 2>/dev/null || echo 0)
|
|
602
|
-
done=$(grep -c '^- \[x\]' "$tasks_file" 2>/dev/null || echo 0)
|
|
603
|
-
pending=$((total - done))
|
|
604
|
-
echo "Tasks: $done/$total done, $pending pending"
|
|
605
|
-
else
|
|
606
|
-
echo "Tasks: tasks.md MISSING"
|
|
607
|
-
fi
|
|
608
|
-
echo ""
|
|
609
|
-
if [[ "$phase_step" == build.blocked-by-* ]]; then
|
|
610
|
-
local dep="$phase_step_data"
|
|
611
|
-
if [ -z "$dep" ] || [ "$dep" = "null" ]; then dep="${phase_step#build.blocked-by-}"; fi
|
|
612
|
-
echo "Recovery action: Blocked by $dep. Run: comate.sh dependency $dep archived"
|
|
613
|
-
elif [ "$isolation" = "null" ] || [ -z "$isolation" ]; then
|
|
614
|
-
echo "Recovery action: Isolation not selected. AskUserQuestion for branch/worktree."
|
|
615
|
-
elif [ "$build_mode" = "null" ] || [ -z "$build_mode" ]; then
|
|
616
|
-
echo "Recovery action: Build mode not selected. AskUserQuestion for execution method."
|
|
617
|
-
elif [ "$tdd_mode" = "null" ] || [ -z "$tdd_mode" ]; then
|
|
618
|
-
echo "Recovery action: TDD mode not selected. AskUserQuestion for tdd/direct."
|
|
619
|
-
elif [ ! -f "$tasks_file" ]; then
|
|
620
|
-
echo "Recovery action: tasks.md missing. Verify change directory integrity."
|
|
621
|
-
elif [ "$pending" -gt 0 ]; then
|
|
622
|
-
echo "Recovery action: Read tasks.md and continue from first unchecked task."
|
|
623
|
-
else
|
|
624
|
-
echo "Recovery action: All tasks done. Run guard to transition to verify."
|
|
625
|
-
fi
|
|
626
|
-
;;
|
|
627
|
-
verify)
|
|
628
|
-
echo "Verification:"
|
|
629
|
-
_field_status "verify_result" "$verify_result"
|
|
630
|
-
_field_status "verify_mode" "$verify_mode"
|
|
631
|
-
_field_status "verification_report" "$verification_report" "$verification_report"
|
|
632
|
-
_field_status "branch_status" "$branch_status"
|
|
633
|
-
_field_status "impl_hash" "$impl_hash"
|
|
634
|
-
echo ""
|
|
635
|
-
if [ "$branch_status" = "discarded" ]; then
|
|
636
|
-
echo "Recovery action: Work was discarded. Run verify-fail to return to build before reimplementing, or explicitly abandon the change."
|
|
637
|
-
elif [ "$verify_result" = "pass" ] && [ "$branch_status" = "handled" ]; then
|
|
638
|
-
echo "Recovery action: Verification complete. Run guard to transition to archive."
|
|
639
|
-
elif [ "$verify_result" = "pass" ]; then
|
|
640
|
-
echo "Recovery action: Verified but branch not handled. Complete branch handling, set branch_status=handled."
|
|
641
|
-
elif [ "$verify_result" = "fail" ]; then
|
|
642
|
-
echo "Recovery action: Verification failed, rolled back to build. Resume from /comate-build."
|
|
643
|
-
else
|
|
644
|
-
echo "Recovery action: Verification not started. Run scale assessment then verify."
|
|
645
|
-
fi
|
|
646
|
-
;;
|
|
647
|
-
archive)
|
|
648
|
-
echo "Archive:"
|
|
649
|
-
_field_status "verify_result" "$verify_result"
|
|
650
|
-
_field_status "archived" "$(yaml_get "$yaml" archived)"
|
|
651
|
-
echo ""
|
|
652
|
-
echo "Recovery action: Run /comate-archive to complete archiving."
|
|
653
|
-
;;
|
|
654
|
-
*)
|
|
655
|
-
die "$COMATE_USAGE" "ERROR: unknown phase '$phase'"
|
|
656
|
-
;;
|
|
657
|
-
esac
|
|
658
|
-
echo ""
|
|
659
|
-
echo "=== End Recovery Context ==="
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
# --- scale (assess verify_mode, base-ref annotation) ---
|
|
663
|
-
|
|
664
|
-
state_scale() {
|
|
665
|
-
local name="$1"
|
|
666
|
-
validate_change_name "$name"
|
|
667
|
-
local dir yaml
|
|
668
|
-
dir=$(comate_change_dir "$name")
|
|
669
|
-
yaml="$dir/.comate.yaml"
|
|
670
|
-
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "ERROR: .comate.yaml not found at $yaml"
|
|
671
|
-
|
|
672
|
-
local tasks_file="$dir/tasks.md" task_count=0
|
|
673
|
-
[ -f "$tasks_file" ] && task_count=$(grep -c '^- \[' "$tasks_file" 2>/dev/null || echo 0)
|
|
674
|
-
|
|
675
|
-
local delta_spec_count=0
|
|
676
|
-
if [ -d "$dir/specs" ]; then
|
|
677
|
-
delta_spec_count=$(find "$dir/specs" -name spec.md -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
678
|
-
fi
|
|
679
|
-
|
|
680
|
-
# Changed files: plan base-ref > yaml base_ref > worktree diff. Annotate source.
|
|
681
|
-
local changed_files=0 basis="worktree-diff" base_ref=""
|
|
682
|
-
if git rev-parse --git-dir >/dev/null 2>&1; then
|
|
683
|
-
local plan_file
|
|
684
|
-
plan_file=$(yaml_get "$yaml" plan)
|
|
685
|
-
if [ -n "$plan_file" ] && [ "$plan_file" != "null" ] && [ -f "$plan_file" ]; then
|
|
686
|
-
base_ref=$(grep '^base-ref:' "$plan_file" 2>/dev/null | head -1 | sed 's/^base-ref:[[:space:]]*//')
|
|
687
|
-
[ -n "$base_ref" ] && [ "$base_ref" != "null" ] && basis="plan base-ref"
|
|
688
|
-
fi
|
|
689
|
-
if [ -z "$base_ref" ] || [ "$base_ref" = "null" ]; then
|
|
690
|
-
base_ref=$(yaml_get "$yaml" base_ref)
|
|
691
|
-
[ -n "$base_ref" ] && [ "$base_ref" != "null" ] && basis="state base_ref"
|
|
692
|
-
fi
|
|
693
|
-
if [ -n "$base_ref" ] && [ "$base_ref" != "null" ] && git rev-parse --verify "$base_ref" >/dev/null 2>&1; then
|
|
694
|
-
changed_files=$(git diff --name-only "$base_ref"...HEAD 2>/dev/null | wc -l | tr -d ' ')
|
|
695
|
-
else
|
|
696
|
-
changed_files=$(git diff --name-only HEAD 2>/dev/null | wc -l | tr -d ' ')
|
|
697
|
-
basis="worktree-diff"
|
|
698
|
-
fi
|
|
699
|
-
fi
|
|
700
|
-
|
|
701
|
-
local result="light"
|
|
702
|
-
if [ "$task_count" -gt 3 ] || [ "$delta_spec_count" -gt 1 ] || [ "$changed_files" -gt 4 ]; then
|
|
703
|
-
result="full"
|
|
704
|
-
fi
|
|
705
|
-
|
|
706
|
-
{
|
|
707
|
-
echo "=== Scale Assessment: $name ==="
|
|
708
|
-
echo " Tasks: $task_count (threshold: 3)"
|
|
709
|
-
echo " Delta specs: $delta_spec_count capabilities (threshold: 1)"
|
|
710
|
-
echo " Changed files: $changed_files (threshold: 4) [basis: $basis]"
|
|
711
|
-
echo " -> Result: $result"
|
|
712
|
-
} >&2
|
|
713
|
-
|
|
714
|
-
yaml_set "$yaml" verify_mode "$result"
|
|
715
|
-
log_pass "[SCALE] verify_mode=$result"
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
# =====================================================================
|
|
719
|
-
# guard — phase EXIT evidence checks (table in IMPLEMENTATION.md §4.3)
|
|
720
|
-
# =====================================================================
|
|
721
|
-
|
|
722
|
-
GUARD_BLOCK=0
|
|
723
|
-
declare -a GUARD_RESULTS=() # "name|status"
|
|
724
|
-
|
|
725
|
-
# _guard_check <desc> <command...> — run command; capture output; record result.
|
|
726
|
-
# Always returns 0 so a recorded FAIL never trips the caller's set -e.
|
|
727
|
-
_guard_check() {
|
|
728
|
-
local desc="$1"; shift
|
|
729
|
-
local output rc
|
|
730
|
-
if output=$("$@" 2>&1); then rc=0; else rc=1; fi
|
|
731
|
-
if [ "$rc" -eq 0 ]; then
|
|
732
|
-
GUARD_RESULTS+=("$desc|pass")
|
|
733
|
-
if [ "$GUARD_JSON" -eq 0 ]; then log_pass " [PASS] $desc"; fi
|
|
734
|
-
else
|
|
735
|
-
GUARD_RESULTS+=("$desc|fail")
|
|
736
|
-
GUARD_BLOCK=1
|
|
737
|
-
if [ "$GUARD_JSON" -eq 0 ]; then
|
|
738
|
-
log_fail " [FAIL] $desc"
|
|
739
|
-
if [ -n "$output" ]; then while IFS= read -r line; do log_fail " $line"; done <<< "$output"; fi
|
|
740
|
-
fi
|
|
741
|
-
fi
|
|
742
|
-
return 0
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
# --- evidence predicates (use $G_DIR / $G_YAML / $G_CHANGE set by guard()) ---
|
|
746
|
-
|
|
747
|
-
_g_tasks_all_done() {
|
|
748
|
-
local tasks="$G_DIR/tasks.md"
|
|
749
|
-
[ -f "$tasks" ] || { echo "tasks.md missing at $tasks"; return 1; }
|
|
750
|
-
grep -q '^- \[x\]' "$tasks" || { echo "tasks.md has no completed tasks"; return 1; }
|
|
751
|
-
if grep -q '^- \[ \]' "$tasks"; then
|
|
752
|
-
echo "Unfinished tasks:"; grep -n '^- \[ \]' "$tasks" || true; return 1
|
|
753
|
-
fi
|
|
754
|
-
return 0
|
|
755
|
-
}
|
|
756
|
-
_g_tasks_has_any() {
|
|
757
|
-
local tasks="$G_DIR/tasks.md"
|
|
758
|
-
[ -f "$tasks" ] && grep -q '^- \[' "$tasks"
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
# Resolve a project command: change yaml first, then repo-root config files.
|
|
762
|
-
_g_project_command() {
|
|
763
|
-
local field="$1" value
|
|
764
|
-
value=$(yaml_get "$G_YAML" "$field")
|
|
765
|
-
if [ -n "$value" ] && [ "$value" != "null" ]; then echo "$value"; return 0; fi
|
|
766
|
-
local cfg
|
|
767
|
-
for cfg in ".comate.yaml" "comate.yaml" ".comate.yml" "comate.yml"; do
|
|
768
|
-
if [ -f "$cfg" ]; then
|
|
769
|
-
value=$(yaml_get "$cfg" "$field")
|
|
770
|
-
if [ -n "$value" ] && [ "$value" != "null" ]; then echo "$value"; return 0; fi
|
|
771
|
-
fi
|
|
772
|
-
done
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
_g_is_windows_bash() {
|
|
776
|
-
case "$(uname -s 2>/dev/null || true)" in
|
|
777
|
-
MINGW*|MSYS*|CYGWIN*) return 0 ;;
|
|
778
|
-
*) return 1 ;;
|
|
779
|
-
esac
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
_g_run_cmd() { echo "+ $1" >&2; bash -lc "$1"; }
|
|
783
|
-
|
|
784
|
-
_g_build_passes() {
|
|
785
|
-
[ "${COMATE_SKIP_BUILD:-0}" = "1" ] && return 0
|
|
786
|
-
local cmd; cmd=$(_g_project_command build_command)
|
|
787
|
-
if [ -n "$cmd" ]; then _g_run_cmd "$cmd"; return $?; fi
|
|
788
|
-
if [ -f package.json ] && grep -q '"build"' package.json; then npm run build; return $?; fi
|
|
789
|
-
if [ -f pom.xml ]; then
|
|
790
|
-
if [ -x ./mvnw ]; then ./mvnw compile -q
|
|
791
|
-
elif _g_is_windows_bash && command -v mvn.cmd >/dev/null 2>&1; then mvn.cmd compile -q
|
|
792
|
-
else mvn compile -q; fi
|
|
793
|
-
return $?
|
|
794
|
-
fi
|
|
795
|
-
if [ -f Cargo.toml ]; then cargo build; return $?; fi
|
|
796
|
-
echo "no build_command configured and no known build tool detected"
|
|
797
|
-
echo "Next: set build_command in .comate.yaml, or skip with COMATE_SKIP_BUILD=1 (not recommended)."
|
|
798
|
-
return 1
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
_g_verify_passes() {
|
|
802
|
-
[ "${COMATE_SKIP_BUILD:-0}" = "1" ] && return 0
|
|
803
|
-
local cmd; cmd=$(_g_project_command verify_command)
|
|
804
|
-
if [ -n "$cmd" ]; then _g_run_cmd "$cmd"; return $?; fi
|
|
805
|
-
_g_build_passes
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
_g_isolation_selected() {
|
|
809
|
-
local v; v=$(yaml_get "$G_YAML" isolation)
|
|
810
|
-
case "$v" in branch|worktree) return 0 ;; esac
|
|
811
|
-
echo "isolation must be branch or worktree, got '${v:-null}'"
|
|
812
|
-
echo "Next: comate.sh state $G_CHANGE set isolation <branch|worktree>"
|
|
813
|
-
return 1
|
|
814
|
-
}
|
|
815
|
-
_g_build_mode_selected() {
|
|
816
|
-
local v; v=$(yaml_get "$G_YAML" build_mode)
|
|
817
|
-
case "$v" in subagent-driven-development|executing-plans|direct) return 0 ;; esac
|
|
818
|
-
echo "build_mode must be selected, got '${v:-null}'"
|
|
819
|
-
return 1
|
|
820
|
-
}
|
|
821
|
-
_g_build_mode_allowed() {
|
|
822
|
-
local workflow build_mode direct_override
|
|
823
|
-
workflow=$(yaml_get "$G_YAML" workflow)
|
|
824
|
-
build_mode=$(yaml_get "$G_YAML" build_mode)
|
|
825
|
-
direct_override=$(yaml_get "$G_YAML" direct_override)
|
|
826
|
-
[ "$build_mode" != "direct" ] && return 0
|
|
827
|
-
case "$workflow" in fix) return 0 ;; esac
|
|
828
|
-
[ "$direct_override" = "true" ] && return 0
|
|
829
|
-
echo "build_mode=direct only allowed for fix unless direct_override=true"
|
|
830
|
-
return 1
|
|
831
|
-
}
|
|
832
|
-
_g_tdd_mode_selected() {
|
|
833
|
-
local v; v=$(yaml_get "$G_YAML" tdd_mode)
|
|
834
|
-
case "$v" in tdd|direct) return 0 ;; esac
|
|
835
|
-
echo "tdd_mode must be tdd or direct, got '${v:-null}'"
|
|
836
|
-
echo "Next: comate.sh state $G_CHANGE set tdd_mode <tdd|direct>"
|
|
837
|
-
return 1
|
|
838
|
-
}
|
|
839
|
-
_g_verification_report_exists() {
|
|
840
|
-
local v; v=$(yaml_get "$G_YAML" verification_report)
|
|
841
|
-
[ -n "$v" ] && [ "$v" != "null" ] && [ -f "$v" ]
|
|
842
|
-
}
|
|
843
|
-
_g_branch_status_handled() {
|
|
844
|
-
[ "$(yaml_get "$G_YAML" branch_status)" = "handled" ]
|
|
845
|
-
}
|
|
846
|
-
_g_archived_is_true() {
|
|
847
|
-
[ "$(yaml_get "$G_YAML" archived)" = "true" ]
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
# source-tracking drift: knowledge-base files unchanged since they were pinned.
|
|
851
|
-
# conversation sources (no manifest) pass trivially.
|
|
852
|
-
_g_sources_not_drifted() {
|
|
853
|
-
local kind; kind=$(yaml_get "$G_YAML" source_kind)
|
|
854
|
-
[ "$kind" = "file" ] || return 0
|
|
855
|
-
local manifest; manifest=$(yaml_get "$G_YAML" source_manifest)
|
|
856
|
-
if [ -z "$manifest" ] || [ "$manifest" = "null" ]; then
|
|
857
|
-
echo "source_kind=file but source_manifest is null"; return 1
|
|
858
|
-
fi
|
|
859
|
-
local out rc
|
|
860
|
-
out=$(sources_drift "$manifest"); rc=$?
|
|
861
|
-
if [ "$rc" -ne 0 ]; then
|
|
862
|
-
[ -n "$out" ] && echo "$out"
|
|
863
|
-
echo "Next: review the knowledge-base change; rerun comate.sh sources $G_CHANGE track <file> if intended."
|
|
864
|
-
return 1
|
|
865
|
-
fi
|
|
866
|
-
return 0
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
# Handoff source set (design phase) — proposal/design/tasks + delta specs.
|
|
870
|
-
_g_handoff_sources() {
|
|
871
|
-
printf '%s\n' "$G_DIR/proposal.md" "$G_DIR/design.md" "$G_DIR/tasks.md"
|
|
872
|
-
[ -d "$G_DIR/specs" ] && find "$G_DIR/specs" -path '*/spec.md' -type f 2>/dev/null | sort
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
_g_design_handoff_valid() {
|
|
876
|
-
local context recorded actual md
|
|
877
|
-
context=$(yaml_get "$G_YAML" handoff_context)
|
|
878
|
-
recorded=$(yaml_get "$G_YAML" handoff_hash)
|
|
879
|
-
if [ -z "$context" ] || [ "$context" = "null" ]; then
|
|
880
|
-
echo "handoff_context missing; run: comate.sh handoff $G_CHANGE design --write"; return 1
|
|
881
|
-
fi
|
|
882
|
-
[ -s "$context" ] || { echo "handoff_context not a non-empty file: $context"; return 1; }
|
|
883
|
-
if [[ ! "$recorded" =~ ^[a-f0-9]{64}$ ]]; then
|
|
884
|
-
echo "handoff_hash missing or invalid: ${recorded:-null}"; return 1
|
|
885
|
-
fi
|
|
886
|
-
# Recompute over current sources; flag drift.
|
|
887
|
-
local files=()
|
|
888
|
-
while IFS= read -r f; do files+=("$f"); done < <(_g_handoff_sources)
|
|
889
|
-
actual=$(hash_files_manifest "${files[@]}")
|
|
890
|
-
if [ "$actual" != "$recorded" ]; then
|
|
891
|
-
echo "OpenSpec artifacts changed after handoff was generated."
|
|
892
|
-
echo " recorded: $recorded"
|
|
893
|
-
echo " actual: $actual"
|
|
894
|
-
echo "Next: rerun comate.sh handoff $G_CHANGE design --write"
|
|
895
|
-
return 1
|
|
896
|
-
fi
|
|
897
|
-
md="${context%.json}.md"
|
|
898
|
-
[ -s "$md" ] || { echo "design handoff markdown missing/empty: $md"; return 1; }
|
|
899
|
-
return 0
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
_g_design_handoff_traceable() {
|
|
903
|
-
local context md
|
|
904
|
-
context=$(yaml_get "$G_YAML" handoff_context)
|
|
905
|
-
[ -n "$context" ] && [ "$context" != "null" ] || { echo "handoff_context missing"; return 1; }
|
|
906
|
-
md="${context%.json}.md"
|
|
907
|
-
[ -s "$md" ] || { echo "handoff markdown missing/empty: $md"; return 1; }
|
|
908
|
-
grep -q '^Generated-by: comate$' "$md" || { echo "missing Generated-by marker"; return 1; }
|
|
909
|
-
grep -Eq '^- Mode: (compact|full)$' "$md" || { echo "missing Mode marker"; return 1; }
|
|
910
|
-
local f rc=0
|
|
911
|
-
while IFS= read -r f; do
|
|
912
|
-
[ -f "$f" ] || continue
|
|
913
|
-
grep -q "^- Source: $f$" "$md" || { echo "missing source reference: $f"; rc=1; }
|
|
914
|
-
grep -q "^- SHA256: $(hash_file "$f")$" "$md" || { echo "missing current sha256 for: $f"; rc=1; }
|
|
915
|
-
done < <(_g_handoff_sources)
|
|
916
|
-
return $rc
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
# Design Doc frontmatter helper.
|
|
920
|
-
_g_frontmatter_has() {
|
|
921
|
-
local file="$1" field="$2" expected="$3"
|
|
922
|
-
awk '
|
|
923
|
-
{ line=$0; sub(/^\357\273\277/, "", line) }
|
|
924
|
-
!in_fm && line == "---" { in_fm=1; next }
|
|
925
|
-
in_fm && line == "---" { exit }
|
|
926
|
-
in_fm { print line }
|
|
927
|
-
' "$file" | grep -Eq "^${field}: ['\"]?${expected}['\"]?[[:space:]]*$"
|
|
928
|
-
}
|
|
929
|
-
_g_design_doc_links_change() {
|
|
930
|
-
local dd; dd=$(yaml_get "$G_YAML" design_doc)
|
|
931
|
-
[ -n "$dd" ] && [ "$dd" != "null" ] && [ -s "$dd" ] || { echo "design_doc must point to an existing Design Doc"; return 1; }
|
|
932
|
-
_g_frontmatter_has "$dd" comate_change "$G_CHANGE" || { echo "Design Doc frontmatter missing comate_change: $G_CHANGE"; return 1; }
|
|
933
|
-
}
|
|
934
|
-
_g_design_doc_role() {
|
|
935
|
-
local dd; dd=$(yaml_get "$G_YAML" design_doc)
|
|
936
|
-
[ -n "$dd" ] && [ "$dd" != "null" ] && [ -s "$dd" ] && _g_frontmatter_has "$dd" role technical-design
|
|
937
|
-
}
|
|
938
|
-
_g_design_doc_canonical() {
|
|
939
|
-
local dd; dd=$(yaml_get "$G_YAML" design_doc)
|
|
940
|
-
[ -n "$dd" ] && [ "$dd" != "null" ] && [ -s "$dd" ] && _g_frontmatter_has "$dd" canonical_spec openspec
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
# impl-context drift: verify entry plan+tasks match build exit.
|
|
944
|
-
_g_impl_context_written() {
|
|
945
|
-
local ctx hash
|
|
946
|
-
ctx=$(yaml_get "$G_YAML" impl_context)
|
|
947
|
-
hash=$(yaml_get "$G_YAML" impl_hash)
|
|
948
|
-
if [ -z "$ctx" ] || [ "$ctx" = "null" ] || [ ! -f "$ctx" ]; then
|
|
949
|
-
echo "impl_context not written; run: comate.sh handoff $G_CHANGE build --write"
|
|
950
|
-
return 1
|
|
951
|
-
fi
|
|
952
|
-
if [[ ! "$hash" =~ ^[a-f0-9]{64}$ ]]; then
|
|
953
|
-
echo "impl_hash missing or invalid: ${hash:-null}"; return 1
|
|
954
|
-
fi
|
|
955
|
-
return 0
|
|
956
|
-
}
|
|
957
|
-
_g_impl_hash_matches() {
|
|
958
|
-
local recorded plan actual
|
|
959
|
-
recorded=$(yaml_get "$G_YAML" impl_hash)
|
|
960
|
-
if [ -z "$recorded" ] || [ "$recorded" = "null" ]; then
|
|
961
|
-
echo "impl_hash missing; run: comate.sh handoff $G_CHANGE build --write at end of build"; return 1
|
|
962
|
-
fi
|
|
963
|
-
plan=$(yaml_get "$G_YAML" plan)
|
|
964
|
-
local files=("$G_DIR/tasks.md")
|
|
965
|
-
[ -n "$plan" ] && [ "$plan" != "null" ] && [ -f "$plan" ] && files=("$plan" "$G_DIR/tasks.md")
|
|
966
|
-
actual=$(hash_files_manifest "${files[@]}")
|
|
967
|
-
if [ "$actual" != "$recorded" ]; then
|
|
968
|
-
echo "plan/tasks changed between build exit and verify entry."
|
|
969
|
-
echo " recorded: $recorded"
|
|
970
|
-
echo " actual: $actual"
|
|
971
|
-
echo "Next: investigate the edit; rerun build handoff only if the change is intended."
|
|
972
|
-
return 1
|
|
973
|
-
fi
|
|
974
|
-
return 0
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
guard() {
|
|
978
|
-
local name="$1" phase="${2:-}"
|
|
979
|
-
validate_change_name "$name"
|
|
980
|
-
validate_enum phase "$phase"
|
|
981
|
-
[ -n "$phase" ] || die "$COMATE_USAGE" "Usage: comate.sh guard <name> <phase> [--apply] [--json]"
|
|
982
|
-
|
|
983
|
-
G_CHANGE="$name"
|
|
984
|
-
G_DIR=$(comate_change_dir "$name")
|
|
985
|
-
G_YAML="$G_DIR/.comate.yaml"
|
|
986
|
-
|
|
987
|
-
# preflight: directory + state file + flat-yaml schema.
|
|
988
|
-
[ -d "$G_DIR" ] || die "$COMATE_CORRUPT" "FATAL: change directory not found: $G_DIR"
|
|
989
|
-
[ -f "$G_YAML" ] || die "$COMATE_CORRUPT" "FATAL: .comate.yaml not found in $G_DIR"
|
|
990
|
-
validate_flat_yaml "$G_YAML"
|
|
991
|
-
|
|
992
|
-
[ "$GUARD_JSON" -eq 0 ] && log_info "=== Guard: $phase ==="
|
|
993
|
-
|
|
994
|
-
case "$phase" in
|
|
995
|
-
open)
|
|
996
|
-
_guard_check "proposal.md non-empty" file_nonempty "$G_DIR/proposal.md"
|
|
997
|
-
_guard_check "design.md non-empty" file_nonempty "$G_DIR/design.md"
|
|
998
|
-
_guard_check "tasks.md non-empty" file_nonempty "$G_DIR/tasks.md"
|
|
999
|
-
_guard_check "tasks.md has at least one task" _g_tasks_has_any
|
|
1000
|
-
;;
|
|
1001
|
-
design)
|
|
1002
|
-
_guard_check "proposal.md non-empty" file_nonempty "$G_DIR/proposal.md"
|
|
1003
|
-
_guard_check "design.md non-empty" file_nonempty "$G_DIR/design.md"
|
|
1004
|
-
_guard_check "tasks.md non-empty" file_nonempty "$G_DIR/tasks.md"
|
|
1005
|
-
_guard_check "design handoff context valid (hash not drifted)" _g_design_handoff_valid
|
|
1006
|
-
_guard_check "design handoff markdown traceable" _g_design_handoff_traceable
|
|
1007
|
-
_guard_check "source files not drifted" _g_sources_not_drifted
|
|
1008
|
-
local dd; dd=$(yaml_get "$G_YAML" design_doc)
|
|
1009
|
-
if [ -n "$dd" ] && [ "$dd" != "null" ]; then
|
|
1010
|
-
_guard_check "Design Doc exists ($dd)" file_nonempty "$dd"
|
|
1011
|
-
_guard_check "Design Doc frontmatter links change" _g_design_doc_links_change
|
|
1012
|
-
_guard_check "Design Doc declares technical-design role" _g_design_doc_role
|
|
1013
|
-
_guard_check "Design Doc declares canonical_spec: openspec" _g_design_doc_canonical
|
|
1014
|
-
else
|
|
1015
|
-
# Design Doc is required for full workflow moving to build
|
|
1016
|
-
_guard_check "Design Doc recorded (design_doc field)" false
|
|
1017
|
-
fi
|
|
1018
|
-
;;
|
|
1019
|
-
build)
|
|
1020
|
-
_guard_check "isolation selected" _g_isolation_selected
|
|
1021
|
-
_guard_check "build_mode selected" _g_build_mode_selected
|
|
1022
|
-
_guard_check "build_mode allowed for workflow" _g_build_mode_allowed
|
|
1023
|
-
_guard_check "tdd_mode selected" _g_tdd_mode_selected
|
|
1024
|
-
_guard_check "tasks.md all tasks checked" _g_tasks_all_done
|
|
1025
|
-
_guard_check "proposal.md non-empty" file_nonempty "$G_DIR/proposal.md"
|
|
1026
|
-
_guard_check "impl-context written" _g_impl_context_written
|
|
1027
|
-
_guard_check "source files not drifted" _g_sources_not_drifted
|
|
1028
|
-
_guard_check "build passes" _g_build_passes
|
|
1029
|
-
;;
|
|
1030
|
-
verify)
|
|
1031
|
-
_guard_check "tasks.md all tasks checked" _g_tasks_all_done
|
|
1032
|
-
_guard_check "verify command passes" _g_verify_passes
|
|
1033
|
-
_guard_check "verification_report exists" _g_verification_report_exists
|
|
1034
|
-
_guard_check "branch_status=handled" _g_branch_status_handled
|
|
1035
|
-
_guard_check "impl_hash matches current plan+tasks" _g_impl_hash_matches
|
|
1036
|
-
;;
|
|
1037
|
-
archive)
|
|
1038
|
-
_guard_check "archived is true" _g_archived_is_true
|
|
1039
|
-
_guard_check "proposal.md non-empty" file_nonempty "$G_DIR/proposal.md"
|
|
1040
|
-
_guard_check "tasks.md all tasks checked" _g_tasks_all_done
|
|
1041
|
-
;;
|
|
1042
|
-
esac
|
|
1043
|
-
|
|
1044
|
-
if [ "$GUARD_JSON" -eq 1 ]; then
|
|
1045
|
-
local out='{"phase":"'"$phase"'","checks":['
|
|
1046
|
-
local first=1 r cname cstatus
|
|
1047
|
-
for r in "${GUARD_RESULTS[@]}"; do
|
|
1048
|
-
cname="${r%|*}"; cstatus="${r#*|}"
|
|
1049
|
-
[ "$first" -eq 0 ] && out+=','
|
|
1050
|
-
first=0
|
|
1051
|
-
out+='{"name":"'"$(json_escape "$cname")"'","status":"'"$cstatus"'"}'
|
|
1052
|
-
done
|
|
1053
|
-
out+='],"block":'"$([ "$GUARD_BLOCK" -eq 1 ] && echo true || echo false)"
|
|
1054
|
-
out+=',"applied":false}'
|
|
1055
|
-
printf '%s\n' "$out"
|
|
1056
|
-
if [ "$GUARD_BLOCK" -eq 1 ]; then exit "$COMATE_BLOCK"; fi
|
|
1057
|
-
# In JSON mode, still apply if requested (silent).
|
|
1058
|
-
if [ "$GUARD_APPLY" -eq 1 ]; then _guard_apply "$phase" >/dev/null 2>&1 || true; fi
|
|
1059
|
-
exit "$COMATE_OK"
|
|
1060
|
-
fi
|
|
1061
|
-
|
|
1062
|
-
echo "" >&2
|
|
1063
|
-
if [ "$GUARD_BLOCK" -eq 1 ]; then
|
|
1064
|
-
log_fail "BLOCKED — fix failing checks before proceeding to next phase"
|
|
1065
|
-
exit "$COMATE_BLOCK"
|
|
1066
|
-
fi
|
|
1067
|
-
log_pass "ALL CHECKS PASSED — ready for next phase"
|
|
1068
|
-
if [ "$GUARD_APPLY" -eq 1 ]; then
|
|
1069
|
-
_guard_apply "$phase"
|
|
1070
|
-
fi
|
|
1071
|
-
exit "$COMATE_OK"
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
# _guard_apply <phase> — delegate to the matching transition.
|
|
1075
|
-
_guard_apply() {
|
|
1076
|
-
case "$1" in
|
|
1077
|
-
open) state_transition "$G_CHANGE" open-complete ;;
|
|
1078
|
-
design) state_transition "$G_CHANGE" design-complete ;;
|
|
1079
|
-
build) state_transition "$G_CHANGE" build-complete ;;
|
|
1080
|
-
verify) state_transition "$G_CHANGE" verify-pass ;;
|
|
1081
|
-
archive) : ;; # archive completion handled by the archive command
|
|
1082
|
-
esac
|
|
1083
|
-
local np; np=$(yaml_get "$G_YAML" phase)
|
|
1084
|
-
log_pass " [APPLY] .comate.yaml updated: phase=$np"
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
# =====================================================================
|
|
1088
|
-
# handoff — machine-owned context packages between phases
|
|
1089
|
-
# =====================================================================
|
|
1090
|
-
|
|
1091
|
-
# Shared: emit a traceable markdown excerpt for one file.
|
|
1092
|
-
_handoff_excerpt() {
|
|
1093
|
-
local file="$1" max_lines="$2" mode="$3" total
|
|
1094
|
-
total=$(wc -l < "$file" | tr -d ' ')
|
|
1095
|
-
echo "## $file"
|
|
1096
|
-
echo ""
|
|
1097
|
-
echo "- Source: $file"
|
|
1098
|
-
echo "- Lines: 1-$total"
|
|
1099
|
-
echo "- SHA256: $(hash_file "$file")"
|
|
1100
|
-
echo ""
|
|
1101
|
-
if [ "$mode" = "full" ] || [ "$total" -le "$max_lines" ]; then
|
|
1102
|
-
echo '```md'; cat "$file"; echo '```'
|
|
1103
|
-
else
|
|
1104
|
-
echo "[TRUNCATED]"; echo ""
|
|
1105
|
-
echo '```md'; sed -n "1,${max_lines}p" "$file"; echo '```'
|
|
1106
|
-
echo ""; echo "Full source: $file"
|
|
1107
|
-
fi
|
|
1108
|
-
echo ""
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
handoff() {
|
|
1112
|
-
local name="$1" phase="${2:-}" mode="${3:-}" full="${4:-}"
|
|
1113
|
-
validate_change_name "$name"
|
|
1114
|
-
local dir yaml
|
|
1115
|
-
dir=$(comate_change_dir "$name")
|
|
1116
|
-
yaml="$dir/.comate.yaml"
|
|
1117
|
-
[ -d "$dir" ] || die "$COMATE_CORRUPT" "ERROR: change directory not found: $dir"
|
|
1118
|
-
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "ERROR: .comate.yaml not found at $yaml"
|
|
1119
|
-
[ "$mode" = "--write" ] || die "$COMATE_USAGE" "Usage: comate.sh handoff <name> <design|build> --write [--full]"
|
|
1120
|
-
|
|
1121
|
-
case "$phase" in
|
|
1122
|
-
design) _handoff_design "$name" "$dir" "$yaml" "$full" ;;
|
|
1123
|
-
build) _handoff_build "$name" "$dir" "$yaml" ;;
|
|
1124
|
-
*) die "$COMATE_USAGE" "Usage: comate.sh handoff <name> <design|build> --write [--full]" ;;
|
|
1125
|
-
esac
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
_handoff_design() {
|
|
1129
|
-
local name="$1" dir="$2" yaml="$3" full="$4"
|
|
1130
|
-
local hmode="compact"
|
|
1131
|
-
case "$full" in
|
|
1132
|
-
"") hmode="compact" ;;
|
|
1133
|
-
--full) hmode="full" ;;
|
|
1134
|
-
*) die "$COMATE_USAGE" "Usage: comate.sh handoff <name> design --write [--full]" ;;
|
|
1135
|
-
esac
|
|
1136
|
-
[ "$(yaml_get "$yaml" phase)" = "design" ] || die "$COMATE_BLOCK" "ERROR: design handoff requires phase: design"
|
|
1137
|
-
|
|
1138
|
-
local f
|
|
1139
|
-
for f in proposal.md design.md tasks.md; do
|
|
1140
|
-
[ -s "$dir/$f" ] || die "$COMATE_BLOCK" "ERROR: required artifact missing/empty: $dir/$f"
|
|
1141
|
-
done
|
|
1142
|
-
|
|
1143
|
-
# Source set: proposal/design/tasks + delta specs.
|
|
1144
|
-
local files=("$dir/proposal.md" "$dir/design.md" "$dir/tasks.md")
|
|
1145
|
-
if [ -d "$dir/specs" ]; then
|
|
1146
|
-
while IFS= read -r s; do files+=("$s"); done < <(find "$dir/specs" -path '*/spec.md' -type f 2>/dev/null | sort)
|
|
1147
|
-
fi
|
|
1148
|
-
|
|
1149
|
-
local hdir="$dir/.comate/handoff"
|
|
1150
|
-
mkdir -p "$hdir"
|
|
1151
|
-
local cjson="$hdir/design-context.json" cmd="$hdir/design-context.md"
|
|
1152
|
-
local chash; chash=$(hash_files_manifest "${files[@]}")
|
|
1153
|
-
|
|
1154
|
-
# markdown
|
|
1155
|
-
{
|
|
1156
|
-
echo "# comate Design Handoff"
|
|
1157
|
-
echo ""
|
|
1158
|
-
echo "- Change: $name"
|
|
1159
|
-
echo "- Phase: design"
|
|
1160
|
-
echo "- Mode: $hmode"
|
|
1161
|
-
echo "- Context hash: $chash"
|
|
1162
|
-
echo ""
|
|
1163
|
-
echo "Generated-by: comate"
|
|
1164
|
-
echo ""
|
|
1165
|
-
echo "OpenSpec remains the canonical capability spec. This handoff is a deterministic, source-traceable context pack, not an agent-authored summary."
|
|
1166
|
-
echo ""
|
|
1167
|
-
for f in "${files[@]}"; do
|
|
1168
|
-
[ -f "$f" ] || continue
|
|
1169
|
-
_handoff_excerpt "$f" 80 "$hmode"
|
|
1170
|
-
done
|
|
1171
|
-
} > "$cmd"
|
|
1172
|
-
|
|
1173
|
-
# json
|
|
1174
|
-
{
|
|
1175
|
-
echo "{"
|
|
1176
|
-
echo " \"change\": \"$(json_escape "$name")\","
|
|
1177
|
-
echo " \"phase\": \"design\","
|
|
1178
|
-
echo " \"mode\": \"$hmode\","
|
|
1179
|
-
echo " \"canonical_spec\": \"openspec\","
|
|
1180
|
-
echo " \"generated_by\": \"comate\","
|
|
1181
|
-
echo " \"context_hash\": \"$chash\","
|
|
1182
|
-
echo " \"files\": ["
|
|
1183
|
-
local first=1
|
|
1184
|
-
for f in "${files[@]}"; do
|
|
1185
|
-
[ -f "$f" ] || continue
|
|
1186
|
-
[ "$first" -eq 0 ] && echo ","
|
|
1187
|
-
first=0
|
|
1188
|
-
printf ' { "path": "%s", "sha256": "%s" }' "$(json_escape "$f")" "$(hash_file "$f")"
|
|
1189
|
-
done
|
|
1190
|
-
echo ""
|
|
1191
|
-
echo " ]"
|
|
1192
|
-
echo "}"
|
|
1193
|
-
} > "$cjson"
|
|
1194
|
-
|
|
1195
|
-
yaml_set "$yaml" handoff_context "$cjson"
|
|
1196
|
-
yaml_set "$yaml" handoff_hash "$chash"
|
|
1197
|
-
log_pass "[HANDOFF] wrote $cjson"
|
|
1198
|
-
log_pass "[HANDOFF] wrote $cmd"
|
|
1199
|
-
log_pass "[HANDOFF] handoff_hash=$chash"
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
# build->verify handoff: hash plan + tasks.
|
|
1203
|
-
_handoff_build() {
|
|
1204
|
-
local name="$1" dir="$2" yaml="$3"
|
|
1205
|
-
[ "$(yaml_get "$yaml" phase)" = "build" ] || die "$COMATE_BLOCK" "ERROR: build handoff requires phase: build"
|
|
1206
|
-
[ -s "$dir/tasks.md" ] || die "$COMATE_BLOCK" "ERROR: tasks.md missing/empty: $dir/tasks.md"
|
|
1207
|
-
|
|
1208
|
-
local plan; plan=$(yaml_get "$yaml" plan)
|
|
1209
|
-
local files=("$dir/tasks.md")
|
|
1210
|
-
if [ -n "$plan" ] && [ "$plan" != "null" ] && [ -f "$plan" ]; then
|
|
1211
|
-
files=("$plan" "$dir/tasks.md")
|
|
1212
|
-
fi
|
|
1213
|
-
|
|
1214
|
-
local hdir="$dir/.comate/handoff"
|
|
1215
|
-
mkdir -p "$hdir"
|
|
1216
|
-
local cjson="$hdir/impl-context.json"
|
|
1217
|
-
local chash; chash=$(hash_files_manifest "${files[@]}")
|
|
1218
|
-
|
|
1219
|
-
{
|
|
1220
|
-
echo "{"
|
|
1221
|
-
echo " \"change\": \"$(json_escape "$name")\","
|
|
1222
|
-
echo " \"phase\": \"build\","
|
|
1223
|
-
echo " \"generated_by\": \"comate\","
|
|
1224
|
-
echo " \"impl_hash\": \"$chash\","
|
|
1225
|
-
echo " \"files\": ["
|
|
1226
|
-
local first=1 f
|
|
1227
|
-
for f in "${files[@]}"; do
|
|
1228
|
-
[ -f "$f" ] || continue
|
|
1229
|
-
[ "$first" -eq 0 ] && echo ","
|
|
1230
|
-
first=0
|
|
1231
|
-
printf ' { "path": "%s", "sha256": "%s" }' "$(json_escape "$f")" "$(hash_file "$f")"
|
|
1232
|
-
done
|
|
1233
|
-
echo ""
|
|
1234
|
-
echo " ]"
|
|
1235
|
-
echo "}"
|
|
1236
|
-
} > "$cjson"
|
|
1237
|
-
|
|
1238
|
-
yaml_set "$yaml" impl_context "$cjson"
|
|
1239
|
-
yaml_set "$yaml" impl_hash "$chash"
|
|
1240
|
-
log_pass "[HANDOFF] wrote $cjson"
|
|
1241
|
-
log_pass "[HANDOFF] impl_hash=$chash"
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
# =====================================================================
|
|
1245
|
-
# sources — knowledge-base provenance for a change
|
|
1246
|
-
# =====================================================================
|
|
1247
|
-
|
|
1248
|
-
# _sources_pin_ref <yaml> — echo the commit ref to pin a tracked file to:
|
|
1249
|
-
# the change's base_ref, falling back to current HEAD, else "null".
|
|
1250
|
-
_sources_pin_ref() {
|
|
1251
|
-
local yaml="$1" ref
|
|
1252
|
-
ref=$(yaml_get "$yaml" base_ref)
|
|
1253
|
-
if [ -z "$ref" ] || [ "$ref" = "null" ]; then
|
|
1254
|
-
ref=$(git rev-parse --short HEAD 2>/dev/null || echo "null")
|
|
1255
|
-
elif git rev-parse --git-dir >/dev/null 2>&1; then
|
|
1256
|
-
ref=$(git rev-parse --short "$ref" 2>/dev/null || echo "$ref")
|
|
1257
|
-
fi
|
|
1258
|
-
echo "$ref"
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
# sources <name> track <file...> [--note <text>] — record knowledge-base files
|
|
1262
|
-
# as the source of this change. Out-of-range files are skipped (caller stays on
|
|
1263
|
-
# conversation). Re-tracking a path replaces its prior record.
|
|
1264
|
-
sources_track() {
|
|
1265
|
-
local name="$1"; shift
|
|
1266
|
-
validate_change_name "$name"
|
|
1267
|
-
local yaml; yaml=$(require_yaml "$name")
|
|
1268
|
-
local manifest; manifest=$(sources_manifest_for "$name")
|
|
1269
|
-
|
|
1270
|
-
local note="" files=()
|
|
1271
|
-
while [ $# -gt 0 ]; do
|
|
1272
|
-
case "$1" in
|
|
1273
|
-
--note) note="${2:-}"; shift 2 || shift ;;
|
|
1274
|
-
*) files+=("$1"); shift ;;
|
|
1275
|
-
esac
|
|
1276
|
-
done
|
|
1277
|
-
[ "${#files[@]}" -gt 0 ] || die "$COMATE_USAGE" "Usage: comate.sh sources <name> track <file...> [--note <text>]"
|
|
1278
|
-
|
|
1279
|
-
local ref; ref=$(_sources_pin_ref "$yaml")
|
|
1280
|
-
mkdir -p "$(dirname "$manifest")"
|
|
1281
|
-
[ -f "$manifest" ] || : > "$manifest"
|
|
1282
|
-
|
|
1283
|
-
local tracked=0 skipped=0 f rel
|
|
1284
|
-
for f in "${files[@]}"; do
|
|
1285
|
-
if rel=$(validate_kb_path "$f"); then
|
|
1286
|
-
_sources_remove_path "$manifest" "$rel"
|
|
1287
|
-
{
|
|
1288
|
-
[ -s "$manifest" ] && printf -- '---\n'
|
|
1289
|
-
printf 'path:%s\n' "$rel"
|
|
1290
|
-
printf 'sha256:%s\n' "$(hash_file "$rel")"
|
|
1291
|
-
printf 'ref:%s\n' "$ref"
|
|
1292
|
-
[ -n "$note" ] && printf 'note:%s\n' "$note"
|
|
1293
|
-
} >> "$manifest"
|
|
1294
|
-
log_pass "[SOURCES] tracked $rel"
|
|
1295
|
-
tracked=$((tracked + 1))
|
|
1296
|
-
else
|
|
1297
|
-
log_warn "[SOURCES] not in $(comate_kb_root)/ or unreadable, skipped: $f"
|
|
1298
|
-
skipped=$((skipped + 1))
|
|
1299
|
-
fi
|
|
1300
|
-
done
|
|
1301
|
-
|
|
1302
|
-
if [ "$tracked" -gt 0 ]; then
|
|
1303
|
-
yaml_set "$yaml" source_kind file
|
|
1304
|
-
yaml_set "$yaml" source_manifest "$manifest"
|
|
1305
|
-
log_pass "[SOURCES] source_kind=file, source_manifest=$manifest"
|
|
1306
|
-
else
|
|
1307
|
-
log_warn "[SOURCES] no file tracked; source_kind stays conversation"
|
|
1308
|
-
fi
|
|
1309
|
-
[ "$skipped" -eq 0 ] || return "$COMATE_OK"
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
# _sources_remove_path <manifest> <path> — drop the record block for one path.
|
|
1313
|
-
_sources_remove_path() {
|
|
1314
|
-
local manifest="$1" target="$2" tmp
|
|
1315
|
-
[ -f "$manifest" ] || return 0
|
|
1316
|
-
tmp=$(mktemp "${TMPDIR:-/tmp}/comate.XXXXXX") || die "$COMATE_ENV" "ERROR: mktemp failed"
|
|
1317
|
-
awk -v target="$target" '
|
|
1318
|
-
function flush() {
|
|
1319
|
-
if (block != "" && keep) printf "%s", block
|
|
1320
|
-
block=""; keep=1
|
|
1321
|
-
}
|
|
1322
|
-
BEGIN { keep=1 }
|
|
1323
|
-
/^---$/ { flush(); next }
|
|
1324
|
-
/^path:/ { if ($0 == "path:" target) keep=0 }
|
|
1325
|
-
{ block = block $0 "\n" }
|
|
1326
|
-
END { flush() }
|
|
1327
|
-
' "$manifest" > "$tmp"
|
|
1328
|
-
# Collapse any leading blank lines, then normalise record separators.
|
|
1329
|
-
awk 'NF || NR>1 {print}' "$tmp" > "$manifest"
|
|
1330
|
-
rm -f "$tmp"
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
# sources <name> check — drift check; conversation skips. Mirrors guard codes.
|
|
1334
|
-
sources_check() {
|
|
1335
|
-
local name="$1"
|
|
1336
|
-
validate_change_name "$name"
|
|
1337
|
-
local yaml; yaml=$(require_yaml "$name")
|
|
1338
|
-
local kind; kind=$(yaml_get "$yaml" source_kind)
|
|
1339
|
-
if [ "$kind" != "file" ]; then
|
|
1340
|
-
log_info "[SOURCES] source_kind=$kind — no source files to check"
|
|
1341
|
-
exit "$COMATE_OK"
|
|
1342
|
-
fi
|
|
1343
|
-
local manifest; manifest=$(yaml_get "$yaml" source_manifest)
|
|
1344
|
-
if [ -z "$manifest" ] || [ "$manifest" = "null" ]; then
|
|
1345
|
-
die "$COMATE_CORRUPT" "ERROR: source_kind=file but source_manifest is null"
|
|
1346
|
-
fi
|
|
1347
|
-
local out rc=0
|
|
1348
|
-
out=$(sources_drift "$manifest") || rc=$?
|
|
1349
|
-
[ -n "$out" ] && printf '%s\n' "$out" >&2
|
|
1350
|
-
if [ "$rc" -eq 0 ]; then
|
|
1351
|
-
log_pass "[SOURCES] no drift ($manifest)"
|
|
1352
|
-
fi
|
|
1353
|
-
exit "$rc"
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
# sources <name> list — print the manifest.
|
|
1357
|
-
sources_list() {
|
|
1358
|
-
local name="$1"
|
|
1359
|
-
validate_change_name "$name"
|
|
1360
|
-
local manifest; manifest=$(sources_manifest_for "$name")
|
|
1361
|
-
if [ -f "$manifest" ]; then cat "$manifest"; else echo "no source manifest for $name"; fi
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
# sources find <file> — reverse lookup: which changes reference this file.
|
|
1365
|
-
sources_find() {
|
|
1366
|
-
local target="$1"
|
|
1367
|
-
[ -n "$target" ] || die "$COMATE_USAGE" "Usage: comate.sh sources find <file>"
|
|
1368
|
-
local rel="$target"
|
|
1369
|
-
if r=$(validate_kb_path "$target" 2>/dev/null); then rel="$r"; fi
|
|
1370
|
-
local any=0 m chg
|
|
1371
|
-
while IFS= read -r m; do
|
|
1372
|
-
if grep -q "^path:$rel$" "$m" 2>/dev/null; then
|
|
1373
|
-
chg=$(basename "$(dirname "$(dirname "$m")")")
|
|
1374
|
-
echo "$chg ($m)"
|
|
1375
|
-
any=1
|
|
1376
|
-
fi
|
|
1377
|
-
done < <(find openspec/changes -path '*/.comate/sources.manifest' -type f 2>/dev/null | sort)
|
|
1378
|
-
[ "$any" -eq 0 ] && echo "no change references: $rel"
|
|
1379
|
-
return 0
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
# dependency <name> archived — prove completion from archived state, rather
|
|
1383
|
-
# than inferring it from absence in `openspec list`.
|
|
1384
|
-
dependency_archived() {
|
|
1385
|
-
local name="$1"
|
|
1386
|
-
validate_change_name "$name"
|
|
1387
|
-
[ ! -d "openspec/changes/$name" ] || \
|
|
1388
|
-
die "$COMATE_BLOCK" "BLOCK: dependency '$name' is still active"
|
|
1389
|
-
local found="" yaml
|
|
1390
|
-
while IFS= read -r yaml; do
|
|
1391
|
-
[ "$(yaml_get "$yaml" archived)" = "true" ] && found="$yaml"
|
|
1392
|
-
done < <(find openspec/changes/archive -mindepth 2 -maxdepth 2 \
|
|
1393
|
-
-path "*/????-??-??-${name}/.comate.yaml" -type f 2>/dev/null | sort)
|
|
1394
|
-
[ -n "$found" ] || \
|
|
1395
|
-
die "$COMATE_BLOCK" "BLOCK: no archived=true record found for dependency '$name'"
|
|
1396
|
-
log_pass "[DEPENDENCY] archived: $name ($found)"
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
# =====================================================================
|
|
1400
|
-
# gate — phase 0: PRD quality gate + split precondition
|
|
1401
|
-
# =====================================================================
|
|
1402
|
-
|
|
1403
|
-
# require_prd <prd> — ensure the PRD path is real, non-empty, and conventionally named.
|
|
1404
|
-
validate_prd_filename() {
|
|
1405
|
-
local prd="$1" base
|
|
1406
|
-
base=$(basename "$prd")
|
|
1407
|
-
case "$base" in
|
|
1408
|
-
PRD-[0-9][0-9][0-9]-?*.md) return 0 ;;
|
|
1409
|
-
*) die "$COMATE_USAGE" "ERROR: PRD filename must match PRD-NNN-title.md: $base" ;;
|
|
1410
|
-
esac
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
require_prd() {
|
|
1414
|
-
local prd="$1"
|
|
1415
|
-
[ -n "$prd" ] || die "$COMATE_USAGE" "ERROR: PRD path required"
|
|
1416
|
-
case "$prd" in *..*) die "$COMATE_USAGE" "ERROR: PRD path cannot contain '..': $prd" ;; esac
|
|
1417
|
-
[ -f "$prd" ] || die "$COMATE_CORRUPT" "ERROR: PRD not found: $prd"
|
|
1418
|
-
file_nonempty "$prd" || die "$COMATE_CORRUPT" "ERROR: PRD is empty: $prd"
|
|
1419
|
-
validate_prd_filename "$prd"
|
|
1420
|
-
echo "$prd"
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
# gate <prd> review — create or refresh gate.yaml next to the PRD and pin the
|
|
1424
|
-
# current PRD hash. Existing passed gates are re-opened to `reviewing` only when
|
|
1425
|
-
# the PRD changed (hash drift); otherwise the review report path is (re)emitted
|
|
1426
|
-
# so the skill knows where to write its human-readable audit. The skill does the
|
|
1427
|
-
# semantic scoring and writes score/blocking_open/gate_status via `gate set`.
|
|
1428
|
-
gate_review() {
|
|
1429
|
-
local prd; prd=$(require_prd "$1")
|
|
1430
|
-
local gate report cur_hash
|
|
1431
|
-
gate=$(gate_path_for "$prd")
|
|
1432
|
-
report=$(gate_report_for "$prd")
|
|
1433
|
-
cur_hash=$(hash_file "$prd")
|
|
1434
|
-
|
|
1435
|
-
if [ ! -f "$gate" ]; then
|
|
1436
|
-
cat > "$gate" <<EOF
|
|
1437
|
-
# comate PRD gate — flat key:value only, no nesting
|
|
1438
|
-
schema_version: $GATE_SCHEMA_VERSION
|
|
1439
|
-
prd_path: $prd
|
|
1440
|
-
prd_hash: $cur_hash
|
|
1441
|
-
gate_status: reviewing
|
|
1442
|
-
score: null
|
|
1443
|
-
blocking_open: null
|
|
1444
|
-
review_report: $report
|
|
1445
|
-
spawned_changes: null
|
|
1446
|
-
changes_map: null
|
|
1447
|
-
gated_at: null
|
|
1448
|
-
created_at: $(today)
|
|
1449
|
-
EOF
|
|
1450
|
-
log_pass "[GATE] created $gate (status=reviewing)"
|
|
1451
|
-
else
|
|
1452
|
-
local old_hash old_status
|
|
1453
|
-
old_hash=$(yaml_get "$gate" prd_hash)
|
|
1454
|
-
old_status=$(yaml_get "$gate" gate_status)
|
|
1455
|
-
gate_yaml_set "$gate" prd_path "$prd"
|
|
1456
|
-
gate_yaml_set "$gate" review_report "$report"
|
|
1457
|
-
if [ "$old_hash" != "$cur_hash" ]; then
|
|
1458
|
-
gate_yaml_set "$gate" prd_hash "$cur_hash"
|
|
1459
|
-
gate_yaml_set "$gate" gate_status reviewing
|
|
1460
|
-
log_warn "[GATE] PRD changed since last review — status reset to reviewing (was $old_status)"
|
|
1461
|
-
else
|
|
1462
|
-
log_info "[GATE] $gate (status=$old_status, unchanged PRD)"
|
|
1463
|
-
fi
|
|
1464
|
-
fi
|
|
1465
|
-
echo "review_report: $report"
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
# gate <prd> set <field> <value> — write a gate field (score/blocking_open/
|
|
1469
|
-
# gate_status/…) with the same validation as gate_yaml_set.
|
|
1470
|
-
gate_set() {
|
|
1471
|
-
local prd; prd=$(require_prd "$1")
|
|
1472
|
-
local field="${2:-}" value="${3:-}"
|
|
1473
|
-
[ -n "$field" ] || die "$COMATE_USAGE" "Usage: comate.sh gate <prd> set <field> <value>"
|
|
1474
|
-
local gate; gate=$(gate_path_for "$prd")
|
|
1475
|
-
[ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found (run 'gate review' first): $gate"
|
|
1476
|
-
if [ "$field" = "changes_map" ]; then
|
|
1477
|
-
gate_validate_changes_map "$value"
|
|
1478
|
-
local current_map current_status
|
|
1479
|
-
current_map=$(yaml_get "$gate" changes_map)
|
|
1480
|
-
current_status=$(yaml_get "$gate" gate_status)
|
|
1481
|
-
case "$current_status" in
|
|
1482
|
-
passed|passed-with-tbd)
|
|
1483
|
-
[ "$value" = "$current_map" ] || \
|
|
1484
|
-
die "$COMATE_BLOCK" "BLOCK: changes_map is immutable after gate pass; re-run gate review before remapping"
|
|
1485
|
-
;;
|
|
1486
|
-
esac
|
|
1487
|
-
fi
|
|
1488
|
-
gate_yaml_set "$gate" "$field" "$value"
|
|
1489
|
-
log_pass "[GATE] set $field=$value"
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
# gate_validate_changes_map <changes_map> — reject malformed entries and
|
|
1493
|
-
# duplicate names when the user-confirmed mapping is first persisted.
|
|
1494
|
-
gate_validate_changes_map() {
|
|
1495
|
-
local map="$1" rest entry name items seen=""
|
|
1496
|
-
[ -n "$map" ] && [ "$map" != "null" ] || \
|
|
1497
|
-
die "$COMATE_USAGE" "ERROR: changes_map cannot be empty"
|
|
1498
|
-
case "$map" in
|
|
1499
|
-
\;*|*\;\;*|*\;) die "$COMATE_USAGE" "ERROR: changes_map contains an empty entry" ;;
|
|
1500
|
-
esac
|
|
1501
|
-
rest="$map"
|
|
1502
|
-
while [ -n "$rest" ]; do
|
|
1503
|
-
case "$rest" in
|
|
1504
|
-
*\;*) entry="${rest%%;*}"; rest="${rest#*;}" ;;
|
|
1505
|
-
*) entry="$rest"; rest="" ;;
|
|
1506
|
-
esac
|
|
1507
|
-
name="${entry%%:*}"
|
|
1508
|
-
items="${entry#*:}"
|
|
1509
|
-
[ -n "$name" ] && [ "$name" != "$entry" ] || \
|
|
1510
|
-
die "$COMATE_USAGE" "ERROR: malformed changes_map entry '$entry'"
|
|
1511
|
-
[ -n "$(printf '%s' "$items" | tr -d '[:space:]')" ] || \
|
|
1512
|
-
die "$COMATE_USAGE" "ERROR: empty function mapping for change '$name'"
|
|
1513
|
-
validate_change_name "$name"
|
|
1514
|
-
case ",$seen," in
|
|
1515
|
-
*",$name,"*) die "$COMATE_USAGE" "ERROR: duplicate change '$name' in changes_map" ;;
|
|
1516
|
-
esac
|
|
1517
|
-
[ -n "$seen" ] && seen="$seen,"
|
|
1518
|
-
seen="$seen$name"
|
|
1519
|
-
done
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
# gate <prd> pass [--tbd] — flip to passed/passed-with-tbd. Normal pass requires
|
|
1523
|
-
# zero blockers; --tbd requires at least one explicitly accepted blocker.
|
|
1524
|
-
gate_pass() {
|
|
1525
|
-
local prd; prd=$(require_prd "$1")
|
|
1526
|
-
local tbd=0
|
|
1527
|
-
[ "${2:-}" = "--tbd" ] && tbd=1
|
|
1528
|
-
local gate; gate=$(gate_path_for "$prd")
|
|
1529
|
-
[ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found (run 'gate review' first): $gate"
|
|
1530
|
-
|
|
1531
|
-
local score blocking changes_map
|
|
1532
|
-
score=$(yaml_get "$gate" score)
|
|
1533
|
-
blocking=$(yaml_get "$gate" blocking_open)
|
|
1534
|
-
changes_map=$(yaml_get "$gate" changes_map)
|
|
1535
|
-
[[ "$score" =~ ^[0-9]+$ ]] && [ "$score" -ge 0 ] && [ "$score" -le 100 ] || \
|
|
1536
|
-
die "$COMATE_BLOCK" "BLOCK: score must be an integer from 0 to 100 (got '$score')"
|
|
1537
|
-
[[ "$blocking" =~ ^[0-9]+$ ]] || \
|
|
1538
|
-
die "$COMATE_BLOCK" "BLOCK: blocking_open must be a non-negative integer (got '$blocking')"
|
|
1539
|
-
if [ "$tbd" -eq 1 ]; then
|
|
1540
|
-
[ "$blocking" -gt 0 ] || \
|
|
1541
|
-
die "$COMATE_BLOCK" "BLOCK: --tbd requires at least one recorded unresolved blocker"
|
|
1542
|
-
elif [ "$blocking" -ne 0 ]; then
|
|
1543
|
-
die "$COMATE_BLOCK" "BLOCK: blocking_open='$blocking' — resolve all blocking issues or use --tbd after explicit user authorization"
|
|
1544
|
-
fi
|
|
1545
|
-
if [ -z "$changes_map" ] || [ "$changes_map" = "null" ]; then
|
|
1546
|
-
die "$COMATE_BLOCK" "BLOCK: changes_map is required before passing the gate"
|
|
1547
|
-
fi
|
|
1548
|
-
gate_validate_changes_map "$changes_map"
|
|
1549
|
-
local report; report=$(yaml_get "$gate" review_report)
|
|
1550
|
-
if [ -z "$report" ] || [ "$report" = "null" ] || ! file_nonempty "$report"; then
|
|
1551
|
-
die "$COMATE_BLOCK" "BLOCK: review report missing or empty ($report) — the gate report is mandatory"
|
|
1552
|
-
fi
|
|
1553
|
-
|
|
1554
|
-
gate_yaml_set "$gate" prd_hash "$(hash_file "$prd")"
|
|
1555
|
-
if [ "$tbd" -eq 1 ]; then
|
|
1556
|
-
gate_yaml_set "$gate" gate_status passed-with-tbd
|
|
1557
|
-
else
|
|
1558
|
-
gate_yaml_set "$gate" gate_status passed
|
|
1559
|
-
fi
|
|
1560
|
-
gate_yaml_set "$gate" gated_at "$(today)"
|
|
1561
|
-
log_pass "[GATE] $(yaml_get "$gate" gate_status) — changes may now be spawned from $prd"
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
# gate <prd> spawned <change> — append a change name to spawned_changes
|
|
1565
|
-
# (comma-joined), recording which changes this PRD fanned out into.
|
|
1566
|
-
gate_spawned() {
|
|
1567
|
-
local prd; prd=$(require_prd "$1")
|
|
1568
|
-
local change="${2:-}"
|
|
1569
|
-
validate_change_name "$change"
|
|
1570
|
-
local gate; gate=$(gate_path_for "$prd")
|
|
1571
|
-
[ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found: $gate"
|
|
1572
|
-
local changes_map; changes_map=$(yaml_get "$gate" changes_map)
|
|
1573
|
-
gate_change_is_mapped "$changes_map" "$change" || \
|
|
1574
|
-
die "$COMATE_BLOCK" "BLOCK: change '$change' is not present in changes_map"
|
|
1575
|
-
local cur; cur=$(yaml_get "$gate" spawned_changes)
|
|
1576
|
-
case ",$cur," in
|
|
1577
|
-
*",$change,"*) log_info "[GATE] $change already recorded" ; return 0 ;;
|
|
1578
|
-
esac
|
|
1579
|
-
gate_check "$prd" >/dev/null
|
|
1580
|
-
gate_require_skeleton "$prd" "$change"
|
|
1581
|
-
if [ -z "$cur" ] || [ "$cur" = "null" ]; then
|
|
1582
|
-
gate_yaml_set "$gate" spawned_changes "$change"
|
|
1583
|
-
else
|
|
1584
|
-
gate_yaml_set "$gate" spawned_changes "$cur,$change"
|
|
1585
|
-
fi
|
|
1586
|
-
log_pass "[GATE] recorded spawned change: $change"
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
# gate_require_skeleton <prd> <change> — prove that comate-open finished the
|
|
1590
|
-
# gate skeleton before it is counted as spawned.
|
|
1591
|
-
gate_require_skeleton() {
|
|
1592
|
-
local prd="$1" change="$2" dir yaml manifest tracked_prd f
|
|
1593
|
-
dir=$(comate_change_dir "$change")
|
|
1594
|
-
for f in .openspec.yaml proposal.md design.md tasks.md .comate.yaml; do
|
|
1595
|
-
file_nonempty "$dir/$f" || \
|
|
1596
|
-
die "$COMATE_BLOCK" "BLOCK: change '$change' skeleton incomplete: missing or empty $dir/$f"
|
|
1597
|
-
done
|
|
1598
|
-
yaml="$dir/.comate.yaml"
|
|
1599
|
-
validate_flat_yaml "$yaml"
|
|
1600
|
-
[ "$(yaml_get "$yaml" phase)" = "open" ] || \
|
|
1601
|
-
die "$COMATE_BLOCK" "BLOCK: change '$change' skeleton must remain in phase=open"
|
|
1602
|
-
[ "$(yaml_get "$yaml" source_kind)" = "file" ] || \
|
|
1603
|
-
die "$COMATE_BLOCK" "BLOCK: change '$change' skeleton has not tracked its PRD source"
|
|
1604
|
-
manifest=$(yaml_get "$yaml" source_manifest)
|
|
1605
|
-
[ -n "$manifest" ] && [ "$manifest" != "null" ] && [ -f "$manifest" ] || \
|
|
1606
|
-
die "$COMATE_BLOCK" "BLOCK: change '$change' source manifest is missing"
|
|
1607
|
-
tracked_prd=$(validate_kb_path "$prd") || \
|
|
1608
|
-
die "$COMATE_BLOCK" "BLOCK: PRD source path is outside the knowledge base: $prd"
|
|
1609
|
-
local record_ok=0 line record_path="" record_hash=""
|
|
1610
|
-
while IFS= read -r line || [ -n "$line" ]; do
|
|
1611
|
-
case "$line" in
|
|
1612
|
-
path:*) record_path="${line#path:}"; record_hash="" ;;
|
|
1613
|
-
sha256:*)
|
|
1614
|
-
record_hash="${line#sha256:}"
|
|
1615
|
-
if [ "$record_path" = "$tracked_prd" ] && [ -n "$record_hash" ]; then record_ok=1; fi
|
|
1616
|
-
;;
|
|
1617
|
-
---|"") record_path=""; record_hash="" ;;
|
|
1618
|
-
esac
|
|
1619
|
-
done < "$manifest"
|
|
1620
|
-
[ "$record_ok" -eq 1 ] || \
|
|
1621
|
-
die "$COMATE_BLOCK" "BLOCK: change '$change' source manifest lacks a complete path/sha256 record for $tracked_prd"
|
|
1622
|
-
local drift rc=0
|
|
1623
|
-
drift=$(sources_drift "$manifest") || rc=$?
|
|
1624
|
-
[ "$rc" -eq 0 ] || die "$COMATE_BLOCK" "BLOCK: change '$change' source evidence is stale: $drift"
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
# gate_change_names <changes_map> — emit mapped change names, one per line.
|
|
1628
|
-
# The persisted format is `name:function item,...;name:function item,...`.
|
|
1629
|
-
gate_change_names() {
|
|
1630
|
-
local map="$1" entry name
|
|
1631
|
-
[ -n "$map" ] && [ "$map" != "null" ] || return 0
|
|
1632
|
-
gate_validate_changes_map "$map"
|
|
1633
|
-
while [ -n "$map" ]; do
|
|
1634
|
-
case "$map" in
|
|
1635
|
-
*\;*) entry="${map%%;*}"; map="${map#*;}" ;;
|
|
1636
|
-
*) entry="$map"; map="" ;;
|
|
1637
|
-
esac
|
|
1638
|
-
name="${entry%%:*}"
|
|
1639
|
-
[ -n "$name" ] || die "$COMATE_USAGE" "ERROR: malformed changes_map entry '$entry'"
|
|
1640
|
-
[ "$name" != "$entry" ] || die "$COMATE_USAGE" "ERROR: malformed changes_map entry '$entry'"
|
|
1641
|
-
validate_change_name "$name"
|
|
1642
|
-
printf '%s\n' "$name"
|
|
1643
|
-
done
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
# gate_change_is_mapped <changes_map> <change> — membership test for a mapped name.
|
|
1647
|
-
gate_change_is_mapped() {
|
|
1648
|
-
local map="$1" wanted="$2" name
|
|
1649
|
-
gate_validate_changes_map "$map"
|
|
1650
|
-
while IFS= read -r name; do
|
|
1651
|
-
[ "$name" = "$wanted" ] && return 0
|
|
1652
|
-
done <<EOF
|
|
1653
|
-
$(gate_change_names "$map")
|
|
1654
|
-
EOF
|
|
1655
|
-
return 1
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
# gate_fanout_status <prd> [--json] — calculate persisted fanout progress.
|
|
1659
|
-
# This is deliberately derived from changes_map and spawned_changes so an
|
|
1660
|
-
# interrupted run can resume from the missing names instead of relying on
|
|
1661
|
-
# conversation/task state.
|
|
1662
|
-
gate_fanout_status() {
|
|
1663
|
-
local prd; prd=$(require_prd "$1")
|
|
1664
|
-
local json="${2:-}" gate; gate=$(gate_path_for "$prd")
|
|
1665
|
-
[ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found: $gate"
|
|
1666
|
-
local map spawned name expected=0 count=0 missing="" invalid=""
|
|
1667
|
-
map=$(yaml_get "$gate" changes_map); spawned=$(yaml_get "$gate" spawned_changes)
|
|
1668
|
-
gate_validate_changes_map "$map"
|
|
1669
|
-
while IFS= read -r name; do
|
|
1670
|
-
[ -n "$name" ] || continue
|
|
1671
|
-
expected=$((expected + 1))
|
|
1672
|
-
case ",$spawned," in
|
|
1673
|
-
*",$name,"*) count=$((count + 1)) ;;
|
|
1674
|
-
*) [ -n "$missing" ] && missing="$missing,"; missing="$missing$name" ;;
|
|
1675
|
-
esac
|
|
1676
|
-
done <<EOF
|
|
1677
|
-
$(gate_change_names "$map")
|
|
1678
|
-
EOF
|
|
1679
|
-
if [ -n "$spawned" ] && [ "$spawned" != "null" ]; then
|
|
1680
|
-
local rest="$spawned" item
|
|
1681
|
-
while [ -n "$rest" ]; do
|
|
1682
|
-
case "$rest" in
|
|
1683
|
-
*,*) item="${rest%%,*}"; rest="${rest#*,}" ;;
|
|
1684
|
-
*) item="$rest"; rest="" ;;
|
|
1685
|
-
esac
|
|
1686
|
-
gate_change_is_mapped "$map" "$item" || { [ -n "$invalid" ] && invalid="$invalid,"; invalid="$invalid$item"; }
|
|
1687
|
-
done
|
|
1688
|
-
fi
|
|
1689
|
-
local complete=false
|
|
1690
|
-
[ "$expected" -gt 0 ] && [ "$count" -eq "$expected" ] && [ -z "$invalid" ] && complete=true
|
|
1691
|
-
if [ "$json" = "--json" ]; then
|
|
1692
|
-
printf '{"expected":%s,"spawned":%s,"complete":%s,"missing":"%s","invalid":"%s"}\n' \
|
|
1693
|
-
"$expected" "$count" "$complete" "$(json_escape "$missing")" "$(json_escape "$invalid")"
|
|
1694
|
-
else
|
|
1695
|
-
[ -z "$missing" ] && missing=none
|
|
1696
|
-
[ -z "$invalid" ] && invalid=none
|
|
1697
|
-
echo "fanout: expected=$expected spawned=$count complete=$complete missing=$missing invalid=$invalid"
|
|
1698
|
-
fi
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
# _gate_is_stale <gate> <prd> — true (0) if the recorded hash no longer matches
|
|
1702
|
-
# the PRD on disk.
|
|
1703
|
-
_gate_is_stale() {
|
|
1704
|
-
local gate="$1" prd="$2" recorded actual
|
|
1705
|
-
recorded=$(yaml_get "$gate" prd_hash)
|
|
1706
|
-
[ -f "$prd" ] || return 0
|
|
1707
|
-
actual=$(hash_file "$prd")
|
|
1708
|
-
[ "$recorded" != "$actual" ]
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
# gate <prd> status [--json] — print current gate status, computing stale on the
|
|
1712
|
-
# fly (hash drift). Does not mutate the file.
|
|
1713
|
-
gate_status() {
|
|
1714
|
-
local prd="$1" json="${2:-}"
|
|
1715
|
-
[ -n "$prd" ] || die "$COMATE_USAGE" "Usage: comate.sh gate <prd> status [--json]"
|
|
1716
|
-
local gate; gate=$(gate_path_for "$prd")
|
|
1717
|
-
if [ ! -f "$gate" ]; then
|
|
1718
|
-
if [ "$json" = "--json" ]; then
|
|
1719
|
-
printf '{"gate":"none","prd":"%s"}\n' "$(json_escape "$prd")"
|
|
1720
|
-
else
|
|
1721
|
-
echo "gate: none ($prd) — run 'gate review' to start"
|
|
1722
|
-
fi
|
|
1723
|
-
return "$COMATE_OK"
|
|
1724
|
-
fi
|
|
1725
|
-
local st score blocking; st=$(yaml_get "$gate" gate_status)
|
|
1726
|
-
score=$(yaml_get "$gate" score); blocking=$(yaml_get "$gate" blocking_open)
|
|
1727
|
-
if [ -f "$prd" ] && _gate_is_stale "$gate" "$prd"; then st="stale"; fi
|
|
1728
|
-
if [ "$json" = "--json" ]; then
|
|
1729
|
-
printf '{"gate":"%s","status":"%s","score":"%s","blocking_open":"%s","report":"%s"}\n' \
|
|
1730
|
-
"$(json_escape "$gate")" "$(json_escape "$st")" "$(json_escape "$score")" \
|
|
1731
|
-
"$(json_escape "$blocking")" "$(json_escape "$(yaml_get "$gate" review_report)")"
|
|
1732
|
-
else
|
|
1733
|
-
echo "gate: $st score=$score blocking_open=$blocking ($gate)"
|
|
1734
|
-
fi
|
|
1735
|
-
return "$COMATE_OK"
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
# gate <prd> check — the precondition guard comate-open runs before spawning a
|
|
1739
|
-
# change. PASS (exit 0) requires: gate.yaml exists, status ∈ {passed,
|
|
1740
|
-
# passed-with-tbd}, and prd_hash matches the current PRD. Anything else BLOCKs.
|
|
1741
|
-
gate_check() {
|
|
1742
|
-
local prd; prd=$(require_prd "$1")
|
|
1743
|
-
local gate; gate=$(gate_path_for "$prd")
|
|
1744
|
-
if [ ! -f "$gate" ]; then
|
|
1745
|
-
die "$COMATE_BLOCK" "BLOCK: no gate for $prd — run /comate-gate to review the PRD before creating a change"
|
|
1746
|
-
fi
|
|
1747
|
-
local st; st=$(yaml_get "$gate" gate_status)
|
|
1748
|
-
case "$st" in
|
|
1749
|
-
passed|passed-with-tbd) ;;
|
|
1750
|
-
*) die "$COMATE_BLOCK" "BLOCK: gate_status='$st' (not passed) — a change cannot be spawned until the PRD gate passes" ;;
|
|
1751
|
-
esac
|
|
1752
|
-
if _gate_is_stale "$gate" "$prd"; then
|
|
1753
|
-
die "$COMATE_BLOCK" "BLOCK: PRD changed since it passed the gate (hash mismatch) — re-run /comate-gate to re-gate before spawning a change"
|
|
1754
|
-
fi
|
|
1755
|
-
log_pass "[GATE] check PASS ($st) — $prd"
|
|
1756
|
-
return "$COMATE_OK"
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
# =====================================================================
|
|
1760
|
-
# archive
|
|
1761
|
-
# =====================================================================
|
|
1762
|
-
|
|
1763
|
-
archive() {
|
|
1764
|
-
local name="$1" dry="${2:-}"
|
|
1765
|
-
validate_change_name "$name"
|
|
1766
|
-
local dry_run=0
|
|
1767
|
-
[ "$dry" = "--dry-run" ] && dry_run=1
|
|
1768
|
-
|
|
1769
|
-
local dir="openspec/changes/$name"
|
|
1770
|
-
local yaml="$dir/.comate.yaml"
|
|
1771
|
-
local today archive_name archive_dir
|
|
1772
|
-
today=$(today)
|
|
1773
|
-
archive_name="${today}-${name}"
|
|
1774
|
-
archive_dir="openspec/changes/archive/${archive_name}"
|
|
1775
|
-
|
|
1776
|
-
[ -f "$yaml" ] || die "$COMATE_CORRUPT" "FATAL: .comate.yaml not found in $dir"
|
|
1777
|
-
validate_flat_yaml "$yaml"
|
|
1778
|
-
|
|
1779
|
-
local phase verify_result archived design_doc plan
|
|
1780
|
-
phase=$(yaml_get "$yaml" phase)
|
|
1781
|
-
verify_result=$(yaml_get "$yaml" verify_result)
|
|
1782
|
-
archived=$(yaml_get "$yaml" archived)
|
|
1783
|
-
design_doc=$(yaml_get "$yaml" design_doc)
|
|
1784
|
-
plan=$(yaml_get "$yaml" plan)
|
|
1785
|
-
|
|
1786
|
-
[ "$phase" = "archive" ] || die "$COMATE_BLOCK" "FATAL: phase is '$phase', expected 'archive'"
|
|
1787
|
-
[ "$verify_result" = "pass" ] || die "$COMATE_BLOCK" "FATAL: verify_result is '$verify_result', expected 'pass'"
|
|
1788
|
-
[ "$archived" != "true" ] || die "$COMATE_BLOCK" "FATAL: change already archived"
|
|
1789
|
-
[ ! -d "$archive_dir" ] || die "$COMATE_BLOCK" "FATAL: archive target already exists: $archive_dir"
|
|
1790
|
-
|
|
1791
|
-
local steps_ok=0 steps_total=0
|
|
1792
|
-
_step_ok() { log_pass " [OK] $1"; steps_ok=$((steps_ok+1)); steps_total=$((steps_total+1)); }
|
|
1793
|
-
_step_dry() { log_warn " [DRY-RUN] $1"; steps_ok=$((steps_ok+1)); steps_total=$((steps_total+1)); }
|
|
1794
|
-
_step_fail(){ log_fail " [FAIL] $1"; steps_total=$((steps_total+1)); }
|
|
1795
|
-
|
|
1796
|
-
log_info "=== comate Archive: $name ==="
|
|
1797
|
-
_step_ok "Entry state verified (phase=archive, verify_result=pass)"
|
|
1798
|
-
|
|
1799
|
-
# OpenSpec owns delta-spec merge semantics. Do not copy change specs over
|
|
1800
|
-
# main specs directly; delta specs may contain ADDED/MODIFIED sections rather
|
|
1801
|
-
# than a complete canonical spec.
|
|
1802
|
-
local has_delta_specs=0
|
|
1803
|
-
if [ -d "$dir/specs" ] && find "$dir/specs" -path '*/spec.md' -type f -print -quit 2>/dev/null | grep -q .; then
|
|
1804
|
-
has_delta_specs=1
|
|
1805
|
-
fi
|
|
1806
|
-
|
|
1807
|
-
# Annotate frontmatter (design doc + plan), and rewrite any body references to
|
|
1808
|
-
# the change's pre-archive OpenSpec path so they survive the move (class B).
|
|
1809
|
-
# NOTE: design_doc/plan live under docs/superpowers and do NOT move with the
|
|
1810
|
-
# change. Annotation runs AFTER openspec has moved the change, using the REAL
|
|
1811
|
-
# archive name discovered from disk — comate must not guess the dir name, as a
|
|
1812
|
-
# locally-computed date can skew vs openspec's across a UTC/local midnight.
|
|
1813
|
-
local old_prefix="openspec/changes/$name/"
|
|
1814
|
-
local new_prefix="" # set to real archive path after the move
|
|
1815
|
-
local ann_archive_name="" # set to real archive name after the move
|
|
1816
|
-
_annotate() {
|
|
1817
|
-
local file="$1" extra="$2"
|
|
1818
|
-
[ -f "$file" ] || return 0
|
|
1819
|
-
if [ "$dry_run" -eq 1 ]; then _step_dry "Would annotate: $file"; return 0; fi
|
|
1820
|
-
local tmp; tmp=$(mktemp)
|
|
1821
|
-
if head -1 "$file" | grep -q '^---'; then
|
|
1822
|
-
awk -v archive="$ann_archive_name" -v extra="$extra" \
|
|
1823
|
-
-v oldp="$old_prefix" -v newp="$new_prefix" '
|
|
1824
|
-
/^archived-with:/ { next }
|
|
1825
|
-
NR==1 && /^---/ { print; next }
|
|
1826
|
-
/^---/ && NR>1 && fm==0 { print "archived-with: " archive; if (extra != "") print extra; fm=1; print; next }
|
|
1827
|
-
fm==1 { gsub(oldp, newp) } 1
|
|
1828
|
-
' "$file" > "$tmp"
|
|
1829
|
-
else
|
|
1830
|
-
{
|
|
1831
|
-
echo "---"; echo "archived-with: $ann_archive_name"; [ -n "$extra" ] && echo "$extra"; echo "status: final"; echo "---"
|
|
1832
|
-
sed "s#$old_prefix#$new_prefix#g" "$file"
|
|
1833
|
-
} > "$tmp"
|
|
1834
|
-
fi
|
|
1835
|
-
mv "$tmp" "$file"
|
|
1836
|
-
_step_ok "Annotated: $file"
|
|
1837
|
-
}
|
|
1838
|
-
# Dry-run previews annotation up front (real name not needed for the message).
|
|
1839
|
-
if [ "$dry_run" -eq 1 ]; then
|
|
1840
|
-
[ -n "$design_doc" ] && [ "$design_doc" != "null" ] && _annotate "$design_doc" "status: final"
|
|
1841
|
-
[ -n "$plan" ] && [ "$plan" != "null" ] && _annotate "$plan" ""
|
|
1842
|
-
fi
|
|
1843
|
-
|
|
1844
|
-
# Delegate spec updates and change movement to OpenSpec.
|
|
1845
|
-
if [ "$dry_run" -eq 1 ]; then
|
|
1846
|
-
if [ "$has_delta_specs" -eq 1 ]; then
|
|
1847
|
-
_step_dry "Would run: openspec archive $name -y"
|
|
1848
|
-
else
|
|
1849
|
-
_step_dry "Would run: openspec archive $name -y --skip-specs"
|
|
1850
|
-
fi
|
|
1851
|
-
else
|
|
1852
|
-
command -v openspec >/dev/null 2>&1 || die "$COMATE_ENV" "FATAL: openspec CLI not found; install OpenSpec before archiving"
|
|
1853
|
-
if [ "$has_delta_specs" -eq 1 ]; then
|
|
1854
|
-
openspec archive "$name" -y
|
|
1855
|
-
_step_ok "OpenSpec archived change and updated main specs"
|
|
1856
|
-
else
|
|
1857
|
-
openspec archive "$name" -y --skip-specs
|
|
1858
|
-
_step_ok "OpenSpec archived change (no delta specs)"
|
|
1859
|
-
fi
|
|
1860
|
-
fi
|
|
1861
|
-
|
|
1862
|
-
# Mark archived via transition. OpenSpec normally uses YYYY-MM-DD-<name>,
|
|
1863
|
-
# but fall back to locating the moved state file if that ever changes.
|
|
1864
|
-
if [ "$dry_run" -eq 1 ]; then
|
|
1865
|
-
_step_dry "Would set archived: true"
|
|
1866
|
-
else
|
|
1867
|
-
local moved_archive_name="$archive_name"
|
|
1868
|
-
if [ ! -f "$archive_dir/.comate.yaml" ]; then
|
|
1869
|
-
local found
|
|
1870
|
-
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)
|
|
1871
|
-
if [ -n "$found" ]; then
|
|
1872
|
-
moved_archive_name=$(basename "$(dirname "$found")")
|
|
1873
|
-
archive_dir=$(dirname "$found")
|
|
1874
|
-
fi
|
|
1875
|
-
fi
|
|
1876
|
-
if [ -f "$archive_dir/.comate.yaml" ]; then
|
|
1877
|
-
state_transition "$moved_archive_name" archived >/dev/null
|
|
1878
|
-
_step_ok "archived: true"
|
|
1879
|
-
# Now that the change has moved and the REAL archive name is known, annotate
|
|
1880
|
-
# design doc + plan (they stay under docs/superpowers; openspec never moved
|
|
1881
|
-
# them). Using the discovered name avoids any local-vs-openspec date skew.
|
|
1882
|
-
ann_archive_name="$moved_archive_name"
|
|
1883
|
-
new_prefix="$archive_dir/"
|
|
1884
|
-
[ -n "$design_doc" ] && [ "$design_doc" != "null" ] && _annotate "$design_doc" "status: final"
|
|
1885
|
-
[ -n "$plan" ] && [ "$plan" != "null" ] && _annotate "$plan" ""
|
|
1886
|
-
# Rewrite self-pointers that moved with the change (class A): fields under
|
|
1887
|
-
# the change's own .comate/ dir become stale after the move. design_doc /
|
|
1888
|
-
# plan live under docs/superpowers and do not move, so leave them alone.
|
|
1889
|
-
local moved_yaml="$archive_dir/.comate.yaml" k v
|
|
1890
|
-
local actual_old="openspec/changes/$name/"
|
|
1891
|
-
local actual_new="$archive_dir/"
|
|
1892
|
-
for k in source_manifest handoff_context impl_context; do
|
|
1893
|
-
v=$(yaml_get "$moved_yaml" "$k")
|
|
1894
|
-
case "$v" in
|
|
1895
|
-
"$actual_old"*)
|
|
1896
|
-
yaml_set "$moved_yaml" "$k" "$actual_new${v#"$actual_old"}"
|
|
1897
|
-
_step_ok "Repointed $k -> archive path"
|
|
1898
|
-
;;
|
|
1899
|
-
esac
|
|
1900
|
-
done
|
|
1901
|
-
else
|
|
1902
|
-
_step_fail "archived: true (.comate.yaml not found after move)"
|
|
1903
|
-
fi
|
|
1904
|
-
fi
|
|
1905
|
-
|
|
1906
|
-
echo "" >&2
|
|
1907
|
-
if [ "$dry_run" -eq 1 ]; then
|
|
1908
|
-
log_warn "Dry run complete. $steps_ok/$steps_total steps would succeed."
|
|
1909
|
-
else
|
|
1910
|
-
log_pass "Archive complete. $steps_ok/$steps_total steps succeeded."
|
|
1911
|
-
fi
|
|
1912
|
-
if [ "$steps_ok" -lt "$steps_total" ]; then exit "$COMATE_BLOCK"; fi
|
|
1913
|
-
exit "$COMATE_OK"
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
# =====================================================================
|
|
1917
|
-
# status — list active changes + next command
|
|
1918
|
-
# =====================================================================
|
|
1919
|
-
|
|
1920
|
-
_next_command() {
|
|
1921
|
-
case "$1" in
|
|
1922
|
-
open) echo "/comate-open" ;;
|
|
1923
|
-
design) echo "/comate-design" ;;
|
|
1924
|
-
build) echo "/comate-build" ;;
|
|
1925
|
-
verify) echo "/comate-verify" ;;
|
|
1926
|
-
archive) echo "/comate-archive" ;;
|
|
1927
|
-
*) echo "" ;;
|
|
1928
|
-
esac
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
status() {
|
|
1932
|
-
local base="${1:-.}"
|
|
1933
|
-
local changes_dir="$base/openspec/changes"
|
|
1934
|
-
if [ ! -d "$changes_dir" ]; then
|
|
1935
|
-
echo "No active changes."
|
|
1936
|
-
return 0
|
|
1937
|
-
fi
|
|
1938
|
-
local any=0 d name yaml phase phase_step workflow build_mode vr dd plan total done tasks_file
|
|
1939
|
-
for d in "$changes_dir"/*/; do
|
|
1940
|
-
[ -d "$d" ] || continue
|
|
1941
|
-
name=$(basename "$d")
|
|
1942
|
-
[ "$name" = "archive" ] && continue
|
|
1943
|
-
yaml="$d/.comate.yaml"
|
|
1944
|
-
[ -f "$yaml" ] || continue
|
|
1945
|
-
[ "$(yaml_get "$yaml" archived)" = "true" ] && continue
|
|
1946
|
-
any=1
|
|
1947
|
-
phase=$(yaml_get "$yaml" phase)
|
|
1948
|
-
phase_step=$(yaml_get "$yaml" phase_step)
|
|
1949
|
-
workflow=$(yaml_get "$yaml" workflow)
|
|
1950
|
-
build_mode=$(yaml_get "$yaml" build_mode)
|
|
1951
|
-
vr=$(yaml_get "$yaml" verify_result)
|
|
1952
|
-
dd=$(yaml_get "$yaml" design_doc)
|
|
1953
|
-
plan=$(yaml_get "$yaml" plan)
|
|
1954
|
-
tasks_file="$d/tasks.md"
|
|
1955
|
-
total=0; done=0
|
|
1956
|
-
if [ -f "$tasks_file" ]; then
|
|
1957
|
-
total=$(grep -c '^- \[' "$tasks_file" 2>/dev/null || echo 0)
|
|
1958
|
-
done=$(grep -c '^- \[x\]' "$tasks_file" 2>/dev/null || echo 0)
|
|
1959
|
-
fi
|
|
1960
|
-
echo "• $name [phase: $phase, $done/$total tasks]"
|
|
1961
|
-
echo " workflow: $workflow | build_mode: $build_mode"
|
|
1962
|
-
[ -n "$phase_step" ] && [ "$phase_step" != "null" ] && echo " step: $phase_step"
|
|
1963
|
-
[ -n "$dd" ] && [ "$dd" != "null" ] && echo " design: $dd"
|
|
1964
|
-
[ -n "$plan" ] && [ "$plan" != "null" ] && echo " plan: $plan"
|
|
1965
|
-
[ "$phase" = "verify" ] && echo " verify_result: $vr"
|
|
1966
|
-
echo " next: $(_next_command "$phase")"
|
|
1967
|
-
echo ""
|
|
1968
|
-
done
|
|
1969
|
-
[ "$any" -eq 0 ] && echo "No active changes."
|
|
1970
|
-
return 0
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
# =====================================================================
|
|
1974
|
-
# doctor — prerequisite self-check
|
|
1975
|
-
# =====================================================================
|
|
1976
|
-
|
|
1977
|
-
doctor() {
|
|
1978
|
-
local fail=0 warn=0
|
|
1979
|
-
_d_pass() { log_pass " ✓ $1"; }
|
|
1980
|
-
_d_warn() { log_warn " ⚠ $1"; warn=$((warn+1)); }
|
|
1981
|
-
_d_fail() { log_fail " ✗ $1"; fail=$((fail+1)); }
|
|
1982
|
-
|
|
1983
|
-
log_info "comate doctor"
|
|
1984
|
-
|
|
1985
|
-
# The state machine is written for Bash 3.2-compatible syntax.
|
|
1986
|
-
if [ "${BASH_VERSINFO[0]:-0}" -ge 3 ]; then
|
|
1987
|
-
_d_pass "bash ${BASH_VERSION} (>= 3)"
|
|
1988
|
-
else
|
|
1989
|
-
_d_fail "bash ${BASH_VERSION:-unknown} — comate needs bash >= 3"
|
|
1990
|
-
fi
|
|
1991
|
-
|
|
1992
|
-
# git.
|
|
1993
|
-
if command -v git >/dev/null 2>&1; then _d_pass "git present"; else _d_fail "git not found — install git"; fi
|
|
1994
|
-
|
|
1995
|
-
# sha256 tool.
|
|
1996
|
-
if command -v sha256sum >/dev/null 2>&1; then _d_pass "sha256sum present"
|
|
1997
|
-
elif command -v shasum >/dev/null 2>&1; then _d_pass "shasum present"
|
|
1998
|
-
else _d_fail "neither sha256sum nor shasum found — install coreutils or perl shasum"; fi
|
|
1999
|
-
|
|
2000
|
-
# openspec CLI.
|
|
2001
|
-
if command -v openspec >/dev/null 2>&1; then
|
|
2002
|
-
_d_pass "openspec CLI present ($(openspec --version 2>/dev/null | head -1 || echo installed))"
|
|
2003
|
-
else
|
|
2004
|
-
_d_warn "openspec CLI not found — install: npm install -g @fission-ai/openspec@latest"
|
|
2005
|
-
fi
|
|
2006
|
-
|
|
2007
|
-
# Superpowers skill directory — check known skill roots only (bounded; an
|
|
2008
|
-
# unbounded $HOME scan is too slow on large home directories). The skills CLI
|
|
2009
|
-
# (including `claude codeskills add`) installs Superpowers as a flat bundle of
|
|
2010
|
-
# named skills (for example using-superpowers/SKILL.md), so looking only for
|
|
2011
|
-
# a directory named "superpowers" produces a false warning.
|
|
2012
|
-
local sp_found=0 root
|
|
2013
|
-
for root in \
|
|
2014
|
-
"./.agents/skills" "./.claude/skills" "./.cursor/skills" "./.codex/skills" \
|
|
2015
|
-
"$HOME/.agents/skills" "$HOME/.claude/skills" "$HOME/.config/skills" "$HOME/.cursor/skills" "$HOME/.codex/skills" "$HOME/.gemini/skills" \
|
|
2016
|
-
"./.claude/plugins" "$HOME/.claude/plugins"; do
|
|
2017
|
-
if [ -f "$root/using-superpowers/SKILL.md" ] || \
|
|
2018
|
-
[ -f "$root/brainstorming/SKILL.md" ] || \
|
|
2019
|
-
{ [ -d "$root" ] && find "$root" -maxdepth 3 -type d -name 'superpowers*' -print -quit 2>/dev/null | grep -q .; }; then
|
|
2020
|
-
sp_found=1; break
|
|
2021
|
-
fi
|
|
2022
|
-
done
|
|
2023
|
-
if [ "$sp_found" -eq 1 ]; then
|
|
2024
|
-
_d_pass "Superpowers skills found"
|
|
2025
|
-
else
|
|
2026
|
-
_d_warn "Superpowers skills not found in known roots — run: comate init from your project directory"
|
|
2027
|
-
fi
|
|
2028
|
-
|
|
2029
|
-
# comate's own files present.
|
|
2030
|
-
local missing=0 f
|
|
2031
|
-
for f in comate-lib.sh comate.sh; do
|
|
2032
|
-
[ -f "$SCRIPT_DIR/$f" ] || { _d_fail "missing comate file: scripts/$f"; missing=1; }
|
|
2033
|
-
done
|
|
2034
|
-
[ "$missing" -eq 0 ] && _d_pass "comate scripts complete"
|
|
2035
|
-
|
|
2036
|
-
# LF line endings on .sh (Windows-critical).
|
|
2037
|
-
local crlf=0
|
|
2038
|
-
for f in "$SCRIPT_DIR"/*.sh; do
|
|
2039
|
-
if grep -lq $'\r' "$f" 2>/dev/null; then _d_warn "CRLF line endings in $(basename "$f") — convert to LF"; crlf=1; fi
|
|
2040
|
-
done
|
|
2041
|
-
[ "$crlf" -eq 0 ] && _d_pass ".sh files use LF line endings"
|
|
2042
|
-
|
|
2043
|
-
echo "" >&2
|
|
2044
|
-
if [ "$fail" -gt 0 ]; then
|
|
2045
|
-
die "$COMATE_ENV" "$fail failure(s), $warn warning(s) — fix failures before using comate"
|
|
2046
|
-
fi
|
|
2047
|
-
log_pass "0 failures, $warn warning(s)"
|
|
2048
|
-
exit "$COMATE_OK"
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
# =====================================================================
|
|
2052
|
-
# main router
|
|
2053
|
-
# =====================================================================
|
|
2054
|
-
|
|
2055
|
-
CMD="${1:-}"
|
|
2056
|
-
shift || true
|
|
2057
|
-
|
|
2058
|
-
case "$CMD" in
|
|
2059
|
-
state)
|
|
2060
|
-
NAME="${1:-}"; SUB="${2:-}"
|
|
2061
|
-
[ -n "$NAME" ] && [ -n "$SUB" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
2062
|
-
shift 2 || true
|
|
2063
|
-
case "$SUB" in
|
|
2064
|
-
init) state_init "$NAME" "${1:-}" ;;
|
|
2065
|
-
get) state_get "$NAME" "${1:-}" ;;
|
|
2066
|
-
set) state_set "$NAME" "${1:-}" "${2:-}" ;;
|
|
2067
|
-
transition) state_transition "$NAME" "${1:-}" ;;
|
|
2068
|
-
check)
|
|
2069
|
-
CHECK_JSON=0
|
|
2070
|
-
[ "${2:-}" = "--json" ] && CHECK_JSON=1
|
|
2071
|
-
state_check "$NAME" "${1:-}"
|
|
2072
|
-
;;
|
|
2073
|
-
recover) state_recover "$NAME" "${1:-}" ;;
|
|
2074
|
-
scale) state_scale "$NAME" ;;
|
|
2075
|
-
validate) state_validate "$NAME" ;;
|
|
2076
|
-
*) die "$COMATE_USAGE" "Unknown state subcommand: $SUB" ;;
|
|
2077
|
-
esac
|
|
2078
|
-
;;
|
|
2079
|
-
guard)
|
|
2080
|
-
NAME="${1:-}"; PHASE="${2:-}"
|
|
2081
|
-
[ -n "$NAME" ] && [ -n "$PHASE" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
2082
|
-
GUARD_APPLY=0; GUARD_JSON=0
|
|
2083
|
-
for arg in "${@:3}"; do
|
|
2084
|
-
case "$arg" in
|
|
2085
|
-
--apply) GUARD_APPLY=1 ;;
|
|
2086
|
-
--json) GUARD_JSON=1 ;;
|
|
2087
|
-
esac
|
|
2088
|
-
done
|
|
2089
|
-
guard "$NAME" "$PHASE"
|
|
2090
|
-
;;
|
|
2091
|
-
handoff)
|
|
2092
|
-
handoff "${1:-}" "${2:-}" "${3:-}" "${4:-}"
|
|
2093
|
-
;;
|
|
2094
|
-
sources)
|
|
2095
|
-
NAME="${1:-}"; SUB="${2:-}"
|
|
2096
|
-
if [ "$NAME" = "find" ]; then
|
|
2097
|
-
sources_find "${2:-}"
|
|
2098
|
-
else
|
|
2099
|
-
[ -n "$NAME" ] && [ -n "$SUB" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
2100
|
-
shift 2 || true
|
|
2101
|
-
case "$SUB" in
|
|
2102
|
-
track) sources_track "$NAME" "$@" ;;
|
|
2103
|
-
check) sources_check "$NAME" ;;
|
|
2104
|
-
list) sources_list "$NAME" ;;
|
|
2105
|
-
*) die "$COMATE_USAGE" "Unknown sources subcommand: $SUB" ;;
|
|
2106
|
-
esac
|
|
2107
|
-
fi
|
|
2108
|
-
;;
|
|
2109
|
-
dependency)
|
|
2110
|
-
NAME="${1:-}"; SUB="${2:-}"
|
|
2111
|
-
[ -n "$NAME" ] && [ "$SUB" = "archived" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
2112
|
-
dependency_archived "$NAME"
|
|
2113
|
-
;;
|
|
2114
|
-
gate)
|
|
2115
|
-
PRD="${1:-}"; SUB="${2:-}"
|
|
2116
|
-
[ -n "$PRD" ] && [ -n "$SUB" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
2117
|
-
shift 2 || true
|
|
2118
|
-
case "$SUB" in
|
|
2119
|
-
review) gate_review "$PRD" ;;
|
|
2120
|
-
set) gate_set "$PRD" "${1:-}" "${2:-}" ;;
|
|
2121
|
-
pass) gate_pass "$PRD" "${1:-}" ;;
|
|
2122
|
-
spawned) gate_spawned "$PRD" "${1:-}" ;;
|
|
2123
|
-
fanout-status) gate_fanout_status "$PRD" "${1:-}" ;;
|
|
2124
|
-
status) gate_status "$PRD" "${1:-}" ;;
|
|
2125
|
-
check) gate_check "$PRD" ;;
|
|
2126
|
-
*) die "$COMATE_USAGE" "Unknown gate subcommand: $SUB" ;;
|
|
2127
|
-
esac
|
|
2128
|
-
;;
|
|
2129
|
-
archive)
|
|
2130
|
-
[ -n "${1:-}" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
2131
|
-
archive "$1" "${2:-}"
|
|
2132
|
-
;;
|
|
2133
|
-
status)
|
|
2134
|
-
status "${1:-.}"
|
|
2135
|
-
;;
|
|
2136
|
-
doctor)
|
|
2137
|
-
doctor
|
|
2138
|
-
;;
|
|
2139
|
-
""|-h|--help|help)
|
|
2140
|
-
usage_top
|
|
2141
|
-
exit "$COMATE_OK"
|
|
2142
|
-
;;
|
|
2143
|
-
*)
|
|
2144
|
-
usage_top
|
|
2145
|
-
exit "$COMATE_USAGE"
|
|
2146
|
-
;;
|
|
2147
|
-
esac
|