@calltelemetry/openclaw-linear 0.8.5 → 0.8.7

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/README.md CHANGED
@@ -16,6 +16,7 @@ Connect Linear to AI agents. Issues get triaged, implemented, and audited — au
16
16
  - **Say "let's plan the features"?** A planner interviews you, writes user stories, and builds your full issue hierarchy.
17
17
  - **Plan looks good?** A different AI model automatically audits the plan before dispatch.
18
18
  - **Agent goes silent?** A watchdog kills it and retries automatically.
19
+ - **Linear guidance?** Workspace and team-level guidance from Linear flows into every agent prompt — triage, dispatch, worker, audit.
19
20
  - **Want updates?** Get notified on Discord, Slack, Telegram, or Signal.
20
21
 
21
22
  ---
@@ -495,6 +496,8 @@ Add settings under the plugin entry in `openclaw.json`:
495
496
  | `inactivitySec` | number | `120` | Kill agent if silent this long |
496
497
  | `maxTotalSec` | number | `7200` | Max total agent session time |
497
498
  | `toolTimeoutSec` | number | `600` | Max single `code_run` time |
499
+ | `enableGuidance` | boolean | `true` | Inject Linear workspace/team guidance into agent prompts |
500
+ | `teamGuidanceOverrides` | object | — | Per-team guidance toggle. Key = team ID, value = boolean. Unset teams inherit `enableGuidance`. |
498
501
  | `claudeApiKey` | string | — | Anthropic API key for Claude CLI (passed as `ANTHROPIC_API_KEY` env var). Required if using Claude backend. |
499
502
 
500
503
  ### Environment Variables
@@ -652,6 +655,37 @@ Prompts merge in this order (later layers override earlier ones):
652
655
 
653
656
  Each layer only overrides the specific sections you define. Everything else keeps its default.
654
657
 
