@awebai/oats 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +380 -0
  3. package/bin/oats.mjs +3294 -0
  4. package/capabilities/oats-authoring/oats.json +12 -0
  5. package/capabilities/oats-aweb/bin/oats-aweb.mjs +297 -0
  6. package/capabilities/oats-aweb/injects/aweb.md +55 -0
  7. package/capabilities/oats-aweb/oats.json +47 -0
  8. package/capabilities/oats-aweb/package.json +20 -0
  9. package/capabilities/oats-jira/bin/oats-jira.mjs +40 -0
  10. package/capabilities/oats-jira/injects/jira.md +10 -0
  11. package/capabilities/oats-jira/oats.json +22 -0
  12. package/capabilities/oats-jira/package.json +25 -0
  13. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +176 -0
  14. package/capabilities/oats-linear/README.md +234 -0
  15. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +32 -0
  16. package/capabilities/oats-linear/bin/oats-linear.mjs +344 -0
  17. package/capabilities/oats-linear/injects/linear.md +8 -0
  18. package/capabilities/oats-linear/oats.json +24 -0
  19. package/capabilities/oats-linear/package.json +29 -0
  20. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +217 -0
  21. package/capabilities/oats-linear/test/oats-linear.test.mjs +168 -0
  22. package/capabilities/oats-okf/agents/memory-harvest.md +25 -0
  23. package/capabilities/oats-okf/bin/oats-okf.mjs +283 -0
  24. package/capabilities/oats-okf/injects/okf.md +62 -0
  25. package/capabilities/oats-okf/oats.json +20 -0
  26. package/capabilities/oats-okf/package.json +22 -0
  27. package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +100 -0
  28. package/capabilities/oats-okf/skills/okf/SKILL.md +107 -0
  29. package/capabilities/oats-okf/skills/okf/scripts/okf-validate.mjs +123 -0
  30. package/capabilities/oats-review/agents/reviewer/AGENTS.md +53 -0
  31. package/capabilities/oats-review/agents/reviewer/soul.yaml +6 -0
  32. package/capabilities/oats-review/injects/review.md +68 -0
  33. package/capabilities/oats-review/oats.json +10 -0
  34. package/capabilities/oats-review/skills/code-review/SKILL.md +44 -0
  35. package/capabilities/oats-review/skills/security-review/SKILL.md +59 -0
  36. package/docs/capabilities.md +505 -0
  37. package/docs/capability-manifest.schema.json +223 -0
  38. package/docs/configuration.md +482 -0
  39. package/docs/conventions.md +73 -0
  40. package/docs/design/desktop-ux-plan.md +362 -0
  41. package/docs/design/package-engine-contract.md +813 -0
  42. package/docs/design/package-runtime-api.md +414 -0
  43. package/docs/desktop-cli-api.md +89 -0
  44. package/docs/desktop-succession.md +51 -0
  45. package/docs/desktop.md +187 -0
  46. package/docs/implementation.md +282 -0
  47. package/docs/integrations.md +123 -0
  48. package/docs/knowledge-theory.md +97 -0
  49. package/docs/knowledge.md +139 -0
  50. package/docs/layers.md +110 -0
  51. package/docs/migration-from-oas.md +122 -0
  52. package/docs/oats-config.schema.json +140 -0
  53. package/docs/oats-lock.schema.json +175 -0
  54. package/docs/oats-package.schema.json +129 -0
  55. package/docs/packages.md +442 -0
  56. package/docs/release-lane.md +132 -0
  57. package/docs/release-notes/v0.18.2.md +79 -0
  58. package/docs/release-notes/v0.18.3.md +63 -0
  59. package/docs/release-notes/v0.18.4.md +65 -0
  60. package/docs/release-notes/v0.18.5.md +66 -0
  61. package/docs/release-notes/v0.18.6.md +87 -0
  62. package/docs/release-notes/v0.19.0.md +186 -0
  63. package/docs/release-notes/v0.19.1.md +24 -0
  64. package/docs/release-notes/v0.19.2.md +32 -0
  65. package/docs/release-notes/v0.19.3.md +16 -0
  66. package/docs/release-notes/v0.19.4.md +12 -0
  67. package/docs/release-notes/v0.20.0.md +89 -0
  68. package/docs/release-notes/v0.22.0.md +180 -0
  69. package/docs/souls-and-instances.md +344 -0
  70. package/injects/framework-workspace.md +7 -0
  71. package/injects/instance-boundary.md +43 -0
  72. package/injects/local-soul.md +19 -0
  73. package/injects/oats.md +11 -0
  74. package/injects/work-attached.md +16 -0
  75. package/injects/work-checkout.md +12 -0
  76. package/injects/work-workspace.md +22 -0
  77. package/injects/work-worktree.md +13 -0
  78. package/lib/core.mjs +6792 -0
  79. package/lib/packages.mjs +1375 -0
  80. package/lib/tmux-config.mjs +35 -0
  81. package/package-catalog.json +44 -0
  82. package/package.json +55 -0
  83. package/packages/record/README.md +151 -0
  84. package/packages/record/bin/capture.mjs +256 -0
  85. package/packages/record/bin/recall.mjs +102 -0
  86. package/packages/record/bin/setup.mjs +281 -0
  87. package/packages/record/bin/turn-record.mjs +47 -0
  88. package/packages/record/docs/turn-record-sot.md +350 -0
  89. package/packages/record/lib/canonical.mjs +151 -0
  90. package/packages/record/lib/capture-aw.mjs +179 -0
  91. package/packages/record/lib/capture-cc.mjs +322 -0
  92. package/packages/record/lib/formats.mjs +296 -0
  93. package/packages/record/lib/ignore.mjs +155 -0
  94. package/packages/record/lib/index-db.mjs +524 -0
  95. package/packages/record/lib/project-aweb.mjs +251 -0
  96. package/packages/record/lib/segments.mjs +186 -0
  97. package/packages/record/lib/store.mjs +447 -0
  98. package/packages/record/lib/tags.mjs +114 -0
  99. package/packages/record/package.json +45 -0
  100. package/packages/record/test/vectors/README.md +34 -0
  101. package/packages/record/test/vectors/aweb-projection-v1.json +231 -0
  102. package/packages/record/test/vectors/journal-merge-v1.json +261 -0
  103. package/packages/record/test/vectors/turn-id-v1.json +152 -0
  104. package/packages/record/test/vectors/validate.mjs +391 -0
  105. package/skills/integration-authoring/SKILL.md +76 -0
  106. package/skills/oats/SKILL.md +151 -0
  107. package/skills/oats-config/SKILL.md +162 -0
  108. package/skills/oats-getting-started/SKILL.md +159 -0
  109. package/skills/oats-packages/SKILL.md +180 -0
  110. package/skills/oats-support/SKILL.md +79 -0
  111. package/skills/skill-craft/SKILL.md +109 -0
  112. package/skills/soul-craft/SKILL.md +109 -0
