opencastle 0.31.7 → 0.32.1

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 (220) hide show
  1. package/README.md +4 -1
  2. package/bin/cli.mjs +15 -0
  3. package/dist/cli/agents.d.ts.map +1 -1
  4. package/dist/cli/agents.js +19 -5
  5. package/dist/cli/agents.js.map +1 -1
  6. package/dist/cli/artifacts-cli.d.ts +3 -0
  7. package/dist/cli/artifacts-cli.d.ts.map +1 -0
  8. package/dist/cli/artifacts-cli.js +36 -0
  9. package/dist/cli/artifacts-cli.js.map +1 -0
  10. package/dist/cli/baselines.d.ts.map +1 -1
  11. package/dist/cli/baselines.js +11 -0
  12. package/dist/cli/baselines.js.map +1 -1
  13. package/dist/cli/convoy/artifacts.d.ts +25 -0
  14. package/dist/cli/convoy/artifacts.d.ts.map +1 -0
  15. package/dist/cli/convoy/artifacts.js +129 -0
  16. package/dist/cli/convoy/artifacts.js.map +1 -0
  17. package/dist/cli/convoy/artifacts.test.d.ts +2 -0
  18. package/dist/cli/convoy/artifacts.test.d.ts.map +1 -0
  19. package/dist/cli/convoy/artifacts.test.js +169 -0
  20. package/dist/cli/convoy/artifacts.test.js.map +1 -0
  21. package/dist/cli/convoy/compaction.d.ts +23 -0
  22. package/dist/cli/convoy/compaction.d.ts.map +1 -0
  23. package/dist/cli/convoy/compaction.js +117 -0
  24. package/dist/cli/convoy/compaction.js.map +1 -0
  25. package/dist/cli/convoy/compaction.test.d.ts +2 -0
  26. package/dist/cli/convoy/compaction.test.d.ts.map +1 -0
  27. package/dist/cli/convoy/compaction.test.js +205 -0
  28. package/dist/cli/convoy/compaction.test.js.map +1 -0
  29. package/dist/cli/convoy/contracts.d.ts +22 -0
  30. package/dist/cli/convoy/contracts.d.ts.map +1 -0
  31. package/dist/cli/convoy/contracts.js +254 -0
  32. package/dist/cli/convoy/contracts.js.map +1 -0
  33. package/dist/cli/convoy/contracts.test.d.ts +2 -0
  34. package/dist/cli/convoy/contracts.test.d.ts.map +1 -0
  35. package/dist/cli/convoy/contracts.test.js +239 -0
  36. package/dist/cli/convoy/contracts.test.js.map +1 -0
  37. package/dist/cli/convoy/dag-analysis.d.ts +40 -0
  38. package/dist/cli/convoy/dag-analysis.d.ts.map +1 -0
  39. package/dist/cli/convoy/dag-analysis.js +282 -0
  40. package/dist/cli/convoy/dag-analysis.js.map +1 -0
  41. package/dist/cli/convoy/dag-analysis.test.d.ts +2 -0
  42. package/dist/cli/convoy/dag-analysis.test.d.ts.map +1 -0
  43. package/dist/cli/convoy/dag-analysis.test.js +289 -0
  44. package/dist/cli/convoy/dag-analysis.test.js.map +1 -0
  45. package/dist/cli/convoy/effort-scaling.d.ts +20 -0
  46. package/dist/cli/convoy/effort-scaling.d.ts.map +1 -0
  47. package/dist/cli/convoy/effort-scaling.js +82 -0
  48. package/dist/cli/convoy/effort-scaling.js.map +1 -0
  49. package/dist/cli/convoy/effort-scaling.test.d.ts +2 -0
  50. package/dist/cli/convoy/effort-scaling.test.d.ts.map +1 -0
  51. package/dist/cli/convoy/effort-scaling.test.js +120 -0
  52. package/dist/cli/convoy/effort-scaling.test.js.map +1 -0
  53. package/dist/cli/convoy/engine.d.ts.map +1 -1
  54. package/dist/cli/convoy/engine.js +280 -6
  55. package/dist/cli/convoy/engine.js.map +1 -1
  56. package/dist/cli/convoy/engine.test.js +155 -18
  57. package/dist/cli/convoy/engine.test.js.map +1 -1
  58. package/dist/cli/convoy/event-schemas.d.ts.map +1 -1
  59. package/dist/cli/convoy/event-schemas.js +55 -0
  60. package/dist/cli/convoy/event-schemas.js.map +1 -1
  61. package/dist/cli/convoy/isolation.d.ts +27 -0
  62. package/dist/cli/convoy/isolation.d.ts.map +1 -0
  63. package/dist/cli/convoy/isolation.js +120 -0
  64. package/dist/cli/convoy/isolation.js.map +1 -0
  65. package/dist/cli/convoy/isolation.test.d.ts +2 -0
  66. package/dist/cli/convoy/isolation.test.d.ts.map +1 -0
  67. package/dist/cli/convoy/isolation.test.js +105 -0
  68. package/dist/cli/convoy/isolation.test.js.map +1 -0
  69. package/dist/cli/convoy/review-stages.d.ts +9 -0
  70. package/dist/cli/convoy/review-stages.d.ts.map +1 -0
  71. package/dist/cli/convoy/review-stages.js +134 -0
  72. package/dist/cli/convoy/review-stages.js.map +1 -0
  73. package/dist/cli/convoy/review-stages.test.d.ts +2 -0
  74. package/dist/cli/convoy/review-stages.test.d.ts.map +1 -0
  75. package/dist/cli/convoy/review-stages.test.js +197 -0
  76. package/dist/cli/convoy/review-stages.test.js.map +1 -0
  77. package/dist/cli/convoy/skill-refinement.d.ts +39 -0
  78. package/dist/cli/convoy/skill-refinement.d.ts.map +1 -0
  79. package/dist/cli/convoy/skill-refinement.js +239 -0
  80. package/dist/cli/convoy/skill-refinement.js.map +1 -0
  81. package/dist/cli/convoy/skill-refinement.test.d.ts +2 -0
  82. package/dist/cli/convoy/skill-refinement.test.d.ts.map +1 -0
  83. package/dist/cli/convoy/skill-refinement.test.js +230 -0
  84. package/dist/cli/convoy/skill-refinement.test.js.map +1 -0
  85. package/dist/cli/convoy/spec-builder.d.ts +1 -0
  86. package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
  87. package/dist/cli/convoy/spec-builder.js +11 -0
  88. package/dist/cli/convoy/spec-builder.js.map +1 -1
  89. package/dist/cli/convoy/spec-builder.test.js +54 -0
  90. package/dist/cli/convoy/spec-builder.test.js.map +1 -1
  91. package/dist/cli/convoy/store.d.ts +3 -2
  92. package/dist/cli/convoy/store.d.ts.map +1 -1
  93. package/dist/cli/convoy/store.js +20 -2
  94. package/dist/cli/convoy/store.js.map +1 -1
  95. package/dist/cli/convoy/store.test.js +15 -15
  96. package/dist/cli/convoy/store.test.js.map +1 -1
  97. package/dist/cli/convoy/tdd-gate.d.ts +15 -0
  98. package/dist/cli/convoy/tdd-gate.d.ts.map +1 -0
  99. package/dist/cli/convoy/tdd-gate.js +119 -0
  100. package/dist/cli/convoy/tdd-gate.js.map +1 -0
  101. package/dist/cli/convoy/tdd-gate.test.d.ts +2 -0
  102. package/dist/cli/convoy/tdd-gate.test.d.ts.map +1 -0
  103. package/dist/cli/convoy/tdd-gate.test.js +227 -0
  104. package/dist/cli/convoy/tdd-gate.test.js.map +1 -0
  105. package/dist/cli/convoy/types.d.ts +91 -0
  106. package/dist/cli/convoy/types.d.ts.map +1 -1
  107. package/dist/cli/convoy/types.js +8 -0
  108. package/dist/cli/convoy/types.js.map +1 -1
  109. package/dist/cli/insights.d.ts +3 -0
  110. package/dist/cli/insights.d.ts.map +1 -0
  111. package/dist/cli/insights.js +94 -0
  112. package/dist/cli/insights.js.map +1 -0
  113. package/dist/cli/lesson.d.ts.map +1 -1
  114. package/dist/cli/lesson.js +7 -0
  115. package/dist/cli/lesson.js.map +1 -1
  116. package/dist/cli/log.d.ts.map +1 -1
  117. package/dist/cli/log.js +7 -0
  118. package/dist/cli/log.js.map +1 -1
  119. package/dist/cli/package-config.d.ts +12 -0
  120. package/dist/cli/package-config.d.ts.map +1 -0
  121. package/dist/cli/package-config.js +37 -0
  122. package/dist/cli/package-config.js.map +1 -0
  123. package/dist/cli/package.d.ts +23 -0
  124. package/dist/cli/package.d.ts.map +1 -0
  125. package/dist/cli/package.js +285 -0
  126. package/dist/cli/package.js.map +1 -0
  127. package/dist/cli/package.test.d.ts +2 -0
  128. package/dist/cli/package.test.d.ts.map +1 -0
  129. package/dist/cli/package.test.js +236 -0
  130. package/dist/cli/package.test.js.map +1 -0
  131. package/dist/cli/pipeline.d.ts +6 -0
  132. package/dist/cli/pipeline.d.ts.map +1 -1
  133. package/dist/cli/pipeline.js +15 -2
  134. package/dist/cli/pipeline.js.map +1 -1
  135. package/dist/cli/run/schema.d.ts.map +1 -1
  136. package/dist/cli/run/schema.js +32 -0
  137. package/dist/cli/run/schema.js.map +1 -1
  138. package/dist/cli/run/schema.test.js +51 -0
  139. package/dist/cli/run/schema.test.js.map +1 -1
  140. package/dist/cli/skills.d.ts +3 -0
  141. package/dist/cli/skills.d.ts.map +1 -0
  142. package/dist/cli/skills.js +107 -0
  143. package/dist/cli/skills.js.map +1 -0
  144. package/dist/cli/types.d.ts +4 -1
  145. package/dist/cli/types.d.ts.map +1 -1
  146. package/dist/dashboard/scripts/etl.d.ts.map +1 -1
  147. package/dist/dashboard/scripts/etl.js +44 -11
  148. package/dist/dashboard/scripts/etl.js.map +1 -1
  149. package/package.json +2 -1
  150. package/src/cli/agents.ts +20 -5
  151. package/src/cli/artifacts-cli.ts +41 -0
  152. package/src/cli/baselines.ts +12 -0
  153. package/src/cli/convoy/artifacts.test.ts +201 -0
  154. package/src/cli/convoy/artifacts.ts +186 -0
  155. package/src/cli/convoy/compaction.test.ts +245 -0
  156. package/src/cli/convoy/compaction.ts +164 -0
  157. package/src/cli/convoy/contracts.test.ts +279 -0
  158. package/src/cli/convoy/contracts.ts +280 -0
  159. package/src/cli/convoy/dag-analysis.test.ts +349 -0
  160. package/src/cli/convoy/dag-analysis.ts +371 -0
  161. package/src/cli/convoy/effort-scaling.test.ts +140 -0
  162. package/src/cli/convoy/effort-scaling.ts +90 -0
  163. package/src/cli/convoy/engine.test.ts +175 -18
  164. package/src/cli/convoy/engine.ts +301 -7
  165. package/src/cli/convoy/event-schemas.ts +55 -0
  166. package/src/cli/convoy/isolation.test.ts +137 -0
  167. package/src/cli/convoy/isolation.ts +165 -0
  168. package/src/cli/convoy/review-stages.test.ts +235 -0
  169. package/src/cli/convoy/review-stages.ts +166 -0
  170. package/src/cli/convoy/skill-refinement.test.ts +277 -0
  171. package/src/cli/convoy/skill-refinement.ts +306 -0
  172. package/src/cli/convoy/spec-builder.test.ts +61 -0
  173. package/src/cli/convoy/spec-builder.ts +9 -0
  174. package/src/cli/convoy/store.test.ts +15 -15
  175. package/src/cli/convoy/store.ts +26 -4
  176. package/src/cli/convoy/tdd-gate.test.ts +281 -0
  177. package/src/cli/convoy/tdd-gate.ts +154 -0
  178. package/src/cli/convoy/types.ts +51 -0
  179. package/src/cli/insights.ts +99 -0
  180. package/src/cli/lesson.ts +8 -0
  181. package/src/cli/log.ts +8 -0
  182. package/src/cli/package-config.ts +48 -0
  183. package/src/cli/package.test.ts +276 -0
  184. package/src/cli/package.ts +329 -0
  185. package/src/cli/pipeline.ts +21 -2
  186. package/src/cli/run/schema.test.ts +58 -0
  187. package/src/cli/run/schema.ts +33 -0
  188. package/src/cli/skills.ts +121 -0
  189. package/src/cli/types.ts +4 -1
  190. package/src/dashboard/dist/_astro/index.D6quLrA6.css +1 -0
  191. package/src/dashboard/dist/data/convoy-list.json +21 -7
  192. package/src/dashboard/dist/data/convoys/demo-api-v2.json +3 -3
  193. package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +5 -5
  194. package/src/dashboard/dist/data/convoys/demo-convoy-1.json +2 -2
  195. package/src/dashboard/dist/data/convoys/demo-convoy-2.json +1 -1
  196. package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +7 -7
  197. package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +3 -3
  198. package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +2 -2
  199. package/src/dashboard/dist/data/convoys/demo-docs-update.json +2 -2
  200. package/src/dashboard/dist/data/convoys/demo-perf-opt.json +4 -4
  201. package/src/dashboard/dist/index.html +306 -33
  202. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  203. package/src/dashboard/public/data/convoy-list.json +21 -7
  204. package/src/dashboard/public/data/convoys/demo-api-v2.json +3 -3
  205. package/src/dashboard/public/data/convoys/demo-auth-revamp.json +5 -5
  206. package/src/dashboard/public/data/convoys/demo-convoy-1.json +2 -2
  207. package/src/dashboard/public/data/convoys/demo-convoy-2.json +1 -1
  208. package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +7 -7
  209. package/src/dashboard/public/data/convoys/demo-data-pipeline.json +3 -3
  210. package/src/dashboard/public/data/convoys/demo-deploy-ci.json +2 -2
  211. package/src/dashboard/public/data/convoys/demo-docs-update.json +2 -2
  212. package/src/dashboard/public/data/convoys/demo-perf-opt.json +4 -4
  213. package/src/dashboard/scripts/etl.test.ts +14 -0
  214. package/src/dashboard/scripts/etl.ts +48 -16
  215. package/src/dashboard/scripts/generate-demo-db.ts +18 -10
  216. package/src/dashboard/src/pages/index.astro +348 -45
  217. package/src/dashboard/src/styles/dashboard.css +56 -0
  218. package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
  219. package/src/orchestrator/prompts/generate-convoy.prompt.md +19 -0
  220. package/src/dashboard/dist/_astro/index.BRDFmNzR.css +0 -1
