@aiwg/cli 2026.7.19 → 2026.7.21

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.
Files changed (88) hide show
  1. package/README.md +397 -385
  2. package/dist/src/api/index.d.ts +1 -0
  3. package/dist/src/api/index.js +1 -0
  4. package/dist/src/artifacts/browser-export.js +7 -0
  5. package/dist/src/artifacts/citation-parser.js +96 -35
  6. package/dist/src/artifacts/cli.js +59 -5
  7. package/dist/src/artifacts/discover-facets.js +15 -0
  8. package/dist/src/artifacts/discovery-eval.js +290 -0
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +1 -1
  10. package/dist/src/artifacts/fortemi-shard-export.js +1 -1
  11. package/dist/src/artifacts/index-builder.js +54 -17
  12. package/dist/src/artifacts/query-engine.js +10 -6
  13. package/dist/src/artifacts/state-transfer.js +27 -0
  14. package/dist/src/artifacts/stats.js +8 -0
  15. package/dist/src/cli/cli-extension-loader.js +73 -0
  16. package/dist/src/cli/handlers/help.js +2 -1
  17. package/dist/src/cli/handlers/index.js +6 -2
  18. package/dist/src/cli/handlers/resource-versions.js +247 -0
  19. package/dist/src/cli/handlers/sessions.js +966 -0
  20. package/dist/src/cli/handlers/skill-lint.js +49 -45
  21. package/dist/src/cli/handlers/subcommands.js +55 -3
  22. package/dist/src/cli/handlers/use.js +154 -59
  23. package/dist/src/cli/handlers/utilities.js +49 -34
  24. package/dist/src/cli/skill-usage.js +146 -24
  25. package/dist/src/config/cli.js +13 -9
  26. package/dist/src/config/project-artifacts-runtime.mjs +68 -0
  27. package/dist/src/config/project-artifacts.js +1 -68
  28. package/dist/src/extensions/commands/definitions.js +65 -2
  29. package/dist/src/extensions/manifest.js +29 -0
  30. package/dist/src/extensions/project-local-discovery.js +86 -2
  31. package/dist/src/extensions/project-local-remove.js +52 -56
  32. package/dist/src/extensions/shadow-resolver.js +3 -1
  33. package/dist/src/plugins/standalone-packager.js +143 -0
  34. package/dist/src/resources/cache-cleanup.js +67 -0
  35. package/dist/src/resources/doctor.js +107 -0
  36. package/dist/src/resources/lockfile.js +125 -0
  37. package/dist/src/resources/resolver.js +133 -0
  38. package/dist/src/resources/web-release.d.ts +8 -0
  39. package/dist/src/resources/web-release.js +159 -1
  40. package/dist/src/sessions/adapters/claude.js +357 -0
  41. package/dist/src/sessions/adapters/codex.js +521 -0
  42. package/dist/src/sessions/adapters/copilot.js +226 -0
  43. package/dist/src/sessions/adapters/cursor.js +372 -0
  44. package/dist/src/sessions/adapters/factory.js +345 -0
  45. package/dist/src/sessions/adapters/generic.js +225 -0
  46. package/dist/src/sessions/adapters/hermes.js +341 -0
  47. package/dist/src/sessions/adapters/openclaw.js +381 -0
  48. package/dist/src/sessions/adapters/opencode.js +454 -0
  49. package/dist/src/sessions/adapters/openhuman.js +315 -0
  50. package/dist/src/sessions/adapters/warp.js +160 -0
  51. package/dist/src/sessions/adapters/windsurf.js +212 -0
  52. package/dist/src/sessions/candidates.js +210 -0
  53. package/dist/src/sessions/contracts.js +310 -0
  54. package/dist/src/sessions/discovery.js +51 -0
  55. package/dist/src/sessions/fixtures.js +12 -0
  56. package/dist/src/sessions/importer.js +315 -0
  57. package/dist/src/sessions/index.js +25 -0
  58. package/dist/src/sessions/knowledge-shard.js +61 -0
  59. package/dist/src/sessions/optional-backends.js +238 -0
  60. package/dist/src/sessions/policy.js +192 -0
  61. package/dist/src/sessions/ports.js +2 -0
  62. package/dist/src/sessions/promotion.js +367 -0
  63. package/dist/src/sessions/readers.js +176 -0
  64. package/dist/src/sessions/repository.js +1551 -0
  65. package/dist/src/skills/adapters/agent-skills.js +59 -0
  66. package/dist/src/skills/adapters/local.js +19 -1
  67. package/dist/src/skills/agent-skills.js +249 -0
  68. package/dist/src/skills/cli.js +463 -7
  69. package/dist/src/skills/deployer.js +554 -0
  70. package/dist/src/skills/doctor.js +105 -0
  71. package/dist/src/skills/exporter.js +382 -0
  72. package/dist/src/skills/importer.js +921 -0
  73. package/dist/src/skills/registry.js +19 -0
  74. package/dist/src/skills/validator.js +323 -0
  75. package/dist/src/smiths/context-pipeline/aiwg-md.js +5 -1
  76. package/dist/src/smiths/context-pipeline/claude-hook.js +21 -1
  77. package/dist/src/smiths/context-pipeline/finalization.js +5 -3
  78. package/dist/src/smiths/context-pipeline/generator.js +4 -1
  79. package/dist/src/smiths/context-pipeline/parallelism-section.js +34 -1
  80. package/dist/src/smiths/context-pipeline/workspace-context.js +15 -3
  81. package/dist/src/smiths/mcpsmith/example.js +3 -1
  82. package/dist/src/smiths/mcpsmith/generator.js +3 -1
  83. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +2 -1
  84. package/dist/src/storage/cli.js +3 -2
  85. package/dist/src/storage/subsystem-cli.js +7 -2
  86. package/dist/src/update/notifier.mjs +1 -1
  87. package/dist/src/update/service.mjs +123 -0
  88. package/package.json +3 -2
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @implements #539
8
8
  */
