@awebai/oats 0.30.0 → 0.30.2

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.
Files changed (128) hide show
  1. package/bin/oats.mjs +1 -1
  2. package/docs/capabilities.md +3 -3
  3. package/docs/design/2026-09-23-workspace-module-contracts.md +2 -1
  4. package/docs/desktop-cli-api.md +23 -11
  5. package/docs/first-team.md +1 -1
  6. package/docs/implementation.md +2 -1
  7. package/docs/integrations.md +1 -1
  8. package/docs/knowledge-capability-authoring.md +1 -1
  9. package/docs/knowledge.md +4 -4
  10. package/docs/official-catalog.md +4 -4
  11. package/docs/packages.md +13 -13
  12. package/docs/plans/0.30-close-out.md +24 -2
  13. package/docs/release-lane.md +7 -2
  14. package/docs/release-notes/v0.30.1.md +123 -0
  15. package/docs/release-notes/v0.30.2.md +85 -0
  16. package/docs/souls-and-instances.md +6 -5
  17. package/docs/workspaces.md +7 -2
  18. package/lib/core.mjs +42 -28
  19. package/lib/instance-inspect.mjs +1 -1
  20. package/lib/instance-resolution.mjs +15 -8
  21. package/lib/materialize.mjs +33 -19
  22. package/lib/packages.mjs +1 -1
  23. package/lib/resolve.mjs +1 -1
  24. package/package-catalog.json +3 -3
  25. package/package.json +1 -3
  26. package/skills/oats-getting-started/SKILL.md +2 -2
  27. package/capabilities/oats-authoring/LICENSE +0 -21
  28. package/capabilities/oats-authoring/oats-package.json +0 -11
  29. package/capabilities/oats-authoring/oats.json +0 -12
  30. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +0 -84
  31. package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +0 -109
  32. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +0 -116
  33. package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +0 -11
  34. package/capabilities/oats-aweb/bin/oats-aweb.mjs +0 -1672
  35. package/capabilities/oats-aweb/injects/aweb.md +0 -47
  36. package/capabilities/oats-aweb/lib/binding-wire.mjs +0 -365
  37. package/capabilities/oats-aweb/lib/captured-execution.mjs +0 -91
  38. package/capabilities/oats-aweb/lib/captured-native.mjs +0 -91
  39. package/capabilities/oats-aweb/lib/grant-custody.mjs +0 -38
  40. package/capabilities/oats-aweb/lib/invocation-shape.mjs +0 -135
  41. package/capabilities/oats-aweb/lib/portable-binding.mjs +0 -146
  42. package/capabilities/oats-aweb/lib/session-readiness.mjs +0 -56
  43. package/capabilities/oats-aweb/lib/wake-receive.mjs +0 -56
  44. package/capabilities/oats-aweb/oats.json +0 -201
  45. package/capabilities/oats-aweb/skills/LICENSE +0 -21
  46. package/capabilities/oats-aweb/skills/VENDORED.md +0 -31
  47. package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +0 -201
  48. package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +0 -161
  49. package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +0 -61
  50. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +0 -116
  51. package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +0 -74
  52. package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +0 -286
  53. package/capabilities/oats-code-review/injects/reviewer.md +0 -26
  54. package/capabilities/oats-code-review/oats.json +0 -16
  55. package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +0 -66
  56. package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +0 -30
  57. package/capabilities/oats-code-review/skills/security-review/SKILL.md +0 -56
  58. package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +0 -34
  59. package/capabilities/oats-developer/injects/developer.md +0 -38
  60. package/capabilities/oats-developer/oats.json +0 -17
  61. package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +0 -43
  62. package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +0 -47
  63. package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +0 -65
  64. package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +0 -37
  65. package/capabilities/oats-developer/skills/worktrees/SKILL.md +0 -36
  66. package/capabilities/oats-engineering-expert/injects/expert.md +0 -37
  67. package/capabilities/oats-engineering-expert/oats.json +0 -17
  68. package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +0 -37
  69. package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +0 -52
  70. package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +0 -50
  71. package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +0 -53
  72. package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +0 -49
  73. package/capabilities/oats-jira/bin/oats-jira.mjs +0 -40
  74. package/capabilities/oats-jira/injects/jira.md +0 -10
  75. package/capabilities/oats-jira/oats.json +0 -22
  76. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +0 -179
  77. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +0 -34
  78. package/capabilities/oats-linear/bin/oats-linear.mjs +0 -344
  79. package/capabilities/oats-linear/injects/linear.md +0 -8
  80. package/capabilities/oats-linear/oats.json +0 -24
  81. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +0 -223
  82. package/capabilities/oats-okf/bin/oats-okf-binding.mjs +0 -14
  83. package/capabilities/oats-okf/bin/oats-okf.mjs +0 -213
  84. package/capabilities/oats-okf/injects/okf.md +0 -42
  85. package/capabilities/oats-okf/lib/binding-wire.mjs +0 -380
  86. package/capabilities/oats-okf/lib/captured-worker.mjs +0 -109
  87. package/capabilities/oats-okf/lib/config.mjs +0 -124
  88. package/capabilities/oats-okf/lib/consult.mjs +0 -518
  89. package/capabilities/oats-okf/lib/harvest-status.mjs +0 -88
  90. package/capabilities/oats-okf/lib/harvest-switch.mjs +0 -94
  91. package/capabilities/oats-okf/lib/inspection.mjs +0 -138
  92. package/capabilities/oats-okf/lib/invocation-context.mjs +0 -111
  93. package/capabilities/oats-okf/lib/invocation-shape.mjs +0 -135
  94. package/capabilities/oats-okf/lib/io.mjs +0 -118
  95. package/capabilities/oats-okf/lib/migration.mjs +0 -137
  96. package/capabilities/oats-okf/lib/okf-validate.mjs +0 -123
  97. package/capabilities/oats-okf/lib/portable-binding.mjs +0 -199
  98. package/capabilities/oats-okf/lib/source-contract.mjs +0 -46
  99. package/capabilities/oats-okf/lib/sources.mjs +0 -438
  100. package/capabilities/oats-okf/lib/stores.mjs +0 -473
  101. package/capabilities/oats-okf/lib/worker.mjs +0 -486
  102. package/capabilities/oats-okf/oats.json +0 -151
  103. package/capabilities/oats-okf/schemas/okf-base.schema.json +0 -46
  104. package/capabilities/oats-okf/schemas/okf-bindings.schema.json +0 -112
  105. package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +0 -87
  106. package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +0 -113
  107. package/capabilities/oats-okf/schemas/okf-soul.schema.json +0 -37
  108. package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +0 -144
  109. package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +0 -86
  110. package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +0 -104
  111. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +0 -140
  112. package/capabilities/oats-okf-harvest/injects/harvester.md +0 -12
  113. package/capabilities/oats-okf-harvest/oats.json +0 -26
  114. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +0 -168
  115. package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +0 -192
  116. package/capabilities/oats-okf-harvest/skills/okf-authoring/SKILL.md +0 -151
  117. package/capabilities/oats-okf-harvest/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  118. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +0 -170
  119. package/capabilities/oats-okf-maintenance/injects/maintainer.md +0 -12
  120. package/capabilities/oats-okf-maintenance/lib/provenance.mjs +0 -50
  121. package/capabilities/oats-okf-maintenance/oats.json +0 -21
  122. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +0 -159
  123. package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +0 -192
  124. package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +0 -151
  125. package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
  126. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +0 -134
  127. package/capabilities/oats-workspace-experts/injects/oats-experts.md +0 -26
  128. package/capabilities/oats-workspace-experts/oats.json +0 -9
