syntaur 0.3.0 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntaur",
3
- "version": "0.3.0",
3
+ "version": "0.3.3",
4
4
  "description": "Project workflow CLI with dashboard, Claude Code plugin, and Codex plugin",
5
5
  "homepage": "https://github.com/prong-horn/syntaur#readme",
6
6
  "repository": {
@@ -5,5 +5,9 @@
5
5
  "name": "Brennen",
6
6
  "email": ""
7
7
  },
8
- "version": "0.1.7"
8
+ "version": "0.1.8",
9
+ "statusLine": {
10
+ "type": "command",
11
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/statusline.sh"
12
+ }
9
13
  }
@@ -191,7 +191,7 @@ Only the assigned agent may write to its own assignment folder.
191
191
  ### Session Tracking
192
192
  | Command | Description |
193
193
  |---------|-------------|
194
- | `syntaur track-session --project M --assignment A --agent N` | Register agent session |
194
+ | `syntaur track-session --project M --assignment A --agent N --session-id <real-id> --transcript-path <path>` | Register agent session. `--session-id` is required and must be the agent runtime's real id (Claude: `~/.claude/sessions/<pid>.json` or SessionStart hook payload; Codex: `payload.id` from `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`). Do not synthesize. |
195
195
 
196
196
  All commands support `--dir <path>` to override the default `~/.syntaur/projects/` directory.
197
197
 
@@ -218,6 +218,7 @@ plugin/
218
218
  track-session/track-session.md # Register tmux sessions
219
219
  hooks/
220
220
  hooks.json # Hook definitions
221
+ session-start.sh # Merge real session_id + transcript_path into existing .syntaur/context.json
221
222
  session-cleanup.sh # Mark sessions stopped on exit
222
223
  enforce-boundaries.sh # Write boundary enforcement
223
224
  references/
@@ -241,6 +242,7 @@ plugin/
241
242
  | Hook | Event | Behavior |
242
243
  |------|-------|----------|
243
244
  | PostToolUse: ExitPlanMode | User exits plan mode | Prompts to write the plan to the next unused `plan-v<N>.md` (or `plan.md` if none exists) and append a linked todo in the `## Todos` section of `assignment.md` |
245
+ | SessionStart | Claude Code session starts | Runs session-start.sh to merge the real `session_id` + `transcript_path` into an EXISTING `.syntaur/context.json`. Does nothing if context.json is absent (no active assignment). |
244
246
  | SessionEnd | Claude Code session exits | Runs session-cleanup.sh to mark session as stopped |
245
247
  | PreToolUse: enforce-boundaries | Edit/Write/MultiEdit | Validates target path is within assignment boundaries |
246
248
 
@@ -370,7 +372,7 @@ syntaur dashboard
370
372
 
371
373
  ## Context File (.syntaur/context.json)
372
374
 
373
- Created by `/grab-assignment` in the current working directory. Contains:
375
+ Created by `/grab-assignment` in the current working directory. The SessionStart hook merges `sessionId` / `transcriptPath` into this file on each Claude Code session start — it never creates the file, only enriches an existing one. Contents:
374
376
  ```json
375
377
  {
376
378
  "projectSlug": "my-first-project",
@@ -381,7 +383,8 @@ Created by `/grab-assignment` in the current working directory. Contains:
381
383
  "title": "Design the schema",
382
384
  "branch": "feature/design-the-schema",
383
385
  "grabbedAt": "2026-03-18T14:30:00Z",
384
- "sessionId": "uuid-v4"
386
+ "sessionId": "<real-claude-session-id>",
387
+ "transcriptPath": "/Users/you/.claude/projects/<encoded-cwd>/<session-id>.jsonl"
385
388
  }
386
389
  ```
387
390
 
@@ -11,6 +11,8 @@ arguments:
11
11
 
12
12
  Register the current Claude Code session as an agent session in the Syntaur dashboard. Works standalone or linked to a project/assignment.
13
13
 
14
+ Only real Claude Code session IDs are accepted — no synthesis. The real id is written to `.syntaur/context.json` by the SessionStart hook, with `~/.claude/sessions/<pid>.json` as the fallback source.
15
+
14
16
  ## Usage
15
17
 
16
18
  - `/track-session` — register a standalone session
@@ -29,37 +31,60 @@ Extract optional flags from the argument string:
29
31
  - `--project <slug>` — project to link to
30
32
  - `--assignment <slug>` — assignment to link to
31
33
 
