@thebassclef/lite 1.9.10 → 1.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ ---
2
+ tier: lite
3
+ title: Last-block marker
4
+ anchor_luminaries:
5
+ - john-ousterhout
6
+ - tony-hoare
7
+ - michael-feathers
8
+ - don-norman
9
+ - alan-cooper
10
+ - steve-krug
11
+ ---
12
+
13
+ # Last-block marker
14
+
15
+ Every call to `blocked_banner()` or `guardrail_banner()` in `.claude/hooks/session-reflection.sh` persists a marker at `state/markers/last-block/<hook>.marker`. The marker carries the hook name, severity, timestamp, and verbatim banner text. The `/last-block` skill reads markers back.
16
+
17
+ This rule is the methodology layer. The mechanical layer is `lib/last-block.sh` sourced by `session-reflection.sh`. Both required together to close Kunal Finding #10.
18
+
19
+ ## When this rule fires
20
+
21
+ Every call to `blocked_banner()` or `guardrail_banner()` in `.claude/hooks/session-reflection.sh`. The banner helpers already call `record_last_block` from the sourced lib. Hook authors who call these helpers get the marker for free.
22
+
23
+ ## Marker format
24
+
25
+ Plaintext layout per `lib/last-block.sh`:
26
+
27
+ ```
28
+ hook: <hook-name>
29
+ severity: blocking|advisory|info|guardrail
30
+ fired_at: <ISO-8601 UTC>
31
+ session: <CLAUDE_SESSION_ID or "unknown">
32
+ ---
33
+ <verbatim banner text, truncated at 8KB with a truncation marker>
34
+ ```
35
+
36
+ Grep-friendly. One marker file per hook. Overwrites on each fire. No rotation in V1.
37
+
38
+ ## What the rule requires
39
+
40
+ For hooks that fire BLOCK via `blocked_banner()` or `guardrail_banner()`:
41
+
42
+ - Pass the hook name via the `BASSCLEF_CALLING_HOOK` env var before the call. The default is `session-reflection` when unset.
43
+ - Pass a plain severity (`blocking` / `advisory` / `info`) to `blocked_banner()` as the second arg.
44
+ - The banner helper writes the marker on its own. No extra step.
45
+
46
+ For hooks that fire BLOCK via plain `echo` to stderr (not via the shared helpers):
47
+
48
+ - Follow-on retrofit per `.claude/rules/bootstrap-pair-discipline.md`. Adopt the shared helpers when the hook is next touched.
49
+
50
+ ## Anti-patterns
51
+
52
+ **Hook writes a marker directly without using the lib.**
53
+ INSTEAD source `lib/last-block.sh` and call `record_last_block`. The lib owns the format.
54
+
55
+ **Marker format drift.**
56
+ INSTEAD the plaintext layout stays pinned in `lib/last-block.sh`. Change the lib + Tier 0 tests + this rule in one PR.
57
+
58
+ **BLOCK banner emitted directly to stderr without recording.**
59
+ INSTEAD route through `blocked_banner()` so the marker lands with the banner. If a hook has a reason to skip the shared helper, call `record_last_block` directly after the echo.
60
+
61
+ ## Override
62
+
63
+ `SKIP_LAST_BLOCK_RECORD=1` bypasses marker recording for one call. Logged via trace-helper. Rare — mostly for test fixtures that need to control marker writing timing.
64
+
65
+ ## Composes with
66
+
67
+ - `.claude/rules/blocked-items.md` — sister rule; the two banner shapes live there, and this rule adds the persistence requirement
68
+ - `.claude/rules/bootstrap-pair-discipline.md` — this rule + lib + skill + tests + settings wire ship together
69
+ - `.claude/rules/testing-tier-config.md` — Tier 0 strict TDD on `lib/last-block.sh`
70
+ - `.claude/rules/we-dont-break-adopters.md` — additive change; existing banner callers see no behavior change
71
+ - `.claude/hooks/session-reflection.sh` — the hook that sources the lib + calls record_last_block from both banner helpers
72
+ - `lib/last-block.sh` — the deep module
73
+ - `.claude/skills/last-block/SKILL.md` — the user-invocable surface
74
+ - @luminary john-ousterhout — deep module anchor
75
+ - @luminary tony-hoare — pre/postcondition anchor (write path fail-soft)
76
+ - @luminary michael-feathers — characterization anchor (Tier 0 tests pin every path)
77
+ - @luminary don-norman — signifier (banner footer teaches the recovery path)
78
+ - @luminary alan-cooper — goal-directed (one goal: get banner text back)
79
+ - @luminary steve-krug — discoverability (no-args default)
@@ -371,7 +371,13 @@ fi
371
371
 
