@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.
- package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
- package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
- package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
- package/agentic/code/providers/model-capabilities.v1.json +74 -50
- package/bin/aiwg.mjs +3 -1
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/citation-parser.js +8 -1
- package/dist/src/artifacts/cli.js +57 -6
- package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
- package/dist/src/artifacts/fortemi-core-sync.js +26 -8
- package/dist/src/artifacts/graph-query.js +18 -8
- package/dist/src/artifacts/index-adapters.js +339 -0
- package/dist/src/artifacts/index-builder.js +143 -14
- package/dist/src/artifacts/index-files.js +15 -4
- package/dist/src/artifacts/index-reader.js +44 -0
- package/dist/src/artifacts/index-refresh.js +114 -55
- package/dist/src/artifacts/index-status.js +20 -5
- package/dist/src/artifacts/memory-maintenance.js +503 -0
- package/dist/src/artifacts/prebuilt-relations.js +3 -1
- package/dist/src/artifacts/query-engine.js +263 -108
- package/dist/src/artifacts/relations-builder.js +2 -2
- package/dist/src/artifacts/source-graph.js +2 -2
- package/dist/src/artifacts/types.js +9 -1
- package/dist/src/cli/handlers/decision.js +19 -1
- package/dist/src/cli/handlers/guided-setup.js +470 -0
- package/dist/src/cli/handlers/help.js +2 -2
- package/dist/src/cli/handlers/housekeeper.js +1110 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/init.js +41 -6
- package/dist/src/cli/handlers/local-executor.js +5 -3
- package/dist/src/cli/handlers/mc.js +509 -53
- package/dist/src/cli/handlers/models.js +32 -2
- package/dist/src/cli/handlers/ralph-launcher.js +76 -4
- package/dist/src/cli/handlers/rules.js +1 -1
- package/dist/src/cli/handlers/script-runner.js +73 -2
- package/dist/src/cli/handlers/serve.js +5 -3
- package/dist/src/cli/handlers/sessions.js +1 -0
- package/dist/src/cli/handlers/setup.js +428 -1
- package/dist/src/cli/handlers/skill-lint.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +118 -8
- package/dist/src/cli/handlers/use.js +279 -93
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/prompt-utils.js +8 -0
- package/dist/src/cli/router.js +16 -1
- package/dist/src/config/aiwg-config.js +42 -0
- package/dist/src/config/cli.js +7 -3
- package/dist/src/config/decision-config.js +14 -1
- package/dist/src/config/native-sandbox-warnings.js +25 -0
- package/dist/src/contributors/validation.js +6 -5
- package/dist/src/decision/admission.js +7 -3
- package/dist/src/decision/benchmark.js +469 -0
- package/dist/src/decision/ledger.js +22 -3
- package/dist/src/decision/pricing.js +147 -0
- package/dist/src/decision/qualification/gate-evidence.js +18 -15
- package/dist/src/decision/qualification/load-staged.js +539 -0
- package/dist/src/decision/qualification/load.js +109 -33
- package/dist/src/decision/qualification/runner.js +5 -2
- package/dist/src/decision/review/service.js +6 -4
- package/dist/src/decision/review/validate.js +23 -2
- package/dist/src/decision/savings.js +8 -3
- package/dist/src/discovery/index.js +492 -0
- package/dist/src/discovery/interview.js +233 -0
- package/dist/src/discovery/plan-apply.js +571 -0
- package/dist/src/discovery/plan-store.js +171 -0
- package/dist/src/discovery/plan-types.js +2 -0
- package/dist/src/discovery/plan-validation.js +139 -0
- package/dist/src/discovery/types.js +2 -0
- package/dist/src/extensions/commands/definitions.js +49 -9
- package/dist/src/extensions/manifest.js +71 -44
- package/dist/src/extensions/project-local-discovery.js +1 -1
- package/dist/src/extensions/project-quickref.js +18 -17
- package/dist/src/extensions/validation.js +32 -29
- package/dist/src/housekeeper/daemon-config.js +148 -0
- package/dist/src/housekeeper/ipc.js +84 -0
- package/dist/src/housekeeper/lifecycle.js +148 -0
- package/dist/src/housekeeper/platform.js +35 -0
- package/dist/src/housekeeper/status.js +70 -0
- package/dist/src/housekeeper/units.js +114 -0
- package/dist/src/jobs/flow.js +22 -21
- package/dist/src/lint/rule-activation.js +20 -5
- package/dist/src/lint/runner.js +1 -1
- package/dist/src/mcp/cli.mjs +5 -6
- package/dist/src/mcp/config-file.mjs +6 -2
- package/dist/src/mcp/config-layers.mjs +23 -7
- package/dist/src/mcp/profiles.js +18 -31
- package/dist/src/mcp/profiles.mjs +18 -32
- package/dist/src/mcp/registry.js +7 -27
- package/dist/src/mcp/registry.mjs +7 -28
- package/dist/src/mcp/server.mjs +34 -1
- package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
- package/dist/src/mcp/tools/command-run.mjs +3 -2
- package/dist/src/mcp/tools/decision.mjs +49 -48
- package/dist/src/mcp/tools/discovery.mjs +46 -21
- package/dist/src/mcp/tools/orchestration.mjs +22 -22
- package/dist/src/mcp/tools/subsystems.mjs +9 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
- package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
- package/dist/src/memory/audit.js +719 -0
- package/dist/src/memory/canonical-context.js +392 -13
- package/dist/src/memory/canonical-maintenance.js +95 -0
- package/dist/src/memory/cli-result.js +22 -0
- package/dist/src/memory/cli.js +176 -0
- package/dist/src/memory/context-pack.js +57 -6
- package/dist/src/memory/cost.js +512 -0
- package/dist/src/memory/firewall-classification.d.mts +7 -0
- package/dist/src/memory/firewall-classification.mjs +32 -0
- package/dist/src/memory/index.js +8 -0
- package/dist/src/memory/line-maintenance.js +68 -0
- package/dist/src/memory/maintenance-cli.js +286 -0
- package/dist/src/memory/maintenance-contract.js +136 -0
- package/dist/src/memory/maintenance-index.js +18 -0
- package/dist/src/memory/maintenance-owner.js +2 -0
- package/dist/src/memory/maintenance-record.js +138 -0
- package/dist/src/memory/maintenance.js +254 -0
- package/dist/src/memory/provider-memory-adapter.js +154 -0
- package/dist/src/memory/recommend-cli.js +83 -0
- package/dist/src/memory/recommend.js +269 -0
- package/dist/src/memory/scheduled.js +81 -0
- package/dist/src/memory/taxonomy.js +44 -0
- package/dist/src/mission-protocol/codecs.js +19 -9
- package/dist/src/models/config-loader.js +11 -1
- package/dist/src/models/model-capabilities.v1.json +74 -50
- package/dist/src/models/provider-policy.js +125 -62
- package/dist/src/models/resolver.js +45 -8
- package/dist/src/models/worker-installation.d.mts +7 -0
- package/dist/src/models/worker-installation.mjs +169 -37
- package/dist/src/models/wrapper-route.js +1 -1
- package/dist/src/output-modes/registry.js +8 -7
- package/dist/src/plugin/metadata-validator.js +1 -1
- package/dist/src/providers/deploy-version.d.mts +1 -0
- package/dist/src/providers/deploy-version.mjs +17 -0
- package/dist/src/providers/provider-definitions.js +19 -16
- package/dist/src/providers/skill-ownership.d.mts +11 -2
- package/dist/src/providers/skill-ownership.mjs +35 -17
- package/dist/src/rules/activation.d.mts +11 -2
- package/dist/src/rules/activation.mjs +82 -6
- package/dist/src/rules/inventory.js +1 -1
- package/dist/src/rules/rule-frontmatter.schema.json +2 -0
- package/dist/src/schema/zod-validation.d.mts +6 -0
- package/dist/src/schema/zod-validation.mjs +50 -0
- package/dist/src/serve/executor-registry.js +297 -20
- package/dist/src/serve/executor-token.d.mts +1 -0
- package/dist/src/serve/executor-token.mjs +8 -0
- package/dist/src/serve/shared-host-scheduler.js +1102 -31
- package/dist/src/serve/worker-invocation.js +114 -15
- package/dist/src/sessions/adapters/claude.js +19 -18
- package/dist/src/sessions/adapters/codex.js +10 -9
- package/dist/src/sessions/adapters/copilot.js +14 -13
- package/dist/src/sessions/adapters/cursor.js +19 -18
- package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
- package/dist/src/sessions/adapters/factory.js +11 -10
- package/dist/src/sessions/adapters/generic.js +32 -23
- package/dist/src/sessions/adapters/grokbot.js +35 -35
- package/dist/src/sessions/adapters/hermes.js +20 -19
- package/dist/src/sessions/adapters/muse.js +50 -49
- package/dist/src/sessions/adapters/omp.js +5 -4
- package/dist/src/sessions/adapters/openclaw.js +30 -29
- package/dist/src/sessions/adapters/opencode.js +26 -26
- package/dist/src/sessions/adapters/openhuman.js +24 -23
- package/dist/src/sessions/adapters/pi.js +5 -4
- package/dist/src/sessions/adapters/windsurf.js +8 -8
- package/dist/src/sessions/analytics.js +10 -9
- package/dist/src/sessions/candidates.js +7 -6
- package/dist/src/sessions/contracts.js +63 -53
- package/dist/src/sessions/output-registration.js +11 -10
- package/dist/src/sessions/workspace-discovery.js +3 -4
- package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
- package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
- package/dist/src/update/checker.d.mts +4 -0
- package/dist/src/update/checker.mjs +14 -0
- package/dist/src/writing/exemplar-selection.js +6 -6
- package/dist/src/writing/voice-evaluation.js +24 -23
- package/dist/src/writing/writer-migration.js +14 -13
- package/dist/src/writing/writer-profile.js +19 -18
- package/dist/src/writing/writing-brief.js +18 -17
- package/dist/src/writing/writing-receipt.js +47 -46
- package/package.json +5 -2
- package/schemas/decision/DecisionReview.schema.json +78 -1
- package/schemas/mission-v1.schema.json +3 -0
- package/tools/agents/deploy-agents.mjs +67 -39
- package/tools/agents/providers/antigravity.mjs +6 -1
- package/tools/agents/providers/base.mjs +19 -13
- package/tools/agents/providers/claude.mjs +1 -1
- package/tools/agents/providers/codex.mjs +50 -56
- package/tools/agents/providers/copilot.mjs +8 -2
- package/tools/agents/providers/cursor.mjs +1 -1
- package/tools/agents/providers/deepseek-harness.mjs +1 -1
- package/tools/agents/providers/factory.mjs +7 -1
- package/tools/agents/providers/grok-build.mjs +18 -2
- package/tools/agents/providers/grokbot.mjs +1 -1
- package/tools/agents/providers/hermes.mjs +7 -13
- package/tools/agents/providers/muse-hooks.mjs +21 -14
- package/tools/agents/providers/muse.mjs +7 -2
- package/tools/agents/providers/omp.mjs +28 -4
- package/tools/agents/providers/opencode.mjs +14 -5
- package/tools/agents/providers/openhuman.mjs +1 -1
- package/tools/agents/providers/pi.mjs +1 -1
- package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
- package/tools/agents/providers/rule-delivery.mjs +263 -71
- package/tools/agents/providers/windsurf.mjs +7 -0
- package/tools/cli/workspace-status.mjs +5 -0
- package/tools/commands/deploy-prompts-codex.mjs +11 -8
- package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
- package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
- package/tools/ralph-external/process-monitor.mjs +7 -4
- package/tools/ralph-external/session-launcher.mjs +280 -32
- package/tools/skills/deploy-skills-codex.mjs +11 -7
- 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
|
|
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))
|
|
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
|
-
|
|
846
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
870
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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:
|
|
24
|
+
order: integer().optional(),
|
|
24
25
|
instructions: z.string(),
|
|
25
|
-
provenance: z.
|
|
26
|
-
validation: z.
|
|
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
|
-
})
|
|
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:
|
|
37
|
+
contextCost: integer().nonnegative().optional(),
|
|
37
38
|
mergeStrategy: z.literal('weighted-voice').optional(),
|
|
38
|
-
}).
|
|
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.
|
|
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:
|
|
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), {
|
|
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:
|
|
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:
|
|
99
|
-
recommendedMaxLines:
|
|
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:
|
|
123
|
-
maxDescriptionLength:
|
|
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:
|
|
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:
|
|
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 =
|
|
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 (
|
|
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?:
|
|
9
|
-
export function releaseSkillOwnership(skillDir: string, owner: { frameworkSlug?: string; skillSource?: string; sourceRoot?: string }, opts?:
|
|
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
|
-
|
|
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
|
-
|
|
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 !==
|
|
72
|
+
while (parent !== root) {
|
|
61
73
|
try { fs.rmdirSync(parent); } catch { break; }
|
|
62
74
|
parent = path.dirname(parent);
|
|
63
75
|
}
|
|
64
|
-
} catch { /*
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
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 };
|