@aiwg/cli 2026.8.12 → 2026.8.14
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/dist/src/artifacts/output-policy.js +87 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/output-mode.js +69 -0
- package/dist/src/cli/handlers/run.js +42 -4
- package/dist/src/cli/handlers/use.js +7 -6
- package/dist/src/cli/scope-resolver.js +6 -1
- package/dist/src/config/aiwg-config.js +8 -0
- package/dist/src/config/cli.js +2 -0
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/extensions/project-quickref.js +9 -0
- package/dist/src/output-modes/registry.js +158 -0
- package/dist/src/output-modes/runtime.js +64 -0
- package/dist/src/output-modes/types.js +2 -0
- package/dist/src/providers/hermes-home.js +20 -0
- package/dist/src/providers/provider-definitions.js +5 -4
- package/dist/src/skills/deployer.js +5 -1
- package/dist/src/skills/run.js +3 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +27 -7
- package/package.json +1 -1
- package/tools/agents/deploy-agents.mjs +8 -2
- package/tools/agents/providers/base.mjs +29 -2
- package/tools/agents/providers/hermes.mjs +163 -19
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
export function defaultArtifactOutputs() {
|
|
4
|
+
return { canonical: 'aiwg', provider_native: 'explicit-only', destinations: {} };
|
|
5
|
+
}
|
|
6
|
+
export function validateArtifactOutputs(value) {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return [];
|
|
9
|
+
const errors = [];
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
|
+
return ['artifact_outputs must be an object'];
|
|
12
|
+
if (value.canonical !== undefined && value.canonical !== 'aiwg')
|
|
13
|
+
errors.push("artifact_outputs.canonical must be 'aiwg'");
|
|
14
|
+
if (value.provider_native !== undefined && !['disabled', 'explicit-only', 'project-default'].includes(value.provider_native))
|
|
15
|
+
errors.push('artifact_outputs.provider_native must be disabled, explicit-only, or project-default');
|
|
16
|
+
for (const [id, destination] of Object.entries(value.destinations ?? {})) {
|
|
17
|
+
if (!/^[a-z0-9][a-z0-9.-]*$/.test(id))
|
|
18
|
+
errors.push(`artifact_outputs destination '${id}' has an invalid stable ID`);
|
|
19
|
+
if (!destination || typeof destination !== 'object' || Array.isArray(destination)) {
|
|
20
|
+
errors.push(`artifact_outputs.destinations.${id} must be an object`);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (destination.enabled !== undefined && typeof destination.enabled !== 'boolean')
|
|
24
|
+
errors.push(`artifact_outputs.destinations.${id}.enabled must be boolean`);
|
|
25
|
+
if (destination.use_when !== undefined && !['disabled', 'user-requested', 'project-default'].includes(destination.use_when))
|
|
26
|
+
errors.push(`artifact_outputs.destinations.${id}.use_when is invalid`);
|
|
27
|
+
}
|
|
28
|
+
return errors;
|
|
29
|
+
}
|
|
30
|
+
export function resolveArtifactOutputs(options) {
|
|
31
|
+
const project = { ...defaultArtifactOutputs(), ...(options.project ?? {}) };
|
|
32
|
+
const supported = new Set(options.supportedDestinations ?? []);
|
|
33
|
+
const presentations = [];
|
|
34
|
+
const authority = {};
|
|
35
|
+
const diagnostics = [];
|
|
36
|
+
const explicit = new Set(options.explicitDestinations ?? []);
|
|
37
|
+
const candidateAuthority = new Map();
|
|
38
|
+
for (const id of options.providerDefaults ?? [])
|
|
39
|
+
candidateAuthority.set(id, 'provider-default');
|
|
40
|
+
if (project.provider_native === 'project-default') {
|
|
41
|
+
for (const [id, destination] of Object.entries(project.destinations ?? {})) {
|
|
42
|
+
if (destination.use_when === 'project-default')
|
|
43
|
+
candidateAuthority.set(id, 'project-default');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (options.userPreference?.provider_native === 'project-default') {
|
|
47
|
+
for (const [id, destination] of Object.entries(options.userPreference.destinations ?? {})) {
|
|
48
|
+
if (destination.enabled !== false && destination.use_when === 'project-default')
|
|
49
|
+
candidateAuthority.set(id, 'user-preference');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const id of explicit)
|
|
53
|
+
candidateAuthority.set(id, 'explicit-task');
|
|
54
|
+
const candidates = candidateAuthority.keys();
|
|
55
|
+
for (const id of candidates) {
|
|
56
|
+
if (!supported.has(id)) {
|
|
57
|
+
diagnostics.push(`Destination '${id}' is unknown or unsupported and was not selected.`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const policy = project.destinations?.[id];
|
|
61
|
+
if (project.provider_native === 'disabled' || policy?.enabled === false || policy?.use_when === 'disabled') {
|
|
62
|
+
diagnostics.push(`Destination '${id}' is disabled by project policy.`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const selectedBy = candidateAuthority.get(id);
|
|
66
|
+
if (selectedBy === 'explicit-task') {
|
|
67
|
+
presentations.push(id);
|
|
68
|
+
authority[id] = 'explicit-task';
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (project.provider_native === 'project-default' && policy?.use_when === 'project-default') {
|
|
72
|
+
presentations.push(id);
|
|
73
|
+
authority[id] = selectedBy;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
diagnostics.push(`Destination '${id}' requires an explicit per-task request; provider and user defaults cannot select it.`);
|
|
77
|
+
}
|
|
78
|
+
return { canonical: 'aiwg', presentations: [...new Set(presentations)].sort(), authority, diagnostics };
|
|
79
|
+
}
|
|
80
|
+
export async function recordArtifactOutputProvenance(artifactRoot, record) {
|
|
81
|
+
const path = join(artifactRoot, 'provenance', 'artifact-outputs.jsonl');
|
|
82
|
+
await mkdir(dirname(path), { recursive: true });
|
|
83
|
+
const value = { schemaVersion: 'aiwg.artifact-output-provenance.v1', createdAt: new Date().toISOString(), ...record };
|
|
84
|
+
await appendFile(path, `${JSON.stringify(value)}\n`, 'utf8');
|
|
85
|
+
return path;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=output-policy.js.map
|
|
@@ -62,12 +62,13 @@ import { jobHandler } from './job.js';
|
|
|
62
62
|
import { costReportHandler } from './cost-report.js';
|
|
63
63
|
import { evidenceHandler } from './evidence.js';
|
|
64
64
|
import { artifactVerifyHandler } from './artifact-verify.js';
|
|
65
|
+
import { outputModeHandler } from './output-mode.js';
|
|
65
66
|
// Re-export individual handlers
|
|
66
67
|
export {
|
|
67
68
|
// Maintenance
|
|
68
69
|
helpHandler, versionHandler, authHandler, doctorHandler, contextFirewallHandler, updateHandler, refreshHandler, regenerateHandler, workspaceContextHandler,
|
|
69
70
|
// Framework management
|
|
70
|
-
useHandler, listHandler, removeHandler, promoteHandler, installHandler, packagesHandler, marketplaceHandler, initHandler, setupHandler, setupGenerateHandler, setupRunHandler, setupValidateHandler, issueHandler, issueAuditHandler, runHandler, jobHandler, costReportHandler, evidenceHandler, artifactVerifyHandler,
|
|
71
|
+
useHandler, listHandler, removeHandler, promoteHandler, installHandler, packagesHandler, marketplaceHandler, initHandler, setupHandler, setupGenerateHandler, setupRunHandler, setupValidateHandler, issueHandler, issueAuditHandler, runHandler, jobHandler, costReportHandler, evidenceHandler, artifactVerifyHandler, outputModeHandler,
|
|
71
72
|
// Project
|
|
72
73
|
newBundleHandler, quickrefHandler, newProjectHandler, sessionHandler, sessionsHandler,
|
|
73
74
|
// Workspace
|
|
@@ -150,6 +151,7 @@ export const allHandlers = [
|
|
|
150
151
|
costReportHandler,
|
|
151
152
|
evidenceHandler,
|
|
152
153
|
artifactVerifyHandler,
|
|
154
|
+
outputModeHandler,
|
|
153
155
|
// Workspace management
|
|
154
156
|
...workspaceHandlers,
|
|
155
157
|
// Subcommand handlers (MCP, catalog, index, skills)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { loadOutputModeRegistry, readOutputModeState, resolveOutputModes, writeOutputModeState } from '../../output-modes/registry.js';
|
|
2
|
+
function flagValue(args, name) {
|
|
3
|
+
const index = args.indexOf(name);
|
|
4
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
5
|
+
}
|
|
6
|
+
function parseScope(args) {
|
|
7
|
+
const scope = flagValue(args, '--scope') ?? 'session';
|
|
8
|
+
if (!['invocation', 'session', 'project'].includes(scope))
|
|
9
|
+
throw new Error(`Invalid scope '${scope}'; expected invocation, session, or project.`);
|
|
10
|
+
return scope;
|
|
11
|
+
}
|
|
12
|
+
async function execute(ctx) {
|
|
13
|
+
const [action = 'status', id] = ctx.args.filter((arg, index, all) => !arg.startsWith('-') && all[index - 1] !== '--scope');
|
|
14
|
+
const registry = await loadOutputModeRegistry(ctx.cwd, ctx.frameworkRoot);
|
|
15
|
+
if (action === 'list') {
|
|
16
|
+
const rows = [...registry.values()].sort((a, b) => a.id.localeCompare(b.id)).map(p => `${p.id}\t${p.kind}\t${p.validation.level}\t${p.source}\t${p.description}`);
|
|
17
|
+
return { exitCode: 0, rawOutput: true, message: `ID\tKIND\tVALIDATION\tSOURCE\tDESCRIPTION\n${rows.join('\n')}` };
|
|
18
|
+
}
|
|
19
|
+
if (action === 'show') {
|
|
20
|
+
if (!id)
|
|
21
|
+
return { exitCode: 1, message: 'Usage: aiwg output-mode show <id>' };
|
|
22
|
+
const p = registry.get(id);
|
|
23
|
+
if (!p)
|
|
24
|
+
return { exitCode: 1, message: `Unknown output mode '${id}'.` };
|
|
25
|
+
return { exitCode: 0, rawOutput: true, message: JSON.stringify(p, null, 2) };
|
|
26
|
+
}
|
|
27
|
+
if (action === 'status') {
|
|
28
|
+
const invocation = ctx.args.flatMap((arg, i) => arg === '--output-mode' && ctx.args[i + 1] ? [ctx.args[i + 1]] : []);
|
|
29
|
+
const resolved = await resolveOutputModes(ctx.cwd, ctx.frameworkRoot, invocation);
|
|
30
|
+
if (resolved.modes.length === 0)
|
|
31
|
+
return { exitCode: 0, rawOutput: true, message: 'Effective output mode: unaltered\nContext cost: 0 tokens\nNo transformations active.' };
|
|
32
|
+
const lines = resolved.modes.map((p, i) => `${i + 1}. ${p.id} [${p.kind}/${p.stage}] source=${p.source} scope=${p.scope} validation=${p.validation.level} context≈${p.contextCost ?? 0}`);
|
|
33
|
+
return { exitCode: 0, rawOutput: true, message: `Effective ordered stack:\n${lines.join('\n')}\nEstimated context cost: ${resolved.modes.reduce((n, p) => n + (p.contextCost ?? 0), 0)} tokens` };
|
|
34
|
+
}
|
|
35
|
+
if (!['enable', 'disable', 'clear'].includes(action))
|
|
36
|
+
return { exitCode: 1, message: `Unknown output-mode action '${action}'.` };
|
|
37
|
+
const scope = parseScope(ctx.args);
|
|
38
|
+
if (scope === 'invocation') {
|
|
39
|
+
if (action !== 'enable' || !id)
|
|
40
|
+
return { exitCode: 1, message: 'Invocation scope is ephemeral; pass --output-mode <id> to the command being run.' };
|
|
41
|
+
if (!registry.has(id))
|
|
42
|
+
return { exitCode: 1, message: `Unknown output mode '${id}'.` };
|
|
43
|
+
return { exitCode: 0, message: `Invocation mode '${id}' validated. Pass --output-mode ${id} to the command being run; no files were modified.` };
|
|
44
|
+
}
|
|
45
|
+
const state = await readOutputModeState(ctx.cwd, scope);
|
|
46
|
+
let modes = [...state.modes];
|
|
47
|
+
if (action === 'clear')
|
|
48
|
+
modes = [];
|
|
49
|
+
else {
|
|
50
|
+
if (!id)
|
|
51
|
+
return { exitCode: 1, message: `Usage: aiwg output-mode ${action} <id> --scope ${scope}` };
|
|
52
|
+
if (!registry.has(id))
|
|
53
|
+
return { exitCode: 1, message: `Unknown output mode '${id}'.` };
|
|
54
|
+
modes = action === 'enable' ? [...new Set([...modes, id])] : modes.filter(value => value !== id);
|
|
55
|
+
}
|
|
56
|
+
await resolveOutputModes(ctx.cwd, ctx.frameworkRoot, scope === 'session' ? modes : []);
|
|
57
|
+
const path = await writeOutputModeState(ctx.cwd, scope, modes);
|
|
58
|
+
return { exitCode: 0, message: `${action === 'clear' ? 'Cleared' : `${action}d`} ${scope} output modes (${modes.join(', ') || 'unaltered'}) at ${path}` };
|
|
59
|
+
}
|
|
60
|
+
export const outputModeHandler = {
|
|
61
|
+
id: 'output-mode', name: 'Output Modes', description: 'List, inspect, and select composable output modes', category: 'project', aliases: ['output-modes'],
|
|
62
|
+
async execute(ctx) { try {
|
|
63
|
+
return await execute(ctx);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return { exitCode: 1, message: error.message };
|
|
67
|
+
} },
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=output-mode.js.map
|
|
@@ -16,6 +16,23 @@ import { spawn } from 'child_process';
|
|
|
16
16
|
import { readAiwgConfig, getProjectDir } from '../../config/aiwg-config.js';
|
|
17
17
|
import { handlerResultFromError } from '../errors.js';
|
|
18
18
|
import * as ui from '../ui.js';
|
|
19
|
+
import { resolveOutputModes } from '../../output-modes/registry.js';
|
|
20
|
+
function extractOutputModes(args) {
|
|
21
|
+
const cleaned = [];
|
|
22
|
+
const modes = [];
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
if (args[i] === '--output-mode') {
|
|
25
|
+
const id = args[i + 1];
|
|
26
|
+
if (!id || id.startsWith('-'))
|
|
27
|
+
throw new Error('--output-mode requires a mode ID');
|
|
28
|
+
modes.push(id);
|
|
29
|
+
i += 1;
|
|
30
|
+
}
|
|
31
|
+
else
|
|
32
|
+
cleaned.push(args[i]);
|
|
33
|
+
}
|
|
34
|
+
return { args: cleaned, modes };
|
|
35
|
+
}
|
|
19
36
|
/**
|
|
20
37
|
* Execute a shell command with inherited stdio.
|
|
21
38
|
* Returns the exit code.
|
|
@@ -41,8 +58,28 @@ export const runHandler = {
|
|
|
41
58
|
category: 'utility',
|
|
42
59
|
aliases: [],
|
|
43
60
|
async execute(ctx) {
|
|
44
|
-
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = extractOutputModes(ctx.args);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return { exitCode: 1, message: error.message };
|
|
67
|
+
}
|
|
68
|
+
const scriptName = parsed.args[0];
|
|
45
69
|
const projectDir = getProjectDir(ctx, ctx.args);
|
|
70
|
+
let outputModeEnv = {};
|
|
71
|
+
try {
|
|
72
|
+
const resolved = await resolveOutputModes(projectDir, ctx.frameworkRoot, parsed.modes);
|
|
73
|
+
if (resolved.modes.length > 0) {
|
|
74
|
+
outputModeEnv = {
|
|
75
|
+
AIWG_OUTPUT_MODES: resolved.modes.map(mode => mode.id).join(','),
|
|
76
|
+
AIWG_OUTPUT_MODES_JSON: JSON.stringify(resolved.modes),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return { exitCode: 1, message: `Output mode resolution failed: ${error.message}` };
|
|
82
|
+
}
|
|
46
83
|
// #1231 — intercept --help/-h before script lookup so the user sees
|
|
47
84
|
// help for both run forms, not "No script named '--help'".
|
|
48
85
|
if (scriptName === '--help' || scriptName === '-h') {
|
|
@@ -55,7 +92,7 @@ export const runHandler = {
|
|
|
55
92
|
if (scriptName === 'skill') {
|
|
56
93
|
try {
|
|
57
94
|
const { main } = await import('../../skills/run.js');
|
|
58
|
-
const exitCode = await main(
|
|
95
|
+
const exitCode = await main(parsed.args, outputModeEnv);
|
|
59
96
|
return { exitCode };
|
|
60
97
|
}
|
|
61
98
|
catch (error) {
|
|
@@ -119,6 +156,7 @@ export const runHandler = {
|
|
|
119
156
|
const env = {
|
|
120
157
|
AIWG_PROJECT: projectDir,
|
|
121
158
|
AIWG_PROVIDERS: config.providers.join(','),
|
|
159
|
+
...outputModeEnv,
|
|
122
160
|
};
|
|
123
161
|
ui.blank();
|
|
124
162
|
console.log(` ${ui.brandMark()} ${ui.bold(`aiwg run ${scriptName}`)}`);
|
|
@@ -140,8 +178,8 @@ export const runHandler = {
|
|
|
140
178
|
},
|
|
141
179
|
};
|
|
142
180
|
function printRunUsage() {
|
|
143
|
-
console.log('Usage: aiwg run <script-name> [args...]');
|
|
144
|
-
console.log(' aiwg run skill <skill-name> [--cwd <path>] [-- <args forwarded to script>]');
|
|
181
|
+
console.log('Usage: aiwg run <script-name> [--output-mode <id>] [args...]');
|
|
182
|
+
console.log(' aiwg run skill <skill-name> [--output-mode <id>] [--cwd <path>] [-- <args forwarded to script>]');
|
|
145
183
|
console.log('');
|
|
146
184
|
console.log('Two forms share the `run` namespace:');
|
|
147
185
|
console.log('');
|
|
@@ -674,7 +674,7 @@ const SESSION_RELOAD_NOTICE = {
|
|
|
674
674
|
rationale: 'OpenCode loads agent files on session start and does not hot-reload.',
|
|
675
675
|
},
|
|
676
676
|
hermes: {
|
|
677
|
-
action: 'In an active Hermes session, run /reload-skills to pick up new skills in
|
|
677
|
+
action: 'In an active Hermes session, run /reload-skills to pick up new skills in $HERMES_HOME/skills/ and /reload-mcp to pick up MCP server changes ($HERMES_HOME/config.yaml) — both are in-session slash commands, no chat restart needed. Restart the chat only as a fallback if the slash commands are unavailable.',
|
|
678
678
|
rationale: 'Hermes loads skills and MCP config at session start (verified in hermes_cli/commands.py:178 and hermes_cli/config.py:1228). The /reload-skills and /reload-mcp slash commands re-scan in place; /reload-mcp prompts for confirmation by default.',
|
|
679
679
|
symptom: 'Until reloaded, newly deployed kernel skills are missing from `hermes skills list` and unreachable via natural-language invocation; new MCP servers (incl. AIWG) are missing from the tool surface.',
|
|
680
680
|
},
|
|
@@ -3249,11 +3249,12 @@ export class UseHandler {
|
|
|
3249
3249
|
// Collect deployment counts for registry persistence and the final
|
|
3250
3250
|
// orchestrated report. Presentation happens once, after verification, so
|
|
3251
3251
|
// users do not see a second competing summary.
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
}
|
|
3252
|
+
//
|
|
3253
|
+
// Counts are always populated from the on-disk artifacts so that the
|
|
3254
|
+
// registry record written below (#621) reflects the real deploy even on
|
|
3255
|
+
// a verbose run — the prior `if (quiet)` guard left the record
|
|
3256
|
+
// `{agents: 0, commands: 0, skills: 0, rules: 0}` on `-v` runs.
|
|
3257
|
+
const counts = await countDeployedArtifacts(target, paths, provider);
|
|
3257
3258
|
// Deploy CI workflow files when --ci-hooks-enabled is set (#661)
|
|
3258
3259
|
if (ciHooksEnabled) {
|
|
3259
3260
|
await deployCiHooks({ frameworkRoot, framework, target, dryRun });
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
|
+
import { resolveHermesHome, resolveHermesHomePath } from '../providers/hermes-home.js';
|
|
15
|
+
export const hermesHome = resolveHermesHome;
|
|
14
16
|
/**
|
|
15
17
|
* User-scope deploy paths per provider per ADR-4 §2. Each path is absolute
|
|
16
18
|
* (rooted in os.homedir()) so the orchestrator's existing path-join logic
|
|
@@ -159,7 +161,10 @@ export const USER_SCOPE_PATHS = {
|
|
|
159
161
|
},
|
|
160
162
|
hermes: {
|
|
161
163
|
agents: '',
|
|
162
|
-
|
|
164
|
+
// #2119: honor HERMES_HOME so `--scope user` deploys land under the same
|
|
165
|
+
// root the running Hermes session scans, matching the hermes provider's
|
|
166
|
+
// paths.skills resolution.
|
|
167
|
+
skills: resolveHermesHomePath('skills'),
|
|
163
168
|
commands: '',
|
|
164
169
|
rules: '',
|
|
165
170
|
behaviors: '',
|
|
@@ -16,6 +16,7 @@ import { getProviderDefinition, getProviderKernelSkillPath, PROVIDER_IDS, resolv
|
|
|
16
16
|
import { validateAuthorization, } from '../policy/authorization.js';
|
|
17
17
|
import { projectAiwgPath, projectControlPath, resolveProjectAiwgDir, } from './project-artifacts.js';
|
|
18
18
|
import { defaultThreatAssessmentConfig, validateThreatAssessmentConfig, } from '../security/threat-assessment-config.js';
|
|
19
|
+
import { defaultArtifactOutputs, validateArtifactOutputs } from '../artifacts/output-policy.js';
|
|
19
20
|
const CONFIG_FILENAME = 'aiwg.config';
|
|
20
21
|
/**
|
|
21
22
|
* Operations that a workspace may authorize for one member repository.
|
|
@@ -638,6 +639,7 @@ export function emptyConfig(providers = ['claude']) {
|
|
|
638
639
|
security: {
|
|
639
640
|
threatAssessment: defaultThreatAssessmentConfig(),
|
|
640
641
|
},
|
|
642
|
+
artifact_outputs: defaultArtifactOutputs(),
|
|
641
643
|
delivery: {
|
|
642
644
|
mode: 'pr-required',
|
|
643
645
|
default_branch: 'main',
|
|
@@ -730,6 +732,9 @@ export async function readAiwgConfig(projectDir) {
|
|
|
730
732
|
if (threatAssessmentErrors.length > 0) {
|
|
731
733
|
throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
|
|
732
734
|
}
|
|
735
|
+
const artifactOutputErrors = validateArtifactOutputs(parsed.artifact_outputs);
|
|
736
|
+
if (artifactOutputErrors.length > 0)
|
|
737
|
+
throw new Error(`Invalid .aiwg/aiwg.config:\n${artifactOutputErrors.join('\n')}`);
|
|
733
738
|
return parsed;
|
|
734
739
|
}
|
|
735
740
|
/**
|
|
@@ -742,6 +747,9 @@ export async function writeAiwgConfig(projectDir, config) {
|
|
|
742
747
|
if (threatAssessmentErrors.length > 0) {
|
|
743
748
|
throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
|
|
744
749
|
}
|
|
750
|
+
const artifactOutputErrors = validateArtifactOutputs(config.artifact_outputs);
|
|
751
|
+
if (artifactOutputErrors.length > 0)
|
|
752
|
+
throw new Error(`Invalid .aiwg/aiwg.config:\n${artifactOutputErrors.join('\n')}`);
|
|
745
753
|
const localPath = getConfigPath(projectDir);
|
|
746
754
|
const artifactDir = resolveProjectAiwgDir(projectDir);
|
|
747
755
|
const artifactPath = join(artifactDir, CONFIG_FILENAME);
|
package/dist/src/config/cli.js
CHANGED
|
@@ -155,6 +155,8 @@ const ENUM_RULES = {
|
|
|
155
155
|
'remotes.transport.protocol': ['ssh', 'https'],
|
|
156
156
|
'repo_maintainer.tiers.local': ['collaborator', 'maintainer', 'admin'],
|
|
157
157
|
'security.threatAssessment.mode': ['off', 'audit', 'enforce'],
|
|
158
|
+
'artifact_outputs.canonical': ['aiwg'],
|
|
159
|
+
'artifact_outputs.provider_native': ['disabled', 'explicit-only', 'project-default'],
|
|
158
160
|
};
|
|
159
161
|
const BOOLEAN_FIELDS = new Set([
|
|
160
162
|
'delivery.delete_branch_on_merge',
|
|
@@ -1061,6 +1061,24 @@ export const sessionCommand = {
|
|
|
1061
1061
|
},
|
|
1062
1062
|
},
|
|
1063
1063
|
};
|
|
1064
|
+
export const outputModeCommand = {
|
|
1065
|
+
id: 'output-mode',
|
|
1066
|
+
type: 'command',
|
|
1067
|
+
name: 'Output Modes',
|
|
1068
|
+
description: 'List, inspect, enable, disable, clear, and report composable output modes',
|
|
1069
|
+
version: '1.0.0',
|
|
1070
|
+
capabilities: ['cli', 'voice', 'output-mode', 'controlled-language', 'presentation'],
|
|
1071
|
+
keywords: ['output-mode', 'voice', 'style', 'asd-ste', 'presentation'],
|
|
1072
|
+
category: 'project',
|
|
1073
|
+
platforms: { claude: 'full', generic: 'full' },
|
|
1074
|
+
deployment: { pathTemplate: '.{platform}/commands/{id}.md', core: true },
|
|
1075
|
+
metadata: {
|
|
1076
|
+
type: 'command',
|
|
1077
|
+
template: 'utility',
|
|
1078
|
+
argumentHint: '<list|show|enable|disable|clear|status> [id] [--scope invocation|session|project]',
|
|
1079
|
+
allowedTools: ['Read', 'Write'],
|
|
1080
|
+
},
|
|
1081
|
+
};
|
|
1064
1082
|
// Session Catalog Command (#1903)
|
|
1065
1083
|
export const sessionsCommand = {
|
|
1066
1084
|
id: 'sessions',
|
|
@@ -3701,6 +3719,7 @@ export const commandDefinitions = [
|
|
|
3701
3719
|
// Session (#884)
|
|
3702
3720
|
sessionCommand,
|
|
3703
3721
|
sessionsCommand,
|
|
3722
|
+
outputModeCommand,
|
|
3704
3723
|
];
|
|
3705
3724
|
// ============================================
|
|
3706
3725
|
// Helper Functions
|
|
@@ -11,6 +11,7 @@ import { basename, dirname, isAbsolute, join, resolve } from 'path';
|
|
|
11
11
|
import { homedir } from 'os';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { getProviderDefinition, normalizeProviderDefinitionId, } from '../providers/provider-definitions.js';
|
|
14
|
+
import { resolveHermesHome } from '../providers/hermes-home.js';
|
|
14
15
|
import { OPERATIONAL_SHOW_TYPES } from '../artifacts/types.js';
|
|
15
16
|
import { projectAiwgPath } from '../config/project-artifacts.js';
|
|
16
17
|
import { appendAiwgSourceTrackBlock } from './project-local-gitignore.js';
|
|
@@ -339,6 +340,14 @@ function resolveProviderSkillsRoot(provider, projectDir, homeDir) {
|
|
|
339
340
|
const definition = getProviderDefinition(normalized);
|
|
340
341
|
if (!definition)
|
|
341
342
|
throw new Error(`Provider definition unavailable for '${provider}'`);
|
|
343
|
+
if (normalized === 'hermes') {
|
|
344
|
+
return {
|
|
345
|
+
provider: normalized,
|
|
346
|
+
root: resolve(resolveHermesHome(homeDir), 'skills'),
|
|
347
|
+
emulated: false,
|
|
348
|
+
global: true,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
342
351
|
const configured = definition.paths.kernelSkills ?? definition.paths.artifacts.skills;
|
|
343
352
|
if (!configured)
|
|
344
353
|
throw new Error(`Provider '${normalized}' has no supported skill or aggregation target`);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { dirname, extname, join, resolve } from 'node:path';
|
|
6
|
+
import { parse, stringify } from 'yaml';
|
|
7
|
+
const PROTECTED = ['code', 'commands', 'citations', 'quoted-text', 'identifiers', 'machine-readable-blocks'];
|
|
8
|
+
const STAGE_ORDER = ['semantic', 'voice', 'controlled-language', 'structure', 'presentation'];
|
|
9
|
+
const BUILTINS = [
|
|
10
|
+
{
|
|
11
|
+
id: 'unaltered', version: '1.0.0', description: 'No-op mode; preserves the provider output path unchanged.',
|
|
12
|
+
kind: 'presentation', stage: 'presentation', order: -1000, instructions: '',
|
|
13
|
+
provenance: { source: 'AIWG', license: 'MIT' }, validation: { level: 'advisory' }, contextCost: 0,
|
|
14
|
+
protectedContent: PROTECTED,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'wittgenstein-inspired', version: '1.0.0', description: 'Concise, proposition-oriented stylistic profile; not impersonation or attribution.',
|
|
18
|
+
kind: 'voice', stage: 'voice', order: 100, instructions: 'Prefer concise propositions, clarify terms in use, and expose category errors. Do not imitate or attribute text to Ludwig Wittgenstein.',
|
|
19
|
+
provenance: { source: 'AIWG original style guidance', license: 'MIT' }, validation: { level: 'advisory' }, contextCost: 48,
|
|
20
|
+
protectedContent: PROTECTED,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'asd-ste', version: '1.0.0', description: 'Operator-configured ASD Simplified Technical English adapter.',
|
|
24
|
+
kind: 'controlled-language', stage: 'controlled-language', order: 200,
|
|
25
|
+
instructions: 'Apply only operator-supplied ASD-STE rules and approved terminology. Without licensed rules and a configured validator, describe output as advisory and never claim conformance.',
|
|
26
|
+
provenance: { source: 'AIWG adapter; standard content supplied by operator', license: 'MIT adapter only' },
|
|
27
|
+
validation: { level: 'advisory', standardVersion: 'operator-configured' }, contextCost: 64,
|
|
28
|
+
protectedContent: PROTECTED,
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
function profileDirs(cwd) {
|
|
32
|
+
return [
|
|
33
|
+
{ dir: join(cwd, '.aiwg', 'output-modes'), source: 'project' },
|
|
34
|
+
{ dir: join(homedir(), '.config', 'aiwg', 'output-modes'), source: 'user' },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
async function readable(path) {
|
|
38
|
+
try {
|
|
39
|
+
await access(path, constants.R_OK);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function validateProfile(value, path) {
|
|
47
|
+
if (!value || typeof value !== 'object')
|
|
48
|
+
throw new Error(`Invalid output mode profile at ${path}: expected an object`);
|
|
49
|
+
const p = value;
|
|
50
|
+
for (const field of ['id', 'version', 'description', 'kind', 'stage', 'instructions', 'provenance', 'validation']) {
|
|
51
|
+
if (p[field] === undefined)
|
|
52
|
+
throw new Error(`Invalid output mode profile at ${path}: missing ${field}`);
|
|
53
|
+
}
|
|
54
|
+
if (!['voice', 'controlled-language', 'structure', 'presentation'].includes(String(p.kind)))
|
|
55
|
+
throw new Error(`Invalid output mode kind in ${path}: ${p.kind}`);
|
|
56
|
+
if (!STAGE_ORDER.includes(String(p.stage)))
|
|
57
|
+
throw new Error(`Invalid output mode stage in ${path}: ${p.stage}`);
|
|
58
|
+
return p;
|
|
59
|
+
}
|
|
60
|
+
async function loadDirectory(dir, source) {
|
|
61
|
+
if (!(await readable(dir)))
|
|
62
|
+
return [];
|
|
63
|
+
const result = [];
|
|
64
|
+
for (const name of (await readdir(dir)).sort()) {
|
|
65
|
+
if (!['.yaml', '.yml', '.json'].includes(extname(name)))
|
|
66
|
+
continue;
|
|
67
|
+
const sourcePath = join(dir, name);
|
|
68
|
+
const raw = await readFile(sourcePath, 'utf8');
|
|
69
|
+
const value = extname(name) === '.json' ? JSON.parse(raw) : parse(raw);
|
|
70
|
+
result.push({ ...validateProfile(value, sourcePath), source, sourcePath });
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
async function loadVoiceAdapters(frameworkRoot) {
|
|
75
|
+
const dir = join(frameworkRoot, 'agentic', 'code', 'addons', 'voice-framework', 'voices', 'templates');
|
|
76
|
+
if (!(await readable(dir)))
|
|
77
|
+
return [];
|
|
78
|
+
const result = [];
|
|
79
|
+
for (const name of (await readdir(dir)).filter(n => /\.ya?ml$/.test(n)).sort()) {
|
|
80
|
+
const sourcePath = join(dir, name);
|
|
81
|
+
const voice = parse(await readFile(sourcePath, 'utf8'));
|
|
82
|
+
const id = String(voice.id ?? name.replace(/\.ya?ml$/, ''));
|
|
83
|
+
result.push({
|
|
84
|
+
id, version: String(voice.version ?? '1.0.0'), description: String(voice.description ?? `Adapted voice profile: ${id}`),
|
|
85
|
+
kind: 'voice', stage: 'voice', order: 100, instructions: `Apply the existing voice profile '${id}' through voice-apply.`,
|
|
86
|
+
provenance: { source: sourcePath, license: String(voice.license ?? 'project license') },
|
|
87
|
+
validation: { level: 'advisory' }, protectedContent: PROTECTED, contextCost: 32,
|
|
88
|
+
mergeStrategy: 'weighted-voice', source: 'voice-adapter', sourcePath,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
export async function loadOutputModeRegistry(cwd, frameworkRoot) {
|
|
94
|
+
const registry = new Map();
|
|
95
|
+
for (const profile of BUILTINS)
|
|
96
|
+
registry.set(profile.id, { ...profile, source: 'builtin' });
|
|
97
|
+
for (const profile of await loadVoiceAdapters(frameworkRoot))
|
|
98
|
+
if (!registry.has(profile.id))
|
|
99
|
+
registry.set(profile.id, profile);
|
|
100
|
+
// User overrides built-ins; project overrides user.
|
|
101
|
+
for (const entry of [...profileDirs(cwd)].reverse())
|
|
102
|
+
for (const profile of await loadDirectory(entry.dir, entry.source))
|
|
103
|
+
registry.set(profile.id, profile);
|
|
104
|
+
return registry;
|
|
105
|
+
}
|
|
106
|
+
function statePath(cwd, scope) {
|
|
107
|
+
if (scope === 'project')
|
|
108
|
+
return join(cwd, '.aiwg', 'output-modes.yaml');
|
|
109
|
+
const workspace = createHash('sha256').update(resolve(cwd)).digest('hex').slice(0, 16);
|
|
110
|
+
const session = process.env.AIWG_SESSION_ID?.replace(/[^a-zA-Z0-9_.-]/g, '_') || 'default';
|
|
111
|
+
return join(tmpdir(), 'aiwg-output-modes', `${workspace}-${session}.yaml`);
|
|
112
|
+
}
|
|
113
|
+
export async function readOutputModeState(cwd, scope) {
|
|
114
|
+
const path = statePath(cwd, scope);
|
|
115
|
+
if (!(await readable(path)))
|
|
116
|
+
return { version: 1, modes: [] };
|
|
117
|
+
const value = parse(await readFile(path, 'utf8'));
|
|
118
|
+
return { version: 1, modes: Array.isArray(value.modes) ? value.modes.map(String) : [] };
|
|
119
|
+
}
|
|
120
|
+
export async function writeOutputModeState(cwd, scope, modes) {
|
|
121
|
+
const path = statePath(cwd, scope);
|
|
122
|
+
await mkdir(dirname(path), { recursive: true });
|
|
123
|
+
await writeFile(path, stringify({ version: 1, modes }), 'utf8');
|
|
124
|
+
return path;
|
|
125
|
+
}
|
|
126
|
+
export async function resolveOutputModes(cwd, frameworkRoot, invocation = []) {
|
|
127
|
+
const registry = await loadOutputModeRegistry(cwd, frameworkRoot);
|
|
128
|
+
const project = await readOutputModeState(cwd, 'project');
|
|
129
|
+
const session = await readOutputModeState(cwd, 'session');
|
|
130
|
+
const selected = [...project.modes.map(id => ({ id, scope: 'project' })), ...session.modes.map(id => ({ id, scope: 'session' })), ...invocation.map(id => ({ id, scope: 'invocation' }))];
|
|
131
|
+
const effective = new Map();
|
|
132
|
+
const diagnostics = [];
|
|
133
|
+
for (const item of selected) {
|
|
134
|
+
const profile = registry.get(item.id);
|
|
135
|
+
if (!profile)
|
|
136
|
+
throw new Error(`Unknown output mode '${item.id}'. Unknown provider-native or custom modes fail safe; run 'aiwg output-mode list'.`);
|
|
137
|
+
effective.set(item.id, { ...profile, scope: item.scope });
|
|
138
|
+
}
|
|
139
|
+
const modes = [...effective.values()].sort((a, b) => STAGE_ORDER.indexOf(a.stage) - STAGE_ORDER.indexOf(b.stage) || (a.order ?? 0) - (b.order ?? 0) || a.id.localeCompare(b.id));
|
|
140
|
+
for (let i = 0; i < modes.length; i++)
|
|
141
|
+
for (let j = i + 1; j < modes.length; j++) {
|
|
142
|
+
const a = modes[i], b = modes[j];
|
|
143
|
+
if (a.conflicts?.includes(b.id) || b.conflicts?.includes(a.id))
|
|
144
|
+
throw new Error(`Output modes '${a.id}' and '${b.id}' conflict. Disable one or configure an explicit merge strategy.`);
|
|
145
|
+
if (a.kind === b.kind && a.kind !== 'voice')
|
|
146
|
+
throw new Error(`Output modes '${a.id}' and '${b.id}' share kind '${a.kind}' without a merge strategy.`);
|
|
147
|
+
if (a.kind === 'voice' && b.kind === 'voice' && a.mergeStrategy !== 'weighted-voice' && b.mergeStrategy !== 'weighted-voice')
|
|
148
|
+
throw new Error(`Voice modes '${a.id}' and '${b.id}' require an explicit weighted-voice merge strategy.`);
|
|
149
|
+
}
|
|
150
|
+
for (const mode of modes)
|
|
151
|
+
for (const requirement of mode.requires ?? [])
|
|
152
|
+
if (!effective.has(requirement))
|
|
153
|
+
throw new Error(`Output mode '${mode.id}' requires '${requirement}'.`);
|
|
154
|
+
if (modes.length === 0)
|
|
155
|
+
diagnostics.push('unaltered: no configured modes; no instructions or post-processing are added');
|
|
156
|
+
return { modes, diagnostics };
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function protectedPattern(classes) {
|
|
2
|
+
const set = new Set(classes);
|
|
3
|
+
const alternatives = [];
|
|
4
|
+
if (set.has('machine-readable-blocks') || set.has('code'))
|
|
5
|
+
alternatives.push('```[\\s\\S]*?```');
|
|
6
|
+
if (set.has('code') || set.has('commands'))
|
|
7
|
+
alternatives.push('`[^`\\n]+`');
|
|
8
|
+
if (set.has('quoted-text'))
|
|
9
|
+
alternatives.push('^>.*(?:\\n>.*)*', '“[^”]*”', '"[^"\\n]+"');
|
|
10
|
+
if (set.has('citations'))
|
|
11
|
+
alternatives.push('\\[[^\\]\\n]+\\]\\([^\\s)]+\\)', '\\[[0-9]+\\]');
|
|
12
|
+
if (set.has('identifiers'))
|
|
13
|
+
alternatives.push('\\b(?:[A-Za-z_$][\\w$]*\\.)+[A-Za-z_$][\\w$]*\\b', '\\b[A-Z][A-Z0-9_]{2,}\\b');
|
|
14
|
+
return alternatives.length ? new RegExp(alternatives.join('|'), 'gm') : null;
|
|
15
|
+
}
|
|
16
|
+
function protect(content, classes) {
|
|
17
|
+
const literals = [];
|
|
18
|
+
const pattern = protectedPattern(classes);
|
|
19
|
+
const protectedContent = pattern ? content.replace(pattern, value => {
|
|
20
|
+
const token = `\uE000${literals.length}\uE001`;
|
|
21
|
+
literals.push({ token, value });
|
|
22
|
+
return token;
|
|
23
|
+
}) : content;
|
|
24
|
+
return { content: protectedContent, literals };
|
|
25
|
+
}
|
|
26
|
+
function restore(content, literals, mode) {
|
|
27
|
+
let restored = content;
|
|
28
|
+
for (const literal of literals) {
|
|
29
|
+
if (!restored.includes(literal.token))
|
|
30
|
+
throw new Error(`Output mode '${mode}' modified or removed a protected literal.`);
|
|
31
|
+
restored = restored.replaceAll(literal.token, literal.value);
|
|
32
|
+
}
|
|
33
|
+
return restored;
|
|
34
|
+
}
|
|
35
|
+
export async function applyOutputModes(input, modes, options) {
|
|
36
|
+
if (modes.length === 0)
|
|
37
|
+
return { content: input, diagnostics: [], applied: [], fallback: 'none' };
|
|
38
|
+
let content = input;
|
|
39
|
+
const diagnostics = [];
|
|
40
|
+
const applied = [];
|
|
41
|
+
for (const mode of modes) {
|
|
42
|
+
const snapshot = content;
|
|
43
|
+
const masked = protect(content, mode.protectedContent ?? []);
|
|
44
|
+
const transformed = await options.transform(masked.content, mode);
|
|
45
|
+
content = restore(transformed, masked.literals, mode.id);
|
|
46
|
+
if (mode.validation.level !== 'advisory') {
|
|
47
|
+
if (!options.validate)
|
|
48
|
+
throw new Error(`Output mode '${mode.id}' declares ${mode.validation.level} validation but no validator is configured.`);
|
|
49
|
+
const result = await options.validate(content, mode);
|
|
50
|
+
if (!result.valid) {
|
|
51
|
+
diagnostics.push({ mode: mode.id, level: mode.validation.level, message: result.message ?? 'validation failed' });
|
|
52
|
+
if ((options.onMandatoryValidationFailure ?? 'unaltered') === 'fail')
|
|
53
|
+
throw new Error(`Output mode '${mode.id}' validation failed: ${result.message ?? 'no diagnostic'}`);
|
|
54
|
+
return { content: input, diagnostics, applied, fallback: 'unaltered' };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// A transform may only change semantic presentation, never return an absent result.
|
|
58
|
+
if (typeof content !== 'string')
|
|
59
|
+
content = snapshot;
|
|
60
|
+
applied.push(mode.id);
|
|
61
|
+
}
|
|
62
|
+
return { content, diagnostics, applied, fallback: 'none' };
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
/** Match Hermes Agent's process-level HERMES_HOME resolution contract. */
|
|
4
|
+
export function resolveHermesHome(userHome = homedir()) {
|
|
5
|
+
const configured = (process.env.HERMES_HOME || '').trim();
|
|
6
|
+
if (configured)
|
|
7
|
+
return configured;
|
|
8
|
+
if (process.platform === 'win32') {
|
|
9
|
+
const localAppData = (process.env.LOCALAPPDATA || '').trim();
|
|
10
|
+
return localAppData
|
|
11
|
+
? path.join(localAppData, 'hermes')
|
|
12
|
+
: path.join(userHome, 'AppData', 'Local', 'hermes');
|
|
13
|
+
}
|
|
14
|
+
return path.join(userHome, '.hermes');
|
|
15
|
+
}
|
|
16
|
+
/** Resolve a path exactly as a Hermes process would consume HERMES_HOME. */
|
|
17
|
+
export function resolveHermesHomePath(...segments) {
|
|
18
|
+
return path.resolve(resolveHermesHome(), ...segments);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=hermes-home.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import { resolveHermesHomePath } from './hermes-home.js';
|
|
4
5
|
import { getProviderCapabilities, } from './capability-matrix.js';
|
|
5
6
|
const ArtifactPathsSchema = z.object({
|
|
6
7
|
agents: z.string().nullable(),
|
|
@@ -464,7 +465,7 @@ const BUILT_IN_SEEDS = [
|
|
|
464
465
|
id: 'hermes',
|
|
465
466
|
aliases: [],
|
|
466
467
|
builtIn: true,
|
|
467
|
-
surfaces: { primary: 'hermes', compatibility: [], precedence: ['
|
|
468
|
+
surfaces: { primary: 'hermes', compatibility: [], precedence: ['.hermes.md', 'AGENTS.md', resolveHermesHomePath('skills')], related: [] },
|
|
468
469
|
detection: {
|
|
469
470
|
env: [],
|
|
470
471
|
process: ['hermes'],
|
|
@@ -474,18 +475,18 @@ const BUILT_IN_SEEDS = [
|
|
|
474
475
|
artifacts: {
|
|
475
476
|
agents: null,
|
|
476
477
|
commands: null,
|
|
477
|
-
skills: '
|
|
478
|
+
skills: resolveHermesHomePath('skills', '.aiwg'),
|
|
478
479
|
rules: null,
|
|
479
480
|
behaviors: null,
|
|
480
481
|
},
|
|
481
|
-
kernelSkills: '
|
|
482
|
+
kernelSkills: resolveHermesHomePath('skills'),
|
|
482
483
|
configFile: 'AGENTS.md',
|
|
483
484
|
contextFiles: { aiwgMd: true, agentsMd: true, claudeMdHook: false, hookFile: '.hermes.md', contextFile: 'AGENTS.md' },
|
|
484
485
|
},
|
|
485
486
|
smithPaths: {
|
|
486
487
|
agents: null,
|
|
487
488
|
commands: null,
|
|
488
|
-
skills: '
|
|
489
|
+
skills: resolveHermesHomePath('skills'),
|
|
489
490
|
rules: null,
|
|
490
491
|
fileExtension: '.md',
|
|
491
492
|
configFile: 'AGENTS.md',
|
|
@@ -15,6 +15,7 @@ import { getProviderDefinition, normalizeProviderDefinitionId, } from '../provid
|
|
|
15
15
|
import { AGENT_SKILLS_SIDECAR_SCHEMA, AIWG_SKILL_CONTROL_FIELDS, createAgentSkillSidecar, projectStrictAgentSkill, } from './agent-skills.js';
|
|
16
16
|
import { getImportedAgentSkill } from './importer.js';
|
|
17
17
|
import { validateAgentSkillContent } from './validator.js';
|
|
18
|
+
import { resolveHermesHomePath } from '../providers/hermes-home.js';
|
|
18
19
|
export const AGENT_SKILL_MANAGED_MARKER = '.aiwg-managed';
|
|
19
20
|
export const AGENT_SKILL_DEPLOYMENT_SIDECAR = '.aiwg-agent-skill.json';
|
|
20
21
|
const MARKER_CONTENT = 'aiwg-agent-skill-v1\n';
|
|
@@ -78,7 +79,10 @@ function resolvePolicy(target, options) {
|
|
|
78
79
|
reasons.push('applies the Factory description guidance before strict validation');
|
|
79
80
|
break;
|
|
80
81
|
case 'hermes':
|
|
81
|
-
|
|
82
|
+
if (options.homeDir === undefined) {
|
|
83
|
+
root = resolveHermesHomePath('skills');
|
|
84
|
+
}
|
|
85
|
+
reasons.push('uses the active HERMES_HOME skills surface with strict managed ownership markers');
|
|
82
86
|
break;
|
|
83
87
|
case 'openhuman':
|
|
84
88
|
status = 'projected';
|
package/dist/src/skills/run.js
CHANGED
|
@@ -148,6 +148,7 @@ export async function runSkill(opts) {
|
|
|
148
148
|
AIWG_SKILL_DIR: skillDir,
|
|
149
149
|
AIWG_PROJECT_ROOT: opts.cwd,
|
|
150
150
|
...(aiwgRoot ? { AIWG_ROOT: aiwgRoot } : {}),
|
|
151
|
+
...opts.env,
|
|
151
152
|
};
|
|
152
153
|
const fullArgs = [...invocation.prefixArgs, entrypointPath, ...opts.args];
|
|
153
154
|
return new Promise((resolve) => {
|
|
@@ -181,7 +182,7 @@ export async function runSkill(opts) {
|
|
|
181
182
|
* Anything after `--` is verbatim-forwarded. If no `--`, all args after
|
|
182
183
|
* the skill name are forwarded.
|
|
183
184
|
*/
|
|
184
|
-
export async function main(args) {
|
|
185
|
+
export async function main(args, env) {
|
|
185
186
|
// First positional must be the kind ("skill" — reserved for future kinds).
|
|
186
187
|
if (args.length === 0) {
|
|
187
188
|
printUsage();
|
|
@@ -243,6 +244,7 @@ export async function main(args) {
|
|
|
243
244
|
name,
|
|
244
245
|
args: scriptArgs,
|
|
245
246
|
cwdOverride,
|
|
247
|
+
env,
|
|
246
248
|
});
|
|
247
249
|
}
|
|
248
250
|
function printUsage() {
|
|
@@ -24,18 +24,30 @@ import * as path from 'path';
|
|
|
24
24
|
import { buildProviderBootstrapBlock, PROVIDER_BOOTSTRAP_START, PROVIDER_BOOTSTRAP_END, } from './workspace-context.js';
|
|
25
25
|
export const CLAUDE_HOOK_START = '<!-- AIWG:claude-md-hook:start -->';
|
|
26
26
|
export const CLAUDE_HOOK_END = '<!-- AIWG:claude-md-hook:end -->';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
function buildClaudeArtifactOutputPolicy(policy = {}) {
|
|
28
|
+
const providerNative = policy.provider_native ?? 'explicit-only';
|
|
29
|
+
const design = policy.destinations?.['claude-code.design'];
|
|
30
|
+
const designEnabled = design?.enabled !== false && design?.use_when !== 'disabled';
|
|
31
|
+
return [
|
|
32
|
+
'## AIWG artifact output policy',
|
|
33
|
+
'',
|
|
34
|
+
`- Canonical durable artifacts: AIWG artifact store (policy: ${policy.canonical ?? 'aiwg'}).`,
|
|
35
|
+
`- Provider-native presentation/export: ${providerNative}.`,
|
|
36
|
+
`- Claude Design: ${designEnabled ? 'available only when explicitly selected by the resolved policy or requested by the user' : 'disabled by project policy'}.`,
|
|
37
|
+
'- Never substitute, relocate, or omit the canonical AIWG plan/review artifact because Claude adds or changes a provider default.',
|
|
38
|
+
'- When both canonical and presentation outputs are selected, write the canonical artifact first; treat Design as a derived export and record provenance linking it to the canonical source of truth.',
|
|
39
|
+
'- Unknown provider-native destinations fail safe with a diagnostic. Higher-authority project policy overrides user preferences; explicit task selection overrides provider defaults only within the project policy ceiling.',
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
export function buildClaudeHookBlock(policy = {}) {
|
|
33
43
|
return [
|
|
34
44
|
CLAUDE_HOOK_START,
|
|
35
45
|
'',
|
|
36
46
|
buildProviderBootstrapBlock('claude'),
|
|
37
47
|
'@.aiwg/aiwg.config',
|
|
38
48
|
'',
|
|
49
|
+
...buildClaudeArtifactOutputPolicy(policy),
|
|
50
|
+
'',
|
|
39
51
|
'<!--',
|
|
40
52
|
' This block is managed by `aiwg regenerate` and `aiwg use`.',
|
|
41
53
|
' Operator content above and below this block is preserved on regenerate.',
|
|
@@ -57,7 +69,15 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
57
69
|
action: 'skipped',
|
|
58
70
|
warnings: [],
|
|
59
71
|
};
|
|
60
|
-
|
|
72
|
+
let policy = {};
|
|
73
|
+
try {
|
|
74
|
+
const config = JSON.parse(await fs.readFile(path.join(projectPath, '.aiwg', 'aiwg.config'), 'utf8'));
|
|
75
|
+
policy = config.artifact_outputs ?? {};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Missing/legacy/temporarily malformed config receives the safe default.
|
|
79
|
+
}
|
|
80
|
+
const block = buildClaudeHookBlock(policy);
|
|
61
81
|
// Case 1: CLAUDE.md does not exist — create a minimal one with just the block.
|
|
62
82
|
let existing;
|
|
63
83
|
try {
|
package/package.json
CHANGED
|
@@ -210,7 +210,13 @@ function resolveCommandMirrorDir(provider, target) {
|
|
|
210
210
|
// Closest conventional location for providers whose primary command
|
|
211
211
|
// surface is MCP/aggregation rather than a documented command directory.
|
|
212
212
|
if (provider.name === 'hermes') {
|
|
213
|
-
|
|
213
|
+
// #2119: HERMES_HOME is the single source of truth the running Hermes
|
|
214
|
+
// runtime uses to locate its files — resolve the home the same way the
|
|
215
|
+
// provider does instead of hardcoding $HOME/.hermes.
|
|
216
|
+
const hermesHome = typeof provider.getHermesHome === 'function'
|
|
217
|
+
? provider.getHermesHome()
|
|
218
|
+
: path.join(os.homedir(), '.hermes');
|
|
219
|
+
return path.resolve(hermesHome, 'commands');
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
return null;
|
|
@@ -530,7 +536,7 @@ Providers (all deploy agents, commands, skills, and rules):
|
|
|
530
536
|
devin - Devin Desktop (preferred; aliases: devin-desktop, windsurf)
|
|
531
537
|
Paths: .windsurf/agents/, .windsurf/workflows/, .windsurf/skills/, .windsurf/rules/
|
|
532
538
|
hermes - Hermes Agent (MCP-based integration)
|
|
533
|
-
Skills:
|
|
539
|
+
Skills: $HERMES_HOME/skills/ (user-global; defaults to ~/.hermes/skills/) | Agents: AGENTS.md
|
|
534
540
|
Commands/Rules: served via MCP, not file-deployed
|
|
535
541
|
|
|
536
542
|
Modes:
|
|
@@ -463,9 +463,36 @@ export function injectPlatformInContent(content, targetPlatform) {
|
|
|
463
463
|
return open + fmLines.join('\n') + close + body;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
-
/**
|
|
466
|
+
/**
|
|
467
|
+
* Remove the `platforms:` field from a SKILL.md frontmatter block.
|
|
468
|
+
*
|
|
469
|
+
* Hermes (and other providers that treat `platforms:` as an OS gate —
|
|
470
|
+
* linux / macos / windows) hide any skill whose value isn't a recognized
|
|
471
|
+
* OS. AIWG source skills use the field as a *provider* restriction token
|
|
472
|
+
* (`[all]`, provider names), which is the opposite meaning, so deployed
|
|
473
|
+
* copies destined for such providers must drop the field entirely. An
|
|
474
|
+
* absent field is the documented "compatible with all platforms" default.
|
|
475
|
+
*/
|
|
467
476
|
export function stripPlatformsFromContent(content) {
|
|
468
|
-
|
|
477
|
+
const fmMatch = content.match(/^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))([\s\S]*)$/);
|
|
478
|
+
if (!fmMatch) return content;
|
|
479
|
+
|
|
480
|
+
const [, open, fm, close, body] = fmMatch;
|
|
481
|
+
let updated = fm.replace(/^platforms:[^\r\n]*(?:\r?\n|$)/m, '');
|
|
482
|
+
|
|
483
|
+
// Multi-line list form:
|
|
484
|
+
// platforms:
|
|
485
|
+
// - claude-code
|
|
486
|
+
// - hermes
|
|
487
|
+
if (updated === fm) {
|
|
488
|
+
updated = fm.replace(
|
|
489
|
+
/^platforms:[ \t]*\r?\n(?:[ \t]+-[ \t]+\S[^\r\n]*(?:\r?\n|$))*/m,
|
|
490
|
+
'',
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (updated === fm) return content;
|
|
495
|
+
return open + updated + close + body;
|
|
469
496
|
}
|
|
470
497
|
|
|
471
498
|
/**
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* server is an optional enrichment hook that Hermes can call when configured.
|
|
6
6
|
*
|
|
7
7
|
* What this provider DOES deploy:
|
|
8
|
-
* - Skills:
|
|
9
|
-
* - AGENTS.md: project root (
|
|
8
|
+
* - Skills: $HERMES_HOME/skills/ (user-global, for agentic skills callable by Hermes)
|
|
9
|
+
* - AGENTS.md: project root (full AIWG routing guide referenced by .hermes.md)
|
|
10
10
|
*
|
|
11
11
|
* What this provider SKIPS:
|
|
12
12
|
* - Commands: Hermes has no AIWG slash-command file surface
|
|
@@ -22,6 +22,7 @@ let fs;
|
|
|
22
22
|
try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
|
|
23
23
|
import path from 'path';
|
|
24
24
|
import os from 'os';
|
|
25
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
25
26
|
import {
|
|
26
27
|
ensureDir,
|
|
27
28
|
listMdFiles,
|
|
@@ -37,8 +38,61 @@ import {
|
|
|
37
38
|
collectFrameworkArtifacts,
|
|
38
39
|
listOnDemandRuleFiles,
|
|
39
40
|
renderOnDemandRuleSection,
|
|
41
|
+
stripPlatformsFromContent,
|
|
40
42
|
} from './base.mjs';
|
|
41
43
|
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Hermes home resolution (HERMES_HOME) — #2119
|
|
46
|
+
// ============================================================================
|
|
47
|
+
//
|
|
48
|
+
// Mirrors `hermes_constants.get_hermes_home()` in the Hermes Agent runtime.
|
|
49
|
+
// Resolution order (upstream, hermes_constants.py:114-):
|
|
50
|
+
// 1. A context-local override installed in-process via
|
|
51
|
+
// set_hermes_home_override() — AIWG runs in a separate node process and
|
|
52
|
+
// cannot observe that token, so step 1 is intentionally NOT part of the
|
|
53
|
+
// cross-process contract.
|
|
54
|
+
// 2. The process `HERMES_HOME` env var.
|
|
55
|
+
// 3. The platform-native default:
|
|
56
|
+
// win32 → %LOCALAPPDATA%/hermes (falls back to %USERPROFILE%/AppData/
|
|
57
|
+
// Local/hermes when LOCALAPPDATA is unset)
|
|
58
|
+
// other → $HOME/.hermes
|
|
59
|
+
//
|
|
60
|
+
// AIWG reads the env var when `getHermesHome()` is invoked; the module-level
|
|
61
|
+
// constants below lock in the value captured at first use so every consumer
|
|
62
|
+
// (paths, kernels, orchestrate, legacy migration) sees the same resolved root
|
|
63
|
+
// for the life of the process.
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the Hermes home directory.
|
|
67
|
+
*
|
|
68
|
+
* Honors HERMES_HOME the way the running Hermes runtime does, so that
|
|
69
|
+
* `aiwg use --provider hermes` writes skills under the same root the live
|
|
70
|
+
* session scans. See #2119 — before this helper, the provider hardcoded
|
|
71
|
+
* `os.homedir()/.hermes` and any operator running Hermes under a non-default
|
|
72
|
+
* HERMES_HOME (multi-profile, hermes-role wrappers, dev containers) got a
|
|
73
|
+
* silently divergent deployment.
|
|
74
|
+
*/
|
|
75
|
+
export function getHermesHome() {
|
|
76
|
+
const env = (process.env.HERMES_HOME || '').trim();
|
|
77
|
+
if (env) {
|
|
78
|
+
// Match upstream's Path(env) contract exactly. Hermes does not expand a
|
|
79
|
+
// leading `~` or resolve relative values here; both remain relative to the
|
|
80
|
+
// process working directory when the path is consumed.
|
|
81
|
+
return env;
|
|
82
|
+
}
|
|
83
|
+
if (process.platform === 'win32') {
|
|
84
|
+
const localAppData = (process.env.LOCALAPPDATA || '').trim();
|
|
85
|
+
return localAppData
|
|
86
|
+
? path.join(localAppData, 'hermes')
|
|
87
|
+
: path.join(os.homedir(), 'AppData', 'Local', 'hermes');
|
|
88
|
+
}
|
|
89
|
+
return path.join(os.homedir(), '.hermes');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The value captured below is the target any running Hermes session with the
|
|
93
|
+
// same environ would use as its scan root.
|
|
94
|
+
const HERMES_HOME = getHermesHome();
|
|
95
|
+
|
|
42
96
|
// ============================================================================
|
|
43
97
|
// Provider Configuration
|
|
44
98
|
// ============================================================================
|
|
@@ -49,10 +103,15 @@ export const aliases = [];
|
|
|
49
103
|
export const paths = {
|
|
50
104
|
agents: 'AGENTS.md', // Aggregated routing guide at project root
|
|
51
105
|
commands: '', // Not applicable — no AIWG slash-command file surface
|
|
52
|
-
// Standard skills under
|
|
53
|
-
// recursively discovered (verified `agent/skill_utils.py:478-489`,
|
|
54
|
-
// subdirs except .git/.github/.hub/.archive).
|
|
55
|
-
|
|
106
|
+
// Standard skills under <HERMES_HOME>/skills/.aiwg/ — child of Hermes's
|
|
107
|
+
// scanned root, recursively discovered (verified `agent/skill_utils.py:478-489`,
|
|
108
|
+
// os.walk follows subdirs except .git/.github/.hub/.archive).
|
|
109
|
+
//
|
|
110
|
+
// HERMES_HOME honors the process env var and falls back to $HOME/.hermes
|
|
111
|
+
// (win32: %LOCALAPPDATA%/hermes), matching hermes_constants.get_hermes_home().
|
|
112
|
+
// #2119: previously hardcoded os.homedir() — wrong for any operator running
|
|
113
|
+
// Hermes under a non-default HERMES_HOME (multi-profile, hermes-role, etc.).
|
|
114
|
+
skills: path.resolve(HERMES_HOME, 'skills', '.aiwg'),
|
|
56
115
|
rules: '', // Inlined into AGENTS.md + reachable via `aiwg show rule`
|
|
57
116
|
};
|
|
58
117
|
|
|
@@ -60,12 +119,17 @@ export const paths = {
|
|
|
60
119
|
// Standard skills land in the .aiwg/ subdirectory under the same root —
|
|
61
120
|
// Hermes recursively walks the skill root (verified against upstream v0.13.0,
|
|
62
121
|
// `agent/skill_utils.py:478-489`).
|
|
63
|
-
export const kernelSkillsPath = path.
|
|
122
|
+
export const kernelSkillsPath = path.resolve(HERMES_HOME, 'skills');
|
|
123
|
+
|
|
124
|
+
// Resolved home directory this provider's paths were computed against.
|
|
125
|
+
// Consumers (deploy verification, doctor, status) should use this rather than
|
|
126
|
+
// re-reading os.homedir() to stay consistent with the deploy target.
|
|
127
|
+
export const hermesHome = HERMES_HOME;
|
|
64
128
|
|
|
65
129
|
export const support = {
|
|
66
130
|
agents: 'aggregated', // Agents aggregated into lean AGENTS.md
|
|
67
131
|
commands: 'none', // Hermes has no AIWG slash-command file surface
|
|
68
|
-
skills: 'native', //
|
|
132
|
+
skills: 'native', // $HERMES_HOME/skills/ is the native skill location
|
|
69
133
|
rules: 'agents-md+cli', // compressed in AGENTS.md; full bodies via CLI/MCP
|
|
70
134
|
};
|
|
71
135
|
|
|
@@ -77,6 +141,43 @@ export const capabilities = {
|
|
|
77
141
|
homeDirectoryDeploy: true, // Skills deploy to home dir
|
|
78
142
|
};
|
|
79
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Project portable Agent Skills metadata into Hermes's native frontmatter.
|
|
146
|
+
*
|
|
147
|
+
* The portable Agent Skills contract restricts `metadata` values to strings,
|
|
148
|
+
* while Hermes expects tags at `metadata.hermes.tags`. AIWG stores the tag
|
|
149
|
+
* list as a comma-separated `metadata.hermes-tags` string in source and
|
|
150
|
+
* performs the provider-specific projection only in the deployed copy.
|
|
151
|
+
*/
|
|
152
|
+
export function transformHermesSkillContent(content) {
|
|
153
|
+
const stripped = stripPlatformsFromContent(content);
|
|
154
|
+
const match = stripped.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
|
|
155
|
+
if (!match) return stripped;
|
|
156
|
+
|
|
157
|
+
let frontmatter;
|
|
158
|
+
try {
|
|
159
|
+
frontmatter = parseYaml(match[1]);
|
|
160
|
+
} catch {
|
|
161
|
+
return stripped;
|
|
162
|
+
}
|
|
163
|
+
if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter)) {
|
|
164
|
+
return stripped;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const metadata = frontmatter.metadata;
|
|
168
|
+
const encodedTags = metadata && typeof metadata === 'object' && !Array.isArray(metadata)
|
|
169
|
+
? metadata['hermes-tags']
|
|
170
|
+
: undefined;
|
|
171
|
+
if (typeof encodedTags !== 'string') return stripped;
|
|
172
|
+
|
|
173
|
+
const tags = encodedTags.split(',').map((tag) => tag.trim()).filter(Boolean);
|
|
174
|
+
delete metadata['hermes-tags'];
|
|
175
|
+
metadata.hermes = { tags };
|
|
176
|
+
|
|
177
|
+
const body = stripped.slice(match[0].length);
|
|
178
|
+
return `---\n${stringifyYaml(frontmatter).trimEnd()}\n---\n${body}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
80
181
|
// ============================================================================
|
|
81
182
|
// Model Mapping (not applicable — Hermes uses local Ollama models)
|
|
82
183
|
// ============================================================================
|
|
@@ -203,7 +304,8 @@ export function generateAgentsMd(agentCount, skillCount, targetDir, opts) {
|
|
|
203
304
|
const header = `# AIWG Integration
|
|
204
305
|
|
|
205
306
|
AIWG connected through file-based deployment. Native Hermes skills are available
|
|
206
|
-
at
|
|
307
|
+
at \`$HERMES_HOME/skills/\` (kernel) and \`$HERMES_HOME/skills/.aiwg/\` (standard).
|
|
308
|
+
When unset, \`HERMES_HOME\` defaults to the platform-native Hermes home.
|
|
207
309
|
Use \`aiwg discover\` and \`aiwg show <type> <name>\` for the on-demand catalog.
|
|
208
310
|
The MCP sidecar (\`aiwg mcp serve\`) is optional.
|
|
209
311
|
|
|
@@ -285,10 +387,12 @@ AIWG project context lives in \`AGENTS.md\` (this file is a thin Hermes pointer)
|
|
|
285
387
|
|
|
286
388
|
**Routing**: see \`AGENTS.md\` in this directory.
|
|
287
389
|
**MCP**: AIWG is reachable via \`mcp_aiwg_*\` tools.
|
|
288
|
-
**Skills**: kernel skills at
|
|
390
|
+
**Skills**: kernel skills at \`$HERMES_HOME/skills/\`; standard skills at \`$HERMES_HOME/skills/.aiwg/\`.
|
|
391
|
+
When unset, \`HERMES_HOME\` defaults to the platform-native Hermes home.
|
|
289
392
|
|
|
290
|
-
Hermes loads \`.hermes.md\`
|
|
291
|
-
file minimal
|
|
393
|
+
Hermes loads only \`.hermes.md\` when it is present (first-match-wins). Keep
|
|
394
|
+
this file minimal; its routing instruction tells the agent to read \`AGENTS.md\`
|
|
395
|
+
when the full AIWG project context is needed.
|
|
292
396
|
`;
|
|
293
397
|
const destPath = path.join(targetDir, '.hermes.md');
|
|
294
398
|
if (dryRun) {
|
|
@@ -309,8 +413,8 @@ file minimal — Hermes will load AGENTS.md content next via the routing chain.
|
|
|
309
413
|
*
|
|
310
414
|
* Skills are user-global in Hermes, deployed once, available in all
|
|
311
415
|
* projects. Kernel routing per the cross-provider pattern:
|
|
312
|
-
* - kernel skills →
|
|
313
|
-
* - standard →
|
|
416
|
+
* - kernel skills → $HERMES_HOME/skills/ (platform-native, always-loaded)
|
|
417
|
+
* - standard → $HERMES_HOME/skills/.aiwg/ (recursively walked by Hermes)
|
|
314
418
|
*/
|
|
315
419
|
export function deploySkills(skillDirs, opts) {
|
|
316
420
|
const standardDestDir = paths.skills;
|
|
@@ -375,7 +479,18 @@ export async function deploy(opts) {
|
|
|
375
479
|
allSkillDirs.push(...(artifacts.skills || []));
|
|
376
480
|
|
|
377
481
|
if (allSkillDirs.length > 0) {
|
|
378
|
-
|
|
482
|
+
// Hermes's skill loader reads `platforms:` as an OS gate
|
|
483
|
+
// (linux / macos / windows). AIWG's shared deploy path injects
|
|
484
|
+
// `[hermes]` into that field, which then filters every skill out
|
|
485
|
+
// on Linux. Strip the field post-injection — hermes documents
|
|
486
|
+
// "absent field = all platforms" as its default. This leaves the
|
|
487
|
+
// other providers' `transformSkillMd` pipeline untouched.
|
|
488
|
+
const skillOpts = {
|
|
489
|
+
...opts,
|
|
490
|
+
provider: 'hermes', // ensure deploySkillDir's injectPlatform branch runs
|
|
491
|
+
transformSkillMd: transformHermesSkillContent,
|
|
492
|
+
};
|
|
493
|
+
deploySkills(allSkillDirs, skillOpts);
|
|
379
494
|
} else if (!opts.quiet) {
|
|
380
495
|
console.log(' No skills found to deploy');
|
|
381
496
|
}
|
|
@@ -424,7 +539,7 @@ export async function deploy(opts) {
|
|
|
424
539
|
|
|
425
540
|
// ── aiwg-orchestrate convenience skill (#1242) ──────────────────────────────
|
|
426
541
|
// First-deploy-only copy: lays down the delegate_task wrapper at
|
|
427
|
-
//
|
|
542
|
+
// $HERMES_HOME/skills/aiwg-orchestrate/SKILL.md if it isn't already present.
|
|
428
543
|
// The skill provides ~95% per-workflow context reduction by routing AIWG
|
|
429
544
|
// calls through Hermes's `delegate_task` instead of inline MCP. Idempotent
|
|
430
545
|
// on re-run — operator edits are preserved across `aiwg use` invocations.
|
|
@@ -435,10 +550,35 @@ export async function deploy(opts) {
|
|
|
435
550
|
// ── Post-deployment hint ───────────────────────────────────────────────────
|
|
436
551
|
if (!opts.quiet) {
|
|
437
552
|
console.log('');
|
|
553
|
+
console.log(`Skills root: ${kernelSkillsPath}`);
|
|
438
554
|
console.log('Rules are in AGENTS.md as compressed directives; full bodies via `aiwg show rule <name>`.');
|
|
439
|
-
console.log('Optional: configure
|
|
555
|
+
console.log('Optional: configure config.yaml to connect AIWG MCP server.');
|
|
440
556
|
console.log('See: docs/integrations/hermes-quickstart.md (optional MCP setup)');
|
|
441
557
|
}
|
|
558
|
+
|
|
559
|
+
// ── Consumer visibility check (#2119) ──────────────────────────────────────
|
|
560
|
+
// The running Hermes runtime reads skills from get_skills_dir(), which is
|
|
561
|
+
// HERMES_HOME/skills (hermes_constants.get_hermes_home: context-local
|
|
562
|
+
// override → HERMES_HOME env → $HOME/.hermes). AIWG can observe only ITS
|
|
563
|
+
// own process environment, so this check reports which root AIWG resolved
|
|
564
|
+
// and warns when that root may be invisible to a Hermes session the
|
|
565
|
+
// operator launched elsewhere (custom HERMES_HOME, hermes-role wrapper).
|
|
566
|
+
//
|
|
567
|
+
// Before #2119 this was a silent failure: AIWG wrote to $HOME/.hermes/skills
|
|
568
|
+
// unconditionally and `aiwg status --probe` reported healthy even when the
|
|
569
|
+
// live session scanned a different HERMES_HOME.
|
|
570
|
+
if (!dryRun && !opts.quiet) {
|
|
571
|
+
const envHome = (process.env.HERMES_HOME || '').trim();
|
|
572
|
+
const hermesActive = Boolean(
|
|
573
|
+
process.env.HERMES_SESSION_ID ||
|
|
574
|
+
(process.env.AI_AGENT || '').includes('hermes')
|
|
575
|
+
);
|
|
576
|
+
if (hermesActive && !envHome) {
|
|
577
|
+
console.warn(`Warning: HERMES_HOME is not set in this AIWG process — deployed skills landed under \`${kernelSkillsPath}\` (default $HOME/.hermes).`);
|
|
578
|
+
console.warn(' If your running Hermes session uses a non-default HERMES_HOME, it CANNOT see these skills.');
|
|
579
|
+
console.warn(' Re-run with the matching value: HERMES_HOME=<that value> aiwg use --provider hermes');
|
|
580
|
+
}
|
|
581
|
+
}
|
|
442
582
|
}
|
|
443
583
|
|
|
444
584
|
// ============================================================================
|
|
@@ -584,7 +724,7 @@ export function migrateLegacySkillPath(opts) {
|
|
|
584
724
|
// ============================================================================
|
|
585
725
|
|
|
586
726
|
/**
|
|
587
|
-
* Copy the aiwg-orchestrate skill template to
|
|
727
|
+
* Copy the aiwg-orchestrate skill template to $HERMES_HOME/skills/ on first
|
|
588
728
|
* deploy. Skip if a SKILL.md already exists — preserves operator edits and
|
|
589
729
|
* any prior version they're running. Errors during the copy are non-fatal:
|
|
590
730
|
* the rest of the deploy must succeed even if the home dir is read-only or
|
|
@@ -631,7 +771,11 @@ function deployAiwgOrchestrateSkill(srcRoot, opts) {
|
|
|
631
771
|
try {
|
|
632
772
|
ensureDir(destDir);
|
|
633
773
|
const content = fs.readFileSync(templatePath, 'utf8');
|
|
634
|
-
|
|
774
|
+
// Defensive: the template is hermes-specific and should not carry a
|
|
775
|
+
// `platforms:` field (hermes reads that as an OS gate). Strip it if a
|
|
776
|
+
// future template regression reintroduces one so this orphan path
|
|
777
|
+
// stays consistent with the main deploy pipeline.
|
|
778
|
+
fs.writeFileSync(destPath, transformHermesSkillContent(content), 'utf8');
|
|
635
779
|
if (!opts.quiet) {
|
|
636
780
|
console.log(` Installed aiwg-orchestrate to ${destPath} (delegate_task wrapper, 95% context reduction)`);
|
|
637
781
|
}
|