Skip to content

Commit 45cc95d

Browse files
garrytanclaude
andauthored
v1.57.5.0 feat: cross-session decision memory + gbrain dream-stage call graph (garrytan#1910)
* feat(gbrain-sync): add cycleCompleted() cycle-state probe Reads `gbrain doctor` cycle_freshness to classify whether a source has completed a full cycle (completed/never/unknown). A fail naming this source -> never; a fail naming only other sources -> completed; an absent or unparseable check -> unknown, so an unrelated doctor failure never masks a real state. Gates the automatic call-graph build on --full. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(gbrain-sync): --dream call-graph stage with lock-free gate + honest outcome guard Adds a source-scoped `gbrain dream --source <id>` stage that builds this worktree's call graph (code-callers/code-callees). Runs lock-free after the sync lock releases so it never blocks sibling worktrees; a .dream-in-progress marker dedupes concurrent dreams. --full auto-runs it only when the cycle was never built; explicit --dream always forces; --no-dream opts out. The stage parses the cycle's own output and reports the truth, not a flat "built": a WARN when the schema pack can't extract code symbols, when the embed phase failed for a missing key, or when 0 edges resolved; OK with the resolved-edge count otherwise. gbrain exits 0 even when it skips on a held cycle lock (e.g. autopilot), so that case reports SKIP, not success. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: ignore gbrain .sources/ local staging dir gbrain writes per-source staging and capability-check artifacts under .sources/ in the repo root. It's machine-local runtime state, not source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(gbrain): honest call-graph guidance in /sync-gbrain + pin works on gbrain>=0.41.38 sync-gbrain frames the --dream offer honestly: building a call graph requires a code-aware schema pack, and the dream stage reports a WARN when it can't. The verdict's Call graph row mirrors the dream stage's real outcome instead of assuming a completed cycle means edges exist. The ## GBrain Search Guidance block written into CLAUDE.md drops the old code-callers --source caveat: gbrain >=0.41.38.0 honors the .gbrain-source pin for code-callers/code-callees. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(jsonl-store): shared audited JSONL plumbing (injection-reject + atomic append + tolerant read) Single source of truth extracted for D2A: gstack-learnings-* and the upcoming gstack-decision-* bins share one injection-pattern list, one atomic single-line appender, and one tolerant reader. No more drift between stores. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(learnings-log): use shared hasInjection from lib/jsonl-store (D2A) Replace the inline injection-pattern copy with the shared list. One audited write-path rejection across learnings + the upcoming decision store. Behavior unchanged (35/35 learnings tests green); learnings-search keeps its inline copy because a structural test pins its bash/bun shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(decision): event-sourced decision-memory model (lib/gstack-decision) decide/supersede/redact events on lib/jsonl-store; active set is computed (no mutable status), dangling refs tolerated. Free-text is injection-checked and redact-scanned on write (HIGH secret -> reject). Scope filter (repo/branch/issue) for relevant resurfacing. File-only + reliable; gbrain not required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(decision): bounded active snapshot + compaction (redact expunges, supersede archives) writeSnapshot/readSnapshot/rebuildSnapshot give an O(active) bounded read for the session-start hot path (D1A). compact() rewrites the log to active, archives superseded decisions for history, and EXPUNGES redacted ones (dropped, never archived) so an accidentally-captured secret leaves the store for good. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(decision): gstack-decision-log + gstack-decision-search bins (non-interactive) Two bins mirroring gstack-learnings-* (D3A). log writes decide/--supersede/--redact/ --compact events + refreshes the bounded snapshot + enqueues for cross-machine sync; search reads the O(active) snapshot, scope-filtered to current branch, newest-first, --all to include superseded, --json for machines. Empty store returns silently (no snapshot write on an empty read). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): surface active decisions at session start + capture nudge (Context Recovery) Context Recovery now shows recent scope-relevant active decisions (bounded read of decisions.active.json via gstack-decision-search) and instructs the agent to treat them as settled calls and to log durable decisions/reversals. Closes the Phase-1 capture->curate->resurface loop, reliable + file-only. Regen across all hosts folded in (squash-with-regen); parity 10/10, freshness green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: refresh ship golden baselines for the memory-loop preamble change Context Recovery now emits the cross-session-decisions block, so ship's preamble (all hosts) changed. Golden baselines are hand-maintained copies (gen does not write them); refresh them from the fresh gen so golden-file regression passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(memory): document the cross-session decision-memory loop in CLAUDE.md Adds a '## Cross-session decision memory' section: how to resurface (gstack-decision-search) and capture (gstack-decision-log) durable decisions, the supersede/redact/compact verbs, and a crisp durable-vs-trivial definition so the store stays signal. Reliable file-only path; gbrain not required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): emit durable decisions from ship/ceo/eng/spec at structured points Wires the four skills that finalize real decisions to capture them in the cross-session decision store, from their STRUCTURED outputs (never free-text scraping): - ship: the version bump (level + why) at write time - plan-ceo-review: accepted scope + verdict (branch-scoped) - plan-eng-review: the architecture verdict + key call (branch-scoped) - spec: the filed issue's core approach (issue-scoped) All emits are non-interactive, schema-correct (content in decision/rationale, source=skill, confidence 1-10), and best-effort (|| true) so a decision-log failure never blocks the workflow. Includes regen across hosts + refreshed ship golden baselines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 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> * feat(gbrain-sync): self-heal stale autopilot lock (dead-pid) detectAutopilot treated a lock FILE as proof of life, so a crashed gbrain daemon left a stale lock that wedged every sync forever (observed: a dead pid refused --full indefinitely). Now read the holder pid (bare or JSON body) and check liveness via signal-0: ESRCH=dead → ignore the stale signal and keep checking; EPERM=alive (other user) → active. A stale lock never masks a live autopilot process. Pure decision function — does not delete the file; the caller may clean it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(review): drop stray trailing code fence in TODOS-format Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): align section-loading E2E testNames with their TOUCHFILES keys Pre-existing on main (v1.56.x): the two section-loading E2E tests used human-label testNames ('/ship section-loading') that don't match their slug keys ('ship-section-loading') in E2E_TOUCHFILES/E2E_TIERS. Every other E2E test uses the slug as its testName, and the TOUCHFILES completeness gate requires testName to be a registered key — so the gate was red. Align both testNames to their slug keys (also fixes tier lookup for these two periodic tests). Verified failing on a clean origin/main checkout before the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: pre-landing review fixes (datamark, DRY, compact, coverage) Addresses the pre-landing review findings (all INFORMATIONAL, no criticals): - security: datamark resurfaced decision text at the render boundary (lib/gstack-decision.ts datamark() — neutralizes code fences, --- banners, <|role|>/</system> markers, control chars, newlines). Applied in gstack-decision-search human output so stored text can't masquerade as instructions in Context Recovery (codex hardening garrytan#3 / AC garrytan#7). --json stays raw. - DRY: extract resolveSlug/gitBranch/flagValue to lib/bin-context.ts; both decision bins use it instead of duplicating the helpers. - compact(): batch the archive append (one write, not N) and shrink the mid-compact crash window; simplify the opaque branch/issue ternary. - coverage: learnings-log injection rejection (D2A wiring), search --recent/ --scope + NaN-safe --recent, datamark-applied, unparseable lock body, compact-empty, corrupt-snapshot degrade. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(security): close adversarial-review findings in decision memory Adversarial review (Claude subagent) found a CRITICAL the specialist pass missed: - F1 (CRITICAL): 'Human:'/'Assistant:' turn-prefixes bypassed BOTH the write-time denylist AND datamark(), landing verbatim in agent context inside the trusted ACTIVE DECISIONS fence. Add 'human:' (+ 'disregard previous', 'from now on') to the shared denylist, and have datamark() neutralize Human:/Assistant:/System:/User: turn-prefixes (ZWSP) at the render boundary. - F2: datamark() only stripped ASCII C0; extend to Unicode line terminators (U+0085/2028/2029) and U+007F so 'strip newlines' actually holds. - F3: validateDecide blocked only HIGH secrets; MEDIUM-tier PII (e.g. SSN) persisted silently and synced cross-machine. The store is non-interactive (no confirm path), so fail closed on MEDIUM too. - F4: compact() was a lock-free read-modify-rewrite that could clobber a concurrent append (lost decision). Add an O_EXCL compact lock + a pre-rename size recheck that aborts untouched (skipped=true) if an append landed; caller re-runs. - F7: filterByScope unknown/garbage scope fell through to 'return true' (leaked into every context); fail conservative (false). F5 (pid reuse) and F6 (pgrep over-match) are intentionally left as-is: both fail SAFE (over-refuse sync); making them precise would introduce a fail-DANGEROUS path (allowing sync during a real autopilot). True disambiguation needs gbrain to stamp the lock with a start-time, which gstack doesn't own. F8 (compact moves history to archive) is by design. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(security): close cross-model (Codex) adversarial findings Codex adversarial review found a HIGH the Claude pass missed plus 3 mediums: - C1 (HIGH): gstack-decision-search --all returned every decide and IGNORED redact events, so a redacted secret still resurfaced via --all until compact ran. --all now excludes redacted (redact = expunge from every read path), still showing superseded history. - C-med: semantic (external gbrain) slug/snippet were printed raw — datamark them too so a gbrain hit can't spoof role markers / fences into agent context. - C4: semanticRecall fell back to an UNSCOPED gbrain search when no curated-memory source resolved, pulling code/doc corpora mislabeled as 'related decisions'. Now returns null (degrade) when there's no worktree-backed memory source. - C5: validateDecide scanned only decision/rationale/alternatives; branch and issue are stored + surfaced (raw via --json), so include them in the injection+secret scan. C2 (snapshot staleness) / C3 (compact TOCTOU residual): accepted for a single-user store — atomic appends never lose the event, rebuilds self-heal, and the compact size-recheck leaves only a sub-ms window; full append-locking would break the lock-free append design. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v1.57.5.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 41c6d3e commit 45cc95d

