syntaur 0.70.0 → 0.71.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 (136) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-DKk6tHtk.js → _basePickBy-BfpzwNC3.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-DM-f7DWz.js → _baseUniq-wFEHBTCT.js} +1 -1
  4. package/dashboard/dist/assets/{arc-ZBlA3YdV.js → arc-Bdcw4sCg.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BUmvtGTF.js → architectureDiagram-2XIMDMQ5-BessFAtJ.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-B3qxWK6s.js → blockDiagram-WCTKOSBZ-CmjyyZPp.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-BEq_UJO-.js → c4Diagram-IC4MRINW-dT6p8cwc.js} +1 -1
  8. package/dashboard/dist/assets/channel-BOguQ5S4.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-C-Y9ryMm.js → chunk-4BX2VUAB-BlY-k8DY.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-CGdtbsjw.js → chunk-55IACEB6-CtQ0p0jb.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-DllxJhUp.js → chunk-FMBD7UC4-DEevt_Yh.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-jjMM8O5F.js → chunk-JSJVCQXG-CpjLL7Nh.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-B_6BPltQ.js → chunk-KX2RTZJC-BWNRms_o.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-D0hJiXHp.js → chunk-NQ4KR5QH-qo2zbylv.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-BCWo4hLS.js → chunk-QZHKN3VN-DRvojtzc.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-DH_jEAwg.js → chunk-WL4C6EOR-A40iPU5N.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BLSHooma.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BLSHooma.js +1 -0
  19. package/dashboard/dist/assets/clone-iSEXw5tA.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-5ld00TOH.js → cose-bilkent-S5V4N54A-CYaO7nsk.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-Cnu6eQWy.js → dagre-KLK3FWXG-NX3inwVZ.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-_CBBKNP-.js → diagram-E7M64L7V-D9cJwhQW.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-DE6WjIb1.js → diagram-IFDJBPK2-Bo5sFrMg.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-DpW7UzNK.js → diagram-P4PSJMXO-DfRH0XU7.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-BD2409fE.js → erDiagram-INFDFZHY-BgP1bwgw.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-1p0khhFI.js → flowDiagram-PKNHOUZH-C8Wws1ci.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-B2zFyA4s.js → ganttDiagram-A5KZAMGK-BgJhUuj8.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-bH-4YH7h.js → gitGraphDiagram-K3NZZRJ6-CI9FgCgo.js} +1 -1
  29. package/dashboard/dist/assets/{graph-BT24B6iQ.js → graph-B_7GyeiA.js} +1 -1
  30. package/dashboard/dist/assets/{index-Cxqr3rQB.js → index-CbndxFlO.js} +3 -3
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-CMzP4Hcg.js → infoDiagram-LFFYTUFH-BEtZcRxI.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-DMEmFC7M.js → ishikawaDiagram-PHBUUO56-0o9Bksuc.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CAtzYQUm.js → journeyDiagram-4ABVD52K-RyRU3as7.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-d1JVbtvX.js → kanban-definition-K7BYSVSG-hLrHHizB.js} +1 -1
  35. package/dashboard/dist/assets/{layout-BVuI38I_.js → layout-w458wydd.js} +1 -1
  36. package/dashboard/dist/assets/{linear-Bc8PGMbp.js → linear-CnRlR5_w.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-UtwFYLNj.js → mermaid.core-DJ_9ty0Y.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-DHc5RCDj.js → mindmap-definition-YRQLILUH-C7TIzJVC.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-9anIsdIA.js → pieDiagram-SKSYHLDU-BZ9ysteM.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-FZ0D9HnU.js → quadrantDiagram-337W2JSQ-DpaMVSO1.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BkjCvH_u.js → requirementDiagram-Z7DCOOCP-EXFcskZZ.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DzqwYHDo.js → sankeyDiagram-WA2Y5GQK-BdImdXkY.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BW4g5Ao-.js → sequenceDiagram-2WXFIKYE-CWN3faYf.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D0tKU3Z0.js → stateDiagram-RAJIS63D-DIQdn-BF.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-1u62oWzq.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-Bu269QDX.js → timeline-definition-YZTLITO2-DMTIpUOE.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-BGu_rrva.js → treemap-KZPCXAKY-Y2AH2GB7.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Cx_n5FRZ.js → vennDiagram-LZ73GAT5-BF4bJK-l.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BOJsKV_W.js → xychartDiagram-JWTSCODW-DH-tOBC5.js} +1 -1
  50. package/dashboard/dist/index.html +1 -1
  51. package/dist/dashboard/server.js +1384 -540
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +2769 -1535
  54. package/dist/index.js.map +1 -1
  55. package/dist/launch/index.js +594 -109
  56. package/dist/launch/index.js.map +1 -1
  57. package/package.json +1 -1
  58. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  59. package/platforms/claude-code/hooks/enforce-boundaries.sh +49 -16
  60. package/platforms/claude-code/hooks/session-cleanup.sh +4 -2
  61. package/platforms/claude-code/skills/add-memory/SKILL.md +3 -2
  62. package/platforms/claude-code/skills/add-resource/SKILL.md +3 -2
  63. package/platforms/claude-code/skills/bundle-worktree/SKILL.md +3 -3
  64. package/platforms/claude-code/skills/capture-artifacts/SKILL.md +2 -2
  65. package/platforms/claude-code/skills/claim-resource/SKILL.md +3 -3
  66. package/platforms/claude-code/skills/clear-assignment/SKILL.md +9 -27
  67. package/platforms/claude-code/skills/complete-assignment/SKILL.md +3 -3
  68. package/platforms/claude-code/skills/create-assignment/SKILL.md +2 -2
  69. package/platforms/claude-code/skills/extend-resource/SKILL.md +2 -2
  70. package/platforms/claude-code/skills/grab-assignment/SKILL.md +17 -12
  71. package/platforms/claude-code/skills/grab-bundle/SKILL.md +10 -6
  72. package/platforms/claude-code/skills/log-progress/SKILL.md +6 -4
  73. package/platforms/claude-code/skills/plan-assignment/SKILL.md +3 -3
  74. package/platforms/claude-code/skills/plan-bundle/SKILL.md +4 -3
  75. package/platforms/claude-code/skills/replan/SKILL.md +9 -6
  76. package/platforms/claude-code/skills/resume-session/SKILL.md +14 -8
  77. package/platforms/claude-code/skills/save-session-summary/SKILL.md +4 -4
  78. package/platforms/claude-code/skills/set-workspace/SKILL.md +9 -6
  79. package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +9 -7
  80. package/platforms/claude-code/skills/syntaur-protocol/references/file-ownership.md +1 -1
  81. package/platforms/claude-code/skills/syntaur-worktree/SKILL.md +11 -7
  82. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  83. package/platforms/codex/scripts/enforce-boundaries.sh +74 -12
  84. package/platforms/codex/scripts/session-cleanup.sh +20 -13
  85. package/platforms/codex/skills/add-memory/SKILL.md +3 -2
  86. package/platforms/codex/skills/add-resource/SKILL.md +3 -2
  87. package/platforms/codex/skills/bundle-worktree/SKILL.md +3 -3
  88. package/platforms/codex/skills/capture-artifacts/SKILL.md +2 -2
  89. package/platforms/codex/skills/claim-resource/SKILL.md +3 -3
  90. package/platforms/codex/skills/clear-assignment/SKILL.md +9 -27
  91. package/platforms/codex/skills/complete-assignment/SKILL.md +3 -3
  92. package/platforms/codex/skills/create-assignment/SKILL.md +2 -2
  93. package/platforms/codex/skills/extend-resource/SKILL.md +2 -2
  94. package/platforms/codex/skills/grab-assignment/SKILL.md +17 -12
  95. package/platforms/codex/skills/grab-bundle/SKILL.md +10 -6
  96. package/platforms/codex/skills/log-progress/SKILL.md +6 -4
  97. package/platforms/codex/skills/plan-assignment/SKILL.md +3 -3
  98. package/platforms/codex/skills/plan-bundle/SKILL.md +4 -3
  99. package/platforms/codex/skills/replan/SKILL.md +9 -6
  100. package/platforms/codex/skills/resume-session/SKILL.md +14 -8
  101. package/platforms/codex/skills/save-session-summary/SKILL.md +4 -4
  102. package/platforms/codex/skills/set-workspace/SKILL.md +9 -6
  103. package/platforms/codex/skills/syntaur-protocol/SKILL.md +9 -7
  104. package/platforms/codex/skills/syntaur-protocol/references/file-ownership.md +1 -1
  105. package/platforms/codex/skills/syntaur-worktree/SKILL.md +11 -7
  106. package/platforms/hermes/plugins/syntaur/__init__.py +29 -3
  107. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  108. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  109. package/platforms/hermes/plugins/syntaur/boundary.py +18 -9
  110. package/platforms/pi/extensions/syntaur/index.ts +84 -29
  111. package/skills/add-memory/SKILL.md +3 -2
  112. package/skills/add-resource/SKILL.md +3 -2
  113. package/skills/bundle-worktree/SKILL.md +3 -3
  114. package/skills/capture-artifacts/SKILL.md +2 -2
  115. package/skills/claim-resource/SKILL.md +3 -3
  116. package/skills/clear-assignment/SKILL.md +9 -27
  117. package/skills/complete-assignment/SKILL.md +3 -3
  118. package/skills/create-assignment/SKILL.md +2 -2
  119. package/skills/extend-resource/SKILL.md +2 -2
  120. package/skills/grab-assignment/SKILL.md +17 -12
  121. package/skills/grab-bundle/SKILL.md +10 -6
  122. package/skills/log-progress/SKILL.md +6 -4
  123. package/skills/plan-assignment/SKILL.md +3 -3
  124. package/skills/plan-bundle/SKILL.md +4 -3
  125. package/skills/replan/SKILL.md +9 -6
  126. package/skills/resume-session/SKILL.md +14 -8
  127. package/skills/save-session-summary/SKILL.md +4 -4
  128. package/skills/set-workspace/SKILL.md +9 -6
  129. package/skills/syntaur-protocol/SKILL.md +9 -7
  130. package/skills/syntaur-protocol/references/file-ownership.md +1 -1
  131. package/skills/syntaur-worktree/SKILL.md +11 -7
  132. package/dashboard/dist/assets/channel-fypxffzQ.js +0 -1
  133. package/dashboard/dist/assets/classDiagram-VBA2DB6C-1KnjQvtL.js +0 -1
  134. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-1KnjQvtL.js +0 -1
  135. package/dashboard/dist/assets/clone-CKMabBhS.js +0 -1
  136. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DxWQhjNO.js +0 -1
