@jhizzard/termdeck-stack 1.8.4 → 1.9.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.
@@ -0,0 +1,14 @@
1
+ {"id":"publish-before-push","title":"Publish to npm before git push","severity":"high","scope":"universal","audience":"all","trigger":"pre-release","check":{"type":"manual"},"enforcement":{"surface":"inject-advisory","max_severity":"warn","ref":"docs/RELEASE.md"},"source":{"incident":"Sprint 35 close-out reversed publish/push order","memory_recall_query":"RELEASE.md publish order npm before push Passkey not OTP"},"advisory":{"one_line":"Release order is npm publish (Passkey, never --otp) BEFORE git push; read docs/RELEASE.md first.","procedure_path":"docs/RELEASE.md","cooldown_hours":24},"status":"active","version":1}
2
+ {"id":"rls-five-gates","title":"Supabase RLS five hygiene gates on every new DB object","severity":"critical","scope":"universal","audience":"all","trigger":"pre-release","check":{"type":"manual"},"enforcement":{"surface":"inject-advisory","max_severity":"warn","ref":"CLAUDE.md#supabase-rls"},"source":{"incident":"2026-05-06 RLS sweep found PUBLIC INSERT + PUBLIC EXECUTE + mutable search_path","memory_recall_query":"Supabase RLS holes case study 2026-05-06 five gates"},"advisory":{"one_line":"New DB object? Verify 5 RLS gates: RLS enabled, no WITH CHECK (true)/PUBLIC, REVOKE EXECUTE FROM PUBLIC, search_path pinned, no anon write path.","procedure_path":"CLAUDE.md","cooldown_hours":24},"status":"active","version":1}
3
+ {"id":"two-stage-inject","title":"Two-stage submit for TermDeck panel injects","severity":"high","scope":"universal","audience":"all","trigger":"sprint-inject","check":{"type":"manual"},"enforcement":{"surface":"inject-advisory","max_severity":"warn","ref":"CLAUDE.md#two-stage-submit"},"source":{"incident":"Single-stage paste+CR races; 4th panel left awaiting human Enter (broken sleep 2026-04-26/27)","memory_recall_query":"overnight broken sleep inject submit single stage two-stage"},"advisory":{"one_line":"Inject in two stages: POST the bracketed-paste body, settle ~400ms, then POST the submit (CR) alone — never ride the CR on the paste write. Prefer {submit:true}.","procedure_path":"CLAUDE.md","cooldown_hours":24},"status":"active","version":1}
4
+ {"id":"status-post-grammar","title":"STATUS.md post-shape grammar","severity":"medium","scope":"universal","audience":"all","trigger":"status-append","check":{"type":"regex","pattern":"^(### )?\\[T\\d+(?:-[A-Z]+)?\\] (FINDING|FIX-PROPOSED|FIX-LANDED|AUDIT-PASS|AUDIT-FAIL|CHECKPOINT|HANDOFF-REQUEST|HANDOFF-ACK|DONE)\\b"},"enforcement":{"surface":"status-append","max_severity":"warn","ref":"PLANNING.md#5-lane-discipline"},"source":{"incident":"Sprint 51.7 lane-shape mismatch broke cross-lane grep","memory_recall_query":"lane shape mismatch case Sprint 51.7 uniform post header"},"advisory":{"one_line":"STATUS posts must match: ### [T<n>] VERB YYYY-MM-DD HH:MM ET — gist, VERB in the allowed set. Uniform shape keeps cross-lane grep working.","procedure_path":"PLANNING.md","cooldown_hours":12},"status":"active","version":1}
5
+ {"id":"checkpoint-cadence","title":"Auditor CHECKPOINT cadence","severity":"medium","scope":"universal","audience":"all","trigger":"sprint-audit","check":{"type":"manual"},"enforcement":{"surface":"status-append","max_severity":"advise","ref":"CLAUDE.md#three-hardening-rules"},"source":{"incident":"Auditor panel compacts mid-sprint and loses in-context audit state","memory_recall_query":"auditor panel compacts mid-sprint CHECKPOINT discipline"},"advisory":{"one_line":"Auditor: post a CHECKPOINT at every phase boundary and at least every 15 min — phase, verified (file:line), pending, latest FIX-LANDED ref.","procedure_path":"CLAUDE.md","cooldown_hours":1},"status":"active","version":1}
6
+ {"id":"tolerant-idle-poll","title":"Tolerant idle-poll regex for cross-lane waits","severity":"low","scope":"universal","audience":"all","trigger":"sprint-coordination","check":{"type":"manual"},"enforcement":{"surface":"inject-advisory","max_severity":"advise","ref":"CLAUDE.md#three-hardening-rules"},"source":{"incident":"Brittle prefix-required DONE poll missed prefixless posts","memory_recall_query":"tolerant idle-poll regex hardening cross-lane monitoring"},"advisory":{"one_line":"Cross-lane DONE polls must use a tolerant regex: ^(### )?\\[T<n>\\] DONE\\b — never the brittle prefix-required form.","procedure_path":"CLAUDE.md","cooldown_hours":12},"status":"active","version":1}
7
+ {"id":"done-with-open-yellow","title":"No DONE while a YELLOW is open","severity":"medium","scope":"universal","audience":"all","trigger":"status-append","check":{"type":"manual"},"enforcement":{"surface":"status-append","max_severity":"warn","ref":"PLANNING.md#5-lane-discipline"},"source":{"incident":"Lane posted DONE with an unresolved YELLOW; close-out missed it","memory_recall_query":"DONE with open YELLOW lane close-out seam"},"advisory":{"one_line":"Don't post DONE while your lane has an open YELLOW or unresolved FINDING — resolve or hand off first; ORCH close-out trusts DONE.","procedure_path":"PLANNING.md","cooldown_hours":6},"status":"active","version":1}
8
+ {"id":"secrets-in-commits","title":"No secrets in commits (gitleaks pre-commit + pre-push)","severity":"critical","scope":"universal","audience":"all","trigger":"commit","check":{"type":"script","script":"gitleaks protect --staged"},"enforcement":{"surface":"git-hook","max_severity":"block","ref":"CLAUDE.md#secret-leak-prevention"},"source":{"incident":"gitleaks hooks installed 2026-05-06 to block secret + forbidden-string commits","memory_recall_query":"gitleaks mirror backup install verification 2026-05-06"},"advisory":{"one_line":"Secrets and forbidden strings are blocked at commit/push by gitleaks hooks. Don't --no-verify; fix the source or allowlist a true false-positive.","procedure_path":"CLAUDE.md","cooldown_hours":24},"status":"active","version":1}
9
+ {"id":"compaction-near-capture","title":"Auto-capture on compaction-near (hook + periodic timer)","severity":"high","scope":"universal","audience":"all","trigger":"compaction-near","check":{"type":"manual"},"enforcement":{"surface":"server-monitor","max_severity":"warn","ref":"CLAUDE.md#auto-commit-compaction-near"},"source":{"incident":"Sprint 64: PreCompact hook (Claude) + server periodic-capture timer (Codex/Gemini/Grok)","memory_recall_query":"orthogonal harness-hook server-side substrate compaction-near capture"},"advisory":{"one_line":"Compaction-near capture is mechanized: PreCompact hook for Claude panels, server periodic-capture timer for non-Claude panels. Manual remember is fallback.","procedure_path":"CLAUDE.md","cooldown_hours":24},"status":"active","version":1}
10
+ {"id":"err-git-push-rejected","title":"Git push rejected (non-fast-forward)","severity":"medium","scope":"universal","audience":"all","trigger":["T-ERR"],"check":{"type":"regex","pattern":"non-fast-forward|Updates were rejected|failed to push some refs","flags":"i"},"enforcement":{"surface":"inject-advisory","max_severity":"warn","ref":"CLAUDE.md#secret-leak-prevention"},"source":{"incident":"Push rejected; before force-push the repo mirror must be current","memory_recall_query":"gitleaks mirror backup before destructive history rewrite force push"},"advisory":{"one_line":"Push rejected (non-fast-forward). Pull/rebase or fetch first. Before ANY force-push, confirm the ~/git-backups mirror is current (git-mirror-active-repos.sh).","procedure_path":"CLAUDE.md","cooldown_hours":6},"status":"active","version":1}
11
+ {"id":"err-pg-permission-denied","title":"Postgres permission denied (RLS/EXECUTE)","severity":"high","scope":"universal","audience":"all","trigger":["T-ERR"],"check":{"type":"regex","pattern":"permission denied for (relation|table|function|schema|sequence)|code.?:.?.?42501|error: *42501","flags":"i"},"enforcement":{"surface":"inject-advisory","max_severity":"warn","ref":"CLAUDE.md#supabase-rls"},"source":{"incident":"PostgREST/psql 42501 traces to a missing RLS policy or EXECUTE grant","memory_recall_query":"Supabase RLS holes five gates REVOKE EXECUTE FROM PUBLIC search_path"},"advisory":{"one_line":"Postgres permission denied (42501). Check the table's RLS policy + function EXECUTE grants; service_role bypasses RLS, anon/authenticated need an explicit policy.","procedure_path":"CLAUDE.md","cooldown_hours":6},"status":"active","version":1}
12
+ {"id":"err-npm-publish-auth","title":"npm publish auth failure (use Passkey, not --otp)","severity":"high","scope":"universal","audience":"all","trigger":["T-ERR"],"check":{"type":"regex","pattern":"npm (error|ERR!).*(E403|ENEEDAUTH|EOTP|403 Forbidden|one-time)|this operation requires a one-time password","flags":"i"},"enforcement":{"surface":"inject-advisory","max_severity":"warn","ref":"docs/RELEASE.md"},"source":{"incident":"@jhizzard/* publishes via web Passkey; --otp is the wrong path","memory_recall_query":"RELEASE.md publish order npm before push Passkey not OTP"},"advisory":{"one_line":"npm publish auth failure: @jhizzard/* authenticates via web Passkey — never pass --otp. Read docs/RELEASE.md (publish BEFORE push).","procedure_path":"docs/RELEASE.md","cooldown_hours":6},"status":"active","version":1}
13
+ {"id":"err-port-in-use","title":"Port already in use (EADDRINUSE)","severity":"low","scope":"universal","audience":"all","trigger":["T-ERR"],"check":{"type":"regex","pattern":"EADDRINUSE|address already in use|listen EADDRINUSE","flags":"i"},"enforcement":{"surface":"inject-advisory","max_severity":"advise","ref":"docs/ARCHITECTURE.md"},"source":{"incident":"Sprint substrate uses :3001 when :3000 is the daily-driver instance","memory_recall_query":"TermDeck server port 3000 3001 substrate daily-driver instance"},"advisory":{"one_line":"Port already in use (EADDRINUSE). The TermDeck sprint substrate runs on :3001 when :3000 is the daily-driver — target :3001 or free the port first.","procedure_path":"docs/ARCHITECTURE.md","cooldown_hours":6},"status":"active","version":1}
14
+ {"id":"err-gitleaks-blocked","title":"Commit/push blocked by gitleaks","severity":"high","scope":"universal","audience":"all","trigger":["T-ERR"],"check":{"type":"regex","pattern":"gitleaks.*(leaks? found|finding)|secret detected|commit (blocked|rejected) by gitleaks|leaks found:","flags":"i"},"enforcement":{"surface":"inject-advisory","max_severity":"warn","ref":"CLAUDE.md#secret-leak-prevention"},"source":{"incident":"gitleaks pre-commit/pre-push hooks block secrets + forbidden strings","memory_recall_query":"gitleaks mirror backup install verification forbidden strings"},"advisory":{"one_line":"Commit/push blocked by gitleaks — a real secret or a forbidden string. Don't --no-verify; fix the source, or allowlist a genuine false-positive in ~/.gitleaks.toml.","procedure_path":"CLAUDE.md","cooldown_hours":6},"status":"active","version":1}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck-stack",
3
- "version": "1.8.4",
3
+ "version": "1.9.0",
4
4
  "description": "One-command installer for the TermDeck developer memory stack: TermDeck + Mnestra + Rumen + Supabase MCP",
