@aiwg/cli 2026.8.15 → 2026.8.17

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,5 +1,5 @@
1
- import { createHash } from 'node:crypto';
2
- import { access, lstat, readFile, readdir } from 'node:fs/promises';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { access, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { readAiwgConfig } from '../config/aiwg-config.js';
@@ -25,6 +25,65 @@ const FRAMEWORK_BUNDLE_DIRS = {
25
25
  validation: 'validation-complete',
26
26
  'knowledge-base': 'knowledge-base',
27
27
  };
28
+ const PROVIDER_TRANSFORMATION_EVIDENCE_STATE_SCHEMA = 'aiwg.provider-transformation-evidence-state.v1';
29
+ export function providerTransformationEvidenceStatePath(projectRoot, provider, scope) {
30
+ const receiptPath = providerTransformationReceiptPath(projectRoot, provider, scope);
31
+ return receiptPath.replace(/\.json$/, '.evidence.json');
32
+ }
33
+ function validateEvidenceState(value) {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value))
35
+ throw new Error('evidence state must be an object');
36
+ const state = value;
37
+ if (state.schemaVersion !== PROVIDER_TRANSFORMATION_EVIDENCE_STATE_SCHEMA)
38
+ throw new Error('unsupported evidence state schema');
39
+ if (!Number.isFinite(Date.parse(state.recordedAt)))
40
+ throw new Error('recordedAt must be an RFC 3339 date-time');
41
+ if (state.scope !== 'project' && state.scope !== 'user')
42
+ throw new Error('scope must be project or user');
43
+ if (!['local-source', 'source-unavailable', 'verification-failed'].includes(state.disposition)) {
44
+ throw new Error('unsupported source evidence disposition');
45
+ }
46
+ if (!state.provider || state.provider.includes('/') || state.provider.includes('\\'))
47
+ throw new Error('provider is invalid');
48
+ return state;
49
+ }
50
+ async function writeEvidenceState(options, disposition) {
51
+ const provider = normalizeProviderDefinitionId(options.provider) ?? options.provider;
52
+ const target = providerTransformationEvidenceStatePath(options.projectRoot, provider, options.scope);
53
+ await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
54
+ const temporary = path.join(path.dirname(target), `.${path.basename(target)}.${randomUUID()}.tmp`);
55
+ const state = {
56
+ schemaVersion: PROVIDER_TRANSFORMATION_EVIDENCE_STATE_SCHEMA,
57
+ recordedAt: options.generatedAt ?? new Date().toISOString(),
58
+ provider,
59
+ scope: options.scope,
60
+ disposition,
61
+ };
62
+ try {
63
+ await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
64
+ await rename(temporary, target);
65
+ }
66
+ catch (error) {
67
+ await rm(temporary, { force: true }).catch(() => undefined);
68
+ throw error;
69
+ }
70
+ await rm(providerTransformationReceiptPath(options.projectRoot, provider, options.scope), { force: true });
71
+ return target;
72
+ }
73
+ async function readEvidenceState(options) {
74
+ const provider = normalizeProviderDefinitionId(options.provider) ?? options.provider;
75
+ try {
76
+ const state = validateEvidenceState(JSON.parse(await readFile(providerTransformationEvidenceStatePath(options.projectRoot, provider, options.scope), 'utf8')));
77
+ if (state.provider !== provider || state.scope !== options.scope)
78
+ throw new Error('evidence state identity does not match deployment');
79
+ return state;
80
+ }
81
+ catch (error) {
82
+ if (error.code === 'ENOENT')
83
+ return null;
84
+ throw error;
85
+ }
86
+ }
28
87
  function sha256(value) {
29
88
  return createHash('sha256').update(value).digest('hex');
30
89
  }
@@ -225,6 +284,17 @@ function receiptBundles(installed, options) {
225
284
  .sort();
226
285
  return deployed.length > 0 ? deployed : [...new Set(options.requestedBundles)].sort();
227
286
  }