@@ -38,12 +38,15 @@ todos are never deleted.**
38
38
 
39
39
  ## Step 1: Verify there is an active assignment
40
40
 
41
- Read `.syntaur/context.json` from the current working directory. Extract
42
- `assignmentSlug`, `projectSlug` (may be null for standalone), and
43
- `assignmentDir`.
44
-
45
- If the file is missing or has no `assignmentSlug`, abort with: "No active
46
- Syntaur assignment. Run `grab-assignment` first." Do not invent values.
41
+ The active assignment is resolved from the session's open engagement. Run
42
+ `syntaur session resume --json` to read the bound `assignmentSlug`,
43
+ `projectSlug` (may be null for standalone), and `assignmentDir`.
44
+ `.syntaur/context.json` is only a workspace marker — do not read the assignment
45
+ from it.
46
+
47
+ If there is no open engagement (no active assignment), abort with: "No active
48
+ assignment for this session — grab one first." Run `grab-assignment` first. Do
49
+ not invent values.
47
50
 
48
51
  ## Step 2: Confirm the prior plan was implemented
49
52
 
@@ -2,8 +2,9 @@
2
2
  name: resume-session
3
3
  description: >-
4
4
  Re-orient a fresh Syntaur session on the active assignment without
5
- re-reading the full transcript. Loads the latest saved session summary,
6
- `.syntaur/context.json`, and any open handoff. Use when the user says
5
+ re-reading the full transcript. Resolves the active assignment from the
6
+ session's open engagement and loads the latest saved session summary and any
7
+ open handoff. Use when the user says
7
8
  "resume", "pick up where we left off", "continue this assignment", or after
