@aiwg/cli 2026.8.14 → 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.
@@ -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,
@@ -478,6 +478,7 @@ export function resolveRemoteProvider(remoteUrl) {
478
478
  * Defaults:
479
479
  * - `primary` defaults to "origin"
480
480
  * - `issue_tracker` defaults to `primary`
481
+ * - customer tracker fields remain unset unless explicitly configured
481
482
  * - `ci` defaults to `primary`
482
483
  * - `secondary` defaults to `[]`
483
484
  *
@@ -491,6 +492,9 @@ export function resolveRemotes(remotes) {
491
492
  issue_provider: remotes?.issue_provider,
492
493
  ci: remotes?.ci ?? primary,
493
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,
494
498
  transport: remotes?.transport,
495
499
  secondary: remotes?.secondary ?? [],
496
500
  };
@@ -151,7 +151,9 @@ 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'],
@@ -169,6 +171,7 @@ const BOOLEAN_FIELDS = new Set([
169
171
  ]);
170
172
  const STRING_ARRAY_FIELDS = new Set([
171
173
  'remotes.tracker_actor.forbid_actors',
174
+ 'remotes.customer_tracker_actor.forbid_actors',
172
175
  'command_log.scopes',
173
176
  'telemetry.skill_usage.scopes',
174
177
  ]);
@@ -592,6 +595,9 @@ For project-level config: aiwg config show --project [--json]
592
595
  const remotesView = {
593
596
  primary: { name: resolvedRemotes.primary, url: getUrl(resolvedRemotes.primary) },
594
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,
595
601
  ci: { name: resolvedRemotes.ci, url: getUrl(resolvedRemotes.ci) },
596
602
  secondary: resolvedRemotes.secondary.map((s) => ({
597
603
  ...s,
@@ -653,7 +659,10 @@ For project-level config: aiwg config show --project [--json]
653
659
  };
654
660
  console.log(fmt('Primary ', remotesView.primary));
655
661
  if (remotesView.issue_tracker.name !== remotesView.primary.name) {
656
- 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));
657
666
  }
658
667
  if (remotesView.ci.name !== remotesView.primary.name) {
659
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
@@ -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.
@@ -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.14",
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",