@rpamis/comet 0.2.0 → 0.2.1

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,253 +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
-
7
- set -euo pipefail
8
-
9
- red() { echo -e "\033[31m$1\033[0m" >&2; }
10
- green() { echo -e "\033[32m$1\033[0m" >&2; }
11
- warn() { echo -e "\033[33m$1\033[0m" >&2; }
12
-
13
- # Input validation - prevent path traversal
14
- validate_change_name() {
15
- local name="$1"
16
- # Reject empty names
17
- if [ -z "$name" ]; then
18
- red "ERROR: Change name cannot be empty" >&2
19
- exit 1
20
- fi
21
- # Only allow alphanumeric, hyphens, and underscores
22
- if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
23
- red "ERROR: Invalid change name: '$name'" >&2
24
- red "Valid characters: a-z, A-Z, 0-9, -, _" >&2
25
- exit 1
26
- fi
27
- # Reject path traversal attempts
28
- if [[ "$name" =~ \.\. ]]; then
29
- red "ERROR: Change name cannot contain '..' (path traversal not allowed)" >&2
30
- exit 1
31
- fi
32
- }
33
-
34
- validate_change_name "$1"
35
-
36
- CHANGE="$1"
37
- PHASE="$2"
38
- APPLY=0
39
- if [[ "${3:-}" == "--apply" ]]; then
40
- APPLY=1
41
- fi
42
- CHANGE_DIR="openspec/changes/$CHANGE"
43
-
44
- BLOCK=0
45
- check() {
46
- local desc="$1"
47
- shift
48
- if "$@" 2>/dev/null; then
49
- green " [PASS] $desc"
50
- else
51
- red " [FAIL] $desc"
52
- BLOCK=1
53
- fi
54
- }
55
-
56
- # --- Helper functions ---
57
-
58
- tasks_all_done() {
59
- local tasks="$CHANGE_DIR/tasks.md"
60
- [ -f "$tasks" ] || return 1
61
- grep -q '\- \[x\]' "$tasks" || return 1
62
- ! grep -q '\- \[ \]' "$tasks"
63
- }
64
-
65
- tasks_has_any() {
66
- local tasks="$CHANGE_DIR/tasks.md"
67
- [ -f "$tasks" ] && grep -q '\- \[' "$tasks"
68
- }
69
-
70
- yaml_field_value() {
71
- local field="$1"
72
- local yaml="$CHANGE_DIR/.comet.yaml"
73
- if [ -f "$yaml" ]; then
74
- grep "^${field}:" "$yaml" | sed "s/^${field}: *//" | tr -d '"' | tr -d "'"
75
- fi
76
- }
77
-
78
- file_nonempty() {
79
- [ -f "$1" ] && [ -s "$1" ]
80
- }
81
-
82
- preflight() {
83
- local expected_phase="$1"
84
-
85
- if [ ! -d "$CHANGE_DIR" ]; then
86
- red "FATAL: change directory not found: $CHANGE_DIR"
87
- exit 1
88
- fi
89
- if [ ! -f "$CHANGE_DIR/.comet.yaml" ]; then
90
- red "FATAL: .comet.yaml not found in $CHANGE_DIR"
91
- exit 1
92
- fi
93
-
94
- local actual_phase
95
- actual_phase=$(yaml_field_value "phase" 2>/dev/null || true)
96
- if [ "$actual_phase" != "$expected_phase" ]; then
97
- red "FATAL: .comet.yaml phase is '$actual_phase', expected '$expected_phase'"
98
- exit 1
99
- fi
100
-
101
- # Schema validation
102
- local script_dir validate_script
103
- script_dir="$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" 2>/dev/null || dirname "$0")"
104
- validate_script="$script_dir/comet-yaml-validate.sh"
105
- if [ -f "$validate_script" ]; then
106
- if ! bash "$validate_script" "$CHANGE" 2>/dev/null; then
107
- bash "$validate_script" "$CHANGE"
108
- red "FATAL: .comet.yaml schema validation failed"
109
- exit 1
110
- fi
111
- fi
112
- }
113
-
114
- maven_compiles() {
115
- if [ "${COMET_SKIP_BUILD:-0}" = "1" ]; then
116
- return 0
117
- fi
118
- mvn compile -q 2>/dev/null
119
- }
120
-
121
- verify_result_is_pass() {
122
- local result
123
- result=$(yaml_field_value "verify_result" 2>/dev/null || true)
124
- [ "$result" = "pass" ]
125
- }
126
-
127
- archived_is_true() {
128
- local val
129
- val=$(yaml_field_value "archived" 2>/dev/null || true)
130
- [ "$val" = "true" ]
131
- }
132
-
133
- # --- Phase-specific checks ---
134
-
135
- guard_open() {
136
- echo "=== Guard: open → design ===" >&2
137
-
138
- check "proposal.md exists and non-empty" file_nonempty "$CHANGE_DIR/proposal.md"
139
- check "design.md exists and non-empty" file_nonempty "$CHANGE_DIR/design.md"
140
- check "tasks.md exists and non-empty" file_nonempty "$CHANGE_DIR/tasks.md"
141
- check "tasks.md has at least one task" tasks_has_any
142
- }
143
-
144
- guard_design() {
145
- echo "=== Guard: design → build ===" >&2
146
-
147
- local design_doc
148
- design_doc=$(yaml_field_value "design_doc" 2>/dev/null || true)
149
-
150
- check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
151
- check "tasks.md exists" file_nonempty "$CHANGE_DIR/tasks.md"
152
-
153
- if [ -n "$design_doc" ] && [ "$design_doc" != "null" ]; then
154
- check "Design Doc ($design_doc) exists" file_nonempty "$design_doc"
155
- else
156
- warn " [WARN] No design_doc recorded in .comet.yaml"
157
- fi
158
- }
159
-
160
- guard_build() {
161
- echo "=== Guard: build → verify ===" >&2
162
-
163
- check "tasks.md all tasks checked" tasks_all_done
164
- check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
165
- check "Maven compile passes" maven_compiles
166
- }
167
-
168
- guard_verify() {
169
- echo "=== Guard: verify → archive ===" >&2
170
-
171
- check "verify_result is pass" verify_result_is_pass
172
- check "tasks.md all tasks checked" tasks_all_done
173
- check "Maven compile passes" maven_compiles
174
- }
175
-
176
- guard_archive() {
177
- echo "=== Guard: archive completeness ===" >&2
178
-
179
- check "archived is true" archived_is_true
180
- check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
181
- check "tasks.md all tasks checked" tasks_all_done
182
- }
183
-
184
- apply_state_update() {
185
- local state_sh="$SCRIPT_DIR/comet-state.sh"
186
- local p="$1"
187
-
188
- if [ -f "$state_sh" ]; then
189
- case "$p" in
190
- open) bash "$state_sh" set "$CHANGE" phase design ;;
191
- design) bash "$state_sh" set "$CHANGE" phase build ;;
192
- build)
193
- bash "$state_sh" set "$CHANGE" phase verify
194
- bash "$state_sh" set "$CHANGE" verify_result pending
195
- ;;
196
- verify)
197
- bash "$state_sh" set "$CHANGE" phase archive
198
- bash "$state_sh" set "$CHANGE" verify_result pass
199
- bash "$state_sh" set "$CHANGE" verified_at "$(date +%Y-%m-%d)"
200
- ;;
201
- esac
202
- else
203
- local yaml="$CHANGE_DIR/.comet.yaml"
204
- case "$p" in
205
- open) sed -i 's/^phase:.*/phase: design/' "$yaml" ;;
206
- design) sed -i 's/^phase:.*/phase: build/' "$yaml" ;;
207
- build) sed -i 's/^phase:.*/phase: verify/' "$yaml"; sed -i 's/^verify_result:.*/verify_result: pending/' "$yaml" ;;
208
- verify)
209
- sed -i 's/^phase:.*/phase: archive/' "$yaml"
210
- sed -i 's/^verify_result:.*/verify_result: pass/' "$yaml"
211
- if ! grep -q '^verified_at:' "$yaml" 2>/dev/null; then
212
- echo "verified_at: $(date +%Y-%m-%d)" >> "$yaml"
213
- else
214
- sed -i "s/^verified_at:.*/verified_at: $(date +%Y-%m-%d)/" "$yaml"
215
- fi
216
- ;;
217
- esac
218
- fi
219
- }
220
-
221
- # --- Main ---
222
-
223
- case "$PHASE" in
224
- open) preflight "design" ; guard_open ;;
225
- design) preflight "build" ; guard_design ;;
226
- build) preflight "verify" ; guard_build ;;
227
- verify) preflight "archive" ; guard_verify ;;
228
- archive) preflight "archive" ; guard_archive ;;
229
- *)
230
- red "Unknown phase: $PHASE"
231
- echo "Valid phases: open, design, build, verify, archive" >&2
232
- exit 1
233
- ;;
234
- esac
235
-
236
- if [ "$BLOCK" -eq 1 ]; then
237
- echo "" >&2
238
- red "BLOCKED fix failing checks before proceeding to next phase"
239
- exit 1
240
- else
241
- echo "" >&2
242
- green "ALL CHECKS PASSED — ready for next phase"
243
- if [ "$APPLY" -eq 1 ]; then
244
- apply_state_update "$PHASE"
245
- case "$PHASE" in
246
- open) green " [APPLY] .comet.yaml updated: phase=design" ;;
247
- design) green " [APPLY] .comet.yaml updated: phase=build" ;;
248
- build) green " [APPLY] .comet.yaml updated: phase=verify, verify_result=pending" ;;
249
- verify) green " [APPLY] .comet.yaml updated: phase=archive, verify_result=pass" ;;
250
- esac
251
- fi
252
- exit 0
253
- 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
@@ -22,6 +22,7 @@ yellow() { echo -e "\033[33m$1\033[0m" >&2; }
22
22
 
