@aiwg/cli 2026.8.7 → 2026.8.8

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.
@@ -6,10 +6,24 @@ import { syncFortemiCoreIndex } from './fortemi-core-sync.js';
6
6
  import { PROJECT_AIWG_LOCATION_FILE, expandProjectArtifactPath, resolveProjectAiwgDir, } from '../config/project-artifacts.js';
7
7
  const GITIGNORE_BLOCK = [
8
8
  '',
9
- '# AIWG artifact root pointer (local/private path)',
9
+ '# AIWG external artifact corpus (retain the project-local control plane)',
10
+ '!.aiwg/',
11
+ '.aiwg/*',
12
+ '!.aiwg/AIWG.md',
13
+ '!.aiwg/aiwg.config',
14
+ '!.aiwg/frameworks/',
15
+ '.aiwg/frameworks/*',
16
+ '!.aiwg/frameworks/registry.json',
17
+ '',
18
+ '# AIWG artifact root pointer (machine-local/private path)',
10
19
  PROJECT_AIWG_LOCATION_FILE,
11
20
  '',
12
21
  ].join('\n');
22
+ const LOCAL_CONTROL_PLANE_FILES = [
23
+ 'AIWG.md',
24
+ 'aiwg.config',
25
+ path.join('frameworks', 'registry.json'),
26
+ ];
13
27
  async function exists(filePath) {
14
28
  try {
15
29
  await access(filePath);
@@ -49,7 +63,8 @@ async function ensureGitignorePointer(projectDir, dryRun) {
49
63
  throw error;
50
64
  }
51
65
  const lines = current.split(/\r?\n/).map(line => line.trim());
52
- if (lines.includes(PROJECT_AIWG_LOCATION_FILE))
66
+ const required = GITIGNORE_BLOCK.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
67
+ if (required.every(line => lines.includes(line)))
53
68
  return false;
54
69
  if (!dryRun) {
55
70
  const separator = current.length === 0 || current.endsWith('\n') ? '' : '\n';
@@ -57,6 +72,28 @@ async function ensureGitignorePointer(projectDir, dryRun) {
57
72
  }
58
73
  return true;
59
74
  }
75
+ async function materializeLocalControlPlane(projectDir, artifactRoot, dryRun) {
76
+ const localRoot = path.join(projectDir, '.aiwg');
77
+ for (const relativePath of LOCAL_CONTROL_PLANE_FILES) {
78
+ const sourcePath = path.join(artifactRoot, relativePath);
79
+ if (!(await exists(sourcePath)))
80
+ continue;
81
+ const destinationPath = path.join(localRoot, relativePath);
82
+ const sourceContent = await readFile(sourcePath);
83
+ if (await exists(destinationPath)) {
84
+ const destinationContent = await readFile(destinationPath);
85
+ if (!sourceContent.equals(destinationContent)) {
86
+ throw new Error(`Local AIWG control-plane file differs from the external artifact root: ${destinationPath}. `
87
+ + 'Reconcile the files before attaching the corpus.');
88
+ }
89
+ continue;
90
+ }
91
+ if (!dryRun) {
92
+ await mkdir(path.dirname(destinationPath), { recursive: true });
93
+ await writeFile(destinationPath, sourceContent);
94
+ }
95
+ }
96
+ }
60
97
  async function writePointer(projectDir, pointerValue, dryRun) {
61
98
  const pointerPath = path.join(projectDir, PROJECT_AIWG_LOCATION_FILE);
62
99
  if (!dryRun) {
@@ -134,6 +171,7 @@ export async function moveProjectArtifacts(options) {
134
171
  if (!dryRun && !attach) {
135
172
  await moveDirectory(source, destination);
136
173
  }
174
+ await materializeLocalControlPlane(projectDir, destination, dryRun);
137
175
  await writePointer(projectDir, pointerValue, dryRun);
138
176
  let reindexed = false;
139
177
  let fortemiSynced = false;
@@ -26,6 +26,7 @@ import { generate as generateContextFiles, discoverDeployedArtifacts, shouldEmit
26
26
  import { resolveActiveProvider } from '../provider-resolution.js';
27
27
  import { getProviderContextDiscoveryPathStrings } from '../../providers/provider-definitions.js';
28
28
  import { projectAiwgPath } from '../../config/project-artifacts.js';
29
+ import { selectRegenerateBranch } from '../regenerate-selector.js';
29
30
  async function handleRegenerate(args, cwd) {
30
31
  if (args.includes('--help') || args.includes('-h')) {
31
32
  console.log(`
@@ -40,7 +41,8 @@ async function handleRegenerate(args, cwd) {
40
41
  or commands — use 'aiwg refresh' for that.
41
42
 
42
43
  Options:
43
- --workspace Canonical WORKSPACE.md AIWG.md graph (default)
44
+ (no branch flag) Intelligently select workspace refresh or adoption preview
45
+ --workspace Explicit canonical WORKSPACE.md → AIWG.md graph
44
46
  --existing-project Transactionally extract an established project into WORKSPACE.md
45
47
  --legacy, --full-inject Legacy inline compatibility branch
46
48
  --apply Apply --existing-project after its mandatory preflight
@@ -57,6 +59,7 @@ async function handleRegenerate(args, cwd) {
57
59
  aiwg regenerate --workspace
58
60
  aiwg regenerate --existing-project --dry-run
59
61
  aiwg regenerate --existing-project --apply
62
+ aiwg regenerate --apply
60
63
  aiwg regenerate --full-inject
61
64
  aiwg regenerate --dry-run
62
65
  aiwg regenerate --provider codex
@@ -69,9 +72,9 @@ async function handleRegenerate(args, cwd) {
69
72
  const skipAiwgMd = args.includes('--no-aiwg-md');
70
73
  const skipAgentsMd = args.includes('--no-agents-md');
71
74
  const skipWorkspaceMd = args.includes('--no-workspace-md');
72
- const legacy = args.includes('--legacy') || args.includes('--full-inject');
73
- const workspace = args.includes('--workspace');
74
- const existingProject = args.includes('--existing-project');
75
+ const requestedLegacy = args.includes('--legacy') || args.includes('--full-inject');
76
+ const requestedWorkspace = args.includes('--workspace');
77
+ const requestedExistingProject = args.includes('--existing-project');
75
78
  const apply = args.includes('--apply');
76
79
  const valueFlags = new Set(['--provider']);
77
80
  const booleanFlags = new Set([
@@ -95,23 +98,27 @@ async function handleRegenerate(args, cwd) {
95
98
  });
96
99
  }
97
100
  }
98
- const selectedBranches = Number(legacy) + Number(workspace) + Number(existingProject);
101
+ const selectedBranches = Number(requestedLegacy) + Number(requestedWorkspace) + Number(requestedExistingProject);
99
102
  if (selectedBranches > 1)
100
103
  throw new AiwgError({
101
104
  code: 'ERR_USAGE_CONFLICTING_FLAGS',
102
105
  message: 'Choose exactly one regenerate branch: --workspace, --existing-project, or --full-inject.',
103
106
  exitCode: EXIT_CODES.USAGE,
104
107
  });
105
- if (apply && !existingProject)
108
+ if (dryRun && apply)
106
109
  throw new AiwgError({
107
110
  code: 'ERR_USAGE_CONFLICTING_FLAGS',
108
- message: '--apply is only valid with --existing-project.',
111
+ message: 'Choose either --dry-run or --apply.',
109
112
  exitCode: EXIT_CODES.USAGE,
110
113
  });
111
- if (existingProject && dryRun && apply)
114
+ const selection = await selectRegenerateBranch(cwd, args);
115
+ const legacy = selection.branch === 'legacy';
116
+ const existingProject = selection.branch === 'existing-project';
117
+ if (apply && !existingProject)
112
118
  throw new AiwgError({
113
119
  code: 'ERR_USAGE_CONFLICTING_FLAGS',
114
- message: 'Choose either --dry-run or --apply for --existing-project.',
120
+ message: '--apply is only valid when the existing-project branch is selected.',
121
+ hint: 'Use `aiwg regenerate --existing-project --apply`, or run without --apply to inspect the selected branch.',
115
122
  exitCode: EXIT_CODES.USAGE,
116
123
  });
117
124
  if (existingProject && (force || skipAiwgMd || skipAgentsMd || skipWorkspaceMd))
@@ -140,6 +147,9 @@ async function handleRegenerate(args, cwd) {
140
147
  console.log(` Provider: ${provider}`);
141
148
  console.log(` Target: ${target}`);
142
149
  console.log(` Branch: ${legacy ? 'legacy full injection' : existingProject ? 'canonical existing-project extraction' : 'canonical workspace graph'}`);
150
+ console.log(` Selected: ${selection.explicit ? 'explicit' : 'inferred'} — ${selection.reason}`);
151
+ if (selection.evidence.length > 0)
152
+ console.log(` Evidence: ${selection.evidence.join(', ')}`);
143
153
  if (existingProject) {
144
154
  const preflight = await migrateWorkspaceContext(target, {
145
155
  dryRun: true,
@@ -1203,15 +1203,16 @@ function projectRootCandidate(start) {
1203
1203
  catch {
1204
1204
  current = resolve(start);
1205
1205
  }
1206
- let gitRoot = null;
1207
1206
  while (true) {
1208
1207
  if (existsSync(resolve(current, '.aiwg', 'aiwg.config')))
1209
1208
  return current;
1210
- if (!gitRoot && existsSync(resolve(current, '.git')))
1211
- gitRoot = current;
1209
+ // A repository root is the project boundary. Do not let an unrelated
1210
+ // ancestor workspace configuration capture session catalog reads.
1211
+ if (existsSync(resolve(current, '.git')))
1212
+ return current;
1212
1213
  const parent = dirname(current);
1213
1214
  if (parent === current)
1214
- return gitRoot;
1215
+ return null;
1215
1216
  current = parent;
1216
1217
  }
1217
1218
  }
@@ -47,6 +47,8 @@ export const quickrefHandler = {
47
47
  console.log(`${verb} ${result.skillName}`);
48
48
  console.log(` Source: ${result.sourcePath}`);
49
49
  console.log(` Output: ${result.outputPath}`);
50
+ for (const warning of result.warnings)
51
+ console.log(` Warning: ${warning}`);
50
52
  if (ctx.dryRun) {
51
53
  console.log('\n--- preview ---\n');
52
54
  console.log(result.content);
@@ -850,6 +852,26 @@ export const promoteHandler = {
850
852
  console.log(`✓ Promoted '${positional}' → ${result.plan?.destination}`);
851
853
  if (cleanup) {
852
854
  console.log(' Source removed from .aiwg/');
855
+ try {
856
+ const { deployProjectQuickref, generateProjectQuickref, hasProjectQuickref } = await import('../../extensions/project-quickref.js');
857
+ if (await hasProjectQuickref(projectDir)) {
858
+ if (config.providers.length > 0) {
859
+ for (const provider of config.providers)
860
+ await deployProjectQuickref(projectDir, provider);
861
+ console.log(` Managed project quickref refreshed for: ${config.providers.join(', ')}.`);
862
+ }
863
+ else {
864
+ await generateProjectQuickref(projectDir);
865
+ console.log(' Managed project quickref generated; no providers are configured for deployment.');
866
+ }
867
+ }
868
+ else {
869
+ console.log(' Managed project quickref is now empty; run `aiwg doctor --project-local` to inspect deployed stale copies.');
870
+ }
871
+ }
872
+ catch (error) {
873
+ console.log(` Managed project quickref refresh failed: ${error.message}`);
874
+ }
853
875
  }
854
876
  return { exitCode: 0 };
855
877
  }
@@ -956,6 +978,14 @@ export const newBundleHandler = {
956
978
  catch {
957
979
  // .gitignore management is best-effort; don't fail the scaffold
958
980
  }
981
+ try {
982
+ const { generateProjectQuickref } = await import('../../extensions/project-quickref.js');
983
+ await generateProjectQuickref(ctx.cwd);
984
+ console.log(' → Managed project quickref refreshed from discovered capabilities.');
985
+ }
986
+ catch (error) {
987
+ console.log(` → Managed project quickref refresh deferred: ${error.message}`);
988
+ }
959
989
  // #1235 / #1758 — auto-rebuild the project graph and refresh the
960
990
  // Fortemi Core static cache so the new bundle is immediately
961
991
  // discoverable via top-level `aiwg discover` / `aiwg show`.
@@ -1079,18 +1079,17 @@ async function deployProjectLocalBundles(opts) {
1079
1079
  : discovery.bundles.filter(b => b.type !== 'provider');
1080
1080
  if (targetBundles.length === 0) {
1081
1081
  if (!onlyBundleId) {
1082
- const { loadProjectQuickref, deployProjectQuickref } = await import('../../extensions/project-quickref.js');
1083
- const quickref = await loadProjectQuickref(projectDir);
1084
- if (quickref.exists) {
1085
- try {
1082
+ const { hasProjectQuickref, deployProjectQuickref } = await import('../../extensions/project-quickref.js');
1083
+ try {
1084
+ if (await hasProjectQuickref(projectDir)) {
1086
1085
  await deployProjectQuickref(projectDir, provider, { dryRun });
1087
1086
  if (verbose || dryRun)
1088
1087
  ui.dim(` + project quickref -> ${provider}`);
1089
1088
  }
1090
- catch (error) {
1091
- ui.warn(`Project quickref deployment failed: ${error.message}`);
1092
- return { deployed: 0, failed: 1, bundles: [] };
1093
- }
1089
+ }
1090
+ catch (error) {
1091
+ ui.warn(`Project quickref deployment failed: ${error.message}`);
1092
+ return { deployed: 0, failed: 1, bundles: [] };
1094
1093
  }
1095
1094
  }
1096
1095
  return { deployed: 0, failed: 0, bundles: [] };
@@ -1204,22 +1203,20 @@ async function deployProjectLocalBundles(opts) {
1204
1203
  }
1205
1204
  }
1206
1205
  }
1207
- // A committed `.aiwg/quickref.json` is the canonical orientation source.
1208
- // Refresh its provider kernel copy whenever project-local bundles deploy so
1209
- // `aiwg use <bundle>` keeps the always-visible surface in sync.
1210
- const { loadProjectQuickref, deployProjectQuickref } = await import('../../extensions/project-quickref.js');
1211
- const quickref = await loadProjectQuickref(projectDir);
1212
- if (quickref.exists) {
1213
- try {
1206
+ // Refresh the project kernel quickref from either legacy operator input or
1207
+ // managed project-local discovery whenever bundles deploy.
1208
+ const { hasProjectQuickref, deployProjectQuickref } = await import('../../extensions/project-quickref.js');
1209
+ try {
1210
+ if (await hasProjectQuickref(projectDir)) {
1214
1211
  const quickrefResult = await deployProjectQuickref(projectDir, provider, { dryRun });
1215
1212
  if (verbose || dryRun) {
1216
1213
  ui.dim(` + project quickref -> ${quickrefResult.provider}${quickrefResult.emulated ? ' (emulated)' : ''}`);
1217
1214
  }
1218
1215
  }
1219
- catch (error) {
1220
- failed++;
1221
- ui.warn(`Project quickref deployment failed: ${error.message}`);
1222
- }
1216
+ }
1217
+ catch (error) {
1218
+ failed++;
1219
+ ui.warn(`Project quickref deployment failed: ${error.message}`);
1223
1220
  }
1224
1221
  return { deployed, failed, bundles: targetBundles };
1225
1222
  }
@@ -0,0 +1,94 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { auditWorkspaceContext, PROJECT_EXTRACTION_END, PROJECT_EXTRACTION_START, WORKSPACE_MANAGED_END, WORKSPACE_MANAGED_START, WORKSPACE_OPERATOR_END, WORKSPACE_OPERATOR_START, } from '../smiths/context-pipeline/index.js';
4
+ import { AiwgError, EXIT_CODES } from './errors.js';
5
+ function markerPair(content, start, end) {
6
+ const startIndex = content.indexOf(start);
7
+ const endIndex = content.indexOf(end);
8
+ const present = startIndex >= 0 || endIndex >= 0;
9
+ return {
10
+ present,
11
+ valid: !present || (startIndex >= 0 && endIndex > startIndex
12
+ && content.indexOf(start, startIndex + start.length) < 0
13
+ && content.indexOf(end, endIndex + end.length) < 0),
14
+ };
15
+ }
16
+ function malformedWorkspace(message) {
17
+ throw new AiwgError({
18
+ code: 'ERR_USAGE_REGENERATE_STATE_MALFORMED',
19
+ message,
20
+ hint: 'Repair the managed marker pair or restore WORKSPACE.md from version control, then rerun `aiwg regenerate --dry-run`.',
21
+ exitCode: EXIT_CODES.USAGE,
22
+ });
23
+ }
24
+ async function readOptional(filePath) {
25
+ try {
26
+ return await fs.readFile(filePath, 'utf8');
27
+ }
28
+ catch (error) {
29
+ if (error.code === 'ENOENT')
30
+ return null;
31
+ throw error;
32
+ }
33
+ }
34
+ export async function selectRegenerateBranch(cwd, args) {
35
+ const legacy = args.includes('--legacy') || args.includes('--full-inject');
36
+ const workspace = args.includes('--workspace');
37
+ const existingProject = args.includes('--existing-project');
38
+ if (legacy)
39
+ return { branch: 'legacy', state: 'legacy-context', reason: 'explicit legacy compatibility branch', evidence: ['--legacy/--full-inject'], explicit: true };
40
+ if (workspace)
41
+ return { branch: 'workspace', state: 'fresh', reason: 'explicit canonical workspace branch', evidence: ['--workspace'], explicit: true };
42
+ if (existingProject)
43
+ return { branch: 'existing-project', state: 'established-unextracted', reason: 'explicit existing-project adoption branch', evidence: ['--existing-project'], explicit: true };
44
+ const workspaceContent = await readOptional(path.join(cwd, 'WORKSPACE.md'));
45
+ let workspaceMarkers = null;
46
+ if (workspaceContent !== null) {
47
+ workspaceMarkers = {
48
+ managed: markerPair(workspaceContent, WORKSPACE_MANAGED_START, WORKSPACE_MANAGED_END),
49
+ operator: markerPair(workspaceContent, WORKSPACE_OPERATOR_START, WORKSPACE_OPERATOR_END),
50
+ extraction: markerPair(workspaceContent, PROJECT_EXTRACTION_START, PROJECT_EXTRACTION_END),
51
+ };
52
+ const { managed, operator, extraction } = workspaceMarkers;
53
+ if (!managed.valid || !operator.valid || !extraction.valid) {
54
+ malformedWorkspace('WORKSPACE.md contains an incomplete, duplicated, or out-of-order AIWG managed marker pair.');
55
+ }
56
+ if (extraction.present && !managed.present)
57
+ malformedWorkspace('WORKSPACE.md contains a project-extraction block without the canonical workspace managed graph.');
58
+ if (managed.present !== operator.present)
59
+ malformedWorkspace('WORKSPACE.md contains only part of the canonical managed/operator structure.');
60
+ }
61
+ const audit = await auditWorkspaceContext(cwd);
62
+ const projectSources = audit.plan.projectSources;
63
+ const operatorSources = audit.sources
64
+ .filter((source) => source.path !== 'WORKSPACE.md' && source.operatorContent.trim().length > 0)
65
+ .map((source) => source.path);
66
+ if (workspaceContent !== null) {
67
+ const { managed, extraction } = workspaceMarkers;
68
+ if (managed.present && extraction.present)
69
+ return {
70
+ branch: 'workspace', state: 'adopted', reason: 'canonical workspace already contains an extracted project snapshot', evidence: ['WORKSPACE.md project-extraction marker'], explicit: false,
71
+ };
72
+ if (managed.present && projectSources.length > 0)
73
+ return {
74
+ branch: 'existing-project', state: 'canonical-unextracted', reason: 'canonical workspace exists but stable project metadata has not been adopted', evidence: projectSources, explicit: false,
75
+ };
76
+ if (managed.present)
77
+ return {
78
+ branch: 'workspace', state: 'fresh', reason: 'canonical workspace exists and no stable project sources were detected', evidence: ['WORKSPACE.md managed graph'], explicit: false,
79
+ };
80
+ return {
81
+ branch: 'existing-project', state: 'operator-owned-workspace', reason: 'operator-owned WORKSPACE.md requires transactional adoption before canonical refresh', evidence: ['WORKSPACE.md without AIWG managed markers', ...projectSources], explicit: false,
82
+ };
83
+ }
84
+ if (projectSources.length > 0)
85
+ return {
86
+ branch: 'existing-project', state: 'established-unextracted', reason: 'stable existing-project sources were detected without an extracted workspace snapshot', evidence: projectSources, explicit: false,
87
+ };
88
+ if (audit.legacyCompatible || operatorSources.length > 0)
89
+ return {
90
+ branch: 'existing-project', state: 'legacy-context', reason: 'operator-authored provider context requires transactional adoption', evidence: operatorSources, explicit: false,
91
+ };
92
+ return { branch: 'workspace', state: 'fresh', reason: 'no prior workspace setup or stable project sources were detected', evidence: [], explicit: false };
93
+ }
94
+ //# sourceMappingURL=regenerate-selector.js.map
@@ -54,12 +54,15 @@ export async function buildProjectLocalDoctorSection(opts) {
54
54
  const quickrefAudit = await auditProjectQuickref(projectDir, config?.providers ?? []);
55
55
  const quickrefErrors = [...quickrefAudit.errors];
56
56
  if (quickrefAudit.exists) {
57
- const quickrefRelPath = projectRelativePathIfInside(projectDir, projectAiwgPath(projectDir, 'quickref.json'));
58
- const ignored = quickrefRelPath
59
- ? await checkBundleManifestIgnored(projectDir, quickrefRelPath)
60
- : null;
61
- if (ignored === true && quickrefRelPath) {
62
- quickrefErrors.push(`${quickrefRelPath} is ignored by git; canonical project quickref source must be committed`);
57
+ for (const name of ['quickref.json', 'quickref.config.json']) {
58
+ const sourcePath = projectAiwgPath(projectDir, name);
59
+ const quickrefRelPath = projectRelativePathIfInside(projectDir, sourcePath);
60
+ const ignored = quickrefRelPath
61
+ ? await checkBundleManifestIgnored(projectDir, quickrefRelPath)
62
+ : null;
63
+ if (ignored === true && quickrefRelPath) {
64
+ quickrefErrors.push(`${quickrefRelPath} is ignored by git; operator project quickref input must be committed`);
65
+ }
63
66
  }
64
67
  }
65
68
  // No project-local content → no section at all
@@ -236,6 +239,7 @@ export async function buildProjectLocalDoctorSection(opts) {
236
239
  }
237
240
  lines.push(' Project-local bundle source should be tracked. Add to .gitignore:');
238
241
  lines.push(' !.aiwg/quickref.json');
242
+ lines.push(' !.aiwg/quickref.config.json');
239
243
  lines.push(' !.aiwg/addons/');
240
244
  lines.push(' !.aiwg/extensions/');
241
245
  lines.push(' !.aiwg/frameworks/');
@@ -39,6 +39,7 @@ export const AIWG_GITIGNORE_BLOCK = [
39
39
  AIWG_GITIGNORE_SENTINEL,
40
40
  '!.aiwg/aiwg.config',
41
41
  '!.aiwg/quickref.json',
42
+ '!.aiwg/quickref.config.json',
42
43
  '!.aiwg/addons/',
43
44
  '!.aiwg/extensions/',
44
45
  '!.aiwg/frameworks/',
@@ -120,11 +121,14 @@ export async function appendAiwgSourceTrackBlock(projectDir) {
120
121
  // runs.
121
122
  if (report.hasManagedBlock) {
122
123
  const path = join(projectDir, '.gitignore');
123
- const existing = await readFile(path, 'utf8');
124
- if (!existing.split(/\r?\n/).some(line => line.trim() === '!.aiwg/quickref.json')) {
124
+ let existing = await readFile(path, 'utf8');
125
+ const required = ['!.aiwg/quickref.json', '!.aiwg/quickref.config.json'];
126
+ const missing = required.filter(negation => !existing.split(/\r?\n/).some(line => line.trim() === negation));
127
+ if (missing.length > 0) {
125
128
  const sep = existing.endsWith('\n') ? '' : '\n';
126
- await writeFile(path, `${existing}${sep}!.aiwg/quickref.json\n`, 'utf8');
127
- return { added: true, reason: 'updated managed block to track .aiwg/quickref.json' };
129
+ existing = `${existing}${sep}${missing.join('\n')}\n`;
130
+ await writeFile(path, existing, 'utf8');
131
+ return { added: true, reason: `updated managed block to track ${missing.join(', ')}` };
128
132
  }
129
133
  return { added: false, reason: 'block already present — no change' };
130
134
  }
@@ -7,13 +7,15 @@
7
7
  */
8
8
  import { createHash } from 'crypto';
9
9
  import { access, mkdir, readFile, readdir, rm, writeFile } from 'fs/promises';
10
- import { dirname, isAbsolute, join, resolve } from 'path';
10
+ import { basename, dirname, isAbsolute, join, resolve } from 'path';
11
11
  import { homedir } from 'os';
12
12
  import { z } from 'zod';
13
13
  import { getProviderDefinition, normalizeProviderDefinitionId, } from '../providers/provider-definitions.js';
14
14
  import { OPERATIONAL_SHOW_TYPES } from '../artifacts/types.js';
15
15
  import { projectAiwgPath } from '../config/project-artifacts.js';
16
16
  import { appendAiwgSourceTrackBlock } from './project-local-gitignore.js';
17
+ import { discoverProjectLocalBundles } from './project-local-discovery.js';
18
+ import { enumerateBundleArtifacts } from './shadow-resolver.js';
17
19
  const OWNERSHIP_MARKER = '.aiwg-project-quickref.json';
18
20
  const ShowHintSchema = z.object({
19
21
  type: z.enum(OPERATIONAL_SHOW_TYPES),
@@ -35,6 +37,25 @@ export const ProjectQuickrefSchema = z.object({
35
37
  precedence: z.string().min(1).max(1024),
36
38
  entries: z.array(QuickrefEntrySchema).min(1).max(50),
37
39
  }).strict();
40
+ const QuickrefOverrideSchema = z.object({
41
+ title: z.string().min(1).max(128).optional(),
42
+ summary: z.string().min(1).max(512).optional(),
43
+ discover: z.array(z.string().min(1).max(256)).max(10).optional(),
44
+ show: z.array(ShowHintSchema).max(20).optional(),
45
+ hidden: z.boolean().optional(),
46
+ order: z.number().int().optional(),
47
+ }).strict();
48
+ export const ProjectQuickrefConfigSchema = z.object({
49
+ version: z.literal('1'),
50
+ project: ProjectQuickrefSchema.shape.project.optional(),
51
+ precedence: z.string().min(1).max(1024).optional(),
52
+ entries: z.array(QuickrefEntrySchema).max(50).default([]),
53
+ discovery: z.object({
54
+ enabled: z.boolean().default(true),
55
+ excludeBundles: z.array(z.string().min(1)).max(200).default([]),
56
+ overrides: z.record(z.string(), QuickrefOverrideSchema).default({}),
57
+ }).strict().default({ enabled: true, excludeBundles: [], overrides: {} }),
58
+ }).strict();
38
59
  function sha256(content) {
39
60
  return createHash('sha256').update(content).digest('hex');
40
61
  }
@@ -78,6 +99,158 @@ export async function loadProjectQuickref(projectDir) {
78
99
  return { sourcePath, errors: [`${sourcePath}: invalid JSON: ${error.message}`], exists: true };
79
100
  }
80
101
  }
102
+ function slug(value) {
103
+ const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
104
+ return normalized || 'project';
105
+ }
106
+ async function inferredProject(projectDir) {
107
+ let packageName = basename(resolve(projectDir));
108
+ let description = `Managed project-specific orientation for ${packageName}.`;
109
+ try {
110
+ const parsed = JSON.parse(await readFile(join(projectDir, 'package.json'), 'utf8'));
111
+ if (parsed.name)
112
+ packageName = parsed.name;
113
+ if (parsed.description)
114
+ description = parsed.description.slice(0, 512);
115
+ }
116
+ catch {
117
+ // package metadata is optional
118
+ }
119
+ const id = slug(packageName);
120
+ const name = packageName
121
+ .replace(/^@[^/]+\//, '')
122
+ .split(/[-_]/)
123
+ .filter(Boolean)
124
+ .map(part => part[0]?.toUpperCase() + part.slice(1))
125
+ .join(' ') || 'Project';
126
+ return { id, name, description };
127
+ }
128
+ async function loadManagedConfig(projectDir) {
129
+ const path = projectAiwgPath(projectDir, 'quickref.config.json');
130
+ const raw = await readIfPresent(path);
131
+ if (raw === null)
132
+ return { path };
133
+ let json;
134
+ try {
135
+ json = JSON.parse(raw);
136
+ }
137
+ catch (error) {
138
+ throw new Error(`${path}: invalid JSON: ${error.message}`);
139
+ }
140
+ const parsed = ProjectQuickrefConfigSchema.safeParse(json);
141
+ if (!parsed.success) {
142
+ throw new Error(parsed.error.issues.map(issue => `${path}: ${issue.path.join('.') || '(root)'}: ${issue.message}`).join('\n'));
143
+ }
144
+ return { path, config: parsed.data };
145
+ }
146
+ function dedupe(values) {
147
+ const seen = new Set();
148
+ return values.filter(value => {
149
+ const key = value.trim().toLowerCase();
150
+ if (!key || seen.has(key))
151
+ return false;
152
+ seen.add(key);
153
+ return true;
154
+ });
155
+ }
156
+ function dedupeShow(values) {
157
+ const seen = new Set();
158
+ return values.filter(value => {
159
+ const key = `${value.type}:${value.name.toLowerCase()}`;
160
+ if (seen.has(key))
161
+ return false;
162
+ seen.add(key);
163
+ return true;
164
+ });
165
+ }
166
+ /** Resolve legacy operator input or synthesize a managed definition from project-local bundles. */
167
+ export async function resolveProjectQuickref(projectDir) {
168
+ const legacy = await loadProjectQuickref(projectDir);
169
+ if (legacy.exists) {
170
+ if (!legacy.definition)
171
+ throw new Error(legacy.errors.join('\n'));
172
+ return {
173
+ definition: legacy.definition,
174
+ exists: true,
175
+ sourcePath: legacy.sourcePath,
176
+ provenance: 'legacy',
177
+ warnings: [],
178
+ };
179
+ }
180
+ const managed = await loadManagedConfig(projectDir);
181
+ const discovery = await discoverProjectLocalBundles(projectDir);
182
+ if (discovery.errors.length > 0) {
183
+ throw new Error(discovery.errors.map(error => `${error.path}: ${error.field}: ${error.actual}`).join('\n'));
184
+ }
185
+ if (!managed.config && discovery.bundles.length === 0) {
186
+ return { exists: false, sourcePath: managed.path, provenance: 'managed', warnings: [] };
187
+ }
188
+ const config = managed.config ?? ProjectQuickrefConfigSchema.parse({ version: '1' });
189
+ const excluded = new Set(config.discovery.excludeBundles.map(id => id.toLowerCase()));
190
+ const warnings = [];
191
+ const candidates = [];
192
+ if (config.discovery.enabled) {
193
+ for (const bundle of discovery.bundles) {
194
+ if (excluded.has(bundle.id.toLowerCase()))
195
+ continue;
196
+ const override = config.discovery.overrides[bundle.id];
197
+ if (override?.hidden)
198
+ continue;
199
+ const artifacts = (await enumerateBundleArtifacts(bundle.artifactPath ?? bundle.bundlePath))
200
+ .sort((a, b) => a.type.localeCompare(b.type) || a.id.localeCompare(b.id));
201
+ const inferredShow = dedupeShow(artifacts.map(artifact => ({
202
+ type: artifact.type,
203
+ name: artifact.id,
204
+ }))).slice(0, 8);
205
+ if (artifacts.length > inferredShow.length && inferredShow.length === 8) {
206
+ warnings.push(`${bundle.id}: show hints truncated from ${artifacts.length} artifacts to 8`);
207
+ }
208
+ let discover = override?.discover ?? dedupe([
209
+ bundle.id,
210
+ bundle.manifest.name,
211
+ ...(bundle.manifest.keywords ?? []),
212
+ ]).slice(0, 3);
213
+ const show = override?.show ? dedupeShow(override.show) : inferredShow;
214
+ if (discover.length === 0 && show.length === 0)
215
+ discover = [bundle.id];
216
+ candidates.push({
217
+ order: override?.order ?? 0,
218
+ type: bundle.type,
219
+ id: bundle.id,
220
+ entry: {
221
+ title: override?.title ?? bundle.manifest.name,
222
+ summary: override?.summary ?? bundle.manifest.description,
223
+ discover,
224
+ show,
225
+ },
226
+ });
227
+ }
228
+ }
229
+ candidates.sort((a, b) => a.order - b.order || a.type.localeCompare(b.type) || a.id.localeCompare(b.id));
230
+ const discoveredEntries = candidates.map(candidate => candidate.entry);
231
+ const entries = [...discoveredEntries, ...config.entries];
232
+ if (entries.length > 50)
233
+ warnings.push(`quickref entries truncated from ${entries.length} to 50`);
234
+ const bounded = entries.slice(0, 50);
235
+ if (bounded.length === 0) {
236
+ return { exists: false, sourcePath: managed.path, provenance: 'managed', warnings };
237
+ }
238
+ return {
239
+ definition: {
240
+ version: '1',
241
+ project: config.project ?? await inferredProject(projectDir),
242
+ precedence: config.precedence ?? 'Use project-local capabilities before generic AIWG workflows when they apply.',
243
+ entries: bounded,
244
+ },
245
+ exists: true,
246
+ sourcePath: managed.path,
247
+ provenance: 'managed',
248
+ warnings,
249
+ };
250
+ }
251
+ export async function hasProjectQuickref(projectDir) {
252
+ return (await resolveProjectQuickref(projectDir)).exists;
253
+ }
81
254
  export function renderProjectQuickref(definition) {
82
255
  const skillName = projectQuickrefSkillName(definition.project.id);
83
256
  const lines = [
@@ -111,11 +284,9 @@ export function renderProjectQuickref(definition) {
111
284
  return lines.join('\n');
112
285
  }
113
286
  export async function generateProjectQuickref(projectDir, options = {}) {
114
- const loaded = await loadProjectQuickref(projectDir);
115
- if (!loaded.exists)
287
+ const loaded = await resolveProjectQuickref(projectDir);
288
+ if (!loaded.exists || !loaded.definition)
116
289
  throw new Error(`Project quickref source not found: ${loaded.sourcePath}`);
117
- if (!loaded.definition)
118
- throw new Error(loaded.errors.join('\n'));
119
290
  if (!options.dryRun)
120
291
  await appendAiwgSourceTrackBlock(projectDir);
121
292
  const skillName = projectQuickrefSkillName(loaded.definition.project.id);
@@ -127,6 +298,14 @@ export async function generateProjectQuickref(projectDir, options = {}) {
127
298
  await mkdir(dirname(outputPath), { recursive: true });
128
299
  await writeFile(outputPath, content, 'utf8');
129
300
  }
301
+ if (!options.dryRun && loaded.provenance === 'managed') {
302
+ const snapshotPath = projectAiwgPath(projectDir, 'generated', 'project-quickref', 'definition.json');
303
+ const snapshot = JSON.stringify(loaded.definition, null, 2) + '\n';
304
+ if (await readIfPresent(snapshotPath) !== snapshot) {
305
+ await mkdir(dirname(snapshotPath), { recursive: true });
306
+ await writeFile(snapshotPath, snapshot, 'utf8');
307
+ }
308
+ }
130
309
  if (!options.dryRun) {
131
310
  const generatedRoot = projectAiwgPath(projectDir, 'generated', 'project-quickref');
132
311
  try {
@@ -149,6 +328,8 @@ export async function generateProjectQuickref(projectDir, options = {}) {
149
328
  content,
150
329
  changed,
151
330
  dryRun: options.dryRun ?? false,
331
+ provenance: loaded.provenance,
332
+ warnings: loaded.warnings,
152
333
  };
153
334
  }
154
335
  function resolveProviderSkillsRoot(provider, projectDir, homeDir) {
@@ -206,9 +387,9 @@ async function findStaleOwnedQuickrefs(root, projectDir, keepName, global) {
206
387
  }
207
388
  export async function deployProjectQuickref(projectDir, provider, options = {}) {
208
389
  const generated = await generateProjectQuickref(projectDir, { dryRun: options.dryRun });
209
- const loaded = await loadProjectQuickref(projectDir);
390
+ const loaded = await resolveProjectQuickref(projectDir);
210
391
  if (!loaded.definition)
211
- throw new Error(loaded.errors.join('\n'));
392
+ throw new Error(`Project quickref source not found: ${loaded.sourcePath}`);
212
393
  const target = resolveProviderSkillsRoot(provider, projectDir, options.homeDir ?? homedir());
213
394
  const targetDir = join(target.root, generated.skillName);
214
395
  const targetPath = join(targetDir, 'SKILL.md');
@@ -259,11 +440,17 @@ export async function deployProjectQuickref(projectDir, provider, options = {})
259
440
  };
260
441
  }
261
442
  export async function auditProjectQuickref(projectDir, providers, options = {}) {
262
- const loaded = await loadProjectQuickref(projectDir);
443
+ let loaded;
444
+ try {
445
+ loaded = await resolveProjectQuickref(projectDir);
446
+ }
447
+ catch (error) {
448
+ return { exists: true, errors: [error.message], drift: [] };
449
+ }
263
450
  if (!loaded.exists)
264
- return { exists: false, errors: loaded.errors, drift: [] };
451
+ return { exists: false, errors: [], drift: [] };
265
452
  if (!loaded.definition)
266
- return { exists: true, errors: loaded.errors, drift: [] };
453
+ return { exists: true, errors: ['project quickref definition unavailable'], drift: [] };
267
454
  const content = renderProjectQuickref(loaded.definition);
268
455
  const skillName = projectQuickrefSkillName(loaded.definition.project.id);
269
456
  const generatedPath = projectAiwgPath(projectDir, 'generated', 'project-quickref', skillName, 'SKILL.md');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.8.7",
3
+ "version": "2026.8.8",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -657,6 +657,13 @@ for (const [id, sourceRoot] of MANIFEST_PLUGIN_SOURCES) {
657
657
  keywords: manifest.keywords || manifest.tags || [],
658
658
  category: manifest.category || 'productivity',
659
659
  sourceRoot,
660
+ declaredSkills: manifest.skills ?? [],
661
+ // Plugin payloads run outside the checkout. Rewrite references back to
662
+ // their packaged root, including the pre-rename Ralph path retained by
663
+ // older Agent Loop documentation.
664
+ selfReferenceRoots: id === 'agent-loop'
665
+ ? [sourceRoot, 'agentic/code/addons/ralph']
666
+ : [sourceRoot],
660
667
  };
661
668
  }
662
669
 
@@ -774,6 +781,48 @@ function copyDir(src, dest, dryRun = false, filter = null) {
774
781
  return copied;
775
782
  }
776
783
 
784
+ function rewritePackagedSelfReferences(pluginDir, roots = []) {
785
+ if (roots.length === 0) return;
786
+ const pending = [pluginDir];
787
+ while (pending.length > 0) {
788
+ const current = pending.pop();
789
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
790
+ const target = path.join(current, entry.name);
791
+ if (entry.isDirectory()) {
792
+ pending.push(target);
793
+ } else if (entry.isFile() && /\.(?:md|json|ya?ml)$/.test(entry.name)) {
794
+ const original = fs.readFileSync(target, 'utf8');
795
+ let rewritten = original;
796
+ for (const root of roots) {
797
+ rewritten = rewritten.replaceAll(`${root}/`, '${CLAUDE_PLUGIN_ROOT}/');
798
+ }
799
+ if (rewritten !== original) fs.writeFileSync(target, rewritten, 'utf8');
800
+ }
801
+ }
802
+ }
803
+ }
804
+
805
+ function pruneUndeclaredPackagedSkills(pluginDir, declaredSkills) {
806
+ if (!declaredSkills) return;
807
+ const skillsRoot = path.join(pluginDir, 'skills');
808
+ if (!fs.existsSync(skillsRoot)) return;
809
+ const allowed = new Set(declaredSkills);
810
+ for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
811
+ if (entry.isDirectory() && !allowed.has(entry.name)) {
812
+ fs.rmSync(path.join(skillsRoot, entry.name), { recursive: true, force: true });
813
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
814
+ const skillId = path.basename(entry.name, '.md');
815
+ if (allowed.has(skillId)) {
816
+ const skillDir = path.join(skillsRoot, skillId);
817
+ fs.mkdirSync(skillDir, { recursive: true });
818
+ fs.renameSync(path.join(skillsRoot, entry.name), path.join(skillDir, 'SKILL.md'));
819
+ } else {
820
+ fs.rmSync(path.join(skillsRoot, entry.name), { force: true });
821
+ }
822
+ }
823
+ }
824
+ }
825
+
777
826
  // Clean plugin directory (except .claude-plugin)
778
827
  function cleanPlugin(pluginDir) {
779
828
  if (!fs.existsSync(pluginDir)) return;
@@ -811,6 +860,10 @@ function packagePlugin(name, config, options) {
811
860
  console.log(` 📁 Copying self-contained source from ${config.sourceRoot}...`);
812
861
  const count = copyDir(config.sourceRoot, pluginDir, options.dryRun);
813
862
  console.log(` ${count} files`);
863
+ if (!options.dryRun) {
864
+ rewritePackagedSelfReferences(pluginDir, config.selfReferenceRoots);
865
+ pruneUndeclaredPackagedSkills(pluginDir, config.declaredSkills);
866
+ }
814
867
  }
815
868
 
816
869
  // Copy sources