@geraldmaron/construct 1.0.5 → 1.0.7

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 (268) hide show
  1. package/README.md +24 -18
  2. package/agents/contracts.json +617 -2
  3. package/agents/contracts.schema.json +83 -0
  4. package/agents/prompts/cx-accessibility.md +5 -3
  5. package/agents/prompts/cx-ai-engineer.md +9 -7
  6. package/agents/prompts/cx-architect.md +8 -6
  7. package/agents/prompts/cx-business-strategist.md +8 -6
  8. package/agents/prompts/cx-data-analyst.md +9 -7
  9. package/agents/prompts/cx-data-engineer.md +5 -3
  10. package/agents/prompts/cx-debugger.md +8 -6
  11. package/agents/prompts/cx-designer.md +7 -5
  12. package/agents/prompts/cx-devil-advocate.md +6 -4
  13. package/agents/prompts/cx-docs-keeper.md +7 -5
  14. package/agents/prompts/cx-engineer.md +7 -5
  15. package/agents/prompts/cx-evaluator.md +4 -2
  16. package/agents/prompts/cx-explorer.md +10 -8
  17. package/agents/prompts/cx-legal-compliance.md +4 -2
  18. package/agents/prompts/cx-operations.md +7 -5
  19. package/agents/prompts/cx-orchestrator.md +12 -10
  20. package/agents/prompts/cx-platform-engineer.md +7 -5
  21. package/agents/prompts/cx-product-manager.md +6 -4
  22. package/agents/prompts/cx-qa.md +10 -8
  23. package/agents/prompts/cx-rd-lead.md +11 -9
  24. package/agents/prompts/cx-release-manager.md +7 -5
  25. package/agents/prompts/cx-researcher.md +24 -22
  26. package/agents/prompts/cx-reviewer.md +9 -7
  27. package/agents/prompts/cx-security.md +12 -10
  28. package/agents/prompts/cx-sre.md +9 -7
  29. package/agents/prompts/cx-test-automation.md +5 -3
  30. package/agents/prompts/cx-trace-reviewer.md +10 -8
  31. package/agents/prompts/cx-ux-researcher.md +5 -3
  32. package/agents/registry.json +8 -3
  33. package/bin/construct +444 -53
  34. package/commands/build/feature.md +4 -4
  35. package/commands/build/fix.md +8 -8
  36. package/commands/design/access.md +3 -3
  37. package/commands/design/flow.md +3 -3
  38. package/commands/design/ui.md +4 -4
  39. package/commands/measure/experiment.md +5 -5
  40. package/commands/measure/metrics.md +3 -3
  41. package/commands/measure/results.md +4 -4
  42. package/commands/plan/api.md +3 -3
  43. package/commands/plan/challenge.md +3 -3
  44. package/commands/plan/decide.md +3 -3
  45. package/commands/plan/feature.md +7 -7
  46. package/commands/plan/requirements.md +3 -3
  47. package/commands/remember/context.md +5 -5
  48. package/commands/remember/handoff.md +3 -3
  49. package/commands/remember/runbook.md +3 -3
  50. package/commands/review/code.md +8 -8
  51. package/commands/review/quality.md +4 -4
  52. package/commands/review/security.md +3 -3
  53. package/commands/ship/ready.md +3 -3
  54. package/commands/ship/release.md +3 -3
  55. package/commands/ship/status.md +4 -4
  56. package/commands/understand/docs.md +3 -3
  57. package/commands/understand/this.md +4 -4
  58. package/commands/understand/why.md +8 -8
  59. package/commands/work/clean.md +14 -14
  60. package/commands/work/drive.md +10 -10
  61. package/commands/work/optimize-prompts.md +9 -9
  62. package/commands/work/parallel-review.md +8 -8
  63. package/db/schema/006_graph.sql +24 -0
  64. package/examples/provider-plugin/README.md +7 -7
  65. package/examples/seed-observations/README.md +6 -6
  66. package/examples/seed-observations/anti-patterns.md +14 -14
  67. package/examples/seed-observations/decisions.md +4 -4
  68. package/examples/seed-observations/patterns.md +14 -14
  69. package/lib/auto-docs.mjs +13 -8
  70. package/lib/boundary.mjs +126 -0
  71. package/lib/cache-strategy-google.js +26 -31
  72. package/lib/cli-commands.mjs +2 -2
  73. package/lib/comment-lint.mjs +134 -0
  74. package/lib/contracts/validate.mjs +323 -0
  75. package/lib/daemons/contract.mjs +210 -0
  76. package/lib/docs-verify.mjs +59 -6
  77. package/lib/doctor/cli.mjs +16 -1
  78. package/lib/doctor/index.mjs +3 -1
  79. package/lib/doctor/watchers/consistency.mjs +310 -0
  80. package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
  81. package/lib/document-extract.mjs +211 -1
  82. package/lib/embed/cli.mjs +124 -3
  83. package/lib/embed/daemon.mjs +43 -4
  84. package/lib/embed/docs-lifecycle.mjs +1 -1
  85. package/lib/embed/inbox.mjs +2 -0
  86. package/lib/embed/scheduler.mjs +33 -5
  87. package/lib/evaluator-optimizer.mjs +2 -3
  88. package/lib/flavors/loader.mjs +1 -1
  89. package/lib/hooks/comment-lint.mjs +16 -0
  90. package/lib/hooks/mcp-audit.mjs +2 -1
  91. package/lib/hooks/proactive-activation.mjs +0 -14
  92. package/lib/hooks/rule-verifier.mjs +217 -0
  93. package/lib/hooks/session-optimize.mjs +2 -1
  94. package/lib/init-unified.mjs +55 -65
  95. package/lib/intake/classify.mjs +108 -24
  96. package/lib/intake/daemon.mjs +121 -0
  97. package/lib/intake/filesystem-queue.mjs +6 -1
  98. package/lib/intake/intake-config.mjs +2 -1
  99. package/lib/intake/prepare.mjs +0 -1
  100. package/lib/intake/session-prelude.mjs +7 -1
  101. package/lib/intake/traceability.mjs +90 -0
  102. package/lib/knowledge/graph.mjs +213 -0
  103. package/lib/knowledge/research-store.mjs +2 -0
  104. package/lib/maintenance/cleanup.mjs +315 -0
  105. package/lib/mcp/memory-bridge.mjs +276 -0
  106. package/lib/mcp/server.mjs +189 -1
  107. package/lib/mcp/tools/profile.mjs +270 -0
  108. package/lib/mcp/tools/workflow.mjs +25 -0
  109. package/lib/mcp-catalog.json +12 -8
  110. package/lib/mcp-platform-config.mjs +16 -8
  111. package/lib/migrations/index.mjs +106 -0
  112. package/lib/migrations/v1-baseline.mjs +33 -0
  113. package/lib/observation-store.mjs +9 -4
  114. package/lib/outcomes/record.mjs +2 -0
  115. package/lib/profiles/rebrand.mjs +46 -0
  116. package/lib/project-init-shared.mjs +12 -0
  117. package/lib/provider-capabilities.js +20 -7
  118. package/lib/providers/auth-manager.mjs +58 -17
  119. package/lib/reflect.mjs +49 -12
  120. package/lib/server/index.mjs +22 -28
  121. package/lib/session-store.mjs +6 -4
  122. package/lib/setup.mjs +14 -3
  123. package/lib/telemetry/client.mjs +5 -1
  124. package/lib/version.mjs +51 -0
  125. package/lib/worker/trace.mjs +5 -1
  126. package/package.json +4 -1
  127. package/personas/construct.md +3 -1
  128. package/platforms/claude/CLAUDE.md +6 -6
  129. package/rules/common/agents.md +2 -2
  130. package/rules/common/beads-hygiene.md +11 -11
  131. package/rules/common/code-review.md +1 -1
  132. package/rules/common/coding-style.md +1 -1
  133. package/rules/common/comments.md +8 -8
  134. package/rules/common/commit-approval.md +4 -4
  135. package/rules/common/cx-agent-routing.md +2 -2
  136. package/rules/common/cx-skill-routing.md +2 -2
  137. package/rules/common/development-workflow.md +3 -2
  138. package/rules/common/doc-ownership.md +2 -2
  139. package/rules/common/efficiency.md +3 -3
  140. package/rules/common/framing.md +1 -1
  141. package/rules/common/git-workflow.md +1 -1
  142. package/rules/common/no-fabrication.md +69 -0
  143. package/rules/common/patterns.md +1 -1
  144. package/rules/common/performance.md +1 -1
  145. package/rules/common/release-gates.md +7 -7
  146. package/rules/common/research.md +4 -4
  147. package/rules/common/review-before-change.md +58 -0
  148. package/rules/common/security.md +1 -1
  149. package/rules/common/skill-composition.md +8 -8
  150. package/rules/common/testing.md +1 -1
  151. package/rules/golang/coding-style.md +2 -2
  152. package/rules/golang/hooks.md +1 -1
  153. package/rules/golang/patterns.md +1 -1
  154. package/rules/golang/security.md +1 -1
  155. package/rules/golang/testing.md +1 -1
  156. package/rules/python/coding-style.md +1 -1
  157. package/rules/python/hooks.md +1 -1
  158. package/rules/python/patterns.md +1 -1
  159. package/rules/python/security.md +1 -1
  160. package/rules/python/testing.md +1 -1
  161. package/rules/swift/coding-style.md +3 -3
  162. package/rules/swift/hooks.md +2 -2
  163. package/rules/swift/patterns.md +2 -2
  164. package/rules/swift/security.md +4 -4
  165. package/rules/swift/testing.md +2 -2
  166. package/rules/typescript/coding-style.md +1 -1
  167. package/rules/typescript/hooks.md +1 -1
  168. package/rules/typescript/patterns.md +1 -1
  169. package/rules/typescript/security.md +1 -1
  170. package/rules/typescript/testing.md +1 -1
  171. package/rules/web/coding-style.md +1 -1
  172. package/rules/web/design-quality.md +1 -1
  173. package/rules/web/hooks.md +1 -1
  174. package/rules/web/patterns.md +1 -1
  175. package/rules/web/performance.md +1 -1
  176. package/rules/web/security.md +1 -1
  177. package/rules/web/testing.md +1 -1
  178. package/scripts/sync-agents.mjs +45 -14
  179. package/skills/ai/agent-dev.md +1 -1
  180. package/skills/ai/llm-security.md +1 -1
  181. package/skills/ai/ml-ops.md +6 -6
  182. package/skills/ai/orchestration-workflow.md +1 -1
  183. package/skills/ai/prompt-and-eval.md +1 -1
  184. package/skills/ai/prompt-optimizer.md +13 -13
  185. package/skills/ai/rag-system.md +1 -1
  186. package/skills/architecture/api-design.md +1 -1
  187. package/skills/architecture/caching.md +1 -1
  188. package/skills/architecture/cloud-native.md +1 -1
  189. package/skills/architecture/message-queue.md +1 -1
  190. package/skills/architecture/security-arch.md +1 -1
  191. package/skills/compliance/ai-disclosure.md +1 -1
  192. package/skills/compliance/data-privacy.md +1 -1
  193. package/skills/compliance/license-audit.md +2 -2
  194. package/skills/compliance/regulatory-review.md +1 -1
  195. package/skills/development/cpp.md +1 -1
  196. package/skills/development/go.md +1 -1
  197. package/skills/development/java.md +1 -1
  198. package/skills/development/kotlin.md +9 -9
  199. package/skills/development/mobile-crossplatform.md +13 -13
  200. package/skills/development/python.md +1 -1
  201. package/skills/development/rust.md +1 -1
  202. package/skills/development/shell.md +1 -1
  203. package/skills/development/swift.md +6 -6
  204. package/skills/development/typescript.md +1 -1
  205. package/skills/devops/ci-cd.md +5 -5
  206. package/skills/devops/containerization.md +9 -9
  207. package/skills/devops/cost-optimization.md +1 -1
  208. package/skills/devops/data-engineering.md +2 -2
  209. package/skills/devops/database.md +1 -1
  210. package/skills/devops/dependency-management.md +3 -3
  211. package/skills/devops/devsecops.md +1 -1
  212. package/skills/devops/git-workflow.md +1 -1
  213. package/skills/devops/incident-response.md +18 -18
  214. package/skills/devops/monorepo.md +5 -5
  215. package/skills/devops/observability.md +1 -1
  216. package/skills/devops/performance.md +1 -1
  217. package/skills/devops/testing.md +1 -1
  218. package/skills/docs/adr-workflow.md +2 -2
  219. package/skills/docs/backlog-proposal-workflow.md +1 -1
  220. package/skills/docs/customer-profile-workflow.md +1 -1
  221. package/skills/docs/document-ingest-workflow.md +1 -1
  222. package/skills/docs/evidence-ingest-workflow.md +1 -1
  223. package/skills/docs/init-docs.md +15 -15
  224. package/skills/docs/init-project.md +1 -1
  225. package/skills/docs/prd-workflow.md +3 -3
  226. package/skills/docs/prfaq-workflow.md +1 -1
  227. package/skills/docs/product-intelligence-review.md +1 -1
  228. package/skills/docs/product-intelligence-workflow.md +1 -1
  229. package/skills/docs/product-signal-workflow.md +9 -9
  230. package/skills/docs/research-workflow.md +10 -10
  231. package/skills/docs/runbook-workflow.md +2 -2
  232. package/skills/docs/strategy-workflow.md +3 -3
  233. package/skills/exploration/repo-map.md +11 -11
  234. package/skills/frameworks/django.md +15 -15
  235. package/skills/frameworks/nextjs.md +16 -16
  236. package/skills/frameworks/react.md +12 -12
  237. package/skills/frameworks/spring-boot.md +12 -12
  238. package/skills/frontend-design/accessibility.md +6 -6
  239. package/skills/frontend-design/component-patterns.md +1 -1
  240. package/skills/frontend-design/engineering.md +1 -1
  241. package/skills/frontend-design/state-management.md +1 -1
  242. package/skills/frontend-design/ui-aesthetics.md +1 -1
  243. package/skills/frontend-design/ux-principles.md +1 -1
  244. package/skills/operating/orchestration-reference.md +27 -27
  245. package/skills/quality-gates/review-work.md +3 -3
  246. package/skills/quality-gates/verify-change.md +1 -1
  247. package/skills/quality-gates/verify-module.md +1 -1
  248. package/skills/quality-gates/verify-quality.md +1 -1
  249. package/skills/quality-gates/verify-security.md +1 -1
  250. package/skills/routing.md +14 -14
  251. package/skills/security/blue-team.md +1 -1
  252. package/skills/security/code-audit.md +1 -1
  253. package/skills/security/pentest.md +1 -1
  254. package/skills/security/red-team.md +1 -1
  255. package/skills/security/threat-intel.md +1 -1
  256. package/skills/security/vuln-research.md +1 -1
  257. package/skills/utility/clean-code.md +2 -2
  258. package/templates/docs/changelog-entry.md +1 -1
  259. package/templates/docs/construct_guide.md +18 -18
  260. package/templates/docs/meta-prd.md +16 -16
  261. package/templates/docs/one-pager.md +1 -1
  262. package/templates/docs/prd-business.md +1 -1
  263. package/templates/docs/prd-platform.md +1 -1
  264. package/templates/docs/prd.md +17 -17
  265. package/templates/docs/research-brief.md +8 -8
  266. package/templates/docs/rfc.md +1 -1
  267. package/templates/docs/skill-artifact.md +1 -1
  268. package/templates/docs/strategy.md +1 -1
