@rpamis/comet 0.2.5 → 0.2.6

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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 rpamis
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rpamis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -235,6 +235,8 @@ verified_at: null
235
235
  archived: false
236
236
  ```
237
237
 
238
+ In full workflow, `build_mode` and `isolation` may temporarily be `null` at init time, but they must be resolved before `build → verify`. `direct` is allowed by default only for hotfix/tweak; full workflow requires `direct_override: true`. Projects can configure `build_command` / `verify_command` in the change or repo root, and guard will run those commands first and print failure output.
239
+
238
240
  All states and execution phases are updated via scripts, and **each phase verifies that tasks are truly completed before exiting — conditions are met before the phase exits and state is updated**. Compared to recording complex state management mechanisms in Skills, the script approach strongly guarantees the reliability of core state transitions, correctness of YAML files, and convenience of breakpoint recovery — Agents only need to use Comet's built-in commands to read state and know the current Spec's situation.
239
241
 
240
242
  ### Reliability Features
@@ -252,18 +254,23 @@ Comet ensures agent execution reliability through automated state transitions:
252
254
  - Guard and archive scripts use `comet-state.sh` internally for state management
253
255
 
254
256
  3. **Schema Validation** — `comet-yaml-validate.sh` ensures data integrity
255
- - Validates required fields (12 fields)
256
- - Validates enum values (8 enum types)
257
+ - Validates required and optional fields
258
+ - Validates enum values, including `direct_override`
257
259
  - Validates referenced file paths exist
258
260
  - Detects unknown/typos fields
259
261
 
260
- 4. **Verification Evidence** — Guard enforces proof before phase advance
262
+ 4. **Build Decision Enforcement** — Guard and state transitions both block skipped build choices
263
+ - `isolation` must be `branch` or `worktree`
264
+ - `build_mode` must be selected before leaving build
265
+ - Full workflow `build_mode: direct` requires `direct_override: true`
266
+
267
+ 5. **Verification Evidence** — Guard enforces proof before phase advance
261
268
  - `verify-pass` transition requires `verification_report` pointing to an existing report file
262
269
  - `branch_status` must be `handled` before verify can pass
263
270
  - Guard checks `verification_report exists` and `branch_status=handled` as hard prerequisites
264
271
  - Prevents false phase advances when verification or branch handling was skipped
265
272
 
266
- 5. **Archive Automation** — `comet-archive.sh` handles the full archive flow in one command
273
+ 6. **Archive Automation** — `comet-archive.sh` handles the full archive flow in one command
267
274
  - Validates entry state, syncs delta specs to main specs
268
275
  - Annotates design doc and plan frontmatter
269
276
  - Moves change to archive directory and updates `archived: true`
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.5",
2
+ "version": "0.2.6",
3
3
  "skills": [
4
4
  "comet/SKILL.md",
5
5
  "comet/scripts/comet-guard.sh",
@@ -156,6 +156,8 @@ build_mode: subagent-driven-development
156
156
  isolation: branch
157
157
  verify_mode: light
158
158
  verify_result: pending
159
+ verification_report: null
160
+ branch_status: pending
159
161
  verified_at: null
160
162
  archived: false
161
163
  ```
@@ -167,12 +169,28 @@ archived: false
167
169
  | `design_doc` | Associated Superpowers Design Doc path, can be empty |
168
170
  | `plan` | Associated Superpowers Plan path, can be empty |
169
171
  | `build_mode` | Selected execution mode, can be empty |
170
- | `isolation` | `branch` or `worktree`, workspace isolation method, defaults to `branch` |
172
+ | `isolation` | `branch` or `worktree`, workspace isolation method. Full workflow init may leave this as `null`, but only until `/comet-build` Step 3; hotfix/tweak default to `branch` |
171
173
  | `verify_mode` | `light` or `full`, can be empty |
172
174
  | `verify_result` | `pending`, `pass`, or `fail` |
175
+ | `verification_report` | Verification report file path; must point to an existing file before verify can pass |
176
+ | `branch_status` | `pending` or `handled`; set to `handled` after branch handling completes |
173
177
  | `verified_at` | Verification pass time, can be empty |
174
178
  | `archived` | Whether change is archived |
175
179
 