5
5
  "bin": {
6
6
  "termdeck-stack": "./src/index.js"
package/src/index.js CHANGED
@@ -26,6 +26,7 @@
26
26
  const fs = require('node:fs');
27
27
  const os = require('node:os');
28
28
  const path = require('node:path');
29
+ const crypto = require('node:crypto');
29
30
  const readline = require('node:readline/promises');
30
31
  const { spawn, spawnSync } = require('node:child_process');
31
32
 
@@ -87,6 +88,18 @@ const PRECOMPACT_HOOK_COMMAND = _hookCommandFor('memory-pre-compact.js');
87
88
  const PRECOMPACT_HOOK_TIMEOUT_SECONDS = 30;
88
89
  const SECRETS_PATH = path.join(HOME, '.termdeck', 'secrets.env');
89
90
 
91
+ // Sprint 78 T1 — doctrine registry vendoring. A READ-ONLY copy of the doctrine
92
+ // registry (audience:'all' + active entries only, baked at publish) lands at
93
+ // ~/.claude/doctrine/registry.shipped.jsonl so Brad has an inspectable
94
+ // artifact. This is NOT a loader read-path: the loader (doctrine/index.js)
95
+ // reads the registry package-relative, and doctrine/ ships in @jhizzard/termdeck
96
+ // via the files whitelist, so the loader is always co-located with its own
97
+ // registry. The shipped copy is purely inspectable + refresh-gated on a
98
+ // FULL-FILE content hash.
99
+ const DOCTRINE_DEST_DIR = path.join(HOME, '.claude', 'doctrine');
100
+ const DOCTRINE_SHIPPED_DEST = path.join(DOCTRINE_DEST_DIR, 'registry.shipped.jsonl');
101
+ const DOCTRINE_SHIPPED_SOURCE = path.join(__dirname, '..', 'assets', 'doctrine', 'registry.shipped.jsonl');
102
+
90
103
  // Read ~/.termdeck/secrets.env into a plain object. Returns {} if the file
91
104
  // is absent or unreadable. Used to populate the mnestra MCP env block with
92
105
  // concrete values — Claude Code does NOT shell-expand `${VAR}` references
@@ -941,6 +954,63 @@ async function installPreCompactHook(opts = {}) {
941
954
  return { fileStatus, settingsStatus };
942
955
  }
943
956
 
957
+ // ── Doctrine registry (Sprint 78 T1) ──────────────────────────────────
958
+ //
959
+ // CRITICAL — FULL-FILE stamp, NOT the 4KB-head stamp. `_readHookSignatureVersion`
960
+ // above reads `slice(0, 4096)` + `HOOK_SIGNATURE_REGEX` — that is the exact stamp
961
+ // that failed in Sprint 51.6 (a file whose marker/content sits past the first
962
+ // 4KB is mis-graded, so bundled fixes never land). The doctrine copy on Brad's
963
+ // machine is READ-ONLY (he never hand-edits it), so the refresh gate is a plain
964
+ // full-file sha256 compare: any drift ⇒ refresh from the bundled copy. No
965
+ // version-number bookkeeping to forget; the content hash IS the stamp. This
966
+ // avoids the INSTALLER-PITFALLS 4KB-head failure class by construction.
967
+
968
+ function _fileSha256(filepath) {
969
+ // FULL file read — never a 4KB-head slice.
970
+ try { return crypto.createHash('sha256').update(fs.readFileSync(filepath)).digest('hex'); }
971
+ catch (_) { return null; }
972
+ }
973
+
974
+ // Read-only refresh model: refresh when dest is missing OR its full-file hash
975
+ // differs from the bundled copy. Returns true ⇒ refresh needed.
976
+ function _doctrineRefreshNeeded(sourcePath, destPath) {
977
+ const srcHash = _fileSha256(sourcePath);
978
+ if (srcHash == null) return false; // bundled asset missing — nothing to vendor
979
+ return _fileSha256(destPath) !== srcHash;
980
+ }
981
+
982
+ // Install / refresh the read-only doctrine registry copy. Promptless (the file
983
+ // is TermDeck-managed read-only; Brad never edits it, so there is no hand-edit
984
+ // to preserve). Fail-soft: any error logs a status line + returns, never throws
985
+ // into the installer flow.
986
+ function installDoctrineRegistry(opts = {}) {
987
+ const dryRun = !!opts.dryRun;
988
+ const sourcePath = opts.sourcePath || DOCTRINE_SHIPPED_SOURCE;
989
+ const destPath = opts.destPath || DOCTRINE_SHIPPED_DEST;
990
+ try {
991
+ if (!fs.existsSync(sourcePath)) return { status: 'no-bundled-asset' };
992
+ if (!_doctrineRefreshNeeded(sourcePath, destPath)) {
993
+ statusLine(`${ANSI.dim}=${ANSI.reset}`, 'doctrine registry', 'already current (read-only)');
994
+ return { status: 'already-current' };
995
+ }
996
+ if (dryRun) {
997
+ statusLine(`${ANSI.yellow}↩${ANSI.reset}`, '(dry-run)', `would refresh read-only doctrine registry at ${destPath}`);
998
+ return { status: 'would-refresh' };
999
+ }
1000
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
1001
+ // The dest is 0o444 after a prior install — make it writable for the
1002
+ // overwrite, then re-lock read-only.
1003
+ try { if (fs.existsSync(destPath)) fs.chmodSync(destPath, 0o644); } catch (_) { /* best-effort */ }
1004
+ fs.copyFileSync(sourcePath, destPath);
1005
+ try { fs.chmodSync(destPath, 0o444); } catch (_) { /* best-effort */ }
1006
+ statusLine(`${ANSI.green}↻${ANSI.reset}`, 'doctrine registry', `refreshed read-only copy at ${destPath}`);
1007
+ return { status: 'refreshed' };
1008
+ } catch (err) {
1009
+ statusLine(`${ANSI.yellow}!${ANSI.reset}`, 'doctrine registry', `skipped (fail-soft: ${err && err.message})`);
1010
+ return { status: 'error', error: err && err.message };
1011
+ }
1012
+ }
1013
+
944
1014
  // ── Next steps ──────────────────────────────────────────────────────
945
1015
 
946
1016
  function printNextSteps(plan, opts) {
@@ -1081,6 +1151,11 @@ async function main(argv) {
1081
1151
  assumeYes: args.yes,
1082
1152
  });
1083
1153
 
1154
+ // Sprint 78 T1 — vendor the read-only doctrine registry copy (audience:'all'
1155
+ // + active entries, baked at publish) so Brad has an inspectable artifact.
1156
+ // Promptless; full-file-hash refresh gate; fail-soft (never aborts install).
1157
+ installDoctrineRegistry({ dryRun: args.dryRun });
1158
+
1084
1159
  printNextSteps(wantedLayers, { dryRun: args.dryRun });
1085
1160
 
1086
1161
  if (failures > 0) {
@@ -1121,6 +1196,11 @@ module.exports.HOOK_TIMEOUT_SECONDS = HOOK_TIMEOUT_SECONDS;
1121
1196
  module.exports.HOOK_SOURCE = HOOK_SOURCE;
1122
1197
  // Sprint 64 T3 — PreCompact hook (Investigation 2 closure) exports.
1123
1198
  module.exports.installPreCompactHook = installPreCompactHook;
1199
+ module.exports.installDoctrineRegistry = installDoctrineRegistry;
1200
+ module.exports._fileSha256 = _fileSha256;
1201
+ module.exports._doctrineRefreshNeeded = _doctrineRefreshNeeded;
1202
+ module.exports.DOCTRINE_SHIPPED_SOURCE = DOCTRINE_SHIPPED_SOURCE;
1203
+ module.exports.DOCTRINE_SHIPPED_DEST = DOCTRINE_SHIPPED_DEST;
1124
1204
  module.exports._isPreCompactHookEntry = _isPreCompactHookEntry;
1125
1205
  module.exports._mergePreCompactHookEntry = _mergePreCompactHookEntry;
1126
1206
  module.exports.PRECOMPACT_HOOK_COMMAND = PRECOMPACT_HOOK_COMMAND;