syntaur 0.3.0 → 0.4.0

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.
Files changed (41) hide show
  1. package/README.md +26 -3
  2. package/dist/dashboard/server.js +497 -315
  3. package/dist/dashboard/server.js.map +1 -1
  4. package/dist/index.js +1240 -550
  5. package/dist/index.js.map +1 -1
  6. package/package.json +8 -2
  7. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  8. package/platforms/claude-code/agents/syntaur-expert.md +35 -20
  9. package/platforms/claude-code/commands/complete-assignment/complete-assignment.md +20 -0
  10. package/platforms/claude-code/commands/create-assignment/create-assignment.md +20 -0
  11. package/platforms/claude-code/commands/create-project/create-project.md +20 -0
  12. package/platforms/claude-code/commands/grab-assignment/grab-assignment.md +20 -0
  13. package/platforms/claude-code/commands/plan-assignment/plan-assignment.md +20 -0
  14. package/platforms/claude-code/commands/track-session/track-session.md +43 -18
  15. package/platforms/claude-code/hooks/hooks.json +11 -0
  16. package/platforms/claude-code/hooks/session-cleanup.sh +13 -23
  17. package/platforms/claude-code/hooks/session-start.sh +80 -0
  18. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  19. package/platforms/codex/agents/syntaur-operator.md +6 -4
  20. package/platforms/codex/scripts/resolve-session.sh +49 -0
  21. package/statusline/statusline.sh +133 -0
  22. package/vendor/syntaur-skills/LICENSE +21 -0
  23. package/vendor/syntaur-skills/README.md +43 -0
  24. package/vendor/syntaur-skills/skills/complete-assignment/SKILL.md +146 -0
  25. package/vendor/syntaur-skills/skills/create-assignment/SKILL.md +72 -0
  26. package/vendor/syntaur-skills/skills/create-project/SKILL.md +56 -0
  27. package/vendor/syntaur-skills/skills/grab-assignment/SKILL.md +158 -0
  28. package/vendor/syntaur-skills/skills/plan-assignment/SKILL.md +137 -0
  29. package/vendor/syntaur-skills/skills/syntaur-protocol/SKILL.md +119 -0
  30. package/vendor/syntaur-skills/skills/syntaur-protocol/references/file-ownership.md +67 -0
  31. package/vendor/syntaur-skills/skills/syntaur-protocol/references/protocol-summary.md +82 -0
  32. package/platforms/claude-code/skills/complete-assignment/SKILL.md +0 -155
  33. package/platforms/claude-code/skills/create-assignment/SKILL.md +0 -67
  34. package/platforms/claude-code/skills/grab-assignment/SKILL.md +0 -187
  35. package/platforms/claude-code/skills/plan-assignment/SKILL.md +0 -148
  36. package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +0 -86
  37. package/platforms/codex/skills/complete-assignment/SKILL.md +0 -64
  38. package/platforms/codex/skills/create-assignment/SKILL.md +0 -49
  39. package/platforms/codex/skills/grab-assignment/SKILL.md +0 -71
  40. package/platforms/codex/skills/plan-assignment/SKILL.md +0 -57
  41. package/platforms/codex/skills/syntaur-protocol/SKILL.md +0 -102
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntaur",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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": {
@@ -29,7 +29,11 @@
29
29
  ".agents",
30
30
  "platforms",
31
31
  "examples",
32
- "dashboard/dist"
32
+ "dashboard/dist",
33
+ "statusline",
34
+ "vendor/syntaur-skills/skills/**",
35
+ "vendor/syntaur-skills/LICENSE",
36
+ "vendor/syntaur-skills/README.md"
33
37
  ],
