@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/init-update.mjs
CHANGED
|
@@ -1,168 +1,283 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/init-update.mjs —
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
3
|
+
* lib/init-update.mjs — non-destructive project-standard updates and template conflict resolution.
|
|
4
|
+
*
|
|
5
|
+
* `construct init:update` helps projects adopt current Construct standards without replacing
|
|
6
|
+
* user-managed instruction files. Proposed updates are written under .cx/proposals/ for
|
|
7
|
+
* manual review and merge.
|
|
8
|
+
*
|
|
9
|
+
* checkTemplateConflicts(targetDir) compares project templates against the construct install's
|
|
10
|
+
* templates/docs/ directory. resolveTemplateConflict(conflict, resolution, targetDir) applies
|
|
11
|
+
* one of three resolutions: keep-project, use-construct, or move-to-cx-override.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import fs from "node:fs";
|
|
14
15
|
import path from "node:path";
|
|
15
|
-
import
|
|
16
|
+
import crypto from "node:crypto";
|
|
17
|
+
import { constructDir } from "./paths.mjs";
|
|
16
18
|
|
|
17
19
|
const args = process.argv.slice(2);
|
|
18
20
|
const dryRun = args.includes("--dry-run");
|
|
19
|
-
const cwdArg = args.find(arg => arg.startsWith("--cwd="));
|
|
21
|
+
const cwdArg = args.find((arg) => arg.startsWith("--cwd="));
|
|
20
22
|
const cwd = cwdArg ? path.resolve(cwdArg.split("=")[1]) : process.cwd();
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
const REQUIRED_AGENT_SECTIONS = [
|
|
25
|
+
"Operating hierarchy",
|
|
26
|
+
"Start-of-session rules",
|
|
27
|
+
"Maintenance rules",
|
|
28
|
+
"End-of-session rules",
|
|
29
|
+
"Verification rules",
|
|
30
|
+
];
|
|
23
31
|
|
|
24
|
-
// Load the AGENTS.md template from project-init-shared
|
|
25
32
|
async function loadAgentsTemplate(projectName) {
|
|
26
|
-
const modulePath = path.join(import.meta.dirname,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (module.buildAgentsGuide) {
|
|
31
|
-
return module.buildAgentsGuide(projectName);
|
|
32
|
-
}
|
|
33
|
-
throw new Error('Could not find buildAgentsGuide function in project-init-shared.mjs');
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.error(`❌ Failed to load AGENTS.md template: ${error.message}`);
|
|
36
|
-
process.exit(1);
|
|
33
|
+
const modulePath = path.join(import.meta.dirname, "project-init-shared.mjs");
|
|
34
|
+
const module = await import(modulePath);
|
|
35
|
+
if (!module.buildAgentsGuide) {
|
|
36
|
+
throw new Error("buildAgentsGuide not available from project-init-shared.mjs");
|
|
37
37
|
}
|
|
38
|
+
return module.buildAgentsGuide(projectName);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.log(`📋 Dry run - would update AGENTS.md`);
|
|
48
|
-
console.log(`--- Current AGENTS.md (first 200 chars) ---`);
|
|
49
|
-
console.log(existingContent.substring(0, 200) + '...');
|
|
50
|
-
console.log(`--- New AGENTS.md (first 200 chars) ---`);
|
|
51
|
-
console.log(newTemplate.substring(0, 200) + '...');
|
|
52
|
-
return null;
|
|
41
|
+
function inferProjectName(targetDir) {
|
|
42
|
+
const packagePath = path.join(targetDir, "package.json");
|
|
43
|
+
if (fs.existsSync(packagePath)) {
|
|
44
|
+
try {
|
|
45
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
46
|
+
if (pkg.name) return pkg.name;
|
|
47
|
+
} catch { /* fall through */ }
|
|
53
48
|
}
|
|
54
|
-
|
|
55
|
-
// Backup existing file
|
|
56
|
-
const backupPath = path.join(cwd, 'AGENTS.md.backup');
|
|
57
|
-
writeFileSync(backupPath, existingContent, 'utf8');
|
|
58
|
-
console.log(`📦 Created backup at ${backupPath}`);
|
|
59
|
-
|
|
60
|
-
// Write new template
|
|
61
|
-
const agentsPath = path.join(cwd, 'AGENTS.md');
|
|
62
|
-
writeFileSync(agentsPath, newTemplate, 'utf8');
|
|
63
|
-
console.log(`✅ Updated AGENTS.md`);
|
|
64
|
-
|
|
65
|
-
return newTemplate;
|
|
49
|
+
return path.basename(targetDir);
|
|
66
50
|
}
|
|
67
51
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
52
|
+
function findMissingAgentsSections(content) {
|
|
53
|
+
return REQUIRED_AGENT_SECTIONS.filter((section) => !content.toLowerCase().includes(section.toLowerCase()));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ensureProposalDir(targetDir) {
|
|
57
|
+
const dir = path.join(targetDir, ".cx", "proposals");
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeProposal(targetDir, fileName, content) {
|
|
63
|
+
const proposalDir = ensureProposalDir(targetDir);
|
|
64
|
+
const proposalPath = path.join(proposalDir, fileName);
|
|
65
|
+
fs.writeFileSync(proposalPath, content, "utf8");
|
|
66
|
+
return proposalPath;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildAgentsProposal(existingContent, template, missingSections) {
|
|
70
|
+
const missingList = missingSections.map((section) => `- ${section}`).join("\n");
|
|
71
|
+
return [
|
|
72
|
+
"<!--",
|
|
73
|
+
"Construct init:update proposal",
|
|
74
|
+
"Review this file and merge the needed sections into AGENTS.md manually.",
|
|
75
|
+
"The original AGENTS.md was not modified.",
|
|
76
|
+
"-->",
|
|
77
|
+
"",
|
|
78
|
+
"# Proposed AGENTS.md Update",
|
|
79
|
+
"",
|
|
80
|
+
"## Why this proposal exists",
|
|
81
|
+
"",
|
|
82
|
+
"Your current `AGENTS.md` is missing these required sections:",
|
|
83
|
+
missingList,
|
|
84
|
+
"",
|
|
85
|
+
"## Current AGENTS.md",
|
|
86
|
+
"",
|
|
87
|
+
"```md",
|
|
88
|
+
existingContent.trimEnd(),
|
|
89
|
+
"```",
|
|
90
|
+
"",
|
|
91
|
+
"## Current Construct template",
|
|
92
|
+
"",
|
|
93
|
+
"```md",
|
|
94
|
+
template.trimEnd(),
|
|
95
|
+
"```",
|
|
96
|
+
"",
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildWorkflowProposal(existingContent) {
|
|
101
|
+
const updatedContent = existingContent.replace(
|
|
102
|
+
/(\s+- run: node bin\/construct doctor\s*\n)/,
|
|
103
|
+
"$1 - run: node bin/construct docs:verify\n",
|
|
104
|
+
);
|
|
105
|
+
if (updatedContent === existingContent) return null;
|
|
106
|
+
return [
|
|
107
|
+
"# Proposed CI Workflow Update",
|
|
108
|
+
"",
|
|
109
|
+
"Add `construct docs:verify` next to the existing doctor check.",
|
|
110
|
+
"",
|
|
111
|
+
"```yaml",
|
|
112
|
+
updatedContent.trimEnd(),
|
|
113
|
+
"```",
|
|
114
|
+
"",
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Template conflict detection ───────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function sha256File(filePath) {
|
|
121
|
+
const buf = fs.readFileSync(filePath);
|
|
122
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Compare project templates against the construct install's templates/docs/ directory.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} targetDir — project root to inspect
|
|
129
|
+
* @returns {{ conflicts: Array<{ name, constructPath, projectPath, constructSha, projectSha }>, identical: string[], missing: string[] }}
|
|
130
|
+
*/
|
|
131
|
+
export function checkTemplateConflicts(targetDir) {
|
|
132
|
+
const constructTemplatesDir = path.join(constructDir(), 'templates', 'docs');
|
|
133
|
+
const projectTemplatesDir = path.join(targetDir, 'templates', 'docs');
|
|
134
|
+
|
|
135
|
+
const conflicts = [];
|
|
136
|
+
const identical = [];
|
|
137
|
+
const missing = [];
|
|
138
|
+
|
|
139
|
+
if (!fs.existsSync(constructTemplatesDir)) {
|
|
140
|
+
return { conflicts, identical, missing };
|
|
81
141
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return true;
|
|
142
|
+
|
|
143
|
+
const constructFiles = fs.readdirSync(constructTemplatesDir).filter((f) => f.endsWith('.md'));
|
|
144
|
+
|
|
145
|
+
for (const name of constructFiles) {
|
|
146
|
+
const constructPath = path.join(constructTemplatesDir, name);
|
|
147
|
+
const projectPath = path.join(projectTemplatesDir, name);
|
|
148
|
+
|
|
149
|
+
if (!fs.existsSync(projectPath)) {
|
|
150
|
+
missing.push(name);
|
|
151
|
+
continue;
|
|
93
152
|
}
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
153
|
+
|
|
154
|
+
const constructSha = sha256File(constructPath);
|
|
155
|
+
const projectSha = sha256File(projectPath);
|
|
156
|
+
|
|
157
|
+
if (constructSha === projectSha) {
|
|
158
|
+
identical.push(name);
|
|
159
|
+
} else {
|
|
160
|
+
conflicts.push({ name, constructPath, projectPath, constructSha, projectSha });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { conflicts, identical, missing };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Apply a resolution to a single template conflict.
|
|
169
|
+
*
|
|
170
|
+
* Resolutions:
|
|
171
|
+
* 'keep-project' — do nothing; leave the project template as-is
|
|
172
|
+
* 'use-construct' — overwrite project template with construct's version; backup original to <name>.bak
|
|
173
|
+
* 'move-to-cx-override' — copy project template to .cx/templates/docs/<name>,
|
|
174
|
+
* then write construct's template to templates/docs/<name>
|
|
175
|
+
*
|
|
176
|
+
* @param {{ name, constructPath, projectPath, constructSha, projectSha }} conflict
|
|
177
|
+
* @param {'keep-project'|'use-construct'|'move-to-cx-override'} resolution
|
|
178
|
+
* @param {string} targetDir
|
|
179
|
+
*/
|
|
180
|
+
export function resolveTemplateConflict(conflict, resolution, targetDir) {
|
|
181
|
+
const { name, constructPath, projectPath } = conflict;
|
|
182
|
+
|
|
183
|
+
if (resolution === 'keep-project') {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (resolution === 'use-construct') {
|
|
188
|
+
const backupPath = `${projectPath}.bak`;
|
|
189
|
+
fs.copyFileSync(projectPath, backupPath);
|
|
190
|
+
fs.copyFileSync(constructPath, projectPath);
|
|
191
|
+
return;
|
|
100
192
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
193
|
+
|
|
194
|
+
if (resolution === 'move-to-cx-override') {
|
|
195
|
+
const cxOverrideDir = path.join(targetDir, '.cx', 'templates', 'docs');
|
|
196
|
+
fs.mkdirSync(cxOverrideDir, { recursive: true });
|
|
197
|
+
fs.copyFileSync(projectPath, path.join(cxOverrideDir, name));
|
|
198
|
+
fs.copyFileSync(constructPath, projectPath);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
throw new Error(`Unknown resolution: ${resolution}. Expected keep-project, use-construct, or move-to-cx-override.`);
|
|
104
203
|
}
|
|
105
204
|
|
|
205
|
+
// ── Init update workflow ──────────────────────────────────────────────────────
|
|
206
|
+
|
|
106
207
|
async function main() {
|
|
107
|
-
console.log(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.log(`❌ AGENTS.md not found. Run 'construct init' first.`);
|
|
208
|
+
console.log(`Preparing Construct standards update for ${cwd}`);
|
|
209
|
+
|
|
210
|
+
const agentsPath = path.join(cwd, "AGENTS.md");
|
|
211
|
+
if (!fs.existsSync(agentsPath)) {
|
|
212
|
+
console.error("AGENTS.md not found. Run `construct init` first.");
|
|
113
213
|
process.exit(1);
|
|
114
214
|
}
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
215
|
+
|
|
216
|
+
const projectName = inferProjectName(cwd);
|
|
217
|
+
const existingAgents = fs.readFileSync(agentsPath, "utf8");
|
|
218
|
+
const template = await loadAgentsTemplate(projectName);
|
|
219
|
+
const missingSections = findMissingAgentsSections(existingAgents);
|
|
220
|
+
|
|
221
|
+
const planned = [];
|
|
222
|
+
|
|
223
|
+
if (missingSections.length > 0) {
|
|
224
|
+
const proposalPath = path.join(".cx", "proposals", "AGENTS.md.construct-update.md");
|
|
225
|
+
planned.push({
|
|
226
|
+
label: `AGENTS.md proposal (${missingSections.length} missing section${missingSections.length === 1 ? "" : "s"})`,
|
|
227
|
+
write() {
|
|
228
|
+
return writeProposal(cwd, "AGENTS.md.construct-update.md", buildAgentsProposal(existingAgents, template, missingSections));
|
|
229
|
+
},
|
|
230
|
+
relativePath: proposalPath,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const workflowPath = path.join(cwd, ".github", "workflows", "ci.yml");
|
|
235
|
+
if (fs.existsSync(workflowPath)) {
|
|
236
|
+
const workflowContent = fs.readFileSync(workflowPath, "utf8");
|
|
237
|
+
if (!workflowContent.includes("node bin/construct docs:verify")) {
|
|
238
|
+
const proposal = buildWorkflowProposal(workflowContent);
|
|
239
|
+
if (proposal) {
|
|
240
|
+
planned.push({
|
|
241
|
+
label: "CI workflow proposal (add docs:verify)",
|
|
242
|
+
write() {
|
|
243
|
+
return writeProposal(cwd, "ci.yml.construct-update.md", proposal);
|
|
244
|
+
},
|
|
245
|
+
relativePath: path.join(".cx", "proposals", "ci.yml.construct-update.md"),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
127
248
|
}
|
|
128
249
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
console.log(` • AGENTS.md found (${existingAgents.length} chars)`);
|
|
134
|
-
console.log(` • Latest template loaded (${template.length} chars)`);
|
|
135
|
-
console.log(` • Project name: ${projectName}`);
|
|
136
|
-
|
|
137
|
-
// Check if AGENTS.md has documentation enforcement section
|
|
138
|
-
const hasDocEnforcement = existingAgents.includes('Documentation Rules');
|
|
139
|
-
console.log(` • Has documentation enforcement: ${hasDocEnforcement ? '✅' : '❌'}`);
|
|
140
|
-
|
|
141
|
-
// Check for CI workflow
|
|
142
|
-
const ciUpdated = updateCiWorkflow(cwd);
|
|
143
|
-
|
|
144
|
-
// Update AGENTS.md if it doesn't have the latest sections
|
|
145
|
-
if (!hasDocEnforcement) {
|
|
146
|
-
updateAgentsMd(existingAgents, template);
|
|
147
|
-
} else {
|
|
148
|
-
console.log(`✅ AGENTS.md already has documentation enforcement rules`);
|
|
250
|
+
|
|
251
|
+
if (planned.length === 0) {
|
|
252
|
+
console.log("No proposals needed. The project already satisfies the current update checks.");
|
|
253
|
+
return;
|
|
149
254
|
}
|
|
150
|
-
|
|
151
|
-
console.log(
|
|
152
|
-
console.log(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
255
|
+
|
|
256
|
+
console.log("");
|
|
257
|
+
console.log("Planned proposals:");
|
|
258
|
+
for (const item of planned) {
|
|
259
|
+
console.log(` - ${item.label} -> ${item.relativePath}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
158
262
|
if (dryRun) {
|
|
159
|
-
console.log(
|
|
263
|
+
console.log("");
|
|
264
|
+
console.log("Dry run only. No files were written.");
|
|
265
|
+
return;
|
|
160
266
|
}
|
|
267
|
+
|
|
268
|
+
console.log("");
|
|
269
|
+
for (const item of planned) {
|
|
270
|
+
const proposalPath = item.write();
|
|
271
|
+
console.log(`Wrote proposal: ${proposalPath}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log("");
|
|
275
|
+
console.log("Review the proposal files under .cx/proposals/ and merge the needed changes manually.");
|
|
161
276
|
}
|
|
162
277
|
|
|
163
278
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
164
|
-
main().catch(error => {
|
|
165
|
-
console.error(
|
|
279
|
+
main().catch((error) => {
|
|
280
|
+
console.error(`Error: ${error.message}`);
|
|
166
281
|
process.exit(1);
|
|
167
282
|
});
|
|
168
|
-
}
|
|
283
|
+
}
|
|
@@ -209,6 +209,7 @@ export async function verifyIntent({ request, specialist, flavor, matchedKeyword
|
|
|
209
209
|
// Kept here (not in orchestration-policy) so the classifier module stays
|
|
210
210
|
// the canonical source of "what does verified mean for this role."
|
|
211
211
|
const ROLE_PROMPT_KEYWORDS = {
|
|
212
|
+
engineer: ['ai', 'platform', 'data', 'prompt', 'pipeline', 'deploy'],
|
|
212
213
|
productManager: ['platform', 'enterprise', 'ai-product', 'growth', 'go-to-market'],
|
|
213
214
|
architect: ['ai-systems', 'integration', 'data model', 'enterprise', 'platform'],
|
|
214
215
|
qa: ['ai-eval', 'api-contract', 'data-pipeline', 'web-ui'],
|
package/lib/knowledge/layout.mjs
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
export const KNOWLEDGE_ROOT = '.cx/knowledge';
|
|
22
|
+
export const KNOWLEDGE_INTERNAL_ROOT = `${KNOWLEDGE_ROOT}/internal`;
|
|
23
|
+
export const KNOWLEDGE_REFERENCE_ROOT = `${KNOWLEDGE_ROOT}/reference`;
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* All valid knowledge subdirectory names.
|
|
@@ -70,3 +72,11 @@ export function inferKnowledgeTarget(filePath) {
|
|
|
70
72
|
if (/\bpost.?mortem\b|\bincident\b|\brca\b/.test(name)) return 'internal';
|
|
71
73
|
return 'internal';
|
|
72
74
|
}
|
|
75
|
+
|
|
76
|
+
export function knowledgeInternalStore(...segments) {
|
|
77
|
+
return ['knowledge', 'internal', ...segments].join('/');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function knowledgeReferenceStore(...segments) {
|
|
81
|
+
return ['knowledge', 'reference', ...segments].join('/');
|
|
82
|
+
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* lib/mcp/tools/telemetry.mjs — Telemetry MCP tools: trace/score, session usage, efficiency snapshot.
|
|
3
3
|
*
|
|
4
4
|
* Exposes cxTrace, cxScore, sessionUsage, and efficiencySnapshot.
|
|
5
|
-
* Requires ROOT_DIR injected via opts.
|
|
5
|
+
* Requires ROOT_DIR injected via opts. Remote export uses the shared telemetry
|
|
6
|
+
* adapter when configured; local JSONL remains available by default.
|
|
6
7
|
*/
|
|
7
8
|
import { join, resolve } from 'node:path';
|
|
8
9
|
import { homedir } from 'node:os';
|
|
9
10
|
import { readFileSync, existsSync } from 'node:fs';
|
|
10
|
-
import {
|
|
11
|
+
import { createTelemetryClient } from '../../telemetry/client.mjs';
|
|
11
12
|
import { summarizePromptComposition } from '../../prompt-composer.js';
|
|
12
13
|
import { enrichMetadataWithPrompt } from '../../prompt-metadata.mjs';
|
|
13
14
|
import { readCurrentModels, resolveExecutionContractModelMetadata, selectModelTierForWorkCategory } from '../../model-router.mjs';
|
|
@@ -16,7 +17,6 @@ import { loadWorkflow } from '../../workflow-state.mjs';
|
|
|
16
17
|
import { buildStatus } from '../../status.mjs';
|
|
17
18
|
import { readEfficiencyLog, buildCompactEfficiencyDigest } from '../../efficiency.mjs';
|
|
18
19
|
import { addObservation } from '../../observation-store.mjs';
|
|
19
|
-
// Local mock ingest client for fallback when ingestion service is unavailable
|
|
20
20
|
import { loadConstructEnv } from '../../env-config.mjs';
|
|
21
21
|
|
|
22
22
|
// Load config.env once at module init so config.env values win over shell env
|
|
@@ -34,15 +34,6 @@ function readJSON(filePath) {
|
|
|
34
34
|
|
|
35
35
|
import { execSync as _execSync } from 'node:child_process';
|
|
36
36
|
|
|
37
|
-
// Mock ingest client for fallback to direct API when the real client is not available.
|
|
38
|
-
|
|
39
|
-
function getOrCreateIngestClient() {
|
|
40
|
-
return {
|
|
41
|
-
available: false,
|
|
42
|
-
trace: () => {}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
37
|
function resolveReleaseTag(cwd) {
|
|
47
38
|
try {
|
|
48
39
|
return _execSync('git rev-parse --short HEAD', { stdio: 'pipe', cwd, timeout: 2000 }).toString().trim() || undefined;
|
|
@@ -51,18 +42,8 @@ function resolveReleaseTag(cwd) {
|
|
|
51
42
|
}
|
|
52
43
|
}
|
|
53
44
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
const secret = CONF_ENV.CONSTRUCT_TELEMETRY_SECRET_KEY ?? process.env.CONSTRUCT_TELEMETRY_SECRET_KEY;
|
|
57
|
-
if (!key || !secret) throw new Error('CONSTRUCT_TELEMETRY_PUBLIC_KEY and CONSTRUCT_TELEMETRY_SECRET_KEY must be set.');
|
|
58
|
-
return {
|
|
59
|
-
Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`,
|
|
60
|
-
'Content-Type': 'application/json',
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function telemetryBaseUrl() {
|
|
65
|
-
return (CONF_ENV.CONSTRUCT_TELEMETRY_URL ?? process.env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
|
|
45
|
+
function telemetryEnv() {
|
|
46
|
+
return { ...process.env, ...CONF_ENV };
|
|
66
47
|
}
|
|
67
48
|
|
|
68
49
|
function resolveSessionContext() {
|
|
@@ -128,10 +109,8 @@ export async function cxTrace(args, { ROOT_DIR }) {
|
|
|
128
109
|
}, { rootDir: ROOT_DIR });
|
|
129
110
|
const traceId = args.id ?? crypto.randomUUID();
|
|
130
111
|
try {
|
|
131
|
-
const available = await telemetryAvailable();
|
|
132
|
-
if (!available) return { ok: false, error: 'Telemetry credentials not configured', id: traceId };
|
|
133
112
|
const teamId = args.metadata?.teamId ?? metadata.teamId;
|
|
134
|
-
const workCategoryValue = route?.workCategory ??
|
|
113
|
+
const workCategoryValue = route?.workCategory ?? null;
|
|
135
114
|
const body = {
|
|
136
115
|
id: traceId,
|
|
137
116
|
name: args.name,
|
|
@@ -156,27 +135,17 @@ export async function cxTrace(args, { ROOT_DIR }) {
|
|
|
156
135
|
release: ctx.release,
|
|
157
136
|
};
|
|
158
137
|
|
|
159
|
-
|
|
160
|
-
body.metadata.version = 'v1';
|
|
161
|
-
|
|
162
|
-
// Use ingestion batch for creation (handles large payloads better than direct API)
|
|
163
|
-
const ingestClient = getOrCreateIngestClient();
|
|
164
|
-
if (ingestClient?.available) {
|
|
165
|
-
ingestClient.trace(body);
|
|
166
|
-
return { ok: true, id: traceId };
|
|
167
|
-
}
|
|
138
|
+
body.metadata.version = 'v1';
|
|
168
139
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
return { ok: true, id: traceId };
|
|
140
|
+
const client = createTelemetryClient({ env: telemetryEnv(), rootDir: ROOT_DIR });
|
|
141
|
+
client.trace(body);
|
|
142
|
+
await client.flush();
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
id: traceId,
|
|
146
|
+
backend: client.backend,
|
|
147
|
+
remoteStatus: client.remoteStatus,
|
|
148
|
+
};
|
|
180
149
|
} catch (err) {
|
|
181
150
|
return { ok: false, error: err.message, id: traceId };
|
|
182
151
|
}
|
|
@@ -193,25 +162,16 @@ export async function cxTraceUpdate(args) {
|
|
|
193
162
|
const metadata = args.metadata;
|
|
194
163
|
|
|
195
164
|
try {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
metadata: {
|
|
205
|
-
...(metadata && typeof metadata === 'object' ? metadata : {}),
|
|
206
|
-
traceUpdatedAt: new Date().toISOString(),
|
|
207
|
-
},
|
|
208
|
-
}),
|
|
165
|
+
const client = createTelemetryClient({ env: telemetryEnv(), rootDir: process.cwd() });
|
|
166
|
+
client.traceUpdate({
|
|
167
|
+
id: traceId,
|
|
168
|
+
output,
|
|
169
|
+
metadata: {
|
|
170
|
+
...(metadata && typeof metadata === 'object' ? metadata : {}),
|
|
171
|
+
traceUpdatedAt: new Date().toISOString(),
|
|
172
|
+
},
|
|
209
173
|
});
|
|
210
|
-
|
|
211
|
-
if (!res.ok) {
|
|
212
|
-
const text = await res.text().catch(() => '');
|
|
213
|
-
return { ok: false, error: `Telemetry PATCH error ${res.status}: ${text}` };
|
|
214
|
-
}
|
|
174
|
+
await client.flush();
|
|
215
175
|
|
|
216
176
|
// Also record as observation for local learning
|
|
217
177
|
if (output || metadata) {
|
|
@@ -227,7 +187,7 @@ export async function cxTraceUpdate(args) {
|
|
|
227
187
|
});
|
|
228
188
|
}
|
|
229
189
|
|
|
230
|
-
return { ok: true, traceId };
|
|
190
|
+
return { ok: true, traceId, backend: client.backend, remoteStatus: client.remoteStatus };
|
|
231
191
|
} catch (err) {
|
|
232
192
|
return { ok: false, error: err.message };
|
|
233
193
|
}
|
|
@@ -240,8 +200,6 @@ const SCORE_GOOD_THRESHOLD = 0.85; // at or above this → record positive pat
|
|
|
240
200
|
export async function cxScore(args) {
|
|
241
201
|
const traceId = args.trace_id ?? '';
|
|
242
202
|
try {
|
|
243
|
-
const available = await telemetryAvailable();
|
|
244
|
-
if (!available) return { ok: false, error: 'Telemetry credentials not configured' };
|
|
245
203
|
const body = {
|
|
246
204
|
id: crypto.randomUUID(),
|
|
247
205
|
traceId,
|
|
@@ -250,15 +208,9 @@ export async function cxScore(args) {
|
|
|
250
208
|
dataType: 'NUMERIC',
|
|
251
209
|
comment: args.comment,
|
|
252
210
|
};
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
body: JSON.stringify(body),
|
|
257
|
-
});
|
|
258
|
-
if (!res.ok) {
|
|
259
|
-
const text = await res.text().catch(() => '');
|
|
260
|
-
return { ok: false, error: `Telemetry API error ${res.status}: ${text}` };
|
|
261
|
-
}
|
|
211
|
+
const client = createTelemetryClient({ env: telemetryEnv(), rootDir: process.cwd() });
|
|
212
|
+
client.score(body);
|
|
213
|
+
await client.flush();
|
|
262
214
|
|
|
263
215
|
// Feed score back into the local observation store so future agents learn from it.
|
|
264
216
|
// Low scores generate anti-pattern observations; high scores reinforce positive patterns.
|
|
@@ -291,7 +243,7 @@ export async function cxScore(args) {
|
|
|
291
243
|
}
|
|
292
244
|
}
|
|
293
245
|
|
|
294
|
-
return { ok: true, traceId };
|
|
246
|
+
return { ok: true, traceId, backend: client.backend, remoteStatus: client.remoteStatus };
|
|
295
247
|
} catch (err) {
|
|
296
248
|
return { ok: false, error: err.message };
|
|
297
249
|
}
|