32
- ### Step 2: Run the CLI command
34
+ ### Step 2: Source the real session id + transcript path
35
+
36
+ In priority order:
37
+
38
+ 1. Read `.syntaur/context.json` if present. If it contains `sessionId`, use it. Also pick up `transcriptPath` if present.
39
+ 2. Otherwise, read the most-recently-modified file under `~/.claude/sessions/*.json` whose `cwd` matches `$(pwd)` and use its `sessionId` field. The transcript path is conventionally `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`; include it if the file exists, otherwise omit.
40
+ 3. If neither source yields an id, abort with: "Could not resolve a real Claude Code session id. Restart the Claude session so the SessionStart hook can populate `.syntaur/context.json`, or run `/rename <slug>` then try again."
41
+
42
+ DO NOT generate a UUID. `syntaur track-session` rejects missing session IDs.
33
43
 
34
- Run the track-session CLI command via Bash (use `dangerouslyDisableSandbox: true` since it writes to `~/.syntaur/`):
44
+ ### Step 3: Run the CLI command
45
+
46
+ Run the track-session CLI via Bash (use `dangerouslyDisableSandbox: true` since it writes to `~/.syntaur/`):
35
47
 
36
48
  ```bash
37
- syntaur track-session --agent claude --path $(pwd) [--description "<text>"] [--project <slug>] [--assignment <slug>]
49
+ syntaur track-session \
50
+ --agent claude \
51
+ --session-id "$SESSION_ID" \
52
+ --transcript-path "$TRANSCRIPT_PATH" \
53
+ --path "$(pwd)" \
54
+ [--description "<text>"] \
55
+ [--project <slug>] \
56
+ [--assignment <slug>]
38
57
  ```
39
58
 
40
- ### Step 3: Parse the session ID
59
+ Omit `--transcript-path` entirely (don't pass an empty string) if no transcript path could be resolved.
41
60
 
42
- The CLI output will be one of:
61
+ The CLI prints one of:
43
62
  - `Registered standalone agent session <sessionId>.`
44
63
  - `Registered agent session <sessionId> for <assignment> in <project>.`
45
64
 
46
- Extract the session ID from the output.
65
+ Registration is idempotent — re-running the command with the same session id safely upserts project/assignment/description onto the existing row.
47
66
 
48
- ### Step 4: Write context file
67
+ ### Step 4: Merge context.json
49
68
 
50
- Write the session ID to `.syntaur/context.json` so the SessionEnd hook can mark it stopped when this conversation ends:
69
+ Ensure `.syntaur/context.json` has the session fields (so SessionEnd and future `/track-session` runs find them). Merge, don't overwrite:
51
70
 
52
- - If `.syntaur/context.json` already exists, read it and add `"sessionId": "<id>"` to the existing JSON
53
- - If it doesn't exist, create the `.syntaur/` directory and write:
54
- ```json
55
- {
56
- "sessionId": "<id>"
57
- }
58
- ```
71
+ ```bash
72
+ mkdir -p .syntaur
73
+ if [ -f .syntaur/context.json ]; then
74
+ jq --arg sid "$SESSION_ID" --arg tp "$TRANSCRIPT_PATH" \
75
+ '. + {sessionId: $sid} + (if ($tp | length) > 0 then {transcriptPath: $tp} else {} end)' \
76
+ .syntaur/context.json > .syntaur/context.json.tmp \
77
+ && mv .syntaur/context.json.tmp .syntaur/context.json
78
+ else
79
+ jq -n --arg sid "$SESSION_ID" --arg tp "$TRANSCRIPT_PATH" \
80
+ '{sessionId: $sid} + (if ($tp | length) > 0 then {transcriptPath: $tp} else {} end)' \
81
+ > .syntaur/context.json
82
+ fi
83
+ ```
59
84
 
60
85
  ### Step 5: Confirm
61
86
 
62
87
  Tell the user:
63
- - The session was registered (include the short session ID)
64
- - It will be auto-stopped when this conversation ends
65
- - If linked to a project, mention which project/assignment
88
+ - The session was registered (include the short session id).
89
+ - It will be auto-stopped when this conversation ends via the SessionEnd hook.
90
+ - If linked to a project, mention which project/assignment.
@@ -12,6 +12,17 @@
12
12
  ]
13
13
  }
14
14
  ],
