@aiwg/cli 2026.7.20 → 2026.7.23

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 (62) hide show
  1. package/README.md +18 -7
  2. package/dist/src/api/index.d.ts +2 -0
  3. package/dist/src/api/index.js +2 -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/index-builder.js +54 -17
  7. package/dist/src/artifacts/state-transfer.js +27 -0
  8. package/dist/src/artifacts/stats.js +8 -0
  9. package/dist/src/cli/cli-extension-loader.js +73 -0
  10. package/dist/src/cli/handlers/index.js +3 -1
  11. package/dist/src/cli/handlers/sessions.js +1265 -0
  12. package/dist/src/cli/handlers/skill-lint.js +49 -45
  13. package/dist/src/cli/handlers/use.js +143 -60
  14. package/dist/src/cli/handlers/utilities.js +22 -8
  15. package/dist/src/cli/skill-usage.js +146 -24
  16. package/dist/src/config/aiwg-config.js +12 -0
  17. package/dist/src/config/cli.js +16 -3
  18. package/dist/src/extensions/commands/definitions.js +29 -0
  19. package/dist/src/extensions/manifest.js +29 -0
  20. package/dist/src/security/threat-assessment-config.js +296 -0
  21. package/dist/src/sessions/adapters/claude.js +385 -0
  22. package/dist/src/sessions/adapters/codex.js +548 -0
  23. package/dist/src/sessions/adapters/copilot.js +226 -0
  24. package/dist/src/sessions/adapters/cursor.js +528 -0
  25. package/dist/src/sessions/adapters/factory.js +386 -0
  26. package/dist/src/sessions/adapters/generic.js +225 -0
  27. package/dist/src/sessions/adapters/hermes.js +341 -0
  28. package/dist/src/sessions/adapters/openclaw.js +381 -0
  29. package/dist/src/sessions/adapters/opencode.js +454 -0
  30. package/dist/src/sessions/adapters/openhuman.js +315 -0
  31. package/dist/src/sessions/adapters/warp.js +160 -0
  32. package/dist/src/sessions/adapters/windsurf.js +212 -0
  33. package/dist/src/sessions/batch-contracts.js +121 -0
  34. package/dist/src/sessions/batch-import.js +265 -0
  35. package/dist/src/sessions/candidates.js +210 -0
  36. package/dist/src/sessions/contracts.js +337 -0
  37. package/dist/src/sessions/discovery.js +51 -0
  38. package/dist/src/sessions/fixtures.js +12 -0
  39. package/dist/src/sessions/import-lease.js +152 -0
  40. package/dist/src/sessions/importer.js +464 -0
  41. package/dist/src/sessions/index.js +31 -0
  42. package/dist/src/sessions/knowledge-shard.js +61 -0
  43. package/dist/src/sessions/optional-backends.js +238 -0
  44. package/dist/src/sessions/origin.js +117 -0
  45. package/dist/src/sessions/policy.js +192 -0
  46. package/dist/src/sessions/ports.js +2 -0
  47. package/dist/src/sessions/promotion.js +367 -0
  48. package/dist/src/sessions/readers.js +176 -0
  49. package/dist/src/sessions/repository.js +1892 -0
  50. package/dist/src/sessions/timeline.js +148 -0
  51. package/dist/src/sessions/workspace-discovery.js +319 -0
  52. package/dist/src/skills/adapters/agent-skills.js +59 -0
  53. package/dist/src/skills/adapters/local.js +19 -1
  54. package/dist/src/skills/agent-skills.js +249 -0
  55. package/dist/src/skills/cli.js +463 -7
  56. package/dist/src/skills/deployer.js +554 -0
  57. package/dist/src/skills/doctor.js +105 -0
  58. package/dist/src/skills/exporter.js +382 -0
  59. package/dist/src/skills/importer.js +921 -0
  60. package/dist/src/skills/registry.js +19 -0
  61. package/dist/src/skills/validator.js +323 -0
  62. package/package.json +2 -2
package/README.md CHANGED
@@ -10,10 +10,15 @@ AIWG skills and agents use this CLI to perform common operations with
10
10
  predictable, structured calls instead of spending context on shell discovery,
11
11
  filesystem traversal, command reconstruction, and repeated tool output.
12
12
 