8
9
  a compact / new session start. Symmetric counterpart to
9
10
  `/save-session-summary`.
@@ -28,15 +29,20 @@ freely; nothing on disk changes.
28
29
  - Saving the current session's progress — that's `/save-session-summary` (the
29
30
  symmetric write side).
30
31
 
31
- ## Step 1: Verify there is an active context
32
+ ## Step 1: Verify there is an active assignment
32
33
 
33
34
  Run `syntaur session resume`. The CLI:
34
35
 
35
- 1. Reads `.syntaur/context.json` from the current working directory.
36
- 2. Aborts (exit 1) with a clear message if missing or if no `assignmentDir`
37
- is set telling the user to run `grab-assignment` first.
38
- 3. Otherwise scans `<assignmentDir>/sessions/<sid>/summary.md` and picks the
39
- most recently modified.
36
+ 1. Resolves the active assignment from the session's OPEN engagement (the
37
+ assignment this session is currently bound to). `.syntaur/context.json` is
38
+ only a workspace marker it identifies the repository/branch/worktree, not
39
+ the active assignment.
40
+ 2. Aborts (exit 1) with a clear message when there is no open engagement —
41
+ "No active assignment for this session — grab one first" — telling the user
42
+ to run `grab-assignment`.
43
+ 3. Otherwise resolves the assignment dir from the engagement and scans
44
+ `<assignmentDir>/sessions/<sid>/summary.md`, picking the most recently
45
+ modified.
40
46
  4. Reads `<assignmentDir>/handoff.md` (the canonical single-file handoff per
41
47
  assignment, managed by `complete-assignment`) and reports it if its body
42
48
  has been written beyond the scaffolded placeholder.
@@ -26,11 +26,11 @@ mid-assignment continuity** — distinct from `handoff.md`, which is the
26
26
 
27
27
  ## Step 1: Load Context
28
28
 
29
- Read `.syntaur/context.json` from the current working directory.
29
+ The active assignment is resolved from the session's open engagement. Run `syntaur session resume --json` to read it.
30
30
 
31
- If the file does not exist, tell the user: "No active assignment found. Run `grab-assignment` first." and stop.
31
+ If there is no open engagement (no active assignment), tell the user: "No active assignment for this session — grab one first." and stop. `.syntaur/context.json` is only a workspace marker; do not read the assignment from it.
32
32
 
33
- Extract:
33
+ From the resolved engagement, note:
34
34
  - `assignmentDir` (absolute path) — required.
35
35
 
36
36
  **Do not read the session id from `context.json` for identity.** That scalar is
@@ -86,7 +86,7 @@ printf '%s' "$BODY" | syntaur session save
86
86
  # Pass --session-id <id> only to override (e.g. you already have $CLAUDE_CODE_SESSION_ID).
87
87
  ```
88
88
 
89
- Resolves the active assignment from `.syntaur/context.json` (or pass
89
+ Resolves the active assignment from the session's open engagement (or pass
90
90
  `--assignment <slug> [--project <slug>]`). `--session-id` now defaults to the
91
91
  **resolved** session (env → process tree → transcript), falling back to the
92
92
  `context.json` hint only as a last resort — so a co-tenant that clobbered the
@@ -35,11 +35,14 @@ implementation code until workspace fields are set.
35
35
 
36
36
  ## Step 1: Resolve the assignment file
37
37
 
38
- Read `.syntaur/context.json` from the current working directory. Extract
39
- `assignmentDir`. The target file is `<assignmentDir>/assignment.md`.
38
+ The active assignment is resolved from the session's open engagement — `syntaur
39
+ workspace set` (Step 3) targets it automatically. `.syntaur/context.json` is
40
+ only a workspace marker; do not read the assignment from it.
40
41
 
41
- If no context, abort with: "No active assignment. Run `grab-assignment`
42
- first."
42
+ If there is no open engagement (no active assignment), the CLI aborts with "No
43
+ active assignment for this session — grab one first." Run `grab-assignment`
44
+ first, or pass `--assignment <slug> [--project <slug>]` to target one
45
+ explicitly.
43
46
 
44
47
  ## Step 2: Gather inputs
45
48
 
@@ -68,8 +71,8 @@ syntaur workspace set \
68
71
  --parent-branch <parent>
69
72
  ```
70
73
 
71
- Targets the active assignment from `.syntaur/context.json` by default; pass
72
- `--assignment <slug> [--project <slug>]` to target one explicitly. The command
74
+ Targets the active assignment from the session's open engagement by default;
75
+ pass `--assignment <slug> [--project <slug>]` to target one explicitly. The command
73
76
  does the whole safe write in one atomic step:
