@aiwg/cli 2026.10.0 → 2026.10.3

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 (168) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +270 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +3091 -24
  3. package/bin/aiwg.mjs +86 -21
  4. package/dist/src/a2a/codecs.js +9 -0
  5. package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
  6. package/dist/src/artifacts/repair.js +9 -4
  7. package/dist/src/channel/manager.mjs +59 -10
  8. package/dist/src/cli/child-process-result.js +40 -0
  9. package/dist/src/cli/env.js +38 -16
  10. package/dist/src/cli/flag-parsing.js +236 -0
  11. package/dist/src/cli/handlers/decision.js +297 -2
  12. package/dist/src/cli/handlers/diagnose.js +9 -2
  13. package/dist/src/cli/handlers/feedback.js +14 -5
  14. package/dist/src/cli/handlers/init.js +245 -30
  15. package/dist/src/cli/handlers/mc.js +23 -0
  16. package/dist/src/cli/handlers/models.js +69 -6
  17. package/dist/src/cli/handlers/ralph.js +3 -0
  18. package/dist/src/cli/handlers/refresh.js +16 -0
  19. package/dist/src/cli/handlers/regenerate.js +14 -0
  20. package/dist/src/cli/handlers/runtime-info.js +24 -9
  21. package/dist/src/cli/handlers/script-runner.js +16 -6
  22. package/dist/src/cli/handlers/session.js +396 -62
  23. package/dist/src/cli/handlers/sessions.js +146 -40
  24. package/dist/src/cli/handlers/setup-manifest.js +439 -39
  25. package/dist/src/cli/handlers/setup.js +139 -39
  26. package/dist/src/cli/handlers/subcommands.js +3 -1
  27. package/dist/src/cli/handlers/team.js +18 -8
  28. package/dist/src/cli/handlers/use.js +13 -0
  29. package/dist/src/cli/handlers/utilities.js +106 -23
  30. package/dist/src/cli/prompt-utils.js +72 -0
  31. package/dist/src/cli/provider-resolution.js +11 -0
  32. package/dist/src/config/aiwg-config.js +268 -4
  33. package/dist/src/config/cli.js +146 -19
  34. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  35. package/dist/src/config/project-artifacts-health.mjs +14 -3
  36. package/dist/src/config/user-config.js +36 -10
  37. package/dist/src/decision/adapters/jev.js +80 -28
  38. package/dist/src/decision/ask.js +352 -0
  39. package/dist/src/decision/driver.js +4 -0
  40. package/dist/src/decision/entry.js +38 -6
  41. package/dist/src/decision/heldout/collector.js +10 -1
  42. package/dist/src/decision/index.js +3 -0
  43. package/dist/src/decision/interop.js +60 -8
  44. package/dist/src/decision/jev-credentials.js +121 -0
  45. package/dist/src/decision/ledger.js +839 -0
  46. package/dist/src/decision/validate.js +6 -3
  47. package/dist/src/effects/dispatch-verifier.js +252 -0
  48. package/dist/src/effects/index.js +1 -0
  49. package/dist/src/features/installer.js +2 -2
  50. package/dist/src/flow/graph-metadata.js +5 -0
  51. package/dist/src/gates/providers/loader.js +3 -1
  52. package/dist/src/installation/manager-command.mjs +1 -1
  53. package/dist/src/installation/manager.mjs +17 -2
  54. package/dist/src/lint/runner.js +2 -2
  55. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  56. package/dist/src/mcp/cli.mjs +587 -77
  57. package/dist/src/mcp/config-file.mjs +279 -0
  58. package/dist/src/mcp/config-layers.mjs +348 -0
  59. package/dist/src/mcp/credentials.mjs +484 -0
  60. package/dist/src/mcp/grok-build-config.mjs +16 -26
  61. package/dist/src/mcp/helpers.mjs +106 -2
  62. package/dist/src/mcp/omp-config.mjs +50 -29
  63. package/dist/src/mcp/profiles.js +97 -23
  64. package/dist/src/mcp/profiles.mjs +99 -27
  65. package/dist/src/mcp/registry.js +264 -57
  66. package/dist/src/mcp/registry.mjs +198 -45
  67. package/dist/src/mcp/server.mjs +403 -20
  68. package/dist/src/mcp/toml-string.mjs +9 -0
  69. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  70. package/dist/src/mcp/tool-filters.mjs +357 -0
  71. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  72. package/dist/src/mcp/tools/decision.mjs +58 -0
  73. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  74. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  75. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  76. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  77. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  78. package/dist/src/memory/project-registry.js +2 -2
  79. package/dist/src/mission-protocol/codecs.js +639 -1
  80. package/dist/src/mission-protocol/types.js +110 -0
  81. package/dist/src/mission-protocol/worker-status.js +2 -0
  82. package/dist/src/models/config-loader.js +70 -13
  83. package/dist/src/models/grok-build-models.js +93 -41
  84. package/dist/src/models/model-capabilities.v1.json +3091 -24
  85. package/dist/src/models/model-discovery.js +862 -63
  86. package/dist/src/models/provider-policy.js +625 -11
  87. package/dist/src/models/qualification-digest.mjs +17 -0
  88. package/dist/src/models/resolver.js +259 -0
  89. package/dist/src/models/worker-direct-readiness.js +529 -0
  90. package/dist/src/models/worker-dispatch-offer.js +169 -0
  91. package/dist/src/models/worker-health-readiness.js +131 -0
  92. package/dist/src/models/worker-installation.mjs +879 -0
  93. package/dist/src/models/wrapper-route.js +18 -0
  94. package/dist/src/ops/registry.js +9 -14
  95. package/dist/src/plugins/standalone-packager.js +4 -3
  96. package/dist/src/process/bounded-child-process.mjs +387 -0
  97. package/dist/src/process/cli-command.mjs +75 -0
  98. package/dist/src/providers/capability-matrix.js +8 -0
  99. package/dist/src/providers/capability-matrix.yaml +270 -0
  100. package/dist/src/providers/grok-build-headless.js +43 -21
  101. package/dist/src/providers/provider-definitions.js +9 -1
  102. package/dist/src/providers/provider-definitions.mjs +9 -2
  103. package/dist/src/serve/executor-registry.js +16 -24
  104. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  105. package/dist/src/serve/mission-conductor.js +9 -0
  106. package/dist/src/serve/shared-host-scheduler.js +2 -0
  107. package/dist/src/serve/worker-invocation.js +538 -0
  108. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  109. package/dist/src/sessions/adapter-registry.js +45 -0
  110. package/dist/src/sessions/adapters/generic.js +37 -3
  111. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  112. package/dist/src/sessions/adapters/grokbot.js +423 -24
  113. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  114. package/dist/src/sessions/adapters/muse.js +68 -29
  115. package/dist/src/sessions/adapters/pi.js +1 -0
  116. package/dist/src/sessions/batch-contracts.js +13 -1
  117. package/dist/src/sessions/batch-import.js +23 -42
  118. package/dist/src/sessions/candidates.js +47 -11
  119. package/dist/src/sessions/contracts.js +35 -0
  120. package/dist/src/sessions/coverage-preflight.js +134 -0
  121. package/dist/src/sessions/discovery.js +3 -0
  122. package/dist/src/sessions/grok-build-cli.js +67 -5
  123. package/dist/src/sessions/importer.js +24 -8
  124. package/dist/src/sessions/index.js +2 -0
  125. package/dist/src/sessions/repository.js +54 -13
  126. package/dist/src/sessions/workspace-discovery.js +99 -37
  127. package/dist/src/skills/adapters/clawhub.js +10 -8
  128. package/dist/src/skills/adapters/openclaw.js +10 -7
  129. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  130. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  131. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  132. package/dist/src/smiths/mcpsmith/example.js +1 -1
  133. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  134. package/dist/src/storage/backends/logseq.js +68 -7
  135. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  136. package/dist/src/testing/generators/test-code-generator.js +57 -30
  137. package/dist/src/uhp/client.js +11 -1
  138. package/dist/src/uhp/mission.js +5 -0
  139. package/dist/src/update/checker.mjs +5 -5
  140. package/dist/src/update/notifier.mjs +54 -2
  141. package/dist/src/writing/fidelity.js +18 -1
  142. package/package.json +18 -1
  143. package/schemas/mission-v1.schema.json +2429 -0
  144. package/schemas/models/model-catalog.v1.schema.json +301 -0
  145. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  146. package/schemas/models/model-policy.v1.schema.json +14 -0
  147. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  148. package/tools/_resolve-impl.mjs +39 -0
  149. package/tools/agents/deploy-agents.mjs +15 -2
  150. package/tools/agents/providers/base.mjs +63 -43
  151. package/tools/agents/providers/grokbot.mjs +11 -12
  152. package/tools/agents/providers/hermes.mjs +6 -3
  153. package/tools/agents/providers/omp.mjs +5 -2
  154. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  155. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  156. package/tools/providers/omp-transport.mjs +192 -0
  157. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  158. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  159. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  160. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  161. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  162. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  163. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  164. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  165. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  166. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  167. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  168. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -1,7 +1,7 @@