13
- ```bash
14
- npm install --global @aiwg/cli
13
+ New to AIWG? Install the full `aiwg` package and let the agentic installer
14
+ connect the complete system. `@aiwg/cli` is the smaller execution layer for
15
+ agents, CI, and web-backed installations.
15
16
 
16
- aiwg doctor
17
+ ```text
18
+ Install or repair AIWG for this project by following
19
+ https://raw.githubusercontent.com/jmagly/aiwg/main/setup.aiwg.yaml
20
+ Explain the plan before changing anything, preserve my existing work, and ask
21
+ me only for choices you cannot safely determine.
17
22
  ```
18
23
 
19
24
  [![npm version](https://img.shields.io/npm/v/%40aiwg%2Fcli/latest?label=%40aiwg%2Fcli&color=CB3837&logo=npm&style=flat-square)](https://www.npmjs.com/package/@aiwg/cli)
@@ -209,6 +214,12 @@ execution layer and can obtain resources from the signed web release. Choose
209
214
  `aiwg` when local authoring, the full bundled corpus, or completely cold
210
215
  offline operation is required.
211
216
 
217
+ For a first installation, an uncertain environment, or a machine with an old
218
+ or broken AIWG setup, use the full `aiwg` package and the
219
+ [agentic installer manifest](https://raw.githubusercontent.com/jmagly/aiwg/main/setup.aiwg.yaml).
220
+ The flow detects development checkouts and preserves development mode unless
221
+ the user explicitly approves switching to the published package.
222
+
212
223
  Both CLI packages expose the same `aiwg` executable name. Install one globally
213
224
  at a time unless you deliberately manage separate npm prefixes.
214
225
 
@@ -247,7 +258,7 @@ rather than merely printed.
247
258
  Agents do not need to memorize the remaining command surface. AIWG discovery
248
259
  finds the relevant skill, and the skill supplies the right CLI step. Operators
249
260
  who need the complete syntax and examples can use the
250
- [AIWG CLI reference](https://docs.aiwg.io/pages/cli-reference.html).
261
+ [AIWG CLI reference](https://github.com/jmagly/aiwg/blob/main/docs/agents/cli-reference.md).
251
262
 
252
263
  ## How It Works
253
264
 
@@ -438,7 +449,7 @@ This README therefore documents the operating model, package boundary, trust
438
449
  model, and troubleshooting path instead of duplicating every command and flag.
439
450
  The complete operator reference is maintained at:
440
451
 
441
- **[AIWG CLI Reference — every command and example](https://docs.aiwg.io/pages/cli-reference.html)**
452
+ **[AIWG CLI Reference — every command and example](https://github.com/jmagly/aiwg/blob/main/docs/agents/cli-reference.md)**
442
453
 
443
454
  Keeping the command catalog in one canonical location prevents package
444
455
  documentation from drifting as the runtime grows.
@@ -492,7 +503,7 @@ security guidance.
492
503
  Do not routinely call `aiwg help` and place the full output in the model
493
504
  context. If a skill exists, use it. If a maintainer or operator needs an
494
505
  unfamiliar command, link to the canonical
495
- [CLI reference](https://docs.aiwg.io/pages/cli-reference.html) or retrieve only
506
+ [CLI reference](https://github.com/jmagly/aiwg/blob/main/docs/agents/cli-reference.md) or retrieve only
496
507
  the relevant section.
497
508
 
498
509
  ## Using AIWG from a Web-Connected Chat
@@ -1020,7 +1031,7 @@ Package invariants include:
1020
1031
 
1021
1032
  ## Documentation
1022
1033
 
1023
- - [Complete AIWG CLI reference](https://docs.aiwg.io/pages/cli-reference.html)
1034
+ - [Complete AIWG CLI reference](https://github.com/jmagly/aiwg/blob/main/docs/agents/cli-reference.md)
1024
1035
  - [AIWG documentation](https://docs.aiwg.io/)
1025
1036
  - [AIWG project README](https://github.com/jmagly/aiwg#readme)
1026
1037
  - [Web-backed resources guide](https://github.com/jmagly/aiwg/blob/main/docs/install/web-backed-resources.md)
@@ -7,4 +7,6 @@
7
7
  */
8
8
  export { run } from '../cli/router.js';
9
9
  export * from '../resources/index.js';
10
+ export * from '../sessions/index.js';
11
+ export * from '../security/threat-assessment-config.js';
10
12
  //# sourceMappingURL=index.d.ts.map
@@ -7,4 +7,6 @@
7
7
  */
8
8
  export { run } from '../cli/router.js';
9
9
  export * from '../resources/index.js';
10
+ export * from '../sessions/index.js';
11
+ export * from '../security/threat-assessment-config.js';
10
12
  //# sourceMappingURL=index.js.map
@@ -508,6 +508,13 @@ function recordForEntry(cwd, entry, graphName, dependencyGraph, privacy, schemaV
508
508
  ...(entry.operationalState
509
509
  ? { operational_state: entry.operationalState }
510
510
  : {}),
511
+ ...(entry.stateTransfer
512
+ ? {
513
+ state_transfer: {
514
+ deleted_at: entry.stateTransfer.deletedAt,
515
+ },
516
+ }
517
+ : {}),
511
518
  }
512
519
  : {}),
513
520
  updated_at: entry.updated,
@@ -8,7 +8,7 @@
8
8
  * - **Incoming**: corpus papers that cite this work (column: "REF") → `cited-by` edges
9
9
  *
10
10
  * Supported node-id forms (#105):
11
- * - `REF-\d+` research-paper IDs (REF-001, REF-029, ...)
11
+ * - `REF-\d+[a-z]?` research-paper IDs (REF-001, REF-434a, ...)
12
12
  * - `PROF-[POFG]-[a-z0-9-]+` entity-profile IDs:
13
13
  * - `PROF-P-*` people, `PROF-O-*` orgs, `PROF-F-*` funders, `PROF-G-*` groups
14
14
  *
@@ -26,17 +26,43 @@ import { parseFrontmatter } from './index-builder.js';
26
26
  * Match a single node identifier (REF-* or PROF-*) anywhere in a string.
27
27
  * Used by `extractRefsFromTable` to pull every ID out of a table cell.
28
28
  */
29
- const NODE_ID_PATTERN = /(?:REF-\d+|PROF-[POFG]-[a-z0-9-]+)/g;
29
+ const NODE_ID_PATTERN = /(?:REF-\d+[a-z]?|PROF-[POFG]-[a-z0-9-]+)/g;
30
30
  /**
31
31
  * Validate that a string is a complete node identifier.
32
32
  * Used by `parseCitationSidecar` and `buildRefToPathMap` to gate
33
33
  * frontmatter `ref` values.
34
34
  */
35
- const NODE_ID_FULL = /^(?:REF-\d+|PROF-[POFG]-[a-z0-9-]+)$/;
35
+ const NODE_ID_FULL = /^(?:REF-\d+[a-z]?|PROF-[POFG]-[a-z0-9-]+)$/;
36
+ /** Explicit column aliases used by historical citation-sidecar variants. */
37
+ const CITATION_REF_COLUMNS = [
38
+ 'Inducted REF',
39
+ 'REF',
40
+ 'Corpus REF',
41
+ 'In-corpus REF',
42
+ ];
43
+ /**
44
+ * Preserve the corpus snapshot's legacy edge semantics: every node ID on a
45
+ * pipe-delimited line inside an outgoing/incoming section is a declaration.
46
+ * Explicit column parsing remains the primary path, while this compatibility
47
+ * scan covers malformed rows and prose continuations that historical corpus
48
+ * snapshots already count.
49
+ */
50
+ function extractSectionNodeIds(section) {
51
+ const refs = [];
52
+ for (const line of section.split('\n')) {
53
+ if (!line.includes('|'))
54
+ continue;
55
+ NODE_ID_PATTERN.lastIndex = 0;
56
+ const matches = line.match(NODE_ID_PATTERN);
57
+ if (matches)
58
+ refs.push(...matches);
59
+ }
60
+ return [...new Set(refs)];
61
+ }
36
62
  /**
37
63
  * Test whether a string is a valid sidecar node identifier.
38
64
  *
39
- * Accepts `REF-\d+` and `PROF-[POFG]-[a-z0-9-]+`. Returns false for any
65
+ * Accepts `REF-\d+[a-z]?` and `PROF-[POFG]-[a-z0-9-]+`. Returns false for any
40
66
  * other input (including unrelated `PROF-` prefixed strings that don't
41
67
  * match the four-letter type code form).
42
68
  */
@@ -54,34 +80,71 @@ export function isNodeId(value) {
54
80
  * @returns Array of node identifiers found
55
81
  */
56
82
  export function extractRefsFromTable(tableText, columnName) {
57
- const lines = tableText.split('\n').filter(l => l.trim().startsWith('|'));
58
- if (lines.length < 3)
59
- return []; // Need header + separator + at least one row
60
- // Parse header to find column index
61
- const headerCells = lines[0].split('|').map(c => c.trim()).filter(Boolean);
62
- const colIndex = headerCells.findIndex(h => h.toLowerCase() === columnName.toLowerCase());
63
- if (colIndex === -1)
64
- return [];
65
- // Skip header (line 0) and separator (line 1), parse data rows
83
+ const aliases = (Array.isArray(columnName) ? columnName : [columnName])
84
+ .map(name => name.trim().toLowerCase());
85
+ const tableBlocks = [];
86
+ let currentBlock = [];
87
+ for (const line of tableText.split('\n')) {
88
+ if (line.trim().startsWith('|')) {
89
+ currentBlock.push(line);
90
+ }
91
+ else if (currentBlock.length > 0) {
92
+ tableBlocks.push(currentBlock);
93
+ currentBlock = [];
94
+ }
95
+ }
96
+ if (currentBlock.length > 0)
97
+ tableBlocks.push(currentBlock);
98
+ const parseRow = (line) => {
99
+ let row = line.trim();
100
+ if (row.startsWith('|'))
101
+ row = row.slice(1);
102
+ if (row.endsWith('|'))
103
+ row = row.slice(0, -1);
104
+ return row.split('|').map(cell => cell.trim());
105
+ };
66
106
  const refs = [];
67
- for (let i = 2; i < lines.length; i++) {
68
- const cells = lines[i].split('|').map(c => c.trim()).filter(Boolean);
69
- if (colIndex >= cells.length)
107
+ for (const lines of tableBlocks) {
108
+ const headerCells = parseRow(lines[0]);
109
+ const colIndex = headerCells.findIndex(header => {
110
+ const normalized = header.toLowerCase().replace(/[`*_]/g, '').trim();
111
+ return aliases.some(alias => normalized === alias ||
112
+ normalized.startsWith(`${alias} /`) ||
113
+ normalized.startsWith(`${alias} (`));
114
+ });
115
+ if (lines.length < 3 || colIndex === -1) {
116
+ // Legacy sidecars sometimes have headerless continuation rows or a
117
+ // generic table whose rows were extended with a final REF cell. The
118
+ // corpus snapshot has always treated every in-section table REF as an
119
+ // outgoing/incoming declaration, so retain that compatibility fallback.
120
+ const dataLines = lines.length >= 3 ? lines.slice(2) : lines;
121
+ for (const line of dataLines) {
122
+ NODE_ID_PATTERN.lastIndex = 0;
123
+ const refMatches = line.match(NODE_ID_PATTERN);
124
+ if (refMatches)
125
+ refs.push(...refMatches);
126
+ }
70
127
  continue;
71
- const value = cells[colIndex].trim();
72
- // Skip empty, dash, or em-dash values
73
- if (!value || value === '—' || value === '-' || value === '–')
74
- continue;
75
- // Extract node-id pattern(s) (REF-* or PROF-*) from the cell.
76
- // Reset the lastIndex defensively — NODE_ID_PATTERN is a module-level
77
- // /g RegExp shared across calls.
78
- NODE_ID_PATTERN.lastIndex = 0;
79
- const refMatches = value.match(NODE_ID_PATTERN);
80
- if (refMatches) {
81
- refs.push(...refMatches);
128
+ }
129
+ // Skip this table's header and separator, preserving all interior cells.
130
+ for (let i = 2; i < lines.length; i++) {
131
+ const cells = parseRow(lines[i]);
132
+ if (colIndex >= cells.length)
133
+ continue;
134
+ // Historical rows may expand the named REF cell into an issue/REF pair
135
+ // or append placeholder cells without extending the header. Scan from
136
+ // the named column through the remainder of the row; columns before the
137
+ // alias (title/authors/year) remain excluded.
138
+ const value = cells.slice(colIndex).join(' | ');
139
+ if (!value || value === '—' || value === '-' || value === '–')
140
+ continue;
141
+ NODE_ID_PATTERN.lastIndex = 0;
142
+ const refMatches = value.match(NODE_ID_PATTERN);
143
+ if (refMatches)
144
+ refs.push(...refMatches);
82
145
  }
83
146
  }
