@aiwg/cli 2026.9.15 → 2026.9.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +80 -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 +26 -0
  7. package/dist/src/cli/services/deployment-verification.js +90 -2
  8. package/dist/src/config/aiwg-config.js +41 -0
  9. package/dist/src/config/user-registry.js +9 -0
  10. package/dist/src/dataset/conformance-types.d.ts +2 -1
  11. package/dist/src/dataset/conformance.js +10 -1
  12. package/dist/src/dataset/fortemi-live-qualification.d.ts +10 -0
  13. package/dist/src/dataset/fortemi-live-qualification.js +134 -0
  14. package/dist/src/extensions/manifest.js +1 -0
  15. package/dist/src/models/model-capabilities.v1.json +119 -34
  16. package/dist/src/models/model-catalog.v1.json +259 -56
  17. package/dist/src/models/model-discovery.js +7 -0
  18. package/dist/src/models/provider-policy.js +1 -1
  19. package/dist/src/plugin/skill-command-translator.js +1 -0
  20. package/dist/src/providers/capability-matrix.yaml +80 -0
  21. package/dist/src/providers/grok-build-inspect.js +245 -0
  22. package/dist/src/providers/grok-build-paths.js +92 -0
  23. package/dist/src/providers/grokbot-paths.js +103 -0
  24. package/dist/src/providers/provider-definitions.js +172 -2
  25. package/dist/src/providers/provider-inventory.js +2 -0
  26. package/dist/src/sessions/adapters/grokbot.js +49 -0
  27. package/dist/src/sessions/adapters/hermes.js +17 -9
  28. package/dist/src/sessions/adapters/opencode.js +12 -1
  29. package/dist/src/sessions/contracts.js +1 -1
  30. package/dist/src/sessions/export-plan.js +130 -4
  31. package/dist/src/sessions/fortemi-export-mapping.js +10 -6
  32. package/dist/src/sessions/fortemi-shard-blobs.js +78 -0
  33. package/dist/src/sessions/index.js +2 -0
  34. package/dist/src/sessions/workspace-discovery.js +1 -1
  35. package/dist/src/skills/deployer.js +17 -0
  36. package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
  37. package/package.json +1 -1
  38. package/schemas/dataset/conformance-receipt.v1.schema.json +1 -1
  39. package/tools/agents/deploy-agents.mjs +4 -2
  40. package/tools/agents/providers/grok-build.mjs +153 -0
  41. package/tools/agents/providers/grokbot.mjs +285 -0
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Grok Bot session adapter — manual-export only until product evidence exists
3
+ * for a native session filesystem or API locator.
4
+ *
5
+ * Auto-discover is unsupported and must never scrape invented ~/.grokbot,
6
+ * Cursor session paths, or other unverified roots.
7
+ *
8
+ * Authorized imports reuse the AIWG generic session interchange format so
9
+ * operators can supply an explicit export without inventing a Grok schema.
10
+ *
11
+ * @see docs/providers/grokbot-sessions.md
12
+ * @issue #215
13
+ */
14
+ import { SessionContractError, } from '../contracts.js';
15
+ import { GenericSessionInterchangeAdapter, } from './generic.js';
16
+ export const GROKBOT_ADAPTER_VERSION = '1.0.0';
17
+ export const GROKBOT_EXPORT_SCHEMA_VERSION = '1.0.0';
18
+ export const GROKBOT_LOCATOR_CLASS = 'manual-export';
19
+ export class GrokbotSessionAdapter {
20
+ provider = 'grokbot';
21
+ adapterVersion = GROKBOT_ADAPTER_VERSION;
22
+ disposition = 'manual-only';
23
+ supportedOperations = ['inspect', 'stream'];
24
+ acquisitionModes = ['manual-export'];
25
+ interchange;
26
+ constructor(limits) {
27
+ this.interchange = new GenericSessionInterchangeAdapter(limits);
28
+ }
29
+ async *discover(_scope) {
30
+ throw new SessionContractError('UNSUPPORTED_OPERATION', 'Grok Bot session auto-discover is unsupported until a verified native locator exists; '
31
+ + 'select an authorized manual export explicitly. AIWG does not scrape ~/.grokbot, '
32
+ + '~/grokbot-skills, or Cursor session paths.');
33
+ }
34
+ async inspect(source) {
35
+ this.assertManualExport(source);
36
+ return this.interchange.inspect(source);
37
+ }
38
+ async *stream(source, cursor) {
39
+ this.assertManualExport(source);
40
+ yield* this.interchange.stream(source, cursor);
41
+ }
42
+ assertManualExport(source) {
43
+ if (source.locatorClass !== GROKBOT_LOCATOR_CLASS) {
44
+ throw new SessionContractError('UNSUPPORTED_OPERATION', `Grok Bot supports only locatorClass "${GROKBOT_LOCATOR_CLASS}" until native session evidence exists `
45
+ + `(got "${source.locatorClass}"). Do not reuse Cursor session locators.`);
46
+ }
47
+ }
48
+ }
49
+ //# sourceMappingURL=grokbot.js.map
@@ -4,23 +4,31 @@ import { readBoundedJsonLines, streamBoundedJsonLines, } from '../readers.js';
4
4
  export const HERMES_ADAPTER_VERSION = '1.0.0';
