@awebai/oats 0.39.0 → 0.39.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/docs/desktop-cli-api.md +11 -0
- package/docs/integrations.md +8 -2
- package/docs/official-catalog.md +1 -1
- package/docs/packages.md +2 -2
- package/docs/release-notes/v0.39.1.md +42 -0
- package/lib/instance-inspect.mjs +51 -5
- package/lib/instance-resolution.mjs +15 -2
- package/lib/remote.mjs +4 -2
- package/package-catalog.json +1 -1
- package/package.json +1 -1
package/docs/desktop-cli-api.md
CHANGED
|
@@ -468,6 +468,17 @@ confirmed member (`evidence: {repoKey, workspace, commit}`); `fail` when not
|
|
|
468
468
|
(the remedy names `oats-membership.yaml`); `unknown` when the workspace could
|
|
469
469
|
not be read. External souls and standalone views are `not-applicable`,
|
|
470
470
|
`required: false` (a standalone item carries `evidence.standaloneReason`).
|
|
471
|
+
A package soul is trusted through the workspace's `packages:` pin, never
|
|
472
|
+
through membership ([the non-collapse rule](workspaces.md#member-tier-vs-package-tier-the-non-collapse-rule)):
|
|
473
|
+
its item is `package <package>/<soul>` (producer `workspace lock`,
|
|
474
|
+
`evidence: {repoKey, workspace, commit, version, integrity, from}` with
|
|
475
|
+
`from.kind: "package"`). It passes when the package is declared and locked
|
|
476
|
+
and ships the soul at the locked commit, and the soul's copy is intact: for
|
|
477
|
+
`--soul`, the copy a spawn would link matches the locked digest; for
|
|
478
|
+
`--home`, the home's copy matches the digest it recorded, and a lock still
|
|
479
|
+
at that commit records the same digest. The copy is digested on every read.
|
|
480
|
+
It fails otherwise, with `code: "E_PACKAGE_INTEGRITY"` on a digest mismatch
|
|
481
|
+
and the remedy naming `oats sync` or a fresh spawn.
|
|
471
482
|
|
|
472
483
|
**`providers`** (producer `provider binding check`): for each module whose
|
|
473
484
|
manifest declares `binding`, the kernel runs its `binding.check` and relays
|
package/docs/integrations.md
CHANGED
|
@@ -41,7 +41,7 @@ arrives from.
|
|
|
41
41
|
# oats-workspace.yaml: one default per slot, for every soul
|
|
42
42
|
packages:
|
|
43
43
|
oats.okf: v4.1.1
|
|
44
|
-
oats.aweb: v1.21.
|
|
44
|
+
oats.aweb: v1.21.1
|
|
45
45
|
oats.linear: v1.0.1
|
|
46
46
|
oats.jira: v1.0.1
|
|
47
47
|
defaults:
|
|
@@ -89,7 +89,13 @@ operations in full.
|
|
|
89
89
|
`knowledge-maintainer` package soul reviews the resulting PRs.
|
|
90
90
|
- **`oats.aweb`** mints a messaging identity for each instance at spawn and
|
|
91
91
|
removes it at retire, contributes the aweb messaging skills, and wires the
|
|
92
|
-
channel so sessions are woken by mail.
|
|
92
|
+
channel so sessions are woken by mail. Under `delivery: channel`, Claude Code
|
|
93
|
+
loads the channel plugin with `--dangerously-load-development-channels`,
|
|
94
|
+
since `aweb-channel` is not on its approved channel list, so every such
|
|
95
|
+
Claude start waits at Claude Code's
|
|
96
|
+
development-channels confirmation until someone answers it in the
|
|
97
|
+
instance's terminal; the start's warnings and readiness say so
|
|
98
|
+
(`channel-dev-confirmation`). `oats aweb roster` lists the team:
|
|
93
99
|
its membership certificates and workspaces, one entry per alias with its
|
|
94
100
|
sources, status and kind (global identities first), and says when either
|
|
95
101
|
source is incomplete. After `oats server connect`, `oats aweb connect
|
package/docs/official-catalog.md
CHANGED
|
@@ -11,7 +11,7 @@ or workspace membership alone does not make a package official.
|
|
|
11
11
|
|---|---|---|---|
|
|
12
12
|
| `oats.framework` | `oats-framework/v1.5.0` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
|
|
13
13
|
| `oats.okf` | `v4.1.1` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
|
|
14
|
-
| `oats.aweb` | `v1.21.
|
|
14
|
+
| `oats.aweb` | `v1.21.1` | `oats.aweb` (messaging) | |
|
|
15
15
|
| `oats.engineering` | `v1.5.0` | `oats.engineering-expert`, `oats.developer`, `oats.code-review` | `code-reviewer` |
|
|
16
16
|
| `oats.authoring` | `v1.0.3` | `oats.authoring` | |
|
|
17
17
|
| `oats.jira` | `v1.0.1` | `oats.jira` (tasks) | |
|
package/docs/packages.md
CHANGED
|
@@ -76,7 +76,7 @@ members:
|
|
|
76
76
|
packages:
|
|
77
77
|
oats.framework: v1.5.0
|
|
78
78
|
oats.okf: v4.1.1
|
|
79
|
-
oats.aweb: v1.21.
|
|
79
|
+
oats.aweb: v1.21.1
|
|
80
80
|
teams:
|
|
81
81
|
platform: { team: "platform:acme.aweb.ai", description: Platform engineering }
|
|
82
82
|
defaults:
|
|
@@ -134,7 +134,7 @@ Declaring a package in the workspace's `packages:` is the trust decision
|
|
|
134
134
|
## `oats package add | remove`
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
|
-
oats package add oats.aweb v1.21.
|
|
137
|
+
oats package add oats.aweb v1.21.1 # a catalog version
|
|
138
138
|
oats package add acme.tools git:github.com/acme/tools@v0.4.0
|
|
139
139
|
oats package remove acme.tools
|
|
140
140
|
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# OATS 0.39.1
|
|
2
|
+
|
|
3
|
+
## Changed
|
|
4
|
+
|
|
5
|
+
- **oats.aweb 1.21.1: a Claude Code start says that it will wait at Claude
|
|
6
|
+
Code's development-channels confirmation** (awebai/oats-aweb#44). Under
|
|
7
|
+
`delivery: channel`, Claude Code loads the `aweb-channel` plugin with
|
|
8
|
+
`--dangerously-load-development-channels`, because the plugin is not on
|
|
9
|
+
Claude Code's approved channel list. Claude Code then stops at its "Loading
|
|
10
|
+
development channels" confirmation before the session starts, and waits
|
|
11
|
+
until someone answers it in the instance's terminal. Unattended starts
|
|
12
|
+
(Desktop starts and restarts, automations, successors) waited there with
|
|
13
|
+
nothing saying so. Now:
|
|
14
|
+
- every spawn or start that adds the flag carries the warning
|
|
15
|
+
`channel-dev-confirmation` once;
|
|
16
|
+
- `oats readiness` reports it, without changing the status, for a home
|
|
17
|
+
whose last start was Claude Code under `channel`.
|
|
18
|
+
|
|
19
|
+
Nothing answers the confirmation on the human's behalf. Launch arguments
|
|
20
|
+
and settings are unchanged. `--channels` registers only plugins on Claude
|
|
21
|
+
Code's approved list; for a plugin not on it, Claude Code prints a startup
|
|
22
|
+
warning and the channel does not register. That list is Anthropic's
|
|
23
|
+
default, or a Team/Enterprise organization's managed
|
|
24
|
+
`allowedChannelPlugins`, which replaces the default and requires
|
|
25
|
+
`channelsEnabled: true`.
|
|
26
|
+
|
|
27
|
+
## Fixed
|
|
28
|
+
|
|
29
|
+
- **`oats readiness` no longer fails package souls on membership**
|
|
30
|
+
(awebai/oats#533). A package soul (`oats.okf/knowledge-maintainer`,
|
|
31
|
+
`oats.engineering/code-reviewer`, …) failed its `member` check with
|
|
32
|
+
"member <its package's repository>: not a member of the workspace", and the
|
|
33
|
+
remedy asked to list the package's repository in `members:`. A package
|
|
34
|
+
soul is trusted through the workspace's `packages:` pin, never through
|
|
35
|
+
membership (see [the non-collapse rule](../workspaces.md#member-tier-vs-package-tier-the-non-collapse-rule)).
|
|
36
|
+
Its `member` item is now `package <package>/<soul>`. It passes when the
|
|
37
|
+
package is declared and locked, ships the soul at the locked commit, and
|
|
38
|
+
the soul's copy matches its digest: for `--soul`, the copy a spawn would
|
|
39
|
+
link against the lock; for `--home`, the home's copy against the digest it
|
|
40
|
+
recorded, and the lock against that same digest while it still pins the
|
|
41
|
+
same commit. A mismatch fails with `E_PACKAGE_INTEGRITY`. Member and
|
|
42
|
+
external souls are unchanged. Spawn already applied this rule.
|
package/lib/instance-inspect.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { accessSync, constants as fsConstants, existsSync, readFileSync, realpat
|
|
|
15
15
|
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { capabilityManifests, sessionDefaults, homeLaunchLayers, instanceSoulDir, launchConfigsAt, launchFolderTrust, launchReportFor, manifestOperations, parseYamlNested, servedIdentityOf, teamEnv, upgradeHomeMeta, withConfigFile } from "./core.mjs";
|
|
18
|
-
import { agentDirOf, discoverOrStandalone, ensureWorkspaceSoul, findSoulEntry, liveTeams, prepareInstance } from "./instance-resolution.mjs";
|
|
18
|
+
import { agentDirOf, discoverOrStandalone, ensureWorkspaceSoul, findSoulEntry, liveTeams, prepareInstance, soulCopyDigests } from "./instance-resolution.mjs";
|
|
19
19
|
import { declaredSettings } from "./capability-contract.mjs";
|
|
20
20
|
import { kernelCompatibility } from "./resolve.mjs";
|
|
21
21
|
import { discoveredTeamKeys, isRemovedTeamKeys, isTeamRefusal, recordedTeams, reportRows, soulKeyOf, teamKeyOf, soulTeams, teamModel, teamProblems } from "./teams.mjs";
|
|
@@ -133,7 +133,8 @@ export async function homeTarget(home, meta, { remoteOptions, discover = true, l
|
|
|
133
133
|
kind: "instance", home: realHome, meta, deployment, agentsRoot, teams: live.teams, defaultTeam: live.defaultTeam ?? null, teamsSource: live.source, launch, launchCurrent, session: newSessionDefaults(deployment),
|
|
134
134
|
recordedDefaultTeam: recordedTeams(meta).defaultTeam, teamModel: model, teamKey,
|
|
135
135
|
subject: { kind: "instance", instance: meta.instance, home, soul: meta.agent },
|
|
136
|
-
soul: { name: meta.agent, repoKey: ws.soul?.repoKey ?? null, commit: ws.soul?.commit ?? null, external, path: null, soulDir, definition, problems: [...problems, ...loadProblems]
|
|
136
|
+
soul: { name: meta.agent, repoKey: ws.soul?.repoKey ?? null, commit: ws.soul?.commit ?? null, external, path: null, soulDir, definition, problems: [...problems, ...loadProblems],
|
|
137
|
+
...(obj(ws.soul?.package) && typeof ws.soul.package.id === "string" ? { package: { id: ws.soul.package.id, soul: ws.soul.name ?? meta.agent, version: ws.soul.package.version ?? null, commit: ws.soul.package.commit ?? ws.soul.commit ?? null, integrity: ws.soul.package.digest ?? null } } : {}) },
|
|
137
138
|
workspace: { key: ws.key ?? null, name: typeof ws.name === "string" ? ws.name : discovery?.workspace?.name ?? null, deployment, commit: ws.commit ?? null, standalone: ws.standalone === true },
|
|
138
139
|
modules: Object.keys(modules).sort().map((name) => ({ name, from: modules[name]?.from ?? null, manifest: manifests[name] ?? null, dir: join(realHome, ".oats", "modules", name) })),
|
|
139
140
|
payloads: obj(meta.providers) ? meta.providers : {},
|
|
@@ -189,7 +190,8 @@ export async function soulTarget(contextDir, soul, { remoteOptions } = {}) {
|
|
|
189
190
|
kind: "soul", home: null, meta: null, deployment, agentsRoot: join(deployment, "agents"), teams, defaultTeam, teamsSource: "live", teamModel: model, teamKey, launch, session: newSessionDefaults(deployment),
|
|
190
191
|
subject: { kind: "soul", soul: soulEntry.name, repoKey: soulEntry.repoKey ?? null, commit: soulEntry.commit ?? null },
|
|
191
192
|
soul: { name: soulEntry.name, repoKey: soulEntry.repoKey ?? null, commit: soulEntry.commit ?? null, external: soulEntry.external === true, path: soulEntry.path ?? null,
|
|
192
|
-
soulDir, definition: soulEntry.definition ?? null, problems: [], ...(soulCopyError ? { copyError: soulCopyError } : {})
|
|
193
|
+
soulDir, definition: soulEntry.definition ?? null, problems: [], ...(soulCopyError ? { copyError: soulCopyError } : {}),
|
|
194
|
+
...(typeof soulEntry.package === "string" ? { package: { id: soulEntry.package, soul: soulEntry.name, version: soulEntry.version ?? null, commit: soulEntry.commit ?? null, integrity: soulEntry.digest ?? null } } : {}) },
|
|
193
195
|
workspace: { key: discovery?.key ?? null, name: discovery?.workspace?.name ?? null, deployment, commit: discovery?.commit ?? null, standalone: discovery?.standalone === true },
|
|
194
196
|
modules: (res?.modules || []).map((m) => ({ name: m.name, from: m.from ?? null, manifest: m.manifest ?? null, dir: null, module: m })).sort((a, b) => a.name.localeCompare(b.name)),
|
|
195
197
|
payloads: obj(res?.payloads) ? res.payloads : {}, payloadOrigins: obj(res?.payloadOrigins) ? res.payloadOrigins : {}, slots: obj(res?.slots) ? res.slots : Object.fromEntries(LAYERS.map((l) => [l, null])), slotsFrom: obj(res?.slotsFrom) ? res.slotsFrom : {},
|
|
@@ -404,6 +406,48 @@ export function runProviderCheck(t, mod, dir, { timeoutMs = 30000 } = {}) {
|
|
|
404
406
|
|| (res.status === "ready" && res.problems.length)) return invalid();
|
|
405
407
|
return { outcome: "result", result: { status: res.status, problems: codeMessages(res.problems), warnings: codeMessages(res.warnings ?? []) } };
|
|
406
408
|
}
|
|
409
|
+
/** The `member` item of a package soul. A package soul is trusted through the workspace's
|
|
410
|
+
* `packages:` pin (the lock's commit and integrity), never through membership: its
|
|
411
|
+
* repository is not a member and need not be (docs/workspaces.md, the non-collapse rule).
|
|
412
|
+
* It passes when the discovery lists the soul from the lock (the package declared and
|
|
413
|
+
* locked, the soul shipped at the locked commit) and the soul's copy is intact: for a
|
|
414
|
+
* soul, the copy a spawn would link matches the locked digest; for a home, its copy
|
|
415
|
+
* matches the digest it recorded, and a lock still at that commit records the same one.
|
|
416
|
+
* A copy is digested every read: a complete copy is reused, never refetched. */
|
|
417
|
+
function packageSoulItem(t, d) {
|
|
418
|
+
const p = t.soul.package;
|
|
419
|
+
const subject = `package ${p.id}/${p.soul}`;
|
|
420
|
+
const from = { kind: "package", package: p.id, version: p.version, commit: p.commit, integrity: p.integrity };
|
|
421
|
+
const evidence = { repoKey: t.soul.repoKey, workspace: d?.key ?? null, commit: p.commit, version: p.version, integrity: p.integrity, from };
|
|
422
|
+
if (!d) return item(subject, "unknown", { producer: "workspace discovery", reason: t.discoveryError ? `the workspace could not be read: ${t.discoveryError.message}` : "no workspace observation", evidence });
|
|
423
|
+
if (d.standalone === true) return item(subject, "not-applicable", { required: false, producer: "workspace discovery", reason: `standalone view (${d.standaloneReason ?? "explicit"}): the workspace's packages: is not read, so the package pin is recorded, not observed`, evidence });
|
|
424
|
+
const shipped = (d.packageSouls || []).filter((s) => s.package === p.id);
|
|
425
|
+
const live = shipped.find((s) => s.name === p.soul);
|
|
426
|
+
if (!live) {
|
|
427
|
+
return item(subject, "fail", { producer: "workspace lock", evidence,
|
|
428
|
+
reason: shipped.length ? `the version of ${p.id} locked in this workspace does not ship the soul ${p.soul}` : `${p.id} is not locked in this workspace's packages:`,
|
|
429
|
+
remedy: shipped.length ? `pin a release of ${p.id} that ships ${p.soul} in the workspace's packages:, then oats sync` : `declare ${p.id} in the workspace's packages:, then oats sync` });
|
|
430
|
+
}
|
|
431
|
+
const integrity = (reason, remedy) => item(subject, "fail", { producer: "workspace lock", code: "E_PACKAGE_INTEGRITY", reason, evidence, remedy });
|
|
432
|
+
const lockRemedy = "oats sync (a lock that was edited: remove the entry and sync again)";
|
|
433
|
+
const copy = t.soul.copyError;
|
|
434
|
+
if (copy?.code === "E_PACKAGE_INTEGRITY") return integrity(copy.message, lockRemedy);
|
|
435
|
+
const dir = t.soul.soulDir;
|
|
436
|
+
if (!dir) return item(subject, "unknown", { producer: "workspace lock", reason: "the soul's copy is not available, so its integrity cannot be checked", evidence });
|
|
437
|
+
const expected = t.home ? "recorded" : "locked";
|
|
438
|
+
let digests;
|
|
439
|
+
try { digests = soulCopyDigests(dir); }
|
|
440
|
+
catch (e) { return integrity(`the soul's copy at ${dir} cannot be digested: ${e.message}`, `move ${dir} aside, then spawn again (a fresh copy is fetched and verified)`); }
|
|
441
|
+
if (!digests.includes(p.integrity)) {
|
|
442
|
+
return integrity(`the soul's copy at ${dir} digests ${digests.at(-1)}, not the ${expected} ${p.integrity}${t.home ? "" : " (the lock's)"}`,
|
|
443
|
+
t.home ? `the copy changed after its spawn, and a spawn reuses it: move ${dir} aside, then spawn a new instance of ${p.id}/${p.soul} (a fresh copy is fetched and verified against the lock)` : `move ${dir} aside, then spawn again (a fresh copy is fetched and verified against the lock); if the lock was edited: ${lockRemedy}`);
|
|
444
|
+
}
|
|
445
|
+
if (t.home && live.commit === p.commit && live.digest !== p.integrity) {
|
|
446
|
+
return integrity(`the lock records ${live.digest} for the same commit ${String(p.commit).slice(0, 12)}; this home recorded ${p.integrity} at its spawn`, lockRemedy);
|
|
447
|
+
}
|
|
448
|
+
return item(subject, "pass", { producer: "workspace lock", evidence });
|
|
449
|
+
}
|
|
450
|
+
|
|
407
451
|
/** The total time the providers check may take in one readiness read. */
|
|
408
452
|
export const PROVIDER_CHECK_BUDGET_MS = 60_000;
|
|
409
453
|
|
|
@@ -430,9 +474,11 @@ export async function readinessDocument(t, { selector = null, remoteOptions, cat
|
|
|
430
474
|
}
|
|
431
475
|
configured.push(...teamItems(t), ...launchItems(t), ...folderTrustItems(t));
|
|
432
476
|
// member — the soul's member repository is a confirmed member of the workspace
|
|
433
|
-
// (oats-membership.yaml backlink observed over the remotes)
|
|
477
|
+
// (oats-membership.yaml backlink observed over the remotes); a package soul's is its
|
|
478
|
+
// package instead (packageSoulItem).
|
|
434
479
|
const d = t.discovery;
|
|
435
|
-
if (t.soul.
|
|
480
|
+
if (t.soul.package) member.push(packageSoulItem(t, d));
|
|
481
|
+
else if (t.soul.external) member.push(item(`soul ${t.soul.name}`, "not-applicable", { required: false, producer: "workspace discovery", reason: "an external soul is declared by the workspace; it has no member repository", evidence: { repoKey: t.soul.repoKey } }));
|
|
436
482
|
else if (!d) member.push(item(`member ${t.soul.repoKey ?? "?"}`, "unknown", { producer: "workspace discovery", reason: t.discoveryError ? `the workspace could not be read: ${t.discoveryError.message}` : "no workspace observation", evidence: { repoKey: t.soul.repoKey } }));
|
|
437
483
|
// A standalone view is an allowed mode (decision 10): membership cannot be confirmed
|
|
438
484
|
// there by definition, so it is not a readiness requirement (as for an external soul).
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Nothing here reads `oats-config.yaml`, an installed-capability directory or a
|
|
18
18
|
* per-soul `source:` — those do not exist in this model. */
|
|
19
|
-
import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "node:fs";
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
20
20
|
import { join, resolve as resolvePath, dirname, relative, isAbsolute, sep } from "node:path";
|
|
21
21
|
import { oatsError } from "./errors.mjs";
|
|
22
22
|
import { loadLocal, discoverWorkspace, discoverRepo, standaloneRepo, observeWorkspace } from "./workspace.mjs";
|
|
@@ -25,7 +25,7 @@ import { BY_TEAM_REMOVED, isRemovedTeamKeys, isTeamRefusal, memberNameOf, record
|
|
|
25
25
|
import { launchLayers } from "./launch-preference.mjs";
|
|
26
26
|
import { declaredSettings } from "./capability-contract.mjs";
|
|
27
27
|
import { materialize, moduleSkills, MODULES_DIR, SKILLS_DIR } from "./materialize.mjs";
|
|
28
|
-
import { fetchRemoteTree } from "./remote.mjs";
|
|
28
|
+
import { contentDigest, fetchRemoteTree } from "./remote.mjs";
|
|
29
29
|
import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync, symlinkSync, statSync } from "node:fs";
|
|
30
30
|
import { tmpdir } from "node:os";
|
|
31
31
|
import { spawnSync } from "node:child_process";
|
|
@@ -232,6 +232,19 @@ async function fetchSoulSource(prepared, dest) {
|
|
|
232
232
|
if (!existsSync(join(dest, "CLAUDE.md"))) symlinkSync("AGENTS.md", join(dest, "CLAUDE.md"));
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/** The digests a soul copy may carry as its source was fetched (fetchSoulSource): the copy itself, and,
|
|
236
|
+
* when it holds the CLAUDE.md → AGENTS.md alias, the copy without it, since fetchSoulSource adds that
|
|
237
|
+
* alias to a source that has none. A package soul's copy is intact when one of them is its digest. */
|
|
238
|
+
export function soulCopyDigests(dir) {
|
|
239
|
+
const options = { allowSymlinks: SOUL_ALIAS_SYMLINK };
|
|
240
|
+
const alias = join(dir, "CLAUDE.md");
|
|
241
|
+
let aliased = false;
|
|
242
|
+
try { aliased = lstatSync(alias).isSymbolicLink() && readlinkSync(alias) === "AGENTS.md"; } catch { aliased = false; }
|
|
243
|
+
const digests = [contentDigest(dir, options)];
|
|
244
|
+
if (aliased) digests.push(contentDigest(dir, { ...options, omit: (rel) => rel === "CLAUDE.md" }));
|
|
245
|
+
return digests;
|
|
246
|
+
}
|
|
247
|
+
|
|
235
248
|
/** A preview's soul source. A preview writes NOTHING in the deployment (K6b), so it
|
|
236
249
|
* never populates the per-commit cache or moves the `soul` pointer: it reads the
|
|
237
250
|
* cache entry for the prepared commit when one is already complete, else fetches
|
package/lib/remote.mjs
CHANGED
|
@@ -2139,7 +2139,8 @@ function createDigest() {
|
|
|
2139
2139
|
}
|
|
2140
2140
|
|
|
2141
2141
|
/** Same digest as fetchRemoteTree, computed over a local directory. Symlinks and
|
|
2142
|
-
* non-regular entries are refused (E_REMOTE_TREE_UNSAFE); a missing dir → E_REMOTE_PATH_MISSING.
|
|
2142
|
+
* non-regular entries are refused (E_REMOTE_TREE_UNSAFE); a missing dir → E_REMOTE_PATH_MISSING.
|
|
2143
|
+
* `omit(relPath, absPath)` → true leaves an entry out (what a copy gained after its fetch). */
|
|
2143
2144
|
function posixNormalize(p) {
|
|
2144
2145
|
const out = [];
|
|
2145
2146
|
for (const seg of p.split("/")) { if (!seg || seg === ".") continue; if (seg === "..") { if (out.length && out.at(-1) !== "..") out.pop(); else out.push(".."); } else out.push(seg); }
|
|
@@ -2155,7 +2156,7 @@ function posixNormalize(p) {
|
|
|
2155
2156
|
*/
|
|
2156
2157
|
export const OATS_ALIAS_SYMLINK = (relPath) => typeof relPath === "string" && (relPath === "CLAUDE.md" || relPath.endsWith("/CLAUDE.md"));
|
|
2157
2158
|
|
|
2158
|
-
export function contentDigest(dir, { allowSymlinks = null } = {}) {
|
|
2159
|
+
export function contentDigest(dir, { allowSymlinks = null, omit = null } = {}) {
|
|
2159
2160
|
if (typeof dir !== "string" || !isAbsolute(dir)) throw fail("E_REPO_REF", "contentDigest requires an absolute directory path", { path: dir });
|
|
2160
2161
|
let st;
|
|
2161
2162
|
try { st = lstatSync(dir); } catch { throw fail("E_REMOTE_PATH_MISSING", `${dir} does not exist`, { path: dir }); }
|
|
@@ -2165,6 +2166,7 @@ export function contentDigest(dir, { allowSymlinks = null } = {}) {
|
|
|
2165
2166
|
for (const name of readdirSync(abs)) {
|
|
2166
2167
|
if (!rel && name === ".git") continue; // a local checkout's metadata is not content
|
|
2167
2168
|
const p = join(abs, name), r = rel ? `${rel}/${name}` : name, s = lstatSync(p);
|
|
2169
|
+
if (typeof omit === "function" && omit(r, p)) continue;
|
|
2168
2170
|
if (s.isSymbolicLink()) {
|
|
2169
2171
|
// Refused by default (contract §1). The same narrow opt-in fetchRemoteTree
|
|
2170
2172
|
// honours (a relative, non-escaping alias such as CLAUDE.md → AGENTS.md)
|
package/package-catalog.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.1",
|
|
4
4
|
"description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|