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/dist/dashboard/server.js +497 -315
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +712 -517
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/claude-code/.claude-plugin/plugin.json +5 -1
- package/platforms/claude-code/agents/syntaur-expert.md +6 -3
- package/platforms/claude-code/commands/track-session/track-session.md +43 -18
- package/platforms/claude-code/hooks/hooks.json +11 -0
- package/platforms/claude-code/hooks/session-cleanup.sh +13 -23
- package/platforms/claude-code/hooks/session-start.sh +80 -0
- package/platforms/claude-code/hooks/statusline.sh +110 -0
- package/platforms/claude-code/skills/grab-assignment/SKILL.md +30 -15
- package/platforms/claude-code/skills/plan-assignment/SKILL.md +16 -8
- package/platforms/codex/.codex-plugin/plugin.json +1 -1
- package/platforms/codex/agents/syntaur-operator.md +6 -4
- package/platforms/codex/scripts/resolve-session.sh +49 -0
- package/platforms/codex/skills/grab-assignment/SKILL.md +7 -5
- package/platforms/codex/skills/plan-assignment/SKILL.md +8 -4
package/package.json
CHANGED
|
@@ -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
|
|
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.
|
|
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": "
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
59
|
+
Omit `--transcript-path` entirely (don't pass an empty string) if no transcript path could be resolved.
|
|
41
60
|
|
|
42
|
-
The CLI
|
|
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
|
-
|
|
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:
|
|
67
|
+
### Step 4: Merge context.json
|
|
49
68
|
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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.
|
|
@@ -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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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://
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
161
|
+
After merging context, register this session in the dashboard.
|
|
146
162
|
|
|
147
|
-
1.
|
|
148
|
-
|
|
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
|
-
|
|
168
|
+
2. **Merge `sessionId` and `transcriptPath` back into context.json** (safe even if already present — jq merge is idempotent).
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
syntaur track-session --project <projectSlug> --assignment <assignmentSlug> --agent claude --session-id <
|
|
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
|
|
50
|
-
2. Read `<
|
|
51
|
-
3. Read `<projectDir>/
|
|
52
|
-
4. Read `<projectDir>/
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
-
|
|
148
|
-
-
|
|
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.
|
|
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.
|
|
108
|
-
6.
|
|
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
|