@aiwg/cli 2026.9.15 → 2026.9.16

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 (33) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +42 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +119 -34
  3. package/agentic/code/providers/model-catalog.v1.json +259 -56
  4. package/dist/src/cli/handlers/sessions.js +159 -45
  5. package/dist/src/cli/handlers/use.js +20 -1
  6. package/dist/src/cli/scope-resolver.js +13 -0
  7. package/dist/src/cli/services/deployment-verification.js +33 -2
  8. package/dist/src/config/aiwg-config.js +39 -0
  9. package/dist/src/config/user-registry.js +9 -0
  10. package/dist/src/extensions/manifest.js +1 -0
  11. package/dist/src/models/model-capabilities.v1.json +119 -34
  12. package/dist/src/models/model-catalog.v1.json +259 -56
  13. package/dist/src/models/model-discovery.js +7 -0
  14. package/dist/src/models/provider-policy.js +1 -1
  15. package/dist/src/plugin/skill-command-translator.js +1 -0
  16. package/dist/src/providers/capability-matrix.yaml +42 -0
  17. package/dist/src/providers/grokbot-paths.js +103 -0
  18. package/dist/src/providers/provider-definitions.js +85 -1
  19. package/dist/src/providers/provider-inventory.js +1 -0
  20. package/dist/src/sessions/adapters/grokbot.js +49 -0
  21. package/dist/src/sessions/adapters/hermes.js +17 -9
  22. package/dist/src/sessions/adapters/opencode.js +12 -1
  23. package/dist/src/sessions/contracts.js +1 -1
  24. package/dist/src/sessions/export-plan.js +130 -4
  25. package/dist/src/sessions/fortemi-export-mapping.js +10 -6
  26. package/dist/src/sessions/fortemi-shard-blobs.js +78 -0
  27. package/dist/src/sessions/index.js +2 -0
  28. package/dist/src/sessions/workspace-discovery.js +1 -1
  29. package/dist/src/skills/deployer.js +17 -0
  30. package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
  31. package/package.json +1 -1
  32. package/tools/agents/deploy-agents.mjs +3 -2
  33. package/tools/agents/providers/grokbot.mjs +285 -0
