@geraldmaron/construct 1.0.4 → 1.0.6

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 (293) hide show
  1. package/README.md +57 -44
  2. package/agents/prompts/cx-accessibility.md +3 -3
  3. package/agents/prompts/cx-ai-engineer.md +7 -7
  4. package/agents/prompts/cx-architect.md +6 -6
  5. package/agents/prompts/cx-business-strategist.md +6 -6
  6. package/agents/prompts/cx-data-analyst.md +7 -7
  7. package/agents/prompts/cx-data-engineer.md +3 -3
  8. package/agents/prompts/cx-debugger.md +6 -6
  9. package/agents/prompts/cx-designer.md +5 -5
  10. package/agents/prompts/cx-devil-advocate.md +4 -4
  11. package/agents/prompts/cx-docs-keeper.md +5 -5
  12. package/agents/prompts/cx-engineer.md +5 -5
  13. package/agents/prompts/cx-evaluator.md +2 -2
  14. package/agents/prompts/cx-explorer.md +8 -8
  15. package/agents/prompts/cx-legal-compliance.md +2 -2
  16. package/agents/prompts/cx-operations.md +5 -5
  17. package/agents/prompts/cx-orchestrator.md +10 -10
  18. package/agents/prompts/cx-platform-engineer.md +5 -5
  19. package/agents/prompts/cx-product-manager.md +4 -4
  20. package/agents/prompts/cx-qa.md +8 -8
  21. package/agents/prompts/cx-rd-lead.md +9 -9
  22. package/agents/prompts/cx-release-manager.md +5 -5
  23. package/agents/prompts/cx-researcher.md +22 -22
  24. package/agents/prompts/cx-reviewer.md +7 -7
  25. package/agents/prompts/cx-security.md +10 -10
  26. package/agents/prompts/cx-sre.md +7 -7
  27. package/agents/prompts/cx-test-automation.md +3 -3
  28. package/agents/prompts/cx-trace-reviewer.md +8 -8
  29. package/agents/prompts/cx-ux-researcher.md +3 -3
  30. package/bin/construct +470 -4
  31. package/commands/build/feature.md +4 -4
  32. package/commands/build/fix.md +8 -8
  33. package/commands/design/access.md +3 -3
  34. package/commands/design/flow.md +3 -3
  35. package/commands/design/ui.md +4 -4
  36. package/commands/measure/experiment.md +5 -5
  37. package/commands/measure/metrics.md +3 -3
  38. package/commands/measure/results.md +4 -4
  39. package/commands/plan/api.md +3 -3
  40. package/commands/plan/challenge.md +3 -3
  41. package/commands/plan/decide.md +3 -3
  42. package/commands/plan/feature.md +7 -7
  43. package/commands/plan/requirements.md +3 -3
  44. package/commands/remember/context.md +5 -5
  45. package/commands/remember/handoff.md +3 -3
  46. package/commands/remember/runbook.md +3 -3
  47. package/commands/review/code.md +8 -8
  48. package/commands/review/quality.md +4 -4
  49. package/commands/review/security.md +3 -3
  50. package/commands/ship/ready.md +3 -3
  51. package/commands/ship/release.md +3 -3
  52. package/commands/ship/status.md +4 -4
  53. package/commands/understand/docs.md +3 -3
  54. package/commands/understand/research.md +5 -3
  55. package/commands/understand/this.md +4 -4
  56. package/commands/understand/why.md +8 -8
  57. package/commands/work/clean.md +14 -14
  58. package/commands/work/drive.md +10 -10
  59. package/commands/work/optimize-prompts.md +9 -9
  60. package/commands/work/parallel-review.md +8 -8
  61. package/db/schema/006_graph.sql +24 -0
  62. package/examples/provider-plugin/README.md +7 -7
  63. package/examples/seed-observations/README.md +6 -6
  64. package/examples/seed-observations/anti-patterns.md +14 -14
  65. package/examples/seed-observations/decisions.md +4 -4
  66. package/examples/seed-observations/patterns.md +14 -14
  67. package/lib/auto-docs.mjs +10 -5
  68. package/lib/cli-commands.mjs +45 -1
  69. package/lib/comment-lint.mjs +7 -1
  70. package/lib/config/schema.mjs +3 -0
  71. package/lib/flavors/loader.mjs +136 -0
  72. package/lib/hooks/agent-tracker.mjs +22 -3
  73. package/lib/hooks/pre-push-gate.mjs +14 -1
  74. package/lib/hooks/session-optimize.mjs +3 -2
  75. package/lib/hooks/session-reflect.mjs +68 -0
  76. package/lib/init-unified.mjs +25 -2
  77. package/lib/intake/classify.mjs +61 -183
  78. package/lib/intake/prepare.mjs +7 -0
  79. package/lib/intake/tables/creative.mjs +94 -0
  80. package/lib/intake/tables/operations.mjs +85 -0
  81. package/lib/intake/tables/research.mjs +85 -0
  82. package/lib/intake/tables/rnd.mjs +175 -0
  83. package/lib/knowledge/graph.mjs +213 -0
  84. package/lib/knowledge/research-store.mjs +109 -0
  85. package/lib/mcp/server.mjs +187 -1
  86. package/lib/mcp/tools/profile.mjs +270 -0
  87. package/lib/observation-store.mjs +19 -0
  88. package/lib/outcomes/aggregate.mjs +104 -0
  89. package/lib/outcomes/record.mjs +115 -0
  90. package/lib/parity.mjs +6 -9
  91. package/lib/profiles/lifecycle.mjs +388 -0
  92. package/lib/profiles/loader.mjs +123 -0
  93. package/lib/profiles/validate-custom.mjs +114 -0
  94. package/lib/reflect/extractor.mjs +193 -0
  95. package/lib/reflect.mjs +89 -2
  96. package/lib/sandbox.mjs +102 -0
  97. package/package.json +6 -1
  98. package/personas/construct.md +20 -20
  99. package/platforms/claude/CLAUDE.md +6 -6
  100. package/platforms/claude/settings.template.json +13 -0
  101. package/rules/common/agents.md +2 -2
  102. package/rules/common/beads-hygiene.md +11 -11
  103. package/rules/common/code-review.md +1 -1
  104. package/rules/common/coding-style.md +1 -1
  105. package/rules/common/comments.md +8 -8
  106. package/rules/common/commit-approval.md +4 -4
  107. package/rules/common/cx-agent-routing.md +2 -2
  108. package/rules/common/cx-skill-routing.md +2 -2
  109. package/rules/common/development-workflow.md +1 -1
  110. package/rules/common/doc-ownership.md +2 -2
  111. package/rules/common/efficiency.md +3 -3
  112. package/rules/common/framing.md +1 -1
  113. package/rules/common/git-workflow.md +1 -1
  114. package/rules/common/patterns.md +1 -1
  115. package/rules/common/performance.md +1 -1
  116. package/rules/common/release-gates.md +7 -7
  117. package/rules/common/research.md +4 -4
  118. package/rules/common/security.md +1 -1
  119. package/rules/common/skill-composition.md +8 -8
  120. package/rules/common/testing.md +1 -1
  121. package/rules/golang/coding-style.md +2 -2
  122. package/rules/golang/hooks.md +1 -1
  123. package/rules/golang/patterns.md +1 -1
  124. package/rules/golang/security.md +1 -1
  125. package/rules/golang/testing.md +1 -1
  126. package/rules/python/coding-style.md +1 -1
  127. package/rules/python/hooks.md +1 -1
  128. package/rules/python/patterns.md +1 -1
  129. package/rules/python/security.md +1 -1
  130. package/rules/python/testing.md +1 -1
  131. package/rules/swift/coding-style.md +3 -3
  132. package/rules/swift/hooks.md +2 -2
  133. package/rules/swift/patterns.md +2 -2
  134. package/rules/swift/security.md +4 -4
  135. package/rules/swift/testing.md +2 -2
  136. package/rules/typescript/coding-style.md +1 -1
  137. package/rules/typescript/hooks.md +1 -1
  138. package/rules/typescript/patterns.md +1 -1
  139. package/rules/typescript/security.md +1 -1
  140. package/rules/typescript/testing.md +1 -1
  141. package/rules/web/coding-style.md +1 -1
  142. package/rules/web/design-quality.md +1 -1
  143. package/rules/web/hooks.md +1 -1
  144. package/rules/web/patterns.md +1 -1
  145. package/rules/web/performance.md +1 -1
  146. package/rules/web/security.md +1 -1
  147. package/rules/web/testing.md +1 -1
  148. package/scripts/sync-agents.mjs +11 -0
  149. package/skills/ai/agent-dev.md +1 -1
  150. package/skills/ai/llm-security.md +1 -1
  151. package/skills/ai/ml-ops.md +6 -6
  152. package/skills/ai/orchestration-workflow.md +1 -1
  153. package/skills/ai/prompt-and-eval.md +1 -1
  154. package/skills/ai/prompt-optimizer.md +13 -13
  155. package/skills/ai/rag-system.md +1 -1
  156. package/skills/architecture/api-design.md +1 -1
  157. package/skills/architecture/caching.md +1 -1
  158. package/skills/architecture/cloud-native.md +1 -1
  159. package/skills/architecture/message-queue.md +1 -1
  160. package/skills/architecture/security-arch.md +1 -1
  161. package/skills/compliance/ai-disclosure.md +1 -1
  162. package/skills/compliance/data-privacy.md +1 -1
  163. package/skills/compliance/license-audit.md +2 -2
  164. package/skills/compliance/regulatory-review.md +1 -1
  165. package/skills/development/cpp.md +1 -1
  166. package/skills/development/go.md +1 -1
  167. package/skills/development/java.md +1 -1
  168. package/skills/development/kotlin.md +9 -9
  169. package/skills/development/mobile-crossplatform.md +13 -13
  170. package/skills/development/python.md +1 -1
  171. package/skills/development/rust.md +1 -1
  172. package/skills/development/shell.md +1 -1
  173. package/skills/development/swift.md +6 -6
  174. package/skills/development/typescript.md +1 -1
  175. package/skills/devops/ci-cd.md +5 -5
  176. package/skills/devops/containerization.md +9 -9
  177. package/skills/devops/cost-optimization.md +1 -1
  178. package/skills/devops/data-engineering.md +2 -2
  179. package/skills/devops/database.md +1 -1
  180. package/skills/devops/dependency-management.md +3 -3
  181. package/skills/devops/devsecops.md +1 -1
  182. package/skills/devops/git-workflow.md +1 -1
  183. package/skills/devops/incident-response.md +18 -18
  184. package/skills/devops/monorepo.md +5 -5
  185. package/skills/devops/observability.md +1 -1
  186. package/skills/devops/performance.md +1 -1
  187. package/skills/devops/testing.md +1 -1
  188. package/skills/docs/adr-workflow.md +2 -2
  189. package/skills/docs/backlog-proposal-workflow.md +1 -1
  190. package/skills/docs/customer-profile-workflow.md +1 -1
  191. package/skills/docs/document-ingest-workflow.md +1 -1
  192. package/skills/docs/evidence-ingest-workflow.md +1 -1
  193. package/skills/docs/init-docs.md +15 -15
  194. package/skills/docs/init-project.md +1 -1
  195. package/skills/docs/prd-workflow.md +3 -3
  196. package/skills/docs/prfaq-workflow.md +1 -1
  197. package/skills/docs/product-intelligence-review.md +1 -1
  198. package/skills/docs/product-intelligence-workflow.md +1 -1
  199. package/skills/docs/product-signal-workflow.md +9 -9
  200. package/skills/docs/research-workflow.md +10 -10
  201. package/skills/docs/runbook-workflow.md +2 -2
  202. package/skills/docs/strategy-workflow.md +3 -3
  203. package/skills/exploration/repo-map.md +11 -11
  204. package/skills/frameworks/django.md +15 -15
  205. package/skills/frameworks/nextjs.md +16 -16
  206. package/skills/frameworks/react.md +12 -12
  207. package/skills/frameworks/spring-boot.md +12 -12
  208. package/skills/frontend-design/accessibility.md +6 -6
  209. package/skills/frontend-design/component-patterns.md +1 -1
  210. package/skills/frontend-design/engineering.md +1 -1
  211. package/skills/frontend-design/state-management.md +1 -1
  212. package/skills/frontend-design/ui-aesthetics.md +1 -1
  213. package/skills/frontend-design/ux-principles.md +1 -1
  214. package/skills/operating/orchestration-reference.md +27 -27
  215. package/skills/quality-gates/review-work.md +3 -3
  216. package/skills/quality-gates/verify-change.md +1 -1
  217. package/skills/quality-gates/verify-module.md +1 -1
  218. package/skills/quality-gates/verify-quality.md +1 -1
  219. package/skills/quality-gates/verify-security.md +1 -1
  220. package/skills/roles/architect.ai-systems.md +4 -2
  221. package/skills/roles/architect.data.md +4 -2
  222. package/skills/roles/architect.enterprise.md +4 -2
  223. package/skills/roles/architect.integration.md +4 -2
  224. package/skills/roles/architect.md +7 -5
  225. package/skills/roles/architect.platform.md +4 -2
  226. package/skills/roles/data-analyst.experiment.md +4 -2
  227. package/skills/roles/data-analyst.md +9 -7
  228. package/skills/roles/data-analyst.product-intelligence.md +4 -2
  229. package/skills/roles/data-analyst.product.md +4 -2
  230. package/skills/roles/data-analyst.telemetry.md +4 -2
  231. package/skills/roles/data-engineer.pipeline.md +4 -2
  232. package/skills/roles/data-engineer.vector-retrieval.md +4 -2
  233. package/skills/roles/data-engineer.warehouse.md +4 -2
  234. package/skills/roles/debugger.md +7 -5
  235. package/skills/roles/designer.accessibility.md +4 -2
  236. package/skills/roles/designer.md +10 -8
  237. package/skills/roles/engineer.ai.md +4 -2
  238. package/skills/roles/engineer.data.md +5 -3
  239. package/skills/roles/engineer.md +14 -12
  240. package/skills/roles/engineer.platform.md +5 -3
  241. package/skills/roles/operator.docs.md +6 -4
  242. package/skills/roles/operator.md +6 -4
  243. package/skills/roles/operator.release.md +4 -2
  244. package/skills/roles/operator.sre.md +5 -3
  245. package/skills/roles/orchestrator.md +5 -3
  246. package/skills/roles/product-manager.ai-product.md +4 -2
  247. package/skills/roles/product-manager.business-strategy.md +4 -2
  248. package/skills/roles/product-manager.enterprise.md +4 -2
  249. package/skills/roles/product-manager.growth.md +4 -2
  250. package/skills/roles/product-manager.md +6 -4
  251. package/skills/roles/product-manager.platform.md +4 -2
  252. package/skills/roles/product-manager.product.md +4 -2
  253. package/skills/roles/qa.ai-eval.md +4 -2
  254. package/skills/roles/qa.api-contract.md +4 -2
  255. package/skills/roles/qa.data-pipeline.md +4 -2
  256. package/skills/roles/qa.md +7 -5
  257. package/skills/roles/qa.test-automation.md +5 -3
  258. package/skills/roles/qa.web-ui.md +4 -2
  259. package/skills/roles/researcher.explorer.md +4 -2
  260. package/skills/roles/researcher.md +11 -9
  261. package/skills/roles/researcher.ux.md +4 -2
  262. package/skills/roles/reviewer.devil-advocate.md +4 -2
  263. package/skills/roles/reviewer.evaluator.md +4 -2
  264. package/skills/roles/reviewer.md +14 -12
  265. package/skills/roles/reviewer.trace.md +4 -2
  266. package/skills/roles/security.ai.md +4 -2
  267. package/skills/roles/security.appsec.md +4 -2
  268. package/skills/roles/security.cloud.md +4 -2
  269. package/skills/roles/security.legal-compliance.md +4 -2
  270. package/skills/roles/security.md +7 -5
  271. package/skills/roles/security.privacy.md +4 -2
  272. package/skills/roles/security.supply-chain.md +4 -2
  273. package/skills/routing.md +14 -14
  274. package/skills/security/blue-team.md +1 -1
  275. package/skills/security/code-audit.md +1 -1
  276. package/skills/security/pentest.md +1 -1
  277. package/skills/security/red-team.md +1 -1
  278. package/skills/security/threat-intel.md +1 -1
  279. package/skills/security/vuln-research.md +1 -1
  280. package/skills/utility/clean-code.md +2 -2
  281. package/templates/docs/changelog-entry.md +1 -1
  282. package/templates/docs/construct_guide.md +13 -13
  283. package/templates/docs/meta-prd.md +16 -16
  284. package/templates/docs/one-pager.md +1 -1
  285. package/templates/docs/persona-artifact.md +36 -0
  286. package/templates/docs/prd-business.md +1 -1
  287. package/templates/docs/prd-platform.md +1 -1
  288. package/templates/docs/prd.md +17 -17
  289. package/templates/docs/research-brief.md +8 -8
  290. package/templates/docs/research-finding.md +26 -0
  291. package/templates/docs/rfc.md +1 -1
  292. package/templates/docs/skill-artifact.md +27 -0
  293. package/templates/docs/strategy.md +1 -1
