@awebai/oats 0.22.19 → 0.23.1
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 +54 -20
- package/bin/oats.mjs +24 -10
- package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +18 -24
- package/capabilities/oats-okf/agents/memory-harvest/soul.yaml +2 -2
- package/capabilities/oats-okf/bin/oats-okf.mjs +105 -517
- package/capabilities/oats-okf/injects/okf.md +32 -67
- package/capabilities/oats-okf/lib/config.mjs +112 -0
- package/capabilities/oats-okf/lib/inspection.mjs +96 -0
- package/capabilities/oats-okf/lib/io.mjs +103 -0
- package/capabilities/oats-okf/lib/migration.mjs +116 -0
- package/capabilities/oats-okf/lib/sources.mjs +238 -0
- package/capabilities/oats-okf/lib/stores.mjs +331 -0
- package/capabilities/oats-okf/lib/worker.mjs +352 -0
- package/capabilities/oats-okf/oats.json +23 -7
- package/capabilities/oats-okf/schemas/okf-base.schema.json +46 -0
- package/capabilities/oats-okf/schemas/okf-bindings.schema.json +112 -0
- package/capabilities/oats-okf/schemas/okf-soul.schema.json +37 -0
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +263 -140
- package/capabilities/oats-okf/skills/okf/SKILL.md +13 -4
- package/docs/capabilities.md +14 -3
- package/docs/configuration.md +11 -1
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +558 -0
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +744 -0
- package/docs/design/2026-09-13-knowledge-implementation.md +127 -0
- package/docs/design/2026-09-13-knowledge-location-contract.md +340 -0
- package/docs/design/okf-mirror-provenance.md +105 -0
- package/docs/design/package-runtime-api.md +177 -3
- package/docs/desktop-cli-api.md +60 -11
- package/docs/execution-targets.md +16 -0
- package/docs/first-team-demo.md +6 -1
- package/docs/first-team.md +151 -115
- package/docs/integrations.md +42 -42
- package/docs/knowledge-capability-authoring.md +101 -0
- package/docs/knowledge-migration.md +138 -0
- package/docs/knowledge-reference/acceptance.md +108 -0
- package/docs/knowledge-reference/adoption.md +61 -0
- package/docs/knowledge-reference/harvester.md +107 -0
- package/docs/knowledge-reference/model.md +84 -0
- package/docs/knowledge-reference/package-craft.md +126 -0
- package/docs/knowledge-reference/provider-mapping.md +77 -0
- package/docs/knowledge-reference/reader-capture.md +87 -0
- package/docs/knowledge-theory.md +20 -6
- package/docs/knowledge.md +316 -129
- package/docs/layers.md +65 -69
- package/docs/migration-from-oas.md +7 -1
- package/docs/oats-config.schema.json +5 -2
- package/docs/packages.md +26 -2
- package/docs/release-notes/v0.23.0.md +93 -0
- package/docs/release-notes/v0.23.1.md +97 -0
- package/docs/schedules.md +42 -3
- package/docs/souls-and-instances.md +72 -49
- package/injects/work-directory.md +18 -0
- package/lib/core.mjs +279 -56
- package/lib/schedule.mjs +12 -2
- package/package-catalog.json +6 -1
- package/package.json +2 -2
- package/packages/record/README.md +19 -0
- package/packages/record/bin/capture.mjs +96 -48
- package/packages/record/bin/recall.mjs +17 -11
- package/packages/record/bin/record-native-start.mjs +11 -0
- package/packages/record/lib/capture-cc.mjs +82 -27
- package/packages/record/lib/capture-lock.mjs +15 -2
- package/packages/record/lib/formats.mjs +108 -21
- package/packages/record/lib/native-history.mjs +87 -0
- package/packages/record/lib/session-roots.mjs +90 -0
- package/packages/record/lib/session-snapshot.mjs +61 -0
- package/packages/record/lib/sessions-for-home.mjs +88 -56
- package/skills/oats/SKILL.md +3 -1
- package/capabilities/oats-okf/lib/harvest-branch.mjs +0 -43
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
// instance's own, and nothing outside it — not the parent workspace, not a
|
|
13
13
|
// sibling home — is ever swept in.
|
|
14
14
|
|
|
15
|
-
import { closeSync, openSync, readSync, realpathSync
|
|
16
|
-
import { resolve, sep } from "node:path";
|
|
17
|
-
import {
|
|
15
|
+
import { closeSync, fstatSync, openSync, readSync, realpathSync } from "node:fs";
|
|
16
|
+
import { basename, resolve, sep } from "node:path";
|
|
17
|
+
import { isUtf8 } from "node:buffer";
|
|
18
18
|
|
|
19
19
|
import { SESSION_FORMATS } from "./formats.mjs";
|
|
20
|
+
import { hashPrefix, identity, sameVersion, verifySnapshot } from "./session-snapshot.mjs";
|
|
21
|
+
import { sourceSessionEnvironment } from "./session-roots.mjs";
|
|
22
|
+
import { historicalSessionRoots } from "./native-history.mjs";
|
|
20
23
|
|
|
21
24
|
// A session's first lines can be large (Claude Code queue operations and
|
|
22
25
|
// file-history snapshots run to 100 KB and more) and the first cwd-bearing
|
|
@@ -27,64 +30,82 @@ import { SESSION_FORMATS } from "./formats.mjs";
|
|
|
27
30
|
const CHUNK_BYTES = 64 * 1024;
|
|
28
31
|
export const CWD_SCAN_BOUND_BYTES = 8 * 1024 * 1024;
|
|
29
32
|
|
|
30
|
-
function* wholeLines(
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
function* wholeLines(fd, bound) {
|
|
34
|
+
const buf = Buffer.alloc(CHUNK_BYTES);
|
|
35
|
+
let pieces = [], size = 0; // keep raw bytes across UTF-8/chunk boundaries
|
|
36
|
+
let offset = 0;
|
|
37
|
+
while (offset < bound) {
|
|
38
|
+
const n = readSync(fd, buf, 0, Math.min(CHUNK_BYTES, bound - offset), offset);
|
|
39
|
+
if (n === 0) break;
|
|
40
|
+
offset += n;
|
|
41
|
+
let from = 0;
|
|
42
|
+
for (let nl = buf.indexOf(10, from); nl >= 0 && nl < n; nl = buf.indexOf(10, from)) {
|
|
43
|
+
const tail = buf.subarray(from, nl);
|
|
44
|
+
const bytes = pieces.length ? Buffer.concat([...pieces, tail], size + tail.length) : tail;
|
|
45
|
+
// Replacement decoding could fabricate a cwd. Leave corrupt lines
|
|
46
|
+
// unattributed; capture will report incomplete bytes if a later
|
|
47
|
+
// valid line supplies the attribution.
|
|
48
|
+
yield isUtf8(bytes) ? bytes.toString("utf8") : null;
|
|
49
|
+
pieces = []; size = 0; from = nl + 1;
|
|
50
|
+
}
|
|
51
|
+
if (from < n) {
|
|
52
|
+
const piece = Buffer.from(buf.subarray(from, n)); // buf is reused
|
|
53
|
+
pieces.push(piece); size += piece.length;
|
|
47
54
|
}
|
|
48
|
-
// The remainder is a fragment unless the file ended exactly there.
|
|
49
|
-
if (carry && offset < bound) yield carry;
|
|
50
|
-
} finally {
|
|
51
|
-
closeSync(fd);
|
|
52
55
|
}
|
|
56
|
+
// A JSON-shaped EOF fragment is still an uncommitted native record.
|
|
57
|
+
// Never attribute a source using a line its writer has not terminated.
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
function cwdOfLine(source, line) {
|
|
56
|
-
if (!line.trim()) return undefined;
|
|
61
|
+
if (line === null || !line.trim()) return undefined;
|
|
57
62
|
let d;
|
|
58
63
|
try {
|
|
59
64
|
d = JSON.parse(line);
|
|
60
65
|
} catch {
|
|
61
66
|
return undefined; // a non-JSON native line
|
|
62
67
|
}
|
|
63
|
-
if (source === "cc" && typeof d
|
|
64
|
-
if (source === "pi" && d
|
|
65
|
-
if (source === "codex" && d
|
|
68
|
+
if (source === "cc" && typeof d?.cwd === "string") return d.cwd;
|
|
69
|
+
if (source === "pi" && d?.type === "session" && typeof d?.cwd === "string") return d.cwd;
|
|
70
|
+
if (source === "codex" && d?.type === "session_meta" && typeof d.payload?.cwd === "string") return d.payload.cwd;
|
|
66
71
|
return undefined;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
/**
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
/** Descriptor-derived attribution and (for accepted cwd) content witness.
|
|
75
|
+
* No cwd within `bound` means unknown format, torn input or no attribution. */
|
|
76
|
+
export function sessionAttribution(source, path, { bound = CWD_SCAN_BOUND_BYTES, acceptCwd = () => true } = {}) {
|
|
77
|
+
const fd = openSync(path, "r");
|
|
73
78
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
const snapshot = identity(fstatSync(fd));
|
|
80
|
+
let cwd;
|
|
81
|
+
for (const line of wholeLines(fd, Math.min(bound, snapshot.size))) {
|
|
82
|
+
cwd = cwdOfLine(source, line);
|
|
83
|
+
if (cwd) break;
|
|
77
84
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
// Never hash/read the body of an unrelated or unattributable session.
|
|
86
|
+
if (cwd && acceptCwd(cwd)) snapshot.hash = hashPrefix(fd, snapshot.size, path);
|
|
87
|
+
// Never combine attribution read from an old prefix with a new witness.
|
|
88
|
+
// Discovery requires a stable read; append growth between discovery and
|
|
89
|
+
// capture is supported by the witness's prefix hash.
|
|
90
|
+
const after = identity(fstatSync(fd));
|
|
91
|
+
if (!sameVersion(after, snapshot)) {
|
|
92
|
+
throw new Error(`session source changed during attribution: ${path}`);
|
|
93
|
+
}
|
|
94
|
+
verifySnapshot(fd, path, snapshot);
|
|
95
|
+
return { cwd, snapshot };
|
|
96
|
+
} finally { closeSync(fd); }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Scan complete lines for the first native cwd; never attribute EOF fragments. */
|
|
100
|
+
export function sessionCwd(source, path, options) {
|
|
101
|
+
return sessionAttribution(source, path, { ...options, acceptCwd: () => false }).cwd;
|
|
82
102
|
}
|
|
83
103
|
|
|
84
104
|
function canonical(p) {
|
|
85
105
|
try {
|
|
86
106
|
return realpathSync(p);
|
|
87
|
-
} catch {
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err.code !== "ENOENT") throw err;
|
|
88
109
|
return resolve(p); // a retired home's cwd no longer exists; compare the lexical path
|
|
89
110
|
}
|
|
90
111
|
}
|
|
@@ -94,35 +115,46 @@ function within(child, parent) {
|
|
|
94
115
|
}
|
|
95
116
|
|
|
96
117
|
/** Session files whose recorded cwd is `home` or below it, oldest first.
|
|
97
|
-
*
|
|
98
|
-
*
|
|
118
|
+
* By default uses independent managed-launch history, never observer env.
|
|
119
|
+
* `roots` is an explicit inventory ({ cc, pi, codex }; omitted formats are
|
|
120
|
+
* excluded). `fallback: "current-env"` explicitly chooses observer-time
|
|
121
|
+
* recipe/env discovery for standalone or legacy sources. All supplied or
|
|
122
|
+
* historical roots must be readable; missing roots are not optional.
|
|
99
123
|
* `onUnattributed(source, path)` is called for a file that carries no cwd
|
|
100
124
|
* within the scan bound, so a caller can report it instead of losing it.
|
|
101
|
-
*
|
|
102
|
-
|
|
125
|
+
* Read/scan failures throw; they are not unattributed or empty scans.
|
|
126
|
+
* `ignore` excludes files BEFORE reading, with optional `onIgnored`.
|
|
127
|
+
* Each entry includes a descriptor-derived `snapshot` witness; pass the
|
|
128
|
+
* entries intact as captureSessions({ files }) to retain attribution. */
|
|
129
|
+
export function sessionsForHome(home, { roots, onUnattributed, bound, ignore, onIgnored, env = process.env, fallback } = {}) {
|
|
103
130
|
const target = canonical(home);
|
|
104
131
|
const out = [];
|
|
132
|
+
// Explicit roots are a caller-owned inventory; unspecified formats are
|
|
133
|
+
// excluded, not filled from an unrelated observer. The opt-in fallback is
|
|
134
|
+
// for standalone/legacy inventories, never proof of historical completeness.
|
|
135
|
+
let context;
|
|
136
|
+
if (!roots && fallback !== "current-env") roots = historicalSessionRoots(home);
|
|
137
|
+
if (!roots) context = sourceSessionEnvironment(home, env);
|
|
105
138
|
for (const fmt of Object.values(SESSION_FORMATS)) {
|
|
106
|
-
const rs = roots
|
|
107
|
-
for (const path of fmt.listFiles(rs)) {
|
|
108
|
-
const
|
|
139
|
+
const rs = roots ? (roots[fmt.source] ?? []) : fmt.defaultRoots(context.home, context.env, { cwd: home });
|
|
140
|
+
for (const path of fmt.listFiles(rs, { strict: true })) {
|
|
141
|
+
const sessionId = fmt.sessionId(path);
|
|
142
|
+
if (ignore?.ignores(path, [basename(path), sessionId, ...(fmt.ignoreKeys?.(path) ?? [])])) {
|
|
143
|
+
onIgnored?.(fmt.source, path);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const { cwd, snapshot } = sessionAttribution(fmt.source, path, { bound, acceptCwd: cwd => within(canonical(cwd), target) });
|
|
109
147
|
if (!cwd) { if (onUnattributed) onUnattributed(fmt.source, path); continue; }
|
|
110
148
|
if (!within(canonical(cwd), target)) continue;
|
|
111
|
-
let stat;
|
|
112
|
-
try {
|
|
113
|
-
stat = statSync(path);
|
|
114
|
-
} catch {
|
|
115
|
-
continue; // vanished between listing and stat
|
|
116
|
-
}
|
|
117
|
-
const sessionId = fmt.sessionId(path);
|
|
118
149
|
out.push({
|
|
119
150
|
source: fmt.source,
|
|
120
151
|
sessionId,
|
|
121
152
|
thread: `${fmt.source}:session:${sessionId}`,
|
|
122
153
|
path,
|
|
123
154
|
cwd,
|
|
124
|
-
bytes:
|
|
125
|
-
mtime:
|
|
155
|
+
bytes: snapshot.size,
|
|
156
|
+
mtime: new Date(snapshot.mtimeMs).toISOString(),
|
|
157
|
+
snapshot,
|
|
126
158
|
});
|
|
127
159
|
}
|
|
128
160
|
}
|
package/skills/oats/SKILL.md
CHANGED
|
@@ -38,7 +38,9 @@ oats status [--json]
|
|
|
38
38
|
oats status --team [--json] # whole-team roster when config declares team: (all repos in the team scope)
|
|
39
39
|
# with the aweb messaging integration active, `oats aweb roster` adds the
|
|
40
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]
|
|
41
|
+
oats create <name> [--description ...] [--type <agent-type>] [--repo ...] [--work worktree|checkout|attached|workspace|directory]
|
|
42
|
+
# directory mode = owned execution directory; repo is config context (no Git
|
|
43
|
+
# required); --work-dir and --branch are rejected; retirement preserves work.
|
|
42
44
|
# workspace mode = cross-repo coordinator: ./work is the whole team scope; read
|
|
43
45
|
# all member repos, edit none; if a knowledge layer is active, IT defines how
|
|
44
46
|
# soul updates are delivered (see that capability's own instructions)
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// A workspace-mode harvest delivers its promotion as a PR from a branch named
|
|
2
|
-
// memory-harvest/<slug> in the soul's repository. After that PR merges, the
|
|
3
|
-
// local branch may still exist and the next harvest's spawn would refuse it.
|
|
4
|
-
// A branch fully merged into the base is stale and is deleted before the
|
|
5
|
-
// spawn; an unmerged one is the previous harvester's unfinished work and the
|
|
6
|
-
// harvest refuses with the exact remedy instead of touching it.
|
|
7
|
-
import { execFileSync } from "node:child_process";
|
|
8
|
-
|
|
9
|
-
/** Single-quote shell escaping for the operator remedy: the repo path may hold spaces or shell metacharacters. */
|
|
10
|
-
export function shellQuote(s) { return "'" + String(s).replace(/'/g, "'\\''") + "'"; }
|
|
11
|
-
|
|
12
|
-
function git(repo, args) {
|
|
13
|
-
return execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** The repository's base branch: origin/HEAD's target when known, else main, else master. */
|
|
17
|
-
export function baseBranchOf(repo) {
|
|
18
|
-
try { const ref = git(repo, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]); if (ref) return ref.replace(/^refs\/remotes\//, ""); } catch { /* no origin/HEAD */ }
|
|
19
|
-
for (const b of ["origin/main", "main", "origin/master", "master"]) {
|
|
20
|
-
try { git(repo, ["rev-parse", "--verify", "--quiet", b]); return b; } catch { /* next */ }
|
|
21
|
-
}
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Returns { action: "absent" | "deleted", base } or throws E_HARVEST_BRANCH_EXISTS. */
|
|
26
|
-
export function reclaimHarvestBranch(repo, branch) {
|
|
27
|
-
try { git(repo, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]); }
|
|
28
|
-
catch { return { action: "absent" }; }
|
|
29
|
-
const base = baseBranchOf(repo);
|
|
30
|
-
let merged = false;
|
|
31
|
-
if (base) { try { git(repo, ["merge-base", "--is-ancestor", branch, base]); merged = true; } catch { merged = false; } }
|
|
32
|
-
if (!merged) {
|
|
33
|
-
const err = new Error(`branch ${branch} already exists in ${repo} and is not merged into ${base || "any base branch"}: a previous harvest's promotion is unfinished — review and merge or delete it (git -C ${shellQuote(repo)} branch -D ${shellQuote(branch)}) before harvesting again`);
|
|
34
|
-
err.code = "E_HARVEST_BRANCH_EXISTS";
|
|
35
|
-
throw err;
|
|
36
|
-
}
|
|
37
|
-
// -D, not -d: the merge check above is against the BASE (origin/main when
|
|
38
|
-
// present). `branch -d` re-checks against the branch's upstream or the
|
|
39
|
-
// current HEAD instead, so with the soul's local main behind origin/main a
|
|
40
|
-
// branch fully merged upstream would still be refused as "not fully merged".
|
|
41
|
-
git(repo, ["branch", "-D", branch]);
|
|
42
|
-
return { action: "deleted", base };
|
|
43
|
-
}
|