@geraldmaron/construct 1.0.3 โ 1.0.5
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 +51 -42
- 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 +279 -4
- package/commands/understand/research.md +5 -3
- package/db/schema/004_recommendations.sql +46 -0
- package/db/schema/005_strategy.sql +21 -0
- package/lib/auto-docs.mjs +3 -4
- package/lib/beads-automation.mjs +16 -7
- package/lib/cli-commands.mjs +52 -2
- package/lib/comment-lint.mjs +7 -1
- package/lib/config/schema.mjs +3 -0
- 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/flavors/loader.mjs +136 -0
- package/lib/gates-audit.mjs +3 -3
- package/lib/health-check.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +22 -3
- package/lib/hooks/pre-compact.mjs +3 -0
- package/lib/hooks/pre-push-gate.mjs +14 -1
- package/lib/hooks/read-tracker.mjs +10 -101
- package/lib/hooks/session-optimize.mjs +3 -2
- package/lib/hooks/session-reflect.mjs +68 -0
- package/lib/host-capabilities.mjs +90 -1
- package/lib/init-unified.mjs +25 -2
- package/lib/init-update.mjs +246 -131
- package/lib/intake/classify.mjs +61 -183
- package/lib/intake/prepare.mjs +7 -0
- package/lib/intake/tables/creative.mjs +94 -0
- package/lib/intake/tables/operations.mjs +85 -0
- package/lib/intake/tables/research.mjs +85 -0
- package/lib/intake/tables/rnd.mjs +175 -0
- package/lib/intent-classifier.mjs +1 -0
- package/lib/knowledge/layout.mjs +10 -0
- package/lib/knowledge/research-store.mjs +109 -0
- package/lib/mcp/tools/telemetry.mjs +30 -78
- package/lib/model-router.mjs +61 -1
- package/lib/observation-store.mjs +19 -0
- package/lib/ollama-manager.mjs +1 -1
- package/lib/opencode-telemetry.mjs +4 -5
- package/lib/orchestration-policy.mjs +9 -0
- package/lib/outcomes/aggregate.mjs +104 -0
- package/lib/outcomes/record.mjs +115 -0
- package/lib/parity.mjs +121 -21
- package/lib/profiles/lifecycle.mjs +388 -0
- package/lib/profiles/loader.mjs +123 -0
- package/lib/profiles/validate-custom.mjs +114 -0
- package/lib/prompt-composer.js +106 -29
- package/lib/read-tracker-store.mjs +149 -0
- package/lib/reflect/extractor.mjs +193 -0
- package/lib/reflect.mjs +89 -2
- package/lib/sandbox.mjs +102 -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 +10 -2
- package/personas/construct.md +20 -20
- package/platforms/claude/settings.template.json +13 -0
- package/rules/common/research.md +44 -12
- package/scripts/sync-agents.mjs +11 -0
- 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/architect.ai-systems.md +4 -2
- package/skills/roles/architect.data.md +4 -2
- package/skills/roles/architect.enterprise.md +4 -2
- package/skills/roles/architect.integration.md +4 -2
- package/skills/roles/architect.md +7 -5
- package/skills/roles/architect.platform.md +4 -2
- package/skills/roles/data-analyst.experiment.md +4 -2
- package/skills/roles/data-analyst.md +9 -7
- package/skills/roles/data-analyst.product-intelligence.md +5 -3
- package/skills/roles/data-analyst.product.md +4 -2
- package/skills/roles/data-analyst.telemetry.md +4 -2
- package/skills/roles/data-engineer.pipeline.md +4 -2
- package/skills/roles/data-engineer.vector-retrieval.md +4 -2
- package/skills/roles/data-engineer.warehouse.md +4 -2
- package/skills/roles/debugger.md +7 -5
- package/skills/roles/designer.accessibility.md +4 -2
- package/skills/roles/designer.md +10 -8
- package/skills/roles/engineer.ai.md +4 -2
- package/skills/roles/engineer.data.md +5 -3
- package/skills/roles/engineer.md +14 -12
- package/skills/roles/engineer.platform.md +5 -3
- package/skills/roles/operator.docs.md +6 -4
- package/skills/roles/operator.md +6 -4
- package/skills/roles/operator.release.md +4 -2
- package/skills/roles/operator.sre.md +5 -3
- package/skills/roles/orchestrator.md +5 -3
- package/skills/roles/product-manager.ai-product.md +4 -2
- package/skills/roles/product-manager.business-strategy.md +4 -2
- package/skills/roles/product-manager.enterprise.md +4 -2
- package/skills/roles/product-manager.growth.md +4 -2
- package/skills/roles/product-manager.md +6 -4
- package/skills/roles/product-manager.platform.md +4 -2
- package/skills/roles/product-manager.product.md +4 -2
- package/skills/roles/qa.ai-eval.md +4 -2
- package/skills/roles/qa.api-contract.md +4 -2
- package/skills/roles/qa.data-pipeline.md +4 -2
- package/skills/roles/qa.md +7 -5
- package/skills/roles/qa.test-automation.md +5 -3
- package/skills/roles/qa.web-ui.md +4 -2
- package/skills/roles/researcher.explorer.md +4 -2
- package/skills/roles/researcher.md +35 -20
- package/skills/roles/researcher.ux.md +4 -2
- package/skills/roles/reviewer.devil-advocate.md +4 -2
- package/skills/roles/reviewer.evaluator.md +4 -2
- package/skills/roles/reviewer.md +14 -12
- package/skills/roles/reviewer.trace.md +4 -2
- package/skills/roles/security.ai.md +4 -2
- package/skills/roles/security.appsec.md +4 -2
- package/skills/roles/security.cloud.md +4 -2
- package/skills/roles/security.legal-compliance.md +4 -2
- package/skills/roles/security.md +7 -5
- package/skills/roles/security.privacy.md +4 -2
- package/skills/roles/security.supply-chain.md +4 -2
- package/skills/routing.md +8 -1
- package/templates/docs/persona-artifact.md +36 -0
- package/templates/docs/research-brief.md +63 -9
- package/templates/docs/research-finding.md +26 -0
- package/templates/docs/skill-artifact.md +27 -0
- package/templates/docs/strategy.md +36 -0
- package/templates/homebrew/construct.rb +6 -6
package/bin/construct
CHANGED
|
@@ -186,14 +186,49 @@ function runNodeScript(scriptPath, args = [], extraEnv = {}) {
|
|
|
186
186
|
async function cmdStatus() {
|
|
187
187
|
const jsonOutput = restArgsCache.includes('--json');
|
|
188
188
|
const status = await buildStatus({ rootDir: ROOT_DIR, cwd: process.cwd(), homeDir: HOME, env: process.env });
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
if (jsonOutput) {
|
|
191
191
|
println(JSON.stringify(status, null, 2));
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
println(formatStatusReport(status));
|
|
196
|
-
|
|
196
|
+
|
|
197
|
+
// Append the learning-loops summary so the operator gets a single answer
|
|
198
|
+
// to "is this thing actually learning?" without remembering a second
|
|
199
|
+
// command. Cheap: just reads .cx/observations + .cx/outcomes summaries.
|
|
200
|
+
try {
|
|
201
|
+
const { resolveActiveProfile } = await import('../lib/profiles/loader.mjs');
|
|
202
|
+
const { readSummary } = await import('../lib/outcomes/aggregate.mjs');
|
|
203
|
+
const fs = await import('node:fs');
|
|
204
|
+
const path = await import('node:path');
|
|
205
|
+
const cwd = process.cwd();
|
|
206
|
+
const active = resolveActiveProfile(cwd);
|
|
207
|
+
const obsIdxPath = path.join(cwd, '.cx', 'observations', 'index.json');
|
|
208
|
+
let obsTotal = 0;
|
|
209
|
+
let obs24h = 0;
|
|
210
|
+
if (fs.existsSync(obsIdxPath)) {
|
|
211
|
+
const idx = JSON.parse(fs.readFileSync(obsIdxPath, 'utf8'));
|
|
212
|
+
obsTotal = idx.length;
|
|
213
|
+
const since = Date.now() - 24 * 60 * 60 * 1000;
|
|
214
|
+
obs24h = idx.filter((e) => Date.parse(e.createdAt) >= since).length;
|
|
215
|
+
}
|
|
216
|
+
const researchDir = path.join(cwd, '.cx', 'knowledge', 'external', 'research');
|
|
217
|
+
const researchCount = fs.existsSync(researchDir)
|
|
218
|
+
? fs.readdirSync(researchDir).filter((f) => f.endsWith('.md')).length
|
|
219
|
+
: 0;
|
|
220
|
+
const summary = readSummary(cwd);
|
|
221
|
+
const outcomeRoles = summary?.roles ? Object.keys(summary.roles).length : 0;
|
|
222
|
+
|
|
223
|
+
println('');
|
|
224
|
+
println('Learning loops');
|
|
225
|
+
println('โโโโโโโโโโโโโโ');
|
|
226
|
+
println(` Profile ${active.id} (${active.displayName || ''})`);
|
|
227
|
+
println(` Observations ${obsTotal} total, ${obs24h} in last 24h`);
|
|
228
|
+
println(` Research files ${researchCount}`);
|
|
229
|
+
println(` Outcome roles ${outcomeRoles}`);
|
|
230
|
+
println(` Detail: npm run learning:status`);
|
|
231
|
+
} catch { /* best-effort surface; never block status */ }
|
|
197
232
|
}
|
|
198
233
|
|
|
199
234
|
async function cmdShow() {
|
|
@@ -421,6 +456,22 @@ async function cmdDoctor() {
|
|
|
421
456
|
add('Claude Code agents dir', fs.existsSync(path.join(HOME, '.claude', 'agents')), true);
|
|
422
457
|
add('Codex agents dir', fs.existsSync(path.join(HOME, '.codex', 'agents')), true);
|
|
423
458
|
add('Copilot prompts dir', fs.existsSync(path.join(HOME, '.github', 'prompts')), true);
|
|
459
|
+
add('Cursor MCP config', fs.existsSync(path.join(HOME, '.cursor', 'mcp.json')), true);
|
|
460
|
+
const vscodeSettingsPaths = process.platform === 'darwin'
|
|
461
|
+
? [
|
|
462
|
+
path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
|
463
|
+
path.join(HOME, 'Library', 'Application Support', 'Code - Insiders', 'User', 'settings.json'),
|
|
464
|
+
]
|
|
465
|
+
: process.platform === 'linux'
|
|
466
|
+
? [
|
|
467
|
+
path.join(HOME, '.config', 'Code', 'User', 'settings.json'),
|
|
468
|
+
path.join(HOME, '.config', 'Code - Insiders', 'User', 'settings.json'),
|
|
469
|
+
]
|
|
470
|
+
: [
|
|
471
|
+
path.join(process.env.APPDATA ?? path.join(HOME, 'AppData', 'Roaming'), 'Code', 'User', 'settings.json'),
|
|
472
|
+
path.join(process.env.APPDATA ?? path.join(HOME, 'AppData', 'Roaming'), 'Code - Insiders', 'User', 'settings.json'),
|
|
473
|
+
];
|
|
474
|
+
add('VS Code settings file', vscodeSettingsPaths.some((candidate) => fs.existsSync(candidate)), true);
|
|
424
475
|
add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(path.join(HOME, '.construct')), true);
|
|
425
476
|
add('skills/ directory', fs.existsSync(path.join(ROOT_DIR, 'skills')));
|
|
426
477
|
add('rules/common/', fs.existsSync(path.join(ROOT_DIR, 'rules', 'common')));
|
|
@@ -3217,6 +3268,27 @@ async function cmdHook(args) {
|
|
|
3217
3268
|
const [command, ...rest] = process.argv.slice(2);
|
|
3218
3269
|
const restArgsCache = rest;
|
|
3219
3270
|
|
|
3271
|
+
// Parse `--key=value` and repeatable `--source-url=<url>` for `construct knowledge add`.
|
|
3272
|
+
function parseKnowledgeAddArgs(args) {
|
|
3273
|
+
const opts = { slug: null, topic: null, confidence: 'inferred', sources: [] };
|
|
3274
|
+
for (const arg of args) {
|
|
3275
|
+
if (arg.startsWith('--slug=')) opts.slug = arg.slice('--slug='.length);
|
|
3276
|
+
else if (arg.startsWith('--topic=')) opts.topic = arg.slice('--topic='.length);
|
|
3277
|
+
else if (arg.startsWith('--confidence=')) opts.confidence = arg.slice('--confidence='.length);
|
|
3278
|
+
else if (arg.startsWith('--source-url=')) opts.sources.push({ url: arg.slice('--source-url='.length) });
|
|
3279
|
+
else if (arg.startsWith('--source=')) { /* tag, ignored for now */ }
|
|
3280
|
+
}
|
|
3281
|
+
return opts;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
// Read all of stdin as a UTF-8 string. Returns '' if nothing was piped.
|
|
3285
|
+
async function readStdin() {
|
|
3286
|
+
if (process.stdin.isTTY) return '';
|
|
3287
|
+
let raw = '';
|
|
3288
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
3289
|
+
return raw;
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3220
3292
|
const handlers = new Map([
|
|
3221
3293
|
// Core
|
|
3222
3294
|
['dev', cmdUp],
|
|
@@ -3252,6 +3324,24 @@ const handlers = new Map([
|
|
|
3252
3324
|
['handoffs', cmdHandoffs],
|
|
3253
3325
|
['headhunt', cmdHeadhunt],
|
|
3254
3326
|
['init', cmdInit],
|
|
3327
|
+
['drop', async (args) => {
|
|
3328
|
+
const { runDropCli } = await import('../lib/drop.mjs');
|
|
3329
|
+
return runDropCli(args, { cwd: process.cwd(), env: process.env });
|
|
3330
|
+
}],
|
|
3331
|
+
['docs', async (args) => {
|
|
3332
|
+
const sub = args[0];
|
|
3333
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
3334
|
+
println('Usage: construct docs <check|verify|update|site>');
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
const rest = args.slice(1);
|
|
3338
|
+
if (sub === 'verify') return cmdDocsVerify(rest);
|
|
3339
|
+
if (sub === 'update') return cmdDocsUpdate(rest);
|
|
3340
|
+
if (sub === 'check') return cmdDocsCheck(rest);
|
|
3341
|
+
if (sub === 'site') return cmdDocsSite();
|
|
3342
|
+
errorln(`Unknown docs subcommand: ${sub}. Available: check, verify, update, site`);
|
|
3343
|
+
process.exit(1);
|
|
3344
|
+
}],
|
|
3255
3345
|
['docs:verify', cmdDocsVerify],
|
|
3256
3346
|
['init:update', cmdInitUpdate],
|
|
3257
3347
|
['models', cmdModels],
|
|
@@ -3817,7 +3907,185 @@ const handlers = new Map([
|
|
|
3817
3907
|
}
|
|
3818
3908
|
return;
|
|
3819
3909
|
}
|
|
3820
|
-
|
|
3910
|
+
if (sub === 'add') {
|
|
3911
|
+
const opts = parseKnowledgeAddArgs(args.slice(1));
|
|
3912
|
+
if (!opts.slug || !opts.topic) {
|
|
3913
|
+
errorln('Usage: construct knowledge add --source=research --slug=<id> --topic="..." [--confidence=confirmed|inferred|weak] [--source-url=<url>] (body on stdin)');
|
|
3914
|
+
process.exit(1);
|
|
3915
|
+
}
|
|
3916
|
+
const body = await readStdin();
|
|
3917
|
+
if (!body || body.trim().length < 10) {
|
|
3918
|
+
errorln('construct knowledge add: stdin body is empty or too short');
|
|
3919
|
+
process.exit(1);
|
|
3920
|
+
}
|
|
3921
|
+
const { addResearchFinding } = await import('../lib/knowledge/research-store.mjs');
|
|
3922
|
+
try {
|
|
3923
|
+
const { path: outPath, bytes } = await addResearchFinding({
|
|
3924
|
+
cwd: process.cwd(),
|
|
3925
|
+
slug: opts.slug,
|
|
3926
|
+
topic: opts.topic,
|
|
3927
|
+
body,
|
|
3928
|
+
confidence: opts.confidence,
|
|
3929
|
+
sources: opts.sources,
|
|
3930
|
+
});
|
|
3931
|
+
process.stdout.write(`โ wrote ${outPath} (${bytes} bytes)\n`);
|
|
3932
|
+
return;
|
|
3933
|
+
} catch (err) {
|
|
3934
|
+
errorln(`construct knowledge add: ${err.message}`);
|
|
3935
|
+
process.exit(1);
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
errorln(`Unknown knowledge subcommand: ${sub}. Available: trends, index, add`);
|
|
3939
|
+
process.exit(1);
|
|
3940
|
+
}],
|
|
3941
|
+
['sandbox', async (args) => {
|
|
3942
|
+
const sub = args[0] || 'create';
|
|
3943
|
+
const { createSandbox, listSandboxes, deleteSandbox, pruneSandboxes } = await import('../lib/sandbox.mjs');
|
|
3944
|
+
if (sub === 'create') {
|
|
3945
|
+
const profileArg = args.find((a) => a.startsWith('--profile='));
|
|
3946
|
+
const profile = profileArg ? profileArg.split('=')[1] : null;
|
|
3947
|
+
const { id, path: p } = createSandbox({ profile });
|
|
3948
|
+
process.stdout.write(`โ created sandbox ${id}\n path: ${p}\n`);
|
|
3949
|
+
if (profile) process.stdout.write(` profile: ${profile}\n`);
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3952
|
+
if (sub === 'list') {
|
|
3953
|
+
const all = listSandboxes();
|
|
3954
|
+
if (all.length === 0) { process.stdout.write(' (no sandboxes)\n'); return; }
|
|
3955
|
+
for (const s of all) process.stdout.write(` ${s.id.padEnd(30)} ${s.path}\n`);
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
if (sub === 'delete') {
|
|
3959
|
+
const id = args[1];
|
|
3960
|
+
if (!id) { errorln('Usage: construct sandbox delete <id>'); process.exit(1); }
|
|
3961
|
+
process.stdout.write(deleteSandbox(id) ? `โ removed ${id}\n` : `not found: ${id}\n`);
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
if (sub === 'prune') {
|
|
3965
|
+
const daysArg = args.find((a) => a.startsWith('--days='));
|
|
3966
|
+
const days = daysArg ? Number(daysArg.split('=')[1]) || 7 : 7;
|
|
3967
|
+
const removed = pruneSandboxes({ olderThanDays: days });
|
|
3968
|
+
process.stdout.write(`โ pruned ${removed} sandbox${removed === 1 ? '' : 'es'} older than ${days} day${days === 1 ? '' : 's'}\n`);
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
errorln(`Unknown sandbox subcommand: ${sub}. Available: create, list, delete, prune`);
|
|
3972
|
+
process.exit(1);
|
|
3973
|
+
}],
|
|
3974
|
+
['profile', async (args) => {
|
|
3975
|
+
const sub = args[0] || 'show';
|
|
3976
|
+
const { listProfiles, loadProfile, resolveActiveProfile } = await import('../lib/profiles/loader.mjs');
|
|
3977
|
+
if (sub === 'list') {
|
|
3978
|
+
for (const id of listProfiles()) {
|
|
3979
|
+
const p = loadProfile(id);
|
|
3980
|
+
process.stdout.write(` ${id.padEnd(14)} ${p?.displayName || ''}\n`);
|
|
3981
|
+
}
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
if (sub === 'show') {
|
|
3985
|
+
const active = resolveActiveProfile(process.cwd());
|
|
3986
|
+
process.stdout.write(JSON.stringify({
|
|
3987
|
+
id: active.id,
|
|
3988
|
+
displayName: active.displayName,
|
|
3989
|
+
tagline: active.tagline,
|
|
3990
|
+
roles: active.roles?.length || 0,
|
|
3991
|
+
intakeTypes: active.intake?.types?.length || 0,
|
|
3992
|
+
}, null, 2) + '\n');
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3995
|
+
if (sub === 'set') {
|
|
3996
|
+
const id = args[1];
|
|
3997
|
+
if (!id) { errorln('Usage: construct profile set <id>'); process.exit(1); }
|
|
3998
|
+
const target = loadProfile(id);
|
|
3999
|
+
if (!target) {
|
|
4000
|
+
errorln(`Unknown profile: ${id}. Available: ${listProfiles().join(', ')}`);
|
|
4001
|
+
process.exit(1);
|
|
4002
|
+
}
|
|
4003
|
+
const { findProjectConfigPath, loadProjectConfig, writeProjectConfig, PROJECT_CONFIG_FILENAME } = await import('../lib/config/project-config.mjs');
|
|
4004
|
+
const fsMod = await import('node:fs');
|
|
4005
|
+
const pathMod = await import('node:path');
|
|
4006
|
+
const found = findProjectConfigPath(process.cwd());
|
|
4007
|
+
const cfgPath = found || pathMod.join(process.cwd(), PROJECT_CONFIG_FILENAME);
|
|
4008
|
+
const cfg = found ? (loadProjectConfig(process.cwd()) || { version: 1 }) : { version: 1 };
|
|
4009
|
+
cfg.profile = id;
|
|
4010
|
+
writeProjectConfig(cfgPath, cfg, { validate: true });
|
|
4011
|
+
process.stdout.write(`โ profile set to ${id} (${target.displayName})\n`);
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
if (sub === 'create') {
|
|
4015
|
+
const id = args[1];
|
|
4016
|
+
if (!id) {
|
|
4017
|
+
errorln('Usage: construct profile create <id> [--display="..."] [--role=<id>...] [--department=<id>:<displayName>...]');
|
|
4018
|
+
process.exit(1);
|
|
4019
|
+
}
|
|
4020
|
+
const displayArg = args.find((a) => a.startsWith('--display='));
|
|
4021
|
+
const displayName = displayArg ? displayArg.split('=')[1] : id;
|
|
4022
|
+
const seedRoles = args
|
|
4023
|
+
.filter((a) => a.startsWith('--role='))
|
|
4024
|
+
.map((a) => a.slice('--role='.length))
|
|
4025
|
+
.filter(Boolean);
|
|
4026
|
+
const seedDepartments = args
|
|
4027
|
+
.filter((a) => a.startsWith('--department='))
|
|
4028
|
+
.map((a) => a.slice('--department='.length))
|
|
4029
|
+
.map((spec) => {
|
|
4030
|
+
const [deptId, deptDisplay] = spec.split(':');
|
|
4031
|
+
return { id: deptId, displayName: deptDisplay || deptId };
|
|
4032
|
+
});
|
|
4033
|
+
const { createDraftProfile } = await import('../lib/profiles/lifecycle.mjs');
|
|
4034
|
+
try {
|
|
4035
|
+
const result = createDraftProfile({ cwd: process.cwd(), id, displayName, seedRoles, seedDepartments });
|
|
4036
|
+
const pathMod = await import('node:path');
|
|
4037
|
+
process.stdout.write(`โ draft created for ${id}\n`);
|
|
4038
|
+
process.stdout.write(` brief: ${pathMod.relative(process.cwd(), result.briefPath)}\n`);
|
|
4039
|
+
process.stdout.write(` draft: ${pathMod.relative(process.cwd(), result.draftPath)}\n`);
|
|
4040
|
+
if (result.personaPaths.length) process.stdout.write(` personas: ${result.personaPaths.length} scaffold(s) under personas/\n`);
|
|
4041
|
+
if (result.departmentPaths.length) process.stdout.write(` departments: ${result.departmentPaths.length} charter(s) under departments/\n`);
|
|
4042
|
+
process.stdout.write(`\nNext steps (in order, per docs/concepts/profile-lifecycle.md):\n`);
|
|
4043
|
+
process.stdout.write(` 1. Discover: dispatch cx-ux-researcher to fill personas/<role>.md from interviews + primary sources.\n`);
|
|
4044
|
+
process.stdout.write(` 2. Frame: dispatch cx-product-manager to fill departments/<dept>.md charters and the intake taxonomy in profile.json.\n`);
|
|
4045
|
+
process.stdout.write(` 3. Architect: dispatch cx-architect to reconcile role reuse vs new, and to populate departments[].\n`);
|
|
4046
|
+
process.stdout.write(` 4. Validate: dispatch cx-evaluator to run persona-eval and classifier-eval against representative signals.\n`);
|
|
4047
|
+
return;
|
|
4048
|
+
} catch (err) {
|
|
4049
|
+
errorln(err.message);
|
|
4050
|
+
process.exit(1);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
if (sub === 'drafts') {
|
|
4054
|
+
const { listDrafts } = await import('../lib/profiles/lifecycle.mjs');
|
|
4055
|
+
const drafts = listDrafts(process.cwd());
|
|
4056
|
+
if (drafts.length === 0) { process.stdout.write(' (no drafts)\n'); return; }
|
|
4057
|
+
for (const d of drafts) {
|
|
4058
|
+
const flags = [d.hasBrief ? 'brief' : null, d.hasProfile ? 'profile' : null].filter(Boolean).join(', ');
|
|
4059
|
+
process.stdout.write(` ${d.id.padEnd(20)} ${flags}\n`);
|
|
4060
|
+
}
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
4063
|
+
if (sub === 'archive') {
|
|
4064
|
+
const id = args[1];
|
|
4065
|
+
const reasonArg = args.find((a) => a.startsWith('--reason='));
|
|
4066
|
+
const reason = reasonArg ? reasonArg.split('=')[1] : '';
|
|
4067
|
+
if (!id) { errorln('Usage: construct profile archive <id> --reason="..."'); process.exit(1); }
|
|
4068
|
+
const { archiveProfile } = await import('../lib/profiles/lifecycle.mjs');
|
|
4069
|
+
try {
|
|
4070
|
+
const { archived } = archiveProfile({ id, reason });
|
|
4071
|
+
process.stdout.write(`โ archived ${id} to ${archived}\n`);
|
|
4072
|
+
return;
|
|
4073
|
+
} catch (err) {
|
|
4074
|
+
errorln(err.message);
|
|
4075
|
+
process.exit(1);
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
if (sub === 'health') {
|
|
4079
|
+
const id = args[1];
|
|
4080
|
+
if (!id) { errorln('Usage: construct profile health <id> [--days=N]'); process.exit(1); }
|
|
4081
|
+
const daysArg = args.find((a) => a.startsWith('--days='));
|
|
4082
|
+
const windowDays = daysArg ? Number(daysArg.split('=')[1]) || 30 : 30;
|
|
4083
|
+
const { profileHealth } = await import('../lib/profiles/lifecycle.mjs');
|
|
4084
|
+
const report = profileHealth(process.cwd(), id, { windowDays });
|
|
4085
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
errorln(`Unknown profile subcommand: ${sub}. Available: list, show, set, create, drafts, archive, health`);
|
|
3821
4089
|
process.exit(1);
|
|
3822
4090
|
}],
|
|
3823
4091
|
]);
|
|
@@ -3834,6 +4102,13 @@ if (command === '--help' || command === '-h' || command === 'help') {
|
|
|
3834
4102
|
process.exit(0);
|
|
3835
4103
|
}
|
|
3836
4104
|
|
|
4105
|
+
// Version flags โ print version and exit
|
|
4106
|
+
if (command === '--version' || command === '-V') {
|
|
4107
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8'));
|
|
4108
|
+
println(`construct v${pkg.version}`);
|
|
4109
|
+
process.exit(0);
|
|
4110
|
+
}
|
|
4111
|
+
|
|
3837
4112
|
const handler = handlers.get(command);
|
|
3838
4113
|
if (!handler) {
|
|
3839
4114
|
errorln(`Unknown command: ${command}`);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
commands/understand/research.md
|
|
2
|
+
commands/understand/research.md. Research a topic. Verify facts from primary sources, separate evidence from inference.
|
|
3
3
|
|
|
4
|
-
Research a topic
|
|
4
|
+
Research a topic. Verify facts from primary sources, separate evidence from inference.
|
|
5
5
|
-->
|
|
6
6
|
---
|
|
7
|
-
description: Research a topic
|
|
7
|
+
description: Research a topic. Verify facts from primary sources, separate evidence from inference.
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
You are Construct. Research: $ARGUMENTS
|
|
@@ -20,3 +20,5 @@ Method:
|
|
|
20
20
|
For each finding: source, date, confidence (confirmed / inferred / weak signal).
|
|
21
21
|
|
|
22
22
|
Output: FINDINGS (with citations) | INFERENCES (labeled) | GAPS | RECOMMENDATION
|
|
23
|
+
|
|
24
|
+
Persistence: after producing the output, run `construct knowledge add --source=research --slug=<topic-slug> --topic="<one-line topic>" --confidence=<confirmed|inferred|weak>` and pass the FINDINGS+INFERENCES+GAPS+RECOMMENDATION block on stdin. The store writes a frontmatter-stamped markdown file under `.cx/knowledge/external/research/` and indexes it for future hybrid search. Pass `--source-url=<url>` repeatedly for each cited source (required when confidence=confirmed).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- 004_recommendations.sql โ Recommendations table for team and enterprise mode.
|
|
2
|
+
-- Solo mode uses the JSONL store under .cx/intake/ instead.
|
|
3
|
+
--
|
|
4
|
+
-- Dedup key is enforced at the (project, dedup_key) level โ matches the in-memory
|
|
5
|
+
-- dedup contract in lib/embed/recommendation-store.mjs.
|
|
6
|
+
-- Active recommendations: dismissed_at IS NULL AND superseded_at IS NULL.
|
|
7
|
+
|
|
8
|
+
create table if not exists construct_recommendations (
|
|
9
|
+
id text primary key,
|
|
10
|
+
project text not null,
|
|
11
|
+
dedup_key text not null,
|
|
12
|
+
type text not null,
|
|
13
|
+
title text not null,
|
|
14
|
+
reason text,
|
|
15
|
+
lane text,
|
|
16
|
+
signal_count int not null default 1,
|
|
17
|
+
total_signal_count int not null default 1,
|
|
18
|
+
customer_impact int not null default 0,
|
|
19
|
+
recency_bonus int not null default 0,
|
|
20
|
+
strategic_bonus int not null default 0,
|
|
21
|
+
score float not null default 0,
|
|
22
|
+
priority text not null default 'P3',
|
|
23
|
+
source_signal_ids jsonb not null default '[]'::jsonb,
|
|
24
|
+
first_seen timestamptz not null default now(),
|
|
25
|
+
last_seen timestamptz not null default now(),
|
|
26
|
+
dismissed_at timestamptz,
|
|
27
|
+
dismiss_reason text,
|
|
28
|
+
superseded_at timestamptz,
|
|
29
|
+
superseded_by_id text,
|
|
30
|
+
suppressed_until timestamptz,
|
|
31
|
+
suppress_reason text,
|
|
32
|
+
updated_at timestamptz not null default now()
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
create unique index if not exists construct_recommendations_project_dedup_key_idx
|
|
36
|
+
on construct_recommendations (project, dedup_key);
|
|
37
|
+
|
|
38
|
+
create index if not exists construct_recommendations_priority_score_idx
|
|
39
|
+
on construct_recommendations (project, priority, score desc);
|
|
40
|
+
|
|
41
|
+
create index if not exists construct_recommendations_last_seen_idx
|
|
42
|
+
on construct_recommendations (project, last_seen desc);
|
|
43
|
+
|
|
44
|
+
create index if not exists construct_recommendations_active_idx
|
|
45
|
+
on construct_recommendations (project, dismissed_at)
|
|
46
|
+
where dismissed_at is null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- 005_strategy.sql โ Product strategy store for team and enterprise mode.
|
|
2
|
+
-- Solo mode uses ~/.cx/strategy.md instead.
|
|
3
|
+
--
|
|
4
|
+
-- Each row is an immutable version snapshot. The latest version for a project
|
|
5
|
+
-- is the row with the highest version number (or max updated_at when equal).
|
|
6
|
+
-- Callers insert a new row on each write; old versions are retained for history.
|
|
7
|
+
|
|
8
|
+
create table if not exists construct_strategy (
|
|
9
|
+
id bigserial primary key,
|
|
10
|
+
project text not null,
|
|
11
|
+
content text not null,
|
|
12
|
+
version int not null default 1,
|
|
13
|
+
updated_at timestamptz not null default now(),
|
|
14
|
+
updated_by text
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
create unique index if not exists construct_strategy_project_version_idx
|
|
18
|
+
on construct_strategy (project, version);
|
|
19
|
+
|
|
20
|
+
create index if not exists construct_strategy_project_updated_at_idx
|
|
21
|
+
on construct_strategy (project, updated_at desc);
|
package/lib/auto-docs.mjs
CHANGED
|
@@ -71,7 +71,7 @@ function buildCoreDocsContract() {
|
|
|
71
71
|
'| `docs/README.md` | Docs index and maintenance contract | Core docs set or maintenance expectations change |',
|
|
72
72
|
'| `docs/concepts/architecture.md` | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |',
|
|
73
73
|
'',
|
|
74
|
-
'`plan.md` is a local working document. `construct init` creates it for the active session, but it is gitignored and not committed
|
|
74
|
+
'`plan.md` is a local working document. `construct init` creates it for the active session, but it is gitignored and not committed; durable work belongs in the tracker (Beads or external).',
|
|
75
75
|
'',
|
|
76
76
|
'Tracker hierarchy: external tracker (prefer Beads) for durable work, `plan.md` for the local working plan, and cass-memory via MCP `memory` for cross-session recall.',
|
|
77
77
|
'',
|
|
@@ -82,13 +82,13 @@ function buildCoreDocsContract() {
|
|
|
82
82
|
|
|
83
83
|
const DIR_DESCRIPTIONS = {
|
|
84
84
|
agents: 'Registry and generated platform adapter chains',
|
|
85
|
-
apps: 'User-facing apps shipped from this repo (e.g
|
|
85
|
+
apps: 'User-facing apps shipped from this repo (e.g. apps/docs/, the Fumadocs docs site)',
|
|
86
86
|
bin: 'CLI entrypoint (`construct`)',
|
|
87
87
|
claude: 'Claude Code integration (agents, settings template)',
|
|
88
88
|
commands: 'Command prompt assets',
|
|
89
89
|
codex: 'GitHub Copilot / Codex integration',
|
|
90
90
|
docs: 'Architecture notes, runbooks, and documentation contract',
|
|
91
|
-
telemetry: '
|
|
91
|
+
telemetry: 'Local trace capture and optional telemetry export',
|
|
92
92
|
lib: 'Core runtime: CLI, hooks, MCP, status, sync, workflow',
|
|
93
93
|
opencode: 'OpenCode integration config',
|
|
94
94
|
personas: 'Persona prompt definitions',
|
|
@@ -470,4 +470,3 @@ export function buildFumadocsReference({ rootDir } = {}) {
|
|
|
470
470
|
|
|
471
471
|
return { written };
|
|
472
472
|
}
|
|
473
|
-
|
package/lib/beads-automation.mjs
CHANGED
|
@@ -22,6 +22,11 @@ const HANDOFFS_DIR = '.cx/handoffs';
|
|
|
22
22
|
const CONFIRM_TIMEOUT = 60000; // 60 second timeout for confirmation prompts
|
|
23
23
|
const POSITIVE_INTENT = /^(y|yes|yeah|yep|yup|sure|ok|okay|go|proceed|do it)\b/i;
|
|
24
24
|
|
|
25
|
+
function beadsConfirmEnabled(env = process.env) {
|
|
26
|
+
const raw = String(env.CONSTRUCT_BEADS_CONFIRM ?? '').trim().toLowerCase();
|
|
27
|
+
return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
// ---------------------------------------------------------------------------
|
|
26
31
|
// Confirmation Utility
|
|
27
32
|
// ---------------------------------------------------------------------------
|
|
@@ -148,14 +153,18 @@ export async function confirmAndCommit({
|
|
|
148
153
|
}
|
|
149
154
|
console.error('');
|
|
150
155
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
if (yes || beadsConfirmEnabled()) {
|
|
157
|
+
const confirmed = await confirm(
|
|
158
|
+
'Commit these changes?',
|
|
159
|
+
{ yes, default: 'n' }
|
|
160
|
+
);
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
if (!confirmed) {
|
|
163
|
+
console.error('[beads] Commit cancelled');
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
console.error('[beads] CONSTRUCT_BEADS_CONFIRM is off โ committing without interactive prompt.');
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
runGit(['commit', '-m', message], { cwd });
|
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',
|
|
@@ -122,6 +128,50 @@ export const CLI_COMMANDS = [
|
|
|
122
128
|
{ name: 'search <query>', desc: 'Search customer profiles by name/alias' },
|
|
123
129
|
],
|
|
124
130
|
},
|
|
131
|
+
{
|
|
132
|
+
name: 'knowledge',
|
|
133
|
+
emoji: '๐ง ',
|
|
134
|
+
category: 'Work',
|
|
135
|
+
core: false,
|
|
136
|
+
description: 'Query, index, or add to the project knowledge base',
|
|
137
|
+
usage: 'construct knowledge trends|index|add',
|
|
138
|
+
subcommands: [
|
|
139
|
+
{ name: 'trends', desc: 'Show trend report across observations and artifacts' },
|
|
140
|
+
{ name: 'index', desc: 'Rebuild the local RAG corpus over .cx/ artifacts' },
|
|
141
|
+
{ name: 'add --source=research --slug=<id> --topic="..." [--source-url=<url>]', desc: 'Persist a research finding into .cx/knowledge/external/research/' },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'sandbox',
|
|
146
|
+
emoji: '๐งช',
|
|
147
|
+
category: 'Core',
|
|
148
|
+
core: true,
|
|
149
|
+
description: 'Isolated tmpdir-based environment for QA / specialist dry-runs',
|
|
150
|
+
usage: 'construct sandbox create|list|delete|prune [--profile=<id>]',
|
|
151
|
+
subcommands: [
|
|
152
|
+
{ name: 'create [--profile=<id>]', desc: 'Create a new sandbox under ~/.cx/sandboxes/' },
|
|
153
|
+
{ name: 'list', desc: 'List existing sandboxes, newest first' },
|
|
154
|
+
{ name: 'delete <id>', desc: 'Remove one sandbox by id' },
|
|
155
|
+
{ name: 'prune [--days=N]', desc: 'Remove sandboxes older than N days (default 7)' },
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'profile',
|
|
160
|
+
emoji: '๐งญ',
|
|
161
|
+
category: 'Core',
|
|
162
|
+
core: true,
|
|
163
|
+
description: 'Manage the active org profile and its lifecycle (draft, promote, archive, health)',
|
|
164
|
+
usage: 'construct profile show|list|set|create|drafts|archive|health',
|
|
165
|
+
subcommands: [
|
|
166
|
+
{ name: 'show', desc: 'Show the active profile' },
|
|
167
|
+
{ name: 'list', desc: 'List curated profiles' },
|
|
168
|
+
{ name: 'set <id>', desc: 'Switch the active profile (writes construct.config.json)' },
|
|
169
|
+
{ name: 'create <id> [--display="..."]', desc: 'Scaffold a draft profile + requirements brief under .cx/profiles/draft-<id>/' },
|
|
170
|
+
{ name: 'drafts', desc: 'List in-progress draft profiles' },
|
|
171
|
+
{ name: 'archive <id> --reason="..."', desc: 'Move a curated profile into archive/profiles/<id>/' },
|
|
172
|
+
{ name: 'health <id> [--days=N]', desc: 'Per-profile observation + outcome rollup' },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
125
175
|
{
|
|
126
176
|
name: 'workspace',
|
|
127
177
|
emoji: '๐ข',
|
|
@@ -475,7 +525,7 @@ export const CLI_COMMANDS = [
|
|
|
475
525
|
category: 'Advanced',
|
|
476
526
|
core: false,
|
|
477
527
|
description: 'Show version',
|
|
478
|
-
usage: 'construct version',
|
|
528
|
+
usage: 'construct version | construct --version',
|
|
479
529
|
},
|
|
480
530
|
{
|
|
481
531
|
name: 'cleanup',
|
package/lib/comment-lint.mjs
CHANGED
|
@@ -42,7 +42,13 @@ function requiresHeader(rel) {
|
|
|
42
42
|
if (rel.startsWith('lib/server/static/')) {
|
|
43
43
|
return { required: false, type: null };
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
// Markdown files always get markdown-style headers, even if the directory
|
|
46
|
+
// glob (e.g. ^tests/) primarily targets JS sources. Without this, .md docs
|
|
47
|
+
// co-located with .mjs tests were forced into /** */ format.
|
|
48
|
+
let type;
|
|
49
|
+
if (ext === '.md') type = mdMatch || jsMatch ? 'md' : null;
|
|
50
|
+
else if (jsMatch && ext !== '.html') type = 'js';
|
|
51
|
+
else type = (jsMatch || mdMatch) ? 'md' : null;
|
|
46
52
|
return { required: jsMatch || mdMatch, type };
|
|
47
53
|
}
|
|
48
54
|
|
package/lib/config/schema.mjs
CHANGED
|
@@ -20,6 +20,7 @@ export const CONFIG_SCHEMA_VERSION = 1;
|
|
|
20
20
|
|
|
21
21
|
export const DEPLOYMENT_MODES = ['solo', 'team', 'enterprise'];
|
|
22
22
|
export const MCP_BROKER_VALUES = ['auto', 'on', 'off'];
|
|
23
|
+
export const DEFAULT_PROFILE_ID = 'rnd';
|
|
23
24
|
|
|
24
25
|
export const DEFAULT_PROJECT_CONFIG = Object.freeze({
|
|
25
26
|
version: CONFIG_SCHEMA_VERSION,
|
|
@@ -31,6 +32,7 @@ export const DEFAULT_PROJECT_CONFIG = Object.freeze({
|
|
|
31
32
|
tenantId: null,
|
|
32
33
|
}),
|
|
33
34
|
providers: Object.freeze({}),
|
|
35
|
+
profile: DEFAULT_PROFILE_ID,
|
|
34
36
|
autoEmbed: false,
|
|
35
37
|
telemetry: Object.freeze({
|
|
36
38
|
enabled: true,
|
|
@@ -77,6 +79,7 @@ export const FIELD_RULES = {
|
|
|
77
79
|
},
|
|
78
80
|
},
|
|
79
81
|
providers: { type: 'object', required: false },
|
|
82
|
+
profile: { type: 'string', required: false, maxLength: 40 },
|
|
80
83
|
autoEmbed: { type: 'boolean', required: false },
|
|
81
84
|
telemetry: {
|
|
82
85
|
type: 'object',
|