@aiwg/cli 2026.10.5 → 2026.10.7

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 (263) 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 +7 -3
  6. package/dist/src/a2a/agent-card.js +11 -0
  7. package/dist/src/api/index.d.ts +1 -0
  8. package/dist/src/api/index.js +1 -0
  9. package/dist/src/artifacts/browser-export.js +1 -0
  10. package/dist/src/artifacts/citation-parser.js +8 -1
  11. package/dist/src/artifacts/cli.js +57 -6
  12. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  13. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  14. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  15. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  16. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  17. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  18. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  19. package/dist/src/artifacts/graph-query.js +18 -8
  20. package/dist/src/artifacts/index-adapters.js +339 -0
  21. package/dist/src/artifacts/index-builder.js +143 -14
  22. package/dist/src/artifacts/index-files.js +15 -4
  23. package/dist/src/artifacts/index-preparation.js +2 -0
  24. package/dist/src/artifacts/index-reader.js +44 -0
  25. package/dist/src/artifacts/index-refresh.js +114 -55
  26. package/dist/src/artifacts/index-status.js +20 -5
  27. package/dist/src/artifacts/memory-maintenance.js +503 -0
  28. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  29. package/dist/src/artifacts/query-engine.js +263 -108
  30. package/dist/src/artifacts/relations-builder.js +2 -2
  31. package/dist/src/artifacts/source-graph.js +2 -2
  32. package/dist/src/artifacts/types.js +9 -1
  33. package/dist/src/auth/client.js +5 -1
  34. package/dist/src/auth/credential-store.js +5 -1
  35. package/dist/src/auth/sensitive-input.js +151 -0
  36. package/dist/src/channel/manager.mjs +3 -0
  37. package/dist/src/cli/handlers/agentcard.js +47 -25
  38. package/dist/src/cli/handlers/auth.js +22 -2
  39. package/dist/src/cli/handlers/decision.js +22 -3
  40. package/dist/src/cli/handlers/effect.js +4 -84
  41. package/dist/src/cli/handlers/guided-setup.js +470 -0
  42. package/dist/src/cli/handlers/help.js +2 -2
  43. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  44. package/dist/src/cli/handlers/index.js +4 -2
  45. package/dist/src/cli/handlers/init.js +41 -6
  46. package/dist/src/cli/handlers/install.js +6 -0
  47. package/dist/src/cli/handlers/local-executor.js +5 -3
  48. package/dist/src/cli/handlers/marketplace.js +15 -4
  49. package/dist/src/cli/handlers/mc.js +509 -53
  50. package/dist/src/cli/handlers/models.js +32 -2
  51. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  52. package/dist/src/cli/handlers/rules.js +1 -1
  53. package/dist/src/cli/handlers/script-runner.js +73 -2
  54. package/dist/src/cli/handlers/serve.js +5 -3
  55. package/dist/src/cli/handlers/sessions.js +1 -0
  56. package/dist/src/cli/handlers/setup.js +432 -1
  57. package/dist/src/cli/handlers/skill-lint.js +1 -1
  58. package/dist/src/cli/handlers/subcommands.js +118 -8
  59. package/dist/src/cli/handlers/use.js +335 -107
  60. package/dist/src/cli/handlers/workspace.js +3 -1
  61. package/dist/src/cli/prompt-utils.js +81 -0
  62. package/dist/src/cli/router.js +16 -1
  63. package/dist/src/config/aiwg-config.js +42 -0
  64. package/dist/src/config/cli.js +7 -3
  65. package/dist/src/config/decision-config.js +14 -1
  66. package/dist/src/config/gitignore.js +4 -0
  67. package/dist/src/config/native-sandbox-warnings.js +25 -0
  68. package/dist/src/config/workspace.js +2 -1
  69. package/dist/src/contributors/validation.js +6 -5
  70. package/dist/src/decision/adapters/jev.js +20 -31
  71. package/dist/src/decision/admission.js +7 -3
  72. package/dist/src/decision/ask.js +6 -1
  73. package/dist/src/decision/benchmark.js +482 -0
  74. package/dist/src/decision/ledger.js +22 -3
  75. package/dist/src/decision/pricing.js +147 -0
  76. package/dist/src/decision/qualification/gate-evidence.js +23 -18
  77. package/dist/src/decision/qualification/load-staged.js +576 -0
  78. package/dist/src/decision/qualification/load.js +109 -33
  79. package/dist/src/decision/qualification/runner.js +12 -5
  80. package/dist/src/decision/review/service.js +6 -4
  81. package/dist/src/decision/review/validate.js +23 -2
  82. package/dist/src/decision/savings.js +8 -3
  83. package/dist/src/discovery/index.js +492 -0
  84. package/dist/src/discovery/interview.js +238 -0
  85. package/dist/src/discovery/plan-apply.js +567 -0
  86. package/dist/src/discovery/plan-store.js +171 -0
  87. package/dist/src/discovery/plan-types.js +2 -0
  88. package/dist/src/discovery/plan-validation.js +139 -0
  89. package/dist/src/discovery/types.js +2 -0
  90. package/dist/src/effects/host.js +91 -0
  91. package/dist/src/extensions/commands/definitions.js +49 -9
  92. package/dist/src/extensions/manifest.js +71 -44
  93. package/dist/src/extensions/project-local-discovery.js +1 -1
  94. package/dist/src/extensions/project-local-gitignore.js +2 -0
  95. package/dist/src/extensions/project-quickref.js +18 -17
  96. package/dist/src/extensions/validation.js +32 -29
  97. package/dist/src/housekeeper/daemon-config.js +148 -0
  98. package/dist/src/housekeeper/ipc.js +84 -0
  99. package/dist/src/housekeeper/lifecycle.js +148 -0
  100. package/dist/src/housekeeper/platform.js +35 -0
  101. package/dist/src/housekeeper/status.js +70 -0
  102. package/dist/src/housekeeper/units.js +114 -0
  103. package/dist/src/issues/cli.js +16 -5
  104. package/dist/src/issues/live.js +44 -12
  105. package/dist/src/jobs/flow.js +22 -21
  106. package/dist/src/lint/rule-activation.js +20 -5
  107. package/dist/src/lint/runner.js +1 -1
  108. package/dist/src/marketplace/exchange.js +18 -6
  109. package/dist/src/marketplace/provenance.js +3 -2
  110. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  111. package/dist/src/mcp/cli.mjs +22 -96
  112. package/dist/src/mcp/config-file.mjs +6 -2
  113. package/dist/src/mcp/config-layers.mjs +23 -7
  114. package/dist/src/mcp/grok-build-config.mjs +2 -1
  115. package/dist/src/mcp/muse-native-config.mjs +2 -1
  116. package/dist/src/mcp/profiles.js +18 -31
  117. package/dist/src/mcp/profiles.mjs +18 -32
  118. package/dist/src/mcp/registry.js +7 -27
  119. package/dist/src/mcp/registry.mjs +7 -28
  120. package/dist/src/mcp/server.mjs +34 -1
  121. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  122. package/dist/src/mcp/tools/command-run.mjs +3 -2
  123. package/dist/src/mcp/tools/decision.mjs +49 -48
  124. package/dist/src/mcp/tools/discovery.mjs +46 -21
  125. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  126. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  127. package/dist/src/mcp/tools/worker-invoke.mjs +30 -6
  128. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  129. package/dist/src/memory/audit.js +719 -0
  130. package/dist/src/memory/canonical-context.js +392 -13
  131. package/dist/src/memory/canonical-maintenance.js +95 -0
  132. package/dist/src/memory/cli-result.js +22 -0
  133. package/dist/src/memory/cli.js +176 -0
  134. package/dist/src/memory/context-pack.js +57 -6
  135. package/dist/src/memory/cost.js +512 -0
  136. package/dist/src/memory/firewall-classification.d.mts +7 -0
  137. package/dist/src/memory/firewall-classification.mjs +32 -0
  138. package/dist/src/memory/index.js +8 -0
  139. package/dist/src/memory/line-maintenance.js +68 -0
  140. package/dist/src/memory/maintenance-cli.js +286 -0
  141. package/dist/src/memory/maintenance-contract.js +136 -0
  142. package/dist/src/memory/maintenance-index.js +18 -0
  143. package/dist/src/memory/maintenance-owner.js +2 -0
  144. package/dist/src/memory/maintenance-record.js +138 -0
  145. package/dist/src/memory/maintenance.js +254 -0
  146. package/dist/src/memory/project-registry.js +2 -1
  147. package/dist/src/memory/provider-memory-adapter.js +154 -0
  148. package/dist/src/memory/recommend-cli.js +83 -0
  149. package/dist/src/memory/recommend.js +269 -0
  150. package/dist/src/memory/scheduled.js +81 -0
  151. package/dist/src/memory/taxonomy.js +44 -0
  152. package/dist/src/mission-protocol/codecs.js +19 -9
  153. package/dist/src/mission-protocol/worker-status.js +91 -1
  154. package/dist/src/models/config-loader.js +11 -1
  155. package/dist/src/models/model-capabilities.v1.json +74 -50
  156. package/dist/src/models/provider-policy.js +125 -62
  157. package/dist/src/models/resolver.js +45 -8
  158. package/dist/src/models/worker-installation.d.mts +8 -1
  159. package/dist/src/models/worker-installation.mjs +231 -38
  160. package/dist/src/models/wrapper-route.js +1 -1
  161. package/dist/src/output-modes/registry.js +8 -7
  162. package/dist/src/packages/adapters/git.js +99 -23
  163. package/dist/src/packages/adapters/gitea.js +8 -27
  164. package/dist/src/packages/adapters/github.js +7 -25
  165. package/dist/src/packages/registry.js +4 -4
  166. package/dist/src/plugin/metadata-validator.js +1 -1
  167. package/dist/src/process/cli-command.d.mts +5 -0
  168. package/dist/src/process/cli-command.mjs +42 -0
  169. package/dist/src/providers/deploy-version.d.mts +1 -0
  170. package/dist/src/providers/deploy-version.mjs +17 -0
  171. package/dist/src/providers/provider-definitions.js +19 -16
  172. package/dist/src/providers/skill-ownership.d.mts +11 -2
  173. package/dist/src/providers/skill-ownership.mjs +35 -17
  174. package/dist/src/rules/activation.d.mts +11 -2
  175. package/dist/src/rules/activation.mjs +82 -6
  176. package/dist/src/rules/inventory.js +1 -1
  177. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  178. package/dist/src/schema/zod-validation.d.mts +6 -0
  179. package/dist/src/schema/zod-validation.mjs +50 -0
  180. package/dist/src/serve/executor-registry.js +297 -20
  181. package/dist/src/serve/executor-token.d.mts +1 -0
  182. package/dist/src/serve/executor-token.mjs +8 -0
  183. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  184. package/dist/src/serve/worker-invocation.js +124 -15
  185. package/dist/src/sessions/adapters/claude.js +19 -18
  186. package/dist/src/sessions/adapters/codex.js +10 -9
  187. package/dist/src/sessions/adapters/copilot.js +14 -13
  188. package/dist/src/sessions/adapters/cursor.js +19 -18
  189. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  190. package/dist/src/sessions/adapters/factory.js +11 -10
  191. package/dist/src/sessions/adapters/generic.js +32 -23
  192. package/dist/src/sessions/adapters/grokbot.js +35 -35
  193. package/dist/src/sessions/adapters/hermes.js +20 -19
  194. package/dist/src/sessions/adapters/muse.js +50 -49
  195. package/dist/src/sessions/adapters/omp.js +5 -4
  196. package/dist/src/sessions/adapters/openclaw.js +30 -29
  197. package/dist/src/sessions/adapters/opencode.js +26 -26
  198. package/dist/src/sessions/adapters/openhuman.js +24 -23
  199. package/dist/src/sessions/adapters/pi.js +5 -4
  200. package/dist/src/sessions/adapters/windsurf.js +8 -8
  201. package/dist/src/sessions/analytics.js +10 -9
  202. package/dist/src/sessions/candidates.js +7 -6
  203. package/dist/src/sessions/contracts.js +63 -53
  204. package/dist/src/sessions/output-registration.js +11 -10
  205. package/dist/src/sessions/workspace-discovery.js +3 -4
  206. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  207. package/dist/src/smiths/context-pipeline/claude-hook.js +46 -10
  208. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  209. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  210. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  211. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  212. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  213. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  214. package/dist/src/smiths/context-pipeline/workspace-context.js +110 -22
  215. package/dist/src/tracker/project-inputs.js +2 -0
  216. package/dist/src/update/checker.d.mts +4 -0
  217. package/dist/src/update/checker.mjs +14 -0
  218. package/dist/src/update/notifier.mjs +2 -4
  219. package/dist/src/writing/exemplar-selection.js +6 -6
  220. package/dist/src/writing/voice-evaluation.js +24 -23
  221. package/dist/src/writing/writer-migration.js +14 -13
  222. package/dist/src/writing/writer-profile.js +19 -18
  223. package/dist/src/writing/writing-brief.js +18 -17
  224. package/dist/src/writing/writing-receipt.js +47 -46
  225. package/package.json +5 -2
  226. package/schemas/decision/DecisionReview.schema.json +78 -1
  227. package/schemas/mission-v1.schema.json +3 -0
  228. package/tools/agents/deploy-agents.mjs +67 -39
  229. package/tools/agents/providers/antigravity.mjs +6 -1
  230. package/tools/agents/providers/base.mjs +24 -14
  231. package/tools/agents/providers/claude.mjs +6 -3
  232. package/tools/agents/providers/codex.mjs +54 -57
  233. package/tools/agents/providers/context-write.mjs +28 -0
  234. package/tools/agents/providers/copilot.mjs +13 -4
  235. package/tools/agents/providers/cursor.mjs +1 -1
  236. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  237. package/tools/agents/providers/factory.mjs +7 -1
  238. package/tools/agents/providers/grok-build.mjs +18 -2
  239. package/tools/agents/providers/grokbot.mjs +1 -1
  240. package/tools/agents/providers/hermes.mjs +12 -15
  241. package/tools/agents/providers/muse-hooks.mjs +21 -14
  242. package/tools/agents/providers/muse.mjs +7 -2
  243. package/tools/agents/providers/omp.mjs +31 -5
  244. package/tools/agents/providers/opencode.mjs +14 -5
  245. package/tools/agents/providers/openhuman.mjs +1 -1
  246. package/tools/agents/providers/pi.mjs +1 -1
  247. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  248. package/tools/agents/providers/rule-delivery.mjs +289 -80
  249. package/tools/agents/providers/warp.mjs +4 -1
  250. package/tools/agents/providers/windsurf.mjs +13 -3
  251. package/tools/cli/workspace-status.mjs +5 -0
  252. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  253. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  254. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  255. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  256. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  257. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  258. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  259. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  260. package/tools/ralph-external/process-monitor.mjs +49 -8
  261. package/tools/ralph-external/session-launcher.mjs +405 -35
  262. package/tools/skills/deploy-skills-codex.mjs +11 -7
  263. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -20,7 +20,7 @@ import { writeNormalizedAiwgMd } from './finalization.js';