@@ -3,7 +3,7 @@
3
3
  * lib/mcp/server.mjs — Construct MCP server: tool registry and request dispatcher.
4
4
  *
5
5
  * Thin dispatcher only — all tool implementations live in lib/mcp/tools/*.mjs.
6
- * Registers 40 tools across 7 modules: project, document, storage, skills, workflow, telemetry, memory.
6
+ * Registers 52 tools across 8 modules: project, document, storage, skills, workflow, telemetry, memory, profile.
7
7
  * Consumed by Claude Code, OpenCode, and any MCP-compatible host.
8
8
  */
9
9
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -48,6 +48,12 @@ import {
48
48
  memorySearch, memoryAddObservations, memoryCreateEntities, memoryRecent,
49
49
  sessionList, sessionLoad, sessionSearch, sessionSave, rovoSearch,
50
50
  } from './tools/memory.mjs';
51
+ import {
52
+ profileShow, profileList, profileDrafts, profileHealthTool,
53
+ outcomesSummary, outcomesRecord, knowledgeAdd,
54
+ profileCreate, profileArchive, sandboxList, learningStatus,
55
+ knowledgeGraphAsk,
56
+ } from './tools/profile.mjs';
51
57
 
52
58
  const DEFAULT_ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
53
59
  const ROOT_DIR = resolve(process.env.CX_TOOLKIT_DIR || DEFAULT_ROOT_DIR);
