@goodandready/dsh-moa 0.2.74 → 0.2.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -195,6 +195,12 @@ Dispatches `moa/judge-escalation` Cordis events and displays escalation decision
195
195
  ### 22. Self-Consistency Temperature Sampling for Single Models
196
196
  Allows single-model configurations (such as a local Ollama/vLLM instance or a single API key) to leverage Mixture-of-Agents diversity. When enabled on a preset with 1 model slot, the runner expands it into $N$ parallel candidate streams with distinct temperatures (e.g. 0.2, 0.6, 0.9) and complementary personas (`proposer`, `challenger`, `minimalist`), calculates agreement ratio, and performs full MoA consensus synthesis. Dispatches `moa/self-consistency-expand` Cordis events. Configured via `self_consistency_enabled: true`, `self_consistency_samples: 3`, and `self_consistency_temperatures: [0.2, 0.6, 0.9]`.
197
197
 
198
+ ### 23. Stack-Aware Dynamic Persona Evolution
199
+ Scans project manifests (`package.json`, `Cargo.toml`, `pyproject.toml`, `requirements.txt`, `go.mod`) to detect languages, frameworks, and libraries. Automatically specializes candidate proposers into domain-specific roles (e.g. React Optimization, TypeScript Safety, Accessibility, FastAPI Async) to maximize hypothesis diversity. Emits `moa/stack-detected` Cordis events. Configured via `stack_aware_personas_enabled: true`.
200
+
201
+ ### 24. Self-Learning Developer Preferences Metaprompt
202
+ Maintains a persistent profile of developer coding principles and architectural preferences in `~/.dsh/moa/preferences.json`. Injects active principles into the Judge prompt to bias consensus toward team standards (strict typing, functional immutability, error boundaries). Manageable via `GET/POST/DELETE /dsh-moa/preferences` REST endpoints and preset toggle `developer_preferences_enabled: true`.
203
+
198
204
  ### 6. Live Canvas 1-Click Preview (optional)
199
205
  If `@goodandready/dsh-live-canvas` is installed in the same profile, `dsh-moa` pushes the promoted HTML file to the Live Canvas REST contract (`POST /dsh-live-canvas/api/preview`, served by the same harness webServer) and appends a one-click preview link (`/dsh-live-canvas/sandbox/<id>`) to the answer. Without the plugin the step is skipped silently — no errors in the log, no dead links.
200
206
 
package/README.ru.md CHANGED
@@ -195,6 +195,12 @@ graph TD
195
195
  ### 22. Самосогласованное температурное сэмплирование для одиночных моделей (Self-Consistency)
196
196
  Позволяет пользователям с единственной моделью (локальная Ollama, vLLM или единственный API-ключ) получать преимущества ансамблевой валидации MoA. Автоматически разворачивает одиночный слот на $N$ параллельных запусков с различными температурами (`0.2, 0.6, 0.9`) и дополняющими друг друга инженерными персонами (`proposer`, `challenger`, `minimalist`), вычисляет коэффициент согласия (Self-Consistency Agreement) и синтезирует итоговый результат. Генерирует событие `moa/self-consistency-expand`.
197
197
 
198
+ ### 23. Динамическая адаптация персон по манифестам проекта (Stack-Aware Personas)
199
+ Автоматически сканирует манифесты проекта (`package.json`, `Cargo.toml`, `pyproject.toml`, `requirements.txt`, `go.mod`) для определения стека технологий и библиотек. Специализирует роли кандидатов под конкретный стек (оптимизация рендера, строгая типизация, доступность, асинхронные интерфейсы), обеспечивая многоаспектный анализ задачи. Генерирует событие `moa/stack-detected`. Включается через `stack_aware_personas_enabled: true`.
200
+
201
+ ### 24. Метапромпт предпочтений разработчика (Developer Preferences)
202
+ Хранит профиль архитектурных стандартов и стилевых предпочтений разработчика в `~/.dsh/moa/preferences.json`. Инжектирует активные принципы в контекст судьи, гарантируя соответствие выбранного решения вкусам команды (строгая типизация, иммутабельность, обработка ошибок). Доступен REST API (`GET/POST/DELETE /dsh-moa/preferences`) и опция пресета `developer_preferences_enabled: true`.
203
+
198
204
  ### 6. Live Canvas 1-клик предпросмотр (опционально)
199
205
  Если в профиле установлен `@goodandready/dsh-live-canvas`, `dsh-moa` отправляет промоученный HTML-файл в REST-контракт Live Canvas (`POST /dsh-live-canvas/api/preview`, тот же webServer харнесса) и добавляет к ответу ссылку на предпросмотр (`/dsh-live-canvas/sandbox/<id>`). Без плагина шаг пропускается тихо — без ошибок в журнале и без битых ссылок.
200
206
 
package/README.zh.md CHANGED
@@ -172,6 +172,12 @@ graph TD
172
172
  ### 22. 单模型自洽性温度采样 (Self-Consistency Temperature Sampling)
173
173
  专为仅配置单模型(如本地 Ollama/vLLM 或单一 API Key)的场景设计。开启后自动将单槽位扩展为 $N$ 个并行采样(分别采用 0.2、0.6、0.9 等递增温度与 `proposer`、`challenger`、`minimalist` 互补提示词),评估自洽一致性比率并进行全流程 MoA 综合聚合,触发 `moa/self-consistency-expand` 事件。
174
174
 
