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.
- package/README.md +4 -1
- 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 +280 -6
- 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/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/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/dashboard/scripts/etl.d.ts.map +1 -1
- package/dist/dashboard/scripts/etl.js +44 -11
- package/dist/dashboard/scripts/etl.js.map +1 -1
- package/package.json +2 -1
- 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 +301 -7
- 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/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/skills.ts +121 -0
- package/src/cli/types.ts +4 -1
- package/src/dashboard/dist/_astro/index.D6quLrA6.css +1 -0
- package/src/dashboard/dist/data/convoy-list.json +21 -7
- package/src/dashboard/dist/data/convoys/demo-api-v2.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +5 -5
- package/src/dashboard/dist/data/convoys/demo-convoy-1.json +2 -2
- package/src/dashboard/dist/data/convoys/demo-convoy-2.json +1 -1
- package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +7 -7
- package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +2 -2
- package/src/dashboard/dist/data/convoys/demo-docs-update.json +2 -2
- package/src/dashboard/dist/data/convoys/demo-perf-opt.json +4 -4
- package/src/dashboard/dist/index.html +306 -33
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/public/data/convoy-list.json +21 -7
- package/src/dashboard/public/data/convoys/demo-api-v2.json +3 -3
- package/src/dashboard/public/data/convoys/demo-auth-revamp.json +5 -5
- package/src/dashboard/public/data/convoys/demo-convoy-1.json +2 -2
- package/src/dashboard/public/data/convoys/demo-convoy-2.json +1 -1
- package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +7 -7
- package/src/dashboard/public/data/convoys/demo-data-pipeline.json +3 -3
- package/src/dashboard/public/data/convoys/demo-deploy-ci.json +2 -2
- package/src/dashboard/public/data/convoys/demo-docs-update.json +2 -2
- package/src/dashboard/public/data/convoys/demo-perf-opt.json +4 -4
- package/src/dashboard/scripts/etl.test.ts +14 -0
- package/src/dashboard/scripts/etl.ts +48 -16
- package/src/dashboard/scripts/generate-demo-db.ts +18 -10
- package/src/dashboard/src/pages/index.astro +348 -45
- package/src/dashboard/src/styles/dashboard.css +56 -0
- package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +19 -0
- package/src/dashboard/dist/_astro/index.BRDFmNzR.css +0 -1
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', {
|
|
@@ -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
|
})
|
|
@@ -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
|
+
})
|