@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.
- package/.dz-manifest.json +49 -49
- package/README.md +6 -1
- package/dist/feature-adr-checkpoints.d.ts +11 -2
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +37 -2
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +56 -21
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +187 -58
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard.d.ts +25 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +59 -1
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loop-blobs.generated.js +8 -8
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/loop-plan.d.ts +13 -1
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +15 -1
- package/dist/loop-plan.js.map +1 -1
- package/dist/loop-render.d.ts.map +1 -1
- package/dist/loop-render.js +51 -6
- package/dist/loop-render.js.map +1 -1
- package/dist/loop-trace.d.ts +20 -1
- package/dist/loop-trace.d.ts.map +1 -1
- package/dist/loop-trace.js +83 -1
- package/dist/loop-trace.js.map +1 -1
- package/dist/model-recommender.d.ts +8 -0
- package/dist/model-recommender.d.ts.map +1 -1
- package/dist/model-recommender.js +31 -4
- package/dist/model-recommender.js.map +1 -1
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +18 -12
- package/dist/workflow-run.js.map +1 -1
- package/package.json +18 -15
- package/sbom.json +48 -48
- package/src/feature-adr-checkpoints.ts +38 -2
- package/src/feature-adr-routing.ts +217 -54
- package/src/guard.ts +79 -1
- package/src/index.ts +2 -1
- package/src/loop-blobs.generated.ts +8 -8
- package/src/loop-plan.ts +36 -3
- package/src/loop-render.ts +50 -6
- package/src/loop-trace.ts +91 -2
- package/src/model-recommender.ts +35 -4
- 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.
|
|
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.
|
|
55
|
-
contentHash: "
|
|
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-
|
|
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: "
|
|
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
|
|
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: "
|
|
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: "
|
|
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/
|
|
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: {
|
|
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/
|
|
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
|
},
|
package/src/loop-render.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
}
|
package/src/model-recommender.ts
CHANGED
|
@@ -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
|
-
|
|
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: {
|
|
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
|
|
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 };
|
package/src/workflow-run.ts
CHANGED
|
@@ -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
|
|
370
|
-
* `
|
|
371
|
-
*
|
|
372
|
-
*
|
|
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(
|