@khoavu882/doflow 2.0.0-beta.4 → 2.0.0-beta.5

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 (101) hide show
  1. package/core/.antigravity-plugin/plugin.json +0 -1
  2. package/core/.claude-plugin/plugin.json +1 -1
  3. package/core/.codex-plugin/plugin.json +1 -1
  4. package/core/.plugin/plugin.json +1 -1
  5. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
  6. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  7. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  8. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  9. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  10. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  11. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  12. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  13. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  14. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  15. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  16. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  17. package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
  18. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  19. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
  20. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
  21. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
  22. package/core/harnesses/gemini/hooks/session-start.sh +5 -3
  23. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
  24. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
  25. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
  26. package/core/harnesses/kiro/hooks/session-start.sh +11 -5
  27. package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
  28. package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
  29. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
  30. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  31. package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
  32. package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
  33. package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
  34. package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
  35. package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
  36. package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
  37. package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
  38. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
  39. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
  40. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
  41. package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
  42. package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
  43. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
  44. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
  45. package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
  46. package/core/registry/assets.json +19 -0
  47. package/core/registry/contracts.json +2 -2
  48. package/core/registry/harnesses.json +196 -10
  49. package/core/registry/lifecycle.json +2626 -7
  50. package/core/registry/storage.json +24 -1
  51. package/core/shared/scripts/doflow/bin/doflow-run +10 -6
  52. package/core/shared/skills/do-design/SKILL.md +6 -6
  53. package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
  54. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
  55. package/core/shared/skills/do-implement/SKILL.md +3 -2
  56. package/core/shared/skills/do-plan/SKILL.md +7 -8
  57. package/package.json +1 -1
  58. package/src/adapters/antigravity/index.js +83 -47
  59. package/src/adapters/claude/index.js +15 -1
  60. package/src/adapters/codex/hooks.js +40 -5
  61. package/src/adapters/codex/index.js +52 -9
  62. package/src/adapters/copy-tree.js +4 -4
  63. package/src/adapters/gemini/hooks.js +57 -16
  64. package/src/adapters/gemini/index.js +57 -18
  65. package/src/adapters/kiro/index.js +15 -1
  66. package/src/adapters/pi/hooks.js +119 -0
  67. package/src/adapters/pi/index.js +34 -9
  68. package/src/cli/commands/install.js +1 -1
  69. package/src/cli/commands/update.js +1 -1
  70. package/src/cli/index.js +17 -5
  71. package/src/cli/runtime-commands.js +51 -12
  72. package/src/lifecycle/coverage.js +57 -0
  73. package/src/lifecycle/hook-step.js +61 -0
  74. package/src/lifecycle/index.js +28 -14
  75. package/src/registry/index.js +136 -7
  76. package/src/runtime/candidate.js +505 -0
  77. package/src/runtime/claims.js +14 -5
  78. package/src/runtime/cli-result.js +11 -4
  79. package/src/runtime/cli.js +35 -268
  80. package/src/runtime/context/budget.js +203 -0
  81. package/src/runtime/context/compose.js +1367 -0
  82. package/src/runtime/context/delivery-store.js +102 -0
  83. package/src/runtime/context/mandatory.js +594 -0
  84. package/src/runtime/context/pointer.js +142 -0
  85. package/src/runtime/context-pack.js +1029 -244
  86. package/src/runtime/evidence-fields.js +39 -0
  87. package/src/runtime/evidence-ledger.js +7 -0
  88. package/src/runtime/freshness.js +44 -7
  89. package/src/runtime/outcome.js +1 -1
  90. package/src/runtime/readiness.js +83 -13
  91. package/src/runtime/retrieval-plan.js +422 -73
  92. package/src/runtime/retrieval-record.js +223 -0
  93. package/src/runtime/scaffold/artifacts.js +3 -0
  94. package/src/runtime/source-version.js +289 -0
  95. package/src/runtime/task-attempts.js +1 -1
  96. package/src/runtime/task-scope.js +28 -3
  97. package/src/runtime/task-state.js +72 -7
  98. package/core/.antigravity-plugin/hooks.json +0 -39
  99. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  100. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  101. package/core/harnesses/shared/hooks/hooks.json +0 -47
@@ -2,26 +2,61 @@
2
2
 
3
3
  // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
4
 
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const crypto = require('node:crypto');
5
8
  const { EvidenceLedger } = require('./evidence-ledger.js');
6
9
  const { ClaimsManager } = require('./claims.js');
7
10
  const { ResearchRequestStore } = require('./research-request.js');
8
- const { finishRuntime, usageError } = require('./cli-result.js');
9
- const { resolveActiveFeature } = require('./feature-resolve.js');
11
+ const { finishRuntime, usageError, EXIT } = require('./cli-result.js');
12
+ const nativeConsoleLog = console.log;
13
+ const { resolveFeature } = require('./task-scope.js');
10
14
  const { readRegister } = require('./decision-register.js');
15
+ const compose = require('./context/compose.js');
16
+ const {
17
+ FIELD_CLASSES,
18
+ buildPackItem,
19
+ buildObjectiveItem,
20
+ buildConstraintItem,
21
+ buildAcceptanceCriteriaItem,
22
+ buildVerificationRequirementItem,
23
+ buildScopeExcludedItem,
24
+ buildClaimConflictItem,
25
+ buildBlockingGapItem,
26
+ buildResearchRequestItem,
27
+ buildDecisionReasonItem,
28
+ buildDecisionLiveItem,
29
+ buildClaimSupportedItem,
30
+ buildClaimHypothesisItem,
31
+ buildPointerItem,
32
+ buildRelevantFileItem,
33
+ buildEvidenceSummaryItem,
34
+ buildStructuralContextItem,
35
+ computeEnvelopes,
36
+ linearFit,
37
+ groupOmissions,
38
+ computePackDigest,
39
+ formatOverflowMessage,
40
+ formatMarkdown,
41
+ formatJson,
42
+ } = require('./context/compose.js');
43
+ const {
44
+ extractMandatory,
45
+ parseMandatoryFile,
46
+ applyMandatoryOverride,
47
+ } = require('./context/mandatory.js');
11
48
 
12
49
  const NO_DECISIONS = Object.freeze({ available: false, live: [], liveCount: 0 });
13
50
 
14
- const TASK_CLASS_BUDGETS = Object.freeze({
15
- feature: 225000,
16
- 'dependency-change': 85000,
17
- bug: 71000,
18
- refactor: 71000,
19
- documentation: 57000,
20
- operations: 45000,
21
- review: 42000,
22
- research: 35302,
23
- 'trivial-edit': 30000,
24
- });
51
+ const { TASK_CLASS_BUDGETS, resolveBudget, getRegisteredHarnessIds, getRegisteredHarnesses } = require('./context/budget.js');
52
+ const { createDeliveryEntry, readDeliveryEntry, recordDelivery, makeTokenRecord } = require('./context/delivery-store.js');
53
+ const { boundedAcquire } = require('./task-state.js');
54
+ const pointerTools = require('./context/pointer.js');
55
+ const { updateTaskState } = require('./task-state.js');
56
+ const { taskStoreDir } = require('./task-scope.js');
57
+
58
+ let correctiveTrimFallbackCount = 0;
59
+ function getCorrectiveTrimFallbackCount() { return correctiveTrimFallbackCount; }
25
60
 
26
61
  /**
27
62
  * The live decisions of the feature the caller is working on (IC-012). The feature is resolved
@@ -34,7 +69,7 @@ const TASK_CLASS_BUDGETS = Object.freeze({
34
69
  */
