@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.
Files changed (75) hide show
  1. package/README.md +4 -4
  2. package/agents/prompts/cx-ai-engineer.md +6 -26
  3. package/agents/prompts/cx-architect.md +1 -0
  4. package/agents/prompts/cx-business-strategist.md +2 -0
  5. package/agents/prompts/cx-data-analyst.md +6 -26
  6. package/agents/prompts/cx-docs-keeper.md +1 -31
  7. package/agents/prompts/cx-explorer.md +1 -0
  8. package/agents/prompts/cx-orchestrator.md +40 -112
  9. package/agents/prompts/cx-platform-engineer.md +2 -22
  10. package/agents/prompts/cx-product-manager.md +2 -1
  11. package/agents/prompts/cx-qa.md +0 -20
  12. package/agents/prompts/cx-rd-lead.md +2 -0
  13. package/agents/prompts/cx-researcher.md +77 -31
  14. package/agents/prompts/cx-security.md +11 -49
  15. package/agents/prompts/cx-sre.md +9 -43
  16. package/agents/prompts/cx-ux-researcher.md +1 -0
  17. package/agents/role-manifests.json +4 -4
  18. package/bin/construct +23 -0
  19. package/db/schema/004_recommendations.sql +46 -0
  20. package/db/schema/005_strategy.sql +21 -0
  21. package/lib/auto-docs.mjs +1 -2
  22. package/lib/beads-automation.mjs +16 -7
  23. package/lib/cli-commands.mjs +8 -2
  24. package/lib/embed/conflict-detection.mjs +26 -9
  25. package/lib/embed/customer-profiles.mjs +37 -17
  26. package/lib/embed/daemon.mjs +10 -8
  27. package/lib/embed/recommendation-store.mjs +213 -14
  28. package/lib/embed/workspaces.mjs +53 -18
  29. package/lib/gates-audit.mjs +3 -3
  30. package/lib/health-check.mjs +1 -1
  31. package/lib/hooks/pre-compact.mjs +3 -0
  32. package/lib/hooks/read-tracker.mjs +10 -101
  33. package/lib/host-capabilities.mjs +90 -1
  34. package/lib/init-update.mjs +246 -131
  35. package/lib/intent-classifier.mjs +1 -0
  36. package/lib/knowledge/layout.mjs +10 -0
  37. package/lib/mcp/tools/telemetry.mjs +30 -78
  38. package/lib/model-router.mjs +61 -1
  39. package/lib/ollama-manager.mjs +1 -1
  40. package/lib/opencode-telemetry.mjs +4 -5
  41. package/lib/orchestration-policy.mjs +9 -0
  42. package/lib/parity.mjs +124 -21
  43. package/lib/prompt-composer.js +106 -29
  44. package/lib/read-tracker-store.mjs +149 -0
  45. package/lib/server/index.mjs +76 -0
  46. package/lib/server/telemetry-login.mjs +17 -25
  47. package/lib/service-manager.mjs +30 -22
  48. package/lib/services/local-postgres.mjs +15 -0
  49. package/lib/services/telemetry-backend.mjs +1 -2
  50. package/lib/setup.mjs +8 -43
  51. package/lib/status.mjs +51 -5
  52. package/lib/storage/backend.mjs +12 -2
  53. package/lib/strategy-store.mjs +371 -0
  54. package/lib/telemetry/backends/local.mjs +6 -4
  55. package/lib/telemetry/client.mjs +185 -0
  56. package/lib/telemetry/ingest.mjs +13 -5
  57. package/lib/telemetry/team-rollup.mjs +9 -2
  58. package/lib/worker/trace.mjs +17 -27
  59. package/package.json +5 -2
  60. package/rules/common/research.md +44 -12
  61. package/skills/docs/backlog-proposal-workflow.md +2 -2
  62. package/skills/docs/customer-profile-workflow.md +1 -1
  63. package/skills/docs/evidence-ingest-workflow.md +5 -5
  64. package/skills/docs/prfaq-workflow.md +1 -1
  65. package/skills/docs/product-intelligence-review.md +1 -1
  66. package/skills/docs/product-intelligence-workflow.md +3 -3
  67. package/skills/docs/product-signal-workflow.md +48 -18
  68. package/skills/docs/research-workflow.md +26 -14
  69. package/skills/docs/strategy-workflow.md +36 -0
  70. package/skills/roles/data-analyst.product-intelligence.md +1 -1
  71. package/skills/roles/researcher.md +28 -15
  72. package/skills/routing.md +8 -1
  73. package/templates/docs/research-brief.md +63 -9
  74. package/templates/docs/strategy.md +36 -0
  75. 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, appendFileSync } from 'fs';
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
- const stats = loadEfficiencyStats(nowIso);
99
- const existingFile = stats.files[absPath];
100
- stats.readCount += 1;
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
- lastReadAt: nowIso,
112
- lastRequestedLimit: effectiveLimit,
113
- };
52
+ limit: effectiveLimit,
53
+ ts: nowIso,
54
+ }, process.env);
114
55
 
115
- // LRU cap: bound stats.files to FILES_LRU_CAP entries by lastReadAt so the
116
- // structure does not grow without limit across long sessions.
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