@khoavu882/doflow 2.0.0-beta.3 → 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 (236) hide show
  1. package/bin/doflow.js +2 -2
  2. package/core/.antigravity-plugin/plugin.json +0 -1
  3. package/core/.claude-plugin/plugin.json +1 -1
  4. package/core/.codex-plugin/plugin.json +1 -1
  5. package/core/.plugin/plugin.json +1 -1
  6. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
  7. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  8. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  9. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  10. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  11. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  12. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  13. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  14. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  15. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  16. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  17. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  18. package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
  19. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  20. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
  21. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
  22. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
  23. package/core/harnesses/gemini/hooks/session-start.sh +5 -3
  24. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
  25. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
  26. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
  27. package/core/harnesses/kiro/hooks/session-start.sh +11 -5
  28. package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
  29. package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
  30. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
  31. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  32. package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
  33. package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
  34. package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
  35. package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
  36. package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
  37. package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
  38. package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
  39. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
  40. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
  41. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
  42. package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
  43. package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
  44. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
  45. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
  46. package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
  47. package/core/registry/assets.json +19 -0
  48. package/core/registry/contracts.json +2 -2
  49. package/core/registry/harnesses.json +196 -10
  50. package/core/registry/lifecycle.json +2626 -7
  51. package/core/registry/storage.json +110 -83
  52. package/core/registry/verification.json +1 -1
  53. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
  54. package/core/shared/scripts/doflow/bin/doflow-run +10 -6
  55. package/core/shared/skills/do-design/SKILL.md +6 -6
  56. package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
  57. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
  58. package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
  59. package/core/shared/skills/do-implement/SKILL.md +3 -2
  60. package/core/shared/skills/do-plan/SKILL.md +7 -8
  61. package/package.json +4 -2
  62. package/src/adapters/antigravity/{index.ts → index.js} +141 -105
  63. package/src/adapters/claude/{index.ts → index.js} +63 -49
  64. package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
  65. package/src/adapters/codex/{agents.ts → agents.js} +24 -24
  66. package/src/adapters/codex/{config.ts → config.js} +49 -49
  67. package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
  68. package/src/adapters/codex/{index.ts → index.js} +148 -105
  69. package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
  70. package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
  71. package/src/adapters/copilot/{index.ts → index.js} +42 -42
  72. package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
  73. package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
  74. package/src/adapters/gemini/{index.ts → index.js} +96 -57
  75. package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
  76. package/src/adapters/{index.ts → index.js} +13 -13
  77. package/src/adapters/kiro/{index.ts → index.js} +49 -35
  78. package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
  79. package/src/adapters/opencode/{index.ts → index.js} +52 -52
  80. package/src/adapters/pi/hooks.js +119 -0
  81. package/src/adapters/pi/{index.ts → index.js} +64 -39
  82. package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
  83. package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
  84. package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
  85. package/src/cli/commands/{install.ts → install.js} +25 -25
  86. package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
  87. package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
  88. package/src/cli/commands/{remove.ts → remove.js} +10 -10
  89. package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
  90. package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
  91. package/src/cli/commands/{status.ts → status.js} +24 -24
  92. package/src/cli/commands/{tools.ts → tools.js} +16 -16
  93. package/src/cli/commands/{update.ts → update.js} +20 -20
  94. package/src/cli/{index.ts → index.js} +43 -31
  95. package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
  96. package/src/cli/{shared.ts → shared.js} +40 -40
  97. package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
  98. package/src/helper/{git.ts → git.js} +2 -2
  99. package/src/helper/{guardrails.ts → guardrails.js} +10 -10
  100. package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
  101. package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
  102. package/src/helper/{prompt.ts → prompt.js} +5 -5
  103. package/src/helper/{repo-root.ts → repo-root.js} +2 -2
  104. package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
  105. package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
  106. package/src/helper/{toml.ts → toml.js} +48 -48
  107. package/src/install/{backup-set.ts → backup-set.js} +16 -16
  108. package/src/install/{backup.ts → backup.js} +65 -65
  109. package/src/install/{context.ts → context.js} +15 -15
  110. package/src/install/{manifest.ts → manifest.js} +27 -27
  111. package/src/install/{mcp.ts → mcp.js} +30 -30
  112. package/src/install/{paths.ts → paths.js} +15 -15
  113. package/src/install/{targets.ts → targets.js} +10 -10
  114. package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
  115. package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
  116. package/src/lifecycle/coverage.js +57 -0
  117. package/src/lifecycle/hook-step.js +61 -0
  118. package/src/lifecycle/{index.ts → index.js} +160 -146
  119. package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
  120. package/src/lifecycle/{policies.ts → policies.js} +11 -11
  121. package/src/lifecycle/{view.ts → view.js} +28 -28
  122. package/src/registry/{index.ts → index.js} +195 -66
  123. package/src/release/build-package.js +91 -0
  124. package/src/release/changelog-section.js +70 -0
  125. package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
  126. package/src/runtime/advice/{cli.ts → cli.js} +31 -31
  127. package/src/runtime/advice/{client.ts → client.js} +44 -44
  128. package/src/runtime/advice/{config.ts → config.js} +16 -16
  129. package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
  130. package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
  131. package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
  132. package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
  133. package/src/runtime/advice/{record.ts → record.js} +19 -19
  134. package/src/runtime/advice/{registry.ts → registry.js} +16 -16
  135. package/src/runtime/advice/{strength.ts → strength.js} +7 -7
  136. package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
  137. package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
  138. package/src/runtime/candidate.js +505 -0
  139. package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
  140. package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
  141. package/src/runtime/{claims.ts → claims.js} +44 -35
  142. package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
  143. package/src/runtime/{cli.ts → cli.js} +59 -292
  144. package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
  145. package/src/runtime/context/budget.js +203 -0
  146. package/src/runtime/context/compose.js +1367 -0
  147. package/src/runtime/context/delivery-store.js +102 -0
  148. package/src/runtime/context/mandatory.js +594 -0
  149. package/src/runtime/context/pointer.js +142 -0
  150. package/src/runtime/context-pack.js +1277 -0
  151. package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
  152. package/src/runtime/evidence-fields.js +39 -0
  153. package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
  154. package/src/runtime/failure/{capture.ts → capture.js} +29 -29
  155. package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
  156. package/src/runtime/failure/{cli.ts → cli.js} +30 -30
  157. package/src/runtime/failure/{home.ts → home.js} +6 -6
  158. package/src/runtime/failure/{store.ts → store.js} +32 -32
  159. package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
  160. package/src/runtime/{freshness.ts → freshness.js} +59 -22
  161. package/src/runtime/{health.ts → health.js} +72 -72
  162. package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
  163. package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
  164. package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
  165. package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
  166. package/src/runtime/inventory/{index.ts → index.js} +36 -36
  167. package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
  168. package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
  169. package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
  170. package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
  171. package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
  172. package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
  173. package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
  174. package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
  175. package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
  176. package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
  177. package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
  178. package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
  179. package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
  180. package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
  181. package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
  182. package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
  183. package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
  184. package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
  185. package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
  186. package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
  187. package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
  188. package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
  189. package/src/runtime/{mask.ts → mask.js} +22 -22
  190. package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
  191. package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
  192. package/src/runtime/{model-router.ts → model-router.js} +20 -20
  193. package/src/runtime/{outcome.ts → outcome.js} +27 -27
  194. package/src/runtime/{reach.ts → reach.js} +21 -21
  195. package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
  196. package/src/runtime/{readiness.ts → readiness.js} +113 -43
  197. package/src/runtime/{recovery.ts → recovery.js} +15 -15
  198. package/src/runtime/{research-request.ts → research-request.js} +34 -34
  199. package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
  200. package/src/runtime/retrieval-record.js +223 -0
  201. package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
  202. package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
  203. package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
  204. package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
  205. package/src/runtime/source-version.js +289 -0
  206. package/src/runtime/storage/{cli.ts → cli.js} +9 -9
  207. package/src/runtime/storage/{engine.ts → engine.js} +37 -37
  208. package/src/runtime/storage/{registry.ts → registry.js} +19 -19
  209. package/src/runtime/storage/{roots.ts → roots.js} +8 -8
  210. package/src/runtime/storage/{rules.ts → rules.js} +26 -26
  211. package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
  212. package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
  213. package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
  214. package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
  215. package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
  216. package/src/runtime/{task-state.ts → task-state.js} +86 -21
  217. package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
  218. package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
  219. package/src/runtime/trace/{render.ts → render.js} +7 -7
  220. package/src/runtime/trace/{views.ts → views.js} +40 -40
  221. package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
  222. package/src/runtime/verification/{engine.ts → engine.js} +143 -143
  223. package/src/runtime/verification/{registry.ts → registry.js} +6 -6
  224. package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
  225. package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
  226. package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
  227. package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
  228. package/src/runtime/{worktree.ts → worktree.js} +31 -31
  229. package/src/state/{index.ts → index.js} +26 -26
  230. package/src/state/{lockfile.ts → lockfile.js} +20 -20
  231. package/src/state/{run-lock.ts → run-lock.js} +44 -44
  232. package/core/.antigravity-plugin/hooks.json +0 -39
  233. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  234. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  235. package/core/harnesses/shared/hooks/hooks.json +0 -47
  236. package/src/runtime/context-pack.ts +0 -492
