@geraldmaron/construct 1.0.7 → 1.0.9

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 (147) hide show
  1. package/README.md +20 -17
  2. package/bin/construct +1199 -69
  3. package/commands/work/optimize-prompts.md +1 -1
  4. package/db/schema/007_tags.sql +30 -0
  5. package/db/schema/008_skill_usage.sql +24 -0
  6. package/db/schema/009_scheduler.sql +14 -0
  7. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +1 -1
  8. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +1 -1
  9. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +1 -1
  10. package/examples/internal/roles/engineer/golden/read-before-write.md +1 -1
  11. package/examples/internal/roles/qa/bad/coverage-theater.md +1 -1
  12. package/examples/internal/roles/qa/golden/regression-gate.md +1 -1
  13. package/examples/seed-observations/decisions.md +1 -1
  14. package/lib/audit-skills.mjs +3 -3
  15. package/lib/auto-docs.mjs +2 -2
  16. package/lib/cli-commands.mjs +276 -122
  17. package/lib/comment-lint.mjs +5 -1
  18. package/lib/completions.mjs +1 -1
  19. package/lib/config/schema.mjs +15 -1
  20. package/lib/contracts/validate.mjs +65 -17
  21. package/lib/distill.mjs +22 -4
  22. package/lib/doc-stamp.mjs +48 -0
  23. package/lib/doctor/watchers/consistency.mjs +62 -12
  24. package/lib/document-extract.mjs +221 -28
  25. package/lib/document-ingest.mjs +2 -0
  26. package/lib/embed/inbox.mjs +48 -3
  27. package/lib/embed/role-framing.mjs +3 -3
  28. package/lib/env-config.mjs +9 -3
  29. package/lib/extractors/calendar.mjs +173 -0
  30. package/lib/extractors/shared/drop-info.mjs +22 -0
  31. package/lib/extractors/transcript.mjs +0 -0
  32. package/lib/gates-audit.mjs +8 -2
  33. package/lib/git-hooks/prepare-commit-msg +1 -1
  34. package/lib/headhunt.mjs +2 -2
  35. package/lib/hooks/agent-tracker.mjs +6 -11
  36. package/lib/hooks/guard-bash.mjs +35 -14
  37. package/lib/hooks/mcp-audit.mjs +12 -0
  38. package/lib/hooks/pre-compact.mjs +92 -5
  39. package/lib/hooks/pre-push-gate.mjs +41 -8
  40. package/lib/hooks/registry-sync.mjs +2 -2
  41. package/lib/hooks/stop-notify.mjs +7 -6
  42. package/lib/host-capabilities.mjs +10 -1
  43. package/lib/hygiene/scan.mjs +141 -0
  44. package/lib/init-unified.mjs +18 -0
  45. package/lib/install/stage-project.mjs +4 -4
  46. package/lib/intake/classify.mjs +222 -55
  47. package/lib/intake/filesystem-queue.mjs +25 -5
  48. package/lib/intake/postgres-queue.mjs +45 -3
  49. package/lib/intake/prepare.mjs +3 -0
  50. package/lib/intake/quarantine.mjs +205 -0
  51. package/lib/knowledge/postgres-search.mjs +132 -0
  52. package/lib/knowledge/search.mjs +14 -4
  53. package/lib/mcp/server.mjs +9 -3
  54. package/lib/mcp/tools/skills.mjs +32 -11
  55. package/lib/mcp/tools/workflow.mjs +1 -1
  56. package/lib/migrations/index.mjs +1 -1
  57. package/lib/model-registry.mjs +1 -1
  58. package/lib/opencode-runtime-plugin.mjs +1 -1
  59. package/lib/orchestration-policy.mjs +4 -4
  60. package/lib/overrides/resolver.mjs +3 -3
  61. package/lib/parity.mjs +4 -4
  62. package/lib/policy/engine.mjs +2 -2
  63. package/lib/profiles/lifecycle.mjs +1 -1
  64. package/lib/prompt-metadata.mjs +4 -4
  65. package/lib/providers/circuit-breaker.mjs +14 -0
  66. package/lib/providers/contract.mjs +67 -3
  67. package/lib/providers/creds.mjs +219 -0
  68. package/lib/providers/scaffold.mjs +107 -0
  69. package/lib/role-preload.mjs +5 -0
  70. package/lib/roles/catalog.mjs +1 -1
  71. package/lib/roles/manifest.mjs +2 -2
  72. package/lib/scheduler/index.mjs +112 -0
  73. package/lib/scheduler/solo.mjs +183 -0
  74. package/lib/server/index.mjs +7 -1
  75. package/lib/server/insights.mjs +14 -12
  76. package/lib/server/langfuse-login.mjs +58 -0
  77. package/lib/server/static/assets/index-ab25c707.js +1 -1
  78. package/lib/{agent-contracts-enforce.mjs → specialist-contracts-enforce.mjs} +4 -4
  79. package/lib/{agent-contracts.mjs → specialist-contracts.mjs} +9 -8
  80. package/lib/{agents → specialists}/postconditions.mjs +3 -3
  81. package/lib/{agents → specialists}/schema.mjs +6 -6
  82. package/lib/status.mjs +13 -11
  83. package/lib/storage/backup.mjs +2 -2
  84. package/lib/tags/lifecycle.mjs +278 -0
  85. package/lib/tags/vocabulary.mjs +140 -0
  86. package/lib/telemetry/otel-tracer.mjs +184 -0
  87. package/lib/telemetry/skill-calls.mjs +73 -12
  88. package/lib/uninstall/uninstall.mjs +1 -1
  89. package/lib/update.mjs +1 -1
  90. package/lib/validator.mjs +57 -56
  91. package/lib/workflows/instantiate.mjs +320 -0
  92. package/package.json +13 -3
  93. package/personas/construct.md +2 -2
  94. package/platforms/claude/CLAUDE.md +1 -1
  95. package/rules/common/no-fabrication.md +1 -1
  96. package/scripts/{sync-agents.mjs → sync-specialists.mjs} +130 -58
  97. package/skills/ai/prompt-optimizer.md +3 -3
  98. package/skills/exploration/dependency-graph-reading.md +98 -0
  99. package/skills/exploration/tracer-bullet-method.md +71 -0
  100. package/skills/exploration/unknown-codebase-onboarding.md +91 -0
  101. package/skills/operating/change-management.md +91 -0
  102. package/skills/operating/incident-response.md +75 -0
  103. package/skills/operating/oncall-rotation.md +95 -0
  104. package/skills/operating/orchestration-reference.md +2 -2
  105. package/skills/strategy/competitive-landscape.md +75 -0
  106. package/skills/strategy/market-research-methods.md +87 -0
  107. package/skills/strategy/narrative-arc.md +77 -0
  108. package/skills/strategy/pricing-positioning.md +94 -0
  109. package/{agents → specialists}/contracts.schema.json +1 -1
  110. package/specialists/policy-inventory.json +160 -0
  111. package/{agents → specialists}/prompts/cx-accessibility.md +1 -1
  112. package/{agents → specialists}/prompts/cx-ai-engineer.md +1 -1
  113. package/{agents → specialists}/prompts/cx-architect.md +1 -1
  114. package/{agents → specialists}/prompts/cx-business-strategist.md +1 -1
  115. package/{agents → specialists}/prompts/cx-data-analyst.md +1 -1
  116. package/{agents → specialists}/prompts/cx-data-engineer.md +1 -1
  117. package/{agents → specialists}/prompts/cx-debugger.md +1 -1
  118. package/{agents → specialists}/prompts/cx-designer.md +1 -1
  119. package/{agents → specialists}/prompts/cx-devil-advocate.md +1 -1
  120. package/{agents → specialists}/prompts/cx-docs-keeper.md +2 -2
  121. package/{agents → specialists}/prompts/cx-engineer.md +2 -2
  122. package/{agents → specialists}/prompts/cx-evaluator.md +1 -1
  123. package/{agents → specialists}/prompts/cx-explorer.md +1 -1
  124. package/{agents → specialists}/prompts/cx-legal-compliance.md +1 -1
  125. package/{agents → specialists}/prompts/cx-operations.md +1 -1
  126. package/{agents → specialists}/prompts/cx-orchestrator.md +2 -2
  127. package/{agents → specialists}/prompts/cx-platform-engineer.md +1 -1
  128. package/{agents → specialists}/prompts/cx-product-manager.md +1 -1
  129. package/{agents → specialists}/prompts/cx-qa.md +1 -1
  130. package/{agents → specialists}/prompts/cx-rd-lead.md +1 -1
  131. package/{agents → specialists}/prompts/cx-release-manager.md +1 -1
  132. package/{agents → specialists}/prompts/cx-researcher.md +1 -1
  133. package/{agents → specialists}/prompts/cx-reviewer.md +2 -2
  134. package/{agents → specialists}/prompts/cx-security.md +1 -1
  135. package/{agents → specialists}/prompts/cx-sre.md +2 -2
  136. package/{agents → specialists}/prompts/cx-test-automation.md +1 -1
  137. package/{agents → specialists}/prompts/cx-trace-reviewer.md +3 -3
  138. package/{agents → specialists}/prompts/cx-ux-researcher.md +1 -1
  139. package/{agents → specialists}/registry.json +651 -423
  140. package/{agents → specialists}/role-manifests.json +8 -8
  141. package/templates/provider-scaffold/health.test.mjs +30 -0
  142. package/templates/provider-scaffold/index.mjs +48 -0
  143. package/templates/workflows/cross-team-handoff.yml +85 -0
  144. package/templates/workflows/engineering-onboarding.yml +77 -0
  145. package/templates/workflows/new-feature.yml +53 -0
  146. /package/{agents → specialists}/contracts.json +0 -0
  147. /package/{agents → specialists}/teams.json +0 -0
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * sync-agents.mjs — regenerate agent adapters for all platforms from agents/registry.json.
3
+ * sync-specialists.mjs — regenerate agent adapters for all platforms from specialists/registry.json.
4
4
  *