372
372
  ### Phase 2b — Adopter-mode framing (bassclef#943)
373
373
 
374
- Two paths — adopter-mode and operator-mode. Default is adopter when the parent /launch used `--local` (cold-adopter context); operator-mode otherwise. Mode detection reads `docs/preview-state/<slug>.yml`: when `local_url` is present, /launch shipped via the adopter path, so /build follows.
374
+ Two paths — adopter-mode and operator-mode. Default is adopter after a `/launch --local` run. Operator-mode is the default otherwise.
375
+
376
+ Mode detection reads the goal doc's `preview_state:` frontmatter field. /launch writes that field. Open the file it names. When `local_url` is present, /build picks adopter-mode. Default to operator-mode when the field is absent.
377
+
378
+ <!-- FKGL-EXEMPT: ticket reference -->
379
+ Per sunj-labs/bassclef-cli#326.
380
+ <!-- /FKGL-EXEMPT -->
375
381
 
376
382
  #### Adopter-mode pre-dispatch surface
377
383
 
@@ -62,6 +62,14 @@ Derive nav from the screen map. Apply Peter Morville's IA framework:
62
62
  - **C**ategory: by type or classification
63
63
  - **H**ierarchy: by importance or score
64
64
 
65
+ ## Output file
66
+
67
+ Write to `docs/ia-models/<slug>.md`. Use the active goal or sprint slug. `/launch` Phase 6 reads from this path. `/build` Phase 0.5 reads from the same path.
68
+
69
+ <!-- FKGL-EXEMPT: ticket reference -->
70
+ Per sunj-labs/bassclef-cli#317.
71
+ <!-- /FKGL-EXEMPT -->
72
+
65
73
  ## Output Format
66
74
 
67
75
  ```markdown
@@ -68,6 +68,14 @@ BACKSTAGE (system does): Score → Index → Enrich → Notify
68
68
  SUPPORT PROCESSES: Ingest → Classify → Deduplicate
69
69
  ```
70
70
 
71
+ ## Output file
72
+
73
+ Write to `docs/interaction-designs/<slug>.md`. Use the active goal or sprint slug. `/build` Phase 0.5 reads from this path. `/launch` Phase 7 reads from the same path.
74
+
75
+ <!-- FKGL-EXEMPT: ticket reference -->
76
+ Per sunj-labs/bassclef-cli#317.
77
+ <!-- /FKGL-EXEMPT -->
78
+
71
79
  ## Output Format
72
80
 
73
81
  For each interaction flow:
@@ -188,9 +188,17 @@ For each JTBD, produce:
188
188
  | 1.1 ... | [job] | [screen or component implied] |
