@geraldmaron/construct 1.0.6 → 1.0.8

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 (218) hide show
  1. package/README.md +39 -34
  2. package/bin/construct +1403 -96
  3. package/commands/build/feature.md +1 -1
  4. package/commands/build/fix.md +1 -1
  5. package/commands/design/access.md +1 -1
  6. package/commands/design/flow.md +1 -1
  7. package/commands/design/ui.md +1 -1
  8. package/commands/measure/experiment.md +1 -1
  9. package/commands/measure/metrics.md +1 -1
  10. package/commands/measure/results.md +1 -1
  11. package/commands/plan/api.md +1 -1
  12. package/commands/plan/challenge.md +1 -1
  13. package/commands/plan/decide.md +1 -1
  14. package/commands/plan/feature.md +1 -1
  15. package/commands/plan/requirements.md +1 -1
  16. package/commands/remember/context.md +1 -1
  17. package/commands/remember/handoff.md +1 -1
  18. package/commands/remember/runbook.md +1 -1
  19. package/commands/review/code.md +1 -1
  20. package/commands/review/quality.md +1 -1
  21. package/commands/review/security.md +1 -1
  22. package/commands/ship/ready.md +1 -1
  23. package/commands/ship/release.md +1 -1
  24. package/commands/ship/status.md +1 -1
  25. package/commands/understand/docs.md +1 -1
  26. package/commands/understand/this.md +1 -1
  27. package/commands/understand/why.md +1 -1
  28. package/commands/work/clean.md +1 -1
  29. package/commands/work/drive.md +1 -1
  30. package/commands/work/optimize-prompts.md +2 -2
  31. package/commands/work/parallel-review.md +1 -1
  32. package/db/schema/007_tags.sql +30 -0
  33. package/db/schema/008_skill_usage.sql +24 -0
  34. package/db/schema/009_scheduler.sql +14 -0
  35. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +1 -1
  36. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +1 -1
  37. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +1 -1
  38. package/examples/internal/roles/engineer/golden/read-before-write.md +1 -1
  39. package/examples/internal/roles/qa/bad/coverage-theater.md +1 -1
  40. package/examples/internal/roles/qa/golden/regression-gate.md +1 -1
  41. package/examples/seed-observations/decisions.md +1 -1
  42. package/lib/audit-skills.mjs +3 -3
  43. package/lib/auto-docs.mjs +8 -8
  44. package/lib/boundary.mjs +126 -0
  45. package/lib/cache-strategy-google.js +26 -31
  46. package/lib/cli-commands.mjs +276 -122
  47. package/lib/comment-lint.mjs +139 -1
  48. package/lib/completions.mjs +1 -1
  49. package/lib/config/schema.mjs +15 -1
  50. package/lib/contracts/validate.mjs +371 -0
  51. package/lib/daemons/contract.mjs +210 -0
  52. package/lib/distill.mjs +22 -4
  53. package/lib/doc-stamp.mjs +48 -0
  54. package/lib/docs-verify.mjs +59 -6
  55. package/lib/doctor/cli.mjs +16 -1
  56. package/lib/doctor/index.mjs +3 -1
  57. package/lib/doctor/watchers/consistency.mjs +360 -0
  58. package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
  59. package/lib/document-extract.mjs +420 -17
  60. package/lib/document-ingest.mjs +2 -0
  61. package/lib/embed/cli.mjs +124 -3
  62. package/lib/embed/daemon.mjs +43 -4
  63. package/lib/embed/docs-lifecycle.mjs +1 -1
  64. package/lib/embed/inbox.mjs +50 -3
  65. package/lib/embed/role-framing.mjs +3 -3
  66. package/lib/embed/scheduler.mjs +33 -5
  67. package/lib/env-config.mjs +9 -3
  68. package/lib/evaluator-optimizer.mjs +2 -3
  69. package/lib/extractors/calendar.mjs +173 -0
  70. package/lib/extractors/shared/drop-info.mjs +22 -0
  71. package/lib/extractors/transcript.mjs +0 -0
  72. package/lib/gates-audit.mjs +8 -2
  73. package/lib/git-hooks/prepare-commit-msg +1 -1
  74. package/lib/headhunt.mjs +2 -2
  75. package/lib/hooks/agent-tracker.mjs +6 -11
  76. package/lib/hooks/comment-lint.mjs +16 -0
  77. package/lib/hooks/guard-bash.mjs +35 -14
  78. package/lib/hooks/mcp-audit.mjs +14 -1
  79. package/lib/hooks/pre-compact.mjs +92 -5
  80. package/lib/hooks/pre-push-gate.mjs +41 -8
  81. package/lib/hooks/proactive-activation.mjs +0 -14
  82. package/lib/hooks/registry-sync.mjs +2 -2
  83. package/lib/hooks/rule-verifier.mjs +217 -0
  84. package/lib/hooks/session-optimize.mjs +2 -1
  85. package/lib/hooks/stop-notify.mjs +7 -6
  86. package/lib/host-capabilities.mjs +10 -1
  87. package/lib/hygiene/scan.mjs +141 -0
  88. package/lib/init-unified.mjs +73 -65
  89. package/lib/install/stage-project.mjs +4 -4
  90. package/lib/intake/classify.mjs +309 -58
  91. package/lib/intake/daemon.mjs +121 -0
  92. package/lib/intake/filesystem-queue.mjs +31 -6
  93. package/lib/intake/intake-config.mjs +2 -1
  94. package/lib/intake/postgres-queue.mjs +45 -3
  95. package/lib/intake/prepare.mjs +3 -1
  96. package/lib/intake/quarantine.mjs +205 -0
  97. package/lib/intake/session-prelude.mjs +7 -1
  98. package/lib/intake/traceability.mjs +90 -0
  99. package/lib/knowledge/postgres-search.mjs +132 -0
  100. package/lib/knowledge/research-store.mjs +2 -0
  101. package/lib/knowledge/search.mjs +14 -4
  102. package/lib/maintenance/cleanup.mjs +315 -0
  103. package/lib/mcp/memory-bridge.mjs +276 -0
  104. package/lib/mcp/server.mjs +11 -3
  105. package/lib/mcp/tools/skills.mjs +32 -11
  106. package/lib/mcp/tools/workflow.mjs +25 -0
  107. package/lib/mcp-catalog.json +12 -8
  108. package/lib/mcp-platform-config.mjs +16 -8
  109. package/lib/migrations/index.mjs +106 -0
  110. package/lib/migrations/v1-baseline.mjs +33 -0
  111. package/lib/model-registry.mjs +1 -1
  112. package/lib/observation-store.mjs +9 -4
  113. package/lib/opencode-runtime-plugin.mjs +1 -1
  114. package/lib/orchestration-policy.mjs +4 -4
  115. package/lib/outcomes/record.mjs +2 -0
  116. package/lib/overrides/resolver.mjs +3 -3
  117. package/lib/parity.mjs +4 -4
  118. package/lib/policy/engine.mjs +2 -2
  119. package/lib/profiles/lifecycle.mjs +1 -1
  120. package/lib/profiles/rebrand.mjs +46 -0
  121. package/lib/project-init-shared.mjs +12 -0
  122. package/lib/prompt-metadata.mjs +4 -4
  123. package/lib/provider-capabilities.js +20 -7
  124. package/lib/providers/auth-manager.mjs +58 -17
  125. package/lib/providers/circuit-breaker.mjs +14 -0
  126. package/lib/providers/contract.mjs +67 -3
  127. package/lib/providers/creds.mjs +219 -0
  128. package/lib/providers/scaffold.mjs +107 -0
  129. package/lib/reflect.mjs +49 -12
  130. package/lib/role-preload.mjs +5 -0
  131. package/lib/roles/catalog.mjs +1 -1
  132. package/lib/roles/manifest.mjs +2 -2
  133. package/lib/scheduler/index.mjs +112 -0
  134. package/lib/scheduler/solo.mjs +183 -0
  135. package/lib/server/index.mjs +29 -29
  136. package/lib/server/insights.mjs +14 -12
  137. package/lib/server/langfuse-login.mjs +58 -0
  138. package/lib/server/static/assets/index-ab25c707.js +1 -1
  139. package/lib/session-store.mjs +6 -4
  140. package/lib/setup.mjs +14 -3
  141. package/lib/{agent-contracts-enforce.mjs → specialist-contracts-enforce.mjs} +4 -4
  142. package/lib/{agent-contracts.mjs → specialist-contracts.mjs} +9 -8
  143. package/lib/{agents → specialists}/postconditions.mjs +3 -3
  144. package/lib/{agents → specialists}/schema.mjs +6 -6
  145. package/lib/status.mjs +13 -11
  146. package/lib/storage/backup.mjs +2 -2
  147. package/lib/tags/lifecycle.mjs +278 -0
  148. package/lib/tags/vocabulary.mjs +140 -0
  149. package/lib/telemetry/client.mjs +5 -1
  150. package/lib/telemetry/otel-tracer.mjs +184 -0
  151. package/lib/telemetry/skill-calls.mjs +73 -12
  152. package/lib/uninstall/uninstall.mjs +1 -1
  153. package/lib/update.mjs +1 -1
  154. package/lib/validator.mjs +57 -56
  155. package/lib/version.mjs +51 -0
  156. package/lib/worker/trace.mjs +5 -1
  157. package/lib/workflows/instantiate.mjs +320 -0
  158. package/package.json +16 -3
  159. package/personas/construct.md +5 -3
  160. package/platforms/claude/CLAUDE.md +1 -1
  161. package/rules/common/development-workflow.md +2 -1
  162. package/rules/common/no-fabrication.md +69 -0
  163. package/rules/common/review-before-change.md +58 -0
  164. package/scripts/{sync-agents.mjs → sync-specialists.mjs} +171 -68
  165. package/skills/ai/prompt-optimizer.md +3 -3
  166. package/skills/exploration/dependency-graph-reading.md +98 -0
  167. package/skills/exploration/tracer-bullet-method.md +71 -0
  168. package/skills/exploration/unknown-codebase-onboarding.md +91 -0
  169. package/skills/operating/change-management.md +91 -0
  170. package/skills/operating/incident-response.md +75 -0
  171. package/skills/operating/oncall-rotation.md +95 -0
  172. package/skills/operating/orchestration-reference.md +2 -2
  173. package/skills/strategy/competitive-landscape.md +75 -0
  174. package/skills/strategy/market-research-methods.md +87 -0
  175. package/skills/strategy/narrative-arc.md +77 -0
  176. package/skills/strategy/pricing-positioning.md +94 -0
  177. package/specialists/contracts.json +1002 -0
  178. package/specialists/contracts.schema.json +83 -0
  179. package/specialists/policy-inventory.json +160 -0
  180. package/{agents → specialists}/prompts/cx-accessibility.md +3 -1
  181. package/{agents → specialists}/prompts/cx-ai-engineer.md +3 -1
  182. package/{agents → specialists}/prompts/cx-architect.md +3 -1
  183. package/{agents → specialists}/prompts/cx-business-strategist.md +3 -1
  184. package/{agents → specialists}/prompts/cx-data-analyst.md +3 -1
  185. package/{agents → specialists}/prompts/cx-data-engineer.md +3 -1
  186. package/{agents → specialists}/prompts/cx-debugger.md +3 -1
  187. package/{agents → specialists}/prompts/cx-designer.md +3 -1
  188. package/{agents → specialists}/prompts/cx-devil-advocate.md +3 -1
  189. package/{agents → specialists}/prompts/cx-docs-keeper.md +4 -2
  190. package/{agents → specialists}/prompts/cx-engineer.md +4 -2
  191. package/{agents → specialists}/prompts/cx-evaluator.md +3 -1
  192. package/{agents → specialists}/prompts/cx-explorer.md +3 -1
  193. package/{agents → specialists}/prompts/cx-legal-compliance.md +3 -1
  194. package/{agents → specialists}/prompts/cx-operations.md +3 -1
  195. package/{agents → specialists}/prompts/cx-orchestrator.md +4 -2
  196. package/{agents → specialists}/prompts/cx-platform-engineer.md +3 -1
  197. package/{agents → specialists}/prompts/cx-product-manager.md +3 -1
  198. package/{agents → specialists}/prompts/cx-qa.md +3 -1
  199. package/{agents → specialists}/prompts/cx-rd-lead.md +3 -1
  200. package/{agents → specialists}/prompts/cx-release-manager.md +3 -1
  201. package/{agents → specialists}/prompts/cx-researcher.md +3 -1
  202. package/{agents → specialists}/prompts/cx-reviewer.md +4 -2
  203. package/{agents → specialists}/prompts/cx-security.md +3 -1
  204. package/{agents → specialists}/prompts/cx-sre.md +4 -2
  205. package/{agents → specialists}/prompts/cx-test-automation.md +3 -1
  206. package/{agents → specialists}/prompts/cx-trace-reviewer.md +5 -3
  207. package/{agents → specialists}/prompts/cx-ux-researcher.md +3 -1
  208. package/{agents → specialists}/registry.json +651 -418
  209. package/{agents → specialists}/role-manifests.json +8 -8
  210. package/templates/docs/construct_guide.md +6 -6
  211. package/templates/docs/skill-artifact.md +1 -1
  212. package/templates/provider-scaffold/health.test.mjs +30 -0
  213. package/templates/provider-scaffold/index.mjs +48 -0
  214. package/templates/workflows/cross-team-handoff.yml +85 -0
  215. package/templates/workflows/engineering-onboarding.yml +77 -0
  216. package/templates/workflows/new-feature.yml +53 -0
  217. package/agents/contracts.json +0 -387
  218. /package/{agents → specialists}/teams.json +0 -0
