@aiwg/cli 2026.10.4 → 2026.10.6

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 (220) hide show
  1. package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
  2. package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
  3. package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
  4. package/agentic/code/providers/model-capabilities.v1.json +74 -50
  5. package/bin/aiwg.mjs +3 -1
  6. package/dist/src/artifacts/browser-export.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +8 -1
  8. package/dist/src/artifacts/cli.js +72 -9
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  10. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  11. package/dist/src/artifacts/graph-query.js +18 -8
  12. package/dist/src/artifacts/index-adapters.js +339 -0
  13. package/dist/src/artifacts/index-builder.js +146 -16
  14. package/dist/src/artifacts/index-files.js +15 -4
  15. package/dist/src/artifacts/index-preparation.js +91 -26
  16. package/dist/src/artifacts/index-reader.js +44 -0
  17. package/dist/src/artifacts/index-refresh.js +130 -56
  18. package/dist/src/artifacts/index-status.js +20 -5
  19. package/dist/src/artifacts/index-worker.mjs +9 -7
  20. package/dist/src/artifacts/memory-maintenance.js +503 -0
  21. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  22. package/dist/src/artifacts/query-engine.js +263 -108
  23. package/dist/src/artifacts/relations-builder.js +2 -2
  24. package/dist/src/artifacts/source-graph.js +2 -2
  25. package/dist/src/artifacts/types.js +9 -1
  26. package/dist/src/cli/handlers/decision.js +19 -1
  27. package/dist/src/cli/handlers/guided-setup.js +470 -0
  28. package/dist/src/cli/handlers/help.js +2 -2
  29. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  30. package/dist/src/cli/handlers/index.js +4 -2
  31. package/dist/src/cli/handlers/init.js +41 -6
  32. package/dist/src/cli/handlers/local-executor.js +5 -3
  33. package/dist/src/cli/handlers/mc.js +509 -53
  34. package/dist/src/cli/handlers/models.js +32 -2
  35. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  36. package/dist/src/cli/handlers/rules.js +1 -1
  37. package/dist/src/cli/handlers/script-runner.js +73 -2
  38. package/dist/src/cli/handlers/serve.js +5 -3
  39. package/dist/src/cli/handlers/sessions.js +40 -6
  40. package/dist/src/cli/handlers/setup.js +428 -1
  41. package/dist/src/cli/handlers/skill-lint.js +1 -1
  42. package/dist/src/cli/handlers/subcommands.js +118 -8
  43. package/dist/src/cli/handlers/use.js +279 -93
  44. package/dist/src/cli/handlers/workspace.js +3 -1
  45. package/dist/src/cli/prompt-utils.js +8 -0
  46. package/dist/src/cli/router.js +16 -1
  47. package/dist/src/config/aiwg-config.js +42 -0
  48. package/dist/src/config/cli.js +7 -3
  49. package/dist/src/config/decision-config.js +14 -1
  50. package/dist/src/config/native-sandbox-warnings.js +25 -0
  51. package/dist/src/contributors/validation.js +6 -5
  52. package/dist/src/decision/admission.js +7 -3
  53. package/dist/src/decision/benchmark.js +469 -0
  54. package/dist/src/decision/ledger.js +22 -3
  55. package/dist/src/decision/pricing.js +147 -0
  56. package/dist/src/decision/qualification/gate-evidence.js +18 -15
  57. package/dist/src/decision/qualification/load-staged.js +539 -0
  58. package/dist/src/decision/qualification/load.js +109 -33
  59. package/dist/src/decision/qualification/runner.js +5 -2
  60. package/dist/src/decision/review/service.js +6 -4
  61. package/dist/src/decision/review/validate.js +23 -2
  62. package/dist/src/decision/savings.js +8 -3
  63. package/dist/src/discovery/index.js +492 -0
  64. package/dist/src/discovery/interview.js +233 -0
  65. package/dist/src/discovery/plan-apply.js +571 -0
  66. package/dist/src/discovery/plan-store.js +171 -0
  67. package/dist/src/discovery/plan-types.js +2 -0
  68. package/dist/src/discovery/plan-validation.js +139 -0
  69. package/dist/src/discovery/types.js +2 -0
  70. package/dist/src/extensions/commands/definitions.js +49 -9
  71. package/dist/src/extensions/manifest.js +71 -44
  72. package/dist/src/extensions/project-local-discovery.js +1 -1
  73. package/dist/src/extensions/project-quickref.js +18 -17
  74. package/dist/src/extensions/validation.js +32 -29
  75. package/dist/src/housekeeper/daemon-config.js +148 -0
  76. package/dist/src/housekeeper/ipc.js +84 -0
  77. package/dist/src/housekeeper/lifecycle.js +148 -0
  78. package/dist/src/housekeeper/platform.js +35 -0
  79. package/dist/src/housekeeper/status.js +70 -0
  80. package/dist/src/housekeeper/units.js +114 -0
  81. package/dist/src/jobs/flow.js +22 -21
  82. package/dist/src/lint/rule-activation.js +20 -5
  83. package/dist/src/lint/runner.js +1 -1
  84. package/dist/src/mcp/cli.mjs +5 -6
  85. package/dist/src/mcp/config-file.mjs +6 -2
  86. package/dist/src/mcp/config-layers.mjs +23 -7
  87. package/dist/src/mcp/profiles.js +18 -31
  88. package/dist/src/mcp/profiles.mjs +18 -32
  89. package/dist/src/mcp/registry.js +7 -27
  90. package/dist/src/mcp/registry.mjs +7 -28
  91. package/dist/src/mcp/server.mjs +34 -1
  92. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  93. package/dist/src/mcp/tools/command-run.mjs +3 -2
  94. package/dist/src/mcp/tools/decision.mjs +49 -48
  95. package/dist/src/mcp/tools/discovery.mjs +46 -21
  96. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  97. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  98. package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
  99. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  100. package/dist/src/memory/audit.js +719 -0
  101. package/dist/src/memory/canonical-context.js +392 -13
  102. package/dist/src/memory/canonical-maintenance.js +95 -0
  103. package/dist/src/memory/cli-result.js +22 -0
  104. package/dist/src/memory/cli.js +176 -0
  105. package/dist/src/memory/context-pack.js +57 -6
  106. package/dist/src/memory/cost.js +512 -0
  107. package/dist/src/memory/firewall-classification.d.mts +7 -0
  108. package/dist/src/memory/firewall-classification.mjs +32 -0
  109. package/dist/src/memory/index.js +8 -0
  110. package/dist/src/memory/line-maintenance.js +68 -0
  111. package/dist/src/memory/maintenance-cli.js +286 -0
  112. package/dist/src/memory/maintenance-contract.js +136 -0
  113. package/dist/src/memory/maintenance-index.js +18 -0
  114. package/dist/src/memory/maintenance-owner.js +2 -0
  115. package/dist/src/memory/maintenance-record.js +138 -0
  116. package/dist/src/memory/maintenance.js +254 -0
  117. package/dist/src/memory/provider-memory-adapter.js +154 -0
  118. package/dist/src/memory/recommend-cli.js +83 -0
  119. package/dist/src/memory/recommend.js +269 -0
  120. package/dist/src/memory/scheduled.js +81 -0
  121. package/dist/src/memory/taxonomy.js +44 -0
  122. package/dist/src/mission-protocol/codecs.js +19 -9
  123. package/dist/src/models/config-loader.js +11 -1
  124. package/dist/src/models/model-capabilities.v1.json +74 -50
  125. package/dist/src/models/provider-policy.js +125 -62
  126. package/dist/src/models/resolver.js +45 -8
  127. package/dist/src/models/worker-installation.d.mts +7 -0
  128. package/dist/src/models/worker-installation.mjs +169 -37
  129. package/dist/src/models/wrapper-route.js +1 -1
  130. package/dist/src/output-modes/registry.js +8 -7
  131. package/dist/src/plugin/metadata-validator.js +1 -1
  132. package/dist/src/providers/deploy-version.d.mts +1 -0
  133. package/dist/src/providers/deploy-version.mjs +17 -0
  134. package/dist/src/providers/provider-definitions.js +19 -16
  135. package/dist/src/providers/skill-ownership.d.mts +11 -2
  136. package/dist/src/providers/skill-ownership.mjs +35 -17
  137. package/dist/src/rules/activation.d.mts +11 -2
  138. package/dist/src/rules/activation.mjs +82 -6
  139. package/dist/src/rules/inventory.js +1 -1
  140. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  141. package/dist/src/schema/zod-validation.d.mts +6 -0
  142. package/dist/src/schema/zod-validation.mjs +50 -0
  143. package/dist/src/serve/executor-registry.js +297 -20
  144. package/dist/src/serve/executor-token.d.mts +1 -0
  145. package/dist/src/serve/executor-token.mjs +8 -0
  146. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  147. package/dist/src/serve/worker-invocation.js +114 -15
  148. package/dist/src/sessions/adapters/claude.js +19 -18
  149. package/dist/src/sessions/adapters/codex.js +33 -19
  150. package/dist/src/sessions/adapters/copilot.js +14 -13
  151. package/dist/src/sessions/adapters/cursor.js +19 -18
  152. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  153. package/dist/src/sessions/adapters/factory.js +11 -10
  154. package/dist/src/sessions/adapters/generic.js +32 -23
  155. package/dist/src/sessions/adapters/grokbot.js +35 -35
  156. package/dist/src/sessions/adapters/hermes.js +20 -19
  157. package/dist/src/sessions/adapters/muse.js +50 -49
  158. package/dist/src/sessions/adapters/omp.js +5 -4
  159. package/dist/src/sessions/adapters/openclaw.js +30 -29
  160. package/dist/src/sessions/adapters/opencode.js +26 -26
  161. package/dist/src/sessions/adapters/openhuman.js +24 -23
  162. package/dist/src/sessions/adapters/pi.js +5 -4
  163. package/dist/src/sessions/adapters/windsurf.js +8 -8
  164. package/dist/src/sessions/analytics.js +10 -9
  165. package/dist/src/sessions/batch-contracts.js +1 -1
  166. package/dist/src/sessions/batch-import.js +202 -2
  167. package/dist/src/sessions/candidates.js +7 -6
  168. package/dist/src/sessions/contracts.js +73 -54
  169. package/dist/src/sessions/coverage-preflight.js +26 -1
  170. package/dist/src/sessions/output-registration.js +11 -10
  171. package/dist/src/sessions/policy.js +18 -7
  172. package/dist/src/sessions/refresh.js +2 -3
  173. package/dist/src/sessions/repository.js +4 -2
  174. package/dist/src/sessions/workspace-discovery.js +92 -26
  175. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  176. package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
  177. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  178. package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
  179. package/dist/src/update/checker.d.mts +4 -0
  180. package/dist/src/update/checker.mjs +14 -0
  181. package/dist/src/writing/exemplar-selection.js +6 -6
  182. package/dist/src/writing/voice-evaluation.js +24 -23
  183. package/dist/src/writing/writer-migration.js +14 -13
  184. package/dist/src/writing/writer-profile.js +19 -18
  185. package/dist/src/writing/writing-brief.js +18 -17
  186. package/dist/src/writing/writing-receipt.js +47 -46
  187. package/package.json +5 -2
  188. package/schemas/decision/DecisionReview.schema.json +78 -1
  189. package/schemas/mission-v1.schema.json +3 -0
  190. package/tools/agents/deploy-agents.mjs +67 -39
  191. package/tools/agents/providers/antigravity.mjs +6 -1
  192. package/tools/agents/providers/base.mjs +19 -13
  193. package/tools/agents/providers/claude.mjs +1 -1
  194. package/tools/agents/providers/codex.mjs +50 -56
  195. package/tools/agents/providers/copilot.mjs +8 -2
  196. package/tools/agents/providers/cursor.mjs +1 -1
  197. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  198. package/tools/agents/providers/factory.mjs +7 -1
  199. package/tools/agents/providers/grok-build.mjs +18 -2
  200. package/tools/agents/providers/grokbot.mjs +1 -1
  201. package/tools/agents/providers/hermes.mjs +7 -13
  202. package/tools/agents/providers/muse-hooks.mjs +21 -14
  203. package/tools/agents/providers/muse.mjs +7 -2
  204. package/tools/agents/providers/omp.mjs +28 -4
  205. package/tools/agents/providers/opencode.mjs +14 -5
  206. package/tools/agents/providers/openhuman.mjs +1 -1
  207. package/tools/agents/providers/pi.mjs +1 -1
  208. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  209. package/tools/agents/providers/rule-delivery.mjs +263 -71
  210. package/tools/agents/providers/windsurf.mjs +7 -0
  211. package/tools/cli/workspace-status.mjs +5 -0
  212. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  213. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  214. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  215. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  216. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  217. package/tools/ralph-external/process-monitor.mjs +7 -4
  218. package/tools/ralph-external/session-launcher.mjs +280 -32
  219. package/tools/skills/deploy-skills-codex.mjs +11 -7
  220. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -5,10 +5,11 @@ import { inspectMuseNativeWorkspace } from './adapters/muse.js';
