@aiwg/cli 2026.8.14 → 2026.8.16
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/index-builder.js +4 -64
- package/dist/src/artifacts/index-files.js +80 -0
- package/dist/src/artifacts/stats.js +22 -50
- package/dist/src/cli/handlers/setup.js +27 -0
- package/dist/src/config/aiwg-config.js +4 -0
- package/dist/src/config/cli.js +10 -1
- package/dist/src/config/workspace.js +26 -10
- package/dist/src/smiths/context-pipeline/aiwg-md.js +1 -1
- package/dist/src/smiths/context-pipeline/finalization.js +21 -3
- package/dist/src/tracker/capability-protocol.js +26 -3
- package/package.json +1 -1
|
@@ -17,30 +17,10 @@ import { DEFAULT_INDEX_EXTENSIONS, INDEX_EXTRACTOR_VERSION, INDEX_VERSION, INDEX
|
|
|
17
17
|
import { parseCitationSidecar, citationResultToEdges, buildRefToPathMap } from './citation-parser.js';
|
|
18
18
|
import { writeIndexFile, resolveIndexDir, loadGraphIndexFile } from './index-reader.js';
|
|
19
19
|
import { loadManifest, writeManifest, statMatches, makeEntry } from './checksum-manifest.js';
|
|
20
|
-
import { workspaceLinkedFiles } from '../smiths/context-pipeline/workspace-context.js';
|
|
21
20
|
import { normalizeOperationalState } from './operational-state.js';
|
|
22
21
|
import { DEFAULT_PROJECT_AIWG_DIR, resolveProjectAiwgDir, } from '../config/project-artifacts.js';
|
|
23
22
|
import { normalizeStateTransferProjection } from './state-transfer.js';
|
|
24
|
-
|
|
25
|
-
const relative = path.relative(parent, child);
|
|
26
|
-
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
27
|
-
}
|
|
28
|
-
function toPosixPath(value) {
|
|
29
|
-
return value.split(path.sep).join('/');
|
|
30
|
-
}
|
|
31
|
-
function indexPathFor(cwd, fullPath, graph) {
|
|
32
|
-
if (!graph || graph === 'project') {
|
|
33
|
-
const artifactRoot = resolveProjectAiwgDir(cwd);
|
|
34
|
-
if (pathContains(artifactRoot, fullPath)) {
|
|
35
|
-
const relative = toPosixPath(path.relative(artifactRoot, fullPath));
|
|
36
|
-
return relative ? `${DEFAULT_PROJECT_AIWG_DIR}/${relative}` : DEFAULT_PROJECT_AIWG_DIR;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const rel = path.relative(cwd, fullPath);
|
|
40
|
-
if (!rel.startsWith('..') && !path.isAbsolute(rel))
|
|
41
|
-
return toPosixPath(rel);
|
|
42
|
-
return fullPath;
|
|
43
|
-
}
|
|
23
|
+
import { collectGraphIndexFiles, findArtifactFiles, indexPathFor } from './index-files.js';
|
|
44
24
|
function absoluteEntryPath(cwd, entryPath, graph) {
|
|
45
25
|
if ((!graph || graph === 'project') && entryPath.startsWith(`${DEFAULT_PROJECT_AIWG_DIR}/`)) {
|
|
46
26
|
return path.join(resolveProjectAiwgDir(cwd), entryPath.slice(DEFAULT_PROJECT_AIWG_DIR.length + 1));
|
|
@@ -587,31 +567,6 @@ function applyMetadataSupplements(entries, supplements, cwd) {
|
|
|
587
567
|
}
|
|
588
568
|
}
|
|
589
569
|
}
|
|
590
|
-
/**
|
|
591
|
-
* Recursively find all indexable files under a directory
|
|
592
|
-
*/
|
|
593
|
-
function findArtifactFiles(dir, extensions = [...DEFAULT_INDEX_EXTENSIONS]) {
|
|
594
|
-
const results = [];
|
|
595
|
-
if (!fs.existsSync(dir))
|
|
596
|
-
return results;
|
|
597
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
598
|
-
for (const entry of entries) {
|
|
599
|
-
const fullPath = path.join(dir, entry.name);
|
|
600
|
-
if (entry.isSymbolicLink() && !fs.existsSync(fullPath)) {
|
|
601
|
-
continue;
|
|
602
|
-
}
|
|
603
|
-
if (entry.isDirectory()) {
|
|
604
|
-
// Skip hidden dirs and .index
|
|
605
|
-
if (entry.name.startsWith('.'))
|
|
606
|
-
continue;
|
|
607
|
-
results.push(...findArtifactFiles(fullPath, extensions));
|
|
608
|
-
}
|
|
609
|
-
else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
610
|
-
results.push(fullPath);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return results;
|
|
614
|
-
}
|
|
615
570
|
/**
|
|
616
571
|
* Build the artifact index
|
|
617
572
|
*/
|
|
@@ -798,24 +753,9 @@ export async function buildIndex(cwd, options = {}) {
|
|
|
798
753
|
pruned: 0,
|
|
799
754
|
};
|
|
800
755
|
// Collect files from all scan directories
|
|
801
|
-
const files =
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
// WORKSPACE.md is the root of the project context graph. Index it and its
|
|
806
|
-
// local Markdown-linked nodes without copying them into provider trees.
|
|
807
|
-
if (!scope && (!graph || graph === 'project')) {
|
|
808
|
-
const workspacePath = path.join(cwd, 'WORKSPACE.md');
|
|
809
|
-
const contextFiles = [
|
|
810
|
-
...(fs.existsSync(workspacePath) ? [workspacePath] : []),
|
|
811
|
-
...await workspaceLinkedFiles(cwd),
|
|
812
|
-
];
|
|
813
|
-
for (const contextFile of contextFiles) {
|
|
814
|
-
if (fileExtensions.some((extension) => contextFile.endsWith(extension)) && !files.includes(contextFile)) {
|
|
815
|
-
files.push(contextFile);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
756
|
+
const files = scope
|
|
757
|
+
? existingDirs.flatMap(dir => findArtifactFiles(dir, fileExtensions))
|
|
758
|
+
: await collectGraphIndexFiles(cwd, graph);
|
|
819
759
|
const entries = {};
|
|
820
760
|
const tagIndex = {};
|
|
821
761
|
const depGraph = {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared artifact source-file enumeration.
|
|
3
|
+
*
|
|
4
|
+
* Index builds and coverage reporting must use the same file set so project
|
|
5
|
+
* context files cannot inflate the indexed count beyond the reported total.
|
|
6
|
+
*
|
|
7
|
+
* @implements jmagly/aiwg#146
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { DEFAULT_INDEX_EXTENSIONS, GRAPH_CONFIGS, resolveGraphScanDir, } from './types.js';
|
|
12
|
+
import { DEFAULT_PROJECT_AIWG_DIR, resolveProjectAiwgDir, } from '../config/project-artifacts.js';
|
|
13
|
+
import { workspaceLinkedFiles } from '../smiths/context-pipeline/workspace-context.js';
|
|
14
|
+
function pathContains(parent, child) {
|
|
15
|
+
const relative = path.relative(parent, child);
|
|
16
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
17
|
+
}
|
|
18
|
+
function toPosixPath(value) {
|
|
19
|
+
return value.split(path.sep).join('/');
|
|
20
|
+
}
|
|
21
|
+
export function indexPathFor(cwd, fullPath, graph) {
|
|
22
|
+
if (!graph || graph === 'project') {
|
|
23
|
+
const artifactRoot = resolveProjectAiwgDir(cwd);
|
|
24
|
+
if (pathContains(artifactRoot, fullPath)) {
|
|
25
|
+
const relative = toPosixPath(path.relative(artifactRoot, fullPath));
|
|
26
|
+
return relative ? `${DEFAULT_PROJECT_AIWG_DIR}/${relative}` : DEFAULT_PROJECT_AIWG_DIR;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const relative = path.relative(cwd, fullPath);
|
|
30
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
31
|
+
return toPosixPath(relative);
|
|
32
|
+
return fullPath;
|
|
33
|
+
}
|
|
34
|
+
/** Recursively find indexable files, excluding hidden directories such as .index. */
|
|
35
|
+
export function findArtifactFiles(dir, extensions = DEFAULT_INDEX_EXTENSIONS) {
|
|
36
|
+
const results = [];
|
|
37
|
+
if (!fs.existsSync(dir))
|
|
38
|
+
return results;
|
|
39
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const fullPath = path.join(dir, entry.name);
|
|
42
|
+
if (entry.isSymbolicLink() && !fs.existsSync(fullPath))
|
|
43
|
+
continue;
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
if (entry.name.startsWith('.'))
|
|
46
|
+
continue;
|
|
47
|
+
results.push(...findArtifactFiles(fullPath, extensions));
|
|
48
|
+
}
|
|
49
|
+
else if (extensions.some(extension => entry.name.endsWith(extension))) {
|
|
50
|
+
results.push(fullPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
/** Return the exact current source-file set used by a standard graph build. */
|
|
56
|
+
export async function collectGraphIndexFiles(cwd, graph) {
|
|
57
|
+
const config = graph ? GRAPH_CONFIGS[graph] : undefined;
|
|
58
|
+
const scanDirs = config
|
|
59
|
+
? config.scanDirs.map(directory => resolveGraphScanDir(cwd, directory))
|
|
60
|
+
: [resolveProjectAiwgDir(cwd)];
|
|
61
|
+
const extensions = config?.extensions ?? [...DEFAULT_INDEX_EXTENSIONS];
|
|
62
|
+
const files = new Set();
|
|
63
|
+
for (const scanDir of scanDirs) {
|
|
64
|
+
for (const file of findArtifactFiles(scanDir, extensions))
|
|
65
|
+
files.add(file);
|
|
66
|
+
}
|
|
67
|
+
if (!graph || graph === 'project') {
|
|
68
|
+
const workspacePath = path.join(cwd, 'WORKSPACE.md');
|
|
69
|
+
const contextFiles = [
|
|
70
|
+
...(fs.existsSync(workspacePath) ? [workspacePath] : []),
|
|
71
|
+
...await workspaceLinkedFiles(cwd),
|
|
72
|
+
];
|
|
73
|
+
for (const contextFile of contextFiles) {
|
|
74
|
+
if (extensions.some(extension => contextFile.endsWith(extension)))
|
|
75
|
+
files.add(contextFile);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...files];
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=index-files.js.map
|
|
@@ -7,40 +7,23 @@
|
|
|
7
7
|
* @source @src/artifacts/types.ts
|
|
8
8
|
* @tests @test/unit/artifacts/stats.test.ts
|
|
9
9
|
*/
|
|
10
|
-
import
|
|
11
|
-
import path from 'path';
|
|
12
|
-
import { GRAPH_CONFIGS, loadUserGraphConfigs, resolveGraphScanDir } from './types.js';
|
|
10
|
+
import { GRAPH_CONFIGS, loadUserGraphConfigs } from './types.js';
|
|
13
11
|
import { loadIndexStats, loadGraphIndexFile } from './index-reader.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const full = path.join(dir, entry.name);
|
|
30
|
-
if (entry.isDirectory()) {
|
|
31
|
-
if (entry.name.startsWith('.'))
|
|
32
|
-
continue; // Skip .index, etc.
|
|
33
|
-
walk(full);
|
|
34
|
-
}
|
|
35
|
-
else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
36
|
-
count++;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
for (const dir of scanDirs) {
|
|
41
|
-
walk(dir);
|
|
42
|
-
}
|
|
43
|
-
return count;
|
|
12
|
+
import { collectGraphIndexFiles, indexPathFor } from './index-files.js';
|
|
13
|
+
/** Calculate coverage over the same current file set used by the index builder. */
|
|
14
|
+
async function calculateCoverage(cwd, stats, graphType) {
|
|
15
|
+
const sourcePaths = new Set((await collectGraphIndexFiles(cwd, graphType))
|
|
16
|
+
.map(file => indexPathFor(cwd, file, graphType)));
|
|
17
|
+
const index = loadGraphIndexFile(cwd, 'metadata.json', graphType);
|
|
18
|
+
const indexed = index
|
|
19
|
+
? Object.keys(index.entries).filter(entryPath => sourcePaths.has(entryPath)).length
|
|
20
|
+
: Math.min(stats.totalArtifacts, sourcePaths.size);
|
|
21
|
+
const totalFiles = sourcePaths.size;
|
|
22
|
+
return {
|
|
23
|
+
indexed,
|
|
24
|
+
totalFiles,
|
|
25
|
+
percentage: totalFiles > 0 ? Math.round((indexed / totalFiles) * 100) : 100,
|
|
26
|
+
};
|
|
44
27
|
}
|
|
45
28
|
/**
|
|
46
29
|
* Show artifact index statistics
|
|
@@ -84,14 +67,10 @@ export async function showStats(cwd, options = {}) {
|
|
|
84
67
|
// JSON mode: aggregate all graphs into one response
|
|
85
68
|
const combined = {};
|
|
86
69
|
for (const { type, stats: s } of availableGraphs) {
|
|
87
|
-
const
|
|
70
|
+
const coverage = await calculateCoverage(cwd, s, type);
|
|
88
71
|
combined[type] = {
|
|
89
72
|
...s,
|
|
90
|
-
coverage
|
|
91
|
-
indexed: s.totalArtifacts,
|
|
92
|
-
totalFiles,
|
|
93
|
-
percentage: totalFiles > 0 ? Math.round((s.totalArtifacts / totalFiles) * 100) : 100,
|
|
94
|
-
},
|
|
73
|
+
coverage,
|
|
95
74
|
};
|
|
96
75
|
}
|
|
97
76
|
console.log(JSON.stringify(combined, null, 2));
|
|
@@ -108,14 +87,10 @@ export async function showStats(cwd, options = {}) {
|
|
|
108
87
|
*/
|
|
109
88
|
async function renderStats(cwd, stats, options, graphType) {
|
|
110
89
|
if (options.json) {
|
|
111
|
-
const
|
|
90
|
+
const coverage = await calculateCoverage(cwd, stats, graphType);
|
|
112
91
|
console.log(JSON.stringify({
|
|
113
92
|
...stats,
|
|
114
|
-
coverage
|
|
115
|
-
indexed: stats.totalArtifacts,
|
|
116
|
-
totalFiles,
|
|
117
|
-
percentage: totalFiles > 0 ? Math.round((stats.totalArtifacts / totalFiles) * 100) : 100,
|
|
118
|
-
},
|
|
93
|
+
coverage,
|
|
119
94
|
}, null, 2));
|
|
120
95
|
return;
|
|
121
96
|
}
|
|
@@ -167,11 +142,8 @@ async function renderStats(cwd, stats, options, graphType) {
|
|
|
167
142
|
}
|
|
168
143
|
console.log('');
|
|
169
144
|
// Coverage
|
|
170
|
-
const
|
|
171
|
-
const coverage = totalFiles > 0
|
|
172
|
-
? Math.round((stats.totalArtifacts / totalFiles) * 100)
|
|
173
|
-
: 100;
|
|
145
|
+
const coverage = await calculateCoverage(cwd, stats, graphType);
|
|
174
146
|
console.log('Index Health:');
|
|
175
|
-
console.log(` Coverage: ${
|
|
147
|
+
console.log(` Coverage: ${coverage.indexed}/${coverage.totalFiles} artifacts indexed (${coverage.percentage}%)`);
|
|
176
148
|
}
|
|
177
149
|
//# sourceMappingURL=stats.js.map
|
|
@@ -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
|
};
|
package/dist/src/config/cli.js
CHANGED
|
@@ -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('
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
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.',
|