74
77
 
75
78
  - **Pre-write validation** — runs the same checks as `syntaur doctor
@@ -32,7 +32,7 @@ Respect file ownership boundaries. The Claude Code and Codex plugins enforce the
32
32
  - `~/.syntaur/projects/<project>/resources/<slug>.md`
33
33
  - `~/.syntaur/projects/<project>/memories/<slug>.md`
34
34
  3. **Workspace files** inside the assignment's configured `workspace.worktreePath` / `workspace.repository`.
35
- 4. **Context file:** `.syntaur/context.json` in the current working directory.
35
+ 4. **Workspace marker:** `.syntaur/context.json` in the current working directory (repository/branch/worktree markers plus legacy session/lease bookkeeping — not the active-assignment source of truth).
36
36
 
37
37
  ### Files written only via CLI (never edit directly)
38
38
 
@@ -51,16 +51,18 @@ Per-project `agent.md` / `claude.md` do NOT exist in protocol v2.0. Agent-level
51
51
 
52
52
  ## Current Assignment Context
53
53
 
54
- If `.syntaur/context.json` exists in the current working directory, read it before making changes. Fields you will see:
54
+ The **active assignment** is resolved from the session's OPEN engagement — the assignment this session is currently bound to (established by `syntaur track-session`). It is NOT read from `.syntaur/context.json`. To learn the active assignment, run `syntaur session resume` (or `--json`); to bind a different one, `grab-assignment` (which calls `track-session`).
55
55
 
56
- - `projectSlug` containing project slug (`null` for standalone assignments)
57
- - `assignmentSlug` — assignment slug (for standalone, the UUID is the folder name; slug is display-only)
58
- - `projectDir` — absolute path to the project folder (`null` for standalone)
59
- - `assignmentDir` — absolute path to the assignment folder
56
+ `.syntaur/context.json` is a WORKSPACE MARKER file — it identifies the workspace directory and carries legacy session and resource-lease bookkeeping. It is NOT authoritative for the active assignment. Read it for workspace markers and leases; do NOT treat `projectSlug` / `assignmentSlug` / `assignmentDir` as the active-assignment source of truth (any such scalars are non-authoritative legacy hints). Fields you may see:
57
+
58
+ - `repository` — workspace repository (path or remote URL)
59
+ - `branch` — workspace branch, if known
60
+ - `worktreePath` — absolute path to the worktree, if this workspace is a worktree
60
61
  - `workspaceRoot` — absolute path to the code workspace
61
- - `sessionId` — real agent-runtime session id (never a synthesized UUID)
62
+ - `sessionId` — real agent-runtime session id (legacy hint; never a synthesized UUID, and a co-tenant can clobber it — not authoritative for identity)
62
63
  - `transcriptPath` — absolute path to the agent's rollout/transcript file, if known
63
64
  - `leases` — array of active resource-lease records (managed by `/claim-resource` and `/release-resource`). Entry shape: `{ lease_id, inventory_slug, member_id, expires_at, metadata, claimed_at }`. Leases are NOT auto-released on session end or assignment completion in v1 — call `/release-resource` explicitly (or let the TTL expire).
65
+ - `bundleId` and related bundle fields — present when this workspace is bound to a todo bundle (managed by the bundle skills).
64
66
 
65
67
  ## Required Reading Order
66
68
 
@@ -64,4 +64,4 @@ When working on code (not protocol files), you may write to files within the wor
64
64
 
65
65
  - `workspace.worktreePath` or `workspace.repository` defines your code root.
66
66
  - You may create and edit source files within that workspace.
67
- - The `.syntaur/context.json` context file in your working directory is also writable (merge, don't overwrite — the platform SessionStart hook may have populated `sessionId` and `transcriptPath`).
67
+ - The `.syntaur/context.json` workspace-marker file in your working directory is also writable (merge, don't overwrite — the platform SessionStart hook may have populated `sessionId` and `transcriptPath`). It marks the workspace (repository/branch/worktree); it is not the active-assignment source of truth — the active assignment resolves from the session's open engagement.
@@ -23,8 +23,9 @@ operations:
23
23
  transactionally — rolls back the worktree on write failure).
24
24
  3. `syntaur assign` + `syntaur start` — claim the assignment for this agent
25
25
  and transition to `in_progress`.
26
- 4. Write `.syntaur/context.json` inside the new worktree so the session is
27
- bound there.
26
+ 4. Write `.syntaur/context.json` (a workspace marker) inside the new worktree,
27
+ then `syntaur track-session` to bind the session's engagement to the
28
+ assignment.
28
29
 
29
30
  ## When NOT to use this skill
30
31
 
@@ -82,12 +83,15 @@ syntaur start <assignment> --project <project-slug> # only if status was pendi
82
83
  Skip `start` for any non-`pending` status — never rewind a `review`,
83
84
  `completed`, or `failed` assignment.
84
85
 
85
- ## Step 5: Bind the new worktree to your session
86
+ ## Step 5: Write the workspace marker
86
87
 
87
- `cd` into the new worktree path. Write
88
- `<worktreePath>/.syntaur/context.json` mirroring the format produced by
89
- `/grab-assignment` (projectSlug, assignmentSlug, projectDir, assignmentDir,
90
- workspaceRoot, title, branch, sessionId, transcriptPath, grabbedAt).
88
+ `cd` into the new worktree path. Write `<worktreePath>/.syntaur/context.json`
89
+ mirroring the WORKSPACE-MARKER format produced by `/grab-assignment`:
90
+ `repository`, `branch`, `worktreePath`, `workspaceRoot`, `grabbedAt`, plus
91
+ `sessionId` / `transcriptPath` when known. Do NOT write `projectSlug` /
92
+ `assignmentSlug` / `assignmentDir` / `projectDir` / `title` — context.json is a
93
+ workspace marker, not the active-assignment source. The assignment binds in the
94
+ next step via the session's engagement (`track-session`).
91
95
 
92
96
  If the runtime exposes a real session id (e.g. Claude Code's
93
97
  `~/.claude/sessions/`), include it. Otherwise omit `sessionId` entirely; the
@@ -57,6 +57,24 @@ def _dashboard_port():
57
57
  return "4800"
58
58
 
59
59
 
60
+ def _resolve_boundary(cwd, session_id=None):
61
+ """Resolve the write boundary {assignmentDir, projectDir, workspaceRoot} from
62
+ the session's OPEN engagement via the CLI. context.json is a workspace marker
63
+ now — the active assignment lives on the engagement, so the enforcer can no
64
+ longer read assignmentDir/projectDir straight from the file. Returns {} on any
65
+ failure, which makes is_write_allowed fall back to workspace-only enforcement
66
+ (never fail-open)."""
67
+ argv = ["syntaur", "session", "boundary", "--json", "--cwd", cwd]
68
+ if session_id:
69
+ argv += ["--session-id", session_id]
70
+ try:
71
+ out = subprocess.run(argv, capture_output=True, text=True, timeout=3)
72
+ data = json.loads(out.stdout or "{}")
73
+ return data if isinstance(data, dict) else {}
74
+ except Exception:
75
+ return {}
76
+
77
+
60
78
  def _log_violation(reason):
61
79
  try:
62
80
  sys.stderr.write("[syntaur] " + reason + "\n")
@@ -80,7 +98,14 @@ def _on_pre_tool_call(tool_name=None, args=None, task_id=None, **kwargs):
80
98
  cwd = os.getcwd()
81
99
  ctx = boundary.load_context(cwd)
82
100
  if not ctx:
83
- return None
101
+ return None # no .syntaur/context.json → not in a workspace → allow
102
+ # Resolve the real assignment/project boundary from the session's open
103
+ # engagement and merge it over the workspace markers in context.json.
104
+ # If nothing resolves, is_write_allowed enforces workspace-only.
105
+ resolved = _resolve_boundary(cwd, ctx.get("sessionId"))
106
+ for k in ("assignmentDir", "projectDir", "workspaceRoot"):
107
+ if resolved.get(k):
108
+ ctx[k] = resolved[k]
84
109
  abs_path = path if os.path.isabs(path) else os.path.join(cwd, path)
85
110
  context_file = os.path.join(cwd, ".syntaur", "context.json")
86
111
  allowed, reason = boundary.is_write_allowed(abs_path, ctx, context_file)
@@ -129,8 +154,9 @@ def _make_command_handler(cmd):
129
154
  except Exception as exc:
130
155
  return "Failed to run syntaur %s: %s" % (" ".join(cmd["argv"]), exc)
131
156
  return (
132
- 'Follow the Syntaur "%s" skill (installed via skills). It derives the active '
133
- "assignment/session from .syntaur/context.json." % cmd["skill"]
157
+ 'Follow the Syntaur "%s" skill (installed via skills). It resolves the active '
158
+ "assignment from the session's open engagement (.syntaur/context.json is a "
159
+ "workspace marker)." % cmd["skill"]
134
160
  )
135
161
 
136
162
  return handler
@@ -18,7 +18,16 @@ def _expand_home(p):
18
18
 
19
19
 
20
20
  def load_context(cwd):
21
- """Read <cwd>/.syntaur/context.json; return a dict or None (fail-open)."""
21
+ """Read <cwd>/.syntaur/context.json; return its WORKSPACE-MARKER fields, or
22
+ None when absent/unparseable (caller treats None as "not in a workspace").
23
+
24
+ Deliberately does NOT surface the demoted assignment scalars
25
+ (assignmentDir/projectDir): the active assignment/project dirs are resolved
26
+ from the session's open engagement by the caller and merged in. Surfacing
27
+ stale legacy scalars here would let them leak through on a boundary-resolution
28
+ failure, so the {}-failure path could wrongly allow writes under a stale dir
29
+ instead of staying workspace-only.
30
+ """
22
31
  path = os.path.join(cwd, ".syntaur", "context.json")
23
32
  try:
24
33
  with open(path, "r", encoding="utf-8") as fh:
@@ -37,8 +46,6 @@ def load_context(cwd):
37
46
  return _expand_home(v) if v else None
38
47
 
39
48
  return {
40
- "assignmentDir": home("assignmentDir"),
41
- "projectDir": home("projectDir"),
42
49
  "workspaceRoot": home("workspaceRoot"),
43
50
  "sessionId": s("sessionId"),
44
51
  "projectSlug": s("projectSlug"),
@@ -67,12 +74,14 @@ def is_write_allowed(abs_file_path, ctx, context_file_abs=None):
67
74
  resources/memories (excluding derived `_*` files), equal to the context file,
68
75
  or under the workspace root. Otherwise blocked.
69
76
  """
70
- # Parity with the bash hook (enforce-boundaries.sh:69): enforcement requires
71
- # BOTH assignmentDir and projectDir. If either is missing (standalone / old /
72
- # partially-written context), fail OPEN allow everything.
73
- if not ctx.get("assignmentDir") or not ctx.get("projectDir"):
74
- return True, None
75
-
77
+ # context.json is a WORKSPACE MARKER now (the assignment scalars were
78
+ # demoted); the active assignment resolves from the session's open engagement.
79
+ # When the caller cannot supply assignmentDir/projectDir (no engagement
80
+ # resolved), we do NOT fail open — we enforce WORKSPACE-ONLY via the
81
+ # workspaceRoot marker below. Each dir check is guarded for absence, so a
82
+ # missing assignmentDir/projectDir simply narrows the allowlist rather than
83
+ # disabling it. (Assignment-record writes under ~/.syntaur go through the CLI,
84
+ # not the agent's file tools, so they never hit this hook.)
76
85
  f = _norm(abs_file_path)
77
86
 
78
87
  if ctx.get("assignmentDir") and _is_under(f, ctx["assignmentDir"]):
@@ -13,18 +13,30 @@
13
13
  // pi.registerCommand(name, { description, handler }) }`. The `tool_call` handler