79 files changed

Lines changed: 3085 additions & 71 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ supabase/.temp/
3737

3838
# Throughput analysis — local-only, regenerate via scripts/garry-output-comparison.ts
3939
docs/throughput-*.json
40+
41+
# gbrain local source-staging dir (capability checks, source clones) — runtime artifact
42+
.sources/

CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,52 @@
11
# Changelog
22

3+
## [1.57.5.0] - 2026-06-07
4+
5+
## **Your agent now keeps its decisions, not just its code.**
6+
## **The durable calls you make, and the "why" behind them, are captured, curated, and resurfaced across sessions, with no daemon to run.**
7+
8+
Every session you and the agent settle real decisions: pick an architecture, cut a scope, choose a tool, reverse an earlier call. Until now that reasoning lived only in a transcript that scrolls away, so the next session re-litigates settled questions or loses the "why." This release adds an institutional decision memory. Durable decisions land in an append-only, event-sourced store, the scope-relevant ones surface automatically at session start, and you can search them any time. It is file-only and works with gbrain off; when gbrain is up you can add semantic recall on top. The planning and ship skills capture their own key calls so the high-value decisions get recorded without anyone remembering to. Separately, `/sync-gbrain` learned to build the cross-reference call graph and to heal a crashed daemon's stale lock instead of wedging every sync.
9+
10+
### The numbers that matter
11+
12+
No speed benchmark here, the win is capability and reliability. These are the real shape of the release (`git diff 1.57.0.0..HEAD`, `bun test`):
13+
14+
| Metric | Value |
15+
|--------|-------|
16+
| New commands | 2 (`gstack-decision-log`, `gstack-decision-search`) |
17+
| Session-start read cost | O(active) bounded snapshot, not a full-history scan |
18+
| Works with gbrain OFF | Yes, every capture/curate/resurface path is files + bins only |
19+
| New source | ~2,550 lines across 26 files |
20+
| New tests | 117 across the decision store + gbrain stages |
21+
22+
Resurfaced decision text is treated as data, not instructions (datamarked at the render boundary), secrets are blocked on write, and `redact` expunges a decision from every read path. The whole loop degrades cleanly: turn gbrain off and you still capture, curate, and resurface.
23+
24+
### What this means for you
25+
26+
Start a session tomorrow and the agent already knows what you settled and why, instead of asking again or quietly reversing it. Log a call with `gstack-decision-log`, reverse one with `--supersede`, pull the relevant history with `gstack-decision-search`. CEO, eng, spec, and ship reviews record their decisions for you. Run `/sync-gbrain` and a crashed autopilot no longer blocks your next sync.
27+
28+
### Itemized changes
29+
30+
#### Added
31+
- **Cross-session decision memory.** An event-sourced (`decide`/`supersede`/`redact`) store at `~/.gstack/projects/<slug>/decisions.jsonl`. "Active" is computed, never a mutable flag, so the history stays honest and tolerant of dangling references.
32+
- **`gstack-decision-log`** — capture a durable decision, reverse one (`--supersede <id>`), expunge an accidental secret (`--redact <id>`), or rewrite the log to its active set (`--compact`). Non-interactive, injection-sanitized, blocks HIGH and MEDIUM secrets on write.
33+
- **`gstack-decision-search`** — read active decisions, scope-filtered to the current branch/issue, with `--recent N`, `--scope`, `--query`, `--all`, `--json`. Add `--semantic` (with `--query`) to append related hits from gbrain memory when it is up; it degrades silently to the reliable file results when gbrain is off.
34+
- **Session-start resurfacing.** Context Recovery shows the scope-relevant active decisions at the top of a session, from a bounded snapshot so it stays fast as the log grows.
35+
- **Skill capture.** `/plan-ceo-review`, `/plan-eng-review`, `/spec`, and `/ship` record their structured decisions (accepted scope, architecture verdict, filed spec, version bump) automatically.
36+
- **A `## Cross-session decision memory` section in CLAUDE.md** documenting when and how to capture and resurface.
37+
- **`/sync-gbrain` call-graph build (`--dream`).** Builds the symbol cross-reference graph behind a lock-free gate, with an honest outcome guard that reports a degraded no-op as WARN rather than a false success.
38+
39+
#### Changed
40+
- Decision text that resurfaces into agent context is datamarked (code fences, `---` banners, `<|role|>`/`</system>` tags, chat turn-prefixes, and Unicode line terminators are neutralized) so stored text can never masquerade as instructions.
41+
- `/sync-gbrain` pin guidance is accurate for current gbrain, and the worktree-scoped `.gbrain-source` pin routes code queries correctly.
42+
43+
#### Fixed
44+
- `/sync-gbrain` no longer wedges forever on a crashed autopilot daemon's stale lock: it reads the holder pid, confirms liveness, and ignores a dead one (it stays conservative when it cannot tell).
45+
46+
#### For contributors
47+
- New shared `lib/jsonl-store.ts` (injection-reject + atomic single-line append + tolerant read) backs both the learnings and decision stores, so the sanitization path is audited in one place.
48+
- `lib/bin-context.ts` shares slug/branch/flag plumbing across the decision bins.
49+
350
## [1.57.4.0] - 2026-06-08
451