20
20
  import { shouldEmitAgentsMd, shouldEmitAiwgMd, shouldEmitClaudeMdHook } from './provider-policy.js';
21
21
  import { ensureClaudeMdHook } from './claude-hook.js';
22
22
  import { ensureManagedHook } from './managed-hook.js';
23
- import { assertSafeContextFile } from './context-file-safety.js';
23
+ import { assertSafeContextFile, writeContextFileSync } from './context-file-safety.js';
24
24
  import { buildProviderBootstrapBlock, ensureWorkspaceContext, registerProviderContext, } from './workspace-context.js';
25
25
  import { projectControlPath } from '../../config/project-artifacts.js';
26
26
  const SECTION_TITLES = {
@@ -195,9 +195,8 @@ export async function isOverwriteSafe(filePath) {
195
195
  }
196
196
  }
197
197
  /**
198
- * Atomically write a file via tmpfile + rename.
199
- *
200
- * Per ADR-4 §4 atomic write requirement; on failure no partial state is persisted.
198
+ * Preserve rule-owned blocks, then write through the safe context boundary.
199
+ * The deploy journal retains the preimage even if a later step fails.
201
200
  */
202
201
  async function atomicWrite(filePath, content, provider) {
203
202
  // Rule delivery owns this block. Context regeneration runs after deployment
@@ -217,18 +216,7 @@ async function atomicWrite(filePath, content, provider) {
217
216
  if (provider === 'hermes' && ['AGENTS.md', '.hermes.md'].includes(path.basename(filePath)) && content.length > 19000) {
218
217
  throw new Error(`Hermes ${path.basename(filePath)} exceeds hard cap of 19000 characters`);
219
218
  }
220
- const dir = path.dirname(filePath);
221
- const tmp = path.join(dir, `.${path.basename(filePath)}.tmp.${process.pid}`);
222
- await fs.mkdir(dir, { recursive: true });
223
- await fs.writeFile(tmp, content, 'utf8');
224
- try {
225
- await fs.rename(tmp, filePath);
226
- }
227
- catch (err) {
228
- // Best-effort cleanup of tmpfile on rename failure.
229
- await fs.unlink(tmp).catch(() => undefined);
230
- throw err;
231
- }
219
+ writeContextFileSync(filePath, content);
232
220
  return bytes;
233
221
  }
234
222
  /**
@@ -312,7 +300,7 @@ async function writeTwinFiles(provider, projectPath, agentsMdContent, opts, resu
312
300
  const original = await fs.readFile(twinPath, 'utf8');
313
301
  if (!original.includes(AIWG_SIGNATURE_COMMENT) && !/Generated by\s+(AIWG|aiwg)/i.test(original.split('\n').slice(0, 4).join('\n'))) {
314
302
  const backupPath = `${twinPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
315
- await fs.writeFile(backupPath, original, 'utf8');
303
+ writeContextFileSync(backupPath, original, 'utf8');
316
304
  result.backupPaths.push(backupPath);
317
305
  }
318
306
  }
@@ -412,7 +400,7 @@ export async function generate(opts) {
412
400
  const original = await fs.readFile(agentsMdPath, 'utf8');
413
401
  if (!original.includes(AIWG_SIGNATURE_COMMENT)) {
414
402
  const backupPath = `${agentsMdPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
415
- await fs.writeFile(backupPath, original, 'utf8');
403
+ writeContextFileSync(backupPath, original, 'utf8');
416
404
  result.backupPaths.push(backupPath);
417
405
  }
418
406
  }
@@ -463,7 +451,7 @@ export async function generate(opts) {
463
451
  const original = await fs.readFile(aiwgMdPath, 'utf8');
464
452
  if (!original.includes(AIWG_SIGNATURE_COMMENT)) {
465
453
  const backupPath = `${aiwgMdPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
466
- await fs.writeFile(backupPath, original, 'utf8');
454
+ writeContextFileSync(backupPath, original, 'utf8');
467
455
  result.backupPaths.push(backupPath);
468
456
  }
469
457
  }
@@ -1,3 +1,4 @@
1
+ import { writeContextFileSync } from './context-file-safety.js';
1
2
  import { promises as fs } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { getProviderDefinition } from '../../providers/provider-definitions.js';
@@ -60,15 +61,7 @@ async function readOptional(file) {
60
61
  }
61
62
  async function atomicWrite(file, content) {
62
63
  await fs.mkdir(path.dirname(file), { recursive: true });
63
- const temporary = path.join(path.dirname(file), `.${path.basename(file)}.tmp.${process.pid}`);
64
- await fs.writeFile(temporary, content, 'utf8');
65
- try {
66
- await fs.rename(temporary, file);
67
- }
68
- catch (error) {
69
- await fs.rm(temporary, { force: true });
70
- throw error;
71
- }
64
+ writeContextFileSync(file, content);
72
65
  }
73
66
  export async function injectLegacyContext(provider, projectPath, frameworkContent, options = {}) {
74
67
  const definition = getProviderDefinition(provider);
@@ -93,7 +86,7 @@ export async function injectLegacyContext(provider, projectPath, frameworkConten
93
86
  if (existing !== null) {
94
87
  const backup = `${target}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
95
88
  await fs.mkdir(path.dirname(backup), { recursive: true });
96
- await fs.writeFile(backup, existing, 'utf8');
89
+ writeContextFileSync(backup, existing, 'utf8');
97
90
  backups.push(backup);
98
91
  }
99
92
  await atomicWrite(target, output);
@@ -15,7 +15,7 @@ import * as fs from 'fs/promises';
15
15
  import * as path from 'path';
16
16
  import { buildProviderBootstrapBlock } from './workspace-context.js';
17
17
  import { dominantLineEnding, withLineEnding } from './line-endings.js';
18
- import { assertSafeContextFile } from './context-file-safety.js';
18
+ import { assertSafeContextFile, writeContextFileSync } from './context-file-safety.js';
19
19
  export const CONTEXT_HOOK_START = '<!-- AIWG:context-hook:start -->';
20
20
  export const CONTEXT_HOOK_END = '<!-- AIWG:context-hook:end -->';
21
21
  /** The managed block — loads canonical workspace context before framework context. */
@@ -53,7 +53,7 @@ export async function ensureManagedHook(filePath, opts = {}) {
53
53
  }
54
54
  catch (err) {
55
55
  if (err.code === 'ENOENT') {
56
- await fs.writeFile(filePath, `${block}\n`, 'utf8');
56
+ writeContextFileSync(filePath, `${block}\n`, 'utf8');
57
57
  result.action = 'created';
58
58
  return result;
59
59
  }
@@ -71,7 +71,7 @@ export async function ensureManagedHook(filePath, opts = {}) {
71
71
  // No managed block — append it to the end, preserving everything above.
72
72
  if (s === -1 && e === -1) {
73
73
  const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
74
- await fs.writeFile(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
74
+ writeContextFileSync(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
75
75
  result.action = 'inserted';
76
76
  return result;
77
77
  }
@@ -79,7 +79,7 @@ export async function ensureManagedHook(filePath, opts = {}) {
79
79
  if (s === -1 || e === -1) {
80
80
  if (opts.force) {
81
81
  const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
82
- await fs.writeFile(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
82
+ writeContextFileSync(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
83
83
  result.action = 'inserted';
84
84
  return result;
85
85
  }
@@ -93,7 +93,7 @@ export async function ensureManagedHook(filePath, opts = {}) {
93
93
  result.action = 'unchanged';
94
94
  return result;
95
95
  }
96
- await fs.writeFile(filePath, existing.substring(0, s) + block + existing.substring(endOff), 'utf8');
96
+ writeContextFileSync(filePath, existing.substring(0, s) + block + existing.substring(endOff), 'utf8');
97
97
  result.action = 'updated';
98
98
  return result;
99
99
  }
@@ -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);
@@ -1,3 +1,4 @@
1
+ import { writeContextFileSync } from './context-file-safety.js';
1
2
  /**
2
3
  * Canonical cross-provider workspace context graph (#1811).
3
4
  *
@@ -14,11 +15,14 @@ import { buildAiwgMdContent } from './aiwg-md.js';
14
15
  import { buildNormalizedAiwgMd } from './finalization.js';
15
16
  import { getProviderDefinition, listProviderDefinitions, } from '../../providers/provider-definitions.js';
16
17
  import { buildMuseBridgeText } from './muse-bridge.js';
17
- import { readAiwgConfig } from '../../config/aiwg-config.js';
18
+ import { readAiwgConfig, readAiwgConfigCandidate } from '../../config/aiwg-config.js';
19
+ import { resolveRulesInlineBudget } from '../../rules/inline-budget.mjs';
18
20
  import { projectAiwgPath, projectControlPath, resolveProjectAiwgDir, } from '../../config/project-artifacts.js';
19
21
  import { dominantLineEnding, withLineEnding } from './line-endings.js';
20
22
  export const WORKSPACE_MANAGED_START = '<!-- AIWG:workspace-context:start -->';
21
23
  export const WORKSPACE_MANAGED_END = '<!-- AIWG:workspace-context:end -->';
24
+ export const RESIDENT_MEMORY_START = '<!-- AIWG:resident-memory:start -->';
25
+ export const RESIDENT_MEMORY_END = '<!-- AIWG:resident-memory:end -->';
22
26
  export const WORKSPACE_OPERATOR_START = '<!-- AIWG:workspace-operator:start -->';
23
27
  export const WORKSPACE_OPERATOR_END = '<!-- AIWG:workspace-operator:end -->';
24
28
  export const PROJECT_EXTRACTION_START = '<!-- AIWG:project-extraction:start -->';
@@ -68,16 +72,7 @@ async function readOptional(filePath) {
68
72
  }
69
73
  }
70
74
  async function atomicWrite(filePath, content) {
71
- await fs.mkdir(path.dirname(filePath), { recursive: true });
72
- const temporary = path.join(path.dirname(filePath), `.${path.basename(filePath)}.tmp.${process.pid}`);
73
- await fs.writeFile(temporary, content, 'utf8');
74
- try {
75
- await fs.rename(temporary, filePath);
76
- }
77
- catch (error) {
78
- await fs.rm(temporary, { force: true }).catch(() => undefined);
79
- throw error;
80
- }
75
+ writeContextFileSync(filePath, content);
81
76
  }
82
77
  function resolveProjectRelative(projectPath, relativePath) {
83
78
  if (path.isAbsolute(relativePath))
@@ -288,7 +283,13 @@ export async function extractExistingProjectContext(projectPath) {
288
283
  lines.push('', '### Testing', '', ...tests.map((source) => `- ${projectLink(source)}`));
289
284
  }
290
285
  if (workflows.length) {
291
- lines.push('', '### Continuous Integration', '', ...workflows.map((source) => `- ${projectLink(source)}`));
286
+ // #3451: directory pointers with counts, not one line per workflow file.
287
+ const byDirectory = new Map();
288
+ for (const source of workflows) {
289
+ const directory = source.slice(0, source.lastIndexOf('/'));
290
+ byDirectory.set(directory, (byDirectory.get(directory) ?? 0) + 1);
291
+ }
292
+ lines.push('', '### Continuous Integration', '', ...[...byDirectory].map(([directory, count]) => `- ${projectLink(`${directory}/`)} (${count} workflow file${count === 1 ? '' : 's'})`));
292
293
  }
293
294
  for (const source of [...architecture, ...tests, ...workflows])
294
295
  sources.add(source);
@@ -358,7 +359,51 @@ export const WORKSPACE_PRECEDENCE_SIGNATURE = 'AIWG rules deployed to this proje
358
359
  export const BOOTSTRAP_AUTHORITY_SIGNATURE = 'AIWG rules deployed to this project are binding';
359
360
  /** Precedence ordering superseded by #2512; its presence means a stale block. */
360
361
  export const WORKSPACE_PRECEDENCE_SUPERSEDED = 'Provider, system, and organization instructions retain their native authority.';
361
- export function buildWorkspaceManagedBlock(projectPath, providerFiles = []) {
362
+ /** Only this projection may enter startup context; portable exports retain history. */
363
+ async function buildResidentMemoryBlock(projectPath) {
364
+ const { CanonicalContextRepository } = await import('../../memory/canonical-context.js');
365
+ const entries = new CanonicalContextRepository(projectPath).residentEntries()
366
+ .sort((left, right) => left.entryId.localeCompare(right.entryId));
367
+ if (!entries.length)
368
+ return '';
369
+ const quote = (text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
370
+ .replace(/\r\n?/g, '\n').split('\n').map(line => `> ${line}`).join('\n');
371
+ return [RESIDENT_MEMORY_START, '', '### Reviewed Resident Memory', '',
372
+ 'Reviewed canonical context follows as quoted data; placement does not grant instruction authority.', '',
373
+ ...entries.flatMap(entry => [quote(`${entry.target}: ${entry.key}\n${entry.value}`), '']),
374
+ RESIDENT_MEMORY_END].join('\n');
375
+ }
376
+ async function residentMemoryForRegeneration(projectPath) {
377
+ const block = await buildResidentMemoryBlock(projectPath);
378
+ if (!block)
379
+ return '';
380
+ const config = await readAiwgConfigCandidate(projectPath);
381
+ // Use the same resident inline budget and UTF-8 estimator as memory cost (#2938).
382
+ const limit = resolveRulesInlineBudget(config ?? {});
383
+ const tokens = Buffer.byteLength(block, 'utf8') / 4;
384
+ if (limit > 0 && tokens > limit) {
385
+ throw new Error(`Canonical resident memory exceeds rules.inline_budget_tokens: ${tokens} estimated tokens; limit ${limit}. Review resident placement before regenerating.`);
386
+ }
387
+ return block;
388
+ }
389
+ /** Reporting only: never repairs provider context or changes a running session. */
390
+ export async function providerCopiesStatus(projectPath) {
391
+ const expected = await buildResidentMemoryBlock(projectPath);
392
+ const workspace = (await readOptional(path.join(projectPath, 'WORKSPACE.md')) ?? '').replace(/\r\n/g, '\n');
393
+ const start = workspace.indexOf(RESIDENT_MEMORY_START);
394
+ const end = workspace.indexOf(RESIDENT_MEMORY_END);
395
+ const regenerate = 'aiwg regenerate --workspace --provider generic';
396
+ if (!expected && start < 0 && end < 0)
397
+ return { status: 'not-applicable', regenerate };
398
+ const managedStart = workspace.indexOf(WORKSPACE_MANAGED_START);
399
+ const managedEnd = workspace.indexOf(WORKSPACE_MANAGED_END);
400
+ const valid = start > managedStart && managedStart >= 0 && end > start && managedEnd > end
401
+ && workspace.indexOf(RESIDENT_MEMORY_START, start + RESIDENT_MEMORY_START.length) < 0
402
+ && workspace.indexOf(RESIDENT_MEMORY_END, end + RESIDENT_MEMORY_END.length) < 0;
403
+ const emitted = valid ? workspace.slice(start, end + RESIDENT_MEMORY_END.length) : '';
404
+ return { status: valid && sha256(emitted) === sha256(expected) ? 'current' : 'stale', regenerate };
405
+ }
406
+ export function buildWorkspaceManagedBlock(projectPath, providerFiles = [], residentMemory = '') {
362
407
  const links = workspaceLinks(projectPath, providerFiles);
363
408
  return [
364
409
  WORKSPACE_MANAGED_START,
@@ -404,17 +449,18 @@ export function buildWorkspaceManagedBlock(projectPath, providerFiles = []) {
404
449
  '',
405
450
  ...links.map((link) => `- ${link}`),
406
451
  '',
452
+ ...(residentMemory ? [residentMemory, ''] : []),
407
453
  WORKSPACE_MANAGED_END,
408
454
  ].join('\n');
409
455
  }
410
- export function buildWorkspaceDocument(projectPath, operatorContent = '', providerFiles = []) {
456
+ export function buildWorkspaceDocument(projectPath, operatorContent = '', providerFiles = [], residentMemory = '') {
411
457
  const content = operatorContent.trim() || PROJECT_CONTEXT_PLACEHOLDER;
412
458
  return [
413
459
  '# WORKSPACE.md',
414
460
  WORKSPACE_SIGNATURE,
415
461
  '<!-- Generated structure by AIWG; operator content is protected by markers. -->',
416
462
  '',
417
- buildWorkspaceManagedBlock(projectPath, providerFiles),
463
+ buildWorkspaceManagedBlock(projectPath, providerFiles, residentMemory),
418
464
  '',
419
465
  WORKSPACE_OPERATOR_START,
420
466
  '',
@@ -433,11 +479,12 @@ export async function ensureWorkspaceContext(projectPath, options = {}) {
433
479
  // so their links survive every regeneration instead of only the one that
434
480
  // wrote them (#2558).
435
481
  const providerFiles = options.providerFiles ?? await migratedProviderFiles(projectPath);
482
+ const residentMemory = await residentMemoryForRegeneration(projectPath);
436
483
  if (existing === null) {
437
- await atomicWrite(workspacePath, buildWorkspaceDocument(projectPath, '', providerFiles));
484
+ await atomicWrite(workspacePath, buildWorkspaceDocument(projectPath, '', providerFiles, residentMemory));
438
485
  return { path: workspacePath, action: 'created', warnings };
439
486
  }
440
- const block = buildWorkspaceManagedBlock(projectPath, providerFiles);
487
+ const block = buildWorkspaceManagedBlock(projectPath, providerFiles, residentMemory);
441
488
  if (existing.includes(WORKSPACE_MANAGED_START) || existing.includes(WORKSPACE_MANAGED_END)) {
442
489
  let updated = replaceBlock(existing, WORKSPACE_MANAGED_START, WORKSPACE_MANAGED_END, block);
443
490
  // A managed project-extraction block explicitly points operators back to
@@ -458,8 +505,8 @@ export async function ensureWorkspaceContext(projectPath, options = {}) {
458
505
  return { path: workspacePath, action: 'preserved', warnings };
459
506
  }
460
507
  const backupPath = `${workspacePath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
461
- await fs.writeFile(backupPath, existing, 'utf8');
462
- await atomicWrite(workspacePath, buildWorkspaceDocument(projectPath, existing, providerFiles));
508
+ writeContextFileSync(backupPath, existing, 'utf8');
509
+ await atomicWrite(workspacePath, buildWorkspaceDocument(projectPath, existing, providerFiles, residentMemory));
463
510
  return { path: workspacePath, action: 'updated', backupPath, warnings };
464
511
  }
465
512
  export function buildProviderBootstrapBlock(provider) {
@@ -526,9 +573,50 @@ export function buildProviderBootstrapBlock(provider) {
526
573
  PROVIDER_BOOTSTRAP_END,
527
574
  ].join('\n');
528
575
  }
576
+ export const PROVIDER_BOOTSTRAP_HEADING = '# Provider workspace bootstrap';
577
+ /**
578
+ * Collapse repeated bootstrap headings and repeated managed bootstrap blocks.
579
+ *
580
+ * Root cause of the duplicated "# Provider workspace bootstrap" (#3451): the
581
+ * bootstrap *block* already carries its own heading, and the whole-file
582
+ * wrapper used to prepend a second one. Regeneration then replaced only the
583
+ * block, so the wrapper heading survived next to the block heading. The
584
+ * wrapper no longer emits it; this repairs files that already have it and
585
+ * keeps regeneration idempotent. Operator content is never touched.
586
+ */
587
+ export function dedupeProviderBootstrap(content) {
588
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
589
+ let result = content;
590
+ // A heading line that precedes the managed block's own heading is the
591
+ // wrapper copy: drop it (and only it) from the top of the file.
592
+ const lines = result.split(/\r?\n/);
593
+ const headingIdx = lines.reduce((acc, line, i) => {
594
+ if (line.trimEnd() === PROVIDER_BOOTSTRAP_HEADING)
595
+ acc.push(i);
596
+ return acc;
597
+ }, []);
598
+ if (headingIdx.length >= 2 && headingIdx[0] === 0) {
599
+ lines.splice(0, 1);
600
+ result = lines.join(eol);
601
+ }
602
+ // Identical managed bootstrap blocks repeated back-to-back or elsewhere:
603
+ // keep the first.
604
+ if (result.split(PROVIDER_BOOTSTRAP_START).length > 2) {
605
+ const first = result.indexOf(PROVIDER_BOOTSTRAP_START);
606
+ const firstEnd = result.indexOf(PROVIDER_BOOTSTRAP_END, first);
607
+ if (first >= 0 && firstEnd > first) {
608
+ const blockEnd = firstEnd + PROVIDER_BOOTSTRAP_END.length;
609
+ const firstBlock = result.slice(first, blockEnd);
610
+ const head = result.slice(0, blockEnd);
611
+ const tail = result.slice(blockEnd).split(firstBlock).join('');
612
+ result = head + tail.replace(new RegExp(`(?:${eol}){3,}`, 'g'), eol + eol);
613
+ }
614
+ }
615
+ return result;
616
+ }
529
617
  export function buildProviderBootstrapFile(provider) {
618
+ // No heading here: the block below owns "# Provider workspace bootstrap".
530
619
  return [
531
- '# Provider workspace bootstrap',
532
620
  WORKSPACE_SIGNATURE,
533
621
  '<!-- Generated by AIWG. Project/operator context belongs in WORKSPACE.md. -->',
534
622
  '',
@@ -916,7 +1004,7 @@ async function stageMigrationWrites(projectPath, audit, options = {}) {
916
1004
  const extracted = await extractExistingProjectContext(projectPath);
917
1005
  workspaceOperatorContent = mergeProjectExtraction(workspaceOperatorContent, extracted.content);
918
1006
  }
919
- writes.set('WORKSPACE.md', buildWorkspaceDocument(projectPath, workspaceOperatorContent, providerFiles));
1007
+ writes.set('WORKSPACE.md', buildWorkspaceDocument(projectPath, workspaceOperatorContent, providerFiles, await residentMemoryForRegeneration(projectPath)));
920
1008
  const providers = await configuredProviders(projectPath);
921
1009
  const targetProviders = new Map();
922
1010
  for (const provider of providers) {
@@ -1026,7 +1114,7 @@ export async function migrateWorkspaceContext(projectPath, options = {}) {
1026
1114
  let preimage = null;
1027
1115
  if (existing !== null) {
1028
1116
  preimage = path.relative(projectPath, path.join(preimageDir, safePreimageName(relativePath))).replace(/\\/g, '/');
1029
- await fs.writeFile(path.join(projectPath, preimage), existing, 'utf8');
1117
+ writeContextFileSync(path.join(projectPath, preimage), existing, 'utf8');
1030
1118
  backups.push(preimage);
1031
1119
  }
1032
1120
  manifest.files.push({ path: relativePath, existed: existing !== null, preimage, outputChecksum: sha256(content) });
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { execFile } from 'node:child_process';
7
7
  import { promisify } from 'node:util';
8
+ import { helperEnvironment } from '../process/cli-command.mjs';
8
9
  import { readAiwgConfig } from '../config/aiwg-config.js';
9
10
  const execFileAsync = promisify(execFile);
10
11
  /** The project config, or `null` when it is missing or unreadable. */
@@ -20,6 +21,7 @@ export async function readConfig(projectPath) {
20
21
  export async function readGitRemoteUrls(projectPath) {
21
22
  try {
22
23
  const { stdout } = await execFileAsync('git', ['-C', projectPath, 'remote', '-v'], {
24
+ env: helperEnvironment(),
23
25
  maxBuffer: 1024 * 1024,
24
26
  timeout: 10_000,
25
27
  });
@@ -1,2 +1,6 @@
1
1
  export function checkForUpdates(): Promise<void>;
2
2
  export function forceUpdateCheck(): Promise<void>;
3
+ export function checkUpdateStatus(options?: { channel?: string }): Promise<{
4
+ status: string; channel: string; checkOnly: boolean; reason?: string;
5
+ current?: string; latest?: string; available?: boolean;
6
+ }>;
@@ -395,3 +395,17 @@ export async function forceUpdateCheck() {
395
395
  }
396
396
  }
397
397
  }
398
+
399
+ /** Check-only owner API for non-interactive maintenance. Never prompts or installs.
400
+ * Edge has no read-only remote probe contract here; callers must report unsupported.
401
+ */
402
+ export async function checkUpdateStatus({ channel = 'stable' } = {}) {
403
+ if (!['stable', 'next', 'nightly'].includes(channel)) {
404
+ return { status: 'unsupported', channel, checkOnly: true, reason: 'unsupported-channel' };
405
+ }
406
+ const packageJson = JSON.parse(await fs.readFile(path.join(getPackageRoot(), 'package.json'), 'utf8'));
407
+ const latest = await fetchNpmDistTag(channel === 'stable' ? 'latest' : channel);
408
+ if (!latest) throw new Error('Update registry probe failed');
409
+ return { status: 'ok', channel, checkOnly: true, current: packageJson.version, latest,
410
+ available: isNewerVersion(packageJson.version, latest) };
411
+ }
@@ -36,6 +36,7 @@ import { spawn } from 'child_process';
36
36
  import { fileURLToPath } from 'url';
37
37
  import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
38
38
  import { loadInstallationIdentity } from '../installation/manager.mjs';
39
+ import { helperEnvironment } from '../process/cli-command.mjs';
39
40
 
40
41
  const CACHE_DIR = resolveUserConfigDir();
41
42
  const CACHE_FILE = path.join(CACHE_DIR, 'update-notifier.json');
@@ -261,10 +262,7 @@ const NOTIFIER_CHILD_ENV_ALLOWLIST = Object.freeze([
261
262
  * secret-shaped variable" (go/no-go review, blocker 1 tests).
262
263
  */
263
264
  export function notifierChildEnv(parentEnv = process.env) {
264
- const env = {};
265
- for (const name of NOTIFIER_CHILD_ENV_ALLOWLIST) {
266
- if (parentEnv[name] !== undefined) env[name] = parentEnv[name];
267
- }
265
+ const env = helperEnvironment(parentEnv, NOTIFIER_CHILD_ENV_ALLOWLIST);
268
266
  env.AIWG_UPDATE_NOTIFIER_CHILD = '1';
269
267
  return env;
270
268
  }
@@ -4,14 +4,14 @@ import { parseWriterProfile } from './writer-profile.js';
4
4
  const digest = (text) => createHash('sha256').update(text).digest('hex');
5
5
  const identifier = z.string().min(1).max(120);
6
6
  const tags = z.array(z.string().min(1).max(100)).max(100);
7
- const descriptorSchema = z.object({ sampleId: identifier, profileId: identifier, genre: z.string().min(1).max(100), topicTags: tags, styleTags: tags }).strict();
8
- const partitionSchema = z.object({
7
+ export const exemplarDescriptorSchema = z.strictObject({ sampleId: identifier, profileId: identifier, genre: z.string().min(1).max(100), topicTags: tags, styleTags: tags });
8
+ const partitionSchema = z.strictObject({
9
9
  version: identifier, purpose: z.enum(['development', 'final-author']),
10
10
  developmentAuthorIds: z.array(identifier), finalAuthorIds: z.array(identifier),
11
- holdouts: z.array(z.object({ id: identifier, authorId: identifier, text: z.string().max(1_000_000) }).strict()),
11
+ holdouts: z.array(z.strictObject({ id: identifier, authorId: identifier, text: z.string().max(1_000_000) })),
12
12
  nearDuplicateThreshold: z.number().min(0).max(1),
13
13
  duplicateMetric: z.literal('normalized-word-trigram-jaccard-v1'),
14
- }).strict();
14
+ });
15
15
  export const EXEMPLAR_STRATEGIES = ['random', 'style-varied', 'length-matched', 'topic-matched'];
16
16
  /** Byte budget, not an exact model token count. Transport/prompt overhead is outside this payload. */
17
17
  export const UTF8_BYTE_BUDGET = {
@@ -82,7 +82,7 @@ function enroll(profile, descriptors, partition) {
82
82
  export function createExemplarStore(input, descriptorInput, partitionInput) {
83
83
  const profile = parseWriterProfile(input);
84
84
  const partition = validatePartition(profile.id, partitionInput);
85
- const parsed = z.array(descriptorSchema).safeParse(descriptorInput);
85
+ const parsed = z.array(exemplarDescriptorSchema).safeParse(descriptorInput);
86
86
  if (!parsed.success || new Set(descriptorInput.map(d => d.sampleId)).size !== descriptorInput.length)
87
87
  throw new Error('Invalid or duplicate exemplar descriptors');
88
88
  const { accepted, exclusions } = enroll(profile, parsed.data, partition);
@@ -96,7 +96,7 @@ function validateStore(profile, store) {
96
96
  if (storeFingerprint(body) !== fingerprint)
97
97
  throw new Error('Exemplar store integrity changed; recreate enrollment');
98
98
  validatePartition(profile.id, store.partition);
99
- if (!z.array(descriptorSchema).safeParse(store.descriptors).success)
99
+ if (!z.array(exemplarDescriptorSchema).safeParse(store.descriptors).success)
100
100
  throw new Error('Invalid exemplar descriptors');
101
101
  }
102
102
  function checkedCount(counter, value) {