Skip to content

Commit 64f9aaf

Browse files
garrytanRookclaudejbetala7Christoph
authored
v1.44.1.0 fix wave: post-windhoek paper-cut — 9 community PRs in one bundle (#1682)
* fix(office-hours): #1671 — session writer was writing to the legacy file User-visible symptom: returning /office-hours users get the same closing pitch every visit, no matter how many times they've run the skill. The welcome_back tier (which exists specifically to skip the pitch for returning users) was unreachable. Live since 2026-04-18 / v1.0.0.0 on every fresh-$HOME user. Root cause: the v1.0.0.0 migration moved the read path to ~/.gstack/developer-profile.json but left the writer in office-hours/SKILL.md.tmpl writing to the legacy ~/.gstack/builder-profile.jsonl. Reader and writer disagreed on storage, so SESSION_COUNT never incremented and /office-hours always treated the user as a first-timer. Fix: - bin/gstack-developer-profile: new --log-session subcommand that read-modify-writes developer-profile.json's sessions[] array (atomic mktemp+mv, signals/resources/topics aggregation, gbrain-enqueue mirror of gstack-timeline-log:40). Naming matches the gstack-*-log family verb. - bin/gstack-developer-profile: do_read filters mode:"resources" entries when picking LAST_PROJECT/LAST_ASSIGNMENT/LAST_DESIGN_TITLE so the Phase 6 resources auto-append doesn't clobber real-session state. Latent bug that was masked by the broken writer; activated by the fix. - office-hours/SKILL.md.tmpl: lines 490 + 893 swap echo >> for --log-session. - test/gstack-developer-profile.test.ts: +8 tests covering --log-session contract (regression, aggregation, dedup, validation, ts handling) plus the mode-filter regression. All 8 fail on main, all 8 pass with this fix. - test/static-no-legacy-writes.test.ts: new static-grep invariant walking every skill dir to prevent future regressions onto the legacy file. Affected users: stranded builder-profile.jsonl entries are not recovered automatically by this PR. On their next /office-hours run, the first new session lands in welcome_back; past data stays in the legacy file (still readable by other tools during deprecation). Most pre-existing users have only a handful of stranded sessions. See docs/designs/FIX_1671_PROFILE_MIGRATION.md for scope decisions (RC2/RC3 follow-ups, what was intentionally left out, and why). Issue: #1671 * test(office-hours): refine #1671 invariant regex comment for literal-path scope Clarifies that the WRITE_PATTERN regex catches literal-path writes only; variable-indirected writes (FILE=...; echo >> "$FILE") are not detected. The SKILL.md.tmpl assertions in the same suite pin the exact #1671 regression class directly; this regex is a backstop, not a flow analyzer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(timeline): pass read filters as data * feat(next-version): support monorepo VERSION paths via --version-path + .gstack/version-path The workspace-aware ship queue hardcoded the VERSION file at the repo root. In monorepos where versioning is subproject-scoped (one app inside a larger repo), every PR's VERSION lookup 404s, the queue silently empties, and parallel /ship sessions all bump from "current main + 1" — producing a cascade of slot collisions. Repro: tinas-second-brain repo. Root VERSION is absent; the real VERSION lives at "Tinas Second Brain/health-tracker/VERSION". In one day, four sequential collisions: 0.4.0.1 -> 0.5.0.0 -> 0.5.0.1 -> 0.5.0.2 -> 0.5.0.3. Fix: add a --version-path flag and a repo-local .gstack/version-path config file. Resolution priority: CLI flag > .gstack/version-path > "VERSION". The resolved path threads through all four call sites — git show origin/<base>:<path>, the GitHub Contents API, the GitLab files API, and the local sibling-worktree scan — and shows up in the JSON output as version_path so /ship and operators can see what got picked. The previous warning "could not fetch VERSION (fork or private)" was misleading whenever the real cause was wrong path. The new wording names the path that 404'd and hints at the two knobs. Backward-compatible: no flag, no config, no change in behavior. Tests: 6 unit tests for resolveVersionPath (priority, parsing, blank / missing / empty edge cases) + a second integration smoke that drives --version-path end-to-end and asserts it surfaces in JSON output. * fix(investigate): support standalone freeze hook path * fix(browse): clarify localhost bind failures * fix(migration): defer v1.40.0.0 done-marker until every repair succeeds (#1581) The v1.40.0.0 migration unconditionally `touch`ed its done-marker, even when the jq-gated `.brain-privacy-map.json` patch was skipped because jq was missing on the user's machine. On subsequent runs, the script short-circuited on the marker so the privacy-map repair never landed. Federation sync then silently dropped `/plan-eng-review` test plans. Track every failure mode via a single `incomplete` flag: jq missing, malformed JSON, jq mutation failure, tempfile creation failure, `mv` failure, allowlist append failure, gitattributes append failure. The marker is written only when `incomplete=0`, so the migration runner retries on the next /gstack-upgrade once the prerequisites are met. * test(migration): unit tests for v1.40.0.0 deferred done-marker fix (#1581) 8 cases pinning the fix: - Case 1 (happy path): jq present, fresh privacy-map → all three files patched, marker written. - Case 2 (regression for #1581): jq missing, privacy-map present → marker must NOT be written. Fails against the buggy script, passes against the fix. - Case 3 (recovery): jq missing, then jq restored → patch lands on second run. - Case 4 (idempotency): privacy-map already has correct entry → no mutation, marker written. - Case 5 (fresh-init): privacy-map file absent → allowlist + gitattrs patched, marker written. - Case 6 (malformed JSON): broken privacy-map JSON → no marker, no mutation. - Case 7 (jq mutation failure): fake jq returning 1 → no marker, tempfile cleaned up. - Case 8 (allowlist append failure): read-only allowlist → no marker. Tests use spawnSync('bash', [MIGRATION], …) with isolated tmpHomes. "jq missing" sets PATH to a curated dir of symlinks to standard utils, omitting jq; "jq mutation fails" uses an `exit 1` shim. Avoids blanket-clearing PATH (which would hide bash/grep/etc). * fix(brain-sync): make artifact sync work on Windows (discover-new + drain) Automatic artifact sync was fully non-functional on Windows (Git Bash): --discover-new enqueued nothing and the --once drain staged nothing, so artifacts_sync_mode looked active but no artifacts ever reached the repo. Three independent Windows-only causes in bin/gstack-brain-sync: 1. discover-new matched os.path.relpath (backslash separators on Windows) against the forward-slash allowlist globs, so no nested file ever matched. Normalized the relpath to "/". 2. discover-new enqueued via subprocess.run([gstack-brain-enqueue, rel]), but Windows Python cannot exec a bash-shebang script, so nothing was enqueued even once matched. Now appends to the queue in-process. 3. compute_paths_to_stage ends in print(p); Windows Python emits CRLF, the bash `read -r` keeps the trailing CR, and `git add -- "path<CR>"` matches nothing under `2>/dev/null || true`. Now strips the CR before staging. The in-process enqueue mirrors gstack-brain-enqueue's contract: one atomic O_APPEND write per record (each line < PIPE_BUF) so a parallel writer-shim append can't interleave mid-record, and the discover cursor advances only after the write succeeds, so a failed write retries instead of silently recording the file as synced. Skip-list entries are separator-normalized on both the discover and drain (compute_paths_to_stage) sides, so a backslash .brain-skip.txt entry can't be honored at discovery yet bypassed at commit. Adds test/brain-sync-windows-paths.test.ts (static invariants -- behavioral spawn tests cannot run on the Windows lane, since Node/Bun cannot exec the bin/ shebang scripts there) and wires it into windows-free-tests.yml. Verified red->green and end-to-end on Windows 11 / Git Bash; macOS/Linux behavior unchanged (os.sep is already "/", no CRLF, compute path logic unchanged besides the shared skip normalization). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: detect bun.lock (Bun v1.2+ text lockfile) in diff-scope CONFIG gstack-diff-scope only matched the legacy binary lockfile `bun.lockb` but not the newer text-based `bun.lock` introduced in Bun v1.2+. Projects using current Bun versions were silently missing the SCOPE_CONFIG signal when only the lockfile changed. 🤖 Generated with [Qoder][https://qoder.com] * fix(ios-qa): resolve CoreDevice tunnel via devicectl + keep tunnel alive The daemon's tunnel bootstrap used `dns.resolve6` to look up `<device>.coredevice.local`, which fails with ESERVFAIL on macOS 26.x (Darwin 25.x) because Node's resolve6 path goes through libresolv and does NOT consult mDNSResponder. `dns.lookup` (getaddrinfo) does. Even when resolution works, CoreDevice in Xcode 26 only holds the USB tunnel up while a devicectl command is in-flight, so the IPv6 ULA becomes unroutable within ~10-15s of idle and subsequent proxy requests time out. Two-part fix: 1. Resolution order is now (a) `xcrun devicectl device info details --json-output` to read `result.connectionProperties.tunnelIPAddress` directly, (b) mDNS via `dns.lookup`, (c) legacy `dns.resolve6` as a last-ditch fallback. 2. After a successful bootstrap the daemon spawns a periodic `devicectl device info details` (~5s) to keep the tunnel session alive. Cleaned up on SIGINT/SIGTERM/exit. Adds tests for `getDeviceTunnelIPv6FromDevicectl`, the `resolveTunnelIPv6` fallback chain, and `startTunnelKeepalive`. Existing bootstrap tests updated to include the new `device info details` spawn step. Tested against: iPhone 12 Pro on iOS 26.x via Mac Mini M-series running macOS Sequoia 15.x / Darwin 25.3.0. * chore(release): v1.44.1.0 — 9-PR community fix wave (post-windhoek paper-cut) Bump VERSION + CHANGELOG entry. Wave covers /office-hours session counter, iOS QA macOS 26 tunnels, Windows brain-sync, browse server bind diagnostics, monorepo VERSION layouts, /investigate freeze hook on standalone installs, gstack-timeline-read quote injection, v1.40.0.0 migration on jq-less machines, bun.lock detection. 9 community PRs: #1676 #1635 #1627 #1648 #1664 #1589 #1672 #1649 #1673 9 contributors credited: @pryow @jbetala7 @cfeddersen @Gujiassh @spacegeologist @stedfn @daveowenatl @hiSandog @sternryan 4 issues closed: #1671 #1677 #1634 #1647 #1581 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Rook <rook@robomovers.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com> Co-authored-by: Christoph <astaran@herr-der-ringe-film.de> Co-authored-by: gujishh <baiaoshh@163.com> Co-authored-by: zhengzuo0-ai <zheng.zuo0@gmail.com> Co-authored-by: Stefan Neamtu <stefan.neamtu@nearone.org> Co-authored-by: Dave Owen <daveowen66@gmail.com> Co-authored-by: 陈家名 <chenjiaming@kezaihui.com> Co-authored-by: Ryan Stern <206953196+sternryan@users.noreply.github.com>
1 parent 920a13a commit 64f9aaf

29 files changed

Lines changed: 1696 additions & 116 deletions

.github/workflows/windows-free-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ jobs:
116116
test/setup-windows-fallback.test.ts \
117117
test/build-script-shell-compat.test.ts \
118118
test/docs-config-keys.test.ts \
119+
test/brain-sync-windows-paths.test.ts \
119120
make-pdf/test/browseClient.test.ts \
120121
make-pdf/test/pdftotext.test.ts
121122
shell: bash

CHANGELOG.md

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

3+
## [1.44.1.0] - 2026-05-24
4+
5+
## **Nine community fixes ship in one bundle.** Office-hours session counter works again, iOS QA tunnels survive macOS 26.x, Windows brain-sync stops dropping artifacts, browse server tells you whether the bind failure was a port collision or a sandbox block.
6+
7+
The fix wave pattern runs its second pass after v1.43.2.0's 15-PR Daegu wave. Nine contributor PRs land in eleven commits plus a merge from new main. Each cherry-pick routes through `git cherry-pick` per-commit so contributor authorship survives in `git log --author`, with `Co-Authored-By` trailers for GitHub's contribution UI. Wave-meta files (VERSION, CHANGELOG, version-only `package.json` bumps) stripped per cherry-pick so the wave owns its own bump cleanly.
8+
9+
The triage caught a real failure mode mid-flight. An initial scope of 18 PRs went through Codex review as outside voice; Codex flagged that 9 of the 18 had already shipped via v1.43.2.0 or sibling commits. Verified against current main (`bin/gstack-gbrain-sync.ts:404` already wraps `{sources:[...]}`, `browser-manager.ts:30` already has `isCustomChromium`, `server.ts:209` already has `ownsTerminalAgent`). Recompute trimmed the wave from 18 to 9, saving nine empty cherry-picks and nine misleading "landed in" close comments to contributors whose work had already merged via another route.
10+
11+
### The numbers that matter
12+
13+
Source: `git log origin/main..HEAD` and `gh pr view --json closingIssuesReferences` per wave PR.
14+
15+
| Metric | Value |
16+
|----------------------------------------------|------------|
17+
| Community PRs landed | 9 |
18+
| Distinct contributors credited | 9 |
19+
| Issues auto-closed by merge | 4 |
20+
| Files changed | 26 |
21+
| Lines added | 1,651 |
22+
| Lines removed | 114 |
23+
| Wave commits (excluding merge) | 11 |
24+
| Already-shipped PRs caught + politely closed | 9 |
25+
| Paid eval suites that ran (all PASS) | 6 |
26+
27+
### What this means for contributors
28+
29+
Your fix lands as a commit with your name in `git log --author=<your-handle>`. If your PR had multiple commits, each lands separately so dates and trailers survive. If your fix was the same as something that shipped via another route in v1.43.2.0, you get a close comment pointing at the CHANGELOG line that credits you by name. The recompute step that catches duplicates is now part of every future fix wave.
30+
31+
### Itemized changes
32+
33+
**Added**
34+
- `/investigate` freeze hook resolves on standalone marketplace installs. Falls back through both bundled and standalone freeze-bin paths instead of crashing on a hardcoded `../freeze/` lookup. Closes #1647. Contributed by @Gujiassh via PR #1648.
35+
- `gstack-next-version --version-path` flag plus `.gstack/version-path` config: monorepo VERSION layouts now work. Contributed by @cfeddersen via PR #1627.
36+
37+
**Fixed**
38+
- `/office-hours` SESSION_COUNT stuck at 0 since v1.0. Writer wrote to legacy `builder-profile.jsonl`, reader read from new `developer-profile.json`. Reader-path auto-migrates existing legacy data on first call; existing users keep their session history. 33 regression tests plus a static-grep invariant pinning the no-legacy-writes contract. Closes #1671, #1677. Contributed by @pryow via PR #1676.
39+
- `gstack-timeline-read --branch "feature/o'hare"` no longer breaks on single-quoted branch names. Filters passed as data, not interpolated into a shell command. Closes #1634. Contributed by @jbetala7 via PR #1635.
40+
- `browse` server localhost bind: distinguishes `EADDRINUSE` (real port collision) from sandbox `EPERM` (Codex/Conductor shell sandbox blocking the bind syscall). Tells the user which one happened. Contributed by @spacegeologist via PR #1664.
41+
- `v1.40.0.0` migration on jq-less machines: defers done-marker until every repair succeeds, instead of writing it unconditionally. Re-runs the migration on next upgrade for users who hit the pre-fix path. 8-case regression test. Closes #1581. Contributed by @stedfn via PR #1589.
42+
- Three Windows brain-sync bugs: backslash vs forward-slash globs, bash-shebang subprocess fail on `cmd.exe`, CRLF on stdout breaking `git add`. Static-invariant tests added to `windows-free-tests.yml`. Contributed by @daveowenatl via PR #1672.
43+
- `gstack-diff-scope` detects `bun.lock` (Bun v1.2+ text lockfile) alongside `bun.lockb`. Without this, eval-select skipped lockfile changes on Bun 1.2+. Contributed by @hiSandog via PR #1649.
44+
- iOS QA on macOS 26.x: `coredevice.local` resolution falls through `xcrun devicectl` → `dns.lookup` → `dns.resolve6` so the tunnel comes up even when mDNSResponder is bypassed. Tunnel keepalive added so long-running QA sessions survive. Contributed by @sternryan via PR #1673.
45+
346
## [1.44.0.0] - 2026-05-23
447

548
## **Sidebar Claude Code now survives the day.** WebSocket keepalive, transparent re-attach across network blips with scrollback intact, and a restart button that actually kills the old claude before spawning the new one. Outer supervisor opt-in so the browse server itself can crash and recover without you noticing.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.44.0.0
1+
1.44.1.0

bin/gstack-brain-sync

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,11 @@ def load_privacy_map(path):
136136
137137
allowlist_globs = load_lines(allowlist_path)
138138
privacy_map = load_privacy_map(privacy_path)
139-
skip_lines = set(load_lines(skip_path))
139+
# Normalize skip entries to the POSIX form queued paths use, so a backslash
140+
# entry in .brain-skip.txt still matches on Windows. The drain is the safety
141+
# boundary that actually stages files, so it must normalize identically to
142+
# discover_new — otherwise an explicitly-skipped file gets committed.
143+
skip_lines = {s.replace(os.sep, "/") for s in load_lines(skip_path)}
140144
141145
# Read queue; collect unique file paths.
142146
queue_paths = set()
@@ -253,6 +257,8 @@ subcmd_once() {
253257

254258
# Stage with git add -f (forces past .gitignore=*) explicit paths only.
255259
while IFS= read -r p; do
260+
p="${p%$'\r'}" # Windows: compute_paths_to_stage's python print() emits CRLF;
261+
# a trailing CR makes the pathspec match nothing (silent no-stage).
256262
[ -z "$p" ] && continue
257263
git -C "$GSTACK_HOME" add -f -- "$p" 2>/dev/null || true
258264
done < "$paths_file"
@@ -376,10 +382,13 @@ subcmd_discover_new() {
376382
exit 0
377383
fi
378384
# Walk allowlist globs; enqueue any file where mtime+size differs from cursor.
379-
python3 - "$GSTACK_HOME" "$ALLOWLIST" "$DISCOVER_CURSOR" "$SCRIPT_DIR/gstack-brain-enqueue" <<'PYEOF' 2>/dev/null || true
380-
import sys, os, json, glob, fnmatch, subprocess, hashlib
385+
python3 - "$GSTACK_HOME" "$ALLOWLIST" "$DISCOVER_CURSOR" <<'PYEOF' 2>/dev/null || true
386+
import sys, os, json, fnmatch
387+
from datetime import datetime, timezone
381388
382-
gstack_home, allowlist_path, cursor_path, enqueue_bin = sys.argv[1:5]
389+
gstack_home, allowlist_path, cursor_path = sys.argv[1:4]
390+
queue_path = os.path.join(gstack_home, ".brain-queue.jsonl")
391+
skip_path = os.path.join(gstack_home, ".brain-skip.txt")
383392
384393
def load_lines(path):
385394
try:
@@ -403,8 +412,12 @@ def save_cursor(path, data):
403412
pass
404413
405414
allowlist = load_lines(allowlist_path)
415+
# Normalize skip entries to the same POSIX form as `rel` below, so a
416+
# backslash entry in .brain-skip.txt still matches a normalized path on Windows.
417+
skip = {s.replace(os.sep, "/") for s in load_lines(skip_path)}
406418
cursor = load_cursor(cursor_path)
407419
new_cursor = dict(cursor)
420+
to_enqueue = []
408421
409422
# Walk all files under gstack_home, match against allowlist.
410423
for root, dirs, files in os.walk(gstack_home):
@@ -413,22 +426,54 @@ for root, dirs, files in os.walk(gstack_home):
413426
continue
414427
for name in files:
415428
full = os.path.join(root, name)
416-
rel = os.path.relpath(full, gstack_home)
429+
# Repo paths are POSIX-relative. os.path.relpath yields backslash
430+
# separators on Windows, which never match the forward-slash allowlist
431+
# globs (e.g. "projects/*/learnings.jsonl"), so discovery silently
432+
# enqueued nothing under projects/ on Windows. Normalize to "/".
433+
rel = os.path.relpath(full, gstack_home).replace(os.sep, "/")
417434
if rel.startswith(".brain-"):
418435
continue
419-
matched = any(fnmatch.fnmatchcase(rel, pat) for pat in allowlist)
420-
if not matched:
436+
if not any(fnmatch.fnmatchcase(rel, pat) for pat in allowlist):
437+
continue
438+
if rel in skip:
421439
continue
422440
try:
423441
st = os.stat(full)
424442
key = f"{int(st.st_mtime)}:{st.st_size}"
425443
except OSError:
426444
continue
427-
prev = cursor.get(rel)
428-
if prev != key:
429-
# Enqueue via the shim (respects sync mode + skip list).
430-
subprocess.run([enqueue_bin, rel], check=False)
431-
new_cursor[rel] = key
445+
if cursor.get(rel) != key:
446+
to_enqueue.append((rel, key))
447+
448+
# Append to the queue directly. The previous implementation shelled out to
449+
# gstack-brain-enqueue once per file, but Windows Python cannot exec a
450+
# bash-shebang script (the spawn fails with a fork error), so discovery
451+
# enqueued nothing on Windows even after the path-match fix above.
452+
# Writing the queue line here is platform-agnostic; the drain step
453+
# (compute_paths_to_stage) still re-applies the skip-list + privacy filters.
454+
if to_enqueue:
455+
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
456+
try:
457+
# One atomic append per record (O_APPEND, each line < PIPE_BUF), matching
458+
# gstack-brain-enqueue's concurrency contract so a writer-shim append
459+
# running in parallel can't interleave mid-record. Buffered text writes
460+
# don't guarantee that. Compact separators match the shim's JSON shape.
461+
fd = os.open(queue_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
462+
try:
463+
for rel, key in to_enqueue:
464+
rec = json.dumps({"file": rel, "ts": ts}, separators=(",", ":"))
465+
os.write(fd, (rec + "\n").encode("utf-8"))
466+
finally:
467+
os.close(fd)
468+
except OSError:
469+
# Queue write failed (disk full, AV file lock). Leave the cursor
470+
# unadvanced so these files are retried on the next discover instead of
471+
# being silently recorded as synced (which loses the change until the
472+
# file next changes).
473+
to_enqueue = []
474+
# Advance the cursor only for records actually written.
475+
for rel, key in to_enqueue:
476+
new_cursor[rel] = key
432477
433478
save_cursor(cursor_path, new_cursor)
434479
PYEOF

bin/gstack-developer-profile

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
# --check-mismatch detect meaningful gaps between declared and observed.
1818
# --migrate migrate builder-profile.jsonl → developer-profile.json.
1919
# Idempotent; archives the source file on success.
20+
# --log-session append a session entry (from /office-hours) to
21+
# sessions[] and update aggregates. Required fields:
22+
# date, mode. Silent skip on invalid input.
2023
#
2124
# Profile file: ~/.gstack/developer-profile.json (unified schema — see
2225
# docs/designs/PLAN_TUNING_V0.md). Event file: ~/.gstack/projects/{SLUG}/
@@ -154,6 +157,65 @@ ensure_profile() {
154157
EOF
155158
}
156159

160+
# -----------------------------------------------------------------------
161+
# Record session: append a session entry from /office-hours to sessions[]
162+
# and update aggregates (signals_accumulated, resources_shown, topics).
163+
# Fix for #1671: the writer side of the v1.0.0.0 migration. Reader and
164+
# writer now share the same file.
165+
# Silent skip on invalid input (matches gstack-timeline-log:22-26 pattern).
166+
# -----------------------------------------------------------------------
167+
do_log_session() {
168+
local INPUT="${1:-}"
169+
if [ -z "$INPUT" ]; then
170+
return 0
171+
fi
172+
173+
# Validate: input must be parseable JSON with required fields (date, mode).
174+
if ! printf '%s' "$INPUT" | bun -e "
175+
const j = JSON.parse(await Bun.stdin.text());
176+
if (!j.date || !j.mode) process.exit(1);
177+
" 2>/dev/null; then
178+
return 0
179+
fi
180+
181+
ensure_profile
182+
183+
local TMPOUT
184+
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.XXXXXX.tmp")
185+
trap 'rm -f "$TMPOUT"' EXIT
186+
187+
PROFILE_FILE_PATH="$PROFILE_FILE" RECORD_INPUT="$INPUT" TMPOUT_PATH="$TMPOUT" bun -e "
188+
const fs = require('fs');
189+
const entry = JSON.parse(process.env.RECORD_INPUT);
190+
if (!entry.ts) entry.ts = new Date().toISOString();
191+
192+
const profile = JSON.parse(fs.readFileSync(process.env.PROFILE_FILE_PATH, 'utf-8'));
193+
profile.sessions = profile.sessions || [];
194+
profile.sessions.push(entry);
195+
196+
profile.signals_accumulated = profile.signals_accumulated || {};
197+
for (const s of (entry.signals || [])) {
198+
profile.signals_accumulated[s] = (profile.signals_accumulated[s] || 0) + 1;
199+
}
200+
201+
profile.resources_shown = profile.resources_shown || [];
202+
const resSet = new Set(profile.resources_shown);
203+
for (const r of (entry.resources_shown || [])) resSet.add(r);
204+
profile.resources_shown = Array.from(resSet);
205+
206+
profile.topics = profile.topics || [];
207+
const topicSet = new Set(profile.topics);
208+
for (const t of (entry.topics || [])) topicSet.add(t);
209+
profile.topics = Array.from(topicSet);
210+
211+
fs.writeFileSync(process.env.TMPOUT_PATH, JSON.stringify(profile, null, 2));
212+
"
213+
214+
mv "$TMPOUT" "$PROFILE_FILE"
215+
trap - EXIT
216+
"$SCRIPT_DIR/gstack-brain-enqueue" "developer-profile.json" 2>/dev/null &
217+
}
218+
157219
# -----------------------------------------------------------------------
158220
# Read: emit legacy KEY: VALUE output for /office-hours compat.
159221
# -----------------------------------------------------------------------
@@ -168,14 +230,19 @@ do_read() {
168230
else if (count >= 4) tier = 'regular';
169231
else if (count >= 1) tier = 'welcome_back';
170232
171-
const last = sessions[count - 1] || {};
172-
const prev = sessions[count - 2] || {};
233+
// LAST_* / CROSS_PROJECT must reflect real sessions, not resource-tracking
234+
// events (the Phase 6 auto-append). Without this filter, a session's
235+
// resources entry written immediately after the real session would clobber
236+
// LAST_PROJECT/LAST_ASSIGNMENT/LAST_DESIGN_TITLE.
237+
const realSessions = sessions.filter(e => e.mode !== 'resources');
238+
const last = realSessions[realSessions.length - 1] || {};
239+
const prev = realSessions[realSessions.length - 2] || {};
173240
const crossProject = prev.project_slug && last.project_slug
174241
? prev.project_slug !== last.project_slug
175242
: false;
176243
177-
const designs = sessions.map(e => e.design_doc || '').filter(Boolean);
178-
const designTitles = sessions
244+
const designs = realSessions.map(e => e.design_doc || '').filter(Boolean);
245+
const designTitles = realSessions
179246
.map(e => (e.design_doc ? (e.project_slug || 'unknown') : ''))
180247
.filter(Boolean);
181248
@@ -441,6 +508,7 @@ case "$CMD" in
441508
--vibe) do_vibe ;;
442509
--check-mismatch) do_check_mismatch ;;
443510
--migrate) do_migrate ;;
511+
--log-session) do_log_session "$@" ;;
444512
--help|-h) sed -n '1,/^set -euo/p' "$0" | sed 's|^# \?||' ;;
445513
*)
446514
echo "gstack-developer-profile: unknown subcommand '$CMD'" >&2

bin/gstack-diff-scope

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ while IFS= read -r f; do
5757
*.md) DOCS=true ;;
5858

5959
# Config
60-
package.json|package-lock.json|yarn.lock|bun.lockb) CONFIG=true ;;
60+
package.json|package-lock.json|yarn.lock|bun.lock|bun.lockb) CONFIG=true ;;
6161
Gemfile|Gemfile.lock) CONFIG=true ;;
6262
*.yml|*.yaml) CONFIG=true ;;
6363
.github/*) CONFIG=true ;;

0 commit comments

Comments
 (0)