@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,391 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Turn-record conformance validator (docs/turn-record-sot.md).
|
|
3
|
+
//
|
|
4
|
+
// Dependency-free, Node >= 18. Independent re-implementation of canonical
|
|
5
|
+
// JSON, turn ids, did:key Ed25519 verification, the aweb projection, and the
|
|
6
|
+
// merge rules; the vectors are generated by the Python awid stack, so a green
|
|
7
|
+
// run is a two-language agreement on every byte.
|
|
8
|
+
|
|
9
|
+
import { createHash, createPublicKey, verify as edVerify } from "node:crypto";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------- canonical JSON
|
|
17
|
+
|
|
18
|
+
class CoreNumberError extends Error {}
|
|
19
|
+
|
|
20
|
+
function canon(v) {
|
|
21
|
+
if (v === null) return "null";
|
|
22
|
+
const t = typeof v;
|
|
23
|
+
if (t === "string") return JSON.stringify(v);
|
|
24
|
+
if (t === "boolean") return v ? "true" : "false";
|
|
25
|
+
if (t === "number") {
|
|
26
|
+
if (!Number.isInteger(v) || Math.abs(v) > Number.MAX_SAFE_INTEGER) {
|
|
27
|
+
throw new CoreNumberError(`non-integer number in canonical core: ${v}`);
|
|
28
|
+
}
|
|
29
|
+
return String(v);
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(v)) return "[" + v.map(canon).join(",") + "]";
|
|
32
|
+
if (t === "object") {
|
|
33
|
+
const keys = Object.keys(v).sort();
|
|
34
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canon(v[k])).join(",") + "}";
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`unsupported value in canonical core: ${t}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function coreString(turn) {
|
|
40
|
+
const core = {};
|
|
41
|
+
for (const k of Object.keys(turn)) if (k !== "id" && k !== "sig") core[k] = turn[k];
|
|
42
|
+
return canon(core);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function turnId(turn) {
|
|
46
|
+
return "t1:" + createHash("sha256").update(coreString(turn), "utf8").digest("hex");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ------------------------------------------------------- did:key + ed25519
|
|
50
|
+
|
|
51
|
+
const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
52
|
+
|
|
53
|
+
function b58decode(s) {
|
|
54
|
+
let n = 0n;
|
|
55
|
+
for (const c of s) {
|
|
56
|
+
const i = B58.indexOf(c);
|
|
57
|
+
if (i < 0) throw new Error(`invalid base58 char ${c}`);
|
|
58
|
+
n = n * 58n + BigInt(i);
|
|
59
|
+
}
|
|
60
|
+
const bytes = [];
|
|
61
|
+
while (n > 0n) {
|
|
62
|
+
bytes.push(Number(n & 0xffn));
|
|
63
|
+
n >>= 8n;
|
|
64
|
+
}
|
|
65
|
+
bytes.reverse();
|
|
66
|
+
for (const c of s) {
|
|
67
|
+
if (c === "1") bytes.unshift(0);
|
|
68
|
+
else break;
|
|
69
|
+
}
|
|
70
|
+
return Uint8Array.from(bytes);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function publicKeyFromDid(did) {
|
|
74
|
+
if (!did.startsWith("did:key:z")) throw new Error(`unsupported did ${did}`);
|
|
75
|
+
const decoded = b58decode(did.slice("did:key:z".length));
|
|
76
|
+
if (decoded[0] !== 0xed || decoded[1] !== 0x01 || decoded.length !== 34) {
|
|
77
|
+
throw new Error(`not an ed25519 did:key: ${did}`);
|
|
78
|
+
}
|
|
79
|
+
const SPKI = Buffer.from("302a300506032b6570032100", "hex");
|
|
80
|
+
return createPublicKey({
|
|
81
|
+
key: Buffer.concat([SPKI, Buffer.from(decoded.subarray(2))]),
|
|
82
|
+
format: "der",
|
|
83
|
+
type: "spki",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function b64decode(s) {
|
|
88
|
+
const normalized = s.replaceAll("-", "+").replaceAll("_", "/");
|
|
89
|
+
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
90
|
+
return Buffer.from(padded, "base64");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function verifySig(did, payloadUtf8, sigB64) {
|
|
94
|
+
return edVerify(null, Buffer.from(payloadUtf8, "utf8"), publicKeyFromDid(did), b64decode(sigB64));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ------------------------------------------------------------- aweb project
|
|
98
|
+
|
|
99
|
+
function projectSigned(row, kind) {
|
|
100
|
+
const p = JSON.parse(row.signed_payload);
|
|
101
|
+
return {
|
|
102
|
+
v: 1,
|
|
103
|
+
ts: p.timestamp,
|
|
104
|
+
from: p.from,
|
|
105
|
+
to: p.to,
|
|
106
|
+
thread: "aweb:conv:" + row.conversation_id,
|
|
107
|
+
kind,
|
|
108
|
+
body: { subject: p.subject, text: p.body },
|
|
109
|
+
signature: row.signature,
|
|
110
|
+
signed_payload: row.signed_payload,
|
|
111
|
+
provenance: {
|
|
112
|
+
source: kind === "mail" ? "aweb-mail" : "aweb-chat",
|
|
113
|
+
fidelity: "projected",
|
|
114
|
+
origin: { message_id: row.message_id, conversation_id: row.conversation_id },
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function projectLegacy(row) {
|
|
120
|
+
const origin = {
|
|
121
|
+
message_id: row.message_id,
|
|
122
|
+
conversation_id: row.conversation_id,
|
|
123
|
+
};
|
|
124
|
+
for (const k of ["from_did", "from_stable_id", "to_did", "to_stable_id", "priority"]) {
|
|
125
|
+
if (row[k] !== undefined) origin[k] = row[k];
|
|
126
|
+
}
|
|
127
|
+
origin.timestamp_source = "created_at";
|
|
128
|
+
return {
|
|
129
|
+
v: 1,
|
|
130
|
+
ts: row.created_at,
|
|
131
|
+
from: row.from,
|
|
132
|
+
to: row.to,
|
|
133
|
+
thread: "aweb:conv:" + row.conversation_id,
|
|
134
|
+
kind: "mail",
|
|
135
|
+
body: { subject: row.subject, text: row.body },
|
|
136
|
+
provenance: { source: "aweb-mail", fidelity: "projected", origin },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function checkSignedConsistency(turn) {
|
|
141
|
+
const p = JSON.parse(turn.signed_payload);
|
|
142
|
+
const pairs = [
|
|
143
|
+
[turn.ts, p.timestamp, "ts/timestamp"],
|
|
144
|
+
[turn.from, p.from, "from"],
|
|
145
|
+
[turn.to, p.to, "to"],
|
|
146
|
+
[turn.body.subject, p.subject, "body.subject/subject"],
|
|
147
|
+
[turn.body.text, p.body, "body.text/body"],
|
|
148
|
+
];
|
|
149
|
+
for (const [a, b, what] of pairs) {
|
|
150
|
+
if (a !== b) throw new Error(`signed consistency violation on ${what}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function unproject(turn) {
|
|
155
|
+
const out = {};
|
|
156
|
+
if (turn.signed_payload !== undefined) {
|
|
157
|
+
const p = JSON.parse(turn.signed_payload);
|
|
158
|
+
Object.assign(out, p);
|
|
159
|
+
out.message_id = turn.provenance.origin.message_id;
|
|
160
|
+
out.conversation_id = turn.provenance.origin.conversation_id;
|
|
161
|
+
out.signature = turn.signature;
|
|
162
|
+
out.signed_payload = turn.signed_payload;
|
|
163
|
+
if (p.message_id !== undefined && p.message_id !== out.message_id) {
|
|
164
|
+
throw new Error("signed_payload message_id disagrees with origin");
|
|
165
|
+
}
|
|
166
|
+
if (p.conversation_id !== undefined && p.conversation_id !== out.conversation_id) {
|
|
167
|
+
throw new Error("signed_payload conversation_id disagrees with origin");
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
const origin = turn.provenance.origin;
|
|
172
|
+
out.message_id = origin.message_id;
|
|
173
|
+
out.conversation_id = origin.conversation_id;
|
|
174
|
+
for (const k of ["from_did", "from_stable_id", "to_did", "to_stable_id", "priority"]) {
|
|
175
|
+
if (origin[k] !== undefined) out[k] = origin[k];
|
|
176
|
+
}
|
|
177
|
+
out.from = turn.from;
|
|
178
|
+
out.to = turn.to;
|
|
179
|
+
out.subject = turn.body.subject;
|
|
180
|
+
out.body = turn.body.text;
|
|
181
|
+
if (origin.timestamp_source === "created_at") out.created_at = turn.ts;
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// -------------------------------------------------------------- merge rules
|
|
186
|
+
|
|
187
|
+
function isPrefix(shorter, longer) {
|
|
188
|
+
if (shorter.length > longer.length) return false;
|
|
189
|
+
for (let i = 0; i < shorter.length; i++) {
|
|
190
|
+
if (shorter[i].id !== longer[i].id) return false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function mergeStreams(replicas) {
|
|
196
|
+
const merged = {};
|
|
197
|
+
for (const replica of Object.values(replicas)) {
|
|
198
|
+
for (const [stream, copy] of Object.entries(replica.streams)) {
|
|
199
|
+
const prev = merged[stream];
|
|
200
|
+
if (!prev) {
|
|
201
|
+
merged[stream] = copy;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const [a, b] = prev.length <= copy.length ? [prev, copy] : [copy, prev];
|
|
205
|
+
if (!isPrefix(a, b)) throw new Error(`non-prefix copies of stream ${stream}`);
|
|
206
|
+
merged[stream] = b;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return merged;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function effectiveRecord(merged, recordOwner) {
|
|
213
|
+
const byId = new Map();
|
|
214
|
+
for (const copy of Object.values(merged)) {
|
|
215
|
+
for (const turn of copy) if (!byId.has(turn.id)) byId.set(turn.id, turn);
|
|
216
|
+
}
|
|
217
|
+
const hidden = new Set();
|
|
218
|
+
for (const turn of byId.values()) {
|
|
219
|
+
if (turn.kind !== "tombstone") continue;
|
|
220
|
+
for (const link of turn.links ?? []) {
|
|
221
|
+
if (link.rel !== "tombstones") continue;
|
|
222
|
+
const target = byId.get(link.ref);
|
|
223
|
+
if (!target) continue;
|
|
224
|
+
if (turn.from === target.from || turn.from === recordOwner) hidden.add(target.id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { byId, hidden };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ------------------------------------------------------------------ harness
|
|
231
|
+
|
|
232
|
+
let failures = 0;
|
|
233
|
+
let checks = 0;
|
|
234
|
+
|
|
235
|
+
function assert(cond, label) {
|
|
236
|
+
checks++;
|
|
237
|
+
if (!cond) {
|
|
238
|
+
failures++;
|
|
239
|
+
console.error(`FAIL ${label}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function assertThrows(fn, label) {
|
|
244
|
+
checks++;
|
|
245
|
+
try {
|
|
246
|
+
fn();
|
|
247
|
+
failures++;
|
|
248
|
+
console.error(`FAIL ${label} (expected rejection, got acceptance)`);
|
|
249
|
+
} catch {
|
|
250
|
+
/* expected */
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function load(name) {
|
|
255
|
+
return JSON.parse(readFileSync(join(HERE, name), "utf8"));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// turn-id-v1
|
|
259
|
+
{
|
|
260
|
+
const doc = load("turn-id-v1.json");
|
|
261
|
+
for (const c of doc.cases) {
|
|
262
|
+
assert(coreString(c.turn) === c.expected_canonical_core, `${c.name}: canonical core bytes`);
|
|
263
|
+
assert(turnId(c.turn) === c.expected_id, `${c.name}: id`);
|
|
264
|
+
assert(c.turn.id === c.expected_id, `${c.name}: stored id`);
|
|
265
|
+
if (c.turn.sig) {
|
|
266
|
+
assert(
|
|
267
|
+
verifySig(c.turn.sig.by, coreString(c.turn), c.turn.sig.sig),
|
|
268
|
+
`${c.name}: envelope sig`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const n of doc.negatives) {
|
|
273
|
+
if (n.reject === "id") {
|
|
274
|
+
assert(turnId(n.turn) !== n.turn.id, `${n.name}: id mismatch detected`);
|
|
275
|
+
} else if (n.reject === "sig") {
|
|
276
|
+
checks++;
|
|
277
|
+
let ok = false;
|
|
278
|
+
try {
|
|
279
|
+
ok = verifySig(n.turn.sig.by, coreString(n.turn), n.turn.sig.sig);
|
|
280
|
+
} catch {
|
|
281
|
+
ok = false;
|
|
282
|
+
}
|
|
283
|
+
if (ok) {
|
|
284
|
+
failures++;
|
|
285
|
+
console.error(`FAIL ${n.name}: invalid signature accepted`);
|
|
286
|
+
}
|
|
287
|
+
} else if (n.reject === "number") {
|
|
288
|
+
assertThrows(() => coreString(n.turn), `${n.name}: float rejected from core`);
|
|
289
|
+
} else {
|
|
290
|
+
assert(false, `${n.name}: unknown negative kind ${n.reject}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// aweb-projection-v1
|
|
296
|
+
{
|
|
297
|
+
const doc = load("aweb-projection-v1.json");
|
|
298
|
+
for (const c of doc.cases) {
|
|
299
|
+
const expected = c.expected_turn;
|
|
300
|
+
const projected = expected.signed_payload
|
|
301
|
+
? projectSigned(c.row, expected.kind)
|
|
302
|
+
: projectLegacy(c.row);
|
|
303
|
+
assert(
|
|
304
|
+
coreString(projected) === coreString(expected),
|
|
305
|
+
`${c.name}: independent projection matches expected core bytes`,
|
|
306
|
+
);
|
|
307
|
+
assert(turnId(projected) === expected.id, `${c.name}: projected id`);
|
|
308
|
+
|
|
309
|
+
if (expected.signed_payload) {
|
|
310
|
+
assert(
|
|
311
|
+
expected.signed_payload === c.row.signed_payload,
|
|
312
|
+
`${c.name}: signed_payload byte-verbatim`,
|
|
313
|
+
);
|
|
314
|
+
const p = JSON.parse(expected.signed_payload);
|
|
315
|
+
assert(
|
|
316
|
+
verifySig(p.from_did, expected.signed_payload, expected.signature),
|
|
317
|
+
`${c.name}: source signature verifies against from_did`,
|
|
318
|
+
);
|
|
319
|
+
checks++;
|
|
320
|
+
try {
|
|
321
|
+
checkSignedConsistency(expected);
|
|
322
|
+
} catch (e) {
|
|
323
|
+
failures++;
|
|
324
|
+
console.error(`FAIL ${c.name}: ${e.message}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const row = unproject(expected);
|
|
329
|
+
for (const f of c.round_trip_fields) {
|
|
330
|
+
const want = c.row[f] !== undefined ? c.row[f] : JSON.parse(c.row.signed_payload ?? "{}")[f];
|
|
331
|
+
assert(
|
|
332
|
+
JSON.stringify(row[f]) === JSON.stringify(want),
|
|
333
|
+
`${c.name}: round-trip field ${f}`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const n of doc.negatives) {
|
|
338
|
+
if (n.reject === "consistency") {
|
|
339
|
+
assert(turnId(n.turn) === n.turn.id, `${n.name}: id itself is valid`);
|
|
340
|
+
assertThrows(() => checkSignedConsistency(n.turn), `${n.name}: consistency rejected`);
|
|
341
|
+
} else {
|
|
342
|
+
assert(false, `${n.name}: unknown negative kind ${n.reject}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// journal-merge-v1
|
|
348
|
+
{
|
|
349
|
+
const doc = load("journal-merge-v1.json");
|
|
350
|
+
const merged = mergeStreams(doc.replicas);
|
|
351
|
+
for (const [stream, len] of Object.entries(doc.expected.merged_stream_lengths)) {
|
|
352
|
+
assert(merged[stream]?.length === len, `merge: stream ${stream} length ${len}`);
|
|
353
|
+
}
|
|
354
|
+
const { byId, hidden } = effectiveRecord(merged, doc.record_owner);
|
|
355
|
+
assert(
|
|
356
|
+
JSON.stringify([...byId.keys()].sort()) === JSON.stringify(doc.expected.union_ids),
|
|
357
|
+
"merge: union ids",
|
|
358
|
+
);
|
|
359
|
+
assert(
|
|
360
|
+
JSON.stringify([...hidden].sort()) === JSON.stringify([...doc.expected.hidden_ids].sort()),
|
|
361
|
+
"merge: hidden ids (tombstone dominance + authority)",
|
|
362
|
+
);
|
|
363
|
+
const visible = [...byId.keys()].filter((id) => !hidden.has(id)).sort();
|
|
364
|
+
assert(
|
|
365
|
+
JSON.stringify(visible) === JSON.stringify(doc.expected.visible_ids),
|
|
366
|
+
"merge: visible ids",
|
|
367
|
+
);
|
|
368
|
+
for (const t of byId.values()) {
|
|
369
|
+
assert(turnId(t) === t.id, `merge: turn ${t.id.slice(0, 12)}… id recomputes`);
|
|
370
|
+
}
|
|
371
|
+
for (const n of doc.negatives) {
|
|
372
|
+
if (n.reject === "prefix") {
|
|
373
|
+
assertThrows(
|
|
374
|
+
() =>
|
|
375
|
+
mergeStreams({
|
|
376
|
+
a: { streams: { [n.stream]: n.copy_a } },
|
|
377
|
+
b: { streams: { [n.stream]: n.copy_b } },
|
|
378
|
+
}),
|
|
379
|
+
`${n.name}: non-prefix copies quarantined`,
|
|
380
|
+
);
|
|
381
|
+
} else {
|
|
382
|
+
assert(false, `${n.name}: unknown negative kind ${n.reject}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (failures) {
|
|
388
|
+
console.error(`\n${failures}/${checks} checks failed`);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
console.log(`ok — ${checks} checks passed`);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: integration-authoring
|
|
3
|
+
description: >-
|
|
4
|
+
Route custom OATS capability-package and integration work to the framework's
|
|
5
|
+
integrations expert. Use when building, adapting, or debugging a reusable
|
|
6
|
+
capability, new task/messaging/knowledge integration, oats.json manifest,
|
|
7
|
+
lifecycle hook, or operational command—not merely activating an existing
|
|
8
|
+
package. Triggers: "custom integration", "capability package", "integrate
|
|
9
|
+
our tracker", "new messaging integration", "write an oats.json".
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Capability and integration authoring — delegate
|
|
13
|
+
|
|
14
|
+
A capability package may ship skills, instance instructions, requirements,
|
|
15
|
+
namespaced commands, and approved hooks. An integration is the constrained
|
|
16
|
+
subtype implementing exactly one fundamental layer. Building either requires
|
|
17
|
+
manifest, security, targeting-boundary, collision, and probe discipline; use
|
|
18
|
+
the framework's **integrations-expert** soul rather than improvising.
|
|
19
|
+
|
|
20
|
+
If the user only wants an existing package, use:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
oats install <source> # external acquisition + exact lock; inactive
|
|
24
|
+
oats trust <id> # only if commands/hooks exist
|
|
25
|
+
oats use <id> --global|--type <t>|--soul <s>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 1. Locate the OATS framework repository
|
|
29
|
+
|
|
30
|
+
Check a local pi package path, then likely locations such as
|
|
31
|
+
`~/oats`; verify with `git -C <dir> remote get-url origin`. Avoid
|
|
32
|
+
pi-managed git clones because updates reset them. If absent, ask where to
|
|
33
|
+
clone `https://github.com/awebai/oats`.
|
|
34
|
+
|
|
35
|
+
## 2. Spawn the expert against the user's repository
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node -e "
|
|
39
|
+
import('<framework-repo>/lib/core.mjs').then(m => {
|
|
40
|
+
const root = '<framework-repo>/agents';
|
|
41
|
+
const a = m.findAgent(root, 'integrations-expert');
|
|
42
|
+
const r = m.spawnInstance(root, a, {
|
|
43
|
+
purpose: '<package-slug>',
|
|
44
|
+
repo: '<users-workspace-or-repo>',
|
|
45
|
+
work: 'checkout',
|
|
46
|
+
task: '<capability intent; layer if any; skills/instructions/commands/hooks; external tools; desired global/group/soul targets; distribution path>',
|
|
47
|
+
});
|
|
48
|
+
console.log('window:', r.tmux.window, '| attach:', r.attach);
|
|
49
|
+
})"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The work tree is the user's repository, where a local package belongs under
|
|
53
|
+
`.agents/capabilities/<name>/`. A framework contribution belongs under
|
|
54
|
+
`capabilities/<name>/` in the framework worktree; an independently published
|
|
55
|
+
package uses its own repository.
|
|
56
|
+
|
|
57
|
+
## 3. Brief the design boundary
|
|
58
|
+
|
|
59
|
+
Tell the expert:
|
|
60
|
+
|
|
61
|
+
- whether it is additive or implements exactly one of knowledge/messaging/tasks;
|
|
62
|
+
- external requirements and executable surfaces;
|
|
63
|
+
- intended distribution and version/compatibility;
|
|
64
|
+
- desired config-owned targets and settings; and
|
|
65
|
+
- expected skill/instruction/scaffold collisions.
|
|
66
|
+
|
|
67
|
+
Targets never belong in the manifest. The expert must test exact pi/Claude
|
|
68
|
+
instance materialization, generated instructions, lock/trust behavior,
|
|
69
|
+
command gating, deterministic hooks, and scaffold ownership as applicable.
|
|
70
|
+
|
|
71
|
+
## 4. Hand off
|
|
72
|
+
|
|
73
|
+
Report the tmux window (`tmux attach -t pi-agents`). The expert follows its
|
|
74
|
+
package/integration craft, runs a scaffold-only probe, and leaves acquisition
|
|
75
|
+
and activation commands for the user. Its durable lessons harvest back into
|
|
76
|
+
its soul.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oats
|
|
3
|
+
description: >-
|
|
4
|
+
How to operate inside OATS (Open Agent Team Specification): instance layout and
|
|
5
|
+
lifecycle, status, spawn, retire, doctor, operational capability commands,
|
|
6
|
+
canonical-vs-generated instructions, or explaining OATS. For configuring
|
|
7
|
+
deployments (capabilities, layers, agent types, injections) load the
|
|
8
|
+
oats-config skill. Triggers: "spawn an agent", "what agents are running",
|
|
9
|
+
"retire this instance", "oats doctor", "oats status", "how does OATS work".
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Operating in OATS
|
|
13
|
+
|
|
14
|
+
A **soul** is a durable specialized agent. An **instance** is one disposable,
|
|
15
|
+
resumable incarnation. A **capability package** distributes reusable skills,
|
|
16
|
+
instructions, commands, and approved lifecycle hooks. An **integration** is a
|
|
17
|
+
capability selected for one exclusive knowledge, messaging, or tasks layer.
|
|
18
|
+
|
|
19
|
+
## Instance home
|
|
20
|
+
|
|
21
|
+
| Path | Meaning |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `TASK.md` | briefing and task |
|
|
24
|
+
| `soul/` | linked canonical soul |
|
|
25
|
+
| `AGENTS.md` | generated canonical soul + active capability instructions |
|
|
26
|
+
| `CLAUDE.md -> AGENTS.md` | compatibility view |
|
|
27
|
+
| `.agents/skills/` | exact runtime skill set |
|
|
28
|
+
| `work/` | all repository work happens here |
|
|
29
|
+
| `instance.json` | repo, branch, capabilities, skills, instruction sources, trust, hooks |
|
|
30
|
+
|
|
31
|
+
Memory files exist only when the selected knowledge integration creates them.
|
|
32
|
+
Follow their injected protocol.
|
|
33
|
+
|
|
34
|
+
## Lifecycle and roster
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
oats status [--json]
|
|
38
|
+
oats status --team [--json] # whole-team roster when config declares team: (all repos in the team scope)
|
|
39
|
+
# with the aweb messaging integration active, `oats aweb roster` adds the
|
|
40
|
+
# cross-machine view: aweb team members, where OATS aliases are instance names
|
|
41
|
+
oats create <name> [--description ...] [--type <agent-type>] [--repo ...] [--work worktree|checkout|attached|workspace]
|
|
42
|
+
# workspace mode = cross-repo coordinator: ./work is the whole team scope; read
|
|
43
|
+
# all member repos, edit none; if a knowledge layer is active, IT defines how
|
|
44
|
+
# soul updates are delivered (see that capability's own instructions)
|
|
45
|
+
oats spawn <agent> [--task ...] [--purpose ...] [--relation child|sibling|parent|unrelated --relative-to <instance>] [--parent <instance>] [--no-launch] [--json]
|
|
46
|
+
# lineage is explicit: agents spawning sub-agents declare their RELATION to the
|
|
47
|
+
# new instance with --relation + --relative-to (--parent X is sugar for
|
|
48
|
+
# --relative-to X --relation child). Without a relation the spawn is
|
|
49
|
+
# operator-origin and appears top-level. Attached-mode spawns are ALWAYS
|
|
50
|
+
# children of the work-tree owner (relation flags are rejected there).
|
|
51
|
+
# when config declares team:, spawn/retire also resolve souls and instances
|
|
52
|
+
# defined in sibling repos of the team scope (unique match wins; the instance
|
|
53
|
+
# homes with its owning repo, works in that repo, resolves that repo's config)
|
|
54
|
+
oats retire <instance> [--delete-branch]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Spawn relations — choosing how the new instance relates to you
|
|
58
|
+
|
|
59
|
+
When you spawn, declare what the new instance IS to you — the workspace is
|
|
60
|
+
viewed as clusters of related agents, and the relation is how clusters form:
|
|
61
|
+
|
|
62
|
+
- **child** (`--relation child --relative-to <you>`, or `--parent <you>`) —
|
|
63
|
+
the new instance works FOR you and nests under you. Example: a coordinator
|
|
64
|
+
spawning the developers of its feature.
|
|
65
|
+
- **parent** (`--relation parent --relative-to <you>`) — the new instance
|
|
66
|
+
oversees YOU: your recorded lineage is re-pointed so it becomes your parent.
|
|
67
|
+
Example: spawning a maintainer of your own PR — the maintainer sits
|
|
68
|
+
above you. When it later retires, lineage is spliced automatically: you
|
|
69
|
+
return to your previous parent (or top-level).
|
|
70
|
+
- **sibling** (`--relation sibling --relative-to <you>`) — a peer at your
|
|
71
|
+
level, in your cluster. Example: enlisting a peer coordinator in another
|
|
72
|
+
repo, or an architecture coordinator helping you.
|
|
73
|
+
- **unrelated** (default, no flags) — no link. Example: work with no
|
|
74
|
+
connection to yours.
|
|
75
|
+
|
|
76
|
+
Exception: **attached** work mode implies child-of-owner — an attached agent
|
|
77
|
+
shares its owner's work tree and is always that owner's child; relation flags
|
|
78
|
+
are rejected there.
|
|
79
|
+
|
|
80
|
+
This is judgment, not mandate: every workspace differs, and a soul's own
|
|
81
|
+
explicit relation instructions (in its AGENTS.md or task briefing) take
|
|
82
|
+
precedence over these defaults. When unsure which relation fits, check with
|
|
83
|
+
the human.
|
|
84
|
+
|
|
85
|
+
Do not spawn on your own judgment. Spawn when the human asks or a documented
|
|
86
|
+
workflow requires it.
|
|
87
|
+
|
|
88
|
+
### Instance naming
|
|
89
|
+
|
|
90
|
+
Name an instance for both **who it is** and **what this incarnation does** by
|
|
91
|
+
spawning with `oats spawn <soul> --purpose <descriptive-role>`. OATS constructs
|
|
92
|
+
`<full-soul-name>-<descriptive-role>`; use a short, lowercase kebab-case role
|
|
93
|
+
suffix (for example, `desktop-ux` or `terminal-safety`), not an opaque number
|
|
94
|
+
or generic word. The current spawn command always retains the full soul name,
|
|
95
|
+
so shorten the purpose—not the soul prefix—when the result would be unwieldy.
|
|
96
|
+
Do **not** use `oats create` to name an incarnation: it creates a new persistent
|
|
97
|
+
soul. Never put secrets, user data, or volatile task details in an instance
|
|
98
|
+
name.
|
|
99
|
+
|
|
100
|
+
To self-retire, first finish memory/commit/reporting requirements, report final
|
|
101
|
+
status, then run `oats retire <own-instance> --self`. Never retire merely to
|
|
102
|
+
clean up; retirement deletes the instance home.
|
|
103
|
+
|
|
104
|
+
## Canonical versus generated
|
|
105
|
+
|
|
106
|
+
Edit `soul/AGENTS.md` for durable role instructions. Instance `AGENTS.md` is a
|
|
107
|
+
generated view; marked blocks name their source. Config changes do not mutate
|
|
108
|
+
the committed soul. Preview a fresh composition with:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
oats doctor /path/to/context --soul <name>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The instance's `.agents/skills/` holds the exact OATS-composed set (kernel +
|
|
115
|
+
soul + active capabilities); `.claude/skills` mirrors it. Harness-ambient
|
|
116
|
+
skills (user-level, packages, work tree) coexist with this set. Duplicate
|
|
117
|
+
names *within* the OATS set fail spawn unless `skill-overrides` explicitly
|
|
118
|
+
chooses a source.
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
Deployments are configured in scoped `oats-config.yaml` files (laptop /
|
|
123
|
+
workspace / repository) declaring capability packages, exclusive
|
|
124
|
+
knowledge/messaging/tasks layers, agent types, targeting, and injection
|
|
125
|
+
overrides. The CLI is the config author (`oats init`, `oats use`, `oats type`,
|
|
126
|
+
`oats inject eject`). **Load the `oats-config` skill for all configuration
|
|
127
|
+
work** — this skill covers operating, not configuring.
|
|
128
|
+
|
|
129
|
+
## Commands and doctor
|
|
130
|
+
|
|
131
|
+
Operational namespaces run only when their package is active in the current
|
|
132
|
+
context/instance:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
oats okf harvest
|
|
136
|
+
oats linear issue list ...
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Package-management commands remain global. Use doctor first when something is
|
|
140
|
+
missing:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
oats doctor [context] [--soul <name>] [--json]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It shows config chain, acquired/active packages, layer selection, target and
|
|
147
|
+
settings provenance, requirements, trust, skill sources, instruction blocks,
|
|
148
|
+
and—with `--soul`—final composed text.
|
|
149
|
+
|
|
150
|
+
Infrastructure faults should be reported to the spawner/human, not repaired by
|
|
151
|
+
an instance ad hoc.
|