5
5
  import { readOmpSessionHeader } from './adapters/omp.js';
6
6
  import { grokbotConversationIdFromLocator } from './adapters/grokbot-locator.js';
7
7
  import { resolveOmpPaths } from '../providers/omp-paths.mjs';
8
- 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';
9
10
  import { redactSourceLocator } from './discovery.js';
10
11
  import { createSessionAdapterRegistry } from './adapter-registry.js';
11
- import { coveragePreflight, expectedSessionProviders } from './coverage-preflight.js';
12
+ import { coveragePreflight, providerScopeWarnings, resolveSessionProviderScope } from './coverage-preflight.js';
12
13
  import { fingerprintSourceFile, streamJsonLines } from './readers.js';
13
14
  import { matchSessionWorkspace } from './workspace-matching.js';
14
15
  import { matchImportExclusion, nativeSessionIdFromLocator, resolveImportExclusions } from './import-exclusions.js';
@@ -20,9 +21,26 @@ const MANUAL_EXPORT_PROVIDERS = new Set([
20
21
  export async function discoverWorkspaceHistories(options) {
21
22
  const registry = options.registry ?? createSessionAdapterRegistry();
22
23
  const exclusions = resolveImportExclusions(undefined, options.exclude);
23
- const expectedProviders = expectedSessionProviders(options.expectedProviders ?? SESSION_PROVIDER_IDS);
24
24
  const supportedProviders = new Set(registry.list().map((adapter) => adapter.provider));
25
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;
26
44
  const workspaceId = workspacePath;
27
45
  const workspacePaths = [...new Set([workspacePath, resolve(options.workspace),
28
46
  ...(options.workspaceAliases ?? []).map((alias) => resolve(alias))])];
@@ -52,12 +70,19 @@ export async function discoverWorkspaceHistories(options) {
52
70
  ? [resolve(options.grokbotRoot)] : [] },
53
71
  {
54
72
  provider: 'claude',
55
- roots: providerHomes.flatMap((providerHome) => workspacePaths.map((workspace) => join(providerHome, '.claude', 'projects', claudeProjectKey(workspace)))),
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),
56
77
  },
57
78
  {
58
79
  provider: 'codex',
59
- roots: options.authorizedRoots || !expectedProviders.includes('codex')
60
- ? [] : await codexDiscoveryRoots(options),
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
+ ] }),
61
86
  },
