@aiwg/cli 2026.10.3 → 2026.10.5
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 +69 -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 +286 -131
- package/dist/src/artifacts/index-files.js +132 -18
- package/dist/src/artifacts/index-preparation.js +130 -0
- package/dist/src/artifacts/index-refresh.js +591 -0
- package/dist/src/artifacts/index-worker.mjs +11 -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 +137 -43
- 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 +95 -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 +252 -12
- package/dist/src/sessions/contracts.js +50 -3
- package/dist/src/sessions/coverage-preflight.js +27 -2
- 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 +145 -26
- package/dist/src/sessions/readers.js +106 -53
- package/dist/src/sessions/refresh.js +105 -0
- package/dist/src/sessions/repository.js +151 -54
- package/dist/src/sessions/workspace-discovery.js +292 -60
- 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
|
@@ -24,6 +24,7 @@ import path from 'path';
|
|
|
24
24
|
import os from 'os';
|
|
25
25
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
26
26
|
import {
|
|
27
|
+
withRuleDeployment,
|
|
27
28
|
ensureDir,
|
|
28
29
|
listMdFiles,
|
|
29
30
|
deploySkillDir,
|
|
@@ -192,8 +193,8 @@ export function mapModel(shorthand, modelCfg, modelsConfig) {
|
|
|
192
193
|
/**
|
|
193
194
|
* Generate a lean AGENTS.md for Hermes
|
|
194
195
|
*
|
|
195
|
-
* Hermes
|
|
196
|
-
*
|
|
196
|
+
* Hermes reaches AGENTS.md through its startup pointer on every task. Reserve
|
|
197
|
+
* the 19,000-character budget for CRITICAL kernel bodies and the kernel index.
|
|
197
198
|
* See: docs/integrations/hermes-quickstart.md (Part 3)
|
|
198
199
|
*/
|
|
199
200
|
// Hermes context cap is 20K chars (head-tail truncated above). Hard limit
|
|
@@ -201,6 +202,23 @@ export function mapModel(shorthand, modelCfg, modelsConfig) {
|
|
|
201
202
|
const HERMES_AGENTS_MD_HARD_CAP = 19_000;
|
|
202
203
|
const HERMES_AGENTS_MD_SOFT_WARN = 15_000;
|
|
203
204
|
|
|
205
|
+
/** Never truncate CRITICAL bodies. Report each contribution before promotion. */
|
|
206
|
+
export function assertContextBudget(content, name = 'AGENTS.md', rules = []) {
|
|
207
|
+
if (content.length <= HERMES_AGENTS_MD_HARD_CAP) return;
|
|
208
|
+
const critical = rules.filter(rule => rule.tagged && rule.activation === 'kernel'
|
|
209
|
+
&& String(rule.enforcement || '').trim().toLowerCase() === 'critical');
|
|
210
|
+
const sizes = critical.map(rule => ({ name: rule.name,
|
|
211
|
+
chars: `## Rule: ${rule.name}\n\n${rule.body.trim()}`.length }));
|
|
212
|
+
const total = sizes.reduce((sum, rule) => sum + rule.chars, 0) + Math.max(0, sizes.length - 1) * 2;
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Hermes ${name} (${content.length} chars) exceeds hard cap of ${HERMES_AGENTS_MD_HARD_CAP}. ` +
|
|
215
|
+
`Hermes truncates above 20K. ` +
|
|
216
|
+
(total > HERMES_AGENTS_MD_HARD_CAP ? 'CRITICAL kernel set alone exceeds the cap. ' : 'Assembled context exceeds the cap; review wrappers and operator additions. ') +
|
|
217
|
+
`CRITICAL kernel total: ${total} chars (bodies and rule headings).\n` +
|
|
218
|
+
sizes.map(rule => ` ${rule.name}: ${rule.chars} chars`).join('\n')
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
204
222
|
/**
|
|
205
223
|
* Top-7 CRITICAL rule directives, inlined into AGENTS.md for guaranteed
|
|
206
224
|
* priming on every Hermes turn (#1318 / S7; skill-discovery added #1347).
|
|
@@ -297,41 +315,32 @@ surface as provider-agnostic tools when configured.
|
|
|
297
315
|
**Artifacts** (project-required, .aiwg/ directory):
|
|
298
316
|
- \`mcp_aiwg_artifact_read\` / \`mcp_aiwg_artifact_write\``;
|
|
299
317
|
|
|
318
|
+
function mergeHermesContext(file, generated, kind) {
|
|
319
|
+
let existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
320
|
+
existing = existing.replace(/\n?<!-- AIWG:rule-startup:start -->[\s\S]*?<!-- AIWG:rule-startup:end -->\n?/g, '');
|
|
321
|
+
const start = `<!-- AIWG:hermes-${kind}:start -->`;
|
|
322
|
+
const end = `<!-- AIWG:hermes-${kind}:end -->`;
|
|
323
|
+
const managed = `${start}\n${generated}\n${end}`;
|
|
324
|
+
if (existing.includes(start)) return existing.slice(0, existing.indexOf(start)) + managed + existing.slice(existing.indexOf(end) + end.length);
|
|
325
|
+
if (kind === 'context' && existing.startsWith('# AIWG Integration') && existing.includes(HERMES_AGENTS_MD_FOOTER)) {
|
|
326
|
+
existing = existing.slice(existing.indexOf(HERMES_AGENTS_MD_FOOTER) + HERMES_AGENTS_MD_FOOTER.length);
|
|
327
|
+
} else if (kind === 'routing' && existing.includes(generated)) {
|
|
328
|
+
existing = existing.replace(generated, '');
|
|
329
|
+
}
|
|
330
|
+
return existing ? `${existing.trimEnd()}\n\n${managed}\n` : `<!-- aiwg-managed -->\n${managed}\n`;
|
|
331
|
+
}
|
|
332
|
+
|
|
300
333
|
export function generateAgentsMd(agentCount, skillCount, targetDir, opts) {
|
|
301
334
|
const { dryRun } = opts;
|
|
302
335
|
|
|
336
|
+
// Startup is reserved for rule contracts. Workflow routing, artifact catalogs,
|
|
337
|
+
// and the optional MCP tool inventory duplicate discover/show and consume
|
|
338
|
+
// Hermes's finite per-turn context before the kernel is appended.
|
|
303
339
|
const header = `# AIWG Integration
|
|
304
340
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
Use \`aiwg discover\` and \`aiwg show <type> <name>\` for the on-demand catalog.
|
|
309
|
-
The MCP sidecar (\`aiwg mcp serve\`) is optional.
|
|
310
|
-
|
|
311
|
-
## Route to AIWG When
|
|
312
|
-
|
|
313
|
-
- Structured artifacts needed (requirements, architecture, test plans, risk registers)
|
|
314
|
-
- Multi-step workflows with phase gates or checkpoints
|
|
315
|
-
- Template-driven output that persists across sessions
|
|
316
|
-
|
|
317
|
-
Handle in Hermes directly: one-off questions, short tasks, conversation.
|
|
318
|
-
|
|
319
|
-
## Memory Boundary
|
|
320
|
-
|
|
321
|
-
When AIWG returns an artifact: store path + one-sentence summary in MEMORY.md.
|
|
322
|
-
Do NOT copy artifact body text into memory. Reference, don't replicate.
|
|
323
|
-
|
|
324
|
-
Use \`delegate_task(goal="...", context="...")\` for AIWG workflows.
|
|
325
|
-
Child agents automatically exclude context files and memory.
|
|
326
|
-
|
|
327
|
-
## Artifact Store (.aiwg/)
|
|
328
|
-
|
|
329
|
-
Fetch on demand via \`mcp_aiwg_artifact_read\`:
|
|
330
|
-
- \`requirements/\` — use cases, user stories
|
|
331
|
-
- \`architecture/\` — SAD, ADRs
|
|
332
|
-
- \`planning/\` — phase plans
|
|
333
|
-
- \`testing/\` — test strategy
|
|
334
|
-
- \`security/\` — threat models
|
|
341
|
+
Skills: \`$HERMES_HOME/skills/\` (kernel), \`$HERMES_HOME/skills/.aiwg/\` (standard).
|
|
342
|
+
Find: \`aiwg discover "rule <topic>" --type rule\`. Fetch: \`aiwg show rule <name>\`.
|
|
343
|
+
MCP is optional; \`mcp_aiwg_rule_show\` exposes the same rule catalog.
|
|
335
344
|
`;
|
|
336
345
|
|
|
337
346
|
// On-demand tier (#1675): the MEDIUM/LOW rules not inlined above. Hermes has
|
|
@@ -339,18 +348,15 @@ Fetch on demand via \`mcp_aiwg_artifact_read\`:
|
|
|
339
348
|
const onDemandSection = opts.srcRoot
|
|
340
349
|
? renderOnDemandRuleSection(listOnDemandRuleFiles(opts.srcRoot))
|
|
341
350
|
: '';
|
|
342
|
-
const
|
|
351
|
+
const migrated = new Set((opts.ruleDelivery?.rules || []).filter(rule => rule.tagged).map(rule => rule.name));
|
|
352
|
+
const directives = CRITICAL_RULE_DIRECTIVES.split(/(?=^### Rule: )/m).filter(section => section.startsWith('### Rule: ') && !migrated.has(section.match(/^### Rule: ([^ \n]+)/)?.[1])).join('');
|
|
353
|
+
const sections = [header, directives];
|
|
343
354
|
if (onDemandSection) sections.push(onDemandSection);
|
|
344
|
-
|
|
345
|
-
const output = sections.join('\n\n');
|
|
355
|
+
// The optional tool inventory is available on demand through the CLI.
|
|
346
356
|
const destPath = path.join(targetDir, 'AGENTS.md');
|
|
357
|
+
const output = mergeHermesContext(destPath, sections.join('\n\n'), 'context');
|
|
347
358
|
|
|
348
|
-
|
|
349
|
-
throw new Error(
|
|
350
|
-
`Hermes AGENTS.md (${output.length} chars) exceeds hard cap of ${HERMES_AGENTS_MD_HARD_CAP}. ` +
|
|
351
|
-
`Hermes truncates above 20K. Trim the priming block or split rule bodies further.`
|
|
352
|
-
);
|
|
353
|
-
}
|
|
359
|
+
assertContextBudget(output, 'AGENTS.md', opts.ruleDelivery?.rules);
|
|
354
360
|
|
|
355
361
|
if (dryRun) {
|
|
356
362
|
console.log(`[dry-run] Would write AGENTS.md (${output.length} chars, ${Math.round(output.length / 4)} tokens estimated)`);
|
|
@@ -397,7 +403,7 @@ when the full AIWG project context is needed.
|
|
|
397
403
|
if (dryRun) {
|
|
398
404
|
console.log(`[dry-run] Would write .hermes.md (${body.length} chars)`);
|
|
399
405
|
} else {
|
|
400
|
-
fs.writeFileSync(destPath, body, 'utf8');
|
|
406
|
+
fs.writeFileSync(destPath, mergeHermesContext(destPath, body, 'routing'), 'utf8');
|
|
401
407
|
console.log(` Created .hermes.md (${body.length} chars)`);
|
|
402
408
|
}
|
|
403
409
|
return 1;
|
|
@@ -417,7 +423,7 @@ when the full AIWG project context is needed.
|
|
|
417
423
|
*/
|
|
418
424
|
export function deploySkills(skillDirs, opts) {
|
|
419
425
|
const standardDestDir = paths.skills;
|
|
420
|
-
const kernelDestDir = kernelSkillsPath;
|
|
426
|
+
const kernelDestDir = opts.kernelSkillsPath || kernelSkillsPath;
|
|
421
427
|
|
|
422
428
|
if (!opts.dryRun) {
|
|
423
429
|
console.log(` Deploying ${skillDirs.length} skills (kernel + standard split)...`);
|
|
@@ -431,6 +437,10 @@ export function deploySkills(skillDirs, opts) {
|
|
|
431
437
|
// ============================================================================
|
|
432
438
|
|
|
433
439
|
export async function deploy(opts) {
|
|
440
|
+
return withRuleDeployment(opts, { name, paths, kernelSkillsPath, assertContextBudget }, deploySelected);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function deploySelected(opts) {
|
|
434
444
|
const {
|
|
435
445
|
srcRoot,
|
|
436
446
|
target,
|
|
@@ -509,7 +519,7 @@ export async function deploy(opts) {
|
|
|
509
519
|
const kernelNames = computeAllKernelNames(srcRoot);
|
|
510
520
|
if (kernelNames != null) {
|
|
511
521
|
const desiredKernel = [...kernelNames, 'aiwg-orchestrate'];
|
|
512
|
-
pruneStaleAiwgSkills(kernelSkillsPath, desiredKernel, opts);
|
|
522
|
+
pruneStaleAiwgSkills(opts.kernelSkillsPath || kernelSkillsPath, desiredKernel, opts);
|
|
513
523
|
|
|
514
524
|
// Register kernel skills in Hermes's bundled manifest so the Curator
|
|
515
525
|
// (v0.12.0+, 7-day archival cycle) does not archive them. Standard
|
|
@@ -603,7 +613,7 @@ export async function deploy(opts) {
|
|
|
603
613
|
*/
|
|
604
614
|
export function updateBundledManifest(kernelSkillNames, opts) {
|
|
605
615
|
const { dryRun, quiet } = opts;
|
|
606
|
-
const manifestPath = path.join(kernelSkillsPath, '.bundled_manifest');
|
|
616
|
+
const manifestPath = path.join(opts.kernelSkillsPath || kernelSkillsPath, '.bundled_manifest');
|
|
607
617
|
const aiwgTag = 'aiwg-managed';
|
|
608
618
|
|
|
609
619
|
// Read existing manifest (if any) and split into "ours" vs "theirs"
|
|
@@ -668,7 +678,7 @@ export function migrateLegacySkillPath(opts) {
|
|
|
668
678
|
// this provider (env var first, platform default otherwise): with
|
|
669
679
|
// HERMES_HOME set, the legacy tree lives under that root — never under
|
|
670
680
|
// the process home directory.
|
|
671
|
-
const legacyPath = path.join(getHermesHome(), '.aiwg', 'skills');
|
|
681
|
+
const legacyPath = opts.hermesLegacySkills || path.join(getHermesHome(), '.aiwg', 'skills');
|
|
672
682
|
const newPath = paths.skills;
|
|
673
683
|
|
|
674
684
|
let legacyExists = false;
|
|
@@ -754,7 +764,7 @@ function deployAiwgOrchestrateSkill(srcRoot, opts) {
|
|
|
754
764
|
return;
|
|
755
765
|
}
|
|
756
766
|
|
|
757
|
-
const destDir = path.join(kernelSkillsPath, 'aiwg-orchestrate');
|
|
767
|
+
const destDir = path.join(opts.kernelSkillsPath || kernelSkillsPath, 'aiwg-orchestrate');
|
|
758
768
|
const destPath = path.join(destDir, 'SKILL.md');
|
|
759
769
|
|
|
760
770
|
if (fs.existsSync(destPath)) {
|
|
@@ -26,6 +26,7 @@ try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs;
|
|
|
26
26
|
import path from 'path';
|
|
27
27
|
import os from 'os';
|
|
28
28
|
import {
|
|
29
|
+
withRuleDeployment,
|
|
29
30
|
ensureDir,
|
|
30
31
|
listMdFiles,
|
|
31
32
|
listMdFilesRecursive,
|
|
@@ -191,7 +192,7 @@ function deploySkills(skillDirs, opts) {
|
|
|
191
192
|
// Standard: ~/.openclaw/.aiwg/skills/ (no extra aiwg/ — the .aiwg/ segment IS the namespace)
|
|
192
193
|
const standardDestDir = paths.skills;
|
|
193
194
|
// Kernel: ~/.openclaw/skills/aiwg/ (preserves legacy 2-level namespacing)
|
|
194
|
-
const kernelDestDir = path.join(kernelSkillsPath, 'aiwg');
|
|
195
|
+
const kernelDestDir = path.join(opts.kernelSkillsPath || kernelSkillsPath, 'aiwg');
|
|
195
196
|
|
|
196
197
|
deploySkillsWithKernelRouting(skillDirs, standardDestDir, kernelDestDir, opts);
|
|
197
198
|
|
|
@@ -396,6 +397,10 @@ export function getFileExtension(type) {
|
|
|
396
397
|
// ============================================================================
|
|
397
398
|
|
|
398
399
|
export async function deploy(opts) {
|
|
400
|
+
return withRuleDeployment(opts, { name, paths, kernelSkillsPath, resolveOpenClawWorkspace }, deploySelected);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function deploySelected(opts) {
|
|
399
404
|
const {
|
|
400
405
|
srcRoot,
|
|
401
406
|
mode,
|
|
@@ -485,9 +490,9 @@ export async function deploy(opts) {
|
|
|
485
490
|
// (`aiwg use` invokes deploy-agents.mjs once per framework + addon
|
|
486
491
|
// batch).
|
|
487
492
|
{
|
|
488
|
-
const _kernelDestDir = path.isAbsolute(kernelSkillsPath)
|
|
493
|
+
const _kernelDestDir = opts.kernelSkillsPath || (path.isAbsolute(kernelSkillsPath)
|
|
489
494
|
? kernelSkillsPath
|
|
490
|
-
: path.join(target, kernelSkillsPath);
|
|
495
|
+
: path.join(target, kernelSkillsPath));
|
|
491
496
|
pruneStaleAiwgSkills(_kernelDestDir, computeAllKernelNames(srcRoot), opts);
|
|
492
497
|
}
|
|
493
498
|
}
|
|
@@ -549,7 +554,7 @@ export async function deploy(opts) {
|
|
|
549
554
|
srcRoot, 'agentic', 'code', 'frameworks', 'sdlc-complete', 'templates', 'openclaw', 'AGENTS.md.aiwg-template',
|
|
550
555
|
);
|
|
551
556
|
if (fs.existsSync(templatePath)) {
|
|
552
|
-
const workspace = resolveOpenClawWorkspace();
|
|
557
|
+
const workspace = opts.activeWorkspace || resolveOpenClawWorkspace();
|
|
553
558
|
ensureDir(workspace, dryRun);
|
|
554
559
|
if (verbose || !opts.quiet) {
|
|
555
560
|
console.log(`\nBridging AIWG context into ${path.join(workspace, 'AGENTS.md')} + SOUL.md (#1562/#1572)...`);
|
|
@@ -11,6 +11,7 @@ import path from 'path';
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import {
|
|
13
13
|
collectFrameworkArtifacts,
|
|
14
|
+
withRuleDeployment,
|
|
14
15
|
createAgentsMdFromTemplate,
|
|
15
16
|
deployFiles,
|
|
16
17
|
deploySkillsWithKernelRouting,
|
|
@@ -139,6 +140,10 @@ export function getFileExtension() {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
export async function deploy(opts) {
|
|
143
|
+
return withRuleDeployment(opts, { name, paths, kernelSkillsPath }, deploySelected);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function deploySelected(opts) {
|
|
142
147
|
const normalizedMode = normalizeDeploymentMode(opts.mode);
|
|
143
148
|
const agentFiles = [];
|
|
144
149
|
const commandFiles = [];
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fork } from 'node:child_process';
|
|
5
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
8
|
+
import {
|
|
9
|
+
createRuleDeploymentInputPool,
|
|
10
|
+
describeRuleDeploymentInputPool,
|
|
11
|
+
disposeRuleDeploymentInputPool,
|
|
12
|
+
adoptRuleDeploymentPlan,
|
|
13
|
+
commitRuleDeploymentPlan,
|
|
14
|
+
disposeRuleDeploymentPlan,
|
|
15
|
+
} from './rule-delivery.mjs';
|
|
16
|
+
|
|
17
|
+
const { trackTemporaryDirectory } = await importImpl(import.meta.url, 'utils/temporary-directory.js');
|
|
18
|
+
const { signalChild } = await importImpl(import.meta.url, 'process/signal-child.mjs');
|
|
19
|
+
const DEFAULT_WORKER = fileURLToPath(new URL('./provider-prepare-worker.mjs', import.meta.url));
|
|
20
|
+
const CHECKOUT_NEEDS_TS_TRANSFORM = !fs.existsSync(fileURLToPath(new URL('../../../dist/src', import.meta.url)));
|
|
21
|
+
const PLAN_LIMIT = 256 * 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
const defaultTransaction = {
|
|
24
|
+
createPool: createRuleDeploymentInputPool,
|
|
25
|
+
describePool: describeRuleDeploymentInputPool,
|
|
26
|
+
disposePool: disposeRuleDeploymentInputPool,
|
|
27
|
+
adopt: adoptRuleDeploymentPlan,
|
|
28
|
+
commit: commitRuleDeploymentPlan,
|
|
29
|
+
disposePlan: disposeRuleDeploymentPlan,
|
|
30
|
+
isStale: error => error?.code === 'AIWG_RULE_DEPLOYMENT_STALE',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function exactKeys(value, keys) {
|
|
34
|
+
return value && typeof value === 'object' && !Array.isArray(value) &&
|
|
35
|
+
JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateJob(job, expectedOrder) {
|
|
39
|
+
if (!job || job.order !== expectedOrder || !Number.isSafeInteger(job.order) ||
|
|
40
|
+
typeof job.provider !== 'string' || !job.provider || typeof job.target !== 'string' || !path.isAbsolute(job.target) ||
|
|
41
|
+
!Array.isArray(job.args) || job.args.some(value => typeof value !== 'string')) {
|
|
42
|
+
throw new Error(`Invalid provider preparation job at order ${expectedOrder}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readPlan(response, expectedPath) {
|
|
47
|
+
if (!exactKeys(response.plan, ['path', 'size', 'sha256']) || response.plan.path !== expectedPath ||
|
|
48
|
+
!Number.isSafeInteger(response.plan.size) || response.plan.size < 2 || response.plan.size > PLAN_LIMIT ||
|
|
49
|
+
!/^[a-f0-9]{64}$/.test(response.plan.sha256)) throw new Error('Invalid provider preparation plan descriptor');
|
|
50
|
+
const stat = fs.lstatSync(expectedPath);
|
|
51
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== response.plan.size) throw new Error('Invalid provider preparation plan file');
|
|
52
|
+
const body = fs.readFileSync(expectedPath);
|
|
53
|
+
if (body.length !== response.plan.size || createHash('sha256').update(body).digest('hex') !== response.plan.sha256) {
|
|
54
|
+
throw new Error('Provider preparation plan digest mismatch');
|
|
55
|
+
}
|
|
56
|
+
return JSON.parse(body.toString('utf8'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function prepareInChild({ job, attempt, sessionRoot, poolDescriptor, workerPath, workerEnv, activeChildren, timeoutMs }) {
|
|
60
|
+
const scratch = path.join(sessionRoot, `${job.order}-${attempt}-${randomUUID()}`);
|
|
61
|
+
fs.mkdirSync(scratch, { mode: 0o700 });
|
|
62
|
+
const capability = randomBytes(32).toString('hex');
|
|
63
|
+
const jobId = randomUUID();
|
|
64
|
+
const expectedPlan = path.join(scratch, 'plan.json');
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const child = fork(workerPath, [], {
|
|
67
|
+
cwd: process.cwd(),
|
|
68
|
+
env: { ...process.env, ...workerEnv },
|
|
69
|
+
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
70
|
+
serialization: 'json',
|
|
71
|
+
execArgv: CHECKOUT_NEEDS_TS_TRANSFORM ? ['--import', 'tsx'] : [],
|
|
72
|
+
});
|
|
73
|
+
activeChildren.add(child);
|
|
74
|
+
const stderr = [];
|
|
75
|
+
let stderrBytes = 0;
|
|
76
|
+
child.stderr?.on('data', chunk => {
|
|
77
|
+
const retained = chunk.subarray(0, Math.max(0, 1024 * 1024 - stderrBytes));
|
|
78
|
+
if (retained.length) { stderr.push(Buffer.from(retained)); stderrBytes += retained.length; }
|
|
79
|
+
});
|
|
80
|
+
let settled = false;
|
|
81
|
+
let outcome;
|
|
82
|
+
let killTimer;
|
|
83
|
+
let timedOut = false;
|
|
84
|
+
const finish = () => {
|
|
85
|
+
if (settled) return;
|
|
86
|
+
settled = true;
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
clearTimeout(killTimer);
|
|
89
|
+
activeChildren.delete(child);
|
|
90
|
+
if (child.connected) { try { child.disconnect(); } catch {} }
|
|
91
|
+
if (outcome?.error) reject(outcome.error); else if (outcome) resolve(outcome.value);
|
|
92
|
+
else reject(new Error(`Provider preparation worker exited ${child.exitCode ?? 'without status'} for ${job.provider}${stderrBytes ? `: ${Buffer.concat(stderr).toString('utf8').trim()}` : ''}`));
|
|
93
|
+
};
|
|
94
|
+
const stop = error => {
|
|
95
|
+
if (settled) return;
|
|
96
|
+
outcome = { error };
|
|
97
|
+
try { signalChild(child, 'SIGTERM'); } catch {}
|
|
98
|
+
if (!killTimer) {
|
|
99
|
+
killTimer = setTimeout(() => { try { signalChild(child, 'SIGKILL'); } catch {} }, 2_000);
|
|
100
|
+
killTimer.unref?.();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
timedOut = true;
|
|
105
|
+
stop(new Error(`Provider preparation timed out for ${job.provider}`));
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
timer.unref?.();
|
|
108
|
+
child.once('error', error => {
|
|
109
|
+
// A spawn failure has no operating-system child to reap. Every error
|
|
110
|
+
// after a pid is assigned still waits for the exit event below.
|
|
111
|
+
if (!child.pid) { outcome = { error }; finish(); }
|
|
112
|
+
else stop(error);
|
|
113
|
+
});
|
|
114
|
+
// Settle only after the operating system reports process exit. This keeps
|
|
115
|
+
// every worker in activeChildren until it is confirmed gone, including workers
|
|
116
|
+
// that send a response and retain an unexpected live handle.
|
|
117
|
+
child.once('exit', (code, signal) => {
|
|
118
|
+
if (outcome?.value && (code !== 0 || signal)) {
|
|
119
|
+
outcome = { error: new Error(`Provider preparation worker exited ${signal || code || 'without status'} after responding for ${job.provider}`) };
|
|
120
|
+
}
|
|
121
|
+
if (!outcome) outcome = { error: new Error(`${timedOut ? 'Provider preparation timed out' : `Provider preparation worker exited ${code ?? 'without status'}`} for ${job.provider}${stderrBytes ? `: ${Buffer.concat(stderr).toString('utf8').trim()}` : ''}`) };
|
|
122
|
+
finish();
|
|
123
|
+
});
|
|
124
|
+
child.once('message', response => {
|
|
125
|
+
try {
|
|
126
|
+
if (timedOut) throw new Error(`Provider preparation timed out for ${job.provider}`);
|
|
127
|
+
if (!exactKeys(response, response?.type === 'error'
|
|
128
|
+
? ['version', 'type', 'capability', 'jobId', 'error']
|
|
129
|
+
: ['version', 'type', 'capability', 'jobId', 'plan']) ||
|
|
130
|
+
response.version !== 1 || response.capability !== capability || response.jobId !== jobId) {
|
|
131
|
+
throw new Error('Invalid provider preparation worker response');
|
|
132
|
+
}
|
|
133
|
+
if (response.type === 'error') {
|
|
134
|
+
if (!exactKeys(response.error, ['message']) || typeof response.error.message !== 'string') throw new Error('Invalid provider preparation worker error');
|
|
135
|
+
throw new Error(`${job.provider}: ${response.error.message}`);
|
|
136
|
+
}
|
|
137
|
+
if (response.type !== 'prepared') throw new Error('Invalid provider preparation worker response type');
|
|
138
|
+
outcome = { value: { detached: readPlan(response, expectedPlan), scratch, attempt } };
|
|
139
|
+
// Closing the IPC channel lets the single-request worker exit normally;
|
|
140
|
+
// the exit handler above confirms termination before exposing its plan.
|
|
141
|
+
if (child.connected) { try { child.disconnect(); } catch {} }
|
|
142
|
+
} catch (error) { stop(error); }
|
|
143
|
+
});
|
|
144
|
+
child.send({ version: 1, type: 'prepare', capability, jobId, order: job.order, attempt, provider: job.provider,
|
|
145
|
+
target: job.target, args: job.args, scratch, inputPool: poolDescriptor }, error => { if (error) stop(error); });
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Prepare provider projections in at most two subprocesses. The returned
|
|
150
|
+
* session owns every stage and promotes them only through ordered commit(). */
|
|
151
|
+
export async function prepareProviderDeployments({
|
|
152
|
+
srcRoot,
|
|
153
|
+
jobs,
|
|
154
|
+
maxConcurrency = 2,
|
|
155
|
+
workerPath = DEFAULT_WORKER,
|
|
156
|
+
workerEnv = {},
|
|
157
|
+
timeoutMs = 120_000,
|
|
158
|
+
transaction = defaultTransaction,
|
|
159
|
+
onSessionRoot,
|
|
160
|
+
signal = signalChild,
|
|
161
|
+
}) {
|
|
162
|
+
if (typeof srcRoot !== 'string' || !path.isAbsolute(srcRoot) || !Array.isArray(jobs) || !jobs.length) throw new Error('Invalid provider preparation request');
|
|
163
|
+
jobs.forEach(validateJob);
|
|
164
|
+
const concurrency = Math.max(1, Math.min(2, Number.isSafeInteger(maxConcurrency) ? maxConcurrency : 2, jobs.length));
|
|
165
|
+
const sessionRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'aiwg-provider-prepare-'));
|
|
166
|
+
const removeSession = trackTemporaryDirectory(sessionRoot);
|
|
167
|
+
onSessionRoot?.(sessionRoot);
|
|
168
|
+
const activeChildren = new Set();
|
|
169
|
+
let pool;
|
|
170
|
+
let poolDescriptor;
|
|
171
|
+
try {
|
|
172
|
+
pool = transaction.createPool(srcRoot);
|
|
173
|
+
poolDescriptor = transaction.describePool(pool);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (pool) transaction.disposePool(pool);
|
|
176
|
+
removeSession();
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
const prepared = new Array(jobs.length);
|
|
180
|
+
let next = 0;
|
|
181
|
+
let closed = false;
|
|
182
|
+
let nextCommit = 0;
|
|
183
|
+
const stopChildren = async () => {
|
|
184
|
+
const children = [...activeChildren];
|
|
185
|
+
await Promise.all(children.map(child => new Promise(resolve => {
|
|
186
|
+
if (child.exitCode !== null || child.signalCode !== null) return resolve();
|
|
187
|
+
if (!Number.isSafeInteger(child.pid) || child.pid <= 0) return resolve();
|
|
188
|
+
const hardKill = setTimeout(() => { try { signal(child, 'SIGKILL'); } catch {} }, 2_000);
|
|
189
|
+
hardKill.unref?.();
|
|
190
|
+
child.once('exit', () => { clearTimeout(hardKill); resolve(); });
|
|
191
|
+
try { signal(child, 'SIGTERM'); } catch { /* escalation and exit confirmation remain required */ }
|
|
192
|
+
})));
|
|
193
|
+
};
|
|
194
|
+
const cleanup = async () => {
|
|
195
|
+
if (closed) return;
|
|
196
|
+
closed = true;
|
|
197
|
+
await stopChildren();
|
|
198
|
+
for (const item of prepared) if (item?.plan && !item.committed) transaction.disposePlan(item.plan);
|
|
199
|
+
transaction.disposePool(pool);
|
|
200
|
+
removeSession();
|
|
201
|
+
};
|
|
202
|
+
const prepareOne = async (index, attempt = 0) => {
|
|
203
|
+
const raw = await prepareInChild({ job: jobs[index], attempt, sessionRoot, poolDescriptor, workerPath, workerEnv, activeChildren, timeoutMs });
|
|
204
|
+
const plan = transaction.adopt(raw.detached, {
|
|
205
|
+
scratch: raw.scratch,
|
|
206
|
+
target: jobs[index].target,
|
|
207
|
+
inputPool: pool,
|
|
208
|
+
cleanup: () => fs.rmSync(raw.scratch, { recursive: true, force: true }),
|
|
209
|
+
});
|
|
210
|
+
return { ...raw, plan, committed: false };
|
|
211
|
+
};
|
|
212
|
+
try {
|
|
213
|
+
await Promise.all(Array.from({ length: concurrency }, async () => {
|
|
214
|
+
for (;;) {
|
|
215
|
+
const index = next++;
|
|
216
|
+
if (index >= jobs.length) return;
|
|
217
|
+
prepared[index] = await prepareOne(index);
|
|
218
|
+
}
|
|
219
|
+
}));
|
|
220
|
+
} catch (error) {
|
|
221
|
+
await cleanup();
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
get providers() { return jobs.map(job => job.provider); },
|
|
226
|
+
async commit(index) {
|
|
227
|
+
if (closed) throw new Error('Provider preparation session is closed');
|
|
228
|
+
if (index !== nextCommit) throw new Error(`Provider plans must commit in order: expected ${nextCommit}, received ${index}`);
|
|
229
|
+
let item = prepared[index];
|
|
230
|
+
try {
|
|
231
|
+
let report;
|
|
232
|
+
try { report = transaction.commit(item.plan); }
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (!transaction.isStale(error) || item.attempt >= 1) throw error;
|
|
235
|
+
item = prepared[index] = await prepareOne(index, item.attempt + 1);
|
|
236
|
+
report = transaction.commit(item.plan);
|
|
237
|
+
}
|
|
238
|
+
item.committed = true;
|
|
239
|
+
nextCommit++;
|
|
240
|
+
return report;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
async dispose() { await cleanup(); },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { main as deployProvider, parseArgs } from '../deploy-agents.mjs';
|
|
6
|
+
import { retainPreparationChannel } from './provider-prepare-channel.mjs';
|
|
7
|
+
import {
|
|
8
|
+
importRuleDeploymentInputPool,
|
|
9
|
+
disposeRuleDeploymentInputPool,
|
|
10
|
+
installRuleDeploymentPreparationHook,
|
|
11
|
+
} from './rule-delivery.mjs';
|
|
12
|
+
|
|
13
|
+
function exactKeys(value, keys) {
|
|
14
|
+
return value && typeof value === 'object' && !Array.isArray(value) &&
|
|
15
|
+
JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validRequest(request) {
|
|
19
|
+
return exactKeys(request, ['version', 'type', 'capability', 'jobId', 'order', 'attempt', 'provider', 'target', 'args', 'scratch', 'inputPool']) &&
|
|
20
|
+
request.version === 1 && request.type === 'prepare' && /^[a-f0-9]{64}$/.test(request.capability) &&
|
|
21
|
+
typeof request.jobId === 'string' && Number.isSafeInteger(request.order) && request.order >= 0 &&
|
|
22
|
+
(request.attempt === 0 || request.attempt === 1) && typeof request.provider === 'string' && request.provider &&
|
|
23
|
+
typeof request.target === 'string' && path.isAbsolute(request.target) && typeof request.scratch === 'string' && path.isAbsolute(request.scratch) &&
|
|
24
|
+
Array.isArray(request.args) && request.args.every(value => typeof value === 'string');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function publish(message) {
|
|
28
|
+
return new Promise(resolve => {
|
|
29
|
+
if (!process.connected) return resolve();
|
|
30
|
+
process.send(message, () => resolve());
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
process.once('message', async request => {
|
|
35
|
+
retainPreparationChannel();
|
|
36
|
+
if (!validRequest(request)) {
|
|
37
|
+
await publish({ version: 1, type: 'error', capability: request?.capability || '', jobId: request?.jobId || '', error: { message: 'Invalid provider preparation request' } });
|
|
38
|
+
try { process.disconnect(); } catch {}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let pool;
|
|
42
|
+
let clearHook = () => {};
|
|
43
|
+
let response;
|
|
44
|
+
try {
|
|
45
|
+
const stat = fs.lstatSync(request.scratch);
|
|
46
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.readdirSync(request.scratch).length) throw new Error('Invalid provider preparation scratch directory');
|
|
47
|
+
const parsed = parseArgs(request.args);
|
|
48
|
+
if (parsed.provider !== request.provider || path.resolve(parsed.target) !== path.resolve(request.target)) {
|
|
49
|
+
throw new Error('Provider preparation request does not match its deployment arguments');
|
|
50
|
+
}
|
|
51
|
+
pool = importRuleDeploymentInputPool(request.inputPool);
|
|
52
|
+
let detached;
|
|
53
|
+
clearHook = installRuleDeploymentPreparationHook({
|
|
54
|
+
scratch: request.scratch,
|
|
55
|
+
inputPool: pool,
|
|
56
|
+
accept: plan => {
|
|
57
|
+
if (detached) throw new Error('Provider attempted more than one transactional preparation');
|
|
58
|
+
detached = plan;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
await deployProvider(request.args);
|
|
62
|
+
if (!detached) throw new Error('Provider did not produce a transactional preparation plan');
|
|
63
|
+
if (detached.target !== path.resolve(request.target)) throw new Error('Provider preparation target mismatch');
|
|
64
|
+
const body = Buffer.from(JSON.stringify(detached));
|
|
65
|
+
const planPath = path.join(request.scratch, 'plan.json');
|
|
66
|
+
const fd = fs.openSync(planPath, 'wx', 0o600);
|
|
67
|
+
try { fs.writeFileSync(fd, body); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
68
|
+
response = {
|
|
69
|
+
version: 1,
|
|
70
|
+
type: 'prepared',
|
|
71
|
+
capability: request.capability,
|
|
72
|
+
jobId: request.jobId,
|
|
73
|
+
plan: { path: planPath, size: body.length, sha256: createHash('sha256').update(body).digest('hex') },
|
|
74
|
+
};
|
|
75
|
+
} catch (error) {
|
|
76
|
+
response = { version: 1, type: 'error', capability: request.capability, jobId: request.jobId,
|
|
77
|
+
error: { message: error instanceof Error ? error.message : String(error) } };
|
|
78
|
+
} finally {
|
|
79
|
+
clearHook();
|
|
80
|
+
if (pool) disposeRuleDeploymentInputPool(pool);
|
|
81
|
+
}
|
|
82
|
+
await publish(response);
|
|
83
|
+
try { process.disconnect(); } catch {}
|
|
84
|
+
});
|