@aiwg/cli 2026.10.3 → 2026.10.5

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 (279) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/model-capabilities.v1.json +96 -4
  3. package/bin/aiwg.mjs +50 -27
  4. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  5. package/dist/src/artifacts/browser-export.js +6 -0
  6. package/dist/src/artifacts/checksum-manifest.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +173 -92
  8. package/dist/src/artifacts/citation-table-schema.js +59 -0
  9. package/dist/src/artifacts/cli.js +69 -18
  10. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  11. package/dist/src/artifacts/corpus-tools/vision-extract.js +2 -0
  12. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  13. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  14. package/dist/src/artifacts/discovery-eval.js +6 -4
  15. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  16. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  17. package/dist/src/artifacts/framework-capabilities.js +171 -0
  18. package/dist/src/artifacts/framework-index-paths.js +73 -0
  19. package/dist/src/artifacts/framework-source-digest.js +75 -0
  20. package/dist/src/artifacts/index-builder.js +286 -131
  21. package/dist/src/artifacts/index-files.js +132 -18
  22. package/dist/src/artifacts/index-preparation.js +130 -0
  23. package/dist/src/artifacts/index-refresh.js +591 -0
  24. package/dist/src/artifacts/index-worker.mjs +11 -0
  25. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  26. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  27. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  28. package/dist/src/artifacts/query-engine.js +410 -88
  29. package/dist/src/artifacts/relations-builder.js +168 -0
  30. package/dist/src/artifacts/relations.js +148 -0
  31. package/dist/src/artifacts/stats.js +5 -2
  32. package/dist/src/artifacts/types.js +30 -10
  33. package/dist/src/artifacts/watcher.js +1 -1
  34. package/dist/src/auth/credential-store.js +2 -1
  35. package/dist/src/catalog/cli.d.mts +5 -0
  36. package/dist/src/channel/manager.d.mts +20 -0
  37. package/dist/src/channel/manager.mjs +19 -2
  38. package/dist/src/cli/command-log.js +2 -3
  39. package/dist/src/cli/handlers/artifacts.js +3 -0
  40. package/dist/src/cli/handlers/auth.js +3 -0
  41. package/dist/src/cli/handlers/decision.js +19 -12
  42. package/dist/src/cli/handlers/diagnose.js +4 -3
  43. package/dist/src/cli/handlers/effect.js +4 -2
  44. package/dist/src/cli/handlers/evidence.js +3 -0
  45. package/dist/src/cli/handlers/help.js +1 -0
  46. package/dist/src/cli/handlers/index.js +3 -1
  47. package/dist/src/cli/handlers/init.js +4 -1
  48. package/dist/src/cli/handlers/lint.js +5 -0
  49. package/dist/src/cli/handlers/models.js +104 -8
  50. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  51. package/dist/src/cli/handlers/refresh.js +12 -0
  52. package/dist/src/cli/handlers/resource-versions.js +3 -0
  53. package/dist/src/cli/handlers/rules.js +195 -0
  54. package/dist/src/cli/handlers/run.js +4 -0
  55. package/dist/src/cli/handlers/runtime-info.js +1 -2
  56. package/dist/src/cli/handlers/script-runner.js +3 -2
  57. package/dist/src/cli/handlers/session.js +19 -9
  58. package/dist/src/cli/handlers/sessions.js +137 -43
  59. package/dist/src/cli/handlers/setup-manifest.js +39 -44
  60. package/dist/src/cli/handlers/setup.js +41 -0
  61. package/dist/src/cli/handlers/steward.js +11 -0
  62. package/dist/src/cli/handlers/subcommands.js +57 -2
  63. package/dist/src/cli/handlers/uhp.js +3 -0
  64. package/dist/src/cli/handlers/use.js +294 -55
  65. package/dist/src/cli/handlers/utilities.js +26 -10
  66. package/dist/src/cli/handlers/version.js +8 -0
  67. package/dist/src/cli/router.js +15 -1
  68. package/dist/src/cli/services/deployment-verification.js +19 -9
  69. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  70. package/dist/src/cli/skill-usage.js +2 -3
  71. package/dist/src/config/aiwg-config.js +87 -37
  72. package/dist/src/config/cli.js +111 -5
  73. package/dist/src/config/decision-config.js +106 -0
  74. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  75. package/dist/src/config/gitignore.js +50 -0
  76. package/dist/src/config/index-graph-overrides.js +36 -0
  77. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  78. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  79. package/dist/src/config/user-config-dir.d.mts +9 -0
  80. package/dist/src/config/user-config.js +2 -1
  81. package/dist/src/config/user-registry.js +23 -15
  82. package/dist/src/config/user-state.js +7 -0
  83. package/dist/src/decision/ask.js +203 -43
  84. package/dist/src/decision/context-live-qualification.js +2 -2
  85. package/dist/src/decision/driver.js +13 -8
  86. package/dist/src/decision/ensemble/aggregate.js +23 -10
  87. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  88. package/dist/src/decision/entry.js +2 -15
  89. package/dist/src/decision/graph-live-qualification.js +1 -1
  90. package/dist/src/decision/graph-run.js +9 -0
  91. package/dist/src/decision/heldout/collector.js +5 -13
  92. package/dist/src/decision/heldout/contract.js +30 -7
  93. package/dist/src/decision/jev-credentials.js +15 -1
  94. package/dist/src/decision/ledger.js +131 -28
  95. package/dist/src/decision/offload-activation.js +137 -0
  96. package/dist/src/decision/offload.js +550 -0
  97. package/dist/src/decision/qualification/load.js +4 -6
  98. package/dist/src/decision/savings.js +28 -0
  99. package/dist/src/effects/types.js +5 -1
  100. package/dist/src/effects/verifiers/git.js +2 -1
  101. package/dist/src/effects/verifiers/tracker.js +26 -14
  102. package/dist/src/extensions/commands/definitions.js +19 -0
  103. package/dist/src/extensions/project-local-remove.js +36 -1
  104. package/dist/src/gates/providers/loader.js +61 -17
  105. package/dist/src/installation/manager-command.d.mts +18 -0
  106. package/dist/src/installation/manager-command.mjs +14 -2
  107. package/dist/src/installation/manager.d.mts +49 -0
  108. package/dist/src/installation/manager.mjs +78 -3
  109. package/dist/src/issues/cli.js +4 -4
  110. package/dist/src/issues/local.js +24 -5
  111. package/dist/src/jobs/runner.js +1 -1
  112. package/dist/src/lint/cli.js +27 -1
  113. package/dist/src/lint/loader.js +2 -1
  114. package/dist/src/lint/reporters.js +25 -1
  115. package/dist/src/lint/rule-activation.js +43 -0
  116. package/dist/src/lint/runner.js +137 -62
  117. package/dist/src/mcp/adapters/codex-runtime.js +7 -1
  118. package/dist/src/mcp/claude-install.d.mts +4 -0
  119. package/dist/src/mcp/claude-install.mjs +41 -0
  120. package/dist/src/mcp/cli.d.mts +1 -0
  121. package/dist/src/mcp/cli.mjs +36 -11
  122. package/dist/src/mcp/config-file.d.mts +28 -0
  123. package/dist/src/mcp/config-layers.d.mts +36 -0
  124. package/dist/src/mcp/credentials.d.mts +51 -0
  125. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  126. package/dist/src/mcp/grok-build-config.mjs +1 -1
  127. package/dist/src/mcp/helpers.mjs +3 -2
  128. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  129. package/dist/src/mcp/muse-native-config.mjs +1 -0
  130. package/dist/src/mcp/omp-config.d.mts +4 -0
  131. package/dist/src/mcp/registry.js +3 -93
  132. package/dist/src/mcp/registry.mjs +3 -87
  133. package/dist/src/mcp/server-config.d.mts +3 -0
  134. package/dist/src/mcp/server-config.mjs +89 -0
  135. package/dist/src/mcp/server.mjs +37 -375
  136. package/dist/src/mcp/test-simple.mjs +2 -1
  137. package/dist/src/mcp/toml-editor.d.mts +4 -0
  138. package/dist/src/mcp/toml-string.d.mts +1 -0
  139. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  140. package/dist/src/mcp/tool-filters.d.mts +46 -0
  141. package/dist/src/mcp/tools/decision.mjs +12 -11
  142. package/dist/src/mcp/tools/discovery.mjs +62 -55
  143. package/dist/src/mcp/tools/orchestration.mjs +12 -2
  144. package/dist/src/mcp/tools/worker-invoke.mjs +1340 -23
  145. package/dist/src/mcp/tools/worker-shell-control.mjs +52 -21
  146. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +3 -3
  147. package/dist/src/mission-protocol/codecs.js +8 -0
  148. package/dist/src/models/config-loader.js +7 -2
  149. package/dist/src/models/model-capabilities.v1.json +96 -4
  150. package/dist/src/models/model-discovery.js +76 -11
  151. package/dist/src/models/provider-policy.js +101 -7
  152. package/dist/src/models/qualification-digest.d.mts +2 -0
  153. package/dist/src/models/resolver.js +12 -10
  154. package/dist/src/models/worker-direct-readiness.js +65 -44
  155. package/dist/src/models/worker-dispatch-offer.js +3 -3
  156. package/dist/src/models/worker-health-readiness.js +16 -3
  157. package/dist/src/models/worker-installation.d.mts +39 -0
  158. package/dist/src/models/worker-installation.mjs +135 -28
  159. package/dist/src/process/bounded-child-process.d.mts +84 -0
  160. package/dist/src/process/bounded-child-process.mjs +43 -0
  161. package/dist/src/process/cli-command.d.mts +16 -0
  162. package/dist/src/process/signal-child.d.mts +6 -0
  163. package/dist/src/process/signal-child.mjs +7 -0
  164. package/dist/src/providers/deployed-skill-roots.js +20 -0
  165. package/dist/src/providers/grok-build-acp.js +3 -2
  166. package/dist/src/providers/grok-build-headless.js +12 -4
  167. package/dist/src/providers/grok-build-inspect.js +1 -0
  168. package/dist/src/providers/omp-agent.d.mts +8 -0
  169. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  170. package/dist/src/providers/omp-paths.d.mts +13 -0
  171. package/dist/src/providers/skill-ownership.d.mts +9 -0
  172. package/dist/src/providers/skill-ownership.mjs +88 -0
  173. package/dist/src/rules/activation.d.mts +21 -0
  174. package/dist/src/rules/activation.mjs +62 -0
  175. package/dist/src/rules/inline-budget.d.mts +26 -0
  176. package/dist/src/rules/inline-budget.mjs +66 -0
  177. package/dist/src/rules/inventory.js +129 -0
  178. package/dist/src/rules/pin-state.js +73 -0
  179. package/dist/src/rules/pins.d.mts +14 -0
  180. package/dist/src/rules/pins.mjs +112 -0
  181. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  182. package/dist/src/serve/mission-conductor.js +38 -12
  183. package/dist/src/serve/worker-invocation.js +30 -10
  184. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  185. package/dist/src/sessions/adapter-registry.js +23 -19
  186. package/dist/src/sessions/adapters/claude.js +39 -54
  187. package/dist/src/sessions/adapters/codex.js +95 -67
  188. package/dist/src/sessions/adapters/copilot.js +4 -4
  189. package/dist/src/sessions/adapters/cursor.js +40 -57
  190. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  191. package/dist/src/sessions/adapters/factory.js +31 -27
  192. package/dist/src/sessions/adapters/generic.js +10 -10
  193. package/dist/src/sessions/adapters/grok-build.js +11 -11
  194. package/dist/src/sessions/adapters/grokbot.js +24 -13
  195. package/dist/src/sessions/adapters/hermes.js +27 -29
  196. package/dist/src/sessions/adapters/muse-workspace-scan.js +31 -11
  197. package/dist/src/sessions/adapters/muse.js +150 -93
  198. package/dist/src/sessions/adapters/omp.js +28 -14
  199. package/dist/src/sessions/adapters/openclaw.js +89 -79
  200. package/dist/src/sessions/adapters/opencode.js +27 -24
  201. package/dist/src/sessions/adapters/openhuman.js +18 -60
  202. package/dist/src/sessions/adapters/pi.js +29 -14
  203. package/dist/src/sessions/adapters/warp.js +3 -3
  204. package/dist/src/sessions/adapters/windsurf.js +19 -48
  205. package/dist/src/sessions/batch-contracts.js +16 -8
  206. package/dist/src/sessions/batch-import.js +252 -12
  207. package/dist/src/sessions/contracts.js +50 -3
  208. package/dist/src/sessions/coverage-preflight.js +27 -2
  209. package/dist/src/sessions/export-plan.js +4 -3
  210. package/dist/src/sessions/import-exclusions.js +72 -0
  211. package/dist/src/sessions/import-lease.js +34 -18
  212. package/dist/src/sessions/import-limits.js +37 -0
  213. package/dist/src/sessions/importer.js +60 -38
  214. package/dist/src/sessions/lifecycle.js +45 -0
  215. package/dist/src/sessions/policy.js +145 -26
  216. package/dist/src/sessions/readers.js +106 -53
  217. package/dist/src/sessions/refresh.js +105 -0
  218. package/dist/src/sessions/repository.js +151 -54
  219. package/dist/src/sessions/workspace-discovery.js +292 -60
  220. package/dist/src/sessions/workspace-matching.js +107 -0
  221. package/dist/src/skills/importer.js +1 -0
  222. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  223. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  224. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  225. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  226. package/dist/src/smiths/context-pipeline/parallelism-section.js +7 -15
  227. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  228. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  229. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +76 -18
  230. package/dist/src/storage/backends/fs.js +18 -76
  231. package/dist/src/storage/backends/obsidian.js +1 -0
  232. package/dist/src/tracker/forge-http.js +2 -1
  233. package/dist/src/update/checker.d.mts +2 -0
  234. package/dist/src/update/service.d.mts +22 -0
  235. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  236. package/dist/src/utils/contained-filesystem.mjs +683 -0
  237. package/dist/src/utils/temporary-directory.js +36 -0
  238. package/dist/src/writing/example-generator.js +1 -1
  239. package/package.json +15 -2
  240. package/schemas/models/model-catalog.v1.schema.json +1 -1
  241. package/schemas/models/provider-model-capabilities.v1.schema.json +6 -0
  242. package/tools/_resolve-impl.mjs +13 -15
  243. package/tools/agents/deploy-agents.mjs +49 -21
  244. package/tools/agents/providers/base.mjs +104 -134
  245. package/tools/agents/providers/claude.mjs +8 -24
  246. package/tools/agents/providers/codex.mjs +18 -11
  247. package/tools/agents/providers/cursor.mjs +19 -18
  248. package/tools/agents/providers/grok-build.mjs +3 -1
  249. package/tools/agents/providers/hermes.mjs +57 -47
  250. package/tools/agents/providers/openclaw.mjs +9 -4
  251. package/tools/agents/providers/pi.mjs +5 -0
  252. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  253. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  254. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  255. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  256. package/tools/agents/providers/warp.mjs +20 -26
  257. package/tools/cli/decision-offload-report.mjs +40 -0
  258. package/tools/cli/workspace-status.mjs +862 -0
  259. package/tools/plugin/package-plugins.mjs +186 -8
  260. package/tools/plugin/plugin-links.mjs +114 -0
  261. package/tools/providers/antigravity-transport.mjs +3 -1
  262. package/tools/providers/deepseek-harness-transport.mjs +6 -4
  263. package/tools/providers/omp-transport.mjs +4 -2
  264. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  265. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  266. package/tools/ralph-external/lib/omp-adapter.mjs +1 -1
  267. package/tools/ralph-external/lib/pi-adapter.mjs +0 -4
  268. package/tools/ralph-external/lib/provider-adapter.mjs +59 -1
  269. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  270. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  271. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  272. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  273. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  274. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  275. package/tools/ralph-external/process-monitor.mjs +1017 -0
  276. package/tools/ralph-external/session-launcher.mjs +5057 -0
  277. package/tools/scripts/resource-preflight.mjs +541 -0
  278. package/tools/skills/deploy-skills-codex.mjs +2 -1
  279. package/vscode-extension/schemas/aiwg.config.v1.json +1194 -231