34
38
  "scripts": {
35
39
  "build": "tsup",
@@ -40,7 +44,9 @@
40
44
  "typecheck": "tsc --noEmit",
41
45
  "test": "vitest run",
42
46
  "test:watch": "vitest",
47
+ "prepack": "node scripts/verify-vendored-skills.mjs",
43
48
  "prepublishOnly": "npm run build && npm ci --prefix dashboard && npm run build --prefix dashboard",
49
+ "postinstall": "node scripts/postinstall-submodules.mjs",
44
50
  "try": "node scripts/try.mjs",
45
51
  "untry": "npm unlink -g syntaur && npm install -g syntaur@latest && echo '\\n✓ global syntaur restored to latest published version'"
46
52
  },
@@ -5,5 +5,5 @@
5
5
  "name": "Brennen",
6
6
  "email": ""
7
7
  },
8
- "version": "0.1.7"
8
+ "version": "0.1.8"
9
9
  }
@@ -12,10 +12,12 @@ When answering questions, read the actual source files rather than relying solel
12
12
 
13
13
  ## Key Source Files
14
14
 
15
- - **Protocol summary:** `${CLAUDE_PLUGIN_ROOT}/references/protocol-summary.md`
15
+ - **Protocol summary:** `${CLAUDE_PLUGIN_ROOT}/references/protocol-summary.md` (or `~/.claude/skills/syntaur-protocol/references/protocol-summary.md` for the installed skill version)
16
16
  - **File ownership:** `${CLAUDE_PLUGIN_ROOT}/references/file-ownership.md`
17
17
  - **Plugin manifest:** `${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json`
18
- - **Skills:** `${CLAUDE_PLUGIN_ROOT}/skills/`
18
+ - **Protocol skills (installed by `syntaur install-plugin`):** `~/.claude/skills/{syntaur-protocol,grab-assignment,plan-assignment,complete-assignment,create-assignment,create-project}/`
19
+ - **Protocol skills source (vendored via submodule):** `<syntaur-repo>/vendor/syntaur-skills/skills/` — standalone repo at https://github.com/prong-horn/syntaur-skills
20
+ - **Slash commands (ship in plugin):** `${CLAUDE_PLUGIN_ROOT}/commands/` — thin wrappers that invoke the corresponding installed skill
19
21
  - **Hooks:** `${CLAUDE_PLUGIN_ROOT}/hooks/`
20
22
 
21
23
  For the live CLI surface, run `syntaur --help` in the user environment.
@@ -191,7 +193,7 @@ Only the assigned agent may write to its own assignment folder.
191
193
  ### Session Tracking
192
194
  | Command | Description |
193
195
  |---------|-------------|
194
- | `syntaur track-session --project M --assignment A --agent N` | Register agent session |
196
+ | `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
197
 
196
198
  All commands support `--dir <path>` to override the default `~/.syntaur/projects/` directory.
197
199
 
@@ -204,27 +206,38 @@ The Syntaur Claude Code plugin is installed by `syntaur install-plugin`, which r
204
206
  ```
205
207
  plugin/
206
208
  .claude-plugin/
207
- plugin.json # Plugin metadata
209
+ plugin.json # Plugin metadata
208
210
  agents/
209
- syntaur-expert.md # This agent
210
- skills/
211
- syntaur-protocol/SKILL.md # Core protocol rules (background)
212
- grab-assignment/SKILL.md # Claim a pending assignment
213
- create-project/SKILL.md # Create new project
214
- create-assignment/SKILL.md # Create new assignment
215
- plan-assignment/SKILL.md # Write implementation plan
216
- complete-assignment/SKILL.md # Handoff and complete
211
+ syntaur-expert.md # This agent
217
212
  commands/
218
- track-session/track-session.md # Register tmux sessions
213
+ grab-assignment/grab-assignment.md # Slash wrapper for grab-assignment skill
214
+ plan-assignment/plan-assignment.md # Slash wrapper for plan-assignment skill
215
+ complete-assignment/complete-assignment.md # Slash wrapper for complete-assignment skill
216
+ create-assignment/create-assignment.md # Slash wrapper for create-assignment skill
217
+ create-project/create-project.md # Slash wrapper for create-project skill
218
+ track-session/track-session.md # Claude-specific session registration
219
+ doctor-syntaur/... # Diagnose install
220
+ track-server/... # Register a running server
219
221
  hooks/
