@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.
- package/LICENSE +21 -0
- package/README.md +380 -0
- package/bin/oats.mjs +3294 -0
- package/capabilities/oats-authoring/oats.json +12 -0
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +297 -0
- package/capabilities/oats-aweb/injects/aweb.md +55 -0
- package/capabilities/oats-aweb/oats.json +47 -0
- package/capabilities/oats-aweb/package.json +20 -0
- package/capabilities/oats-jira/bin/oats-jira.mjs +40 -0
- package/capabilities/oats-jira/injects/jira.md +10 -0
- package/capabilities/oats-jira/oats.json +22 -0
- package/capabilities/oats-jira/package.json +25 -0
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +176 -0
- package/capabilities/oats-linear/README.md +234 -0
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +32 -0
- package/capabilities/oats-linear/bin/oats-linear.mjs +344 -0
- package/capabilities/oats-linear/injects/linear.md +8 -0
- package/capabilities/oats-linear/oats.json +24 -0
- package/capabilities/oats-linear/package.json +29 -0
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +217 -0
- package/capabilities/oats-linear/test/oats-linear.test.mjs +168 -0
- package/capabilities/oats-okf/agents/memory-harvest.md +25 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +283 -0
- package/capabilities/oats-okf/injects/okf.md +62 -0
- package/capabilities/oats-okf/oats.json +20 -0
- package/capabilities/oats-okf/package.json +22 -0
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +100 -0
- package/capabilities/oats-okf/skills/okf/SKILL.md +107 -0
- package/capabilities/oats-okf/skills/okf/scripts/okf-validate.mjs +123 -0
- package/capabilities/oats-review/agents/reviewer/AGENTS.md +53 -0
- package/capabilities/oats-review/agents/reviewer/soul.yaml +6 -0
- package/capabilities/oats-review/injects/review.md +68 -0
- package/capabilities/oats-review/oats.json +10 -0
- package/capabilities/oats-review/skills/code-review/SKILL.md +44 -0
- package/capabilities/oats-review/skills/security-review/SKILL.md +59 -0
- package/docs/capabilities.md +505 -0
- package/docs/capability-manifest.schema.json +223 -0
- package/docs/configuration.md +482 -0
- package/docs/conventions.md +73 -0
- package/docs/design/desktop-ux-plan.md +362 -0
- package/docs/design/package-engine-contract.md +813 -0
- package/docs/design/package-runtime-api.md +414 -0
- package/docs/desktop-cli-api.md +89 -0
- package/docs/desktop-succession.md +51 -0
- package/docs/desktop.md +187 -0
- package/docs/implementation.md +282 -0
- package/docs/integrations.md +123 -0
- package/docs/knowledge-theory.md +97 -0
- package/docs/knowledge.md +139 -0
- package/docs/layers.md +110 -0
- package/docs/migration-from-oas.md +122 -0
- package/docs/oats-config.schema.json +140 -0
- package/docs/oats-lock.schema.json +175 -0
- package/docs/oats-package.schema.json +129 -0
- package/docs/packages.md +442 -0
- package/docs/release-lane.md +132 -0
- package/docs/release-notes/v0.18.2.md +79 -0
- package/docs/release-notes/v0.18.3.md +63 -0
- package/docs/release-notes/v0.18.4.md +65 -0
- package/docs/release-notes/v0.18.5.md +66 -0
- package/docs/release-notes/v0.18.6.md +87 -0
- package/docs/release-notes/v0.19.0.md +186 -0
- package/docs/release-notes/v0.19.1.md +24 -0
- package/docs/release-notes/v0.19.2.md +32 -0
- package/docs/release-notes/v0.19.3.md +16 -0
- package/docs/release-notes/v0.19.4.md +12 -0
- package/docs/release-notes/v0.20.0.md +89 -0
- package/docs/release-notes/v0.22.0.md +180 -0
- package/docs/souls-and-instances.md +344 -0
- package/injects/framework-workspace.md +7 -0
- package/injects/instance-boundary.md +43 -0
- package/injects/local-soul.md +19 -0
- package/injects/oats.md +11 -0
- package/injects/work-attached.md +16 -0
- package/injects/work-checkout.md +12 -0
- package/injects/work-workspace.md +22 -0
- package/injects/work-worktree.md +13 -0
- package/lib/core.mjs +6792 -0
- package/lib/packages.mjs +1375 -0
- package/lib/tmux-config.mjs +35 -0
- package/package-catalog.json +44 -0
- package/package.json +55 -0
- package/packages/record/README.md +151 -0
- package/packages/record/bin/capture.mjs +256 -0
- package/packages/record/bin/recall.mjs +102 -0
- package/packages/record/bin/setup.mjs +281 -0
- package/packages/record/bin/turn-record.mjs +47 -0
- package/packages/record/docs/turn-record-sot.md +350 -0
- package/packages/record/lib/canonical.mjs +151 -0
- package/packages/record/lib/capture-aw.mjs +179 -0
- package/packages/record/lib/capture-cc.mjs +322 -0
- package/packages/record/lib/formats.mjs +296 -0
- package/packages/record/lib/ignore.mjs +155 -0
- package/packages/record/lib/index-db.mjs +524 -0
- package/packages/record/lib/project-aweb.mjs +251 -0
- package/packages/record/lib/segments.mjs +186 -0
- package/packages/record/lib/store.mjs +447 -0
- package/packages/record/lib/tags.mjs +114 -0
- package/packages/record/package.json +45 -0
- package/packages/record/test/vectors/README.md +34 -0
- package/packages/record/test/vectors/aweb-projection-v1.json +231 -0
- package/packages/record/test/vectors/journal-merge-v1.json +261 -0
- package/packages/record/test/vectors/turn-id-v1.json +152 -0
- package/packages/record/test/vectors/validate.mjs +391 -0
- package/skills/integration-authoring/SKILL.md +76 -0
- package/skills/oats/SKILL.md +151 -0
- package/skills/oats-config/SKILL.md +162 -0
- package/skills/oats-getting-started/SKILL.md +159 -0
- package/skills/oats-packages/SKILL.md +180 -0
- package/skills/oats-support/SKILL.md +79 -0
- package/skills/skill-craft/SKILL.md +109 -0
- 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).
|