opencastle 0.31.6 → 0.32.0

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 (210) hide show
  1. package/LICENSE +93 -21
  2. package/README.md +9 -3
  3. package/bin/cli.mjs +15 -0
  4. package/dist/cli/agents.d.ts.map +1 -1
  5. package/dist/cli/agents.js +19 -5
  6. package/dist/cli/agents.js.map +1 -1
  7. package/dist/cli/artifacts-cli.d.ts +3 -0
  8. package/dist/cli/artifacts-cli.d.ts.map +1 -0
  9. package/dist/cli/artifacts-cli.js +36 -0
  10. package/dist/cli/artifacts-cli.js.map +1 -0
  11. package/dist/cli/baselines.d.ts.map +1 -1
  12. package/dist/cli/baselines.js +11 -0
  13. package/dist/cli/baselines.js.map +1 -1
  14. package/dist/cli/convoy/artifacts.d.ts +25 -0
  15. package/dist/cli/convoy/artifacts.d.ts.map +1 -0
  16. package/dist/cli/convoy/artifacts.js +129 -0
  17. package/dist/cli/convoy/artifacts.js.map +1 -0
  18. package/dist/cli/convoy/artifacts.test.d.ts +2 -0
  19. package/dist/cli/convoy/artifacts.test.d.ts.map +1 -0
  20. package/dist/cli/convoy/artifacts.test.js +169 -0
  21. package/dist/cli/convoy/artifacts.test.js.map +1 -0
  22. package/dist/cli/convoy/compaction.d.ts +23 -0
  23. package/dist/cli/convoy/compaction.d.ts.map +1 -0
  24. package/dist/cli/convoy/compaction.js +117 -0
  25. package/dist/cli/convoy/compaction.js.map +1 -0
  26. package/dist/cli/convoy/compaction.test.d.ts +2 -0
  27. package/dist/cli/convoy/compaction.test.d.ts.map +1 -0
  28. package/dist/cli/convoy/compaction.test.js +205 -0
  29. package/dist/cli/convoy/compaction.test.js.map +1 -0
  30. package/dist/cli/convoy/contracts.d.ts +22 -0
  31. package/dist/cli/convoy/contracts.d.ts.map +1 -0
  32. package/dist/cli/convoy/contracts.js +254 -0
  33. package/dist/cli/convoy/contracts.js.map +1 -0
  34. package/dist/cli/convoy/contracts.test.d.ts +2 -0
  35. package/dist/cli/convoy/contracts.test.d.ts.map +1 -0
  36. package/dist/cli/convoy/contracts.test.js +239 -0
  37. package/dist/cli/convoy/contracts.test.js.map +1 -0
  38. package/dist/cli/convoy/dag-analysis.d.ts +40 -0
  39. package/dist/cli/convoy/dag-analysis.d.ts.map +1 -0
  40. package/dist/cli/convoy/dag-analysis.js +282 -0
  41. package/dist/cli/convoy/dag-analysis.js.map +1 -0
  42. package/dist/cli/convoy/dag-analysis.test.d.ts +2 -0
  43. package/dist/cli/convoy/dag-analysis.test.d.ts.map +1 -0
  44. package/dist/cli/convoy/dag-analysis.test.js +289 -0
  45. package/dist/cli/convoy/dag-analysis.test.js.map +1 -0
  46. package/dist/cli/convoy/effort-scaling.d.ts +20 -0
  47. package/dist/cli/convoy/effort-scaling.d.ts.map +1 -0
  48. package/dist/cli/convoy/effort-scaling.js +82 -0
  49. package/dist/cli/convoy/effort-scaling.js.map +1 -0
  50. package/dist/cli/convoy/effort-scaling.test.d.ts +2 -0
  51. package/dist/cli/convoy/effort-scaling.test.d.ts.map +1 -0
  52. package/dist/cli/convoy/effort-scaling.test.js +120 -0
  53. package/dist/cli/convoy/effort-scaling.test.js.map +1 -0
  54. package/dist/cli/convoy/engine.d.ts.map +1 -1
  55. package/dist/cli/convoy/engine.js +298 -11
  56. package/dist/cli/convoy/engine.js.map +1 -1
  57. package/dist/cli/convoy/engine.test.js +155 -18
  58. package/dist/cli/convoy/engine.test.js.map +1 -1
  59. package/dist/cli/convoy/event-schemas.d.ts.map +1 -1
  60. package/dist/cli/convoy/event-schemas.js +55 -0
  61. package/dist/cli/convoy/event-schemas.js.map +1 -1
  62. package/dist/cli/convoy/isolation.d.ts +27 -0
  63. package/dist/cli/convoy/isolation.d.ts.map +1 -0
  64. package/dist/cli/convoy/isolation.js +120 -0
  65. package/dist/cli/convoy/isolation.js.map +1 -0
  66. package/dist/cli/convoy/isolation.test.d.ts +2 -0
  67. package/dist/cli/convoy/isolation.test.d.ts.map +1 -0
  68. package/dist/cli/convoy/isolation.test.js +105 -0
  69. package/dist/cli/convoy/isolation.test.js.map +1 -0
  70. package/dist/cli/convoy/review-stages.d.ts +9 -0
  71. package/dist/cli/convoy/review-stages.d.ts.map +1 -0
  72. package/dist/cli/convoy/review-stages.js +134 -0
  73. package/dist/cli/convoy/review-stages.js.map +1 -0
  74. package/dist/cli/convoy/review-stages.test.d.ts +2 -0
  75. package/dist/cli/convoy/review-stages.test.d.ts.map +1 -0
  76. package/dist/cli/convoy/review-stages.test.js +197 -0
  77. package/dist/cli/convoy/review-stages.test.js.map +1 -0
  78. package/dist/cli/convoy/skill-refinement.d.ts +39 -0
  79. package/dist/cli/convoy/skill-refinement.d.ts.map +1 -0
  80. package/dist/cli/convoy/skill-refinement.js +239 -0
  81. package/dist/cli/convoy/skill-refinement.js.map +1 -0
  82. package/dist/cli/convoy/skill-refinement.test.d.ts +2 -0
  83. package/dist/cli/convoy/skill-refinement.test.d.ts.map +1 -0
  84. package/dist/cli/convoy/skill-refinement.test.js +230 -0
  85. package/dist/cli/convoy/skill-refinement.test.js.map +1 -0
  86. package/dist/cli/convoy/spec-builder.d.ts +1 -0
  87. package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
  88. package/dist/cli/convoy/spec-builder.js +11 -0
  89. package/dist/cli/convoy/spec-builder.js.map +1 -1
  90. package/dist/cli/convoy/spec-builder.test.js +54 -0
  91. package/dist/cli/convoy/spec-builder.test.js.map +1 -1
  92. package/dist/cli/convoy/store.d.ts +3 -2
  93. package/dist/cli/convoy/store.d.ts.map +1 -1
  94. package/dist/cli/convoy/store.js +20 -2
  95. package/dist/cli/convoy/store.js.map +1 -1
  96. package/dist/cli/convoy/store.test.js +15 -15
  97. package/dist/cli/convoy/store.test.js.map +1 -1
  98. package/dist/cli/convoy/tdd-gate.d.ts +15 -0
  99. package/dist/cli/convoy/tdd-gate.d.ts.map +1 -0
  100. package/dist/cli/convoy/tdd-gate.js +119 -0
  101. package/dist/cli/convoy/tdd-gate.js.map +1 -0
  102. package/dist/cli/convoy/tdd-gate.test.d.ts +2 -0
  103. package/dist/cli/convoy/tdd-gate.test.d.ts.map +1 -0
  104. package/dist/cli/convoy/tdd-gate.test.js +227 -0
  105. package/dist/cli/convoy/tdd-gate.test.js.map +1 -0
  106. package/dist/cli/convoy/types.d.ts +91 -0
  107. package/dist/cli/convoy/types.d.ts.map +1 -1
  108. package/dist/cli/convoy/types.js +8 -0
  109. package/dist/cli/convoy/types.js.map +1 -1
  110. package/dist/cli/dashboard.d.ts.map +1 -1
  111. package/dist/cli/dashboard.js +54 -0
  112. package/dist/cli/dashboard.js.map +1 -1
  113. package/dist/cli/insights.d.ts +3 -0
  114. package/dist/cli/insights.d.ts.map +1 -0
  115. package/dist/cli/insights.js +94 -0
  116. package/dist/cli/insights.js.map +1 -0
  117. package/dist/cli/lesson.d.ts.map +1 -1
  118. package/dist/cli/lesson.js +7 -0
  119. package/dist/cli/lesson.js.map +1 -1
  120. package/dist/cli/log.d.ts.map +1 -1
  121. package/dist/cli/log.js +7 -0
  122. package/dist/cli/log.js.map +1 -1
  123. package/dist/cli/package-config.d.ts +12 -0
  124. package/dist/cli/package-config.d.ts.map +1 -0
  125. package/dist/cli/package-config.js +37 -0
  126. package/dist/cli/package-config.js.map +1 -0
  127. package/dist/cli/package.d.ts +23 -0
  128. package/dist/cli/package.d.ts.map +1 -0
  129. package/dist/cli/package.js +285 -0
  130. package/dist/cli/package.js.map +1 -0
  131. package/dist/cli/package.test.d.ts +2 -0
  132. package/dist/cli/package.test.d.ts.map +1 -0
  133. package/dist/cli/package.test.js +236 -0
  134. package/dist/cli/package.test.js.map +1 -0
  135. package/dist/cli/pipeline.d.ts +6 -0
  136. package/dist/cli/pipeline.d.ts.map +1 -1
  137. package/dist/cli/pipeline.js +15 -2
  138. package/dist/cli/pipeline.js.map +1 -1
  139. package/dist/cli/run/schema.d.ts.map +1 -1
  140. package/dist/cli/run/schema.js +32 -0
  141. package/dist/cli/run/schema.js.map +1 -1
  142. package/dist/cli/run/schema.test.js +51 -0
  143. package/dist/cli/run/schema.test.js.map +1 -1
  144. package/dist/cli/run.d.ts.map +1 -1
  145. package/dist/cli/run.js +10 -1
  146. package/dist/cli/run.js.map +1 -1
  147. package/dist/cli/skills.d.ts +3 -0
  148. package/dist/cli/skills.d.ts.map +1 -0
  149. package/dist/cli/skills.js +107 -0
  150. package/dist/cli/skills.js.map +1 -0
  151. package/dist/cli/types.d.ts +4 -1
  152. package/dist/cli/types.d.ts.map +1 -1
  153. package/dist/cli/update.js +2 -2
  154. package/package.json +3 -2
  155. package/src/cli/agents.ts +20 -5
  156. package/src/cli/artifacts-cli.ts +41 -0
  157. package/src/cli/baselines.ts +12 -0
  158. package/src/cli/convoy/artifacts.test.ts +201 -0
  159. package/src/cli/convoy/artifacts.ts +186 -0
  160. package/src/cli/convoy/compaction.test.ts +245 -0
  161. package/src/cli/convoy/compaction.ts +164 -0
  162. package/src/cli/convoy/contracts.test.ts +279 -0
  163. package/src/cli/convoy/contracts.ts +280 -0
  164. package/src/cli/convoy/dag-analysis.test.ts +349 -0
  165. package/src/cli/convoy/dag-analysis.ts +371 -0
  166. package/src/cli/convoy/effort-scaling.test.ts +140 -0
  167. package/src/cli/convoy/effort-scaling.ts +90 -0
  168. package/src/cli/convoy/engine.test.ts +175 -18
  169. package/src/cli/convoy/engine.ts +315 -12
  170. package/src/cli/convoy/event-schemas.ts +55 -0
  171. package/src/cli/convoy/isolation.test.ts +137 -0
  172. package/src/cli/convoy/isolation.ts +165 -0
  173. package/src/cli/convoy/review-stages.test.ts +235 -0
  174. package/src/cli/convoy/review-stages.ts +166 -0
  175. package/src/cli/convoy/skill-refinement.test.ts +277 -0
  176. package/src/cli/convoy/skill-refinement.ts +306 -0
  177. package/src/cli/convoy/spec-builder.test.ts +61 -0
  178. package/src/cli/convoy/spec-builder.ts +9 -0
  179. package/src/cli/convoy/store.test.ts +15 -15
  180. package/src/cli/convoy/store.ts +26 -4
  181. package/src/cli/convoy/tdd-gate.test.ts +281 -0
  182. package/src/cli/convoy/tdd-gate.ts +154 -0
  183. package/src/cli/convoy/types.ts +51 -0
  184. package/src/cli/dashboard.ts +55 -0
  185. package/src/cli/insights.ts +99 -0
  186. package/src/cli/lesson.ts +8 -0
  187. package/src/cli/log.ts +8 -0
  188. package/src/cli/package-config.ts +48 -0
  189. package/src/cli/package.test.ts +276 -0
  190. package/src/cli/package.ts +329 -0
  191. package/src/cli/pipeline.ts +21 -2
  192. package/src/cli/run/schema.test.ts +58 -0
  193. package/src/cli/run/schema.ts +33 -0
  194. package/src/cli/run.ts +14 -1
  195. package/src/cli/skills.ts +121 -0
  196. package/src/cli/types.ts +4 -1
  197. package/src/cli/update.ts +2 -2
  198. package/src/dashboard/dist/_astro/{index.Je1YjU_y.css → index.BRDFmNzR.css} +1 -1
  199. package/src/dashboard/dist/index.html +163 -2
  200. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  201. package/src/dashboard/src/pages/index.astro +162 -1
  202. package/src/dashboard/src/styles/dashboard.css +85 -0
  203. package/src/orchestrator/agents/developer.agent.md +8 -0
  204. package/src/orchestrator/agents/ui-ux-expert.agent.md +7 -0
  205. package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
  206. package/src/orchestrator/prompts/brainstorm.prompt.md +18 -0
  207. package/src/orchestrator/prompts/generate-convoy.prompt.md +61 -0
  208. package/src/orchestrator/skills/decomposition/SKILL.md +35 -0
  209. package/src/orchestrator/skills/frontend-design/SKILL.md +27 -1
  210. package/src/orchestrator/skills/project-consistency/SKILL.md +350 -0
