@geraldmaron/construct 1.0.2 → 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/.env.example +1 -1
- 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 +72 -11
- package/bin/construct-postinstall.mjs +1 -1
- 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/bootstrap/resources.mjs +2 -2
- package/lib/cli-commands.mjs +8 -2
- package/lib/document-ingest.mjs +6 -5
- package/lib/embed/cli.mjs +16 -3
- package/lib/embed/conflict-detection.mjs +26 -9
- package/lib/embed/customer-profiles.mjs +38 -18
- package/lib/embed/daemon.mjs +59 -50
- package/lib/embed/inbox.mjs +30 -0
- package/lib/embed/recommendation-store.mjs +214 -15
- package/lib/embed/workspaces.mjs +53 -18
- package/lib/evaluator-optimizer.mjs +0 -2
- package/lib/features.mjs +11 -0
- package/lib/gates-audit.mjs +3 -3
- package/lib/health-check.mjs +3 -5
- 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-unified.mjs +119 -3
- package/lib/init-update.mjs +246 -131
- package/lib/install/first-invocation.mjs +4 -4
- package/lib/intake/queue.mjs +1 -1
- package/lib/integrations/intake-integrations.mjs +4 -5
- package/lib/intent-classifier.mjs +1 -0
- package/lib/knowledge/layout.mjs +10 -0
- package/lib/knowledge/rag.mjs +16 -0
- package/lib/mcp/tools/telemetry.mjs +30 -78
- package/lib/model-cheapest-provider.mjs +231 -0
- package/lib/model-router.mjs +68 -9
- 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/project-profile.mjs +1 -1
- package/lib/prompt-composer.js +106 -29
- package/lib/read-tracker-store.mjs +149 -0
- package/lib/roles/catalog.mjs +133 -0
- package/lib/roles/preference.mjs +74 -0
- package/lib/server/index.mjs +109 -47
- package/lib/server/insights.mjs +1 -1
- package/lib/server/telemetry-login.mjs +17 -25
- package/lib/service-manager.mjs +32 -24
- package/lib/services/local-postgres.mjs +15 -0
- package/lib/services/telemetry-backend.mjs +2 -3
- package/lib/setup-prompts.mjs +2 -2
- package/lib/setup.mjs +55 -46
- package/lib/status.mjs +56 -8
- package/lib/storage/backend.mjs +12 -2
- package/lib/storage/postgres-backup.mjs +1 -1
- 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/uninstall/uninstall.mjs +1 -1
- 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/construct_guide.md +2 -2
- package/templates/docs/research-brief.md +63 -9
- package/templates/docs/strategy.md +36 -0
- package/templates/homebrew/construct.rb +7 -7
|
@@ -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
|
|
package/lib/init-unified.mjs
CHANGED
|
@@ -591,6 +591,108 @@ async function askDocumentationQuestions() {
|
|
|
591
591
|
return { lanes: selectedLanes, withArchitecture, withReadme };
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
+
// Intake collection — suggest directories to watch for context
|
|
595
|
+
function discoverProjectDirs(targetPath) {
|
|
596
|
+
const dirs = [];
|
|
597
|
+
let entries;
|
|
598
|
+
try { entries = fs.readdirSync(targetPath, { withFileTypes: true }); } catch { return dirs; }
|
|
599
|
+
|
|
600
|
+
for (const entry of entries) {
|
|
601
|
+
if (!entry.isDirectory()) continue;
|
|
602
|
+
const name = entry.name;
|
|
603
|
+
// Skip hidden dirs, node_modules, build artifacts
|
|
604
|
+
if (name.startsWith('.')) continue;
|
|
605
|
+
if (name === 'node_modules') continue;
|
|
606
|
+
if (name === 'dist' || name === 'build' || name === '.next' || name === '.out') continue;
|
|
607
|
+
dirs.push(name);
|
|
608
|
+
}
|
|
609
|
+
return dirs.sort();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const INTAKE_DIR_PRESETS = [
|
|
613
|
+
{ value: 'src', label: 'src/', reason: 'Source code — most projects put code here' },
|
|
614
|
+
{ value: 'lib', label: 'lib/', reason: 'Library code' },
|
|
615
|
+
{ value: 'packages', label: 'packages/', reason: 'Monorepo packages' },
|
|
616
|
+
{ value: 'services', label: 'services/', reason: 'Microservices or backend services' },
|
|
617
|
+
{ value: 'apps', label: 'apps/', reason: 'Monorepo apps' },
|
|
618
|
+
{ value: 'docs', label: 'docs/', reason: 'Documentation source' },
|
|
619
|
+
{ value: 'tests', label: 'tests/', reason: 'Test files' },
|
|
620
|
+
{ value: 'spec', label: 'spec/', reason: 'Specifications' },
|
|
621
|
+
{ value: 'infra', label: 'infra/', reason: 'Infrastructure code (Terraform, etc.)' },
|
|
622
|
+
{ value: 'config', label: 'config/', reason: 'Configuration files' },
|
|
623
|
+
{ value: 'scripts', label: 'scripts/', reason: 'Build/ops scripts' },
|
|
624
|
+
{ value: 'tools', label: 'tools/', reason: 'Internal tooling' },
|
|
625
|
+
];
|
|
626
|
+
|
|
627
|
+
async function askIntakeCollection(targetPath, skipInteractive) {
|
|
628
|
+
if (skipInteractive) {
|
|
629
|
+
// Non-interactive: auto-detect and suggest based on what exists
|
|
630
|
+
const existingDirs = discoverProjectDirs(targetPath);
|
|
631
|
+
const selected = INTAKE_DIR_PRESETS
|
|
632
|
+
.filter(preset => existingDirs.includes(preset.value))
|
|
633
|
+
.map(p => p.value);
|
|
634
|
+
|
|
635
|
+
if (selected.length === 0) return null;
|
|
636
|
+
|
|
637
|
+
console.log('');
|
|
638
|
+
console.log('═══════════════════════════════════════════════════════════');
|
|
639
|
+
console.log(' INTAKE COLLECTION');
|
|
640
|
+
console.log('═══════════════════════════════════════════════════════════');
|
|
641
|
+
console.log('');
|
|
642
|
+
console.log(`Auto-detected ${selected.length} directory(ies) to watch for context:`);
|
|
643
|
+
for (const d of selected) console.log(` • ${d}/`);
|
|
644
|
+
console.log('');
|
|
645
|
+
console.log('The inbox watcher will scan these directories for new files');
|
|
646
|
+
console.log('and route them through R&D triage automatically.');
|
|
647
|
+
console.log('');
|
|
648
|
+
|
|
649
|
+
return { parentDirs: selected, maxDepth: 4 };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Interactive mode
|
|
653
|
+
const existingDirs = discoverProjectDirs(targetPath);
|
|
654
|
+
const presetOptions = INTAKE_DIR_PRESETS.map(p => ({
|
|
655
|
+
label: p.label,
|
|
656
|
+
value: p.value,
|
|
657
|
+
checked: existingDirs.includes(p.value),
|
|
658
|
+
description: p.reason,
|
|
659
|
+
}));
|
|
660
|
+
|
|
661
|
+
console.log('');
|
|
662
|
+
console.log('═══════════════════════════════════════════════════════════');
|
|
663
|
+
console.log(' INTAKE COLLECTION');
|
|
664
|
+
console.log('═══════════════════════════════════════════════════════════');
|
|
665
|
+
console.log('');
|
|
666
|
+
console.log('Intake watches directories for new files and routes them');
|
|
667
|
+
console.log('through R&D triage. Select which directories to watch.');
|
|
668
|
+
console.log('(directories already found in your project are pre-checked)');
|
|
669
|
+
console.log('');
|
|
670
|
+
|
|
671
|
+
const selected = await multiSelect({
|
|
672
|
+
title: 'Directories to Watch',
|
|
673
|
+
instructions: 'Press Enter to confirm your selection',
|
|
674
|
+
options: presetOptions,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
if (selected.length === 0) {
|
|
678
|
+
console.log('');
|
|
679
|
+
console.log('No directories selected. The inbox watcher will only scan');
|
|
680
|
+
console.log('.cx/inbox/ and docs/intake/ (built-in zones).');
|
|
681
|
+
console.log('');
|
|
682
|
+
console.log('You can always add more later with:');
|
|
683
|
+
console.log(' construct intake config set --add-dir=src');
|
|
684
|
+
console.log('');
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
console.log('');
|
|
689
|
+
console.log(`Watching ${selected.length} directory(ies):`);
|
|
690
|
+
for (const d of selected) console.log(` • ${d}/`);
|
|
691
|
+
console.log('');
|
|
692
|
+
|
|
693
|
+
return { parentDirs: selected, maxDepth: 4 };
|
|
694
|
+
}
|
|
695
|
+
|
|
594
696
|
function buildProjectReadme(projectName) {
|
|
595
697
|
return `# ${projectName}
|
|
596
698
|
|
|
@@ -824,6 +926,20 @@ async function main() {
|
|
|
824
926
|
console.log('');
|
|
825
927
|
}
|
|
826
928
|
|
|
929
|
+
// Intake collection — ask which directories to watch for context
|
|
930
|
+
console.log('[TRACE init:intake-ask]');
|
|
931
|
+
|
|
932
|
+
const intakeConfig = await askIntakeCollection(target, skipInteractive);
|
|
933
|
+
if (intakeConfig) {
|
|
934
|
+
const { saveIntakeConfig } = await import('./intake/intake-config.mjs');
|
|
935
|
+
try {
|
|
936
|
+
saveIntakeConfig(target, intakeConfig);
|
|
937
|
+
created.push('.cx/intake-config.json');
|
|
938
|
+
} catch (err) {
|
|
939
|
+
console.warn(`⚠️ Could not write intake config: ${err.message}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
827
943
|
// Ask about documentation system
|
|
828
944
|
console.log('[TRACE init:docs-ask]');
|
|
829
945
|
|
|
@@ -895,7 +1011,7 @@ async function main() {
|
|
|
895
1011
|
// can compare new intake against established PRDs/RFCs/ADRs from day
|
|
896
1012
|
// one. Best-effort: skipped silently when Postgres + embedding model
|
|
897
1013
|
// aren't ready yet (DATABASE_URL unset, no ONNX cache). User can re-run
|
|
898
|
-
// `construct ingest` or `construct
|
|
1014
|
+
// `construct ingest` or `construct init` later to seed manually.
|
|
899
1015
|
|
|
900
1016
|
try {
|
|
901
1017
|
const { syncFileStateToSql } = await import('./storage/sync.mjs');
|
|
@@ -950,10 +1066,10 @@ async function main() {
|
|
|
950
1066
|
if (verbose) console.log('\nStarting services...');
|
|
951
1067
|
try {
|
|
952
1068
|
const { startServices } = await import('./service-manager.mjs');
|
|
953
|
-
const {
|
|
1069
|
+
const { homedir } = await import('node:os');
|
|
954
1070
|
const { detectDockerCompose } = await import('./setup.mjs');
|
|
955
1071
|
|
|
956
|
-
const homeDir =
|
|
1072
|
+
const homeDir = homedir();
|
|
957
1073
|
const composeRunner = detectDockerCompose();
|
|
958
1074
|
|
|
959
1075
|
const { results } = await startServices({
|