@calltelemetry/openclaw-linear 0.8.5 → 0.8.6
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 +276 -1
- package/openclaw.plugin.json +3 -1
- package/package.json +1 -1
- package/prompts.yaml +24 -12
- package/src/__test__/fixtures/webhook-payloads.ts +9 -2
- package/src/__test__/webhook-scenarios.test.ts +150 -0
- package/src/infra/doctor.test.ts +3 -3
- package/src/pipeline/guidance.test.ts +222 -0
- package/src/pipeline/guidance.ts +156 -0
- package/src/pipeline/pipeline.ts +23 -2
- package/src/pipeline/webhook.ts +68 -23
- package/src/tools/linear-issues-tool.test.ts +453 -0
- package/src/tools/linear-issues-tool.ts +338 -0
- package/src/tools/tools.test.ts +36 -7
- package/src/tools/tools.ts +9 -2
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
|
-
|
|
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
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
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
|
-
#
|
|
11
|
-
# Worker
|
|
12
|
-
# Auditor
|
|
10
|
+
# Orchestrator — linear_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
|
|
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
|
|
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
|
|
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
|
-
|
|
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: `
|
|
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: `
|
|
71
|
-
- If PASS: update status: `
|
|
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
|
|
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
|
-
|
|
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
|
});
|
package/src/infra/doctor.test.ts
CHANGED
|
@@ -519,7 +519,7 @@ describe("checkCodeRunDeep", () => {
|
|
|
519
519
|
}
|
|
520
520
|
});
|
|
521
521
|
|
|
522
|
-
// API key tests —
|
|
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
|
});
|