dual-brain 0.2.30 → 0.3.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/.dual-brain/docs/claude-code-extension-points.md +32 -0
- package/.dual-brain/docs/data-tools-capabilities.md +181 -0
- package/.dual-brain/docs/ecosystem-tools.md +91 -0
- package/.dual-brain/docs/panel-handoff.md +124 -0
- package/.dual-brain/docs/ruflo-analysis.md +48 -0
- package/bin/dual-brain.mjs +56 -56
- package/dist/mcp-server/index.d.ts +27 -0
- package/dist/mcp-server/index.js +359 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/agent-protocol.d.ts +163 -0
- package/dist/src/agent-protocol.js +368 -0
- package/dist/src/agent-protocol.js.map +1 -0
- package/dist/src/agents/registry.d.ts +52 -0
- package/dist/src/agents/registry.js +393 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/awareness.d.ts +93 -0
- package/dist/src/awareness.js +406 -0
- package/dist/src/awareness.js.map +1 -0
- package/dist/src/brief.d.ts +48 -0
- package/dist/src/brief.js +179 -0
- package/dist/src/brief.js.map +1 -0
- package/dist/src/calibration.d.ts +32 -0
- package/dist/src/calibration.js +133 -0
- package/dist/src/calibration.js.map +1 -0
- package/dist/src/checkpoint.d.ts +33 -0
- package/dist/src/checkpoint.js +99 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/ci-triage.d.ts +33 -0
- package/dist/src/ci-triage.js +193 -0
- package/dist/src/ci-triage.js.map +1 -0
- package/dist/src/cognitive-loop.d.ts +56 -0
- package/dist/src/cognitive-loop.js +495 -0
- package/dist/src/cognitive-loop.js.map +1 -0
- package/dist/src/collaboration.d.ts +147 -0
- package/dist/src/collaboration.js +438 -0
- package/dist/src/collaboration.js.map +1 -0
- package/dist/src/context-intel.d.ts +47 -0
- package/dist/src/context-intel.js +156 -0
- package/dist/src/context-intel.js.map +1 -0
- package/dist/src/context.d.ts +53 -0
- package/dist/src/context.js +332 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/continuity.d.ts +89 -0
- package/dist/src/continuity.js +230 -0
- package/dist/src/continuity.js.map +1 -0
- package/dist/src/cost-tracker.d.ts +47 -0
- package/dist/src/cost-tracker.js +170 -0
- package/dist/src/cost-tracker.js.map +1 -0
- package/dist/src/debrief.d.ts +53 -0
- package/dist/src/debrief.js +222 -0
- package/dist/src/debrief.js.map +1 -0
- package/dist/src/decide.d.ts +96 -0
- package/dist/src/decide.js +744 -0
- package/dist/src/decide.js.map +1 -0
- package/dist/src/decompose.d.ts +39 -0
- package/dist/src/decompose.js +218 -0
- package/dist/src/decompose.js.map +1 -0
- package/dist/src/detect.d.ts +91 -0
- package/dist/src/detect.js +544 -0
- package/dist/src/detect.js.map +1 -0
- package/dist/src/dispatch.d.ts +154 -0
- package/dist/src/dispatch.js +1306 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/doctor.d.ts +421 -0
- package/dist/src/doctor.js +1689 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/engine.d.ts +70 -0
- package/dist/src/engine.js +155 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/envelope.d.ts +36 -0
- package/dist/src/envelope.js +80 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/failure-memory.d.ts +55 -0
- package/dist/src/failure-memory.js +175 -0
- package/dist/src/failure-memory.js.map +1 -0
- package/dist/src/fx.d.ts +87 -0
- package/dist/src/fx.js +272 -0
- package/dist/src/fx.js.map +1 -0
- package/dist/src/governance.d.ts +93 -0
- package/dist/src/governance.js +261 -0
- package/dist/src/governance.js.map +1 -0
- package/dist/src/handoff.d.ts +11 -0
- package/dist/src/handoff.js +90 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/head-protocol.d.ts +76 -0
- package/dist/src/head-protocol.js +109 -0
- package/dist/src/head-protocol.js.map +1 -0
- package/dist/src/head.d.ts +222 -0
- package/dist/src/head.js +765 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/health.d.ts +132 -0
- package/dist/src/health.js +435 -0
- package/dist/src/health.js.map +1 -0
- package/dist/src/inbox.d.ts +70 -0
- package/dist/src/inbox.js +218 -0
- package/dist/src/inbox.js.map +1 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/install-hooks.d.ts +13 -0
- package/dist/src/install-hooks.js +88 -0
- package/dist/src/install-hooks.js.map +1 -0
- package/dist/src/integrity.d.ts +59 -0
- package/dist/src/integrity.js +206 -0
- package/dist/src/integrity.js.map +1 -0
- package/dist/src/intelligence.d.ts +104 -0
- package/dist/src/intelligence.js +391 -0
- package/dist/src/intelligence.js.map +1 -0
- package/dist/src/ledger.d.ts +54 -0
- package/dist/src/ledger.js +179 -0
- package/dist/src/ledger.js.map +1 -0
- package/dist/src/living-docs.d.ts +14 -0
- package/dist/src/living-docs.js +197 -0
- package/dist/src/living-docs.js.map +1 -0
- package/dist/src/memory-tiers.d.ts +37 -0
- package/dist/src/memory-tiers.js +160 -0
- package/dist/src/memory-tiers.js.map +1 -0
- package/dist/src/model-profiles.d.ts +65 -0
- package/dist/src/model-profiles.js +568 -0
- package/dist/src/model-profiles.js.map +1 -0
- package/dist/src/models.d.ts +58 -0
- package/dist/src/models.js +327 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/narrative.d.ts +54 -0
- package/dist/src/narrative.js +163 -0
- package/dist/src/narrative.js.map +1 -0
- package/dist/src/nextstep.d.ts +16 -0
- package/dist/src/nextstep.js +103 -0
- package/dist/src/nextstep.js.map +1 -0
- package/dist/src/observer.d.ts +18 -0
- package/dist/src/observer.js +251 -0
- package/dist/src/observer.js.map +1 -0
- package/dist/src/outcome.d.ts +110 -0
- package/dist/src/outcome.js +377 -0
- package/dist/src/outcome.js.map +1 -0
- package/dist/src/pipeline.d.ts +167 -0
- package/dist/src/pipeline.js +1503 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/playbook.d.ts +59 -0
- package/dist/src/playbook.js +238 -0
- package/dist/src/playbook.js.map +1 -0
- package/dist/src/pr-agent.d.ts +97 -0
- package/dist/src/pr-agent.js +195 -0
- package/dist/src/pr-agent.js.map +1 -0
- package/dist/src/predictive.d.ts +57 -0
- package/dist/src/predictive.js +230 -0
- package/dist/src/predictive.js.map +1 -0
- package/dist/src/profile.d.ts +294 -0
- package/dist/src/profile.js +1347 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/prompt-audit.d.ts +22 -0
- package/dist/src/prompt-audit.js +194 -0
- package/dist/src/prompt-audit.js.map +1 -0
- package/dist/src/prompt-intel.d.ts +12 -0
- package/dist/src/prompt-intel.js +321 -0
- package/dist/src/prompt-intel.js.map +1 -0
- package/dist/src/provider-context.d.ts +121 -0
- package/dist/src/provider-context.js +222 -0
- package/dist/src/provider-context.js.map +1 -0
- package/dist/src/provider-manager.d.ts +92 -0
- package/dist/src/provider-manager.js +428 -0
- package/dist/src/provider-manager.js.map +1 -0
- package/dist/src/receipt.d.ts +87 -0
- package/dist/src/receipt.js +326 -0
- package/dist/src/receipt.js.map +1 -0
- package/dist/src/recommendations.d.ts +13 -0
- package/dist/src/recommendations.js +291 -0
- package/dist/src/recommendations.js.map +1 -0
- package/dist/src/redact.d.ts +15 -0
- package/dist/src/redact.js +129 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/replit.d.ts +397 -0
- package/dist/src/replit.js +1160 -0
- package/dist/src/replit.js.map +1 -0
- package/dist/src/repo.d.ts +149 -0
- package/dist/src/repo.js +416 -0
- package/dist/src/repo.js.map +1 -0
- package/dist/src/revert.d.ts +30 -0
- package/dist/src/revert.js +166 -0
- package/dist/src/revert.js.map +1 -0
- package/dist/src/room.d.ts +102 -0
- package/dist/src/room.js +212 -0
- package/dist/src/room.js.map +1 -0
- package/dist/src/routing-advisor.d.ts +57 -0
- package/dist/src/routing-advisor.js +221 -0
- package/dist/src/routing-advisor.js.map +1 -0
- package/dist/src/self-correct.d.ts +40 -0
- package/dist/src/self-correct.js +137 -0
- package/dist/src/self-correct.js.map +1 -0
- package/dist/src/session-lock.d.ts +35 -0
- package/dist/src/session-lock.js +134 -0
- package/dist/src/session-lock.js.map +1 -0
- package/dist/src/session.d.ts +267 -0
- package/dist/src/session.js +1660 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/settings-tui.d.ts +5 -0
- package/dist/src/settings-tui.js +422 -0
- package/dist/src/settings-tui.js.map +1 -0
- package/dist/src/setup-flow.d.ts +63 -0
- package/dist/src/setup-flow.js +233 -0
- package/dist/src/setup-flow.js.map +1 -0
- package/dist/src/signal.d.ts +19 -0
- package/dist/src/signal.js +122 -0
- package/dist/src/signal.js.map +1 -0
- package/dist/src/simmer.d.ts +85 -0
- package/dist/src/simmer.js +224 -0
- package/dist/src/simmer.js.map +1 -0
- package/dist/src/state-export.d.ts +129 -0
- package/dist/src/state-export.js +233 -0
- package/dist/src/state-export.js.map +1 -0
- package/dist/src/strategy.d.ts +54 -0
- package/dist/src/strategy.js +95 -0
- package/dist/src/strategy.js.map +1 -0
- package/dist/src/subscription.d.ts +40 -0
- package/dist/src/subscription.js +189 -0
- package/dist/src/subscription.js.map +1 -0
- package/dist/src/templates.d.ts +208 -0
- package/dist/src/templates.js +238 -0
- package/dist/src/templates.js.map +1 -0
- package/dist/src/test.d.ts +9 -0
- package/dist/src/test.js +1173 -0
- package/dist/src/test.js.map +1 -0
- package/dist/src/think-engine.d.ts +67 -0
- package/dist/src/think-engine.js +412 -0
- package/dist/src/think-engine.js.map +1 -0
- package/dist/src/tui.d.ts +71 -0
- package/dist/src/tui.js +242 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/types.d.ts +177 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/update-check.d.ts +7 -0
- package/dist/src/update-check.js +36 -0
- package/dist/src/update-check.js.map +1 -0
- package/dist/src/wave-planner.d.ts +30 -0
- package/dist/src/wave-planner.js +281 -0
- package/dist/src/wave-planner.js.map +1 -0
- package/hooks/head-guard.sh +41 -0
- package/hooks/precompact.mjs +3 -3
- package/hooks/session-end.mjs +3 -3
- package/hooks/task-classifier.mjs +328 -0
- package/hooks/vibe-router.mjs +387 -0
- package/install.mjs +2 -2
- package/package.json +29 -153
- package/src/agents/registry.mjs +0 -405
- package/src/awareness.mjs +0 -425
- package/src/brief.mjs +0 -266
- package/src/calibration.mjs +0 -148
- package/src/checkpoint.mjs +0 -109
- package/src/ci-triage.mjs +0 -191
- package/src/cognitive-loop.mjs +0 -562
- package/src/collaboration.mjs +0 -545
- package/src/context-intel.mjs +0 -158
- package/src/context.mjs +0 -389
- package/src/continuity.mjs +0 -298
- package/src/cost-tracker.mjs +0 -184
- package/src/debrief.mjs +0 -228
- package/src/decide.mjs +0 -1099
- package/src/decompose.mjs +0 -331
- package/src/detect.mjs +0 -702
- package/src/dispatch.mjs +0 -1447
- package/src/doctor.mjs +0 -1607
- package/src/envelope.mjs +0 -139
- package/src/failure-memory.mjs +0 -178
- package/src/fx.mjs +0 -276
- package/src/governance.mjs +0 -279
- package/src/handoff.mjs +0 -87
- package/src/head-protocol.mjs +0 -128
- package/src/head.mjs +0 -952
- package/src/health.mjs +0 -528
- package/src/inbox.mjs +0 -195
- package/src/index.mjs +0 -44
- package/src/install-hooks.mjs +0 -100
- package/src/integrity.mjs +0 -245
- package/src/intelligence.mjs +0 -447
- package/src/ledger.mjs +0 -196
- package/src/living-docs.mjs +0 -210
- package/src/memory-tiers.mjs +0 -193
- package/src/models.mjs +0 -363
- package/src/narrative.mjs +0 -169
- package/src/nextstep.mjs +0 -100
- package/src/observer.mjs +0 -241
- package/src/outcome.mjs +0 -400
- package/src/pipeline.mjs +0 -1711
- package/src/playbook.mjs +0 -257
- package/src/pr-agent.mjs +0 -214
- package/src/predictive.mjs +0 -250
- package/src/profile.mjs +0 -1411
- package/src/prompt-audit.mjs +0 -231
- package/src/prompt-intel.mjs +0 -325
- package/src/provider-context.mjs +0 -257
- package/src/receipt.mjs +0 -344
- package/src/recommendations.mjs +0 -296
- package/src/redact.mjs +0 -192
- package/src/replit.mjs +0 -1210
- package/src/repo.mjs +0 -445
- package/src/revert.mjs +0 -149
- package/src/routing-advisor.mjs +0 -204
- package/src/self-correct.mjs +0 -147
- package/src/session-lock.mjs +0 -160
- package/src/session.mjs +0 -1655
- package/src/settings-tui.mjs +0 -373
- package/src/setup-flow.mjs +0 -223
- package/src/signal.mjs +0 -115
- package/src/simmer.mjs +0 -241
- package/src/strategy.mjs +0 -235
- package/src/subscription.mjs +0 -212
- package/src/templates.mjs +0 -260
- package/src/think-engine.mjs +0 -428
- package/src/tui.mjs +0 -276
- package/src/update-check.mjs +0 -35
- package/src/wave-planner.mjs +0 -294
package/src/signal.mjs
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// signal.mjs — Compound outcome signal scoring
|
|
2
|
-
// Combines multiple weak signals into one reliable reward score.
|
|
3
|
-
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import { execSync } from 'node:child_process';
|
|
7
|
-
|
|
8
|
-
export const EXPECTED_DURATION_MS = { search: 15000, execute: 45000, think: 30000, review: 40000 };
|
|
9
|
-
|
|
10
|
-
export function scoreDurationRatio(durationMs, tier) {
|
|
11
|
-
try {
|
|
12
|
-
if (durationMs <= 0) return null;
|
|
13
|
-
const expectedMs = EXPECTED_DURATION_MS[tier] || EXPECTED_DURATION_MS.execute;
|
|
14
|
-
const ratio = durationMs / expectedMs;
|
|
15
|
-
if (ratio >= 0.5 && ratio <= 1.5) return 1.0;
|
|
16
|
-
if (ratio < 0.2) return 0.5;
|
|
17
|
-
if (ratio > 3.0) return 0.3;
|
|
18
|
-
if (ratio < 0.5) return 0.5 + ((ratio - 0.2) / (0.5 - 0.2)) * 0.5;
|
|
19
|
-
// ratio 1.5–3.0
|
|
20
|
-
return 1.0 - ((ratio - 1.5) / (3.0 - 1.5)) * 0.7;
|
|
21
|
-
} catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function measureFileSurvival(outcome, cwd) {
|
|
27
|
-
try {
|
|
28
|
-
const files = Array.isArray(outcome.filesChanged)
|
|
29
|
-
? outcome.filesChanged
|
|
30
|
-
: [];
|
|
31
|
-
if (files.length === 0) return 1.0;
|
|
32
|
-
|
|
33
|
-
let changed;
|
|
34
|
-
try {
|
|
35
|
-
changed = new Set(
|
|
36
|
-
execSync('git diff --name-only', { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
37
|
-
.split('\n')
|
|
38
|
-
.map(f => f.trim())
|
|
39
|
-
.filter(Boolean)
|
|
40
|
-
);
|
|
41
|
-
} catch {
|
|
42
|
-
changed = new Set();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const survived = files.filter(f => {
|
|
46
|
-
const abs = join(cwd, f);
|
|
47
|
-
return existsSync(abs) && !changed.has(f);
|
|
48
|
-
});
|
|
49
|
-
return survived.length / files.length;
|
|
50
|
-
} catch {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function scoreOutcome(outcome, context = {}) {
|
|
56
|
-
try {
|
|
57
|
-
const tier = outcome.tier ?? 'execute';
|
|
58
|
-
const signals = [];
|
|
59
|
-
|
|
60
|
-
// Signal 1: exit success (weight 0.3)
|
|
61
|
-
let exitVal;
|
|
62
|
-
if (outcome.success === true) exitVal = 1.0;
|
|
63
|
-
else if (outcome.status === 'partial') exitVal = 0.4;
|
|
64
|
-
else exitVal = 0.0;
|
|
65
|
-
signals.push({ name: 'exitSuccess', value: exitVal, weight: 0.3 });
|
|
66
|
-
|
|
67
|
-
// Signal 2: duration ratio (weight 0.25)
|
|
68
|
-
const durationMs = outcome.durationMs ?? 0;
|
|
69
|
-
const durVal = durationMs > 0 ? scoreDurationRatio(durationMs, tier) : null;
|
|
70
|
-
signals.push({ name: 'durationRatio', value: durVal, weight: 0.25 });
|
|
71
|
-
|
|
72
|
-
// Signal 3: token efficiency (weight 0.25)
|
|
73
|
-
let effVal = null;
|
|
74
|
-
const filesChanged = outcome.filesChanged ?? 0;
|
|
75
|
-
const fileCount = Array.isArray(filesChanged) ? filesChanged.length : (typeof filesChanged === 'number' ? filesChanged : 0);
|
|
76
|
-
if (!(fileCount === 0 && tier === 'think')) {
|
|
77
|
-
const tokensUsed =
|
|
78
|
-
outcome.tokensUsed?.output ??
|
|
79
|
-
(durationMs > 0 ? Math.round(durationMs / 100) : null);
|
|
80
|
-
if (tokensUsed !== null) {
|
|
81
|
-
const efficiency = fileCount / Math.max(1, tokensUsed / 1000);
|
|
82
|
-
if (efficiency > 2) effVal = 1.0;
|
|
83
|
-
else if (efficiency >= 0.5) effVal = 0.5 + ((efficiency - 0.5) / 1.5) * 0.5;
|
|
84
|
-
else if (efficiency < 0.1) effVal = 0.2;
|
|
85
|
-
else effVal = 0.2 + ((efficiency - 0.1) / 0.4) * 0.3;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
signals.push({ name: 'tokenEfficiency', value: effVal, weight: 0.25 });
|
|
89
|
-
|
|
90
|
-
// Signal 4: file survival (weight 0.2) — delayed, may be null
|
|
91
|
-
const survivalVal = context.fileSurvival ?? null;
|
|
92
|
-
signals.push({ name: 'fileSurvival', value: survivalVal, weight: 0.2 });
|
|
93
|
-
|
|
94
|
-
// Compound score with weight redistribution
|
|
95
|
-
const active = signals.filter(s => s.value !== null);
|
|
96
|
-
const totalWeight = active.reduce((sum, s) => sum + s.weight, 0);
|
|
97
|
-
const reward = totalWeight > 0
|
|
98
|
-
? active.reduce((sum, s) => sum + (s.value * s.weight / totalWeight), 0)
|
|
99
|
-
: 0;
|
|
100
|
-
const confidence = totalWeight;
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
reward: Math.min(1, Math.max(0, reward)),
|
|
104
|
-
confidence: Math.min(1, confidence),
|
|
105
|
-
signals: {
|
|
106
|
-
exitSuccess: exitVal,
|
|
107
|
-
durationRatio: durVal,
|
|
108
|
-
tokenEfficiency: effVal,
|
|
109
|
-
fileSurvival: survivalVal,
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
} catch {
|
|
113
|
-
return { reward: 0, confidence: 0, signals: { exitSuccess: false, durationRatio: null, tokenEfficiency: null, fileSurvival: null } };
|
|
114
|
-
}
|
|
115
|
-
}
|
package/src/simmer.mjs
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
// simmer.mjs — Ideas that aren't tasks yet. They sit, gather heat, and crystallize.
|
|
2
|
-
//
|
|
3
|
-
// The "song" insight: users drop ideas casually. HEAD tends to acknowledge them
|
|
4
|
-
// verbally then move on. The simmer buffer catches these — every idea gets stored
|
|
5
|
-
// with a heat score. Heat rises when: the idea recurs, evidence supports it,
|
|
6
|
-
// adjacent work makes it more relevant, or time passes and it keeps nagging.
|
|
7
|
-
// When heat crosses a threshold, the idea crystallizes into an actionable item
|
|
8
|
-
// and surfaces to HEAD during deliberation.
|
|
9
|
-
|
|
10
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
|
|
13
|
-
const STATE_DIR = join(process.cwd(), '.dualbrain');
|
|
14
|
-
const SIMMER_FILE = join(STATE_DIR, 'simmer.json');
|
|
15
|
-
|
|
16
|
-
const CRYSTALLIZE_THRESHOLD = 5;
|
|
17
|
-
const MAX_ITEMS = 30;
|
|
18
|
-
const HEAT_DECAY_PER_HOUR = 0.3;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @typedef {object} SimmerItem
|
|
22
|
-
* @property {string} id
|
|
23
|
-
* @property {string} idea - The raw idea in prose
|
|
24
|
-
* @property {string} origin - Where it came from (user quote, observation, debrief finding)
|
|
25
|
-
* @property {number} heat - Current heat score
|
|
26
|
-
* @property {number} createdAt
|
|
27
|
-
* @property {number} lastHeated - Last time heat was added
|
|
28
|
-
* @property {string[]} signals - Evidence trail (why heat was added)
|
|
29
|
-
* @property {boolean} crystallized - Whether it's crossed the threshold
|
|
30
|
-
* @property {string|null} crystallizedAs - What it became (task description, architecture decision, etc)
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Add a new idea to the simmer buffer.
|
|
35
|
-
* If a similar idea already exists (fuzzy match), heat it instead of duplicating.
|
|
36
|
-
*
|
|
37
|
-
* @param {string} idea - The idea in natural language
|
|
38
|
-
* @param {object} opts
|
|
39
|
-
* @param {string} opts.origin - Where this came from
|
|
40
|
-
* @param {number} opts.initialHeat - Starting heat (default 1)
|
|
41
|
-
* @returns {SimmerItem} The created or heated item
|
|
42
|
-
*/
|
|
43
|
-
export function add(idea, { origin = 'observation', initialHeat = 1 } = {}) {
|
|
44
|
-
const items = _load();
|
|
45
|
-
|
|
46
|
-
// Check for similar existing idea
|
|
47
|
-
const existing = _findSimilar(items, idea);
|
|
48
|
-
if (existing) {
|
|
49
|
-
return heat(existing.id, initialHeat, `Recurrence: "${idea.slice(0, 60)}"`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const item = {
|
|
53
|
-
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
|
|
54
|
-
idea,
|
|
55
|
-
origin,
|
|
56
|
-
heat: initialHeat,
|
|
57
|
-
createdAt: Date.now(),
|
|
58
|
-
lastHeated: Date.now(),
|
|
59
|
-
signals: [`Created from: ${origin}`],
|
|
60
|
-
crystallized: false,
|
|
61
|
-
crystallizedAs: null,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
items.push(item);
|
|
65
|
-
_save(items);
|
|
66
|
-
return item;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Add heat to an existing item. If it crosses the threshold, mark as crystallized.
|
|
71
|
-
*
|
|
72
|
-
* @param {string} id
|
|
73
|
-
* @param {number} amount - Heat to add (default 1)
|
|
74
|
-
* @param {string} signal - Why heat is being added
|
|
75
|
-
* @returns {SimmerItem|null}
|
|
76
|
-
*/
|
|
77
|
-
export function heat(id, amount = 1, signal = '') {
|
|
78
|
-
const items = _load();
|
|
79
|
-
const item = items.find(i => i.id === id);
|
|
80
|
-
if (!item) return null;
|
|
81
|
-
|
|
82
|
-
item.heat += amount;
|
|
83
|
-
item.lastHeated = Date.now();
|
|
84
|
-
if (signal) item.signals.push(signal);
|
|
85
|
-
|
|
86
|
-
// Cap signals array
|
|
87
|
-
if (item.signals.length > 10) {
|
|
88
|
-
item.signals = item.signals.slice(-10);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Check crystallization
|
|
92
|
-
if (!item.crystallized && item.heat >= CRYSTALLIZE_THRESHOLD) {
|
|
93
|
-
item.crystallized = true;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
_save(items);
|
|
97
|
-
return item;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Get all items that have crystallized but haven't been surfaced yet.
|
|
102
|
-
* These should be presented to HEAD during deliberation.
|
|
103
|
-
*
|
|
104
|
-
* @returns {SimmerItem[]}
|
|
105
|
-
*/
|
|
106
|
-
export function harvest() {
|
|
107
|
-
const items = _load();
|
|
108
|
-
return items.filter(i => i.crystallized && !i.crystallizedAs);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Mark a crystallized item as actioned — record what it became.
|
|
113
|
-
*
|
|
114
|
-
* @param {string} id
|
|
115
|
-
* @param {string} became - Description of what action was taken
|
|
116
|
-
*/
|
|
117
|
-
export function resolve(id, became) {
|
|
118
|
-
const items = _load();
|
|
119
|
-
const item = items.find(i => i.id === id);
|
|
120
|
-
if (!item) return;
|
|
121
|
-
item.crystallizedAs = became;
|
|
122
|
-
_save(items);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Get all active (non-resolved) simmering items, sorted by heat descending.
|
|
127
|
-
* Used by the narrative to include "what's brewing" context.
|
|
128
|
-
*
|
|
129
|
-
* @returns {SimmerItem[]}
|
|
130
|
-
*/
|
|
131
|
-
export function active() {
|
|
132
|
-
const items = _load();
|
|
133
|
-
_applyDecay(items);
|
|
134
|
-
return items
|
|
135
|
-
.filter(i => !i.crystallizedAs)
|
|
136
|
-
.sort((a, b) => b.heat - a.heat);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Check if an idea already exists in the buffer (for deduplication).
|
|
141
|
-
* @param {string} idea
|
|
142
|
-
* @returns {SimmerItem|null}
|
|
143
|
-
*/
|
|
144
|
-
export function find(idea) {
|
|
145
|
-
const items = _load();
|
|
146
|
-
return _findSimilar(items, idea);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Generate a brief for HEAD showing what's simmering.
|
|
151
|
-
* Included in the narrative context so HEAD is aware of brewing ideas.
|
|
152
|
-
*
|
|
153
|
-
* @returns {string} Prose summary of active simmer items, or empty string
|
|
154
|
-
*/
|
|
155
|
-
export function brief() {
|
|
156
|
-
const items = active();
|
|
157
|
-
if (items.length === 0) return '';
|
|
158
|
-
|
|
159
|
-
const crystallized = items.filter(i => i.crystallized);
|
|
160
|
-
const hot = items.filter(i => !i.crystallized && i.heat >= 3);
|
|
161
|
-
const warm = items.filter(i => !i.crystallized && i.heat >= 1.5 && i.heat < 3);
|
|
162
|
-
|
|
163
|
-
const parts = [];
|
|
164
|
-
|
|
165
|
-
if (crystallized.length > 0) {
|
|
166
|
-
parts.push(`Crystallized (ready to act): ${crystallized.map(i => i.idea.slice(0, 80)).join('; ')}`);
|
|
167
|
-
}
|
|
168
|
-
if (hot.length > 0) {
|
|
169
|
-
parts.push(`Hot (building momentum): ${hot.map(i => `${i.idea.slice(0, 60)} [heat:${i.heat.toFixed(1)}]`).join('; ')}`);
|
|
170
|
-
}
|
|
171
|
-
if (warm.length > 0 && parts.length < 2) {
|
|
172
|
-
parts.push(`Warm: ${warm.slice(0, 3).map(i => i.idea.slice(0, 50)).join('; ')}`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return parts.join('\n');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Prune resolved and cold-dead items.
|
|
180
|
-
*/
|
|
181
|
-
export function prune() {
|
|
182
|
-
let items = _load();
|
|
183
|
-
_applyDecay(items);
|
|
184
|
-
// Remove: resolved items older than 1h, or items with heat <= 0
|
|
185
|
-
const cutoff = Date.now() - 60 * 60 * 1000;
|
|
186
|
-
items = items.filter(i => {
|
|
187
|
-
if (i.crystallizedAs && i.lastHeated < cutoff) return false;
|
|
188
|
-
if (i.heat <= 0) return false;
|
|
189
|
-
return true;
|
|
190
|
-
});
|
|
191
|
-
_save(items);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ── Internal ──────────────────────────────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
function _load() {
|
|
197
|
-
try {
|
|
198
|
-
if (existsSync(SIMMER_FILE)) {
|
|
199
|
-
return JSON.parse(readFileSync(SIMMER_FILE, 'utf8'));
|
|
200
|
-
}
|
|
201
|
-
} catch {}
|
|
202
|
-
return [];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function _save(items) {
|
|
206
|
-
// Cap total items
|
|
207
|
-
if (items.length > MAX_ITEMS) {
|
|
208
|
-
items.sort((a, b) => b.heat - a.heat);
|
|
209
|
-
items = items.slice(0, MAX_ITEMS);
|
|
210
|
-
}
|
|
211
|
-
mkdirSync(STATE_DIR, { recursive: true });
|
|
212
|
-
writeFileSync(SIMMER_FILE, JSON.stringify(items, null, 2));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function _applyDecay(items) {
|
|
216
|
-
const now = Date.now();
|
|
217
|
-
for (const item of items) {
|
|
218
|
-
if (item.crystallized) continue; // Crystallized items don't decay
|
|
219
|
-
const hoursSinceHeat = (now - item.lastHeated) / (60 * 60 * 1000);
|
|
220
|
-
if (hoursSinceHeat > 1) {
|
|
221
|
-
item.heat -= HEAT_DECAY_PER_HOUR * hoursSinceHeat;
|
|
222
|
-
if (item.heat < 0) item.heat = 0;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function _findSimilar(items, idea) {
|
|
228
|
-
const normalized = idea.toLowerCase().replace(/[^a-z0-9\s]/g, '');
|
|
229
|
-
const words = normalized.split(/\s+/).filter(w => w.length > 4);
|
|
230
|
-
if (words.length === 0) return null;
|
|
231
|
-
|
|
232
|
-
for (const item of items) {
|
|
233
|
-
if (item.crystallizedAs) continue; // Skip resolved
|
|
234
|
-
const itemNorm = item.idea.toLowerCase().replace(/[^a-z0-9\s]/g, '');
|
|
235
|
-
const matchCount = words.filter(w => itemNorm.includes(w)).length;
|
|
236
|
-
if (matchCount >= Math.ceil(words.length * 0.5)) {
|
|
237
|
-
return item;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return null;
|
|
241
|
-
}
|
package/src/strategy.mjs
DELETED
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
// strategy.mjs — Dispatch strategy library + selection
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
// ─── Strategy definitions ──────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
export const STRATEGIES = {
|
|
8
|
-
direct: {
|
|
9
|
-
id: 'direct',
|
|
10
|
-
label: 'Direct dispatch',
|
|
11
|
-
description: 'Single agent, single task. Best for clear, focused work.',
|
|
12
|
-
applicability: { maxFiles: 3, maxComplexity: 'moderate', maxRisk: 'medium' },
|
|
13
|
-
cost: 1.0,
|
|
14
|
-
},
|
|
15
|
-
cascade: {
|
|
16
|
-
id: 'cascade',
|
|
17
|
-
label: 'Think → Execute cascade',
|
|
18
|
-
description: 'Cheap thinker refines spec, then worker executes. Best for routine-but-multi-step tasks.',
|
|
19
|
-
applicability: { minFiles: 1, minComplexity: 'moderate', maxRisk: 'high' },
|
|
20
|
-
cost: 1.3,
|
|
21
|
-
},
|
|
22
|
-
split: {
|
|
23
|
-
id: 'split',
|
|
24
|
-
label: 'Decompose → parallel dispatch',
|
|
25
|
-
description: 'Break into sub-tasks, dispatch each at optimal tier. Best for large multi-file changes.',
|
|
26
|
-
applicability: { minFiles: 4, minComplexity: 'complex' },
|
|
27
|
-
cost: 2.0,
|
|
28
|
-
},
|
|
29
|
-
'dual-review': {
|
|
30
|
-
id: 'dual-review',
|
|
31
|
-
label: 'Execute → adversarial review',
|
|
32
|
-
description: 'Worker implements, second model reviews. Best for high-risk/security code.',
|
|
33
|
-
applicability: { minRisk: 'high' },
|
|
34
|
-
cost: 1.5,
|
|
35
|
-
},
|
|
36
|
-
'architect-editor': {
|
|
37
|
-
id: 'architect-editor',
|
|
38
|
-
label: 'Architect reasons → editor implements',
|
|
39
|
-
description: 'Opus/o3 reasons freely, sonnet/haiku formats the edits. Best for complex architecture + implementation.',
|
|
40
|
-
applicability: { minComplexity: 'complex', minFiles: 3 },
|
|
41
|
-
cost: 1.8,
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
const COMPLEXITY_RANK = { trivial: 0, simple: 1, moderate: 2, complex: 3 };
|
|
48
|
-
const RISK_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
49
|
-
|
|
50
|
-
const COST_CAPS = {
|
|
51
|
-
frugal: 1.0,
|
|
52
|
-
'cost-saver': 1.3,
|
|
53
|
-
balanced: 2.0,
|
|
54
|
-
'quality-first': 3.0,
|
|
55
|
-
maximum: Infinity,
|
|
56
|
-
aggressive: Infinity, // maps to maximum behaviour
|
|
57
|
-
fullpower: Infinity,
|
|
58
|
-
fast: 1.3,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const SECURITY_KEYWORDS = /\b(auth|security|billing|payment|credential|secret|token|encrypt|permission|oauth|jwt)\b/i;
|
|
62
|
-
|
|
63
|
-
function costCap(workStyle) {
|
|
64
|
-
return COST_CAPS[workStyle] ?? 2.0;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function fileCount(detection) {
|
|
68
|
-
return detection?.fileCount ?? detection?.files ?? 0;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function complexityRank(detection) {
|
|
72
|
-
return COMPLEXITY_RANK[detection?.complexity] ?? 1;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function riskRank(detection) {
|
|
76
|
-
return RISK_RANK[detection?.risk] ?? 0;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function prompt(detection) {
|
|
80
|
-
return detection?.prompt ?? detection?.description ?? '';
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ─── Scoring ───────────────────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
function scoreStrategies(detection, workStyle) {
|
|
86
|
-
const files = fileCount(detection);
|
|
87
|
-
const cRank = complexityRank(detection);
|
|
88
|
-
const rRank = riskRank(detection);
|
|
89
|
-
const text = prompt(detection);
|
|
90
|
-
const frugal = workStyle === 'frugal';
|
|
91
|
-
const saver = workStyle === 'cost-saver' || workStyle === 'fast';
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
direct: 0.5,
|
|
95
|
-
|
|
96
|
-
cascade: 0
|
|
97
|
-
+ (cRank >= COMPLEXITY_RANK.moderate ? 0.3 : 0)
|
|
98
|
-
+ (files >= 2 ? 0.2 : 0)
|
|
99
|
-
- (frugal ? 0.5 : 0),
|
|
100
|
-
|
|
101
|
-
split: 0
|
|
102
|
-
+ (files >= 4 ? 0.4 : 0)
|
|
103
|
-
+ (cRank >= COMPLEXITY_RANK.complex ? 0.3 : 0)
|
|
104
|
-
- (frugal || saver ? 0.5 : 0),
|
|
105
|
-
|
|
106
|
-
'dual-review': 0
|
|
107
|
-
+ (rRank >= RISK_RANK.high ? 0.5 : 0)
|
|
108
|
-
+ (SECURITY_KEYWORDS.test(text) ? 0.3 : 0)
|
|
109
|
-
- (frugal ? 0.3 : 0),
|
|
110
|
-
|
|
111
|
-
'architect-editor': 0
|
|
112
|
-
+ (cRank >= COMPLEXITY_RANK.complex && files >= 3 ? 0.4 : 0)
|
|
113
|
-
- (saver ? 0.3 : 0),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ─── Export 1: selectStrategy ─────────────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Select the best dispatch strategy for a task.
|
|
121
|
-
* @param {object} detection — from detect.mjs (detectTask output)
|
|
122
|
-
* @param {object} decision — from decide.mjs (decideRoute output)
|
|
123
|
-
* @param {object} profile — user profile (workStyle, etc.)
|
|
124
|
-
* @returns {{ strategy: string, reason: string, alternatives: string[] }}
|
|
125
|
-
*/
|
|
126
|
-
export function selectStrategy(detection, decision, profile) {
|
|
127
|
-
try {
|
|
128
|
-
const workStyle = profile?.workStyle ?? profile?.bias ?? 'balanced';
|
|
129
|
-
const cap = costCap(workStyle);
|
|
130
|
-
const scores = scoreStrategies(detection, workStyle);
|
|
131
|
-
|
|
132
|
-
// Filter by cost cap, then rank
|
|
133
|
-
const ranked = Object.entries(scores)
|
|
134
|
-
.filter(([id]) => STRATEGIES[id].cost <= cap)
|
|
135
|
-
.sort(([, a], [, b]) => b - a);
|
|
136
|
-
|
|
137
|
-
if (!ranked.length) {
|
|
138
|
-
// Fallback — always allow direct
|
|
139
|
-
return { strategy: 'direct', reason: 'Cost cap allows only direct dispatch.', alternatives: [] };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const [bestId] = ranked[0];
|
|
143
|
-
const alternatives = ranked.slice(1).map(([id]) => id);
|
|
144
|
-
|
|
145
|
-
const reasons = {
|
|
146
|
-
direct: 'Clear, focused task within single-agent scope.',
|
|
147
|
-
cascade: 'Multi-step task benefits from spec refinement before execution.',
|
|
148
|
-
split: 'Large file count warrants decomposition into parallel sub-tasks.',
|
|
149
|
-
'dual-review': 'High-risk or security-sensitive work requires adversarial review.',
|
|
150
|
-
'architect-editor': 'Complex architecture + implementation benefits from dual-model reasoning.',
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
strategy: bestId,
|
|
155
|
-
reason: reasons[bestId] ?? 'Best match for task profile.',
|
|
156
|
-
alternatives,
|
|
157
|
-
};
|
|
158
|
-
} catch {
|
|
159
|
-
return { strategy: 'direct', reason: 'Fallback to direct dispatch.', alternatives: [] };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ─── Export 2: describeStrategy ───────────────────────────────────────────────
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Human-readable description of a strategy.
|
|
167
|
-
* @param {string} strategyId
|
|
168
|
-
* @returns {string}
|
|
169
|
-
*/
|
|
170
|
-
export function describeStrategy(strategyId) {
|
|
171
|
-
const s = STRATEGIES[strategyId];
|
|
172
|
-
if (!s) return `Unknown strategy: ${strategyId}`;
|
|
173
|
-
return `${s.label} (cost ×${s.cost})\n${s.description}`;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ─── Export 3: getStrategyForTask ─────────────────────────────────────────────
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Convenience: load profile + decision context, select strategy, return with execution plan.
|
|
180
|
-
* @param {object} detection — from detect.mjs
|
|
181
|
-
* @param {string} [cwd] — working directory (for profile loading)
|
|
182
|
-
* @returns {{ strategy: string, reason: string, alternatives: string[], plan: { steps: object[] } }}
|
|
183
|
-
*/
|
|
184
|
-
export function getStrategyForTask(detection, cwd) {
|
|
185
|
-
const dir = cwd ?? process.cwd();
|
|
186
|
-
let profile = {};
|
|
187
|
-
try {
|
|
188
|
-
const p = join(dir, '.dualbrain', 'config.json');
|
|
189
|
-
if (existsSync(p)) profile = JSON.parse(readFileSync(p, 'utf8'));
|
|
190
|
-
} catch { /* non-throwing */ }
|
|
191
|
-
|
|
192
|
-
// Minimal decision stub (model resolved from profile if available)
|
|
193
|
-
const decision = { model: profile?.models?.execute ?? 'sonnet' };
|
|
194
|
-
const selected = selectStrategy(detection, decision, profile);
|
|
195
|
-
|
|
196
|
-
return { ...selected, plan: buildPlan(selected.strategy, decision) };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ─── Plan builder ─────────────────────────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
function buildPlan(strategyId, decision) {
|
|
202
|
-
const m = decision?.model ?? 'sonnet';
|
|
203
|
-
const plans = {
|
|
204
|
-
direct: [
|
|
205
|
-
{ role: 'worker', model: m, description: 'Execute task' },
|
|
206
|
-
],
|
|
207
|
-
cascade: [
|
|
208
|
-
{ role: 'thinker', model: 'sonnet', description: 'Refine spec' },
|
|
209
|
-
{ role: 'worker', model: 'from-think', description: 'Execute refined spec' },
|
|
210
|
-
],
|
|
211
|
-
split: [
|
|
212
|
-
{ role: 'thinker', model: 'sonnet', description: 'Decompose into sub-tasks' },
|
|
213
|
-
{ role: 'worker', model: 'varies', description: 'Execute each sub-task' },
|
|
214
|
-
],
|
|
215
|
-
'dual-review': [
|
|
216
|
-
{ role: 'worker', model: m, description: 'Implement' },
|
|
217
|
-
{ role: 'reviewer', model: 'sonnet', description: 'Adversarial review' },
|
|
218
|
-
],
|
|
219
|
-
'architect-editor': [
|
|
220
|
-
{ role: 'thinker', model: 'opus', description: 'Architect solution' },
|
|
221
|
-
{ role: 'worker', model: 'haiku', description: 'Format edits' },
|
|
222
|
-
],
|
|
223
|
-
};
|
|
224
|
-
return { steps: plans[strategyId] ?? plans.direct };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ─── Export 4: listStrategies ─────────────────────────────────────────────────
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* List all strategies for display.
|
|
231
|
-
* @returns {{ id: string, label: string, description: string, cost: number }[]}
|
|
232
|
-
*/
|
|
233
|
-
export function listStrategies() {
|
|
234
|
-
return Object.values(STRATEGIES).map(({ id, label, description, cost }) => ({ id, label, description, cost }));
|
|
235
|
-
}
|