84
- return refs;
147
+ return [...new Set(refs)];
85
148
  }
86
149
  /**
87
150
  * Parse a citation sidecar markdown file into structured edges.
@@ -102,17 +165,15 @@ export function parseCitationSidecar(content) {
102
165
  for (const section of sections) {
103
166
  const sectionLower = section.toLowerCase();
104
167
  if (sectionLower.startsWith('outgoing')) {
105
- // Outgoing table: extract from "Inducted REF" column
106
- cites = extractRefsFromTable(section, 'Inducted REF');
168
+ cites.push(...extractRefsFromTable(section, CITATION_REF_COLUMNS));
169
+ cites.push(...extractSectionNodeIds(section));
107
170
  }
108
171
  else if (sectionLower.startsWith('incoming')) {
109
- // Incoming table: extract from "REF" column
110
- // The incoming section may have subsections (### Corpus Cross-References)
111
- // Look for tables anywhere in this section
112
- citedBy = extractRefsFromTable(section, 'REF');
172
+ citedBy.push(...extractRefsFromTable(section, CITATION_REF_COLUMNS));
173
+ citedBy.push(...extractSectionNodeIds(section));
113
174
  }
114
175
  }
115
- return { ref, cites, citedBy };
176
+ return { ref, cites: [...new Set(cites)], citedBy: [...new Set(citedBy)] };
116
177
  }
117
178
  /**
118
179
  * Convert a CitationParseResult into TypedEdge arrays for the dependency graph.
@@ -20,6 +20,7 @@ import { loadManifest, writeManifest, statMatches, makeEntry } from './checksum-
20
20
  import { workspaceLinkedFiles } from '../smiths/context-pipeline/workspace-context.js';
21
21
  import { normalizeOperationalState } from './operational-state.js';
22
22
  import { DEFAULT_PROJECT_AIWG_DIR, resolveProjectAiwgDir, } from '../config/project-artifacts.js';
23
+ import { normalizeStateTransferProjection } from './state-transfer.js';
23
24
  function pathContains(parent, child) {
24
25
  const relative = path.relative(parent, child);
25
26
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
@@ -322,10 +323,10 @@ export function parseRunbookDoc(data, body, filePath) {
322
323
  /**
323
324
  * Extract trigger phrases from a SKILL.md / agent body.
324
325
  *
325
- * Skills declare alternate activation phrases under a `## Triggers`
326
- * heading; the body typically lists them as bullet points. This
327
- * function pulls each bullet's leading phrase (the part before any
328
- * `→` arrow or em-dash explanation), lowercased and trimmed.
326
+ * Skills declare alternate activation phrases in `triggers`, `aliases`,
327
+ * `deprecated_names`, or under a `## Triggers` heading. This function pulls
328
+ * those names plus each bullet's leading phrase (the part before any `→`
329
+ * arrow or em-dash explanation), lowercased and trimmed.
329
330
  *
330
331
  * Returns an empty array when no `## Triggers` section is found —
331
332
  * non-skill artifacts get `triggers: undefined` after this is wired.
@@ -334,18 +335,20 @@ export function parseRunbookDoc(data, body, filePath) {
334
335
  */