14
14
  // returns `{ block: true, reason }` to DENY a tool call; any other return allows.
15
15
 
16
- import { readFileSync } from 'node:fs';
16
+ import { spawnSync } from 'node:child_process';
17
+ import { existsSync, readFileSync } from 'node:fs';
17
18
  import { homedir } from 'node:os';
18
19
  import { isAbsolute, resolve, sep } from 'node:path';
19
20
 
20
21
  export interface SyntaurContext {
21
- assignmentDir?: string;
22
- projectDir?: string;
22
+ /** Workspace marker; the only path scalar still authoritative in context.json. */
23
23
  workspaceRoot?: string;
24
24
  sessionId?: string;
25
25
  projectSlug?: string;
26
26
  }
27
27
 
28
+ /**
29
+ * The resolved write boundary for a session — mirrors the CLI's
30
+ * `syntaur session boundary --json` contract. The assignment/project dirs come
31
+ * from the session's OPEN engagement (NOT context.json, whose assignment scalars
32
+ * were demoted). `workspaceRoot` is the context.json marker.
33
+ */
34
+ export interface SyntaurBoundary {
35
+ assignmentDir?: string;
36
+ projectDir?: string;
37
+ workspaceRoot?: string;
38
+ }
39
+
28
40
  /** Expand a leading `~` to the home dir (the only expansion the bash hooks do). */
