@awebai/oats 0.24.1 → 0.24.3

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.
@@ -37,24 +37,34 @@ export function prepareProviderBindings({seed,plan,manifests,declarations},invok
37
37
  const normalized=new Map(),problems=[],requirements=[...plan.requirements],candidates=[...plan.candidates];
38
38
  const settingsFor=id=>Object.fromEntries(Object.entries(plan.settings[id] ?? {}).map(([name,key])=>[name,plan.choices[key].value]));
39
39
  const run=(id,phase,input)=>invoke({artifacts:seed.artifacts,capability:id,phase,settings:settingsFor(id),input});
40
- const refuse=error=>({code:safeCodes.has(error?.code)?error.code:'provider-not-qualified',message:'provider binding could not be prepared',origins:[]});
40
+ const originsFor=id=>[...new Map((plan.capabilities[id]?.choiceKeys ?? []).flatMap(key=>{
41
+ const choice=plan.choices[key];return choice?.selectedBy?[choice.selectedBy]:[];
42
+ }).map(origin=>[canonicalJson(origin),origin])).values()];
43
+ const problem=(slot,id,code,message,origins=originsFor(id))=>({code,message,origins,slot,capability:id});
44
+ const refuse=(slot,id,phase,error)=>problem(slot,id,safeCodes.has(error?.code)?error.code:'provider-not-qualified',
45
+ `${id} ${slot} ${phase} binding could not be prepared`); // Never provider free text.
41
46
  for (const [id,manifest] of manifests) {
42
47
  if (!Object.hasOwn(seed.artifacts.capabilities,id) || !manifest.layer) continue;
43
- if (plan.providers[manifest.layer] !== id) problems.push({code:'needs-configuration',message:'fundamental capability must be selected in its own layer',origins:[]});
48
+ if (plan.providers[manifest.layer] !== id) problems.push(problem(manifest.layer,id,'needs-configuration','fundamental capability must be selected in its own layer'));
44
49
  }
45
- if (problems.length) return {plan,seed,problems};
46
50
  for (const [slot,id] of Object.entries(plan.providers)) {
47
51
  if (id === null) continue;
48
52
  const manifest=manifests.get(id);
49
- if (manifest?.layer !== slot || !manifest.binding) { problems.push({code:'provider-not-qualified',message:'selected provider has no matching binding interface',origins:[]});continue; }
53
+ if (manifest?.layer !== slot) { problems.push(problem(slot,id,'provider-not-qualified',`${id} does not declare the selected ${slot} layer`));continue; }
54
+ if (!manifest.binding) { problems.push(problem(slot,id,'provider-not-qualified',`${id}@${manifest.version} declares no binding interface; ${slot} cannot be prepared`));continue; }
50
55
  try {
51
56
  const value=run(id,'normalize',{declarations,context:seed.context});
52
57
  normalized.set(slot,{id,value}); requirements.push(...value.requirements);candidates.push(...value.candidates);
53
- } catch(error) { problems.push(refuse(error)); }
58
+ } catch(error) { problems.push(refuse(slot,id,'normalize',error)); }
54
59
  }
55
- if (problems.length) return {plan,seed,problems};
60
+ // One unsupported slot must not mask another slot's normalized requirements
61
+ // or invalid choices. The same resolver still decides every value.
56
62
  const resolved=resolveChoices({requirements,candidates});
57
- if (resolved.status !== 'resolved') return {plan:{...plan,...resolved},seed,problems:resolved.problems};
63
+ for (const item of resolved.problems) {
64
+ const slot=Object.keys(plan.providers).find(slot=>bindingField(item.key,slot));
65
+ if (!slot) throw oatsError('invalid-binding-output','provider choice problem has no owned binding slot');
66
+ problems.push({...item,slot,capability:plan.providers[slot]});
67
+ }
58
68
  const nextPlan={...plan,...resolved,requirements,candidates},bindings=Object.create(null);