335
336
  export function extractTriggers(body, frontmatter) {
336
337
  const phrases = [];
337
- const declaredTriggers = Array.isArray(frontmatter?.triggers)
338
- ? frontmatter.triggers
339
- : [];
340
- for (const trigger of declaredTriggers) {
341
- if (typeof trigger !== 'string')
342
- continue;
343
- const phrase = trigger.trim().toLowerCase();
344
- if (phrase.length === 0)
345
- continue;
346
- if (phrase.length > 200)
347
- continue;
348
- phrases.push(phrase);
338
+ for (const field of ['triggers', 'aliases', 'deprecated_names']) {
339
+ const declaredValues = Array.isArray(frontmatter?.[field])
340
+ ? frontmatter[field]
341
+ : [];
342
+ for (const value of declaredValues) {
343
+ if (typeof value !== 'string')
344
+ continue;
345
+ const phrase = value.trim().toLowerCase();
346
+ if (phrase.length === 0)
347
+ continue;
348
+ if (phrase.length > 200)
349
+ continue;
350
+ phrases.push(phrase);
351
+ }
349
352
  }
350
353
  // Find a triggers heading (case-insensitive). Accepted variants:
351
354
  // ## Triggers
@@ -882,6 +885,7 @@ export async function buildIndex(cwd, options = {}) {
882
885
  // Script entrypoint metadata is meaningful for skills only (#1227).
883
886
  const script = type === 'skill' ? extractSkillScript(data) : undefined;
884
887
  const operationalState = normalizeOperationalState(data.operational_state);
888
+ const stateTransfer = normalizeStateTransferProjection(data.state_transfer);
885
889
  // Canonical short name (#1233) — used by the scorer to floor exact-name
886
890
  // queries to 1.0 so hyphenated kernel-skill names like `aiwg-doctor`
887
891
  // remain searchable even when the rendered title strips the hyphen.
@@ -907,6 +911,7 @@ export async function buildIndex(cwd, options = {}) {
907
911
  ...(kernel ? { kernel } : {}),
908
912
  ...(script ? { script } : {}),
909
913
  ...(operationalState ? { operationalState } : {}),
914
+ ...(stateTransfer ? { stateTransfer } : {}),
910
915
  };
911
916
  }
912
917
  entries[relativePath] = entry;
@@ -957,6 +962,7 @@ export async function buildIndex(cwd, options = {}) {
957
962
  }
958
963
  }
959
964
  // Run citation sidecar edge extraction if configured
965
+ let citationMetrics = null;
960
966
  if (graphConfig?.edgeExtraction?.parser === 'citation-sidecar') {
961
967
  // Build REF-XXX → path map from all entries with ref frontmatter
962
968
  const entryFrontmatter = new Map();
@@ -971,6 +977,10 @@ export async function buildIndex(cwd, options = {}) {
971
977
  const refToPath = buildRefToPathMap(entryFrontmatter);
972
978
  // Parse each entry as a citation sidecar and extract edges
973
979
  let citationEdgeCount = 0;
980
+ let outgoingDeclarations = 0;
981
+ let incomingDeclarations = 0;
982
+ const canonicalOutgoing = new Set();
983
+ const declaredIncoming = new Set();
974
984
  for (const entryPath of Object.keys(entries)) {
975
985
  const fullPath = absoluteEntryPath(cwd, entryPath, graph);
976
986
  if (!fs.existsSync(fullPath))
@@ -979,6 +989,14 @@ export async function buildIndex(cwd, options = {}) {
979
989
  const result = parseCitationSidecar(content);
980
990
  if (!result)
981
991
  continue;
992
+ outgoingDeclarations += result.cites.length;
993
+ incomingDeclarations += result.citedBy.length;
994
+ for (const targetRef of result.cites) {
995
+ canonicalOutgoing.add(`${result.ref}\0${targetRef}`);
996
+ }
997
+ for (const sourceRef of result.citedBy) {
998
+ declaredIncoming.add(`${sourceRef}\0${result.ref}`);
999
+ }
982
1000
  const edges = citationResultToEdges(result, refToPath);
983
1001
  if (!depGraph[entryPath]) {
984
1002
  depGraph[entryPath] = { upstream: [], downstream: [] };
@@ -1004,6 +1022,15 @@ export async function buildIndex(cwd, options = {}) {
1004
1022
  citationEdgeCount++;
1005
1023
  }
1006
1024
  }
1025
+ citationMetrics = {
1026
+ canonicalEdges: canonicalOutgoing.size,
1027
+ outgoingDeclarations,
1028
+ incomingDeclarations,
1029
+ unmirroredOutgoing: [...canonicalOutgoing]
1030
+ .filter(edge => !declaredIncoming.has(edge)).length,
1031
+ unmirroredIncoming: [...declaredIncoming]
1032
+ .filter(edge => !canonicalOutgoing.has(edge)).length,
1033
+ };
1007
1034
  if (verbose && citationEdgeCount > 0) {
1008
1035
  console.log(` citation edges: ${citationEdgeCount}`);
1009
1036
  }
@@ -1038,7 +1065,9 @@ export async function buildIndex(cwd, options = {}) {
1038
1065
  manifestStats.pruned = 0;
1039
1066
  writeManifest(indexOutputDir, nextManifest);
1040
1067
  // Write stats
1041
- const totalEdges = Object.values(depGraph).reduce((sum, node) => sum + node.downstream.length, 0);
1068
+ const downstreamEdges = Object.values(depGraph).reduce((sum, node) => sum + node.downstream.length, 0);
1069
+ const adjacencyEntries = Object.values(depGraph).reduce((sum, node) => sum + node.upstream.length + node.downstream.length, 0);
1070
+ const totalEdges = citationMetrics?.canonicalEdges ?? downstreamEdges;
1042
1071
  const orphaned = Object.entries(depGraph).filter(([, node]) => node.upstream.length === 0 && node.downstream.length === 0).length;
1043
1072
  const mostReferenced = Object.entries(depGraph)
1044
1073
  .map(([p, node]) => ({ path: p, count: node.downstream.length }))
@@ -1064,6 +1093,14 @@ export async function buildIndex(cwd, options = {}) {
1064
1093
  tagDistribution: tagDist,
1065
1094
  graphMetrics: {
1066
1095
  totalEdges,
1096
+ ...(citationMetrics ? {
1097
+ canonicalEdges: citationMetrics.canonicalEdges,
1098
+ outgoingDeclarations: citationMetrics.outgoingDeclarations,
1099
+ incomingDeclarations: citationMetrics.incomingDeclarations,
1100
+ adjacencyEntries,
1101
+ unmirroredOutgoing: citationMetrics.unmirroredOutgoing,
1102
+ unmirroredIncoming: citationMetrics.unmirroredIncoming,
1103
+ } : {}),
1067
1104
  orphanedArtifacts: orphaned,
1068
1105
  mostReferenced,
1069
1106
  },
@@ -0,0 +1,27 @@
1
+ export function normalizeStateTransferProjection(value) {
2
+ if (value === undefined)
3
+ return undefined;
4
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
5
+ throw new Error("state_transfer must be an object");
6
+ }
7
+ const record = value;
8
+ const unknown = Object.keys(record).filter((key) => key !== "deleted_at");
9
+ if (unknown.length > 0) {
10
+ throw new Error(`state_transfer has unsupported fields: ${unknown.join(", ")}`);
11
+ }
12
+ if (!Object.hasOwn(record, "deleted_at")) {
13
+ throw new Error("state_transfer.deleted_at is required");
14
+ }
15
+ const deletedAt = record.deleted_at;
16
+ if (deletedAt === null)
17
+ return { deletedAt: null };
18
+ if (deletedAt instanceof Date && !Number.isNaN(deletedAt.getTime())) {
19
+ return { deletedAt: deletedAt.toISOString() };
20
+ }
21
+ if (typeof deletedAt !== "string"
22
+ || Number.isNaN(Date.parse(deletedAt))) {
23
+ throw new Error("state_transfer.deleted_at must be null or an ISO date-time");
24
+ }
25
+ return { deletedAt: new Date(deletedAt).toISOString() };
26
+ }
27
+ //# sourceMappingURL=state-transfer.js.map
@@ -153,6 +153,14 @@ async function renderStats(cwd, stats, options, graphType) {
153
153
  // Dependency graph
154
154
  console.log('Dependency Graph:');
155
155
  console.log(` Total edges: ${stats.graphMetrics.totalEdges}`);
156
+ if (stats.graphMetrics.canonicalEdges !== undefined) {
157
+ console.log(` Canonical edges: ${stats.graphMetrics.canonicalEdges}`);
158
+ console.log(` Outgoing declares: ${stats.graphMetrics.outgoingDeclarations}`);
159
+ console.log(` Incoming declares: ${stats.graphMetrics.incomingDeclarations}`);
160
+ console.log(` Adjacency entries: ${stats.graphMetrics.adjacencyEntries}`);
161
+ console.log(` Unmirrored outgoing:${String(stats.graphMetrics.unmirroredOutgoing).padStart(3)}`);
162
+ console.log(` Unmirrored incoming:${String(stats.graphMetrics.unmirroredIncoming).padStart(3)}`);
163
+ }
156
164
  console.log(` Orphaned artifacts: ${stats.graphMetrics.orphanedArtifacts}`);
157
165
  if (stats.graphMetrics.mostReferenced) {
158
166
  console.log(` Most referenced: ${stats.graphMetrics.mostReferenced.path} (${stats.graphMetrics.mostReferenced.count} dependents)`);
@@ -41,10 +41,83 @@ export async function writeRegistry(cwd, registry) {
41
41
  * Called by `aiwg use <addon>` after reading the addon manifest.
42
42
  */
43
43
  export async function registerCliCommands(cwd, namespace, description, source, subcommands) {
44
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(namespace)) {
45
+ throw new Error(`Invalid CLI namespace: ${namespace}`);
46
+ }
47
+ if (!source || !path.isAbsolute(source)) {
48
+ throw new Error('CLI extension source must be an absolute path.');
49
+ }
50
+ if (Object.keys(subcommands).length === 0) {
51
+ throw new Error(`CLI namespace '${namespace}' declares no subcommands.`);
52
+ }
53
+ for (const [name, subcommand] of Object.entries(subcommands)) {
54
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(name)) {
55
+ throw new Error(`Invalid CLI subcommand: ${namespace} ${name}`);
56
+ }
57
+ if (!subcommand
58
+ || typeof subcommand.file !== 'string'
59
+ || !/^[a-zA-Z0-9_-]+\.mjs$/.test(subcommand.file)) {
60
+ throw new Error(`CLI subcommand '${namespace} ${name}' must name a local .mjs file.`);
61
+ }
62
+ if (typeof subcommand.description !== 'string' || !subcommand.description.trim()) {
63
+ throw new Error(`CLI subcommand '${namespace} ${name}' requires a description.`);
64
+ }
65
+ }
44
66
  const existing = await readRegistry(cwd) ?? {};
45
67
  existing[namespace] = { source, description, subcommands };
46
68
  await writeRegistry(cwd, existing);
47
69
  }
70
+ /**
71
+ * Read and validate an addon-shaped manifest's expandable CLI declaration.
72
+ *
73
+ * `sourceRoot` may be a bundled addon, a project-local addon, or the validated
74
+ * payload of a project-local plugin wrapper. Module files are constrained to
75
+ * the declared commands directory; registration never follows `..` paths.
76
+ */
77
+ export async function loadCliCommandsContribution(sourceRoot) {
78
+ const manifestPath = path.join(sourceRoot, 'manifest.json');
79
+ let raw;
80
+ try {
81
+ raw = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
82
+ }
83
+ catch (error) {
84
+ if (error.code === 'ENOENT')
85
+ return null;
86
+ throw new Error(`Unable to read CLI extension manifest at ${manifestPath}: ${error.message}`);
87
+ }
88
+ const candidate = raw.cli_commands;
89
+ if (candidate === undefined)
90
+ return null;
91
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
92
+ throw new Error(`Invalid cli_commands block in ${manifestPath}`);
93
+ }
94
+ const commands = candidate;
95
+ if (typeof commands.namespace !== 'string'
96
+ || typeof commands.description !== 'string'
97
+ || !commands.subcommands
98
+ || typeof commands.subcommands !== 'object'
99
+ || Array.isArray(commands.subcommands)) {
100
+ throw new Error(`Incomplete cli_commands block in ${manifestPath}`);
101
+ }
102
+ const entry = commands.entry ?? 'commands/';
103
+ if (typeof entry !== 'string'
104
+ || path.isAbsolute(entry)
105
+ || entry.split(/[\\/]+/).includes('..')) {
106
+ throw new Error(`Unsafe cli_commands.entry in ${manifestPath}`);
107
+ }
108
+ const commandsSource = path.resolve(sourceRoot, entry);
109
+ const relative = path.relative(path.resolve(sourceRoot), commandsSource);
110
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
111
+ throw new Error(`cli_commands.entry escapes its addon root in ${manifestPath}`);
112
+ }
113
+ const manifest = {
114
+ namespace: commands.namespace,
115
+ description: commands.description,
116
+ entry,
117
+ subcommands: commands.subcommands,
118
+ };
119
+ return { manifest, commandsSource };
120
+ }
48
121
  /**
49
122
  * Try to execute an addon-contributed CLI command
50
123
  *
@@ -44,6 +44,7 @@ import { serveHandler } from './serve.js';
44
44
  import { lintHandler } from './lint.js';
45
45
  import { feedbackHandler } from './feedback.js';
46
46
  import { sessionHandler } from './session.js';
47
+ import { sessionsHandler } from './sessions.js';
47
48
  import { sandboxHandler, sandboxHandlers } from './sandbox.js';
48
49
  import { diagnoseHandler } from './diagnose.js';
49
50
  import { localExecutorHandler, localExecutorServeHandler } from './local-executor.js';
@@ -62,7 +63,7 @@ helpHandler, versionHandler, doctorHandler, updateHandler, refreshHandler, regen
62
63
  // Framework management
63
64
  useHandler, listHandler, removeHandler, promoteHandler, installHandler, packagesHandler, marketplaceHandler, initHandler, setupHandler, issueHandler, issueAuditHandler, runHandler,
64
65
  // Project
65
- newBundleHandler, quickrefHandler, newProjectHandler,
66
+ newBundleHandler, quickrefHandler, newProjectHandler, sessionHandler, sessionsHandler,
66
67
  // Workspace
67
68
  statusHandler, wizardHandler, migrateWorkspaceHandler, rollbackWorkspaceHandler,
68
69
  // Subcommands
@@ -224,6 +225,7 @@ export const allHandlers = [
224
225
  feedbackHandler,
225
226
  // Session (#884)
226
227
  sessionHandler,
228
+ sessionsHandler,
227
229
  // Repo access policy (#1376)
228
230
  ...repoAccessHandlers,
229
231
  ];