@soleri/core 9.3.0 → 9.4.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.d.ts +2 -0
- package/dist/engine/module-manifest.d.ts.map +1 -1
- package/dist/engine/module-manifest.js +117 -2
- 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/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -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 +52 -19
- 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 +42 -0
- package/dist/planning/task-complexity-assessor.d.ts.map +1 -0
- package/dist/planning/task-complexity-assessor.js +132 -0
- package/dist/planning/task-complexity-assessor.js.map +1 -0
- 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 +118 -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 +109 -31
- 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/extraction-quality.test.ts +323 -0
- package/src/brain/intelligence.ts +133 -30
- package/src/brain/learning-radar.ts +8 -5
- package/src/brain/second-brain-features.test.ts +1 -1
- package/src/control/intent-router.test.ts +73 -3
- package/src/control/intent-router.ts +38 -1
- package/src/control/types.ts +13 -2
- package/src/curator/curator.test.ts +92 -0
- package/src/curator/curator.ts +29 -1
- package/src/curator/schema.ts +8 -0
- package/src/domain-packs/types.ts +8 -0
- package/src/engine/module-manifest.test.ts +51 -2
- package/src/engine/module-manifest.ts +119 -2
- 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/index.ts +30 -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/planning/plan-lifecycle.ts +80 -22
- package/src/planning/planner-types.ts +6 -0
- package/src/planning/planner.ts +74 -4
- package/src/planning/task-complexity-assessor.test.ts +302 -0
- package/src/planning/task-complexity-assessor.ts +180 -0
- package/src/runtime/admin-ops.test.ts +159 -3
- package/src/runtime/admin-ops.ts +123 -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/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.ts +18 -0
- package/src/runtime/facades/memory-facade.test.ts +14 -12
- package/src/runtime/facades/memory-facade.ts +10 -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 +404 -0
- package/src/runtime/orchestrate-ops.ts +129 -37
- 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/session-briefing.test.ts +1 -0
- package/src/runtime/session-briefing.ts +10 -1
- package/src/runtime/types.ts +3 -0
- package/src/skills/sync-skills.ts +14 -7
- package/src/vault/vault-scaling.test.ts +5 -5
- package/vitest.config.ts +1 -0
|
@@ -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,9 +501,51 @@ 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
|
-
const planId = params.planId as string;
|
|
548
|
+
const planId = params.planId as string | undefined;
|
|
501
549
|
const sessionId = params.sessionId as string;
|
|
502
550
|
const outcome = (params.outcome as string) ?? 'completed';
|
|
503
551
|
const completionSummary = (params.summary as string) ?? '';
|
|
@@ -505,20 +553,26 @@ export function createOrchestrateOps(
|
|
|
505
553
|
const filesModified = (params.filesModified as string[]) ?? [];
|
|
506
554
|
const overrideRationalization = (params.overrideRationalization as boolean) ?? false;
|
|
507
555
|
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
556
|
+
// Look up plan — optional for direct tasks that skipped planning
|
|
557
|
+
const planObj = planId ? planner.get(planId) : null;
|
|
558
|
+
|
|
559
|
+
// Anti-rationalization gate: only if we have acceptance criteria from a plan
|
|
560
|
+
const criteria = planObj && planId ? collectAcceptanceCriteria(planner, planId) : [];
|
|
561
|
+
if (
|
|
562
|
+
outcome === 'completed' &&
|
|
563
|
+
criteria.length > 0 &&
|
|
564
|
+
completionSummary &&
|
|
565
|
+
!overrideRationalization
|
|
566
|
+
) {
|
|
567
|
+
const report = detectRationalizations(criteria, completionSummary);
|
|
568
|
+
if (report.detected) {
|
|
569
|
+
captureRationalizationAntiPattern(vault, report);
|
|
570
|
+
return {
|
|
571
|
+
blocked: true,
|
|
572
|
+
reason: 'Rationalization language detected in completion summary',
|
|
573
|
+
rationalization: report,
|
|
574
|
+
hint: 'Address the unmet criteria, or set overrideRationalization: true to bypass this gate.',
|
|
575
|
+
};
|
|
522
576
|
}
|
|
523
577
|
}
|
|
524
578
|
|
|
@@ -527,7 +581,6 @@ export function createOrchestrateOps(
|
|
|
527
581
|
if (filesModified.length > 0) {
|
|
528
582
|
try {
|
|
529
583
|
const analyzer = new ImpactAnalyzer();
|
|
530
|
-
const planObj = planner.get(planId);
|
|
531
584
|
const scopeHints = planObj?.scope ? [planObj.scope] : undefined;
|
|
532
585
|
impactReport = analyzer.analyzeImpact(
|
|
533
586
|
filesModified,
|
|
@@ -549,10 +602,31 @@ export function createOrchestrateOps(
|
|
|
549
602
|
}
|
|
550
603
|
}
|
|
551
604
|
|
|
552
|
-
// Complete the planner plan (legacy lifecycle)
|
|
553
|
-
|
|
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[] = [];
|
|
609
|
+
let completedPlan;
|
|
610
|
+
if (planObj && planId) {
|
|
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
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
completedPlan = {
|
|
623
|
+
id: planId ?? `direct-${Date.now()}`,
|
|
624
|
+
status: 'completed',
|
|
625
|
+
objective: completionSummary || 'Direct execution',
|
|
626
|
+
};
|
|
627
|
+
}
|
|
554
628
|
|
|
555
|
-
// End brain session
|
|
629
|
+
// End brain session — runs regardless of plan existence
|
|
556
630
|
const session = brainIntelligence.lifecycle({
|
|
557
631
|
action: 'end',
|
|
558
632
|
sessionId,
|
|
@@ -562,7 +636,7 @@ export function createOrchestrateOps(
|
|
|
562
636
|
filesModified,
|
|
563
637
|
});
|
|
564
638
|
|
|
565
|
-
// Extract knowledge
|
|
639
|
+
// Extract knowledge — runs regardless of plan existence
|
|
566
640
|
let extraction = null;
|
|
567
641
|
try {
|
|
568
642
|
extraction = brainIntelligence.extractKnowledge(sessionId);
|
|
@@ -572,31 +646,49 @@ export function createOrchestrateOps(
|
|
|
572
646
|
|
|
573
647
|
// Run flow-engine epilogue if we have a flow plan
|
|
574
648
|
let epilogueResult = null;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
649
|
+
if (planId) {
|
|
650
|
+
const entry = planStore.get(planId);
|
|
651
|
+
if (entry) {
|
|
652
|
+
try {
|
|
653
|
+
const dispatch = buildDispatch(agentId, runtime, facades);
|
|
654
|
+
const summary = `${outcome}: ${entry.plan.summary}. Tools: ${toolsUsed.join(', ') || 'none'}. Files: ${filesModified.join(', ') || 'none'}.`;
|
|
655
|
+
epilogueResult = await runEpilogue(
|
|
656
|
+
dispatch,
|
|
657
|
+
entry.plan.context.probes,
|
|
658
|
+
entry.plan.context.projectPath,
|
|
659
|
+
summary,
|
|
660
|
+
);
|
|
661
|
+
} catch {
|
|
662
|
+
// Epilogue is best-effort
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Clean up plan store
|
|
666
|
+
planStore.delete(planId);
|
|
588
667
|
}
|
|
668
|
+
}
|
|
589
669
|
|
|
590
|
-
|
|
591
|
-
|
|
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
|
+
}
|
|
592
683
|
}
|
|
593
684
|
|
|
594
685
|
return {
|
|
595
|
-
plan,
|
|
686
|
+
plan: completedPlan,
|
|
596
687
|
session,
|
|
597
688
|
extraction,
|
|
598
689
|
epilogue: epilogueResult,
|
|
599
690
|
...(impactReport ? { impactAnalysis: impactReport } : {}),
|
|
691
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
600
692
|
};
|
|
601
693
|
},
|
|
602
694
|
},
|
|
@@ -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');
|