#!/usr/bin/env bun /** * gstack-decision-search — read active decisions (the curated "what did we decide" view). * * Usage: * gstack-decision-search [--query KW] [--scope repo|branch|issue] * [--branch B] [--issue I] [--recent N] [--all] [--json] * [--semantic] * * Reads the BOUNDED active snapshot (decisions.active.json) — O(active), not a full * history scan — and rebuilds it from the event log if missing. Scope-filtered to the * current branch/issue context (recency != relevance). NON-INTERACTIVE. `--all` shows * superseded decisions too (from the full log). Exit 0 silently when there are none. * * `--semantic` (with `--query`) appends an OPTIONAL "related from memory" block from * gbrain semantic recall. It is a pure enhancement: when gbrain is off/unconfigured/ * empty it degrades silently to the reliable file results above. The reliable path * never loads gbrain code (the semantic module is imported lazily only here). */ import { existsSync } from "fs"; import { decisionPaths, readSnapshot, rebuildSnapshot, readEvents, filterByScope, datamark, type ActiveDecision, } from "../lib/gstack-decision"; import { resolveSlug, gitBranch, flagValue } from "../lib/bin-context"; const HERE = import.meta.dir; const args = process.argv.slice(2); const slug = resolveSlug(`${HERE}/gstack-slug`); const paths = decisionPaths(slug); const queryRaw = flagValue(args, "--query"); const query = queryRaw?.toLowerCase(); const scope = flagValue(args, "--scope"); const branch = flagValue(args, "--branch") ?? gitBranch(); const issue = flagValue(args, "--issue"); const recentRaw = flagValue(args, "--recent"); const recent = recentRaw ? parseInt(recentRaw, 10) : undefined; const showAll = args.includes("--all"); const asJson = args.includes("--json"); const semantic = args.includes("--semantic"); let rows: ActiveDecision[]; if (showAll) { // --all includes SUPERSEDED decisions (history), but NEVER redacted ones — a redact // is an expunge, so it must remove the text from every read path, not just active. const events = readEvents(paths); const redacted = new Set( events.filter((e) => e.kind === "redact" && e.supersedes).map((e) => e.supersedes as string), ); rows = events.filter((e): e is ActiveDecision => e.kind === "decide" && !redacted.has(e.id)); } else { rows = readSnapshot(paths); // Rebuild only when a snapshot is absent but a log exists (don't write a snapshot // into a nonexistent store on an empty read — just return nothing). if (!rows.length && existsSync(paths.log)) rows = rebuildSnapshot(paths); } rows = filterByScope(rows, { branch, issue }); if (scope) rows = rows.filter((d) => d.scope === scope); if (query) { rows = rows.filter((d) => [d.decision, d.rationale, d.alternatives_considered] .filter((s): s is string => typeof s === "string") .some((s) => s.toLowerCase().includes(query)), ); } rows.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)); // newest first if (recent && recent > 0) rows = rows.slice(0, recent); if (asJson) { // --json stays reliable-only (semantic recall is a human-facing supplement). console.log(JSON.stringify(rows)); process.exit(0); } for (const d of rows) { // Datamark all stored free-text (decision, rationale, branch/issue) — it lands in // agent context via Context Recovery, so treat it as DATA, not instructions. const branchTag = d.branch ? `:${datamark(d.branch)}` : ""; const issueTag = d.issue ? `:${datamark(d.issue)}` : ""; const scopeTag = d.scope === "repo" ? "" : ` [${d.scope}${branchTag}${issueTag}]`; console.log(`- ${datamark(d.decision ?? "")}${scopeTag} (${d.source}, ${d.date.slice(0, 10)})`); if (d.rationale) console.log(` why: ${datamark(d.rationale)}`); } // OPTIONAL gbrain enhancement. Lazy import so the reliable path above never loads // gbrain code. Degrades silently: null (gbrain off) or [] (nothing found) leaves the // reliable results above as the answer. if (semantic && queryRaw) { const { semanticRecall } = await import("../lib/gstack-decision-semantic"); const hits = semanticRecall(queryRaw); if (hits && hits.length) { console.log("\nRelated from memory (gbrain semantic recall):"); for (const h of hits) { // gbrain hits are EXTERNAL corpus content — datamark slug + snippet too so they // can't spoof role markers / fences when printed into agent context. const snip = datamark(h.snippet.length > 100 ? `${h.snippet.slice(0, 100)}…` : h.snippet); console.log(` [${h.score.toFixed(2)}] ${datamark(h.slug)}: ${snip}`); } } }