@geraldmaron/construct 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +4 -4
  2. package/agents/prompts/cx-ai-engineer.md +6 -26
  3. package/agents/prompts/cx-architect.md +1 -0
  4. package/agents/prompts/cx-business-strategist.md +2 -0
  5. package/agents/prompts/cx-data-analyst.md +6 -26
  6. package/agents/prompts/cx-docs-keeper.md +1 -31
  7. package/agents/prompts/cx-explorer.md +1 -0
  8. package/agents/prompts/cx-orchestrator.md +40 -112
  9. package/agents/prompts/cx-platform-engineer.md +2 -22
  10. package/agents/prompts/cx-product-manager.md +2 -1
  11. package/agents/prompts/cx-qa.md +0 -20
  12. package/agents/prompts/cx-rd-lead.md +2 -0
  13. package/agents/prompts/cx-researcher.md +77 -31
  14. package/agents/prompts/cx-security.md +11 -49
  15. package/agents/prompts/cx-sre.md +9 -43
  16. package/agents/prompts/cx-ux-researcher.md +1 -0
  17. package/agents/role-manifests.json +4 -4
  18. package/bin/construct +23 -0
  19. package/db/schema/004_recommendations.sql +46 -0
  20. package/db/schema/005_strategy.sql +21 -0
  21. package/lib/auto-docs.mjs +1 -2
  22. package/lib/beads-automation.mjs +16 -7
  23. package/lib/cli-commands.mjs +8 -2
  24. package/lib/embed/conflict-detection.mjs +26 -9
  25. package/lib/embed/customer-profiles.mjs +37 -17
  26. package/lib/embed/daemon.mjs +10 -8
  27. package/lib/embed/recommendation-store.mjs +213 -14
  28. package/lib/embed/workspaces.mjs +53 -18
  29. package/lib/gates-audit.mjs +3 -3
  30. package/lib/health-check.mjs +1 -1
  31. package/lib/hooks/pre-compact.mjs +3 -0
  32. package/lib/hooks/read-tracker.mjs +10 -101
  33. package/lib/host-capabilities.mjs +90 -1
  34. package/lib/init-update.mjs +246 -131
  35. package/lib/intent-classifier.mjs +1 -0
  36. package/lib/knowledge/layout.mjs +10 -0
  37. package/lib/mcp/tools/telemetry.mjs +30 -78
  38. package/lib/model-router.mjs +61 -1
  39. package/lib/ollama-manager.mjs +1 -1
  40. package/lib/opencode-telemetry.mjs +4 -5
  41. package/lib/orchestration-policy.mjs +9 -0
  42. package/lib/parity.mjs +124 -21
  43. package/lib/prompt-composer.js +106 -29
  44. package/lib/read-tracker-store.mjs +149 -0
  45. package/lib/server/index.mjs +76 -0
  46. package/lib/server/telemetry-login.mjs +17 -25
  47. package/lib/service-manager.mjs +30 -22
  48. package/lib/services/local-postgres.mjs +15 -0
  49. package/lib/services/telemetry-backend.mjs +1 -2
  50. package/lib/setup.mjs +8 -43
  51. package/lib/status.mjs +51 -5
  52. package/lib/storage/backend.mjs +12 -2
  53. package/lib/strategy-store.mjs +371 -0
  54. package/lib/telemetry/backends/local.mjs +6 -4
  55. package/lib/telemetry/client.mjs +185 -0
  56. package/lib/telemetry/ingest.mjs +13 -5
  57. package/lib/telemetry/team-rollup.mjs +9 -2
  58. package/lib/worker/trace.mjs +17 -27
  59. package/package.json +5 -2
  60. package/rules/common/research.md +44 -12
  61. package/skills/docs/backlog-proposal-workflow.md +2 -2
  62. package/skills/docs/customer-profile-workflow.md +1 -1
  63. package/skills/docs/evidence-ingest-workflow.md +5 -5
  64. package/skills/docs/prfaq-workflow.md +1 -1
  65. package/skills/docs/product-intelligence-review.md +1 -1
  66. package/skills/docs/product-intelligence-workflow.md +3 -3
  67. package/skills/docs/product-signal-workflow.md +48 -18
  68. package/skills/docs/research-workflow.md +26 -14
  69. package/skills/docs/strategy-workflow.md +36 -0
  70. package/skills/roles/data-analyst.product-intelligence.md +1 -1
  71. package/skills/roles/researcher.md +28 -15
  72. package/skills/routing.md +8 -1
  73. package/templates/docs/research-brief.md +63 -9
  74. package/templates/docs/strategy.md +36 -0
  75. package/templates/homebrew/construct.rb +6 -6
