@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,497 +1,497 @@
1
- #!/bin/bash
2
- # Comet State — unified interface for .comet.yaml state management
3
- # Usage: comet-state.sh <subcommand> <change-name> [args...]
4
- #
5
- # Subcommands:
6
- # init <change-name> <workflow> — Initialize .comet.yaml with workflow defaults
7
- # get <change-name> <field> — Read a field value from .comet.yaml
8
- # set <change-name> <field> <val> — Update a field value
9
- # check <phase> <change-name> — Verify entry requirements for a phase
10
- # scale <change-name> — Assess and set verification mode based on metrics
11
- #
12
- # Workflows: full, hotfix, tweak
13
- # Phases for check: open, design, build, verify, archive
14
-
15
- set -euo pipefail
16
-
17
- # --- Color output helpers ---
18
-
19
- red() { echo -e "\033[31m$1\033[0m" >&2; }
20
- green() { echo -e "\033[32m$1\033[0m" >&2; }
21
- yellow() { echo -e "\033[33m$1\033[0m" >&2; }
22
-
23
- # --- Script location ---
24
-
25
- # shellcheck disable=SC2034
26
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
27
-
28
- # --- Input validation ---
29
-
30
- validate_change_name() {
31
- local name="$1"
32
- # Reject empty names
33
- if [ -z "$name" ]; then
34
- red "ERROR: Change name cannot be empty" >&2
35
- exit 1
36
- fi
37
- # Only allow alphanumeric, hyphens, and underscores
38
- if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
39
- red "ERROR: Invalid change name: '$name'" >&2
40
- red "Valid characters: a-z, A-Z, 0-9, -, _" >&2
41
- exit 1
42
- fi
43
- # Reject path traversal attempts
44
- if [[ "$name" =~ \.\. ]]; then
45
- red "ERROR: Change name cannot contain '..' (path traversal not allowed)" >&2
46
- exit 1
47
- fi
48
- }
49
-
50
- validate_enum() {
51
- local value="$1"
52
- shift
53
- local valid_values=("$@")
54
-
55
- for valid in "${valid_values[@]}"; do
56
- if [ "$value" = "$valid" ]; then
57
- return 0
58
- fi
59
- done
60
-
61
- red "ERROR: Invalid value: '$value'" >&2
62
- red "Valid values: ${valid_values[*]}" >&2
63
- exit 1
64
- }
65
-
66
- # --- Helper functions ---
67
-
68
- yaml_field() {
69
- local field="$1"
70
- local yaml_file="$2"
71
- if [ -f "$yaml_file" ]; then
72
- grep "^${field}:" "$yaml_file" | sed "s/^${field}: *//" | tr -d '"' | tr -d "'"
73
- fi
74
- }
75
-
76
- file_nonempty() {
77
- [ -f "$1" ] && [ -s "$1" ]
78
- }
79
-
80
- # --- Subcommands ---
81
-
82
- cmd_init() {
83
- local change_name="$1"
84
- local workflow="$2"
85
-
86
- validate_change_name "$change_name"
87
- validate_enum "$workflow" "full" "hotfix" "tweak"
88
-
89
- local change_dir="openspec/changes/$change_name"
90
- local yaml_file="$change_dir/.comet.yaml"
91
-
92
- # Check if .comet.yaml already exists
93
- if [ -f "$yaml_file" ]; then
94
- red "ERROR: .comet.yaml already exists at $yaml_file"
95
- exit 1
96
- fi
97
-
98
- # Create change directory if it doesn't exist
99
- mkdir -p "$change_dir"
100
-
101
- # Set workflow-appropriate defaults
102
- local phase build_mode isolation verify_mode
103
-
104
- case "$workflow" in
105
- full)
106
- phase="design"
107
- build_mode="null"
108
- isolation="null"
109
- verify_mode="null"
110
- ;;
111
- hotfix|tweak)
112
- phase="build"
113
- build_mode="direct"
114
- isolation="branch"
115
- verify_mode="light"
116
- ;;
117
- esac
118
-
119
- # Write .comet.yaml
120
- cat > "$yaml_file" <<EOF
121
- workflow: $workflow
122
- phase: $phase
123
- build_mode: $build_mode
124
- isolation: $isolation
125
- verify_mode: $verify_mode
126
- design_doc: null
127
- plan: null
128
- verify_result: pending
129
- verified_at: null
130
- archived: false
131
- EOF
132
-
133
- green "Initialized: $yaml_file (workflow=$workflow)"
134
- }
135
-
136
- cmd_get() {
137
- local change_name="$1"
138
- local field="$2"
139
-
140
- validate_change_name "$change_name"
141
-
142
- local change_dir="openspec/changes/$change_name"
143
- local yaml_file="$change_dir/.comet.yaml"
144
-
145
- # Check if .comet.yaml exists
146
- if [ ! -f "$yaml_file" ]; then
147
- red "ERROR: .comet.yaml not found at $yaml_file"
148
- exit 1
149
- fi
150
-
151
- # Read and output the field value
152
- local value
153
- value=$(yaml_field "$field" "$yaml_file")
154
- echo "${value:-}"
155
- }
156
-
157
- cmd_set() {
158
- local change_name="$1"
159
- local field="$2"
160
- local value="$3"
161
-
162
- validate_change_name "$change_name"
163
-
164
- local change_dir="openspec/changes/$change_name"
165
- local yaml_file="$change_dir/.comet.yaml"
166
-
167
- # Check if .comet.yaml exists
168
- if [ ! -f "$yaml_file" ]; then
169
- red "ERROR: .comet.yaml not found at $yaml_file"
170
- exit 1
171
- fi
172
-
173
- # Validate field name
174
- case "$field" in
175
- workflow|phase|build_mode|isolation|verify_mode|verify_result|archived|design_doc|plan|verified_at)
176
- # Valid field
177
- ;;
178
- *)
179
- red "ERROR: Unknown field: '$field'" >&2
180
- red "Valid fields: workflow, phase, design_doc, plan, build_mode, isolation, verify_mode, verify_result, verified_at, archived" >&2
181
- exit 1
182
- ;;
183
- esac
184
-
185
- # Validate enum values
186
- case "$field" in
187
- workflow)
188
- validate_enum "$value" "full" "hotfix" "tweak"
189
- ;;
190
- phase)
191
- validate_enum "$value" "design" "build" "verify" "archive"
192
- ;;
193
- build_mode)
194
- validate_enum "$value" "subagent-driven-development" "executing-plans" "direct"
195
- ;;
196
- isolation)
197
- validate_enum "$value" "branch" "worktree"
198
- ;;
199
- verify_mode)
200
- validate_enum "$value" "light" "full"
201
- ;;
202
- verify_result)
203
- validate_enum "$value" "pending" "pass" "fail"
204
- ;;
205
- archived)
206
- validate_enum "$value" "true" "false"
207
- ;;
208
- design_doc|plan|verified_at)
209
- # No validation for path fields and date fields
210
- ;;
211
- esac
212
-
213
- # Write or update the field
214
- if grep -q "^${field}:" "$yaml_file"; then
215
- # Field exists, replace it (use | delimiter to avoid / conflicts in paths)
216
- sed -i "s|^${field}:.*|${field}: ${value}|" "$yaml_file"
217
- else
218
- # Field doesn't exist, append it
219
- echo "${field}: ${value}" >> "$yaml_file"
220
- fi
221
-
222
- green "[SET] ${field}=${value}"
223
- }
224
-
225
- # --- Check helpers for entry verification ---
226
-
227
- CHECK_BLOCK=0
228
-
229
- check_pass() {
230
- local msg="$1"
231
- echo " $(green "[PASS]") $msg"
232
- }
233
-
234
- check_fail() {
235
- local msg="$1"
236
- echo " $(red "[FAIL]") $msg"
237
- CHECK_BLOCK=1
238
- }
239
-
240
- check_nonempty() {
241
- local desc="$1"
242
- local path="$2"
243
- if file_nonempty "$path"; then
244
- check_pass "$desc non-empty"
245
- else
246
- check_fail "$desc missing or empty"
247
- fi
248
- }
249
-
250
- check_yaml_is() {
251
- local field="$1"
252
- local expected="$2"
253
- local change_name="$3"
254
- local actual
255
- actual=$(cmd_get "$change_name" "$field")
256
- if [ "$actual" = "$expected" ]; then
257
- check_pass "${field}=${actual} (expected: ${expected})"
258
- else
259
- check_fail "${field}=${actual} (expected: ${expected})"
260
- fi
261
- }
262
-
263
- check_yaml_empty() {
264
- local field="$1"
265
- local change_name="$2"
266
- local value
267
- value=$(cmd_get "$change_name" "$field")
268
- if [ -z "$value" ] || [ "$value" = "null" ]; then
269
- check_pass "${field} is empty/null"
270
- else
271
- check_fail "${field}=${value} (expected: empty/null)"
272
- fi
273
- }
274
-
275
- check_file_not_exists() {
276
- local desc="$1"
277
- local path="$2"
278
- if [ ! -f "$path" ]; then
279
- check_pass "$desc does not exist"
280
- else
281
- check_fail "$desc exists (should not exist)"
282
- fi
283
- }
284
-
285
- cmd_check() {
286
- local phase="$1"
287
- local change_name="$2"
288
-
289
- validate_change_name "$change_name"
290
- validate_enum "$phase" "open" "design" "build" "verify" "archive"
291
-
292
- local change_dir="openspec/changes/$change_name"
293
- local yaml_file="$change_dir/.comet.yaml"
294
- local proposal_file="$change_dir/proposal.md"
295
- local design_file="$change_dir/design.md"
296
- local tasks_file="$change_dir/tasks.md"
297
-
298
- echo "=== Entry Check: comet-${phase} ==="
299
-
300
- # For non-open phases, .comet.yaml must exist
301
- if [ "$phase" != "open" ]; then
302
- if [ ! -f "$yaml_file" ]; then
303
- red "ERROR: .comet.yaml not found at $yaml_file"
304
- exit 1
305
- fi
306
- fi
307
-
308
- # Phase-specific checks
309
- case "$phase" in
310
- open)
311
- check_file_not_exists ".comet.yaml" "$yaml_file"
312
- check_nonempty "proposal.md" "$proposal_file"
313
- check_nonempty "design.md" "$design_file"
314
- check_nonempty "tasks.md" "$tasks_file"
315
- ;;
316
- design)
317
- check_pass ".comet.yaml exists"
318
- check_yaml_is "phase" "design" "$change_name"
319
- check_yaml_is "workflow" "full" "$change_name"
320
- check_yaml_empty "design_doc" "$change_name"
321
- check_nonempty "proposal.md" "$proposal_file"
322
- check_nonempty "design.md" "$design_file"
323
- check_nonempty "tasks.md" "$tasks_file"
324
- ;;
325
- build)
326
- check_pass ".comet.yaml exists"
327
- check_yaml_is "phase" "build" "$change_name"
328
- # Check design_doc is non-null and file exists
329
- local design_doc
330
- design_doc=$(cmd_get "$change_name" "design_doc")
331
- if [ -n "$design_doc" ] && [ "$design_doc" != "null" ] && [ -f "$change_dir/$design_doc" ]; then
332
- check_pass "design_doc=${design_doc} (file exists)"
333
- else
334
- check_fail "design_doc=${design_doc} (expected: non-null and file exists)"
335
- fi
336
- check_nonempty "proposal.md" "$proposal_file"
337
- check_nonempty "tasks.md" "$tasks_file"
338
- ;;
339
- verify)
340
- check_pass ".comet.yaml exists"
341
- check_yaml_is "phase" "verify" "$change_name"
342
- # Check verify_result is pending or null
343
- local verify_result
344
- verify_result=$(cmd_get "$change_name" "verify_result")
345
- if [ "$verify_result" = "pending" ] || [ -z "$verify_result" ] || [ "$verify_result" = "null" ]; then
346
- check_pass "verify_result=${verify_result} (expected: pending or null)"
347
- else
348
- check_fail "verify_result=${verify_result} (expected: pending or null)"
349
- fi
350
- ;;
351
- archive)
352
- check_pass ".comet.yaml exists"
353
- check_yaml_is "phase" "archive" "$change_name"
354
- check_yaml_is "verify_result" "pass" "$change_name"
355
- # Check archived is NOT true
356
- local archived
357
- archived=$(cmd_get "$change_name" "archived")
358
- if [ "$archived" != "true" ]; then
359
- check_pass "archived=${archived} (expected: not true)"
360
- else
361
- check_fail "archived=${archived} (expected: not true)"
362
- fi
363
- ;;
364
- *)
365
- red "ERROR: Unknown phase for check: $phase"
366
- exit 1
367
- ;;
368
- esac
369
-
370
- echo ""
371
- if [ "$CHECK_BLOCK" -eq 1 ]; then
372
- red "BLOCKED — fix failing checks before proceeding"
373
- exit 1
374
- else
375
- green "ALL CHECKS PASSED — ready to proceed"
376
- exit 0
377
- fi
378
- }
379
-
380
- cmd_scale() {
381
- local change_name="$1"
382
-
383
- validate_change_name "$change_name"
384
-
385
- local change_dir="openspec/changes/$change_name"
386
- local yaml_file="$change_dir/.comet.yaml"
387
-
388
- # Verify .comet.yaml exists
389
- if [ ! -f "$yaml_file" ]; then
390
- red "ERROR: .comet.yaml not found at $yaml_file"
391
- exit 1
392
- fi
393
-
394
- # Read metrics
395
- # 1. Task count: count lines matching `- [` in tasks.md
396
- local tasks_file="$change_dir/tasks.md"
397
- local task_count=0
398
- if [ -f "$tasks_file" ]; then
399
- task_count=$(grep -c '^\- \[' "$tasks_file" 2>/dev/null || echo "0")
400
- fi
401
-
402
- # 2. Delta spec count: count files named spec.md under specs/*/spec.md
403
- local delta_spec_count=0
404
- if [ -d "$change_dir/specs" ]; then
405
- delta_spec_count=$(find "$change_dir/specs" -name "spec.md" -type f 2>/dev/null | wc -l | tr -d ' ')
406
- fi
407
-
408
- # 3. Changed files: from git diff --stat HEAD
409
- local changed_files=0
410
- if git rev-parse --git-dir > /dev/null 2>&1; then
411
- # Extract the number before "file" in the last line
412
- local stat_output
413
- stat_output=$(git diff --stat HEAD 2>/dev/null | tail -1)
414
- if [[ "$stat_output" =~ ([0-9]+)\ file ]]; then
415
- changed_files="${BASH_REMATCH[1]}"
416
- fi
417
- fi
418
-
419
- # Decision rules
420
- local result="light"
421
- if [ "$task_count" -gt 3 ] || [ "$delta_spec_count" -gt 1 ] || [ "$changed_files" -gt 5 ]; then
422
- result="full"
423
- fi
424
-
425
- # Output assessment to stderr
426
- echo "=== Scale Assessment: $change_name ===" >&2
427
- echo " Tasks: $task_count (threshold: 3)" >&2
428
- echo " Delta specs: $delta_spec_count capabilities (threshold: 1)" >&2
429
- echo " Changed files: $changed_files (threshold: 5)" >&2
430
- echo " → Result: $result" >&2
431
-
432
- # Update verify_mode in .comet.yaml
433
- sed -i "s/^verify_mode:.*/verify_mode: $result/" "$yaml_file"
434
-
435
- green "[SCALE] verify_mode=$result"
436
- }
437
-
438
- # --- Main ---
439
-
440
- SUBCOMMAND="${1:-}"
441
- shift || true
442
-
443
- case "$SUBCOMMAND" in
444
- init)
445
- if [ $# -lt 2 ]; then
446
- red "Usage: comet-state.sh init <change-name> <workflow>" >&2
447
- red "Workflows: full, hotfix, tweak" >&2
448
- exit 1
449
- fi
450
- cmd_init "$@"
451
- ;;
452
- get)
453
- if [ $# -lt 2 ]; then
454
- red "Usage: comet-state.sh get <change-name> <field>" >&2
455
- exit 1
456
- fi
457
- cmd_get "$@"
458
- ;;
459
- set)
460
- if [ $# -lt 3 ]; then
461
- red "Usage: comet-state.sh set <change-name> <field> <value>" >&2
462
- exit 1
463
- fi
464
- cmd_set "$@"
465
- ;;
466
- check)
467
- if [ $# -lt 2 ]; then
468
- red "Usage: comet-state.sh check <phase> <change-name>" >&2
469
- red "Phases: open, design, build, verify, archive" >&2
470
- exit 1
471
- fi
472
- cmd_check "$@"
473
- ;;
474
- scale)
475
- if [ $# -lt 1 ]; then
476
- red "Usage: comet-state.sh scale <change-name>" >&2
477
- exit 1
478
- fi
479
- cmd_scale "$@"
480
- ;;
481
- *)
482
- red "Unknown subcommand: $SUBCOMMAND" >&2
483
- echo "" >&2
484
- echo "Usage: comet-state.sh <subcommand> <change-name> [args...]" >&2
485
- echo "" >&2
486
- echo "Subcommands:" >&2
487
- echo " init <change-name> <workflow> — Initialize .comet.yaml with workflow defaults" >&2
488
- echo " get <change-name> <field> — Read a field value from .comet.yaml" >&2
489
- echo " set <change-name> <field> <val> — Update a field value in .comet.yaml" >&2
490
- echo " check <phase> <change-name> — Verify entry requirements for a phase" >&2
491
- echo " scale <change-name> — Assess and set verification mode based on metrics" >&2
492
- echo "" >&2
493
- echo "Workflows: full, hotfix, tweak" >&2
494
- echo "Phases for check: open, design, build, verify, archive" >&2
495
- exit 1
496
- ;;
497
- esac
1
+ #!/bin/bash
2
+ # Comet State — unified interface for .comet.yaml state management
3
+ # Usage: comet-state.sh <subcommand> <change-name> [args...]
4
+ #
5
+ # Subcommands:
6
+ # init <change-name> <workflow> — Initialize .comet.yaml with workflow defaults
7
+ # get <change-name> <field> — Read a field value from .comet.yaml
8
+ # set <change-name> <field> <val> — Update a field value
9
+ # check <phase> <change-name> — Verify entry requirements for a phase
10
+ # scale <change-name> — Assess and set verification mode based on metrics
11
+ #
12
+ # Workflows: full, hotfix, tweak
13
+ # Phases for check: open, design, build, verify, archive
14
+
15
+ set -euo pipefail
16
+
17
+ # --- Color output helpers ---
18
+
19
+ red() { echo -e "\033[31m$1\033[0m" >&2; }
20
+ green() { echo -e "\033[32m$1\033[0m" >&2; }
21
+ yellow() { echo -e "\033[33m$1\033[0m" >&2; }
22
+
23
+ # --- Script location ---
24
+
25
+ # shellcheck disable=SC2034
26
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
27
+
28
+ # --- Input validation ---
29
+
30
+ validate_change_name() {
31
+ local name="$1"
32
+ # Reject empty names
33
+ if [ -z "$name" ]; then
34
+ red "ERROR: Change name cannot be empty" >&2
35
+ exit 1
36
+ fi
37
+ # Only allow alphanumeric, hyphens, and underscores
38
+ if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
39
+ red "ERROR: Invalid change name: '$name'" >&2
40
+ red "Valid characters: a-z, A-Z, 0-9, -, _" >&2
41
+ exit 1
42
+ fi
43
+ # Reject path traversal attempts
44
+ if [[ "$name" =~ \.\. ]]; then
45
+ red "ERROR: Change name cannot contain '..' (path traversal not allowed)" >&2
46
+ exit 1
47
+ fi
48
+ }
49
+
50
+ validate_enum() {
51
+ local value="$1"
52
+ shift
53
+ local valid_values=("$@")
54
+
55
+ for valid in "${valid_values[@]}"; do
56
+ if [ "$value" = "$valid" ]; then
57
+ return 0
58
+ fi
59
+ done
60
+
61
+ red "ERROR: Invalid value: '$value'" >&2
62
+ red "Valid values: ${valid_values[*]}" >&2
63
+ exit 1
64
+ }
65
+
66
+ # --- Helper functions ---
67
+
68
+ yaml_field() {
69
+ local field="$1"
70
+ local yaml_file="$2"
71
+ if [ -f "$yaml_file" ]; then
72
+ grep "^${field}:" "$yaml_file" | sed "s/^${field}: *//" | tr -d '"' | tr -d "'"
73
+ fi
74
+ }
75
+
76
+ file_nonempty() {
77
+ [ -f "$1" ] && [ -s "$1" ]
78
+ }
79
+
80
+ # --- Subcommands ---
81
+
82
+ cmd_init() {
83
+ local change_name="$1"
84
+ local workflow="$2"
85
+
86
+ validate_change_name "$change_name"
87
+ validate_enum "$workflow" "full" "hotfix" "tweak"
88
+
89
+ local change_dir="openspec/changes/$change_name"
90
+ local yaml_file="$change_dir/.comet.yaml"
91
+
92
+ # Check if .comet.yaml already exists
93
+ if [ -f "$yaml_file" ]; then
94
+ red "ERROR: .comet.yaml already exists at $yaml_file"
95
+ exit 1
96
+ fi
97
+
98
+ # Create change directory if it doesn't exist
99
+ mkdir -p "$change_dir"
100
+
101
+ # Set workflow-appropriate defaults
102
+ local phase build_mode isolation verify_mode
103
+
104
+ case "$workflow" in
105
+ full)
106
+ phase="design"
107
+ build_mode="null"
108
+ isolation="null"
109
+ verify_mode="null"
110
+ ;;
111
+ hotfix|tweak)
112
+ phase="build"
113
+ build_mode="direct"
114
+ isolation="branch"
115
+ verify_mode="light"
116
+ ;;
117
+ esac
118
+
119
+ # Write .comet.yaml
120
+ cat > "$yaml_file" <<EOF
121
+ workflow: $workflow
122
+ phase: $phase
123
+ build_mode: $build_mode
124
+ isolation: $isolation
125
+ verify_mode: $verify_mode
126
+ design_doc: null
127
+ plan: null
128
+ verify_result: pending
129
+ verified_at: null
130
+ archived: false
131
+ EOF
132
+
133
+ green "Initialized: $yaml_file (workflow=$workflow)"
134
+ }
135
+
136
+ cmd_get() {
137
+ local change_name="$1"
138
+ local field="$2"
139
+
140
+ validate_change_name "$change_name"
141
+
142
+ local change_dir="openspec/changes/$change_name"
143
+ local yaml_file="$change_dir/.comet.yaml"
144
+
145
+ # Check if .comet.yaml exists
146
+ if [ ! -f "$yaml_file" ]; then
147
+ red "ERROR: .comet.yaml not found at $yaml_file"
148
+ exit 1
149
+ fi
150
+
151
+ # Read and output the field value
152
+ local value
153
+ value=$(yaml_field "$field" "$yaml_file")
154
+ echo "${value:-}"
155
+ }
156
+
157
+ cmd_set() {
158
+ local change_name="$1"
159
+ local field="$2"
160
+ local value="$3"
161
+
162
+ validate_change_name "$change_name"
163
+
164
+ local change_dir="openspec/changes/$change_name"
165
+ local yaml_file="$change_dir/.comet.yaml"
166
+
167
+ # Check if .comet.yaml exists
168
+ if [ ! -f "$yaml_file" ]; then
169
+ red "ERROR: .comet.yaml not found at $yaml_file"
170
+ exit 1
171
+ fi
172
+
173
+ # Validate field name
174
+ case "$field" in
175
+ workflow|phase|build_mode|isolation|verify_mode|verify_result|archived|design_doc|plan|verified_at)
176
+ # Valid field
177
+ ;;
178
+ *)
179
+ red "ERROR: Unknown field: '$field'" >&2
180
+ red "Valid fields: workflow, phase, design_doc, plan, build_mode, isolation, verify_mode, verify_result, verified_at, archived" >&2
181
+ exit 1
182
+ ;;
183
+ esac
184
+
185
+ # Validate enum values
186
+ case "$field" in
187
+ workflow)
188
+ validate_enum "$value" "full" "hotfix" "tweak"
189
+ ;;
190
+ phase)
191
+ validate_enum "$value" "design" "build" "verify" "archive"
192
+ ;;
193
+ build_mode)
194
+ validate_enum "$value" "subagent-driven-development" "executing-plans" "direct"
195
+ ;;
196
+ isolation)
197
+ validate_enum "$value" "branch" "worktree"
198
+ ;;
199
+ verify_mode)
200
+ validate_enum "$value" "light" "full"
201
+ ;;
202
+ verify_result)
203
+ validate_enum "$value" "pending" "pass" "fail"
204
+ ;;
205
+ archived)
206
+ validate_enum "$value" "true" "false"
207
+ ;;
208
+ design_doc|plan|verified_at)
209
+ # No validation for path fields and date fields
210
+ ;;
211
+ esac
212
+
213
+ # Write or update the field
214
+ if grep -q "^${field}:" "$yaml_file"; then
215
+ # Field exists, replace it (use | delimiter to avoid / conflicts in paths)
216
+ sed -i "s|^${field}:.*|${field}: ${value}|" "$yaml_file"
217
+ else
218
+ # Field doesn't exist, append it
219
+ echo "${field}: ${value}" >> "$yaml_file"
220
+ fi
221
+
222
+ green "[SET] ${field}=${value}"
223
+ }
224
+
225
+ # --- Check helpers for entry verification ---
226
+
227
+ CHECK_BLOCK=0
228
+
229
+ check_pass() {
230
+ local msg="$1"
231
+ echo " $(green "[PASS]") $msg"
232
+ }
233
+
234
+ check_fail() {
235
+ local msg="$1"
236
+ echo " $(red "[FAIL]") $msg"
237
+ CHECK_BLOCK=1
238
+ }
239
+
240
+ check_nonempty() {
241
+ local desc="$1"
242
+ local path="$2"
243
+ if file_nonempty "$path"; then
244
+ check_pass "$desc non-empty"
245
+ else
246
+ check_fail "$desc missing or empty"
247
+ fi
248
+ }
249
+
250
+ check_yaml_is() {
251
+ local field="$1"
252
+ local expected="$2"
253
+ local change_name="$3"
254
+ local actual
255
+ actual=$(cmd_get "$change_name" "$field")
256
+ if [ "$actual" = "$expected" ]; then
257
+ check_pass "${field}=${actual} (expected: ${expected})"
258
+ else
259
+ check_fail "${field}=${actual} (expected: ${expected})"
260
+ fi
261
+ }
262
+
263
+ check_yaml_empty() {
264
+ local field="$1"
265
+ local change_name="$2"
266
+ local value
267
+ value=$(cmd_get "$change_name" "$field")
268
+ if [ -z "$value" ] || [ "$value" = "null" ]; then
269
+ check_pass "${field} is empty/null"
270
+ else
271
+ check_fail "${field}=${value} (expected: empty/null)"
272
+ fi
273
+ }
274
+
275
+ check_file_not_exists() {
276
+ local desc="$1"
277
+ local path="$2"
278
+ if [ ! -f "$path" ]; then
279
+ check_pass "$desc does not exist"
280
+ else
281
+ check_fail "$desc exists (should not exist)"
282
+ fi
283
+ }
284
+
285
+ cmd_check() {
286
+ local phase="$1"
287
+ local change_name="$2"
288
+
289
+ validate_change_name "$change_name"
290
+ validate_enum "$phase" "open" "design" "build" "verify" "archive"
291
+
292
+ local change_dir="openspec/changes/$change_name"
293
+ local yaml_file="$change_dir/.comet.yaml"
294
+ local proposal_file="$change_dir/proposal.md"
295
+ local design_file="$change_dir/design.md"
296
+ local tasks_file="$change_dir/tasks.md"
297
+
298
+ echo "=== Entry Check: comet-${phase} ==="
299
+
300
+ # For non-open phases, .comet.yaml must exist
301
+ if [ "$phase" != "open" ]; then
302
+ if [ ! -f "$yaml_file" ]; then
303
+ red "ERROR: .comet.yaml not found at $yaml_file"
304
+ exit 1
305
+ fi
306
+ fi
307
+
308
+ # Phase-specific checks
309
+ case "$phase" in
310
+ open)
311
+ check_file_not_exists ".comet.yaml" "$yaml_file"
312
+ check_nonempty "proposal.md" "$proposal_file"
313
+ check_nonempty "design.md" "$design_file"
314
+ check_nonempty "tasks.md" "$tasks_file"
315
+ ;;
316
+ design)
317
+ check_pass ".comet.yaml exists"
318
+ check_yaml_is "phase" "design" "$change_name"
319
+ check_yaml_is "workflow" "full" "$change_name"
320
+ check_yaml_empty "design_doc" "$change_name"
321
+ check_nonempty "proposal.md" "$proposal_file"
322
+ check_nonempty "design.md" "$design_file"
323
+ check_nonempty "tasks.md" "$tasks_file"
324
+ ;;
325
+ build)
326
+ check_pass ".comet.yaml exists"
327
+ check_yaml_is "phase" "build" "$change_name"
328
+ # Check design_doc is non-null and file exists
329
+ local design_doc
330
+ design_doc=$(cmd_get "$change_name" "design_doc")
331
+ if [ -n "$design_doc" ] && [ "$design_doc" != "null" ] && [ -f "$change_dir/$design_doc" ]; then
332
+ check_pass "design_doc=${design_doc} (file exists)"
333
+ else
334
+ check_fail "design_doc=${design_doc} (expected: non-null and file exists)"
335
+ fi
336
+ check_nonempty "proposal.md" "$proposal_file"
337
+ check_nonempty "tasks.md" "$tasks_file"
338
+ ;;
339
+ verify)
340
+ check_pass ".comet.yaml exists"
341
+ check_yaml_is "phase" "verify" "$change_name"
342
+ # Check verify_result is pending or null
343
+ local verify_result
344
+ verify_result=$(cmd_get "$change_name" "verify_result")
345
+ if [ "$verify_result" = "pending" ] || [ -z "$verify_result" ] || [ "$verify_result" = "null" ]; then
346
+ check_pass "verify_result=${verify_result} (expected: pending or null)"
347
+ else
348
+ check_fail "verify_result=${verify_result} (expected: pending or null)"
349
+ fi
350
+ ;;
351
+ archive)
352
+ check_pass ".comet.yaml exists"
353
+ check_yaml_is "phase" "archive" "$change_name"
354
+ check_yaml_is "verify_result" "pass" "$change_name"
355
+ # Check archived is NOT true
356
+ local archived
357
+ archived=$(cmd_get "$change_name" "archived")
358
+ if [ "$archived" != "true" ]; then
359
+ check_pass "archived=${archived} (expected: not true)"
360
+ else
361
+ check_fail "archived=${archived} (expected: not true)"
362
+ fi
363
+ ;;
364
+ *)
365
+ red "ERROR: Unknown phase for check: $phase"
366
+ exit 1
367
+ ;;
368
+ esac
369
+
370
+ echo ""
371
+ if [ "$CHECK_BLOCK" -eq 1 ]; then
372
+ red "BLOCKED — fix failing checks before proceeding"
373
+ exit 1
374
+ else
375
+ green "ALL CHECKS PASSED — ready to proceed"
376
+ exit 0
377
+ fi
378
+ }
379
+
380
+ cmd_scale() {
381
+ local change_name="$1"
382
+
383
+ validate_change_name "$change_name"
384
+
385
+ local change_dir="openspec/changes/$change_name"
386
+ local yaml_file="$change_dir/.comet.yaml"
387
+
388
+ # Verify .comet.yaml exists
389
+ if [ ! -f "$yaml_file" ]; then
390
+ red "ERROR: .comet.yaml not found at $yaml_file"
391
+ exit 1
392
+ fi
393
+
394
+ # Read metrics
395
+ # 1. Task count: count lines matching `- [` in tasks.md
396
+ local tasks_file="$change_dir/tasks.md"
397
+ local task_count=0
398
+ if [ -f "$tasks_file" ]; then
399
+ task_count=$(grep -c '^\- \[' "$tasks_file" 2>/dev/null || echo "0")
400
+ fi
401
+
402
+ # 2. Delta spec count: count files named spec.md under specs/*/spec.md
403
+ local delta_spec_count=0
404
+ if [ -d "$change_dir/specs" ]; then
405
+ delta_spec_count=$(find "$change_dir/specs" -name "spec.md" -type f 2>/dev/null | wc -l | tr -d ' ')
406
+ fi
407
+
408
+ # 3. Changed files: from git diff --stat HEAD
409
+ local changed_files=0
410
+ if git rev-parse --git-dir > /dev/null 2>&1; then
411
+ # Extract the number before "file" in the last line
412
+ local stat_output
413
+ stat_output=$(git diff --stat HEAD 2>/dev/null | tail -1)
414
+ if [[ "$stat_output" =~ ([0-9]+)\ file ]]; then
415
+ changed_files="${BASH_REMATCH[1]}"
416
+ fi
417
+ fi
418
+
419
+ # Decision rules
420
+ local result="light"
421
+ if [ "$task_count" -gt 3 ] || [ "$delta_spec_count" -gt 1 ] || [ "$changed_files" -gt 5 ]; then
422
+ result="full"
423
+ fi
424
+
425
+ # Output assessment to stderr
426
+ echo "=== Scale Assessment: $change_name ===" >&2
427
+ echo " Tasks: $task_count (threshold: 3)" >&2
428
+ echo " Delta specs: $delta_spec_count capabilities (threshold: 1)" >&2
429
+ echo " Changed files: $changed_files (threshold: 5)" >&2
430
+ echo " → Result: $result" >&2
431
+
432
+ # Update verify_mode in .comet.yaml
433
+ sed -i "s/^verify_mode:.*/verify_mode: $result/" "$yaml_file"
434
+
435
+ green "[SCALE] verify_mode=$result"
436
+ }
437
+
438
+ # --- Main ---
439
+
440
+ SUBCOMMAND="${1:-}"
441
+ shift || true
442
+
443
+ case "$SUBCOMMAND" in
444
+ init)
445
+ if [ $# -lt 2 ]; then
446
+ red "Usage: comet-state.sh init <change-name> <workflow>" >&2
447
+ red "Workflows: full, hotfix, tweak" >&2
448
+ exit 1
449
+ fi
450
+ cmd_init "$@"
451
+ ;;
452
+ get)
453
+ if [ $# -lt 2 ]; then
454
+ red "Usage: comet-state.sh get <change-name> <field>" >&2
455
+ exit 1
456
+ fi
457
+ cmd_get "$@"
458
+ ;;
459
+ set)
460
+ if [ $# -lt 3 ]; then
461
+ red "Usage: comet-state.sh set <change-name> <field> <value>" >&2
462
+ exit 1
463
+ fi
464
+ cmd_set "$@"
465
+ ;;
466
+ check)
467
+ if [ $# -lt 2 ]; then
468
+ red "Usage: comet-state.sh check <phase> <change-name>" >&2
469
+ red "Phases: open, design, build, verify, archive" >&2
470
+ exit 1
471
+ fi
472
+ cmd_check "$@"
473
+ ;;
474
+ scale)
475
+ if [ $# -lt 1 ]; then
476
+ red "Usage: comet-state.sh scale <change-name>" >&2
477
+ exit 1
478
+ fi
479
+ cmd_scale "$@"
480
+ ;;
481
+ *)
482
+ red "Unknown subcommand: $SUBCOMMAND" >&2
483
+ echo "" >&2
484
+ echo "Usage: comet-state.sh <subcommand> <change-name> [args...]" >&2
485
+ echo "" >&2
486
+ echo "Subcommands:" >&2
487
+ echo " init <change-name> <workflow> — Initialize .comet.yaml with workflow defaults" >&2
488
+ echo " get <change-name> <field> — Read a field value from .comet.yaml" >&2
489
+ echo " set <change-name> <field> <val> — Update a field value in .comet.yaml" >&2
490
+ echo " check <phase> <change-name> — Verify entry requirements for a phase" >&2
491
+ echo " scale <change-name> — Assess and set verification mode based on metrics" >&2
492
+ echo "" >&2
493
+ echo "Workflows: full, hotfix, tweak" >&2
494
+ echo "Phases for check: open, design, build, verify, archive" >&2
495
+ exit 1
496
+ ;;
497
+ esac