175
+ ### 23. 基于项目清单的感知角色演化 (Stack-Aware Personas)
176
+ 自动扫描工作区清单 (`package.json`, `Cargo.toml`, `pyproject.toml`, `go.mod`) 识别编程语言、框架与核心依赖,动态为各候选槽位分派互补的领域专家角色(如 React 渲染优化、TypeScript 严格类型安全、可访问性审查、FastAPI 异步架构),触发 `moa/stack-detected` 事件。通过 `stack_aware_personas_enabled: true` 配置。
177
+
178
+ ### 24. 自学习开发者代码偏好元提示词 (Developer Preferences)
179
+ 在本地 `~/.dsh/moa/preferences.json` 持久化存储团队编码规范与架构偏好,将启用的规则动态注入裁判评审阶段,使综合方案贴合开发者个人习惯(严格类型、函数不可变性、防御性错误边界)。提供 `GET/POST/DELETE /dsh-moa/preferences` REST 端点,通过 `developer_preferences_enabled: true` 开启。
180
+
175
181
  ### 6. Live Canvas 一键预览(可选)
176
182
  若同一 profile 中安装了 `@goodandready/dsh-live-canvas`,`dsh-moa` 会将提升后的 HTML 文件推送到 Live Canvas 的 REST 契约(`POST /dsh-live-canvas/api/preview`,由同一 harness webServer 提供服务),并在回答中附上一键预览链接(`/dsh-live-canvas/sandbox/<id>`)。未安装该插件时此步骤静默跳过——日志无报错,也不会出现死链接。
177
183
 
