@aiwg/cli 2026.10.5 → 2026.10.6

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 (212) hide show
  1. package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
  2. package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
  3. package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
  4. package/agentic/code/providers/model-capabilities.v1.json +74 -50
  5. package/bin/aiwg.mjs +3 -1
  6. package/dist/src/artifacts/browser-export.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +8 -1
  8. package/dist/src/artifacts/cli.js +57 -6
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  10. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  11. package/dist/src/artifacts/graph-query.js +18 -8
  12. package/dist/src/artifacts/index-adapters.js +339 -0
  13. package/dist/src/artifacts/index-builder.js +143 -14
  14. package/dist/src/artifacts/index-files.js +15 -4
  15. package/dist/src/artifacts/index-reader.js +44 -0
  16. package/dist/src/artifacts/index-refresh.js +114 -55
  17. package/dist/src/artifacts/index-status.js +20 -5
  18. package/dist/src/artifacts/memory-maintenance.js +503 -0
  19. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  20. package/dist/src/artifacts/query-engine.js +263 -108
  21. package/dist/src/artifacts/relations-builder.js +2 -2
  22. package/dist/src/artifacts/source-graph.js +2 -2
  23. package/dist/src/artifacts/types.js +9 -1
  24. package/dist/src/cli/handlers/decision.js +19 -1
  25. package/dist/src/cli/handlers/guided-setup.js +470 -0
  26. package/dist/src/cli/handlers/help.js +2 -2
  27. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  28. package/dist/src/cli/handlers/index.js +4 -2
  29. package/dist/src/cli/handlers/init.js +41 -6
  30. package/dist/src/cli/handlers/local-executor.js +5 -3
  31. package/dist/src/cli/handlers/mc.js +509 -53
  32. package/dist/src/cli/handlers/models.js +32 -2
  33. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  34. package/dist/src/cli/handlers/rules.js +1 -1
  35. package/dist/src/cli/handlers/script-runner.js +73 -2
  36. package/dist/src/cli/handlers/serve.js +5 -3
  37. package/dist/src/cli/handlers/sessions.js +1 -0
  38. package/dist/src/cli/handlers/setup.js +428 -1
  39. package/dist/src/cli/handlers/skill-lint.js +1 -1
  40. package/dist/src/cli/handlers/subcommands.js +118 -8
  41. package/dist/src/cli/handlers/use.js +279 -93
  42. package/dist/src/cli/handlers/workspace.js +3 -1
  43. package/dist/src/cli/prompt-utils.js +8 -0
  44. package/dist/src/cli/router.js +16 -1
  45. package/dist/src/config/aiwg-config.js +42 -0
  46. package/dist/src/config/cli.js +7 -3
  47. package/dist/src/config/decision-config.js +14 -1
  48. package/dist/src/config/native-sandbox-warnings.js +25 -0
  49. package/dist/src/contributors/validation.js +6 -5
  50. package/dist/src/decision/admission.js +7 -3
  51. package/dist/src/decision/benchmark.js +469 -0
  52. package/dist/src/decision/ledger.js +22 -3
  53. package/dist/src/decision/pricing.js +147 -0
  54. package/dist/src/decision/qualification/gate-evidence.js +18 -15
  55. package/dist/src/decision/qualification/load-staged.js +539 -0
  56. package/dist/src/decision/qualification/load.js +109 -33
  57. package/dist/src/decision/qualification/runner.js +5 -2
  58. package/dist/src/decision/review/service.js +6 -4
  59. package/dist/src/decision/review/validate.js +23 -2
  60. package/dist/src/decision/savings.js +8 -3
  61. package/dist/src/discovery/index.js +492 -0
  62. package/dist/src/discovery/interview.js +233 -0
  63. package/dist/src/discovery/plan-apply.js +571 -0
  64. package/dist/src/discovery/plan-store.js +171 -0
  65. package/dist/src/discovery/plan-types.js +2 -0
  66. package/dist/src/discovery/plan-validation.js +139 -0
  67. package/dist/src/discovery/types.js +2 -0
  68. package/dist/src/extensions/commands/definitions.js +49 -9
  69. package/dist/src/extensions/manifest.js +71 -44
  70. package/dist/src/extensions/project-local-discovery.js +1 -1
  71. package/dist/src/extensions/project-quickref.js +18 -17
  72. package/dist/src/extensions/validation.js +32 -29
  73. package/dist/src/housekeeper/daemon-config.js +148 -0
  74. package/dist/src/housekeeper/ipc.js +84 -0
  75. package/dist/src/housekeeper/lifecycle.js +148 -0
  76. package/dist/src/housekeeper/platform.js +35 -0
  77. package/dist/src/housekeeper/status.js +70 -0
  78. package/dist/src/housekeeper/units.js +114 -0
  79. package/dist/src/jobs/flow.js +22 -21
  80. package/dist/src/lint/rule-activation.js +20 -5
  81. package/dist/src/lint/runner.js +1 -1
  82. package/dist/src/mcp/cli.mjs +5 -6
  83. package/dist/src/mcp/config-file.mjs +6 -2
  84. package/dist/src/mcp/config-layers.mjs +23 -7
  85. package/dist/src/mcp/profiles.js +18 -31
  86. package/dist/src/mcp/profiles.mjs +18 -32
  87. package/dist/src/mcp/registry.js +7 -27
  88. package/dist/src/mcp/registry.mjs +7 -28
  89. package/dist/src/mcp/server.mjs +34 -1
  90. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  91. package/dist/src/mcp/tools/command-run.mjs +3 -2
  92. package/dist/src/mcp/tools/decision.mjs +49 -48
  93. package/dist/src/mcp/tools/discovery.mjs +46 -21
  94. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  95. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  96. package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
  97. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  98. package/dist/src/memory/audit.js +719 -0
  99. package/dist/src/memory/canonical-context.js +392 -13
  100. package/dist/src/memory/canonical-maintenance.js +95 -0
  101. package/dist/src/memory/cli-result.js +22 -0
  102. package/dist/src/memory/cli.js +176 -0
  103. package/dist/src/memory/context-pack.js +57 -6
  104. package/dist/src/memory/cost.js +512 -0
  105. package/dist/src/memory/firewall-classification.d.mts +7 -0
  106. package/dist/src/memory/firewall-classification.mjs +32 -0
  107. package/dist/src/memory/index.js +8 -0
  108. package/dist/src/memory/line-maintenance.js +68 -0
  109. package/dist/src/memory/maintenance-cli.js +286 -0
  110. package/dist/src/memory/maintenance-contract.js +136 -0
  111. package/dist/src/memory/maintenance-index.js +18 -0
  112. package/dist/src/memory/maintenance-owner.js +2 -0
  113. package/dist/src/memory/maintenance-record.js +138 -0
  114. package/dist/src/memory/maintenance.js +254 -0
  115. package/dist/src/memory/provider-memory-adapter.js +154 -0
  116. package/dist/src/memory/recommend-cli.js +83 -0
  117. package/dist/src/memory/recommend.js +269 -0
  118. package/dist/src/memory/scheduled.js +81 -0
  119. package/dist/src/memory/taxonomy.js +44 -0
  120. package/dist/src/mission-protocol/codecs.js +19 -9
  121. package/dist/src/models/config-loader.js +11 -1
  122. package/dist/src/models/model-capabilities.v1.json +74 -50
  123. package/dist/src/models/provider-policy.js +125 -62
  124. package/dist/src/models/resolver.js +45 -8
  125. package/dist/src/models/worker-installation.d.mts +7 -0
  126. package/dist/src/models/worker-installation.mjs +169 -37
  127. package/dist/src/models/wrapper-route.js +1 -1
  128. package/dist/src/output-modes/registry.js +8 -7
  129. package/dist/src/plugin/metadata-validator.js +1 -1
  130. package/dist/src/providers/deploy-version.d.mts +1 -0
  131. package/dist/src/providers/deploy-version.mjs +17 -0
  132. package/dist/src/providers/provider-definitions.js +19 -16
  133. package/dist/src/providers/skill-ownership.d.mts +11 -2
  134. package/dist/src/providers/skill-ownership.mjs +35 -17
  135. package/dist/src/rules/activation.d.mts +11 -2
  136. package/dist/src/rules/activation.mjs +82 -6
  137. package/dist/src/rules/inventory.js +1 -1
  138. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  139. package/dist/src/schema/zod-validation.d.mts +6 -0
  140. package/dist/src/schema/zod-validation.mjs +50 -0
  141. package/dist/src/serve/executor-registry.js +297 -20
  142. package/dist/src/serve/executor-token.d.mts +1 -0
  143. package/dist/src/serve/executor-token.mjs +8 -0
  144. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  145. package/dist/src/serve/worker-invocation.js +114 -15
  146. package/dist/src/sessions/adapters/claude.js +19 -18
  147. package/dist/src/sessions/adapters/codex.js +10 -9
  148. package/dist/src/sessions/adapters/copilot.js +14 -13
  149. package/dist/src/sessions/adapters/cursor.js +19 -18
  150. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  151. package/dist/src/sessions/adapters/factory.js +11 -10
  152. package/dist/src/sessions/adapters/generic.js +32 -23
  153. package/dist/src/sessions/adapters/grokbot.js +35 -35
  154. package/dist/src/sessions/adapters/hermes.js +20 -19
  155. package/dist/src/sessions/adapters/muse.js +50 -49
  156. package/dist/src/sessions/adapters/omp.js +5 -4
  157. package/dist/src/sessions/adapters/openclaw.js +30 -29
  158. package/dist/src/sessions/adapters/opencode.js +26 -26
  159. package/dist/src/sessions/adapters/openhuman.js +24 -23
  160. package/dist/src/sessions/adapters/pi.js +5 -4
  161. package/dist/src/sessions/adapters/windsurf.js +8 -8
  162. package/dist/src/sessions/analytics.js +10 -9
  163. package/dist/src/sessions/candidates.js +7 -6
  164. package/dist/src/sessions/contracts.js +63 -53
  165. package/dist/src/sessions/output-registration.js +11 -10
  166. package/dist/src/sessions/workspace-discovery.js +3 -4
  167. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  168. package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
  169. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  170. package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
  171. package/dist/src/update/checker.d.mts +4 -0
  172. package/dist/src/update/checker.mjs +14 -0
  173. package/dist/src/writing/exemplar-selection.js +6 -6
  174. package/dist/src/writing/voice-evaluation.js +24 -23
  175. package/dist/src/writing/writer-migration.js +14 -13
  176. package/dist/src/writing/writer-profile.js +19 -18
  177. package/dist/src/writing/writing-brief.js +18 -17
  178. package/dist/src/writing/writing-receipt.js +47 -46
  179. package/package.json +5 -2
  180. package/schemas/decision/DecisionReview.schema.json +78 -1
  181. package/schemas/mission-v1.schema.json +3 -0
  182. package/tools/agents/deploy-agents.mjs +67 -39
  183. package/tools/agents/providers/antigravity.mjs +6 -1
  184. package/tools/agents/providers/base.mjs +19 -13
  185. package/tools/agents/providers/claude.mjs +1 -1
  186. package/tools/agents/providers/codex.mjs +50 -56
  187. package/tools/agents/providers/copilot.mjs +8 -2
  188. package/tools/agents/providers/cursor.mjs +1 -1
  189. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  190. package/tools/agents/providers/factory.mjs +7 -1
  191. package/tools/agents/providers/grok-build.mjs +18 -2
  192. package/tools/agents/providers/grokbot.mjs +1 -1
  193. package/tools/agents/providers/hermes.mjs +7 -13
  194. package/tools/agents/providers/muse-hooks.mjs +21 -14
  195. package/tools/agents/providers/muse.mjs +7 -2
  196. package/tools/agents/providers/omp.mjs +28 -4
  197. package/tools/agents/providers/opencode.mjs +14 -5
  198. package/tools/agents/providers/openhuman.mjs +1 -1
  199. package/tools/agents/providers/pi.mjs +1 -1
  200. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  201. package/tools/agents/providers/rule-delivery.mjs +263 -71
  202. package/tools/agents/providers/windsurf.mjs +7 -0
  203. package/tools/cli/workspace-status.mjs +5 -0
  204. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  205. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  206. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  207. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  208. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  209. package/tools/ralph-external/process-monitor.mjs +7 -4
  210. package/tools/ralph-external/session-launcher.mjs +280 -32
  211. package/tools/skills/deploy-skills-codex.mjs +11 -7
  212. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -1,7 +1,7 @@