@@ -0,0 +1,1367 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const crypto = require('node:crypto');
6
+
7
+ /**
8
+ * IC-006 Pack item model and classification table.
9
+ * Exactly 17 rows, exported frozen.
10
+ */
11
+ const FIELD_CLASSES = Object.freeze([
12
+ Object.freeze({ field: 'objective', cls: 'mandatory', class: 'mandatory', optionalPriority: 'n/a', priority: null, itemId: 'objective' }),
13
+ Object.freeze({ field: 'acceptanceCriteria', cls: 'mandatory', class: 'mandatory', optionalPriority: 'n/a', priority: null, itemId: 'ac:<n>' }),
14
+ Object.freeze({ field: 'constraints', cls: 'mandatory', class: 'mandatory', optionalPriority: 'n/a', priority: null, itemId: 'nfr:<NFR-id>' }),
15
+ Object.freeze({ field: 'verificationRequirements', cls: 'mandatory', class: 'mandatory', optionalPriority: 'n/a', priority: null, itemId: 'vr:<n>' }),
16
+ Object.freeze({ field: 'scopeExcluded', cls: 'mandatory', class: 'mandatory', optionalPriority: 'n/a', priority: null, itemId: 'scope:<n>' }),
17
+ Object.freeze({ field: 'claims.conflicts', cls: 'mandatory', class: 'mandatory', optionalPriority: 'n/a', priority: null, itemId: 'claim id' }),
18
+ Object.freeze({ field: 'blockingGaps', cls: 'mandatory', class: 'mandatory', optionalPriority: 'n/a', priority: null, itemId: 'gap:<n>' }),
19
+ Object.freeze({ field: 'researchRequests (blocking, not ANSWERED)', cls: 'mandatory', class: 'mandatory', optionalPriority: 'n/a', priority: null, itemId: 'request id' }),
20
+ Object.freeze({ field: 'decisions.reason (register unreadable)', cls: 'mandatory', class: 'mandatory', optionalPriority: 'n/a', priority: null, itemId: 'decisions:reason' }),
21
+ Object.freeze({ field: 'decisions.live', cls: 'optional', class: 'optional', optionalPriority: 1, priority: 1, itemId: 'decision id' }),
22
+ Object.freeze({ field: 'claims.supported', cls: 'optional', class: 'optional', optionalPriority: 2, priority: 2, itemId: 'claim id' }),
23
+ Object.freeze({ field: 'researchRequests (others)', cls: 'optional', class: 'optional', optionalPriority: 3, priority: 3, itemId: 'request id' }),
24
+ Object.freeze({ field: 'claims.hypotheses', cls: 'optional', class: 'optional', optionalPriority: 4, priority: 4, itemId: 'claim id' }),
25
+ Object.freeze({ field: 'pointers', cls: 'optional', class: 'optional', optionalPriority: 5, priority: 5, itemId: 'pointer id (IC-016)' }),
26
+ Object.freeze({ field: 'relevantFiles', cls: 'optional', class: 'optional', optionalPriority: 6, priority: 6, itemId: 'file:<path>' }),
27
+ Object.freeze({ field: 'evidenceSummary', cls: 'optional', class: 'optional', optionalPriority: 7, priority: 7, itemId: 'evidence id' }),
28
+ Object.freeze({ field: 'structuralContext', cls: 'optional', class: 'optional', optionalPriority: 8, priority: 8, itemId: 'struct:<n>' }),
29
+ ]);
30
+
31
+ const ITEM_CAP_LIMITS = Object.freeze({
32
+ relevantFiles: 15,
33
+ 'claims.supported': 20,
34
+ 'claims.hypotheses': 20,
35
+ evidenceSummary: 25,
36
+ 'researchRequests (others)': 20,
37
+ structuralContext: 5,
38
+ });
39
+
40
+ /**
41
+ * Produces canonical JSON with sorted object keys.
42
+ */
43
+ function canonicalJson(val ) {
44
+ if (val === null || typeof val !== 'object') {
45
+ return JSON.stringify(val);
46
+ }
47
+ if (Array.isArray(val)) {
48
+ return '[' + val.map(canonicalJson).join(',') + ']';
49
+ }
50
+ const keys = Object.keys(val).sort();
51
+ return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(val[k])).join(',') + '}';
52
+ }
53
+
54
+ /**
55
+ * Computes sha256 digest of canonical JSON.
56
+ */
57
+ function itemDigest(value ) {
58
+ const hash = crypto.createHash('sha256').update(canonicalJson(value)).digest('hex');
59
+ return `sha256:${hash}`;
60
+ }
61
+
62
+ /**
63
+ * Computes sha256 of the ordered list of item digests.
64
+ */
65
+ function computePackDigest(items ) {
66
+ const hash = crypto.createHash('sha256');
67
+ for (const item of items) {
68
+ hash.update((item.digest || '') + '\n');
69
+ }
70
+ return `sha256:${hash.digest('hex')}`;
71
+ }
72
+
73
+ function computeItemJsonBytes(field , value ) {
74
+ if (value === undefined || value === null) return 0;
75
+ if (field === 'objective') {
76
+ return Buffer.byteLength(` "objective": ${JSON.stringify(value)},\n`, 'utf8');
77
+ }
78
+ if (field === 'decisions.reason (register unreadable)') {
79
+ const r = typeof value === 'object' && value.reason ? value.reason : value;
80
+ return Buffer.byteLength(` "reason": ${JSON.stringify(r)},\n`, 'utf8');
81
+ }
82
+ const indent = field.startsWith('claims.') || field === 'decisions.live' ? ' ' : ' ';
83
+ const raw = JSON.stringify(value, null, 2);
84
+ const lines = raw.split('\n');
85
+ let bytes = 0;
86
+ for (const line of lines) {
87
+ bytes += Buffer.byteLength(indent + line + '\n', 'utf8');
88
+ }
89
+ bytes += 1; // comma ','
90
+ return bytes;
91
+ }
92
+
93
+ /**
94
+ * Constructs a PackItem, enforcing that `field` exists in FIELD_CLASSES (FR-011).
95
+ */
96
+ function buildPackItem({
97
+ id,
98
+ field,
99
+ value,
100
+ md,
101
+ priority = null,
102
+ mdBytes,
103
+ jsonBytes,
104
+ digest,
105
+ freshness,
106
+ } ) {
107
+ const entry = FIELD_CLASSES.find((e ) => e.field === field);
108
+ if (!entry) {
109
+ throw new Error(`Field '${field}' is outside the IC-006 classification table (FR-011)`);
110
+ }
111
+ const cls = entry.cls;
112
+ const prio = priority !== null && priority !== undefined ? priority : (entry.priority ?? 0);
113
+ const computedMdBytes = mdBytes !== undefined ? mdBytes : Buffer.byteLength(md, 'utf8');
114
+ const computedJsonBytes = jsonBytes !== undefined
115
+ ? jsonBytes
116
+ : computeItemJsonBytes(field, value);
117
+ const computedDigest = digest !== undefined ? digest : itemDigest(value);
118
+
119
+ return {
120
+ id,
121
+ field,
122
+ cls,
123
+ priority: prio,
124
+ value,
125
+ md,
126
+ jsonBytes: computedJsonBytes,
127
+ mdBytes: computedMdBytes,
128
+ digest: computedDigest,
129
+ ...(freshness ? { freshness } : {}),
130
+ };
131
+ }
132
+
133
+ // Item builders for existing fields
134
+ function buildObjectiveItem(objective ) {
135
+ return buildPackItem({
136
+ id: 'objective',
137
+ field: 'objective',
138
+ value: objective,
139
+ md: objective ? `**Objective:** ${objective}\n\n` : '',
140
+ priority: 0,
141
+ });
142
+ }
143
+
144
+ function buildConstraintItem(c , index = 0) {
145
+ const id = typeof c === 'object' && c.id ? c.id : `nfr:${index + 1}`;
146
+ const text = typeof c === 'object' ? c.text : c;
147
+ const kind = typeof c === 'object' ? c.kind : 'constraint';
148
+ const val = typeof c === 'object' ? c : { id, text, kind };
149
+ return buildPackItem({
150
+ id,
151
+ field: 'constraints',
152
+ value: val,
153
+ md: `- ! ${id} ${text} (${kind})\n`,
154
+ priority: 0,
155
+ });
156
+ }
157
+
158
+ function buildAcceptanceCriteriaItem(ac , index = 0) {
159
+ const id = typeof ac === 'object' && ac.id ? ac.id : `ac:${index + 1}`;
160
+ const text = typeof ac === 'object' ? ac.text : ac;
161
+ const reqs = typeof ac === 'object' && Array.isArray(ac.requirements) ? ac.requirements : [];
162
+ const stories = typeof ac === 'object' && Array.isArray(ac.stories) ? ac.stories : [];
163
+ const val = typeof ac === 'object' ? ac : { id, text, requirements: reqs, stories };
164
+ const suffix = reqs.length > 0 ? ` (${reqs.join(', ')})` : '';
165
+ return buildPackItem({
166
+ id,
167
+ field: 'acceptanceCriteria',
168
+ value: val,
169
+ md: `- [ ] ${text}${suffix}\n`,
170
+ priority: 0,
171
+ });
172
+ }
173
+
174
+ function buildVerificationRequirementItem(vr , index = 0) {
175
+ const id = typeof vr === 'object' && vr.id ? vr.id : `vr:${index + 1}`;
176
+ const text = typeof vr === 'object' ? vr.text : vr;
177
+ const val = typeof vr === 'object' ? vr : { id, text };
178
+ return buildPackItem({
179
+ id,
180
+ field: 'verificationRequirements',
181
+ value: val,
182
+ md: `- ${text}\n`,
183
+ priority: 0,
184
+ });
185
+ }
186
+
187
+ function buildScopeExcludedItem(scope , index = 0) {
188
+ const id = typeof scope === 'object' && scope.id ? scope.id : `scope:${index + 1}`;
189
+ const text = typeof scope === 'object' ? scope.text : scope;
190
+ const val = typeof scope === 'object' ? scope : { id, text };
191
+ return buildPackItem({
192
+ id,
193
+ field: 'scopeExcluded',
194
+ value: val,
195
+ md: `- ${text}\n`,
196
+ priority: 0,
197
+ });
198
+ }
199
+
200
+ function buildClaimConflictItem(claim ) {
201
+ const supporting = Array.isArray(claim.supportingEvidence) ? claim.supportingEvidence : [];
202
+ const contradicting = Array.isArray(claim.contradictingEvidence) ? claim.contradictingEvidence : [];
203
+ return buildPackItem({
204
+ id: claim.id,
205
+ field: 'claims.conflicts',
206
+ value: {
207
+ id: claim.id,
208
+ statement: claim.statement,
209
+ supportingEvidence: supporting,
210
+ contradictingEvidence: contradicting,
211
+ },
212
+ md: `- CONFLICT ${claim.statement} (supports: ${supporting.join(', ')}; contradicts: ${contradicting.join(', ')})\n`,
213
+ priority: 0,
214
+ });
215
+ }
216
+
217
+ function buildBlockingGapItem(gap , index = 0) {
218
+ const id = typeof gap === 'object' && gap.id ? gap.id : `gap:${index + 1}`;
219
+ const text = typeof gap === 'object' ? gap.text : gap;
220
+ const val = typeof gap === 'object' ? gap : { id, text, source: 'requirement' };
221
+ return buildPackItem({
222
+ id,
223
+ field: 'blockingGaps',
224
+ value: val,
225
+ md: `- ${text}\n`,
226
+ priority: 0,
227
+ });
228
+ }
229
+
230
+ function buildResearchRequestItem(r ) {
231
+ const isBlockingUnanswered = Boolean(r.blocking && r.status !== 'ANSWERED');
232
+ const field = isBlockingUnanswered
233
+ ? 'researchRequests (blocking, not ANSWERED)'
234
+ : 'researchRequests (others)';
235
+ const val = {
236
+ id: r.id,
237
+ stageId: r.stageId,
238
+ question: r.question,
239
+ status: r.status,
240
+ blocking: r.blocking,
241
+ claimId: r.claimId,
242
+ evidenceIds: r.evidenceIds,
243
+ gap: r.gap,
244
+ };
245
+ let mdLine = isBlockingUnanswered
246
+ ? `- ${r.status} (blocking) ${r.stageId}: ${r.question}`
247
+ : `- ${r.status} ${r.blocking ? '(blocking)' : '(nonblocking)'} ${r.stageId}: ${r.question}`;
248
+ if (r.gap) mdLine += ` — ${r.gap}`;
249
+ if (r.claimId) mdLine += ` (claim: ${r.claimId})`;
250
+ mdLine += '\n';
251
+
252
+ return buildPackItem({
253
+ id: r.id,
254
+ field,
255
+ value: val,
256
+ md: mdLine,
257
+ priority: isBlockingUnanswered ? 0 : 3,
258
+ });
259
+ }
260
+
261
+ function buildDecisionReasonItem(reason ) {
262
+ return buildPackItem({
263
+ id: 'decisions:reason',
264
+ field: 'decisions.reason (register unreadable)',
265
+ value: { reason },
266
+ md: `- not supplied: ${reason}\n`,
267
+ priority: 0,
268
+ });
269
+ }
270
+
271
+ function buildDecisionLiveItem(d ) {
272
+ return buildPackItem({
273
+ id: d.id,
274
+ field: 'decisions.live',
275
+ value: {
276
+ id: d.id,
277
+ topic: d.topic,
278
+ statement: d.statement,
279
+ decidedBy: d.decidedBy,
280
+ stage: d.stage,
281
+ },
282
+ md: `- ${d.id} (${d.topic}): ${d.statement}\n`,
283
+ priority: 1,
284
+ });
285
+ }
286
+
287
+ function buildClaimSupportedItem(c ) {
288
+ const ev = Array.isArray(c.evidenceIds)
289
+ ? c.evidenceIds
290
+ : (Array.isArray(c.supportingEvidence) ? c.supportingEvidence : []);
291
+ const evSuffix = ev.length > 0 ? ` *(ev: ${ev.join(', ')})*` : '';
292
+ return buildPackItem({
293
+ id: c.id,
294
+ field: 'claims.supported',
295
+ value: { id: c.id, statement: c.statement, evidenceIds: ev },
296
+ md: `- ✓ ${c.statement}${evSuffix}\n`,
297
+ priority: 2,
298
+ });
299
+ }
300
+
301
+ function buildClaimHypothesisItem(h ) {
302
+ const id = typeof h === 'object' && h.id ? h.id : `hypothesis:${typeof h === 'object' ? h.statement : h}`;
303
+ const statement = typeof h === 'object' ? h.statement : h;
304
+ return buildPackItem({
305
+ id,
306
+ field: 'claims.hypotheses',
307
+ value: { id, statement },
308
+ md: `- ? ${statement}\n`,
309
+ priority: 4,
310
+ });
311
+ }
312
+
313
+ function buildPointerItem(p ) {
314
+ const rangeStr = p.range ? `:${p.range.startLine}-${p.range.endLine}` : '';
315
+ return buildPackItem({
316
+ id: p.id,
317
+ field: 'pointers',
318
+ value: p,
319
+ md: `- ${p.id} ${p.path}${rangeStr} ${p.bytes} B\n`,
320
+ priority: 5,
321
+ });
322
+ }
323
+
324
+ function buildRelevantFileItem(f ) {
325
+ return buildPackItem({
326
+ id: `file:${f}`,
327
+ field: 'relevantFiles',
328
+ value: f,
329
+ md: `- \`${f}\`\n`,
330
+ priority: 6,
331
+ });
332
+ }
333
+
334
+ function buildEvidenceSummaryItem(ev ) {
335
+ return buildPackItem({
336
+ id: ev.id,
337
+ field: 'evidenceSummary',
338
+ value: { id: ev.id, kind: ev.kind, locator: ev.locator, summary: ev.summary },
339
+ md: `- [${ev.kind}] ${ev.summary || ''} (${ev.locator?.file || ''})\n`,
340
+ priority: 7,
341
+ freshness: ev.freshness?.status,
342
+ });
343
+ }
344
+
345
+ function buildStructuralContextItem(struct , index = 0) {
346
+ const id = `struct:${index + 1}`;
347
+ return buildPackItem({
348
+ id,
349
+ field: 'structuralContext',
350
+ value: struct,
351
+ md: `- ${struct}\n`,
352
+ priority: 8,
353
+ });
354
+ }
355
+
356
+ const OPTIONAL_HEADING_BYTES_MD = Object.freeze({
357
+ 'decisions.live': Buffer.byteLength('### Live decisions\n\n', 'utf8'),
358
+ 'claims.supported': Buffer.byteLength('### Supported Facts & Invariants\n\n', 'utf8'),
359
+ 'researchRequests (others)': Buffer.byteLength('### Feature Research Requests\n\n', 'utf8'),
360
+ 'claims.hypotheses': Buffer.byteLength('### Active Hypotheses (Unverified)\n\n', 'utf8'),
361
+ pointers: Buffer.byteLength('### Pointers\n\n', 'utf8'),
362
+ relevantFiles: Buffer.byteLength('### Relevant File Locators\n\n', 'utf8'),
363
+ evidenceSummary: Buffer.byteLength('### Evidence\n\n', 'utf8'),
364
+ structuralContext: Buffer.byteLength('### Structural Context\n\n', 'utf8'),
365
+ });
366
+
367
+ const OPTIONAL_HEADING_BYTES_JSON = Object.freeze({
368
+ 'decisions.live': 5,
369
+ 'claims.supported': 5,
370
+ 'researchRequests (others)': 3,
371
+ 'claims.hypotheses': 4,
372
+ pointers: 3,
373
+ relevantFiles: 3,
374
+ evidenceSummary: 3,
375
+ structuralContext: 3,
376
+ });
377
+
378
+ const EMPTY_BLOCK_JSON = JSON.stringify({ total: 0, groups: [] }, null, 2);
379
+ const EMPTY_LINES_COUNT = EMPTY_BLOCK_JSON.split('\n').length;
380
+ const EMPTY_BYTES_JSON = Buffer.byteLength(EMPTY_BLOCK_JSON, 'utf8') + EMPTY_LINES_COUNT * 2;
381
+
382
+ function countNewlines(str ) {
383
+ let count = 0;
384
+ for (let i = 0; i < str.length; i++) {
385
+ if (str.charCodeAt(i) === 10) count++;
386
+ }
387
+ return count;
388
+ }
389
+
390
+ function computeOmissionBytes(omissionsList = []) {
391
+ if (!omissionsList || omissionsList.length === 0) {
392
+ return { md: 0, json: 0 };
393
+ }
394
+ const grouped = groupOmissions(omissionsList);
395
+ let mdStr = '### Omitted\n';
396
+ for (const g of grouped.groups) {
397
+ const fresh = g.freshness ? ` ${g.freshness}` : '';
398
+ mdStr += `- ${g.reason}${fresh} (${g.count}): ${g.ids.join(', ')}\n`;
399
+ }
400
+ mdStr += '\n';
401
+ const mdBytes = Buffer.byteLength(mdStr, 'utf8');
402
+
403
+ const fullBlock = JSON.stringify(grouped, null, 2);
404
+ const fullLinesCount = countNewlines(fullBlock) + 1;
405
+ const fullBytes = Buffer.byteLength(fullBlock, 'utf8') + fullLinesCount * 2;
406
+ const diffJson = Math.max(0, fullBytes - EMPTY_BYTES_JSON);
407
+ return { md: mdBytes, json: diffJson };
408
+ }
409
+
410
+ /**
411
+ * Computes both envelopes per IC-007 step 2.
412
+ */
413
+ function computeEnvelopes({
414
+ taskId = 'default',
415
+ taskClass = 'feature',
416
+ budget,
417
+ mandatory,
418
+ empty = false,
419
+ mode = 'full',
420
+ hasMandatoryItems = {},
421
+ overflow = false,
422
+ overflowAction = null,
423
+ toolLimitNotice = null,
424
+ limits = null,
425
+ selfRefWidth = null,
426
+ recordingExpected = false,
427
+ form = 'json',
428
+ harnessId = 'unstated',
429
+ } ) {
430
+ // --- Markdown envelope ---
431
+ let md = `## ContextPack: [${taskClass.toUpperCase()}] ${taskId}\n`;
432
+ if (overflowAction?.message) {
433
+ md += `> ${overflowAction.message}\n`;
434
+ }
435
+ if (toolLimitNotice?.message) {
436
+ md += `> ${toolLimitNotice.message}\n`;
437
+ }
438
+ if (mode !== 'bare') {
439
+ const mandSections = [
440
+ { key: 'acceptanceCriteria', heading: '### Acceptance Criteria\n' },
441
+ { key: 'constraints', heading: '### Constraints\n' },
442
+ { key: 'verificationRequirements', heading: '### Verification Requirements\n' },
443
+ { key: 'scopeExcluded', heading: '### Out of Scope\n' },
444
+ { key: 'blockingGaps', heading: '### Blocking Gaps\n' },
445
+ ];
446
+ for (const sec of mandSections) {
447
+ if (hasMandatoryItems[sec.key]) {
448
+ md += sec.heading + '\n';
449
+ } else {
450
+ const prov = mandatory?.provenance?.[sec.key] || mandatory?.fields?.[sec.key] || mandatory?.[sec.key];
451
+ const reason = prov?.reason || 'none';
452
+ md += `${sec.heading}- not supplied: ${reason}\n\n`;
453
+ }
454
+ }
455
+ if (hasMandatoryItems['claims.conflicts']) {
456
+ md += `### Blocking Conflicts\n\n`;
457
+ } else {
458
+ md += `### Blocking Conflicts\n- not supplied: none-recorded\n\n`;
459
+ }
460
+ // The decisions:reason item shares this section heading with live decisions.
461
+ md += `### Live decisions\n`;
462
+ if (mandatory) {
463
+ md += `### Mandatory Sources\n`;
464
+ const mandKeys = ['objective', 'acceptanceCriteria', 'constraints', 'verificationRequirements', 'scopeExcluded', 'blockingGaps'];
465
+ const provMap = mandatory.provenance || mandatory;
466
+ for (const k of mandKeys) {
467
+ const prov = provMap[k];
468
+ if (prov) {
469
+ if (prov.supplied) {
470
+ const digest12 = prov.digest ? prov.digest.slice(7, 19) : '000000000000';
471
+ const loc = prov.locator ? `${prov.locator}@${digest12}` : `@${digest12}`;
472
+ md += `- ${k}: ${prov.source} ${loc}\n`;
473
+ } else {
474
+ md += `- ${k}: not supplied: ${prov.reason || 'unknown'}\n`;
475
+ }
476
+ }
477
+ }
478
+ md += '\n';
479
+ }
480
+ if (empty) {
481
+ md += `_No evidence or claims are recorded for task '${taskId}'; this pack states nothing._\n\n`;
482
+ }
483
+ }
484
+
485
+ const paddedBytes = '000000000';
486
+ const maxBytes = budget?.maxBytes || 225000;
487
+ const source = budget?.source || 'task-class';
488
+ const tokensEst = Math.ceil(maxBytes / 4);
489
+ if (mode !== 'bare') {
490
+ md += `_Budget ${maxBytes} B (${source}) · emitted ${paddedBytes} B · tokens ~${tokensEst} (estimated:bytes/4) · actual unmeasured_\n`;
491
+ }
492
+ md += `[end of pack] mode=${mode} objective=1 ac=00 constraints=00 verification=00 scope=00 conflicts=00 gaps=00 omitted=000 bytes=${paddedBytes} digest=sha256:000000000000\n`;
493
+ const envelopeMd = Buffer.byteLength(md, 'utf8');
494
+
495
+ // --- JSON envelope ---
496
+ const capDigits = String(maxBytes).length;
497
+ const renderedWidth = selfRefWidth !== null && selfRefWidth !== undefined ? selfRefWidth : capDigits;
498
+ const selfRefBytes = Number('9'.repeat(Math.max(1, renderedWidth)));
499
+ const selfRefTokens = Math.ceil(selfRefBytes / 4);
500
+
501
+ const provMap = mandatory?.provenance || mandatory || {};
502
+ const skeletonOrdered = {
503
+ version: 1,
504
+ status: overflow ? 'OVERFLOW' : (empty ? 'EMPTY' : 'OK'),
505
+ exitCode: overflow ? 3 : (empty ? 1 : 0),
506
+ taskId,
507
+ taskClass,
508
+ compiledAt: '2026-10-13T00:00:00.000Z',
509
+ mode,
510
+ budget: budget || { maxBytes, source, harness: { id: null, source: 'unstated' }, capacity: { value: null, basis: 'unknown:unstated' }, toolResultLimitBytes: null },
511
+ overflow: overflow ? { cause: 'mandatory', mandatoryBytes: selfRefBytes, budgetBytes: maxBytes, excessBytes: 0, action: 'split-or-new-session' } : null,
512
+ overflowAction: overflowAction ? { message: overflowAction.message } : null,
513
+ toolLimitNotice: toolLimitNotice ? { message: toolLimitNotice.message } : null,
514
+ ...(hasMandatoryItems.objective ? {} : { objective: '' }),
515
+ acceptanceCriteria: [],
516
+ constraints: [],
517
+ verificationRequirements: [],
518
+ scopeExcluded: [],
519
+ blockingGaps: [],
520
+ claims: { conflicts: [], supported: [], hypotheses: [] },
521
+ researchRequests: [],
522
+ mandatory: provMap,
523
+ decisions: { available: true, live: [], liveCount: 0 },
524
+ pointers: [],
525
+ relevantFiles: [],
526
+ evidenceCount: 0,
527
+ evidenceSummary: [],
528
+ structuralContext: [],
529
+ budgetEnforcement: {
530
+ totalFiles: 0,
531
+ totalSupportedClaims: 0,
532
+ totalActiveHypotheses: 0,
533
+ limits: limits ? {
534
+ maxFiles: limits.maxFiles,
535
+ maxClaims: limits.maxClaims,
536
+ maxEvidenceItems: limits.maxEvidenceItems,
537
+ maxResearchRequests: limits.maxResearchRequests,
538
+ maxStructuralItems: limits.maxStructuralItems,
539
+ } : {},
540
+ },
541
+ empty,
542
+ omissions: { total: 0, groups: [] },
543
+ size: { totalBytes: selfRefBytes, mandatoryBytes: selfRefBytes, optionalBytes: selfRefBytes, fittedOn: 'json' },
544
+ accounting: { tokens: { inlineTokensEst: Math.ceil(maxBytes / 4), netTokensEst: Math.ceil(maxBytes / 4), basis: 'estimated:bytes/4', actual: { unmeasured: 'no harness reports token usage to an external CLI; profile tokenTelemetry unverified' } } },
545
+ delivery: { key: recordingExpected ? `${'sha256:' + '0'.repeat(64)}.${form}.${harnessId}` : null, packDigest: 'sha256:' + '0'.repeat(64), recorded: recordingExpected, reason: null, since: null, deltaRefused: null },
546
+ delta: null,
547
+ packEnd: { mode, objective: hasMandatoryItems.objective ? 1 : 0, acceptanceCriteria: 0, constraints: 0, verificationRequirements: 0, scopeExcluded: 0, conflicts: 0, blockingGaps: 0, omitted: 0, bytes: selfRefBytes, digest: 'sha256:000000000000' },
548
+ };
549
+
550
+ const jsonStr = JSON.stringify(skeletonOrdered, null, 2) + '\n';
551
+ let envelopeJson = Buffer.byteLength(jsonStr, 'utf8');
552
+
553
+ const mandKeysWithItems = ['acceptanceCriteria', 'constraints', 'verificationRequirements', 'scopeExcluded', 'blockingGaps'];
554
+ for (const k of mandKeysWithItems) {
555
+ if (hasMandatoryItems[k]) {
556
+ envelopeJson += 3;
557
+ }
558
+ }
559
+ if (hasMandatoryItems['claims.conflicts']) {
560
+ envelopeJson += 5;
561
+ }
562
+ if (hasMandatoryItems['researchRequests']) {
563
+ envelopeJson += 3;
564
+ }
565
+
566
+ return { envelopeMd, envelopeJson };
567
+ }
568
+
569
+ /**
570
+ * Linear fit on max(md, json) per IC-007.
571
+ */
572
+ function linearFit({
573
+ items,
574
+ budget,
575
+ taskId = 'default',
576
+ taskClass = 'feature',
577
+ envelopeMd = 0,
578
+ envelopeJson = 0,
579
+ includeOmissionCosts = true,
580
+ itemCapLimits = ITEM_CAP_LIMITS,
581
+ initialOmissions = [],
582
+ sectionHeadingsMd = OPTIONAL_HEADING_BYTES_MD,
583
+ sectionHeadingsJson = OPTIONAL_HEADING_BYTES_JSON,
584
+ } ) {
585
+ const mandatoryItems = items.filter((i ) => i.cls === 'mandatory' || i.class === 'mandatory' || i.deltaForced);
586
+ const allOptional = items.filter((i ) => (i.cls === 'optional' || i.class === 'optional') && !i.deltaForced);
587
+
588
+ // Enforce item limits on optional items (reason: 'item-cap')
589
+ const fieldCounts = {};
590
+ const eligibleOptional = [];
591
+ const cappedOmissions = [];
592
+
593
+ for (const item of allOptional) {
594
+ const limit = itemCapLimits[item.field];
595
+ const current = fieldCounts[item.field] || 0;
596
+ if (limit !== undefined && current >= limit) {
597
+ cappedOmissions.push({
598
+ id: item.id,
599
+ field: item.field,
600
+ reason: 'item-cap',
601
+ ...(item.freshness ? { freshness: item.freshness } : {}),
602
+ });
603
+ } else {
604
+ fieldCounts[item.field] = current + 1;
605
+ eligibleOptional.push(item);
606
+ }
607
+ }
608
+
609
+ // Mandatory costs
610
+ const mandatoryMdBytes = mandatoryItems.reduce((acc , i ) => acc + i.mdBytes, 0) + envelopeMd;
611
+ const mandatoryJsonBytes = mandatoryItems.reduce((acc , i ) => acc + i.jsonBytes, 0) + envelopeJson;
612
+
613
+ // Uncompressed omissions cost when all optional items are omitted
614
+ const allOmitted = [
615
+ ...initialOmissions,
616
+ ...eligibleOptional.map((i ) => ({
617
+ id: i.id,
618
+ field: i.field,
619
+ reason: 'mandatory-overflow',
620
+ ...(i.freshness ? { freshness: i.freshness } : {}),
621
+ })),
622
+ ...cappedOmissions,
623
+ ];
624
+ const allOmittedBytes = includeOmissionCosts ? computeOmissionBytes(allOmitted) : { md: 0, json: 0 };
625
+
626
+ const initialCostMd = mandatoryMdBytes + allOmittedBytes.md;
627
+ const initialCostJson = mandatoryJsonBytes + allOmittedBytes.json;
628
+
629
+ // Even when the all-omitted accounting form is over budget, test optional items
630
+ // in priority order: retaining an item can cost less than naming it as omitted.
631
+
632
+ // Sort optional items by priority ascending, then source order
633
+ const sortedOptional = [...eligibleOptional].sort((a , b ) => {
634
+ if (a.priority !== b.priority) return a.priority - b.priority;
635
+ return 0; // keeps source order
636
+ });
637
+
638
+ const keptOptional = [];
639
+ const rejectedOptional = [];
640
+ const activeSections = new Set ();
641
+ let keptMdSum = 0;
642
+ let keptJsonSum = 0;
643
+ let activeHeadingsMd = 0;
644
+ let activeHeadingsJson = 0;
645
+
646
+ // One linear pass
647
+ for (let idx = 0; idx < sortedOptional.length; idx++) {
648
+ const opt = sortedOptional[idx];
649
+ const isNewSection = !activeSections.has(opt.field);
650
+ const trialHeadingsMd = activeHeadingsMd + (isNewSection ? (sectionHeadingsMd[opt.field] || 0) : 0);
651
+ const trialHeadingsJson = activeHeadingsJson + (isNewSection ? (sectionHeadingsJson[opt.field] || 0) : 0);
652
+
653
+ const trialKeptMd = keptMdSum + opt.mdBytes;
654
+ const trialKeptJson = keptJsonSum + opt.jsonBytes;
655
+
656
+ const trialOmitted = [
657
+ ...initialOmissions,
658
+ ...rejectedOptional.map((i ) => ({
659
+ id: i.id,
660
+ field: i.field,
661
+ reason: 'budget',
662
+ ...(i.freshness ? { freshness: i.freshness } : {}),
663
+ })),
664
+ ...sortedOptional.slice(idx + 1).map((i ) => ({
665
+ id: i.id,
666
+ field: i.field,
667
+ reason: 'budget',
668
+ ...(i.freshness ? { freshness: i.freshness } : {}),
669
+ })),
670
+ ...cappedOmissions,
671
+ ];
672
+ const trialOmissionsBytes = includeOmissionCosts ? computeOmissionBytes(trialOmitted) : { md: 0, json: 0 };
673
+
674
+ const trialCostMd = mandatoryMdBytes + trialKeptMd + trialHeadingsMd + trialOmissionsBytes.md;
675
+ const trialCostJson = mandatoryJsonBytes + trialKeptJson + trialHeadingsJson + trialOmissionsBytes.json;
676
+
677
+ if (Math.max(trialCostMd, trialCostJson) <= budget) {
678
+ keptOptional.push(opt);
679
+ keptMdSum = trialKeptMd;
680
+ keptJsonSum = trialKeptJson;
681
+ if (isNewSection) {
682
+ activeSections.add(opt.field);
683
+ activeHeadingsMd = trialHeadingsMd;
684
+ activeHeadingsJson = trialHeadingsJson;
685
+ }
686
+ } else {
687
+ rejectedOptional.push(opt);
688
+ }
689
+ }
690
+
691
+ const finalOmissions = [
692
+ ...initialOmissions,
693
+ ...rejectedOptional.map((i ) => ({
694
+ id: i.id,
695
+ field: i.field,
696
+ reason: 'budget',
697
+ ...(i.freshness ? { freshness: i.freshness } : {}),
698
+ })),
699
+ ...cappedOmissions,
700
+ ];
701
+ const finalOmissionsBytes = includeOmissionCosts ? computeOmissionBytes(finalOmissions) : { md: 0, json: 0 };
702
+ const finalHeadingsMd = Array.from(activeSections).reduce((sum , f ) => sum + (sectionHeadingsMd[f] || 0), 0);
703
+ const finalHeadingsJson = Array.from(activeSections).reduce((sum , f ) => sum + (sectionHeadingsJson[f] || 0), 0);
704
+ const finalKeptMd = keptOptional.reduce((acc , i ) => acc + i.mdBytes, 0);
705
+ const finalKeptJson = keptOptional.reduce((acc , i ) => acc + i.jsonBytes, 0);
706
+
707
+ const finalCostMd = mandatoryMdBytes + finalKeptMd + finalHeadingsMd + finalOmissionsBytes.md;
708
+ const finalCostJson = mandatoryJsonBytes + finalKeptJson + finalHeadingsJson + finalOmissionsBytes.json;
709
+
710
+ const fittedOn = finalCostJson > finalCostMd ? 'json' : 'markdown';
711
+ const totalBytes = Math.max(finalCostMd, finalCostJson);
712
+ const mandatoryBytesByForm = { markdown: mandatoryMdBytes, json: mandatoryJsonBytes };
713
+ const mandatoryBytes = Math.max(mandatoryMdBytes, mandatoryJsonBytes);
714
+ const optionalBytes = totalBytes - mandatoryBytes;
715
+ const kept = [...mandatoryItems, ...keptOptional];
716
+
717
+ // Overflow is final only after every optional item has had its fit decision.
718
+ if (totalBytes > budget) {
719
+ const cause = mandatoryBytes > budget ? 'mandatory' : 'accounting';
720
+ return {
721
+ taskId,
722
+ taskClass,
723
+ budgetBytes: budget,
724
+ totalBytes,
725
+ mandatoryBytes,
726
+ mandatoryBytesByForm,
727
+ optionalBytes: 0,
728
+ overflow: true,
729
+ cause,
730
+ fittedOn,
731
+ kept: mandatoryItems,
732
+ omissions: allOmitted,
733
+ packDigest: computePackDigest(mandatoryItems),
734
+ };
735
+ }
736
+
737
+ return {
738
+ taskId,
739
+ taskClass,
740
+ budgetBytes: budget,
741
+ totalBytes,
742
+ mandatoryBytes,
743
+ mandatoryBytesByForm,
744
+ optionalBytes,
745
+ overflow: false,
746
+ cause: null,
747
+ fittedOn,
748
+ kept,
749
+ omissions: finalOmissions,
750
+ packDigest: computePackDigest(kept),
751
+ };
752
+ }
753
+
754
+ /**
755
+ * Groups omissions according to IC-008.
756
+ * Order: not-fresh STALE, not-fresh UNKNOWN, superseded, mandatory-overflow, budget, item-cap, unchanged.
757
+ */
758
+ function groupOmissions(omissionsList = [], recorded = false) {
759
+ if (!omissionsList || omissionsList.length === 0) {
760
+ return { total: 0, groups: [] };
761
+ }
762
+
763
+ const rank = (g ) => {
764
+ if (g.reason === 'not-fresh' && g.freshness === 'STALE') return 1;
765
+ if (g.reason === 'not-fresh' && g.freshness === 'UNKNOWN') return 2;
766
+ if (g.reason === 'not-fresh') return 2;
767
+ if (g.reason === 'superseded') return 3;
768
+ if (g.reason === 'mandatory-overflow') return 4;
769
+ if (g.reason === 'budget') return 5;
770
+ if (g.reason === 'item-cap') return 6;
771
+ if (g.reason === 'unchanged') return 7;
772
+ return 8;
773
+ };
774
+
775
+ const groupMap = new Map ();
776
+
777
+ for (const item of omissionsList) {
778
+ const key = `${item.reason}:${item.freshness || ''}`;
779
+ if (!groupMap.has(key)) {
780
+ groupMap.set(key, {
781
+ reason: item.reason,
782
+ ...(item.freshness ? { freshness: item.freshness } : {}),
783
+ ids: [],
784
+ ranges: [],
785
+ });
786
+ }
787
+ groupMap.get(key) .ids.push(item.id);
788
+ }
789
+
790
+ const groups = Array.from(groupMap.entries())
791
+ .map(([key, g]) => {
792
+ const matching = omissionsList.filter((item) => `${item.reason}:${item.freshness || ''}` === key);
793
+ const ranges = [];
794
+ const ranged = new Set ();
795
+ if (recorded) {
796
+ for (let start = 0; start < matching.length;) {
797
+ const first = matching[start];
798
+ const match = /^(.+?)(\d+)$/.exec(first.id);
799
+ let end = start;
800
+ if (match) {
801
+ let previous = Number(match[2]);
802
+ for (let i = start + 1; i < matching.length; i += 1) {
803
+ const next = /^(.+?)(\d+)$/.exec(matching[i].id);
804
+ if (matching[i].field !== first.field || !next || next[1] !== match[1] || Number(next[2]) !== previous + 1) break;
805
+ previous = Number(next[2]);
806
+ end = i;
807
+ }
808
+ }
809
+ if (end - start + 1 >= 3) {
810
+ for (let i = start; i <= end; i += 1) ranged.add(i);
811
+ ranges.push({ field: first.field, from: matching[start].id, to: matching[end].id, count: end - start + 1 });
812
+ }
813
+ start = end + 1;
814
+ }
815
+ }
816
+ const ids = g.ids.filter((_, index) => !ranged.has(index));
817
+ return { ...g, ids, ranges, count: g.ids.length };
818
+ })
819
+ .sort((a, b) => rank(a) - rank(b));
820
+
821
+ return {
822
+ total: omissionsList.length,
823
+ groups,
824
+ };
825
+ }
826
+
827
+ /**
828
+ * Formats the OVERFLOW notice per IC-005.
829
+ */
830
+ function formatOverflowMessage({ cause, mandatoryBytes, totalBytes, budgetBytes, source } ) {
831
+ const secondSentence = cause === 'accounting'
832
+ ? `The omission record brings the pack to ${totalBytes} B against a ${budgetBytes} B budget.`
833
+ : `It is ${mandatoryBytes} B against a ${budgetBytes} B budget (${source}).`;
834
+ return `OVERFLOW (exit 3): mandatory content is complete and nothing in it was dropped. ${secondSentence} Split the task or its context, or start a new session, then request the pack again.`;
835
+ }
836
+
837
+ /**
838
+ * Formats short pack digest: sha256:<first 12 hex> per IC-009.
839
+ */
840
+ function formatShortDigest(packDigest ) {
841
+ if (!packDigest) return 'sha256:000000000000';
842
+ const hex = packDigest.replace(/^sha256:/, '');
843
+ return `sha256:${hex.slice(0, 12)}`;
844
+ }
845
+
846
+ /**
847
+ * Parses the [end of pack] line from the end of a markdown pack.
848
+ * Returns null if the line is missing or malformed.
849
+ */
850
+ function parsePackEndLine(md ) {
851
+ if (typeof md !== 'string') return null;
852
+ const lines = md.trimEnd().split('\n');
853
+ const lastLine = lines[lines.length - 1];
854
+ if (!lastLine || !lastLine.startsWith('[end of pack] ')) return null;
855
+
856
+ const match = lastLine.match(
857
+ /^\[end of pack\] mode=(\S+) objective=(\d+) ac=(\d+) constraints=(\d+) verification=(\d+) scope=(\d+) conflicts=(\d+) gaps=(\d+) omitted=(\d+) bytes=(\d+) digest=(\S+)$/
858
+ );
859
+ if (!match) return null;
860
+
861
+ return {
862
+ mode: match[1],
863
+ objective: parseInt(match[2], 10),
864
+ acceptanceCriteria: parseInt(match[3], 10),
865
+ constraints: parseInt(match[4], 10),
866
+ verificationRequirements: parseInt(match[5], 10),
867
+ scopeExcluded: parseInt(match[6], 10),
868
+ conflicts: parseInt(match[7], 10),
869
+ blockingGaps: parseInt(match[8], 10),
870
+ omitted: parseInt(match[9], 10),
871
+ bytes: parseInt(match[10], 10),
872
+ rawBytes: match[10],
873
+ isPadded: match[10].length === 9,
874
+ digest: match[11],
875
+ };
876
+ }
877
+
878
+ /**
879
+ * Verifies that a markdown pack ends with a valid, padded [end of pack] line
880
+ * whose byte count equals the total byte length of the markdown.
881
+ */
882
+ function verifyPackEndLine(md ) {
883
+ if (typeof md !== 'string') {
884
+ return { ok: false, reason: 'invalid-input' };
885
+ }
886
+ const parsed = parsePackEndLine(md);
887
+ if (!parsed) {
888
+ return { ok: false, reason: 'missing-end-of-pack-line' };
889
+ }
890
+ if (!parsed.isPadded) {
891
+ return { ok: false, reason: 'unpadded-bytes-field', parsed };
892
+ }
893
+ const actualLen = Buffer.byteLength(md, 'utf8');
894
+ if (parsed.bytes !== actualLen) {
895
+ return {
896
+ ok: false,
897
+ reason: `byte-count-mismatch: declared ${parsed.bytes}, actual ${actualLen}`,
898
+ parsed,
899
+ };
900
+ }
901
+ return { ok: true, parsed };
902
+ }
903
+
904
+ /**
905
+ * Formats a ContextPack into markdown per IC-005.
906
+ */
907
+ function formatMarkdown(pack , options = {}) {
908
+ const throwOnOverBudget = options.throwOnOverBudget !== false;
909
+ const toolLimitBytes = pack.budget?.toolResultLimitBytes;
910
+ const isToolLimitDeclared = typeof toolLimitBytes === 'number' && toolLimitBytes > 0;
911
+ const harnessName = pack.budget?.harness?.id || 'harness';
912
+
913
+ const getToolLimitMessage = (bytes ) => {
914
+ return `TOOL LIMIT: this pack is ${bytes} B and ${harnessName} delivers tool output up to ${toolLimitBytes} B, so it may arrive cut. If the last line is not "[end of pack] ...", it was cut.`;
915
+ };
916
+
917
+ const mode = pack.mode || 'full';
918
+ if (pack.expansion) {
919
+ const body = `### Expanded: ${pack.expansion.path}\n${pack.expansion.content}${pack.expansion.targetChanged ? '\n> Target changed since this pointer was recorded.\n' : ''}${pack.delivery?.recorded === false ? `\n> Expansion not recorded: ${pack.delivery.reason || 'unknown'}\n` : ''}\n`;
920
+ const digest = pack.packEnd?.digest || formatShortDigest(pack.delivery?.packDigest);
921
+ let total = pack.size?.totalBytes || 0;
922
+ for (let i = 0; i < 6; i++) {
923
+ const header = `## ContextPack: [${pack.taskClass.toUpperCase()}] ${pack.taskId}\n`;
924
+ const footer = `_Budget ${pack.budget.maxBytes} B (${pack.budget.source}) · emitted ${total} B · tokens ~${Math.round(total / 4)} (estimated:bytes/4) · actual unmeasured_\n`;
925
+ const end = `[end of pack] mode=expand objective=0 ac=0 constraints=0 verification=0 scope=0 conflicts=0 gaps=0 omitted=0 bytes=${String(total).padStart(9, '0')} digest=${digest}\n`;
926
+ const output = header + body + footer + end;
927
+ const actual = Buffer.byteLength(output, 'utf8');
928
+ if (actual === total) {
929
+ pack.size.totalBytes = actual;
930
+ pack.packEnd.bytes = actual;
931
+ if (throwOnOverBudget && actual > pack.budget.maxBytes) throw new Error(`Emitted markdown pack size (${actual} B) exceeds budget (${pack.budget.maxBytes} B)`);
932
+ return output;
933
+ }
934
+ total = actual;
935
+ }
936
+ }
937
+ let md = '';
938
+ if (pack.objective) {
939
+ md += `**Objective:** ${pack.objective}\n\n`;
940
+ }
941
+
942
+ if (mode !== 'bare') {
943
+ // Mandatory sections
944
+ md += `### Acceptance Criteria\n`;
945
+ if (pack.acceptanceCriteria && pack.acceptanceCriteria.length > 0) {
946
+ for (const ac of pack.acceptanceCriteria) {
947
+ const text = typeof ac === 'object' ? ac.text : ac;
948
+ const reqs = typeof ac === 'object' && Array.isArray(ac.requirements) && ac.requirements.length > 0
949
+ ? ` (${ac.requirements.join(', ')})`
950
+ : '';
951
+ md += `- [ ] ${text}${reqs}\n`;
952
+ }
953
+ } else {
954
+ const reason = pack.mandatory?.acceptanceCriteria?.reason || 'none';
955
+ md += `- not supplied: ${reason}\n`;
956
+ }
957
+ md += '\n';
958
+
959
+ md += `### Constraints\n`;
960
+ if (pack.constraints && pack.constraints.length > 0) {
961
+ for (const c of pack.constraints) {
962
+ const id = typeof c === 'object' && c.id ? c.id : '';
963
+ const text = typeof c === 'object' ? c.text : c;
964
+ const kind = typeof c === 'object' && c.kind ? ` (${c.kind})` : '';
965
+ md += `- ! ${id ? id + ' ' : ''}${text}${kind}\n`;
966
+ }
967
+ } else {
968
+ const reason = pack.mandatory?.constraints?.reason || 'none';
969
+ md += `- not supplied: ${reason}\n`;
970
+ }
971
+ md += '\n';
972
+
973
+ md += `### Verification Requirements\n`;
974
+ if (pack.verificationRequirements && pack.verificationRequirements.length > 0) {
975
+ for (const vr of pack.verificationRequirements) {
976
+ const text = typeof vr === 'object' ? vr.text : vr;
977
+ md += `- ${text}\n`;
978
+ }
979
+ } else {
980
+ const reason = pack.mandatory?.verificationRequirements?.reason || 'none';
981
+ md += `- not supplied: ${reason}\n`;
982
+ }
983
+ md += '\n';
984
+
985
+ md += `### Out of Scope\n`;
986
+ if (pack.scopeExcluded && pack.scopeExcluded.length > 0) {
987
+ for (const se of pack.scopeExcluded) {
988
+ const text = typeof se === 'object' ? se.text : se;
989
+ md += `- ${text}\n`;
990
+ }
991
+ } else {
992
+ const reason = pack.mandatory?.scopeExcluded?.reason || 'none';
993
+ md += `- not supplied: ${reason}\n`;
994
+ }
995
+ md += '\n';
996
+
997
+ if (pack.claims?.conflicts && pack.claims.conflicts.length > 0) {
998
+ md += `### Blocking Conflicts\n`;
999
+ for (const c of pack.claims.conflicts) {
1000
+ const sup = Array.isArray(c.supportingEvidence) ? c.supportingEvidence.join(', ') : '';
1001
+ const con = Array.isArray(c.contradictingEvidence) ? c.contradictingEvidence.join(', ') : '';
1002
+ md += `- CONFLICT ${c.statement} (supports: ${sup}; contradicts: ${con})\n`;
1003
+ }
1004
+ md += '\n';
1005
+ } else {
1006
+ md += `### Blocking Conflicts\n- not supplied: none-recorded\n\n`;
1007
+ }
1008
+
1009
+ const blockingUnanswered = (pack.researchRequests || []).filter((r ) => r.blocking && r.status !== 'ANSWERED');
1010
+ const otherRequests = (pack.researchRequests || []).filter((r ) => !(r.blocking && r.status !== 'ANSWERED'));
1011
+
1012
+ md += `### Blocking Gaps\n`;
1013
+ const hasBlockingGaps = (pack.blockingGaps && pack.blockingGaps.length > 0) || blockingUnanswered.length > 0;
1014
+ if (hasBlockingGaps) {
1015
+ if (pack.blockingGaps && pack.blockingGaps.length > 0) {
1016
+ for (const bg of pack.blockingGaps) {
1017
+ const text = typeof bg === 'object' ? bg.text : bg;
1018
+ md += `- ${text}\n`;
1019
+ }
1020
+ }
1021
+ for (const r of blockingUnanswered) {
1022
+ let line = `- ${r.status} (blocking) ${r.stageId}: ${r.question}`;
1023
+ if (r.gap) line += ` — ${r.gap}`;
1024
+ if (r.claimId) line += ` (claim: ${r.claimId})`;
1025
+ md += `${line}\n`;
1026
+ }
1027
+ } else {
1028
+ const reason = pack.mandatory?.blockingGaps?.reason || 'none';
1029
+ md += `- not supplied: ${reason}\n`;
1030
+ }
1031
+ md += '\n';
1032
+
1033
+ if (pack.mandatory) {
1034
+ md += `### Mandatory Sources\n`;
1035
+ const mandKeys = ['objective', 'acceptanceCriteria', 'constraints', 'verificationRequirements', 'scopeExcluded', 'blockingGaps'];
1036
+ for (const k of mandKeys) {
1037
+ const prov = pack.mandatory[k];
1038
+ if (prov) {
1039
+ if (prov.supplied) {
1040
+ const digest12 = prov.digest ? prov.digest.slice(7, 19) : '';
1041
+ const loc = prov.locator ? `${prov.locator}@${digest12}` : `@${digest12}`;
1042
+ md += `- ${k}: ${prov.source} ${loc}\n`;
1043
+ } else {
1044
+ md += `- ${k}: not supplied: ${prov.reason || 'unknown'}\n`;
1045
+ }
1046
+ }
1047
+ }
1048
+ md += '\n';
1049
+ }
1050
+
1051
+ // Optional sections
1052
+ if ((pack.decisions?.live && pack.decisions.live.length > 0) || pack.decisions?.reason) {
1053
+ md += `### Live decisions\n`;
1054
+ if (pack.decisions?.reason) {
1055
+ md += `- not supplied: ${pack.decisions.reason}\n`;
1056
+ }
1057
+ for (const d of pack.decisions.live || []) {
1058
+ md += `- ${d.id} (${d.topic}): ${d.statement}\n`;
1059
+ }
1060
+ md += '\n';
1061
+ }
1062
+
1063
+ if (pack.claims?.supported && pack.claims.supported.length > 0) {
1064
+ md += `### Supported Facts & Invariants\n`;
1065
+ for (const c of pack.claims.supported) {
1066
+ const evList = Array.isArray(c.evidenceIds) ? c.evidenceIds : (Array.isArray(c.supportingEvidence) ? c.supportingEvidence : []);
1067
+ const ev = evList.length > 0 ? ` *(ev: ${evList.join(', ')})*` : '';
1068
+ md += `- ✓ ${c.statement}${ev}\n`;
1069
+ }
1070
+ md += '\n';
1071
+ }
1072
+
1073
+ if (otherRequests.length > 0) {
1074
+ md += `### Feature Research Requests\n`;
1075
+ for (const r of otherRequests) {
1076
+ let line = `- ${r.status} ${r.blocking ? '(blocking)' : '(nonblocking)'} ${r.stageId}: ${r.question}`;
1077
+ if (r.gap) line += ` — ${r.gap}`;
1078
+ if (r.claimId) line += ` (claim: ${r.claimId})`;
1079
+ md += `${line}\n`;
1080
+ }
1081
+ md += '\n';
1082
+ }
1083
+
1084
+ if (pack.claims?.hypotheses && pack.claims.hypotheses.length > 0) {
1085
+ md += `### Active Hypotheses (Unverified)\n`;
1086
+ for (const h of pack.claims.hypotheses) {
1087
+ const statement = typeof h === 'object' ? h.statement : h;
1088
+ md += `- ? ${statement}\n`;
1089
+ }
1090
+ md += '\n';
1091
+ }
1092
+
1093
+ if (pack.pointers && pack.pointers.length > 0) {
1094
+ md += `### Pointers\n`;
1095
+ for (const p of pack.pointers) {
1096
+ const range = p.range ? `:${p.range.startLine}-${p.range.endLine}` : '';
1097
+ md += `- ${p.id} ${p.path}${range} ${p.bytes} B\n`;
1098
+ }
1099
+ md += '\n';
1100
+ }
1101
+
1102
+ if (pack.relevantFiles && pack.relevantFiles.length > 0) {
1103
+ md += `### Relevant File Locators\n`;
1104
+ for (const f of pack.relevantFiles) {
1105
+ md += `- \`${f}\`\n`;
1106
+ }
1107
+ md += '\n';
1108
+ }
1109
+
1110
+ if (pack.evidenceSummary && pack.evidenceSummary.length > 0) {
1111
+ md += `### Evidence\n`;
1112
+ for (const ev of pack.evidenceSummary) {
1113
+ md += `- [${ev.kind}] ${ev.summary || ''} (${ev.locator?.file || ''})\n`;
1114
+ }
1115
+ md += '\n';
1116
+ }
1117
+
1118
+ if (pack.structuralContext && pack.structuralContext.length > 0) {
1119
+ md += `### Structural Context\n`;
1120
+ for (const s of pack.structuralContext) {
1121
+ md += `- ${s}\n`;
1122
+ }
1123
+ md += '\n';
1124
+ }
1125
+
1126
+ if (pack.empty) {
1127
+ md += `_No evidence or claims are recorded for task '${pack.taskId}'; this pack states nothing._\n\n`;
1128
+ }
1129
+
1130
+ if (mode === 'delta' && pack.delta?.removed?.length) {
1131
+ md += `### Removed since ${formatShortDigest(pack.delta.since)}\n`;
1132
+ for (const id of pack.delta.removed) md += `- ${id}\n`;
1133
+ md += '\n';
1134
+ }
1135
+
1136
+ if (pack.omissions?.groups && pack.omissions.groups.length > 0) {
1137
+ md += `### Omitted\n`;
1138
+ for (const g of pack.omissions.groups) {
1139
+ const fresh = g.freshness ? ` ${g.freshness}` : '';
1140
+ const entries = [
1141
+ ...g.ids,
1142
+ ...(g.ranges || []).map((range ) => `${range.from}..${range.to} (${range.count})`),
1143
+ ];
1144
+ md += `- ${g.reason}${fresh} (${g.count}): ${entries.join(', ')}\n`;
1145
+ }
1146
+ md += '\n';
1147
+ }
1148
+ }
1149
+
1150
+ const body = md;
1151
+ const endObj = pack.packEnd || {};
1152
+ const shortDigest = endObj.digest || formatShortDigest(pack.delivery?.packDigest);
1153
+
1154
+ const objectiveCount = endObj.objective ?? (pack.objective ? 1 : 0);
1155
+ const acCount = endObj.acceptanceCriteria ?? (pack.acceptanceCriteria?.length || 0);
1156
+ const constraintsCount = endObj.constraints ?? (pack.constraints?.length || 0);
1157
+ const verificationCount = endObj.verificationRequirements ?? (pack.verificationRequirements?.length || 0);
1158
+ const scopeCount = endObj.scopeExcluded ?? (pack.scopeExcluded?.length || 0);
1159
+ const conflictsCount = endObj.conflicts ?? (pack.claims?.conflicts?.length || 0);
1160
+ const gapsCount = endObj.blockingGaps ?? (pack.blockingGaps?.length || 0);
1161
+ const omittedCount = endObj.omitted ?? (pack.omissions?.total || 0);
1162
+
1163
+ let totalBytes = pack.size?.totalBytes || 1000;
1164
+ for (let iter = 0; iter < 5; iter++) {
1165
+ let header = `## ContextPack: [${pack.taskClass.toUpperCase()}] ${pack.taskId}\n`;
1166
+ if (pack.overflowAction?.message) {
1167
+ header += `> ${pack.overflowAction.message}\n`;
1168
+ }
1169
+ if (isToolLimitDeclared && totalBytes > toolLimitBytes) {
1170
+ const msg = getToolLimitMessage(totalBytes);
1171
+ header += `> ${msg}\n`;
1172
+ pack.toolLimitNotice = { message: msg };
1173
+ } else {
1174
+ pack.toolLimitNotice = null;
1175
+ }
1176
+
1177
+ const tokensEst = Math.ceil(totalBytes / 4);
1178
+ const footer = mode === 'bare'
1179
+ ? ''
1180
+ : `_Budget ${pack.budget.maxBytes} B (${pack.budget.source}) · emitted ${totalBytes} B · tokens ~${tokensEst} (estimated:bytes/4) · actual unmeasured_\n`;
1181
+ const paddedBytes = String(totalBytes).padStart(9, '0');
1182
+ const endLine = `[end of pack] mode=${mode} objective=${objectiveCount} ac=${acCount} constraints=${constraintsCount} verification=${verificationCount} scope=${scopeCount} conflicts=${conflictsCount} gaps=${gapsCount} omitted=${omittedCount} bytes=${paddedBytes} digest=${shortDigest}\n`;
1183
+
1184
+ const full = header + body + footer + endLine;
1185
+ const actualLen = Buffer.byteLength(full, 'utf8');
1186
+ if (actualLen === totalBytes) {
1187
+ if (pack.size) {
1188
+ pack.size.totalBytes = actualLen;
1189
+ }
1190
+ if (pack.packEnd) pack.packEnd.bytes = actualLen;
1191
+ if (throwOnOverBudget && pack.status === 'OK' && pack.budget?.maxBytes && actualLen > pack.budget.maxBytes) {
1192
+ throw new Error(`Emitted markdown pack size (${actualLen} B) exceeds budget (${pack.budget.maxBytes} B)`);
1193
+ }
1194
+ return full;
1195
+ }
1196
+ totalBytes = actualLen;
1197
+ }
1198
+
1199
+ let header = `## ContextPack: [${pack.taskClass.toUpperCase()}] ${pack.taskId}\n`;
1200
+ if (pack.overflowAction?.message) {
1201
+ header += `> ${pack.overflowAction.message}\n`;
1202
+ }
1203
+ if (isToolLimitDeclared && totalBytes > toolLimitBytes) {
1204
+ header += `> ${getToolLimitMessage(totalBytes)}\n`;
1205
+ pack.toolLimitNotice = { message: getToolLimitMessage(totalBytes) };
1206
+ } else {
1207
+ pack.toolLimitNotice = null;
1208
+ }
1209
+ const tokensEst = Math.ceil(totalBytes / 4);
1210
+ const footer = `_Budget ${pack.budget.maxBytes} B (${pack.budget.source}) · emitted ${totalBytes} B · tokens ~${tokensEst} (estimated:bytes/4) · actual unmeasured_\n`;
1211
+ const paddedBytes = String(totalBytes).padStart(9, '0');
1212
+ const endLine = `[end of pack] mode=${mode} objective=${objectiveCount} ac=${acCount} constraints=${constraintsCount} verification=${verificationCount} scope=${scopeCount} conflicts=${conflictsCount} gaps=${gapsCount} omitted=${omittedCount} bytes=${paddedBytes} digest=${shortDigest}\n`;
1213
+ const full = header + body + footer + endLine;
1214
+ const actualLen = Buffer.byteLength(full, 'utf8');
1215
+ if (pack.size) {
1216
+ pack.size.totalBytes = actualLen;
1217
+ }
1218
+ if (pack.packEnd) pack.packEnd.bytes = actualLen;
1219
+ if (throwOnOverBudget && pack.status === 'OK' && pack.budget?.maxBytes && actualLen > pack.budget.maxBytes) {
1220
+ throw new Error(`Emitted markdown pack size (${actualLen} B) exceeds budget (${pack.budget.maxBytes} B)`);
1221
+ }
1222
+ return full;
1223
+ }
1224
+
1225
+ /**
1226
+ * Formats a ContextPack into JSON per IC-004.
1227
+ */
1228
+ function formatJson(pack , options = {}) {
1229
+ const throwOnOverBudget = options.throwOnOverBudget !== false;
1230
+ const toolLimitBytes = pack.budget?.toolResultLimitBytes;
1231
+ const isToolLimitDeclared = typeof toolLimitBytes === 'number' && toolLimitBytes > 0;
1232
+ const harnessName = pack.budget?.harness?.id || 'harness';
1233
+
1234
+ const getToolLimitMessage = (bytes ) => {
1235
+ return `TOOL LIMIT: this pack is ${bytes} B and ${harnessName} delivers tool output up to ${toolLimitBytes} B, so it may arrive cut. If the last line is not "[end of pack] ...", it was cut.`;
1236
+ };
1237
+
1238
+ const ordered = {};
1239
+ const keys = [
1240
+ 'version', 'status', 'exitCode', 'taskId', 'taskClass', 'compiledAt', 'mode',
1241
+ 'budget', 'overflow', 'overflowAction', 'toolLimitNotice', 'objective',
1242
+ 'acceptanceCriteria', 'constraints', 'verificationRequirements', 'scopeExcluded',
1243
+ 'blockingGaps', 'claims', 'researchRequests', 'mandatory', 'decisions',
1244
+ 'pointers', 'relevantFiles', 'evidenceCount', 'evidenceSummary', 'structuralContext',
1245
+ 'budgetEnforcement', 'empty', 'omissions', 'size', 'accounting', 'delivery',
1246
+ 'delta', 'expansion', 'expansionRecorded', 'packEnd',
1247
+ ];
1248
+
1249
+ for (const k of keys) {
1250
+ if (pack[k] !== undefined) {
1251
+ ordered[k] = pack[k];
1252
+ } else {
1253
+ ordered[k] = null;
1254
+ }
1255
+ }
1256
+
1257
+ const endObj = pack.packEnd || {};
1258
+ const mode = endObj.mode || pack.mode || 'full';
1259
+ const shortDigest = endObj.digest || formatShortDigest(pack.delivery?.packDigest);
1260
+
1261
+ const objectiveCount = endObj.objective ?? (pack.objective ? 1 : 0);
1262
+ const acCount = endObj.acceptanceCriteria ?? (pack.acceptanceCriteria?.length || 0);
1263
+ const constraintsCount = endObj.constraints ?? (pack.constraints?.length || 0);
1264
+ const verificationCount = endObj.verificationRequirements ?? (pack.verificationRequirements?.length || 0);
1265
+ const scopeCount = endObj.scopeExcluded ?? (pack.scopeExcluded?.length || 0);
1266
+ const conflictsCount = endObj.conflicts ?? (pack.claims?.conflicts?.length || 0);
1267
+ const gapsCount = endObj.blockingGaps ?? (pack.blockingGaps?.length || 0);
1268
+ const omittedCount = endObj.omitted ?? (pack.omissions?.total || 0);
1269
+
1270
+ ordered.packEnd = {
1271
+ mode,
1272
+ objective: objectiveCount,
1273
+ acceptanceCriteria: acCount,
1274
+ constraints: constraintsCount,
1275
+ verificationRequirements: verificationCount,
1276
+ scopeExcluded: scopeCount,
1277
+ conflicts: conflictsCount,
1278
+ blockingGaps: gapsCount,
1279
+ omitted: omittedCount,
1280
+ bytes: pack.size?.totalBytes || 1000,
1281
+ digest: shortDigest,
1282
+ };
1283
+
1284
+ let totalBytes = pack.size?.totalBytes || 1000;
1285
+ for (let iter = 0; iter < 5; iter++) {
1286
+ ordered.size.totalBytes = totalBytes;
1287
+ ordered.packEnd.bytes = totalBytes;
1288
+ if (isToolLimitDeclared) {
1289
+ if (totalBytes > toolLimitBytes) {
1290
+ const msg = getToolLimitMessage(totalBytes);
1291
+ ordered.toolLimitNotice = { message: msg };
1292
+ pack.toolLimitNotice = ordered.toolLimitNotice;
1293
+ } else {
1294
+ ordered.toolLimitNotice = null;
1295
+ pack.toolLimitNotice = null;
1296
+ }
1297
+ }
1298
+ const jsonStr = JSON.stringify(ordered, null, 2) + '\n';
1299
+ const actualLen = Buffer.byteLength(jsonStr, 'utf8');
1300
+ if (actualLen === totalBytes) {
1301
+ if (pack.size) {
1302
+ pack.size.totalBytes = actualLen;
1303
+ ordered.size.totalBytes = pack.size.totalBytes;
1304
+ }
1305
+ if (pack.packEnd) pack.packEnd.bytes = actualLen;
1306
+ ordered.packEnd.bytes = actualLen;
1307
+ if (throwOnOverBudget && pack.status === 'OK' && pack.budget?.maxBytes && actualLen > pack.budget.maxBytes) {
1308
+ throw new Error(`Emitted JSON pack size (${actualLen} B) exceeds budget (${pack.budget.maxBytes} B)`);
1309
+ }
1310
+ return jsonStr;
1311
+ }
1312
+ totalBytes = actualLen;
1313
+ }
1314
+
1315
+ ordered.size.totalBytes = totalBytes;
1316
+ ordered.packEnd.bytes = totalBytes;
1317
+ const jsonStr = JSON.stringify(ordered, null, 2) + '\n';
1318
+ const actualLen = Buffer.byteLength(jsonStr, 'utf8');
1319
+ if (pack.size) {
1320
+ pack.size.totalBytes = actualLen;
1321
+ ordered.size.totalBytes = pack.size.totalBytes;
1322
+ }
1323
+ if (pack.packEnd) pack.packEnd.bytes = actualLen;
1324
+ ordered.packEnd.bytes = actualLen;
1325
+ if (throwOnOverBudget && pack.status === 'OK' && pack.budget?.maxBytes && actualLen > pack.budget.maxBytes) {
1326
+ throw new Error(`Emitted JSON pack size (${actualLen} B) exceeds budget (${pack.budget.maxBytes} B)`);
1327
+ }
1328
+ return jsonStr;
1329
+ }
1330
+
1331
+ module.exports = {
1332
+ FIELD_CLASSES,
1333
+ ITEM_CAP_LIMITS,
1334
+ canonicalJson,
1335
+ itemDigest,
1336
+ computePackDigest,
1337
+ formatShortDigest,
1338
+ parsePackEndLine,
1339
+ verifyPackEndLine,
1340
+ buildPackItem,
1341
+ buildObjectiveItem,
1342
+ buildConstraintItem,
1343
+ buildAcceptanceCriteriaItem,
1344
+ buildVerificationRequirementItem,
1345
+ buildScopeExcludedItem,
1346
+ buildClaimConflictItem,
1347
+ buildBlockingGapItem,
1348
+ buildResearchRequestItem,
1349
+ buildDecisionReasonItem,
1350
+ buildDecisionLiveItem,
1351
+ buildClaimSupportedItem,
1352
+ buildClaimHypothesisItem,
1353
+ buildPointerItem,
1354
+ buildRelevantFileItem,
1355
+ buildEvidenceSummaryItem,
1356
+ buildStructuralContextItem,
1357
+ computeItemJsonBytes,
1358
+ computeOmissionBytes,
1359
+ computeEnvelopes,
1360
+ OPTIONAL_HEADING_BYTES_MD,
1361
+ OPTIONAL_HEADING_BYTES_JSON,
1362
+ linearFit,
1363
+ groupOmissions,
1364
+ formatOverflowMessage,
1365
+ formatMarkdown,
1366
+ formatJson,
1367
+ };