@@ -54,8 +54,8 @@ members: # repo refs, NO @revision (E_WORKSPAC
54
54
  - git:github.com/acme/tools # a member that ALSO publishes a package (see below)
55
55
 
56
56
  packages: # the ONLY versioned things
57
- oats.framework: v1.4.0 # bare version → resolves through the official catalog
58
- oats.okf: v4.0.4
57
+ oats.framework: v1.4.1 # bare version → resolves through the official catalog
58
+ oats.okf: v4.0.5
59
59
  acme.tools: git:github.com/acme/tools@v0.4.0 # outside the catalog → git:<repo>@<tag|OID>; still a package
60
60
 
61
61
  teams: # SHARED teams: the same provider team for everyone
@@ -226,6 +226,11 @@ A repository may be a **member** (it completed the handshake; its `souls/*` and
226
226
  version }` on the member row (informational) and does **not** list the
227
227
  package's capabilities as member capabilities.
228
228
 
229
+ A publisher that also keeps copies of its package's capabilities (a mirror, a
230
+ fixture) keeps them outside `capabilities/`, or discovery lists them as member
231
+ capabilities too: the same capability offered twice, once at latest state. The
232
+ `oats` repository keeps its mirrors of the official packages in `mirrors/`.
233
+
229
234
  So the framework's own souls say `oats.okf: { from: package }` even though
230
235
  `oats-okf` is a member of the OATS workspace, and every package repo carries a
231
236
  member soul that is the expert in that capability (`oats-okf-expert`,
package/lib/core.mjs CHANGED
@@ -2992,7 +2992,7 @@ function* spawnBody(root, agent, o = {}) {
2992
2992
  return e;
2993
2993
  };
2994
2994
  // Workspace model: copy every resolved capability WHOLE into the new home
2995
- // (.oats/modules/<cap>/ + .agents/skills/<cap>/) and record modules/providers
2995
+ // (.oats/modules/<cap>/, its skills flat in .agents/skills/<skill>/) and record modules/providers
2996
2996
  // in instance.json. Then REBUILD the capability rows against the copies that
2997
2997
  // landed (H1): until now `resolvedCfg.capabilities` were PLANNED rows (no
2998
2998
  // skills, no inject, no dir) — hooks, environment, requirements, retirement
@@ -3034,9 +3034,14 @@ function* spawnBody(root, agent, o = {}) {
3034
3034
  const row = rows.find((c) => c.id === r.module);
3035
3035
  if (r.type === "injection") { r.path = row?.inject; continue; }
3036
3036
  if (r.type === "skill-tree") {
3037
- const skillsRoot = join(home, ".agents", "skills", r.module);
3037
+ // Module skills land flat in the canonical skills root; the entries are
3038
+ // the names materialize placed for this module (what the resolution
3039
+ // promised), verified against the root in the completeness check.
3040
+ const skillsRoot = join(home, ".agents", "skills");
3038
3041
  r.path = existsSync(skillsRoot) ? skillsRoot : undefined;
3039
- r.entries = (row?.skills || []).map((p) => basename(p));
3042
+ r.entries = Array.isArray(materializeOutcome?.skills)
3043
+ ? materializeOutcome.skills.filter((s) => s.module === r.module).map((s) => s.name)
3044
+ : (row?.skills || []).map((p) => basename(p));
3040
3045
  }
3041
3046
  }
3042
3047
  } catch (e) { throw rollbackEmptyOrPreparedHome(e); }
@@ -3074,30 +3079,43 @@ function* spawnBody(root, agent, o = {}) {
3074
3079
  }
3075
3080
  if (!existsSync(join(home, "CLAUDE.md"))) symlinkSync("AGENTS.md", join(home, "CLAUDE.md"));
3076
3081
 
3077
- // Skills: module skills were copied WHOLE into <home>/.agents/skills/<module>/
3082
+ // Skills: module skills were copied WHOLE and FLAT into <home>/.agents/skills/<skill>/
3078
3083
  // by materialize (decision 7/13); here only the soul's own skills join them, one
3079
- // directory each. There is no override: a soul skill named like a module's
3080
- // directory is a duplicate (decision 16). The harness is launched with its normal
3084
+ // directory each, at the same level — the one level every harness discovers. There
3085
+ // is no override: a soul skill named like a module's skill is a duplicate (decision 16). The harness is launched with its normal
3081
3086
  // discovery — the machine's and the repo's skills are the harness's business.
3082
3087
  const sources = [];
3083
3088
  const soulSkills = join(soulDir, "skills");
3084
3089
  if (existsSync(soulSkills)) sources.push({ id: "soul", path: soulSkills });
3085
3090
  const chosen = new Map();
3091
+ // Names compare case-insensitively: one directory per skill on a case-insensitive
3092
+ // filesystem (APFS, NTFS) must not silently merge `Foo` into `foo`.
3093
+ const moduleSkillOwner = new Map((materializeOutcome?.skills || []).map((s) => [s.name.toLowerCase(), s.module]));
3094
+ const chosenKeys = new Map();
3086
3095
  const offer = (name, src, source) => {
3087
- if (chosen.has(name) || existsSync(join(home, ".agents", "skills", name))) throw oatsError("E_SKILL_DUPLICATE", `skill "${name}" from ${source} collides with ${chosen.get(name)?.source ?? `module ${name}'s skill directory`}`);
3096
+ const key = name.toLowerCase();
3097
+ if (chosenKeys.has(key) || moduleSkillOwner.has(key) || existsSync(join(home, ".agents", "skills", name))) {
3098
+ const other = chosenKeys.get(key) ?? (moduleSkillOwner.has(key) ? `module ${moduleSkillOwner.get(key)}'s skill` : `the existing .agents/skills/${name}`);
3099
+ throw oatsError("E_SKILL_DUPLICATE", `skill "${name}" from ${source} collides with ${other}`);
3100
+ }
3088
3101
  chosen.set(name, { src, source });
