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.
- package/LICENSE +93 -21
- package/README.md +9 -3
- package/bin/cli.mjs +15 -0
- package/dist/cli/agents.d.ts.map +1 -1
- package/dist/cli/agents.js +19 -5
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/artifacts-cli.d.ts +3 -0
- package/dist/cli/artifacts-cli.d.ts.map +1 -0
- package/dist/cli/artifacts-cli.js +36 -0
- package/dist/cli/artifacts-cli.js.map +1 -0
- package/dist/cli/baselines.d.ts.map +1 -1
- package/dist/cli/baselines.js +11 -0
- package/dist/cli/baselines.js.map +1 -1
- package/dist/cli/convoy/artifacts.d.ts +25 -0
- package/dist/cli/convoy/artifacts.d.ts.map +1 -0
- package/dist/cli/convoy/artifacts.js +129 -0
- package/dist/cli/convoy/artifacts.js.map +1 -0
- package/dist/cli/convoy/artifacts.test.d.ts +2 -0
- package/dist/cli/convoy/artifacts.test.d.ts.map +1 -0
- package/dist/cli/convoy/artifacts.test.js +169 -0
- package/dist/cli/convoy/artifacts.test.js.map +1 -0
- package/dist/cli/convoy/compaction.d.ts +23 -0
- package/dist/cli/convoy/compaction.d.ts.map +1 -0
- package/dist/cli/convoy/compaction.js +117 -0
- package/dist/cli/convoy/compaction.js.map +1 -0
- package/dist/cli/convoy/compaction.test.d.ts +2 -0
- package/dist/cli/convoy/compaction.test.d.ts.map +1 -0
- package/dist/cli/convoy/compaction.test.js +205 -0
- package/dist/cli/convoy/compaction.test.js.map +1 -0
- package/dist/cli/convoy/contracts.d.ts +22 -0
- package/dist/cli/convoy/contracts.d.ts.map +1 -0
- package/dist/cli/convoy/contracts.js +254 -0
- package/dist/cli/convoy/contracts.js.map +1 -0
- package/dist/cli/convoy/contracts.test.d.ts +2 -0
- package/dist/cli/convoy/contracts.test.d.ts.map +1 -0
- package/dist/cli/convoy/contracts.test.js +239 -0
- package/dist/cli/convoy/contracts.test.js.map +1 -0
- package/dist/cli/convoy/dag-analysis.d.ts +40 -0
- package/dist/cli/convoy/dag-analysis.d.ts.map +1 -0
- package/dist/cli/convoy/dag-analysis.js +282 -0
- package/dist/cli/convoy/dag-analysis.js.map +1 -0
- package/dist/cli/convoy/dag-analysis.test.d.ts +2 -0
- package/dist/cli/convoy/dag-analysis.test.d.ts.map +1 -0
- package/dist/cli/convoy/dag-analysis.test.js +289 -0
- package/dist/cli/convoy/dag-analysis.test.js.map +1 -0
- package/dist/cli/convoy/effort-scaling.d.ts +20 -0
- package/dist/cli/convoy/effort-scaling.d.ts.map +1 -0
- package/dist/cli/convoy/effort-scaling.js +82 -0
- package/dist/cli/convoy/effort-scaling.js.map +1 -0
- package/dist/cli/convoy/effort-scaling.test.d.ts +2 -0
- package/dist/cli/convoy/effort-scaling.test.d.ts.map +1 -0
- package/dist/cli/convoy/effort-scaling.test.js +120 -0
- package/dist/cli/convoy/effort-scaling.test.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +298 -11
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +155 -18
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/event-schemas.d.ts.map +1 -1
- package/dist/cli/convoy/event-schemas.js +55 -0
- package/dist/cli/convoy/event-schemas.js.map +1 -1
- package/dist/cli/convoy/isolation.d.ts +27 -0
- package/dist/cli/convoy/isolation.d.ts.map +1 -0
- package/dist/cli/convoy/isolation.js +120 -0
- package/dist/cli/convoy/isolation.js.map +1 -0
- package/dist/cli/convoy/isolation.test.d.ts +2 -0
- package/dist/cli/convoy/isolation.test.d.ts.map +1 -0
- package/dist/cli/convoy/isolation.test.js +105 -0
- package/dist/cli/convoy/isolation.test.js.map +1 -0
- package/dist/cli/convoy/review-stages.d.ts +9 -0
- package/dist/cli/convoy/review-stages.d.ts.map +1 -0
- package/dist/cli/convoy/review-stages.js +134 -0
- package/dist/cli/convoy/review-stages.js.map +1 -0
- package/dist/cli/convoy/review-stages.test.d.ts +2 -0
- package/dist/cli/convoy/review-stages.test.d.ts.map +1 -0
- package/dist/cli/convoy/review-stages.test.js +197 -0
- package/dist/cli/convoy/review-stages.test.js.map +1 -0
- package/dist/cli/convoy/skill-refinement.d.ts +39 -0
- package/dist/cli/convoy/skill-refinement.d.ts.map +1 -0
- package/dist/cli/convoy/skill-refinement.js +239 -0
- package/dist/cli/convoy/skill-refinement.js.map +1 -0
- package/dist/cli/convoy/skill-refinement.test.d.ts +2 -0
- package/dist/cli/convoy/skill-refinement.test.d.ts.map +1 -0
- package/dist/cli/convoy/skill-refinement.test.js +230 -0
- package/dist/cli/convoy/skill-refinement.test.js.map +1 -0
- package/dist/cli/convoy/spec-builder.d.ts +1 -0
- package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
- package/dist/cli/convoy/spec-builder.js +11 -0
- package/dist/cli/convoy/spec-builder.js.map +1 -1
- package/dist/cli/convoy/spec-builder.test.js +54 -0
- package/dist/cli/convoy/spec-builder.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +3 -2
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +20 -2
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +15 -15
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/tdd-gate.d.ts +15 -0
- package/dist/cli/convoy/tdd-gate.d.ts.map +1 -0
- package/dist/cli/convoy/tdd-gate.js +119 -0
- package/dist/cli/convoy/tdd-gate.js.map +1 -0
- package/dist/cli/convoy/tdd-gate.test.d.ts +2 -0
- package/dist/cli/convoy/tdd-gate.test.d.ts.map +1 -0
- package/dist/cli/convoy/tdd-gate.test.js +227 -0
- package/dist/cli/convoy/tdd-gate.test.js.map +1 -0
- package/dist/cli/convoy/types.d.ts +91 -0
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/convoy/types.js +8 -0
- package/dist/cli/convoy/types.js.map +1 -1
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +54 -0
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/insights.d.ts +3 -0
- package/dist/cli/insights.d.ts.map +1 -0
- package/dist/cli/insights.js +94 -0
- package/dist/cli/insights.js.map +1 -0
- package/dist/cli/lesson.d.ts.map +1 -1
- package/dist/cli/lesson.js +7 -0
- package/dist/cli/lesson.js.map +1 -1
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +7 -0
- package/dist/cli/log.js.map +1 -1
- package/dist/cli/package-config.d.ts +12 -0
- package/dist/cli/package-config.d.ts.map +1 -0
- package/dist/cli/package-config.js +37 -0
- package/dist/cli/package-config.js.map +1 -0
- package/dist/cli/package.d.ts +23 -0
- package/dist/cli/package.d.ts.map +1 -0
- package/dist/cli/package.js +285 -0
- package/dist/cli/package.js.map +1 -0
- package/dist/cli/package.test.d.ts +2 -0
- package/dist/cli/package.test.d.ts.map +1 -0
- package/dist/cli/package.test.js +236 -0
- package/dist/cli/package.test.js.map +1 -0
- package/dist/cli/pipeline.d.ts +6 -0
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +15 -2
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +32 -0
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +51 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +10 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.d.ts.map +1 -0
- package/dist/cli/skills.js +107 -0
- package/dist/cli/skills.js.map +1 -0
- package/dist/cli/types.d.ts +4 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.js +2 -2
- package/package.json +3 -2
- package/src/cli/agents.ts +20 -5
- package/src/cli/artifacts-cli.ts +41 -0
- package/src/cli/baselines.ts +12 -0
- package/src/cli/convoy/artifacts.test.ts +201 -0
- package/src/cli/convoy/artifacts.ts +186 -0
- package/src/cli/convoy/compaction.test.ts +245 -0
- package/src/cli/convoy/compaction.ts +164 -0
- package/src/cli/convoy/contracts.test.ts +279 -0
- package/src/cli/convoy/contracts.ts +280 -0
- package/src/cli/convoy/dag-analysis.test.ts +349 -0
- package/src/cli/convoy/dag-analysis.ts +371 -0
- package/src/cli/convoy/effort-scaling.test.ts +140 -0
- package/src/cli/convoy/effort-scaling.ts +90 -0
- package/src/cli/convoy/engine.test.ts +175 -18
- package/src/cli/convoy/engine.ts +315 -12
- package/src/cli/convoy/event-schemas.ts +55 -0
- package/src/cli/convoy/isolation.test.ts +137 -0
- package/src/cli/convoy/isolation.ts +165 -0
- package/src/cli/convoy/review-stages.test.ts +235 -0
- package/src/cli/convoy/review-stages.ts +166 -0
- package/src/cli/convoy/skill-refinement.test.ts +277 -0
- package/src/cli/convoy/skill-refinement.ts +306 -0
- package/src/cli/convoy/spec-builder.test.ts +61 -0
- package/src/cli/convoy/spec-builder.ts +9 -0
- package/src/cli/convoy/store.test.ts +15 -15
- package/src/cli/convoy/store.ts +26 -4
- package/src/cli/convoy/tdd-gate.test.ts +281 -0
- package/src/cli/convoy/tdd-gate.ts +154 -0
- package/src/cli/convoy/types.ts +51 -0
- package/src/cli/dashboard.ts +55 -0
- package/src/cli/insights.ts +99 -0
- package/src/cli/lesson.ts +8 -0
- package/src/cli/log.ts +8 -0
- package/src/cli/package-config.ts +48 -0
- package/src/cli/package.test.ts +276 -0
- package/src/cli/package.ts +329 -0
- package/src/cli/pipeline.ts +21 -2
- package/src/cli/run/schema.test.ts +58 -0
- package/src/cli/run/schema.ts +33 -0
- package/src/cli/run.ts +14 -1
- package/src/cli/skills.ts +121 -0
- package/src/cli/types.ts +4 -1
- package/src/cli/update.ts +2 -2
- package/src/dashboard/dist/_astro/{index.Je1YjU_y.css → index.BRDFmNzR.css} +1 -1
- package/src/dashboard/dist/index.html +163 -2
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/src/pages/index.astro +162 -1
- package/src/dashboard/src/styles/dashboard.css +85 -0
- package/src/orchestrator/agents/developer.agent.md +8 -0
- package/src/orchestrator/agents/ui-ux-expert.agent.md +7 -0
- package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
- package/src/orchestrator/prompts/brainstorm.prompt.md +18 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +61 -0
- package/src/orchestrator/skills/decomposition/SKILL.md +35 -0
- package/src/orchestrator/skills/frontend-design/SKILL.md +27 -1
- package/src/orchestrator/skills/project-consistency/SKILL.md +350 -0
package/src/cli/convoy/engine.ts
CHANGED
|
@@ -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', {
|
|
@@ -1439,11 +1483,11 @@ async function runConvoy(
|
|
|
1439
1483
|
try {
|
|
1440
1484
|
// SECURITY: Gate/hook commands come from the .convoy.yml spec file, which is operator-controlled.
|
|
1441
1485
|
// They are NOT user-supplied and are part of the trusted build configuration.
|
|
1442
|
-
await execFile('sh', ['-c', command], { cwd: worktreePath ?? basePath })
|
|
1486
|
+
await execFile('sh', ['-c', command], { cwd: worktreePath ?? basePath, maxBuffer: 10 * 1024 * 1024 })
|
|
1443
1487
|
} catch (err) {
|
|
1444
1488
|
const execErr = err as Error & { code?: unknown; stderr?: string; stdout?: string }
|
|
1445
1489
|
const code = typeof execErr.code === 'number' ? execErr.code : 1
|
|
1446
|
-
const output = execErr.stderr
|
|
1490
|
+
const output = [execErr.stderr, execErr.stdout].filter(Boolean).join('\n').trim() || execErr.message || ''
|
|
1447
1491
|
gateFailure = { command, exitCode: code, output }
|
|
1448
1492
|
break
|
|
1449
1493
|
}
|
|
@@ -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
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
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
|
})
|
|
@@ -2430,13 +2711,13 @@ async function runConvoy(
|
|
|
2430
2711
|
try {
|
|
2431
2712
|
// SECURITY: Gate/hook commands come from the .convoy.yml spec file, which is operator-controlled.
|
|
2432
2713
|
// They are NOT user-supplied and are part of the trusted build configuration.
|
|
2433
|
-
await execFile('sh', ['-c', command], { cwd: basePath })
|
|
2714
|
+
await execFile('sh', ['-c', command], { cwd: basePath, maxBuffer: 10 * 1024 * 1024 })
|
|
2434
2715
|
gateResults.push({ command, exitCode: 0, passed: true })
|
|
2435
2716
|
process.stdout.write(` ${c.green('✓')} ${c.dim(command)}\n`)
|
|
2436
2717
|
} catch (err) {
|
|
2437
2718
|
const execErr = err as Error & { code?: unknown; stderr?: string; stdout?: string }
|
|
2438
2719
|
const code = typeof execErr.code === 'number' ? execErr.code : 1
|
|
2439
|
-
const output = execErr.stderr
|
|
2720
|
+
const output = [execErr.stderr, execErr.stdout].filter(Boolean).join('\n').trim() || execErr.message || ''
|
|
2440
2721
|
gateResults.push({ command, exitCode: code, passed: false, output })
|
|
2441
2722
|
process.stdout.write(` ${c.red('✗')} ${c.dim(command)}\n`)
|
|
2442
2723
|
}
|
|
@@ -2454,7 +2735,16 @@ async function runConvoy(
|
|
|
2454
2735
|
.map(g => `Command: ${g.command}\nExit code: ${g.exitCode}\nOutput:\n${g.output ?? '(no output)'}`)
|
|
2455
2736
|
.join('\n\n---\n\n')
|
|
2456
2737
|
|
|
2457
|
-
|
|
2738
|
+
// Gather files touched by convoy tasks to give the fix agent context
|
|
2739
|
+
const allTasks = store.getTasksByConvoy(convoyId)
|
|
2740
|
+
const touchedFiles = allTasks
|
|
2741
|
+
.filter(t => t.files)
|
|
2742
|
+
.flatMap(t => { try { return JSON.parse(t.files as string) as string[] } catch { return [] } })
|
|
2743
|
+
const filesContext = touchedFiles.length > 0
|
|
2744
|
+
? `\n\nFiles modified by the convoy tasks:\n${touchedFiles.map(f => `- ${f}`).join('\n')}\n`
|
|
2745
|
+
: ''
|
|
2746
|
+
|
|
2747
|
+
const fixPrompt = `The following validation gates failed after all convoy tasks completed. Fix the issues so these commands pass.${filesContext}\n\n${failureSummary}`
|
|
2458
2748
|
const fixTaskId = `gate-fix-${gateAttempt}`
|
|
2459
2749
|
|
|
2460
2750
|
process.stdout.write(`\n ${c.yellow('⟳')} ${c.bold(`[${fixTaskId}]`)} fixing gate failures (attempt ${gateAttempt}/${maxGateRetries})\n`)
|
|
@@ -2506,6 +2796,18 @@ async function runConvoy(
|
|
|
2506
2796
|
try { consolidateIssues(basePath) } catch { /* non-critical */ }
|
|
2507
2797
|
}
|
|
2508
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
|
+
|
|
2509
2811
|
// ── Final status & summary ────────────────────────────────────────────────
|
|
2510
2812
|
|
|
2511
2813
|
const allTasksFinal = store.getTasksByConvoy(convoyId)
|
|
@@ -2991,6 +3293,7 @@ export function createConvoyEngine(options: ConvoyEngineOptions): ConvoyEngine {
|
|
|
2991
3293
|
dispute_id: null,
|
|
2992
3294
|
drift_score: null,
|
|
2993
3295
|
drift_retried: 0,
|
|
3296
|
+
compaction_count: 0,
|
|
2994
3297
|
outputs: null,
|
|
2995
3298
|
inputs: null,
|
|
2996
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
|
+
})
|