mustflow 2.58.1 → 2.68.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 (47) hide show
  1. package/README.md +4 -0
  2. package/dist/cli/commands/context.js +81 -6
  3. package/dist/cli/commands/quality.js +89 -0
  4. package/dist/cli/commands/skill.js +116 -0
  5. package/dist/cli/i18n/en.js +16 -0
  6. package/dist/cli/i18n/es.js +16 -0
  7. package/dist/cli/i18n/fr.js +16 -0
  8. package/dist/cli/i18n/hi.js +16 -0
  9. package/dist/cli/i18n/ko.js +16 -0
  10. package/dist/cli/i18n/zh.js +16 -0
  11. package/dist/cli/index.js +1 -0
  12. package/dist/cli/lib/agent-context.js +981 -8
  13. package/dist/cli/lib/command-registry.js +12 -0
  14. package/dist/cli/lib/local-index/constants.js +4 -5
  15. package/dist/cli/lib/local-index/freshness.js +5 -1
  16. package/dist/cli/lib/local-index/index.js +1 -1
  17. package/dist/cli/lib/repo-map.js +7 -1
  18. package/dist/cli/lib/validation/constants.js +3 -0
  19. package/dist/cli/lib/validation/index.js +41 -2
  20. package/dist/core/check-issues.js +5 -0
  21. package/dist/core/prompt-cache-rendering.js +19 -0
  22. package/dist/core/public-json-contracts.js +35 -0
  23. package/dist/core/quality-gaming.js +304 -0
  24. package/dist/core/skill-route-fixtures.js +173 -0
  25. package/dist/core/skill-route-resolution.js +398 -0
  26. package/dist/core/source-anchors.js +91 -5
  27. package/package.json +1 -1
  28. package/schemas/README.md +9 -1
  29. package/schemas/context-report.schema.json +442 -0
  30. package/schemas/quality-gaming-report.schema.json +96 -0
  31. package/schemas/route-fixture.schema.json +57 -0
  32. package/schemas/skill-route-report.schema.json +242 -0
  33. package/templates/default/common/.mustflow/config/commands.toml +34 -2
  34. package/templates/default/common/.mustflow/config/mustflow.toml +16 -10
  35. package/templates/default/i18n.toml +14 -8
  36. package/templates/default/locales/en/.mustflow/context/PROJECT.md +5 -3
  37. package/templates/default/locales/en/.mustflow/docs/agent-workflow.md +13 -9
  38. package/templates/default/locales/en/.mustflow/skills/INDEX.md +3 -2
  39. package/templates/default/locales/en/.mustflow/skills/llm-token-cost-control-review/SKILL.md +5 -2
  40. package/templates/default/locales/en/.mustflow/skills/quality-gaming-guard/SKILL.md +165 -0
  41. package/templates/default/locales/en/.mustflow/skills/router.toml +67 -0
  42. package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
  43. package/templates/default/locales/en/AGENTS.md +15 -7
  44. package/templates/default/locales/ko/.mustflow/context/PROJECT.md +4 -2
  45. package/templates/default/locales/ko/.mustflow/docs/agent-workflow.md +18 -12
  46. package/templates/default/locales/ko/AGENTS.md +8 -6
  47. package/templates/default/manifest.toml +9 -1
@@ -6,28 +6,31 @@ import { readRetentionStore } from '../../core/retention-policy.js';
6
6
  import { toPosixPath } from './filesystem.js';
7
7
  import { readLocalIndexPromptContext } from './local-index.js';
8
8
  import { inspectManifestLock } from './manifest-lock.js';
9
- import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile, readMustflowTextFileIfExists, } from './mustflow-read.js';
9
+ import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile, readMustflowTextFileIfExists, readMustflowTextFileResult, } from './mustflow-read.js';
10
10
  import { createRunPlan } from './run-plan.js';
11
11
  import { readMustflowTomlFile } from './toml.js';
12
12
  import { normalizeTechnologyPreferencesTable, TECHNOLOGY_CONFIG_RELATIVE_PATH, } from '../../core/technology-preferences.js';
13
+ import { isPromptCacheStableLeafSkillSurface, measurePromptCacheReferenceBlockBytes, renderPromptCacheReferenceBlock, } from '../../core/prompt-cache-rendering.js';
14
+ import { resolveSkillRoutes } from '../../core/skill-route-resolution.js';
13
15
  const CONTEXT_SCHEMA_VERSION = '1';
14
16
  const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
15
17
  const MUSTFLOW_RELATIVE_PATH = '.mustflow/config/mustflow.toml';
16
18
  const PREFERENCES_RELATIVE_PATH = '.mustflow/config/preferences.toml';
19
+ const SKILL_ROUTER_RELATIVE_PATH = '.mustflow/skills/router.toml';
20
+ const SKILL_ROUTE_CANDIDATES_VIRTUAL_PATH = '.mustflow/generated/skill-route-candidates.compact.json';
17
21
  const TECHNOLOGY_RELATIVE_PATH = TECHNOLOGY_CONFIG_RELATIVE_PATH;
18
22
  const LATEST_RUN_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
19
23
  const CACHE_RELATIVE_PATH = '.mustflow/cache/';
20
24
  const STATE_RELATIVE_PATH = '.mustflow/state/';
21
25
  const DEFAULT_PROMPT_CACHE_STABLE_READ = [
22
26
  'AGENTS.md',
23
- '.mustflow/docs/agent-workflow.md',
24
- '.mustflow/config/mustflow.toml',
25
- '.mustflow/config/commands.toml',
26
- '.mustflow/skills/INDEX.md',
27
+ '.mustflow/skills/router.toml',
27
28
  ];
28
29
  const DEFAULT_PROMPT_CACHE_TASK_SOURCES = [
29
30
  '.mustflow/context/INDEX.md',
30
- 'REPO_MAP.md',
31
+ 'skill_route_candidates',
32
+ 'route_metadata_fallback',
33
+ 'repo_map_navigation',
31
34
  'matching_skill',
32
35
  'relevant_source_files',
33
36
  ];
