Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .claude/commands/issues-ls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
description: "List open GitHub issues for this repo, if it's hosted on GitHub. Read-only, no side effects."
argument-hint: "[--state open|closed|all] [gh issue list flags...]"
---

# /issues-ls — list GitHub issues for this repo

Read-only. Lists issues from GitHub if (and only if) this repo's remote is
a GitHub repo — no writes, no approval gate needed.

## Steps
1. **Check the remote is GitHub.** Run `git remote get-url origin` (fall
back to another remote if `origin` doesn't exist). If it doesn't
resolve, or the host isn't `github.com`, stop and report "not a GitHub
repo — skip" — not an error, just nothing to do.
2. **Check `gh` CLI is available and authenticated.** Run `gh auth
status`. If `gh` isn't installed or isn't authenticated, stop and
report the exact output plus a one-line hint (`gh auth login`) — don't
work around it (no calling the GitHub REST API directly with a token).
3. **List issues.** `gh issue list --state open --limit 50` by default.
If `$ARGUMENTS` is given, pass it through verbatim as extra flags to
`gh issue list` instead of the defaults (e.g. `/issues-ls --state all`,
`/issues-ls --label bug --assignee @me`).
4. **Display as a table**: issue number, title, labels, state,
updated-at, URL — whatever `gh issue list` returns is enough, don't
reformat or re-fetch per-issue unless the arguments ask for more detail
(e.g. a `--json` variant).
5. **No writes.** Never close/comment/edit an issue from this command —
that's a separate manual `gh issue` call (or `/release`'s own
issue-closing step), out of scope here.

## Runtime
Requires `gh` CLI authenticated against the project's GitHub remote. If
the repo isn't on GitHub, or `gh` isn't set up, report why and stop — no
fallback to scraping or an unauthenticated API call.
105 changes: 81 additions & 24 deletions .claude/skills/hub-tokens/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
---
name: hub-tokens
description: "Report the token cost of agent-hub/ — how much gets read every worker session (recurring cost) vs cold storage (evidence/, archived diagram rows) that's only opened on demand. Usage: /hub-tokens. Read-only, makes no changes."
description: "Report the token cost of agent-hub/ — how much gets read every worker session (recurring cost) vs cold storage (evidence/, archived diagram/PROJECT.md/log rows) that's only opened on demand. Usage: /hub-tokens. Read-only, makes no changes."
---

# /hub-tokens — measure agent-hub's token cost

Read-only diagnostic. No file changes, no seal gate needed.

## Why this exists
`haven/diagrams/dev-loop.prime-mermaid.md` is read in full by every worker
session (implementer, verifier, and every subagent spawned for a `/todo`
verify pass re-loads it from scratch). Left unchecked it grows forever and
becomes the single biggest recurring token cost in the hub — this is what
the `dev-loop-archive.md` convention (see the diagram file's own header
note) exists to bound. This command measures whether that's actually
happening, instead of guessing.
`haven/diagrams/dev-loop.prime-mermaid.md` and `doctrine/domains/
PROJECT.md` are both read in full by every worker session (implementer,
verifier, and every subagent spawned for a `/todo` verify pass re-loads
them from scratch). Left unchecked either grows forever and becomes the
single biggest recurring token cost in the hub — this is what the
`dev-loop-archive.md` / `PROJECT-archive.md` conventions (see each file's
own header note) exist to bound. This command measures whether that's
actually happening, instead of guessing.

There's no exact tokenizer available here — the report uses `bytes / 4` as
a documented, consistent proxy (not a real token count). Good enough to
Expand All @@ -32,12 +33,23 @@ HUB="$ROOT/agent-hub"
bytes_glob() { find $1 -maxdepth "${2:-99}" -type f \( -name "*.md" -o -name "*.yaml" -o -name "*.yml" \) 2>/dev/null -exec cat {} + 2>/dev/null | wc -c | tr -d ' '; }
bytes_glob_exclude() { find "$1" -type f \( -name "*.md" -o -name "*.yaml" -o -name "*.yml" \) ! -iname "*archive*" 2>/dev/null -exec cat {} + 2>/dev/null | wc -c | tr -d ' '; }
row() { local label="$1" b="$2"; local t=$(( b / 4 )); printf " %-40s %9d B ~%8d tok\n" "$label" "$b" "$t"; }
check_threshold() {
local file="$1" threshold="$2" hint="$3"
[ -f "$file" ] || return
local b; b=$(wc -c < "$file" | tr -d ' ')
local kb=$(( threshold / 1024 ))
if [ "$b" -gt "$threshold" ]; then
echo " ⚠ $(basename "$file") is ${b}B (>${kb}KB threshold) — $hint"
else
echo " ✓ $(basename "$file") is ${b}B, under the ${kb}KB threshold"
fi
}

