@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
package/lib/cli-commands.mjs
CHANGED
|
@@ -79,7 +79,13 @@ export const CLI_COMMANDS = [
|
|
|
79
79
|
category: 'Core',
|
|
80
80
|
core: true,
|
|
81
81
|
description: 'Sync agent adapters to AI tools',
|
|
82
|
-
usage: 'construct sync',
|
|
82
|
+
usage: 'construct sync [--project] [--dry-run] [--no-docs] [--compress-personas]',
|
|
83
|
+
options: [
|
|
84
|
+
{ flag: '--project', desc: 'Write project-local Claude adapters into the current repo only' },
|
|
85
|
+
{ flag: '--dry-run', desc: 'Preview adapter changes without writing files' },
|
|
86
|
+
{ flag: '--no-docs', desc: 'Skip AUTO docs regeneration after syncing adapters' },
|
|
87
|
+
{ flag: '--compress-personas', desc: 'Write compressed runtime persona prompts without changing the source prompts' },
|
|
88
|
+
],
|
|
83
89
|
},
|
|
84
90
|
{
|
|
85
91
|
name: 'intake',
|
|
@@ -475,7 +481,7 @@ export const CLI_COMMANDS = [
|
|
|
475
481
|
category: 'Advanced',
|
|
476
482
|
core: false,
|
|
477
483
|
description: 'Show version',
|
|
478
|
-
usage: 'construct version',
|
|
484
|
+
usage: 'construct version | construct --version',
|
|
479
485
|
},
|
|
480
486
|
{
|
|
481
487
|
name: 'cleanup',
|
package/lib/document-ingest.mjs
CHANGED
|
@@ -77,7 +77,7 @@ function renderMarkdown({ sourcePath, extractedAt, title, extractionMethod, char
|
|
|
77
77
|
return lines.join('\n');
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function collectInputFiles(inputPath) {
|
|
80
|
+
function collectInputFiles(inputPath, { maxDepth = 10 } = {}) {
|
|
81
81
|
const resolvedPath = resolve(inputPath);
|
|
82
82
|
if (!existsSync(resolvedPath)) throw new Error(`Input path not found: ${resolvedPath}`);
|
|
83
83
|
|
|
@@ -90,13 +90,14 @@ function collectInputFiles(inputPath) {
|
|
|
90
90
|
if (!stat.isDirectory()) return [];
|
|
91
91
|
|
|
92
92
|
const files = [];
|
|
93
|
-
const stack = [resolvedPath];
|
|
93
|
+
const stack = [{ path: resolvedPath, depth: 0 }];
|
|
94
94
|
while (stack.length > 0) {
|
|
95
|
-
const current = stack.pop();
|
|
95
|
+
const { path: current, depth } = stack.pop();
|
|
96
|
+
if (depth >= maxDepth) continue;
|
|
96
97
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
97
98
|
const full = join(current, entry.name);
|
|
98
99
|
if (entry.isDirectory()) {
|
|
99
|
-
stack.push(full);
|
|
100
|
+
stack.push({ path: full, depth: depth + 1 });
|
|
100
101
|
continue;
|
|
101
102
|
}
|
|
102
103
|
if (entry.isFile() && isExtractableDocumentPath(full)) files.push(full);
|
|
@@ -135,7 +136,7 @@ export async function ingestDocuments(inputPaths, {
|
|
|
135
136
|
throw new Error('--out can only be used with a single input path');
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
const files = inputPaths.flatMap((inputPath) => collectInputFiles(isAbsolute(inputPath) ? inputPath : resolve(cwd, inputPath)));
|
|
139
|
+
const files = inputPaths.flatMap((inputPath) => collectInputFiles(isAbsolute(inputPath) ? inputPath : resolve(cwd, inputPath), { maxDepth: 10 }));
|
|
139
140
|
if (files.length === 0) {
|
|
140
141
|
throw new Error('No supported document files found');
|
|
141
142
|
}
|
package/lib/embed/cli.mjs
CHANGED
|
@@ -174,7 +174,7 @@ function parseArgs(args) {
|
|
|
174
174
|
// Subcommand: start
|
|
175
175
|
// ---------------------------------------------------------------------------
|
|
176
176
|
|
|
177
|
-
async function cmdEmbedStart(args, { homeDir = os.homedir(), rootDir } = {}) {
|
|
177
|
+
async function cmdEmbedStart(args, { homeDir = os.homedir(), rootDir, _workerPath, _livenessCheckMs = 400 } = {}) {
|
|
178
178
|
const existing = readRunningState(homeDir);
|
|
179
179
|
if (existing) {
|
|
180
180
|
process.stdout.write(`embed daemon already running (pid ${existing.pid})\n`);
|
|
@@ -188,7 +188,11 @@ async function cmdEmbedStart(args, { homeDir = os.homedir(), rootDir } = {}) {
|
|
|
188
188
|
? path.join(os.homedir(), '.construct', 'embed.yaml')
|
|
189
189
|
: null;
|
|
190
190
|
|
|
191
|
-
const workerPath = path.join(rootDir, 'lib', 'embed', 'worker.mjs');
|
|
191
|
+
const workerPath = _workerPath ?? path.join(rootDir, 'lib', 'embed', 'worker.mjs');
|
|
192
|
+
if (!fs.existsSync(workerPath)) {
|
|
193
|
+
throw new Error(`embed worker not found at ${workerPath}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
192
196
|
const workerArgs = configPath ? [workerPath, '--config', configPath] : [workerPath];
|
|
193
197
|
const log = logPath(homeDir);
|
|
194
198
|
fs.mkdirSync(path.dirname(log), { recursive: true });
|
|
@@ -199,9 +203,18 @@ async function cmdEmbedStart(args, { homeDir = os.homedir(), rootDir } = {}) {
|
|
|
199
203
|
stdio: ['ignore', fd, fd],
|
|
200
204
|
env: { ...process.env },
|
|
201
205
|
});
|
|
206
|
+
fs.closeSync(fd);
|
|
202
207
|
child.unref();
|
|
203
208
|
|
|
204
209
|
writeState({ pid: child.pid, configPath: configPath ?? 'auto', startedAt: new Date().toISOString() }, homeDir);
|
|
210
|
+
|
|
211
|
+
// Brief liveness check — detects immediate crashes (e.g. missing module, bad config path)
|
|
212
|
+
await new Promise(r => setTimeout(r, _livenessCheckMs));
|
|
213
|
+
if (!processExists(child.pid)) {
|
|
214
|
+
clearState(homeDir);
|
|
215
|
+
throw new Error(`embed worker (pid ${child.pid}) exited immediately — check ${log}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
205
218
|
process.stdout.write(`embed daemon started (pid ${child.pid})\n`);
|
|
206
219
|
process.stdout.write(`config: ${configPath ?? 'auto-discover from config.env'}\n`);
|
|
207
220
|
process.stdout.write(`log: ${log}\n`);
|
|
@@ -299,7 +312,7 @@ export async function runEmbedCli(args, opts = {}) {
|
|
|
299
312
|
const rootDir = opts.rootDir ?? new URL('../..', import.meta.url).pathname;
|
|
300
313
|
|
|
301
314
|
switch (sub) {
|
|
302
|
-
case 'start': return cmdEmbedStart(subArgs, { homeDir, rootDir });
|
|
315
|
+
case 'start': return cmdEmbedStart(subArgs, { homeDir, rootDir, _workerPath: opts._workerPath, _livenessCheckMs: opts._livenessCheckMs });
|
|
303
316
|
case 'stop': return cmdEmbedStop(subArgs, { homeDir });
|
|
304
317
|
case 'status': return cmdEmbedStatus(subArgs, { homeDir });
|
|
305
318
|
case 'snapshot': return cmdEmbedSnapshot(subArgs, { homeDir });
|
|
@@ -11,18 +11,33 @@
|
|
|
11
11
|
* 2. Contradiction detection — new signal contradicts a documented decision
|
|
12
12
|
*
|
|
13
13
|
* Storage:
|
|
14
|
-
* ~/.cx/
|
|
14
|
+
* ~/.cx/knowledge/reference/artifact-index.json — cached artifact embeddings
|
|
15
15
|
* Rebuilt on first use or when artifact file count changes.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, renameSync } from 'node:fs';
|
|
19
19
|
import { join } from 'node:path';
|
|
20
|
-
import { homedir } from 'node:os';
|
|
21
20
|
import { embed, cosineSimilarity, extractTextFromPacket } from './semantic.mjs';
|
|
22
21
|
import { listArtifacts } from './artifact.mjs';
|
|
22
|
+
import { cxDir } from '../paths.mjs';
|
|
23
|
+
import { knowledgeReferenceStore } from '../knowledge/layout.mjs';
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
function migrateLegacyIndex(indexFile, legacyFile) {
|
|
26
|
+
if (existsSync(indexFile) || !existsSync(legacyFile)) return;
|
|
27
|
+
mkdirSync(join(indexFile, '..'), { recursive: true });
|
|
28
|
+
try { renameSync(legacyFile, indexFile); } catch { /* compatibility-only */ }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function indexPaths({ migrate = false } = {}) {
|
|
32
|
+
const indexDir = join(cxDir(), knowledgeReferenceStore());
|
|
33
|
+
const legacyFile = join(cxDir(), 'product-intel', 'artifact-index.json');
|
|
34
|
+
const indexFile = join(indexDir, 'artifact-index.json');
|
|
35
|
+
if (migrate) migrateLegacyIndex(indexFile, legacyFile);
|
|
36
|
+
return {
|
|
37
|
+
indexDir,
|
|
38
|
+
indexFile,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
26
41
|
|
|
27
42
|
const OVERLAP_THRESHOLD = 0.6;
|
|
28
43
|
const CONTRADICTION_THRESHOLD = 0.7;
|
|
@@ -32,7 +47,8 @@ const STALE_INDEX_HOURS = 24;
|
|
|
32
47
|
* Ensure index directory exists.
|
|
33
48
|
*/
|
|
34
49
|
function ensureDir() {
|
|
35
|
-
|
|
50
|
+
const { indexDir } = indexPaths({ migrate: true });
|
|
51
|
+
if (!existsSync(indexDir)) mkdirSync(indexDir, { recursive: true });
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
let _artifactIndex = null;
|
|
@@ -55,9 +71,10 @@ export async function buildArtifactIndex({ rootDir, forceRebuild = false } = {})
|
|
|
55
71
|
const cacheHash = cacheKey.length;
|
|
56
72
|
|
|
57
73
|
// Check if cached index is fresh
|
|
58
|
-
|
|
74
|
+
const { indexFile } = indexPaths();
|
|
75
|
+
if (!forceRebuild && existsSync(indexFile)) {
|
|
59
76
|
try {
|
|
60
|
-
const cached = JSON.parse(readFileSync(
|
|
77
|
+
const cached = JSON.parse(readFileSync(indexFile, 'utf8'));
|
|
61
78
|
if (cached.cacheHash === cacheHash && cached.index?.length === artifacts.length) {
|
|
62
79
|
const age = Date.now() - new Date(cached.builtAt).getTime();
|
|
63
80
|
if (age < STALE_INDEX_HOURS * 60 * 60 * 1000) {
|
|
@@ -96,7 +113,7 @@ export async function buildArtifactIndex({ rootDir, forceRebuild = false } = {})
|
|
|
96
113
|
embedding: a.embedding ? Array.from(a.embedding) : null,
|
|
97
114
|
}));
|
|
98
115
|
|
|
99
|
-
writeFileSync(
|
|
116
|
+
writeFileSync(indexFile, JSON.stringify({
|
|
100
117
|
cacheHash,
|
|
101
118
|
builtAt: new Date().toISOString(),
|
|
102
119
|
artifactCount: artifacts.length,
|
|
@@ -1,31 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/embed/customer-profiles.mjs — Global customer profile store.
|
|
3
3
|
*
|
|
4
|
-
* Maintains durable customer/account memory in ~/.cx/
|
|
4
|
+
* Maintains durable customer/account memory in ~/.cx/knowledge/internal/customer-profiles/.
|
|
5
5
|
* Profiles are additive — history is preserved unless explicitly deleted.
|
|
6
|
-
*
|
|
6
|
+
* Links signals to customers and detects account-level patterns during intake triage.
|
|
7
7
|
*
|
|
8
8
|
* Storage:
|
|
9
|
-
* ~/.cx/
|
|
10
|
-
* ~/.cx/
|
|
9
|
+
* ~/.cx/knowledge/internal/customer-profiles/<customer-id>.md
|
|
10
|
+
* ~/.cx/knowledge/internal/customer-profiles/index.json — quick lookup by name/email
|
|
11
11
|
*
|
|
12
12
|
* Each profile follows templates/docs/customer-profile.md schema.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, appendFileSync } from 'node:fs';
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, appendFileSync, renameSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
|
-
import {
|
|
18
|
+
import { cxDir } from '../paths.mjs';
|
|
19
|
+
import { knowledgeInternalStore } from '../knowledge/layout.mjs';
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
function migrateLegacyDir(modernDir, legacyDir) {
|
|
22
|
+
if (existsSync(modernDir) || !existsSync(legacyDir)) return;
|
|
23
|
+
mkdirSync(join(modernDir, '..'), { recursive: true });
|
|
24
|
+
try { renameSync(legacyDir, modernDir); } catch { /* compatibility-only */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function profilePaths({ migrate = false } = {}) {
|
|
28
|
+
const modernDir = join(cxDir(), knowledgeInternalStore('customer-profiles'));
|
|
29
|
+
const legacyDir = join(cxDir(), 'product-intel', 'customer-profiles');
|
|
30
|
+
if (migrate) migrateLegacyDir(modernDir, legacyDir);
|
|
31
|
+
const profilesDir = existsSync(modernDir) || !existsSync(legacyDir) ? modernDir : legacyDir;
|
|
32
|
+
return {
|
|
33
|
+
profilesDir,
|
|
34
|
+
indexFile: join(profilesDir, 'index.json'),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
22
37
|
|
|
23
38
|
/**
|
|
24
39
|
* Ensure profile directory exists.
|
|
25
40
|
*/
|
|
26
41
|
function ensureDir() {
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
const { profilesDir } = profilePaths({ migrate: true });
|
|
43
|
+
if (!existsSync(profilesDir)) {
|
|
44
|
+
mkdirSync(profilesDir, { recursive: true });
|
|
29
45
|
}
|
|
30
46
|
}
|
|
31
47
|
|
|
@@ -34,9 +50,10 @@ function ensureDir() {
|
|
|
34
50
|
* @returns {{ [key: string]: { id: string, name: string, status: string, updatedAt: string } }}
|
|
35
51
|
*/
|
|
36
52
|
function readIndex() {
|
|
37
|
-
|
|
53
|
+
const { indexFile } = profilePaths();
|
|
54
|
+
if (!existsSync(indexFile)) return {};
|
|
38
55
|
try {
|
|
39
|
-
return JSON.parse(readFileSync(
|
|
56
|
+
return JSON.parse(readFileSync(indexFile, 'utf8'));
|
|
40
57
|
} catch (err) {
|
|
41
58
|
process.stderr.write('[customer-profiles.mjs] readIndex: ' + (err?.message ?? String(err)) + '\n');
|
|
42
59
|
return {};
|
|
@@ -48,8 +65,9 @@ function readIndex() {
|
|
|
48
65
|
* @param {object} index
|
|
49
66
|
*/
|
|
50
67
|
function writeIndex(index) {
|
|
68
|
+
const { indexFile } = profilePaths();
|
|
51
69
|
ensureDir();
|
|
52
|
-
writeFileSync(
|
|
70
|
+
writeFileSync(indexFile, JSON.stringify(index, null, 2) + '\n');
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
/**
|
|
@@ -95,7 +113,7 @@ export function createCustomerProfile({ name, owner, workspace, status = 'active
|
|
|
95
113
|
const id = generateCustomerId();
|
|
96
114
|
const slug = slugify(name);
|
|
97
115
|
const filename = `${slug}-${id}.md`;
|
|
98
|
-
const filePath = join(
|
|
116
|
+
const filePath = join(profilePaths().profilesDir, filename);
|
|
99
117
|
|
|
100
118
|
const date = new Date().toISOString().slice(0, 10);
|
|
101
119
|
const profile = {
|
|
@@ -219,12 +237,13 @@ TODO
|
|
|
219
237
|
*/
|
|
220
238
|
export function getCustomerProfile(customerId) {
|
|
221
239
|
ensureDir();
|
|
240
|
+
const { profilesDir } = profilePaths();
|
|
222
241
|
|
|
223
242
|
// Try direct lookup by ID in filename
|
|
224
|
-
const files = readdirSync(
|
|
243
|
+
const files = readdirSync(profilesDir).filter(f => f.endsWith('.md'));
|
|
225
244
|
for (const file of files) {
|
|
226
245
|
if (file.includes(customerId)) {
|
|
227
|
-
const filePath = join(
|
|
246
|
+
const filePath = join(profilesDir, file);
|
|
228
247
|
return parseProfileFile(filePath);
|
|
229
248
|
}
|
|
230
249
|
}
|
|
@@ -239,6 +258,7 @@ export function getCustomerProfile(customerId) {
|
|
|
239
258
|
*/
|
|
240
259
|
export function searchCustomerProfiles(query) {
|
|
241
260
|
ensureDir();
|
|
261
|
+
const { profilesDir } = profilePaths();
|
|
242
262
|
|
|
243
263
|
const index = readIndex();
|
|
244
264
|
const querySlug = slugify(query);
|
|
@@ -268,7 +288,7 @@ export function searchCustomerProfiles(query) {
|
|
|
268
288
|
id: entry.id,
|
|
269
289
|
name: entry.name,
|
|
270
290
|
status: entry.status,
|
|
271
|
-
path: join(
|
|
291
|
+
path: join(profilesDir, `${slugify(entry.name)}-${entry.id}.md`),
|
|
272
292
|
});
|
|
273
293
|
}
|
|
274
294
|
}
|
|
@@ -328,7 +348,7 @@ export function updateCustomerProfile(customerId, updates) {
|
|
|
328
348
|
throw new Error(`Customer profile not found: ${customerId}`);
|
|
329
349
|
}
|
|
330
350
|
|
|
331
|
-
const filePath = join(
|
|
351
|
+
const filePath = join(profilePaths().profilesDir, `${slugify(profile.name)}-${customerId}.md`);
|
|
332
352
|
let content = readFileSync(filePath, 'utf8');
|
|
333
353
|
const date = new Date().toISOString().slice(0, 10);
|
|
334
354
|
|
package/lib/embed/daemon.mjs
CHANGED
|
@@ -58,8 +58,8 @@ function logCatch(source, err) {
|
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Resolve an LLM API key from multiple sources: env → config.env → ~/.env → shell rc.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
61
|
+
* Supports 1Password op:// refs in .zshrc or other shell config files for the
|
|
62
|
+
* telemetry-generation probe.
|
|
63
63
|
*/
|
|
64
64
|
async function resolveLLMKey(varName, env) {
|
|
65
65
|
if (env[varName] && typeof env[varName] === 'string' && env[varName].length > 0) return env[varName];
|
|
@@ -467,15 +467,16 @@ export class EmbedDaemon {
|
|
|
467
467
|
process.stderr.write(`[embed] Distilled ${itemsWritten} snapshot item(s) into observation store\n`);
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
-
// Regenerate roadmap each tick
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
470
|
+
// Regenerate roadmap each tick unless disabled by the host config.
|
|
471
|
+
if (process.env.CONSTRUCT_EMBED_ROADMAP_ENABLED !== '0') {
|
|
472
|
+
try {
|
|
473
|
+
const roadmap = generateRoadmap({ targetPath: this.#rootDir, snapshot, roles: this.#config?.roles });
|
|
474
|
+
if (!roadmap.skipped) {
|
|
475
|
+
process.stderr.write(`[embed] Roadmap refreshed: ${roadmap.itemCount} open item(s) → ${roadmap.path}\n`);
|
|
476
|
+
}
|
|
477
|
+
} catch (err) {
|
|
478
|
+
process.stderr.write(`[embed] Roadmap refresh failed: ${err.message}\n`);
|
|
476
479
|
}
|
|
477
|
-
} catch (err) {
|
|
478
|
-
process.stderr.write(`[embed] Roadmap refresh failed: ${err.message}\n`);
|
|
479
480
|
}
|
|
480
481
|
},
|
|
481
482
|
{ runImmediately: true },
|
|
@@ -575,7 +576,8 @@ this.#scheduler.register(
|
|
|
575
576
|
const ok = result.results.filter(r => r.success).length;
|
|
576
577
|
const total = result.results.length;
|
|
577
578
|
process.stderr.write(`[embed] Telemetry setup: ${ok}/${total} resources configured\n`);
|
|
578
|
-
} else if (!result.ok) {
|
|
579
|
+
} else if (!result.ok && !result.error?.includes('required')) {
|
|
580
|
+
// Suppress the "keys required" message — normal for solo mode without telemetry configured.
|
|
579
581
|
process.stderr.write(`[embed] Telemetry setup skipped: ${result.error}\n`);
|
|
580
582
|
} else if (result.results?.some(r => !r.success)) {
|
|
581
583
|
const failures = result.results.filter(r => !r.success);
|
|
@@ -743,6 +745,7 @@ this.#scheduler.register(
|
|
|
743
745
|
'roadmap',
|
|
744
746
|
60 * 60_000,
|
|
745
747
|
async () => {
|
|
748
|
+
if (process.env.CONSTRUCT_EMBED_ROADMAP_ENABLED === '0') return;
|
|
746
749
|
if (!this.#lastSnapshot) return; // wait until at least one snapshot exists
|
|
747
750
|
try {
|
|
748
751
|
const result = generateRoadmap({ targetPath: this.#rootDir, snapshot: this.#lastSnapshot, roles: this.#config?.roles });
|
|
@@ -840,21 +843,23 @@ this.#scheduler.register(
|
|
|
840
843
|
meta: { gaps: result.gaps.length, conflicts: result.conflicts?.length || 0, recommendations: result.recommendations?.length || 0, autoFixed, queued },
|
|
841
844
|
});
|
|
842
845
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
846
|
+
try {
|
|
847
|
+
emitTraceEvent({
|
|
848
|
+
rootDir: this.#rootDir,
|
|
849
|
+
eventType: 'lifecycle.completed',
|
|
850
|
+
traceId: this.#daemonTraceId,
|
|
851
|
+
spanId: newSpanId(),
|
|
852
|
+
env: this.#env,
|
|
853
|
+
metadata: {
|
|
854
|
+
gaps: result.gaps.length,
|
|
855
|
+
conflicts: result.conflicts?.length || 0,
|
|
856
|
+
recommendations: result.recommendations?.length || 0,
|
|
857
|
+
autoFixed,
|
|
858
|
+
queued,
|
|
859
|
+
targets: result.targets,
|
|
860
|
+
},
|
|
861
|
+
});
|
|
862
|
+
} catch { /* observability must not break the caller */ }
|
|
858
863
|
} catch (err) {
|
|
859
864
|
process.stderr.write(`[embed] Docs lifecycle error: ${err.message}\n`);
|
|
860
865
|
}
|
|
@@ -1114,18 +1119,20 @@ this.#scheduler.register(
|
|
|
1114
1119
|
|
|
1115
1120
|
this.#status = 'running';
|
|
1116
1121
|
this.#daemonTraceId = newTraceId();
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1122
|
+
try {
|
|
1123
|
+
emitTraceEvent({
|
|
1124
|
+
rootDir: this.#rootDir,
|
|
1125
|
+
eventType: 'daemon.started',
|
|
1126
|
+
traceId: this.#daemonTraceId,
|
|
1127
|
+
spanId: newSpanId(),
|
|
1128
|
+
env: this.#env,
|
|
1129
|
+
metadata: {
|
|
1130
|
+
pid: process.pid,
|
|
1131
|
+
configSnapshotIntervalMs: this.#config.snapshot.intervalMs,
|
|
1132
|
+
version: process.env.CONSTRUCT_VERSION || '0.0.0',
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
} catch { /* observability must not break the caller */ }
|
|
1129
1136
|
process.stderr.write(`[embed] Daemon started. Snapshot interval: ${this.#config.snapshot.intervalMs}ms\n`);
|
|
1130
1137
|
}
|
|
1131
1138
|
|
|
@@ -1134,14 +1141,16 @@ this.#scheduler.register(
|
|
|
1134
1141
|
this.#inboxLiveWatcher = null;
|
|
1135
1142
|
this.#scheduler?.stop();
|
|
1136
1143
|
this.#status = 'stopped';
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1144
|
+
try {
|
|
1145
|
+
emitTraceEvent({
|
|
1146
|
+
rootDir: this.#rootDir,
|
|
1147
|
+
eventType: 'daemon.stopped',
|
|
1148
|
+
traceId: this.#daemonTraceId,
|
|
1149
|
+
spanId: newSpanId(),
|
|
1150
|
+
env: this.#env,
|
|
1151
|
+
metadata: { pid: process.pid, uptimeMs: process.uptime() * 1000 },
|
|
1152
|
+
});
|
|
1153
|
+
} catch { /* observability must not break the caller */ }
|
|
1145
1154
|
process.stderr.write('[embed] Daemon stopped.\n');
|
|
1146
1155
|
}
|
|
1147
1156
|
|
|
@@ -1182,10 +1191,10 @@ this.#scheduler.register(
|
|
|
1182
1191
|
try {
|
|
1183
1192
|
// Query strategy/PRDs/RFCs from knowledge base
|
|
1184
1193
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1194
|
+
const strategyDocs = searchObservations(this.#rootDir, 'PRD RFC strategy', {
|
|
1195
|
+
category: 'insight',
|
|
1196
|
+
limit: 50,
|
|
1197
|
+
});
|
|
1189
1198
|
|
|
1190
1199
|
// Query existing Jira tickets via Atlassian provider
|
|
1191
1200
|
|
package/lib/embed/inbox.mjs
CHANGED
|
@@ -36,6 +36,7 @@ const STATE_FILE = '.cx/runtime/inbox-state.json';
|
|
|
36
36
|
const DEFAULT_INBOX_SUBDIR = '.cx/inbox';
|
|
37
37
|
const DOCS_INTAKE_SUBDIR = 'docs/intake';
|
|
38
38
|
const MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB hard cap
|
|
39
|
+
const STALE_STATE_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
39
40
|
|
|
40
41
|
function nextAvailablePath(targetPath) {
|
|
41
42
|
if (!existsSync(targetPath)) return targetPath;
|
|
@@ -50,6 +51,28 @@ function nextAvailablePath(targetPath) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// ─── Stale state pruning ─────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Remove state entries for source files absent from disk for more than 7 days.
|
|
58
|
+
* Returns the count of removed entries.
|
|
59
|
+
*/
|
|
60
|
+
function pruneStaleState(rootDir, state) {
|
|
61
|
+
const cutoffMs = Date.now() - STALE_STATE_AGE_MS;
|
|
62
|
+
let removed = 0;
|
|
63
|
+
for (const key of Object.keys(state)) {
|
|
64
|
+
const entry = state[key];
|
|
65
|
+
if (!existsSync(key) && entry.processedAt) {
|
|
66
|
+
const processedMs = new Date(entry.processedAt).getTime();
|
|
67
|
+
if (processedMs < cutoffMs) {
|
|
68
|
+
delete state[key];
|
|
69
|
+
removed++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return removed;
|
|
74
|
+
}
|
|
75
|
+
|
|
53
76
|
// ─── State helpers ────────────────────────────────────────────────────────────
|
|
54
77
|
|
|
55
78
|
function statePath(rootDir) {
|
|
@@ -327,6 +350,13 @@ export class InboxWatcher {
|
|
|
327
350
|
}
|
|
328
351
|
}
|
|
329
352
|
|
|
353
|
+
// Prune stale state entries: remove entries for source files absent from disk
|
|
354
|
+
// for more than 7 days (covers moved/renamed files that linger in state).
|
|
355
|
+
const staleRemoved = pruneStaleState(this.#rootDir, state);
|
|
356
|
+
if (staleRemoved > 0) {
|
|
357
|
+
process.stderr.write(`[inbox] pruned ${staleRemoved} stale state entries\n`);
|
|
358
|
+
}
|
|
359
|
+
|
|
330
360
|
if (processed.length || errors.length) {
|
|
331
361
|
writeState(this.#rootDir, state);
|
|
332
362
|
}
|