189
189
  ```
190
190
 
191
+ ## Output file
192
+
193
+ Write to `docs/jtbd/<slug>.md`. Use the active goal or sprint slug. `/build` Phase 0.5 reads from this path. See the ticket noted at the end of this skill body.
194
+
195
+ <!-- FKGL-EXEMPT: ticket reference -->
196
+ Per sunj-labs/bassclef-cli#317.
197
+ <!-- /FKGL-EXEMPT -->
198
+
191
199
  ## Next Step
192
200
 
193
- Output feeds into `/task-scenarios` (user stories) or directly into `/ia-model` (entity-driven IA).
201
+ The output feeds `/task-scenarios` for user stories. Or it feeds `/ia-model` to drive IA.
194
202
 
195
203
  ## References
196
204
 
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: last-block
3
+ description: Print the exact text of a BLOCK or guardrail banner you saw this session or prior. No args shows the newest. One hook name shows that hook. --list enumerates every stored banner newest first.
4
+ tier: lite
5
+ user_invocable: true
6
+ disable_model_invocation: false
7
+ authoring_luminaries:
8
+ primary: john-ousterhout
9
+ supporting:
10
+ - tony-hoare
11
+ - michael-feathers
12
+ composes_with: []
13
+ ---
14
+
15
+ # /last-block
16
+
17
+ ## What you get
18
+
19
+ An adopter who sees a BLOCK banner can retrieve the exact text later. No scrolling through the terminal. No grep over trace logs. Type `/last-block`. The banner prints.
20
+
21
+ ## When to use it
22
+
23
+ Three cases.
24
+
25
+ - You saw a BLOCK banner, scrolled past it, and want to re-read the recipe or override instruction.
26
+ - You want to file a ticket and need the exact BLOCK text as evidence.
27
+ - You want to see which hooks fired BLOCK during the prior session.
28
+
29
+ ## Usage
30
+
31
+ ```
32
+ /last-block # print the newest stored banner
33
+ /last-block <hook-name> # print the banner stored under <hook-name>
34
+ /last-block --list # list every stored banner newest first
35
+ ```
36
+
37
+ ## How it works
38
+
39
+ Every BLOCK banner printed by `blocked_banner()` or `guardrail_banner()` persists a marker at `state/markers/last-block/<hook>.marker`. Per-hook markers overwrite on each fire. The lib `lib/last-block.sh` reads them back.
40
+
41
+ Marker format is plaintext:
42
+
43
+ ```
44
+ hook: <hook-name>
45
+ severity: blocking|advisory|info|guardrail
46
+ fired_at: <ISO-8601 UTC>
47
+ session: <session-id or unknown>
48
+ ---
49
+ <verbatim banner text, truncated at 8KB>
50
+ ```
51
+
52
+ ## Procedure
53
+
54
+ 1. Source `lib/last-block.sh`.
55
+ 2. If `--list` arg, call `list_last_blocks` and print to stdout.
56
+ 3. If a hook-name arg, call `read_last_block <hook>` and print.
57
+ 4. Else call `read_last_block` and print the newest.
58
+ 5. Exit 0 on hit, 1 on miss.
59
+
60
+ ## Example
61
+
62
+ ```
63
+ $ /last-block
64
+ hook: pre-commit-gate
65
+ severity: blocking
66
+ fired_at: 2026-10-04T15:21:08Z
67
+ session: 2026-10-04-slot-6
68
+ ---
69
+ 🛑 BLOCKED: path contains operator identifier /Users/<name>/...
70
+ Override: SKIP_OPERATOR_PATHS=1 git commit ...
71
+ ```
72
+
73
+ ## FAQ
74
+
75
+ ### What hooks persist markers?
76
+
77
+ Any hook that calls `blocked_banner()` or `guardrail_banner()` from `session-reflection.sh`. ~27 call sites today. Hooks that fire BLOCK via plain `echo` to stderr are a follow-on retrofit.
78
+
79
+ ### Does the marker persist across machines?
80
+
81
+ No. Markers live in `state/markers/last-block/` which bassclef-upstream gitignores. The marker survives the session that wrote it; it does not sync to teammates.
82
+
83
+ ### Can I clear markers?
84
+
85
+ Yes — delete `state/markers/last-block/<hook>.marker` or the whole directory. Next BLOCK fire re-populates.
86
+
87
+ ### What if the lib is missing?
88
+
89
+ The banner helpers detect missing lib and skip the record call. Banners still print. The marker is just not written. `/last-block` reports "no block recorded".
90
+
91
+ ## Refs
92
+
93
+ - Ticket: bassclef-upstream#2036 Finding #10
94
+ - UC: `docs/use-cases/UC-skill-last-block.md`
95
+ - Rule: `.claude/rules/last-block-marker.md`
96
+ - Lib: `lib/last-block.sh`
97
+ - Sister rule: `.claude/rules/blocked-items.md`
98
+ - Luminary: `.claude/luminaries/john-ousterhout.md` (deep module)
99
+ - /rfc outside lenses: `.claude/luminaries/don-norman.md` + `.claude/luminaries/steve-krug.md` + `.claude/luminaries/alan-cooper.md`
@@ -379,7 +379,7 @@ Produces `InputArtifact` at `docs/input-artifacts/<sprint-slug>-input.json`. Dow
379
379
  ### Phase 2 — `/objectory-decompose` (full BCE matrix)
380
380
 
381
381
  Same as `/stage` Phase 2 — Jacobson method via `/objectory-decompose`.
382
- Output: `docs/decompositions/<slug>-domain.md` with entity inventory
382
+ Output: `docs/decompositions/<sprint-slug>-input-domain.md` with entity inventory
383
383
  + actor list + verb-goal pairs + BCE matrix.
384
384
 
385
385
  **Distinction from `/stage`**: medium + full consume the FULL BCE
@@ -500,7 +500,7 @@ map derived from BCE entities.
500
500
  /interaction-design # consumes /ia-model + /use-case
501
501
  ```