180
+ Optional fields:
181
+
182
+ | Field | Meaning |
183
+ |-------|---------|
184
+ | `direct_override` | `true`/`false`. Full workflow may use `build_mode: direct` only when this is explicitly `true` |
185
+ | `build_command` | Project build command. Guard runs this first and prints failure output |
186
+ | `verify_command` | Project verification command. Verify guard runs this first; if absent, it falls back to the build command |
187
+
188
+ State-machine hard constraints:
189
+ - Before `build → verify`, `isolation` must be `branch` or `worktree`
190
+ - Before `build → verify`, `build_mode` must be selected
191
+ - `build_mode: direct` is allowed by default only for `hotfix` / `tweak`; full workflow requires `direct_override: true`
192
+ - These constraints are enforced by both `comet-guard.sh build --apply` and `comet-state.sh transition <name> build-complete`
193
+
176
194
  ### Script Location
177
195
 
178
196
  Comet scripts are distributed in `comet/scripts/`. **Do not hardcode paths** — locate once, cache in env vars:
@@ -1,258 +1,255 @@
1
- #!/bin/bash
2
- # Comet Archive — automates the archive phase in one command
3
- # Usage: comet-archive.sh <change-name> [--dry-run]
4
- # Exit 0 = archive complete, exit 1 = fatal error
5
-
6
- set -euo pipefail
7
-
8
- red() { echo -e "\033[31m$1\033[0m" >&2; }
9
- green() { echo -e "\033[32m$1\033[0m" >&2; }
10
- yellow() { echo -e "\033[33m$1\033[0m" >&2; }
11
-
12
- DRY_RUN=0
13
- if [[ "${2:-}" == "--dry-run" ]]; then
14
- DRY_RUN=1
15
- fi
16
-
17
- # Input validation
18
- validate_change_name() {
19
- local name="$1"
20
- if [ -z "$name" ]; then
21
- red "FATAL: Change name cannot be empty"
22
- exit 1
23
- fi
24
- if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
25
- red "FATAL: Invalid change name: '$name'"
26
- red "Valid characters: a-z, A-Z, 0-9, -, _"
27
- exit 1
28
- fi
29
- if [[ "$name" =~ \.\. ]]; then
30
- red "FATAL: Change name cannot contain '..'"
31
- exit 1
32
- fi
33
- }
34
-
35
- CHANGE="$1"
36
- validate_change_name "$CHANGE"
37
-
38
- CHANGE_DIR="openspec/changes/$CHANGE"
39
- YAML="$CHANGE_DIR/.comet.yaml"
40
- SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" 2>/dev/null || dirname "$0")" && pwd)"
41
- STATE_SH="$SCRIPT_DIR/comet-state.sh"
42
- TODAY=$(date +%Y-%m-%d)
43
- ARCHIVE_NAME="${TODAY}-${CHANGE}"
44
- ARCHIVE_DIR="openspec/changes/archive/${ARCHIVE_NAME}"
45
-
46
- STEPS_OK=0
47
- STEPS_TOTAL=0
48
-
49
- step_ok() {
50
- green " [OK] $1"
51
- STEPS_OK=$((STEPS_OK + 1))
52
- STEPS_TOTAL=$((STEPS_TOTAL + 1))
53
- }
54
-
55
- step_fail() {
56
- red " [FAIL] $1"
57
- STEPS_TOTAL=$((STEPS_TOTAL + 1))
58
- }
59
-
60
- echo "=== Comet Archive: $CHANGE ===" >&2
61
-
62
- # --- Step 1: Read .comet.yaml, extract paths ---
63
-
64
- yaml_field() {
65
- local field="$1"
66
- if [ -f "$STATE_SH" ]; then
67
- bash "$STATE_SH" get "$CHANGE" "$field" 2>/dev/null
68
- else
69
- if [ -f "$YAML" ]; then
70
- grep "^${field}:" "$YAML" | sed "s/^${field}: *//" | tr -d '"' | tr -d "'"
71
- fi
72
- fi
73
- }
74
-
75
- if [ ! -f "$YAML" ]; then
76
- red "FATAL: .comet.yaml not found in $CHANGE_DIR/"
77
- exit 1
78
- fi
79
-
80
- DESIGN_DOC=$(yaml_field "design_doc")
81
- PLAN_PATH=$(yaml_field "plan")
82
-
83
- # --- Step 2: Validate entry state ---
84
-
85
- PHASE_VAL=$(yaml_field "phase")
86
- VERIFY_VAL=$(yaml_field "verify_result")
87
- ARCHIVED_VAL=$(yaml_field "archived")
88
-
89
- if [ "$PHASE_VAL" != "archive" ]; then
90
- red "FATAL: phase is '$PHASE_VAL', expected 'archive'"
91
- exit 1
92
- fi
93
-
94
- if [ "$VERIFY_VAL" != "pass" ]; then
95
- red "FATAL: verify_result is '$VERIFY_VAL', expected 'pass'. Run comet-verify first."
96
- exit 1
97
- fi
98
-
99
- if [ "$ARCHIVED_VAL" = "true" ]; then
100
- red "FATAL: change already archived"
101
- exit 1
102
- fi
103
-
104
- step_ok "Entry state verified"
105
-
106
- # --- Step 3: Check archive target ---
107
-
108
- if [ -d "$ARCHIVE_DIR" ]; then
109
- red "FATAL: archive target already exists: $ARCHIVE_DIR"
110
- exit 1
111
- fi
112
-
113
- step_ok "Archive target available"
114
-
115
- # --- Step 4: Sync delta specs main specs ---
116
-
117
- sync_delta_specs() {
118
- local delta_root="$CHANGE_DIR/specs"
119
- if [ ! -d "$delta_root" ]; then
120
- return 0
121
- fi
122
-
123
- for delta_spec_dir in "$delta_root"/*/; do
124
- [ -d "$delta_spec_dir" ] || continue
125
- local capability
126
- capability=$(basename "$delta_spec_dir")
127
- local delta_spec="$delta_spec_dir/spec.md"
128
- local main_spec="openspec/specs/$capability/spec.md"
129
-
130
- if [ ! -f "$delta_spec" ]; then
131
- continue
132
- fi
133
-
134
- STEPS_TOTAL=$((STEPS_TOTAL + 1))
135
-
136
- if [ "$DRY_RUN" -eq 1 ]; then
137
- yellow " [DRY-RUN] Would sync: $capability → $main_spec"
138
- STEPS_OK=$((STEPS_OK + 1))
139
- continue
140
- fi
141
-
142
- if [ ! -f "$main_spec" ]; then
143
- mkdir -p "openspec/specs/$capability"
144
- fi
145
- cp "$delta_spec" "$main_spec"
146
-
147
- step_ok "Delta spec synced: $capability openspec/specs/$capability/spec.md"
148
- done
149
- }
150
-
151
- sync_delta_specs
152
-
153
- # --- Step 5: Annotate design doc frontmatter ---
154
-
155
- annotate_frontmatter() {
156
- local file="$1"
157
- local extra_fields="$2"
158
-
159
- if [ ! -f "$file" ]; then
160
- return 0
161
- fi
162
-
163
- STEPS_TOTAL=$((STEPS_TOTAL + 1))
164
-
165
- if [ "$DRY_RUN" -eq 1 ]; then
166
- yellow " [DRY-RUN] Would annotate: $file"
167
- STEPS_OK=$((STEPS_OK + 1))
168
- return 0
169
- fi
170
-
171
- if head -1 "$file" | grep -q '^---'; then
172
- local tmp_file
173
- tmp_file=$(mktemp)
174
- awk -v archive="$ARCHIVE_NAME" -v extra="$extra_fields" '
175
- /^archived-with:/ { next }
176
- NR==1 && /^---/ { print; next }
177
- /^---/ && NR>1 {
178
- print "archived-with: " archive
179
- if (extra != "") print extra
180
- print; next
181
- }
182
- { print }
183
- ' "$file" > "$tmp_file"
184
- mv "$tmp_file" "$file"
185
- else
186
- local tmp_file
187
- tmp_file=$(mktemp)
188
- {
189
- echo "---"
190
- echo "archived-with: $ARCHIVE_NAME"
191
- if [ -n "$extra_fields" ]; then
192
- echo "$extra_fields"
193
- fi
194
- echo "status: final"
195
- echo "---"
196
- cat "$file"
197
- } > "$tmp_file"
198
- mv "$tmp_file" "$file"
199
- fi
200
-
201
- step_ok "Annotated: $file"
202
- }
203
-
204
- if [ -n "$DESIGN_DOC" ] && [ "$DESIGN_DOC" != "null" ]; then
205
- annotate_frontmatter "$DESIGN_DOC" "status: final"
206
- fi
207
-
208
- # --- Step 6: Annotate plan frontmatter ---
209
-
210
- if [ -n "$PLAN_PATH" ] && [ "$PLAN_PATH" != "null" ]; then
211
- annotate_frontmatter "$PLAN_PATH" ""
212
- fi
213
-
214
- # --- Step 7: Move change to archive ---
215
-
216
- STEPS_TOTAL=$((STEPS_TOTAL + 1))
217
-
218
- if [ "$DRY_RUN" -eq 1 ]; then
219
- yellow " [DRY-RUN] Would move: $CHANGE_DIR $ARCHIVE_DIR"
220
- STEPS_OK=$((STEPS_OK + 1))
221
- else
222
- mkdir -p "openspec/changes/archive"
223
- mv "$CHANGE_DIR" "$ARCHIVE_DIR"
224
- step_ok "Moved to: $ARCHIVE_DIR"
225
- fi
226
-
227
- # --- Step 8: Mark archived via comet-state transition ---
228
-
229
- STEPS_TOTAL=$((STEPS_TOTAL + 1))
230
-
231
- ARCHIVE_YAML="$ARCHIVE_DIR/.comet.yaml"
232
-
233
- if [ "$DRY_RUN" -eq 1 ]; then
234
- yellow " [DRY-RUN] Would set archived: true in $ARCHIVE_YAML"
235
- STEPS_OK=$((STEPS_OK + 1))
236
- else
237
- if [ -f "$ARCHIVE_YAML" ]; then
238
- bash "$STATE_SH" transition "$ARCHIVE_NAME" archived >/dev/null
239
- step_ok "archived: true"
240
- else
241
- step_fail "archived: true (.comet.yaml not found after move)"
242
- fi
243
- fi
244
-
245
- # --- Step 9: Print summary ---
246
-
247
- echo "" >&2
248
- if [ "$DRY_RUN" -eq 1 ]; then
249
- yellow "Dry run complete. $STEPS_OK/$STEPS_TOTAL steps would succeed."
250
- else
251
- green "Archive complete. $STEPS_OK/$STEPS_TOTAL steps succeeded."
252
- fi
253
-
254
- if [ "$STEPS_OK" -lt "$STEPS_TOTAL" ]; then
255
- exit 1
256
- fi
257
-
258
- exit 0
1
+ #!/bin/bash
2
+ # Comet Archive — automates the archive phase in one command
3
+ # Usage: comet-archive.sh <change-name> [--dry-run]
4
+ # Exit 0 = archive complete, exit 1 = fatal error
5
+
6
+ set -euo pipefail
7
+
8
+ red() { echo -e "\033[31m$1\033[0m" >&2; }
9
+ green() { echo -e "\033[32m$1\033[0m" >&2; }
10
+ yellow() { echo -e "\033[33m$1\033[0m" >&2; }
11
+
12
+ DRY_RUN=0
13
+ if [[ "${2:-}" == "--dry-run" ]]; then
14
+ DRY_RUN=1
15
+ fi
16
+
17
+ # Input validation
18
+ validate_change_name() {
19
+ local name="$1"
20
+ if [ -z "$name" ]; then
21
+ red "FATAL: Change name cannot be empty"
22
+ exit 1
23
+ fi
24
+ if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
25
+ red "FATAL: Invalid change name: '$name'"
26
+ red "Valid characters: a-z, A-Z, 0-9, -, _"
27
+ exit 1
28
+ fi
29
+ if [[ "$name" =~ \.\. ]]; then
30
+ red "FATAL: Change name cannot contain '..'"
31
+ exit 1
32
+ fi
33
+ }
34
+
35
+ CHANGE="$1"
36
+ validate_change_name "$CHANGE"
37
+
38
+ CHANGE_DIR="openspec/changes/$CHANGE"
39
+ YAML="$CHANGE_DIR/.comet.yaml"
40
+ SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" 2>/dev/null || dirname "$0")" && pwd)"
41
+ STATE_SH="$SCRIPT_DIR/comet-state.sh"
42
+ TODAY=$(date +%Y-%m-%d)
43
+ ARCHIVE_NAME="${TODAY}-${CHANGE}"
44
+ ARCHIVE_DIR="openspec/changes/archive/${ARCHIVE_NAME}"
45
+
46
+ STEPS_OK=0
47
+ STEPS_TOTAL=0
48
+
49
+ step_ok() {
50
+ green " [OK] $1"
51
+ STEPS_OK=$((STEPS_OK + 1))
52
+ STEPS_TOTAL=$((STEPS_TOTAL + 1))
53
+ }
54
+
55
+ step_fail() {
56
+ red " [FAIL] $1"
57
+ STEPS_TOTAL=$((STEPS_TOTAL + 1))
58
+ }
59
+
60
+ step_dry_run() {
61
+ yellow " [DRY-RUN] $1"
62
+ STEPS_OK=$((STEPS_OK + 1))
63
+ STEPS_TOTAL=$((STEPS_TOTAL + 1))
64
+ }
65
+
66
+ echo "=== Comet Archive: $CHANGE ===" >&2
67
+
68
+ # --- Step 1: Read .comet.yaml, extract paths ---
69
+
70
+ yaml_field() {
71
+ local field="$1"
72
+ if [ -f "$STATE_SH" ]; then
73
+ bash "$STATE_SH" get "$CHANGE" "$field" 2>/dev/null
74
+ else
75
+ if [ -f "$YAML" ]; then
76
+ grep "^${field}:" "$YAML" | sed "s/^${field}: *//" | tr -d '"' | tr -d "'"
77
+ fi
78
+ fi
79
+ }
80
+
81
+ if [ ! -f "$YAML" ]; then
82
+ red "FATAL: .comet.yaml not found in $CHANGE_DIR/"
83
+ exit 1
84
+ fi
85
+
86
+ DESIGN_DOC=$(yaml_field "design_doc")
87
+ PLAN_PATH=$(yaml_field "plan")
88
+
89
+ # --- Step 2: Validate entry state ---
90
+
91
+ PHASE_VAL=$(yaml_field "phase")
92
+ VERIFY_VAL=$(yaml_field "verify_result")
93
+ ARCHIVED_VAL=$(yaml_field "archived")
94
+
95
+ if [ "$PHASE_VAL" != "archive" ]; then
96
+ red "FATAL: phase is '$PHASE_VAL', expected 'archive'"
97
+ exit 1
98
+ fi
99
+
100
+ if [ "$VERIFY_VAL" != "pass" ]; then
101
+ red "FATAL: verify_result is '$VERIFY_VAL', expected 'pass'. Run comet-verify first."
102
+ exit 1
103
+ fi
104
+
105
+ if [ "$ARCHIVED_VAL" = "true" ]; then
106
+ red "FATAL: change already archived"
107
+ exit 1
108
+ fi
109
+
110
+ step_ok "Entry state verified"
111
+
112
+ # --- Step 3: Check archive target ---
113
+
114
+ if [ -d "$ARCHIVE_DIR" ]; then
115
+ red "FATAL: archive target already exists: $ARCHIVE_DIR"
116
+ exit 1
117
+ fi
118
+
119
+ step_ok "Archive target available"
120
+
121
+ # --- Step 4: Sync delta specs → main specs ---
122
+
123
+ sync_delta_specs() {
124
+ local delta_root="$CHANGE_DIR/specs"
125
+ if [ ! -d "$delta_root" ]; then
126
+ return 0
127
+ fi
128
+
129
+ for delta_spec_dir in "$delta_root"/*/; do
130
+ [ -d "$delta_spec_dir" ] || continue
131
+ local capability
132
+ capability=$(basename "$delta_spec_dir")
133
+ local delta_spec="$delta_spec_dir/spec.md"
134
+ local main_spec="openspec/specs/$capability/spec.md"
135
+
136
+ if [ ! -f "$delta_spec" ]; then
137
+ continue
138
+ fi
139
+
140
+ if [ "$DRY_RUN" -eq 1 ]; then
141
+ step_dry_run "Would sync: $capability → $main_spec"
142
+ continue
143
+ fi
144
+
145
+ if [ ! -f "$main_spec" ]; then
146
+ mkdir -p "openspec/specs/$capability"
147
+ elif ! cmp -s "$main_spec" "$delta_spec"; then
148
+ yellow " [DIFF] Delta spec differs from main spec before sync: $capability"
149
+ diff -u "$main_spec" "$delta_spec" >&2 || true
150
+ fi
151
+ cp "$delta_spec" "$main_spec"
152
+
153
+ step_ok "Delta spec synced: $capability openspec/specs/$capability/spec.md"
154
+ done
155
+ }
156
+
157
+ sync_delta_specs
158
+
159
+ # --- Step 5: Annotate design doc frontmatter ---
160
+
161
+ annotate_frontmatter() {
162
+ local file="$1"
163
+ local extra_fields="$2"
164
+
165
+ if [ ! -f "$file" ]; then
166
+ return 0
167
+ fi
168
+
169
+ if [ "$DRY_RUN" -eq 1 ]; then
170
+ step_dry_run "Would annotate: $file"
171
+ return 0
172
+ fi
173
+
174
+ if head -1 "$file" | grep -q '^---'; then
175
+ local tmp_file
176
+ tmp_file=$(mktemp)
177
+ awk -v archive="$ARCHIVE_NAME" -v extra="$extra_fields" '
178
+ /^archived-with:/ { next }
179
+ NR==1 && /^---/ { print; next }
180
+ /^---/ && NR>1 {
181
+ print "archived-with: " archive
182
+ if (extra != "") print extra
183
+ print; next
184
+ }
185
+ { print }
186
+ ' "$file" > "$tmp_file"
187
+ mv "$tmp_file" "$file"
188
+ else
189
+ local tmp_file
190
+ tmp_file=$(mktemp)
191
+ {
192
+ echo "---"
193
+ echo "archived-with: $ARCHIVE_NAME"
194
+ if [ -n "$extra_fields" ]; then
195
+ echo "$extra_fields"
196
+ fi
197
+ echo "status: final"
198
+ echo "---"
199
+ cat "$file"
200
+ } > "$tmp_file"
201
+ mv "$tmp_file" "$file"
202
+ fi
203
+
204
+ step_ok "Annotated: $file"
205
+ }
206
+
207
+ if [ -n "$DESIGN_DOC" ] && [ "$DESIGN_DOC" != "null" ]; then
208
+ annotate_frontmatter "$DESIGN_DOC" "status: final"
209
+ fi
210
+
211
+ # --- Step 6: Annotate plan frontmatter ---
212
+
213
+ if [ -n "$PLAN_PATH" ] && [ "$PLAN_PATH" != "null" ]; then
214
+ annotate_frontmatter "$PLAN_PATH" ""
215
+ fi
216
+
217
+ # --- Step 7: Move change to archive ---
218
+
219
+ if [ "$DRY_RUN" -eq 1 ]; then
220
+ step_dry_run "Would move: $CHANGE_DIR → $ARCHIVE_DIR"
221
+ else
222
+ mkdir -p "openspec/changes/archive"
223
+ mv "$CHANGE_DIR" "$ARCHIVE_DIR"
224
+ step_ok "Moved to: $ARCHIVE_DIR"
225
+ fi
226
+
227
+ # --- Step 8: Mark archived via comet-state transition ---
228
+
229
+ ARCHIVE_YAML="$ARCHIVE_DIR/.comet.yaml"
230
+
231
+ if [ "$DRY_RUN" -eq 1 ]; then
232
+ step_dry_run "Would set archived: true in $ARCHIVE_YAML"
233
+ else
234
+ if [ -f "$ARCHIVE_YAML" ]; then
235
+ bash "$STATE_SH" transition "$ARCHIVE_NAME" archived >/dev/null
236
+ step_ok "archived: true"
237
+ else
238
+ step_fail "archived: true (.comet.yaml not found after move)"
239
+ fi
240
+ fi
241
+
242
+ # --- Step 9: Print summary ---
243
+
244
+ echo "" >&2
245
+ if [ "$DRY_RUN" -eq 1 ]; then
246
+ yellow "Dry run complete. $STEPS_OK/$STEPS_TOTAL steps would succeed."
247
+ else
248
+ green "Archive complete. $STEPS_OK/$STEPS_TOTAL steps succeeded."
249
+ fi
250
+
251
+ if [ "$STEPS_OK" -lt "$STEPS_TOTAL" ]; then
252
+ exit 1
253
+ fi
254
+
255
+ exit 0