15
+ "SessionStart": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh",
21
+ "timeout": 5
22
+ }
23
+ ]
24
+ }
25
+ ],
15
26
  "SessionEnd": [
16
27
  {
17
28
  "hooks": [
@@ -34,39 +34,29 @@ SESSION_ID=$(jq -r '.sessionId // empty' "$CONTEXT_FILE" 2>/dev/null)
34
34
  MISSION_SLUG=$(jq -r '.projectSlug // empty' "$CONTEXT_FILE" 2>/dev/null)
35
35
  ASSIGNMENT_SLUG=$(jq -r '.assignmentSlug // empty' "$CONTEXT_FILE" 2>/dev/null)
36
36
 
37
- PORT=$(cat "$HOME/.syntaur/dashboard-port" 2>/dev/null || echo "4800")
38
-
39
- # --- Step 5: If no session was registered, try to auto-register (requires project+assignment) ---
37
+ # Fall back to the SessionEnd stdin payload if context.json didn't have the id.
38
+ # Claude Code passes session_id on stdin for SessionEnd.
40
39
  if [ -z "$SESSION_ID" ]; then
41
- # Can only auto-register if we have project and assignment context
42
- if [ -z "$MISSION_SLUG" ] || [ -z "$ASSIGNMENT_SLUG" ]; then
43
- exit 0
44
- fi
45
-
46
- # Generate a session ID for the log entry
47
- SESSION_ID=$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "ses-$(date +%s)")
48
- # Lowercase the UUID (uuidgen on macOS outputs uppercase)
49
- SESSION_ID=$(echo "$SESSION_ID" | tr '[:upper:]' '[:lower:]')
40
+ SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
41
+ fi
50
42
 
51
- RESPONSE=$(curl -sf -X POST "http://localhost:${PORT}/api/agent-sessions" \
52
- -H "Content-Type: application/json" \
53
- -d "{\"projectSlug\": \"${MISSION_SLUG}\", \"assignmentSlug\": \"${ASSIGNMENT_SLUG}\", \"agent\": \"claude\", \"sessionId\": \"${SESSION_ID}\", \"path\": \"${CWD}\"}" \
54
- 2>/dev/null) || true
43
+ # No real session id available — exit quietly. We never synthesize one.
44
+ [ -z "$SESSION_ID" ] && exit 0
55
45
 
56
- # If registration succeeded, update the context file with the session ID
57
- if [ -n "$RESPONSE" ]; then
58
- jq --arg sid "$SESSION_ID" '. + {sessionId: $sid}' "$CONTEXT_FILE" > "${CONTEXT_FILE}.tmp" 2>/dev/null \
59
- && mv "${CONTEXT_FILE}.tmp" "$CONTEXT_FILE" 2>/dev/null || true
60
- fi
46
+ # --- Dashboard endpoint resolution (mirror session-start.sh exactly so start
47
+ # and end hooks always target the same host:port) ---
48
+ PORT="${SYNTAUR_DASHBOARD_PORT:-}"
49
+ if [ -z "$PORT" ]; then
50
+ PORT=$(cat "$HOME/.syntaur/dashboard-port" 2>/dev/null || echo "4800")
61
51
  fi
62
52
 
63
- # --- Step 6: Mark session as stopped via dashboard API ---
53
+ # --- Step 5: Mark session as stopped via dashboard API ---
64
54
  BODY="{\"status\": \"stopped\"}"
65
55
  if [ -n "$MISSION_SLUG" ]; then
66
56
  BODY="{\"status\": \"stopped\", \"projectSlug\": \"${MISSION_SLUG}\"}"
67
57
  fi
68
58
 
69
- curl -sf -X PATCH "http://localhost:${PORT}/api/agent-sessions/${SESSION_ID}/status" \
59
+ curl -sf --max-time 3 -X PATCH "http://127.0.0.1:${PORT}/api/agent-sessions/${SESSION_ID}/status" \
70
60
  -H "Content-Type: application/json" \
71
61
  -d "$BODY" \
72
62
  -o /dev/null 2>/dev/null || true
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ # Syntaur SessionStart Hook
3
+ # (1) Merges the real Claude Code session_id + transcript_path into an
4
+ # EXISTING .syntaur/context.json. Never creates context.json — that would
5
+ # break grab-assignment's "context.json implies active assignment" semantic.
6
+ # (2) Pre-registers a minimal row in the dashboard sessions table so
7
+ # SessionEnd's PATCH /status always has a row to target. Best-effort —
8
+ # silently ignores dashboard-unreachable.
9
+ #
10
+ # Reads JSON from stdin per Claude Code SessionStart contract:
11
+ # { "session_id": "...", "transcript_path": "...", "cwd": "...", ... }
12
+ #
13
+ # Always exits 0.
14
+
15
+ set -o pipefail 2>/dev/null || true
16
+
17
+ command -v jq >/dev/null 2>&1 || exit 0
18
+
19
+ INPUT=$(cat)
20
+ [ -z "$INPUT" ] && exit 0
21
+
22
+ SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
23
+ TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
24
+ CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
25
+
26
+ [ -z "$SESSION_ID" ] && exit 0
27
+ [ -z "$CWD" ] && exit 0
28
+
29
+ CONTEXT_FILE="$CWD/.syntaur/context.json"
30
+
31
+ # REQUIRED invariant: only operate on an EXISTING context file. If the current
32
+ # cwd has no active Syntaur assignment, leave the filesystem untouched.
33
+ [ ! -f "$CONTEXT_FILE" ] && exit 0
34
+
35
+ # --- (1) Merge session fields into context.json.
36
+ # Always replace both sessionId and transcriptPath together. If the incoming
37
+ # transcript_path is empty, explicitly null the stored transcriptPath so a new
38
+ # session never inherits a stale transcript path from the prior session.
39
+ TMP="${CONTEXT_FILE}.tmp.$$"
40
+ jq \
41
+ --arg sid "$SESSION_ID" \
42
+ --arg tp "$TRANSCRIPT_PATH" \
43
+ '. + {sessionId: $sid, transcriptPath: (if ($tp | length) > 0 then $tp else null end)}' \
44
+ "$CONTEXT_FILE" > "$TMP" 2>/dev/null \
45
+ && mv "$TMP" "$CONTEXT_FILE" 2>/dev/null \
46
+ || rm -f "$TMP"
47
+
48
+ # --- (2) Best-effort pre-registration in the dashboard.
49
+ # Read project/assignment context if present so the pre-registered row is
50
+ # already linked. Upsert semantics on the server mean this is idempotent with
51
+ # later /track-session or grab-assignment calls.
52
+ MISSION_SLUG=$(jq -r '.projectSlug // empty' "$CONTEXT_FILE" 2>/dev/null)
53
+ ASSIGNMENT_SLUG=$(jq -r '.assignmentSlug // empty' "$CONTEXT_FILE" 2>/dev/null)
54
+
55
+ PORT="${SYNTAUR_DASHBOARD_PORT:-}"
56
+ if [ -z "$PORT" ]; then
57
+ PORT=$(cat "$HOME/.syntaur/dashboard-port" 2>/dev/null || echo "4800")
58
+ fi
59
+
60
+ BODY=$(jq -cn \
61
+ --arg sid "$SESSION_ID" \
62
+ --arg tp "$TRANSCRIPT_PATH" \
63
+ --arg proj "$MISSION_SLUG" \
64
+ --arg assn "$ASSIGNMENT_SLUG" \
65
+ --arg path "$CWD" \
66
+ '{ agent: "claude", sessionId: $sid, path: $path }
67
+ + (if ($tp | length) > 0 then {transcriptPath: $tp} else {} end)
68
+ + (if ($proj | length) > 0 then {projectSlug: $proj} else {} end)
69
+ + (if ($assn | length) > 0 then {assignmentSlug: $assn} else {} end)' 2>/dev/null)
70
+
71
+ if [ -n "$BODY" ]; then
72
+ # --max-time bounds the hook's wall-clock cost if the dashboard socket
73
+ # accepts but then hangs. The hook itself is registered with timeout: 5.
74
+ curl -sf --max-time 3 -X POST "http://127.0.0.1:${PORT}/api/agent-sessions" \
75
+ -H "Content-Type: application/json" \
76
+ -d "$BODY" \
77
+ -o /dev/null 2>/dev/null || true
78
+ fi
79
+
80
+ exit 0
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ # Syntaur Claude Code statusLine.
3
+ #
4
+ # Renders a single line with:
5
+ # <branch> · <worktree-basename> · <assignment> · <sessionId-suffix>
6
+ #
7
+ # Reads JSON from stdin per Claude Code statusLine contract:
8
+ # { "session_id": "...", "cwd": "...", "workspace": { "current_dir": "..." }, ... }
9
+ #
10
+ # Empty segments are omitted. Never fails the terminal — always exits 0.
11
+
12
+ set -o pipefail 2>/dev/null || true
13
+
14
+ INPUT=$(cat)
15
+
16
+ # Degrade cleanly if jq is unavailable.
17
+ if ! command -v jq >/dev/null 2>&1; then
18
+ printf '%s' '(syntaur: jq missing)'
19
+ exit 0
20
+ fi
21
+
22
+ SESSION_ID=""
23
+ CWD=""
24
+
25
+ if [ -n "$INPUT" ]; then
26
+ SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
27
+ CWD=$(printf '%s' "$INPUT" | jq -r '.workspace.current_dir // .cwd // empty' 2>/dev/null)
28
+ fi
29
+
30
+ # Fall back to the shell's CWD if the payload omits it.
31
+ [ -z "$CWD" ] && CWD="$PWD"
32
+
33
+ # --- Segment 1: branch ---
34
+ BRANCH=""
35
+ if [ -n "$CWD" ] && [ -d "$CWD" ]; then
36
+ BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null)
37
+ if [ "$BRANCH" = "HEAD" ] || [ -z "$BRANCH" ]; then
38
+ SHORT=$(git -C "$CWD" rev-parse --short HEAD 2>/dev/null)
39
+ if [ -n "$SHORT" ]; then
40
+ BRANCH="detached@$SHORT"
41
+ else
42
+ BRANCH=""
43
+ fi
44
+ fi
45
+ fi
46
+
47
+ # --- Segment 2: worktree basename ---
48
+ WORKTREE=""
49
+ if [ -n "$CWD" ] && [ -d "$CWD" ]; then
50
+ WT_PATH=$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null)
51
+ if [ -n "$WT_PATH" ]; then
52
+ WORKTREE=$(basename "$WT_PATH")
53
+ fi
54
+ fi
55
+
56
+ # --- Segment 3: active syntaur assignment ---
57
+ ASSIGNMENT=""
58
+ CONTEXT_FILE="$CWD/.syntaur/context.json"
59
+ if [ -f "$CONTEXT_FILE" ]; then
60
+ PROJECT_SLUG=$(jq -r '.projectSlug // empty' "$CONTEXT_FILE" 2>/dev/null)
61
+ ASSIGNMENT_SLUG=$(jq -r '.assignmentSlug // empty' "$CONTEXT_FILE" 2>/dev/null)
62
+ ASSIGNMENT_DIR=$(jq -r '.assignmentDir // empty' "$CONTEXT_FILE" 2>/dev/null)
63
+
64
+ TITLE=""
65
+ if [ -n "$ASSIGNMENT_DIR" ] && [ -f "$ASSIGNMENT_DIR/assignment.md" ]; then
66
+ TITLE=$(awk '/^title:/{sub(/^title:[[:space:]]*"?/,""); sub(/"?[[:space:]]*$/,""); print; exit}' "$ASSIGNMENT_DIR/assignment.md" 2>/dev/null)
67
+ fi
68
+
69
+ LABEL=""
70
+ if [ -n "$PROJECT_SLUG" ] && [ -n "$ASSIGNMENT_SLUG" ]; then
71
+ LABEL="$PROJECT_SLUG/$ASSIGNMENT_SLUG"
72
+ elif [ -n "$ASSIGNMENT_SLUG" ]; then
73
+ # Standalone assignment — assignmentSlug is the UUID folder name. Take the
74
+ # first 8 chars for terseness.
75
+ UUID_PREFIX="${ASSIGNMENT_SLUG:0:8}"
76
+ LABEL="standalone/$UUID_PREFIX"
77
+ fi
78
+
79
+ if [ -n "$LABEL" ] && [ -n "$TITLE" ]; then
80
+ ASSIGNMENT="$LABEL — $TITLE"
81
+ elif [ -n "$LABEL" ]; then
82
+ ASSIGNMENT="$LABEL"
83
+ fi
84
+ fi
85
+
86
+ # --- Segment 4: session id suffix ---
87
+ SESSION_SUFFIX=""
88
+ if [ -n "$SESSION_ID" ]; then
89
+ LEN=${#SESSION_ID}
90
+ if [ "$LEN" -gt 8 ]; then
91
+ SESSION_SUFFIX="…${SESSION_ID: -8}"
92
+ else
93
+ SESSION_SUFFIX="$SESSION_ID"
94
+ fi
95
+ fi
96
+
97
+ # --- Join segments with ' · ', suppressing empties. ---
98
+ OUT=""
99
+ for seg in "$BRANCH" "$WORKTREE" "$ASSIGNMENT" "$SESSION_SUFFIX"; do
100
+ if [ -n "$seg" ]; then
101
+ if [ -z "$OUT" ]; then
102
+ OUT="$seg"
103
+ else
104
+ OUT="$OUT · $seg"
105
+ fi
106
+ fi
107
+ done
108
+
109
+ printf '%s' "$OUT"
110
+ exit 0
@@ -27,8 +27,9 @@ Parse the arguments:
27
27
  ## Pre-flight Check
28
28
 
29
29
  1. Check if `.syntaur/context.json` already exists in the current working directory.
30
- - If it exists, read it and warn the user: "You already have an active assignment: `<assignmentSlug>` in project `<projectSlug>`. Grabbing a new assignment will replace this context. Proceed?"
30
+ - If it exists AND contains BOTH `projectSlug` and `assignmentSlug`, read it and warn the user: "You already have an active assignment: `<assignmentSlug>` in project `<projectSlug>`. Grabbing a new assignment will replace this context. Proceed?"
31
31
  - If the user says no, stop.
32
+ - If the file exists but only has session fields (`sessionId`, `transcriptPath`) and no project/assignment, do NOT warn — that context was populated by the SessionStart hook and is not an "active assignment" marker. Proceed silently and merge new assignment fields in Step 5.
32
33
 
33
34
  ## Step 1: Discover the Project
34
35
 
@@ -113,15 +114,17 @@ workspace:
113
114
  worktreePath: /absolute/path/to/cwd
114
115
  ```
115
116
 
116
- ## Step 5: Create Context File
117
+ ## Step 5: Create or Merge Context File
117
118
 
118
- Write `.syntaur/context.json` in the current working directory with the assignment context. First ensure the directory exists:
119
+ Merge assignment context into `.syntaur/context.json`. Do NOT overwrite: if the file already exists (e.g., the SessionStart hook populated `sessionId` + `transcriptPath`), preserve those fields.
120
+
121
+ First ensure the directory exists:
119
122
 
120
123
  ```bash
121
124
  mkdir -p .syntaur
122
125
  ```
123
126
 
124
- Then write the JSON file with this structure:
127
+ Prepare the assignment payload:
125
128
 
126
129
  ```json
127
130
  {
@@ -136,26 +139,38 @@ Then write the JSON file with this structure:
136
139
  }
137
140
  ```
138
141
 
142
+ Merge it on top of whatever context.json already contains:
143
+
144
+ ```bash
145
+ if [ -f .syntaur/context.json ]; then
146
+ jq --slurpfile new <(echo "$NEW_CONTEXT_JSON") '. + $new[0]' .syntaur/context.json > .syntaur/context.json.tmp \
147
+ && mv .syntaur/context.json.tmp .syntaur/context.json
148
+ else
149
+ echo "$NEW_CONTEXT_JSON" > .syntaur/context.json
150
+ fi
151
+ ```
152
+
153
+ This preserves any `sessionId` / `transcriptPath` the SessionStart hook wrote, while layering assignment fields on top.
154
+
139
155
  Use absolute paths (expand `~` to the actual home directory). Note: `workspace.repository` may be a remote URL (e.g., `https://github.com/...`) -- only use it as `workspaceRoot` if it starts with `/` (local path). If it is a URL, set `workspaceRoot` to the current working directory.
140
156
 
141
157
  **IMPORTANT:** The `workspaceRoot` must NEVER be null when the agent will be writing code. If no workspace was configured, default to the current working directory.
142
158
 
143
159
  ## Step 5.5: Register Agent Session
144
160
 
145
- After creating the context file, register this session in the project's agent session log so it appears in the dashboard.
161
+ After merging context, register this session in the dashboard.
146
162
 
147
- 1. Find the current Claude Code session ID by reading from `~/.claude/sessions/`. These are JSON files keyed by PID containing `sessionId`, `cwd`, etc. Find the most recently modified file whose `cwd` matches the current working directory:
148
- ```bash
149
- ls -t ~/.claude/sessions/*.json | head -5
150
- ```
151
- Read the most recent file(s) and find the one whose `cwd` matches `$(pwd)`. Extract the `sessionId` field — this is the real Claude Code session ID that can be used with `claude --resume <sessionId>` to resume this exact conversation.
163
+ 1. **Source the real Claude session_id + transcript_path.** In priority order:
164
+ 1. If `.syntaur/context.json` already has `sessionId` (SessionStart hook populated it), use that ID and read `transcriptPath` from the same file.
165
+ 2. Otherwise, fall back to `~/.claude/sessions/*.json`: `ls -t ~/.claude/sessions/*.json | head -5`, read each, and pick the most recently modified entry whose `cwd` matches `$(pwd)`. Extract `sessionId`. The transcript path for Claude Code lives at `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl` — construct that path if you need it.
166
+ 3. If neither source yields a real ID, DO NOT synthesize one. Abort with: "Could not resolve a real Claude Code session id. Restart the Claude session so the SessionStart hook can populate `.syntaur/context.json`, or run `/rename <assignment-slug>` and retry."
152
167
 
153
- If you cannot find a matching session file (e.g., no file matches the cwd, or the sessions directory is empty), ask the user to run `/rename <assignment-slug>` to name the current session after the assignment. Then store the assignment slug as the session name in context.json (`"sessionName": "<assignment-slug>"`) instead of `sessionId`. The user can later resume with `claude --resume <assignment-slug>`.
168
+ 2. **Merge `sessionId` and `transcriptPath` back into context.json** (safe even if already present jq merge is idempotent).
154
169
 
155
- 2. Run the track-session command (use `dangerouslyDisableSandbox: true` since it writes to `~/.syntaur/`):
156
- ```bash
157
- syntaur track-session --project <projectSlug> --assignment <assignmentSlug> --agent claude --session-id <claude-session-id> --path $(pwd)
158
- ```
170
+ 3. **Run the track-session CLI** (use `dangerouslyDisableSandbox: true` since it writes to `~/.syntaur/`). Both `--session-id` and real path are required:
171
+ ```bash
172
+ syntaur track-session --project <projectSlug> --assignment <assignmentSlug> --agent claude --session-id <real-session-id> --transcript-path <transcript-path> --path $(pwd)
173
+ ```
159
174
 
160
175
  3. Update the `.syntaur/context.json` context file to include the session ID. Add `"sessionId": "<claude-session-id>"` to the JSON object you wrote in Step 5.
161
176
 
@@ -46,13 +46,20 @@ For each file found, read it and follow its directives. Playbooks may contain ru
46
46
 
47
47
  Read the following files to understand the assignment:
48
48
 
49
- 1. Read `<assignmentDir>/assignment.md` -- extract the objective, acceptance criteria, context section, and any Q&A
50
- 2. Read `<projectDir>/agent.md` -- extract conventions and boundaries
51
- 3. Read `<projectDir>/claude.md` if it exists -- extract Claude-specific instructions
52
- 4. Read `<projectDir>/project.md` -- extract the project goal for broader context
53
-
54
- If the assignment has dependencies (`dependsOn` in frontmatter), read the handoff.md from each dependency's assignment folder for integration context:
49
+ 1. Read `<assignmentDir>/assignment.md` -- extract the objective, acceptance criteria, context section, and the `## Todos` list
50
+ 2. Read `<assignmentDir>/comments.md` if it exists -- inherited questions, notes, and feedback
51
+ 3. Read `<projectDir>/project.md` -- extract the project goal for broader context
52
+ 4. Read `<projectDir>/manifest.md` -- navigation index for the project
53
+
54
+ Per-project `agent.md` and `claude.md` were removed in v0.2.0. The agent-level
55
+ conventions now live in the repo root `CLAUDE.md` / `AGENTS.md` and in
56
+ `~/.syntaur/playbooks/`, which Step 1.5 already loaded.
57
+
58
+ If the assignment has dependencies (`dependsOn` in frontmatter), read each
59
+ dependency's `handoff.md` and `decision-record.md` for integration context and
60
+ upstream decisions:
55
61
  - `<projectDir>/assignments/<dep-slug>/handoff.md`
62
+ - `<projectDir>/assignments/<dep-slug>/decision-record.md`
56
63
 
57
64
  ## Step 3: Explore Workspace (if set)
58
65
 
@@ -144,5 +151,6 @@ After writing the plan:
144
151
 
145
152
  **Remind the agent about recordkeeping during implementation:**
146
153
  - Check off acceptance criteria in `assignment.md` as each one is completed, not in a batch at the end
147
- - Update the `## Progress` section in `assignment.md` after each meaningful milestone
148
- - The assignment file is a live document it should reflect current state at all times
154
+ - Append timestamped milestones to `progress.md` (separate append-only file) — not to `assignment.md`
155
+ - Use `syntaur comment <slug-or-uuid> "body" --type note|question|feedback` to add a comment to `comments.md`
156
+ - `assignment.md` is a live document — keep its status, todos, and acceptance checkboxes reflecting current state at all times
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntaur",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Run Syntaur project and assignment workflows from Codex, including claiming work, planning, completing handoffs, session tracking, and write-boundary enforcement.",
5
5
  "author": {
6
6
  "name": "Brennen"
@@ -93,7 +93,7 @@ Use these commands directly when needed:
93
93
  - `syntaur comment <assignment-slug-or-uuid> "body" --type question|note|feedback [--reply-to <id>] [--project <slug>]` — append to `comments.md`
94
94
  - `syntaur request <target-slug-or-uuid> "text" [--from <source>] [--project <slug>]` — append to target's `## Todos`, annotated `(from: <source>)`
95
95
  - `syntaur uninstall [--all] [--yes]`
96
- - `syntaur track-session --project <project-slug> --assignment <assignment-slug> --agent codex --session-id <id> --path <cwd>`
96
+ - `syntaur track-session --project <project-slug> --assignment <assignment-slug> --agent codex --session-id <real-id> --transcript-path <rollout-path> --path <cwd>` (both `--session-id` and `--transcript-path` must come from the matching Codex rollout file — never synthesize)
97
97
  - `syntaur setup-adapter codex --project <project-slug> --assignment <assignment-slug>`
98
98
 
99
99
  ## Standard Workflows
@@ -103,9 +103,11 @@ Use these commands directly when needed:
103
103
  1. Discover the project and pending assignments.
104
104
  2. Run `syntaur assign ... --agent codex`.
105
105
  3. Run `syntaur start ...`.
106
- 4. Create `.syntaur/context.json` in the working directory.
107
- 5. Register the session with `syntaur track-session`.
108
- 6. If needed, run `syntaur setup-adapter codex --project <slug> --assignment <slug>`.
106
+ 4. Create (or merge into) `.syntaur/context.json` in the working directory. If a prior context file exists, preserve its fields.
107
+ 5. Resolve the real Codex session id and rollout path: `bash ./scripts/resolve-session.sh "$(pwd)"` (relative to the plugin root). Parse `session_id=<id>` and `transcript_path=<abs path>`. If the helper exits non-zero, there is no matching Codex rollout in this cwd — start the Codex session first, then retry. Never `uuidgen`.
108
+ 6. Merge `sessionId` + `transcriptPath` into `.syntaur/context.json`.
109
+ 7. Register the session: `syntaur track-session --project <slug> --assignment <slug> --agent codex --session-id <id> --transcript-path <path> --path "$(pwd)"`.
110
+ 8. If needed, run `syntaur setup-adapter codex --project <slug> --assignment <slug>`.
109
111
 
110
112
  ### Plan an assignment
111
113
 
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # Syntaur Codex resolve-session helper
3
+ # Finds the most-recent Codex rollout file whose session_meta.payload.cwd
4
+ # matches $1 (default $PWD) and emits two lines to stdout:
5
+ # session_id=<id>
6
+ # transcript_path=<absolute path>
7
+ # Exits non-zero with nothing on stdout if no match.
8
+ #
9
+ # Override the search root via CODEX_SESSIONS_DIR (default: $HOME/.codex/sessions).
10
+ # Known limitation: if multiple concurrent Codex sessions share the same cwd,
11
+ # this picks the newest-by-mtime. Users can bypass by passing --session-id and
12
+ # --transcript-path explicitly to `syntaur track-session`.
13
+
14
+ set -o pipefail 2>/dev/null || true
15
+
16
+ command -v jq >/dev/null 2>&1 || { exit 1; }
17
+
18
+ TARGET_CWD="${1:-$PWD}"
19
+ SESSIONS_ROOT="${CODEX_SESSIONS_DIR:-$HOME/.codex/sessions}"
20
+
21
+ shopt -s nullglob 2>/dev/null || true
22
+
23
+ # Expand the glob explicitly via bash. If no files match, `files` stays empty
24
+ # and we exit without invoking ls — guards against `ls -1t` falling back to
25
+ # listing the current directory when the glob strips to zero operands.
26
+ files=("$SESSIONS_ROOT"/*/*/*/rollout-*.jsonl)
27
+ [ "${#files[@]}" -eq 0 ] && exit 1
28
+
29
+ MATCHED_FILE=""
30
+ MATCHED_ID=""
31
+
32
+ while IFS= read -r f; do
33
+ [ -z "$f" ] && continue
34
+ FIRST=$(head -n 1 "$f" 2>/dev/null)
35
+ [ -z "$FIRST" ] && continue
36
+ SESSION_CWD=$(printf '%s' "$FIRST" | jq -r 'select(.type=="session_meta") | .payload.cwd // empty' 2>/dev/null)
37
+ SESSION_ID=$(printf '%s' "$FIRST" | jq -r 'select(.type=="session_meta") | .payload.id // empty' 2>/dev/null)
38
+ if [ "$SESSION_CWD" = "$TARGET_CWD" ] && [ -n "$SESSION_ID" ]; then
39
+ MATCHED_FILE="$f"
40
+ MATCHED_ID="$SESSION_ID"
41
+ break
42
+ fi
43
+ done < <(ls -1t "${files[@]}" 2>/dev/null)
44
+
45
+ [ -z "$MATCHED_FILE" ] && exit 1
46
+
47
+ printf 'session_id=%s\n' "$MATCHED_ID"
48
+ printf 'transcript_path=%s\n' "$MATCHED_FILE"
49
+ exit 0