3102
+ chosenKeys.set(key, source);
3089
3103
  };
3090
- // Same enumerator preflight used, so "what a tree promises" and "what gets
3091
- // copied" cannot drift apart.
3092
- for (const source of sources) for (const entry of skillEntriesIn(source.path)) offer(entry.name, entry.src, source.id);
3093
- mkdirSync(join(home, ".agents", "skills"), { recursive: true });
3094
- mkdirSync(join(home, ".claude"), { recursive: true });
3095
- for (const [name, selected] of [...chosen].sort(([a], [b]) => a.localeCompare(b))) {
3096
- // Pi's recursive skill scanner does not descend through directory symlinks.
3097
- // Copy each selected tree so the exact instance-local set is real and immutable.
3098
- copyTreeSafe(realpathSync(selected.src), join(home, ".agents", "skills", name));
3099
- }
3100
- if (!existsSync(join(home, ".claude", "skills"))) symlinkSync(join("..", ".agents", "skills"), join(home, ".claude", "skills"));
3104
+ // A refusal here comes after materialize populated the home: remove it whole,
3105
+ // like every other failure between materialize and the completeness check.
3106
+ try {
3107
+ // Same enumerator preflight used, so "what a tree promises" and "what gets
3108
+ // copied" cannot drift apart.
3109
+ for (const source of sources) for (const entry of skillEntriesIn(source.path)) offer(entry.name, entry.src, source.id);
3110
+ mkdirSync(join(home, ".agents", "skills"), { recursive: true });
3111
+ mkdirSync(join(home, ".claude"), { recursive: true });
3112
+ for (const [name, selected] of [...chosen].sort(([a], [b]) => a.localeCompare(b))) {
3113
+ // Pi's recursive skill scanner does not descend through directory symlinks.
3114
+ // Copy each selected tree so the exact instance-local set is real and immutable.
3115
+ copyTreeSafe(realpathSync(selected.src), join(home, ".agents", "skills", name));
3116
+ }
3117
+ if (!existsSync(join(home, ".claude", "skills"))) symlinkSync(join("..", ".agents", "skills"), join(home, ".claude", "skills"));
3118
+ } catch (e) { throw rollbackEmptyOrPreparedHome(e); }
3101
3119
 