287
+ /**
288
+ * Return whether the deployed provider surface includes project-local source
289
+ * material that cannot be authenticated by an AIWG signed web release.
290
+ */
291
+ export async function providerReceiptHasLocalSources(rawOptions) {
292
+ const provider = normalizeProviderDefinitionId(rawOptions.provider) ?? rawOptions.provider;
293
+ const options = { ...rawOptions, provider };
294
+ const installed = await installedEntries(options);
295
+ return receiptBundles(installed, options)
296
+ .some(bundle => installed[bundle]?.source === 'project-local');
297
+ }
228
298
  /**
229
299
  * Convert an already signature-verified web release into the stable verifier
230
300
  * result contract for the complete canonical bundle consumed by deployment.
@@ -375,6 +445,27 @@ export async function resolveProviderReceiptRuntimeEvidence(rawOptions) {
375
445
  };
376
446
  }
377
447
  export async function finalizeProviderTransformationReceipt(options) {
448
+ if (options.sourceDisposition) {
449
+ const evidenceStatePath = await writeEvidenceState(options, options.sourceDisposition);
450
+ if (options.sourceDisposition === 'local-source') {
451
+ return {
452
+ status: 'policy-exempt',
453
+ receiptPath: null,
454
+ evidenceStatePath,
455
+ outputCount: 0,
456
+ reason: 'local-source development deployments are exempt from signed-release receipt issuance',
457
+ };
458
+ }
459
+ return {
460
+ status: options.sourceDisposition === 'source-unavailable' ? 'source-unavailable' : 'skipped',
461
+ receiptPath: null,
462
+ evidenceStatePath,
463
+ outputCount: 0,
464
+ reason: options.sourceDisposition === 'source-unavailable'
465
+ ? 'verified signed-release source evidence is not available from cache or configured resource access'
466
+ : 'canonical source verification failed',
467
+ };
468
+ }
378
469
  if (!options.sourceVerifications) {
379
470
  return {
380
471
  status: 'skipped',
@@ -412,9 +503,11 @@ export async function finalizeProviderTransformationReceipt(options) {
412
503
  transformer: evidence.transformer,
413
504
  outputPaths: evidence.outputPaths,
414
505
  });
506
+ const receiptPath = await writeProviderTransformationReceipt(options.projectRoot, receipt);
507
+ await rm(providerTransformationEvidenceStatePath(options.projectRoot, evidence.provider, options.scope), { force: true });
415
508
  return {
416
509
  status: 'written',
417
- receiptPath: await writeProviderTransformationReceipt(options.projectRoot, receipt),
510
+ receiptPath,
418
511
  outputCount: receipt.outputs.length,
419
512
  };
420
513
  }
@@ -425,6 +518,40 @@ export async function diagnoseIntegratedProviderTransformationReceipt(options) {
425
518
  await access(receiptPath);
426
519
  }
427
520
  catch {
521
+ const state = await readEvidenceState(options);
522
+ if (state?.disposition === 'local-source') {
523
+ return {
524
+ status: 'policy-exempt',
525
+ receiptPath,
526
+ checkedOutputs: 0,
527
+ findings: [{
528
+ kind: 'policy-exempt',
529
+ message: 'This local-source development deployment is explicitly exempt from signed-release transformation receipts.',
530
+ }],
531
+ };
532
+ }
533
+ if (state?.disposition === 'source-unavailable') {
534
+ return {
535
+ status: 'source-evidence-unavailable',
536
+ receiptPath,
537
+ checkedOutputs: 0,
538
+ findings: [{
539
+ kind: 'source-evidence-unavailable',
540
+ message: 'The deployment succeeded, but verified signed-release source evidence was unavailable from cache or configured resource access.',
541
+ }],
542
+ };
543
+ }
544
+ if (state?.disposition === 'verification-failed') {
545
+ return {
546
+ status: 'drifted',
547
+ receiptPath,
548
+ checkedOutputs: 0,
549
+ findings: [{
550
+ kind: 'source-verification-failure',
551
+ message: 'Canonical source verification failed during receipt finalization.',
552
+ }],
553
+ };
554
+ }
428
555
  return {
429
556
  status: 'missing-receipt',
430
557
  receiptPath,
@@ -1,4 +1,4 @@
1
- import { ARTIFACT_TRUST_STATE_SCHEMA_VERSION, channelStateKey, decodeBase64, dssePae, isIdentityRevoked, parseTrustRoot, publicKeyFingerprint, scopeMatches, selectDelegations, sha256, validateTrustState, verifyBytes, } from './artifact-trust.js';
1
+ import { ARTIFACT_TRUST_STATE_SCHEMA_VERSION, canonicalJson, channelStateKey, decodeBase64, dssePae, isIdentityRevoked, parseTrustRoot, publicKeyFingerprint, scopeMatches, selectDelegations, sha256, validateTrustState, verifyBytes, } from './artifact-trust.js';
2
2
  export const ARTIFACT_VERIFICATION_RESULT_SCHEMA_VERSION = 'aiwg.verify.result.v1';
3
3
  export const ARTIFACT_VERIFICATION_EXIT_CODES = {
4
4
  verified: 0,
@@ -351,6 +351,12 @@ export async function verifyArtifact(input) {
351
351
  identities: allAuthenticated.map(identity => identity.id).sort(),
352
352
  });
353
353
  }
354
+ if (!Buffer.from(canonicalJson(statement), 'utf8').equals(payload)) {
355
+ return result('mismatched', input, [{ code: 'NONCANONICAL_SIGNED_PAYLOAD', message: 'Signed provenance payload is not canonical JSON' }], {
356
+ ...common,
357
+ identities: allAuthenticated.map(identity => identity.id).sort(),
358
+ });
359
+ }
354
360
  const scopeInput = {
355
361
  assetType: statement.predicate.assetType,
356
362
  namespace: statement.predicate.publisher.namespace,
@@ -695,19 +695,14 @@ export async function handlePtyConnection(sessionId, ws, command = 'aiwg', cmdAr
695
695
  }
696
696
  }
697
697
  else if (!session.exited) {
698
- // Reconnect to existing session replay buffer
698
+ // Reconnect to an existing session by replaying the complete retained
699
+ // buffer. Do not trim at the last full-screen erase: terminal erase
700
+ // sequences repaint the current viewport but xterm still needs the bytes
701
+ // that preceded them to reconstruct scrollback when a user switches away
702
+ // from a session and later returns (#2146).
699
703
  registry.addClient(sessionId, clientId, ws);
700
704
  if (session.outputBuffer) {
701
- // Trim replay to start from the last full-screen erase so that tmux's
702
- // screen-init sequences (cursor moves, status-bar paint) from before the
703
- // erase don't render as literal garbage in a fresh xterm.js context.
704
- // Everything before \x1b[2J would be cleared by the erase anyway;
705
- // everything after is the session content tmux redrew (MOTD, history, etc).
706
- // If no erase is found, replay the whole buffer unchanged.
707
- const ERASE = '\x1b[2J';
708
- const lastErase = session.outputBuffer.lastIndexOf(ERASE);
709
- const replay = lastErase !== -1 ? session.outputBuffer.slice(lastErase) : session.outputBuffer;
710
- ws.send(JSON.stringify({ type: 'data', payload: replay }));
705
+ ws.send(JSON.stringify({ type: 'data', payload: session.outputBuffer }));
711
706
  }
712
707
  }
713
708
  else {
@@ -25,6 +25,8 @@ import { spawn } from 'node:child_process';
25
25
  import { promises as fs } from 'node:fs';
26
26
  import * as path from 'node:path';
27
27
  import { resolveRuntime, supportedRuntimes } from './runtime.js';
28
+ import { recordTypeForEntry, stableRecordId } from '../artifacts/browser-export.js';
29
+ import { loadFortemiCoreMetadataEntries } from '../artifacts/fortemi-core-query-adapter.js';
28
30
  /**
29
31
  * Resolve the AIWG installation root. Prefers `$AIWG_ROOT` env, falls
30
32
  * back to the channel manager's framework-root resolver.
@@ -53,22 +55,29 @@ async function findSkillEntry(cwd, name) {
53
55
  const reader = await import('../artifacts/index-reader.js');
54
56
  const entries = [];
55
57
  for (const g of ['framework', 'project', 'codebase']) {
56
- const idx = reader.loadGraphIndexFile(cwd, 'metadata.json', g);
57
- if (idx)
58
- entries.push(...Object.values(idx.entries));
58
+ const canonical = loadFortemiCoreMetadataEntries(cwd, g);
59
+ if (canonical.entries.length > 0) {
60
+ entries.push(...canonical.entries);
61
+ }
62
+ else {
63
+ const idx = reader.loadGraphIndexFile(cwd, 'metadata.json', g);
64
+ if (idx)
65
+ entries.push(...Object.values(idx.entries));
66
+ }
59
67
  }
60
68
  if (entries.length === 0) {
61
69
  const legacy = reader.loadMetadataIndex(cwd);
62
70
  if (legacy)
63
71
  entries.push(...Object.values(legacy.entries));
64
72
  }
65
- const skills = entries.filter(e => e.type === 'skill');
73
+ const skills = entries.filter(e => e.type === 'skill' || e.type === 'aiwg.skill');
66
74
  const needle = name.trim();
67
75
  // Basename match — skills are conventionally `<dir>/SKILL.md`
68
76
  const matches = skills.filter(e => {
69
77
  const dir = path.basename(path.dirname(e.path));
70
78
  const stem = path.basename(e.path).replace(/\.[^.]+$/, '');
71
- return dir === needle || stem === needle || e.path === needle;
79
+ const id = stableRecordId(recordTypeForEntry({ ...e, type: 'skill' }, 'v2'), e.path);
80
+ return id === needle || e.name === needle || dir === needle || stem === needle || e.path === needle;
72
81
  });
73
82
  if (matches.length === 0)
74
83
  return null;
@@ -248,7 +257,7 @@ export async function main(args, env) {
248
257
  });
249
258
  }
250
259
  function printUsage() {
251
- console.log('Usage: aiwg run skill <name> [--cwd <path>] [-- <args...>]');
260
+ console.log('Usage: aiwg run skill <stable-id-or-name> [--cwd <path>] [-- <args...>]');
252
261
  console.log('');
253
262
  console.log('Examples:');
254
263
  console.log(' aiwg run skill voice-apply -- --voice technical-authority --input draft.md');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.8.15",
3
+ "version": "2026.8.17",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,6 +67,7 @@
67
67
  "dependencies": {
68
68
  "@fortemi/core": "2026.7.15",
69
69
  "@modelcontextprotocol/sdk": "^1.30.0",
70
+ "ajv": "^8.20.0",
70
71
  "chalk": "^4.1.2",
71
72
  "chokidar": "^4.0.3",
72
73
  "commander": "^12.1.0",