@@ -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
- ## Tool Contracts
40
-
41
- ### scan_secrets
42
- - **Input:** `{ files: string[], content?: string[], diff?: string }`
43
- - **Output:** `{ findings: SecretFinding[], falsePositives: string[], scanDuration: number }`
44
- - **Errors:** FILE_NOT_FOUND, SCAN_TIMEOUT, RATE_LIMITED
45
- - **Rate:** 20/min
46
-
47
- ### audit_auth
48
- - **Input:** `{ authFlow: AuthFlow, trustBoundaries: string[], jwtValidation: boolean }`
49
- - **Output:** `{ vulnerabilities: AuthVulnerability[], bypassPaths: string[], severity: Severity }`
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
 
@@ -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
- ## Tool Contracts
29
-
30
- ### define_slo
31
- - **Input:** `{ service: string, metric: string, measurementMethod: string, target: number }`
32
- - **Output:** `{ slo: SLO, errorBudget: number, alertThreshold: number, dashboard: DashboardConfig }`
33
- - **Errors:** INVALID_METRIC, UNMEASURABLE_TARGET
34
- - **Rate:** 10/min
35
-
36
- ### create_runbook
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/product-intel/**"],
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/orchestration/**", "docs/workflows/**"], "allowedBdLabels": ["orchestration", "workflow"], "allowedCommands": ["bd create", "bd note", "bd update"], "approvalRequired": ["commit", "push", "edit:**"] }, "outputs": { "bdLabels": ["orchestration"], "docTypes": ["memo"] }, "handoffCandidates": ["product-manager", "architect", "engineer"], "killSwitchEnv": "CONSTRUCT_ROLE_ORCHESTRATOR" },
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: 'Telemetry backend for agent observability',
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
-
@@ -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
- const confirmed = await confirm(
152
- 'Commit these changes?',
153
- { yes, default: 'n' }
154
- );
156
+ if (yes || beadsConfirmEnabled()) {
157
+ const confirmed = await confirm(
158
+ 'Commit these changes?',
159
+ { yes, default: 'n' }
160
+ );
155
161
 
156
- if (!confirmed) {
157
- console.error('[beads] Commit cancelled');
158
- return null;
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 });
@@ -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/product-intel/artifact-index.json — cached artifact embeddings
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
- const INDEX_DIR = join(homedir(), '.cx', 'product-intel');
25
- const INDEX_FILE = join(INDEX_DIR, 'artifact-index.json');
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
- if (!existsSync(INDEX_DIR)) mkdirSync(INDEX_DIR, { recursive: true });
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
- if (!forceRebuild && existsSync(INDEX_FILE)) {
74
+ const { indexFile } = indexPaths();
75
+ if (!forceRebuild && existsSync(indexFile)) {
59
76
  try {
60
- const cached = JSON.parse(readFileSync(INDEX_FILE, 'utf8'));
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(INDEX_FILE, JSON.stringify({
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/product-intel/customer-profiles/.
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/product-intel/customer-profiles/<customer-id>.md
10
- * ~/.cx/product-intel/customer-profiles/index.json — quick lookup by name/email
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 { homedir } from 'node:os';
18
+ import { cxDir } from '../paths.mjs';
19
+ import { knowledgeInternalStore } from '../knowledge/layout.mjs';
19
20
 
20
- const PROFILES_DIR = join(homedir(), '.cx', 'product-intel', 'customer-profiles');
21
- const INDEX_FILE = join(PROFILES_DIR, 'index.json');
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
- if (!existsSync(PROFILES_DIR)) {
28
- mkdirSync(PROFILES_DIR, { recursive: true });
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
- if (!existsSync(INDEX_FILE)) return {};
53
+ const { indexFile } = profilePaths();
54
+ if (!existsSync(indexFile)) return {};
38
55
  try {
39
- return JSON.parse(readFileSync(INDEX_FILE, 'utf8'));
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(INDEX_FILE, JSON.stringify(index, null, 2) + '\n');
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(PROFILES_DIR, filename);
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(PROFILES_DIR).filter(f => f.endsWith('.md'));
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(PROFILES_DIR, file);
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(PROFILES_DIR, `${slugify(entry.name)}-${entry.id}.md`),
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(PROFILES_DIR, `${slugify(profile.name)}-${customerId}.md`);
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
 
@@ -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 so .cx/roadmap.md stays current and the
471
- // missing-focal-resource gap clears on the next snapshot.
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`);
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 });