552
## **The completeness principle is now Boil the Ocean, matching the post it came from.**

CLAUDE.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,31 @@ Key routing rules:
905905
- Save progress → invoke /context-save
906906
- Resume context → invoke /context-restore
907907

908+
## Cross-session decision memory
909+
910+
Durable decisions and their rationale are captured in an append-only, event-sourced
911+
store at `~/.gstack/projects/<slug>/decisions.jsonl` so neither you nor the user
912+
re-litigates a settled call or loses the "why" across sessions. This is the reliable,
913+
file-only path: it works with gbrain OFF. (gbrain semantic recall is an optional
914+
enhancement layered on top, never a dependency.)
915+
916+
- **Resurface** active decisions before re-deciding: `bin/gstack-decision-search`
917+
(`--recent N`, `--scope repo|branch|issue`, `--query KW`, `--all`, `--json`).
918+
Add `--semantic` (with `--query`) to append related hits from gbrain memory when
919+
it's up; it degrades silently to the reliable file results when gbrain is off.
920+
Session start already surfaces scope-relevant active decisions via Context Recovery.
921+
If a decision is listed, treat it as settled with its rationale; if you're about to
922+
reverse it, say so explicitly.
923+
- **Capture** a DURABLE decision when you or the user make one:
924+
`bin/gstack-decision-log '{"decision":"...","rationale":"...","scope":"repo|branch|issue","source":"user|skill|agent","confidence":1-10}'`.
925+
Reverse a prior call with `--supersede <id>`; expunge an accidental secret with
926+
`--redact <id>`; rewrite the log to the active set with `--compact`. Non-interactive
927+
(never prompts), injection-sanitized, and HIGH-secret-blocking on write.
928+
- **Durable means:** architecture choice, scope cut, tool/vendor choice, or a reversal
929+
of a prior call. NOT a turn-level edit, a phrasing tweak, or anything trivially
930+
re-derivable. Capture is curated at the source — log durable decisions only, or the
931+
store becomes noise.
932+
908933
## GBrain Search Guidance (configured by /sync-gbrain)
909934
<!-- gstack-gbrain-search-guidance:start -->
910935

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.57.4.0
1+
1.57.5.0

