@geraldmaron/construct 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/agents/prompts/cx-ai-engineer.md +6 -26
- package/agents/prompts/cx-architect.md +1 -0
- package/agents/prompts/cx-business-strategist.md +2 -0
- package/agents/prompts/cx-data-analyst.md +6 -26
- package/agents/prompts/cx-docs-keeper.md +1 -31
- package/agents/prompts/cx-explorer.md +1 -0
- package/agents/prompts/cx-orchestrator.md +40 -112
- package/agents/prompts/cx-platform-engineer.md +2 -22
- package/agents/prompts/cx-product-manager.md +2 -1
- package/agents/prompts/cx-qa.md +0 -20
- package/agents/prompts/cx-rd-lead.md +2 -0
- package/agents/prompts/cx-researcher.md +77 -31
- package/agents/prompts/cx-security.md +11 -49
- package/agents/prompts/cx-sre.md +9 -43
- package/agents/prompts/cx-ux-researcher.md +1 -0
- package/agents/role-manifests.json +4 -4
- package/bin/construct +23 -0
- package/db/schema/004_recommendations.sql +46 -0
- package/db/schema/005_strategy.sql +21 -0
- package/lib/auto-docs.mjs +1 -2
- package/lib/beads-automation.mjs +16 -7
- package/lib/cli-commands.mjs +8 -2
- package/lib/embed/conflict-detection.mjs +26 -9
- package/lib/embed/customer-profiles.mjs +37 -17
- package/lib/embed/daemon.mjs +10 -8
- package/lib/embed/recommendation-store.mjs +213 -14
- package/lib/embed/workspaces.mjs +53 -18
- package/lib/gates-audit.mjs +3 -3
- package/lib/health-check.mjs +1 -1
- package/lib/hooks/pre-compact.mjs +3 -0
- package/lib/hooks/read-tracker.mjs +10 -101
- package/lib/host-capabilities.mjs +90 -1
- package/lib/init-update.mjs +246 -131
- package/lib/intent-classifier.mjs +1 -0
- package/lib/knowledge/layout.mjs +10 -0
- package/lib/mcp/tools/telemetry.mjs +30 -78
- package/lib/model-router.mjs +61 -1
- package/lib/ollama-manager.mjs +1 -1
- package/lib/opencode-telemetry.mjs +4 -5
- package/lib/orchestration-policy.mjs +9 -0
- package/lib/parity.mjs +124 -21
- package/lib/prompt-composer.js +106 -29
- package/lib/read-tracker-store.mjs +149 -0
- package/lib/server/index.mjs +76 -0
- package/lib/server/telemetry-login.mjs +17 -25
- package/lib/service-manager.mjs +30 -22
- package/lib/services/local-postgres.mjs +15 -0
- package/lib/services/telemetry-backend.mjs +1 -2
- package/lib/setup.mjs +8 -43
- package/lib/status.mjs +51 -5
- package/lib/storage/backend.mjs +12 -2
- package/lib/strategy-store.mjs +371 -0
- package/lib/telemetry/backends/local.mjs +6 -4
- package/lib/telemetry/client.mjs +185 -0
- package/lib/telemetry/ingest.mjs +13 -5
- package/lib/telemetry/team-rollup.mjs +9 -2
- package/lib/worker/trace.mjs +17 -27
- package/package.json +5 -2
- package/rules/common/research.md +44 -12
- package/skills/docs/backlog-proposal-workflow.md +2 -2
- package/skills/docs/customer-profile-workflow.md +1 -1
- package/skills/docs/evidence-ingest-workflow.md +5 -5
- package/skills/docs/prfaq-workflow.md +1 -1
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +3 -3
- package/skills/docs/product-signal-workflow.md +48 -18
- package/skills/docs/research-workflow.md +26 -14
- package/skills/docs/strategy-workflow.md +36 -0
- package/skills/roles/data-analyst.product-intelligence.md +1 -1
- package/skills/roles/researcher.md +28 -15
- package/skills/routing.md +8 -1
- package/templates/docs/research-brief.md +63 -9
- package/templates/docs/strategy.md +36 -0
- package/templates/homebrew/construct.rb +6 -6
package/lib/model-router.mjs
CHANGED
|
@@ -45,6 +45,61 @@ export const MODEL_TIER_BY_WORK_CATEGORY = {
|
|
|
45
45
|
analysis: "standard",
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
export const MODEL_OPERATING_PROFILES = Object.freeze({
|
|
49
|
+
balanced: {
|
|
50
|
+
id: 'balanced',
|
|
51
|
+
label: 'Balanced',
|
|
52
|
+
maxPromptTokens: 3000,
|
|
53
|
+
learnedPatternsTokens: 200,
|
|
54
|
+
taskPacketTokens: 150,
|
|
55
|
+
contextDigestTokens: 200,
|
|
56
|
+
hostConstraintsTokens: 75,
|
|
57
|
+
roleFlavorTokens: 600,
|
|
58
|
+
retrievalFirst: false,
|
|
59
|
+
preferCompressedRoleGuidance: false,
|
|
60
|
+
},
|
|
61
|
+
small: {
|
|
62
|
+
id: 'small',
|
|
63
|
+
label: 'Small-model',
|
|
64
|
+
maxPromptTokens: 1800,
|
|
65
|
+
learnedPatternsTokens: 120,
|
|
66
|
+
taskPacketTokens: 110,
|
|
67
|
+
contextDigestTokens: 120,
|
|
68
|
+
hostConstraintsTokens: 40,
|
|
69
|
+
roleFlavorTokens: 280,
|
|
70
|
+
retrievalFirst: true,
|
|
71
|
+
preferCompressedRoleGuidance: true,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function normalizeModelOperatingProfile(value) {
|
|
76
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
77
|
+
if (!normalized) return null;
|
|
78
|
+
if (normalized === 'default') return 'balanced';
|
|
79
|
+
return MODEL_OPERATING_PROFILES[normalized] ? normalized : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function inferSmallModelProfile(selectedModel) {
|
|
83
|
+
const model = String(selectedModel || '').toLowerCase();
|
|
84
|
+
if (!model) return false;
|
|
85
|
+
if (/^(ollama|local)\//.test(model) && /(?:[:/-])(3b|7b|8b|13b|14b|32b)\b/.test(model)) return true;
|
|
86
|
+
if (/^(anthropic|openrouter\/anthropic)\/.*haiku/.test(model)) return true;
|
|
87
|
+
if (/gpt-5\.1-mini|gemma-3|gemma-4|phi3:mini/.test(model)) return true;
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveModelOperatingProfile({
|
|
92
|
+
envValues = {},
|
|
93
|
+
selectedModel = null,
|
|
94
|
+
} = {}) {
|
|
95
|
+
const explicit = normalizeModelOperatingProfile(
|
|
96
|
+
envValues.CONSTRUCT_MODEL_PROFILE ?? envValues.constructModelProfile
|
|
97
|
+
);
|
|
98
|
+
if (explicit) return MODEL_OPERATING_PROFILES[explicit];
|
|
99
|
+
if (inferSmallModelProfile(selectedModel)) return MODEL_OPERATING_PROFILES.small;
|
|
100
|
+
return MODEL_OPERATING_PROFILES.balanced;
|
|
101
|
+
}
|
|
102
|
+
|
|
48
103
|
/**
|
|
49
104
|
* Provider-family definitions. Each entry contains:
|
|
50
105
|
* - `test`: RegExp that matches provider URLs.
|
|
@@ -229,7 +284,7 @@ const PROVIDER_ENV_MAP = {
|
|
|
229
284
|
'github-copilot': ['GITHUB_TOKEN', 'GH_TOKEN'],
|
|
230
285
|
'openai': ['OPENAI_API_KEY'],
|
|
231
286
|
'openrouter-llama': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
|
|
232
|
-
'ollama': ['OLLAMA_BASE_URL'],
|
|
287
|
+
'ollama': ['OLLAMA_BASE_URL', 'OLLAMA_HOST'],
|
|
233
288
|
'local': ['LOCAL_LLM_BASE_URL'],
|
|
234
289
|
};
|
|
235
290
|
|
|
@@ -630,6 +685,10 @@ export function resolveExecutionContractModelMetadata({
|
|
|
630
685
|
const tiers = resolveTierAssignments(envValues, registryModels);
|
|
631
686
|
const selectedTier = requestedTier ?? selectModelTierForWorkCategory(workCategory);
|
|
632
687
|
const selected = selectedTier ? tiers[selectedTier] : null;
|
|
688
|
+
const profile = resolveModelOperatingProfile({
|
|
689
|
+
envValues,
|
|
690
|
+
selectedModel: selected?.model ?? null,
|
|
691
|
+
});
|
|
633
692
|
|
|
634
693
|
return {
|
|
635
694
|
version: "v1",
|
|
@@ -638,6 +697,7 @@ export function resolveExecutionContractModelMetadata({
|
|
|
638
697
|
selectedTier: selectedTier ?? null,
|
|
639
698
|
selectedModel: selected?.model ?? null,
|
|
640
699
|
selectedModelSource: selected?.source ?? null,
|
|
700
|
+
profile,
|
|
641
701
|
tiers,
|
|
642
702
|
};
|
|
643
703
|
}
|
package/lib/ollama-manager.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { execSync } from 'node:child_process';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
12
|
|
|
13
|
-
const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
|
13
|
+
const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Check if Ollama is installed and running
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* trace/generation/span/event observations, and flushes on session.idle.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import {
|
|
8
|
+
import { createTelemetryClient } from "./telemetry/client.mjs";
|
|
9
9
|
|
|
10
10
|
const SECRET_PATH_PATTERNS = [
|
|
11
11
|
/(^|\/)\.env(\.|$)/i,
|
|
@@ -24,10 +24,9 @@ let cachedIngest = null;
|
|
|
24
24
|
|
|
25
25
|
export function getIngestClient(env = process.env) {
|
|
26
26
|
if (cachedIngest) return cachedIngest;
|
|
27
|
-
cachedIngest =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
secretKey: env.CONSTRUCT_TELEMETRY_SECRET_KEY,
|
|
27
|
+
cachedIngest = createTelemetryClient({
|
|
28
|
+
env,
|
|
29
|
+
rootDir: env.CX_TOOLKIT_DIR,
|
|
31
30
|
onError: (err) => {
|
|
32
31
|
if (env.CONSTRUCT_TRACE_DEBUG === "1") {
|
|
33
32
|
// eslint-disable-next-line no-console
|
|
@@ -324,6 +324,14 @@ export function classifyDataEngineerFlavor(request = '') {
|
|
|
324
324
|
return null;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
export function classifyEngineerFlavor(request = '') {
|
|
328
|
+
const text = String(request).toLowerCase();
|
|
329
|
+
if (containsAny(text, ['llm', 'agent', 'prompt', 'eval', 'hallucination', 'rag', 'model behavior', 'model routing'])) return 'ai';
|
|
330
|
+
if (containsAny(text, ['vector', 'embedding', 'retrieval', 'warehouse', 'etl', 'elt', 'pipeline', 'streaming', 'data contract'])) return 'data';
|
|
331
|
+
if (containsAny(text, ['ci', 'build', 'deploy', 'docker', 'kubernetes', 'terraform', 'helm', 'release pipeline', 'platform tooling', 'developer experience'])) return 'platform';
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
327
335
|
export function isDataAnalysisRequest(request = '') {
|
|
328
336
|
return Boolean(classifyDataAnalystFlavor(request));
|
|
329
337
|
}
|
|
@@ -406,6 +414,7 @@ export function isVisualDeliverableRequest(request = '') {
|
|
|
406
414
|
|
|
407
415
|
export function classifyRoleFlavors(request = '') {
|
|
408
416
|
return {
|
|
417
|
+
engineer: classifyEngineerFlavor(request),
|
|
409
418
|
architect: classifyArchitectFlavor(request),
|
|
410
419
|
productManager: isProductIntelligenceRequest(request) ? classifyProductManagerFlavor(request) : null,
|
|
411
420
|
qa: classifyQaFlavor(request),
|
package/lib/parity.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* lib/parity.mjs — Cross-surface parity verifier.
|
|
3
3
|
*
|
|
4
4
|
* After `construct sync` writes adapters to multiple surfaces (Claude Code,
|
|
5
|
-
* OpenCode, Codex), the module diffs each surface's actual state against the
|
|
5
|
+
* OpenCode, Codex, Copilot, VS Code, Cursor), the module diffs each surface's actual state against the
|
|
6
6
|
* canonical `agents/registry.json`. Backs `construct doctor` for surfacing
|
|
7
7
|
* silent divergence — for instance, an agent added to the registry that
|
|
8
8
|
* never made it to OpenCode because of a sync regression.
|
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
* the field are mirrored everywhere.
|
|
15
15
|
*
|
|
16
16
|
* Surfaces checked:
|
|
17
|
-
* claude
|
|
18
|
-
* opencode
|
|
19
|
-
* codex
|
|
17
|
+
* claude ~/.claude/agents/*.md
|
|
18
|
+
* opencode ~/.config/opencode/opencode.json (agent table)
|
|
19
|
+
* codex ~/.codex/agents/*.toml
|
|
20
|
+
* copilot ~/.github/prompts/*.prompt.md
|
|
21
|
+
* vscode VS Code user settings (github.copilot.mcpServers)
|
|
22
|
+
* cursor ~/.cursor/mcp.json
|
|
20
23
|
*/
|
|
21
24
|
|
|
22
25
|
import fs from 'node:fs';
|
|
@@ -59,16 +62,15 @@ function diffSets(expected, actual) {
|
|
|
59
62
|
return { missing, extra };
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
if (!fs.existsSync(dir)) return { surface: 'claude', status: 'absent', dir };
|
|
65
|
-
const expected = entriesForSurface(registry, 'claude');
|
|
65
|
+
function checkFileSurface({ surface, kind, dir, extension, expected }) {
|
|
66
|
+
if (!fs.existsSync(dir)) return { surface, kind, status: 'absent', dir };
|
|
66
67
|
const actual = fs.readdirSync(dir)
|
|
67
|
-
.filter((name) => name.endsWith(
|
|
68
|
-
.map((name) => name.
|
|
68
|
+
.filter((name) => name.endsWith(extension))
|
|
69
|
+
.map((name) => name.slice(0, -extension.length));
|
|
69
70
|
const { missing, extra } = diffSets(expected, actual);
|
|
70
71
|
return {
|
|
71
|
-
surface
|
|
72
|
+
surface,
|
|
73
|
+
kind,
|
|
72
74
|
status: missing.length === 0 && extra.length === 0 ? 'ok' : 'drift',
|
|
73
75
|
dir,
|
|
74
76
|
expectedCount: expected.length,
|
|
@@ -78,20 +80,56 @@ function checkClaude(registry, { homeDir = os.homedir() } = {}) {
|
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
function getVSCodeSettingsPaths(homeDir) {
|
|
84
|
+
const platform = os.platform();
|
|
85
|
+
if (platform === 'darwin') {
|
|
86
|
+
return [
|
|
87
|
+
path.join(homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
|
88
|
+
path.join(homeDir, 'Library', 'Application Support', 'Code - Insiders', 'User', 'settings.json'),
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
if (platform === 'linux') {
|
|
92
|
+
return [
|
|
93
|
+
path.join(homeDir, '.config', 'Code', 'User', 'settings.json'),
|
|
94
|
+
path.join(homeDir, '.config', 'Code - Insiders', 'User', 'settings.json'),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
if (platform === 'win32') {
|
|
98
|
+
const appData = process.env.APPDATA ?? path.join(homeDir, 'AppData', 'Roaming');
|
|
99
|
+
return [
|
|
100
|
+
path.join(appData, 'Code', 'User', 'settings.json'),
|
|
101
|
+
path.join(appData, 'Code - Insiders', 'User', 'settings.json'),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function checkClaude(registry, { homeDir = os.homedir() } = {}) {
|
|
108
|
+
const expected = entriesForSurface(registry, 'claude');
|
|
109
|
+
return checkFileSurface({
|
|
110
|
+
surface: 'claude',
|
|
111
|
+
kind: 'agents',
|
|
112
|
+
dir: path.join(homeDir, '.claude', 'agents'),
|
|
113
|
+
extension: '.md',
|
|
114
|
+
expected,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
81
118
|
function checkOpenCode(registry, { homeDir = os.homedir() } = {}) {
|
|
82
119
|
const file = path.join(homeDir, '.config', 'opencode', 'opencode.json');
|
|
83
|
-
if (!fs.existsSync(file)) return { surface: 'opencode', status: 'absent', file };
|
|
120
|
+
if (!fs.existsSync(file)) return { surface: 'opencode', kind: 'agents', status: 'absent', file };
|
|
84
121
|
let config;
|
|
85
122
|
try {
|
|
86
123
|
config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
87
124
|
} catch (err) {
|
|
88
|
-
return { surface: 'opencode', status: 'unreadable', file, error: err.message };
|
|
125
|
+
return { surface: 'opencode', kind: 'agents', status: 'unreadable', file, error: err.message };
|
|
89
126
|
}
|
|
90
127
|
const expected = entriesForSurface(registry, 'opencode');
|
|
91
128
|
const actual = Object.keys(config.agent || config.agents || {});
|
|
92
129
|
const { missing, extra } = diffSets(expected, actual);
|
|
93
130
|
return {
|
|
94
131
|
surface: 'opencode',
|
|
132
|
+
kind: 'agents',
|
|
95
133
|
status: missing.length === 0 && extra.length === 0 ? 'ok' : 'drift',
|
|
96
134
|
file,
|
|
97
135
|
expectedCount: expected.length,
|
|
@@ -102,17 +140,79 @@ function checkOpenCode(registry, { homeDir = os.homedir() } = {}) {
|
|
|
102
140
|
}
|
|
103
141
|
|
|
104
142
|
function checkCodex(registry, { homeDir = os.homedir() } = {}) {
|
|
105
|
-
const dir = path.join(homeDir, '.codex', 'agents');
|
|
106
|
-
if (!fs.existsSync(dir)) return { surface: 'codex', status: 'absent', dir };
|
|
107
143
|
const expected = entriesForSurface(registry, 'codex');
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
144
|
+
return checkFileSurface({
|
|
145
|
+
surface: 'codex',
|
|
146
|
+
kind: 'agents',
|
|
147
|
+
dir: path.join(homeDir, '.codex', 'agents'),
|
|
148
|
+
extension: '.toml',
|
|
149
|
+
expected,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function checkCopilot(registry, { homeDir = os.homedir() } = {}) {
|
|
154
|
+
const prefix = registry.prefix || 'cx';
|
|
155
|
+
const entries = [
|
|
156
|
+
...(registry.personas || []).map((p) => ({ ...p, isPersona: true })),
|
|
157
|
+
...(registry.agents || []).map((a) => ({ ...a, isPersona: false })),
|
|
158
|
+
];
|
|
159
|
+
const expected = entries
|
|
160
|
+
.filter((entry) => !entry.internal)
|
|
161
|
+
.filter((entry) => !Array.isArray(entry.platforms) || entry.platforms.includes('copilot'))
|
|
162
|
+
.map((entry) => adapterName(entry, prefix));
|
|
163
|
+
return checkFileSurface({
|
|
164
|
+
surface: 'copilot',
|
|
165
|
+
kind: 'prompts',
|
|
166
|
+
dir: path.join(homeDir, '.github', 'prompts'),
|
|
167
|
+
extension: '.prompt.md',
|
|
168
|
+
expected,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function checkVSCode(registry, { homeDir = os.homedir() } = {}) {
|
|
173
|
+
const paths = getVSCodeSettingsPaths(homeDir).filter((settingsPath) => fs.existsSync(settingsPath));
|
|
174
|
+
if (paths.length === 0) return { surface: 'vscode', kind: 'mcps', status: 'absent', paths };
|
|
175
|
+
const expected = Object.keys(registry.mcpServers ?? {}).filter((id) => id !== 'memory');
|
|
176
|
+
const actualSet = new Set();
|
|
177
|
+
for (const settingsPath of paths) {
|
|
178
|
+
try {
|
|
179
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
180
|
+
for (const id of Object.keys(settings['github.copilot.mcpServers'] ?? {})) actualSet.add(id);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
return { surface: 'vscode', kind: 'mcps', status: 'unreadable', file: settingsPath, error: err.message };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const actual = [...actualSet].sort();
|
|
111
186
|
const { missing, extra } = diffSets(expected, actual);
|
|
112
187
|
return {
|
|
113
|
-
surface: '
|
|
188
|
+
surface: 'vscode',
|
|
189
|
+
kind: 'mcps',
|
|
114
190
|
status: missing.length === 0 && extra.length === 0 ? 'ok' : 'drift',
|
|
115
|
-
|
|
191
|
+
paths,
|
|
192
|
+
expectedCount: expected.length,
|
|
193
|
+
actualCount: actual.length,
|
|
194
|
+
missing,
|
|
195
|
+
extra,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function checkCursor(registry, { homeDir = os.homedir() } = {}) {
|
|
200
|
+
const file = path.join(homeDir, '.cursor', 'mcp.json');
|
|
201
|
+
if (!fs.existsSync(file)) return { surface: 'cursor', kind: 'mcps', status: 'absent', file };
|
|
202
|
+
let config;
|
|
203
|
+
try {
|
|
204
|
+
config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return { surface: 'cursor', kind: 'mcps', status: 'unreadable', file, error: err.message };
|
|
207
|
+
}
|
|
208
|
+
const expected = Object.keys(registry.mcpServers ?? {}).filter((id) => id !== 'memory');
|
|
209
|
+
const actual = Object.keys(config.mcpServers ?? {});
|
|
210
|
+
const { missing, extra } = diffSets(expected, actual);
|
|
211
|
+
return {
|
|
212
|
+
surface: 'cursor',
|
|
213
|
+
kind: 'mcps',
|
|
214
|
+
status: missing.length === 0 && extra.length === 0 ? 'ok' : 'drift',
|
|
215
|
+
file,
|
|
116
216
|
expectedCount: expected.length,
|
|
117
217
|
actualCount: actual.length,
|
|
118
218
|
missing,
|
|
@@ -130,13 +230,16 @@ export function checkParity({ rootDir = ROOT_DIR, homeDir = os.homedir() } = {})
|
|
|
130
230
|
checkClaude(registry, { homeDir }),
|
|
131
231
|
checkOpenCode(registry, { homeDir }),
|
|
132
232
|
checkCodex(registry, { homeDir }),
|
|
233
|
+
checkCopilot(registry, { homeDir }),
|
|
234
|
+
checkVSCode(registry, { homeDir }),
|
|
235
|
+
checkCursor(registry, { homeDir }),
|
|
133
236
|
];
|
|
134
237
|
|
|
135
238
|
const ok = surfaces.every((s) => s.status === 'ok' || s.status === 'absent');
|
|
136
239
|
const summary = surfaces.map((s) => {
|
|
137
240
|
if (s.status === 'absent') return `${s.surface}: not installed`;
|
|
138
241
|
if (s.status === 'unreadable') return `${s.surface}: unreadable (${s.error})`;
|
|
139
|
-
if (s.status === 'ok') return `${s.surface}: ok (${s.actualCount}/${s.expectedCount})`;
|
|
242
|
+
if (s.status === 'ok') return `${s.surface}: ok (${s.actualCount}/${s.expectedCount} ${s.kind})`;
|
|
140
243
|
const parts = [];
|
|
141
244
|
if (s.missing.length) parts.push(`missing: ${s.missing.join(', ')}`);
|
|
142
245
|
if (s.extra.length) parts.push(`extra: ${s.extra.join(', ')}`);
|
package/lib/prompt-composer.js
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
* NEW contract (no backward compat):
|
|
9
9
|
* { metadata, fragments, system, messages, staticEndIndex, totalTokens }
|
|
10
10
|
*
|
|
11
|
-
* Fragment order: core → role-flavor → task-context → learned-patterns → task-packet → context-digest → host-constraints
|
|
12
|
-
*
|
|
11
|
+
* Fragment order: core → role-flavor → model-profile → task-context → learned-patterns → task-packet → context-digest → strategy → host-constraints
|
|
12
|
+
* strategy is injected after context-digest so agents have strategic grounding before learned-patterns affect framing.
|
|
13
|
+
*
|
|
14
|
+
* Workspace-type overlay auto-selection: when workspaceType is passed, the matching role overlay
|
|
15
|
+
* is selected automatically (e.g. workspaceType='platform' → product-manager.platform overlay)
|
|
16
|
+
* unless roleFlavors explicitly overrides it.
|
|
13
17
|
*/
|
|
14
18
|
import crypto from 'node:crypto';
|
|
15
19
|
import fs from 'node:fs';
|
|
@@ -17,29 +21,45 @@ import { homedir } from 'node:os';
|
|
|
17
21
|
import path from 'node:path';
|
|
18
22
|
|
|
19
23
|
import { buildContextDigest, readContextState } from './context-state.mjs';
|
|
20
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
MODEL_OPERATING_PROFILES,
|
|
26
|
+
resolveExecutionContractModelMetadata,
|
|
27
|
+
resolveModelOperatingProfile,
|
|
28
|
+
selectModelTierForWorkCategory,
|
|
29
|
+
} from './model-router.mjs';
|
|
21
30
|
import { searchObservations } from './observation-store.mjs';
|
|
22
31
|
import { routeRequest } from './orchestration-policy.mjs';
|
|
23
32
|
import { resolvePromptEntry, resolvePromptMetadata } from './prompt-metadata.mjs';
|
|
24
33
|
import { readRoleFile } from './role-preload.mjs';
|
|
25
34
|
import { estimateTokens, estimatePromptTokens, estimateTokensSync } from './token-engine.js';
|
|
35
|
+
import { cxDir } from './paths.mjs';
|
|
36
|
+
import { getStrategyDigestSync } from './strategy-store.mjs';
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
const LEARNED_PATTERNS_TOKEN_LIMIT = 200;
|
|
29
|
-
const MAX_OBSERVATIONS = 3; // matched to token budget
|
|
30
|
-
const MAX_CONTEXT_TOKENS = 3000; // total prompt token budget
|
|
38
|
+
const MAX_OBSERVATIONS = 3;
|
|
31
39
|
|
|
32
40
|
// Priority tiers (1 = never drop, 5 = drop first)
|
|
33
41
|
const PRIORITY = {
|
|
34
42
|
'core': 1,
|
|
35
43
|
'task-packet': 1,
|
|
36
44
|
'role-flavor': 2,
|
|
45
|
+
'model-profile': 2,
|
|
37
46
|
'context-digest': 3,
|
|
47
|
+
'strategy': 3,
|
|
38
48
|
'learned-patterns': 4,
|
|
39
49
|
'host-constraints': 5,
|
|
40
50
|
};
|
|
41
51
|
|
|
52
|
+
// workspaceType → roleFlavors auto-selection when caller doesn't override
|
|
53
|
+
const WORKSPACE_FLAVOR_MAP = {
|
|
54
|
+
platform: { productManager: 'platform', architect: 'platform', engineer: 'platform' },
|
|
55
|
+
enterprise: { productManager: 'enterprise', architect: 'enterprise' },
|
|
56
|
+
'ai-product': { productManager: 'ai-product', architect: 'ai-systems' },
|
|
57
|
+
growth: { productManager: 'growth' },
|
|
58
|
+
product: {},
|
|
59
|
+
};
|
|
60
|
+
|
|
42
61
|
const AGENT_FLAVOR_MAP = {
|
|
62
|
+
engineer: { classifierKey: 'engineer', rolePrefix: 'engineer' },
|
|
43
63
|
architect: { classifierKey: 'architect', rolePrefix: 'architect' },
|
|
44
64
|
'product-manager': { classifierKey: 'productManager', rolePrefix: 'product-manager' },
|
|
45
65
|
qa: { classifierKey: 'qa', rolePrefix: 'qa' },
|
|
@@ -48,6 +68,18 @@ const AGENT_FLAVOR_MAP = {
|
|
|
48
68
|
'data-engineer': { classifierKey: 'dataEngineer', rolePrefix: 'data-engineer' },
|
|
49
69
|
};
|
|
50
70
|
|
|
71
|
+
// Merge workspaceType-derived overlays with explicit roleFlavors. Explicit always wins.
|
|
72
|
+
function resolveRoleFlavors(roleFlavors, workspaceType, shortName) {
|
|
73
|
+
const workspaceDefaults = workspaceType ? (WORKSPACE_FLAVOR_MAP[workspaceType] ?? {}) : {};
|
|
74
|
+
if (!roleFlavors && !workspaceType) return null;
|
|
75
|
+
return { ...workspaceDefaults, ...(roleFlavors || {}) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Synchronous strategy digest for prompt injection — reads all scope files from the strategy directory.
|
|
79
|
+
function buildStrategyBlock({ tokenLimit = 400 } = {}) {
|
|
80
|
+
return getStrategyDigestSync();
|
|
81
|
+
}
|
|
82
|
+
|
|
51
83
|
function compactTokens(text, tokenLimit = 300, { modelId = 'default' } = {}) {
|
|
52
84
|
if (!text) return '';
|
|
53
85
|
const normalized = String(text).trim();
|
|
@@ -65,7 +97,13 @@ function readPromptBody(promptFile, rootDir) {
|
|
|
65
97
|
return fs.readFileSync(filePath, 'utf8').trim();
|
|
66
98
|
}
|
|
67
99
|
|
|
68
|
-
function buildLearnedPatternsBlock(agentName, {
|
|
100
|
+
function buildLearnedPatternsBlock(agentName, {
|
|
101
|
+
intent = null,
|
|
102
|
+
workCategory = null,
|
|
103
|
+
project = null,
|
|
104
|
+
modelId = 'default',
|
|
105
|
+
tokenLimit = MODEL_OPERATING_PROFILES.balanced.learnedPatternsTokens,
|
|
106
|
+
} = {}) {
|
|
69
107
|
try {
|
|
70
108
|
const rootDir = homedir();
|
|
71
109
|
const query = [agentName, intent, workCategory].filter(Boolean).join(' ');
|
|
@@ -93,7 +131,7 @@ function buildLearnedPatternsBlock(agentName, { intent = null, workCategory = nu
|
|
|
93
131
|
const prefix = obs.category === 'anti-pattern' ? '⚠ ' : obs.category === 'decision' ? '✓ ' : '• ';
|
|
94
132
|
const line = `${prefix}${obs.summary}`;
|
|
95
133
|
const lineTokens = estimateTokensSync(line + '\n', { modelId });
|
|
96
|
-
if (tokens + lineTokens >
|
|
134
|
+
if (tokens + lineTokens > tokenLimit) break;
|
|
97
135
|
lines.push(line);
|
|
98
136
|
tokens += lineTokens + 1;
|
|
99
137
|
}
|
|
@@ -144,42 +182,68 @@ export function composePrompt(agentName, {
|
|
|
144
182
|
intent = null,
|
|
145
183
|
workCategory = null,
|
|
146
184
|
roleFlavors = null,
|
|
185
|
+
workspaceType = null,
|
|
147
186
|
project = null,
|
|
148
187
|
injectLearnedPatterns = true,
|
|
149
188
|
modelId = 'default',
|
|
189
|
+
executionContractModel = null,
|
|
150
190
|
} = {}) {
|
|
151
191
|
const entry = resolvePromptEntry(agentName, { rootDir, registry });
|
|
152
192
|
if (!entry?.promptFile) return { metadata: {}, fragments: [], system: '', messages: [], staticEndIndex: -1, totalTokens: 0 };
|
|
153
193
|
|
|
154
194
|
const metadata = resolvePromptMetadata(agentName, { rootDir, registry });
|
|
155
195
|
const fragments = [];
|
|
196
|
+
const selectedProfile = resolveModelOperatingProfile({
|
|
197
|
+
envValues: executionContractModel?.profile ? { CONSTRUCT_MODEL_PROFILE: executionContractModel.profile.id } : {},
|
|
198
|
+
selectedModel: executionContractModel?.selectedModel ?? modelId,
|
|
199
|
+
});
|
|
156
200
|
|
|
157
|
-
// 1. Core (priority 1 — never dropped)
|
|
158
201
|
fragments.push({ type: 'core', priority: PRIORITY['core'], label: entry.name, content: readPromptBody(entry.promptFile, rootDir), tokenBudget: null });
|
|
159
202
|
|
|
160
|
-
// 2. Role flavor overlay (priority 2)
|
|
161
203
|
const shortName = String(agentName).replace(/^cx-/, '');
|
|
162
204
|
const flavorMapping = AGENT_FLAVOR_MAP[shortName];
|
|
163
|
-
|
|
164
|
-
|
|
205
|
+
|
|
206
|
+
// When workspaceType is provided and roleFlavors doesn't already specify this agent's flavor,
|
|
207
|
+
// auto-select the matching overlay so agents get domain guidance without requiring explicit caller config.
|
|
208
|
+
const effectiveRoleFlavors = resolveRoleFlavors(roleFlavors, workspaceType, shortName);
|
|
209
|
+
|
|
210
|
+
if (flavorMapping && effectiveRoleFlavors) {
|
|
211
|
+
const flavor = effectiveRoleFlavors[flavorMapping.classifierKey];
|
|
165
212
|
if (flavor) {
|
|
166
213
|
const overlayBody = readRoleFile(rootDir, `${flavorMapping.rolePrefix}.${flavor}`, {
|
|
167
214
|
source: 'prompt-composer',
|
|
168
215
|
callerContext: agentName,
|
|
169
216
|
});
|
|
170
217
|
if (overlayBody) {
|
|
218
|
+
const overlayContent = selectedProfile.preferCompressedRoleGuidance
|
|
219
|
+
? compactTokens(overlayBody, selectedProfile.roleFlavorTokens, { modelId })
|
|
220
|
+
: overlayBody;
|
|
171
221
|
fragments.push({
|
|
172
222
|
type: 'role-flavor',
|
|
173
223
|
priority: PRIORITY['role-flavor'],
|
|
174
224
|
label: `${flavorMapping.rolePrefix}.${flavor}`,
|
|
175
|
-
content: `### ${flavor} domain guidance\n\n${
|
|
176
|
-
tokenBudget:
|
|
225
|
+
content: `### ${flavor} domain guidance\n\n${overlayContent}`,
|
|
226
|
+
tokenBudget: selectedProfile.roleFlavorTokens,
|
|
177
227
|
});
|
|
178
228
|
}
|
|
179
229
|
}
|
|
180
230
|
}
|
|
181
231
|
|
|
182
|
-
|
|
232
|
+
if (selectedProfile.retrievalFirst) {
|
|
233
|
+
fragments.push({
|
|
234
|
+
type: 'model-profile',
|
|
235
|
+
priority: PRIORITY['model-profile'],
|
|
236
|
+
label: `model-profile.${selectedProfile.id}`,
|
|
237
|
+
content: [
|
|
238
|
+
`## ${selectedProfile.label} operating mode`,
|
|
239
|
+
'',
|
|
240
|
+
'Prefer retrieval-first execution: gather focused evidence before broad edits.',
|
|
241
|
+
'Keep plans and summaries compact, stage work in verified steps, and avoid whole-file rewrites unless the task requires them.',
|
|
242
|
+
].join('\n'),
|
|
243
|
+
tokenBudget: 90,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
183
247
|
if (intent || workCategory) {
|
|
184
248
|
const text = `Intent: ${intent || 'unknown'}\nWork category: ${workCategory || 'unknown'}`;
|
|
185
249
|
fragments.push({
|
|
@@ -191,10 +255,13 @@ export function composePrompt(agentName, {
|
|
|
191
255
|
});
|
|
192
256
|
}
|
|
193
257
|
|
|
194
|
-
// 4. Learned patterns (priority 4)
|
|
195
258
|
if (injectLearnedPatterns) {
|
|
196
259
|
const { text: learnedBlock, tokens: learnedTokens } = buildLearnedPatternsBlock(agentName, {
|
|
197
|
-
intent,
|
|
260
|
+
intent,
|
|
261
|
+
workCategory,
|
|
262
|
+
project,
|
|
263
|
+
modelId,
|
|
264
|
+
tokenLimit: selectedProfile.learnedPatternsTokens,
|
|
198
265
|
});
|
|
199
266
|
if (learnedBlock) {
|
|
200
267
|
fragments.push({
|
|
@@ -202,13 +269,12 @@ export function composePrompt(agentName, {
|
|
|
202
269
|
priority: PRIORITY['learned-patterns'],
|
|
203
270
|
label: 'observations',
|
|
204
271
|
content: learnedBlock,
|
|
205
|
-
tokenBudget:
|
|
272
|
+
tokenBudget: selectedProfile.learnedPatternsTokens,
|
|
206
273
|
estimatedTokens: learnedTokens,
|
|
207
274
|
});
|
|
208
275
|
}
|
|
209
276
|
}
|
|
210
277
|
|
|
211
|
-
// 5. Task packet (priority 1 — never dropped)
|
|
212
278
|
if (task) {
|
|
213
279
|
const taskBlock = [
|
|
214
280
|
task.title ? `Task: ${task.title}` : null,
|
|
@@ -229,13 +295,12 @@ export function composePrompt(agentName, {
|
|
|
229
295
|
type: 'task-packet',
|
|
230
296
|
priority: PRIORITY['task-packet'],
|
|
231
297
|
label: 'workflow-task',
|
|
232
|
-
content: compactTokens(taskBlock,
|
|
233
|
-
tokenBudget:
|
|
298
|
+
content: compactTokens(taskBlock, selectedProfile.taskPacketTokens, { modelId }),
|
|
299
|
+
tokenBudget: selectedProfile.taskPacketTokens,
|
|
234
300
|
});
|
|
235
301
|
}
|
|
236
302
|
}
|
|
237
303
|
|
|
238
|
-
// 6. Context digest (priority 3)
|
|
239
304
|
const digest = buildContextDigest(contextState);
|
|
240
305
|
if (digest) {
|
|
241
306
|
const digestStr = JSON.stringify(digest);
|
|
@@ -243,24 +308,34 @@ export function composePrompt(agentName, {
|
|
|
243
308
|
type: 'context-digest',
|
|
244
309
|
priority: PRIORITY['context-digest'],
|
|
245
310
|
label: 'context',
|
|
246
|
-
content: compactTokens(digestStr,
|
|
247
|
-
tokenBudget:
|
|
311
|
+
content: compactTokens(digestStr, selectedProfile.contextDigestTokens, { modelId }),
|
|
312
|
+
tokenBudget: selectedProfile.contextDigestTokens,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const strategyText = buildStrategyBlock({ modelId, tokenLimit: selectedProfile.strategyTokens ?? 400 });
|
|
317
|
+
if (strategyText) {
|
|
318
|
+
fragments.push({
|
|
319
|
+
type: 'strategy',
|
|
320
|
+
priority: PRIORITY['strategy'],
|
|
321
|
+
label: 'active-strategy',
|
|
322
|
+
content: strategyText,
|
|
323
|
+
tokenBudget: selectedProfile.strategyTokens ?? 400,
|
|
248
324
|
});
|
|
249
325
|
}
|
|
250
326
|
|
|
251
|
-
// 7. Host constraints (priority 5 — dropped first)
|
|
252
327
|
if (hostConstraints) {
|
|
253
328
|
fragments.push({
|
|
254
329
|
type: 'host-constraints',
|
|
255
330
|
priority: PRIORITY['host-constraints'],
|
|
256
331
|
label: 'host',
|
|
257
|
-
content: compactTokens(JSON.stringify(hostConstraints),
|
|
258
|
-
tokenBudget:
|
|
332
|
+
content: compactTokens(JSON.stringify(hostConstraints), selectedProfile.hostConstraintsTokens, { modelId }),
|
|
333
|
+
tokenBudget: selectedProfile.hostConstraintsTokens,
|
|
259
334
|
});
|
|
260
335
|
}
|
|
261
336
|
|
|
262
337
|
// Priority-based pruning
|
|
263
|
-
const pruned = pruneFragments(fragments,
|
|
338
|
+
const pruned = pruneFragments(fragments, selectedProfile.maxPromptTokens, modelId);
|
|
264
339
|
const staticEndIndex = findStaticEndIndex(pruned);
|
|
265
340
|
|
|
266
341
|
// Assemble outputs
|
|
@@ -366,6 +441,7 @@ export function summarizePromptComposition(agentName, options = {}) {
|
|
|
366
441
|
roleFlavors: options.roleFlavors || route?.roleFlavors || null,
|
|
367
442
|
});
|
|
368
443
|
const fragmentTypes = composed.fragments.map((fragment) => fragment.type);
|
|
444
|
+
const flavorFragment = composed.fragments.find((fragment) => fragment.type === 'role-flavor');
|
|
369
445
|
const composedPromptHash = composed.system
|
|
370
446
|
? crypto.createHash('sha256').update(composed.system).digest('hex')
|
|
371
447
|
: null;
|
|
@@ -393,6 +469,7 @@ export function summarizePromptComposition(agentName, options = {}) {
|
|
|
393
469
|
routeDispatchPlan: route.dispatchPlan,
|
|
394
470
|
...(route.roleFlavors ? { routeRoleFlavors: route.roleFlavors } : {}),
|
|
395
471
|
} : {}),
|
|
472
|
+
...(flavorFragment ? { promptRoleFlavor: flavorFragment.label } : {}),
|
|
396
473
|
promptHasRoleFlavor: fragmentTypes.includes('role-flavor'),
|
|
397
474
|
promptHasLearnedPatterns: fragmentTypes.includes('learned-patterns'),
|
|
398
475
|
executionContractModel,
|