@@ -0,0 +1,270 @@
1
+ /**
2
+ * lib/mcp/tools/profile.mjs — MCP wrappers for the org-profile + learning surfaces.
3
+ *
4
+ * Bridges PR #67 capabilities (profiles, outcomes, sandboxes, knowledge_add,
5
+ * learning status) so subagents talking through the construct-mcp server can
6
+ * read the same state the CLI exposes. Operator-only surfaces (optimize_apply,
7
+ * optimize_rollback) remain CLI-only and are not registered here.
8
+ *
9
+ * Pattern: each export is a thin schema-validated wrapper around the existing
10
+ * lib/* function the CLI already uses. Errors return `{ error: string }` so
11
+ * the MCP dispatcher hands a structured failure back instead of crashing.
12
+ * Destructive operations (profile_archive) require `confirm: true`, matching
13
+ * the storage_reset / delete_ingested_artifacts pattern in tools/storage.mjs.
14
+ */
15
+ import { resolve } from 'node:path';
16
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+
19
+ import {
20
+ loadProfile,
21
+ listProfiles,
22
+ loadCustomProfile,
23
+ resolveActiveProfile,
24
+ } from '../../profiles/loader.mjs';
25
+ import {
26
+ createDraftProfile,
27
+ listDrafts,
28
+ archiveProfile,
29
+ profileHealth,
30
+ } from '../../profiles/lifecycle.mjs';
31
+ import { recordOutcome } from '../../outcomes/record.mjs';
32
+ import { aggregateOutcomes, readSummary } from '../../outcomes/aggregate.mjs';
33
+ import { addResearchFinding } from '../../knowledge/research-store.mjs';
34
+ import { askGlobal } from '../../knowledge/graph.mjs';
35
+ import { listSandboxes } from '../../sandbox.mjs';
36
+
37
+ function cwdOf(args) {
38
+ return args && args.cwd ? resolve(String(args.cwd)) : process.cwd();
39
+ }
40
+
41
+ // Read-only: active profile.
42
+
43
+ export function profileShow(args = {}) {
44
+ const cwd = cwdOf(args);
45
+ const profile = resolveActiveProfile(cwd, args.id ? String(args.id) : null);
46
+ return {
47
+ id: profile.id,
48
+ displayName: profile.displayName ?? profile.id,
49
+ tagline: profile.tagline ?? null,
50
+ custom: profile.custom === true,
51
+ roles: Array.isArray(profile.roles) ? profile.roles : [],
52
+ departments: Array.isArray(profile.departments)
53
+ ? profile.departments.map((d) => ({ id: d.id, displayName: d.displayName, roleCount: Array.isArray(d.roles) ? d.roles.length : 0 }))
54
+ : [],
55
+ intake: {
56
+ types: profile.intake?.types ?? [],
57
+ stages: profile.intake?.stages ?? [],
58
+ },
59
+ docTemplates: profile.docTemplates ?? [],
60
+ hooks: profile.hooks ?? null,
61
+ rebrand: profile.rebrand ?? null,
62
+ };
63
+ }
64
+
65
+ // Read-only: catalog summary.
66
+
67
+ export function profileList() {
68
+ return {
69
+ profiles: listProfiles().map((id) => {
70
+ const p = loadProfile(id);
71
+ return p
72
+ ? {
73
+ id: p.id,
74
+ displayName: p.displayName ?? p.id,
75
+ tagline: p.tagline ?? null,
76
+ roleCount: Array.isArray(p.roles) ? p.roles.length : 0,
77
+ departmentCount: Array.isArray(p.departments) ? p.departments.length : 0,
78
+ }
79
+ : { id, error: 'failed to parse' };
80
+ }),
81
+ };
82
+ }
83
+
84
+ // Read-only: drafts + custom profile under .cx/.
85
+
86
+ export function profileDrafts(args = {}) {
87
+ const cwd = cwdOf(args);
88
+ const drafts = listDrafts(cwd).map((d) => ({
89
+ id: d.id,
90
+ dir: d.dir,
91
+ hasProfile: d.hasProfile,
92
+ hasBrief: d.hasBrief,
93
+ }));
94
+ const custom = loadCustomProfile(cwd);
95
+ return {
96
+ drafts,
97
+ custom: custom ? { id: custom.id, displayName: custom.displayName ?? custom.id } : null,
98
+ };
99
+ }
100
+
101
+ // Read-only: per-profile health rollup.
102
+
103
+ export function profileHealthTool(args = {}) {
104
+ const cwd = cwdOf(args);
105
+ const id = args.id ? String(args.id) : resolveActiveProfile(cwd).id;
106
+ const windowDays = Number.isFinite(args.window_days) ? Number(args.window_days) : 30;
107
+ return { id, windowDays, ...profileHealth(cwd, id, { windowDays }) };
108
+ }
109
+
110
+ // Read-only: outcomes rollup. Optional aggregate=true rebuilds _summary.json.
111
+
112
+ export function outcomesSummary(args = {}) {
113
+ const cwd = cwdOf(args);
114
+ if (args.aggregate === true) {
115
+ try { aggregateOutcomes(cwd); } catch (err) { return { error: `aggregate failed: ${err.message}` }; }
116
+ }
117
+ const summary = readSummary(cwd);
118
+ if (!summary) return { roles: {}, note: 'no outcomes recorded yet' };
119
+ return summary;
120
+ }
121
+
122
+ // Mutating, gated: append an outcome line.
123
+
124
+ export function outcomesRecord(args = {}) {
125
+ if (args.confirm !== true) {
126
+ return { error: 'outcomes_record requires confirm=true to write durable state' };
127
+ }
128
+ const cwd = cwdOf(args);
129
+ if (!args.role || typeof args.success !== 'boolean') {
130
+ return { error: 'outcomes_record requires role:string and success:boolean' };
131
+ }
132
+ const profile = args.profile ? String(args.profile) : resolveActiveProfile(cwd)?.id;
133
+ const file = recordOutcome(cwd, {
134
+ role: String(args.role),
135
+ intakeId: args.intake_id ? String(args.intake_id) : null,
136
+ profile,
137
+ success: !!args.success,
138
+ escalated: !!args.escalated,
139
+ durationMs: Number.isFinite(args.duration_ms) ? Number(args.duration_ms) : null,
140
+ notes: args.notes ? String(args.notes) : null,
141
+ source: args.source ? String(args.source) : 'mcp',
142
+ });
143
+ if (!file) return { error: 'outcomes_record: write failed' };
144
+ return { ok: true, file };
145
+ }
146
+
147
+ // Mutating, gated: persist a research finding.
148
+
149
+ export async function knowledgeAdd(args = {}) {
150
+ if (args.confirm !== true) {
151
+ return { error: 'knowledge_add requires confirm=true to write durable state' };
152
+ }
153
+ const cwd = cwdOf(args);
154
+ try {
155
+ const res = await addResearchFinding({
156
+ cwd,
157
+ slug: String(args.slug || ''),
158
+ topic: String(args.topic || ''),
159
+ body: String(args.body || ''),
160
+ confidence: args.confidence ? String(args.confidence) : 'inferred',
161
+ sources: Array.isArray(args.sources) ? args.sources : [],
162
+ ttlDays: Number.isFinite(args.ttl_days) ? Number(args.ttl_days) : undefined,
163
+ });
164
+ return { ok: true, path: res.path, bytes: res.bytes };
165
+ } catch (err) {
166
+ return { error: err.message ?? String(err) };
167
+ }
168
+ }
169
+
170
+ // Mutating, gated: scaffold a draft profile.
171
+
172
+ export function profileCreate(args = {}) {
173
+ if (args.confirm !== true) {
174
+ return { error: 'profile_create requires confirm=true to scaffold files' };
175
+ }
176
+ const cwd = cwdOf(args);
177
+ if (!args.id) return { error: 'profile_create requires id:string' };
178
+ try {
179
+ const res = createDraftProfile({
180
+ cwd,
181
+ id: String(args.id),
182
+ displayName: args.display_name ? String(args.display_name) : undefined,
183
+ seedRoles: Array.isArray(args.seed_roles) ? args.seed_roles.map(String) : [],
184
+ seedDepartments: Array.isArray(args.seed_departments) ? args.seed_departments : [],
185
+ });
186
+ return {
187
+ ok: true,
188
+ dir: res.dir,
189
+ briefPath: res.briefPath,
190
+ draftPath: res.draftPath,
191
+ personaPaths: res.personaPaths,
192
+ departmentPaths: res.departmentPaths,
193
+ };
194
+ } catch (err) {
195
+ return { error: err.message ?? String(err) };
196
+ }
197
+ }
198
+
199
+ // Mutating, gated, destructive: archive a curated profile.
200
+
201
+ export function profileArchive(args = {}) {
202
+ if (args.confirm !== true) {
203
+ return { error: 'profile_archive requires confirm=true (destructive: moves files into archive/profiles/)' };
204
+ }
205
+ if (!args.id || !args.reason || String(args.reason).trim().length < 8) {
206
+ return { error: 'profile_archive requires id:string and reason:string (>=8 chars)' };
207
+ }
208
+ try {
209
+ return { ok: true, ...archiveProfile({ id: String(args.id), reason: String(args.reason) }) };
210
+ } catch (err) {
211
+ return { error: err.message ?? String(err) };
212
+ }
213
+ }
214
+
215
+ // Read-only: sandbox roster.
216
+
217
+ export function sandboxList() {
218
+ return { sandboxes: listSandboxes() };
219
+ }
220
+
221
+ // Read-only: GraphRAG global query over the entity graph.
222
+
223
+ export function knowledgeGraphAsk(args = {}) {
224
+ const cwd = cwdOf(args);
225
+ if (!args.query || typeof args.query !== 'string') {
226
+ return { error: 'knowledge_graph_ask requires query:string' };
227
+ }
228
+ const topK = Number.isFinite(args.top_k) ? Number(args.top_k) : 5;
229
+ const minSize = Number.isFinite(args.min_size) ? Number(args.min_size) : undefined;
230
+ return askGlobal({ query: String(args.query), rootDir: cwd, topK, ...(minSize !== undefined ? { minSize } : {}) });
231
+ }
232
+
233
+ // Read-only: learning dashboard mirror.
234
+
235
+ export function learningStatus(args = {}) {
236
+ const cwd = cwdOf(args);
237
+
238
+ let observations = { total: 0, last24h: 0 };
239
+ try {
240
+ const idxPath = join(cwd, '.cx', 'observations', 'index.json');
241
+ if (existsSync(idxPath)) {
242
+ const idx = JSON.parse(readFileSync(idxPath, 'utf8'));
243
+ if (Array.isArray(idx)) {
244
+ const since = Date.now() - 24 * 60 * 60 * 1000;
245
+ observations = {
246
+ total: idx.length,
247
+ last24h: idx.filter((e) => Date.parse(e?.createdAt) >= since).length,
248
+ };
249
+ }
250
+ }
251
+ } catch { /* best effort */ }
252
+
253
+ let researchCount = 0;
254
+ try {
255
+ const dir = join(cwd, '.cx', 'knowledge', 'external', 'research');
256
+ if (existsSync(dir)) {
257
+ researchCount = readdirSync(dir).filter((f) => f.endsWith('.md')).length;
258
+ }
259
+ } catch { /* best effort */ }
260
+
261
+ const profile = resolveActiveProfile(cwd);
262
+ const outcomes = readSummary(cwd) || { roles: {} };
263
+
264
+ return {
265
+ profile: { id: profile.id, displayName: profile.displayName ?? profile.id, custom: profile.custom === true },
266
+ observations,
267
+ research: { count: researchCount },
268
+ outcomes,
269
+ };
270
+ }
@@ -96,6 +96,31 @@ export function workflowValidate(args) {
96
96
  return { cwd, ...result };
97
97
  }