@@ -22,7 +22,7 @@ import { createEventEmitter, ndjsonPathForConvoy, recoverNdjson, type ConvoyEven
22
22
  import { createWorktreeManager, type WorktreeManager } from './worktree.js'
23
23
  import { createMergeQueue, MergeConflictError, type MergeQueue } from './merge.js'
24
24
  import { createHealthMonitor, detectDrift } from './health.js'
25
- import type { TaskRecord, ConvoyStatus, ConvoyTaskStatus, GuardConfig, CircuitBreakerConfig, TaskStep, Hook, TaskOutput, TaskInput } from './types.js'
25
+ import type { TaskRecord, ConvoyStatus, ConvoyTaskStatus, GuardConfig, CircuitBreakerConfig, TaskStep, Hook, TaskOutput, TaskInput, TDDGateConfig } from './types.js'
26
26
  import { buildPhases, formatDuration } from '../run/executor.js'
27
27
  import { parseTimeout, parseYaml } from '../run/schema.js'
28
28
  import { getAdapter, detectAdapter } from '../run/adapters/index.js'
@@ -33,6 +33,13 @@ import { readLessons, captureLessons, consolidateLessons } from './lessons.js'
33
33
  import { updateExpertise, feedCircuitBreaker } from './expertise.js'
34
34
  import { buildKnowledgeGraph } from './knowledge.js'
35
35
  import { injectDiscoveredIssuesInstruction, checkDiscoveredIssues, consolidateIssues } from './issues.js'
36
+ import { validateOutput, buildContractInstruction, buildContractRetryPrompt } from './contracts.js'
37
+ import { runTwoStageReview } from './review-stages.js'
38
+ import { buildIsolationPreamble, resolveDependencyResults, detectPartitionViolations } from './isolation.js'
39
+ import { checkTDD, formatTDDFailure, DEFAULT_TDD_CONFIG } from './tdd-gate.js'
40
+ import { runSkillRefinementCheck } from './skill-refinement.js'
41
+ import { getArtifactDir, extractArtifactRefs } from './artifacts.js'
42
+ import { shouldCompact, parseCompactionSummary, saveCompaction, canCompact, getMaxCompactions, generateCompactionPrompt, buildContinuationPrompt } from './compaction.js'
36
43
 
37
44
  const execFile = promisify(execFileCb)
38
45
 
@@ -823,6 +830,7 @@ function pollInjectFile(
823
830
  dispute_id: null,
824
831
  drift_score: null,
825
832
  drift_retried: 0,
833
+ compaction_count: 0,
826
834
  outputs: null,
827
835
  inputs: null,
828
836
  discovered_issues: null,
@@ -1260,6 +1268,36 @@ async function runConvoy(
1260
1268
  const steps: TaskStep[] | undefined = specTask?.steps
1261
1269
  const taskHooks: Hook[] = specTask?.hooks ?? []
1262
1270
 
1271
+ // ── Context isolation preamble (Phase 41) ────────────────────────────
1272
+ try {
1273
+ const taskFiles = taskRecord.files ? JSON.parse(taskRecord.files) as string[] : []
1274
+ const depIds = taskRecord.depends_on ? JSON.parse(taskRecord.depends_on) as string[] : []
1275
+ const depResults = resolveDependencyResults(store, convoyId, depIds)
1276
+ const preamble = buildIsolationPreamble(
1277
+ { id: taskRecord.id, description: taskRecord.prompt.slice(0, 200), prompt: taskRecord.prompt, files: taskFiles, agent: taskRecord.agent },
1278
+ depResults,
1279
+ )
1280
+ task.prompt = preamble + '\n\n' + task.prompt
1281
+ } catch { /* non-critical — isolation preamble is best-effort */ }
1282
+
1283
+ // ── Artifact output instructions (Phase 43) ────────────────────────────
1284
+ try {
1285
+ const artifactDir = getArtifactDir(convoyId, taskRecord.id)
1286
+ const artifactInstructions = [
1287
+ '',
1288
+ '## Artifact Output (for large results)',
1289
+ 'If your output includes large content (>100 lines of code, full reports, data dumps),',
1290
+ 'write it to an artifact file instead of including it inline:',
1291
+ '',
1292
+ '1. Write the content to: ' + artifactDir + '{filename}',
1293
+ '2. In your response, reference it: `[ARTIFACT: {filename}] {1-line summary}`',
1294
+ '3. Keep your inline response focused on the summary and key decisions.',
1295
+ '',
1296
+ 'Small outputs (< 100 lines) can remain inline.',
1297
+ ].join('\n')
1298
+ task.prompt = task.prompt + '\n' + artifactInstructions
1299
+ } catch { /* non-critical */ }
1300
+
1263
1301
  // ── Intelligence: inject lessons (Phase 18.1) ─────────────────────────
1264
1302
  if (spec.defaults?.inject_lessons !== false) {
1265
1303
  try {
@@ -1292,6 +1330,12 @@ async function runConvoy(
1292
1330
  task.prompt = injectDiscoveredIssuesInstruction(task.prompt)
1293
1331
  }
1294
1332
 
1333
+ // ── Output contract injection ─────────────────────────────────────────
1334
+ const contractInstruction = buildContractInstruction(taskRecord.agent)
1335
+ if (contractInstruction) {
1336
+ task.prompt = task.prompt + '\n\n' + contractInstruction
1337
+ }
1338
+
1295
1339
  // ── pre_task hooks ────────────────────────────────────────────────────────
1296
1340
  if (taskHooks.length > 0) {
1297
1341
  const preResult = await runHooks(taskHooks, 'pre_task', {
@@ -1683,6 +1727,100 @@ async function runConvoy(
1683
1727
  return
1684
1728
  }
1685
1729
  }
1730
+
1731
+ // ── Partition violation check (Phase 41) ────────────────────────────
1732
+ if (changedFiles.length > 0) {
1733
+ try {
1734
+ const taskFiles = taskRecord.files ? JSON.parse(taskRecord.files) as string[] : []
1735
+ if (taskFiles.length > 0) {
1736
+ const violation = detectPartitionViolations(taskRecord.id, taskFiles, changedFiles)
1737
+ if (violation) {
1738
+ events.emit('partition_violation', {
1739
+ task_id: taskRecord.id,
1740
+ allowed: violation.allowedFiles,
1741
+ actual: violation.actualFiles,
1742
+ violations: violation.violations,
1743
+ }, { convoy_id: convoyId, task_id: taskRecord.id })
1744
+ process.stdout.write(` ${c.yellow('⚠')} ${c.bold(`[${taskRecord.id}]`)} partition violation: ${violation.violations.join(', ')}\n`)
1745
+ }
1746
+ }
1747
+ } catch { /* non-critical */ }
1748
+ }
1749
+
1750
+ // ── TDD gate ──────────────────────────────────────────────────────────
1751
+ if (builtInGates.tdd_check && changedFiles.length > 0) {
1752
+ const tddConfig: TDDGateConfig = typeof builtInGates.tdd_check === 'object'
1753
+ ? { ...DEFAULT_TDD_CONFIG, ...builtInGates.tdd_check }
1754
+ : DEFAULT_TDD_CONFIG
1755
+ const specTaskForTDD = (spec.tasks ?? []).find(t => t.id === taskRecord.id)
1756
+ const tddResult = checkTDD(changedFiles, changedFiles, tddConfig, specTaskForTDD?.agent ?? taskRecord.agent)
1757
+
1758
+ if (tddResult.skipped) {
1759
+ events.emit('tdd_check_skipped', {
1760
+ task_id: taskRecord.id,
1761
+ reason: tddResult.skip_reason,
1762
+ agent: specTaskForTDD?.agent ?? taskRecord.agent,
1763
+ }, { convoy_id: convoyId, task_id: taskRecord.id })
1764
+ } else if (tddResult.passed) {
1765
+ events.emit('tdd_check_passed', {
1766
+ task_id: taskRecord.id,
1767
+ new_source_files: tddResult.new_source_files.length,
1768
+ existing_test_files: tddResult.existing_test_files.length,
1769
+ }, { convoy_id: convoyId, task_id: taskRecord.id })
1770
+ } else {
1771
+ const failureMsg = formatTDDFailure(tddResult)
1772
+ events.emit('tdd_check_failed', {
1773
+ task_id: taskRecord.id,
1774
+ missing_test_files: tddResult.missing_test_files,
1775
+ new_source_files: tddResult.new_source_files.length,
1776
+ }, { convoy_id: convoyId, task_id: taskRecord.id })
1777
+
1778
+ if (tddConfig.mode === 'block') {
1779
+ await removeWorktree()
1780
+ const freshRecord = store.getTask(taskRecord.id, convoyId)!
1781
+ if (freshRecord.retries < freshRecord.max_retries && spec.on_failure !== 'stop') {
1782
+ store.updateTaskStatus(taskRecord.id, convoyId, 'pending', {
1783
+ retries: freshRecord.retries + 1,
1784
+ worker_id: null,
1785
+ worktree: null,
1786
+ started_at: null,
1787
+ finished_at: null,
1788
+ prompt: `TDD gate failed.\n${failureMsg}\n\nCreate the missing test files and try again.\n\n${taskRecord.prompt}`,
1789
+ })
1790
+ store.updateWorkerStatus(workerId, 'failed', { finished_at: finishedAt })
1791
+ process.stdout.write(
1792
+ ` ${c.yellow('⟳')} ${c.bold(`[${taskRecord.id}]`)} TDD gate failed, retry ${freshRecord.retries + 1}/${freshRecord.max_retries}\n`,
1793
+ )
1794
+ } else {
1795
+ store.withTransaction(() => {
1796
+ store.updateTaskStatus(taskRecord.id, convoyId, 'gate-failed', {
1797
+ finished_at: finishedAt,
1798
+ output: `Built-in gate (tdd_check) failed:\n${failureMsg}`,
1799
+ exit_code: 1,
1800
+ })
1801
+ store.updateWorkerStatus(workerId, 'failed', { finished_at: finishedAt })
1802
+ })
1803
+ completedCount++
1804
+ process.stdout.write(
1805
+ ` ${c.red('✗')} ${c.bold(`[${taskRecord.id}]`)} TDD gate failed ${elapsed} ${c.dim(`[${completedCount}/${totalTasks}]`)}\n`,
1806
+ )
1807
+ events.emit(
1808
+ 'task_failed',
1809
+ { reason: 'gate-failed', gate: 'tdd_check', worker_id: workerId },
1810
+ { convoy_id: convoyId, task_id: taskRecord.id, worker_id: workerId },
1811
+ )
1812
+ handleExhaustion(freshRecord, 'tdd-check', failureMsg)
1813
+ }
1814
+ taskAdapterMap.delete(taskRecord.id)
1815
+ return
1816
+ } else {
1817
+ // warn mode — log but continue
1818
+ process.stdout.write(
1819
+ ` ${c.yellow('⚠')} ${c.bold(`[${taskRecord.id}]`)} TDD gate warning: ${tddResult.missing_test_files.length} source file(s) without tests\n`,
1820
+ )
1821
+ }
1822
+ }
1823
+ }
1686
1824
  }
1687
1825
 
1688
1826
  // ── Drift detection ──────────────────────────────────────────────────
@@ -1804,7 +1942,19 @@ async function runConvoy(
1804
1942
  await reviewSemaphore.acquire()
1805
1943
  let reviewResult: ReviewResult
1806
1944
  try {
1807
- if (reviewRunner) {
1945
+ if (reviewRunner && spec.defaults?.review_stages !== false) {
1946
+ // Two-stage review: spec compliance first, then code quality
1947
+ const twoStageResult = await runTwoStageReview(taskRecord, reviewRunner, reviewerModel)
1948
+ for (const stage of twoStageResult.stages) {
1949
+ events.emit('review_stage_completed', { stage: stage.stage, verdict: stage.verdict, tokens: stage.tokens_used, task_id: taskRecord.id, model: reviewerModel }, { convoy_id: convoyId, task_id: taskRecord.id })
1950
+ }
1951
+ reviewResult = {
1952
+ verdict: twoStageResult.overall_verdict,
1953
+ feedback: twoStageResult.stages.flatMap(s => s.issues).join('\n'),
1954
+ tokens: twoStageResult.total_tokens,
1955
+ model: reviewerModel,
1956
+ }
1957
+ } else if (reviewRunner) {
1808
1958
  reviewResult = await reviewRunner(taskRecord, 'fast', reviewerModel)
1809
1959
  } else {
1810
1960
  reviewResult = { verdict: 'pass', feedback: '', tokens: 0, model: reviewerModel }
@@ -1865,11 +2015,32 @@ async function runConvoy(
1865
2015
  try {
1866
2016
  const noopRunner = (_t: TaskRecord, _l: ReviewLevel, m: string) => Promise.resolve({ verdict: 'pass' as const, feedback: '', tokens: 0, model: m })
1867
2017
  const runner = reviewRunner ?? noopRunner
1868
- panelResults = await Promise.all([
1869
- runner(taskRecord, 'panel', reviewerModel),
1870
- runner(taskRecord, 'panel', reviewerModel),
1871
- runner(taskRecord, 'panel', reviewerModel),
1872
- ])
2018
+ const twoStageEnabled = spec.defaults?.review_stages !== false
2019
+ if (twoStageEnabled && reviewRunner) {
2020
+ // Each panel reviewer runs both stages; majority vote on overall_verdict
2021
+ const twoStageResults = await Promise.all([
2022
+ runTwoStageReview(taskRecord, runner, reviewerModel),
2023
+ runTwoStageReview(taskRecord, runner, reviewerModel),
2024
+ runTwoStageReview(taskRecord, runner, reviewerModel),
2025
+ ])
2026
+ for (const tsr of twoStageResults) {
2027
+ for (const stage of tsr.stages) {
2028
+ events.emit('review_stage_completed', { stage: stage.stage, verdict: stage.verdict, tokens: stage.tokens_used, task_id: taskRecord.id, model: reviewerModel }, { convoy_id: convoyId, task_id: taskRecord.id })
2029
+ }
2030
+ }
2031
+ panelResults = twoStageResults.map(tsr => ({
2032
+ verdict: tsr.overall_verdict,
2033
+ feedback: tsr.stages.flatMap(s => s.issues).join('\n'),
2034
+ tokens: tsr.total_tokens,
2035
+ model: reviewerModel,
2036
+ }))
2037
+ } else {
2038
+ panelResults = await Promise.all([
2039
+ runner(taskRecord, 'panel', reviewerModel),
2040
+ runner(taskRecord, 'panel', reviewerModel),
2041
+ runner(taskRecord, 'panel', reviewerModel),
2042
+ ])
2043
+ }
1873
2044
  } finally {
1874
2045
  reviewSemaphore.release()
1875
2046
  }
@@ -2107,6 +2278,7 @@ async function runConvoy(
2107
2278
  dispute_id: null,
2108
2279
  drift_score: null,
2109
2280
  drift_retried: 0,
2281
+ compaction_count: 0,
2110
2282
  outputs: null,
2111
2283
  inputs: null,
2112
2284
  discovered_issues: null,
@@ -2154,6 +2326,72 @@ async function runConvoy(
2154
2326
  if (result.usage.total_tokens != null) usageExtra.total_tokens = result.usage.total_tokens
2155
2327
  }
2156
2328
 
2329
+ // ── Context compaction check (Phase 44) ─────────────────────────────
2330
+ const compactionConfig = spec.defaults?.compaction
2331
+ if (compactionConfig?.enabled && usageExtra.total_tokens != null && taskRecord.model) {
2332
+ if (shouldCompact(usageExtra.total_tokens, taskRecord.model, compactionConfig)) {
2333
+ if (canCompact(taskRecord.compaction_count)) {
2334
+ const newCount = taskRecord.compaction_count + 1
2335
+ store.updateTaskCompaction(taskRecord.id, convoyId, newCount)
2336
+
2337
+ const summaryFromOutput = parseCompactionSummary(result.output, taskRecord.id, convoyId)
2338
+ let summaryPath: string | undefined
2339
+ if (summaryFromOutput) {
2340
+ try {
2341
+ summaryPath = saveCompaction(convoyId, taskRecord.id, summaryFromOutput, newCount)
2342
+ } catch { /* non-critical */ }
2343
+ }
2344
+
2345
+ const compactionTaskFiles = taskRecord.files ? JSON.parse(taskRecord.files) as string[] : []
2346
+ const compactionDepIds = taskRecord.depends_on ? JSON.parse(taskRecord.depends_on) as string[] : []
2347
+ const compactionDepResults = resolveDependencyResults(store, convoyId, compactionDepIds)
2348
+ const compactionPreamble = buildIsolationPreamble(
2349
+ { id: taskRecord.id, description: taskRecord.prompt.slice(0, 200), prompt: taskRecord.prompt, files: compactionTaskFiles, agent: taskRecord.agent },
2350
+ compactionDepResults,
2351
+ )
2352
+
2353
+ const continuationPrompt = summaryPath
2354
+ ? buildContinuationPrompt(taskRecord.prompt, summaryPath, compactionPreamble)
2355
+ : compactionPreamble + '\n\n' + generateCompactionPrompt(taskRecord.id) + '\n\n' + taskRecord.prompt
2356
+
2357
+ store.updateTaskStatus(taskRecord.id, convoyId, 'pending', {
2358
+ worker_id: null,
2359
+ worktree: null,
2360
+ started_at: null,
2361
+ finished_at: null,
2362
+ prompt: continuationPrompt,
2363
+ })
2364
+ store.updateWorkerStatus(workerId, 'failed', { finished_at: finishedAt })
2365
+
2366
+ events.emit('context_compacted', {
2367
+ task_id: taskRecord.id,
2368
+ compaction_count: newCount,
2369
+ summary_path: summaryPath ?? '',
2370
+ model: taskRecord.model,
2371
+ tokens_used: usageExtra.total_tokens,
2372
+ }, { convoy_id: convoyId, task_id: taskRecord.id })
2373
+
2374
+ taskAdapterMap.delete(taskRecord.id)
2375
+ return
2376
+ } else {
2377
+ // Max compactions exceeded — fail the task
2378
+ const exhaustedAt = new Date().toISOString()
2379
+ store.updateTaskStatus(taskRecord.id, convoyId, 'failed', {
2380
+ finished_at: exhaustedAt,
2381
+ output: `Context exhausted: reached maximum ${getMaxCompactions()} compactions`,
2382
+ exit_code: 1,
2383
+ })
2384
+ store.updateWorkerStatus(workerId, 'failed', { finished_at: exhaustedAt })
2385
+ events.emit('task_failed', {
2386
+ reason: 'context_exhausted',
2387
+ worker_id: workerId,
2388
+ }, { convoy_id: convoyId, task_id: taskRecord.id })
2389
+ taskAdapterMap.delete(taskRecord.id)
2390
+ return
2391
+ }
2392
+ }
2393
+ }
2394
+
2157
2395
  // ── Capture outputs as artifacts ────────────────────────────────────────
2158
2396
  if (taskRecord.outputs) {
2159
2397
  const outputs: TaskOutput[] = JSON.parse(taskRecord.outputs)
@@ -2190,6 +2428,48 @@ async function runConvoy(
2190
2428
  }
2191
2429
  }
2192
2430
 
2431
+ // ── Extract filesystem artifacts (Phase 43) ────────────────────────
2432
+ try {
2433
+ const fsArtifactRefs = extractArtifactRefs(taskRecord.id, convoyId, result.output)
2434
+ if (fsArtifactRefs.length > 0) {
2435
+ events.emit('artifacts_extracted', {
2436
+ task_id: taskRecord.id,
2437
+ count: fsArtifactRefs.length,
2438
+ artifacts: fsArtifactRefs.map(r => ({ filename: r.filename, summary: r.summary })),
2439
+ }, { convoy_id: convoyId, task_id: taskRecord.id })
2440
+ }
2441
+ } catch (err) {
2442
+ process.stderr.write(`[artifacts] Warning: extraction failed for task ${taskRecord.id}: ${(err as Error).message}\n`)
2443
+ }
2444
+
2445
+ // ── Output contract validation ────────────────────────────────────────
2446
+ const contractResult = validateOutput(taskRecord.agent, result.output)
2447
+ if (!contractResult.valid) {
2448
+ const freshRecordForContract = store.getTask(taskRecord.id, convoyId)!
2449
+ if (freshRecordForContract.retries < freshRecordForContract.max_retries) {
2450
+ const retryPrefix = buildContractRetryPrompt(contractResult) + '\n\n'
2451
+ store.updateTaskStatus(taskRecord.id, convoyId, 'pending', {
2452
+ retries: freshRecordForContract.retries + 1,
2453
+ worker_id: null,
2454
+ worktree: null,
2455
+ started_at: null,
2456
+ finished_at: null,
2457
+ prompt: retryPrefix + taskRecord.prompt,
2458
+ })
2459
+ store.updateWorkerStatus(workerId, 'failed', { finished_at: finishedAt })
2460
+ process.stdout.write(` ${c.yellow('⟳')} ${c.bold(`[${taskRecord.id}]`)} contract retry ${freshRecordForContract.retries + 1}/${freshRecordForContract.max_retries}\n`)
2461
+ taskAdapterMap.delete(taskRecord.id)
2462
+ return
2463
+ }
2464
+ events.emit('contract_violation', {
2465
+ task_id: taskRecord.id,
2466
+ agent: taskRecord.agent,
2467
+ missing: contractResult.missing,
2468
+ warnings: contractResult.warnings,
2469
+ }, { convoy_id: convoyId, task_id: taskRecord.id })
2470
+ process.stdout.write(` ${c.yellow('⚠')} ${c.bold(`[${taskRecord.id}]`)} contract violation: missing ${contractResult.missing.join(', ')}\n`)
2471
+ }
2472
+
2193
2473
  // ── Intelligence: capture persistent agent identity (Phase 17.2) ─────
2194
2474
  const specTaskForCapture = (spec.tasks ?? []).find(t => t.id === taskRecord.id)
2195
2475
  if (specTaskForCapture?.persistent && result.output) {
@@ -2231,6 +2511,7 @@ async function runConvoy(
2231
2511
  output: result.output,
2232
2512
  exit_code: result.exitCode,
2233
2513
  ...usageExtra,
2514
+ contract_result: JSON.stringify(contractResult),
2234
2515
  })
2235
2516
  store.updateWorkerStatus(workerId, 'done', { finished_at: finishedAt })
2236
2517
  })
@@ -2515,6 +2796,18 @@ async function runConvoy(
2515
2796
  try { consolidateIssues(basePath) } catch { /* non-critical */ }
2516
2797
  }
2517
2798
 
2799
+ // ── Intelligence: skill refinement check ───────────────────────────────
2800
+ try {
2801
+ const proposals = runSkillRefinementCheck(convoyId, basePath)
2802
+ for (const p of proposals) {
2803
+ events.emit('skill_refinement_proposed', {
2804
+ skill_name: p.skill,
2805
+ proposal_path: p.proposalPath,
2806
+ }, { convoy_id: convoyId })
2807
+ process.stdout.write(` ${c.yellow('◆')} Skill refinement proposed for "${p.skill}". Review at ${p.proposalPath}\n`)
2808
+ }
2809
+ } catch { /* non-critical */ }
2810
+
2518
2811
  // ── Final status & summary ────────────────────────────────────────────────
2519
2812
 
2520
2813
  const allTasksFinal = store.getTasksByConvoy(convoyId)
@@ -3000,6 +3293,7 @@ export function createConvoyEngine(options: ConvoyEngineOptions): ConvoyEngine {
3000
3293
  dispute_id: null,
3001
3294
  drift_score: null,
3002
3295
  drift_retried: 0,
3296
+ compaction_count: 0,
3003
3297
  outputs: null,
3004
3298
  inputs: null,
3005
3299
  discovered_issues: null,
@@ -178,6 +178,61 @@ export const EVENT_DATA_SCHEMAS: Record<string, AnySchema> = {
178
178
  description: v.optional(v.string()),
179
179
  severity: v.optional(v.string()),
180
180
  }),
181
+ contract_violation: v.looseObject({
182
+ task_id: v.optional(v.string()),
183
+ agent: v.optional(v.string()),
184
+ missing: v.optional(v.array(v.string())),
185
+ warnings: v.optional(v.array(v.string())),
186
+ }),
187
+ partition_violation: v.looseObject({
188
+ task_id: v.optional(v.string()),
189
+ allowed: v.optional(v.array(v.string())),
190
+ actual: v.optional(v.array(v.string())),
191
+ violations: v.optional(v.array(v.string())),
192
+ }),
193
+ context_compacted: v.looseObject({
194
+ task_id: v.optional(v.string()),
195
+ compaction_count: v.optional(v.number()),
196
+ summary_path: v.optional(v.string()),
197
+ model: v.optional(v.string()),
198
+ tokens_used: v.optional(v.number()),
199
+ }),
200
+ skill_refinement_proposed: v.looseObject({
201
+ skill_name: v.optional(v.string()),
202
+ proposal_path: v.optional(v.string()),
203
+ failure_count: v.optional(v.number()),
204
+ confidence: v.optional(v.string()),
205
+ }),
206
+ tdd_check_passed: v.looseObject({
207
+ task_id: v.optional(v.string()),
208
+ new_source_files: v.optional(v.number()),
209
+ existing_test_files: v.optional(v.number()),
210
+ }),
211
+ tdd_check_failed: v.looseObject({
212
+ task_id: v.optional(v.string()),
213
+ missing_test_files: v.optional(v.array(v.string())),
214
+ new_source_files: v.optional(v.number()),
215
+ }),
216
+ tdd_check_skipped: v.looseObject({
217
+ task_id: v.optional(v.string()),
218
+ reason: v.optional(v.string()),
219
+ agent: v.optional(v.string()),
220
+ }),
221
+ review_stage_completed: v.looseObject({
222
+ stage: v.string(),
223
+ verdict: v.string(),
224
+ tokens: v.number(),
225
+ task_id: v.optional(v.string()),
226
+ model: v.optional(v.string()),
227
+ }),
228
+ artifacts_extracted: v.looseObject({
229
+ task_id: v.optional(v.string()),
230
+ count: v.optional(v.number()),
231
+ artifacts: v.optional(v.array(v.looseObject({
232
+ filename: v.string(),
233
+ summary: v.optional(v.string()),
234
+ }))),
235
+ }),
181
236
  }
182
237
  export function validateEventData(
183
238
  type: string,
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ buildIsolationPreamble,
4
+ formatDependencyResults,
5
+ detectPartitionViolations,
6
+ type DependencyResult,
7
+ } from './isolation.js'
8
+
9
+ describe('buildIsolationPreamble', () => {
10
+ const baseTask = {
11
+ id: 'task-1',
12
+ description: 'Implement the auth service',
13
+ prompt: 'Please implement the auth service with JWT tokens',
14
+ files: ['src/auth/', 'src/services/auth.ts'],
15
+ agent: 'developer',
16
+ }
17
+
18
+ it('with no dependencies contains task ID, agent, description, file list, and no-dependency text', () => {
19
+ const result = buildIsolationPreamble(baseTask, [])
20
+ expect(result).toContain('task-1')
21
+ expect(result).toContain('developer')
22
+ expect(result).toContain('Implement the auth service')
23
+ expect(result).toContain('src/auth/')
24
+ expect(result).toContain('src/services/auth.ts')
25
+ expect(result).toContain('No dependencies')
26
+ expect(result).toContain('first phase')
27
+ })
28
+
29
+ it('with 2 completed dependencies includes dependency summaries and files', () => {
30
+ const depResults: DependencyResult[] = [
31
+ { taskId: 'task-0', agent: 'architect', status: 'done', summary: 'Designed the auth schema', filesChanged: ['schema.ts', 'types.ts'] },
32
+ { taskId: 'task-0b', agent: 'developer', status: 'done', summary: 'Set up project structure', filesChanged: ['package.json'] },
33
+ ]
34
+ const result = buildIsolationPreamble(baseTask, depResults)
35
+ expect(result).toContain('task-0')
36
+ expect(result).toContain('Designed the auth schema')
37
+ expect(result).toContain('schema.ts, types.ts')
38
+ expect(result).toContain('task-0b')
39
+ expect(result).toContain('package.json')
40
+ expect(result).not.toContain('No dependencies')
41
+ })
42
+
43
+ it('with failed dependency includes failure status', () => {
44
+ const depResults: DependencyResult[] = [
45
+ { taskId: 'task-x', agent: 'developer', status: 'failed', summary: 'Build failed due to type errors', filesChanged: [] },
46
+ ]
47
+ const result = buildIsolationPreamble(baseTask, depResults)
48
+ expect(result).toContain('failed')
49
+ expect(result).toContain('Build failed due to type errors')
50
+ })
51
+
52
+ it('uses prompt slice when no description', () => {
53
+ const longPrompt = 'A'.repeat(300)
54
+ const task = { ...baseTask, description: '', prompt: longPrompt }
55
+ const result = buildIsolationPreamble(task, [])
56
+ expect(result).toContain('A'.repeat(200))
57
+ expect(result).not.toContain('A'.repeat(201))
58
+ })
59
+ })
60
+
61
+ describe('formatDependencyResults', () => {
62
+ it('compact format includes summary and filesChanged but not full output', () => {
63
+ const deps: DependencyResult[] = [
64
+ {
65
+ taskId: 'dep-1',
66
+ agent: 'developer',
67
+ status: 'done',
68
+ summary: 'Completed auth setup',
69
+ filesChanged: ['src/auth.ts', 'src/index.ts'],
70
+ },
71
+ ]
72
+ const result = formatDependencyResults(deps)
73
+ expect(result).toContain('dep-1')
74
+ expect(result).toContain('developer')
75
+ expect(result).toContain('done')
76
+ expect(result).toContain('Completed auth setup')
77
+ expect(result).toContain('src/auth.ts, src/index.ts')
78
+ })
79
+
80
+ it('shows no-summary placeholder when summary is null', () => {
81
+ const deps: DependencyResult[] = [
82
+ { taskId: 'dep-2', agent: 'architect', status: 'done', summary: null, filesChanged: [] },
83
+ ]
84
+ const result = formatDependencyResults(deps)
85
+ expect(result).toContain('No summary available.')
86
+ expect(result).toContain('Files changed: none')
87
+ })
88
+ })
89
+
90
+ describe('detectPartitionViolations', () => {
91
+ it('returns null when all files are within partition', () => {
92
+ const result = detectPartitionViolations(
93
+ 'task-1',
94
+ ['src/auth/', 'src/types.ts'],
95
+ ['src/auth/service.ts', 'src/auth/utils.ts', 'src/types.ts'],
96
+ )
97
+ expect(result).toBeNull()
98
+ })
99
+
100
+ it('detects files outside partition', () => {
101
+ const result = detectPartitionViolations(
102
+ 'task-1',
103
+ ['src/auth/'],
104
+ ['src/auth/service.ts', 'src/other/unrelated.ts'],
105
+ )
106
+ expect(result).not.toBeNull()
107
+ expect(result!.violations).toContain('src/other/unrelated.ts')
108
+ expect(result!.violations).not.toContain('src/auth/service.ts')
109
+ expect(result!.taskId).toBe('task-1')
110
+ expect(result!.allowedFiles).toEqual(['src/auth/'])
111
+ })
112
+
113
+ it('handles directory paths - src/auth/ allows src/auth/service.ts', () => {
114
+ const result = detectPartitionViolations(
115
+ 'task-1',
116
+ ['src/auth/'],
117
+ ['src/auth/service.ts', 'src/auth/utils/helper.ts'],
118
+ )
119
+ expect(result).toBeNull()
120
+ })
121
+
122
+ it('handles exact file matches - src/index.ts allows only that exact file', () => {
123
+ const result = detectPartitionViolations(
124
+ 'task-1',
125
+ ['src/index.ts'],
126
+ ['src/index.ts', 'src/other.ts'],
127
+ )
128
+ expect(result).not.toBeNull()
129
+ expect(result!.violations).toContain('src/other.ts')
130
+ expect(result!.violations).not.toContain('src/index.ts')
131
+ })
132
+
133
+ it('returns null for empty actualFiles', () => {
134
+ const result = detectPartitionViolations('task-1', ['src/auth/'], [])
135
+ expect(result).toBeNull()
136
+ })
137
+ })