502
502
 
503
- Output: state diagrams + sequence diagrams in `docs/interaction-design/<slug>/`.
503
+ Output: state diagrams + sequence diagrams in `docs/interaction-designs/<slug>.md` (per sunj-labs/bassclef-cli#317).
504
504
 
505
505
  ### Phase 8 — `/decompose` (GRASP responsibility assignment)
506
506
 
@@ -552,12 +552,18 @@ to produce reviews per perspective. Reviews surface in PR body.
552
552
  /ux-migration <chosen-variant-slug> # if base= provided, use that; else operator chooses post-gallery
553
553
  ```
554
554
 
555
- Output: `docs/specs/ux-migration-<slug>.md` — token extraction +
556
- prototype-HTML→React mapping table + work packages with dependency-aware
557
- build order.
555
+ <!-- FKGL-EXEMPT: cli#324 cure — technical precision required naming paired SKILL steps + file paths -->
558
556
 
559
- This is the `/ux-migration` MUST gate per existing prototype-variants
560
- SKILL.md §"Step 7 — Selection → pipeline to Construction".
557
+ Output: `docs/specs/ux-migration-<slug>.md` carries token extraction, prototype-to-React mapping, and work packages.
558
+
559
+ **When to run full `/ux-migration` vs partial (cli#324):**
560
+
561
+ - **Existing UI (redesign).** Run the full skill. The `/ux-migration` SKILL names this a MUST for redesigns.
562
+ - **Greenfield (no existing UI).** Run only Step 3b and Step 3c. Step 3b extracts design tokens. Step 3c maps prototype HTML to React components. The `/ux-migration` SKILL says skip the full migration for greenfield.
563
+
564
+ Pick by project state. Does `src/app/` carry a shipping UI? Yes means redesign. No means greenfield. When unsure, default to greenfield partial. The full gate's output is a component diff. A component diff has no value on an empty tree.
565
+
566
+ <!-- /FKGL-EXEMPT -->
561
567
 
562
568
  ### Phase 12 — Deploy or local-serve (mode-gated)
563
569
 
@@ -621,9 +627,9 @@ status != build-ready.
621
627
  ### Phase 14 — Scaffold Construction iteration-goal draft
622
628
 
623
629
  ```yaml
624
- # docs/iteration-bets/<YYYY-MM-DD>-<sprint-slug>-construction.md (DRAFT)
630
+ # docs/iteration-bets/<sprint-slug>-construction.md (DRAFT) # cli#326
625
631
  ---
626
- slug: <YYYY-MM-DD>-<sprint-slug>-construction
632
+ slug: <sprint-slug>-construction
627
633
  phase: Construction
628
634
  status: draft # operator must change to "active" before /build-iteration runs
629
635
  parent_roadmap: docs/roadmaps/<roadmap-slug>.md # if known
@@ -637,8 +643,8 @@ references:
637
643
  - {type: spec, id: <sprint-slug>}
638
644
  - {type: decomposition, id: <sprint-slug>-grasp}
639
645
  - {type: input-artifact, id: <sprint-slug>-input}
640
- appetite:
641
- mode: scope-bounded
646
+ time_budget:
647
+ mode: fixed-scope
642
648
  estimated_turns: <derived from /spec step enumeration>
643
649
  stopping_criteria:
644
650
  - All step PRs filed (count from spec)
@@ -677,7 +683,7 @@ appetite:
677
683
  **Viability signal**:
678
684
  (derived from /spec acceptance criteria; operator confirms)
679
685
 
680
- **Appetite**:
686
+ **Time budget**:
681
687
  (derived from spec step enumeration; operator finalizes)
682
688
  ```
683
689
 
@@ -57,12 +57,6 @@ source lib/state.sh
57
57
  INPUT=$(state_reference_get input-artifact "<slug>")
58
58
  ```
59
59
 
60
- Or via wrapper:
61
-
62
- ```bash
63
- INPUT=$(bash scripts/state.sh get reference input-artifact "<slug>")
64
- ```
65
-
66
60
  **Required fields** (extract via `jq`):
67
61
  - `extracted_text` — the load-bearing input for analysis
68
62
  - `type` — guides extraction (text vs. URL vs. image have different signals)
@@ -164,18 +158,12 @@ phase: jacobson # marks Phase B MVP scope; cockburn is follow-up
164
158
 
165
159
  ### 4. Validate output
166
160
 
167
- After writing, the state-validate.sh hook validates the frontmatter
168
- against `decomposition.schema.json`. If invalid, the write BLOCKs;
169
- fix frontmatter and retry.
170
-
171
- Or invoke explicitly:
172
-
173
- ```bash
174
- bash scripts/state.sh validate docs/decompositions/<slug>-domain.md
175
- ```
161
+ The state-validate.sh hook runs on write. It checks the frontmatter
162
+ against `decomposition.schema.json`. A failed check BLOCKs the write.
163
+ Fix the frontmatter and retry.
176
164
 
177
- Exit 0 = clean; exit 3 = schema violation (BLOCK message names the
178
- field).
165
+ Exit 0 = clean. Exit 3 = schema violation. The BLOCK message names
166
+ the field.
179
167
 
180
168
  ### 5. Surface to next step
181
169
 
@@ -101,10 +101,11 @@ if a required input is missing:
101
101
  1. **Personas** — `docs/personas/*.md` (preferred) or
102
102
  `docs/design/personas/*.md` (legacy). Use-case's **primary
103
103
  actor** MUST be a persona slug, not "user."
104
- 2. **JTBDs** — `docs/design/personas/[persona]/jtbds.md` or
105
- equivalent. Each use case has a single **goal** that traces to
106
- a JTBD outcome. Halt if no JTBDs for the scope in question
107
- (warn the operator; offer to run `/jtbd-tasks` first).
104
+ 2. **JTBDs** — `docs/jtbd/*.md` (preferred, per cli#317) or
105
+ `docs/design/personas/[persona]/jtbds.md` (legacy fallback).
106
+ Each use case has a single **goal** that traces to a JTBD
107
+ outcome. Halt if no JTBDs for the scope in question (warn the
108
+ operator; offer to run `/jtbd-tasks` first).
108
109
  3. **Value Prop Canvas** (optional) — `docs/value-prop-canvas/*.md`.
109
110
  When present, Pain-Relievers + Gain-Creators ground the "why
110
111
  this goal matters" framing in the stakeholders+interests block.
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "v1.6.6",
3
- "marker_tag": "release-2026-10-05-484c6f3e",
4
- "release_date": "2026-10-05T03:42:33Z",
5
- "release_sha": "484c6f3e",
6
- "release_tag": "release-2026-10-05-484c6f3e"
2
+ "version": "v1.8.0",
3
+ "marker_tag": "release-2026-10-06-a874c748",
4
+ "release_date": "2026-10-06T20:56:57Z",
5
+ "release_sha": "a874c748",
6
+ "release_tag": "v1.8.0"
7
7
  }
@@ -269,6 +269,18 @@ composer_preflight_check() {
269
269
  echo "check_6_message=hosting_platform=none + --no-launch (variants-only run)"
270
270
  echo "check_6_recommendation="
271
271
  pass=$((pass + 1))
272
+ elif [ "$skill_name" = "build" ]; then
273
+ # cli#309 cure — /build has no --no-launch flag and its Phase 7 deploy
274
+ # is a documented stub (see .claude/skills/build/SKILL.md Phase 7). Warn
275
+ # (not fail) so dispatch proceeds; Phase 7 will emit a TODO rather than
276
+ # run a real deploy. Adopter can set a real hosting_platform when ready
277
+ # to use Phase 7 live.
278
+ # Anchor: @luminary tony-hoare (postcondition per skill) +
279
+ # @luminary michael-nygard (fail-soft when it does not matter).
280
+ echo "check_6_status=warn"
281
+ echo "check_6_message=hosting_platform=none — /build Phase 7 deploy is a stub; dispatch proceeds, deploy emits TODO instead of running"
282
+ echo "check_6_recommendation=Set hosting_platform to a deploy provider (amplify) when ready to live-deploy; see .claude/skills/build/SKILL.md Phase 7"
283
+ warn=$((warn + 1))
272
284
  else
273
285
  echo "check_6_status=fail"
274
286
  echo "check_6_message=hosting_platform=none — composer would generate variants that can't launch"
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env bash
2
+ # tier: lite
3
+ # lib/last-block.sh — persist and read BLOCK banners.
4
+ #
5
+ # Per @luminary john-ousterhout deep module: three narrow functions hide
6
+ # the plaintext marker format. Callers persist, read, and list without
7
+ # touching file layout.
8
+ #
9
+ # Exposed functions:
10
+ # record_last_block <hook> <severity> <message> — write a marker
11
+ # read_last_block [<hook>] — read one marker (newest if no arg)
12
+ # list_last_blocks — list every marker newest first
13
+ #
14
+ # Marker format (plaintext, grep-friendly):
15
+ # hook: <hook-name>
16
+ # severity: blocking|advisory|info
17
+ # fired_at: <ISO-8601 UTC>
18
+ # session: <session-id or "unknown">
19
+ # ---
20
+ # <verbatim banner text, truncated to 8KB with [truncated] marker>
21
+ #
22
+ # Fail-soft: every write path returns 0 even on failure; stderr WARN
23
+ # describes the problem. Per @luminary michael-nygard — a persistence lib
24
+ # must never crash its caller.
25
+ #
26
+ # Per bassclef-upstream#2036 Finding #10 + UC-skill-last-block.md.
27
+
28
+ # Resolve the marker directory. Caller overrides via LAST_BLOCK_DIR for
29
+ # tests. Default is state/markers/last-block/ under CLAUDE_PROJECT_DIR
30
+ # (fallback to current dir when unset).
31
+ _last_block_dir() {
32
+ if [ -n "${LAST_BLOCK_DIR:-}" ]; then
33
+ echo "$LAST_BLOCK_DIR"
34
+ else
35
+ echo "${CLAUDE_PROJECT_DIR:-.}/state/markers/last-block"
36
+ fi
37
+ }
38
+
39
+ # record_last_block <hook> <severity> <message>
40
+ # Writes a marker. Fail-soft on every error (returns 0; WARN to stderr).
41
+ record_last_block() {
42
+ local hook="${1:-unknown}"
43
+ local severity="${2:-blocking}"
44
+ local message="${3:-}"
45
+ local dir
46
+ dir="$(_last_block_dir)"
47
+
48
+ # mkdir -p fail-soft — read-only disk, permission denied, etc.
49
+ if ! mkdir -p "$dir" 2>/dev/null; then
50
+ echo "record_last_block: cannot create '$dir' — marker not written" >&2
51
+ return 0
52
+ fi
53
+
54
+ # Truncate banner text to 8192 bytes to bound marker size.
55
+ local max_bytes=8192
56
+ local truncated=""
57
+ local body_bytes
58
+ body_bytes=$(printf '%s' "$message" | wc -c | tr -d ' ')
59
+ if [ "$body_bytes" -gt "$max_bytes" ]; then
60
+ message="$(printf '%s' "$message" | head -c "$max_bytes")"
61
+ truncated=$'\n[truncated — original was '"$body_bytes"' bytes]'
62
+ fi
63
+
64
+ local ts
65
+ ts="$(date -u +'%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo 'unknown')"
66
+ local session_id="${CLAUDE_SESSION_ID:-unknown}"
67
+ local marker="$dir/${hook}.marker"
68
+ local tmp="${marker}.tmp.$$"
69
+
70
+ {
71
+ printf 'hook: %s\n' "$hook"
72
+ printf 'severity: %s\n' "$severity"
73
+ printf 'fired_at: %s\n' "$ts"
74
+ printf 'session: %s\n' "$session_id"
75
+ printf '%s\n' '---'
76
+ printf '%s%s\n' "$message" "$truncated"
77
+ } > "$tmp" 2>/dev/null || {
78
+ echo "record_last_block: write to '$tmp' failed — marker not written" >&2
79
+ rm -f "$tmp" 2>/dev/null
80
+ return 0
81
+ }
82
+
83
+ if ! mv "$tmp" "$marker" 2>/dev/null; then
84
+ echo "record_last_block: rename to '$marker' failed — marker not written" >&2
85
+ rm -f "$tmp" 2>/dev/null
86
+ return 0
87
+ fi
88
+
89
+ return 0
90
+ }
91
+
92
+ # read_last_block [<hook>]
93
+ # Prints a marker's banner. With a hook arg, reads that marker. With no
94
+ # args, reads the newest marker by mtime. Returns 1 when no marker found.
95
+ read_last_block() {
96
+ local dir
97
+ dir="$(_last_block_dir)"
98
+
99
+ if [ ! -d "$dir" ]; then
100
+ echo "no block recorded (no markers directory at $dir)" >&2
101
+ return 1
102
+ fi
103
+
104
+ local marker
105
+ if [ $# -ge 1 ] && [ -n "$1" ]; then
106
+ marker="$dir/${1}.marker"
107
+ if [ ! -f "$marker" ]; then
108
+ echo "no block recorded for '$1'" >&2
109
+ return 1
110
+ fi
111
+ else
112
+ # Newest by mtime. ls -t is intentional (sort-by-mtime is its purpose).
113
+ # shellcheck disable=SC2012
114
+ marker="$(ls -t "$dir"/*.marker 2>/dev/null | head -1)"
115
+ if [ -z "$marker" ]; then
116
+ echo "no block recorded this session or prior" >&2
117
+ return 1
118
+ fi
119
+ fi
120
+
121
+ cat "$marker"
122
+ return 0
123
+ }
124
+
125
+ # list_last_blocks
126
+ # Lists every marker newest first — one line per marker with hook name,
127
+ # severity, timestamp. Returns 1 when the directory is empty or absent.
128
+ list_last_blocks() {
129
+ local dir
130
+ dir="$(_last_block_dir)"
131
+
132
+ if [ ! -d "$dir" ]; then
133
+ echo "no blocks recorded (no markers directory at $dir)" >&2
134
+ return 1
135
+ fi
136
+
137
+ local markers
138
+ # shellcheck disable=SC2012
139
+ markers="$(ls -t "$dir"/*.marker 2>/dev/null)"
140
+ if [ -z "$markers" ]; then
141
+ echo "no blocks recorded (directory empty)" >&2
142
+ return 1
143
+ fi
144
+
145
+ local m hook severity fired_at
146
+ echo "$markers" | while IFS= read -r m; do
147
+ hook="$(awk -F': ' '/^hook:/{print $2; exit}' "$m" 2>/dev/null)"
148
+ severity="$(awk -F': ' '/^severity:/{print $2; exit}' "$m" 2>/dev/null)"
149
+ fired_at="$(awk -F': ' '/^fired_at:/{print $2; exit}' "$m" 2>/dev/null)"
150
+ printf '%s\t%s\t%s\n' "$hook" "$severity" "$fired_at"
151
+ done
152
+ return 0
153
+ }
@@ -1090,6 +1090,17 @@ state_reference_get() {
1090
1090
  marker)
1091
1091
  state_marker_get "$id"
1092
1092
  ;;
1093
+ input-artifact)
1094
+ # InputArtifact — Shape C (pure JSON) per state-spine.md L262.
1095
+ # Added per sunj-labs/bassclef-cli#315: /objectory-decompose Step 1
1096
+ # reads input-artifact via this accessor; without a case the call
1097
+ # exited 5 and the SKILL could not read the artifact in lite.
1098
+ if [ -f "docs/input-artifacts/${id}.json" ]; then
1099
+ cat "docs/input-artifacts/${id}.json"
1100
+ else
1101
+ return 1
1102
+ fi
1103
+ ;;
1093
1104
  gate)
1094
1105
  # Gates derive from Markers; not addressable directly in v0
1095
1106
  echo "state_reference_get: gate type derives from Marker; not directly addressable in v0" >&2
@@ -52,12 +52,14 @@ SOURCE=""
52
52
  TYPE="auto"
53
53
  PERSIST=0
54
54
  SLUG=""
55
+ INTENT="" # cli#319 cure — operator intent text; defaults to SOURCE in text mode
55
56
 
56
57
  while [ $# -gt 0 ]; do
57
58
  case "$1" in
58
59
  --type=*) TYPE="${1#--type=}"; shift ;;
59
60
  --persist) PERSIST=1; shift ;;