23
23
  # --- Script location ---
24
24
 
25
+ # shellcheck disable=SC2034
25
26
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
26
27
 
27
28
  # --- Input validation ---
@@ -211,8 +212,8 @@ cmd_set() {
211
212
 
212
213
  # Write or update the field
213
214
  if grep -q "^${field}:" "$yaml_file"; then
214
- # Field exists, replace it
215
- sed -i "s/^${field}:.*/${field}: ${value}/" "$yaml_file"
215
+ # Field exists, replace it (use | delimiter to avoid / conflicts in paths)
216
+ sed -i "s|^${field}:.*|${field}: ${value}|" "$yaml_file"
216
217
  else
217
218
  # Field doesn't exist, append it
218
219
  echo "${field}: ${value}" >> "$yaml_file"
@@ -238,7 +239,7 @@ check_fail() {
238
239
 
239
240
  check_nonempty() {
240
241
  local desc="$1"
241
- local path="$1"
242
+ local path="$2"
242
243
  if file_nonempty "$path"; then
243
244
  check_pass "$desc non-empty"
244
245
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpamis/comet",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "OpenSpec + Superpowers dual-star development workflow",
5
5
  "keywords": [
6
6
  "comet",
@@ -41,8 +41,9 @@
41
41
  "prepublishOnly": "node scripts/prepublish-check.js && pnpm run build",
42
42
  "postinstall": "node scripts/postinstall.js"
43
43
  },
44
+ "packageManager": "pnpm@10.18.3",
44
45
  "engines": {
45
- "node": ">=18"
46
+ "node": ">=20"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@eslint/js": "^10.0.1",