1
1
  import { execFile as execFileCallback } from 'node:child_process';
2
2
  import { constants as fsConstants } from 'node:fs';
3
- import { lstat, open, realpath, writeFile } from 'node:fs/promises';
4
- import { dirname, isAbsolute, join, parse, resolve } from 'node:path';
3
+ import { access, lstat, open, realpath, stat, writeFile } from 'node:fs/promises';
4
+ import { delimiter, dirname, isAbsolute, join, parse, resolve } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { resolveGrokHomeResult } from '../providers/grok-build-paths.js';
7
7
  import { SessionContractError } from './contracts.js';
@@ -115,10 +115,10 @@ async function runGrok(options, args) {
115
115
  if (!home.ok)
116
116
  throw new SessionContractError('SOURCE_NOT_AUTHORIZED', home.message);
117
117
  await rejectSymlinkComponents(home.path);
118
- const binary = options.binary ?? 'grok';
119
- if (binary !== 'grok' && !isAbsolute(binary)) {
118
+ if (options.binary !== undefined && !isAbsolute(options.binary)) {
120
119
  throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'Grok Build binary override must be absolute');
121
120
  }
121
+ const binary = options.binary ?? await defaultGrokBinary(env, home.path);
122
122
  const runner = options.runner ?? execFile;
123
123
  try {
124
124
  const result = await runner(binary, ['--no-auto-update', ...args], {
@@ -136,7 +136,69 @@ async function runGrok(options, args) {
136
136
  if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
137
137
  throw new SessionContractError('RESOURCE_LIMIT_EXCEEDED', 'Grok Build CLI output exceeds 2 MB');
138
138
  }
139
- throw new SessionContractError('UNSUPPORTED_OPERATION', 'Grok Build CLI failed or timed out; check the local `grok` installation, authentication, and selected workspace.');
139
+ throw grokFailure(error, binary);
140
+ }
141
+ }
142
+ /**
143
+ * Map a failed `grok` launch to an actionable error (#2968). stderr is never
144
+ * echoed: Grok Build diagnostics can carry session content.
145
+ */
146
+ function grokFailure(error, binary) {
147
+ const name = binary === 'grok' ? '`grok`' : `\`${binary}\``;
148
+ if (error.code === 'ENOENT') {
149
+ return new SessionContractError('UNSUPPORTED_OPERATION', `Grok Build CLI not found: ${name} is not on PATH and no $GROK_HOME/bin/grok was found; `
150
+ + 'pass --grok-bin <absolute path> or add the Grok Build bin directory to PATH.');
151
+ }
152
+ if (error.code === 'EACCES') {
153
+ return new SessionContractError('UNSUPPORTED_OPERATION', `Grok Build CLI ${name} is not executable; check its permissions or pass --grok-bin <absolute path>.`);
154
+ }
155
+ if (error.killed || error.signal) {
156
+ return new SessionContractError('UNSUPPORTED_OPERATION', `Grok Build CLI ${name} timed out after ${TIMEOUT_MS / 1000}s; check that it is authenticated `
157
+ + 'and does not prompt for input.');
158
+ }
159
+ if (typeof error.code === 'number') {
160
+ return new SessionContractError('UNSUPPORTED_OPERATION', `Grok Build CLI ${name} exited with code ${error.code}; run \`grok sessions list\` in the selected `
161
+ + 'workspace to check authentication and installation.');
162
+ }
163
+ return new SessionContractError('UNSUPPORTED_OPERATION', 'Grok Build CLI failed; check the local `grok` installation, authentication, and selected workspace.');
164
+ }
165
+ /**
166
+ * Resolve `grok` from PATH, else the installer's `$GROK_HOME/bin/grok` (#2968).
167
+ * The installer links `bin/grok` to a versioned binary beside it, so that one
168
+ * link is followed only when it resolves to a regular file inside `bin`.
169
+ */
170
+ async function defaultGrokBinary(env, grokHome) {
171
+ for (const directory of (env.PATH ?? '').split(delimiter)) {
172
+ if (!directory || !isAbsolute(directory))
173
+ continue;
174
+ if (await isExecutableFile(join(directory, 'grok')))
175
+ return 'grok';
176
+ }
177
+ const binDirectory = join(grokHome, 'bin');
178
+ try {
179
+ await rejectSymlinkComponents(binDirectory);
180
+ const canonicalBin = await realpath(binDirectory);
181
+ const target = await realpath(join(binDirectory, 'grok'));
182
+ if (dirname(target) !== canonicalBin || !(await isExecutableFile(target)))
183
+ return 'grok';
184
+ return target;
185
+ }
186
+ catch (error) {
187
+ if (error instanceof SessionContractError)
188
+ throw error;
189
+ return 'grok';
190
+ }
191
+ }
192
+ async function isExecutableFile(path) {
193
+ try {
194
+ const item = await stat(path);
195
+ if (!item.isFile())
196
+ return false;
197
+ await access(path, fsConstants.X_OK);
198
+ return true;
199
+ }
200
+ catch {
201
+ return false;
140
202
  }
141
203
  }
142
204
  function parseSessionIds(stdout, mode) {
@@ -65,6 +65,7 @@ export class IncrementalSessionImporter {
65
65
  policyVersion: request.policyVersion,
66
66
  continuity: continuity
67
67
  ? (!previous || previous.positionKind === 'byte-offset'
68
+ || previous.positionKind === 'provider-native'
68
69
  ? continuity.outcome
69
70
  : 'unverified')
70
71
  : (previous ? 'unverified' : 'new-generation'),
@@ -152,6 +153,14 @@ export class IncrementalSessionImporter {
152
153
  checkpoint.cursor = record.sourceCursor;
153
154
  checkpoint.bytesRead = Number(record.sourceCursor.slice(5));
154
155
  }
156
+ else if (record.sourceCursor?.startsWith('native:')) {
157
+ // An opaque provider-native resume token (e.g. a monotonic native
158
+ // sequence) — never a byte offset. Stored verbatim; `bytesRead`
159
+ // still accumulates so resource limits remain meaningful.
160
+ checkpoint.positionKind = 'provider-native';
161
+ checkpoint.cursor = record.sourceCursor;
162
+ checkpoint.bytesRead += bytes;
163
+ }
155
164
  else {
156
165
  checkpoint.bytesRead += bytes;
157
166
  checkpoint.cursor = String(checkpoint.recordsRead);
@@ -435,9 +444,15 @@ async function sourceContinuity(request, previous) {
435
444
  selectedPath: request.selectedSource.locator,
436
445
  allowedRoots: request.selectedSource.authorizedScope.allowedRoots,
437
446
  };
447
+ // Snapshot sources are rewritten wholesale on every save (no stable
448
+ // append-only byte region, and the file identity may legitimately change
449
+ // via rename-swap): skip the shrink, file-identity, and prefix-rewrite
450
+ // throws below. Idempotency instead rests on the adapter's own monotonic
451
+ // native cursor plus the catalog's identity-keyed inserts.
452
+ const isSnapshot = request.adapter.sourceContinuity === 'snapshot';
438
453
  const skipPrefixBytes = await request.adapter.mutablePrefixBytes?.(request.selectedSource) ?? 0;
439
454
  const metadata = await fingerprintSourcePrefix(authorization, 0);
440
- if (previous?.sourceSize !== undefined) {
455
+ if (!isSnapshot && previous?.sourceSize !== undefined) {
441
456
  if (metadata.size < previous.sourceSize) {
442
457
  throw new SessionContractError('SCHEMA_DRIFT', 'session source was truncated before its durable checkpoint');
443
458
  }
@@ -451,17 +466,18 @@ async function sourceContinuity(request, previous) {
451
466
  }
452
467
  }
453
468
  const current = await fingerprintSourcePrefix(authorization, metadata.size, skipPrefixBytes);
454
- const generation = previous?.sourceGeneration ?? sha256([
455
- sourceGenerationDigest(request),
456
- metadata.fileIdentity,
457
- ].join('\0'));
469
+ const generation = previous?.sourceGeneration ?? (isSnapshot
470
+ ? sourceGenerationDigest(request)
471
+ : sha256([sourceGenerationDigest(request), metadata.fileIdentity].join('\0')));
458
472
  return {
459
473
  sourceGeneration: generation,
460
474
  outcome: !previous
461
475
  ? 'new-generation'
462
- : metadata.size === previous.sourceSize
463
- && current.digest === previous.prefixDigest
464
- ? 'unchanged-replay' : 'validated-append',
476
+ : isSnapshot
477
+ ? (current.digest === previous.prefixDigest ? 'unchanged-replay' : 'validated-append')
478
+ : metadata.size === previous.sourceSize
479
+ && current.digest === previous.prefixDigest
480
+ ? 'unchanged-replay' : 'validated-append',
465
481
  size: metadata.size,
466
482
  mtimeMs: metadata.mtimeMs,
467
483
  fileIdentity: metadata.fileIdentity,
@@ -42,4 +42,6 @@ export * from './adapters/warp.js';
42
42
  export * from './adapters/windsurf.js';
43
43
  export * from './adapters/muse.js';
44
44
  export * from './adapters/omp.js';
45
+ export * from './adapter-registry.js';
46
+ export * from './coverage-preflight.js';
45
47
  //# sourceMappingURL=index.js.map
@@ -5,6 +5,11 @@ import { redactSessionText, sanitizeNativeExtensions } from './policy.js';
5
5
  import { deriveSessionAnalytics, } from './analytics.js';
6
6
  const POLICY_PROVIDER_MIGRATION = 'policy-provider-identity:v2';
7
7
  const EVENT_ORIGIN_INTENT_MIGRATION = 'event-origin-intent:v1';
8
+ const VISIBLE_SESSION_PREDICATE = `s.lifecycle != 'tombstoned' AND EXISTS (
9
+ SELECT 1 FROM session_events e
10
+ JOIN import_runs r ON r.import_run_id=e.import_run_id
11
+ WHERE e.session_id=s.session_id AND r.status='committed'
12
+ )`;
8
13
  export class SessionRepository {
9
14
  db;
10
15
  constructor(path = ':memory:') {
@@ -620,16 +625,56 @@ export class SessionRepository {
620
625
  }
621
626
  getSession(id, workspaceId) {
622
627
  const row = this.db.prepare(`SELECT s.data FROM sessions s
623
- WHERE s.session_id=? AND s.lifecycle != ?
624
- ${workspaceId ? 'AND s.workspace_id=?' : ''}
625
- AND EXISTS (
626
- SELECT 1 FROM session_events e
627
- JOIN import_runs r ON r.import_run_id=e.import_run_id
628
- WHERE e.session_id=s.session_id AND r.status='committed'
629
- )`)
630
- .get(...(workspaceId ? [id, 'tombstoned', workspaceId] : [id, 'tombstoned']));
628
+ WHERE s.session_id=? AND ${VISIBLE_SESSION_PREDICATE}
629
+ ${workspaceId ? 'AND s.workspace_id=?' : ''}`)
630
+ .get(...(workspaceId ? [id, workspaceId] : [id]));
631
631
  return row ? JSON.parse(String(row.data)) : null;
632
632
  }
633
+ findSessionsByReference(reference, workspaceId) {
634
+ const internal = this.getSession(reference, workspaceId);
635
+ if (internal)
636
+ return [internal];
637
+ const query = (predicate) => this.db.prepare(`SELECT s.data FROM sessions s
638
+ WHERE ${workspaceId ? 's.workspace_id=? AND' : ''} ${VISIBLE_SESSION_PREDICATE} AND (${predicate})
639
+ ORDER BY s.session_id`).all(...(workspaceId ? [workspaceId, reference] : [reference]))
640
+ .map((row) => JSON.parse(String(row.data)));
641
+ const exact = query('s.native_session_id=?');
642
+ if (exact.length === 1)
643
+ return exact;
644
+ if (exact.length > 1) {
645
+ // Artifact IDs are redacted by native-extension sanitization. The
646
+ // adapter's boolean subagent flag survives. Only prefer a parent when
647
+ // every other match is a proven Claude subagent in the same workspace.
648
+ if (exact.every((session) => session.provider === 'claude'
649
+ && session.workspaceId === exact[0].workspaceId)) {
650
+ const classified = exact.map((session) => {
651
+ const flags = this.db.prepare(`SELECT
652
+ MAX(json_extract(e.data, '$.extensions."native.claude".transcriptFamily.subagent')=0) AS parent,
653
+ MAX(json_extract(e.data, '$.extensions."native.claude".transcriptFamily.subagent')=1) AS subagent
654
+ FROM session_events e JOIN import_runs r ON r.import_run_id=e.import_run_id
655
+ WHERE e.session_id=? AND r.status='committed'`).get(session.sessionId);
656
+ return { session, parent: flags.parent === 1, subagent: flags.subagent === 1 };
657
+ });
658
+ const parents = classified.filter((item) => item.parent && !item.subagent);
659
+ if (parents.length === 1 && classified.every((item) => item === parents[0]
660
+ || (item.subagent && !item.parent)))
661
+ return [parents[0].session];
662
+ }
663
+ return exact;
664
+ }
665
+ // substr treats %, _ and other characters literally rather than as LIKE patterns.
666
+ return this.db.prepare(`SELECT s.data FROM sessions s
667
+ WHERE ${workspaceId ? 's.workspace_id=? AND' : ''} ${VISIBLE_SESSION_PREDICATE}
668
+ AND (substr(s.native_session_id, 1, length(?))=?
669
+ OR substr(s.session_id, 1, length(?))=?)
670
+ ORDER BY s.session_id`).all(...(workspaceId ? [workspaceId, reference, reference, reference, reference]
671
+ : [reference, reference, reference, reference]))
672
+ .map((row) => JSON.parse(String(row.data)));
673
+ }
674
+ listWorkspaceSessionCounts() {
675
+ return this.db.prepare(`SELECT s.workspace_id, COUNT(*) AS count FROM sessions s
676
+ WHERE ${VISIBLE_SESSION_PREDICATE} GROUP BY s.workspace_id ORDER BY s.workspace_id`).all().map((row) => ({ workspaceId: String(row.workspace_id), count: Number(row.count) }));
677
+ }
633
678
  listSources() {
634
679
  return this.db.prepare('SELECT data FROM session_sources ORDER BY provider, source_id').all().map((row) => JSON.parse(String(row.data)));
635
680
  }
@@ -670,11 +715,7 @@ export class SessionRepository {
670
715
  : coverage;
671
716
  }
672
717
  listSessions(options) {
673
- const where = [`s.lifecycle != 'tombstoned'`, `EXISTS (
674
- SELECT 1 FROM session_events e
675
- JOIN import_runs r ON r.import_run_id=e.import_run_id
676
- WHERE e.session_id=s.session_id AND r.status='committed'
677
- )`];
718
+ const where = [VISIBLE_SESSION_PREDICATE];
678
719
  const scopeDigest = sessionListScopeDigest(options);
679
720
  const cursor = decodeSessionListCursor(options.cursor);
680
721
  if (cursor && cursor.scopeDigest !== scopeDigest) {
@@ -3,46 +3,53 @@ import { access, mkdir, readFile, realpath, rename, stat, writeFile, } from 'nod
3
3
  import { homedir, userInfo } from 'node:os';
4
4
  import { basename, dirname, join, resolve } from 'node:path';
5
5
  import { createInterface } from 'node:readline';
6
- import { ClaudeSessionAdapter } from './adapters/claude.js';
7
- import { CodexSessionAdapter } from './adapters/codex.js';
8
- import { CursorSessionAdapter } from './adapters/cursor.js';
9
- import { FactorySessionAdapter } from './adapters/factory.js';
10
- import { MuseSessionAdapter, museNativeSourceMatchesWorkspace } from './adapters/muse.js';
11
- import { OmpSessionAdapter, readOmpSessionHeader } from './adapters/omp.js';
6
+ import { inspectMuseNativeWorkspace } from './adapters/muse.js';
7
+ import { readOmpSessionHeader } from './adapters/omp.js';
8
+ import { grokbotConversationIdFromLocator } from './adapters/grokbot-locator.js';
12
9
  import { resolveOmpPaths } from '../providers/omp-paths.mjs';
13
- import { PiSessionAdapter } from './adapters/pi.js';
14
- import { DeepSeekHarnessSessionAdapter } from './adapters/deepseek-harness.js';
15
- import { SESSION_PROVIDER_IDS, sha256, } from './contracts.js';
10
+ import { SESSION_PROVIDER_IDS, SessionCoveragePreflightSchema, SessionContractError, sha256, } from './contracts.js';
16
11
  import { redactSourceLocator } from './discovery.js';
12
+ import { createSessionAdapterRegistry } from './adapter-registry.js';
13
+ import { coveragePreflight, expectedSessionProviders } from './coverage-preflight.js';
17
14
  import { fingerprintSourceFile } from './readers.js';
18
15
  export const DISCOVERY_MANIFEST_VERSION = '1.0.0';
19
16
  const MANUAL_EXPORT_PROVIDERS = new Set([
20
- 'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman', 'grokbot', 'grok-build',
17
+ 'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman', 'grok-build',
21
18
  'warp', 'devin-desktop', 'generic',
22
19
  ]);
23
20
  export async function discoverWorkspaceHistories(options) {
21
+ const registry = options.registry ?? createSessionAdapterRegistry();
22
+ const expectedProviders = expectedSessionProviders(options.expectedProviders ?? SESSION_PROVIDER_IDS);
23
+ const supportedProviders = new Set(registry.list().map((adapter) => adapter.provider));
24
24
  const workspacePath = await canonicalPath(options.workspace);
25
25
  const workspaceId = workspacePath;
26
26
  const providerHomes = providerHomeCandidates(options.providerHome, options.operatorHome);
27
27
  const keyWithLeadingDash = workspaceKey(workspacePath, true);
28
28
  const keyWithoutLeadingDash = workspaceKey(workspacePath, false);
29
29
  const discoverable = [
30
- { provider: 'deepseek-harness', adapter: new DeepSeekHarnessSessionAdapter(), roots: options.dshRoot
30
+ { provider: 'deepseek-harness', roots: options.dshRoot
31
31
  ? [resolve(options.dshRoot)] : options.providerHome
32
32
  ? [join(resolve(options.providerHome), '.dsh', 'sessions')] : [] },
33
- { provider: 'omp', adapter: new OmpSessionAdapter(), roots: options.ompRoot
33
+ { provider: 'omp', roots: options.ompRoot
34
34
  ? [resolve(options.ompRoot)] : options.providerHome
35
35
  ? [resolveOmpPaths({ home: resolve(options.providerHome), cwd: workspacePath }).sessionsDir] : [] },
36
- { provider: 'muse', adapter: new MuseSessionAdapter(), roots: options.museRoot
36
+ { provider: 'muse', roots: options.museRoot
37
37
  ? [resolve(options.museRoot)] : [] },
38
+ // Grok Bot conversations carry no workspace/cwd field, so unlike
39
+ // claude/cursor/factory there is no per-workspace default path to derive
40
+ // from providerHome. Native discovery requires an explicit
41
+ // --grokbot-root (same shared-root authorization model as
42
+ // codex/omp/deepseek-harness/muse below), and every authorized
43
+ // conversation surfaces to whichever workspace runs discovery (same
44
+ // no-filtering behavior already used for `pi`/`deepseek-harness`).
45
+ { provider: 'grokbot', roots: options.grokbotRoot
46
+ ? [resolve(options.grokbotRoot)] : [] },
38
47
  {
39
48
  provider: 'claude',
40
- adapter: new ClaudeSessionAdapter(),
41
49
  roots: providerHomes.map((providerHome) => join(providerHome, '.claude', 'projects', keyWithLeadingDash)),
42
50
  },
43
51
  {
44
52
  provider: 'codex',
45
- adapter: new CodexSessionAdapter(),
46
53
  roots: options.codexRoot
47
54
  ? [resolve(options.codexRoot)]
48
55
  : options.providerHome
@@ -51,31 +58,39 @@ export async function discoverWorkspaceHistories(options) {
51
58
  },
52
59
  {
53
60
  provider: 'cursor',
54
- adapter: new CursorSessionAdapter(),
55
61
  roots: providerHomes.map((providerHome) => join(providerHome, '.cursor', 'projects', keyWithoutLeadingDash, 'agent-transcripts')),
56
62
  },
57
63
  {
58
64
  provider: 'factory',
59
- adapter: new FactorySessionAdapter(),
60
65
  roots: providerHomes.flatMap((providerHome) => [
61
66
  join(providerHome, '.factory', 'projects', keyWithLeadingDash),
62
67
  join(providerHome, '.factory', 'sessions', keyWithLeadingDash),
63
68
  ]),
64
69
  },
65
70
  {
71
+ // Pi stores every working directory's sessions in its own
72
+ // subdirectory of the sessions root, named `--<path>--` where
73
+ // `<path>` has its leading separator removed and `/`, `\`, and `:`
74
+ // replaced with `-` (https://pi.dev/docs/latest/session-format,
75
+ // "File Location", verified 2026-10-12) — the same transform
76
+ // `workspaceKey(path, false)` already applies for Cursor below.
77
+ // Scanning the whole sessions root (as before) would import every
78
+ // other workspace's history into this one's catalog (#2656).
66
79
  provider: 'pi',
67
- adapter: new PiSessionAdapter(),
68
- roots: options.providerHome
69
- ? [process.env.PI_CODING_AGENT_SESSION_DIR
70
- ? resolve(process.env.PI_CODING_AGENT_SESSION_DIR)
71
- : join(resolve(options.providerHome), '.pi', 'agent', 'sessions')]
72
- : [],
80
+ roots: (process.env.PI_CODING_AGENT_SESSION_DIR
81
+ ? [resolve(process.env.PI_CODING_AGENT_SESSION_DIR)]
82
+ : providerHomes.map((providerHome) => join(providerHome, '.pi', 'agent', 'sessions'))).map((sessionsRoot) => join(sessionsRoot, `--${keyWithoutLeadingDash}--`)),
73
83
  },
74
84
  ];
75
85
  const reports = new Map();
76
86
  const candidates = [];
77
87
  const seen = new Set();
78
88
  for (const entry of discoverable) {
89
+ if (!expectedProviders.includes(entry.provider) || !supportedProviders.has(entry.provider))
90
+ continue;
91
+ const adapter = registry.get(entry.provider);
92
+ if (adapter.disposition === 'unsupported' || !adapter.supportedOperations.includes('discover'))
93
+ continue;
79
94
  const availableRoots = [];
80
95
  for (const root of entry.roots) {
81
96
  if (await pathExists(root))
@@ -83,12 +98,13 @@ export async function discoverWorkspaceHistories(options) {
83
98
  }
84
99
  if (availableRoots.length === 0) {
85
100
  const codexNeedsAuthorization = (entry.provider === 'codex' || entry.provider === 'omp'
86
- || entry.provider === 'deepseek-harness' || entry.provider === 'muse')
101
+ || entry.provider === 'deepseek-harness' || entry.provider === 'muse'
102
+ || entry.provider === 'grokbot')
87
103
  && entry.roots.length === 0;
88
104
  reports.set(entry.provider, providerReport(entry.provider, codexNeedsAuthorization ? 'export-required' : 'unavailable', codexNeedsAuthorization ? 'manual-export' : 'discoverable', [], codexNeedsAuthorization
89
105
  ? 'SHARED_ROOT_AUTHORIZATION_REQUIRED'
90
106
  : 'PROVIDER_ROOT_UNAVAILABLE', codexNeedsAuthorization
91
- ? `Pass --${entry.provider}-root with an explicitly authorized sessions root.`
107
+ ? `Pass --${entry.provider === 'deepseek-harness' ? 'dsh' : entry.provider}-root with an explicitly authorized sessions root.`
92
108
  : `No authorized ${entry.provider} workspace history root was found.`));
93
109
  continue;
94
110
  }
@@ -97,17 +113,25 @@ export async function discoverWorkspaceHistories(options) {
97
113
  allowedRoots: availableRoots,
98
114
  };
99
115
  const providerSources = [];
100
- for await (const descriptor of entry.adapter.discover(scope)) {
101
- const locator = await canonicalPath(descriptor.locator);
102
- if (entry.provider === 'codex'
103
- && !await codexSourceMatchesWorkspace(locator, workspacePath))
104
- continue;
105
- if (entry.provider === 'muse'
106
- && !await museNativeSourceMatchesWorkspace({
116
+ const museSourceInspections = [];
117
+ for await (const descriptor of adapter.discover(scope)) {
118
+ if (entry.provider === 'muse') {
119
+ const inspection = await inspectMuseNativeWorkspace({
107
120
  ...descriptor,
108
121
  sourceId: 'discovery',
109
122
  authorizedScope: scope,
110
- }, workspacePath))
123
+ }, workspacePath);
124
+ museSourceInspections.push({
125
+ sourceKey: sha256(['muse-workspace-inspection-v1', descriptor.locator].join('\0')),
126
+ redactedLocator: redactSourceLocator(descriptor.locator),
127
+ ...inspection,
128
+ });
129
+ if (inspection.outcome !== 'matched')
130
+ continue;
131
+ }
132
+ const locator = await canonicalPath(descriptor.locator);
133
+ if (entry.provider === 'codex'
134
+ && !await codexSourceMatchesWorkspace(locator, workspacePath))
111
135
  continue;
112
136
  const ompHeader = entry.provider === 'omp' ? await readOmpSessionHeader({ ...descriptor, sourceId: 'discovery', authorizedScope: scope }) : undefined;
113
137
  if (ompHeader && resolve(ompHeader.cwd) !== workspacePath)
@@ -127,17 +151,40 @@ export async function discoverWorkspaceHistories(options) {
127
151
  const source = sourceFromDescriptor(descriptor, locator, authorizedRoot, details, fingerprint.digest);
128
152
  if (ompHeader)
129
153
  source.sourceId = sha256(['omp-native-source-v1', workspaceId, ompHeader.id].join('\0'));
154
+ if (entry.provider === 'grokbot') {
155
+ // The replica is rewritten wholesale as its sliding window evicts
156
+ // old entries, so a content-digest sourceId (the default above)
157
+ // would churn every re-discovery and break checkpoint continuity.
158
+ // Key it on the conversation id instead, same rationale as the OMP
159
+ // override above.
160
+ const conversationId = grokbotConversationIdFromLocator(locator);
161
+ if (conversationId) {
162
+ source.sourceId = sha256(['grokbot-native-source-v1', workspaceId, conversationId].join('\0'));
163
+ }
164
+ }
130
165
  providerSources.push(source);
131
166
  candidates.push(source);
132
167
  }
133
- reports.set(entry.provider, providerReport(entry.provider, 'checked', 'discoverable', providerSources, providerSources.length === 0 ? 'NO_WORKSPACE_SOURCES' : null, providerSources.length === 0
134
- ? `The ${entry.provider} root was checked, but no source matched the authorized workspace.`
135
- : null));
168
+ const incompleteMuseInspection = museSourceInspections.some((inspection) => inspection.outcome === 'inconclusive');
169
+ const report = providerReport(entry.provider, incompleteMuseInspection ? 'not-checked' : 'checked', 'discoverable', providerSources, incompleteMuseInspection ? 'MUSE_SOURCE_INSPECTION_INCOMPLETE'
170
+ : providerSources.length === 0 ? 'NO_WORKSPACE_SOURCES' : null, incompleteMuseInspection
171
+ ? 'Review museSourceInspections; authorize a supported Muse export for unresolved sources before claiming workspace coverage.'
172
+ : providerSources.length === 0
173
+ ? `The ${entry.provider} root was checked, but no source matched the authorized workspace.`
174
+ : null);
175
+ if (entry.provider === 'muse') {
176
+ report.museSourceInspections = museSourceInspections.sort((a, b) => a.sourceKey.localeCompare(b.sourceKey));
177
+ }
178
+ reports.set(entry.provider, report);
136
179
  }
137
- for (const provider of SESSION_PROVIDER_IDS) {
180
+ for (const provider of expectedProviders) {
138
181
  if (reports.has(provider))
139
182
  continue;
140
183
  const manual = MANUAL_EXPORT_PROVIDERS.has(provider);
184
+ if (!supportedProviders.has(provider)) {
185
+ reports.set(provider, providerReport(provider, 'not-checked', 'unsupported', [], 'MISSING_ADAPTER', 'Deliberately select a compatible runtime or an authorized supported export.'));
186
+ continue;
187
+ }
141
188
  const grokBuild = provider === 'grok-build';
142
189
  reports.set(provider, providerReport(provider, manual ? 'export-required' : 'not-checked', manual ? 'manual-export' : 'unsupported', [], grokBuild ? 'CLI_EXPORT_REQUIRED'
143
190
  : manual ? 'EXPLICIT_EXPORT_REQUIRED' : 'PROVIDER_NOT_CHECKED', grokBuild
@@ -148,9 +195,14 @@ export async function discoverWorkspaceHistories(options) {
148
195
  }
149
196
  const sources = candidates.sort(compareSources);
150
197
  const providers = [...reports.values()].sort((a, b) => a.provider.localeCompare(b.provider));
198
+ const preflight = coveragePreflight({
199
+ expectedProviders, expectedProviderBasis: options.expectedProviders ? 'explicit' : 'declared-default',
200
+ registry, runtime: options.runtime, reports: providers,
201
+ });
151
202
  const identity = {
152
203
  schemaVersion: DISCOVERY_MANIFEST_VERSION,
153
204
  workspaceId,
205
+ preflight,
154
206
  sources: sources.map((source) => ({
155
207
  sourceId: source.sourceId,
156
208
  provider: source.provider,
@@ -164,6 +216,7 @@ export async function discoverWorkspaceHistories(options) {
164
216
  provider: provider.provider,
165
217
  status: provider.status,
166
218
  reasonCode: provider.reasonCode,
219
+ ...(provider.museSourceInspections ? { museSourceInspections: provider.museSourceInspections } : {}),
167
220
  })),
168
221
  };
169
222
  return {
@@ -172,6 +225,7 @@ export async function discoverWorkspaceHistories(options) {
172
225
  createdAt: options.createdAt ?? new Date().toISOString(),
173
226
  workspaceId,
174
227
  workspacePath,
228
+ preflight,
175
229
  providers,
176
230
  sources,
177
231
  };
@@ -194,6 +248,11 @@ export async function readDiscoveryManifest(path) {
194
248
  || !Array.isArray(value.sources)) {
195
249
  throw new Error('session discovery manifest is malformed or unsupported');
196
250
  }
251
+ if (value.preflight) {
252
+ const parsed = SessionCoveragePreflightSchema.safeParse(value.preflight);
253
+ if (!parsed.success)
254
+ throw new SessionContractError('INVALID_ARGUMENT', 'invalid discovery coverage preflight');
255
+ }
197
256
  const expected = await rediscoverManifestIdentity(value);
198
257
  if (expected !== value.manifestId) {
199
258
  throw new Error('session discovery manifest identity does not match its contents');
@@ -207,6 +266,7 @@ export function publicDiscoveryManifest(manifest) {
207
266
  createdAt: manifest.createdAt,
208
267
  workspaceId: manifest.workspaceId,
209
268
  workspacePath: manifest.workspacePath,
269
+ preflight: manifest.preflight,
210
270
  providers: manifest.providers,
211
271
  sources: manifest.sources.map(({ locator: _locator, authorizedRoot: _root, ...source }) => source),
212
272
  totals: {
@@ -250,6 +310,7 @@ async function rediscoverManifestIdentity(manifest) {
250
310
  const identity = {
251
311
  schemaVersion: manifest.schemaVersion,
252
312
  workspaceId: manifest.workspaceId,
313
+ ...(manifest.preflight ? { preflight: manifest.preflight } : {}),
253
314
  sources: manifest.sources.map((source) => ({
254
315
  sourceId: source.sourceId,
255
316
  provider: source.provider,
@@ -263,6 +324,7 @@ async function rediscoverManifestIdentity(manifest) {
263
324
  provider: provider.provider,
264
325
  status: provider.status,
265
326
  reasonCode: provider.reasonCode,
327
+ ...(provider.museSourceInspections ? { museSourceInspections: provider.museSourceInspections } : {}),
266
328
  })),
267
329
  };
268
330
  return sha256(JSON.stringify(identity));
@@ -11,7 +11,8 @@
11
11
  * @see https://clawhub.ai
12
12
  * @see https://docs.openclaw.ai/tools/clawhub
13
13
  */
14
- import { execSync } from 'child_process';
14
+ import { boundedExecFileSync } from '../../process/bounded-child-process.mjs';
15
+ import { cliCommand } from '../../process/cli-command.mjs';
15
16
  import fs from 'fs';
16
17
  import path from 'path';
17
18
  import { getProviderDefinition, resolveProviderPathValue, } from '../../providers/provider-definitions.js';
@@ -47,7 +48,8 @@ async function clawHubFetch(endpoint) {
47
48
  */
48
49
  function runClawHub(args) {
49
50
  try {
50
- return execSync(`clawhub ${args}`, {
51
+ const [file, argv] = cliCommand('clawhub', args);
52
+ return boundedExecFileSync(file, argv, 'quick', {
51
53
  encoding: 'utf-8',
52
54
  timeout: 30000,
53
55
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -65,7 +67,7 @@ export class ClawHubAdapter {
65
67
  if (this.available !== null)
66
68
  return this.available;
67
69
  // Check CLI
68
- const cliAvailable = runClawHub('--version 2>/dev/null') !== null;
70
+ const cliAvailable = runClawHub(['--version']) !== null;
69
71
  if (cliAvailable) {
70
72
  this.available = true;
71
73
  return true;
@@ -89,7 +91,7 @@ export class ClawHubAdapter {
89
91
  }));
90
92
  }
91
93
  // Try CLI
92
- const cliOutput = runClawHub(`search "${query}" --json 2>/dev/null`);
94
+ const cliOutput = runClawHub(['search', query, '--json']);
93
95
  if (cliOutput) {
94
96
  try {
95
97
  const parsed = JSON.parse(cliOutput);
@@ -127,7 +129,7 @@ export class ClawHubAdapter {
127
129
  };
128
130
  }
129
131
  // Try CLI
130
- const cliOutput = runClawHub(`info "${name}" --json 2>/dev/null`);
132
+ const cliOutput = runClawHub(['info', name, '--json']);
131
133
  if (cliOutput) {
132
134
  try {
133
135
  const parsed = JSON.parse(cliOutput);
@@ -162,7 +164,7 @@ export class ClawHubAdapter {
162
164
  }));
163
165
  }
164
166
  // Try CLI
165
- const cliOutput = runClawHub('list --json 2>/dev/null');
167
+ const cliOutput = runClawHub(['list', '--json']);
166
168
  if (cliOutput) {
167
169
  try {
168
170
  const parsed = JSON.parse(cliOutput);
@@ -195,7 +197,7 @@ export class ClawHubAdapter {
195
197
  targetDir = resolveSkillFallbackPath(target, options.projectDir, name);
196
198
  }
197
199
  // Try CLI install (downloads to a temp location, then we copy)
198
- const cliResult = runClawHub(`install "${name}" --output "${targetDir}" 2>/dev/null`);
200
+ const cliResult = runClawHub(['install', name, '--output', targetDir]);
199
201
  if (cliResult !== null)
200
202
  return;
201
203
  // Try API download
@@ -240,7 +242,7 @@ export class ClawHubAdapter {
240
242
  }
241
243
  }
242
244
  // Try CLI publish
243
- const cliResult = runClawHub(`publish "${packageDir}" 2>/dev/null`);
245
+ const cliResult = runClawHub(['publish', packageDir]);
244
246
  if (cliResult !== null)
245
247
  return;
246
248
  throw new Error('ClawHub publish requires the clawhub CLI. ' +