Files
gstack/bin/gstack-decision-search
T
Garry Tan fa250db27b feat(memory): optional gbrain --semantic recall for decision search
Adds gstack-decision-search --semantic (with --query): appends a 'Related from
memory' block from gbrain semantic search, scoped to the curated-memory source.
Pure enhancement, reliability-first: a new lib/gstack-decision-semantic.ts is the
ONLY decision module that touches gbrain and is imported lazily only on --semantic,
so the reliable file path never loads gbrain code. Every path degrades to the
reliable file results when gbrain is off, unconfigured, empty, or errors (never
throws, 10s timeout).

Built against the verified gbrain 0.42.x surface (text output [score] slug --
snippet, NOT JSON; curated-memory source resolved by worktree path, not a
gstack-brain-<user> id). Deterministic-contract tests only: parser units,
degrade-to-null when gbrain absent, and a fake-gbrain shim proving scope+search
end-to-end. find-contradictions deferred (no verifiable CLI surface yet + curated
memory not indexed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:59:34 -07:00

111 lines
4.3 KiB
TypeScript
Executable File

#!/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 { spawnSync } from "child_process";
import {
decisionPaths,
readSnapshot,
rebuildSnapshot,
readEvents,
filterByScope,
type ActiveDecision,
} from "../lib/gstack-decision";
const HERE = import.meta.dir;
const args = process.argv.slice(2);
function resolveSlug(): string {
const r = spawnSync(`${HERE}/gstack-slug`, { encoding: "utf-8" });
const m = (r.stdout || "").match(/^SLUG=(.+)$/m);
return m ? m[1].trim() : "unknown";
}
function gitBranch(): string | undefined {
const r = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf-8" });
const b = (r.stdout || "").trim();
return b && b !== "HEAD" ? b : undefined;
}
function flagValue(name: string): string | undefined {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
}
const slug = resolveSlug();
const paths = decisionPaths(slug);
const queryRaw = flagValue("--query");
const query = queryRaw?.toLowerCase();
const scope = flagValue("--scope");
const branch = flagValue("--branch") ?? gitBranch();
const issue = flagValue("--issue");
const recent = flagValue("--recent") ? parseInt(flagValue("--recent") as string, 10) : undefined;
const showAll = args.includes("--all");
const asJson = args.includes("--json");
const semantic = args.includes("--semantic");
let rows: ActiveDecision[];
if (showAll) {
// include superseded: every decide event from the full log (no active filter)
rows = readEvents(paths).filter((e): e is ActiveDecision => e.kind === "decide");
} 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) {
const scopeTag = d.scope === "repo" ? "" : ` [${d.scope}${d.branch ? `:${d.branch}` : ""}${d.issue ? `:${d.issue}` : ""}]`;
console.log(`- ${d.decision}${scopeTag} (${d.source}, ${d.date.slice(0, 10)})`);
if (d.rationale) console.log(` why: ${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) {
const snip = h.snippet.length > 100 ? `${h.snippet.slice(0, 100)}` : h.snippet;
console.log(` [${h.score.toFixed(2)}] ${h.slug}: ${snip}`);
}
}
}