@gepeiyu/smart 0.1.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.
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env bash
2
+ # smart-state.sh — .smart.yaml state machine for the Smart skill.
3
+ # Usage: smart-state.sh <subcommand> <change-name> [args...]
4
+ # Subcommands: init / get / set / transition / check [--recover] / scale / task-checkoff / next
5
+ # 样板版本: v1
6
+ # v1: set -uo pipefail (no -e; scripts use explicit exit 1 + `&&` shorthand).
7
+ # v1.1 TODO: convert `A && B` to if-blocks and re-enable `set -e` for full comet parity.
8
+ set -uo pipefail
9
+
10
+ _smart_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
11
+ SMART_ENV="${SMART_ENV:-$_smart_script_dir/smart-env.sh}"
12
+ if [ ! -f "$SMART_ENV" ]; then
13
+ SMART_ENV="$(find . "$HOME"/.*/skills "$HOME/.config" "$HOME/.gemini" -path '*/scripts/smart-env.sh' -type f -print -quit 2>/dev/null)"
14
+ fi
15
+ if [ -z "${SMART_ENV:-}" ] || [ ! -f "$SMART_ENV" ]; then
16
+ echo "ERROR: smart-env.sh not found. Ensure the smart skill is installed (scripts/smart-env.sh)." >&2
17
+ exit 1
18
+ fi
19
+ . "$SMART_ENV"
20
+
21
+ # --- path helpers ---
22
+ change_dir_for() {
23
+ local n="$1"
24
+ if [ -d "openspec/changes/$n" ]; then echo "openspec/changes/$n"
25
+ elif [ -d "openspec/changes/archive/$n" ]; then echo "openspec/changes/archive/$n"
26
+ else echo "openspec/changes/$n"; fi
27
+ }
28
+ yaml_file_for() { echo "$(change_dir_for "$1")/.smart.yaml"; }
29
+
30
+ # --- project config readers (.smart/config.yaml) ---
31
+ project_context_compression() {
32
+ local value="off"
33
+ if [ -n "${SMART_CONTEXT_COMPRESSION:-}" ]; then value="$SMART_CONTEXT_COMPRESSION"
34
+ elif [ -f ".smart/config.yaml" ]; then value=$(yaml_field "context_compression" ".smart/config.yaml"); value="${value:-off}"; fi
35
+ case "$value" in off|beta) printf '%s\n' "$value" ;; *) red "ERROR: invalid context_compression: '$value' (off|beta)"; exit 1 ;; esac
36
+ }
37
+ project_auto_transition_default() {
38
+ local value="true"
39
+ if [ -n "${SMART_AUTO_TRANSITION:-}" ]; then value="$SMART_AUTO_TRANSITION"
40
+ elif [ -f ".smart/config.yaml" ]; then local raw; raw=$(yaml_field "auto_transition" ".smart/config.yaml" 2>/dev/null || true); [ -n "$raw" ] && value="$raw"; fi
41
+ case "$value" in true|false) printf '%s\n' "$value" ;; *) red "ERROR: invalid auto_transition: '$value' (true|false)"; exit 1 ;; esac
42
+ }
43
+ project_review_mode_default() {
44
+ local value="null"
45
+ if [ -n "${SMART_REVIEW_MODE:-}" ]; then value="$SMART_REVIEW_MODE"
46
+ elif [ -f ".smart/config.yaml" ]; then local raw; raw=$(yaml_field "review_mode" ".smart/config.yaml" 2>/dev/null || true); [ -n "$raw" ] && value="$raw"; fi
47
+ case "$value" in null|off|standard|thorough) printf '%s\n' "$value" ;; *) red "ERROR: invalid review_mode: '$value' (off|standard|thorough)"; exit 1 ;; esac
48
+ }
49
+
50
+ # --- cmd_init ---
51
+ cmd_init() {
52
+ local change_name="$1" workflow="$2"
53
+ local issue_number="${3:-null}" issue_repo="${4:-null}"
54
+ validate_change_name "$change_name"
55
+ validate_enum "$workflow" "full" "hotfix" "tweak"
56
+ case "$issue_number" in null|[0-9]*) ;; *) red "ERROR: issue_number must be null or a number: '$issue_number'"; exit 1 ;; esac
57
+ case "$issue_repo" in null) ;; *[!A-Za-z0-9._/-]*) red "ERROR: issue_repo invalid: '$issue_repo'"; exit 1 ;; esac
58
+
59
+ local change_dir yaml_file
60
+ change_dir=$(change_dir_for "$change_name")
61
+ yaml_file=$(yaml_file_for "$change_name")
62
+ if [ -f "$yaml_file" ]; then red "ERROR: .smart.yaml already exists at $yaml_file"; exit 1; fi
63
+ mkdir -p "$change_dir"
64
+
65
+ local build_mode isolation verify_mode tdd_mode review_mode context_compression auto_transition
66
+ context_compression=$(project_context_compression)
67
+ auto_transition=$(project_auto_transition_default)
68
+ case "$workflow" in
69
+ full) build_mode="null"; tdd_mode="null"; review_mode="$(project_review_mode_default)"; isolation="null"; verify_mode="null" ;;
70
+ hotfix|tweak) build_mode="direct"; tdd_mode="direct"; review_mode="off"; isolation="branch"; verify_mode="light" ;;
71
+ esac
72
+
73
+ local base_ref="null"
74
+ if git rev-parse --verify HEAD >/dev/null 2>&1; then base_ref=$(git rev-parse HEAD 2>/dev/null || echo null); fi
75
+
76
+ cat > "$yaml_file" <<EOF
77
+ change_name: $change_name
78
+ workflow: $workflow
79
+ phase: issue
80
+ issue_number: $issue_number
81
+ issue_repo: $issue_repo
82
+ context_compression: $context_compression
83
+ build_mode: $build_mode
84
+ build_pause: null
85
+ subagent_dispatch: null
86
+ tdd_mode: $tdd_mode
87
+ review_mode: $review_mode
88
+ isolation: $isolation
89
+ verify_mode: $verify_mode
90
+ auto_transition: $auto_transition
91
+ base_ref: $base_ref
92
+ design_doc: null
93
+ plan: null
94
+ verify_result: pending
95
+ verification_report: null
96
+ branch_status: pending
97
+ created_at: $(date -u +%Y-%m-%d)
98
+ verified_at: null
99
+ archived: false
100
+ direct_override: null
101
+ build_command: null
102
+ verify_command: null
103
+ handoff_context: null
104
+ handoff_hash: null
105
+ EOF
106
+ green "Initialized: $yaml_file (workflow=$workflow, issue_number=$issue_number)"
107
+ }
108
+
109
+ # --- cmd_get ---
110
+ cmd_get() {
111
+ local change_name="$1" field="$2"
112
+ validate_change_name "$change_name"
113
+ local yaml_file; yaml_file=$(yaml_file_for "$change_name")
114
+ [ -f "$yaml_file" ] || { red "ERROR: .smart.yaml not found at $yaml_file"; exit 1; }
115
+ local value; value=$(yaml_field "$field" "$yaml_file")
116
+ if [ "$field" = "auto_transition" ] && { [ -z "$value" ] || [ "$value" = "null" ]; }; then value="$(project_auto_transition_default)"; fi
117
+ echo "${value:-}"
118
+ }
119
+
120
+ # --- cmd_set ---
121
+ cmd_set() {
122
+ local change_name="$1" field="$2" value="$3"
123
+ validate_change_name "$change_name"
124
+ local yaml_file; yaml_file=$(yaml_file_for "$change_name")
125
+ [ -f "$yaml_file" ] || { red "ERROR: .smart.yaml not found at $yaml_file"; exit 1; }
126
+
127
+ case "$field" in
128
+ phase)
129
+ if [ "${_SMART_IN_TRANSITION:-}" != "1" ] && [ "${SMART_FORCE_PHASE:-}" != "1" ]; then
130
+ red "ERROR: Setting 'phase' directly is not allowed; it bypasses state machine evidence checks." >&2
131
+ red " Use: smart-state.sh transition <change-name> <event>" >&2
132
+ red " Repair escape hatch: SMART_FORCE_PHASE=1 smart-state.sh set <change-name> phase <value>" >&2
133
+ exit 1
134
+ fi
135
+ validate_enum "$value" "issue" "design" "build" "verify" "archive" ;;
136
+ workflow|context_compression|build_mode|build_pause|subagent_dispatch|tdd_mode|review_mode|isolation|verify_mode|auto_transition|verify_result|verification_report|branch_status|archived|design_doc|plan|verified_at|created_at|direct_override|build_command|verify_command|handoff_context|handoff_hash|base_ref|issue_number|issue_repo|change_name|verify_mode) ;;
137
+ *) red "ERROR: Unknown field: '$field'"; exit 1 ;;
138
+ esac
139
+
140
+ case "$field" in
141
+ workflow) validate_enum "$value" "full" "hotfix" "tweak" ;;
142
+ context_compression) validate_enum "$value" "off" "beta" ;;
143
+ build_mode) validate_enum "$value" "subagent-driven-development" "executing-plans" "direct" ;;
144
+ build_pause) validate_enum "$value" "null" "plan-ready" ;;
145
+ subagent_dispatch) validate_enum "$value" "null" "confirmed" ;;
146
+ tdd_mode) validate_enum "$value" "tdd" "direct" ;;
147
+ review_mode) validate_enum "$value" "off" "standard" "thorough" ;;
148
+ isolation) validate_enum "$value" "branch" "worktree" ;;
149
+ verify_mode) validate_enum "$value" "light" "full" ;;
150
+ auto_transition) validate_enum "$value" "true" "false" ;;
151
+ verify_result) validate_enum "$value" "pending" "pass" "fail" ;;
152
+ branch_status) validate_enum "$value" "pending" "handled" ;;
153
+ archived) validate_enum "$value" "true" "false" ;;
154
+ direct_override) validate_enum "$value" "true" "false" ;;
155
+ design_doc|plan|verification_report|handoff_context|handoff_hash) validate_path_field "$value" "$field" ;;
156
+ issue_number) case "$value" in null|[0-9]*) ;; *) red "ERROR: issue_number must be null or number"; exit 1 ;; esac ;;
157
+ esac
158
+
159
+ if grep -q "^${field}:" "$yaml_file"; then replace_yaml_field "$yaml_file" "$field" "$value"
160
+ else echo "${field}: ${value}" >> "$yaml_file"; fi
161
+ green "[SET] ${field}=${value}"
162
+ }
163
+
164
+ # --- transition require_* helpers ---
165
+ require_phase() {
166
+ local change_name="$1" expected="$2" actual
167
+ actual=$(cmd_get "$change_name" "phase")
168
+ [ "$actual" = "$expected" ] || { red "ERROR: Cannot transition '$change_name': expected phase $expected, got $actual"; exit 1; }
169
+ }
170
+ require_open_artifacts() {
171
+ local change_name="$1" change_dir workflow f
172
+ change_dir=$(change_dir_for "$change_name"); workflow=$(cmd_get "$change_name" "workflow")
173
+ for f in proposal.md tasks.md; do [ -s "$change_dir/$f" ] || { red "ERROR: $f must exist and be non-empty before leaving issue"; exit 1; }; done
174
+ if [ "$workflow" = "full" ] && [ ! -s "$change_dir/design.md" ]; then red "ERROR: design.md must be non-empty before leaving issue (full workflow)"; exit 1; fi
175
+ }
176
+ require_design_evidence() {
177
+ local design_doc; design_doc=$(cmd_get "$1" "design_doc")
178
+ { [ -n "$design_doc" ] && [ "$design_doc" != "null" ] && [ -s "$design_doc" ]; } || { red "ERROR: design_doc must point to an existing Design Doc before leaving design"; exit 1; }
179
+ }
180
+ require_verification_evidence() {
181
+ local report branch_status
182
+ report=$(cmd_get "$1" "verification_report"); branch_status=$(cmd_get "$1" "branch_status")
183
+ { [ -n "$report" ] && [ "$report" != "null" ] && [ -f "$report" ]; } || { red "ERROR: verification_report must point to an existing report"; exit 1; }
184
+ [ "$branch_status" = "handled" ] || { red "ERROR: branch_status must be handled"; exit 1; }
185
+ }
186
+ require_build_decisions() {
187
+ local change_name="$1" workflow build_mode isolation direct_override subagent_dispatch tdd_mode review_mode
188
+ workflow=$(cmd_get "$change_name" "workflow"); build_mode=$(cmd_get "$change_name" "build_mode")
189
+ isolation=$(cmd_get "$change_name" "isolation"); direct_override=$(cmd_get "$change_name" "direct_override" 2>/dev/null || true)
190
+ subagent_dispatch=$(cmd_get "$change_name" "subagent_dispatch" 2>/dev/null || true)
191
+ tdd_mode=$(cmd_get "$change_name" "tdd_mode" 2>/dev/null || true); review_mode=$(cmd_get "$change_name" "review_mode" 2>/dev/null || true)
192
+ case "$isolation" in branch|worktree) ;; *) red "ERROR: isolation must be branch|worktree, got '${isolation:-null}'"; exit 1 ;; esac
193
+ case "$build_mode" in subagent-driven-development|executing-plans|direct) ;; *) red "ERROR: build_mode must be selected, got '${build_mode:-null}'"; exit 1 ;; esac
194
+ if [ "$build_mode" = "direct" ] && [ "$workflow" != "hotfix" ] && [ "$workflow" != "tweak" ] && [ "$direct_override" != "true" ]; then red "ERROR: build_mode=direct only for hotfix/tweak unless direct_override=true"; exit 1; fi
195
+ if [ "$build_mode" = "subagent-driven-development" ] && [ "$subagent_dispatch" != "confirmed" ]; then red "ERROR: subagent_dispatch must be confirmed for subagent-driven-development"; exit 1; fi
196
+ if [ "$workflow" = "full" ] && { [ "$tdd_mode" = "null" ] || [ -z "$tdd_mode" ]; }; then red "ERROR: tdd_mode must be selected (full workflow)"; exit 1; fi
197
+ if [ "$workflow" = "full" ]; then case "$review_mode" in off|standard|thorough) ;; *) red "ERROR: review_mode must be off|standard|thorough (full), got '${review_mode:-null}'"; exit 1 ;; esac; fi
198
+ }
199
+
200
+ # --- cmd_transition ---
201
+ cmd_transition() {
202
+ local change_name="$1" event="$2"
203
+ local _SMART_IN_TRANSITION=1
204
+ validate_change_name "$change_name"
205
+ validate_enum "$event" "issue-complete" "design-complete" "build-complete" "verify-pass" "verify-fail" "archive-reopen" "archived"
206
+ case "$event" in
207
+ issue-complete)
208
+ require_phase "$change_name" "issue"; require_open_artifacts "$change_name"
209
+ local workflow; workflow=$(cmd_get "$change_name" "workflow")
210
+ if [ "$workflow" = "full" ]; then cmd_set "$change_name" phase design; else cmd_set "$change_name" phase build; fi ;;
211
+ design-complete)
212
+ require_phase "$change_name" "design"; require_design_evidence "$change_name"
213
+ cmd_set "$change_name" phase build ;;
214
+ build-complete)
215
+ require_phase "$change_name" "build"; require_build_decisions "$change_name"
216
+ local cur; cur=$(cmd_get "$change_name" "verify_result")
217
+ cmd_set "$change_name" phase verify; cmd_set "$change_name" verify_result pending
218
+ if [ "$cur" != "fail" ]; then cmd_set "$change_name" verification_report null; cmd_set "$change_name" branch_status pending; fi ;;
219
+ verify-pass)
220
+ require_phase "$change_name" "verify"; require_verification_evidence "$change_name"
221
+ cmd_set "$change_name" verify_result pass; cmd_set "$change_name" phase archive; cmd_set "$change_name" verified_at "$(date -u +%Y-%m-%d)" ;;
222
+ verify-fail)
223
+ require_phase "$change_name" "verify"
224
+ cmd_set "$change_name" verify_result fail; cmd_set "$change_name" phase build ;;
225
+ archive-reopen)
226
+ require_phase "$change_name" "archive"
227
+ local archived; archived=$(cmd_get "$change_name" "archived")
228
+ [ "$archived" != "true" ] || { red "ERROR: Cannot archive-reopen: already archived"; exit 1; }
229
+ cmd_set "$change_name" verify_result pending; cmd_set "$change_name" phase verify; cmd_set "$change_name" verified_at null ;;
230
+ archived)
231
+ require_phase "$change_name" "archive"
232
+ local vr; vr=$(cmd_get "$change_name" "verify_result")
233
+ [ "$vr" = "pass" ] || { red "ERROR: verify_result must be pass before archiving"; exit 1; }
234
+ cmd_set "$change_name" archived true ;;
235
+ esac
236
+ green "[TRANSITION] ${event}"
237
+ }
238
+
239
+ # --- cmd_check ---
240
+ CHECK_BLOCK=0
241
+ check_pass() { printf ' \033[32m[PASS]\033[0m %s\n' "$1" >&2; }
242
+ check_fail() { printf ' \033[31m[FAIL]\033[0m %s\n' "$1" >&2; CHECK_BLOCK=1; }
243
+ check_nonempty() { file_nonempty "$2" && check_pass "$1 non-empty" || check_fail "$1 missing or empty"; }
244
+ check_yaml_is() { local a; a=$(cmd_get "$3" "$1"); [ "$a" = "$2" ] && check_pass "$1=$a (expected $2)" || check_fail "$1=$a (expected $2)"; }
245
+ check_yaml_empty() { local v; v=$(cmd_get "$2" "$1"); { [ -z "$v" ] || [ "$v" = "null" ]; } && check_pass "$1 empty/null" || check_fail "$1=$v (expected empty/null)"; }
246
+
247
+ cmd_check() {
248
+ local change_name="$1" phase="$2"
249
+ validate_change_name "$change_name"; validate_enum "$phase" "issue" "design" "build" "verify" "archive"
250
+ local change_dir; change_dir=$(change_dir_for "$change_name")
251
+ local yaml_file="$change_dir/.smart.yaml"
252
+ echo "=== Entry Check: smart-$phase ===" >&2
253
+ [ -f "$yaml_file" ] || { red "ERROR: .smart.yaml not found at $yaml_file"; exit 1; }
254
+ case "$phase" in
255
+ issue) check_pass ".smart.yaml exists"; check_yaml_is "phase" "issue" "$change_name" ;;
256
+ design) check_pass ".smart.yaml exists"; check_yaml_is "phase" "design" "$change_name"; check_yaml_is "workflow" "full" "$change_name"; check_yaml_empty "design_doc" "$change_name"; check_nonempty "proposal.md" "$change_dir/proposal.md"; check_nonempty "design.md" "$change_dir/design.md"; check_nonempty "tasks.md" "$change_dir/tasks.md" ;;
257
+ build) check_pass ".smart.yaml exists"; check_yaml_is "phase" "build" "$change_name";
258
+ local workflow; workflow=$(cmd_get "$change_name" "workflow")
259
+ if [ "$workflow" = "full" ]; then local dd; dd=$(cmd_get "$change_name" "design_doc"); { [ -n "$dd" ] && [ "$dd" != "null" ] && [ -f "$dd" ]; } && check_pass "design_doc=$dd (exists)" || check_fail "design_doc=$dd (expected non-null, file exists)"; else check_pass "workflow=$workflow (design_doc not required)"; fi
260
+ check_nonempty "proposal.md" "$change_dir/proposal.md"; check_nonempty "tasks.md" "$change_dir/tasks.md" ;;
261
+ verify) check_pass ".smart.yaml exists"; check_yaml_is "phase" "verify" "$change_name";
262
+ local vr; vr=$(cmd_get "$change_name" "verify_result"); { [ "$vr" = "pending" ] || [ -z "$vr" ] || [ "$vr" = "null" ]; } && check_pass "verify_result=$vr" || check_fail "verify_result=$vr (expected pending/null)" ;;
263
+ archive) check_pass ".smart.yaml exists"; check_yaml_is "phase" "archive" "$change_name"; check_yaml_is "verify_result" "pass" "$change_name";
264
+ local ar; ar=$(cmd_get "$change_name" "archived"); [ "$ar" != "true" ] && check_pass "archived=$ar (not true)" || check_fail "archived=$ar (expected not true)" ;;
265
+ *) red "ERROR: Unknown phase: $phase"; exit 1 ;;
266
+ esac
267
+ echo "" >&2
268
+ if [ "$CHECK_BLOCK" -eq 1 ]; then red "BLOCKED — fix failing checks before proceeding"; exit 1; fi
269
+ green "ALL CHECKS PASSED — ready to proceed"
270
+ }
271
+
272
+ # --- cmd_scale ---
273
+ cmd_scale() {
274
+ local change_name="$1"
275
+ validate_change_name "$change_name"
276
+ local change_dir; change_dir=$(change_dir_for "$change_name")
277
+ local yaml_file="$change_dir/.smart.yaml"
278
+ [ -f "$yaml_file" ] || { red "ERROR: .smart.yaml not found at $yaml_file"; exit 1; }
279
+ local tasks_file="$change_dir/tasks.md" task_count=0 delta_spec_count=0 changed_files=0
280
+ [ -f "$tasks_file" ] && task_count=$(grep -c '^\- \[' "$tasks_file" 2>/dev/null || echo 0)
281
+ [ -d "$change_dir/specs" ] && delta_spec_count=$(find "$change_dir/specs" -name spec.md -type f 2>/dev/null | wc -l | tr -d ' ')
282
+ if git rev-parse --git-dir >/dev/null 2>&1; then
283
+ local plan_file base_ref=""
284
+ plan_file=$(cmd_get "$change_name" "plan" 2>/dev/null || true)
285
+ if [ -n "$plan_file" ] && [ "$plan_file" != "null" ] && [ -f "$plan_file" ]; then base_ref=$(grep '^base-ref:' "$plan_file" 2>/dev/null | head -1 | sed 's/^base-ref: *//' || true); fi
286
+ if [ -z "$base_ref" ] || [ "$base_ref" = "null" ]; then base_ref=$(cmd_get "$change_name" "base_ref" 2>/dev/null || true); fi
287
+ if [ -n "${base_ref:-}" ] && [ "$base_ref" != "null" ] && git rev-parse --verify "$base_ref" >/dev/null 2>&1; then changed_files=$(git diff --name-only "$base_ref"...HEAD 2>/dev/null | wc -l | tr -d ' ')
288
+ else changed_files=$(git diff --name-only HEAD 2>/dev/null | wc -l | tr -d ' '); fi
289
+ fi
290
+ local result="light"
291
+ if [ "$task_count" -gt 3 ] || [ "$delta_spec_count" -gt 1 ] || [ "$changed_files" -gt 4 ]; then result="full"; fi
292
+ echo "=== Scale: $change_name ===" >&2
293
+ echo " Tasks: $task_count (thr 3); Delta specs: $delta_spec_count (thr 1); Changed files: $changed_files (thr 4) -> $result" >&2
294
+ replace_yaml_field "$yaml_file" "verify_mode" "$result"
295
+ green "[SCALE] verify_mode=$result"
296
+ }
297
+
298
+ # --- cmd_task_checkoff ---
299
+ cmd_task_checkoff() {
300
+ local task_file="$1" task_text="$2"
301
+ validate_path_field "$task_file" "task file"
302
+ [ -n "$task_text" ] || { red "ERROR: Task text cannot be empty"; exit 1; }
303
+ [ -f "$task_file" ] || { red "ERROR: Task file not found: $task_file"; exit 1; }
304
+ local counts; counts=$(TASK_TEXT="$task_text" awk 'BEGIN{task=ENVIRON["TASK_TEXT"]} {sub(/\r$/,""); if($0=="- [ ] "task||$0=="- [x] "task||$0=="- [X] "task)total++; if($0=="- [x] "task||$0=="- [X] "task)checked++} END{printf "%d %d\n",total+0,checked+0}' "$task_file")
305
+ local total="${counts%% *}" checked="${counts##* }"
306
+ [ "$total" -eq 1 ] || { red "ERROR: task text must appear exactly once (found $total): $task_text"; exit 1; }
307
+ [ "$checked" -eq 1 ] || { red "ERROR: task not checked: $task_text"; exit 1; }
308
+ echo "TASK_CHECKOFF: PASS"; echo "FILE: $task_file"; echo "TASK: $task_text"
309
+ }
310
+
311
+ # --- cmd_next (single-skill: SKILL=smart, PHASE=current) ---
312
+ cmd_next() {
313
+ local change_name="$1"
314
+ validate_change_name "$change_name"
315
+ local yaml_file; yaml_file=$(yaml_file_for "$change_name")
316
+ [ -f "$yaml_file" ] || { red "ERROR: .smart.yaml not found at $yaml_file"; exit 1; }
317
+ local phase workflow auto_transition archived skill
318
+ phase=$(cmd_get "$change_name" "phase" 2>/dev/null || true)
319
+ workflow=$(cmd_get "$change_name" "workflow" 2>/dev/null || true)
320
+ auto_transition=$(cmd_get "$change_name" "auto_transition" 2>/dev/null || true)
321
+ archived=$(cmd_get "$change_name" "archived" 2>/dev/null || true)
322
+ if [ -z "$auto_transition" ] || [ "$auto_transition" = "null" ]; then auto_transition="$(project_auto_transition_default)"; fi
323
+ if [ "$archived" = "true" ]; then echo "NEXT: done"; return 0; fi
324
+ case "$phase" in
325
+ issue) skill="smart-issue" ;;
326
+ design) skill="smart-design" ;;
327
+ build) case "$workflow" in hotfix) skill="smart-hotfix" ;; tweak) skill="smart-tweak" ;; *) skill="smart-build" ;; esac ;;
328
+ verify) skill="smart-verify" ;;
329
+ archive) skill="smart-archive" ;;
330
+ *) red "ERROR: unknown phase: ${phase:-null}"; exit 1 ;;
331
+ esac
332
+ if [ "$auto_transition" = "false" ]; then
333
+ echo "NEXT: manual"; echo "SKILL: $skill"; echo "HINT: phase is '$phase'; run /$skill manually to continue"
334
+ else
335
+ echo "NEXT: auto"; echo "SKILL: $skill"
336
+ fi
337
+ }
338
+
339
+ # --- main ---
340
+ SUBCOMMAND="${1:-}"; shift || true
341
+ case "$SUBCOMMAND" in
342
+ init) [ $# -lt 2 ] && { red "Usage: smart-state.sh init <name> <full|hotfix|tweak> [issue_number] [issue_repo]"; exit 1; }; cmd_init "$@" ;;
343
+ get) [ $# -lt 2 ] && { red "Usage: smart-state.sh get <name> <field>"; exit 1; }; cmd_get "$@" ;;
344
+ set) [ $# -lt 3 ] && { red "Usage: smart-state.sh set <name> <field> <value>"; exit 1; }; cmd_set "$@" ;;
345
+ transition) [ $# -lt 2 ] && { red "Usage: smart-state.sh transition <name> <event>"; red "Events: issue-complete, design-complete, build-complete, verify-pass, verify-fail, archive-reopen, archived"; exit 1; }; cmd_transition "$@" ;;
346
+ check) [ $# -lt 2 ] && { red "Usage: smart-state.sh check <name> <phase> [--recover]"; exit 1; }; cmd_check "$@" ;;
347
+ scale) [ $# -lt 1 ] && { red "Usage: smart-state.sh scale <name>"; exit 1; }; cmd_scale "$@" ;;
348
+ task-checkoff) [ $# -lt 2 ] && { red "Usage: smart-state.sh task-checkoff <file> <task-text>"; exit 1; }; cmd_task_checkoff "$@" ;;
349
+ next) [ $# -lt 1 ] && { red "Usage: smart-state.sh next <name>"; exit 1; }; cmd_next "$@" ;;
350
+ -h|--help|help)
351
+ cat <<'EOF'
352
+ Usage: smart-state.sh <subcommand> <change-name> [args...]
353
+ Subcommands:
354
+ init <name> <full|hotfix|tweak> [issue_number] [issue_repo]
355
+ get <name> <field>
356
+ set <name> <field> <value>
357
+ transition <name> <event> (issue-complete|design-complete|build-complete|verify-pass|verify-fail|archive-reopen|archived)
358
+ check <name> <phase> (issue|design|build|verify|archive)
359
+ scale <name>
360
+ task-checkoff <file> <task-text>
361
+ next <name>
362
+ EOF
363
+ ;;
364
+ *) red "Unknown subcommand: $SUBCOMMAND"; exit 1 ;;
365
+ esac
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: smart-archive
3
+ description: "Smart 阶段 5:归档。用 /smart-archive 调用。按 OpenSpec delta 语义合并主 spec,归档 change。"
4
+ ---
5
+
6
+ # Smart 阶段 5:归档(Archive)
7
+
8
+ ## 前置条件
9
+
10
+ - 验证已通过(阶段 4 完成)
11
+ - 分支已处理
12
+ - `openspec/changes/<name>/.smart.yaml` 中 `verify_result: pass`
13
+
14
+ ## 步骤
15
+
16
+ ### 0. 输出语言约束
17
+
18
+ 归档摘要和生命周期闭环说明必须使用触发本次工作流的用户请求语言。
19
+
20
+ ### 0b. 入口状态验证(Entry Check)
21
+
22
+ 执行入口验证:
23
+
24
+ ```bash
25
+ SMART_ENV="${SMART_ENV:-$(find . "$HOME"/.*/skills "$HOME/.config" "$HOME/.gemini" -path '*/smart/scripts/smart-env.sh' -type f -print -quit 2>/dev/null)}"
26
+ if [ -z "$SMART_ENV" ]; then
27
+ echo "ERROR: smart-env.sh not found. Ensure the smart skill is installed." >&2
28
+ return 1
29
+ fi
30
+ . "$SMART_ENV"
31
+ "$SMART_BASH" "$SMART_STATE" check <name> archive
32
+ ```
33
+
34
+ 验证通过后继续 Step 1。验证失败时脚本会输出具体失败原因。
35
+
36
+ ### 1. 归档前最终确认(阻塞点)
37
+
38
+ 入口验证通过后,**必须按 `smart/reference/decision-point.md` 的协议暂停并等待用户确认是否立即归档**。不得在用户确认前运行 `"$SMART_BASH" "$SMART_ARCHIVE" "<change-name>"`。
39
+
40
+ 确认前必须向用户展示简短摘要:
41
+ - change 名称
42
+ - 验证报告路径和结论
43
+ - 分支处理状态
44
+ - 本次归档将执行的不可逆动作:按 OpenSpec delta 语义合并主 spec、标注 design doc / plan、移动 change 到 archive 目录
45
+
46
+ 用户确认问题必须以单选题形式呈现,包含以下选项:
47
+ - 「确认归档」— 立即执行归档脚本,完成 spec 合并和 change 移动
48
+ - 「需要调整或重新验证」— 不执行归档;运行 `"$SMART_BASH" "$SMART_STATE" transition <change-name> archive-reopen` 回到 `phase: verify`,再调用 `/smart-verify`。若验证阶段确认需要修复,再按 `/smart-verify` 的验证失败决策回到 `/smart-build`
49
+ - 「暂不归档」— 不执行归档,保留当前 `phase: archive` 状态,等待用户稍后再次调用 `/smart-archive`
50
+
51
+ 只有用户选择「确认归档」后,才允许继续 Step 2。用户选择「需要调整或重新验证」后,必须先执行 `archive-reopen` 状态回退,不得手动编辑 `.smart.yaml`。
52
+
53
+ ### 2. 执行归档
54
+
55
+ 运行归档脚本,自动完成以下全部步骤:
56
+
57
+ ```bash
58
+ "$SMART_BASH" "$SMART_ARCHIVE" "<change-name>"
59
+ ```
60
+
61
+ 脚本自动执行:
62
+ 1. 入口状态验证(phase=archive, verify_result=pass, archived=false)
63
+ 2. Design doc 前置元数据标注(archived-with, status)
64
+ 3. Plan 前置元数据标注(archived-with)
65
+ 4. 调用 OpenSpec archive 按 delta 语义合并主 spec 并移动 change 到归档目录
66
+ 5. 校验主 spec 未残留 delta-only section 标题
67
+ 6. 通过 `smart-state transition <archive-name> archived` 更新 `archived: true`
68
+
69
+ 如脚本返回非零退出码,报告错误并停止。
70
+ 如脚本返回零退出码,归档完成。
71
+ 脚本摘要中的 `X/Y steps succeeded` 以真实执行步骤计数,不会因 delta spec 同步或文档标注重复累计。
72
+
73
+ 脚本会调用 OpenSpec 归档能力按 `ADDED/MODIFIED/REMOVED/RENAMED` 语义合并主 spec,并在归档后校验主 spec 中没有残留 delta-only section 标题。
74
+
75
+ 如需预览而不实际执行,使用 `--dry-run` 参数。
76
+
77
+ Smart 扩展:归档脚本 smart-archive.sh 在合并主 spec、移动 change、设置 archived=true 后,若 issue_number 非 null,会执行 `gh issue close <n>`(issue_repo 已设时带 `--repo`)—— Issue 关闭即 change 归档。
78
+
79
+ ### 3. 生命周期闭环
80
+
81
+ Spec 生命周期在此完成:
82
+ ```
83
+ brainstorming → delta spec → 实施 → 验证 → 主 spec 合并 → design doc 标注 → 归档
84
+ ```
85
+
86
+ ## 退出条件
87
+
88
+ - 归档脚本执行成功(退出码 0)
89
+ - 归档目录 `openspec/changes/archive/YYYY-MM-DD-<change-name>/` 存在
90
+ - 归档后的 `.smart.yaml` 中 `archived: true`
91
+
92
+ 归档脚本会把 `openspec/changes/<name>/` 移动到 `openspec/changes/archive/YYYY-MM-DD-<name>/`。
93
+
94
+ > **WARNING**: 归档成功后**不要再对原 change 名运行** `"$SMART_BASH" "$SMART_GUARD" <change-name> archive`,因为原活跃目录已经不存在。误调会导致 guard 报错"change directory not found"。归档完整性以脚本退出码和归档目录状态为准。
95
+
96
+ ## 完成
97
+
98
+ Smart 流程全部完成。如需开始新工作,调用 `/smart` 或 `/smart-issue`。
99
+
100
+ ## 上下文压缩恢复
101
+
102
+ 按 `smart/reference/context-recovery.md` 执行,phase 参数为 `archive`。若 `archived: true` 且归档目录存在,归档已完成,无需再次执行归档操作。