220
- hooks.json # Hook definitions
221
- session-cleanup.sh # Mark sessions stopped on exit
222
- enforce-boundaries.sh # Write boundary enforcement
222
+ hooks.json # Hook definitions
223
+ session-start.sh # Merge real session_id + transcript_path into existing .syntaur/context.json
224
+ session-cleanup.sh # Mark sessions stopped on exit
225
+ enforce-boundaries.sh # Write boundary enforcement
223
226
  references/
224
- protocol-summary.md # One-page protocol quick reference
225
- file-ownership.md # Write boundary rules
227
+ protocol-summary.md # One-page protocol quick reference
228
+ file-ownership.md # Write boundary rules
229
+
230
+ ~/.claude/skills/ # Installed by `syntaur install-plugin` (vendored from syntaur-skills repo)
231
+ syntaur-protocol/SKILL.md # Auto-activates on Syntaur file contexts
232
+ grab-assignment/SKILL.md
233
+ plan-assignment/SKILL.md
234
+ complete-assignment/SKILL.md
235
+ create-assignment/SKILL.md
236
+ create-project/SKILL.md
226
237
  ```
227
238
 
239
+ Slash commands (`/grab-assignment` etc.) are thin wrappers that delegate to the installed skills. This lets the same protocol skills work in Claude Code (via slash command + auto-activation) and Codex (via auto-activation only).
240
+
228
241
  ### Skills Summary
229
242
 
230
243
  | Skill | Trigger | Purpose |
@@ -241,6 +254,7 @@ plugin/
241
254
  | Hook | Event | Behavior |
242
255
  |------|-------|----------|
243
256
  | 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` |
257
+ | 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
258
  | SessionEnd | Claude Code session exits | Runs session-cleanup.sh to mark session as stopped |
245
259
  | PreToolUse: enforce-boundaries | Edit/Write/MultiEdit | Validates target path is within assignment boundaries |
246
260
 
@@ -370,7 +384,7 @@ syntaur dashboard
370
384
 
371
385
  ## Context File (.syntaur/context.json)
372
386
 
373
- Created by `/grab-assignment` in the current working directory. Contains:
387
+ 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
388
  ```json
375
389
  {
376
390
  "projectSlug": "my-first-project",
@@ -381,7 +395,8 @@ Created by `/grab-assignment` in the current working directory. Contains:
381
395
  "title": "Design the schema",
382
396
  "branch": "feature/design-the-schema",
383
397
  "grabbedAt": "2026-03-18T14:30:00Z",
384
- "sessionId": "uuid-v4"
398
+ "sessionId": "<real-claude-session-id>",
399
+ "transcriptPath": "/Users/you/.claude/projects/<encoded-cwd>/<session-id>.jsonl"
385
400
  }
386
401
  ```
