@aiwg/cli 2026.10.3 → 2026.10.4

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 +57 -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 +285 -131
  21. package/dist/src/artifacts/index-files.js +132 -18
  22. package/dist/src/artifacts/index-preparation.js +65 -0
  23. package/dist/src/artifacts/index-refresh.js +576 -0
  24. package/dist/src/artifacts/index-worker.mjs +9 -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 +98 -37
  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 +82 -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 +51 -11
  207. package/dist/src/sessions/contracts.js +40 -2
  208. package/dist/src/sessions/coverage-preflight.js +1 -1
  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 +133 -25
  216. package/dist/src/sessions/readers.js +106 -53
  217. package/dist/src/sessions/refresh.js +106 -0
  218. package/dist/src/sessions/repository.js +147 -52
  219. package/dist/src/sessions/workspace-discovery.js +221 -56
  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,8 +1,6 @@
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';
@@ -11,7 +9,9 @@ import { SESSION_PROVIDER_IDS, SessionCoveragePreflightSchema, SessionContractEr
11
9
  import { redactSourceLocator } from './discovery.js';
12
10
  import { createSessionAdapterRegistry } from './adapter-registry.js';
13
11
  import { coveragePreflight, expectedSessionProviders } from './coverage-preflight.js';
14
- import { fingerprintSourceFile } from './readers.js';
12
+ import { fingerprintSourceFile, streamJsonLines } from './readers.js';
13
+ import { matchSessionWorkspace } from './workspace-matching.js';
14
+ import { matchImportExclusion, nativeSessionIdFromLocator, resolveImportExclusions } from './import-exclusions.js';
15
15
  export const DISCOVERY_MANIFEST_VERSION = '1.0.0';
16
16
  const MANUAL_EXPORT_PROVIDERS = new Set([
17
17
  'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman', 'grok-build',
@@ -19,13 +19,19 @@ const MANUAL_EXPORT_PROVIDERS = new Set([
19
19
  ]);
20
20
  export async function discoverWorkspaceHistories(options) {
21
21
  const registry = options.registry ?? createSessionAdapterRegistry();
22
+ const exclusions = resolveImportExclusions(undefined, options.exclude);
22
23
  const expectedProviders = expectedSessionProviders(options.expectedProviders ?? SESSION_PROVIDER_IDS);
23
24
  const supportedProviders = new Set(registry.list().map((adapter) => adapter.provider));
24
25
  const workspacePath = await canonicalPath(options.workspace);
25
26
  const workspaceId = workspacePath;
27
+ const workspacePaths = [...new Set([workspacePath, resolve(options.workspace),
28
+ ...(options.workspaceAliases ?? []).map((alias) => resolve(alias))])];
29
+ for (const alias of workspacePaths) {
30
+ if (await canonicalPath(alias) !== workspacePath) {
31
+ throw new SessionContractError('INVALID_ARGUMENT', 'workspace alias resolves to a different workspace');
32
+ }
33
+ }
26
34
  const providerHomes = providerHomeCandidates(options.providerHome, options.operatorHome);
27
- const keyWithLeadingDash = workspaceKey(workspacePath, true);
28
- const keyWithoutLeadingDash = workspaceKey(workspacePath, false);
29
35
  const discoverable = [
30
36
  { provider: 'deepseek-harness', roots: options.dshRoot
31
37
  ? [resolve(options.dshRoot)] : options.providerHome
@@ -46,26 +52,23 @@ export async function discoverWorkspaceHistories(options) {
46
52
  ? [resolve(options.grokbotRoot)] : [] },
47
53
  {
48
54
  provider: 'claude',
49
- roots: providerHomes.map((providerHome) => join(providerHome, '.claude', 'projects', keyWithLeadingDash)),
55
+ roots: providerHomes.flatMap((providerHome) => workspacePaths.map((workspace) => join(providerHome, '.claude', 'projects', claudeProjectKey(workspace)))),
50
56
  },
51
57
  {
52
58
  provider: 'codex',
53
- roots: options.codexRoot
54
- ? [resolve(options.codexRoot)]
55
- : options.providerHome
56
- ? [join(resolve(options.providerHome), '.codex', 'sessions')]
57
- : [],
59
+ roots: options.authorizedRoots || !expectedProviders.includes('codex')
60
+ ? [] : await codexDiscoveryRoots(options),
58
61
  },
59
62
  {
60
63
  provider: 'cursor',
61
- roots: providerHomes.map((providerHome) => join(providerHome, '.cursor', 'projects', keyWithoutLeadingDash, 'agent-transcripts')),
64
+ roots: providerHomes.flatMap((providerHome) => workspacePaths.map((workspace) => join(providerHome, '.cursor', 'projects', workspaceKey(workspace, false), 'agent-transcripts'))),
62
65
  },
63
66
  {
64
67
  provider: 'factory',
65
- roots: providerHomes.flatMap((providerHome) => [
66
- join(providerHome, '.factory', 'projects', keyWithLeadingDash),
67
- join(providerHome, '.factory', 'sessions', keyWithLeadingDash),
68
- ]),
68
+ roots: providerHomes.flatMap((providerHome) => workspacePaths.flatMap((workspace) => [
69
+ join(providerHome, '.factory', 'projects', workspaceKey(workspace, true)),
70
+ join(providerHome, '.factory', 'sessions', workspaceKey(workspace, true)),
71
+ ])),
69
72
  },
70
73
  {
71
74
  // Pi stores every working directory's sessions in its own
@@ -79,9 +82,10 @@ export async function discoverWorkspaceHistories(options) {
79
82
  provider: 'pi',
80
83
  roots: (process.env.PI_CODING_AGENT_SESSION_DIR
81
84
  ? [resolve(process.env.PI_CODING_AGENT_SESSION_DIR)]
82
- : providerHomes.map((providerHome) => join(providerHome, '.pi', 'agent', 'sessions'))).map((sessionsRoot) => join(sessionsRoot, `--${keyWithoutLeadingDash}--`)),
85
+ : providerHomes.map((providerHome) => join(providerHome, '.pi', 'agent', 'sessions'))).flatMap((sessionsRoot) => workspacePaths.map((workspace) => join(sessionsRoot, `--${workspaceKey(workspace, false)}--`))),
83
86
  },
84
87
  ];
88
+ const authorizedRoots = { ...options.authorizedRoots };
85
89
  const reports = new Map();
86
90
  const candidates = [];
87
91
  const seen = new Set();
@@ -92,9 +96,27 @@ export async function discoverWorkspaceHistories(options) {
92
96
  if (adapter.disposition === 'unsupported' || !adapter.supportedOperations.includes('discover'))
93
97
  continue;
94
98
  const availableRoots = [];
95
- for (const root of entry.roots) {
96
- if (await pathExists(root))
97
- availableRoots.push(await canonicalPath(root));
99
+ const rootCounts = new Map();
100
+ for (const root of options.authorizedRoots ? options.authorizedRoots[entry.provider] ?? [] : entry.roots) {
101
+ if (await pathExists(root)) {
102
+ const canonical = await canonicalPath(root);
103
+ if (options.authorizedRoots && canonical !== root) {
104
+ throw new SessionContractError('INVALID_ARGUMENT', 'previously authorized provider root changed; rediscover with explicit authorization');
105
+ }
106
+ if (!availableRoots.includes(canonical)) {
107
+ availableRoots.push(canonical);
108
+ rootCounts.set(canonical, {
109
+ root: redactSourceLocator(canonical), rootId: sha256(canonical),
110
+ status: 'checked', sourceCount: 0, duplicateCount: 0,
111
+ });
112
+ }
113
+ }
114
+ else {
115
+ rootCounts.set(root, {
116
+ root: redactSourceLocator(root), rootId: sha256(root),
117
+ status: 'unavailable', sourceCount: 0, duplicateCount: 0,
118
+ });
119
+ }
98
120
  }
99
121
  if (availableRoots.length === 0) {
100
122
  const codexNeedsAuthorization = (entry.provider === 'codex' || entry.provider === 'omp'
@@ -106,15 +128,44 @@ export async function discoverWorkspaceHistories(options) {
106
128
  : 'PROVIDER_ROOT_UNAVAILABLE', codexNeedsAuthorization
107
129
  ? `Pass --${entry.provider === 'deepseek-harness' ? 'dsh' : entry.provider}-root with an explicitly authorized sessions root.`
108
130
  : `No authorized ${entry.provider} workspace history root was found.`));
131
+ if (entry.provider === 'codex')
132
+ reports.get(entry.provider).rootCounts = [...rootCounts.values()];
109
133
  continue;
110
134
  }
135
+ authorizedRoots[entry.provider] = availableRoots;
111
136
  const scope = {
112
137
  workspaceId,
113
138
  allowedRoots: availableRoots,
114
139
  };
115
140
  const providerSources = [];
141
+ let matchedBySubfolder = 0;
142
+ let matchedByWorktree = 0;
116
143
  const museSourceInspections = [];
117
144
  for await (const descriptor of adapter.discover(scope)) {
145
+ const known = options.knownSources?.find(source => source.provider === descriptor.provider && source.locator === descriptor.locator);
146
+ const nativeSessionId = known?.nativeSessionId ?? descriptor.nativeSessionId ?? nativeSessionIdFromLocator(descriptor);
147
+ const exclusion = matchImportExclusion({ ...descriptor, nativeSessionId }, exclusions);
148
+ if (exclusion) {
149
+ const locator = resolve(descriptor.locator);
150
+ const authorizedRoot = scope.allowedRoots.find(root => locator === root || locator.startsWith(`${root}/`));
151
+ if (!authorizedRoot)
152
+ continue;
153
+ const key = `excluded\0${entry.provider}\0${locator}`;
154
+ if (seen.has(key))
155
+ continue;
156
+ seen.add(key);
157
+ const source = {
158
+ sourceId: known?.sourceId ?? sha256(key), provider: entry.provider,
159
+ locator, redactedLocator: redactSourceLocator(locator), locatorClass: descriptor.locatorClass,
160
+ authorizedRoot, modifiedAt: '', sizeBytes: 0, digest: '', nativeSessionId,
161
+ status: 'excluded', exclusion,
162
+ };
163
+ rootCounts.get(authorizedRoot).sourceCount += 1;
164
+ providerSources.push(source);
165
+ candidates.push(source);
166
+ continue;
167
+ }
168
+ let workspaceMatch = null;
118
169
  if (entry.provider === 'muse') {
119
170
  const inspection = await inspectMuseNativeWorkspace({
120
171
  ...descriptor,
@@ -124,31 +175,54 @@ export async function discoverWorkspaceHistories(options) {
124
175
  museSourceInspections.push({
125
176
  sourceKey: sha256(['muse-workspace-inspection-v1', descriptor.locator].join('\0')),
126
177
  redactedLocator: redactSourceLocator(descriptor.locator),
178
+ nativeSessionId: basename(dirname(descriptor.locator)),
127
179
  ...inspection,
128
180
  });
129
181
  if (inspection.outcome !== 'matched')
130
182
  continue;
183
+ workspaceMatch = inspection.matchKind ?? null;
131
184
  }
132
185
  const locator = await canonicalPath(descriptor.locator);
133
- if (entry.provider === 'codex'
134
- && !await codexSourceMatchesWorkspace(locator, workspacePath))
135
- continue;
186
+ if (entry.provider === 'codex') {
187
+ workspaceMatch = await codexSourceWorkspaceMatch(locator, workspacePath);
188
+ if (!workspaceMatch)
189
+ continue;
190
+ }
136
191
  const ompHeader = entry.provider === 'omp' ? await readOmpSessionHeader({ ...descriptor, sourceId: 'discovery', authorizedScope: scope }) : undefined;
137
- if (ompHeader && resolve(ompHeader.cwd) !== workspacePath)
138
- continue;
192
+ if (ompHeader) {
193
+ workspaceMatch = await matchSessionWorkspace(ompHeader.cwd, workspacePath);
194
+ if (!workspaceMatch)
195
+ continue;
196
+ }
139
197
  const details = await stat(locator);
140
198
  const authorizedRoot = scope.allowedRoots.find((root) => locator === root || locator.startsWith(`${root}/`));
141
199
  if (!authorizedRoot)
142
200
  continue;
201
+ if (entry.provider === 'claude'
202
+ && !await claudeSourceMatchesWorkspace(locator, workspacePath))
203
+ continue;
143
204
  const fingerprint = await fingerprintSourceFile({
144
205
  selectedPath: locator,
145
206
  allowedRoots: [authorizedRoot],
146
207
  });
147
- const dedupeKey = `${entry.provider}\0${fingerprint.digest}\0${fingerprint.size}`;
148
- if (seen.has(dedupeKey))
208
+ const rolloutId = entry.provider === 'codex' && descriptor.locatorClass === 'codex-rollout-jsonl'
209
+ ? await codexRolloutId(locator) : undefined;
210
+ const dedupeKey = rolloutId
211
+ ? `codex\0rollout\0${rolloutId}`
212
+ : `${entry.provider}\0${fingerprint.digest}\0${fingerprint.size}`;
213
+ const rootCount = rootCounts.get(authorizedRoot);
214
+ if (seen.has(dedupeKey)) {
215
+ rootCount.duplicateCount += 1;
149
216
  continue;
217
+ }
150
218
  seen.add(dedupeKey);
219
+ if (workspaceMatch === 'subfolder')
220
+ matchedBySubfolder += 1;
221
+ if (workspaceMatch === 'worktree')
222
+ matchedByWorktree += 1;
223
+ rootCount.sourceCount += 1;
151
224
  const source = sourceFromDescriptor(descriptor, locator, authorizedRoot, details, fingerprint.digest);
225
+ source.nativeSessionId = rolloutId ?? ompHeader?.id ?? nativeSessionId;
152
226
  if (ompHeader)
153
227
  source.sourceId = sha256(['omp-native-source-v1', workspaceId, ompHeader.id].join('\0'));
154
228
  if (entry.provider === 'grokbot') {
@@ -168,13 +242,17 @@ export async function discoverWorkspaceHistories(options) {
168
242
  const incompleteMuseInspection = museSourceInspections.some((inspection) => inspection.outcome === 'inconclusive');
169
243
  const report = providerReport(entry.provider, incompleteMuseInspection ? 'not-checked' : 'checked', 'discoverable', providerSources, incompleteMuseInspection ? 'MUSE_SOURCE_INSPECTION_INCOMPLETE'
170
244
  : providerSources.length === 0 ? 'NO_WORKSPACE_SOURCES' : null, incompleteMuseInspection
171
- ? 'Review museSourceInspections; authorize a supported Muse export for unresolved sources before claiming workspace coverage.'
245
+ ? 'Review unattributed museSourceInspections and their limit name/value; authorize a supported Muse export for unresolved sources before claiming workspace coverage.'
172
246
  : providerSources.length === 0
173
247
  ? `The ${entry.provider} root was checked, but no source matched the authorized workspace.`
174
248
  : null);
175
249
  if (entry.provider === 'muse') {
176
250
  report.museSourceInspections = museSourceInspections.sort((a, b) => a.sourceKey.localeCompare(b.sourceKey));
177
251
  }
252
+ report.matchedBySubfolder = matchedBySubfolder;
253
+ report.matchedByWorktree = matchedByWorktree;
254
+ if (entry.provider === 'codex')
255
+ report.rootCounts = [...rootCounts.values()];
178
256
  reports.set(entry.provider, report);
179
257
  }
180
258
  for (const provider of expectedProviders) {
@@ -203,6 +281,7 @@ export async function discoverWorkspaceHistories(options) {
203
281
  schemaVersion: DISCOVERY_MANIFEST_VERSION,
204
282
  workspaceId,
205
283
  preflight,
284
+ authorizedRoots,
206
285
  sources: sources.map((source) => ({
207
286
  sourceId: source.sourceId,
208
287
  provider: source.provider,
@@ -211,11 +290,16 @@ export async function discoverWorkspaceHistories(options) {
211
290
  modifiedAt: source.modifiedAt,
212
291
  sizeBytes: source.sizeBytes,
213
292
  digest: source.digest,
293
+ ...(source.nativeSessionId ? { nativeSessionId: source.nativeSessionId } : {}),
294
+ ...(source.status ? { status: source.status, exclusion: source.exclusion } : {}),
214
295
  })),
215
296
  providers: providers.map((provider) => ({
216
297
  provider: provider.provider,
217
298
  status: provider.status,
218
299
  reasonCode: provider.reasonCode,
300
+ ...(provider.matchedBySubfolder !== undefined ? { matchedBySubfolder: provider.matchedBySubfolder } : {}),
301
+ ...(provider.matchedByWorktree !== undefined ? { matchedByWorktree: provider.matchedByWorktree } : {}),
302
+ ...(provider.rootCounts ? { rootCounts: provider.rootCounts } : {}),
219
303
  ...(provider.museSourceInspections ? { museSourceInspections: provider.museSourceInspections } : {}),
220
304
  })),
221
305
  };
@@ -226,6 +310,7 @@ export async function discoverWorkspaceHistories(options) {
226
310
  workspaceId,
227
311
  workspacePath,
228
312
  preflight,
313
+ authorizedRoots,
229
314
  providers,
230
315
  sources,
231
316
  };
@@ -272,6 +357,9 @@ export function publicDiscoveryManifest(manifest) {
272
357
  totals: {
273
358
  providers: manifest.providers.length,
274
359
  sources: manifest.sources.length,
360
+ excluded: manifest.sources.filter(source => source.status === 'excluded').length,
361
+ matchedBySubfolder: manifest.providers.reduce((sum, provider) => sum + (provider.matchedBySubfolder ?? 0), 0),
362
+ matchedByWorktree: manifest.providers.reduce((sum, provider) => sum + (provider.matchedByWorktree ?? 0), 0),
275
363
  },
276
364
  };
277
365
  }
@@ -291,12 +379,14 @@ function sourceFromDescriptor(descriptor, locator, authorizedRoot, details, dige
291
379
  };
292
380
  }
293
381
  function providerReport(provider, status, disposition, sources, reasonCode, remediation) {
294
- const timestamps = sources.map((source) => source.modifiedAt).sort();
382
+ const timestamps = sources.map((source) => source.modifiedAt).filter(Boolean).sort();
295
383
  return {
296
384
  provider,
297
385
  status,
298
386
  disposition,
299
387
  sourceCount: sources.length,
388
+ matchedBySubfolder: 0,
389
+ matchedByWorktree: 0,
300
390
  dateRange: {
301
391
  earliest: timestamps.at(0) ?? null,
302
392
  latest: timestamps.at(-1) ?? null,
@@ -311,6 +401,7 @@ async function rediscoverManifestIdentity(manifest) {
311
401
  schemaVersion: manifest.schemaVersion,
312
402
  workspaceId: manifest.workspaceId,
313
403
  ...(manifest.preflight ? { preflight: manifest.preflight } : {}),
404
+ ...(manifest.authorizedRoots ? { authorizedRoots: manifest.authorizedRoots } : {}),
314
405
  sources: manifest.sources.map((source) => ({
315
406
  sourceId: source.sourceId,
316
407
  provider: source.provider,
@@ -319,47 +410,84 @@ async function rediscoverManifestIdentity(manifest) {
319
410
  modifiedAt: source.modifiedAt,
320
411
  sizeBytes: source.sizeBytes,
321
412
  digest: source.digest,
413
+ ...(source.nativeSessionId ? { nativeSessionId: source.nativeSessionId } : {}),
414
+ ...(source.status ? { status: source.status, exclusion: source.exclusion } : {}),
322
415
  })),
323
416
  providers: manifest.providers.map((provider) => ({
324
417
  provider: provider.provider,
325
418
  status: provider.status,
326
419
  reasonCode: provider.reasonCode,
420
+ ...(provider.matchedBySubfolder !== undefined ? { matchedBySubfolder: provider.matchedBySubfolder } : {}),
421
+ ...(provider.matchedByWorktree !== undefined ? { matchedByWorktree: provider.matchedByWorktree } : {}),
422
+ ...(provider.rootCounts ? { rootCounts: provider.rootCounts } : {}),
327
423
  ...(provider.museSourceInspections ? { museSourceInspections: provider.museSourceInspections } : {}),
328
424
  })),
329
425
  };
330
426
  return sha256(JSON.stringify(identity));
331
427
  }
332
- async function codexSourceMatchesWorkspace(locator, workspacePath) {
333
- const input = createReadStream(locator, { encoding: 'utf8' });
334
- const lines = createInterface({ input, crlfDelay: Infinity });
335
- let count = 0;
428
+ async function codexDiscoveryRoots(options) {
429
+ const explicit = [...(options.codexRoots ?? []), ...(options.codexRoot ? [options.codexRoot] : [])];
430
+ if (explicit.length > 0)
431
+ return [...new Set(explicit.map((root) => resolve(root)))];
432
+ // Shared stores require the same explicit provider-home authorization as the default root.
433
+ if (!options.providerHome)
434
+ return [];
435
+ const codexHome = join(resolve(options.providerHome), '.codex');
436
+ const roots = [join(codexHome, 'sessions')];
437
+ if (process.env.CODEX_HOME)
438
+ roots.push(resolve(process.env.CODEX_HOME, 'sessions'));
336
439
  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;
440
+ const roles = await readdir(join(codexHome, 'roles-runtime'), { withFileTypes: true });
441
+ for (const role of roles.sort((a, b) => a.name.localeCompare(b.name))) {
442
+ if (role.isDirectory() || role.isSymbolicLink()) {
443
+ roots.push(join(codexHome, 'roles-runtime', role.name, '.codex', 'sessions'));
355
444
  }
356
445
  }
357
- return false;
358
446
  }
359
- finally {
360
- lines.close();
361
- input.destroy();
447
+ catch (error) {
448
+ if (!['ENOENT', 'ENOTDIR'].includes(error.code ?? ''))
449
+ throw error;
362
450
  }
451
+ return [...new Set(roots)];
452
+ }
453
+ async function codexRolloutId(locator) {
454
+ let count = 0;
455
+ for await (const { line } of streamJsonLines(locator)) {
456
+ if (++count > 50)
457
+ break;
458
+ let value;
459
+ try {
460
+ value = JSON.parse(line);
461
+ }
462
+ catch {
463
+ continue;
464
+ }
465
+ const record = asObject(value);
466
+ const id = asObject(record.payload).id;
467
+ if (record.type === 'session_meta' && typeof id === 'string' && id.length > 0)
468
+ return id;
469
+ }
470
+ return /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i
471
+ .exec(basename(locator))?.[1];
472
+ }
473
+ async function codexSourceWorkspaceMatch(locator, workspacePath) {
474
+ let count = 0;
475
+ for await (const { line } of streamJsonLines(locator)) {
476
+ if (++count > 50)
477
+ break;
478
+ let value;
479
+ try {
480
+ value = JSON.parse(line);
481
+ }
482
+ catch {
483
+ continue;
484
+ }
485
+ const cwd = codexWorkspaceField(value);
486
+ if (!cwd)
487
+ continue;
488
+ return await matchSessionWorkspace(cwd, workspacePath);
489
+ }
490
+ return null;
363
491
  }
364
492
  function codexWorkspaceField(value) {
365
493
  const root = asObject(value);
@@ -384,6 +512,43 @@ function asObject(value) {
384
512
  async function canonicalPath(path) {
385
513
  return realpath(resolve(path));
386
514
  }
515
+ async function claudeSourceMatchesWorkspace(locator, workspacePath) {
516
+ let count = 0;
517
+ for await (const { line } of streamJsonLines(locator, { end: 1024 * 1024 - 1 })) {
518
+ if (++count > 50)
519
+ break;
520
+ let value;
521
+ try {
522
+ value = JSON.parse(line);
523
+ }
524
+ catch {
525
+ continue;
526
+ }
527
+ const cwd = asObject(value).cwd;
528
+ if (typeof cwd !== 'string' || cwd.length === 0)
529
+ continue;
530
+ try {
531
+ return await canonicalPath(cwd) === workspacePath;
532
+ }
533
+ catch {
534
+ return resolve(cwd) === workspacePath;
535
+ }
536
+ }
537
+ // Older transcripts without cwd still use the selected project folder.
538
+ return true;
539
+ }
540
+ export function claudeProjectKey(path) {
541
+ // Claude Code 2.1.302 replaces each character, then caps keys at 200
542
+ // characters with a base-36 suffix from the original path's 32-bit hash.
543
+ const key = path.replace(/[^A-Za-z0-9-]/g, '-');
544
+ if (key.length <= 200)
545
+ return key;
546
+ let hash = 0;
547
+ for (let index = 0; index < path.length; index += 1) {
548
+ hash = ((hash << 5) - hash + path.charCodeAt(index)) | 0;
549
+ }
550
+ return `${key.slice(0, 200)}-${Math.abs(hash).toString(36)}`;
551
+ }
387
552
  function workspaceKey(path, retainLeadingSeparator) {
388
553
  const normalized = path.replace(/\\/g, '/');
389
554
  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') {
@@ -17,6 +17,8 @@
17
17
  import { promises as fs } from 'node:fs';
18
18
  import * as path from 'node:path';
19
19
  import { projectControlPath } from '../../config/project-artifacts.js';
20
+ import { buildClaudeHookBlock, CLAUDE_HOOK_START, CLAUDE_HOOK_END } from './claude-hook.js';
21
+ import { dominantLineEnding, withLineEnding } from './line-endings.js';
20
22
  import { buildParallelismSection, replaceOrAppendParallelismBlock } from './parallelism-section.js';
21
23
  import { buildContextFinalizationBlock, replaceOrAppendFinalizationBlock } from './finalization.js';
22
24
  import { buildExternalLinksSection, replaceOrAppendExternalLinksBlock, } from './external-links-section.js';
@@ -55,6 +57,15 @@ export async function buildAiwgMdContent(projectPath, stagedClaudeMdContent) {
55
57
  if (claudeMdContent) {
56
58
  // Insert the AIWG signature comment as the second line.
57
59
  // Top line is typically `# AIWG` or `# Project` heading; preserve it.
60
+ // Regenerate may mirror CLAUDE.md before its hook updater runs. Normalize
61
+ // that managed block here too so legacy duplicate policy never reaches
62
+ // AIWG.md, even on the first refresh. Operator content stays untouched.
63
+ const start = claudeMdContent.indexOf(CLAUDE_HOOK_START);
64
+ const end = claudeMdContent.indexOf(CLAUDE_HOOK_END, start);
65
+ if (start >= 0 && end >= start) {
66
+ const hook = withLineEnding(buildClaudeHookBlock(), dominantLineEnding(claudeMdContent));
67
+ claudeMdContent = claudeMdContent.slice(0, start) + hook + claudeMdContent.slice(end + CLAUDE_HOOK_END.length);
68
+ }
58
69
  const lines = claudeMdContent.split('\n');
59
70
  // #1268: strip `@AIWG.md` self-include directives. CLAUDE.md uses
60
71
  // `@AIWG.md` to pull in the framework context; AIWG.md IS that framework
@@ -26,29 +26,14 @@ import { dominantLineEnding, withLineEnding } from './line-endings.js';
26
26
  import { assertSafeContextFile } from './context-file-safety.js';
27
27
  export const CLAUDE_HOOK_START = '<!-- AIWG:claude-md-hook:start -->';
28
28
  export const CLAUDE_HOOK_END = '<!-- AIWG:claude-md-hook:end -->';
29
- function buildClaudeArtifactOutputPolicy(policy = {}) {
30
- const providerNative = policy.provider_native ?? 'explicit-only';
31
- const design = policy.destinations?.['claude-code.design'];
32
- const designEnabled = design?.enabled !== false && design?.use_when !== 'disabled';
33
- return [
34
- '## AIWG artifact output policy',
35
- '',
36
- `- Canonical durable artifacts: AIWG artifact store (policy: ${policy.canonical ?? 'aiwg'}).`,
37
- `- Provider-native presentation/export: ${providerNative}.`,
38
- `- Claude Design: ${designEnabled ? 'available only when explicitly selected by the resolved policy or requested by the user' : 'disabled by project policy'}.`,
39
- '- Never substitute, relocate, or omit the canonical AIWG plan/review artifact because Claude adds or changes a provider default.',
40
- '- When both canonical and presentation outputs are selected, write the canonical artifact first; treat Design as a derived export and record provenance linking it to the canonical source of truth.',
41
- '- Unknown provider-native destinations fail safe with a diagnostic. Higher-authority project policy overrides user preferences; explicit task selection overrides provider defaults only within the project policy ceiling.',
42
- ];
43
- }
44
- export function buildClaudeHookBlock(policy = {}) {
29
+ export function buildClaudeHookBlock(_policy = {}) {
45
30
  return [
46
31
  CLAUDE_HOOK_START,
47
32
  '',
48
33
  buildProviderBootstrapBlock('claude'),
49
34
  '@.aiwg/aiwg.config',
50
35
  '',
51
- ...buildClaudeArtifactOutputPolicy(policy),
36
+ 'Artifact destination authority: `aiwg show rule artifact-output-policy` (kernel).',
52
37
  '',
53
38
  '<!--',
54
39
  ' This block is managed by `aiwg regenerate` and `aiwg use`.',
@@ -72,15 +57,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
72
57
  action: 'skipped',
73
58
  warnings: [],
74
59
  };
75
- let policy = {};
76
- try {
77
- const config = JSON.parse(await fs.readFile(path.join(projectPath, '.aiwg', 'aiwg.config'), 'utf8'));
78
- policy = config.artifact_outputs ?? {};
79
- }
80
- catch {
81
- // Missing/legacy/temporarily malformed config receives the safe default.
82
- }
83
- let block = buildClaudeHookBlock(policy);
60
+ let block = buildClaudeHookBlock();
84
61
  // Case 1: CLAUDE.md does not exist — create a minimal one with just the block.
85
62
  let existing;
86
63
  try {