3102
3120
  // EXPECTED == MATERIALIZED. Preflight proved every declared resource resolves;
3103
3121
  // this proves the copies actually landed, so "the composition is complete" is
@@ -3119,18 +3137,14 @@ function* spawnBody(root, agent, o = {}) {
3119
3137
  if (!chosen.has(name)) incomplete.push(`skill "${name}", promised by ${r.source} (${r.declared}), is missing from the composed set`);
3120
3138
  }
3121
3139
  }
3122
- // Prepared spawn: every module's declared skill must have been copied under
3123
- // .agents/skills/<module>/<skill>/ by materialize — verify the copies landed
3124
- // as readable skills (the module directory alone proves nothing).
3140
+ // Prepared spawn: every module's declared skill must have been copied to
3141
+ // .agents/skills/<skill>/ by materialize — verify the copies landed as
3142
+ // readable skills, one level deep, where the harnesses discover them.
3125
3143
  if (o.prepared) {
3126
- for (const m of o.prepared.resolution.modules) for (const s of m.manifest.skills || []) {
3127
- const sk = join(home, ".agents", "skills", m.name);
3128
- if (!existsSync(sk)) { incomplete.push(`module "${m.name}" declares skills but .agents/skills/${m.name} is absent`); break; }
3129
- }
3130
3144
  for (const r of expectedResources) {
3131
3145
  if (r.deferred !== "materialize" || r.type !== "skill-tree") continue;
3132
- if (!r.path) { incomplete.push(`module "${r.module}" declares skill tree ${r.declared} but .agents/skills/${r.module} is absent`); continue; }
3133
- if (!r.entries.length) incomplete.push(`module "${r.module}" declares skill tree ${r.declared} but no skill was copied under .agents/skills/${r.module}`);
3146
+ if (!r.path) { incomplete.push(`module "${r.module}" declares skill tree ${r.declared} but .agents/skills is absent`); continue; }
3147
+ if (!r.entries.length) incomplete.push(`module "${r.module}" declares skill tree ${r.declared} but none of its skills was copied into .agents/skills`);
3134
3148
  for (const name of r.entries) if (!hasSkillDoc(join(r.path, name))) incomplete.push(`skill "${name}" (module ${r.module}) did not materialize as a readable SKILL.md`);
3135
3149
  }
3136
3150
  }
@@ -3475,7 +3489,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
3475
3489
  if (triggerEventFile) recipe.env.OATS_TRIGGER_EVENT_FILE = triggerEventFile;
3476
3490
  const cmdline = renderLaunchRecipe(recipe, { home, instance });
3477
3491
 
3478
- // Module skills as materialize landed them (.agents/skills/<module>/<skill>/),
3492
+ // Module skills as materialize landed them (flat, .agents/skills/<skill>/),
3479
3493
  // beside the soul's own: per-skill provenance `module:<cap>` (lead decision c3),
3480
3494
  // `from` = the home's module copy the skill was copied from.
3481
3495
  const moduleSkills = (materializeOutcome?.skills || []).map((row) => ({ name: row.name, source: `module:${row.module}`, from: join(home, row.from) }));
@@ -221,7 +221,7 @@ function capabilityRows(t) {
221
221
  return { ...op, argv: [m.command ?? null, op.command], available: !reason, reason };
222
222
  });
223
223
  // composedFrom (feature desktop-facts): where the soul's composition took it from — "workspace" |
