@awebai/oats 0.22.17 → 0.23.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/README.md +7 -2
- package/bin/oats.mjs +365 -31
- package/docs/configuration.md +65 -0
- package/docs/design/2026-09-07-mobile-agent-management-proposal.md +228 -0
- 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/launch-configurations.md +164 -0
- package/docs/design/package-runtime-api.md +177 -3
- package/docs/desktop-cli-api.md +68 -2
- package/docs/desktop-instance-start.md +39 -3
- package/docs/execution-targets.md +16 -0
- package/docs/knowledge-capability-authoring.md +98 -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/layers.md +8 -7
- package/docs/oats-config.schema.json +33 -2
- package/docs/release-notes/v0.22.18.md +101 -0
- package/docs/release-notes/v0.22.19.md +115 -0
- package/docs/release-notes/v0.23.0.md +93 -0
- package/docs/souls-and-instances.md +18 -1
- package/injects/work-directory.md +18 -0
- package/lib/core.mjs +1109 -187
- package/lib/schedule.mjs +12 -2
- package/lib/servers.mjs +89 -4
- package/package.json +2 -2
- package/packages/record/README.md +19 -0
- package/packages/record/bin/capture.mjs +144 -53
- 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 +81 -5
- 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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Explicit observer-time fallback only; managed capture uses native-history.
|
|
2
|
+
// Resolve only transcript-location inputs. Never execute recorded launch
|
|
3
|
+
// commands or disclose unrelated environment values (which may be secrets).
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
|
|
8
|
+
const LOCATION_ENV = ["HOME", "CLAUDE_CONFIG_DIR", "PI_CODING_AGENT_DIR", "PI_CODING_AGENT_SESSION_DIR", "CODEX_HOME"];
|
|
9
|
+
|
|
10
|
+
export function sourceSessionEnvironment(home, base = process.env) {
|
|
11
|
+
const env = { ...base };
|
|
12
|
+
let meta;
|
|
13
|
+
try { meta = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); }
|
|
14
|
+
catch (err) {
|
|
15
|
+
if (err.code !== "ENOENT") throw new Error(`cannot resolve source transcript roots from instance.json (${err.code || "invalid JSON"})`);
|
|
16
|
+
}
|
|
17
|
+
const recipe = meta?.launch;
|
|
18
|
+
if (recipe !== undefined) {
|
|
19
|
+
if (!recipe || recipe.version !== 1 || !["claude", "pi", "codex"].includes(recipe.runtime)) {
|
|
20
|
+
throw new Error("cannot resolve source transcript roots: unsupported recorded launch recipe");
|
|
21
|
+
}
|
|
22
|
+
for (const layer of [recipe.hooks?.env, recipe.env]) {
|
|
23
|
+
if (layer === undefined) continue;
|
|
24
|
+
if (!layer || typeof layer !== "object" || Array.isArray(layer)) throw new Error("cannot resolve source transcript roots: invalid launch environment");
|
|
25
|
+
for (const name of LOCATION_ENV) {
|
|
26
|
+
if (!Object.hasOwn(layer, name)) continue;
|
|
27
|
+
const value = layer[name];
|
|
28
|
+
if (typeof value === "string") env[name] = value;
|
|
29
|
+
else if (value && typeof value.fromEnv === "string" && typeof base[value.fromEnv] === "string") env[name] = base[value.fromEnv];
|
|
30
|
+
else throw new Error(`cannot resolve source transcript roots: unresolved recorded ${name}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Pi also supports a direct session-directory override. Do not certify
|
|
34
|
+
// default roots when native options direct evidence somewhere else.
|
|
35
|
+
if (recipe.runtime === "pi") {
|
|
36
|
+
const args = recipe.args ?? [];
|
|
37
|
+
if (!Array.isArray(args) || args.some((a) => typeof a !== "string")) throw new Error("cannot resolve source transcript roots: invalid launch arguments");
|
|
38
|
+
for (let i = 0; i < args.length; i++) {
|
|
39
|
+
if (args[i] === "--session-dir" || args[i].startsWith("--session-dir=")) {
|
|
40
|
+
const value = args[i] === "--session-dir" ? args[++i] : args[i].slice("--session-dir=".length);
|
|
41
|
+
if (!value || value.startsWith("--")) throw new Error("cannot resolve source transcript roots: invalid --session-dir");
|
|
42
|
+
env.PI_CODING_AGENT_SESSION_DIR = value;
|
|
43
|
+
} else if (args[i] === "--session" || args[i].startsWith("--session=")) {
|
|
44
|
+
throw new Error("cannot certify transcript roots for a recorded explicit --session; capture its source directory explicitly");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (/--session(?:-dir)?(?:[=\s]|$)/.test(recipe.hooks?.launch?.pi ?? "")) {
|
|
48
|
+
throw new Error("cannot resolve source transcript roots from shell-form session options in launch hooks");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else if (typeof meta?.command === "string" && /(?:^|\s)(?:CLAUDE_CONFIG_DIR|PI_CODING_AGENT_(?:SESSION_)?DIR|CODEX_HOME|HOME)=|--session(?:-dir)?(?:[=\s]|$)/.test(meta.command)) {
|
|
52
|
+
// Old command strings cannot safely be interpreted as environment maps.
|
|
53
|
+
throw new Error("cannot resolve source transcript roots from a legacy launch command; a recorded launch environment is required");
|
|
54
|
+
}
|
|
55
|
+
const userHome = env.HOME ?? homedir();
|
|
56
|
+
if (!userHome || !isAbsolute(userHome)) throw new Error("cannot resolve source transcript roots: HOME must be absolute");
|
|
57
|
+
return { env, home: userHome };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function nativeDirectory(value, { home = homedir(), cwd = process.cwd(), tilde = false } = {}) {
|
|
61
|
+
if (typeof value !== "string" || !value || value.includes("\0")) throw new Error("cannot resolve native transcript directory");
|
|
62
|
+
if (value.startsWith("~")) {
|
|
63
|
+
if (!tilde || !(value === "~" || value.startsWith("~/"))) throw new Error("cannot resolve unexpanded native transcript directory");
|
|
64
|
+
value = join(home, value.slice(2));
|
|
65
|
+
}
|
|
66
|
+
return resolve(cwd, value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Native execution-side locations, without existence filtering. Unlike a
|
|
70
|
+
* background observer scan, Claude's native default is exactly ~/.claude,
|
|
71
|
+
* not every .claude* profile found under an observer's HOME. */
|
|
72
|
+
export function nativeLaunchLocations(runtime, { cwd, env = process.env, args = [] } = {}) {
|
|
73
|
+
const home = env.HOME || homedir();
|
|
74
|
+
if (!isAbsolute(home)) throw new Error("native launch HOME must be absolute");
|
|
75
|
+
if (!Array.isArray(args) || args.some(a => typeof a !== "string")) throw new Error("invalid native launch arguments");
|
|
76
|
+
if (runtime === "claude") return [join(nativeDirectory(env.CLAUDE_CONFIG_DIR || join(home, ".claude"), { home, cwd }), "projects")];
|
|
77
|
+
if (runtime === "codex") return [join(nativeDirectory(env.CODEX_HOME || join(home, ".codex"), { home, cwd }), "sessions")];
|
|
78
|
+
if (runtime !== "pi") throw new Error("unsupported native record runtime");
|
|
79
|
+
let sessionDir = env.PI_CODING_AGENT_SESSION_DIR;
|
|
80
|
+
for (let i = 0; i < args.length; i++) {
|
|
81
|
+
const arg = args[i];
|
|
82
|
+
if (arg === "--session" || arg.startsWith("--session=")) throw new Error("explicit Pi --session has no supported directory custody; capture explicit roots instead");
|
|
83
|
+
if (arg === "--session-dir" || arg.startsWith("--session-dir=")) {
|
|
84
|
+
sessionDir = arg === "--session-dir" ? args[++i] : arg.slice("--session-dir=".length);
|
|
85
|
+
if (!sessionDir || sessionDir.startsWith("--")) throw new Error("invalid native --session-dir");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [sessionDir ? nativeDirectory(sessionDir, { home, cwd, tilde: true })
|
|
89
|
+
: join(nativeDirectory(env.PI_CODING_AGENT_DIR || join(home, ".pi", "agent"), { home, cwd, tilde: true }), "sessions")];
|
|
90
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// File witnesses cross the discovery -> capture-lock boundary. Paths alone
|
|
2
|
+
// are not attribution: a renamed/replaced source must never donate bytes to
|
|
3
|
+
// the previous source's stream. Reads and validation use the SAME descriptor.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { fstatSync, readSync, statSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
export function identity(stat) {
|
|
8
|
+
if (!stat.isFile()) throw new Error("session source is not a regular file");
|
|
9
|
+
return { dev: stat.dev, ino: stat.ino, size: stat.size, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function sameFile(a, b) { return a.dev === b.dev && a.ino === b.ino; }
|
|
13
|
+
export function sameVersion(a, b) {
|
|
14
|
+
return sameFile(a, b) && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
15
|
+
}
|
|
16
|
+
export function digest(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
|
|
17
|
+
|
|
18
|
+
export function readRange(fd, start, size, path) {
|
|
19
|
+
const buf = Buffer.alloc(size - start);
|
|
20
|
+
let done = 0;
|
|
21
|
+
while (done < buf.length) {
|
|
22
|
+
const n = readSync(fd, buf, done, buf.length - done, start + done);
|
|
23
|
+
if (n === 0) throw new Error(`short read of session source: ${path}`);
|
|
24
|
+
done += n;
|
|
25
|
+
}
|
|
26
|
+
return buf;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hashPrefix(fd, size, path) {
|
|
30
|
+
const hash = createHash("sha256"), buf = Buffer.alloc(64 * 1024);
|
|
31
|
+
for (let pos = 0; pos < size;) {
|
|
32
|
+
const n = readSync(fd, buf, 0, Math.min(buf.length, size - pos), pos);
|
|
33
|
+
if (n === 0) throw new Error(`short read of session source: ${path}`);
|
|
34
|
+
hash.update(buf.subarray(0, n)); pos += n;
|
|
35
|
+
}
|
|
36
|
+
return hash.digest("hex");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function assertIdentity(stat, expected, path) {
|
|
40
|
+
if (!stat.isFile() || !sameFile(stat, expected) || stat.size < expected.size) {
|
|
41
|
+
throw new Error(`session source identity changed or shrank since attribution: ${path}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Append growth is allowed, but only with an identical witnessed prefix.
|
|
46
|
+
// A changed same-size file is not append growth. Verification happens before
|
|
47
|
+
// journal writes; afterwards the buffer to append is independent of the path.
|
|
48
|
+
export function verifySnapshot(fd, path, snapshot) {
|
|
49
|
+
const after = fstatSync(fd), named = statSync(path);
|
|
50
|
+
assertIdentity(after, snapshot, path);
|
|
51
|
+
assertIdentity(named, snapshot, path);
|
|
52
|
+
if (!sameVersion(after, named)) throw new Error(`session source changed during capture: ${path}`);
|
|
53
|
+
if (sameVersion(after, snapshot)) return;
|
|
54
|
+
if (after.size <= snapshot.size || hashPrefix(fd, snapshot.size, path) !== snapshot.hash) {
|
|
55
|
+
throw new Error(`session source changed during capture: ${path}`);
|
|
56
|
+
}
|
|
57
|
+
// A second change while validating is uncertain, not absence of evidence.
|
|
58
|
+
if (!sameVersion(after, fstatSync(fd)) || !sameVersion(after, statSync(path))) {
|
|
59
|
+
throw new Error(`session source changed during verification: ${path}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -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)
|