658
+ ### Linear Guidance
659
+
660
+ Linear's [agent guidance system](https://linear.app/docs/agents-in-linear) lets admins configure workspace-wide and team-specific instructions for agents. This plugin automatically extracts that guidance and appends it as supplementary instructions to all agent prompts.
661
+
662
+ Guidance is configured in Linear at:
663
+ - **Workspace level:** Settings > Agents > Additional guidance (applies across entire org)
664
+ - **Team level:** Team settings > Agents > Additional guidance (takes priority over workspace guidance)
665
+
666
+ See [Agents in Linear](https://linear.app/docs/agents-in-linear) for full documentation on how guidance works.
667
+
668
+ Guidance flows into:
669
+ - **Orchestrator prompts** — AgentSessionEvent and comment handler paths
670
+ - **Worker prompts** — Appended to the task via `{{guidance}}` template variable
671
+ - **Audit prompts** — Appended to the audit task
672
+ - **Triage and closure prompts** — Appended to the triage and close_issue handlers
673
+
674
+ Guidance is cached per-team (24h TTL) so comment webhooks (which don't carry guidance from Linear) can also benefit.
675
+
676
+ **Disable guidance globally:**
677
+ ```json
678
+ { "enableGuidance": false }
679
+ ```
680
+
681
+ **Disable for a specific team:**
682
+ ```json
683
+ {
684
+ "enableGuidance": true,
685
+ "teamGuidanceOverrides": { "team-id-here": false }
686
+ }
687
+ ```
688
+
655
689
  ### Example Custom Prompts
656
690
 
657
691
  ```yaml
@@ -690,6 +724,7 @@ rework:
690
724
  | `{{planSnapshot}}` | Current plan structure (planner prompts) |
691
725
  | `{{reviewModel}}` | Name of cross-model reviewer (planner review) |
692
726
  | `{{crossModelFeedback}}` | Review recommendations (planner review) |
727
+ | `{{guidance}}` | Linear workspace/team guidance (if available, empty string otherwise) |
693
728
 
694
729
  ### CLI
695
730
 
@@ -856,6 +891,245 @@ Configure per-agent in `agent-profiles.json` or globally in plugin config.
856
891
 
857
892
  ---
858
893
 
894
+ ## Agent Tools
895
+
896
+ Every agent session gets these registered tools. They're available as native tool calls — no CLI parsing, no shell execution, no flag guessing.
897
+
898
+ ### `code_run` — Coding backend dispatch
899
+
900
+ Sends a task to whichever coding CLI is configured (Codex, Claude Code, or Gemini). The agent writes the prompt; the plugin handles backend selection, worktree setup, and output capture.
901
+
902
+ ### `linear_issues` — Native Linear API
903
+
904
+ Agents call `linear_issues` with typed JSON parameters. The tool wraps the Linear GraphQL API directly and handles all name-to-ID resolution automatically.
905
+
906
+ | Action | What it does | Key parameters |
907
+ |---|---|---|
908
+ | `read` | Get full issue details (status, labels, comments, relations) | `issueId` |
909
+ | `create` | Create a new issue or sub-issue | `title`, `description`, `teamId` or `parentIssueId` |
910
+ | `update` | Change status, priority, labels, estimate, or title | `issueId` + fields |
911
+ | `comment` | Post a comment on an issue | `issueId`, `body` |
912
+ | `list_states` | Get available workflow states for a team | `teamId` |
913
+ | `list_labels` | Get available labels for a team | `teamId` |
914
+
915
+ **Sub-issues:** Use `action="create"` with `parentIssueId` to create sub-issues under an existing issue. The new issue inherits `teamId` and `projectId` from its parent automatically. Agents are instructed to break large work into sub-issues for granular tracking — any task with multiple distinct deliverables should be decomposed. Auditors can also create sub-issues for remaining work when an implementation is partial.
916
+
917
+ ### `spawn_agent` / `ask_agent` — Multi-agent orchestration
918
+
919
+ Delegate work to other crew agents. `spawn_agent` is fire-and-forget (parallel), `ask_agent` waits for a reply (synchronous). Disabled with `enableOrchestration: false`.
920
+
921
+ ### `dispatch_history` — Recent dispatch context
922
+
923
+ Returns recent dispatch activity. Agents use this for situational awareness when working on related issues.
924
+
925
+ ### Access model
926
+
927
+ Not all agents get write access. The webhook prompts enforce this:
928
+
929
+ | Context | `linear_issues` access | `code_run` |
930
+ |---|---|---|
931
+ | Triaged issue (In Progress, etc.) | Full (read + create + update + comment) | Yes |
932
+ | Untriaged issue (Backlog, Triage) | Read only | Yes |
933
+ | Auditor | Full (read + create + update + comment) | Yes |
934
+ | Worker (inside `code_run`) | None | N/A |
935
+
936
+ ---
937
+
938
+ ## Linear API & Hook Architecture
939
+
940
+ This section documents every interaction between the plugin and the Linear GraphQL API, the webhook event routing, the hook lifecycle, and the dispatch pipeline internals.
941
+
942
+ ### GraphQL API Layer
943
+
944
+ All Linear API calls go through `LinearAgentApi` (`src/api/linear-api.ts`), which wraps `https://api.linear.app/graphql` with automatic token refresh, retry resilience, and 401 recovery.
945
+
946
+ **Token resolution** (`resolveLinearToken`) checks three sources in priority order:
947
+
948
+ 1. `pluginConfig.accessToken` — static config
949
+ 2. Auth profile store (`~/.openclaw/auth-profiles.json`) — OAuth tokens with auto-refresh
950
+ 3. `LINEAR_ACCESS_TOKEN` / `LINEAR_API_KEY` environment variable
951
+
952
+ OAuth tokens get a `Bearer` prefix; personal API keys do not. Tokens are refreshed 60 seconds before expiry via `refreshLinearToken()`, and the refreshed credentials are persisted back to the auth profile store.
953
+
954
+ **API methods by category:**
955
+
956
+ | Category | Method | GraphQL Operation | Used By |
957
+ |---|---|---|---|
958
+ | **Issues** | `getIssueDetails(issueId)` | `query Issue` | Triage, audit, close, `linear_issues` tool |
959
+ | | `createIssue(input)` | `mutation IssueCreate` | Planner |
960
+ | | `updateIssue(issueId, input)` | `mutation IssueUpdate` | Triage (labels, estimate, priority) |
961
+ | | `updateIssueExtended(issueId, input)` | `mutation IssueUpdate` | `linear_issues` tool, close handler |
962
+ | | `createIssueRelation(input)` | `mutation IssueRelationCreate` | Planner (dependency DAG) |
963
+ | **Comments** | `createComment(issueId, body, opts)` | `mutation CommentCreate` | All phases (fallback delivery) |
964
+ | | `createReaction(commentId, emoji)` | `mutation ReactionCreate` | Acknowledgment reactions |
965
+ | **Sessions** | `createSessionOnIssue(issueId)` | `mutation AgentSessionCreateOnIssue` | Comment handler, close handler |
966
+ | | `emitActivity(sessionId, content)` | `mutation AgentActivityCreate` | Primary response delivery |
967
+ | | `updateSession(sessionId, input)` | `mutation AgentSessionUpdate` | External URLs, plan text |
968
+ | **Teams** | `getTeamStates(teamId)` | `query TeamStates` | `linear_issues` tool, close handler |
969
+ | | `getTeamLabels(teamId)` | `query TeamLabels` | `linear_issues` tool, triage |
970
+ | | `getTeams()` | `query Teams` | Doctor health check |
971
+ | | `createLabel(teamId, name, opts)` | `mutation IssueLabelCreate` | Triage (auto-create labels) |
972
+ | **Projects** | `getProject(projectId)` | `query Project` | Planner |
973
+ | | `getProjectIssues(projectId)` | `query ProjectIssues` | Planner, DAG dispatch |
974
+ | **Webhooks** | `listWebhooks()` | `query Webhooks` | Doctor, webhook setup CLI |
975
+ | | `createWebhook(input)` | `mutation WebhookCreate` | Webhook setup CLI |
976
+ | | `updateWebhook(id, input)` | `mutation WebhookUpdate` | Webhook management |
977
+ | | `deleteWebhook(id)` | `mutation WebhookDelete` | Webhook cleanup |
978
+ | **Notifications** | `getAppNotifications(count)` | `query Notifications` | Doctor (connectivity check) |
979
+ | **Identity** | `getViewerId()` | `query Viewer` | Self-comment filtering |
980
+
981
+ ### Webhook Event Routing
982
+
983
+ The plugin registers an HTTP route at `/linear/webhook` that receives POST payloads from two Linear webhook sources:
984
+
985
+ 1. **Workspace webhook** — `Comment.create`, `Issue.update`, `Issue.create`
986
+ 2. **OAuth app webhook** — `AgentSessionEvent.created`, `AgentSessionEvent.prompted`
987
+
988
+ Both must point to the same URL. `AgentSessionEvent` payloads carry workspace/team guidance which is extracted, cached per-team, and appended to all agent prompts. Comment webhook paths use the cached guidance since Linear does not include guidance in `Comment.create` payloads. See [Linear Guidance](#linear-guidance).
989
+
990
+ The handler dispatches by `type + action`:
991
+
992
+ ```
993
+ Incoming POST /linear/webhook
994
+
995
+ ├─ type=AgentSessionEvent, action=created
996
+ │ └─ New agent session on issue → dedup → create LinearAgentApi → run agent
997
+
998
+ ├─ type=AgentSessionEvent, action=prompted
999
+ │ └─ Follow-up message in existing session → dedup → resume agent with context
1000
+
1001
+ ├─ type=Comment, action=create
1002
+ │ └─ Comment on issue → filter self-comments (viewerId) → dedup →
1003
+ │ intent classify → route to handler (see Intent Classification below)
1004
+
1005
+ ├─ type=Issue, action=update
1006
+ │ └─ Issue field changed → check assignment → if assigned to app user →
1007
+ │ dispatch (triage or full implementation)
1008
+
1009
+ ├─ type=Issue, action=create
1010
+ │ └─ New issue created → triage (estimate, labels, priority)
1011
+
1012
+ └─ type=AppUserNotification
1013
+ └─ Immediately discarded (duplicates workspace webhook events)
1014
+ ```
1015
+
1016
+ ### Intent Classification
1017
+
1018
+ When a `Comment.create` event arrives, the plugin classifies the user's intent using a two-tier system:
1019
+
1020
+ 1. **LLM classifier** (~300 tokens, ~2-5s) — a small/fast model parses the comment and returns structured JSON with intent + reasoning
1021
+ 2. **Regex fallback** — if the LLM call fails or times out, static patterns catch common cases
1022
+
1023
+ | Intent | Trigger | Handler |
1024
+ |---|---|---|
1025
+ | `plan_start` | "let's plan the features" | Start planner interview session |
1026
+ | `plan_finalize` | "looks good, ship it" | Run plan audit + cross-model review |
1027
+ | `plan_abandon` | "cancel planning" | End planning session |
1028
+ | `plan_continue` | Any message during active planning | Continue planner conversation |
1029
+ | `ask_agent` | "@kaylee" or "hey kaylee" | Route to specific agent by name |
1030
+ | `request_work` | "fix the search bug" | Dispatch to default agent |
1031
+ | `question` | "what's the status?" | Agent answers without code changes |
1032
+ | `close_issue` | "close this" / "mark as done" | Generate closure report + transition state |
1033
+ | `general` | Noise, automated messages | Silently dropped |
1034
+
1035
+ ### Hook Lifecycle
1036
+
1037
+ The plugin registers three lifecycle hooks via `api.on()` in `index.ts`:
1038
+
1039
+ **`agent_end`** — Dispatch pipeline state machine. When a sub-agent (worker or auditor) finishes:
1040
+ - Looks up the session key in dispatch state to find the active dispatch
1041
+ - Validates the attempt number matches (rejects stale events from old retries)
1042
+ - If the worker finished → triggers the audit phase (`triggerAudit`)
1043
+ - If the auditor finished → processes the verdict (`processVerdict` → pass/fail/stuck)
1044
+
1045
+ **`before_agent_start`** — Context injection. For `linear-worker-*` and `linear-audit-*` sessions:
1046
+ - Reads dispatch state and finds up to 3 active dispatches
1047
+ - Prepends a `<dispatch-history>` block so the agent has situational awareness of concurrent work
1048
+
1049
+ **`message_sending`** — Narration guard. Catches short (~250 char) "Let me explore..." responses where the agent narrates intent without actually calling tools:
1050
+ - Appends a warning: "Agent acknowledged but may not have completed the task"
1051
+ - Prevents users from thinking the agent did something when it only said it would
1052
+
1053
+ ### Response Delivery
1054
+
1055
+ Agent responses follow an **emitActivity-first** pattern:
1056
+
1057
+ 1. Try `emitActivity(sessionId, { type: "response", body })` — appears as agent activity in Linear's UI, no duplicate comment
1058
+ 2. If `emitActivity` fails (no session, API error) → fall back to `createComment(issueId, body)`
1059
+ 3. Comments posted outside sessions use `createCommentWithDedup()` — pre-registers the comment ID to prevent the echo webhook from triggering reprocessing
1060
+
1061
+ ### Close Issue Flow
1062
+
1063
+ When intent classification returns `close_issue`:
1064
+
1065
+ ```
1066
+ close_issue intent
1067
+
1068
+ ├─ Fetch full issue details (getIssueDetails)
1069
+ ├─ Find team's "completed" state (getTeamStates → type=completed)
1070
+ ├─ Create agent session on issue (createSessionOnIssue)
1071
+ ├─ Emit "preparing closure report" thought (emitActivity)
1072
+ ├─ Run agent in read-only mode to generate closure report (runAgent)
1073
+ ├─ Transition issue state to completed (updateIssue → stateId)
1074
+ └─ Post closure report (emitActivity → createComment fallback)
1075
+ ```
1076
+
1077
+ This is a **static action** — the intent triggers direct API calls orchestrated by the plugin, not by giving the agent write tools. The agent only generates the closure report text; all state transitions are handled by the plugin.
1078
+
1079
+ ### Dispatch Pipeline Internals
1080
+
1081
+ The full dispatch flow for implementing an issue:
1082
+
1083
+ ```
1084
+ Issue assigned to app user
1085
+
1086
+ ├─ 1. Assess complexity tier (runAgent → junior/medior/senior)
1087
+ ├─ 2. Create isolated git worktree (createWorktree)
1088
+ ├─ 3. Register dispatch in state file (registerDispatch)
1089
+ ├─ 4. Write .claw/manifest.json with issue metadata
1090
+ ├─ 5. Notify: "dispatched as {tier}"
1091
+
1092
+ ├─ 6. Worker phase (spawnWorker)
1093
+ │ ├─ Build prompt from prompts.yaml (worker.system + worker.task)
1094
+ │ ├─ If retry: append rework.addendum with prior audit gaps
1095
+ │ ├─ Tool access: code_run YES, linear_issues NO
1096
+ │ └─ Output captured as text → saved to .claw/worker-{attempt}.md
1097
+
1098
+ ├─ 7. Audit phase (triggerAudit)
1099
+ │ ├─ Build prompt from prompts.yaml (audit.system + audit.task)
1100
+ │ ├─ Tool access: code_run YES, linear_issues READ+WRITE
1101
+ │ ├─ Auditor verifies acceptance criteria, runs tests, reviews diff
1102
+ │ └─ Must return JSON verdict: {pass, criteria, gaps, testResults}
1103
+
1104
+ └─ 8. Verdict (processVerdict)
1105
+ ├─ PASS → updateIssue(stateId=Done), post summary, notify ✅
1106
+ ├─ FAIL + retries left → back to step 6 with audit gaps as context
1107
+ └─ FAIL + no retries → escalate, notify 🚨, status="stuck"
1108
+ ```
1109
+
1110
+ **State persistence:** Dispatch state is written to `~/.openclaw/linear-dispatch-state.json` with active dispatches, completed history, session mappings, and processed event IDs.
1111
+
1112
+ **Watchdog:** A configurable inactivity timer (`inactivitySec`, default 120s) monitors agent output. If no tool calls or text output for the configured period, the agent process is killed and retried once. If the retry also times out, the dispatch is escalated.
1113
+
1114
+ ### `linear_issues` Tool → API Mapping
1115
+
1116
+ The `linear_issues` registered tool translates agent requests into `LinearAgentApi` method calls:
1117
+
1118
+ | Tool Action | API Methods Called |
1119
+ |---|---|
1120
+ | `read` | `getIssueDetails(issueId)` |
1121
+ | `create` | `getIssueDetails(parentIssueId)` (if parent) → `getTeamLabels` (if labels) → `createIssue(input)` |
1122
+ | `update` | `getIssueDetails` → `getTeamStates` (if status) → `getTeamLabels` (if labels) → `updateIssueExtended` |
1123
+ | `comment` | `createComment(issueId, body)` |
1124
+ | `list_states` | `getTeamStates(teamId)` |
1125
+ | `list_labels` | `getTeamLabels(teamId)` |
1126
+
1127
+ The `update` action's key feature is **name-to-ID resolution**: agents say `status: "In Progress"` and the tool automatically resolves it to the correct `stateId` via `getTeamStates`. Same for labels — `labels: ["bug", "urgent"]` resolves to `labelIds` via `getTeamLabels`. Case-insensitive matching with descriptive errors when names don't match.
1128
+
1129
+ The `create` action supports **sub-issue creation** via `parentIssueId`. When provided, the new issue inherits `teamId` and `projectId` from the parent, and the `GraphQL-Features: sub_issues` header is sent automatically. Agents are instructed to decompose large tasks into sub-issues for granular planning and parallel dispatch.
1130
+
1131
+ ---
1132
+
859
1133
  ## Testing & Verification
860
1134
 
861
1135
  ### Health check
@@ -942,7 +1216,7 @@ This is separate from the main `doctor` because each live test spawns a real CLI
942
1216
 
943
1217
  ### Unit tests
944
1218
 
945
- 532 tests covering the full pipeline — triage, dispatch, audit, planning, intent classification, cross-model review, notifications, and infrastructure:
1219
+ 551 tests covering the full pipeline — triage, dispatch, audit, planning, intent classification, native issue tools, cross-model review, notifications, and infrastructure:
946
1220
 
947
1221
  ```bash
948
1222
  cd ~/claw-extensions/linear
@@ -1150,6 +1424,7 @@ For detailed diagnostics, see [docs/troubleshooting.md](docs/troubleshooting.md)
1150
1424
 
1151
1425
  - [Architecture](docs/architecture.md) — Internal design, state machines, diagrams
1152
1426
  - [Troubleshooting](docs/troubleshooting.md) — Diagnostic commands, curl examples, log analysis
1427
+ - [Agents in Linear](https://linear.app/docs/agents-in-linear) — Linear's agent guidance system (workspace & team-level instructions)
1153
1428
 
1154
1429
  ---
1155
1430
 
@@ -59,7 +59,9 @@
59
59
  "inactivitySec": { "type": "number", "description": "Kill sessions with no I/O for this many seconds (default: 120)", "default": 120 },
60
60
  "maxTotalSec": { "type": "number", "description": "Max total runtime for agent sessions in seconds (default: 7200)", "default": 7200 },
61
61
  "toolTimeoutSec": { "type": "number", "description": "Max runtime for a single code_run CLI invocation in seconds (default: 600)", "default": 600 },
62
- "claudeApiKey": { "type": "string", "description": "Anthropic API key for Claude CLI backend (passed as ANTHROPIC_API_KEY env var)", "sensitive": true }
62
+ "claudeApiKey": { "type": "string", "description": "Anthropic API key for Claude CLI backend (passed as ANTHROPIC_API_KEY env var)", "sensitive": true },
63
+ "enableGuidance": { "type": "boolean", "description": "Inject Linear workspace/team guidance into agent prompts (default: true)", "default": true },
64
+ "teamGuidanceOverrides": { "type": "object", "description": "Per-team guidance toggle. Key = Linear team ID, value = boolean. Unset teams inherit enableGuidance.", "additionalProperties": { "type": "boolean" } }
63
65
  }
64
66
  }
65
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
4
4
  "description": "Linear Agent plugin for OpenClaw — webhook-driven AI pipeline with OAuth, multi-agent routing, and issue triage",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/prompts.yaml CHANGED
@@ -1,20 +1,20 @@
1
1
  # prompts.yaml — Externalized phase prompts for the Linear dispatch pipeline.
2
2
  #
3
3
  # Template variables: {{identifier}}, {{title}}, {{description}},
4
- # {{worktreePath}}, {{gaps}}, {{tier}}, {{attempt}}
4
+ # {{worktreePath}}, {{gaps}}, {{tier}}, {{attempt}}, {{guidance}}
5
5
  #
6
6
  # Edit these to customize worker/audit behavior without rebuilding the plugin.
7
7
  # Override path via `promptsPath` in plugin config.
8
8
  #
9
9
  # Access model:
10
- # Zoe (orchestrator) linearis READ ONLY (issues read/list/search)
11
- # Worker — NO linearis access. Return text output only.
12
- # Auditor linearis READ + WRITE (can update status, close, comment)
10
+ # Orchestratorlinear_issues READ + CREATE (action=read, create, list_states, list_labels)
11
+ # Worker — NO linear_issues access. Return text output only.
12
+ # Auditor linear_issues READ + WRITE (can update status, close, comment, create sub-issues)
13
13
 
14
14
  worker:
15
15
  system: |
16
16
  You are a coding worker implementing a Linear issue. Your ONLY job is to write code and return a text summary.
17
- You do NOT have access to linearis or any Linear issue management tools.
17
+ You do NOT have access to linear_issues or any Linear issue management tools.
18
18
  Do NOT attempt to update, close, comment on, or modify the Linear issue in any way.
19
19
  Do NOT mark the issue as Done — the audit system handles all issue lifecycle.
20
20
  Just write code and return your implementation summary as text.
@@ -36,7 +36,8 @@ worker:
36
36
  7. Commit your work with a clear commit message
37
37
  8. Return a text summary: what you changed, what tests passed, any assumptions you made, and any open questions
38
38
 
39
- Your text output will be captured automatically. Do NOT use linearis or attempt to post comments.
39
+ Your text output will be captured automatically. Do NOT use linear_issues or attempt to post comments.
40
+ {{guidance}}
40
41
 
41
42
  audit:
42
43
  system: |
@@ -46,10 +47,12 @@ audit:
46
47
  Worker output is secondary evidence of what was attempted.
47
48
  You must be thorough and objective. Do not rubber-stamp.
48
49
 
49
- You have WRITE access to linearis. After auditing, you are responsible for:
50
+ You have WRITE access to the `linear_issues` tool. After auditing, you are responsible for:
50
51
  - Posting an audit summary comment on the issue
51
52
  - Updating the issue status if the audit passes
52
- Use `linearis` CLI via exec for these operations.
53
+ - If work is partially done but remaining items are clearly separable, create sub-issues
54
+ for unfinished work using action="create" with parentIssueId="{{identifier}}"
55
+ Use the `linear_issues` tool for these operations.
53
56
  task: |
54
57
  Audit issue {{identifier}}: {{title}}
55
58
 
@@ -60,21 +63,27 @@ audit:
60
63
 
61
64
  Checklist:
62
65
  1. Identify ALL acceptance criteria from the issue body
63
- 2. Read worker comments: `linearis issues read {{identifier}}`
66
+ 2. Read worker comments: `linear_issues` with action="read", issueId="{{identifier}}"
64
67
  3. Verify each acceptance criterion is addressed in the code
65
68
  4. Run tests in the worktree — verify they pass
66
69
  5. Check test coverage if expectations are stated in the issue
67
70
  6. Review the code diff for quality and correctness
68
71
 
69
72
  After auditing:
70
- - Post your audit findings as a comment: `linearis comments create {{identifier}} --body "..."`
71
- - If PASS: update status: `linearis issues update {{identifier}} --status "Done"`
73
+ - Post your audit findings as a comment: `linear_issues` with action="comment", issueId="{{identifier}}", body="..."
74
+ - If PASS: update status: `linear_issues` with action="update", issueId="{{identifier}}", status="Done"
75
+ - If PARTIAL PASS (some criteria met, others clearly separable): pass the completed work,
76
+ then create sub-issues for remaining items using `linear_issues` with action="create",
77
+ parentIssueId="{{identifier}}", title="...", description="..." (include acceptance criteria).
78
+ This is better than failing the entire audit when significant progress was made.
72
79
 
73
80
  When posting your audit comment, include a brief assessment: what was done well,
74
81
  what the gaps are (if any), and what the user should look at next.
82
+ If you created sub-issues, list them in your comment.
75
83
 
76
84
  You MUST return a JSON verdict as the last line of your response:
77
85
  {"pass": true/false, "criteria": ["list of criteria found"], "gaps": ["list of unmet criteria"], "testResults": "summary of test output"}
86
+ {{guidance}}
78
87
 
79
88
  rework:
80
89
  addendum: |
@@ -83,7 +92,7 @@ rework:
83
92
 
84
93
  Address these specific issues in your rework. Focus ONLY on the gaps listed above.
85
94
  Do NOT undo or rewrite parts that already work — preserve correct code from prior attempts.
86
- Remember: you do NOT have linearis access. Just fix the code and return a text summary.
95
+ Remember: you do NOT have linear_issues access. Just fix the code and return a text summary.
87
96
 
88
97
  planner:
89
98
  system: |
@@ -95,6 +104,9 @@ planner:
95
104
  - Epics are high-level feature areas. Mark them with isEpic=true.
96
105
  - Issues under epics are concrete deliverables with acceptance criteria.
97
106
  - Sub-issues are atomic work units that together complete a parent issue.
107
+ Use sub-issues aggressively — any issue estimated at 3+ story points should be
108
+ decomposed into smaller sub-issues (1-2 points each). Each sub-issue must be
109
+ independently implementable and testable.
98
110
  - Use "blocks" relationships to express ordering: if A must finish before B starts, A blocks B.
99
111
  - Every non-epic issue needs a story point estimate and priority.
100
112
  - Every issue description must include:
@@ -111,8 +111,14 @@ export function makeAgentSessionEventCreated(overrides?: Record<string, unknown>
111
111
  title: "Fix webhook routing",
112
112
  },
113
113
  },
114
- previousComments: [],
115
- guidance: "Please investigate this issue",
114
+ previousComments: [
115
+ {
116
+ body: "Please investigate this issue",
117
+ userId: "user-1",
118
+ createdAt: new Date().toISOString(),
119
+ },
120
+ ],
121
+ guidance: "Always use the main branch. Run make test before closing.",
116
122
  ...overrides,
117
123
  };
118
124
  }
@@ -133,6 +139,7 @@ export function makeAgentSessionEventPrompted(overrides?: Record<string, unknown
133
139
  },
134
140
  },
135
141
  agentActivity: { content: { body: "Follow-up question here" } },
142
+ promptContext: "## Issue\nENG-123: Fix webhook routing\n\n## Guidance\nAlways run lint before committing.\n\n## Comments\nSome prior thread.",
136
143
  webhookId: "webhook-prompted-1",
137
144
  ...overrides,
138
145
  };
@@ -628,4 +628,154 @@ describe("webhook scenario tests — full handler flows", () => {
628
628
  expect(mockGetIssueDetails).not.toHaveBeenCalled();
629
629
  });
630
630
  });
631
+
632
+ describe("Guidance integration", () => {
633
+ it("created: appends guidance to agent prompt", async () => {
634
+ const api = createApi();
635
+ const payload = makeAgentSessionEventCreated({
636
+ guidance: "Always use the main branch. Run make test before closing.",
637
+ });
638
+ await postWebhook(api, payload);
639
+
640
+ await waitForMock(mockClearActiveSession);
641
+
642
+ expect(mockRunAgent).toHaveBeenCalledOnce();
643
+ const runArgs = mockRunAgent.mock.calls[0][0];
644
+ expect(runArgs.message).toContain("Additional Guidance");
645
+ expect(runArgs.message).toContain("Always use the main branch");
646
+ });
647
+
648
+ it("created: guidance is NOT used as user message", async () => {
649
+ const api = createApi();
650
+ const payload = makeAgentSessionEventCreated({
651
+ guidance: "Always use the main branch. Run make test before closing.",
652
+ previousComments: [
653
+ { body: "Please fix the routing bug", userId: "user-1", createdAt: new Date().toISOString() },
654
+ ],
655
+ });
656
+ await postWebhook(api, payload);
657
+
658
+ await waitForMock(mockClearActiveSession);
659
+
660
+ expect(mockRunAgent).toHaveBeenCalledOnce();
661
+ const msg = mockRunAgent.mock.calls[0][0].message;
662
+
663
+ // Guidance text should appear in the appendix section, not as the user's comment
664
+ const userMsgSection = msg.split("Additional Guidance")[0];
665
+ expect(userMsgSection).toContain("Please fix the routing bug");
666
+ // The guidance string itself should not appear before the appendix
667
+ expect(userMsgSection).not.toContain("Always use the main branch");
668
+ });
669
+
670
+ it("prompted: includes guidance from promptContext", async () => {
671
+ const api = createApi();
672
+ const payload = makeAgentSessionEventPrompted({
673
+ agentActivity: { content: { body: "Can you also fix the tests?" } },
674
+ promptContext: "## Issue\nENG-123\n\n## Guidance\nUse TypeScript strict mode.\n\n## Comments\nThread.",
675
+ });
676
+ await postWebhook(api, payload);
677
+
678
+ await waitForMock(mockClearActiveSession);
679
+
680
+ expect(mockRunAgent).toHaveBeenCalledOnce();
681
+ const msg = mockRunAgent.mock.calls[0][0].message;
682
+ expect(msg).toContain("Can you also fix the tests?");
683
+ expect(msg).toContain("Additional Guidance");
684
+ expect(msg).toContain("Use TypeScript strict mode");
685
+ });
686
+
687
+ it("guidance disabled via config: no guidance section in prompt", async () => {
688
+ const api = createApi();
689
+ (api as any).pluginConfig = { defaultAgentId: "mal", enableGuidance: false };
690
+ const payload = makeAgentSessionEventCreated({
691
+ guidance: "Should not appear in prompt",
692
+ });
693
+ await postWebhook(api, payload);
694
+
695
+ await waitForMock(mockClearActiveSession);
696
+
697
+ expect(mockRunAgent).toHaveBeenCalledOnce();
698
+ const msg = mockRunAgent.mock.calls[0][0].message;
699
+ expect(msg).not.toContain("Additional Guidance");
700
+ expect(msg).not.toContain("Should not appear in prompt");
701
+ });
702
+
703
+ it("team override disables guidance for specific team", async () => {
704
+ const api = createApi();
705
+ (api as any).pluginConfig = {
706
+ defaultAgentId: "mal",
707
+ enableGuidance: true,
708
+ teamGuidanceOverrides: { "team-1": false },
709
+ };
710
+ const payload = makeAgentSessionEventCreated({
711
+ guidance: "Should be suppressed for team-1",
712
+ });
713
+ await postWebhook(api, payload);
714
+
715
+ await waitForMock(mockClearActiveSession);
716
+
717
+ expect(mockRunAgent).toHaveBeenCalledOnce();
718
+ const msg = mockRunAgent.mock.calls[0][0].message;
719
+ expect(msg).not.toContain("Additional Guidance");
720
+ expect(msg).not.toContain("Should be suppressed");
721
+ });
722
+
723
+ it("comment handler uses cached guidance from prior session event", async () => {
724
+ // Step 1: Trigger a created event to cache guidance
725
+ const api = createApi();
726
+ const sessionPayload = makeAgentSessionEventCreated({
727
+ guidance: "Cached guidance from session event",
728
+ });
729
+ await postWebhook(api, sessionPayload);
730
+ await waitForMock(mockClearActiveSession);
731
+
732
+ // Reset mocks for the next webhook
733
+ vi.clearAllMocks();
734
+ mockGetViewerId.mockResolvedValue("viewer-bot-1");
735
+ mockGetIssueDetails.mockResolvedValue(makeIssueDetails());
736
+ mockCreateComment.mockResolvedValue("comment-new-id");
737
+ mockEmitActivity.mockResolvedValue(undefined);
738
+ mockUpdateSession.mockResolvedValue(undefined);
739
+ mockUpdateIssue.mockResolvedValue(true);
740
+ mockCreateSessionOnIssue.mockResolvedValue({ sessionId: "session-mock-2" });
741
+ mockGetTeamLabels.mockResolvedValue([]);
742
+ mockGetTeamStates.mockResolvedValue([
743
+ { id: "st-backlog", name: "Backlog", type: "backlog" },
744
+ { id: "st-started", name: "In Progress", type: "started" },
745
+ { id: "st-done", name: "Done", type: "completed" },
746
+ ]);
747
+ mockRunAgent.mockResolvedValue({ success: true, output: "Agent response text" });
748
+ mockClassifyIntent.mockResolvedValue({
749
+ intent: "ask_agent",
750
+ agentId: "mal",
751
+ reasoning: "user requesting help",
752
+ fromFallback: false,
753
+ });
754
+
755
+ // Step 2: Now send a comment — it should pick up cached guidance
756
+ const commentPayload = makeCommentCreate({
757
+ data: {
758
+ id: "comment-guidance-1",
759
+ body: "Can you investigate further?",
760
+ user: { id: "user-human", name: "Human" },
761
+ issue: {
762
+ id: "issue-1",
763
+ identifier: "ENG-123",
764
+ title: "Fix webhook routing",
765
+ team: { id: "team-1" },
766
+ project: null,
767
+ },
768
+ createdAt: new Date().toISOString(),
769
+ },
770
+ });
771
+ await postWebhook(api, commentPayload);
772
+
773
+ await waitForMock(mockClearActiveSession);
774
+
775
+ expect(mockRunAgent).toHaveBeenCalledOnce();
776
+ const msg = mockRunAgent.mock.calls[0][0].message;
777
+ expect(msg).toContain("Additional Guidance");
778
+ expect(msg).toContain("Cached guidance from session event");
779
+ });
780
+ });
631
781
  });
@@ -519,7 +519,7 @@ describe("checkCodeRunDeep", () => {
519
519
  }
520
520
  });
521
521
 
522
- // API key tests — these are fast (no live invocation), use separate calls
522
+ // API key tests — still call checkCodeRunDeep which runs live CLI checks
523
523
  it("detects API key from plugin config", async () => {
524
524
  const origKey = process.env.ANTHROPIC_API_KEY;
525
525
  delete process.env.ANTHROPIC_API_KEY;
@@ -533,7 +533,7 @@ describe("checkCodeRunDeep", () => {
533
533
  if (origKey) process.env.ANTHROPIC_API_KEY = origKey;
534
534
  else delete process.env.ANTHROPIC_API_KEY;
535
535
  }
536
- });
536
+ }, 120_000);
537
537
 
538
538
  it("warns when API key missing", async () => {
539
539
  const origKeys = {
@@ -556,5 +556,5 @@ describe("checkCodeRunDeep", () => {
556
556
  else delete process.env[k];
557
557
  }
558
558
  }
559
- });
559
+ }, 120_000);
560
560
  });