@soleri/core 9.3.1 → 9.5.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/dist/brain/intelligence.d.ts +5 -0
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +115 -26
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/learning-radar.d.ts +3 -3
- package/dist/brain/learning-radar.d.ts.map +1 -1
- package/dist/brain/learning-radar.js +8 -4
- package/dist/brain/learning-radar.js.map +1 -1
- package/dist/control/intent-router.d.ts +2 -2
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +35 -1
- package/dist/control/intent-router.js.map +1 -1
- package/dist/control/types.d.ts +10 -2
- package/dist/control/types.d.ts.map +1 -1
- package/dist/curator/curator.d.ts +4 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +23 -1
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/schema.d.ts +1 -1
- package/dist/curator/schema.d.ts.map +1 -1
- package/dist/curator/schema.js +8 -0
- package/dist/curator/schema.js.map +1 -1
- package/dist/domain-packs/types.d.ts +6 -0
- package/dist/domain-packs/types.d.ts.map +1 -1
- package/dist/domain-packs/types.js +1 -0
- package/dist/domain-packs/types.js.map +1 -1
- package/dist/engine/module-manifest.js +3 -3
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts +9 -0
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +59 -1
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/facades/types.d.ts +5 -1
- package/dist/facades/types.d.ts.map +1 -1
- package/dist/facades/types.js.map +1 -1
- package/dist/hooks/candidate-scorer.d.ts +28 -0
- package/dist/hooks/candidate-scorer.d.ts.map +1 -0
- package/dist/hooks/candidate-scorer.js +20 -0
- package/dist/hooks/candidate-scorer.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/operator/operator-context-store.d.ts +54 -0
- package/dist/operator/operator-context-store.d.ts.map +1 -0
- package/dist/operator/operator-context-store.js +434 -0
- package/dist/operator/operator-context-store.js.map +1 -0
- package/dist/operator/operator-context-types.d.ts +101 -0
- package/dist/operator/operator-context-types.d.ts.map +1 -0
- package/dist/operator/operator-context-types.js +27 -0
- package/dist/operator/operator-context-types.js.map +1 -0
- package/dist/packs/index.d.ts +2 -2
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +1 -1
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/lockfile.d.ts +3 -0
- package/dist/packs/lockfile.d.ts.map +1 -1
- package/dist/packs/lockfile.js.map +1 -1
- package/dist/packs/types.d.ts +8 -2
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +6 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts +12 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +54 -16
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +6 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/planning/planner.d.ts +21 -1
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +62 -3
- package/dist/planning/planner.js.map +1 -1
- package/dist/planning/task-complexity-assessor.d.ts.map +1 -1
- package/dist/planning/task-complexity-assessor.js.map +1 -1
- package/dist/plugins/types.d.ts +18 -18
- package/dist/runtime/admin-ops.d.ts +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +100 -3
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +19 -9
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +35 -7
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +4 -2
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/control-facade.d.ts.map +1 -1
- package/dist/runtime/facades/control-facade.js +8 -2
- package/dist/runtime/facades/control-facade.js.map +1 -1
- package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
- package/dist/runtime/facades/curator-facade.js +13 -0
- package/dist/runtime/facades/curator-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +10 -12
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +36 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +20 -4
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +71 -4
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/plan-feedback-helper.d.ts +21 -0
- package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
- package/dist/runtime/plan-feedback-helper.js +52 -0
- package/dist/runtime/plan-feedback-helper.js.map +1 -0
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +73 -34
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +9 -1
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/types.d.ts +3 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +13 -7
- package/dist/skills/sync-skills.js.map +1 -1
- package/package.json +1 -1
- package/src/brain/brain-intelligence.test.ts +30 -0
- package/src/brain/brain.ts +120 -46
- package/src/brain/extraction-quality.test.ts +323 -0
- package/src/brain/intelligence.ts +175 -64
- package/src/brain/learning-radar.ts +8 -5
- package/src/brain/second-brain-features.test.ts +1 -1
- package/src/chat/agent-loop.ts +1 -1
- package/src/chat/notifications.ts +4 -0
- package/src/control/intent-router.test.ts +73 -3
- package/src/control/intent-router.ts +48 -9
- package/src/control/types.ts +13 -2
- package/src/curator/curator.test.ts +92 -0
- package/src/curator/curator.ts +162 -18
- package/src/curator/schema.ts +8 -0
- package/src/domain-packs/types.ts +8 -0
- package/src/engine/module-manifest.test.ts +8 -2
- package/src/engine/module-manifest.ts +3 -3
- package/src/engine/register-engine.test.ts +73 -1
- package/src/engine/register-engine.ts +61 -1
- package/src/facades/types.ts +5 -0
- package/src/hooks/candidate-scorer.test.ts +76 -0
- package/src/hooks/candidate-scorer.ts +39 -0
- package/src/hooks/index.ts +6 -0
- package/src/index.ts +24 -0
- package/src/llm/llm-client.ts +1 -0
- package/src/operator/operator-context-store.test.ts +698 -0
- package/src/operator/operator-context-store.ts +569 -0
- package/src/operator/operator-context-types.ts +139 -0
- package/src/packs/index.ts +3 -1
- package/src/packs/lockfile.ts +3 -0
- package/src/packs/types.ts +9 -0
- package/src/persistence/sqlite-provider.ts +1 -0
- package/src/planning/github-projection.ts +48 -44
- package/src/planning/plan-lifecycle.ts +93 -22
- package/src/planning/planner-types.ts +6 -0
- package/src/planning/planner.ts +74 -4
- package/src/planning/task-complexity-assessor.test.ts +6 -2
- package/src/planning/task-complexity-assessor.ts +1 -4
- package/src/queue/pipeline-runner.ts +4 -0
- package/src/runtime/admin-ops.test.ts +139 -6
- package/src/runtime/admin-ops.ts +104 -3
- package/src/runtime/admin-setup-ops.ts +30 -10
- package/src/runtime/capture-ops.test.ts +84 -0
- package/src/runtime/capture-ops.ts +35 -7
- package/src/runtime/curator-extra-ops.test.ts +7 -0
- package/src/runtime/curator-extra-ops.ts +10 -1
- package/src/runtime/facades/admin-facade.test.ts +1 -1
- package/src/runtime/facades/brain-facade.ts +6 -3
- package/src/runtime/facades/control-facade.ts +10 -2
- package/src/runtime/facades/curator-facade.test.ts +7 -0
- package/src/runtime/facades/curator-facade.ts +18 -0
- package/src/runtime/facades/memory-facade.test.ts +14 -12
- package/src/runtime/facades/memory-facade.ts +197 -12
- package/src/runtime/facades/orchestrate-facade.ts +33 -1
- package/src/runtime/facades/plan-facade.test.ts +213 -0
- package/src/runtime/facades/plan-facade.ts +23 -4
- package/src/runtime/orchestrate-ops.test.ts +202 -2
- package/src/runtime/orchestrate-ops.ts +88 -7
- package/src/runtime/plan-feedback-helper.test.ts +173 -0
- package/src/runtime/plan-feedback-helper.ts +63 -0
- package/src/runtime/planning-extra-ops.test.ts +43 -1
- package/src/runtime/planning-extra-ops.ts +96 -33
- package/src/runtime/runtime.test.ts +50 -2
- package/src/runtime/runtime.ts +117 -89
- package/src/runtime/session-briefing.test.ts +1 -0
- package/src/runtime/session-briefing.ts +10 -1
- package/src/runtime/shutdown-registry.test.ts +151 -0
- package/src/runtime/shutdown-registry.ts +85 -0
- package/src/runtime/types.ts +7 -1
- package/src/skills/sync-skills.ts +14 -7
- package/src/transport/http-server.ts +50 -3
- package/src/transport/ws-server.ts +8 -0
- package/src/vault/linking.test.ts +12 -0
- package/src/vault/linking.ts +90 -44
- package/src/vault/vault-maintenance.ts +11 -18
- package/src/vault/vault-memories.ts +21 -13
- package/src/vault/vault-schema.ts +21 -0
- package/src/vault/vault.ts +8 -3
- package/vitest.config.ts +1 -0
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
4
|
import { createOrchestrateOps } from './orchestrate-ops.js';
|
|
3
5
|
import { assessTaskComplexity } from '../planning/task-complexity-assessor.js';
|
|
4
6
|
import type { AgentRuntime } from './types.js';
|
|
5
7
|
|
|
8
|
+
vi.mock('node:fs', () => ({
|
|
9
|
+
default: {
|
|
10
|
+
mkdirSync: vi.fn(),
|
|
11
|
+
writeFileSync: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
6
15
|
// ---------------------------------------------------------------------------
|
|
7
16
|
// Mocks for external modules
|
|
8
17
|
// ---------------------------------------------------------------------------
|
|
@@ -441,8 +450,6 @@ describe('createOrchestrateOps', () => {
|
|
|
441
450
|
});
|
|
442
451
|
|
|
443
452
|
it('orchestrate_complete captures knowledge in both paths', async () => {
|
|
444
|
-
const completeOp = findOp(ops, 'orchestrate_complete');
|
|
445
|
-
|
|
446
453
|
// ── Simple path (no planId) ──
|
|
447
454
|
vi.clearAllMocks();
|
|
448
455
|
rt = mockRuntime();
|
|
@@ -492,6 +499,199 @@ describe('createOrchestrateOps', () => {
|
|
|
492
499
|
expect(result.reasoning.length).toBeGreaterThan(0);
|
|
493
500
|
});
|
|
494
501
|
|
|
502
|
+
it('orchestrate_complete compounds operator signals when provided', async () => {
|
|
503
|
+
const compoundSignalsMock = vi.fn();
|
|
504
|
+
(rt as Record<string, unknown>).operatorContextStore = {
|
|
505
|
+
compoundSignals: compoundSignalsMock,
|
|
506
|
+
hasDrifted: vi.fn().mockReturnValue(false),
|
|
507
|
+
renderContextFile: vi.fn(),
|
|
508
|
+
};
|
|
509
|
+
ops = createOrchestrateOps(rt);
|
|
510
|
+
|
|
511
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
512
|
+
await op.handler({
|
|
513
|
+
sessionId: 'session-1',
|
|
514
|
+
outcome: 'completed',
|
|
515
|
+
operatorSignals: {
|
|
516
|
+
expertise: [{ topic: 'typescript', level: 'expert', confidence: 0.9 }],
|
|
517
|
+
corrections: [{ rule: 'use conventional commits', scope: 'global' }],
|
|
518
|
+
interests: [{ tag: 'coffee' }],
|
|
519
|
+
patterns: [{ pattern: 'prefers small PRs', frequency: 'frequent' }],
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
expect(compoundSignalsMock).toHaveBeenCalledWith(
|
|
524
|
+
{
|
|
525
|
+
expertise: [{ topic: 'typescript', level: 'expert', confidence: 0.9 }],
|
|
526
|
+
corrections: [{ rule: 'use conventional commits', scope: 'global' }],
|
|
527
|
+
interests: [{ tag: 'coffee' }],
|
|
528
|
+
patterns: [{ pattern: 'prefers small PRs', frequency: 'frequent' }],
|
|
529
|
+
},
|
|
530
|
+
'session-1',
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('orchestrate_complete handles empty operator signals gracefully', async () => {
|
|
535
|
+
const compoundSignalsMock = vi.fn();
|
|
536
|
+
(rt as Record<string, unknown>).operatorContextStore = {
|
|
537
|
+
compoundSignals: compoundSignalsMock,
|
|
538
|
+
hasDrifted: vi.fn().mockReturnValue(false),
|
|
539
|
+
renderContextFile: vi.fn(),
|
|
540
|
+
};
|
|
541
|
+
ops = createOrchestrateOps(rt);
|
|
542
|
+
|
|
543
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
544
|
+
await op.handler({
|
|
545
|
+
sessionId: 'session-1',
|
|
546
|
+
outcome: 'completed',
|
|
547
|
+
operatorSignals: {},
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Empty object with default arrays should be passed through
|
|
551
|
+
expect(compoundSignalsMock).toHaveBeenCalledTimes(1);
|
|
552
|
+
const [passedSignals, passedSessionId] = compoundSignalsMock.mock.calls[0];
|
|
553
|
+
expect(passedSessionId).toBe('session-1');
|
|
554
|
+
// Zod defaults produce empty arrays for each field
|
|
555
|
+
expect(passedSignals).toBeDefined();
|
|
556
|
+
expect(Array.isArray(passedSignals.expertise ?? [])).toBe(true);
|
|
557
|
+
expect(Array.isArray(passedSignals.corrections ?? [])).toBe(true);
|
|
558
|
+
expect(Array.isArray(passedSignals.interests ?? [])).toBe(true);
|
|
559
|
+
expect(Array.isArray(passedSignals.patterns ?? [])).toBe(true);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('orchestrate_complete works when operatorContextStore not available', async () => {
|
|
563
|
+
// Ensure no operatorContextStore on runtime (backward compat)
|
|
564
|
+
delete (rt as Record<string, unknown>).operatorContextStore;
|
|
565
|
+
ops = createOrchestrateOps(rt);
|
|
566
|
+
|
|
567
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
568
|
+
const result = (await op.handler({
|
|
569
|
+
sessionId: 'session-1',
|
|
570
|
+
outcome: 'completed',
|
|
571
|
+
operatorSignals: {
|
|
572
|
+
expertise: [{ topic: 'react', level: 'intermediate' }],
|
|
573
|
+
corrections: [],
|
|
574
|
+
interests: [],
|
|
575
|
+
patterns: [],
|
|
576
|
+
},
|
|
577
|
+
})) as Record<string, unknown>;
|
|
578
|
+
|
|
579
|
+
// Should complete normally without errors
|
|
580
|
+
expect(result).toHaveProperty('plan');
|
|
581
|
+
expect(result).toHaveProperty('session');
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('orchestrate_complete re-renders context file when drift detected', async () => {
|
|
585
|
+
const compoundSignalsMock = vi.fn();
|
|
586
|
+
const hasDriftedMock = vi.fn().mockReturnValue(true);
|
|
587
|
+
const renderContextFileMock = vi
|
|
588
|
+
.fn()
|
|
589
|
+
.mockReturnValue(
|
|
590
|
+
'# Operator Context\n\n**Expertise:** typescript (expert, 1 sessions, confidence 0.90).',
|
|
591
|
+
);
|
|
592
|
+
(rt as Record<string, unknown>).operatorContextStore = {
|
|
593
|
+
compoundSignals: compoundSignalsMock,
|
|
594
|
+
hasDrifted: hasDriftedMock,
|
|
595
|
+
renderContextFile: renderContextFileMock,
|
|
596
|
+
};
|
|
597
|
+
rt.config.agentDir = '/tmp/test-agent';
|
|
598
|
+
ops = createOrchestrateOps(rt);
|
|
599
|
+
|
|
600
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
601
|
+
await op.handler({
|
|
602
|
+
sessionId: 'session-1',
|
|
603
|
+
outcome: 'completed',
|
|
604
|
+
operatorSignals: {
|
|
605
|
+
expertise: [{ topic: 'typescript', level: 'expert', confidence: 0.9 }],
|
|
606
|
+
corrections: [],
|
|
607
|
+
interests: [],
|
|
608
|
+
patterns: [],
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
expect(compoundSignalsMock).toHaveBeenCalled();
|
|
613
|
+
expect(hasDriftedMock).toHaveBeenCalled();
|
|
614
|
+
expect(renderContextFileMock).toHaveBeenCalled();
|
|
615
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join('/tmp/test-agent', 'instructions'), {
|
|
616
|
+
recursive: true,
|
|
617
|
+
});
|
|
618
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
619
|
+
path.join('/tmp/test-agent', 'instructions', 'operator-context.md'),
|
|
620
|
+
'# Operator Context\n\n**Expertise:** typescript (expert, 1 sessions, confidence 0.90).',
|
|
621
|
+
'utf-8',
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('orchestrate_complete skips file write when no agentDir', async () => {
|
|
626
|
+
const compoundSignalsMock = vi.fn();
|
|
627
|
+
const hasDriftedMock = vi.fn().mockReturnValue(true);
|
|
628
|
+
const renderContextFileMock = vi.fn().mockReturnValue('# Operator Context');
|
|
629
|
+
(rt as Record<string, unknown>).operatorContextStore = {
|
|
630
|
+
compoundSignals: compoundSignalsMock,
|
|
631
|
+
hasDrifted: hasDriftedMock,
|
|
632
|
+
renderContextFile: renderContextFileMock,
|
|
633
|
+
};
|
|
634
|
+
// agentDir is NOT set
|
|
635
|
+
delete (rt.config as Record<string, unknown>).agentDir;
|
|
636
|
+
ops = createOrchestrateOps(rt);
|
|
637
|
+
|
|
638
|
+
vi.mocked(fs.mkdirSync).mockClear();
|
|
639
|
+
vi.mocked(fs.writeFileSync).mockClear();
|
|
640
|
+
|
|
641
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
642
|
+
await op.handler({
|
|
643
|
+
sessionId: 'session-1',
|
|
644
|
+
outcome: 'completed',
|
|
645
|
+
operatorSignals: {
|
|
646
|
+
expertise: [{ topic: 'react', level: 'intermediate' }],
|
|
647
|
+
corrections: [],
|
|
648
|
+
interests: [],
|
|
649
|
+
patterns: [],
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
expect(compoundSignalsMock).toHaveBeenCalled();
|
|
654
|
+
expect(hasDriftedMock).toHaveBeenCalled();
|
|
655
|
+
// Should NOT write to disk since agentDir is missing
|
|
656
|
+
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
|
657
|
+
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('orchestrate_complete skips file write when no drift', async () => {
|
|
661
|
+
const compoundSignalsMock = vi.fn();
|
|
662
|
+
const hasDriftedMock = vi.fn().mockReturnValue(false);
|
|
663
|
+
const renderContextFileMock = vi.fn();
|
|
664
|
+
(rt as Record<string, unknown>).operatorContextStore = {
|
|
665
|
+
compoundSignals: compoundSignalsMock,
|
|
666
|
+
hasDrifted: hasDriftedMock,
|
|
667
|
+
renderContextFile: renderContextFileMock,
|
|
668
|
+
};
|
|
669
|
+
rt.config.agentDir = '/tmp/test-agent';
|
|
670
|
+
ops = createOrchestrateOps(rt);
|
|
671
|
+
|
|
672
|
+
vi.mocked(fs.mkdirSync).mockClear();
|
|
673
|
+
vi.mocked(fs.writeFileSync).mockClear();
|
|
674
|
+
|
|
675
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
676
|
+
await op.handler({
|
|
677
|
+
sessionId: 'session-1',
|
|
678
|
+
outcome: 'completed',
|
|
679
|
+
operatorSignals: {
|
|
680
|
+
expertise: [],
|
|
681
|
+
corrections: [],
|
|
682
|
+
interests: [],
|
|
683
|
+
patterns: [],
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
expect(compoundSignalsMock).toHaveBeenCalled();
|
|
688
|
+
expect(hasDriftedMock).toHaveBeenCalled();
|
|
689
|
+
// No drift means no file write
|
|
690
|
+
expect(renderContextFileMock).not.toHaveBeenCalled();
|
|
691
|
+
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
|
692
|
+
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
|
693
|
+
});
|
|
694
|
+
|
|
495
695
|
it('assessment result includes non-empty reasoning for complex tasks', () => {
|
|
496
696
|
const result = assessTaskComplexity({
|
|
497
697
|
prompt: 'add authentication across all API routes',
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - orchestrate_quick_capture: one-call knowledge capture without full planning
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
12
14
|
import { z } from 'zod';
|
|
13
15
|
import type { OpDefinition, FacadeConfig } from '../facades/types.js';
|
|
14
16
|
import type { AgentRuntime } from './types.js';
|
|
@@ -18,6 +20,7 @@ import { createDispatcher } from '../flows/dispatch-registry.js';
|
|
|
18
20
|
import { runEpilogue } from '../flows/epilogue.js';
|
|
19
21
|
import type { OrchestrationPlan, ExecutionResult } from '../flows/types.js';
|
|
20
22
|
import type { ContextHealthStatus } from './context-health.js';
|
|
23
|
+
import type { OperatorSignals } from '../operator/operator-context-types.js';
|
|
21
24
|
import {
|
|
22
25
|
detectGitHubContext,
|
|
23
26
|
findMatchingMilestone,
|
|
@@ -472,7 +475,10 @@ export function createOrchestrateOps(
|
|
|
472
475
|
'end brain session, and clean up.',
|
|
473
476
|
auth: 'write',
|
|
474
477
|
schema: z.object({
|
|
475
|
-
planId: z
|
|
478
|
+
planId: z
|
|
479
|
+
.string()
|
|
480
|
+
.optional()
|
|
481
|
+
.describe('ID of the executing plan to complete (optional for direct tasks)'),
|
|
476
482
|
sessionId: z.string().describe('ID of the brain session to end'),
|
|
477
483
|
outcome: z
|
|
478
484
|
.enum(['completed', 'abandoned', 'partial'])
|
|
@@ -495,6 +501,48 @@ export function createOrchestrateOps(
|
|
|
495
501
|
.optional()
|
|
496
502
|
.default(false)
|
|
497
503
|
.describe('Set true to bypass rationalization gate and impact warnings after review'),
|
|
504
|
+
operatorSignals: z
|
|
505
|
+
.object({
|
|
506
|
+
expertise: z
|
|
507
|
+
.array(
|
|
508
|
+
z.object({
|
|
509
|
+
topic: z.string(),
|
|
510
|
+
level: z.enum(['learning', 'intermediate', 'expert']),
|
|
511
|
+
evidence: z.string().optional(),
|
|
512
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
513
|
+
}),
|
|
514
|
+
)
|
|
515
|
+
.default([]),
|
|
516
|
+
corrections: z
|
|
517
|
+
.array(
|
|
518
|
+
z.object({
|
|
519
|
+
rule: z.string(),
|
|
520
|
+
quote: z.string().optional(),
|
|
521
|
+
scope: z.enum(['global', 'project']).default('global'),
|
|
522
|
+
}),
|
|
523
|
+
)
|
|
524
|
+
.default([]),
|
|
525
|
+
interests: z
|
|
526
|
+
.array(
|
|
527
|
+
z.object({
|
|
528
|
+
tag: z.string(),
|
|
529
|
+
context: z.string().optional(),
|
|
530
|
+
}),
|
|
531
|
+
)
|
|
532
|
+
.default([]),
|
|
533
|
+
patterns: z
|
|
534
|
+
.array(
|
|
535
|
+
z.object({
|
|
536
|
+
pattern: z.string(),
|
|
537
|
+
frequency: z.enum(['once', 'occasional', 'frequent']).optional(),
|
|
538
|
+
}),
|
|
539
|
+
)
|
|
540
|
+
.default([]),
|
|
541
|
+
})
|
|
542
|
+
.default({})
|
|
543
|
+
.describe(
|
|
544
|
+
'Your silent assessment of the operator this session. Fill what you observed, empty arrays for what you did not. Never announce this to the operator.',
|
|
545
|
+
),
|
|
498
546
|
}),
|
|
499
547
|
handler: async (params) => {
|
|
500
548
|
const planId = params.planId as string | undefined;
|
|
@@ -510,7 +558,12 @@ export function createOrchestrateOps(
|
|
|
510
558
|
|
|
511
559
|
// Anti-rationalization gate: only if we have acceptance criteria from a plan
|
|
512
560
|
const criteria = planObj && planId ? collectAcceptanceCriteria(planner, planId) : [];
|
|
513
|
-
if (
|
|
561
|
+
if (
|
|
562
|
+
outcome === 'completed' &&
|
|
563
|
+
criteria.length > 0 &&
|
|
564
|
+
completionSummary &&
|
|
565
|
+
!overrideRationalization
|
|
566
|
+
) {
|
|
514
567
|
const report = detectRationalizations(criteria, completionSummary);
|
|
515
568
|
if (report.detected) {
|
|
516
569
|
captureRationalizationAntiPattern(vault, report);
|
|
@@ -549,10 +602,22 @@ export function createOrchestrateOps(
|
|
|
549
602
|
}
|
|
550
603
|
}
|
|
551
604
|
|
|
552
|
-
// Complete the planner plan (legacy lifecycle) —
|
|
605
|
+
// Complete the planner plan (legacy lifecycle) — best-effort
|
|
606
|
+
// The epilogue (brain session, knowledge extraction, flow epilogue) MUST run
|
|
607
|
+
// even if plan transition fails (e.g. already completed, missing, invalid state).
|
|
608
|
+
const warnings: string[] = [];
|
|
553
609
|
let completedPlan;
|
|
554
610
|
if (planObj && planId) {
|
|
555
|
-
|
|
611
|
+
try {
|
|
612
|
+
completedPlan = planner.complete(planId);
|
|
613
|
+
} catch (err) {
|
|
614
|
+
warnings.push(`Plan transition skipped: ${(err as Error).message}`);
|
|
615
|
+
completedPlan = {
|
|
616
|
+
id: planId,
|
|
617
|
+
status: planObj.status ?? 'completed',
|
|
618
|
+
objective: planObj.objective ?? (completionSummary || 'Direct execution'),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
556
621
|
} else {
|
|
557
622
|
completedPlan = {
|
|
558
623
|
id: planId ?? `direct-${Date.now()}`,
|
|
@@ -602,12 +667,28 @@ export function createOrchestrateOps(
|
|
|
602
667
|
}
|
|
603
668
|
}
|
|
604
669
|
|
|
670
|
+
// Compound operator signals (silent learning)
|
|
671
|
+
const signals = params.operatorSignals as OperatorSignals | undefined;
|
|
672
|
+
if (signals && runtime.operatorContextStore) {
|
|
673
|
+
runtime.operatorContextStore.compoundSignals(signals, sessionId);
|
|
674
|
+
|
|
675
|
+
// Re-render operator context file if profile drifted
|
|
676
|
+
const agentDir = runtime.config.agentDir;
|
|
677
|
+
if (runtime.operatorContextStore.hasDrifted() && agentDir) {
|
|
678
|
+
const content = runtime.operatorContextStore.renderContextFile();
|
|
679
|
+
const contextPath = path.join(agentDir, 'instructions', 'operator-context.md');
|
|
680
|
+
fs.mkdirSync(path.dirname(contextPath), { recursive: true });
|
|
681
|
+
fs.writeFileSync(contextPath, content, 'utf-8');
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
605
685
|
return {
|
|
606
686
|
plan: completedPlan,
|
|
607
687
|
session,
|
|
608
688
|
extraction,
|
|
609
689
|
epilogue: epilogueResult,
|
|
610
690
|
...(impactReport ? { impactAnalysis: impactReport } : {}),
|
|
691
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
611
692
|
};
|
|
612
693
|
},
|
|
613
694
|
},
|
|
@@ -766,7 +847,7 @@ export function createOrchestrateOps(
|
|
|
766
847
|
}
|
|
767
848
|
|
|
768
849
|
// 2. Detect GitHub context
|
|
769
|
-
const ctx = detectGitHubContext(projectPath);
|
|
850
|
+
const ctx = await detectGitHubContext(projectPath);
|
|
770
851
|
if (!ctx) {
|
|
771
852
|
return {
|
|
772
853
|
status: 'skipped',
|
|
@@ -804,7 +885,7 @@ export function createOrchestrateOps(
|
|
|
804
885
|
};
|
|
805
886
|
}
|
|
806
887
|
|
|
807
|
-
const updated = updateGitHubIssueBody(ctx.repo, linkToIssue, body);
|
|
888
|
+
const updated = await updateGitHubIssueBody(ctx.repo, linkToIssue, body);
|
|
808
889
|
if (!updated) {
|
|
809
890
|
return {
|
|
810
891
|
status: 'error',
|
|
@@ -868,7 +949,7 @@ export function createOrchestrateOps(
|
|
|
868
949
|
continue;
|
|
869
950
|
}
|
|
870
951
|
|
|
871
|
-
const issueNumber = createGitHubIssue(ctx.repo, task.title, body, {
|
|
952
|
+
const issueNumber = await createGitHubIssue(ctx.repo, task.title, body, {
|
|
872
953
|
milestone: milestoneNumber,
|
|
873
954
|
labels: labels.length > 0 ? labels : undefined,
|
|
874
955
|
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { recordPlanFeedback } from './plan-feedback-helper.js';
|
|
3
|
+
|
|
4
|
+
// ─── Mock Factories ───────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
function makeBrain() {
|
|
7
|
+
return {
|
|
8
|
+
recordFeedback: vi.fn(),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function makeBrainIntelligence() {
|
|
13
|
+
return {
|
|
14
|
+
maybeAutoBuildOnFeedback: vi.fn(),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makePlan(decisions: (string | { decision: string })[] = []) {
|
|
19
|
+
return {
|
|
20
|
+
objective: 'Test objective',
|
|
21
|
+
decisions,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Tests ────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe('recordPlanFeedback', () => {
|
|
28
|
+
it('should extract entryIds from decision strings and record feedback', () => {
|
|
29
|
+
const brain = makeBrain();
|
|
30
|
+
const intelligence = makeBrainIntelligence();
|
|
31
|
+
const plan = makePlan([
|
|
32
|
+
'Brain pattern: TDD (strength: 52.5) [entryId:method-tdd-123]',
|
|
33
|
+
'Brain pattern: Vault hooks (strength: 87.5) [entryId:arch-vault-456]',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const count = recordPlanFeedback(plan, brain as unknown, intelligence as unknown);
|
|
37
|
+
|
|
38
|
+
expect(count).toBe(2);
|
|
39
|
+
expect(brain.recordFeedback).toHaveBeenCalledTimes(2);
|
|
40
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith(
|
|
41
|
+
'Test objective',
|
|
42
|
+
'method-tdd-123',
|
|
43
|
+
'accepted',
|
|
44
|
+
);
|
|
45
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith(
|
|
46
|
+
'Test objective',
|
|
47
|
+
'arch-vault-456',
|
|
48
|
+
'accepted',
|
|
49
|
+
);
|
|
50
|
+
expect(intelligence.maybeAutoBuildOnFeedback).toHaveBeenCalledOnce();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle decision objects with .decision property', () => {
|
|
54
|
+
const brain = makeBrain();
|
|
55
|
+
const plan = makePlan([{ decision: 'Use vault pattern [entryId:obj-entry-1]' }]);
|
|
56
|
+
|
|
57
|
+
const count = recordPlanFeedback(plan, brain as unknown);
|
|
58
|
+
|
|
59
|
+
expect(count).toBe(1);
|
|
60
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith('Test objective', 'obj-entry-1', 'accepted');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should skip decisions without entryId markers', () => {
|
|
64
|
+
const brain = makeBrain();
|
|
65
|
+
const plan = makePlan([
|
|
66
|
+
'Brain pattern: TDD (strength: 52.5)',
|
|
67
|
+
'Some decision without an entry ID',
|
|
68
|
+
'Brain pattern: Vault hooks (strength: 87.5) [entryId:arch-vault-456]',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const count = recordPlanFeedback(plan, brain as unknown);
|
|
72
|
+
|
|
73
|
+
expect(count).toBe(1);
|
|
74
|
+
expect(brain.recordFeedback).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith(
|
|
76
|
+
'Test objective',
|
|
77
|
+
'arch-vault-456',
|
|
78
|
+
'accepted',
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should skip malformed entryId markers gracefully', () => {
|
|
83
|
+
const brain = makeBrain();
|
|
84
|
+
const plan = makePlan(['Brain pattern: X [entryId:]', 'Brain pattern: Y [entryId:valid-id]']);
|
|
85
|
+
|
|
86
|
+
const count = recordPlanFeedback(plan, brain as unknown);
|
|
87
|
+
|
|
88
|
+
// [entryId:] won't match because the regex requires at least one char after :
|
|
89
|
+
// Actually the regex [^\]]+ requires 1+ chars, so empty entryId won't match
|
|
90
|
+
expect(count).toBe(1);
|
|
91
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith('Test objective', 'valid-id', 'accepted');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should not double-record duplicate entryIds', () => {
|
|
95
|
+
const brain = makeBrain();
|
|
96
|
+
const plan = makePlan([
|
|
97
|
+
'Decision 1 [entryId:same-entry]',
|
|
98
|
+
'Decision 2 [entryId:same-entry]',
|
|
99
|
+
'Decision 3 [entryId:different-entry]',
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
const count = recordPlanFeedback(plan, brain as unknown);
|
|
103
|
+
|
|
104
|
+
expect(count).toBe(2);
|
|
105
|
+
expect(brain.recordFeedback).toHaveBeenCalledTimes(2);
|
|
106
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith('Test objective', 'same-entry', 'accepted');
|
|
107
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith(
|
|
108
|
+
'Test objective',
|
|
109
|
+
'different-entry',
|
|
110
|
+
'accepted',
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should gracefully handle recordFeedback throwing', () => {
|
|
115
|
+
const brain = makeBrain();
|
|
116
|
+
brain.recordFeedback.mockImplementationOnce(() => {
|
|
117
|
+
throw new Error('Entry not found');
|
|
118
|
+
});
|
|
119
|
+
const plan = makePlan([
|
|
120
|
+
'Decision 1 [entryId:missing-entry]',
|
|
121
|
+
'Decision 2 [entryId:valid-entry]',
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const count = recordPlanFeedback(plan, brain as unknown);
|
|
125
|
+
|
|
126
|
+
// First one throws, second succeeds
|
|
127
|
+
expect(count).toBe(1);
|
|
128
|
+
expect(brain.recordFeedback).toHaveBeenCalledTimes(2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should return 0 and not call maybeAutoBuild when no entryIds found', () => {
|
|
132
|
+
const brain = makeBrain();
|
|
133
|
+
const intelligence = makeBrainIntelligence();
|
|
134
|
+
const plan = makePlan(['Decision without markers', 'Another plain decision']);
|
|
135
|
+
|
|
136
|
+
const count = recordPlanFeedback(plan, brain as unknown, intelligence as unknown);
|
|
137
|
+
|
|
138
|
+
expect(count).toBe(0);
|
|
139
|
+
expect(brain.recordFeedback).not.toHaveBeenCalled();
|
|
140
|
+
expect(intelligence.maybeAutoBuildOnFeedback).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should handle empty decisions array', () => {
|
|
144
|
+
const brain = makeBrain();
|
|
145
|
+
const plan = makePlan([]);
|
|
146
|
+
|
|
147
|
+
const count = recordPlanFeedback(plan, brain as unknown);
|
|
148
|
+
|
|
149
|
+
expect(count).toBe(0);
|
|
150
|
+
expect(brain.recordFeedback).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should work without brainIntelligence (optional param)', () => {
|
|
154
|
+
const brain = makeBrain();
|
|
155
|
+
const plan = makePlan(['Decision [entryId:entry-1]']);
|
|
156
|
+
|
|
157
|
+
const count = recordPlanFeedback(plan, brain as unknown);
|
|
158
|
+
|
|
159
|
+
expect(count).toBe(1);
|
|
160
|
+
// No error thrown despite missing brainIntelligence
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should extract multiple entryIds from a single decision string', () => {
|
|
164
|
+
const brain = makeBrain();
|
|
165
|
+
const plan = makePlan(['Combined: [entryId:first-entry] and also [entryId:second-entry]']);
|
|
166
|
+
|
|
167
|
+
const count = recordPlanFeedback(plan, brain as unknown);
|
|
168
|
+
|
|
169
|
+
expect(count).toBe(2);
|
|
170
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith('Test objective', 'first-entry', 'accepted');
|
|
171
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith('Test objective', 'second-entry', 'accepted');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for recording brain feedback from plan decisions and context.
|
|
3
|
+
*
|
|
4
|
+
* Used by both plan_complete_lifecycle (planning-extra-ops.ts) and
|
|
5
|
+
* orchestrate_complete (orchestrate-ops.ts) to close the brain learning loop.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Brain } from '../brain/brain.js';
|
|
9
|
+
import type { BrainIntelligence } from '../brain/intelligence.js';
|
|
10
|
+
|
|
11
|
+
/** Regex to extract vault entry IDs embedded in decision/context strings. */
|
|
12
|
+
const ENTRY_ID_REGEX = /\[entryId:([^\]]+)\]/g;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract entry IDs from an array of decision or context strings,
|
|
16
|
+
* record positive feedback for each, and optionally trigger auto-rebuild.
|
|
17
|
+
*
|
|
18
|
+
* @returns Number of feedback entries recorded.
|
|
19
|
+
*/
|
|
20
|
+
export function recordPlanFeedback(
|
|
21
|
+
plan: { objective: string; decisions: (string | { decision: string })[] },
|
|
22
|
+
brain: Brain,
|
|
23
|
+
brainIntelligence?: BrainIntelligence,
|
|
24
|
+
): number {
|
|
25
|
+
let feedbackRecorded = 0;
|
|
26
|
+
const seen = new Set<string>();
|
|
27
|
+
|
|
28
|
+
// Collect all strings to scan: decisions + any context strings
|
|
29
|
+
const strings: string[] = [];
|
|
30
|
+
|
|
31
|
+
for (const d of plan.decisions) {
|
|
32
|
+
const str = typeof d === 'string' ? d : d.decision;
|
|
33
|
+
strings.push(str);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const str of strings) {
|
|
37
|
+
// Use matchAll to find all entryId markers in each string
|
|
38
|
+
for (const match of str.matchAll(ENTRY_ID_REGEX)) {
|
|
39
|
+
const entryId = match[1];
|
|
40
|
+
// Skip duplicates within the same plan
|
|
41
|
+
if (seen.has(entryId)) continue;
|
|
42
|
+
seen.add(entryId);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
brain.recordFeedback(plan.objective, entryId, 'accepted');
|
|
46
|
+
feedbackRecorded++;
|
|
47
|
+
} catch {
|
|
48
|
+
// Graceful degradation — skip if entry not found or already recorded
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Trigger auto-rebuild check after recording feedback
|
|
54
|
+
if (feedbackRecorded > 0 && brainIntelligence) {
|
|
55
|
+
try {
|
|
56
|
+
brainIntelligence.maybeAutoBuildOnFeedback();
|
|
57
|
+
} catch {
|
|
58
|
+
// Auto-rebuild is best-effort
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return feedbackRecorded;
|
|
63
|
+
}
|
|
@@ -69,7 +69,7 @@ function createMockRuntime(): AgentRuntime {
|
|
|
69
69
|
|
|
70
70
|
return {
|
|
71
71
|
planner: {
|
|
72
|
-
iterate: vi.fn(() => plan),
|
|
72
|
+
iterate: vi.fn(() => ({ plan, mutated: 1 })),
|
|
73
73
|
splitTasks: vi.fn(() => ({
|
|
74
74
|
...plan,
|
|
75
75
|
tasks: [plan.tasks[0], { id: 'task-2', title: 'Task 2' }],
|
|
@@ -159,6 +159,48 @@ describe('createPlanningExtraOps', () => {
|
|
|
159
159
|
);
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
+
it('returns iterated: false when no changes detected', async () => {
|
|
163
|
+
vi.mocked(runtime.planner.iterate).mockReturnValue({
|
|
164
|
+
plan: makePlan() as unknown,
|
|
165
|
+
mutated: 0,
|
|
166
|
+
} as unknown);
|
|
167
|
+
const result = (await findOp(ops, 'plan_iterate').handler({
|
|
168
|
+
planId: 'plan-1',
|
|
169
|
+
})) as Record<string, unknown>;
|
|
170
|
+
expect(result.iterated).toBe(false);
|
|
171
|
+
expect(result.reason).toBe('no changes detected');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('passes alternatives to planner.iterate', async () => {
|
|
175
|
+
const result = (await findOp(ops, 'plan_iterate').handler({
|
|
176
|
+
planId: 'plan-1',
|
|
177
|
+
alternatives: [
|
|
178
|
+
{ approach: 'Alt A', pros: ['fast'], cons: ['fragile'], rejected_reason: 'Too risky' },
|
|
179
|
+
],
|
|
180
|
+
})) as Record<string, unknown>;
|
|
181
|
+
expect(result.iterated).toBe(true);
|
|
182
|
+
expect(runtime.planner.iterate).toHaveBeenCalledWith(
|
|
183
|
+
'plan-1',
|
|
184
|
+
expect.objectContaining({
|
|
185
|
+
alternatives: [expect.objectContaining({ approach: 'Alt A' })],
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('passes decisions to planner.iterate', async () => {
|
|
191
|
+
const result = (await findOp(ops, 'plan_iterate').handler({
|
|
192
|
+
planId: 'plan-1',
|
|
193
|
+
decisions: [{ decision: 'Use FTS5', rationale: 'Performance' }],
|
|
194
|
+
})) as Record<string, unknown>;
|
|
195
|
+
expect(result.iterated).toBe(true);
|
|
196
|
+
expect(runtime.planner.iterate).toHaveBeenCalledWith(
|
|
197
|
+
'plan-1',
|
|
198
|
+
expect.objectContaining({
|
|
199
|
+
decisions: [{ decision: 'Use FTS5', rationale: 'Performance' }],
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
162
204
|
it('returns error on failure', async () => {
|
|
163
205
|
vi.mocked(runtime.planner.iterate).mockImplementation(() => {
|
|
164
206
|
throw new Error('Not a draft');
|