@thebassclef/lite 1.9.3 → 1.9.5
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/README.md +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/bassclef-orientation.md +20 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +23 -0
- package/dist/lite/.claude/rules/blocked-items.md +18 -0
- package/dist/lite/.claude/skills/longrun/SKILL.md +66 -0
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +33 -0
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/lib/ancestor-claude-check.sh +26 -4
- package/dist/lite/lib/bassclef-dir-resolver.sh +20 -14
- package/dist/lite/lib/capability-probe.sh +11 -17
- package/dist/lite/lib/statusline-version.sh +103 -0
- package/dist/lite/presence/cli/bassclef-statusline.dispatcher.sh +26 -7
- package/dist/lite/presence/install/bassclef-hook-connect.sh +56 -2
- package/dist/lite/presence/install/bassclef-sync.template.sh +43 -0
- package/dist/lite/scripts/generate-lite-manifest.sh +12 -6
- package/dist/lite/scripts/interpret-input/extract-repo.sh +239 -0
- package/dist/lite/scripts/interpret-input.sh +212 -0
- package/dist/lite/scripts/launch/local-serve-stop.sh +41 -0
- package/dist/lite/scripts/launch/local-serve.sh +122 -0
- package/dist/lite/scripts/launch/parse-input-args.sh +177 -0
- package/dist/lite/scripts/longrun-preset-detect.sh +87 -0
- package/dist/lite/standards/lite-manifest-schema-changes.md +36 -0
- package/dist/lite/standards/lite-manifest.json +71 -15
- package/dist/lite/standards/session-start-banner-discipline.md +18 -5
- package/package.json +1 -1
|
@@ -292,6 +292,42 @@ _template_emit_install_postcondition() {
|
|
|
292
292
|
fi
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
# === #1981 — sync state file writer (parity with operator hook L688-707) ===
|
|
296
|
+
# Persist sync state so lib/statusline-version.sh reader can render
|
|
297
|
+
# `bassclef v<X> · <sync_status>` on the adopter terminal statusline every
|
|
298
|
+
# ~300ms tick. Mirrors the operator-scope write block at
|
|
299
|
+
# .claude/hooks/bassclef-sync.sh L688-707. Both hooks must produce the same
|
|
300
|
+
# postcondition per ADR-032 two-stage rocket.
|
|
301
|
+
#
|
|
302
|
+
# Fail-soft: jq missing OR write path unwritable → file simply doesn't land;
|
|
303
|
+
# reader falls back to `bassclef · initializing` per lib/statusline-version.sh L83-85.
|
|
304
|
+
# Never break the sync summary that follows.
|
|
305
|
+
#
|
|
306
|
+
# Defined ABOVE the BASSCLEF_SYNC_TEST_MODE guard so tests can source + invoke.
|
|
307
|
+
#
|
|
308
|
+
# Anchor luminaries — @luminary michael-nygard (bounded-time observable state),
|
|
309
|
+
# @luminary michael-feathers (mirrors the operator hook exactly — characterization
|
|
310
|
+
# parity), @luminary jerome-saltzer-and-michael-schroeder (complete mediation
|
|
311
|
+
# across both operator + adopter paths).
|
|
312
|
+
_template_write_sync_status_file() {
|
|
313
|
+
local cwd="$1"
|
|
314
|
+
local sync_status="$2"
|
|
315
|
+
local status_file="${cwd}/state/bassclef-sync-status.json"
|
|
316
|
+
local version release_tag
|
|
317
|
+
version=$(jq -r '.version // ""' "${cwd}/bassclef-version.json" 2>/dev/null || echo "")
|
|
318
|
+
release_tag=$(jq -r '.release_tag // ""' "${cwd}/bassclef-version.json" 2>/dev/null || echo "")
|
|
319
|
+
mkdir -p "$(dirname "$status_file")" 2>/dev/null || return 0
|
|
320
|
+
cat > "$status_file" 2>/dev/null <<EOF
|
|
321
|
+
{
|
|
322
|
+
"version": "${version}",
|
|
323
|
+
"release_tag": "${release_tag}",
|
|
324
|
+
"sync_status": "${sync_status}",
|
|
325
|
+
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
326
|
+
}
|
|
327
|
+
EOF
|
|
328
|
+
return 0
|
|
329
|
+
}
|
|
330
|
+
|
|
295
331
|
# === Test-mode early return (#897 Cure A) ===
|
|
296
332
|
# Tests source this template to access helper functions (see
|
|
297
333
|
# .claude/hooks/tests/bassclef-sync-*.test.sh). Early return under
|
|
@@ -1936,6 +1972,13 @@ if [ -f "$HOOK_SELF" ] && [ -f "$HOOK_BASSCLEF" ]; then
|
|
|
1936
1972
|
esac
|
|
1937
1973
|
fi
|
|
1938
1974
|
|
|
1975
|
+
# --- Persist sync state for statusline reader (bassclef-upstream#1981) ---
|
|
1976
|
+
# Mirrors operator hook L688-707. Adopters who opt in to
|
|
1977
|
+
# lib/statusline-version.sh at ~/.claude/settings.json read this file at every
|
|
1978
|
+
# prompt to render `bassclef v<X> · <sync_status>`. Written here at end-of-run;
|
|
1979
|
+
# sole writer for the adopter path. Fail-soft — never break sync summary.
|
|
1980
|
+
{ _template_write_sync_status_file "$CWD" "$SYNC_STATUS"; } 2>/dev/null || true
|
|
1981
|
+
|
|
1939
1982
|
echo ""
|
|
1940
1983
|
echo "### BASSCLEF SUBSTRATE — SYNCED"
|
|
1941
1984
|
echo ""
|
|
@@ -479,13 +479,15 @@ done < <(find "$REPO_ROOT/.claude/embeddings" -type f -name '*.json' 2>/dev/null
|
|
|
479
479
|
LIB_FILES=()
|
|
480
480
|
while IFS= read -r f; do
|
|
481
481
|
LIB_FILES+=("$f")
|
|
482
|
-
done < <(find "$REPO_ROOT/lib" -
|
|
482
|
+
done < <(find "$REPO_ROOT/lib" -type f -name '*.sh' \
|
|
483
|
+
-not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
|
|
483
484
|
|
|
484
485
|
# Script files (bash) — per #1392 Shape 3; adopter runtime needs scripts/bassclef-*.sh helpers
|
|
485
486
|
SCRIPT_FILES=()
|
|
486
487
|
while IFS= read -r f; do
|
|
487
488
|
SCRIPT_FILES+=("$f")
|
|
488
|
-
done < <(find "$REPO_ROOT/scripts" -
|
|
489
|
+
done < <(find "$REPO_ROOT/scripts" -type f -name '*.sh' \
|
|
490
|
+
-not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
|
|
489
491
|
|
|
490
492
|
# Hook fragments (bash) — per goal 2026-09-16a (bassclef-upstream#1690): session-reflection.sh
|
|
491
493
|
# iterates session-reflection.d/*.sh at SessionStart; fragments ship as type=hook.
|
|
@@ -506,25 +508,29 @@ done < <(find "$REPO_ROOT/.claude" -maxdepth 1 -type f -name '*.md' 2>/dev/null
|
|
|
506
508
|
PRESENCE_TEMPLATE_SH_FILES=()
|
|
507
509
|
while IFS= read -r f; do
|
|
508
510
|
PRESENCE_TEMPLATE_SH_FILES+=("$f")
|
|
509
|
-
done < <(find "$REPO_ROOT/presence/install" -
|
|
511
|
+
done < <(find "$REPO_ROOT/presence/install" -type f -name '*.sh' \
|
|
512
|
+
-not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
|
|
510
513
|
|
|
511
514
|
PRESENCE_TEMPLATE_MD_FILES=()
|
|
512
515
|
while IFS= read -r f; do
|
|
513
516
|
PRESENCE_TEMPLATE_MD_FILES+=("$f")
|
|
514
|
-
done < <(find "$REPO_ROOT/presence/install" -
|
|
517
|
+
done < <(find "$REPO_ROOT/presence/install" -type f -name '*.md' \
|
|
518
|
+
-not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
|
|
515
519
|
|
|
516
520
|
# Presence templates (jsonc) — Ship 1 (bassclef-cli#104) — configs template
|
|
517
521
|
# ships so `bassclef init` can copy it to adopter's `.claude/bassclef-configs.jsonc`.
|
|
518
522
|
PRESENCE_TEMPLATE_JSONC_FILES=()
|
|
519
523
|
while IFS= read -r f; do
|
|
520
524
|
PRESENCE_TEMPLATE_JSONC_FILES+=("$f")
|
|
521
|
-
done < <(find "$REPO_ROOT/presence/install" -
|
|
525
|
+
done < <(find "$REPO_ROOT/presence/install" -type f -name '*.jsonc' \
|
|
526
|
+
-not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
|
|
522
527
|
|
|
523
528
|
# Templates (md) — per #1392 Shape 3; chronicle-template + iteration-goal etc adopters copy
|
|
524
529
|
TEMPLATE_FILES=()
|
|
525
530
|
while IFS= read -r f; do
|
|
526
531
|
TEMPLATE_FILES+=("$f")
|
|
527
|
-
done < <(find "$REPO_ROOT/templates" -
|
|
532
|
+
done < <(find "$REPO_ROOT/templates" -type f -name '*.md' \
|
|
533
|
+
-not -path '*/tests/*' -not -path '*/fixtures/*' 2>/dev/null | sort)
|
|
528
534
|
|
|
529
535
|
# Root docs (md) — per #1392 Shape 3; README + CLAUDE + AGENTS adopters read on install
|
|
530
536
|
ROOT_DOC_FILES=()
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# scripts/interpret-input/extract-repo.sh — Phase C repo extractor.
|
|
4
|
+
#
|
|
5
|
+
# Walks a local repo directory and extracts:
|
|
6
|
+
# - README excerpt (first 30 lines of README.md or equivalent)
|
|
7
|
+
# - Title (first H1 in README, or repo dirname)
|
|
8
|
+
# - Domain entity candidates (Prisma models + directory-pattern heuristic)
|
|
9
|
+
# - Boundary objects (Next.js routes, pages, API endpoints)
|
|
10
|
+
# - Actor candidates (role/auth keywords in source)
|
|
11
|
+
#
|
|
12
|
+
# Outputs JSON to stdout. Wrapper composes into the InputArtifact.
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# bash scripts/interpret-input/extract-repo.sh <local-repo-path>
|
|
16
|
+
#
|
|
17
|
+
# Exit codes:
|
|
18
|
+
# 0 = success (even partial extraction is success — empty arrays for missing signals)
|
|
19
|
+
# 2 = bad input (missing path or not a directory)
|
|
20
|
+
#
|
|
21
|
+
# Pairs with: scripts/interpret-input.sh + standards/input-handler-interface.md
|
|
22
|
+
|
|
23
|
+
set -u
|
|
24
|
+
|
|
25
|
+
REPO_PATH="${1:-}"
|
|
26
|
+
|
|
27
|
+
if [ -z "$REPO_PATH" ]; then
|
|
28
|
+
echo "extract-repo: requires <repo-path>" >&2
|
|
29
|
+
exit 2
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if [ ! -d "$REPO_PATH" ]; then
|
|
33
|
+
echo "extract-repo: $REPO_PATH is not a directory" >&2
|
|
34
|
+
exit 2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Use absolute path so cd doesn't break relative output
|
|
38
|
+
REPO_PATH="$(cd "$REPO_PATH" && pwd)"
|
|
39
|
+
cd "$REPO_PATH" || { echo "extract-repo: cannot cd to $REPO_PATH" >&2; exit 2; }
|
|
40
|
+
|
|
41
|
+
# === README excerpt + title ===
|
|
42
|
+
README_EXCERPT=""
|
|
43
|
+
README_PATH=""
|
|
44
|
+
for candidate in README.md README.MD readme.md README.rst README.txt; do
|
|
45
|
+
if [ -f "$candidate" ]; then
|
|
46
|
+
README_PATH="$candidate"
|
|
47
|
+
README_EXCERPT=$(head -30 "$candidate")
|
|
48
|
+
break
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
TITLE=""
|
|
53
|
+
if [ -n "$README_PATH" ]; then
|
|
54
|
+
TITLE=$(grep -m1 '^# ' "$README_PATH" 2>/dev/null | sed 's/^# *//')
|
|
55
|
+
fi
|
|
56
|
+
if [ -z "$TITLE" ]; then
|
|
57
|
+
TITLE=$(basename "$REPO_PATH")
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# === Entity candidates ===
|
|
61
|
+
|
|
62
|
+
# Strategy 1: Prisma models
|
|
63
|
+
ENTITIES_PRISMA=()
|
|
64
|
+
for schema in prisma/schema.prisma prisma/*.prisma; do
|
|
65
|
+
if [ -f "$schema" ]; then
|
|
66
|
+
while IFS= read -r model_name; do
|
|
67
|
+
[ -z "$model_name" ] && continue
|
|
68
|
+
ENTITIES_PRISMA+=("$model_name")
|
|
69
|
+
done < <(grep -E '^model [A-Z][a-zA-Z]+' "$schema" 2>/dev/null | awk '{print $2}')
|
|
70
|
+
fi
|
|
71
|
+
done
|
|
72
|
+
|
|
73
|
+
# Strategy 2: directory-pattern entities
|
|
74
|
+
# A directory containing >=3 files/subdirs of consistent shape suggests a collection.
|
|
75
|
+
# The singularized dirname is a candidate entity.
|
|
76
|
+
ENTITIES_DIR=()
|
|
77
|
+
|
|
78
|
+
# Collect candidate dirs at depth <=4, skipping known noise
|
|
79
|
+
CANDIDATE_DIRS=$(find . -maxdepth 4 -mindepth 1 -type d \
|
|
80
|
+
-not -path '*/node_modules*' \
|
|
81
|
+
-not -path '*/.git*' \
|
|
82
|
+
-not -path '*/.next*' \
|
|
83
|
+
-not -path '*/dist*' \
|
|
84
|
+
-not -path '*/build*' \
|
|
85
|
+
-not -path '*/coverage*' \
|
|
86
|
+
-not -path '*/__pycache__*' \
|
|
87
|
+
2>/dev/null)
|
|
88
|
+
|
|
89
|
+
while IFS= read -r dir; do
|
|
90
|
+
[ -z "$dir" ] && continue
|
|
91
|
+
DIRNAME=$(basename "$dir")
|
|
92
|
+
case "$DIRNAME" in
|
|
93
|
+
.|..|node_modules|.git|.next|dist|build|coverage|__tests__|tests|test|src|lib|components|utils|public|static|scripts|bin|hooks) continue ;;
|
|
94
|
+
esac
|
|
95
|
+
# Only consider directories whose name suggests a collection (plural-ish)
|
|
96
|
+
case "$DIRNAME" in
|
|
97
|
+
*s|*ies)
|
|
98
|
+
# Count direct children
|
|
99
|
+
COUNT=$(find "$dir" -maxdepth 1 -mindepth 1 \
|
|
100
|
+
\( -type d -o -name '*.md' -o -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' -o -name '*.json' -o -name '*.yml' -o -name '*.yaml' \) \
|
|
101
|
+
2>/dev/null | wc -l | tr -d ' ')
|
|
102
|
+
if [ "$COUNT" -ge 3 ]; then
|
|
103
|
+
# Singularize: ies → y; ses/xes/zes/ches/shes → strip "es"; else strip "s"
|
|
104
|
+
case "$DIRNAME" in
|
|
105
|
+
*ies) SINGULAR="${DIRNAME%ies}y" ;;
|
|
106
|
+
*ses|*xes|*zes|*ches|*shes) SINGULAR="${DIRNAME%es}" ;;
|
|
107
|
+
*s) SINGULAR="${DIRNAME%s}" ;;
|
|
108
|
+
esac
|
|
109
|
+
# Skip too-short or too-noisy singulars
|
|
110
|
+
if [ "${#SINGULAR}" -ge 3 ]; then
|
|
111
|
+
ENTITIES_DIR+=("$SINGULAR")
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
;;
|
|
115
|
+
esac
|
|
116
|
+
done <<<"$CANDIDATE_DIRS"
|
|
117
|
+
|
|
118
|
+
# Combine + dedupe entities
|
|
119
|
+
ALL_ENTITIES=()
|
|
120
|
+
SEEN=""
|
|
121
|
+
for e in "${ENTITIES_PRISMA[@]:-}" "${ENTITIES_DIR[@]:-}"; do
|
|
122
|
+
[ -z "$e" ] && continue
|
|
123
|
+
case " $SEEN " in
|
|
124
|
+
*" $e "*) ;;
|
|
125
|
+
*) ALL_ENTITIES+=("$e"); SEEN="$SEEN $e" ;;
|
|
126
|
+
esac
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
# === Boundary objects (routes + pages + API endpoints) ===
|
|
130
|
+
BOUNDARY_OBJECTS=()
|
|
131
|
+
|
|
132
|
+
# Next.js App Router routes (app/**/page.{ts,tsx,js,jsx})
|
|
133
|
+
for app_dir in app src/app; do
|
|
134
|
+
if [ -d "$app_dir" ]; then
|
|
135
|
+
while IFS= read -r f; do
|
|
136
|
+
[ -z "$f" ] && continue
|
|
137
|
+
# Convert path to route: app/deals/page.tsx → /deals
|
|
138
|
+
ROUTE=$(echo "$f" | sed -E "s|^./${app_dir}||" | sed 's|/page\.[a-zA-Z]*$||')
|
|
139
|
+
[ -z "$ROUTE" ] && ROUTE="/"
|
|
140
|
+
BOUNDARY_OBJECTS+=("route:$ROUTE")
|
|
141
|
+
done < <(find "./$app_dir" -maxdepth 6 \( -name 'page.tsx' -o -name 'page.ts' -o -name 'page.js' -o -name 'page.jsx' \) 2>/dev/null | head -50)
|
|
142
|
+
fi
|
|
143
|
+
done
|
|
144
|
+
|
|
145
|
+
# Next.js Pages Router routes (pages/**/*.{ts,tsx,js,jsx})
|
|
146
|
+
for pages_dir in pages src/pages; do
|
|
147
|
+
if [ -d "$pages_dir" ]; then
|
|
148
|
+
while IFS= read -r f; do
|
|
149
|
+
[ -z "$f" ] && continue
|
|
150
|
+
ROUTE=$(echo "$f" | sed -E "s|^./${pages_dir}||" | sed 's/\.[a-zA-Z]*$//')
|
|
151
|
+
[ -z "$ROUTE" ] && ROUTE="/"
|
|
152
|
+
BOUNDARY_OBJECTS+=("page:$ROUTE")
|
|
153
|
+
done < <(find "./$pages_dir" -maxdepth 6 -type f \( -name '*.tsx' -o -name '*.ts' -o -name '*.js' -o -name '*.jsx' \) -not -name '_*' -not -name 'api' 2>/dev/null | head -30)
|
|
154
|
+
fi
|
|
155
|
+
done
|
|
156
|
+
|
|
157
|
+
# API routes
|
|
158
|
+
for api_dir in app/api src/app/api pages/api src/pages/api; do
|
|
159
|
+
if [ -d "$api_dir" ]; then
|
|
160
|
+
while IFS= read -r f; do
|
|
161
|
+
[ -z "$f" ] && continue
|
|
162
|
+
ROUTE=$(echo "$f" | sed -E "s|^./${api_dir}||" | sed 's|/route\.[a-zA-Z]*$||' | sed 's/\.[a-zA-Z]*$//')
|
|
163
|
+
[ -z "$ROUTE" ] && ROUTE="/"
|
|
164
|
+
BOUNDARY_OBJECTS+=("api:$ROUTE")
|
|
165
|
+
done < <(find "./$api_dir" -maxdepth 6 -type f \( -name 'route.ts' -o -name 'route.tsx' -o -name 'route.js' -o -name '*.ts' \) 2>/dev/null | head -20)
|
|
166
|
+
fi
|
|
167
|
+
done
|
|
168
|
+
|
|
169
|
+
# === Actor candidates ===
|
|
170
|
+
# Heuristic: grep role/auth keywords in source code
|
|
171
|
+
ACTOR_CANDIDATES=()
|
|
172
|
+
ACTOR_RAW=$(grep -rEho \
|
|
173
|
+
'\b(admin|owner|operator|viewer|editor|guest|member|customer|client|reviewer|approver|manager)\b' \
|
|
174
|
+
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.prisma' --include='*.md' \
|
|
175
|
+
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=.next --exclude-dir=dist --exclude-dir=build \
|
|
176
|
+
. 2>/dev/null | tr '[:upper:]' '[:lower:]' | sort -u | head -10)
|
|
177
|
+
|
|
178
|
+
while IFS= read -r kw; do
|
|
179
|
+
[ -z "$kw" ] && continue
|
|
180
|
+
ACTOR_CANDIDATES+=("$kw")
|
|
181
|
+
done <<<"$ACTOR_RAW"
|
|
182
|
+
|
|
183
|
+
# === Compose extracted_text prose ===
|
|
184
|
+
ENTITY_LIST=$(IFS=,; echo "${ALL_ENTITIES[*]:-}")
|
|
185
|
+
ACTOR_LIST=$(IFS=,; echo "${ACTOR_CANDIDATES[*]:-}")
|
|
186
|
+
BOUNDARY_COUNT=${#BOUNDARY_OBJECTS[@]}
|
|
187
|
+
|
|
188
|
+
EXTRACTED_TEXT="Repository: $TITLE
|
|
189
|
+
Path: $REPO_PATH
|
|
190
|
+
|
|
191
|
+
Detected entities (${#ALL_ENTITIES[@]}): ${ENTITY_LIST:-none}
|
|
192
|
+
Detected actors (${#ACTOR_CANDIDATES[@]}): ${ACTOR_LIST:-none}
|
|
193
|
+
Detected boundary objects: ${BOUNDARY_COUNT} routes/pages/api endpoints"
|
|
194
|
+
|
|
195
|
+
if [ -n "$README_EXCERPT" ]; then
|
|
196
|
+
EXTRACTED_TEXT="${EXTRACTED_TEXT}
|
|
197
|
+
|
|
198
|
+
README excerpt:
|
|
199
|
+
${README_EXCERPT}"
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# === Build JSON output ===
|
|
203
|
+
# Convert arrays to JSON; handle empty arrays explicitly (bash 3.x compat — no namerefs).
|
|
204
|
+
if [ ${#ALL_ENTITIES[@]} -eq 0 ]; then
|
|
205
|
+
ENTITIES_JSON='[]'
|
|
206
|
+
else
|
|
207
|
+
ENTITIES_JSON=$(printf '%s\n' "${ALL_ENTITIES[@]}" | jq -R . | jq -s -c '. | unique')
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
if [ ${#ACTOR_CANDIDATES[@]} -eq 0 ]; then
|
|
211
|
+
ACTORS_JSON='[]'
|
|
212
|
+
else
|
|
213
|
+
ACTORS_JSON=$(printf '%s\n' "${ACTOR_CANDIDATES[@]}" | jq -R . | jq -s -c '. | unique')
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
if [ ${#BOUNDARY_OBJECTS[@]} -eq 0 ]; then
|
|
217
|
+
BOUNDARY_JSON='[]'
|
|
218
|
+
else
|
|
219
|
+
BOUNDARY_JSON=$(printf '%s\n' "${BOUNDARY_OBJECTS[@]}" | jq -R . | jq -s -c '. | unique')
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# Output
|
|
223
|
+
jq -n \
|
|
224
|
+
--arg extracted_text "$EXTRACTED_TEXT" \
|
|
225
|
+
--arg title "$TITLE" \
|
|
226
|
+
--arg readme_excerpt "$README_EXCERPT" \
|
|
227
|
+
--argjson entities_seen "$ENTITIES_JSON" \
|
|
228
|
+
--argjson actor_candidates "$ACTORS_JSON" \
|
|
229
|
+
--argjson boundary_objects "$BOUNDARY_JSON" \
|
|
230
|
+
'{
|
|
231
|
+
extracted_text: $extracted_text,
|
|
232
|
+
structural_hints: {
|
|
233
|
+
entities_seen: $entities_seen,
|
|
234
|
+
actor_candidates: $actor_candidates,
|
|
235
|
+
boundary_objects: $boundary_objects,
|
|
236
|
+
readme_excerpt: $readme_excerpt,
|
|
237
|
+
title: $title
|
|
238
|
+
}
|
|
239
|
+
}'
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# scripts/interpret-input.sh — input-handler CLI wrapper.
|
|
4
|
+
#
|
|
5
|
+
# Phase B: text-mode fully; other modes return stub InputArtifacts
|
|
6
|
+
# (schema-valid; extracted_text="deferred to Phase C").
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# bash scripts/interpret-input.sh "<source>" [--type=auto|text|url|image|repo|transcript|napkin|mixed] [--persist] [--slug=<slug>]
|
|
10
|
+
#
|
|
11
|
+
# Output:
|
|
12
|
+
# --persist OFF (default): InputArtifact JSON to stdout
|
|
13
|
+
# --persist ON: JSON written to docs/input-artifacts/<slug>.json; stdout = path
|
|
14
|
+
#
|
|
15
|
+
# Exit codes:
|
|
16
|
+
# 0 = success
|
|
17
|
+
# 2 = invalid input
|
|
18
|
+
# 3 = jq construction failed
|
|
19
|
+
# 4 = filesystem error (overwrite or mkdir)
|
|
20
|
+
# 5 = repo extraction setup failed (clone error, mktemp error)
|
|
21
|
+
#
|
|
22
|
+
# Pairs with: standards/input-handler-interface.md + .claude/skills/interpret-input/SKILL.md
|
|
23
|
+
|
|
24
|
+
set -u
|
|
25
|
+
|
|
26
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
27
|
+
|
|
28
|
+
usage() {
|
|
29
|
+
cat <<'EOF'
|
|
30
|
+
/interpret-input — produce schema-validated InputArtifact from any input
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
bash scripts/interpret-input.sh "<source>" [flags]
|
|
34
|
+
|
|
35
|
+
Flags:
|
|
36
|
+
--type=<type> text|url|image|repo|transcript|napkin|mixed (default: auto)
|
|
37
|
+
--persist write to docs/input-artifacts/<slug>.json
|
|
38
|
+
--slug=<slug> override auto-generated slug
|
|
39
|
+
--help show this message
|
|
40
|
+
|
|
41
|
+
Auto-detection:
|
|
42
|
+
^https?:// → url
|
|
43
|
+
\.(png|jpg|jpeg|gif|webp|svg)$ → image
|
|
44
|
+
\.git$ or contains github.com → repo
|
|
45
|
+
(else) → text
|
|
46
|
+
|
|
47
|
+
Phase B: text fully; other modes stub (extracted_text="deferred to Phase C").
|
|
48
|
+
EOF
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
SOURCE=""
|
|
52
|
+
TYPE="auto"
|
|
53
|
+
PERSIST=0
|
|
54
|
+
SLUG=""
|
|
55
|
+
|
|
56
|
+
while [ $# -gt 0 ]; do
|
|
57
|
+
case "$1" in
|
|
58
|
+
--type=*) TYPE="${1#--type=}"; shift ;;
|
|
59
|
+
--persist) PERSIST=1; shift ;;
|
|
60
|
+
--slug=*) SLUG="${1#--slug=}"; shift ;;
|
|
61
|
+
--help|-h) usage; exit 0 ;;
|
|
62
|
+
--*) echo "interpret-input: unknown flag '$1'" >&2; usage >&2; exit 2 ;;
|
|
63
|
+
*)
|
|
64
|
+
if [ -z "$SOURCE" ]; then SOURCE="$1"; shift
|
|
65
|
+
else echo "interpret-input: multiple positional sources not supported in v0" >&2; exit 2
|
|
66
|
+
fi
|
|
67
|
+
;;
|
|
68
|
+
esac
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
if [ -z "$SOURCE" ]; then
|
|
72
|
+
echo "interpret-input: requires <source>" >&2
|
|
73
|
+
usage >&2
|
|
74
|
+
exit 2
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Auto-detect type
|
|
78
|
+
if [ "$TYPE" = "auto" ]; then
|
|
79
|
+
if echo "$SOURCE" | grep -qE '^https?://'; then
|
|
80
|
+
TYPE="url"
|
|
81
|
+
elif echo "$SOURCE" | grep -qiE '\.(png|jpg|jpeg|gif|webp|svg)$'; then
|
|
82
|
+
TYPE="image"
|
|
83
|
+
elif echo "$SOURCE" | grep -qE '\.git$'; then
|
|
84
|
+
TYPE="repo"
|
|
85
|
+
elif [ -d "$SOURCE" ] && [ -d "$SOURCE/.git" ]; then
|
|
86
|
+
# Local directory that's a git repo
|
|
87
|
+
TYPE="repo"
|
|
88
|
+
else
|
|
89
|
+
TYPE="text"
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Validate type against schema enum
|
|
94
|
+
case "$TYPE" in
|
|
95
|
+
text|url|image|repo|transcript|napkin|mixed) ;;
|
|
96
|
+
*)
|
|
97
|
+
echo "interpret-input: unknown type '$TYPE' (must be: text|url|image|repo|transcript|napkin|mixed)" >&2
|
|
98
|
+
exit 2
|
|
99
|
+
;;
|
|
100
|
+
esac
|
|
101
|
+
|
|
102
|
+
# Generate slug if not provided
|
|
103
|
+
if [ -z "$SLUG" ]; then
|
|
104
|
+
TS=$(date -u +%Y-%m-%d)
|
|
105
|
+
HASH=$(printf '%s' "$SOURCE" | md5 -q 2>/dev/null \
|
|
106
|
+
|| printf '%s' "$SOURCE" | md5sum 2>/dev/null | cut -c1-6 \
|
|
107
|
+
|| echo "abc123")
|
|
108
|
+
HASH="${HASH:0:6}"
|
|
109
|
+
SLUG="${TS}-${TYPE}-${HASH}"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# Validate slug pattern (matches schema's id pattern)
|
|
113
|
+
if ! echo "$SLUG" | grep -qE '^[a-z0-9][a-z0-9-]*$'; then
|
|
114
|
+
echo "interpret-input: slug '$SLUG' does not match required pattern ^[a-z0-9][a-z0-9-]*$" >&2
|
|
115
|
+
exit 2
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
119
|
+
|
|
120
|
+
STRUCTURAL_HINTS='{}'
|
|
121
|
+
|
|
122
|
+
case "$TYPE" in
|
|
123
|
+
text)
|
|
124
|
+
EXTRACTED_TEXT="$SOURCE"
|
|
125
|
+
EXTERNAL_URLS=$(printf '%s' "$SOURCE" | grep -oE 'https?://[^[:space:]<>"`]+' \
|
|
126
|
+
| sed 's/[.,;:)]*$//' \
|
|
127
|
+
| jq -R . | jq -s -c . 2>/dev/null || echo '[]')
|
|
128
|
+
;;
|
|
129
|
+
repo)
|
|
130
|
+
# Phase C: walk a local repo or clone a URL, extract structural signals.
|
|
131
|
+
# Resolve source to a local path. If the source isn't a URL or an existing
|
|
132
|
+
# directory, fall back to stub mode (preserves Phase B contract for
|
|
133
|
+
# not-yet-extractable sources).
|
|
134
|
+
CLEANUP_TMP=0
|
|
135
|
+
REPO_LOCAL=""
|
|
136
|
+
if echo "$SOURCE" | grep -qE '^https?://'; then
|
|
137
|
+
TMP_DIR=$(mktemp -d 2>/dev/null) || { echo "interpret-input: mktemp failed" >&2; exit 5; }
|
|
138
|
+
if ! git clone --depth=1 --quiet "$SOURCE" "$TMP_DIR" 2>/dev/null; then
|
|
139
|
+
rm -rf "$TMP_DIR"
|
|
140
|
+
echo "interpret-input: git clone failed for $SOURCE" >&2
|
|
141
|
+
exit 5
|
|
142
|
+
fi
|
|
143
|
+
REPO_LOCAL="$TMP_DIR"
|
|
144
|
+
CLEANUP_TMP=1
|
|
145
|
+
elif [ -d "$SOURCE" ]; then
|
|
146
|
+
REPO_LOCAL="$SOURCE"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
if [ -z "$REPO_LOCAL" ]; then
|
|
150
|
+
# Source can't be resolved; return Phase B-style stub (still valid InputArtifact)
|
|
151
|
+
EXTRACTED_TEXT="deferred to Phase C"
|
|
152
|
+
STRUCTURAL_HINTS='{}'
|
|
153
|
+
else
|
|
154
|
+
EXTRACT_OUT=$(bash "$REPO_ROOT/scripts/interpret-input/extract-repo.sh" "$REPO_LOCAL" 2>/dev/null)
|
|
155
|
+
EXTRACT_RC=$?
|
|
156
|
+
|
|
157
|
+
if [ "$CLEANUP_TMP" = "1" ]; then
|
|
158
|
+
rm -rf "$TMP_DIR"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
if [ "$EXTRACT_RC" != "0" ] || [ -z "$EXTRACT_OUT" ]; then
|
|
162
|
+
EXTRACTED_TEXT="repo extraction returned no signal for $SOURCE"
|
|
163
|
+
STRUCTURAL_HINTS='{}'
|
|
164
|
+
else
|
|
165
|
+
EXTRACTED_TEXT=$(echo "$EXTRACT_OUT" | jq -r '.extracted_text')
|
|
166
|
+
STRUCTURAL_HINTS=$(echo "$EXTRACT_OUT" | jq -c '.structural_hints')
|
|
167
|
+
fi
|
|
168
|
+
fi
|
|
169
|
+
EXTERNAL_URLS='[]'
|
|
170
|
+
;;
|
|
171
|
+
*)
|
|
172
|
+
EXTRACTED_TEXT="deferred to Phase C"
|
|
173
|
+
EXTERNAL_URLS='[]'
|
|
174
|
+
;;
|
|
175
|
+
esac
|
|
176
|
+
|
|
177
|
+
ARTIFACT=$(jq -n \
|
|
178
|
+
--arg id "$SLUG" \
|
|
179
|
+
--arg type "$TYPE" \
|
|
180
|
+
--arg source "$SOURCE" \
|
|
181
|
+
--arg extracted_text "$EXTRACTED_TEXT" \
|
|
182
|
+
--arg created_at "$NOW" \
|
|
183
|
+
--argjson external_urls "$EXTERNAL_URLS" \
|
|
184
|
+
--argjson structural_hints "$STRUCTURAL_HINTS" \
|
|
185
|
+
'{
|
|
186
|
+
id: $id,
|
|
187
|
+
type: $type,
|
|
188
|
+
source: $source,
|
|
189
|
+
extracted_text: $extracted_text,
|
|
190
|
+
structural_hints: $structural_hints,
|
|
191
|
+
external_urls: $external_urls,
|
|
192
|
+
created_at: $created_at
|
|
193
|
+
}')
|
|
194
|
+
|
|
195
|
+
if [ -z "$ARTIFACT" ]; then
|
|
196
|
+
echo "interpret-input: jq failed to construct InputArtifact JSON" >&2
|
|
197
|
+
exit 3
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
if [ "$PERSIST" = "1" ]; then
|
|
201
|
+
TARGET_DIR="docs/input-artifacts"
|
|
202
|
+
TARGET_PATH="${TARGET_DIR}/${SLUG}.json"
|
|
203
|
+
if [ -f "$TARGET_PATH" ]; then
|
|
204
|
+
echo "interpret-input: $TARGET_PATH already exists; --slug=<new> to write a different one" >&2
|
|
205
|
+
exit 4
|
|
206
|
+
fi
|
|
207
|
+
mkdir -p "$TARGET_DIR" || { echo "interpret-input: mkdir $TARGET_DIR failed" >&2; exit 4; }
|
|
208
|
+
echo "$ARTIFACT" | jq . > "$TARGET_PATH" || { echo "interpret-input: write $TARGET_PATH failed" >&2; exit 4; }
|
|
209
|
+
echo "$TARGET_PATH"
|
|
210
|
+
else
|
|
211
|
+
echo "$ARTIFACT" | jq .
|
|
212
|
+
fi
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# local-serve-stop.sh — stop a local-serve.sh server by slug
|
|
4
|
+
#
|
|
5
|
+
# Per bassclef#923 A2. Companion to scripts/launch/local-serve.sh.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash scripts/launch/local-serve-stop.sh <gallery-slug>
|
|
9
|
+
#
|
|
10
|
+
# If a server is running for the slug, kills the PID and removes the
|
|
11
|
+
# state file. If no server is running, exits 0 silently.
|
|
12
|
+
|
|
13
|
+
set -e
|
|
14
|
+
|
|
15
|
+
SLUG="${1:-}"
|
|
16
|
+
if [ -z "$SLUG" ]; then
|
|
17
|
+
echo "ERROR: gallery-slug required" >&2
|
|
18
|
+
echo "usage: $0 <gallery-slug>" >&2
|
|
19
|
+
exit 2
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
STATE_DIR="${REPO_ROOT:-$(pwd)}/state/markers/local-serve"
|
|
23
|
+
STATE_FILE="$STATE_DIR/${SLUG}.state"
|
|
24
|
+
|
|
25
|
+
if [ ! -f "$STATE_FILE" ]; then
|
|
26
|
+
echo "local-serve-stop: no state file for $SLUG — nothing to stop" >&2
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
PID=$(awk -F= '/^pid=/{print $2}' "$STATE_FILE" 2>/dev/null)
|
|
31
|
+
URL=$(awk -F= '/^url=/{print $2}' "$STATE_FILE" 2>/dev/null)
|
|
32
|
+
|
|
33
|
+
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
|
|
34
|
+
kill "$PID" 2>/dev/null || true
|
|
35
|
+
echo "local-serve-stop: stopped server at $URL (pid=$PID)" >&2
|
|
36
|
+
else
|
|
37
|
+
echo "local-serve-stop: state file present but PID $PID not alive — cleaning up" >&2
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
rm -f "$STATE_FILE" "${STATE_DIR}/${SLUG}.log"
|
|
41
|
+
exit 0
|