echo "agent-hub token report — $(date +%Y-%m-%d) [$ROOT]"
echo "==================================================================="
echo "READ EVERY WORKER SESSION (this is the recurring cost):"
ROOT_B=$(bytes_glob "$HUB" 1)
DOCTRINE_B=$(bytes_glob "$HUB/doctrine")
DOCTRINE_B=$(bytes_glob_exclude "$HUB/doctrine")
DIAG_ACTIVE_B=$(bytes_glob_exclude "$HUB/haven/diagrams")
IMPL_B=$(bytes_glob "$HUB/haven/workers/implementer")
VERIF_B=$(bytes_glob "$HUB/haven/workers/verifier")
Expand All @@ -52,14 +64,18 @@ echo
echo "COLD STORAGE (opened on demand only, NOT re-read wholesale by"
echo "pick_next/verify_seal — large size here is not a recurring cost):"
ARCHIVE_B=$(find "$HUB/haven/diagrams" -type f -iname "*archive*" 2>/dev/null -exec cat {} + 2>/dev/null | wc -c | tr -d ' ')
DOCTRINE_ARCHIVE_B=$(find "$HUB/doctrine" -type f -iname "*archive*" 2>/dev/null -exec cat {} + 2>/dev/null | wc -c | tr -d ' ')
EVI_I_B=$(bytes_glob "$HUB/evidence/implementer")
EVI_V_B=$(bytes_glob "$HUB/evidence/verifier")
TODO_LOG_B=$(wc -c < "$HUB/evidence/worker-runs.log" 2>/dev/null | tr -d ' '); TODO_LOG_B=${TODO_LOG_B:-0}
TODO_LOG_B=$([ -f "$HUB/evidence/worker-runs.log" ] && wc -c < "$HUB/evidence/worker-runs.log" | tr -d ' ' || echo 0)
TODO_LOG_ARCHIVE_B=$([ -f "$HUB/evidence/worker-runs-archive.log" ] && wc -c < "$HUB/evidence/worker-runs-archive.log" | tr -d ' ' || echo 0)
row "haven/diagrams/*archive*" "$ARCHIVE_B"
row "doctrine/**/*archive*" "$DOCTRINE_ARCHIVE_B"
row "evidence/implementer/" "$EVI_I_B"
row "evidence/verifier/" "$EVI_V_B"
row "evidence/worker-runs.log" "$TODO_LOG_B"
COLD_B=$(( ARCHIVE_B + EVI_I_B + EVI_V_B + TODO_LOG_B ))
row "evidence/worker-runs-archive.log" "$TODO_LOG_ARCHIVE_B"
COLD_B=$(( ARCHIVE_B + DOCTRINE_ARCHIVE_B + EVI_I_B + EVI_V_B + TODO_LOG_B + TODO_LOG_ARCHIVE_B ))
row "= cold storage total" "$COLD_B"
echo
TOTAL_B=$(( SESSION_B + COLD_B ))
Expand All @@ -76,33 +92,74 @@ if [ -f "$DIAG_FILE" ]; then
POINTER_SEALED=$(grep -cE '— archived, see' "$DIAG_FILE" 2>/dev/null || echo 0)
REAL_SEALED=$(( FULL_SEALED - POINTER_SEALED ))
if [ "$DB" -gt 15360 ]; then
echo " ⚠ dev-loop.prime-mermaid.md is ${DB}B (>15KB threshold), $REAL_SEALED full SEALED entries not yet archived — consider moving nodes older than the current work session to haven/diagrams/dev-loop-archive.md"
echo " ⚠ dev-loop.prime-mermaid.md is ${DB}B (>15KB threshold), $REAL_SEALED full SEALED entries not yet archived."
echo " Ready-to-move rows (copy each VERBATIM into dev-loop-archive.md's"
echo " PM status table, then replace it here with a compact pointer row"
echo " '| node | state | date — archived, see dev-loop-archive.md. Evidence: ... |'):"
grep -E '\| SEALED \|' "$DIAG_FILE" 2>/dev/null | grep -vE '— archived, see' | sed 's/^/ /'
else
echo " ✓ dev-loop.prime-mermaid.md is ${DB}B, under the 15KB threshold ($REAL_SEALED full SEALED entries, $POINTER_SEALED archived pointers)"
fi
fi
check_threshold "$HUB/doctrine/domains/PROJECT.md" 15360 \
"consider moving Traps/Decisions rows older than the current work session to doctrine/domains/PROJECT-archive.md"
check_threshold "$HUB/evidence/worker-runs.log" 15360 \
"consider moving lines older than the current work session to evidence/worker-runs-archive.log (see evidence/README.md's archiving convention)"
echo
echo " Static reference files (should stay small by design — no accumulating"
echo " list to archive; growth here likely means misplaced content, not a"
echo " normal archive candidate):"
for f in "$HUB/doctrine/MEMORY.md" "$HUB/doctrine/SOUL.md" "$HUB/doctrine/INDEX.md" \
"$HUB/doctrine/standards/edit-verification.md" "$HUB/doctrine/standards/recipes.md"; do
check_threshold "$f" 8192 \
"unexpected growth for a static file — check for a Correction that belongs in the worker's own MEMORY.md, or a Decision that belongs in PROJECT.md, before creating a dedicated archive file for this one"
done
```

2. Report the output verbatim — don't paraphrase the numbers into prose,
the table is already the report.
3. If the flag fires (active diagram over 15KB), that's a real signal to
do an archive pass (see `haven/diagrams/dev-loop-archive.md`'s own
convention note, or the equivalent section in
`haven/diagrams/dev-loop.prime-mermaid.md`'s PM-status header) — but
this command itself never edits anything. Archiving is a separate,
explicit action.
3. If a flag fires on `dev-loop.prime-mermaid.md`, `PROJECT.md`, or
`worker-runs.log`, that's a real signal to do an archive pass (see
`dev-loop-archive.md` / `PROJECT-archive.md` / `worker-runs-archive.log`'s
own convention notes, or the equivalent header sections in the active
files) — but this command itself never edits anything. Archiving is a
separate, explicit action. [amended 2026-09-02] The diagram flag prints
the exact rows to move (not just "consider moving nodes") — copy-paste
is the whole remaining effort, so there's no excuse to defer it past the
current session the way a vague warning invites.
4. [added 2026-09-05] If a flag fires on one of the 5 static reference
files (`MEMORY.md`, `SOUL.md`, `INDEX.md`, `standards/*.md`), that's
NOT an archive signal — those files have no accumulating list and no
defined archive destination by design. Treat it as an anomaly: read the
file, find what's misplaced (a Correction that belongs in the worker's
own `MEMORY.md`, a Decision that belongs in `PROJECT.md`, a recipe that
belongs in `haven/workers/<wid>/recipes/`), and move it to its one
correct home instead of inventing a new archive file for a file that
was never meant to grow.

## If this hub uses epic sharding [added 2026-09-02]
If `haven/diagrams/index.md` exists (opt-in, see
`kit/agent-hub-templates.md` §9️⃣.3), `DIAG_ACTIVE_B` above sums bytes
across **every** `dev-loop-<epic>.prime-mermaid.md`, not just the one(s)
marked `active: true`. Treat "haven/diagrams/ (active file only)" as an
**upper bound** in that case, not the real per-session cost — `/boot` and
`pick_next` only read the active epic file(s) + `index.md`, per
`boot.md` step 5. This script doesn't parse `index.md`'s `active` column
(keeping it a plain byte-counting script, not a markdown-table parser) —
if you need the real per-session number under sharding, sum
`index.md` + only the active epic file(s) by hand.

## What the numbers mean
- **Recurring per-session cost** — what a fresh implementer or verifier
worker reads before touching any code. This is the number that actually
compounds: every subagent spawned for a verify pass pays it again, from
zero, with no cache reuse across separate agent contexts.
- **Cold storage** — `evidence/` and archived diagram rows. Large here is
normal and not itself a problem: `/boot` and `pick_next` only touch a
handful of the most recent evidence notes, not the whole directory. Only
worth worrying about if something starts reading it in bulk (e.g. a
recipe that globs all of `evidence/` instead of the specific notes it
needs).
- **Cold storage** — `evidence/` and archived rows from the diagram,
`PROJECT.md`, and `worker-runs.log`. Large here is normal and not itself
a problem: `/boot` and `pick_next` only touch a handful of the most
recent evidence notes, not the whole directory. Only worth worrying
about if something starts reading it in bulk (e.g. a recipe that globs
all of `evidence/` instead of the specific notes it needs).

## Runtime
`/hub-tokens`. Read-only — no seal gate, no evidence note, no worker
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 2026-09-05 — add-logout-all-sessions (plan + diff)

- Worker: implementer
- Version: 0.1.0
- Node: `add-logout-all-sessions` (`haven/diagrams/dev-loop.prime-mermaid.md`)
- Task (verbatim): `/todo "#74"` — GitHub issue #74, "Log out of all
devices" — revoke all active sessions. Full issue body (problem +
proposal) passed through as the task text.

## Hub bytes before: 49747

## Investigation (before touching code)
`pick_next` found no PENDING node for this task on the diagram. Per
`NodeBeforeCode`, before drafting a fresh node, grepped `src/` for any
prior work matching the issue's own vocabulary
(`tokenVersion`/`sessionsInvalidatedAt`/`logout-all`) to avoid duplicating
existing work:

```
grep -rn "logout-all\|logoutAll\|sessionsInvalidatedAt\|tokenVersion" src/ --include="*.ts"
```

Result: the feature is **already fully implemented and merged** —
`git log --oneline` shows `03bcb66 feat(auth): add logout-all endpoint to
revoke all sessions (#74)`, merged via `4cde424 Merge pull request #105
from datvt243/feat/issue-74-logout-all-sessions` (2026-09-02), currently
on `staging` (`git status`: clean, up to date with `origin/staging`). No
diagram node or evidence note exists for it — a bookkeeping gap from
whatever session did that work, not a fresh implementation task. Verified
`git log -1 --format=%B 03bcb66` includes `Closes #74` — the GitHub issue
is still OPEN only because the merge landed on `staging`, not `main`
(auto-close fires on default-branch merge only, per
`doctrine/domains/PROJECT.md`'s recorded 2026-08-30 release-workflow
decision) — not a sign the work is incomplete.

## Diff
No new `src/` changes — the implementation predates this session. Files
already in place (read, not modified, this pass):

| File | Role |
|---|---|
| `src/utils/sessionRevocation.ts` | `invalidateAllSessions` / `getSessionsInvalidatedAt` / `isSessionRevoked` — Redis-with-in-memory-fallback "invalidated-before" timestamp, same shape as `tokenBlacklist.ts` |
| `src/middlewares/verifyToken.middleware.ts` | Rejects any token whose `iat` predates the candidate's last logout-all (`TokenRevokedError`) |
| `src/auth/auth.controller.ts` | `authLogoutAll` handler (calls `invalidateAllSessions`); `authRefreshToken` also rejects a stale refresh token the same way |
| `src/routers/api/v1/auth.route.ts` | `router.post('/logout-all', verifyToken, authLogoutAll)` + Swagger doc block |
| `src/locales/en.ts`, `src/locales/vi.ts` | `logoutAllSuccess` message, both languages |
| `src/__tests__/middlewares/verifyToken.test.ts`, `src/__tests__/auth/refreshToken.test.ts`, `src/__tests__/auth/auth.controller.test.ts` | Existing test coverage for the revocation check + `authLogoutAll` |

Design note (from the code's own comments, `sessionRevocation.ts:1-18`):
deviates from the issue's `tokenVersion`-on-`Candidate`-model proposal on
purpose — avoids adding a Mongo field + an extra DB lookup per
authenticated request, reusing the existing Redis/mem blacklist pattern
instead. The issue text itself flagged this exact tradeoff as open
("worth measuring... before deciding the final design"), so this counts
as resolving that open question, not diverging from the ask.

## Command
```
npx tsc --noEmit
```
Output: clean, no errors (no stdout).

```
npm run build
```
Output:
```
> resume-nodejs-api@1.2.1 build
> tsc && npm run copy

> resume-nodejs-api@1.2.1 copy
> cp -R ./src/views ./src/public ./dist/
```
Clean, no errors.

```
npm test
```
(from `/Users/_david/Workspace/Project/resume/resume-nodejs-api`, copied
verbatim from `doctrine/MEMORY.md`)

Output (tail):
```
PASS src/__tests__/auth/auth.controller.test.ts
auth.controller
authLogoutAll
✓ invalidates all sessions for the authenticated candidate
✓ fails when there is no authenticated user on the request (1 ms)

Test Suites: 13 passed, 13 total
Tests: 77 passed, 77 total
Snapshots: 0 total
Time: 5.149 s
Ran all test suites.
```
Full relevant section also includes (same run):
```
logout-all (issue #74)
✓ calls next with TokenRevokedError when token was issued before the last logout-all (1 ms)
✓ calls next and attaches req.user when token was issued after the last logout-all
✓ calls next with TokenRevokedError when a logout-all is in effect but the token has no iat
```
(from `src/__tests__/middlewares/verifyToken.test.ts`)

## Acceptance
| Criterion (from issue #74) | Evidence |
|---|---|
| A way to invalidate every outstanding token at once, not just the current one | `POST /api/v1/auth/logout-all` route registered (`auth.route.ts`), `authLogoutAll` controller calls `invalidateAllSessions(candidateId)` |
| Every previously-issued token instantly invalid, without enumerating/blacklisting each one | `sessionRevocation.ts` stores one per-candidate "invalidated-before" timestamp; `isSessionRevoked` compares every token's `iat` against it — O(1) regardless of how many tokens were issued |
| Checked on every authenticated request | `verifyToken.middleware.ts` calls `getSessionsInvalidatedAt` + `isSessionRevoked` before attaching `req.user`, same `TokenRevokedError` used for blacklisted tokens |
| Refresh path also covered (stolen long-lived refresh token) | `authRefreshToken` (`auth.controller.ts`) runs the identical check before minting a new pair — test: `'returns 403 when the refresh token predates the last logout-all (issue #74)'` in `refreshToken.test.ts` |
| Design tradeoff (extra Mongo lookup vs. cached read) actually decided, not left open | Redis/mem lookup chosen (matches existing blacklist check already on every request) — 0 new Mongo round trips, documented in the commit message and file header |
| `npm test` passes | See Command/Output above — 77/77, 13/13 suites |
| `npx tsc --noEmit` clean | See Command/Output above |
| `npm run build` clean | See Command/Output above |

## Noticed, not done
- GitHub issue #74 is still shown OPEN by `gh issue list` — expected per
the `staging`→`main` release workflow (auto-close needs a `main`
merge), not a defect in this node. Will self-resolve on the next
`/release`, or can be closed manually by the operator now if desired —
out of scope for `/todo` to close issues itself.
- This node is a documentation/evidence backfill, not new code — flagging
for the verifier that "diff" here means "confirmed pre-existing,"
matching `NodeBeforeCode`'s intent (a node must exist and be traceable)
even though the code came first in real history.

## Seal gate
No outward-facing action taken this pass — no `commit`/`push` (nothing to
commit; only `agent-hub/` was written, which is not outward-facing per
`CLAUDE.md`). The `src/` code itself was already committed and merged in
a prior, separate session (PR #105) — that seal-gate approval, if any,
predates this note and is not re-litigated here.
Loading
Loading