@aiwg/cli 2026.10.10 → 2026.10.11

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 (34) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +78 -14
  2. package/dist/src/cli/handlers/sessions.js +198 -15
  3. package/dist/src/cli/handlers/use.js +12 -4
  4. package/dist/src/config/worker-context.js +337 -0
  5. package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
  6. package/dist/src/models/model-capabilities.v1.json +78 -14
  7. package/dist/src/models/provider-policy.js +7 -0
  8. package/dist/src/policy/authorization.js +29 -1
  9. package/dist/src/process/aiwg-probe-command.d.mts +15 -0
  10. package/dist/src/process/aiwg-probe-command.mjs +37 -0
  11. package/dist/src/sessions/adapters/grokbot.js +7 -0
  12. package/dist/src/sessions/batch-import.js +34 -6
  13. package/dist/src/sessions/contracts.js +19 -0
  14. package/dist/src/sessions/coverage-preflight.js +7 -1
  15. package/dist/src/sessions/fortemi-export-mapping.js +33 -3
  16. package/dist/src/sessions/host-catalog.js +157 -0
  17. package/dist/src/sessions/image-blobs.js +141 -0
  18. package/dist/src/sessions/importer.js +23 -3
  19. package/dist/src/sessions/index.js +2 -0
  20. package/dist/src/sessions/refresh.js +25 -2
  21. package/dist/src/sessions/repository.js +164 -22
  22. package/dist/src/sessions/workspace-discovery.js +5 -2
  23. package/package.json +7 -1
  24. package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
  25. package/tools/ralph-external/lib/worker-context.mjs +237 -0
  26. package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
  27. package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
  28. package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
  29. package/tools/ralph-external/lib/worker-isolation.mjs +17 -29
  30. package/tools/ralph-external/lib/worker-launch-cli.mjs +3 -1
  31. package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
  32. package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
  33. package/tools/ralph-external/lib/worker-termination.mjs +82 -0
  34. package/tools/ralph-external/session-launcher.mjs +51 -96
@@ -4,6 +4,9 @@
4
4
  "verifiedAt": "2026-07-20",
