@dzhechkov/harness-core 0.8.5 → 0.8.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 (50) hide show
  1. package/.dz-manifest.json +49 -49
  2. package/README.md +6 -1
  3. package/dist/feature-adr-checkpoints.d.ts +11 -2
  4. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  5. package/dist/feature-adr-checkpoints.js +37 -2
  6. package/dist/feature-adr-checkpoints.js.map +1 -1
  7. package/dist/feature-adr-routing.d.ts +56 -21
  8. package/dist/feature-adr-routing.d.ts.map +1 -1
  9. package/dist/feature-adr-routing.js +187 -58
  10. package/dist/feature-adr-routing.js.map +1 -1
  11. package/dist/guard.d.ts +25 -0
  12. package/dist/guard.d.ts.map +1 -1
  13. package/dist/guard.js +59 -1
  14. package/dist/guard.js.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/loop-blobs.generated.js +8 -8
  20. package/dist/loop-blobs.generated.js.map +1 -1
  21. package/dist/loop-plan.d.ts +13 -1
  22. package/dist/loop-plan.d.ts.map +1 -1
  23. package/dist/loop-plan.js +15 -1
  24. package/dist/loop-plan.js.map +1 -1
  25. package/dist/loop-render.d.ts.map +1 -1
  26. package/dist/loop-render.js +51 -6
  27. package/dist/loop-render.js.map +1 -1
  28. package/dist/loop-trace.d.ts +20 -1
  29. package/dist/loop-trace.d.ts.map +1 -1
  30. package/dist/loop-trace.js +83 -1
  31. package/dist/loop-trace.js.map +1 -1
  32. package/dist/model-recommender.d.ts +8 -0
  33. package/dist/model-recommender.d.ts.map +1 -1
  34. package/dist/model-recommender.js +31 -4
  35. package/dist/model-recommender.js.map +1 -1
  36. package/dist/workflow-run.d.ts.map +1 -1
  37. package/dist/workflow-run.js +18 -12
  38. package/dist/workflow-run.js.map +1 -1
  39. package/package.json +18 -15
  40. package/sbom.json +48 -48
  41. package/src/feature-adr-checkpoints.ts +38 -2
  42. package/src/feature-adr-routing.ts +217 -54
  43. package/src/guard.ts +79 -1
  44. package/src/index.ts +2 -1
  45. package/src/loop-blobs.generated.ts +8 -8
  46. package/src/loop-plan.ts +36 -3
  47. package/src/loop-render.ts +50 -6
  48. package/src/loop-trace.ts +91 -2
  49. package/src/model-recommender.ts +35 -4
  50. package/src/workflow-run.ts +18 -11