1
1
  /** Shared installed-worker qualification owner, also used by the Ralph compatibility facade. */
2
2
  import { spawn } from 'node:child_process';
3
3
  import { constants } from 'node:fs';
4
- import { open, realpath, lstat, mkdir, rename, writeFile } from 'node:fs/promises';
4
+ import { open, realpath, lstat, mkdir, rename, writeFile, unlink } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import { homedir } from 'node:os';
7
7
  import { pathToFileURL } from 'node:url';
@@ -163,7 +163,7 @@ export async function qualifyWorkerInstallation(instance, options) {
163
163
  }
164
164
 
165
165
  export async function prepareInstalledWorkerInvocation(instance, invocation, qualification) {
166
- const { prepareWorkerInvocation, WorkerPathScopeError } = await import('../serve/worker-invocation.js');
166
+ const { prepareWorkerInvocation, WorkerPathScopeError, WorkerInvocationCheckError } = await import('../serve/worker-invocation.js');
167
167
  const { requiredWorkerControls, WorkerPolicyError, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
168
168
  if (!isOperatorQualifiedWorkerAdapter(qualification.adapter)) qualification = { ...qualification, requiredControls: requiredWorkerControls({
169
169
  ...qualification, effort: invocation.selected.effort.value }) };
@@ -187,7 +187,7 @@ export async function prepareInstalledWorkerInvocation(instance, invocation, qua
187
187
  throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Worker installation qualification changed');
188
188
  }
189
189
  if (await realpath(result.cwd) !== scope.cwd) {
190
- throw new WorkerPolicyError('OG_CONFIG_REFERENCE', 'Worker cwd differs from the qualified workspace');
190
+ throw new WorkerInvocationCheckError('OG_CONFIG_REFERENCE', 'qualified_workspace_mismatch');
191
191
  }
192
192
  for (const key of ['briefPath', 'stdoutPath', 'stderrPath', 'finalPath']) {
193
193
  const target = await canonicalFuturePath(result[key]);
@@ -443,6 +443,19 @@ export function boundedVersionProbe(executable, argv, environment, cwd, timeoutM
443
443
  || !Array.isArray(argv) || argv.some(arg => typeof arg !== 'string' || arg.includes('\0'))) {
444
444
  throw new Error('Invalid probe bounds');
445
445
  }
446
+ // ETXTBSY: the executable still has a writer somewhere, typically a sibling
447
+ // process forked while the file was being written or a CLI mid-update. It
448
+ // clears in milliseconds, so retry the spawn within the same overall bound.
449
+ const deadline = Date.now() + timeoutMs;
450
+ const attempt = delayMs => runBoundedVersionProbe(executable, argv, environment, cwd, timeoutMs, maxBytes, versionPattern)
451
+ .catch(error => {
452
+ if (error?.code !== 'ETXTBSY' || Date.now() + delayMs >= deadline) throw error;
453
+ return new Promise(resolveDelay => setTimeout(resolveDelay, delayMs)).then(() => attempt(Math.min(delayMs * 2, 200)));
454
+ });
455
+ return attempt(10);
456
+ }
457
+
458
+ function runBoundedVersionProbe(executable, argv, environment, cwd, timeoutMs, maxBytes, versionPattern) {
446
459
  return new Promise((resolve, reject) => {
447
460
  const child = spawn(executable, argv, { cwd, env: environment, stdio: 'pipe', detached: true, shell: false });
448
461
  let output = Buffer.alloc(0);
@@ -663,7 +676,9 @@ async function loadQualifiedModules({ registry, modules, workerWritableRoots, pl
663
676
  for (const id of entry.adapterIds) {
664
677
  const builtinConflict = canonicalIds.has(id) ? false : providerRegistry.has(id);
665
678
  if (ids.has(id) || ((builtinConflict || installedModules.has(inspectedModule.key))
666
- && installedModules.get(id) !== inspectedModule.key)) throw new Error(`Duplicate worker adapter ID: ${id}`);
679
+ && installedModules.get(id) !== inspectedModule.key)) {
680
+ throw Object.assign(new Error(`Duplicate worker adapter ID: ${id}`), { code: 'WORKER_ADAPTER_CONFLICT' });
681
+ }
667
682
  ids.add(id);
668
683
  }
669
684
  inspected.push(inspectedModule);
@@ -825,6 +840,37 @@ async function readInstallationStore(filename, expectedDigest) {
825
840
  }
826
841
  }
827
842
 
843
+ /** Offline projection of recorded approvals only. Never imports drivers, reads auth, or probes CLIs.
844
+ * This is historical evidence, not current launch authority; loadRecordedWorkerInstallations
845
+ * performs binary/version/scope checks when dispatch actually requests a binding.
846
+ */
847
+ export async function inspectRecordedWorkerQualifications({ cwd, policy, parallelism }) {
848
+ const filename = installationStorePath();
849
+ await assertTrustedStore(filename, [cwd]);
850
+ const { stored } = await readInstallationStore(filename);
851
+ const workspace = await realpath(cwd);
852
+ const results = [];
853
+ const { qualificationDigest } = await import('./qualification-digest.mjs');
854
+ for (const record of stored.registrations) {
855
+ try {
856
+ const { cli, installation, adapter, binding } = record;
857
+ const configured = parallelism?.clis?.[cli];
858
+ if (!configured || configured.enabled === false || !/^[a-z][a-z0-9-]{0,62}$/.test(cli)
859
+ || await realpath(installation.cwd) !== workspace
860
+ || qualificationDigest(policy) !== installation.effectiveConfigDigest
861
+ || configured.adapter !== `providers:${adapter.provider}` && configured.adapter !== adapter.id
862
+ || configured.accountScope !== binding.accountScope
863
+ || binding.profile !== installation.profile || binding.cliVersion !== installation.cliVersion
864
+ || (configured.executable && await realpath(configured.executable) !== await realpath(installation.executable))) continue;
865
+ const proof = adapter.invocationProfiles?.[binding.profile]?.installationQualification;
866
+ if (!proof || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef'].some(key => proof[key] !== installation.evidence?.[key])) continue;
867
+ await assertWorkerInstallationEvidence({ ...installation, adapter, now: Date.now(), policy }, proof.scopeDigest);
868
+ results.push({ cli, provider: adapter.provider });
869
+ } catch { /* Malformed, expired or unbound evidence is not qualification. */ }
870
+ }
871
+ return results.filter(row => results.filter(other => row.cli === other.cli).length === 1);
872
+ }
873
+
828
874
  /** Explicit host operation: successful bounded qualification is required before
829
875
  * recording; credentials are reduced to names and are never written to disk. */
830
876
  export async function recordWorkerInstallation(instance, options) {
@@ -842,79 +888,154 @@ export async function recordWorkerInstallation(instance, options) {
842
888
  || binding.accountScope !== installation.accountScope) throw new Error('Installation binding differs from qualification');
843
889
  const filename = installationStorePath();
844
890
  await assertTrustedStore(filename, installation.workerWritableRoots);
845
- const { stored } = await readInstallationStore(filename);
846
- const environmentNames = Object.keys(installation.environment ?? {});
847
- delete installation.environment;
848
- delete installation.adapter;
849
- delete installation.now;
850
- const record = { cli, ...(options.modelObservations ? { modelObservations: structuredClone(options.modelObservations) } : {}), binding: { ...structuredClone(binding), boundary: installation.boundary }, adapter,
851
- installation: { ...installation, environmentNames, versionProbe: { rawBanner: result.rawVersionBanner, version: result.cliVersion } },
852
- ...(options.mcpSchemas ? { mcpSchemas: structuredClone(options.mcpSchemas) } : {}),
853
- ...(options.mcpProfile ? { mcpProfile: structuredClone(options.mcpProfile) } : {}) };
854
- stored.registrations = stored.registrations.filter(entry => entry.cli !== cli);
855
- stored.registrations.push(record);
891
+ // Canonical workspace identity is an exact approval key, never a prefix.
892
+ installation.cwd = await realpath(installation.cwd);
856
893
  await mkdir(path.dirname(filename), { recursive: true, mode: 0o700 });
857
- await assertTrustedStore(filename, installation.workerWritableRoots);
858
- const temporary = `${filename}.${process.pid}.${createHash('sha256').update(String(Math.random())).digest('hex')}.tmp`;
859
- await writeFile(temporary, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
860
- await rename(temporary, filename);
861
- return result;
894
+ // Serialize read/modify/rename across qualifying CLI processes. A stale lock
895
+ // fails closed; qualification never steals another process's lock.
896
+ const lockPath = `${filename}.lock`;
897
+ let lock;
898
+ for (let attempt = 0; attempt < 100; attempt++) {
899
+ try { lock = await open(lockPath, 'wx', 0o600); break; }
900
+ catch (error) {
901
+ if (error.code !== 'EEXIST' || attempt === 99) throw error;
902
+ await new Promise(resolve => setTimeout(resolve, 20));
903
+ }
904
+ }
905
+ try {
906
+ await assertTrustedStore(filename, installation.workerWritableRoots);
907
+ const { stored } = await readInstallationStore(filename);
908
+ const environmentNames = Object.keys(installation.environment ?? {});
909
+ delete installation.environment;
910
+ delete installation.adapter;
911
+ delete installation.now;
912
+ const record = { cli, ...(options.modelObservations ? { modelObservations: structuredClone(options.modelObservations) } : {}), binding: { ...structuredClone(binding), boundary: installation.boundary }, adapter,
913
+ installation: { ...installation, environmentNames, versionProbe: { rawBanner: result.rawVersionBanner, version: result.cliVersion } },
914
+ ...(options.mcpSchemas ? { mcpSchemas: structuredClone(options.mcpSchemas) } : {}),
915
+ ...(options.mcpProfile ? { mcpProfile: structuredClone(options.mcpProfile) } : {}) };
916
+ const retained = [];
917
+ for (const entry of stored.registrations) {
918
+ const workspace = typeof entry.installation?.cwd === 'string'
919
+ ? await realpath(entry.installation.cwd).catch(() => entry.installation.cwd) : null;
920
+ if (entry.cli !== cli || workspace !== installation.cwd) retained.push(entry);
921
+ }
922
+ // Bound host authority without silently evicting another workspace approval.
923
+ if (retained.length >= 128) throw new Error('Worker installation store is full (128 workspace approvals); remove an obsolete approval before qualifying another workspace');
924
+ stored.registrations = retained;
925
+ stored.registrations.push(record);
926
+ await mkdir(path.dirname(filename), { recursive: true, mode: 0o700 });
927
+ await assertTrustedStore(filename, installation.workerWritableRoots);
928
+ const temporary = `${filename}.${process.pid}.${createHash('sha256').update(String(Math.random())).digest('hex')}.tmp`;
929
+ await writeFile(temporary, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
930
+ await rename(temporary, filename);
931
+ return result;
932
+ } finally {
933
+ await lock.close();
934
+ await unlink(lockPath);
935
+ }
862
936
  }
863
937
 
864
938
  /** Read recorded host authority, validate installation bytes and the current CLI
865
939
  * version, then load the same approved factory used by Ralph. */
866
- export async function loadRecordedWorkerInstallations({ registry, cwd, policy, parallelism, storePath, storeDigest }) {
940
+ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, parallelism, storePath, storeDigest, precheckPolicy }) {
867
941
  const { WorkerPolicyError, parseProviderModelCapabilities } = await import('./provider-policy.js');
868
942
  const { qualificationDigest } = await import('./qualification-digest.mjs');
869
- const fail = () => new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED', 'Trusted installation qualification is required');
870
- const filename = storePath ?? installationStorePath();
943
+ // Only fixed stage codes and bounded record indexes may leave this boundary.
944
+ // Underlying errors can contain probe output, environment values or paths.
945
+ const rejections = [];
946
+ const fail = () => new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED',
947
+ `Trusted installation qualification is required (${[...new Set(rejections.map(row => row.reason))].join(', ')})`,
948
+ { rejections });
949
+ let reason = 'store-binding';
950
+ let filename;
871
951
  let stored, digest;
872
952
  try {
953
+ filename = storePath ?? installationStorePath();
873
954
  if (!path.isAbsolute(filename) || (storePath !== undefined && !/^sha256:[a-f0-9]{64}$/.test(storeDigest ?? ''))) throw fail();
955
+ reason = 'store-untrusted';
874
956
  await assertTrustedStore(filename, [cwd]);
957
+ reason = 'store-record';
875
958
  ({ stored, digest } = await readInstallationStore(filename, storeDigest));
876
959
  }
877
- catch { throw fail(); }
960
+ catch { rejections.push({ registration: null, reason }); throw fail(); }
961
+ if (!stored.registrations.length) {
962
+ rejections.push({ registration: null, reason: 'no-records' });
963
+ throw fail();
964
+ }
965
+ // Request policy errors remain actionable before digest/adapter checks,
966
+ // while an absent approval retains the qualification contract (#3070).
967
+ precheckPolicy?.();
878
968
  const installations = {};
879
969
  const drivers = {};
880
970
  const observations = {};
881
971
  const resolvedRegistry = structuredClone(registry);
972
+ const requestedWorkspace = await realpath(cwd);
973
+ const workspaces = await Promise.all(stored.registrations.map(async record => {
974
+ try { return await realpath(record?.installation?.cwd); } catch { return null; }
975
+ }));
882
976
  const counts = new Map();
883
- for (const record of stored.registrations) counts.set(record?.cli, (counts.get(record?.cli) ?? 0) + 1);
884
- for (const record of stored.registrations) {
977
+ const selected = new Map();
978
+ for (const [index, record] of stored.registrations.entries()) {
979
+ const key = JSON.stringify([record?.cli, workspaces[index]]);
980
+ counts.set(key, (counts.get(key) ?? 0) + 1);
981
+ // Prefer an exact approval. Retain the latest other-workspace record only
982
+ // to preserve the typed workspace-mismatch diagnostic for unapproved dirs.
983
+ const prior = selected.get(record?.cli);
984
+ if (prior === undefined || workspaces[prior] !== requestedWorkspace) selected.set(record?.cli, index);
985
+ }
986
+ for (const [registration, record] of stored.registrations.entries()) {
987
+ reason = 'record-identity';
885
988
  try {
886
989
  const { cli, binding, adapter } = record;
887
- if (!/^[a-z][a-z0-9-]{0,62}$/.test(cli) || counts.get(cli) !== 1) throw fail();
990
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(cli)) throw fail();
991
+ if (counts.get(JSON.stringify([cli, workspaces[registration]])) !== 1) throw fail();
992
+ if (selected.get(cli) !== registration) continue;
888
993
  const installation = { ...record.installation, adapter, now: Date.now(), environment: {} };
994
+ reason = 'environment-name';
889
995
  if (record.installation.environment && Object.keys(record.installation.environment).length) throw fail();
890
996
  for (const name of installation.environmentNames ?? []) {
891
997
  if (typeof name !== 'string' || process.env[name] === undefined) throw fail();
892
998
  installation.environment[name] = process.env[name];
893
999
  }
1000
+ reason = 'binding-mismatch';
894
1001
  if (binding.profile !== installation.profile || binding.cliVersion !== installation.cliVersion
895
1002
  || binding.accountScope !== installation.accountScope
896
- || qualificationDigest(binding.boundary) !== qualificationDigest(installation.boundary)
897
- || qualificationDigest(policy) !== installation.effectiveConfigDigest) throw fail();
1003
+ || qualificationDigest(binding.boundary) !== qualificationDigest(installation.boundary)) throw fail();
1004
+ reason = 'effective-config-digest';
1005
+ if (qualificationDigest(policy) !== installation.effectiveConfigDigest) throw fail();
1006
+ reason = 'evidence';
898
1007
  const proof = adapter.invocationProfiles?.[binding.profile]?.installationQualification;
899
1008
  if (!proof || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef'].some(key => proof[key] !== installation.evidence?.[key])) throw fail();
900
1009
  // Reject expired/unbound negative observations before any factory import.
901
1010
  const { requiredWorkerControls, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
902
1011
  if (!isOperatorQualifiedWorkerAdapter(adapter)) installation.requiredControls = requiredWorkerControls(installation);
903
1012
  await assertWorkerInstallationEvidence(installation, proof.scopeDigest);
1013
+ reason = 'store-untrusted';
904
1014
  await assertTrustedStore(filename, installation.workerWritableRoots);
1015
+ reason = 'adapter-metadata';
905
1016
  const candidateRegistry = structuredClone(resolvedRegistry);
906
1017
  if (candidateRegistry.providers[adapter.id]) candidateRegistry.providers[adapter.id].workerAdapter = adapter;
907
1018
  else { candidateRegistry.workerAdapters ??= {}; candidateRegistry.workerAdapters[adapter.id] = adapter; }
908
1019
  parseProviderModelCapabilities(candidateRegistry);
909
1020
  // The loader validates package identity, bytes and static exports BEFORE import.
910
1021
  // Only this installation's metadata is needed; unrelated absent modules do not block it.
911
- const loadingRegistry = structuredClone(registry);
912
- for (const provider of Object.values(loadingRegistry.providers)) delete provider.workerAdapter;
913
- loadingRegistry.workerAdapters = { [adapter.id]: adapter };
1022
+ // Preserve the canonical provider slot used to distinguish upgrades from ID conflicts.
1023
+ const loadingRegistry = structuredClone(candidateRegistry);
1024
+ const canonical = loadingRegistry.providers[adapter.id]?.workerAdapter?.id === adapter.id;
1025
+ for (const [name, provider] of Object.entries(loadingRegistry.providers)) {
1026
+ if (canonical && name === adapter.id) provider.workerAdapter = adapter;
1027
+ else delete provider.workerAdapter;
1028
+ }
1029
+ loadingRegistry.workerAdapters = canonical ? {} : { [adapter.id]: adapter };
1030
+ reason = 'module-registration';
914
1031
  await loadInstalledWorkerAdapters({ registry: loadingRegistry, modules: installation.modules,
915
1032
  workerWritableRoots: installation.workerWritableRoots, platform: process.platform });
1033
+ reason = 'binary-identity';
916
1034
  const instance = createProvider(adapter.id);
917
- const { scope } = await recheckWorkerInstallation(instance, installation);
1035
+ const scope = await inspectWorkerInstallation(instance, installation);
1036
+ reason = 'scope-digest';
1037
+ await assertWorkerInstallationEvidence(installation, scope.scopeDigest);
1038
+ reason = 'binary-identity';
918
1039
  if (parallelism && isOperatorQualifiedWorkerAdapter(adapter)) {
919
1040
  const { resolveWorkerGroupInstallation } = await import('../config/aiwg-config.js');
920
1041
  const configured = resolveWorkerGroupInstallation(parallelism, cli, candidateRegistry);
@@ -931,15 +1052,19 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
931
1052
  }
932
1053
  if (!executable || await realpath(executable) !== scope.binary.realpath) throw fail();
933
1054
  }
1055
+ reason = 'scope-digest';
934
1056
  if (scope.scopeDigest !== proof.scopeDigest) throw fail();
935
1057
  // A launcher can keep the same bytes while its CLI payload is updated.
936
1058
  // Reuse the bounded, credential-free doctor probe before granting authority.
1059
+ reason = 'version';
937
1060
  const { version } = await boundedVersionProbe(scope.binary.realpath,
938
1061
  adapter.invocation.versionArgv, workerVersionProbeEnvironment(), cwd,
939
1062
  workerVersionProbeDefaults.timeoutMs, workerVersionProbeDefaults.maxBytes,
940
1063
  adapter.invocation.versionPattern);
941
- if (version !== binding.cliVersion
942
- || (await inspectWorkerInstallation(instance, installation)).scopeDigest !== scope.scopeDigest) throw fail();
1064
+ if (version !== binding.cliVersion) throw fail();
1065
+ reason = 'scope-digest';
1066
+ if ((await inspectWorkerInstallation(instance, installation)).scopeDigest !== scope.scopeDigest) throw fail();
1067
+ reason = 'driver';
943
1068
  const profile = adapter.invocationProfiles[binding.profile];
944
1069
  let mcpRegistry;
945
1070
  if (profile.transport === 'mcp') {
@@ -979,8 +1104,15 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
979
1104
  installations[cli] = structuredClone(binding);
980
1105
  if (record.modelObservations) observations[cli] = structuredClone(record.modelObservations);
981
1106
  Object.assign(resolvedRegistry, candidateRegistry);
982
- } catch { /* One missing/changed installation cannot grant authority or hide qualified peers. */ }
1107
+ } catch (error) {
1108
+ // One rejected installation cannot grant authority or hide qualified peers.
1109
+ rejections.push({ registration, reason: reason === 'module-registration' && error?.code === 'WORKER_ADAPTER_CONFLICT'
1110
+ ? 'adapter-conflict' : reason });
1111
+ }
1112
+ }
1113
+ if (!Object.keys(installations).length) {
1114
+ if (!rejections.length) rejections.push({ registration: null, reason: 'no-records' });
1115
+ throw fail();
983
1116
  }
984
- if (!Object.keys(installations).length) throw fail();
985
1117
  return { registry: resolvedRegistry, installations, drivers, observations };
986
1118
  }
@@ -47,7 +47,7 @@ export function buildWrapperRouteEnvelope(request) {
47
47
  ? [
48
48
  `Run this assignment through ${wrapper}.`,
49
49
  `Selected capability: ${request.capability.type} ${request.capability.name} (${request.capability.id}).`,
50
- `Load it with aiwg show ${request.capability.type === 'workflow' ? 'metadata' : request.capability.type} ${request.capability.id} after discover-first validation.`,
50
+ `Load it with aiwg show ${request.capability.type === 'workflow' ? 'metadata' : request.capability.type} ${request.capability.id}${request.capability.type === 'workflow' ? '' : ' --body'} after discover-first validation.`,
51
51
  `Assignment: ${request.assignment.trim()}`,
52
52
  'Return capability evidence, changed artifacts, validation results, and remaining risks.',
53
53
  ].join('\n')
@@ -5,6 +5,7 @@ import { createHash } from 'node:crypto';
5
5
  import { dirname, extname, join, resolve } from 'node:path';
6
6
  import { parse, stringify } from 'yaml';
7
7
  import { z } from 'zod';
8
+ import { integer } from '../schema/zod-validation.mjs';
8
9
  import { getWritingChannelPack } from '../writing/channel-packs.js';
9
10
  import { WriterProfileStore } from '../writing/writer-profile-store.js';
10
11
  import { compileWriterProfile } from '../writing/writer-profile.js';
@@ -14,28 +15,28 @@ const PROTECTED = ['code', 'commands', 'citations', 'quoted-text', 'identifiers'
14
15
  const STAGE_ORDER = ['semantic', 'voice', 'controlled-language', 'structure', 'presentation'];
15
16
  const modeIdSchema = z.string().regex(MODE_ID_PATTERN, 'must start with a lowercase letter or number and contain only lowercase letters, numbers, dots, or hyphens');
16
17
  const uniqueModeIds = z.array(modeIdSchema).refine(values => new Set(values).size === values.length, 'must not contain duplicates');
17
- const outputModeProfileSchema = z.object({
18
+ const outputModeProfileSchema = z.strictObject({
18
19
  id: modeIdSchema,
19
20
  version: z.string().min(1),
20
21
  description: z.string().min(1),
21
22
  kind: z.enum(['voice', 'controlled-language', 'structure', 'presentation']),
22
23
  stage: z.enum(STAGE_ORDER),
23
- order: z.number().int().optional(),
24
+ order: integer().optional(),
24
25
  instructions: z.string(),
25
- provenance: z.object({ source: z.string().min(1), license: z.string().min(1) }).strict(),
26
- validation: z.object({
26
+ provenance: z.strictObject({ source: z.string().min(1), license: z.string().min(1) }),
27
+ validation: z.strictObject({
27
28
  level: z.enum(['advisory', 'validated', 'conformance']),
28
29
  hook: z.string().min(1).optional(),
29
30
  standardVersion: z.string().min(1).optional(),
30
- }).strict(),
31
+ }),
31
32
  compatible: uniqueModeIds.optional(),
32
33
  conflicts: uniqueModeIds.optional(),
33
34
  requires: uniqueModeIds.optional(),
34
35
  supersedes: uniqueModeIds.optional(),
35
36
  protectedContent: z.array(z.enum(PROTECTED)).refine(values => new Set(values).size === values.length, 'must not contain duplicates').optional(),
36
- contextCost: z.number().int().nonnegative().optional(),
37
+ contextCost: integer().nonnegative().optional(),
37
38
  mergeStrategy: z.literal('weighted-voice').optional(),
38
- }).strict().superRefine((profile, ctx) => {
39
+ }).superRefine((profile, ctx) => {
39
40
  if (profile.validation.level !== 'advisory' && !profile.validation.hook) {
40
41
  ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['validation', 'hook'], message: `${profile.validation.level} modes require a validator hook` });
41
42
  }
@@ -177,7 +177,7 @@ export class MetadataValidator {
177
177
  result.manifest = manifest;
178
178
  }
179
179
  else {
180
- for (const issue of skillResult.errors.errors) {
180
+ for (const issue of skillResult.errors.issues) {
181
181
  result.errors.push({
182
182
  path: filename,
183
183
  field: issue.path.join('.'),
@@ -0,0 +1 @@
1
+ export function resolveDeployVersion(root: string, explicitVersion?: string | null, warn?: (message: string) => void): string;
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /** Resolve the selected corpus version before provider inputs are staged. */
5
+ export function resolveDeployVersion(root, explicitVersion, warn = console.warn) {
6
+ const valid = value => typeof value === 'string' && value.trim() &&
7
+ !/unknown|\s/i.test(value);
8
+ if (valid(explicitVersion)) return explicitVersion;
9
+ if (!explicitVersion) {
10
+ try {
11
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
12
+ if (valid(pkg.version)) return pkg.version;
13
+ } catch { /* Report missing, unreadable and invalid versions consistently. */ }
14
+ }
15
+ warn(`Unable to resolve AIWG deploy version from ${path.join(root, 'package.json')}; managed markers will use vunknown. Restore the package version or pass --deploy-version to the deployer.`);
16
+ return 'unknown';
17
+ }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
+ import { integer, url } from '../schema/zod-validation.mjs';
2
3
  import { homedir } from 'os';
3
- import { join } from 'path';
4
+ import { isAbsolute, join } from 'path';
4
5
  import { resolveHermesHomePath } from './hermes-home.js';
5
6
  import { resolveGrokbotSkillsDir } from './grokbot-paths.js';
6
7
  import { getProviderCapabilities, } from './capability-matrix.js';
@@ -38,9 +39,9 @@ const ProviderDefinitionSchema = z.object({
38
39
  status: z.enum(['stable', 'experimental', 'deprecated']),
39
40
  builtIn: z.boolean(),
40
41
  upstream: z.object({
41
- source: z.string().url(),
42
+ source: url(),
42
43
  version: z.string().min(1),
43
- revision: z.string().regex(/^[0-9a-f]{40}$/).refine((value) => value !== '0'.repeat(40), { message: 'upstream.revision must be a real reviewed SHA, not all-zero' }),
44
+ revision: z.string().regex(/^[0-9a-f]{40}$/).refine((value) => value !== '0'.repeat(40), { error: 'upstream.revision must be a real reviewed SHA, not all-zero' }),
44
45
  runtime: z.string().min(1),
45
46
  lastVerified: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
46
47
  }).optional(),
@@ -67,7 +68,7 @@ const ProviderDefinitionSchema = z.object({
67
68
  env: z.array(z.string().min(1)),
68
69
  process: z.array(z.string().min(1)),
69
70
  capabilityId: z.string().min(1),
70
- runtimeEnvPriority: z.number().int().positive().optional(),
71
+ runtimeEnvPriority: integer().positive().optional(),
71
72
  }),
72
73
  paths: z.object({
73
74
  deployTarget: z.enum(['project', 'home', 'mixed']),
@@ -95,8 +96,8 @@ const ProviderDefinitionSchema = z.object({
95
96
  includeSyntax: z.string().nullable(),
96
97
  configRegistration: z.object({ file: z.string().min(1), key: z.string().min(1) }).nullable(),
97
98
  bootstrapTargets: z.array(z.string().min(1)),
98
- maxContextBytes: z.number().int().positive().nullable(),
99
- recommendedMaxLines: z.number().int().positive().nullable(),
99
+ maxContextBytes: integer().positive().nullable(),
100
+ recommendedMaxLines: integer().positive().nullable(),
100
101
  nestedContext: z.boolean(),
101
102
  support: z.enum(['supported', 'degraded', 'unsupported']),
102
103
  verification: z.object({
@@ -119,8 +120,8 @@ const ProviderDefinitionSchema = z.object({
119
120
  pathType: z.enum(['project', 'home-dir']),
120
121
  skillsBaseDir: z.string().min(1),
121
122
  subdirLayout: z.boolean(),
122
- maxNameLength: z.number().int().positive().optional(),
123
- maxDescriptionLength: z.number().int().positive().optional(),
123
+ maxNameLength: integer().positive().optional(),
124
+ maxDescriptionLength: integer().positive().optional(),
124
125
  appendToDescription: z.string().min(1).optional(),
125
126
  }),
126
127
  adapters: z.object({
@@ -132,8 +133,8 @@ const ProviderDefinitionSchema = z.object({
132
133
  }),
133
134
  capabilities: z.object({
134
135
  matrixRef: z.string().nullable(),
135
- nativeFeatures: z.record(z.boolean()),
136
- emulation: z.record(z.string().nullable()),
136
+ nativeFeatures: z.record(z.string(), z.boolean()),
137
+ emulation: z.record(z.string(), z.string().nullable()),
137
138
  }),
138
139
  });
139
140
  export const PROVIDER_IDS = [
@@ -755,10 +756,11 @@ const BUILT_IN_SEEDS = [
755
756
  agents: null,
756
757
  commands: null,
757
758
  skills: resolveHermesHomePath('skills', '.aiwg'),
758
- rules: null,
759
+ rules: '.hermes/rules',
759
760
  behaviors: null,
760
761
  },
761
762
  kernelSkills: resolveHermesHomePath('skills'),
763
+ contextDiscovery: { agents: null, skills: resolveHermesHomePath('skills', '.aiwg'), rules: null, behaviors: null },
762
764
  configFile: 'AGENTS.md',
763
765
  contextFiles: { aiwgMd: true, agentsMd: true, claudeMdHook: false, hookFile: '.hermes.md', contextFile: 'AGENTS.md' },
764
766
  },
@@ -1080,7 +1082,7 @@ const BUILT_IN_SEEDS = [
1080
1082
  agents: '.agents/skills',
1081
1083
  commands: '.pi/prompts',
1082
1084
  skills: '.pi/.aiwg/skills',
1083
- rules: null,
1085
+ rules: '.pi/rules',
1084
1086
  behaviors: '.pi/extensions',
1085
1087
  },
1086
1088
  kernelSkills: '.agents/skills',
@@ -1425,13 +1427,14 @@ export function getProviderKernelSkillPath(provider) {
1425
1427
  const definition = getProviderDefinition(provider);
1426
1428
  return expandProviderHomePath(definition?.paths.kernelSkills ?? null);
1427
1429
  }
1428
- export function resolveProviderPathValue(providerPath, projectPath) {
1429
- const expandedPath = expandProviderHomePath(providerPath);
1430
+ export function resolveProviderPathValue(providerPath, projectPath, paths = { join, isAbsolute }, homeDirectory = homedir()) {
1431
+ const expandedPath = !providerPath ? '' : providerPath === '~' ? homeDirectory
1432
+ : providerPath.startsWith('~/') ? paths.join(homeDirectory, providerPath.slice(2)) : providerPath;
1430
1433
  if (!expandedPath)
1431
1434
  return '';
1432
- if (expandedPath.startsWith('/'))
1435
+ if (paths.isAbsolute(expandedPath))
1433
1436
  return expandedPath;
1434
- return join(projectPath, expandedPath);
1437
+ return paths.join(projectPath, expandedPath);
1435
1438
  }
1436
1439
  export function normalizeProviderDefinitionId(provider) {
1437
1440
  const normalized = provider?.trim().toLowerCase();
@@ -2,8 +2,17 @@ export interface SkillReceipt {
2
2
  skillSource?: string;
3
3
  managed?: Record<string, { hash: string; frameworkSlug?: string; source?: string; version?: string }>;
4
4
  }
5
+ export interface SkillRemovalPlanEntry {
6
+ path: string;
7
+ action: 'remove' | 'preserve';
8
+ reason: string;
9
+ }
10
+ export interface SkillRemovalOptions {
11
+ dryRun?: boolean;
12
+ plan?: SkillRemovalPlanEntry[];
13
+ }
5
14
  export function extractFrameworkSlug(source: string): string | null;
6
15
  export function sameSkillOwner(receipt: SkillReceipt | null, skillSource: string, frameworkSlug?: string | null): boolean;
7
16
  export function canDeploySkill(receipt: SkillReceipt | null, skillSource: string, opts?: { force?: boolean }): boolean;
8
- export function pruneRecordedSkillFiles(skillDir: string, managed: SkillReceipt['managed'], desired: Set<string>, opts?: { dryRun?: boolean }): void;
9
- export function releaseSkillOwnership(skillDir: string, owner: { frameworkSlug?: string; skillSource?: string; sourceRoot?: string }, opts?: { dryRun?: boolean }): boolean | null;
17
+ export function pruneRecordedSkillFiles(skillDir: string, managed: SkillReceipt['managed'], desired: Set<string>, opts?: SkillRemovalOptions): void;
18
+ export function releaseSkillOwnership(skillDir: string, owner: { frameworkSlug?: string; skillSource?: string; sourceRoot?: string }, opts?: SkillRemovalOptions): boolean | null;
@@ -40,28 +40,40 @@ export function canDeploySkill(receipt, skillSource, { force = false } = {}) {
40
40
 
41
41
  /** Remove unchanged recorded files, allowing symlinks above (but never inside) the skill root. */
42
42
  export function pruneRecordedSkillFiles(skillDir, managed, desired, opts = {}) {
43
- if (opts.dryRun) return;
43
+ const root = path.resolve(skillDir);
44
44
  for (const [relative, record] of Object.entries(managed || {})) {
45
- if (desired.has(relative)) continue;
46
45
  const target = path.resolve(skillDir, relative);
47
- if (!target.startsWith(`${path.resolve(skillDir)}${path.sep}`)) continue;
46
+ let action = 'preserve';
47
+ let reason = 'modified or unverifiable managed file';
48
+ if (desired.has(relative)) reason = 'retained managed file';
49
+ else if (!target.startsWith(`${root}${path.sep}`)) reason = 'path outside skill directory';
50
+ else {
51
+ try {
52
+ const expected = path.resolve(fs.realpathSync(skillDir), relative);
53
+ let current = target;
54
+ let symlink = false;
55
+ while (current !== root) {
56
+ if (fs.lstatSync(current).isSymbolicLink()) { symlink = true; break; }
57
+ current = path.dirname(current);
58
+ }
59
+ if (symlink) reason = 'symlink preserved';
60
+ else if (fs.realpathSync(target) === expected && fs.lstatSync(target).isFile()
61
+ && `sha256:${createHash('sha256').update(fs.readFileSync(target)).digest('hex')}` === record?.hash) {
62
+ action = 'remove';
63
+ reason = 'unchanged managed file';
64
+ }
65
+ } catch { reason = 'missing or unreadable path; cannot verify'; }
66
+ }
67
+ opts.plan?.push({ path: target, action, reason });
68
+ if (opts.dryRun || action !== 'remove') continue;
48
69
  try {
49
- const expected = path.resolve(fs.realpathSync(skillDir), relative);
50
- let current = target;
51
- let symlink = false;
52
- while (current !== path.resolve(skillDir)) {
53
- if (fs.lstatSync(current).isSymbolicLink()) { symlink = true; break; }
54
- current = path.dirname(current);
55
- }
56
- if (symlink || fs.realpathSync(target) !== expected || !fs.lstatSync(target).isFile()) continue;
57
- if (`sha256:${createHash('sha256').update(fs.readFileSync(target)).digest('hex')}` !== record.hash) continue;
58
70
  fs.unlinkSync(target);
59
71
  let parent = path.dirname(target);
60
- while (parent !== path.resolve(skillDir)) {
72
+ while (parent !== root) {
61
73
  try { fs.rmdirSync(parent); } catch { break; }
62
74
  parent = path.dirname(parent);
63
75
  }
64
- } catch { /* missing, modified, or unreadable files are left alone */ }
76
+ } catch { /* Files that cannot be removed are left alone. */ }
65
77
  }
66
78
  }
67
79
 
@@ -80,9 +92,15 @@ export function releaseSkillOwnership(skillDir, owner, opts = {}) {
80
92
  && !path.isAbsolute(path.relative(path.resolve(owner.sourceRoot), path.resolve(receipt.skillSource)));
81
93
  if (!insideRoot && (slug ? slug !== owner.frameworkSlug
82
94
  : !owner.skillSource || !sameSkillOwner(receipt, owner.skillSource, null))) return null;
83
- if (opts.dryRun) return false;
84
95
  pruneRecordedSkillFiles(skillDir, receipt.managed, new Set(), opts);
85
- fs.rmSync(manifestPath, { force: true });
86
- fs.rmSync(path.join(skillDir, '.aiwg-managed'), { force: true });
96
+ for (const target of [manifestPath, path.join(skillDir, '.aiwg-managed')]) {
97
+ // Metadata is released even when operator files keep the directory alive.
98
+ try {
99
+ fs.lstatSync(target);
100
+ opts.plan?.push({ path: path.resolve(target), action: 'remove', reason: 'ownership metadata released' });
101
+ } catch { /* Missing metadata has nothing to remove. */ }
102
+ if (!opts.dryRun) fs.rmSync(target, { force: true });
103
+ }
104
+ if (opts.dryRun) return false;
87
105
  try { fs.rmdirSync(skillDir); return true; } catch { return false; }
88
106
  }
@@ -1,4 +1,4 @@
1
- export type RuleActivation = 'kernel' | 'summary' | 'bound' | 'paths' | 'on-demand';
1
+ export type RuleActivation = 'core' | 'kernel' | 'summary' | 'bound' | 'paths' | 'on-demand';
2
2
  export type RuleBindingKey = 'skills' | 'agents' | 'flows';
3
3
  export interface RuleBindings { skills: string[]; agents: string[]; flows: string[] }
4
4
  export interface RuleFrontmatter extends Record<string, unknown> {
@@ -16,6 +16,15 @@ export type RuleValidationResult =
16
16
  export const RULE_ACTIVATIONS: readonly RuleActivation[];
17
17
  export const RULE_BINDING_TYPES: Readonly<{ skills: 'skill'; agents: 'agent'; flows: 'flow' }>;
18
18
  export const ruleFrontmatterSchema: Record<string, unknown>;
19
- export function inferRuleActivation(enforcement: unknown): 'kernel' | 'on-demand';
19
+ export function inferRuleActivation(enforcement?: unknown): 'on-demand';
20
20
  export function validateRuleFrontmatter(data: unknown): RuleValidationResult;
21
21
  export function parseRuleFrontmatter(content: string): RuleValidationResult & { data: unknown; body: string };
22
+ export const CORE_BODY_MAX_BYTES: number;
23
+ export const CORE_TOTAL_MAX_BYTES: number;
24
+ export const CORE_RULES_START: string;
25
+ export const CORE_RULES_END: string;
26
+ export function coreBodyPath(ruleFile: string): string;
27
+ export function validateCoreBody(text: string, label?: string): string[];
28
+ export function validateCoreSet(bodies: Array<{ name: string; text: string }>): string[];
29
+ export function readCoreBody(ruleFile: string): string;
30
+ export function measureDeployedCoreRules(target: string, provider: string, rulesDir?: string): { bodies: Array<{ name: string; bytes: number }>; bytes: number; limit: number };