387
402
 
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: complete-assignment
3
+ description: Append a progress entry + handoff and transition the current Syntaur assignment to review or completed
4
+ arguments:
5
+ - name: args
6
+ description: "Optional — see the complete-assignment skill for supported flags"
7
+ required: false
8
+ ---
9
+
10
+ # /complete-assignment
11
+
12
+ Thin wrapper that invokes the `complete-assignment` skill. The skill lives in `~/.claude/skills/complete-assignment/` (installed by `syntaur setup` / `syntaur install-plugin`) and contains the full protocol — verifying acceptance criteria and todos, appending a progress.md entry, writing a handoff.md section, and calling `syntaur review` or `syntaur complete`.
13
+
14
+ ## Instructions
15
+
16
+ Invoke the `complete-assignment` skill via the Skill tool, passing the user's arguments. The skill handles everything else.
17
+
18
+ Arguments: $ARGUMENTS
19
+
20
+ If the skill is not installed, tell the user to run `syntaur install-plugin` (or `syntaur setup` if they haven't set up yet).
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: create-assignment
3
+ description: Create a new Syntaur assignment (project-nested or standalone one-off)
4
+ arguments:
5
+ - name: args
6
+ description: "Title and flags. See the create-assignment skill for supported forms (e.g. --project <slug>, --one-off, --type <type>)."
7
+ required: false
8
+ ---
9
+
10
+ # /create-assignment
11
+
12
+ Thin wrapper that invokes the `create-assignment` skill. The skill lives in `~/.claude/skills/create-assignment/` (installed by `syntaur setup` / `syntaur install-plugin`) and contains the full protocol — picking project-nested or standalone, validating the type, scaffolding assignment.md / progress.md / comments.md.
13
+
14
+ ## Instructions
15
+
16
+ Invoke the `create-assignment` skill via the Skill tool, passing the user's arguments. The skill handles everything else.
17
+
18
+ Arguments: $ARGUMENTS
19
+
20
+ If the skill is not installed, tell the user to run `syntaur install-plugin` (or `syntaur setup` if they haven't set up yet).
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: create-project
3
+ description: Create a new Syntaur project with full scaffolding
4
+ arguments:
5
+ - name: args
6
+ description: "Title and optional flags (--slug, --dir, --workspace). See the create-project skill for full usage."
7
+ required: false
8
+ ---
9
+
10
+ # /create-project
11
+
12
+ Thin wrapper that invokes the `create-project` skill. The skill lives in `~/.claude/skills/create-project/` (installed by `syntaur setup` / `syntaur install-plugin`) and contains the full protocol — calling `syntaur create-project`, reading the generated project.md, and guiding next steps.
13
+
14
+ ## Instructions
15
+
16
+ Invoke the `create-project` skill via the Skill tool, passing the user's arguments. The skill handles everything else.
17
+
18
+ Arguments: $ARGUMENTS
19
+
20
+ If the skill is not installed, tell the user to run `syntaur install-plugin` (or `syntaur setup` if they haven't set up yet).
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: grab-assignment
3
+ description: Claim a Syntaur assignment and load it into the current working context
4
+ arguments:
5
+ - name: args
6
+ description: "Project slug and optional assignment slug, or --id <uuid> for standalone. See the grab-assignment skill for full forms."
7
+ required: false
8
+ ---
9
+
10
+ # /grab-assignment
11
+
12
+ Thin wrapper that invokes the `grab-assignment` skill. The skill lives in `~/.claude/skills/grab-assignment/` (installed by `syntaur setup` / `syntaur install-plugin`) and contains the full protocol — discovering pending assignments, merging `.syntaur/context.json`, registering the agent session, reading the assignment.
13
+
14
+ ## Instructions
15
+
16
+ Invoke the `grab-assignment` skill via the Skill tool, passing the user's arguments. The skill handles everything else.
17
+
18
+ Arguments: $ARGUMENTS
19
+
20
+ If the skill is not installed, tell the user to run `syntaur install-plugin` (or `syntaur setup` if they haven't set up yet).
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: plan-assignment
3
+ description: Create a detailed implementation plan for the current Syntaur assignment
4
+ arguments:
5
+ - name: args
6
+ description: "Optional — see the plan-assignment skill for supported flags"
7
+ required: false
8
+ ---
9
+
10
+ # /plan-assignment
11
+
12
+ Thin wrapper that invokes the `plan-assignment` skill. The skill lives in `~/.claude/skills/plan-assignment/` (installed by `syntaur setup` / `syntaur install-plugin`) and contains the full protocol — picking the next `plan-v<N>.md`, writing it, appending a `## Todos` entry, marking any prior plan todo superseded, and recording key decisions in `decision-record.md`.
13
+
14
+ ## Instructions
15
+
16
+ Invoke the `plan-assignment` skill via the Skill tool, passing the user's arguments. The skill handles everything else.
17
+
18
+ Arguments: $ARGUMENTS
19
+
20
+ If the skill is not installed, tell the user to run `syntaur install-plugin` (or `syntaur setup` if they haven't set up yet).
@@ -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
@@ -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