5
5
  export const HERMES_EXPORT_SCHEMA_VERSION = '1.0.0';
6
6
  export const HERMES_NATIVE_SCHEMA_VERSION = '23.0.0';
7
+ const sqliteBoolean = z.union([z.boolean(), z.literal(0), z.literal(1)])
8
+ .transform((value) => Boolean(value));
9
+ function nullableOptional(schema) {
10
+ return z.preprocess((value) => value === null ? undefined : value, schema.optional());
11
+ }
7
12
  const MessageSchema = z.object({
8
13
  id: z.union([z.string(), z.number()]),
9
14
  role: z.string().min(1),
10
15
  content: z.unknown().optional(),
11
- tool_call_id: z.string().optional(),
16
+ tool_call_id: nullableOptional(z.string()),
12
17
  tool_calls: z.unknown().optional(),
13
- tool_name: z.string().optional(),
18
+ tool_name: nullableOptional(z.string()),
14
19
  timestamp: z.number().optional(),
15
- token_count: z.number().optional(),
16
- finish_reason: z.string().optional(),
17
- reasoning: z.string().optional(),
18
- reasoning_content: z.string().optional(),
20
+ token_count: nullableOptional(z.number()),
21
+ finish_reason: nullableOptional(z.string()),
22
+ reasoning: nullableOptional(z.string()),
23
+ reasoning_content: nullableOptional(z.string()),
19
24
  reasoning_details: z.unknown().optional(),
20
25
  codex_reasoning_items: z.unknown().optional(),
21
26
  }).passthrough();