autoplan/SKILL.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,12 +599,19 @@ if [ -d "$_PROJ" ]; then
599599
fi
600600
_LATEST_CP=$(find "$_PROJ/checkpoints" -name "*.md" -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1)
601601
[ -n "$_LATEST_CP" ] && echo "LATEST_CHECKPOINT: $_LATEST_CP"
602+
if [ -f "$_PROJ/decisions.active.json" ]; then
603+
echo "--- ACTIVE DECISIONS (recent, scope-relevant) ---"
604+
~/.claude/skills/gstack/bin/gstack-decision-search --recent 5 2>/dev/null
605+
echo "--- END DECISIONS ---"
606+
fi
602607
echo "--- END ARTIFACTS ---"
603608
fi
604609
```
605610

606611
If artifacts are listed, read the newest useful one. If `LAST_SESSION` or `LATEST_CHECKPOINT` appears, give a 2-sentence welcome back summary. If `RECENT_PATTERN` clearly implies a next skill, suggest it once.
607612

613+
**Cross-session decisions.** If `ACTIVE DECISIONS` are listed, treat them as prior settled calls with their rationale — do not silently re-litigate them; if you're about to reverse one, say so explicitly. Reach for `~/.claude/skills/gstack/bin/gstack-decision-search` whenever a question touches a past decision ("what did we decide / why / did we try"). When you or the user make a DURABLE decision (architecture, scope, tool/vendor choice, or a reversal) — NOT a turn-level or trivial choice — log it with `~/.claude/skills/gstack/bin/gstack-decision-log` (`--supersede <id>` for a reversal). Reliable and local; gbrain not required.
614+
608615
## Writing Style (skip entirely if `EXPLAIN_LEVEL: terse` appears in the preamble echo OR the user's current message explicitly requests terse / no-explanations output)
609616

610617
Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format is structure; this is prose quality.

bin/gstack-decision-log

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* gstack-decision-log — append a durable decision (or supersede/redact/compact it).
4+
*
5+
* Usage:
6+
* gstack-decision-log '{"decision":"...","rationale":"...","scope":"repo","source":"user"}'
7+
* gstack-decision-log --supersede <decision-id>
8+
* gstack-decision-log --redact <decision-id>
9+
* gstack-decision-log --compact
10+
*
11+
* Event-sourced (lib/gstack-decision): every call appends an event and refreshes the
12+
* bounded active snapshot. NON-INTERACTIVE — never prompts (agents/skills call this;
13+
* a prompt would hang them). Validation + injection + HIGH-secret rejection happen in
14+
* validateDecide; a rejected decision exits 1 with a message, nothing persisted.
15+
*/
16+
17+
import { mkdirSync } from "fs";
18+
import { dirname } from "path";
19+
import { spawnSync } from "child_process";
20+
import {
21+
decisionPaths,
22+
validateDecide,
23+
makeRefEvent,
24+
appendEvent,
25+
rebuildSnapshot,
26+
compact,
27+
type DecisionEvent,
28+
} from "../lib/gstack-decision";
29+
import { resolveSlug, gitBranch, flagValue } from "../lib/bin-context";
30+
31+
const HERE = import.meta.dir;
32+
33+
const args = process.argv.slice(2);
34+
const slug = resolveSlug(`${HERE}/gstack-slug`);
35+
const paths = decisionPaths(slug);
36+
mkdirSync(dirname(paths.log), { recursive: true });
37+
38+
function enqueue(): void {
39+
// Fire-and-forget cross-machine sync (no-op when artifacts_sync is off).
40+
spawnSync(`${HERE}/gstack-brain-enqueue`, [`projects/${slug}/decisions.jsonl`], { stdio: "ignore" });
41+
}
42+
43+
if (args.includes("--compact")) {
44+
const r = compact(paths);
45+
if (r.skipped) {
46+
console.log("compact skipped: a concurrent write/compact is in progress; log left intact — re-run");
47+
process.exit(0);
48+
}
49+
console.log(`compacted: ${r.activeCount} active, ${r.archivedCount} archived, ${r.expungedCount} expunged`);
50+
enqueue();
51+
process.exit(0);
52+
}
53+
54+
const supersedeId = flagValue(args, "--supersede");
55+
const redactId = flagValue(args, "--redact");
56+
if (supersedeId || redactId) {
57+
const kind = supersedeId ? "supersede" : "redact";
58+
const targetId = (supersedeId || redactId) as string;
59+
appendEvent(paths, makeRefEvent(kind, targetId, { source: "agent" }));
60+
rebuildSnapshot(paths);
61+
enqueue();
62+
console.log(`${kind}: ${targetId}`);
63+
process.exit(0);
64+
}
65+
66+
const jsonArg = args.find((a) => !a.startsWith("--"));
67+
if (!jsonArg) {
68+
process.stderr.write(
69+
"gstack-decision-log: provide a JSON decision, or --supersede/--redact <id>, or --compact\n",
70+
);
71+
process.exit(1);
72+
}
73+
let obj: Partial<DecisionEvent>;
74+
try {
75+
obj = JSON.parse(jsonArg);
76+
} catch {
77+
process.stderr.write("gstack-decision-log: invalid JSON\n");
78+
process.exit(1);
79+
}
80+
if (obj.scope === "branch" && !obj.branch) obj.branch = gitBranch();
81+
const res = validateDecide(obj);
82+
if (!res.ok) {
83+
process.stderr.write(`gstack-decision-log: ${res.error}\n`);
84+
process.exit(1);
85+
}
86+
appendEvent(paths, res.event);
87+
rebuildSnapshot(paths);
88+
enqueue();
89+
console.log(res.event.id);

bin/gstack-decision-search

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* gstack-decision-search — read active decisions (the curated "what did we decide" view).
4+
*
5+
* Usage:
6+
* gstack-decision-search [--query KW] [--scope repo|branch|issue]
7+
* [--branch B] [--issue I] [--recent N] [--all] [--json]
8+
* [--semantic]
9+
*
10+
* Reads the BOUNDED active snapshot (decisions.active.json) — O(active), not a full
11+
* history scan — and rebuilds it from the event log if missing. Scope-filtered to the
12+
* current branch/issue context (recency != relevance). NON-INTERACTIVE. `--all` shows
13+
* superseded decisions too (from the full log). Exit 0 silently when there are none.
14+
*
15+
* `--semantic` (with `--query`) appends an OPTIONAL "related from memory" block from
16+
* gbrain semantic recall. It is a pure enhancement: when gbrain is off/unconfigured/
17+
* empty it degrades silently to the reliable file results above. The reliable path
18+
* never loads gbrain code (the semantic module is imported lazily only here).
19+
*/
20+
21+
import { existsSync } from "fs";
22+
import {
23+
decisionPaths,
24+
readSnapshot,
25+
rebuildSnapshot,
26+
readEvents,
27+
filterByScope,
28+
datamark,
29+
type ActiveDecision,
30+
} from "../lib/gstack-decision";
31+
import { resolveSlug, gitBranch, flagValue } from "../lib/bin-context";
32+
33+
const HERE = import.meta.dir;
34+
const args = process.argv.slice(2);
35+
36+
const slug = resolveSlug(`${HERE}/gstack-slug`);
37+
const paths = decisionPaths(slug);
38+
const queryRaw = flagValue(args, "--query");
39+
const query = queryRaw?.toLowerCase();
40+
const scope = flagValue(args, "--scope");
41+
const branch = flagValue(args, "--branch") ?? gitBranch();
42+
const issue = flagValue(args, "--issue");
43+
const recentRaw = flagValue(args, "--recent");
44+
const recent = recentRaw ? parseInt(recentRaw, 10) : undefined;
45+
const showAll = args.includes("--all");
46+
const asJson = args.includes("--json");
47+
const semantic = args.includes("--semantic");
48+
49+
let rows: ActiveDecision[];
50+
if (showAll) {
51+
// --all includes SUPERSEDED decisions (history), but NEVER redacted ones — a redact
52+
// is an expunge, so it must remove the text from every read path, not just active.
53+
const events = readEvents(paths);
54+
const redacted = new Set(
55+
events.filter((e) => e.kind === "redact" && e.supersedes).map((e) => e.supersedes as string),
56+
);
57+
rows = events.filter((e): e is ActiveDecision => e.kind === "decide" && !redacted.has(e.id));
58+
} else {
59+
rows = readSnapshot(paths);
60+
// Rebuild only when a snapshot is absent but a log exists (don't write a snapshot
61+
// into a nonexistent store on an empty read — just return nothing).
62+
if (!rows.length && existsSync(paths.log)) rows = rebuildSnapshot(paths);
63+
}
64+
65+
rows = filterByScope(rows, { branch, issue });
66+
if (scope) rows = rows.filter((d) => d.scope === scope);
67+
if (query) {
68+
rows = rows.filter((d) =>
69+
[d.decision, d.rationale, d.alternatives_considered]
70+
.filter((s): s is string => typeof s === "string")
71+
.some((s) => s.toLowerCase().includes(query)),
72+
);
73+
}
74+
rows.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)); // newest first
75+
if (recent && recent > 0) rows = rows.slice(0, recent);
76+
77+
if (asJson) {
78+
// --json stays reliable-only (semantic recall is a human-facing supplement).
79+
console.log(JSON.stringify(rows));
80+
process.exit(0);
81+
}
82+
83+
for (const d of rows) {
84+
// Datamark all stored free-text (decision, rationale, branch/issue) — it lands in
85+
// agent context via Context Recovery, so treat it as DATA, not instructions.
86+
const branchTag = d.branch ? `:${datamark(d.branch)}` : "";
87+
const issueTag = d.issue ? `:${datamark(d.issue)}` : "";
88+
const scopeTag = d.scope === "repo" ? "" : ` [${d.scope}${branchTag}${issueTag}]`;
89+
console.log(`- ${datamark(d.decision ?? "")}${scopeTag} (${d.source}, ${d.date.slice(0, 10)})`);
90+
if (d.rationale) console.log(` why: ${datamark(d.rationale)}`);
91+
}
92+
93+
// OPTIONAL gbrain enhancement. Lazy import so the reliable path above never loads
94+
// gbrain code. Degrades silently: null (gbrain off) or [] (nothing found) leaves the
95+
// reliable results above as the answer.
96+
if (semantic && queryRaw) {
97+
const { semanticRecall } = await import("../lib/gstack-decision-semantic");
98+
const hits = semanticRecall(queryRaw);
99+
if (hits && hits.length) {
100+
console.log("\nRelated from memory (gbrain semantic recall):");
101+
for (const h of hits) {
102+
// gbrain hits are EXTERNAL corpus content — datamark slug + snippet too so they
103+
// can't spoof role markers / fences when printed into agent context.
104+
const snip = datamark(h.snippet.length > 100 ? `${h.snippet.slice(0, 100)}…` : h.snippet);
105+
console.log(` [${h.score.toFixed(2)}] ${datamark(h.slug)}: ${snip}`);
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)