@@ -42,7 +42,7 @@ export const BLOB_COVERAGE_MANIFEST: { coveredWorkflows: string[] } = {
42
42
  export const BLOBS: Record<string, LoopBlob> = {
43
43
  "checkpoints": {
44
44
  name: "checkpoints",
45
- version: "1.1.0",
45
+ version: "1.2.0",
46
46
  contentHash: "a44560c6036fd143b7a3f125fec00fa8b91c3c06ac5b9e1ecfb83d27a5211e9b",
47
47
  sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts",
48
48
  requires: [],
@@ -51,30 +51,30 @@ export const BLOBS: Record<string, LoopBlob> = {
51
51
  },
52
52
  "training-pairs": {
53
53
  name: "training-pairs",
54
- version: "1.1.0",
55
- contentHash: "199eaaa8ce5dba4f54744b4a4f34eea32a3c33d4de8dbbddd3d119b70459361d",
54
+ version: "1.2.0",
55
+ contentHash: "7c836995b72f0b8fc69074cf6bb7b8a58fac609e4f5c365c82b8745c655da39c",
56
56
  sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts",
57
57
  requires: ["checkpoints"],
58
58
  exports: ["TRAINPAIR_SCHEMA_VERSION","TRAINPAIR_MAX_IO_CHARS","trainingPairFamily","trainingPairPath","TRAINPAIR_PRIVACY_NOTE","buildTrainingPair","serializeTrainingPair","trainingPairAppendCmd","decideCaptureMode","captureFailureRecord","trainingPairBackfillCmd","TP_BACKFILL_OK","TP_BACKFILL_SKIP"],
59
- code: "function decideCaptureMode(opts) {\n if (!opts.enabled)\n return 'skip-disabled';\n if (!Number.isInteger(opts.recordCount) || opts.recordCount <= 0)\n return 'skip-empty';\n return opts.resumed ? 'backfill' : 'capture';\n}\nfunction captureFailureRecord(stage, mode, reason, detail) {\n const normalizedStage = typeof stage === 'string' && stage.trim() !== '' ? stage : 'unknown';\n const normalizedMode = mode === 'capture' || mode === 'backfill' || mode === 'skip-disabled' || mode === 'skip-empty'\n ? mode\n : null;\n const normalizedReason = reason === 'threw' || reason === 'unserializable' || reason === 'unverified' || reason === 'backfill-unverified' || reason === 'empty-output'\n ? reason\n : 'threw';\n let normalizedDetail = null;\n if (detail !== null && detail !== undefined) {\n try {\n const text = String(detail);\n if (text !== '')\n normalizedDetail = text.length > 500 ? text.slice(0, 500) + '…' : text;\n }\n catch {\n normalizedDetail = null;\n }\n }\n return { stage: normalizedStage, mode: normalizedMode, reason: normalizedReason, detail: normalizedDetail };\n}\nconst TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-2';\nconst TRAINPAIR_MAX_IO_CHARS = 48000;\nfunction trainingPairFamily(spec) {\n return /codex|gpt|openai/i.test(String(spec ?? '')) ? 'codex' : 'claude';\n}\nfunction trainingPairPath(slug, stage) {\n return '.dz/fa-training/' + slug + '/' + stage + '.jsonl';\n}\nconst TRAINPAIR_PRIVACY_NOTE = \"feature-adr TRAINING PAIRS (backlog 70e0f083): per-stage SFT records - STAGE INPUT (full prompt/context) -> STAGE OUTPUT (artifact/result) -> EVALUATION (QE grade + injected lessons) with model+family provenance; one JSONL file per stage per slug. PRIVACY: pairs may contain TARGET-REPO CODE and full prompts. This directory is NOT gitignored yet by explicit owner decision - review contents before sharing or publishing anything that embeds it. ts is the CAPTURE time. On a record with captureMode: 'backfill' that is the RECONSTRUCTION time, NOT the stage's observation time — the original stage's timing lives in that run's .fa-state checkpoint.\";\nconst TP_PROFILE_MARKER_START = '<!-- dz:profile:start -->';\nconst TP_PROFILE_MARKER_END = '<!-- dz:profile:end -->';\nconst TP_PROFILE_REDACTED = '[dz:profile REDACTED]';\nfunction redactProfileBlock(text) {\n if (typeof text !== 'string' || text === '')\n return typeof text === 'string' ? text : '';\n let out = '';\n let rest = text;\n for (;;) {\n const start = rest.indexOf(TP_PROFILE_MARKER_START);\n if (start === -1)\n return out + rest;\n out += rest.slice(0, start) + TP_PROFILE_REDACTED;\n const end = rest.indexOf(TP_PROFILE_MARKER_END, start + TP_PROFILE_MARKER_START.length);\n if (end === -1)\n return out;\n rest = rest.slice(end + TP_PROFILE_MARKER_END.length);\n }\n}\nfunction coerceText(v) {\n if (typeof v === 'string')\n return v;\n if (v === null || v === undefined)\n return '';\n try {\n const s = JSON.stringify(v);\n return typeof s === 'string' ? s : String(v);\n }\n catch {\n return String(v);\n }\n}\nfunction buildTrainingPair(opts) {\n let input = redactProfileBlock(coerceText(opts.input));\n let output = redactProfileBlock(coerceText(opts.output));\n let truncated = null;\n if (input.length + output.length > TRAINPAIR_MAX_IO_CHARS) {\n truncated = { inputChars: input.length, outputChars: output.length, inputHash: fnv1a64(input), outputHash: fnv1a64(output) };\n const half = Math.floor(TRAINPAIR_MAX_IO_CHARS / 2);\n let inKeep = input.length;\n let outKeep = output.length;\n if (outKeep <= half)\n inKeep = TRAINPAIR_MAX_IO_CHARS - outKeep;\n else if (inKeep <= half)\n outKeep = TRAINPAIR_MAX_IO_CHARS - inKeep;\n else {\n inKeep = half;\n outKeep = TRAINPAIR_MAX_IO_CHARS - half;\n }\n if (inKeep < input.length)\n input = input.slice(0, inKeep) + '\\n…[TRUNCATED ' + (truncated.inputChars - inKeep) + ' chars — full-text fnv1a64=' + truncated.inputHash + ']';\n if (outKeep < output.length)\n output = output.slice(0, outKeep) + '\\n…[TRUNCATED ' + (truncated.outputChars - outKeep) + ' chars — full-text fnv1a64=' + truncated.outputHash + ']';\n }\n const ev = opts.evaluation || {};\n const pv = opts.provenance || {};\n return {\n schema: TRAINPAIR_SCHEMA_VERSION,\n slug: opts.slug,\n stage: opts.stage,\n ts: opts.ts === undefined ? null : opts.ts,\n input,\n output,\n evaluation: {\n grade: typeof ev.grade === 'string' && ev.grade.trim() !== '' ? ev.grade : null,\n gradedBy: typeof ev.gradedBy === 'string' && ev.gradedBy !== '' ? ev.gradedBy : null,\n lessonsInjected: Array.isArray(ev.lessonsInjected) ? ev.lessonsInjected.filter((s) => typeof s === 'string' && s !== '') : [],\n },\n provenance: {\n model: typeof pv.model === 'string' && pv.model !== '' ? pv.model : 'unknown',\n family: pv.family === 'claude' || pv.family === 'codex' ? pv.family : trainingPairFamily(pv.model),\n role: typeof pv.role === 'string' && pv.role !== '' ? pv.role : 'unknown',\n tokens: typeof pv.tokens === 'number' && Number.isFinite(pv.tokens) ? pv.tokens : null,\n minutes: typeof pv.minutes === 'number' && Number.isFinite(pv.minutes) ? pv.minutes : null,\n },\n truncated,\n captureMode: opts.captureMode === 'backfill' ? 'backfill' : 'capture',\n resumed: opts.resumed === true,\n };\n}\nfunction serializeTrainingPair(pair) {\n try {\n const line = JSON.stringify(pair);\n return typeof line === 'string' ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction trainingPairAppendCmd(repoAbs, slug, stage, line) {\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n \" && printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs));\n}\nconst TP_BACKFILL_OK = 'TP-BACKFILL-OK';\nconst TP_BACKFILL_SKIP = 'TP-BACKFILL-SKIP';\nconst TP_BACKFILL_DUP = 'TP-BACKFILL-DUP';\nfunction trainingPairBackfillCmd(repoAbs, slug, stage, lines, markKey) {\n if (typeof repoAbs !== 'string' || repoAbs === '')\n return null;\n if (typeof slug !== 'string' || slug === '')\n return null;\n if (typeof stage !== 'string' || stage === '')\n return null;\n if (!Array.isArray(lines) || lines.length === 0 || !lines.every(line => typeof line === 'string' && line !== ''))\n return null;\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n const markDir = repoAbs + '/.dz/fa-training/.backfill-marks';\n const markStage = stage.replace(/\\.\\./g, '_').replace(/\\//g, '_');\n const resolvedMarkKey = markKey === undefined ? fnv1a64(stage + '\\0' + lines.join('\\n')) : markKey;\n const markPath = markDir + '/' + markStage + '-' + resolvedMarkKey;\n const appends = lines\n .map(line => \"printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs))\n .join(' && ');\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n ' && mkdir -p ' + shellQuote(markDir) +\n ' && if mkdir ' + shellQuote(markPath) + ' 2>/dev/null; then ' +\n 'if [ -f ' + shellQuote(fileAbs) + ' ]; then echo ' + shellQuote(TP_BACKFILL_SKIP) +\n '; else { ' + appends + ' && echo ' + shellQuote(TP_BACKFILL_OK) + '; } || { rmdir ' + shellQuote(markPath) + ' 2>/dev/null; false; }; fi' +\n '; else echo ' + shellQuote(TP_BACKFILL_DUP) + '; fi');\n}",
59
+ code: "function decideCaptureMode(opts) {\n if (!opts.enabled)\n return 'skip-disabled';\n if (!Number.isInteger(opts.recordCount) || opts.recordCount <= 0)\n return 'skip-empty';\n return opts.resumed ? 'backfill' : 'capture';\n}\nfunction captureFailureRecord(stage, mode, reason, detail) {\n const normalizedStage = typeof stage === 'string' && stage.trim() !== '' ? stage : 'unknown';\n const normalizedMode = mode === 'capture' || mode === 'backfill' || mode === 'skip-disabled' || mode === 'skip-empty'\n ? mode\n : null;\n const normalizedReason = reason === 'threw' || reason === 'unserializable' || reason === 'unverified' || reason === 'backfill-unverified' || reason === 'empty-output'\n ? reason\n : 'threw';\n let normalizedDetail = null;\n if (detail !== null && detail !== undefined) {\n try {\n const text = String(detail);\n if (text !== '')\n normalizedDetail = text.length > 500 ? text.slice(0, 500) + '…' : text;\n }\n catch {\n normalizedDetail = null;\n }\n }\n return { stage: normalizedStage, mode: normalizedMode, reason: normalizedReason, detail: normalizedDetail };\n}\nconst TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-3';\nconst TRAINPAIR_MAX_IO_CHARS = 48000;\nfunction trainingPairFamily(spec) {\n return /codex|gpt|openai/i.test(String(spec ?? '')) ? 'codex' : 'claude';\n}\nfunction trainingPairPath(slug, stage) {\n return '.dz/fa-training/' + slug + '/' + stage + '.jsonl';\n}\nconst TRAINPAIR_PRIVACY_NOTE = \"feature-adr TRAINING PAIRS (backlog 70e0f083): per-stage SFT records - STAGE INPUT (full prompt/context) -> STAGE OUTPUT (artifact/result) -> EVALUATION (QE grade + injected lessons) with model+family provenance; one JSONL file per stage per slug. PRIVACY: pairs may contain TARGET-REPO CODE and full prompts. This directory is NOT gitignored yet by explicit owner decision - review contents before sharing or publishing anything that embeds it. ts is the CAPTURE time. On a record with captureMode: 'backfill' that is the RECONSTRUCTION time, NOT the stage's observation time — the original stage's timing lives in that run's .fa-state checkpoint.\";\nconst TP_PROFILE_MARKER_START = '<!-- dz:profile:start -->';\nconst TP_PROFILE_MARKER_END = '<!-- dz:profile:end -->';\nconst TP_PROFILE_REDACTED = '[dz:profile REDACTED]';\nfunction redactProfileBlock(text) {\n if (typeof text !== 'string' || text === '')\n return typeof text === 'string' ? text : '';\n let out = '';\n let rest = text;\n for (;;) {\n const start = rest.indexOf(TP_PROFILE_MARKER_START);\n if (start === -1)\n return out + rest;\n out += rest.slice(0, start) + TP_PROFILE_REDACTED;\n const end = rest.indexOf(TP_PROFILE_MARKER_END, start + TP_PROFILE_MARKER_START.length);\n if (end === -1)\n return out;\n rest = rest.slice(end + TP_PROFILE_MARKER_END.length);\n }\n}\nfunction coerceText(v) {\n if (typeof v === 'string')\n return v;\n if (v === null || v === undefined)\n return '';\n try {\n const s = JSON.stringify(v);\n return typeof s === 'string' ? s : String(v);\n }\n catch {\n return String(v);\n }\n}\nfunction normalizeTrainingPairBudget(raw) {\n try {\n if (raw === undefined)\n return { primary: 'claude', claude: 'normal', codex: 'normal', preset: 'unset' };\n if (raw === null || typeof raw !== 'object')\n return null;\n const value = raw;\n const primary = value.primary;\n const claude = value.claude;\n const codex = value.codex;\n if (primary !== 'claude' && primary !== 'codex')\n return null;\n if (claude !== 'normal' && claude !== 'eco')\n return null;\n if (codex !== 'normal' && codex !== 'eco')\n return null;\n if (value.preset === 'unset')\n return { primary, claude, codex, preset: 'unset' };\n let preset = 'custom';\n if (claude === 'normal' && codex === 'normal')\n preset = 'normal';\n else if (claude === 'eco' && codex === 'eco')\n preset = 'eco';\n else if (claude === 'eco' && codex === 'normal')\n preset = 'hybrid';\n return { primary, claude, codex, preset };\n }\n catch {\n return null;\n }\n}\nfunction buildTrainingPair(opts) {\n let input = redactProfileBlock(coerceText(opts.input));\n let output = redactProfileBlock(coerceText(opts.output));\n let truncated = null;\n if (input.length + output.length > TRAINPAIR_MAX_IO_CHARS) {\n truncated = { inputChars: input.length, outputChars: output.length, inputHash: fnv1a64(input), outputHash: fnv1a64(output) };\n const half = Math.floor(TRAINPAIR_MAX_IO_CHARS / 2);\n let inKeep = input.length;\n let outKeep = output.length;\n if (outKeep <= half)\n inKeep = TRAINPAIR_MAX_IO_CHARS - outKeep;\n else if (inKeep <= half)\n outKeep = TRAINPAIR_MAX_IO_CHARS - inKeep;\n else {\n inKeep = half;\n outKeep = TRAINPAIR_MAX_IO_CHARS - half;\n }\n if (inKeep < input.length)\n input = input.slice(0, inKeep) + '\\n…[TRUNCATED ' + (truncated.inputChars - inKeep) + ' chars — full-text fnv1a64=' + truncated.inputHash + ']';\n if (outKeep < output.length)\n output = output.slice(0, outKeep) + '\\n…[TRUNCATED ' + (truncated.outputChars - outKeep) + ' chars — full-text fnv1a64=' + truncated.outputHash + ']';\n }\n const ev = opts.evaluation || {};\n const pv = opts.provenance || {};\n return {\n schema: TRAINPAIR_SCHEMA_VERSION,\n slug: opts.slug,\n stage: opts.stage,\n ts: opts.ts === undefined ? null : opts.ts,\n input,\n output,\n evaluation: {\n grade: typeof ev.grade === 'string' && ev.grade.trim() !== '' ? ev.grade : null,\n gradedBy: typeof ev.gradedBy === 'string' && ev.gradedBy !== '' ? ev.gradedBy : null,\n lessonsInjected: Array.isArray(ev.lessonsInjected) ? ev.lessonsInjected.filter((s) => typeof s === 'string' && s !== '') : [],\n },\n provenance: {\n model: typeof pv.model === 'string' && pv.model !== '' ? pv.model : 'unknown',\n family: pv.family === 'claude' || pv.family === 'codex' ? pv.family : trainingPairFamily(pv.model),\n role: typeof pv.role === 'string' && pv.role !== '' ? pv.role : 'unknown',\n tokens: typeof pv.tokens === 'number' && Number.isFinite(pv.tokens) ? pv.tokens : null,\n minutes: typeof pv.minutes === 'number' && Number.isFinite(pv.minutes) ? pv.minutes : null,\n },\n budgetMode: normalizeTrainingPairBudget(opts.budgetMode),\n truncated,\n captureMode: opts.captureMode === 'backfill' ? 'backfill' : 'capture',\n resumed: opts.resumed === true,\n };\n}\nfunction serializeTrainingPair(pair) {\n try {\n const line = JSON.stringify(pair);\n return typeof line === 'string' ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction trainingPairAppendCmd(repoAbs, slug, stage, line) {\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n \" && printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs));\n}\nconst TP_BACKFILL_OK = 'TP-BACKFILL-OK';\nconst TP_BACKFILL_SKIP = 'TP-BACKFILL-SKIP';\nconst TP_BACKFILL_DUP = 'TP-BACKFILL-DUP';\nfunction trainingPairBackfillCmd(repoAbs, slug, stage, lines, markKey) {\n if (typeof repoAbs !== 'string' || repoAbs === '')\n return null;\n if (typeof slug !== 'string' || slug === '')\n return null;\n if (typeof stage !== 'string' || stage === '')\n return null;\n if (!Array.isArray(lines) || lines.length === 0 || !lines.every(line => typeof line === 'string' && line !== ''))\n return null;\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n const markDir = repoAbs + '/.dz/fa-training/.backfill-marks';\n const markStage = stage.replace(/\\.\\./g, '_').replace(/\\//g, '_');\n const resolvedMarkKey = markKey === undefined ? fnv1a64(stage + '\\0' + lines.join('\\n')) : markKey;\n const markPath = markDir + '/' + markStage + '-' + resolvedMarkKey;\n const appends = lines\n .map(line => \"printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs))\n .join(' && ');\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n ' && mkdir -p ' + shellQuote(markDir) +\n ' && if mkdir ' + shellQuote(markPath) + ' 2>/dev/null; then ' +\n 'if [ -f ' + shellQuote(fileAbs) + ' ]; then echo ' + shellQuote(TP_BACKFILL_SKIP) +\n '; else { ' + appends + ' && echo ' + shellQuote(TP_BACKFILL_OK) + '; } || { rmdir ' + shellQuote(markPath) + ' 2>/dev/null; false; }; fi' +\n '; else echo ' + shellQuote(TP_BACKFILL_DUP) + '; fi');\n}",
60
60
  },
61
61
  "model-resolver": {
62
62
  name: "model-resolver",
63
63
  version: "1.0.0",
64
- contentHash: "e82ac2137279537f5b65725cbd8de5817fcbc89571068e2df843587352fa83f5",
64
+ contentHash: "fcf8c1dfc4b5364adb6e532565953ef30d94a45070b2b903411170965a1fb372",
65
65
  sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-routing.ts",
66
66
  requires: [],
67
67
  exports: ["specToOpts","resolveStageModel","KNOWN_CODEX","mergeOpts","stageLabel","modelLabel"],
68
- code: "const OVERRIDE_REASONING = {\n router: 'high',\n requirements: 'xhigh',\n research: 'xhigh',\n adr: 'xhigh',\n ideation: 'xhigh',\n ddd: 'xhigh',\n architecture: 'xhigh',\n plan: 'xhigh',\n code: 'xhigh',\n qe: 'high',\n fleet: 'high',\n};\nfunction topCodexId(env) {\n let top = env.CODEX_MODEL;\n if (top === 'auto') {\n const ids = Object.keys(KNOWN_CODEX);\n for (let i = 0; i < ids.length; i++) {\n if (ids[i] !== 'auto')\n top = ids[i] || top;\n }\n }\n return top;\n}\nconst KNOWN_CODEX = { auto: 1, 'gpt-5.5': 1, 'gpt-5.6': 1, 'gpt-5.6-luna': 1, 'gpt-5.6-terra': 1, 'gpt-5.6-sol': 1 };\nconst CLAUDE_NAMES = { fable: 1, opus: 1, sonnet: 1, haiku: 1 };\nconst VALID_REASONING = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1 };\nconst DEFAULT_MODELS = {\n router: 'fable',\n requirements: 'sonnet',\n research: 'sonnet',\n adr: 'opus',\n ideation: 'sonnet',\n ddd: 'opus',\n architecture: 'opus',\n plan: 'sonnet',\n code: null,\n qe: null,\n fleet: 'sonnet',\n};\nfunction specToOpts(spec, env) {\n const log = env.log || function () { };\n if (!spec)\n return {};\n const parts = String(spec).split(':');\n const head = parts[0] || '';\n if (head === 'codex') {\n let id = parts[1] || env.CODEX_MODEL;\n if (id !== 'auto' && !KNOWN_CODEX[id]) {\n log('models: unknown codex id ' + id + ' — using ' + env.CODEX_MODEL);\n id = env.CODEX_MODEL;\n }\n let reasoning = parts[2] || 'high';\n if (!VALID_REASONING[reasoning]) {\n log('models: unknown reasoning ' + reasoning + ' — using high');\n reasoning = 'high';\n }\n return { agentType: 'codex:codex-rescue', codexModel: id, _reasoning: reasoning };\n }\n if (CLAUDE_NAMES[head])\n return { model: head };\n log('models: unknown spec ' + spec + ' — session-inherited');\n return {};\n}\nfunction resolveCoderSpec(env) {\n if (env.CODER === 'codex' || env.CODER === 'codex-fallback')\n return 'codex:' + env.CODEX_MODEL + ':high';\n return 'opus';\n}\nfunction coderIsCodex(env) {\n if (env.CODER === 'codex' || env.CODER === 'codex-fallback')\n return true;\n const codeSpec = env.MODELS.code;\n if (codeSpec && String(codeSpec).split(':')[0] === 'codex')\n return true;\n return false;\n}\nfunction resolveQeSpec(env) {\n if (coderIsCodex(env))\n return 'opus';\n const CODEX_AVAILABLE = env.codexAvailable !== false;\n if (!CODEX_AVAILABLE)\n return 'opus';\n return 'codex:' + topCodexId(env) + ':high';\n}\nfunction routingRequested(env) {\n return (Object.keys(env.MODELS).length > 0 ||\n env.PLANNER === 'codex' ||\n env.CODER === 'codex' ||\n env.CODER === 'codex-fallback' ||\n env.QE_REVIEWER === 'codex' ||\n env.QE_REVIEWER === 'codex-fallback');\n}\nfunction resolveStageModel(stage, env) {\n if (env.usageOverride) {\n const r = (env.usageReasoning && env.usageReasoning[stage]) || OVERRIDE_REASONING[stage] || 'high';\n const o = specToOpts('codex:' + topCodexId(env) + ':' + r, env);\n o._usageSwitched = true;\n return o;\n }\n let spec = env.MODELS[stage];\n if (spec === undefined) {\n if (!routingRequested(env))\n return {};\n spec = DEFAULT_MODELS[stage];\n }\n if (stage === 'code' && (spec === null || spec === undefined))\n return specToOpts(resolveCoderSpec(env), env);\n if (stage === 'qe' && (spec === null || spec === undefined))\n return specToOpts(resolveQeSpec(env), env);\n return specToOpts(spec, env);\n}\nfunction modelLabel(opts) {\n if (opts && opts.agentType === 'codex:codex-rescue') {\n const base = 'codex:' + opts.codexModel + ':' + opts._reasoning;\n return opts._usageSwitched ? base + ' (usage-switched)' : base;\n }\n if (opts && opts.model)\n return opts.model;\n return 'session';\n}\nfunction stageLabel(base, opts) {\n const m = modelLabel(opts);\n return m === 'session' ? base : base + ' · ' + m;\n}\nfunction mergeOpts(base, extra) {\n const out = {};\n for (const k in base)\n out[k] = base[k];\n for (const k in extra)\n out[k] = extra[k];\n return out;\n}",
68
+ code: "const STAGE_EFFORT = { override: {\n router: 'medium',\n requirements: 'medium',\n research: 'medium',\n adr: 'high',\n ideation: 'medium',\n ddd: 'high',\n architecture: 'high',\n plan: 'high',\n code: 'medium',\n qe: 'high',\n fleet: 'medium',\n } };\nfunction topCodexId(env) {\n return env.CODEX_MODEL !== 'auto' ? env.CODEX_MODEL : CODEX_TIERS.flagship;\n}\nconst KNOWN_CODEX = { auto: 1, 'gpt-5.5': 1, 'gpt-5.6': 1, 'gpt-5.6-luna': 1, 'gpt-5.6-terra': 1, 'gpt-5.6-sol': 1 };\nconst CODEX_TIERS = {\n flagship: 'gpt-5.6-sol',\n workhorse: 'gpt-5.6-terra',\n 'high-volume': 'gpt-5.6-luna',\n};\nfunction codexIdForTier(tier, env) {\n return env.CODEX_MODEL !== 'auto' ? env.CODEX_MODEL : CODEX_TIERS[tier];\n}\nconst BUDGET_PRESETS = {\n normal: { claude: 'normal', codex: 'normal' },\n eco: { claude: 'eco', codex: 'eco' },\n hybrid: { claude: 'eco', codex: 'normal' },\n};\nfunction resolveBudgetMode(raw) {\n if (raw === undefined)\n return BUDGET_PRESETS.normal;\n if (typeof raw === 'string') {\n const preset = BUDGET_PRESETS[raw];\n if (!preset)\n throw new RangeError('budget: unknown preset \"' + raw + '\" — valid: normal|eco|hybrid');\n return preset;\n }\n if (raw && typeof raw === 'object') {\n const value = raw;\n for (const key of Object.keys(value)) {\n if (key !== 'claude' && key !== 'codex') {\n throw new RangeError('budget: unknown family key \"' + key + '\" — valid: claude|codex');\n }\n }\n for (const key of ['claude', 'codex']) {\n const level = value[key];\n if (level !== undefined && level !== 'normal' && level !== 'eco') {\n throw new RangeError('budget.' + key + ': unknown level \"' + level + '\" — valid: normal|eco');\n }\n }\n return {\n claude: value.claude || 'normal',\n codex: value.codex || 'normal',\n };\n }\n throw new RangeError('budget: expected a preset name or {claude,codex} object, got ' + typeof raw);\n}\nconst CLAUDE_NAMES = { fable: 1, opus: 1, sonnet: 1, haiku: 1 };\nconst VALID_REASONING = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1, max: 1 };\nconst DEFAULT_MODELS = {\n router: 'fable',\n requirements: 'sonnet',\n research: 'sonnet',\n adr: 'opus',\n ideation: 'sonnet',\n ddd: 'opus',\n architecture: 'opus',\n plan: 'sonnet',\n code: null,\n qe: null,\n fleet: 'sonnet',\n};\nconst ROUTING_TABLES = {\n claude: {\n claude: {\n normal: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'fable', ideation: 'sonnet', ddd: 'fable', architecture: 'fable', plan: 'opus', code: 'sonnet', fleet: 'sonnet' },\n eco: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'opus', ideation: 'sonnet', ddd: 'opus', architecture: 'opus', plan: 'sonnet', code: 'sonnet', fleet: 'sonnet' },\n },\n codex: { normal: {}, eco: {} },\n },\n codex: {\n claude: {\n normal: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' },\n eco: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' },\n },\n codex: { normal: {}, eco: {} },\n },\n};\nfunction codexCell(tier, effort, env) {\n return 'codex:' + codexIdForTier(tier, env) + ':' + effort;\n}\nfunction budgetTable(primary, mode, env) {\n const claudeHalf = ROUTING_TABLES[primary].claude[mode.claude];\n let codexHalf;\n if (primary === 'claude') {\n const qeSpec = mode.codex === 'normal'\n ? codexCell('flagship', 'high', env)\n : codexCell('workhorse', 'medium', env);\n codexHalf = { ...ROUTING_TABLES.claude.codex[mode.codex], qe: env.codexAvailable === false ? 'opus' : qeSpec };\n }\n else {\n const normal = mode.codex === 'normal';\n const design = codexCell(normal ? 'flagship' : 'workhorse', normal ? 'high' : 'medium', env);\n codexHalf = {\n requirements: design,\n research: design,\n adr: design,\n ideation: design,\n ddd: design,\n architecture: design,\n plan: codexCell(normal ? 'flagship' : 'workhorse', normal ? 'high' : 'low', env),\n code: codexCell(normal ? 'flagship' : 'workhorse', 'medium', env),\n };\n }\n return { ...claudeHalf, ...codexHalf };\n}\nfunction specToOpts(spec, env) {\n const log = env.log || function () { };\n if (!spec)\n return {};\n const parts = String(spec).split(':');\n const head = parts[0] || '';\n if (head === 'codex') {\n let id = parts[1] || env.CODEX_MODEL;\n if (id !== 'auto' && !KNOWN_CODEX[id]) {\n log('models: unknown codex id ' + id + ' — using ' + env.CODEX_MODEL);\n id = env.CODEX_MODEL;\n }\n let reasoning = parts[2] || 'high';\n if (!VALID_REASONING[reasoning]) {\n throw new RangeError('models: invalid reasoning \"' + reasoning + '\"valid: ' + Object.keys(VALID_REASONING).join('|'));\n }\n return { agentType: 'codex:codex-rescue', codexModel: id, _reasoning: reasoning };\n }\n if (CLAUDE_NAMES[head])\n return { model: head };\n log('models: unknown spec ' + spec + ' — session-inherited');\n return {};\n}\nfunction resolveCoderSpec(env) {\n if (env.CODER === 'codex' || env.CODER === 'codex-fallback')\n return 'codex:' + env.CODEX_MODEL + ':high';\n return 'opus';\n}\nfunction coderIsCodex(env) {\n const codeSpec = env.MODELS.code;\n if (codeSpec !== undefined && codeSpec !== null)\n return String(codeSpec).split(':')[0] === 'codex';\n if (env.CODER === 'codex' || env.CODER === 'codex-fallback')\n return true;\n return env.primary === 'codex';\n}\nfunction resolveQeSpecForCoder(coderCodex, env) {\n if (coderCodex)\n return 'sonnet';\n const CODEX_AVAILABLE = env.codexAvailable !== false;\n if (!CODEX_AVAILABLE)\n return 'opus';\n const budget = resolveBudgetMode(env.budget);\n return budget.codex === 'eco'\n ? 'codex:' + codexIdForTier('workhorse', env) + ':medium'\n : 'codex:' + codexIdForTier('flagship', env) + ':high';\n}\nfunction resolveQeSpec(env) {\n return resolveQeSpecForCoder(coderIsCodex(env), env);\n}\nfunction routingRequested(env) {\n return (Object.keys(env.MODELS).length > 0 ||\n env.primary !== undefined ||\n env.budget !== undefined ||\n env.PLANNER === 'codex' ||\n env.CODER === 'codex' ||\n env.CODER === 'codex-fallback' ||\n env.QE_REVIEWER === 'codex' ||\n env.QE_REVIEWER === 'codex-fallback');\n}\nfunction resolveStageModel(stage, env) {\n if (env.usageOverride) {\n const r = (env.usageReasoning && env.usageReasoning[stage]) || STAGE_EFFORT.override[stage] || 'medium';\n const o = specToOpts('codex:' + topCodexId(env) + ':' + r, env);\n o._usageSwitched = true;\n return o;\n }\n let spec = env.MODELS[stage];\n if (spec === undefined) {\n if (!routingRequested(env))\n return {};\n if (stage === 'code' && (env.CODER === 'codex' || env.CODER === 'codex-fallback')) {\n return specToOpts(resolveCoderSpec(env), env);\n }\n if (stage === 'plan' && env.PLANNER === 'codex') {\n return specToOpts('codex:' + env.CODEX_MODEL + ':high', env);\n }\n if (stage === 'qe') {\n return specToOpts(resolveQeSpec(env), env);\n }\n const resolvedPrimary = env.primary || 'claude';\n const cell = budgetTable(resolvedPrimary, resolveBudgetMode(env.budget), env)[stage];\n spec = cell !== undefined ? cell : DEFAULT_MODELS[stage];\n }\n if (stage === 'code' && (spec === null || spec === undefined))\n return specToOpts(resolveCoderSpec(env), env);\n if (stage === 'qe' && (spec === null || spec === undefined))\n return specToOpts(resolveQeSpec(env), env);\n return specToOpts(spec, env);\n}\nfunction modelLabel(opts) {\n if (opts && opts.agentType === 'codex:codex-rescue') {\n const base = 'codex:' + opts.codexModel + ':' + opts._reasoning;\n return opts._usageSwitched ? base + ' (usage-switched)' : base;\n }\n if (opts && opts.model)\n return opts.model;\n return 'session';\n}\nfunction stageLabel(base, opts) {\n const m = modelLabel(opts);\n return m === 'session' ? base : base + ' · ' + m;\n}\nfunction mergeOpts(base, extra) {\n const out = {};\n for (const k in base)\n out[k] = base[k];\n for (const k in extra)\n out[k] = extra[k];\n return out;\n}",
69
69
  },
70
70
  "usage-probes": {
71
71
  name: "usage-probes",
72
72
  version: "1.0.0",
73
- contentHash: "4ae2504a50fe05aad4383e223d460b21396805b17dfefab50e60022d58cdd19d",
73
+ contentHash: "a7d7fecc30202595c05cd31d5c36ee3bfa59f803f3e707e3c28c3ae372bc373b",
74
74
  sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-routing.ts",
75
75
  requires: ["model-resolver"],
76
76
  exports: ["decideUsageAction","OVERRIDE_REASONING","topCodexId"],
77
- code: "function decideUsageAction(prevOverride, signal, threshold) {\n if (signal === null || signal === undefined) {\n if (prevOverride)\n return { override: true, action: 'keep' };\n return { override: true, action: 'fail-safe-switch' };\n }\n const s = signal.sessionPct;\n const w = signal.weeklyPct;\n const sKnown = typeof s === 'number' && isFinite(s) && s >= 0;\n const wKnown = typeof w === 'number' && isFinite(w) && w >= 0;\n if ((sKnown && s >= threshold) || (wKnown && w >= threshold)) {\n return { override: true, action: prevOverride ? 'keep' : 'switch' };\n }\n if (sKnown && wKnown) {\n return { override: false, action: prevOverride ? 'restore' : 'none' };\n }\n return { override: prevOverride, action: prevOverride ? 'keep' : 'none' };\n}",
77
+ code: "const OVERRIDE_REASONING = Object.freeze(STAGE_EFFORT.override);\nfunction decideUsageAction(prevOverride, signal, threshold) {\n if (signal === null || signal === undefined) {\n if (prevOverride)\n return { override: true, action: 'keep' };\n return { override: true, action: 'fail-safe-switch' };\n }\n const s = signal.sessionPct;\n const w = signal.weeklyPct;\n const sKnown = typeof s === 'number' && isFinite(s) && s >= 0;\n const wKnown = typeof w === 'number' && isFinite(w) && w >= 0;\n if ((sKnown && s >= threshold) || (wKnown && w >= threshold)) {\n return { override: true, action: prevOverride ? 'keep' : 'switch' };\n }\n if (sKnown && wKnown) {\n return { override: false, action: prevOverride ? 'restore' : 'none' };\n }\n return { override: prevOverride, action: prevOverride ? 'keep' : 'none' };\n}",
78
78
  },
79
79
  "codex-dispatch": {
80
80
  name: "codex-dispatch",
package/src/loop-plan.ts CHANGED
@@ -150,6 +150,11 @@ export interface LoopFanout {
150
150
  registry: string[];
151
151
  /** Hard concurrency bound — REQUIRED, >= 1 (INV-2; unbounded fanout is unrepresentable-invalid). */
152
152
  maxFanout: number;
153
+ /** Admission policy when registry length exceeds maxFanout. Absent means window: dispatch all,
154
+ * while maxFanout bounds only concurrent work. */
155
+ overflow?: 'window' | 'truncate';
156
+ /** Mandatory human-readable receipt when overflow:'truncate' deliberately drops work. */
157
+ truncateReason?: string;
153
158
  dedup?: boolean;
154
159
  reasonRequired?: boolean;
155
160
  /** Per-item step chain (pipeline shape) — stepIds run in sequence per member. */
@@ -371,6 +376,8 @@ export const FIELD_DOMAINS: Record<string, FieldDomain> = {
371
376
  'LoopFanout.stage': { t: 'string' },
372
377
  'LoopFanout.registry': { t: 'string[]' },
373
378
  'LoopFanout.maxFanout': { t: 'number' },
379
+ 'LoopFanout.overflow': { t: 'enum', values: ['window', 'truncate'] },
380
+ 'LoopFanout.truncateReason': { t: 'string' },
374
381
  'LoopFanout.dedup': { t: 'boolean' },
375
382
  'LoopFanout.reasonRequired': { t: 'boolean' },
376
383
  'LoopFanout.chain': { t: 'string[]' },
@@ -992,6 +999,11 @@ export function validatePlan(plan: LoopPlan): Diagnostic[] {
992
999
  if (!Array.isArray(f.registry) || f.registry.length === 0) {
993
1000
  out.push({ invariant: 'INV-2', path: `$.fanouts[${f.stage}].registry`, message: 'a fanout requires a non-empty member registry' });
994
1001
  }
1002
+ if (f.overflow !== undefined && f.overflow !== 'window' && f.overflow !== 'truncate') {
1003
+ out.push({ invariant: 'INV-2b', path: `$.fanouts[${f.stage}].overflow`, message: `overflow must be one of window|truncate (received ${JSON.stringify(f.overflow)})` });
1004
+ } else if (f.overflow === 'truncate' && (typeof f.truncateReason !== 'string' || f.truncateReason.trim() === '')) {
1005
+ out.push({ invariant: 'INV-2b', path: `$.fanouts[${f.stage}].overflow`, message: 'overflow:"truncate" requires a non-blank truncateReason — deliberate dropped work needs a receipt' });
1006
+ }
995
1007
  });
996
1008
 
997
1009
  // INV-3: every fanout (parallel region) names an explicit join with a closed-set joinPolicy.
@@ -1388,7 +1400,7 @@ export function toLintProjection(plan: LoopPlan): LintProjection {
1388
1400
  }
1389
1401
 
1390
1402
  export interface TraceProjection {
1391
- kind: 'trace-projection/1';
1403
+ kind: 'trace-projection/2';
1392
1404
  /** Expected happens-before edges: every dispatch of `step` must come after the settle of each
1393
1405
  * dep. Only DISPATCHING step kinds (agent/gate) appear — fanout/join/pause are structural
1394
1406
  * pseudo-steps that emit no trace events of their own (their ordering lives in `regions`). */
@@ -1396,7 +1408,20 @@ export interface TraceProjection {
1396
1408
  /** Parallel regions with the runtime bound + concurrency shape the trace must exhibit.
1397
1409
  * `after` = the dispatching steps that depend on the region (fanout or join) — the trace-visible
1398
1410
  * witnesses of the barrier. */
1399
- regions: { fanout: string; join: string; joinPolicy: string; maxFanout: number; members: string[]; shape: ConcurrencyShape; after: string[] }[];
1411
+ regions: {
1412
+ fanout: string;
1413
+ join: string;
1414
+ joinPolicy: string;
1415
+ maxFanout: number;
1416
+ members: string[];
1417
+ shape: ConcurrencyShape;
1418
+ after: string[];
1419
+ /** Projection/2 evidence used to prove positional dispatch coverage. */
1420
+ registry: string[];
1421
+ registrySize: number;
1422
+ overflow: 'window' | 'truncate';
1423
+ dedup: boolean;
1424
+ }[];
1400
1425
  /** Steps expected to appear in the trace, keyed by stepId (iteration/attempt keys are runtime axes). */
1401
1426
  expectedSteps: string[];
1402
1427
  }
@@ -1432,10 +1457,14 @@ export function toTraceProjection(plan: LoopPlan): TraceProjection {
1432
1457
  members: [...(f.chain ?? [])],
1433
1458
  shape: shapes.get(f.stage) ?? 'barrier',
1434
1459
  after,
1460
+ registry: [...f.registry],
1461
+ registrySize: f.registry.length,
1462
+ overflow: f.overflow ?? 'window',
1463
+ dedup: f.dedup === true,
1435
1464
  };
1436
1465
  });
1437
1466
  return {
1438
- kind: 'trace-projection/1',
1467
+ kind: 'trace-projection/2',
1439
1468
  happensBefore,
1440
1469
  regions,
1441
1470
  expectedSteps: norm.steps.filter((s) => s.kind === 'agent' || s.kind === 'gate').map((s) => s.stepId),
@@ -1501,6 +1530,8 @@ export interface RunBoundary {
1501
1530
  maxFanout: number;
1502
1531
  registry: string[];
1503
1532
  dedup: boolean;
1533
+ overflow: 'window' | 'truncate';
1534
+ truncateReason: string | null;
1504
1535
  shape: ConcurrencyShape;
1505
1536
  chain: RunStepSpec[];
1506
1537
  };
@@ -1586,6 +1617,8 @@ export function toRunProjection(plan: LoopPlan): RunProjection {
1586
1617
  maxFanout: typeof f?.maxFanout === 'number' ? f.maxFanout : 0,
1587
1618
  registry: [...(f?.registry ?? [])],
1588
1619
  dedup: f?.dedup === true,
1620
+ overflow: f?.overflow ?? 'window',
1621
+ truncateReason: typeof f?.truncateReason === 'string' ? f.truncateReason : null,
1589
1622
  shape: s.concurrency ?? 'barrier',
1590
1623
  chain: (f?.chain ?? []).map((c) => byId.get(c)).filter((c): c is LoopStep => c !== undefined).map(specOf),
1591
1624
  },
@@ -431,9 +431,18 @@ function renderFanout(plan: LoopPlan, fanoutStep: LoopStep): string {
431
431
  lines.push(G(`step:${id} kind=fanout shape=${shape} maxFanout=${f.maxFanout}`));
432
432
  for (const ms of members) lines.push(...stepPromptAssembly(ms, plan));
433
433
  lines.push(`const REGISTRY_${ident(id)} = ${JSON.stringify(f.registry)}`);
434
- // dedup (enacted — QE round-3 B1): a declared dedup DEDUPLICATES the registry before the cap.
434
+ // dedup (enacted — QE round-3 B1): a declared dedup DEDUPLICATES the registry before admission.
435
435
  const registryExpr = f.dedup === true ? `REGISTRY_${ident(id)}.filter(function (x, i) { return REGISTRY_${ident(id)}.indexOf(x) === i })` : `REGISTRY_${ident(id)}`;
436
- lines.push(`const MEMBERS_${ident(id)} = ${registryExpr}.slice(0, ${f.maxFanout}) // bounded fanout (INV-2): never args-derived, never uncapped`);
436
+ if (f.overflow === 'truncate') {
437
+ const reason = f.truncateReason ?? '';
438
+ const commentReason = reason.replace(/\r?\n/g, ' ').replace(/\*\//g, '* /');
439
+ lines.push(`// ==================== FANOUT TRUNCATION DECLARED: ${commentReason} ====================`);
440
+ lines.push(`const MEMBERS_${ident(id)} = ${registryExpr}.slice(0, ${f.maxFanout}) // declared truncation (INV-2b): intentionally drops registry positions beyond maxFanout`);
441
+ lines.push(`console.error('[fanout-truncated] ${id}: ' + MEMBERS_${ident(id)}.length + ' of ' + REGISTRY_${ident(id)}.length + ' items — ' + ${jsString(reason)})`);
442
+ lines.push(`if (__hooks.onFanoutTruncated) { __hooks.onFanoutTruncated({ stage: ${jsString(id)}, registrySize: REGISTRY_${ident(id)}.length, dispatched: MEMBERS_${ident(id)}.length, reason: ${jsString(reason)} }) }`);
443
+ } else {
444
+ lines.push(`const MEMBERS_${ident(id)} = ${registryExpr} // full registry (INV-2b): every item dispatched; maxFanout bounds CONCURRENCY, not work`);
445
+ }
437
446
  // ROUND-6 B3 SHAPE: the region's awaits (member dispatches via parallel + the join) ride ONE
438
447
  // settle-routed try, mirroring the per-step shape. Member runStep failures settle themselves
439
448
  // durably before rejecting; the outer catch is the structural belt for the region as a whole.
@@ -445,7 +454,7 @@ function renderFanout(plan: LoopPlan, fanoutStep: LoopStep): string {
445
454
  // dispatch(B:item1) is allocated a seq before settle(A:item3). Predecessor by POSITION (G15),
446
455
  // qualified by the branch OCCURRENCE (round 7 — duplicate registry values are distinct
447
456
  // branches, so a chain predecessor is looked up per occurrence, never per value).
448
- lines.push(` R_${ident(id)} = await __drainAll(MEMBERS_${ident(id)}.map((it, __ix) => async () => {`);
457
+ lines.push(` R_${ident(id)} = await __drainAllWindowed(MEMBERS_${ident(id)}.map((it, __ix) => async () => {`);
449
458
  let prev: string | null = null;
450
459
  for (let ci = 0; ci < members.length; ci++) {
451
460
  const cs = members[ci] as LoopStep;
@@ -457,13 +466,13 @@ function renderFanout(plan: LoopPlan, fanoutStep: LoopStep): string {
457
466
  prev = `v_${ident(cs.stepId)}`;
458
467
  }
459
468
  lines.push(` return ${prev ?? 'null'}`);
460
- lines.push(` }))`);
469
+ lines.push(` }), ${f.maxFanout})`);
461
470
  } else {
462
471
  // BARRIER shape: all members dispatched, one join closes the region (exactly one member step —
463
472
  // MEMBER-2; extra barrier chain entries used to be silently never dispatched).
464
473
  const ms = members[0] as LoopStep;
465
474
  const call = stepCallExpr(ms, memberDeps(ms), { chainCausedBy: null, inputExpr: null });
466
- lines.push(` R_${ident(id)} = await __drainAll(MEMBERS_${ident(id)}.map((it, __ix) => () => ${call}))`);
475
+ lines.push(` R_${ident(id)} = await __drainAllWindowed(MEMBERS_${ident(id)}.map((it, __ix) => () => ${call}), ${f.maxFanout})`);
467
476
  }
468
477
  lines.push(` J_${ident(id)} = await __joinSettled(${jsString(j.stage)}, ${jsString(fanoutStep.phase)}, R_${ident(id)}, { policy: ${jsString(j.joinPolicy)}, onInvalid: ${jsString(j.onInvalid ?? 'named-failure')}, region: ${jsString(id)} })`);
469
478
  lines.push(`} catch (__stepErr) { await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(fanoutStep.phase)}, outcome: 'failed', error: __stepErr }) }`);
@@ -478,6 +487,7 @@ function renderFanout(plan: LoopPlan, fanoutStep: LoopStep): string {
478
487
  * attach hooks INSIDE it, and when opted out the hook sites are no-ops). */
479
488
  function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: LoopBlob[]): string {
480
489
  const traceOn = plan.trace?.emit === true;
490
+ const hasDeclaredTruncation = (plan.fanouts ?? []).some((f) => f.overflow === 'truncate');
481
491
  const ckptOn = plan.checkpointing?.enabled === true || plan.subsystems?.checkpoints === true;
482
492
  // budget: declared per-step budgets PLUS the declared gate-redo allowance (QE round-3 B1 — a
483
493
  // plan-declared redo must be affordable; an undeclared one still hits the guard loudly). The
@@ -505,7 +515,7 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
505
515
  lines.push(`let __ledgerDone = false`);
506
516
  if (traceOn) lines.push(`let __faLegWarned = false`);
507
517
  lines.push(`function __spendBudget(stepId) { if (__budget.left <= 0) { throw new Error('loop budget exhausted before ' + stepId) } __budget.left-- }`);
508
- lines.push(`const __hooks = { onDispatch: null, onSettle: null }`);
518
+ lines.push(`const __hooks = { onDispatch: null, onSettle: null${hasDeclaredTruncation ? ', onFanoutTruncated: null' : ''} }`);
509
519
  lines.push(`const __settled = {}`);
510
520
  lines.push(`// settle identity is PER-OCCURRENCE (round-7; Codex round-6 R2: with dedup:false and a`);
511
521
  lines.push(`// duplicated registry value, two branches shared one (stepId,itemKey) slot — the second`);
@@ -650,12 +660,46 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
650
660
  lines.push(` if (__primary !== null) { throw __primary.error } // drained FIRST, then the primary failure propagates`);
651
661
  lines.push(` return __results`);
652
662
  lines.push(`}`);
663
+ lines.push(`async function __drainAllWindowed(thunks, limit) {`);
664
+ lines.push(` const __limit = Math.max(1, Math.floor(Number(limit) || 1))`);
665
+ lines.push(` if (thunks.length <= __limit) { return __drainAll(thunks) }`);
666
+ lines.push(` const __out = new Array(thunks.length)`);
667
+ lines.push(` const __values = new Array(thunks.length)`);
668
+ lines.push(` let __next = 0`);
669
+ lines.push(` let __order = 0`);
670
+ lines.push(` const __workers = []`);
671
+ lines.push(` const __workerCount = Math.min(__limit, thunks.length)`);
672
+ lines.push(` for (let w = 0; w < __workerCount; w++) {`);
673
+ lines.push(` __workers.push(async function () {`);
674
+ lines.push(` while (__next < thunks.length) {`);
675
+ lines.push(` const i = __next++`);
676
+ lines.push(` try { const v = await thunks[i](); __values[i] = v; __out[i] = { ok: true, value: v, at: __order++ } }`);
677
+ lines.push(` catch (e) { __out[i] = { ok: false, error: e, at: __order++ } }`);
678
+ lines.push(` }`);
679
+ lines.push(` })`);
680
+ lines.push(` }`);
681
+ lines.push(` await parallel(__workers) // workers never reject ⇒ all registry positions drain to SETTLEMENT`);
682
+ lines.push(` let __primary = null`);
683
+ lines.push(` for (let i = 0; i < __out.length; i++) {`);
684
+ lines.push(` const o = __out[i]`);
685
+ lines.push(` if (o && o.ok !== true && (__primary === null || o.at < __primary.at)) { __primary = o }`);
686
+ lines.push(` }`);
687
+ lines.push(` if (__primary !== null) { throw __primary.error }`);
688
+ lines.push(` return __values`);
689
+ lines.push(`}`);
653
690
  }
654
691
  if (traceOn) {
655
692
  lines.push(`// trace wiring (blob-provided emitter; hooks INSIDE runStep — ADR-003)`);
656
693
  lines.push(`const __traceState = traceInit(RUN_ID, PLAN_DIGEST, EXEC_FP, 'rendered-script')`);
657
694
  lines.push(`__hooks.onDispatch = function (e) { return traceOnDispatch(__traceState, e) }`);
658
695
  lines.push(`__hooks.onSettle = function (e) { return traceOnSettle(__traceState, e) }`);
696
+ if (hasDeclaredTruncation) {
697
+ lines.push(`__hooks.onFanoutTruncated = function (e) {`);
698
+ lines.push(` const ev = { v: 1, runId: __traceState.runId, seq: ++__traceState.seq, event: 'fanout-truncated', stage: e.stage, registrySize: e.registrySize, dispatched: e.dispatched, reason: e.reason }`);
699
+ lines.push(` __traceState.buffer.push(JSON.stringify(ev))`);
700
+ lines.push(` return ev.seq`);
701
+ lines.push(`}`);
702
+ }
659
703
  lines.push(`async function __traceFlushNow(phaseName, stepLabel) {`);
660
704
  lines.push(` if (TRACE_FILE === null) { return }`);
661
705
  lines.push(` // cmd must be let: the trace payload stays LEFT and must never be replaced by the fa-record panel leg; both ride the SAME writer agent.`);
package/src/loop-trace.ts CHANGED
@@ -89,7 +89,19 @@ export interface TraceRunClosed {
89
89
  counts: { dispatched: number; settled: number };
90
90
  }
91
91
 
92
- export type TraceEvent = TraceDispatchEvent | TraceSettleEvent | TraceRunOpened | TraceRunClosed;
92
+ /** A deliberate work-dropping receipt. Its presence is required for overflow:'truncate' to grade. */
93
+ export interface TraceFanoutTruncatedEvent {
94
+ v: 1;
95
+ runId: string;
96
+ seq: number;
97
+ event: 'fanout-truncated';
98
+ stage: string;
99
+ registrySize: number;
100
+ dispatched: number;
101
+ reason: string;
102
+ }
103
+
104
+ export type TraceEvent = TraceDispatchEvent | TraceSettleEvent | TraceRunOpened | TraceRunClosed | TraceFanoutTruncatedEvent;
93
105
 
94
106
  export interface TraceState {
95
107
  runId: string;
@@ -148,6 +160,20 @@ export function traceValidateEvent(e: unknown): string | null {
148
160
  return 'unknown event kind';
149
161
  }
150
162
 
163
+ /** Reader extension kept outside the generated emitter slice: the two locked feature-adr workflow
164
+ * copies embed the v1 emitter byte-for-byte and cannot be rewritten during this parallel Step 7. */
165
+ function traceValidateReaderEvent(e: unknown): string | null {
166
+ const base = traceValidateEvent(e);
167
+ if (base !== 'unknown event kind') return base;
168
+ const ev = e as Record<string, unknown>;
169
+ if (ev['event'] !== 'fanout-truncated') return base;
170
+ if (typeof ev['stage'] !== 'string' || !TRACE_KEY_RE.test(ev['stage'])) return 'fanout-truncated stage fails its VO regex';
171
+ if (typeof ev['registrySize'] !== 'number' || !Number.isInteger(ev['registrySize']) || ev['registrySize'] < 0) return 'fanout-truncated registrySize must be a non-negative integer';
172
+ if (typeof ev['dispatched'] !== 'number' || !Number.isInteger(ev['dispatched']) || ev['dispatched'] < 0 || ev['dispatched'] > ev['registrySize']) return 'fanout-truncated dispatched must be an integer within registrySize';
173
+ if (typeof ev['reason'] !== 'string' || ev['reason'].trim() === '') return 'fanout-truncated reason required';
174
+ return null;
175
+ }
176
+
151
177
  /**
152
178
  * Open a trace state and buffer the run.opened frame. Throws on an invalid runId (fail-closed).
153
179
  *
@@ -204,6 +230,20 @@ export function traceOnSettle(state: TraceState, e: { invocationId: string; outc
204
230
  return seq;
205
231
  }
206
232
 
233
+ /** Emit the trace half of the loud truncation receipt. This is not a dispatch and does not alter
234
+ * run.closed invocation counts. */
235
+ export function traceOnFanoutTruncated(
236
+ state: TraceState,
237
+ e: { stage: string; registrySize: number; dispatched: number; reason: string },
238
+ ): number {
239
+ const seq = ++state.seq;
240
+ const event: TraceFanoutTruncatedEvent = { v: 1, runId: state.runId, seq, event: 'fanout-truncated', ...e };
241
+ const err = traceValidateReaderEvent(event);
242
+ if (err !== null) throw new Error('loop-trace: refusing non-conforming event (' + err + ') — the authoritative ordering source is never repaired later');
243
+ state.buffer.push(JSON.stringify(event));
244
+ return seq;
245
+ }
246
+
207
247
  /** Buffer the run.closed frame (a trace without it parses as incomplete). */
208
248
  export function traceClose(state: TraceState): void {
209
249
  const closed: TraceRunClosed = {
@@ -378,7 +418,7 @@ export function parseTrace(text: string): TraceRun {
378
418
  run.parseErrors.push('unparseable line: ' + t.slice(0, 120));
379
419
  continue;
380
420
  }
381
- const err = traceValidateEvent(e);
421
+ const err = traceValidateReaderEvent(e);
382
422
  if (err !== null) {
383
423
  run.parseErrors.push('invalid event (' + err + '): ' + t.slice(0, 120));
384
424
  continue;
@@ -646,6 +686,53 @@ export function runInvariants(projection: TraceProjection, run: TraceRun): Invar
646
686
  if (peak > bound) out.push({ id: idC, status: 'fail', message: `observed scheduling concurrency ${peak} exceeds maxFanout ${bound}` });
647
687
  else out.push({ id: idC, status: 'pass', message: `peak scheduled concurrency ${peak} <= maxFanout ${bound}` });
648
688
  }
689
+
690
+ // ADR-001: coverage is matched to registry POSITIONS, never inferred from equal totals. A
691
+ // duplicate-valued registry therefore owns two independent expected slots. Retries do not
692
+ // manufacture coverage: only attempt 1 of the first member stage witnesses branch admission.
693
+ const idD = 'region-dispatch-completeness:' + region.fanout;
694
+ if ((projection as { kind: string }).kind === 'trace-projection/1' || !Array.isArray(region.registry) || typeof region.registrySize !== 'number' || region.overflow === undefined) {
695
+ out.push({ id: idD, status: 'inconclusive', message: 'trace-projection/1 region has no positional registry evidence — dispatch completeness cannot be proved' });
696
+ } else {
697
+ const registry = region.dedup === true
698
+ ? region.registry.filter((key, index, all) => all.indexOf(key) === index)
699
+ : region.registry;
700
+ const expected = region.overflow === 'truncate' ? registry.slice(0, Math.max(0, region.maxFanout)) : registry;
701
+ const witness = region.members[0];
702
+ const observed = witness === undefined
703
+ ? []
704
+ : run.events.filter((e): e is TraceDispatchEvent => e.event === 'dispatched' && e.stepId === witness && e.attempt === 1).map((e) => e.itemKey);
705
+ const unmatched = new Set(expected.map((_key, index) => index));
706
+ const extras: (string | null)[] = [];
707
+ for (const key of observed) {
708
+ const position = [...unmatched].find((index) => expected[index] === key);
709
+ if (position === undefined) extras.push(key);
710
+ else unmatched.delete(position);
711
+ }
712
+ const missing = [...unmatched].map((index) => `#${index + 1}:${expected[index]}`);
713
+ const receipt = run.events.find((e): e is TraceFanoutTruncatedEvent =>
714
+ e.event === 'fanout-truncated' && e.stage === region.fanout,
715
+ );
716
+ const badReceipt = region.overflow === 'truncate' && (
717
+ receipt === undefined ||
718
+ receipt.registrySize !== region.registrySize ||
719
+ receipt.dispatched !== expected.length ||
720
+ receipt.reason.trim() === ''
721
+ );
722
+ if (missing.length > 0 || extras.length > 0 || badReceipt) {
723
+ const parts: string[] = [];
724
+ if (missing.length > 0) parts.push('missing registry position(s) ' + missing.join(', '));
725
+ if (extras.length > 0) parts.push('unexpected dispatch key(s) ' + extras.map((x) => String(x)).join(', '));
726
+ if (badReceipt) parts.push('missing or mismatched fanout-truncated trace receipt');
727
+ out.push({
728
+ id: idD,
729
+ status: run.incomplete && region.overflow === 'window' && missing.length > 0 && extras.length === 0 ? 'inconclusive' : 'fail',
730
+ message: parts.join('; ') + (run.incomplete ? ' (trace incomplete)' : ''),
731
+ });
732
+ } else {
733
+ out.push({ id: idD, status: 'pass', message: `every one of ${expected.length} expected registry position(s) has exactly one first-attempt dispatch${region.overflow === 'truncate' ? ' and a matching truncation receipt' : ''}` });
734
+ }
735
+ }
649
736
  // Join coverage: the join itself is a structural pseudo-step (no trace event of its own), so
650
737
  // its trace-visible witnesses are (a) every dispatched member settles (a dispatched branch is
651
738
  // never skippable) and (b) every post-region dispatching step (`after`) dispatches AFTER every
@@ -833,6 +920,8 @@ export function assembleTimeline(input: {
833
920
  rows.push({ seq: e.seq, kind: 'trace', label: `settle ${e.invocationId}`, detail: `outcome=${e.outcome}`, wallTime: e.wallTime ?? null });
834
921
  } else if (e.event === 'run.opened') {
835
922
  rows.push({ seq: e.seq, kind: 'trace', label: 'run.opened', detail: `plan=${e.planDigest.slice(0, 12)} exec-fp=${e.execFp.slice(0, 12)}`, wallTime: null });
923
+ } else if (e.event === 'fanout-truncated') {
924
+ rows.push({ seq: e.seq, kind: 'trace', label: `fanout-truncated ${e.stage}`, detail: `${e.dispatched} of ${e.registrySize} — ${e.reason}`, wallTime: null });
836
925
  } else {
837
926
  rows.push({ seq: e.seq, kind: 'trace', label: 'run.closed', detail: JSON.stringify(e.counts), wallTime: null });
838
927
  }
@@ -92,7 +92,9 @@ export function harvestStageOutcomes(records: readonly unknown[]): Harvest {
92
92
  if (!isRecord(modelsUsed)) { skipped.noModels += 1; continue; }
93
93
  const grade = extractGrade(result);
94
94
  if (grade === null) { skipped.noGrade += 1; continue; }
95
- const runId = typeof source['runId'] === 'string' ? source['runId'] : JSON.stringify(modelsUsed).slice(0, 40);
95
+ // Full canonical JSON is intentional: the former 40-character prefix merged distinct records
96
+ // whose stage names shared a long prefix, manufacturing idempotency across different runs.
97
+ const runId = typeof source['runId'] === 'string' ? source['runId'] : `models:${JSON.stringify(modelsUsed)}`;
96
98
  const ts = typeof source['timestamp'] === 'string' ? source['timestamp'] : null;
97
99
  const tier = typeof result['tier'] === 'string' && result['tier'] !== '' ? result['tier'] : 'unknown';
98
100
  let contributed = false;
@@ -138,6 +140,8 @@ export interface Recommendation {
138
140
  readonly rule: string;
139
141
  readonly skipped: Harvest['skipped'];
140
142
  readonly crossFamilyNote: string;
143
+ readonly unfed: { readonly count: number; readonly runIds: string[] };
144
+ readonly freshness: 'current' | 'stale' | 'unfed';
141
145
  };
142
146
  }
143
147
 
@@ -154,7 +158,7 @@ const CROSS_NOTE =
154
158
  'the qe pick is computed with the family FORCED to the cross of the code pick — a same-family qe recommendation is unrepresentable (ADR-001 D3)';
155
159
 
156
160
  /** Recommend per stage over the harvested samples (optionally one tier's slice). */
157
- export function recommendModels(harvest: Harvest, opts: { tier?: string; qualityBar?: number; minSamples?: number } = {}): Recommendation {
161
+ export function recommendModels(harvest: Harvest, opts: { tier?: string; qualityBar?: number; minSamples?: number; alreadyFed?: readonly string[] } = {}): Recommendation {
158
162
  const slice = opts.tier === undefined ? harvest.samples : harvest.samples.filter((s) => s.tier === opts.tier);
159
163
  const tierLabel = opts.tier ?? 'all';
160
164
  const byStage = new Map<string, HarvestSample[]>();
@@ -197,9 +201,21 @@ export function recommendModels(harvest: Harvest, opts: { tier?: string; quality
197
201
  }
198
202
  }
199
203
  if (!stages.includes('code')) perStage.unshift(code);
204
+ const pending = unfedRuns(harvest.samples, opts.alreadyFed ?? []);
205
+ const harvestRunIds = [...new Set(harvest.samples.map((s) => s.runId))];
206
+ const fed = new Set(opts.alreadyFed ?? []);
207
+ const fedInHarvest = harvestRunIds.filter((runId) => fed.has(runId)).length;
200
208
  return {
201
209
  perStage,
202
- basis: { runsUsed: harvest.runsUsed, window: harvest.window, rule: harvest.rule, skipped: harvest.skipped, crossFamilyNote: CROSS_NOTE },
210
+ basis: {
211
+ runsUsed: harvest.runsUsed,
212
+ window: harvest.window,
213
+ rule: harvest.rule,
214
+ skipped: harvest.skipped,
215
+ crossFamilyNote: CROSS_NOTE,
216
+ unfed: { count: pending.length, runIds: pending },
217
+ freshness: pending.length === 0 ? 'current' : fedInHarvest === 0 ? 'unfed' : 'stale',
218
+ },
203
219
  };
204
220
  }
205
221
 
@@ -213,15 +229,30 @@ export interface FeedPlan {
213
229
  readonly fedAfter: string[];
214
230
  }
215
231
 
232
+ /** One shared definition of the telemetry/store gap, used by recommendation, apply and guard. */
233
+ export function unfedRuns(samples: readonly HarvestSample[], alreadyFed: readonly string[]): string[] {
234
+ const fed = new Set(alreadyFed);
235
+ return [...new Set(samples.map((s) => s.runId))].filter((runId) => !fed.has(runId)).sort();
236
+ }
237
+
216
238
  /** A run feeds ONCE. Double-feeding the same telemetry manufactures confidence the data does not
217
239
  * contain — the second `--apply` must feed 0 and say which runs it skipped. */
218
240
  export function planFeed(samples: readonly HarvestSample[], alreadyFed: readonly string[]): FeedPlan {
219
241
  const fed = new Set(alreadyFed);
220
242
  const toFeed: HarvestSample[] = [];
221
243
  const skippedRuns = new Set<string>();
244
+ const seenSamples = new Set<string>();
222
245
  for (const s of samples) {
223
246
  if (fed.has(s.runId)) skippedRuns.add(s.runId);
224
- else toFeed.push(s);
247
+ else {
248
+ // A multi-stage run legitimately feeds one sample per stage. Only a repeated sample from a
249
+ // duplicated record is suppressed within this harvest.
250
+ const sampleKey = JSON.stringify([s.runId, s.stage]);
251
+ if (!seenSamples.has(sampleKey)) {
252
+ seenSamples.add(sampleKey);
253
+ toFeed.push(s);
254
+ }
255
+ }
225
256
  }
226
257
  const fedAfter = [...new Set([...alreadyFed, ...toFeed.map((s) => s.runId)])].sort();
227
258
  return { toFeed, skippedRuns: [...skippedRuns].sort(), fedAfter };
@@ -33,6 +33,7 @@ import {
33
33
  traceInit,
34
34
  traceLedgerLine,
35
35
  traceOnDispatch,
36
+ traceOnFanoutTruncated,
36
37
  traceOnSettle,
37
38
  traceShellQuote,
38
39
  TRACE_KEY_RE,
@@ -336,7 +337,7 @@ export function computeBoundaryReservations(
336
337
  if (b.kind === 'region') {
337
338
  const r = b.region;
338
339
  if (r === undefined) continue;
339
- const members = activatedMembers(r.registry, r.dedup, r.maxFanout).length;
340
+ const members = activatedMembers(r.registry, r.dedup, r.maxFanout, r.overflow).length;
340
341
  const perItem = r.chain.reduce((n, s) => n + worstCaseInvocations(s), 0);
341
342
  const perItemWall = r.chain.reduce((n, s) => n + worstCaseInvocations(s) * timeoutMsFor(s), 0);
342
343
  out.push({
@@ -366,17 +367,14 @@ export function computeBoundaryReservations(
366
367
  /**
367
368
  * The members a fanout actually ACTIVATES.
368
369
  *
369
- * `maxFanout` caps ACTIVATION, not merely concurrency the rendered script emits
370
- * `registry.slice(0, maxFanout)` ("bounded fanout (INV-2): never args-derived, never uncapped").
371
- * Getting this wrong is a STRUCTURAL-EQUIVALENCE break, not a performance detail: a runner that
372
- * treated the bound as a concurrency limit would dispatch all six entries of a six-item registry
373
- * where the Claude host dispatched three, and the two hosts' traces would disagree on dispatch
374
- * multiplicity for the same plan. (MEASURED: that is exactly what the F5 equivalence leg caught
375
- * against the committed `pkg-audit-1` run — 6 lanes here, 3 there.)
370
+ * `maxFanout` caps CONCURRENCY under the default/window policy. Only the explicitly declared
371
+ * `overflow:'truncate'` escape hatch caps activation, and that path owes stderr + trace receipts.
372
+ * This function is shared by reservation arithmetic and enactment so neither can silently retain
373
+ * the old prefix-only interpretation.
376
374
  */
377
- function activatedMembers(registry: string[], dedup: boolean, maxFanout: number): string[] {
375
+ function activatedMembers(registry: string[], dedup: boolean, maxFanout: number, overflow: 'window' | 'truncate'): string[] {
378
376
  const base = dedup ? [...new Set(registry)] : [...registry];
379
- return maxFanout > 0 ? base.slice(0, maxFanout) : base;
377
+ return overflow === 'truncate' && maxFanout > 0 ? base.slice(0, maxFanout) : base;
380
378
  }
381
379
 
382
380
  /**
@@ -1605,7 +1603,16 @@ async function runStage(ctx: RunCtx, b: RunBoundary): Promise<string | null> {
1605
1603
  async function runRegion(ctx: RunCtx, b: RunBoundary): Promise<void> {
1606
1604
  const r = b.region;
1607
1605
  if (r === undefined) return;
1608
- const members = activatedMembers(r.registry, r.dedup, r.maxFanout);
1606
+ const members = activatedMembers(r.registry, r.dedup, r.maxFanout, r.overflow);
1607
+ if (r.overflow === 'truncate') {
1608
+ traceOnFanoutTruncated(ctx.trace, {
1609
+ stage: r.fanout,
1610
+ registrySize: r.registry.length,
1611
+ dispatched: members.length,
1612
+ reason: r.truncateReason ?? '',
1613
+ });
1614
+ flush(ctx);
1615
+ }
1609
1616
  for (const k of members) {
1610
1617
  if (!safeItemKey(k)) {
1611
1618
  throw new RunFailure(