59
69
  let messagingChoice={schemaVersion:1,enabled:false};
60
70
  const known=[];
@@ -64,6 +74,7 @@ export function prepareProviderBindings({seed,plan,manifests,declarations},invok
64
74
  }
65
75
  const witnessed=origin=>bindingOriginWitnessed(origin,declarations) || known.some(candidate=>same(candidate,origin));
66
76
  for (const [slot,{id,value}] of normalized) {
77
+ if (resolved.problems.some(item=>bindingField(item.key,slot))) continue;
67
78
  try {
68
79
  const choices=Object.fromEntries(Object.entries(resolved.choices).filter(([key])=>bindingField(key,slot)));
69
80
  const bound=run(id,'bind',{model:value.model,choices,context:seed.context});
@@ -72,7 +83,9 @@ export function prepareProviderBindings({seed,plan,manifests,declarations},invok
72
83
  }
73
84
  bindings[slot]=bound.binding;
74
85
  if (slot === 'messaging') messagingChoice=bound.messagingChoice;
75
- } catch(error) { problems.push(refuse(error)); }
86
+ } catch(error) { problems.push(refuse(slot,id,'bind',error)); }
76
87
  }
88
+ nextPlan.problems=problems;
89
+ nextPlan.status=problems.some(item=>item.code==='requirement-conflict')?'conflict':problems.length?'needs-configuration':'resolved';
77
90
  return {plan:nextPlan,seed:{...seed,choices:resolved.choices,bindings,messagingChoice},problems};
78
91
  }
@@ -1,6 +1,8 @@
1
1
  /** Execute only an approved retained provider command. Preparation does not need
2
2
  * a fabricated complete resolution merely to obtain its binding. */
3
3
  import { spawnSync } from 'node:child_process';
4
+ import { realpathSync } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
4
6
  import { canonicalJson } from './portable-values.mjs';
5
7
  import { objectAt } from './portable-shape.mjs';
6
8
  import { validateArtifactSet } from './resolution-shape.mjs';
@@ -12,6 +14,9 @@ import { validateBindingInterface } from './provider-binding.mjs';
12
14
  import { BINDING_LIMITS, validateBindingRequest, decodeBindingResponse } from './provider-binding-wire.mjs';
13
15
  import { oatsError } from './errors.mjs';
14
16
 