5
5
  * Reads registry.json, resolves env vars and model tiers, then writes Claude Code,
6
6
  * OpenCode, Codex, Copilot, VS Code, and Cursor adapters. Called by 'construct sync'.
@@ -50,7 +50,7 @@ for (const [key, value] of Object.entries(mergedEnv)) {
50
50
  if (!(key in process.env)) process.env[key] = value;
51
51
  }
52
52
  if (!process.env.CX_TOOLKIT_DIR) process.env.CX_TOOLKIT_DIR = root;
53
- const registryPath = path.join(root, "agents", "registry.json");
53
+ const registryPath = path.join(root, "specialists", "registry.json");
54
54
  const registry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
55
55
 
56
56
  function validateRegistry(registry) {
@@ -58,31 +58,32 @@ function validateRegistry(registry) {
58
58
  if (!registry.version) errors.push("Missing 'version' field");
59
59
  if (!registry.system) errors.push("Missing 'system' field");
60
60
  if (!registry.prefix) errors.push("Missing 'prefix' field");
61
- if (!Array.isArray(registry.agents)) errors.push("Missing or invalid 'agents' array");
62
- if (!Array.isArray(registry.personas)) errors.push("Missing or invalid 'personas' array");
61
+ if (!registry.orchestrator) errors.push("Missing 'orchestrator' field");
62
+ if (!Array.isArray(registry.specialists)) errors.push("Missing or invalid 'specialists' array");
63
63
  const validTiers = new Set(["reasoning", "standard", "fast"]);
64
64
  const names = new Set();
65
65
 
66
- for (const persona of registry.personas ?? []) {
67
- if (!persona.name) { errors.push("Persona missing 'name'"); continue; }
68
- if (names.has(persona.name)) errors.push(`Duplicate name: ${persona.name}`);
69
- names.add(persona.name);
70
- if (!persona.description) errors.push(`${persona.name}: missing 'description'`);
71
- if (!persona.promptFile) errors.push(`${persona.name}: missing 'promptFile'`);
72
- if (!persona.displayName) errors.push(`${persona.name}: missing 'displayName'`);
66
+ if (registry.orchestrator) {
67
+ const o = registry.orchestrator;
68
+ if (!o.name) errors.push("Orchestrator missing 'name'");
69
+ if (names.has(o.name)) errors.push(`Duplicate name: ${o.name}`);
70
+ names.add(o.name);
71
+ if (!o.description) errors.push("Orchestrator: missing 'description'");
72
+ if (!o.promptFile) errors.push("Orchestrator: missing 'promptFile'");
73
+ if (!o.displayName) errors.push("Orchestrator: missing 'displayName'");
73
74
  }
74
75
 
75
- for (const agent of registry.agents ?? []) {
76
- if (!agent.name) { errors.push("Agent missing 'name'"); continue; }
77
- if (names.has(agent.name)) errors.push(`Duplicate name: ${agent.name}`);
78
- names.add(agent.name);
79
- if (!agent.prompt && !agent.promptFile) errors.push(`${agent.name}: missing 'prompt' or 'promptFile'`);
80
- if (!agent.description) errors.push(`${agent.name}: missing 'description'`);
81
- if (!agent.model && !agent.modelTier) errors.push(`${agent.name}: needs 'model' or 'modelTier'`);
82
- if (agent.modelTier && !validTiers.has(agent.modelTier)) errors.push(`${agent.name}: invalid modelTier '${agent.modelTier}'`);
83
- if (!agent.claudeTools) errors.push(`${agent.name}: missing 'claudeTools'`);
84
- if (agent.modelGuidance && typeof agent.modelGuidance !== "object") {
85
- errors.push(`${agent.name}: modelGuidance must be an object`);
76
+ for (const specialist of registry.specialists ?? []) {
77
+ if (!specialist.name) { errors.push("Specialist missing 'name'"); continue; }
78
+ if (names.has(specialist.name)) errors.push(`Duplicate name: ${specialist.name}`);
79
+ names.add(specialist.name);
80
+ if (!specialist.prompt && !specialist.promptFile) errors.push(`${specialist.name}: missing 'prompt' or 'promptFile'`);
81
+ if (!specialist.description) errors.push(`${specialist.name}: missing 'description'`);
82
+ if (!specialist.model && !specialist.modelTier) errors.push(`${specialist.name}: needs 'model' or 'modelTier'`);
83
+ if (specialist.modelTier && !validTiers.has(specialist.modelTier)) errors.push(`${specialist.name}: invalid modelTier '${specialist.modelTier}'`);
84
+ if (!specialist.claudeTools) errors.push(`${specialist.name}: missing 'claudeTools'`);
85
+ if (specialist.modelGuidance && typeof specialist.modelGuidance !== "object") {
86
+ errors.push(`${specialist.name}: modelGuidance must be an object`);
86
87
  }
87
88
  }
88
89
 
@@ -137,8 +138,9 @@ if (validationErrors.length > 0) {
137
138
 
138
139
  const DRY_RUN = process.argv.includes("--dry-run");
139
140
  const COMPRESS_PERSONAS = process.argv.includes("--compress-personas");
140
- const lockPath = path.join(root, ".cx", "sync.lock");
141
- const stagingDir = path.join(root, ".cx", "sync-staging");
141
+ const projectDir = process.argv.includes("--project") ? process.cwd() : null;
142
+ const lockPath = path.join(projectDir || root, ".cx", "sync.lock");
143
+ const stagingDir = path.join(projectDir || root, ".cx", "sync-staging");
142
144
 
143
145
  /** Acquire an exclusive lockfile. Aborts if already held by a live process. */
144
146
  function acquireLock() {
@@ -180,7 +182,7 @@ const _stagedPairs = []; // [{ staging, real, content }]
180
182
 
181
183
  function writeFile(file, content) {
182
184
  mkdirp(path.dirname(file));
183
- const stamped = file.endsWith('.md') ? stampFrontmatter(content, { generator: 'construct/sync-agents' }) : content;
185
+ const stamped = file.endsWith('.md') ? stampFrontmatter(content, { generator: 'construct/sync-specialists' }) : content;
184
186
 
185
187
  if (DRY_RUN) {
186
188
  // Stage in memory only — compare against current on-disk content.
@@ -191,7 +193,7 @@ function writeFile(file, content) {
191
193
  }
192
194
 
193
195
  // Two-phase: write to staging, commit later.
194
- const rel = path.relative(root, file);
196
+ const rel = path.relative(projectDir || root, file);
195
197
  const stagingPath = path.join(stagingDir, rel);
196
198
  mkdirp(path.dirname(stagingPath));
197
199
  fs.writeFileSync(stagingPath, stamped);
@@ -228,15 +230,15 @@ const sharedGuidance = registry.sharedGuidance ?? [];
228
230
  const platformGuidance = registry.platformGuidance ?? {};
229
231
  const globalModelGuidance = registry.modelGuidance ?? {};
230
232
 
231
- const generatedHeader = `# Generated by ${systemName}/sync-agents.mjs. Edit agents/registry.json instead.`;
233
+ const generatedHeader = `# Generated by ${systemName}/sync-specialists.mjs. Edit specialists/registry.json instead.`;
232
234
  const generatedMarkdownNote = [
233
235
  '<!--',
234
- `Generated by construct sync from agents/registry.json.`,
236
+ `Generated by construct sync from specialists/registry.json.`,
235
237
  'Do not edit this file directly — changes will be overwritten on next sync.',
236
238
  'Regenerate: construct sync',
237
239
  '-->',
238
240
  '',
239
- '> Generated from `agents/registry.json`. Edit the registry, then run `construct sync`.',
241
+ '> Generated from `specialists/registry.json`. Edit the registry, then run `construct sync`.',
240
242
  ].join('\n');
241
243
 
242
244
  const standardConstructTools = [
@@ -362,7 +364,7 @@ function resolveModelChain(entry) {
362
364
  function mkdirp(dir) { fs.mkdirSync(dir, { recursive: true }); }
363
365
 
364
366
  function adapterName(entry) {
365
- return entry.isPersona ? entry.name : `${agentPrefix}${entry.name}`;
367
+ return entry.isOrchestrator ? entry.name : `${agentPrefix}${entry.name}`;
366
368
  }
367
369
 
368
370
  function loadPersonaPrompt(persona) {
@@ -398,7 +400,7 @@ function buildRoleFooter(entry) {
398
400
  if (collaborators.length > 0) {
399
401
  lines.push(`Collaborators: ${collaborators.map((c) => (c.startsWith("cx-") ? c : `cx-${c}`)).join(", ")}.`);
400
402
  }
401
- if (entry.isPersona !== true && entry.canEdit === false) {
403
+ if (entry.isOrchestrator !== true && entry.canEdit === false) {
402
404
  lines.push("Do not implement code or edit source files.");
403
405
  }
404
406
  if (entry.returnsStructured !== false) {
@@ -444,7 +446,7 @@ function buildPrompt(entry, allEntries, platform) {
444
446
  console.error(
445
447
  `[sync] Hard cap exceeded. Options:\n` +
446
448
  ` - trim the prompt body or move detail to a skill (preferred)\n` +
447
- ` - set "wordCapOverride": <N> on this entry in agents/registry.json with a written reason\n` +
449
+ ` - set "wordCapOverride": <N> on this entry in specialists/registry.json with a written reason\n` +
448
450
  ` - re-run with --force or CONSTRUCT_SYNC_FORCE=1 as a temporary escape hatch\n` +
449
451
  `Prompt budget is a hard contract because every over-cap agent degrades every session that dispatches it.`,
450
452
  );
@@ -501,18 +503,19 @@ function removeStaleAdapters(dir, ext, entries) {
501
503
  function buildEntries() {
502
504
  const entries = [];
503
505
 
504
- for (const persona of registry.personas ?? []) {
505
- const personaCanEdit = persona.permissions?.edit === "allow";
506
+ // Orchestrator entry
507
+ if (registry.orchestrator) {
508
+ const orchestratorCanEdit = registry.orchestrator.permissions?.edit === "allow";
506
509
  entries.push({
507
- ...persona,
508
- isPersona: true,
509
- prompt: loadPersonaPrompt(persona),
510
- codexSandbox: persona.codexSandbox ?? (personaCanEdit ? "workspace-write" : "read-only"),
511
- reasoningEffort: persona.reasoningEffort ?? "high",
510
+ ...registry.orchestrator,
511
+ isOrchestrator: true,
512
+ prompt: loadPersonaPrompt(registry.orchestrator),
513
+ codexSandbox: registry.orchestrator.codexSandbox ?? (orchestratorCanEdit ? "workspace-write" : "read-only"),
514
+ reasoningEffort: registry.orchestrator.reasoningEffort ?? "high",
512
515
  });
513
516
  }
514
517
 
515
- // Filter agents by the active profile's role set. RND lists every specialist
518
+ // Filter specialists by the active profile's role set. RND lists every specialist
516
519
  // so behavior is unchanged for the default; non-RND profiles emit only the
517
520
  // specialists they declare. Profiles whose role list is empty (legacy/test)
518
521
  // fall through to no filter so callers don't get a silent empty registry.
@@ -521,11 +524,11 @@ function buildEntries() {
521
524
  ? new Set(activeProfile.roles)
522
525
  : null;
523
526
 
524
- for (const agent of registry.agents ?? []) {
525
- if (profileRoles && !profileRoles.has(agent.name)) continue;
527
+ for (const specialist of registry.specialists ?? []) {
528
+ if (profileRoles && !profileRoles.has(specialist.name)) continue;
526
529
  entries.push({
527
- ...agent,
528
- isPersona: false,
530
+ ...specialist,
531
+ isSpecialist: true,
529
532
  });
530
533
  }
531
534
 
@@ -650,16 +653,16 @@ function syncClaude(entries, targetDir = null) {
650
653
  if (!targetDir) {
651
654
  const claudeMdPath = path.join(home, ".claude", "CLAUDE.md");
652
655
  const existing = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, "utf8") : "# Claude Global Instructions\n";
653
- const personaList = entries.filter((e) => e.isPersona).map((e) => `- \`${adapterName(e)}\`: ${e.role} — ${e.description}`).join("\n");
654
- // Only surface non-internal agents in CLAUDE.md — internal agents are dispatched by Construct, not invoked by users
655
- const agentList = entries.filter((e) => !e.isPersona && !e.internal).map((e) => `- \`${adapterName(e)}\`: ${e.description}`).join("\n");
656
+ const personaList = entries.filter((e) => e.isOrchestrator).map((e) => `- \`${adapterName(e)}\`: ${e.role} — ${e.description}`).join("\n");
657
+ // Only surface non-internal specialists in CLAUDE.md — internal specialists are dispatched by Construct, not invoked by users
658
+ const specialistList = entries.filter((e) => !e.isOrchestrator && !e.internal).map((e) => `- \`${adapterName(e)}\`: ${e.description}`).join("\n");
656
659
  const note = `## ${systemName.charAt(0).toUpperCase() + systemName.slice(1)} Personas
657
660
 
658
661
  ${personaList}
659
662
 
660
663
  ## Internal Specialists
661
664
 
662
- ${agentList || "(all specialists are internal — routed through Construct)"}`;
665
+ ${specialistList || "(all specialists are internal — routed through Construct)"}`;
663
666
  writeFile(claudeMdPath, replaceManagedBlock(existing, note, mdManagedStart, mdManagedEnd));
664
667
 
665
668
  // Sync MCP servers into ~/.claude/settings.json if it exists
@@ -1011,8 +1014,8 @@ function syncOpencode(entries) {
1011
1014
  const prefixes = [agentPrefix];
1012
1015
  for (const key of Object.keys(config.agent)) {
1013
1016
  const isManaged = prefixes.some((p) => key.startsWith(p));
1014
- const isPersona = registry.personas.some((p) => p.name === key);
1015
- if ((isManaged || isPersona) && !entries.find((e) => adapterName(e) === key)) {
1017
+ const isOrchestrator = registry.orchestrator?.name === key;
1018
+ if ((isManaged || isOrchestrator) && !entries.find((e) => adapterName(e) === key)) {
1016
1019
  delete config.agent[key];
1017
1020
  }
1018
1021
  }
@@ -1022,10 +1025,10 @@ function syncOpencode(entries) {
1022
1025
  const name = adapterName(entry);
1023
1026
  const perms = opencodePermissions(entry);
1024
1027
  config.agent[name] = {
1025
- description: entry.isPersona
1028
+ description: entry.isOrchestrator
1026
1029
  ? `${entry.role} — ${entry.description}`
1027
1030
  : entry.description,
1028
- mode: entry.isPersona ? "all" : "subagent",
1031
+ mode: entry.isOrchestrator ? "all" : "subagent",
1029
1032
  prompt: buildPrompt(entry, entries, "opencode"),
1030
1033
  permission: {
1031
1034
  ...perms,
@@ -1102,9 +1105,77 @@ function syncCommands(targetDir = null) {
1102
1105
  return count;
1103
1106
  }
1104
1107
 
1108
+ // --- Skills sync ---
1109
+
1110
+ /**
1111
+ * Walk skills/ recursively and collect every .md file (flat) and every
1112
+ * <name>/SKILL.md directory entry. Returns [{ name, content }] where name
1113
+ * is the slash-relative path without extension, e.g. "strategy/narrative-arc".
1114
+ */
1115
+ function collectSkills() {
1116
+ const skillsDir = path.join(root, "skills");
1117
+ const results = [];
1118
+
1119
+ function walk(dir, prefix) {
1120
+ if (!fs.existsSync(dir)) return;
1121
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1122
+ const full = path.join(dir, entry.name);
1123
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
1124
+
1125
+ if (entry.isDirectory()) {
1126
+ // Check if this is a SKILL.md directory form: <name>/SKILL.md
1127
+ const skillMdPath = path.join(full, "SKILL.md");
1128
+ if (fs.existsSync(skillMdPath)) {
1129
+ // Avoid double-walking; record this skill and do not descend further.
1130
+ const name = rel;
1131
+ results.push({ name, content: fs.readFileSync(skillMdPath, "utf8") });
1132
+ } else {
1133
+ walk(full, rel);
1134
+ }
1135
+ } else if (entry.name.endsWith(".md") && entry.name !== "routing.md" && entry.name !== "SKILL.md") {
1136
+ const name = rel.replace(/\.md$/, "");
1137
+ results.push({ name, content: fs.readFileSync(full, "utf8") });
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ walk(skillsDir, "");
1143
+ return results;
1144
+ }
1145
+
1146
+ /**
1147
+ * Prefix prepended to every generated SKILL.md. The comment must survive
1148
+ * round-trips — it is the canonical signal that a file was produced by sync
1149
+ * and must not be hand-edited.
1150
+ */
1151
+ const SKILL_GENERATED_PREFIX = "# Generated by construct sync\n\n";
1152
+
1153
+ /**
1154
+ * Write collected skills to both .claude/skills/ and .agents/skills/ in
1155
+ * SKILL.md directory format.
1156
+ *
1157
+ * Idempotency: content written is deterministic; writing the same content
1158
+ * twice is a no-op because writeFile skips unchanged files in dry-run mode
1159
+ * and two-phase staging in normal mode produces identical results.
1160
+ */
1161
+ function syncSkills() {
1162
+ const skills = collectSkills();
1163
+ if (skills.length === 0) return 0;
1164
+
1165
+ const claudeSkillsDir = path.join(home, ".claude", "skills");
1166
+ const agentsSkillsDir = path.join(home, ".agents", "skills");
1167
+
1168
+ for (const { name, content } of skills) {
1169
+ const generated = `${SKILL_GENERATED_PREFIX}${content}`;
1170
+ writeFile(path.join(claudeSkillsDir, name, "SKILL.md"), generated);
1171
+ writeFile(path.join(agentsSkillsDir, name, "SKILL.md"), generated);
1172
+ }
1173
+
1174
+ return skills.length;
1175
+ }
1176
+
1105
1177
  // --- Main ---
1106
1178
 
1107
- const projectDir = process.argv.includes("--project") ? process.cwd() : null;
1108
1179
  const entries = buildEntries();
1109
1180
 
1110
1181
  if (COMPRESS_PERSONAS) {
@@ -1117,7 +1188,7 @@ if (COMPRESS_PERSONAS) {
1117
1188
  let totalIn = 0;
1118
1189
  let totalOut = 0;
1119
1190
  for (const entry of entries) {
1120
- if (!entry.isPersona || !entry.prompt) continue;
1191
+ if (!entry.isOrchestrator || !entry.prompt) continue;
1121
1192
  const before = entry.prompt;
1122
1193
  try {
1123
1194
  const after = await compressor.compress(before, { ratio: 0.6 });
@@ -1132,7 +1203,7 @@ if (COMPRESS_PERSONAS) {
1132
1203
  }
1133
1204
  if (totalIn > 0) {
1134
1205
  const ratio = ((totalOut / totalIn) * 100).toFixed(0);
1135
- console.log(`compress-personas: ${totalIn} → ${totalOut} chars (${ratio}%) across ${entries.filter((e) => e.isPersona).length} personas`);
1206
+ console.log(`compress-personas: ${totalIn} → ${totalOut} chars (${ratio}%) across ${entries.filter((e) => e.isOrchestrator).length} personas`);
1136
1207
  }
1137
1208
  }
1138
1209
 
@@ -1148,8 +1219,8 @@ try {
1148
1219
  console.log(`Synced ${entries.length} agents + ${cmdCount} commands to ${path.join(projectDir, ".claude/")} (project mode).`);
1149
1220
  }
1150
1221
  } else {
1151
- const personaCount = entries.filter((e) => e.isPersona).length;
1152
- const agentCount = entries.filter((e) => !e.isPersona).length;
1222
+ const personaCount = entries.filter((e) => e.isOrchestrator).length;
1223
+ const specialistCount = entries.filter((e) => !e.isOrchestrator).length;
1153
1224
 
1154
1225
  syncCodex(entries);
1155
1226
  syncClaude(entries);
@@ -1158,6 +1229,7 @@ try {
1158
1229
  const vscodeOk = syncVSCode();
1159
1230
  const cursorOk = syncCursor();
1160
1231
  const cmdCount = syncCommands();
1232
+ const skillCount = syncSkills();
1161
1233
 
1162
1234
  if (DRY_RUN) {
1163
1235
  printDryRunDiff();
@@ -1172,7 +1244,7 @@ try {
1172
1244
  vscodeOk && "VS Code",
1173
1245
  cursorOk && "Cursor",
1174
1246
  ].filter(Boolean).join(", ");
1175
- console.log(`Synced ${personaCount} personas + ${agentCount} specialists + ${cmdCount} commands to ${targets}.`);
1247
+ console.log(`Synced ${personaCount} orchestrator + ${specialistCount} specialists + ${cmdCount} commands + ${skillCount} skills to ${targets}.`);
1176
1248
 
1177
1249
  // Regenerate shell completions so new commands are immediately tab-completable
1178
1250
  const completionsDir = generateCompletions();
@@ -6,7 +6,7 @@ and the agent registry + construct sync as the deployment layer.
6
6
  -->
7
7
  # Prompt Auto-Optimization Loop
8
8
 
9
- Construct's prompt improvement system uses telemetry traces and quality scores as the feedback signal, Claude as the optimizer, and the agent registry (`agents/registry.json`) as the deployment layer. This is a closed loop: production data → failure analysis → improved prompt → staging → promotion.
9
+ Construct's prompt improvement system uses telemetry traces and quality scores as the feedback signal, Claude as the optimizer, and the agent registry (`specialists/registry.json`) as the deployment layer. This is a closed loop: production data → failure analysis → improved prompt → staging → promotion.
10
10
 
11
11
  ## Running the optimizer
12
12
 
@@ -34,7 +34,7 @@ The optimizer requires Python 3.12+ (`pip3` must be available). It will auto-ins
34
34
 
35
35
  ## Step 1: Gather signal
36
36
 
37
- Retrieve the current production prompt for the target agent from `agents/registry.json` (or the corresponding `promptFile` if using extracted prompts).
37
+ Retrieve the current production prompt for the target agent from `specialists/registry.json` (or the corresponding `promptFile` if using extracted prompts).
38
38
 
39
39
  Then retrieve recent traces via the telemetry backend REST API:
40
40
 
@@ -76,7 +76,7 @@ Write an improved prompt that directly addresses the diagnosed failures. Rules:
76
76
 
77
77
  ## Step 4: Push to staging
78
78
 
79
- Update the agent's prompt in `agents/registry.json` (or the corresponding `promptFile`) with a staging marker comment. Tag the version by writing to `.cx/decisions/prompt-staging-{agent}-{date}.md`.
79
+ Update the agent's prompt in `specialists/registry.json` (or the corresponding `promptFile`) with a staging marker comment. Tag the version by writing to `.cx/decisions/prompt-staging-{agent}-{date}.md`.
80
80
 
81
81
  Log the candidate prompt as a span attribute on a test run batch using `cx_trace`. Tag spans with `promptVersion: staging-{timestamp}` and score them with `cx_score` as traces complete.
82
82
 
@@ -0,0 +1,98 @@
1
+ <!--
2
+ skills/exploration/dependency-graph-reading.md (Dependency Graph Reading)
3
+ Use when auditing a project's supply chain risk, evaluating upgrade paths,
4
+ or assessing the risk surface of a new codebase. Covers npm/cargo/maven
5
+ lockfile interpretation, transitive dependency patterns, and risk signals.
6
+ -->
7
+
8
+ # Dependency Graph Reading
9
+
10
+ Use when assessing the risk surface of a project's dependencies, planning upgrades, or evaluating a new codebase's supply chain posture.
11
+
12
+ ## npm / package-lock.json
13
+
14
+ The lockfile is the authoritative record of what actually runs in production.
15
+
16
+ **What `package-lock.json` tells you:**
17
+ - Exact resolved versions of every dependency (direct + transitive).
18
+ - The registry URL each package resolved from (watch for non-npm registry sources).
19
+ - Integrity hashes for tamper detection (missing or mismatched hashes are a supply chain signal).
20
+
21
+ **Commands:**
22
+ ```bash
23
+ # Tree of direct dependencies with their transitive deps
24
+ npm ls --all --depth=5 <package>
25
+
26
+ # Why is this package installed? (shows the resolution path)
27
+ npm ls <package>
28
+
29
+ # Outdated direct dependencies
30
+ npm outdated
31
+
32
+ # Audit for known CVEs
33
+ npm audit --json | jq '.vulnerabilities | to_entries | sort_by(.value.severity) | reverse[]'
34
+ ```
35
+
36
+ **Risk signals in the lockfile:**
37
+ - Packages sourced from `github:`, `file:`, or private registries, higher supply-chain risk.
38
+ - Very old `lockfileVersion` (v1), may not capture integrity hashes properly.
39
+ - Transitive dependencies with `requires: {}` instead of resolved versions, resolution ambiguity.
40
+
41
+ ## Cargo.lock (Rust)
42
+
43
+ Cargo maintains a flat lockfile with explicit source declarations.
44
+
45
+ **Key fields:**
46
+ - `[[package]]`: each entry is a resolved crate version.
47
+ - `source = "registry+..."`: standard crates.io source. `source = "git+..."`: git dependency (less auditable).
48
+ - `checksum`: SHA256 of the downloaded crate.
49
+
50
+ **Commands:**
51
+ ```bash
52
+ # Dependency tree with feature flags
53
+ cargo tree
54
+
55
+ # Find all versions of a crate (version conflicts)
56
+ cargo tree -d
57
+
58
+ # Audit for known vulnerabilities
59
+ cargo audit
60
+ ```
61
+
62
+ ## Maven / gradle (JVM)
63
+
64
+ The effective POM / resolved configuration tells you what actually runs.
65
+
66
+ ```bash
67
+ # Full dependency tree with transitive
68
+ mvn dependency:tree -Dverbose
69
+
70
+ # Resolved versions for a specific scope
71
+ mvn dependency:list -Dscope=compile
72
+
73
+ # Gradle
74
+ ./gradlew dependencies --configuration runtimeClasspath
75
+ ```
76
+
77
+ **Risk signals:**
78
+ - Version range declarations (`[1.0,)`), means any future version could be resolved at build time.
79
+ - `SNAPSHOT` versions in non-snapshot builds, unstable artifact references.
80
+ - Duplicate versions of the same artifact (version conflict, one wins, one is silently dropped).
81
+
82
+ ## Cross-ecosystem risk signals
83
+
84
+ | Signal | Risk | Action |
85
+ |---|---|---|
86
+ | Dependency with <100 weekly downloads | Supply chain (abandoned / obscure) | Evaluate inlining or replacing |
87
+ | Dependency last published >2 years ago | Maintenance risk | Check for forks or replacements |
88
+ | Deep transitive chain (>10 levels) | Blast radius | Map critical path; isolate |
89
+ | Single maintainer with no 2FA | Takeover risk | Monitor; prefer alternatives with org ownership |
90
+ | Package name squatting (typosquat) | Injection risk | Verify exact package name + registry source |
91
+ | Wildcard `*` version pin | Reproducibility failure | Lock to exact version |
92
+
93
+ ## Practical audit workflow
94
+
95
+ 1. **Enumerate the blast radius**: which packages, if compromised, have write access to your production environment or sensitive data? These are the highest-risk nodes.
96
+ 2. **Check freshness on the critical path**: `npm audit`, `cargo audit`, `snyk test`. Focus on high/critical severity findings that affect runtime code (not devDependencies).
97
+ 3. **Identify orphaned dependencies**: packages no code imports directly but appear in the lockfile. May be safe to remove, reducing surface area.
98
+ 4. **Check for version conflicts**: multiple resolved versions of the same package indicate an incomplete deduplication. Usually benign; occasionally a security issue if an old insecure version is in the tree.
@@ -0,0 +1,71 @@
1
+ <!--
2
+ skills/exploration/tracer-bullet-method.md (Tracer Bullet Method)
3
+ Use when starting implementation in an unfamiliar or complex system.
4
+ A tracer bullet is a minimal end-to-end path through the system,
5
+ real code, real infrastructure, real calls, before any deep work begins.
6
+ -->
7
+
8
+ # Tracer Bullet Method
9
+
10
+ Use when beginning implementation in a new system, new integration, or new architectural layer. A tracer bullet is not a prototype; it is the thinnest possible complete path through the real system.
11
+
12
+ ## What a tracer bullet is
13
+
14
+ A tracer bullet hits every layer of the architecture, UI, API, business logic, data store, external service, with real code and real infrastructure. It is not a mock; it is a thin vertical slice.
15
+
16
+ **The goal:** prove that the end-to-end path works before committing to the full implementation.
17
+
18
+ **What it is not:**
19
+ - A prototype (a prototype tests a concept; a tracer bullet tests the infrastructure).
20
+ - A spike (a spike is time-boxed exploration; a tracer bullet produces code you keep).
21
+ - A stub (stubs replace real behavior; the tracer bullet uses real behavior).
22
+
23
+ ## When to use it
24
+
25
+ Use a tracer bullet when:
26
+ - You're integrating two systems for the first time and don't know where the friction is.
27
+ - You're adding a new capability to a system with unfamiliar internals.
28
+ - The architecture requires multiple components to cooperate, and you need to verify the cooperation works before building each component out.
29
+ - You're prototyping a new data model that touches multiple layers.
30
+
31
+ ## How to fire one
32
+
33
+ **Step 1: Define the thinnest path**
34
+
35
+ What is the simplest possible user action (or API call) that touches every layer you need to implement? This is your target path.
36
+
37
+ Example: for a new feature "user uploads an image":
38
+ - Thinnest path: user sends a single 1KB image, it is stored, and the storage URL is returned.
39
+
40
+ **Step 2: Identify every seam**
41
+
42
+ Map the layers the path crosses:
43
+ - Presentation: what receives the request?
44
+ - Transport: what format does the request take?
45
+ - Validation: what checks the input?
46
+ - Business logic: what transforms or routes it?
47
+ - Persistence: what stores it?
48
+ - External services: what calls are made?
49
+
50
+ **Step 3: Implement one thin vertical slice**
51
+
52
+ Write just enough code to make the tracer path work end-to-end. No error handling, no edge cases, no optimization. The only question is: does the path close?
53
+
54
+ **Step 4: Verify closure**
55
+
56
+ Run it. Check each layer produced the expected artifact. A tracer bullet that doesn't reach the end is valuable, it tells you exactly where the friction is before you've invested in the full implementation.
57
+
58
+ **Step 5: Expand**
59
+
60
+ Once the path closes, add error handling, validation, edge cases, and tests. You are now implementing on a system you know works.
61
+
62
+ ## Tracer bullet vs. walking skeleton
63
+
64
+ A walking skeleton (Cockburn) is similar but broader: the minimum architecture that demonstrates a complete feature. A tracer bullet is more specific, it is a single request path through an existing or new system. The terms are used interchangeably in practice; the distinction matters less than the principle.
65
+
66
+ ## Anti-patterns
67
+
68
+ - **Mocking too much**: if the tracer bullet uses mocked services, you haven't proved the real path works. Mocks are fine for unit tests, not for the tracer.
69
+ - **Doing too much at once**: if the tracer path covers 5 new capabilities, you won't know which one caused the failure. Fire one bullet at a time.
70
+ - **Skipping the tracer for "simple" changes**: the changes most likely to surprise you are the ones that seem simple. Fire anyway.
71
+ - **Throwing the bullet away**: a tracer bullet is production-quality code on a thin path. It is not throwaway. The skeleton expands from it.
@@ -0,0 +1,91 @@
1
+ <!--
2
+ skills/exploration/unknown-codebase-onboarding.md (Unknown Codebase Onboarding)
3
+ Use when a specialist is operating in a new or unfamiliar codebase and
4
+ needs a systematic path from zero context to productive contribution.
5
+ Covers first-hour, first-day, and first-week checkpoints.
6
+ -->
7
+
8
+ # Unknown Codebase Onboarding
9
+
10
+ Use when entering an unfamiliar codebase for the first time, or when the task requires understanding a system you haven't touched before.
11
+
12
+ ## First-hour checklist (orientation)
13
+
14
+ The goal in the first hour is to build a mental model of the system's shape, not to understand every detail.
15
+
16
+ **1. Start with the entry points:**
17
+ ```bash
18
+ # What runs? What are the main binaries / services?
19
+ ls bin/ scripts/ cmd/ src/main*
20
+
21
+ # What does the README say is the first command to run?
22
+ cat README.md | head -60
23
+ ```
24
+
25
+ **2. Understand the dependency graph:**
26
+ ```bash
27
+ cat package.json | jq '{name, version, scripts, dependencies: (.dependencies | keys), devDependencies: (.devDependencies | keys)}'
28
+ # or: cat Cargo.toml, go.mod, pom.xml, requirements.txt
29
+ ```
30
+
31
+ **3. Find the test suite:**
32
+ ```bash
33
+ ls tests/ test/ spec/ __tests__/
34
+ # What's the test runner? How do you run it?
35
+ grep -i test package.json | head -5
36
+ ```
37
+
38
+ **4. Find the data model:**
39
+ ```bash
40
+ ls db/ schema/ migrations/ prisma/ models/
41
+ # For a service: what does it store, and how is it structured?
42
+ ```
43
+
44
+ **5. Trace one request end-to-end:**
45
+ Pick the simplest happy path through the system and trace it from entry point to output. This reveals the architectural seams and where decisions live.
46
+
47
+ ## First-day checklist (orientation to action)
48
+
49
+ **Identify the change surface:**
50
+ - What is the most recently changed directory? (`git log --name-only --pretty=format: | sort | uniq -c | sort -rn | head -20`)
51
+ - What directories do most PRs touch? High-churn areas are where active work lives.
52
+
53
+ **Understand the invariants:**
54
+ - Are there comments marked `INVARIANT`, `IMPORTANT`, `DO NOT CHANGE`, or `CRITICAL`? These are the load-bearing beliefs.
55
+ - Are there tests marked `regression` or referencing bug IDs? These document specific failure modes.
56
+
57
+ **Find the configuration layer:**
58
+ ```bash
59
+ # Environment variables the system reads
60
+ grep -r 'process\.env\.\|os\.environ\|getenv' --include='*.js' --include='*.py' --include='*.go' | grep -v test | grep -v node_modules
61
+ ```
62
+
63
+ **Identify the seams:**
64
+ - Where does the system boundary end? What does it call out to (APIs, databases, queues)?
65
+ - What are the contracts at those seams (types, schemas, documented expectations)?
66
+
67
+ ## First-week checklist (productive contribution)
68
+
69
+ **Build a dependency map:**
70
+ - Which modules are leaf nodes (pure logic, few dependencies)?
71
+ - Which modules are hubs (imported everywhere, high coupling)?
72
+ - Leaf nodes are safe to modify; hub changes have blast radius.
73
+
74
+ **Read the ADRs and postmortems:**
75
+ - Architecture decisions (`docs/adr/`, `decisions/`, `DECISIONS.md`) explain why the code is shaped the way it is.
76
+ - Postmortems explain what has already broken and how the system defends against it. Don't break a defense you don't understand.
77
+
78
+ **Find the test coverage gaps:**
79
+ - Which modules have no tests? These are the highest-risk areas to modify.
80
+ - Which tests are flaky (retry logic, `skip`, `todo`)? Unreliable tests produce unreliable confidence.
81
+
82
+ **Validate your mental model:**
83
+ - Before making a change, explain to a colleague (or write down) what the change does and why it won't break anything. If you can't explain it, you don't understand it yet.
84
+ - After making a change, check: did any behavior you didn't intend to change actually change? Diff the test output.
85
+
86
+ ## Anti-patterns when onboarding
87
+
88
+ - **Deep-diving before breadth-surveying**: the codebase is large; get the shape before the details.
89
+ - **Modifying without understanding invariants**: breaking a behavior that was intentional because you didn't know why it existed.
90
+ - **Trusting test coverage numbers**: 80% coverage means 80% of lines are executed, not that 80% of behaviors are tested.
91
+ - **Ignoring comments with bug IDs**: comments that say `// workaround for PROJ-123` are load-bearing. Look up the issue before removing them.