@@ -21,8 +21,14 @@ export function listSkills({ ROOT_DIR }) {
21
21
  const entries = readdirSync(dir, { withFileTypes: true });
22
22
  for (const entry of entries) {
23
23
  if (entry.isDirectory()) {
24
- results = results.concat(listDirRecursive(join(dir, entry.name), `${prefix}${entry.name}/`));
25
- } else if (entry.name.endsWith('.md') && entry.name !== 'SKILL.md') {
24
+ // Directory form: <name>/SKILL.md — emit the directory name as the skill id.
25
+ const skillMd = join(dir, entry.name, 'SKILL.md');
26
+ if (existsSync(skillMd)) {
27
+ results.push(`${prefix}${entry.name}`);
28
+ } else {
29
+ results = results.concat(listDirRecursive(join(dir, entry.name), `${prefix}${entry.name}/`));
30
+ }
31
+ } else if (entry.name.endsWith('.md') && entry.name !== 'SKILL.md' && entry.name !== 'routing.md') {
26
32
  results.push(`${prefix}${entry.name.replace('.md', '')}`);
27
33
  }
28
34
  }
@@ -37,13 +43,28 @@ export function getSkill(args, { ROOT_DIR }) {
37
43
  const { path: skillPath } = args;
38
44
  if (!skillPath) return { error: 'Missing path argument' };
39
45
 
40
- const fullPath = join(ROOT_DIR, 'skills', `${skillPath}.md`);
46
+ // Primary: flat file form skills/<path>.md
47
+ // Fallback: directory form skills/<path>/SKILL.md (one minor-release grace period)
48
+ const candidates = [
49
+ join(ROOT_DIR, 'skills', `${skillPath}.md`),
50
+ join(ROOT_DIR, 'skills', skillPath, 'SKILL.md'),
51
+ ];
41
52
 
42
- if (!existsSync(fullPath)) {
53
+ const fullPath = candidates.find(existsSync);
54
+ if (!fullPath) {
43
55
  return { error: `Skill not found: ${skillPath}` };
44
56
  }
57
+ const t0 = Date.now();
45
58
  const content = readFileSync(fullPath, 'utf8');
46
- logSkillCall({ skillId: skillPath, source: 'mcp' });
59
+ logSkillCall({
60
+ skillId: skillPath,
61
+ source: 'mcp',
62
+ latencyMs: Date.now() - t0,
63
+ callerContext: args.callerContext,
64
+ agentId: args.agentId,
65
+ sessionId: args.sessionId,
66
+ tokensReturned: Math.ceil(content.length / 4),
67
+ });
47
68
  return { content };
48
69
  }
49
70
 
@@ -114,8 +135,8 @@ export async function agentContract(args = {}) {
114
135
  getOutgoingContracts,
115
136
  getIncomingContracts,
116
137
  summarize,
117
- } = await import('../../agent-contracts.mjs');
118
- const { describePostconditions } = await import('../../agents/postconditions.mjs');
138
+ } = await import('../../specialist-contracts.mjs');
139
+ const { describePostconditions } = await import('../../specialists/postconditions.mjs');
119
140
  const { id, producer, consumer } = args;
120
141
  // Attach binary postconditions to every returned contract so a specialist
121
142
  // calling this tool at handoff time sees the hard checks their output
@@ -382,8 +403,8 @@ export async function workerRun(args = {}) {
382
403
  }
383
404
 
384
405
  export function listTeams({ ROOT_DIR }) {
385
- const teamsPath = join(ROOT_DIR, 'agents', 'teams.json');
386
- if (!existsSync(teamsPath)) return { error: 'agents/teams.json not found' };
406
+ const teamsPath = join(ROOT_DIR, 'specialists', 'teams.json');
407
+ if (!existsSync(teamsPath)) return { error: 'specialists/teams.json not found' };
387
408
  const { templates } = JSON.parse(readFileSync(teamsPath, 'utf8'));
388
409
  return {
389
410
  teams: Object.entries(templates).map(([name, t]) => ({
@@ -400,8 +421,8 @@ export function listTeams({ ROOT_DIR }) {
400
421
  export function getTeam(args, { ROOT_DIR }) {
401
422
  const { name } = args;
402
423
  if (!name) return { error: 'Missing name argument' };
403
- const teamsPath = join(ROOT_DIR, 'agents', 'teams.json');
404
- if (!existsSync(teamsPath)) return { error: 'agents/teams.json not found' };
424
+ const teamsPath = join(ROOT_DIR, 'specialists', 'teams.json');
425
+ if (!existsSync(teamsPath)) return { error: 'specialists/teams.json not found' };
405
426
  const { templates } = JSON.parse(readFileSync(teamsPath, 'utf8'));
406
427
  const team = templates[name];
407
428
  if (!team) return { error: `Team not found: ${name}`, available: Object.keys(templates) };
@@ -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 specialists/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 (specialists/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,7 +19,7 @@ import path from 'node:path';
19
19
  * RESOLUTION ORDER (highest to lowest):
20
20
  * 1. Environment variables (CX_MODEL_*): Emergency overrides
21
21
  * 2. User config (~/.construct/config.env): Personal defaults
22
- * 3. Registry (agents/registry.json): Project recommendations
22
+ * 3. Registry (specialists/registry.json): Project recommendations
23
23
  * 4. Built-in defaults: Last resort fallbacks
24
24
  *
25
25
  * Primary resolution path. All code must use this function.
@@ -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',
@@ -415,7 +415,7 @@ function buildHostConstraints() {
415
415
 
416
416
  function readRegistryModels(rootDir) {
417
417
  try {
418
- const registryPath = join(rootDir, "agents", "registry.json");
418
+ const registryPath = join(rootDir, "specialists", "registry.json");
419
419
  if (!existsSync(registryPath)) return {};
420
420
  const registry = JSON.parse(readFileSync(registryPath, "utf8"));
421
421
  return registry?.models ?? {};
@@ -6,14 +6,14 @@
6
6
  * 2. framing/research/doc-ownership gates (what must be true before work starts)
7
7
  * 3. contract chain (resolveContractChain) (what the typed handoffs are)
8
8
  *
9
- * Agent-to-agent contracts are defined in agents/contracts.json and loaded via
10
- * lib/agent-contracts.mjs. That file is the single source of truth for
9
+ * Agent-to-agent contracts are defined in specialists/contracts.json and loaded via
10
+ * lib/specialist-contracts.mjs. That file is the single source of truth for
11
11
  * producer→consumer expectations, replacing the scattered DOC_OWNERSHIP map,
12
12
  * SPECIALIST_MAP, and informal "collaborators" lists for anything contract-
13
13
  * shaped. The older maps remain for quick lookups but defer to contracts.json
14
14
  * for authoritative semantics.
15
15
  */
16
- import { resolveContractChain } from './agent-contracts.mjs';
16
+ import { resolveContractChain } from './specialist-contracts.mjs';
17
17
  import { verifyRoute } from './intent-classifier.mjs';
18
18
 
19
19
  export const EXECUTION_TRACKS = {
@@ -80,7 +80,7 @@ export const DOC_OWNERSHIP = {
80
80
  // Event ownership for the role framework (lib/roles/**). When a hook emits
81
81
  // an event of one of these types, the gateway routes the invocation to the
82
82
  // listed persona. Empty owners mean the event is recorded but no persona
83
- // fires until onboarded. Keep in sync with agents/role-manifests.json.
83
+ // fires until onboarded. Keep in sync with specialists/role-manifests.json.
84
84
 
85
85
  export const EVENT_OWNERSHIP = {
86
86
  'push_gate.fail': 'cx-sre',
@@ -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
 
@@ -38,11 +38,11 @@ export const SUPPORTED_CATEGORIES = Object.freeze([
38
38
 
39
39
  const CATEGORY_RULES = {
40
40
  personas: { originalDir: 'personas', defaultExt: '.md' },
41
- agents: { originalDir: 'agents/prompts', defaultExt: '.md' },
41
+ agents: { originalDir: 'specialists/prompts', defaultExt: '.md' },
42
42
  skills: { originalDir: 'skills', defaultExt: '.md' },
43
43
  rules: { originalDir: 'rules', defaultExt: '.md' },
44
- contracts: { originalDir: 'agents', defaultExt: '.json', singleFile: 'contracts.json' },
45
- 'role-manifests': { originalDir: 'agents', defaultExt: '.json', singleFile: 'role-manifests.json' },
44
+ contracts: { originalDir: 'specialists', defaultExt: '.json', singleFile: 'contracts.json' },
45
+ 'role-manifests': { originalDir: 'specialists', defaultExt: '.json', singleFile: 'role-manifests.json' },
46
46
  };
47
47
 
48
48
  function categoryRule(category) {
package/lib/parity.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * After `construct sync` writes adapters to multiple surfaces (Claude Code,
5
5
  * OpenCode, Codex, Copilot, VS Code, Cursor), the module diffs each surface's actual state against the
6
- * canonical `agents/registry.json`. Backs `construct doctor` for surfacing
6
+ * canonical `specialists/registry.json`. Backs `construct doctor` for surfacing
7
7
  * silent divergence — for instance, an agent added to the registry that
8
8
  * never made it to OpenCode because of a sync regression.
9
9
  *
@@ -31,7 +31,7 @@ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
31
31
  const ROOT_DIR = path.resolve(MODULE_DIR, '..');
32
32
 
33
33
  function loadRegistry(rootDir = ROOT_DIR) {
34
- const file = path.join(rootDir, 'agents', 'registry.json');
34
+ const file = path.join(rootDir, 'specialists', 'registry.json');
35
35
  return JSON.parse(fs.readFileSync(file, 'utf8'));
36
36
  }
37
37
 
@@ -42,8 +42,8 @@ function adapterName(entry, prefix) {
42
42
  function entriesForSurface(registry, surface) {
43
43
  const prefix = registry.prefix || 'cx';
44
44
  const entries = [
45
- ...(registry.personas || []).map((p) => ({ ...p, isPersona: true })),
46
- ...(registry.agents || []).map((a) => ({ ...a, isPersona: false })),
45
+ ...(registry.orchestrator ? [{ ...registry.orchestrator, isPersona: true }] : []),
46
+ ...(registry.specialists || []).map((s) => ({ ...s, isPersona: false })),
47
47
  ];
48
48
 
49
49
  return entries
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * lib/policy/engine.mjs — role-based policy decisions for tool / action access.
3
3
  *
4
- * Reads role permissions from `agents/role-manifests.json` and decides
4
+ * Reads role permissions from `specialists/role-manifests.json` and decides
5
5
  * whether a given (role, project, tool, action, risk) tuple is allowed,
6
6
  * needs approval, or is denied outright. Powers the MCP broker for team
7
7
  * and enterprise deployments; solo mode leaves the broker off so this
@@ -20,7 +20,7 @@ import path from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
21
 
22
22
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
23
- const DEFAULT_MANIFEST_PATH = path.join(MODULE_DIR, '..', '..', 'agents', 'role-manifests.json');
23
+ const DEFAULT_MANIFEST_PATH = path.join(MODULE_DIR, '..', '..', 'specialists', 'role-manifests.json');
24
24
 
25
25
  const HIGH_RISK_AUTONOMOUS = new Set(['security', 'sre', 'release-manager']);
26
26
 
@@ -76,7 +76,7 @@ Goal: define the role set and how it interlocks with the curated cx-* registry.
76
76
  reuse-existing (name the cx-* agent), create-new (specify scope), or compose-overlay (which base role + which flavor).
77
77
  - Identify cross-role handoffs. Where can ambiguity stall the loop? Name the orchestrator role.
78
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.
79
+ - Acceptance: every declared role either exists in specialists/registry.json or has a written scope statement.
80
80
 
81
81
  ## 4. Validation (cx-evaluator)
82
82
 
@@ -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.
@@ -25,9 +25,9 @@ function normalizeName(name) {
25
25
  function findEntry(registry, agentName) {
26
26
  const normalized = normalizeName(agentName);
27
27
  const entries = [
28
- ...(registry.personas ?? []),
29
- ...(registry.agents ?? []),
30
- ];
28
+ registry.orchestrator,
29
+ ...(registry.specialists ?? []),
30
+ ].filter(Boolean);
31
31
  return entries.find((entry) => {
32
32
  const entryName = normalizeName(entry.name);
33
33
  return entryName === normalized || `cx-${entryName}` === String(agentName ?? '').trim().toLowerCase();
@@ -36,7 +36,7 @@ function findEntry(registry, agentName) {
36
36
 
37
37
  export function resolvePromptEntry(agentName, { rootDir = process.cwd(), registry } = {}) {
38
38
  if (!agentName) return null;
39
- const loadedRegistry = registry ?? readJson(path.join(rootDir, 'agents', 'registry.json'));
39
+ const loadedRegistry = registry ?? readJson(path.join(rootDir, 'specialists', 'registry.json'));
40
40
  if (!loadedRegistry) return null;
41
41
  return findEntry(loadedRegistry, agentName);
42
42
  }