Merge remote-tracking branch 'origin/main' into garrytan/colombo-v3

# Conflicts:
#	CHANGELOG.md
#	VERSION
#	package.json
This commit is contained in:
Garry Tan
2026-05-20 07:49:31 -07:00
75 changed files with 3231 additions and 215 deletions
+4 -1
View File
@@ -192,7 +192,10 @@ function resolveSkillFile(args: CliArgs): string | null {
function gbrainAvailable(): boolean {
try {
execFileSync("command", ["-v", "gbrain"], { stdio: "ignore" });
execFileSync("gbrain", ["--version"], {
stdio: "ignore",
timeout: MCP_TIMEOUT_MS,
});
return true;
} catch {
return false;
+9 -2
View File
@@ -287,13 +287,20 @@ function gbrainSupportsSourcesRename(env?: NodeJS.ProcessEnv): boolean {
* `env` is the environment passed to the spawned `gbrain` process; defaults
* to `process.env`. Tests inject a PATH that points at a gbrain shim so the
* helper can be exercised without a real gbrain CLI.
*
* Shape note: `gbrain sources list --json` returns `{sources: [...]}` (v0.20+);
* older versions returned a flat array. Accept both for forward/backward compat
* (mirrors `probeSource`/`sourcePageCount` in lib/gbrain-sources.ts).
*/
export function sourceLocalPath(sourceId: string, env?: NodeJS.ProcessEnv): string | null {
const list = execGbrainJson<Array<{ id: string; local_path?: string }>>(
const raw = execGbrainJson<unknown>(
["sources", "list", "--json"],
{ baseEnv: env },
);
if (!list) return null;
if (!raw) return null;
const list: Array<{ id?: string; local_path?: string }> = Array.isArray(raw)
? (raw as Array<{ id?: string; local_path?: string }>)
: ((raw as { sources?: Array<{ id?: string; local_path?: string }> }).sources ?? []);
const found = list.find((s) => s.id === sourceId);
return found?.local_path ?? null;
}
+13 -6
View File
@@ -273,16 +273,23 @@ function resolveClaudeCodeCwd(
return null;
}
function extractCwdFromJsonl(filePath: string): string | null {
export function extractCwdFromJsonl(filePath: string): string | null {
// Read a capped prefix so huge JSONL files don't blow up memory. 64KB
// comfortably fits the largest observed session headers; the old 8KB cap
// would sometimes fall inside a single long line and silently drop the
// project (JSON.parse failure on the truncated tail).
const MAX_BYTES = 64 * 1024;
const MAX_LINES = 30;
try {
// Read only the first 8KB to avoid loading huge JSONL files into memory
const fd = openSync(filePath, "r");
const buf = Buffer.alloc(8192);
const bytesRead = readSync(fd, buf, 0, 8192, 0);
const buf = Buffer.alloc(MAX_BYTES);
const bytesRead = readSync(fd, buf, 0, MAX_BYTES, 0);
closeSync(fd);
const text = buf.toString("utf-8", 0, bytesRead);
const lines = text.split("\n").slice(0, 15);
for (const line of lines) {
// Drop the final segment — it may be an incomplete line at the cap boundary.
const parts = text.split("\n");
const completeLines = parts.length > 1 ? parts.slice(0, -1) : parts;
for (const line of completeLines.slice(0, MAX_LINES)) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line);
+3 -3
View File
@@ -194,7 +194,7 @@ Options:
--all-history Walk transcripts older than 90 days too.
--sources <list> Comma-separated subset: ${ALL_TYPES.join(",")}
--limit <N> Stop after N pages written (smoke testing).
--no-write Skip gbrain put_page calls (still updates state file).
--no-write Skip gbrain put calls (still updates state file).
Used by tests + dry runs without actual ingest.
--scan-secrets Opt-in per-file gitleaks scan during prepare. Off by
default; gstack-brain-sync already gates the git-push
@@ -1061,7 +1061,7 @@ async function probeMode(args: CliArgs): Promise<ProbeReport> {
}
// Per ED2: ~25-35 min for ~11.7K transcripts = ~150ms/page synchronous
// (gitleaks + render + put_page + embedding). Scale linearly.
// (gitleaks + render + put + embedding). Scale linearly.
const estimateMinutes = Math.max(1, Math.round((newCount + updatedCount) * 0.15 / 60));
return {
@@ -1374,7 +1374,7 @@ async function ingestPass(args: CliArgs): Promise<BulkResult> {
if (args.noWrite) {
// --no-write: skip the gbrain import call but still record state for
// prepared pages (treat them as ingested for dedup purposes). Matches
// the prior contract from --help: "Skip gbrain put_page calls (still
// the prior contract from --help: "Skip gbrain put calls (still
// updates state file)".
const nowIso = new Date().toISOString();
for (const p of prep.prepared) {
+6 -2
View File
@@ -9,7 +9,7 @@
# CI / container env where HOME may be unset.
#
# Chains:
# GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA -> $HOME/.gstack -> .gstack
# GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA (only when CLAUDE_PLUGIN_ROOT=*gstack*) -> $HOME/.gstack -> .gstack
# PLAN_ROOT: GSTACK_PLAN_DIR -> CLAUDE_PLANS_DIR -> $HOME/.claude/plans -> .claude/plans
# TMP_ROOT: TMPDIR -> TMP -> .gstack/tmp (and mkdir -p, best-effort)
#
@@ -21,7 +21,11 @@ set -u
# State root: where gstack writes projects/, sessions/, analytics/.
if [ -n "${GSTACK_HOME:-}" ]; then
_state_root="$GSTACK_HOME"
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ]; then
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ] && echo "${CLAUDE_PLUGIN_ROOT:-}" | grep -qi "gstack"; then
# Guard: only trust CLAUDE_PLUGIN_DATA when CLAUDE_PLUGIN_ROOT confirms we are
# running as the gstack plugin. Without this, a CLAUDE_PLUGIN_DATA from another
# plugin (e.g. codex) that leaked into the session env via CLAUDE_ENV_FILE would
# be picked up, writing all gstack state into the wrong directory.
_state_root="$CLAUDE_PLUGIN_DATA"
elif [ -n "${HOME:-}" ]; then
_state_root="$HOME/.gstack"
+7 -1
View File
@@ -107,7 +107,13 @@ BATCH="$BATCH]"
[ "$COUNT" -eq 0 ] && exit 0
# ─── POST to edge function ───────────────────────────────────
RESP_FILE="$(mktemp /tmp/gstack-sync-XXXXXX 2>/dev/null || echo "/tmp/gstack-sync-$$")"
# Create response file atomically. If mktemp fails, refuse to continue rather
# than fall back to a predictable $$-based path (race + overwrite footgun).
RESP_FILE="$(mktemp "${TMPDIR:-/tmp}/gstack-sync-XXXXXX")" || {
echo "gstack-telemetry-sync: mktemp failed — skipping this run" >&2
exit 0
}
trap 'rm -f "$RESP_FILE"' EXIT
HTTP_CODE="$(curl -s -w '%{http_code}' --max-time 10 \
-X POST "${SUPABASE_URL}/functions/v1/telemetry-ingest" \
-H "Content-Type: application/json" \