@aiwg/cli 2026.10.3 → 2026.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/agentic/code/providers/model-capabilities.v1.json +96 -4
- package/bin/aiwg.mjs +50 -27
- package/dist/src/agents/grounding/knowledge-base.js +54 -6
- package/dist/src/artifacts/browser-export.js +6 -0
- package/dist/src/artifacts/checksum-manifest.js +1 -0
- package/dist/src/artifacts/citation-parser.js +173 -92
- package/dist/src/artifacts/citation-table-schema.js +59 -0
- package/dist/src/artifacts/cli.js +57 -18
- package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
- package/dist/src/artifacts/corpus-tools/vision-extract.js +2 -0
- package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
- package/dist/src/artifacts/corpus-views/renderers.js +3 -3
- package/dist/src/artifacts/discovery-eval.js +6 -4
- package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
- package/dist/src/artifacts/fortemi-core-sync.js +166 -40
- package/dist/src/artifacts/framework-capabilities.js +171 -0
- package/dist/src/artifacts/framework-index-paths.js +73 -0
- package/dist/src/artifacts/framework-source-digest.js +75 -0
- package/dist/src/artifacts/index-builder.js +285 -131
- package/dist/src/artifacts/index-files.js +132 -18
- package/dist/src/artifacts/index-preparation.js +65 -0
- package/dist/src/artifacts/index-refresh.js +576 -0
- package/dist/src/artifacts/index-worker.mjs +9 -0
- package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
- package/dist/src/artifacts/prebuilt-deployment.js +46 -0
- package/dist/src/artifacts/prebuilt-relations.js +80 -0
- package/dist/src/artifacts/query-engine.js +410 -88
- package/dist/src/artifacts/relations-builder.js +168 -0
- package/dist/src/artifacts/relations.js +148 -0
- package/dist/src/artifacts/stats.js +5 -2
- package/dist/src/artifacts/types.js +30 -10
- package/dist/src/artifacts/watcher.js +1 -1
- package/dist/src/auth/credential-store.js +2 -1
- package/dist/src/catalog/cli.d.mts +5 -0
- package/dist/src/channel/manager.d.mts +20 -0
- package/dist/src/channel/manager.mjs +19 -2
- package/dist/src/cli/command-log.js +2 -3
- package/dist/src/cli/handlers/artifacts.js +3 -0
- package/dist/src/cli/handlers/auth.js +3 -0
- package/dist/src/cli/handlers/decision.js +19 -12
- package/dist/src/cli/handlers/diagnose.js +4 -3
- package/dist/src/cli/handlers/effect.js +4 -2
- package/dist/src/cli/handlers/evidence.js +3 -0
- package/dist/src/cli/handlers/help.js +1 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/init.js +4 -1
- package/dist/src/cli/handlers/lint.js +5 -0
- package/dist/src/cli/handlers/models.js +104 -8
- package/dist/src/cli/handlers/ralph-launcher.js +2 -0
- package/dist/src/cli/handlers/refresh.js +12 -0
- package/dist/src/cli/handlers/resource-versions.js +3 -0
- package/dist/src/cli/handlers/rules.js +195 -0
- package/dist/src/cli/handlers/run.js +4 -0
- package/dist/src/cli/handlers/runtime-info.js +1 -2
- package/dist/src/cli/handlers/script-runner.js +3 -2
- package/dist/src/cli/handlers/session.js +19 -9
- package/dist/src/cli/handlers/sessions.js +98 -37
- package/dist/src/cli/handlers/setup-manifest.js +39 -44
- package/dist/src/cli/handlers/setup.js +41 -0
- package/dist/src/cli/handlers/steward.js +11 -0
- package/dist/src/cli/handlers/subcommands.js +57 -2
- package/dist/src/cli/handlers/uhp.js +3 -0
- package/dist/src/cli/handlers/use.js +294 -55
- package/dist/src/cli/handlers/utilities.js +26 -10
- package/dist/src/cli/handlers/version.js +8 -0
- package/dist/src/cli/router.js +15 -1
- package/dist/src/cli/services/deployment-verification.js +19 -9
- package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
- package/dist/src/cli/skill-usage.js +2 -3
- package/dist/src/config/aiwg-config.js +87 -37
- package/dist/src/config/cli.js +111 -5
- package/dist/src/config/decision-config.js +106 -0
- package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
- package/dist/src/config/gitignore.js +50 -0
- package/dist/src/config/index-graph-overrides.js +36 -0
- package/dist/src/config/project-artifacts-health.d.mts +37 -0
- package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
- package/dist/src/config/user-config-dir.d.mts +9 -0
- package/dist/src/config/user-config.js +2 -1
- package/dist/src/config/user-registry.js +23 -15
- package/dist/src/config/user-state.js +7 -0
- package/dist/src/decision/ask.js +203 -43
- package/dist/src/decision/context-live-qualification.js +2 -2
- package/dist/src/decision/driver.js +13 -8
- package/dist/src/decision/ensemble/aggregate.js +23 -10
- package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
- package/dist/src/decision/entry.js +2 -15
- package/dist/src/decision/graph-live-qualification.js +1 -1
- package/dist/src/decision/graph-run.js +9 -0
- package/dist/src/decision/heldout/collector.js +5 -13
- package/dist/src/decision/heldout/contract.js +30 -7
- package/dist/src/decision/jev-credentials.js +15 -1
- package/dist/src/decision/ledger.js +131 -28
- package/dist/src/decision/offload-activation.js +137 -0
- package/dist/src/decision/offload.js +550 -0
- package/dist/src/decision/qualification/load.js +4 -6
- package/dist/src/decision/savings.js +28 -0
- package/dist/src/effects/types.js +5 -1
- package/dist/src/effects/verifiers/git.js +2 -1
- package/dist/src/effects/verifiers/tracker.js +26 -14
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/extensions/project-local-remove.js +36 -1
- package/dist/src/gates/providers/loader.js +61 -17
- package/dist/src/installation/manager-command.d.mts +18 -0
- package/dist/src/installation/manager-command.mjs +14 -2
- package/dist/src/installation/manager.d.mts +49 -0
- package/dist/src/installation/manager.mjs +78 -3
- package/dist/src/issues/cli.js +4 -4
- package/dist/src/issues/local.js +24 -5
- package/dist/src/jobs/runner.js +1 -1
- package/dist/src/lint/cli.js +27 -1
- package/dist/src/lint/loader.js +2 -1
- package/dist/src/lint/reporters.js +25 -1
- package/dist/src/lint/rule-activation.js +43 -0
- package/dist/src/lint/runner.js +137 -62
- package/dist/src/mcp/adapters/codex-runtime.js +7 -1
- package/dist/src/mcp/claude-install.d.mts +4 -0
- package/dist/src/mcp/claude-install.mjs +41 -0
- package/dist/src/mcp/cli.d.mts +1 -0
- package/dist/src/mcp/cli.mjs +36 -11
- package/dist/src/mcp/config-file.d.mts +28 -0
- package/dist/src/mcp/config-layers.d.mts +36 -0
- package/dist/src/mcp/credentials.d.mts +51 -0
- package/dist/src/mcp/grok-build-config.d.mts +6 -0
- package/dist/src/mcp/grok-build-config.mjs +1 -1
- package/dist/src/mcp/helpers.mjs +3 -2
- package/dist/src/mcp/muse-native-config.d.mts +13 -0
- package/dist/src/mcp/muse-native-config.mjs +1 -0
- package/dist/src/mcp/omp-config.d.mts +4 -0
- package/dist/src/mcp/registry.js +3 -93
- package/dist/src/mcp/registry.mjs +3 -87
- package/dist/src/mcp/server-config.d.mts +3 -0
- package/dist/src/mcp/server-config.mjs +89 -0
- package/dist/src/mcp/server.mjs +37 -375
- package/dist/src/mcp/test-simple.mjs +2 -1
- package/dist/src/mcp/toml-editor.d.mts +4 -0
- package/dist/src/mcp/toml-string.d.mts +1 -0
- package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
- package/dist/src/mcp/tool-filters.d.mts +46 -0
- package/dist/src/mcp/tools/decision.mjs +12 -11
- package/dist/src/mcp/tools/discovery.mjs +62 -55
- package/dist/src/mcp/tools/orchestration.mjs +12 -2
- package/dist/src/mcp/tools/worker-invoke.mjs +1340 -23
- package/dist/src/mcp/tools/worker-shell-control.mjs +52 -21
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +3 -3
- package/dist/src/mission-protocol/codecs.js +8 -0
- package/dist/src/models/config-loader.js +7 -2
- package/dist/src/models/model-capabilities.v1.json +96 -4
- package/dist/src/models/model-discovery.js +76 -11
- package/dist/src/models/provider-policy.js +101 -7
- package/dist/src/models/qualification-digest.d.mts +2 -0
- package/dist/src/models/resolver.js +12 -10
- package/dist/src/models/worker-direct-readiness.js +65 -44
- package/dist/src/models/worker-dispatch-offer.js +3 -3
- package/dist/src/models/worker-health-readiness.js +16 -3
- package/dist/src/models/worker-installation.d.mts +39 -0
- package/dist/src/models/worker-installation.mjs +135 -28
- package/dist/src/process/bounded-child-process.d.mts +84 -0
- package/dist/src/process/bounded-child-process.mjs +43 -0
- package/dist/src/process/cli-command.d.mts +16 -0
- package/dist/src/process/signal-child.d.mts +6 -0
- package/dist/src/process/signal-child.mjs +7 -0
- package/dist/src/providers/deployed-skill-roots.js +20 -0
- package/dist/src/providers/grok-build-acp.js +3 -2
- package/dist/src/providers/grok-build-headless.js +12 -4
- package/dist/src/providers/grok-build-inspect.js +1 -0
- package/dist/src/providers/omp-agent.d.mts +8 -0
- package/dist/src/providers/omp-diagnostics.d.mts +1 -0
- package/dist/src/providers/omp-paths.d.mts +13 -0
- package/dist/src/providers/skill-ownership.d.mts +9 -0
- package/dist/src/providers/skill-ownership.mjs +88 -0
- package/dist/src/rules/activation.d.mts +21 -0
- package/dist/src/rules/activation.mjs +62 -0
- package/dist/src/rules/inline-budget.d.mts +26 -0
- package/dist/src/rules/inline-budget.mjs +66 -0
- package/dist/src/rules/inventory.js +129 -0
- package/dist/src/rules/pin-state.js +73 -0
- package/dist/src/rules/pins.d.mts +14 -0
- package/dist/src/rules/pins.mjs +112 -0
- package/dist/src/rules/rule-frontmatter.schema.json +197 -0
- package/dist/src/serve/mission-conductor.js +38 -12
- package/dist/src/serve/worker-invocation.js +30 -10
- package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
- package/dist/src/sessions/adapter-registry.js +23 -19
- package/dist/src/sessions/adapters/claude.js +39 -54
- package/dist/src/sessions/adapters/codex.js +82 -67
- package/dist/src/sessions/adapters/copilot.js +4 -4
- package/dist/src/sessions/adapters/cursor.js +40 -57
- package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
- package/dist/src/sessions/adapters/factory.js +31 -27
- package/dist/src/sessions/adapters/generic.js +10 -10
- package/dist/src/sessions/adapters/grok-build.js +11 -11
- package/dist/src/sessions/adapters/grokbot.js +24 -13
- package/dist/src/sessions/adapters/hermes.js +27 -29
- package/dist/src/sessions/adapters/muse-workspace-scan.js +31 -11
- package/dist/src/sessions/adapters/muse.js +150 -93
- package/dist/src/sessions/adapters/omp.js +28 -14
- package/dist/src/sessions/adapters/openclaw.js +89 -79
- package/dist/src/sessions/adapters/opencode.js +27 -24
- package/dist/src/sessions/adapters/openhuman.js +18 -60
- package/dist/src/sessions/adapters/pi.js +29 -14
- package/dist/src/sessions/adapters/warp.js +3 -3
- package/dist/src/sessions/adapters/windsurf.js +19 -48
- package/dist/src/sessions/batch-contracts.js +16 -8
- package/dist/src/sessions/batch-import.js +51 -11
- package/dist/src/sessions/contracts.js +40 -2
- package/dist/src/sessions/coverage-preflight.js +1 -1
- package/dist/src/sessions/export-plan.js +4 -3
- package/dist/src/sessions/import-exclusions.js +72 -0
- package/dist/src/sessions/import-lease.js +34 -18
- package/dist/src/sessions/import-limits.js +37 -0
- package/dist/src/sessions/importer.js +60 -38
- package/dist/src/sessions/lifecycle.js +45 -0
- package/dist/src/sessions/policy.js +133 -25
- package/dist/src/sessions/readers.js +106 -53
- package/dist/src/sessions/refresh.js +106 -0
- package/dist/src/sessions/repository.js +147 -52
- package/dist/src/sessions/workspace-discovery.js +221 -56
- package/dist/src/sessions/workspace-matching.js +107 -0
- package/dist/src/skills/importer.js +1 -0
- package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
- package/dist/src/smiths/context-pipeline/finalization.js +6 -5
- package/dist/src/smiths/context-pipeline/generator.js +21 -4
- package/dist/src/smiths/context-pipeline/parallelism-section.js +7 -15
- package/dist/src/smiths/mcpsmith/generator.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +76 -18
- package/dist/src/storage/backends/fs.js +18 -76
- package/dist/src/storage/backends/obsidian.js +1 -0
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/update/checker.d.mts +2 -0
- package/dist/src/update/service.d.mts +22 -0
- package/dist/src/utils/contained-filesystem.d.mts +37 -0
- package/dist/src/utils/contained-filesystem.mjs +683 -0
- package/dist/src/utils/temporary-directory.js +36 -0
- package/dist/src/writing/example-generator.js +1 -1
- package/package.json +15 -2
- package/schemas/models/model-catalog.v1.schema.json +1 -1
- package/schemas/models/provider-model-capabilities.v1.schema.json +6 -0
- package/tools/_resolve-impl.mjs +13 -15
- package/tools/agents/deploy-agents.mjs +49 -21
- package/tools/agents/providers/base.mjs +104 -134
- package/tools/agents/providers/claude.mjs +8 -24
- package/tools/agents/providers/codex.mjs +18 -11
- package/tools/agents/providers/cursor.mjs +19 -18
- package/tools/agents/providers/grok-build.mjs +3 -1
- package/tools/agents/providers/hermes.mjs +57 -47
- package/tools/agents/providers/openclaw.mjs +9 -4
- package/tools/agents/providers/pi.mjs +5 -0
- package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
- package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
- package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
- package/tools/agents/providers/rule-delivery.mjs +1270 -0
- package/tools/agents/providers/warp.mjs +20 -26
- package/tools/cli/decision-offload-report.mjs +40 -0
- package/tools/cli/workspace-status.mjs +862 -0
- package/tools/plugin/package-plugins.mjs +186 -8
- package/tools/plugin/plugin-links.mjs +114 -0
- package/tools/providers/antigravity-transport.mjs +3 -1
- package/tools/providers/deepseek-harness-transport.mjs +6 -4
- package/tools/providers/omp-transport.mjs +4 -2
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +1 -1
- package/tools/ralph-external/lib/pi-adapter.mjs +0 -4
- package/tools/ralph-external/lib/provider-adapter.mjs +59 -1
- package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
- package/tools/ralph-external/lib/session-environment.mjs +28 -0
- package/tools/ralph-external/lib/systemd-client.mjs +49 -0
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
- package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
- package/tools/ralph-external/process-monitor.mjs +1017 -0
- package/tools/ralph-external/session-launcher.mjs +5057 -0
- package/tools/scripts/resource-preflight.mjs +541 -0
- package/tools/skills/deploy-skills-codex.mjs +2 -1
- package/vscode-extension/schemas/aiwg.config.v1.json +1194 -231
|
@@ -0,0 +1,1270 @@
|
|
|
1
|
+
/** Activation-selected deployment, shared by the six provider projections (#2939). */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { stringify } from 'yaml';
|
|
6
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
7
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
8
|
+
import {
|
|
9
|
+
listMdFiles, discoverAddons, getFrameworksForMode, collectBehaviorDirs,
|
|
10
|
+
resolveAiwgRoot, ruleEnforcementLevel, stripTriggersFromContent,
|
|
11
|
+
addManagedMarker, contentHash, replaceBoundRulesBlock, deployEmulatedBehaviors, filterAgentFiles,
|
|
12
|
+
} from './base.mjs';
|
|
13
|
+
|
|
14
|
+
const { parseRuleFrontmatter, inferRuleActivation, RULE_BINDING_TYPES } = await importImpl(import.meta.url, 'rules/activation.mjs');
|
|
15
|
+
const { trackTemporaryDirectory } = await importImpl(import.meta.url, 'utils/temporary-directory.js');
|
|
16
|
+
const { loadRulePinConfigs, installedRuleCapabilities, resolveRulePins } = await importImpl(import.meta.url, 'rules/pins.mjs');
|
|
17
|
+
const { guardRulesInlineBudget } = await importImpl(import.meta.url, 'rules/inline-budget.mjs');
|
|
18
|
+
const { userConfigFile } = await importImpl(import.meta.url, 'config/user-config-dir.mjs');
|
|
19
|
+
const { resolveProjectAiwgDir } = await importImpl(import.meta.url, 'config/project-artifacts-runtime.mjs');
|
|
20
|
+
const PREPARATION_HOOK = Symbol.for('aiwg.provider-preparation-hook.v1');
|
|
21
|
+
|
|
22
|
+
export function installRuleDeploymentPreparationHook(hook) {
|
|
23
|
+
if (!hook || typeof hook.scratch !== 'string' || !hook.inputPool || typeof hook.accept !== 'function' || globalThis[PREPARATION_HOOK]) {
|
|
24
|
+
throw new Error('Invalid or already active rule deployment preparation hook');
|
|
25
|
+
}
|
|
26
|
+
globalThis[PREPARATION_HOOK] = hook;
|
|
27
|
+
return () => { if (globalThis[PREPARATION_HOOK] === hook) delete globalThis[PREPARATION_HOOK]; };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isRuleDeploymentPreparationActive() {
|
|
31
|
+
return Boolean(globalThis[PREPARATION_HOOK]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function walk(dir) {
|
|
35
|
+
if (!fs.existsSync(dir)) return [];
|
|
36
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
|
37
|
+
const file = path.join(dir, entry.name);
|
|
38
|
+
return entry.isDirectory() ? walk(file) : entry.isFile() ? [file] : [];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// Keep only digests in the transaction snapshot. Artifact stores and binary skill
|
|
42
|
+
// resources can be much larger than the provider output being changed.
|
|
43
|
+
function fileDigest(file) {
|
|
44
|
+
const hash = createHash('sha256');
|
|
45
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
46
|
+
const fd = fs.openSync(file, 'r');
|
|
47
|
+
try {
|
|
48
|
+
let count;
|
|
49
|
+
while ((count = fs.readSync(fd, buffer, 0, buffer.length, null))) hash.update(buffer.subarray(0, count));
|
|
50
|
+
return hash.digest('hex');
|
|
51
|
+
} finally {
|
|
52
|
+
fs.closeSync(fd);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Empty directories are part of workspace and skill-resource scaffolds too.
|
|
56
|
+
function promoteDirectories(source, destination, skip = () => false, prefix = '') {
|
|
57
|
+
const entries = fs.readdirSync(source, { withFileTypes: true });
|
|
58
|
+
if (!entries.length && !fs.existsSync(destination)) fs.mkdirSync(destination, { recursive: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const rel = path.join(prefix, entry.name);
|
|
61
|
+
if (entry.isDirectory() && !skip(rel)) {
|
|
62
|
+
promoteDirectories(path.join(source, entry.name), path.join(destination, entry.name), skip, rel);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const read = file => fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
67
|
+
const structural = file => /^(RULES-INDEX|RULES-ONDEMAND)\.(md|mdc)$/.test(path.basename(file));
|
|
68
|
+
|
|
69
|
+
/** Collect selected source contracts, before any tier filter or projection. */
|
|
70
|
+
export function collectDeploymentRuleSources(opts) {
|
|
71
|
+
if (opts.ruleSources) return [...new Set([...opts.ruleSources, ...(opts.previousRuleSources || []).filter(file => fs.existsSync(file))])];
|
|
72
|
+
const roots = [opts.srcRoot];
|
|
73
|
+
for (const framework of getFrameworksForMode(opts.srcRoot, opts.mode)) roots.push(framework.path);
|
|
74
|
+
for (const addon of discoverAddons(opts.srcRoot)) roots.push(addon.path);
|
|
75
|
+
const files = [...(opts.previousRuleSources || []).filter(file => fs.existsSync(file)), ...roots.flatMap(root => listMdFiles(path.join(root, 'rules')))].filter(file => !structural(file));
|
|
76
|
+
const behaviors = collectBehaviorDirs(opts.srcRoot).map(dir => path.join(dir, 'BEHAVIOR.md'));
|
|
77
|
+
return [...new Set([...files, ...behaviors])];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function prepareRuleDelivery(opts) {
|
|
81
|
+
const rules = collectDeploymentRuleSources(opts).map(file => {
|
|
82
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
83
|
+
const parsed = parseRuleFrontmatter(content);
|
|
84
|
+
if (!parsed.valid) throw new Error(`${file}: ${parsed.diagnostics.map(d => d.message).join('; ')}`);
|
|
85
|
+
const behavior = path.basename(file) === 'BEHAVIOR.md';
|
|
86
|
+
const rule = { ...parsed.rule, file, content, body: parsed.body, behavior,
|
|
87
|
+
name: behavior ? `behavior/${path.basename(path.dirname(file))}` : parsed.data.name || path.basename(file, '.md') };
|
|
88
|
+
if (!rule.tagged) rule.activation = behavior ? 'kernel' : inferRuleActivation(ruleEnforcementLevel(content));
|
|
89
|
+
for (const diagnostic of parsed.diagnostics) {
|
|
90
|
+
console.warn(`${opts.logicalInputPath ? opts.logicalInputPath(file) : file}: ${diagnostic.code}: ${diagnostic.code === 'missing-activation' ? `Missing activation; inferred ${rule.activation} delivery (migration fallback).` : diagnostic.message}`);
|
|
91
|
+
}
|
|
92
|
+
return rule;
|
|
93
|
+
});
|
|
94
|
+
const configs = opts.pinConfigs || loadRulePinConfigs(opts.target, opts.config);
|
|
95
|
+
const installed = installedRuleCapabilities(configs);
|
|
96
|
+
const root = resolveAiwgRoot(opts.srcRoot) || opts.srcRoot;
|
|
97
|
+
const { loadRelations, selectPrebuiltRelations } = await importImpl(import.meta.url, 'artifacts/relations.js');
|
|
98
|
+
const packageRelations = selectPrebuiltRelations(root, opts.target).reader;
|
|
99
|
+
const relations = packageRelations || loadRelations(opts.target) || loadRelations(root, opts.target);
|
|
100
|
+
const ruleIdsByPath = new Map(Object.entries(relations?.index.rules || {}).map(([id, rule]) => [rule.path, id]));
|
|
101
|
+
const pinRules = rules.map(rule => {
|
|
102
|
+
const owner = rule.file.replaceAll('\\', '/').match(/(?:frameworks|addons|extensions|plugins)\/([^/]+)\//)?.[1];
|
|
103
|
+
const local = Object.entries(configs.project.installed || {}).find(([, entry]) => entry.localPath && rule.file.startsWith(path.resolve(opts.target, entry.localPath) + path.sep))?.[0];
|
|
104
|
+
const indexedPath = path.relative(root, rule.file);
|
|
105
|
+
return { ...rule, id: ruleIdsByPath.get(indexedPath) || rule.file, path: rule.file, indexedPath,
|
|
106
|
+
capability: local || owner || opts.capability, enforcement: ruleEnforcementLevel(rule.content) };
|
|
107
|
+
});
|
|
108
|
+
const pinned = resolveRulePins({ rules: pinRules, configs, installed, relations });
|
|
109
|
+
for (const warning of pinned.warnings) console.warn(warning);
|
|
110
|
+
const pinsByFile = new Map(pinned.rules.map(rule => [rule.file, rule]));
|
|
111
|
+
for (const rule of rules) {
|
|
112
|
+
const pin = pinsByFile.get(rule.file);
|
|
113
|
+
if (!pin) continue;
|
|
114
|
+
rule.canonicalActivation = rule.activation;
|
|
115
|
+
rule.pinMatches = pin.matches;
|
|
116
|
+
rule.pinned = true;
|
|
117
|
+
rule.activation = 'kernel';
|
|
118
|
+
// All pins use native kernel projection, including legacy untagged rules.
|
|
119
|
+
rule.tagged = true;
|
|
120
|
+
}
|
|
121
|
+
const bindings = new Map();
|
|
122
|
+
const targets = [];
|
|
123
|
+
const targetKeys = new Set();
|
|
124
|
+
if (rules.some(rule => Object.values(rule.bound_to).some(ids => ids.length))) {
|
|
125
|
+
const { loadLocalCapabilityEntries, loadLocalOverrideCapabilityEntries, createRuleBindingTargetResolver } = await importImpl(import.meta.url, 'artifacts/query-engine.js');
|
|
126
|
+
const root = resolveAiwgRoot(opts.srcRoot) || opts.srcRoot;
|
|
127
|
+
// Framework graph paths are package-relative; project graph paths belong
|
|
128
|
+
// to the queried project. Normalize before merging so one source cannot
|
|
129
|
+
// become two apparently ambiguous targets after a source-index rebuild.
|
|
130
|
+
const indexEntries = new Map();
|
|
131
|
+
const entryPaths = new Map();
|
|
132
|
+
const loadEntries = (cwd, overridesOnly = false) => (overridesOnly
|
|
133
|
+
? loadLocalOverrideCapabilityEntries(cwd)
|
|
134
|
+
: loadLocalCapabilityEntries(cwd, undefined, indexEntries)).map(entry => {
|
|
135
|
+
const anchor = entry.path.startsWith('agentic/code/') ? root : cwd;
|
|
136
|
+
const key = `${anchor}\0${entry.path}`;
|
|
137
|
+
if (!entryPaths.has(key)) entryPaths.set(key, path.resolve(anchor, entry.path));
|
|
138
|
+
const resolvedPath = entryPaths.get(key);
|
|
139
|
+
return { ...entry, path: opts.mapInputPath ? opts.mapInputPath(resolvedPath) : resolvedPath };
|
|
140
|
+
});
|
|
141
|
+
let rebuilt = false;
|
|
142
|
+
const cacheDir = process.env.AIWG_RULE_BINDING_INDEX_CACHE;
|
|
143
|
+
const cacheFile = cacheDir && path.join(cacheDir, `${contentHash(opts.logicalInputPath ? opts.logicalInputPath(root) : root)}.json`);
|
|
144
|
+
const buildSourceEntries = async (refresh = false) => {
|
|
145
|
+
if (!refresh && cacheFile) {
|
|
146
|
+
try { return JSON.parse(fs.readFileSync(cacheFile, 'utf8')).map(entry => ({ ...entry, path: opts.mapInputPath ? opts.mapInputPath(entry.path) : entry.path })); }
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const indexTemp = fs.mkdtempSync(path.join(os.tmpdir(), 'aiwg-rule-index-'));
|
|
152
|
+
try {
|
|
153
|
+
const { buildIndex } = await importImpl(import.meta.url, 'artifacts/index-builder.js');
|
|
154
|
+
await buildIndex(root, { scope: root === opts.srcRoot && !fs.existsSync(path.join(root, 'agentic')) ? '.' : 'agentic/code', outputDir: indexTemp, force: true, quiet: true });
|
|
155
|
+
rebuilt = true;
|
|
156
|
+
const index = JSON.parse(read(path.join(indexTemp, '.aiwg', '.index', 'metadata.json')));
|
|
157
|
+
const entries = Object.values(index.entries).map(entry => ({ ...entry, path: path.resolve(root, entry.path) }));
|
|
158
|
+
if (cacheFile) {
|
|
159
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
160
|
+
// Workers may publish the same immutable source snapshot concurrently.
|
|
161
|
+
const temporary = `${cacheFile}.${process.pid}.tmp`;
|
|
162
|
+
fs.writeFileSync(temporary, JSON.stringify(entries.map(entry => ({ ...entry, path: opts.logicalInputPath ? opts.logicalInputPath(entry.path) : entry.path }))));
|
|
163
|
+
fs.renameSync(temporary, cacheFile);
|
|
164
|
+
}
|
|
165
|
+
return entries;
|
|
166
|
+
} finally { fs.rmSync(indexTemp, { recursive: true, force: true }); }
|
|
167
|
+
};
|
|
168
|
+
const localEntries = opts.bindingEntries || loadEntries(opts.target, Boolean(packageRelations));
|
|
169
|
+
let entries = [...localEntries];
|
|
170
|
+
if (!opts.bindingEntries) {
|
|
171
|
+
// The validated package table is authoritative for its exact source digest.
|
|
172
|
+
// Keep the canonical resolver for aliases, local overrides and ambiguity.
|
|
173
|
+
const sourceEntries = packageRelations
|
|
174
|
+
? Object.values(packageRelations.index.artifacts).map(entry => ({ ...entry,
|
|
175
|
+
indexScope: 'packaged', indexGraph: 'framework' }))
|
|
176
|
+
: loadEntries(root);
|
|
177
|
+
// A relocated project graph may also be visible while querying the
|
|
178
|
+
// package root. Keep its project-anchored instance only.
|
|
179
|
+
const uniqueSources = sourceEntries.filter(entry => !(opts.sharedProjectGraph &&
|
|
180
|
+
entry.path.startsWith(path.join(root, '.aiwg') + path.sep) &&
|
|
181
|
+
localEntries.some(local => local.type === entry.type && local.name === entry.name)));
|
|
182
|
+
entries.push(...uniqueSources);
|
|
183
|
+
if (!sourceEntries.length) entries.push(...await buildSourceEntries());
|
|
184
|
+
}
|
|
185
|
+
// Project-local installed bundles are outside the package source index.
|
|
186
|
+
// Reuse the existing binding index builder only for those source roots.
|
|
187
|
+
if (!opts.bindingEntries) {
|
|
188
|
+
for (const entry of Object.values(configs.project.installed || {})) {
|
|
189
|
+
if (!entry.localPath) continue;
|
|
190
|
+
const localRoot = path.resolve(opts.target, entry.localPath);
|
|
191
|
+
if (localRoot === root || !fs.existsSync(localRoot)) continue;
|
|
192
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'aiwg-local-rule-index-'));
|
|
193
|
+
try {
|
|
194
|
+
const { buildIndex } = await importImpl(import.meta.url, 'artifacts/index-builder.js');
|
|
195
|
+
await buildIndex(localRoot, { scope: '.', outputDir: temp, force: true, quiet: true });
|
|
196
|
+
const index = JSON.parse(read(path.join(temp, '.aiwg', '.index', 'metadata.json')));
|
|
197
|
+
const scoped = Object.values(index.entries).map(entry => ({ ...entry, path: path.resolve(localRoot, entry.path) }));
|
|
198
|
+
localEntries.push(...scoped);
|
|
199
|
+
entries.push(...scoped);
|
|
200
|
+
} finally { fs.rmSync(temp, { recursive: true, force: true }); }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// A stored index can predate the source tree (edge/dev checkouts). Rebuild once from source
|
|
204
|
+
// before reporting a binding target as missing.
|
|
205
|
+
const resolvedTargets = new Map();
|
|
206
|
+
let resolveBinding = createRuleBindingTargetResolver(entries);
|
|
207
|
+
const resolveTarget = async (type, id) => {
|
|
208
|
+
const key = `${type}:${id}`;
|
|
209
|
+
if (resolvedTargets.has(key)) return resolvedTargets.get(key);
|
|
210
|
+
let target;
|
|
211
|
+
try { target = resolveBinding(type, id); }
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (packageRelations || rebuilt || opts.bindingEntries || !/^Missing /.test(error.message)) throw error;
|
|
214
|
+
indexEntries.clear();
|
|
215
|
+
entryPaths.clear();
|
|
216
|
+
entries = [...localEntries, ...await buildSourceEntries(true)];
|
|
217
|
+
rebuilt = true;
|
|
218
|
+
resolveBinding = createRuleBindingTargetResolver(entries);
|
|
219
|
+
target = resolveBinding(type, id);
|
|
220
|
+
// A rebuild replaces the input set for every lookup.
|
|
221
|
+
resolvedTargets.clear();
|
|
222
|
+
}
|
|
223
|
+
resolvedTargets.set(key, target);
|
|
224
|
+
return target;
|
|
225
|
+
};
|
|
226
|
+
for (const rule of rules.filter(rule => rule.tagged)) {
|
|
227
|
+
for (const [key, type] of Object.entries(RULE_BINDING_TYPES)) {
|
|
228
|
+
for (const id of rule.bound_to[key]) {
|
|
229
|
+
const target = await resolveTarget(type, id);
|
|
230
|
+
const source = path.isAbsolute(target.path) ? target.path : path.resolve(target.path.startsWith('agentic/code/') ? root : opts.target, target.path);
|
|
231
|
+
if (!fs.existsSync(source)) throw new Error(`${rule.file}: binding target content missing: ${source}`);
|
|
232
|
+
bindings.set(source, [...(bindings.get(source) || []), rule]);
|
|
233
|
+
const key = `${target.type}:${source}`;
|
|
234
|
+
if (!targetKeys.has(key)) {
|
|
235
|
+
targetKeys.add(key);
|
|
236
|
+
targets.push({ ...target, source });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { rules, bindings, targets, pins: pinned.rules, pinWarnings: pinned.warnings };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function renderRuleKernelIndex(rules, nativePaths = false) {
|
|
246
|
+
const summary = rules.filter(rule => rule.tagged && (rule.canonicalActivation || rule.activation) === 'summary');
|
|
247
|
+
const paths = nativePaths ? [] : rules.filter(rule => rule.tagged && (rule.canonicalActivation || rule.activation) === 'paths');
|
|
248
|
+
if (!summary.length && !paths.length) return '';
|
|
249
|
+
const lines = ['# Rule kernel index', ''];
|
|
250
|
+
for (const rule of summary) lines.push(`- ${rule.name} — ${rule.summary} Fetch and follow: \`aiwg show rule ${rule.name}\`.`, '');
|
|
251
|
+
if (paths.length) {
|
|
252
|
+
lines.push('## Path rules (prompt emulation)', '',
|
|
253
|
+
'Before reading or changing files, compare intended project-relative paths with the globs below and fetch matching rules before their first read/write. If paths are initially unknown, check when they become known. An agent-only task has no path match. This is prompt emulation, not a filesystem hook.', '');
|
|
254
|
+
for (const rule of paths) lines.push(`- ${rule.name} | ${rule.paths.map(glob => `\`${glob}\``).join(', ')} | ${rule.summary} | \`aiwg show rule ${rule.name}\``, '');
|
|
255
|
+
}
|
|
256
|
+
return lines.join('\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function managed(content, opts) {
|
|
260
|
+
return addManagedMarker(content, opts.deployVersion || 'unknown', opts.deploySource || 'bundled');
|
|
261
|
+
}
|
|
262
|
+
function put(file, content) {
|
|
263
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
264
|
+
fs.writeFileSync(file, content);
|
|
265
|
+
}
|
|
266
|
+
function block(content, body) {
|
|
267
|
+
const marker = /\n?<!-- AIWG:rule-startup:start -->[\s\S]*?<!-- AIWG:rule-startup:end -->\n?/g;
|
|
268
|
+
const clean = content.replace(marker, '');
|
|
269
|
+
return body ? `${clean.trimEnd()}\n\n<!-- AIWG:rule-startup:start -->\n${body}\n<!-- AIWG:rule-startup:end -->\n` : clean;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function putStartup(file, body) {
|
|
273
|
+
const content = fs.existsSync(file) ? read(file) : '<!-- aiwg-managed -->\n';
|
|
274
|
+
put(file, block(content, body));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Tagged rules bypass legacy aggregate writers. Untagged projections retain their tier policy. */
|
|
278
|
+
function projectRules(opts, provider) {
|
|
279
|
+
const { rules } = opts.ruleDelivery;
|
|
280
|
+
const nativePaths = ['claude', 'cursor'].includes(provider.name);
|
|
281
|
+
const rulesDir = path.isAbsolute(provider.paths.rules || '') ? provider.paths.rules : path.join(opts.target, provider.paths.rules || `.${provider.name}/rules`);
|
|
282
|
+
const tagged = rules.filter(rule => rule.tagged);
|
|
283
|
+
const extension = provider.name === 'cursor' ? '.mdc' : '.md';
|
|
284
|
+
const writeRule = (rule, content) => {
|
|
285
|
+
const name = rule.behavior ? `behaviors/${path.basename(path.dirname(rule.file))}` : path.basename(rule.file, '.md');
|
|
286
|
+
put(path.join(rulesDir, `${name}${extension}`), managed(content, opts));
|
|
287
|
+
};
|
|
288
|
+
const priorManifestPath = path.join(rulesDir, '.aiwg-manifest.json');
|
|
289
|
+
const priorSources = fs.existsSync(priorManifestPath) ? JSON.parse(read(priorManifestPath)).ruleSources || [] : [];
|
|
290
|
+
for (const source of priorSources.filter(file => !fs.existsSync(file))) {
|
|
291
|
+
const name = path.basename(source) === 'BEHAVIOR.md' ? `behaviors/${path.basename(path.dirname(source))}` : path.basename(source, '.md');
|
|
292
|
+
for (const ext of ['.md', '.mdc']) {
|
|
293
|
+
const stale = path.join(rulesDir, name + ext);
|
|
294
|
+
if (/aiwg:managed/.test(read(stale))) fs.rmSync(stale);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Remove former startup bodies even on activation changes and legacy .md -> .mdc migration.
|
|
298
|
+
for (const rule of tagged) {
|
|
299
|
+
const name = rule.behavior ? `behaviors/${path.basename(path.dirname(rule.file))}` : path.basename(rule.file, '.md');
|
|
300
|
+
for (const ext of ['.md', '.mdc']) {
|
|
301
|
+
const dest = path.join(rulesDir, `${name}${ext}`);
|
|
302
|
+
if (/aiwg:managed/.test(read(dest))) fs.rmSync(dest);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const rule of tagged) {
|
|
306
|
+
if (rule.activation === 'kernel' || (nativePaths && rule.activation === 'paths')) {
|
|
307
|
+
let content = stripTriggersFromContent(rule.content);
|
|
308
|
+
if (rule.pinned) {
|
|
309
|
+
const parsed = parseRuleFrontmatter(content);
|
|
310
|
+
const data = { ...parsed.data, activation: 'kernel' };
|
|
311
|
+
delete data.paths;
|
|
312
|
+
delete data.bound_to;
|
|
313
|
+
content = `---\n${stringify(data)}---\n${parsed.body}`;
|
|
314
|
+
}
|
|
315
|
+
if (provider.name === 'cursor') content = provider.transformRule(rule.file, content, opts);
|
|
316
|
+
writeRule(rule, content);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// A legacy consolidated index can mention now-bound rules. Replace it with an
|
|
320
|
+
// activation-selected index when migration starts; pure legacy deployments keep it.
|
|
321
|
+
// Hermes's 20K context limit requires a fixed provider projection: only
|
|
322
|
+
// frontmatter-CRITICAL kernel bodies are inline (operator decision 2026-10-19).
|
|
323
|
+
// Explicit operator pins retain full bodies; other activation metadata and
|
|
324
|
+
// every other provider's projection remain unchanged.
|
|
325
|
+
const isCritical = rule => String(rule.enforcement || '').trim().toLowerCase() === 'critical';
|
|
326
|
+
const deferredKernels = provider.name === 'hermes'
|
|
327
|
+
? rules.filter(rule => rule.tagged && rule.activation === 'kernel' && !rule.pinned && !isCritical(rule)) : [];
|
|
328
|
+
const indexRules = deferredKernels.length ? rules.map(rule => deferredKernels.includes(rule)
|
|
329
|
+
? { ...rule, activation: 'summary', summary: rule.summary || 'Fetch and follow the full rule before governed work.' } : rule) : rules;
|
|
330
|
+
const index = renderRuleKernelIndex(indexRules, nativePaths);
|
|
331
|
+
if (tagged.length) {
|
|
332
|
+
for (const ext of ['.md', '.mdc']) {
|
|
333
|
+
const file = path.join(rulesDir, `RULES-INDEX${ext}`);
|
|
334
|
+
if (/aiwg:managed/.test(read(file))) fs.rmSync(file);
|
|
335
|
+
}
|
|
336
|
+
if (index) put(path.join(rulesDir, `RULES-INDEX${extension}`), managed(provider.name === 'cursor' ? `---\nalwaysApply: true\n---\n${index}` : index, opts));
|
|
337
|
+
}
|
|
338
|
+
fs.rmSync(path.join(rulesDir, '.aiwg-rules-budget.json'), { force: true });
|
|
339
|
+
const manifestPath = path.join(rulesDir, '.aiwg-manifest.json');
|
|
340
|
+
const manifest = fs.existsSync(manifestPath) ? JSON.parse(read(manifestPath)) : { managed: {} };
|
|
341
|
+
manifest.ruleSources = rules.map(rule => rule.file);
|
|
342
|
+
for (const file of walk(rulesDir).filter(file => /\.(md|mdc)$/.test(file) && /aiwg:managed/.test(read(file)))) {
|
|
343
|
+
const name = path.relative(rulesDir, file);
|
|
344
|
+
manifest.managed[name] = { ...manifest.managed[name], hash: `sha256:${contentHash(read(file))}` };
|
|
345
|
+
}
|
|
346
|
+
for (const name of Object.keys(manifest.managed)) if (!fs.existsSync(path.join(rulesDir, name))) delete manifest.managed[name];
|
|
347
|
+
put(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
348
|
+
if (!nativePaths) {
|
|
349
|
+
// The conventional directory is not a loader: startup reaches the complete
|
|
350
|
+
// selected bodies through the provider's existing context owner.
|
|
351
|
+
const kernels = rules.filter(rule => rule.activation === 'kernel' &&
|
|
352
|
+
rule.tagged && (provider.name !== 'hermes' || rule.pinned || isCritical(rule)));
|
|
353
|
+
const routes = [...new Set(opts.definitionRoutes || [])];
|
|
354
|
+
const routing = routes.length ? `## Invoked artifact definitions\n\nRead the complete definition before invoking its artifact:\n${routes.map(route => `- ${route}`).join('\n')}` : '';
|
|
355
|
+
let startupIndex = index;
|
|
356
|
+
let startupRouting = routing;
|
|
357
|
+
const fetchKernels = deferredKernels.length ? 'Before every task and subagent task, fetch and follow the kernel rules listed below with `aiwg show rule <name>` before governed work. Their full rules are binding.' : '';
|
|
358
|
+
const renderBody = () => [...kernels.map(rule => `## Rule: ${rule.name}\n\n${rule.body.trim()}`), fetchKernels, startupIndex, startupRouting].filter(Boolean).join('\n\n');
|
|
359
|
+
const dest = path.join(opts.target, 'AGENTS.md');
|
|
360
|
+
if (provider.name === 'hermes' && block(read(dest), renderBody()).length > 19_000) {
|
|
361
|
+
// The complete summary/path index and definition routes can themselves
|
|
362
|
+
// exceed 10K. Keep kernel fetch obligations inline and explicitly reach
|
|
363
|
+
// the rest through the existing index, rather than dropping contracts.
|
|
364
|
+
if (index || routing) {
|
|
365
|
+
const name = `RULES-INDEX${extension}`;
|
|
366
|
+
const content = managed([index, routing].filter(Boolean).join('\n\n'), opts);
|
|
367
|
+
put(path.join(rulesDir, name), content);
|
|
368
|
+
manifest.managed[name] = { ...manifest.managed[name], hash: `sha256:${contentHash(content)}` };
|
|
369
|
+
put(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
370
|
+
}
|
|
371
|
+
const deferredFiles = new Set(deferredKernels.map(rule => rule.file));
|
|
372
|
+
const kernelIndex = renderRuleKernelIndex(indexRules.filter(rule => deferredFiles.has(rule.file)));
|
|
373
|
+
startupIndex = [kernelIndex,
|
|
374
|
+
index || routing ? 'Before every task and subagent task, read and follow `.hermes/rules/RULES-INDEX.md` for summary rules, path checks, and invoked artifact definitions. Fetch matching full rules before governed work.' : '',
|
|
375
|
+
].filter(Boolean).join('\n\n');
|
|
376
|
+
startupRouting = '';
|
|
377
|
+
}
|
|
378
|
+
const body = renderBody();
|
|
379
|
+
if (!body) {
|
|
380
|
+
// No activation-selected startup contract: retain legacy context owners
|
|
381
|
+
// without inventing an empty policy file or a dangling bootstrap read.
|
|
382
|
+
const contexts = ['AIWG.md', 'AGENTS.md', 'WARP.md', '.hermes.md'].map(name => path.join(opts.target, name));
|
|
383
|
+
if (opts.activeWorkspace) contexts.push(path.join(opts.activeWorkspace, 'AGENTS.md'));
|
|
384
|
+
for (const file of contexts) {
|
|
385
|
+
if (!fs.existsSync(file)) continue;
|
|
386
|
+
const content = read(file);
|
|
387
|
+
const clean = block(content, '');
|
|
388
|
+
if (clean !== content) put(file, clean);
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (provider.name === 'hermes') {
|
|
393
|
+
const dest = path.join(opts.target, 'AGENTS.md');
|
|
394
|
+
putStartup(dest, body);
|
|
395
|
+
putStartup(path.join(opts.target, '.hermes.md'), 'Read and follow `AGENTS.md` in this directory before every task and subagent task.');
|
|
396
|
+
} else if (provider.name === 'warp') {
|
|
397
|
+
const dest = path.join(opts.target, 'WARP.md');
|
|
398
|
+
putStartup(dest, body);
|
|
399
|
+
} else {
|
|
400
|
+
const dest = path.join(opts.target, 'AIWG.md');
|
|
401
|
+
putStartup(dest, body);
|
|
402
|
+
const bridge = provider.name === 'openclaw' ? path.join(opts.activeWorkspace, 'AGENTS.md') : path.join(opts.target, 'AGENTS.md');
|
|
403
|
+
putStartup(bridge, `Read and follow ${JSON.stringify(dest)} before every task and subagent task. It contains the complete activation-selected kernel and rule index.`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** External helpers also project skills/flows. Inject into their body, never descriptions. */
|
|
409
|
+
function finishBindings(opts, provider) {
|
|
410
|
+
const roots = [opts.target, opts.deploymentHome, ...Object.values(provider.paths).filter(p => path.isAbsolute(p))];
|
|
411
|
+
const files = [...new Set(roots.flatMap(walk))];
|
|
412
|
+
const targetsBySource = new Map();
|
|
413
|
+
for (const target of opts.ruleDelivery.targets) {
|
|
414
|
+
targetsBySource.set(target.source, [...(targetsBySource.get(target.source) || []), target]);
|
|
415
|
+
}
|
|
416
|
+
for (const file of files.filter(file => /\.(md|toml)$/.test(file))) {
|
|
417
|
+
if (!/(?:[/\\](?:skills|agents|commands)[/\\])/.test(file)) continue;
|
|
418
|
+
if (/[/\\]rules[/\\]/.test(file) || /[/\\]\.aiwg[/\\](?!skills[/\\])/.test(file.replace(opts.target, ''))) continue;
|
|
419
|
+
let content = read(file);
|
|
420
|
+
if (!/aiwg:managed/.test(content) && !fs.existsSync(path.join(path.dirname(file), '.aiwg-managed')) && !fs.existsSync(path.join(path.dirname(file), '.aiwg-manifest.json'))) continue;
|
|
421
|
+
const skill = path.basename(file) === 'SKILL.md';
|
|
422
|
+
const name = skill ? path.basename(path.dirname(file)) : path.basename(file).replace(/\.(md|toml)$/, '');
|
|
423
|
+
const sidecarPath = path.join(skill ? path.dirname(file) : path.dirname(file), '.aiwg-manifest.json');
|
|
424
|
+
const sidecar = fs.existsSync(sidecarPath) ? JSON.parse(read(sidecarPath)) : null;
|
|
425
|
+
const source = sidecar?.skillSource ? path.join(sidecar.skillSource, 'SKILL.md') : sidecar?.managed?.[path.basename(file)]?.sourcePath;
|
|
426
|
+
const flowSource = opts.flowSkillSources?.get(file);
|
|
427
|
+
const targets = [...(flowSource ? targetsBySource.get(flowSource) || [] : []),
|
|
428
|
+
...(source ? targetsBySource.get(path.resolve(source)) || [] : opts.ruleDelivery.targets.filter(target =>
|
|
429
|
+
(skill ? target.type !== 'agent' : file.includes('/agents/') ? target.type === 'agent' : target.type === 'flow') &&
|
|
430
|
+
[target.name, path.basename(target.source, '.md'), path.basename(path.dirname(target.source))].includes(name)))];
|
|
431
|
+
const rules = targets.flatMap(target => opts.ruleDelivery.bindings.get(target.source) || []);
|
|
432
|
+
if (!rules.length && !content.includes('<!-- AIWG:bound-rules:start -->')) continue;
|
|
433
|
+
if (file.endsWith('.toml')) content = content.replace(/^developer_instructions = (".*")$/m, (_, value) => `developer_instructions = ${JSON.stringify(replaceBoundRulesBlock(JSON.parse(value), rules))}`);
|
|
434
|
+
else content = replaceBoundRulesBlock(content, rules);
|
|
435
|
+
put(file, content);
|
|
436
|
+
// Keep deployment hashes honest after projection by external helpers.
|
|
437
|
+
if (sidecar?.managed?.[path.basename(file)]) {
|
|
438
|
+
sidecar.managed[path.basename(file)].hash = `sha256:${contentHash(content)}`;
|
|
439
|
+
put(sidecarPath, `${JSON.stringify(sidecar, null, 2)}\n`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Bindings annotate the selected deployment; they do not select standard skills
|
|
445
|
+
* (ADR lines 102–108). Flow projections follow the same opt-in and tier routing. */
|
|
446
|
+
function projectBoundFlowSkills(opts, provider, selected) {
|
|
447
|
+
opts.flowSkillSources = new Map();
|
|
448
|
+
if (opts.rulesOnly || opts.commandsOnly || !(opts.deploySkills || opts.skillsOnly)) return;
|
|
449
|
+
const kernel = path.isAbsolute(provider.kernelSkillsPath || '') ? opts.kernelSkillsPath
|
|
450
|
+
: path.join(opts.target, provider.kernelSkillsPath || `.${provider.name}/skills`);
|
|
451
|
+
// Codex's opt-in standard tier shares the native skills directory.
|
|
452
|
+
const standard = provider.name === 'codex' ? kernel : path.isAbsolute(provider.paths.skills)
|
|
453
|
+
? provider.paths.skills : path.join(opts.target, provider.paths.skills);
|
|
454
|
+
const flows = opts.ruleDelivery.targets.filter(entry => entry.type === 'flow' && selected(entry));
|
|
455
|
+
const sources = new Set(flows.map(entry => entry.source));
|
|
456
|
+
for (const root of new Set([kernel, standard])) {
|
|
457
|
+
if (!fs.existsSync(root)) continue;
|
|
458
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
459
|
+
if (!entry.isDirectory()) continue;
|
|
460
|
+
const dir = path.join(root, entry.name);
|
|
461
|
+
const file = path.join(dir, 'SKILL.md');
|
|
462
|
+
const content = read(file);
|
|
463
|
+
const receiptFile = path.join(dir, '.aiwg-manifest.json');
|
|
464
|
+
const receipt = fs.existsSync(receiptFile) ? JSON.parse(read(receiptFile)) : null;
|
|
465
|
+
// Recognize the old projector's exact generated wrapper as well as new
|
|
466
|
+
// receipts. Ordinary deployed skills and operator files are preserved.
|
|
467
|
+
const legacy = !receipt?.skillSource && !receipt?.flowSource && /aiwg:managed/.test(content)
|
|
468
|
+
&& content.includes(`description: Invoke ${entry.name}.\n`)
|
|
469
|
+
&& content.includes('```yaml\n') && content.includes('<!-- AIWG:bound-rules:start -->');
|
|
470
|
+
const stale = receipt?.flowSource && (opts.copyStandardSkills !== true
|
|
471
|
+
|| !sources.has(receipt.flowSource) || root !== standard);
|
|
472
|
+
if (!legacy && !stale) continue;
|
|
473
|
+
fs.rmSync(file, { force: true });
|
|
474
|
+
if (receipt?.flowSource) {
|
|
475
|
+
fs.rmSync(receiptFile, { force: true });
|
|
476
|
+
fs.rmSync(path.join(dir, '.aiwg-managed'), { force: true });
|
|
477
|
+
}
|
|
478
|
+
if (!fs.readdirSync(dir).length) fs.rmdirSync(dir);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (opts.copyStandardSkills !== true) return;
|
|
482
|
+
for (const entry of flows) {
|
|
483
|
+
const name = entry.name || path.basename(entry.source).replace(/\.(md|ya?ml)$/, '');
|
|
484
|
+
const dir = path.join(standard, name);
|
|
485
|
+
const file = path.join(dir, 'SKILL.md');
|
|
486
|
+
const receiptFile = path.join(dir, '.aiwg-manifest.json');
|
|
487
|
+
const receipt = fs.existsSync(receiptFile) ? JSON.parse(read(receiptFile)) : null;
|
|
488
|
+
// A selected authored skill may already represent this flow. Keep its body;
|
|
489
|
+
// finishBindings adds both artifact identities' contracts after projection.
|
|
490
|
+
if (!fs.existsSync(file) || receipt?.flowSource === entry.source) {
|
|
491
|
+
const body = read(entry.source);
|
|
492
|
+
const markdown = entry.source.endsWith('.md') ? body : `---\nname: ${name}\nnamespace: aiwg\ndescription: Invoke ${name}.\n---\n\n\`\`\`yaml\n${body}\n\`\`\`\n`;
|
|
493
|
+
const content = managed(replaceBoundRulesBlock(markdown, opts.ruleDelivery.bindings.get(entry.source)), opts);
|
|
494
|
+
put(file, content);
|
|
495
|
+
put(path.join(dir, '.aiwg-managed'), 'true\n');
|
|
496
|
+
put(receiptFile, `${JSON.stringify({ flowSource: entry.source,
|
|
497
|
+
managed: { 'SKILL.md': { sourcePath: entry.source, hash: `sha256:${contentHash(content)}` } } }, null, 2)}\n`);
|
|
498
|
+
}
|
|
499
|
+
opts.flowSkillSources.set(file, entry.source);
|
|
500
|
+
if (['warp', 'hermes'].includes(provider.name)) opts.definitionRoutes.push(`${name}: ${file}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Count rule sections, excluding aggregated agent/command definitions and project prose. */
|
|
505
|
+
export function warpRuleContent(content) {
|
|
506
|
+
const blocks = [...content.matchAll(/<!-- AIWG:rule-startup:start -->[\s\S]*?<!-- AIWG:rule-startup:end -->/g)].map(match => match[0]);
|
|
507
|
+
// Remove managed blocks first so their internal headings cannot be counted twice.
|
|
508
|
+
const remaining = content.replace(/<!-- AIWG:rule-startup:start -->[\s\S]*?<!-- AIWG:rule-startup:end -->/g, '');
|
|
509
|
+
const sections = [...remaining.matchAll(/^## (?:AIWG Rules|On-Demand Rules)\r?\n[\s\S]*?(?=^## |(?![\s\S]))/gm)].map(match => match[0]);
|
|
510
|
+
return [...sections, ...blocks].join('\n');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function startupFiles(opts, provider) {
|
|
514
|
+
const bridgeNames = { claude: ['CLAUDE.md'], cursor: ['AGENTS.md'], codex: ['AGENTS.md'], hermes: ['AGENTS.md', '.hermes.md'], pi: ['AGENTS.md'], warp: ['WARP.md'], openclaw: [] };
|
|
515
|
+
const paths = ['AIWG.md', 'WORKSPACE.md', '.aiwg/AIWG.md', ...bridgeNames[provider.name]].map(name => path.join(opts.target, name));
|
|
516
|
+
if (opts.activeWorkspace) paths.push(path.join(opts.activeWorkspace, 'AGENTS.md'));
|
|
517
|
+
if (['claude', 'cursor', 'openclaw'].includes(provider.name)) {
|
|
518
|
+
const dir = path.isAbsolute(provider.paths.rules) ? provider.paths.rules : path.join(opts.target, provider.paths.rules);
|
|
519
|
+
paths.push(...walk(dir).filter(file => /\.(md|mdc)$/.test(file)));
|
|
520
|
+
}
|
|
521
|
+
return [...new Set(paths)].filter(file => fs.existsSync(file)).map(file => {
|
|
522
|
+
let content = read(file);
|
|
523
|
+
if (['claude', 'cursor'].includes(provider.name)) {
|
|
524
|
+
const parsed = parseRuleFrontmatter(content);
|
|
525
|
+
if (parsed.data?.paths || parsed.data?.globs) content = content.slice(0, content.length - parsed.body.length);
|
|
526
|
+
}
|
|
527
|
+
// WARP.md aggregates agents and commands as well; only its rule content
|
|
528
|
+
// (managed startup blocks, legacy inline rules, and on-demand lists) counts
|
|
529
|
+
// toward the rules startup budget.
|
|
530
|
+
if (provider.name === 'warp' && path.basename(file) === 'WARP.md') content = warpRuleContent(content);
|
|
531
|
+
return { path: file, content };
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Plans contain only JSON values and a scratch directory. The preparing process
|
|
536
|
+
// owns that directory until disposal; cross-process transfer needs a separate lease.
|
|
537
|
+
const stagedCleanups = new Map();
|
|
538
|
+
export class RuleDeploymentStaleError extends Error {
|
|
539
|
+
constructor(message, paths = []) {
|
|
540
|
+
super(message);
|
|
541
|
+
this.name = 'RuleDeploymentStaleError';
|
|
542
|
+
this.code = 'AIWG_RULE_DEPLOYMENT_STALE';
|
|
543
|
+
this.paths = paths;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function capturePreimage(file, ancestors = new Set()) {
|
|
547
|
+
let stat;
|
|
548
|
+
try { stat = fs.lstatSync(file); } catch (error) {
|
|
549
|
+
if (error.code === 'ENOENT') return { kind: 'missing' };
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
const mode = stat.mode & 0o7777;
|
|
553
|
+
if (stat.isSymbolicLink()) {
|
|
554
|
+
const target = fs.readlinkSync(file);
|
|
555
|
+
const resolved = path.resolve(path.dirname(file), target);
|
|
556
|
+
if (ancestors.has(resolved)) throw new Error(`Cyclic deployment preimage: ${file}`);
|
|
557
|
+
return { kind: 'symlink', mode, target, resolved: capturePreimage(resolved, new Set([...ancestors, resolved])) };
|
|
558
|
+
}
|
|
559
|
+
if (stat.isFile()) return { kind: 'file', mode, digest: fileDigest(file) };
|
|
560
|
+
if (stat.isDirectory()) return { kind: 'directory', mode, entries: fs.readdirSync(file).sort().map(name => [name, capturePreimage(path.join(file, name), new Set([...ancestors, path.resolve(file)]))]) };
|
|
561
|
+
throw new Error(`Unsupported deployment preimage: ${file}`);
|
|
562
|
+
}
|
|
563
|
+
// cpSync materializes links; compare its output with the recursively resolved
|
|
564
|
+
// content while retaining the original link-shaped preimage for commit drift.
|
|
565
|
+
function materializeInput(from, to, ancestors = new Set()) {
|
|
566
|
+
if (!fs.existsSync(from)) return;
|
|
567
|
+
const real = fs.realpathSync(from);
|
|
568
|
+
if (ancestors.has(real)) throw new Error(`Cyclic deployment input: ${from}`);
|
|
569
|
+
const stat = fs.statSync(from);
|
|
570
|
+
if (stat.isDirectory()) {
|
|
571
|
+
fs.mkdirSync(to, { recursive: true });
|
|
572
|
+
for (const name of fs.readdirSync(from)) materializeInput(path.join(from, name), path.join(to, name), new Set([...ancestors, real]));
|
|
573
|
+
fs.chmodSync(to, stat.mode & 0o7777);
|
|
574
|
+
} else if (stat.isFile()) {
|
|
575
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
576
|
+
fs.copyFileSync(from, to); fs.chmodSync(to, stat.mode & 0o7777);
|
|
577
|
+
} else throw new Error(`Unsupported deployment input: ${from}`);
|
|
578
|
+
}
|
|
579
|
+
function materializedPreimage(state) {
|
|
580
|
+
if (state.kind === 'symlink') return materializedPreimage(state.resolved);
|
|
581
|
+
if (state.kind === 'directory') return { ...state, entries: state.entries.map(([name, child]) => [name, materializedPreimage(child)]) };
|
|
582
|
+
return state;
|
|
583
|
+
}
|
|
584
|
+
function snapshotPreimages(paths) {
|
|
585
|
+
return [...new Set(paths.map(file => path.resolve(file)))].map(file => ({ path: file, state: capturePreimage(file) }));
|
|
586
|
+
}
|
|
587
|
+
function planSeal(plan) { return createHash('sha256').update(JSON.stringify(plan)).digest('hex'); }
|
|
588
|
+
function planOwner(plan) {
|
|
589
|
+
const owner = stagedCleanups.get(plan?.lease);
|
|
590
|
+
return owner && owner.seal === planSeal(plan) ? owner : null;
|
|
591
|
+
}
|
|
592
|
+
export function disposeRuleDeploymentPlan(plan) {
|
|
593
|
+
const owner = planOwner(plan);
|
|
594
|
+
if (owner) { stagedCleanups.delete(plan.lease); owner.cleanup(); }
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Release process-local ownership while retaining a coordinator-owned stage. */
|
|
598
|
+
export function detachRuleDeploymentPlan(plan) {
|
|
599
|
+
const owner = planOwner(plan);
|
|
600
|
+
if (!owner || !owner.transferable) throw new Error('Rule deployment plan is not transferable');
|
|
601
|
+
stagedCleanups.delete(plan.lease);
|
|
602
|
+
owner.detach();
|
|
603
|
+
return plan;
|
|
604
|
+
}
|
|
605
|
+
function captureAnchors(paths) {
|
|
606
|
+
const names = new Set();
|
|
607
|
+
for (const name of paths) {
|
|
608
|
+
let current = path.resolve(name);
|
|
609
|
+
for (;;) {
|
|
610
|
+
names.add(current);
|
|
611
|
+
const parent = path.dirname(current);
|
|
612
|
+
if (parent === current) break;
|
|
613
|
+
current = parent;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return [...names].sort().map(file => {
|
|
617
|
+
try {
|
|
618
|
+
const stat = fs.lstatSync(file, { bigint: true });
|
|
619
|
+
return { path: file, dev: String(stat.dev), ino: String(stat.ino), mode: String(stat.mode),
|
|
620
|
+
link: stat.isSymbolicLink() ? fs.readlinkSync(file) : null, real: fs.realpathSync(file) };
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (error.code === 'ENOENT') return { path: file, missing: true };
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Opaque coordinator-owned input pool. A plan retains a lease until committed or
|
|
629
|
+
// disposed, so disposing the creator's handle cannot invalidate active stages.
|
|
630
|
+
const inputPools = new WeakMap();
|
|
631
|
+
// Executable links are identity-bound but their installed code is trusted host
|
|
632
|
+
// state. Data files, modes, directory membership, and links are sealed in full.
|
|
633
|
+
function poolInventory(file) {
|
|
634
|
+
let stat;
|
|
635
|
+
try { stat = fs.lstatSync(file); } catch (error) {
|
|
636
|
+
if (error.code === 'ENOENT') return { kind: 'missing' };
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
const mode = stat.mode & 0o7777;
|
|
640
|
+
if (stat.isSymbolicLink()) return { kind: 'symlink', mode, target: fs.readlinkSync(file) };
|
|
641
|
+
if (stat.isFile()) return { kind: 'file', mode, digest: fileDigest(file) };
|
|
642
|
+
if (stat.isDirectory()) return { kind: 'directory', mode, entries: fs.readdirSync(file).sort().map(name => [name, poolInventory(path.join(file, name))]) };
|
|
643
|
+
throw new Error(`Unsupported shared input pool entry: ${file}`);
|
|
644
|
+
}
|
|
645
|
+
function setPoolWritable(file, writable) {
|
|
646
|
+
let stat;
|
|
647
|
+
try { stat = fs.lstatSync(file); } catch (error) { if (error.code === 'ENOENT') return; throw error; }
|
|
648
|
+
if (stat.isSymbolicLink()) return;
|
|
649
|
+
if (stat.isDirectory() && writable) fs.chmodSync(file, (stat.mode & 0o7777) | 0o700);
|
|
650
|
+
if (stat.isDirectory()) for (const name of fs.readdirSync(file)) setPoolWritable(path.join(file, name), writable);
|
|
651
|
+
if (!writable) fs.chmodSync(file, (stat.mode & 0o7777) & ~0o222);
|
|
652
|
+
}
|
|
653
|
+
function verifyInputPool(pool) {
|
|
654
|
+
try {
|
|
655
|
+
if (planSeal(poolInventory(pool.snapshotRoot)) === pool.inventorySeal &&
|
|
656
|
+
JSON.stringify(captureAnchors(pool.execAnchors.map(anchor => anchor.path))) === JSON.stringify(pool.execAnchors)) return;
|
|
657
|
+
} catch (error) { throw new Error(`Shared input pool changed: ${error.message}`); }
|
|
658
|
+
throw new Error('Shared input pool changed');
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function createRuleDeploymentInputPool(srcRoot) {
|
|
662
|
+
const corpusRoot = resolveAiwgRoot(srcRoot) || srcRoot;
|
|
663
|
+
const sourceTree = fs.existsSync(path.join(corpusRoot, 'agentic', 'code')) ? path.join(corpusRoot, 'agentic', 'code') : srcRoot;
|
|
664
|
+
const isCorpusTree = path.relative(corpusRoot, sourceTree) === path.join('agentic', 'code');
|
|
665
|
+
const paths = [sourceTree, path.join(corpusRoot, 'prebuilt', 'framework-relations'), path.join(corpusRoot, 'package.json'), path.join(corpusRoot, 'schemas')];
|
|
666
|
+
const preimages = snapshotPreimages(paths);
|
|
667
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'aiwg-rule-input-'));
|
|
668
|
+
const cleanup = trackTemporaryDirectory(temp);
|
|
669
|
+
try {
|
|
670
|
+
const snapshotRoot = path.join(temp, 'input');
|
|
671
|
+
const snapshotTree = isCorpusTree ? path.join(snapshotRoot, 'agentic', 'code') : snapshotRoot;
|
|
672
|
+
materializeInput(sourceTree, snapshotTree);
|
|
673
|
+
if (JSON.stringify(capturePreimage(snapshotTree)) !== JSON.stringify(materializedPreimage(preimages[0].state))) throw new Error('Source changed while preparing immutable snapshot');
|
|
674
|
+
const trustedExec = [];
|
|
675
|
+
if (isCorpusTree) {
|
|
676
|
+
for (const rel of ['package.json', 'prebuilt/framework-relations', 'schemas']) {
|
|
677
|
+
const from = path.join(corpusRoot, rel), to = path.join(snapshotRoot, rel);
|
|
678
|
+
materializeInput(from, to);
|
|
679
|
+
const expected = preimages.find(item => item.path === path.resolve(from));
|
|
680
|
+
if (JSON.stringify(capturePreimage(to)) !== JSON.stringify(materializedPreimage(expected.state))) throw new Error(`Package input changed while preparing snapshot: ${from}`);
|
|
681
|
+
}
|
|
682
|
+
for (const rel of ['tools', 'dist', 'node_modules', 'src']) {
|
|
683
|
+
const from = path.join(corpusRoot, rel), to = path.join(snapshotRoot, rel);
|
|
684
|
+
if (fs.existsSync(from)) { fs.symlinkSync(from, to, 'dir'); trustedExec.push(from); }
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
setPoolWritable(snapshotRoot, false);
|
|
688
|
+
const inventorySeal = planSeal(poolInventory(snapshotRoot));
|
|
689
|
+
const execAnchors = captureAnchors(trustedExec);
|
|
690
|
+
const safeCleanup = () => { setPoolWritable(snapshotRoot, true); cleanup(); };
|
|
691
|
+
const token = Object.freeze({});
|
|
692
|
+
inputPools.set(token, { corpusRoot: path.resolve(corpusRoot), sourceTree: path.resolve(sourceTree), snapshotRoot, snapshotTree, isCorpusTree, preimages, inventorySeal, execAnchors, refs: 1, closed: false, cleanup: safeCleanup, transferable: true });
|
|
693
|
+
return token;
|
|
694
|
+
} catch (error) { setPoolWritable(path.join(temp, 'input'), true); cleanup(); throw error; }
|
|
695
|
+
}
|
|
696
|
+
function releaseInputPool(pool) { if (--pool.refs === 0) pool.cleanup(); }
|
|
697
|
+
export function disposeRuleDeploymentInputPool(token) {
|
|
698
|
+
const pool = inputPools.get(token);
|
|
699
|
+
if (pool && !pool.closed) { pool.closed = true; releaseInputPool(pool); }
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** A descriptor contains immutable snapshot identity, never its cleanup lease. */
|
|
703
|
+
export function describeRuleDeploymentInputPool(token) {
|
|
704
|
+
const pool = inputPools.get(token);
|
|
705
|
+
if (!pool || pool.closed || !pool.transferable) throw new Error('Invalid or closed rule deployment input pool');
|
|
706
|
+
verifyInputPool(pool);
|
|
707
|
+
return {
|
|
708
|
+
version: 1,
|
|
709
|
+
corpusRoot: pool.corpusRoot,
|
|
710
|
+
sourceTree: pool.sourceTree,
|
|
711
|
+
snapshotRoot: pool.snapshotRoot,
|
|
712
|
+
snapshotTree: pool.snapshotTree,
|
|
713
|
+
isCorpusTree: pool.isCorpusTree,
|
|
714
|
+
inventorySeal: pool.inventorySeal,
|
|
715
|
+
execAnchors: pool.execAnchors,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** Import a read-only pool view in a preparation worker. The coordinator keeps
|
|
720
|
+
* the only cleanup lease and later restores the live-source preimages. */
|
|
721
|
+
export function importRuleDeploymentInputPool(descriptor) {
|
|
722
|
+
if (!descriptor || descriptor.version !== 1 || typeof descriptor.inventorySeal !== 'string' ||
|
|
723
|
+
!Array.isArray(descriptor.execAnchors)) throw new Error('Invalid rule deployment input pool descriptor');
|
|
724
|
+
for (const key of ['corpusRoot', 'sourceTree', 'snapshotRoot', 'snapshotTree']) {
|
|
725
|
+
if (typeof descriptor[key] !== 'string' || !path.isAbsolute(descriptor[key])) throw new Error('Invalid rule deployment input pool descriptor');
|
|
726
|
+
}
|
|
727
|
+
const token = Object.freeze({});
|
|
728
|
+
const pool = { ...descriptor, corpusRoot: path.resolve(descriptor.corpusRoot), sourceTree: path.resolve(descriptor.sourceTree),
|
|
729
|
+
snapshotRoot: path.resolve(descriptor.snapshotRoot), snapshotTree: path.resolve(descriptor.snapshotTree), preimages: [],
|
|
730
|
+
refs: 1, closed: false, cleanup: () => {}, transferable: false };
|
|
731
|
+
verifyInputPool(pool);
|
|
732
|
+
inputPools.set(token, pool);
|
|
733
|
+
return token;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function within(root, file) {
|
|
737
|
+
const relative = path.relative(path.resolve(root), path.resolve(file));
|
|
738
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function safeRelative(root, name) {
|
|
742
|
+
return typeof name === 'string' && name !== '' && name !== '.' && path.normalize(name) === name && !path.isAbsolute(name) && within(root, path.join(root, name));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function exactObjectKeys(value, keys) {
|
|
746
|
+
return value && typeof value === 'object' && !Array.isArray(value) &&
|
|
747
|
+
JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function validPreimageState(state) {
|
|
751
|
+
if (!state || typeof state !== 'object' || Array.isArray(state) || typeof state.kind !== 'string') return false;
|
|
752
|
+
if (state.kind === 'missing') return Object.keys(state).length === 1;
|
|
753
|
+
if (!Number.isSafeInteger(state.mode) || state.mode < 0) return false;
|
|
754
|
+
if (state.kind === 'file') return Object.keys(state).length === 3 && /^[a-f0-9]{64}$/.test(state.digest);
|
|
755
|
+
if (state.kind === 'symlink') return Object.keys(state).length === 4 && typeof state.target === 'string' && validPreimageState(state.resolved);
|
|
756
|
+
return state.kind === 'directory' && Object.keys(state).length === 3 && Array.isArray(state.entries) && state.entries.every(entry =>
|
|
757
|
+
Array.isArray(entry) && entry.length === 2 && typeof entry[0] === 'string' && entry[0] !== '' && !entry[0].includes(path.sep) && validPreimageState(entry[1]));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function expectedAnchorPaths(paths) {
|
|
761
|
+
const names = new Set();
|
|
762
|
+
for (const name of paths) {
|
|
763
|
+
let current = path.resolve(name);
|
|
764
|
+
for (;;) {
|
|
765
|
+
names.add(current);
|
|
766
|
+
const parent = path.dirname(current);
|
|
767
|
+
if (parent === current) break;
|
|
768
|
+
current = parent;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return [...names].sort();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** Adopt a detached worker plan into the coordinator's process-local lease. */
|
|
775
|
+
export function adoptRuleDeploymentPlan(detached, { scratch, target, inputPool, cleanup }) {
|
|
776
|
+
const invalid = () => { throw new Error('Invalid transferred rule deployment plan'); };
|
|
777
|
+
if (!exactObjectKeys(detached, ['version', 'lease', 'temp', 'target', 'stage', 'rootNames', 'workspaceDirs', 'preimages', 'poolSeal', 'anchors', 'snapshotRoot', 'corpusRoot', 'sourceMappings', 'staged', 'report', 'mappings']) ||
|
|
778
|
+
detached.version !== 1 || typeof detached.lease !== 'string' || !(detached.poolSeal === null || typeof detached.poolSeal === 'string') ||
|
|
779
|
+
!detached.report || typeof detached.report !== 'object' || Array.isArray(detached.report) || typeof scratch !== 'string' || typeof target !== 'string' || typeof cleanup !== 'function') invalid();
|
|
780
|
+
const root = path.resolve(scratch), expectedTarget = path.resolve(target);
|
|
781
|
+
if (path.resolve(detached.temp || '') !== root || path.resolve(detached.stage || '') !== path.join(root, 'project') || path.resolve(detached.target || '') !== expectedTarget) invalid();
|
|
782
|
+
if (!Array.isArray(detached.staged) || !Array.isArray(detached.mappings) || !Array.isArray(detached.rootNames) || !Array.isArray(detached.preimages) ||
|
|
783
|
+
!Array.isArray(detached.workspaceDirs) || !Array.isArray(detached.anchors) || !Array.isArray(detached.sourceMappings) ||
|
|
784
|
+
typeof detached.snapshotRoot !== 'string' || !path.isAbsolute(detached.snapshotRoot) || typeof detached.corpusRoot !== 'string' || !path.isAbsolute(detached.corpusRoot)) invalid();
|
|
785
|
+
if (detached.rootNames.some(name => !safeRelative(expectedTarget, name))) invalid();
|
|
786
|
+
const pool = inputPool && inputPools.get(inputPool);
|
|
787
|
+
if (inputPool && (!pool || pool.closed || !pool.transferable)) invalid();
|
|
788
|
+
if (pool) {
|
|
789
|
+
verifyInputPool(pool);
|
|
790
|
+
if (detached.poolSeal !== pool.inventorySeal || path.resolve(detached.snapshotRoot || '') !== pool.snapshotRoot) invalid();
|
|
791
|
+
} else if (detached.poolSeal) invalid();
|
|
792
|
+
const expectedArtifactRoot = path.resolve(resolveProjectAiwgDir(expectedTarget));
|
|
793
|
+
const validSourceMapping = item => within(root, item.to) || (pool && within(pool.snapshotRoot, item.to)) ||
|
|
794
|
+
(path.resolve(item.from) === path.join(expectedTarget, '.aiwg') && path.resolve(item.to) === expectedArtifactRoot);
|
|
795
|
+
if (detached.workspaceDirs.some(name => !safeRelative(expectedTarget, name)) ||
|
|
796
|
+
detached.preimages.some(item => !item || Object.keys(item).length !== 2 || typeof item.path !== 'string' || !path.isAbsolute(item.path) || !validPreimageState(item.state)) ||
|
|
797
|
+
detached.staged.some(item => !item || Object.keys(item).length !== 2 || typeof item.path !== 'string' || !within(root, item.path) || !validPreimageState(item.state)) ||
|
|
798
|
+
detached.sourceMappings.some(item => !item || Object.keys(item).length !== 2 || typeof item.from !== 'string' || !path.isAbsolute(item.from) ||
|
|
799
|
+
typeof item.to !== 'string' || !path.isAbsolute(item.to) || !validSourceMapping(item)) ||
|
|
800
|
+
detached.mappings.some(item => !item || Object.keys(item).length !== 3 || typeof item.from !== 'string' || !path.isAbsolute(item.from) ||
|
|
801
|
+
typeof item.to !== 'string' || !within(root, item.to) || within(root, item.from) || !Array.isArray(item.before) || item.before.some(entry =>
|
|
802
|
+
!Array.isArray(entry) || entry.length !== 2 || !safeRelative(item.to, entry[0]) || !/^[a-f0-9]{64}$/.test(entry[1])))) invalid();
|
|
803
|
+
const expectedStaged = [path.join(root, 'project'), ...detached.mappings.map(item => path.resolve(item.to))].sort();
|
|
804
|
+
if (JSON.stringify(detached.staged.map(item => path.resolve(item.path)).sort()) !== JSON.stringify([...new Set(expectedStaged)])) invalid();
|
|
805
|
+
const anchorPaths = expectedAnchorPaths([expectedTarget, ...detached.mappings.map(item => item.from), ...detached.workspaceDirs.map(name => path.join(expectedTarget, name))]);
|
|
806
|
+
if (detached.anchors.some(item => !item || typeof item.path !== 'string' || !path.isAbsolute(item.path) ||
|
|
807
|
+
(item.missing === true ? Object.keys(item).length !== 2 : Object.keys(item).sort().join(',') !== 'dev,ino,link,mode,path,real' ||
|
|
808
|
+
!['dev', 'ino', 'mode', 'real'].every(key => typeof item[key] === 'string') || !(item.link === null || typeof item.link === 'string'))) ||
|
|
809
|
+
JSON.stringify(detached.anchors.map(item => path.resolve(item.path)).sort()) !== JSON.stringify(anchorPaths)) invalid();
|
|
810
|
+
const plan = structuredClone(detached);
|
|
811
|
+
plan.lease = randomUUID();
|
|
812
|
+
if (pool) {
|
|
813
|
+
const existing = new Set(plan.preimages.map(item => path.resolve(item.path)));
|
|
814
|
+
plan.preimages.unshift(...pool.preimages.filter(item => !existing.has(path.resolve(item.path))));
|
|
815
|
+
pool.refs++;
|
|
816
|
+
}
|
|
817
|
+
const ownerCleanup = () => { try { cleanup(); } finally { if (pool) releaseInputPool(pool); } };
|
|
818
|
+
stagedCleanups.set(plan.lease, { temp: root, cleanup: ownerCleanup, pool, seal: planSeal(plan), transferable: false, detach: () => {} });
|
|
819
|
+
return plan;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** Stage only provider/context output, never the user's source tree. On failure
|
|
823
|
+
* no staged output (including skills and behaviors) reaches the prior deployment. */
|
|
824
|
+
export async function prepareRuleDeployment(original, provider, deploy) {
|
|
825
|
+
const budgetEnv = { ...process.env };
|
|
826
|
+
const ruleDir = path.isAbsolute(provider.paths.rules || '') ? provider.paths.rules : path.join(original.target, provider.paths.rules || `.${provider.name}/rules`);
|
|
827
|
+
const manifestFile = path.join(ruleDir, '.aiwg-manifest.json');
|
|
828
|
+
const previousText = fs.existsSync(manifestFile) ? read(manifestFile) : '';
|
|
829
|
+
const previous = previousText ? JSON.parse(previousText) : {};
|
|
830
|
+
const corpusRoot = resolveAiwgRoot(original.srcRoot) || original.srcRoot;
|
|
831
|
+
const sourceTree = fs.existsSync(path.join(corpusRoot, 'agentic', 'code'))
|
|
832
|
+
? path.join(corpusRoot, 'agentic', 'code') : original.srcRoot;
|
|
833
|
+
const pool = original.inputPool && inputPools.get(original.inputPool);
|
|
834
|
+
if (original.inputPool && (!pool || pool.closed || pool.corpusRoot !== path.resolve(corpusRoot) || pool.sourceTree !== path.resolve(sourceTree))) throw new Error('Invalid or closed rule deployment input pool');
|
|
835
|
+
if (pool) verifyInputPool(pool);
|
|
836
|
+
let projectConfig = original.config;
|
|
837
|
+
if (!projectConfig) {
|
|
838
|
+
try { projectConfig = JSON.parse(read(path.join(original.target, '.aiwg', 'aiwg.config')) || '{}'); } catch { projectConfig = {}; }
|
|
839
|
+
}
|
|
840
|
+
const artifactRoot = resolveProjectAiwgDir(original.target);
|
|
841
|
+
const userIndexRoot = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'aiwg', 'index');
|
|
842
|
+
const projectGraphFiles = ['.index/metadata.json', '.index/project/metadata.json'].map(name => path.join(artifactRoot, name));
|
|
843
|
+
const userGraphFiles = ['user'].map(name => path.join(userIndexRoot, name, 'metadata.json'));
|
|
844
|
+
const localSources = Object.values(projectConfig.installed || {}).filter(entry => entry?.localPath)
|
|
845
|
+
.map(entry => path.resolve(original.target, entry.localPath));
|
|
846
|
+
const selectedRules = collectDeploymentRuleSources({ ...original, previousRuleSources: previous.ruleSources || [] });
|
|
847
|
+
const initialPreimages = [...(pool ? pool.preimages : snapshotPreimages([sourceTree, path.join(corpusRoot, 'prebuilt', 'framework-relations'), path.join(corpusRoot, 'package.json'), path.join(corpusRoot, 'schemas')])), ...snapshotPreimages([path.join(corpusRoot, '.aiwg', '.index'),
|
|
848
|
+
manifestFile, path.join(original.target, '.aiwg', 'aiwg.config'), path.join(original.target, '.aiwg', 'AIWG.md'), path.join(original.target, '.aiwg', 'skills'), path.join(original.target, '.aiwg-location'), ...projectGraphFiles, ...userGraphFiles, ...localSources,
|
|
849
|
+
...(path.resolve(original.srcRoot) === path.resolve(corpusRoot) ? [] : [original.srcRoot]), ...selectedRules, ...selectedRules.map(file => path.dirname(file)),
|
|
850
|
+
userConfigFile('aiwg.config'), process.env.AIWG_USER_REGISTRY_PATH || userConfigFile('installed.json'),
|
|
851
|
+
...(original.ruleSources || []), ...(previous.ruleSources || []), ...(original.bindingEntries || []).map(entry => entry.path)])];
|
|
852
|
+
let manifestState = initialPreimages.find(item => item.path === path.resolve(manifestFile)).state;
|
|
853
|
+
if (manifestState.kind === 'symlink') manifestState = manifestState.resolved;
|
|
854
|
+
const manifestDigest = previousText ? createHash('sha256').update(previousText).digest('hex') : null;
|
|
855
|
+
if ((manifestState.kind === 'missing' ? null : manifestState.digest) !== manifestDigest) throw new Error('Rule manifest changed while preparing snapshot');
|
|
856
|
+
const transferable = Boolean(original.ruleDeploymentScratch);
|
|
857
|
+
const temp = transferable ? path.resolve(original.ruleDeploymentScratch) : fs.mkdtempSync(path.join(os.tmpdir(), 'aiwg-rule-deploy-'));
|
|
858
|
+
if (transferable) {
|
|
859
|
+
const stat = fs.lstatSync(temp);
|
|
860
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.readdirSync(temp).length) throw new Error('Invalid rule deployment scratch directory');
|
|
861
|
+
}
|
|
862
|
+
const removeStage = transferable
|
|
863
|
+
? () => fs.rmSync(temp, { recursive: true, force: true })
|
|
864
|
+
: trackTemporaryDirectory(temp);
|
|
865
|
+
if (pool) pool.refs++;
|
|
866
|
+
const removeTemp = () => { removeStage(); if (pool) releaseInputPool(pool); };
|
|
867
|
+
const lease = randomUUID();
|
|
868
|
+
stagedCleanups.set(lease, { temp, cleanup: removeTemp, detach: () => { if (pool) releaseInputPool(pool); }, pool, seal: null, transferable });
|
|
869
|
+
const mappings = [];
|
|
870
|
+
const preimages = [...initialPreimages];
|
|
871
|
+
let retained = false;
|
|
872
|
+
const originals = { ...provider.paths };
|
|
873
|
+
const logicalTarget = original.target;
|
|
874
|
+
const priorAiwgRoot = process.env.AIWG_ROOT;
|
|
875
|
+
const priorRegistry = process.env.AIWG_USER_REGISTRY_PATH;
|
|
876
|
+
const priorArtifacts = process.env.AIWG_ARTIFACTS_PATH;
|
|
877
|
+
const priorXdgData = process.env.XDG_DATA_HOME;
|
|
878
|
+
try {
|
|
879
|
+
const snapshotRoot = pool ? pool.snapshotRoot : path.join(temp, 'input');
|
|
880
|
+
const treeRelative = path.relative(corpusRoot, sourceTree);
|
|
881
|
+
const isCorpusTree = treeRelative === path.join('agentic', 'code');
|
|
882
|
+
const snapshotTree = isCorpusTree ? path.join(snapshotRoot, 'agentic', 'code') : snapshotRoot;
|
|
883
|
+
if (!pool) { fs.mkdirSync(path.dirname(snapshotTree), { recursive: true }); materializeInput(sourceTree, snapshotTree); }
|
|
884
|
+
const expectedTree = initialPreimages.find(item => item.path === path.resolve(sourceTree));
|
|
885
|
+
if (!pool && JSON.stringify(capturePreimage(snapshotTree)) !== JSON.stringify(materializedPreimage(expectedTree.state))) throw new Error('Source changed while preparing immutable snapshot');
|
|
886
|
+
if (isCorpusTree && !pool) {
|
|
887
|
+
for (const rel of ['package.json', 'prebuilt', 'schemas']) {
|
|
888
|
+
const from = path.join(corpusRoot, rel), to = path.join(snapshotRoot, rel);
|
|
889
|
+
if (fs.existsSync(from)) materializeInput(from, to);
|
|
890
|
+
const expected = initialPreimages.find(item => item.path === path.resolve(from));
|
|
891
|
+
if (expected && JSON.stringify(capturePreimage(to)) !== JSON.stringify(materializedPreimage(expected.state))) throw new Error(`Package input changed while preparing snapshot: ${from}`);
|
|
892
|
+
}
|
|
893
|
+
// The projector helpers execute installed code but receive the snapshot as data.
|
|
894
|
+
for (const rel of ['tools', 'dist', 'node_modules', 'src']) {
|
|
895
|
+
const from = path.join(corpusRoot, rel), to = path.join(snapshotRoot, rel);
|
|
896
|
+
if (fs.existsSync(from)) fs.symlinkSync(from, to, 'dir');
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
// Keep standalone fixture/bundle snapshots from inheriting the installed
|
|
900
|
+
// package's unrelated prebuilt relations through module-root fallback.
|
|
901
|
+
process.env.AIWG_ROOT = snapshotRoot;
|
|
902
|
+
const frozenConfigDir = path.join(temp, 'config');
|
|
903
|
+
fs.mkdirSync(frozenConfigDir);
|
|
904
|
+
const frozenFile = (from, name, expected) => {
|
|
905
|
+
const to = path.join(frozenConfigDir, name);
|
|
906
|
+
if (fs.existsSync(from)) { fs.copyFileSync(from, to); fs.chmodSync(to, fs.statSync(from).mode & 0o7777); }
|
|
907
|
+
if (JSON.stringify(capturePreimage(to)) !== JSON.stringify(materializedPreimage(expected || { kind: 'missing' }))) throw new Error(`Configuration changed while preparing snapshot: ${from}`);
|
|
908
|
+
return to;
|
|
909
|
+
};
|
|
910
|
+
const projectFile = path.join(logicalTarget, '.aiwg', 'aiwg.config');
|
|
911
|
+
const projectFileState = initialPreimages.find(item => item.path === path.resolve(projectFile))?.state;
|
|
912
|
+
const userFile = userConfigFile('aiwg.config');
|
|
913
|
+
const registryFile = priorRegistry || userConfigFile('installed.json');
|
|
914
|
+
const projectSnapshot = frozenFile(projectFile, 'project.json', projectFileState);
|
|
915
|
+
const userSnapshot = frozenFile(userFile, 'user.json', initialPreimages.find(item => item.path === path.resolve(userFile))?.state);
|
|
916
|
+
const registrySnapshot = frozenFile(registryFile, 'registry.json', initialPreimages.find(item => item.path === path.resolve(registryFile))?.state);
|
|
917
|
+
const readConfig = file => fs.existsSync(file) ? JSON.parse(read(file)) : {};
|
|
918
|
+
const effectiveConfig = structuredClone(original.config || readConfig(projectSnapshot));
|
|
919
|
+
const pinConfigs = structuredClone(original.pinConfigs || { project: effectiveConfig, user: readConfig(userSnapshot) });
|
|
920
|
+
const frozenLocal = Object.values(pinConfigs.project.installed || {}).filter(entry => entry?.localPath)
|
|
921
|
+
.map(entry => path.resolve(logicalTarget, entry.localPath));
|
|
922
|
+
if (JSON.stringify([...new Set(frozenLocal)].sort()) !== JSON.stringify([...new Set(localSources)].sort())) throw new Error('Project-local configuration changed while preparing snapshot');
|
|
923
|
+
const sourceMappings = [{ from: path.resolve(sourceTree), to: snapshotTree }];
|
|
924
|
+
for (const [index, from] of [...new Set(localSources)].entries()) {
|
|
925
|
+
const to = path.join(temp, 'local-input', String(index));
|
|
926
|
+
const expected = initialPreimages.find(item => item.path === from)?.state;
|
|
927
|
+
if (fs.existsSync(from)) { fs.mkdirSync(path.dirname(to), { recursive: true }); materializeInput(from, to); }
|
|
928
|
+
if (JSON.stringify(capturePreimage(to)) !== JSON.stringify(materializedPreimage(expected || { kind: 'missing' }))) throw new Error(`Project-local input changed while preparing snapshot: ${from}`);
|
|
929
|
+
sourceMappings.push({ from, to });
|
|
930
|
+
}
|
|
931
|
+
const externalInputs = [...new Set([...(path.resolve(original.srcRoot) === path.resolve(corpusRoot) ? [] : [original.srcRoot]), ...(original.ruleSources || []), ...(previous.ruleSources || []),
|
|
932
|
+
...(original.bindingEntries || []).map(entry => entry.path)].map(file => path.resolve(file)))];
|
|
933
|
+
for (const [index, from] of externalInputs.entries()) {
|
|
934
|
+
if (sourceMappings.some(item => from === item.from || from.startsWith(`${item.from}${path.sep}`))) continue;
|
|
935
|
+
const to = path.join(temp, 'external-input', String(index), path.basename(from));
|
|
936
|
+
const expected = initialPreimages.find(item => item.path === from)?.state;
|
|
937
|
+
if (fs.existsSync(from)) { fs.mkdirSync(path.dirname(to), { recursive: true }); materializeInput(from, to); }
|
|
938
|
+
if (JSON.stringify(capturePreimage(to)) !== JSON.stringify(materializedPreimage(expected || { kind: 'missing' }))) throw new Error(`External input changed while preparing snapshot: ${from}`);
|
|
939
|
+
sourceMappings.push({ from, to });
|
|
940
|
+
}
|
|
941
|
+
for (const entry of Object.values(pinConfigs.project.installed || {})) {
|
|
942
|
+
if (!entry?.localPath) continue;
|
|
943
|
+
const from = path.resolve(logicalTarget, entry.localPath);
|
|
944
|
+
entry.localPath = sourceMappings.find(mapping => mapping.from === from).to;
|
|
945
|
+
}
|
|
946
|
+
process.env.AIWG_USER_REGISTRY_PATH = registrySnapshot;
|
|
947
|
+
let stagedProjectRoot;
|
|
948
|
+
const mapSource = file => {
|
|
949
|
+
let absolute = path.resolve(file);
|
|
950
|
+
if (stagedProjectRoot && absolute.startsWith(`${stagedProjectRoot}${path.sep}`)) {
|
|
951
|
+
const logical = path.join(logicalTarget, path.relative(stagedProjectRoot, absolute));
|
|
952
|
+
if (sourceMappings.some(item => logical === item.from || logical.startsWith(`${item.from}${path.sep}`)) || !fs.existsSync(absolute)) absolute = logical;
|
|
953
|
+
}
|
|
954
|
+
const mapping = [...sourceMappings].sort((a, b) => b.from.length - a.from.length)
|
|
955
|
+
.find(item => absolute === item.from || absolute.startsWith(`${item.from}${path.sep}`));
|
|
956
|
+
if (mapping) return path.join(mapping.to, path.relative(mapping.from, absolute));
|
|
957
|
+
if (isCorpusTree && absolute === path.resolve(corpusRoot)) return snapshotRoot;
|
|
958
|
+
return file;
|
|
959
|
+
};
|
|
960
|
+
const logicalInputPath = file => {
|
|
961
|
+
let result = file;
|
|
962
|
+
for (const mapping of [...sourceMappings].sort((a, b) => b.to.length - a.to.length)) result = result.split(mapping.to).join(mapping.from);
|
|
963
|
+
return result.split(snapshotRoot).join(corpusRoot);
|
|
964
|
+
};
|
|
965
|
+
original = { ...original, config: effectiveConfig, srcRoot: mapSource(original.srcRoot), pinConfigs, mapInputPath: mapSource, logicalInputPath,
|
|
966
|
+
...(original.ruleSources ? { ruleSources: original.ruleSources.map(mapSource) } : {}),
|
|
967
|
+
...(original.bindingEntries ? { bindingEntries: original.bindingEntries.map(entry => ({ ...entry, path: mapSource(entry.path) })) } : {}) };
|
|
968
|
+
const target = path.resolve(original.target);
|
|
969
|
+
const stage = path.join(temp, 'project');
|
|
970
|
+
stagedProjectRoot = stage;
|
|
971
|
+
fs.mkdirSync(stage);
|
|
972
|
+
const mount = (from, to) => {
|
|
973
|
+
if (mappings.some(mapping => mapping.from === from)) return;
|
|
974
|
+
const mountPreimage = snapshotPreimages([from])[0];
|
|
975
|
+
preimages.push(mountPreimage);
|
|
976
|
+
if (fs.existsSync(from)) materializeInput(from, to);
|
|
977
|
+
const before = new Map(walk(to).map(file => [path.relative(to, file), fileDigest(file)]));
|
|
978
|
+
if (JSON.stringify(capturePreimage(to)) !== JSON.stringify(materializedPreimage(mountPreimage.state))) throw new Error(`Deployment input changed while preparing snapshot: ${from}`);
|
|
979
|
+
mappings.push({ from, to, before });
|
|
980
|
+
};
|
|
981
|
+
// Mount directories independently so only provider-owned surfaces are promoted.
|
|
982
|
+
for (const dir of [`.${provider.name}`, '.agents', '.aiwg/skills']) mount(path.join(target, dir), path.join(stage, dir));
|
|
983
|
+
fs.mkdirSync(path.join(stage, '.aiwg'), { recursive: true });
|
|
984
|
+
if (fs.existsSync(projectSnapshot)) materializeInput(projectSnapshot, path.join(stage, '.aiwg', 'aiwg.config'));
|
|
985
|
+
const rootNames = ['AGENTS.md', 'AIWG.md', 'WORKSPACE.md', 'CLAUDE.md', 'WARP.md', '.hermes.md', '.aiwg/AIWG.md', '.aiwg/frameworks/registry.json'];
|
|
986
|
+
for (const framework of getFrameworksForMode(original.srcRoot, original.mode)) {
|
|
987
|
+
for (const entry of framework.manifest?.memory?.creates || []) {
|
|
988
|
+
if (typeof entry?.path !== 'string' || !entry.path.startsWith('.aiwg/') || !path.extname(entry.path)) continue;
|
|
989
|
+
const safe = path.normalize(entry.path);
|
|
990
|
+
if (!safe.startsWith(`.aiwg${path.sep}`)) throw new Error(`Invalid framework workspace path: ${entry.path}`);
|
|
991
|
+
if (!rootNames.includes(safe)) rootNames.push(safe);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
preimages.push(...snapshotPreimages(rootNames.map(name => path.join(target, name))));
|
|
995
|
+
for (const name of rootNames) {
|
|
996
|
+
const from = path.join(target, name), to = path.join(stage, name);
|
|
997
|
+
if (fs.existsSync(from)) { fs.mkdirSync(path.dirname(to), { recursive: true }); fs.copyFileSync(from, to); fs.chmodSync(to, fs.statSync(from).mode & 0o7777); }
|
|
998
|
+
const expected = preimages.find(item => item.path === path.resolve(from));
|
|
999
|
+
if (JSON.stringify(capturePreimage(to)) !== JSON.stringify(materializedPreimage(expected.state))) throw new Error(`Deployment input changed while preparing snapshot: ${from}`);
|
|
1000
|
+
}
|
|
1001
|
+
const home = path.join(temp, 'home');
|
|
1002
|
+
for (const [key, dir] of Object.entries(provider.paths)) {
|
|
1003
|
+
if (!path.isAbsolute(dir)) continue;
|
|
1004
|
+
const to = path.join(temp, 'native', key);
|
|
1005
|
+
mount(dir, to);
|
|
1006
|
+
provider.paths[key] = to;
|
|
1007
|
+
}
|
|
1008
|
+
let kernel = provider.kernelSkillsPath;
|
|
1009
|
+
if (kernel && path.isAbsolute(kernel)) {
|
|
1010
|
+
const to = path.join(temp, 'native', 'kernel-skills');
|
|
1011
|
+
mount(kernel, to);
|
|
1012
|
+
kernel = to;
|
|
1013
|
+
}
|
|
1014
|
+
// Codex's legacy prompt helper is home-global. Give it an isolated home and
|
|
1015
|
+
// promote only its output after validation.
|
|
1016
|
+
if (provider.name === 'codex' && (original.deployCommands || original.commandsOnly)) mount(path.join(os.homedir(), '.codex', 'prompts'), path.join(home, '.codex', 'prompts'));
|
|
1017
|
+
let hermesLegacySkills;
|
|
1018
|
+
let legacyCodexSkills;
|
|
1019
|
+
if (provider.name === 'hermes') {
|
|
1020
|
+
const source = path.join(path.dirname(provider.kernelSkillsPath), '.aiwg', 'skills');
|
|
1021
|
+
hermesLegacySkills = path.join(temp, 'hermes-legacy-skills');
|
|
1022
|
+
mount(source, hermesLegacySkills);
|
|
1023
|
+
}
|
|
1024
|
+
if (provider.name === 'codex' && (original.deploySkills || original.skillsOnly)) {
|
|
1025
|
+
legacyCodexSkills = path.join(temp, 'codex-legacy-skills');
|
|
1026
|
+
mount(path.join(os.homedir(), '.codex', 'skills'), legacyCodexSkills);
|
|
1027
|
+
}
|
|
1028
|
+
// Existing receipts carry logical source paths. Projectors operate against
|
|
1029
|
+
// the immutable snapshot, so translate those paths before stale cleanup;
|
|
1030
|
+
// promotion normalizes them back to their logical paths.
|
|
1031
|
+
for (const { to } of mappings) {
|
|
1032
|
+
for (const file of walk(to).filter(name => path.basename(name) === '.aiwg-manifest.json')) {
|
|
1033
|
+
let receipt; try { receipt = JSON.parse(read(file)); } catch { continue; }
|
|
1034
|
+
let changed = false;
|
|
1035
|
+
const remap = value => {
|
|
1036
|
+
if (!value || typeof value !== 'object') return;
|
|
1037
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1038
|
+
if (['skillSource', 'flowSource', 'sourcePath'].includes(key) && typeof child === 'string' && path.isAbsolute(child)) {
|
|
1039
|
+
const mapped = mapSource(child); if (mapped !== child) { value[key] = mapped; changed = true; }
|
|
1040
|
+
}
|
|
1041
|
+
else remap(child);
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
remap(receipt); if (changed) fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
let activeWorkspace;
|
|
1048
|
+
if (provider.resolveOpenClawWorkspace) {
|
|
1049
|
+
const from = provider.resolveOpenClawWorkspace();
|
|
1050
|
+
activeWorkspace = path.join(temp, 'active-workspace');
|
|
1051
|
+
mount(from, activeWorkspace);
|
|
1052
|
+
}
|
|
1053
|
+
// Graph discovery must read the same immutable inputs as projection, including
|
|
1054
|
+
// relocated project indexes and shared user indexes.
|
|
1055
|
+
const graphRoot = path.join(temp, 'graph-artifacts');
|
|
1056
|
+
const xdgRoot = path.join(temp, 'xdg-data');
|
|
1057
|
+
const copyGraph = (from, to) => {
|
|
1058
|
+
const expected = initialPreimages.find(item => item.path === path.resolve(from)).state;
|
|
1059
|
+
materializeInput(from, to);
|
|
1060
|
+
if (JSON.stringify(capturePreimage(to)) !== JSON.stringify(materializedPreimage(expected))) throw new Error(`Graph input changed while preparing snapshot: ${from}`);
|
|
1061
|
+
};
|
|
1062
|
+
for (const from of projectGraphFiles) copyGraph(from, path.join(graphRoot, path.relative(artifactRoot, from)));
|
|
1063
|
+
for (const from of userGraphFiles) copyGraph(from, path.join(xdgRoot, 'aiwg', 'index', path.relative(userIndexRoot, from)));
|
|
1064
|
+
// Non-index graph targets remain logical until selected, then are copied
|
|
1065
|
+
// individually by the discovered-input loop below.
|
|
1066
|
+
if (path.resolve(artifactRoot) !== path.resolve(target, '.aiwg')) sourceMappings.push({ from: path.resolve(target, '.aiwg'), to: artifactRoot });
|
|
1067
|
+
process.env.AIWG_ARTIFACTS_PATH = graphRoot;
|
|
1068
|
+
process.env.XDG_DATA_HOME = xdgRoot;
|
|
1069
|
+
const delivery = await prepareRuleDelivery({ ...original, target: stage, sharedProjectGraph: path.resolve(artifactRoot) !== path.resolve(target, '.aiwg'), previousRuleSources: (previous.ruleSources || []).map(mapSource) });
|
|
1070
|
+
// Provider writes belong to the staged project control plane, not the
|
|
1071
|
+
// read-only graph snapshot used for binding discovery.
|
|
1072
|
+
process.env.AIWG_ARTIFACTS_PATH = path.join(stage, '.aiwg');
|
|
1073
|
+
// Discovery can select absolute user/project sources not known at entry.
|
|
1074
|
+
// Bind the exact copied bytes before any projector consumes them.
|
|
1075
|
+
const remappedBindings = new Map();
|
|
1076
|
+
for (const entry of delivery.targets) {
|
|
1077
|
+
const source = path.resolve(entry.source);
|
|
1078
|
+
const alreadyFrozen = source.startsWith(`${temp}${path.sep}`);
|
|
1079
|
+
if (!alreadyFrozen) {
|
|
1080
|
+
const state = capturePreimage(source);
|
|
1081
|
+
const to = path.join(temp, 'discovered-input', String(sourceMappings.length), path.basename(source));
|
|
1082
|
+
materializeInput(source, to);
|
|
1083
|
+
if (JSON.stringify(capturePreimage(to)) !== JSON.stringify(materializedPreimage(state))) throw new Error(`Discovered input changed while preparing snapshot: ${source}`);
|
|
1084
|
+
preimages.push({ path: source, state });
|
|
1085
|
+
sourceMappings.push({ from: source, to });
|
|
1086
|
+
entry.source = to;
|
|
1087
|
+
}
|
|
1088
|
+
const rules = delivery.bindings.get(source);
|
|
1089
|
+
if (rules) remappedBindings.set(entry.source, rules);
|
|
1090
|
+
}
|
|
1091
|
+
delivery.bindings = remappedBindings;
|
|
1092
|
+
preimages.push(...snapshotPreimages([...delivery.rules.map(rule => rule.file), ...delivery.targets.map(entry => entry.source), path.join(target, '.aiwg', 'aiwg.config')]));
|
|
1093
|
+
const opts = { ...original, target: stage, dryRun: false, ruleDelivery: delivery,
|
|
1094
|
+
kernelSkillsPath: kernel, deploymentHome: home, activeWorkspace, hermesLegacySkills, legacyCodexSkills, staging: true };
|
|
1095
|
+
await deploy(opts);
|
|
1096
|
+
if (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly && opts.deployBehaviors !== false) {
|
|
1097
|
+
deployEmulatedBehaviors(collectBehaviorDirs(opts.srcRoot), provider.name, stage, opts);
|
|
1098
|
+
}
|
|
1099
|
+
const selectedRoots = [original.srcRoot, ...getFrameworksForMode(original.srcRoot, original.mode).map(framework => framework.path), ...discoverAddons(original.srcRoot).map(addon => addon.path)];
|
|
1100
|
+
const selected = entry => selectedRoots.some(root => /^(agents|skills|commands|flows|workflows)[/\\]/.test(path.relative(root, entry.source)));
|
|
1101
|
+
opts.definitionRoutes = [];
|
|
1102
|
+
if (provider.name === 'hermes' && !opts.kernelOnly && !opts.skillsOnly && !opts.commandsOnly && !opts.rulesOnly) {
|
|
1103
|
+
const agents = delivery.targets.filter(target => target.type === 'agent' && selected(target));
|
|
1104
|
+
const selectedAgents = new Set(filterAgentFiles(agents.map(target => target.source), opts));
|
|
1105
|
+
for (const target of agents.filter(target => selectedAgents.has(target.source))) {
|
|
1106
|
+
const source = target.source;
|
|
1107
|
+
put(path.join(stage, '.hermes', 'agents', path.basename(source)), replaceBoundRulesBlock(read(source), delivery.bindings.get(source)));
|
|
1108
|
+
opts.definitionRoutes.push(`${target.name}: .hermes/agents/${path.basename(source)}`);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
projectBoundFlowSkills(opts, provider, selected);
|
|
1112
|
+
if (provider.name === 'warp') {
|
|
1113
|
+
for (const entry of delivery.targets.filter(entry => entry.type === 'agent' && selected(entry))) {
|
|
1114
|
+
const dest = path.join(stage, '.warp', 'agents', path.basename(entry.source));
|
|
1115
|
+
if (fs.existsSync(dest)) opts.definitionRoutes.push(`${entry.name}: .warp/agents/${path.basename(entry.source)}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (opts.deployRules || opts.rulesOnly || (!opts.skillsOnly && !opts.commandsOnly && ['hermes', 'warp'].includes(provider.name))) projectRules(opts, provider);
|
|
1119
|
+
finishBindings(opts, provider);
|
|
1120
|
+
// Normalize all generated path wrappers before counting or promoting bytes.
|
|
1121
|
+
const normalize = content => {
|
|
1122
|
+
for (const mapping of [...mappings].sort((a, b) => b.to.length - a.to.length)) content = content.split(mapping.to).join(mapping.from);
|
|
1123
|
+
for (const mapping of [...sourceMappings].sort((a, b) => b.to.length - a.to.length)) content = content.split(mapping.to).join(mapping.from);
|
|
1124
|
+
return content.split(snapshotRoot).join(corpusRoot).split(stage).join(target);
|
|
1125
|
+
};
|
|
1126
|
+
const projected = startupFiles(opts, provider).map(file => ({ path: normalize(file.path), content: normalize(file.content) }));
|
|
1127
|
+
const config = effectiveConfig;
|
|
1128
|
+
const guarded = guardRulesInlineBudget({ provider: provider.name, files: projected, config, env: budgetEnv,
|
|
1129
|
+
reportOnly: delivery.pins.length ? original.allowOverBudget === true : original.reportOnly !== false && !original.strict && !budgetEnv.CI });
|
|
1130
|
+
guarded.report.pins = delivery.pins.map(rule => ({ id: rule.id, name: rule.name, matches: rule.matches, canonical_activation: rule.canonical_activation, bytes: Buffer.byteLength(rule.body || '') }));
|
|
1131
|
+
const pinDetails = delivery.pins.map(rule => `${rule.name}: ${Buffer.byteLength(rule.body || '')} body bytes via ${JSON.stringify(rule.matches)}`).join('; ');
|
|
1132
|
+
const budgetFailure = message => new Error(`${provider.name}: ${message}; ${guarded.report.selected_bytes} startup bytes. Pins: ${pinDetails || '(none)'}.${delivery.pins.length ? ' Use --allow-over-budget to retain every pinned body.' : ''}`);
|
|
1133
|
+
for (const diagnostic of guarded.diagnostics) console.warn(diagnostic.message);
|
|
1134
|
+
if (!guarded.passed) throw budgetFailure(guarded.diagnostics.map(d => d.message).join('\n'));
|
|
1135
|
+
const agents = normalize(read(path.join(stage, 'AGENTS.md')));
|
|
1136
|
+
if (provider.name === 'codex' && Buffer.byteLength(agents) >= 32_768 && !(delivery.pins.length && original.allowOverBudget)) throw budgetFailure(`Codex AGENTS.md (${Buffer.byteLength(agents)} bytes) exceeds its 32 KB hard cap`);
|
|
1137
|
+
if (provider.name === 'hermes') {
|
|
1138
|
+
for (const name of ['AGENTS.md', '.hermes.md']) {
|
|
1139
|
+
const content = normalize(read(path.join(stage, name)));
|
|
1140
|
+
if (!(delivery.pins.length && original.allowOverBudget)) {
|
|
1141
|
+
try { provider.assertContextBudget(content, name, delivery.rules); }
|
|
1142
|
+
catch (error) { throw budgetFailure(error.message); }
|
|
1143
|
+
}
|
|
1144
|
+
if (content.length > 15_000) console.warn(`Hermes ${name} above 15000 character soft limit (${content.length})`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
// Absolute provider roots can nest (Hermes standard skills live beneath
|
|
1148
|
+
// its kernel root). A more specific mount owns that subtree exclusively;
|
|
1149
|
+
// the parent's older snapshot must not resurrect pruned files/directories.
|
|
1150
|
+
const delegated = (from, rel) => mappings.some(mapping => mapping.from !== from
|
|
1151
|
+
&& mapping.from.startsWith(`${from}${path.sep}`)
|
|
1152
|
+
&& (path.join(from, rel) === mapping.from || path.join(from, rel).startsWith(`${mapping.from}${path.sep}`)));
|
|
1153
|
+
if (pool) verifyInputPool(pool);
|
|
1154
|
+
if (original.previewReport) return { report: guarded.report, noop: true };
|
|
1155
|
+
if (original.dryRun) {
|
|
1156
|
+
// A validated plan must describe the operator's final paths, rather than
|
|
1157
|
+
// exposing only the temporary paths used to assemble that plan.
|
|
1158
|
+
for (const { from, to, before } of mappings) {
|
|
1159
|
+
const after = new Set(walk(to).map(file => path.relative(to, file)).filter(rel => !delegated(from, rel)));
|
|
1160
|
+
for (const rel of after) {
|
|
1161
|
+
const file = path.join(to, rel);
|
|
1162
|
+
if (before.get(rel) !== fileDigest(file)) console.log(`[dry-run] Would write ${normalize(file)}`);
|
|
1163
|
+
}
|
|
1164
|
+
for (const rel of before.keys()) if (!delegated(from, rel) && !after.has(rel)) console.log(`[dry-run] Would remove ${path.join(from, rel)}`);
|
|
1165
|
+
}
|
|
1166
|
+
for (const name of rootNames) {
|
|
1167
|
+
const file = path.join(stage, name);
|
|
1168
|
+
if (fs.existsSync(file) && normalize(read(file)) !== read(path.join(target, name))) console.log(`[dry-run] Would write ${path.join(target, name)}`);
|
|
1169
|
+
}
|
|
1170
|
+
console.log(`[dry-run] Planned ${provider.name} deployment: ${guarded.report.selected_bytes} startup bytes; no output promoted.`);
|
|
1171
|
+
return { report: guarded.report, noop: true };
|
|
1172
|
+
}
|
|
1173
|
+
const workspaceDirs = [];
|
|
1174
|
+
const collectDirs = (dir, rel = '') => {
|
|
1175
|
+
if (!fs.existsSync(dir)) return;
|
|
1176
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1177
|
+
if (!entry.isDirectory()) continue;
|
|
1178
|
+
const name = path.join(rel, entry.name); workspaceDirs.push(name);
|
|
1179
|
+
collectDirs(path.join(dir, entry.name), name);
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
collectDirs(path.join(stage, '.aiwg'), '.aiwg');
|
|
1183
|
+
const plan = { version: 1, lease, temp, target, stage, rootNames, workspaceDirs, preimages, poolSeal: pool?.inventorySeal || null,
|
|
1184
|
+
anchors: captureAnchors([target, ...mappings.map(mapping => mapping.from), ...workspaceDirs.map(dir => path.join(target, dir))]),
|
|
1185
|
+
snapshotRoot, corpusRoot, sourceMappings, staged: snapshotPreimages([stage, ...mappings.map(mapping => mapping.to)]), report: guarded.report,
|
|
1186
|
+
mappings: mappings.map(({ from, to, before }) => ({ from, to, before: [...before] })) };
|
|
1187
|
+
stagedCleanups.get(lease).seal = planSeal(plan);
|
|
1188
|
+
retained = true;
|
|
1189
|
+
return plan;
|
|
1190
|
+
} finally {
|
|
1191
|
+
Object.assign(provider.paths, originals);
|
|
1192
|
+
if (priorAiwgRoot === undefined) delete process.env.AIWG_ROOT; else process.env.AIWG_ROOT = priorAiwgRoot;
|
|
1193
|
+
if (priorRegistry === undefined) delete process.env.AIWG_USER_REGISTRY_PATH; else process.env.AIWG_USER_REGISTRY_PATH = priorRegistry;
|
|
1194
|
+
if (priorArtifacts === undefined) delete process.env.AIWG_ARTIFACTS_PATH; else process.env.AIWG_ARTIFACTS_PATH = priorArtifacts;
|
|
1195
|
+
if (priorXdgData === undefined) delete process.env.XDG_DATA_HOME; else process.env.XDG_DATA_HOME = priorXdgData;
|
|
1196
|
+
if (!retained) { stagedCleanups.delete(lease); removeTemp(); }
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/** Validate every captured input before applying a staged provider plan. */
|
|
1201
|
+
export function commitRuleDeploymentPlan(plan) {
|
|
1202
|
+
if (plan.noop) return plan.report;
|
|
1203
|
+
try {
|
|
1204
|
+
if (plan.version !== 1 || !plan.temp || !plan.stage || !Array.isArray(plan.preimages)) throw new Error('Invalid rule deployment plan');
|
|
1205
|
+
const owner = planOwner(plan);
|
|
1206
|
+
if (!owner) throw new Error('Unowned or modified rule deployment plan');
|
|
1207
|
+
if (owner.pool) { if (plan.poolSeal !== owner.pool.inventorySeal) throw new Error('Shared input pool lease changed'); verifyInputPool(owner.pool); }
|
|
1208
|
+
if (JSON.stringify(snapshotPreimages(plan.staged.map(item => item.path))) !== JSON.stringify(plan.staged)) throw new Error('Staged provider output changed');
|
|
1209
|
+
if (JSON.stringify(captureAnchors(plan.anchors.map(item => item.path))) !== JSON.stringify(plan.anchors)) {
|
|
1210
|
+
throw new RuleDeploymentStaleError('Provider deployment root or ancestor changed', plan.anchors.map(item => item.path));
|
|
1211
|
+
}
|
|
1212
|
+
const stale = plan.preimages.filter(item => JSON.stringify(capturePreimage(item.path)) !== JSON.stringify(item.state)).map(item => item.path);
|
|
1213
|
+
if (stale.length) throw new RuleDeploymentStaleError(`Provider deployment preimage changed: ${stale.join(', ')}`, stale);
|
|
1214
|
+
const { target, stage, rootNames } = plan;
|
|
1215
|
+
for (const dir of plan.workspaceDirs || []) fs.mkdirSync(path.join(target, dir), { recursive: true });
|
|
1216
|
+
const mappings = plan.mappings.map(({ from, to, before }) => ({ from, to, before: new Map(before) }));
|
|
1217
|
+
const normalize = content => {
|
|
1218
|
+
for (const mapping of [...mappings].sort((a, b) => b.to.length - a.to.length)) content = content.split(mapping.to).join(mapping.from);
|
|
1219
|
+
for (const mapping of [...plan.sourceMappings].sort((a, b) => b.to.length - a.to.length)) content = content.split(mapping.to).join(mapping.from);
|
|
1220
|
+
return content.split(plan.snapshotRoot).join(plan.corpusRoot).split(stage).join(target);
|
|
1221
|
+
};
|
|
1222
|
+
const delegated = (from, rel) => mappings.some(mapping => mapping.from !== from
|
|
1223
|
+
&& mapping.from.startsWith(`${from}${path.sep}`)
|
|
1224
|
+
&& (path.join(from, rel) === mapping.from || path.join(from, rel).startsWith(`${mapping.from}${path.sep}`)));
|
|
1225
|
+
for (const { from, to, before } of mappings) {
|
|
1226
|
+
if (!fs.existsSync(to)) {
|
|
1227
|
+
if (before.size) fs.rmSync(from, { recursive: true, force: true });
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
promoteDirectories(to, from, rel => delegated(from, rel));
|
|
1231
|
+
const after = new Set(walk(to).map(file => path.relative(to, file)).filter(rel => !delegated(from, rel)));
|
|
1232
|
+
for (const rel of before.keys()) if (!delegated(from, rel) && !after.has(rel)) {
|
|
1233
|
+
fs.rmSync(path.join(from, rel), { force: true });
|
|
1234
|
+
// A pruned managed skill must disappear as a directory too. Remove
|
|
1235
|
+
// only empty parents absent from the staged result; preserve siblings.
|
|
1236
|
+
let parent = path.dirname(rel);
|
|
1237
|
+
while (parent !== '.' && !fs.existsSync(path.join(to, parent))) {
|
|
1238
|
+
try { fs.rmdirSync(path.join(from, parent)); } catch { break; }
|
|
1239
|
+
parent = path.dirname(parent);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
for (const rel of after) {
|
|
1243
|
+
const file = path.join(to, rel);
|
|
1244
|
+
if (before.get(rel) === fileDigest(file)) continue;
|
|
1245
|
+
const buffer = fs.readFileSync(file);
|
|
1246
|
+
// Provider output includes binary skill resources; normalize text only.
|
|
1247
|
+
const output = buffer.includes(0) ? buffer : Buffer.from(normalize(buffer.toString('utf8')));
|
|
1248
|
+
fs.mkdirSync(path.dirname(path.join(from, rel)), { recursive: true });
|
|
1249
|
+
fs.writeFileSync(path.join(from, rel), output);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
for (const name of rootNames) if (fs.existsSync(path.join(stage, name))) put(path.join(target, name), normalize(read(path.join(stage, name))));
|
|
1253
|
+
return plan.report;
|
|
1254
|
+
} finally {
|
|
1255
|
+
disposeRuleDeploymentPlan(plan);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** Stage and commit serially for existing provider callers. */
|
|
1260
|
+
export async function deployWithRuleDelivery(original, provider, deploy) {
|
|
1261
|
+
if (original.ruleDelivery) return deploy(original);
|
|
1262
|
+
const hook = globalThis[PREPARATION_HOOK];
|
|
1263
|
+
if (hook) {
|
|
1264
|
+
const plan = await prepareRuleDeployment({ ...original, inputPool: hook.inputPool, ruleDeploymentScratch: hook.scratch }, provider, deploy);
|
|
1265
|
+
const report = plan.report;
|
|
1266
|
+
hook.accept(detachRuleDeploymentPlan(plan));
|
|
1267
|
+
return report;
|
|
1268
|
+
}
|
|
1269
|
+
return commitRuleDeploymentPlan(await prepareRuleDeployment(original, provider, deploy));
|
|
1270
|
+
}
|