package/lib/client.js CHANGED
@@ -224,7 +224,11 @@ window.__ModuleLoader__.load({
224
224
  "aggregator.escalation_policy_upgrade": "Auto-Upgrade to Senior Judge on Divergence",
225
225
  "aggregator.escalation_policy_council": "Convene 3-Judge Majority Council on Divergence",
226
226
  "aggregator.escalation_threshold": "Divergence Threshold:",
227
- "aggregator.escalation_judge_label": "Senior Judge:"
227
+ "aggregator.escalation_judge_label": "Senior Judge:",
228
+ "aggregator.stack_personas_title": "\u{1F4E6} Stack-Aware Dynamic Persona Evolution (Project Manifests)",
229
+ "aggregator.stack_personas_desc": "Scans project manifests (package.json, Cargo.toml, pyproject.toml, go.mod) and assigns specialized engineering roles to candidate slots.",
230
+ "aggregator.preferences_title": "\u{1F9E0} Developer Coding Principles & Preferences Metaprompt",
231
+ "aggregator.preferences_desc": "Injects learned developer preferences (strict typing, functional immutability, error boundaries) to guide Judge synthesis."
228
232
  };
229
233
  const zh = {
230
234
  title: "\u667A\u80FD\u4F53\u6DF7\u5408 (MoA)",
@@ -439,7 +443,11 @@ window.__ModuleLoader__.load({
439
443
  "aggregator.escalation_policy_upgrade": "\u5206\u6B67\u65F6\u81EA\u52A8\u5347\u7EA7\u81F3\u9AD8\u9636\u88C1\u5224",
440
444
  "aggregator.escalation_policy_council": "\u5206\u6B67\u65F6\u7EC4\u5EFA 3 \u88C1\u5224\u591A\u6570\u51B3\u7B56\u5E2D",
441
445
  "aggregator.escalation_threshold": "\u5206\u6B67\u9608\u503C:",
442
- "aggregator.escalation_judge_label": "\u9AD8\u9636\u88C1\u5224\u6A21\u578B:"
446
+ "aggregator.escalation_judge_label": "\u9AD8\u9636\u88C1\u5224\u6A21\u578B:",
447
+ "aggregator.stack_personas_title": "\u{1F4E6} \u57FA\u4E8E\u9879\u76EE\u914D\u7F6E\u6E05\u5355\u7684\u611F\u77E5\u89D2\u8272\u6F14\u5316 (Stack-Aware Personas)",
448
+ "aggregator.stack_personas_desc": "\u81EA\u52A8\u626B\u63CF\u9879\u76EE\u6E05\u5355 (package.json, Cargo.toml, pyproject.toml, go.mod) \u5E76\u4E3A\u5404\u5019\u9009\u69FD\u4F4D\u6307\u6D3E\u4E13\u5C5E\u4E13\u4E1A\u89D2\u8272\u3002",
449
+ "aggregator.preferences_title": "\u{1F9E0} \u81EA\u5B66\u4E60\u5F00\u53D1\u8005\u4EE3\u7801\u504F\u597D\u5143\u63D0\u793A\u8BCD (Developer Preferences)",
450
+ "aggregator.preferences_desc": "\u5C06\u5F00\u53D1\u8005\u504F\u597D\uFF08\u4E25\u683C\u7C7B\u578B\u3001\u51FD\u6570\u4E0D\u53EF\u53D8\u6027\u3001\u9632\u5FA1\u6027\u9519\u8BEF\u5904\u7406\u7B49\uFF09\u6CE8\u5165\u88C1\u5224\u7EFC\u5408\u8BC4\u5BA1\u9636\u6BB5\u3002"
443
451
  };
444
452
  function makeT(dict, fallback) {
445
453
  return function t(key, vars) {
@@ -2354,6 +2362,7 @@ window.__ModuleLoader__.load({
2354
2362
  )
2355
2363
  )
2356
2364
  ),
2365
+ // --- src/client/86-card-aggregator-ext2.js ---
2357
2366
  /* Pre-Judge Local Syntax Gate (#303) & Early Semantic Consensus (#299) */
2358
2367
  React.createElement(
2359
2368
  "div",
@@ -2559,6 +2568,48 @@ window.__ModuleLoader__.load({
2559
2568
  )
2560
2569
  )
2561
2570
  ),
2571
+ /* Stack-Aware Dynamic Persona Evolution (#308) */
2572
+ React.createElement(
2573
+ "div",
2574
+ { style: { display: "flex", flexDirection: "column", gap: 6, padding: "8px 12px", background: "var(--dsw-alias-bg-layer-2)", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", marginTop: 8 } },
2575
+ React.createElement(
2576
+ "label",
2577
+ { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
2578
+ React.createElement("input", {
2579
+ type: "checkbox",
2580
+ checked: Boolean(currentPreset.stack_aware_personas_enabled),
2581
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, stack_aware_personas_enabled: e.target.checked })),
2582
+ style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
2583
+ }),
2584
+ t("aggregator.stack_personas_title") || "\u{1F4E6} Stack-Aware Dynamic Persona Evolution (Project Manifests)"
2585
+ ),
2586
+ React.createElement(
2587
+ "div",
2588
+ { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
2589
+ t("aggregator.stack_personas_desc") || "Scans project manifests (package.json, Cargo.toml, pyproject.toml, go.mod) and assigns specialized engineering roles to candidate slots."
2590
+ )
2591
+ ),
2592
+ /* Self-Learning Developer Preferences Metaprompt (#309) */
2593
+ React.createElement(
2594
+ "div",
2595
+ { style: { display: "flex", flexDirection: "column", gap: 6, padding: "8px 12px", background: "var(--dsw-alias-bg-layer-2)", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", marginTop: 8 } },
2596
+ React.createElement(
2597
+ "label",
2598
+ { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
2599
+ React.createElement("input", {
2600
+ type: "checkbox",
2601
+ checked: Boolean(currentPreset.developer_preferences_enabled),
2602
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, developer_preferences_enabled: e.target.checked })),
2603
+ style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
2604
+ }),
2605
+ t("aggregator.preferences_title") || "\u{1F9E0} Developer Coding Principles & Preferences Metaprompt"
2606
+ ),
2607
+ React.createElement(
2608
+ "div",
2609
+ { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
2610
+ t("aggregator.preferences_desc") || "Injects learned developer preferences (strict typing, functional immutability, error boundaries) to guide Judge synthesis."
2611
+ )
2612
+ ),
2562
2613
  /* Fallback Judges Chain */
2563
2614
  React.createElement(
2564
2615
  "div",
package/lib/events.js CHANGED
@@ -9,6 +9,8 @@ export const MOA_EVENTS = {
9
9
  EARLY_CONSENSUS: 'moa/early-consensus',
10
10
  JUDGE_ESCALATION: 'moa/judge-escalation',
11
11
  SELF_CONSISTENCY_EXPAND: 'moa/self-consistency-expand',
12
+ STACK_DETECTED: 'moa/stack-detected',
13
+ PREFERENCES_APPLIED: 'moa/preferences-applied',
12
14
  CRITIQUE_COMPLETE: 'moa/critique-complete',
13
15
  JUDGE_VERDICT: 'moa/judge-verdict',
14
16
  CONTEXT_PRUNING: 'moa/context-pruning',
package/lib/moa-schema.js CHANGED
@@ -89,6 +89,8 @@ export const PresetSchema = z.object({
89
89
  escalation_policy: z.string().default('single'),
90
90
  escalation_disagreement_threshold: z.number().default(0.40),
91
91
  escalation_judge: ModelSlotSchema.default({ provider: '', model: '' }),
92
+ stack_aware_personas_enabled: z.boolean().default(false),
93
+ developer_preferences_enabled: z.boolean().default(false),
92
94
  })
93
95
 
94
96
  export const Config = z.object({
@@ -124,6 +126,8 @@ export const Config = z.object({
124
126
  escalation_policy: z.string().default('single').volatile(),
125
127
  escalation_disagreement_threshold: z.number().default(0.40).volatile(),
126
128
  escalation_judge: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
129
+ stack_aware_personas_enabled: z.boolean().default(false).volatile(),
130
+ developer_preferences_enabled: z.boolean().default(false).volatile(),
127
131
  smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
128
132
  prices: z.dict(PriceRow).default({}).volatile(),
129
133
  presets: z.array(PresetSchema).default([
@@ -1,3 +1,5 @@
1
+ import { detectProjectStack, assignStackAwarePersonas, formatStackAwareDirective } from './manifest-stack.js'
2
+ import { formatPreferencesDirective } from './developer-preferences.js'
1
3
  import { expandSelfConsistencySlots } from './self-consistency.js'
2
4
  import { emitEvent, MOA_EVENTS } from '../events.js'
3
5
  import { balanceCandidateQuotas } from '../moa-quota-balancer.js'
@@ -81,6 +83,34 @@ export async function stageProjectContext({
81
83
  const semanticCacheEnabled = Boolean(preset?.semantic_cache_enabled)
82
84
  const semanticCacheTtlHours = typeof preset?.semantic_cache_ttl_hours === 'number' ? preset.semantic_cache_ttl_hours : 168
83
85
 
86
+ const stackInfo = detectProjectStack(cwd)
87
+ if (stackInfo.manifestFound) {
88
+ emitEvent(ctx, MOA_EVENTS.STACK_DETECTED, {
89
+ primaryLanguage: stackInfo.primaryLanguage,
90
+ frameworks: stackInfo.frameworks,
91
+ libraries: stackInfo.libraries,
92
+ stackSummary: stackInfo.stackSummary,
93
+ })
94
+ if (typeof onProgress === 'function' && stackInfo.stackSummary) {
95
+ onProgress(`📦 *Project Stack: Detected ${stackInfo.stackSummary}*\n`)
96
+ }
97
+ }
98
+
99
+ const stackAwarePersonasEnabled = Boolean(preset?.stack_aware_personas_enabled)
100
+ if (stackAwarePersonasEnabled && !selfConsistencyExpanded) {
101
+ const stackRes = assignStackAwarePersonas(effectiveReferenceModels, stackInfo, { enabled: true })
102
+ if (stackRes.isSpecialized) {
103
+ effectiveReferenceModels = stackRes.references
104
+ }
105
+ }
106
+
107
+ const developerPreferencesEnabled = Boolean(preset?.developer_preferences_enabled)
108
+ const developerPreferencesDirective = formatPreferencesDirective(null, { enabled: developerPreferencesEnabled })
109
+ if (developerPreferencesDirective) {
110
+ emitEvent(ctx, MOA_EVENTS.PREFERENCES_APPLIED, { enabled: true })
111
+ }
112
+ const stackAwareDirective = formatStackAwareDirective(stackInfo)
113
+
84
114
  const collectedCtx = await collectProjectContext(cwd, 16000)
85
115
  const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
86
116
  let projectContext = ''
@@ -170,6 +200,7 @@ export async function stageProjectContext({
170
200
  semanticCacheEnabled, semanticCacheTtlHours,
171
201
  selfConsistencyEnabled, selfConsistencyExpanded, selfConsistencySamples, selfConsistencyTemperatures,
172
202
  escalationPolicy, escalationDisagreementThreshold, escalationJudge,
203
+ stackInfo, stackAwareDirective, developerPreferencesDirective,
173
204
  }
174
205
  }
175
206
 
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Self-Learning Developer Preferences Metaprompt (#309)
3
+ *
4
+ * Tracks developer coding principles, architectural standards, and preferred idioms
5
+ * in a persistent local store (~/.dsh/moa/preferences.json).
6
+ * Injects a concise "Developer Coding Principles" directive into the Judge
7
+ * synthesis phase (and candidate prompts) to bias consensus toward the engineer's tastes.
8
+ */
9
+
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+ import os from 'node:os'
13
+
14
+ export const DEFAULT_PREFERENCES = Object.freeze([
15
+ {
16
+ id: 'strict-typing',
17
+ title: 'Strict Types & Explicit Contracts',
18
+ rule: "Never use loose 'any' types or unchecked type casts. Explicitly define interfaces, return types, and input validation schemas.",
19
+ category: 'typing',
20
+ enabled: true,
21
+ },
22
+ {
23
+ id: 'functional-purity',
24
+ title: 'Functional Immutability',
25
+ rule: 'Prefer pure functions, immutable data transforms, and explicit return values over hidden global state mutations.',
26
+ category: 'architecture',
27
+ enabled: true,
28
+ },
29
+ {
30
+ id: 'defensive-errors',
31
+ title: 'Defensive Error Handling',
32
+ rule: 'Never swallow exceptions silently. Use structured error types, descriptive messages, and clean fallback strategies.',
33
+ category: 'reliability',
34
+ enabled: true,
35
+ },
36
+ ])
37
+
38
+ /**
39
+ * Resolves the persistent preferences file path.
40
+ *
41
+ * @param {string} [customPath]
42
+ * @returns {string}
43
+ */
44
+ export function getPreferencesFilePath(customPath = null) {
45
+ if (customPath && typeof customPath === 'string' && customPath.trim()) {
46
+ return path.resolve(customPath.trim())
47
+ }
48
+ return path.join(os.homedir(), '.dsh', 'moa', 'preferences.json')
49
+ }
50
+
51
+ /**
52
+ * Loads developer preferences from disk with default fallback.
53
+ *
54
+ * @param {string} [customPath]
55
+ * @returns {Array<object>}
56
+ */
57
+ export function loadDeveloperPreferences(customPath = null) {
58
+ const filePath = getPreferencesFilePath(customPath)
59
+ if (!fs.existsSync(filePath)) {
60
+ // Seed default preferences file
61
+ try {
62
+ const dir = path.dirname(filePath)
63
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
64
+ fs.writeFileSync(filePath, JSON.stringify(DEFAULT_PREFERENCES, null, 2), 'utf8')
65
+ } catch {
66
+ // safe fallback if filesystem is read-only
67
+ }
68
+ return [...DEFAULT_PREFERENCES]
69
+ }
70
+
71
+ try {
72
+ const raw = fs.readFileSync(filePath, 'utf8')
73
+ const parsed = JSON.parse(raw)
74
+ if (Array.isArray(parsed)) {
75
+ return parsed.filter((p) => p && typeof p === 'object' && p.id && p.rule)
76
+ }
77
+ } catch {
78
+ // safe fallback
79
+ }
80
+
81
+ return [...DEFAULT_PREFERENCES]
82
+ }
83
+
84
+ /**
85
+ * Atomically saves developer preferences to disk.
86
+ *
87
+ * @param {Array<object>} preferences
88
+ * @param {string} [customPath]
89
+ * @returns {boolean}
90
+ */
91
+ export function saveDeveloperPreferences(preferences, customPath = null) {
92
+ if (!Array.isArray(preferences)) return false
93
+ const filePath = getPreferencesFilePath(customPath)
94
+
95
+ try {
96
+ const dir = path.dirname(filePath)
97
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
98
+ const tmp = `${filePath}.tmp.${Date.now()}`
99
+ fs.writeFileSync(tmp, JSON.stringify(preferences, null, 2), 'utf8')
100
+ fs.renameSync(tmp, filePath)
101
+ return true
102
+ } catch {
103
+ return false
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Adds a new developer preference rule.
109
+ *
110
+ * @param {object} item - { title, rule, category, enabled }
111
+ * @param {string} [customPath]
112
+ * @returns {object|null}
113
+ */
114
+ export function addDeveloperPreference(item = {}, customPath = null) {
115
+ if (!item || !item.rule || typeof item.rule !== 'string') return null
116
+ const current = loadDeveloperPreferences(customPath)
117
+
118
+ const id = item.id || `pref-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
119
+ const newPref = {
120
+ id,
121
+ title: (item.title && String(item.title).trim()) || 'Custom Coding Standard',
122
+ rule: String(item.rule).trim(),
123
+ category: (item.category && String(item.category).trim()) || 'general',
124
+ enabled: item.enabled !== false,
125
+ createdAt: new Date().toISOString(),
126
+ }
127
+
128
+ current.push(newPref)
129
+ saveDeveloperPreferences(current, customPath)
130
+ return newPref
131
+ }
132
+
133
+ /**
134
+ * Updates an existing developer preference.
135
+ *
136
+ * @param {string} id
137
+ * @param {object} updates
138
+ * @param {string} [customPath]
139
+ * @returns {object|null}
140
+ */
141
+ export function updateDeveloperPreference(id, updates = {}, customPath = null) {
142
+ if (!id) return null
143
+ const current = loadDeveloperPreferences(customPath)
144
+ const idx = current.findIndex((p) => p.id === id)
145
+ if (idx === -1) return null
146
+
147
+ const target = current[idx]
148
+ if (typeof updates.title === 'string') target.title = updates.title.trim()
149
+ if (typeof updates.rule === 'string') target.rule = updates.rule.trim()
150
+ if (typeof updates.category === 'string') target.category = updates.category.trim()
151
+ if (typeof updates.enabled === 'boolean') target.enabled = updates.enabled
152
+
153
+ saveDeveloperPreferences(current, customPath)
154
+ return target
155
+ }
156
+
157
+ /**
158
+ * Deletes a developer preference by ID.
159
+ *
160
+ * @param {string} id
161
+ * @param {string} [customPath]
162
+ * @returns {boolean}
163
+ */
164
+ export function deleteDeveloperPreference(id, customPath = null) {
165
+ if (!id) return false
166
+ const current = loadDeveloperPreferences(customPath)
167
+ const filtered = current.filter((p) => p.id !== id)
168
+ if (filtered.length === current.length) return false
169
+
170
+ return saveDeveloperPreferences(filtered, customPath)
171
+ }
172
+
173
+ /**
174
+ * Formats active developer preferences into an injected prompt directive.
175
+ *
176
+ * @param {Array<object>} [preferences] - Array of preferences (loads from disk if omitted)
177
+ * @param {object} [options]
178
+ * @param {boolean} [options.enabled=true]
179
+ * @returns {string|null}
180
+ */
181
+ export function formatPreferencesDirective(preferences = null, options = {}) {
182
+ const { enabled = true, customPath = null } = options
183
+ if (!enabled) return null
184
+
185
+ const list = Array.isArray(preferences) ? preferences : loadDeveloperPreferences(customPath)
186
+ const active = list.filter((p) => p && p.enabled !== false && p.rule)
187
+
188
+ if (active.length === 0) return null
189
+
190
+ const lines = active.map((p) => `- [${p.title}]: ${p.rule}`)
191
+
192
+ return `### Developer Coding Principles & Preferred Conventions
193
+ The developer has established the following coding standards for this project:
194
+ ${lines.join('\n')}
195
+ Evaluate all proposals against these principles and favor implementations adhering to them.`
196
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Stack-Aware Dynamic Persona Evolution from Project Manifests (#308)
3
+ *
4
+ * Scans project manifests (package.json, Cargo.toml, pyproject.toml, requirements.txt, go.mod)
5
+ * in the workspace root to infer primary programming languages, frameworks, ORMs,
6
+ * and test suites. Evolves candidate proposers into specialized domain experts
7
+ * (e.g. React Optimization, TypeScript Strict Safety, Accessibility, FastAPI Async)
8
+ * to maximize architectural hypothesis diversity.
9
+ */
10
+
11
+ import fs from 'node:fs'
12
+ import path from 'node:path'
13
+
14
+ /**
15
+ * Scans workspace root manifests to detect project languages and dependencies.
16
+ *
17
+ * @param {string} cwd - Workspace root directory
18
+ * @returns {object} Detected stack information
19
+ */
20
+ export function detectProjectStack(cwd = process.cwd()) {
21
+ const stack = {
22
+ primaryLanguage: 'unknown',
23
+ frameworks: [],
24
+ libraries: [],
25
+ manifestFound: false,
26
+ stackSummary: '',
27
+ }
28
+
29
+ if (!cwd || typeof cwd !== 'string') return stack
30
+
31
+ // 1. Node / JavaScript / TypeScript (package.json)
32
+ const pkgPath = path.join(cwd, 'package.json')
33
+ if (fs.existsSync(pkgPath)) {
34
+ try {
35
+ const raw = fs.readFileSync(pkgPath, 'utf8')
36
+ const pkg = JSON.parse(raw)
37
+ stack.manifestFound = true
38
+ stack.primaryLanguage = 'javascript'
39
+
40
+ const allDeps = {
41
+ ...(pkg.dependencies || {}),
42
+ ...(pkg.devDependencies || {}),
43
+ ...(pkg.peerDependencies || {}),
44
+ }
45
+
46
+ if (allDeps.typescript || fs.existsSync(path.join(cwd, 'tsconfig.json'))) {
47
+ stack.primaryLanguage = 'typescript'
48
+ stack.libraries.push('typescript')
49
+ }
50
+
51
+ // Frameworks
52
+ if (allDeps.next) stack.frameworks.push('next.js')
53
+ else if (allDeps.react) stack.frameworks.push('react')
54
+
55
+ if (allDeps.nuxt) stack.frameworks.push('nuxt')
56
+ else if (allDeps.vue) stack.frameworks.push('vue')
57
+
58
+ if (allDeps['@sveltejs/kit']) stack.frameworks.push('sveltekit')
59
+ else if (allDeps.svelte) stack.frameworks.push('svelte')
60
+
61
+ if (allDeps['@angular/core']) stack.frameworks.push('angular')
62
+ if (allDeps['@nestjs/core']) stack.frameworks.push('nestjs')
63
+ if (allDeps.fastify) stack.frameworks.push('fastify')
64
+ if (allDeps.express) stack.frameworks.push('express')
65
+
66
+ // Styling & UI
67
+ if (allDeps.tailwindcss) stack.libraries.push('tailwindcss')
68
+
69
+ // ORM & Data
70
+ if (allDeps.prisma || allDeps['@prisma/client']) stack.libraries.push('prisma')
71
+ if (allDeps['drizzle-orm']) stack.libraries.push('drizzle')
72
+ if (allDeps.typeorm) stack.libraries.push('typeorm')
73
+ if (allDeps.mongoose) stack.libraries.push('mongoose')
74
+
75
+ // Validation
76
+ if (allDeps.zod) stack.libraries.push('zod')
77
+ if (allDeps.valibot) stack.libraries.push('valibot')
78
+
79
+ // Testing
80
+ if (allDeps.vitest) stack.libraries.push('vitest')
81
+ if (allDeps.jest) stack.libraries.push('jest')
82
+ if (allDeps.playwright || allDeps['@playwright/test']) stack.libraries.push('playwright')
83
+ } catch {
84
+ // safe fallback on corrupted package.json
85
+ }
86
+ }
87
+
88
+ // 2. Rust (Cargo.toml)
89
+ const cargoPath = path.join(cwd, 'Cargo.toml')
90
+ if (fs.existsSync(cargoPath)) {
91
+ try {
92
+ const content = fs.readFileSync(cargoPath, 'utf8')
93
+ stack.manifestFound = true
94
+ stack.primaryLanguage = 'rust'
95
+
96
+ if (content.includes('axum')) stack.frameworks.push('axum')
97
+ if (content.includes('actix-web')) stack.frameworks.push('actix-web')
98
+ if (content.includes('tokio')) stack.libraries.push('tokio')
99
+ if (content.includes('serde')) stack.libraries.push('serde')
100
+ if (content.includes('sqlx')) stack.libraries.push('sqlx')
101
+ if (content.includes('diesel')) stack.libraries.push('diesel')
102
+ } catch {
103
+ // safe fallback
104
+ }
105
+ }
106
+
107
+ // 3. Python (pyproject.toml, requirements.txt)
108
+ const pyprojectPath = path.join(cwd, 'pyproject.toml')
109
+ const reqPath = path.join(cwd, 'requirements.txt')
110
+ if (fs.existsSync(pyprojectPath) || fs.existsSync(reqPath)) {
111
+ try {
112
+ const pyContent = (fs.existsSync(pyprojectPath) ? fs.readFileSync(pyprojectPath, 'utf8') : '') +
113
+ '\n' + (fs.existsSync(reqPath) ? fs.readFileSync(reqPath, 'utf8') : '')
114
+ stack.manifestFound = true
115
+ if (stack.primaryLanguage === 'unknown') stack.primaryLanguage = 'python'
116
+
117
+ if (pyContent.includes('fastapi')) stack.frameworks.push('fastapi')
118
+ if (pyContent.includes('django')) stack.frameworks.push('django')
119
+ if (pyContent.includes('flask')) stack.frameworks.push('flask')
120
+ if (pyContent.includes('pydantic')) stack.libraries.push('pydantic')
121
+ if (pyContent.includes('sqlalchemy')) stack.libraries.push('sqlalchemy')
122
+ if (pyContent.includes('pytest')) stack.libraries.push('pytest')
123
+ if (pyContent.includes('torch')) stack.libraries.push('pytorch')
124
+ if (pyContent.includes('pandas')) stack.libraries.push('pandas')
125
+ } catch {
126
+ // safe fallback
127
+ }
128
+ }
129
+
130
+ // 4. Go (go.mod)
131
+ const goModPath = path.join(cwd, 'go.mod')
132
+ if (fs.existsSync(goModPath)) {
133
+ try {
134
+ const goContent = fs.readFileSync(goModPath, 'utf8')
135
+ stack.manifestFound = true
136
+ if (stack.primaryLanguage === 'unknown') stack.primaryLanguage = 'go'
137
+
138
+ if (goContent.includes('gin-gonic/gin')) stack.frameworks.push('gin')
139
+ if (goContent.includes('gofiber/fiber')) stack.frameworks.push('fiber')
140
+ if (goContent.includes('labstack/echo')) stack.frameworks.push('echo')
141
+ if (goContent.includes('gorm.io/gorm')) stack.libraries.push('gorm')
142
+ if (goContent.includes('stretchr/testify')) stack.libraries.push('testify')
143
+ } catch {
144
+ // safe fallback
145
+ }
146
+ }
147
+
148
+ // Build summary string
149
+ const parts = []
150
+ if (stack.primaryLanguage !== 'unknown') parts.push(stack.primaryLanguage)
151
+ if (stack.frameworks.length > 0) parts.push(`framework: ${stack.frameworks.join(', ')}`)
152
+ if (stack.libraries.length > 0) parts.push(`libs: ${stack.libraries.slice(0, 4).join(', ')}`)
153
+ stack.stackSummary = parts.join(' | ')
154
+
155
+ return stack
156
+ }
157
+
158
+ /**
159
+ * Returns specialized domain personas based on detected stack.
160
+ *
161
+ * @param {object} stackInfo
162
+ * @returns {Array<string>} Array of specialized persona role descriptions
163
+ */
164
+ export function getStackPersonaSpecialties(stackInfo = {}) {
165
+ const lang = stackInfo.primaryLanguage || 'unknown'
166
+ const frameworks = Array.isArray(stackInfo.frameworks) ? stackInfo.frameworks : []
167
+ const libs = Array.isArray(stackInfo.libraries) ? stackInfo.libraries : []
168
+
169
+ // React / Next.js / Frontend
170
+ if (frameworks.includes('react') || frameworks.includes('next.js') || frameworks.includes('vue') || frameworks.includes('svelte')) {
171
+ return [
172
+ 'Component Architecture, Hooks & Re-render Optimization Specialist',
173
+ libs.includes('typescript') || lang === 'typescript'
174
+ ? 'TypeScript Strict Typing, Generic Invariants & Defensive Schema Expert'
175
+ : 'State Management, Data Flow & Edge-Case Specialist',
176
+ 'Accessibility (a11y), Semantic UI & Modern CSS Performance Reviewer',
177
+ ]
178
+ }
179
+
180
+ // Python / FastAPI / Django
181
+ if (lang === 'python') {
182
+ return [
183
+ frameworks.includes('fastapi')
184
+ ? 'FastAPI Async Concurrency & High-Throughput REST Architect'
185
+ : 'Pythonic Clean Architecture & Idiomatic Object Design Specialist',
186
+ libs.includes('pydantic')
187
+ ? 'Pydantic V2 Schema Validation & Type-Safe Domain Modeler'
188
+ : 'Defensive Data Integrity & Error Boundary Engineer',
189
+ libs.includes('pytest')
190
+ ? 'Pytest Fixtures, Mocking & Comprehensive Edge-Case Test Engineer'
191
+ : 'Performance Profiling & Memory Leak Auditor',
192
+ ]
193
+ }
194
+
195
+ // Rust
196
+ if (lang === 'rust') {
197
+ return [
198
+ 'Rust Zero-Cost Abstractions & Concurrency Architect',
199
+ 'Rust Memory Safety, Error Handling & Lifetime Specialist',
200
+ 'Rust Idiomatic API & Performance Specialist',
201
+ ]
202
+ }
203
+
204
+ // Go
205
+ if (lang === 'go') {
206
+ return [
207
+ 'Go Idiomatic Concurrency & Channel Synchronization Architect',
208
+ 'Go Interface Design, Error Wrapping & Zero-Allocation Specialist',
209
+ 'Go Production Reliability & Table-Driven Unit Testing Specialist',
210
+ ]
211
+ }
212
+
213
+ // General TypeScript / Node
214
+ if (lang === 'typescript') {
215
+ return [
216
+ 'TypeScript Strict Type Safety & Clean Architecture Specialist',
217
+ 'Async Flow, Micro-Optimization & Error Boundary Engineer',
218
+ 'Maintainability, Modularity & Test Coverage Auditor',
219
+ ]
220
+ }
221
+
222
+ // Universal Default
223
+ return [
224
+ 'System Architecture & Core Correctness Engineer',
225
+ 'Edge-Case Analysis & Defensive Error Handling Specialist',
226
+ 'Clean Code, Maintainability & Documentation Specialist',
227
+ ]
228
+ }
229
+
230
+ /**
231
+ * Assigns stack-aware specialized personas to candidate proposer slots.
232
+ *
233
+ * @param {Array<object>} references - Proposer slots
234
+ * @param {object} stackInfo - Output of detectProjectStack
235
+ * @param {object} [options]
236
+ * @param {boolean} [options.enabled=true] - Whether stack-aware personas are enabled
237
+ * @returns {{ references: Array<object>, isSpecialized: boolean, stackSummary: string }}
238
+ */
239
+ export function assignStackAwarePersonas(references = [], stackInfo = {}, options = {}) {
240
+ const { enabled = true } = options
241
+
242
+ if (!enabled || !Array.isArray(references) || references.length === 0) {
243
+ return {
244
+ references: Array.isArray(references) ? references : [],
245
+ isSpecialized: false,
246
+ stackSummary: stackInfo.stackSummary || '',
247
+ }
248
+ }
249
+
250
+ const specialties = getStackPersonaSpecialties(stackInfo)
251
+ const evolved = references.map((slot, idx) => {
252
+ // If user already specified a custom non-default persona, preserve it
253
+ const hasCustom = slot.role_persona && slot.role_persona !== 'general' && slot.role_persona !== 'proposer'
254
+ if (hasCustom) {
255
+ return { ...slot }
256
+ }
257
+
258
+ const specialty = specialties[idx % specialties.length]
259
+ return {
260
+ ...slot,
261
+ role_persona: specialty,
262
+ stackSpecialization: specialty,
263
+ }
264
+ })
265
+
266
+ return {
267
+ references: evolved,
268
+ isSpecialized: Boolean(stackInfo.manifestFound),
269
+ stackSummary: stackInfo.stackSummary || '',
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Formats a directive summarizing the project stack for injection into judge or synthesis prompts.
275
+ *
276
+ * @param {object} stackInfo
277
+ * @returns {string|null}
278
+ */
279
+ export function formatStackAwareDirective(stackInfo = {}) {
280
+ if (!stackInfo || !stackInfo.manifestFound || !stackInfo.stackSummary) {
281
+ return null
282
+ }
283
+
284
+ return `### Project Stack & Architecture Context
285
+ - Detected Environment: ${stackInfo.stackSummary}
286
+ - Verification Requirement: All proposals must adhere to the conventions and idiomatic idioms of this technology stack.`
287
+ }
@@ -307,7 +307,7 @@ export async function runMoAPipeline({
307
307
  }
308
308
  }
309
309
 
310
- const fileScoringDirective = [rawFileDirective, visualResult?.directive, adversarialTddResult?.directive, syntaxDirective, pruningDirective].filter(Boolean).join("\n\n") || null
310
+ const fileScoringDirective = [rawFileDirective, visualResult?.directive, adversarialTddResult?.directive, syntaxDirective, pruningDirective, ctxStage.stackAwareDirective, ctxStage.developerPreferencesDirective].filter(Boolean).join("\n\n") || null
311
311
 
312
312
  // Stage 7: Synthesis Phase
313
313
  const synthRes = await stageSynthesis({
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Developer Preferences Management Routes (#309)
3
+ * Exposes endpoints to inspect, add, toggle, and delete developer coding preferences.
4
+ */
5
+
6
+ import {
7
+ loadDeveloperPreferences,
8
+ addDeveloperPreference,
9
+ updateDeveloperPreference,
10
+ deleteDeveloperPreference,
11
+ } from '../pipeline/developer-preferences.js'
12
+ import { writeJson, readBody, assertAuthorizedRequest } from '../access.js'
13
+ import { isSafeWriteRequest } from '../updater.js'
14
+
15
+ export function registerPreferencesRoutes(ctx) {
16
+ // 1. Exact route: /dsh-moa/preferences
17
+ ctx.effect(() => {
18
+ return ctx.webServer.register({
19
+ kind: 'exact',
20
+ path: '/dsh-moa/preferences',
21
+ handler: async (req, res) => {
22
+ if (!assertAuthorizedRequest(req, res, ctx)) return
23
+
24
+ if (req.method === 'GET') {
25
+ const preferences = loadDeveloperPreferences()
26
+ return writeJson(res, 200, { ok: true, preferences })
27
+ }
28
+
29
+ if (req.method === 'POST') {
30
+ if (!isSafeWriteRequest(req)) {
31
+ return writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
32
+ }
33
+
34
+ let body = {}
35
+ try {
36
+ const raw = await readBody(req)
37
+ if (raw) body = JSON.parse(raw)
38
+ } catch (_err) {
39
+ return writeJson(res, 400, { ok: false, error: 'Malformed JSON payload' })
40
+ }
41
+
42
+ if (body.action === 'delete' && body.id) {
43
+ const deleted = deleteDeveloperPreference(body.id)
44
+ return writeJson(res, 200, { ok: true, deleted, id: body.id })
45
+ }
46
+
47
+ if (body.action === 'update' && body.id) {
48
+ const updated = updateDeveloperPreference(body.id, body.updates || body)
49
+ if (!updated) return writeJson(res, 404, { ok: false, error: 'Preference not found' })
50
+ return writeJson(res, 200, { ok: true, preference: updated })
51
+ }
52
+
53
+ const created = addDeveloperPreference(body)
54
+ if (!created) {
55
+ return writeJson(res, 400, { ok: false, error: 'Missing required rule field' })
56
+ }
57
+ return writeJson(res, 201, { ok: true, preference: created })
58
+ }
59
+
60
+ return writeJson(res, 405, { ok: false, error: 'GET or POST required' })
61
+ },
62
+ })
63
+ })
64
+
65
+ // 2. Prefix route: /dsh-moa/preferences/ for REST ID operations
66
+ ctx.effect(() => {
67
+ return ctx.webServer.register({
68
+ kind: 'prefix',
69
+ path: '/dsh-moa/preferences/',
70
+ handler: async (req, res) => {
71
+ if (!assertAuthorizedRequest(req, res, ctx)) return
72
+
73
+ const url = new URL(req.url, 'http://127.0.0.1')
74
+ const id = url.pathname.replace(/^\/dsh-moa\/preferences\//, '').trim()
75
+
76
+ if (!id) {
77
+ return writeJson(res, 400, { ok: false, error: 'Missing preference ID in URL path' })
78
+ }
79
+
80
+ if (req.method === 'DELETE') {
81
+ if (!isSafeWriteRequest(req)) {
82
+ return writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
83
+ }
84
+ const deleted = deleteDeveloperPreference(id)
85
+ return writeJson(res, 200, { ok: true, deleted, id })
86
+ }
87
+
88
+ if (req.method === 'PATCH' || req.method === 'POST') {
89
+ if (!isSafeWriteRequest(req)) {
90
+ return writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
91
+ }
92
+ let body = {}
93
+ try {
94
+ const raw = await readBody(req)
95
+ if (raw) body = JSON.parse(raw)
96
+ } catch {
97
+ return writeJson(res, 400, { ok: false, error: 'Malformed JSON payload' })
98
+ }
99
+ const updated = updateDeveloperPreference(id, body)
100
+ if (!updated) return writeJson(res, 404, { ok: false, error: 'Preference not found' })
101
+ return writeJson(res, 200, { ok: true, preference: updated })
102
+ }
103
+
104
+ return writeJson(res, 405, { ok: false, error: 'DELETE or PATCH required' })
105
+ },
106
+ })
107
+ })
108
+ }
package/lib/routes.js CHANGED
@@ -8,12 +8,14 @@ import { registerHistoryRoutes } from './routes/history.js'
8
8
  import { registerWorkbenchRoutes } from './routes/workbench.js'
9
9
  import { registerPipelineRoutes } from './routes/pipeline.js'
10
10
  import { registerCacheRoutes } from './routes/cache.js'
11
+ import { registerPreferencesRoutes } from './routes/preferences.js'
11
12
 
12
13
  export { registerPresetRoutes } from './routes/presets.js'
13
14
  export { registerHistoryRoutes } from './routes/history.js'
14
15
  export { registerWorkbenchRoutes } from './routes/workbench.js'
15
16
  export { registerPipelineRoutes } from './routes/pipeline.js'
16
17
  export { registerCacheRoutes } from './routes/cache.js'
18
+ export { registerPreferencesRoutes } from './routes/preferences.js'
17
19
 
18
20
  export function registerMoaRoutes(ctx, deps) {
19
21
  registerPresetRoutes(ctx, deps)
@@ -21,4 +23,5 @@ export function registerMoaRoutes(ctx, deps) {
21
23
  registerWorkbenchRoutes(ctx, deps)
22
24
  registerPipelineRoutes(ctx, deps)
23
25
  registerCacheRoutes(ctx, deps)
26
+ registerPreferencesRoutes(ctx, deps)
24
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.74",
3
+ "version": "0.2.75",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",