@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.
- package/dist/src/artifacts/output-policy.js +87 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/output-mode.js +69 -0
- package/dist/src/cli/handlers/run.js +42 -4
- package/dist/src/cli/handlers/setup.js +27 -0
- package/dist/src/config/aiwg-config.js +12 -0
- package/dist/src/config/cli.js +12 -1
- package/dist/src/config/workspace.js +26 -10
- 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/aiwg-md.js +1 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +27 -7
- package/dist/src/smiths/context-pipeline/finalization.js +21 -3
- package/dist/src/tracker/capability-protocol.js +26 -3
- 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('');
|
|
@@ -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);
|
package/dist/src/config/cli.js
CHANGED
|
@@ -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('
|
|
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
|
|
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 =
|
|
134
|
-
const
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
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() {
|
|
@@ -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
|
-
|
|
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 {
|
|
@@ -54,7 +54,21 @@ function displayProjectPath(projectPath, targetPath) {
|
|
|
54
54
|
return relative;
|
|
55
55
|
return targetPath;
|
|
56
56
|
}
|
|
57
|
-
|
|
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(
|
|
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
|
|
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}](
|
|
106
|
-
`-
|
|
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.',
|