@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.
- package/README.md +39 -34
- package/bin/construct +1403 -96
- package/commands/build/feature.md +1 -1
- package/commands/build/fix.md +1 -1
- package/commands/design/access.md +1 -1
- package/commands/design/flow.md +1 -1
- package/commands/design/ui.md +1 -1
- package/commands/measure/experiment.md +1 -1
- package/commands/measure/metrics.md +1 -1
- package/commands/measure/results.md +1 -1
- package/commands/plan/api.md +1 -1
- package/commands/plan/challenge.md +1 -1
- package/commands/plan/decide.md +1 -1
- package/commands/plan/feature.md +1 -1
- package/commands/plan/requirements.md +1 -1
- package/commands/remember/context.md +1 -1
- package/commands/remember/handoff.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/commands/review/code.md +1 -1
- package/commands/review/quality.md +1 -1
- package/commands/review/security.md +1 -1
- package/commands/ship/ready.md +1 -1
- package/commands/ship/release.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/understand/docs.md +1 -1
- package/commands/understand/this.md +1 -1
- package/commands/understand/why.md +1 -1
- package/commands/work/clean.md +1 -1
- package/commands/work/drive.md +1 -1
- package/commands/work/optimize-prompts.md +2 -2
- package/commands/work/parallel-review.md +1 -1
- package/db/schema/007_tags.sql +30 -0
- package/db/schema/008_skill_usage.sql +24 -0
- package/db/schema/009_scheduler.sql +14 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +1 -1
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +1 -1
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +1 -1
- package/examples/internal/roles/engineer/golden/read-before-write.md +1 -1
- package/examples/internal/roles/qa/bad/coverage-theater.md +1 -1
- package/examples/internal/roles/qa/golden/regression-gate.md +1 -1
- package/examples/seed-observations/decisions.md +1 -1
- package/lib/audit-skills.mjs +3 -3
- package/lib/auto-docs.mjs +8 -8
- package/lib/boundary.mjs +126 -0
- package/lib/cache-strategy-google.js +26 -31
- package/lib/cli-commands.mjs +276 -122
- package/lib/comment-lint.mjs +139 -1
- package/lib/completions.mjs +1 -1
- package/lib/config/schema.mjs +15 -1
- package/lib/contracts/validate.mjs +371 -0
- package/lib/daemons/contract.mjs +210 -0
- package/lib/distill.mjs +22 -4
- package/lib/doc-stamp.mjs +48 -0
- package/lib/docs-verify.mjs +59 -6
- package/lib/doctor/cli.mjs +16 -1
- package/lib/doctor/index.mjs +3 -1
- package/lib/doctor/watchers/consistency.mjs +360 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
- package/lib/document-extract.mjs +420 -17
- package/lib/document-ingest.mjs +2 -0
- package/lib/embed/cli.mjs +124 -3
- package/lib/embed/daemon.mjs +43 -4
- package/lib/embed/docs-lifecycle.mjs +1 -1
- package/lib/embed/inbox.mjs +50 -3
- package/lib/embed/role-framing.mjs +3 -3
- package/lib/embed/scheduler.mjs +33 -5
- package/lib/env-config.mjs +9 -3
- package/lib/evaluator-optimizer.mjs +2 -3
- package/lib/extractors/calendar.mjs +173 -0
- package/lib/extractors/shared/drop-info.mjs +22 -0
- package/lib/extractors/transcript.mjs +0 -0
- package/lib/gates-audit.mjs +8 -2
- package/lib/git-hooks/prepare-commit-msg +1 -1
- package/lib/headhunt.mjs +2 -2
- package/lib/hooks/agent-tracker.mjs +6 -11
- package/lib/hooks/comment-lint.mjs +16 -0
- package/lib/hooks/guard-bash.mjs +35 -14
- package/lib/hooks/mcp-audit.mjs +14 -1
- package/lib/hooks/pre-compact.mjs +92 -5
- package/lib/hooks/pre-push-gate.mjs +41 -8
- package/lib/hooks/proactive-activation.mjs +0 -14
- package/lib/hooks/registry-sync.mjs +2 -2
- package/lib/hooks/rule-verifier.mjs +217 -0
- package/lib/hooks/session-optimize.mjs +2 -1
- package/lib/hooks/stop-notify.mjs +7 -6
- package/lib/host-capabilities.mjs +10 -1
- package/lib/hygiene/scan.mjs +141 -0
- package/lib/init-unified.mjs +73 -65
- package/lib/install/stage-project.mjs +4 -4
- package/lib/intake/classify.mjs +309 -58
- package/lib/intake/daemon.mjs +121 -0
- package/lib/intake/filesystem-queue.mjs +31 -6
- package/lib/intake/intake-config.mjs +2 -1
- package/lib/intake/postgres-queue.mjs +45 -3
- package/lib/intake/prepare.mjs +3 -1
- package/lib/intake/quarantine.mjs +205 -0
- package/lib/intake/session-prelude.mjs +7 -1
- package/lib/intake/traceability.mjs +90 -0
- package/lib/knowledge/postgres-search.mjs +132 -0
- package/lib/knowledge/research-store.mjs +2 -0
- package/lib/knowledge/search.mjs +14 -4
- package/lib/maintenance/cleanup.mjs +315 -0
- package/lib/mcp/memory-bridge.mjs +276 -0
- package/lib/mcp/server.mjs +11 -3
- package/lib/mcp/tools/skills.mjs +32 -11
- package/lib/mcp/tools/workflow.mjs +25 -0
- package/lib/mcp-catalog.json +12 -8
- package/lib/mcp-platform-config.mjs +16 -8
- package/lib/migrations/index.mjs +106 -0
- package/lib/migrations/v1-baseline.mjs +33 -0
- package/lib/model-registry.mjs +1 -1
- package/lib/observation-store.mjs +9 -4
- package/lib/opencode-runtime-plugin.mjs +1 -1
- package/lib/orchestration-policy.mjs +4 -4
- package/lib/outcomes/record.mjs +2 -0
- package/lib/overrides/resolver.mjs +3 -3
- package/lib/parity.mjs +4 -4
- package/lib/policy/engine.mjs +2 -2
- package/lib/profiles/lifecycle.mjs +1 -1
- package/lib/profiles/rebrand.mjs +46 -0
- package/lib/project-init-shared.mjs +12 -0
- package/lib/prompt-metadata.mjs +4 -4
- package/lib/provider-capabilities.js +20 -7
- package/lib/providers/auth-manager.mjs +58 -17
- package/lib/providers/circuit-breaker.mjs +14 -0
- package/lib/providers/contract.mjs +67 -3
- package/lib/providers/creds.mjs +219 -0
- package/lib/providers/scaffold.mjs +107 -0
- package/lib/reflect.mjs +49 -12
- package/lib/role-preload.mjs +5 -0
- package/lib/roles/catalog.mjs +1 -1
- package/lib/roles/manifest.mjs +2 -2
- package/lib/scheduler/index.mjs +112 -0
- package/lib/scheduler/solo.mjs +183 -0
- package/lib/server/index.mjs +29 -29
- package/lib/server/insights.mjs +14 -12
- package/lib/server/langfuse-login.mjs +58 -0
- package/lib/server/static/assets/index-ab25c707.js +1 -1
- package/lib/session-store.mjs +6 -4
- package/lib/setup.mjs +14 -3
- package/lib/{agent-contracts-enforce.mjs → specialist-contracts-enforce.mjs} +4 -4
- package/lib/{agent-contracts.mjs → specialist-contracts.mjs} +9 -8
- package/lib/{agents → specialists}/postconditions.mjs +3 -3
- package/lib/{agents → specialists}/schema.mjs +6 -6
- package/lib/status.mjs +13 -11
- package/lib/storage/backup.mjs +2 -2
- package/lib/tags/lifecycle.mjs +278 -0
- package/lib/tags/vocabulary.mjs +140 -0
- package/lib/telemetry/client.mjs +5 -1
- package/lib/telemetry/otel-tracer.mjs +184 -0
- package/lib/telemetry/skill-calls.mjs +73 -12
- package/lib/uninstall/uninstall.mjs +1 -1
- package/lib/update.mjs +1 -1
- package/lib/validator.mjs +57 -56
- package/lib/version.mjs +51 -0
- package/lib/worker/trace.mjs +5 -1
- package/lib/workflows/instantiate.mjs +320 -0
- package/package.json +16 -3
- package/personas/construct.md +5 -3
- package/platforms/claude/CLAUDE.md +1 -1
- package/rules/common/development-workflow.md +2 -1
- package/rules/common/no-fabrication.md +69 -0
- package/rules/common/review-before-change.md +58 -0
- package/scripts/{sync-agents.mjs → sync-specialists.mjs} +171 -68
- package/skills/ai/prompt-optimizer.md +3 -3
- package/skills/exploration/dependency-graph-reading.md +98 -0
- package/skills/exploration/tracer-bullet-method.md +71 -0
- package/skills/exploration/unknown-codebase-onboarding.md +91 -0
- package/skills/operating/change-management.md +91 -0
- package/skills/operating/incident-response.md +75 -0
- package/skills/operating/oncall-rotation.md +95 -0
- package/skills/operating/orchestration-reference.md +2 -2
- package/skills/strategy/competitive-landscape.md +75 -0
- package/skills/strategy/market-research-methods.md +87 -0
- package/skills/strategy/narrative-arc.md +77 -0
- package/skills/strategy/pricing-positioning.md +94 -0
- package/specialists/contracts.json +1002 -0
- package/specialists/contracts.schema.json +83 -0
- package/specialists/policy-inventory.json +160 -0
- package/{agents → specialists}/prompts/cx-accessibility.md +3 -1
- package/{agents → specialists}/prompts/cx-ai-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-architect.md +3 -1
- package/{agents → specialists}/prompts/cx-business-strategist.md +3 -1
- package/{agents → specialists}/prompts/cx-data-analyst.md +3 -1
- package/{agents → specialists}/prompts/cx-data-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-debugger.md +3 -1
- package/{agents → specialists}/prompts/cx-designer.md +3 -1
- package/{agents → specialists}/prompts/cx-devil-advocate.md +3 -1
- package/{agents → specialists}/prompts/cx-docs-keeper.md +4 -2
- package/{agents → specialists}/prompts/cx-engineer.md +4 -2
- package/{agents → specialists}/prompts/cx-evaluator.md +3 -1
- package/{agents → specialists}/prompts/cx-explorer.md +3 -1
- package/{agents → specialists}/prompts/cx-legal-compliance.md +3 -1
- package/{agents → specialists}/prompts/cx-operations.md +3 -1
- package/{agents → specialists}/prompts/cx-orchestrator.md +4 -2
- package/{agents → specialists}/prompts/cx-platform-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-product-manager.md +3 -1
- package/{agents → specialists}/prompts/cx-qa.md +3 -1
- package/{agents → specialists}/prompts/cx-rd-lead.md +3 -1
- package/{agents → specialists}/prompts/cx-release-manager.md +3 -1
- package/{agents → specialists}/prompts/cx-researcher.md +3 -1
- package/{agents → specialists}/prompts/cx-reviewer.md +4 -2
- package/{agents → specialists}/prompts/cx-security.md +3 -1
- package/{agents → specialists}/prompts/cx-sre.md +4 -2
- package/{agents → specialists}/prompts/cx-test-automation.md +3 -1
- package/{agents → specialists}/prompts/cx-trace-reviewer.md +5 -3
- package/{agents → specialists}/prompts/cx-ux-researcher.md +3 -1
- package/{agents → specialists}/registry.json +651 -418
- package/{agents → specialists}/role-manifests.json +8 -8
- package/templates/docs/construct_guide.md +6 -6
- package/templates/docs/skill-artifact.md +1 -1
- package/templates/provider-scaffold/health.test.mjs +30 -0
- package/templates/provider-scaffold/index.mjs +48 -0
- package/templates/workflows/cross-team-handoff.yml +85 -0
- package/templates/workflows/engineering-onboarding.yml +77 -0
- package/templates/workflows/new-feature.yml +53 -0
- package/agents/contracts.json +0 -387
- /package/{agents → specialists}/teams.json +0 -0
package/lib/mcp/tools/skills.mjs
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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('../../
|
|
118
|
-
const { describePostconditions } = await import('../../
|
|
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, '
|
|
386
|
-
if (!existsSync(teamsPath)) return { error: '
|
|
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, '
|
|
404
|
-
if (!existsSync(teamsPath)) return { error: '
|
|
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 ?? '';
|
package/lib/mcp-catalog.json
CHANGED
|
@@ -55,21 +55,25 @@
|
|
|
55
55
|
"id": "memory",
|
|
56
56
|
"name": "Memory",
|
|
57
57
|
"category": "optional",
|
|
58
|
-
"description": "cass-memory
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
|
|
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": "
|
|
69
|
-
"opencode": { "mode": "
|
|
70
|
-
"codex": { "mode": "
|
|
72
|
+
"claude": { "mode": "managed" },
|
|
73
|
+
"opencode": { "mode": "managed" },
|
|
74
|
+
"codex": { "mode": "managed" }
|
|
71
75
|
},
|
|
72
|
-
"setupNote": "
|
|
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,
|
|
61
|
+
const headers = buildRemoteHeaders(mcpDef, values);
|
|
54
62
|
const url =
|
|
55
|
-
id === "memory" &&
|
|
56
|
-
? `http://127.0.0.1:${
|
|
57
|
-
: resolveTemplateString(mcpDef.url,
|
|
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,
|
|
73
|
+
const env = buildLocalEnvironment(mcpDef, values);
|
|
66
74
|
return {
|
|
67
75
|
command: mcpDef.command,
|
|
68
|
-
args: resolveArgs(mcpDef.args,
|
|
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
|
+
};
|
package/lib/model-registry.mjs
CHANGED
|
@@ -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 (
|
|
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, "
|
|
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
|
|
10
|
-
* lib/
|
|
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 './
|
|
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
|
|
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',
|
package/lib/outcomes/record.mjs
CHANGED
|
@@ -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: '
|
|
41
|
+
agents: { originalDir: 'specialists/prompts', defaultExt: '.md' },
|
|
42
42
|
skills: { originalDir: 'skills', defaultExt: '.md' },
|
|
43
43
|
rules: { originalDir: 'rules', defaultExt: '.md' },
|
|
44
|
-
contracts: { originalDir: '
|
|
45
|
-
'role-manifests': { originalDir: '
|
|
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 `
|
|
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, '
|
|
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.
|
|
46
|
-
...(registry.
|
|
45
|
+
...(registry.orchestrator ? [{ ...registry.orchestrator, isPersona: true }] : []),
|
|
46
|
+
...(registry.specialists || []).map((s) => ({ ...s, isPersona: false })),
|
|
47
47
|
];
|
|
48
48
|
|
|
49
49
|
return entries
|
package/lib/policy/engine.mjs
CHANGED
|
@@ -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 `
|
|
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, '..', '..', '
|
|
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
|
|
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.
|
package/lib/prompt-metadata.mjs
CHANGED
|
@@ -25,9 +25,9 @@ function normalizeName(name) {
|
|
|
25
25
|
function findEntry(registry, agentName) {
|
|
26
26
|
const normalized = normalizeName(agentName);
|
|
27
27
|
const entries = [
|
|
28
|
-
|
|
29
|
-
...(registry.
|
|
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, '
|
|
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
|
}
|