@@ -108,6 +111,145 @@ function readScalarObject(table) {
108
111
  function sha256(content) {
109
112
  return `sha256:${createHash('sha256').update(content).digest('hex')}`;
110
113
  }
114
+ function estimateTokens(renderedBytes) {
115
+ return Math.ceil(renderedBytes / 4);
116
+ }
117
+ function budgetBytes(budgetKb) {
118
+ return budgetKb === null ? null : Math.round(budgetKb * 1024);
119
+ }
120
+ function budgetStatus(renderedBytes, budget) {
121
+ if (renderedBytes === null || budget === null) {
122
+ return 'unknown';
123
+ }
124
+ return renderedBytes <= budget ? 'within_budget' : 'over_budget';
125
+ }
126
+ function renderedDigest(relativePath, content) {
127
+ return sha256(renderPromptCacheReferenceBlock(relativePath, content));
128
+ }
129
+ function calculateBudgetShare(renderedBytes, budget) {
130
+ if (renderedBytes === null || budget === null || budget <= 0) {
131
+ return null;
132
+ }
133
+ return Number((renderedBytes / budget).toFixed(6));
134
+ }
135
+ function isDirectPromptCacheSource(source) {
136
+ return (source.includes('/') ||
137
+ source.endsWith('.md') ||
138
+ source.endsWith('.toml') ||
139
+ source.endsWith('.json') ||
140
+ source.endsWith('.yaml') ||
141
+ source.endsWith('.yml'));
142
+ }
143
+ function taskSourceReferencePath(source) {
144
+ const normalized = toPosixPath(source);
145
+ if (normalized === 'route_metadata_fallback') {
146
+ return '.mustflow/skills/routes.toml';
147
+ }
148
+ if (normalized === 'expanded_skill_index_fallback') {
149
+ return '.mustflow/skills/INDEX.md';
150
+ }
151
+ if (isDirectPromptCacheSource(normalized)) {
152
+ return normalized;
153
+ }
154
+ return null;
155
+ }
156
+ function taskSourceSelectionPolicy(source) {
157
+ const normalized = toPosixPath(source);
158
+ if (normalized === '.mustflow/skills/routes.toml' ||
159
+ normalized === '.mustflow/skills/INDEX.md' ||
160
+ normalized === 'route_metadata_fallback' ||
161
+ normalized === 'expanded_skill_index_fallback') {
162
+ return 'fallback_when_needed';
163
+ }
164
+ if (isDirectPromptCacheSource(normalized)) {
165
+ return 'read_when_selected';
166
+ }
167
+ return 'selected_at_runtime';
168
+ }
169
+ function taskSourceKind(source) {
170
+ return taskSourceReferencePath(source) === null ? 'dynamic_selection' : 'file_reference';
171
+ }
172
+ function taskSourceCacheability(source, selectionPolicy) {
173
+ if (selectionPolicy === 'fallback_when_needed') {
174
+ return 'task_fallback';
175
+ }
176
+ return taskSourceKind(source) === 'file_reference' ? 'task_selective' : 'runtime_selection';
177
+ }
178
+ function taskSourceReloadOn(source, selectionPolicy) {
179
+ if (selectionPolicy === 'fallback_when_needed') {
180
+ return ['route_resolution_changed'];
181
+ }
182
+ if (source === 'repo_map_navigation') {
183
+ return ['navigation_scope_changed', 'source_anchor_selection_changed'];
184
+ }
185
+ if (taskSourceKind(source) === 'file_reference') {
186
+ return ['task_selection_changed', 'source_file_hash_changed'];
187
+ }
188
+ if (source === 'relevant_source_files') {
189
+ return ['source_selection_changed'];
190
+ }
191
+ return ['route_resolution_changed'];
192
+ }
193
+ function isDeferredTaskFallback(selectionPolicy) {
194
+ return selectionPolicy === 'fallback_when_needed';
195
+ }
196
+ function taskSourceIssue(source, measurementStatus) {
197
+ if (measurementStatus === 'hash_only_deferred') {
198
+ return `task source is selection-gated; hash is recorded, but content is measured only when selected: ${toPosixPath(source)}`;
199
+ }
200
+ return `task source is selected at runtime and cannot be measured before the current task is assembled: ${source}`;
201
+ }
202
+ function hasPromptCacheRouteInput(input) {
203
+ return Boolean(input?.taskText?.trim() || input?.paths.length || input?.reasons.length);
204
+ }
205
+ function resolvePromptCacheSkillRoutes(projectRoot, input) {
206
+ if (!hasPromptCacheRouteInput(input)) {
207
+ return null;
208
+ }
209
+ return resolveSkillRoutes(projectRoot, input);
210
+ }
211
+ function selectedSkillRoutePaths(routeReport) {
212
+ return routeReport?.read_plan.selected_skill_paths ?? [];
213
+ }
214
+ function selectedTaskBlockOrder(sourceIndex, selectedIndex) {
215
+ return ((sourceIndex + 1) * 100) + selectedIndex + 1;
216
+ }
217
+ function compactSkillRouteCandidate(candidate) {
218
+ return {
219
+ skill: candidate.skill,
220
+ skill_path: candidate.skill_path,
221
+ category: candidate.category,
222
+ route_type: candidate.route_type,
223
+ score: Number(candidate.score.toFixed(3)),
224
+ selection_reasons: candidate.selection_reasons,
225
+ verification_intents: candidate.verification_intents,
226
+ };
227
+ }
228
+ function readCompactSkillRouteCandidateContent(routeReport) {
229
+ if (routeReport === null) {
230
+ return null;
231
+ }
232
+ return `${JSON.stringify({
233
+ schema_version: '1',
234
+ kind: 'skill_route_candidates_compact',
235
+ input: routeReport.input,
236
+ signals: {
237
+ task_terms: routeReport.signals.task_terms,
238
+ path_terms: routeReport.signals.path_terms,
239
+ reasons: routeReport.signals.reasons,
240
+ },
241
+ selected: {
242
+ main: routeReport.selected.main === null ? null : compactSkillRouteCandidate(routeReport.selected.main),
243
+ adjuncts: routeReport.selected.adjuncts.map(compactSkillRouteCandidate),
244
+ },
245
+ candidates: routeReport.candidates.map(compactSkillRouteCandidate),
246
+ read_plan: {
247
+ selected_skill_paths: routeReport.read_plan.selected_skill_paths,
248
+ candidate_skill_paths: routeReport.read_plan.candidate_skill_paths,
249
+ avoid_by_default: routeReport.read_plan.avoid_by_default,
250
+ },
251
+ }, null, 2)}\n`;
252
+ }
111
253
  function readCommandContractContext(projectRoot) {
112
254
  const commands = readTomlTableIfExists(projectRoot, COMMANDS_RELATIVE_PATH);
113
255
  if (!commands || !isRecord(commands.intents)) {
@@ -283,6 +425,66 @@ function mapLocalIndexPromptContext(context) {
283
425
  refresh_hint: context.refreshHint,
284
426
  };
285
427
  }
428
+ function readSkillRouterTable(projectRoot) {
429
+ try {
430
+ const parsed = readMustflowTomlFile(projectRoot, SKILL_ROUTER_RELATIVE_PATH);
431
+ return isRecord(parsed) ? parsed : undefined;
432
+ }
433
+ catch {
434
+ return undefined;
435
+ }
436
+ }
437
+ function readTaskPromptCacheRouteReadPlan(projectRoot) {
438
+ const router = readSkillRouterTable(projectRoot);
439
+ const readWhen = readNestedTable(router, 'read_when');
440
+ const fullRoutes = readOptionalString(router, 'full_routes') ?? '.mustflow/skills/routes.toml';
441
+ const expandedIndex = readOptionalString(router, 'expanded_index') ?? '.mustflow/skills/INDEX.md';
442
+ const skillRoot = readOptionalString(router, 'skill_root') ?? '.mustflow/skills';
443
+ const candidates = readNumber(router, 'selection_limit') ?? 5;
444
+ const main = readNumber(router, 'main_limit') ?? 1;
445
+ const adjuncts = readNumber(router, 'adjunct_limit') ?? 2;
446
+ return {
447
+ resolver_command: ['mf', 'skill', 'route', '--json'],
448
+ stable_kernel: [toPosixPath(SKILL_ROUTER_RELATIVE_PATH)],
449
+ route_sources: [
450
+ toPosixPath(fullRoutes),
451
+ `${toPosixPath(skillRoot)}/*/SKILL.md frontmatter`,
452
+ ],
453
+ selected_skill_paths_source: 'mf skill route --json read_plan.selected_skill_paths',
454
+ route_metadata_fallback: {
455
+ path: toPosixPath(fullRoutes),
456
+ read_when: readOptionalStringArray(readWhen, 'full_routes') ?? [],
457
+ avoid_by_default: true,
458
+ },
459
+ expanded_index_fallback: {
460
+ path: toPosixPath(expandedIndex),
461
+ read_when: readOptionalStringArray(readWhen, 'expanded_index') ?? [],
462
+ avoid_by_default: true,
463
+ },
464
+ selection_limits: { candidates, main, adjuncts },
465
+ };
466
+ }
467
+ function readTaskPromptCacheRepoMapReadPlan() {
468
+ return {
469
+ source: 'repo_map_navigation',
470
+ strategy: 'select_anchors_or_spans_before_full_map',
471
+ anchor_sources: [
472
+ 'local_index source anchors',
473
+ 'REPO_MAP.md Priority Anchors',
474
+ 'REPO_MAP.md Source Anchors',
475
+ ],
476
+ fallback: {
477
+ path: 'REPO_MAP.md',
478
+ read_when: [
479
+ 'broader repository navigation is needed',
480
+ 'local index is missing, stale, or unavailable',
481
+ 'selected anchors or spans do not identify the needed files',
482
+ 'task edits the repository map generator or generated map contract',
483
+ ],
484
+ avoid_by_default: true,
485
+ },
486
+ };
487
+ }
286
488
  async function readTaskPromptCacheLayer(projectRoot, mustflow) {
287
489
  const layer = readPromptCacheLayer(mustflow, 'task');
288
490
  const localIndex = await readLocalIndexPromptContext(projectRoot);
@@ -290,6 +492,8 @@ async function readTaskPromptCacheLayer(projectRoot, mustflow) {
290
492
  cache_layer: 'task',
291
493
  read_policy: readOptionalString(layer, 'read_policy'),
292
494
  sources: readOptionalStringArray(layer, 'sources') ?? [...DEFAULT_PROMPT_CACHE_TASK_SOURCES],
495
+ route_read_plan: readTaskPromptCacheRouteReadPlan(projectRoot),
496
+ repo_map_read_plan: readTaskPromptCacheRepoMapReadPlan(),
293
497
  local_index: mapLocalIndexPromptContext(localIndex),
294
498
  };
295
499
  }
@@ -303,14 +507,783 @@ function readVolatilePromptCacheLayer(mustflow) {
303
507
  include_latest_run: false,
304
508
  };
305
509
  }
306
- export async function getPromptCacheProfileContext(projectRoot, profile) {
510
+ function readStablePromptBundleLayer(projectRoot, mustflow) {
511
+ const layer = readPromptCacheLayer(mustflow, 'stable');
512
+ const read = readOptionalStringArray(layer, 'read') ?? [...DEFAULT_PROMPT_CACHE_STABLE_READ];
513
+ return {
514
+ cache_layer: 'stable',
515
+ blocks: read.map((relativePath, index) => {
516
+ const path = toPosixPath(relativePath);
517
+ const content = safeRead(projectRoot, relativePath);
518
+ const renderedBytes = content === null ? null : measurePromptCacheReferenceBlockBytes(relativePath, content);
519
+ const issue = content === null ? `stable prefix document is missing: ${path}` : null;
520
+ return {
521
+ id: `stable:${index + 1}:${path}`,
522
+ cache_layer: 'stable',
523
+ order: index + 1,
524
+ kind: content === null ? 'source_placeholder' : 'file',
525
+ path: content === null ? null : path,
526
+ source: null,
527
+ source_kind: 'file_reference',
528
+ selection_policy: 'always_rendered',
529
+ content_included: false,
530
+ content_hash: content === null ? null : sha256(content),
531
+ rendered_digest: content === null ? null : renderedDigest(relativePath, content),
532
+ rendered_bytes: renderedBytes,
533
+ estimated_tokens: renderedBytes === null ? null : estimateTokens(renderedBytes),
534
+ cacheability: 'provider_prefix_candidate',
535
+ reload_on: ['stable_file_hash_changed'],
536
+ issue,
537
+ };
538
+ }),
539
+ };
540
+ }
541
+ function readTaskPromptBundleLayer(projectRoot, mustflow, routeReport) {
542
+ const layer = readPromptCacheLayer(mustflow, 'task');
543
+ const sources = readOptionalStringArray(layer, 'sources') ?? [...DEFAULT_PROMPT_CACHE_TASK_SOURCES];
544
+ const selectedSkillPaths = selectedSkillRoutePaths(routeReport);
545
+ return {
546
+ cache_layer: 'task',
547
+ blocks: sources.flatMap((source, index) => {
548
+ const selectionPolicy = taskSourceSelectionPolicy(source);
549
+ const sourceKind = taskSourceKind(source);
550
+ const referencePath = taskSourceReferencePath(source);
551
+ const content = referencePath === null ? null : safeRead(projectRoot, referencePath);
552
+ const routeCandidateContent = source === 'skill_route_candidates'
553
+ ? readCompactSkillRouteCandidateContent(routeReport)
554
+ : null;
555
+ const deferFallback = isDeferredTaskFallback(selectionPolicy);
556
+ let contentHash = null;
557
+ let renderedBytes = null;
558
+ let blockRenderedDigest = null;
559
+ if (routeCandidateContent !== null) {
560
+ contentHash = sha256(routeCandidateContent);
561
+ renderedBytes = measurePromptCacheReferenceBlockBytes(SKILL_ROUTE_CANDIDATES_VIRTUAL_PATH, routeCandidateContent);
562
+ blockRenderedDigest = renderedDigest(SKILL_ROUTE_CANDIDATES_VIRTUAL_PATH, routeCandidateContent);
563
+ }
564
+ else if (content !== null) {
565
+ contentHash = sha256(content);
566
+ if (referencePath !== null && !deferFallback) {
567
+ renderedBytes = measurePromptCacheReferenceBlockBytes(referencePath, content);
568
+ blockRenderedDigest = renderedDigest(referencePath, content);
569
+ }
570
+ }
571
+ const hasResolvedMatchingSkill = source === 'matching_skill' && selectedSkillPaths.length > 0;
572
+ const hasResolvedDynamicSource = hasResolvedMatchingSkill || routeCandidateContent !== null;
573
+ const issue = !hasResolvedDynamicSource && (content === null || deferFallback)
574
+ ? taskSourceIssue(referencePath ?? source, sourceKind === 'file_reference' ? 'hash_only_deferred' : 'dynamic_unmeasured')
575
+ : null;
576
+ const baseBlock = {
577
+ id: `task:${index + 1}:${source}`,
578
+ cache_layer: 'task',
579
+ order: index + 1,
580
+ kind: sourceKind === 'file_reference' && content !== null && !deferFallback ? 'file' : 'source_placeholder',
581
+ path: sourceKind === 'file_reference' && content !== null ? referencePath : null,
582
+ source,
583
+ source_kind: sourceKind,
584
+ selection_policy: selectionPolicy,
585
+ content_included: false,
586
+ content_hash: contentHash,
587
+ rendered_digest: blockRenderedDigest,
588
+ rendered_bytes: renderedBytes,
589
+ estimated_tokens: renderedBytes === null ? null : estimateTokens(renderedBytes),
590
+ cacheability: taskSourceCacheability(source, selectionPolicy),
591
+ reload_on: taskSourceReloadOn(source, selectionPolicy),
592
+ issue,
593
+ };
594
+ if (source !== 'matching_skill') {
595
+ return [baseBlock];
596
+ }
597
+ const selectedBlocks = selectedSkillPaths.map((skillPath, selectedIndex) => {
598
+ const normalizedPath = toPosixPath(skillPath);
599
+ const skillContent = safeRead(projectRoot, normalizedPath);
600
+ const skillRenderedBytes = skillContent === null ? null : measurePromptCacheReferenceBlockBytes(normalizedPath, skillContent);
601
+ const skillIssue = skillContent === null
602
+ ? taskSourceIssue(normalizedPath, 'hash_only_deferred')
603
+ : null;
604
+ return {
605
+ id: `task:${index + 1}:matching_skill:${selectedIndex + 1}:${normalizedPath}`,
606
+ cache_layer: 'task',
607
+ order: selectedTaskBlockOrder(index, selectedIndex),
608
+ kind: skillContent === null ? 'source_placeholder' : 'file',
609
+ path: skillContent === null ? null : normalizedPath,
610
+ source,
611
+ source_kind: skillContent === null ? 'dynamic_selection' : 'file_reference',
612
+ selection_policy: 'selected_at_runtime',
613
+ content_included: false,
614
+ content_hash: skillContent === null ? null : sha256(skillContent),
615
+ rendered_digest: skillContent === null ? null : renderedDigest(normalizedPath, skillContent),
616
+ rendered_bytes: skillRenderedBytes,
617
+ estimated_tokens: skillRenderedBytes === null ? null : estimateTokens(skillRenderedBytes),
618
+ cacheability: 'runtime_selection',
619
+ reload_on: ['route_resolution_changed', 'source_file_hash_changed'],
620
+ issue: skillIssue,
621
+ };
622
+ });
623
+ return [baseBlock, ...selectedBlocks];
624
+ }),
625
+ };
626
+ }
627
+ function readVolatilePromptBundleLayer(mustflow) {
628
+ const layer = readPromptCacheLayer(mustflow, 'volatile');
629
+ const sources = readOptionalStringArray(layer, 'sources') ?? [...DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES];
630
+ const issue = 'volatile suffix sources are runtime-only; current task, changed files, and command tails are not rendered here';
631
+ return {
632
+ cache_layer: 'volatile',
633
+ blocks: sources.map((source, index) => ({
634
+ id: `volatile:${index + 1}:${source}`,
635
+ cache_layer: 'volatile',
636
+ order: index + 1,
637
+ kind: 'source_placeholder',
638
+ path: null,
639
+ source,
640
+ source_kind: 'runtime_volatile',
641
+ selection_policy: 'volatile_runtime',
642
+ content_included: false,
643
+ content_hash: null,
644
+ rendered_digest: null,
645
+ rendered_bytes: null,
646
+ estimated_tokens: null,
647
+ cacheability: 'volatile_suffix',
648
+ reload_on: ['volatile_state_changed'],
649
+ issue,
650
+ })),
651
+ };
652
+ }
653
+ function readPromptBundle(projectRoot, mustflow, profile, routeReport = null) {
654
+ const layers = [];
655
+ if (profile === 'stable' || profile === 'all') {
656
+ layers.push(readStablePromptBundleLayer(projectRoot, mustflow));
657
+ }
658
+ if (profile === 'task' || profile === 'all') {
659
+ layers.push(readTaskPromptBundleLayer(projectRoot, mustflow, routeReport));
660
+ }
661
+ if (profile === 'volatile' || profile === 'all') {
662
+ layers.push(readVolatilePromptBundleLayer(mustflow));
663
+ }
664
+ const blocks = layers.flatMap((layer) => layer.blocks);
665
+ const shapeMaterial = blocks
666
+ .map((block) => [block.cache_layer, block.order, block.kind, block.path ?? '', block.source ?? '', block.selection_policy, block.cacheability].join('\0'))
667
+ .join('\n');
668
+ const bundleMaterial = blocks
669
+ .map((block) => [block.id, block.content_hash ?? '', block.rendered_digest ?? '', block.issue ?? ''].join('\0'))
670
+ .join('\n');
671
+ return {
672
+ schema_version: '1',
673
+ renderer: 'reference_bundle_utf8_lf_v1',
674
+ content_included: false,
675
+ request_shape_hash: sha256(shapeMaterial),
676
+ bundle_hash: sha256(bundleMaterial),
677
+ layers,
678
+ issues: [...new Set(blocks.flatMap((block) => (block.issue === null ? [] : [block.issue])))],
679
+ };
680
+ }
681
+ function isPromptBundleBlock(value) {
682
+ return isRecord(value) && typeof value.id === 'string' && typeof value.cache_layer === 'string' && typeof value.order === 'number';
683
+ }
684
+ function isPromptBundleLayer(value) {
685
+ return isRecord(value) && typeof value.cache_layer === 'string' && Array.isArray(value.blocks) && value.blocks.every(isPromptBundleBlock);
686
+ }
687
+ function isPromptBundleContext(value) {
688
+ return (isRecord(value) &&
689
+ typeof value.request_shape_hash === 'string' &&
690
+ typeof value.bundle_hash === 'string' &&
691
+ Array.isArray(value.layers) &&
692
+ value.layers.every(isPromptBundleLayer));
693
+ }
694
+ function readBaselinePromptBundle(projectRoot, baselinePath) {
695
+ const normalizedPath = toPosixPath(baselinePath);
696
+ const readResult = readMustflowTextFileResult(projectRoot, normalizedPath, { maxBytes: MUSTFLOW_JSON_MAX_BYTES });
697
+ if (!readResult.ok) {
698
+ if (!readResult.exists) {
699
+ return {
700
+ status: 'baseline_missing',
701
+ bundle: null,
702
+ issues: [`baseline context report is missing: ${normalizedPath}`],
703
+ };
704
+ }
705
+ return {
706
+ status: 'baseline_unreadable',
707
+ bundle: null,
708
+ issues: [`baseline context report is unreadable: ${readResult.error ?? normalizedPath}`],
709
+ };
710
+ }
711
+ let parsed;
712
+ try {
713
+ parsed = JSON.parse(readResult.content);
714
+ }
715
+ catch (error) {
716
+ return {
717
+ status: 'baseline_invalid',
718
+ bundle: null,
719
+ issues: [`baseline context report is not valid JSON: ${error instanceof Error ? error.message : String(error)}`],
720
+ };
721
+ }
722
+ if (!isRecord(parsed) || !('prompt_bundle' in parsed)) {
723
+ return {
724
+ status: 'baseline_without_prompt_bundle',
725
+ bundle: null,
726
+ issues: [`baseline context report does not contain prompt_bundle: ${normalizedPath}`],
727
+ };
728
+ }
729
+ if (!isPromptBundleContext(parsed.prompt_bundle)) {
730
+ return {
731
+ status: 'baseline_invalid',
732
+ bundle: null,
733
+ issues: [`baseline prompt_bundle has an unsupported shape: ${normalizedPath}`],
734
+ };
735
+ }
736
+ return { status: 'unchanged', bundle: parsed.prompt_bundle, issues: [] };
737
+ }
738
+ function blockKey(block) {
739
+ return `${block.cache_layer}:${block.order}`;
740
+ }
741
+ function blockLocation(block) {
742
+ return [blockKey(block), block.path ?? block.source ?? block.id].join(':');
743
+ }
744
+ function compareStringArray(left, right) {
745
+ return left.length === right.length && left.every((entry, index) => entry === right[index]);
746
+ }
747
+ function changedBlockFields(current, baseline) {
748
+ const fields = [];
749
+ if (current.id !== baseline.id) {
750
+ fields.push('id');
751
+ }
752
+ if (current.kind !== baseline.kind) {
753
+ fields.push('kind');
754
+ }
755
+ if (current.path !== baseline.path) {
756
+ fields.push('path');
757
+ }
758
+ if (current.source !== baseline.source) {
759
+ fields.push('source');
760
+ }
761
+ if (current.source_kind !== baseline.source_kind) {
762
+ fields.push('source_kind');
763
+ }
764
+ if (current.selection_policy !== baseline.selection_policy) {
765
+ fields.push('selection_policy');
766
+ }
767
+ if (current.content_hash !== baseline.content_hash) {
768
+ fields.push('content_hash');
769
+ }
770
+ if (current.rendered_digest !== baseline.rendered_digest) {
771
+ fields.push('rendered_digest');
772
+ }
773
+ if (current.rendered_bytes !== baseline.rendered_bytes) {
774
+ fields.push('rendered_bytes');
775
+ }
776
+ if (current.estimated_tokens !== baseline.estimated_tokens) {
777
+ fields.push('estimated_tokens');
778
+ }
779
+ if (current.cacheability !== baseline.cacheability) {
780
+ fields.push('cacheability');
781
+ }
782
+ if (!compareStringArray(current.reload_on, baseline.reload_on)) {
783
+ fields.push('reload_on');
784
+ }
785
+ if (current.issue !== baseline.issue) {
786
+ fields.push('issue');
787
+ }
788
+ return fields;
789
+ }
790
+ function flattenBundleBlocks(bundle) {
791
+ return bundle.layers.flatMap((layer) => layer.blocks);
792
+ }
793
+ function stableBundleBlocks(bundle) {
794
+ return flattenBundleBlocks(bundle).filter((block) => block.cache_layer === 'stable');
795
+ }
796
+ function promptBundleBlocksMatch(current, baseline) {
797
+ return changedBlockFields(current, baseline).length === 0;
798
+ }
799
+ function comparePromptBundles(current, baseline) {
800
+ const currentBlocks = flattenBundleBlocks(current);
801
+ const baselineBlocks = flattenBundleBlocks(baseline);
802
+ const currentByKey = new Map(currentBlocks.map((block) => [blockKey(block), block]));
803
+ const baselineByKey = new Map(baselineBlocks.map((block) => [blockKey(block), block]));
804
+ const keys = [...new Set([...baselineBlocks.map(blockKey), ...currentBlocks.map(blockKey)])].sort();
805
+ const diffs = [];
806
+ for (const key of keys) {
807
+ const currentBlock = currentByKey.get(key);
808
+ const baselineBlock = baselineByKey.get(key);
809
+ if (currentBlock && !baselineBlock) {
810
+ diffs.push({
811
+ kind: 'added',
812
+ cache_layer: currentBlock.cache_layer,
813
+ order: currentBlock.order,
814
+ current_id: currentBlock.id,
815
+ baseline_id: null,
816
+ reason: `prompt bundle block added at ${key}`,
817
+ fields: [],
818
+ });
819
+ continue;
820
+ }
821
+ if (!currentBlock && baselineBlock) {
822
+ diffs.push({
823
+ kind: 'removed',
824
+ cache_layer: baselineBlock.cache_layer,
825
+ order: baselineBlock.order,
826
+ current_id: null,
827
+ baseline_id: baselineBlock.id,
828
+ reason: `prompt bundle block removed at ${key}`,
829
+ fields: [],
830
+ });
831
+ continue;
832
+ }
833
+ if (!currentBlock || !baselineBlock) {
834
+ continue;
835
+ }
836
+ const fields = changedBlockFields(currentBlock, baselineBlock);
837
+ if (fields.length > 0) {
838
+ diffs.push({
839
+ kind: 'changed',
840
+ cache_layer: currentBlock.cache_layer,
841
+ order: currentBlock.order,
842
+ current_id: currentBlock.id,
843
+ baseline_id: baselineBlock.id,
844
+ reason: `prompt bundle block changed at ${key}: ${fields.join(', ')}`,
845
+ fields,
846
+ });
847
+ }
848
+ }
849
+ return diffs;
850
+ }
851
+ function emptyStableDiff(currentBundle) {
852
+ return {
853
+ stable_block_count: stableBundleBlocks(currentBundle).length,
854
+ baseline_stable_block_count: null,
855
+ matching_prefix_blocks: 0,
856
+ matching_prefix_ratio: null,
857
+ stable_prefix_preserved: null,
858
+ first_stable_difference: null,
859
+ first_stable_invalidation_reason: null,
860
+ };
861
+ }
862
+ function readStablePrefixDifference(currentBlock, baselineBlock, index) {
863
+ const key = `stable prefix block ${index + 1}`;
864
+ if (currentBlock && !baselineBlock) {
865
+ return {
866
+ kind: 'added',
867
+ cache_layer: currentBlock.cache_layer,
868
+ order: currentBlock.order,
869
+ current_id: currentBlock.id,
870
+ baseline_id: null,
871
+ reason: `${key} added at ${blockLocation(currentBlock)}`,
872
+ fields: [],
873
+ };
874
+ }
875
+ if (!currentBlock && baselineBlock) {
876
+ return {
877
+ kind: 'removed',
878
+ cache_layer: baselineBlock.cache_layer,
879
+ order: baselineBlock.order,
880
+ current_id: null,
881
+ baseline_id: baselineBlock.id,
882
+ reason: `${key} removed at ${blockLocation(baselineBlock)}`,
883
+ fields: [],
884
+ };
885
+ }
886
+ if (!currentBlock || !baselineBlock) {
887
+ throw new Error('stable prefix comparison reached an impossible block state');
888
+ }
889
+ const fields = changedBlockFields(currentBlock, baselineBlock);
890
+ return {
891
+ kind: 'changed',
892
+ cache_layer: currentBlock.cache_layer,
893
+ order: currentBlock.order,
894
+ current_id: currentBlock.id,
895
+ baseline_id: baselineBlock.id,
896
+ reason: `${key} changed at ${blockLocation(currentBlock)}: ${fields.join(', ')}`,
897
+ fields,
898
+ };
899
+ }
900
+ function readPromptBundleStableDiff(currentBundle, baselineBundle) {
901
+ const currentStableBlocks = stableBundleBlocks(currentBundle);
902
+ const baselineStableBlocks = stableBundleBlocks(baselineBundle);
903
+ const comparedBlockCount = Math.max(currentStableBlocks.length, baselineStableBlocks.length);
904
+ let matchingPrefixBlocks = 0;
905
+ let firstStableDifference = null;
906
+ let index = 0;
907
+ while (index < comparedBlockCount) {
908
+ const currentBlock = currentStableBlocks[index];
909
+ const baselineBlock = baselineStableBlocks[index];
910
+ if (!currentBlock || !baselineBlock || !promptBundleBlocksMatch(currentBlock, baselineBlock)) {
911
+ firstStableDifference = readStablePrefixDifference(currentBlock, baselineBlock, index);
912
+ break;
913
+ }
914
+ matchingPrefixBlocks += 1;
915
+ index += 1;
916
+ }
917
+ return {
918
+ stable_block_count: currentStableBlocks.length,
919
+ baseline_stable_block_count: baselineStableBlocks.length,
920
+ matching_prefix_blocks: matchingPrefixBlocks,
921
+ matching_prefix_ratio: comparedBlockCount === 0
922
+ ? null
923
+ : Number((matchingPrefixBlocks / comparedBlockCount).toFixed(6)),
924
+ stable_prefix_preserved: firstStableDifference === null && currentStableBlocks.length === baselineStableBlocks.length,
925
+ first_stable_difference: firstStableDifference,
926
+ first_stable_invalidation_reason: firstStableDifference?.reason ?? null,
927
+ };
928
+ }
929
+ function readPromptBundleDiff(projectRoot, currentBundle, baselinePath) {
930
+ const normalizedPath = toPosixPath(baselinePath);
931
+ const baseline = readBaselinePromptBundle(projectRoot, normalizedPath);
932
+ if (!baseline.bundle) {
933
+ return {
934
+ baseline_path: normalizedPath,
935
+ status: baseline.status,
936
+ baseline_request_shape_hash: null,
937
+ current_request_shape_hash: currentBundle.request_shape_hash,
938
+ request_shape_changed: null,
939
+ baseline_bundle_hash: null,
940
+ current_bundle_hash: currentBundle.bundle_hash,
941
+ bundle_changed: null,
942
+ first_difference: null,
943
+ stable_diff: emptyStableDiff(currentBundle),
944
+ changed_blocks: [],
945
+ issues: baseline.issues,
946
+ };
947
+ }
948
+ const changedBlocks = comparePromptBundles(currentBundle, baseline.bundle);
949
+ const requestShapeChanged = baseline.bundle.request_shape_hash !== currentBundle.request_shape_hash;
950
+ const bundleChanged = baseline.bundle.bundle_hash !== currentBundle.bundle_hash;
951
+ return {
952
+ baseline_path: normalizedPath,
953
+ status: requestShapeChanged || bundleChanged || changedBlocks.length > 0 ? 'changed' : 'unchanged',
954
+ baseline_request_shape_hash: baseline.bundle.request_shape_hash,
955
+ current_request_shape_hash: currentBundle.request_shape_hash,
956
+ request_shape_changed: requestShapeChanged,
957
+ baseline_bundle_hash: baseline.bundle.bundle_hash,
958
+ current_bundle_hash: currentBundle.bundle_hash,
959
+ bundle_changed: bundleChanged,
960
+ first_difference: changedBlocks[0] ?? null,
961
+ stable_diff: readPromptBundleStableDiff(currentBundle, baseline.bundle),
962
+ changed_blocks: changedBlocks.slice(0, 20),
963
+ issues: [],
964
+ };
965
+ }
966
+ function readStablePromptCacheAuditLayer(projectRoot, mustflow, settings) {
967
+ const layer = readPromptCacheLayer(mustflow, 'stable');
968
+ const read = readOptionalStringArray(layer, 'read') ?? [...DEFAULT_PROMPT_CACHE_STABLE_READ];
969
+ const budget = budgetBytes(settings.max_stable_prefix_kb);
970
+ const targetKb = readNumber(layer, 'target_kb');
971
+ const target = budgetBytes(targetKb);
972
+ const issues = [];
973
+ const blocks = read.map((relativePath, index) => {
974
+ const content = safeRead(projectRoot, relativePath);
975
+ const id = `stable:${index + 1}:${toPosixPath(relativePath)}`;
976
+ if (content === null) {
977
+ const issue = `stable prefix document is missing: ${toPosixPath(relativePath)}`;
978
+ issues.push(issue);
979
+ return {
980
+ id,
981
+ kind: 'file',
982
+ path: toPosixPath(relativePath),
983
+ source: null,
984
+ source_kind: 'file_reference',
985
+ selection_policy: 'always_rendered',
986
+ measurement_status: 'measured',
987
+ candidate_exists: false,
988
+ candidate_content_hash: null,
989
+ exists: false,
990
+ content_hash: null,
991
+ rendered_bytes: 0,
992
+ estimated_tokens: 0,
993
+ budget_share: calculateBudgetShare(0, budget),
994
+ issue,
995
+ };
996
+ }
997
+ const renderedBytes = measurePromptCacheReferenceBlockBytes(relativePath, content);
998
+ const contentHash = sha256(content);
999
+ return {
1000
+ id,
1001
+ kind: 'file',
1002
+ path: toPosixPath(relativePath),
1003
+ source: null,
1004
+ source_kind: 'file_reference',
1005
+ selection_policy: 'always_rendered',
1006
+ measurement_status: 'measured',
1007
+ candidate_exists: true,
1008
+ candidate_content_hash: contentHash,
1009
+ exists: true,
1010
+ content_hash: contentHash,
1011
+ rendered_bytes: renderedBytes,
1012
+ estimated_tokens: estimateTokens(renderedBytes),
1013
+ budget_share: calculateBudgetShare(renderedBytes, budget),
1014
+ issue: null,
1015
+ };
1016
+ });
1017
+ const renderedBytes = blocks.reduce((total, block) => total + (block.rendered_bytes ?? 0), 0);
1018
+ const estimatedTokens = estimateTokens(renderedBytes);
1019
+ const status = budgetStatus(renderedBytes, budget);
1020
+ const targetStatus = budgetStatus(renderedBytes, target);
1021
+ if (status === 'over_budget') {
1022
+ issues.push(`stable prefix exceeds max_stable_prefix_kb: ${renderedBytes} rendered bytes > ${budget} budget bytes`);
1023
+ }
1024
+ return {
1025
+ cache_layer: 'stable',
1026
+ budget_kb: settings.max_stable_prefix_kb,
1027
+ budget_bytes: budget,
1028
+ target_kb: targetKb,
1029
+ target_bytes: target,
1030
+ target_status: targetStatus,
1031
+ rendered_bytes: renderedBytes,
1032
+ estimated_tokens: estimatedTokens,
1033
+ budget_status: status,
1034
+ blocks,
1035
+ largest_blocks: [...blocks]
1036
+ .sort((left, right) => (right.rendered_bytes ?? 0) - (left.rendered_bytes ?? 0))
1037
+ .slice(0, 5),
1038
+ issues,
1039
+ };
1040
+ }
1041
+ function readTaskSourceAuditLayer(projectRoot, sources, budgetKb, routeReport) {
1042
+ const budget = budgetBytes(budgetKb);
1043
+ const issues = new Set();
1044
+ const selectedSkillPaths = selectedSkillRoutePaths(routeReport);
1045
+ const blocks = sources.flatMap((source, index) => {
1046
+ const selectionPolicy = taskSourceSelectionPolicy(source);
1047
+ const sourceKind = taskSourceKind(source);
1048
+ const referencePath = taskSourceReferencePath(source);
1049
+ const content = referencePath === null ? null : safeRead(projectRoot, referencePath);
1050
+ const routeCandidateContent = source === 'skill_route_candidates'
1051
+ ? readCompactSkillRouteCandidateContent(routeReport)
1052
+ : null;
1053
+ const deferFallback = isDeferredTaskFallback(selectionPolicy);
1054
+ let contentHash = null;
1055
+ let renderedBytes = null;
1056
+ if (routeCandidateContent !== null) {
1057
+ contentHash = sha256(routeCandidateContent);
1058
+ renderedBytes = measurePromptCacheReferenceBlockBytes(SKILL_ROUTE_CANDIDATES_VIRTUAL_PATH, routeCandidateContent);
1059
+ }
1060
+ else if (content !== null) {
1061
+ contentHash = sha256(content);
1062
+ if (referencePath !== null && !deferFallback) {
1063
+ renderedBytes = measurePromptCacheReferenceBlockBytes(referencePath, content);
1064
+ }
1065
+ }
1066
+ const measurementStatus = routeCandidateContent !== null
1067
+ ? 'measured'
1068
+ : sourceKind === 'file_reference'
1069
+ ? content === null || deferFallback
1070
+ ? 'hash_only_deferred'
1071
+ : 'measured'
1072
+ : 'dynamic_unmeasured';
1073
+ const hasResolvedMatchingSkill = source === 'matching_skill' && selectedSkillPaths.length > 0;
1074
+ const hasResolvedDynamicSource = hasResolvedMatchingSkill || routeCandidateContent !== null;
1075
+ const issue = !hasResolvedDynamicSource && (content === null || deferFallback)
1076
+ ? taskSourceIssue(referencePath ?? source, measurementStatus)
1077
+ : null;
1078
+ if (issue) {
1079
+ issues.add(issue);
1080
+ }
1081
+ if (!hasResolvedDynamicSource && sourceKind === 'dynamic_selection') {
1082
+ issues.add(taskSourceIssue(source, measurementStatus));
1083
+ }
1084
+ const baseBlock = {
1085
+ id: `task:${index + 1}:${source}`,
1086
+ kind: sourceKind === 'file_reference' && content !== null && !deferFallback ? 'file' : 'source_placeholder',
1087
+ path: sourceKind === 'file_reference' && content !== null ? referencePath : null,
1088
+ source,
1089
+ source_kind: sourceKind,
1090
+ selection_policy: selectionPolicy,
1091
+ measurement_status: measurementStatus,
1092
+ candidate_exists: sourceKind === 'file_reference' ? content !== null : null,
1093
+ candidate_content_hash: contentHash,
1094
+ exists: sourceKind === 'file_reference' ? content !== null : null,
1095
+ content_hash: contentHash,
1096
+ rendered_bytes: renderedBytes,
1097
+ estimated_tokens: renderedBytes === null ? null : estimateTokens(renderedBytes),
1098
+ budget_share: calculateBudgetShare(renderedBytes, budget),
1099
+ issue,
1100
+ };
1101
+ if (source !== 'matching_skill') {
1102
+ return [baseBlock];
1103
+ }
1104
+ const selectedBlocks = selectedSkillPaths.map((skillPath, selectedIndex) => {
1105
+ const normalizedPath = toPosixPath(skillPath);
1106
+ const skillContent = safeRead(projectRoot, normalizedPath);
1107
+ const skillRenderedBytes = skillContent === null ? null : measurePromptCacheReferenceBlockBytes(normalizedPath, skillContent);
1108
+ const skillMeasurementStatus = skillContent === null
1109
+ ? 'hash_only_deferred'
1110
+ : 'measured';
1111
+ const skillIssue = skillContent === null ? taskSourceIssue(normalizedPath, skillMeasurementStatus) : null;
1112
+ if (skillIssue) {
1113
+ issues.add(skillIssue);
1114
+ }
1115
+ return {
1116
+ id: `task:${index + 1}:matching_skill:${selectedIndex + 1}:${normalizedPath}`,
1117
+ kind: skillContent === null ? 'source_placeholder' : 'file',
1118
+ path: skillContent === null ? null : normalizedPath,
1119
+ source,
1120
+ source_kind: skillContent === null ? 'dynamic_selection' : 'file_reference',
1121
+ selection_policy: 'selected_at_runtime',
1122
+ measurement_status: skillMeasurementStatus,
1123
+ candidate_exists: skillContent !== null,
1124
+ candidate_content_hash: skillContent === null ? null : sha256(skillContent),
1125
+ exists: skillContent !== null,
1126
+ content_hash: skillContent === null ? null : sha256(skillContent),
1127
+ rendered_bytes: skillRenderedBytes,
1128
+ estimated_tokens: skillRenderedBytes === null ? null : estimateTokens(skillRenderedBytes),
1129
+ budget_share: calculateBudgetShare(skillRenderedBytes, budget),
1130
+ issue: skillIssue,
1131
+ };
1132
+ });
1133
+ return [baseBlock, ...selectedBlocks];
1134
+ });
1135
+ const measuredBytes = blocks.reduce((total, block) => total + (block.rendered_bytes ?? 0), 0);
1136
+ const renderedBytes = measuredBytes > 0 ? measuredBytes : null;
1137
+ const hasDynamicSources = blocks.some((block) => block.measurement_status === 'dynamic_unmeasured');
1138
+ const measuredStatus = budgetStatus(renderedBytes, budget);
1139
+ const status = measuredStatus === 'over_budget' || !hasDynamicSources
1140
+ ? measuredStatus
1141
+ : 'unknown';
1142
+ if (measuredStatus === 'over_budget' && renderedBytes !== null && budget !== null) {
1143
+ issues.add(`task context measured sources exceed max_task_context_kb: ${renderedBytes} rendered bytes > ${budget} budget bytes`);
1144
+ }
1145
+ return {
1146
+ cache_layer: 'task',
1147
+ budget_kb: budgetKb,
1148
+ budget_bytes: budget,
1149
+ target_kb: null,
1150
+ target_bytes: null,
1151
+ target_status: 'unknown',
1152
+ rendered_bytes: renderedBytes,
1153
+ estimated_tokens: renderedBytes === null ? null : estimateTokens(renderedBytes),
1154
+ budget_status: status,
1155
+ blocks,
1156
+ largest_blocks: [...blocks]
1157
+ .filter((block) => block.rendered_bytes !== null)
1158
+ .sort((left, right) => (right.rendered_bytes ?? 0) - (left.rendered_bytes ?? 0))
1159
+ .slice(0, 5),
1160
+ issues: [...issues],
1161
+ };
1162
+ }
1163
+ function readVolatileSourceAuditLayer(sources, budgetKb) {
1164
+ const issue = 'volatile suffix sources are runtime-only; current task, changed files, and command tails are not rendered here';
1165
+ const blocks = sources.map((source, index) => ({
1166
+ id: `volatile:${index + 1}:${source}`,
1167
+ kind: 'source_placeholder',
1168
+ path: null,
1169
+ source,
1170
+ source_kind: 'runtime_volatile',
1171
+ selection_policy: 'volatile_runtime',
1172
+ measurement_status: 'dynamic_unmeasured',
1173
+ candidate_exists: null,
1174
+ candidate_content_hash: null,
1175
+ exists: null,
1176
+ content_hash: null,
1177
+ rendered_bytes: null,
1178
+ estimated_tokens: null,
1179
+ budget_share: null,
1180
+ issue,
1181
+ }));
1182
+ return {
1183
+ cache_layer: 'volatile',
1184
+ budget_kb: budgetKb,
1185
+ budget_bytes: budgetBytes(budgetKb),
1186
+ target_kb: null,
1187
+ target_bytes: null,
1188
+ target_status: 'unknown',
1189
+ rendered_bytes: null,
1190
+ estimated_tokens: null,
1191
+ budget_status: 'unknown',
1192
+ blocks,
1193
+ largest_blocks: [],
1194
+ issues: [issue],
1195
+ };
1196
+ }
1197
+ function readPromptCacheAuditSummary(layers) {
1198
+ const blocks = layers.flatMap((layer) => layer.blocks);
1199
+ const measuredBytes = blocks.reduce((total, block) => total + (block.rendered_bytes ?? 0), 0);
1200
+ const measuredBlockCount = blocks.filter((block) => block.measurement_status === 'measured').length;
1201
+ const stableLayer = layers.find((layer) => layer.cache_layer === 'stable') ?? null;
1202
+ const taskLayer = layers.find((layer) => layer.cache_layer === 'task') ?? null;
1203
+ const volatileLayer = layers.find((layer) => layer.cache_layer === 'volatile') ?? null;
1204
+ const stableLayerIndex = layers.findIndex((layer) => layer.cache_layer === 'stable');
1205
+ const volatileBeforeStableCount = stableLayerIndex === -1
1206
+ ? 0
1207
+ : layers.slice(0, stableLayerIndex).filter((layer) => layer.cache_layer === 'volatile').length;
1208
+ const stableLargestBlockBudgetShare = stableLayer?.largest_blocks
1209
+ .map((block) => block.budget_share)
1210
+ .filter((share) => share !== null)
1211
+ .sort((left, right) => right - left)[0] ?? null;
1212
+ const stableLeafSkillRiskPaths = stableLayer?.blocks
1213
+ .map((block) => block.path)
1214
+ .filter((blockPath) => blockPath !== null && isPromptCacheStableLeafSkillSurface(blockPath)) ?? [];
1215
+ return {
1216
+ rendered_bytes: measuredBlockCount === 0 ? null : measuredBytes,
1217
+ estimated_tokens: measuredBlockCount === 0 ? null : estimateTokens(measuredBytes),
1218
+ measured_block_count: measuredBlockCount,
1219
+ dynamic_source_count: blocks.filter((block) => block.source_kind !== 'file_reference').length,
1220
+ unresolved_reference_count: blocks.filter((block) => block.source_kind === 'file_reference' && block.measurement_status !== 'measured').length,
1221
+ volatile_before_stable_count: volatileBeforeStableCount,
1222
+ serialization_deterministic: true,
1223
+ stable_leaf_skill_isolated: stableLayer === null ? null : stableLeafSkillRiskPaths.length === 0,
1224
+ stable_leaf_skill_risk_paths: stableLeafSkillRiskPaths,
1225
+ leaf_skill_change_stable_hash_delta: stableLeafSkillRiskPaths.length === 0 ? 0 : null,
1226
+ stable_rendered_bytes: stableLayer?.rendered_bytes ?? null,
1227
+ stable_estimated_tokens: stableLayer?.estimated_tokens ?? null,
1228
+ stable_budget_status: stableLayer?.budget_status ?? null,
1229
+ stable_largest_block_budget_share: stableLargestBlockBudgetShare,
1230
+ task_budget_status: taskLayer?.budget_status ?? null,
1231
+ volatile_budget_status: volatileLayer?.budget_status ?? null,
1232
+ };
1233
+ }
1234
+ function readPromptCacheAudit(projectRoot, mustflow, profile, settings, routeReport = null) {
1235
+ const layers = [];
1236
+ if (profile === 'stable' || profile === 'all') {
1237
+ layers.push(readStablePromptCacheAuditLayer(projectRoot, mustflow, settings));
1238
+ }
1239
+ if (profile === 'task' || profile === 'all') {
1240
+ const taskLayer = readPromptCacheLayer(mustflow, 'task');
1241
+ layers.push(readTaskSourceAuditLayer(projectRoot, readOptionalStringArray(taskLayer, 'sources') ?? [...DEFAULT_PROMPT_CACHE_TASK_SOURCES], settings.max_task_context_kb, routeReport));
1242
+ }
1243
+ if (profile === 'volatile' || profile === 'all') {
1244
+ const volatileLayer = readPromptCacheLayer(mustflow, 'volatile');
1245
+ layers.push(readVolatileSourceAuditLayer(readOptionalStringArray(volatileLayer, 'sources') ?? [...DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES], settings.max_volatile_suffix_kb));
1246
+ }
1247
+ return {
1248
+ measurement: 'reference_bundle',
1249
+ estimator: {
1250
+ name: 'reference_bundle_utf8_lf_v1',
1251
+ estimated_bytes_per_token: 4,
1252
+ caveat: 'Token counts are rough byte-based estimates, not provider billing or tokenizer output.',
1253
+ },
1254
+ canonicalization: [
1255
+ 'UTF-8 byte length',
1256
+ 'LF line endings',
1257
+ 'trim trailing blank lines',
1258
+ 'stable document path header before each block',
1259
+ ],
1260
+ summary: readPromptCacheAuditSummary(layers),
1261
+ layers,
1262
+ issues: layers.flatMap((layer) => layer.issues),
1263
+ };
1264
+ }
1265
+ export async function getPromptCacheProfileContext(projectRoot, profile, options = {}) {
307
1266
  const mustflow = readTomlTableIfExists(projectRoot, MUSTFLOW_RELATIVE_PATH);
308
1267
  const lockInspection = inspectManifestLock(projectRoot);
1268
+ const promptCacheSettings = readPromptCacheSettings(mustflow);
1269
+ const routeReport = resolvePromptCacheSkillRoutes(projectRoot, options.routeInput);
1270
+ const promptBundle = readPromptBundle(projectRoot, mustflow, profile, routeReport);
309
1271
  const output = {
310
1272
  schema_version: CONTEXT_SCHEMA_VERSION,
311
1273
  command: 'context',
312
1274
  cache_profile: profile,
313
- prompt_cache: readPromptCacheSettings(mustflow),
1275
+ prompt_cache: promptCacheSettings,
1276
+ prompt_bundle: promptBundle,
1277
+ ...(options.comparePath
1278
+ ? {
1279
+ prompt_bundle_diff: readPromptBundleDiff(projectRoot, promptBundle, options.comparePath),
1280
+ }
1281
+ : {}),
1282
+ ...(options.includeAudit
1283
+ ? {
1284
+ cache_audit: readPromptCacheAudit(projectRoot, mustflow, profile, promptCacheSettings, routeReport),
1285
+ }
1286
+ : {}),
314
1287
  issues: lockInspection.issues,
315
1288
  };
316
1289
  if (profile === 'stable' || profile === 'all') {