@geraldmaron/construct 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/agents/prompts/cx-ai-engineer.md +6 -26
- package/agents/prompts/cx-architect.md +1 -0
- package/agents/prompts/cx-business-strategist.md +2 -0
- package/agents/prompts/cx-data-analyst.md +6 -26
- package/agents/prompts/cx-docs-keeper.md +1 -31
- package/agents/prompts/cx-explorer.md +1 -0
- package/agents/prompts/cx-orchestrator.md +40 -112
- package/agents/prompts/cx-platform-engineer.md +2 -22
- package/agents/prompts/cx-product-manager.md +2 -1
- package/agents/prompts/cx-qa.md +0 -20
- package/agents/prompts/cx-rd-lead.md +2 -0
- package/agents/prompts/cx-researcher.md +77 -31
- package/agents/prompts/cx-security.md +11 -49
- package/agents/prompts/cx-sre.md +9 -43
- package/agents/prompts/cx-ux-researcher.md +1 -0
- package/agents/role-manifests.json +4 -4
- package/bin/construct +23 -0
- package/db/schema/004_recommendations.sql +46 -0
- package/db/schema/005_strategy.sql +21 -0
- package/lib/auto-docs.mjs +1 -2
- package/lib/beads-automation.mjs +16 -7
- package/lib/cli-commands.mjs +8 -2
- package/lib/embed/conflict-detection.mjs +26 -9
- package/lib/embed/customer-profiles.mjs +37 -17
- package/lib/embed/daemon.mjs +10 -8
- package/lib/embed/recommendation-store.mjs +213 -14
- package/lib/embed/workspaces.mjs +53 -18
- package/lib/gates-audit.mjs +3 -3
- package/lib/health-check.mjs +1 -1
- package/lib/hooks/pre-compact.mjs +3 -0
- package/lib/hooks/read-tracker.mjs +10 -101
- package/lib/host-capabilities.mjs +90 -1
- package/lib/init-update.mjs +246 -131
- package/lib/intent-classifier.mjs +1 -0
- package/lib/knowledge/layout.mjs +10 -0
- package/lib/mcp/tools/telemetry.mjs +30 -78
- package/lib/model-router.mjs +61 -1
- package/lib/ollama-manager.mjs +1 -1
- package/lib/opencode-telemetry.mjs +4 -5
- package/lib/orchestration-policy.mjs +9 -0
- package/lib/parity.mjs +124 -21
- package/lib/prompt-composer.js +106 -29
- package/lib/read-tracker-store.mjs +149 -0
- package/lib/server/index.mjs +76 -0
- package/lib/server/telemetry-login.mjs +17 -25
- package/lib/service-manager.mjs +30 -22
- package/lib/services/local-postgres.mjs +15 -0
- package/lib/services/telemetry-backend.mjs +1 -2
- package/lib/setup.mjs +8 -43
- package/lib/status.mjs +51 -5
- package/lib/storage/backend.mjs +12 -2
- package/lib/strategy-store.mjs +371 -0
- package/lib/telemetry/backends/local.mjs +6 -4
- package/lib/telemetry/client.mjs +185 -0
- package/lib/telemetry/ingest.mjs +13 -5
- package/lib/telemetry/team-rollup.mjs +9 -2
- package/lib/worker/trace.mjs +17 -27
- package/package.json +5 -2
- package/rules/common/research.md +44 -12
- package/skills/docs/backlog-proposal-workflow.md +2 -2
- package/skills/docs/customer-profile-workflow.md +1 -1
- package/skills/docs/evidence-ingest-workflow.md +5 -5
- package/skills/docs/prfaq-workflow.md +1 -1
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +3 -3
- package/skills/docs/product-signal-workflow.md +48 -18
- package/skills/docs/research-workflow.md +26 -14
- package/skills/docs/strategy-workflow.md +36 -0
- package/skills/roles/data-analyst.product-intelligence.md +1 -1
- package/skills/roles/researcher.md +28 -15
- package/skills/routing.md +8 -1
- package/templates/docs/research-brief.md +63 -9
- package/templates/docs/strategy.md +36 -0
- package/templates/homebrew/construct.rb +6 -6
|
@@ -36,55 +36,17 @@ Check in this order:
|
|
|
36
36
|
|
|
37
37
|
Provide: severity, location (file:line), description, trigger condition, and concrete fix. For CVE checks, delegate to cx-researcher. Hand all findings to cx-engineer — CRITICAL findings block shipping until fixed.
|
|
38
38
|
|
|
39
|
-
##
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- **
|
|
44
|
-
- **
|
|
45
|
-
- **
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- **Errors:** INCOMPLETE_FLOW, MISSING_VALIDATION
|
|
51
|
-
- **Rate:** 10/min
|
|
52
|
-
|
|
53
|
-
### check_injection
|
|
54
|
-
- **Input:** `{ sinks: string[], sources: string[], sanitizers?: string[] }`
|
|
55
|
-
- **Output:** `{ injectionPoints: InjectionPoint[], severity: Severity, fix: string }`
|
|
56
|
-
- **Errors:** UNREACHABLE_SINK, FALSE_POSITIVE
|
|
57
|
-
- **Rate:** 15/min
|
|
58
|
-
|
|
59
|
-
### assess_dependencies
|
|
60
|
-
- **Input:** `{ dependencies: Dependency[], ecosystem: string, severityThreshold: string }`
|
|
61
|
-
- **Output:** `{ cves: CVE[], upgrades: UpgradeRecommendation[], sbom: SBOM }`
|
|
62
|
-
- **Errors:** UNKNOWN_PACKAGE, RATE_LIMITED
|
|
63
|
-
- **Rate:** 5/min
|
|
64
|
-
|
|
65
|
-
## Parallel Execution
|
|
66
|
-
|
|
67
|
-
When auditing code or reviewing changes, these checks run in parallel:
|
|
68
|
-
|
|
69
|
-
- **Secrets scan** (always runs — fast, non-blocking)
|
|
70
|
-
- **Auth/authorization audit** (if auth logic, JWT, sessions touched)
|
|
71
|
-
- **Injection path analysis** (if user input reaches sinks)
|
|
72
|
-
- **Data exposure check** (if logging, errors, APIs return data)
|
|
73
|
-
- **Dependency CVE scan** (if package.json or lock files changed)
|
|
74
|
-
|
|
75
|
-
All checks are independent — run concurrently and aggregate findings.
|
|
76
|
-
|
|
77
|
-
### Execution Pattern
|
|
78
|
-
```javascript
|
|
79
|
-
// Parallel security checks
|
|
80
|
-
const [secrets, auth, injection, exposure, deps] = await Promise.all([
|
|
81
|
-
scan_secrets({ files }),
|
|
82
|
-
audit_auth({ authFlow }),
|
|
83
|
-
check_injection({ sinks, sources }),
|
|
84
|
-
assess_data_exposure({ logs, errors }),
|
|
85
|
-
assess_dependencies({ dependencies })
|
|
86
|
-
]);
|
|
87
|
-
```
|
|
39
|
+
## Parallel audit discipline
|
|
40
|
+
|
|
41
|
+
Run these checks concurrently — they are independent and can be grep-driven in parallel:
|
|
42
|
+
|
|
43
|
+
- **Secrets scan** (always — fast, covers all files in scope)
|
|
44
|
+
- **Auth/authorization audit** (if auth logic, JWT, sessions, or privilege paths touched)
|
|
45
|
+
- **Injection path analysis** (if user input reaches sinks — exec, eval, query, template)
|
|
46
|
+
- **Data exposure check** (if logging, error responses, or APIs return data)
|
|
47
|
+
- **Dependency CVE scan** (if package.json or lock files changed — delegate to cx-researcher for CVE lookups)
|
|
48
|
+
|
|
49
|
+
Aggregate findings by severity before reporting. Do not report each category separately.
|
|
88
50
|
|
|
89
51
|
## Learning Capture
|
|
90
52
|
|
package/agents/prompts/cx-sre.md
CHANGED
|
@@ -25,49 +25,15 @@ RUNBOOK for each alert:
|
|
|
25
25
|
|
|
26
26
|
Review code changes for: missing error handling on request paths, N+1 queries, unbounded operations, missing timeouts, operations that don't degrade gracefully.
|
|
27
27
|
|
|
28
|
-
##
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- **Input:** `{ alertName: string, triggerCondition: string, triageSteps: Step[] }`
|
|
38
|
-
- **Output:** `{ runbook: Runbook, escalationPath: string[], rollbackProcedure: Rollback }`
|
|
39
|
-
- **Errors:** MISSING_TRIGGER, INCOMPLETE_TRIAGE
|
|
40
|
-
- **Rate:** 10/min
|
|
41
|
-
|
|
42
|
-
### review_reliability
|
|
43
|
-
- **Input:** `{ codeChanges: Diff[], statefulOps: boolean, degradationPlan?: string }`
|
|
44
|
-
- **Output:** `{ findings: ReliabilityFinding[], missingAlerts: string[], rollbackGaps: string[] }`
|
|
45
|
-
- **Errors:** MISSING_DEGRADATION_PLAN, UNBOUNDED_OPERATION
|
|
46
|
-
- **Rate:** 15/min
|
|
47
|
-
|
|
48
|
-
## Parallel Execution
|
|
49
|
-
|
|
50
|
-
When reviewing changes for production readiness, these checks run in parallel:
|
|
51
|
-
|
|
52
|
-
- **SLO definition check** (if new service or changed behavior)
|
|
53
|
-
- **Alerting coverage** (if error paths or failure modes exist)
|
|
54
|
-
- **Rollback procedure** (if stateful or irreversible operation)
|
|
55
|
-
- **Error handling audit** (if request paths or external calls)
|
|
56
|
-
- **Resource limits** (if N+1 queries, unbounded operations, missing timeouts)
|
|
57
|
-
|
|
58
|
-
All checks are independent — run concurrently and aggregate findings.
|
|
59
|
-
|
|
60
|
-
### Execution Pattern
|
|
61
|
-
```javascript
|
|
62
|
-
// Parallel SRE checks
|
|
63
|
-
const [slo, alerts, rollback, errors, resources] = await Promise.all([
|
|
64
|
-
define_slo({ service, metric, target }),
|
|
65
|
-
check_alerting_coverage({ failureModes }),
|
|
66
|
-
verify_rollback({ statefulOps }),
|
|
67
|
-
audit_error_handling({ requestPaths }),
|
|
68
|
-
check_resource_limits({ queries, operations })
|
|
69
|
-
]);
|
|
70
|
-
```
|
|
28
|
+
## Production readiness checklist
|
|
29
|
+
|
|
30
|
+
For each change review, check these independently and aggregate before reporting:
|
|
31
|
+
|
|
32
|
+
- **SLO definition** — is there a measurable target with an error budget for this service or behavior?
|
|
33
|
+
- **Alerting coverage** — is every meaningful failure mode covered by an alert with a runbook?
|
|
34
|
+
- **Rollback procedure** — is there a tested, documented path back from this change?
|
|
35
|
+
- **Error handling** — do request paths and external calls fail gracefully and within timeouts?
|
|
36
|
+
- **Resource bounds** — are there N+1 queries, unbounded loops, or missing timeouts?
|
|
71
37
|
|
|
72
38
|
## Learning Capture
|
|
73
39
|
|
|
@@ -14,6 +14,7 @@ You have watched enough users fail to know that what they say they want and what
|
|
|
14
14
|
**Failure mode warning**: If your brief has no friction points, you haven't talked to users. Every product has places where users get stuck.
|
|
15
15
|
|
|
16
16
|
**Role guidance**: call `get_skill("roles/researcher.ux")` before drafting.
|
|
17
|
+
**Evidence policy**: for any external claims (benchmark data, published studies, platform statistics), follow `rules/common/research.md` — most-recent-first, primary sources, verified URLs. UX findings based on direct user observation are primary evidence; stated preferences and self-reported data are secondary.
|
|
17
18
|
|
|
18
19
|
Produce a UX brief:
|
|
19
20
|
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"events": ["handoff.received", "backlog.stale", "prd.requested"],
|
|
126
126
|
"severityImmediate": [],
|
|
127
127
|
"fence": {
|
|
128
|
-
"allowedPaths": ["docs/prd/**", "docs/meta-prd/**", "docs/prfaq/**", "docs/one-pager/**", ".cx/
|
|
128
|
+
"allowedPaths": ["docs/prd/**", "docs/meta-prd/**", "docs/prfaq/**", "docs/one-pager/**", ".cx/knowledge/**"],
|
|
129
129
|
"allowedBdLabels": ["product", "prd", "backlog", "feature"],
|
|
130
130
|
"allowedCommands": ["bd create", "bd note", "bd update", "bd label", "bd priority"],
|
|
131
131
|
"approvalRequired": ["commit", "push", "edit:lib/**", "edit:bin/**"]
|
|
@@ -192,7 +192,7 @@
|
|
|
192
192
|
"trace-reviewer": { "events": ["handoff.received", "trace.anomaly"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/traces/**", ".cx/traces/**"], "allowedBdLabels": ["trace", "observability"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:**"] }, "outputs": { "bdLabels": ["trace"], "docTypes": ["memo"] }, "handoffCandidates": ["sre", "ai-engineer", "engineer"], "killSwitchEnv": "CONSTRUCT_ROLE_TRACE_REVIEWER" },
|
|
193
193
|
"test-automation": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["tests/**", "docs/test-automation/**"], "allowedBdLabels": ["test-automation", "ci", "test-infra"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:lib/**", "edit:bin/**"] }, "outputs": { "bdLabels": ["test-automation"], "docTypes": ["memo"] }, "handoffCandidates": ["qa", "engineer", "platform-engineer"], "killSwitchEnv": "CONSTRUCT_ROLE_TEST_AUTOMATION" },
|
|
194
194
|
"ai-engineer": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/ai/**", "docs/agents/**", "agents/**", "skills/ai/**"], "allowedBdLabels": ["ai", "agent", "model"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:lib/**", "edit:bin/**"] }, "outputs": { "bdLabels": ["ai"], "docTypes": ["memo"] }, "handoffCandidates": ["engineer", "architect", "evaluator"], "killSwitchEnv": "CONSTRUCT_ROLE_AI_ENGINEER" },
|
|
195
|
-
"business-strategist": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/strategy/**", ".cx/strategy/**"], "allowedBdLabels": ["strategy", "business"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:**"] }, "outputs": { "bdLabels": ["strategy"], "docTypes": ["memo"] }, "handoffCandidates": ["product-manager", "researcher"], "killSwitchEnv": "CONSTRUCT_ROLE_BUSINESS_STRATEGIST" },
|
|
195
|
+
"business-strategist": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/strategy/**", ".cx/knowledge/decisions/strategy/**"], "allowedBdLabels": ["strategy", "business"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:**"] }, "outputs": { "bdLabels": ["strategy"], "docTypes": ["memo", "strategy"] }, "handoffCandidates": ["product-manager", "researcher"], "killSwitchEnv": "CONSTRUCT_ROLE_BUSINESS_STRATEGIST" },
|
|
196
196
|
"researcher": {
|
|
197
197
|
"events": ["handoff.received", "research.requested", "evidence.requested"],
|
|
198
198
|
"severityImmediate": [],
|
|
@@ -209,9 +209,9 @@
|
|
|
209
209
|
"ux-researcher": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/ux-research/**", ".cx/ux-research/**"], "allowedBdLabels": ["ux-research", "user-research"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:**"] }, "outputs": { "bdLabels": ["ux-research"], "docTypes": ["memo"] }, "handoffCandidates": ["product-manager", "designer", "researcher"], "killSwitchEnv": "CONSTRUCT_ROLE_UX_RESEARCHER" },
|
|
210
210
|
"explorer": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/explorations/**", ".cx/explorations/**"], "allowedBdLabels": ["exploration", "spike"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:**"] }, "outputs": { "bdLabels": ["exploration"], "docTypes": ["memo"] }, "handoffCandidates": ["engineer", "architect", "researcher"], "killSwitchEnv": "CONSTRUCT_ROLE_EXPLORER" },
|
|
211
211
|
"operations": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/ops/**", "docs/operations/**"], "allowedBdLabels": ["ops", "operations"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:**"] }, "outputs": { "bdLabels": ["ops"], "docTypes": ["memo", "runbook"] }, "handoffCandidates": ["sre", "platform-engineer", "release-manager"], "killSwitchEnv": "CONSTRUCT_ROLE_OPERATIONS" },
|
|
212
|
-
"orchestrator": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs
|
|
212
|
+
"orchestrator": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/**", "plan.md", ".cx/context.md"], "allowedBdLabels": ["orchestration", "workflow", "dispatch"], "allowedCommands": ["bd create", "bd note", "bd update", "bd label", "bd link"], "approvalRequired": ["commit", "push", "edit:lib/**", "edit:bin/**", "edit:agents/**"] }, "outputs": { "bdLabels": ["orchestration", "dispatch"], "docTypes": ["memo"] }, "handoffCandidates": ["product-manager", "architect", "engineer", "security", "qa", "sre"], "killSwitchEnv": "CONSTRUCT_ROLE_ORCHESTRATOR" },
|
|
213
213
|
"rd-lead": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/research/**", "docs/experiments/**", ".cx/experiments/**"], "allowedBdLabels": ["rd", "research", "experiment"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:**"] }, "outputs": { "bdLabels": ["rd"], "docTypes": ["memo", "research-brief"] }, "handoffCandidates": ["researcher", "architect", "ai-engineer"], "killSwitchEnv": "CONSTRUCT_ROLE_RD_LEAD" },
|
|
214
|
-
"legal-compliance": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/legal/**", "docs/compliance/**"], "allowedBdLabels": ["legal", "compliance", "privacy"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:**"] }, "outputs": { "bdLabels": ["legal", "compliance"], "docTypes": ["memo"] }, "handoffCandidates": ["security", "product-manager"], "killSwitchEnv": "CONSTRUCT_ROLE_LEGAL_COMPLIANCE" },
|
|
214
|
+
"legal-compliance": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/legal/**", "docs/compliance/**", "docs/security/**", ".cx/knowledge/**"], "allowedBdLabels": ["legal", "compliance", "privacy"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:**"] }, "outputs": { "bdLabels": ["legal", "compliance"], "docTypes": ["memo"] }, "handoffCandidates": ["security", "product-manager"], "killSwitchEnv": "CONSTRUCT_ROLE_LEGAL_COMPLIANCE" },
|
|
215
215
|
"devil-advocate": { "events": ["handoff.received"], "severityImmediate": [], "fence": { "allowedPaths": ["docs/critiques/**"], "allowedBdLabels": ["critique", "devil-advocate"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:**"] }, "outputs": { "bdLabels": ["critique"], "docTypes": ["memo"] }, "handoffCandidates": ["architect", "product-manager", "engineer"], "killSwitchEnv": "CONSTRUCT_ROLE_DEVIL_ADVOCATE" }
|
|
216
216
|
}
|
|
217
217
|
}
|
package/bin/construct
CHANGED
|
@@ -421,6 +421,22 @@ async function cmdDoctor() {
|
|
|
421
421
|
add('Claude Code agents dir', fs.existsSync(path.join(HOME, '.claude', 'agents')), true);
|
|
422
422
|
add('Codex agents dir', fs.existsSync(path.join(HOME, '.codex', 'agents')), true);
|
|
423
423
|
add('Copilot prompts dir', fs.existsSync(path.join(HOME, '.github', 'prompts')), true);
|
|
424
|
+
add('Cursor MCP config', fs.existsSync(path.join(HOME, '.cursor', 'mcp.json')), true);
|
|
425
|
+
const vscodeSettingsPaths = process.platform === 'darwin'
|
|
426
|
+
? [
|
|
427
|
+
path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
|
428
|
+
path.join(HOME, 'Library', 'Application Support', 'Code - Insiders', 'User', 'settings.json'),
|
|
429
|
+
]
|
|
430
|
+
: process.platform === 'linux'
|
|
431
|
+
? [
|
|
432
|
+
path.join(HOME, '.config', 'Code', 'User', 'settings.json'),
|
|
433
|
+
path.join(HOME, '.config', 'Code - Insiders', 'User', 'settings.json'),
|
|
434
|
+
]
|
|
435
|
+
: [
|
|
436
|
+
path.join(process.env.APPDATA ?? path.join(HOME, 'AppData', 'Roaming'), 'Code', 'User', 'settings.json'),
|
|
437
|
+
path.join(process.env.APPDATA ?? path.join(HOME, 'AppData', 'Roaming'), 'Code - Insiders', 'User', 'settings.json'),
|
|
438
|
+
];
|
|
439
|
+
add('VS Code settings file', vscodeSettingsPaths.some((candidate) => fs.existsSync(candidate)), true);
|
|
424
440
|
add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(path.join(HOME, '.construct')), true);
|
|
425
441
|
add('skills/ directory', fs.existsSync(path.join(ROOT_DIR, 'skills')));
|
|
426
442
|
add('rules/common/', fs.existsSync(path.join(ROOT_DIR, 'rules', 'common')));
|
|
@@ -3834,6 +3850,13 @@ if (command === '--help' || command === '-h' || command === 'help') {
|
|
|
3834
3850
|
process.exit(0);
|
|
3835
3851
|
}
|
|
3836
3852
|
|
|
3853
|
+
// Version flags → print version and exit
|
|
3854
|
+
if (command === '--version' || command === '-V') {
|
|
3855
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8'));
|
|
3856
|
+
println(`construct v${pkg.version}`);
|
|
3857
|
+
process.exit(0);
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3837
3860
|
const handler = handlers.get(command);
|
|
3838
3861
|
if (!handler) {
|
|
3839
3862
|
errorln(`Unknown command: ${command}`);
|
|
@@ -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
|
@@ -88,7 +88,7 @@ const DIR_DESCRIPTIONS = {
|
|
|
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',
|
|
@@ -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',
|
|
@@ -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
|
@@ -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 },
|
|
@@ -744,6 +745,7 @@ this.#scheduler.register(
|
|
|
744
745
|
'roadmap',
|
|
745
746
|
60 * 60_000,
|
|
746
747
|
async () => {
|
|
748
|
+
if (process.env.CONSTRUCT_EMBED_ROADMAP_ENABLED === '0') return;
|
|
747
749
|
if (!this.#lastSnapshot) return; // wait until at least one snapshot exists
|
|
748
750
|
try {
|
|
749
751
|
const result = generateRoadmap({ targetPath: this.#rootDir, snapshot: this.#lastSnapshot, roles: this.#config?.roles });
|