@@ -99,11 +99,11 @@ describe('DB creation', () => {
99
99
  expect(row.journal_mode).toBe('wal')
100
100
  })
101
101
 
102
- it('sets schema version to 10', () => {
102
+ it('sets schema version to 11', () => {
103
103
  const db = new DatabaseSync(dbPath)
104
104
  const row = db.prepare('PRAGMA user_version').get() as { user_version: number }
105
105
  db.close()
106
- expect(row.user_version).toBe(10)
106
+ expect(row.user_version).toBe(11)
107
107
  })
108
108
 
109
109
  it('creates all required tables', () => {
@@ -131,7 +131,7 @@ describe('DB creation', () => {
131
131
  store2.close()
132
132
  // Reassign so afterEach does not double-close
133
133
  store = createConvoyStore(dbPath)
134
- expect(row.user_version).toBe(10)
134
+ expect(row.user_version).toBe(11)
135
135
  })
136
136
  })
137
137
 
@@ -208,8 +208,8 @@ describe('schema migration', () => {
208
208
  verifyDb.close()
209
209
 
210
210
  expect(cols.map(c => c.name)).toContain('adapter')
211
- // v1 chains through v2→v3→v4→...→v7→v8→v9→v10 in one init, so final version is 10
212
- expect(version.user_version).toBe(10)
211
+ // v1 chains through v2→v3→v4→...→v7→v8→v9→v10→v11 in one init, so final version is 11
212
+ expect(version.user_version).toBe(11)
213
213
  })
214
214
 
215
215
  it('schema migration v2 to v3 adds cost columns', () => {
@@ -295,7 +295,7 @@ describe('schema migration', () => {
295
295
  expect(convoyColNames).toContain('total_tokens')
296
296
  expect(convoyColNames).toContain('total_cost_usd')
297
297
 
298
- expect(version.user_version).toBe(10)
298
+ expect(version.user_version).toBe(11)
299
299
  })
300
300
 
301
301
  it('schema migration v1 to v3 chains correctly in a single init', () => {
@@ -381,7 +381,7 @@ describe('schema migration', () => {
381
381
  expect(convoyColNames).toContain('total_tokens')
382
382
  expect(convoyColNames).toContain('total_cost_usd')
383
383
 
384
- expect(version.user_version).toBe(10)
384
+ expect(version.user_version).toBe(11)
385
385
  })
386
386
 
387
387
  it('schema migration v3 to v4 creates pipeline table and adds pipeline_id to convoy', () => {
@@ -464,7 +464,7 @@ describe('schema migration', () => {
464
464
 
465
465
  expect(convoyCols.map(c => c.name)).toContain('pipeline_id')
466
466
  expect(tables.map(t => t.name)).toContain('pipeline')
467
- expect(version.user_version).toBe(10)
467
+ expect(version.user_version).toBe(11)
468
468
  })
469
469
  })
470
470
 
@@ -1444,7 +1444,7 @@ describe('schema migration v5 → v6', () => {
1444
1444
  v5Verify.close()
1445
1445
  migratedStore.close()
1446
1446
 
1447
- expect(row.user_version).toBe(10)
1447
+ expect(row.user_version).toBe(11)
1448
1448
  expect(taskStepTable?.name).toBe('task_step')
1449
1449
  expect(convoy?.id).toBe('convoy-auto')
1450
1450
  expect(task?.id).toBe('task-auto')
@@ -1614,7 +1614,7 @@ describe('schema migration v6→v7 (drift detection columns)', () => {
1614
1614
 
1615
1615
  expect(cols.map(c => c.name)).toContain('drift_score')
1616
1616
  expect(cols.map(c => c.name)).toContain('drift_retried')
1617
- expect(version.user_version).toBe(10)
1617
+ expect(version.user_version).toBe(11)
1618
1618
  })
1619
1619
 
1620
1620
  it('new databases include drift_score and drift_retried in CREATE TABLE', () => {
@@ -1847,9 +1847,9 @@ describe('migration full chain v4→v10', () => {
1847
1847
 
1848
1848
  const verifyDb = new DatabaseSync(chainDbPath)
1849
1849
 
1850
- // Verify user_version = 10
1850
+ // Verify user_version = 11
1851
1851
  const version = (verifyDb.prepare('PRAGMA user_version').get() as { user_version: number }).user_version
1852
- expect(version).toBe(10)
1852
+ expect(version).toBe(11)
1853
1853
 
1854
1854
  // Verify all new tables exist
1855
1855
  const tables = (verifyDb.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as Array<{ name: string }>).map(t => t.name)
@@ -1863,7 +1863,7 @@ describe('migration full chain v4→v10', () => {
1863
1863
  'gates', 'on_exhausted', 'injected', 'provenance', 'idempotency_key',
1864
1864
  'current_step', 'total_steps', 'review_level', 'review_verdict', 'review_tokens',
1865
1865
  'review_model', 'panel_attempts', 'dispute_id', 'drift_score', 'drift_retried',
1866
- 'outputs', 'inputs', 'discovered_issues',
1866
+ 'outputs', 'inputs', 'discovered_issues', 'compaction_count',
1867
1867
  ]) {
1868
1868
  expect(taskCols).toContain(col)
1869
1869
  }
@@ -2525,10 +2525,10 @@ describe('v9→v10 migration', () => {
2525
2525
 
2526
2526
  migratedStore.close()
2527
2527
 
2528
- // Verify version = 10
2528
+ // Verify version = 11
2529
2529
  const verifyDb = new DatabaseSync(migDb)
2530
2530
  const version = (verifyDb.prepare('PRAGMA user_version').get() as { user_version: number }).user_version
2531
- expect(version).toBe(10)
2531
+ expect(version).toBe(11)
2532
2532
 
2533
2533
  // Verify new REAL columns exist
2534
2534
  const convoyCols = (verifyDb.prepare('PRAGMA table_info(convoy)').all() as Array<{ name: string }>).map(c => c.name)
@@ -17,7 +17,7 @@ import type {
17
17
  TaskStepRecord,
18
18
  } from './types.js'
19
19
 
20
- const SCHEMA_VERSION = 10
20
+ const SCHEMA_VERSION = 11
21
21
 
22
22
  // ── Size limits (bytes) ────────────────────────────────────────────────────────
23
23
  const LIMIT_SPEC_YAML = 256 * 1024 // 256 KB
@@ -83,7 +83,7 @@ export interface ConvoyStore {
83
83
  | 'on_exhausted' | 'injected' | 'provenance' | 'idempotency_key'
84
84
  | 'current_step' | 'total_steps' | 'review_level' | 'review_verdict'
85
85
  | 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'
86
- | 'drift_score' | 'drift_retried' | 'discovered_issues'
86
+ | 'drift_score' | 'drift_retried' | 'discovered_issues' | 'compaction_count'
87
87
  > & { outputs?: string | null; inputs?: string | null },
88
88
  ): void
89
89
  insertInjectedTask(record: TaskRecord): void
@@ -101,6 +101,7 @@ export interface ConvoyStore {
101
101
  TaskRecord,
102
102
  | 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
103
103
  | 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd' | 'prompt'
104
+ | 'contract_result'
104
105
  >
105
106
  >,
106
107
  ): void
@@ -114,6 +115,7 @@ export interface ConvoyStore {
114
115
  convoyId: string,
115
116
  fields: Partial<Pick<TaskRecord, 'drift_score' | 'drift_retried'>>,
116
117
  ): void
118
+ updateTaskCompaction(taskId: string, convoyId: string, compactionCount: number): void
117
119
  updateTaskDisputeStatus(taskId: string, convoyId: string, status: ConvoyTaskStatus, disputeId: string): void
118
120
  getReadyTasks(convoyId: string): TaskRecord[]
119
121
  insertTaskStep(record: Omit<TaskStepRecord, 'id'>): number
@@ -259,9 +261,11 @@ class ConvoyStoreImpl implements ConvoyStore {
259
261
  dispute_id TEXT,
260
262
  drift_score REAL,
261
263
  drift_retried INTEGER NOT NULL DEFAULT 0,
264
+ compaction_count INTEGER NOT NULL DEFAULT 0,
262
265
  outputs TEXT,
263
266
  inputs TEXT,
264
- discovered_issues TEXT
267
+ discovered_issues TEXT,
268
+ contract_result TEXT
265
269
  );
266
270
 
267
271
  CREATE UNIQUE INDEX IF NOT EXISTS idx_task_idempotency ON task(convoy_id, idempotency_key)
@@ -416,6 +420,10 @@ class ConvoyStoreImpl implements ConvoyStore {
416
420
  migrateSchema(this.db, this.dbPath, 9, 10)
417
421
  version = 10
418
422
  }
423
+ if (version === 10) {
424
+ migrateSchema(this.db, this.dbPath, 10, 11)
425
+ version = 11
426
+ }
419
427
  }
420
428
 
421
429
  insertConvoy(
@@ -504,7 +512,7 @@ class ConvoyStoreImpl implements ConvoyStore {
504
512
  | 'on_exhausted' | 'injected' | 'provenance' | 'idempotency_key'
505
513
  | 'current_step' | 'total_steps' | 'review_level' | 'review_verdict'
506
514
  | 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'
507
- | 'drift_score' | 'drift_retried' | 'discovered_issues'
515
+ | 'drift_score' | 'drift_retried' | 'discovered_issues' | 'compaction_count'
508
516
  > & { outputs?: string | null; inputs?: string | null },
509
517
  ): void {
510
518
  this.db
@@ -592,6 +600,7 @@ class ConvoyStoreImpl implements ConvoyStore {
592
600
  TaskRecord,
593
601
  | 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
594
602
  | 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd' | 'prompt'
603
+ | 'contract_result'
595
604
  >
596
605
  >,
597
606
  ): void {
@@ -603,6 +612,7 @@ class ConvoyStoreImpl implements ConvoyStore {
603
612
  const extraFields = [
604
613
  'worker_id', 'worktree', 'output', 'exit_code', 'started_at', 'finished_at',
605
614
  'retries', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'cost_usd', 'prompt',
615
+ 'contract_result',
606
616
  ] as const
607
617
 
608
618
  if (extra) {
@@ -708,6 +718,12 @@ class ConvoyStoreImpl implements ConvoyStore {
708
718
  this.db.prepare(`UPDATE task SET ${sets.join(', ')} WHERE id = :id AND convoy_id = :convoy_id`).run(params)
709
719
  }
710
720
 
721
+ updateTaskCompaction(taskId: string, convoyId: string, compactionCount: number): void {
722
+ this.db
723
+ .prepare('UPDATE task SET compaction_count = :compaction_count WHERE id = :id AND convoy_id = :convoy_id')
724
+ .run({ id: taskId, convoy_id: convoyId, compaction_count: compactionCount })
725
+ }
726
+
711
727
  updateTaskDisputeStatus(taskId: string, convoyId: string, status: ConvoyTaskStatus, disputeId: string): void {
712
728
  this.db
713
729
  .prepare(
@@ -1334,6 +1350,12 @@ export function migrateSchema(db: DatabaseSync, dbPath: string, fromVersion: num
1334
1350
  UPDATE pipeline SET total_cost_usd_num = CAST(total_cost_usd AS REAL) WHERE total_cost_usd IS NOT NULL;
1335
1351
  `)
1336
1352
  }
1353
+ if (v === 10) {
1354
+ db.exec(`
1355
+ ALTER TABLE task ADD COLUMN contract_result TEXT;
1356
+ ALTER TABLE task ADD COLUMN compaction_count INTEGER NOT NULL DEFAULT 0;
1357
+ `)
1358
+ }
1337
1359
  db.exec('COMMIT')
1338
1360
  } catch (err) {
1339
1361
  try { db.exec('ROLLBACK') } catch { /* ignore */ }
@@ -0,0 +1,281 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ checkTDD,
4
+ formatTDDFailure,
5
+ DEFAULT_TDD_CONFIG,
6
+ type TDDCheckResult,
7
+ } from './tdd-gate.js'
8
+ import type { TDDGateConfig } from './types.js'
9
+
10
+ const BASE_CONFIG: TDDGateConfig = { ...DEFAULT_TDD_CONFIG }
11
+
12
+ describe('checkTDD', () => {
13
+ describe('disabled / skipped', () => {
14
+ it('returns skipped when config.enabled is false', () => {
15
+ const result = checkTDD(['src/foo.ts'], [], { ...BASE_CONFIG, enabled: false })
16
+ expect(result.skipped).toBe(true)
17
+ expect(result.skip_reason).toBe('disabled')
18
+ expect(result.passed).toBe(true)
19
+ })
20
+
21
+ it('returns skipped for exempt agent', () => {
22
+ const result = checkTDD(['src/foo.ts'], [], BASE_CONFIG, 'documentation-writer')
23
+ expect(result.skipped).toBe(true)
24
+ expect(result.skip_reason).toBe('exempt_agent')
25
+ expect(result.passed).toBe(true)
26
+ })
27
+
28
+ it('returns skipped for all exempt agents', () => {
29
+ for (const agent of ['documentation-writer', 'copywriter', 'seo-specialist', 'researcher']) {
30
+ const result = checkTDD(['src/foo.ts'], [], BASE_CONFIG, agent)
31
+ expect(result.skipped).toBe(true)
32
+ }
33
+ })
34
+
35
+ it('does NOT skip for unknown agent', () => {
36
+ const result = checkTDD(['src/foo.ts', 'src/foo.test.ts'], [], BASE_CONFIG, 'developer')
37
+ expect(result.skipped).toBe(false)
38
+ })
39
+ })
40
+
41
+ describe('exclude patterns', () => {
42
+ it('excludes **/types.ts', () => {
43
+ const result = checkTDD(['src/cli/convoy/types.ts'], [], BASE_CONFIG)
44
+ expect(result.excluded_files).toContain('src/cli/convoy/types.ts')
45
+ expect(result.new_source_files).toHaveLength(0)
46
+ expect(result.passed).toBe(true)
47
+ })
48
+
49
+ it('excludes **/index.ts', () => {
50
+ const result = checkTDD(['src/index.ts'], [], BASE_CONFIG)
51
+ expect(result.excluded_files).toContain('src/index.ts')
52
+ expect(result.passed).toBe(true)
53
+ })
54
+
55
+ it('excludes **/*.d.ts', () => {
56
+ const result = checkTDD(['src/foo.d.ts'], [], BASE_CONFIG)
57
+ expect(result.excluded_files).toContain('src/foo.d.ts')
58
+ expect(result.passed).toBe(true)
59
+ })
60
+
61
+ it('excludes **/constants.ts', () => {
62
+ const result = checkTDD(['src/constants.ts'], [], BASE_CONFIG)
63
+ expect(result.excluded_files).toContain('src/constants.ts')
64
+ expect(result.passed).toBe(true)
65
+ })
66
+
67
+ it('excludes **/schemas.ts', () => {
68
+ const result = checkTDD(['src/schemas.ts'], [], BASE_CONFIG)
69
+ expect(result.excluded_files).toContain('src/schemas.ts')
70
+ expect(result.passed).toBe(true)
71
+ })
72
+ })
73
+
74
+ describe('test file self-exclusion', () => {
75
+ it('excludes .test.ts files from source candidates', () => {
76
+ const result = checkTDD(['src/foo.test.ts'], [], BASE_CONFIG)
77
+ expect(result.excluded_files).toContain('src/foo.test.ts')
78
+ expect(result.new_source_files).toHaveLength(0)
79
+ expect(result.passed).toBe(true)
80
+ })
81
+
82
+ it('excludes .spec.ts files from source candidates', () => {
83
+ const result = checkTDD(['src/foo.spec.ts'], [], BASE_CONFIG)
84
+ expect(result.excluded_files).toContain('src/foo.spec.ts')
85
+ expect(result.passed).toBe(true)
86
+ })
87
+ })
88
+
89
+ describe('block mode (default)', () => {
90
+ it('passes when source file has test in changedFiles', () => {
91
+ const result = checkTDD(['src/foo.ts', 'src/foo.test.ts'], [], BASE_CONFIG)
92
+ expect(result.passed).toBe(true)
93
+ expect(result.missing_test_files).toHaveLength(0)
94
+ expect(result.existing_test_files).toContain('src/foo.ts')
95
+ })
96
+
97
+ it('blocks when source file has no test', () => {
98
+ const result = checkTDD(['src/foo.ts'], [], BASE_CONFIG)
99
+ expect(result.passed).toBe(false)
100
+ expect(result.missing_test_files).toContain('src/foo.ts')
101
+ })
102
+
103
+ it('passes when test already exists in allFiles (not changedFiles)', () => {
104
+ const result = checkTDD(['src/foo.ts'], ['src/foo.test.ts'], BASE_CONFIG)
105
+ expect(result.passed).toBe(true)
106
+ expect(result.existing_test_files).toContain('src/foo.ts')
107
+ })
108
+
109
+ it('recognizes .spec.ts as valid test file', () => {
110
+ const result = checkTDD(['src/foo.ts', 'src/foo.spec.ts'], [], BASE_CONFIG)
111
+ expect(result.passed).toBe(true)
112
+ expect(result.existing_test_files).toContain('src/foo.ts')
113
+ })
114
+
115
+ it('handles multiple files, some with tests and some without', () => {
116
+ const result = checkTDD(
117
+ ['src/a.ts', 'src/b.ts', 'src/a.test.ts'],
118
+ [],
119
+ BASE_CONFIG,
120
+ )
121
+ expect(result.passed).toBe(false)
122
+ expect(result.missing_test_files).toContain('src/b.ts')
123
+ expect(result.existing_test_files).toContain('src/a.ts')
124
+ expect(result.missing_test_files).not.toContain('src/a.ts')
125
+ })
126
+
127
+ it('passes when all source files have tests', () => {
128
+ const result = checkTDD(
129
+ ['src/a.ts', 'src/b.ts', 'src/a.test.ts', 'src/b.test.ts'],
130
+ [],
131
+ BASE_CONFIG,
132
+ )
133
+ expect(result.passed).toBe(true)
134
+ expect(result.missing_test_files).toHaveLength(0)
135
+ })
136
+ })
137
+
138
+ describe('warn mode', () => {
139
+ const warnConfig: TDDGateConfig = { ...BASE_CONFIG, mode: 'warn' }
140
+
141
+ it('passes even when test file is missing', () => {
142
+ const result = checkTDD(['src/foo.ts'], [], warnConfig)
143
+ expect(result.passed).toBe(true)
144
+ expect(result.missing_test_files).toContain('src/foo.ts')
145
+ expect(result.skipped).toBe(false)
146
+ })
147
+
148
+ it('still reports all missing test files', () => {
149
+ const result = checkTDD(['src/a.ts', 'src/b.ts'], [], warnConfig)
150
+ expect(result.passed).toBe(true)
151
+ expect(result.missing_test_files).toHaveLength(2)
152
+ })
153
+ })
154
+
155
+ describe('source pattern filtering', () => {
156
+ it('ignores files not matching source_patterns', () => {
157
+ const result = checkTDD(['README.md', 'package.json', '.gitignore'], [], BASE_CONFIG)
158
+ expect(result.new_source_files).toHaveLength(0)
159
+ expect(result.passed).toBe(true)
160
+ })
161
+
162
+ it('custom source_patterns work', () => {
163
+ const config: TDDGateConfig = { ...BASE_CONFIG, source_patterns: ['lib/**/*.ts'] }
164
+ const result = checkTDD(['lib/foo.ts'], [], config)
165
+ expect(result.passed).toBe(false)
166
+ expect(result.missing_test_files).toContain('lib/foo.ts')
167
+ })
168
+ })
169
+
170
+ describe('nested file paths', () => {
171
+ it('resolves test path correctly for deeply nested source file', () => {
172
+ const result = checkTDD(
173
+ ['src/cli/convoy/foo.ts', 'src/cli/convoy/foo.test.ts'],
174
+ [],
175
+ BASE_CONFIG,
176
+ )
177
+ expect(result.passed).toBe(true)
178
+ expect(result.existing_test_files).toContain('src/cli/convoy/foo.ts')
179
+ })
180
+
181
+ it('resolves test path against allFiles for pre-existing test', () => {
182
+ const result = checkTDD(
183
+ ['src/cli/convoy/compaction.ts'],
184
+ ['src/cli/convoy/compaction.test.ts'],
185
+ BASE_CONFIG,
186
+ )
187
+ expect(result.passed).toBe(true)
188
+ })
189
+
190
+ it('blocks when nested source file has no test', () => {
191
+ const result = checkTDD(['src/cli/convoy/artifacts.ts'], [], BASE_CONFIG)
192
+ expect(result.passed).toBe(false)
193
+ expect(result.missing_test_files).toContain('src/cli/convoy/artifacts.ts')
194
+ })
195
+ })
196
+
197
+ describe('empty inputs', () => {
198
+ it('passes with no changed files', () => {
199
+ const result = checkTDD([], [], BASE_CONFIG)
200
+ expect(result.passed).toBe(true)
201
+ expect(result.new_source_files).toHaveLength(0)
202
+ })
203
+
204
+ it('passes with no changed files even with allFiles', () => {
205
+ const result = checkTDD([], ['src/foo.ts', 'src/foo.test.ts'], BASE_CONFIG)
206
+ expect(result.passed).toBe(true)
207
+ })
208
+ })
209
+ })
210
+
211
+ describe('formatTDDFailure', () => {
212
+ it('returns success message when no missing test files', () => {
213
+ const result: TDDCheckResult = {
214
+ passed: true,
215
+ new_source_files: ['src/foo.ts'],
216
+ missing_test_files: [],
217
+ existing_test_files: ['src/foo.ts'],
218
+ excluded_files: [],
219
+ skipped: false,
220
+ }
221
+ expect(formatTDDFailure(result)).toBe('TDD Gate: all source files have corresponding tests.')
222
+ })
223
+
224
+ it('formats missing file with correct test path', () => {
225
+ const result: TDDCheckResult = {
226
+ passed: false,
227
+ new_source_files: ['src/cli/convoy/artifacts.ts'],
228
+ missing_test_files: ['src/cli/convoy/artifacts.ts'],
229
+ existing_test_files: [],
230
+ excluded_files: [],
231
+ skipped: false,
232
+ }
233
+ const output = formatTDDFailure(result)
234
+ expect(output).toContain('TDD Gate BLOCKED: New source files without tests:')
235
+ expect(output).toContain('src/cli/convoy/artifacts.ts → missing src/cli/convoy/artifacts.test.ts')
236
+ })
237
+
238
+ it('formats multiple missing files', () => {
239
+ const result: TDDCheckResult = {
240
+ passed: false,
241
+ new_source_files: ['src/cli/convoy/artifacts.ts', 'src/cli/convoy/compaction.ts'],
242
+ missing_test_files: ['src/cli/convoy/artifacts.ts', 'src/cli/convoy/compaction.ts'],
243
+ existing_test_files: [],
244
+ excluded_files: [],
245
+ skipped: false,
246
+ }
247
+ const output = formatTDDFailure(result)
248
+ expect(output).toContain('src/cli/convoy/artifacts.ts → missing src/cli/convoy/artifacts.test.ts')
249
+ expect(output).toContain('src/cli/convoy/compaction.ts → missing src/cli/convoy/compaction.test.ts')
250
+ })
251
+
252
+ it('formats file without directory correctly', () => {
253
+ const result: TDDCheckResult = {
254
+ passed: false,
255
+ new_source_files: ['foo.ts'],
256
+ missing_test_files: ['foo.ts'],
257
+ existing_test_files: [],
258
+ excluded_files: [],
259
+ skipped: false,
260
+ }
261
+ const output = formatTDDFailure(result)
262
+ expect(output).toContain('foo.ts → missing foo.test.ts')
263
+ })
264
+ })
265
+
266
+ describe('DEFAULT_TDD_CONFIG', () => {
267
+ it('has expected defaults', () => {
268
+ expect(DEFAULT_TDD_CONFIG.enabled).toBe(true)
269
+ expect(DEFAULT_TDD_CONFIG.mode).toBe('block')
270
+ expect(DEFAULT_TDD_CONFIG.exempt_agents).toContain('documentation-writer')
271
+ expect(DEFAULT_TDD_CONFIG.exempt_agents).toContain('copywriter')
272
+ expect(DEFAULT_TDD_CONFIG.exempt_agents).toContain('seo-specialist')
273
+ expect(DEFAULT_TDD_CONFIG.exempt_agents).toContain('researcher')
274
+ expect(DEFAULT_TDD_CONFIG.source_patterns).toContain('src/**/*.ts')
275
+ expect(DEFAULT_TDD_CONFIG.test_patterns).toContain('{name}.test.ts')
276
+ expect(DEFAULT_TDD_CONFIG.test_patterns).toContain('{name}.spec.ts')
277
+ expect(DEFAULT_TDD_CONFIG.exclude_patterns).toContain('**/types.ts')
278
+ expect(DEFAULT_TDD_CONFIG.exclude_patterns).toContain('**/index.ts')
279
+ expect(DEFAULT_TDD_CONFIG.exclude_patterns).toContain('**/*.d.ts')
280
+ })
281
+ })
@@ -0,0 +1,154 @@
1
+ import type { TDDGateConfig } from './types.js'
2
+
3
+ export type { TDDGateConfig }
4
+
5
+ export const DEFAULT_TDD_CONFIG: TDDGateConfig = {
6
+ enabled: true,
7
+ source_patterns: ['src/**/*.ts'],
8
+ test_patterns: ['{name}.test.ts', '{name}.spec.ts'],
9
+ exclude_patterns: [
10
+ '**/types.ts',
11
+ '**/index.ts',
12
+ '**/*.d.ts',
13
+ '**/constants.ts',
14
+ '**/schemas.ts',
15
+ ],
16
+ mode: 'block',
17
+ exempt_agents: ['documentation-writer', 'copywriter', 'seo-specialist', 'researcher'],
18
+ }
19
+
20
+ export interface TDDCheckResult {
21
+ passed: boolean
22
+ new_source_files: string[]
23
+ missing_test_files: string[]
24
+ existing_test_files: string[]
25
+ excluded_files: string[]
26
+ skipped: boolean
27
+ skip_reason?: string
28
+ }
29
+
30
+ // ── Glob matching ─────────────────────────────────────────────────────────────
31
+
32
+ function matchGlob(pattern: string, filePath: string): boolean {
33
+ const path = filePath.replace(/\\/g, '/')
34
+ const regexStr = pattern
35
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
36
+ .replace(/\*\*\//g, '(?:.+/)?')
37
+ .replace(/\*\*/g, '.*')
38
+ .replace(/\*/g, '[^/]*')
39
+ return new RegExp('^' + regexStr + '$').test(path)
40
+ }
41
+
42
+ function isTestFile(filePath: string, testPatterns: string[]): boolean {
43
+ const basename = filePath.split('/').pop() ?? ''
44
+ return testPatterns.some(pattern => {
45
+ const idx = pattern.indexOf('{name}')
46
+ if (idx < 0) return false
47
+ const prefix = pattern.slice(0, idx).replace(/[.+^${}()|[\]\\]/g, '\\$&')
48
+ const suffix = pattern.slice(idx + '{name}'.length).replace(/[.+^${}()|[\]\\]/g, '\\$&')
49
+ return new RegExp('^' + prefix + '[^/]+' + suffix + '$').test(basename)
50
+ })
51
+ }
52
+
53
+ function resolveTestPaths(sourceFile: string, testPatterns: string[]): string[] {
54
+ const lastSlash = sourceFile.lastIndexOf('/')
55
+ const dir = lastSlash >= 0 ? sourceFile.slice(0, lastSlash) : ''
56
+ const basename = lastSlash >= 0 ? sourceFile.slice(lastSlash + 1) : sourceFile
57
+ const lastDot = basename.lastIndexOf('.')
58
+ const nameWithoutExt = lastDot >= 0 ? basename.slice(0, lastDot) : basename
59
+
60
+ return testPatterns.map(pattern => {
61
+ const testBasename = pattern.replace('{name}', nameWithoutExt)
62
+ return dir ? dir + '/' + testBasename : testBasename
63
+ })
64
+ }
65
+
66
+ // ── Public API ────────────────────────────────────────────────────────────────
67
+
68
+ export function checkTDD(
69
+ changedFiles: string[],
70
+ allFiles: string[],
71
+ config: TDDGateConfig,
72
+ agent?: string,
73
+ ): TDDCheckResult {
74
+ const empty: TDDCheckResult = {
75
+ passed: true,
76
+ new_source_files: [],
77
+ missing_test_files: [],
78
+ existing_test_files: [],
79
+ excluded_files: [],
80
+ skipped: false,
81
+ }
82
+
83
+ if (!config.enabled) {
84
+ return { ...empty, skipped: true, skip_reason: 'disabled' }
85
+ }
86
+
87
+ if (agent !== undefined && config.exempt_agents.includes(agent)) {
88
+ return { ...empty, skipped: true, skip_reason: 'exempt_agent' }
89
+ }
90
+
91
+ const allFileSet = new Set([...changedFiles, ...allFiles])
92
+
93
+ // Filter to files matching source_patterns
94
+ const sourceFiles = changedFiles.filter(f =>
95
+ config.source_patterns.some(p => matchGlob(p, f)),
96
+ )
97
+
98
+ // Separate excluded vs candidate files
99
+ const excluded: string[] = []
100
+ const candidates: string[] = []
101
+ for (const f of sourceFiles) {
102
+ if (config.exclude_patterns.some(p => matchGlob(p, f))) {
103
+ excluded.push(f)
104
+ continue
105
+ }
106
+ if (isTestFile(f, config.test_patterns)) {
107
+ excluded.push(f)
108
+ continue
109
+ }
110
+ candidates.push(f)
111
+ }
112
+
113
+ // For each candidate, check for corresponding test
114
+ const missingTestFiles: string[] = []
115
+ const existingTestFiles: string[] = []
116
+
117
+ for (const sourceFile of candidates) {
118
+ const testPaths = resolveTestPaths(sourceFile, config.test_patterns)
119
+ const hasTest = testPaths.some(tp => allFileSet.has(tp))
120
+ if (hasTest) {
121
+ existingTestFiles.push(sourceFile)
122
+ } else {
123
+ missingTestFiles.push(sourceFile)
124
+ }
125
+ }
126
+
127
+ const passed = missingTestFiles.length === 0 || config.mode === 'warn'
128
+
129
+ return {
130
+ passed,
131
+ new_source_files: candidates,
132
+ missing_test_files: missingTestFiles,
133
+ existing_test_files: existingTestFiles,
134
+ excluded_files: excluded,
135
+ skipped: false,
136
+ }
137
+ }
138
+
139
+ export function formatTDDFailure(result: TDDCheckResult): string {
140
+ if (result.missing_test_files.length === 0) {
141
+ return 'TDD Gate: all source files have corresponding tests.'
142
+ }
143
+ const lines = ['TDD Gate BLOCKED: New source files without tests:']
144
+ for (const sourceFile of result.missing_test_files) {
145
+ const lastSlash = sourceFile.lastIndexOf('/')
146
+ const dir = lastSlash >= 0 ? sourceFile.slice(0, lastSlash) : ''
147
+ const basename = lastSlash >= 0 ? sourceFile.slice(lastSlash + 1) : sourceFile
148
+ const lastDot = basename.lastIndexOf('.')
149
+ const nameWithoutExt = lastDot >= 0 ? basename.slice(0, lastDot) : basename
150
+ const testFile = (dir ? dir + '/' : '') + nameWithoutExt + '.test.ts'
151
+ lines.push(` - ${sourceFile} → missing ${testFile}`)
152
+ }
153
+ return lines.join('\n')
154
+ }