224
- // "team:<label>" | "soul" (layers-from vocabulary); null for a home (not recorded at spawn). `from` is the module's origin.
224
+ // "soul" (layers-from vocabulary); null for a home (not recorded at spawn). `from` is the module's origin.
225
225
  return { id: name, version: m.version ?? null, layer: m.layer ?? null, command: m.command ?? null, from, composedFrom: t.capabilitiesFrom?.[name] ?? null, dir,
226
226
  settings: obj(t.payloads[name]) ? t.payloads[name] : {}, declares: declaredSettings(m), compatibility: compatibilityRow(name, m), missingRequires: missing, operations };
227
227
  });
@@ -5,7 +5,7 @@
5
5
  * its Git remotes in the operator's access context → find the soul among the
6
6
  * confirmed members (or external souls) → resolve every capability by `from:`
7
7
  * (member = latest state, package = the locked version) → materialize
8
- * each capability WHOLE into the new home (`.oats/modules/`, `.agents/skills/`) →
8
+ * each capability WHOLE into the new home (`.oats/modules/`, skills flat in `.agents/skills/`) →
9
9
  * compose AGENTS.md → launch the harness normally.
10
10
  *
11
11
  * This module owns the async half (discover + resolve) and the materialize call;
@@ -24,7 +24,7 @@ import { resolveSoul, kernelCompatibility } from "./resolve.mjs";
24
24
  import { BY_TEAM_REMOVED, recordedTeams, reportRows, soulKeyOf, soulTeams, teamModel } from "./teams.mjs";
25
25
  import { launchLayers } from "./launch-preference.mjs";
26
26
  import { declaredSettings } from "./capability-contract.mjs";
27
- import { materialize, MODULES_DIR, SKILLS_DIR } from "./materialize.mjs";
27
+ import { materialize, moduleSkills, MODULES_DIR, SKILLS_DIR } from "./materialize.mjs";
28
28
  import { fetchRemoteTree } from "./remote.mjs";
29
29
  import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync, symlinkSync, statSync } from "node:fs";
30
30
  import { tmpdir } from "node:os";
