@aiwg/cli 2026.8.18 → 2026.8.19
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 +63 -12
- package/dist/src/artifacts/index-files.js +26 -5
- package/dist/src/artifacts/query-engine.js +67 -67
- package/dist/src/artifacts/stats.js +6 -2
- package/dist/src/artifacts/types.js +1 -1
- package/dist/src/cli/handlers/installation.js +1 -1
- package/dist/src/cli/handlers/refresh.js +6 -4
- package/dist/src/cli/handlers/use.js +19 -4
- package/dist/src/installation/manager-command.mjs +31 -0
- package/dist/src/installation/manager.mjs +21 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +8 -5
- package/dist/src/smiths/context-pipeline/line-endings.js +12 -0
- package/dist/src/smiths/context-pipeline/managed-hook.js +8 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +3 -1
- package/dist/src/update/service.mjs +3 -1
- package/package.json +1 -1
- package/tools/agents/deploy-agents.mjs +28 -8
- package/tools/agents/providers/base.mjs +5 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Artifact Index Builder
|
|
3
3
|
*
|
|
4
4
|
* Scans .aiwg/ directories, extracts metadata from artifact frontmatter,
|
|
5
|
-
* computes checksums, extracts @-mention dependencies, and builds a
|
|
5
|
+
* computes checksums, extracts @-mention and Markdown-link dependencies, and builds a
|
|
6
6
|
* structured index at .aiwg/.index/.
|
|
7
7
|
*
|
|
8
8
|
* @implements #415
|
|
@@ -68,6 +68,57 @@ export function extractMentions(content) {
|
|
|
68
68
|
}
|
|
69
69
|
return Array.from(mentions);
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Extract relative Markdown links that may resolve to graph-local artifacts.
|
|
73
|
+
*
|
|
74
|
+
* External URLs, absolute paths, and anchor-only links are intentionally absent
|
|
75
|
+
* from the accepted pattern. Resolution still happens later against indexed
|
|
76
|
+
* nodes, so a parsed link outside the active graph cannot create an edge.
|
|
77
|
+
*/
|
|
78
|
+
export function extractMarkdownLinks(content) {
|
|
79
|
+
const links = new Set();
|
|
80
|
+
const pattern = /(!?)\[[^\]]+\]\((\.\/?[^)#\s]+)(?:#[^)]+)?\)/g;
|
|
81
|
+
let match;
|
|
82
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
83
|
+
if (match[1] === '!')
|
|
84
|
+
continue;
|
|
85
|
+
links.add(match[2]);
|
|
86
|
+
}
|
|
87
|
+
return Array.from(links);
|
|
88
|
+
}
|
|
89
|
+
function resolveMarkdownLinkDependency(cwd, sourcePath, rawLink, entries, graph) {
|
|
90
|
+
const target = rawLink.split('#')[0]?.trim();
|
|
91
|
+
if (!target)
|
|
92
|
+
return null;
|
|
93
|
+
const sourceFullPath = absoluteEntryPath(cwd, sourcePath, graph);
|
|
94
|
+
const targetFullPath = path.resolve(path.dirname(sourceFullPath), target);
|
|
95
|
+
let stat;
|
|
96
|
+
try {
|
|
97
|
+
stat = fs.statSync(targetFullPath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (!stat.isFile())
|
|
103
|
+
return null;
|
|
104
|
+
const indexedPath = indexPathFor(cwd, targetFullPath, graph);
|
|
105
|
+
return entries[indexedPath] ? indexedPath : null;
|
|
106
|
+
}
|
|
107
|
+
function addDependencyEdge(depGraph, entries, sourcePath, targetPath, type) {
|
|
108
|
+
if (sourcePath === targetPath)
|
|
109
|
+
return false;
|
|
110
|
+
if (!depGraph[sourcePath])
|
|
111
|
+
depGraph[sourcePath] = { upstream: [], downstream: [] };
|
|
112
|
+
if (!depGraph[targetPath])
|
|
113
|
+
depGraph[targetPath] = { upstream: [], downstream: [] };
|
|
114
|
+
if (depGraph[sourcePath].upstream.some(edge => edge.path === targetPath))
|
|
115
|
+
return false;
|
|
116
|
+
depGraph[sourcePath].upstream.push({ path: targetPath, type });
|
|
117
|
+
depGraph[targetPath].downstream.push({ path: sourcePath, type });
|
|
118
|
+
if (entries[targetPath])
|
|
119
|
+
entries[targetPath].dependents.push(sourcePath);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
71
122
|
/**
|
|
72
123
|
* Extract title from content (first # heading or frontmatter title)
|
|
73
124
|
*/
|
|
@@ -846,6 +897,7 @@ export async function buildIndex(cwd, options = {}) {
|
|
|
846
897
|
const tags = flow ? flow.tags : (Array.isArray(data.tags) ? data.tags.map(String) : []);
|
|
847
898
|
const summary = flow?.description ?? schemaDoc?.capability ?? runbook?.capability ?? extractSummary(data, body);
|
|
848
899
|
const dependencies = extractMentions(content);
|
|
900
|
+
const markdownLinks = extractMarkdownLinks(content);
|
|
849
901
|
// Discovery metadata (#1214, #1540, #1792) — meaningful for operational
|
|
850
902
|
// AIWG artifact kinds. Kept undefined on document types so the index file
|
|
851
903
|
// stays small for the common case.
|
|
@@ -879,6 +931,7 @@ export async function buildIndex(cwd, options = {}) {
|
|
|
879
931
|
checksum,
|
|
880
932
|
summary,
|
|
881
933
|
dependencies,
|
|
934
|
+
...(markdownLinks.length > 0 ? { markdownLinks } : {}),
|
|
882
935
|
dependents: [], // Computed after all entries are processed
|
|
883
936
|
...(name ? { name } : {}),
|
|
884
937
|
...(triggers && triggers.length > 0 ? { triggers } : {}),
|
|
@@ -915,6 +968,7 @@ export async function buildIndex(cwd, options = {}) {
|
|
|
915
968
|
}
|
|
916
969
|
}
|
|
917
970
|
// Build dependency graph and compute dependents
|
|
971
|
+
let markdownLinkEdgeCount = 0;
|
|
918
972
|
for (const entry of Object.values(entries)) {
|
|
919
973
|
if (!depGraph[entry.path]) {
|
|
920
974
|
depGraph[entry.path] = { upstream: [], downstream: [] };
|
|
@@ -923,17 +977,13 @@ export async function buildIndex(cwd, options = {}) {
|
|
|
923
977
|
// Normalize: check if referenced path exists in the index
|
|
924
978
|
const normalizedDep = Object.keys(entries).find(p => p === dep || p.endsWith(dep));
|
|
925
979
|
if (normalizedDep && normalizedDep !== entry.path) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
// Also update the dependents field on the target entry
|
|
934
|
-
if (entries[normalizedDep]) {
|
|
935
|
-
entries[normalizedDep].dependents.push(entry.path);
|
|
936
|
-
}
|
|
980
|
+
addDependencyEdge(depGraph, entries, entry.path, normalizedDep, 'depends-on');
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
for (const link of entry.markdownLinks ?? []) {
|
|
984
|
+
const normalizedDep = resolveMarkdownLinkDependency(cwd, entry.path, link, entries, graph);
|
|
985
|
+
if (normalizedDep && addDependencyEdge(depGraph, entries, entry.path, normalizedDep, 'markdown-link')) {
|
|
986
|
+
markdownLinkEdgeCount++;
|
|
937
987
|
}
|
|
938
988
|
}
|
|
939
989
|
}
|
|
@@ -1069,6 +1119,7 @@ export async function buildIndex(cwd, options = {}) {
|
|
|
1069
1119
|
tagDistribution: tagDist,
|
|
1070
1120
|
graphMetrics: {
|
|
1071
1121
|
totalEdges,
|
|
1122
|
+
markdownLinkEdges: markdownLinkEdgeCount,
|
|
1072
1123
|
...(citationMetrics ? {
|
|
1073
1124
|
canonicalEdges: citationMetrics.canonicalEdges,
|
|
1074
1125
|
outgoingDeclarations: citationMetrics.outgoingDeclarations,
|
|
@@ -31,22 +31,40 @@ export function indexPathFor(cwd, fullPath, graph) {
|
|
|
31
31
|
return toPosixPath(relative);
|
|
32
32
|
return fullPath;
|
|
33
33
|
}
|
|
34
|
-
/** Recursively find indexable files, excluding hidden directories such as .index. */
|
|
35
34
|
export function findArtifactFiles(dir, extensions = DEFAULT_INDEX_EXTENSIONS) {
|
|
35
|
+
return walkArtifactFiles(dir, extensions, new Set());
|
|
36
|
+
}
|
|
37
|
+
/** Recursively find indexable files, excluding hidden directories such as .index. */
|
|
38
|
+
function walkArtifactFiles(dir, extensions, seenRealDirs) {
|
|
36
39
|
const results = [];
|
|
37
40
|
if (!fs.existsSync(dir))
|
|
38
41
|
return results;
|
|
42
|
+
let realDir;
|
|
43
|
+
try {
|
|
44
|
+
realDir = fs.realpathSync(dir);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
49
|
+
if (seenRealDirs.has(realDir))
|
|
50
|
+
return results;
|
|
51
|
+
seenRealDirs.add(realDir);
|
|
39
52
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
53
|
for (const entry of entries) {
|
|
41
54
|
const fullPath = path.join(dir, entry.name);
|
|
42
|
-
|
|
55
|
+
let stat;
|
|
56
|
+
try {
|
|
57
|
+
stat = fs.statSync(fullPath);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
43
60
|
continue;
|
|
44
|
-
|
|
61
|
+
}
|
|
62
|
+
if (stat.isDirectory()) {
|
|
45
63
|
if (entry.name.startsWith('.'))
|
|
46
64
|
continue;
|
|
47
|
-
results.push(...
|
|
65
|
+
results.push(...walkArtifactFiles(fullPath, extensions, seenRealDirs));
|
|
48
66
|
}
|
|
49
|
-
else if (extensions.some(extension => entry.name.endsWith(extension))) {
|
|
67
|
+
else if (stat.isFile() && extensions.some(extension => entry.name.endsWith(extension))) {
|
|
50
68
|
results.push(fullPath);
|
|
51
69
|
}
|
|
52
70
|
}
|
|
@@ -55,6 +73,9 @@ export function findArtifactFiles(dir, extensions = DEFAULT_INDEX_EXTENSIONS) {
|
|
|
55
73
|
/** Return the exact current source-file set used by a standard graph build. */
|
|
56
74
|
export async function collectGraphIndexFiles(cwd, graph) {
|
|
57
75
|
const config = graph ? GRAPH_CONFIGS[graph] : undefined;
|
|
76
|
+
if (graph && !config) {
|
|
77
|
+
throw new Error(`Unknown graph: ${graph}`);
|
|
78
|
+
}
|
|
58
79
|
const scanDirs = config
|
|
59
80
|
? config.scanDirs.map(directory => resolveGraphScanDir(cwd, directory))
|
|
60
81
|
: [resolveProjectAiwgDir(cwd)];
|
|
@@ -181,7 +181,7 @@ const SCORE_STOPWORDS = new Set([
|
|
|
181
181
|
'with', 'into', 'from', 'is', 'are', 'be', 'i', 'we', 'my',
|
|
182
182
|
// pronouns / determiners / fillers
|
|
183
183
|
'it', 'you', 'me', 'us', 'your', 'our', 'this', 'that', 'these', 'those',
|
|
184
|
-
'there', 'here', 'some', 'any', 'all', 'also', 'please', 'about',
|
|
184
|
+
'there', 'here', 'some', 'any', 'all', 'also', 'please', 'about', 'project',
|
|
185
185
|
// question words
|
|
186
186
|
'how', 'what', 'which', 'where', 'when', 'who', 'why',
|
|
187
187
|
// asking / request verbs ("find a skill that handles …")
|
|
@@ -191,6 +191,7 @@ const SCORE_STOPWORDS = new Set([
|
|
|
191
191
|
// AIWG meta-type nouns — zero discriminating signal in a discover query
|
|
192
192
|
'aiwg', 'skill', 'skills', 'agent', 'agents', 'command', 'commands',
|
|
193
193
|
'rule', 'rules', 'schema', 'schemas', 'flow', 'flows', 'workflow', 'workflows',
|
|
194
|
+
'template', 'templates',
|
|
194
195
|
]);
|
|
195
196
|
/**
|
|
196
197
|
* Tokenize a query phrase into lowercased keywords for multi-word
|
|
@@ -200,8 +201,18 @@ const SCORE_STOPWORDS = new Set([
|
|
|
200
201
|
function tokenize(text) {
|
|
201
202
|
return text
|
|
202
203
|
.toLowerCase()
|
|
203
|
-
.split(/[^a-z0-9
|
|
204
|
-
.filter(t => t.length > 1 && !SCORE_STOPWORDS.has(t))
|
|
204
|
+
.split(/[^a-z0-9]+/)
|
|
205
|
+
.filter(t => t.length > 1 && !SCORE_STOPWORDS.has(t))
|
|
206
|
+
.map(token => token.length > 4 && token.endsWith('s') && !token.endsWith('ss')
|
|
207
|
+
? token.slice(0, -1)
|
|
208
|
+
: token);
|
|
209
|
+
}
|
|
210
|
+
function matchedFieldTokens(queryTokens, field) {
|
|
211
|
+
const fieldTokens = new Set(tokenize(field));
|
|
212
|
+
return queryTokens.filter(token => fieldTokens.has(token));
|
|
213
|
+
}
|
|
214
|
+
function fieldContainsQuery(field, queryTokens) {
|
|
215
|
+
return containsTokenSequence(tokenize(field), queryTokens);
|
|
205
216
|
}
|
|
206
217
|
/**
|
|
207
218
|
* Score a metadata entry against a keyword query.
|
|
@@ -297,6 +308,7 @@ function scoreEntryDetailed(entry, text, opts = {}) {
|
|
|
297
308
|
const personaIdentitySuppressed = diagnoseFacetActivations(text).some((activation) => activation.facet === 'persona-identity' && activation.status === 'suppressed');
|
|
298
309
|
let score = 0;
|
|
299
310
|
const matches = [];
|
|
311
|
+
const creditedTokens = new Set();
|
|
300
312
|
const finish = (uncappedScore = score, cap = 1) => ({
|
|
301
313
|
score: Math.min(uncappedScore, cap),
|
|
302
314
|
diagnostic: {
|
|
@@ -311,6 +323,17 @@ function scoreEntryDetailed(entry, text, opts = {}) {
|
|
|
311
323
|
score += contribution;
|
|
312
324
|
matches.push({ ...match, contribution });
|
|
313
325
|
};
|
|
326
|
+
const addTokenMatch = (contributionPerToken, hits, match) => {
|
|
327
|
+
const newlyMatched = hits.filter(token => !creditedTokens.has(token));
|
|
328
|
+
if (newlyMatched.length === 0)
|
|
329
|
+
return;
|
|
330
|
+
newlyMatched.forEach(token => creditedTokens.add(token));
|
|
331
|
+
addMatch(contributionPerToken * newlyMatched.length, {
|
|
332
|
+
...match,
|
|
333
|
+
matched_tokens: newlyMatched,
|
|
334
|
+
query_token_coverage: tokens.length > 0 ? newlyMatched.length / tokens.length : 0,
|
|
335
|
+
});
|
|
336
|
+
};
|
|
314
337
|
// Exact-name floor (#1233) — if the query (normalized) exactly matches
|
|
315
338
|
// the entry's canonical name, this is the artifact the user is asking
|
|
316
339
|
// for and it must surface at the top regardless of how cluttered the
|
|
@@ -422,14 +445,12 @@ function scoreEntryDetailed(entry, text, opts = {}) {
|
|
|
422
445
|
});
|
|
423
446
|
}
|
|
424
447
|
else if (useMultiToken) {
|
|
425
|
-
const hits = tokens
|
|
448
|
+
const hits = matchedFieldTokens(tokens, trigger);
|
|
426
449
|
if (overlapOK(hits.length)) {
|
|
427
|
-
|
|
450
|
+
addTokenMatch(0.1 * 4, hits, {
|
|
428
451
|
field: 'trigger',
|
|
429
452
|
match: 'token-overlap',
|
|
430
453
|
value: trigger,
|
|
431
|
-
matched_tokens: hits,
|
|
432
|
-
query_token_coverage: hits.length / tokens.length,
|
|
433
454
|
});
|
|
434
455
|
}
|
|
435
456
|
}
|
|
@@ -437,7 +458,7 @@ function scoreEntryDetailed(entry, text, opts = {}) {
|
|
|
437
458
|
}
|
|
438
459
|
// Capability description (2x weight) — full phrase first, then tokens
|
|
439
460
|
if (capabilityLower) {
|
|
440
|
-
if (capabilityLower
|
|
461
|
+
if (fieldContainsQuery(capabilityLower, tokens)) {
|
|
441
462
|
addMatch(0.2 * 2, {
|
|
442
463
|
field: 'capability',
|
|
443
464
|
match: 'contained-phrase',
|
|
@@ -445,20 +466,18 @@ function scoreEntryDetailed(entry, text, opts = {}) {
|
|
|
445
466
|
});
|
|
446
467
|
}
|
|
447
468
|
else if (useMultiToken) {
|
|
448
|
-
const hits = tokens
|
|
469
|
+
const hits = matchedFieldTokens(tokens, capabilityLower);
|
|
449
470
|
if (overlapOK(hits.length)) {
|
|
450
|
-
|
|
471
|
+
addTokenMatch(0.1 * 2, hits, {
|
|
451
472
|
field: 'capability',
|
|
452
473
|
match: 'token-overlap',
|
|
453
474
|
value: entry.capability,
|
|
454
|
-
matched_tokens: hits,
|
|
455
|
-
query_token_coverage: hits.length / tokens.length,
|
|
456
475
|
});
|
|
457
476
|
}
|
|
458
477
|
}
|
|
459
478
|
}
|
|
460
479
|
// Title (3x weight)
|
|
461
|
-
if (titleLower
|
|
480
|
+
if (fieldContainsQuery(titleLower, tokens)) {
|
|
462
481
|
addMatch(0.3 * 3, {
|
|
463
482
|
field: 'title',
|
|
464
483
|
match: titleLower === lower ? 'exact' : 'contained-phrase',
|
|
@@ -469,93 +488,83 @@ function scoreEntryDetailed(entry, text, opts = {}) {
|
|
|
469
488
|
}
|
|
470
489
|
}
|
|
471
490
|
else if (useMultiToken) {
|
|
472
|
-
const hits = tokens
|
|
491
|
+
const hits = matchedFieldTokens(tokens, titleLower);
|
|
473
492
|
if (overlapOK(hits.length)) {
|
|
474
|
-
|
|
493
|
+
addTokenMatch(0.08 * 3, hits, {
|
|
475
494
|
field: 'title',
|
|
476
495
|
match: 'token-overlap',
|
|
477
496
|
value: entry.title,
|
|
478
|
-
matched_tokens: hits,
|
|
479
|
-
query_token_coverage: hits.length / tokens.length,
|
|
480
497
|
});
|
|
481
498
|
}
|
|
482
499
|
}
|
|
483
500
|
// Tags (2x weight)
|
|
484
501
|
for (const tag of tagsLower) {
|
|
485
|
-
if (tag
|
|
502
|
+
if (fieldContainsQuery(tag, tokens)) {
|
|
486
503
|
addMatch(0.2 * 2, { field: 'tag', match: 'contained-phrase', value: tag });
|
|
487
504
|
}
|
|
488
505
|
else if (useMultiToken) {
|
|
489
|
-
const hits = tokens
|
|
506
|
+
const hits = matchedFieldTokens(tokens, tag);
|
|
490
507
|
if (overlapOK(hits.length)) {
|
|
491
|
-
|
|
508
|
+
addTokenMatch(0.05 * 2, hits, {
|
|
492
509
|
field: 'tag',
|
|
493
510
|
match: 'token-overlap',
|
|
494
511
|
value: tag,
|
|
495
|
-
matched_tokens: hits,
|
|
496
|
-
query_token_coverage: hits.length / tokens.length,
|
|
497
512
|
});
|
|
498
513
|
}
|
|
499
514
|
}
|
|
500
515
|
}
|
|
501
516
|
// Structure-aware language terms (1.5x weight). These are deliberately
|
|
502
517
|
// below declared triggers/capabilities but above generic body summaries.
|
|
503
|
-
if (searchTermsLower
|
|
518
|
+
if (fieldContainsQuery(searchTermsLower, tokens)) {
|
|
504
519
|
addMatch(0.18 * 1.5, { field: 'search_terms', match: 'contained-phrase' });
|
|
505
520
|
}
|
|
506
521
|
else if (useMultiToken) {
|
|
507
|
-
const hits = tokens
|
|
522
|
+
const hits = matchedFieldTokens(tokens, searchTermsLower);
|
|
508
523
|
if (overlapOK(hits.length)) {
|
|
509
|
-
|
|
524
|
+
addTokenMatch(0.06 * 1.5, hits, {
|
|
510
525
|
field: 'search_terms',
|
|
511
526
|
match: 'token-overlap',
|
|
512
|
-
matched_tokens: hits,
|
|
513
|
-
query_token_coverage: hits.length / tokens.length,
|
|
514
527
|
});
|
|
515
528
|
}
|
|
516
529
|
}
|
|
517
530
|
// Exact declarative kind and physical source classification are compact,
|
|
518
531
|
// useful routing signals (e.g. FlowPlaybook vs OpsInventory; runbook that
|
|
519
532
|
// originated under templates/).
|
|
520
|
-
if (kindLower
|
|
533
|
+
if (fieldContainsQuery(kindLower, tokens)) {
|
|
521
534
|
addMatch(0.15, { field: 'kind', match: 'contained-phrase', value: entry.kind });
|
|
522
535
|
}
|
|
523
|
-
if (sourceTypeLower
|
|
536
|
+
if (fieldContainsQuery(sourceTypeLower, tokens)) {
|
|
524
537
|
addMatch(0.08, { field: 'source_type', match: 'contained-phrase', value: entry.sourceType });
|
|
525
538
|
}
|
|
526
539
|
// Summary (1x weight)
|
|
527
|
-
if (summaryLower
|
|
540
|
+
if (fieldContainsQuery(summaryLower, tokens)) {
|
|
528
541
|
addMatch(0.15, { field: 'summary', match: 'contained-phrase' });
|
|
529
542
|
}
|
|
530
543
|
else if (useMultiToken) {
|
|
531
|
-
const hits = tokens
|
|
544
|
+
const hits = matchedFieldTokens(tokens, summaryLower);
|
|
532
545
|
if (overlapOK(hits.length)) {
|
|
533
|
-
|
|
546
|
+
addTokenMatch(0.04, hits, {
|
|
534
547
|
field: 'summary',
|
|
535
548
|
match: 'token-overlap',
|
|
536
|
-
matched_tokens: hits,
|
|
537
|
-
query_token_coverage: hits.length / tokens.length,
|
|
538
549
|
});
|
|
539
550
|
}
|
|
540
551
|
}
|
|
541
552
|
// Path (0.5x weight)
|
|
542
|
-
if (pathLower
|
|
553
|
+
if (fieldContainsQuery(pathLower, tokens)) {
|
|
543
554
|
addMatch(0.1, { field: 'path', match: 'contained-phrase', value: entry.path });
|
|
544
555
|
}
|
|
545
556
|
else if (useMultiToken) {
|
|
546
|
-
const hits = tokens
|
|
557
|
+
const hits = matchedFieldTokens(tokens, pathLower);
|
|
547
558
|
if (overlapOK(hits.length)) {
|
|
548
|
-
|
|
559
|
+
addTokenMatch(0.03, hits, {
|
|
549
560
|
field: 'path',
|
|
550
561
|
match: 'token-overlap',
|
|
551
562
|
value: entry.path,
|
|
552
|
-
matched_tokens: hits,
|
|
553
|
-
query_token_coverage: hits.length / tokens.length,
|
|
554
563
|
});
|
|
555
564
|
}
|
|
556
565
|
}
|
|
557
566
|
// Type (0.5x weight)
|
|
558
|
-
if (typeLower
|
|
567
|
+
if (fieldContainsQuery(typeLower, tokens)) {
|
|
559
568
|
addMatch(0.1, { field: 'type', match: 'contained-phrase', value: entry.type });
|
|
560
569
|
}
|
|
561
570
|
return finish();
|
|
@@ -1078,36 +1087,27 @@ export async function discoverCapability(cwd, params) {
|
|
|
1078
1087
|
// lexical ranking so canonical domain phrases rank their owning capability
|
|
1079
1088
|
// top-K instead of being out-scored by artifacts that merely mention the
|
|
1080
1089
|
// word. Facet activation can also rescue an otherwise-empty strict pass.
|
|
1081
|
-
|
|
1082
|
-
//
|
|
1083
|
-
// (
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const relaxedFull = candidates
|
|
1098
|
-
.map(entry => {
|
|
1099
|
-
const detailed = scoreEntryDetailed(entry, params.phrase, { relaxOverlap: true });
|
|
1090
|
+
// #154 — a strict result anywhere in the corpus must not suppress relevant
|
|
1091
|
+
// partial matches for a natural-language query. Score the relaxed pass on
|
|
1092
|
+
// matched terms (unmatched terms do not divide the score), apply a noise
|
|
1093
|
+
// floor, and union it with strict matches before ranking. Word-boundary
|
|
1094
|
+
// token matching keeps this from resurrecting substring noise such as UX in
|
|
1095
|
+
// Linux.
|
|
1096
|
+
const RELAXED_MIN_SCORE = 0.02;
|
|
1097
|
+
const strictPaths = new Set(strictScored.map(result => result.entry.path));
|
|
1098
|
+
const combinedByPath = new Map(strictScored.map(result => [result.entry.path, result]));
|
|
1099
|
+
for (const entry of candidates) {
|
|
1100
|
+
const detailed = scoreEntryDetailed(entry, params.phrase, { relaxOverlap: true });
|
|
1101
|
+
if (detailed.score < RELAXED_MIN_SCORE)
|
|
1102
|
+
continue;
|
|
1103
|
+
const existing = combinedByPath.get(entry.path);
|
|
1104
|
+
if (!existing || detailed.score > existing.score) {
|
|
1105
|
+
combinedByPath.set(entry.path, { entry, score: detailed.score });
|
|
1100
1106
|
lexicalDiagnostics.set(entry.path, detailed.diagnostic);
|
|
1101
|
-
return { entry, score: detailed.score };
|
|
1102
|
-
})
|
|
1103
|
-
.filter(r => r.score >= RELAXED_MIN_SCORE)
|
|
1104
|
-
.sort(compareDiscoverResults);
|
|
1105
|
-
const relaxedScored = dedupeDiscoverResults(await applyFacetFusion(relaxedFull, candidates, params.phrase)).slice(0, limit);
|
|
1106
|
-
if (relaxedScored.length > 0) {
|
|
1107
|
-
scored = relaxedScored;
|
|
1108
|
-
relaxed = true;
|
|
1109
1107
|
}
|
|
1110
1108
|
}
|
|
1109
|
+
const scored = dedupeDiscoverResults(await applyFacetFusion(Array.from(combinedByPath.values()).sort(compareDiscoverResults), candidates, params.phrase)).slice(0, limit);
|
|
1110
|
+
const relaxed = scored.some(result => !strictPaths.has(result.entry.path));
|
|
1111
1111
|
const queryTimeMs = Date.now() - startTime;
|
|
1112
1112
|
/**
|
|
1113
1113
|
* Resolve a stored framework-graph path to an absolute AIWG_ROOT
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @source @src/artifacts/types.ts
|
|
8
8
|
* @tests @test/unit/artifacts/stats.test.ts
|
|
9
9
|
*/
|
|
10
|
-
import { GRAPH_CONFIGS, loadUserGraphConfigs } from './types.js';
|
|
10
|
+
import { GRAPH_CONFIGS, loadGlobalGraphConfigs, loadUserGraphConfigs } from './types.js';
|
|
11
11
|
import { loadIndexStats, loadGraphIndexFile } from './index-reader.js';
|
|
12
12
|
import { collectGraphIndexFiles, indexPathFor } from './index-files.js';
|
|
13
13
|
/** Calculate coverage over the same current file set used by the index builder. */
|
|
@@ -30,6 +30,8 @@ async function calculateCoverage(cwd, stats, graphType) {
|
|
|
30
30
|
*/
|
|
31
31
|
export async function showStats(cwd, options = {}) {
|
|
32
32
|
const { graph } = options;
|
|
33
|
+
loadUserGraphConfigs(cwd);
|
|
34
|
+
loadGlobalGraphConfigs();
|
|
33
35
|
if (graph) {
|
|
34
36
|
// Single graph mode
|
|
35
37
|
const stats = loadGraphIndexFile(cwd, 'stats.json', graph);
|
|
@@ -42,7 +44,6 @@ export async function showStats(cwd, options = {}) {
|
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
46
|
// No graph specified: show all graphs with defaultBuild=true
|
|
45
|
-
loadUserGraphConfigs(cwd);
|
|
46
47
|
const graphTypes = Object.entries(GRAPH_CONFIGS)
|
|
47
48
|
.filter(([, config]) => config.defaultBuild)
|
|
48
49
|
.map(([name]) => name);
|
|
@@ -128,6 +129,9 @@ async function renderStats(cwd, stats, options, graphType) {
|
|
|
128
129
|
// Dependency graph
|
|
129
130
|
console.log('Dependency Graph:');
|
|
130
131
|
console.log(` Total edges: ${stats.graphMetrics.totalEdges}`);
|
|
132
|
+
if (stats.graphMetrics.markdownLinkEdges !== undefined) {
|
|
133
|
+
console.log(` Markdown link edges:${String(stats.graphMetrics.markdownLinkEdges).padStart(3)}`);
|
|
134
|
+
}
|
|
131
135
|
if (stats.graphMetrics.canonicalEdges !== undefined) {
|
|
132
136
|
console.log(` Canonical edges: ${stats.graphMetrics.canonicalEdges}`);
|
|
133
137
|
console.log(` Outgoing declares: ${stats.graphMetrics.outgoingDeclarations}`);
|
|
@@ -104,7 +104,7 @@ export const INDEX_VERSION = '1.0.0';
|
|
|
104
104
|
* making the serialized index schema incompatible; a mismatch simply forces a
|
|
105
105
|
* one-time content re-extraction during the next incremental build.
|
|
106
106
|
*/
|
|
107
|
-
export const INDEX_EXTRACTOR_VERSION = '2026.
|
|
107
|
+
export const INDEX_EXTRACTOR_VERSION = '2026.08.24.1';
|
|
108
108
|
/**
|
|
109
109
|
* Built-in graph definitions
|
|
110
110
|
*/
|
|
@@ -45,7 +45,7 @@ export const installationHandler = {
|
|
|
45
45
|
};
|
|
46
46
|
if (action === 'show') {
|
|
47
47
|
const identity = loadInstallationIdentity({ ...common, createIfMissing: true });
|
|
48
|
-
display(inspectInstallation({ ...common, identity }), json);
|
|
48
|
+
display(inspectInstallation({ ...common, identity, probeManager: true }), json);
|
|
49
49
|
return { exitCode: 0 };
|
|
50
50
|
}
|
|
51
51
|
if (action === 'adopt') {
|
|
@@ -205,6 +205,7 @@ export const refreshHandler = {
|
|
|
205
205
|
if (!quiet)
|
|
206
206
|
ui.info(dryRun ? 'Would refresh remote packages...' : 'Refreshing remote packages...');
|
|
207
207
|
const deploymentFailures = [];
|
|
208
|
+
let updateFailure = null;
|
|
208
209
|
if (!dryRun) {
|
|
209
210
|
try {
|
|
210
211
|
const refreshed = await refreshAllPackages();
|
|
@@ -242,10 +243,10 @@ export const refreshHandler = {
|
|
|
242
243
|
ui.success('Package up to date');
|
|
243
244
|
}
|
|
244
245
|
else {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
246
|
+
updateFailure = { exitCode: updateResult.exitCode };
|
|
247
|
+
if (!quiet) {
|
|
248
|
+
ui.warn('Installation update failed; continuing with re-deployment. Run `aiwg installation show` for canonical-install diagnostics.');
|
|
249
|
+
}
|
|
249
250
|
}
|
|
250
251
|
}
|
|
251
252
|
}
|
|
@@ -465,6 +466,7 @@ export const refreshHandler = {
|
|
|
465
466
|
channel: channel || undefined,
|
|
466
467
|
staleAgentRemovals,
|
|
467
468
|
deploymentFailures,
|
|
469
|
+
updateFailure,
|
|
468
470
|
});
|
|
469
471
|
console.log(output);
|
|
470
472
|
}
|
|
@@ -1873,11 +1873,13 @@ async function deploySourceDirectory(opts) {
|
|
|
1873
1873
|
args.push('--force');
|
|
1874
1874
|
if (opts.copyAll)
|
|
1875
1875
|
args.push('--copy-all');
|
|
1876
|
+
if (opts.kernelOnly)
|
|
1877
|
+
args.push('--kernel-only');
|
|
1876
1878
|
if (opts.quiet)
|
|
1877
1879
|
args.unshift('--quiet');
|
|
1878
1880
|
const runner = createScriptRunner(opts.frameworkRoot);
|
|
1879
1881
|
const result = await runner.run('tools/agents/deploy-agents.mjs', args, opts.quiet ? { capture: true } : {});
|
|
1880
|
-
if (result.exitCode === 0) {
|
|
1882
|
+
if (result.exitCode === 0 && !opts.kernelOnly) {
|
|
1881
1883
|
try {
|
|
1882
1884
|
await registerSourceCliCommands({
|
|
1883
1885
|
source: opts.source,
|
|
@@ -2436,6 +2438,7 @@ export class UseHandler {
|
|
|
2436
2438
|
}
|
|
2437
2439
|
ui.dim(' Use `aiwg use all` for the full deployment.');
|
|
2438
2440
|
for (const providerName of providersForFiltered) {
|
|
2441
|
+
const kernelOnly = !copyAll;
|
|
2439
2442
|
for (const selected of selectedFrameworks) {
|
|
2440
2443
|
const frameworkDir = resolveFrameworkDir(selected);
|
|
2441
2444
|
if (!frameworkDir)
|
|
@@ -2450,6 +2453,7 @@ export class UseHandler {
|
|
|
2450
2453
|
verbose,
|
|
2451
2454
|
force,
|
|
2452
2455
|
copyAll,
|
|
2456
|
+
kernelOnly,
|
|
2453
2457
|
quiet,
|
|
2454
2458
|
modelArgs: modelDeployArgs,
|
|
2455
2459
|
});
|
|
@@ -2467,6 +2471,7 @@ export class UseHandler {
|
|
|
2467
2471
|
verbose,
|
|
2468
2472
|
force,
|
|
2469
2473
|
copyAll,
|
|
2474
|
+
kernelOnly,
|
|
2470
2475
|
quiet,
|
|
2471
2476
|
modelArgs: modelDeployArgs,
|
|
2472
2477
|
});
|
|
@@ -2484,6 +2489,7 @@ export class UseHandler {
|
|
|
2484
2489
|
verbose,
|
|
2485
2490
|
force,
|
|
2486
2491
|
copyAll,
|
|
2492
|
+
kernelOnly,
|
|
2487
2493
|
quiet,
|
|
2488
2494
|
modelArgs: modelDeployArgs,
|
|
2489
2495
|
});
|
|
@@ -2985,6 +2991,11 @@ export class UseHandler {
|
|
|
2985
2991
|
const providerDeployArgs = builtInProviderResolution.requestedProvider
|
|
2986
2992
|
? withProviderOverride(deployFilteredArgs, provider)
|
|
2987
2993
|
: deployFilteredArgs;
|
|
2994
|
+
const bulkKernelOnly = framework === 'all'
|
|
2995
|
+
&& !remainingArgs.includes('--copy-all')
|
|
2996
|
+
&& !remainingArgs.includes('--copy-standard-skills');
|
|
2997
|
+
if (bulkKernelOnly)
|
|
2998
|
+
providerDeployArgs.push('--kernel-only');
|
|
2988
2999
|
const targetIdx = remainingArgs.findIndex(a => a === '--target');
|
|
2989
3000
|
const target = targetIdx >= 0 && remainingArgs[targetIdx + 1] ? remainingArgs[targetIdx + 1] : process.cwd();
|
|
2990
3001
|
if ((verbose || dryRun) && projectLocalProviderResolution.requestedProvider) {
|
|
@@ -3084,11 +3095,15 @@ export class UseHandler {
|
|
|
3084
3095
|
}
|
|
3085
3096
|
// Build common args for addon deployments (inherit provider and target)
|
|
3086
3097
|
const addonBaseArgs = ['--deploy-commands', '--deploy-skills', '--deploy-rules'];
|
|
3098
|
+
if (bulkKernelOnly)
|
|
3099
|
+
addonBaseArgs.push('--kernel-only');
|
|
3087
3100
|
addonBaseArgs.push(...modelDeployArgs);
|
|
3088
3101
|
if (provider)
|
|
3089
3102
|
addonBaseArgs.push('--provider', provider);
|
|
3090
3103
|
if (target)
|
|
3091
3104
|
addonBaseArgs.push('--target', target);
|
|
3105
|
+
if (dryRun)
|
|
3106
|
+
addonBaseArgs.push('--dry-run');
|
|
3092
3107
|
if (verbose)
|
|
3093
3108
|
addonBaseArgs.push('--verbose');
|
|
3094
3109
|
// Forward --copy-all to addon deploys so the legacy mirror behavior
|
|
@@ -3182,7 +3197,7 @@ export class UseHandler {
|
|
|
3182
3197
|
}
|
|
3183
3198
|
await ensureProviderGeneratedDirsIgnored(target, provider, { dryRun, verbose });
|
|
3184
3199
|
const paths = getProviderPaths(provider);
|
|
3185
|
-
if (!dryRun && !skipUtils) {
|
|
3200
|
+
if (!dryRun && !skipUtils && !bulkKernelOnly) {
|
|
3186
3201
|
const wrapperValidation = await validateDeployedModelWrappers({
|
|
3187
3202
|
provider,
|
|
3188
3203
|
target,
|
|
@@ -3200,7 +3215,7 @@ export class UseHandler {
|
|
|
3200
3215
|
const targetKernelSkillsDir = kernelSkillsPath ? resolveProviderPath(target, kernelSkillsPath) : '';
|
|
3201
3216
|
// Translate deployed skills to commands for providers that require legacy command format.
|
|
3202
3217
|
// (#550) Skills are canonical; commands are generated deployment artifacts.
|
|
3203
|
-
if (providerNeedsCommands(provider) && targetCommandsDir) {
|
|
3218
|
+
if (!bulkKernelOnly && providerNeedsCommands(provider) && targetCommandsDir) {
|
|
3204
3219
|
try {
|
|
3205
3220
|
const translationResult = await translateSkillsToCommands(targetSkillsDir, {
|
|
3206
3221
|
provider,
|
|
@@ -3222,7 +3237,7 @@ export class UseHandler {
|
|
|
3222
3237
|
// provider loads skills natively: users still expect setup, update,
|
|
3223
3238
|
// status, intake, and flow workflows to show up in the provider's `/`
|
|
3224
3239
|
// command picker where supported.
|
|
3225
|
-
if (targetCommandsDir) {
|
|
3240
|
+
if (!bulkKernelOnly && targetCommandsDir) {
|
|
3226
3241
|
try {
|
|
3227
3242
|
const standardMirrored = await mirrorStandardCommandSkills({
|
|
3228
3243
|
provider,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
function quoteCmdArgument(value) {
|
|
4
|
+
return `"${String(value).replace(/%/g, '%%').replace(/"/g, '""')}"`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a package-manager invocation without asking Node to execute a
|
|
9
|
+
* Windows command script directly. Node rejects direct .cmd/.bat execution on
|
|
10
|
+
* current Windows releases; cmd.exe is the native interpreter for those files.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveManagerCommand(file, args, options = {}) {
|
|
13
|
+
const platform = options.platform ?? process.platform;
|
|
14
|
+
if (platform !== 'win32' || !/\.(?:cmd|bat)$/i.test(file)) {
|
|
15
|
+
return { file, args };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const env = options.env ?? process.env;
|
|
19
|
+
const commandInterpreter = env.ComSpec || env.COMSPEC || 'cmd.exe';
|
|
20
|
+
const command = `"${[file, ...args].map(quoteCmdArgument).join(' ')}"`;
|
|
21
|
+
return {
|
|
22
|
+
file: commandInterpreter,
|
|
23
|
+
args: ['/d', '/s', '/c', command],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function executeManagerCommand(file, args, options = {}) {
|
|
28
|
+
const invocation = resolveManagerCommand(file, args, options);
|
|
29
|
+
const execute = options.execute ?? execFileSync;
|
|
30
|
+
return execute(invocation.file, invocation.args, options.execOptions ?? { stdio: 'inherit' });
|
|
31
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
|
|
15
|
+
import { executeManagerCommand } from './manager-command.mjs';
|
|
15
16
|
|
|
16
17
|
export const INSTALLATION_IDENTITY_VERSION = 1;
|
|
17
18
|
export const INSTALLATION_FILE = 'installation.json';
|
|
@@ -188,6 +189,25 @@ export function inspectInstallation(options = {}) {
|
|
|
188
189
|
if (identity.managerExecutable && !executableIsUsable(identity.managerExecutable)) {
|
|
189
190
|
drift.push(`recorded manager executable is missing or not executable: ${identity.managerExecutable}`);
|
|
190
191
|
}
|
|
192
|
+
let managerProbe = null;
|
|
193
|
+
if (
|
|
194
|
+
options.probeManager === true &&
|
|
195
|
+
identity.managerExecutable &&
|
|
196
|
+
executableIsUsable(identity.managerExecutable)
|
|
197
|
+
) {
|
|
198
|
+
try {
|
|
199
|
+
executeManagerCommand(identity.managerExecutable, ['--version'], {
|
|
200
|
+
...options,
|
|
201
|
+
execute: options.executeManager,
|
|
202
|
+
execOptions: { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000 },
|
|
203
|
+
});
|
|
204
|
+
managerProbe = { state: 'usable' };
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
managerProbe = { state: 'failed', error: message };
|
|
208
|
+
drift.push(`recorded manager executable cannot be invoked: ${message}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
191
211
|
return {
|
|
192
212
|
state: drift.length === 0 ? 'aligned' : (existsSync(canonicalRoot) ? 'mismatch' : 'stale'),
|
|
193
213
|
identity,
|
|
@@ -195,6 +215,7 @@ export function inspectInstallation(options = {}) {
|
|
|
195
215
|
actualRoot,
|
|
196
216
|
actualMethod,
|
|
197
217
|
drift,
|
|
218
|
+
managerProbe,
|
|
198
219
|
};
|
|
199
220
|
}
|
|
200
221
|
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import * as fs from 'fs/promises';
|
|
23
23
|
import * as path from 'path';
|
|
24
24
|
import { buildProviderBootstrapBlock, PROVIDER_BOOTSTRAP_START, PROVIDER_BOOTSTRAP_END, } from './workspace-context.js';
|
|
25
|
+
import { dominantLineEnding, withLineEnding } from './line-endings.js';
|
|
25
26
|
export const CLAUDE_HOOK_START = '<!-- AIWG:claude-md-hook:start -->';
|
|
26
27
|
export const CLAUDE_HOOK_END = '<!-- AIWG:claude-md-hook:end -->';
|
|
27
28
|
function buildClaudeArtifactOutputPolicy(policy = {}) {
|
|
@@ -77,7 +78,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
77
78
|
catch {
|
|
78
79
|
// Missing/legacy/temporarily malformed config receives the safe default.
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
+
let block = buildClaudeHookBlock(policy);
|
|
81
82
|
// Case 1: CLAUDE.md does not exist — create a minimal one with just the block.
|
|
82
83
|
let existing;
|
|
83
84
|
try {
|
|
@@ -92,6 +93,8 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
92
93
|
}
|
|
93
94
|
throw err;
|
|
94
95
|
}
|
|
96
|
+
const lineEnding = dominantLineEnding(existing);
|
|
97
|
+
block = withLineEnding(block, lineEnding);
|
|
95
98
|
const startIdx = existing.indexOf(CLAUDE_HOOK_START);
|
|
96
99
|
const endIdx = existing.indexOf(CLAUDE_HOOK_END);
|
|
97
100
|
// Case 2: marker block does not exist — append the block to end of file.
|
|
@@ -117,8 +120,8 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
117
120
|
return result;
|
|
118
121
|
}
|
|
119
122
|
// Ensure the file ends with a single newline before appending.
|
|
120
|
-
const trimmed = existing.replace(
|
|
121
|
-
const updated = `${trimmed}
|
|
123
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
124
|
+
const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
|
|
122
125
|
await fs.writeFile(claudeMdPath, updated, 'utf8');
|
|
123
126
|
result.action = 'inserted';
|
|
124
127
|
return result;
|
|
@@ -135,8 +138,8 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
135
138
|
await fs.writeFile(backupPath, existing, 'utf8');
|
|
136
139
|
result.backupPath = backupPath;
|
|
137
140
|
}
|
|
138
|
-
const trimmed = existing.replace(
|
|
139
|
-
const updated = `${trimmed}
|
|
141
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
142
|
+
const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
|
|
140
143
|
await fs.writeFile(claudeMdPath, updated, 'utf8');
|
|
141
144
|
result.action = 'inserted';
|
|
142
145
|
return result;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Select the majority line ending, preferring LF for ties and new files. */
|
|
2
|
+
export function dominantLineEnding(content) {
|
|
3
|
+
const crlfCount = content.match(/\r\n/g)?.length ?? 0;
|
|
4
|
+
const newlineCount = content.match(/\n/g)?.length ?? 0;
|
|
5
|
+
const bareLfCount = newlineCount - crlfCount;
|
|
6
|
+
return crlfCount > bareLfCount ? '\r\n' : '\n';
|
|
7
|
+
}
|
|
8
|
+
/** Render generated text using the line-ending convention of existing content. */
|
|
9
|
+
export function withLineEnding(content, lineEnding) {
|
|
10
|
+
return content.replace(/\r?\n/g, lineEnding);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=line-endings.js.map
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import * as fs from 'fs/promises';
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { buildProviderBootstrapBlock } from './workspace-context.js';
|
|
17
|
+
import { dominantLineEnding, withLineEnding } from './line-endings.js';
|
|
17
18
|
export const CONTEXT_HOOK_START = '<!-- AIWG:context-hook:start -->';
|
|
18
19
|
export const CONTEXT_HOOK_END = '<!-- AIWG:context-hook:end -->';
|
|
19
20
|
/** The managed block — loads canonical workspace context before framework context. */
|
|
@@ -42,7 +43,7 @@ export function hasContextHook(content) {
|
|
|
42
43
|
*/
|
|
43
44
|
export async function ensureManagedHook(filePath, opts = {}) {
|
|
44
45
|
const base = path.basename(filePath);
|
|
45
|
-
|
|
46
|
+
let block = buildContextHookBlock(opts.provider);
|
|
46
47
|
const result = { path: filePath, action: 'skipped', warnings: [] };
|
|
47
48
|
let existing;
|
|
48
49
|
try {
|
|
@@ -56,6 +57,8 @@ export async function ensureManagedHook(filePath, opts = {}) {
|
|
|
56
57
|
}
|
|
57
58
|
throw err;
|
|
58
59
|
}
|
|
60
|
+
const lineEnding = dominantLineEnding(existing);
|
|
61
|
+
block = withLineEnding(block, lineEnding);
|
|
59
62
|
// Already has both bare includes (operator wired them by hand) — nothing to do.
|
|
60
63
|
if (!existing.includes(CONTEXT_HOOK_START) && /^[ \t]*@WORKSPACE\.md[ \t]*$/m.test(existing) && /^[ \t]*@AIWG\.md[ \t]*$/m.test(existing)) {
|
|
61
64
|
result.action = 'unchanged';
|
|
@@ -65,16 +68,16 @@ export async function ensureManagedHook(filePath, opts = {}) {
|
|
|
65
68
|
const e = existing.indexOf(CONTEXT_HOOK_END);
|
|
66
69
|
// No managed block — append it to the end, preserving everything above.
|
|
67
70
|
if (s === -1 && e === -1) {
|
|
68
|
-
const trimmed = existing.replace(
|
|
69
|
-
await fs.writeFile(filePath, `${trimmed}
|
|
71
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
72
|
+
await fs.writeFile(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
|
|
70
73
|
result.action = 'inserted';
|
|
71
74
|
return result;
|
|
72
75
|
}
|
|
73
76
|
// Malformed (one marker only) — repair only with --force to avoid clobbering.
|
|
74
77
|
if (s === -1 || e === -1) {
|
|
75
78
|
if (opts.force) {
|
|
76
|
-
const trimmed = existing.replace(
|
|
77
|
-
await fs.writeFile(filePath, `${trimmed}
|
|
79
|
+
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
80
|
+
await fs.writeFile(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
|
|
78
81
|
result.action = 'inserted';
|
|
79
82
|
return result;
|
|
80
83
|
}
|
|
@@ -14,6 +14,7 @@ import { buildNormalizedAiwgMd } from './finalization.js';
|
|
|
14
14
|
import { getProviderDefinition, listProviderDefinitions, } from '../../providers/provider-definitions.js';
|
|
15
15
|
import { readAiwgConfig } from '../../config/aiwg-config.js';
|
|
16
16
|
import { projectAiwgPath, projectControlPath, resolveProjectAiwgDir, } from '../../config/project-artifacts.js';
|
|
17
|
+
import { dominantLineEnding, withLineEnding } from './line-endings.js';
|
|
17
18
|
export const WORKSPACE_MANAGED_START = '<!-- AIWG:workspace-context:start -->';
|
|
18
19
|
export const WORKSPACE_MANAGED_END = '<!-- AIWG:workspace-context:end -->';
|
|
19
20
|
export const WORKSPACE_OPERATOR_START = '<!-- AIWG:workspace-operator:start -->';
|
|
@@ -90,7 +91,8 @@ function replaceBlock(content, start, end, block) {
|
|
|
90
91
|
return null;
|
|
91
92
|
if (startIndex < 0 || endIndex < startIndex)
|
|
92
93
|
throw new Error(`Malformed managed block: ${start} / ${end}`);
|
|
93
|
-
|
|
94
|
+
const renderedBlock = withLineEnding(block, dominantLineEnding(content));
|
|
95
|
+
return content.slice(0, startIndex) + renderedBlock + content.slice(endIndex + end.length);
|
|
94
96
|
}
|
|
95
97
|
function stripGeneratedBlocks(content) {
|
|
96
98
|
let stripped = content;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
loadInstallationIdentity,
|
|
17
17
|
saveInstallationIdentity,
|
|
18
18
|
} from '../installation/manager.mjs';
|
|
19
|
+
import { resolveManagerCommand } from '../installation/manager-command.mjs';
|
|
19
20
|
|
|
20
21
|
const VALID_MODES = new Set(['npm', 'web', 'source']);
|
|
21
22
|
|
|
@@ -148,8 +149,9 @@ export async function updateInstallation(options = {}) {
|
|
|
148
149
|
throw new Error('Canonical npm installation has no package-manager executable. Run `aiwg installation adopt --manager <absolute-path-to-npm>`.');
|
|
149
150
|
}
|
|
150
151
|
if (!dryRun) {
|
|
152
|
+
const invocation = resolveManagerCommand(managerExecutable, command, options);
|
|
151
153
|
const execute = options.execute ?? ((file, args) => execFileSync(file, args, { stdio: 'inherit' }));
|
|
152
|
-
execute(
|
|
154
|
+
execute(invocation.file, invocation.args);
|
|
153
155
|
if (detected.identity && detected.identityPersistent && options.persistIdentity !== false) {
|
|
154
156
|
saveInstallationIdentity({ ...detected.identity, channel }, options);
|
|
155
157
|
}
|
package/package.json
CHANGED
|
@@ -340,7 +340,7 @@ function mirrorSkillsAsCommands(provider, target, srcRoot, opts) {
|
|
|
340
340
|
* @param {string|null} explicitSource the raw `--source` value (null when unset)
|
|
341
341
|
*/
|
|
342
342
|
function pruneStaleAiwgArtifacts(provider, target, srcRoot, opts, explicitSource) {
|
|
343
|
-
if (opts.skillsOnly) return; // skills run their own prune in the provider
|
|
343
|
+
if (opts.skillsOnly && !opts.kernelOnly) return; // skills run their own prune in the provider
|
|
344
344
|
|
|
345
345
|
const aiwgRoot = resolveAiwgRoot(srcRoot);
|
|
346
346
|
if (!aiwgRoot) return; // no AIWG tree → bundle/standalone deploy; never prune
|
|
@@ -355,6 +355,20 @@ function pruneStaleAiwgArtifacts(provider, target, srcRoot, opts, explicitSource
|
|
|
355
355
|
if (!underCode) return; // project-local bundle / external source → skip
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
if (opts.kernelOnly) {
|
|
359
|
+
for (const type of ['agents', 'commands', 'rules']) {
|
|
360
|
+
const relPath = provider.paths?.[type];
|
|
361
|
+
if (!relPath || relPath.endsWith('.md')) continue;
|
|
362
|
+
const destDir = path.isAbsolute(relPath) ? relPath : path.join(target, relPath);
|
|
363
|
+
pruneStaleAiwgFiles(destDir, new Set(), {
|
|
364
|
+
dryRun: opts.dryRun,
|
|
365
|
+
verbose: opts.verbose,
|
|
366
|
+
artifactExtensions: ['.md', '.mdc', '.toml'],
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
358
372
|
const typesThisRun = [];
|
|
359
373
|
if (!opts.commandsOnly && !opts.rulesOnly) typesThisRun.push('agents');
|
|
360
374
|
if (opts.deployCommands || opts.commandsOnly) typesThisRun.push('commands');
|
|
@@ -406,6 +420,7 @@ function parseArgs() {
|
|
|
406
420
|
commandsOnly: false,
|
|
407
421
|
skillsOnly: false,
|
|
408
422
|
rulesOnly: false,
|
|
423
|
+
kernelOnly: false,
|
|
409
424
|
filter: null, // Glob pattern for agent names
|
|
410
425
|
filterRole: null, // Filter by role: reasoning|coding|efficiency
|
|
411
426
|
save: false, // Save model config to project models.json
|
|
@@ -438,6 +453,7 @@ function parseArgs() {
|
|
|
438
453
|
else if (a === '--commands-only') cfg.commandsOnly = true;
|
|
439
454
|
else if (a === '--skills-only') cfg.skillsOnly = true;
|
|
440
455
|
else if (a === '--rules-only') cfg.rulesOnly = true;
|
|
456
|
+
else if (a === '--kernel-only') cfg.kernelOnly = true;
|
|
441
457
|
else if (a === '--deploy-behaviors') cfg.deployBehaviors = true;
|
|
442
458
|
else if (a === '--filter' && args[i + 1]) cfg.filter = args[++i];
|
|
443
459
|
else if (a === '--filter-role' && args[i + 1]) cfg.filterRole = args[++i];
|
|
@@ -474,6 +490,7 @@ Options:
|
|
|
474
490
|
--commands-only Deploy only commands (skip agents)
|
|
475
491
|
--skills-only Deploy only skills (skip agents)
|
|
476
492
|
--rules-only Deploy only rules (skip agents)
|
|
493
|
+
--kernel-only Deploy kernel skills only and prune managed bulk artifacts
|
|
477
494
|
--dry-run Show what would be deployed without writing
|
|
478
495
|
--force Overwrite existing files
|
|
479
496
|
--provider <name> Target provider (see below)
|
|
@@ -490,7 +507,9 @@ Options:
|
|
|
490
507
|
--create-agents-md Create/update AGENTS.md template
|
|
491
508
|
--skip-commands-migration Skip deleting the commands directory before skills deployment
|
|
492
509
|
--copy-all Copy ALL skills per-project (legacy mirror at <provider>/.aiwg/skills/).
|
|
493
|
-
|
|
510
|
+
For aiwg use all, this also restores the legacy full agent,
|
|
511
|
+
command, and expanded-rule copy. Default bulk deployment is
|
|
512
|
+
kernel-only + index-driven discovery for the rest (#1217).
|
|
494
513
|
Use this for sandboxed runtimes / air-gapped corpora where
|
|
495
514
|
$AIWG_ROOT isn't readable from the agent's working dir.
|
|
496
515
|
Alias: --copy-standard-skills — rc.29 era).
|
|
@@ -879,13 +898,14 @@ export async function main() {
|
|
|
879
898
|
modelsConfig,
|
|
880
899
|
asAgentsMd: cfg.asAgentsMd,
|
|
881
900
|
createAgentsMd: cfg.createAgentsMd,
|
|
882
|
-
deployCommands: cfg.deployCommands,
|
|
883
|
-
deploySkills: cfg.deploySkills,
|
|
884
|
-
deployRules: cfg.deployRules,
|
|
885
|
-
deployBehaviors: cfg.deployBehaviors,
|
|
901
|
+
deployCommands: cfg.kernelOnly ? false : cfg.deployCommands,
|
|
902
|
+
deploySkills: cfg.kernelOnly ? true : cfg.deploySkills,
|
|
903
|
+
deployRules: cfg.kernelOnly ? false : cfg.deployRules,
|
|
904
|
+
deployBehaviors: cfg.kernelOnly ? false : cfg.deployBehaviors,
|
|
886
905
|
commandsOnly: cfg.commandsOnly,
|
|
887
|
-
skillsOnly: cfg.skillsOnly,
|
|
906
|
+
skillsOnly: cfg.skillsOnly || cfg.kernelOnly,
|
|
888
907
|
rulesOnly: cfg.rulesOnly,
|
|
908
|
+
kernelOnly: cfg.kernelOnly,
|
|
889
909
|
filter: cfg.filter,
|
|
890
910
|
filterRole: cfg.filterRole,
|
|
891
911
|
save: cfg.save,
|
|
@@ -893,7 +913,7 @@ export async function main() {
|
|
|
893
913
|
verbose: cfg.verbose,
|
|
894
914
|
quiet: cfg.quiet,
|
|
895
915
|
asPlugin: cfg.asPlugin,
|
|
896
|
-
deployBehaviors: cfg.deployBehaviors,
|
|
916
|
+
deployBehaviors: cfg.kernelOnly ? false : cfg.deployBehaviors,
|
|
897
917
|
skipCommandsMigration: cfg.skipCommandsMigration,
|
|
898
918
|
// #1217 / #1219: --copy-all flag forces legacy per-project mirror
|
|
899
919
|
// for the standard tier. Default is no-copy + index-driven discovery.
|
|
@@ -1183,7 +1183,7 @@ export function computeAllKernelNames(srcRoot) {
|
|
|
1183
1183
|
* located — see `computeAllKernelNames`), pruning is skipped entirely so a
|
|
1184
1184
|
* project-local-bundle deploy without AIWG_ROOT never empties the kernel
|
|
1185
1185
|
* skills directory (#123).
|
|
1186
|
-
* @param {object} opts `{ dryRun, verbose }`
|
|
1186
|
+
* @param {object} opts `{ dryRun, verbose, artifactExtensions }`
|
|
1187
1187
|
* @returns {number} count of pruned entries
|
|
1188
1188
|
*/
|
|
1189
1189
|
export function pruneStaleAiwgSkills(kernelDestDir, desiredKernelNames, opts = {}) {
|
|
@@ -1348,7 +1348,8 @@ export function resolveAiwgRoot(srcRoot) {
|
|
|
1348
1348
|
* `pruneStaleAiwgSkills`.
|
|
1349
1349
|
*
|
|
1350
1350
|
* Removes a file from `destDir` only when ALL hold:
|
|
1351
|
-
* 1. It
|
|
1351
|
+
* 1. It has an allowed deployed-artifact extension (`.md` / `.mdc` by
|
|
1352
|
+
* default; callers may include `.toml`), is not `RULES-INDEX.md`,
|
|
1352
1353
|
* and not the sidecar manifest.
|
|
1353
1354
|
* 2. Its stem is NOT in `desiredStems` (the source no longer ships it).
|
|
1354
1355
|
* 3. It carries an AIWG ownership signal — either a `.aiwg-manifest.json`
|
|
@@ -1369,6 +1370,7 @@ export function resolveAiwgRoot(srcRoot) {
|
|
|
1369
1370
|
*/
|
|
1370
1371
|
export function pruneStaleAiwgFiles(destDir, desiredStems, opts = {}) {
|
|
1371
1372
|
const { dryRun = false, verbose = false } = opts;
|
|
1373
|
+
const artifactExtensions = opts.artifactExtensions || ['.md', '.mdc'];
|
|
1372
1374
|
const removed = [];
|
|
1373
1375
|
if (!destDir || !fs.existsSync(destDir)) return removed;
|
|
1374
1376
|
|
|
@@ -1391,7 +1393,7 @@ export function pruneStaleAiwgFiles(destDir, desiredStems, opts = {}) {
|
|
|
1391
1393
|
if (name === 'RULES-INDEX.md') continue;
|
|
1392
1394
|
if (name === 'RULES-ONDEMAND.md') continue; // generated on-demand index (#1673)
|
|
1393
1395
|
const lower = name.toLowerCase();
|
|
1394
|
-
if (!
|
|
1396
|
+
if (!artifactExtensions.some(extension => lower.endsWith(extension))) continue;
|
|
1395
1397
|
|
|
1396
1398
|
if (desired.has(artifactStem(name))) continue;
|
|
1397
1399
|
|