@geraldmaron/construct 1.0.0
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.
- package/.env.example +69 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/agents/prompts/cx-accessibility.md +29 -0
- package/agents/prompts/cx-ai-engineer.md +31 -0
- package/agents/prompts/cx-architect.md +40 -0
- package/agents/prompts/cx-business-strategist.md +25 -0
- package/agents/prompts/cx-data-analyst.md +30 -0
- package/agents/prompts/cx-data-engineer.md +32 -0
- package/agents/prompts/cx-debugger.md +27 -0
- package/agents/prompts/cx-designer.md +33 -0
- package/agents/prompts/cx-devil-advocate.md +36 -0
- package/agents/prompts/cx-docs-keeper.md +50 -0
- package/agents/prompts/cx-engineer.md +36 -0
- package/agents/prompts/cx-evaluator.md +26 -0
- package/agents/prompts/cx-explorer.md +38 -0
- package/agents/prompts/cx-legal-compliance.md +25 -0
- package/agents/prompts/cx-operations.md +28 -0
- package/agents/prompts/cx-orchestrator.md +30 -0
- package/agents/prompts/cx-platform-engineer.md +25 -0
- package/agents/prompts/cx-product-manager.md +30 -0
- package/agents/prompts/cx-qa.md +42 -0
- package/agents/prompts/cx-rd-lead.md +25 -0
- package/agents/prompts/cx-release-manager.md +31 -0
- package/agents/prompts/cx-researcher.md +31 -0
- package/agents/prompts/cx-reviewer.md +36 -0
- package/agents/prompts/cx-security.md +37 -0
- package/agents/prompts/cx-sre.md +26 -0
- package/agents/prompts/cx-test-automation.md +35 -0
- package/agents/prompts/cx-trace-reviewer.md +76 -0
- package/agents/prompts/cx-ux-researcher.md +30 -0
- package/agents/registry.json +879 -0
- package/agents/teams.json +94 -0
- package/bin/construct +613 -0
- package/commands/build/feature.md +21 -0
- package/commands/build/fix.md +20 -0
- package/commands/design/access.md +19 -0
- package/commands/design/flow.md +16 -0
- package/commands/design/ui.md +18 -0
- package/commands/measure/experiment.md +18 -0
- package/commands/measure/metrics.md +18 -0
- package/commands/measure/results.md +18 -0
- package/commands/plan/api.md +19 -0
- package/commands/plan/challenge.md +18 -0
- package/commands/plan/decide.md +20 -0
- package/commands/plan/feature.md +49 -0
- package/commands/plan/requirements.md +24 -0
- package/commands/remember/context.md +26 -0
- package/commands/remember/handoff.md +19 -0
- package/commands/remember/runbook.md +20 -0
- package/commands/review/code.md +21 -0
- package/commands/review/quality.md +19 -0
- package/commands/review/security.md +16 -0
- package/commands/ship/ready.md +19 -0
- package/commands/ship/release.md +19 -0
- package/commands/ship/status.md +18 -0
- package/commands/understand/docs.md +22 -0
- package/commands/understand/research.md +22 -0
- package/commands/understand/this.md +23 -0
- package/commands/understand/why.md +18 -0
- package/commands/work/clean.md +32 -0
- package/commands/work/drive.md +69 -0
- package/commands/work/optimize-prompts.md +46 -0
- package/commands/work/parallel-review.md +40 -0
- package/db/migrations/001_init.sql +38 -0
- package/langfuse/docker-compose.yml +82 -0
- package/lib/audit-skills.mjs +118 -0
- package/lib/auto-docs.mjs +293 -0
- package/lib/cli-commands.mjs +409 -0
- package/lib/codex-config.mjs +114 -0
- package/lib/comment-lint.mjs +191 -0
- package/lib/completions.mjs +170 -0
- package/lib/context-state.mjs +99 -0
- package/lib/cost.mjs +213 -0
- package/lib/distill.mjs +403 -0
- package/lib/efficiency.mjs +139 -0
- package/lib/env-config.mjs +53 -0
- package/lib/eval-harness.mjs +59 -0
- package/lib/features.mjs +209 -0
- package/lib/headhunt.mjs +597 -0
- package/lib/hooks/adaptive-lint.mjs +112 -0
- package/lib/hooks/agent-tracker.mjs +50 -0
- package/lib/hooks/bootstrap-guard.mjs +90 -0
- package/lib/hooks/comment-lint.mjs +26 -0
- package/lib/hooks/config-protection.mjs +52 -0
- package/lib/hooks/console-warn.mjs +43 -0
- package/lib/hooks/context-window-recovery.mjs +90 -0
- package/lib/hooks/continuation-enforcer.mjs +72 -0
- package/lib/hooks/dep-audit.mjs +155 -0
- package/lib/hooks/drive-guard.mjs +89 -0
- package/lib/hooks/edit-accumulator.mjs +36 -0
- package/lib/hooks/edit-error-recovery.mjs +46 -0
- package/lib/hooks/edit-guard.mjs +109 -0
- package/lib/hooks/env-check.mjs +80 -0
- package/lib/hooks/guard-bash.mjs +83 -0
- package/lib/hooks/mcp-audit.mjs +57 -0
- package/lib/hooks/mcp-health-check.mjs +51 -0
- package/lib/hooks/mcp-task-scope.mjs +47 -0
- package/lib/hooks/model-fallback.mjs +105 -0
- package/lib/hooks/pre-compact.mjs +212 -0
- package/lib/hooks/pre-push-gate.mjs +129 -0
- package/lib/hooks/read-tracker.mjs +129 -0
- package/lib/hooks/registry-sync.mjs +23 -0
- package/lib/hooks/scan-secrets.mjs +76 -0
- package/lib/hooks/session-start.mjs +95 -0
- package/lib/hooks/stop-notify.mjs +191 -0
- package/lib/hooks/stop-typecheck.mjs +83 -0
- package/lib/hooks/task-completed-guard.mjs +43 -0
- package/lib/hooks/teammate-idle-guard.mjs +54 -0
- package/lib/hooks/workflow-guard.mjs +62 -0
- package/lib/host-capabilities.mjs +123 -0
- package/lib/init-docs.mjs +382 -0
- package/lib/mcp/server.mjs +1123 -0
- package/lib/mcp-catalog.json +243 -0
- package/lib/mcp-manager.mjs +433 -0
- package/lib/mcp-platform-config.mjs +104 -0
- package/lib/model-router.mjs +810 -0
- package/lib/opencode-config.mjs +44 -0
- package/lib/opencode-runtime-plugin.mjs +909 -0
- package/lib/opencode-telemetry.mjs +433 -0
- package/lib/orchestration-policy.mjs +258 -0
- package/lib/prompt-composer.mjs +196 -0
- package/lib/prompt-metadata.mjs +68 -0
- package/lib/review.mjs +429 -0
- package/lib/role-preload.mjs +52 -0
- package/lib/schemas/decision.json +50 -0
- package/lib/schemas/implementation.json +51 -0
- package/lib/schemas/review-report.json +32 -0
- package/lib/schemas/test-report.json +43 -0
- package/lib/server/index.mjs +334 -0
- package/lib/server/static/app.js +841 -0
- package/lib/server/static/index.html +820 -0
- package/lib/service-manager.mjs +225 -0
- package/lib/setup.mjs +319 -0
- package/lib/status.mjs +707 -0
- package/lib/storage/backend.mjs +44 -0
- package/lib/storage/embeddings.mjs +70 -0
- package/lib/storage/hybrid-query.mjs +184 -0
- package/lib/storage/sql-store.mjs +55 -0
- package/lib/storage/state-source.mjs +101 -0
- package/lib/storage/sync.mjs +164 -0
- package/lib/storage/vector-store.mjs +59 -0
- package/lib/telemetry/backends/langfuse.mjs +58 -0
- package/lib/telemetry/backends/noop.mjs +16 -0
- package/lib/telemetry/langfuse-ingest.mjs +108 -0
- package/lib/telemetry/langfuse-model-sync.mjs +270 -0
- package/lib/telemetry/team-rollup.mjs +143 -0
- package/lib/toolkit-env.mjs +30 -0
- package/lib/validator.mjs +181 -0
- package/lib/workflow-state.mjs +794 -0
- package/package.json +53 -0
- package/personas/construct.md +94 -0
- package/platforms/claude/CLAUDE.md +30 -0
- package/platforms/claude/settings.template.json +472 -0
- package/platforms/opencode/config.template.json +65 -0
- package/platforms/opencode/plugins/construct-fallback.js +5 -0
- package/platforms/opencode/sync-config.mjs +69 -0
- package/rules/common/agents.md +55 -0
- package/rules/common/code-review.md +129 -0
- package/rules/common/coding-style.md +95 -0
- package/rules/common/comments.md +139 -0
- package/rules/common/cx-agent-routing.md +61 -0
- package/rules/common/cx-skill-routing.md +11 -0
- package/rules/common/development-workflow.md +49 -0
- package/rules/common/git-workflow.md +29 -0
- package/rules/common/hooks.md +35 -0
- package/rules/common/patterns.md +36 -0
- package/rules/common/performance.md +71 -0
- package/rules/common/security.md +34 -0
- package/rules/common/testing.md +62 -0
- package/rules/golang/coding-style.md +37 -0
- package/rules/golang/hooks.md +22 -0
- package/rules/golang/patterns.md +50 -0
- package/rules/golang/security.md +39 -0
- package/rules/golang/testing.md +36 -0
- package/rules/python/coding-style.md +47 -0
- package/rules/python/hooks.md +24 -0
- package/rules/python/patterns.md +44 -0
- package/rules/python/security.md +35 -0
- package/rules/python/testing.md +43 -0
- package/rules/swift/coding-style.md +52 -0
- package/rules/swift/hooks.md +25 -0
- package/rules/swift/patterns.md +71 -0
- package/rules/swift/security.md +38 -0
- package/rules/swift/testing.md +50 -0
- package/rules/typescript/coding-style.md +204 -0
- package/rules/typescript/hooks.md +27 -0
- package/rules/typescript/patterns.md +57 -0
- package/rules/typescript/security.md +33 -0
- package/rules/typescript/testing.md +23 -0
- package/rules/web/coding-style.md +101 -0
- package/rules/web/design-quality.md +68 -0
- package/rules/web/hooks.md +125 -0
- package/rules/web/patterns.md +84 -0
- package/rules/web/performance.md +69 -0
- package/rules/web/security.md +62 -0
- package/rules/web/testing.md +60 -0
- package/rules/zh/README.md +113 -0
- package/rules/zh/agents.md +55 -0
- package/rules/zh/code-review.md +129 -0
- package/rules/zh/coding-style.md +53 -0
- package/rules/zh/development-workflow.md +49 -0
- package/rules/zh/git-workflow.md +29 -0
- package/rules/zh/hooks.md +35 -0
- package/rules/zh/patterns.md +36 -0
- package/rules/zh/performance.md +60 -0
- package/rules/zh/security.md +34 -0
- package/rules/zh/testing.md +34 -0
- package/skills/ai/agent-dev.md +102 -0
- package/skills/ai/llm-security.md +105 -0
- package/skills/ai/ml-ops.md +169 -0
- package/skills/ai/orchestration-workflow.md +87 -0
- package/skills/ai/prompt-and-eval.md +114 -0
- package/skills/ai/prompt-optimizer.md +114 -0
- package/skills/ai/rag-system.md +104 -0
- package/skills/architecture/api-design.md +100 -0
- package/skills/architecture/caching.md +101 -0
- package/skills/architecture/cloud-native.md +97 -0
- package/skills/architecture/message-queue.md +100 -0
- package/skills/architecture/security-arch.md +105 -0
- package/skills/development/cpp.md +127 -0
- package/skills/development/go.md +129 -0
- package/skills/development/java.md +134 -0
- package/skills/development/kotlin.md +140 -0
- package/skills/development/mobile-crossplatform.md +144 -0
- package/skills/development/python.md +109 -0
- package/skills/development/rust.md +127 -0
- package/skills/development/shell.md +127 -0
- package/skills/development/swift.md +129 -0
- package/skills/development/typescript.md +129 -0
- package/skills/devops/ci-cd.md +108 -0
- package/skills/devops/containerization.md +106 -0
- package/skills/devops/cost-optimization.md +105 -0
- package/skills/devops/data-engineering.md +181 -0
- package/skills/devops/database.md +95 -0
- package/skills/devops/dependency-management.md +91 -0
- package/skills/devops/devsecops.md +96 -0
- package/skills/devops/git-workflow.md +100 -0
- package/skills/devops/incident-response.md +167 -0
- package/skills/devops/monorepo.md +87 -0
- package/skills/devops/observability.md +103 -0
- package/skills/devops/performance.md +104 -0
- package/skills/devops/testing.md +104 -0
- package/skills/docs/adr-workflow.md +32 -0
- package/skills/docs/backlog-proposal-workflow.md +19 -0
- package/skills/docs/customer-profile-workflow.md +22 -0
- package/skills/docs/evidence-ingest-workflow.md +23 -0
- package/skills/docs/init-docs.md +160 -0
- package/skills/docs/init-project.md +50 -0
- package/skills/docs/prd-workflow.md +57 -0
- package/skills/docs/prfaq-workflow.md +25 -0
- package/skills/docs/product-intelligence-review.md +22 -0
- package/skills/docs/product-intelligence-workflow.md +62 -0
- package/skills/docs/product-signal-workflow.md +27 -0
- package/skills/docs/research-workflow.md +28 -0
- package/skills/docs/runbook-workflow.md +24 -0
- package/skills/exploration/repo-map.md +297 -0
- package/skills/frameworks/django.md +159 -0
- package/skills/frameworks/nextjs.md +148 -0
- package/skills/frameworks/react.md +132 -0
- package/skills/frameworks/spring-boot.md +165 -0
- package/skills/frontend-design/accessibility.md +153 -0
- package/skills/frontend-design/component-patterns.md +111 -0
- package/skills/frontend-design/engineering.md +122 -0
- package/skills/frontend-design/state-management.md +118 -0
- package/skills/frontend-design/ui-aesthetics.md +102 -0
- package/skills/frontend-design/ux-principles.md +126 -0
- package/skills/quality-gates/review-work.md +90 -0
- package/skills/quality-gates/verify-change.md +94 -0
- package/skills/quality-gates/verify-module.md +96 -0
- package/skills/quality-gates/verify-quality.md +93 -0
- package/skills/quality-gates/verify-security.md +95 -0
- package/skills/roles/architect.ai-systems.md +37 -0
- package/skills/roles/architect.data.md +37 -0
- package/skills/roles/architect.enterprise.md +37 -0
- package/skills/roles/architect.integration.md +37 -0
- package/skills/roles/architect.md +68 -0
- package/skills/roles/architect.platform.md +37 -0
- package/skills/roles/data-analyst.experiment.md +37 -0
- package/skills/roles/data-analyst.md +68 -0
- package/skills/roles/data-analyst.product-intelligence.md +37 -0
- package/skills/roles/data-analyst.product.md +37 -0
- package/skills/roles/data-analyst.telemetry.md +37 -0
- package/skills/roles/data-engineer.pipeline.md +37 -0
- package/skills/roles/data-engineer.vector-retrieval.md +37 -0
- package/skills/roles/data-engineer.warehouse.md +37 -0
- package/skills/roles/debugger.md +68 -0
- package/skills/roles/designer.accessibility.md +43 -0
- package/skills/roles/designer.md +68 -0
- package/skills/roles/engineer.ai.md +49 -0
- package/skills/roles/engineer.data.md +49 -0
- package/skills/roles/engineer.md +85 -0
- package/skills/roles/engineer.platform.md +49 -0
- package/skills/roles/operator.docs.md +43 -0
- package/skills/roles/operator.md +68 -0
- package/skills/roles/operator.release.md +43 -0
- package/skills/roles/operator.sre.md +43 -0
- package/skills/roles/orchestrator.md +66 -0
- package/skills/roles/product-manager.ai-product.md +37 -0
- package/skills/roles/product-manager.business-strategy.md +43 -0
- package/skills/roles/product-manager.enterprise.md +37 -0
- package/skills/roles/product-manager.growth.md +37 -0
- package/skills/roles/product-manager.md +67 -0
- package/skills/roles/product-manager.platform.md +37 -0
- package/skills/roles/product-manager.product.md +37 -0
- package/skills/roles/qa.ai-eval.md +37 -0
- package/skills/roles/qa.api-contract.md +37 -0
- package/skills/roles/qa.data-pipeline.md +37 -0
- package/skills/roles/qa.md +68 -0
- package/skills/roles/qa.test-automation.md +49 -0
- package/skills/roles/qa.web-ui.md +37 -0
- package/skills/roles/researcher.explorer.md +43 -0
- package/skills/roles/researcher.md +68 -0
- package/skills/roles/researcher.ux.md +43 -0
- package/skills/roles/reviewer.devil-advocate.md +43 -0
- package/skills/roles/reviewer.evaluator.md +43 -0
- package/skills/roles/reviewer.md +68 -0
- package/skills/roles/reviewer.trace.md +43 -0
- package/skills/roles/security.ai.md +37 -0
- package/skills/roles/security.appsec.md +37 -0
- package/skills/roles/security.cloud.md +37 -0
- package/skills/roles/security.legal-compliance.md +49 -0
- package/skills/roles/security.md +68 -0
- package/skills/roles/security.privacy.md +37 -0
- package/skills/roles/security.supply-chain.md +37 -0
- package/skills/routing.md +139 -0
- package/skills/security/blue-team.md +83 -0
- package/skills/security/code-audit.md +94 -0
- package/skills/security/pentest.md +84 -0
- package/skills/security/red-team.md +81 -0
- package/skills/security/threat-intel.md +89 -0
- package/skills/security/vuln-research.md +87 -0
- package/skills/utility/clean-code.md +165 -0
- package/sync-agents.mjs +899 -0
- package/templates/docs/adr.md +27 -0
- package/templates/docs/backlog-proposal.md +26 -0
- package/templates/docs/customer-profile.md +34 -0
- package/templates/docs/evidence-brief.md +37 -0
- package/templates/docs/incident-report.md +46 -0
- package/templates/docs/memo.md +27 -0
- package/templates/docs/meta-prd.md +56 -0
- package/templates/docs/one-pager.md +24 -0
- package/templates/docs/prd-business.md +61 -0
- package/templates/docs/prd-platform.md +62 -0
- package/templates/docs/prd.md +47 -0
- package/templates/docs/prfaq.md +31 -0
- package/templates/docs/product-intelligence-report.md +31 -0
- package/templates/docs/research-brief.md +29 -0
- package/templates/docs/rfc-platform.md +60 -0
- package/templates/docs/rfc.md +46 -0
- package/templates/docs/runbook.md +33 -0
- package/templates/docs/signal-brief.md +25 -0
package/lib/headhunt.mjs
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/headhunt.mjs — <one-line purpose>
|
|
4
|
+
*
|
|
5
|
+
* <2–6 line summary.>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { createInterface } from 'node:readline';
|
|
12
|
+
|
|
13
|
+
import { loadWorkflow, updateTask } from './workflow-state.mjs';
|
|
14
|
+
|
|
15
|
+
const OVERLAY_DIRNAME = 'domain-overlays';
|
|
16
|
+
const PROMOTION_DIRNAME = 'promotion-requests';
|
|
17
|
+
const TEAMS_FILE = 'agents/teams.json';
|
|
18
|
+
|
|
19
|
+
function loadTeamTemplates(root) {
|
|
20
|
+
try {
|
|
21
|
+
const teamsPath = path.join(root, TEAMS_FILE);
|
|
22
|
+
return JSON.parse(fs.readFileSync(teamsPath, 'utf8'));
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function listTeamTemplates(cwd = process.cwd()) {
|
|
29
|
+
const root = findConstructRoot(cwd);
|
|
30
|
+
const teams = loadTeamTemplates(root);
|
|
31
|
+
if (!teams) { process.stdout.write('No teams.json found.\n'); return; }
|
|
32
|
+
process.stdout.write('Available team templates:\n');
|
|
33
|
+
for (const [name, tpl] of Object.entries(teams.templates)) {
|
|
34
|
+
process.stdout.write(` ${name.padEnd(20)} ${tpl.description}\n`);
|
|
35
|
+
process.stdout.write(` Members: ${tpl.members.join(', ')}\n`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function slugify(value) {
|
|
40
|
+
return String(value || '')
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
43
|
+
.replace(/^-+|-+$/g, '') || 'overlay';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseArgs(args = []) {
|
|
47
|
+
const flags = {};
|
|
48
|
+
const positional = [];
|
|
49
|
+
for (const arg of args) {
|
|
50
|
+
if (arg.startsWith('--')) {
|
|
51
|
+
const [key, ...rest] = arg.slice(2).split('=');
|
|
52
|
+
flags[key] = rest.length ? rest.join('=') : true;
|
|
53
|
+
} else {
|
|
54
|
+
positional.push(arg);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { flags, positional };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function promptQuestion(rl, question) {
|
|
61
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function classifyOverlay(objective = '', explicitTeam = '') {
|
|
65
|
+
if (explicitTeam) {
|
|
66
|
+
return {
|
|
67
|
+
focus: 'custom',
|
|
68
|
+
attachTo: explicitTeam.split(',').map((value) => value.trim()).filter(Boolean),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const text = objective.toLowerCase();
|
|
73
|
+
if (/security|policy|compliance|secrets|iam|threat|audit/.test(text)) {
|
|
74
|
+
return { focus: 'security', attachTo: ['cx-security', 'cx-architect'] };
|
|
75
|
+
}
|
|
76
|
+
if (/implement|build|module|deploy|pipeline|ci\/cd|apply|code/.test(text)) {
|
|
77
|
+
return { focus: 'implementation', attachTo: ['cx-engineer', 'cx-architect'] };
|
|
78
|
+
}
|
|
79
|
+
if (/runbook|docs|documentation|guide|onboard/.test(text)) {
|
|
80
|
+
return { focus: 'documentation', attachTo: ['cx-docs-keeper', 'cx-researcher'] };
|
|
81
|
+
}
|
|
82
|
+
if (/research|compare|recent|latest|best practices|upgrade|migration/.test(text)) {
|
|
83
|
+
return { focus: 'research', attachTo: ['cx-researcher', 'cx-architect'] };
|
|
84
|
+
}
|
|
85
|
+
return { focus: 'architecture', attachTo: ['cx-architect', 'cx-researcher'] };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildOverlayPrompt({ domain, objective, scope, permanence, focus, freshness }) {
|
|
89
|
+
return [
|
|
90
|
+
`Domain overlay: ${domain}`,
|
|
91
|
+
`Objective: ${objective}`,
|
|
92
|
+
scope ? `Scope: ${scope}` : null,
|
|
93
|
+
`Permanence: ${permanence}`,
|
|
94
|
+
`Primary focus: ${focus}`,
|
|
95
|
+
`Freshness requirement: ${freshness}`,
|
|
96
|
+
'Rules:',
|
|
97
|
+
'- Treat this as an internal Construct overlay, not a permanent public role.',
|
|
98
|
+
'- Start research-first and citation-first using primary sources.',
|
|
99
|
+
'- Prefer official docs, release notes, provider docs, tracked issues, then ecosystem references.',
|
|
100
|
+
'- Separate confirmed facts from inference.',
|
|
101
|
+
'- Respect existing Construct workflow, security, and validation boundaries.',
|
|
102
|
+
'- Do not broaden tool permissions just because the domain is specialized.',
|
|
103
|
+
].filter(Boolean).join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildPromotionGates() {
|
|
107
|
+
return [
|
|
108
|
+
'Explicit user request to save/promote capability',
|
|
109
|
+
'Demonstrated reuse beyond a one-off task',
|
|
110
|
+
'Clear scope, non-goals, and maintainer owner',
|
|
111
|
+
'Fits existing Construct routing without exposing internals',
|
|
112
|
+
'Security/risk review passes if domain touches infra/auth/data',
|
|
113
|
+
'Docs and validation plan exist before any persistent registry changes',
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ensureDir(dir) {
|
|
118
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function writeJson(filePath, value) {
|
|
122
|
+
ensureDir(path.dirname(filePath));
|
|
123
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function writeText(filePath, value) {
|
|
127
|
+
ensureDir(path.dirname(filePath));
|
|
128
|
+
fs.writeFileSync(filePath, `${value.trim()}\n`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readJsonIfExists(filePath) {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function findConstructRoot(startPath = process.cwd()) {
|
|
140
|
+
let current = path.resolve(startPath);
|
|
141
|
+
while (true) {
|
|
142
|
+
if (fs.existsSync(path.join(current, '.cx'))) return current;
|
|
143
|
+
const parent = path.dirname(current);
|
|
144
|
+
if (parent === current) return path.resolve(startPath);
|
|
145
|
+
current = parent;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function listJsonFiles(dir) {
|
|
150
|
+
try {
|
|
151
|
+
return fs.readdirSync(dir)
|
|
152
|
+
.filter((file) => file.endsWith('.json'))
|
|
153
|
+
.sort()
|
|
154
|
+
.map((file) => path.join(dir, file));
|
|
155
|
+
} catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getOverlayDir(cwd) {
|
|
161
|
+
return path.join(cwd, '.cx', OVERLAY_DIRNAME);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getPromotionDir(cwd) {
|
|
165
|
+
return path.join(cwd, '.cx', PROMOTION_DIRNAME);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function loadOverlayRecords(cwd) {
|
|
169
|
+
return listJsonFiles(getOverlayDir(cwd))
|
|
170
|
+
.map((filePath) => ({ filePath, record: readJsonIfExists(filePath) }))
|
|
171
|
+
.filter((entry) => entry.record && entry.record.type === 'domain-overlay');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function loadPromotionRecords(cwd) {
|
|
175
|
+
return listJsonFiles(getPromotionDir(cwd))
|
|
176
|
+
.map((filePath) => ({ filePath, record: readJsonIfExists(filePath) }))
|
|
177
|
+
.filter((entry) => entry.record && entry.record.type === 'promotion-request');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function unique(values = []) {
|
|
181
|
+
return [...new Set((values || []).filter(Boolean))];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function findWorkflowTaskForOverlay(workflow, overlayId = null) {
|
|
185
|
+
const tasks = workflow?.tasks || [];
|
|
186
|
+
if (overlayId) {
|
|
187
|
+
const linkedTask = tasks.find((task) => Array.isArray(task.overlays) && task.overlays.includes(overlayId));
|
|
188
|
+
if (linkedTask) return linkedTask;
|
|
189
|
+
}
|
|
190
|
+
if (workflow?.currentTaskKey) {
|
|
191
|
+
const currentTask = tasks.find((task) => task.key === workflow.currentTaskKey);
|
|
192
|
+
if (currentTask) return currentTask;
|
|
193
|
+
}
|
|
194
|
+
return tasks.find((task) => task.status === 'in-progress' || task.status === 'blocked_needs_user') || null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function attachOverlayToWorkflow(root, overlay, { challengeRequired = false, challengeStatus = null } = {}) {
|
|
198
|
+
const workflow = loadWorkflow(root);
|
|
199
|
+
const task = findWorkflowTaskForOverlay(workflow);
|
|
200
|
+
if (!task) return null;
|
|
201
|
+
|
|
202
|
+
updateTask(root, task.key, {
|
|
203
|
+
overlays: unique([...(task.overlays || []), overlay.id]),
|
|
204
|
+
challengeRequired: challengeRequired || task.challengeRequired,
|
|
205
|
+
challengeStatus: challengeStatus ?? task.challengeStatus,
|
|
206
|
+
note: `Attached domain overlay ${overlay.id} (${overlay.domain}).`,
|
|
207
|
+
});
|
|
208
|
+
return task.key;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function syncPromotionToWorkflow(root, request) {
|
|
212
|
+
const workflow = loadWorkflow(root);
|
|
213
|
+
const task = findWorkflowTaskForOverlay(workflow, request.id);
|
|
214
|
+
if (!task) return null;
|
|
215
|
+
|
|
216
|
+
updateTask(root, task.key, {
|
|
217
|
+
overlays: unique([...(task.overlays || []), request.id]),
|
|
218
|
+
challengeRequired: true,
|
|
219
|
+
challengeStatus: request.challenge?.status || task.challengeStatus || 'pending',
|
|
220
|
+
note: `Updated promotion request ${request.id} challenge status to ${request.challenge?.status || 'pending'}.`,
|
|
221
|
+
});
|
|
222
|
+
return task.key;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isExpired(record) {
|
|
226
|
+
return Boolean(record?.expiresAt && new Date(record.expiresAt).getTime() <= Date.now());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function getActiveOverlays(cwd = process.cwd()) {
|
|
230
|
+
const root = findConstructRoot(cwd);
|
|
231
|
+
return loadOverlayRecords(root)
|
|
232
|
+
.map((entry) => entry.record)
|
|
233
|
+
.filter((record) => !isExpired(record) && record.status !== 'archived');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function getPromotionRequests(cwd = process.cwd()) {
|
|
237
|
+
const root = findConstructRoot(cwd);
|
|
238
|
+
return loadPromotionRecords(root)
|
|
239
|
+
.map((entry) => entry.record)
|
|
240
|
+
.filter((record) => record.status !== 'archived');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatOverlayLine(record) {
|
|
244
|
+
const mode = record.permanence === 'temporary' ? 'temp' : 'promotion';
|
|
245
|
+
const expiry = record.expiresAt ? ` · expires ${record.expiresAt}` : '';
|
|
246
|
+
return `- ${record.id} · ${record.domain} · ${mode} · ${record.focus} · ${record.attachTo.join(', ')}${expiry}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function printHeadhuntList(cwd = process.cwd()) {
|
|
250
|
+
const root = findConstructRoot(cwd);
|
|
251
|
+
const overlays = loadOverlayRecords(root);
|
|
252
|
+
const promotions = loadPromotionRecords(root);
|
|
253
|
+
process.stdout.write('Headhunt overlays\n');
|
|
254
|
+
if (overlays.length === 0) process.stdout.write('- none\n');
|
|
255
|
+
for (const { record } of overlays) {
|
|
256
|
+
process.stdout.write(`${formatOverlayLine(record)}\n`);
|
|
257
|
+
}
|
|
258
|
+
process.stdout.write('\nPromotion requests\n');
|
|
259
|
+
if (promotions.length === 0) process.stdout.write('- none\n');
|
|
260
|
+
for (const { record } of promotions) {
|
|
261
|
+
process.stdout.write(`- ${record.id} · ${record.domain} · ${record.status} · ${record.attachTo.join(', ')}\n`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function promoteHeadhunt(id, { cwd = process.cwd(), owner = null } = {}) {
|
|
266
|
+
const root = findConstructRoot(cwd);
|
|
267
|
+
const overlayEntry = loadOverlayRecords(root).find((entry) => entry.record.id === id);
|
|
268
|
+
if (!overlayEntry) throw new Error(`Unknown overlay: ${id}`);
|
|
269
|
+
|
|
270
|
+
const existingRequest = loadPromotionRecords(root).find((entry) => entry.record.id === id);
|
|
271
|
+
if (existingRequest) {
|
|
272
|
+
process.stdout.write(`Promotion request already exists: ${path.relative(root, existingRequest.filePath)}\n`);
|
|
273
|
+
return existingRequest.record;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const request = {
|
|
277
|
+
id,
|
|
278
|
+
type: 'promotion-request',
|
|
279
|
+
domain: overlayEntry.record.domain,
|
|
280
|
+
objective: overlayEntry.record.objective,
|
|
281
|
+
scope: overlayEntry.record.scope,
|
|
282
|
+
requestedAt: new Date().toISOString(),
|
|
283
|
+
status: 'pending_review',
|
|
284
|
+
owner,
|
|
285
|
+
attachTo: overlayEntry.record.attachTo,
|
|
286
|
+
focus: overlayEntry.record.focus,
|
|
287
|
+
sourceOverlay: overlayEntry.filePath,
|
|
288
|
+
reviewFlow: ['cx-architect', 'cx-devil-advocate', 'cx-docs-keeper'],
|
|
289
|
+
challenge: {
|
|
290
|
+
required: true,
|
|
291
|
+
owner: 'cx-devil-advocate',
|
|
292
|
+
status: 'pending',
|
|
293
|
+
reason: 'Persistent capabilities must be challenged before promotion.',
|
|
294
|
+
},
|
|
295
|
+
promotionGates: buildPromotionGates(),
|
|
296
|
+
};
|
|
297
|
+
const promotionPath = path.join(getPromotionDir(root), `${id}.json`);
|
|
298
|
+
writeJson(promotionPath, request);
|
|
299
|
+
syncPromotionToWorkflow(root, request);
|
|
300
|
+
process.stdout.write(`Created promotion request: ${path.relative(root, promotionPath)}\n`);
|
|
301
|
+
return request;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function updatePromotionChallenge(id, { cwd = process.cwd(), status, note = null } = {}) {
|
|
305
|
+
const root = findConstructRoot(cwd);
|
|
306
|
+
const requestPath = path.join(getPromotionDir(root), `${id}.json`);
|
|
307
|
+
const request = readJsonIfExists(requestPath);
|
|
308
|
+
if (!request) throw new Error(`Unknown promotion request: ${id}`);
|
|
309
|
+
request.challenge = request.challenge || { required: true, owner: 'cx-devil-advocate', status: 'pending' };
|
|
310
|
+
if (status) request.challenge.status = status;
|
|
311
|
+
if (note) request.challenge.note = note;
|
|
312
|
+
request.updatedAt = new Date().toISOString();
|
|
313
|
+
writeJson(requestPath, request);
|
|
314
|
+
syncPromotionToWorkflow(root, request);
|
|
315
|
+
return request;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function cleanupHeadhunt({ cwd = process.cwd(), now = new Date() } = {}) {
|
|
319
|
+
const root = findConstructRoot(cwd);
|
|
320
|
+
const overlays = loadOverlayRecords(root);
|
|
321
|
+
let removed = 0;
|
|
322
|
+
for (const { filePath, record } of overlays) {
|
|
323
|
+
if (record.expiresAt && new Date(record.expiresAt).getTime() <= now.getTime()) {
|
|
324
|
+
fs.rmSync(filePath, { force: true });
|
|
325
|
+
const markdownPath = filePath.replace(/\.json$/, '.md');
|
|
326
|
+
fs.rmSync(markdownPath, { force: true });
|
|
327
|
+
removed += 1;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
process.stdout.write(`Removed ${removed} expired overlay(s).\n`);
|
|
331
|
+
return removed;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function parseSubcommand(positional) {
|
|
335
|
+
const first = positional[0];
|
|
336
|
+
if (['list', 'promote', 'challenge', 'cleanup', 'template'].includes(first)) {
|
|
337
|
+
return { subcommand: first, rest: positional.slice(1) };
|
|
338
|
+
}
|
|
339
|
+
return { subcommand: 'create', rest: positional };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export async function runHeadhunt({ args = [], cwd = process.cwd(), homeDir = os.homedir() } = {}) {
|
|
343
|
+
const { flags, positional } = parseArgs(args);
|
|
344
|
+
const { subcommand, rest } = parseSubcommand(positional);
|
|
345
|
+
if (flags.help || flags.h) {
|
|
346
|
+
process.stdout.write('Usage:\n construct headhunt <domain> [--for=OBJECTIVE] [--scope=TEXT] [--temp|--save] [--team=a,b] [--freshness=current|stable]\n construct headhunt list\n construct headhunt promote <overlay-id> [--owner=NAME]\n construct headhunt cleanup\n');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (subcommand === 'list') {
|
|
351
|
+
printHeadhuntList(cwd);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (subcommand === 'promote') {
|
|
356
|
+
const id = rest[0];
|
|
357
|
+
if (!id) throw new Error('Usage: construct headhunt promote <overlay-id> [--owner=NAME]');
|
|
358
|
+
return promoteHeadhunt(id, { cwd, owner: typeof flags.owner === 'string' ? flags.owner : null });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (subcommand === 'challenge') {
|
|
362
|
+
const id = rest[0];
|
|
363
|
+
if (!id) throw new Error('Usage: construct headhunt challenge <overlay-id> --status=pending|approved|rejected [--note=TEXT]');
|
|
364
|
+
return updatePromotionChallenge(id, { cwd, status: typeof flags.status === 'string' ? flags.status : null, note: typeof flags.note === 'string' ? flags.note : null });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (subcommand === 'cleanup') {
|
|
368
|
+
return cleanupHeadhunt({ cwd });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (subcommand === 'template') {
|
|
372
|
+
const templateName = rest[0];
|
|
373
|
+
if (!templateName) { listTeamTemplates(cwd); return; }
|
|
374
|
+
const root = findConstructRoot(cwd);
|
|
375
|
+
const teams = loadTeamTemplates(root);
|
|
376
|
+
if (!teams) throw new Error('agents/teams.json not found. Run from the Construct root.');
|
|
377
|
+
const tpl = teams.templates[templateName];
|
|
378
|
+
if (!tpl) throw new Error(`Unknown template: ${templateName}. Run 'construct headhunt template' to list available templates.`);
|
|
379
|
+
const objective = typeof flags.for === 'string' ? flags.for.trim() : rest.slice(1).join(' ').trim();
|
|
380
|
+
if (!objective) throw new Error(`Usage: construct headhunt template ${templateName} --for="<objective>"`);
|
|
381
|
+
const permanence = flags.save ? 'permanent_request' : 'temporary';
|
|
382
|
+
const freshness = typeof flags.freshness === 'string' ? flags.freshness : 'current';
|
|
383
|
+
const now = new Date();
|
|
384
|
+
const overlayId = `${slugify(templateName)}-team-${now.toISOString().replace(/[:.]/g, '-')}`;
|
|
385
|
+
const overlayDir = getOverlayDir(root);
|
|
386
|
+
const expiresAt = permanence === 'temporary' ? new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString() : null;
|
|
387
|
+
const workflow = loadWorkflow(root);
|
|
388
|
+
|
|
389
|
+
const overlay = {
|
|
390
|
+
id: overlayId,
|
|
391
|
+
type: 'domain-overlay',
|
|
392
|
+
domain: templateName,
|
|
393
|
+
objective,
|
|
394
|
+
scope: typeof flags.scope === 'string' ? flags.scope.trim() : null,
|
|
395
|
+
permanence,
|
|
396
|
+
focus: tpl.focus,
|
|
397
|
+
attachTo: tpl.members,
|
|
398
|
+
freshness,
|
|
399
|
+
teamTemplate: templateName,
|
|
400
|
+
teamId: overlayId,
|
|
401
|
+
createdAt: now.toISOString(),
|
|
402
|
+
expiresAt,
|
|
403
|
+
project: path.basename(root),
|
|
404
|
+
workflowId: workflow?.id || null,
|
|
405
|
+
taskKey: workflow?.currentTaskKey || null,
|
|
406
|
+
status: 'active',
|
|
407
|
+
prompt: buildOverlayPrompt({ domain: templateName, objective, scope: typeof flags.scope === 'string' ? flags.scope : '', permanence, focus: tpl.focus, freshness }),
|
|
408
|
+
skills: tpl.skills ?? [],
|
|
409
|
+
promotionGates: tpl.promotionGates ?? buildPromotionGates(),
|
|
410
|
+
challenge: null,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const overlayJsonPath = path.join(overlayDir, `${overlayId}.json`);
|
|
414
|
+
const overlayMdPath = path.join(overlayDir, `${overlayId}.md`);
|
|
415
|
+
writeJson(overlayJsonPath, overlay);
|
|
416
|
+
writeText(overlayMdPath, [
|
|
417
|
+
`# Team: ${templateName}`,
|
|
418
|
+
'',
|
|
419
|
+
`- objective: ${objective}`,
|
|
420
|
+
`- focus: ${tpl.focus}`,
|
|
421
|
+
`- members: ${tpl.members.join(', ')}`,
|
|
422
|
+
tpl.skills?.length ? `- skills: ${tpl.skills.join(', ')}` : null,
|
|
423
|
+
expiresAt ? `- expiresAt: ${expiresAt}` : null,
|
|
424
|
+
'',
|
|
425
|
+
'## Promotion Gates',
|
|
426
|
+
...overlay.promotionGates.map((g) => `- ${g}`),
|
|
427
|
+
].filter(Boolean).join('\n'));
|
|
428
|
+
|
|
429
|
+
const attachedTaskKey = attachOverlayToWorkflow(root, overlay, {});
|
|
430
|
+
if (attachedTaskKey) { overlay.taskKey = attachedTaskKey; writeJson(overlayJsonPath, overlay); }
|
|
431
|
+
|
|
432
|
+
const lines = [
|
|
433
|
+
`Construct activated team template: ${templateName}`,
|
|
434
|
+
`Objective: ${objective}`,
|
|
435
|
+
`Members: ${tpl.members.join(', ')}`,
|
|
436
|
+
`Overlay ID (teamId): ${overlayId}`,
|
|
437
|
+
`Overlay: ${path.relative(cwd, overlayJsonPath)}`,
|
|
438
|
+
];
|
|
439
|
+
if (expiresAt) lines.push(`Expires: ${expiresAt}`);
|
|
440
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
441
|
+
return { overlay, overlayJsonPath, overlayMdPath };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const domain = rest[0];
|
|
445
|
+
if (!domain) throw new Error('Usage: construct headhunt <domain> [--for=OBJECTIVE] [--scope=TEXT] [--temp|--save]');
|
|
446
|
+
if (flags.temp && flags.save) throw new Error('Use either --temp or --save, not both.');
|
|
447
|
+
|
|
448
|
+
const interactive = process.stdin.isTTY;
|
|
449
|
+
const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
450
|
+
try {
|
|
451
|
+
let permanence = flags.save ? 'permanent_request' : 'temporary';
|
|
452
|
+
if (!flags.temp && !flags.save && interactive) {
|
|
453
|
+
const answer = (await promptQuestion(rl, 'Use this expertise temporarily for the current task, or save it as a promotion request? [temp/save]: ')).trim().toLowerCase();
|
|
454
|
+
permanence = answer === 'save' ? 'permanent_request' : 'temporary';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let objective = typeof flags.for === 'string' ? flags.for.trim() : rest.slice(1).join(' ').trim();
|
|
458
|
+
if (!objective && interactive) {
|
|
459
|
+
objective = (await promptQuestion(rl, `What should the ${domain} expertise help with? `)).trim();
|
|
460
|
+
}
|
|
461
|
+
if (!objective) throw new Error('A task objective is required. Use --for="..." or provide a trailing goal.');
|
|
462
|
+
|
|
463
|
+
let scope = typeof flags.scope === 'string' ? flags.scope.trim() : '';
|
|
464
|
+
if (!scope && interactive) {
|
|
465
|
+
scope = (await promptQuestion(rl, 'Optional scope boundary (press enter to skip): ')).trim();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const freshness = typeof flags.freshness === 'string' ? flags.freshness : 'current';
|
|
469
|
+
const classification = classifyOverlay(objective, typeof flags.team === 'string' ? flags.team : '');
|
|
470
|
+
const now = new Date();
|
|
471
|
+
const root = findConstructRoot(cwd);
|
|
472
|
+
const workflow = loadWorkflow(root);
|
|
473
|
+
const overlayId = `${slugify(domain)}-${now.toISOString().replace(/[:.]/g, '-')}`;
|
|
474
|
+
const overlayDir = getOverlayDir(root);
|
|
475
|
+
const promotionDir = getPromotionDir(root);
|
|
476
|
+
const expiresAt = permanence === 'temporary' ? new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString() : null;
|
|
477
|
+
|
|
478
|
+
const overlay = {
|
|
479
|
+
id: overlayId,
|
|
480
|
+
type: 'domain-overlay',
|
|
481
|
+
domain,
|
|
482
|
+
objective,
|
|
483
|
+
scope: scope || null,
|
|
484
|
+
permanence,
|
|
485
|
+
focus: classification.focus,
|
|
486
|
+
attachTo: classification.attachTo,
|
|
487
|
+
freshness,
|
|
488
|
+
createdAt: now.toISOString(),
|
|
489
|
+
expiresAt,
|
|
490
|
+
project: path.basename(root),
|
|
491
|
+
workflowId: workflow?.id || null,
|
|
492
|
+
taskKey: workflow?.currentTaskKey || null,
|
|
493
|
+
status: permanence === 'temporary' ? 'active' : 'promotion_requested',
|
|
494
|
+
teamId: overlayId,
|
|
495
|
+
sourceStrategy: [
|
|
496
|
+
'official docs',
|
|
497
|
+
'release notes/changelogs',
|
|
498
|
+
'provider docs',
|
|
499
|
+
'tracked issues',
|
|
500
|
+
'ecosystem references',
|
|
501
|
+
],
|
|
502
|
+
toolBoundary: 'research-first; no automatic registry mutation or expanded write authority',
|
|
503
|
+
prompt: buildOverlayPrompt({
|
|
504
|
+
domain,
|
|
505
|
+
objective,
|
|
506
|
+
scope,
|
|
507
|
+
permanence,
|
|
508
|
+
focus: classification.focus,
|
|
509
|
+
freshness,
|
|
510
|
+
}),
|
|
511
|
+
promotionGates: buildPromotionGates(),
|
|
512
|
+
challenge: permanence === 'temporary' ? null : {
|
|
513
|
+
required: true,
|
|
514
|
+
owner: 'cx-devil-advocate',
|
|
515
|
+
status: 'pending',
|
|
516
|
+
reason: 'Promotion requests must be challenged before any persistent capability is added.',
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const overlayJsonPath = path.join(overlayDir, `${overlayId}.json`);
|
|
521
|
+
const overlayMdPath = path.join(overlayDir, `${overlayId}.md`);
|
|
522
|
+
writeJson(overlayJsonPath, overlay);
|
|
523
|
+
writeText(overlayMdPath, [
|
|
524
|
+
`# ${domain} overlay`,
|
|
525
|
+
'',
|
|
526
|
+
`- objective: ${objective}`,
|
|
527
|
+
`- permanence: ${permanence}`,
|
|
528
|
+
`- focus: ${classification.focus}`,
|
|
529
|
+
`- attachTo: ${classification.attachTo.join(', ')}`,
|
|
530
|
+
scope ? `- scope: ${scope}` : null,
|
|
531
|
+
`- freshness: ${freshness}`,
|
|
532
|
+
expiresAt ? `- expiresAt: ${expiresAt}` : null,
|
|
533
|
+
'',
|
|
534
|
+
'## Prompt',
|
|
535
|
+
'',
|
|
536
|
+
overlay.prompt,
|
|
537
|
+
].filter(Boolean).join('\n'));
|
|
538
|
+
|
|
539
|
+
const attachedTaskKey = attachOverlayToWorkflow(root, overlay, {
|
|
540
|
+
challengeRequired: permanence === 'permanent_request',
|
|
541
|
+
challengeStatus: permanence === 'permanent_request' ? 'pending' : null,
|
|
542
|
+
});
|
|
543
|
+
if (attachedTaskKey) {
|
|
544
|
+
overlay.taskKey = attachedTaskKey;
|
|
545
|
+
writeJson(overlayJsonPath, overlay);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let promotionPath = null;
|
|
549
|
+
if (permanence === 'permanent_request') {
|
|
550
|
+
const request = {
|
|
551
|
+
id: overlayId,
|
|
552
|
+
type: 'promotion-request',
|
|
553
|
+
domain,
|
|
554
|
+
objective,
|
|
555
|
+
scope: scope || null,
|
|
556
|
+
requestedAt: now.toISOString(),
|
|
557
|
+
status: 'pending_review',
|
|
558
|
+
owner: typeof flags.owner === 'string' ? flags.owner : null,
|
|
559
|
+
attachTo: classification.attachTo,
|
|
560
|
+
focus: classification.focus,
|
|
561
|
+
sourceOverlay: overlayJsonPath,
|
|
562
|
+
reviewFlow: ['cx-architect', 'cx-devil-advocate', 'cx-docs-keeper'],
|
|
563
|
+
challenge: {
|
|
564
|
+
required: true,
|
|
565
|
+
owner: 'cx-devil-advocate',
|
|
566
|
+
status: 'pending',
|
|
567
|
+
reason: 'Promotion requests must be challenged before any persistent capability is added.',
|
|
568
|
+
},
|
|
569
|
+
promotionGates: buildPromotionGates(),
|
|
570
|
+
};
|
|
571
|
+
promotionPath = path.join(promotionDir, `${overlayId}.json`);
|
|
572
|
+
writeJson(promotionPath, request);
|
|
573
|
+
syncPromotionToWorkflow(root, request);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const lines = [];
|
|
577
|
+
lines.push(`Construct activated ${domain} expertise for this request.`);
|
|
578
|
+
lines.push(`Mode: ${permanence === 'temporary' ? 'temporary overlay' : 'promotion request + temporary overlay'}`);
|
|
579
|
+
lines.push(`Attach to: ${classification.attachTo.join(', ')}`);
|
|
580
|
+
lines.push(`Overlay: ${path.relative(cwd, overlayJsonPath)}`);
|
|
581
|
+
if (attachedTaskKey) lines.push(`Workflow task: ${attachedTaskKey}`);
|
|
582
|
+
if (promotionPath) lines.push(`Promotion request: ${path.relative(cwd, promotionPath)}`);
|
|
583
|
+
if (expiresAt) lines.push(`Expires: ${expiresAt}`);
|
|
584
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
585
|
+
|
|
586
|
+
return { overlay, overlayJsonPath, overlayMdPath, promotionPath };
|
|
587
|
+
} finally {
|
|
588
|
+
rl?.close();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
593
|
+
runHeadhunt({ args: process.argv.slice(2) }).catch((error) => {
|
|
594
|
+
process.stderr.write(`Error: ${error.message}\n`);
|
|
595
|
+
process.exit(1);
|
|
596
|
+
});
|
|
597
|
+
}
|