@@ -524,9 +524,11 @@ export function toCapabilityRows(resolution, home) {
524
524
  for (const m of resolution.modules) {
525
525
  const dir = join(home, MODULES_DIR, m.name);
526
526
  const manifest = m.manifest;
527
- const skills = [];
528
- const skillsRoot = join(home, SKILLS_DIR, m.name);
529
- if (existsSync(skillsRoot)) for (const e of readdirSync(skillsRoot, { withFileTypes: true })) if (e.isDirectory()) skills.push(join(skillsRoot, e.name));
527
+ // Skills are flat under <home>/.agents/skills/<skill>/; which of them this module
528
+ // contributed is read from its own copy, enumerated exactly as materialize placed them.
529
+ const skills = existsSync(dir)
530
+ ? moduleSkills(resolution, m, dir).map((s) => join(home, SKILLS_DIR, s.name)).filter((p) => existsSync(p))
531
+ : [];
530
532
  const inject = manifest.inject ? join(dir, manifest.inject) : undefined;
531
533
  rows.push({
532
534
  id: m.name, capability: m.name, manifest, layer: manifest.layer ?? undefined, command: manifest.command,
@@ -564,8 +566,12 @@ function requiredHooksOf(manifest) {
564
566
  return Object.entries(hooks).filter(([, s]) => s && typeof s === "object" && s.required === true).map(([e]) => e);
565
567
  }
566
568
 
567
- /** What a preview shows about modules: from/commit/digest per module and whether
568
- * it changed since the newest existing instance of the same soul in `agentsRoot`. */
569
+ /** What a preview shows about modules: from/commit/digest per module, whether
570
+ * it changed since the newest existing instance of the same soul in `agentsRoot`,
571
+ * and `composedFrom` (feature preview-composed-from): why the soul's composition
572
+ * has it — "soul" | "workspace", the resolution's capabilitiesFrom. Provenance
573
+ * only: it is outside every fingerprint. `null` only for a resolution without
574
+ * capabilitiesFrom (an older in-process caller); never for a resolved module. */
569
575
  export function modulesPreview(resolution, agentsRoot, soulName) {
570
576
  let previous = null;
571
577
  const instancesDir = join(agentsRoot, soulName, "instances");
@@ -583,7 +589,8 @@ export function modulesPreview(resolution, agentsRoot, soulName) {
583
589
  return resolution.modules.map((m) => {
584
590
  const prev = previous?.modules?.[m.name];
585
591
  const changedSince = !previous ? null : !prev ? { instance: previous.name, was: null } : (prev.commit !== m.from.commit ? { instance: previous.name, was: prev.commit } : false);
586
- return { name: m.name, from: m.from, layer: m.manifest.layer ?? null, private: !!m.private, declares: declaredSettings(m.manifest), changedSince };
592
+ return { name: m.name, from: m.from, layer: m.manifest.layer ?? null, private: !!m.private, declares: declaredSettings(m.manifest), changedSince,
593
+ composedFrom: resolution.capabilitiesFrom?.[m.name] ?? null };
587
594
  });
588
595
  }
589
596
 
@@ -6,7 +6,9 @@
6
6
  * materialize(resolution, home, options)
7
7
  * For every module of the resolution: fetch its capability directory whole into
8
8
  * <home>/.oats/modules/<name>/, verify the copy's content digest, copy its skills as
9
- * FULL copies into <home>/.agents/skills/<name>/<skill>/, compose <home>/AGENTS.md
9
+ * FULL copies into <home>/.agents/skills/<skill>/ — FLAT, one level deep, where every
10
+ * harness discovers them (Claude Code through .claude/skills/<skill>/); skill names are
11
+ * unique across the modules (E_SKILL_DUPLICATE) — compose <home>/AGENTS.md
10
12
  * (soul body + each module's inject, same marker comments as the kernel composer),
11
13
  * keep the CLAUDE.md / .claude/skills aliases, and record modules + providers in
12
14
  * <home>/instance.json.
@@ -16,7 +18,7 @@
16
18
  * place. Any failure before the commit removes the staging directory and leaves the home
17
19
  * exactly as it was (a `.oats/` directory created only for staging is removed too). The
18
20
  * commit re-checks the home's shape (no symlink planted at .oats/.agents/.claude or the
19
- * module targets during the fetch), then runs a short sequence of renames — modules,
21
+ * module and skill targets during the fetch), then runs a short sequence of renames — modules,
20
22
  * skills, AGENTS.md, instance.json, and the CLAUDE.md / .claude/skills aliases — with a
21
23
  * rollback journal: should any step fail, everything already placed is undone and the
22
24
  * previous AGENTS.md / instance.json restored before a named error (E_MATERIALIZE_HOME)
@@ -205,7 +207,7 @@ function hasSkillDoc(dir) {
205
207
  /** The skills a module contributes → [{ name, path }] (path relative to the module root).
206
208
  * Precedence: resolution.skills rows for the module; else manifest.skills; else every
207
209
  * skills/<dir>/SKILL.md in the fetched tree. */
208
- function moduleSkills(resolution, module, moduleDir) {
210
+ export function moduleSkills(resolution, module, moduleDir) {
209
211
  const declared = (resolution.skills || []).filter((s) => s && s.module === module.name);
210
212
  const rows = [];
211
213
  if (declared.length) {
@@ -335,13 +337,22 @@ export async function materialize(resolution, home, options = {}) {
335
337
  const parents = [join(homeAbs, ".oats"), modulesRoot, join(homeAbs, ".agents"), skillsRoot, join(homeAbs, ".claude")];
336
338
  /** The home must still be what we checked: module targets absent, every parent a real directory (or absent).
337
339
  * Run before the fetch AND immediately before the commit — a symlink planted in between must not be written through. */
340
+ // Skill targets are known only once each module is fetched; the commit-time re-check covers them.
341
+ // skill name, lower-cased → { module, path, name }: one directory per skill, and on a
342
+ // case-insensitive filesystem `Foo` and `foo` are one directory.
343
+ const skillOwners = new Map();
338
344
  const assertHomeShape = () => {
339
345
  for (const { module } of sources) {
340
- for (const target of [join(modulesRoot, module.name), join(skillsRoot, module.name)]) {
341
- let st = null;
342
- try { st = lstatSync(target); } catch {}
343
- if (st) throw fail("E_MATERIALIZE_HOME", `${target} already exists — the instance already carries module ${module.name}; materialize never overwrites a module in place`, { home: homeAbs, module: module.name, path: target });
344
- }
346
+ const target = join(modulesRoot, module.name);
347
+ let st = null;
348
+ try { st = lstatSync(target); } catch {}
349
+ if (st) throw fail("E_MATERIALIZE_HOME", `${target} already exists — the instance already carries module ${module.name}; materialize never overwrites a module in place`, { home: homeAbs, module: module.name, path: target });
350
+ }
351
+ for (const owner of skillOwners.values()) {
352
+ const name = owner.name, target = join(skillsRoot, name);
353
+ let st = null;
354
+ try { st = lstatSync(target); } catch {}
355
+ if (st) throw fail("E_MATERIALIZE_HOME", `${target} already exists — the instance already carries a skill named ${name}; materialize never overwrites a skill in place`, { home: homeAbs, module: owner.module, skill: name, path: target });
345
356
  }
346
357
  for (const p of parents) {
347
358
  let st = null;
@@ -378,7 +389,6 @@ export async function materialize(resolution, home, options = {}) {
378
389
  mkdirSync(join(staging, "skills"), { recursive: true, mode: 0o755 });
379
390
 
380
391
  const moduleRows = [], skillRows = [], capabilityBlocks = [], recorded = {};
381
- const skillOwners = new Map();
382
392
  for (const { module, source } of sources) {
383
393
  const stagedModule = join(staging, "modules", module.name);
384
394
  const finalModule = join(modulesRoot, module.name);
@@ -412,15 +422,21 @@ export async function materialize(resolution, home, options = {}) {
412
422
  }
413
423
 
414
424
  for (const skill of moduleSkills(resolution, module, stagedModule)) {
415
- const owner = skillOwners.get(skill.name);
416
- if (owner && owner !== module.name) {
417
- throw fail("E_SKILL_DUPLICATE", `skill ${JSON.stringify(skill.name)} is contributed by both ${owner} and ${module.name}`, { name: skill.name, modules: [owner, module.name] });
425
+ // Skills land flat, so a name is one directory in the home: two sources for one
426
+ // name — two modules, or two paths of one module — are a duplicate.
427
+ const key = skill.name.toLowerCase();
428
+ const owner = skillOwners.get(key);
429
+ if (owner && owner.module === module.name && owner.path === skill.path && owner.name === skill.name) continue;
430
+ if (owner) {
431
+ const detail = owner.module === module.name ? `${module.name} (${owner.path} and ${skill.path})` : `both ${owner.module} and ${module.name}`;
432
+ const names = owner.name === skill.name ? JSON.stringify(skill.name) : `${JSON.stringify(owner.name)} / ${JSON.stringify(skill.name)} (one directory on a case-insensitive filesystem)`;
433
+ throw fail("E_SKILL_DUPLICATE", `skill ${names} is contributed by ${detail}`, { name: skill.name, modules: [owner.module, module.name] });
418
434
  }
419
- skillOwners.set(skill.name, module.name);
435
+ skillOwners.set(key, { module: module.name, path: skill.path, name: skill.name });
420
436
  const src = join(stagedModule, ...skill.path.split("/"));
421
- const staged = join(staging, "skills", module.name, skill.name);
437
+ const staged = join(staging, "skills", skill.name);
422
438
  copyTree(src, staged, `${module.name}/${skill.path}`);
423
- skillRows.push({ module: module.name, name: skill.name, from: `${MODULES_DIR}/${module.name}/${skill.path}`.split(sep).join("/"), path: join(skillsRoot, module.name, skill.name) });
439
+ skillRows.push({ module: module.name, name: skill.name, from: `${MODULES_DIR}/${module.name}/${skill.path}`.split(sep).join("/"), path: join(skillsRoot, skill.name) });
424
440
  }
425
441
 
426
442
  for (const rel of moduleInjects(resolution, module, stagedModule)) {
@@ -478,10 +494,8 @@ export async function materialize(resolution, home, options = {}) {
478
494
  };
479
495
  try {
480
496
  for (const p of [join(homeAbs, ".oats"), modulesRoot, join(homeAbs, ".agents"), skillsRoot, join(homeAbs, ".claude")]) mkdirTracked(p);
481
- for (const { module } of sources) {
482
- placeDir(join(staging, "modules", module.name), join(modulesRoot, module.name));
483
- if (existsSync(join(staging, "skills", module.name))) placeDir(join(staging, "skills", module.name), join(skillsRoot, module.name));
484
- }
497
+ for (const { module } of sources) placeDir(join(staging, "modules", module.name), join(modulesRoot, module.name));
498
+ for (const row of skillRows) placeDir(join(staging, "skills", row.name), row.path);
485
499
  swapIn(join(staging, "AGENTS.md"), join(homeAbs, "AGENTS.md"), "AGENTS.md");
486
500
  swapIn(join(staging, "instance.json"), instanceFile, "instance.json");
487
501
  // Aliases (relative symlinks), only when absent — the canonical-plus-alias construction stays; inside the transaction.
package/lib/packages.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  * OATS packages — versions, lock v3 (workspace model v2).
3
3
  *
4
4
  * Contract: docs/design/2026-09-23-workspace-module-contracts.md §4.
5
- * Decision: agents/oats-expert/soul/knowledge/decisions/workspace-model-v2.md.
5
+ * Decision: https://github.com/awebai/oats-knowledge/blob/main/knowledge/nodes/oats-expert/decisions/workspace-model-v2.md.
6
6
  *
7
7
  * A package is a place to fetch from WITH a version attached. Nothing is
8
8
  * installed: `resolvePackages` turns each `workspace.packages` entry into an
package/lib/resolve.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  * lib/resolve.mjs — from a soul to an immutable resolution (module contract §3).
3
3
  *
4
4
  * Contract: docs/design/2026-09-23-workspace-module-contracts.md §3.
5
- * Decision: agents/oats-expert/soul/knowledge/decisions/workspace-model-v2.md (6, 12, 14, 16, 19–21).
5
+ * Decision: https://github.com/awebai/oats-knowledge/blob/main/knowledge/nodes/oats-expert/decisions/workspace-model-v2.md (6, 12, 14, 16, 19–21).
6
6
  *
7
7
  * `resolveSoul(discovery, soulEntry, options)` turns a discovered soul into the
8
8
  * exact set of modules an instance will be built from: which capability comes
@@ -3,7 +3,7 @@
3
3
  "packages": {
4
4
  "oats.okf": {
5
5
  "url": "https://github.com/awebai/oats-okf.git",
6
- "ref": "v4.0.4",
6
+ "ref": "v4.0.5",
7
7
  "path": "oats-package"
8
8
  },
9
9
  "oats.aweb": {
@@ -28,12 +28,12 @@
28
28
  },
29
29
  "oats.engineering": {
30
30
  "url": "https://github.com/awebai/oats-engineering.git",
31
- "ref": "v1.1.0",
31
+ "ref": "v1.3.0",
32
32
  "path": "oats-package"
33
33
  },
34
34
  "oats.framework": {
35
35
  "url": "https://github.com/awebai/oats.git",
36
- "ref": "oats-framework/v1.4.0",
36
+ "ref": "oats-framework/v1.4.1",
37
37
  "path": "oats-package"
38
38
  }
39
39
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.30.0",
3
+ "version": "0.30.2",
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",
@@ -18,7 +18,6 @@
18
18
  "check": "node scripts/check-package-dry-runs.mjs --syntax-only",
19
19
  "check:pi": "node --experimental-strip-types --check packages/pi/extension/index.ts",
20
20
  "validate": "node scripts/validate-project.mjs",
21
- "validate:okf": "node scripts/validate-okf.mjs",
22
21
  "pack:check": "node scripts/check-package-dry-runs.mjs",
23
22
  "smoke:tarball": "node scripts/clean-room-smoke.mjs"
24
23
  },
@@ -32,7 +31,6 @@
32
31
  "lib/",
33
32
  "skills/",
34
33
  "injects/",
35
- "capabilities/",
36
34
  "docs/",
37
35
  "README.md",
38
36
  "package-catalog.json",
@@ -60,8 +60,8 @@ members:
60
60
  - git:github.com/acme/agents # the host is a member too
61
61
  - git:github.com/acme/platform
62
62
  packages:
63
- oats.framework: v1.4.0 # bare versions resolve through the official catalog
64
- oats.okf: v4.0.4
63
+ oats.framework: v1.4.1 # bare versions resolve through the official catalog
64
+ oats.okf: v4.0.5
65
65
  defaults:
66
66
  capabilities: { oats.core: { from: package } }
67
67
  knowledge: { oats.okf: { from: package } }
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 OATS Framework
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,11 +0,0 @@
1
- {
2
- "package": "oats.authoring",
3
- "version": "1.0.3",
4
- "description": "Official additive OATS guidance for capability, skill, and soul authoring.",
5
- "compatibility": {
6
- "oats": ">=0.25.0"
7
- },
8
- "capabilities": [
9
- "."
10
- ]
11
- }
@@ -1,12 +0,0 @@
1
- {
2
- "capability": "oats.authoring",
3
- "version": "1.0.3",
4
- "compatibility": { "oats": ">=0.25.0" },
5
- "description": "Additive framework-authoring guidance for capability packages, agent skills, and souls.",
6
- "requires": [],
7
- "skills": [
8
- "skills/integration-authoring",
9
- "skills/skill-craft",
10
- "skills/soul-craft"
11
- ]
12
- }
@@ -1,84 +0,0 @@
1
- ---
2
- name: integration-authoring
3
- description: >-
4
- Route custom OATS capability-package and integration work to the framework's
5
- integrations expert. Use when building, adapting, or debugging a reusable
6
- capability, new tasks/messaging/knowledge core capability, oats.json manifest,
7
- lifecycle hook, or operational command—not merely activating an existing
8
- package. Triggers: "custom integration", "capability package", "integrate
9
- our tracker", "new messaging integration", "write an oats.json".
10
- ---
11
-
12
- # Capability and integration authoring — delegate
13
-
14
- A capability package may ship skills, instance instructions, requirements,
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
18
- manifest, security, targeting-boundary, collision, and probe discipline; use
19
- the framework's **integrations-expert** soul rather than improvising.
20
-
21
- If the user only wants an existing package, declare it and give it to souls;
22
- no build is needed:
23
-
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 }
32
- ```
33
-
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.
43
-
44
- ## 2. Spawn the expert against the package's repository
45
-
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:
49
-
50
- ```bash
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
57
- ```
58
-
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.
63
-
64
- ## 3. Brief the design boundary
65
-
66
- Tell the expert:
67
-
68
- - whether it is additive or implements exactly one of knowledge/messaging/tasks;
69
- - external requirements and executable surfaces (commands, hooks);
70
- - intended distribution and version/compatibility;
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).
73
-
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.
79
-
80
- ## 4. Hand off
81
-
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.