@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.
- package/README.md +1 -1
- package/dist/cli.cjs +79 -1
- package/dist/cli.js +80 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/artifact-ingestion-gate.sh +9 -3
- package/dist/lite/.claude/hooks/save-state.sh +6 -1
- package/dist/lite/.claude/hooks/session-reflection.d/97-last-block-count.sh +53 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/97-last-block-count.test.sh +131 -0
- package/dist/lite/.claude/hooks/session-reflection.sh +22 -0
- package/dist/lite/.claude/rules/last-block-marker.md +79 -0
- package/dist/lite/.claude/skills/build/SKILL.md +7 -1
- package/dist/lite/.claude/skills/ia-model/SKILL.md +8 -0
- package/dist/lite/.claude/skills/interaction-design/SKILL.md +8 -0
- package/dist/lite/.claude/skills/jtbd-tasks/SKILL.md +9 -1
- package/dist/lite/.claude/skills/last-block/SKILL.md +99 -0
- package/dist/lite/.claude/skills/launch/SKILL.md +18 -12
- package/dist/lite/.claude/skills/objectory-decompose/SKILL.md +5 -17
- package/dist/lite/.claude/skills/use-case/SKILL.md +5 -4
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/lib/composer-preflight.sh +12 -0
- package/dist/lite/lib/last-block.sh +153 -0
- package/dist/lite/lib/state.sh +11 -0
- package/dist/lite/scripts/interpret-input.sh +19 -4
- package/dist/lite/standards/lite-manifest-schema-changes.md +70 -0
- package/dist/lite/standards/lite-manifest.json +47 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ your browser.
|
|
|
67
67
|
|
|
68
68
|
## Current release
|
|
69
69
|
|
|
70
|
-
<!-- version-start -->1.9.
|
|
70
|
+
<!-- version-start -->1.9.12<!-- version-end -->
|
|
71
71
|
|
|
72
72
|
See [CHANGELOG.md](CHANGELOG.md) for the full changelog. Adopter-facing
|
|
73
73
|
release notes live at [docs.bassclef.dev/docs/whats-new/releases](https://docs.bassclef.dev/docs/whats-new/releases).
|
package/dist/cli.cjs
CHANGED
|
@@ -272,6 +272,47 @@ function hashContent(content) {
|
|
|
272
272
|
normalized = normalized.replace(/\r\n/g, "\n");
|
|
273
273
|
return node_crypto.createHash("sha256").update(normalized, "utf8").digest("hex");
|
|
274
274
|
}
|
|
275
|
+
const SCHEMA_VERSION = "1.0.0";
|
|
276
|
+
const WRITER_SLUG = "bassclef-init-npm";
|
|
277
|
+
function registerInstallWrittenPaths(targetDir, paths, now = /* @__PURE__ */ new Date()) {
|
|
278
|
+
if (paths.length === 0) return;
|
|
279
|
+
const manifestPath = node_path.join(targetDir, "state", "install-written-paths.json");
|
|
280
|
+
let manifest = {
|
|
281
|
+
schema_version: SCHEMA_VERSION,
|
|
282
|
+
entries: []
|
|
283
|
+
};
|
|
284
|
+
if (node_fs.existsSync(manifestPath)) {
|
|
285
|
+
try {
|
|
286
|
+
const parsed = JSON.parse(node_fs.readFileSync(manifestPath, "utf8"));
|
|
287
|
+
if (parsed && typeof parsed === "object" && "entries" in parsed && Array.isArray(parsed.entries)) {
|
|
288
|
+
manifest = parsed;
|
|
289
|
+
manifest.schema_version = SCHEMA_VERSION;
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const writtenAt = now.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
295
|
+
const uniqueInputs = /* @__PURE__ */ new Map();
|
|
296
|
+
for (const p of paths) {
|
|
297
|
+
uniqueInputs.set(p.relativePath, p);
|
|
298
|
+
}
|
|
299
|
+
const incomingPaths = new Set(uniqueInputs.keys());
|
|
300
|
+
manifest.entries = manifest.entries.filter((e) => !incomingPaths.has(e.path));
|
|
301
|
+
for (const p of uniqueInputs.values()) {
|
|
302
|
+
manifest.entries.push({
|
|
303
|
+
path: p.relativePath,
|
|
304
|
+
writer: WRITER_SLUG,
|
|
305
|
+
written_at: writtenAt,
|
|
306
|
+
content_hash: `sha256:${p.contentHashSha256}`
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
node_fs.mkdirSync(node_path.dirname(manifestPath), { recursive: true });
|
|
310
|
+
node_fs.writeFileSync(
|
|
311
|
+
manifestPath,
|
|
312
|
+
JSON.stringify(manifest, null, 2) + "\n",
|
|
313
|
+
"utf8"
|
|
314
|
+
);
|
|
315
|
+
}
|
|
275
316
|
const SUBSTRATE_CONFIG_TEMPLATE_VERSION = "0.0.1";
|
|
276
317
|
function substrateConfigMdTemplate(pkgVersion) {
|
|
277
318
|
return [
|
|
@@ -549,6 +590,7 @@ function copySubstrate(targetDir, options = {}) {
|
|
|
549
590
|
copied: [],
|
|
550
591
|
copiedEntries: [],
|
|
551
592
|
refused: [],
|
|
593
|
+
unchanged: [],
|
|
552
594
|
errored: [],
|
|
553
595
|
erroredMessages: [],
|
|
554
596
|
hookCount: 0,
|
|
@@ -736,6 +778,16 @@ function copyOne(relPath, bundleRoot, options, result, scopeDecision) {
|
|
|
736
778
|
} catch (e) {
|
|
737
779
|
if (e instanceof WriteError) {
|
|
738
780
|
if (e.kind === "AlreadyExists") {
|
|
781
|
+
if (scope === "user") {
|
|
782
|
+
try {
|
|
783
|
+
const existing = node_fs.readFileSync(targetPath, "utf8");
|
|
784
|
+
if (hashContent(existing) === hashContent(outputContent)) {
|
|
785
|
+
result.unchanged.push(adopterRelPath);
|
|
786
|
+
return "unchanged";
|
|
787
|
+
}
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
}
|
|
739
791
|
result.refused.push(adopterRelPath);
|
|
740
792
|
return "refused";
|
|
741
793
|
}
|
|
@@ -1079,13 +1131,14 @@ function buildInitReport(input) {
|
|
|
1079
1131
|
else project += 1;
|
|
1080
1132
|
}
|
|
1081
1133
|
const refused = input.refused.length;
|
|
1134
|
+
const unchanged = input.unchanged?.length ?? 0;
|
|
1082
1135
|
const errored = input.errored.length;
|
|
1083
1136
|
const written = input.configs.length + input.entries.length;
|
|
1084
1137
|
return {
|
|
1085
1138
|
schema_version: 3,
|
|
1086
1139
|
tier: input.tier,
|
|
1087
1140
|
totals: {
|
|
1088
|
-
files: written + refused + errored,
|
|
1141
|
+
files: written + refused + unchanged + errored,
|
|
1089
1142
|
written,
|
|
1090
1143
|
project,
|
|
1091
1144
|
user,
|
|
@@ -1098,6 +1151,7 @@ function buildInitReport(input) {
|
|
|
1098
1151
|
files: catalog.hooks
|
|
1099
1152
|
},
|
|
1100
1153
|
refused,
|
|
1154
|
+
unchanged,
|
|
1101
1155
|
errored,
|
|
1102
1156
|
failed: refused + errored
|
|
1103
1157
|
};
|
|
@@ -1203,6 +1257,7 @@ function runInit(argv) {
|
|
|
1203
1257
|
entries: outcome.result?.wouldCopyEntries ?? [],
|
|
1204
1258
|
configs: plans.map((pl) => ({ path: pl.relativePath })),
|
|
1205
1259
|
refused: outcome.result?.refused ?? [],
|
|
1260
|
+
unchanged: outcome.result?.unchanged ?? [],
|
|
1206
1261
|
// A dry run still reads every source file, so it can still fail
|
|
1207
1262
|
// to read one. Hard-coding this empty hid those failures from
|
|
1208
1263
|
// the JSON (RFC-0005 A-3).
|
|
@@ -1301,6 +1356,7 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, say
|
|
|
1301
1356
|
}
|
|
1302
1357
|
const parts = [];
|
|
1303
1358
|
if (result.copied.length > 0) parts.push(`${result.copied.length} substrate files copied`);
|
|
1359
|
+
if (result.unchanged.length > 0) parts.push(`${result.unchanged.length} already-present`);
|
|
1304
1360
|
if (result.refused.length > 0) parts.push(`${result.refused.length} refused`);
|
|
1305
1361
|
if (result.errored.length > 0) parts.push(`${result.errored.length} error(s)`);
|
|
1306
1362
|
say(`bassclef init: ${parts.join(", ")}.
|
|
@@ -1531,12 +1587,25 @@ function runReal$1(plans, force, verbose, targetDir, allowRoot, json, skipStatus
|
|
|
1531
1587
|
entries: walker.result?.copiedEntries ?? [],
|
|
1532
1588
|
configs: results.map((r) => ({ path: r.plan.relativePath })),
|
|
1533
1589
|
refused: walker.result?.refused ?? [],
|
|
1590
|
+
unchanged: walker.result?.unchanged ?? [],
|
|
1534
1591
|
errored: walker.result?.errored ?? [],
|
|
1535
1592
|
hookCount: walker.result?.hookCount ?? 0,
|
|
1536
1593
|
declaredHooksCopied,
|
|
1537
1594
|
tier: RESOLVED_TIER
|
|
1538
1595
|
});
|
|
1539
1596
|
writeManifest(targetDir, results, walker.result);
|
|
1597
|
+
const createdConfigPaths = results.filter((r) => r.outcome === "created").map((r) => ({
|
|
1598
|
+
relativePath: r.plan.relativePath,
|
|
1599
|
+
contentHashSha256: hashContent(r.plan.content)
|
|
1600
|
+
}));
|
|
1601
|
+
const createdBundlePaths = (walker.result?.copiedEntries ?? []).filter((e) => typeof e.content_hash_sha256 === "string").map((e) => ({
|
|
1602
|
+
relativePath: e.path,
|
|
1603
|
+
contentHashSha256: e.content_hash_sha256
|
|
1604
|
+
}));
|
|
1605
|
+
registerInstallWrittenPaths(targetDir, [
|
|
1606
|
+
...createdConfigPaths,
|
|
1607
|
+
...createdBundlePaths
|
|
1608
|
+
]);
|
|
1540
1609
|
if (walker.code === 0) {
|
|
1541
1610
|
say(
|
|
1542
1611
|
`bassclef init: your substrate lives under .claude/. Add .claude/ to .gitignore if you have not.
|
|
@@ -1606,6 +1675,15 @@ function writeManifest(targetDir, results, walkerResult) {
|
|
|
1606
1675
|
source: "bundle"
|
|
1607
1676
|
});
|
|
1608
1677
|
}
|
|
1678
|
+
for (const path of walkerResult.unchanged) {
|
|
1679
|
+
entries.push({
|
|
1680
|
+
path,
|
|
1681
|
+
template: `bundle:${path}`,
|
|
1682
|
+
template_version: index.version,
|
|
1683
|
+
outcome: "unchanged",
|
|
1684
|
+
source: "bundle"
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1609
1687
|
for (const path of walkerResult.errored) {
|
|
1610
1688
|
entries.push({
|
|
1611
1689
|
path,
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { version } from "./index.js";
|
|
3
|
-
import { realpathSync, statSync, constants, lstatSync, mkdirSync, accessSync, unlinkSync, openSync, writeSync, closeSync, readlinkSync,
|
|
3
|
+
import { realpathSync, statSync, constants, lstatSync, mkdirSync, accessSync, unlinkSync, openSync, writeSync, closeSync, readlinkSync, existsSync, readFileSync, writeFileSync, chmodSync, readdirSync, renameSync } from "node:fs";
|
|
4
4
|
import { isAbsolute, resolve, dirname, join, normalize, relative, basename as basename$1 } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
@@ -249,6 +249,47 @@ function hashContent(content) {
|
|
|
249
249
|
normalized = normalized.replace(/\r\n/g, "\n");
|
|
250
250
|
return createHash("sha256").update(normalized, "utf8").digest("hex");
|
|
251
251
|
}
|
|
252
|
+
const SCHEMA_VERSION = "1.0.0";
|
|
253
|
+
const WRITER_SLUG = "bassclef-init-npm";
|
|
254
|
+
function registerInstallWrittenPaths(targetDir, paths, now = /* @__PURE__ */ new Date()) {
|
|
255
|
+
if (paths.length === 0) return;
|
|
256
|
+
const manifestPath = join(targetDir, "state", "install-written-paths.json");
|
|
257
|
+
let manifest = {
|
|
258
|
+
schema_version: SCHEMA_VERSION,
|
|
259
|
+
entries: []
|
|
260
|
+
};
|
|
261
|
+
if (existsSync(manifestPath)) {
|
|
262
|
+
try {
|
|
263
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
264
|
+
if (parsed && typeof parsed === "object" && "entries" in parsed && Array.isArray(parsed.entries)) {
|
|
265
|
+
manifest = parsed;
|
|
266
|
+
manifest.schema_version = SCHEMA_VERSION;
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const writtenAt = now.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
272
|
+
const uniqueInputs = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const p of paths) {
|
|
274
|
+
uniqueInputs.set(p.relativePath, p);
|
|
275
|
+
}
|
|
276
|
+
const incomingPaths = new Set(uniqueInputs.keys());
|
|
277
|
+
manifest.entries = manifest.entries.filter((e) => !incomingPaths.has(e.path));
|
|
278
|
+
for (const p of uniqueInputs.values()) {
|
|
279
|
+
manifest.entries.push({
|
|
280
|
+
path: p.relativePath,
|
|
281
|
+
writer: WRITER_SLUG,
|
|
282
|
+
written_at: writtenAt,
|
|
283
|
+
content_hash: `sha256:${p.contentHashSha256}`
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
287
|
+
writeFileSync(
|
|
288
|
+
manifestPath,
|
|
289
|
+
JSON.stringify(manifest, null, 2) + "\n",
|
|
290
|
+
"utf8"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
252
293
|
const SUBSTRATE_CONFIG_TEMPLATE_VERSION = "0.0.1";
|
|
253
294
|
function substrateConfigMdTemplate(pkgVersion) {
|
|
254
295
|
return [
|
|
@@ -526,6 +567,7 @@ function copySubstrate(targetDir, options = {}) {
|
|
|
526
567
|
copied: [],
|
|
527
568
|
copiedEntries: [],
|
|
528
569
|
refused: [],
|
|
570
|
+
unchanged: [],
|
|
529
571
|
errored: [],
|
|
530
572
|
erroredMessages: [],
|
|
531
573
|
hookCount: 0,
|
|
@@ -713,6 +755,16 @@ function copyOne(relPath, bundleRoot, options, result, scopeDecision) {
|
|
|
713
755
|
} catch (e) {
|
|
714
756
|
if (e instanceof WriteError) {
|
|
715
757
|
if (e.kind === "AlreadyExists") {
|
|
758
|
+
if (scope === "user") {
|
|
759
|
+
try {
|
|
760
|
+
const existing = readFileSync(targetPath, "utf8");
|
|
761
|
+
if (hashContent(existing) === hashContent(outputContent)) {
|
|
762
|
+
result.unchanged.push(adopterRelPath);
|
|
763
|
+
return "unchanged";
|
|
764
|
+
}
|
|
765
|
+
} catch {
|
|
766
|
+
}
|
|
767
|
+
}
|
|
716
768
|
result.refused.push(adopterRelPath);
|
|
717
769
|
return "refused";
|
|
718
770
|
}
|
|
@@ -1056,13 +1108,14 @@ function buildInitReport(input) {
|
|
|
1056
1108
|
else project += 1;
|
|
1057
1109
|
}
|
|
1058
1110
|
const refused = input.refused.length;
|
|
1111
|
+
const unchanged = input.unchanged?.length ?? 0;
|
|
1059
1112
|
const errored = input.errored.length;
|
|
1060
1113
|
const written = input.configs.length + input.entries.length;
|
|
1061
1114
|
return {
|
|
1062
1115
|
schema_version: 3,
|
|
1063
1116
|
tier: input.tier,
|
|
1064
1117
|
totals: {
|
|
1065
|
-
files: written + refused + errored,
|
|
1118
|
+
files: written + refused + unchanged + errored,
|
|
1066
1119
|
written,
|
|
1067
1120
|
project,
|
|
1068
1121
|
user,
|
|
@@ -1075,6 +1128,7 @@ function buildInitReport(input) {
|
|
|
1075
1128
|
files: catalog.hooks
|
|
1076
1129
|
},
|
|
1077
1130
|
refused,
|
|
1131
|
+
unchanged,
|
|
1078
1132
|
errored,
|
|
1079
1133
|
failed: refused + errored
|
|
1080
1134
|
};
|
|
@@ -1180,6 +1234,7 @@ function runInit(argv) {
|
|
|
1180
1234
|
entries: outcome.result?.wouldCopyEntries ?? [],
|
|
1181
1235
|
configs: plans.map((pl) => ({ path: pl.relativePath })),
|
|
1182
1236
|
refused: outcome.result?.refused ?? [],
|
|
1237
|
+
unchanged: outcome.result?.unchanged ?? [],
|
|
1183
1238
|
// A dry run still reads every source file, so it can still fail
|
|
1184
1239
|
// to read one. Hard-coding this empty hid those failures from
|
|
1185
1240
|
// the JSON (RFC-0005 A-3).
|
|
@@ -1278,6 +1333,7 @@ function dispatchSubstrateCopy(targetDir, force, verbose, dryRun, allowRoot, say
|
|
|
1278
1333
|
}
|
|
1279
1334
|
const parts = [];
|
|
1280
1335
|
if (result.copied.length > 0) parts.push(`${result.copied.length} substrate files copied`);
|
|
1336
|
+
if (result.unchanged.length > 0) parts.push(`${result.unchanged.length} already-present`);
|
|
1281
1337
|
if (result.refused.length > 0) parts.push(`${result.refused.length} refused`);
|
|
1282
1338
|
if (result.errored.length > 0) parts.push(`${result.errored.length} error(s)`);
|
|
1283
1339
|
say(`bassclef init: ${parts.join(", ")}.
|
|
@@ -1508,12 +1564,25 @@ function runReal$1(plans, force, verbose, targetDir, allowRoot, json, skipStatus
|
|
|
1508
1564
|
entries: walker.result?.copiedEntries ?? [],
|
|
1509
1565
|
configs: results.map((r) => ({ path: r.plan.relativePath })),
|
|
1510
1566
|
refused: walker.result?.refused ?? [],
|
|
1567
|
+
unchanged: walker.result?.unchanged ?? [],
|
|
1511
1568
|
errored: walker.result?.errored ?? [],
|
|
1512
1569
|
hookCount: walker.result?.hookCount ?? 0,
|
|
1513
1570
|
declaredHooksCopied,
|
|
1514
1571
|
tier: RESOLVED_TIER
|
|
1515
1572
|
});
|
|
1516
1573
|
writeManifest(targetDir, results, walker.result);
|
|
1574
|
+
const createdConfigPaths = results.filter((r) => r.outcome === "created").map((r) => ({
|
|
1575
|
+
relativePath: r.plan.relativePath,
|
|
1576
|
+
contentHashSha256: hashContent(r.plan.content)
|
|
1577
|
+
}));
|
|
1578
|
+
const createdBundlePaths = (walker.result?.copiedEntries ?? []).filter((e) => typeof e.content_hash_sha256 === "string").map((e) => ({
|
|
1579
|
+
relativePath: e.path,
|
|
1580
|
+
contentHashSha256: e.content_hash_sha256
|
|
1581
|
+
}));
|
|
1582
|
+
registerInstallWrittenPaths(targetDir, [
|
|
1583
|
+
...createdConfigPaths,
|
|
1584
|
+
...createdBundlePaths
|
|
1585
|
+
]);
|
|
1517
1586
|
if (walker.code === 0) {
|
|
1518
1587
|
say(
|
|
1519
1588
|
`bassclef init: your substrate lives under .claude/. Add .claude/ to .gitignore if you have not.
|
|
@@ -1583,6 +1652,15 @@ function writeManifest(targetDir, results, walkerResult) {
|
|
|
1583
1652
|
source: "bundle"
|
|
1584
1653
|
});
|
|
1585
1654
|
}
|
|
1655
|
+
for (const path of walkerResult.unchanged) {
|
|
1656
|
+
entries.push({
|
|
1657
|
+
path,
|
|
1658
|
+
template: `bundle:${path}`,
|
|
1659
|
+
template_version: version,
|
|
1660
|
+
outcome: "unchanged",
|
|
1661
|
+
source: "bundle"
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1586
1664
|
for (const path of walkerResult.errored) {
|
|
1587
1665
|
entries.push({
|
|
1588
1666
|
path,
|
package/dist/index.cjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const version: "1.9.
|
|
1
|
+
export declare const version: "1.9.12";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -143,9 +143,15 @@ if [ "$HAS_SOURCES_READ" -eq 1 ]; then
|
|
|
143
143
|
exit
|
|
144
144
|
}
|
|
145
145
|
')
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
# cli#311 cure — treat null / ~ / empty as "no parent declared" (Peirce three-case).
|
|
147
|
+
# YAML null shorthand is `null`, `~`, or an empty value. Prior code caught empty
|
|
148
|
+
# only; `parent_bet: null` tripped the gate on root goals that genuinely have no
|
|
149
|
+
# parent. Mirrors the #70 cure in bet-doc-gate.sh.
|
|
150
|
+
case "$PARENT_VALUE" in
|
|
151
|
+
"" | "null" | "Null" | "NULL" | "~" | "None")
|
|
152
|
+
continue
|
|
153
|
+
;;
|
|
154
|
+
esac
|
|
149
155
|
|
|
150
156
|
# Check if the parent path (or its basename without extension) appears
|
|
151
157
|
# in the Sources-read section. Accepts either:
|
|
@@ -209,7 +209,12 @@ DIRTY=$(git status --porcelain 2>/dev/null | grep -v '\.DS_Store' | grep -v 'SES
|
|
|
209
209
|
if [ -n "$DIRTY" ]; then
|
|
210
210
|
# Stage tracked modified files + any docs/ or chronicle/ or runtime state
|
|
211
211
|
git add -u 2>/dev/null || true
|
|
212
|
-
|
|
212
|
+
# docs/ except docs/prototypes/ — prototypes are review-gated mocks per
|
|
213
|
+
# .claude/rules/prototype-workflow.md; auto-save must not commit them
|
|
214
|
+
# before the operator confirms the rendered output (cli#323).
|
|
215
|
+
# Same pathspec pattern used for state/markers below.
|
|
216
|
+
git add 'docs/' ':!docs/prototypes' ':!docs/prototypes/**' 2>/dev/null || true
|
|
217
|
+
git add chronicle/ specs/ 2>/dev/null || true
|
|
213
218
|
# state/ except state/markers/ — markers are runtime gate evidence that
|
|
214
219
|
# trigger session-rescue false-positives if committed (bassclef#341 / WU-A2).
|
|
215
220
|
# Negative pathspec syntax requires git >= 2.13 (stable since 2017).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# install-class: dual
|
|
4
|
+
# Session-reflection fragment — surface last-block marker count.
|
|
5
|
+
#
|
|
6
|
+
# Per @luminary don-norman signifier: when BLOCK banners from prior
|
|
7
|
+
# sessions sit in state/markers/last-block/, this fragment tells the
|
|
8
|
+
# adopter they exist and names /last-block as the retrieval path.
|
|
9
|
+
#
|
|
10
|
+
# Fires once at session-start. Reads marker count. Prints one line to
|
|
11
|
+
# stdout when count > 0. Silent on empty.
|
|
12
|
+
#
|
|
13
|
+
# Composition — sourced by session-reflection.sh under set -e. Must use
|
|
14
|
+
# `return`, not `exit`, else the coordinator dies. Per test-sufficiency
|
|
15
|
+
# criterion 12 (invocation-mode variation).
|
|
16
|
+
#
|
|
17
|
+
# Per bassclef-upstream#2036 Finding #10 + UC-skill-last-block.md.
|
|
18
|
+
|
|
19
|
+
# Resolve directory (same convention as lib/last-block.sh).
|
|
20
|
+
_last_block_count_dir() {
|
|
21
|
+
if [ -n "${LAST_BLOCK_DIR:-}" ]; then
|
|
22
|
+
echo "$LAST_BLOCK_DIR"
|
|
23
|
+
else
|
|
24
|
+
echo "${CLAUDE_PROJECT_DIR:-.}/state/markers/last-block"
|
|
25
|
+
fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
LBC_DIR="$(_last_block_count_dir)"
|
|
29
|
+
|
|
30
|
+
if [ ! -d "$LBC_DIR" ]; then
|
|
31
|
+
# Fresh install or dir never created. Silent.
|
|
32
|
+
# shellcheck disable=SC2317 # dual-use sourced-or-standalone exit path
|
|
33
|
+
return 0 2>/dev/null || exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
LBC_COUNT=$(find "$LBC_DIR" -maxdepth 1 -name '*.marker' -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
37
|
+
|
|
38
|
+
if [ "$LBC_COUNT" -gt 0 ]; then
|
|
39
|
+
if [ "$LBC_COUNT" = "1" ]; then
|
|
40
|
+
echo "### LAST BLOCK"
|
|
41
|
+
echo ""
|
|
42
|
+
echo "1 BLOCK banner stored. Read it with /last-block."
|
|
43
|
+
echo ""
|
|
44
|
+
else
|
|
45
|
+
echo "### LAST BLOCKS"
|
|
46
|
+
echo ""
|
|
47
|
+
echo "${LBC_COUNT} BLOCK banners stored. Read the newest with /last-block, or list all with /last-block --list."
|
|
48
|
+
echo ""
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# shellcheck disable=SC2317 # dual-use sourced-or-standalone exit path
|
|
53
|
+
return 0 2>/dev/null || exit 0
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: lite
|
|
3
|
+
# fixtures: bassclef-upstream-self
|
|
4
|
+
# shellcheck disable=SC2034
|
|
5
|
+
# Tier 0 test for .claude/hooks/session-reflection.d/97-last-block-count.sh
|
|
6
|
+
#
|
|
7
|
+
# Covers invocation-mode variation (criterion 12 of test-sufficiency rubric):
|
|
8
|
+
# both standalone and sourced-under-set-e paths.
|
|
9
|
+
#
|
|
10
|
+
# Ticket: bassclef-upstream#2036 Finding #10
|
|
11
|
+
#
|
|
12
|
+
# test-list:
|
|
13
|
+
# [x] T01: standalone + no dir -> silent, exit 0, no return error
|
|
14
|
+
# [x] T02: standalone + empty dir -> silent, exit 0
|
|
15
|
+
# [x] T03: standalone + 1 marker -> prints "1 BLOCK banner stored"
|
|
16
|
+
# [x] T04: standalone + 3 markers -> prints "3 BLOCK banners stored"
|
|
17
|
+
# [x] T05: sourced under set -e -> parent survives (no exit)
|
|
18
|
+
# [x] T06: honors LAST_BLOCK_DIR env override
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
FRAG_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
24
|
+
FRAG="$FRAG_DIR/97-last-block-count.sh"
|
|
25
|
+
|
|
26
|
+
if [ ! -f "$FRAG" ]; then
|
|
27
|
+
echo "RED: $FRAG does not exist yet" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
FIXTURE_ROOT="$(mktemp -d)"
|
|
32
|
+
RESULTS_FILE="$FIXTURE_ROOT/results.log"
|
|
33
|
+
touch "$RESULTS_FILE"
|
|
34
|
+
trap 'rm -rf "$FIXTURE_ROOT"' EXIT
|
|
35
|
+
|
|
36
|
+
pass() { echo "PASS" >> "$RESULTS_FILE"; echo "PASS: $1"; }
|
|
37
|
+
fail() { echo "FAIL" >> "$RESULTS_FILE"; echo "FAIL: $1"; }
|
|
38
|
+
|
|
39
|
+
# T01: standalone + no dir
|
|
40
|
+
( LAST_BLOCK_DIR="$FIXTURE_ROOT/nonexistent"
|
|
41
|
+
export LAST_BLOCK_DIR
|
|
42
|
+
OUT=$(bash "$FRAG" 2>&1)
|
|
43
|
+
RC=$?
|
|
44
|
+
if [ "$RC" = "0" ] && [ -z "$OUT" ]; then
|
|
45
|
+
pass "T01: standalone + no dir -> silent"
|
|
46
|
+
else
|
|
47
|
+
fail "T01: standalone + no dir -> silent (rc=$RC out=$OUT)"
|
|
48
|
+
fi
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# T02: standalone + empty dir
|
|
52
|
+
( LAST_BLOCK_DIR="$(mktemp -d)"
|
|
53
|
+
export LAST_BLOCK_DIR
|
|
54
|
+
OUT=$(bash "$FRAG" 2>&1)
|
|
55
|
+
RC=$?
|
|
56
|
+
if [ "$RC" = "0" ] && [ -z "$OUT" ]; then
|
|
57
|
+
pass "T02: standalone + empty dir -> silent"
|
|
58
|
+
else
|
|
59
|
+
fail "T02: standalone + empty dir -> silent (rc=$RC out=$OUT)"
|
|
60
|
+
fi
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# T03: standalone + 1 marker
|
|
64
|
+
( DIR="$(mktemp -d)"
|
|
65
|
+
LAST_BLOCK_DIR="$DIR"
|
|
66
|
+
export LAST_BLOCK_DIR
|
|
67
|
+
echo "hook: a" > "$DIR/a.marker"
|
|
68
|
+
OUT=$(bash "$FRAG" 2>&1)
|
|
69
|
+
if echo "$OUT" | grep -qF "1 BLOCK banner stored"; then
|
|
70
|
+
pass "T03: 1 marker -> prints singular"
|
|
71
|
+
else
|
|
72
|
+
fail "T03: 1 marker (got: $OUT)"
|
|
73
|
+
fi
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# T04: standalone + 3 markers
|
|
77
|
+
( DIR="$(mktemp -d)"
|
|
78
|
+
LAST_BLOCK_DIR="$DIR"
|
|
79
|
+
export LAST_BLOCK_DIR
|
|
80
|
+
echo "hook: a" > "$DIR/a.marker"
|
|
81
|
+
echo "hook: b" > "$DIR/b.marker"
|
|
82
|
+
echo "hook: c" > "$DIR/c.marker"
|
|
83
|
+
OUT=$(bash "$FRAG" 2>&1)
|
|
84
|
+
if echo "$OUT" | grep -qF "3 BLOCK banners stored"; then
|
|
85
|
+
pass "T04: 3 markers -> prints plural with count"
|
|
86
|
+
else
|
|
87
|
+
fail "T04: 3 markers (got: $OUT)"
|
|
88
|
+
fi
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# T05: sourced under set -e -> parent survives (criterion 12)
|
|
92
|
+
( DIR="$(mktemp -d)"
|
|
93
|
+
LAST_BLOCK_DIR="$DIR"
|
|
94
|
+
export LAST_BLOCK_DIR
|
|
95
|
+
echo "hook: a" > "$DIR/a.marker"
|
|
96
|
+
# Simulate coordinator: set -e wrapper + source
|
|
97
|
+
CANARY_FILE="$FIXTURE_ROOT/canary-t05"
|
|
98
|
+
(
|
|
99
|
+
set -e
|
|
100
|
+
# shellcheck source=/dev/null
|
|
101
|
+
source "$FRAG"
|
|
102
|
+
# If frag used `exit` instead of `return`, this line never runs.
|
|
103
|
+
touch "$CANARY_FILE"
|
|
104
|
+
) >/dev/null 2>&1 || true
|
|
105
|
+
if [ -f "$CANARY_FILE" ]; then
|
|
106
|
+
pass "T05: sourced under set -e survives (canary ran)"
|
|
107
|
+
else
|
|
108
|
+
fail "T05: sourced under set -e survives (canary missing)"
|
|
109
|
+
fi
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# T06: honors LAST_BLOCK_DIR env override
|
|
113
|
+
( DIR="$(mktemp -d)"
|
|
114
|
+
LAST_BLOCK_DIR="$DIR"
|
|
115
|
+
export LAST_BLOCK_DIR
|
|
116
|
+
echo "hook: a" > "$DIR/a.marker"
|
|
117
|
+
OUT=$(bash "$FRAG" 2>&1)
|
|
118
|
+
if echo "$OUT" | grep -qF "1 BLOCK banner stored"; then
|
|
119
|
+
pass "T06: honors LAST_BLOCK_DIR override"
|
|
120
|
+
else
|
|
121
|
+
fail "T06: honors LAST_BLOCK_DIR override (got: $OUT)"
|
|
122
|
+
fi
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
echo
|
|
126
|
+
echo "=== Results ==="
|
|
127
|
+
PASS=$(awk '/^PASS$/ {c++} END {print c+0}' "$RESULTS_FILE")
|
|
128
|
+
FAIL=$(awk '/^FAIL$/ {c++} END {print c+0}' "$RESULTS_FILE")
|
|
129
|
+
echo "PASS: $PASS"
|
|
130
|
+
echo "FAIL: $FAIL"
|
|
131
|
+
if [ "$FAIL" -gt 0 ]; then exit 1; fi
|
|
@@ -74,6 +74,14 @@ else
|
|
|
74
74
|
fi
|
|
75
75
|
export BASSCLEF_DIR
|
|
76
76
|
|
|
77
|
+
# Source lib/last-block.sh so banner calls persist markers. Per Finding
|
|
78
|
+
# #10 — adopters need a first-class way to retrieve BLOCK text they saw.
|
|
79
|
+
# The lib fails soft; missing lib leaves the banners untouched.
|
|
80
|
+
# shellcheck source=/dev/null
|
|
81
|
+
if [ -f "${BASSCLEF_DIR}/lib/last-block.sh" ]; then
|
|
82
|
+
source "${BASSCLEF_DIR}/lib/last-block.sh"
|
|
83
|
+
fi
|
|
84
|
+
|
|
77
85
|
# High-contrast BLOCKED banner; used by every module that raises a gap.
|
|
78
86
|
# RFC-0008 F3 — "Silence is not deferral" is a commitment device. It works
|
|
79
87
|
# because it forecloses the easy out, and it is consumable: spend it on a
|
|
@@ -81,9 +89,13 @@ export BASSCLEF_DIR
|
|
|
81
89
|
# Callers that can prove their own state is benign pass a second argument
|
|
82
90
|
# and get a softer close. Callers that pass one argument behave exactly as
|
|
83
91
|
# before, so the other 27 sites are untouched.
|
|
92
|
+
#
|
|
93
|
+
# Per Finding #10 — each call now also writes a marker so the adopter can
|
|
94
|
+
# retrieve the exact banner text later via /last-block.
|
|
84
95
|
blocked_banner() {
|
|
85
96
|
local msg="$1"
|
|
86
97
|
local severity="${2:-blocking}"
|
|
98
|
+
local hook_name="${BASSCLEF_CALLING_HOOK:-session-reflection}"
|
|
87
99
|
echo ""
|
|
88
100
|
case "$severity" in
|
|
89
101
|
info)
|
|
@@ -106,9 +118,14 @@ blocked_banner() {
|
|
|
106
118
|
echo "────────────────────────────────────────────"
|
|
107
119
|
echo "ACTION: resolve OR explicitly defer (per .claude/rules/blocked-items.md)."
|
|
108
120
|
echo "Silence is not deferral. Propose this as item #1 in your session plan."
|
|
121
|
+
echo "(Retrieve this banner later with: /last-block ${hook_name})"
|
|
109
122
|
;;
|
|
110
123
|
esac
|
|
111
124
|
echo ""
|
|
125
|
+
# Persist marker for /last-block retrieval. Fail-soft.
|
|
126
|
+
if type -t record_last_block >/dev/null 2>&1; then
|
|
127
|
+
record_last_block "$hook_name" "$severity" "$msg" 2>/dev/null || true
|
|
128
|
+
fi
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
# Vendor-swap constant — the AI in the harness running bassclef. Adopter
|
|
@@ -131,7 +148,12 @@ guardrail_banner() {
|
|
|
131
148
|
echo "────────────────────────────────────────────"
|
|
132
149
|
echo "bassclef is steering $AI_NAME to $step — $retry"
|
|
133
150
|
echo "────────────────────────────────────────────"
|
|
151
|
+
echo "(Retrieve this banner later with: /last-block ${hook_name})"
|
|
134
152
|
echo ""
|
|
153
|
+
# Persist marker for /last-block retrieval. Fail-soft.
|
|
154
|
+
if type -t record_last_block >/dev/null 2>&1; then
|
|
155
|
+
record_last_block "$hook_name" "guardrail" "$step — $retry" 2>/dev/null || true
|
|
156
|
+
fi
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
# RFC-0008 F6 — this block is steering aimed at the agent, but the adopter
|