60
61
  --slug=*) SLUG="${1#--slug=}"; shift ;;
62
+ --intent=*) INTENT="${1#--intent=}"; shift ;; # cli#319
61
63
  --help|-h) usage; exit 0 ;;
62
64
  --*) echo "interpret-input: unknown flag '$1'" >&2; usage >&2; exit 2 ;;
63
65
  *)
@@ -117,7 +119,15 @@ fi
117
119
 
118
120
  NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
119
121
 
120
- STRUCTURAL_HINTS='{}'
122
+ # cli#319 cure — default INTENT to SOURCE in text mode; empty otherwise.
123
+ # /pick-luminaries matches intent against the luminary catalog; an empty
124
+ # intent is explicitly valid (adopters may inject it later), but text input
125
+ # carries operator intent in SOURCE already — reuse it as a safe default.
126
+ if [ -z "$INTENT" ] && [ "$TYPE" = "text" ]; then
127
+ INTENT="$SOURCE"
128
+ fi
129
+
130
+ STRUCTURAL_HINTS='{"verb_goal_pairs": []}'
121
131
 
122
132
  case "$TYPE" in
123
133
  text)
@@ -149,7 +159,7 @@ case "$TYPE" in
149
159
  if [ -z "$REPO_LOCAL" ]; then
150
160
  # Source can't be resolved; return Phase B-style stub (still valid InputArtifact)
