@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.
- 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/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/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/skills/run.js +3 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +27 -7
- package/package.json +1 -1
|
@@ -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('');
|
|
@@ -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
|
|
@@ -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
|
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 {
|