@aiwg/cli 2026.10.5 → 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 (212) 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 +57 -6
  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 +143 -14
  14. package/dist/src/artifacts/index-files.js +15 -4
  15. package/dist/src/artifacts/index-reader.js +44 -0
  16. package/dist/src/artifacts/index-refresh.js +114 -55
  17. package/dist/src/artifacts/index-status.js +20 -5
  18. package/dist/src/artifacts/memory-maintenance.js +503 -0
  19. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  20. package/dist/src/artifacts/query-engine.js +263 -108
  21. package/dist/src/artifacts/relations-builder.js +2 -2
  22. package/dist/src/artifacts/source-graph.js +2 -2
  23. package/dist/src/artifacts/types.js +9 -1
  24. package/dist/src/cli/handlers/decision.js +19 -1
  25. package/dist/src/cli/handlers/guided-setup.js +470 -0
  26. package/dist/src/cli/handlers/help.js +2 -2
  27. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  28. package/dist/src/cli/handlers/index.js +4 -2
  29. package/dist/src/cli/handlers/init.js +41 -6
  30. package/dist/src/cli/handlers/local-executor.js +5 -3
  31. package/dist/src/cli/handlers/mc.js +509 -53
  32. package/dist/src/cli/handlers/models.js +32 -2
  33. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  34. package/dist/src/cli/handlers/rules.js +1 -1
  35. package/dist/src/cli/handlers/script-runner.js +73 -2
  36. package/dist/src/cli/handlers/serve.js +5 -3
  37. package/dist/src/cli/handlers/sessions.js +1 -0
  38. package/dist/src/cli/handlers/setup.js +428 -1
  39. package/dist/src/cli/handlers/skill-lint.js +1 -1
  40. package/dist/src/cli/handlers/subcommands.js +118 -8
  41. package/dist/src/cli/handlers/use.js +279 -93
  42. package/dist/src/cli/handlers/workspace.js +3 -1
  43. package/dist/src/cli/prompt-utils.js +8 -0
  44. package/dist/src/cli/router.js +16 -1
  45. package/dist/src/config/aiwg-config.js +42 -0
  46. package/dist/src/config/cli.js +7 -3
  47. package/dist/src/config/decision-config.js +14 -1
  48. package/dist/src/config/native-sandbox-warnings.js +25 -0
  49. package/dist/src/contributors/validation.js +6 -5
  50. package/dist/src/decision/admission.js +7 -3
  51. package/dist/src/decision/benchmark.js +469 -0
  52. package/dist/src/decision/ledger.js +22 -3
  53. package/dist/src/decision/pricing.js +147 -0
  54. package/dist/src/decision/qualification/gate-evidence.js +18 -15
  55. package/dist/src/decision/qualification/load-staged.js +539 -0
  56. package/dist/src/decision/qualification/load.js +109 -33
  57. package/dist/src/decision/qualification/runner.js +5 -2
  58. package/dist/src/decision/review/service.js +6 -4
  59. package/dist/src/decision/review/validate.js +23 -2
  60. package/dist/src/decision/savings.js +8 -3
  61. package/dist/src/discovery/index.js +492 -0
  62. package/dist/src/discovery/interview.js +233 -0
  63. package/dist/src/discovery/plan-apply.js +571 -0
  64. package/dist/src/discovery/plan-store.js +171 -0
  65. package/dist/src/discovery/plan-types.js +2 -0
  66. package/dist/src/discovery/plan-validation.js +139 -0
  67. package/dist/src/discovery/types.js +2 -0
  68. package/dist/src/extensions/commands/definitions.js +49 -9
  69. package/dist/src/extensions/manifest.js +71 -44
  70. package/dist/src/extensions/project-local-discovery.js +1 -1
  71. package/dist/src/extensions/project-quickref.js +18 -17
  72. package/dist/src/extensions/validation.js +32 -29
  73. package/dist/src/housekeeper/daemon-config.js +148 -0
  74. package/dist/src/housekeeper/ipc.js +84 -0
  75. package/dist/src/housekeeper/lifecycle.js +148 -0
  76. package/dist/src/housekeeper/platform.js +35 -0
  77. package/dist/src/housekeeper/status.js +70 -0
  78. package/dist/src/housekeeper/units.js +114 -0
  79. package/dist/src/jobs/flow.js +22 -21
  80. package/dist/src/lint/rule-activation.js +20 -5
  81. package/dist/src/lint/runner.js +1 -1
  82. package/dist/src/mcp/cli.mjs +5 -6
  83. package/dist/src/mcp/config-file.mjs +6 -2
  84. package/dist/src/mcp/config-layers.mjs +23 -7
  85. package/dist/src/mcp/profiles.js +18 -31
  86. package/dist/src/mcp/profiles.mjs +18 -32
  87. package/dist/src/mcp/registry.js +7 -27
  88. package/dist/src/mcp/registry.mjs +7 -28
  89. package/dist/src/mcp/server.mjs +34 -1
  90. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  91. package/dist/src/mcp/tools/command-run.mjs +3 -2
  92. package/dist/src/mcp/tools/decision.mjs +49 -48
  93. package/dist/src/mcp/tools/discovery.mjs +46 -21
  94. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  95. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  96. package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
  97. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  98. package/dist/src/memory/audit.js +719 -0
  99. package/dist/src/memory/canonical-context.js +392 -13
  100. package/dist/src/memory/canonical-maintenance.js +95 -0
  101. package/dist/src/memory/cli-result.js +22 -0
  102. package/dist/src/memory/cli.js +176 -0
  103. package/dist/src/memory/context-pack.js +57 -6
  104. package/dist/src/memory/cost.js +512 -0
  105. package/dist/src/memory/firewall-classification.d.mts +7 -0
  106. package/dist/src/memory/firewall-classification.mjs +32 -0
  107. package/dist/src/memory/index.js +8 -0
  108. package/dist/src/memory/line-maintenance.js +68 -0
  109. package/dist/src/memory/maintenance-cli.js +286 -0
  110. package/dist/src/memory/maintenance-contract.js +136 -0
  111. package/dist/src/memory/maintenance-index.js +18 -0
  112. package/dist/src/memory/maintenance-owner.js +2 -0
  113. package/dist/src/memory/maintenance-record.js +138 -0
  114. package/dist/src/memory/maintenance.js +254 -0
  115. package/dist/src/memory/provider-memory-adapter.js +154 -0
  116. package/dist/src/memory/recommend-cli.js +83 -0
  117. package/dist/src/memory/recommend.js +269 -0
  118. package/dist/src/memory/scheduled.js +81 -0
  119. package/dist/src/memory/taxonomy.js +44 -0
  120. package/dist/src/mission-protocol/codecs.js +19 -9
  121. package/dist/src/models/config-loader.js +11 -1
  122. package/dist/src/models/model-capabilities.v1.json +74 -50
  123. package/dist/src/models/provider-policy.js +125 -62
  124. package/dist/src/models/resolver.js +45 -8
  125. package/dist/src/models/worker-installation.d.mts +7 -0
  126. package/dist/src/models/worker-installation.mjs +169 -37
  127. package/dist/src/models/wrapper-route.js +1 -1
  128. package/dist/src/output-modes/registry.js +8 -7
  129. package/dist/src/plugin/metadata-validator.js +1 -1
  130. package/dist/src/providers/deploy-version.d.mts +1 -0
  131. package/dist/src/providers/deploy-version.mjs +17 -0
  132. package/dist/src/providers/provider-definitions.js +19 -16
  133. package/dist/src/providers/skill-ownership.d.mts +11 -2
  134. package/dist/src/providers/skill-ownership.mjs +35 -17
  135. package/dist/src/rules/activation.d.mts +11 -2
  136. package/dist/src/rules/activation.mjs +82 -6
  137. package/dist/src/rules/inventory.js +1 -1
  138. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  139. package/dist/src/schema/zod-validation.d.mts +6 -0
  140. package/dist/src/schema/zod-validation.mjs +50 -0
  141. package/dist/src/serve/executor-registry.js +297 -20
  142. package/dist/src/serve/executor-token.d.mts +1 -0
  143. package/dist/src/serve/executor-token.mjs +8 -0
  144. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  145. package/dist/src/serve/worker-invocation.js +114 -15
  146. package/dist/src/sessions/adapters/claude.js +19 -18
  147. package/dist/src/sessions/adapters/codex.js +10 -9
  148. package/dist/src/sessions/adapters/copilot.js +14 -13
  149. package/dist/src/sessions/adapters/cursor.js +19 -18
  150. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  151. package/dist/src/sessions/adapters/factory.js +11 -10
  152. package/dist/src/sessions/adapters/generic.js +32 -23
  153. package/dist/src/sessions/adapters/grokbot.js +35 -35
  154. package/dist/src/sessions/adapters/hermes.js +20 -19
  155. package/dist/src/sessions/adapters/muse.js +50 -49
  156. package/dist/src/sessions/adapters/omp.js +5 -4
  157. package/dist/src/sessions/adapters/openclaw.js +30 -29
  158. package/dist/src/sessions/adapters/opencode.js +26 -26
  159. package/dist/src/sessions/adapters/openhuman.js +24 -23
  160. package/dist/src/sessions/adapters/pi.js +5 -4
  161. package/dist/src/sessions/adapters/windsurf.js +8 -8
  162. package/dist/src/sessions/analytics.js +10 -9
  163. package/dist/src/sessions/candidates.js +7 -6
  164. package/dist/src/sessions/contracts.js +63 -53
  165. package/dist/src/sessions/output-registration.js +11 -10
  166. package/dist/src/sessions/workspace-discovery.js +3 -4
  167. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  168. package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
  169. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  170. package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
  171. package/dist/src/update/checker.d.mts +4 -0
  172. package/dist/src/update/checker.mjs +14 -0
  173. package/dist/src/writing/exemplar-selection.js +6 -6
  174. package/dist/src/writing/voice-evaluation.js +24 -23
  175. package/dist/src/writing/writer-migration.js +14 -13
  176. package/dist/src/writing/writer-profile.js +19 -18
  177. package/dist/src/writing/writing-brief.js +18 -17
  178. package/dist/src/writing/writing-receipt.js +47 -46
  179. package/package.json +5 -2
  180. package/schemas/decision/DecisionReview.schema.json +78 -1
  181. package/schemas/mission-v1.schema.json +3 -0
  182. package/tools/agents/deploy-agents.mjs +67 -39
  183. package/tools/agents/providers/antigravity.mjs +6 -1
  184. package/tools/agents/providers/base.mjs +19 -13
  185. package/tools/agents/providers/claude.mjs +1 -1
  186. package/tools/agents/providers/codex.mjs +50 -56
  187. package/tools/agents/providers/copilot.mjs +8 -2
  188. package/tools/agents/providers/cursor.mjs +1 -1
  189. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  190. package/tools/agents/providers/factory.mjs +7 -1
  191. package/tools/agents/providers/grok-build.mjs +18 -2
  192. package/tools/agents/providers/grokbot.mjs +1 -1
  193. package/tools/agents/providers/hermes.mjs +7 -13
  194. package/tools/agents/providers/muse-hooks.mjs +21 -14
  195. package/tools/agents/providers/muse.mjs +7 -2
  196. package/tools/agents/providers/omp.mjs +28 -4
  197. package/tools/agents/providers/opencode.mjs +14 -5
  198. package/tools/agents/providers/openhuman.mjs +1 -1
  199. package/tools/agents/providers/pi.mjs +1 -1
  200. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  201. package/tools/agents/providers/rule-delivery.mjs +263 -71
  202. package/tools/agents/providers/windsurf.mjs +7 -0
  203. package/tools/cli/workspace-status.mjs +5 -0
  204. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  205. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  206. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  207. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  208. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  209. package/tools/ralph-external/process-monitor.mjs +7 -4
  210. package/tools/ralph-external/session-launcher.mjs +280 -32
  211. package/tools/skills/deploy-skills-codex.mjs +11 -7
  212. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -2,34 +2,35 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameS
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { dirname, relative, resolve, sep } from 'node:path';
4
4
  import { z } from 'zod';
