@awebai/oats 0.22.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 (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +380 -0
  3. package/bin/oats.mjs +3294 -0
  4. package/capabilities/oats-authoring/oats.json +12 -0
  5. package/capabilities/oats-aweb/bin/oats-aweb.mjs +297 -0
  6. package/capabilities/oats-aweb/injects/aweb.md +55 -0
  7. package/capabilities/oats-aweb/oats.json +47 -0
  8. package/capabilities/oats-aweb/package.json +20 -0
  9. package/capabilities/oats-jira/bin/oats-jira.mjs +40 -0
  10. package/capabilities/oats-jira/injects/jira.md +10 -0
  11. package/capabilities/oats-jira/oats.json +22 -0
  12. package/capabilities/oats-jira/package.json +25 -0
  13. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +176 -0
  14. package/capabilities/oats-linear/README.md +234 -0
  15. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +32 -0
  16. package/capabilities/oats-linear/bin/oats-linear.mjs +344 -0
  17. package/capabilities/oats-linear/injects/linear.md +8 -0
  18. package/capabilities/oats-linear/oats.json +24 -0
  19. package/capabilities/oats-linear/package.json +29 -0
  20. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +217 -0
  21. package/capabilities/oats-linear/test/oats-linear.test.mjs +168 -0
  22. package/capabilities/oats-okf/agents/memory-harvest.md +25 -0
  23. package/capabilities/oats-okf/bin/oats-okf.mjs +283 -0
  24. package/capabilities/oats-okf/injects/okf.md +62 -0
  25. package/capabilities/oats-okf/oats.json +20 -0
  26. package/capabilities/oats-okf/package.json +22 -0
  27. package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +100 -0
  28. package/capabilities/oats-okf/skills/okf/SKILL.md +107 -0
  29. package/capabilities/oats-okf/skills/okf/scripts/okf-validate.mjs +123 -0
  30. package/capabilities/oats-review/agents/reviewer/AGENTS.md +53 -0
  31. package/capabilities/oats-review/agents/reviewer/soul.yaml +6 -0
  32. package/capabilities/oats-review/injects/review.md +68 -0
  33. package/capabilities/oats-review/oats.json +10 -0
  34. package/capabilities/oats-review/skills/code-review/SKILL.md +44 -0
  35. package/capabilities/oats-review/skills/security-review/SKILL.md +59 -0
  36. package/docs/capabilities.md +505 -0
  37. package/docs/capability-manifest.schema.json +223 -0
  38. package/docs/configuration.md +482 -0
  39. package/docs/conventions.md +73 -0
  40. package/docs/design/desktop-ux-plan.md +362 -0
  41. package/docs/design/package-engine-contract.md +813 -0
  42. package/docs/design/package-runtime-api.md +414 -0
  43. package/docs/desktop-cli-api.md +89 -0
  44. package/docs/desktop-succession.md +51 -0
  45. package/docs/desktop.md +187 -0
  46. package/docs/implementation.md +282 -0
  47. package/docs/integrations.md +123 -0
  48. package/docs/knowledge-theory.md +97 -0
  49. package/docs/knowledge.md +139 -0
  50. package/docs/layers.md +110 -0
  51. package/docs/migration-from-oas.md +122 -0
  52. package/docs/oats-config.schema.json +140 -0
  53. package/docs/oats-lock.schema.json +175 -0
  54. package/docs/oats-package.schema.json +129 -0
  55. package/docs/packages.md +442 -0
  56. package/docs/release-lane.md +132 -0
  57. package/docs/release-notes/v0.18.2.md +79 -0
  58. package/docs/release-notes/v0.18.3.md +63 -0
  59. package/docs/release-notes/v0.18.4.md +65 -0
  60. package/docs/release-notes/v0.18.5.md +66 -0
  61. package/docs/release-notes/v0.18.6.md +87 -0
  62. package/docs/release-notes/v0.19.0.md +186 -0
  63. package/docs/release-notes/v0.19.1.md +24 -0
  64. package/docs/release-notes/v0.19.2.md +32 -0
  65. package/docs/release-notes/v0.19.3.md +16 -0
  66. package/docs/release-notes/v0.19.4.md +12 -0
  67. package/docs/release-notes/v0.20.0.md +89 -0
  68. package/docs/release-notes/v0.22.0.md +180 -0
  69. package/docs/souls-and-instances.md +344 -0
  70. package/injects/framework-workspace.md +7 -0
  71. package/injects/instance-boundary.md +43 -0
  72. package/injects/local-soul.md +19 -0
  73. package/injects/oats.md +11 -0
  74. package/injects/work-attached.md +16 -0
  75. package/injects/work-checkout.md +12 -0
  76. package/injects/work-workspace.md +22 -0
  77. package/injects/work-worktree.md +13 -0
  78. package/lib/core.mjs +6792 -0
  79. package/lib/packages.mjs +1375 -0
  80. package/lib/tmux-config.mjs +35 -0
  81. package/package-catalog.json +44 -0
  82. package/package.json +55 -0
  83. package/packages/record/README.md +151 -0
  84. package/packages/record/bin/capture.mjs +256 -0
  85. package/packages/record/bin/recall.mjs +102 -0
  86. package/packages/record/bin/setup.mjs +281 -0
  87. package/packages/record/bin/turn-record.mjs +47 -0
  88. package/packages/record/docs/turn-record-sot.md +350 -0
  89. package/packages/record/lib/canonical.mjs +151 -0
  90. package/packages/record/lib/capture-aw.mjs +179 -0
  91. package/packages/record/lib/capture-cc.mjs +322 -0
  92. package/packages/record/lib/formats.mjs +296 -0
  93. package/packages/record/lib/ignore.mjs +155 -0
  94. package/packages/record/lib/index-db.mjs +524 -0
  95. package/packages/record/lib/project-aweb.mjs +251 -0
  96. package/packages/record/lib/segments.mjs +186 -0
  97. package/packages/record/lib/store.mjs +447 -0
  98. package/packages/record/lib/tags.mjs +114 -0
  99. package/packages/record/package.json +45 -0
  100. package/packages/record/test/vectors/README.md +34 -0
  101. package/packages/record/test/vectors/aweb-projection-v1.json +231 -0
  102. package/packages/record/test/vectors/journal-merge-v1.json +261 -0
  103. package/packages/record/test/vectors/turn-id-v1.json +152 -0
  104. package/packages/record/test/vectors/validate.mjs +391 -0
  105. package/skills/integration-authoring/SKILL.md +76 -0
  106. package/skills/oats/SKILL.md +151 -0
  107. package/skills/oats-config/SKILL.md +162 -0
  108. package/skills/oats-getting-started/SKILL.md +159 -0
  109. package/skills/oats-packages/SKILL.md +180 -0
  110. package/skills/oats-support/SKILL.md +79 -0
  111. package/skills/skill-craft/SKILL.md +109 -0
  112. package/skills/soul-craft/SKILL.md +109 -0
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * JSON-first Linear task operations for OATS.
4
+ *
5
+ * Uses Linear's official GraphQL API directly. No third-party Linear CLI or
6
+ * SDK is required; authentication is a personal key in LINEAR_API_KEY.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+
10
+ const API_URL = process.env.LINEAR_API_URL || "https://api.linear.app/graphql";
11
+ const argv = process.argv.slice(2);
12
+ const command = argv.shift();
13
+
14
+ function die(message, details) {
15
+ process.stderr.write(JSON.stringify({ error: String(message), ...(details ? { details } : {}) }, null, 2) + "\n");
16
+ process.exit(1);
17
+ }
18
+ function print(value) { process.stdout.write(JSON.stringify(value, null, 2) + "\n"); }
19
+ function parseArgs(values) {
20
+ const options = new Map();
21
+ const positional = [];
22
+ for (let i = 0; i < values.length; i++) {
23
+ const value = values[i];
24
+ if (!value.startsWith("--")) { positional.push(value); continue; }
25
+ const name = value.slice(2);
26
+ const next = values[i + 1];
27
+ const parsed = next !== undefined && !next.startsWith("--") ? values[++i] : true;
28
+ const previous = options.get(name) || [];
29
+ previous.push(parsed);
30
+ options.set(name, previous);
31
+ }
32
+ return {
33
+ positional,
34
+ has: (name) => options.has(name),
35
+ one: (name) => options.get(name)?.at(-1),
36
+ many: (name) => options.get(name) || [],
37
+ };
38
+ }
39
+ const args = parseArgs(argv);
40
+
41
+ async function graphql(query, variables = {}) {
42
+ const key = process.env.LINEAR_API_KEY;
43
+ if (!key) die("LINEAR_API_KEY is not set", "Create a personal API key in Linear Settings → Security & access → API keys, export it, then run `oats linear auth`.");
44
+ let response;
45
+ try {
46
+ response = await fetch(API_URL, {
47
+ method: "POST",
48
+ headers: {
49
+ "Authorization": key,
50
+ "Content-Type": "application/json",
51
+ "User-Agent": "oats-linear/0.1",
52
+ },
53
+ body: JSON.stringify({ query, variables }),
54
+ signal: AbortSignal.timeout(30000),
55
+ });
56
+ } catch (error) {
57
+ die(`Linear API request failed: ${error.message || error}`);
58
+ }
59
+ const text = await response.text();
60
+ let payload;
61
+ try { payload = JSON.parse(text); }
62
+ catch { die(`Linear API returned HTTP ${response.status} with non-JSON content`, text.slice(0, 500)); }
63
+ if (!response.ok || payload.errors?.length) {
64
+ const errors = (payload.errors || []).map((error) => ({
65
+ message: error.extensions?.userPresentableMessage || error.message,
66
+ code: error.extensions?.code,
67
+ path: error.path,
68
+ }));
69
+ const hint = response.status === 401 ? "Check LINEAR_API_KEY and run `oats linear auth`." : undefined;
70
+ die(`Linear API request failed (HTTP ${response.status})`, { errors, ...(hint ? { hint } : {}) });
71
+ }
72
+ return payload.data;
73
+ }
74
+
75
+ const PAGE_INFO = "pageInfo { hasNextPage endCursor }";
76
+ const ISSUE_FIELDS = `
77
+ id identifier title description url priority priorityLabel
78
+ team { id key name }
79
+ state { id name type }
80
+ project { id name slugId }
81
+ parent { id identifier title }
82
+ assignee { id name email }
83
+ labels(first: 100) { nodes { id name } }
84
+ `;
85
+
86
+ async function teamByKey(key) {
87
+ if (!key || key === true) die("--team <KEY> is required");
88
+ const data = await graphql(`
89
+ query OatsLinearTeam($key: String!) {
90
+ teams(first: 2, filter: { key: { eqIgnoreCase: $key } }) { nodes { id key name } }
91
+ }
92
+ `, { key });
93
+ if (data.teams.nodes.length === 0) die(`Linear team "${key}" was not found`, "Run `oats linear teams` and use its key.");
94
+ if (data.teams.nodes.length > 1) die(`Linear team key "${key}" is ambiguous`);
95
+ return data.teams.nodes[0];
96
+ }
97
+
98
+ async function statesForTeam(team) {
99
+ const data = await graphql(`
100
+ query OatsLinearStates($id: String!) {
101
+ team(id: $id) { states(first: 100) { nodes { id name type position } } }
102
+ }
103
+ `, { id: team.id });
104
+ return data.team.states.nodes.sort((a, b) => a.position - b.position);
105
+ }
106
+ async function stateByName(team, name) {
107
+ const states = await statesForTeam(team);
108
+ const matches = states.filter((state) => state.id === name || state.name.toLowerCase() === String(name).toLowerCase());
109
+ if (matches.length !== 1) die(`Workflow state "${name}" was not found for team ${team.key}`, { available: states.map((state) => `${state.name} (${state.type})`) });
110
+ return matches[0];
111
+ }
112
+
113
+ async function labelsForTeam(team) {
114
+ const data = await graphql(`
115
+ query OatsLinearLabels($teamId: ID!) {
116
+ issueLabels(first: 250, filter: { or: [
117
+ { team: { null: true } },
118
+ { team: { id: { eq: $teamId } } }
119
+ ] }) {
120
+ nodes { id name color isGroup team { id key } }
121
+ }
122
+ }
123
+ `, { teamId: team.id });
124
+ return data.issueLabels.nodes;
125
+ }
126
+ async function findLabel(team, name) {
127
+ const labels = await labelsForTeam(team);
128
+ const matches = labels.filter((label) => !label.isGroup && label.name.toLowerCase() === String(name).toLowerCase());
129
+ const scoped = matches.find((label) => label.team?.id === team.id);
130
+ return scoped || matches.find((label) => !label.team);
131
+ }
132
+ async function ensureAgentLabel(team, alias) {
133
+ const name = `agent-${alias}`;
134
+ const existing = await findLabel(team, name);
135
+ if (existing) return existing;
136
+ const data = await graphql(`
137
+ mutation OatsLinearCreateLabel($input: IssueLabelCreateInput!) {
138
+ issueLabelCreate(input: $input) { success issueLabel { id name color team { id key } } }
139
+ }
140
+ `, { input: { name, teamId: team.id, color: "#5E6AD2", description: "OATS agent instance identity" } });
141
+ if (!data.issueLabelCreate.success) die(`Linear did not create label "${name}"`);
142
+ return data.issueLabelCreate.issueLabel;
143
+ }
144
+ async function labelByName(team, name) {
145
+ const label = await findLabel(team, name);
146
+ if (!label) die(`Label "${name}" was not found for team ${team.key}`, "Create it in Linear first. Agent labels are created automatically by --agent.");
147
+ return label;
148
+ }
149
+
150
+ async function projectsForTeam(team) {
151
+ const data = await graphql(`
152
+ query OatsLinearProjects($teamId: ID!) {
153
+ projects(first: 250, filter: { accessibleTeams: { some: { id: { eq: $teamId } } } }) {
154
+ nodes { id name slugId status { id name type } teams(first: 20) { nodes { id key name } } }
155
+ }
156
+ }
157
+ `, { teamId: team.id });
158
+ return data.projects.nodes;
159
+ }
160
+ async function projectByRef(team, ref) {
161
+ const projects = await projectsForTeam(team);
162
+ const needle = String(ref).toLowerCase();
163
+ const matches = projects.filter((project) =>
164
+ project.id === ref || project.slugId.toLowerCase() === needle || project.name.toLowerCase() === needle);
165
+ if (matches.length !== 1) die(`Project "${ref}" ${matches.length ? "is ambiguous" : "was not found"} for team ${team.key}`, { available: projects.map((project) => ({ name: project.name, slug: project.slugId })) });
166
+ return matches[0];
167
+ }
168
+
169
+ async function issueById(id) {
170
+ if (!id || id === true) die("an issue identifier such as ENG-123 is required");
171
+ const data = await graphql(`
172
+ query OatsLinearIssue($id: String!) { issue(id: $id) { ${ISSUE_FIELDS} } }
173
+ `, { id });
174
+ return data.issue;
175
+ }
176
+
177
+ function textOption(name) {
178
+ const inline = args.one(name);
179
+ const file = args.one(`${name}-file`);
180
+ if (inline !== undefined && file !== undefined) die(`use only one of --${name} or --${name}-file`);
181
+ if (file !== undefined) {
182
+ if (file === true) die(`--${name}-file needs a path`);
183
+ try { return readFileSync(file, "utf8").trim(); }
184
+ catch (error) { die(`cannot read --${name}-file ${file}: ${error.message}`); }
185
+ }
186
+ return inline;
187
+ }
188
+ function assertTerminalAllowed(state) {
189
+ if (["completed", "canceled", "duplicate"].includes(state.type) && !args.has("allow-terminal")) {
190
+ die(`refusing terminal state "${state.name}" without --allow-terminal`, "Agents should hand work to review, not close or cancel it. Use --allow-terminal only with explicit human authorization.");
191
+ }
192
+ }
193
+
194
+ async function auth() {
195
+ const data = await graphql(`
196
+ query OatsLinearAuth { viewer { id name email } organization { id name urlKey } }
197
+ `);
198
+ print({ authenticated: true, endpoint: API_URL, viewer: data.viewer, workspace: data.organization });
199
+ }
200
+ async function teams() {
201
+ const data = await graphql(`
202
+ query OatsLinearTeams { teams(first: 100) { nodes { id key name } } }
203
+ `);
204
+ print(data.teams.nodes);
205
+ }
206
+ async function states() {
207
+ const team = await teamByKey(args.one("team"));
208
+ print(await statesForTeam(team));
209
+ }
210
+ async function projects() {
211
+ const team = await teamByKey(args.one("team"));
212
+ print(await projectsForTeam(team));
213
+ }
214
+ async function labels() {
215
+ const team = await teamByKey(args.one("team"));
216
+ print(await labelsForTeam(team));
217
+ }
218
+
219
+ async function listIssues() {
220
+ const team = await teamByKey(args.one("team"));
221
+ const requestedLimit = Number(args.one("limit") || 100);
222
+ if (!Number.isInteger(requestedLimit) || requestedLimit < 1 || requestedLimit > 250) die("--limit must be an integer from 1 to 250");
223
+ const filter = { team: { id: { eq: team.id } } };
224
+ if (!args.has("all")) filter.state = { type: { nin: ["completed", "canceled", "duplicate"] } };
225
+ if (args.one("agent")) filter.labels = { some: { name: { eqIgnoreCase: `agent-${args.one("agent")}` } } };
226
+ if (args.one("project")) {
227
+ const project = await projectByRef(team, args.one("project"));
228
+ filter.project = { id: { eq: project.id } };
229
+ }
230
+ const data = await graphql(`
231
+ query OatsLinearIssues($first: Int!, $filter: IssueFilter) {
232
+ issues(first: $first, filter: $filter) { nodes { ${ISSUE_FIELDS} } ${PAGE_INFO} }
233
+ }
234
+ `, { first: requestedLimit, filter });
235
+ print({ issues: data.issues.nodes, pageInfo: data.issues.pageInfo });
236
+ }
237
+ async function createIssue() {
238
+ const team = await teamByKey(args.one("team"));
239
+ const title = args.one("title");
240
+ if (!title || title === true) die("--title <text> is required");
241
+ let description = textOption("description");
242
+ const input = { teamId: team.id, title };
243
+ if (description !== undefined) input.description = description;
244
+ if (args.one("project")) input.projectId = (await projectByRef(team, args.one("project"))).id;
245
+ if (args.one("parent")) input.parentId = args.one("parent");
246
+ if (args.one("state")) {
247
+ const state = await stateByName(team, args.one("state"));
248
+ assertTerminalAllowed(state);
249
+ input.stateId = state.id;
250
+ }
251
+ const issueLabels = [];
252
+ if (args.one("agent")) {
253
+ const alias = args.one("agent");
254
+ issueLabels.push(await ensureAgentLabel(team, alias));
255
+ if (!/^Agent:/mi.test(description || "")) {
256
+ description = `${description ? `${description.trim()}\n\n` : ""}---\nAgent: ${alias}`;
257
+ input.description = description;
258
+ }
259
+ }
260
+ for (const name of args.many("label")) issueLabels.push(await labelByName(team, name));
261
+ if (issueLabels.length) input.labelIds = [...new Set(issueLabels.map((label) => label.id))];
262
+ const data = await graphql(`
263
+ mutation OatsLinearIssueCreate($input: IssueCreateInput!) {
264
+ issueCreate(input: $input) { success issue { ${ISSUE_FIELDS} } }
265
+ }
266
+ `, { input });
267
+ if (!data.issueCreate.success || !data.issueCreate.issue) die("Linear did not create the issue");
268
+ print(data.issueCreate.issue);
269
+ }
270
+ async function updateIssue(id) {
271
+ const current = await issueById(id);
272
+ const team = current.team;
273
+ const input = {};
274
+ if (args.has("title")) input.title = args.one("title");
275
+ const description = textOption("description");
276
+ if (description !== undefined) input.description = description;
277
+ if (args.one("state")) {
278
+ const state = await stateByName(team, args.one("state"));
279
+ assertTerminalAllowed(state);
280
+ input.stateId = state.id;
281
+ }
282
+ const added = [];
283
+ if (args.one("agent")) added.push((await ensureAgentLabel(team, args.one("agent"))).id);
284
+ for (const name of args.many("add-label")) added.push((await labelByName(team, name)).id);
285
+ const removed = [];
286
+ for (const name of args.many("remove-label")) removed.push((await labelByName(team, name)).id);
287
+ if (added.length) input.addedLabelIds = [...new Set(added)];
288
+ if (removed.length) input.removedLabelIds = [...new Set(removed)];
289
+ if (Object.keys(input).length === 0) die("no update supplied", "Use --title, --description[-file], --state, --agent, --add-label, or --remove-label.");
290
+ const data = await graphql(`
291
+ mutation OatsLinearIssueUpdate($id: String!, $input: IssueUpdateInput!) {
292
+ issueUpdate(id: $id, input: $input) { success issue { ${ISSUE_FIELDS} } }
293
+ }
294
+ `, { id, input });
295
+ if (!data.issueUpdate.success || !data.issueUpdate.issue) die(`Linear did not update ${id}`);
296
+ print(data.issueUpdate.issue);
297
+ }
298
+ async function commentIssue(id) {
299
+ const body = textOption("body");
300
+ if (!body || body === true) die("--body <markdown> or --body-file <path> is required");
301
+ const data = await graphql(`
302
+ mutation OatsLinearComment($input: CommentCreateInput!) {
303
+ commentCreate(input: $input) { success comment { id body createdAt url user { id name } } }
304
+ }
305
+ `, { input: { issueId: id, body } });
306
+ if (!data.commentCreate.success) die(`Linear did not comment on ${id}`);
307
+ print(data.commentCreate.comment);
308
+ }
309
+
310
+ function usage() {
311
+ process.stderr.write(`oats linear commands (all output JSON):
312
+ auth
313
+ teams
314
+ states --team <KEY>
315
+ projects --team <KEY>
316
+ labels --team <KEY>
317
+ issue list --team <KEY> [--agent <alias>] [--project <name|slug>] [--all] [--limit 100]
318
+ issue get <KEY-123>
319
+ issue create --team <KEY> --title <text> [--description <md>|--description-file <path>]
320
+ [--project <name|slug>] [--parent <KEY-123>] [--state <name>] [--agent <alias>]
321
+ [--label <name> ...]
322
+ issue update <KEY-123> [--title <text>] [--description <md>|--description-file <path>]
323
+ [--state <name>] [--agent <alias>] [--add-label <name> ...] [--remove-label <name> ...]
324
+ [--allow-terminal]
325
+ issue comment <KEY-123> (--body <md>|--body-file <path>)
326
+ `);
327
+ process.exit(1);
328
+ }
329
+
330
+ if (command === "auth") await auth();
331
+ else if (command === "teams") await teams();
332
+ else if (command === "states") await states();
333
+ else if (command === "projects") await projects();
334
+ else if (command === "labels") await labels();
335
+ else if (command === "issue") {
336
+ const subcommand = args.positional[0];
337
+ const id = args.positional[1];
338
+ if (subcommand === "list") await listIssues();
339
+ else if (subcommand === "get") print(await issueById(id));
340
+ else if (subcommand === "create") await createIssue();
341
+ else if (subcommand === "update") await updateIssue(id);
342
+ else if (subcommand === "comment") await commentIssue(id);
343
+ else usage();
344
+ } else usage();
@@ -0,0 +1,8 @@
1
+ ## Tasks: Linear
2
+
3
+ Your tasks layer is **Linear**, operated through the JSON-first `oats linear`
4
+ commands. You are identified by the label `agent-<your-instance-name>`, not
5
+ by changing the human assignee. Load the **linear-tasks** skill before reading
6
+ your queue, creating or updating issues/sub-issues, changing status, or posting
7
+ handoffs. Tasks only: status and outcomes live in Linear; conversation lives
8
+ in your deployment's messaging layer.
@@ -0,0 +1,24 @@
1
+ {
2
+ "capability": "oats.linear",
3
+ "command": "linear",
4
+ "version": "1.0.0",
5
+ "compatibility": { "oats": ">=0.6.2" },
6
+ "layer": "tasks",
7
+ "description": "Tasks layer via Linear: JSON-first GraphQL commands, project/issue/sub-issue workflow, label-based agent identity.",
8
+ "requires": [],
9
+ "skills": [
10
+ "skills"
11
+ ],
12
+ "commands": {
13
+ "auth": "bin/oats-linear.mjs auth",
14
+ "teams": "bin/oats-linear.mjs teams",
15
+ "states": "bin/oats-linear.mjs states",
16
+ "projects": "bin/oats-linear.mjs projects",
17
+ "labels": "bin/oats-linear.mjs labels",
18
+ "issue": "bin/oats-linear.mjs issue"
19
+ },
20
+ "inject": "injects/linear.md",
21
+ "hooks": {
22
+ "spawn": "bin/oats-linear-hook.mjs spawn"
23
+ }
24
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@awebai/oats-linear",
3
+ "version": "0.1.0",
4
+ "description": "OATS tasks provider: Linear via its GraphQL API, with JSON-first commands and label-based agent identity",
5
+ "keywords": [
6
+ "pi-package",
7
+ "oats",
8
+ "linear",
9
+ "tasks"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/awebai/oats",
14
+ "directory": "capabilities/oats-linear"
15
+ },
16
+ "license": "MIT",
17
+ "type": "module",
18
+ "scripts": {
19
+ "test": "node --test"
20
+ },
21
+ "bin": {
22
+ "oats-linear": "./bin/oats-linear.mjs"
23
+ },
24
+ "pi": {
25
+ "skills": [
26
+ "./skills"
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,217 @@
1
+ ---
2
+ name: linear-tasks
3
+ description: >-
4
+ Linear task tracking for OATS agent instances. Use when reading an agent's
5
+ Linear work queue, opening or inspecting an issue, creating issues or
6
+ sub-issues, claiming work with an agent label, posting progress/blocker/
7
+ handoff comments, or moving work through Linear workflow states. Also use
8
+ when asked about "my issue", "the project", "the board", a Linear issue key
9
+ such as ENG-123, or shared task status. Uses JSON-first `oats linear` commands.
10
+ ---
11
+
12
+ # Agent task tracking in Linear
13
+
14
+ Linear is your deployment's **tasks layer**: task status and outcomes live
15
+ here. Conversation lives in the messaging layer; a message may nudge someone,
16
+ but it never replaces the Linear update.
17
+
18
+ ## Deployment target and authentication
19
+
20
+ Get the target from the `Tasks: Linear` line in your `TASK.md` briefing:
21
+
22
+ - **team** is required and uses Linear's issue-prefix key (for example `ENG`).
23
+ - **project** is an optional deployment default. Do not invent one when unset.
24
+ - **alias** is your exact OATS instance name; your label is `agent-<alias>`.
25
+
26
+ Before the first operation, run:
27
+
28
+ ```bash
29
+ oats linear auth
30
+ oats linear teams
31
+ ```
32
+
33
+ If `LINEAR_API_KEY` is missing or rejected, **stop and ask the human** to create
34
+ or export a personal API key (Linear Settings → Security & access → API keys).
35
+ Never ask for the key's value, print it, put it in a command argument, or store
36
+ it in OATS config/files. Never attempt an interactive login.
37
+
38
+ Commands emit JSON. An error is JSON on stderr with a non-zero exit code; act
39
+ on that error rather than retrying variants blindly.
40
+
41
+ ## Hierarchy
42
+
43
+ - **Project** — an optional, human-owned outcome or initiative container. Do
44
+ not create, rename, change status, or close projects.
45
+ - **Issue** — the normal bounded work item assigned to an agent.
46
+ - **Sub-issue** — an issue with a parent, used only when the parent genuinely
47
+ decomposes into multiple independently verifiable pieces.
48
+
49
+ Do not create placeholder parent issues for one child. Every issue belongs to
50
+ a team; project membership is optional unless your briefing names a project.
51
+
52
+ ## Project context and documentation
53
+
54
+ Use project-level and issue-level records deliberately:
55
+
56
+ - **Project overview**: intent, scope/non-goals, ownership, constraints, human
57
+ gates, architecture, and success criteria.
58
+ - **Project documents**: detailed designs, decisions, runbooks, and research.
59
+ - **Issues/sub-issues**: bounded execution and acceptance criteria.
60
+ - **Issue comments**: milestones, blockers, handoffs, verification, and links.
61
+
62
+ The overview/documents explain the work; issues execute it. Link a governing
63
+ project document from each affected issue rather than copying inconsistent
64
+ versions. Keep task status in issues, not project prose or messaging.
65
+
66
+ The current wrapper can discover project metadata but **cannot read or mutate
67
+ project overview Markdown or Linear documents**:
68
+
69
+ ```bash
70
+ oats linear projects --team <TEAM>
71
+ ```
72
+
73
+ That output includes project IDs, names, slugs, status, and teams. Project
74
+ creation, lifecycle/status, overview content, documents, and project updates
75
+ remain human-owned in the Linear UI. If your task depends on unavailable
76
+ project documentation, ask the human for its URL/content; never infer policy
77
+ from an issue title.
78
+
79
+ ## Identity and ownership
80
+
81
+ - Keep the **human assignee unchanged**. A personal API key acts as its human;
82
+ OATS agents are not Linear users.
83
+ - Claim work with label `agent-<exact-instance-name>`. `--agent <alias>` creates
84
+ this team-scoped label on first use and applies it.
85
+ - New issue descriptions also receive `Agent: <alias>`. On existing issues,
86
+ use the label and comments; do not rewrite a human's description merely to
87
+ add the line.
88
+ - Never delete issues, labels, or comments. Do not change cycle, priority,
89
+ project, parent, or assignee unless explicitly directed.
90
+
91
+ ## Read before writing
92
+
93
+ ```bash
94
+ # Your open queue (terminal states excluded by default)
95
+ oats linear issue list --team <TEAM> --agent <alias>
96
+
97
+ # Narrow to the deployment project when one is configured
98
+ oats linear issue list --team <TEAM> --agent <alias> --project "<PROJECT>"
99
+
100
+ # Read full task context before acting
101
+ oats linear issue get <TEAM>-123
102
+
103
+ # Discover this team's real workflow names; never guess them
104
+ oats linear states --team <TEAM>
105
+ ```
106
+
107
+ `issue get` includes team, status/type, project, parent, assignee, labels,
108
+ description, and URL. Read the parent too when working a sub-issue. Record the
109
+ issue key in instance memory (`STATE.md`) if your knowledge layer provides it.
110
+
111
+ ## Work an issue
112
+
113
+ 1. Read the issue and parent/project context.
114
+ 2. If not already claimed, apply your identity label:
115
+
116
+ ```bash
117
+ oats linear issue update <TEAM>-123 --agent <alias>
118
+ ```
119
+
120
+ 3. Move to the deployment's `started` workflow state (often `In Progress`),
121
+ using the exact name returned by `oats linear states`:
122
+
123
+ ```bash
124
+ oats linear issue update <TEAM>-123 --state "In Progress"
125
+ ```
126
+
127
+ 4. Post only useful durable events, prefixed with your alias:
128
+
129
+ ```bash
130
+ oats linear issue comment <TEAM>-123 \
131
+ --body "[<alias>] milestone: implemented parser; tests pass with node --test"
132
+ oats linear issue comment <TEAM>-123 \
133
+ --body "[<alias>] blocked: need API scope decision from @owner"
134
+ oats linear issue comment <TEAM>-123 \
135
+ --body "[<alias>] handoff → <next-alias>: branch agents/x, verify with npm test"
136
+ ```
137
+
138
+ 5. When implementation is review-ready, comment the outcome (branch/PR and
139
+ verification), then move to the team's review state. Do **not** mark it
140
+ completed:
141
+
142
+ ```bash
143
+ oats linear issue comment <TEAM>-123 \
144
+ --body "[<alias>] review-ready: PR <url>; verified npm test"
145
+ oats linear issue update <TEAM>-123 --state "In Review"
146
+ ```
147
+
148
+ Workflow names vary. Agents may use backlog/unstarted/started states. The
149
+ wrapper refuses `completed`, `canceled`, and `duplicate` state types unless
150
+ `--allow-terminal` is supplied; use that override only after explicit human
151
+ authorization and mention that authorization in a comment.
152
+
153
+ ## Create bounded work
154
+
155
+ Use ≤12 words in the title. Describe requirements and acceptance checks, not a
156
+ speculative implementation. For multiline Markdown, prefer a file so shell
157
+ quoting cannot corrupt it.
158
+
159
+ ```bash
160
+ cat > /tmp/linear-description.md <<'EOF'
161
+ Why this is needed.
162
+
163
+ Acceptance:
164
+ - [ ] Observable outcome one
165
+ - [ ] Verification command or evidence
166
+ EOF
167
+
168
+ oats linear issue create --team <TEAM> --project "<PROJECT>" \
169
+ --title "Bounded outcome" --description-file /tmp/linear-description.md \
170
+ --agent <alias>
171
+ ```
172
+
173
+ Omit `--project` when the briefing has none. Create a sub-issue only for a real
174
+ independent slice:
175
+
176
+ ```bash
177
+ oats linear issue create --team <TEAM> --parent <TEAM>-123 \
178
+ --title "Independent child outcome" \
179
+ --description-file /tmp/linear-description.md --agent <alias>
180
+ ```
181
+
182
+ `--project` sets project membership; `--parent` sets issue hierarchy. They are
183
+ independent, so supply both when a sub-issue must explicitly carry the project:
184
+
185
+ ```bash
186
+ oats linear issue create --team <TEAM> --project "<PROJECT>" \
187
+ --parent <TEAM>-123 --title "Independent child outcome" \
188
+ --description-file /tmp/linear-description.md --agent <alias>
189
+ ```
190
+
191
+ Use an existing non-agent label only after discovery:
192
+
193
+ ```bash
194
+ oats linear labels --team <TEAM>
195
+ oats linear issue create --team <TEAM> --title "Fix token refresh" \
196
+ --label bug --agent <alias>
197
+ ```
198
+
199
+ ## Current command boundary
200
+
201
+ Supported: discover teams/states/projects/labels; list/get/create/update/comment
202
+ on issues; create sub-issues; claim work with agent labels.
203
+
204
+ Not supported: create/update/close projects; read/edit project overviews;
205
+ list/read/create/edit project documents; publish project updates; move an
206
+ existing issue into/out of a project; reparent an existing issue; or create
207
+ issue relations such as blocks/related. Those operations stay in the Linear
208
+ UI with the human. **Do not invent GraphQL calls or command flags to bypass
209
+ this boundary.**
210
+
211
+ ## Validate every mutation
212
+
213
+ Mutation output is the resulting issue/comment. Check its identifier, status,
214
+ project/parent, and labels immediately. Then run `issue get` for
215
+ correctness-critical changes. If a GraphQL permission or validation error
216
+ persists, post no partial workaround: preserve the task state and escalate to
217
+ the human with the exact error (never the key).