Skip to content

Goal-bound agent sessions for Claude Code and Codex (behind ALIGN_GOAL_SESSIONS=1) - #8

Merged
allsmog merged 8 commits into
mainfrom
agent/align-v2
Oct 23, 2026
Merged

allsmog merged 8 commits into
mainfrom
agent/align-v2

Conversation

@allsmog

@allsmog allsmog commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What this is

The redesign after the CPSec test on 10-22. It replaces passcodes and fixed team slots with people who sign in as themselves, and connects their own Claude Code and Codex sessions to team goals. Everything is behind ALIGN_GOAL_SESSIONS=1; with it unset every /app* and /api/v2/* path is 404 and v1 and Northstar are unchanged.

  • Server (lib/v2, app/api/v2, app/app): Better Auth 1.7.7 sign-in (Google, GitHub; email and password only with ALIGN_GOAL_SESSIONS_TEST_IDP=1 on a local server), several workspaces per person, pods with lead/member roles, versioned goals (private, pod or workspace visible), device pairing with key-bound rotating tokens, shared sessions with binding epochs, directives by cursor with three receipt states, alignment requests with computed approvers, spend per goal. Our own authorization module; a workspace owner cannot read someone's private goal.
  • Local agent (packages/align-agent): the align CLI and hooks for Claude Code and Codex (SessionStart, UserPromptSubmit, PreToolUse, Stop). No daemon, no telemetry settings. Usage comes from the harnesses' own transcripts and rollouts, on this machine. Hooks answer from local files and start a short one-pass align sync that exits; the Stop hook carries each turn's usage up.
  • Sharing: align share --dir <folder> --goal <goal> shares every new session started in that folder (the folder path stays local; this also covers /clear). Sessions and folders connect to goals by goal id. Per-session share, bind and unshare stay available, in the CLI and in-session as ::align ... lines that never reach the model.
  • What the agent sees: the goal once at session start (about 600 characters), a short diff when it changes (under 300, at the next prompt or tool call), nothing on unchanged prompts. A stop from the web blocks the next tool call, fail closed.
  • Wire contract: docs/v2/WIRE.md. Harness facts with sources: packages/align-agent/HARNESS_FACTS.md.

Verified

On 4266780, rebased onto main 4dbf876 (all of #10, #11, #13, #14, #15 and #16):

  • scripts/test-all.sh 3512: all 18 steps pass, including test-v2 145/145, test-tenancy 175/175, test-ui-loop 41/41, snapshot and snapshot-v2on identical to 850971e, ui-northstar identical to the v6 baseline (125 files). Run on the tree before the CI: rebased-on-main check for main's branch protection #16 rebase, which only added .github/workflows/rebased-on-main.yml.
  • packages/align-agent: npm test 50/50 (load average about 2).
  • The rebase onto main was clean, and range-diff shows the 7 earlier commits unchanged.

What the last commit changes:

  • Hosted cookie writes (lib/v2/http.ts requireSameSite) now follow main's LIM-2437 rule: Origin is required and must be exactly https://<Host>. An http:// page, another port, null or no Origin gets 403 cross_site. The local rule is unchanged. The site check now runs before the session lookup. test-v2 has a new hosted check for all five refusals and the exact Origin.
  • The flag is renamed: ALIGN_V2 is now ALIGN_GOAL_SESSIONS, and ALIGN_V2_TEST_IDP is now ALIGN_GOAL_SESSIONS_TEST_IDP.

Earlier, on 10-22 (before the rebase):

  • scripts/test-all.sh 3512 on 8405c8f (rebased on LIM-2424: custom record, launch decision, Switch team, Halden fixture (stacked on #6) #7 at 260a252): all 17 steps pass, including test-v2 133/133, test-tenancy 166/166, snapshot and ui-northstar identical to 850971e with v2 off and on.
  • packages/align-agent: npm test 35/35 twice on 8405c8f. On 07e5cf6 (device secret fix, plus a new test) 35/36 while the machine was at load average 14; the one failure is the hook-latency test, which passed alone at normal load and does not touch the changed file. To rerun at normal load.
  • Pre-push liminal-review: one finding (f_2c5d0c1c5473, the device secret was regenerated on any read error), fixed in 07e5cf6 and recorded as accepted.
  • Live run in tmux on 10-22 with real Claude Code 2.1.309 and real Codex 0.163.0 against a local v2 server, using sandboxed --settings and CODEX_HOME (the real ~/.claude and ~/.codex checksums were unchanged):
    • folder rules shared a Claude session and a Codex session to one goal; both agents stated the goal and its measures with no prompt about it;
    • a goal edit on the web (VLM cut, 16 GB budget) reached both agents in the same turn; both rewrote PLAN.md to the new scope; Claude took it at the ls tool call;
    • each turn's usage reached the server with no further prompt (Claude 4/4, Codex 5/5);
    • an approved "work toward" moved only the Codex session to the other person's goal (new epoch at its next prompt); spend split at that point;
    • a stop issued while Codex was idle blocked its next tool call (touch denied, file not created), receipts received and enforced within 1 s; resume worked;
    • prompt cache: Claude's cache reads kept growing across the injection (56,090 to 56,640 tokens; 226 tokens written for the turn).

Known gaps

  • No Linear issue yet; commits have no LIM- id.
  • Goal changes made while a session is idle land one prompt late when that prompt uses no tools (the sync runs at that prompt); with tools they land in the same turn.
  • Real Google and GitHub OAuth not exercised; hosted signup is open to any account (rate limited).
  • claude-sonnet-5-5 and claude-haiku-5-5 are not in the price table, so they show as unpriced.
  • When an ::align line is blocked, Claude Code puts the text back in the input box.
  • A transport benchmark (no-Align baseline vs this vs a resident process vs a per-session MCP server vs a Claude channel) is in progress and will be posted here.

@liminalshruti

Copy link
Copy Markdown
Collaborator

Review: PR #8 @ 07e5cf6eb37eab63421e3c1b4fb90815c65f5f78 (Align v2, behind ALIGN_V2=1)

Automated code review, high effort, read-only. Checked = read in code at this head. Unchecked = reviewer finding not yet confirmed. Note: this PR's base is an old head of #7 (28 commits behind a54570f); rebase after #7.

High

  1. Partly checked. Privacy: usage from a stretch the user unshared can upload to a different workspace. packages/align-agent/src/sync.ts:276 uses epochFor() (ledger.ts:~268): SELECT * FROM epochs WHERE local_id=? AND starts_at<=? ORDER BY starts_at DESC LIMIT 1, with no ends_at or stopped check. The withhold rule covers suspended_at only. Sequence: share to A → ::align unshare (UI says usage "stays on this machine") → share to B. Rows from the private stretch map to A's synced epoch and are POSTed to B's /sessions/<key>/usage. B counts them as conflicts, and the client marks them uploaded, so they are also lost.
  2. Unchecked. share.ts:151: local stopSharing runs DELETE FROM latches, so ::align unshare clears a stop a workspace owner set. Afterwards setAgentStop returns 409 sharing_stopped and the owner can't re-stop.
  3. Unchecked. cli.ts:193: cmdLogin overwrites device.key before pairing succeeds. An aborted re-login (Ctrl-C, expired code or denial) leaves credentials.json with the old device_id and the new key, so the next refresh gets 401 invalid_grant and the device is signed out.
  4. Unchecked. sync.ts:354: a 400 or 413 on a usage batch breaks with no mark and no log. One bad row (a model string over 100 characters, a long price_table_version, a ts the strict ISO regex rejects) blocks that session's upload on every later pass.

Medium

  1. lib/v2/sessions.ts:126: the openBindingEpoch idempotency lookup runs outside tx(), so a concurrent or retried identical POST returns a PK-violation 500 instead of the existing epoch.
  2. lib/v2/devices.ts:174: refresh-reuse detection revokes the device when the client simply never received the rotated token (15 s client abort or a dropped connection). Consider a short grace window or replaying the last rotation.
  3. packages/align-agent/src/hooks/handlers.ts:397: /\s*(confirm|--yes|-y)$/i has no word boundary. ::align share Reconfirm counts as confirmed with arg "Re", then matches a different goal by prefix and skips the preview.
  4. lib/v2/devices.ts:71: stale cleanup excludes approved, so approved-but-unclaimed requests (user_id, ws_ids_json) are kept forever.

Cleanup

  1. lib/v2/http.ts:129: readJson is a near copy of lib/http.ts cappedJson (its rejectReserved drops the field path). Call cappedJson and add the 415 check.
  2. lib/v2/alignments.ts:370: listAlignments runs about 10 or more synchronous queries per alignment. Filter and batch in SQL.

@liminalshruti

Copy link
Copy Markdown
Collaborator

@allsmog: re-check at the new head 4ced1d7649aa3aac9bd2a7e2e9ac4f5d32e91771 (your 2 commits pushed after my review: Sonnet and Haiku 5.5 prices; plugin install, subagent-safe deltas, StopFailure sync). Neither commit touches the findings in my review above, and all five High findings are still present, each read in the code at this head:

  1. Privacy: ledger.ts:268 epochFor still picks starts_at<=? ORDER BY starts_at DESC LIMIT 1 with no stopped or ends_at check. Share A → unshare → share B uploads the private stretch to B.
  2. share.ts:151 and :163 still run DELETE FROM latches WHERE local_id=? on local unshare, clearing an owner's stop.
  3. cli.ts cmdLogin still calls generateDeviceKey() (which writes device.key via writeFileAtomic) before pairing succeeds.
  4. sync.ts usage upload: on a 400 or 413 the loop breaks with no log (only 403/404/409 log) and no row is marked, so one bad row blocks the session forever.
  5. hooks/handlers.ts:438: /\s*(confirm|--yes|-y)$/i still has no word boundary.

I didn't re-check the four Medium findings (5, 6, 8, 9); their files weren't touched.

Base: this PR still targets agent/LIM-2424-v6-custom-record. That's #7's branch, merged to main at 05:11Z (main = 696c6b3e0feb4f180e09d4fc6299824965cc6ef2). Please retarget to main and rebase; it's 30 commits behind main.

@allsmog allsmog changed the title Align v2: goal-bound agent sessions behind ALIGN_V2=1 (stacked on #7) Goal-bound agent sessions for Claude Code and Codex (behind ALIGN_V2=1) Oct 23, 2026
@allsmog
allsmog changed the base branch from agent/LIM-2424-v6-custom-record to main October 23, 2026 16:46
@allsmog allsmog changed the title Goal-bound agent sessions for Claude Code and Codex (behind ALIGN_V2=1) Goal-bound agent sessions for Claude Code and Codex (behind ALIGN_GOAL_SESSIONS=1) Oct 23, 2026
People sign in as themselves (Google or GitHub; a test sign-in for local use),
belong to several workspaces, and work in pods with lead/member roles. Goals
are versioned and private, pod or workspace visible.

The local agent (packages/align-agent) meters Claude Code and Codex from their
own transcripts and rollouts. A user shares a session, or a whole folder
(align share --dir), and each shared session is bound to a goal by binding
epochs that change only at a prompt boundary. Hooks give the agent the goal
once, a short diff when it changes (at the next prompt or tool call), and
nothing on unchanged prompts. A stop from the web blocks the next tool call,
fail closed. There is no daemon and no telemetry setup: hooks start a short
one-pass sync that exits, and the Stop hook carries each turn's usage up.

Aligning a session to someone else's goal needs that owner's approval; an
approved "work toward" moves future usage and the session's directives to the
target goal, and revoking suspends the binding. Spend is reported per goal,
labelled as list-price estimates, with no per-person view.

v1 and Northstar are unchanged: every v2 route is 404 unless ALIGN_V2=1, and
the snapshot gate runs with v2 off and on. Wire contract: docs/v2/WIRE.md.
deviceSecret wrote a fresh secret over the old one on any read error or a file
of the wrong length, which would silently change every session, repo and
request key and orphan sessions the device already shared. It now creates the
secret only when the file does not exist (exclusive create, so two first-run
processes cannot write different secrets) and otherwise refuses with a clear
message. Found by the pre-push review (f_2c5d0c1c5473).
Adds list-price rows for claude-sonnet-5-5 and claude-haiku-5-5. Haiku 5.5 has
two rate cards: prompts above 100K tokens (input plus cache read and write)
use the higher card. Price table version align-prices-2026-10-22.2.
…ilure sync

- align install now writes a skills-directory plugin for Claude Code
  (CLAUDE_CONFIG_DIR/skills/align) instead of merging hooks into
  settings.json. Claude Code loads it in every new session; `claude plugin
  disable align@skills-dir` turns it off; uninstall deletes the folder.
  Handlers an earlier Align merged into settings.json are removed (with a
  backup) so they do not run twice. Install refuses to replace a skills/align
  folder that is not Align's. Codex keeps the hooks.json merge.
- PreToolUse fires inside subagents too, and its context reaches only the
  subagent (checked with Claude Code 2.1.309). The mid-turn delta now skips
  calls with agent_id so the main agent still receives the goal change; the
  stop latch still applies in subagents.
- Parallel tool calls run their PreToolUse hooks at once. A lock directory
  lets exactly one of them deliver the delta.
- Claude Code runs StopFailure instead of Stop when a turn ends on an API
  error. It is now installed and starts the same end-of-turn sync, so that
  turn's usage uploads without waiting for the next prompt.

Agent suite 40/40; test-all 17/17 (Northstar identical with v2 off and on).
Two live tmux runs with hooks loaded only from the plugin: goal change
delivered 2/2, stop held 2/2, +0.6% prompt tokens vs no Align.
Server
- Approved alignments with a source goal make it a child (contributes_to).
  Children record the parent version they last reviewed and show as stale
  when the parent moves on; POST /goals/:goal/review confirms the fit.
- depends_on is a versioned goal field (at most 5).
- Activity feed: POST/GET /workspaces/:ws/activity. Edits, commits, task
  labels and claims, kept 24 h (claims 1 h), visible to readers of the
  session's goal. Paths and text only with work-signal consent; otherwise
  hashed repo and path ids.
- Goal page: tree card with stale pill and "Still fits", depends-on picker.

Agent
- Hooks log edits (including shell writes), reads, commit subjects and
  ::align task / ::align claim. The sync pass also detects git working-tree
  changes and new commits, uploads activity and fetches the pod feed.
- A card of at most 300 chars at prompt or session start, at most once per
  10 minutes unless work overlaps. A one-time hold for same-laptop edits to
  a file another session just changed; a note for cross-person overlap or
  claims. align pod and ::align pod show the detail.
- Parent and dependency changes arrive as one note naming the changed
  measure.

Tests: agent 49/49, test-v2 144/144, test-all 17/17.
The pod card and cross-person notes put text from other machines into
agent context: names, task labels, commit subjects, claim reasons and
paths. Anyone in the pod could use those fields to steer another
person's agent.

- From other people the card and notes now carry only a first name
  (letters only) and paths made of path characters; overlap and note
  files use this session's own path for the file.
- Their task labels, commit subjects and claim reasons appear only in
  `align pod`, which marks quoted text as information, not instructions.
- This machine's own sessions are unchanged.
- The envelope invariant comment and README say what reaches the model.

Tests: agent 50/50 (new injection test), test-all 17/17.
A summary from another machine with a blank account name was treated as
one of this machine's sessions, so its task label and commit subject
reached model context. Remote summaries now carry remote: true and the
card, the pod report and the quoted-text marker check that flag.

Tests: agent 50/50 (blank-name case added), test-all 17/17.
…>; rename the ALIGN_V2 flags

requireSameSite now matches lib/http.ts (LIM-2437): when hosted, Origin is required and must be exactly https://<Host>, so an http:// page, another port or a missing Origin is refused. The local rule is unchanged. The site check runs before the session lookup, so a cross-site write is refused without reading a session. test-v2 covers none, http, another port, null and foreign (403 cross_site) and the exact Origin (401 without a session).

ALIGN_V2 is now ALIGN_GOAL_SESSIONS and ALIGN_V2_TEST_IDP is ALIGN_GOAL_SESSIONS_TEST_IDP. Comments and docs no longer say Align v2.
@allsmog
allsmog marked this pull request as ready for review October 23, 2026 16:47
@allsmog
allsmog merged commit 9cf81e2 into main Oct 23, 2026
1 check passed
@liminalshruti

Copy link
Copy Markdown
Collaborator

Merged without an approving review; per the founder (Fri ~10 AM PT) it stays merged. The review findings and needed fixes are in LIM-2441 (https://linear.app/liminal-space-io/issue/LIM-2441), for after the Sprint. Six Highs are confirmed in the code on main 9cf81e2: unshared usage can upload to another workspace (ledger.ts:268); unshare clears an owner's stop (share.ts); an aborted re-login signs the device out (cli.ts:196); a 400 or 413 silently blocks uploads (sync.ts:356); a viewer pod lead can change members (policy.ts:148); anyLatch blocks other sessions (hook.ts:39). Three more Highs and the Medium/Low items are in the ticket. ALIGN_GOAL_SESSIONS stays off on any hosted or tester build until every High is fixed with a test and re-reviewed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants