@rpamis/comet 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,255 +1,255 @@
1
- #!/bin/bash
2
- # Comet Phase Guard — validates exit conditions before phase transitions
3
- # Usage: comet-guard.sh <change-name> <current-phase> [--apply]
4
- # Phases: open, design, build, verify, archive
5
- # Exit 0 = all checks pass, exit 1 = blocked (reasons printed to stderr)
6
- # shellcheck disable=SC2329 # Functions called indirectly via check() dispatch
7
-
8
- set -euo pipefail
9
-
10
- red() { echo -e "\033[31m$1\033[0m" >&2; }
11
- green() { echo -e "\033[32m$1\033[0m" >&2; }
12
- warn() { echo -e "\033[33m$1\033[0m" >&2; }
13
-
14
- # Input validation - prevent path traversal
15
- validate_change_name() {
16
- local name="$1"
17
- # Reject empty names
18
- if [ -z "$name" ]; then
19
- red "ERROR: Change name cannot be empty" >&2
20
- exit 1
21
- fi
22
- # Only allow alphanumeric, hyphens, and underscores
23
- if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
24
- red "ERROR: Invalid change name: '$name'" >&2
25
- red "Valid characters: a-z, A-Z, 0-9, -, _" >&2
26
- exit 1
27
- fi
28
- # Reject path traversal attempts
29
- if [[ "$name" =~ \.\. ]]; then
30
- red "ERROR: Change name cannot contain '..' (path traversal not allowed)" >&2
31
- exit 1
32
- fi
33
- }
34
-
35
- validate_change_name "$1"
36
-
37
- CHANGE="$1"
38
- PHASE="$2"
39
- APPLY=0
40
- if [[ "${3:-}" == "--apply" ]]; then
41
- APPLY=1
42
- fi
43
- CHANGE_DIR="openspec/changes/$CHANGE"
44
-
45
- BLOCK=0
46
- check() {
47
- local desc="$1"
48
- shift
49
- if "$@" 2>/dev/null; then
50
- green " [PASS] $desc"
51
- else
52
- red " [FAIL] $desc"
53
- BLOCK=1
54
- fi
55
- }
56
-
57
- # --- Helper functions ---
58
-
59
- tasks_all_done() {
60
- local tasks="$CHANGE_DIR/tasks.md"
61
- [ -f "$tasks" ] || return 1
62
- grep -q '\- \[x\]' "$tasks" || return 1
63
- ! grep -q '\- \[ \]' "$tasks"
64
- }
65
-
66
- tasks_has_any() {
67
- local tasks="$CHANGE_DIR/tasks.md"
68
- [ -f "$tasks" ] && grep -q '\- \[' "$tasks"
69
- }
70
-
71
- yaml_field_value() {
72
- local field="$1"
73
- local yaml="$CHANGE_DIR/.comet.yaml"
74
- if [ -f "$yaml" ]; then
75
- grep "^${field}:" "$yaml" | sed "s/^${field}: *//" | tr -d '"' | tr -d "'"
76
- fi
77
- }
78
-
79
- file_nonempty() {
80
- [ -f "$1" ] && [ -s "$1" ]
81
- }
82
-
83
- preflight() {
84
- local expected_phase="$1"
85
-
86
- if [ ! -d "$CHANGE_DIR" ]; then
87
- red "FATAL: change directory not found: $CHANGE_DIR"
88
- exit 1
89
- fi
90
- if [ ! -f "$CHANGE_DIR/.comet.yaml" ]; then
91
- red "FATAL: .comet.yaml not found in $CHANGE_DIR"
92
- exit 1
93
- fi
94
-
95
- local actual_phase
96
- actual_phase=$(yaml_field_value "phase" 2>/dev/null || true)
97
- if [ "$actual_phase" != "$expected_phase" ]; then
98
- red "FATAL: .comet.yaml phase is '$actual_phase', expected '$expected_phase'"
99
- exit 1
100
- fi
101
-
102
- # Schema validation
103
- local script_dir validate_script
104
- script_dir="$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" 2>/dev/null || dirname "$0")"
105
- validate_script="$script_dir/comet-yaml-validate.sh"
106
- if [ -f "$validate_script" ]; then
107
- if ! bash "$validate_script" "$CHANGE" 2>/dev/null; then
108
- bash "$validate_script" "$CHANGE"
109
- red "FATAL: .comet.yaml schema validation failed"
110
- exit 1
111
- fi
112
- fi
113
- }
114
-
115
- build_passes() {
116
- if [ "${COMET_SKIP_BUILD:-0}" = "1" ]; then
117
- return 0
118
- fi
119
- # Attempt common build commands; succeeds if any pass
120
- (npm run build 2>/dev/null) || (mvn compile -q 2>/dev/null) || (cargo build 2>/dev/null) || true
121
- }
122
-
123
- verify_result_is_pass() {
124
- local result
125
- result=$(yaml_field_value "verify_result" 2>/dev/null || true)
126
- [ "$result" = "pass" ]
127
- }
128
-
129
- archived_is_true() {
130
- local val
131
- val=$(yaml_field_value "archived" 2>/dev/null || true)
132
- [ "$val" = "true" ]
133
- }
134
-
135
- # --- Phase-specific checks ---
136
-
137
- guard_open() {
138
- echo "=== Guard: open → design ===" >&2
139
-
140
- check "proposal.md exists and non-empty" file_nonempty "$CHANGE_DIR/proposal.md"
141
- check "design.md exists and non-empty" file_nonempty "$CHANGE_DIR/design.md"
142
- check "tasks.md exists and non-empty" file_nonempty "$CHANGE_DIR/tasks.md"
143
- check "tasks.md has at least one task" tasks_has_any
144
- }
145
-
146
- guard_design() {
147
- echo "=== Guard: design → build ===" >&2
148
-
149
- local design_doc
150
- design_doc=$(yaml_field_value "design_doc" 2>/dev/null || true)
151
-
152
- check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
153
- check "tasks.md exists" file_nonempty "$CHANGE_DIR/tasks.md"
154
-
155
- if [ -n "$design_doc" ] && [ "$design_doc" != "null" ]; then
156
- check "Design Doc ($design_doc) exists" file_nonempty "$design_doc"
157
- else
158
- warn " [WARN] No design_doc recorded in .comet.yaml"
159
- fi
160
- }
161
-
162
- guard_build() {
163
- echo "=== Guard: build → verify ===" >&2
164
-
165
- check "tasks.md all tasks checked" tasks_all_done
166
- check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
167
- check "Build passes" build_passes
168
- }
169
-
170
- guard_verify() {
171
- echo "=== Guard: verify → archive ===" >&2
172
-
173
- check "verify_result is pass" verify_result_is_pass
174
- check "tasks.md all tasks checked" tasks_all_done
175
- check "Build passes" build_passes
176
- }
177
-
178
- guard_archive() {
179
- echo "=== Guard: archive completeness ===" >&2
180
-
181
- check "archived is true" archived_is_true
182
- check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
183
- check "tasks.md all tasks checked" tasks_all_done
184
- }
185
-
186
- apply_state_update() {
187
- local state_sh="$script_dir/comet-state.sh"
188
- local p="$1"
189
-
190
- if [ -f "$state_sh" ]; then
191
- case "$p" in
192
- open) bash "$state_sh" set "$CHANGE" phase design ;;
193
- design) bash "$state_sh" set "$CHANGE" phase build ;;
194
- build)
195
- bash "$state_sh" set "$CHANGE" phase verify
196
- bash "$state_sh" set "$CHANGE" verify_result pending
197
- ;;
198
- verify)
199
- bash "$state_sh" set "$CHANGE" phase archive
200
- bash "$state_sh" set "$CHANGE" verify_result pass
201
- bash "$state_sh" set "$CHANGE" verified_at "$(date +%Y-%m-%d)"
202
- ;;
203
- esac
204
- else
205
- local yaml="$CHANGE_DIR/.comet.yaml"
206
- case "$p" in
207
- open) sed -i 's/^phase:.*/phase: design/' "$yaml" ;;
208
- design) sed -i 's/^phase:.*/phase: build/' "$yaml" ;;
209
- build) sed -i 's/^phase:.*/phase: verify/' "$yaml"; sed -i 's/^verify_result:.*/verify_result: pending/' "$yaml" ;;
210
- verify)
211
- sed -i 's/^phase:.*/phase: archive/' "$yaml"
212
- sed -i 's/^verify_result:.*/verify_result: pass/' "$yaml"
213
- if ! grep -q '^verified_at:' "$yaml" 2>/dev/null; then
214
- echo "verified_at: $(date +%Y-%m-%d)" >> "$yaml"
215
- else
216
- sed -i "s/^verified_at:.*/verified_at: $(date +%Y-%m-%d)/" "$yaml"
217
- fi
218
- ;;
219
- esac
220
- fi
221
- }
222
-
223
- # --- Main ---
224
-
225
- case "$PHASE" in
226
- open) preflight "design" ; guard_open ;;
227
- design) preflight "build" ; guard_design ;;
228
- build) preflight "verify" ; guard_build ;;
229
- verify) preflight "archive" ; guard_verify ;;
230
- archive) preflight "archive" ; guard_archive ;;
231
- *)
232
- red "Unknown phase: $PHASE"
233
- echo "Valid phases: open, design, build, verify, archive" >&2
234
- exit 1
235
- ;;
236
- esac
237
-
238
- if [ "$BLOCK" -eq 1 ]; then
239
- echo "" >&2
240
- red "BLOCKED — fix failing checks before proceeding to next phase"
241
- exit 1
242
- else
243
- echo "" >&2
244
- green "ALL CHECKS PASSED — ready for next phase"
245
- if [ "$APPLY" -eq 1 ]; then
246
- apply_state_update "$PHASE"
247
- case "$PHASE" in
248
- open) green " [APPLY] .comet.yaml updated: phase=design" ;;
249
- design) green " [APPLY] .comet.yaml updated: phase=build" ;;
250
- build) green " [APPLY] .comet.yaml updated: phase=verify, verify_result=pending" ;;
251
- verify) green " [APPLY] .comet.yaml updated: phase=archive, verify_result=pass" ;;
252
- esac
253
- fi
254
- exit 0
255
- fi
1
+ #!/bin/bash
2
+ # Comet Phase Guard — validates exit conditions before phase transitions
3
+ # Usage: comet-guard.sh <change-name> <current-phase> [--apply]
4
+ # Phases: open, design, build, verify, archive
5
+ # Exit 0 = all checks pass, exit 1 = blocked (reasons printed to stderr)
6
+ # shellcheck disable=SC2329 # Functions called indirectly via check() dispatch
7
+
8
+ set -euo pipefail
9
+
10
+ red() { echo -e "\033[31m$1\033[0m" >&2; }
11
+ green() { echo -e "\033[32m$1\033[0m" >&2; }
12
+ warn() { echo -e "\033[33m$1\033[0m" >&2; }
13
+
14
+ # Input validation - prevent path traversal
15
+ validate_change_name() {
16
+ local name="$1"
17
+ # Reject empty names
18
+ if [ -z "$name" ]; then
19
+ red "ERROR: Change name cannot be empty" >&2
20
+ exit 1
21
+ fi
22
+ # Only allow alphanumeric, hyphens, and underscores
23
+ if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
24
+ red "ERROR: Invalid change name: '$name'" >&2
25
+ red "Valid characters: a-z, A-Z, 0-9, -, _" >&2
26
+ exit 1
27
+ fi
28
+ # Reject path traversal attempts
29
+ if [[ "$name" =~ \.\. ]]; then
30
+ red "ERROR: Change name cannot contain '..' (path traversal not allowed)" >&2
31
+ exit 1
32
+ fi
33
+ }
34
+
35
+ validate_change_name "$1"
36
+
37
+ CHANGE="$1"
38
+ PHASE="$2"
39
+ APPLY=0
40
+ if [[ "${3:-}" == "--apply" ]]; then
41
+ APPLY=1
42
+ fi
43
+ CHANGE_DIR="openspec/changes/$CHANGE"
44
+
45
+ BLOCK=0
46
+ check() {
47
+ local desc="$1"
48
+ shift
49
+ if "$@" 2>/dev/null; then
50
+ green " [PASS] $desc"
51
+ else
52
+ red " [FAIL] $desc"
53
+ BLOCK=1
54
+ fi
55
+ }
56
+
57
+ # --- Helper functions ---
58
+
59
+ tasks_all_done() {
60
+ local tasks="$CHANGE_DIR/tasks.md"
61
+ [ -f "$tasks" ] || return 1
62
+ grep -q '\- \[x\]' "$tasks" || return 1
63
+ ! grep -q '\- \[ \]' "$tasks"
64
+ }
65
+
66
+ tasks_has_any() {
67
+ local tasks="$CHANGE_DIR/tasks.md"
68
+ [ -f "$tasks" ] && grep -q '\- \[' "$tasks"
69
+ }
70
+
71
+ yaml_field_value() {
72
+ local field="$1"
73
+ local yaml="$CHANGE_DIR/.comet.yaml"
74
+ if [ -f "$yaml" ]; then
75
+ grep "^${field}:" "$yaml" | sed "s/^${field}: *//" | tr -d '"' | tr -d "'"
76
+ fi
77
+ }
78
+
79
+ file_nonempty() {
80
+ [ -f "$1" ] && [ -s "$1" ]
81
+ }
82
+
83
+ preflight() {
84
+ local expected_phase="$1"
85
+
86
+ if [ ! -d "$CHANGE_DIR" ]; then
87
+ red "FATAL: change directory not found: $CHANGE_DIR"
88
+ exit 1
89
+ fi
90
+ if [ ! -f "$CHANGE_DIR/.comet.yaml" ]; then
91
+ red "FATAL: .comet.yaml not found in $CHANGE_DIR"
92
+ exit 1
93
+ fi
94
+
95
+ local actual_phase
96
+ actual_phase=$(yaml_field_value "phase" 2>/dev/null || true)
97
+ if [ "$actual_phase" != "$expected_phase" ]; then
98
+ red "FATAL: .comet.yaml phase is '$actual_phase', expected '$expected_phase'"
99
+ exit 1
100
+ fi
101
+
102
+ # Schema validation
103
+ local script_dir validate_script
104
+ script_dir="$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" 2>/dev/null || dirname "$0")"
105
+ validate_script="$script_dir/comet-yaml-validate.sh"
106
+ if [ -f "$validate_script" ]; then
107
+ if ! bash "$validate_script" "$CHANGE" 2>/dev/null; then
108
+ bash "$validate_script" "$CHANGE"
109
+ red "FATAL: .comet.yaml schema validation failed"
110
+ exit 1
111
+ fi
112
+ fi
113
+ }
114
+
115
+ build_passes() {
116
+ if [ "${COMET_SKIP_BUILD:-0}" = "1" ]; then
117
+ return 0
118
+ fi
119
+ # Attempt common build commands; succeeds if any pass
120
+ (npm run build 2>/dev/null) || (mvn compile -q 2>/dev/null) || (cargo build 2>/dev/null) || true
121
+ }
122
+
123
+ verify_result_is_pass() {
124
+ local result
125
+ result=$(yaml_field_value "verify_result" 2>/dev/null || true)
126
+ [ "$result" = "pass" ]
127
+ }
128
+
129
+ archived_is_true() {
130
+ local val
131
+ val=$(yaml_field_value "archived" 2>/dev/null || true)
132
+ [ "$val" = "true" ]
133
+ }
134
+
135
+ # --- Phase-specific checks ---
136
+
137
+ guard_open() {
138
+ echo "=== Guard: open → design ===" >&2
139
+
140
+ check "proposal.md exists and non-empty" file_nonempty "$CHANGE_DIR/proposal.md"
141
+ check "design.md exists and non-empty" file_nonempty "$CHANGE_DIR/design.md"
142
+ check "tasks.md exists and non-empty" file_nonempty "$CHANGE_DIR/tasks.md"
143
+ check "tasks.md has at least one task" tasks_has_any
144
+ }
145
+
146
+ guard_design() {
147
+ echo "=== Guard: design → build ===" >&2
148
+
149
+ local design_doc
150
+ design_doc=$(yaml_field_value "design_doc" 2>/dev/null || true)
151
+
152
+ check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
153
+ check "tasks.md exists" file_nonempty "$CHANGE_DIR/tasks.md"
154
+
155
+ if [ -n "$design_doc" ] && [ "$design_doc" != "null" ]; then
156
+ check "Design Doc ($design_doc) exists" file_nonempty "$design_doc"
157
+ else
158
+ warn " [WARN] No design_doc recorded in .comet.yaml"
159
+ fi
160
+ }
161
+
162
+ guard_build() {
163
+ echo "=== Guard: build → verify ===" >&2
164
+
165
+ check "tasks.md all tasks checked" tasks_all_done
166
+ check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
167
+ check "Build passes" build_passes
168
+ }
169
+
170
+ guard_verify() {
171
+ echo "=== Guard: verify → archive ===" >&2
172
+
173
+ check "verify_result is pass" verify_result_is_pass
174
+ check "tasks.md all tasks checked" tasks_all_done
175
+ check "Build passes" build_passes
176
+ }
177
+
178
+ guard_archive() {
179
+ echo "=== Guard: archive completeness ===" >&2
180
+
181
+ check "archived is true" archived_is_true
182
+ check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
183
+ check "tasks.md all tasks checked" tasks_all_done
184
+ }
185
+
186
+ apply_state_update() {
187
+ local state_sh="$script_dir/comet-state.sh"
188
+ local p="$1"
189
+
190
+ if [ -f "$state_sh" ]; then
191
+ case "$p" in
192
+ open) bash "$state_sh" set "$CHANGE" phase design ;;
193
+ design) bash "$state_sh" set "$CHANGE" phase build ;;
194
+ build)
195
+ bash "$state_sh" set "$CHANGE" phase verify
196
+ bash "$state_sh" set "$CHANGE" verify_result pending
197
+ ;;
198
+ verify)
199
+ bash "$state_sh" set "$CHANGE" phase archive
200
+ bash "$state_sh" set "$CHANGE" verify_result pass
201
+ bash "$state_sh" set "$CHANGE" verified_at "$(date +%Y-%m-%d)"
202
+ ;;
203
+ esac
204
+ else
205
+ local yaml="$CHANGE_DIR/.comet.yaml"
206
+ case "$p" in
207
+ open) sed -i 's/^phase:.*/phase: design/' "$yaml" ;;
208
+ design) sed -i 's/^phase:.*/phase: build/' "$yaml" ;;
209
+ build) sed -i 's/^phase:.*/phase: verify/' "$yaml"; sed -i 's/^verify_result:.*/verify_result: pending/' "$yaml" ;;
210
+ verify)
211
+ sed -i 's/^phase:.*/phase: archive/' "$yaml"
212
+ sed -i 's/^verify_result:.*/verify_result: pass/' "$yaml"
213
+ if ! grep -q '^verified_at:' "$yaml" 2>/dev/null; then
214
+ echo "verified_at: $(date +%Y-%m-%d)" >> "$yaml"
215
+ else
216
+ sed -i "s/^verified_at:.*/verified_at: $(date +%Y-%m-%d)/" "$yaml"
217
+ fi
218
+ ;;
219
+ esac
220
+ fi
221
+ }
222
+
223
+ # --- Main ---
224
+
225
+ case "$PHASE" in
226
+ open) preflight "design" ; guard_open ;;
227
+ design) preflight "build" ; guard_design ;;
228
+ build) preflight "verify" ; guard_build ;;
229
+ verify) preflight "archive" ; guard_verify ;;
230
+ archive) preflight "archive" ; guard_archive ;;
231
+ *)
232
+ red "Unknown phase: $PHASE"
233
+ echo "Valid phases: open, design, build, verify, archive" >&2
234
+ exit 1
235
+ ;;
236
+ esac
237
+
238
+ if [ "$BLOCK" -eq 1 ]; then
239
+ echo "" >&2
240
+ red "BLOCKED — fix failing checks before proceeding to next phase"
241
+ exit 1
242
+ else
243
+ echo "" >&2
244
+ green "ALL CHECKS PASSED — ready for next phase"
245
+ if [ "$APPLY" -eq 1 ]; then
246
+ apply_state_update "$PHASE"
247
+ case "$PHASE" in
248
+ open) green " [APPLY] .comet.yaml updated: phase=design" ;;
249
+ design) green " [APPLY] .comet.yaml updated: phase=build" ;;
250
+ build) green " [APPLY] .comet.yaml updated: phase=verify, verify_result=pending" ;;
251
+ verify) green " [APPLY] .comet.yaml updated: phase=archive, verify_result=pass" ;;
252
+ esac
253
+ fi
254
+ exit 0
255
+ fi