@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
|
@@ -7,63 +7,14 @@
|
|
|
7
7
|
* @p95ms 10
|
|
8
8
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
9
9
|
*/
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
11
11
|
import { createHash } from 'crypto';
|
|
12
12
|
import { join, resolve } from 'path';
|
|
13
13
|
import { homedir } from 'os';
|
|
14
|
+
import { flushReadTrackerDeltas, recordReadDelta } from '../read-tracker-store.mjs';
|
|
14
15
|
|
|
15
16
|
const CX_DIR = join(homedir(), '.cx');
|
|
16
17
|
const HASH_STORE = join(CX_DIR, 'file-hashes.json');
|
|
17
|
-
const EFFICIENCY_STORE = join(CX_DIR, 'session-efficiency.json');
|
|
18
|
-
const WARN_FLAGS = join(CX_DIR, 'warn-flags.txt');
|
|
19
|
-
const SESSION_IDLE_RESET_MS = 2 * 60 * 60 * 1000;
|
|
20
|
-
const REPEATED_READ_WARNING_THRESHOLD = 5;
|
|
21
|
-
const REPEATED_READ_TIER2_THRESHOLD = 8;
|
|
22
|
-
const LARGE_READ_WARNING_THRESHOLD = 3;
|
|
23
|
-
const TOTAL_BYTES_WARNING_THRESHOLD = 750_000;
|
|
24
|
-
|
|
25
|
-
// Bound per-session `stats.files` so the read-tracking map cannot grow
|
|
26
|
-
// without limit across long sessions. The hook is a fresh node process per
|
|
27
|
-
// Read, so every invocation must read-mutate-write the full state — there
|
|
28
|
-
// is no in-memory accumulator that would survive between calls. The LRU cap
|
|
29
|
-
// drops the oldest entries by `lastReadAt`.
|
|
30
|
-
|
|
31
|
-
const FILES_LRU_CAP = 200;
|
|
32
|
-
|
|
33
|
-
function loadEfficiencyStats(nowIso) {
|
|
34
|
-
const fresh = {
|
|
35
|
-
sessionStartedAt: nowIso,
|
|
36
|
-
lastUpdatedAt: nowIso,
|
|
37
|
-
readCount: 0,
|
|
38
|
-
uniqueFileCount: 0,
|
|
39
|
-
repeatedReadCount: 0,
|
|
40
|
-
largeReadCount: 0,
|
|
41
|
-
totalBytesRead: 0,
|
|
42
|
-
warnings: {},
|
|
43
|
-
files: {},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
const existing = JSON.parse(readFileSync(EFFICIENCY_STORE, 'utf8'));
|
|
48
|
-
const lastUpdated = new Date(existing.lastUpdatedAt || 0).getTime();
|
|
49
|
-
if (!lastUpdated || Date.now() - lastUpdated > SESSION_IDLE_RESET_MS) return fresh;
|
|
50
|
-
return { ...fresh, ...existing, warnings: existing.warnings || {}, files: existing.files || {} };
|
|
51
|
-
} catch {
|
|
52
|
-
return fresh;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function appendWarning(message) {
|
|
57
|
-
try { appendFileSync(WARN_FLAGS, `${message}\n`); } catch { /* best effort */ }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function topRepeatedPath(files) {
|
|
61
|
-
return Object.entries(files || {})
|
|
62
|
-
.map(([filePath, value]) => ({ filePath, count: Number(value?.count || 0) }))
|
|
63
|
-
.filter((entry) => entry.count > 1)
|
|
64
|
-
.sort((a, b) => b.count - a.count || a.filePath.localeCompare(b.filePath))[0];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
18
|
let input = {};
|
|
68
19
|
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
69
20
|
|
|
@@ -81,7 +32,6 @@ try {
|
|
|
81
32
|
const nowIso = new Date().toISOString();
|
|
82
33
|
const requestedLimit = Number(input?.tool_input?.limit || 0);
|
|
83
34
|
const effectiveLimit = requestedLimit > 0 ? requestedLimit : 2000;
|
|
84
|
-
const isLargeRead = effectiveLimit > 400;
|
|
85
35
|
|
|
86
36
|
mkdirSync(CX_DIR, { recursive: true });
|
|
87
37
|
let store = {};
|
|
@@ -95,58 +45,17 @@ try {
|
|
|
95
45
|
if (new Date(v.ts).getTime() < cutoff) delete store[k];
|
|
96
46
|
}
|
|
97
47
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
stats.totalBytesRead += content.length;
|
|
102
|
-
if (isLargeRead) stats.largeReadCount += 1;
|
|
103
|
-
if (existingFile) {
|
|
104
|
-
stats.repeatedReadCount += 1;
|
|
105
|
-
} else {
|
|
106
|
-
stats.uniqueFileCount += 1;
|
|
107
|
-
}
|
|
108
|
-
stats.files[absPath] = {
|
|
109
|
-
count: (existingFile?.count || 0) + 1,
|
|
48
|
+
writeFileSync(HASH_STORE, JSON.stringify(store, null, 2));
|
|
49
|
+
recordReadDelta({
|
|
50
|
+
path: absPath,
|
|
110
51
|
size: content.length,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
};
|
|
52
|
+
limit: effectiveLimit,
|
|
53
|
+
ts: nowIso,
|
|
54
|
+
}, process.env);
|
|
114
55
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const fileEntries = Object.entries(stats.files);
|
|
118
|
-
if (fileEntries.length > FILES_LRU_CAP) {
|
|
119
|
-
fileEntries.sort((a, b) => (a[1]?.lastReadAt || '').localeCompare(b[1]?.lastReadAt || ''));
|
|
120
|
-
const drop = fileEntries.length - FILES_LRU_CAP;
|
|
121
|
-
for (let i = 0; i < drop; i++) delete stats.files[fileEntries[i][0]];
|
|
56
|
+
if (process.env.CONSTRUCT_READ_TRACKER_FLUSH === '1') {
|
|
57
|
+
flushReadTrackerDeltas({ nowIso, env: process.env });
|
|
122
58
|
}
|
|
123
|
-
|
|
124
|
-
if (stats.repeatedReadCount >= REPEATED_READ_WARNING_THRESHOLD && !stats.warnings.repeatedReads) {
|
|
125
|
-
const top = topRepeatedPath(stats.files);
|
|
126
|
-
const topNote = top ? ` Top repeat: ${top.filePath} (${top.count}x).` : '';
|
|
127
|
-
appendWarning(`Efficiency: ${stats.repeatedReadCount} repeated reads this session.${topNote} Use rg or construct distill before re-reading more files.`);
|
|
128
|
-
stats.warnings.repeatedReads = nowIso;
|
|
129
|
-
|
|
130
|
-
if (stats.repeatedReadCount >= REPEATED_READ_TIER2_THRESHOLD && !stats.warnings.repeatedReadsTier2) {
|
|
131
|
-
appendWarning(`Efficiency: ${stats.repeatedReadCount} repeated reads this session — consider using construct distill or rg before re-reading entire files.`);
|
|
132
|
-
stats.warnings.repeatedReadsTier2 = nowIso;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (stats.largeReadCount >= LARGE_READ_WARNING_THRESHOLD && !stats.warnings.largeReads) {
|
|
137
|
-
appendWarning(`Efficiency: ${stats.largeReadCount} large reads this session — prefer rg/glob plus targeted reads under 400 lines.`);
|
|
138
|
-
stats.warnings.largeReads = nowIso;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (stats.totalBytesRead >= TOTAL_BYTES_WARNING_THRESHOLD && !stats.warnings.totalBytes) {
|
|
142
|
-
appendWarning(`Efficiency: ${Math.round(stats.totalBytesRead / 1024)} KB read this session — consider distill/query-focused retrieval or compact context before continuing.`);
|
|
143
|
-
stats.warnings.totalBytes = nowIso;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
stats.lastUpdatedAt = nowIso;
|
|
147
|
-
|
|
148
|
-
writeFileSync(HASH_STORE, JSON.stringify(store, null, 2));
|
|
149
|
-
writeFileSync(EFFICIENCY_STORE, JSON.stringify(stats, null, 2));
|
|
150
59
|
} catch { /* best effort */ }
|
|
151
60
|
|
|
152
61
|
process.exit(0);
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { execFileSync, execSync } from "node:child_process";
|
|
11
11
|
import { readOpenCodeConfig } from "./opencode-config.mjs";
|
|
12
|
+
import fs from "node:fs";
|
|
12
13
|
import net from "node:net";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
13
16
|
|
|
14
17
|
const LOWEST_PORT = 0;
|
|
15
18
|
const HIGHEST_PORT = 65535;
|
|
@@ -46,7 +49,8 @@ function commandVersion(command, args = ["--version"]) {
|
|
|
46
49
|
try {
|
|
47
50
|
return execFileSync(command, args, { stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
|
|
48
51
|
.toString()
|
|
49
|
-
.trim()
|
|
52
|
+
.trim()
|
|
53
|
+
.split(/\r?\n/, 1)[0];
|
|
50
54
|
} catch {
|
|
51
55
|
return null;
|
|
52
56
|
}
|
|
@@ -71,6 +75,49 @@ function versionAtLeast(version, minimum) {
|
|
|
71
75
|
return true;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
function detectVsCodeAvailability(homeDir = os.homedir()) {
|
|
79
|
+
const settingsCandidates = (() => {
|
|
80
|
+
const platform = os.platform();
|
|
81
|
+
if (platform === "darwin") {
|
|
82
|
+
return [
|
|
83
|
+
path.join(homeDir, "Library", "Application Support", "Code", "User", "settings.json"),
|
|
84
|
+
path.join(homeDir, "Library", "Application Support", "Code - Insiders", "User", "settings.json"),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
if (platform === "linux") {
|
|
88
|
+
return [
|
|
89
|
+
path.join(homeDir, ".config", "Code", "User", "settings.json"),
|
|
90
|
+
path.join(homeDir, ".config", "Code - Insiders", "User", "settings.json"),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
if (platform === "win32") {
|
|
94
|
+
const appData = process.env.APPDATA ?? path.join(homeDir, "AppData", "Roaming");
|
|
95
|
+
return [
|
|
96
|
+
path.join(appData, "Code", "User", "settings.json"),
|
|
97
|
+
path.join(appData, "Code - Insiders", "User", "settings.json"),
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
const version = commandVersion("code") || commandVersion("code-insiders");
|
|
104
|
+
const hasSettings = settingsCandidates.some((candidate) => fs.existsSync(candidate));
|
|
105
|
+
return { version, hasSettings };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function detectCursorAvailability(homeDir = os.homedir()) {
|
|
109
|
+
const version = commandVersion("cursor") || commandVersion("cursor-agent");
|
|
110
|
+
const hasConfig = fs.existsSync(path.join(homeDir, ".cursor", "mcp.json"));
|
|
111
|
+
return { version, hasConfig };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function detectCopilotAvailability(homeDir = os.homedir()) {
|
|
115
|
+
const promptsDir = path.join(homeDir, ".github", "prompts");
|
|
116
|
+
const instructionsPath = path.join(homeDir, ".github", "copilot-instructions.md");
|
|
117
|
+
const hasFiles = fs.existsSync(promptsDir) || fs.existsSync(instructionsPath);
|
|
118
|
+
return { hasFiles };
|
|
119
|
+
}
|
|
120
|
+
|
|
74
121
|
export function detectHostCapabilities() {
|
|
75
122
|
const claudeRaw = commandVersion("claude");
|
|
76
123
|
const claudeVersion = parseClaudeVersion(claudeRaw);
|
|
@@ -78,6 +125,9 @@ export function detectHostCapabilities() {
|
|
|
78
125
|
const tmuxRaw = commandVersion("tmux", ["-V"]);
|
|
79
126
|
const opencodeRaw = commandVersion("opencode");
|
|
80
127
|
const codexRaw = commandVersion("codex");
|
|
128
|
+
const vscode = detectVsCodeAvailability();
|
|
129
|
+
const cursor = detectCursorAvailability();
|
|
130
|
+
const copilot = detectCopilotAvailability();
|
|
81
131
|
|
|
82
132
|
return [
|
|
83
133
|
{
|
|
@@ -123,6 +173,45 @@ export function detectHostCapabilities() {
|
|
|
123
173
|
"Native profile switching is not automatic; continue in-session when dispatch is unavailable.",
|
|
124
174
|
],
|
|
125
175
|
},
|
|
176
|
+
{
|
|
177
|
+
host: "VS Code",
|
|
178
|
+
availability: vscode.version || vscode.hasSettings ? "installed" : "missing",
|
|
179
|
+
version: vscode.version,
|
|
180
|
+
orchestration: "copilot-mcp",
|
|
181
|
+
promptableWorkers: false,
|
|
182
|
+
sharedTaskRuntime: "editor-session",
|
|
183
|
+
lifecycleHooks: [],
|
|
184
|
+
notes: [
|
|
185
|
+
"Construct manages GitHub Copilot MCP registrations in user settings when a VS Code settings.json already exists.",
|
|
186
|
+
"VS Code is an editor surface, not Construct's primary multi-agent runtime.",
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
host: "Cursor",
|
|
191
|
+
availability: cursor.version || cursor.hasConfig ? "installed" : "missing",
|
|
192
|
+
version: cursor.version,
|
|
193
|
+
orchestration: "mcp-only",
|
|
194
|
+
promptableWorkers: false,
|
|
195
|
+
sharedTaskRuntime: "editor-session",
|
|
196
|
+
lifecycleHooks: [],
|
|
197
|
+
notes: [
|
|
198
|
+
"Construct manages Cursor MCP registrations in ~/.cursor/mcp.json when that file exists.",
|
|
199
|
+
"Cursor agent profiles are not generated today; use Construct through MCP-backed tools and shared project state.",
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
host: "Copilot",
|
|
204
|
+
availability: copilot.hasFiles ? "installed" : "missing",
|
|
205
|
+
version: null,
|
|
206
|
+
orchestration: "prompt-profiles",
|
|
207
|
+
promptableWorkers: false,
|
|
208
|
+
sharedTaskRuntime: "editor-session",
|
|
209
|
+
lifecycleHooks: [],
|
|
210
|
+
notes: [
|
|
211
|
+
"Construct writes reusable prompt profiles under ~/.github/prompts/ plus a managed block in ~/.github/copilot-instructions.md.",
|
|
212
|
+
"Copilot does not expose Construct-style subagent dispatch; use the generated profiles for focused passes.",
|
|
213
|
+
],
|
|
214
|
+
},
|
|
126
215
|
];
|
|
127
216
|
}
|
|
128
217
|
|