@geraldmaron/construct 1.0.5 → 1.0.7
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 +24 -18
- package/agents/contracts.json +617 -2
- package/agents/contracts.schema.json +83 -0
- package/agents/prompts/cx-accessibility.md +5 -3
- package/agents/prompts/cx-ai-engineer.md +9 -7
- package/agents/prompts/cx-architect.md +8 -6
- package/agents/prompts/cx-business-strategist.md +8 -6
- package/agents/prompts/cx-data-analyst.md +9 -7
- package/agents/prompts/cx-data-engineer.md +5 -3
- package/agents/prompts/cx-debugger.md +8 -6
- package/agents/prompts/cx-designer.md +7 -5
- package/agents/prompts/cx-devil-advocate.md +6 -4
- package/agents/prompts/cx-docs-keeper.md +7 -5
- package/agents/prompts/cx-engineer.md +7 -5
- package/agents/prompts/cx-evaluator.md +4 -2
- package/agents/prompts/cx-explorer.md +10 -8
- package/agents/prompts/cx-legal-compliance.md +4 -2
- package/agents/prompts/cx-operations.md +7 -5
- package/agents/prompts/cx-orchestrator.md +12 -10
- package/agents/prompts/cx-platform-engineer.md +7 -5
- package/agents/prompts/cx-product-manager.md +6 -4
- package/agents/prompts/cx-qa.md +10 -8
- package/agents/prompts/cx-rd-lead.md +11 -9
- package/agents/prompts/cx-release-manager.md +7 -5
- package/agents/prompts/cx-researcher.md +24 -22
- package/agents/prompts/cx-reviewer.md +9 -7
- package/agents/prompts/cx-security.md +12 -10
- package/agents/prompts/cx-sre.md +9 -7
- package/agents/prompts/cx-test-automation.md +5 -3
- package/agents/prompts/cx-trace-reviewer.md +10 -8
- package/agents/prompts/cx-ux-researcher.md +5 -3
- package/agents/registry.json +8 -3
- package/bin/construct +444 -53
- package/commands/build/feature.md +4 -4
- package/commands/build/fix.md +8 -8
- package/commands/design/access.md +3 -3
- package/commands/design/flow.md +3 -3
- package/commands/design/ui.md +4 -4
- package/commands/measure/experiment.md +5 -5
- package/commands/measure/metrics.md +3 -3
- package/commands/measure/results.md +4 -4
- package/commands/plan/api.md +3 -3
- package/commands/plan/challenge.md +3 -3
- package/commands/plan/decide.md +3 -3
- package/commands/plan/feature.md +7 -7
- package/commands/plan/requirements.md +3 -3
- package/commands/remember/context.md +5 -5
- package/commands/remember/handoff.md +3 -3
- package/commands/remember/runbook.md +3 -3
- package/commands/review/code.md +8 -8
- package/commands/review/quality.md +4 -4
- package/commands/review/security.md +3 -3
- package/commands/ship/ready.md +3 -3
- package/commands/ship/release.md +3 -3
- package/commands/ship/status.md +4 -4
- package/commands/understand/docs.md +3 -3
- package/commands/understand/this.md +4 -4
- package/commands/understand/why.md +8 -8
- package/commands/work/clean.md +14 -14
- package/commands/work/drive.md +10 -10
- package/commands/work/optimize-prompts.md +9 -9
- package/commands/work/parallel-review.md +8 -8
- package/db/schema/006_graph.sql +24 -0
- package/examples/provider-plugin/README.md +7 -7
- package/examples/seed-observations/README.md +6 -6
- package/examples/seed-observations/anti-patterns.md +14 -14
- package/examples/seed-observations/decisions.md +4 -4
- package/examples/seed-observations/patterns.md +14 -14
- package/lib/auto-docs.mjs +13 -8
- package/lib/boundary.mjs +126 -0
- package/lib/cache-strategy-google.js +26 -31
- package/lib/cli-commands.mjs +2 -2
- package/lib/comment-lint.mjs +134 -0
- package/lib/contracts/validate.mjs +323 -0
- package/lib/daemons/contract.mjs +210 -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 +310 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
- package/lib/document-extract.mjs +211 -1
- 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 +2 -0
- package/lib/embed/scheduler.mjs +33 -5
- package/lib/evaluator-optimizer.mjs +2 -3
- package/lib/flavors/loader.mjs +1 -1
- package/lib/hooks/comment-lint.mjs +16 -0
- package/lib/hooks/mcp-audit.mjs +2 -1
- package/lib/hooks/proactive-activation.mjs +0 -14
- package/lib/hooks/rule-verifier.mjs +217 -0
- package/lib/hooks/session-optimize.mjs +2 -1
- package/lib/init-unified.mjs +55 -65
- package/lib/intake/classify.mjs +108 -24
- package/lib/intake/daemon.mjs +121 -0
- package/lib/intake/filesystem-queue.mjs +6 -1
- package/lib/intake/intake-config.mjs +2 -1
- package/lib/intake/prepare.mjs +0 -1
- package/lib/intake/session-prelude.mjs +7 -1
- package/lib/intake/traceability.mjs +90 -0
- package/lib/knowledge/graph.mjs +213 -0
- package/lib/knowledge/research-store.mjs +2 -0
- package/lib/maintenance/cleanup.mjs +315 -0
- package/lib/mcp/memory-bridge.mjs +276 -0
- package/lib/mcp/server.mjs +189 -1
- package/lib/mcp/tools/profile.mjs +270 -0
- 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/observation-store.mjs +9 -4
- package/lib/outcomes/record.mjs +2 -0
- package/lib/profiles/rebrand.mjs +46 -0
- package/lib/project-init-shared.mjs +12 -0
- package/lib/provider-capabilities.js +20 -7
- package/lib/providers/auth-manager.mjs +58 -17
- package/lib/reflect.mjs +49 -12
- package/lib/server/index.mjs +22 -28
- package/lib/session-store.mjs +6 -4
- package/lib/setup.mjs +14 -3
- package/lib/telemetry/client.mjs +5 -1
- package/lib/version.mjs +51 -0
- package/lib/worker/trace.mjs +5 -1
- package/package.json +4 -1
- package/personas/construct.md +3 -1
- package/platforms/claude/CLAUDE.md +6 -6
- package/rules/common/agents.md +2 -2
- package/rules/common/beads-hygiene.md +11 -11
- package/rules/common/code-review.md +1 -1
- package/rules/common/coding-style.md +1 -1
- package/rules/common/comments.md +8 -8
- package/rules/common/commit-approval.md +4 -4
- package/rules/common/cx-agent-routing.md +2 -2
- package/rules/common/cx-skill-routing.md +2 -2
- package/rules/common/development-workflow.md +3 -2
- package/rules/common/doc-ownership.md +2 -2
- package/rules/common/efficiency.md +3 -3
- package/rules/common/framing.md +1 -1
- package/rules/common/git-workflow.md +1 -1
- package/rules/common/no-fabrication.md +69 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/performance.md +1 -1
- package/rules/common/release-gates.md +7 -7
- package/rules/common/research.md +4 -4
- package/rules/common/review-before-change.md +58 -0
- package/rules/common/security.md +1 -1
- package/rules/common/skill-composition.md +8 -8
- package/rules/common/testing.md +1 -1
- package/rules/golang/coding-style.md +2 -2
- package/rules/golang/hooks.md +1 -1
- package/rules/golang/patterns.md +1 -1
- package/rules/golang/security.md +1 -1
- package/rules/golang/testing.md +1 -1
- package/rules/python/coding-style.md +1 -1
- package/rules/python/hooks.md +1 -1
- package/rules/python/patterns.md +1 -1
- package/rules/python/security.md +1 -1
- package/rules/python/testing.md +1 -1
- package/rules/swift/coding-style.md +3 -3
- package/rules/swift/hooks.md +2 -2
- package/rules/swift/patterns.md +2 -2
- package/rules/swift/security.md +4 -4
- package/rules/swift/testing.md +2 -2
- package/rules/typescript/coding-style.md +1 -1
- package/rules/typescript/hooks.md +1 -1
- package/rules/typescript/patterns.md +1 -1
- package/rules/typescript/security.md +1 -1
- package/rules/typescript/testing.md +1 -1
- package/rules/web/coding-style.md +1 -1
- package/rules/web/design-quality.md +1 -1
- package/rules/web/hooks.md +1 -1
- package/rules/web/patterns.md +1 -1
- package/rules/web/performance.md +1 -1
- package/rules/web/security.md +1 -1
- package/rules/web/testing.md +1 -1
- package/scripts/sync-agents.mjs +45 -14
- package/skills/ai/agent-dev.md +1 -1
- package/skills/ai/llm-security.md +1 -1
- package/skills/ai/ml-ops.md +6 -6
- package/skills/ai/orchestration-workflow.md +1 -1
- package/skills/ai/prompt-and-eval.md +1 -1
- package/skills/ai/prompt-optimizer.md +13 -13
- package/skills/ai/rag-system.md +1 -1
- package/skills/architecture/api-design.md +1 -1
- package/skills/architecture/caching.md +1 -1
- package/skills/architecture/cloud-native.md +1 -1
- package/skills/architecture/message-queue.md +1 -1
- package/skills/architecture/security-arch.md +1 -1
- package/skills/compliance/ai-disclosure.md +1 -1
- package/skills/compliance/data-privacy.md +1 -1
- package/skills/compliance/license-audit.md +2 -2
- package/skills/compliance/regulatory-review.md +1 -1
- package/skills/development/cpp.md +1 -1
- package/skills/development/go.md +1 -1
- package/skills/development/java.md +1 -1
- package/skills/development/kotlin.md +9 -9
- package/skills/development/mobile-crossplatform.md +13 -13
- package/skills/development/python.md +1 -1
- package/skills/development/rust.md +1 -1
- package/skills/development/shell.md +1 -1
- package/skills/development/swift.md +6 -6
- package/skills/development/typescript.md +1 -1
- package/skills/devops/ci-cd.md +5 -5
- package/skills/devops/containerization.md +9 -9
- package/skills/devops/cost-optimization.md +1 -1
- package/skills/devops/data-engineering.md +2 -2
- package/skills/devops/database.md +1 -1
- package/skills/devops/dependency-management.md +3 -3
- package/skills/devops/devsecops.md +1 -1
- package/skills/devops/git-workflow.md +1 -1
- package/skills/devops/incident-response.md +18 -18
- package/skills/devops/monorepo.md +5 -5
- package/skills/devops/observability.md +1 -1
- package/skills/devops/performance.md +1 -1
- package/skills/devops/testing.md +1 -1
- package/skills/docs/adr-workflow.md +2 -2
- package/skills/docs/backlog-proposal-workflow.md +1 -1
- package/skills/docs/customer-profile-workflow.md +1 -1
- package/skills/docs/document-ingest-workflow.md +1 -1
- package/skills/docs/evidence-ingest-workflow.md +1 -1
- package/skills/docs/init-docs.md +15 -15
- package/skills/docs/init-project.md +1 -1
- package/skills/docs/prd-workflow.md +3 -3
- package/skills/docs/prfaq-workflow.md +1 -1
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +1 -1
- package/skills/docs/product-signal-workflow.md +9 -9
- package/skills/docs/research-workflow.md +10 -10
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/strategy-workflow.md +3 -3
- package/skills/exploration/repo-map.md +11 -11
- package/skills/frameworks/django.md +15 -15
- package/skills/frameworks/nextjs.md +16 -16
- package/skills/frameworks/react.md +12 -12
- package/skills/frameworks/spring-boot.md +12 -12
- package/skills/frontend-design/accessibility.md +6 -6
- package/skills/frontend-design/component-patterns.md +1 -1
- package/skills/frontend-design/engineering.md +1 -1
- package/skills/frontend-design/state-management.md +1 -1
- package/skills/frontend-design/ui-aesthetics.md +1 -1
- package/skills/frontend-design/ux-principles.md +1 -1
- package/skills/operating/orchestration-reference.md +27 -27
- package/skills/quality-gates/review-work.md +3 -3
- package/skills/quality-gates/verify-change.md +1 -1
- package/skills/quality-gates/verify-module.md +1 -1
- package/skills/quality-gates/verify-quality.md +1 -1
- package/skills/quality-gates/verify-security.md +1 -1
- package/skills/routing.md +14 -14
- package/skills/security/blue-team.md +1 -1
- package/skills/security/code-audit.md +1 -1
- package/skills/security/pentest.md +1 -1
- package/skills/security/red-team.md +1 -1
- package/skills/security/threat-intel.md +1 -1
- package/skills/security/vuln-research.md +1 -1
- package/skills/utility/clean-code.md +2 -2
- package/templates/docs/changelog-entry.md +1 -1
- package/templates/docs/construct_guide.md +18 -18
- package/templates/docs/meta-prd.md +16 -16
- package/templates/docs/one-pager.md +1 -1
- package/templates/docs/prd-business.md +1 -1
- package/templates/docs/prd-platform.md +1 -1
- package/templates/docs/prd.md +17 -17
- package/templates/docs/research-brief.md +8 -8
- package/templates/docs/rfc.md +1 -1
- package/templates/docs/skill-artifact.md +1 -1
- package/templates/docs/strategy.md +1 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mcp/tools/profile.mjs — MCP wrappers for the org-profile + learning surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Bridges PR #67 capabilities (profiles, outcomes, sandboxes, knowledge_add,
|
|
5
|
+
* learning status) so subagents talking through the construct-mcp server can
|
|
6
|
+
* read the same state the CLI exposes. Operator-only surfaces (optimize_apply,
|
|
7
|
+
* optimize_rollback) remain CLI-only and are not registered here.
|
|
8
|
+
*
|
|
9
|
+
* Pattern: each export is a thin schema-validated wrapper around the existing
|
|
10
|
+
* lib/* function the CLI already uses. Errors return `{ error: string }` so
|
|
11
|
+
* the MCP dispatcher hands a structured failure back instead of crashing.
|
|
12
|
+
* Destructive operations (profile_archive) require `confirm: true`, matching
|
|
13
|
+
* the storage_reset / delete_ingested_artifacts pattern in tools/storage.mjs.
|
|
14
|
+
*/
|
|
15
|
+
import { resolve } from 'node:path';
|
|
16
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
loadProfile,
|
|
21
|
+
listProfiles,
|
|
22
|
+
loadCustomProfile,
|
|
23
|
+
resolveActiveProfile,
|
|
24
|
+
} from '../../profiles/loader.mjs';
|
|
25
|
+
import {
|
|
26
|
+
createDraftProfile,
|
|
27
|
+
listDrafts,
|
|
28
|
+
archiveProfile,
|
|
29
|
+
profileHealth,
|
|
30
|
+
} from '../../profiles/lifecycle.mjs';
|
|
31
|
+
import { recordOutcome } from '../../outcomes/record.mjs';
|
|
32
|
+
import { aggregateOutcomes, readSummary } from '../../outcomes/aggregate.mjs';
|
|
33
|
+
import { addResearchFinding } from '../../knowledge/research-store.mjs';
|
|
34
|
+
import { askGlobal } from '../../knowledge/graph.mjs';
|
|
35
|
+
import { listSandboxes } from '../../sandbox.mjs';
|
|
36
|
+
|
|
37
|
+
function cwdOf(args) {
|
|
38
|
+
return args && args.cwd ? resolve(String(args.cwd)) : process.cwd();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Read-only: active profile.
|
|
42
|
+
|
|
43
|
+
export function profileShow(args = {}) {
|
|
44
|
+
const cwd = cwdOf(args);
|
|
45
|
+
const profile = resolveActiveProfile(cwd, args.id ? String(args.id) : null);
|
|
46
|
+
return {
|
|
47
|
+
id: profile.id,
|
|
48
|
+
displayName: profile.displayName ?? profile.id,
|
|
49
|
+
tagline: profile.tagline ?? null,
|
|
50
|
+
custom: profile.custom === true,
|
|
51
|
+
roles: Array.isArray(profile.roles) ? profile.roles : [],
|
|
52
|
+
departments: Array.isArray(profile.departments)
|
|
53
|
+
? profile.departments.map((d) => ({ id: d.id, displayName: d.displayName, roleCount: Array.isArray(d.roles) ? d.roles.length : 0 }))
|
|
54
|
+
: [],
|
|
55
|
+
intake: {
|
|
56
|
+
types: profile.intake?.types ?? [],
|
|
57
|
+
stages: profile.intake?.stages ?? [],
|
|
58
|
+
},
|
|
59
|
+
docTemplates: profile.docTemplates ?? [],
|
|
60
|
+
hooks: profile.hooks ?? null,
|
|
61
|
+
rebrand: profile.rebrand ?? null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Read-only: catalog summary.
|
|
66
|
+
|
|
67
|
+
export function profileList() {
|
|
68
|
+
return {
|
|
69
|
+
profiles: listProfiles().map((id) => {
|
|
70
|
+
const p = loadProfile(id);
|
|
71
|
+
return p
|
|
72
|
+
? {
|
|
73
|
+
id: p.id,
|
|
74
|
+
displayName: p.displayName ?? p.id,
|
|
75
|
+
tagline: p.tagline ?? null,
|
|
76
|
+
roleCount: Array.isArray(p.roles) ? p.roles.length : 0,
|
|
77
|
+
departmentCount: Array.isArray(p.departments) ? p.departments.length : 0,
|
|
78
|
+
}
|
|
79
|
+
: { id, error: 'failed to parse' };
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Read-only: drafts + custom profile under .cx/.
|
|
85
|
+
|
|
86
|
+
export function profileDrafts(args = {}) {
|
|
87
|
+
const cwd = cwdOf(args);
|
|
88
|
+
const drafts = listDrafts(cwd).map((d) => ({
|
|
89
|
+
id: d.id,
|
|
90
|
+
dir: d.dir,
|
|
91
|
+
hasProfile: d.hasProfile,
|
|
92
|
+
hasBrief: d.hasBrief,
|
|
93
|
+
}));
|
|
94
|
+
const custom = loadCustomProfile(cwd);
|
|
95
|
+
return {
|
|
96
|
+
drafts,
|
|
97
|
+
custom: custom ? { id: custom.id, displayName: custom.displayName ?? custom.id } : null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Read-only: per-profile health rollup.
|
|
102
|
+
|
|
103
|
+
export function profileHealthTool(args = {}) {
|
|
104
|
+
const cwd = cwdOf(args);
|
|
105
|
+
const id = args.id ? String(args.id) : resolveActiveProfile(cwd).id;
|
|
106
|
+
const windowDays = Number.isFinite(args.window_days) ? Number(args.window_days) : 30;
|
|
107
|
+
return { id, windowDays, ...profileHealth(cwd, id, { windowDays }) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Read-only: outcomes rollup. Optional aggregate=true rebuilds _summary.json.
|
|
111
|
+
|
|
112
|
+
export function outcomesSummary(args = {}) {
|
|
113
|
+
const cwd = cwdOf(args);
|
|
114
|
+
if (args.aggregate === true) {
|
|
115
|
+
try { aggregateOutcomes(cwd); } catch (err) { return { error: `aggregate failed: ${err.message}` }; }
|
|
116
|
+
}
|
|
117
|
+
const summary = readSummary(cwd);
|
|
118
|
+
if (!summary) return { roles: {}, note: 'no outcomes recorded yet' };
|
|
119
|
+
return summary;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Mutating, gated: append an outcome line.
|
|
123
|
+
|
|
124
|
+
export function outcomesRecord(args = {}) {
|
|
125
|
+
if (args.confirm !== true) {
|
|
126
|
+
return { error: 'outcomes_record requires confirm=true to write durable state' };
|
|
127
|
+
}
|
|
128
|
+
const cwd = cwdOf(args);
|
|
129
|
+
if (!args.role || typeof args.success !== 'boolean') {
|
|
130
|
+
return { error: 'outcomes_record requires role:string and success:boolean' };
|
|
131
|
+
}
|
|
132
|
+
const profile = args.profile ? String(args.profile) : resolveActiveProfile(cwd)?.id;
|
|
133
|
+
const file = recordOutcome(cwd, {
|
|
134
|
+
role: String(args.role),
|
|
135
|
+
intakeId: args.intake_id ? String(args.intake_id) : null,
|
|
136
|
+
profile,
|
|
137
|
+
success: !!args.success,
|
|
138
|
+
escalated: !!args.escalated,
|
|
139
|
+
durationMs: Number.isFinite(args.duration_ms) ? Number(args.duration_ms) : null,
|
|
140
|
+
notes: args.notes ? String(args.notes) : null,
|
|
141
|
+
source: args.source ? String(args.source) : 'mcp',
|
|
142
|
+
});
|
|
143
|
+
if (!file) return { error: 'outcomes_record: write failed' };
|
|
144
|
+
return { ok: true, file };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Mutating, gated: persist a research finding.
|
|
148
|
+
|
|
149
|
+
export async function knowledgeAdd(args = {}) {
|
|
150
|
+
if (args.confirm !== true) {
|
|
151
|
+
return { error: 'knowledge_add requires confirm=true to write durable state' };
|
|
152
|
+
}
|
|
153
|
+
const cwd = cwdOf(args);
|
|
154
|
+
try {
|
|
155
|
+
const res = await addResearchFinding({
|
|
156
|
+
cwd,
|
|
157
|
+
slug: String(args.slug || ''),
|
|
158
|
+
topic: String(args.topic || ''),
|
|
159
|
+
body: String(args.body || ''),
|
|
160
|
+
confidence: args.confidence ? String(args.confidence) : 'inferred',
|
|
161
|
+
sources: Array.isArray(args.sources) ? args.sources : [],
|
|
162
|
+
ttlDays: Number.isFinite(args.ttl_days) ? Number(args.ttl_days) : undefined,
|
|
163
|
+
});
|
|
164
|
+
return { ok: true, path: res.path, bytes: res.bytes };
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return { error: err.message ?? String(err) };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Mutating, gated: scaffold a draft profile.
|
|
171
|
+
|
|
172
|
+
export function profileCreate(args = {}) {
|
|
173
|
+
if (args.confirm !== true) {
|
|
174
|
+
return { error: 'profile_create requires confirm=true to scaffold files' };
|
|
175
|
+
}
|
|
176
|
+
const cwd = cwdOf(args);
|
|
177
|
+
if (!args.id) return { error: 'profile_create requires id:string' };
|
|
178
|
+
try {
|
|
179
|
+
const res = createDraftProfile({
|
|
180
|
+
cwd,
|
|
181
|
+
id: String(args.id),
|
|
182
|
+
displayName: args.display_name ? String(args.display_name) : undefined,
|
|
183
|
+
seedRoles: Array.isArray(args.seed_roles) ? args.seed_roles.map(String) : [],
|
|
184
|
+
seedDepartments: Array.isArray(args.seed_departments) ? args.seed_departments : [],
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
dir: res.dir,
|
|
189
|
+
briefPath: res.briefPath,
|
|
190
|
+
draftPath: res.draftPath,
|
|
191
|
+
personaPaths: res.personaPaths,
|
|
192
|
+
departmentPaths: res.departmentPaths,
|
|
193
|
+
};
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return { error: err.message ?? String(err) };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Mutating, gated, destructive: archive a curated profile.
|
|
200
|
+
|
|
201
|
+
export function profileArchive(args = {}) {
|
|
202
|
+
if (args.confirm !== true) {
|
|
203
|
+
return { error: 'profile_archive requires confirm=true (destructive: moves files into archive/profiles/)' };
|
|
204
|
+
}
|
|
205
|
+
if (!args.id || !args.reason || String(args.reason).trim().length < 8) {
|
|
206
|
+
return { error: 'profile_archive requires id:string and reason:string (>=8 chars)' };
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
return { ok: true, ...archiveProfile({ id: String(args.id), reason: String(args.reason) }) };
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return { error: err.message ?? String(err) };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Read-only: sandbox roster.
|
|
216
|
+
|
|
217
|
+
export function sandboxList() {
|
|
218
|
+
return { sandboxes: listSandboxes() };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Read-only: GraphRAG global query over the entity graph.
|
|
222
|
+
|
|
223
|
+
export function knowledgeGraphAsk(args = {}) {
|
|
224
|
+
const cwd = cwdOf(args);
|
|
225
|
+
if (!args.query || typeof args.query !== 'string') {
|
|
226
|
+
return { error: 'knowledge_graph_ask requires query:string' };
|
|
227
|
+
}
|
|
228
|
+
const topK = Number.isFinite(args.top_k) ? Number(args.top_k) : 5;
|
|
229
|
+
const minSize = Number.isFinite(args.min_size) ? Number(args.min_size) : undefined;
|
|
230
|
+
return askGlobal({ query: String(args.query), rootDir: cwd, topK, ...(minSize !== undefined ? { minSize } : {}) });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Read-only: learning dashboard mirror.
|
|
234
|
+
|
|
235
|
+
export function learningStatus(args = {}) {
|
|
236
|
+
const cwd = cwdOf(args);
|
|
237
|
+
|
|
238
|
+
let observations = { total: 0, last24h: 0 };
|
|
239
|
+
try {
|
|
240
|
+
const idxPath = join(cwd, '.cx', 'observations', 'index.json');
|
|
241
|
+
if (existsSync(idxPath)) {
|
|
242
|
+
const idx = JSON.parse(readFileSync(idxPath, 'utf8'));
|
|
243
|
+
if (Array.isArray(idx)) {
|
|
244
|
+
const since = Date.now() - 24 * 60 * 60 * 1000;
|
|
245
|
+
observations = {
|
|
246
|
+
total: idx.length,
|
|
247
|
+
last24h: idx.filter((e) => Date.parse(e?.createdAt) >= since).length,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch { /* best effort */ }
|
|
252
|
+
|
|
253
|
+
let researchCount = 0;
|
|
254
|
+
try {
|
|
255
|
+
const dir = join(cwd, '.cx', 'knowledge', 'external', 'research');
|
|
256
|
+
if (existsSync(dir)) {
|
|
257
|
+
researchCount = readdirSync(dir).filter((f) => f.endsWith('.md')).length;
|
|
258
|
+
}
|
|
259
|
+
} catch { /* best effort */ }
|
|
260
|
+
|
|
261
|
+
const profile = resolveActiveProfile(cwd);
|
|
262
|
+
const outcomes = readSummary(cwd) || { roles: {} };
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
profile: { id: profile.id, displayName: profile.displayName ?? profile.id, custom: profile.custom === true },
|
|
266
|
+
observations,
|
|
267
|
+
research: { count: researchCount },
|
|
268
|
+
outcomes,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
@@ -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 agents/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 (agents/registry.json, contracts.json,
|
|
5
|
+
* role-manifests.json, .cx/config.json, profile JSON) carry an integer
|
|
6
|
+
* `version` field. When the installed binary's expected schema version is
|
|
7
|
+
* higher than the on-disk version, migrations apply in order. When the on-disk
|
|
8
|
+
* version is higher, the runner refuses to load and instructs the operator to
|
|
9
|
+
* upgrade.
|
|
10
|
+
*
|
|
11
|
+
* Each migration is a module under lib/migrations/ that exports:
|
|
12
|
+
* - from: integer
|
|
13
|
+
* - to: integer
|
|
14
|
+
* - apply(artifactPath, options): Promise<{ changed, summary }>
|
|
15
|
+
*
|
|
16
|
+
* v1 is the baseline — no migration needed. The registry below is the contract
|
|
17
|
+
* between the installed binary and migration authors.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import v1Baseline from './v1-baseline.mjs';
|
|
21
|
+
|
|
22
|
+
const REGISTRY = [
|
|
23
|
+
v1Baseline,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export const CURRENT_SCHEMA_VERSION = Math.max(...REGISTRY.map((m) => m.to));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compute the migration path from `fromVersion` to CURRENT_SCHEMA_VERSION.
|
|
30
|
+
* Returns an array of migrations to apply in order, or null if no path exists.
|
|
31
|
+
*/
|
|
32
|
+
export function planMigrations(fromVersion, toVersion = CURRENT_SCHEMA_VERSION) {
|
|
33
|
+
if (fromVersion === toVersion) return [];
|
|
34
|
+
if (fromVersion > toVersion) return null;
|
|
35
|
+
const steps = [];
|
|
36
|
+
let cursor = fromVersion;
|
|
37
|
+
while (cursor < toVersion) {
|
|
38
|
+
const step = REGISTRY.find((m) => m.from === cursor);
|
|
39
|
+
if (!step) return null;
|
|
40
|
+
steps.push(step);
|
|
41
|
+
cursor = step.to;
|
|
42
|
+
}
|
|
43
|
+
return steps;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Apply a planned migration sequence. Each step receives the artifact path
|
|
48
|
+
* and any options. Returns a summary including every step's outcome.
|
|
49
|
+
*
|
|
50
|
+
* When dryRun is true, steps must not write to disk; they only report what
|
|
51
|
+
* they would change.
|
|
52
|
+
*/
|
|
53
|
+
export async function runMigrations({ artifactPath, fromVersion, toVersion = CURRENT_SCHEMA_VERSION, dryRun = false }) {
|
|
54
|
+
const plan = planMigrations(fromVersion, toVersion);
|
|
55
|
+
if (plan === null) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: `no migration path from version ${fromVersion} to ${toVersion}`,
|
|
59
|
+
applied: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (plan.length === 0) {
|
|
63
|
+
return { ok: true, applied: [], summary: 'no migrations needed' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const applied = [];
|
|
67
|
+
for (const step of plan) {
|
|
68
|
+
try {
|
|
69
|
+
const result = await step.apply(artifactPath, { dryRun });
|
|
70
|
+
applied.push({ from: step.from, to: step.to, ...result });
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error: `migration ${step.from}→${step.to} failed: ${err.message}`,
|
|
75
|
+
applied,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { ok: true, applied, summary: `applied ${applied.length} migration(s)` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check artifact compatibility. Returns:
|
|
84
|
+
* { compatible: true } — versions match
|
|
85
|
+
* { compatible: false, needsMigration: true, ... } — on-disk is older
|
|
86
|
+
* { compatible: false, needsUpgrade: true, ... } — on-disk is newer
|
|
87
|
+
*/
|
|
88
|
+
export function checkCompatibility(artifactVersion, expected = CURRENT_SCHEMA_VERSION) {
|
|
89
|
+
if (artifactVersion === expected) return { compatible: true };
|
|
90
|
+
if (artifactVersion < expected) {
|
|
91
|
+
return {
|
|
92
|
+
compatible: false,
|
|
93
|
+
needsMigration: true,
|
|
94
|
+
fromVersion: artifactVersion,
|
|
95
|
+
toVersion: expected,
|
|
96
|
+
message: `artifact schema version ${artifactVersion} is older than expected ${expected}; run 'construct migrate'`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
compatible: false,
|
|
101
|
+
needsUpgrade: true,
|
|
102
|
+
fromVersion: artifactVersion,
|
|
103
|
+
toVersion: expected,
|
|
104
|
+
message: `artifact schema version ${artifactVersion} is newer than this binary supports (${expected}); run 'construct upgrade'`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/migrations/v1-baseline.mjs — No-op baseline migration.
|
|
3
|
+
*
|
|
4
|
+
* Marks v0 (pre-versioned artifacts) as compatible with v1 by stamping the
|
|
5
|
+
* artifact with `version: 1` when absent. Schema bumps after v1 will add new
|
|
6
|
+
* migrations rather than modifying this one.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
from: 0,
|
|
13
|
+
to: 1,
|
|
14
|
+
description: 'Stamp pre-versioned artifacts with version: 1 (no shape changes)',
|
|
15
|
+
async apply(artifactPath, { dryRun = false } = {}) {
|
|
16
|
+
if (!existsSync(artifactPath)) {
|
|
17
|
+
return { changed: false, summary: `${artifactPath}: not present (skip)` };
|
|
18
|
+
}
|
|
19
|
+
let data;
|
|
20
|
+
try { data = JSON.parse(readFileSync(artifactPath, 'utf8')); }
|
|
21
|
+
catch (err) { throw new Error(`failed to parse ${artifactPath}: ${err.message}`); }
|
|
22
|
+
|
|
23
|
+
if (data && typeof data === 'object' && data.version === 1) {
|
|
24
|
+
return { changed: false, summary: `${artifactPath}: already at v1` };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const next = { ...data, version: 1 };
|
|
28
|
+
if (!dryRun) {
|
|
29
|
+
writeFileSync(artifactPath, JSON.stringify(next, null, 2) + '\n');
|
|
30
|
+
}
|
|
31
|
+
return { changed: true, summary: `${artifactPath}: stamped version: 1` };
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -19,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',
|
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
|
|
|
@@ -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.
|