151
161
  EXTRACTED_TEXT="deferred to Phase C"
152
- STRUCTURAL_HINTS='{}'
162
+ STRUCTURAL_HINTS='{"verb_goal_pairs": []}'
153
163
  else
154
164
  EXTRACT_OUT=$(bash "$REPO_ROOT/scripts/interpret-input/extract-repo.sh" "$REPO_LOCAL" 2>/dev/null)
155
165
  EXTRACT_RC=$?
@@ -160,10 +170,13 @@ case "$TYPE" in
160
170
 
161
171
  if [ "$EXTRACT_RC" != "0" ] || [ -z "$EXTRACT_OUT" ]; then
162
172
  EXTRACTED_TEXT="repo extraction returned no signal for $SOURCE"
163
- STRUCTURAL_HINTS='{}'
173
+ STRUCTURAL_HINTS='{"verb_goal_pairs": []}'
164
174
  else
165
175
  EXTRACTED_TEXT=$(echo "$EXTRACT_OUT" | jq -r '.extracted_text')
166
- STRUCTURAL_HINTS=$(echo "$EXTRACT_OUT" | jq -c '.structural_hints')
176
+ # cli#312 cure — merge default verb_goal_pairs: [] so downstream readers
177
+ # (/build, /launch templates) always find the key even when extract
178
+ # doesn't produce it. Extract wins on any key it does emit.
179
+ STRUCTURAL_HINTS=$(echo "$EXTRACT_OUT" | jq -c '{verb_goal_pairs: []} + .structural_hints')
167
180
  fi
168
181
  fi
169
182
  EXTERNAL_URLS='[]'
@@ -179,6 +192,7 @@ ARTIFACT=$(jq -n \
179
192
  --arg type "$TYPE" \
180
193
  --arg source "$SOURCE" \
181
194
  --arg extracted_text "$EXTRACTED_TEXT" \
195
+ --arg intent "$INTENT" \
182
196
  --arg created_at "$NOW" \
183
197
  --argjson external_urls "$EXTERNAL_URLS" \
184
198
  --argjson structural_hints "$STRUCTURAL_HINTS" \
@@ -187,6 +201,7 @@ ARTIFACT=$(jq -n \
187
201
  type: $type,
188
202
  source: $source,
189
203
  extracted_text: $extracted_text,
204
+ intent: $intent,
190
205
  structural_hints: $structural_hints,
191
206
  external_urls: $external_urls,
192
207
  created_at: $created_at