@massu/core 1.13.0 → 1.14.0

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.
@@ -1049,6 +1049,45 @@ function initMemorySchema(db) {
1049
1049
  CREATE INDEX IF NOT EXISTS idx_so_file ON shared_observations(file_path);
1050
1050
  CREATE INDEX IF NOT EXISTS idx_so_module ON shared_observations(module);
1051
1051
  `);
1052
+ db.exec(`
1053
+ CREATE TABLE IF NOT EXISTS team_promotion_outbound (
1054
+ prompt_hash TEXT PRIMARY KEY,
1055
+ destination TEXT NOT NULL,
1056
+ draft_text TEXT NOT NULL,
1057
+ score REAL,
1058
+ signals_json TEXT NOT NULL DEFAULT '[]',
1059
+ content_hash TEXT NOT NULL,
1060
+ -- PA3-004 (Phase 3 Stream A): hardened-destination publish carries the
1061
+ -- publisher's review attestation so the server CHECK (hardened rows need a
1062
+ -- review_attestation) is satisfiable. hardened=0 for the Phase-2 rows.
1063
+ hardened INTEGER NOT NULL DEFAULT 0,
1064
+ review_attestation_json TEXT,
1065
+ created_at TEXT DEFAULT (datetime('now'))
1066
+ );
1067
+ CREATE TABLE IF NOT EXISTS team_revocation_outbound (
1068
+ prompt_hash TEXT PRIMARY KEY,
1069
+ created_at TEXT DEFAULT (datetime('now'))
1070
+ );
1071
+ `);
1072
+ db.exec(`
1073
+ CREATE TABLE IF NOT EXISTS rule_promotion_events_outbound (
1074
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1075
+ prompt_hash TEXT NOT NULL,
1076
+ event_type TEXT NOT NULL,
1077
+ metadata_json TEXT NOT NULL DEFAULT '{}',
1078
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1079
+ );
1080
+ CREATE INDEX IF NOT EXISTS idx_rpe_outbound_created ON rule_promotion_events_outbound(created_at);
1081
+ `);
1082
+ db.exec(`
1083
+ CREATE TABLE IF NOT EXISTS analytics_events (
1084
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1085
+ event_type TEXT NOT NULL,
1086
+ event_data TEXT NOT NULL DEFAULT '{}',
1087
+ created_at TEXT DEFAULT (datetime('now'))
1088
+ );
1089
+ CREATE INDEX IF NOT EXISTS idx_analytics_events_type ON analytics_events(event_type);
1090
+ `);
1052
1091
  db.exec(`
1053
1092
  CREATE TABLE IF NOT EXISTS knowledge_conflicts (
1054
1093
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1129,6 +1168,13 @@ function initMemorySchema(db) {
1129
1168
  `ALTER TABLE license_cache ADD COLUMN signed_payload_json TEXT NOT NULL DEFAULT ''`
1130
1169
  );
1131
1170
  }
1171
+ const outboundCols = db.prepare(`PRAGMA table_info(team_promotion_outbound)`).all();
1172
+ if (!outboundCols.some((c) => c.name === "hardened")) {
1173
+ db.exec(`ALTER TABLE team_promotion_outbound ADD COLUMN hardened INTEGER NOT NULL DEFAULT 0`);
1174
+ }
1175
+ if (!outboundCols.some((c) => c.name === "review_attestation_json")) {
1176
+ db.exec(`ALTER TABLE team_promotion_outbound ADD COLUMN review_attestation_json TEXT`);
1177
+ }
1132
1178
  db.exec(`
1133
1179
  CREATE TABLE IF NOT EXISTS failure_classes (
1134
1180
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1149,6 +1195,28 @@ function initMemorySchema(db) {
1149
1195
  CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
1150
1196
  `);
1151
1197
  }