5
5
  "providers": {
6
6
  "antigravity": {
7
+ "sessionEnvAllowlist": [
8
+ "GEMINI_API_KEY"
9
+ ],
7
10
  "agent": "native",
8
11
  "skill": "native",
9
12
  "globalChild": "inherited",
@@ -19,7 +22,7 @@
19
22
  "invalidPinFallback": "Provider validation/error; never assume fallback",
20
23
  "configTarget": "agy launch arguments; custom-agent model tiers are not projected from generic AIWG model ids",
21
24
  "artifactFormat": "YAML frontmatter plus Markdown; runtime --model and --effort flags",
22
- "verification": "Pinned offline contract (--help/--version only); authenticated model selection was not executed",
25
+ "verification": "Pinned offline contract (--help/--version only); authenticated model selection was not executed. Session credential evidence: $AIWG_ROOT/docs/cli/sensitive-values.md#provider-recipient-exceptions (#3624).",
23
26
  "sourceUrl": "https://antigravity.google/docs/cli/subagents/",
24
27
  "verifiedAt": "2026-09-04",
25
28
  "workerHealthAdapter": {
@@ -154,7 +157,7 @@
154
157
  "Native MCP/client, MSP/ACP, gateway and conversation capabilities do not establish worker authority.",
155
158
  "--dangerously-skip-permissions and --remote-control are never part of the direct profile; neither was ever shipped as a new argv-prompt path.",
156
159
  "A soft-denied tool permission request (exit 0, stderr notice naming the allow-rule needed) is NOT the same as success; its exact stderr wording is not evidenced from help or changelog, so this is an open, honestly-marked detection gap rather than an invented check (tools/ralph-external/lib/antigravity-adapter.mjs ANTIGRAVITY_SOFT_DENY_RISK_NOTE).",
157
- "GEMINI_API_KEY in envAllowlist is a strings-sourced GUESS, not confirmed to authenticate this CLI. Live fix-round-1 probes (D/E, no login present) show a bare run stops at \"Error: authentication required. Run 'agy' to log in, then retry.\" — this is an interactive Google-account login, not a documented env-key or OpenRouter-style flag. The manual smoke run is BLOCKED until an operator completes an interactive `agy` login on the target host.",
160
+ "GEMINI_API_KEY is documented at https://antigravity.google/docs/install/ (reviewed 2026-10-26): settings.json must also set modelProvider to gemini; the key alone has no effect. Earlier 1.3.3 probes only exercised missing account login. This documentation confirms the credential name, not authenticated execution or qualification of this version-bound worker profile.",
158
161
  "The 1.3.3 --effort enum (low|medium|high|xhigh|max) supersedes the stale 1.1.26-pinned three-value set in tools/providers/antigravity-transport.mjs (a separate, unedited legacy transport used by the MCP/doctor bridge, not this direct-shell profile).",
159
162
  "Flag validation happens BEFORE auth: live probe F, `agy --print=x --input-format bogus --output-format stream-json`, returned \"Error: unknown --input-format \\\"bogus\\\" (want text or stream-json)\" without reaching the auth stop, confirming --input-format is validated against exactly {text, stream-json} independent of login state.",
160
163
  "The brief reaches agy through adapter-frame: the installed buildWorkerStdinFrame hook supplies exactly one NDJSON user message and EOF. The stdin control stays unqualified pending one authenticated smoke run."
@@ -865,6 +868,9 @@
865
868
  }
866
869
  },
867
870
  "cursor": {
871
+ "sessionEnvAllowlist": [
872
+ "CURSOR_API_KEY"
873
+ ],
868
874
  "agent": "native",
869
875
  "skill": "unsupported",
870
876
  "globalChild": "inherited",
@@ -873,7 +879,7 @@
873
879
  "invalidPinFallback": "Plan/admin fallback may apply; record observed result",
874
880
  "configTarget": ".cursor/agents/*.md",
875
881
  "artifactFormat": "YAML frontmatter plus Markdown",
876
- "verification": "Schema fixture and entitled Cursor invocation",
882
+ "verification": "Schema fixture and entitled Cursor invocation. Session credential evidence: $AIWG_ROOT/docs/cli/sensitive-values.md#provider-recipient-exceptions (#3624).",
877
883
  "sourceUrl": "https://cursor.com/docs/subagents",
878
884
  "verifiedAt": "2026-07-20",
879
885
  "workerHealthAdapter": {
@@ -1465,6 +1471,9 @@
1465
1471
  }
1466
1472
  },
1467
1473
  "warp": {
1474
+ "sessionEnvAllowlist": [
1475
+ "WARP_API_KEY"
1476
+ ],
1468
1477
  "agent": "global-only",
1469
1478
  "skill": "unsupported",
1470
1479
  "globalChild": "native",
@@ -1473,7 +1482,7 @@
1473
1482
  "invalidPinFallback": "Profile availability dependent",
1474
1483
  "configTarget": "Warp agent profile or run configuration",
1475
1484
  "artifactFormat": "Saved profile / run control plane",
1476
- "verification": "Inspect active profile and run metadata",
1485
+ "verification": "Inspect active profile and run metadata. Session credential evidence: $AIWG_ROOT/docs/cli/sensitive-values.md#provider-recipient-exceptions (#3624).",
1477
1486
  "sourceUrl": "https://docs.warp.dev/agent-platform/capabilities/agent-profiles-permissions",
1478
1487
  "verifiedAt": "2026-07-20",
1479
1488
  "workerHealthAdapter": {
@@ -2284,6 +2293,7 @@
2284
2293
  }
2285
2294
  },
2286
2295
  "windsurf": {
2296
+ "sessionEnvAllowlist": [],
2287
2297
  "agent": "unsupported",
2288
2298
  "skill": "unsupported",
2289
2299
  "globalChild": "inherited",
@@ -2292,7 +2302,7 @@
2292
2302
  "invalidPinFallback": "No portable custom-child selector",
2293
2303
  "configTarget": "No supported portable artifact field",
2294
2304
  "artifactFormat": "Informational policy only",
2295
- "verification": "Assert unsupported fields are omitted",
2305
+ "verification": "Assert unsupported fields are omitted. Session environment is explicitly empty: desktop/cloud login owns authentication; no CLI credential variable is evidenced. Coverage-only: no worker execution or qualified worker adapter. See $AIWG_ROOT/docs/cli/sensitive-values.md#provider-recipient-exceptions (#3624).",
2296
2306
  "sourceUrl": "https://docs.devin.ai/desktop/cascade/skills",
2297
2307
  "verifiedAt": "2026-07-20",
2298
2308
  "workerHealthCoverage": {
@@ -2312,6 +2322,22 @@
2312
2322
  }
2313
2323
  },
2314
2324
  "openclaw": {
2325
+ "sessionEnvAllowlist": [
2326
+ "OPENAI_API_KEY",
2327
+ "ANTHROPIC_API_KEY",
2328
+ "ANTHROPIC_OAUTH_TOKEN",
2329
+ "GEMINI_API_KEY",
2330
+ "GOOGLE_API_KEY",
2331
+ "OPENROUTER_API_KEY",
2332
+ "MISTRAL_API_KEY",
2333
+ "XAI_API_KEY",
2334
+ "OPENCLAW_HOME",
2335
+ "OPENCLAW_STATE_DIR",
2336
+ "OPENCLAW_CONFIG_PATH",
2337
+ "OPENCLAW_GATEWAY_URL",
2338
+ "OPENCLAW_GATEWAY_TOKEN",
2339
+ "OPENCLAW_GATEWAY_PASSWORD"
2340
+ ],
2315
2341
  "agent": "native",
2316
2342
  "skill": "unsupported",
2317
2343
  "globalChild": "native",
@@ -2320,7 +2346,7 @@
2320
2346
  "invalidPinFallback": "Provider/account fallback must be observed",
2321
2347
  "configTarget": "OpenClaw subagent configuration",
2322
2348
  "artifactFormat": "JSON/JSONC runtime configuration",
2323
- "verification": "Inspect resolved subagent metadata",
2349
+ "verification": "Inspect resolved subagent metadata. Session credential evidence: $AIWG_ROOT/docs/cli/sensitive-values.md#provider-recipient-exceptions (#3624).",
2324
2350
  "sourceUrl": "https://docs.openclaw.ai/tools/subagents",
2325
2351
  "verifiedAt": "2026-07-20",
2326
2352
  "workerHealthAdapter": {
@@ -2508,6 +2534,7 @@
2508
2534
  }
2509
2535
  },
2510
2536
  "grokbot": {
2537
+ "sessionEnvAllowlist": [],
2511
2538
  "agent": "unsupported",
2512
2539
  "skill": "unsupported",
2513
2540
  "globalChild": "informational",
@@ -2516,7 +2543,7 @@
2516
2543
  "invalidPinFallback": "Report unsupported; do not fabricate xAI/Grok model pins as native",
2517
2544
  "configTarget": "Grok Bot has no verified AIWG-owned model config path",
2518
2545
  "artifactFormat": "Informational only",
2519
- "verification": "Confirm steward models --route --provider grokbot emits unsupported/informational",
2546
+ "verification": "Confirm steward models --route --provider grokbot emits unsupported/informational. Session environment is explicitly empty: desktop/cloud login owns authentication; no CLI credential variable is evidenced. Coverage-only: no worker execution or qualified worker adapter. See $AIWG_ROOT/docs/cli/sensitive-values.md#provider-recipient-exceptions (#3624).",
2520
2547
  "sourceUrl": "https://github.com/jmagly/aiwg/blob/main/docs/architecture/adr-grokbot-provider-target.md",
2521
2548
  "verifiedAt": "2026-09-15",
2522
2549
  "workerHealthCoverage": {
@@ -2536,6 +2563,10 @@
2536
2563
  }
2537
2564
  },
2538
2565
  "grok-build": {
2566
+ "sessionEnvAllowlist": [
2567
+ "XAI_API_KEY",
2568
+ "GROK_HOME"
2569
+ ],
2539
2570
  "agent": "inherited",
2540
2571
  "skill": "inherited",
2541
2572
  "globalChild": "native",
@@ -2544,7 +2575,7 @@
2544
2575
  "invalidPinFallback": "Report the unavailable alias; use the sole discovered model only when exactly one is available",
2545
2576
  "configTarget": "$GROK_HOME/config.toml [models] and [model.<alias>] (read-only discovery)",
2546
2577
  "artifactFormat": "Grok Build model alias",
2547
- "verification": "grok inspect --json plus bounded parsing of model fields from reported config layers",
2578
+ "verification": "grok inspect --json plus bounded parsing of model fields from reported config layers. Session credential evidence: $AIWG_ROOT/docs/cli/sensitive-values.md#provider-recipient-exceptions (#3624).",
2548
2579
  "sourceUrl": "https://docs.x.ai/build/settings",
2549
2580
  "verifiedAt": "2026-09-21",
2550
2581
  "workerHealthAdapter": {
@@ -2736,6 +2767,25 @@
2736
2767
  }
2737
2768
  },
2738
2769
  "hermes": {
2770
+ "sessionEnvAllowlist": [
2771
+ "OPENAI_API_KEY",
2772
+ "ANTHROPIC_API_KEY",
2773
+ "ANTHROPIC_TOKEN",
2774
+ "CLAUDE_CODE_OAUTH_TOKEN",
2775
+ "GEMINI_API_KEY",
2776
+ "GOOGLE_API_KEY",
2777
+ "OPENROUTER_API_KEY",
2778
+ "GROQ_API_KEY",
2779
+ "MISTRAL_API_KEY",
2780
+ "XAI_API_KEY",
2781
+ "DEEPSEEK_API_KEY",
2782
+ "OPENAI_BASE_URL",
2783
+ "ANTHROPIC_BASE_URL",
2784
+ "OPENROUTER_BASE_URL",
2785
+ "CUSTOM_BASE_URL",
2786
+ "HERMES_HOME",
2787
+ "HERMES_INFERENCE_PROVIDER"
2788
+ ],
2739
2789
  "agent": "global-only",
2740
2790
  "skill": "unsupported",
2741
2791
  "globalChild": "native",
@@ -2744,7 +2794,7 @@
2744
2794
  "invalidPinFallback": "Provider resolution is global; report actual model",
2745
2795
  "configTarget": "Hermes delegation configuration",
2746
2796
  "artifactFormat": "Global CLI/config setting",
2747
- "verification": "Inspect Hermes delegation configuration",
2797
+ "verification": "Inspect Hermes delegation configuration. Session credential evidence: $AIWG_ROOT/docs/cli/sensitive-values.md#provider-recipient-exceptions (#3624).",
2748
2798
  "sourceUrl": "https://hermes-agent.nousresearch.com/docs/user-guide/features/delegation",
2749
2799
  "verifiedAt": "2026-07-20",
2750
2800
  "workerHealthAdapter": {
@@ -2927,6 +2977,19 @@
2927
2977
  }
2928
2978
  },
2929
2979
  "openhuman": {
2980
+ "sessionEnvAllowlist": [
2981
+ "OPENHUMAN_CORE_TOKEN",
2982
+ "OPENHUMAN_CORE_RPC_URL",
2983
+ "OPENHUMAN_CORE_HOST",
2984
+ "OPENHUMAN_CORE_PORT",
2985
+ "OPENHUMAN_WORKSPACE",
2986
+ "OPENHUMAN_DOTENV_PATH",
2987
+ "OPENHUMAN_KEYRING_MASTER_KEY",
2988
+ "OPENHUMAN_KEYRING_MASTER_KEY_FILE",
2989
+ "OPENHUMAN_APP_ENV",
2990
+ "BACKEND_URL",
2991
+ "OPENHUMAN_MODEL"
2992
+ ],
2930
2993
  "agent": "compiled",
2931
2994
  "skill": "unsupported",
2932
2995
  "globalChild": "inherited",
@@ -2935,7 +2998,7 @@
2935
2998
  "invalidPinFallback": "Exact pins validate; semantic hints route dynamically",
2936
2999
  "configTarget": "OpenHuman agent TOML",
2937
3000
  "artifactFormat": "Standalone TOML agent definition",
2938
- "verification": "Parse against OpenHuman definition schema",
3001
+ "verification": "Parse against OpenHuman definition schema. Session credential evidence: $AIWG_ROOT/docs/cli/sensitive-values.md#provider-recipient-exceptions (#3624).",
2939
3002
  "sourceUrl": "https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/agent/harness/definition.rs",
2940
3003
  "verifiedAt": "2026-07-20",
2941
3004
  "workerHealthAdapter": {
@@ -3154,6 +3217,7 @@
3154
3217
  "${effort}",
3155
3218
  "--workspace",
3156
3219
  "${workspace}",
3220
+ "--trust-workspace",
3157
3221
  "--approval-mode",
3158
3222
  "never",
3159
3223
  "--approval-judge",
@@ -3246,8 +3310,8 @@
3246
3310
  },
3247
3311
  "trust": {
3248
3312
  "support": "unqualified",
3249
- "mapping": "[]",
3250
- "evidence": "Installed muse exec --help (Muse Code 1.4.12, 2026-10-23); bounded renderer checked on operator qualification; no backend attestation."
3313
+ "mapping": "[\"--trust-workspace\"]",
3314
+ "evidence": "Installed muse exec --help (Muse Code 1.4.12, 2026-10-23); bounded renderer checked on operator qualification; no backend attestation. Tier 0 workspaces are conductor-prepared worker trees the conductor owns, so the profile binds --trust-workspace by default (#3685: untrusted Muse loads project_sources=0 with user skills only and empty hook admission; trusted loads project_sources=1, project skills and hooks)."
3251
3315
  },
3252
3316
  "children": {
3253
3317
  "support": "unqualified",
@@ -3268,13 +3332,13 @@
3268
3332
  "Live qualification is an open S1 blocker; installed help and synthetic/echo mechanics are not paid inference qualification.",
3269
3333
  "Configured-model events provide runtime-configured identity only, never backend attestation.",
3270
3334
  "Contributor data policy is explicit; shared policy denies contributor models and never silently selects the CLI default.",
3271
- "Workspace trust defaults off; only a host-qualified trust mapping may load rules/skills. Approval-mode never and approval-judge off retain the sandbox. Muse requires a qualified external boundary.",
3335
+ "Workspace trust is bound on for conductor-prepared worker trees (the conductor owns the worktree), so project rules/skills/hooks load; only a host-qualified trust mapping may change it. Approval-mode never and approval-judge off retain the sandbox. Muse requires a qualified external boundary.",
3272
3336
  "Host qualification must bind an absolute output-schema mapping and include that schema in runtimeFiles.",
3273
3337
  "Muse JSONL strict normalization needs the shared strict-parser hook; the MCP bridge must consume the selected provider parser. MSP is not MCP.",
3274
3338
  "Semantic step/tool completion grammar and native session-log layout remain unavailable; unrelated logs, deltas and CPU cannot establish progress.",
3275
3339
  "Shared version gate: installation requires the raw trimmed banner Muse Code 1.4.4 (1.4.4-R5419.1), but semver selection requires 1.4.4-R5419.1. No normalization exists; keep unqualified until the shared owner binds raw and normalized versions.",
3276
3340
  "Operator qualification approves the installed direct renderer, not authenticated inference or backend model identity.",
3277
- "Workspace trust defaults off. Only never approval is supported. Explicit unsupported controls block dispatch.",
3341
+ "Workspace trust is on for conductor-prepared worker trees. Only never approval is supported. Explicit unsupported controls block dispatch.",
3278
3342
  "Self-delegation must be explicitly allowed by policy; no native child-denial flag is assumed."
3279
3343
  ],
3280
3344
  "health": {
@@ -424,6 +424,13 @@ export function assertOperatorWorkerNativeArgv(adapter, argv, selected, permissi
424
424
  throw new Error('Muse direct worker approval requires never');
425
425
  requirePair('--approval-mode', 'never');
426
426
  requirePair('--approval-judge', 'off');
427
+ // #3685: Tier 0 Muse workers run in conductor-prepared worker trees, so
428
+ // project context must load. An argv without exactly one
429
+ // --trust-workspace would run with project_sources=0 (no project
430
+ // rules/skills/hooks) and is refused rather than silently degraded.
431
+ if (args.filter(token => token === '--trust-workspace').length !== 1) {
432
+ throw new Error('Requested native worker control is absent or ambiguous: --trust-workspace');
433
+ }
427
434
  if (args.includes('--disable-sandbox') !== (sandbox === 'external')) {
428
435
  throw new Error('Requested native worker sandbox control does not match');
429
436
  }
@@ -170,13 +170,41 @@ const LEGACY_MANIFESTS = [
170
170
  '.aiwg/ops/security/repo-access.manifest.yaml',
171
171
  '.aiwg/security/repo-access.manifest.yaml',
172
172
  ];
173
+ /**
174
+ * Factory delivery defaults written by `aiwg init`/`aiwg use`
175
+ * (emptyConfig in src/config/aiwg-config.ts). Kept as a literal here —
176
+ * rather than importing the config module — because aiwg-config already
177
+ * imports this module for authorization validation, so a value import
178
+ * would be a cycle. A unit test pins this literal against
179
+ * emptyConfig().delivery (#3635).
180
+ */
181
+ const DEFAULT_DELIVERY_POLICY = {
182
+ mode: 'pr-required',
183
+ default_branch: 'main',
184
+ require_ci_green: true,
185
+ auto_close_issues: true,
186
+ issue_comment_on_cycle: true,
187
+ force_push_policy: 'never',
188
+ };
189
+ /** True when the delivery block is exactly what the tool writes for a fresh project. */
190
+ export function isDefaultDeliveryPolicy(delivery) {
191
+ if (!delivery || typeof delivery !== 'object')
192
+ return false;
193
+ const actual = delivery;
194
+ const actualKeys = Object.keys(actual);
195
+ return actualKeys.length === Object.keys(DEFAULT_DELIVERY_POLICY).length
196
+ && actualKeys.every((key) => actual[key] === DEFAULT_DELIVERY_POLICY[key]);
197
+ }
173
198
  export async function auditLegacyPermissions(projectDir, config) {
174
199
  const out = validateAuthorization(config?.authorization);
175
200
  if (!config?.authorization && config?.repos?.some(repo => repo.allowed?.length))
176
201
  out.push({ severity: 'warning', code: 'legacy-repos-allowed', source: '.aiwg/aiwg.config', message: 'repos[].allowed should be normalized into authorization roles and assignments.' });
177
202
  if (!config?.authorization && Object.keys(config?.repo_maintainer?.tiers ?? {}).length)
178
203
  out.push({ severity: 'warning', code: 'legacy-maintainer-tiers', source: '.aiwg/aiwg.config', message: 'repo_maintainer.tiers should be represented by normalized role assignments.' });
179
- if (!config?.authorization && config?.delivery)
204
+ // Tool-written delivery defaults are not operator legacy input: a fresh
205
+ // `aiwg use` must not produce something that needs migration (#3635).
206
+ // Operator-edited delivery (any key added, removed, or changed) still flags.
207
+ if (!config?.authorization && config?.delivery && !isDefaultDeliveryPolicy(config.delivery))
180
208
  out.push({ severity: 'warning', code: 'legacy-delivery-policy', source: '.aiwg/aiwg.config', message: 'delivery and signing operations should be normalized into scoped role assignments.' });
181
209
  for (const path of LEGACY_MANIFESTS) {
182
210
  try {
@@ -0,0 +1,15 @@
1
+ export interface AiwgProbeCommand {
2
+ file: string;
3
+ prefix: string[];
4
+ via: 'aiwg-bin' | 'entrypoint' | 'package' | 'path';
5
+ }
6
+
7
+ export function resolveAiwgProbeCommand(options?: {
8
+ env?: NodeJS.ProcessEnv;
9
+ argv1?: string | undefined;
10
+ execPath?: string;
11
+ cwd?: string;
12
+ packageRoot?: string | null;
13
+ }): AiwgProbeCommand;
14
+
15
+ export function probeNotOnPathDetail(): string;
@@ -0,0 +1,37 @@
1
+ // Resolves the aiwg invocation for doctor's discovery self-probes so they
2
+ // run the current entrypoint instead of failing when aiwg is not on PATH.
3
+ import { existsSync } from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ export function resolveAiwgProbeCommand({
7
+ env = process.env,
8
+ argv1 = process.argv[1],
9
+ execPath = process.execPath,
10
+ cwd = process.cwd(),
11
+ packageRoot = null,
12
+ } = {}) {
13
+ const override = env?.AIWG_BIN;
14
+ if (typeof override === 'string' && override.length > 0) {
15
+ return { file: override, prefix: [], via: 'aiwg-bin' };
16
+ }
17
+ const entry = typeof argv1 === 'string' && argv1.length > 0
18
+ ? (path.isAbsolute(argv1) ? argv1 : path.resolve(cwd, argv1))
19
+ : null;
20
+ // Only the aiwg launcher itself is a usable entrypoint; doctor also runs as
21
+ // `node tools/cli/doctor.mjs`, where argv[1] is doctor, not aiwg.
22
+ const isLauncher = entry && ['aiwg', 'aiwg.mjs'].includes(path.basename(entry));
23
+ if (entry && isLauncher && existsSync(entry)) {
24
+ return { file: execPath, prefix: [entry], via: 'entrypoint' };
25
+ }
26
+ if (typeof packageRoot === 'string' && packageRoot.length > 0) {
27
+ const bin = path.join(packageRoot, 'bin', 'aiwg.mjs');
28
+ if (existsSync(bin)) {
29
+ return { file: execPath, prefix: [bin], via: 'package' };
30
+ }
31
+ }
32
+ return { file: 'aiwg', prefix: [], via: 'path' };
33
+ }
34
+
35
+ export function probeNotOnPathDetail() {
36
+ return 'aiwg not on PATH (spawn ENOENT) — add the npm global bin to PATH or invoke doctor via the install entrypoint';
37
+ }
@@ -152,6 +152,13 @@ export class GrokbotSessionAdapter {
152
152
  * rename-swap), not appended to. See the class-level doc comment.
153
153
  */
154
154
  sourceContinuity = 'snapshot';
155
+ /**
156
+ * Account scope (#3232): conversations carry no workspace identity, so
157
+ * every workspace authorizing the same `--grokbot-root` would otherwise
158
+ * import the same conversations. The importer routes account-scoped
159
+ * sources to the host catalog once; workspace catalogs keep a reference.
160
+ */
161
+ scopeClass = 'account';
155
162
  interchange;
156
163
  constructor(limits, maxFiles = 10_000) {
157
164
  this.limits = limits;
@@ -6,6 +6,7 @@ import { coveragePreflight, sessionRuntimeIdentity } from './coverage-preflight.
6
6
  import { SESSION_CONTRACT_VERSION, SessionContractError, schemaIssueDetail, SessionSourceSchema, sha256, stableSessionId, stableEventId, } from './contracts.js';
7
7
  import { BATCH_IMPORT_VERSION, coverageFromBatchRun, } from './batch-contracts.js';
8
8
  import { IncrementalSessionImporter, SessionImportFailure } from './importer.js';
9
+ import { HOST_CATALOG_WORKSPACE_ID, isAccountScopedAdapter, publishHostRoutedBatch, } from './host-catalog.js';
9
10
  import { matchImportExclusion, resolveImportExclusions } from './import-exclusions.js';
10
11
  import { resolveImportLimits } from './import-limits.js';
11
12
  import { fingerprintSourceFile } from './readers.js';
@@ -45,6 +46,7 @@ export async function importDiscoveryManifest(options) {
45
46
  run.completedAt = null;
46
47
  options.repository.saveBatchImportRun(run);
47
48
  injectFault(options, run, 'run-saved');
49
+ const hostRoutedSourceIds = [];
48
50
  for (const manifestSource of options.manifest.sources) {
49
51
  const disposition = run.sources.find((candidate) => candidate.sourceId === manifestSource.sourceId);
50
52
  if (!disposition) {
@@ -76,23 +78,33 @@ export async function importDiscoveryManifest(options) {
76
78
  persistProgress(options.repository, run, now());
77
79
  try {
78
80
  await assertManifestSourceUnchanged(manifestSource);
81
+ const adapter = adapterFor(manifestSource.provider, limits);
82
+ // Account-scoped providers (Grok Bot) are imported once into the
83
+ // host catalog; the workspace catalog records a reference (#3232).
84
+ // Any adapter declaring account scope follows this route.
85
+ const hostRouted = options.hostRepository !== undefined
86
+ && options.hostCatalogPath !== undefined
87
+ && isAccountScopedAdapter(adapter);
88
+ const targetRepository = hostRouted ? options.hostRepository : options.repository;
89
+ const importWorkspaceId = hostRouted ? HOST_CATALOG_WORKSPACE_ID : options.manifest.workspaceId;
90
+ if (hostRouted)
91
+ hostRoutedSourceIds.push(manifestSource.sourceId);
79
92
  const selectedSource = {
80
93
  provider: manifestSource.provider,
81
94
  locator: manifestSource.locator,
82
95
  locatorClass: manifestSource.locatorClass,
83
96
  sourceId: manifestSource.sourceId,
84
97
  authorizedScope: {
85
- workspaceId: options.manifest.workspaceId,
98
+ workspaceId: importWorkspaceId,
86
99
  allowedRoots: [manifestSource.authorizedRoot],
87
100
  },
88
101
  };
89
- const adapter = adapterFor(manifestSource.provider, limits);
90
102
  const source = await sessionSource(selectedSource, adapter);
91
- const receipts = await new IncrementalSessionImporter(options.repository).import({
103
+ const receipts = await new IncrementalSessionImporter(targetRepository).import({
92
104
  source,
93
105
  selectedSource,
94
106
  adapter,
95
- workspaceId: options.manifest.workspaceId,
107
+ workspaceId: importWorkspaceId,
96
108
  policyVersion: '1.0.0',
97
109
  batchRunId: run.runId,
98
110
  publish: false,
@@ -101,7 +113,7 @@ export async function importDiscoveryManifest(options) {
101
113
  limits,
102
114
  });
103
115
  await assertManifestSourceUnchanged(manifestSource);
104
- const totals = options.repository.sourceImportTotals(manifestSource.sourceId, run.runId);
116
+ const totals = targetRepository.sourceImportTotals(manifestSource.sourceId, run.runId);
105
117
  disposition.sessionsAccepted = totals.sessions;
106
118
  disposition.eventsAccepted = totals.events;
107
119
  disposition.status = receipts.length > 0
@@ -134,6 +146,18 @@ export async function importDiscoveryManifest(options) {
134
146
  .map((source) => source.sourceId);
135
147
  injectFault(options, run, 'before-publication');
136
148
  options.repository.commitStagedBatch(run.runId, publishable);
149
+ if (options.hostRepository && options.hostCatalogPath && hostRoutedSourceIds.length > 0) {
150
+ const acceptedHost = run.sources
151
+ .filter((source) => hostRoutedSourceIds.includes(source.sourceId) && isAccepted(source.status))
152
+ .map((source) => source.sourceId);
153
+ publishHostRoutedBatch({
154
+ hostRepository: options.hostRepository,
155
+ workspaceRepository: options.repository,
156
+ batchRunId: run.runId,
157
+ sourceIds: acceptedHost,
158
+ hostCatalogPath: options.hostCatalogPath,
159
+ });
160
+ }
137
161
  injectFault(options, run, 'after-publication');
138
162
  run.status = pending ? 'interrupted' : rejected || acquisitionIncomplete ? 'partial' : 'complete';
139
163
  run.updatedAt = now().toISOString();
@@ -360,8 +384,12 @@ async function rebuildFromCatalog(options) {
360
384
  let staged;
361
385
  try {
362
386
  staged = new SessionRepository(join(stagingDirectory, 'staging.sqlite'));
387
+ // The staged rebuild transfers normalized rows (not host references) into
388
+ // the destination, so account-scoped sources rebuild locally here as
389
+ // before; the destination's own host routing applies on its next import.
363
390
  const receipt = await importDiscoveryManifest({ ...options, manifest, repository: staged,
364
- carryForward: undefined, fault: undefined });
391
+ carryForward: undefined, fault: undefined,
392
+ hostRepository: undefined, hostCatalogPath: undefined });
365
393
  const identities = new Set();
366
394
  const destinationCodex = new Map(catalogSessions(options.repository, workspaceId)
367
395
  .filter(session => session.provider === 'codex').map(session => [session.nativeSessionId, session.sourceId]));
@@ -103,6 +103,19 @@ export const SessionSourceSchema = z.object({
103
103
  authorizedAt: dateTime({ offset: true }),
104
104
  extensions: NativeExtensionsSchema.prefault({}),
105
105
  });
106
+ /**
107
+ * Content-addressed reference to a tool-output image whose bytes live in
108
+ * the catalog blob store instead of inline in the event (#3265). The
109
+ * persisted shape's schema is this zod contract (session events have no
110
+ * JSON-schema file; see `lint:schemas`).
111
+ */
112
+ export const SessionImageRefSchema = z.object({
113
+ digest: DigestSchema,
114
+ mediaType: z.string().min(1),
115
+ byteLength: integer().nonnegative(),
116
+ locatorClass: z.string().min(1).optional(),
117
+ offset: integer().nonnegative().optional(),
118
+ });
106
119
  export const SessionEventSchema = z.object({
107
120
  contractVersion: z.literal(SESSION_CONTRACT_VERSION),
108
121
  eventId: z.string().min(1),
@@ -130,6 +143,12 @@ export const SessionEventSchema = z.object({
130
143
  originRule: z.string().min(1).default('legacy:unknown'),
131
144
  originClassifierVersion: VersionSchema.default('1.0.0'),
132
145
  searchableText: z.string(),
146
+ /**
147
+ * Image references extracted from inline payloads at import. Optional so
148
+ * pre-#3265 catalog rows (inline bytes, field absent) still parse; readers
149
+ * must tolerate `undefined` as "no references".
150
+ */
151
+ imageRefs: z.array(SessionImageRefSchema).optional(),
133
152
  digest: DigestSchema,
134
153
  rawReference: z.object({
135
154
  locatorClass: z.string().min(1),
@@ -139,7 +139,13 @@ export function reconcileCoveragePreflight(preflight, sources) {
139
139
  return entry;
140
140
  const selected = sources.filter((source) => source.provider === entry.provider);
141
141
  const imported = selected.length === entry.sourceCount && selected.length > 0
142
- && selected.every((source) => source.status === 'excluded' || (accepted.has(source.status) && source.eventsAccepted > 0));
142
+ // duplicate/previously-committed sources were acquired by an earlier
143
+ // run, so zero new events still counts as imported. A host-routed
144
+ // second workspace import lands here: the host copy already exists
145
+ // and the workspace only records references (#3232). A freshly
146
+ // committed source with zero events stays incomplete.
147
+ && selected.every((source) => source.status === 'excluded' || (accepted.has(source.status)
148
+ && (source.eventsAccepted > 0 || source.status !== 'committed')));
143
149
  return {
144
150
  ...entry,
145
151
  disposition: !imported ? 'import-incomplete'
@@ -75,7 +75,16 @@ export function sessionToAiwgFortemiRecord(session, catalogTags = []) {
75
75
  updated_at: updatedAt,
76
76
  };
77
77
  }
78
- export function sessionEventToAiwgFortemiRecord(session, event) {
78
+ /**
79
+ * Map one catalog event. `images` carries the event's image references
80
+ * resolved against the blob store when the caller can provide them
81
+ * (export build passes `repository.resolveEventImages`); omitted means
82
+ * "not resolved", never "no images" -- the placeholder text in `text` is
83
+ * always present regardless (#3265). Bytes are never embedded here, only
84
+ * the reference plus its availability, so a consumer knows whether the
85
+ * digest is fetchable from the catalog or gone.
86
+ */
87
+ export function sessionEventToAiwgFortemiRecord(session, event, images = []) {
79
88
  const locator = sessionEventRecordLocator(session, event);
80
89
  const parentLocator = sessionRecordLocator(session);
81
90
  const updatedAt = event.occurredAt ?? new Date(0).toISOString();
@@ -110,6 +119,16 @@ export function sessionEventToAiwgFortemiRecord(session, event) {
110
119
  nativeId: event.nativeId,
111
120
  sequence: event.sequence,
112
121
  toolCallId: event.toolCallId,
122
+ ...(images.length > 0
123
+ ? {
124
+ images: images.map((image) => ({
125
+ digest: image.digest,
126
+ mediaType: image.mediaType,
127
+ byteLength: image.byteLength,
128
+ status: image.status,
129
+ })),
130
+ }
131
+ : {}),
113
132
  },
114
133
  }];
115
134
  const tags = [
@@ -117,6 +136,10 @@ export function sessionEventToAiwgFortemiRecord(session, event) {
117
136
  ...(event.role ? [tag('role', event.role)] : []),
118
137
  ...(event.toolName ? [tag('tool', event.toolName)] : []),
119
138
  ...(event.entities ?? []).map((entity) => tag('entity', entity)),
139
+ ...images.map((image) => tag('image', image.digest)),
140
+ ...images
141
+ .filter((image) => image.status === 'unavailable')
142
+ .map((image) => tag('image-unavailable', image.digest)),
120
143
  ];
121
144
  return {
122
145
  schema_version: SESSION_EXPORT_RECORD_SCHEMA_VERSION,
@@ -275,13 +298,20 @@ function exportManifestRecord(repoRef, generatedAt) {
275
298
  * session catalog's workspace id, not a git repo -- the field is repurposed
276
299
  * honestly rather than left to imply something untrue).
277
300
  */
278
- export function buildSessionAiwgFortemiIndexExport(repoRef, sessions, eventsBySessionId, outputs = [], catalogTagsBySessionId = new Map()) {
301
+ export function buildSessionAiwgFortemiIndexExport(repoRef, sessions, eventsBySessionId, outputs = [], catalogTagsBySessionId = new Map(),
302
+ /**
303
+ * Resolve image references per event when the caller's catalog is at
304
+ * hand (`(event) => repository.resolveEventImages(event)`). Absent means
305
+ * references stay unresolved in the records, never that images are
306
+ * missing -- availability is only asserted when actually resolved (#3265).
307
+ */
308
+ imageResolution) {
279
309
  const generatedAt = new Date().toISOString();
280
310
  const items = [exportManifestRecord(repoRef, generatedAt)];
281
311
  for (const session of sessions) {
282
312
  items.push(sessionToAiwgFortemiRecord(session, catalogTagsBySessionId.get(session.sessionId) ?? []));
283
313
  for (const event of eventsBySessionId.get(session.sessionId) ?? []) {
284
- items.push(sessionEventToAiwgFortemiRecord(session, event));
314
+ items.push(sessionEventToAiwgFortemiRecord(session, event, imageResolution?.(event) ?? []));
285
315
  }
286
316
  for (const output of outputs.filter((entry) => entry.sessionId === session.sessionId)) {
287
317
  items.push(sessionOutputToAiwgFortemiRecord(session, output));