@aiwg/cli 2026.8.13 → 2026.8.15

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('');
@@ -68,8 +68,10 @@ export function parseSetupProjectOptions(ctx) {
68
68
  nonInteractive: boolFlag(args, '--non-interactive') || boolFlag(args, '--yes'),
69
69
  primary: flagValue(args, '--primary'),
70
70
  issueTracker: flagValue(args, '--issue-tracker'),
71
+ customerIssueTracker: flagValue(args, '--customer-issue-tracker'),
71
72
  ci: flagValue(args, '--ci'),
72
73
  issueProvider: parseEnum(flagValue(args, '--issue-provider'), ISSUE_PROVIDERS, '--issue-provider'),
74
+ customerIssueProvider: parseEnum(flagValue(args, '--customer-issue-provider'), ISSUE_PROVIDERS, '--customer-issue-provider'),
73
75
  deliveryMode: parseEnum(flagValue(args, '--delivery-mode'), DELIVERY_MODES, '--delivery-mode'),
74
76
  defaultBranch: flagValue(args, '--default-branch'),
75
77
  requireCiGreen: parseBooleanFlag(args, '--require-ci-green'),
@@ -84,6 +86,8 @@ export function parseSetupProjectOptions(ctx) {
84
86
  signingEnforce: parseEnum(flagValue(args, '--signing-enforce'), ['commits', 'tags', 'all'], '--signing-enforce'),
85
87
  trackerActorLogin: flagValue(args, '--tracker-actor-login'),
86
88
  trackerActorVia: parseEnum(flagValue(args, '--tracker-actor-via'), TRACKER_VIA, '--tracker-actor-via'),
89
+ customerTrackerActorLogin: flagValue(args, '--customer-tracker-actor-login'),
90
+ customerTrackerActorVia: parseEnum(flagValue(args, '--customer-tracker-actor-via'), TRACKER_VIA, '--customer-tracker-actor-via'),
87
91
  providers: parseStringList(flagValue(args, '--providers')),
88
92
  };
89
93
  }