1198
+ var FUNNEL_EVENT_OUTBOX_CAP = 2e4;
1199
+ function enqueueRulePromotionEvent(db, ev) {
1200
+ try {
1201
+ db.prepare(`
1202
+ INSERT INTO rule_promotion_events_outbound
1203
+ (prompt_hash, event_type, metadata_json, created_at)
1204
+ VALUES (?, ?, ?, ?)
1205
+ `).run(
1206
+ ev.prompt_hash,
1207
+ ev.event_type,
1208
+ JSON.stringify(ev.metadata ?? {}),
1209
+ ev.created_at
1210
+ );
1211
+ db.prepare(`
1212
+ DELETE FROM rule_promotion_events_outbound
1213
+ WHERE id NOT IN (
1214
+ SELECT id FROM rule_promotion_events_outbound ORDER BY id DESC LIMIT ?
1215
+ )
1216
+ `).run(FUNNEL_EVENT_OUTBOX_CAP);
1217
+ } catch {
1218
+ }
1219
+ }
1152
1220
  function assignImportance(type, vrResult) {
1153
1221
  switch (type) {
1154
1222
  case "decision":
@@ -1340,6 +1408,191 @@ function hashPrompt(promptText) {
1340
1408
  return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
1341
1409
  }
1342
1410
 
1411
+ // src/license.ts
1412
+ import { createHash as createHash2 } from "crypto";
1413
+
1414
+ // src/security/ed25519-envelope-verifier.ts
1415
+ import { createPublicKey, verify as cryptoVerify } from "crypto";
1416
+ var SPKI_ED25519_PREFIX = Buffer.from([
1417
+ 48,
1418
+ 42,
1419
+ 48,
1420
+ 5,
1421
+ 6,
1422
+ 3,
1423
+ 43,
1424
+ 101,
1425
+ 112,
1426
+ 3,
1427
+ 33,
1428
+ 0
1429
+ ]);
1430
+ function verifyEd25519SignedEnvelope(key, payload) {
1431
+ if (!key.knownFingerprints.has(key.fingerprintHex)) {
1432
+ return {
1433
+ kind: "error",
1434
+ reason: `Bundled ${key.keyLabel} pubkey fingerprint ${key.fingerprintHex} is not in the trusted allowlist. Possible build-time tamper.`
1435
+ };
1436
+ }
1437
+ const sig = payload._signature;
1438
+ const alg = payload._signature_alg;
1439
+ const payloadKeys = payload._signature_payload_keys;
1440
+ const sigPubkey = payload._signature_pubkey_fingerprint;
1441
+ if (typeof sig !== "string" || sig.length === 0) {
1442
+ return { kind: "missing_signature" };
1443
+ }
1444
+ if (alg !== "ed25519") {
1445
+ return { kind: "error", reason: `Unsupported signature algorithm: ${alg}` };
1446
+ }
1447
+ if (!Array.isArray(payloadKeys) || payloadKeys.length === 0) {
1448
+ return { kind: "error", reason: "Missing _signature_payload_keys" };
1449
+ }
1450
+ if (typeof sigPubkey === "string" && sigPubkey !== key.fingerprintHex) {
1451
+ return { kind: "unknown_pubkey", got: sigPubkey };
1452
+ }
1453
+ const canonicalObj = {};
1454
+ for (const k of payloadKeys) {
1455
+ if (typeof k !== "string") continue;
1456
+ canonicalObj[k] = payload[k];
1457
+ }
1458
+ const canonical = JSON.stringify(canonicalObj, [...payloadKeys].sort());
1459
+ try {
1460
+ const der = Buffer.concat([SPKI_ED25519_PREFIX, Buffer.from(key.pubkeyBytes)]);
1461
+ const pubkey = createPublicKey({ key: der, format: "der", type: "spki" });
1462
+ const ok = cryptoVerify(
1463
+ null,
1464
+ Buffer.from(canonical, "utf-8"),
1465
+ pubkey,
1466
+ Buffer.from(sig, "base64")
1467
+ );
1468
+ return ok ? { kind: "valid" } : { kind: "bad_signature" };
1469
+ } catch (err) {
1470
+ return {
1471
+ kind: "error",
1472
+ reason: `Signature verification threw: ${err instanceof Error ? err.message : String(err)}`
1473
+ };
1474
+ }
1475
+ }
1476
+
1477
+ // src/security/license-pubkey.generated.ts
1478
+ var LICENSE_PUBKEY_ED25519 = new Uint8Array([39, 136, 108, 146, 85, 233, 119, 252, 223, 226, 123, 155, 234, 168, 200, 150, 36, 249, 174, 6, 130, 146, 125, 196, 136, 224, 202, 150, 53, 228, 114, 15]);
1479
+ var LICENSE_PUBKEY_FINGERPRINT_HEX = "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475";
1480
+ var KNOWN_LICENSE_PUBKEY_FINGERPRINTS = /* @__PURE__ */ new Set([
1481
+ "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475"
1482
+ ]);
1483
+
1484
+ // src/security/license-response-verifier.ts
1485
+ function verifyLicenseResponse(payload) {
1486
+ return verifyEd25519SignedEnvelope(
1487
+ {
1488
+ pubkeyBytes: LICENSE_PUBKEY_ED25519,
1489
+ fingerprintHex: LICENSE_PUBKEY_FINGERPRINT_HEX,
1490
+ knownFingerprints: KNOWN_LICENSE_PUBKEY_FINGERPRINTS,
1491
+ keyLabel: "license"
1492
+ },
1493
+ payload
1494
+ );
1495
+ }
1496
+ function isLicenseSignatureRequired() {
1497
+ return process.env.MASSU_REQUIRE_SIGNED_LICENSE === "true";
1498
+ }
1499
+
1500
+ // src/license.ts
1501
+ var _warnedLicenseSig = false;
1502
+ function warnLicenseSigOnce(reason) {
1503
+ if (_warnedLicenseSig) return;
1504
+ _warnedLicenseSig = true;
1505
+ process.stderr.write(
1506
+ `[massu] WARNING: license-validate response is unsigned or signature invalid (${reason}). Acceptance permitted under transition mode. Operator: provision Supabase Edge Function LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64 then set MASSU_REQUIRE_SIGNED_LICENSE=true to enforce strict mode.
1507
+ `
1508
+ );
1509
+ }
1510
+ var TIER_LEVELS = {
1511
+ free: 0,
1512
+ pro: 1,
1513
+ team: 2,
1514
+ enterprise: 3
1515
+ };
1516
+ function tierLevel(tier) {
1517
+ return TIER_LEVELS[tier] ?? 0;
1518
+ }
1519
+ var PLAN_TO_TIER_MAP = {
1520
+ free: "free",
1521
+ cloud_pro: "pro",
1522
+ cloud_team: "team",
1523
+ cloud_enterprise: "enterprise"
1524
+ };
1525
+ var IN_MEMORY_CACHE_TTL_MS = 15 * 60 * 1e3;
1526
+ var GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
1527
+ function readTrustedCache(cached) {
1528
+ if (!cached.signed_payload_json) {
1529
+ if (isLicenseSignatureRequired()) return null;
1530
+ warnLicenseSigOnce("cache_unsigned_transition");
1531
+ return {
1532
+ tier: cached.tier,
1533
+ validUntil: cached.valid_until,
1534
+ features: JSON.parse(cached.features || "[]")
1535
+ };
1536
+ }
1537
+ let parsed;
1538
+ try {
1539
+ parsed = JSON.parse(cached.signed_payload_json);
1540
+ } catch {
1541
+ return null;
1542
+ }
1543
+ const result = verifyLicenseResponse(parsed);
1544
+ if (result.kind !== "valid") return null;
1545
+ const verifiedPlan = typeof parsed.plan === "string" ? parsed.plan : null;
1546
+ const verifiedTierField = typeof parsed.tier === "string" ? parsed.tier : null;
1547
+ const tier = verifiedPlan ? PLAN_TO_TIER_MAP[verifiedPlan] ?? "free" : verifiedTierField ?? "free";
1548
+ const validUntil = typeof parsed.validUntil === "string" ? parsed.validUntil : "";
1549
+ const features = Array.isArray(parsed.features) ? parsed.features : [];
1550
+ const orgId = typeof parsed.orgId === "string" && parsed.orgId.length > 0 ? parsed.orgId : void 0;
1551
+ return { tier, validUntil, features, orgId };
1552
+ }
1553
+ function getCachedTierReadOnly(memDb) {
1554
+ const config = getConfig();
1555
+ const apiKey = config.cloud?.apiKey;
1556
+ if (!apiKey) return "free";
1557
+ const ownsDb = !memDb;
1558
+ const db = memDb ?? getMemoryDb();
1559
+ try {
1560
+ const keyHash = createHash2("sha256").update(apiKey).digest("hex");
1561
+ const cached = db.prepare(
1562
+ "SELECT tier, valid_until, last_validated, features, signed_payload_json FROM license_cache WHERE api_key_hash = ?"
1563
+ ).get(keyHash);
1564
+ if (!cached) return "free";
1565
+ const trusted = readTrustedCache(cached);
1566
+ if (!trusted) return "free";
1567
+ const lastValidated = new Date(cached.last_validated);
1568
+ const sevenDaysAgo = new Date(Date.now() - GRACE_PERIOD_MS);
1569
+ if (!(lastValidated > sevenDaysAgo)) return "free";
1570
+ return trusted.tier;
1571
+ } catch {
1572
+ return "free";
1573
+ } finally {
1574
+ if (ownsDb) {
1575
+ try {
1576
+ db.close();
1577
+ } catch {
1578
+ }
1579
+ }
1580
+ }
1581
+ }
1582
+
1583
+ // src/auto-learning-entitlement.ts
1584
+ var AUTO_LEARNING_MIN_TIER = "pro";
1585
+ function entitledForAutoLearning(tier) {
1586
+ return tierLevel(tier) >= tierLevel(AUTO_LEARNING_MIN_TIER);
1587
+ }
1588
+ function autoLearningUpgradeMessage(currentTier) {
1589
+ return `Auto-learning (rule-candidate detection + /massu-rule promotion) is a Pro feature. Your tier: ${currentTier.toUpperCase()}. Upgrade at https://massu.ai/pricing`;
1590
+ }
1591
+ var TEAM_SHARED_PROMOTION_MIN_TIER = "team";
1592
+ function entitledForTeamSharedPromotion(tier) {
1593
+ return tierLevel(tier) >= tierLevel(TEAM_SHARED_PROMOTION_MIN_TIER);
1594
+ }
1595
+
1343
1596
  // src/hooks/user-prompt.ts
1344
1597
  async function main() {
1345
1598
  try {
@@ -1428,19 +1681,46 @@ async function main() {
1428
1681
  });
1429
1682
  const candidateDir = join(hookInput.cwd, ".massu", "rule-candidates");
1430
1683
  if (scoreResult.emitCandidate) {
1431
- mkdirSync2(candidateDir, { recursive: true });
1432
- const promptHash = hashPrompt(prompt);
1433
- const candidatePath = join(candidateDir, `${promptHash}.json`);
1434
- if (!existsSync3(candidatePath)) {
1435
- writeFileSync(candidatePath, JSON.stringify({
1436
- prompt,
1437
- prompt_hash: promptHash,
1438
- score: scoreResult.score,
1439
- signals: scoreResult.signals,
1440
- prior_turn_files: priorTurn.files,
1441
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1442
- session_id
1443
- }, null, 2));
1684
+ const cachedTier = getCachedTierReadOnly(db);
1685
+ if (entitledForAutoLearning(cachedTier)) {
1686
+ mkdirSync2(candidateDir, { recursive: true });
1687
+ const promptHash = hashPrompt(prompt);
1688
+ const candidatePath = join(candidateDir, `${promptHash}.json`);
1689
+ if (!existsSync3(candidatePath)) {
1690
+ writeFileSync(candidatePath, JSON.stringify({
1691
+ prompt,
1692
+ prompt_hash: promptHash,
1693
+ score: scoreResult.score,
1694
+ signals: scoreResult.signals,
1695
+ prior_turn_files: priorTurn.files,
1696
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1697
+ session_id
1698
+ }, null, 2));
1699
+ if (entitledForTeamSharedPromotion(cachedTier)) {
1700
+ enqueueRulePromotionEvent(db, {
1701
+ prompt_hash: promptHash,
1702
+ event_type: "proposed",
1703
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1704
+ metadata: {
1705
+ score: scoreResult.score,
1706
+ signal_count: scoreResult.signals.length,
1707
+ category: categorizePrompt(prompt)
1708
+ }
1709
+ });
1710
+ }
1711
+ }
1712
+ } else {
1713
+ const nudgePath = join(candidateDir, ".last-tier-nudge");
1714
+ if (!existsSync3(nudgePath)) {
1715
+ mkdirSync2(candidateDir, { recursive: true });
1716
+ process.stderr.write(
1717
+ `
1718
+ [RULE CANDIDATE] ${autoLearningUpgradeMessage(cachedTier)}
1719
+
1720
+ `
1721
+ );
1722
+ writeFileSync(nudgePath, (/* @__PURE__ */ new Date()).toISOString());
1723
+ }
1444
1724
  }
1445
1725
  }
1446
1726
  if (existsSync3(candidateDir)) {
@@ -1474,7 +1754,9 @@ async function main() {
1474
1754
  const sep = pre && !pre.endsWith("\n") ? "\n" : "";
1475
1755
  writeFileSync(logPath, pre + sep + JSON.stringify({
1476
1756
  session_id,
1477
- prompt_excerpt: prompt.slice(0, 200),
1757
+ // Security review (plan-2026-05-27): log the prompt HASH, never a
1758
+ // raw prompt excerpt — the failure log must not persist PII/secrets.
1759
+ prompt_hash: hashPrompt(prompt),
1478
1760
  error: candidateErr instanceof Error ? candidateErr.message : String(candidateErr),
1479
1761
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1480
1762
  }) + "\n", "utf-8");
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "type": "module",
5
- "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 59 workflow commands, 11 agents, 20+ patterns",
5
+ "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 56 workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "./dist/cli.js",
7
7
  "types": "./dist/adapter.d.ts",
8
8
  "exports": {
@@ -29,7 +29,7 @@
29
29
  "build:bundle-adapters": "tsx scripts/bundle-adapters.ts",
30
30
  "build:cli": "esbuild --bundle --platform=node --format=esm --outfile=dist/cli.js src/cli.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:@massu/adapter-rails --external:@massu/adapter-spring --banner:js='#!/usr/bin/env node\nimport{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
31
31
  "build:hooks": "esbuild --bundle --platform=node --format=esm --outdir=dist/hooks src/hooks/*.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:@massu/adapter-rails --external:@massu/adapter-spring --banner:js='import{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
32
- "prepublishOnly": "bash ../../scripts/prepublish-check.sh && node ../../scripts/bundle-pubkey.mjs && node ../../scripts/bundle-license-pubkey.mjs && cp ../../CHANGELOG.md ./CHANGELOG.md && npm run build",
32
+ "prepublishOnly": "bash ../../scripts/prepublish-check.sh && node ../../scripts/bundle-pubkey.mjs && node ../../scripts/bundle-license-pubkey.mjs && node ../../scripts/bundle-promotion-pubkey.mjs && cp ../../CHANGELOG.md ./CHANGELOG.md && npm run build",
33
33
  "bench:watch": "tsx test/perf/watch-benchmark.ts"
34
34
  },
35
35
  "dependencies": {
@@ -1,84 +0,0 @@
1
- # Eval Runner Protocol
2
-
3
- ## Purpose
4
-
5
- Defines how to spawn a subagent to simulate command execution and score the output against the eval checklist.
6
-
7
- ## Why Subagent-Based Eval?
8
-
9
- Claude Code cannot programmatically invoke `/massu-*` commands. The `claude` CLI accepts prompts but doesn't have a `--skill` flag for direct skill invocation. A subagent with the command text injected as instructions is the most practical simulation.
10
-
11
- ## Eval Execution Steps
12
-
13
- ### Step 1: Read inputs
14
-
15
- Read three files:
16
-
17
- 1. **Target command file** (current version): `.claude/commands/[command].md` (or `[command]/[command].md` for folder-based)
18
- 2. **Eval checklist** (immutable): `.claude/evals/[command].md`
19
- 3. **Fixture input**: `.claude/evals/fixtures/[command]/input-01.md`
20
-
21
- ### Step 2: Spawn eval subagent
22
-
23
- Use the Agent tool with `subagent_type="general-purpose"` and a prompt structured as:
24
-
25
- ```
26
- You are simulating the execution of a Claude Code command. Your job is to produce the output EXACTLY as the command specifies, including all required sections and formatting.
27
-
28
- ## Command Instructions
29
-
30
- [PASTE FULL COMMAND FILE TEXT HERE]
31
-
32
- ## Input
33
-
34
- [PASTE FIXTURE INPUT TEXT HERE]
35
-
36
- ## Your Task
37
-
38
- Execute the command against the input above. Produce the complete output as if you were running the command for real. Include every section the command specifies. Do not skip optional sections — treat everything as required for this simulation.
39
-
40
- Do NOT include any meta-commentary about the simulation. Just produce the command output.
41
- ```
42
-
43
- ### Step 3: Score the output
44
-
45
- After the subagent returns its output, score each check from the eval checklist:
46
-
47
- For each check in `.claude/evals/[command].md`:
48
-
49
- 1. Read the check's `pass_condition`
50
- 2. Evaluate the subagent output against that condition
51
- 3. Record `true` (pass) or `false` (fail)
52
-
53
- Scoring rules:
54
- - Checks are binary — no partial credit
55
- - Section presence checks: the section header must exist AND contain substantive content (not just the header)
56
- - Count checks (e.g., "at least 3 action items"): count actual items, not placeholders
57
- - Conditional checks (e.g., "if score >= 40"): evaluate the condition first, then check
58
-
59
- ### Step 4: Calculate score
60
-
61
- ```
62
- score = (passing_checks / total_checks) * 100
63
- ```
64
-
65
- ### Step 5: Return results
66
-
67
- Return to the main loop:
68
-
69
- 1. `score`: percentage (e.g., 75.0)
70
- 2. `checks`: object mapping check_id to boolean (e.g., `{"has_executive_summary": true, "has_score_table": false, ...}`)
71
- 3. `failing_checks`: list of check_ids that failed
72
- 4. `status`: "success" or "crash" (if subagent errored or output was unparseable)
73
-
74
- ## Output Containment
75
-
76
- The full subagent output is NOT retained in the main agent context. After scoring:
77
-
78
- - Discard the full output text
79
- - Keep only: score, per-check results, failing check names
80
- - This prevents context window exhaustion during long runs (Karpathy insight #4)
81
-
82
- ## Crash Handling
83
-
84
- If the subagent errors, times out, or produces output that cannot be scored, return `status: "crash"` to the main loop. See `safety-rails.md` for crash retry protocol.
@@ -1,125 +0,0 @@
1
- # Safety Rails
2
-
3
- ## Protected Line Patterns
4
-
5
- Lines containing ANY of these patterns are READ-ONLY. The agent may add content adjacent to them but MUST NOT modify or delete these lines:
6
-
7
- - `CR-` (Canonical Rule references)
8
- - `VR-` (Verification Requirement references)
9
- - `NON-NEGOTIABLE`
10
- - `NEVER` — protected when: (a) at start of line, (b) after bullet/dash (`- NEVER`, `* NEVER`), or (c) in ALL-CAPS imperative context (`NEVER use`, `NEVER edit`, `NEVER import`). NOT protected in lowercase prose (`should never`, `would never`, `can never`).
11
- - `MANDATORY`
12
- - `FORBIDDEN`
13
- - `MUST NOT`
14
-
15
- These patterns encode incident-learned safety rules. Modifying them to pass an eval check would optimize the metric while degrading real safety — a Goodhart's Law violation.
16
-
17
- ### Verification
18
-
19
- Before applying any edit, grep the target file for protected patterns in the edit region. If the edit would modify a protected line, reject the edit and try a different approach.
20
-
21
- ---
22
-
23
- ## Backup Protocol
24
-
25
- Before the first iteration of any autoresearch run:
26
-
27
- 1. Create the backups directory if needed: `.claude/metrics/backups/`
28
- 2. Copy the target file to: `.claude/metrics/backups/[command]-autoresearch-[YYYY-MM-DD-HHmmss].md.bak`
29
- 3. Verify the backup exists and is non-empty
30
-
31
- The backup is the ultimate rollback if the entire autoresearch branch needs to be abandoned.
32
-
33
- ---
34
-
35
- ## Git Branch Protocol
36
-
37
- All autoresearch commits go to a dedicated branch:
38
-
39
- 1. **Resolve target path**: Determine the actual target file path — `.claude/commands/[command].md` for flat commands, `.claude/commands/[command]/[command].md` for folder-based commands. Store as `TARGET_PATH` and use for ALL subsequent git add/checkout operations.
40
- 2. **Create branch**: `git checkout -b autoresearch/[command]-[YYYY-MM-DD]`. If branch already exists (same command, same day), append a numeric suffix: `autoresearch/[command]-[YYYY-MM-DD]-2`, `-3`, etc.
41
- 3. **Commit on KEEP**: `git add [TARGET_PATH] && git commit -m "autoresearch([command]): [1-line summary]"`
42
- 4. **Revert on DISCARD**: `git checkout -- [TARGET_PATH]`
43
- 5. **After run**: User reviews the branch and decides to merge, cherry-pick, or discard
44
-
45
- The branch naming convention allows multiple autoresearch runs on different commands without conflict.
46
-
47
- ---
48
-
49
- ## Cost Tracking
50
-
51
- Track estimated token usage per iteration:
52
-
53
- - **Eval subagent prompt**: ~target file tokens + eval file tokens + fixture tokens
54
- - **Eval subagent response**: ~2x fixture tokens (simulated output)
55
- - **Main agent overhead**: ~500 tokens per iteration (scoring, logging, state)
56
-
57
- Rough estimate: each iteration costs ~3x the target file size in tokens.
58
-
59
- **Cost cap**: If cumulative estimated token usage exceeds 500K tokens, **stop the loop** and produce the final report. This is a hard backstop regardless of `--no-limit` mode — it is NOT a pause, it is a termination.
60
-
61
- ---
62
-
63
- ## Tristate Crash Handling
64
-
65
- When an eval produces a CRASH result:
66
-
67
- 1. **Log the crash**: Record in `autoresearch-runs.jsonl` with `"action":"crashed"`
68
- 2. **Do NOT increment** the consecutive reverts counter
69
- 3. **Retry once**: On the retry, use a SIMPLIFIED edit (smaller change, fewer lines modified)
70
- 4. **If retry also crashes**: Log as DISCARD, increment consecutive reverts counter
71
- 5. **Continue the loop** — a single crash does not warrant stopping
72
-
73
- Crash accumulation: if `cumulative_crashed >= 5` in a single run, warn that there may be a systemic issue with the eval or fixture.
74
-
75
- ---
76
-
77
- ## "Think Harder" Graduated Escalation
78
-
79
- When consecutive reverts trigger escalation, the edit strategy changes:
80
-
81
- ### Level 0 (default)
82
- Normal operation. Target the weakest failing check. Try standard edit types.
83
-
84
- ### Level 1 (3 consecutive reverts)
85
- **Switch edit type.** If previous attempts used "add rule", try in this order:
86
- 1. Add a worked example (before/after demonstration)
87
- 2. Restructure the section (reorder for emphasis)
88
- 3. Add a banned anti-pattern
89
- 4. Promote existing requirement higher in the file
90
-
91
- ### Level 2 (5 consecutive reverts)
92
- **Full context reload.** Before the next edit:
93
- 1. Re-read the eval checklist from disk (not from memory)
94
- 2. Re-read the target file from disk
95
- 3. Review the changelog of ALL kept changes so far (from `autoresearch-runs.jsonl`)
96
- 4. Identify which failing check has been attempted most without success
97
- 5. Look for TWO previous near-miss approaches and try COMBINING them
98
-
99
- ### Level 3 (8 consecutive reverts)
100
- **Radical restructuring.** Try structural changes, not content changes:
101
- 1. Reorder major sections of the target file
102
- 2. Merge two related sections into one focused section
103
- 3. Split an overloaded section into two specific sections
104
- 4. Convert abstract rules into concrete worked examples
105
- 5. Add a decision tree or flowchart for complex logic
106
-
107
- ### BAIL (10 consecutive reverts)
108
- **Stagnation exit.** Log: "STAGNATION: 10 consecutive reverts after Level 3 escalation."
109
- - Revert any pending changes
110
- - Produce the final report
111
- - Exit the loop
112
- - This is the only way CR-37 triggers — after Level 3 has been attempted
113
-
114
- ---
115
-
116
- ## Output Containment
117
-
118
- Eval subagent output is captured and scored, NOT inlined into the main agent context.
119
-
120
- The main agent context receives ONLY:
121
- - Score percentage (e.g., 75.0%)
122
- - Per-check pass/fail results (e.g., `has_executive_summary: true, has_score_table: false`)
123
- - Names of failing checks
124
-
125
- This is the Karpathy `> run.log 2>&1` pattern adapted for LLM context management.