syntaur 0.45.0 → 0.47.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 (76) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-RQBuJKcX.js → _basePickBy-DgR0_P-o.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-_J7s4kD3.js → _baseUniq-C8_Ych09.js} +1 -1
  4. package/dashboard/dist/assets/{arc-_9SyUgKQ.js → arc-yMHz4vGa.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-C8LeFMgr.js → architectureDiagram-2XIMDMQ5-ColWcH3P.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-gMh0EPEh.js → blockDiagram-WCTKOSBZ-Bo8Npvfq.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-cHwecwLI.js → c4Diagram-IC4MRINW-B2ky8AT7.js} +1 -1
  8. package/dashboard/dist/assets/channel-CUTEvTdk.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-Bb2anYuQ.js → chunk-4BX2VUAB-CyF6Z6dx.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-DYIRGzA1.js → chunk-55IACEB6-BJOEnwNN.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-sgRWBbaF.js → chunk-FMBD7UC4-D3siQyQ4.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-DlYKMl_j.js → chunk-JSJVCQXG-DKGuxEMf.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-D0YDLAOF.js → chunk-KX2RTZJC-CNIWWO2F.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-D-Y-CUx6.js → chunk-NQ4KR5QH-DXt05c7h.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-D7FpSvb5.js → chunk-QZHKN3VN-CM63uYnf.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-CtXgQLdS.js → chunk-WL4C6EOR-Dqvl_14m.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-Bkoc7orC.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-Bkoc7orC.js +1 -0
  19. package/dashboard/dist/assets/clone-CltBg7cH.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-YbTaohoJ.js → cose-bilkent-S5V4N54A-WBLtT1w9.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-CMtwGAnP.js → dagre-KLK3FWXG-DIdQdwa7.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-D8wBMBAX.js → diagram-E7M64L7V-BEH6P_Sk.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-DfudLpiJ.js → diagram-IFDJBPK2-BuhxBcSy.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-CyMy61wE.js → diagram-P4PSJMXO-DPSNVVzN.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-BlB4ZQl9.js → erDiagram-INFDFZHY-DYJb_rF5.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-DbhDQJM3.js → flowDiagram-PKNHOUZH-B9_8BI26.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DJFqteNi.js → ganttDiagram-A5KZAMGK-Bsg3QOhs.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-D8etA_mm.js → gitGraphDiagram-K3NZZRJ6-Cf5G9x_K.js} +1 -1
  29. package/dashboard/dist/assets/{graph-Ce86jeZn.js → graph-DyXfcrIH.js} +1 -1
  30. package/dashboard/dist/assets/index-C3kYxhbQ.js +567 -0
  31. package/dashboard/dist/assets/index-DKr21dk8.css +1 -0
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-Cx35U-h8.js → infoDiagram-LFFYTUFH-Bu1zlXs2.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-C04Y2nj8.js → ishikawaDiagram-PHBUUO56-fb8C-XRT.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-D8-cxbxE.js → journeyDiagram-4ABVD52K-smlBWs2O.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-DVKqMylP.js → kanban-definition-K7BYSVSG-Bz1AxFRE.js} +1 -1
  36. package/dashboard/dist/assets/{layout-98xZDpgu.js → layout-VsTD3onG.js} +1 -1
  37. package/dashboard/dist/assets/{linear-0jk_IwAc.js → linear-CE8xncGu.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-C337VWfr.js → mermaid.core-C0KQpDyW.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-8sNYGYEP.js → mindmap-definition-YRQLILUH-SRE5Immj.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-afcmzHxf.js → pieDiagram-SKSYHLDU-CaZ_aCcD.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-B4RjcpOq.js → quadrantDiagram-337W2JSQ-Dd6MIruu.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-CRavU6cI.js → requirementDiagram-Z7DCOOCP-BBXvP53l.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DFomU3z-.js → sankeyDiagram-WA2Y5GQK-DnS1SMIm.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CGKO7nmK.js → sequenceDiagram-2WXFIKYE-CLHJ1Uhx.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-BjFI1K8h.js → stateDiagram-RAJIS63D-B6vrAeYw.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BeqNZKbk.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-BBo8XJFG.js → timeline-definition-YZTLITO2-BlHwGfnL.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-COd6i6TE.js → treemap-KZPCXAKY-D9kOGUYR.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-CGQweQ36.js → vennDiagram-LZ73GAT5-BpQgeveT.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-mfJ5So7N.js → xychartDiagram-JWTSCODW-DRch79fE.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +1405 -210
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +2092 -1485
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.d.ts +19 -0
  57. package/dist/launch/index.js +528 -17
  58. package/dist/launch/index.js.map +1 -1
  59. package/package.json +1 -1
  60. package/platforms/SESSION-ID-RESOLUTION.md +41 -4
  61. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  62. package/platforms/claude-code/hooks/session-cleanup.sh +25 -64
  63. package/platforms/claude-code/hooks/session-start.sh +35 -109
  64. package/platforms/claude-code/skills/track-session/SKILL.md +12 -60
  65. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  66. package/platforms/codex/skills/track-session/SKILL.md +12 -60
  67. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  68. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  69. package/skills/track-session/SKILL.md +12 -60
  70. package/dashboard/dist/assets/channel-C36dnl_e.js +0 -1
  71. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BsoGa6_a.js +0 -1
  72. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BsoGa6_a.js +0 -1
  73. package/dashboard/dist/assets/clone-Bz6jW3OY.js +0 -1
  74. package/dashboard/dist/assets/index-DRng26Jg.js +0 -567
  75. package/dashboard/dist/assets/index-DzHQIE2n.css +0 -1
  76. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BtxefYKD.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntaur",
3
- "version": "0.45.0",
3
+ "version": "0.47.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": {
@@ -18,6 +18,32 @@ Each agent's job is only to make its real id reachable by layer 2, 3, or 4 — i
18
18
  to **normalize the key**, never to synthesize an id. Per-agent status and the
19
19
  exact injector below.
20
20
 
21
+ ## Session TRACKING vs id resolution (scanner is the universal floor)
22
+
23
+ Id resolution (above) answers "what is MY session id, right now, in-process."
24
+ Session **tracking** — every session eventually appearing in the sessions DB —
25
+ no longer depends on it:
26
+
27
+ - **Floor — filesystem scanner** (`syntaur session scan`,
28
+ `src/sessions/scanner.ts`): walks each registry target's `sessions` descriptor
29
+ (`AgentTarget.sessions` in `src/targets/registry.ts`; claude + codex today),
30
+ upserts every discovered session with its real transcript timestamps, links
31
+ project/assignment from `<cwd>/.syntaur/context.json`, derives liveness
32
+ (lsof transcript-open, else mtime freshness), and sweeps stale `active` rows
33
+ to `stopped` (`ended` backdated to last mtime). Runs on the dashboard's
34
+ autodiscovery interval (and at start) plus standalone via the CLI. This is
35
+ **Codex's only tracking path** (no SessionStart hook) and the retroactive
36
+ backfill for everything.
37
+ - **Fast path — hooks**: Claude's SessionStart/SessionEnd hooks are thin
38
+ wrappers over `syntaur session register|stop --from-hook` (direct DB writes,
39
+ zero tokens, no dashboard needed). Every session registers — standalone ones
40
+ included.
41
+ - **Birth path — launcher**: `executeLaunchPlan` writes a runtime marker for
42
+ any agent it spawns (pending — no sessionId — for fresh/fork; with the id for
43
+ resume-mode, plus an immediate DB row).
44
+ - Config: `session.autoTrack: all | workspaces-only | off` in
45
+ `~/.syntaur/config.md` (default `all`) gates all three paths.
46
+
21
47
  ## Generic runtime marker (layer 4)
22
48
 
23
49
  Any agent whose start/early hook learns the real id but cannot inject env can
@@ -33,6 +59,11 @@ stamp a marker that both the resolver and the Codex cleanup hook read:
33
59
  Helpers: `writeRuntimeMarker` / `readRuntimeMarker` in `src/utils/session-id.ts`.
34
60
  Override the dir in tests/hooks via `$SYNTAUR_RUNTIME_SESSIONS_DIR`.
35
61
 
62
+ The launch path also writes **pending** markers (no `sessionId`) at spawn time
63
+ for fresh/fork launches — ids are never synthesized. `readRuntimeMarker`
64
+ rejects pending markers, so they never resolve an id until backfilled; the
65
+ scanner registers those sessions from their transcripts instead.
66
+
36
67
  ---
37
68
 
38
69
  ## Claude Code — EXACT (shipped, no new runtime code)
@@ -40,8 +71,10 @@ Override the dir in tests/hooks via `$SYNTAUR_RUNTIME_SESSIONS_DIR`.
40
71
  Native `CLAUDE_CODE_SESSION_ID` env is injected into every child process, so a
41
72
  `syntaur` command is a child and layer 2 hits. Confirmed live:
42
73
  `CLAUDE_CODE_SESSION_ID` and the ancestor-pid file `~/.claude/sessions/<pid>.json`
43
- resolve to the same id. The SessionStart hook still mirrors the id into
44
- `context.json` as a legacy hint (back-compat). **Fixes the reported bug.**
74
+ resolve to the same id. The SessionStart hook (now a thin wrapper over
75
+ `syntaur session register --from-hook`) registers EVERY session directly in the
76
+ DB and still mirrors the id into `context.json` as a legacy hint (back-compat).
77
+ **Fixes the reported bug.**
45
78
 
46
79
  ## OpenCode — injector ships as a plugin (live-build gate)
47
80
 
@@ -109,9 +142,13 @@ id on an existing hook's stdin. If/when it does, an early hook can
109
142
  Codex cleanup hook will resolve it exactly.
110
143
 
111
144
  **Honest floor today:**
145
+ - The **session scanner** is Codex's tracking path: its `sessions` descriptor
146
+ walks `~/.codex/sessions/**/rollout-*.jsonl` and registers every session with
147
+ real timestamps — no hook required.
112
148
  - `session-cleanup.sh` no longer trusts the clobbered scalar; it resolves only
113
- from an exact runtime marker and otherwise **skips** (the dashboard liveness
114
- reaper marks the dead session stopped). It never mis-stops a co-tenant.
149
+ from an exact runtime marker and otherwise **skips** (the scanner's sweep
150
+ marks the dead session stopped on its next tick). It never mis-stops a
151
+ co-tenant.
115
152
  - For attribution that must be exact now, pass explicit `--session-id`
116
153
  (sourced from `payload.id` of the matching `~/.codex/sessions/.../rollout-*.jsonl`,
117
154
  as `platforms/codex/scripts/resolve-session.sh` already does on a cwd basis).
@@ -5,7 +5,7 @@
5
5
  "name": "Brennen",
6
6
  "email": ""
7
7
  },
8
- "version": "0.45.0",
8
+ "version": "0.47.0",
9
9
  "skills": [
10
10
  "./skills/syntaur-protocol",
11
11
  "./skills/grab-assignment",
@@ -1,72 +1,33 @@
1
1
  #!/usr/bin/env bash
2
- # Syntaur SessionEnd Hook
3
- # Logs and marks agent sessions as "stopped" when a Claude Code session exits.
4
- # If the session was never registered but has an active assignment, registers it first.
5
- # Reads JSON from stdin, always exits 0.
2
+ # Syntaur SessionEnd Hook — thin wrapper around `syntaur session stop`.
3
+ # The CLI resolves the ENDING session's id (stdin .session_id first, the shared
4
+ # context.json scalar only as a fallback) and marks the row stopped with a
5
+ # direct DB write. No dashboard required. Reads JSON from stdin, always exits 0.
6
6
 
7
- # --- Safety: never fail ---
8
7
  set -o pipefail 2>/dev/null || true
9
8
 
10
- # --- Step 1: Check for jq ---
11
- if ! command -v jq &>/dev/null; then
12
- exit 0
13
- fi
9
+ command -v jq >/dev/null 2>&1 || exit 0
14
10
 
15
- # --- Step 2: Read stdin ---
16
11
  INPUT=$(cat)
17
- if [ -z "$INPUT" ]; then
18
- exit 0
19
- fi
20
-
21
- # --- Step 3: Find context file ---
22
- CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
23
- if [ -z "$CWD" ]; then
24
- exit 0
25
- fi
26
-
27
- CONTEXT_FILE="$CWD/.syntaur/context.json"
28
- if [ ! -f "$CONTEXT_FILE" ]; then
29
- exit 0
30
- fi
31
-
32
- # --- Step 4: Resolve the ENDING session's id ---
33
- # Prefer the exact, per-process id from the SessionEnd stdin payload (Claude
34
- # Code's contract passes .session_id). The context.json scalar is shared mutable
35
- # state a co-tenant can clobber, so it is only a last-resort fallback — reading
36
- # it first would mark the WRONG session stopped when two sessions share a
37
- # workspace.
38
- SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
39
- if [ -z "$SESSION_ID" ]; then
40
- SESSION_ID=$(jq -r '.sessionId // empty' "$CONTEXT_FILE" 2>/dev/null)
41
- fi
42
- MISSION_SLUG=$(jq -r '.projectSlug // empty' "$CONTEXT_FILE" 2>/dev/null)
43
- ASSIGNMENT_SLUG=$(jq -r '.assignmentSlug // empty' "$CONTEXT_FILE" 2>/dev/null)
44
-
45
- # No real session id available — exit quietly. We never synthesize one.
46
- [ -z "$SESSION_ID" ] && exit 0
47
-
48
- # Defensive: the id becomes a URL path segment — reject anything that isn't a
49
- # plain id (UUID/ULID charset). Real Claude session ids never trip this.
50
- case "$SESSION_ID" in
51
- *[!A-Za-z0-9_-]*) exit 0 ;;
52
- esac
53
-
54
- # --- Dashboard endpoint resolution (mirror session-start.sh exactly so start
55
- # and end hooks always target the same host:port) ---
56
- PORT="${SYNTAUR_DASHBOARD_PORT:-}"
57
- if [ -z "$PORT" ]; then
58
- PORT=$(cat "$HOME/.syntaur/dashboard-port" 2>/dev/null || echo "4800")
59
- fi
60
-
61
- # --- Step 5: Mark session as stopped via dashboard API ---
62
- BODY="{\"status\": \"stopped\"}"
63
- if [ -n "$MISSION_SLUG" ]; then
64
- BODY="{\"status\": \"stopped\", \"projectSlug\": \"${MISSION_SLUG}\"}"
65
- fi
66
-
67
- curl -sf --max-time 3 -X PATCH "http://127.0.0.1:${PORT}/api/agent-sessions/${SESSION_ID}/status" \
68
- -H "Content-Type: application/json" \
69
- -d "$BODY" \
70
- -o /dev/null 2>/dev/null || true
12
+ [ -z "$INPUT" ] && exit 0
13
+
14
+ command -v syntaur >/dev/null 2>&1 || exit 0
15
+
16
+ # Bounded SIGKILL watchdog (portable no `timeout` on stock macOS). ~4s stays
17
+ # under the hook's `timeout: 5` budget. A stale CLI without the subcommand
18
+ # exits non-zero swallowed; the scanner sweeps the row on its next tick.
19
+ syntaur_bounded_stop() {
20
+ local cpid kpid rc
21
+ printf '%s' "$INPUT" | syntaur session stop --from-hook >/dev/null 2>&1 &
22
+ cpid=$!
23
+ ( sleep 4; kill -KILL "$cpid" 2>/dev/null ) >/dev/null 2>&1 &
24
+ kpid=$!
25
+ wait "$cpid" 2>/dev/null
26
+ rc=$?
27
+ kill -KILL "$kpid" 2>/dev/null
28
+ wait "$kpid" 2>/dev/null
29
+ return "$rc"
30
+ }
31
+ syntaur_bounded_stop || true
71
32
 
72
33
  exit 0
@@ -1,11 +1,11 @@
1
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.jsonthat 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.
2
+ # Syntaur SessionStart Hook — thin wrapper around `syntaur session register`.
3
+ #
4
+ # Registers EVERY session (standalone sessions included no context.json
5
+ # required). The CLI does the deterministic work: parses the stdin payload,
6
+ # merges session fields into an EXISTING .syntaur/context.json (never creates
7
+ # one), and writes the session row directly to the sessions DB. No dashboard
8
+ # required.
9
9
  #
10
10
  # Reads JSON from stdin per Claude Code SessionStart contract:
11
11
  # { "session_id": "...", "transcript_path": "...", "cwd": "...", ... }
@@ -19,20 +19,26 @@ command -v jq >/dev/null 2>&1 || exit 0
19
19
  INPUT=$(cat)
20
20
  [ -z "$INPUT" ] && exit 0
21
21
 
22
- # Run `syntaur --version` with a PORTABLE ~1s watchdog (background + kill) so it
23
- # is bounded even where `timeout`/`gtimeout` are absent (stock macOS). Prints the
24
- # trimmed version on success; prints nothing and returns non-zero if it hangs or
25
- # fails. 1s + the dashboard curl's --max-time 3 stays under the hook's 5s budget.
26
- syntaur_bounded_version() {
22
+ # Run a syntaur CLI invocation with a PORTABLE SIGKILL watchdog (background +
23
+ # kill) so it is bounded even where `timeout`/`gtimeout` are absent (stock
24
+ # macOS). $1 = deadline in seconds; remaining args = the syntaur subcommand.
25
+ # Stdin is forwarded; stdout is captured to a temp file and printed on success.
26
+ # Returns non-zero if the CLI is absent, hangs past the deadline, or fails —
27
+ # including a stale installed CLI that predates the subcommand.
28
+ syntaur_bounded() {
27
29
  command -v syntaur >/dev/null 2>&1 || return 1
28
- local out cpid kpid rc
29
- out="${TMPDIR:-/tmp}/syntaur-ver.$$"
30
- syntaur --version >"$out" 2>/dev/null &
30
+ local deadline out cpid kpid rc
31
+ deadline=$1
32
+ shift
33
+ out="${TMPDIR:-/tmp}/syntaur-hook.$$"
34
+ # `<&0` forwards the caller's stdin explicitly — background commands default
35
+ # to stdin-from-/dev/null in non-interactive shells, which would silently
36
+ # drop the piped hook payload.
37
+ syntaur "$@" <&0 >"$out" 2>/dev/null &
31
38
  cpid=$!
32
39
  # Hard deadline via SIGKILL (uncatchable — a TERM-ignoring or hung CLI cannot
33
- # block us) at ~1s, guaranteeing the `wait` below returns. `--version` is a
34
- # node process with no descendants in practice, so killing it is complete.
35
- ( sleep 1; kill -KILL "$cpid" 2>/dev/null ) >/dev/null 2>&1 &
40
+ # block us), guaranteeing the `wait` below returns.
41
+ ( sleep "$deadline"; kill -KILL "$cpid" 2>/dev/null ) >/dev/null 2>&1 &
36
42
  kpid=$!
37
43
  wait "$cpid" 2>/dev/null
38
44
  rc=$?
@@ -40,7 +46,7 @@ syntaur_bounded_version() {
40
46
  kill -KILL "$kpid" 2>/dev/null
41
47
  wait "$kpid" 2>/dev/null
42
48
  if [ "$rc" -eq 0 ]; then
43
- tr -d '[:space:]' <"$out" 2>/dev/null
49
+ cat "$out" 2>/dev/null
44
50
  rm -f "$out"
45
51
  return 0
46
52
  fi
@@ -50,15 +56,14 @@ syntaur_bounded_version() {
50
56
 
51
57
  # --- Best-effort, non-blocking: warn when the installed plugin is stale vs the
52
58
  # running CLI (the CLI updates via npm; the marketplace plugin copy does not).
53
- # Plugin-global, so it runs BEFORE the context-dependent early exits below. Any
54
- # failure → do nothing. NEVER changes the exit status (the hook always exits 0).
59
+ # Any failure do nothing. NEVER changes the exit status (always exits 0).
55
60
  syntaur_plugin_drift_warn() {
56
61
  local marker marker_ver cli_ver msg
57
62
  marker="${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/syntaur}/.syntaur-install.json"
58
63
  [ -f "$marker" ] || return 0
59
64
  marker_ver=$(jq -r '.packageVersion // empty' "$marker" 2>/dev/null)
60
65
  [ -n "$marker_ver" ] || return 0
61
- cli_ver=$(syntaur_bounded_version) || return 0
66
+ cli_ver=$(syntaur_bounded 1 --version | tr -d '[:space:]') || return 0
62
67
  [ -n "$cli_ver" ] || return 0
63
68
  [ "$marker_ver" = "$cli_ver" ] && return 0
64
69
  msg="Syntaur plugin v${marker_ver} differs from the installed CLI v${cli_ver} — run \`syntaur install-plugin --force\` to refresh."
@@ -66,100 +71,21 @@ syntaur_plugin_drift_warn() {
66
71
  }
67
72
  syntaur_plugin_drift_warn || true
68
73
 
69
- SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
70
- TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
71
- CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
72
-
73
- [ -z "$SESSION_ID" ] && exit 0
74
- [ -z "$CWD" ] && exit 0
75
-
76
- CONTEXT_FILE="$CWD/.syntaur/context.json"
77
-
78
- # REQUIRED invariant: only operate on an EXISTING context file. If the current
79
- # cwd has no active Syntaur assignment, leave the filesystem untouched.
80
- [ ! -f "$CONTEXT_FILE" ] && exit 0
81
-
82
- # --- (1) Merge session fields into context.json.
83
- # Always replace both sessionId and transcriptPath together. If the incoming
84
- # transcript_path is empty, explicitly null the stored transcriptPath so a new
85
- # session never inherits a stale transcript path from the prior session.
86
- #
87
- # Also resolve the latest session-summary path (mid-assignment continuity) so
88
- # the resuming agent's first protocol-read can pick it up. Selection rule:
89
- # the summary.md with the most recent file mtime under
90
- # <assignmentDir>/sessions/*/summary.md. Null if none exists.
91
- ASSIGNMENT_DIR_RAW=$(jq -r '.assignmentDir // empty' "$CONTEXT_FILE" 2>/dev/null)
92
- ASSIGNMENT_DIR="${ASSIGNMENT_DIR_RAW/#\~/$HOME}"
93
-
94
- LATEST_SUMMARY=""
95
- if [ -n "$ASSIGNMENT_DIR" ] && [ -d "$ASSIGNMENT_DIR/sessions" ]; then
96
- # Prefer GNU-style stat formatting if available (Linux); fall back to BSD
97
- # (macOS). Resolve newest-by-mtime portably.
98
- while IFS= read -r -d '' f; do
99
- if M=$(stat -f '%m' "$f" 2>/dev/null) || M=$(stat -c '%Y' "$f" 2>/dev/null); then
100
- printf '%s\t%s\n' "$M" "$f"
101
- fi
102
- done < <(find "$ASSIGNMENT_DIR/sessions" -mindepth 2 -maxdepth 2 -type f -name 'summary.md' -print0 2>/dev/null) \
103
- | sort -rn -k1,1 \
104
- | head -1 \
105
- | cut -f2- > "${CONTEXT_FILE}.summary.tmp.$$" 2>/dev/null
106
- LATEST_SUMMARY=$(cat "${CONTEXT_FILE}.summary.tmp.$$" 2>/dev/null || true)
107
- rm -f "${CONTEXT_FILE}.summary.tmp.$$"
108
- fi
109
-
110
- TMP="${CONTEXT_FILE}.tmp.$$"
111
- jq \
112
- --arg sid "$SESSION_ID" \
113
- --arg tp "$TRANSCRIPT_PATH" \
114
- --arg lsp "$LATEST_SUMMARY" \
115
- '. + {sessionId: $sid, transcriptPath: (if ($tp | length) > 0 then $tp else null end), latestSessionSummaryPath: (if ($lsp | length) > 0 then $lsp else null end)}' \
116
- "$CONTEXT_FILE" > "$TMP" 2>/dev/null \
117
- && mv "$TMP" "$CONTEXT_FILE" 2>/dev/null \
118
- || rm -f "$TMP"
119
-
120
- # --- (2) Best-effort pre-registration in the dashboard.
121
- # Read project/assignment context if present so the pre-registered row is
122
- # already linked. Upsert semantics on the server mean this is idempotent with
123
- # later /track-session or grab-assignment calls.
124
- MISSION_SLUG=$(jq -r '.projectSlug // empty' "$CONTEXT_FILE" 2>/dev/null)
125
- ASSIGNMENT_SLUG=$(jq -r '.assignmentSlug // empty' "$CONTEXT_FILE" 2>/dev/null)
126
-
127
74
  # Capture the terminal-session PID that owns this Claude process. Claude
128
75
  # Code's SessionStart payload does NOT include a parent PID, so we approximate
129
- # by walking up one level from the hook's own PID — that is, the shell the
130
- # hook was spawned by, which is the shell that owns claude. The server uses
131
- # this for liveness checks (and an auto-captured ps lstart= for recycling
132
- # defense). If `ps` is unavailable or returns nothing we just omit the field.
76
+ # by walking up one level from the hook's own PID — the shell that owns claude.
133
77
  PID="$(ps -o ppid= -p $$ 2>/dev/null | tr -d '[:space:]' || true)"
134
78
  if [ -n "$PID" ] && ! printf '%s' "$PID" | grep -q '^[0-9]\+$'; then
135
79
  PID=""
136
80
  fi
137
81
 
138
- PORT="${SYNTAUR_DASHBOARD_PORT:-}"
139
- if [ -z "$PORT" ]; then
140
- PORT=$(cat "$HOME/.syntaur/dashboard-port" 2>/dev/null || echo "4800")
141
- fi
142
-
143
- BODY=$(jq -cn \
144
- --arg sid "$SESSION_ID" \
145
- --arg tp "$TRANSCRIPT_PATH" \
146
- --arg proj "$MISSION_SLUG" \
147
- --arg assn "$ASSIGNMENT_SLUG" \
148
- --arg path "$CWD" \
149
- --arg pid "$PID" \
150
- '{ agent: "claude", sessionId: $sid, path: $path }
151
- + (if ($tp | length) > 0 then {transcriptPath: $tp} else {} end)
152
- + (if ($proj | length) > 0 then {projectSlug: $proj} else {} end)
153
- + (if ($assn | length) > 0 then {assignmentSlug: $assn} else {} end)
154
- + (if ($pid | length) > 0 then {pid: ($pid | tonumber)} else {} end)' 2>/dev/null)
155
-
156
- if [ -n "$BODY" ]; then
157
- # --max-time bounds the hook's wall-clock cost if the dashboard socket
158
- # accepts but then hangs. The hook itself is registered with timeout: 5.
159
- curl -sf --max-time 3 -X POST "http://127.0.0.1:${PORT}/api/agent-sessions" \
160
- -H "Content-Type: application/json" \
161
- -d "$BODY" \
162
- -o /dev/null 2>/dev/null || true
82
+ # Register EVERY session via the CLI (context.json merge + direct DB write).
83
+ # ~4s deadline stays under the hook's `timeout: 5` budget. A stale CLI without
84
+ # the subcommand exits non-zero swallowed; the scanner is the safety net.
85
+ if [ -n "$PID" ]; then
86
+ printf '%s' "$INPUT" | syntaur_bounded 4 session register --from-hook --pid "$PID" >/dev/null 2>&1 || true
87
+ else
88
+ printf '%s' "$INPUT" | syntaur_bounded 4 session register --from-hook >/dev/null 2>&1 || true
163
89
  fi
164
90
 
165
91
  exit 0
@@ -5,84 +5,36 @@ description: Use when the user asks to track, register, or log this Claude Code
5
5
 
6
6
  # Track Session
7
7
 
8
- Register the current Claude Code session as an agent session in the Syntaur dashboard. Works standalone or linked to a project/assignment.
8
+ Attach a description and/or a project+assignment link to the current agent session's row in the Syntaur dashboard.
9
9
 
10
- 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.
10
+ Plain registration is automatic now — the SessionStart hook registers every session (and the background scanner backfills any the hook missed), so this skill only matters for the one remaining manual case: adding a description or an explicit project/assignment link. The CLI self-resolves the calling session's id (env → process-tree markers → transcript scan); never pass a synthesized id.
11
11
 
12
12
  ## Usage
13
13
 
14
14
  User arguments: `$ARGUMENTS`
15
15
 
16
- - (no args) — register a standalone session
17
- - `--description "<text>"` — with a description
18
- - `--project <slug> --assignment <slug>` — linked to a project
16
+ - (no args) — upsert the session row as-is (rarely needed; the hook already did this)
17
+ - `--description "<text>"` — attach a description
18
+ - `--project <slug> --assignment <slug>` — link to a project assignment
19
19
  - `--description "<text>" --project <slug> --assignment <slug>` — both
20
20
 
21
21
  ## Workflow
22
22
 
23
- ### Step 1: Parse arguments
24
-
25
- Extract optional flags from the argument string:
26
- - `--description "<text>"` or `--description <text>` — session description
27
- - `--project <slug>` — project to link to
28
- - `--assignment <slug>` — assignment to link to
29
-
30
- ### Step 2: Source the real session id + transcript path
31
-
32
- Resolve the session id from *your* running process, in priority order:
33
-
34
- 1. `$CLAUDE_CODE_SESSION_ID` (or the peer `OPENCODE_SESSION_ID` / `PI_SESSION_ID`) if your runtime injects it.
35
- 2. Otherwise, read the most-recently-modified file under `~/.claude/sessions/<pid>.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.
36
- 3. Only as a last resort, fall back to the `sessionId` scalar in `.syntaur/context.json` (and the companion `transcriptPath` if present). This scalar is a shared, legacy hint a co-tenant sharing this workspace can clobber — never treat it as authoritative.
37
- 4. If no 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."
38
-
39
- DO NOT generate a UUID. `syntaur track-session` rejects missing session IDs.
40
-
41
- ### Step 3: Run the CLI command
42
-
43
- Run the track-session CLI via Bash (use `dangerouslyDisableSandbox: true` since it writes to `~/.syntaur/`):
23
+ Run one Bash call (use `dangerouslyDisableSandbox: true` since it writes to `~/.syntaur/`), passing through whatever optional flags the user gave:
44
24
 
45
25
  ```bash
46
- syntaur track-session \
47
- --agent claude \
48
- --session-id "$SESSION_ID" \
49
- --transcript-path "$TRANSCRIPT_PATH" \
50
- --path "$(pwd)" \
51
- --pid "$(ps -o ppid= -p $$ | tr -d ' ')" \
26
+ syntaur track-session --agent claude \
52
27
  [--description "<text>"] \
53
- [--project <slug>] \
54
- [--assignment <slug>]
28
+ [--project <slug>] [--assignment <slug>]
55
29
  ```
56
30
 
57
- Omit `--transcript-path` entirely (don't pass an empty string) if no transcript path could be resolved. The `--pid` value is the shell PID that owns the Claude process — the dashboard uses it to disable Resume while this session may still be writing the transcript, forcing users to Fork instead. If `ps` is unavailable, omit `--pid` too.
31
+ The CLI resolves the session id, transcript-derived path, owning pid, and HEAD sha itself, and prints one of:
58
32
 
59
- The CLI prints one of:
60
33
  - `Registered standalone agent session <sessionId>.`
61
34
  - `Registered agent session <sessionId> for <assignment> in <project>.`
62
35
 
63
- Registration is idempotent — re-running the command with the same session id safely upserts project/assignment/description onto the existing row.
64
-
65
- ### Step 4: Merge context.json
66
-
67
- Ensure `.syntaur/context.json` has the session fields (so SessionEnd and future `track-session` runs find them). Merge, don't overwrite:
68
-
69
- ```bash
70
- mkdir -p .syntaur
71
- if [ -f .syntaur/context.json ]; then
72
- jq --arg sid "$SESSION_ID" --arg tp "$TRANSCRIPT_PATH" \
73
- '. + {sessionId: $sid} + (if ($tp | length) > 0 then {transcriptPath: $tp} else {} end)' \
74
- .syntaur/context.json > .syntaur/context.json.tmp \
75
- && mv .syntaur/context.json.tmp .syntaur/context.json
76
- else
77
- jq -n --arg sid "$SESSION_ID" --arg tp "$TRANSCRIPT_PATH" \
78
- '{sessionId: $sid} + (if ($tp | length) > 0 then {transcriptPath: $tp} else {} end)' \
79
- > .syntaur/context.json
80
- fi
81
- ```
36
+ Registration is idempotent — re-running with the same session id safely upserts the description/link onto the existing row.
82
37
 
83
- ### Step 5: Confirm
38
+ If it errors with "Could not resolve a session id", restart the Claude session so the SessionStart hook can register it (or pass `--session-id <id>` with a real agent-generated id — never synthesize one).
84
39
 
85
- Tell the user:
86
- - The session was registered (include the short session id).
87
- - It will be auto-stopped when this conversation ends via the SessionEnd hook.
88
- - If linked to a project, mention which project/assignment.
40
+ Confirm to the user: the row was updated (include the short session id), it auto-stops at SessionEnd, and which project/assignment it is linked to, if any.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntaur",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "Run Syntaur project and assignment workflows from Codex, including claiming work, planning, completing handoffs, session/server tracking, save-session-summary continuity, and write-boundary enforcement.",
5
5
  "author": {
6
6
  "name": "Brennen"
@@ -5,84 +5,36 @@ description: Use when the user asks to track, register, or log this Claude Code
5
5
 
6
6
  # Track Session
7
7
 
8
- Register the current Claude Code session as an agent session in the Syntaur dashboard. Works standalone or linked to a project/assignment.
8
+ Attach a description and/or a project+assignment link to the current agent session's row in the Syntaur dashboard.
9
9
 
10
- 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.
10
+ Plain registration is automatic now — the SessionStart hook registers every session (and the background scanner backfills any the hook missed), so this skill only matters for the one remaining manual case: adding a description or an explicit project/assignment link. The CLI self-resolves the calling session's id (env → process-tree markers → transcript scan); never pass a synthesized id.
11
11
 
12
12
  ## Usage
13
13
 
14
14
  User arguments: `$ARGUMENTS`
15
15
 
16
- - (no args) — register a standalone session
17
- - `--description "<text>"` — with a description
18
- - `--project <slug> --assignment <slug>` — linked to a project
16
+ - (no args) — upsert the session row as-is (rarely needed; the hook already did this)
17
+ - `--description "<text>"` — attach a description
18
+ - `--project <slug> --assignment <slug>` — link to a project assignment
19
19
  - `--description "<text>" --project <slug> --assignment <slug>` — both
20
20
 
21
21
  ## Workflow
22
22
 
23
- ### Step 1: Parse arguments
24
-
25
- Extract optional flags from the argument string:
26
- - `--description "<text>"` or `--description <text>` — session description
27
- - `--project <slug>` — project to link to
28
- - `--assignment <slug>` — assignment to link to
29
-
30
- ### Step 2: Source the real session id + transcript path
31
-
32
- Resolve the session id from *your* running process, in priority order:
33
-
34
- 1. `$CLAUDE_CODE_SESSION_ID` (or the peer `OPENCODE_SESSION_ID` / `PI_SESSION_ID`) if your runtime injects it.
35
- 2. Otherwise, read the most-recently-modified file under `~/.claude/sessions/<pid>.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.
36
- 3. Only as a last resort, fall back to the `sessionId` scalar in `.syntaur/context.json` (and the companion `transcriptPath` if present). This scalar is a shared, legacy hint a co-tenant sharing this workspace can clobber — never treat it as authoritative.
37
- 4. If no 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."
38
-
39
- DO NOT generate a UUID. `syntaur track-session` rejects missing session IDs.
40
-
41
- ### Step 3: Run the CLI command
42
-
43
- Run the track-session CLI via Bash (use `dangerouslyDisableSandbox: true` since it writes to `~/.syntaur/`):
23
+ Run one Bash call (use `dangerouslyDisableSandbox: true` since it writes to `~/.syntaur/`), passing through whatever optional flags the user gave:
44
24
 
45
25
  ```bash
46
- syntaur track-session \
47
- --agent claude \
48
- --session-id "$SESSION_ID" \
49
- --transcript-path "$TRANSCRIPT_PATH" \
50
- --path "$(pwd)" \
51
- --pid "$(ps -o ppid= -p $$ | tr -d ' ')" \
26
+ syntaur track-session --agent claude \
52
27
  [--description "<text>"] \
53
- [--project <slug>] \
54
- [--assignment <slug>]
28
+ [--project <slug>] [--assignment <slug>]
55
29
  ```
56
30
 
57
- Omit `--transcript-path` entirely (don't pass an empty string) if no transcript path could be resolved. The `--pid` value is the shell PID that owns the Claude process — the dashboard uses it to disable Resume while this session may still be writing the transcript, forcing users to Fork instead. If `ps` is unavailable, omit `--pid` too.
31
+ The CLI resolves the session id, transcript-derived path, owning pid, and HEAD sha itself, and prints one of:
58
32
 
59
- The CLI prints one of:
60
33
  - `Registered standalone agent session <sessionId>.`
61
34
  - `Registered agent session <sessionId> for <assignment> in <project>.`
62
35
 
63
- Registration is idempotent — re-running the command with the same session id safely upserts project/assignment/description onto the existing row.
64
-
65
- ### Step 4: Merge context.json
66
-
67
- Ensure `.syntaur/context.json` has the session fields (so SessionEnd and future `track-session` runs find them). Merge, don't overwrite:
68
-
69
- ```bash
70
- mkdir -p .syntaur
71
- if [ -f .syntaur/context.json ]; then
72
- jq --arg sid "$SESSION_ID" --arg tp "$TRANSCRIPT_PATH" \
73
- '. + {sessionId: $sid} + (if ($tp | length) > 0 then {transcriptPath: $tp} else {} end)' \
74
- .syntaur/context.json > .syntaur/context.json.tmp \
75
- && mv .syntaur/context.json.tmp .syntaur/context.json
76
- else
77
- jq -n --arg sid "$SESSION_ID" --arg tp "$TRANSCRIPT_PATH" \
78
- '{sessionId: $sid} + (if ($tp | length) > 0 then {transcriptPath: $tp} else {} end)' \
79
- > .syntaur/context.json
80
- fi
81
- ```
36
+ Registration is idempotent — re-running with the same session id safely upserts the description/link onto the existing row.
82
37
 
83
- ### Step 5: Confirm
38
+ If it errors with "Could not resolve a session id", restart the Claude session so the SessionStart hook can register it (or pass `--session-id <id>` with a real agent-generated id — never synthesize one).
84
39
 
85
- Tell the user:
86
- - The session was registered (include the short session id).
87
- - It will be auto-stopped when this conversation ends via the SessionEnd hook.
88
- - If linked to a project, mention which project/assignment.
40
+ Confirm to the user: the row was updated (include the short session id), it auto-stops at SessionEnd, and which project/assignment it is linked to, if any.