@@ -1,17 +1,18 @@
1
- import { createReadStream } from 'node:fs';
2
- import { access, mkdir, readFile, realpath, rename, stat, writeFile, } from 'node:fs/promises';
1
+ import { access, mkdir, readFile, readdir, realpath, rename, stat, writeFile, } from 'node:fs/promises';
3
2
  import { homedir, userInfo } from 'node:os';
4
3
  import { basename, dirname, join, resolve } from 'node:path';
5
- import { createInterface } from 'node:readline';
6
4
  import { inspectMuseNativeWorkspace } from './adapters/muse.js';
7
5
  import { readOmpSessionHeader } from './adapters/omp.js';
8
6
  import { grokbotConversationIdFromLocator } from './adapters/grokbot-locator.js';
9
7
  import { resolveOmpPaths } from '../providers/omp-paths.mjs';
10
- import { SESSION_PROVIDER_IDS, SessionCoveragePreflightSchema, SessionContractError, sha256, } from './contracts.js';
8
+ import { readAiwgConfig } from '../config/aiwg-config.js';
9
+ import { SessionCoveragePreflightSchema, SessionContractError, sha256, } from './contracts.js';
11
10
  import { redactSourceLocator } from './discovery.js';
12
11
  import { createSessionAdapterRegistry } from './adapter-registry.js';
