@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.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/SKILL.md +269 -0
- package/bin/smart.js +3 -0
- package/package.json +39 -0
- package/reference/file-structure.md +61 -0
- package/reference/issue-lifecycle.md +96 -0
- package/reference/smart-yaml-fields.md +66 -0
- package/scripts/smart-archive.sh +138 -0
- package/scripts/smart-env.sh +145 -0
- package/scripts/smart-guard.sh +214 -0
- package/scripts/smart-handoff.sh +127 -0
- package/scripts/smart-state.sh +365 -0
- package/smart-archive/SKILL.md +102 -0
- package/smart-build/SKILL.md +317 -0
- package/smart-design/SKILL.md +264 -0
- package/smart-hotfix/SKILL.md +200 -0
- package/smart-issue/SKILL.md +259 -0
- package/smart-tweak/SKILL.md +176 -0
- package/smart-verify/SKILL.md +232 -0
- package/src/deploy.js +47 -0
- package/src/index.js +54 -0
- package/src/init.js +80 -0
- package/src/platforms.js +20 -0
- package/src/status.js +31 -0
- package/src/uninstall.js +31 -0
|
@@ -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"
|