62
87
  {
63
88
  provider: 'cursor',
@@ -85,11 +110,22 @@ export async function discoverWorkspaceHistories(options) {
85
110
  : providerHomes.map((providerHome) => join(providerHome, '.pi', 'agent', 'sessions'))).flatMap((sessionsRoot) => workspacePaths.map((workspace) => join(sessionsRoot, `--${workspaceKey(workspace, false)}--`))),
86
111
  },
87
112
  ];
88
- const authorizedRoots = { ...options.authorizedRoots };
113
+ const authorizedRoots = { ...savedRoots };
89
114
  const reports = new Map();
90
115
  const candidates = [];
91
116
  const seen = new Set();
92
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
+ }
93
129
  if (!expectedProviders.includes(entry.provider) || !supportedProviders.has(entry.provider))
94
130
  continue;
95
131
  const adapter = registry.get(entry.provider);
@@ -97,10 +133,10 @@ export async function discoverWorkspaceHistories(options) {
97
133
  continue;
98
134
  const availableRoots = [];
99
135
  const rootCounts = new Map();
100
- for (const root of options.authorizedRoots ? options.authorizedRoots[entry.provider] ?? [] : entry.roots) {
136
+ for (const root of savedRoots ? savedRoots[entry.provider] ?? [] : entry.roots) {
101
137
  if (await pathExists(root)) {
102
138
  const canonical = await canonicalPath(root);
103
- if (options.authorizedRoots && canonical !== root) {
139
+ if (savedRoots && canonical !== root) {
104
140
  throw new SessionContractError('INVALID_ARGUMENT', 'previously authorized provider root changed; rediscover with explicit authorization');
105
141
  }
106
142
  if (!availableRoots.includes(canonical)) {
@@ -198,9 +234,12 @@ export async function discoverWorkspaceHistories(options) {
198
234
  const authorizedRoot = scope.allowedRoots.find((root) => locator === root || locator.startsWith(`${root}/`));
199
235
  if (!authorizedRoot)
200
236
  continue;
201
- if (entry.provider === 'claude'
202
- && !await claudeSourceMatchesWorkspace(locator, workspacePath))
203
- 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
+ }
204
243
  const fingerprint = await fingerprintSourceFile({
205
244
  selectedPath: locator,
206
245
  allowedRoots: [authorizedRoot],
@@ -274,7 +313,7 @@ export async function discoverWorkspaceHistories(options) {
274
313
  const sources = candidates.sort(compareSources);
275
314
  const providers = [...reports.values()].sort((a, b) => a.provider.localeCompare(b.provider));
276
315
  const preflight = coveragePreflight({
277
- expectedProviders, expectedProviderBasis: options.expectedProviders ? 'explicit' : 'declared-default',
316
+ ...providerScope,
278
317
  registry, runtime: options.runtime, reports: providers,
279
318
  });
280
319
  const identity = {
@@ -427,13 +466,12 @@ async function rediscoverManifestIdentity(manifest) {
427
466
  }
428
467
  async function codexDiscoveryRoots(options) {
429
468
  const explicit = [...(options.codexRoots ?? []), ...(options.codexRoot ? [options.codexRoot] : [])];
430
- if (explicit.length > 0)
431
- return [...new Set(explicit.map((root) => resolve(root)))];
469
+ const roots = [...new Set(explicit.map((root) => resolve(root)))];
432
470
  // Shared stores require the same explicit provider-home authorization as the default root.
433
471
  if (!options.providerHome)
434
- return [];
472
+ return roots;
435
473
  const codexHome = join(resolve(options.providerHome), '.codex');
436
- const roots = [join(codexHome, 'sessions')];
474
+ roots.push(join(codexHome, 'sessions'));
437
475
  if (process.env.CODEX_HOME)
438
476
  roots.push(resolve(process.env.CODEX_HOME, 'sessions'));
439
477
  try {
@@ -512,7 +550,40 @@ function asObject(value) {
512
550
  async function canonicalPath(path) {
513
551
  return realpath(resolve(path));
514
552
  }
515
- async function claudeSourceMatchesWorkspace(locator, workspacePath) {
553
+ /** Claude keys are lossy: use transcript cwd, never decode hyphens into paths. */
554
+ async function claudeDiscoveryRoots(homes, paths, workspace, registry, exclusions) {
555
+ const roots = homes.flatMap(home => paths.map(path => join(home, '.claude', 'projects', claudeProjectKey(path))));
556
+ const adapter = registry.get('claude');
557
+ for (const home of homes) {
558
+ const projects = join(home, '.claude', 'projects');
559
+ let entries;
560
+ try {
561
+ entries = await readdir(projects, { withFileTypes: true });
562
+ }
563
+ catch (error) {
564
+ if (['ENOENT', 'ENOTDIR'].includes(error.code ?? ''))
565
+ continue;
566
+ throw error;
567
+ }
568
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
569
+ if (!entry.isDirectory())
570
+ continue;
571
+ const root = join(projects, entry.name);
572
+ if (roots.includes(root))
573
+ continue;
574
+ for await (const descriptor of adapter.discover({ workspaceId: workspace, allowedRoots: [root] })) {
575
+ if (matchImportExclusion(descriptor, exclusions))
576
+ continue;
577
+ if (await claudeSourceWorkspaceMatch(descriptor.locator, workspace, false)) {
578
+ roots.push(root);
579
+ break;
580
+ }
581
+ }
582
+ }
583
+ }
584
+ return [...new Set(roots)];
585
+ }
586
+ async function claudeSourceWorkspaceMatch(locator, workspacePath, exactProject) {
516
587
  let count = 0;
517
588
  for await (const { line } of streamJsonLines(locator, { end: 1024 * 1024 - 1 })) {
518
589
  if (++count > 50)
@@ -527,15 +598,10 @@ async function claudeSourceMatchesWorkspace(locator, workspacePath) {
527
598
  const cwd = asObject(value).cwd;
528
599
  if (typeof cwd !== 'string' || cwd.length === 0)
529
600
  continue;
530
- try {
531
- return await canonicalPath(cwd) === workspacePath;
532
- }
533
- catch {
534
- return resolve(cwd) === workspacePath;
535
- }
601
+ return matchSessionWorkspace(cwd, workspacePath);
536
602
  }
537
- // Older transcripts without cwd still use the selected project folder.
538
- return true;
603
+ // Only exact project keys can establish ownership without cwd.
604
+ return exactProject ? 'exact' : null;
539
605
  }
540
606
  export function claudeProjectKey(path) {
541
607
  // Claude Code 2.1.302 replaces each character, then caps keys at 200
@@ -18,6 +18,7 @@ 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
20
  import { buildClaudeHookBlock, CLAUDE_HOOK_START, CLAUDE_HOOK_END } from './claude-hook.js';
21
+ import { dedupeProviderBootstrap } from './workspace-context.js';
21
22
  import { dominantLineEnding, withLineEnding } from './line-endings.js';
22
23
  import { buildParallelismSection, replaceOrAppendParallelismBlock } from './parallelism-section.js';
23
24
  import { buildContextFinalizationBlock, replaceOrAppendFinalizationBlock } from './finalization.js';
@@ -60,6 +61,7 @@ export async function buildAiwgMdContent(projectPath, stagedClaudeMdContent) {
60
61
  // Regenerate may mirror CLAUDE.md before its hook updater runs. Normalize
61
62
  // that managed block here too so legacy duplicate policy never reaches
62
63
  // AIWG.md, even on the first refresh. Operator content stays untouched.
64
+ claudeMdContent = dedupeProviderBootstrap(claudeMdContent);
63
65
  const start = claudeMdContent.indexOf(CLAUDE_HOOK_START);
64
66
  const end = claudeMdContent.indexOf(CLAUDE_HOOK_END, start);
65
67
  if (start >= 0 && end >= start) {
@@ -72,7 +74,22 @@ export async function buildAiwgMdContent(projectPath, stagedClaudeMdContent) {
72
74
  // context, so the directive becomes a self-reference when copied through.
73
75
  // Match the directive when it appears on its own line (possibly indented).
74
76
  const SELF_INCLUDE_PATTERN = /^\s*@AIWG\.md\s*$/;
75
- const filtered = lines.filter((line) => !SELF_INCLUDE_PATTERN.test(line));
77
+ // #3451: CLAUDE.md already `@`-imports WORKSPACE.md; a second `@` path to
78
+ // the same file would load it twice, so AIWG.md refers to it in plain text.
79
+ const CONFIG_IMPORT_PATTERN = /^\s*@\S*aiwg\.config\s*$/;
80
+ const WORKSPACE_IMPORT_PATTERN = /^\s*@(?:\.\.\/)?WORKSPACE\.md\s*$/;
81
+ const filtered = lines
82
+ .filter((line) => !SELF_INCLUDE_PATTERN.test(line))
83
+ .map((line) => {
84
+ if (WORKSPACE_IMPORT_PATTERN.test(line)) {
85
+ return 'WORKSPACE.md is the canonical project context (loaded by CLAUDE.md; read it first if your harness did not).';
86
+ }
87
+ // Legacy CLAUDE.md files may still `@`-import the raw JSON config.
88
+ if (CONFIG_IMPORT_PATTERN.test(line)) {
89
+ return 'Project configuration: `.aiwg/aiwg.config` (not auto-loaded; run `aiwg config show --project` to view it).';
90
+ }
91
+ return line;
92
+ });
76
93
  const firstLine = filtered[0] ?? '';
77
94
  const rest = filtered.slice(1);
78
95
  // If the file already carries the signature elsewhere, don't double-insert.
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import * as fs from 'fs/promises';
23
23
  import * as path from 'path';
24
- import { buildProviderBootstrapBlock, PROVIDER_BOOTSTRAP_START, PROVIDER_BOOTSTRAP_END, } from './workspace-context.js';
24
+ import { buildProviderBootstrapBlock, PROVIDER_BOOTSTRAP_START, PROVIDER_BOOTSTRAP_END, dedupeProviderBootstrap, } from './workspace-context.js';
25
25
  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 -->';
@@ -31,7 +31,9 @@ export function buildClaudeHookBlock(_policy = {}) {
31
31
  CLAUDE_HOOK_START,
32
32
  '',
33
33
  buildProviderBootstrapBlock('claude'),
34
- '@.aiwg/aiwg.config',
34
+ // Plain-text pointer, deliberately not an `@` import: the raw JSON config
35
+ // is several KB and would be pulled into every system prompt (#3451).
36
+ 'Project configuration: `.aiwg/aiwg.config` (not auto-loaded; run `aiwg config show --project` to view it).',
35
37
  '',
36
38
  'Artifact destination authority: `aiwg show rule artifact-output-policy` (kernel).',
37
39
  '',
@@ -45,6 +47,30 @@ export function buildClaudeHookBlock(_policy = {}) {
45
47
  CLAUDE_HOOK_END,
46
48
  ].join('\n');
47
49
  }
50
+ /** Keep the first managed hook block and drop any later complete copies. */
51
+ function collapseExtraHookBlocks(content) {
52
+ const first = content.indexOf(CLAUDE_HOOK_START);
53
+ if (first === -1)
54
+ return content;
55
+ const firstEnd = content.indexOf(CLAUDE_HOOK_END, first);
56
+ if (firstEnd === -1)
57
+ return content;
58
+ const blockEnd = firstEnd + CLAUDE_HOOK_END.length;
59
+ let tail = content.slice(blockEnd);
60
+ const before = tail;
61
+ for (;;) {
62
+ const s = tail.indexOf(CLAUDE_HOOK_START);
63
+ if (s === -1)
64
+ break;
65
+ const e = tail.indexOf(CLAUDE_HOOK_END, s);
66
+ if (e === -1)
67
+ break;
68
+ tail = tail.slice(0, s) + tail.slice(e + CLAUDE_HOOK_END.length);
69
+ }
70
+ if (tail === before)
71
+ return content;
72
+ return content.slice(0, blockEnd) + tail.replace(/(?:\r?\n){3,}/g, '\n\n');
73
+ }
48
74
  /**
49
75
  * Ensure CLAUDE.md at projectPath contains the AIWG-managed marker block
50
76
  * pointing at AIWG.md. Returns a result describing what was done.
@@ -72,6 +98,11 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
72
98
  }
73
99
  throw err;
74
100
  }
101
+ // Repair duplicates left by earlier generations before locating markers, so
102
+ // regeneration converges on exactly one bootstrap (#3451).
103
+ const original = existing;
104
+ existing = collapseExtraHookBlocks(dedupeProviderBootstrap(existing));
105
+ const repaired = existing !== original;
75
106
  const lineEnding = dominantLineEnding(existing);
76
107
  block = withLineEnding(block, lineEnding);
77
108
  const startIdx = existing.indexOf(CLAUDE_HOOK_START);
@@ -90,7 +121,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
90
121
  const updated = existing.substring(0, bootstrapStartIdx)
91
122
  + block
92
123
  + existing.substring(bootstrapEndIdx);
93
- await fs.writeFile(claudeMdPath, updated, 'utf8');
124
+ await fs.writeFile(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
94
125
  result.action = 'updated';
95
126
  return result;
96
127
  }
@@ -101,7 +132,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
101
132
  // Ensure the file ends with a single newline before appending.
102
133
  const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
103
134
  const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
104
- await fs.writeFile(claudeMdPath, updated, 'utf8');
135
+ await fs.writeFile(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
105
136
  result.action = 'inserted';
106
137
  return result;
107
138
  }
@@ -119,7 +150,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
119
150
  }
120
151
  const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
121
152
  const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
122
- await fs.writeFile(claudeMdPath, updated, 'utf8');
153
+ await fs.writeFile(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
123
154
  result.action = 'inserted';
124
155
  return result;
125
156
  }
@@ -127,6 +158,11 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
127
158
  const blockEndOffset = endIdx + CLAUDE_HOOK_END.length;
128
159
  const currentBlock = existing.substring(startIdx, blockEndOffset);
129
160
  if (currentBlock === block) {
161
+ if (repaired) {
162
+ await fs.writeFile(claudeMdPath, existing, 'utf8');
163
+ result.action = 'updated';
164
+ return result;
165
+ }
130
166
  result.action = 'unchanged';
131
167
  return result;
132
168
  }
@@ -142,7 +178,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
142
178
  result.warnings.push(`CLAUDE.md AIWG hook block has been modified inside the markers; updating to canonical content. Operator content outside the block is preserved.`);
143
179
  }
144
180
  const updated = existing.substring(0, startIdx) + block + existing.substring(blockEndOffset);
145
- await fs.writeFile(claudeMdPath, updated, 'utf8');
181
+ await fs.writeFile(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
146
182
  result.action = 'updated';
147
183
  return result;
148
184
  }
@@ -13,12 +13,18 @@
13
13
  *
14
14
  * @implements #1362
15
15
  */
16
- import { dirname } from 'node:path';
16
+ import { readFile } from 'node:fs/promises';
17
+ import { homedir } from 'node:os';
18
+ import { dirname, join } from 'node:path';
17
19
  import { fileURLToPath } from 'node:url';
18
20
  import { findPackageRoot } from '../../cli/find-package-root.js';
19
21
  import { readAiwgConfigCandidate, validateWorkerConfigCandidate, resolveParallelism, PROVIDER_PARALLELISM_DEFAULTS } from '../../config/aiwg-config.js';
22
+ import { ConfigLoader } from '../../models/config-loader.js';
23
+ import { validateWorkerCatalogDiscovery } from '../../models/provider-policy.js';
20
24
  export const PARALLELISM_BLOCK_START = '<!-- AIWG-PARALLELISM-CAP:START -->';
21
25
  export const PARALLELISM_BLOCK_END = '<!-- AIWG-PARALLELISM-CAP:END -->';
26
+ /** Plain-path pointer (not an `@` import) to the long health/supervision explanation. */
27
+ export const WORKER_HEALTH_DOC_POINTER = '`docs/getting-started/worker-dispatch.md` ("Worker health windows and supervision")';
22
28
  /**
23
29
  * One-line note rendered when the bundled model defaults cannot be loaded.
24
30
  * The orchestration health block is omitted; the rest of the section renders
@@ -109,6 +115,58 @@ function groupSummaries(entries, parallelism) {
109
115
  return parts.join('; ') + ')';
110
116
  });
111
117
  }
118
+ /** Same cache used by model discovery; absence/corruption cannot invent evidence. */
119
+ async function readCachedCatalog() {
120
+ try {
121
+ return JSON.parse(await readFile(join(homedir(), '.cache/aiwg/model-catalog.v1.json'), 'utf8'));
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
127
+ async function observationSummaries(entries, deps) {
128
+ const candidate = await (deps.readCatalog ?? readCachedCatalog)();
129
+ const catalog = candidate?.version && candidate.providers && !validateWorkerCatalogDiscovery(candidate.discovery).length
130
+ ? candidate : null;
131
+ const readAt = (deps.now ?? (() => new Date()))();
132
+ const stamp = (value) => {
133
+ const time = value === undefined ? NaN : Date.parse(value);
134
+ if (!Number.isFinite(time))
135
+ return 'unknown';
136
+ const age = readAt.getTime() - time;
137
+ return `${new Date(time).toISOString()} (${age < 0 ? 'future timestamp; age unavailable' : `${Math.floor(age / 1000)}s old`})`;
138
+ };
139
+ const range = (values) => {
140
+ if (!values.length)
141
+ return 'unknown';
142
+ values.sort((a, b) => Date.parse(a) - Date.parse(b));
143
+ const oldest = stamp(values[0]);
144
+ return Date.parse(values[0]) === Date.parse(values[values.length - 1]) ? oldest : `${oldest} to ${stamp(values[values.length - 1])}`;
145
+ };
146
+ return [
147
+ `Cached model observations read at ${readAt.toISOString()}; catalog fetchedAt: ${stamp(catalog?.discovery?.fetchedAt)}. Ages are measured at this read and grow afterward. This cache may differ from the resolver's current installation observations; no eligibility read is performed by context generation. Missing or ambiguous observations have unknown age.`,
148
+ '',
149
+ ...entries.map(([id, group]) => {
150
+ const observations = group.members.flatMap(member => {
151
+ // Mirror the resolver's exact, unambiguous id/API-id lookup per installation.
152
+ const rows = catalog?.discovery?.instances?.[member.cli]?.models
153
+ .filter(row => row.id === member.model || row.apiModelId === member.model) ?? [];
154
+ return rows.length === 1 && rows[0].worker ? [rows[0].worker] : [];
155
+ });
156
+ return `- ${oneLine(id, 64)} cached observations: ${observations.length}/${group.members.length} members; observedAt: ${range(observations.map(row => row.observedAt))}; verifiedAt: ${range(observations.map(row => row.verifiedAt))}.`;
157
+ }),
158
+ '',
159
+ ];
160
+ }
161
+ function capacitySummary(parallelism) {
162
+ const caps = parallelism.admission;
163
+ const summaries = ['groupCaps', 'memberCaps', 'cliCaps', 'modelCaps', 'quotaCaps'].map(field => {
164
+ const entries = Object.entries(caps?.[field] ?? {});
165
+ const shown = entries.slice(0, MAX_SHOWN_GROUPS).map(([key, value]) => `${oneLine(key, 96)}=${value}`).join(', ');
166
+ return `${field}: ${shown || 'none configured'}${entries.length > MAX_SHOWN_GROUPS ? ` (+${entries.length - MAX_SHOWN_GROUPS} more)` : ''}`;
167
+ });
168
+ return `Configured concurrency ceilings (\`parallelism.admission\`): ${summaries.join('; ')}. Local/self-hosted models obey these same ceilings: CLI caps use installation IDs, model caps use canonical inference-provider/API-model keys, and quota caps use the installation's quotaScope. Unset caps add no extra ceiling; a current/request capacity ceiling of zero blocks selection. The resolver returns the minimum applicable ceiling as \`effectiveMaxParallel\`; Tier 0 conductors must self-count active workers, including workers sharing a local server.`;
169
+ }
112
170
  /**
113
171
  * One configured health window with its origin. The origin mirrors
114
172
  * `resolveWorkerEffectiveHealth()` (`src/serve/worker-invocation.ts`): a
@@ -268,15 +326,24 @@ export async function buildParallelismSection(projectPath, deps = {}) {
268
326
  lines.push('');
269
327
  lines.push('A group above shown as configured, with excluded members or an uncovered required role listed, is a static config read — not a live eligibility verdict. Eligibility is resolved per (group, member, effort) tuple against the current catalog and request; see `aiwg models resolve` below to check a specific role/task/group.');
270
328
  lines.push('');
271
- lines.push('Tier 0 direct handoff is the default: use your own shell or a qualified MCP worker tool. No Mission/MC session, scheduler, daemon, or activation marker is needed. Self-count active native + CLI + MCP workers, take the minimum of the project, group, CLI, model, and context caps, and launch each writing task once.');
329
+ lines.push(...await observationSummaries(entries.slice(0, MAX_SHOWN_GROUPS), deps));
330
+ const models = await new ConfigLoader(projectPath, { aiwgHome: resolveAiwgHome() }).load(cfg.parallelism);
331
+ const freshness = models.defaults.workerSelection.freshness;
332
+ lines.push(`Resolver freshness limits from merged model policy: observation warning after ${freshness.warnHours} hours; observation blocked after ${freshness.blockHours} hours; verification blocked after ${freshness.policyDays} days. Future-dated evidence is blocked. Re-resolve before launch; cached ages do not establish eligibility.`);
333
+ lines.push('');
334
+ lines.push('Cross-family review: when required, the reviewer must use a different observed model family from the writer. Different CLI names do not prove different families. The resolver requires unambiguous writer lineage in `reviewOf` with `requireDifferentFamily`; missing, conflicting, unknown, or same-family lineage blocks required review. The direct CLI does not accept review-lineage flags. Family diversity does not establish independent errors; same-vendor or cross-vendor agreement is not proof. Consequential claims require an executable check or human acceptance. Record observed evaluator model/vendor/family, criteria, disagreements and re-review links. Calibration guide: `$AIWG_ROOT/docs/decision/evaluator-calibration.md`.');
335
+ lines.push('');
336
+ lines.push(capacitySummary(cfg.parallelism));
337
+ lines.push('');
338
+ lines.push('Tier 0 direct handoff is the default: use your own shell or a qualified MCP worker tool. Run the rendered command from your own shell outside your own harness sandbox (Claude Code `sandbox.excludedCommands` or an unsandboxed Bash call). The worker CLI native sandbox, per-task worktree and resource scope provide isolation; never nest sandboxes: a CLI sandbox inside a conductor sandbox fails; do not route Tier 0 workers through `aiwg ralph-external --launch-worker`, whose systemd-unit isolation and namespace probe apply only to that launcher and managed launches. No Mission/MC session, scheduler, daemon, or activation marker is needed. Self-count active native + CLI + MCP workers, take the minimum of the project, group, CLI, model, and context caps, and launch each writing task once.');
272
339
  lines.push('');
273
340
  lines.push('Before resolving, qualify the installed CLIs and configured model roles with `aiwg doctor --probe-workers --qualify` (`--yes` for headless confirmation).');
274
341
  lines.push('Agents pass `--authorize-premium` to `aiwg models resolve` only after the human explicitly confirmed the premium escalation (escalation-discipline rule); authorization applies to that resolution only.');
275
342
  lines.push('Resolve each bounded brief with `aiwg models resolve --role <role> --task <class> --group <group> --brief <file> --workdir <tree> --final-output <file> --transport shell --format shell`. Copy the exact rendered command or the MCP server/tool/arguments byte-for-byte; never assemble vendor flags from memory. Re-resolve after any brief, profile, model, or workspace change. `--format shell` prints only the launch command: to see the resolved health windows for an attempt, use `--format json` (the default) and read `invocation.health` with its per-field `origins`.');
276
343
  lines.push('');
277
- lines.push(`Configured health windows (each followed by its origin — \`default\` or \`project config\`): check every ${check.value}s (${check.origin}); confirm startup within ${startup.value}s (${startup.origin}); quiet limit ${quiet.value}s (${quiet.origin}); stall limit ${stall.value}s (${stall.origin}); hard stop ${hard.value}s (${hard.origin}). Per-attempt windows scale quiet and stall by the single largest applicable role/effort/model factor, never their product (for example, reasoning at \`xhigh\` effort resolves 180s/600s from the 90s/300s defaults; an effective 1.25 scale gives 112.5s/375s). Only the hard deadline and its diagnostic/termination grace periods are capped by the remaining wall-clock and transport ceilings; startup, quiet, and stall keep their configured or scaled values, and \`truncatedWindows\` instead records when one of them would exceed that capped deadline (\`resolveWorkerEffectiveHealth()\` in \`src/serve/worker-invocation.ts\`). Resolved values win over these configured values.`);
278
- lines.push('');
279
- lines.push('Supervise by observer state, never by suspicion. `quiet` (no observed activity for the quiet window, reported with confidence `suspected`) is not a stop: read the status (`aiwg ralph-external --status --workers --runs-root <dir> --json` is read-only and never kills or retries), check owned-worktree progress, and keep waiting — the observer’s `shouldStop()` stays false. `stalled` (no progress for the stall window) opens diagnosis through the diagnostic grace period; only `hung` (still no progress after grace, with the process live and connected) authorizes a no-progress stop — and only a worker this conductor owns. The launcher also stops independently of progress state on `failed-to-start`, the hard wall-clock deadline (`HARD_DEADLINE`), the step-count limit (`STEP_LIMIT`), and a fatal observer-record failure (`tools/ralph-external/session-launcher.mjs`). Confirm the stop (a sent signal, lease expiry, or final-file presence is not proof — only a confirmed exit is), preserve partial work for independent verification, then re-resolve within remaining attempts and budgets. Observer loss (`OBSERVER_GAP`, `DISCONNECTED`, degraded or unobserved coverage, opaque MCP output) is unknown, never worker failure: keep polling owned run files and processes from the shell at the same cadence. Heartbeats and log bytes are not progress. On correlated failure across workers sharing one account, quota, or host, pause further launches there and report cause and scope.');
344
+ // #3451: the long window-scaling and supervision explanation lives in the
345
+ // worker-dispatch doc (and the subagent-scoping rule); keep a summary here.
346
+ lines.push(`Worker health windows (origin in parentheses): check every ${check.value}s (${check.origin}); startup ${startup.value}s (${startup.origin}); quiet ${quiet.value}s (${quiet.origin}); stall ${stall.value}s (${stall.origin}); hard stop ${hard.value}s (${hard.origin}). Resolved values win. Supervise by observer state: only \`hung\` authorizes a no-progress stop, and only of a worker you own; read status with \`aiwg ralph-external --status --workers --runs-root <dir> --json\` (read-only). Details: ${WORKER_HEALTH_DOC_POINTER} and \`aiwg show rule subagent-scoping\`.`);
280
347
  lines.push('');
281
348
  }
282
349
  lines.push(PARALLELISM_BLOCK_END);