@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.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/lite/.claude/bassclef-orientation.md +20 -0
  6. package/dist/lite/.claude/hooks/session-reflection.sh +23 -0
  7. package/dist/lite/.claude/rules/blocked-items.md +18 -0
  8. package/dist/lite/.claude/skills/longrun/SKILL.md +66 -0
  9. package/dist/lite/.claude/skills/onboard-repo/SKILL.md +33 -0
  10. package/dist/lite/bassclef-version.json +5 -5
  11. package/dist/lite/lib/ancestor-claude-check.sh +26 -4
  12. package/dist/lite/lib/bassclef-dir-resolver.sh +20 -14
  13. package/dist/lite/lib/capability-probe.sh +11 -17
  14. package/dist/lite/lib/statusline-version.sh +103 -0
  15. package/dist/lite/presence/cli/bassclef-statusline.dispatcher.sh +26 -7
  16. package/dist/lite/presence/install/bassclef-hook-connect.sh +56 -2
  17. package/dist/lite/presence/install/bassclef-sync.template.sh +43 -0
  18. package/dist/lite/scripts/generate-lite-manifest.sh +12 -6
  19. package/dist/lite/scripts/interpret-input/extract-repo.sh +239 -0
  20. package/dist/lite/scripts/interpret-input.sh +212 -0
  21. package/dist/lite/scripts/launch/local-serve-stop.sh +41 -0
  22. package/dist/lite/scripts/launch/local-serve.sh +122 -0
  23. package/dist/lite/scripts/launch/parse-input-args.sh +177 -0
  24. package/dist/lite/scripts/longrun-preset-detect.sh +87 -0
  25. package/dist/lite/standards/lite-manifest-schema-changes.md +36 -0
  26. package/dist/lite/standards/lite-manifest.json +71 -15
  27. package/dist/lite/standards/session-start-banner-discipline.md +18 -5
  28. 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" -maxdepth 1 -type f -name '*.sh' 2>/dev/null | sort)
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" -maxdepth 1 -type f -name '*.sh' 2>/dev/null | sort)
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" -maxdepth 1 -type f -name '*.sh' 2>/dev/null | sort)
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" -maxdepth 1 -type f -name '*.md' 2>/dev/null | sort)
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" -maxdepth 1 -type f -name '*.jsonc' 2>/dev/null | sort)
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" -maxdepth 1 -type f -name '*.md' 2>/dev/null | sort)
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