@@ -711,6 +717,174 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
711
717
  },
712
718
  },
713
719
  },
720
+ {
721
+ name: 'profile_show',
722
+ description: 'Return the active Construct org profile (id, displayName, roles, departments, intake taxonomy, doc templates). Use when a specialist needs to know which role set, classification taxonomy, or doc templates apply before drafting work.',
723
+ inputSchema: {
724
+ type: 'object',
725
+ properties: {
726
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
727
+ id: { type: 'string', description: 'Force a specific profile id instead of resolving from config.' },
728
+ },
729
+ },
730
+ },
731
+ {
732
+ name: 'profile_list',
733
+ description: 'List the curated org profile catalog (rnd, operations, creative, research) with role/department counts. Use to discover which profiles are available before suggesting `construct profile set`.',
734
+ inputSchema: { type: 'object', properties: {} },
735
+ },
736
+ {
737
+ name: 'profile_drafts',
738
+ description: 'List in-progress draft profiles under `.cx/profiles/draft-*` and any user-defined custom profile at `.cx/profile.json`. Use to see what profile work is pending before scaffolding another draft.',
739
+ inputSchema: {
740
+ type: 'object',
741
+ properties: { cwd: { type: 'string', description: 'Project root (default: server cwd).' } },
742
+ },
743
+ },
744
+ {
745
+ name: 'profile_health',
746
+ description: 'Per-profile health rollup over a window: observation count, per-role outcome runs and success rates. Use to check whether a profile is producing data before recommending changes or archive.',
747
+ inputSchema: {
748
+ type: 'object',
749
+ properties: {
750
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
751
+ id: { type: 'string', description: 'Profile id (default: active profile).' },
752
+ window_days: { type: 'number', description: 'Window in days (default 30).' },
753
+ },
754
+ },
755
+ },
756
+ {
757
+ name: 'outcomes_summary',
758
+ description: 'Read `.cx/outcomes/_summary.json` (per-role success rate, 30-day trend). Pass `aggregate=true` to rebuild the summary from JSONL outcome files first. Use to ground tiebreakers and improvement suggestions in real specialist performance.',
759
+ inputSchema: {
760
+ type: 'object',
761
+ properties: {
762
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
763
+ aggregate: { type: 'boolean', description: 'Rebuild `_summary.json` before reading (default false).' },
764
+ },
765
+ },
766
+ },
767
+ {
768
+ name: 'outcomes_record',
769
+ description: 'Append a specialist outcome line to `.cx/outcomes/<role>.jsonl` (writes durable state — requires `confirm=true`). Use when a specialist wants to self-report success/failure outside the automatic agent-tracker path.',
770
+ inputSchema: {
771
+ type: 'object',
772
+ required: ['confirm', 'role', 'success'],
773
+ properties: {
774
+ confirm: { type: 'boolean', description: 'Must be true.' },
775
+ cwd: { type: 'string' },
776
+ role: { type: 'string', description: 'Specialist id (e.g. cx-engineer, product-manager).' },
777
+ success: { type: 'boolean' },
778
+ intake_id: { type: 'string' },
779
+ profile: { type: 'string', description: 'Override active profile id stamp.' },
780
+ escalated: { type: 'boolean' },
781
+ duration_ms: { type: 'number' },
782
+ notes: { type: 'string', description: 'Trimmed to 500 chars.' },
783
+ source: { type: 'string', description: 'Origin tag (default: "mcp").' },
784
+ },
785
+ },
786
+ },
787
+ {
788
+ name: 'knowledge_add',
789
+ description: 'Persist a research finding as `.cx/knowledge/external/research/<slug>.md` with research-specific frontmatter (topic, confidence, sources, expiresAt, profile). Writes durable state — requires `confirm=true`. `confidence=confirmed` requires at least one source.',
790
+ inputSchema: {
791
+ type: 'object',
792
+ required: ['confirm', 'slug', 'topic', 'body'],
793
+ properties: {
794
+ confirm: { type: 'boolean', description: 'Must be true.' },
795
+ cwd: { type: 'string' },
796
+ slug: { type: 'string', description: 'Lowercase hyphenated, max 60 chars.' },
797
+ topic: { type: 'string' },
798
+ body: { type: 'string', description: 'Findings / inferences / gaps / recommendation block. Capped at 50KB total file.' },
799
+ confidence: { type: 'string', enum: ['confirmed', 'inferred', 'weak'], description: 'Default: inferred.' },
800
+ sources: {
801
+ type: 'array',
802
+ description: 'Required when confidence=confirmed.',
803
+ items: {
804
+ type: 'object',
805
+ required: ['url'],
806
+ properties: {
807
+ url: { type: 'string' },
808
+ accessedAt: { type: 'string', description: 'ISO timestamp; default now.' },
809
+ span: { type: 'string', description: 'Citation locator; trimmed to 200 chars.' },
810
+ },
811
+ },
812
+ },
813
+ ttl_days: { type: 'number', description: 'Default 90.' },
814
+ },
815
+ },
816
+ },
817
+ {
818
+ name: 'profile_create',
819
+ description: 'Scaffold a draft org profile under `.cx/profiles/draft-<id>/` (requirements.md + profile.json + persona stubs + department charters). Writes durable state — requires `confirm=true`. For curated catalog work, follow `docs/concepts/profile-lifecycle.md` after creation.',
820
+ inputSchema: {
821
+ type: 'object',
822
+ required: ['confirm', 'id'],
823
+ properties: {
824
+ confirm: { type: 'boolean', description: 'Must be true.' },
825
+ cwd: { type: 'string' },
826
+ id: { type: 'string', description: 'Profile id (^[a-z][a-z0-9-]{1,30}$).' },
827
+ display_name: { type: 'string' },
828
+ seed_roles: {
829
+ type: 'array',
830
+ items: { type: 'string' },
831
+ description: 'Role ids to scaffold persona files for (cap 80).',
832
+ },
833
+ seed_departments: {
834
+ type: 'array',
835
+ items: {
836
+ type: 'object',
837
+ required: ['id'],
838
+ properties: {
839
+ id: { type: 'string' },
840
+ displayName: { type: 'string' },
841
+ },
842
+ },
843
+ description: 'Departments to scaffold charters for (cap 12).',
844
+ },
845
+ },
846
+ },
847
+ },
848
+ {
849
+ name: 'profile_archive',
850
+ description: 'Archive a curated profile: moves `profiles/<id>.json` and its intake table into `archive/profiles/<id>/` with an archive note. Destructive — requires `confirm=true` and a substantive `reason` (>=8 chars). Observations and outcomes are preserved.',
851
+ inputSchema: {
852
+ type: 'object',
853
+ required: ['confirm', 'id', 'reason'],
854
+ properties: {
855
+ confirm: { type: 'boolean', description: 'Must be true.' },
856
+ id: { type: 'string' },
857
+ reason: { type: 'string', description: 'Substantive reason (>= 8 chars).' },
858
+ },
859
+ },
860
+ },
861
+ {
862
+ name: 'sandbox_list',
863
+ description: 'List Construct sandboxes under `~/.cx/sandboxes/` (id, path, createdAt). Use to find an isolated environment for QA or dry-runs without polluting the active project.',
864
+ inputSchema: { type: 'object', properties: {} },
865
+ },
866
+ {
867
+ name: 'knowledge_graph_ask',
868
+ description: 'GraphRAG-style global query over the entity graph in `.cx/observations/`. Detects communities via label propagation, ranks them by BM25 against the query, and returns each top community with its central members and extractive summary. Use for "tell me about how X relates across the project" questions that pure semantic retrieval handles poorly.',
869
+ inputSchema: {
870
+ type: 'object',
871
+ required: ['query'],
872
+ properties: {
873
+ query: { type: 'string', description: 'Natural-language question.' },
874
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
875
+ top_k: { type: 'number', description: 'Max communities to return (default 5).' },
876
+ min_size: { type: 'number', description: 'Skip communities smaller than this (default 2).' },
877
+ },
878
+ },
879
+ },
880
+ {
881
+ name: 'learning_status',
882
+ description: 'One-shot mirror of `npm run learning:status`: active profile, observation counts (last 24h + total), research finding count, per-role outcome rollup. Use to answer "is Construct learning?" without spawning a shell.',
883
+ inputSchema: {
884
+ type: 'object',
885
+ properties: { cwd: { type: 'string', description: 'Project root (default: server cwd).' } },
886
+ },
887
+ },
714
888
  {
715
889
  name: 'knowledge_search',
716
890
  description: 'Search Construct\'s own documentation, knowledge base, and distilled embed observations. Call this immediately — no approval needed — when the user asks what Construct is, how a feature works, what commands exist, or anything about Construct\'s architecture or configuration. Also searches embed observations from GitHub, Jira, and other configured sources. Returns relevant excerpts with source file and heading.',
@@ -786,6 +960,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
786
960
  const { knowledgeSearch } = await import('../knowledge/search.mjs');
787
961
  result = knowledgeSearch({ query: args.query, topK: args.top_k, repoRoot: args.repo_root, rootDir: args.root_dir });
788
962
  }
