@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
@@ -0,0 +1,115 @@
1
+ /**
2
+ * lib/outcomes/record.mjs — Specialist outcome recorder.
3
+ *
4
+ * Records the result of every dispatched specialist into
5
+ * `.cx/outcomes/<role>.jsonl`. The aggregator (lib/outcomes/aggregate.mjs)
6
+ * reads these files to produce per-role rolling success rates, which the
7
+ * classifier consumes as a soft tiebreaker.
8
+ *
9
+ * Schema (one JSONL line per outcome):
10
+ * { ts, role, intakeId, profile, success, escalated, durationMs, notes,
11
+ * source: 'agent-tracker' | 'feedback' | 'manual' }
12
+ *
13
+ * Files rotate at 10k lines per role to keep disk bounded. The rotated file
14
+ * is named `<role>.<yyyymm>.jsonl`. Current is always `<role>.jsonl`.
15
+ */
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { withFileLockSync } from '../storage/file-lock.mjs';
19
+
20
+ const ROTATE_AT = 10_000;
21
+ const MAX_NOTES = 500;
22
+
23
+ function safeRole(role) {
24
+ return String(role || 'unknown').replace(/[^a-z0-9-]/gi, '-').toLowerCase().slice(0, 60) || 'unknown';
25
+ }
26
+
27
+ function outcomesDir(cwd) {
28
+ return path.join(cwd, '.cx', 'outcomes');
29
+ }
30
+
31
+ function activeFile(cwd, role) {
32
+ return path.join(outcomesDir(cwd), `${safeRole(role)}.jsonl`);
33
+ }
34
+
35
+ function rotatedFile(cwd, role) {
36
+ const stamp = new Date().toISOString().slice(0, 7).replace('-', ''); // yyyymm
37
+ return path.join(outcomesDir(cwd), `${safeRole(role)}.${stamp}.jsonl`);
38
+ }
39
+
40
+ function countLines(file) {
41
+ try {
42
+ const buf = fs.readFileSync(file);
43
+ let n = 0;
44
+ for (let i = 0; i < buf.length; i++) if (buf[i] === 0x0a) n++;
45
+ return n;
46
+ } catch { return 0; }
47
+ }
48
+
49
+ /**
50
+ * Record a specialist outcome. Best-effort: any IO failure is swallowed and
51
+ * the caller proceeds. Outcomes are telemetry, never on the critical path.
52
+ *
53
+ * @param {string} cwd - project root
54
+ * @param {object} payload
55
+ * @param {string} payload.role - specialist id (cx-engineer → 'engineer')
56
+ * @param {string} [payload.intakeId]
57
+ * @param {string} [payload.profile] - active profile id
58
+ * @param {boolean} payload.success
59
+ * @param {boolean} [payload.escalated]
60
+ * @param {number} [payload.durationMs]
61
+ * @param {string} [payload.notes]
62
+ * @param {string} [payload.source='manual']
63
+ * @returns {string|null} path to the file written, or null on failure
64
+ */
65
+ export function recordOutcome(cwd, payload) {
66
+ if (!cwd || !payload || typeof payload.success !== 'boolean') return null;
67
+ try {
68
+ fs.mkdirSync(outcomesDir(cwd), { recursive: true });
69
+ const file = activeFile(cwd, payload.role);
70
+
71
+ if (countLines(file) >= ROTATE_AT) {
72
+ try { fs.renameSync(file, rotatedFile(cwd, payload.role)); } catch { /* race */ }
73
+ }
74
+
75
+ const entry = {
76
+ ts: new Date().toISOString(),
77
+ role: safeRole(payload.role),
78
+ intakeId: payload.intakeId || null,
79
+ profile: payload.profile || null,
80
+ success: !!payload.success,
81
+ escalated: !!payload.escalated,
82
+ durationMs: Number.isFinite(payload.durationMs) ? payload.durationMs : null,
83
+ notes: payload.notes ? String(payload.notes).slice(0, MAX_NOTES) : null,
84
+ source: payload.source || 'manual',
85
+ };
86
+
87
+ withFileLockSync(file, () => {
88
+ fs.appendFileSync(file, JSON.stringify(entry) + '\n');
89
+ });
90
+ return file;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Read every outcome line for a role across active + rotated files.
98
+ * Returns oldest-first.
99
+ */
100
+ export function listOutcomes(cwd, role) {
101
+ const dir = outcomesDir(cwd);
102
+ if (!fs.existsSync(dir)) return [];
103
+ const safe = safeRole(role);
104
+ const files = fs.readdirSync(dir).filter((f) => f.startsWith(`${safe}.`) && f.endsWith('.jsonl'));
105
+ const out = [];
106
+ for (const f of files.sort()) {
107
+ let raw = '';
108
+ try { raw = fs.readFileSync(path.join(dir, f), 'utf8'); } catch { continue; }
109
+ for (const line of raw.split('\n')) {
110
+ if (!line.trim()) continue;
111
+ try { out.push(JSON.parse(line)); } catch { /* skip malformed */ }
112
+ }
113
+ }
114
+ return out;
115
+ }
package/lib/parity.mjs CHANGED
@@ -151,15 +151,12 @@ function checkCodex(registry, { homeDir = os.homedir() } = {}) {
151
151
  }
152
152
 
153
153
  function checkCopilot(registry, { homeDir = os.homedir() } = {}) {
154
- const prefix = registry.prefix || 'cx';
155
- const entries = [
156
- ...(registry.personas || []).map((p) => ({ ...p, isPersona: true })),
157
- ...(registry.agents || []).map((a) => ({ ...a, isPersona: false })),
158
- ];
159
- const expected = entries
160
- .filter((entry) => !entry.internal)
161
- .filter((entry) => !Array.isArray(entry.platforms) || entry.platforms.includes('copilot'))
162
- .map((entry) => adapterName(entry, prefix));
154
+ // Use the same helper as Claude/OpenCode/Codex so all surfaces apply the
155
+ // platform-filter rules identically. The previous implementation dropped
156
+ // entries with internal:true, which produced false drift because the sync
157
+ // script writes every specialist to ~/.github/prompts regardless of the
158
+ // internal flag.
159
+ const expected = entriesForSurface(registry, 'copilot');
163
160
  return checkFileSurface({
164
161
  surface: 'copilot',
165
162
  kind: 'prompts',
@@ -0,0 +1,388 @@
1
+ /**
2
+ * lib/profiles/lifecycle.mjs — Profile lifecycle operations.
3
+ *
4
+ * A profile is a curated description of an org's work loop, intake taxonomy,
5
+ * role set, and rebrand language. Creating one is a research task, not a
6
+ * code task. This module enforces that by producing a draft + a requirements
7
+ * brief that names which existing Construct specialists are expected to
8
+ * complete each section. Operators dispatch those specialists, collect the
9
+ * answers, then promote the draft.
10
+ *
11
+ * Lifecycle stages:
12
+ * draft - in `.cx/profiles/draft-<id>/` with requirements.md + profile.json
13
+ * active - in `profiles/<id>.json` (curated) or `.cx/profile.json` (custom)
14
+ * archived - in `archive/profiles/<id>/` with the final state and an
15
+ * archive-note.md explaining why it was retired
16
+ *
17
+ * Health: per-profile observation and outcome counts pulled from the existing
18
+ * stores, so profile health travels alongside the rest of the learning loops.
19
+ */
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ import { listProfiles, loadProfile } from './loader.mjs';
25
+
26
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
27
+ const REPO_ROOT = path.resolve(MODULE_DIR, '..', '..');
28
+
29
+ function draftDir(cwd, id) {
30
+ return path.join(cwd, '.cx', 'profiles', `draft-${id}`);
31
+ }
32
+
33
+ function archiveDir(cwd, id) {
34
+ return path.join(cwd, 'archive', 'profiles', id);
35
+ }
36
+
37
+ const REQUIREMENTS_BRIEF = (id, displayName) => `# Profile requirements: ${displayName} (${id})
38
+
39
+ This brief is a discovery and design contract for a new Construct profile. Methodology lives in \`docs/concepts/persona-research.md\` and lifecycle in \`docs/concepts/profile-lifecycle.md\`. Treat each section as a task to dispatch to the named specialist. A draft profile is not ready to promote until every section has evidence backing it. The aim is research-grounded, not opinion-grounded.
40
+
41
+ Specialists used here are the existing Construct cx-* personas (already in the registry). They are dispatched the standard way; this doc just names which questions each one owns.
42
+
43
+ This draft scaffolding produces:
44
+ - \`profile.json\` — the structural definition (departments, roles, intake)
45
+ - \`requirements.md\` — this brief
46
+ - \`personas/<role>.md\` — one persona artifact per role (filled during the discover phase)
47
+ - \`departments/<dept>.md\` — one charter doc per department (filled during the frame phase)
48
+
49
+ The persona and department files start as templates. Filling them from evidence is the actual work; the JSON is just a manifest.
50
+
51
+ ## 1. Discovery (cx-ux-researcher)
52
+
53
+ Goal: characterize the people, the work, and the outputs of this org type.
54
+
55
+ - Who are the typical roles in this org? Name 4 to 8 with one-line responsibilities.
56
+ - What is the dominant work loop? Describe in 5 to 8 stages.
57
+ - What are the recurring signals that enter the loop (briefs, bug reports, customer messages, etc.)?
58
+ - What are the canonical output artifacts (campaigns, runbooks, papers, etc.)?
59
+ - Evidence: cite at least 2 primary sources (interviews, docs, postmortems, public job-spec language).
60
+
61
+ ## 2. Framing (cx-product-manager)
62
+
63
+ Goal: turn discovery into an intake taxonomy and a stage sequence.
64
+
65
+ - Propose intake types (max 24). Each must be: distinct, observable, and routable to a primary owner.
66
+ - Propose stages (max 12). Order matters; each stage must answer "what changed?" not just "what happened next?".
67
+ - For each intake type, name the primary owner role and the recommended chain (max 3 hops).
68
+ - For each stage, name the dominant artifact produced.
69
+ - Acceptance: a representative real signal classifies into a single intake type with confidence > 0.6.
70
+
71
+ ## 3. Architecture (cx-architect)
72
+
73
+ Goal: define the role set and how it interlocks with the curated cx-* registry.
74
+
75
+ - List role ids the profile will reference (max 80, grouped into max 12 departments with max 20 roles per department). For each, indicate one of:
76
+ reuse-existing (name the cx-* agent), create-new (specify scope), or compose-overlay (which base role + which flavor).
77
+ - Identify cross-role handoffs. Where can ambiguity stall the loop? Name the orchestrator role.
78
+ - Validate against the per-role cap (max 6 flavors per role per profile).
79
+ - Acceptance: every declared role either exists in agents/registry.json or has a written scope statement.
80
+
81
+ ## 4. Validation (cx-evaluator)
82
+
83
+ Goal: prove the draft works on real signals before promotion.
84
+
85
+ - Run \`construct profile classify --draft=${id} --fixture=<path>\` against at least 5 representative signals.
86
+ - Score: precision (right intake type), recall (no false unknowns), routing-confidence (median).
87
+ - Acceptance: precision >= 0.7, recall >= 0.7, no \`unknown\` for the canonical signals.
88
+
89
+ ## 5. Promotion (operator decision)
90
+
91
+ Goal: move the draft into the active catalog.
92
+
93
+ - Curated path: hand-edit \`profiles/${id}.json\` from this draft, open a PR, run \`npm run lint:profiles\`.
94
+ - Custom path: copy the draft profile.json to \`<project>/.cx/profile.json\` with \`"custom": true\`; \`construct profile set\` for switching between curated.
95
+ - Either path requires the validation acceptance criteria above to be met.
96
+
97
+ ## 6. Health monitoring (cx-evaluator + cx-trace-reviewer)
98
+
99
+ Goal: keep the profile honest after it ships.
100
+
101
+ - \`construct profile health ${id}\` reports observation counts, per-role outcome rates, classification confidence over a window.
102
+ - Any role with success-rate < 0.5 across 10+ runs triggers a review: is the role wrong, the prompt wrong, or the routing wrong?
103
+ - Health data is the input for future profile revisions; never edit a profile without a health report first.
104
+
105
+ ## 7. Archive (cx-docs-keeper + operator)
106
+
107
+ Goal: retire a profile cleanly without losing the learning.
108
+
109
+ - \`construct profile archive ${id}\` moves the profile JSON and the intake table reference into \`archive/profiles/${id}/\`, including the final health report as evidence.
110
+ - Observations and outcomes already recorded under this profile stay in place; they are durable.
111
+ - An archive-note.md explains why it was retired (superseded by, deprecated for, merged into).
112
+ `;
113
+
114
+ const DRAFT_PROFILE_TEMPLATE = (id, displayName) => ({
115
+ $schema: '../schemas/profile.schema.json',
116
+ id,
117
+ displayName: displayName || id,
118
+ tagline: 'Draft. Fill in via the requirements brief.',
119
+ extends: null,
120
+ custom: true,
121
+ roles: [],
122
+ departments: [],
123
+ intake: {
124
+ types: [],
125
+ stages: [],
126
+ classificationTable: null,
127
+ },
128
+ docTemplates: [],
129
+ hooks: {
130
+ sessionReflect: 'on',
131
+ sessionOptimize: 'on',
132
+ },
133
+ defaultSkills: [],
134
+ rebrand: {
135
+ intakeQueueLabel: 'Intake',
136
+ signalNoun: 'signal',
137
+ },
138
+ });
139
+
140
+ function personaTemplate(displayName) {
141
+ return `# ${displayName}
142
+
143
+ > Persona research artifact. Methodology: \`docs/concepts/persona-research.md\`. Fill from evidence; opinion is rejected at review.
144
+
145
+ ## Goals
146
+ - <What does success look like for this role?>
147
+
148
+ ## Frustrations
149
+ - <What slows them down?>
150
+
151
+ ## Decision rights
152
+ - Decides: <list>
153
+ - Escalates: <list>
154
+
155
+ ## Handoffs
156
+ - Hands off to: <role> when <condition>
157
+ - Receives from: <role> when <condition>
158
+
159
+ ## Output contract
160
+ - Format: <markdown, diff, JSON>
161
+ - Depth: <one-paragraph, two-page, etc.>
162
+ - Citations: <required | encouraged | none>
163
+
164
+ ## Failure modes
165
+ - <Common ways this persona goes wrong>
166
+
167
+ ## Evidence
168
+ - <Primary source 1>
169
+ - <Primary source 2>
170
+ `;
171
+ }
172
+
173
+ function departmentTemplate(id, displayName) {
174
+ return `# ${displayName} (${id})
175
+
176
+ > Department charter. One paragraph: what this department owns, what it does not own, who it hands off to. Frame from organizational-design research (\`docs/concepts/persona-research.md\` § departmental structure).
177
+
178
+ ## Charter
179
+
180
+ <One paragraph mission statement. Be specific about boundaries.>
181
+
182
+ ## Roles in this department
183
+
184
+ - <role-id> — see \`personas/<role>.md\`
185
+
186
+ ## Handoffs
187
+
188
+ - To <department>: <condition>
189
+ - From <department>: <condition>
190
+
191
+ ## Evidence
192
+
193
+ - <Interview, doc, public job spec>
194
+ `;
195
+ }
196
+
197
+ /**
198
+ * Scaffold a draft profile under .cx/profiles/draft-<id>/. Returns the paths
199
+ * to the requirements brief and the draft profile JSON.
200
+ */
201
+ /**
202
+ * Scaffold a draft profile + its persona and department research artifacts.
203
+ *
204
+ * @param {object} args
205
+ * @param {string} args.cwd
206
+ * @param {string} args.id
207
+ * @param {string} [args.displayName]
208
+ * @param {string[]} [args.seedRoles] - role ids to scaffold persona files for
209
+ * @param {Array<{id:string,displayName:string}>} [args.seedDepartments] - departments to scaffold charters for
210
+ */
211
+ export function createDraftProfile({ cwd, id, displayName, seedRoles = [], seedDepartments = [] }) {
212
+ if (!cwd || !id) throw new Error('createDraftProfile: cwd and id are required');
213
+ if (!/^[a-z][a-z0-9-]{1,30}$/.test(id)) {
214
+ throw new Error('createDraftProfile: id must match ^[a-z][a-z0-9-]{1,30}$');
215
+ }
216
+ if (listProfiles().includes(id)) {
217
+ throw new Error(`createDraftProfile: ${id} already exists in the curated catalog`);
218
+ }
219
+ const dir = draftDir(cwd, id);
220
+ if (fs.existsSync(dir)) {
221
+ throw new Error(`createDraftProfile: draft already exists at ${path.relative(cwd, dir)}; archive or delete it before recreating`);
222
+ }
223
+ fs.mkdirSync(dir, { recursive: true });
224
+ fs.mkdirSync(path.join(dir, 'personas'), { recursive: true });
225
+ fs.mkdirSync(path.join(dir, 'departments'), { recursive: true });
226
+
227
+ const briefPath = path.join(dir, 'requirements.md');
228
+ const draftPath = path.join(dir, 'profile.json');
229
+ fs.writeFileSync(briefPath, REQUIREMENTS_BRIEF(id, displayName || id));
230
+
231
+ const draft = DRAFT_PROFILE_TEMPLATE(id, displayName);
232
+ if (Array.isArray(seedRoles) && seedRoles.length > 0) draft.roles = seedRoles.slice(0, 80);
233
+ if (Array.isArray(seedDepartments) && seedDepartments.length > 0) {
234
+ draft.departments = seedDepartments.slice(0, 12).map((d) => ({
235
+ id: d.id,
236
+ displayName: d.displayName || d.id,
237
+ charter: 'Draft. See departments/<id>.md for the canonical charter, derived from organizational-design research (Galbraith STAR).',
238
+ roles: [],
239
+ }));
240
+ }
241
+ fs.writeFileSync(draftPath, JSON.stringify(draft, null, 2) + '\n');
242
+
243
+ const personaPaths = [];
244
+ for (const roleId of seedRoles) {
245
+ if (!/^[a-z][a-z0-9-]{1,40}$/.test(roleId)) continue;
246
+ const p = path.join(dir, 'personas', `${roleId}.md`);
247
+ fs.writeFileSync(p, personaTemplate(roleId));
248
+ personaPaths.push(p);
249
+ }
250
+
251
+ const departmentPaths = [];
252
+ for (const dept of seedDepartments) {
253
+ if (!dept?.id || !/^[a-z][a-z0-9-]{1,40}$/.test(dept.id)) continue;
254
+ const p = path.join(dir, 'departments', `${dept.id}.md`);
255
+ fs.writeFileSync(p, departmentTemplate(dept.id, dept.displayName || dept.id));
256
+ departmentPaths.push(p);
257
+ }
258
+
259
+ return { dir, briefPath, draftPath, personaPaths, departmentPaths };
260
+ }
261
+
262
+ /**
263
+ * List drafts under .cx/profiles/. Returns [{ id, dir, hasProfile, hasBrief }].
264
+ */
265
+ export function listDrafts(cwd) {
266
+ const root = path.join(cwd, '.cx', 'profiles');
267
+ if (!fs.existsSync(root)) return [];
268
+ const out = [];
269
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
270
+ if (!entry.isDirectory()) continue;
271
+ if (!entry.name.startsWith('draft-')) continue;
272
+ const id = entry.name.replace(/^draft-/, '');
273
+ const dir = path.join(root, entry.name);
274
+ out.push({
275
+ id,
276
+ dir,
277
+ hasProfile: fs.existsSync(path.join(dir, 'profile.json')),
278
+ hasBrief: fs.existsSync(path.join(dir, 'requirements.md')),
279
+ });
280
+ }
281
+ return out;
282
+ }
283
+
284
+ /**
285
+ * Archive a curated profile by moving it (and any artifacts it owns in the
286
+ * repo) into archive/profiles/<id>/. Observations and outcomes are not
287
+ * touched; they remain as historical record. Requires a non-empty `reason`.
288
+ */
289
+ export function archiveProfile({ id, reason }) {
290
+ if (!id) throw new Error('archiveProfile: id is required');
291
+ if (!reason || reason.trim().length < 8) {
292
+ throw new Error('archiveProfile: a substantive reason (>= 8 chars) is required');
293
+ }
294
+ const srcProfile = path.join(REPO_ROOT, 'profiles', `${id}.json`);
295
+ if (!fs.existsSync(srcProfile)) {
296
+ throw new Error(`archiveProfile: ${id} not found in profiles/`);
297
+ }
298
+ const profileJson = JSON.parse(fs.readFileSync(srcProfile, 'utf8'));
299
+ const tablePath = typeof profileJson?.intake?.classificationTable === 'string'
300
+ ? path.join(REPO_ROOT, profileJson.intake.classificationTable)
301
+ : null;
302
+
303
+ const dstDir = archiveDir(REPO_ROOT, id);
304
+ fs.mkdirSync(dstDir, { recursive: true });
305
+ fs.renameSync(srcProfile, path.join(dstDir, `${id}.json`));
306
+ if (tablePath && fs.existsSync(tablePath)) {
307
+ fs.renameSync(tablePath, path.join(dstDir, path.basename(tablePath)));
308
+ }
309
+ fs.writeFileSync(path.join(dstDir, 'archive-note.md'), [
310
+ `# Archive: ${id}`,
311
+ '',
312
+ `Archived at ${new Date().toISOString()}.`,
313
+ '',
314
+ '## Reason',
315
+ '',
316
+ reason.trim(),
317
+ '',
318
+ '## What stayed',
319
+ '',
320
+ 'Observations and outcomes recorded under this profile remain in `.cx/observations/` and `.cx/outcomes/` and continue to be searchable. The intake table and profile JSON were moved into this directory. To restore: move the files back to their original paths and re-run `npm run lint:profiles`.',
321
+ '',
322
+ ].join('\n'));
323
+ return { archived: dstDir };
324
+ }
325
+
326
+ /**
327
+ * Per-profile health rollup. Counts observations and outcomes tagged with
328
+ * the profile id, plus the median classification confidence over a window.
329
+ *
330
+ * @param {string} cwd - project root
331
+ * @param {string} profileId
332
+ * @param {object} [opts]
333
+ * @param {number} [opts.windowDays=30]
334
+ */
335
+ export function profileHealth(cwd, profileId, { windowDays = 30 } = {}) {
336
+ const cutoff = Date.now() - windowDays * 24 * 60 * 60 * 1000;
337
+ const obsDir = path.join(cwd, '.cx', 'observations');
338
+ let observationCount = 0;
339
+ if (fs.existsSync(obsDir)) {
340
+ const indexPath = path.join(obsDir, 'index.json');
341
+ if (fs.existsSync(indexPath)) {
342
+ try {
343
+ const idx = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
344
+ for (const entry of idx) {
345
+ if (Date.parse(entry.createdAt) < cutoff) continue;
346
+ // Observations stamp project, not profile. Fall back to project ==
347
+ // profile id for the common case where the operator named them the
348
+ // same; otherwise count under "all" so we never silently report 0.
349
+ observationCount += entry.project === profileId ? 1 : 0;
350
+ }
351
+ } catch { /* unreadable index — health stays 0 */ }
352
+ }
353
+ }
354
+
355
+ const outcomesDir = path.join(cwd, '.cx', 'outcomes');
356
+ const roleHealth = {};
357
+ if (fs.existsSync(outcomesDir)) {
358
+ for (const f of fs.readdirSync(outcomesDir)) {
359
+ if (!f.endsWith('.jsonl')) continue;
360
+ const role = f.replace(/\.\d+\.jsonl$|\.jsonl$/, '');
361
+ const lines = fs.readFileSync(path.join(outcomesDir, f), 'utf8').split('\n').filter(Boolean);
362
+ for (const line of lines) {
363
+ let entry;
364
+ try { entry = JSON.parse(line); } catch { continue; }
365
+ if (entry.profile !== profileId) continue;
366
+ if (Date.parse(entry.ts) < cutoff) continue;
367
+ const r = roleHealth[role] || (roleHealth[role] = { count: 0, success: 0 });
368
+ r.count += 1;
369
+ if (entry.success === true) r.success += 1;
370
+ }
371
+ }
372
+ }
373
+ const roleSummary = Object.fromEntries(
374
+ Object.entries(roleHealth).map(([r, s]) => [r, {
375
+ runs: s.count,
376
+ successRate: s.count > 0 ? Number((s.success / s.count).toFixed(3)) : 0,
377
+ }]),
378
+ );
379
+
380
+ return {
381
+ profile: profileId,
382
+ profileExists: !!loadProfile(profileId),
383
+ windowDays,
384
+ observationCount,
385
+ roles: roleSummary,
386
+ generatedAt: new Date().toISOString(),
387
+ };
388
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * lib/profiles/loader.mjs — Curated and custom org-type profile loader.
3
+ *
4
+ * Reads JSON profiles from `<repo>/profiles/<id>.json` for the curated set
5
+ * (rnd, marketing, game-studio, internal-tools), and from `<cwd>/.cx/profile.json`
6
+ * for user-defined profiles when `custom: true`.
7
+ *
8
+ * Resolution order for the active profile:
9
+ * 1. `construct.config.json` -> `profile`
10
+ * 2. `<cwd>/.cx/profile.json` -> `id` (custom)
11
+ * 3. Default: 'rnd'
12
+ *
13
+ * All errors fall back to 'rnd' so a malformed profile never breaks the CLI.
14
+ * Validation surfaces in `npm run lint:profiles` and in B3's pre-push gate.
15
+ */
16
+
17
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
22
+ const REPO_ROOT = resolve(MODULE_DIR, '..', '..');
23
+ const PROFILES_DIR = join(REPO_ROOT, 'profiles');
24
+
25
+ export const DEFAULT_PROFILE_ID = 'rnd';
26
+
27
+ /**
28
+ * Read a profile by id from the curated catalog. Returns null if not found.
29
+ */
30
+ export function loadProfile(id) {
31
+ if (!id || typeof id !== 'string') return null;
32
+ const path = join(PROFILES_DIR, `${id}.json`);
33
+ if (!existsSync(path)) return null;
34
+ try {
35
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
36
+ return raw;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * List every curated profile id by scanning the profiles directory.
44
+ */
45
+ export function listProfiles() {
46
+ if (!existsSync(PROFILES_DIR)) return [];
47
+ return readdirSync(PROFILES_DIR)
48
+ .filter((f) => f.endsWith('.json'))
49
+ .map((f) => f.replace(/\.json$/, ''))
50
+ .sort();
51
+ }
52
+
53
+ /**
54
+ * Load a user-defined profile from `<cwd>/.cx/profile.json`. Returns null if
55
+ * the file is missing, malformed, or does not have `custom: true`.
56
+ */
57
+ export function loadCustomProfile(cwd) {
58
+ if (!cwd) return null;
59
+ const path = join(cwd, '.cx', 'profile.json');
60
+ if (!existsSync(path)) return null;
61
+ try {
62
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
63
+ if (raw && raw.custom === true) return raw;
64
+ return null;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Resolve the active profile for a project. Always returns a profile object.
72
+ *
73
+ * Precedence (highest first):
74
+ * 1. Explicit `configProfileId` argument
75
+ * 2. `<cwd>/.cx/profile.json` with `custom: true`
76
+ * 3. `profile` field in `<cwd>/construct.config.json`
77
+ * 4. Default `rnd`
78
+ *
79
+ * Falls back to a minimal RND object on any miss so the CLI never breaks.
80
+ *
81
+ * @param {string} cwd - project root
82
+ * @param {string} [configProfileId] - explicit override, bypasses file lookups
83
+ */
84
+ export function resolveActiveProfile(cwd, configProfileId = null) {
85
+ if (configProfileId) {
86
+ const p = loadProfile(configProfileId);
87
+ if (p) return p;
88
+ }
89
+ const custom = loadCustomProfile(cwd);
90
+ if (custom) return custom;
91
+ const fromConfig = readProfileFromProjectConfig(cwd);
92
+ if (fromConfig) {
93
+ const p = loadProfile(fromConfig);
94
+ if (p) return p;
95
+ }
96
+ return loadProfile(DEFAULT_PROFILE_ID) ?? minimalRndFallback();
97
+ }
98
+
99
+ function readProfileFromProjectConfig(cwd) {
100
+ if (!cwd) return null;
101
+ const p = join(cwd, 'construct.config.json');
102
+ if (!existsSync(p)) return null;
103
+ try {
104
+ const raw = JSON.parse(readFileSync(p, 'utf8'));
105
+ return typeof raw?.profile === 'string' ? raw.profile : null;
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ // Last-resort fallback if the curated rnd profile file is missing or
112
+ // unreadable. Lets callers always rely on a non-null shape.
113
+ function minimalRndFallback() {
114
+ return {
115
+ id: 'rnd',
116
+ displayName: 'Software R&D',
117
+ roles: [],
118
+ intake: { types: [], stages: [] },
119
+ docTemplates: [],
120
+ hooks: { sessionReflect: 'on', sessionOptimize: 'on' },
121
+ rebrand: { intakeQueueLabel: 'R&D intake queue', signalNoun: 'signal' },
122
+ };
123
+ }