@aiwg/cli 2026.8.10 → 2026.8.11

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.
@@ -1,4 +1,5 @@
1
1
  import { promises as fs } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  import path from 'node:path';
3
4
  const MANAGED_MARKER_RE = /(?:^|\n)(?:#|<!--)\s*aiwg:managed\s+v?([^\s]+)\s+([^\s>]+)(?:\s*-->)?/;
4
5
  /** Normalize provider-specific deployed filenames to the source agent id. */
@@ -12,6 +13,31 @@ export function parseManagedArtifactMarker(content) {
12
13
  const match = MANAGED_MARKER_RE.exec(content);
13
14
  return match ? { version: match[1], source: match[2] } : null;
14
15
  }
16
+ /** Extract the developer-instruction body from a canonical Markdown agent. */
17
+ export function extractAgentInstructionBody(content) {
18
+ if (!content.startsWith('---'))
19
+ return content.trim();
20
+ const withoutFrontmatter = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
21
+ return withoutFrontmatter.trim();
22
+ }
23
+ function instructionHash(content) {
24
+ return createHash('sha256').update(content).digest('hex');
25
+ }
26
+ function extractDeployedInstructions(filename, content) {
27
+ if (!filename.toLowerCase().endsWith('.toml')) {
28
+ return extractAgentInstructionBody(content);
29
+ }
30
+ const match = content.match(/^developer_instructions\s*=\s*(.+)$/m);
31
+ if (!match)
32
+ return null;
33
+ try {
34
+ const value = JSON.parse(match[1]);
35
+ return typeof value === 'string' ? value.trim() : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
15
41
  async function collectAgentSources(frameworkRoot, rootDir, inventory, allMarkdownFiles) {
16
42
  let entries;
17
43
  try {
@@ -31,8 +57,12 @@ async function collectAgentSources(frameworkRoot, rootDir, inventory, allMarkdow
31
57
  if (!allMarkdownFiles && path.basename(rootDir) !== 'agents')
32
58
  return;
33
59
  let stat;
60
+ let content;
34
61
  try {
35
- stat = await fs.stat(absolute);
62
+ [stat, content] = await Promise.all([
63
+ fs.stat(absolute),
64
+ fs.readFile(absolute, 'utf8'),
65
+ ]);
36
66
  }
37
67
  catch {
38
68
  return;
@@ -42,6 +72,7 @@ async function collectAgentSources(frameworkRoot, rootDir, inventory, allMarkdow
42
72
  sources.push({
43
73
  path: path.relative(frameworkRoot, absolute),
44
74
  size: stat.size,
75
+ instructionHash: instructionHash(extractAgentInstructionBody(content)),
45
76
  });
46
77
  inventory.set(name, sources);
47
78
  }));
@@ -67,6 +98,11 @@ export function diagnoseOversizedAgent(filename, content, inventory, ceilingByte
67
98
  const packaged = inventory.get(normalizeAgentArtifactName(filename)) ?? [];
68
99
  if (packaged.some((source) => source.size > ceilingBytes))
69
100
  return 'current-package';
101
+ const deployedInstructions = extractDeployedInstructions(filename, content);
102
+ if (deployedInstructions !== null
103
+ && packaged.some((source) => source.instructionHash === instructionHash(deployedInstructions))) {
104
+ return 'current-package';
105
+ }
70
106
  const marker = parseManagedArtifactMarker(content);
71
107
  if (marker?.source === 'bundled')
72
108
  return 'stale-deployment';
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createHash } from "node:crypto";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { GRAPH_CONFIGS, getProjectIndexRoot, loadGlobalGraphConfigs } from "./types.js";
5
6
  import { buildAiwgFortemiIndexExport, } from "./browser-export.js";
6
7
  import { loadGraphIndexFile } from "./index-reader.js";
@@ -25,7 +26,7 @@ function findPackageRoot(startDir) {
25
26
  }
26
27
  }
27
28
  function prebuiltDir(graph) {
28
- const moduleDir = path.dirname(new URL(import.meta.url).pathname);
29
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
29
30
  const packageRoot = findPackageRoot(moduleDir);
30
31
  if (!packageRoot)
31
32
  return null;
@@ -215,17 +216,31 @@ export function getFortemiCorePrebuiltStatus(graph = "framework") {
215
216
  reason: manifestReadReason,
216
217
  };
217
218
  }
219
+ const prebuiltManifest = manifest;
218
220
  const exportExists = fs.existsSync(exportPath);
219
221
  let reason = null;
220
222
  if (exportExists) {
221
223
  try {
222
224
  const exportText = fs.readFileSync(exportPath, "utf-8");
223
225
  const exported = JSON.parse(exportText);
224
- if (sha256(exportText) !== manifest.export_checksum) {
226
+ if (prebuiltManifest.schema_version !== "aiwg.fortemi.prebuilt.v1" ||
227
+ prebuiltManifest.backend !== "fortemi-core" ||
228
+ prebuiltManifest.graph !== graph ||
229
+ prebuiltManifest.export_path !== "aiwg-fortemi-index-v2.json" ||
230
+ prebuiltManifest.export_schema_version !== "aiwg.fortemi.index.export.v2") {
231
+ reason = "prebuilt manifest is incompatible with the requested graph";
232
+ }
233
+ else if (sha256(exportText) !== prebuiltManifest.export_checksum) {
225
234
  reason = "prebuilt export checksum does not match manifest";
226
235
  }
227
- else if (exported.schema_version !== manifest.export_schema_version) {
228
- reason = `prebuilt export schema '${exported.schema_version}' does not match manifest '${manifest.export_schema_version}'`;
236
+ else if (exported.schema_version !== prebuiltManifest.export_schema_version) {
237
+ reason = `prebuilt export schema '${exported.schema_version}' does not match manifest '${prebuiltManifest.export_schema_version}'`;
238
+ }
239
+ else if (exported.source?.graph !== graph) {
240
+ reason = `prebuilt export graph '${exported.source?.graph ?? "unknown"}' does not match requested graph '${graph}'`;
241
+ }
242
+ else if (!Array.isArray(exported.items) || exported.items.length !== prebuiltManifest.item_count) {
243
+ reason = "prebuilt export item count does not match manifest";
229
244
  }
230
245
  }
231
246
  catch (err) {
@@ -241,10 +256,10 @@ export function getFortemiCorePrebuiltStatus(graph = "framework") {
241
256
  exportPath,
242
257
  built: exportExists,
243
258
  stale: !exportExists || reason !== null,
244
- itemCount: manifest.item_count,
245
- exportChecksum: manifest.export_checksum,
246
- generatedAt: manifest.generated_at,
247
- sourceIndexBuiltAt: manifest.source_index_built_at,
259
+ itemCount: prebuiltManifest.item_count,
260
+ exportChecksum: prebuiltManifest.export_checksum,
261
+ generatedAt: prebuiltManifest.generated_at,
262
+ sourceIndexBuiltAt: prebuiltManifest.source_index_built_at,
248
263
  reason: !exportExists ? "prebuilt manifest exists but export file is missing" : reason,
249
264
  };
250
265
  }
@@ -15,6 +15,7 @@
15
15
  import { promises as fs } from 'fs';
16
16
  import path from 'path';
17
17
  import { createScriptRunner } from './script-runner.js';
18
+ import { createUseHandler } from './use.js';
18
19
  import { getFrameworkRoot } from '../../channel/manager.mjs';
19
20
  import { refreshAllPackages } from '../../packages/registry.js';
20
21
  import { resolveActiveProvider } from '../provider-resolution.js';
@@ -97,7 +98,14 @@ export async function pruneStaleManagedAgentFiles(options) {
97
98
  continue;
98
99
  const artifactName = normalizeAgentArtifactName(entry.name);
99
100
  const missingFromCurrentPackage = !desired.has(artifactName);
100
- const fromOlderPackage = currentVersion !== null && isOlderManagedVersion(marker.version, currentVersion);
101
+ // Addons have independent manifest versions. Comparing their managed
102
+ // marker to the top-level package version makes a successful refresh
103
+ // delete freshly restored addon agents. Version-based cleanup remains
104
+ // valid for other provider trees that were not refreshed, while the
105
+ // active provider removes only artifacts absent from current sources.
106
+ const fromOlderPackage = provider !== options.provider
107
+ && currentVersion !== null
108
+ && isOlderManagedVersion(marker.version, currentVersion);
101
109
  if (!missingFromCurrentPackage && !fromOlderPackage)
102
110
  continue;
103
111
  const relFile = path.relative(options.projectRoot, file);
@@ -163,6 +171,7 @@ export const refreshHandler = {
163
171
  const modelDeployArgs = collectModelDeployArgs(ctx.args);
164
172
  const frameworkRoot = await getFrameworkRoot();
165
173
  const runner = createScriptRunner(frameworkRoot);
174
+ const activeUseHandler = createUseHandler();
166
175
  if (!quiet) {
167
176
  ui.blank();
168
177
  // Deprecation notice when invoked as 'sync'
@@ -195,6 +204,7 @@ export const refreshHandler = {
195
204
  // Step 2.5: Refresh remote packages (always, unless --packages-only skips npm)
196
205
  if (!quiet)
197
206
  ui.info(dryRun ? 'Would refresh remote packages...' : 'Refreshing remote packages...');
207
+ const deploymentFailures = [];
198
208
  if (!dryRun) {
199
209
  try {
200
210
  const refreshed = await refreshAllPackages();
@@ -259,13 +269,30 @@ export const refreshHandler = {
259
269
  ui.dim(' No installed frameworks or addons to re-deploy');
260
270
  }
261
271
  for (const fw of frameworks) {
262
- const providerArgs = ['--provider', detectedProvider, ...modelDeployArgs];
263
- const useResult = await runner.run('tools/cli/deploy.mjs', [fw, ...providerArgs], { capture: quiet });
272
+ // Invoke the active installation's handler directly. The historical
273
+ // deploy.mjs bridge shells out to the first `aiwg` on PATH, which can
274
+ // be a different version/root and therefore cannot safely refresh
275
+ // addons installed by this package (#143/#2102).
276
+ const useResult = await activeUseHandler.execute({
277
+ ...ctx,
278
+ cwd: ctx.cwd,
279
+ frameworkRoot,
280
+ args: [
281
+ fw,
282
+ '--provider', detectedProvider,
283
+ '--target', ctx.cwd,
284
+ '--yes',
285
+ '--json',
286
+ ...modelDeployArgs,
287
+ ],
288
+ rawArgs: ['use', fw],
289
+ });
264
290
  if (useResult.exitCode === 0) {
265
291
  if (!quiet)
266
292
  ui.success(`Deployed: ${fw}`);
267
293
  }
268
294
  else {
295
+ deploymentFailures.push(fw);
269
296
  if (!quiet)
270
297
  ui.warn(`Deploy issue: ${fw} (exit ${useResult.exitCode})`);
271
298
  }
@@ -281,11 +308,10 @@ export const refreshHandler = {
281
308
  }
282
309
  }
283
310
  // Step 4.25: Report planned project-local deploys (#1035).
284
- // The actual deploy is performed by `aiwg use` underneath via deploy.mjs;
285
- // this block surfaces what *would* happen during dry-run and what was
286
- // covered during a real refresh.
311
+ // The active use handler performs the actual project-local deploy during
312
+ // framework refresh; this block surfaces dry-run and completion details.
287
313
  try {
288
- const plDiscovery = await discoverProjectLocalBundles(process.cwd());
314
+ const plDiscovery = await discoverProjectLocalBundles(ctx.cwd);
289
315
  const plCount = plDiscovery.bundles.length;
290
316
  if (plCount > 0) {
291
317
  if (dryRun) {
@@ -312,11 +338,12 @@ export const refreshHandler = {
312
338
  if (!quiet)
313
339
  ui.info('Checking for stale deployments...');
314
340
  let staleAgentRemovals = [];
315
- if (!dryRun) {
341
+ if (!dryRun && deploymentFailures.length === 0) {
316
342
  try {
317
343
  staleAgentRemovals = await pruneStaleManagedAgentFiles({
318
344
  projectRoot: ctx.cwd,
319
345
  frameworkRoot,
346
+ provider: detectedProvider,
320
347
  });
321
348
  if (staleAgentRemovals.length > 0 && !quiet) {
322
349
  const total = staleAgentRemovals.reduce((sum, item) => sum + item.paths.length, 0);
@@ -435,10 +462,17 @@ export const refreshHandler = {
435
462
  skipUpdate,
436
463
  channel: channel || undefined,
437
464
  staleAgentRemovals,
465
+ deploymentFailures,
438
466
  });
439
467
  console.log(output);
440
468
  }
441
- return { exitCode: dryRun ? 0 : 0 };
469
+ if (deploymentFailures.length > 0) {
470
+ return {
471
+ exitCode: 1,
472
+ message: `Failed to re-deploy installed bundle(s): ${deploymentFailures.join(', ')}`,
473
+ };
474
+ }
475
+ return { exitCode: 0 };
442
476
  },
443
477
  };
444
478
  //# sourceMappingURL=refresh.js.map
@@ -1673,6 +1673,20 @@ async function ensurePostDeployPhases(opts) {
1673
1673
  // The shared verifier reports the index failure with stable remediation.
1674
1674
  }
1675
1675
  }
1676
+ // The Fortemi export is the default discovery backend. Rebuilding only the
1677
+ // source graph makes any existing export stale, so every successful use
1678
+ // must leave the shared cache synchronized (#142/#2103). This also
1679
+ // materializes a fresh-install cache without requiring a manual index sync.
1680
+ try {
1681
+ const { getFortemiCoreSyncStatus, syncFortemiCoreIndex, } = await import('../../artifacts/fortemi-core-sync.js');
1682
+ const status = getFortemiCoreSyncStatus(opts.frameworkRoot, 'framework');
1683
+ if (!status.built || status.stale) {
1684
+ syncFortemiCoreIndex(opts.frameworkRoot, { graph: 'framework' });
1685
+ }
1686
+ }
1687
+ catch {
1688
+ // The shared verifier reports discovery failures with stable remediation.
1689
+ }
1676
1690
  if (opts.args.includes('--no-context-files'))
1677
1691
  return;
1678
1692
  const paths = getProviderPaths(opts.provider);
@@ -3152,6 +3166,8 @@ export class UseHandler {
3152
3166
  // project (#1217). The output index location is XDG-shared
3153
3167
  // regardless of build cwd.
3154
3168
  await buildIndex(aiwgRootForIndex, { graph: 'framework', explicit: false });
3169
+ const { syncFortemiCoreIndex } = await import('../../artifacts/fortemi-core-sync.js');
3170
+ syncFortemiCoreIndex(aiwgRootForIndex, { graph: 'framework' });
3155
3171
  console.log = origLog;
3156
3172
  const indexElapsedSec = ((Date.now() - indexStart) / 1000).toFixed(1);
3157
3173
  ui.success(`Capability index ready (${indexElapsedSec}s).`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.8.10",
3
+ "version": "2026.8.11",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -180,8 +180,11 @@ const MANIFEST_FILENAME = '.aiwg-manifest.json';
180
180
  *
181
181
  * Idempotent — skips if either form of the marker is already present.
182
182
  */
183
- export function addManagedMarker(content, version, source) {
183
+ export function addManagedMarker(content, version, source, style = 'markdown') {
184
184
  if (MANAGED_MARKER_RE.test(content)) return content;
185
+ if (style === 'line-comment') {
186
+ return `# aiwg:managed v${version} ${source}\n${content}`;
187
+ }
185
188
  // Frontmatter present → inject as YAML comment after the opening `---\n`.
186
189
  if (content.startsWith('---\n')) {
187
190
  return content.replace(
@@ -680,9 +683,18 @@ export function deployFiles(files, destDir, opts, transformFn) {
680
683
  transformedContent = injectPlatformInContent(transformedContent, platformName);
681
684
  }
682
685
 
683
- // Add managed marker for .md / .mdc files (#749; .mdc for Cursor native rules)
686
+ // Add managed marker for provider artifacts (#749). TOML accepts `#`
687
+ // comments, which lets doctor attribute transformed Codex agents even
688
+ // though their YAML frontmatter is removed during serialization.
684
689
  if (base.endsWith('.md') || base.endsWith('.mdc')) {
685
690
  transformedContent = addManagedMarker(transformedContent, deployVersion, deploySource);
691
+ } else if (base.endsWith('.toml')) {
692
+ transformedContent = addManagedMarker(
693
+ transformedContent,
694
+ deployVersion,
695
+ deploySource,
696
+ 'line-comment',
697
+ );
686
698
  }
687
699
 
688
700
  // Compute content hash for sidecar comparison