22
27
  const ExportSchema = z.object({
23
- schemaVersion: z.union([z.string(), z.number()]),
28
+ // Hermes CLI exports currently omit a schema marker. They use the same
29
+ // schema-23 record shape that the adapter already validates; explicit
30
+ // markers still pass through the major-version gate below.
31
+ schemaVersion: z.union([z.string(), z.number()]).default(HERMES_NATIVE_SCHEMA_VERSION),
24
32
  id: z.string().min(1),
25
33
  source: z.string().min(1),
26
34
  user_id: z.string().nullable().optional(),
@@ -31,8 +39,8 @@ const ExportSchema = z.object({
31
39
  started_at: z.number(),
32
40
  ended_at: z.number().nullable().optional(),
33
41
  end_reason: z.string().nullable().optional(),
34
- archived: z.boolean().optional(),
35
- inactive: z.boolean().optional(),
42
+ archived: sqliteBoolean.optional(),
43
+ inactive: sqliteBoolean.optional(),
36
44
  cwd: z.string().nullable().optional(),
37
45
  git_branch: z.string().nullable().optional(),
38
46
  routing_key: z.string().nullable().optional(),
@@ -343,7 +343,18 @@ function normalizeMessage(message, sequence, locatorClass, common) {
343
343
  toolCallId: part.callID,
344
344
  toolState: part.state,
345
345
  attachment: part.type === 'file'
346
- ? { mime: part.mime, filename: part.filename, urlPresent: Boolean(part.url) }
346
+ ? {
347
+ mime: part.mime,
348
+ filename: part.filename,
349
+ displayName: part.filename,
350
+ urlPresent: Boolean(part.url),
351
+ // Preserve only locally recoverable byte locators. Remote URLs
352
+ // remain presence-only metadata and are never fetched by export.
353
+ dataBytes: part.url?.startsWith('data:')
354
+ ? [...Buffer.from(part.url.slice(part.url.indexOf(',') + 1), part.url.includes(';base64,') ? 'base64' : 'utf8')]
355
+ : undefined,
356
+ fileUrl: part.url?.startsWith('file:') ? part.url : undefined,
357
+ }
347
358
  : undefined,
348
359
  opaqueContent: !['text', 'reasoning', 'tool', 'file'].includes(part.type),
349
360
  unknownFields: unknownFields(part, PART_KEYS),
@@ -3,7 +3,7 @@ import { z } from 'zod';
3
3
  export const SESSION_CONTRACT_VERSION = '1.0.0';
4
4
  export const SESSION_PROVIDER_IDS = [
5
5
  'claude', 'codex', 'copilot', 'cursor', 'factory', 'hermes',
6
- 'opencode', 'openclaw', 'openhuman', 'pi', 'omp', 'deepseek-harness', 'warp', 'devin-desktop', 'generic',
6
+ 'opencode', 'openclaw', 'openhuman', 'grokbot', 'pi', 'omp', 'deepseek-harness', 'warp', 'devin-desktop', 'generic',
7
7
  ];
8
8
  export const SessionProviderIdSchema = z.enum(SESSION_PROVIDER_IDS);
9
9
  export const SESSION_PROVIDER_ALIASES = Object.freeze({
@@ -6,17 +6,68 @@
6
6
  * silently exporting drifted data.
7
7
  */
8
8
  import { createHash } from 'node:crypto';
9
+ import { lstatSync, readFileSync, realpathSync } from 'node:fs';
10
+ import { resolve, sep } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
9
12
  import { SessionContractError } from './contracts.js';
10
13
  import { buildSessionAiwgFortemiIndexExport, SESSION_EXPORT_INDEX_SCHEMA_VERSION, } from './fortemi-export-mapping.js';
11
14
  import { discoverSessionOutputCandidates } from './output-lineage.js';
12
15
  import { FilesystemDerivedOutputIndex } from './output-registration.js';
13
- export const SESSION_EXPORT_PLAN_SCHEMA_VERSION = '1.0.0';
16
+ export const SESSION_EXPORT_PLAN_SCHEMA_VERSION = '1.1.0';
14
17
  function eventDigestSummary(events) {
15
18
  const hash = createHash('sha256');
16
19
  for (const event of events)
17
20
  hash.update(event.digest);
18
21
  return `sha256:${hash.digest('hex')}`;
19
22
  }
23
+ function sha256(value) {
24
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
25
+ }
26
+ function sessionMetadataDigest(session, catalogTags) {
27
+ return sha256(JSON.stringify({ session, catalogTags: [...catalogTags].sort() }));
28
+ }
29
+ function localEventAttachment(event, projectRoot) {
30
+ const nativeEnvelope = Object.entries(event.extensions)
31
+ .find(([key, value]) => key.startsWith('native.') && value && typeof value === 'object')?.[1];
32
+ const attachment = event.extensions.attachment ?? nativeEnvelope?.attachment;
33
+ if (!attachment || typeof attachment !== 'object')
34
+ return null;
35
+ const value = attachment;
36
+ const filename = typeof value.displayName === 'string' && value.displayName.length > 0
37
+ ? value.displayName
38
+ : typeof value.filename === 'string' && value.filename.length > 0 && !value.filename.startsWith('[REDACTED:')
39
+ ? value.filename
40
+ : `${event.eventId}.bin`;
41
+ const mediaType = typeof value.mime === 'string' && value.mime.length > 0 ? value.mime : 'application/octet-stream';
42
+ if (Array.isArray(value.dataBytes)
43
+ && value.dataBytes.every((item) => Number.isInteger(item) && Number(item) >= 0 && Number(item) <= 255)) {
44
+ return { filename, mediaType, bytes: Uint8Array.from(value.dataBytes) };
45
+ }
46
+ if (typeof value.dataUrl === 'string') {
47
+ const match = /^data:([^;,]*)(;base64)?,(.*)$/s.exec(value.dataUrl);
48
+ if (!match)
49
+ throw new SessionContractError('MALFORMED_SOURCE', `invalid data URL attachment: ${event.eventId}`);
50
+ const bytes = match[2]
51
+ ? Buffer.from(match[3], 'base64')
52
+ : Buffer.from(decodeURIComponent(match[3]), 'utf8');
53
+ return { filename, mediaType: match[1] || mediaType, bytes };
54
+ }
55
+ if (typeof value.fileUrl === 'string') {
56
+ if (!projectRoot)
57
+ throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'project root is required for file attachments');
58
+ const root = realpathSync(projectRoot);
59
+ const candidate = fileURLToPath(value.fileUrl);
60
+ const stat = lstatSync(candidate);
61
+ if (stat.isSymbolicLink())
62
+ throw new SessionContractError('SOURCE_NOT_AUTHORIZED', `attachment is a symbolic link: ${event.eventId}`);
63
+ const actual = realpathSync(candidate);
64
+ if (actual !== root && !actual.startsWith(`${root}${sep}`)) {
65
+ throw new SessionContractError('SOURCE_OUTSIDE_ALLOWED_ROOT', `attachment resolves outside the project: ${event.eventId}`);
66
+ }
67
+ return { filename, mediaType, bytes: readFileSync(actual) };
68
+ }
69
+ return null;
70
+ }
20
71
  /**
21
72
  * Selects the requested sessions from the repository and builds a plan.
22
73
  * Rejects an empty or duplicate selection, and any session id the
@@ -40,6 +91,7 @@ export function buildSessionExportPlan(repository, options) {
40
91
  throw new SessionContractError('SOURCE_NOT_AUTHORIZED', `session is unknown or not authorized for this workspace: ${sessionId}`);
41
92
  }
42
93
  const events = repository.listEvents(sessionId, options.workspaceId);
94
+ const catalogTags = repository.listTags(sessionId, options.workspaceId);
43
95
  sessions.push(session);
44
96
  eventsBySessionId.set(sessionId, events);
45
97
  entries.push({
@@ -48,6 +100,8 @@ export function buildSessionExportPlan(repository, options) {
48
100
  provider: session.provider,
49
101
  sourceId: session.sourceId,
50
102
  sourceDigest: session.sourceDigest,
103
+ metadataDigest: sessionMetadataDigest(session, catalogTags),
104
+ catalogTags,
51
105
  eventCount: events.length,
52
106
  eventDigestSummary: eventDigestSummary(events),
53
107
  });
@@ -61,11 +115,28 @@ export function buildSessionExportPlan(repository, options) {
61
115
  registrationId: candidate.registration.registrationId,
62
116
  outputLocator: candidate.registration.output.locator,
63
117
  outputDigest: candidate.registration.output.digest,
118
+ outputByteLength: candidate.registration.output.byteLength,
64
119
  mediaType: candidate.registration.output.mediaType,
65
120
  lineage: candidate.lineage,
66
121
  matchReason: candidate.matchReason,
67
122
  }));
68
- const index = buildSessionAiwgFortemiIndexExport(options.workspaceId, sessions, eventsBySessionId, outputs);
123
+ const attachments = [];
124
+ for (const session of sessions) {
125
+ for (const event of eventsBySessionId.get(session.sessionId) ?? []) {
126
+ const attachment = localEventAttachment(event, options.projectRoot);
127
+ if (attachment)
128
+ attachments.push({
129
+ sessionId: session.sessionId,
130
+ eventId: event.eventId,
131
+ filename: attachment.filename,
132
+ mediaType: attachment.mediaType,
133
+ digest: sha256(attachment.bytes),
134
+ byteLength: attachment.bytes.length,
135
+ });
136
+ }
137
+ }
138
+ const catalogTagsBySessionId = new Map(entries.map((entry) => [entry.sessionId, entry.catalogTags]));
139
+ const index = buildSessionAiwgFortemiIndexExport(options.workspaceId, sessions, eventsBySessionId, outputs, catalogTagsBySessionId);
69
140
  const plan = {
70
141
  schemaVersion: SESSION_EXPORT_PLAN_SCHEMA_VERSION,
71
142
  indexSchemaVersion: SESSION_EXPORT_INDEX_SCHEMA_VERSION,
@@ -73,10 +144,12 @@ export function buildSessionExportPlan(repository, options) {
73
144
  workspaceId: options.workspaceId,
74
145
  sessions: entries,
75
146
  outputs,
147
+ attachments,
76
148
  totals: {
77
149
  sessionCount: sessions.length,
78
150
  eventCount: entries.reduce((sum, entry) => sum + entry.eventCount, 0),
79
151
  outputCount: outputs.length,
152
+ attachmentCount: attachments.length,
80
153
  recordCount: index.items.length,
81
154
  },
82
155
  preview: {
@@ -94,9 +167,10 @@ export function buildSessionExportPlan(repository, options) {
94
167
  * acceptance criteria: "Plans reject stale sources ... changed analysis
95
168
  * bytes").
96
169
  */
97
- export function reverifySessionExportPlan(repository, plan) {
170
+ export function reverifySessionExportPlan(repository, plan, options = {}) {
98
171
  const sessions = [];
99
172
  const eventsBySessionId = new Map();
173
+ const catalogTagsBySessionId = new Map();
100
174
  for (const entry of plan.sessions) {
101
175
  const session = repository.getSession(entry.sessionId, plan.workspaceId);
102
176
  if (!session) {
@@ -105,13 +179,65 @@ export function reverifySessionExportPlan(repository, plan) {
105
179
  if (session.sourceDigest !== entry.sourceDigest) {
106
180
  throw new SessionContractError('SCHEMA_DRIFT', `session source changed since the plan was generated: ${entry.sessionId}`);
107
181
  }
182
+ const catalogTags = repository.listTags(entry.sessionId, plan.workspaceId);
183
+ if (entry.metadataDigest !== sessionMetadataDigest(session, catalogTags)
184
+ || JSON.stringify(entry.catalogTags) !== JSON.stringify(catalogTags)) {
185
+ throw new SessionContractError('SCHEMA_DRIFT', `session metadata or tags changed since the plan was generated: ${entry.sessionId}`);
186
+ }
108
187
  const events = repository.listEvents(entry.sessionId, plan.workspaceId);
109
188
  if (events.length !== entry.eventCount || eventDigestSummary(events) !== entry.eventDigestSummary) {
110
189
  throw new SessionContractError('SCHEMA_DRIFT', `session events changed since the plan was generated: ${entry.sessionId}`);
111
190
  }
112
191
  sessions.push(session);
113
192
  eventsBySessionId.set(entry.sessionId, events);
193
+ catalogTagsBySessionId.set(entry.sessionId, catalogTags);
194
+ }
195
+ const outputBytesByRegistrationId = new Map();
196
+ if (plan.outputs.length > 0) {
197
+ if (!options.projectRoot) {
198
+ throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'project root is required to verify planned outputs');
199
+ }
200
+ const root = realpathSync(options.projectRoot);
201
+ for (const output of plan.outputs) {
202
+ const candidate = resolve(root, output.outputLocator);
203
+ let stat;
204
+ try {
205
+ stat = lstatSync(candidate);
206
+ }
207
+ catch {
208
+ throw new SessionContractError('SCHEMA_DRIFT', `registered output is missing since the plan was generated: ${output.outputLocator}`);
209
+ }
210
+ if (stat.isSymbolicLink()) {
211
+ throw new SessionContractError('SOURCE_NOT_AUTHORIZED', `planned output is a symbolic link: ${output.outputLocator}`);
212
+ }
213
+ if (!stat.isFile()) {
214
+ throw new SessionContractError('SCHEMA_DRIFT', `planned output is no longer a regular file: ${output.outputLocator}`);
215
+ }
216
+ if (stat.size !== output.outputByteLength) {
217
+ throw new SessionContractError('SCHEMA_DRIFT', `registered output changed since the plan was generated: ${output.outputLocator}`);
218
+ }
219
+ const actual = realpathSync(candidate);
220
+ if (actual !== root && !actual.startsWith(`${root}${sep}`)) {
221
+ throw new SessionContractError('SOURCE_OUTSIDE_ALLOWED_ROOT', 'planned output resolves outside the project');
222
+ }
223
+ const bytes = readFileSync(actual);
224
+ if (sha256(bytes) !== output.outputDigest) {
225
+ throw new SessionContractError('SCHEMA_DRIFT', `registered output changed since the plan was generated: ${output.outputLocator}`);
226
+ }
227
+ outputBytesByRegistrationId.set(output.registrationId, bytes);
228
+ }
229
+ }
230
+ const attachmentBytesByEventId = new Map();
231
+ for (const planned of plan.attachments ?? []) {
232
+ const event = eventsBySessionId.get(planned.sessionId)?.find((candidate) => candidate.eventId === planned.eventId);
233
+ const attachment = event ? localEventAttachment(event, options.projectRoot) : null;
234
+ if (!attachment || attachment.bytes.length !== planned.byteLength || sha256(attachment.bytes) !== planned.digest) {
235
+ throw new SessionContractError('SCHEMA_DRIFT', `session attachment changed since the plan was generated: ${planned.eventId}`);
236
+ }
237
+ attachmentBytesByEventId.set(planned.eventId, attachment.bytes);
114
238
  }
115
- return { sessions, eventsBySessionId };
239
+ return {
240
+ sessions, eventsBySessionId, catalogTagsBySessionId, outputBytesByRegistrationId, attachmentBytesByEventId,
241
+ };
116
242
  }
117
243
  //# sourceMappingURL=export-plan.js.map
@@ -17,7 +17,7 @@ export function sessionRecordLocator(session) {
17
17
  export function sessionEventRecordLocator(session, event) {
18
18
  return `${sessionRecordLocator(session)}/event/${event.eventId}`;
19
19
  }
20
- export function sessionToAiwgFortemiRecord(session) {
20
+ export function sessionToAiwgFortemiRecord(session, catalogTags = []) {
21
21
  const locator = sessionRecordLocator(session);
22
22
  const updatedAt = session.updatedAt ?? session.startedAt ?? new Date(0).toISOString();
23
23
  return {
@@ -39,7 +39,11 @@ export function sessionToAiwgFortemiRecord(session) {
39
39
  title: session.intent.title ?? `${session.provider} session ${session.nativeSessionId}`,
40
40
  text: session.intent.summary ?? `AIWG session ${session.sessionId} (${session.provider}, ${session.lifecycle})`,
41
41
  facets: {},
42
- tags: [tag('provider', session.provider), tag('lifecycle', session.lifecycle)],
42
+ tags: [
43
+ tag('provider', session.provider),
44
+ tag('lifecycle', session.lifecycle),
45
+ ...catalogTags.map((value) => tag('catalogTag', value)),
46
+ ],
43
47
  concepts: [],
44
48
  relationships: [],
45
49
  provenance: [{
@@ -174,7 +178,7 @@ export function sessionOutputToAiwgFortemiRecord(session, output) {
174
178
  },
175
179
  title: `registered output for ${session.sessionId}`,
176
180
  text: `${output.mediaType} output registered at ${output.outputLocator} (${output.matchReason}); `
177
- + 'bytes not embedded -- reference and digest only.',
181
+ + (output.bytesEmbedded ? 'original bytes embedded as a native attachment.' : 'bytes not embedded -- reference and digest only.'),
178
182
  facets: {},
179
183
  tags: [tag('mediaType', output.mediaType), tag('lineage', 'registered')],
180
184
  concepts: [],
@@ -206,7 +210,7 @@ export function sessionOutputToAiwgFortemiRecord(session, output) {
206
210
  registrationId: output.registrationId,
207
211
  outputLocator: output.outputLocator,
208
212
  matchReason: output.matchReason,
209
- bytesEmbedded: false,
213
+ bytesEmbedded: output.bytesEmbedded === true,
210
214
  },
211
215
  }],
212
216
  privacy: { classification: 'private', pii: false },
@@ -271,11 +275,11 @@ function exportManifestRecord(repoRef, generatedAt) {
271
275
  * session catalog's workspace id, not a git repo -- the field is repurposed
272
276
  * honestly rather than left to imply something untrue).
273
277
  */
274
- export function buildSessionAiwgFortemiIndexExport(repoRef, sessions, eventsBySessionId, outputs = []) {
278
+ export function buildSessionAiwgFortemiIndexExport(repoRef, sessions, eventsBySessionId, outputs = [], catalogTagsBySessionId = new Map()) {
275
279
  const generatedAt = new Date().toISOString();
276
280
  const items = [exportManifestRecord(repoRef, generatedAt)];
277
281
  for (const session of sessions) {
278
- items.push(sessionToAiwgFortemiRecord(session));
282
+ items.push(sessionToAiwgFortemiRecord(session, catalogTagsBySessionId.get(session.sessionId) ?? []));
279
283
  for (const event of eventsBySessionId.get(session.sessionId) ?? []) {
280
284
  items.push(sessionEventToAiwgFortemiRecord(session, event));
281
285
  }
@@ -0,0 +1,78 @@
1
+ import { computeBlobHash, packTarGz, sha256Hex, unpackTarGz, validateFullV1ShardArchive, } from '@fortemi/core';
2
+ function jsonLines(values) {
3
+ return new TextEncoder().encode(`${values.map((value) => JSON.stringify(value)).join('\n')}\n`);
4
+ }
5
+ export async function embedAttachmentsInFullV1Shard(archive, attachments) {
6
+ if (attachments.length === 0)
7
+ return archive;
8
+ const files = unpackTarGz(archive);
9
+ const notesBytes = files.get('notes.jsonl');
10
+ const manifestBytes = files.get('manifest.json');
11
+ if (!notesBytes || !manifestBytes)
12
+ throw new Error('full-v1 shard is missing notes.jsonl or manifest.json');
13
+ const notes = new TextDecoder().decode(notesBytes).split('\n').filter(Boolean)
14
+ .map((line) => JSON.parse(line));
15
+ const byRecordId = new Map(notes.map((note) => [note.metadata?.aiwg_source?.record_id, note]));
16
+ for (const entry of attachments) {
17
+ const note = byRecordId.get(entry.recordId);
18
+ if (!note)
19
+ throw new Error(`cannot attach bytes: shard record is missing: ${entry.recordId}`);
20
+ const checksum = computeBlobHash(entry.bytes);
21
+ note.attachments ??= [];
22
+ note.attachments.push({
23
+ extracted_text: null,
24
+ extraction_status: 'deferred',
25
+ reason: 'no_extracted_text',
26
+ attachment: {
27
+ id: entry.attachmentId,
28
+ path: entry.filename,
29
+ mime: entry.mimeType,
30
+ checksum,
31
+ bytes: entry.bytes.length,
32
+ },
33
+ });
34
+ files.set(`blobs/${checksum.slice('blake3:'.length)}`, entry.bytes);
35
+ }
36
+ const updatedNotes = jsonLines(notes);
37
+ files.set('notes.jsonl', updatedNotes);
38
+ const manifest = JSON.parse(new TextDecoder().decode(manifestBytes));
39
+ manifest.checksums['notes.jsonl'] = await sha256Hex(updatedNotes);
40
+ files.set('manifest.json', new TextEncoder().encode(JSON.stringify(manifest, null, 2)));
41
+ const packed = packTarGz(files);
42
+ const validation = await validateFullV1ShardArchive(packed);
43
+ if (!validation.valid)
44
+ throw new Error(`embedded shard failed validation: ${validation.errors.join('; ')}`);
45
+ return packed;
46
+ }
47
+ export function recoverEmbeddedAttachments(archive) {
48
+ const files = unpackTarGz(archive);
49
+ const notesBytes = files.get('notes.jsonl');
50
+ if (!notesBytes)
51
+ return [];
52
+ const notes = new TextDecoder().decode(notesBytes).split('\n').filter(Boolean)
53
+ .map((line) => JSON.parse(line));
54
+ const recovered = [];
55
+ for (const note of notes) {
56
+ const recordId = note.metadata?.aiwg_source?.record_id;
57
+ if (!recordId)
58
+ continue;
59
+ for (const projection of note.attachments ?? []) {
60
+ const ref = projection.attachment;
61
+ const bytes = files.get(`blobs/${ref.checksum.replace(/^blake3:/, '')}`);
62
+ if (!bytes)
63
+ continue;
64
+ if (bytes.length !== ref.bytes || computeBlobHash(bytes) !== ref.checksum) {
65
+ throw new Error(`embedded attachment failed size/hash verification: ${ref.id}`);
66
+ }
67
+ recovered.push({
68
+ recordId,
69
+ attachmentId: ref.id,
70
+ filename: ref.path,
71
+ mimeType: ref.mime,
72
+ bytes,
73
+ });
74
+ }
75
+ }
76
+ return recovered;
77
+ }
78
+ //# sourceMappingURL=fortemi-shard-blobs.js.map
@@ -9,6 +9,7 @@ export * from './optional-backends.js';
9
9
  export * from './knowledge-shard.js';
10
10
  export * from './fortemi-export-mapping.js';
11
11
  export * from './fortemi-shard-recovery.js';
12
+ export * from './fortemi-shard-blobs.js';
12
13
  export * from './output-lineage.js';
13
14
  export * from './export-plan.js';
14
15
  export * from './candidates.js';
@@ -32,6 +33,7 @@ export * from './adapters/hermes.js';
32
33
  export * from './adapters/opencode.js';
33
34
  export * from './adapters/openclaw.js';
34
35
  export * from './adapters/openhuman.js';
36
+ export * from './adapters/grokbot.js';
35
37
  export * from './adapters/pi.js';
36
38
  export * from './adapters/deepseek-harness.js';
37
39
  export * from './adapters/warp.js';
@@ -16,7 +16,7 @@ import { redactSourceLocator } from './discovery.js';
16
16
  import { fingerprintSourceFile } from './readers.js';
17
17
  export const DISCOVERY_MANIFEST_VERSION = '1.0.0';
18
18
  const MANUAL_EXPORT_PROVIDERS = new Set([
19
- 'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman',
19
+ 'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman', 'grokbot',
20
20
  'warp', 'devin-desktop', 'generic',
21
21
  ]);
22
22
  export async function discoverWorkspaceHistories(options) {
@@ -16,6 +16,7 @@ import { AGENT_SKILLS_SIDECAR_SCHEMA, AIWG_SKILL_CONTROL_FIELDS, createAgentSkil
16
16
  import { getImportedAgentSkill } from './importer.js';
17
17
  import { validateAgentSkillContent } from './validator.js';
18
18
  import { resolveHermesHomePath } from '../providers/hermes-home.js';
19
+ import { GROKBOT_SKILLS_DIR_ENV, grokbotMissingRootRemediation, resolveGrokbotSkillsDir, } from '../providers/grokbot-paths.js';
19
20
  export const AGENT_SKILL_MANAGED_MARKER = '.aiwg-managed';
20
21
  export const AGENT_SKILL_DEPLOYMENT_SIDECAR = '.aiwg-agent-skill.json';
21
22
  const MARKER_CONTENT = 'aiwg-agent-skill-v1\n';
@@ -78,6 +79,22 @@ function resolvePolicy(target, options) {
78
79
  appendToDescription = namespace.appendToDescription;
79
80
  reasons.push('applies the Factory description guidance before strict validation');
80
81
  break;
82
+ case 'grokbot': {
83
+ // Fail closed: never invent ~/grokbot-skills or ~/.grokbot.
84
+ const configured = resolveGrokbotSkillsDir(process.env);
85
+ if (!configured) {
86
+ root = '';
87
+ status = 'unsupported';
88
+ supported = false;
89
+ reasons.push(grokbotMissingRootRemediation(process.env));
90
+ warnings.push(`Agent Skills deploy for grokbot requires absolute ${GROKBOT_SKILLS_DIR_ENV}; no filesystem root was invented`);
91
+ }
92
+ else {
93
+ root = configured;
94
+ reasons.push(`uses the operator-configured ${GROKBOT_SKILLS_DIR_ENV} skills root with strict managed ownership markers`);
95
+ }
96
+ break;
97
+ }
81
98
  case 'hermes':
82
99
  if (options.homeDir === undefined) {
83
100
  root = resolveHermesHomePath('skills');
@@ -36,6 +36,7 @@ const PLATFORM_BUILTINS = {
36
36
  'opencode': ['help', 'run'],
37
37
  'warp': ['help', 'settings'],
38
38
  'hermes': [],
39
+ 'grokbot': [],
39
40
  'openclaw': [],
40
41
  'openhuman': [],
41
42
  'factory': [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.9.15",
3
+ "version": "2026.9.17",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,7 +15,7 @@
15
15
  "$defs": {
16
16
  "version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$" }, "digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, "time": { "type": "string", "format": "date-time", "pattern": "Z$" },
17
17
  "digestMap": { "type": "object", "minProperties": 1, "additionalProperties": { "$ref": "#/$defs/digest" } },
18
- "bindings": { "type": "object", "additionalProperties": false, "required": ["aiwgCommit", "packageDigests", "schemaDigests", "fixtureDigest", "configurationDigest"], "properties": { "aiwgCommit": { "type": "string", "pattern": "^(?:[0-9a-f]{40}|sha256:[0-9a-f]{64})$" }, "fortemiCommit": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, "packageDigests": { "$ref": "#/$defs/digestMap" }, "schemaDigests": { "$ref": "#/$defs/digestMap" }, "fixtureDigest": { "$ref": "#/$defs/digest" }, "descriptorDigest": { "$ref": "#/$defs/digest" }, "configurationDigest": { "$ref": "#/$defs/digest" } } },
18
+ "bindings": { "type": "object", "additionalProperties": false, "required": ["aiwgCommit", "packageDigests", "schemaDigests", "fixtureDigest", "configurationDigest"], "properties": { "aiwgCommit": { "type": "string", "pattern": "^(?:[0-9a-f]{40}|sha256:[0-9a-f]{64})$" }, "fortemiCommit": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, "fortemiServerCommit": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, "packageDigests": { "$ref": "#/$defs/digestMap" }, "schemaDigests": { "$ref": "#/$defs/digestMap" }, "fixtureDigest": { "$ref": "#/$defs/digest" }, "descriptorDigest": { "$ref": "#/$defs/digest" }, "configurationDigest": { "$ref": "#/$defs/digest" } } },
19
19
  "evidence": { "type": "object", "additionalProperties": false, "required": ["kind", "reference", "digest"], "properties": { "kind": { "enum": ["fixture", "real-source", "cross-repo", "live-qualification"] }, "reference": { "type": "string", "minLength": 1 }, "digest": { "$ref": "#/$defs/digest" } } },
20
20
  "result": { "type": "object", "additionalProperties": false, "required": ["cellId", "status", "evidence", "observed"], "properties": { "cellId": { "type": "string", "minLength": 1 }, "status": { "enum": ["passed", "failed", "pending"] }, "diagnostic": { "type": "string", "minLength": 1 }, "evidence": { "type": "array", "items": { "$ref": "#/$defs/evidence" } }, "observed": { "type": "object", "additionalProperties": false, "required": ["networkAttempts"], "properties": { "records": { "type": "integer", "minimum": 0 }, "bytes": { "type": "integer", "minimum": 0 }, "durationMs": { "type": "integer", "minimum": 0 }, "networkAttempts": { "type": "integer", "minimum": 0 } } } } }
21
21
  }
@@ -20,7 +20,7 @@
20
20
  * --rules-only Deploy only rules (skip agents)
21
21
  * --dry-run Show what would be deployed without writing
22
22
  * --force Overwrite existing files
23
- * --provider <name> Target provider: antigravity (agy), claude (default), openai, codex, cursor, opencode, copilot, factory, pi, omp, deepseek-harness (dsh), warp, devin, hermes, or openclaw
23
+ * --provider <name> Target provider: antigravity (agy), claude (default), openai, codex, cursor, opencode, copilot, factory, grokbot, grok-build, pi, omp, deepseek-harness (dsh), warp, devin, hermes, openhuman, or openclaw
24
24
  * --model <name> Override model for all tiers (blanket)
25
25
  * --reasoning-model <name> Override model for reasoning tasks
26
26
  * --coding-model <name> Override model for coding tasks
@@ -123,7 +123,7 @@ const PROVIDER_ALIASES = {
123
123
  'dsh': 'deepseek-harness',
124
124
  };
125
125
 
126
- const AVAILABLE_PROVIDERS = ['antigravity', 'claude', 'factory', 'codex', 'opencode', 'copilot', 'cursor', 'pi', 'omp', 'deepseek-harness', 'warp', 'windsurf', 'hermes', 'openclaw', 'openhuman'];
126
+ const AVAILABLE_PROVIDERS = ['antigravity', 'claude', 'factory', 'codex', 'opencode', 'copilot', 'cursor', 'pi', 'omp', 'deepseek-harness', 'warp', 'windsurf', 'hermes', 'openclaw', 'openhuman', 'grokbot', 'grok-build'];
127
127
 
128
128
  const UNSUPPORTED_PROVIDER_HINTS = {
129
129
  'devin-cli': [
@@ -651,6 +651,8 @@ Providers (all deploy agents, commands, skills, and rules):
651
651
  Paths: AGENTS.md, .agents/skills/, .dsh/aiwg.cordis.patch.yml
652
652
  devin - Devin Desktop (preferred; aliases: devin-desktop, windsurf)
653
653
  Paths: .windsurf/agents/, .windsurf/workflows/, .windsurf/skills/, .windsurf/rules/
654
+ grokbot - Grok Bot (AGENTS.md bridge; skills only with AIWG_GROKBOT_SKILLS_DIR)
655
+ grok-build - Grok Build (experimental; .grok paths + $GROK_HOME; no bare grok alias)
654
656
  hermes - Hermes Agent (MCP-based integration)
655
657
  Skills: $HERMES_HOME/skills/ (user-global; defaults to ~/.hermes/skills/) | Agents: AGENTS.md
656
658
  Commands/Rules: served via MCP, not file-deployed