9
+ import { AgentSkillsAdapter } from './adapters/agent-skills.js';
9
10
  import { LocalAdapter } from './adapters/local.js';
10
11
  import { ClawHubAdapter } from './adapters/clawhub.js';
11
12
  import { OpenClawAdapter } from './adapters/openclaw.js';
@@ -14,9 +15,27 @@ import { OpenClawAdapter } from './adapters/openclaw.js';
14
15
  */
15
16
  const ALL_ADAPTERS = [
16
17
  new LocalAdapter(),
18
+ new AgentSkillsAdapter(),
17
19
  new ClawHubAdapter(),
18
20
  new OpenClawAdapter(),
19
21
  ];
22
+ /**
23
+ * Import a standard Agent Skills directory or pinned Git source.
24
+ */
25
+ export async function importSkillSource(source, options, providerId = 'agentskills') {
26
+ const adapter = getAdapter(providerId);
27
+ if (!adapter) {
28
+ throw new Error(`Unknown registry: ${providerId}`);
29
+ }
30
+ if (!adapter.importSource) {
31
+ throw new Error(`Registry '${providerId}' does not support source import`);
32
+ }
33
+ const available = await adapter.isAvailable();
34
+ if (!available) {
35
+ throw new Error(`Registry '${providerId}' is not available`);
36
+ }
37
+ return adapter.importSource(source, options);
38
+ }
20
39
  /**
21
40
  * Get adapter by ID
22
41
  */
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Shared Agent Skills parser and conformance validator.
3
+ *
4
+ * This module is the only place that parses SKILL.md for Agent Skills
5
+ * conformance. Consumers may add their own quality or deployment policy, but
6
+ * must retain these diagnostics unchanged.
7
+ *
8
+ * @implements #1878
9
+ */
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { parseDocument } from 'yaml';
13
+ import { AGENT_SKILLS_BASELINE, AGENT_SKILL_VALIDATION_PROFILES, AIWG_SKILL_CONTROL_FIELDS, STANDARD_SKILL_FIELDS, validateCompatibleAgentSkillMetadata, } from './agent-skills.js';
14
+ const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
15
+ const MARKDOWN_LINK = /!?\[[^\]]*]\(\s*(?:<([^>]+)>|([^\s)]+))(?:\s+["'][^)]*["'])?\s*\)/g;
16
+ const STANDARD_FIELDS = new Set(STANDARD_SKILL_FIELDS);
17
+ const AIWG_FIELDS = new Set(AIWG_SKILL_CONTROL_FIELDS);
18
+ function diagnostic(code, severity, file, yamlPath, message, remediation) {
19
+ return {
20
+ code,
21
+ severity,
22
+ file,
23
+ yamlPath,
24
+ message,
25
+ upstreamBaseline: AGENT_SKILLS_BASELINE.revision,
26
+ remediation,
27
+ };
28
+ }
29
+ function sortDiagnostics(diagnostics) {
30
+ return diagnostics.sort((left, right) => (left.file.localeCompare(right.file)
31
+ || left.code.localeCompare(right.code)
32
+ || left.yamlPath.localeCompare(right.yamlPath)
33
+ || left.message.localeCompare(right.message)));
34
+ }
35
+ function normalizeMetadataDiagnostics(diagnostics, frontmatter, profile, file) {
36
+ const policy = AGENT_SKILL_VALIDATION_PROFILES[profile];
37
+ const normalized = diagnostics.flatMap((item) => {
38
+ if (item.code === 'AS_FIELD_UNKNOWN') {
39
+ return [{
40
+ ...item,
41
+ severity: policy.unknownField,
42
+ }];
43
+ }
44
+ if (profile === 'discovery'
45
+ && (item.code === 'AS_NAME_FORMAT' || item.code === 'AS_NAME_DIRECTORY')) {
46
+ return [{ ...item, severity: policy.cosmeticNameDefect }];
47
+ }
48
+ return [item];
49
+ });
50
+ if (!policy.recognizedAiwgFields) {
51
+ for (const key of Object.keys(frontmatter).sort()) {
52
+ if (!AIWG_FIELDS.has(key))
53
+ continue;
54
+ normalized.push(diagnostic('AS_FIELD_EXTENSION', 'error', file, `$.${key}`, `AIWG extension field "${key}" is not allowed by the strict profile`, 'Remove the extension field or validate with the compatible profile.'));
55
+ }
56
+ }
57
+ return normalized;
58
+ }
59
+ function resourceDiagnostics(content, file, skillRoot, checkResources) {
60
+ const diagnostics = [];
61
+ const references = new Set();
62
+ for (const match of content.matchAll(MARKDOWN_LINK)) {
63
+ const raw = (match[1] ?? match[2] ?? '').trim();
64
+ if (raw.length === 0
65
+ || raw.startsWith('#')
66
+ || /^(?:https?|mailto|data):/i.test(raw)) {
67
+ continue;
68
+ }
69
+ const withoutFragment = raw.split('#', 1)[0] ?? '';
70
+ const decoded = (() => {
71
+ try {
72
+ return decodeURIComponent(withoutFragment);
73
+ }
74
+ catch {
75
+ return withoutFragment;
76
+ }
77
+ })();
78
+ references.add(decoded);
79
+ }
80
+ for (const reference of [...references].sort()) {
81
+ const normalized = reference.replaceAll('\\', '/');
82
+ if (path.isAbsolute(reference)
83
+ || normalized === '..'
84
+ || normalized.startsWith('../')
85
+ || normalized.includes('/../')) {
86
+ diagnostics.push(diagnostic('AS_RESOURCE_PATH', 'warning', file, '$.body', `resource reference "${reference}" is not an in-skill relative path`, 'Reference a file relative to SKILL.md without parent traversal.'));
87
+ continue;
88
+ }
89
+ const segments = normalized.split('/').filter(Boolean);
90
+ if (segments.length > 2) {
91
+ diagnostics.push(diagnostic('AS_ADVISORY_RESOURCE_DEPTH', 'warning', file, '$.body', `resource reference "${reference}" is deeper than one resource-directory level`, 'Prefer resources such as references/topic.md, scripts/run.sh, or assets/example.json.'));
92
+ }
93
+ if (!checkResources || !skillRoot)
94
+ continue;
95
+ const resolved = path.resolve(skillRoot, reference);
96
+ const relative = path.relative(skillRoot, resolved);
97
+ if (relative.startsWith('..')
98
+ || path.isAbsolute(relative)
99
+ || !fs.existsSync(resolved)) {
100
+ diagnostics.push(diagnostic('AS_RESOURCE_MISSING', 'warning', file, '$.body', `referenced resource "${reference}" does not exist`, 'Add the referenced file or correct the relative resource path.'));
101
+ continue;
102
+ }
103
+ const stat = fs.lstatSync(resolved);
104
+ if (stat.isSymbolicLink() || !stat.isFile()) {
105
+ diagnostics.push(diagnostic('AS_RESOURCE_TYPE', 'warning', file, '$.body', `referenced resource "${reference}" is not a regular file`, 'Replace the reference target with an in-tree regular file.'));
106
+ }
107
+ }
108
+ return diagnostics;
109
+ }
110
+ function resultState(diagnostics, profile) {
111
+ const errors = diagnostics.filter((item) => item.severity === 'error');
112
+ if (errors.length > 0) {
113
+ if (profile === 'discovery'
114
+ && errors.some((item) => (item.code === 'AS_DESCRIPTION_REQUIRED'
115
+ || item.code === 'AS_FRONTMATTER_REQUIRED'
116
+ || item.code === 'AS_YAML_PARSE'
117
+ || item.code === 'AS_YAML_TYPE'))) {
118
+ return 'skipped';
119
+ }
120
+ return 'invalid';
121
+ }
122
+ return diagnostics.some((item) => item.severity === 'warning')
123
+ ? 'warning'
124
+ : 'valid';
125
+ }
126
+ export function validateAgentSkillContent(content, options = {}) {
127
+ const profile = options.profile ?? 'compatible';
128
+ const file = options.file ?? 'SKILL.md';
129
+ const directoryName = options.directoryName
130
+ ?? (options.skillRoot ? path.basename(options.skillRoot) : undefined)
131
+ ?? (path.dirname(file) !== '.' ? path.basename(path.dirname(file)) : undefined);
132
+ const lines = content.split(/\r?\n/).length;
133
+ const metrics = {
134
+ lines,
135
+ // The upstream guidance is advisory. A deterministic UTF-16/4 estimate
136
+ // avoids a runtime tokenizer dependency while keeping CI snapshots stable.
137
+ estimatedTokens: Math.ceil(content.length / 4),
138
+ };
139
+ const diagnostics = [];
140
+ const match = FRONTMATTER.exec(content);
141
+ if (!match) {
142
+ diagnostics.push(diagnostic('AS_FRONTMATTER_REQUIRED', 'error', file, '$', 'SKILL.md must begin with YAML frontmatter', 'Add a leading YAML mapping delimited by `---` lines.'));
143
+ const state = resultState(diagnostics, profile);
144
+ return {
145
+ schemaVersion: 1,
146
+ profile,
147
+ file,
148
+ state,
149
+ valid: false,
150
+ discoverable: false,
151
+ body: content,
152
+ diagnostics,
153
+ metrics,
154
+ };
155
+ }
156
+ const document = parseDocument(match[1] ?? '', {
157
+ prettyErrors: false,
158
+ strict: true,
159
+ uniqueKeys: true,
160
+ });
161
+ if (document.errors.length > 0) {
162
+ for (const error of document.errors) {
163
+ diagnostics.push(diagnostic('AS_YAML_PARSE', 'error', file, '$', `SKILL.md frontmatter is invalid YAML: ${error.message}`, 'Correct the YAML syntax before validation or discovery.'));
164
+ }
165
+ const state = resultState(diagnostics, profile);
166
+ return {
167
+ schemaVersion: 1,
168
+ profile,
169
+ file,
170
+ state,
171
+ valid: false,
172
+ discoverable: false,
173
+ body: content.slice(match[0].length),
174
+ diagnostics: sortDiagnostics(diagnostics),
175
+ metrics,
176
+ };
177
+ }
178
+ const parsed = document.toJS({ maxAliasCount: 100 });
179
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
180
+ diagnostics.push(diagnostic('AS_YAML_TYPE', 'error', file, '$', 'SKILL.md frontmatter must be a YAML mapping', 'Use top-level key/value fields.'));
181
+ const state = resultState(diagnostics, profile);
182
+ return {
183
+ schemaVersion: 1,
184
+ profile,
185
+ file,
186
+ state,
187
+ valid: false,
188
+ discoverable: false,
189
+ body: content.slice(match[0].length),
190
+ diagnostics,
191
+ metrics,
192
+ };
193
+ }
194
+ const frontmatter = parsed;
195
+ diagnostics.push(...normalizeMetadataDiagnostics(validateCompatibleAgentSkillMetadata(frontmatter, directoryName
196
+ ?? (typeof frontmatter['name'] === 'string' ? frontmatter['name'] : ''), file, lines), frontmatter, profile, file));
197
+ if (Object.prototype.hasOwnProperty.call(frontmatter, 'allowed-tools')) {
198
+ const value = frontmatter['allowed-tools'];
199
+ if (typeof value === 'string'
200
+ && (value.trim() !== value || value.length === 0 || /\s{2,}|\t|\r|\n/.test(value))) {
201
+ diagnostics.push(diagnostic('AS_ALLOWED_TOOLS_FORMAT', 'error', file, '$.allowed-tools', 'allowed-tools must contain non-empty tool identifiers separated by single spaces', 'Normalize allowed-tools to a single space-delimited string.'));
202
+ }
203
+ diagnostics.push(diagnostic('AS_ALLOWED_TOOLS_EXPERIMENTAL', 'warning', file, '$.allowed-tools', 'allowed-tools is experimental in the pinned Agent Skills baseline', 'Treat allowed-tools as advisory unless the target provider documents enforcement.'));
204
+ }
205
+ const body = content.slice(match[0].length);
206
+ if (body.trim().length === 0) {
207
+ diagnostics.push(diagnostic('AS_BODY_REQUIRED', 'error', file, '$.body', 'SKILL.md must contain Markdown instructions after frontmatter', 'Add the skill instructions below the closing frontmatter delimiter.'));
208
+ }
209
+ if (metrics.estimatedTokens > 5_000) {
210
+ diagnostics.push(diagnostic('AS_ADVISORY_TOKENS', 'warning', file, '$', `SKILL.md is approximately ${metrics.estimatedTokens} tokens; the recommendation is at most 5,000`, 'Move detailed material to referenced resources.'));
211
+ }
212
+ diagnostics.push(...resourceDiagnostics(body, file, options.skillRoot, options.checkResources ?? Boolean(options.skillRoot)));
213
+ // Defensive policy check: a future field added to one allow-list must not
214
+ // silently escape classification in this parser.
215
+ for (const key of Object.keys(frontmatter).sort()) {
216
+ if (STANDARD_FIELDS.has(key) || AIWG_FIELDS.has(key))
217
+ continue;
218
+ if (!diagnostics.some((item) => item.yamlPath === `$.${key}`)) {
219
+ diagnostics.push(diagnostic('AS_FIELD_UNKNOWN', AGENT_SKILL_VALIDATION_PROFILES[profile].unknownField, file, `$.${key}`, `unrecognized top-level field "${key}"`, 'Remove the field or map it explicitly before granting it policy meaning.'));
220
+ }
221
+ }
222
+ sortDiagnostics(diagnostics);
223
+ const state = resultState(diagnostics, profile);
224
+ return {
225
+ schemaVersion: 1,
226
+ profile,
227
+ file,
228
+ state,
229
+ valid: state === 'valid' || state === 'warning',
230
+ discoverable: state !== 'skipped',
231
+ frontmatter,
232
+ body,
233
+ diagnostics,
234
+ metrics,
235
+ };
236
+ }
237
+ export function validateAgentSkillFile(filePath, options = {}) {
238
+ const resolved = path.resolve(filePath);
239
+ const stat = fs.lstatSync(resolved);
240
+ if (stat.isSymbolicLink() || !stat.isFile()) {
241
+ const content = '';
242
+ const result = validateAgentSkillContent(content, {
243
+ ...options,
244
+ file: resolved,
245
+ skillRoot: path.dirname(resolved),
246
+ });
247
+ result.diagnostics = [diagnostic('AS_SKILL_FILE_TYPE', 'error', resolved, '$', 'SKILL.md must be a regular file, not a symbolic link or special file', 'Replace SKILL.md with an in-tree regular file.')];
248
+ result.state = options.profile === 'discovery' ? 'skipped' : 'invalid';
249
+ result.valid = false;
250
+ result.discoverable = false;
251
+ return result;
252
+ }
253
+ const result = validateAgentSkillContent(fs.readFileSync(resolved, 'utf8'), {
254
+ ...options,
255
+ file: resolved,
256
+ directoryName: options.directoryName ?? path.basename(path.dirname(resolved)),
257
+ skillRoot: path.dirname(resolved),
258
+ checkResources: true,
259
+ });
260
+ if (path.basename(resolved) !== 'SKILL.md') {
261
+ result.diagnostics.push(diagnostic('AS_SKILL_FILENAME', 'error', resolved, '$', 'the skill entrypoint must be named SKILL.md', 'Rename the entrypoint to SKILL.md.'));
262
+ sortDiagnostics(result.diagnostics);
263
+ result.state = resultState(result.diagnostics, result.profile);
264
+ result.valid = false;
265
+ }
266
+ return result;
267
+ }
268
+ function collectSkillFiles(targetPath, recursive, files) {
269
+ const resolved = path.resolve(targetPath);
270
+ if (!fs.existsSync(resolved))
271
+ return;
272
+ const stat = fs.lstatSync(resolved);
273
+ if (stat.isSymbolicLink())
274
+ return;
275
+ if (stat.isFile()) {
276
+ if (path.basename(resolved) === 'SKILL.md')
277
+ files.add(resolved);
278
+ return;
279
+ }
280
+ if (!stat.isDirectory())
281
+ return;
282
+ const direct = path.join(resolved, 'SKILL.md');
283
+ if (fs.existsSync(direct) && !fs.lstatSync(direct).isSymbolicLink()) {
284
+ files.add(direct);
285
+ }
286
+ if (!recursive)
287
+ return;
288
+ for (const entry of fs.readdirSync(resolved, { withFileTypes: true })
289
+ .sort((left, right) => left.name.localeCompare(right.name))) {
290
+ if (!entry.isDirectory()
291
+ || entry.isSymbolicLink()
292
+ || entry.name.startsWith('.')
293
+ || entry.name === 'node_modules') {
294
+ continue;
295
+ }
296
+ collectSkillFiles(path.join(resolved, entry.name), true, files);
297
+ }
298
+ }
299
+ export function scanAgentSkillPaths(targetPaths, options = {}) {
300
+ const profile = options.profile ?? 'compatible';
301
+ const files = new Set();
302
+ for (const target of typeof targetPaths === 'string' ? [targetPaths] : targetPaths) {
303
+ collectSkillFiles(target, options.recursive ?? true, files);
304
+ }
305
+ const results = [...files]
306
+ .sort((left, right) => left.localeCompare(right))
307
+ .map((file) => validateAgentSkillFile(file, { profile }));
308
+ return {
309
+ schemaVersion: 1,
310
+ profile,
311
+ files: results,
312
+ summary: {
313
+ scanned: results.length,
314
+ valid: results.filter((result) => result.state === 'valid').length,
315
+ warnings: results.filter((result) => result.state === 'warning').length,
316
+ invalid: results.filter((result) => result.state === 'invalid').length,
317
+ skipped: results.filter((result) => result.state === 'skipped').length,
318
+ errors: results.reduce((count, result) => count
319
+ + result.diagnostics.filter((item) => item.severity === 'error').length, 0),
320
+ },
321
+ };
322
+ }
323
+ //# sourceMappingURL=validator.js.map
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { promises as fs } from 'node:fs';
18
18
  import * as path from 'node:path';
19
+ import { projectAiwgPath } from '../../config/project-artifacts.js';
19
20
  import { buildParallelismSection, replaceOrAppendParallelismBlock } from './parallelism-section.js';
20
21
  import { buildContextFinalizationBlock, replaceOrAppendFinalizationBlock } from './finalization.js';
21
22
  import { buildExternalLinksSection, replaceOrAppendExternalLinksBlock, } from './external-links-section.js';
@@ -78,13 +79,16 @@ export async function buildAiwgMdContent(projectPath, stagedClaudeMdContent) {
78
79
  return replaceOrAppendExternalLinksBlock(withFinalization, externalLinksSection);
79
80
  }
80
81
  // Fallback stub.
82
+ const normalizedPath = projectAiwgPath(projectPath, 'AIWG.md');
83
+ const normalizedRelative = path.relative(projectPath, normalizedPath).replace(/\\/g, '/');
84
+ const normalizedLink = normalizedRelative.startsWith('.') ? normalizedRelative : `./${normalizedRelative}`;
81
85
  const stub = [
82
86
  '# AIWG.md',
83
87
  AIWG_SIGNATURE_COMMENT,
84
88
  '<!-- CLAUDE.md companion for non-Claude providers. -->',
85
89
  '',
86
90
  'CLAUDE.md was not found at project root. AIWG.md normally mirrors that content.',
87
- 'See [.aiwg/AIWG.md](./.aiwg/AIWG.md) for the project framework context.',
91
+ `See [${normalizedRelative}](${normalizedLink}) for the project framework context.`,
88
92
  '',
89
93
  ].join('\n');
90
94
  const withParallelism = replaceOrAppendParallelismBlock(stub, parallelismSection);
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import * as fs from 'fs/promises';
23
23
  import * as path from 'path';
24
- import { buildProviderBootstrapBlock } from './workspace-context.js';
24
+ import { buildProviderBootstrapBlock, PROVIDER_BOOTSTRAP_START, PROVIDER_BOOTSTRAP_END, } from './workspace-context.js';
25
25
  export const CLAUDE_HOOK_START = '<!-- AIWG:claude-md-hook:start -->';
26
26
  export const CLAUDE_HOOK_END = '<!-- AIWG:claude-md-hook:end -->';
27
27
  /**
@@ -76,6 +76,26 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
76
76
  const endIdx = existing.indexOf(CLAUDE_HOOK_END);
77
77
  // Case 2: marker block does not exist — append the block to end of file.
78
78
  if (startIdx === -1 && endIdx === -1) {
79
+ const bootstrapStarts = existing.split(PROVIDER_BOOTSTRAP_START).length - 1;
80
+ const bootstrapEnds = existing.split(PROVIDER_BOOTSTRAP_END).length - 1;
81
+ // Current and legacy AIWG initialization may have emitted the provider
82
+ // bootstrap directly. Migrate that exact managed block in place instead
83
+ // of appending a hook containing a second copy (#1867).
84
+ if (bootstrapStarts === 1 && bootstrapEnds === 1) {
85
+ const bootstrapStartIdx = existing.indexOf(PROVIDER_BOOTSTRAP_START);
86
+ const bootstrapEndIdx = existing.indexOf(PROVIDER_BOOTSTRAP_END, bootstrapStartIdx)
87
+ + PROVIDER_BOOTSTRAP_END.length;
88
+ const updated = existing.substring(0, bootstrapStartIdx)
89
+ + block
90
+ + existing.substring(bootstrapEndIdx);
91
+ await fs.writeFile(claudeMdPath, updated, 'utf8');
92
+ result.action = 'updated';
93
+ return result;
94
+ }
95
+ if (bootstrapStarts !== bootstrapEnds || bootstrapStarts > 1) {
96
+ result.warnings.push('CLAUDE.md has malformed or duplicate AIWG provider-bootstrap markers; refusing to append a second managed bootstrap.');
97
+ return result;
98
+ }
79
99
  // Ensure the file ends with a single newline before appending.
80
100
  const trimmed = existing.replace(/\n+$/, '\n');
81
101
  const updated = `${trimmed}\n${block}\n`;
@@ -62,6 +62,7 @@ export async function buildContextFinalizationBlock(projectPath) {
62
62
  const installedNames = installed.map(([name]) => name);
63
63
  const providerDeployments = new Set();
64
64
  const normalizedAiwgMdPath = displayProjectPath(projectPath, projectAiwgPath(projectPath, 'AIWG.md'));
65
+ const normalizedAiwgMdLabel = `\`${normalizedAiwgMdPath}\``;
65
66
  for (const [, entry] of installed) {
66
67
  for (const provider of Object.keys(entry.deployedTo ?? {})) {
67
68
  providerDeployments.add(provider);
@@ -94,9 +95,9 @@ export async function buildContextFinalizationBlock(projectPath) {
94
95
  '',
95
96
  '### Source Model',
96
97
  '',
97
- '- `.aiwg/AIWG.md` is the normalized project-local context entry point.',
98
+ `- ${normalizedAiwgMdLabel} is the normalized project-local context entry point.`,
98
99
  '- Root `AIWG.md` is the generated cross-provider companion loaded through `AGENTS.md` and provider twins.',
99
- '- `AGENTS.md`, `WARP.md`, `.hermes.md`, and `.github/copilot-instructions.md` are provider-facing bridges, not replacements for `.aiwg/AIWG.md`.',
100
+ `- \`AGENTS.md\`, \`WARP.md\`, \`.hermes.md\`, and \`.github/copilot-instructions.md\` are provider-facing bridges, not replacements for ${normalizedAiwgMdLabel}.`,
100
101
  FINALIZATION_END,
101
102
  '',
102
103
  ];
@@ -113,6 +114,7 @@ export function replaceOrAppendFinalizationBlock(content, block) {
113
114
  export async function buildNormalizedAiwgMd(projectPath, existing = '') {
114
115
  const block = await buildContextFinalizationBlock(projectPath);
115
116
  const externalLinksSection = await buildExternalLinksSection(projectPath);
117
+ const normalizedAiwgMdPath = displayProjectPath(projectPath, projectAiwgPath(projectPath, 'AIWG.md'));
116
118
  const base = existing.trim().length > 0
117
119
  ? existing
118
120
  : [
@@ -120,7 +122,7 @@ export async function buildNormalizedAiwgMd(projectPath, existing = '') {
120
122
  AIWG_SIGNATURE_COMMENT,
121
123
  '<!-- Normalized project-local AIWG context. Operator notes may live outside AIWG-managed blocks. -->',
122
124
  '',
123
- 'This file is the stable `.aiwg/AIWG.md` entry point for AIWG skills, rules, and generated provider context.',
125
+ `This file is the stable \`${normalizedAiwgMdPath}\` entry point for AIWG skills, rules, and generated provider context.`,
124
126
  '',
125
127
  ].join('\n');
126
128
  const signed = base.includes(AIWG_SIGNATURE_COMMENT)
@@ -21,6 +21,7 @@ import { shouldEmitAgentsMd, shouldEmitAiwgMd, shouldEmitClaudeMdHook } from './
21
21
  import { ensureClaudeMdHook } from './claude-hook.js';
22
22
  import { ensureManagedHook } from './managed-hook.js';
23
23
  import { buildProviderBootstrapBlock, ensureWorkspaceContext, registerProviderContext, } from './workspace-context.js';
24
+ import { projectAiwgPath } from '../../config/project-artifacts.js';
24
25
  const SECTION_TITLES = {
25
26
  agents: 'Agents',
26
27
  rules: 'Rules',
@@ -136,7 +137,9 @@ export async function buildAgentsMd(opts) {
136
137
  parts.push('');
137
138
  parts.push('See [AIWG.md](./AIWG.md) for the full AIWG framework context');
138
139
  parts.push('(active frameworks, addons, agents, behaviors, rules).');
139
- parts.push('Tracker and delivery source of truth: [.aiwg/aiwg.config](./.aiwg/aiwg.config).');
140
+ const configRelative = path.relative(opts.projectPath, projectAiwgPath(opts.projectPath, 'aiwg.config')).replace(/\\/g, '/');
141
+ const configLink = configRelative.startsWith('.') ? configRelative : `./${configRelative}`;
142
+ parts.push(`Tracker and delivery source of truth: [${configRelative}](${configLink}).`);
140
143
  parts.push('');
141
144
  parts.push('Deployed artifacts live under your provider\'s native directory');
142
145
  parts.push('(for example `.codex/agents/`, `.warp/agents/`, `.github/agents/`).');
@@ -16,6 +16,27 @@
16
16
  import { readAiwgConfig, resolveParallelism, PROVIDER_PARALLELISM_DEFAULTS } from '../../config/aiwg-config.js';
17
17
  export const PARALLELISM_BLOCK_START = '<!-- AIWG-PARALLELISM-CAP:START -->';
18
18
  export const PARALLELISM_BLOCK_END = '<!-- AIWG-PARALLELISM-CAP:END -->';
19
+ function delegationSupport(provider) {
20
+ switch (provider) {
21
+ case 'claude':
22
+ case 'codex':
23
+ case 'copilot':
24
+ case 'cursor':
25
+ case 'factory':
26
+ case 'opencode':
27
+ case 'openclaw':
28
+ return `**Provider behavior (${provider})**: native custom subagents can select the deployed model-worker wrapper. Verify the resolved model when provider or account policy may substitute it.`;
29
+ case 'openhuman':
30
+ return '**Provider behavior (openhuman)**: wrapper roles compile to OpenHuman agent definitions; exact pins validate while semantic hints may be routed dynamically.';
31
+ case 'warp':
32
+ case 'hermes':
33
+ return `**Provider behavior (${provider})**: delegation is available, but model selection is global/run-scoped. Use the wrapper rubric for task selection while reporting that heterogeneous per-worker pinning is not enforced.`;
34
+ case 'windsurf':
35
+ return '**Provider behavior (windsurf)**: portable subagent model selection is unsupported. Apply the decomposition rubric in the primary agent and do not claim wrapper pinning or parallel delegation.';
36
+ default:
37
+ return '**Provider behavior (unknown)**: delegation and per-worker model selection are unverified. Apply the rubric only where the active provider exposes those capabilities, and state any fallback explicitly.';
38
+ }
39
+ }
19
40
  /**
20
41
  * Build the parallelism-cap markdown section for injection into context files.
21
42
  * Returns `''` when injection should be skipped:
@@ -61,7 +82,19 @@ export async function buildParallelismSection(projectPath) {
61
82
  lines.push(`*Rationale*: ${resolved.rationale}`);
62
83
  }
63
84
  lines.push('');
64
- lines.push('When spawning parallel subagents, take the MIN of: this cap, `AIWG_CONTEXT_WINDOW` budget, the RLM 7-agent hard cap (RLM dispatches only), and the natural task decomposition. Bump via `aiwg config set --project parallelism.max_parallel_subagents N`.');
85
+ lines.push('### Model-selected delegation rubric');
86
+ lines.push('');
87
+ lines.push('For each non-trivial task, assess whether it contains independent, bounded subtasks that can run concurrently. When delegation is supported, prefer the deployed model-pinned wrappers by task characteristics and consequence:');
88
+ lines.push('');
89
+ lines.push('- `aiwg-model-efficiency-worker`: discovery, inventory, focused edits, and other bounded low-cost work.');
90
+ lines.push('- `aiwg-model-coding-worker`: implementation, tests, debugging, and routine technical delivery.');
91
+ lines.push('- `aiwg-model-reasoning-worker`: architecture, synthesis, difficult analysis, and high-consequence review.');
92
+ lines.push('');
93
+ lines.push('Do not delegate trivial work, tightly coupled changes, serial dependencies, or tasks likely to collide in shared state; also keep work local when coordination costs exceed the expected benefit. Parallelize only independent work, and take the MIN of provider limits, `max_parallel_subagents`, `AIWG_CONTEXT_WINDOW` budget, framework-specific caps (including the RLM 7-agent hard cap for RLM dispatches), and natural task decomposition. Bump the project cap via `aiwg config set --project parallelism.max_parallel_subagents N`.');
94
+ lines.push('');
95
+ lines.push('The primary agent retains orchestration, final integration, conflict resolution, validation, and user-facing accountability.');
96
+ lines.push('');
97
+ lines.push(delegationSupport(primary));
65
98
  lines.push('');
66
99
  lines.push(PARALLELISM_BLOCK_END);
67
100
  lines.push('');
@@ -384,7 +384,15 @@ export async function ensureWorkspaceContext(projectPath, options = {}) {
384
384
  }
385
385
  const block = buildWorkspaceManagedBlock(projectPath, options.providerFiles);
386
386
  if (existing.includes(WORKSPACE_MANAGED_START) || existing.includes(WORKSPACE_MANAGED_END)) {
387
- const updated = replaceBlock(existing, WORKSPACE_MANAGED_START, WORKSPACE_MANAGED_END, block);
387
+ let updated = replaceBlock(existing, WORKSPACE_MANAGED_START, WORKSPACE_MANAGED_END, block);
388
+ // A managed project-extraction block explicitly points operators back to
389
+ // README/package metadata as its source. Refresh that block during normal
390
+ // regeneration so the documented edit-source-then-regenerate contract is
391
+ // true (#1866). Workspaces without an extraction remain opt-in.
392
+ if (updated.includes(PROJECT_EXTRACTION_START) || updated.includes(PROJECT_EXTRACTION_END)) {
393
+ const extracted = await extractExistingProjectContext(projectPath);
394
+ updated = replaceBlock(updated, PROJECT_EXTRACTION_START, PROJECT_EXTRACTION_END, extracted.content);
395
+ }
388
396
  if (updated === existing)
389
397
  return { path: workspacePath, action: 'unchanged', warnings };
390
398
  await atomicWrite(workspacePath, updated);
@@ -512,6 +520,7 @@ function sensitiveEvidence(content) {
512
520
  async function nestedInstructionFiles(projectPath) {
513
521
  const found = [];
514
522
  const ignored = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.aiwg']);
523
+ const artifactRoot = path.resolve(resolveProjectAiwgDir(projectPath));
515
524
  async function walk(directory) {
516
525
  let entries;
517
526
  try {
@@ -522,8 +531,11 @@ async function nestedInstructionFiles(projectPath) {
522
531
  }
523
532
  for (const entry of entries) {
524
533
  if (entry.isDirectory()) {
525
- if (!ignored.has(entry.name) && !entry.name.startsWith('.context-migration-'))
526
- await walk(path.join(directory, entry.name));
534
+ const child = path.join(directory, entry.name);
535
+ if (!ignored.has(entry.name)
536
+ && !entry.name.startsWith('.context-migration-')
537
+ && path.resolve(child) !== artifactRoot)
538
+ await walk(child);
527
539
  }
528
540
  else if (['AGENTS.md', 'CLAUDE.md', 'WARP.md'].includes(entry.name)) {
529
541
  const relative = path.relative(projectPath, path.join(directory, entry.name)).replace(/\\/g, '/');
@@ -12,6 +12,7 @@ import { analyzeCLI } from './analyzers/cli-analyzer.js';
12
12
  import { generateServer, updateRegistry } from './generator.js';
13
13
  import path from 'path';
14
14
  import { fileURLToPath } from 'url';
15
+ import { projectAiwgPath } from '../../config/project-artifacts.js';
15
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
17
  async function main() {
17
18
  console.log('MCPsmith Example: Generating Git MCP Server\n');
@@ -28,7 +29,8 @@ async function main() {
28
29
  console.log();
29
30
  // Step 2: Generate the MCP server
30
31
  console.log('Step 2: Generating MCP server...');
31
- const outputDir = path.join(__dirname, '..', '..', '..', '.aiwg', 'smiths', 'mcpsmith', 'servers');
32
+ const projectRoot = path.resolve(__dirname, '..', '..', '..');
33
+ const outputDir = projectAiwgPath(projectRoot, 'smiths', 'mcpsmith', 'servers');
32
34
  const generatedServer = await generateServer({
33
35
  serverId: 'git',
34
36
  serverName: 'Git CLI MCP Server',
@@ -10,6 +10,7 @@ import fs from 'fs/promises';
10
10
  import path from 'path';
11
11
  import { fileURLToPath } from 'url';
12
12
  import crypto from 'crypto';
13
+ import { projectAiwgPath } from '../../config/project-artifacts.js';
13
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
15
  /**
15
16
  * Generate MCP server from analyzer result
@@ -174,7 +175,8 @@ function createManifest(serverId, serverName, analyzerResult, config) {
174
175
  */
175
176
  async function generateServerCode(serverId, serverName, analyzerResult) {
176
177
  // Load template
177
- const templatePath = path.join(path.dirname(__dirname), '..', '..', '.aiwg', 'smiths', 'mcpsmith', 'templates', 'server-template.mjs');
178
+ const projectRoot = path.resolve(path.dirname(__dirname), '..', '..');
179
+ const templatePath = projectAiwgPath(projectRoot, 'smiths', 'mcpsmith', 'templates', 'server-template.mjs');
178
180
  let template = await fs.readFile(templatePath, 'utf-8');
179
181
  // Replace template variables
180
182
  const now = new Date().toISOString();
@@ -13,6 +13,7 @@ import { existsSync, constants } from 'fs';
13
13
  import { resolve, join, dirname } from 'path';
14
14
  import { platform, arch, homedir, tmpdir, cpus, totalmem, freemem } from 'os';
15
15
  import { fileURLToPath } from 'url';
16
+ import { projectAiwgPath } from '../../config/project-artifacts-runtime.mjs';
16
17
 
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = dirname(__filename);
@@ -22,7 +23,7 @@ const __dirname = dirname(__filename);
22
23
  */
23
24
  export class RuntimeDiscovery {
24
25
  constructor(basePath = null) {
25
- this.basePath = basePath || resolve(process.cwd(), '.aiwg/smiths/toolsmith');
26
+ this.basePath = basePath || projectAiwgPath(process.cwd(), 'smiths', 'toolsmith');
26
27
  this.knownTools = this.#loadKnownTools();
27
28
  }
28
29
 
@@ -18,6 +18,7 @@ import { existsSync } from 'fs';
18
18
  import { mkdir, readFile, writeFile, appendFile } from 'fs/promises';
19
19
  import { dirname, join, resolve as resolvePath } from 'path';
20
20
  import { BACKEND_TYPES, FilesystemAdapter, ObsidianAdapter, LogseqAdapter, FortemiAdapter, SUBSYSTEM_KEYS, getLoadedConfig, initStorage, resolveStorage, resolveSubsystemRoot, storageConfigPath, } from './index.js';
21
+ import { projectAiwgPath, resolveProjectAiwgDir } from '../config/project-artifacts.js';
21
22
  export async function main(args) {
22
23
  const subcommand = args[0];
23
24
  const subArgs = args.slice(1);
@@ -47,7 +48,7 @@ async function handleShow(projectRoot) {
47
48
  const config = await getLoadedConfig(projectRoot);
48
49
  const cfgPath = storageConfigPath(projectRoot);
49
50
  if (!config) {
50
- console.log(`No storage.config — every subsystem uses the default fs backend under .aiwg/.\n`);
51
+ console.log(`No storage.config — every subsystem uses the default fs backend under ${resolveProjectAiwgDir(projectRoot)}.\n`);
51
52
  console.log(`Expected location (when configured): ${cfgPath}\n`);
52
53
  }
53
54
  else {
@@ -178,7 +179,7 @@ async function handleMigrate(projectRoot, args) {
178
179
  return;
179
180
  }
180
181
  // Resume support: read the per-migration log; skip already-done paths
181
- const migrationLogPath = resolvePath(projectRoot, '.aiwg', '.storage-cache', 'migrations', `${opts.subsystem}-${specSlug(opts.from)}-to-${specSlug(opts.to)}.jsonl`);
182
+ const migrationLogPath = projectAiwgPath(projectRoot, '.storage-cache', 'migrations', `${opts.subsystem}-${specSlug(opts.from)}-to-${specSlug(opts.to)}.jsonl`);
182
183
  const completed = await readCompletedSet(migrationLogPath);
183
184
  let copied = 0;
184
185
  let skipped = 0;