@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,138 @@
1
+ #!/usr/bin/env bash
2
+ # smart-archive.sh — one-shot archive for the Smart skill.
3
+ # Usage: smart-archive.sh <change-name> [--dry-run]
4
+ # Steps: validate entry -> openspec archive (merge delta, move change) ->
5
+ # annotate design_doc/plan frontmatter -> smart-state transition archived ->
6
+ # gh issue close (if issue_number set) -> verify main specs clean.
7
+ # 样板版本: v1
8
+ # v1: set -uo pipefail (no -e; explicit exit 1 + `&&` shorthand). v1.1: if-blocks + set -e.
9
+ set -uo pipefail
10
+
11
+ _smart_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
12
+ SMART_ENV="${SMART_ENV:-$_smart_script_dir/smart-env.sh}"
13
+ if [ ! -f "$SMART_ENV" ]; then
14
+ SMART_ENV="$(find . "$HOME"/.*/skills "$HOME/.config" "$HOME/.gemini" -path '*/scripts/smart-env.sh' -type f -print -quit 2>/dev/null)"
15
+ fi
16
+ if [ -z "${SMART_ENV:-}" ] || [ ! -f "$SMART_ENV" ]; then
17
+ echo "ERROR: smart-env.sh not found. Ensure the smart skill is installed (scripts/smart-env.sh)." >&2
18
+ exit 1
19
+ fi
20
+ . "$SMART_ENV"
21
+
22
+ SMART_OPENSPEC="${SMART_OPENSPEC:-openspec}"
23
+ DRY_RUN=0
24
+ [[ "${2:-}" == "--dry-run" ]] && DRY_RUN=1
25
+
26
+ CHANGE="$1"
27
+ validate_change_name "$CHANGE"
28
+
29
+ CHANGE_DIR="openspec/changes/$CHANGE"
30
+ YAML="$CHANGE_DIR/.smart.yaml"
31
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
32
+ STATE_SH="$SCRIPT_DIR/smart-state.sh"
33
+ TODAY=$(date -u +%Y-%m-%d)
34
+ ARCHIVE_NAME="${TODAY}-${CHANGE}"
35
+ ARCHIVE_DIR="openspec/changes/archive/${ARCHIVE_NAME}"
36
+
37
+ STEPS_OK=0; STEPS_TOTAL=0
38
+ step_ok() { green " [OK] $1"; STEPS_OK=$((STEPS_OK+1)); STEPS_TOTAL=$((STEPS_TOTAL+1)); }
39
+ step_fail() { red " [FAIL] $1"; STEPS_TOTAL=$((STEPS_TOTAL+1)); }
40
+ step_dry() { yellow " [DRY-RUN] $1"; STEPS_OK=$((STEPS_OK+1)); STEPS_TOTAL=$((STEPS_TOTAL+1)); }
41
+
42
+ echo "=== Smart Archive: $CHANGE ===" >&2
43
+
44
+ [ -f "$YAML" ] || { red "FATAL: .smart.yaml not found at $YAML"; exit 1; }
45
+
46
+ DESIGN_DOC=$(yaml_field "design_doc" "$YAML")
47
+ PLAN_PATH=$(yaml_field "plan" "$YAML")
48
+ ISSUE_NUMBER=$(yaml_field "issue_number" "$YAML")
49
+ ISSUE_REPO=$(yaml_field "issue_repo" "$YAML")
50
+ PHASE_VAL=$(yaml_field "phase" "$YAML")
51
+ VERIFY_VAL=$(yaml_field "verify_result" "$YAML")
52
+ ARCHIVED_VAL=$(yaml_field "archived" "$YAML")
53
+
54
+ [ "$PHASE_VAL" = "archive" ] || { red "FATAL: phase is '$PHASE_VAL', expected 'archive'"; exit 1; }
55
+ [ "$VERIFY_VAL" = "pass" ] || { red "FATAL: verify_result is '$VERIFY_VAL', expected 'pass'. Run smart verify first."; exit 1; }
56
+ [ "$ARCHIVED_VAL" = "true" ] && { red "FATAL: change already archived"; exit 1; }
57
+ step_ok "Entry state verified"
58
+
59
+ if [ -d "$ARCHIVE_DIR" ]; then red "FATAL: archive target already exists: $ARCHIVE_DIR"; exit 1; fi
60
+ step_ok "Archive target available"
61
+
62
+ # --- annotate frontmatter ---
63
+ annotate_frontmatter() {
64
+ local file="$1" extra="$2"
65
+ [ -f "$file" ] || return 0
66
+ if [ "$DRY_RUN" -eq 1 ]; then step_dry "Would annotate: $file"; return 0; fi
67
+ if head -1 "$file" | grep -q '^---'; then
68
+ local tmp; tmp=$(mktemp); chmod 600 "$tmp"
69
+ awk -v archive="$ARCHIVE_NAME" -v extra="$extra" '/^archived-with:/{next} NR==1 && /^---/{print;next} /^---/ && NR>1 {print "archived-with: " archive; if(extra!="") print extra; print; next} {print}' "$file" > "$tmp"; mv "$tmp" "$file"
70
+ else
71
+ local tmp; tmp=$(mktemp); chmod 600 "$tmp"
72
+ { echo "---"; echo "archived-with: $ARCHIVE_NAME"; [ -n "$extra" ] && echo "$extra"; echo "status: final"; echo "---"; cat "$file"; } > "$tmp"; mv "$tmp" "$file"
73
+ fi
74
+ step_ok "Annotated: $file"
75
+ }
76
+
77
+ # --- openspec archive ---
78
+ verify_main_specs_clean() {
79
+ [ -d "openspec/specs" ] || return 0
80
+ local found=0 matches
81
+ for spec_file in openspec/specs/*/spec.md; do
82
+ [ -f "$spec_file" ] || continue
83
+ matches=$(grep -nE '^## (ADDED|MODIFIED|REMOVED|RENAMED) Requirements$' "$spec_file" 2>/dev/null || true)
84
+ if [ -n "$matches" ]; then red "FATAL: delta-only heading leaked into main spec: $spec_file"; printf '%s\n' "$matches" >&2; found=1; fi
85
+ done
86
+ [ "$found" -eq 0 ]
87
+ }
88
+ resolve_archive_dir() {
89
+ [ -d "$ARCHIVE_DIR" ] && return 0
90
+ local found; found=$(find "openspec/changes/archive" -maxdepth 1 -mindepth 1 -type d -name "*-$CHANGE" 2>/dev/null | head -1 || true)
91
+ [ -n "$found" ] && { ARCHIVE_DIR="$found"; ARCHIVE_NAME=$(basename "$found"); return 0; }
92
+ return 1
93
+ }
94
+
95
+ if [ "$DRY_RUN" -eq 1 ]; then
96
+ step_dry "Would run OpenSpec archive: $CHANGE"
97
+ else
98
+ if ! command -v "$SMART_OPENSPEC" >/dev/null 2>&1; then
99
+ if command -v npx >/dev/null 2>&1; then SMART_OPENSPEC="npx --no-install openspec"; else red "FATAL: openspec CLI not found"; exit 1; fi
100
+ fi
101
+ "$SMART_OPENSPEC" archive "$CHANGE" --yes >&2
102
+ if ! resolve_archive_dir; then step_fail "OpenSpec archive output not found"; exit 1; else step_ok "OpenSpec archive completed: $ARCHIVE_DIR"; fi
103
+ verify_main_specs_clean && step_ok "Main specs verified clean" || step_fail "Main specs clean check"
104
+ fi
105
+
106
+ # --- annotate design doc + plan ---
107
+ if [ -n "$DESIGN_DOC" ] && [ "$DESIGN_DOC" != "null" ]; then annotate_frontmatter "$DESIGN_DOC" "status: final"; fi
108
+ if [ -n "$PLAN_PATH" ] && [ "$PLAN_PATH" != "null" ]; then annotate_frontmatter "$PLAN_PATH" ""; fi
109
+
110
+ # --- mark archived via smart-state transition ---
111
+ ARCHIVE_YAML="$ARCHIVE_DIR/.smart.yaml"
112
+ if [ "$DRY_RUN" -eq 1 ]; then
113
+ step_dry "Would set archived: true in $ARCHIVE_YAML"
114
+ else
115
+ if [ -f "$ARCHIVE_YAML" ]; then
116
+ "$SMART_BASH" "$STATE_SH" transition "$ARCHIVE_NAME" archived >/dev/null && step_ok "archived: true" || step_fail "archived: true"
117
+ else step_fail "archived: true (.smart.yaml not found after move)"; fi
118
+ fi
119
+
120
+ # --- close the GitHub Issue (Smart extension) ---
121
+ if [ "$ISSUE_NUMBER" != "null" ] && [ -n "$ISSUE_NUMBER" ]; then
122
+ if [ "$DRY_RUN" -eq 1 ]; then
123
+ step_dry "Would run: gh issue close $ISSUE_NUMBER${ISSUE_REPO:+ --repo $ISSUE_REPO}"
124
+ else
125
+ if [ "$ISSUE_REPO" != "null" ] && [ -n "$ISSUE_REPO" ]; then
126
+ gh issue close "$ISSUE_NUMBER" --repo "$ISSUE_REPO" && step_ok "closed issue #$ISSUE_NUMBER ($ISSUE_REPO)" || step_fail "gh issue close #$ISSUE_NUMBER"
127
+ else
128
+ gh issue close "$ISSUE_NUMBER" && step_ok "closed issue #$ISSUE_NUMBER" || step_fail "gh issue close #$ISSUE_NUMBER"
129
+ fi
130
+ fi
131
+ fi
132
+
133
+ # --- summary ---
134
+ echo "" >&2
135
+ if [ "$DRY_RUN" -eq 1 ]; then yellow "Dry run complete. $STEPS_OK/$STEPS_TOTAL steps would succeed."
136
+ else green "Archive complete. $STEPS_OK/$STEPS_TOTAL steps succeeded."; fi
137
+ if [ "$STEPS_OK" -lt "$STEPS_TOTAL" ]; then exit 1; fi
138
+ exit 0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bash
2
+ # smart-env.sh — Smart skill script locator, bash resolver, and shared helpers.
3
+ # Sourced by other smart-*.sh scripts and by the agent (per SKILL.md).
4
+ # Sets: SMART_BASH, SMART_STATE, SMART_GUARD, SMART_HANDOFF, SMART_ARCHIVE,
5
+ # SMART_OPENSPEC_ROOT, SMART_CHANGES_DIR, SMART_DIR, SMART_SCRIPTS_DIR.
6
+ # Provides shared helpers: colors, validate_change_name, validate_enum,
7
+ # validate_path_field, yaml_field, replace_yaml_field, strip_inline_comment,
8
+ # strip_wrapping_quotes, hash_stream, hash_file, file_nonempty.
9
+ # 样板版本: v1 (keep in sync across all smart-*.sh and SKILL.md)
10
+
11
+ # --- locate this file ---
12
+ if [ -z "${SMART_ENV:-}" ]; then
13
+ if [ -n "${BASH_SOURCE[1]:-}" ]; then SMART_ENV="${BASH_SOURCE[1]}"
14
+ elif [ -n "${BASH_SOURCE[0]:-}" ]; then SMART_ENV="${BASH_SOURCE[0]}"
15
+ else SMART_ENV="$(find . "$HOME"/.*/skills "$HOME/.config" "$HOME/.gemini" -path '*/scripts/smart-env.sh' -type f -print -quit 2>/dev/null)"; fi
16
+ fi
17
+ if [ -z "${SMART_ENV:-}" ] || [ ! -f "$SMART_ENV" ]; then
18
+ echo "ERROR: smart-env.sh not found. Ensure the smart skill is installed (scripts/smart-env.sh)." >&2
19
+ echo "Expected path pattern: */scripts/smart-env.sh under project root or platform skill directories." >&2
20
+ return 1 2>/dev/null || exit 1
21
+ fi
22
+
23
+ SMART_SCRIPTS_DIR="$(cd "$(dirname "$SMART_ENV")" && pwd -P)"
24
+ SMART_DIR="$(cd "$SMART_SCRIPTS_DIR/.." && pwd -P)"
25
+
26
+ export SMART_STATE="${SMART_STATE:-$SMART_SCRIPTS_DIR/smart-state.sh}"
27
+ export SMART_GUARD="${SMART_GUARD:-$SMART_SCRIPTS_DIR/smart-guard.sh}"
28
+ export SMART_HANDOFF="${SMART_HANDOFF:-$SMART_SCRIPTS_DIR/smart-handoff.sh}"
29
+ export SMART_ARCHIVE="${SMART_ARCHIVE:-$SMART_SCRIPTS_DIR/smart-archive.sh}"
30
+ export SMART_OPENSPEC_ROOT="${SMART_OPENSPEC_ROOT:-openspec}"
31
+ export SMART_CHANGES_DIR="${SMART_CHANGES_DIR:-$SMART_OPENSPEC_ROOT/changes}"
32
+ export SMART_DIR
33
+ export SMART_SCRIPTS_DIR
34
+
35
+ # --- bash resolver (port of comet-env.sh; avoids Windows WSL bash.exe) ---
36
+ _smart_bash_is_usable() {
37
+ local cand="$1"
38
+ [ -n "$cand" ] || return 1
39
+ case "$cand" in
40
+ */Windows/System32/bash.exe|*/windows/system32/bash.exe|*\\Windows\\System32\\bash.exe|*\\windows\\system32\\bash.exe) return 1 ;;
41
+ esac
42
+ "$cand" -lc 'printf smart-bash-ok' >/dev/null 2>&1
43
+ }
44
+ _smart_resolve_bash() {
45
+ local cand
46
+ if _smart_bash_is_usable "${SMART_BASH:-}"; then printf '%s\n' "$SMART_BASH"; return 0; fi
47
+ if _smart_bash_is_usable "${BASH:-}"; then printf '%s\n' "$BASH"; return 0; fi
48
+ cand="$(command -v sh 2>/dev/null | awk '{ sub(/\/sh(\.exe)?$/, "/bash.exe"); print }')"
49
+ if _smart_bash_is_usable "$cand"; then printf '%s\n' "$cand"; return 0; fi
50
+ cand="$(command -v bash 2>/dev/null || true)"
51
+ if _smart_bash_is_usable "$cand"; then printf '%s\n' "$cand"; return 0; fi
52
+ return 1
53
+ }
54
+ SMART_BASH="$(_smart_resolve_bash || true)"
55
+ export SMART_BASH
56
+
57
+ _smart_env_missing=0
58
+ [ -n "$SMART_BASH" ] || { echo "ERROR: usable bash not found. Install Git Bash or set SMART_BASH. Windows WSL launcher bash.exe is not supported." >&2; _smart_env_missing=1; }
59
+ for _s in "$SMART_STATE" "$SMART_GUARD" "$SMART_HANDOFF" "$SMART_ARCHIVE"; do
60
+ [ -f "$_s" ] || { echo "WARN: expected script not found: $_s" >&2; }
61
+ done
62
+ unset _s _smart_env_missing
63
+ unset -f _smart_bash_is_usable _smart_resolve_bash
64
+
65
+ # --- shared color helpers ---
66
+ red() { printf '\033[31m%s\033[0m\n' "$1" >&2; }
67
+ green() { printf '\033[32m%s\033[0m\n' "$1" >&2; }
68
+ yellow() { printf '\033[33m%s\033[0m\n' "$1" >&2; }
69
+ warn() { printf '\033[33m%s\033[0m\n' "$1" >&2; }
70
+
71
+ # --- shared input validation (port of comet-state.sh) ---
72
+ validate_change_name() {
73
+ local name="$1"
74
+ [ -n "$name" ] || { red "ERROR: Change name cannot be empty"; exit 1; }
75
+ [[ "$name" =~ ^[a-zA-Z0-9_-]+$ ]] || { red "ERROR: Invalid change name: '$name' (valid: a-z A-Z 0-9 - _)"; exit 1; }
76
+ [[ "$name" =~ \.\. ]] && { red "ERROR: Change name cannot contain '..' (path traversal not allowed)"; exit 1; }
77
+ }
78
+ validate_enum() {
79
+ local value="$1"; shift
80
+ local valid
81
+ for valid in "$@"; do [ "$value" = "$valid" ] && return 0; done
82
+ red "ERROR: Invalid value: '$value' (valid: $*)"; exit 1
83
+ }
84
+ validate_path_field() {
85
+ local value="$1" field="$2"
86
+ [ -z "$value" ] || [ "$value" = "null" ] && return 0
87
+ case "$value" in
88
+ /*|~*|[A-Za-z]:*|\\*) red "ERROR: $field must be a relative path within the repo: '$value'"; exit 1 ;;
89
+ esac
90
+ [[ "$value" =~ \.\. ]] && { red "ERROR: $field cannot contain '..' (path traversal): '$value'"; exit 1; }
91
+ }
92
+
93
+ # --- shared flat-YAML helpers (port of comet-state.sh; .smart.yaml is key: value) ---
94
+ strip_inline_comment() {
95
+ local value="$1"
96
+ printf '%s\n' "$value" | awk -v squote="'" '
97
+ { out=""; quote=""
98
+ for (i=1; i<=length($0); i++) { c=substr($0,i,1)
99
+ if (quote=="") { if (c=="\""||c==squote) { quote=c } else if (c=="#" && (i==1||substr($0,i-1,1)~/[[:space:]]/)) { sub(/[[:space:]]+$/,"",out); print out; next } }
100
+ else if (c==quote) { quote="" }
101
+ out=out c }
102
+ print out }'
103
+ }
104
+ strip_wrapping_quotes() {
105
+ local value="$1"
106
+ case "$value" in
107
+ \"*\") printf '%s\n' "${value:1:${#value}-2}" ;;
108
+ \'*\") printf '%s\n' "${value:1:${#value}-2}" ;;
109
+ *) printf '%s\n' "$value" ;;
110
+ esac
111
+ }
112
+ # yaml_field <field> <yaml_file> -> prints value (empty if missing)
113
+ yaml_field() {
114
+ local field="$1" yaml_file="$2"
115
+ [ -f "$yaml_file" ] || return 0
116
+ local value
117
+ value=$(grep "^${field}:" "$yaml_file" 2>/dev/null | sed "s/^${field}: *//" || true)
118
+ value=$(strip_inline_comment "$value")
119
+ strip_wrapping_quotes "$value"
120
+ }
121
+ # replace_yaml_field <yaml_file> <field> <value> -> read-modify-write, dedup keeping last
122
+ replace_yaml_field() {
123
+ local yaml_file="$1" field="$2" value="$3" tmp_file
124
+ tmp_file=$(mktemp); chmod 600 "$tmp_file"
125
+ awk -v field="$field" -v value="$value" '
126
+ index($0, field ":") == 1 { $0 = field ": " value }
127
+ { buf[NR]=$0; keys[NR]=$0; sub(/:.*$/,"",keys[NR]); n=NR }
128
+ END { for (i=1;i<=n;i++) last[keys[i]]=i; for (i=1;i<=n;i++) if (last[keys[i]]==i) print buf[i] }
129
+ ' "$yaml_file" > "$tmp_file"
130
+ mv "$tmp_file" "$yaml_file"
131
+ }
132
+
133
+ # --- shared hash/file helpers ---
134
+ hash_stream() {
135
+ if command -v sha256sum >/dev/null 2>&1; then sha256sum | awk '{print $1}'
136
+ elif command -v shasum >/dev/null 2>&1; then shasum -a 256 | awk '{print $1}'
137
+ else red "ERROR: sha256sum or shasum is required"; exit 1; fi
138
+ }
139
+ hash_file() {
140
+ local file="$1"
141
+ if command -v sha256sum >/dev/null 2>&1; then sha256sum "$file" | awk '{print $1}'
142
+ elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$file" | awk '{print $1}'
143
+ else red "ERROR: sha256sum or shasum is required"; exit 1; fi
144
+ }
145
+ file_nonempty() { [ -f "$1" ] && [ -s "$1" ]; }
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env bash
2
+ # smart-guard.sh — phase exit-condition guard for the Smart skill.
3
+ # Usage: smart-guard.sh <change-name> <issue|design|build|verify|archive> [--apply]
4
+ # --apply: if checks pass, advance phase via smart-state transition.
5
+ # 样板版本: v1
6
+ # v1: set -uo pipefail (no -e; explicit exit 1 + `&&` shorthand). v1.1: if-blocks + set -e.
7
+ set -uo pipefail
8
+
9
+ _smart_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
10
+ SMART_ENV="${SMART_ENV:-$_smart_script_dir/smart-env.sh}"
11
+ if [ ! -f "$SMART_ENV" ]; then
12
+ SMART_ENV="$(find . "$HOME"/.*/skills "$HOME/.config" "$HOME/.gemini" -path '*/scripts/smart-env.sh' -type f -print -quit 2>/dev/null)"
13
+ fi
14
+ if [ -z "${SMART_ENV:-}" ] || [ ! -f "$SMART_ENV" ]; then
15
+ echo "ERROR: smart-env.sh not found. Ensure the smart skill is installed (scripts/smart-env.sh)." >&2
16
+ exit 1
17
+ fi
18
+ . "$SMART_ENV"
19
+
20
+ CHANGE="$1"; PHASE="$2"; APPLY=0
21
+ validate_change_name "$CHANGE"
22
+ [ "${3:-}" = "--apply" ] && APPLY=1
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
24
+ CHANGE_DIR="openspec/changes/$CHANGE"
25
+ [ "$PHASE" = "archive" ] && [ ! -d "$CHANGE_DIR" ] && [ -d "openspec/changes/archive/$CHANGE" ] && CHANGE_DIR="openspec/changes/archive/$CHANGE"
26
+
27
+ # local convenience: read field from this change's .smart.yaml
28
+ yaml_field_value() { local f="$1"; yaml_field "$f" "$CHANGE_DIR/.smart.yaml"; }
29
+
30
+ project_config_value() {
31
+ local field="$1" value
32
+ value=$(yaml_field_value "$field" 2>/dev/null || true)
33
+ { [ -n "$value" ] && [ "$value" != "null" ]; } && { echo "$value"; return 0; }
34
+ for cfg in ".smart/config.yaml" ".smart.yaml" ".smart.yml"; do
35
+ if [ -f "$cfg" ]; then value=$(yaml_field "$field" "$cfg"); { [ -n "$value" ] && [ "$value" != "null" ]; } && { echo "$value"; return 0; }; fi
36
+ done
37
+ }
38
+
39
+ # --- check primitives ---
40
+ BLOCK=0
41
+ check() { local desc="$1"; shift; local out; if out=$("$@" 2>&1); then green " [PASS] $desc"; else red " [FAIL] $desc"; printf '%s\n' "$out" | sed 's/^/ /' >&2; BLOCK=1; fi; }
42
+
43
+ tasks_all_done() {
44
+ local tasks="$CHANGE_DIR/tasks.md"
45
+ [ -f "$tasks" ] || { echo "tasks.md missing at $tasks" >&2; return 1; }
46
+ grep -q '\- \[x\]' "$tasks" || { echo "tasks.md has no completed tasks" >&2; return 1; }
47
+ if grep -q '\- \[ \]' "$tasks"; then echo "Unfinished tasks:" >&2; grep -n '\- \[ \]' "$tasks" >&2 || true; return 1; fi
48
+ }
49
+ tasks_has_any() { [ -f "$CHANGE_DIR/tasks.md" ] && grep -q '\- \[' "$CHANGE_DIR/tasks.md"; }
50
+ plan_tasks_all_done() {
51
+ local plan; plan=$(yaml_field_value "plan" 2>/dev/null || true)
52
+ [ -z "$plan" ] || [ "$plan" = "null" ] && return 0
53
+ [ -f "$plan" ] || { echo "plan file missing: $plan" >&2; return 1; }
54
+ ! grep -q '^[[:space:]]*- \[ \]' "$plan"
55
+ }
56
+
57
+ # --- handoff hash + traceability ---
58
+ handoff_source_files() {
59
+ printf '%s\n' "$CHANGE_DIR/proposal.md" "$CHANGE_DIR/design.md" "$CHANGE_DIR/tasks.md"
60
+ [ -d "$CHANGE_DIR/specs" ] && find "$CHANGE_DIR/specs" -path '*/spec.md' -type f 2>/dev/null | sort
61
+ }
62
+ compute_handoff_hash() {
63
+ local inp; inp=$(handoff_source_files | while IFS= read -r f; do [ -f "$f" ] && { printf 'path:%s\n' "$f"; printf 'sha256:%s\n' "$(hash_file "$f")"; }; done)
64
+ printf '%s' "$inp" | hash_stream
65
+ }
66
+ design_handoff_context_valid() {
67
+ local context recorded_hash actual_hash markdown
68
+ context=$(yaml_field_value "handoff_context"); recorded_hash=$(yaml_field_value "handoff_hash")
69
+ { [ -n "$context" ] && [ "$context" != "null" ]; } || { echo "handoff_context missing" >&2; return 1; }
70
+ [ -s "$context" ] || { echo "handoff_context not non-empty: $context" >&2; return 1; }
71
+ [[ "$recorded_hash" =~ ^[a-f0-9]{64}$ ]] || { echo "handoff_hash missing/invalid: ${recorded_hash:-null}" >&2; return 1; }
72
+ actual_hash=$(compute_handoff_hash)
73
+ [ "$actual_hash" = "$recorded_hash" ] || { echo "OpenSpec artifacts changed after handoff. expected=$recorded_hash actual=$actual_hash" >&2; return 1; }
74
+ markdown="${context%.json}.md"; [ -s "$markdown" ] || { echo "handoff markdown missing/empty: $markdown" >&2; return 1; }
75
+ }
76
+ design_handoff_markdown_traceable() {
77
+ local context markdown missing=0
78
+ context=$(yaml_field_value "handoff_context"); [ -n "$context" ] && [ "$context" != "null" ] || { echo "handoff_context missing" >&2; return 1; }
79
+ markdown="${context%.json}.md"; [ -s "$markdown" ] || { echo "handoff markdown missing: $markdown" >&2; return 1; }
80
+ grep -q '^Generated-by: smart-handoff\.sh$' "$markdown" || { echo "markdown missing Generated-by marker" >&2; missing=1; }
81
+ grep -Eq '^- Mode: (compact|full|beta)$' "$markdown" || { echo "markdown missing Mode marker" >&2; missing=1; }
82
+ handoff_source_files | while IFS= read -r f; do [ -f "$f" ] || continue
83
+ grep -q "^- Source: $f$" "$markdown" || { echo "markdown missing source ref: $f" >&2; exit 2; }
84
+ grep -q "^- SHA256: $(hash_file "$f")$" "$markdown" || { echo "markdown missing sha256 for: $f" >&2; exit 2; }
85
+ done || missing=1
86
+ [ "$missing" -eq 0 ]
87
+ }
88
+
89
+ # --- build / verify commands (lenient for skill repos: skip if no build system) ---
90
+ is_windows_bash() { case "$(uname -s 2>/dev/null || true)" in MINGW*|MSYS*|CYGWIN*) return 0 ;; *) return 1 ;; esac; }
91
+ run_command_string() {
92
+ local cmd="$1"; [ -n "$cmd" ] || { red "ERROR: build/verify command empty"; return 1; }
93
+ [[ "$cmd" =~ [\;\|\&\$\`] ]] && { red "ERROR: command contains shell metacharacters: $cmd"; return 1; }
94
+ echo "+ $cmd" >&2; "$SMART_BASH" -lc "$cmd"
95
+ }
96
+ build_passes() {
97
+ if [ "${SMART_SKIP_BUILD:-0}" = "1" ]; then return 0; fi
98
+ local cb; cb=$(project_config_value "build_command"); if [ -n "$cb" ]; then run_command_string "$cb"; return $?; fi
99
+ if [ -f package.json ] && grep -q '"build"' package.json; then npm run build; return $?; fi
100
+ if [ -f pom.xml ]; then if [ -x ./mvnw ]; then ./mvnw compile -q; elif is_windows_bash && command -v mvn.cmd >/dev/null 2>&1; then mvn.cmd compile -q; else mvn compile -q; fi; return $?; fi
101
+ if [ -f Cargo.toml ]; then cargo build; return $?; fi
102
+ warn " [WARN] no build system detected; skipping build check (skill repo)"
103
+ return 0
104
+ }
105
+ verification_command_passes() {
106
+ if [ "${SMART_SKIP_BUILD:-0}" = "1" ]; then return 0; fi
107
+ local cv; cv=$(project_config_value "verify_command"); if [ -n "$cv" ]; then run_command_string "$cv"; return $?; fi
108
+ build_passes
109
+ }
110
+
111
+ # --- design doc frontmatter ---
112
+ design_doc_frontmatter_has() { local doc="$1" field="$2" expected="$3"
113
+ awk '{ line=$0; sub(/^\357\273\277/,"",line) } !in_fm && line=="---"{in_fm=1;next} in_fm && line=="---"{exit} in_fm{print line}' "$doc" | grep -Eq "^${field}: ['\"]?${expected}['\"]?[[:space:]]*$"
114
+ }
115
+ design_doc_links_current_change() { local dd; dd=$(yaml_field_value "design_doc"); { [ -n "$dd" ] && [ "$dd" != "null" ] && [ -s "$dd" ]; } || { echo "design_doc must point to existing Design Doc" >&2; return 1; }; design_doc_frontmatter_has "$dd" "smart_change" "$CHANGE"; }
116
+ design_doc_declares_technical_role() { local dd; dd=$(yaml_field_value "design_doc"); { [ -n "$dd" ] && [ "$dd" != "null" ] && [ -s "$dd" ]; } && design_doc_frontmatter_has "$dd" "role" "technical-design"; }
117
+ design_doc_declares_canonical_spec() { local dd; dd=$(yaml_field_value "design_doc"); { [ -n "$dd" ] && [ "$dd" != "null" ] && [ -s "$dd" ]; } && design_doc_frontmatter_has "$dd" "canonical_spec" "openspec"; }
118
+ design_doc_recorded() { local dd; dd=$(yaml_field_value "design_doc"); { [ -n "$dd" ] && [ "$dd" != "null" ] && [ -f "$dd" ]; } && return 0; echo "design_doc must point to existing Design Doc (full workflow)" >&2; return 1; }
119
+
120
+ # --- decision selectors (reused from comet) ---
121
+ isolation_selected() { local i; i=$(yaml_field_value "isolation"); case "$i" in branch|worktree) return 0 ;; *) echo "isolation must be branch|worktree, got '${i:-null}'" >&2; return 1 ;; esac; }
122
+ build_mode_selected() { local b; b=$(yaml_field_value "build_mode"); case "$b" in subagent-driven-development|executing-plans|direct) return 0 ;; *) echo "build_mode must be selected, got '${b:-null}'" >&2; return 1 ;; esac; }
123
+ build_mode_allowed_for_workflow() { local w b d; w=$(yaml_field_value "workflow"); b=$(yaml_field_value "build_mode"); d=$(yaml_field_value "direct_override"); [ "$b" != "direct" ] && return 0; case "$w" in hotfix|tweak) return 0 ;; *) [ "$d" = "true" ] && return 0; echo "build_mode=direct only for hotfix/tweak unless direct_override=true" >&2; return 1 ;; esac; }
124
+ subagent_dispatch_confirmed() { local b s; b=$(yaml_field_value "build_mode"); s=$(yaml_field_value "subagent_dispatch"); [ "$b" != "subagent-driven-development" ] && return 0; [ "$s" = "confirmed" ] && return 0; echo "subagent_dispatch must be confirmed for subagent-driven-development" >&2; return 1; }
125
+ tdd_mode_selected() { local w t; w=$(yaml_field_value "workflow"); t=$(yaml_field_value "tdd_mode"); case "$w" in hotfix|tweak) return 0 ;; esac; case "$t" in tdd|direct) return 0 ;; *) echo "tdd_mode must be tdd|direct, got '${t:-null}'" >&2; return 1 ;; esac; }
126
+ review_mode_selected() { local w r; w=$(yaml_field_value "workflow"); r=$(yaml_field_value "review_mode"); case "$w" in hotfix|tweak) return 0 ;; esac; case "$r" in off|standard|thorough) return 0 ;; *) echo "review_mode must be off|standard|thorough, got '${r:-null}'" >&2; return 1 ;; esac; }
127
+ verification_report_exists() { local r; r=$(yaml_field_value "verification_report"); [ -n "$r" ] && [ "$r" != "null" ] && [ -f "$r" ]; }
128
+ branch_status_handled() { local s; s=$(yaml_field_value "branch_status"); [ "$s" = "handled" ]; }
129
+ archived_is_true() { local v; v=$(yaml_field_value "archived"); [ "$v" = "true" ]; }
130
+
131
+ # --- phase guards ---
132
+ preflight() {
133
+ [ -d "$CHANGE_DIR" ] || { red "FATAL: change directory not found: $CHANGE_DIR"; exit 1; }
134
+ [ -f "$CHANGE_DIR/.smart.yaml" ] || { red "FATAL: .smart.yaml not found in $CHANGE_DIR"; exit 1; }
135
+ }
136
+ guard_issue() {
137
+ echo "=== Guard: issue -> next ===" >&2
138
+ local workflow; workflow=$(yaml_field_value "workflow")
139
+ check "proposal.md non-empty" file_nonempty "$CHANGE_DIR/proposal.md"
140
+ if [ "$workflow" = "full" ]; then check "design.md non-empty" file_nonempty "$CHANGE_DIR/design.md"; fi
141
+ check "tasks.md non-empty" file_nonempty "$CHANGE_DIR/tasks.md"
142
+ check "tasks.md has a task" tasks_has_any
143
+ }
144
+ guard_design() {
145
+ echo "=== Guard: design -> build ===" >&2
146
+ local design_doc workflow
147
+ design_doc=$(yaml_field_value "design_doc"); workflow=$(yaml_field_value "workflow")
148
+ check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
149
+ check "design.md exists" file_nonempty "$CHANGE_DIR/design.md"
150
+ check "tasks.md exists" file_nonempty "$CHANGE_DIR/tasks.md"
151
+ check "design handoff context valid" design_handoff_context_valid
152
+ check "design handoff markdown traceable" design_handoff_markdown_traceable
153
+ if [ "$workflow" = "full" ]; then check "design_doc recorded (full)" design_doc_recorded; fi
154
+ if [ -n "$design_doc" ] && [ "$design_doc" != "null" ]; then
155
+ check "Design Doc exists" file_nonempty "$design_doc"
156
+ check "Design Doc links current change" design_doc_links_current_change
157
+ check "Design Doc declares technical-design role" design_doc_declares_technical_role
158
+ check "Design Doc declares openspec canonical spec" design_doc_declares_canonical_spec
159
+ elif [ "$workflow" != "full" ]; then warn " [WARN] No design_doc (optional for hotfix/tweak)"; fi
160
+ }
161
+ guard_build() {
162
+ echo "=== Guard: build -> verify ===" >&2
163
+ check "isolation selected" isolation_selected
164
+ check "build_mode selected" build_mode_selected
165
+ check "build_mode allowed for workflow" build_mode_allowed_for_workflow
166
+ check "subagent dispatch confirmed" subagent_dispatch_confirmed
167
+ check "tdd_mode selected" tdd_mode_selected
168
+ check "review_mode selected" review_mode_selected
169
+ check "tasks.md all checked" tasks_all_done
170
+ check "plan tasks all checked" plan_tasks_all_done
171
+ check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
172
+ check "Build passes" build_passes
173
+ }
174
+ guard_verify() {
175
+ echo "=== Guard: verify -> archive ===" >&2
176
+ check "tasks.md all checked" tasks_all_done
177
+ check "verify command passes" verification_command_passes
178
+ check "verification_report exists" verification_report_exists
179
+ check "branch_status=handled" branch_status_handled
180
+ }
181
+ guard_archive() {
182
+ echo "=== Guard: archive completeness ===" >&2
183
+ check "archived is true" archived_is_true
184
+ check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
185
+ check "design.md exists" file_nonempty "$CHANGE_DIR/design.md"
186
+ check "tasks.md all checked" tasks_all_done
187
+ }
188
+
189
+ apply_state_update() {
190
+ local p="$1"
191
+ case "$p" in
192
+ issue) "$SMART_BASH" "$SMART_STATE" transition "$CHANGE" issue-complete ;;
193
+ design) "$SMART_BASH" "$SMART_STATE" transition "$CHANGE" design-complete ;;
194
+ build) "$SMART_BASH" "$SMART_STATE" transition "$CHANGE" build-complete ;;
195
+ verify) "$SMART_BASH" "$SMART_STATE" transition "$CHANGE" verify-pass ;;
196
+ esac
197
+ }
198
+
199
+ case "$PHASE" in
200
+ issue) preflight; guard_issue ;;
201
+ design) preflight; guard_design ;;
202
+ build) preflight; guard_build ;;
203
+ verify) preflight; guard_verify ;;
204
+ archive) preflight; guard_archive ;;
205
+ *) red "Unknown phase: $PHASE"; echo "Valid: issue, design, build, verify, archive" >&2; exit 1 ;;
206
+ esac
207
+
208
+ if [ "$BLOCK" -eq 1 ]; then echo "" >&2; red "BLOCKED — fix failing checks before proceeding"; exit 1; fi
209
+ echo "" >&2; green "ALL CHECKS PASSED — ready for next phase"
210
+ if [ "$APPLY" -eq 1 ]; then
211
+ apply_state_update "$PHASE"
212
+ green " [APPLY] .smart.yaml updated (phase advanced)"
213
+ fi
214
+ exit 0
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env bash
2
+ # smart-handoff.sh — machine-owned context pack between phases.
3
+ # Usage: smart-handoff.sh <change-name> design --write [--full]
4
+ # smart-handoff.sh <change-name> --hash-only
5
+ # 样板版本: v1
6
+ # v1: set -uo pipefail (no -e; explicit exit 1 + `&&` shorthand). v1.1: if-blocks + set -e.
7
+ set -uo pipefail
8
+
9
+ _smart_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
10
+ SMART_ENV="${SMART_ENV:-$_smart_script_dir/smart-env.sh}"
11
+ if [ ! -f "$SMART_ENV" ]; then
12
+ SMART_ENV="$(find . "$HOME"/.*/skills "$HOME/.config" "$HOME/.gemini" -path '*/scripts/smart-env.sh' -type f -print -quit 2>/dev/null)"
13
+ fi
14
+ if [ -z "${SMART_ENV:-}" ] || [ ! -f "$SMART_ENV" ]; then
15
+ echo "ERROR: smart-env.sh not found. Ensure the smart skill is installed (scripts/smart-env.sh)." >&2
16
+ exit 1
17
+ fi
18
+ . "$SMART_ENV"
19
+
20
+ CHANGE="${1:-}"; PHASE="${2:-}"; MODE="${3:-}"; FULL_FLAG="${4:-}"
21
+ validate_change_name "$CHANGE"
22
+
23
+ json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
24
+ file_line_count() { wc -l < "$1" | tr -d ' '; }
25
+
26
+ source_files() {
27
+ printf '%s\n' "$CHANGE_DIR/proposal.md" "$CHANGE_DIR/design.md" "$CHANGE_DIR/tasks.md"
28
+ [ -d "$CHANGE_DIR/specs" ] && find "$CHANGE_DIR/specs" -path '*/spec.md' -type f 2>/dev/null | sort
29
+ }
30
+ compute_context_hash() {
31
+ local inp; inp=$(source_files | while IFS= read -r f; do [ -f "$f" ] && { printf 'path:%s\n' "$f"; printf 'sha256:%s\n' "$(hash_file "$f")"; }; done)
32
+ printf '%s' "$inp" | hash_stream
33
+ }
34
+
35
+ # --hash-only
36
+ if [ "${PHASE:-}" = "--hash-only" ]; then
37
+ CHANGE_DIR="openspec/changes/$CHANGE"
38
+ [ -d "$CHANGE_DIR" ] || { red "ERROR: change dir not found: $CHANGE_DIR"; exit 1; }
39
+ for req in proposal.md design.md tasks.md; do [ -s "$CHANGE_DIR/$req" ] || { red "ERROR: required file missing/empty: $req"; exit 1; }; done
40
+ compute_context_hash; exit 0
41
+ fi
42
+
43
+ [ "$PHASE" = "design" ] && [ "$MODE" = "--write" ] || { red "Usage: smart-handoff.sh <change-name> design --write [--full]"; red " smart-handoff.sh <change-name> --hash-only"; exit 1; }
44
+ case "$FULL_FLAG" in "") HANDOFF_MODE="compact" ;; --full) HANDOFF_MODE="full" ;; *) red "Usage: smart-handoff.sh <change> design --write [--full]"; exit 1 ;; esac
45
+
46
+ CHANGE_DIR="openspec/changes/$CHANGE"
47
+ YAML="$CHANGE_DIR/.smart.yaml"
48
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
49
+ STATE_SH="$SCRIPT_DIR/smart-state.sh"
50
+ [ -d "$CHANGE_DIR" ] || { red "ERROR: change dir not found: $CHANGE_DIR"; exit 1; }
51
+ [ -f "$YAML" ] || { red "ERROR: .smart.yaml not found at $YAML"; exit 1; }
52
+ [ "$(yaml_field phase "$YAML")" = "design" ] || { red "ERROR: design handoff requires phase: design"; exit 1; }
53
+ for req in proposal.md design.md tasks.md; do [ -s "$CHANGE_DIR/$req" ] || { red "ERROR: required artifact missing/empty: $req"; exit 1; }; done
54
+
55
+ HANDOFF_DIR="$CHANGE_DIR/.smart/handoff"
56
+ CONTEXT_COMPRESSION="$(yaml_field context_compression "$YAML" 2>/dev/null || true)"; CONTEXT_COMPRESSION="${CONTEXT_COMPRESSION:-off}"
57
+ case "$CONTEXT_COMPRESSION" in
58
+ off) CONTEXT_JSON="$HANDOFF_DIR/design-context.json"; CONTEXT_MD="$HANDOFF_DIR/design-context.md" ;;
59
+ beta) [ "$HANDOFF_MODE" = "full" ] && warn "[HANDOFF] --full ignored in beta mode"; HANDOFF_MODE="beta"; CONTEXT_JSON="$HANDOFF_DIR/spec-context.json"; CONTEXT_MD="$HANDOFF_DIR/spec-context.md" ;;
60
+ *) red "ERROR: invalid context_compression: $CONTEXT_COMPRESSION (off|beta)"; exit 1 ;;
61
+ esac
62
+ mkdir -p "$HANDOFF_DIR"
63
+ CONTEXT_HASH="$(compute_context_hash)"
64
+
65
+ write_file_excerpt() {
66
+ local file="$1" max="$2" total; total=$(file_line_count "$file")
67
+ echo "## $file"; echo ""
68
+ echo "- Source: $file"; echo "- Lines: 1-$total"; echo "- SHA256: $(hash_file "$file")"; echo ""
69
+ if [ "$HANDOFF_MODE" = "full" ] || [ "$total" -le "$max" ]; then echo '```md'; cat "$file"; echo '```'; else echo "[TRUNCATED]"; echo ""; echo '```md'; sed -n "1,${max}p" "$file"; echo '```'; echo ""; echo "Full source: $file"; fi
70
+ echo ""
71
+ }
72
+ write_markdown_context() {
73
+ local out="$1"
74
+ { echo "# Smart Design Handoff"; echo ""
75
+ echo "- Change: $CHANGE"; echo "- Phase: design"; echo "- Mode: $HANDOFF_MODE"; echo "- Context hash: $CONTEXT_HASH"; echo ""
76
+ echo "Generated-by: smart-handoff.sh"; echo ""
77
+ echo "OpenSpec remains the canonical capability spec. This handoff is a deterministic, source-traceable context pack, not an agent-authored summary."; echo ""
78
+ source_files | while IFS= read -r f; do [ -f "$f" ] && write_file_excerpt "$f" 80; done
79
+ } > "$out"
80
+ }
81
+ write_json_context() {
82
+ local out="$1"
83
+ { echo "{"
84
+ echo " \"change\": \"$(json_escape "$CHANGE")\","
85
+ echo " \"phase\": \"design\","
86
+ echo " \"mode\": \"$HANDOFF_MODE\","
87
+ echo " \"canonical_spec\": \"openspec\","
88
+ echo " \"generated_by\": \"smart-handoff.sh\","
89
+ echo " \"context_hash\": \"$CONTEXT_HASH\","
90
+ echo " \"files\": ["
91
+ local first=1; while IFS= read -r f; do [ -f "$f" ] || continue; [ "$first" -eq 0 ] && echo ","; first=0; printf ' { "path": "%s", "sha256": "%s" }' "$(json_escape "$f")" "$(hash_file "$f")"; done < <(source_files)
92
+ echo ""; echo " ]"; echo "}"
93
+ } > "$out"
94
+ }
95
+ write_spec_projection_for_file() { local f="$1"; echo "## $f"; echo "- Source: $f"; echo "- Lines: 1-$(file_line_count "$f")"; echo "- SHA256: $(hash_file "$f")"; echo ""; echo '```md'; cat "$f"; echo '```'; echo ""; }
96
+ write_spec_markdown_context() {
97
+ local out="$1"
98
+ { echo "# Smart Spec Context"; echo "- Change: $CHANGE"; echo "- Phase: design"; echo "- Mode: beta"; echo "- Context hash: $CONTEXT_HASH"; echo ""; echo "Generated-by: smart-handoff.sh"; echo ""
99
+ echo "OpenSpec remains canonical. This beta context pack verbatim-projects spec files and references supporting artifacts by hash."; echo ""
100
+ echo "## Source References"; echo ""; source_files | while IFS= read -r f; do [ -f "$f" ] && { echo "- Source: $f"; echo "- SHA256: $(hash_file "$f")"; }; done; echo ""
101
+ echo "## Acceptance Projection"; echo ""
102
+ if [ -d "$CHANGE_DIR/specs" ]; then find "$CHANGE_DIR/specs" -path '*/spec.md' -type f 2>/dev/null | sort | while IFS= read -r f; do write_spec_projection_for_file "$f"; done; else echo "No delta spec files found."; echo ""; fi
103
+ echo "Full source files remain canonical."
104
+ } > "$out"
105
+ }
106
+ write_spec_json_context() {
107
+ local out="$1"
108
+ { echo "{"
109
+ echo " \"change\": \"$(json_escape "$CHANGE")\","
110
+ echo " \"phase\": \"design\","
111
+ echo " \"mode\": \"beta\","
112
+ echo " \"canonical_spec\": \"openspec\","
113
+ echo " \"generated_by\": \"smart-handoff.sh\","
114
+ echo " \"context_hash\": \"$CONTEXT_HASH\","
115
+ echo " \"files\": ["
116
+ local first=1; while IFS= read -r f; do [ -f "$f" ] || continue; local role="supporting"; case "$f" in */specs/*/spec.md) role="spec" ;; esac; [ "$first" -eq 0 ] && echo ","; first=0; printf ' { "path": "%s", "sha256": "%s", "role": "%s" }' "$(json_escape "$f")" "$(hash_file "$f")" "$role"; done < <(source_files)
117
+ echo ""; echo " ]"; echo "}"
118
+ } > "$out"
119
+ }
120
+
121
+ if [ "$CONTEXT_COMPRESSION" = "beta" ]; then write_spec_markdown_context "$CONTEXT_MD"; write_spec_json_context "$CONTEXT_JSON"; else write_markdown_context "$CONTEXT_MD"; write_json_context "$CONTEXT_JSON"; fi
122
+
123
+ if [ -f "$STATE_SH" ]; then
124
+ "$SMART_BASH" "$STATE_SH" set "$CHANGE" handoff_context "$CONTEXT_JSON" >/dev/null
125
+ "$SMART_BASH" "$STATE_SH" set "$CHANGE" handoff_hash "$CONTEXT_HASH" >/dev/null
126
+ else red "ERROR: smart-state.sh not found; cannot record handoff fields"; exit 1; fi
127
+ green "[HANDOFF] wrote $CONTEXT_JSON"; green "[HANDOFF] wrote $CONTEXT_MD"; green "[HANDOFF] handoff_hash=$CONTEXT_HASH"