@awebai/oats 0.25.9 → 0.26.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 +8 -6
- package/bin/oats.mjs +576 -1714
- package/capabilities/oats-authoring/oats-package.json +2 -2
- package/capabilities/oats-authoring/oats.json +2 -2
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +279 -93
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
- package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
- package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
- package/capabilities/oats-aweb/oats.json +8 -4
- package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
- package/capabilities/oats-jira/oats.json +2 -2
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
- package/capabilities/oats-linear/oats.json +2 -2
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
- package/capabilities/oats-review/oats.json +3 -2
- package/docs/capabilities.md +218 -47
- package/docs/capability-manifest.schema.json +13 -4
- package/docs/configuration.md +17 -5
- package/docs/conventions.md +16 -26
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
- package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
- package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
- package/docs/design/2026-09-20-redesign-program-board.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +34 -1
- package/docs/design/2026-09-24-phase-d-plan.md +57 -0
- package/docs/design/2026-09-25-teams-contract.md +226 -0
- package/docs/design/README.md +3 -3
- package/docs/design/launch-configurations.md +20 -16
- package/docs/design/operations-contract.md +27 -10
- package/docs/desktop-cli-api.md +537 -261
- package/docs/desktop-instance-start.md +1 -1
- package/docs/desktop.md +7 -13
- package/docs/execution-targets.md +16 -18
- package/docs/first-team.md +14 -17
- package/docs/implementation.md +28 -59
- package/docs/integrations.md +64 -33
- package/docs/knowledge-capability-authoring.md +1 -1
- package/docs/knowledge-reference/package-craft.md +10 -8
- package/docs/knowledge-theory.md +1 -1
- package/docs/knowledge.md +10 -11
- package/docs/layers.md +16 -17
- package/docs/oats-local.schema.json +29 -1
- package/docs/oats-membership.schema.json +5 -3
- package/docs/oats-package.schema.json +2 -2
- package/docs/oats-workspace.schema.json +1 -1
- package/docs/{official-marketplace.md → official-catalog.md} +15 -16
- package/docs/packages.md +75 -52
- package/docs/release-notes/v0.22.0.md +1 -1
- package/docs/release-notes/v0.23.1.md +1 -1
- package/docs/release-notes/v0.26.0.md +670 -0
- package/docs/schedules.md +48 -126
- package/docs/soul.schema.json +11 -4
- package/docs/souls-and-instances.md +56 -43
- package/docs/workspaces.md +80 -58
- package/injects/instance-boundary.md +1 -1
- package/injects/work-attached.md +1 -1
- package/injects/work-workspace.md +2 -2
- package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
- package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
- package/lib/capability-contract.mjs +110 -0
- package/lib/config-data.mjs +2 -2
- package/lib/core.mjs +700 -4824
- package/lib/digest.mjs +12 -0
- package/lib/instance-inspect.mjs +396 -0
- package/lib/instance-lifecycle.mjs +3 -4
- package/lib/instance-resolution.mjs +212 -26
- package/lib/instruction-composition.mjs +0 -20
- package/lib/materialize.mjs +6 -4
- package/lib/operator-dispatch.mjs +33 -13
- package/lib/packages.mjs +25 -190
- package/lib/provider-binding.mjs +4 -2
- package/lib/provider-reasons.mjs +3 -68
- package/lib/resolve.mjs +204 -68
- package/lib/schedule.mjs +97 -272
- package/lib/servers.mjs +13 -13
- package/lib/{portable-shape.mjs → shape.mjs} +4 -3
- package/lib/tree-copy.mjs +44 -0
- package/lib/workspace.mjs +125 -20
- package/package-catalog.json +6 -6
- package/package.json +1 -1
- package/skills/integration-authoring/SKILL.md +48 -40
- package/skills/oats-getting-started/SKILL.md +105 -110
- package/skills/oats-support/SKILL.md +2 -2
- package/skills/soul-craft/SKILL.md +13 -6
- package/bin/oats-pi-sdk-host.mjs +0 -17
- package/docs/2026-09-03-architecture-proposal.md +0 -642
- package/docs/artifact-approvals.schema.json +0 -7
- package/docs/captured-invocation-context.schema.json +0 -7
- package/docs/captured-resolution.schema.json +0 -7
- package/docs/design/package-engine-contract.md +0 -813
- package/docs/design/package-runtime-api.md +0 -588
- package/docs/desktop-succession.md +0 -57
- package/docs/execution-capsule.schema.json +0 -108
- package/docs/first-team-demo.md +0 -92
- package/docs/knowledge-migration.md +0 -147
- package/docs/migration-from-oas.md +0 -103
- package/docs/oats-config.schema.json +0 -172
- package/docs/oats-lock-v3.schema.json +0 -7
- package/docs/oats-lock.schema.json +0 -175
- package/docs/operating-team-migration.md +0 -470
- package/docs/portable.schema.json +0 -2512
- package/docs/provider-check-input.schema.json +0 -7
- package/docs/rebuild-to-v2.md +0 -511
- package/docs/workspace-adoption.md +0 -74
- package/injects/framework-workspace.md +0 -7
- package/injects/local-soul.md +0 -19
- package/injects/oats-portable.md +0 -20
- package/injects/oats.md +0 -11
- package/injects/portable-instance-boundary.md +0 -39
- package/injects/portable-work-directory.md +0 -29
- package/lib/artifact-approvals.mjs +0 -120
- package/lib/artifact-tree.mjs +0 -141
- package/lib/capability-artifacts.mjs +0 -179
- package/lib/capability-execution.mjs +0 -15
- package/lib/capability-inputs.mjs +0 -39
- package/lib/capability-provenance.mjs +0 -231
- package/lib/captured-action-shape.mjs +0 -21
- package/lib/captured-admission-shape.mjs +0 -20
- package/lib/captured-binding-file.mjs +0 -36
- package/lib/captured-dispatch.mjs +0 -66
- package/lib/captured-instance-index.mjs +0 -277
- package/lib/captured-invocation-context.mjs +0 -130
- package/lib/captured-launch-request.mjs +0 -66
- package/lib/captured-operation-process.mjs +0 -15
- package/lib/captured-pi-custody.mjs +0 -29
- package/lib/captured-pi-host.mjs +0 -167
- package/lib/captured-pi-outcome.mjs +0 -172
- package/lib/captured-resolutions.mjs +0 -275
- package/lib/captured-scaffold.mjs +0 -87
- package/lib/captured-selector.mjs +0 -28
- package/lib/captured-session-backend.mjs +0 -52
- package/lib/captured-source-receipt-file.mjs +0 -72
- package/lib/helper-injection-policy.mjs +0 -104
- package/lib/legacy-lock-codec.mjs +0 -106
- package/lib/manifest-settings.mjs +0 -84
- package/lib/package-closure.mjs +0 -48
- package/lib/package-materialization.mjs +0 -83
- package/lib/pi-sdk-host.mjs +0 -229
- package/lib/portable-artifacts.mjs +0 -115
- package/lib/portable-choices.mjs +0 -82
- package/lib/portable-composition.mjs +0 -136
- package/lib/portable-digest.mjs +0 -105
- package/lib/portable-identity.mjs +0 -40
- package/lib/portable-lock.mjs +0 -117
- package/lib/portable-onboarding-request.mjs +0 -49
- package/lib/portable-onboarding.mjs +0 -256
- package/lib/portable-package-preparation.mjs +0 -188
- package/lib/portable-policy.mjs +0 -44
- package/lib/portable-soul.mjs +0 -42
- package/lib/portable-state.mjs +0 -80
- package/lib/prepare-composition.mjs +0 -170
- package/lib/prepared-bindings.mjs +0 -92
- package/lib/prepared-resources.mjs +0 -127
- package/lib/provider-binding-broker.mjs +0 -65
- package/lib/provider-binding-wire.mjs +0 -116
- package/lib/readiness.mjs +0 -225
- package/lib/repository-observation.mjs +0 -226
- package/lib/resolution-shape.mjs +0 -393
- package/lib/schedule-capsule.mjs +0 -206
- package/lib/soul-constraints.mjs +0 -40
- package/lib/source-projection.mjs +0 -84
- package/lib/source-spec.mjs +0 -189
- package/lib/workspace-definition.mjs +0 -126
- package/lib/workspace-discovery.mjs +0 -146
- package/skills/oats/SKILL.md +0 -162
- package/skills/oats-config/SKILL.md +0 -164
- package/skills/oats-packages/SKILL.md +0 -184
- package/skills/oats-portable/SKILL.md +0 -115
- package/skills/oats-portable-artifacts/SKILL.md +0 -63
package/lib/servers.mjs
CHANGED
|
@@ -21,8 +21,7 @@ import { createHash } from "node:crypto";
|
|
|
21
21
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { join, resolve } from "node:path";
|
|
24
|
-
import { parseStrictJson } from "./
|
|
25
|
-
import { capturedSelector } from "./captured-selector.mjs";
|
|
24
|
+
import { parseStrictJson } from "./canonical-json.mjs";
|
|
26
25
|
|
|
27
26
|
const OATS_HOME_DIR = () => process.env.OATS_HOME_DIR || join(homedir(), ".oats");
|
|
28
27
|
export const SERVERS_FILE = () => join(OATS_HOME_DIR(), "servers.json");
|
|
@@ -234,7 +233,7 @@ export function checkRemote(target, io = {}) {
|
|
|
234
233
|
launchOptions: list("launchOptions", []),
|
|
235
234
|
remote: list("remote", []),
|
|
236
235
|
features: list("features", []),
|
|
237
|
-
operationsApi: probe.operationsApi
|
|
236
|
+
operationsApi: [1, 2].includes(probe.operationsApi) ? probe.operationsApi : null,
|
|
238
237
|
scheduleApi: [1, 2].includes(probe.scheduleApi) ? probe.scheduleApi : null,
|
|
239
238
|
advertised: Array.isArray(probe.runtimes),
|
|
240
239
|
};
|
|
@@ -407,8 +406,7 @@ export function routeCommand(serverId, cmd, oatsArgs, io = {}) {
|
|
|
407
406
|
if (explicitName && readSnapshot(serverId, explicitName)) throw serverError("E_ROUTE_EXISTS", `a saved route for ${explicitName} through server ${serverId} already exists (${readSnapshot(serverId, explicitName).home}); retire it (oats retire ${explicitName} --server ${serverId}) or drop it (oats server forget ${serverId} --instance ${explicitName}) before spawning that name again`);
|
|
408
407
|
// The remote roster: the soul's runtime default for the support check,
|
|
409
408
|
// and the remote agents root for the snapshot (the kernel's spawn result
|
|
410
|
-
// does not carry it, and guessing it from the workspace would be wrong
|
|
411
|
-
// for local souls).
|
|
409
|
+
// does not carry it, and guessing it from the workspace would be wrong).
|
|
412
410
|
const status = runRemote(target, json(withScope(["status"])), io).envelope;
|
|
413
411
|
if (!status.ok) return { envelope: status, stderr: "" };
|
|
414
412
|
checkRemoteSupport(remote, target, oatsArgs, status.result);
|
|
@@ -486,7 +484,7 @@ export function routeCommand(serverId, cmd, oatsArgs, io = {}) {
|
|
|
486
484
|
return { envelope: envelope.ok ? { ...envelope, result: { ...envelope.result, server: serverId, target, snapshots: listSnapshots(serverId) } } : envelope, stderr };
|
|
487
485
|
}
|
|
488
486
|
if (OPERATIONS_COMMANDS.has(cmd)) {
|
|
489
|
-
// The operations contract (inspect, operation run
|
|
487
|
+
// The operations contract (inspect, operation run): the
|
|
490
488
|
// remote must advertise it before anything is sent. An explicit --dir is
|
|
491
489
|
// the exact member context the caller chose and travels as is; without
|
|
492
490
|
// one, a --home selection is left to the host (the home is its own
|
|
@@ -505,14 +503,14 @@ export function routeCommand(serverId, cmd, oatsArgs, io = {}) {
|
|
|
505
503
|
if (ii >= 0) { args.splice(ii, 2); if (valueOf("--home") === undefined) args.push("--home", route.home); }
|
|
506
504
|
}
|
|
507
505
|
const remote = checkRemote(target, io);
|
|
508
|
-
if (!Array.isArray(remote.features) || !remote.features.includes("operations") || remote.operationsApi
|
|
506
|
+
if (!Array.isArray(remote.features) || !remote.features.includes("operations") || ![1, 2].includes(remote.operationsApi)) {
|
|
509
507
|
throw serverError("E_REMOTE_INCOMPATIBLE", `remote oats ${remote.version} at ${target.sshHost} does not advertise the operations contract (kernels from ${OPERATIONS_REMOTE_VERSION} do); upgrade it there; nothing was sent`);
|
|
510
508
|
}
|
|
511
509
|
const scoped = args.includes("--dir") || args.includes("--home") ? args : [...args, "--dir", target.workspace];
|
|
512
510
|
const { envelope, stderr } = runRemote(target, json([cmd, ...scoped]), io);
|
|
513
511
|
return { envelope: envelope.result && typeof envelope.result === "object" ? { ...envelope, result: { ...envelope.result, server: serverId, ...(route ? { route: { home: route.home, instance: route.snapshot?.instance || null, frozen: !!route.snapshot } } : {}) } } : envelope, stderr };
|
|
514
512
|
}
|
|
515
|
-
throw serverError("E_USAGE", `--server routes spawn, retire, status, session, okf harvest, schedule, inspect
|
|
513
|
+
throw serverError("E_USAGE", `--server routes spawn, retire, status, session, okf harvest, schedule, inspect and operation only (not ${cmd})`);
|
|
516
514
|
}
|
|
517
515
|
|
|
518
516
|
// ------------------------------------------------------------------- roster
|
|
@@ -656,7 +654,7 @@ export function inspectRemote(serverId, { instance, home } = {}, io = {}) {
|
|
|
656
654
|
export const SESSION_START_REMOTE_VERSION = "0.22.9";
|
|
657
655
|
/** The kernel version whose probe first advertises the operations contract. */
|
|
658
656
|
export const OPERATIONS_REMOTE_VERSION = "0.22.16";
|
|
659
|
-
const OPERATIONS_COMMANDS = new Set(["inspect", "operation"
|
|
657
|
+
const OPERATIONS_COMMANDS = new Set(["inspect", "operation"]);
|
|
660
658
|
|
|
661
659
|
/** `session start` on the execution host for a remote instance: the same
|
|
662
660
|
* route resolution as inspect, refused before any mutation when the remote
|
|
@@ -755,6 +753,8 @@ export function launchConfigRemote(serverId, options = {}, io = {}) {
|
|
|
755
753
|
return { envelope, stderr, target, ...(route ? { route } : {}) };
|
|
756
754
|
}
|
|
757
755
|
|
|
756
|
+
/** The definition keys of a captured (versioned) schedule, removed in 0.26 (as lib/schedule.mjs refuses them). */
|
|
757
|
+
const CAPTURED_SCHEDULE_KEYS = ["definitionVersion", "recurrencePolicy", "execution", "preparation"];
|
|
758
758
|
/** `oats schedule ...` on the execution host: schedules are host-owned, so
|
|
759
759
|
* every subcommand runs in the server's registered workspace; refused
|
|
760
760
|
* before any remote mutation when the remote kernel does not advertise
|
|
@@ -767,14 +767,14 @@ export function scheduleRemote(serverId, oatsArgs, io = {}) {
|
|
|
767
767
|
if (["add", "update"].includes(oatsArgs[0])) {
|
|
768
768
|
const indexes = oatsArgs.flatMap((arg, index) => arg === "--spec-json" ? [index] : []);
|
|
769
769
|
if (indexes.length > 1) throw serverError("E_BAD_ARGS", "remote schedule spec must be unambiguous");
|
|
770
|
-
let versioned = oatsArgs.includes("--file"); // Cannot classify remote file bytes here.
|
|
771
770
|
if (indexes.length) {
|
|
772
771
|
const spec = parseStrictJson(oatsArgs[indexes[0] + 1]);
|
|
773
772
|
if (!spec || typeof spec !== "object" || Array.isArray(spec)) throw serverError("E_BAD_ARGS", "remote schedule spec must be an object");
|
|
774
|
-
versioned
|
|
775
|
-
|
|
773
|
+
// Captured (versioned) schedules were removed in 0.26: refused here, as a local add
|
|
774
|
+
// refuses them, never forwarded to a remote kernel that might still accept one.
|
|
775
|
+
const captured = CAPTURED_SCHEDULE_KEYS.filter((key) => Object.hasOwn(spec, key));
|
|
776
|
+
if (captured.length) throw serverError("E_SCHEDULE_INVALID", `${captured.join(", ")}: captured schedules are refused (the captured/portable path was removed in 0.26); no request was forwarded`);
|
|
776
777
|
}
|
|
777
|
-
if (versioned && remote.scheduleApi !== 2) throw serverError("E_REMOTE_INCOMPATIBLE", "captured schedule mutation requires advertised scheduleApi 2; no request was forwarded");
|
|
778
778
|
}
|
|
779
779
|
const args = ["schedule", ...oatsArgs.filter((a) => a !== "--json"), "--dir", target.workspace, "--json"];
|
|
780
780
|
const { envelope, stderr } = runRemote(target, args, io);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
/** Small shape checks
|
|
2
|
-
* decoded or canonical-data-validated values; these
|
|
1
|
+
/** Small shape checks for decoded documents (capability manifests' binding
|
|
2
|
+
* declarations). Call only on decoded or canonical-data-validated values; these
|
|
3
|
+
* helpers do not resolve policy. */
|
|
3
4
|
import { oatsError } from "./errors.mjs";
|
|
4
|
-
import { scalarString } from "./
|
|
5
|
+
import { scalarString } from "./canonical-json.mjs";
|
|
5
6
|
|
|
6
7
|
export const pointerKey = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
7
8
|
export function invalidShape(pointer, message, code = "invalid-declaration") {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Tree mechanics only: the kernel's catchable recursive copy (spawn, retire and
|
|
2
|
+
* work recovery). No acquisition, selection, trust, lock or lifecycle policy. */
|
|
3
|
+
import {
|
|
4
|
+
chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, readlinkSync, symlinkSync,
|
|
5
|
+
} from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { oatsError } from "./errors.mjs";
|
|
8
|
+
|
|
9
|
+
/** Recursively copy a tree the way `cpSync(..., { recursive: true })` would —
|
|
10
|
+
* except catchably.
|
|
11
|
+
*
|
|
12
|
+
* Node 22's recursive `cpSync` performs its recursion in native code, and on
|
|
13
|
+
* macOS an unreadable directory inside the tree surfaces as an uncaught libc++
|
|
14
|
+
* `filesystem_error` that TERMINATES THE PROCESS. No JS `catch` or `finally`
|
|
15
|
+
* runs, so a transaction using it can never clean up staging or roll back the
|
|
16
|
+
* store, the lock and the ignore file. Every package-, capability- and
|
|
17
|
+
* user-shaped tree in the engine therefore goes through this hand-walk instead,
|
|
18
|
+
* where an EACCES is an ordinary throwable error.
|
|
19
|
+
*
|
|
20
|
+
* Semantics chosen to be safe rather than maximally faithful:
|
|
21
|
+
* - deterministic traversal (sorted entries), so two copies of one tree hash
|
|
22
|
+
* identically;
|
|
23
|
+
* - symlinks are recreated VERBATIM — never followed, never rewritten — because
|
|
24
|
+
* the bytes about to be hashed must be the bytes the author wrote;
|
|
25
|
+
* - FIFOs, sockets and device nodes are rejected fail-closed: they are not
|
|
26
|
+
* distributable content, and copying them has no defined meaning here;
|
|
27
|
+
* - directory modes are applied AFTER their children, so a read-only source
|
|
28
|
+
* directory cannot block writing its own contents. */
|
|
29
|
+
export function copyTreeSafe(src, dest) {
|
|
30
|
+
const st = lstatSync(src);
|
|
31
|
+
if (st.isSymbolicLink()) { symlinkSync(readlinkSync(src), dest); return; }
|
|
32
|
+
if (st.isFile()) { copyFileSync(src, dest); chmodSync(dest, st.mode & 0o7777); return; }
|
|
33
|
+
if (!st.isDirectory()) {
|
|
34
|
+
throw oatsError("invalid-source", `${src} is not a regular file, directory or symlink (${st.isFIFO() ? "FIFO" : st.isSocket() ? "socket" : st.isBlockDevice() || st.isCharacterDevice() ? "device node" : "unsupported file type"}) — package and capability trees carry distributable content only`);
|
|
35
|
+
}
|
|
36
|
+
mkdirSync(dest, { recursive: true });
|
|
37
|
+
for (const e of readdirSync(src, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
38
|
+
copyTreeSafe(join(src, e.name), join(dest, e.name));
|
|
39
|
+
}
|
|
40
|
+
chmodSync(dest, st.mode & 0o7777);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
package/lib/workspace.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { parseConfigData } from "./config-data.mjs";
|
|
|
16
16
|
import { oatsError } from "./errors.mjs";
|
|
17
17
|
import * as defaultRemote from "./remote.mjs";
|
|
18
18
|
import { bindRemote, classifyPackageValue } from "./packages.mjs";
|
|
19
|
+
import { manifestContractProblems } from "./capability-contract.mjs";
|
|
19
20
|
|
|
20
21
|
/* ───────────────────────────── errors ─────────────────────────────────── */
|
|
21
22
|
|
|
@@ -335,14 +336,24 @@ function schemaError(kind, origin, problems, value) {
|
|
|
335
336
|
|
|
336
337
|
/* ───────────────────────────── public API ─────────────────────────────── */
|
|
337
338
|
|
|
338
|
-
/**
|
|
339
|
+
/** What a 0.25 `oats-config.yaml` held, and where each piece lives now (0.26.0 removed the file). */
|
|
340
|
+
export const LEGACY_CONFIG_MIGRATION = "oats-config.yaml is no longer read (removed in 0.26.0): launch-configs moved to the deployment's oats-local.yaml; "
|
|
341
|
+
+ "capability bindings, layers and team moved to oats-workspace.yaml (defaults, teams) and each soul's soul.yaml in its member repository; "
|
|
342
|
+
+ "agents-md-injection, skill-overrides, work-modes and yolo are removed (yolo is --yolo or a launch configuration's yolo). Move what you still need, then delete the file";
|
|
343
|
+
|
|
344
|
+
/** Walk up from dir to find oats-local.yaml → { path, local } | E_LOCAL_MISSING.
|
|
345
|
+
* A 0.25 `oats-config.yaml` between `dir` and the deployment (inclusive) is refused
|
|
346
|
+
* (E_CONFIG_BROKEN, naming the migration): nothing reads it, so it must not look
|
|
347
|
+
* like configuration. Above the deployment is not this deployment's business. */
|
|
339
348
|
export function loadLocal(dir) {
|
|
340
349
|
let current = resolve(dir);
|
|
341
|
-
const visited = [];
|
|
350
|
+
const visited = [], legacy = [];
|
|
342
351
|
for (;;) {
|
|
343
352
|
const candidate = join(current, "oats-local.yaml");
|
|
344
353
|
visited.push(candidate);
|
|
354
|
+
if (existsSync(join(current, "oats-config.yaml"))) legacy.push(join(current, "oats-config.yaml"));
|
|
345
355
|
if (existsSync(candidate)) {
|
|
356
|
+
if (legacy.length) throw fail("E_CONFIG_BROKEN", `${legacy.join(", ")}: ${LEGACY_CONFIG_MIGRATION}`, { dir: resolve(dir), deployment: current, files: legacy, reason: "legacy-config" });
|
|
346
357
|
const origin = { kind: "local", path: candidate };
|
|
347
358
|
const read = readDeclaration("local", readFileSync(candidate), origin);
|
|
348
359
|
if (read.problems) throw schemaError("local", origin, read.problems, read.value);
|
|
@@ -352,7 +363,7 @@ export function loadLocal(dir) {
|
|
|
352
363
|
if (parent === current) break;
|
|
353
364
|
current = parent;
|
|
354
365
|
}
|
|
355
|
-
throw fail("E_LOCAL_MISSING", `no oats-local.yaml found walking up from ${resolve(dir)}`, { dir: resolve(dir), searched: visited });
|
|
366
|
+
throw fail("E_LOCAL_MISSING", `no oats-local.yaml found walking up from ${resolve(dir)}${legacy.length ? ` (${legacy[0]} is a 0.25 deployment's configuration, which 0.26.0 no longer reads: retire its instances with 0.25, then \`oats onboard\` it)` : ""}`, { dir: resolve(dir), searched: visited, ...(legacy.length ? { legacy } : {}) });
|
|
356
367
|
}
|
|
357
368
|
|
|
358
369
|
/** Observe the workspace host: → { key, url, commit, workspace, observedAt }. */
|
|
@@ -374,6 +385,47 @@ export async function observeWorkspace(ref, { at, remote: injected, remoteOption
|
|
|
374
385
|
return { key: obs.key, url: obs.url, ref, commit: obs.commit, workspace: read.value, observedAt: obs.observedAt };
|
|
375
386
|
}
|
|
376
387
|
|
|
388
|
+
/**
|
|
389
|
+
* A soul's team labels as its repository declares them NOW, without a workspace discovery (teams
|
|
390
|
+
* contract decision 6, the live teams of an existing home): the workspace host has already been read
|
|
391
|
+
* (`wsObs`, observeWorkspace); this reads the soul's own repo once — a member at its current commit
|
|
392
|
+
* (`souls/<name>/soul.yaml`, and `oats-membership.yaml` only when the soul declares no `team`), or an
|
|
393
|
+
* `external[]` soul at its pinned commit (its entry's `team` wins, as in discovery).
|
|
394
|
+
* → labels (primary first), or null when the workspace no longer lists the soul's repo.
|
|
395
|
+
* Membership is not re-confirmed here: the home's soul was confirmed at spawn; this reads labels only.
|
|
396
|
+
*/
|
|
397
|
+
export async function observeSoulLabels(wsObs, { name, repoKey }, { remote: injected, remoteOptions } = {}) {
|
|
398
|
+
const remote = remoteOf({ remote: injected, remoteOptions });
|
|
399
|
+
const workspace = wsObs.workspace;
|
|
400
|
+
const readSoul = async (ref, commit, path) => {
|
|
401
|
+
const file = `${path.replace(/\/+$/, "")}/soul.yaml`;
|
|
402
|
+
const { bytes } = await remote.readRemoteFile(ref, commit, file);
|
|
403
|
+
const read = readDeclaration("soul", bytes, { kind: "soul", repoKey, commit, path: file });
|
|
404
|
+
if (read.problems) throw schemaError("soul", { kind: "soul", repoKey, commit, path: file }, read.problems, read.value);
|
|
405
|
+
return read.value;
|
|
406
|
+
};
|
|
407
|
+
for (const entry of workspace.external || []) {
|
|
408
|
+
const pin = entry.source.lastIndexOf("@");
|
|
409
|
+
let key; try { key = refKey(remote, entry.source.slice(0, pin)); } catch { continue; }
|
|
410
|
+
if (key !== repoKey) continue;
|
|
411
|
+
if (typeof entry.team === "string") return [entry.team];
|
|
412
|
+
const soul = await readSoul(entry.source.slice(0, pin), entry.source.slice(pin + 1), entry.soul);
|
|
413
|
+
if (soul.name !== name) continue;
|
|
414
|
+
return labelList(soul.team) ?? [];
|
|
415
|
+
}
|
|
416
|
+
const memberRef = (workspace.members || []).find((ref) => { try { return refKey(remote, ref) === repoKey; } catch { return false; } });
|
|
417
|
+
if (!memberRef) return null;
|
|
418
|
+
const obs = await remote.observeRemote(memberRef);
|
|
419
|
+
const soul = await readSoul(memberRef, obs.commit, `souls/${name}`);
|
|
420
|
+
const own = labelList(soul.team);
|
|
421
|
+
if (own) return own;
|
|
422
|
+
try {
|
|
423
|
+
const { bytes } = await remote.readRemoteFile(memberRef, obs.commit, "oats-membership.yaml");
|
|
424
|
+
const read = readDeclaration("membership", bytes, { kind: "membership", repoKey, commit: obs.commit, path: "oats-membership.yaml" });
|
|
425
|
+
return read.problems ? [] : labelList(read.value.team) ?? [];
|
|
426
|
+
} catch (e) { if (e?.code === "E_REMOTE_PATH_MISSING") return []; throw e; }
|
|
427
|
+
}
|
|
428
|
+
|
|
377
429
|
const unconfirmed = (key, reason, detail, extra = {}) => ({ key, confirmed: false, reason, detail, ...extra });
|
|
378
430
|
|
|
379
431
|
/**
|
|
@@ -422,21 +474,28 @@ export async function confirmMembership(workspaceObs, memberRef, { remote: injec
|
|
|
422
474
|
const caseOnly = backlinkKey.toLowerCase() === workspaceObs.key.toLowerCase();
|
|
423
475
|
return unconfirmed(key, "backlink-elsewhere", `${key}@${obs.commit.slice(0, 12)} names workspace ${backlinkKey}, not ${workspaceObs.key}${caseOnly ? " (the keys differ only by letter case: repo paths are case-sensitive identities; spell the workspace ref exactly as the workspace lists itself)" : ""}`, { commit: obs.commit, backlink: backlinkKey, ...(caseOnly ? { caseOnly: true } : {}) });
|
|
424
476
|
}
|
|
425
|
-
|
|
477
|
+
const labels = labelList(read.value.team) ?? [];
|
|
478
|
+
return { key, commit: obs.commit, confirmed: true, team: labels[0] ?? null, labels };
|
|
426
479
|
}
|
|
427
480
|
|
|
428
481
|
/* ───────────────────────────── enumeration ────────────────────────────── */
|
|
429
482
|
|
|
430
483
|
const SOUL_FILE = /^([^/]+)\/soul\.yaml$/;
|
|
431
484
|
const CAP_FILE = /^([^/]+)\/oats\.json$/;
|
|
432
|
-
|
|
485
|
+
/** A declared `team` as a label list (teams contract 2026-09-25: a label or a non-empty list of distinct
|
|
486
|
+
* labels, the first the primary); null when the item declares none. */
|
|
487
|
+
const labelList = (team) => (typeof team === "string" ? [team] : Array.isArray(team) ? team.filter((l) => typeof l === "string") : null);
|
|
488
|
+
/** The item's labels, else the repo default's (oats-membership.yaml). */
|
|
489
|
+
const labelsOf = (item, fallback) => labelList(item?.team) ?? [...(fallback || [])];
|
|
490
|
+
/** Each label with the path it is declared at: `/team` for one label, `/team/<i>` in a list. */
|
|
491
|
+
const labelPaths = (item, labels, file) => labels.map((label, i) => [label, Array.isArray(item?.team) ? `${file}#/team/${i}` : `${file}#/team`]);
|
|
433
492
|
|
|
434
493
|
/**
|
|
435
494
|
* Enumerate souls/*\/soul.yaml and capabilities/*\/oats.json of one repo at one commit.
|
|
436
495
|
* Validates each item; collects problems instead of aborting.
|
|
437
496
|
* → { souls: [SoulEntry], capabilities: [CapEntry], problems: [{ code, path, message, repoKey }] }
|
|
438
497
|
*/
|
|
439
|
-
async function enumerateRepo(remote, ref, key, commit, {
|
|
498
|
+
async function enumerateRepo(remote, ref, key, commit, { defaultLabels = [], teams = null } = {}) {
|
|
440
499
|
const souls = [];
|
|
441
500
|
const capabilities = [];
|
|
442
501
|
const problems = [];
|
|
@@ -467,9 +526,10 @@ async function enumerateRepo(remote, ref, key, commit, { defaultTeam = null, tea
|
|
|
467
526
|
if (definition.name !== m[1]) problem("E_WORKSPACE_SCHEMA", `${file}#/name`, `soul name ${show(definition.name)} does not match its directory ${show(m[1])}`);
|
|
468
527
|
if (soulNames.has(definition.name)) { problem("E_WORKSPACE_SCHEMA", `${file}#/name`, `soul name ${show(definition.name)} is already declared by ${soulNames.get(definition.name)}; the second declaration is not listed`); continue; }
|
|
469
528
|
soulNames.set(definition.name, file);
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
529
|
+
const labels = labelsOf(definition, defaultLabels);
|
|
530
|
+
for (const [label, at] of labelPaths(definition, labels, file)) checkTeam(label, at);
|
|
531
|
+
// Souls have no private mode since 0.26.0: `private` is accepted, warned (soul-private-ignored) and never hides a soul.
|
|
532
|
+
souls.push({ name: definition.name, path, repoKey: key, commit, team: labels[0] ?? null, labels, private: false, definition });
|
|
473
533
|
}
|
|
474
534
|
const capNames = new Map();
|
|
475
535
|
for (const entry of await list("capabilities")) {
|
|
@@ -497,9 +557,13 @@ async function enumerateRepo(remote, ref, key, commit, { defaultTeam = null, tea
|
|
|
497
557
|
},
|
|
498
558
|
}, manifest);
|
|
499
559
|
if (shape.length) { for (const p of shape) problem("E_WORKSPACE_SCHEMA", `${file}#${p.path}`, p.message); continue; }
|
|
560
|
+
// The kernel contract (launch environment, hooks): a manifest the kernel could not run is not listed.
|
|
561
|
+
const contract = manifestContractProblems(manifest);
|
|
562
|
+
if (contract.length) { for (const p of contract) problem("E_WORKSPACE_SCHEMA", `${file}#${p.pointer}`, p.message); continue; }
|
|
500
563
|
if (capNames.has(manifest.capability)) { problem("E_WORKSPACE_SCHEMA", `${file}#/capability`, `capability ${show(manifest.capability)} is already declared by ${capNames.get(manifest.capability)}; the second declaration is not listed`); continue; }
|
|
501
564
|
capNames.set(manifest.capability, file);
|
|
502
|
-
|
|
565
|
+
// A capability is LISTED under one team (its own `team:`, else the repo default's primary); it joins none.
|
|
566
|
+
const team = typeof manifest.team === "string" ? manifest.team : defaultLabels[0] ?? null;
|
|
503
567
|
checkTeam(team, `${file}#/team`);
|
|
504
568
|
capabilities.push({ name: manifest.capability, path, repoKey: key, commit, team, private: manifest.private === true, manifest });
|
|
505
569
|
}
|
|
@@ -537,7 +601,7 @@ export async function discoverRepo(ref, { at, remote: injected, remoteOptions }
|
|
|
537
601
|
} catch (e) {
|
|
538
602
|
if (e?.code !== "E_REMOTE_PATH_MISSING") throw e;
|
|
539
603
|
}
|
|
540
|
-
const items = await enumerateRepo(remote, ref, obs.key, obs.commit, {
|
|
604
|
+
const items = await enumerateRepo(remote, ref, obs.key, obs.commit, { defaultLabels: labelList(membership?.team) ?? [], teams: null });
|
|
541
605
|
return { key: obs.key, commit: obs.commit, membership, ...items, problems: [...problems, ...items.problems] };
|
|
542
606
|
}
|
|
543
607
|
|
|
@@ -557,7 +621,7 @@ export async function discoverWorkspace(ref, { at, local, remote: injected, remo
|
|
|
557
621
|
const members = [];
|
|
558
622
|
for (const memberRef of workspace.members || []) {
|
|
559
623
|
const confirmation = await confirmMembership(wsObs, memberRef, { remote });
|
|
560
|
-
const row = { key: confirmation.key, commit: confirmation.commit ?? null, confirmed: confirmation.confirmed, team: confirmation.team ?? null, souls: [], capabilities: [], publishes: null };
|
|
624
|
+
const row = { key: confirmation.key, commit: confirmation.commit ?? null, confirmed: confirmation.confirmed, team: confirmation.team ?? null, labels: confirmation.labels ?? [], souls: [], capabilities: [], publishes: null };
|
|
561
625
|
if (!confirmation.confirmed) {
|
|
562
626
|
// Contract: an unconfirmed member contributes nothing but its row.
|
|
563
627
|
row.reason = confirmation.reason;
|
|
@@ -565,8 +629,10 @@ export async function discoverWorkspace(ref, { at, local, remote: injected, remo
|
|
|
565
629
|
members.push(row);
|
|
566
630
|
continue;
|
|
567
631
|
}
|
|
568
|
-
|
|
569
|
-
|
|
632
|
+
for (const [i, label] of row.labels.entries()) {
|
|
633
|
+
if (!teams.includes(label)) problems.push({ code: "E_TEAM_UNKNOWN", repoKey: row.key, path: row.labels.length > 1 ? `oats-membership.yaml#/team/${i}` : "oats-membership.yaml#/team", message: `team ${show(label)} is not declared in the workspace's teams (${teams.join(", ") || "none"})` });
|
|
634
|
+
}
|
|
635
|
+
const items = await enumerateRepo(remote, memberRef, row.key, row.commit, { defaultLabels: row.labels, teams });
|
|
570
636
|
row.souls = items.souls;
|
|
571
637
|
row.capabilities = items.capabilities;
|
|
572
638
|
row.publishes = items.publishes;
|
|
@@ -593,11 +659,50 @@ export async function discoverWorkspace(ref, { at, local, remote: injected, remo
|
|
|
593
659
|
}
|
|
594
660
|
const read = readDeclaration("soul", bytes, { kind: "soul", repoKey: key, commit, path: file });
|
|
595
661
|
if (read.problems) { for (const p of read.problems) pushProblem("E_WORKSPACE_SCHEMA", `${file}#${p.path}`, `${p.message}${schemaHint("soul", read.value)}`); continue; }
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
662
|
+
// The workspace's `external[].team` (one label) overrides the soul's own labels.
|
|
663
|
+
const labels = labelList(entry.team) ?? labelList(read.value.team) ?? [];
|
|
664
|
+
for (const label of labels) if (!teams.includes(label)) pushProblem("E_TEAM_UNKNOWN", `${file}#/team`, `team ${show(label)} is not declared in the workspace's teams`);
|
|
665
|
+
externalRows.push({ source: entry.source, key, commit, soul: { name: read.value.name, path: entry.soul, repoKey: key, commit, team: labels[0] ?? null, labels, private: false, definition: read.value } });
|
|
666
|
+
}
|
|
667
|
+
return { workspace, key: wsObs.key, url: wsObs.url, commit: wsObs.commit, observedAt: wsObs.observedAt, local: local ?? null, members, external: externalRows, problems, warnings: [...unmappedLabelWarnings(workspace, members, externalRows, teams), ...privateSoulWarnings([...members.filter((m) => m.confirmed).flatMap((m) => m.souls), ...externalRows.map((e) => e.soul)])] };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Teams contract decision 5: a soul label the workspace declares in `teams:` but does not map in
|
|
671
|
+
* `messaging.byTeam` is a WARNING, not a problem — the label stays eligible-but-unmapped and the
|
|
672
|
+
* messaging provider falls back to the personal team for it. (An undeclared label is E_TEAM_UNKNOWN.)
|
|
673
|
+
* ONE warning per unmapped label, naming its souls (a personal-only workspace would otherwise print
|
|
674
|
+
* a line per soul): { code, label, souls, paths, message }, sorted by label. */
|
|
675
|
+
const byCodepointOrder = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
676
|
+
function unmappedLabelWarnings(workspace, members, externalRows, teams) {
|
|
677
|
+
const byTeam = isObject(workspace?.messaging) && isObject(workspace.messaging.byTeam) ? workspace.messaging.byTeam : {};
|
|
678
|
+
const byLabel = new Map();
|
|
679
|
+
const souls = [...members.filter((m) => m.confirmed).flatMap((m) => m.souls), ...externalRows.map((e) => e.soul)];
|
|
680
|
+
for (const soul of souls) {
|
|
681
|
+
for (const label of soul.labels || []) {
|
|
682
|
+
if (!teams.includes(label) || Object.hasOwn(byTeam, label)) continue;
|
|
683
|
+
const row = byLabel.get(label) || { souls: [], paths: [] };
|
|
684
|
+
row.souls.push(soul.name); row.paths.push(`${soul.repoKey}:${soul.path}/soul.yaml#/team`);
|
|
685
|
+
byLabel.set(label, row);
|
|
686
|
+
}
|
|
599
687
|
}
|
|
600
|
-
return
|
|
688
|
+
return [...byLabel.keys()].sort(byCodepointOrder).map((label) => {
|
|
689
|
+
const { souls: names, paths } = byLabel.get(label);
|
|
690
|
+
const order = names.map((n, i) => i).sort((x, y) => byCodepointOrder(names[x], names[y]) || byCodepointOrder(paths[x], paths[y]));
|
|
691
|
+
const sorted = order.map((i) => names[i]);
|
|
692
|
+
return { code: "unmapped-team-label", label, souls: sorted, paths: order.map((i) => paths[i]),
|
|
693
|
+
message: `team ${show(label)} has no messaging.byTeam entry; its souls (${sorted.join(", ")}) fall back to the personal team for it` };
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** `private:` in a soul.yaml has no effect since 0.26.0 (human decision 2026-09-25): every soul of a
|
|
698
|
+
* confirmed member, and every external soul, is listed and spawnable. The field is still ACCEPTED
|
|
699
|
+
* (existing files keep validating) and named by ONE warning per soul that carries it:
|
|
700
|
+
* { code: "soul-private-ignored", soul, repoKey, path, message }, sorted by soul name, then path. */
|
|
701
|
+
function privateSoulWarnings(souls) {
|
|
702
|
+
return souls.filter((s) => s.definition && Object.hasOwn(s.definition, "private"))
|
|
703
|
+
.map((s) => ({ code: "soul-private-ignored", soul: s.name, repoKey: s.repoKey, path: `${s.repoKey}:${s.path}/soul.yaml#/private`,
|
|
704
|
+
message: `\`private\` has no effect on a soul since 0.26.0; remove it from ${s.path}/soul.yaml` }))
|
|
705
|
+
.sort((a, b) => byCodepointOrder(a.soul, b.soul) || byCodepointOrder(a.path, b.path));
|
|
601
706
|
}
|
|
602
707
|
|
|
603
708
|
/**
|
|
@@ -648,7 +753,7 @@ export function standaloneRepo(ref, commit, discovery, { remote: injected } = {}
|
|
|
648
753
|
});
|
|
649
754
|
return {
|
|
650
755
|
standalone: true, key, commit: source.commit ?? commit ?? null, workspace: null,
|
|
651
|
-
members: [{ key, commit: source.commit ?? commit ?? null, confirmed: false, reason: "cannot-read", detail: `workspace of ${key} cannot be read; standalone view`, team: source.team ?? source.membership?.team ?? null, souls, capabilities, publishes: source.publishes ?? null }],
|
|
652
|
-
external: [], problems,
|
|
756
|
+
members: [{ key, commit: source.commit ?? commit ?? null, confirmed: false, reason: "cannot-read", detail: `workspace of ${key} cannot be read; standalone view`, team: source.team ?? labelList(source.membership?.team)?.[0] ?? null, souls, capabilities, publishes: source.publishes ?? null }],
|
|
757
|
+
external: [], problems, warnings: privateSoulWarnings(souls),
|
|
653
758
|
};
|
|
654
759
|
}
|
package/package-catalog.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"policy": "docs/official-
|
|
2
|
+
"policy": "docs/official-catalog.md",
|
|
3
3
|
"packages": {
|
|
4
4
|
"oats.okf": {
|
|
5
5
|
"url": "https://github.com/awebai/oats-okf.git",
|
|
@@ -8,27 +8,27 @@
|
|
|
8
8
|
},
|
|
9
9
|
"oats.aweb": {
|
|
10
10
|
"url": "https://github.com/awebai/oats-aweb.git",
|
|
11
|
-
"ref": "v1.
|
|
11
|
+
"ref": "v1.13.1",
|
|
12
12
|
"path": "oats-package"
|
|
13
13
|
},
|
|
14
14
|
"oats.jira": {
|
|
15
15
|
"url": "https://github.com/awebai/oats-jira.git",
|
|
16
|
-
"ref": "v1.0.
|
|
16
|
+
"ref": "v1.0.1",
|
|
17
17
|
"path": "oats-package"
|
|
18
18
|
},
|
|
19
19
|
"oats.linear": {
|
|
20
20
|
"url": "https://github.com/awebai/oats-linear.git",
|
|
21
|
-
"ref": "v1.0.
|
|
21
|
+
"ref": "v1.0.1",
|
|
22
22
|
"path": "oats-package"
|
|
23
23
|
},
|
|
24
24
|
"oats.authoring": {
|
|
25
25
|
"url": "https://github.com/awebai/oats-authoring.git",
|
|
26
|
-
"ref": "v1.0.
|
|
26
|
+
"ref": "v1.0.3",
|
|
27
27
|
"path": "oats-package"
|
|
28
28
|
},
|
|
29
29
|
"oats.dev": {
|
|
30
30
|
"url": "https://github.com/awebai/oats-dev.git",
|
|
31
|
-
"ref": "v1.0.
|
|
31
|
+
"ref": "v1.0.1",
|
|
32
32
|
"path": "oats-package"
|
|
33
33
|
},
|
|
34
34
|
"oats.framework": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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",
|
|
@@ -3,7 +3,7 @@ name: integration-authoring
|
|
|
3
3
|
description: >-
|
|
4
4
|
Route custom OATS capability-package and integration work to the framework's
|
|
5
5
|
integrations expert. Use when building, adapting, or debugging a reusable
|
|
6
|
-
capability, new
|
|
6
|
+
capability, new tasks/messaging/knowledge core capability, oats.json manifest,
|
|
7
7
|
lifecycle hook, or operational command—not merely activating an existing
|
|
8
8
|
package. Triggers: "custom integration", "capability package", "integrate
|
|
9
9
|
our tracker", "new messaging integration", "write an oats.json".
|
|
@@ -12,65 +12,73 @@ description: >-
|
|
|
12
12
|
# Capability and integration authoring — delegate
|
|
13
13
|
|
|
14
14
|
A capability package may ship skills, instance instructions, requirements,
|
|
15
|
-
namespaced commands, and
|
|
16
|
-
|
|
15
|
+
namespaced commands, and declared hooks. A core capability is the constrained
|
|
16
|
+
kind that fills one of the knowledge, messaging or tasks positions (its
|
|
17
|
+
manifest's `layer` field names which). Building either requires
|
|
17
18
|
manifest, security, targeting-boundary, collision, and probe discipline; use
|
|
18
19
|
the framework's **integrations-expert** soul rather than improvising.
|
|
19
20
|
|
|
20
|
-
If the user only wants an existing package,
|
|
21
|
+
If the user only wants an existing package, declare it and give it to souls;
|
|
22
|
+
no build is needed:
|
|
21
23
|
|
|
22
|
-
```
|
|
23
|
-
oats
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
```yaml
|
|
25
|
+
# oats-workspace.yaml (host repository): declaring the package is the trust decision
|
|
26
|
+
packages:
|
|
27
|
+
vendor.review: git:github.com/vendor/review@v1.0.0
|
|
28
|
+
# a soul's soul.yaml, or the workspace defaults: a capability the package exports
|
|
29
|
+
# (a package may export several; the soul names each one it wants)
|
|
30
|
+
capabilities:
|
|
31
|
+
vendor.review: { from: package }
|
|
26
32
|
```
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
Then run `oats sync` (fetch, verify integrity, lock). The oats.setup
|
|
35
|
+
capability's **oats-package-pins** skill has the procedure.
|
|
36
|
+
|
|
37
|
+
## 1. Verify the expert is available
|
|
38
|
+
|
|
39
|
+
Run `oats souls` in the deployment and confirm it resolves the
|
|
40
|
+
`integrations-expert` soul (a member repository or package provides it). If it
|
|
41
|
+
is absent, ask the human which OATS deployment owns reusable package work;
|
|
42
|
+
never locate or import private kernel files.
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
`~/oats`; verify with `git -C <dir> remote get-url origin`. Avoid
|
|
32
|
-
pi-managed git clones because updates reset them. If absent, ask where to
|
|
33
|
-
clone `https://github.com/awebai/oats`.
|
|
44
|
+
## 2. Spawn the expert against the package's repository
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
The package lives in its own repository. Make that repository a member of the
|
|
47
|
+
workspace (or use the member that already holds it), then spawn the expert on
|
|
48
|
+
it:
|
|
36
49
|
|
|
37
50
|
```bash
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
repo: '<users-workspace-or-repo>',
|
|
45
|
-
work: 'checkout',
|
|
46
|
-
task: '<capability intent; layer if any; skills/instructions/commands/hooks; external tools; desired global/group/soul targets; distribution path>',
|
|
47
|
-
});
|
|
48
|
-
console.log('window:', r.tmux.window, '| attach:', r.attach);
|
|
49
|
-
})"
|
|
51
|
+
oats spawn integrations-expert --preview \
|
|
52
|
+
--purpose <package-slug> \
|
|
53
|
+
--repo <member clone of the package repository> \
|
|
54
|
+
--work worktree \
|
|
55
|
+
--task '<capability intent; layer if any; skills/instructions/commands/hooks; external tools; which souls should get it; distribution path>'
|
|
56
|
+
# review the preview, then run the same command without --preview
|
|
50
57
|
```
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
Use `--relation child --relative-to <your-instance>` only when the documented
|
|
60
|
+
workflow makes the expert your child; otherwise leave the spawn unrelated. A
|
|
61
|
+
package is distributed from its own repository as `oats-package/` with a
|
|
62
|
+
version tag; a framework contribution belongs in the framework's repository.
|
|
56
63
|
|
|
57
64
|
## 3. Brief the design boundary
|
|
58
65
|
|
|
59
66
|
Tell the expert:
|
|
60
67
|
|
|
61
68
|
- whether it is additive or implements exactly one of knowledge/messaging/tasks;
|
|
62
|
-
- external requirements and executable surfaces;
|
|
69
|
+
- external requirements and executable surfaces (commands, hooks);
|
|
63
70
|
- intended distribution and version/compatibility;
|
|
64
|
-
-
|
|
65
|
-
- expected skill/instruction
|
|
71
|
+
- which souls or workspace defaults should receive it, and its settings; and
|
|
72
|
+
- expected skill/instruction collisions (a duplicate skill name fails the spawn).
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
Which souls get a capability is declared by the workspace (`defaults`) and the
|
|
75
|
+
souls (`soul.yaml` `capabilities`), never in the manifest. The expert must
|
|
76
|
+
test exact pi/Claude/Codex instance materialization, generated instructions,
|
|
77
|
+
command gating, deterministic hooks, and the lock's integrity check as
|
|
78
|
+
applicable.
|
|
70
79
|
|
|
71
80
|
## 4. Hand off
|
|
72
81
|
|
|
73
|
-
Report the
|
|
74
|
-
package/integration craft, runs a
|
|
75
|
-
and
|
|
76
|
-
its soul.
|
|
82
|
+
Report the new instance (`oats status`). The expert follows its
|
|
83
|
+
package/integration craft, runs a preview-only probe, and leaves the
|
|
84
|
+
`packages:` pin and the `oats sync` for the user.
|