@@ -201,6 +205,12 @@ function validateSetupConfig(config, remotes, issueProvider) {
201
205
  if (config.remotes?.issue_provider && !ISSUE_PROVIDERS.includes(config.remotes.issue_provider)) {
202
206
  errors.push('remotes.issue_provider is invalid');
203
207
  }
208
+ if (config.remotes?.customer_issue_tracker) {
209
+ checkRemote('remotes.customer_issue_tracker', config.remotes.customer_issue_tracker);
210
+ }
211
+ if (config.remotes?.customer_issue_provider && !ISSUE_PROVIDERS.includes(config.remotes.customer_issue_provider)) {
212
+ errors.push('remotes.customer_issue_provider is invalid');
213
+ }
204
214
  checkRemote('remotes.ci', config.remotes?.ci);
205
215
  if (!DELIVERY_MODES.includes(config.delivery?.mode))
206
216
  errors.push('delivery.mode is invalid');
@@ -217,6 +227,9 @@ function validateSetupConfig(config, remotes, issueProvider) {
217
227
  if (config.remotes?.tracker_actor?.via && !TRACKER_VIA.includes(config.remotes.tracker_actor.via)) {
218
228
  errors.push('remotes.tracker_actor.via is invalid');
219
229
  }
230
+ if (config.remotes?.customer_tracker_actor?.via && !TRACKER_VIA.includes(config.remotes.customer_tracker_actor.via)) {
231
+ errors.push('remotes.customer_tracker_actor.via is invalid');
232
+ }
220
233
  if (!config.providers.every(p => VALID_PROVIDERS.includes(p))) {
221
234
  errors.push('providers contains an unknown AIWG provider');
222
235
  }
@@ -237,11 +250,15 @@ export async function buildSetupProjectPlan(options) {
237
250
  ? 'local'
238
251
  : options.issueTracker ?? base.remotes?.issue_tracker ?? primary;
239
252
  const ci = options.ci ?? base.remotes?.ci ?? primary;
253
+ const customerIssueTracker = options.customerIssueTracker ?? base.remotes?.customer_issue_tracker;
254
+ const customerIssueProvider = options.customerIssueProvider ?? base.remotes?.customer_issue_provider;
240
255
  const remotesConfig = {
241
256
  primary,
242
257
  issue_tracker: issueTracker,
243
258
  issue_provider: issueProvider,
244
259
  ci,
260
+ ...(customerIssueTracker ? { customer_issue_tracker: customerIssueTracker } : {}),
261
+ ...(customerIssueProvider ? { customer_issue_provider: customerIssueProvider } : {}),
245
262
  secondary: base.remotes?.secondary ?? secondaryRemotes(remotes, primary, issueTracker, ci),
246
263
  };
247
264
  const trackerLogin = options.trackerActorLogin ?? base.remotes?.tracker_actor?.login;
@@ -253,6 +270,16 @@ export async function buildSetupProjectPlan(options) {
253
270
  ...(trackerVia ? { via: trackerVia } : {}),
254
271
  };
255
272
  }
273
+ const customerTrackerLogin = options.customerTrackerActorLogin ?? base.remotes?.customer_tracker_actor?.login;
274
+ const customerTrackerVia = options.customerTrackerActorVia ?? base.remotes?.customer_tracker_actor?.via
275
+ ?? (customerIssueProvider === 'github' ? 'gh' : customerIssueProvider === 'gitea' ? 'tea' : undefined);
276
+ if (customerTrackerLogin || customerTrackerVia || base.remotes?.customer_tracker_actor?.forbid_actors) {
277
+ remotesConfig.customer_tracker_actor = {
278
+ ...(base.remotes?.customer_tracker_actor ?? {}),
279
+ ...(customerTrackerLogin ? { login: customerTrackerLogin } : {}),
280
+ ...(customerTrackerVia ? { via: customerTrackerVia } : {}),
281
+ };
282
+ }
256
283
  const existingDelivery = base.delivery ?? {};
257
284
  const delivery = {
258
285
  ...existingDelivery,
@@ -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.
@@ -477,6 +478,7 @@ export function resolveRemoteProvider(remoteUrl) {
477
478
  * Defaults:
478
479
  * - `primary` defaults to "origin"
479
480
  * - `issue_tracker` defaults to `primary`
481
+ * - customer tracker fields remain unset unless explicitly configured
480
482
  * - `ci` defaults to `primary`
481
483
  * - `secondary` defaults to `[]`
482
484
  *
@@ -490,6 +492,9 @@ export function resolveRemotes(remotes) {
490
492
  issue_provider: remotes?.issue_provider,
491
493
  ci: remotes?.ci ?? primary,
492
494
  tracker_actor: remotes?.tracker_actor,
495
+ customer_issue_tracker: remotes?.customer_issue_tracker,
496
+ customer_issue_provider: remotes?.customer_issue_provider,
497
+ customer_tracker_actor: remotes?.customer_tracker_actor,
493
498
  transport: remotes?.transport,
494
499
  secondary: remotes?.secondary ?? [],
495
500
  };
@@ -638,6 +643,7 @@ export function emptyConfig(providers = ['claude']) {
638
643
  security: {
639
644
  threatAssessment: defaultThreatAssessmentConfig(),
640
645
  },
646
+ artifact_outputs: defaultArtifactOutputs(),
641
647
  delivery: {
642
648
  mode: 'pr-required',
643
649
  default_branch: 'main',
@@ -730,6 +736,9 @@ export async function readAiwgConfig(projectDir) {
730
736
  if (threatAssessmentErrors.length > 0) {
731
737
  throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
732
738
  }
739
+ const artifactOutputErrors = validateArtifactOutputs(parsed.artifact_outputs);
740
+ if (artifactOutputErrors.length > 0)
741
+ throw new Error(`Invalid .aiwg/aiwg.config:\n${artifactOutputErrors.join('\n')}`);
733
742
  return parsed;
734
743
  }
735
744
  /**
@@ -742,6 +751,9 @@ export async function writeAiwgConfig(projectDir, config) {
742
751
  if (threatAssessmentErrors.length > 0) {
743
752
  throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
744
753
  }
754
+ const artifactOutputErrors = validateArtifactOutputs(config.artifact_outputs);
755
+ if (artifactOutputErrors.length > 0)
756
+ throw new Error(`Invalid .aiwg/aiwg.config:\n${artifactOutputErrors.join('\n')}`);
745
757
  const localPath = getConfigPath(projectDir);
746
758
  const artifactDir = resolveProjectAiwgDir(projectDir);
747
759
  const artifactPath = join(artifactDir, CONFIG_FILENAME);
@@ -151,10 +151,14 @@ const ENUM_RULES = {
151
151
  'delivery.release_signing.format': ['openpgp', 'ssh', 'x509'],
152
152
  'delivery.release_signing.enforce': ['commits', 'tags', 'all'],
153
153
  'remotes.issue_provider': ['gitea', 'github', 'local'],
154
+ 'remotes.customer_issue_provider': ['gitea', 'github', 'local'],
154
155
  'remotes.tracker_actor.via': ['tea', 'gh', 'mcp', 'api'],
156
+ 'remotes.customer_tracker_actor.via': ['tea', 'gh', 'mcp', 'api'],
155
157
  'remotes.transport.protocol': ['ssh', 'https'],
156
158
  'repo_maintainer.tiers.local': ['collaborator', 'maintainer', 'admin'],
157
159
  'security.threatAssessment.mode': ['off', 'audit', 'enforce'],
160
+ 'artifact_outputs.canonical': ['aiwg'],
161
+ 'artifact_outputs.provider_native': ['disabled', 'explicit-only', 'project-default'],
158
162
  };
159
163
  const BOOLEAN_FIELDS = new Set([
160
164
  'delivery.delete_branch_on_merge',
@@ -167,6 +171,7 @@ const BOOLEAN_FIELDS = new Set([
167
171
  ]);
168
172
  const STRING_ARRAY_FIELDS = new Set([
169
173
  'remotes.tracker_actor.forbid_actors',
174
+ 'remotes.customer_tracker_actor.forbid_actors',
170
175
  'command_log.scopes',
171
176
  'telemetry.skill_usage.scopes',
172
177
  ]);
@@ -590,6 +595,9 @@ For project-level config: aiwg config show --project [--json]
590
595
  const remotesView = {
591
596
  primary: { name: resolvedRemotes.primary, url: getUrl(resolvedRemotes.primary) },
592
597
  issue_tracker: { name: resolvedRemotes.issue_tracker, url: getUrl(resolvedRemotes.issue_tracker) },
598
+ customer_issue_tracker: resolvedRemotes.customer_issue_tracker
599
+ ? { name: resolvedRemotes.customer_issue_tracker, url: getUrl(resolvedRemotes.customer_issue_tracker) }
600
+ : null,
593
601
  ci: { name: resolvedRemotes.ci, url: getUrl(resolvedRemotes.ci) },
594
602
  secondary: resolvedRemotes.secondary.map((s) => ({
595
603
  ...s,
@@ -651,7 +659,10 @@ For project-level config: aiwg config show --project [--json]
651
659
  };
652
660
  console.log(fmt('Primary ', remotesView.primary));
653
661
  if (remotesView.issue_tracker.name !== remotesView.primary.name) {
654
- console.log(fmt('Issue tracker ', remotesView.issue_tracker));
662
+ console.log(fmt('Internal issues', remotesView.issue_tracker));
663
+ }
664
+ if (remotesView.customer_issue_tracker) {
665
+ console.log(fmt('Customer issues', remotesView.customer_issue_tracker));
655
666
  }
656
667
  if (remotesView.ci.name !== remotesView.primary.name) {
657
668
  console.log(fmt('CI ', remotesView.ci));
@@ -119,8 +119,7 @@ async function resolveEndpoint(repoPath, remote, providerHint) {
119
119
  : 'unknown',
120
120
  };
121
121
  }
122
- function trackerProviderHint(remotes, fallback) {
123
- const configured = remotes.issue_provider;
122
+ function providerHint(configured, fallback) {
124
123
  if (configured === 'gitea' || configured === 'github')
125
124
  return configured;
126
125
  return fallback;
@@ -130,10 +129,14 @@ async function resolveMember(entry, workspaceRoot) {
130
129
  const configPath = getConfigPath(memberPath);
131
130
  const config = await readAiwgConfig(memberPath);
132
131
  const remotes = resolveRemotes(config?.remotes);
133
- const issueProviderHint = trackerProviderHint(remotes, entry.provider);
134
- const [primary, issueTracker, ci] = await Promise.all([
132
+ const issueProviderHint = providerHint(remotes.issue_provider, entry.provider);
133
+ const customerProviderHint = providerHint(remotes.customer_issue_provider);
134
+ const [primary, issueTracker, customerIssueTracker, ci] = await Promise.all([
135
135
  resolveEndpoint(memberPath, remotes.primary, entry.provider),
136
136
  resolveEndpoint(memberPath, remotes.issue_tracker, issueProviderHint),
137
+ remotes.customer_issue_tracker
138
+ ? resolveEndpoint(memberPath, remotes.customer_issue_tracker, customerProviderHint)
139
+ : Promise.resolve(undefined),
137
140
  resolveEndpoint(memberPath, remotes.ci, entry.provider),
138
141
  ]);
139
142
  const drift = [];
@@ -145,12 +148,18 @@ async function resolveMember(entry, workspaceRoot) {
145
148
  drift.push(`primary remote '${remotes.primary}' is unavailable`);
146
149
  if (!issueTracker.url)
147
150
  drift.push(`issue tracker remote '${remotes.issue_tracker}' is unavailable`);
151
+ if (remotes.customer_issue_tracker && !customerIssueTracker?.url) {
152
+ drift.push(`customer issue tracker remote '${remotes.customer_issue_tracker}' is unavailable`);
153
+ }
148
154
  if (primary.provider === 'unknown') {
149
155
  drift.push(`primary remote provider is unknown for '${primary.domain ?? remotes.primary}'`);
150
156
  }
151
157
  if (issueTracker.provider === 'unknown') {
152
158
  drift.push(`issue tracker provider is unknown for '${issueTracker.domain ?? remotes.issue_tracker}'`);
153
159
  }
160
+ if (customerIssueTracker?.provider === 'unknown') {
161
+ drift.push(`customer issue tracker provider is unknown for '${customerIssueTracker.domain ?? remotes.customer_issue_tracker}'`);
162
+ }
154
163
  if (entry.allowed.includes('issue-comment') && !remotes.tracker_actor?.login) {
155
164
  drift.push('issue-comment is allowed but remotes.tracker_actor.login is missing');
156
165
  }
@@ -158,6 +167,10 @@ async function resolveMember(entry, workspaceRoot) {
158
167
  && remotes.tracker_actor.forbid_actors?.includes(remotes.tracker_actor.login)) {
159
168
  drift.push(`configured tracker actor '${remotes.tracker_actor.login}' is also forbidden`);
160
169
  }
170
+ if (remotes.customer_tracker_actor?.login
171
+ && remotes.customer_tracker_actor.forbid_actors?.includes(remotes.customer_tracker_actor.login)) {
172
+ drift.push(`configured customer tracker actor '${remotes.customer_tracker_actor.login}' is also forbidden`);
173
+ }
161
174
  if (config?.delivery?.signing?.enforce
162
175
  && !config.delivery.signing.key
163
176
  && !config.delivery.signing.key_file) {
@@ -177,6 +190,7 @@ async function resolveMember(entry, workspaceRoot) {
177
190
  remotes,
178
191
  primary,
179
192
  issueTracker,
193
+ ...(customerIssueTracker ? { customerIssueTracker } : {}),
180
194
  ci,
181
195
  drift,
182
196
  };
@@ -251,33 +265,35 @@ export async function authorizeWorkspaceOperation(startPath, targetPath, action,
251
265
  };
252
266
  }
253
267
  /** Enforce the member config tracker_actor + forbid_actors contract. */
254
- export function checkTrackerActor(member, actualActor) {
255
- const configured = member.remotes.tracker_actor;
268
+ export function checkTrackerActor(member, actualActor, role = 'internal') {
269
+ const configured = role === 'customer'
270
+ ? member.remotes.customer_tracker_actor
271
+ : member.remotes.tracker_actor;
256
272
  const actor = actualActor ?? configured?.login;
257
273
  if (!actor) {
258
274
  return {
259
275
  allowed: false,
260
- reason: `repo '${member.name}' does not resolve a tracker actor`,
276
+ reason: `repo '${member.name}' does not resolve a ${role} tracker actor`,
261
277
  };
262
278
  }
263
279
  if (configured?.forbid_actors?.includes(actor)) {
264
280
  return {
265
281
  allowed: false,
266
282
  actor,
267
- reason: `tracker actor '${actor}' is forbidden by repo '${member.name}'`,
283
+ reason: `${role} tracker actor '${actor}' is forbidden by repo '${member.name}'`,
268
284
  };
269
285
  }
270
286
  if (actualActor && configured?.login && actualActor !== configured.login) {
271
287
  return {
272
288
  allowed: false,
273
289
  actor,
274
- reason: `tracker actor '${actualActor}' does not match configured actor '${configured.login}'`,
290
+ reason: `${role} tracker actor '${actualActor}' does not match configured actor '${configured.login}'`,
275
291
  };
276
292
  }
277
293
  return {
278
294
  allowed: true,
279
295
  actor,
280
- reason: `tracker actor '${actor}' is allowed for repo '${member.name}'`,
296
+ reason: `${role} tracker actor '${actor}' is allowed for repo '${member.name}'`,
281
297
  };
282
298
  }
283
299
  //# sourceMappingURL=workspace.js.map
@@ -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() {
@@ -50,7 +50,7 @@ export async function buildAiwgMdContent(projectPath, stagedClaudeMdContent) {
50
50
  // #1362: parallelism cap section, injected after generation so it surfaces
51
51
  // in regenerated context files regardless of CLAUDE.md content.
52
52
  const parallelismSection = await buildParallelismSection(projectPath);
53
- const finalizationBlock = await buildContextFinalizationBlock(projectPath);
53
+ const finalizationBlock = await buildContextFinalizationBlock(projectPath, path.join(projectPath, 'AIWG.md'));
54
54
  const externalLinksSection = await buildExternalLinksSection(projectPath);
55
55
  if (claudeMdContent) {
56
56
  // Insert the AIWG signature comment as the second line.
@@ -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 {
@@ -54,7 +54,21 @@ function displayProjectPath(projectPath, targetPath) {
54
54
  return relative;
55
55
  return targetPath;
56
56
  }
57
- export async function buildContextFinalizationBlock(projectPath) {
57
+ function documentRelativeHref(projectPath, documentPath, targetPath) {
58
+ const absoluteDocument = path.isAbsolute(documentPath)
59
+ ? documentPath
60
+ : path.resolve(projectPath, documentPath);
61
+ const absoluteTarget = path.isAbsolute(targetPath)
62
+ ? targetPath
63
+ : path.resolve(projectPath, targetPath);
64
+ const relative = path.relative(path.dirname(absoluteDocument), absoluteTarget).replace(/\\/g, '/');
65
+ if (!relative)
66
+ return `./${path.basename(absoluteTarget)}`;
67
+ return relative.startsWith('./') || relative.startsWith('../')
68
+ ? relative
69
+ : `./${relative}`;
70
+ }
71
+ export async function buildContextFinalizationBlock(projectPath, documentPath = path.join(projectPath, 'AIWG.md')) {
58
72
  const config = await readConfig(projectPath);
59
73
  const remoteUrls = await readGitRemoteUrls(projectPath);
60
74
  const providers = config?.providers ?? [];
@@ -68,6 +82,7 @@ export async function buildContextFinalizationBlock(projectPath) {
68
82
  providerDeployments.add(provider);
69
83
  }
70
84
  }
85
+ const trackerAuthority = resolveTrackerAuthority(config, remoteUrls);
71
86
  const lines = [
72
87
  FINALIZATION_START,
73
88
  '## Context Finalization',
@@ -91,7 +106,9 @@ export async function buildContextFinalizationBlock(projectPath) {
91
106
  '',
92
107
  'When a user asks whether AIWG is active or engaged in this project, run or read `aiwg status --probe --json` and report the result plainly: engaged state, project root, deployed provider files, installed frameworks/addons, and the next action from the probe. Do not add AIWG attribution, signatures, generated-by text, or passive footers to user files, commits, PRs, comments, code headers, or docs.',
93
108
  '',
94
- renderTrackerProtocol(resolveTrackerAuthority(config, remoteUrls)),
109
+ renderTrackerProtocol(trackerAuthority, {
110
+ configHref: documentRelativeHref(projectPath, documentPath, trackerAuthority.configPath),
111
+ }),
95
112
  '',
96
113
  '### Source Model',
97
114
  '',
@@ -112,7 +129,8 @@ export function replaceOrAppendFinalizationBlock(content, block) {
112
129
  return `${trimmed}\n\n${block}`;
113
130
  }
114
131
  export async function buildNormalizedAiwgMd(projectPath, existing = '') {
115
- const block = await buildContextFinalizationBlock(projectPath);
132
+ const normalizedDocumentPath = projectControlPath(projectPath, 'AIWG.md');
133
+ const block = await buildContextFinalizationBlock(projectPath, normalizedDocumentPath);
116
134
  const externalLinksSection = await buildExternalLinksSection(projectPath);
117
135
  const normalizedAiwgMdPath = displayProjectPath(projectPath, projectControlPath(projectPath, 'AIWG.md'));
118
136
  const base = existing.trim().length > 0
@@ -54,6 +54,15 @@ export function resolveTrackerAuthority(config, remoteUrls = {}, configPath = '.
54
54
  const storageProvider = providerFromIssueStorage(issueStorage);
55
55
  const configuredProvider = remotes.issue_provider ? normalizeProvider(remotes.issue_provider) : 'unknown';
56
56
  const urlProvider = issueTrackerUrl ? normalizeProvider(resolveRemoteProvider(issueTrackerUrl)) : 'unknown';
57
+ const customerIssueTrackerUrl = remotes.customer_issue_tracker
58
+ ? remoteUrls[remotes.customer_issue_tracker]
59
+ : undefined;
60
+ const configuredCustomerProvider = remotes.customer_issue_provider
61
+ ? normalizeProvider(remotes.customer_issue_provider)
62
+ : 'unknown';
63
+ const customerUrlProvider = customerIssueTrackerUrl
64
+ ? normalizeProvider(resolveRemoteProvider(customerIssueTrackerUrl))
65
+ : 'unknown';
57
66
  return {
58
67
  configPath,
59
68
  primaryRemote: remotes.primary,
@@ -66,6 +75,13 @@ export function resolveTrackerAuthority(config, remoteUrls = {}, configPath = '.
66
75
  : storageProvider !== 'unknown'
67
76
  ? storageProvider
68
77
  : urlProvider,
78
+ ...(remotes.customer_issue_tracker ? {
79
+ customerIssueTrackerRemote: remotes.customer_issue_tracker,
80
+ customerIssueTrackerUrl,
81
+ customerProvider: configuredCustomerProvider !== 'unknown'
82
+ ? configuredCustomerProvider
83
+ : customerUrlProvider,
84
+ } : {}),
69
85
  secondaryRemotes: remotes.secondary,
70
86
  };
71
87
  }
@@ -91,7 +107,7 @@ export function chooseTrackerAccess(authority, probes) {
91
107
  ].join(' '),
92
108
  };
93
109
  }
94
- export function renderTrackerProtocol(authority) {
110
+ export function renderTrackerProtocol(authority, options = {}) {
95
111
  const secondary = authority.secondaryRemotes.length > 0
96
112
  ? authority.secondaryRemotes
97
113
  .map((remote) => `${remote.name}${remote.purpose ? ` (${remote.purpose})` : ''}`)
@@ -99,11 +115,16 @@ export function renderTrackerProtocol(authority) {
99
115
  : 'none configured';
100
116
  const issueStorage = authority.issueStorage ?? 'not configured';
101
117
  const trackerUrl = authority.issueTrackerUrl ?? 'remote URL unavailable';
118
+ const customerTracker = authority.customerIssueTrackerRemote
119
+ ? `\`${authority.customerIssueTrackerRemote}\` (${authority.customerProvider ?? 'unknown'}; ${authority.customerIssueTrackerUrl ?? 'remote URL unavailable'})`
120
+ : 'not configured';
121
+ const configHref = options.configHref ?? `./${authority.configPath}`;
102
122
  return [
103
123
  '### Tracker Authority Protocol',
104
124
  '',
105
- `- Source of truth: [${authority.configPath}](./${authority.configPath})`,
106
- `- Canonical tracker: \`${authority.issueTrackerRemote}\` (${authority.provider}; ${trackerUrl})`,
125
+ `- Source of truth: [${authority.configPath}](${configHref})`,
126
+ `- Internal/canonical tracker: \`${authority.issueTrackerRemote}\` (${authority.provider}; ${trackerUrl})`,
127
+ `- Customer issue tracker: ${customerTracker}`,
107
128
  `- Primary repo remote: \`${authority.primaryRemote}\`; CI remote: \`${authority.ciRemote}\``,
108
129
  `- Secondary/mirror remotes: ${secondary}`,
109
130
  `- Issue storage mode: ${issueStorage}`,
@@ -115,6 +136,8 @@ export function renderTrackerProtocol(authority) {
115
136
  '4. Stop and report a blocker.',
116
137
  '',
117
138
  '- Project config decides tracker authority; installed/authenticated CLIs do not.',
139
+ '- Route internal engineering, delivery, and CI-sensitive issue work to the internal tracker.',
140
+ '- Route customer acknowledgements, follow-up, and closure to the customer tracker when configured.',
118
141
  '- Git SSH remote access is repository sync, not issue-tracker API access.',
119
142
  '- Do not file on mirror or secondary remotes just because their CLI is authenticated.',
120
143
  '- Treat an unauthenticated tracker CLI as one failed access path, then continue probing MCP/app/API before blocking.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.8.13",
3
+ "version": "2026.8.15",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",