13
- import { coveragePreflight, expectedSessionProviders } from './coverage-preflight.js';
14
- import { fingerprintSourceFile } from './readers.js';
12
+ import { coveragePreflight, providerScopeWarnings, resolveSessionProviderScope } from './coverage-preflight.js';
13
+ import { fingerprintSourceFile, streamJsonLines } from './readers.js';
14
+ import { matchSessionWorkspace } from './workspace-matching.js';
15
+ import { matchImportExclusion, nativeSessionIdFromLocator, resolveImportExclusions } from './import-exclusions.js';
15
16
  export const DISCOVERY_MANIFEST_VERSION = '1.0.0';
16
17
  const MANUAL_EXPORT_PROVIDERS = new Set([
17
18
  'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman', 'grok-build',
@@ -19,13 +20,36 @@ const MANUAL_EXPORT_PROVIDERS = new Set([
19
20
  ]);
20
21
  export async function discoverWorkspaceHistories(options) {
21
22
  const registry = options.registry ?? createSessionAdapterRegistry();
22
- const expectedProviders = expectedSessionProviders(options.expectedProviders ?? SESSION_PROVIDER_IDS);
23
+ const exclusions = resolveImportExclusions(undefined, options.exclude);
23
24
  const supportedProviders = new Set(registry.list().map((adapter) => adapter.provider));
24
25
  const workspacePath = await canonicalPath(options.workspace);
26
+ const config = await readAiwgConfig(workspacePath);
27
+ const configuredProviders = config?.providers ?? [];
28
+ const explicitlyRootedProviders = [
29
+ ...(options.codexRoot || options.codexRoots?.length || config?.sessions?.codex_roots?.length ? ['codex'] : []),
30
+ ...(options.ompRoot ? ['omp'] : []),
31
+ ...(options.museRoot ? ['muse'] : []),
32
+ ...(options.dshRoot ? ['deepseek-harness'] : []),
33
+ ...(options.grokbotRoot ? ['grokbot'] : []),
34
+ ];
35
+ const savedRoots = options.authorizedRoots ?? options.refreshFrom?.authorizedRoots;
36
+ const authorizedProviders = options.refreshFrom
37
+ ? Object.keys(savedRoots ?? {}).filter(provider => savedRoots?.[provider]?.length)
38
+ : configuredProviders.length ? explicitlyRootedProviders : undefined;
39
+ const providerScope = resolveSessionProviderScope(configuredProviders, options.expectedProviders, authorizedProviders);
40
+ if (options.refreshFrom) {
41
+ providerScope.scopeWarnings = providerScopeWarnings(configuredProviders, options.refreshFrom.preflight?.expectedProviders ?? []);
42
+ }
43
+ const { expectedProviders } = providerScope;
25
44
  const workspaceId = workspacePath;
45
+ const workspacePaths = [...new Set([workspacePath, resolve(options.workspace),
46
+ ...(options.workspaceAliases ?? []).map((alias) => resolve(alias))])];
47
+ for (const alias of workspacePaths) {
48
+ if (await canonicalPath(alias) !== workspacePath) {
49
+ throw new SessionContractError('INVALID_ARGUMENT', 'workspace alias resolves to a different workspace');
50
+ }
51
+ }
26
52
  const providerHomes = providerHomeCandidates(options.providerHome, options.operatorHome);
27
- const keyWithLeadingDash = workspaceKey(workspacePath, true);
28
- const keyWithoutLeadingDash = workspaceKey(workspacePath, false);
29
53
  const discoverable = [
30
54
  { provider: 'deepseek-harness', roots: options.dshRoot
31
55
  ? [resolve(options.dshRoot)] : options.providerHome
@@ -46,26 +70,30 @@ export async function discoverWorkspaceHistories(options) {
46
70
  ? [resolve(options.grokbotRoot)] : [] },
47
71
  {
48
72
  provider: 'claude',
49
- roots: providerHomes.map((providerHome) => join(providerHome, '.claude', 'projects', keyWithLeadingDash)),
73
+ // Same scope rule as codex: configured providers keep their root grant
74
+ // even when a narrow discovery does not scan them (#3402).
75
+ roots: savedRoots || (!expectedProviders.includes('claude') && !providerScope.configuredProviders?.includes('claude'))
76
+ ? [] : await claudeDiscoveryRoots(providerHomes, workspacePaths, workspacePath, registry, exclusions),
50
77
  },
51
78
  {
52
79
  provider: 'codex',
53
- roots: options.codexRoot
54
- ? [resolve(options.codexRoot)]
55
- : options.providerHome
56
- ? [join(resolve(options.providerHome), '.codex', 'sessions')]
57
- : [],
80
+ roots: savedRoots || (!expectedProviders.includes('codex') && !providerScope.configuredProviders?.includes('codex')
81
+ && !explicitlyRootedProviders.includes('codex'))
82
+ ? [] : await codexDiscoveryRoots({ ...options, codexRoots: [
83
+ ...(config?.sessions?.codex_roots ?? []).map(root => resolve(workspacePath, root)),
84
+ ...(options.codexRoots ?? []),
85
+ ] }),
58
86
  },
59
87
  {
60
88
  provider: 'cursor',
61
- roots: providerHomes.map((providerHome) => join(providerHome, '.cursor', 'projects', keyWithoutLeadingDash, 'agent-transcripts')),
89
+ roots: providerHomes.flatMap((providerHome) => workspacePaths.map((workspace) => join(providerHome, '.cursor', 'projects', workspaceKey(workspace, false), 'agent-transcripts'))),
62
90
  },
63
91
  {
64
92
  provider: 'factory',
65
- roots: providerHomes.flatMap((providerHome) => [
66
- join(providerHome, '.factory', 'projects', keyWithLeadingDash),
67
- join(providerHome, '.factory', 'sessions', keyWithLeadingDash),
68
- ]),
93
+ roots: providerHomes.flatMap((providerHome) => workspacePaths.flatMap((workspace) => [
94
+ join(providerHome, '.factory', 'projects', workspaceKey(workspace, true)),
95
+ join(providerHome, '.factory', 'sessions', workspaceKey(workspace, true)),
96
+ ])),
69
97
  },
70
98
  {
71
99
  // Pi stores every working directory's sessions in its own
@@ -79,22 +107,52 @@ export async function discoverWorkspaceHistories(options) {
79
107
  provider: 'pi',
80
108
  roots: (process.env.PI_CODING_AGENT_SESSION_DIR
81
109
  ? [resolve(process.env.PI_CODING_AGENT_SESSION_DIR)]
82
- : providerHomes.map((providerHome) => join(providerHome, '.pi', 'agent', 'sessions'))).map((sessionsRoot) => join(sessionsRoot, `--${keyWithoutLeadingDash}--`)),
110
+ : providerHomes.map((providerHome) => join(providerHome, '.pi', 'agent', 'sessions'))).flatMap((sessionsRoot) => workspacePaths.map((workspace) => join(sessionsRoot, `--${workspaceKey(workspace, false)}--`))),
83
111
  },
84
112
  ];
113
+ const authorizedRoots = { ...savedRoots };
85
114
  const reports = new Map();
86
115
  const candidates = [];
87
116
  const seen = new Set();
88
117
  for (const entry of discoverable) {
118
+ // A hand-run narrow scope must not discard explicit root grants, including
119
+ // configured providers under an authorized home. Do not scan their histories.
120
+ if (!savedRoots && !expectedProviders.includes(entry.provider)
121
+ && (explicitlyRootedProviders.includes(entry.provider)
122
+ || (providerScope.configuredProviders?.includes(entry.provider) && options.providerHome))) {
123
+ const roots = [];
124
+ for (const root of entry.roots)
125
+ if (await pathExists(root))
126
+ roots.push(await canonicalPath(root));
127
+ authorizedRoots[entry.provider] = [...new Set(roots)];
128
+ }
89
129
  if (!expectedProviders.includes(entry.provider) || !supportedProviders.has(entry.provider))
90
130
  continue;
91
131
  const adapter = registry.get(entry.provider);
92
132
  if (adapter.disposition === 'unsupported' || !adapter.supportedOperations.includes('discover'))
93
133
  continue;
94
134
  const availableRoots = [];
95
- for (const root of entry.roots) {
96
- if (await pathExists(root))
97
- availableRoots.push(await canonicalPath(root));
135
+ const rootCounts = new Map();
136
+ for (const root of savedRoots ? savedRoots[entry.provider] ?? [] : entry.roots) {
137
+ if (await pathExists(root)) {
138
+ const canonical = await canonicalPath(root);
139
+ if (savedRoots && canonical !== root) {
140
+ throw new SessionContractError('INVALID_ARGUMENT', 'previously authorized provider root changed; rediscover with explicit authorization');
141
+ }
142
+ if (!availableRoots.includes(canonical)) {
143
+ availableRoots.push(canonical);
144
+ rootCounts.set(canonical, {
145
+ root: redactSourceLocator(canonical), rootId: sha256(canonical),
146
+ status: 'checked', sourceCount: 0, duplicateCount: 0,
147
+ });
148
+ }
149
+ }
150
+ else {
151
+ rootCounts.set(root, {
152
+ root: redactSourceLocator(root), rootId: sha256(root),
153
+ status: 'unavailable', sourceCount: 0, duplicateCount: 0,
154
+ });
155
+ }
98
156
  }
99
157
  if (availableRoots.length === 0) {
100
158
  const codexNeedsAuthorization = (entry.provider === 'codex' || entry.provider === 'omp'
@@ -106,15 +164,44 @@ export async function discoverWorkspaceHistories(options) {
106
164
  : 'PROVIDER_ROOT_UNAVAILABLE', codexNeedsAuthorization
107
165
  ? `Pass --${entry.provider === 'deepseek-harness' ? 'dsh' : entry.provider}-root with an explicitly authorized sessions root.`
108
166
  : `No authorized ${entry.provider} workspace history root was found.`));
167
+ if (entry.provider === 'codex')
168
+ reports.get(entry.provider).rootCounts = [...rootCounts.values()];
109
169
  continue;
110
170
  }
171
+ authorizedRoots[entry.provider] = availableRoots;
111
172
  const scope = {
112
173
  workspaceId,
113
174
  allowedRoots: availableRoots,
114
175
  };
115
176
  const providerSources = [];
177
+ let matchedBySubfolder = 0;
178
+ let matchedByWorktree = 0;
116
179
  const museSourceInspections = [];
117
180
  for await (const descriptor of adapter.discover(scope)) {
181
+ const known = options.knownSources?.find(source => source.provider === descriptor.provider && source.locator === descriptor.locator);
182
+ const nativeSessionId = known?.nativeSessionId ?? descriptor.nativeSessionId ?? nativeSessionIdFromLocator(descriptor);
183
+ const exclusion = matchImportExclusion({ ...descriptor, nativeSessionId }, exclusions);
184
+ if (exclusion) {
185
+ const locator = resolve(descriptor.locator);
186
+ const authorizedRoot = scope.allowedRoots.find(root => locator === root || locator.startsWith(`${root}/`));
187
+ if (!authorizedRoot)
188
+ continue;
189
+ const key = `excluded\0${entry.provider}\0${locator}`;
190
+ if (seen.has(key))
191
+ continue;
192
+ seen.add(key);
193
+ const source = {
194
+ sourceId: known?.sourceId ?? sha256(key), provider: entry.provider,
195
+ locator, redactedLocator: redactSourceLocator(locator), locatorClass: descriptor.locatorClass,
196
+ authorizedRoot, modifiedAt: '', sizeBytes: 0, digest: '', nativeSessionId,
197
+ status: 'excluded', exclusion,
198
+ };
199
+ rootCounts.get(authorizedRoot).sourceCount += 1;
200
+ providerSources.push(source);
201
+ candidates.push(source);
202
+ continue;
203
+ }
204
+ let workspaceMatch = null;
118
205
  if (entry.provider === 'muse') {
119
206
  const inspection = await inspectMuseNativeWorkspace({
120
207
  ...descriptor,
@@ -124,31 +211,57 @@ export async function discoverWorkspaceHistories(options) {
124
211
  museSourceInspections.push({
125
212
  sourceKey: sha256(['muse-workspace-inspection-v1', descriptor.locator].join('\0')),
126
213
  redactedLocator: redactSourceLocator(descriptor.locator),
214
+ nativeSessionId: basename(dirname(descriptor.locator)),
127
215
  ...inspection,
128
216
  });
129
217
  if (inspection.outcome !== 'matched')
130
218
  continue;
219
+ workspaceMatch = inspection.matchKind ?? null;
131
220
  }
132
221
  const locator = await canonicalPath(descriptor.locator);
133
- if (entry.provider === 'codex'
134
- && !await codexSourceMatchesWorkspace(locator, workspacePath))
135
- continue;
222
+ if (entry.provider === 'codex') {
223
+ workspaceMatch = await codexSourceWorkspaceMatch(locator, workspacePath);
224
+ if (!workspaceMatch)
225
+ continue;
226
+ }
136
227
  const ompHeader = entry.provider === 'omp' ? await readOmpSessionHeader({ ...descriptor, sourceId: 'discovery', authorizedScope: scope }) : undefined;
137
- if (ompHeader && resolve(ompHeader.cwd) !== workspacePath)
138
- continue;
228
+ if (ompHeader) {
229
+ workspaceMatch = await matchSessionWorkspace(ompHeader.cwd, workspacePath);
230
+ if (!workspaceMatch)
231
+ continue;
232
+ }
139
233
  const details = await stat(locator);
140
234
  const authorizedRoot = scope.allowedRoots.find((root) => locator === root || locator.startsWith(`${root}/`));
141
235
  if (!authorizedRoot)
142
236
  continue;
237
+ if (entry.provider === 'claude') {
238
+ const exactProject = workspacePaths.some(path => basename(authorizedRoot) === claudeProjectKey(path));
239
+ workspaceMatch = await claudeSourceWorkspaceMatch(locator, workspacePath, exactProject);
240
+ if (!workspaceMatch)
241
+ continue;
242
+ }
143
243
  const fingerprint = await fingerprintSourceFile({
144
244
  selectedPath: locator,
145
245
  allowedRoots: [authorizedRoot],
146
246
  });
147
- const dedupeKey = `${entry.provider}\0${fingerprint.digest}\0${fingerprint.size}`;
148
- if (seen.has(dedupeKey))
247
+ const rolloutId = entry.provider === 'codex' && descriptor.locatorClass === 'codex-rollout-jsonl'
248
+ ? await codexRolloutId(locator) : undefined;
249
+ const dedupeKey = rolloutId
250
+ ? `codex\0rollout\0${rolloutId}`
251
+ : `${entry.provider}\0${fingerprint.digest}\0${fingerprint.size}`;
252
+ const rootCount = rootCounts.get(authorizedRoot);
253
+ if (seen.has(dedupeKey)) {
254
+ rootCount.duplicateCount += 1;
149
255
  continue;
256
+ }
150
257
  seen.add(dedupeKey);
258
+ if (workspaceMatch === 'subfolder')
259
+ matchedBySubfolder += 1;
260
+ if (workspaceMatch === 'worktree')
261
+ matchedByWorktree += 1;
262
+ rootCount.sourceCount += 1;
151
263
  const source = sourceFromDescriptor(descriptor, locator, authorizedRoot, details, fingerprint.digest);
264
+ source.nativeSessionId = rolloutId ?? ompHeader?.id ?? nativeSessionId;
152
265
  if (ompHeader)
153
266
  source.sourceId = sha256(['omp-native-source-v1', workspaceId, ompHeader.id].join('\0'));
154
267
  if (entry.provider === 'grokbot') {
@@ -168,13 +281,17 @@ export async function discoverWorkspaceHistories(options) {
168
281
  const incompleteMuseInspection = museSourceInspections.some((inspection) => inspection.outcome === 'inconclusive');
169
282
  const report = providerReport(entry.provider, incompleteMuseInspection ? 'not-checked' : 'checked', 'discoverable', providerSources, incompleteMuseInspection ? 'MUSE_SOURCE_INSPECTION_INCOMPLETE'
170
283
  : providerSources.length === 0 ? 'NO_WORKSPACE_SOURCES' : null, incompleteMuseInspection
171
- ? 'Review museSourceInspections; authorize a supported Muse export for unresolved sources before claiming workspace coverage.'
284
+ ? 'Review unattributed museSourceInspections and their limit name/value; authorize a supported Muse export for unresolved sources before claiming workspace coverage.'
172
285
  : providerSources.length === 0
173
286
  ? `The ${entry.provider} root was checked, but no source matched the authorized workspace.`
174
287
  : null);
175
288
  if (entry.provider === 'muse') {
176
289
  report.museSourceInspections = museSourceInspections.sort((a, b) => a.sourceKey.localeCompare(b.sourceKey));
177
290
  }
291
+ report.matchedBySubfolder = matchedBySubfolder;
292
+ report.matchedByWorktree = matchedByWorktree;
293
+ if (entry.provider === 'codex')
294
+ report.rootCounts = [...rootCounts.values()];
178
295
  reports.set(entry.provider, report);
179
296
  }
180
297
  for (const provider of expectedProviders) {
@@ -196,13 +313,14 @@ export async function discoverWorkspaceHistories(options) {
196
313
  const sources = candidates.sort(compareSources);
197
314
  const providers = [...reports.values()].sort((a, b) => a.provider.localeCompare(b.provider));
198
315
  const preflight = coveragePreflight({
199
- expectedProviders, expectedProviderBasis: options.expectedProviders ? 'explicit' : 'declared-default',
316
+ ...providerScope,
200
317
  registry, runtime: options.runtime, reports: providers,
201
318
  });
202
319
  const identity = {
203
320
  schemaVersion: DISCOVERY_MANIFEST_VERSION,
204
321
  workspaceId,
205
322
  preflight,
323
+ authorizedRoots,
206
324
  sources: sources.map((source) => ({
207
325
  sourceId: source.sourceId,
208
326
  provider: source.provider,
@@ -211,11 +329,16 @@ export async function discoverWorkspaceHistories(options) {
211
329
  modifiedAt: source.modifiedAt,
212
330
  sizeBytes: source.sizeBytes,
213
331
  digest: source.digest,
332
+ ...(source.nativeSessionId ? { nativeSessionId: source.nativeSessionId } : {}),
333
+ ...(source.status ? { status: source.status, exclusion: source.exclusion } : {}),
214
334
  })),
215
335
  providers: providers.map((provider) => ({
216
336
  provider: provider.provider,
217
337
  status: provider.status,
218
338
  reasonCode: provider.reasonCode,
339
+ ...(provider.matchedBySubfolder !== undefined ? { matchedBySubfolder: provider.matchedBySubfolder } : {}),
340
+ ...(provider.matchedByWorktree !== undefined ? { matchedByWorktree: provider.matchedByWorktree } : {}),
341
+ ...(provider.rootCounts ? { rootCounts: provider.rootCounts } : {}),
219
342
  ...(provider.museSourceInspections ? { museSourceInspections: provider.museSourceInspections } : {}),
220
343
  })),
221
344
  };
@@ -226,6 +349,7 @@ export async function discoverWorkspaceHistories(options) {
226
349
  workspaceId,
227
350
  workspacePath,
228
351
  preflight,
352
+ authorizedRoots,
229
353
  providers,
230
354
  sources,
231
355
  };
@@ -272,6 +396,9 @@ export function publicDiscoveryManifest(manifest) {
272
396
  totals: {
273
397
  providers: manifest.providers.length,
274
398
  sources: manifest.sources.length,
399
+ excluded: manifest.sources.filter(source => source.status === 'excluded').length,
400
+ matchedBySubfolder: manifest.providers.reduce((sum, provider) => sum + (provider.matchedBySubfolder ?? 0), 0),
401
+ matchedByWorktree: manifest.providers.reduce((sum, provider) => sum + (provider.matchedByWorktree ?? 0), 0),
275
402
  },
276
403
  };
277
404
  }
@@ -291,12 +418,14 @@ function sourceFromDescriptor(descriptor, locator, authorizedRoot, details, dige
291
418
  };
292
419
  }
293
420
  function providerReport(provider, status, disposition, sources, reasonCode, remediation) {
294
- const timestamps = sources.map((source) => source.modifiedAt).sort();
421
+ const timestamps = sources.map((source) => source.modifiedAt).filter(Boolean).sort();
295
422
  return {
296
423
  provider,
297
424
  status,
298
425
  disposition,
299
426
  sourceCount: sources.length,
427
+ matchedBySubfolder: 0,
428
+ matchedByWorktree: 0,
300
429
  dateRange: {
301
430
  earliest: timestamps.at(0) ?? null,
302
431
  latest: timestamps.at(-1) ?? null,
@@ -311,6 +440,7 @@ async function rediscoverManifestIdentity(manifest) {
311
440
  schemaVersion: manifest.schemaVersion,
312
441
  workspaceId: manifest.workspaceId,
313
442
  ...(manifest.preflight ? { preflight: manifest.preflight } : {}),
443
+ ...(manifest.authorizedRoots ? { authorizedRoots: manifest.authorizedRoots } : {}),
314
444
  sources: manifest.sources.map((source) => ({
315
445
  sourceId: source.sourceId,
316
446
  provider: source.provider,
@@ -319,47 +449,84 @@ async function rediscoverManifestIdentity(manifest) {
319
449
  modifiedAt: source.modifiedAt,
320
450
  sizeBytes: source.sizeBytes,
321
451
  digest: source.digest,
452
+ ...(source.nativeSessionId ? { nativeSessionId: source.nativeSessionId } : {}),
453
+ ...(source.status ? { status: source.status, exclusion: source.exclusion } : {}),
322
454
  })),
323
455
  providers: manifest.providers.map((provider) => ({
324
456
  provider: provider.provider,
325
457
  status: provider.status,
326
458
  reasonCode: provider.reasonCode,
459
+ ...(provider.matchedBySubfolder !== undefined ? { matchedBySubfolder: provider.matchedBySubfolder } : {}),
460
+ ...(provider.matchedByWorktree !== undefined ? { matchedByWorktree: provider.matchedByWorktree } : {}),
461
+ ...(provider.rootCounts ? { rootCounts: provider.rootCounts } : {}),
327
462
  ...(provider.museSourceInspections ? { museSourceInspections: provider.museSourceInspections } : {}),
328
463
  })),
329
464
  };
330
465
  return sha256(JSON.stringify(identity));
331
466
  }
332
- async function codexSourceMatchesWorkspace(locator, workspacePath) {
333
- const input = createReadStream(locator, { encoding: 'utf8' });
334
- const lines = createInterface({ input, crlfDelay: Infinity });
335
- let count = 0;
467
+ async function codexDiscoveryRoots(options) {
468
+ const explicit = [...(options.codexRoots ?? []), ...(options.codexRoot ? [options.codexRoot] : [])];
469
+ if (explicit.length > 0)
470
+ return [...new Set(explicit.map((root) => resolve(root)))];
471
+ // Shared stores require the same explicit provider-home authorization as the default root.
472
+ if (!options.providerHome)
473
+ return [];
474
+ const codexHome = join(resolve(options.providerHome), '.codex');
475
+ const roots = [join(codexHome, 'sessions')];
476
+ if (process.env.CODEX_HOME)
477
+ roots.push(resolve(process.env.CODEX_HOME, 'sessions'));
336
478
  try {
337
- for await (const line of lines) {
338
- if (++count > 50)
339
- break;
340
- let value;
341
- try {
342
- value = JSON.parse(line);
343
- }
344
- catch {
345
- continue;
346
- }
347
- const cwd = codexWorkspaceField(value);
348
- if (!cwd)
349
- continue;
350
- try {
351
- return await canonicalPath(cwd) === workspacePath;
352
- }
353
- catch {
354
- return resolve(cwd) === workspacePath;
479
+ const roles = await readdir(join(codexHome, 'roles-runtime'), { withFileTypes: true });
480
+ for (const role of roles.sort((a, b) => a.name.localeCompare(b.name))) {
481
+ if (role.isDirectory() || role.isSymbolicLink()) {
482
+ roots.push(join(codexHome, 'roles-runtime', role.name, '.codex', 'sessions'));
355
483
  }
356
484
  }
357
- return false;
358
485
  }
359
- finally {
360
- lines.close();
361
- input.destroy();
486
+ catch (error) {
487
+ if (!['ENOENT', 'ENOTDIR'].includes(error.code ?? ''))
488
+ throw error;
362
489
  }
490
+ return [...new Set(roots)];
491
+ }
492
+ async function codexRolloutId(locator) {
493
+ let count = 0;
494
+ for await (const { line } of streamJsonLines(locator)) {
495
+ if (++count > 50)
496
+ break;
497
+ let value;
498
+ try {
499
+ value = JSON.parse(line);
500
+ }
501
+ catch {
502
+ continue;
503
+ }
504
+ const record = asObject(value);
505
+ const id = asObject(record.payload).id;
506
+ if (record.type === 'session_meta' && typeof id === 'string' && id.length > 0)
507
+ return id;
508
+ }
509
+ return /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i
510
+ .exec(basename(locator))?.[1];
511
+ }
512
+ async function codexSourceWorkspaceMatch(locator, workspacePath) {
513
+ let count = 0;
514
+ for await (const { line } of streamJsonLines(locator)) {
515
+ if (++count > 50)
516
+ break;
517
+ let value;
518
+ try {
519
+ value = JSON.parse(line);
520
+ }
521
+ catch {
522
+ continue;
523
+ }
524
+ const cwd = codexWorkspaceField(value);
525
+ if (!cwd)
526
+ continue;
527
+ return await matchSessionWorkspace(cwd, workspacePath);
528
+ }
529
+ return null;
363
530
  }
364
531
  function codexWorkspaceField(value) {
365
532
  const root = asObject(value);
@@ -384,6 +551,71 @@ function asObject(value) {
384
551
  async function canonicalPath(path) {
385
552
  return realpath(resolve(path));
386
553
  }
554
+ /** Claude keys are lossy: use transcript cwd, never decode hyphens into paths. */
555
+ async function claudeDiscoveryRoots(homes, paths, workspace, registry, exclusions) {
556
+ const roots = homes.flatMap(home => paths.map(path => join(home, '.claude', 'projects', claudeProjectKey(path))));
557
+ const adapter = registry.get('claude');
558
+ for (const home of homes) {
559
+ const projects = join(home, '.claude', 'projects');
560
+ let entries;
561
+ try {
562
+ entries = await readdir(projects, { withFileTypes: true });
563
+ }
564
+ catch (error) {
565
+ if (['ENOENT', 'ENOTDIR'].includes(error.code ?? ''))
566
+ continue;
567
+ throw error;
568
+ }
569
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
570
+ if (!entry.isDirectory())
571
+ continue;
572
+ const root = join(projects, entry.name);
573
+ if (roots.includes(root))
574
+ continue;
575
+ for await (const descriptor of adapter.discover({ workspaceId: workspace, allowedRoots: [root] })) {
576
+ if (matchImportExclusion(descriptor, exclusions))
577
+ continue;
578
+ if (await claudeSourceWorkspaceMatch(descriptor.locator, workspace, false)) {
579
+ roots.push(root);
580
+ break;
581
+ }
582
+ }
583
+ }
584
+ }
585
+ return [...new Set(roots)];
586
+ }
587
+ async function claudeSourceWorkspaceMatch(locator, workspacePath, exactProject) {
588
+ let count = 0;
589
+ for await (const { line } of streamJsonLines(locator, { end: 1024 * 1024 - 1 })) {
590
+ if (++count > 50)
591
+ break;
592
+ let value;
593
+ try {
594
+ value = JSON.parse(line);
595
+ }
596
+ catch {
597
+ continue;
598
+ }
599
+ const cwd = asObject(value).cwd;
600
+ if (typeof cwd !== 'string' || cwd.length === 0)
601
+ continue;
602
+ return matchSessionWorkspace(cwd, workspacePath);
603
+ }
604
+ // Only exact project keys can establish ownership without cwd.
605
+ return exactProject ? 'exact' : null;
606
+ }
607
+ export function claudeProjectKey(path) {
608
+ // Claude Code 2.1.302 replaces each character, then caps keys at 200
609
+ // characters with a base-36 suffix from the original path's 32-bit hash.
610
+ const key = path.replace(/[^A-Za-z0-9-]/g, '-');
611
+ if (key.length <= 200)
612
+ return key;
613
+ let hash = 0;
614
+ for (let index = 0; index < path.length; index += 1) {
615
+ hash = ((hash << 5) - hash + path.charCodeAt(index)) | 0;
616
+ }
617
+ return `${key.slice(0, 200)}-${Math.abs(hash).toString(36)}`;
618
+ }
387
619
  function workspaceKey(path, retainLeadingSeparator) {
388
620
  const normalized = path.replace(/\\/g, '/');
389
621
  const input = retainLeadingSeparator ? normalized : normalized.replace(/^\/+/, '');
@@ -0,0 +1,107 @@
1
+ import { lstat, readFile, realpath, stat } from 'node:fs/promises';
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ export const WORKSPACE_MATCH_KINDS = ['exact', 'subfolder', 'worktree'];
4
+ /** A nested catalog owns its subtree, even when discovery runs one workspace at a time. */
5
+ export async function matchSessionWorkspace(cwd, workspace) {
6
+ try {
7
+ const { path: candidate, missing } = await resolveCandidate(resolve(cwd));
8
+ const workspacePath = await realpath(resolve(workspace));
9
+ const direct = encloses(workspacePath, candidate);
10
+ if (missing && !direct)
11
+ return null;
12
+ const physicalCatalog = await nearestCatalogBetween(candidate, direct ? workspacePath : undefined);
13
+ if (physicalCatalog && !encloses(physicalCatalog, workspacePath))
14
+ return null;
15
+ const mainRepository = await worktreeMainRepository(candidate).catch(() => null);
16
+ // A worktree also belongs to a deeper catalog around its main repository,
17
+ // including when both paths sit beneath the workspace being discovered.
18
+ if (mainRepository && encloses(workspacePath, mainRepository)
19
+ && await nearestCatalogBetween(mainRepository, workspacePath))
20
+ return null;
21
+ if (direct)
22
+ return candidate === workspacePath ? 'exact' : 'subfolder';
23
+ if (!mainRepository || !encloses(workspacePath, mainRepository))
24
+ return null;
25
+ return 'worktree';
26
+ }
27
+ catch {
28
+ // Unresolved paths or Git metadata cannot establish workspace ownership.
29
+ return null;
30
+ }
31
+ }
32
+ async function resolveCandidate(path) {
33
+ let ancestor = path;
34
+ const suffix = [];
35
+ for (;;) {
36
+ try {
37
+ // Stop at symlinks too, so realpath rejects dangling links below.
38
+ await lstat(ancestor);
39
+ break;
40
+ }
41
+ catch (error) {
42
+ if (error.code !== 'ENOENT')
43
+ throw error;
44
+ const parent = dirname(ancestor);
45
+ if (parent === ancestor)
46
+ throw error;
47
+ suffix.unshift(basename(ancestor));
48
+ ancestor = parent;
49
+ }
50
+ }
51
+ return { path: join(await realpath(ancestor), ...suffix), missing: suffix.length > 0 };
52
+ }
53
+ function encloses(parent, child) {
54
+ const suffix = relative(parent, child);
55
+ return suffix === '' || (!isAbsolute(suffix) && suffix !== '..' && !suffix.startsWith(`..${sep}`));
56
+ }
57
+ async function nearestCatalogBetween(candidate, stop) {
58
+ for (let directory = candidate; directory !== stop; directory = dirname(directory)) {
59
+ const catalog = await statIfPresent(join(directory, '.aiwg', 'sessions'));
60
+ if (catalog?.isDirectory())
61
+ return directory;
62
+ if (directory === dirname(directory))
63
+ break;
64
+ }
65
+ return null;
66
+ }
67
+ async function worktreeMainRepository(candidate) {
68
+ for (let directory = candidate;; directory = dirname(directory)) {
69
+ const marker = join(directory, '.git');
70
+ const details = await statIfPresent(marker);
71
+ if (details) {
72
+ // Stop at the nearest repository; a normal repository or submodule
73
+ // must not inherit an outer worktree's ownership.
74
+ if (!details.isFile())
75
+ return null;
76
+ const gitfile = /^gitdir: (.+)\s*$/.exec((await readFile(marker, 'utf8')).trim());
77
+ if (!gitfile)
78
+ return null;
79
+ const gitdir = await realpath(resolve(directory, gitfile[1].trim()));
80
+ const common = (await readFile(join(gitdir, 'commondir'), 'utf8')).trim();
81
+ if (!common)
82
+ return null;
83
+ const commonDir = await realpath(resolve(gitdir, common));
84
+ const backlink = (await readFile(join(gitdir, 'gitdir'), 'utf8')).trim();
85
+ if (!backlink || await realpath(resolve(gitdir, backlink)) !== await realpath(marker))
86
+ return null;
87
+ // Standard linked worktrees share the main repository's .git directory.
88
+ if (basename(commonDir) !== '.git' || commonDir === gitdir)
89
+ return null;
90
+ const mainRepository = dirname(commonDir);
91
+ return await realpath(join(mainRepository, '.git')) === commonDir ? mainRepository : null;
92
+ }
93
+ if (directory === dirname(directory))
94
+ return null;
95
+ }
96
+ }
97
+ async function statIfPresent(path) {
98
+ try {
99
+ return await stat(path);
100
+ }
101
+ catch (error) {
102
+ if (['ENOENT', 'ENOTDIR'].includes(error.code ?? ''))
103
+ return null;
104
+ throw error;
105
+ }
106
+ }
107
+ //# sourceMappingURL=workspace-matching.js.map
@@ -178,6 +178,7 @@ function runGit(args, options = {}) {
178
178
  maxBuffer: options.maxBuffer ?? 16 * 1024 * 1024,
179
179
  stdio: ['ignore', 'pipe', 'pipe'],
180
180
  timeout: 120_000,
181
+ killSignal: 'SIGKILL',
181
182
  });
182
183
  if (result.error) {
183
184
  if (result.error.code === 'ETIMEDOUT') {