98
98
 
99
+ /**
100
+ * Validate a producer→consumer handoff against agents/contracts.json.
101
+ *
102
+ * Args:
103
+ * producer — agent or persona name producing the artifact
104
+ * consumer — agent or persona name receiving it
105
+ * id — optional contract id (overrides producer/consumer lookup)
106
+ * artifact — the handoff payload to validate
107
+ * enforcement — 'warn' (default) or 'block'; 'block' returns ok:false on
108
+ * violation so the workflow can refuse to advance
109
+ *
110
+ * Returns { ok, status?, errors?, warnings?, contract } where status is set
111
+ * to 'BLOCKED_CONTRACT' on enforced violations.
112
+ */
113
+ export async function workflowContractValidate(args) {
114
+ const { validateHandoff } = await import('../../contracts/validate.mjs');
115
+ return validateHandoff({
116
+ producer: args.producer,
117
+ consumer: args.consumer,
118
+ id: args.id,
119
+ artifact: args.artifact,
120
+ enforcement: args.enforcement || process.env.CONSTRUCT_CONTRACT_ENFORCEMENT || 'warn',
121
+ });
122
+ }
123
+
99
124
  export function workflowImportPlan(args) {
100
125
  const cwd = args.cwd ? resolve(args.cwd) : process.cwd();
101
126
  const markdown = args.markdown ?? '';
@@ -55,21 +55,25 @@
55
55
  "id": "memory",
56
56
  "name": "Memory",
57
57
  "category": "optional",
58
- "description": "cass-memory over HTTP. Persistent cross-session and cross-agent memory for decisions, findings, and handoffs.",
59
- "type": "url",
60
- "url": "http://127.0.0.1:8765/",
61
- "env": {},
58
+ "description": "cass-memory persistent cross-session and cross-agent memory, fronted by the Construct stdio MCP bridge so editors get a full MCP handshake.",
59
+ "command": "node",
60
+ "args": [
61
+ "__CX_TOOLKIT_DIR__/lib/mcp/memory-bridge.mjs"
62
+ ],
63
+ "env": {
64
+ "CONSTRUCT_MEMORY_BRIDGE_URL": "http://127.0.0.1:__MEMORY_PORT__/"
65
+ },
62
66
  "requiredEnv": [],
63
67
  "setupModes": [
64
68
  "auto",
65
69
  "manual"
66
70
  ],
67
71
  "hostSupport": {
68
- "claude": { "mode": "unsupported", "label": "construct-mcp (project .mcp.json)" },
69
- "opencode": { "mode": "unsupported", "label": "construct-mcp (stdio)" },
70
- "codex": { "mode": "unsupported", "label": "construct-mcp / local observation tools" }
72
+ "claude": { "mode": "managed" },
73
+ "opencode": { "mode": "managed" },
74
+ "codex": { "mode": "managed" }
71
75
  },
72
- "setupNote": "Claude Code and Codex reject the cass-memory HTTP endpoint because `cm serve` skips the MCP `initialize` handshake. On Claude, memory tools (memory_search, memory_add_observations, memory_recent, memory_create_entities) are exposed through the project-local construct-mcp stdio server in .mcp.json. OpenCode does not support the cass-memory HTTP endpoint (SSE transport fails with 405) — use construct-mcp stdio server instead, which wraps memory tools correctly.",
76
+ "setupNote": "Memory is wired as a stdio MCP bridge (lib/mcp/memory-bridge.mjs). The bridge answers the MCP handshake locally (initialize, ping, notifications/initialized) and forwards tools/list, tools/call, resources/list, resources/read to cm at CONSTRUCT_MEMORY_BRIDGE_URL. cm itself never sees the handshake calls that v0.2.x cannot answer.",
73
77
  "usedBy": [
74
78
  "construct",
75
79
  "cx-docs-keeper",
@@ -48,13 +48,21 @@ function buildRemoteHeaders(mcpDef, resolvedValues) {
48
48
  return stripUnresolvedValues(resolveTemplateObject(mcpDef.headers ?? {}, resolvedValues));
49
49
  }
50
50
 
51
+ function withMemoryDefaults(id, values) {
52
+ if (id !== "memory") return values;
53
+ if (values.MEMORY_PORT) return values;
54
+ return { ...values, MEMORY_PORT: "8765" };
55
+ }
56
+
51
57
  export function buildClaudeMcpEntry(id, mcpDef, resolvedValues = {}) {
58
+ const values = withMemoryDefaults(id, { CX_TOOLKIT_DIR: ROOT_DIR, ...resolvedValues });
59
+
52
60
  if (mcpDef.type === "url") {
53
- const headers = buildRemoteHeaders(mcpDef, resolvedValues);
61
+ const headers = buildRemoteHeaders(mcpDef, values);
54
62
  const url =
55
- id === "memory" && resolvedValues.MEMORY_PORT
56
- ? `http://127.0.0.1:${resolvedValues.MEMORY_PORT}/`
57
- : resolveTemplateString(mcpDef.url, resolvedValues);
63
+ id === "memory" && values.MEMORY_PORT
64
+ ? `http://127.0.0.1:${values.MEMORY_PORT}/`
65
+ : resolveTemplateString(mcpDef.url, values);
58
66
  return {
59
67
  type: "http",
60
68
  url,
@@ -62,19 +70,19 @@ export function buildClaudeMcpEntry(id, mcpDef, resolvedValues = {}) {
62
70
  };
63
71
  }
64
72
 
65
- const env = buildLocalEnvironment(mcpDef, resolvedValues);
73
+ const env = buildLocalEnvironment(mcpDef, values);
66
74
  return {
67
75
  command: mcpDef.command,
68
- args: resolveArgs(mcpDef.args, resolvedValues),
76
+ args: resolveArgs(mcpDef.args, values),
69
77
  ...(Object.keys(env).length > 0 ? { env } : {}),
70
78
  };
71
79
  }
72
80
 
73
81
  export function buildOpenCodeMcpEntry(id, mcpDef, resolvedValues = {}) {
74
- const runtimeValues = {
82
+ const runtimeValues = withMemoryDefaults(id, {
75
83
  CX_TOOLKIT_DIR: ROOT_DIR,
76
84
  ...resolvedValues,
77
- };
85
+ });
78
86
  const openCodeId = getOpenCodeMcpId(id);
79
87
 
80
88
  if (mcpDef.type === "url") {
@@ -0,0 +1,106 @@
1
+ /**
2
+ * lib/migrations/index.mjs — Schema migration registry and runner.
3
+ *
4
+ * Construct's machine-readable artifacts (agents/registry.json, contracts.json,
5
+ * role-manifests.json, .cx/config.json, profile JSON) carry an integer
6
+ * `version` field. When the installed binary's expected schema version is
7
+ * higher than the on-disk version, migrations apply in order. When the on-disk
8
+ * version is higher, the runner refuses to load and instructs the operator to
9
+ * upgrade.
10
+ *
11
+ * Each migration is a module under lib/migrations/ that exports:
12
+ * - from: integer
13
+ * - to: integer
14
+ * - apply(artifactPath, options): Promise<{ changed, summary }>
15
+ *
16
+ * v1 is the baseline — no migration needed. The registry below is the contract
17
+ * between the installed binary and migration authors.
18
+ */
19
+
20
+ import v1Baseline from './v1-baseline.mjs';
21
+
22
+ const REGISTRY = [
23
+ v1Baseline,
24
+ ];
25
+
26
+ export const CURRENT_SCHEMA_VERSION = Math.max(...REGISTRY.map((m) => m.to));
27
+
28
+ /**
29
+ * Compute the migration path from `fromVersion` to CURRENT_SCHEMA_VERSION.
30
+ * Returns an array of migrations to apply in order, or null if no path exists.
31
+ */
32
+ export function planMigrations(fromVersion, toVersion = CURRENT_SCHEMA_VERSION) {
33
+ if (fromVersion === toVersion) return [];
34
+ if (fromVersion > toVersion) return null;
35
+ const steps = [];
36
+ let cursor = fromVersion;
37
+ while (cursor < toVersion) {
38
+ const step = REGISTRY.find((m) => m.from === cursor);
39
+ if (!step) return null;
40
+ steps.push(step);
41
+ cursor = step.to;
42
+ }
43
+ return steps;
44
+ }
45
+
46
+ /**
47
+ * Apply a planned migration sequence. Each step receives the artifact path
48
+ * and any options. Returns a summary including every step's outcome.
49
+ *
50
+ * When dryRun is true, steps must not write to disk; they only report what
51
+ * they would change.
52
+ */
53
+ export async function runMigrations({ artifactPath, fromVersion, toVersion = CURRENT_SCHEMA_VERSION, dryRun = false }) {
54
+ const plan = planMigrations(fromVersion, toVersion);
55
+ if (plan === null) {
56
+ return {
57
+ ok: false,
58
+ error: `no migration path from version ${fromVersion} to ${toVersion}`,
59
+ applied: [],
60
+ };
61
+ }
62
+ if (plan.length === 0) {
63
+ return { ok: true, applied: [], summary: 'no migrations needed' };
64
+ }
65
+
66
+ const applied = [];
67
+ for (const step of plan) {
68
+ try {
69
+ const result = await step.apply(artifactPath, { dryRun });
70
+ applied.push({ from: step.from, to: step.to, ...result });
71
+ } catch (err) {
72
+ return {
73
+ ok: false,
74
+ error: `migration ${step.from}→${step.to} failed: ${err.message}`,
75
+ applied,
76
+ };
77
+ }
78
+ }
79
+ return { ok: true, applied, summary: `applied ${applied.length} migration(s)` };
80
+ }
81
+
82
+ /**
83
+ * Check artifact compatibility. Returns:
84
+ * { compatible: true } — versions match
85
+ * { compatible: false, needsMigration: true, ... } — on-disk is older
86
+ * { compatible: false, needsUpgrade: true, ... } — on-disk is newer
87
+ */
88
+ export function checkCompatibility(artifactVersion, expected = CURRENT_SCHEMA_VERSION) {
89
+ if (artifactVersion === expected) return { compatible: true };
90
+ if (artifactVersion < expected) {
91
+ return {
92
+ compatible: false,
93
+ needsMigration: true,
94
+ fromVersion: artifactVersion,
95
+ toVersion: expected,
96
+ message: `artifact schema version ${artifactVersion} is older than expected ${expected}; run 'construct migrate'`,
97
+ };
98
+ }
99
+ return {
100
+ compatible: false,
101
+ needsUpgrade: true,
102
+ fromVersion: artifactVersion,
103
+ toVersion: expected,
104
+ message: `artifact schema version ${artifactVersion} is newer than this binary supports (${expected}); run 'construct upgrade'`,
105
+ };
106
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * lib/migrations/v1-baseline.mjs — No-op baseline migration.
3
+ *
4
+ * Marks v0 (pre-versioned artifacts) as compatible with v1 by stamping the
5
+ * artifact with `version: 1` when absent. Schema bumps after v1 will add new
6
+ * migrations rather than modifying this one.
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
10
+
11
+ export default {
12
+ from: 0,
13
+ to: 1,
14
+ description: 'Stamp pre-versioned artifacts with version: 1 (no shape changes)',
15
+ async apply(artifactPath, { dryRun = false } = {}) {
16
+ if (!existsSync(artifactPath)) {
17
+ return { changed: false, summary: `${artifactPath}: not present (skip)` };
18
+ }
19
+ let data;
20
+ try { data = JSON.parse(readFileSync(artifactPath, 'utf8')); }
21
+ catch (err) { throw new Error(`failed to parse ${artifactPath}: ${err.message}`); }
22
+
23
+ if (data && typeof data === 'object' && data.version === 1) {
24
+ return { changed: false, summary: `${artifactPath}: already at v1` };
25
+ }
26
+
27
+ const next = { ...data, version: 1 };
28
+ if (!dryRun) {
29
+ writeFileSync(artifactPath, JSON.stringify(next, null, 2) + '\n');
30
+ }
31
+ return { changed: true, summary: `${artifactPath}: stamped version: 1` };
32
+ },
33
+ };
@@ -19,6 +19,7 @@ import { embedSync as embedText } from './storage/embeddings-legacy.mjs';
19
19
  import { embedText as embedTextEngine } from './storage/embeddings-engine.mjs';
20
20
  import { VectorClient } from './storage/vector-client.mjs';
21
21
  import { withFileLockSync } from './storage/file-lock.mjs';
22
+ import { ensureCxDir } from './project-init-shared.mjs';
22
23
 
23
24
  const OBS_DIR = '.cx/observations';
24
25
  const INDEX_FILE = 'index.json';
@@ -36,7 +37,10 @@ const VALID_CATEGORIES = new Set([
36
37
  'pattern', 'anti-pattern', 'dependency', 'decision', 'insight', 'session-summary',
37
38
  ]);
38
39
 
39
- function ensureDir(dir) {
40
+ function ensureDir(dir, rootDir = null) {
41
+ // Preserves the construct context invariant: any code that creates a .cx/
42
+ // subdirectory also initializes context.md from the project template.
43
+ if (rootDir) ensureCxDir(rootDir);
40
44
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
41
45
  }
42
46
 
@@ -91,6 +95,7 @@ function logCapDrop(rootDir, kind, dropped, total) {
91
95
  if (dropped <= 0) return;
92
96
  try {
93
97
  const logPath = path.join(rootDir, '.cx', 'observation-cap-warnings.jsonl');
98
+ ensureCxDir(rootDir);
94
99
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
95
100
  const entry = JSON.stringify({
96
101
  ts: new Date().toISOString(),
@@ -105,7 +110,7 @@ function logCapDrop(rootDir, kind, dropped, total) {
105
110
  }
106
111
 
107
112
  function writeIndex(rootDir, entries) {
108
- ensureDir(obsDir(rootDir));
113
+ ensureDir(obsDir(rootDir), rootDir);
109
114
  const trimmed = entries.slice(0, MAX_INDEX);
110
115
  if (entries.length > MAX_INDEX) {
111
116
  logCapDrop(rootDir, 'observation-index', entries.length - MAX_INDEX, entries.length);
@@ -124,7 +129,7 @@ function readVectors(rootDir) {
124
129
  }
125
130
 
126
131
  function writeVectors(rootDir, records) {
127
- ensureDir(obsDir(rootDir));
132
+ ensureDir(obsDir(rootDir), rootDir);
128
133
  if (records.length > MAX_INDEX) {
129
134
  logCapDrop(rootDir, 'observation-vectors', records.length - MAX_INDEX, records.length);
130
135
  }
@@ -177,7 +182,7 @@ export async function addObservation(rootDir, {
177
182
  // Lock the per-store index so concurrent writers (CLI + hooks) cannot
178
183
  // corrupt the index/vectors JSON pair. The observation JSON file is
179
184
  // unique-per-id and doesn't need the lock; the shared structures do.
180
- ensureDir(obsDir(rootDir));
185
+ ensureDir(obsDir(rootDir), rootDir);
181
186
  fs.writeFileSync(
182
187
  path.join(obsDir(rootDir), `${id}.json`),
183
188
  JSON.stringify(record, null, 2) + '\n',
@@ -16,6 +16,7 @@
16
16
  import fs from 'node:fs';
17
17
  import path from 'node:path';
18
18
  import { withFileLockSync } from '../storage/file-lock.mjs';
19
+ import { ensureCxDir } from '../project-init-shared.mjs';
19
20
 
20
21
  const ROTATE_AT = 10_000;
21
22
  const MAX_NOTES = 500;
@@ -65,6 +66,7 @@ function countLines(file) {
65
66
  export function recordOutcome(cwd, payload) {
66
67
  if (!cwd || !payload || typeof payload.success !== 'boolean') return null;
67
68
  try {
69
+ ensureCxDir(cwd);
68
70
  fs.mkdirSync(outcomesDir(cwd), { recursive: true });
69
71
  const file = activeFile(cwd, payload.role);
70
72
 
@@ -0,0 +1,46 @@
1
+ /**
2
+ * lib/profiles/rebrand.mjs — Profile-aware language helper.
3
+ *
4
+ * Each curated profile carries a `rebrand` block: how the active profile
5
+ * wants the intake queue and individual signal items named in user-facing
6
+ * surfaces (CLI output, daemon logs, session-start prelude). The helper
7
+ * centralises that lookup so every consumer goes through one path with
8
+ * safe defaults instead of hardcoding "intake queue" / "signal" / "intake".
9
+ *
10
+ * Defaults match the legacy strings so behaviour is unchanged for the rnd
11
+ * profile and for any environment where profile resolution fails.
12
+ */
13
+
14
+ import { resolveActiveProfile } from './loader.mjs';
15
+
16
+ export const DEFAULT_REBRAND = Object.freeze({
17
+ intakeQueueLabel: 'Intake queue',
18
+ signalNoun: 'signal',
19
+ });
20
+
21
+ /**
22
+ * Return the active profile's rebrand labels.
23
+ *
24
+ * @param {string} [rootDir] - project root where profile.json / construct.config.json live.
25
+ * @returns {{ intakeQueueLabel: string, signalNoun: string }}
26
+ *
27
+ * Returns defaults when `rootDir` is missing, when the profile cannot be
28
+ * loaded, or when the profile omits its `rebrand` block. Never throws.
29
+ */
30
+ export function getRebrand(rootDir) {
31
+ if (!rootDir || typeof rootDir !== 'string') return { ...DEFAULT_REBRAND };
32
+ try {
33
+ const profile = resolveActiveProfile(rootDir);
34
+ const rb = profile?.rebrand;
35
+ if (!rb || typeof rb !== 'object') return { ...DEFAULT_REBRAND };
36
+ const intakeQueueLabel = typeof rb.intakeQueueLabel === 'string' && rb.intakeQueueLabel.trim()
37
+ ? rb.intakeQueueLabel.trim()
38
+ : DEFAULT_REBRAND.intakeQueueLabel;
39
+ const signalNoun = typeof rb.signalNoun === 'string' && rb.signalNoun.trim()
40
+ ? rb.signalNoun.trim()
41
+ : DEFAULT_REBRAND.signalNoun;
42
+ return { intakeQueueLabel, signalNoun };
43
+ } catch {
44
+ return { ...DEFAULT_REBRAND };
45
+ }
46
+ }
@@ -235,6 +235,18 @@ Describe the current objective in one clear sentence.
235
235
  `;
236
236
  }
237
237
 
238
+ export function ensureCxDir(rootDir) {
239
+ const cxDir = path.join(rootDir, '.cx');
240
+ const contextPath = path.join(cxDir, 'context.md');
241
+ if (!fs.existsSync(cxDir)) {
242
+ fs.mkdirSync(cxDir, { recursive: true });
243
+ }
244
+ if (!fs.existsSync(contextPath)) {
245
+ fs.writeFileSync(contextPath, buildContextMarkdown(), 'utf8');
246
+ }
247
+ return cxDir;
248
+ }
249
+
238
250
  export function buildContextMarkdown() {
239
251
  return `<!--
240
252
  .cx/context.md — concise resumable project context for human and agent handoff.