5
+ import { integer } from '../schema/zod-validation.mjs';
5
6
  import { SessionContractError, sha256 } from './contracts.js';
6
7
  const DigestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/);
7
8
  const ReferenceSchema = z.string().min(1).max(512).refine(value => !/[\r\n\0]/.test(value), 'references must be single-line inert locators');
8
- export const OutputSourceReferenceSchema = z.object({
9
+ export const OutputSourceReferenceSchema = z.strictObject({
9
10
  kind: z.enum(['file', 'url', 'note', 'session', 'artifact', 'context-pack']),
10
11
  ref: ReferenceSchema,
11
12
  digest: DigestSchema.nullable().default(null),
12
- span: z.object({
13
- start: z.number().int().nonnegative(),
14
- end: z.number().int().positive(),
13
+ span: z.strictObject({
14
+ start: integer().nonnegative(),
15
+ end: integer().positive(),
15
16
  quoteDigest: DigestSchema,
16
- }).strict().nullable().default(null),
17
- }).strict().superRefine((value, context) => {
17
+ }).nullable().default(null),
18
+ }).superRefine((value, context) => {
18
19
  if (value.span && value.span.end <= value.span.start) {
19
20
  context.addIssue({ code: z.ZodIssueCode.custom, message: 'source span end must exceed start' });
20
21
  }
21
22
  });
22
- export const OutputRegistrationRequestSchema = z.object({
23
+ export const OutputRegistrationRequestSchema = z.strictObject({
23
24
  outputPath: z.string().min(1),
24
25
  mediaType: z.string().min(1).max(128),
25
- contextPack: z.object({
26
+ contextPack: z.strictObject({
26
27
  id: ReferenceSchema,
27
28
  digest: DigestSchema,
28
29
  sources: z.array(OutputSourceReferenceSchema).min(1).max(256),
29
- }).strict(),
30
+ }),
30
31
  supersedes: z.array(ReferenceSchema).max(128).default([]),
31
32
  conflictsWith: z.array(ReferenceSchema).max(128).default([]),
32
- }).strict();
33
+ });
33
34
  /**
34
35
  * Minimal incremental index sink. Each registration is independently atomic,
35
36
  * discoverable, and replay-safe; corpus-wide index builders can consume these
@@ -466,13 +466,12 @@ async function rediscoverManifestIdentity(manifest) {
466
466
  }
467
467
  async function codexDiscoveryRoots(options) {
468
468
  const explicit = [...(options.codexRoots ?? []), ...(options.codexRoot ? [options.codexRoot] : [])];
469
- if (explicit.length > 0)
470
- return [...new Set(explicit.map((root) => resolve(root)))];
469
+ const roots = [...new Set(explicit.map((root) => resolve(root)))];
471
470
  // Shared stores require the same explicit provider-home authorization as the default root.
472
471
  if (!options.providerHome)
473
- return [];
472
+ return roots;
474
473
  const codexHome = join(resolve(options.providerHome), '.codex');
475
- const roots = [join(codexHome, 'sessions')];
474
+ roots.push(join(codexHome, 'sessions'));
476
475
  if (process.env.CODEX_HOME)
477
476
  roots.push(resolve(process.env.CODEX_HOME, 'sessions'));
478
477
  try {
@@ -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);
@@ -14,11 +14,14 @@ import { buildAiwgMdContent } from './aiwg-md.js';
14
14
  import { buildNormalizedAiwgMd } from './finalization.js';
15
15
  import { getProviderDefinition, listProviderDefinitions, } from '../../providers/provider-definitions.js';
16
16
  import { buildMuseBridgeText } from './muse-bridge.js';
17
- import { readAiwgConfig } from '../../config/aiwg-config.js';
17
+ import { readAiwgConfig, readAiwgConfigCandidate } from '../../config/aiwg-config.js';
18
+ import { resolveRulesInlineBudget } from '../../rules/inline-budget.mjs';
18
19
  import { projectAiwgPath, projectControlPath, resolveProjectAiwgDir, } from '../../config/project-artifacts.js';
19
20
  import { dominantLineEnding, withLineEnding } from './line-endings.js';
20
21
  export const WORKSPACE_MANAGED_START = '<!-- AIWG:workspace-context:start -->';
21
22
  export const WORKSPACE_MANAGED_END = '<!-- AIWG:workspace-context:end -->';
23
+ export const RESIDENT_MEMORY_START = '<!-- AIWG:resident-memory:start -->';
24
+ export const RESIDENT_MEMORY_END = '<!-- AIWG:resident-memory:end -->';
22
25
  export const WORKSPACE_OPERATOR_START = '<!-- AIWG:workspace-operator:start -->';
23
26
  export const WORKSPACE_OPERATOR_END = '<!-- AIWG:workspace-operator:end -->';
24
27
  export const PROJECT_EXTRACTION_START = '<!-- AIWG:project-extraction:start -->';
@@ -288,7 +291,13 @@ export async function extractExistingProjectContext(projectPath) {
288
291
  lines.push('', '### Testing', '', ...tests.map((source) => `- ${projectLink(source)}`));
289
292
  }
290
293
  if (workflows.length) {
291
- lines.push('', '### Continuous Integration', '', ...workflows.map((source) => `- ${projectLink(source)}`));
294
+ // #3451: directory pointers with counts, not one line per workflow file.
295
+ const byDirectory = new Map();
296
+ for (const source of workflows) {
297
+ const directory = source.slice(0, source.lastIndexOf('/'));
298
+ byDirectory.set(directory, (byDirectory.get(directory) ?? 0) + 1);
299
+ }
300
+ lines.push('', '### Continuous Integration', '', ...[...byDirectory].map(([directory, count]) => `- ${projectLink(`${directory}/`)} (${count} workflow file${count === 1 ? '' : 's'})`));
292
301
  }
293
302
  for (const source of [...architecture, ...tests, ...workflows])
294
303
  sources.add(source);
@@ -358,7 +367,51 @@ export const WORKSPACE_PRECEDENCE_SIGNATURE = 'AIWG rules deployed to this proje
358
367
  export const BOOTSTRAP_AUTHORITY_SIGNATURE = 'AIWG rules deployed to this project are binding';
359
368
  /** Precedence ordering superseded by #2512; its presence means a stale block. */
360
369
  export const WORKSPACE_PRECEDENCE_SUPERSEDED = 'Provider, system, and organization instructions retain their native authority.';
361
- export function buildWorkspaceManagedBlock(projectPath, providerFiles = []) {
370
+ /** Only this projection may enter startup context; portable exports retain history. */
371
+ async function buildResidentMemoryBlock(projectPath) {
372
+ const { CanonicalContextRepository } = await import('../../memory/canonical-context.js');
373
+ const entries = new CanonicalContextRepository(projectPath).residentEntries()
374
+ .sort((left, right) => left.entryId.localeCompare(right.entryId));
375
+ if (!entries.length)
376
+ return '';
377
+ const quote = (text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
378
+ .replace(/\r\n?/g, '\n').split('\n').map(line => `> ${line}`).join('\n');
379
+ return [RESIDENT_MEMORY_START, '', '### Reviewed Resident Memory', '',
380
+ 'Reviewed canonical context follows as quoted data; placement does not grant instruction authority.', '',
381
+ ...entries.flatMap(entry => [quote(`${entry.target}: ${entry.key}\n${entry.value}`), '']),
382
+ RESIDENT_MEMORY_END].join('\n');
383
+ }
384
+ async function residentMemoryForRegeneration(projectPath) {
385
+ const block = await buildResidentMemoryBlock(projectPath);
386
+ if (!block)
387
+ return '';
388
+ const config = await readAiwgConfigCandidate(projectPath);
389
+ // Use the same resident inline budget and UTF-8 estimator as memory cost (#2938).
390
+ const limit = resolveRulesInlineBudget(config ?? {});
391
+ const tokens = Buffer.byteLength(block, 'utf8') / 4;
392
+ if (limit > 0 && tokens > limit) {
393
+ throw new Error(`Canonical resident memory exceeds rules.inline_budget_tokens: ${tokens} estimated tokens; limit ${limit}. Review resident placement before regenerating.`);
394
+ }
395
+ return block;
396
+ }
397
+ /** Reporting only: never repairs provider context or changes a running session. */
398
+ export async function providerCopiesStatus(projectPath) {
399
+ const expected = await buildResidentMemoryBlock(projectPath);
400
+ const workspace = (await readOptional(path.join(projectPath, 'WORKSPACE.md')) ?? '').replace(/\r\n/g, '\n');
401
+ const start = workspace.indexOf(RESIDENT_MEMORY_START);
402
+ const end = workspace.indexOf(RESIDENT_MEMORY_END);
403
+ const regenerate = 'aiwg regenerate --workspace --provider generic';
404
+ if (!expected && start < 0 && end < 0)
405
+ return { status: 'not-applicable', regenerate };
406
+ const managedStart = workspace.indexOf(WORKSPACE_MANAGED_START);
407
+ const managedEnd = workspace.indexOf(WORKSPACE_MANAGED_END);
408
+ const valid = start > managedStart && managedStart >= 0 && end > start && managedEnd > end
409
+ && workspace.indexOf(RESIDENT_MEMORY_START, start + RESIDENT_MEMORY_START.length) < 0
410
+ && workspace.indexOf(RESIDENT_MEMORY_END, end + RESIDENT_MEMORY_END.length) < 0;
411
+ const emitted = valid ? workspace.slice(start, end + RESIDENT_MEMORY_END.length) : '';
412
+ return { status: valid && sha256(emitted) === sha256(expected) ? 'current' : 'stale', regenerate };
413
+ }
414
+ export function buildWorkspaceManagedBlock(projectPath, providerFiles = [], residentMemory = '') {
362
415
  const links = workspaceLinks(projectPath, providerFiles);
363
416
  return [
364
417
  WORKSPACE_MANAGED_START,
@@ -404,17 +457,18 @@ export function buildWorkspaceManagedBlock(projectPath, providerFiles = []) {
404
457
  '',
405
458
  ...links.map((link) => `- ${link}`),
406
459
  '',
460
+ ...(residentMemory ? [residentMemory, ''] : []),
407
461
  WORKSPACE_MANAGED_END,
408
462
  ].join('\n');
409
463
  }
410
- export function buildWorkspaceDocument(projectPath, operatorContent = '', providerFiles = []) {
464
+ export function buildWorkspaceDocument(projectPath, operatorContent = '', providerFiles = [], residentMemory = '') {
411
465
  const content = operatorContent.trim() || PROJECT_CONTEXT_PLACEHOLDER;
412
466
  return [
413
467
  '# WORKSPACE.md',
414
468
  WORKSPACE_SIGNATURE,
415
469
  '<!-- Generated structure by AIWG; operator content is protected by markers. -->',
416
470
  '',
417
- buildWorkspaceManagedBlock(projectPath, providerFiles),
471
+ buildWorkspaceManagedBlock(projectPath, providerFiles, residentMemory),
418
472
  '',
419
473
  WORKSPACE_OPERATOR_START,
420
474
  '',
@@ -433,11 +487,12 @@ export async function ensureWorkspaceContext(projectPath, options = {}) {
433
487
  // so their links survive every regeneration instead of only the one that
434
488
  // wrote them (#2558).
435
489
  const providerFiles = options.providerFiles ?? await migratedProviderFiles(projectPath);
490
+ const residentMemory = await residentMemoryForRegeneration(projectPath);
436
491
  if (existing === null) {
437
- await atomicWrite(workspacePath, buildWorkspaceDocument(projectPath, '', providerFiles));
492
+ await atomicWrite(workspacePath, buildWorkspaceDocument(projectPath, '', providerFiles, residentMemory));
438
493
  return { path: workspacePath, action: 'created', warnings };
439
494
  }
440
- const block = buildWorkspaceManagedBlock(projectPath, providerFiles);
495
+ const block = buildWorkspaceManagedBlock(projectPath, providerFiles, residentMemory);
441
496
  if (existing.includes(WORKSPACE_MANAGED_START) || existing.includes(WORKSPACE_MANAGED_END)) {
442
497
  let updated = replaceBlock(existing, WORKSPACE_MANAGED_START, WORKSPACE_MANAGED_END, block);
443
498
  // A managed project-extraction block explicitly points operators back to
@@ -459,7 +514,7 @@ export async function ensureWorkspaceContext(projectPath, options = {}) {
459
514
  }
460
515
  const backupPath = `${workspacePath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
461
516
  await fs.writeFile(backupPath, existing, 'utf8');
462
- await atomicWrite(workspacePath, buildWorkspaceDocument(projectPath, existing, providerFiles));
517
+ await atomicWrite(workspacePath, buildWorkspaceDocument(projectPath, existing, providerFiles, residentMemory));
463
518
  return { path: workspacePath, action: 'updated', backupPath, warnings };
464
519
  }
465
520
  export function buildProviderBootstrapBlock(provider) {
@@ -526,9 +581,50 @@ export function buildProviderBootstrapBlock(provider) {
526
581
  PROVIDER_BOOTSTRAP_END,
527
582
  ].join('\n');
528
583
  }
584
+ export const PROVIDER_BOOTSTRAP_HEADING = '# Provider workspace bootstrap';
585
+ /**
586
+ * Collapse repeated bootstrap headings and repeated managed bootstrap blocks.
587
+ *
588
+ * Root cause of the duplicated "# Provider workspace bootstrap" (#3451): the
589
+ * bootstrap *block* already carries its own heading, and the whole-file
590
+ * wrapper used to prepend a second one. Regeneration then replaced only the
591
+ * block, so the wrapper heading survived next to the block heading. The
592
+ * wrapper no longer emits it; this repairs files that already have it and
593
+ * keeps regeneration idempotent. Operator content is never touched.
594
+ */
595
+ export function dedupeProviderBootstrap(content) {
596
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
597
+ let result = content;
598
+ // A heading line that precedes the managed block's own heading is the
599
+ // wrapper copy: drop it (and only it) from the top of the file.
600
+ const lines = result.split(/\r?\n/);
601
+ const headingIdx = lines.reduce((acc, line, i) => {
602
+ if (line.trimEnd() === PROVIDER_BOOTSTRAP_HEADING)
603
+ acc.push(i);
604
+ return acc;
605
+ }, []);
606
+ if (headingIdx.length >= 2 && headingIdx[0] === 0) {
607
+ lines.splice(0, 1);
608
+ result = lines.join(eol);
609
+ }
610
+ // Identical managed bootstrap blocks repeated back-to-back or elsewhere:
611
+ // keep the first.
612
+ if (result.split(PROVIDER_BOOTSTRAP_START).length > 2) {
613
+ const first = result.indexOf(PROVIDER_BOOTSTRAP_START);
614
+ const firstEnd = result.indexOf(PROVIDER_BOOTSTRAP_END, first);
615
+ if (first >= 0 && firstEnd > first) {
616
+ const blockEnd = firstEnd + PROVIDER_BOOTSTRAP_END.length;
617
+ const firstBlock = result.slice(first, blockEnd);
618
+ const head = result.slice(0, blockEnd);
619
+ const tail = result.slice(blockEnd).split(firstBlock).join('');
620
+ result = head + tail.replace(new RegExp(`(?:${eol}){3,}`, 'g'), eol + eol);
621
+ }
622
+ }
623
+ return result;
624
+ }
529
625
  export function buildProviderBootstrapFile(provider) {
626
+ // No heading here: the block below owns "# Provider workspace bootstrap".
530
627
  return [
531
- '# Provider workspace bootstrap',
532
628
  WORKSPACE_SIGNATURE,
533
629
  '<!-- Generated by AIWG. Project/operator context belongs in WORKSPACE.md. -->',
534
630
  '',
@@ -916,7 +1012,7 @@ async function stageMigrationWrites(projectPath, audit, options = {}) {
916
1012
  const extracted = await extractExistingProjectContext(projectPath);
917
1013
  workspaceOperatorContent = mergeProjectExtraction(workspaceOperatorContent, extracted.content);
918
1014
  }
919
- writes.set('WORKSPACE.md', buildWorkspaceDocument(projectPath, workspaceOperatorContent, providerFiles));
1015
+ writes.set('WORKSPACE.md', buildWorkspaceDocument(projectPath, workspaceOperatorContent, providerFiles, await residentMemoryForRegeneration(projectPath)));
920
1016
  const providers = await configuredProviders(projectPath);
921
1017
  const targetProviders = new Map();
922
1018
  for (const provider of providers) {
@@ -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
+ }
@@ -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) {