@aiwg/cli 2026.9.12 → 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 (36) 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 +348 -42
  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/claude.js +127 -3
  21. package/dist/src/sessions/adapters/grokbot.js +49 -0
  22. package/dist/src/sessions/adapters/hermes.js +17 -9
  23. package/dist/src/sessions/adapters/opencode.js +12 -1
  24. package/dist/src/sessions/contracts.js +1 -1
  25. package/dist/src/sessions/export-plan.js +243 -0
  26. package/dist/src/sessions/fortemi-export-mapping.js +300 -0
  27. package/dist/src/sessions/fortemi-shard-blobs.js +78 -0
  28. package/dist/src/sessions/fortemi-shard-recovery.js +151 -0
  29. package/dist/src/sessions/index.js +6 -0
  30. package/dist/src/sessions/output-lineage.js +43 -0
  31. package/dist/src/sessions/workspace-discovery.js +1 -1
  32. package/dist/src/skills/deployer.js +17 -0
  33. package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
  34. package/package.json +1 -1
  35. package/tools/agents/deploy-agents.mjs +3 -2
  36. package/tools/agents/providers/grokbot.mjs +285 -0
@@ -1,7 +1,8 @@
1
- import { existsSync, mkdirSync, realpathSync, statSync, } from 'node:fs';
2
- import { dirname, isAbsolute, resolve, } from 'node:path';
1
+ import { existsSync, linkSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { basename, dirname, isAbsolute, join, resolve, } from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
4
- 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, } 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';
5
6
  const JSON_CONTRACT_VERSION = '1.0.0';