@@ -0,0 +1,168 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawn } from "node:child_process";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { createServer } from "node:http";
5
+ import { tmpdir } from "node:os";
6
+ import { join, resolve } from "node:path";
7
+ import test from "node:test";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const DIR = resolve(fileURLToPath(new URL("..", import.meta.url)));
11
+ const CLI = join(DIR, "bin", "oats-linear.mjs");
12
+ const HOOK = join(DIR, "bin", "oats-linear-hook.mjs");
13
+
14
+ function run(script, args = [], env = {}) {
15
+ return new Promise((done) => {
16
+ const child = spawn(process.execPath, [script, ...args], {
17
+ env: { ...process.env, ...env },
18
+ stdio: ["ignore", "pipe", "pipe"],
19
+ });
20
+ let stdout = "";
21
+ let stderr = "";
22
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
23
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
24
+ child.on("close", (code) => done({ code, stdout, stderr }));
25
+ });
26
+ }
27
+
28
+ async function mockApi(responder, fn) {
29
+ const requests = [];
30
+ const server = createServer(async (request, response) => {
31
+ let body = "";
32
+ for await (const chunk of request) body += chunk;
33
+ const parsed = JSON.parse(body);
34
+ requests.push({ headers: request.headers, ...parsed });
35
+ const payload = await responder(parsed, requests);
36
+ response.writeHead(payload.status || 200, { "content-type": "application/json" });
37
+ response.end(JSON.stringify(payload.body || payload));
38
+ });
39
+ await new Promise((ready) => server.listen(0, "127.0.0.1", ready));
40
+ const url = `http://127.0.0.1:${server.address().port}/graphql`;
41
+ try { await fn({ url, requests }); }
42
+ finally { await new Promise((closed) => server.close(closed)); }
43
+ }
44
+
45
+ const team = { id: "team-1", key: "ENG", name: "Engineering" };
46
+ const issue = {
47
+ id: "issue-1", identifier: "ENG-1", title: "Test issue", description: "body",
48
+ url: "https://linear.app/issue/ENG-1", priority: 0, priorityLabel: "No priority",
49
+ team, state: { id: "started-1", name: "In Progress", type: "started" },
50
+ project: null, parent: null, assignee: { id: "human-1", name: "Human", email: "human@example.com" },
51
+ labels: { nodes: [] },
52
+ };
53
+
54
+ function operation(query) { return query.match(/(?:query|mutation)\s+(\w+)/)?.[1]; }
55
+
56
+ test("missing API key fails once with setup guidance", async () => {
57
+ const result = await run(CLI, ["auth"], { LINEAR_API_KEY: "" });
58
+ assert.equal(result.code, 1);
59
+ assert.match(result.stderr, /LINEAR_API_KEY is not set/);
60
+ assert.match(result.stderr, /Security & access/);
61
+ });
62
+
63
+ test("auth uses the personal-key Authorization header", async () => {
64
+ await mockApi(() => ({ data: {
65
+ viewer: { id: "user-1", name: "Human", email: "human@example.com" },
66
+ organization: { id: "org-1", name: "Acme", urlKey: "acme" },
67
+ } }), async ({ url, requests }) => {
68
+ const result = await run(CLI, ["auth"], { LINEAR_API_KEY: "secret-key", LINEAR_API_URL: url });
69
+ assert.equal(result.code, 0, result.stderr);
70
+ assert.equal(JSON.parse(result.stdout).authenticated, true);
71
+ assert.equal(requests[0].headers.authorization, "secret-key");
72
+ assert.equal(operation(requests[0].query), "OatsLinearAuth");
73
+ });
74
+ });
75
+
76
+ test("issue list builds an open agent/project filter", async () => {
77
+ await mockApi(({ query }) => {
78
+ if (operation(query) === "OatsLinearTeam") return { data: { teams: { nodes: [team] } } };
79
+ if (operation(query) === "OatsLinearProjects") return { data: { projects: { nodes: [
80
+ { id: "project-1", name: "Agent Platform", slugId: "agent-platform", status: { id: "ps-1", name: "Started", type: "started" }, teams: { nodes: [team] } },
81
+ ] } } };
82
+ if (operation(query) === "OatsLinearIssues") return { data: { issues: { nodes: [issue], pageInfo: { hasNextPage: false, endCursor: null } } } };
83
+ throw new Error(`unexpected operation ${operation(query)}`);
84
+ }, async ({ url, requests }) => {
85
+ const result = await run(CLI, ["issue", "list", "--team", "ENG", "--agent", "worker-1", "--project", "Agent Platform"], {
86
+ LINEAR_API_KEY: "key", LINEAR_API_URL: url,
87
+ });
88
+ assert.equal(result.code, 0, result.stderr);
89
+ const listRequest = requests.find((request) => operation(request.query) === "OatsLinearIssues");
90
+ assert.deepEqual(listRequest.variables.filter, {
91
+ team: { id: { eq: "team-1" } },
92
+ state: { type: { nin: ["completed", "canceled", "duplicate"] } },
93
+ labels: { some: { name: { eqIgnoreCase: "agent-worker-1" } } },
94
+ project: { id: { eq: "project-1" } },
95
+ });
96
+ });
97
+ });
98
+
99
+ test("issue create provisions the agent label and records identity", async () => {
100
+ await mockApi(({ query, variables }) => {
101
+ const op = operation(query);
102
+ if (op === "OatsLinearTeam") return { data: { teams: { nodes: [team] } } };
103
+ if (op === "OatsLinearLabels") return { data: { issueLabels: { nodes: [] } } };
104
+ if (op === "OatsLinearCreateLabel") return { data: { issueLabelCreate: {
105
+ success: true, issueLabel: { id: "label-1", name: variables.input.name, color: "#5E6AD2", team },
106
+ } } };
107
+ if (op === "OatsLinearIssueCreate") return { data: { issueCreate: { success: true, issue } } };
108
+ throw new Error(`unexpected operation ${op}`);
109
+ }, async ({ url, requests }) => {
110
+ const result = await run(CLI, ["issue", "create", "--team", "ENG", "--title", "Bounded work", "--description", "Acceptance", "--agent", "worker-1"], {
111
+ LINEAR_API_KEY: "key", LINEAR_API_URL: url,
112
+ });
113
+ assert.equal(result.code, 0, result.stderr);
114
+ const create = requests.find((request) => operation(request.query) === "OatsLinearIssueCreate");
115
+ assert.equal(create.variables.input.teamId, "team-1");
116
+ assert.deepEqual(create.variables.input.labelIds, ["label-1"]);
117
+ assert.match(create.variables.input.description, /Acceptance\n\n---\nAgent: worker-1/);
118
+ });
119
+ });
120
+
121
+ test("terminal transitions require explicit authorization", async () => {
122
+ await mockApi(({ query }) => {
123
+ const op = operation(query);
124
+ if (op === "OatsLinearIssue") return { data: { issue } };
125
+ if (op === "OatsLinearStates") return { data: { team: { states: { nodes: [
126
+ { id: "done-1", name: "Done", type: "completed", position: 1 },
127
+ ] } } } };
128
+ throw new Error(`unexpected operation ${op}`);
129
+ }, async ({ url, requests }) => {
130
+ const result = await run(CLI, ["issue", "update", "ENG-1", "--state", "Done"], {
131
+ LINEAR_API_KEY: "key", LINEAR_API_URL: url,
132
+ });
133
+ assert.equal(result.code, 1);
134
+ assert.match(result.stderr, /refusing terminal state/);
135
+ assert.equal(requests.some((request) => operation(request.query) === "OatsLinearIssueUpdate"), false);
136
+ });
137
+ });
138
+
139
+ test("comment accepts multiline markdown from a file", async () => {
140
+ const temp = mkdtempSync(join(tmpdir(), "oats-linear-test-"));
141
+ const bodyFile = join(temp, "comment.md");
142
+ writeFileSync(bodyFile, "[worker-1] handoff: details\n\n- tests pass\n");
143
+ try {
144
+ await mockApi(({ query }) => {
145
+ assert.equal(operation(query), "OatsLinearComment");
146
+ return { data: { commentCreate: { success: true, comment: { id: "comment-1", body: "ok", createdAt: "2026-07-10", url: "https://linear.app/c/1", user: { id: "user-1", name: "Human" } } } } };
147
+ }, async ({ url, requests }) => {
148
+ const result = await run(CLI, ["issue", "comment", "ENG-1", "--body-file", bodyFile], {
149
+ LINEAR_API_KEY: "key", LINEAR_API_URL: url,
150
+ });
151
+ assert.equal(result.code, 0, result.stderr);
152
+ assert.equal(requests[0].variables.input.body, "[worker-1] handoff: details\n\n- tests pass");
153
+ });
154
+ } finally { rmSync(temp, { recursive: true, force: true }); }
155
+ });
156
+
157
+ test("spawn hook briefs settings and warns without auth", async () => {
158
+ const result = await run(HOOK, ["spawn"], {
159
+ OATS_EVENT: "spawn", OATS_INSTANCE: "worker-1",
160
+ OATS_SETTINGS: JSON.stringify({ team: "ENG", project: "Agent Platform" }),
161
+ LINEAR_API_KEY: "",
162
+ });
163
+ assert.equal(result.code, 0, result.stderr);
164
+ const payload = JSON.parse(result.stdout);
165
+ assert.deepEqual(payload.meta, { label: "agent-worker-1", team: "ENG", project: "Agent Platform" });
166
+ assert.match(payload.brief, /team ENG, default project Agent Platform/);
167
+ assert.match(payload.warning, /LINEAR_API_KEY/);
168
+ });
@@ -0,0 +1,25 @@
1
+ # memory-harvest — soul promotion from live instances
2
+
3
+ You are a memory-harvest instance. You were spawned because a live agent
4
+ instance committed work while holding pending notes.
5
+
6
+ **Your briefing (TASK.md) is the authority on your situation**: the source
7
+ notes dir, the soul to update, the work tree you were given, and how your
8
+ promotion is delivered. That last part depends on the source soul's custody —
9
+ a commit on the shared tree, a commit plus a PR from your own worktree, or a
10
+ direct edit with nothing to commit at all. Read it before you plan anything.
11
+
12
+ **You are ephemeral.** Skip all episodic-state upkeep of your own: do not
13
+ maintain STATE.md/log.md, do not write notes/, and never harvest yourself.
14
+ Any memory instructions injected below do not apply to you.
15
+
16
+ Follow the **memory-harvest** skill — **load it before touching any
17
+ note**; it is your entire protocol: judge each
18
+ note (promote / merge / drop), route knowledge vs skills, keep index and log
19
+ discipline, validate, delete processed notes, deliver the way your briefing
20
+ says, then `oats retire <your-instance> --self`.
21
+
22
+ Boundaries: only the soul dirs named in your briefing and the source notes
23
+ files, and nothing else. When your briefing attached you to another instance's
24
+ work tree, that tree belongs to an agent still working in it — one focused
25
+ commit, no other changes, never switch branches.
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * oats-okf — OATS knowledge-integration hooks for OKF (Open Knowledge Format).
4
+ *
5
+ * THE KNOWLEDGE INTEGRATION OWNS ALL MEMORY CONVENTIONS. The kernel knows
6
+ * nothing about STATE.md, log.md, notes/, knowledge bundles, or harvest —
7
+ * agents without a knowledge integration simply have none of this.
8
+ *
9
+ * Events (hook contract):
10
+ * soul-scaffold scaffold the soul's OKF knowledge bundle (idempotent)
11
+ * spawn scaffold instance memory (STATE.md, log.md, notes/) + brief
12
+ * retire no-op (promotion is continuous — see harvest)
13
+ * harvest AGENT-INITIATED (not a kernel hook): run from an instance
14
+ * home (`node <pkg>/capabilities/oats-okf/bin/oats-okf.mjs harvest`)
15
+ * after committing with pending notes — spawns the memory-harvest
16
+ * agent attached to this instance's work tree.
17
+ *
18
+ * Env: OATS_EVENT, OATS_INSTANCE, OATS_HOME, OATS_AGENT, OATS_SOUL (soul dir),
19
+ * OATS_CONTEXT, OATS_WORKSPACE, OATS_SETTINGS ({ "sections-file"? }),
20
+ * OATS_TASK (spawn), OATS_REPO/OATS_BRANCH/OATS_WORK (spawn), OATS_META (retire).
21
+ * Output: JSON { meta, brief, warning } on stdout. Failures warn, never block.
22
+ */
23
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, copyFileSync, realpathSync } from "node:fs";
24
+ import { join, isAbsolute, dirname } from "node:path";
25
+ import { fileURLToPath, pathToFileURL } from "node:url";
26
+ import { execSync } from "node:child_process";
27
+
28
+ /** The kernel install root. When this package runs from inside the kernel
29
+ * (marketplace source tree), ../../.. works; when it runs as a copied
30
+ * marketplace install (.agents/capabilities/installed/oats-okf), resolve the
31
+ * kernel through `oats root` — the same mechanism adapters use. */
32
+ const FRAMEWORK_ROOT = (() => {
33
+ const rel = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
34
+ if (existsSync(join(rel, "lib", "core.mjs"))) return rel;
35
+ try {
36
+ const root = execSync("oats root", { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15000 }).trim();
37
+ if (root && existsSync(join(root, "lib", "core.mjs"))) return root;
38
+ } catch { /* fall through */ }
39
+ return rel; // callers report the missing module with a clear path
40
+ })();
41
+
42
+ const out = (o) => { process.stdout.write(JSON.stringify(o) + "\n"); process.exit(0); };
43
+ const warn = (m) => out({ warning: `oats-okf: ${String(m).slice(0, 300)}` });
44
+
45
+ // Desktop CLI API v1: `oats okf harvest --json` emits EXACTLY ONE envelope
46
+ // object on stdout — {schemaVersion:1,ok,result|error} — and a nonzero exit
47
+ // on failure. Ordinary (non---json) output keeps the hook JSON shape above.
48
+ const JSON_MODE = process.argv.includes("--json");
49
+ const jsonOk = (result) => { process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: true, result }) + "\n"); process.exit(0); };
50
+ const jsonFail = (code, message) => { process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message).slice(0, 300) } }) + "\n"); process.exit(1); };
51
+
52
+ const event = process.env.OATS_EVENT || process.argv[2];
53
+ const instance = process.env.OATS_INSTANCE;
54
+ const home = process.env.OATS_HOME || process.cwd();
55
+ const soulDir = process.env.OATS_SOUL;
56
+ const agentName = process.env.OATS_AGENT || "agent";
57
+ // Fallible init stays inside an error boundary: malformed inherited env must
58
+ // never produce a bare stack trace — in --json mode Desktop expects one
59
+ // envelope object on stdout even for init failures.
60
+ let settings = {};
61
+ try { settings = JSON.parse(process.env.OATS_SETTINGS || "{}"); }
62
+ catch (e) {
63
+ if (JSON_MODE) jsonFail("E_HARVEST_FAILED", `malformed OATS_SETTINGS: ${e.message || e}`);
64
+ process.stderr.write(`oats-okf: malformed OATS_SETTINGS (ignoring): ${e.message || e}\n`);
65
+ }
66
+ /** Model for the memory-harvest agent — promotion judgment is cheap-but-good
67
+ * work; default gpt-5.5, overridable via okf settings { "harvest-model": ... }. */
68
+ const DEFAULT_HARVEST_MODEL = "github-copilot/gpt-5.5";
69
+
70
+ /** Append a one-line entry to an OKF log.md (newest-first, date-grouped per spec §7). */
71
+ function appendLogEntry(logPath, entry, title) {
72
+ const today = new Date().toISOString().slice(0, 10);
73
+ let text = existsSync(logPath) ? readFileSync(logPath, "utf8") : `# ${title}\n\n`;
74
+ const heading = `## ${today}`;
75
+ if (text.includes(heading)) text = text.replace(`${heading}\n`, `${heading}\n* ${entry}\n`);
76
+ else text = text.replace(/^(# [^\n]*\n\n?)/, `$1${heading}\n* ${entry}\n\n`);
77
+ writeFileSync(logPath, text);
78
+ }
79
+
80
+ /** Scaffold the soul's OKF knowledge bundle (idempotent). */
81
+ function scaffoldSoul() {
82
+ if (!soulDir) return false;
83
+ const kb = join(soulDir, "knowledge");
84
+ mkdirSync(kb, { recursive: true });
85
+ const index = join(kb, "index.md");
86
+ if (!existsSync(index)) {
87
+ let seeded = "";
88
+ const sf = settings["sections-file"];
89
+ if (sf) {
90
+ const abs = isAbsolute(sf) ? sf : join(process.env.OATS_CONTEXT || home, sf);
91
+ if (existsSync(abs)) seeded = readFileSync(abs, "utf8").trim() + "\n";
92
+ }
93
+ writeFileSync(index, `---
94
+ okf_version: "0.1"
95
+ ---
96
+
97
+ # ${agentName} knowledge base
98
+
99
+ Curated long-term knowledge for the ${agentName} agent (OKF bundle). Follow links
100
+ selectively — read what the current task needs, not everything.
101
+
102
+ # Sections
103
+
104
+ * [lessons/](lessons/) - durable lessons learned (type: Lesson).
105
+ * [decisions/](decisions/) - decisions and their rationale (type: Decision).
106
+ * [playbooks/](playbooks/) - step-by-step procedures kept as knowledge (type: Playbook).
107
+ * [references/](references/) - internal/external reference material (type: Reference).
108
+ ${seeded}
109
+ Grow role-specific sections beyond these as the agent's role demands (e.g.
110
+ architecture/, codebase/) — list them here and log the growth in log.md.
111
+ `);
112
+ }
113
+ const log = join(kb, "log.md");
114
+ if (!existsSync(log)) appendLogEntry(log, "**Initialization**: knowledge bundle scaffolded by oats-okf.", "Knowledge Log");
115
+ return true;
116
+ }
117
+
118
+ if (event === "soul-scaffold") {
119
+ try { out({ meta: { scaffolded: scaffoldSoul() } }); } catch (e) { warn(e.message || e); }
120
+ } else if (event === "spawn") {
121
+ // Ephemeral CAPABILITY agents (reviewer, memory-harvest) carry no episodic
122
+ // state of their own — no STATE.md/log.md/notes scaffolding, and no
123
+ // session-protocol brief. LOCAL souls are full souls: they get everything.
124
+ // ("tmp" is the legacy spelling of local — treat it as local, with memory.)
125
+ if ((process.env.OATS_KIND || "") === "capability") {
126
+ out({ meta: { memory: "none" }, brief: "Memory: none — you are ephemeral; no STATE.md/log.md/notes upkeep, no harvest." });
127
+ }
128
+ try {
129
+ const task = (process.env.OATS_TASK || "").trim();
130
+ writeFileSync(join(home, "STATE.md"), `---
131
+ type: Instance State
132
+ title: ${instance} working state
133
+ description: Live working state for instance ${instance} — rewritten as work progresses.
134
+ timestamp: ${new Date().toISOString()}
135
+ ---
136
+
137
+ # Task
138
+
139
+ ${task || "_No task assigned yet — await instructions._"}
140
+
141
+ # Plan
142
+
143
+ _(numbered steps once you have a plan)_
144
+
145
+ # Progress
146
+
147
+ _(what is done — commits, files touched, verified results)_
148
+
149
+ # Next
150
+
151
+ _(the single next action — keep this current; a fresh session on any model resumes from here)_
152
+
153
+ # Context
154
+
155
+ - repo: ${process.env.OATS_REPO || "?"} (branch ${process.env.OATS_BRANCH || "?"}, mode ${process.env.OATS_WORK || "?"})
156
+ - key files/paths: _(fill in as you learn them)_
157
+ `);
158
+ appendLogEntry(join(home, "log.md"),
159
+ `**Creation**: instance ${instance} spawned from soul ${agentName}${task ? ` — task: ${task.split("\n")[0].slice(0, 120)}` : ""}.`,
160
+ "Instance Log");
161
+ mkdirSync(join(home, "notes"), { recursive: true });
162
+ out({
163
+ meta: { memory: "okf" },
164
+ brief: "Memory: your STATE.md/log.md/notes/ are scaffolded — your AGENTS.md's 'Knowledge: OKF' section has the session protocol.",
165
+ });
166
+ } catch (e) { warn(`instance memory scaffold failed: ${e.message || e}`); }
167
+ } else if (event === "harvest") {
168
+ // AGENT-INITIATED HARVEST. An instance that committed with pending notes
169
+ // runs `harvest` from its home: spawn the
170
+ // memory-harvest agent ATTACHED to the same work tree — sibling home, shared
171
+ // tree — to promote notes into the soul, commit, and retire itself.
172
+ // Long-lived sessions thus feed the soul continuously, on the agent's call.
173
+ try {
174
+ // Derive context from the instance home (cwd) when hook env is absent.
175
+ const metaFile = join(home, "instance.json");
176
+ const meta = existsSync(metaFile) ? JSON.parse(readFileSync(metaFile, "utf8")) : {};
177
+ const inst = instance || meta.instance;
178
+ const agName = process.env.OATS_AGENT || meta.agent || "agent";
179
+ const sDir = soulDir || join(home, "soul");
180
+ const context = process.env.OATS_CONTEXT || meta.repo;
181
+ let root = process.env.OATS_ROOT;
182
+ if (!root) { // walk up from home to the agents/ dir
183
+ let d = home;
184
+ while (d !== dirname(d)) { if (d.endsWith("/instances")) { root = join(d, "..", ".."); break; } d = dirname(d); }
185
+ root = root ? realpathSync(join(root)) : undefined;
186
+ // instances live at <root>/<agent>/instances/<inst>, at the legacy nested
187
+ // <root>/{local,tmp}-agents/<agent>/instances/<inst>, or at the sibling
188
+ // <scope>/local-agents/<agent>/instances/<inst> — canonical root is <scope>/agents.
189
+ if (root && ["local-agents", "tmp-agents"].includes(root.split("/").pop())) {
190
+ const parent = dirname(root);
191
+ root = parent.split("/").pop() === "agents" ? parent : join(parent, "agents");
192
+ }
193
+ }
194
+ const notesDir = join(home, "notes");
195
+ const skip = (why) => (JSON_MODE ? jsonOk({ harvest: "skipped", reason: why }) : out({ meta: { harvestSpawn: "skipped", why } }));
196
+ if (String(agName).startsWith("memory-harvest")) skip("self (loop guard)");
197
+ const notes = existsSync(notesDir) ? readdirSync(notesDir).filter((f) => f.endsWith(".md")) : [];
198
+ if (notes.length === 0) skip("no pending notes");
199
+ if (!root || (!existsSync(root) && !existsSync(join(dirname(root), "local-agents")))) skip("no agents root found above this home");
200
+ if (!inst) skip("no instance identity (run from an instance home)");
201
+ const core = await import(pathToFileURL(join(FRAMEWORK_ROOT, "lib", "core.mjs")).href);
202
+ const slug = String(inst).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30);
203
+ // Debounce: one harvester per source instance at a time (canonical sibling
204
+ // local-agents/ plus legacy nested locations).
205
+ const harvesterHomes = [
206
+ join(dirname(root), "local-agents", "memory-harvest", "instances", `memory-harvest-${slug}`),
207
+ join(root, "local-agents", "memory-harvest", "instances", `memory-harvest-${slug}`),
208
+ join(root, "tmp-agents", "memory-harvest", "instances", `memory-harvest-${slug}`),
209
+ ];
210
+ if (harvesterHomes.some((h) => existsSync(h))) skip("harvester already running for this instance");
211
+ let agentDef = core.findAgent(root, "memory-harvest");
212
+ if (!agentDef) {
213
+ const upsert = core.upsertLocalAgent || core.upsertTmpAgent; // kernel ≥0.18 / older
214
+ upsert(root, { name: "memory-harvest", instructions: readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "agents", "memory-harvest.md"), "utf8") });
215
+ agentDef = core.findAgent(root, "memory-harvest");
216
+ }
217
+ // The harvester is service infrastructure: ALWAYS ephemeral, regardless of
218
+ // its on-disk kind (it homes as a local soul so it is uncommitted).
219
+ agentDef = { ...agentDef, kind: "capability" };
220
+ // Harvest model: explicit okf settings win (hook env, or resolved from config
221
+ // when agent-initiated); else the integration's default.
222
+ let harvestModel = settings["harvest-model"];
223
+ if (!harvestModel && context) {
224
+ try { harvestModel = core.resolveOatsConfig(context).layers?.knowledge?.settings?.["harvest-model"]; } catch { /* config unreadable: use default */ }
225
+ }
226
+ harvestModel = harvestModel || DEFAULT_HARVEST_MODEL;
227
+ const workDir = realpathSync(join(home, "work"));
228
+ const realSoul = realpathSync(sDir);
229
+ const harvName = `memory-harvest-${slug}`;
230
+ const gitRootOf = (start) => { let d = start; while (d !== dirname(d)) { if (existsSync(join(d, ".git"))) return d; d = dirname(d); } return undefined; };
231
+ let r;
232
+ const srcKind = process.env.OATS_KIND || meta.kind || "";
233
+ if (["local", "tmp"].includes(srcKind)) {
234
+ // LOCAL soul: uncommitted by contract (local-agents/, gitignored). The
235
+ // harvester judges notes exactly as usual, but the deliverable is DIRECT
236
+ // edits to the canonical soul — no commit, no PR: there is nothing to
237
+ // version. It must not touch the owner's work tree.
238
+ r = core.spawnInstance(root, agentDef, {
239
+ instance: harvName, parent: inst,
240
+ repo: context, work: "attached", workDir, model: harvestModel,
241
+ task: `Harvest the pending notes of live LOCAL-SOUL instance "${inst}" (agent "${agName}") into its soul — by direct edits, no commit.\n\n- Source notes: ${notesDir} (${notes.join(", ")})\n- Soul knowledge bundle to update: ${join(realSoul, "knowledge")}\n- Soul skills dir (for procedure-shaped notes): ${join(realSoul, "skills")}\n- This soul is LOCAL (uncommitted, gitignored): edit those soul files IN PLACE. Do NOT run git commit — not for the soul, and not in ./work (the shared tree belongs to the working instance; leave it untouched).\n- Follow your memory-harvest skill for everything else: promote/merge/drop each note, knowledge vs skill routing, index + log discipline, validate the bundle, DELETE processed notes from the source notes/ dir.\n- Then run \`oats retire ${harvName} --self\`.`,
242
+ });
243
+ } else if ((process.env.OATS_WORK || meta.work) === "workspace") {
244
+ // WORKSPACE-MODE instance: ./work is the whole workspace, not a git repo —
245
+ // the harvester may NOT commit there. The soul lives in its own home repo
246
+ // (committed to the workspace): harvest in a WORKTREE of that repo and
247
+ // deliver the promotion as a PR, never a direct push to its main branch.
248
+ const soulRepo = gitRootOf(realSoul);
249
+ if (!soulRepo) skip("workspace-mode soul is not inside a git repo — nowhere to deliver a PR");
250
+ const relSoul = realSoul.slice(soulRepo.length + 1);
251
+ r = core.spawnInstance(root, agentDef, {
252
+ instance: harvName, parent: inst,
253
+ repo: soulRepo, work: "worktree", branch: `memory-harvest/${slug}`, model: harvestModel,
254
+ task: `Harvest the pending notes of live WORKSPACE-MODE instance "${inst}" (agent "${agName}") into its soul — delivered as a PR.\n\n- Source notes: ${notesDir} (${notes.join(", ")})\n- Your ./work is a dedicated worktree of the soul's home repo (${soulRepo}), branch memory-harvest/${slug}.\n- Soul knowledge bundle to update: ./work/${join(relSoul, "knowledge")}\n- Soul skills dir (for procedure-shaped notes): ./work/${join(relSoul, "skills")}\n- Follow your memory-harvest skill: promote/merge/drop each note, knowledge vs skill routing, index + log discipline, validate the bundle, DELETE processed notes from the source notes/ dir, commit once (prefixed "memory-harvest:").\n- Then push the branch and open a PR (\`git push -u origin memory-harvest/${slug}\` then \`gh pr create --fill\`). Do NOT merge it; the humans/owners of ${soulRepo} review soul changes. If gh is unavailable, push the branch and report the compare URL.\n- Finally run \`oats retire ${harvName} --self\` (keep the branch: --self only).`,
255
+ });
256
+ } else {
257
+ // Repo-resident souls: write to the soul AS SEEN FROM THE WORK TREE, so the
258
+ // promotion commits onto the instance's own branch. Otherwise the canonical soul.
259
+ const realRepo = realpathSync(context || workDir);
260
+ const soulTarget = realSoul.startsWith(realRepo + "/")
261
+ ? join(workDir, realSoul.slice(realRepo.length + 1))
262
+ : realSoul;
263
+ r = core.spawnInstance(root, agentDef, {
264
+ instance: harvName, parent: inst,
265
+ repo: context, work: "attached", workDir, model: harvestModel,
266
+ task: `Harvest the pending notes of live instance "${inst}" (agent "${agName}") into its soul.\n\n- Source notes: ${notesDir} (${notes.join(", ")})\n- Soul knowledge bundle to update: ${join(soulTarget, "knowledge")}\n- Soul skills dir (for procedure-shaped notes): ${join(soulTarget, "skills")}\n- You are ATTACHED to the instance's work tree (./work) — commit your promotions there as a single commit, prefixed "memory-harvest:".\n- Follow your memory-harvest skill: promote/merge/drop each note, knowledge vs skill routing, index + log discipline, validate the bundle, DELETE processed notes from the source notes/ dir (so they are not re-harvested), commit, then run \`oats retire ${harvName} --self\`.`,
267
+ });
268
+ }
269
+ if (JSON_MODE) jsonOk({ harvest: "spawned", instance: r.instance, window: r.tmux?.window || null });
270
+ out({ meta: { harvestSpawn: r.instance, window: r.tmux?.window } });
271
+ } catch (e) {
272
+ if (JSON_MODE) jsonFail("E_HARVEST_FAILED", `harvest spawn failed (notes are safe on disk): ${e.message || e}`);
273
+ warn(`harvest spawn failed (notes are safe on disk): ${e.message || e}`);
274
+ }
275
+ } else if (event === "retire") {
276
+ // Retirement is intentionally a no-op for knowledge (for now): promotion happens
277
+ // continuously via agent-initiated harvest. Uncommitted notes die with the home —
278
+ // the injection tells instances to bring memory up to date, commit, and harvest
279
+ // before finishing.
280
+ out({ meta: {} });
281
+ } else {
282
+ warn(`unknown event "${event}" (expected soul-scaffold|spawn|retire)`);
283
+ }
@@ -0,0 +1,62 @@
1
+ ## Knowledge: OKF
2
+
3
+ Your knowledge layer is **OKF** (Open Knowledge Format). Long-term knowledge
4
+ lives in your soul's OKF bundle (`./soul/knowledge/`, index-first); episodic
5
+ state lives in `STATE.md`/`log.md`/`notes/`.
6
+
7
+ **Before working — every session, no exceptions:**
8
+
9
+ 1. **Load the okf skill.** It is the protocol for both reading and writing
10
+ your knowledge — do not work your bundle from memory.
11
+ 2. Read `./STATE.md` and recent `./log.md` — if STATE.md has a plan/progress
12
+ you are resuming, continue from its `# Next`.
13
+ 3. **Check your knowledge for the task at hand**: open
14
+ `./soul/knowledge/index.md` and follow the links relevant to what you are
15
+ about to do — index-first and selective (frontmatter `type`/`tags`/
16
+ `description` filters what to open; never bulk-read). Prior decisions,
17
+ lessons, and playbooks are binding context — re-deriving what the soul
18
+ already knows is a bug. Repeat this check before each new non-trivial
19
+ task, not just at session start.
20
+
21
+ Keep STATE.md current as you work (the test: could a fresh session resume
22
+ from files alone? its `# Next` names the single next action). Append dated
23
+ milestones to `./log.md` (newest first).
24
+
25
+ **Write down what you learn.** Anything you figured out that was not obvious
26
+ — a gotcha, a decision and its why, a procedure that worked — goes in
27
+ `./notes/`, one OKF concept per insight, as you go. Do not judge whether it
28
+ is "important enough"; that is someone else's job. Just capture it
29
+ faithfully.
30
+
31
+ **Before every commit, bring memory up to date**: STATE.md current, log.md
32
+ milestone appended, fresh insights in `./notes/`.
33
+
34
+ **After committing with pending notes, launch the harvester yourself**: run
35
+
36
+ ```bash
37
+ oats okf harvest
38
+ ```
39
+
40
+ from your instance home. It spawns the memory-harvest agent attached to your
41
+ work tree to promote your notes into the soul (it skips cleanly when there
42
+ are no notes or a harvester is already running — calling it "too often" is
43
+ safe; not calling it means your insights never reach the soul, and unwritten
44
+ or unharvested notes are lost when your home is retired).
45
+
46
+ **Workspace-mode instances**: your soul lives in its own home repo, and your
47
+ `./work` (the workspace) is not where it commits. `oats okf harvest` handles
48
+ this — it promotes your notes in a worktree of the soul's home repo and
49
+ delivers the update **as a PR to that repo**, never a direct push and never
50
+ a commit into member repos. Your job is unchanged: write notes, commit
51
+ nothing yourself, call the harvester.
52
+
53
+ **Local-soul instances**: your soul is uncommitted by design (it lives in
54
+ `local-agents/`, gitignored). The harvester edits your soul directly — no
55
+ commit, no PR. Your job is still unchanged: write notes, commit your WORK
56
+ normally, call the harvester.
57
+
58
+ The okf skill you loaded at session start also governs writing: notes,
59
+ concepts, index.md, and log.md follow its format craft (concepts,
60
+ frontmatter, index/log discipline, validation). Re-read the relevant section
61
+ before authoring if you have not written OKF this session — notes written
62
+ from memory tend to fail validation and stall the harvest.
@@ -0,0 +1,20 @@
1
+ {
2
+ "capability": "oats.okf",
3
+ "command": "okf",
4
+ "version": "1.4.1",
5
+ "compatibility": { "oats": ">=0.6.2" },
6
+ "layer": "knowledge",
7
+ "description": "Knowledge layer via OKF: soul bundles, instance memory (STATE.md/log.md/notes/), continuous post-commit harvest into the soul (commit, PR, or direct-edit for local souls), craft + memory skills, validator.",
8
+ "requires": [],
9
+ "skills": [
10
+ "skills"
11
+ ],
12
+ "commands": {
13
+ "harvest": "bin/oats-okf.mjs harvest"
14
+ },
15
+ "inject": "injects/okf.md",
16
+ "hooks": {
17
+ "soul-scaffold": "bin/oats-okf.mjs soul-scaffold",
18
+ "spawn": "bin/oats-okf.mjs spawn"
19
+ }
20
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@awebai/oats-okf",
3
+ "version": "0.1.0",
4
+ "description": "OATS knowledge provider: Open Knowledge Format (OKF) craft skill, bundle validator, and agents-md injection",
5
+ "keywords": [
6
+ "pi-package",
7
+ "oats",
8
+ "okf"
9
+ ],
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/awebai/oats",
13
+ "directory": "capabilities/oats-okf"
14
+ },
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "pi": {
18
+ "skills": [
19
+ "./skills"
20
+ ]
21
+ }
22
+ }