17
+ // Same kernel-owned CLI locator as lifecycle hooks; never caller env or PATH.
18
+ const CLI_BIN=fileURLToPath(new URL('../bin/oats.mjs',import.meta.url));
19
+
15
20
  export function invokeProviderBinding(options,codecs) {
16
21
  canonicalJson(options);
17
22
  objectAt(options,['deployment','artifacts','capability','phase','settings','input','timeoutMs'],['deployment','artifacts','capability','phase','settings','input']);
@@ -46,7 +51,7 @@ export function invokeProviderBinding(options,codecs) {
46
51
  const [script,...args]=spec.trim().split(/\s+/),file=codecs.executable(manifest,script);
47
52
  if (!file) throw oatsError('resource-not-found','provider codec executable is unavailable');
48
53
  const environment=Object.fromEntries(Object.entries(process.env).filter(([key])=>! /^(OATS_|OAS_|PI_)/.test(key)));
49
- Object.assign(environment,{OATS_CAPABILITY:capability,OATS_CAPABILITY_ROOT:root,OATS_SETTINGS:canonicalJson(settings,BINDING_LIMITS)});
54
+ Object.assign(environment,{OATS_CAPABILITY:capability,OATS_CAPABILITY_ROOT:root,OATS_SETTINGS:canonicalJson(settings,BINDING_LIMITS),OATS_CLI_BIN:realpathSync(CLI_BIN)});
50
55
  let result;
51
56
  try {
52
57
  result=spawnSync(process.execPath,[file,...args],{cwd:root,env:environment,input:canonicalJson(request,BINDING_LIMITS),
@@ -0,0 +1,76 @@
1
+ /** Read the setup edition as data for a CLASSIC local bootstrap copy.
2
+ * No provider code, workspace activation, enrollment or captured identity. */
3
+ import { lstatSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { parsePortableSoul } from './portable-soul.mjs';
8
+ import { parseRepositorySource, parsePortableSource } from './source-spec.mjs';
9
+ import { createRepositoryTransaction } from './repository-observation.mjs';
10
+ import { createWorkspaceDiscovery } from './workspace-discovery.mjs';
11
+ import { packageIntegrity } from './core.mjs';
12
+ import { oatsError } from './errors.mjs';
13
+
14
+ export const SETUP_EXPERT = 'oats-setup-expert';
15
+ export const SETUP_CAPABILITIES = Object.freeze(['oats.core', 'oats.setup']);
16
+ const EXPORT = `souls/${SETUP_EXPERT}`;
17
+ function validateEdition(bytes) {
18
+ const { declaration: soul } = parsePortableSoul(bytes);
19
+ const required = soul.requires || {}, caps = required.capabilities || {};
20
+ if (soul.name !== SETUP_EXPERT || soul.work !== 'directory'
21
+ || Object.keys(required).some(key => key !== 'capabilities')
22
+ || Object.keys(caps).length !== 2 || SETUP_CAPABILITIES.some(id => caps[id]?.source !== 'repo:oats-package' || Object.keys(caps[id]).some(key => key !== 'source'))
23
+ || ['knowledge', 'messaging', 'tasks'].some(slot => soul.defaults?.[slot] !== 'none')
24
+ || Object.keys(soul.defaults || {}).some(key => !['knowledge', 'messaging', 'tasks'].includes(key))
25
+ || soul.knowledge || soul.teams?.length || soul.resources?.length || soul.yolo === true || soul.backend || soul['launch-config']) {
26
+ throw oatsError('needs-configuration', 'classic setup bootstrap needs the provider-independent directory edition with only oats.core/oats.setup; use explicit portable preparation for other requirements');
27
+ }
28
+ return soul;
29
+ }
30
+ function repositoryRequest(source) {
31
+ if (typeof source !== 'string' || source.includes('#')) throw oatsError('invalid-source', '--workspace needs a Git repository source, optionally @revision, without a package fragment');
32
+ try { return { source: parseRepositorySource(source).normalized }; }
33
+ catch {
34
+ const parsed = parsePortableSource(source);
35
+ if (parsed.kind !== 'git') throw oatsError('invalid-source', '--workspace needs an explicit Git repository source');
36
+ return { source: `git:${parsed.url}`, revision: parsed.selector };
37
+ }
38
+ }
39
+
40
+ export function loadSetupExpertEdition(workspace, repositoryOptions = {}) {
41
+ if (workspace === undefined) {
42
+ const root = fileURLToPath(new URL(`../${EXPORT}/`, import.meta.url));
43
+ return { declaration: validateEdition(readFileSync(join(root, 'soul.yaml'))), instructions: readFileSync(join(root, 'AGENTS.md'), 'utf8'),
44
+ source: { kind: 'packaged-definition', captured: false }, packageIntegrity: null };
45
+ }
46
+ const request = repositoryRequest(workspace), scratch = realpathSync(mkdtempSync(join(tmpdir(), 'oats-setup-source-'))), owned = lstatSync(scratch);
47
+ let transaction;
48
+ try {
49
+ transaction = createRepositoryTransaction({ ...repositoryOptions, directory: scratch, accessContextKey: 'explicit-setup-source' });
50
+ const discovery = createWorkspaceDiscovery(transaction), origin = { kind: 'operator', document: { kind: 'operator', id: 'oats-onboard' }, pointer: '/workspace' };
51
+ const observed = transaction.observe(request.source, { ...request, origin });
52
+ const workspaceDoc = transaction.readFile(observed, 'oats-workspace.yaml', { optional: true });
53
+ const view = workspaceDoc ? discovery.readWorkspace({ ...request, origin }) : null;
54
+ const reference = view?.parsed.imports.find(item => item.alias === SETUP_EXPERT)
55
+ ?? { source: request.source, revision: observed.source.commit, soul: EXPORT, alias: SETUP_EXPERT };
56
+ // A selected workspace import never falls back if its exact source fails.
57
+ const imported = discovery.importSoul(reference, { origin });
58
+ if (reference.adoption) throw oatsError('needs-configuration', 'classic setup copies an edition only; provider adoption values require the portable preparation path');
59
+ const declaration = validateEdition(transaction.readFile(imported.observation, imported.definition).bytes);
60
+ const projection = join(scratch, 'edition');
61
+ transaction.materialize(imported.observation, imported.roots, projection);
62
+ const soulRoot = join(projection, dirname(imported.definition)), body = join(soulRoot, 'AGENTS.md'), alias = join(soulRoot, 'CLAUDE.md');
63
+ if (!lstatSync(body).isFile() || !lstatSync(alias).isSymbolicLink() || readlinkSync(alias) !== 'AGENTS.md'
64
+ || readdirSync(soulRoot).some(name => !['soul.yaml', 'AGENTS.md', 'CLAUDE.md'].includes(name))) {
65
+ throw oatsError('source-incomplete', 'classic setup edition must have canonical AGENTS.md/CLAUDE.md and no omitted private skill or knowledge trees');
66
+ }
67
+ return { declaration, instructions: readFileSync(body, 'utf8'), packageIntegrity: packageIntegrity(join(projection, 'oats-package')),
68
+ source: { kind: 'exported-edition-copy', source: imported.reference.source, revision: imported.observation.source.commit,
69
+ path: imported.reference.soul, workspaceRevision: view?.source.commit ?? null, captured: false } };
70
+ } finally {
71
+ transaction?.close();
72
+ const current = lstatSync(scratch);
73
+ if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError('source-unavailable', 'setup source scratch ownership changed');
74
+ rmSync(scratch, { recursive: true });
75
+ }
76
+ }
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "oats.aweb": {
10
10
  "url": "https://github.com/awebai/oats-aweb.git",
11
- "ref": "v1.10.3",
11
+ "ref": "v1.11.0",
12
12
  "path": "oats-package"
13
13
  },
14
14
  "oats.jira": {
@@ -31,9 +31,9 @@
31
31
  "ref": "v1.0.0",
32
32
  "path": "oats-package"
33
33
  },
34
- "oats.knowledge-theory": {
34
+ "oats.framework": {
35
35
  "url": "https://github.com/awebai/oats.git",
36
- "ref": "v0.23.0",
36
+ "ref": "oats-framework/v1.1.1",
37
37
  "path": "oats-package"
38
38
  }
39
39
  },
@@ -66,6 +66,9 @@
66
66
  "oas.review": {
67
67
  "package": "oats.dev",
68
68
  "capability": "oats.review"
69
- }
69
+ },
70
+ "oats.knowledge-theory": "oats.framework",
71
+ "oats.core": "oats.framework",
72
+ "oats.setup": "oats.framework"
70
73
  }
71
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.24.1",
3
+ "version": "0.24.3",
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",
@@ -36,6 +36,8 @@
36
36
  "docs/",
37
37
  "README.md",
38
38
  "package-catalog.json",
39
+ "souls/oats-setup-expert/soul.yaml",
40
+ "souls/oats-setup-expert/AGENTS.md",
39
41
  "packages/record/bin/",
40
42
  "packages/record/lib/",
41
43
  "packages/record/docs/",
@@ -0,0 +1,60 @@
1
+ # OATS Setup Expert
2
+
3
+ Help an operator turn an empty deployment into a deliberately configured OATS
4
+ workspace. Explain the next small decision, inspect the existing state, obtain
5
+ approval for effects, and verify the result before moving on. Do not replace
6
+ working deployments or turn setup into an implicit enrollment operation.
7
+
8
+ ## Your supplied procedures
9
+
10
+ - Load **oats-workspace-setup** for workspace/source discovery and adoption:
11
+ declare, inspect, prepare, approve, scaffold and start are different steps.
12
+ - Load **oats-config** for version-scoped classic configuration and targeting;
13
+ never use its cascade to fill a missing captured input.
14
+ - Load **oats-packages** for official package discovery, acquisition, exact locks,
15
+ executable approval and updates.
16
+ - Load **oats-operate** for lifecycle, directory boundaries and supported CLI
17
+ operations; load **oats-souls** for source editions, roster and relations.
18
+
19
+ Use the procedures actually included in your composition. Do not fetch a current
20
+ skill or invent a command when an older installed version lacks a feature.
21
+
22
+ ## Setup sequence
23
+
24
+ 1. Establish the operator's intended deployment, work target and workspace/source
25
+ separately. Inspect existing configuration, locks and souls before proposing
26
+ changes. A workspace is a shared definition, not a shared live runtime.
27
+ 2. Explain `oats-workspace.yaml` and each member's separate `oats.yaml` exports
28
+ and backlink. Check reciprocal observations; discovery is neither membership
29
+ enrollment nor capability activation. Pin imports only after a source is
30
+ published at a real reviewed revision; never invent a future commit or tag.
31
+ 3. Select capabilities and their exact sources with the operator. New souls
32
+ declare removable `oats.core` explicitly. Do not add knowledge, messaging or
33
+ tasks merely because the package was discovered or acquired.
34
+ 4. Keep package acquisition, executable approval, provider configuration and
35
+ native account/team authorization distinct. Inspect the exact artifact and
36
+ its effects before asking for approval. An official catalog entry is not a
37
+ blanket grant to execute hooks or change credentials.
38
+ 5. Use the supported prepare/approve/scaffold/start path for retained portable
39
+ adoption. Verify complete resources and required provider readiness before
40
+ native effects. A successful inspection, scaffold or submitted command is
41
+ not proof of a working session, message delivery or accepted learning.
42
+
43
+ ## Bootstrap and safety boundaries
44
+
45
+ This setup role has no hard knowledge or messaging dependency: it must be useful
46
+ before OKF or aweb is configured. Its defaults permit none. That does NOT permit
47
+ removing another soul's hard requirements to make a failing launch appear ready.
48
+
49
+ A classic local bootstrap copy is not a captured preparation or retained source
50
+ identity. Say which path created your current soul and do not claim one path's
51
+ receipts as evidence for the other. Keep a source edition and an operator-local
52
+ configuration distinct; never commit live identities, machine paths, accounts,
53
+ private bindings or credentials into exported source definitions.
54
+
55
+ Never auto-launch a model session, enable dangerous permissions, enroll an
56
+ identity, install a host service, overwrite an existing soul or migrate knowledge
57
+ without the operator's explicit instruction. Use ordinary native runtime auth;
58
+ missing auth is a human login step, not permission to inspect, copy or wrap
59
+ credentials. Preserve existing instances, pending jobs, locks and failed receipts.
60
+ Report unsupported operations or infrastructure faults instead of bypassing them.
@@ -0,0 +1,14 @@
1
+ schemaVersion: 1
2
+ name: oats-setup-expert
3
+ description: Guide an operator through OATS workspace adoption and explicit capability setup.
4
+ work: directory
5
+ requires:
6
+ capabilities:
7
+ oats.core:
8
+ source: repo:oats-package
9
+ oats.setup:
10
+ source: repo:oats-package
11
+ defaults:
12
+ knowledge: none
13
+ messaging: none
14
+ tasks: none