29
41
  function expandHome(p: string): string {
30
42
  if (p === '~') return homedir();
@@ -32,7 +44,12 @@ function expandHome(p: string): string {
32
44
  return p;
33
45
  }
34
46
 
35
- /** Read `<cwd>/.syntaur/context.json`; null when absent/unparseable (fail-open). */
47
+ /**
48
+ * Read `<cwd>/.syntaur/context.json`. context.json is now a WORKSPACE MARKER —
49
+ * this only surfaces `workspaceRoot` / `sessionId` / `projectSlug`. The active
50
+ * assignment/project dirs live on the session's engagement (resolveBoundary).
51
+ * null when absent/unparseable.
52
+ */
36
53
  export function loadContext(cwd: string): SyntaurContext | null {
37
54
  const file = resolve(cwd, '.syntaur', 'context.json');
38
55
  let raw: string;
@@ -49,18 +66,50 @@ export function loadContext(cwd: string): SyntaurContext | null {
49
66
  }
50
67
  const str = (v: unknown): string | undefined =>
51
68
  typeof v === 'string' && v.length > 0 ? v : undefined;
52
- const assignmentDir = str(data.assignmentDir);
53
- const projectDir = str(data.projectDir);
54
69
  const workspaceRoot = str(data.workspaceRoot);
55
70
  return {
56
- assignmentDir: assignmentDir ? expandHome(assignmentDir) : undefined,
57
- projectDir: projectDir ? expandHome(projectDir) : undefined,
58
71
  workspaceRoot: workspaceRoot ? expandHome(workspaceRoot) : undefined,
59
72
  sessionId: str(data.sessionId),
60
73
  projectSlug: str(data.projectSlug),
61
74
  };
62
75
  }
63
76
 
77
+ /** True iff `<cwd>/.syntaur/context.json` exists (the Syntaur-workspace marker). */
78
+ export function hasContextFile(cwd: string): boolean {
79
+ return existsSync(resolve(cwd, '.syntaur', 'context.json'));
80
+ }
81
+
82
+ /**
83
+ * Resolve the write boundary from the session's OPEN engagement by shelling out
84
+ * to the same CLI the bash hooks call: `syntaur session boundary --json`. Returns
85
+ * a {} boundary (all-empty → workspace-only enforcement) on ANY failure — the CLI
86
+ * itself never throws, and a missing/old CLI must NOT fail open. Paths are
87
+ * `~`-expanded for parity with the bash hooks.
88
+ */
89
+ export function resolveBoundary(cwd: string, sessionId?: string): SyntaurBoundary {
90
+ const args = ['session', 'boundary', '--json'];
91
+ if (sessionId) args.push('--session-id', sessionId);
92
+ let parsed: Record<string, unknown> = {};
93
+ try {
94
+ const r = spawnSync('syntaur', args, { cwd, encoding: 'utf-8', timeout: 5000 });
95
+ if (r.status === 0 && typeof r.stdout === 'string' && r.stdout.trim().length > 0) {
96
+ parsed = JSON.parse(r.stdout) as Record<string, unknown>;
97
+ }
98
+ } catch {
99
+ /* CLI missing / spawn error / unparseable → empty boundary (workspace-only) */
100
+ }
101
+ const str = (v: unknown): string | undefined =>
102
+ typeof v === 'string' && v.length > 0 ? v : undefined;
103
+ const assignmentDir = str(parsed.assignmentDir);
104
+ const projectDir = str(parsed.projectDir);
105
+ const workspaceRoot = str(parsed.workspaceRoot);
106
+ return {
107
+ assignmentDir: assignmentDir ? expandHome(assignmentDir) : undefined,
108
+ projectDir: projectDir ? expandHome(projectDir) : undefined,
109
+ workspaceRoot: workspaceRoot ? expandHome(workspaceRoot) : undefined,
110
+ };
111
+ }
112
+
64
113
  /** True if `child` is strictly under `parent` (mirrors the bash `"$X"/*` test). */
65
114
  function isUnder(child: string, parent: string): boolean {
66
115
  if (!parent) return false;
@@ -76,31 +125,31 @@ function basename(p: string): string {
76
125
  }
77
126
 
78
127
  /**
79
- * Decide whether a write to `absFilePath` is allowed under the active assignment.
80
- * Mirrors `platforms/claude-code/hooks/enforce-boundaries.sh` exactly:
81
- * - allow under assignmentDir
128
+ * Decide whether a write to `absFilePath` is allowed under the resolved boundary.
129
+ * Mirrors `platforms/claude-code/hooks/enforce-boundaries.sh`:
130
+ * - allow under assignmentDir (if resolved)
82
131
  * - allow under projectDir/resources/ and projectDir/memories/ EXCEPT derived `_*` files
83
132
  * - allow the `.syntaur/context.json` file itself (caller passes cwd-resolved path)
84
133
  * - allow under workspaceRoot (if set)
85
134
  * - otherwise block
135
+ *
136
+ * CRITICAL: no fail-open. When the boundary has no assignment/project (no open
137
+ * engagement) but a workspace is known, only the workspace-root (and context
138
+ * file) gates can match → WORKSPACE-ONLY enforcement. Every `isUnder` check is
139
+ * already empty-safe (`isUnder` returns false for an empty parent).
86
140
  */
87
141
  export function isWriteAllowed(
88
142
  absFilePath: string,
89
- ctx: SyntaurContext,
143
+ boundary: SyntaurBoundary,
90
144
  contextFileAbs?: string,
91
145
  ): { allowed: boolean; reason?: string } {
92
- // Parity with the bash hook (enforce-boundaries.sh:69): enforcement requires
93
- // BOTH assignmentDir and projectDir. If either is missing (standalone / old /
94
- // partially-written context), fail OPEN — allow everything.
95
- if (!ctx.assignmentDir || !ctx.projectDir) return { allowed: true };
96
-
97
146
  const file = resolve(absFilePath);
98
147
 
99
- if (ctx.assignmentDir && isUnder(file, ctx.assignmentDir)) return { allowed: true };
148
+ if (boundary.assignmentDir && isUnder(file, boundary.assignmentDir)) return { allowed: true };
100
149
 
101
- if (ctx.projectDir) {
102
- const resourcesDir = resolve(ctx.projectDir, 'resources');
103
- const memoriesDir = resolve(ctx.projectDir, 'memories');
150
+ if (boundary.projectDir) {
151
+ const resourcesDir = resolve(boundary.projectDir, 'resources');
152
+ const memoriesDir = resolve(boundary.projectDir, 'memories');
104
153
  for (const dir of [resourcesDir, memoriesDir]) {
105
154
  if (isUnder(file, dir) && !basename(file).startsWith('_')) return { allowed: true };
106
155
  }
@@ -108,12 +157,14 @@ export function isWriteAllowed(
108
157
 
109
158
  if (contextFileAbs && file === resolve(contextFileAbs)) return { allowed: true };
110
159
 
111
- if (ctx.workspaceRoot && isUnder(file, ctx.workspaceRoot)) return { allowed: true };
160
+ if (boundary.workspaceRoot && isUnder(file, boundary.workspaceRoot)) return { allowed: true };
112
161
 
113
- const reason =
114
- `Syntaur write boundary violation: cannot write to '${file}'. Allowed: assignment dir ` +
115
- `(${ctx.assignmentDir ?? 'n/a'}), project resources/memories, workspace ` +
116
- `(${ctx.workspaceRoot ?? 'n/a'}).`;
162
+ const reason = boundary.assignmentDir
163
+ ? `Syntaur write boundary violation: cannot write to '${file}'. Allowed: assignment dir ` +
164
+ `(${boundary.assignmentDir}), project resources/memories, workspace ` +
165
+ `(${boundary.workspaceRoot ?? 'n/a'}).`
166
+ : `Syntaur write boundary violation: cannot write to '${file}'. No active assignment for ` +
167
+ `this session — writes are restricted to the workspace (${boundary.workspaceRoot ?? 'n/a'}).`;
117
168
  return { allowed: false, reason };
118
169
  }
119
170
 
@@ -229,11 +280,16 @@ export default function activate(pi: {
229
280
  const path = extractWritePath(e.toolName, e.input);
230
281
  if (!path) return; // not a write → allow
231
282
  const cwd = process.cwd();
283
+ // context.json presence is the Syntaur-workspace marker. Absent → not a
284
+ // Syntaur workspace → allow (unchanged). Present → enforce. The assignment
285
+ // boundary is resolved from the session's OPEN engagement via the CLI — NOT
286
+ // the demoted context.json scalars. No engagement → workspace-only.
287
+ if (!hasContextFile(cwd)) return; // not a Syntaur workspace → allow
232
288
  const ctx = loadContext(cwd);
233
- if (!ctx) return; // no active assignment → allow
289
+ const boundary = resolveBoundary(cwd, ctx?.sessionId);
234
290
  const abs = resolveAbs(path, cwd);
235
291
  const contextFileAbs = resolve(cwd, '.syntaur', 'context.json');
236
- const { allowed, reason } = isWriteAllowed(abs, ctx, contextFileAbs);
292
+ const { allowed, reason } = isWriteAllowed(abs, boundary, contextFileAbs);
237
293
  if (!allowed) return { block: true, reason };
238
294
  return undefined;
239
295
  });
@@ -249,7 +305,6 @@ export default function activate(pi: {
249
305
  description: cmd.description,
250
306
  handler: async (_args: string, ctx: unknown) => {
251
307
  if (cmd.kind === 'passthrough' && cmd.argv) {
252
- const { spawnSync } = await import('node:child_process');
253
308
  const r = spawnSync('syntaur', cmd.argv, { encoding: 'utf-8' });
254
309
  notify(ctx, (r.stdout || '') + (r.stderr || '') || `ran: syntaur ${cmd.argv.join(' ')}`);
255
310
  return;
@@ -32,8 +32,9 @@ file-ownership protocol.
32
32
 
33
33
  ## Step 1: Resolve project
34
34
 
35
- If `.syntaur/context.json` is present and has `projectSlug`, default to that.
36
- Otherwise ask the user which project to add the memory to.
35
+ If the session has an open engagement with an active assignment (`syntaur
36
+ session resume --json`), default to its `projectSlug`. Otherwise ask the user
37
+ which project to add the memory to.
37
38
 
38
39
  ## Step 2: Gather inputs
39
40
 
@@ -30,8 +30,9 @@ CLI-managed file per the file-ownership protocol).
30
30
 
31
31
  ## Step 1: Resolve project
32
32
 
33
- If `.syntaur/context.json` is present and has `projectSlug`, default to that.
34
- Otherwise ask the user which project to add the resource to.
33
+ If the session has an open engagement with an active assignment (`syntaur
34
+ session resume --json`), default to its `projectSlug`. Otherwise ask the user
35
+ which project to add the resource to.
35
36
 
36
37
  ## Step 2: Gather inputs
37
38
 
@@ -45,9 +45,9 @@ The computed worktree path is **always**
45
45
 
46
46
  ## Step 2: Pre-flight
47
47
 
48
- - Confirm `.syntaur/context.json` is a bundle context (has `bundleId`,
49
- no assignment fields). If it's an assignment context, stop and tell the
50
- user to `/grab-bundle <id>` first.
48
+ - Confirm `.syntaur/context.json` is a bundle context (has `bundleId`). If the
49
+ session is instead bound to an assignment (an open engagement) with no
50
+ `bundleId`, stop and tell the user to `/grab-bundle <id>` first.
51
51
  - Confirm `<repository>/.git` exists.
52
52
  - Confirm the branch does NOT already exist. If it does, the CLI will
53
53
  surface a `GitWorktreeError` — surface that verbatim.
@@ -17,7 +17,7 @@ Attach proof to your work. Reviewers should be able to scroll a single page (`pr
17
17
 
18
18
  ## Input
19
19
 
20
- No arguments. Reads `.syntaur/context.json` from the current working directory to identify the active assignment.
20
+ No arguments. The active assignment is resolved from the session's open engagement (`.syntaur/context.json` is only a workspace marker, not the assignment source).
21
21
 
22
22
  ## Step 1: Decide whether to capture
23
23
 
@@ -60,7 +60,7 @@ Rules:
60
60
  - All other kinds (`screenshot`, `video`, `asciinema`) require `--file`. The CLI rejects nonexistent / non-file paths.
61
61
  - `--criterion <index>` is optional — pass the **0-based** index into the `## Acceptance Criteria` checklist when you want to anchor the artifact to a specific criterion.
62
62
  - `--transcribe` is video-only and writes a sibling `<id>.transcript.md` (requires `ELEVENLABS_API_KEY` + `ffmpeg`). `proof build` renders the transcript next to the player; clicking a phrase seeks the video.
63
- - If you have a `.syntaur/context.json` in the cwd, the positional target argument is unnecessary; otherwise pass `--project <slug> <assignment-slug>` or a bare assignment UUID.
63
+ - If this session has an open engagement (active assignment), the positional target argument is unnecessary the CLI resolves the target from the engagement. Otherwise pass `--project <slug> <assignment-slug>` or a bare assignment UUID.
64
64
 
65
65
  The CLI copies the file (if any) under `<assignmentDir>/proof/<criterion|untagged>/<id>.<ext>` and inserts a row in `~/.syntaur/syntaur.db`. Output prints the artifact id and absolute path.
66
66
 
@@ -29,15 +29,15 @@ Examples:
29
29
 
30
30
  Extract `<inventory_slug>` (required, first positional), plus optional `--ttl <duration>` and `--for <tag>`.
31
31
 
32
- ### Step 2: Pre-check assignment context
32
+ ### Step 2: Pre-check workspace context
33
33
 
34
- Read `.syntaur/context.json` from the current working directory. If the file is missing, OR has neither `sessionId` nor any of `projectSlug`/`assignmentSlug`/`assignmentDir`, abort with:
34
+ Read `.syntaur/context.json` (a workspace marker) from the current working directory. If the file is missing, OR carries no workspace markers (`repository`/`branch`/`worktreePath`/`workspaceRoot`) and no `sessionId`, abort with:
35
35
 
36
36
  > "No active Syntaur context in this workspace. Grab an assignment first (`/grab-assignment <project> <slug>`) or restart the session so the SessionStart hook can populate `.syntaur/context.json`."
37
37
 
38
38
  This guard exists because writing a leases-only context file would trip `syntaur doctor`'s workspace check.
39
39
 
40
- If `--for` was not provided, derive its value from the context: prefer `assignmentSlug` if present, otherwise `sessionId`.
40
+ If `--for` was not provided, derive its value: prefer the active assignment's slug from the session's open engagement (`syntaur session resume --json`); otherwise fall back to the `sessionId` from the workspace marker.
41
41
 
42
42
  ### Step 3: Run the claim CLI
43
43