6
7
  async function createLineMemoryPromotionDestination(projectRoot, manifestPath) {
7
8
  const modulePath = resolve(dirname(manifestPath), 'commands', 'line-memory.mjs');
@@ -33,6 +34,10 @@ Commands:
33
34
  show <session-id> Show a session with events and tags
34
35
  search <query> --workspace <id> Search authorized normalized content
35
36
  extract [session-id] --workspace <id> Extract structural candidates
37
+ export plan --workspace <id> --out <file> <session-id...> Select sessions into a reviewable plan
38
+ export build --plan <file> --out <dir> Build a Fortemi Knowledge Shard from a plan
39
+ export verify --input <shard> Validate a shard and its receipt
40
+ export unpack --input <shard> --out <dir> Recover a shard's records to disk
36
41
  candidates [--state <state>] List the candidate review queue
37
42
  review <id> <version> <state> Record an explicit review transition
38
43
  promote <id> <version> Preview promotion; use --confirm to write
@@ -65,6 +70,9 @@ Options:
65
70
  --workspace <id>, --tag <tag>, --limit <n>, --cursor <n>
66
71
  --page-size <n> Extraction scan page size (default 250, maximum 500)
67
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
68
76
 
69
77
  Search filters:
70
78
  --date-from <rfc3339>, --date-to <rfc3339>
@@ -409,6 +417,7 @@ async function executeCommand(ctx, args) {
409
417
  ? preview(command, { items, count: items.length, durableMemoryWrites: 0, scan })
410
418
  : ok(command, { items, count: items.length, durableMemoryWrites: 0, scan });
411
419
  }
420
+ case 'export': return exportCommand(ctx, args, repository);
412
421
  case 'candidates': {
413
422
  const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
414
423
  const state = candidateState(args.values.get('--state'));
@@ -647,7 +656,7 @@ async function importSource(ctx, args) {
647
656
  if (provider !== 'generic' && provider !== 'claude' && provider !== 'codex'
648
657
  && provider !== 'copilot' && provider !== 'cursor' && provider !== 'factory'
649
658
  && provider !== 'hermes' && provider !== 'opencode' && provider !== 'openclaw'
650
- && 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') {
651
660
  throw new CliError('UNSUPPORTED_OPERATION', `session import is not implemented for ${provider}`, EXIT.unsupported);
652
661
  }
653
662
  const sourceId = requiredValue(args, '--source-id');
@@ -663,6 +672,7 @@ async function importSource(ctx, args) {
663
672
  const isOpenCode = provider === 'opencode';
664
673
  const isOpenClaw = provider === 'openclaw';
665
674
  const isOpenHuman = provider === 'openhuman';
675
+ const isGrokbot = provider === 'grokbot';
666
676
  const isPi = provider === 'pi';
667
677
  const isOmp = provider === 'omp';
668
678
  const isDsh = provider === 'deepseek-harness';
@@ -686,15 +696,17 @@ async function importSource(ctx, args) {
686
696
  ? new OpenClawSessionAdapter()
687
697
  : isOpenHuman
688
698
  ? new OpenHumanSessionAdapter()
689
- : isPi
690
- ? new PiSessionAdapter()
691
- : isWarp
692
- ? new WarpSessionAdapter()
693
- : isDevinDesktop
694
- ? new DevinDesktopSessionAdapter()
695
- : 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();
696
708
  const locatorClass = isDsh ? 'deepseek-harness-session-v2-jsonl' : isOmp ? 'omp-session-v3-jsonl' : isClaude
697
- ? (input.endsWith('.hooks.jsonl') ? 'claude-hook-jsonl' : 'claude-transcript-jsonl')
709
+ ? claudeLocatorClass(input)
698
710
  : isCodex
699
711
  ? (input.endsWith('.app-server.jsonl') ? 'codex-app-server-jsonl' : 'codex-rollout-jsonl')
700
712
  : isCopilot
@@ -711,13 +723,15 @@ async function importSource(ctx, args) {
711
723
  ? 'openclaw-consistent-snapshot-jsonl'
712
724
  : isOpenHuman
713
725
  ? 'openhuman-enriched-jsonl'
714
- : isPi
715
- ? 'pi-session-v3-jsonl'
716
- : isWarp
717
- ? 'warp-markdown-export'
718
- : isDevinDesktop
719
- ? 'devin-desktop-cascade-hook-jsonl'
720
- : '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';
721
735
  const selectedSource = {
722
736
  provider, locator: input, locatorClass, sourceId,
723
737
  authorizedScope: { workspaceId, allowedRoots: [dirname(input)] },
@@ -726,7 +740,7 @@ async function importSource(ctx, args) {
726
740
  const source = SessionSourceSchema.parse({
727
741
  contractVersion: SESSION_CONTRACT_VERSION, sourceId, provider,
728
742
  providerProfile: isDsh ? 'native-session-v2-jsonl' : isOmp ? 'native-title-slot-v3' : isClaude
729
- ? 'documented-local-jsonl'
743
+ ? claudeProviderProfile(locatorClass)
730
744
  : isCodex
731
745
  ? 'app-server-v2-rollout-fallback'
732
746
  : isCopilot
@@ -743,11 +757,13 @@ async function importSource(ctx, args) {
743
757
  ? 'schema-16-event-v3-consistent-snapshot'
744
758
  : isOpenHuman
745
759
  ? 'schema-1-session-raw-enriched'
746
- : isWarp
747
- ? 'manual-lossy-markdown-export'
748
- : isDevinDesktop
749
- ? 'opt-in-cascade-transcript-hook'
750
- : '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',
751
767
  locatorClass, redactedLocator: redactSourceLocator(input),
752
768
  adapterVersion: isDsh ? DEEPSEEK_HARNESS_ADAPTER_VERSION : isOmp ? OMP_ADAPTER_VERSION : isClaude
753
769
  ? CLAUDE_ADAPTER_VERSION
@@ -767,13 +783,15 @@ async function importSource(ctx, args) {
767
783
  ? OPENCLAW_ADAPTER_VERSION
768
784
  : isOpenHuman
769
785
  ? OPENHUMAN_ADAPTER_VERSION
770
- : isWarp
771
- ? WARP_ADAPTER_VERSION
772
- : isDevinDesktop
773
- ? DEVIN_DESKTOP_ADAPTER_VERSION
774
- : 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,
775
793
  sourceSchemaVersion: probe.sourceSchemaVersion,
776
- disposition: isWarp
794
+ disposition: isWarp || isGrokbot
777
795
  ? 'manual-only'
778
796
  : isClaude || isCodex || isCopilot || isCursor || isFactory || isHermes
779
797
  || isOpenCode || isOpenClaw || isOpenHuman || isDevinDesktop || isOmp || isDsh || isPi
@@ -798,14 +816,16 @@ async function importSource(ctx, args) {
798
816
  ? { 'native.openclaw': {} }
799
817
  : isOpenHuman
800
818
  ? { 'native.openhuman': {} }
801
- : isWarp
802
- ? { 'native.warp': {} }
803
- : isDevinDesktop
804
- ? { 'native.devin-desktop': {
805
- product: 'Devin Desktop',
806
- compatibilityProviderId: 'windsurf',
807
- } }
808
- : { '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': {} },
809
829
  });
810
830
  if (isDryRun(ctx, args)) {
811
831
  return preview('import', { source, wouldInspect: true, wouldPersist: false });
@@ -837,6 +857,264 @@ async function importSource(ctx, args) {
837
857
  await lease.release();
838
858
  }
839
859
  }
860
+ /**
861
+ * `aiwg sessions export {plan,build,verify,unpack}` (#2564). Sub-verb after
862
+ * `export`, matching the issue's proposed shape:
863
+ * export plan --workspace ID --out selection.json SESSION_ID...
864
+ * export build --plan selection.json --out DIRECTORY
865
+ * export verify --input evidence.shard
866
+ * export unpack --input evidence.shard --out DIRECTORY
867
+ */
868
+ async function exportCommand(ctx, args, repository) {
869
+ const subVerb = requiredPositional(args, 0, 'export sub-command (plan|build|verify|unpack)');
870
+ const command = `export.${subVerb}`;
871
+ if (subVerb === 'plan')
872
+ return exportPlan(ctx, args, repository, command);
873
+ if (subVerb === 'build')
874
+ return exportBuild(ctx, args, repository, command);
875
+ if (subVerb === 'verify')
876
+ return exportVerify(ctx, args, command);
877
+ if (subVerb === 'unpack')
878
+ return exportUnpack(ctx, args, command);
879
+ throw new CliError('UNKNOWN_COMMAND', `unknown export sub-command: ${subVerb}`, EXIT.usage);
880
+ }
881
+ function exportPlan(ctx, args, repository, command) {
882
+ const workspaceId = normalizeWorkspaceId(ctx.cwd, requiredValue(args, '--workspace'));
883
+ const out = resolve(ctx.cwd, requiredValue(args, '--out'));
884
+ const flagSelection = args.values.get('--session')?.split(',').map((value) => value.trim()).filter(Boolean) ?? [];
885
+ const sessionIds = [...new Set([...flagSelection, ...args.positionals.slice(1)])];
886
+ const { plan } = buildSessionExportPlan(repository, { workspaceId, sessionIds, projectRoot: ctx.cwd });
887
+ mkdirSync(dirname(out), { recursive: true, mode: 0o700 });
888
+ writeFileSync(out, `${JSON.stringify(plan, null, 2)}\n`, { mode: 0o600 });
889
+ return ok(command, {
890
+ plan: out,
891
+ totals: plan.totals,
892
+ sessions: plan.sessions.map((entry) => entry.sessionId),
893
+ outputs: plan.outputs.length,
894
+ });
895
+ }
896
+ async function exportBuild(ctx, args, repository, command) {
897
+ const planPath = resolve(ctx.cwd, requiredValue(args, '--plan'));
898
+ const outDirectory = resolve(ctx.cwd, requiredValue(args, '--out'));
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
+ }
908
+ let plan;
909
+ try {
910
+ plan = JSON.parse(readFileSync(planPath, 'utf8'));
911
+ }
912
+ catch {
913
+ throw new CliError('MALFORMED_SOURCE', `plan file is not readable JSON: ${planPath}`, EXIT.usage);
914
+ }
915
+ if (plan.schemaVersion !== SESSION_EXPORT_PLAN_SCHEMA_VERSION) {
916
+ throw new CliError('UNKNOWN_SCHEMA_MAJOR', `plan schemaVersion ${plan.schemaVersion} is not supported by this build (expected ${SESSION_EXPORT_PLAN_SCHEMA_VERSION})`, EXIT.contract);
917
+ }
918
+ // Recheck source changes before writing (#2564 acceptance criteria):
919
+ // re-select from the live repository and reject on any digest drift.
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);
927
+ // aiwgFortemiIndexToKnowledgeShardWithReport targets the declared
928
+ // full-v1/2.0.0 archive contract and validates losslessness -- unlike the
929
+ // plain aiwgFortemiIndexToKnowledgeShard (core-v1), which silently embeds
930
+ // the whole input record as opaque note metadata regardless of whether it
931
+ // maps to any native component. The mapping in fortemi-export-mapping.ts
932
+ // was built specifically so every AIWG-specific field lands in a real
933
+ // full-v1 component (tags, provenance_events) rather than being smuggled
934
+ // through as an unmapped blob.
935
+ const { aiwgFortemiIndexToKnowledgeShardWithReport } = await import('@fortemi/core');
936
+ const result = await aiwgFortemiIndexToKnowledgeShardWithReport(index);
937
+ if (!result.success || !result.archive) {
938
+ throw new CliError('MALFORMED_SOURCE', `full-v1 shard conversion failed: ${JSON.stringify(result.losses)}`, EXIT.contract);
939
+ }
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
+ }
978
+ mkdirSync(outDirectory, { recursive: true, mode: 0o700 });
979
+ // Interrupted writes must never be mistaken for a completed export: write
980
+ // to a sibling temp file, then rename (atomic on the same filesystem), and
981
+ // only then write the receipt -- the receipt's existence is the completion
982
+ // signal, not the shard file's.
983
+ const tempPath = join(outDirectory, `.${shardName}.${randomUUID()}.tmp`);
984
+ writeFileSync(tempPath, bytes, { mode: 0o600 });
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
+ }
998
+ const readBack = readFileSync(finalPath);
999
+ const shardDigest = sha256(readBack);
1000
+ if (readBack.length !== bytes.length) {
1001
+ throw new CliError('IMPORT_INTERRUPTED', 'shard readback size did not match the written archive', EXIT.storage);
1002
+ }
1003
+ const receipt = {
1004
+ schemaVersion: '1.0.0',
1005
+ generatedAt: new Date().toISOString(),
1006
+ workspaceId: plan.workspaceId,
1007
+ planGeneratedAt: plan.generatedAt,
1008
+ sessionIds: plan.sessions.map((entry) => entry.sessionId),
1009
+ totals: plan.totals,
1010
+ indexSchemaVersion: index.schema_version,
1011
+ archiveProfile: result.profile,
1012
+ archiveSchemaVersion: result.schema_version,
1013
+ lossless: result.lossless,
1014
+ losses: result.losses,
1015
+ fortemiReceipt: result.receipt,
1016
+ shardFile: shardName,
1017
+ shardDigest,
1018
+ shardBytes: readBack.length,
1019
+ embeddedAttachmentCount,
1020
+ embeddedAttachmentBytes,
1021
+ };
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 });
1027
+ }
1028
+ async function exportVerify(ctx, args, command) {
1029
+ const input = resolve(ctx.cwd, requiredValue(args, '--input'));
1030
+ let bytes;
1031
+ try {
1032
+ bytes = readFileSync(input);
1033
+ }
1034
+ catch {
1035
+ throw new CliError('SOURCE_NOT_AUTHORIZED', `shard file is not readable: ${input}`, EXIT.usage);
1036
+ }
1037
+ let recovered;
1038
+ let embeddedAttachments;
1039
+ try {
1040
+ recovered = recoverAiwgFortemiIndexFromFullV1Shard(bytes);
1041
+ embeddedAttachments = recoverEmbeddedAttachments(bytes);
1042
+ }
1043
+ catch (error) {
1044
+ const message = error instanceof Error ? error.message : String(error);
1045
+ throw new CliError('MALFORMED_SOURCE', `shard failed validation: ${message}`, EXIT.contract);
1046
+ }
1047
+ const shardDigest = sha256(bytes);
1048
+ const receiptPath = `${input}.receipt.json`;
1049
+ let receiptMatches = null;
1050
+ if (existsSync(receiptPath)) {
1051
+ try {
1052
+ const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'));
1053
+ receiptMatches = receipt.shardDigest === shardDigest && receipt.shardBytes === bytes.length;
1054
+ }
1055
+ catch {
1056
+ receiptMatches = false;
1057
+ }
1058
+ }
1059
+ return ok(command, {
1060
+ valid: true,
1061
+ schemaVersion: recovered.schema_version,
1062
+ itemCount: recovered.items.length,
1063
+ shardDigest,
1064
+ shardBytes: bytes.length,
1065
+ receiptChecked: existsSync(receiptPath),
1066
+ receiptMatches,
1067
+ embeddedAttachmentCount: embeddedAttachments.length,
1068
+ });
1069
+ }
1070
+ async function exportUnpack(ctx, args, command) {
1071
+ const input = resolve(ctx.cwd, requiredValue(args, '--input'));
1072
+ const outDirectory = resolve(ctx.cwd, requiredValue(args, '--out'));
1073
+ let bytes;
1074
+ try {
1075
+ bytes = readFileSync(input);
1076
+ }
1077
+ catch {
1078
+ throw new CliError('SOURCE_NOT_AUTHORIZED', `shard file is not readable: ${input}`, EXIT.usage);
1079
+ }
1080
+ let recovered;
1081
+ try {
1082
+ recovered = recoverAiwgFortemiIndexFromFullV1Shard(bytes);
1083
+ }
1084
+ catch (error) {
1085
+ const message = error instanceof Error ? error.message : String(error);
1086
+ throw new CliError('MALFORMED_SOURCE', `shard failed validation: ${message}`, EXIT.contract);
1087
+ }
1088
+ const attachments = recoverEmbeddedAttachments(bytes);
1089
+ mkdirSync(outDirectory, { recursive: true, mode: 0o700 });
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
+ }
1100
+ const tempPath = join(outDirectory, `.index.json.${randomUUID()}.tmp`);
1101
+ writeFileSync(tempPath, `${JSON.stringify(recovered, null, 2)}\n`, { mode: 0o600 });
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
+ }
1108
+ return ok(command, {
1109
+ index: finalPath,
1110
+ schemaVersion: recovered.schema_version,
1111
+ itemCount: recovered.items.length,
1112
+ sessionRecordCount: recovered.items.filter((item) => item.type === 'aiwg.session').length,
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),
1116
+ });
1117
+ }
840
1118
  async function discoverWorkspace(ctx, args) {
841
1119
  const workspace = resolve(ctx.cwd, requiredValue(args, '--workspace'));
842
1120
  const manifest = await discoverWorkspaceHistories({
@@ -935,12 +1213,13 @@ function providerDisposition(provider) {
935
1213
  return {
936
1214
  provider, disposition: 'implemented', operationalState: 'available',
937
1215
  supportedOperations: ['discover', 'inspect', 'stream'],
938
- acquisitionModes: ['jsonl', 'hook'],
1216
+ acquisitionModes: ['jsonl', 'hook', 'manual-export'],
939
1217
  reasonCode: null,
940
- remediation: 'Authorize a Claude projects or hook root, then import an explicit JSONL file.',
1218
+ remediation: 'Authorize a Claude projects or hook root and import a JSONL file, '
1219
+ + 'or import an explicitly selected Claude web/account conversations.json export.',
941
1220
  evidence: {
942
1221
  adapterVersion: CLAUDE_ADAPTER_VERSION,
943
- verifiedAt: '2026-07-27',
1222
+ verifiedAt: '2026-09-15',
944
1223
  documentation: 'https://code.claude.com/docs/en/sessions',
945
1224
  },
946
1225
  };
@@ -1082,6 +1361,20 @@ function providerDisposition(provider) {
1082
1361
  },
1083
1362
  };
1084
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
+ }
1085
1378
  if (provider === 'warp') {
1086
1379
  return {
1087
1380
  provider, disposition: 'manual-only', operationalState: 'available',
@@ -1131,6 +1424,18 @@ function providerDisposition(provider) {
1131
1424
  evidence: { adapterVersion: null, verifiedAt: '2026-07-26' },
1132
1425
  };
1133
1426
  }
1427
+ function claudeLocatorClass(input) {
1428
+ if (input.endsWith('.hooks.jsonl') || input.endsWith('.hook.jsonl'))
1429
+ return 'claude-hook-jsonl';
1430
+ if (input.endsWith('.json'))
1431
+ return 'claude-web-export-json';
1432
+ return 'claude-transcript-jsonl';
1433
+ }
1434
+ function claudeProviderProfile(locatorClass) {
1435
+ if (locatorClass === 'claude-web-export-json')
1436
+ return 'web-account-export-conversations-json';
1437
+ return 'documented-local-jsonl';
1438
+ }
1134
1439
  function cursorLocatorClass(input) {
1135
1440
  if (input.endsWith('.md') || input.endsWith('.markdown'))
1136
1441
  return 'cursor-editor-markdown';
@@ -1182,6 +1487,7 @@ function parseArgs(argv) {
1182
1487
  '--inactivity-threshold',
1183
1488
  '--control-events',
1184
1489
  '--session', '--status', '--actor', '--group-by',
1490
+ '--out', '--plan', '--input', '--shard-name', '--max-attachment-bytes',
1185
1491
  ]);
1186
1492
  let command;
1187
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));