963
+ else if (name === 'profile_show') result = profileShow(args);
964
+ else if (name === 'profile_list') result = profileList();
965
+ else if (name === 'profile_drafts') result = profileDrafts(args);
966
+ else if (name === 'profile_health') result = profileHealthTool(args);
967
+ else if (name === 'outcomes_summary') result = outcomesSummary(args);
968
+ else if (name === 'outcomes_record') result = outcomesRecord(args);
969
+ else if (name === 'knowledge_add') result = await knowledgeAdd(args);
970
+ else if (name === 'profile_create') result = profileCreate(args);
971
+ else if (name === 'profile_archive') result = profileArchive(args);
972
+ else if (name === 'sandbox_list') result = sandboxList();
973
+ else if (name === 'learning_status') result = learningStatus(args);
974
+ else if (name === 'knowledge_graph_ask') result = knowledgeGraphAsk(args);
789
975
  else result = { error: `Unknown tool: ${name}` };
790
976
  } catch (err) {
791
977
  result = { error: err.message ?? String(err) };
@@ -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
+ }
@@ -58,6 +58,20 @@ function generateId() {
58
58
  return `obs-${ts}-${rand}`;
59
59
  }
60
60
 
61
+ const MAX_EXTRAS_BYTES = 2 * 1024;
62
+
63
+ function sanitizeExtras(extras) {
64
+ if (extras == null) return null;
65
+ if (typeof extras !== 'object' || Array.isArray(extras)) return null;
66
+ try {
67
+ const serialized = JSON.stringify(extras);
68
+ if (Buffer.byteLength(serialized, 'utf8') > MAX_EXTRAS_BYTES) return null;
69
+ return JSON.parse(serialized);
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
61
75
  function clamp(str, max) {
62
76
  if (!str || str.length <= max) return str || null;
63
77
  return str.slice(0, max - 1) + '\u2026';
@@ -131,6 +145,7 @@ export async function addObservation(rootDir, {
131
145
  confidence = 0.8,
132
146
  source = null,
133
147
  gitSha = null,
148
+ extras = null,
134
149
  } = {}) {
135
150
  const id = generateId();
136
151
  const now = new Date().toISOString();
@@ -139,6 +154,9 @@ export async function addObservation(rootDir, {
139
154
  const clampedSummary = clamp(String(summary), MAX_SUMMARY);
140
155
  const clampedContent = clamp(String(content), MAX_CONTENT);
141
156
  const clampedTags = (Array.isArray(tags) ? tags : []).slice(0, MAX_TAGS).map(String);
157
+ // Structured metadata: opt-in. Capped at 2 KB stringified so a misuse can't
158
+ // bloat the per-id JSON. Plain object only; rejects arrays and functions.
159
+ const clampedExtras = sanitizeExtras(extras);
142
160
 
143
161
  const record = {
144
162
  id,
@@ -151,6 +169,7 @@ export async function addObservation(rootDir, {
151
169
  confidence: Math.max(0, Math.min(1, Number(confidence) || 0.8)),
152
170
  source: source || null,
153
171
  gitSha: gitSha ? String(gitSha).slice(0, 40) : null,
172
+ extras: clampedExtras,
154
173
  createdAt: now,
155
174
  updatedAt: now,
156
175
  };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * lib/outcomes/aggregate.mjs — Per-role rolling success-rate summary.
3
+ *
4
+ * Reads `.cx/outcomes/<role>.jsonl` files and produces `_summary.json`:
5
+ *
6
+ * { generatedAt, roles: { <role>: { count, success, successRate,
7
+ * p50DurationMs, last30: { count, successRate } } } }
8
+ *
9
+ * The classifier reads `successRate` to apply a tie-breaker soft re-rank
10
+ * (capped at ±0.05 in lib/intake/classify.mjs so it cannot invert the
11
+ * primary keyword signal).
12
+ */
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { listOutcomes } from './record.mjs';
16
+
17
+ const DAY_MS = 24 * 60 * 60 * 1000;
18
+ const LAST_WINDOW_DAYS = 30;
19
+
20
+ function median(nums) {
21
+ if (nums.length === 0) return null;
22
+ const sorted = [...nums].sort((a, b) => a - b);
23
+ const mid = Math.floor(sorted.length / 2);
24
+ return sorted.length % 2 === 0 ? Math.round((sorted[mid - 1] + sorted[mid]) / 2) : sorted[mid];
25
+ }
26
+
27
+ function rollupRole(outcomes) {
28
+ const total = outcomes.length;
29
+ if (total === 0) return null;
30
+ const success = outcomes.filter((o) => o.success === true).length;
31
+ const durations = outcomes
32
+ .map((o) => o.durationMs)
33
+ .filter((d) => Number.isFinite(d) && d > 0);
34
+
35
+ const cutoff = Date.now() - LAST_WINDOW_DAYS * DAY_MS;
36
+ const recent = outcomes.filter((o) => Date.parse(o.ts) >= cutoff);
37
+ const recentSuccess = recent.filter((o) => o.success === true).length;
38
+
39
+ return {
40
+ count: total,
41
+ success,
42
+ successRate: total > 0 ? Number((success / total).toFixed(3)) : 0,
43
+ p50DurationMs: median(durations),
44
+ last30: {
45
+ count: recent.length,
46
+ successRate: recent.length > 0 ? Number((recentSuccess / recent.length).toFixed(3)) : 0,
47
+ },
48
+ };
49
+ }
50
+
51
+ /**
52
+ * List every role with outcome data in this project. Returns role ids.
53
+ */
54
+ export function listRolesWithOutcomes(cwd) {
55
+ const dir = path.join(cwd, '.cx', 'outcomes');
56
+ if (!fs.existsSync(dir)) return [];
57
+ const seen = new Set();
58
+ for (const f of fs.readdirSync(dir)) {
59
+ const m = /^([a-z0-9-]+)(\.\d+)?\.jsonl$/.exec(f);
60
+ if (m) seen.add(m[1]);
61
+ }
62
+ return Array.from(seen).sort();
63
+ }
64
+
65
+ /**
66
+ * Rebuild the summary from all per-role JSONL files. Idempotent.
67
+ */
68
+ export function aggregateOutcomes(cwd) {
69
+ const roles = listRolesWithOutcomes(cwd);
70
+ const out = { generatedAt: new Date().toISOString(), roles: {} };
71
+ for (const role of roles) {
72
+ const rollup = rollupRole(listOutcomes(cwd, role));
73
+ if (rollup) out.roles[role] = rollup;
74
+ }
75
+ const dir = path.join(cwd, '.cx', 'outcomes');
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ fs.writeFileSync(path.join(dir, '_summary.json'), JSON.stringify(out, null, 2) + '\n');
78
+ return out;
79
+ }
80
+
81
+ /**
82
+ * Read the cached summary without recomputing. Keeps the classifier
83
+ * tiebreaker on an O(1) hot path.
84
+ */
85
+ export function readSummary(cwd) {
86
+ const p = path.join(cwd, '.cx', 'outcomes', '_summary.json');
87
+ if (!fs.existsSync(p)) return null;
88
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
89
+ }
90
+
91
+ /**
92
+ * Tiebreaker score for the classifier. Returns a number in [-0.05, 0.05]
93
+ * derived from the role's recent success rate. Default behavior on missing
94
+ * data is 0 (no influence).
95
+ */
96
+ export function outcomeBoost(cwd, role, { cap = 0.05 } = {}) {
97
+ const summary = readSummary(cwd);
98
+ const stats = summary?.roles?.[role];
99
+ if (!stats || !stats.last30 || stats.last30.count < 3) return 0;
100
+ // Map [0.5, 1.0] success → [0, cap]; [0, 0.5] → [-cap, 0]. Linear, capped.
101
+ const rate = stats.last30.successRate;
102
+ const centered = (rate - 0.5) * 2; // [-1, 1]
103
+ return Math.max(-cap, Math.min(cap, centered * cap));
104
+ }