35
70
  function loadLiveDecisions({ projectRoot, slug = null } ) {
36
71
  try {
37
- const feature = resolveActiveFeature({ projectRoot, slug });
72
+ const feature = resolveFeature({ projectRoot, slug });
38
73
  if (feature.error) return { ...NO_DECISIONS };
39
74
  let register;
40
75
  try {
@@ -55,6 +90,65 @@ function loadLiveDecisions({ projectRoot, slug = null } ) {
55
90
  }
56
91
  }
57
92
 
93
+ function hashSourceFiles(root , paths ) {
94
+ const result = {};
95
+ for (const source of [...new Set(paths)].sort()) {
96
+ if (typeof source !== 'string' || source === '') continue;
97
+ const absolute = path.resolve(root, source);
98
+ const relative = path.relative(path.resolve(root), absolute);
99
+ if (!relative || relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)) continue;
100
+ try {
101
+ result[relative.split(path.sep).join('/')] = `sha256:${crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex')}`;
102
+ } catch { /* unavailable source paths cannot be represented as verified sources */ }
103
+ }
104
+ return result;
105
+ }
106
+
107
+ function sourcesChanged(root , sources ) {
108
+ for (const [source, digest] of Object.entries(sources || {})) {
109
+ const current = hashSourceFiles(root, [source])[source];
110
+ if (current !== digest) return true;
111
+ }
112
+ return false;
113
+ }
114
+
115
+ function classifyStoreFailure(error ) {
116
+ if (error?.code === 'store-locked' || error?.message === 'store-locked') return 'store-locked';
117
+ if (error?.code === 'store-unreadable') return 'store-unreadable';
118
+ if (['EACCES', 'EROFS', 'EPERM', 'ENOSPC'].includes(error?.code)) return 'store-unwritable';
119
+ return 'store-unwritable';
120
+ }
121
+
122
+ function profileRetention(harnessId , profiles ) {
123
+ if (!harnessId) return null;
124
+ const harnesses = profiles || getRegisteredHarnesses();
125
+ return harnesses.find((item ) => item.id === harnessId)?.contextProfile?.retention || null;
126
+ }
127
+
128
+ function appendPointerExpansion({ projectRoot, taskId, slug, key, expansion, fsImpl = fs } ) {
129
+ const dir = taskStoreDir({ projectRoot, store: 'delivery', taskId, slug });
130
+ const file = path.join(dir, `${taskId}.json`);
131
+ updateTaskState({
132
+ fsImpl, file,
133
+ acquire: (impl , target ) => boundedAcquire(impl, target, { retries: 3, waitMs: 5 }),
134
+ build(disk ) {
135
+ const deliveries = Array.isArray(disk?.deliveries) ? disk.deliveries : [];
136
+ let found = false;
137
+ const updated = deliveries.map((entry ) => {
138
+ if (entry.key !== key) return entry;
139
+ found = true;
140
+ return { ...entry, expansions: [...(entry.expansions || []), expansion] };
141
+ });
142
+ if (!found) {
143
+ const error = new Error('delivery entry disappeared');
144
+ error.code = 'entry-evicted';
145
+ throw error;
146
+ }
147
+ return { taskId, deliveries: updated };
148
+ },
149
+ });
150
+ }
151
+
58
152
  class ContextPackCompiler {
59
153
 
60
154
  /**
@@ -69,189 +163,513 @@ class ContextPackCompiler {
69
163
  maxClaims: defaultOptions.maxClaims || 20,
70
164
  maxEvidenceItems: defaultOptions.maxEvidenceItems || 25,
71
165
  maxResearchRequests: defaultOptions.maxResearchRequests || 20,
166
+ maxStructuralItems: defaultOptions.maxStructuralItems || 5,
167
+ enableCorrective: defaultOptions.enableCorrective !== false,
168
+ selfRefWidth: defaultOptions.selfRefWidth ?? null,
72
169
  };
73
170
  }
74
171
 
75
172
  /**
76
- * Compiles a ContextPack from task evidence and claims.
173
+ * Compiles a ContextPack from task evidence and claims through compose.ts.
77
174
  * @param {Object} params
78
- * @param {string} params.taskId
79
- * @param {string} [params.taskClass='feature']
80
- * @param {string} [params.objective='']
81
- * @param {Array<string>} [params.constraints=[]]
82
- * @param {Array<string>} [params.acceptanceCriteria=[]]
83
- * @param {Object} params.evidenceLedger
84
- * @param {Object} params.claimsManager
85
175
  * @param {Object} [budgetOverrides={}]
86
176
  * @returns {Object} ContextPack
87
177
  */
88
178
  compileContextPack(params , budgetOverrides = {}) {
89
179
  const {
90
- taskId,
180
+ taskId = 'default',
91
181
  taskClass = 'feature',
92
- objective = '',
93
- constraints = [],
94
- acceptanceCriteria = [],
182
+ objective: paramObjective,
183
+ constraints: paramConstraints = [],
184
+ acceptanceCriteria: paramAc = [],
185
+ verificationRequirements: paramVr = [],
186
+ scopeExcluded: paramScope = [],
187
+ blockingGaps: paramGaps = [],
95
188
  evidenceLedger,
96
189
  claimsManager,
97
190
  researchRequests = [],
98
191
  decisions = NO_DECISIONS,
192
+ mandatory: paramMandatory,
193
+ budget: paramBudget,
194
+ maxBytes,
195
+ harness,
196
+ bare = false,
197
+ mode: paramMode,
198
+ noRecord = false,
199
+ reason,
200
+ since,
201
+ expand,
202
+ pointers: suppliedPointers = [],
203
+ expansion = null,
99
204
  } = params;
100
205
 
101
206
  const limits = { ...this.options, ...budgetOverrides };
207
+ const contentLimits = {
208
+ maxFiles: limits.maxFiles,
209
+ maxClaims: limits.maxClaims,
210
+ maxEvidenceItems: limits.maxEvidenceItems,
211
+ maxResearchRequests: limits.maxResearchRequests,
212
+ maxStructuralItems: limits.maxStructuralItems,
213
+ };
102
214
 
103
- const rawClaims = claimsManager ? claimsManager.getClaims(taskId) : [];
104
- const rawEvidence = evidenceLedger ? evidenceLedger.queryEvidence({ taskId }) : [];
215
+ const effectiveTaskClass = (!taskClass || typeof taskClass !== 'string' || taskClass.trim() === '') ? 'feature' : taskClass.trim();
216
+ const resolvedBudget = paramBudget || resolveBudget({ taskClass: effectiveTaskClass, maxBytes, harnessFlag: harness });
105
217
 
106
- // Filter claims
107
- const supportedClaims = [];
108
- const activeHypotheses = [];
109
- const conflictedClaims = [];
218
+ // Determine mandatory fields and provenance
219
+ const mandatoryFields = paramMandatory?.fields || {};
220
+ const mandatoryProv = paramMandatory?.provenance || {};
221
+
222
+ const hasParamObjective = paramObjective !== undefined && paramObjective !== null;
223
+ const objective = hasParamObjective
224
+ ? paramObjective
225
+ : (mandatoryFields.objective || '');
226
+
227
+ const acSource = (paramAc.length > 0 ? paramAc : (mandatoryFields.acceptanceCriteria || []));
228
+ const constraintsSource = (paramConstraints.length > 0 ? paramConstraints : (mandatoryFields.constraints || []));
229
+ const vrSource = (paramVr.length > 0 ? paramVr : (mandatoryFields.verificationRequirements || []));
230
+ const scopeSource = (paramScope.length > 0 ? paramScope : (mandatoryFields.scopeExcluded || []));
231
+ const gapsSource = (paramGaps.length > 0 ? paramGaps : (mandatoryFields.blockingGaps || []));
232
+
233
+ const mandatoryProvenance = {
234
+ objective: hasParamObjective
235
+ ? { supplied: Boolean(objective), source: 'caller', locator: null, digest: null, maturity: null, reason: objective ? null : 'caller-supplied-empty', gaps: [] }
236
+ : (mandatoryProv.objective || { supplied: Boolean(objective), source: objective ? 'caller' : 'none', locator: null, digest: null, maturity: null, reason: objective ? null : 'none-in-artifact', gaps: [] }),
237
+ acceptanceCriteria: mandatoryProv.acceptanceCriteria || { supplied: acSource.length > 0, source: acSource.length > 0 ? 'requirement' : 'none', locator: null, digest: null, maturity: null, reason: acSource.length > 0 ? null : 'artifact-absent', gaps: [] },
238
+ constraints: mandatoryProv.constraints || { supplied: constraintsSource.length > 0, source: constraintsSource.length > 0 ? 'requirement' : 'none', locator: null, digest: null, maturity: null, reason: constraintsSource.length > 0 ? null : 'artifact-absent', gaps: [] },
239
+ verificationRequirements: mandatoryProv.verificationRequirements || { supplied: vrSource.length > 0, source: vrSource.length > 0 ? 'requirement' : 'none', locator: null, digest: null, maturity: null, reason: vrSource.length > 0 ? null : 'artifact-absent', gaps: [] },
240
+ scopeExcluded: mandatoryProv.scopeExcluded || { supplied: scopeSource.length > 0, source: scopeSource.length > 0 ? 'requirement' : 'none', locator: null, digest: null, maturity: null, reason: scopeSource.length > 0 ? null : 'artifact-absent', gaps: [] },
241
+ blockingGaps: mandatoryProv.blockingGaps || { supplied: gapsSource.length > 0, source: gapsSource.length > 0 ? 'requirement' : 'none', locator: null, digest: null, maturity: null, reason: gapsSource.length > 0 ? null : 'artifact-absent', gaps: [] },
242
+ };
243
+
244
+ // Claims
245
+ let rawClaims = [];
246
+ if (claimsManager) {
247
+ rawClaims = claimsManager.getClaims(taskId);
248
+ } else if (params.claims) {
249
+ if (Array.isArray(params.claims)) {
250
+ rawClaims = params.claims;
251
+ } else {
252
+ const cList = (params.claims.conflicts || []).map((c ) => ({ ...c, status: 'conflicted' }));
253
+ const sList = (params.claims.supported || []).map((s ) => ({ ...s, status: s.status || 'supported' }));
254
+ const hList = (params.claims.hypotheses || []).map((h ) => ({ ...h, status: h.status || 'hypothesis' }));
255
+ rawClaims = [...cList, ...sList, ...hList];
256
+ }
257
+ }
258
+ const conflictedClaims = [];
259
+ const supportedClaims = [];
260
+ const activeHypotheses = [];
110
261
 
111
262
  for (const claim of rawClaims) {
112
- if (claim.status === 'supported') {
113
- if (supportedClaims.length < limits.maxClaims) {
114
- supportedClaims.push({
115
- id: claim.id,
116
- statement: claim.statement,
117
- evidenceIds: claim.supportingEvidence,
118
- });
119
- }
120
- } else if (claim.status === 'hypothesis') {
121
- activeHypotheses.push({
122
- id: claim.id,
123
- statement: claim.statement,
124
- });
125
- } else if (claim.status === 'conflicted') {
126
- conflictedClaims.push({
127
- id: claim.id,
128
- statement: claim.statement,
129
- supportingEvidence: claim.supportingEvidence,
130
- contradictingEvidence: claim.contradictingEvidence,
263
+ if (claim.status === 'conflicted' || (Array.isArray(claim.contradictingEvidence) && claim.contradictingEvidence.length > 0)) {
264
+ conflictedClaims.push(claim);
265
+ } else if (claim.status === 'supported') {
266
+ supportedClaims.push({
267
+ ...claim,
268
+ evidenceIds: Array.isArray(claim.evidenceIds)
269
+ ? claim.evidenceIds
270
+ : (Array.isArray(claim.supportingEvidence) ? claim.supportingEvidence : []),
131
271
  });
272
+ } else if (claim.status === 'hypothesis') {
273
+ activeHypotheses.push(claim);
132
274
  }
133
275
  }
134
276
 
135
- // Extract relevant files and structural context from fresh evidence
136
- const relevantFileSet = new Set();
137
- const structuralNodes = [];
138
- const freshEvidenceItems = [];
277
+ // Evidence
278
+ const rawEvidence = evidenceLedger ? evidenceLedger.queryEvidence({ taskId }) : (params.evidence || []);
279
+ const relevantFileSet = new Set ();
280
+ const structuralNodes = [];
281
+ const freshEvidenceItems = [];
282
+ const nonFreshOmissions = [];
139
283
 
140
284
  for (const ev of rawEvidence) {
141
- if (ev.freshness?.status === 'FRESH') {
142
- if (ev.locator?.file) {
143
- relevantFileSet.add(ev.locator.file);
144
- }
145
- if (ev.kind === 'structural' && ev.content) {
146
- structuralNodes.push(ev.content);
147
- }
148
- if (freshEvidenceItems.length < limits.maxEvidenceItems) {
149
- freshEvidenceItems.push({
150
- id: ev.id,
151
- kind: ev.kind,
152
- locator: ev.locator,
153
- summary: typeof ev.content === 'string' ? ev.content.slice(0, 120) : null,
154
- });
155
- }
285
+ if (ev.status === 'superseded') {
286
+ nonFreshOmissions.push({
287
+ id: ev.id,
288
+ field: 'evidenceSummary',
289
+ reason: 'superseded',
290
+ });
291
+ } else if (ev.freshness?.status === 'FRESH') {
292
+ if (ev.locator?.file) relevantFileSet.add(ev.locator.file);
293
+ if (ev.kind === 'structural' && ev.content) structuralNodes.push(ev.content);
294
+ freshEvidenceItems.push({
295
+ id: ev.id,
296
+ kind: ev.kind,
297
+ locator: ev.locator,
298
+ summary: typeof ev.content === 'string' ? ev.content.slice(0, 120) : null,
299
+ freshness: 'FRESH',
300
+ });
301
+ } else {
302
+ const status = ev.freshness?.status === 'STALE' ? 'STALE' : 'UNKNOWN';
303
+ nonFreshOmissions.push({
304
+ id: ev.id,
305
+ field: 'evidenceSummary',
306
+ reason: 'not-fresh',
307
+ freshness: status,
308
+ });
156
309
  }
157
310
  }
311
+ const relevantFiles = params.relevantFiles
312
+ ? Array.from(new Set([...params.relevantFiles, ...relevantFileSet]))
313
+ : Array.from(relevantFileSet);
158
314
 
159
- const relevantFiles = Array.from(relevantFileSet).slice(0, limits.maxFiles);
315
+ // Build items
316
+ const items = [];
317
+ if (objective) items.push(buildObjectiveItem(objective));
160
318
 
161
- return {
162
- version: 1,
163
- taskId: taskId || 'default',
164
- taskClass,
165
- compiledAt: new Date().toISOString(),
166
- objective,
167
- constraints: [...constraints],
168
- // Ported from `evidence/context_pack.py` (plan task B.3): the only field the Python pack
169
- // carried that this one did not. What the task must satisfy belongs beside the evidence
170
- // gathered to satisfy it, otherwise the verification engine has to re-read the requirement
171
- // to find out what it is verifying against.
172
- verificationRequirements: [...acceptanceCriteria],
173
- claims: {
174
- supported: supportedClaims,
175
- hypotheses: activeHypotheses,
176
- conflicts: conflictedClaims,
177
- },
178
- relevantFiles,
179
- structuralContext: structuralNodes.slice(0, 5),
180
- evidenceCount: freshEvidenceItems.length,
181
- evidenceSummary: freshEvidenceItems,
182
- researchRequests: [...researchRequests]
183
- .sort((a, b) => Number(b.blocking && b.status !== 'ANSWERED')
184
- - Number(a.blocking && a.status !== 'ANSWERED'))
185
- .slice(0, limits.maxResearchRequests).map(r => ({
186
- id: r.id, stageId: r.stageId, question: r.question, status: r.status,
187
- blocking: r.blocking, claimId: r.claimId, evidenceIds: r.evidenceIds, gap: r.gap,
188
- })),
189
- decisions: {
190
- available: decisions.available, live: [...decisions.live], liveCount: decisions.liveCount,
191
- ...(decisions.reason ? { reason: decisions.reason } : {}),
192
- },
193
- budgetEnforcement: {
194
- totalFiles: relevantFiles.length,
195
- totalSupportedClaims: supportedClaims.length,
196
- totalActiveHypotheses: activeHypotheses.length,
197
- limits,
198
- },
199
- };
200
- }
319
+ const blockingUnanswered = researchRequests.filter((r ) => r.blocking && r.status !== 'ANSWERED');
320
+ const otherRequests = researchRequests.filter((r ) => !(r.blocking && r.status !== 'ANSWERED'));
201
321
 
202
- /**
203
- * Formats a ContextPack into a concise markdown context block.
204
- * @param {Object} pack
205
- * @returns {string}
206
- */
207
- formatMarkdown(pack ) {
208
- let md = `## ContextPack: [${pack.taskClass.toUpperCase()}] ${pack.taskId}\n`;
209
- if (pack.objective) {
210
- md += `**Objective:** ${pack.objective}\n\n`;
211
- }
322
+ if (!bare) {
323
+ acSource.forEach((ac , i ) => items.push(buildAcceptanceCriteriaItem(ac, i)));
324
+ constraintsSource.forEach((c , i ) => items.push(buildConstraintItem(c, i)));
325
+ vrSource.forEach((vr , i ) => items.push(buildVerificationRequirementItem(vr, i)));
326
+ scopeSource.forEach((se , i ) => items.push(buildScopeExcludedItem(se, i)));
327
+ gapsSource.forEach((bg , i ) => items.push(buildBlockingGapItem(bg, i)));
328
+ conflictedClaims.forEach((c ) => items.push(buildClaimConflictItem(c)));
212
329
 
213
- if (pack.claims.supported.length > 0) {
214
- md += `### Supported Facts & Invariants\n`;
215
- for (const c of pack.claims.supported) {
216
- md += `- ✓ ${c.statement} *(ev: ${c.evidenceIds.join(', ')})*\n`;
217
- }
218
- md += '\n';
219
- }
330
+ blockingUnanswered.forEach((r ) => items.push(buildResearchRequestItem(r)));
331
+ if (decisions.reason) items.push(buildDecisionReasonItem(decisions.reason));
220
332
 
221
- if (pack.claims.hypotheses.length > 0) {
222
- md += `### Active Hypotheses (Unverified)\n`;
223
- for (const h of pack.claims.hypotheses) {
224
- md += `- ? ${h.statement}\n`;
333
+ if (decisions.available && Array.isArray(decisions.live)) {
334
+ decisions.live.forEach((d ) => items.push(buildDecisionLiveItem(d)));
225
335
  }
226
- md += '\n';
336
+ supportedClaims.forEach((c ) => items.push(buildClaimSupportedItem(c)));
337
+ otherRequests.forEach((r ) => items.push(buildResearchRequestItem(r)));
338
+ activeHypotheses.forEach((h ) => items.push(buildClaimHypothesisItem(h)));
339
+ suppliedPointers.forEach((p ) => items.push(buildPointerItem(p)));
340
+ relevantFiles.forEach((f ) => items.push(buildRelevantFileItem(f)));
341
+ freshEvidenceItems.forEach((ev ) => items.push(buildEvidenceSummaryItem(ev)));
342
+ structuralNodes.forEach((sn , i ) => items.push(buildStructuralContextItem(sn, i)));
227
343
  }
228
344
 
229
- if (pack.researchRequests.length > 0) {
230
- md += `### Feature Research Requests\n`;
231
- for (const r of pack.researchRequests) {
232
- md += `- ${r.status} ${r.blocking ? '(blocking)' : '(nonblocking)'} ${r.stageId}: ${r.question}`;
233
- if (r.gap) md += ` — ${r.gap}`;
234
- if (r.claimId) md += ` (claim: ${r.claimId})`;
235
- md += '\n';
345
+ const hasMandatoryItems = {
346
+ objective: Boolean(objective),
347
+ acceptanceCriteria: !bare && acSource.length > 0,
348
+ constraints: !bare && constraintsSource.length > 0,
349
+ verificationRequirements: !bare && vrSource.length > 0,
350
+ scopeExcluded: !bare && scopeSource.length > 0,
351
+ blockingGaps: !bare && (gapsSource.length > 0 || blockingUnanswered.length > 0),
352
+ 'claims.conflicts': !bare && conflictedClaims.length > 0,
353
+ researchRequests: !bare && blockingUnanswered.length > 0,
354
+ };
355
+
356
+ const empty = rawEvidence.length === 0
357
+ && rawClaims.length === 0
358
+ && researchRequests.length === 0
359
+ && (!decisions || decisions.liveCount === 0);
360
+
361
+ const enableCorrective = budgetOverrides.enableCorrective !== undefined
362
+ ? budgetOverrides.enableCorrective
363
+ : (this.options.enableCorrective !== false);
364
+ const registeredHarnessIds = getRegisteredHarnessIds();
365
+ const reservedHarnessWidth = Math.max(...registeredHarnessIds.map((id ) => id.length), 0);
366
+ const selfRefWidth = Math.max(budgetOverrides.selfRefWidth ?? this.options.selfRefWidth ?? 0, reservedHarnessWidth);
367
+ const longestHarnessId = registeredHarnessIds.reduce((longest , id ) => id.length > longest.length ? id : longest, 'unstated');
368
+ const envelopeBudget = {
369
+ ...resolvedBudget,
370
+ harness: { ...resolvedBudget.harness, id: longestHarnessId },
371
+ };
372
+
373
+ const { envelopeMd, envelopeJson } = computeEnvelopes({
374
+ taskId,
375
+ taskClass: effectiveTaskClass,
376
+ budget: envelopeBudget,
377
+ mandatory: paramMandatory,
378
+ empty,
379
+ mode: bare ? 'bare' : (paramMode || 'full'),
380
+ hasMandatoryItems,
381
+ limits,
382
+ selfRefWidth,
383
+ recordingExpected: params.recordingExpected === true,
384
+ // Reserve the longest registered identity and delivery form for parity.
385
+ form: 'markdown',
386
+ harnessId: longestHarnessId,
387
+ });
388
+
389
+ const hasMandatoryRequests = !bare && blockingUnanswered.length > 0;
390
+ let deltaInfo = null;
391
+ let unchangedOmissions = [];
392
+ let removedIds = [];
393
+ let fitItems = items;
394
+ if (params.deltaSinceEntry && paramMode === 'delta') {
395
+ const oldItems = params.deltaSinceEntry.items || {};
396
+ const currentIds = new Set(items.map((item ) => item.id));
397
+ const added = [];
398
+ const changed = [];
399
+ const unchanged = [];
400
+ fitItems = [];
401
+ for (const item of items) {
402
+ const oldDigest = oldItems[item.id];
403
+ if (item.cls === 'mandatory') {
404
+ if (!oldDigest) added.push(item.id);
405
+ else if (oldDigest !== item.digest) changed.push(item.id);
406
+ else unchanged.push(item.id);
407
+ fitItems.push(item);
408
+ } else if (!oldDigest) {
409
+ added.push(item.id); fitItems.push({ ...item, deltaForced: true });
410
+ } else if (oldDigest === item.digest) {
411
+ unchanged.push(item.id);
412
+ unchangedOmissions.push({ id: item.id, field: item.field, reason: 'unchanged' });
413
+ } else {
414
+ changed.push(item.id); fitItems.push({ ...item, deltaForced: true });
415
+ }
236
416
  }
237
- md += '\n';
417
+ removedIds = Object.keys(oldItems).filter((id ) => !currentIds.has(id));
418
+ deltaInfo = { since, added, changed, removed: removedIds, unchanged };
238
419
  }
239
420
 
240
- if (pack.decisions && pack.decisions.liveCount > 0) {
241
- md += `### Live decisions\n`;
242
- for (const d of pack.decisions.live) md += `- ${d.id} (${d.topic}): ${d.statement}\n`;
243
- md += '\n';
244
- }
421
+ const fit = linearFit({
422
+ items: fitItems,
423
+ budget: resolvedBudget.maxBytes,
424
+ taskId,
425
+ taskClass: effectiveTaskClass,
426
+ envelopeMd,
427
+ envelopeJson,
428
+ initialOmissions: bare ? [] : [...nonFreshOmissions, ...unchangedOmissions],
429
+ sectionHeadingsJson: {
430
+ 'decisions.live': 5,
431
+ 'claims.supported': 5,
432
+ 'researchRequests (others)': hasMandatoryRequests ? 0 : 3,
433
+ 'claims.hypotheses': 4,
434
+ pointers: 3,
435
+ relevantFiles: 3,
436
+ evidenceSummary: 3,
437
+ structuralContext: 3,
438
+ },
439
+ itemCapLimits: {
440
+ relevantFiles: limits.maxFiles ?? 15,
441
+ 'claims.supported': limits.maxClaims ?? 20,
442
+ 'claims.hypotheses': limits.maxClaims ?? 20,
443
+ evidenceSummary: limits.maxEvidenceItems ?? 25,
444
+ 'researchRequests (others)': Math.max(0, (limits.maxResearchRequests ?? 20) - blockingUnanswered.length),
445
+ structuralContext: limits.maxStructuralItems ?? 5,
446
+ },
447
+ });
448
+
449
+ const assemblePack = (keptItems , omissionsList , isOverflow = false, overflowCause = null) => {
450
+ const keptAc = keptItems.filter((i ) => i.field === 'acceptanceCriteria').map((i ) => i.value);
451
+ const keptConstraints = keptItems.filter((i ) => i.field === 'constraints').map((i ) => i.value);
452
+ const keptVr = keptItems.filter((i ) => i.field === 'verificationRequirements').map((i ) => i.value);
453
+ const keptScope = keptItems.filter((i ) => i.field === 'scopeExcluded').map((i ) => i.value);
454
+ const keptGaps = keptItems.filter((i ) => i.field === 'blockingGaps').map((i ) => i.value);
455
+ const keptConflicts = keptItems.filter((i ) => i.field === 'claims.conflicts').map((i ) => i.value);
456
+ const keptLiveDecisions = keptItems.filter((i ) => i.field === 'decisions.live').map((i ) => i.value);
457
+ const keptSupported = keptItems.filter((i ) => i.field === 'claims.supported').map((i ) => i.value);
458
+ const keptRequests = keptItems.filter((i ) => i.field.startsWith('researchRequests')).map((i ) => i.value);
459
+ const keptHypotheses = keptItems.filter((i ) => i.field === 'claims.hypotheses').map((i ) => i.value);
460
+ const keptPointers = keptItems.filter((i ) => i.field === 'pointers').map((i ) => i.value);
461
+ const keptFiles = keptItems.filter((i ) => i.field === 'relevantFiles').map((i ) => i.value);
462
+ const keptEvidence = keptItems.filter((i ) => i.field === 'evidenceSummary').map((i ) => i.value);
463
+ const keptStructural = keptItems.filter((i ) => i.field === 'structuralContext').map((i ) => i.value);
464
+
465
+ const omissions = groupOmissions(omissionsList);
466
+ const packDigest = computePackDigest(keptItems);
467
+
468
+ const cause = overflowCause || (isOverflow ? (fit.cause || 'mandatory') : null);
469
+ const packTotalBytes = fit.totalBytes;
470
+ const packMandatoryBytes = fit.mandatoryBytesByForm?.[params.form === 'markdown' ? 'markdown' : 'json'] ?? fit.mandatoryBytes;
471
+ const packOptionalBytes = packTotalBytes - packMandatoryBytes;
472
+
473
+ const packed = {
474
+ version: 1,
475
+ status: isOverflow ? 'OVERFLOW' : (empty ? 'EMPTY' : 'OK'),
476
+ exitCode: isOverflow ? EXIT.OVERFLOW : (empty ? EXIT.EMPTY : EXIT.OK),
477
+ taskId,
478
+ taskClass: effectiveTaskClass,
479
+ compiledAt: new Date().toISOString(),
480
+ mode: bare ? 'bare' : (paramMode || 'full'),
481
+ budget: resolvedBudget,
482
+ overflow: isOverflow ? {
483
+ cause,
484
+ mandatoryBytes: packMandatoryBytes,
485
+ budgetBytes: resolvedBudget.maxBytes,
486
+ excessBytes: Math.max(0, packTotalBytes - resolvedBudget.maxBytes),
487
+ action: 'split-or-new-session',
488
+ } : null,
489
+ overflowAction: isOverflow ? {
490
+ message: formatOverflowMessage({
491
+ cause,
492
+ mandatoryBytes: packMandatoryBytes,
493
+ totalBytes: packTotalBytes,
494
+ budgetBytes: resolvedBudget.maxBytes,
495
+ source: resolvedBudget.source,
496
+ }),
497
+ } : null,
498
+ toolLimitNotice: null,
499
+ objective,
500
+ acceptanceCriteria: keptAc,
501
+ constraints: keptConstraints,
502
+ verificationRequirements: keptVr,
503
+ scopeExcluded: keptScope,
504
+ blockingGaps: keptGaps,
505
+ claims: {
506
+ conflicts: keptConflicts,
507
+ supported: keptSupported,
508
+ hypotheses: keptHypotheses,
509
+ },
510
+ researchRequests: keptRequests,
511
+ mandatory: mandatoryProvenance,
512
+ decisions: {
513
+ available: Boolean(decisions && decisions.available),
514
+ live: keptLiveDecisions,
515
+ liveCount: keptLiveDecisions.length,
516
+ ...(decisions?.reason ? { reason: decisions.reason } : {}),
517
+ },
518
+ pointers: keptPointers,
519
+ ...(expansion ? { expansion } : {}),
520
+ relevantFiles: keptFiles,
521
+ evidenceCount: keptEvidence.length,
522
+ evidenceSummary: keptEvidence,
523
+ structuralContext: keptStructural,
524
+ budgetEnforcement: {
525
+ totalFiles: relevantFiles.length,
526
+ totalSupportedClaims: supportedClaims.length,
527
+ totalActiveHypotheses: activeHypotheses.length,
528
+ limits: contentLimits,
529
+ },
530
+ empty,
531
+ omissions: bare ? { total: 0, groups: [] } : omissions,
532
+ size: {
533
+ totalBytes: packTotalBytes,
534
+ mandatoryBytes: packMandatoryBytes,
535
+ optionalBytes: packOptionalBytes,
536
+ fittedOn: fit.fittedOn,
537
+ },
538
+ accounting: {
539
+ tokens: makeTokenRecord(packTotalBytes),
540
+ },
541
+ delivery: {
542
+ key: params.recordingExpected ? `${packDigest}.${params.form || 'json'}.${params.harness?.id || 'unstated'}` : null,
543
+ packDigest,
544
+ recorded: params.recordingExpected === true,
545
+ reason: null,
546
+ since: since || null,
547
+ deltaRefused: params.deltaRefused || null,
548
+ },
549
+ delta: deltaInfo,
550
+ packEnd: {
551
+ mode: bare ? 'bare' : (paramMode || 'full'),
552
+ objective: objective ? 1 : 0,
553
+ acceptanceCriteria: keptAc.length,
554
+ constraints: keptConstraints.length,
555
+ verificationRequirements: keptVr.length,
556
+ scopeExcluded: keptScope.length,
557
+ conflicts: keptConflicts.length,
558
+ blockingGaps: keptGaps.length,
559
+ omitted: bare ? 0 : omissions.total,
560
+ bytes: packTotalBytes,
561
+ digest: packDigest.slice(0, 19),
562
+ },
563
+ };
564
+ Object.defineProperties(packed, {
565
+ deliveryItems: { value: keptItems.map((item ) => ({ ...item, cls: item.cls || item.class })) },
566
+ deliveryOmissions: { value: bare ? [] : omissionsList.map((item ) => ({ ...item })) },
567
+ deliveryAllItems: { value: items.map((item ) => ({ id: item.id, digest: item.digest, cls: item.cls || item.class })) },
568
+ trimOptional: { value: () => {
569
+ const currentItems = (packed ).deliveryItems ;
570
+ let trimIndex = -1;
571
+ for (let i = currentItems.length - 1; i >= 0; i -= 1) {
572
+ if (currentItems[i].cls === 'optional' && !currentItems[i].deltaForced) { trimIndex = i; break; }
573
+ }
574
+ if (trimIndex < 0) return null;
575
+ const dropped = currentItems[trimIndex];
576
+ const nextItems = currentItems.filter((_ , i ) => i !== trimIndex);
577
+ const nextOmissions = [...(packed ).deliveryOmissions, {
578
+ id: dropped.id, field: dropped.field, reason: 'budget', ...(dropped.freshness ? { freshness: dropped.freshness } : {}),
579
+ }];
580
+ return assemblePack(nextItems, nextOmissions, false, null);
581
+ } },
582
+ });
583
+ return packed;
584
+ };
585
+
586
+ let pack = assemblePack(fit.kept, fit.omissions, fit.overflow, fit.cause);
587
+
588
+ if (enableCorrective && pack.status === 'OK') {
589
+ let currentKept = [...fit.kept];
590
+ let currentOmissions = [...fit.omissions];
591
+ let fallbackCounted = false;
592
+
593
+ while (pack.status === 'OK') {
594
+ const mdOut = formatMarkdown(pack, { throwOnOverBudget: false });
595
+ const jsonOut = formatJson(pack, { throwOnOverBudget: false });
596
+ const mdLen = Buffer.byteLength(mdOut, 'utf8');
597
+ const jsonLen = Buffer.byteLength(jsonOut, 'utf8');
598
+
599
+ if (mdLen <= resolvedBudget.maxBytes && jsonLen <= resolvedBudget.maxBytes) {
600
+ pack.size.fittedOn = jsonLen >= mdLen ? 'json' : 'markdown';
601
+ break;
602
+ }
603
+
604
+ let lastOptIdx = -1;
605
+ for (let idx = currentKept.length - 1; idx >= 0; idx--) {
606
+ const it = currentKept[idx];
607
+ if ((it.cls === 'optional' || it.class === 'optional') && !it.deltaForced) {
608
+ lastOptIdx = idx;
609
+ break;
610
+ }
611
+ }
612
+
613
+ if (lastOptIdx === -1) {
614
+ const mandatoryOnlyKept = currentKept.filter((i ) => i.cls === 'mandatory' || i.class === 'mandatory' || i.deltaForced);
615
+ const allOptOmitted = [
616
+ ...(bare ? [] : nonFreshOmissions),
617
+ ...items.filter((i ) => (i.cls === 'optional' || i.class === 'optional') && !i.deltaForced).map((i ) => ({
618
+ id: i.id,
619
+ field: i.field,
620
+ reason: 'mandatory-overflow',
621
+ ...(i.freshness ? { freshness: i.freshness } : {}),
622
+ })),
623
+ ];
624
+ const cause = Math.max(fit.mandatoryBytes, mdLen, jsonLen) > resolvedBudget.maxBytes ? 'mandatory' : 'accounting';
625
+ pack = assemblePack(mandatoryOnlyKept, allOptOmitted, true, cause);
626
+ break;
627
+ }
245
628
 
246
- if (pack.relevantFiles.length > 0) {
247
- md += `### Relevant File Locators\n`;
248
- for (const f of pack.relevantFiles) {
249
- md += `- \`${f}\`\n`;
629
+ if (!fallbackCounted) {
630
+ correctiveTrimFallbackCount += 1;
631
+ fallbackCounted = true;
632
+ }
633
+ const [dropped] = currentKept.splice(lastOptIdx, 1);
634
+ currentOmissions.push({
635
+ id: dropped.id,
636
+ field: dropped.field,
637
+ reason: 'budget',
638
+ ...(dropped.freshness ? { freshness: dropped.freshness } : {}),
639
+ });
640
+ pack = assemblePack(currentKept, currentOmissions, false, null);
250
641
  }
251
- md += '\n';
252
642
  }
253
643
 
254
- return md;
644
+ const fittedOutput = params.form === 'markdown'
645
+ ? formatMarkdown(pack, { throwOnOverBudget: false })
646
+ : formatJson(pack, { throwOnOverBudget: false });
647
+ pack.size.totalBytes = Buffer.byteLength(fittedOutput, 'utf8');
648
+ pack.size.optionalBytes = pack.size.totalBytes - pack.size.mandatoryBytes;
649
+ pack.packEnd.bytes = pack.size.totalBytes;
650
+ pack.accounting.tokens = makeTokenRecord(pack.size.totalBytes);
651
+
652
+ return pack;
653
+ }
654
+
655
+ /**
656
+ * Formats a ContextPack into markdown per IC-005.
657
+ * @param {Object} pack
658
+ * @param {Object} [options]
659
+ * @returns {string}
660
+ */
661
+ formatMarkdown(pack , options = {}) {
662
+ return formatMarkdown(pack, options);
663
+ }
664
+
665
+ /**
666
+ * Formats a ContextPack into JSON per IC-004.
667
+ * @param {Object} pack
668
+ * @param {Object} [options]
669
+ * @returns {string}
670
+ */
671
+ formatJson(pack , options = {}) {
672
+ return formatJson(pack, options);
255
673
  }
256
674
 
257
675
  /**
@@ -281,9 +699,8 @@ class ContextPackCompiler {
281
699
  optional = {},
282
700
  } = options;
283
701
 
284
- const budgetBytes = Number.isInteger(maxBytes) && maxBytes > 0
285
- ? maxBytes
286
- : (TASK_CLASS_BUDGETS[taskClass] || 225000);
702
+ const resolvedBudget = resolveBudget({ taskClass, maxBytes });
703
+ const budgetBytes = resolvedBudget.maxBytes;
287
704
 
288
705
  const acceptanceCriteria = Array.isArray(mandatory.acceptanceCriteria) ? [...mandatory.acceptanceCriteria] : [];
289
706
  const constraints = Array.isArray(mandatory.constraints) ? [...mandatory.constraints] : [];
@@ -314,17 +731,75 @@ class ContextPackCompiler {
314
731
 
315
732
  const mandatoryBytes = Buffer.byteLength(mandatoryMd, 'utf8');
316
733
 
317
- // Rule 1: Mandatory content is never truncated; if mandatoryBytes > budgetBytes, returns overflow: true.
318
- if (mandatoryBytes > budgetBytes) {
319
- const omissions = [];
320
- const narrativeList = Array.isArray(optional.narrative) ? optional.narrative : [];
321
- const claimsList = Array.isArray(optional.unverifiedClaims) ? optional.unverifiedClaims : [];
322
- const hypList = Array.isArray(optional.hypotheses) ? optional.hypotheses : [];
734
+ const hypotheses = Array.isArray(optional.hypotheses) ? [...optional.hypotheses] : [];
735
+ const unverifiedClaims = Array.isArray(optional.unverifiedClaims) ? [...optional.unverifiedClaims] : [];
736
+ const narrative = Array.isArray(optional.narrative) ? [...optional.narrative] : [];
737
+
738
+ const items = [];
739
+ hypotheses.forEach((h , idx ) => {
740
+ const line = `- ? ${h}\n`;
741
+ items.push({
742
+ id: `hypothesis-${idx + 1}`,
743
+ field: 'hypotheses',
744
+ value: h,
745
+ class: 'optional',
746
+ cls: 'optional',
747
+ priority: 1,
748
+ md: line,
749
+ mdBytes: Buffer.byteLength(line, 'utf8'),
750
+ jsonBytes: 0,
751
+ });
752
+ });
753
+
754
+ unverifiedClaims.forEach((c , idx ) => {
755
+ const line = `- ? ${c}\n`;
756
+ items.push({
757
+ id: `claim-${idx + 1}`,
758
+ field: 'unverifiedClaims',
759
+ value: c,
760
+ class: 'optional',
761
+ cls: 'optional',
762
+ priority: 2,
763
+ md: line,
764
+ mdBytes: Buffer.byteLength(line, 'utf8'),
765
+ jsonBytes: 0,
766
+ });
767
+ });
323
768
 
324
- for (let i = 0; i < narrativeList.length; i++) omissions.push({ id: `narrative-${i + 1}`, reason: 'budget-cap-exceeded' });
325
- for (let i = 0; i < claimsList.length; i++) omissions.push({ id: `claim-${i + 1}`, reason: 'budget-cap-exceeded' });
326
- for (let i = 0; i < hypList.length; i++) omissions.push({ id: `hypothesis-${i + 1}`, reason: 'budget-cap-exceeded' });
769
+ narrative.forEach((n , idx ) => {
770
+ const line = `- ${n}\n`;
771
+ items.push({
772
+ id: `narrative-${idx + 1}`,
773
+ field: 'narrative',
774
+ value: n,
775
+ class: 'optional',
776
+ cls: 'optional',
777
+ priority: 3,
778
+ md: line,
779
+ mdBytes: Buffer.byteLength(line, 'utf8'),
780
+ jsonBytes: 0,
781
+ });
782
+ });
783
+
784
+ const fit = compose.linearFit({
785
+ items,
786
+ budget: budgetBytes,
787
+ taskId,
788
+ taskClass,
789
+ envelopeMd: mandatoryBytes,
790
+ envelopeJson: mandatoryBytes,
791
+ includeOmissionCosts: false,
792
+ sectionHeadingsMd: {
793
+ hypotheses: Buffer.byteLength('### Active Hypotheses (Unverified)\n\n', 'utf8'),
794
+ unverifiedClaims: Buffer.byteLength('### Unverified Claims\n\n', 'utf8'),
795
+ narrative: Buffer.byteLength('### Background Narrative\n\n', 'utf8'),
796
+ },
797
+ sectionHeadingsJson: {},
798
+ itemCapLimits: {},
799
+ });
327
800
 
801
+ if (fit.overflow) {
802
+ const omissions = items.map((i ) => ({ id: i.id, reason: 'budget-cap-exceeded' }));
328
803
  return {
329
804
  taskId,
330
805
  taskClass,
@@ -339,74 +814,30 @@ class ContextPackCompiler {
339
814
  };
340
815
  }
341
816
 
342
- // Rule 2: Optional content is trimmed in reverse priority order when mandatoryBytes + optionalBytes > budgetBytes.
343
- // Priority order: 1. hypotheses, 2. unverifiedClaims, 3. narrative.
344
- // Trimming in reverse priority: narrative first, then claims, then hypotheses.
345
- const hypotheses = Array.isArray(optional.hypotheses) ? [...optional.hypotheses] : [];
346
- const unverifiedClaims = Array.isArray(optional.unverifiedClaims) ? [...optional.unverifiedClaims] : [];
347
- const narrative = Array.isArray(optional.narrative) ? [...optional.narrative] : [];
348
-
349
- const keptHypotheses = [];
350
- const keptClaims = [];
351
- const keptNarrative = [];
352
- const omissions = [];
353
-
354
- const buildPayload = (hList , cList , nList ) => {
355
- let md = mandatoryMd;
356
- if (hList.length > 0) {
357
- md += `### Active Hypotheses (Unverified)\n`;
358
- for (const h of hList) md += `- ? ${h}\n`;
359
- md += '\n';
360
- }
361
- if (cList.length > 0) {
362
- md += `### Unverified Claims\n`;
363
- for (const c of cList) md += `- ? ${c}\n`;
364
- md += '\n';
365
- }
366
- if (nList.length > 0) {
367
- md += `### Background Narrative\n`;
368
- for (const n of nList) md += `- ${n}\n`;
369
- md += '\n';
370
- }
371
- return md;
372
- };
817
+ const keptHypotheses = fit.kept.filter((i ) => i.field === 'hypotheses').map((i ) => i.value);
818
+ const keptClaims = fit.kept.filter((i ) => i.field === 'unverifiedClaims').map((i ) => i.value);
819
+ const keptNarrative = fit.kept.filter((i ) => i.field === 'narrative').map((i ) => i.value);
373
820
 
374
- // Forward trial fitting: hypotheses first
375
- for (let i = 0; i < hypotheses.length; i++) {
376
- const trial = [...keptHypotheses, hypotheses[i]];
377
- const trialMd = buildPayload(trial, [], []);
378
- if (Buffer.byteLength(trialMd, 'utf8') <= budgetBytes) {
379
- keptHypotheses.push(hypotheses[i]);
380
- } else {
381
- omissions.push({ id: `hypothesis-${i + 1}`, reason: 'budget-cap-exceeded' });
382
- }
821
+ let payload = mandatoryMd;
822
+ if (keptHypotheses.length > 0) {
823
+ payload += `### Active Hypotheses (Unverified)\n`;
824
+ for (const h of keptHypotheses) payload += `- ? ${h}\n`;
825
+ payload += '\n';
383
826
  }
384
-
385
- // unverifiedClaims next
386
- for (let i = 0; i < unverifiedClaims.length; i++) {
387
- const trial = [...keptClaims, unverifiedClaims[i]];
388
- const trialMd = buildPayload(keptHypotheses, trial, []);
389
- if (Buffer.byteLength(trialMd, 'utf8') <= budgetBytes) {
390
- keptClaims.push(unverifiedClaims[i]);
391
- } else {
392
- omissions.push({ id: `claim-${i + 1}`, reason: 'budget-cap-exceeded' });
393
- }
827
+ if (keptClaims.length > 0) {
828
+ payload += `### Unverified Claims\n`;
829
+ for (const c of keptClaims) payload += `- ? ${c}\n`;
830
+ payload += '\n';
394
831
  }
395
-
396
- // narrative next
397
- for (let i = 0; i < narrative.length; i++) {
398
- const trial = [...keptNarrative, narrative[i]];
399
- const trialMd = buildPayload(keptHypotheses, keptClaims, trial);
400
- if (Buffer.byteLength(trialMd, 'utf8') <= budgetBytes) {
401
- keptNarrative.push(narrative[i]);
402
- } else {
403
- omissions.push({ id: `narrative-${i + 1}`, reason: 'budget-cap-exceeded' });
404
- }
832
+ if (keptNarrative.length > 0) {
833
+ payload += `### Background Narrative\n`;
834
+ for (const n of keptNarrative) payload += `- ${n}\n`;
835
+ payload += '\n';
405
836
  }
406
837
 
407
- const payload = buildPayload(keptHypotheses, keptClaims, keptNarrative);
408
838
  const totalBytes = Buffer.byteLength(payload, 'utf8');
409
839
  const optionalBytes = totalBytes - mandatoryBytes;
840
+ const omissions = fit.omissions.map((o ) => ({ id: o.id, reason: 'budget-cap-exceeded' }));
410
841
 
411
842
  return {
412
843
  taskId,
@@ -446,42 +877,395 @@ class ContextPackCompiler {
446
877
  * @param {string} [options.slug] feature slug, when the task id is not the feature's own
447
878
  * @returns {number} exit code
448
879
  */
449
- function handleContextPackCommand({ taskId, taskClass, objective, json = false, stateRoot, slug } = {}) {
880
+ function handleContextPackCommand({
881
+ taskId,
882
+ taskClass = 'feature',
883
+ objective,
884
+ json = false,
885
+ stateRoot,
886
+ slug,
887
+ mandatoryFile,
888
+ harness = null,
889
+ maxBytes,
890
+ bare = false,
891
+ noRecord = false,
892
+ reason = null,
893
+ since = null,
894
+ expand = null,
895
+ profiles = null,
896
+ } = {}) {
450
897
  const root = stateRoot || process.cwd();
898
+ const effectiveTaskClass = (!taskClass || typeof taskClass !== 'string' || taskClass.trim() === '') ? 'feature' : taskClass.trim();
899
+
900
+ // IC-001 Usage errors
901
+ if (!taskId || typeof taskId !== 'string' || taskId.trim() === '') {
902
+ return usageError('context-pack', '--task-id is required', json);
903
+ }
904
+ if (bare && expand) {
905
+ return usageError('context-pack', '--bare cannot be combined with --expand', json);
906
+ }
907
+ if (since && !/^sha256:[0-9a-f]{64}$/.test(since)) {
908
+ return usageError('context-pack', "--since must be 'sha256:' followed by 64 lowercase hex", json);
909
+ }
910
+ if (expand && (!since || !reason)) {
911
+ return usageError('context-pack', '--expand requires both --since and --reason', json);
912
+ }
913
+ if (harness) {
914
+ const validIds = getRegisteredHarnessIds();
915
+ if (!validIds.includes(harness)) {
916
+ return usageError('context-pack', `Unknown harness '${harness}'. Valid harnesses: ${validIds.join(', ')}`, json);
917
+ }
918
+ }
919
+
920
+ let parsedMaxBytes ;
921
+ let baseBudget = null;
922
+ if (maxBytes !== undefined && maxBytes !== null) {
923
+ const parsed = typeof maxBytes === 'number' ? maxBytes : parseInt(String(maxBytes).trim(), 10);
924
+ if (!Number.isInteger(parsed) || parsed <= 0 || (typeof maxBytes === 'string' && !/^[1-9]\d*$/.test(maxBytes.trim()))) {
925
+ return usageError('context-pack', '--max-bytes must be a positive integer', json);
926
+ }
927
+ parsedMaxBytes = parsed;
928
+
929
+ baseBudget = resolveBudget({ taskClass: effectiveTaskClass, harnessFlag: harness, profiles });
930
+ if (parsedMaxBytes > baseBudget.maxBytes && !reason) {
931
+ return usageError('context-pack', `--max-bytes (${parsedMaxBytes}) exceeds resolved budget (${baseBudget.maxBytes}) and requires --reason`, json);
932
+ }
933
+ }
934
+
935
+ const budget = resolveBudget({ taskClass: effectiveTaskClass, maxBytes: parsedMaxBytes, harnessFlag: harness, profiles });
936
+
937
+ const feature = resolveFeature({ projectRoot: root, slug: slug || null });
938
+
939
+ let sinceEntry = null;
940
+ let deltaRefused = null;
941
+ let deltaExpansions = [];
942
+ if (parsedMaxBytes !== undefined) {
943
+ if (!baseBudget) {
944
+ baseBudget = resolveBudget({ taskClass: effectiveTaskClass, harnessFlag: harness, profiles });
945
+ }
946
+ if (parsedMaxBytes > baseBudget.maxBytes) {
947
+ deltaExpansions.push({ at: new Date().toISOString(), kind: 'cap-raised', target: null, reason, bytesAdded: null });
948
+ }
949
+ }
950
+ let retention = profileRetention(budget.harness?.id || null, profiles);
951
+ if (since && !expand) {
952
+ sinceEntry = readDeliveryEntry({ projectRoot: root, taskId, slug, packDigest: since });
953
+ if (!sinceEntry) deltaRefused = 'since-unknown';
954
+ else if (sourcesChanged(root, sinceEntry.sources || {})) deltaRefused = 'sources-changed';
955
+ else if (!budget.harness?.id) deltaRefused = 'harness-unstated';
956
+ else if (retention?.value !== 'supported') deltaRefused = 'retention-unproven';
957
+ if (deltaRefused) {
958
+ deltaExpansions.push({ at: new Date().toISOString(), kind: 'delta-widened', target: null, reason: deltaRefused, bytesAdded: null });
959
+ }
960
+ }
961
+
962
+ if (expand) {
963
+ const prior = readDeliveryEntry({ projectRoot: root, taskId, slug, packDigest: since });
964
+ if (!prior) return usageError('context-pack', `Unknown digest '${since}'`, json);
965
+ const pointer = (prior.pointers || []).find((candidate ) => candidate.id === expand);
966
+ if (!pointer) return usageError('context-pack', `Unknown pointer '${expand}' in digest '${since}'`, json);
967
+ const current = pointerTools.readPointerSlice({ projectRoot: root, pointer });
968
+ if (!current) return usageError('context-pack', `Unknown pointer '${expand}' in digest '${since}'`, json);
969
+ const compiler = new ContextPackCompiler();
970
+ const pack = compiler.compileContextPack({ taskId, taskClass: effectiveTaskClass, objective: '', mandatory: { fields: {}, provenance: {} }, budget,
971
+ mode: 'expand', expansion: { id: expand, path: pointer.path, content: current.slice, targetChanged: current.digest !== pointer.digest } });
972
+ pack.status = 'OK';
973
+ pack.exitCode = EXIT.OK;
974
+ pack.mode = 'expand';
975
+ pack.delivery = { key: prior.key, packDigest: since, recorded: false, reason: noRecord ? 'no-record' : null,
976
+ since, deltaRefused: null, expansionRecorded: false };
977
+ pack.packEnd.mode = 'expand';
978
+ pack.packEnd.digest = since.slice(7, 19);
979
+ const sliceBytes = Buffer.byteLength(current.slice, 'utf8');
980
+ let output = '';
981
+ let emitted = 0;
982
+ for (let render = 0; render < 3; render += 1) {
983
+ output = json ? compiler.formatJson(pack, { throwOnOverBudget: false }) : compiler.formatMarkdown(pack, { throwOnOverBudget: false });
984
+ emitted = Buffer.byteLength(output, 'utf8');
985
+ const tokenValue = Math.ceil(emitted / 4);
986
+ if (pack.accounting.tokens?.estimated?.value === tokenValue) break;
987
+ pack.accounting.tokens = makeTokenRecord(emitted);
988
+ }
989
+ if (emitted > budget.maxBytes) {
990
+ return usageError('context-pack', `slice is ${emitted} B, over the ${budget.maxBytes} B cap (${budget.source}); re-run with --max-bytes ${emitted} --reason "${reason}"`, json);
991
+ }
992
+ let expansionRecorded = false;
993
+ let recordReason = noRecord ? 'no-record' : null;
994
+ if (!noRecord) {
995
+ const expansionRecord = { at: new Date().toISOString(), kind: 'pointer-followed', target: expand, reason,
996
+ bytesAdded: sliceBytes, targetChanged: current.digest !== pointer.digest };
997
+ try {
998
+ appendPointerExpansion({ projectRoot: root, taskId, slug, key: prior.key, expansion: expansionRecord });
999
+ expansionRecorded = true;
1000
+ recordReason = null;
1001
+ } catch (error ) {
1002
+ recordReason = error?.code === 'entry-evicted' ? 'entry-evicted' : classifyStoreFailure(error);
1003
+ }
1004
+ }
1005
+ pack.delivery.recorded = expansionRecorded;
1006
+ pack.delivery.reason = recordReason;
1007
+ pack.delivery.expansionRecorded = expansionRecorded;
1008
+ pack.expansionRecorded = expansionRecorded;
1009
+ for (let render = 0; render < 3; render += 1) {
1010
+ output = json ? compiler.formatJson(pack, { throwOnOverBudget: false }) : compiler.formatMarkdown(pack, { throwOnOverBudget: false });
1011
+ emitted = Buffer.byteLength(output, 'utf8');
1012
+ const tokenValue = Math.ceil(emitted / 4);
1013
+ if (pack.accounting.tokens?.estimated?.value === tokenValue) break;
1014
+ pack.accounting.tokens = makeTokenRecord(emitted);
1015
+ }
1016
+ if (console.log !== nativeConsoleLog) console.log(output);
1017
+ else process.stdout.write(output);
1018
+ return finishRuntime(EXIT.OK);
1019
+ }
1020
+
1021
+ let reqText = null;
1022
+ let reqPath = 'requirement.md';
1023
+ if (!feature.error && feature.featureDir) {
1024
+ reqPath = feature.paths?.requirement
1025
+ ? path.resolve(feature.repoRoot || root, feature.paths.requirement)
1026
+ : path.join(feature.featureDir, 'intention', 'requirement.md');
1027
+ try {
1028
+ if (fs.existsSync(reqPath)) {
1029
+ reqText = fs.readFileSync(reqPath, 'utf8');
1030
+ }
1031
+ } catch {
1032
+ reqText = null;
1033
+ }
1034
+ }
1035
+
1036
+ let taskStories = [];
1037
+ if (!feature.error && feature.featureDir && taskId) {
1038
+ let planPath = null;
1039
+ if (feature.paths?.plan) {
1040
+ planPath = path.resolve(feature.repoRoot || root, feature.paths.plan);
1041
+ } else {
1042
+ const nestedPlan = path.join(feature.featureDir, 'plan', 'plan.md');
1043
+ const flatPlan = path.join(feature.featureDir, 'plan.md');
1044
+ if (fs.existsSync(nestedPlan)) planPath = nestedPlan;
1045
+ else if (fs.existsSync(flatPlan)) planPath = flatPlan;
1046
+ }
1047
+ if (planPath && fs.existsSync(planPath)) {
1048
+ try {
1049
+ const planText = fs.readFileSync(planPath, 'utf8');
1050
+ const { parsePlan } = require('./scaffold/artifacts.js');
1051
+ const parsedPlan = parsePlan(planText);
1052
+ const matchedTask = (parsedPlan.tasks || []).find(
1053
+ (t ) => t.id === taskId || t.id.toLowerCase() === taskId.toLowerCase()
1054
+ );
1055
+ if (matchedTask && Array.isArray(matchedTask.stories)) {
1056
+ taskStories = matchedTask.stories;
1057
+ }
1058
+ } catch {
1059
+ taskStories = [];
1060
+ }
1061
+ }
1062
+ }
1063
+
1064
+ let mandatory = extractMandatory({
1065
+ text: reqText,
1066
+ path: reqPath,
1067
+ featureError: feature.error ? (feature.message || feature.error) : null,
1068
+ taskStories,
1069
+ });
1070
+
1071
+ if (mandatoryFile) {
1072
+ try {
1073
+ let fileContent ;
1074
+ if (mandatoryFile === '-') {
1075
+ fileContent = fs.readFileSync(0, 'utf8');
1076
+ } else {
1077
+ if (!fs.existsSync(mandatoryFile)) {
1078
+ const err = new Error(`--mandatory-file '${mandatoryFile}' does not exist`);
1079
+ err.code = 'USAGE_ERROR';
1080
+ throw err;
1081
+ }
1082
+ fileContent = fs.readFileSync(mandatoryFile, 'utf8');
1083
+ }
1084
+ const override = parseMandatoryFile(fileContent, mandatoryFile);
1085
+ mandatory = applyMandatoryOverride(mandatory, override);
1086
+ } catch (err ) {
1087
+ return usageError('context-pack', err.message, json, err);
1088
+ }
1089
+ }
1090
+
451
1091
  const ledger = new EvidenceLedger({ repoRoot: root, slug });
452
1092
  ledger.load(taskId);
453
1093
  const claims = new ClaimsManager({ evidenceLedger: ledger, repoRoot: root, slug });
454
1094
  claims.load(taskId);
455
1095
  claims.evaluateAll();
1096
+
456
1097
  const researchRequests = new ResearchRequestStore({ projectRoot: root, slug }).list(taskId);
457
1098
 
458
1099
  const compiler = new ContextPackCompiler();
459
- const pack = compiler.compileContextPack({
1100
+ let pack = compiler.compileContextPack({
460
1101
  taskId,
461
- // The library's own default. Passed through rather than substituted here so there is one
462
- // place that decides what an unstated class means for a *label* — which is all it is in a
463
- // pack, unlike readiness where it selects the contract.
464
- ...(taskClass ? { taskClass } : {}),
465
- objective: objective || '',
1102
+ taskClass: effectiveTaskClass,
1103
+ objective: objective !== undefined && objective !== null ? objective : undefined,
1104
+ mandatory,
1105
+ budget,
1106
+ harness,
1107
+ maxBytes: parsedMaxBytes,
1108
+ bare,
1109
+ noRecord,
1110
+ recordingExpected: !noRecord,
1111
+ form: json ? 'json' : 'markdown',
1112
+ reason,
1113
+ since,
1114
+ mode: since && !deltaRefused && !expand ? 'delta' : undefined,
1115
+ deltaSinceEntry: since && !deltaRefused && !expand ? sinceEntry : null,
1116
+ deltaRefused,
1117
+ expand,
1118
+ pointers: pointerTools.collectPointers({ projectRoot: root, feature }),
466
1119
  evidenceLedger: ledger,
467
1120
  claimsManager: claims,
468
1121
  researchRequests,
469
1122
  decisions: loadLiveDecisions({ projectRoot: root, slug: slug || null }),
470
1123
  });
471
1124
 
472
- const empty = pack.evidenceCount === 0
473
- && pack.claims.supported.length === 0
474
- && pack.claims.hypotheses.length === 0
475
- && pack.claims.conflicts.length === 0
476
- && pack.researchRequests.length === 0
477
- && pack.decisions.liveCount === 0;
478
-
479
- if (json) console.log(JSON.stringify({ ...pack, empty }, null, 2));
480
- else {
481
- console.log(compiler.formatMarkdown(pack));
482
- if (empty) console.log(`_No evidence or claims are recorded for task '${taskId}'; this pack states nothing._\n`);
1125
+ let deliverySources = null;
1126
+ let deliveryRetention = null;
1127
+ const form = json ? 'json' : 'markdown';
1128
+ let deliveryRecorded = !noRecord;
1129
+ let deliveryReason = noRecord ? 'no-record' : null;
1130
+ pack.delivery.recorded = deliveryRecorded;
1131
+ pack.delivery.reason = deliveryReason;
1132
+ pack.delivery.key = noRecord ? null : `${pack.delivery.packDigest}.${form}.${budget.harness?.id || 'unstated'}`;
1133
+ pack.omissions = pack.mode === 'bare' ? pack.omissions : compose.groupOmissions(pack.deliveryOmissions, !noRecord);
1134
+ if (pack.status === 'OVERFLOW') {
1135
+ pack = { ...pack, deliveryOmissions: pack.deliveryOmissions.map((item ) => item.reason === 'budget' ? { ...item, reason: 'mandatory-overflow' } : item) };
1136
+ pack.omissions = pack.mode === 'bare' ? pack.omissions : compose.groupOmissions(pack.deliveryOmissions, !noRecord);
1137
+ }
1138
+ if (!noRecord) {
1139
+ deliverySources = hashSourceFiles(root, pack.relevantFiles || []);
1140
+ deliveryRetention = retention?.value === 'supported' || retention?.value === 'unsupported'
1141
+ ? { status: retention.value, basis: `declared:${retention.source || 'profile'}` }
1142
+ : { status: 'unknown', basis: retention?.unverified || 'contextProfile.retention is unverified' };
1143
+ }
1144
+
1145
+ const renderTrimmed = () => {
1146
+ while (true) {
1147
+ let mdOutput = '';
1148
+ let jsonOutput = '';
1149
+ let mdBytes = 0;
1150
+ let jsonBytes = 0;
1151
+ let stable = false;
1152
+ for (let render = 0; render < 3; render += 1) {
1153
+ mdOutput = compiler.formatMarkdown(pack, { throwOnOverBudget: false });
1154
+ jsonOutput = compiler.formatJson(pack, { throwOnOverBudget: false });
1155
+ mdBytes = Buffer.byteLength(mdOutput, 'utf8');
1156
+ jsonBytes = Buffer.byteLength(jsonOutput, 'utf8');
1157
+ const emitted = json ? jsonBytes : mdBytes;
1158
+ const priorBytes = pack.size.totalBytes;
1159
+ const priorOptionalBytes = pack.size.optionalBytes;
1160
+ const tokens = makeTokenRecord(emitted);
1161
+ const priorValue = pack.accounting.tokens?.estimated?.value;
1162
+ pack.size.totalBytes = emitted;
1163
+ pack.size.optionalBytes = emitted - pack.size.mandatoryBytes;
1164
+ pack.accounting.tokens = tokens;
1165
+ if (priorValue === tokens.estimated.value && priorBytes === emitted && priorOptionalBytes === pack.size.optionalBytes) { stable = true; break; }
1166
+ }
1167
+ if (!stable) {
1168
+ mdOutput = compiler.formatMarkdown(pack, { throwOnOverBudget: false });
1169
+ jsonOutput = compiler.formatJson(pack, { throwOnOverBudget: false });
1170
+ mdBytes = Buffer.byteLength(mdOutput, 'utf8');
1171
+ jsonBytes = Buffer.byteLength(jsonOutput, 'utf8');
1172
+ pack.size.totalBytes = json ? jsonBytes : mdBytes;
1173
+ pack.size.optionalBytes = pack.size.totalBytes - pack.size.mandatoryBytes;
1174
+ pack.accounting.tokens = makeTokenRecord(pack.size.totalBytes);
1175
+ }
1176
+ if (pack.status !== 'OK' || (mdBytes <= budget.maxBytes && jsonBytes <= budget.maxBytes)) {
1177
+ const output = json ? jsonOutput : mdOutput;
1178
+ const bytes = json ? jsonBytes : mdBytes;
1179
+ pack.packEnd.bytes = bytes;
1180
+ pack.size.totalBytes = bytes;
1181
+ pack.size.optionalBytes = bytes - pack.size.mandatoryBytes;
1182
+ pack.accounting.tokens = makeTokenRecord(bytes);
1183
+ return { output, bytes };
1184
+ }
1185
+ const trimmed = (pack ).trimOptional?.();
1186
+ if (trimmed) {
1187
+ pack = trimmed;
1188
+ pack.delivery.recorded = deliveryRecorded;
1189
+ pack.delivery.reason = deliveryReason;
1190
+ pack.delivery.key = deliveryRecorded ? `${pack.delivery.packDigest}.${form}.${budget.harness?.id || 'unstated'}` : null;
1191
+ pack.accounting.tokens = makeTokenRecord(json ? jsonBytes : mdBytes);
1192
+ pack.omissions = pack.mode === 'bare' ? pack.omissions : compose.groupOmissions(pack.deliveryOmissions, !noRecord);
1193
+ continue;
1194
+ }
1195
+ const cause = pack.size.mandatoryBytes > budget.maxBytes ? 'mandatory' : 'accounting';
1196
+ pack.status = 'OVERFLOW';
1197
+ pack.exitCode = EXIT.OVERFLOW;
1198
+ if (cause === 'mandatory') {
1199
+ pack = { ...pack, deliveryOmissions: pack.deliveryOmissions.map((item ) => item.reason === 'budget' ? { ...item, reason: 'mandatory-overflow' } : item) };
1200
+ pack.omissions = pack.mode === 'bare' ? pack.omissions : compose.groupOmissions(pack.deliveryOmissions, !noRecord);
1201
+ }
1202
+ pack.overflow = { cause, mandatoryBytes: pack.size.mandatoryBytes, budgetBytes: budget.maxBytes,
1203
+ excessBytes: Math.max(0, Math.max(mdBytes, jsonBytes) - budget.maxBytes), action: 'split-or-new-session' };
1204
+ pack.overflowAction = { message: cause === 'accounting'
1205
+ ? `The omission record brings the pack to ${Math.max(mdBytes, jsonBytes)} B against a ${budget.maxBytes} B budget.`
1206
+ : `It is ${pack.size.mandatoryBytes} B against a ${budget.maxBytes} B budget (${budget.source}).` };
1207
+ let output = '';
1208
+ let bytes = 0;
1209
+ for (let render = 0; render < 3; render += 1) {
1210
+ output = json ? compiler.formatJson(pack, { throwOnOverBudget: false }) : compiler.formatMarkdown(pack, { throwOnOverBudget: false });
1211
+ bytes = Buffer.byteLength(output, 'utf8');
1212
+ const tokenValue = Math.ceil(bytes / 4);
1213
+ if (pack.accounting.tokens?.estimated?.value === tokenValue) break;
1214
+ pack.accounting.tokens = makeTokenRecord(bytes);
1215
+ }
1216
+ pack.packEnd.bytes = bytes;
1217
+ pack.size.totalBytes = bytes;
1218
+ return { output, bytes };
1219
+ }
1220
+ };
1221
+ let rendered = renderTrimmed();
1222
+ let output = rendered.output;
1223
+ let emittedBytes = rendered.bytes;
1224
+ if (!noRecord) {
1225
+ const entry = createDeliveryEntry({ pack: { ...pack, deliveryOmissions: pack.deliveryOmissions }, form, harness: budget.harness?.id || null,
1226
+ sources: deliverySources, expansions: deltaExpansions, retention: deliveryRetention });
1227
+ entry.bytes = emittedBytes;
1228
+ entry.tokens = makeTokenRecord(emittedBytes);
1229
+ try {
1230
+ const result = recordDelivery({ projectRoot: root, taskId, slug, entry, acquire: boundedAcquire });
1231
+ pack.delivery.key = result?.key || entry.key;
1232
+ deliveryRecorded = true;
1233
+ deliveryReason = null;
1234
+ pack.delivery.recorded = deliveryRecorded;
1235
+ pack.delivery.reason = deliveryReason;
1236
+ } catch (error ) {
1237
+ deliveryRecorded = false;
1238
+ deliveryReason = classifyStoreFailure(error);
1239
+ pack.delivery.key = null;
1240
+ pack.delivery.recorded = deliveryRecorded;
1241
+ pack.delivery.reason = deliveryReason;
1242
+ }
1243
+ pack.omissions = pack.mode === 'bare' ? pack.omissions : compose.groupOmissions(pack.deliveryOmissions, pack.delivery.recorded);
1244
+ rendered = renderTrimmed();
1245
+ output = rendered.output;
1246
+ emittedBytes = rendered.bytes;
1247
+ }
1248
+ if (pack.status === 'OVERFLOW') {
1249
+ pack.overflow.excessBytes = Math.max(0, emittedBytes - budget.maxBytes);
1250
+ pack.overflowAction.message = formatOverflowMessage({
1251
+ cause: pack.overflow.cause,
1252
+ mandatoryBytes: pack.size.mandatoryBytes,
1253
+ totalBytes: emittedBytes,
1254
+ budgetBytes: budget.maxBytes,
1255
+ source: budget.source,
1256
+ });
1257
+ rendered = renderTrimmed();
1258
+ output = rendered.output;
1259
+ emittedBytes = rendered.bytes;
1260
+ pack.packEnd.bytes = emittedBytes;
1261
+ pack.size.totalBytes = emittedBytes;
1262
+ }
1263
+ if (console.log !== nativeConsoleLog) {
1264
+ console.log(output);
1265
+ } else {
1266
+ process.stdout.write(output);
483
1267
  }
484
- return finishRuntime(empty ? 1 : 0);
1268
+ return finishRuntime(pack.exitCode);
485
1269
  }
486
1270
 
487
1271
  module.exports = {
@@ -489,4 +1273,5 @@ module.exports = {
489
1273
  TASK_CLASS_BUDGETS,
490
1274
  handleContextPackCommand,
491
1275
  loadLiveDecisions,
1276
+ getCorrectiveTrimFallbackCount,
492
1277
  };