@aiwg/cli 2026.8.13 → 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.
@@ -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
- const scriptName = ctx.args[0];
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(ctx.args);
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('');
@@ -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);
@@ -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
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
- * The managed block that gets inserted into CLAUDE.md. Tells Claude Code
29
- * to load AIWG.md as the project context entry point. Kept short — the
30
- * actual context lives in AIWG.md.
31
- */
32
- export function buildClaudeHookBlock() {
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
- const block = buildClaudeHookBlock();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.8.13",
3
+ "version": "2026.8.14",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",