@@ -1,8 +1,8 @@
1
- import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync, } from 'node:fs';
1
+ import { existsSync, linkSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { dirname, isAbsolute, join, resolve, } from 'node:path';
3
+ import { basename, dirname, isAbsolute, join, resolve, } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
- import { CLAUDE_ADAPTER_VERSION, ClaudeSessionAdapter, CODEX_ADAPTER_VERSION, CodexSessionAdapter, COPILOT_ADAPTER_VERSION, CopilotSessionAdapter, CURSOR_ADAPTER_VERSION, CursorSessionAdapter, FACTORY_ADAPTER_VERSION, FactorySessionAdapter, HERMES_ADAPTER_VERSION, HermesSessionAdapter, OPENCODE_ADAPTER_VERSION, OpenCodeSessionAdapter, OPENCLAW_ADAPTER_VERSION, OpenClawSessionAdapter, OPENHUMAN_ADAPTER_VERSION, OpenHumanSessionAdapter, PI_ADAPTER_VERSION, PiSessionAdapter, DEEPSEEK_HARNESS_ADAPTER_VERSION, DeepSeekHarnessSessionAdapter, OmpSessionAdapter, OMP_ADAPTER_VERSION, WARP_ADAPTER_VERSION, WarpSessionAdapter, DEVIN_DESKTOP_ADAPTER_VERSION, DevinDesktopSessionAdapter, CandidateExtractionService, GENERIC_ADAPTER_VERSION, GenericSessionInterchangeAdapter, IncrementalSessionImporter, ImportLeaseContentionError, FilesystemMemoryDestination, FilesystemPromotionDispositionCoordinator, MemoryPromotionGateway, SESSION_CONTRACT_VERSION, SESSION_PROVIDER_IDS, SessionContractError, SessionRepository, SessionSourceSchema, StructuralCandidateExtractor, resolveMemoryConsumerManifest, assertSessionProviderId, acquireImportLease, defaultDiscoveryManifestPath, discoverWorkspaceHistories, deriveSessionTimeline, importDiscoveryManifest, previewDiscoveryImport, publicDiscoveryManifest, readDiscoveryManifest, redactSourceLocator, sha256, parseTimelineGap, writeDiscoveryManifest, buildSessionExportPlan, reverifySessionExportPlan, buildSessionAiwgFortemiIndexExport, recoverAiwgFortemiIndexFromFullV1Shard, SESSION_EXPORT_PLAN_SCHEMA_VERSION, } from '../../sessions/index.js';
5
+ import { CLAUDE_ADAPTER_VERSION, ClaudeSessionAdapter, CODEX_ADAPTER_VERSION, CodexSessionAdapter, COPILOT_ADAPTER_VERSION, CopilotSessionAdapter, CURSOR_ADAPTER_VERSION, CursorSessionAdapter, FACTORY_ADAPTER_VERSION, FactorySessionAdapter, HERMES_ADAPTER_VERSION, HermesSessionAdapter, OPENCODE_ADAPTER_VERSION, OpenCodeSessionAdapter, OPENCLAW_ADAPTER_VERSION, OpenClawSessionAdapter, OPENHUMAN_ADAPTER_VERSION, OpenHumanSessionAdapter, GROKBOT_ADAPTER_VERSION, GrokbotSessionAdapter, PI_ADAPTER_VERSION, PiSessionAdapter, DEEPSEEK_HARNESS_ADAPTER_VERSION, DeepSeekHarnessSessionAdapter, OmpSessionAdapter, OMP_ADAPTER_VERSION, WARP_ADAPTER_VERSION, WarpSessionAdapter, DEVIN_DESKTOP_ADAPTER_VERSION, DevinDesktopSessionAdapter, CandidateExtractionService, GENERIC_ADAPTER_VERSION, GenericSessionInterchangeAdapter, IncrementalSessionImporter, ImportLeaseContentionError, FilesystemMemoryDestination, FilesystemPromotionDispositionCoordinator, MemoryPromotionGateway, SESSION_CONTRACT_VERSION, SESSION_PROVIDER_IDS, SessionContractError, SessionRepository, SessionSourceSchema, StructuralCandidateExtractor, resolveMemoryConsumerManifest, assertSessionProviderId, acquireImportLease, defaultDiscoveryManifestPath, discoverWorkspaceHistories, deriveSessionTimeline, importDiscoveryManifest, previewDiscoveryImport, publicDiscoveryManifest, readDiscoveryManifest, redactSourceLocator, sha256, parseTimelineGap, writeDiscoveryManifest, buildSessionExportPlan, reverifySessionExportPlan, buildSessionAiwgFortemiIndexExport, recoverAiwgFortemiIndexFromFullV1Shard, embedAttachmentsInFullV1Shard, recoverEmbeddedAttachments, SESSION_EXPORT_PLAN_SCHEMA_VERSION, } from '../../sessions/index.js';
6
6
  const JSON_CONTRACT_VERSION = '1.0.0';
7
7
  async function createLineMemoryPromotionDestination(projectRoot, manifestPath) {
8
8
  const modulePath = resolve(dirname(manifestPath), 'commands', 'line-memory.mjs');
@@ -70,6 +70,9 @@ Options:
70
70
  --workspace <id>, --tag <tag>, --limit <n>, --cursor <n>
71
71
  --page-size <n> Extraction scan page size (default 250, maximum 500)
72
72
  --max-documents <n> Explicit extraction safety limit; returns a partial receipt
73
+ --include-bytes Embed registered-output bytes in session export shards
74
+ --max-attachment-bytes <n> Maximum bytes per embedded output (default 67108864)
75
+ --force Permit replacing an existing export or recovery output
73
76
 
74
77
  Search filters:
75
78
  --date-from <rfc3339>, --date-to <rfc3339>
@@ -653,7 +656,7 @@ async function importSource(ctx, args) {
653
656
  if (provider !== 'generic' && provider !== 'claude' && provider !== 'codex'
654
657
  && provider !== 'copilot' && provider !== 'cursor' && provider !== 'factory'
655
658
  && provider !== 'hermes' && provider !== 'opencode' && provider !== 'openclaw'
656
- && provider !== 'openhuman' && provider !== 'pi' && provider !== 'omp' && provider !== 'deepseek-harness' && provider !== 'warp' && provider !== 'devin-desktop') {
659
+ && provider !== 'openhuman' && provider !== 'grokbot' && provider !== 'pi' && provider !== 'omp' && provider !== 'deepseek-harness' && provider !== 'warp' && provider !== 'devin-desktop') {
657
660
  throw new CliError('UNSUPPORTED_OPERATION', `session import is not implemented for ${provider}`, EXIT.unsupported);
658
661
  }
659
662
  const sourceId = requiredValue(args, '--source-id');
@@ -669,6 +672,7 @@ async function importSource(ctx, args) {
669
672
  const isOpenCode = provider === 'opencode';
670
673
  const isOpenClaw = provider === 'openclaw';
671
674
  const isOpenHuman = provider === 'openhuman';
675
+ const isGrokbot = provider === 'grokbot';
672
676
  const isPi = provider === 'pi';
673
677
  const isOmp = provider === 'omp';
674
678
  const isDsh = provider === 'deepseek-harness';
@@ -692,13 +696,15 @@ async function importSource(ctx, args) {
692
696
  ? new OpenClawSessionAdapter()
693
697
  : isOpenHuman
694
698
  ? new OpenHumanSessionAdapter()
695
- : isPi
696
- ? new PiSessionAdapter()
697
- : isWarp
698
- ? new WarpSessionAdapter()
699
- : isDevinDesktop
700
- ? new DevinDesktopSessionAdapter()
701
- : new GenericSessionInterchangeAdapter();
699
+ : isGrokbot
700
+ ? new GrokbotSessionAdapter()
701
+ : isPi
702
+ ? new PiSessionAdapter()
703
+ : isWarp
704
+ ? new WarpSessionAdapter()
705
+ : isDevinDesktop
706
+ ? new DevinDesktopSessionAdapter()
707
+ : new GenericSessionInterchangeAdapter();
702
708
  const locatorClass = isDsh ? 'deepseek-harness-session-v2-jsonl' : isOmp ? 'omp-session-v3-jsonl' : isClaude
703
709
  ? claudeLocatorClass(input)
704
710
  : isCodex
@@ -717,13 +723,15 @@ async function importSource(ctx, args) {
717
723
  ? 'openclaw-consistent-snapshot-jsonl'
718
724
  : isOpenHuman
719
725
  ? 'openhuman-enriched-jsonl'
720
- : isPi
721
- ? 'pi-session-v3-jsonl'
722
- : isWarp
723
- ? 'warp-markdown-export'
724
- : isDevinDesktop
725
- ? 'devin-desktop-cascade-hook-jsonl'
726
- : 'manual-export';
726
+ : isGrokbot
727
+ ? 'manual-export'
728
+ : isPi
729
+ ? 'pi-session-v3-jsonl'
730
+ : isWarp
731
+ ? 'warp-markdown-export'
732
+ : isDevinDesktop
733
+ ? 'devin-desktop-cascade-hook-jsonl'
734
+ : 'manual-export';
727
735
  const selectedSource = {
728
736
  provider, locator: input, locatorClass, sourceId,
729
737
  authorizedScope: { workspaceId, allowedRoots: [dirname(input)] },
@@ -749,11 +757,13 @@ async function importSource(ctx, args) {
749
757
  ? 'schema-16-event-v3-consistent-snapshot'
750
758
  : isOpenHuman
751
759
  ? 'schema-1-session-raw-enriched'
752
- : isWarp
753
- ? 'manual-lossy-markdown-export'
754
- : isDevinDesktop
755
- ? 'opt-in-cascade-transcript-hook'
756
- : 'manual-interchange',
760
+ : isGrokbot
761
+ ? 'manual-interchange'
762
+ : isWarp
763
+ ? 'manual-lossy-markdown-export'
764
+ : isDevinDesktop
765
+ ? 'opt-in-cascade-transcript-hook'
766
+ : 'manual-interchange',
757
767
  locatorClass, redactedLocator: redactSourceLocator(input),
758
768
  adapterVersion: isDsh ? DEEPSEEK_HARNESS_ADAPTER_VERSION : isOmp ? OMP_ADAPTER_VERSION : isClaude
759
769
  ? CLAUDE_ADAPTER_VERSION
@@ -773,13 +783,15 @@ async function importSource(ctx, args) {
773
783
  ? OPENCLAW_ADAPTER_VERSION
774
784
  : isOpenHuman
775
785
  ? OPENHUMAN_ADAPTER_VERSION
776
- : isWarp
777
- ? WARP_ADAPTER_VERSION
778
- : isDevinDesktop
779
- ? DEVIN_DESKTOP_ADAPTER_VERSION
780
- : GENERIC_ADAPTER_VERSION,
786
+ : isGrokbot
787
+ ? GROKBOT_ADAPTER_VERSION
788
+ : isWarp
789
+ ? WARP_ADAPTER_VERSION
790
+ : isDevinDesktop
791
+ ? DEVIN_DESKTOP_ADAPTER_VERSION
792
+ : GENERIC_ADAPTER_VERSION,
781
793
  sourceSchemaVersion: probe.sourceSchemaVersion,
782
- disposition: isWarp
794
+ disposition: isWarp || isGrokbot
783
795
  ? 'manual-only'
784
796
  : isClaude || isCodex || isCopilot || isCursor || isFactory || isHermes
785
797
  || isOpenCode || isOpenClaw || isOpenHuman || isDevinDesktop || isOmp || isDsh || isPi
@@ -804,14 +816,16 @@ async function importSource(ctx, args) {
804
816
  ? { 'native.openclaw': {} }
805
817
  : isOpenHuman
806
818
  ? { 'native.openhuman': {} }
807
- : isWarp
808
- ? { 'native.warp': {} }
809
- : isDevinDesktop
810
- ? { 'native.devin-desktop': {
811
- product: 'Devin Desktop',
812
- compatibilityProviderId: 'windsurf',
813
- } }
814
- : { 'native.generic': {} },
819
+ : isGrokbot
820
+ ? { 'native.grokbot': {} }
821
+ : isWarp
822
+ ? { 'native.warp': {} }
823
+ : isDevinDesktop
824
+ ? { 'native.devin-desktop': {
825
+ product: 'Devin Desktop',
826
+ compatibilityProviderId: 'windsurf',
827
+ } }
828
+ : { 'native.generic': {} },
815
829
  });
816
830
  if (isDryRun(ctx, args)) {
817
831
  return preview('import', { source, wouldInspect: true, wouldPersist: false });
@@ -883,6 +897,14 @@ async function exportBuild(ctx, args, repository, command) {
883
897
  const planPath = resolve(ctx.cwd, requiredValue(args, '--plan'));
884
898
  const outDirectory = resolve(ctx.cwd, requiredValue(args, '--out'));
885
899
  const shardName = args.values.get('--shard-name') ?? 'evidence.shard';
900
+ if (basename(shardName) !== shardName) {
901
+ throw new CliError('INVALID_ARGUMENT', '--shard-name must be a filename, not a path', EXIT.usage);
902
+ }
903
+ const finalPath = join(outDirectory, shardName);
904
+ const receiptPath = `${finalPath}.receipt.json`;
905
+ if (!args.flags.has('--force') && (existsSync(finalPath) || existsSync(receiptPath))) {
906
+ throw new CliError('OUTPUT_EXISTS', `refusing to overwrite an existing shard or receipt: ${finalPath} (use --force to replace)`, EXIT.usage);
907
+ }
886
908
  let plan;
887
909
  try {
888
910
  plan = JSON.parse(readFileSync(planPath, 'utf8'));
@@ -895,8 +917,13 @@ async function exportBuild(ctx, args, repository, command) {
895
917
  }
896
918
  // Recheck source changes before writing (#2564 acceptance criteria):
897
919
  // re-select from the live repository and reject on any digest drift.
898
- const { sessions, eventsBySessionId } = reverifySessionExportPlan(repository, plan);
899
- const index = buildSessionAiwgFortemiIndexExport(plan.workspaceId, sessions, eventsBySessionId, plan.outputs);
920
+ const { sessions, eventsBySessionId, catalogTagsBySessionId, outputBytesByRegistrationId, attachmentBytesByEventId, } = reverifySessionExportPlan(repository, plan, { projectRoot: ctx.cwd });
921
+ const includeBytes = args.flags.has('--include-bytes');
922
+ const outputs = plan.outputs.map((output) => ({
923
+ ...output,
924
+ bytesEmbedded: includeBytes && outputBytesByRegistrationId.has(output.registrationId),
925
+ }));
926
+ const index = buildSessionAiwgFortemiIndexExport(plan.workspaceId, sessions, eventsBySessionId, outputs, catalogTagsBySessionId);
900
927
  // aiwgFortemiIndexToKnowledgeShardWithReport targets the declared
901
928
  // full-v1/2.0.0 archive contract and validates losslessness -- unlike the
902
929
  // plain aiwgFortemiIndexToKnowledgeShard (core-v1), which silently embeds
@@ -910,16 +937,64 @@ async function exportBuild(ctx, args, repository, command) {
910
937
  if (!result.success || !result.archive) {
911
938
  throw new CliError('MALFORMED_SOURCE', `full-v1 shard conversion failed: ${JSON.stringify(result.losses)}`, EXIT.contract);
912
939
  }
913
- const bytes = result.archive;
940
+ let bytes = result.archive;
941
+ let embeddedAttachmentCount = 0;
942
+ let embeddedAttachmentBytes = 0;
943
+ if (includeBytes) {
944
+ const maximum = Number(args.values.get('--max-attachment-bytes') ?? 64 * 1024 * 1024);
945
+ if (!Number.isSafeInteger(maximum) || maximum <= 0) {
946
+ throw new CliError('INVALID_ARGUMENT', '--max-attachment-bytes must be a positive integer', EXIT.usage);
947
+ }
948
+ const attachments = plan.outputs.map((output) => {
949
+ const content = outputBytesByRegistrationId.get(output.registrationId);
950
+ if (content.length > maximum) {
951
+ throw new CliError('SOURCE_TOO_LARGE', `registered output exceeds --max-attachment-bytes: ${output.outputLocator}`, EXIT.contract);
952
+ }
953
+ return {
954
+ recordId: `output_${output.registrationId}`,
955
+ attachmentId: randomUUID(),
956
+ filename: basename(output.outputLocator),
957
+ mimeType: output.mediaType,
958
+ bytes: content,
959
+ };
960
+ });
961
+ for (const attachment of plan.attachments ?? []) {
962
+ const content = attachmentBytesByEventId.get(attachment.eventId);
963
+ if (content.length > maximum) {
964
+ throw new CliError('SOURCE_TOO_LARGE', `session attachment exceeds --max-attachment-bytes: ${attachment.filename}`, EXIT.contract);
965
+ }
966
+ attachments.push({
967
+ recordId: attachment.eventId,
968
+ attachmentId: randomUUID(),
969
+ filename: basename(attachment.filename),
970
+ mimeType: attachment.mediaType,
971
+ bytes: content,
972
+ });
973
+ }
974
+ embeddedAttachmentCount = attachments.length;
975
+ embeddedAttachmentBytes = attachments.reduce((sum, attachment) => sum + attachment.bytes.length, 0);
976
+ bytes = await embedAttachmentsInFullV1Shard(bytes, attachments);
977
+ }
914
978
  mkdirSync(outDirectory, { recursive: true, mode: 0o700 });
915
- const finalPath = join(outDirectory, shardName);
916
979
  // Interrupted writes must never be mistaken for a completed export: write
917
980
  // to a sibling temp file, then rename (atomic on the same filesystem), and
918
981
  // only then write the receipt -- the receipt's existence is the completion
919
982
  // signal, not the shard file's.
920
983
  const tempPath = join(outDirectory, `.${shardName}.${randomUUID()}.tmp`);
921
984
  writeFileSync(tempPath, bytes, { mode: 0o600 });
922
- renameSync(tempPath, finalPath);
985
+ if (args.flags.has('--force'))
986
+ renameSync(tempPath, finalPath);
987
+ else {
988
+ try {
989
+ linkSync(tempPath, finalPath);
990
+ unlinkSync(tempPath);
991
+ }
992
+ catch (error) {
993
+ if (existsSync(tempPath))
994
+ unlinkSync(tempPath);
995
+ throw new CliError('OUTPUT_EXISTS', `refusing to overwrite an existing shard: ${finalPath}`, EXIT.usage, error);
996
+ }
997
+ }
923
998
  const readBack = readFileSync(finalPath);
924
999
  const shardDigest = sha256(readBack);
925
1000
  if (readBack.length !== bytes.length) {
@@ -941,9 +1016,14 @@ async function exportBuild(ctx, args, repository, command) {
941
1016
  shardFile: shardName,
942
1017
  shardDigest,
943
1018
  shardBytes: readBack.length,
1019
+ embeddedAttachmentCount,
1020
+ embeddedAttachmentBytes,
944
1021
  };
945
- writeFileSync(`${finalPath}.receipt.json`, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
946
- return ok(command, { shard: finalPath, receipt: `${finalPath}.receipt.json`, ...receipt });
1022
+ writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, {
1023
+ mode: 0o600,
1024
+ flag: args.flags.has('--force') ? 'w' : 'wx',
1025
+ });
1026
+ return ok(command, { shard: finalPath, receipt: receiptPath, ...receipt });
947
1027
  }
948
1028
  async function exportVerify(ctx, args, command) {
949
1029
  const input = resolve(ctx.cwd, requiredValue(args, '--input'));
@@ -955,8 +1035,10 @@ async function exportVerify(ctx, args, command) {
955
1035
  throw new CliError('SOURCE_NOT_AUTHORIZED', `shard file is not readable: ${input}`, EXIT.usage);
956
1036
  }
957
1037
  let recovered;
1038
+ let embeddedAttachments;
958
1039
  try {
959
1040
  recovered = recoverAiwgFortemiIndexFromFullV1Shard(bytes);
1041
+ embeddedAttachments = recoverEmbeddedAttachments(bytes);
960
1042
  }
961
1043
  catch (error) {
962
1044
  const message = error instanceof Error ? error.message : String(error);
@@ -982,6 +1064,7 @@ async function exportVerify(ctx, args, command) {
982
1064
  shardBytes: bytes.length,
983
1065
  receiptChecked: existsSync(receiptPath),
984
1066
  receiptMatches,
1067
+ embeddedAttachmentCount: embeddedAttachments.length,
985
1068
  });
986
1069
  }
987
1070
  async function exportUnpack(ctx, args, command) {
@@ -1002,17 +1085,34 @@ async function exportUnpack(ctx, args, command) {
1002
1085
  const message = error instanceof Error ? error.message : String(error);
1003
1086
  throw new CliError('MALFORMED_SOURCE', `shard failed validation: ${message}`, EXIT.contract);
1004
1087
  }
1088
+ const attachments = recoverEmbeddedAttachments(bytes);
1005
1089
  mkdirSync(outDirectory, { recursive: true, mode: 0o700 });
1006
1090
  const finalPath = join(outDirectory, 'index.json');
1091
+ if (!args.flags.has('--force') && existsSync(finalPath)) {
1092
+ throw new CliError('OUTPUT_EXISTS', `refusing to overwrite recovered output: ${finalPath}`, EXIT.usage);
1093
+ }
1094
+ const attachmentPaths = attachments.map((attachment) => join(outDirectory, 'attachments', attachment.recordId, basename(attachment.filename)));
1095
+ if (!args.flags.has('--force')) {
1096
+ const conflict = attachmentPaths.find((attachmentPath) => existsSync(attachmentPath));
1097
+ if (conflict)
1098
+ throw new CliError('OUTPUT_EXISTS', `refusing to overwrite recovered attachment: ${conflict}`, EXIT.usage);
1099
+ }
1007
1100
  const tempPath = join(outDirectory, `.index.json.${randomUUID()}.tmp`);
1008
1101
  writeFileSync(tempPath, `${JSON.stringify(recovered, null, 2)}\n`, { mode: 0o600 });
1009
1102
  renameSync(tempPath, finalPath);
1103
+ for (const [index, attachment] of attachments.entries()) {
1104
+ const attachmentDirectory = join(outDirectory, 'attachments', attachment.recordId);
1105
+ mkdirSync(attachmentDirectory, { recursive: true, mode: 0o700 });
1106
+ writeFileSync(attachmentPaths[index], attachment.bytes, { mode: 0o600 });
1107
+ }
1010
1108
  return ok(command, {
1011
1109
  index: finalPath,
1012
1110
  schemaVersion: recovered.schema_version,
1013
1111
  itemCount: recovered.items.length,
1014
1112
  sessionRecordCount: recovered.items.filter((item) => item.type === 'aiwg.session').length,
1015
1113
  eventRecordCount: recovered.items.filter((item) => item.type === 'aiwg.session-event').length,
1114
+ recoveredAttachmentCount: attachments.length,
1115
+ recoveredAttachmentBytes: attachments.reduce((sum, attachment) => sum + attachment.bytes.length, 0),
1016
1116
  });
1017
1117
  }
1018
1118
  async function discoverWorkspace(ctx, args) {
@@ -1261,6 +1361,20 @@ function providerDisposition(provider) {
1261
1361
  },
1262
1362
  };
1263
1363
  }
1364
+ if (provider === 'grokbot') {
1365
+ return {
1366
+ provider, disposition: 'manual-only', operationalState: 'available',
1367
+ supportedOperations: ['inspect', 'stream'],
1368
+ acquisitionModes: ['manual-export'],
1369
+ reasonCode: 'MANUAL_SOURCE_SELECTION_REQUIRED',
1370
+ remediation: 'Select an authorized AIWG session interchange export; Grok Bot auto-discover is unsupported until a native locator exists.',
1371
+ evidence: {
1372
+ adapterVersion: GROKBOT_ADAPTER_VERSION,
1373
+ verifiedAt: '2026-09-15',
1374
+ documentation: 'docs/providers/grokbot-sessions.md',
1375
+ },
1376
+ };
1377
+ }
1264
1378
  if (provider === 'warp') {
1265
1379
  return {
1266
1380
  provider, disposition: 'manual-only', operationalState: 'available',
@@ -1373,7 +1487,7 @@ function parseArgs(argv) {
1373
1487
  '--inactivity-threshold',
1374
1488
  '--control-events',
1375
1489
  '--session', '--status', '--actor', '--group-by',
1376
- '--out', '--plan', '--input', '--shard-name',
1490
+ '--out', '--plan', '--input', '--shard-name', '--max-attachment-bytes',
1377
1491
  ]);
1378
1492
  let command;
1379
1493
  for (let index = 0; index < argv.length; index += 1) {
@@ -3102,6 +3102,7 @@ export class UseHandler {
3102
3102
  version: manifest.version ?? (await getVersionInfo()).version,
3103
3103
  source: 'bundled',
3104
3104
  manifestHash: await hashManifest(manifestPath),
3105
+ asPrimary: Boolean(explicitAddonProvider),
3105
3106
  });
3106
3107
  await writeAiwgConfig(projectDir, updated);
3107
3108
  config = updated;
@@ -3224,6 +3225,10 @@ export class UseHandler {
3224
3225
  }
3225
3226
  if (scope === 'user' && verbose) {
3226
3227
  ui.dim(` --scope user: deploy targets mirror to home-rooted paths per ADR-4 §2`);
3228
+ if (process.env.AIWG_USER_REGISTRY_PATH?.trim()) {
3229
+ ui.warn('AIWG_USER_REGISTRY_PATH is set (test override active); user registry is not writing to default ~/.aiwg/installed.json'
3230
+ + ` (active: ${process.env.AIWG_USER_REGISTRY_PATH})`);
3231
+ }
3227
3232
  }
3228
3233
  const filteredArgs = deployArgs.filter(a => a !== '--no-utils' && a !== '--no-project-local' && a !== '--ci-hooks-enabled' && a !== '--force' && a !== '--skip-conflicts' && a !== '--no-harness-agents');
3229
3234
  const deployFilteredArgs = removeFlagWithOptionalValue(filteredArgs, '--harness-agents');
@@ -3324,6 +3329,15 @@ export class UseHandler {
3324
3329
  message: `--scope user not supported for provider '${provider}' — see docs/customization/user-scope-deployment.md for the supported list`,
3325
3330
  };
3326
3331
  }
3332
+ if (provider === 'grokbot') {
3333
+ const { isGrokbotUserScopeConfigured, grokbotMissingRootRemediation } = await import('../../providers/grokbot-paths.js');
3334
+ if (!isGrokbotUserScopeConfigured()) {
3335
+ return {
3336
+ exitCode: 1,
3337
+ message: grokbotMissingRootRemediation(),
3338
+ };
3339
+ }
3340
+ }
3327
3341
  }
3328
3342
  // Pre-deployment collision check (skip in dry-run — nothing is written)
3329
3343
  if (!dryRun) {
@@ -3853,7 +3867,12 @@ export class UseHandler {
3853
3867
  commands: counts.commands,
3854
3868
  skills: counts.skills,
3855
3869
  rules: counts.rules,
3856
- }, { version: versionInfo.version, source: 'bundled', manifestHash: mHash });
3870
+ }, {
3871
+ version: versionInfo.version,
3872
+ source: 'bundled',
3873
+ manifestHash: mHash,
3874
+ asPrimary: Boolean(explicitProvider),
3875
+ });
3857
3876
  await writeAiwgConfig(projectDir, updatedConfig);
3858
3877
  }
3859
3878
  catch {
@@ -13,6 +13,7 @@ import { resolveOmpPaths } from '../providers/omp-paths.mjs';
13
13
  import { homedir } from 'node:os';
14
14
  import * as path from 'node:path';
15
15
  import { resolveHermesHome, resolveHermesHomePath } from '../providers/hermes-home.js';
16
+ import { resolveGrokbotSkillsDir } from '../providers/grokbot-paths.js';
16
17
  export const hermesHome = resolveHermesHome;
17
18
  function piAgentDir() {
18
19
  const configured = process.env.PI_CODING_AGENT_DIR;
@@ -216,6 +217,18 @@ export const USER_SCOPE_PATHS = {
216
217
  rules: path.join(homedir(), '.openhuman', '.aiwg', 'rules'),
217
218
  behaviors: '',
218
219
  },
220
+ get grokbot() {
221
+ // Fail-closed: skills path is empty until AIWG_GROKBOT_SKILLS_DIR is set.
222
+ // Never invent ~/.grokbot. See adr-grokbot-provider-target / #205/#207.
223
+ const skills = resolveGrokbotSkillsDir() || '';
224
+ return {
225
+ agents: '',
226
+ skills,
227
+ commands: '',
228
+ rules: '',
229
+ behaviors: '',
230
+ };
231
+ },
219
232
  factory: {
220
233
  // #1164 — Verified against Factory docs (docs.factory.ai/cli/configuration/skills).
221
234
  // Skills primary user-scope path is ~/.factory/skills/, NOT the
@@ -6,6 +6,7 @@ import { readUserRegistry } from '../../config/user-registry.js';
6
6
  import { inspectInstallation } from '../../installation/manager.mjs';
7
7
  import { getPackageRoot } from '../../channel/manager.mjs';
8
8
  import { getProviderDefinition, normalizeProviderDefinitionId, resolveProviderPathValue, } from '../../providers/provider-definitions.js';
9
+ import { resolveGrokbotSkillsDir } from '../../providers/grokbot-paths.js';
9
10
  import { diagnoseIntegratedProviderTransformationReceipt } from '../../providers/transformation-receipt-integration.js';
10
11
  import { diagnoseWorkspaceContext, providerContextContract, } from '../../smiths/context-pipeline/workspace-context.js';
11
12
  import { USER_SCOPE_PATHS } from '../scope-resolver.js';
@@ -31,6 +32,11 @@ const RESTART_NOTICES = {
31
32
  action: 'Reload the Cursor workspace so it reloads agents and rules.',
32
33
  reason: 'Cursor reads workspace agents and rules when the workspace opens.',
33
34
  },
35
+ grokbot: {
36
+ policy: 'restart-required',
37
+ action: 'Start a new Grok Bot agent chat (or re-read skills) so deployed AIWG context and skills are visible.',
38
+ reason: 'Grok Bot skill/context reload behavior is not yet verified; AIWG does not claim live refresh.',
39
+ },
34
40
  factory: {
35
41
  policy: 'restart-required',
36
42
  action: 'Restart the Factory droid runtime so it reloads deployed droids.',
@@ -362,7 +368,16 @@ export async function verifyProviderDeployment(options) {
362
368
  const deploymentRoot = options.outputRoot ?? options.projectRoot;
363
369
  const artifactPaths = options.scope === 'user'
364
370
  ? USER_SCOPE_PATHS[normalized] ?? definition.paths.artifacts
365
- : definition.paths.artifacts;
371
+ : { ...definition.paths.artifacts };
372
+ // Grok Bot: project-scope definition keeps skills null (fail-closed sentinel).
373
+ // When the operator configured AIWG_GROKBOT_SKILLS_DIR, count that absolute
374
+ // root so verification matches the deployer (#210 PUW).
375
+ if (normalized === 'grokbot') {
376
+ const configuredSkills = resolveGrokbotSkillsDir();
377
+ if (configuredSkills) {
378
+ artifactPaths.skills = configuredSkills;
379
+ }
380
+ }
366
381
  const writtenSince = options.invocationStartedAt
367
382
  ? Date.parse(options.invocationStartedAt)
368
383
  : Number.NaN;
@@ -402,7 +417,23 @@ export async function verifyProviderDeployment(options) {
402
417
  counts.skills += kernelCount;
403
418
  const artifactTotal = Object.values(counts).reduce((sum, value) => sum + value, 0);
404
419
  if (artifactTotal === 0) {
405
- findings.push(finding(normalized, 'provider-artifacts-missing', 'blocking', `No deployed provider or kernel artifacts were found for ${normalized}.`, `Re-run aiwg use ${options.requestedBundles[0] ?? 'all'} --provider ${normalized}.`, { kernelPath, kernelCount, counts }));
420
+ // Bridge-only providers (e.g. grokbot project scope without skills env)
421
+ // intentionally have null native artifact dirs and land AGENTS.md instead.
422
+ const bridgeName = definition.paths.configFile || definition.paths.contextFiles?.contextFile;
423
+ const bridgePath = bridgeName ? path.join(options.projectRoot, bridgeName) : '';
424
+ const bridgePresent = bridgePath ? await exists(bridgePath) : false;
425
+ const expectsNoNativeArtifacts = (!definition.paths.artifacts.agents
426
+ && !definition.paths.artifacts.commands
427
+ && !definition.paths.artifacts.skills
428
+ && !definition.paths.artifacts.rules
429
+ && !definition.paths.artifacts.behaviors
430
+ && !definition.paths.kernelSkills);
431
+ if (expectsNoNativeArtifacts && bridgePresent) {
432
+ findings.push(finding(normalized, 'provider-bridge-only', 'info', `No native artifact directories for ${normalized}; discover-first bridge ${bridgeName} is present.`, 'Set AIWG_GROKBOT_SKILLS_DIR (absolute) before --scope user / --global when native skill copies are required.', { bridgePath, counts }));
433
+ }
434
+ else {
435
+ findings.push(finding(normalized, 'provider-artifacts-missing', 'blocking', `No deployed provider or kernel artifacts were found for ${normalized}.`, `Re-run aiwg use ${options.requestedBundles[0] ?? 'all'} --provider ${normalized}.`, { kernelPath, kernelCount, counts }));
436
+ }
406
437
  }
407
438
  }
408
439
  findings.push(...await collectRegistryFindings(options, normalized, counts));
@@ -172,6 +172,7 @@ export function resolveDelivery(delivery) {
172
172
  * less aggressive at small fan-outs (10 is a safe middle ground)
173
173
  * - hermes: MCP sidecar; rate-limit depends on upstream provider, operator
174
174
  * should tune. Conservative 10 default.
175
+ * - grokbot: desktop multi-agent; conservative 4 until native evidence.
175
176
  * - unknown: conservative 4 default.
176
177
  */
177
178
  export const PROVIDER_PARALLELISM_DEFAULTS = {
@@ -186,6 +187,13 @@ export const PROVIDER_PARALLELISM_DEFAULTS = {
186
187
  windsurf: { max_parallel_subagents: 10, max_parallel_ralph_loops: 3, max_parallel_mc_missions: 6 },
187
188
  openclaw: { max_parallel_subagents: 10, max_parallel_ralph_loops: 3, max_parallel_mc_missions: 6 },
188
189
  hermes: { max_parallel_subagents: 10, max_parallel_ralph_loops: 3, max_parallel_mc_missions: 6 },
190
+ // Desktop multi-agent; conservative until native concurrency evidence exists.
191
+ grokbot: { max_parallel_subagents: 4, max_parallel_ralph_loops: 2, max_parallel_mc_missions: 4 },
192
+ // Conservative defaults for remaining stable/experimental harnesses (#249).
193
+ openhuman: { max_parallel_subagents: 4, max_parallel_ralph_loops: 2, max_parallel_mc_missions: 4 },
194
+ omp: { max_parallel_subagents: 4, max_parallel_ralph_loops: 2, max_parallel_mc_missions: 4 },
195
+ pi: { max_parallel_subagents: 4, max_parallel_ralph_loops: 2, max_parallel_mc_missions: 4 },
196
+ 'deepseek-harness': { max_parallel_subagents: 4, max_parallel_ralph_loops: 2, max_parallel_mc_missions: 4 },
189
197
  };
190
198
  const UNKNOWN_PROVIDER_PARALLELISM = {
191
199
  max_parallel_subagents: 4,
@@ -928,6 +936,35 @@ export async function writeAiwgConfig(projectDir, config) {
928
936
  }
929
937
  }
930
938
  }
939
+ /**
940
+ * Ensure `provider` appears in `config.providers` without wiping others (#247).
941
+ * First entry is the primary provider for parallelism/doctor labels.
942
+ * When `asPrimary` is true (explicit `--provider` deploy), move the provider to the front.
943
+ */
944
+ export function ensureProviderListed(config, provider, opts = {}) {
945
+ const normalized = provider.trim();
946
+ if (!normalized)
947
+ return config;
948
+ const providers = Array.isArray(config.providers) ? [...config.providers] : [];
949
+ const idx = providers.indexOf(normalized);
950
+ if (opts.asPrimary) {
951
+ if (idx === 0) {
952
+ config.providers = providers;
953
+ return config;
954
+ }
955
+ if (idx > 0)
956
+ providers.splice(idx, 1);
957
+ providers.unshift(normalized);
958
+ }
959
+ else if (idx < 0) {
960
+ providers.push(normalized);
961
+ }
962
+ else {
963
+ return config;
964
+ }
965
+ config.providers = providers;
966
+ return config;
967
+ }
931
968
  /**
932
969
  * Update the `installed` record for a framework after a successful deployment.
933
970
  * Returns the updated config (does not write to disk — caller must call writeAiwgConfig).
@@ -975,6 +1012,8 @@ export function updateInstalled(config, name, provider, counts, opts) {
975
1012
  delete existing.deployedArtifactHashes;
976
1013
  }
977
1014
  config.installed[name] = existing;
1015
+ // Keep providers[] aligned with deployedTo so doctor/parallelism see the right primary (#247).
1016
+ ensureProviderListed(config, provider, { asPrimary: opts.asPrimary === true });
978
1017
  return config;
979
1018
  }
980
1019
  /**
@@ -25,6 +25,15 @@ import { userScopeConfigPath } from '../cli/scope-resolver.js';
25
25
  * never set this — it exists so the test suite can avoid clobbering the real
26
26
  * `~/.aiwg/installed.json` on the developer's machine.
27
27
  */
28
+ /** True when AIWG_USER_REGISTRY_PATH redirects away from ~/.aiwg/installed.json. */
29
+ export function isUserRegistryPathOverrideActive() {
30
+ return Boolean(process.env.AIWG_USER_REGISTRY_PATH?.trim());
31
+ }
32
+ /**
33
+ * Operator-facing warning for a leftover test override (#246).
34
+ * Docs mark AIWG_USER_REGISTRY_PATH as test-only; doctor/status/use must surface it.
35
+ */
36
+ export const USER_REGISTRY_PATH_OVERRIDE_WARN = 'AIWG_USER_REGISTRY_PATH is set (test override active); user registry is not writing to default ~/.aiwg/installed.json';
28
37
  export function userRegistryPath() {
29
38
  const override = process.env.AIWG_USER_REGISTRY_PATH;
30
39
  if (override)
@@ -121,6 +121,7 @@ export const ProviderConfigSchema = z.object({
121
121
  'cursor',
122
122
  'deepseek-harness',
123
123
  'factory',
124
+ 'grokbot',
124
125
  'hermes',
125
126
  'opencode',
126
127
  'openclaw',