@awebai/oats 0.22.0 → 0.22.2
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 +40 -50
- package/bin/oats.mjs +242 -22
- package/capabilities/oats-authoring/LICENSE +21 -0
- package/capabilities/oats-authoring/oats-package.json +11 -0
- package/capabilities/oats-authoring/oats.json +4 -4
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +63 -0
- package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +109 -0
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +109 -0
- package/capabilities/oats-aweb/injects/aweb.md +4 -3
- package/capabilities/oats-aweb/oats.json +7 -7
- package/capabilities/oats-aweb/skills/LICENSE +21 -0
- package/capabilities/oats-aweb/skills/VENDORED.md +26 -0
- package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +201 -0
- package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +161 -0
- package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +61 -0
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +328 -0
- package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +74 -0
- package/capabilities/oats-jira/oats.json +1 -1
- package/capabilities/oats-linear/oats.json +1 -1
- package/capabilities/oats-okf/agents/{memory-harvest.md → memory-harvest/AGENTS.md} +3 -1
- package/capabilities/oats-okf/agents/memory-harvest/soul.yaml +6 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +201 -54
- package/capabilities/oats-okf/injects/okf.md +7 -0
- package/capabilities/oats-okf/oats.json +5 -2
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +42 -1
- package/capabilities/oats-review/oats.json +1 -1
- package/docs/2026-09-03-architecture-proposal.md +642 -0
- package/docs/execution-targets.md +181 -0
- package/docs/first-team-demo.md +87 -0
- package/docs/first-team.md +179 -0
- package/docs/implementation.md +14 -1
- package/docs/integrations.md +83 -65
- package/docs/layers.md +356 -80
- package/docs/migration-from-oas.md +80 -116
- package/docs/oats-config.schema.json +1 -0
- package/docs/operating-team-migration.md +217 -0
- package/docs/release-notes/v0.22.1.md +106 -0
- package/docs/release-notes/v0.22.2.md +69 -0
- package/docs/servers.md +94 -0
- package/docs/souls-and-instances.md +30 -3
- package/lib/core.mjs +626 -415
- package/lib/herdr.mjs +95 -0
- package/lib/servers.mjs +436 -0
- package/lib/session-input.mjs +78 -0
- package/lib/session-viewer.mjs +51 -0
- package/package-catalog.json +2 -2
- package/package.json +1 -1
- package/packages/record/README.md +76 -16
- package/packages/record/bin/capture.mjs +59 -3
- package/packages/record/bin/recall.mjs +67 -1
- package/packages/record/docs/turn-record-sot.md +1 -1
- package/packages/record/lib/sessions-for-home.mjs +130 -0
- package/packages/record/lib/store.mjs +207 -43
- package/skills/oats/SKILL.md +6 -2
- package/capabilities/oats-aweb/package.json +0 -20
- package/capabilities/oats-jira/package.json +0 -25
- package/capabilities/oats-linear/README.md +0 -234
- package/capabilities/oats-linear/package.json +0 -29
- package/capabilities/oats-linear/test/oats-linear.test.mjs +0 -168
- package/capabilities/oats-okf/package.json +0 -22
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
existsSync,
|
|
20
20
|
fsyncSync,
|
|
21
21
|
ftruncateSync,
|
|
22
|
+
linkSync,
|
|
22
23
|
mkdirSync,
|
|
23
24
|
openSync,
|
|
24
25
|
readdirSync,
|
|
@@ -28,8 +29,9 @@ import {
|
|
|
28
29
|
statSync,
|
|
29
30
|
unlinkSync,
|
|
30
31
|
writeFileSync,
|
|
31
|
-
writeSync,
|
|
32
32
|
} from "node:fs";
|
|
33
|
+
import { randomBytes } from "node:crypto";
|
|
34
|
+
import { hostname } from "node:os";
|
|
33
35
|
import { join } from "node:path";
|
|
34
36
|
|
|
35
37
|
import { finishTurn, sha256Hex, verifyTurnId } from "./canonical.mjs";
|
|
@@ -141,16 +143,186 @@ export function parseJournal(input) {
|
|
|
141
143
|
return { turns, validEnd, torn: validEnd < bytes.length };
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
// ----------------------------------------------------------- stream lock
|
|
147
|
+
//
|
|
148
|
+
// The per-stream lock is an exclusive-create lockfile carrying an OWNER
|
|
149
|
+
// TOKEN: the creating process's pid, its hostname, and a random nonce.
|
|
150
|
+
// Two properties follow from the token that a bare lockfile cannot give:
|
|
151
|
+
//
|
|
152
|
+
// - staleness is judged against the HOLDER'S LIVENESS, not the lock's
|
|
153
|
+
// age, so a contender that arrives late against a still-running holder
|
|
154
|
+
// waits (or times out) instead of stealing a live lock; and
|
|
155
|
+
// - release unlinks the lock only while it still carries the releaser's
|
|
156
|
+
// nonce, so a holder whose lock was reclaimed cannot delete the
|
|
157
|
+
// replacement lock out from under its new owner.
|
|
158
|
+
//
|
|
159
|
+
// Crash recovery is preserved: a lock whose pid is gone from this host is
|
|
160
|
+
// provably stale and is reclaimed at once, without waiting out any age.
|
|
161
|
+
//
|
|
162
|
+
// Age remains the fallback for the two cases where liveness is not
|
|
163
|
+
// knowable here: a lock created on another host (file sync can copy one
|
|
164
|
+
// in) and a lock with no readable token (written by an older version, or
|
|
165
|
+
// by hand).
|
|
166
|
+
|
|
167
|
+
function newLockToken() {
|
|
168
|
+
return {
|
|
169
|
+
pid: process.pid,
|
|
170
|
+
host: hostname(),
|
|
171
|
+
nonce: randomBytes(12).toString("hex"),
|
|
172
|
+
acquiredAt: new Date().toISOString(),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Create the lock, or report that someone else holds it. The payload is
|
|
177
|
+
// written to a temp file and hard-linked into place, so the lock never
|
|
178
|
+
// exists with partial or empty content: a contender that can see the file
|
|
179
|
+
// can always read the whole token.
|
|
180
|
+
function createLock(lockPath, dir, token) {
|
|
181
|
+
const tmp = join(dir, `.lock.tmp-${token.pid}-${token.nonce}`);
|
|
182
|
+
try {
|
|
183
|
+
writeFileSync(tmp, JSON.stringify(token) + "\n");
|
|
184
|
+
try {
|
|
185
|
+
linkSync(tmp, lockPath);
|
|
186
|
+
return true;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err.code === "EEXIST") return false;
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
try {
|
|
193
|
+
unlinkSync(tmp);
|
|
194
|
+
} catch {
|
|
195
|
+
/* never created, or already gone */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Read the lock as { token, ino, mtimeMs }; token is null when the file
|
|
201
|
+
// holds no readable owner token. Returns null when the lock has vanished.
|
|
202
|
+
function readLock(lockPath) {
|
|
203
|
+
let st;
|
|
204
|
+
let raw;
|
|
205
|
+
try {
|
|
206
|
+
st = statSync(lockPath);
|
|
207
|
+
raw = readFileSync(lockPath, "utf8");
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (err.code === "ENOENT") return null; // vanished: the holder released
|
|
210
|
+
// Anything else (EACCES, EISDIR, EIO...) is a condition retrying cannot
|
|
211
|
+
// clear. Swallowing it here is what let the acquire loop spin: it read
|
|
212
|
+
// as "vanished" forever. Fail with the underlying code and path so the
|
|
213
|
+
// operator can act on it.
|
|
214
|
+
throw new StoreError(
|
|
215
|
+
`cannot read stream lock ${lockPath}: ${err.code ?? "unknown error"} (${err.message})`,
|
|
216
|
+
{ cause: err },
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
let token = null;
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(raw);
|
|
222
|
+
if (
|
|
223
|
+
parsed &&
|
|
224
|
+
typeof parsed.pid === "number" &&
|
|
225
|
+
typeof parsed.host === "string" &&
|
|
226
|
+
typeof parsed.nonce === "string"
|
|
227
|
+
) {
|
|
228
|
+
token = parsed;
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
/* no token: an older or hand-written lock, judged by age below */
|
|
232
|
+
}
|
|
233
|
+
return { token, ino: st.ino, mtimeMs: st.mtimeMs };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// true / false when the holder's liveness is knowable from this process,
|
|
237
|
+
// null when it is not (the lock was created on another host). EPERM means
|
|
238
|
+
// the process exists and belongs to another user: alive.
|
|
239
|
+
function holderAlive(token) {
|
|
240
|
+
if (token.host !== hostname()) return null;
|
|
241
|
+
try {
|
|
242
|
+
process.kill(token.pid, 0);
|
|
243
|
+
return true;
|
|
244
|
+
} catch (err) {
|
|
245
|
+
return err.code === "ESRCH" ? false : true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function lockIsProvenStale(held, staleMs) {
|
|
250
|
+
if (held.token) {
|
|
251
|
+
const alive = holderAlive(held.token);
|
|
252
|
+
if (alive === true) return false; // live holder: never stealable, at any age
|
|
253
|
+
if (alive === false) return true; // dead holder: crash recovery
|
|
254
|
+
}
|
|
255
|
+
return Date.now() - held.mtimeMs > staleMs;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Remove a lock we have proven stale — and only that lock. Re-checking the
|
|
259
|
+
// file's identity immediately before the unlink DETECTS a replacement that
|
|
260
|
+
// landed while we were proving the old lock stale, so the common cascade
|
|
261
|
+
// (reclaiming from whoever reclaimed first) is caught. It is a detection,
|
|
262
|
+
// not an exclusion: a replacement landing between this recheck and the
|
|
263
|
+
// unlink is still removed, and nothing here defends against that window.
|
|
264
|
+
//
|
|
265
|
+
// Returns null when there is nothing to report — the lock was removed, was
|
|
266
|
+
// already gone, or had been replaced since we proved it stale; all three
|
|
267
|
+
// mean "retry". Returns the underlying error when the removal itself keeps
|
|
268
|
+
// failing (a read-only or hostile directory), which the caller reports on
|
|
269
|
+
// timeout rather than retrying silently forever.
|
|
270
|
+
function removeStaleLock(lockPath, held) {
|
|
271
|
+
let st;
|
|
272
|
+
try {
|
|
273
|
+
st = statSync(lockPath);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
return err.code === "ENOENT" ? null : err; // already gone
|
|
276
|
+
}
|
|
277
|
+
if (st.ino !== held.ino || st.mtimeMs !== held.mtimeMs) return null; // replaced
|
|
278
|
+
try {
|
|
279
|
+
unlinkSync(lockPath);
|
|
280
|
+
return null;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
return err.code === "ENOENT" ? null : err; // beaten to it
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Release: unlink only while the lock still carries our nonce. If it was
|
|
287
|
+
// reclaimed (we ran long enough to be judged dead, or a sync overwrote
|
|
288
|
+
// it), the file now belongs to another holder and is left alone.
|
|
289
|
+
function releaseOwnedLock(lockPath, token) {
|
|
290
|
+
// Release runs in a finally block: it must not replace the critical
|
|
291
|
+
// section's own outcome with a lock error. A lock we cannot read is also
|
|
292
|
+
// a lock whose ownership we cannot prove, so leaving it is the correct
|
|
293
|
+
// move as well as the safe one — it is reclaimable by liveness once this
|
|
294
|
+
// process exits.
|
|
295
|
+
let held;
|
|
296
|
+
try {
|
|
297
|
+
held = readLock(lockPath);
|
|
298
|
+
} catch {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (held === null) return;
|
|
302
|
+
if (!held.token || held.token.nonce !== token.nonce) return;
|
|
303
|
+
try {
|
|
304
|
+
unlinkSync(lockPath);
|
|
305
|
+
} catch {
|
|
306
|
+
/* already gone */
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function describeHolder(held) {
|
|
311
|
+
if (!held || !held.token) return "holder unknown";
|
|
312
|
+
return `held by pid ${held.token.pid} on ${held.token.host} since ${held.token.acquiredAt}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
144
315
|
export class RecordStore {
|
|
145
316
|
constructor(root, { owner, lockTimeoutMs = 10000, lockStaleMs = 30000 } = {}) {
|
|
146
317
|
if (!root) throw new StoreError("record root is required");
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
318
|
+
// lockTimeoutMs is how long a contender waits; lockStaleMs is how old a
|
|
319
|
+
// lock must be before it is reclaimed WHEN ITS HOLDER'S LIVENESS CANNOT
|
|
320
|
+
// BE CHECKED (foreign host, no readable token). There is deliberately no
|
|
321
|
+
// ordering constraint between them: safety against stealing a live
|
|
322
|
+
// holder's lock comes from the owner token, not from the two thresholds
|
|
323
|
+
// being ordered — an ordering that never held, because the waiter's
|
|
324
|
+
// timeout starts when the contender arrives while age is measured from
|
|
325
|
+
// the lock's creation.
|
|
154
326
|
this.root = root;
|
|
155
327
|
this.owner = owner ?? null;
|
|
156
328
|
this.lockTimeoutMs = lockTimeoutMs;
|
|
@@ -166,52 +338,44 @@ export class RecordStore {
|
|
|
166
338
|
// processes: hooks, watchers and manual passes can all fire close
|
|
167
339
|
// together for the same owner, and an unguarded repair can truncate a
|
|
168
340
|
// concurrent writer's already-fsynced turn. Exclusive-create lockfile
|
|
169
|
-
//
|
|
170
|
-
//
|
|
341
|
+
// carrying an owner token; see the stream-lock helpers above for how
|
|
342
|
+
// staleness is proven and why release is ownership-checked.
|
|
171
343
|
withStreamLock(streamId, fn) {
|
|
172
344
|
const dir = join(this.streamsDir(), streamId);
|
|
173
345
|
mkdirSync(dir, { recursive: true });
|
|
174
346
|
const lockPath = join(dir, ".lock");
|
|
347
|
+
const token = newLockToken();
|
|
175
348
|
const deadline = Date.now() + this.lockTimeoutMs;
|
|
349
|
+
// Every iteration that fails to acquire falls through to ONE deadline
|
|
350
|
+
// check and one sleep — there is deliberately no `continue` above them,
|
|
351
|
+
// and no branch that retries "for free". An earlier version checked the
|
|
352
|
+
// deadline only on the readable-and-live branch, so a lock that kept
|
|
353
|
+
// failing to be read, or kept failing to be removed, or kept reading as
|
|
354
|
+
// vanished (a dangling symlink does exactly that) retried forever, hot,
|
|
355
|
+
// with nothing able to stop it. Retrying at once after a reclaim would
|
|
356
|
+
// buy ~25ms on the crash-recovery path and reopen that hole; it is not
|
|
357
|
+
// worth it.
|
|
176
358
|
for (;;) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
} catch {
|
|
191
|
-
continue; // lock vanished between open and stat: retry now
|
|
192
|
-
}
|
|
193
|
-
if (stale) {
|
|
194
|
-
try {
|
|
195
|
-
unlinkSync(lockPath);
|
|
196
|
-
} catch {
|
|
197
|
-
/* another process stole it first */
|
|
198
|
-
}
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (Date.now() >= deadline) {
|
|
202
|
-
throw new StoreError(`timed out waiting for lock on stream ${streamId} (${lockPath})`);
|
|
203
|
-
}
|
|
204
|
-
sleepSync(25);
|
|
359
|
+
if (createLock(lockPath, dir, token)) break;
|
|
360
|
+
const held = readLock(lockPath); // throws on anything but ENOENT
|
|
361
|
+
const reclaimError =
|
|
362
|
+
held !== null && lockIsProvenStale(held, this.lockStaleMs)
|
|
363
|
+
? removeStaleLock(lockPath, held)
|
|
364
|
+
: null;
|
|
365
|
+
if (Date.now() >= deadline) {
|
|
366
|
+
throw new StoreError(
|
|
367
|
+
`timed out waiting for lock on stream ${streamId} (${lockPath}, ${describeHolder(held)})` +
|
|
368
|
+
(reclaimError
|
|
369
|
+
? `; it was judged stale but could not be removed: ${reclaimError.code ?? "unknown error"} (${reclaimError.message})`
|
|
370
|
+
: ""),
|
|
371
|
+
);
|
|
205
372
|
}
|
|
373
|
+
sleepSync(25);
|
|
206
374
|
}
|
|
207
375
|
try {
|
|
208
376
|
return fn();
|
|
209
377
|
} finally {
|
|
210
|
-
|
|
211
|
-
unlinkSync(lockPath);
|
|
212
|
-
} catch {
|
|
213
|
-
/* already stolen as stale; nothing to release */
|
|
214
|
-
}
|
|
378
|
+
releaseOwnedLock(lockPath, token);
|
|
215
379
|
}
|
|
216
380
|
}
|
|
217
381
|
|
package/skills/oats/SKILL.md
CHANGED
|
@@ -98,8 +98,12 @@ soul. Never put secrets, user data, or volatile task details in an instance
|
|
|
98
98
|
name.
|
|
99
99
|
|
|
100
100
|
To self-retire, first finish memory/commit/reporting requirements, report final
|
|
101
|
-
status, then run `oats retire <own-instance> --self`.
|
|
102
|
-
|
|
101
|
+
status, then run `oats retire <own-instance> --self`. That returns at once and
|
|
102
|
+
a detached completion retires you a few seconds later exactly as an external
|
|
103
|
+
`oats retire` would (quiesce, preserve work, hooks, remove the home). If the
|
|
104
|
+
completion fails, your window stays, the failure shows in `oats status` with
|
|
105
|
+
the retry command, and an operator retries. Never retire merely to clean up;
|
|
106
|
+
retirement deletes the instance home.
|
|
103
107
|
|
|
104
108
|
## Canonical versus generated
|
|
105
109
|
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@awebai/oats-aweb",
|
|
3
|
-
"version": "0.2.0",
|
|
4
|
-
"description": "OATS messaging provider: per-instance aweb identities (mint at spawn, self-delete at retire), cross-machine roster, guided setup, and agents-md injection",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"pi-package",
|
|
7
|
-
"oats",
|
|
8
|
-
"aweb"
|
|
9
|
-
],
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "https://github.com/awebai/oats",
|
|
13
|
-
"directory": "capabilities/oats-aweb"
|
|
14
|
-
},
|
|
15
|
-
"license": "MIT",
|
|
16
|
-
"type": "module",
|
|
17
|
-
"dependencies": {
|
|
18
|
-
"@awebai/pi": "^0.2.1"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@awebai/oats-jira",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"description": "OATS tasks provider: Jira via acli — epic/story/task protocol, agent roster, label-based agent identity, and agents-md injection",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"pi-package",
|
|
7
|
-
"oats",
|
|
8
|
-
"jira"
|
|
9
|
-
],
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "https://github.com/awebai/oats",
|
|
13
|
-
"directory": "capabilities/oats-jira"
|
|
14
|
-
},
|
|
15
|
-
"license": "MIT",
|
|
16
|
-
"type": "module",
|
|
17
|
-
"bin": {
|
|
18
|
-
"oats-jira": "./bin/oats-jira.mjs"
|
|
19
|
-
},
|
|
20
|
-
"pi": {
|
|
21
|
-
"skills": [
|
|
22
|
-
"./skills"
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
# OATS Linear tasks integration
|
|
2
|
-
|
|
3
|
-
Binds the OATS `tasks` layer to [Linear](https://linear.app). It ships the
|
|
4
|
-
`linear-tasks` skill, a short soul injection, a spawn briefing, and JSON-first
|
|
5
|
-
`oats linear ...` commands for issue work.
|
|
6
|
-
|
|
7
|
-
## Why GraphQL instead of a Linear CLI?
|
|
8
|
-
|
|
9
|
-
Linear's official `@linear/cli` only supports interactive issue creation and
|
|
10
|
-
branch checkout. It cannot list queues, read issues, transition workflow
|
|
11
|
-
states, label ownership, or post comments. Third-party CLIs expose different
|
|
12
|
-
and unstable command contracts. This integration therefore calls Linear's
|
|
13
|
-
official GraphQL API directly with Node's built-in `fetch`; it adds no external
|
|
14
|
-
CLI or SDK dependency.
|
|
15
|
-
|
|
16
|
-
- Endpoint: `https://api.linear.app/graphql`
|
|
17
|
-
- Authentication: personal API key in the `Authorization` header
|
|
18
|
-
- Documentation: <https://linear.app/developers/graphql>
|
|
19
|
-
|
|
20
|
-
## Setup
|
|
21
|
-
|
|
22
|
-
1. In Linear, open **Settings → Security & access → API keys** and create a
|
|
23
|
-
personal API key with access to the workspace used by your OATS agents.
|
|
24
|
-
2. Put the key in your shell or secret manager, never in `oats-config.yaml`:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
export LINEAR_API_KEY='lin_api_...'
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Start/resume agents from an environment that receives this variable. The
|
|
31
|
-
spawn hook warns when it is absent; API commands fail once with an actionable
|
|
32
|
-
authentication error rather than attempting login.
|
|
33
|
-
3. Activate the bundled integration at the intended target:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
oats use oats.linear --global --dir /path/to/workspace
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
4. Add target settings to that binding (team is the Linear issue-prefix key;
|
|
40
|
-
project is an optional default communicated in each instance briefing):
|
|
41
|
-
|
|
42
|
-
```yaml
|
|
43
|
-
capabilities:
|
|
44
|
-
layers:
|
|
45
|
-
tasks:
|
|
46
|
-
capability: oats.linear
|
|
47
|
-
from: bundled
|
|
48
|
-
settings: { team: ENG, project: Agent Platform }
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
5. Verify resolution and API access (operational commands are available only
|
|
52
|
-
in an active context):
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
oats doctor /path/to/workspace
|
|
56
|
-
oats linear auth
|
|
57
|
-
oats linear teams
|
|
58
|
-
oats linear states --team ENG
|
|
59
|
-
oats linear projects --team ENG
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
The API key acts as the human who created it. Agents preserve the human
|
|
63
|
-
assignee, identify themselves with `agent-<instance-name>` labels, and do not
|
|
64
|
-
move issues to terminal states without explicit human authorization.
|
|
65
|
-
|
|
66
|
-
## Command surface
|
|
67
|
-
|
|
68
|
-
All successful output is JSON; errors are JSON on stderr and return non-zero.
|
|
69
|
-
Run an incomplete command for usage, or load the `linear-tasks` skill for the
|
|
70
|
-
workflow and exact examples.
|
|
71
|
-
|
|
72
|
-
```text
|
|
73
|
-
oats linear auth
|
|
74
|
-
oats linear teams
|
|
75
|
-
oats linear states --team <KEY>
|
|
76
|
-
oats linear projects --team <KEY>
|
|
77
|
-
oats linear labels --team <KEY>
|
|
78
|
-
oats linear issue list|get|create|update|comment ...
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Agent labels are created team-locally on first use of `--agent`. Other labels
|
|
82
|
-
must already exist. `--description-file` and `--body-file` avoid shell quoting
|
|
83
|
-
problems for multiline Markdown.
|
|
84
|
-
|
|
85
|
-
## Projects, project documentation, and related issues
|
|
86
|
-
|
|
87
|
-
### Operating model
|
|
88
|
-
|
|
89
|
-
Use each Linear object for one kind of durable information:
|
|
90
|
-
|
|
91
|
-
| Linear object | What belongs there | Who manages it with this integration |
|
|
92
|
-
|---|---|---|
|
|
93
|
-
| Project | Outcome, ownership, lifecycle, target dates, and the container for related issues | Humans in the Linear UI; agents can discover it |
|
|
94
|
-
| Project overview | Intent, scope/non-goals, architecture, constraints, human gates, and success criteria | Humans in the Linear UI |
|
|
95
|
-
| Project documents | Detailed designs, decision records, runbooks, research, and other long-form project context | Humans in the Linear UI |
|
|
96
|
-
| Issue | One bounded deliverable with acceptance criteria | Agents through `oats linear issue ...` |
|
|
97
|
-
| Sub-issue | An independently verifiable part of a larger issue | Agents through `--parent` |
|
|
98
|
-
| Issue comment | Milestones, blockers, handoffs, verification, and PR/branch links | Agents through `issue comment` |
|
|
99
|
-
| Messaging | Conversation and nudges | The configured messaging layer, never the durable task record |
|
|
100
|
-
|
|
101
|
-
The project overview and documents explain the work; issues execute it. Keep
|
|
102
|
-
project-wide decisions out of an arbitrary issue description, and keep task
|
|
103
|
-
status out of chat. When a project document governs an issue, link that
|
|
104
|
-
document from the issue description or a durable comment.
|
|
105
|
-
|
|
106
|
-
### Discover projects
|
|
107
|
-
|
|
108
|
-
The wrapper currently reads project metadata but not project overview/document
|
|
109
|
-
content:
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
oats linear projects --team ENG
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
The JSON includes project IDs, names, slugs, status, and associated teams. Use
|
|
116
|
-
an exact project name or slug in issue commands. If the configured project is
|
|
117
|
-
missing or ambiguous, stop and ask the human rather than selecting a similar
|
|
118
|
-
name.
|
|
119
|
-
|
|
120
|
-
### List issues in a project
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
# Open issues in the project
|
|
124
|
-
oats linear issue list --team ENG --project "Agent Platform"
|
|
125
|
-
|
|
126
|
-
# Open issues claimed by one OATS instance
|
|
127
|
-
oats linear issue list --team ENG --project "Agent Platform" \
|
|
128
|
-
--agent my-agent-instance
|
|
129
|
-
|
|
130
|
-
# Include terminal issues when auditing history
|
|
131
|
-
oats linear issue list --team ENG --project "Agent Platform" --all
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
`issue list` excludes completed, canceled, and duplicate states unless `--all`
|
|
135
|
-
is supplied. Use `issue get` before acting; its JSON includes the issue's
|
|
136
|
-
project and parent context:
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
oats linear issue get ENG-123
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### Create issues in a project
|
|
143
|
-
|
|
144
|
-
Prefer a Markdown file for acceptance criteria:
|
|
145
|
-
|
|
146
|
-
```bash
|
|
147
|
-
cat > /tmp/issue.md <<'EOF'
|
|
148
|
-
Why this work is needed.
|
|
149
|
-
|
|
150
|
-
Acceptance:
|
|
151
|
-
- [ ] Observable outcome implemented
|
|
152
|
-
- [ ] Verification evidence recorded
|
|
153
|
-
- [ ] Relevant documentation updated
|
|
154
|
-
EOF
|
|
155
|
-
|
|
156
|
-
oats linear issue create --team ENG --project "Agent Platform" \
|
|
157
|
-
--title "Implement token refresh" \
|
|
158
|
-
--description-file /tmp/issue.md \
|
|
159
|
-
--agent my-agent-instance
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
Create a sub-issue only when it is independently verifiable and the parent
|
|
163
|
-
really decomposes into multiple pieces:
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
oats linear issue create --team ENG --parent ENG-123 \
|
|
167
|
-
--title "Add refresh-token tests" \
|
|
168
|
-
--description-file /tmp/issue.md \
|
|
169
|
-
--agent my-agent-instance
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Project membership and parentage are independent: `--project` associates an
|
|
173
|
-
issue with a project; `--parent` makes it a sub-issue. Supply both when the
|
|
174
|
-
sub-issue must explicitly carry project membership.
|
|
175
|
-
|
|
176
|
-
### Work and report within the project
|
|
177
|
-
|
|
178
|
-
```bash
|
|
179
|
-
oats linear issue update ENG-123 --agent my-agent-instance
|
|
180
|
-
oats linear issue update ENG-123 --state "In Progress"
|
|
181
|
-
oats linear issue comment ENG-123 \
|
|
182
|
-
--body "[my-agent-instance] milestone: implementation complete; tests pass"
|
|
183
|
-
oats linear issue comment ENG-123 \
|
|
184
|
-
--body "[my-agent-instance] handoff → reviewer: PR <url>; run npm test"
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
Use the team's exact workflow names from `oats linear states --team ENG`.
|
|
188
|
-
Agents normally stop at the review state. Terminal transitions require both
|
|
189
|
-
explicit human authorization and `--allow-terminal`.
|
|
190
|
-
|
|
191
|
-
### Manage project overviews and documents
|
|
192
|
-
|
|
193
|
-
The current command wrapper does **not** read or mutate project overview
|
|
194
|
-
Markdown or Linear documents. Manage them through the Linear UI:
|
|
195
|
-
|
|
196
|
-
1. Open the project returned by `oats linear projects --team <KEY>`.
|
|
197
|
-
2. Maintain project intent, scope, non-goals, ownership, gates, architecture,
|
|
198
|
-
and success criteria in its overview.
|
|
199
|
-
3. Keep detailed designs, decisions, and runbooks in project documents.
|
|
200
|
-
4. Link governing project documents from related issues.
|
|
201
|
-
5. Record implementation progress on issues; use Linear's project updates for
|
|
202
|
-
human-facing project-level summaries.
|
|
203
|
-
|
|
204
|
-
An agent that needs unavailable project-document context must ask the human for
|
|
205
|
-
its URL/content. It must not infer missing project policy from issue titles.
|
|
206
|
-
|
|
207
|
-
## Current support boundary
|
|
208
|
-
|
|
209
|
-
| Operation | Supported by `oats linear`? | Current path |
|
|
210
|
-
|---|---:|---|
|
|
211
|
-
| Discover teams, workflow states, projects, and labels | Yes | `teams`, `states`, `projects`, `labels` |
|
|
212
|
-
| List/get/create/update/comment on project issues | Yes | `issue ...` commands |
|
|
213
|
-
| Create sub-issues | Yes | `issue create --parent ...` |
|
|
214
|
-
| Create, rename, schedule, change status, or close a project | No | Linear UI; human-owned |
|
|
215
|
-
| Read or edit project overview Markdown | No | Linear UI |
|
|
216
|
-
| List, read, create, or edit project documents | No | Linear UI |
|
|
217
|
-
| Move an existing issue into/out of a project | No | Linear UI |
|
|
218
|
-
| Change an existing issue's parent | No | Linear UI |
|
|
219
|
-
| Publish Linear project status updates | No | Linear UI |
|
|
220
|
-
| Create issue-to-issue relations such as blocks/related | No | Linear UI |
|
|
221
|
-
|
|
222
|
-
Do not invent GraphQL calls or undocumented command flags to bypass this
|
|
223
|
-
boundary. A future, separately reviewed extension could add commands such as:
|
|
224
|
-
|
|
225
|
-
```text
|
|
226
|
-
oats linear project get|create|update
|
|
227
|
-
oats linear project issue-add|issue-remove
|
|
228
|
-
oats linear document list|get|create|update
|
|
229
|
-
oats linear project-update create
|
|
230
|
-
oats linear relation create
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
Before adding those operations, the deployment must decide which project and
|
|
234
|
-
document mutations agents may perform and which remain human-only.
|
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
}
|