@thebassclef/lite 0.1.3 → 1.0.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/dist/cli.cjs +431 -135
- package/dist/cli.js +433 -137
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.bassclef-source.json +10 -0
- package/{substrate → dist/lite}/.claude/hooks/artifact-ingestion-gate.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/assert-verify-steering.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/bassclef-source-config-validate.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/bassclef-sync.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/compound-noun-scrub.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/kiss-expansion-inject.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/longrun-prep-compounding-sequence-check.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/plain-english-steering.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/post-skill-friction-check.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/post-skill-telemetry.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/pre-build-gate.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/pre-commit-gate.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/session-end.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/session-reflection.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/skill-body-grade-gate.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/skill-body-intent-drift.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/state-validate.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/substrate-clarity-gate.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/temperance-gate.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/testing-tier-enforce.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/turn-prose-grade-measure.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/turn-prose-kiss-check.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/vocabulary-migration-check.sh +0 -0
- package/{substrate → dist/lite}/.claude/hooks/whereami-utc-gate.sh +0 -0
- package/dist/lite/.claude/settings.json +212 -0
- package/dist/lite/CLAUDE.md +41 -0
- package/dist/lite/gitignore +58 -0
- package/dist/lite/standards/bassclef-wiring-manifest.json +497 -0
- package/dist/lite/whereami.md +24 -0
- package/package.json +8 -2
- package/substrate/.bassclef/lite-manifest.json +0 -2701
- package/substrate/.claude/agents/architect.md +0 -70
- package/substrate/.claude/agents/builder.md +0 -114
- package/substrate/.claude/agents/designer.md +0 -156
- package/substrate/.claude/agents/reviewer.md +0 -88
- package/substrate/.claude/hooks/longrun-prep-compounding-axis-check.sh +0 -492
- package/substrate/.claude/hooks/skill-step-list-check.sh +0 -171
- package/substrate/.claude/luminaries/alan-cooper.md +0 -170
- package/substrate/.claude/luminaries/alistair-cockburn.md +0 -140
- package/substrate/.claude/luminaries/amazon-pr-faq.md +0 -34
- package/substrate/.claude/luminaries/ash-maurya.md +0 -121
- package/substrate/.claude/luminaries/bill-buxton.md +0 -210
- package/substrate/.claude/luminaries/charles-sanders-peirce.md +0 -150
- package/substrate/.claude/luminaries/david-ogilvy.md +0 -192
- package/substrate/.claude/luminaries/don-norman.md +0 -173
- package/substrate/.claude/luminaries/edward-tufte.md +0 -179
- package/substrate/.claude/luminaries/eric-evans.md +0 -160
- package/substrate/.claude/luminaries/frederick-brooks.md +0 -50
- package/substrate/.claude/luminaries/gang-of-four.md +0 -157
- package/substrate/.claude/luminaries/glenford-myers.md +0 -230
- package/substrate/.claude/luminaries/hunt-thomas.md +0 -115
- package/substrate/.claude/luminaries/hyrum-wright.md +0 -94
- package/substrate/.claude/luminaries/jason-fried-dhh.md +0 -46
- package/substrate/.claude/luminaries/jesse-james-garrett.md +0 -154
- package/substrate/.claude/luminaries/john-ousterhout.md +0 -94
- package/substrate/.claude/luminaries/karl-popper.md +0 -132
- package/substrate/.claude/luminaries/kent-beck.md +0 -168
- package/substrate/.claude/luminaries/linus-torvalds.md +0 -218
- package/substrate/.claude/luminaries/martin-fowler.md +0 -164
- package/substrate/.claude/luminaries/michael-feathers.md +0 -125
- package/substrate/.claude/luminaries/michael-nygard.md +0 -118
- package/substrate/.claude/luminaries/robert-c-martin.md +0 -164
- package/substrate/.claude/luminaries/saltzer-schroeder.md +0 -194
- package/substrate/.claude/luminaries/sophia-prater.md +0 -193
- package/substrate/.claude/luminaries/stephen-toulmin.md +0 -131
- package/substrate/.claude/luminaries/tony-hoare.md +0 -170
- package/substrate/.claude/luminaries/vaughn-vernon.md +0 -50
- package/substrate/.claude/luminaries/w-edwards-deming.md +0 -158
- package/substrate/.claude/rules/accessor-library-discipline.md +0 -138
- package/substrate/.claude/rules/adr-discipline.md +0 -120
- package/substrate/.claude/rules/api-conventions.md +0 -125
- package/substrate/.claude/rules/artifact-ingestion.md +0 -179
- package/substrate/.claude/rules/assert-only-after-verify.md +0 -137
- package/substrate/.claude/rules/blocked-items.md +0 -146
- package/substrate/.claude/rules/bootstrap-pair-discipline.md +0 -141
- package/substrate/.claude/rules/branching.md +0 -28
- package/substrate/.claude/rules/cold-adopter-harness-discipline.md +0 -129
- package/substrate/.claude/rules/commit-conventions.md +0 -22
- package/substrate/.claude/rules/compounding-axis-fresh-analysis.md +0 -188
- package/substrate/.claude/rules/compounding-sequence-fresh-analysis.md +0 -188
- package/substrate/.claude/rules/context-engineering.md +0 -202
- package/substrate/.claude/rules/context-management.md +0 -85
- package/substrate/.claude/rules/defensive-bash.md +0 -68
- package/substrate/.claude/rules/deferred-actions.md +0 -233
- package/substrate/.claude/rules/destructive-operations.md +0 -69
- package/substrate/.claude/rules/diagnosis.md +0 -38
- package/substrate/.claude/rules/github-issue-flash-tweet.md +0 -156
- package/substrate/.claude/rules/guardrails.md +0 -73
- package/substrate/.claude/rules/hook-wire-on-author.md +0 -103
- package/substrate/.claude/rules/identifier-leak-prevention.md +0 -104
- package/substrate/.claude/rules/iteration-bet-brief-completeness.md +0 -54
- package/substrate/.claude/rules/lite-manifest-schema-change-discipline.md +0 -98
- package/substrate/.claude/rules/longrun-prep-plan-doc-compression.md +0 -89
- package/substrate/.claude/rules/loop-discipline.md +0 -81
- package/substrate/.claude/rules/manual-prod-approval.md +0 -100
- package/substrate/.claude/rules/marker-enrichment-discipline.md +0 -99
- package/substrate/.claude/rules/mobile-ephemeral-session.md +0 -109
- package/substrate/.claude/rules/new-dependency-check.md +0 -51
- package/substrate/.claude/rules/oo-ad-entry-point.md +0 -117
- package/substrate/.claude/rules/operator-facing-prose.md +0 -196
- package/substrate/.claude/rules/option-label-discipline.md +0 -108
- package/substrate/.claude/rules/pattern-annotation.md +0 -100
- package/substrate/.claude/rules/plain-english-discipline.md +0 -156
- package/substrate/.claude/rules/plan-enumeration-needs-value-props.md +0 -211
- package/substrate/.claude/rules/pr-body-shape.md +0 -317
- package/substrate/.claude/rules/pr-strategy.md +0 -167
- package/substrate/.claude/rules/pr-title-shape.md +0 -161
- package/substrate/.claude/rules/prototype-workflow.md +0 -65
- package/substrate/.claude/rules/reserved-skill-names.md +0 -123
- package/substrate/.claude/rules/schema-management.md +0 -49
- package/substrate/.claude/rules/sdlc-gates.md +0 -149
- package/substrate/.claude/rules/security.md +0 -37
- package/substrate/.claude/rules/session-artifacts.md +0 -236
- package/substrate/.claude/rules/skill-composition-declarations.md +0 -124
- package/substrate/.claude/rules/skill-description-clarity.md +0 -247
- package/substrate/.claude/rules/skill-procedure-step-list.md +0 -137
- package/substrate/.claude/rules/state-schema-validation.md +0 -162
- package/substrate/.claude/rules/stuck-signal-diagnostic.md +0 -140
- package/substrate/.claude/rules/substrate-config-schema.md +0 -98
- package/substrate/.claude/rules/test-list-discipline.md +0 -175
- package/substrate/.claude/rules/test-sufficiency.md +0 -210
- package/substrate/.claude/rules/testing-tier-config.md +0 -145
- package/substrate/.claude/rules/testing.md +0 -38
- package/substrate/.claude/rules/turn-estimate-grounding.md +0 -134
- package/substrate/.claude/rules/visual-hierarchy.md +0 -437
- package/substrate/.claude/rules/we-dont-break-adopters.md +0 -126
- package/substrate/.claude/rules/whereami-load-bearing.md +0 -202
- package/substrate/.claude/rules/writing-craft-discipline.md +0 -92
- package/substrate/.claude/rules/wu-sequencing-compounds.md +0 -145
- package/substrate/.claude/skills/build/SKILL.md +0 -640
- package/substrate/.claude/skills/chronicle/SKILL.md +0 -55
- package/substrate/.claude/skills/clean-artifacts/SKILL.md +0 -249
- package/substrate/.claude/skills/decompose/SKILL.md +0 -280
- package/substrate/.claude/skills/diagnose/SKILL.md +0 -297
- package/substrate/.claude/skills/feynman/SKILL.md +0 -90
- package/substrate/.claude/skills/howdoi/SKILL.md +0 -105
- package/substrate/.claude/skills/ia-model/SKILL.md +0 -108
- package/substrate/.claude/skills/interaction-design/SKILL.md +0 -112
- package/substrate/.claude/skills/interpret-input/SKILL.md +0 -180
- package/substrate/.claude/skills/journal/SKILL.md +0 -209
- package/substrate/.claude/skills/kiss/SKILL.md +0 -449
- package/substrate/.claude/skills/launch/SKILL.md +0 -915
- package/substrate/.claude/skills/lean-canvas/SKILL.md +0 -332
- package/substrate/.claude/skills/longrun/SKILL.md +0 -463
- package/substrate/.claude/skills/luminary/SKILL.md +0 -481
- package/substrate/.claude/skills/ogilvy-writing-audit/SKILL.md +0 -177
- package/substrate/.claude/skills/onboard-repo/SKILL.md +0 -1624
- package/substrate/.claude/skills/pattern-review/SKILL.md +0 -99
- package/substrate/.claude/skills/personas/SKILL.md +0 -207
- package/substrate/.claude/skills/promote/SKILL.md +0 -283
- package/substrate/.claude/skills/requirement/SKILL.md +0 -98
- package/substrate/.claude/skills/retro/SKILL.md +0 -117
- package/substrate/.claude/skills/riff/SKILL.md +0 -114
- package/substrate/.claude/skills/roadmap-reconcile/SKILL.md +0 -163
- package/substrate/.claude/skills/session-end/SKILL.md +0 -309
- package/substrate/.claude/skills/session-log/SKILL.md +0 -299
- package/substrate/.claude/skills/skills/SKILL.md +0 -228
- package/substrate/.claude/skills/spec/SKILL.md +0 -105
- package/substrate/.claude/skills/sprint/SKILL.md +0 -392
- package/substrate/.claude/skills/stage/SKILL.md +0 -384
- package/substrate/.claude/skills/state-a-problem/SKILL.md +0 -185
- package/substrate/.claude/skills/temperance/SKILL.md +0 -108
- package/substrate/.claude/skills/use-case/SKILL.md +0 -417
- package/substrate/.claude/skills/user-stories/SKILL.md +0 -268
- package/substrate/.claude/skills/value-prop/SKILL.md +0 -251
- package/substrate/.claude/skills/verify/SKILL.md +0 -160
- package/substrate/.claude/skills/visual-review/SKILL.md +0 -503
- package/substrate/.claude/skills/whats-the-plan/SKILL.md +0 -202
- package/substrate/.claude/skills/whereami/SKILL.md +0 -307
- package/substrate/AGENTS.md +0 -79
- package/substrate/CLAUDE-lite.md +0 -85
- package/substrate/CODE_OF_CONDUCT.md +0 -28
- package/substrate/CONTRIBUTING.md +0 -177
- package/substrate/README.md +0 -173
- package/substrate/SECURITY.md +0 -19
- package/substrate/architecture/decisions/ADR-029-release-pipeline.md +0 -79
- package/substrate/architecture/decisions/ADR-031-non-breaking-changes-adopter-discipline.md +0 -139
- package/substrate/architecture/decisions/ADR-032-adopter-sync-dispatcher-architecture.md +0 -192
- package/substrate/architecture/decisions/ADR-039-release-tagging-scheme.md +0 -145
- package/substrate/architecture/decisions/ADR-040-planning-skill-vocabulary-and-lite-profile.md +0 -155
- package/substrate/architecture/decisions/ADR-044-unified-skill-body-template.md +0 -162
- package/substrate/lib/clean-artifacts-sweep.sh +0 -112
- package/substrate/lib/code-comment-discipline.sh +0 -144
- package/substrate/lib/composer-preflight.sh +0 -459
- package/substrate/lib/hook-inject.sh +0 -255
- package/substrate/lib/luminary-pick.sh +0 -96
- package/substrate/lib/output-discipline.sh +0 -143
- package/substrate/lib/prose-scan-boundary.sh +0 -171
- package/substrate/lib/rewrite-check.sh +0 -214
- package/substrate/lib/state.sh +0 -1372
- package/substrate/lib/telemetry.sh +0 -205
- package/substrate/lib/tier-check.sh +0 -187
- package/substrate/lib/tier-dependency-audit.sh +0 -1088
- package/substrate/presence/install/bassclef-hook-connect.sh +0 -178
- package/substrate/presence/install/bassclef-sync.dispatcher.template.sh +0 -841
- package/substrate/presence/install/bassclef-sync.template.sh +0 -2076
- package/substrate/presence/install/schedule-auto-save.cron.sh +0 -88
- package/substrate/presence/install/schedule-auto-save.taskscheduler.md +0 -122
- package/substrate/scripts/aggregate-telemetry.sh +0 -217
- package/substrate/scripts/analyze-tier-dependencies.sh +0 -239
- package/substrate/scripts/generate-lite-manifest.sh +0 -505
- package/substrate/scripts/generate-tier-manifest.sh +0 -28
- package/substrate/scripts/intent-drift-check.sh +0 -456
- package/substrate/scripts/lite-manifest-drift-check.sh +0 -146
- package/substrate/scripts/render-lite-manifest-doc.sh +0 -150
- package/substrate/standards/adr-template.md +0 -86
- package/substrate/standards/api-conventions/nextjs.md +0 -84
- package/substrate/standards/artifact-composition.md +0 -209
- package/substrate/standards/bash-hook-safety.md +0 -246
- package/substrate/standards/bassclef-configs-schema.md +0 -232
- package/substrate/standards/bassclef-evolution.md +0 -143
- package/substrate/standards/bassclef-internal-jargon.md +0 -244
- package/substrate/standards/bassclef-managed-sentinel.md +0 -96
- package/substrate/standards/bassclef-source-config.md +0 -228
- package/substrate/standards/branch-stacking.md +0 -408
- package/substrate/standards/code-safety-principles.md +0 -176
- package/substrate/standards/composer-prerequisites.md +0 -155
- package/substrate/standards/deferred-actions-schema.md +0 -204
- package/substrate/standards/dependency-discipline/cargo.md +0 -39
- package/substrate/standards/dependency-discipline/gem.md +0 -43
- package/substrate/standards/dependency-discipline/go-mod.md +0 -41
- package/substrate/standards/dependency-discipline/npm.md +0 -42
- package/substrate/standards/dependency-discipline/pip.md +0 -42
- package/substrate/standards/deployment-topology/ec2-tailscale.md +0 -225
- package/substrate/standards/deployment-topology.md +0 -69
- package/substrate/standards/docs-sync-allowlist.md +0 -76
- package/substrate/standards/domain-and-dns.md +0 -145
- package/substrate/standards/frontend-stack.md +0 -67
- package/substrate/standards/frontmatter-schema.md +0 -154
- package/substrate/standards/graceful-exit.md +0 -227
- package/substrate/standards/hook-idempotency.md +0 -102
- package/substrate/standards/hook-injection-discipline.md +0 -202
- package/substrate/standards/hook-install-class.md +0 -215
- package/substrate/standards/input-handler-interface.md +0 -152
- package/substrate/standards/lite-manifest-schema-changes.md +0 -135
- package/substrate/standards/luminary-matching.md +0 -105
- package/substrate/standards/luminary-problem-patterns.md +0 -481
- package/substrate/standards/migration-discipline/active-record.md +0 -50
- package/substrate/standards/migration-discipline/alembic.md +0 -43
- package/substrate/standards/migration-discipline/gorm.md +0 -50
- package/substrate/standards/migration-discipline/prisma.md +0 -53
- package/substrate/standards/migration-discipline/sqlalchemy.md +0 -51
- package/substrate/standards/mobile-ephemeral-session.md +0 -167
- package/substrate/standards/model-routing-discipline.md +0 -160
- package/substrate/standards/ogilvy-writing-rules.md +0 -225
- package/substrate/standards/opener-discipline.md +0 -96
- package/substrate/standards/operator-facing-prose-discipline.md +0 -201
- package/substrate/standards/persona-schema.md +0 -229
- package/substrate/standards/pluggable-luminaries.md +0 -323
- package/substrate/standards/pr-body-discipline.md +0 -115
- package/substrate/standards/preview-state-schema.md +0 -189
- package/substrate/standards/project-directory-layout.md +0 -276
- package/substrate/standards/release-tagging.md +0 -137
- package/substrate/standards/reserved-skill-names.md +0 -120
- package/substrate/standards/scannable-multi-option-output.md +0 -261
- package/substrate/standards/sdlc-compliance.md +0 -286
- package/substrate/standards/sdlc-gates/typescript.md +0 -57
- package/substrate/standards/secrets-lifecycle.md +0 -210
- package/substrate/standards/security-scanner-adapter.md +0 -145
- package/substrate/standards/session-board.md +0 -256
- package/substrate/standards/skill-output-discipline.md +0 -90
- package/substrate/standards/state-spine-contract.md +0 -255
- package/substrate/standards/state-spine.md +0 -511
- package/substrate/standards/steering-hints/kiss-words.md +0 -11
- package/substrate/standards/substrate-config-schema.md +0 -267
- package/substrate/standards/tech-stack-config.md +0 -109
- package/substrate/standards/tier-dependency-analysis.md +0 -167
- package/substrate/standards/tier-runtime-deps/lite.md +0 -57
- package/substrate/standards/tier-tag-schema.md +0 -155
- package/substrate/standards/two-layer-config.md +0 -99
- package/substrate/standards/use-case-format.md +0 -292
- package/substrate/standards/user-story-invest.md +0 -268
- package/substrate/standards/velocity-and-appetite.md +0 -229
- package/substrate/standards/voice-input-pattern.md +0 -119
- package/substrate/standards/whereami-schema.md +0 -301
- package/substrate/standards/worktree-management.md +0 -211
- package/substrate/standards/writing-guide.md +0 -213
- package/substrate/templates/chronicle-template.md +0 -75
- package/substrate/templates/deferred-action-template.md +0 -45
- package/substrate/templates/memory-proposal-template.md +0 -77
- package/substrate/templates/persona-template.md +0 -200
- package/substrate/templates/pr-faq.md +0 -45
- package/substrate/templates/secret-rotation-template.md +0 -162
- package/substrate/templates/spec-template.md +0 -131
- package/substrate/templates/use-case-template.md +0 -194
- package/substrate/templates/user-story-template.md +0 -107
- package/substrate/templates/whereami-template.md +0 -101
package/substrate/lib/state.sh
DELETED
|
@@ -1,1372 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# tier: lite
|
|
3
|
-
# lib/state.sh — typed accessor library over bassclef's state spine.
|
|
4
|
-
#
|
|
5
|
-
# Repository pattern (Fowler PoEAA): hides storage backend (JSON-on-git
|
|
6
|
-
# for v0; cxdb for opt-in adopters per bassclef#300) from consumers.
|
|
7
|
-
# Adapter discipline preserves the swap surface — see exit criteria in
|
|
8
|
-
# standards/state-spine-contract.md §"Adapter discipline".
|
|
9
|
-
#
|
|
10
|
-
# Source via: source /path/to/lib/state.sh
|
|
11
|
-
#
|
|
12
|
-
# Naming convention: state_<entity>_<verb>[_<modifier>]
|
|
13
|
-
# Return semantics per state-spine-contract.md §"Return semantics":
|
|
14
|
-
# exit 0 = success, JSON/scalar to stdout
|
|
15
|
-
# exit 1 = not found (empty stdout)
|
|
16
|
-
# exit 2 = invalid input (stderr message)
|
|
17
|
-
# exit 3 = schema validation failed (stderr ajv error)
|
|
18
|
-
# exit 4 = filesystem error (stderr message)
|
|
19
|
-
# exit 5 = unsupported entity / verb (stderr message)
|
|
20
|
-
#
|
|
21
|
-
# Replay-determinism rule: counters/state derived from history on every
|
|
22
|
-
# call; never cache derived values as side state. Same input history →
|
|
23
|
-
# same output. (Per canvas v4 SWF mining.)
|
|
24
|
-
#
|
|
25
|
-
# Write-once-immutability rule (Helland): Markers, Chronicles, Sessions
|
|
26
|
-
# write-once. Updates produce NEW records via supersession; no in-place
|
|
27
|
-
# mutation. Forbidden verbs documented in contract.
|
|
28
|
-
|
|
29
|
-
# === Internal helpers ===
|
|
30
|
-
|
|
31
|
-
# __state_check_binary_deps — fail loud + early when required binaries
|
|
32
|
-
# are missing from $PATH. Per bassclef#598: previously, accessor calls
|
|
33
|
-
# emitted cryptic "command not found: awk / python3" errors with no
|
|
34
|
-
# resolution hint. Now they emit a structured error pointing at the
|
|
35
|
-
# install command.
|
|
36
|
-
#
|
|
37
|
-
# Idempotent: caches result in __STATE_DEPS_CHECKED so we don't grep
|
|
38
|
-
# PATH on every accessor call.
|
|
39
|
-
__state_check_binary_deps() {
|
|
40
|
-
if [ "${__STATE_DEPS_CHECKED:-}" = "1" ]; then
|
|
41
|
-
return 0
|
|
42
|
-
fi
|
|
43
|
-
local missing=""
|
|
44
|
-
for bin in awk python3 jq; do
|
|
45
|
-
command -v "$bin" >/dev/null 2>&1 || missing="$missing $bin"
|
|
46
|
-
done
|
|
47
|
-
if [ -n "$missing" ]; then
|
|
48
|
-
cat >&2 <<EOF
|
|
49
|
-
STATE ACCESSOR FAILURE
|
|
50
|
-
Required binary missing from PATH:$missing
|
|
51
|
-
|
|
52
|
-
These binaries are load-bearing for lib/state.sh accessors (Shape B
|
|
53
|
-
YAML frontmatter extraction + JSON tooling).
|
|
54
|
-
|
|
55
|
-
Resolution:
|
|
56
|
-
macOS: brew install gawk python3 jq
|
|
57
|
-
Debian: sudo apt-get install gawk python3 jq
|
|
58
|
-
|
|
59
|
-
Closes the bassclef#598 silent-failure class.
|
|
60
|
-
EOF
|
|
61
|
-
return 4
|
|
62
|
-
fi
|
|
63
|
-
export __STATE_DEPS_CHECKED=1
|
|
64
|
-
return 0
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
# yq2json — extract YAML frontmatter from markdown file, emit JSON.
|
|
68
|
-
# Used by Shape B (markdown + YAML frontmatter + body) entity accessors.
|
|
69
|
-
#
|
|
70
|
-
# Hoare postcondition (per #1497 silent-empty class cure):
|
|
71
|
-
# stdout is a valid JSON object with at least one field, OR
|
|
72
|
-
# exit code names the reason (1 file missing / 3 parse error /
|
|
73
|
-
# 4 deps missing / 5 empty parse).
|
|
74
|
-
#
|
|
75
|
-
# Exit 5 fires when parse succeeds but the result is empty — either no
|
|
76
|
-
# `---` fences in the file OR fences present with empty body between.
|
|
77
|
-
# The `or {}` coerce that used to mask this class is now explicit in the
|
|
78
|
-
# postcondition check.
|
|
79
|
-
#
|
|
80
|
-
# Adopter opt-in: STATE_ALLOW_EMPTY_PARSE=1 skips the postcondition,
|
|
81
|
-
# returns `{}` exit 0 (legacy behavior). Use during migration window.
|
|
82
|
-
#
|
|
83
|
-
# Anchor: @luminary tony-hoare (postcondition contract) +
|
|
84
|
-
# @luminary john-ousterhout (deep module — fix at helper covers all
|
|
85
|
-
# 3 accessors + migration script) + @luminary michael-nygard (fail-fast
|
|
86
|
-
# on invalid input) + @luminary michael-feathers (characterization
|
|
87
|
-
# tests RED-first via fixtures/2026-09-06-silent-empty/).
|
|
88
|
-
__state_yq2json() {
|
|
89
|
-
__state_check_binary_deps || return 4
|
|
90
|
-
local file="$1"
|
|
91
|
-
if [ ! -f "$file" ]; then
|
|
92
|
-
return 1
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
# Parse cache per bassclef-upstream#1519 (Option D). Cache the JSON
|
|
96
|
-
# output per file+mtime+size within one shell process. Session-end
|
|
97
|
-
# fires N Edit calls to whereami; each triggers state-validate.sh
|
|
98
|
-
# which calls this helper. Parse cost dominates on dense frontmatter.
|
|
99
|
-
# Uses parallel indexed arrays for bash 3+ compat (macOS default is
|
|
100
|
-
# bash 3.2; no associative-array assumption).
|
|
101
|
-
# Opt-out env: STATE_DISABLE_PARSE_CACHE=1 skips cache lookup + store.
|
|
102
|
-
# Fail-safe: any stat error falls through to parse path.
|
|
103
|
-
local cache_key=""
|
|
104
|
-
if [ "${STATE_DISABLE_PARSE_CACHE:-0}" != "1" ]; then
|
|
105
|
-
# Lazy-init arrays. Caller may run under `set -u` (nounset), which
|
|
106
|
-
# trips on `${#unset_array[@]}` — initialize first if not declared.
|
|
107
|
-
if [ -z "${__STATE_YQ2JSON_CACHE_KEYS+x}" ]; then
|
|
108
|
-
__STATE_YQ2JSON_CACHE_KEYS=()
|
|
109
|
-
__STATE_YQ2JSON_CACHE_VALS=()
|
|
110
|
-
fi
|
|
111
|
-
local mtime_size
|
|
112
|
-
mtime_size=$(stat -c '%Y|%s' "$file" 2>/dev/null || stat -f '%m|%z' "$file" 2>/dev/null || echo "")
|
|
113
|
-
if [ -n "$mtime_size" ]; then
|
|
114
|
-
cache_key="${file}|${mtime_size}"
|
|
115
|
-
# Linear scan of parallel arrays. N is small per session-end (< 10).
|
|
116
|
-
local __c_i=0
|
|
117
|
-
local __c_n=${#__STATE_YQ2JSON_CACHE_KEYS[@]}
|
|
118
|
-
while [ $__c_i -lt $__c_n ]; do
|
|
119
|
-
if [ "${__STATE_YQ2JSON_CACHE_KEYS[$__c_i]}" = "$cache_key" ]; then
|
|
120
|
-
printf '%s\n' "${__STATE_YQ2JSON_CACHE_VALS[$__c_i]}"
|
|
121
|
-
return 0
|
|
122
|
-
fi
|
|
123
|
-
__c_i=$((__c_i + 1))
|
|
124
|
-
done
|
|
125
|
-
fi
|
|
126
|
-
fi
|
|
127
|
-
|
|
128
|
-
local out
|
|
129
|
-
local ec
|
|
130
|
-
out=$(awk '/^---$/{c++; next} c==1' "$file" | python3 -c "
|
|
131
|
-
import sys, yaml, json
|
|
132
|
-
try:
|
|
133
|
-
data = yaml.safe_load(sys.stdin) or {}
|
|
134
|
-
print(json.dumps(data, default=str))
|
|
135
|
-
except Exception as e:
|
|
136
|
-
print(f'yaml parse failed: {e}', file=sys.stderr)
|
|
137
|
-
sys.exit(3)
|
|
138
|
-
")
|
|
139
|
-
ec=$?
|
|
140
|
-
# Instrument real-parse count for #1519 tests + observability.
|
|
141
|
-
__STATE_YQ2JSON_PARSE_COUNT=$(( ${__STATE_YQ2JSON_PARSE_COUNT:-0} + 1 ))
|
|
142
|
-
if [ "$ec" -ne 0 ]; then
|
|
143
|
-
printf '%s\n' "$out" >&2
|
|
144
|
-
return "$ec"
|
|
145
|
-
fi
|
|
146
|
-
# Hoare postcondition — stdout must be a non-empty JSON object.
|
|
147
|
-
# Empty parse means: no frontmatter fences OR fences with empty body.
|
|
148
|
-
# Both shapes silently coerced to `{}` before this cure (per #1497).
|
|
149
|
-
if [ "$out" = "{}" ]; then
|
|
150
|
-
if [ "${STATE_ALLOW_EMPTY_PARSE:-0}" = "1" ]; then
|
|
151
|
-
# Adopter opt-in — legacy behavior, no signal.
|
|
152
|
-
printf '%s\n' "$out"
|
|
153
|
-
return 0
|
|
154
|
-
fi
|
|
155
|
-
echo "state accessor: no YAML frontmatter fences OR empty payload in $file (expected Shape A JSON OR Shape B markdown+frontmatter with fields)" >&2
|
|
156
|
-
return 5
|
|
157
|
-
fi
|
|
158
|
-
# Store in parse cache on successful parse (bassclef-upstream#1519).
|
|
159
|
-
# Empty cache_key means STATE_DISABLE_PARSE_CACHE=1 OR stat failed —
|
|
160
|
-
# skip store. Parallel arrays; append via +=.
|
|
161
|
-
if [ -n "$cache_key" ]; then
|
|
162
|
-
__STATE_YQ2JSON_CACHE_KEYS+=("$cache_key")
|
|
163
|
-
__STATE_YQ2JSON_CACHE_VALS+=("$out")
|
|
164
|
-
fi
|
|
165
|
-
printf '%s\n' "$out"
|
|
166
|
-
return 0
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
# Atomic JSON write: temp file + rename.
|
|
170
|
-
__state_atomic_write() {
|
|
171
|
-
local target="$1"
|
|
172
|
-
local content="$2"
|
|
173
|
-
local tmp="${target}.tmp.$$"
|
|
174
|
-
echo "$content" > "$tmp" || return 4
|
|
175
|
-
mv "$tmp" "$target" || { rm -f "$tmp"; return 4; }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
# yaml_write — write a single {field: value} document as safe YAML.
|
|
179
|
-
# Value strings containing YAML-unsafe chars (`:`, `#`, `"`, `'`, newlines)
|
|
180
|
-
# get quoted or emitted as block scalars via python yaml.safe_dump.
|
|
181
|
-
# Round-trips cleanly through yaml.safe_load.
|
|
182
|
-
# Sister to __state_yq2json (read side); used by session-end.sh write path
|
|
183
|
-
# for the operator_recap block per goal 2026-09-06d Cure 6.
|
|
184
|
-
# Anchor: @luminary tony-hoare (postcondition — yaml.safe_load succeeds on
|
|
185
|
-
# output) + @luminary michael-nygard (silent-fail wrap; write does not
|
|
186
|
-
# crash caller).
|
|
187
|
-
__state_yaml_write() {
|
|
188
|
-
__state_check_binary_deps || return 4
|
|
189
|
-
local path="$1"
|
|
190
|
-
local field="$2"
|
|
191
|
-
local value="$3"
|
|
192
|
-
if [ -z "$path" ] || [ -z "$field" ]; then
|
|
193
|
-
echo "__state_yaml_write: requires <path> <field> <value>" >&2
|
|
194
|
-
return 2
|
|
195
|
-
fi
|
|
196
|
-
local dir
|
|
197
|
-
dir="$(dirname "$path")"
|
|
198
|
-
[ -d "$dir" ] || mkdir -p "$dir" || return 4
|
|
199
|
-
local tmp="${path}.tmp.$$"
|
|
200
|
-
BASSCLEF_YAML_FIELD="$field" \
|
|
201
|
-
BASSCLEF_YAML_VALUE="$value" \
|
|
202
|
-
BASSCLEF_YAML_TMP="$tmp" \
|
|
203
|
-
python3 -c '
|
|
204
|
-
import os, yaml, sys
|
|
205
|
-
field = os.environ["BASSCLEF_YAML_FIELD"]
|
|
206
|
-
value = os.environ["BASSCLEF_YAML_VALUE"]
|
|
207
|
-
tmp = os.environ["BASSCLEF_YAML_TMP"]
|
|
208
|
-
data = {field: value}
|
|
209
|
-
try:
|
|
210
|
-
with open(tmp, "w") as f:
|
|
211
|
-
yaml.safe_dump(data, f, allow_unicode=True, default_flow_style=False, width=10000)
|
|
212
|
-
except Exception as e:
|
|
213
|
-
print(f"__state_yaml_write: safe_dump failed: {e}", file=sys.stderr)
|
|
214
|
-
sys.exit(3)
|
|
215
|
-
' 2>&1 || { rm -f "$tmp"; return 3; }
|
|
216
|
-
mv "$tmp" "$path" || { rm -f "$tmp"; return 4; }
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
# === Whereami (Shape A — JSON authoritative + markdown rendered) ===
|
|
220
|
-
|
|
221
|
-
state_whereami_get() {
|
|
222
|
-
# Cure 2 (per ADR-054 D3 fallback discipline): three-tier read.
|
|
223
|
-
# Tier 0 — docs/whereami.json (authoritative when present)
|
|
224
|
-
# Tier 1 — docs/whereami.md YAML frontmatter (fallback via __state_yq2json)
|
|
225
|
-
# Tier 2 — return 1 only when both files missing
|
|
226
|
-
local json_path="docs/whereami.json"
|
|
227
|
-
local md_path="docs/whereami.md"
|
|
228
|
-
if [ -f "$json_path" ]; then
|
|
229
|
-
cat "$json_path"
|
|
230
|
-
return 0
|
|
231
|
-
fi
|
|
232
|
-
if [ -f "$md_path" ]; then
|
|
233
|
-
__state_yq2json "$md_path"
|
|
234
|
-
return $?
|
|
235
|
-
fi
|
|
236
|
-
return 1
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
state_whereami_set() {
|
|
240
|
-
local field="$1"
|
|
241
|
-
local value="$2"
|
|
242
|
-
local path="docs/whereami.json"
|
|
243
|
-
if [ -z "$field" ] || [ -z "$value" ]; then
|
|
244
|
-
echo "state_whereami_set: requires <field> <value>" >&2
|
|
245
|
-
return 2
|
|
246
|
-
fi
|
|
247
|
-
if [ ! -f "$path" ]; then
|
|
248
|
-
echo "state_whereami_set: docs/whereami.json does not exist" >&2
|
|
249
|
-
return 4
|
|
250
|
-
fi
|
|
251
|
-
local updated
|
|
252
|
-
# Use jq's setpath for nested field paths (e.g., 'phase.value')
|
|
253
|
-
updated="$(jq --arg path "$field" --arg val "$value" '
|
|
254
|
-
setpath($path | split("."); $val)
|
|
255
|
-
' "$path")" || return 3
|
|
256
|
-
__state_atomic_write "$path" "$updated"
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
# === IterationBet (Shape B — markdown + YAML frontmatter + body) ===
|
|
260
|
-
|
|
261
|
-
state_iteration_bet_get() {
|
|
262
|
-
local slug="$1"
|
|
263
|
-
if [ -z "$slug" ]; then
|
|
264
|
-
echo "state_iteration_bet_get: requires <slug>" >&2
|
|
265
|
-
return 2
|
|
266
|
-
fi
|
|
267
|
-
local path="docs/iteration-bets/${slug}.md"
|
|
268
|
-
if [ ! -f "$path" ]; then
|
|
269
|
-
return 1
|
|
270
|
-
fi
|
|
271
|
-
__state_yq2json "$path"
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
# Special-purpose: returns the single active iteration bet (status=active),
|
|
275
|
-
# or exit 1 if none. If multiple are active (parallel mode), returns the
|
|
276
|
-
# first by filename order (stable, sorted lexicographically).
|
|
277
|
-
state_iteration_bet_active() {
|
|
278
|
-
local found=""
|
|
279
|
-
if [ ! -d docs/iteration-bets ]; then
|
|
280
|
-
return 1
|
|
281
|
-
fi
|
|
282
|
-
for f in docs/iteration-bets/*.md; do
|
|
283
|
-
[ -f "$f" ] || continue
|
|
284
|
-
local status
|
|
285
|
-
status="$(__state_yq2json "$f" 2>/dev/null | jq -r '.status // empty')"
|
|
286
|
-
if [ "$status" = "active" ]; then
|
|
287
|
-
__state_yq2json "$f"
|
|
288
|
-
return 0
|
|
289
|
-
fi
|
|
290
|
-
done
|
|
291
|
-
return 1
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
# List all iteration bets. Returns JSON array of frontmatter objects to
|
|
295
|
-
# stdout; empty array `[]` if directory absent or empty. Stable order
|
|
296
|
-
# (lexicographic by filename — chronologically earliest first since slugs
|
|
297
|
-
# carry dates).
|
|
298
|
-
state_iteration_bet_list() {
|
|
299
|
-
if [ ! -d docs/iteration-bets ]; then
|
|
300
|
-
echo "[]"
|
|
301
|
-
return 0
|
|
302
|
-
fi
|
|
303
|
-
local result="["
|
|
304
|
-
local first=1
|
|
305
|
-
for f in docs/iteration-bets/*.md; do
|
|
306
|
-
[ -f "$f" ] || continue
|
|
307
|
-
local entry_json
|
|
308
|
-
entry_json="$(__state_yq2json "$f" 2>/dev/null)" || continue
|
|
309
|
-
[ -z "$entry_json" ] && continue
|
|
310
|
-
[ "$entry_json" = "{}" ] && continue
|
|
311
|
-
[ "$entry_json" = "null" ] && continue
|
|
312
|
-
if [ "$first" -eq 1 ]; then
|
|
313
|
-
first=0
|
|
314
|
-
else
|
|
315
|
-
result="${result},"
|
|
316
|
-
fi
|
|
317
|
-
result="${result}${entry_json}"
|
|
318
|
-
done
|
|
319
|
-
result="${result}]"
|
|
320
|
-
echo "$result" | jq -c .
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
# === Roadmap (Shape B — markdown + YAML frontmatter + body) ===
|
|
324
|
-
#
|
|
325
|
-
# Per bassclef#337: distributed agents coordinate on roadmap work via
|
|
326
|
-
# state management. Two primitives: last_event (singular — "where were we")
|
|
327
|
-
# + next_work_units (task list — "what's queued"). Workers claim atomically
|
|
328
|
-
# via state_roadmap_claim_wu, release on completion via state_roadmap_release_wu.
|
|
329
|
-
|
|
330
|
-
state_roadmap_get() {
|
|
331
|
-
local slug="$1"
|
|
332
|
-
if [ -z "$slug" ]; then
|
|
333
|
-
echo "state_roadmap_get: requires <slug>" >&2
|
|
334
|
-
return 2
|
|
335
|
-
fi
|
|
336
|
-
local path="docs/roadmaps/${slug}.md"
|
|
337
|
-
if [ ! -f "$path" ]; then
|
|
338
|
-
return 1
|
|
339
|
-
fi
|
|
340
|
-
__state_yq2json "$path"
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
# Internal: rewrite a roadmap's frontmatter from JSON, preserving the body.
|
|
344
|
-
__state_roadmap_write_frontmatter() {
|
|
345
|
-
local slug="$1"
|
|
346
|
-
local frontmatter_json="$2"
|
|
347
|
-
local path="docs/roadmaps/${slug}.md"
|
|
348
|
-
if [ ! -f "$path" ]; then
|
|
349
|
-
echo "__state_roadmap_write_frontmatter: roadmap $path not found" >&2
|
|
350
|
-
return 1
|
|
351
|
-
fi
|
|
352
|
-
python3 -c "
|
|
353
|
-
import sys, yaml, json
|
|
354
|
-
path = '$path'
|
|
355
|
-
fm = json.loads('''$frontmatter_json''')
|
|
356
|
-
with open(path, 'r') as f:
|
|
357
|
-
content = f.read()
|
|
358
|
-
# Split off the frontmatter block (between the two leading '---' lines)
|
|
359
|
-
parts = content.split('---', 2)
|
|
360
|
-
if len(parts) < 3:
|
|
361
|
-
print('roadmap missing frontmatter delimiter', file=sys.stderr)
|
|
362
|
-
sys.exit(2)
|
|
363
|
-
body = parts[2]
|
|
364
|
-
new_fm = yaml.safe_dump(fm, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
365
|
-
new_content = '---\n' + new_fm + '---' + body
|
|
366
|
-
with open(path, 'w') as f:
|
|
367
|
-
f.write(new_content)
|
|
368
|
-
" 2>&1
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
# Returns the next pending work unit whose depends_on are all completed
|
|
372
|
-
# (status=completed in next_work_units), as JSON. Returns exit 1 if none.
|
|
373
|
-
state_roadmap_next_pending() {
|
|
374
|
-
local slug="$1"
|
|
375
|
-
if [ -z "$slug" ]; then
|
|
376
|
-
echo "state_roadmap_next_pending: requires <slug>" >&2
|
|
377
|
-
return 2
|
|
378
|
-
fi
|
|
379
|
-
local roadmap_json
|
|
380
|
-
roadmap_json="$(state_roadmap_get "$slug")" || return $?
|
|
381
|
-
local result
|
|
382
|
-
result="$(echo "$roadmap_json" | jq -c '
|
|
383
|
-
.next_work_units as $wus |
|
|
384
|
-
[$wus[]? | select(.status == "completed") | .slug] as $done |
|
|
385
|
-
[$wus[]? | select(.status == "pending")] |
|
|
386
|
-
map(select(
|
|
387
|
-
((.depends_on // []) | length == 0) or
|
|
388
|
-
((.depends_on // []) | all(IN($done[])))
|
|
389
|
-
)) |
|
|
390
|
-
first // empty
|
|
391
|
-
' 2>/dev/null)"
|
|
392
|
-
if [ -z "$result" ] || [ "$result" = "null" ]; then
|
|
393
|
-
return 1
|
|
394
|
-
fi
|
|
395
|
-
echo "$result"
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
# Atomically claim the next pending WU (or a specific WU by slug).
|
|
399
|
-
# Args: <roadmap-slug> [wu-slug] [session-id]
|
|
400
|
-
# - wu-slug omitted → claim next_pending result
|
|
401
|
-
# - session-id defaults to ${CLAUDE_SESSION_ID:-unknown}
|
|
402
|
-
# Output: the claimed WU as JSON, or exit 1 if none claimable.
|
|
403
|
-
state_roadmap_claim_wu() {
|
|
404
|
-
local roadmap_slug="$1"
|
|
405
|
-
local wu_slug="${2:-}"
|
|
406
|
-
local session_id="${3:-${CLAUDE_SESSION_ID:-unknown}}"
|
|
407
|
-
if [ -z "$roadmap_slug" ]; then
|
|
408
|
-
echo "state_roadmap_claim_wu: requires <roadmap-slug> [wu-slug] [session-id]" >&2
|
|
409
|
-
return 2
|
|
410
|
-
fi
|
|
411
|
-
local lockfile=".state-roadmap-${roadmap_slug}.lock"
|
|
412
|
-
(
|
|
413
|
-
flock -x 9 2>/dev/null || true
|
|
414
|
-
local roadmap_json
|
|
415
|
-
roadmap_json="$(state_roadmap_get "$roadmap_slug")" || exit 1
|
|
416
|
-
|
|
417
|
-
# If wu_slug not given, compute next_pending
|
|
418
|
-
if [ -z "$wu_slug" ]; then
|
|
419
|
-
wu_slug="$(state_roadmap_next_pending "$roadmap_slug" | jq -r '.slug // empty')"
|
|
420
|
-
if [ -z "$wu_slug" ]; then
|
|
421
|
-
exit 1
|
|
422
|
-
fi
|
|
423
|
-
fi
|
|
424
|
-
|
|
425
|
-
# Check current status — only claim if pending
|
|
426
|
-
local current_status
|
|
427
|
-
current_status="$(echo "$roadmap_json" | jq -r --arg slug "$wu_slug" '.next_work_units[]? | select(.slug == $slug) | .status // empty')"
|
|
428
|
-
if [ "$current_status" != "pending" ]; then
|
|
429
|
-
# Already claimed or completed — return empty (atomicity boundary)
|
|
430
|
-
exit 1
|
|
431
|
-
fi
|
|
432
|
-
|
|
433
|
-
local now
|
|
434
|
-
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
435
|
-
# Advance last_event AND mutate WU status atomically (bassclef#349
|
|
436
|
-
# tail-1: claim is a real Event in workflow history, not just a
|
|
437
|
-
# status mutation — recording it in last_event means polling sessions
|
|
438
|
-
# can tell when work is in progress, not just completed).
|
|
439
|
-
local updated
|
|
440
|
-
updated="$(echo "$roadmap_json" | jq --arg slug "$wu_slug" --arg sid "$session_id" --arg ts "$now" '
|
|
441
|
-
.next_work_units = ((.next_work_units // []) | map(
|
|
442
|
-
if .slug == $slug and .status == "pending" then
|
|
443
|
-
. + {status: "in_progress", claimed_by: $sid, claimed_at: $ts}
|
|
444
|
-
else . end
|
|
445
|
-
)) |
|
|
446
|
-
.last_event = {ts: $ts, event: "task-claimed", refs: [$slug], actor: $sid}
|
|
447
|
-
')" || exit 4
|
|
448
|
-
|
|
449
|
-
# Write back
|
|
450
|
-
__state_roadmap_write_frontmatter "$roadmap_slug" "$updated" || exit 4
|
|
451
|
-
|
|
452
|
-
# Echo the claimed WU
|
|
453
|
-
echo "$updated" | jq --arg slug "$wu_slug" '.next_work_units[] | select(.slug == $slug)' -c
|
|
454
|
-
) 9>"$lockfile"
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
# Release a WU on completion. Updates the WU's status to completed AND
|
|
458
|
-
# advances last_event to record the completion.
|
|
459
|
-
# Args: <roadmap-slug> <wu-slug> [event-name] [refs-json-array]
|
|
460
|
-
state_roadmap_release_wu() {
|
|
461
|
-
local roadmap_slug="$1"
|
|
462
|
-
local wu_slug="$2"
|
|
463
|
-
local event_name="${3:-}"
|
|
464
|
-
local refs_json="${4:-[]}"
|
|
465
|
-
if [ -z "$roadmap_slug" ] || [ -z "$wu_slug" ]; then
|
|
466
|
-
echo "state_roadmap_release_wu: requires <roadmap-slug> <wu-slug> [event-name] [refs-json]" >&2
|
|
467
|
-
return 2
|
|
468
|
-
fi
|
|
469
|
-
local lockfile=".state-roadmap-${roadmap_slug}.lock"
|
|
470
|
-
(
|
|
471
|
-
flock -x 9 2>/dev/null || true
|
|
472
|
-
local roadmap_json
|
|
473
|
-
roadmap_json="$(state_roadmap_get "$roadmap_slug")" || exit 1
|
|
474
|
-
|
|
475
|
-
if [ -z "$event_name" ]; then
|
|
476
|
-
event_name="${wu_slug}_completed"
|
|
477
|
-
fi
|
|
478
|
-
|
|
479
|
-
local now
|
|
480
|
-
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
481
|
-
|
|
482
|
-
local updated
|
|
483
|
-
updated="$(echo "$roadmap_json" | \
|
|
484
|
-
jq --arg slug "$wu_slug" --arg ts "$now" --arg ev "$event_name" --argjson refs "$refs_json" '
|
|
485
|
-
.next_work_units = ((.next_work_units // []) | map(
|
|
486
|
-
if .slug == $slug then . + {status: "completed", completed_at: $ts}
|
|
487
|
-
else . end
|
|
488
|
-
)) |
|
|
489
|
-
.last_event = {ts: $ts, event: $ev, refs: $refs, actor: ((env.CLAUDE_SESSION_ID // "unknown")|tostring)}
|
|
490
|
-
')" || exit 4
|
|
491
|
-
|
|
492
|
-
__state_roadmap_write_frontmatter "$roadmap_slug" "$updated" || exit 4
|
|
493
|
-
|
|
494
|
-
# Echo the new last_event
|
|
495
|
-
echo "$updated" | jq -c '.last_event'
|
|
496
|
-
) 9>"$lockfile"
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
# Add a new task to a roadmap's next_work_units (bassclef#349 tail-3).
|
|
500
|
-
# Validates slug uniqueness within roadmap and that any depends_on slugs
|
|
501
|
-
# already exist in next_work_units. Advances last_event with task-added.
|
|
502
|
-
# Args: <roadmap-slug> <wu-slug> <issue> <wu-id> <title> [depends_on-json]
|
|
503
|
-
# - issue: GitHub-style ref like "bassclef#354"
|
|
504
|
-
# - wu-id: WU identifier like "WU-A" or "tail-3"
|
|
505
|
-
# - depends_on-json: JSON array of dependency slugs; defaults to []
|
|
506
|
-
# Output: the new task as JSON, or exits 4 on validation failure.
|
|
507
|
-
state_roadmap_add_task() {
|
|
508
|
-
local roadmap_slug="${1:-}"
|
|
509
|
-
local wu_slug="${2:-}"
|
|
510
|
-
local issue="${3:-}"
|
|
511
|
-
local wu_id="${4:-}"
|
|
512
|
-
local title="${5:-}"
|
|
513
|
-
local depends_on_json="${6:-[]}"
|
|
514
|
-
if [ -z "$roadmap_slug" ] || [ -z "$wu_slug" ] || [ -z "$issue" ] || [ -z "$wu_id" ] || [ -z "$title" ]; then
|
|
515
|
-
echo "state_roadmap_add_task: requires <roadmap-slug> <wu-slug> <issue> <wu-id> <title> [depends_on-json]" >&2
|
|
516
|
-
return 2
|
|
517
|
-
fi
|
|
518
|
-
# Validate depends_on is a JSON array
|
|
519
|
-
if ! echo "$depends_on_json" | jq -e 'type == "array"' >/dev/null 2>&1; then
|
|
520
|
-
echo "state_roadmap_add_task: depends_on must be a JSON array (got: $depends_on_json)" >&2
|
|
521
|
-
return 2
|
|
522
|
-
fi
|
|
523
|
-
local lockfile=".state-roadmap-${roadmap_slug}.lock"
|
|
524
|
-
(
|
|
525
|
-
flock -x 9 2>/dev/null || true
|
|
526
|
-
local roadmap_json
|
|
527
|
-
roadmap_json="$(state_roadmap_get "$roadmap_slug")" || exit 1
|
|
528
|
-
|
|
529
|
-
# Slug uniqueness check
|
|
530
|
-
local existing
|
|
531
|
-
existing="$(echo "$roadmap_json" | jq -r --arg slug "$wu_slug" '.next_work_units[]? | select(.slug == $slug) | .slug // empty')"
|
|
532
|
-
if [ -n "$existing" ]; then
|
|
533
|
-
echo "state_roadmap_add_task: WU '$wu_slug' already exists in roadmap '$roadmap_slug'" >&2
|
|
534
|
-
exit 4
|
|
535
|
-
fi
|
|
536
|
-
|
|
537
|
-
# depends_on slugs must already exist in roadmap
|
|
538
|
-
local depends_on_count
|
|
539
|
-
depends_on_count="$(echo "$depends_on_json" | jq 'length')"
|
|
540
|
-
if [ "$depends_on_count" -gt 0 ]; then
|
|
541
|
-
local missing
|
|
542
|
-
missing="$(echo "$roadmap_json" | jq -c --argjson dep "$depends_on_json" '
|
|
543
|
-
([.next_work_units[]?.slug] // []) as $existing |
|
|
544
|
-
$dep | map(select(. as $d | ($existing | index($d)) == null))
|
|
545
|
-
')"
|
|
546
|
-
if [ "$(echo "$missing" | jq 'length')" != "0" ]; then
|
|
547
|
-
echo "state_roadmap_add_task: depends_on contains slugs not in roadmap: $missing" >&2
|
|
548
|
-
exit 4
|
|
549
|
-
fi
|
|
550
|
-
fi
|
|
551
|
-
|
|
552
|
-
local now
|
|
553
|
-
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
554
|
-
local updated
|
|
555
|
-
updated="$(echo "$roadmap_json" | \
|
|
556
|
-
jq --arg slug "$wu_slug" --arg issue "$issue" --arg wu_id "$wu_id" \
|
|
557
|
-
--arg title "$title" --arg ts "$now" --argjson dep "$depends_on_json" '
|
|
558
|
-
.next_work_units = ((.next_work_units // []) + [{
|
|
559
|
-
slug: $slug,
|
|
560
|
-
issue: $issue,
|
|
561
|
-
wu: $wu_id,
|
|
562
|
-
title: $title,
|
|
563
|
-
status: "pending",
|
|
564
|
-
depends_on: $dep
|
|
565
|
-
}]) |
|
|
566
|
-
.last_event = {ts: $ts, event: "task-added", refs: [$slug], actor: ((env.CLAUDE_SESSION_ID // "unknown")|tostring)}
|
|
567
|
-
')" || exit 4
|
|
568
|
-
|
|
569
|
-
__state_roadmap_write_frontmatter "$roadmap_slug" "$updated" || exit 4
|
|
570
|
-
|
|
571
|
-
# Echo the new task as JSON
|
|
572
|
-
echo "$updated" | jq -c --arg slug "$wu_slug" '.next_work_units[] | select(.slug == $slug)'
|
|
573
|
-
) 9>"$lockfile"
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
# Abandon a claimed WU (bassclef#349 tail-2). Two modes:
|
|
577
|
-
# - Default (return-to-queue): in_progress → pending; clears claimed_by/claimed_at
|
|
578
|
-
# - Terminal (--terminal flag): in_progress → abandoned; clears claimed_by/claimed_at
|
|
579
|
-
# In both modes, advances last_event with event="task-abandoned" and reason in note.
|
|
580
|
-
# Args: <roadmap-slug> <wu-slug> [reason] [--terminal]
|
|
581
|
-
state_roadmap_abandon_wu() {
|
|
582
|
-
local roadmap_slug="$1"
|
|
583
|
-
local wu_slug="$2"
|
|
584
|
-
local reason="${3:-}"
|
|
585
|
-
local mode="pending" # default: return to queue
|
|
586
|
-
if [ "${4:-}" = "--terminal" ] || [ "${3:-}" = "--terminal" ]; then
|
|
587
|
-
mode="abandoned"
|
|
588
|
-
[ "${3:-}" = "--terminal" ] && reason="" # if --terminal was 3rd arg, no reason
|
|
589
|
-
fi
|
|
590
|
-
if [ -z "$roadmap_slug" ] || [ -z "$wu_slug" ]; then
|
|
591
|
-
echo "state_roadmap_abandon_wu: requires <roadmap-slug> <wu-slug> [reason] [--terminal]" >&2
|
|
592
|
-
return 2
|
|
593
|
-
fi
|
|
594
|
-
local lockfile=".state-roadmap-${roadmap_slug}.lock"
|
|
595
|
-
(
|
|
596
|
-
flock -x 9 2>/dev/null || true
|
|
597
|
-
local roadmap_json
|
|
598
|
-
roadmap_json="$(state_roadmap_get "$roadmap_slug")" || exit 1
|
|
599
|
-
|
|
600
|
-
# Verify WU exists and is in_progress; only abandon active claims
|
|
601
|
-
local current_status
|
|
602
|
-
current_status="$(echo "$roadmap_json" | jq -r --arg slug "$wu_slug" '.next_work_units[]? | select(.slug == $slug) | .status // empty')"
|
|
603
|
-
if [ -z "$current_status" ]; then
|
|
604
|
-
echo "state_roadmap_abandon_wu: WU '$wu_slug' not found in roadmap '$roadmap_slug'" >&2
|
|
605
|
-
exit 1
|
|
606
|
-
fi
|
|
607
|
-
if [ "$current_status" != "in_progress" ]; then
|
|
608
|
-
echo "state_roadmap_abandon_wu: WU '$wu_slug' is '$current_status', not in_progress; cannot abandon" >&2
|
|
609
|
-
exit 1
|
|
610
|
-
fi
|
|
611
|
-
|
|
612
|
-
local now
|
|
613
|
-
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
614
|
-
local updated
|
|
615
|
-
updated="$(echo "$roadmap_json" | \
|
|
616
|
-
jq --arg slug "$wu_slug" --arg ts "$now" --arg mode "$mode" --arg reason "$reason" '
|
|
617
|
-
.next_work_units = ((.next_work_units // []) | map(
|
|
618
|
-
if .slug == $slug then
|
|
619
|
-
(. | del(.claimed_by) | del(.claimed_at)) +
|
|
620
|
-
(if $mode == "abandoned" then {status: "abandoned", abandoned_at: $ts, abandoned_reason: $reason}
|
|
621
|
-
else {status: "pending"} end)
|
|
622
|
-
else . end
|
|
623
|
-
)) |
|
|
624
|
-
.last_event = {ts: $ts, event: "task-abandoned", refs: [$slug], actor: ((env.CLAUDE_SESSION_ID // "unknown")|tostring), note: $reason}
|
|
625
|
-
')" || exit 4
|
|
626
|
-
|
|
627
|
-
__state_roadmap_write_frontmatter "$roadmap_slug" "$updated" || exit 4
|
|
628
|
-
|
|
629
|
-
# Echo the new last_event
|
|
630
|
-
echo "$updated" | jq -c '.last_event'
|
|
631
|
-
) 9>"$lockfile"
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
# === Marker (Shape C — pure JSON) ===
|
|
635
|
-
|
|
636
|
-
state_marker_get() {
|
|
637
|
-
local uuid="$1"
|
|
638
|
-
if [ -z "$uuid" ]; then
|
|
639
|
-
echo "state_marker_get: requires <uuid>" >&2
|
|
640
|
-
return 2
|
|
641
|
-
fi
|
|
642
|
-
if [ ! -d state/markers ]; then
|
|
643
|
-
return 1
|
|
644
|
-
fi
|
|
645
|
-
local file
|
|
646
|
-
file="$(find state/markers -name "${uuid}.json" -type f -print -quit 2>/dev/null)"
|
|
647
|
-
if [ -z "$file" ]; then
|
|
648
|
-
return 1
|
|
649
|
-
fi
|
|
650
|
-
cat "$file"
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
# Counter: derived from history on every call (replay-determinism).
|
|
654
|
-
# Args: [category] [since-days]
|
|
655
|
-
# Returns integer to stdout (0 if none).
|
|
656
|
-
#
|
|
657
|
-
# Per state-spine-contract.md: "since-N-sessions" semantics deferred to
|
|
658
|
-
# spine v1 (requires session-boundary mapping from chronicle history).
|
|
659
|
-
# v0 implements `since-days` (mtime-based) — sufficient for the
|
|
660
|
-
# bassclef#296 floor-lift fix (window-bounded marker count). Override
|
|
661
|
-
# the implementation in spine v1 to use session boundaries; consumers
|
|
662
|
-
# don't change.
|
|
663
|
-
#
|
|
664
|
-
# Also accepts both .json (spine v0) and .marker (legacy) extensions —
|
|
665
|
-
# legacy markers from .claude/verify-markers/ and pre-spine
|
|
666
|
-
# state/markers/ retain .marker extension; this accessor unions
|
|
667
|
-
# both per pre-spine + post-spine reads.
|
|
668
|
-
state_marker_count() {
|
|
669
|
-
local category="${1:-}"
|
|
670
|
-
local since_days="${2:-}"
|
|
671
|
-
if [ ! -d state/markers ]; then
|
|
672
|
-
echo "0"
|
|
673
|
-
return 0
|
|
674
|
-
fi
|
|
675
|
-
local search_dir="state/markers"
|
|
676
|
-
if [ -n "$category" ]; then
|
|
677
|
-
if [ ! -d "state/markers/$category" ]; then
|
|
678
|
-
echo "0"
|
|
679
|
-
return 0
|
|
680
|
-
fi
|
|
681
|
-
search_dir="state/markers/$category"
|
|
682
|
-
fi
|
|
683
|
-
if [ -n "$since_days" ]; then
|
|
684
|
-
if ! [[ "$since_days" =~ ^[0-9]+$ ]]; then
|
|
685
|
-
echo "state_marker_count: since-days must be integer (got: $since_days)" >&2
|
|
686
|
-
return 2
|
|
687
|
-
fi
|
|
688
|
-
find "$search_dir" -type f \( -name "*.json" -o -name "*.marker" \) -mtime "-${since_days}" 2>/dev/null | wc -l | tr -d ' '
|
|
689
|
-
else
|
|
690
|
-
find "$search_dir" -type f \( -name "*.json" -o -name "*.marker" \) 2>/dev/null | wc -l | tr -d ' '
|
|
691
|
-
fi
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
# Append a new marker. Reads marker JSON on stdin; extracts id + category;
|
|
695
|
-
# writes to state/markers/<category>/<id>.json. Returns id to stdout.
|
|
696
|
-
state_marker_add() {
|
|
697
|
-
local marker_json
|
|
698
|
-
marker_json="$(cat)"
|
|
699
|
-
if [ -z "$marker_json" ]; then
|
|
700
|
-
echo "state_marker_add: requires marker JSON on stdin" >&2
|
|
701
|
-
return 2
|
|
702
|
-
fi
|
|
703
|
-
local id category
|
|
704
|
-
id="$(echo "$marker_json" | jq -r '.id // empty')"
|
|
705
|
-
category="$(echo "$marker_json" | jq -r '.category // empty')"
|
|
706
|
-
if [ -z "$id" ] || [ -z "$category" ]; then
|
|
707
|
-
echo "state_marker_add: marker missing id or category" >&2
|
|
708
|
-
return 2
|
|
709
|
-
fi
|
|
710
|
-
local dir="state/markers/$category"
|
|
711
|
-
mkdir -p "$dir" || return 4
|
|
712
|
-
__state_atomic_write "${dir}/${id}.json" "$marker_json" || return 4
|
|
713
|
-
echo "$id"
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
# === Chronicle (Shape B/hybrid — markdown + YAML frontmatter; gates_fired
|
|
717
|
-
# in frontmatter for v0; sidecar JSON ships in WU-4) ===
|
|
718
|
-
|
|
719
|
-
# Special-purpose: extract gates_fired array from a chronicle.
|
|
720
|
-
# Returns JSON array of gate objects to stdout (empty array `[]` if no gates).
|
|
721
|
-
# Per bassclef#298 fix — schema-required field; this accessor reads it deterministically.
|
|
722
|
-
state_chronicle_gate_evidence() {
|
|
723
|
-
local id="$1"
|
|
724
|
-
if [ -z "$id" ]; then
|
|
725
|
-
echo "state_chronicle_gate_evidence: requires <id>" >&2
|
|
726
|
-
return 2
|
|
727
|
-
fi
|
|
728
|
-
# Try bassclef (chronicle/) and consumer-repo (docs/chronicle/) paths
|
|
729
|
-
local path=""
|
|
730
|
-
if [ -f "chronicle/${id}.md" ]; then
|
|
731
|
-
path="chronicle/${id}.md"
|
|
732
|
-
elif [ -f "docs/chronicle/${id}.md" ]; then
|
|
733
|
-
path="docs/chronicle/${id}.md"
|
|
734
|
-
else
|
|
735
|
-
return 1
|
|
736
|
-
fi
|
|
737
|
-
__state_yq2json "$path" | jq -c '.gates_fired // []'
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
# List chronicle IDs (filename without .md extension), optionally limited
|
|
741
|
-
# to the most-recent N entries. Returns one ID per line on stdout (NOT
|
|
742
|
-
# JSON array — chronicles are typically iterated, and bash-friendly
|
|
743
|
-
# output keeps the consumer simple).
|
|
744
|
-
#
|
|
745
|
-
# Args: [--recent N]
|
|
746
|
-
#
|
|
747
|
-
# Tries bassclef (chronicle/) first, then consumer-repo
|
|
748
|
-
# (docs/chronicle/) per the same path convention as
|
|
749
|
-
# state_chronicle_gate_evidence. Sorts lexicographically (chronicles
|
|
750
|
-
# carry YYYY-MM-DD prefix, so this is chronological).
|
|
751
|
-
state_chronicle_list() {
|
|
752
|
-
local limit=""
|
|
753
|
-
if [ "${1:-}" = "--recent" ]; then
|
|
754
|
-
limit="${2:-}"
|
|
755
|
-
if ! [[ "$limit" =~ ^[0-9]+$ ]]; then
|
|
756
|
-
echo "state_chronicle_list: --recent requires integer (got: $limit)" >&2
|
|
757
|
-
return 2
|
|
758
|
-
fi
|
|
759
|
-
elif [ -n "${1:-}" ]; then
|
|
760
|
-
echo "state_chronicle_list: unknown arg '$1' (expected --recent N)" >&2
|
|
761
|
-
return 2
|
|
762
|
-
fi
|
|
763
|
-
local dir=""
|
|
764
|
-
if [ -d chronicle ]; then
|
|
765
|
-
dir="chronicle"
|
|
766
|
-
elif [ -d docs/chronicle ]; then
|
|
767
|
-
dir="docs/chronicle"
|
|
768
|
-
else
|
|
769
|
-
return 0
|
|
770
|
-
fi
|
|
771
|
-
local files
|
|
772
|
-
files="$(find "$dir" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort)"
|
|
773
|
-
[ -z "$files" ] && return 0
|
|
774
|
-
if [ -n "$limit" ]; then
|
|
775
|
-
files="$(echo "$files" | tail -n "$limit")"
|
|
776
|
-
fi
|
|
777
|
-
while IFS= read -r f; do
|
|
778
|
-
[ -z "$f" ] && continue
|
|
779
|
-
local base
|
|
780
|
-
base="$(basename "$f" .md)"
|
|
781
|
-
echo "$base"
|
|
782
|
-
done <<< "$files"
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
# === DeferredAction (Shape B — markdown + YAML frontmatter) ===
|
|
786
|
-
|
|
787
|
-
# Special-purpose: list pending deferred actions, optionally filtered by
|
|
788
|
-
# capability. Pending = files in docs/deferred-actions/ (NOT in completed/).
|
|
789
|
-
# Filter args: zero or more capability tokens; entry matches if its
|
|
790
|
-
# requires_capability set is a SUBSET of the supplied capabilities.
|
|
791
|
-
state_deferred_action_pending() {
|
|
792
|
-
local -a caps=("$@")
|
|
793
|
-
local -a entries=()
|
|
794
|
-
if [ ! -d docs/deferred-actions ]; then
|
|
795
|
-
echo "[]"
|
|
796
|
-
return 0
|
|
797
|
-
fi
|
|
798
|
-
for f in docs/deferred-actions/*.md; do
|
|
799
|
-
[ -f "$f" ] || continue
|
|
800
|
-
[ "$(basename "$f")" = "README.md" ] && continue
|
|
801
|
-
[ "$(basename "$f")" = "template.md" ] && continue
|
|
802
|
-
entries+=("$f")
|
|
803
|
-
done
|
|
804
|
-
if [ "${#entries[@]}" -eq 0 ]; then
|
|
805
|
-
echo "[]"
|
|
806
|
-
return 0
|
|
807
|
-
fi
|
|
808
|
-
# Build JSON array
|
|
809
|
-
local result="["
|
|
810
|
-
local first=1
|
|
811
|
-
for f in "${entries[@]}"; do
|
|
812
|
-
local entry_json
|
|
813
|
-
entry_json="$(__state_yq2json "$f" 2>/dev/null)" || continue
|
|
814
|
-
# Apply capability filter if any caps supplied
|
|
815
|
-
if [ "${#caps[@]}" -gt 0 ]; then
|
|
816
|
-
local match=1
|
|
817
|
-
local entry_caps
|
|
818
|
-
entry_caps="$(echo "$entry_json" | jq -r '.requires_capability // [] | .[]')"
|
|
819
|
-
for needed in $entry_caps; do
|
|
820
|
-
local found=0
|
|
821
|
-
for supplied in "${caps[@]}"; do
|
|
822
|
-
if [ "$needed" = "$supplied" ]; then
|
|
823
|
-
found=1
|
|
824
|
-
break
|
|
825
|
-
fi
|
|
826
|
-
done
|
|
827
|
-
if [ "$found" -eq 0 ]; then
|
|
828
|
-
match=0
|
|
829
|
-
break
|
|
830
|
-
fi
|
|
831
|
-
done
|
|
832
|
-
if [ "$match" -eq 0 ]; then
|
|
833
|
-
continue
|
|
834
|
-
fi
|
|
835
|
-
fi
|
|
836
|
-
if [ "$first" -eq 1 ]; then
|
|
837
|
-
first=0
|
|
838
|
-
else
|
|
839
|
-
result="${result},"
|
|
840
|
-
fi
|
|
841
|
-
result="${result}${entry_json}"
|
|
842
|
-
done
|
|
843
|
-
result="${result}]"
|
|
844
|
-
echo "$result" | jq -c .
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
# List deferred actions. Optional first arg selects scope:
|
|
848
|
-
# pending (default) → docs/deferred-actions/*.md (NOT completed/)
|
|
849
|
-
# completed → docs/deferred-actions/completed/*.md
|
|
850
|
-
# all → both
|
|
851
|
-
# Returns JSON array of frontmatter objects to stdout; empty `[]` if none.
|
|
852
|
-
state_deferred_action_list() {
|
|
853
|
-
local scope="${1:-pending}"
|
|
854
|
-
case "$scope" in
|
|
855
|
-
pending|completed|all) ;;
|
|
856
|
-
*)
|
|
857
|
-
echo "state_deferred_action_list: scope must be pending|completed|all (got: $scope)" >&2
|
|
858
|
-
return 2
|
|
859
|
-
;;
|
|
860
|
-
esac
|
|
861
|
-
local -a paths=()
|
|
862
|
-
if [ "$scope" = "pending" ] || [ "$scope" = "all" ]; then
|
|
863
|
-
if [ -d docs/deferred-actions ]; then
|
|
864
|
-
for f in docs/deferred-actions/*.md; do
|
|
865
|
-
[ -f "$f" ] || continue
|
|
866
|
-
local base="$(basename "$f")"
|
|
867
|
-
[ "$base" = "README.md" ] && continue
|
|
868
|
-
[ "$base" = "template.md" ] && continue
|
|
869
|
-
paths+=("$f")
|
|
870
|
-
done
|
|
871
|
-
fi
|
|
872
|
-
fi
|
|
873
|
-
if [ "$scope" = "completed" ] || [ "$scope" = "all" ]; then
|
|
874
|
-
if [ -d docs/deferred-actions/completed ]; then
|
|
875
|
-
for f in docs/deferred-actions/completed/*.md; do
|
|
876
|
-
[ -f "$f" ] || continue
|
|
877
|
-
local base="$(basename "$f")"
|
|
878
|
-
[ "$base" = "README.md" ] && continue
|
|
879
|
-
paths+=("$f")
|
|
880
|
-
done
|
|
881
|
-
fi
|
|
882
|
-
fi
|
|
883
|
-
if [ "${#paths[@]}" -eq 0 ]; then
|
|
884
|
-
echo "[]"
|
|
885
|
-
return 0
|
|
886
|
-
fi
|
|
887
|
-
local result="["
|
|
888
|
-
local first=1
|
|
889
|
-
for f in "${paths[@]}"; do
|
|
890
|
-
local entry_json
|
|
891
|
-
entry_json="$(__state_yq2json "$f" 2>/dev/null)" || continue
|
|
892
|
-
[ -z "$entry_json" ] && continue
|
|
893
|
-
if [ "$first" -eq 1 ]; then
|
|
894
|
-
first=0
|
|
895
|
-
else
|
|
896
|
-
result="${result},"
|
|
897
|
-
fi
|
|
898
|
-
result="${result}${entry_json}"
|
|
899
|
-
done
|
|
900
|
-
result="${result}]"
|
|
901
|
-
echo "$result" | jq -c .
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
# === Validation accessor (special-purpose; wraps state-validate.sh) ===
|
|
905
|
-
|
|
906
|
-
# Validates an arbitrary file at <path> against its entity schema by
|
|
907
|
-
# invoking the state-validate.sh PreToolUse hook with a synthesized
|
|
908
|
-
# event. Useful for ad-hoc /state validate <path> from operators or
|
|
909
|
-
# scripts.
|
|
910
|
-
#
|
|
911
|
-
# Returns:
|
|
912
|
-
# 0 — file is a state-spine entity AND validates clean (or hook chose
|
|
913
|
-
# legacy pass-through)
|
|
914
|
-
# 2 — invalid input (no path, missing arg)
|
|
915
|
-
# 3 — schema validation failed (BLOCK message on stderr)
|
|
916
|
-
# 4 — filesystem error (file or hook missing)
|
|
917
|
-
#
|
|
918
|
-
# stdout: empty on success; ajv-cli BLOCK output on failure
|
|
919
|
-
# stderr: structured BLOCK message (forwarded from state-validate.sh)
|
|
920
|
-
state_validate() {
|
|
921
|
-
local target="${1:-}"
|
|
922
|
-
if [ -z "$target" ]; then
|
|
923
|
-
echo "state_validate: requires <path>" >&2
|
|
924
|
-
return 2
|
|
925
|
-
fi
|
|
926
|
-
if [ ! -f "$target" ]; then
|
|
927
|
-
echo "state_validate: file not found: $target" >&2
|
|
928
|
-
return 4
|
|
929
|
-
fi
|
|
930
|
-
local hook=".claude/hooks/state-validate.sh"
|
|
931
|
-
if [ ! -x "$hook" ]; then
|
|
932
|
-
echo "state_validate: hook not executable at $hook" >&2
|
|
933
|
-
return 4
|
|
934
|
-
fi
|
|
935
|
-
# Synthesize PreToolUse event JSON; the hook only reads tool_name + file_path.
|
|
936
|
-
printf '{"tool_name":"Edit","tool_input":{"file_path":"%s"}}\n' "$target" | "$hook"
|
|
937
|
-
local hook_exit=$?
|
|
938
|
-
case "$hook_exit" in
|
|
939
|
-
0) return 0 ;;
|
|
940
|
-
2|4|5) return "$hook_exit" ;;
|
|
941
|
-
*) return 3 ;; # any non-zero non-input-error = validation block
|
|
942
|
-
esac
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
# === Reference resolver (typed cross-entity refs) ===
|
|
946
|
-
|
|
947
|
-
# Resolves a typed reference to its entity. Returns JSON of the entity to
|
|
948
|
-
# stdout; exit 1 if dangling; exit 5 if entity type unknown.
|
|
949
|
-
# Args: <type> <id>
|
|
950
|
-
state_reference_get() {
|
|
951
|
-
local type="$1"
|
|
952
|
-
local id="$2"
|
|
953
|
-
if [ -z "$type" ] || [ -z "$id" ]; then
|
|
954
|
-
echo "state_reference_get: requires <type> <id>" >&2
|
|
955
|
-
return 2
|
|
956
|
-
fi
|
|
957
|
-
case "$type" in
|
|
958
|
-
chronicle)
|
|
959
|
-
if [ -f "chronicle/${id}.md" ]; then
|
|
960
|
-
__state_yq2json "chronicle/${id}.md"
|
|
961
|
-
elif [ -f "docs/chronicle/${id}.md" ]; then
|
|
962
|
-
__state_yq2json "docs/chronicle/${id}.md"
|
|
963
|
-
else
|
|
964
|
-
return 1
|
|
965
|
-
fi
|
|
966
|
-
;;
|
|
967
|
-
discovery)
|
|
968
|
-
if [ -f "design/discoveries/${id}.md" ]; then
|
|
969
|
-
__state_yq2json "design/discoveries/${id}.md"
|
|
970
|
-
else
|
|
971
|
-
return 1
|
|
972
|
-
fi
|
|
973
|
-
;;
|
|
974
|
-
adr)
|
|
975
|
-
# ADRs are markdown without YAML frontmatter; return path as JSON
|
|
976
|
-
if [ -f "architecture/decisions/${id}.md" ]; then
|
|
977
|
-
printf '{"path":"architecture/decisions/%s.md","type":"adr","id":"%s"}\n' "$id" "$id"
|
|
978
|
-
else
|
|
979
|
-
return 1
|
|
980
|
-
fi
|
|
981
|
-
;;
|
|
982
|
-
decomposition)
|
|
983
|
-
if [ -f "docs/decompositions/${id}.md" ]; then
|
|
984
|
-
__state_yq2json "docs/decompositions/${id}.md"
|
|
985
|
-
else
|
|
986
|
-
return 1
|
|
987
|
-
fi
|
|
988
|
-
;;
|
|
989
|
-
spec)
|
|
990
|
-
if [ -f "docs/specs/${id}.md" ]; then
|
|
991
|
-
__state_yq2json "docs/specs/${id}.md"
|
|
992
|
-
else
|
|
993
|
-
return 1
|
|
994
|
-
fi
|
|
995
|
-
;;
|
|
996
|
-
session)
|
|
997
|
-
if [ -f "docs/sessions/${id}.md" ]; then
|
|
998
|
-
__state_yq2json "docs/sessions/${id}.md"
|
|
999
|
-
elif [ -f "docs/sessions/${id}.json" ]; then
|
|
1000
|
-
cat "docs/sessions/${id}.json"
|
|
1001
|
-
else
|
|
1002
|
-
return 1
|
|
1003
|
-
fi
|
|
1004
|
-
;;
|
|
1005
|
-
iteration-bet)
|
|
1006
|
-
state_iteration_bet_get "$id"
|
|
1007
|
-
;;
|
|
1008
|
-
roadmap)
|
|
1009
|
-
if [ -f "docs/roadmaps/${id}.md" ]; then
|
|
1010
|
-
__state_yq2json "docs/roadmaps/${id}.md"
|
|
1011
|
-
else
|
|
1012
|
-
return 1
|
|
1013
|
-
fi
|
|
1014
|
-
;;
|
|
1015
|
-
workunit)
|
|
1016
|
-
# Workunits live inline in stack manifests; not directly addressable in v0
|
|
1017
|
-
echo "state_reference_get: workunit type requires Stack lookup; not implemented in v0" >&2
|
|
1018
|
-
return 5
|
|
1019
|
-
;;
|
|
1020
|
-
stack)
|
|
1021
|
-
if [ -f "docs/stacks/${id}.md" ]; then
|
|
1022
|
-
__state_yq2json "docs/stacks/${id}.md"
|
|
1023
|
-
else
|
|
1024
|
-
return 1
|
|
1025
|
-
fi
|
|
1026
|
-
;;
|
|
1027
|
-
marker)
|
|
1028
|
-
state_marker_get "$id"
|
|
1029
|
-
;;
|
|
1030
|
-
gate)
|
|
1031
|
-
# Gates derive from Markers; not addressable directly in v0
|
|
1032
|
-
echo "state_reference_get: gate type derives from Marker; not directly addressable in v0" >&2
|
|
1033
|
-
return 5
|
|
1034
|
-
;;
|
|
1035
|
-
*)
|
|
1036
|
-
echo "state_reference_get: unknown entity type '$type'" >&2
|
|
1037
|
-
return 5
|
|
1038
|
-
;;
|
|
1039
|
-
esac
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
# Returns 0 if reference exists, 1 if dangling, 5 if type unknown.
|
|
1043
|
-
# No JSON output (silent on success and not-found; stderr on type error).
|
|
1044
|
-
state_reference_check() {
|
|
1045
|
-
local type="$1"
|
|
1046
|
-
local id="$2"
|
|
1047
|
-
state_reference_get "$type" "$id" >/dev/null 2>&1
|
|
1048
|
-
local exit_code=$?
|
|
1049
|
-
# Translate: success → 0; not-found → 1; unknown-type → 5; other → 1
|
|
1050
|
-
case "$exit_code" in
|
|
1051
|
-
0) return 0 ;;
|
|
1052
|
-
5) return 5 ;;
|
|
1053
|
-
*) return 1 ;;
|
|
1054
|
-
esac
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
# === BassclefPlatform (Shape D — pure YAML at sibling-repo root) ===
|
|
1058
|
-
#
|
|
1059
|
-
# Org-baseline platform-engineering primitive. Lives at the root of a
|
|
1060
|
-
# sibling repo (e.g., sunj-labs/platform-config) so it can be forked
|
|
1061
|
-
# and edited per adopter without forking bassclef itself.
|
|
1062
|
-
#
|
|
1063
|
-
# Resolution order for the file path:
|
|
1064
|
-
# 1. BASSCLEF_PLATFORM_CONFIG_PATH env var (explicit override)
|
|
1065
|
-
# 2. ~/src/<github_org>/platform-config/bassclef-platform.yml
|
|
1066
|
-
# (default sunj-labs convention; <github_org> from substrate.config.md)
|
|
1067
|
-
# 3. Sibling-repo path declared in .bassclef-source.json
|
|
1068
|
-
# (`platform_config_source` field — V2 thin-pointer extension)
|
|
1069
|
-
#
|
|
1070
|
-
# v0 implementation honors options 1 + 2. Option 3 lands when
|
|
1071
|
-
# .bassclef-source.json schema gains the field (bassclef#418-style
|
|
1072
|
-
# follow-up).
|
|
1073
|
-
|
|
1074
|
-
__state_platform_resolve_path() {
|
|
1075
|
-
if [ -n "${BASSCLEF_PLATFORM_CONFIG_PATH:-}" ]; then
|
|
1076
|
-
echo "$BASSCLEF_PLATFORM_CONFIG_PATH"
|
|
1077
|
-
return 0
|
|
1078
|
-
fi
|
|
1079
|
-
echo "${HOME}/src/sunj-labs/platform-config/bassclef-platform.yml"
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
state_platform_get() {
|
|
1083
|
-
local key="${1:-}"
|
|
1084
|
-
local path
|
|
1085
|
-
path="$(__state_platform_resolve_path)"
|
|
1086
|
-
if [ ! -f "$path" ]; then
|
|
1087
|
-
return 1
|
|
1088
|
-
fi
|
|
1089
|
-
local json
|
|
1090
|
-
json="$(python3 -c "
|
|
1091
|
-
import sys, yaml, json
|
|
1092
|
-
try:
|
|
1093
|
-
with open('$path') as f:
|
|
1094
|
-
data = yaml.safe_load(f) or {}
|
|
1095
|
-
print(json.dumps(data, default=str))
|
|
1096
|
-
except Exception as e:
|
|
1097
|
-
print(f'yaml parse failed: {e}', file=sys.stderr)
|
|
1098
|
-
sys.exit(3)
|
|
1099
|
-
")" || return 3
|
|
1100
|
-
|
|
1101
|
-
if [ -z "$key" ]; then
|
|
1102
|
-
echo "$json"
|
|
1103
|
-
return 0
|
|
1104
|
-
fi
|
|
1105
|
-
# Dot-path key (e.g., "org.github_org", "deploy_tiers.prototype.hosting_platform")
|
|
1106
|
-
echo "$json" | jq -r --arg key "$key" '
|
|
1107
|
-
getpath($key | split(".")) // empty
|
|
1108
|
-
'
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
# Returns the resolved file path (for tooling that needs to display
|
|
1112
|
-
# where state_platform_get is reading from, without reading the file).
|
|
1113
|
-
state_platform_path() {
|
|
1114
|
-
__state_platform_resolve_path
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
# === Pattern Implementation accessors (bassclef#478 Layer 3) ===
|
|
1118
|
-
#
|
|
1119
|
-
# Maps source files to the @pattern catalog entries they consciously
|
|
1120
|
-
# instantiate. Manifest at state/pattern-implementations/<sanitized-path>.json.
|
|
1121
|
-
# Auto-populated by .claude/hooks/pattern-annotation-validate.sh on Edit/Write.
|
|
1122
|
-
# Consumed by /pattern-trace skill for forward (which file uses pattern X)
|
|
1123
|
-
# and reverse (which patterns does file Y use) lookups.
|
|
1124
|
-
|
|
1125
|
-
__state_pattern_implementation_path() {
|
|
1126
|
-
# Sanitize a file path to a manifest filename: replace / with _.
|
|
1127
|
-
local file="$1"
|
|
1128
|
-
local sanitized
|
|
1129
|
-
sanitized=$(echo "$file" | tr '/' '_')
|
|
1130
|
-
echo "state/pattern-implementations/${sanitized}.json"
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
# state_pattern_implementation_get <file_path>
|
|
1134
|
-
# Read the manifest entry for a source file. Returns JSON or empty (not found).
|
|
1135
|
-
state_pattern_implementation_get() {
|
|
1136
|
-
local file="$1"
|
|
1137
|
-
if [ -z "$file" ]; then
|
|
1138
|
-
echo "state_pattern_implementation_get: requires <file>" >&2
|
|
1139
|
-
return 2
|
|
1140
|
-
fi
|
|
1141
|
-
local manifest_path
|
|
1142
|
-
manifest_path="$(__state_pattern_implementation_path "$file")"
|
|
1143
|
-
if [ ! -f "$manifest_path" ]; then
|
|
1144
|
-
return 1
|
|
1145
|
-
fi
|
|
1146
|
-
cat "$manifest_path"
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
# state_pattern_implementation_index_pattern <pattern_path>
|
|
1150
|
-
# Reverse lookup: list all source files that have a @pattern annotation
|
|
1151
|
-
# pointing to <pattern_path>. Returns one file path per line.
|
|
1152
|
-
state_pattern_implementation_index_pattern() {
|
|
1153
|
-
local pattern_path="$1"
|
|
1154
|
-
if [ -z "$pattern_path" ]; then
|
|
1155
|
-
echo "state_pattern_implementation_index_pattern: requires <pattern_path>" >&2
|
|
1156
|
-
return 2
|
|
1157
|
-
fi
|
|
1158
|
-
if [ ! -d "state/pattern-implementations" ]; then
|
|
1159
|
-
return 1
|
|
1160
|
-
fi
|
|
1161
|
-
for manifest in state/pattern-implementations/*.json; do
|
|
1162
|
-
[ -f "$manifest" ] || continue
|
|
1163
|
-
if jq -e --arg p "$pattern_path" '.annotations[] | select(.pattern_path == $p)' "$manifest" >/dev/null 2>&1; then
|
|
1164
|
-
jq -r '.file' "$manifest"
|
|
1165
|
-
fi
|
|
1166
|
-
done
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
# state_pattern_implementation_list
|
|
1170
|
-
# List all manifest entries (one file path per line).
|
|
1171
|
-
state_pattern_implementation_list() {
|
|
1172
|
-
if [ ! -d "state/pattern-implementations" ]; then
|
|
1173
|
-
return 1
|
|
1174
|
-
fi
|
|
1175
|
-
for manifest in state/pattern-implementations/*.json; do
|
|
1176
|
-
[ -f "$manifest" ] || continue
|
|
1177
|
-
jq -r '.file' "$manifest"
|
|
1178
|
-
done
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
# state_pattern_implementation_set <file_path> <pattern_path> <line> [context]
|
|
1182
|
-
# Write/upsert a single @pattern annotation entry. Used by the validate hook
|
|
1183
|
-
# during Write/Edit operations on source files that contain @pattern annotations.
|
|
1184
|
-
# This is the only WRITE accessor for this entity; reads use the get/index/list
|
|
1185
|
-
# methods above.
|
|
1186
|
-
state_pattern_implementation_set() {
|
|
1187
|
-
local file="$1"
|
|
1188
|
-
local pattern_path="$2"
|
|
1189
|
-
local line="$3"
|
|
1190
|
-
local context="${4:-}"
|
|
1191
|
-
if [ -z "$file" ] || [ -z "$pattern_path" ] || [ -z "$line" ]; then
|
|
1192
|
-
echo "state_pattern_implementation_set: requires <file> <pattern_path> <line> [context]" >&2
|
|
1193
|
-
return 2
|
|
1194
|
-
fi
|
|
1195
|
-
local manifest_path
|
|
1196
|
-
manifest_path="$(__state_pattern_implementation_path "$file")"
|
|
1197
|
-
mkdir -p "$(dirname "$manifest_path")"
|
|
1198
|
-
local timestamp
|
|
1199
|
-
timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
1200
|
-
if [ -f "$manifest_path" ]; then
|
|
1201
|
-
# Upsert: replace any existing annotation with same pattern_path+line.
|
|
1202
|
-
jq --arg p "$pattern_path" --argjson l "$line" --arg c "$context" --arg ts "$timestamp" '
|
|
1203
|
-
.last_scanned = $ts |
|
|
1204
|
-
.annotations |= (map(select(.pattern_path != $p or .line != $l)) + [{pattern_path: $p, line: $l} + (if $c != "" then {context: $c} else {} end)])
|
|
1205
|
-
' "$manifest_path" > "${manifest_path}.tmp" && mv "${manifest_path}.tmp" "$manifest_path"
|
|
1206
|
-
else
|
|
1207
|
-
# Create fresh.
|
|
1208
|
-
local entry
|
|
1209
|
-
if [ -n "$context" ]; then
|
|
1210
|
-
entry=$(jq -n --arg p "$pattern_path" --argjson l "$line" --arg c "$context" '{pattern_path: $p, line: $l, context: $c}')
|
|
1211
|
-
else
|
|
1212
|
-
entry=$(jq -n --arg p "$pattern_path" --argjson l "$line" '{pattern_path: $p, line: $l}')
|
|
1213
|
-
fi
|
|
1214
|
-
jq -n --arg f "$file" --argjson a "[$entry]" --arg ts "$timestamp" '{file: $f, annotations: $a, last_scanned: $ts, scanner_version: "1.0"}' > "$manifest_path"
|
|
1215
|
-
fi
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
# === SubstrateHardening (Shape C — pure JSON; per bassclef bet 2026-06-17a Phase Z) ===
|
|
1219
|
-
|
|
1220
|
-
# state_substrate_hardening_get
|
|
1221
|
-
# Read the substrate-hardening inventory. Returns the full inventory JSON
|
|
1222
|
-
# on stdout. Returns empty object `{}` if file does not exist.
|
|
1223
|
-
state_substrate_hardening_get() {
|
|
1224
|
-
local inventory="state/substrate-hardening/inventory.json"
|
|
1225
|
-
if [ ! -f "$inventory" ]; then
|
|
1226
|
-
echo "{}"
|
|
1227
|
-
return 0
|
|
1228
|
-
fi
|
|
1229
|
-
cat "$inventory"
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
# state_substrate_hardening_update_hook <path> <field-json>
|
|
1233
|
-
# Upsert a single hook entry in the inventory. <field-json> is a JSON object
|
|
1234
|
-
# of fields to set (e.g., '{"tier": 0, "has_test": true, "hardened_at": "..."}').
|
|
1235
|
-
# Fields not in the JSON keep their existing values; new fields are added.
|
|
1236
|
-
# If the inventory does not exist, creates it with schema_version=v0 + this entry.
|
|
1237
|
-
state_substrate_hardening_update_hook() {
|
|
1238
|
-
local hook_path="$1"
|
|
1239
|
-
local field_json="$2"
|
|
1240
|
-
if [ -z "$hook_path" ] || [ -z "$field_json" ]; then
|
|
1241
|
-
echo "state_substrate_hardening_update_hook: requires <path> <field-json>" >&2
|
|
1242
|
-
return 2
|
|
1243
|
-
fi
|
|
1244
|
-
local inventory="state/substrate-hardening/inventory.json"
|
|
1245
|
-
mkdir -p "$(dirname "$inventory")"
|
|
1246
|
-
local timestamp
|
|
1247
|
-
timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
1248
|
-
if [ ! -f "$inventory" ]; then
|
|
1249
|
-
jq -n \
|
|
1250
|
-
--arg ts "$timestamp" \
|
|
1251
|
-
--arg path "$hook_path" \
|
|
1252
|
-
--argjson fields "$field_json" \
|
|
1253
|
-
'{schema_version: "v0", generated_at: $ts, hooks: [({path: $path} + $fields)]}' \
|
|
1254
|
-
> "$inventory"
|
|
1255
|
-
return 0
|
|
1256
|
-
fi
|
|
1257
|
-
jq \
|
|
1258
|
-
--arg ts "$timestamp" \
|
|
1259
|
-
--arg path "$hook_path" \
|
|
1260
|
-
--argjson fields "$field_json" \
|
|
1261
|
-
'
|
|
1262
|
-
.generated_at = $ts |
|
|
1263
|
-
.hooks = (
|
|
1264
|
-
if (.hooks | map(select(.path == $path)) | length) > 0 then
|
|
1265
|
-
.hooks | map(if .path == $path then (. + $fields) else . end)
|
|
1266
|
-
else
|
|
1267
|
-
.hooks + [({path: $path} + $fields)]
|
|
1268
|
-
end
|
|
1269
|
-
)
|
|
1270
|
-
' "$inventory" > "${inventory}.tmp" && mv "${inventory}.tmp" "$inventory"
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
# === Release status accessor (per bet 2026-06-29c WU-2) ===
|
|
1274
|
-
#
|
|
1275
|
-
# state_whereami_release_status — returns JSON of release pipeline state.
|
|
1276
|
-
# Live (not cached). Composes data from:
|
|
1277
|
-
# - public bassclef HEAD sha (via bassclef-version.json on public bassclef)
|
|
1278
|
-
# - commits-ahead-of-public (local git rev-list)
|
|
1279
|
-
# - open release PR if any (number, age in hours, CI status)
|
|
1280
|
-
#
|
|
1281
|
-
# Shape:
|
|
1282
|
-
# {
|
|
1283
|
-
# "public_bassclef_head_sha": "<sha>" or null,
|
|
1284
|
-
# "commits_ahead_of_public": <int>,
|
|
1285
|
-
# "open_release_pr": null OR {
|
|
1286
|
-
# "number": <int>,
|
|
1287
|
-
# "age_hours": <int>,
|
|
1288
|
-
# "ci_status": "pass" | "fail" | "pending" | "unknown",
|
|
1289
|
-
# "title": "<str>",
|
|
1290
|
-
# "url": "<str>"
|
|
1291
|
-
# }
|
|
1292
|
-
# }
|
|
1293
|
-
#
|
|
1294
|
-
# Test-only overrides:
|
|
1295
|
-
# COMMITS_AHEAD_OVERRIDE — skip git computation; use this value
|
|
1296
|
-
# PUBLIC_SHA_OVERRIDE — skip gh api call for sha; use this value
|
|
1297
|
-
#
|
|
1298
|
-
# Returns empty when gh missing or fails (graceful).
|
|
1299
|
-
state_whereami_release_status() {
|
|
1300
|
-
if ! command -v gh >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
|
1301
|
-
return 0
|
|
1302
|
-
fi
|
|
1303
|
-
|
|
1304
|
-
# Resolve public_bassclef_head_sha
|
|
1305
|
-
local sha
|
|
1306
|
-
if [ -n "${PUBLIC_SHA_OVERRIDE:-}" ]; then
|
|
1307
|
-
sha="$PUBLIC_SHA_OVERRIDE"
|
|
1308
|
-
else
|
|
1309
|
-
sha=$(gh api repos/sunj-labs/bassclef/contents/bassclef-version.json --jq '.content' 2>/dev/null | base64 -d 2>/dev/null | jq -r '.release_sha // empty' 2>/dev/null)
|
|
1310
|
-
[ -z "$sha" ] && sha=""
|
|
1311
|
-
fi
|
|
1312
|
-
|
|
1313
|
-
# Resolve commits_ahead_of_public
|
|
1314
|
-
local commits_ahead
|
|
1315
|
-
if [ -n "${COMMITS_AHEAD_OVERRIDE:-}" ]; then
|
|
1316
|
-
commits_ahead="$COMMITS_AHEAD_OVERRIDE"
|
|
1317
|
-
elif [ -n "$sha" ]; then
|
|
1318
|
-
commits_ahead=$(git rev-list --count "${sha}..HEAD" 2>/dev/null || echo "0")
|
|
1319
|
-
else
|
|
1320
|
-
commits_ahead=0
|
|
1321
|
-
fi
|
|
1322
|
-
|
|
1323
|
-
# Resolve open_release_pr
|
|
1324
|
-
local pr_json="null"
|
|
1325
|
-
local pr_list
|
|
1326
|
-
pr_list=$(gh pr list -R sunj-labs/bassclef --state open --json number,title,createdAt,headRefName,statusCheckRollup,url 2>/dev/null || echo "[]")
|
|
1327
|
-
local release_pr_entry
|
|
1328
|
-
release_pr_entry=$(echo "$pr_list" | jq -c '
|
|
1329
|
-
[.[] | select(.headRefName | startswith("release-"))][0] // null
|
|
1330
|
-
' 2>/dev/null)
|
|
1331
|
-
if [ -n "$release_pr_entry" ] && [ "$release_pr_entry" != "null" ]; then
|
|
1332
|
-
local pr_date pr_epoch now_epoch age_hours pr_num pr_title pr_url ci_status
|
|
1333
|
-
pr_date=$(echo "$release_pr_entry" | jq -r '.createdAt')
|
|
1334
|
-
pr_num=$(echo "$release_pr_entry" | jq -r '.number')
|
|
1335
|
-
pr_title=$(echo "$release_pr_entry" | jq -r '.title')
|
|
1336
|
-
pr_url=$(echo "$release_pr_entry" | jq -r '.url')
|
|
1337
|
-
pr_epoch=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$pr_date" +%s 2>/dev/null || date -u -d "$pr_date" +%s 2>/dev/null || echo "0")
|
|
1338
|
-
now_epoch=$(date -u +%s)
|
|
1339
|
-
age_hours=$(( (now_epoch - pr_epoch) / 3600 ))
|
|
1340
|
-
|
|
1341
|
-
# CI status: derive from statusCheckRollup
|
|
1342
|
-
local rollup_states
|
|
1343
|
-
rollup_states=$(echo "$release_pr_entry" | jq -r '.statusCheckRollup[]?.conclusion // empty' 2>/dev/null | sort -u)
|
|
1344
|
-
if echo "$rollup_states" | grep -qiE "FAILURE|CANCELLED"; then
|
|
1345
|
-
ci_status="fail"
|
|
1346
|
-
elif echo "$rollup_states" | grep -qi "SUCCESS"; then
|
|
1347
|
-
ci_status="pass"
|
|
1348
|
-
elif [ -z "$rollup_states" ]; then
|
|
1349
|
-
ci_status="pending"
|
|
1350
|
-
else
|
|
1351
|
-
ci_status="unknown"
|
|
1352
|
-
fi
|
|
1353
|
-
|
|
1354
|
-
pr_json=$(jq -n \
|
|
1355
|
-
--argjson num "$pr_num" \
|
|
1356
|
-
--argjson age "$age_hours" \
|
|
1357
|
-
--arg ci "$ci_status" \
|
|
1358
|
-
--arg title "$pr_title" \
|
|
1359
|
-
--arg url "$pr_url" \
|
|
1360
|
-
'{number: $num, age_hours: $age, ci_status: $ci, title: $title, url: $url}')
|
|
1361
|
-
fi
|
|
1362
|
-
|
|
1363
|
-
# Compose final JSON
|
|
1364
|
-
local sha_json="null"
|
|
1365
|
-
[ -n "$sha" ] && sha_json=$(jq -n --arg s "$sha" '$s')
|
|
1366
|
-
|
|
1367
|
-
jq -n \
|
|
1368
|
-
--argjson sha "$sha_json" \
|
|
1369
|
-
--argjson commits "$commits_ahead" \
|
|
1370
|
-
--argjson pr "$pr_json" \
|
|
1371
|
-
'{public_bassclef_head_sha: $sha, commits_ahead_of_public: $commits, open_release_pr: $pr}'
|
|
1372
|
-
}
|