@geraldmaron/construct 1.0.21 → 1.0.23
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 +23 -5
- package/bin/construct +191 -13
- package/bin/construct-postinstall.mjs +25 -15
- package/lib/acp/server.mjs +113 -0
- package/lib/agent-instructions/inject.mjs +94 -0
- package/lib/auto-docs.mjs +10 -2
- package/lib/cli-commands.mjs +43 -15
- package/lib/comment-lint.mjs +115 -0
- package/lib/completions.mjs +7 -1
- package/lib/config/schema.mjs +4 -0
- package/lib/decisions/enforced-baseline.json +2 -0
- package/lib/docs-verify.mjs +15 -18
- package/lib/doctor/cli.mjs +8 -1
- package/lib/document-export.mjs +124 -0
- package/lib/embed/daemon.mjs +1 -1
- package/lib/embed/inbox.mjs +25 -7
- package/lib/features.mjs +11 -11
- package/lib/git-hooks-path.mjs +61 -0
- package/lib/home-namespace.mjs +60 -0
- package/lib/hooks/ci-status-check.mjs +62 -40
- package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
- package/lib/hooks/pre-push-gate.mjs +15 -6
- package/lib/hooks/stop-notify.mjs +32 -17
- package/lib/hooks/stop-typecheck.mjs +7 -2
- package/lib/host-capabilities.mjs +24 -8
- package/lib/host-disposition.mjs +76 -0
- package/lib/ingest/provider-extract.mjs +1 -1
- package/lib/init-docs.mjs +1 -1
- package/lib/init-unified.mjs +320 -219
- package/lib/init-update.mjs +4 -84
- package/lib/init.mjs +9 -25
- package/lib/install/stage-project.mjs +8 -2
- package/lib/intent-classifier.mjs +1 -1
- package/lib/knowledge/search.mjs +52 -3
- package/lib/mcp/server.mjs +57 -14
- package/lib/mcp/tools/memory.mjs +2 -2
- package/lib/mcp/tools/orchestration-run.mjs +125 -0
- package/lib/model-registry.mjs +40 -33
- package/lib/observation-store.mjs +6 -2
- package/lib/opencode-config.mjs +1 -1
- package/lib/orchestration/events.mjs +66 -0
- package/lib/orchestration/runtime.mjs +70 -9
- package/lib/orchestration/worker.mjs +69 -21
- package/lib/orchestration-policy.mjs +27 -3
- package/lib/parity.mjs +46 -24
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-init-shared.mjs +0 -173
- package/lib/reconcile/adapter-prune.mjs +105 -0
- package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
- package/lib/reconcile/gitignore-coverage.mjs +88 -0
- package/lib/reconcile/index.mjs +171 -0
- package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
- package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
- package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
- package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
- package/lib/reconcile/postgres-namespace.mjs +102 -0
- package/lib/runtime/uv-bootstrap.mjs +27 -3
- package/lib/schema-infer.mjs +16 -2
- package/lib/server/csrf.mjs +14 -2
- package/lib/server/index.mjs +95 -0
- package/lib/service-manager.mjs +39 -14
- package/lib/setup-prompts.mjs +2 -1
- package/lib/setup.mjs +165 -141
- package/lib/storage/backend.mjs +14 -0
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/template-registry.mjs +73 -0
- package/lib/term-format.mjs +75 -0
- package/lib/uninstall/uninstall.mjs +180 -7
- package/package.json +2 -2
- package/personas/construct.md +7 -8
- package/platforms/claude/settings.template.json +30 -4
- package/platforms/opencode/config.template.json +2 -2
- package/rules/common/neurodivergent-output.md +66 -0
- package/rules/common/tool-invisibility.md +37 -0
- package/scripts/sync-specialists.mjs +381 -95
- package/skills/operating/orchestration-reference.md +2 -16
- package/specialists/policy-inventory.json +14 -0
- package/specialists/prompts/cx-accessibility.md +2 -6
- package/specialists/prompts/cx-ai-engineer.md +0 -4
- package/specialists/prompts/cx-architect.md +3 -5
- package/specialists/prompts/cx-business-strategist.md +0 -5
- package/specialists/prompts/cx-data-analyst.md +0 -4
- package/specialists/prompts/cx-data-engineer.md +0 -4
- package/specialists/prompts/cx-debugger.md +2 -6
- package/specialists/prompts/cx-designer.md +0 -8
- package/specialists/prompts/cx-devil-advocate.md +2 -2
- package/specialists/prompts/cx-docs-keeper.md +0 -13
- package/specialists/prompts/cx-engineer.md +0 -13
- package/specialists/prompts/cx-evaluator.md +2 -2
- package/specialists/prompts/cx-explorer.md +4 -5
- package/specialists/prompts/cx-legal-compliance.md +4 -5
- package/specialists/prompts/cx-operations.md +0 -5
- package/specialists/prompts/cx-orchestrator.md +0 -4
- package/specialists/prompts/cx-platform-engineer.md +0 -8
- package/specialists/prompts/cx-product-manager.md +0 -8
- package/specialists/prompts/cx-qa.md +2 -11
- package/specialists/prompts/cx-rd-lead.md +0 -5
- package/specialists/prompts/cx-release-manager.md +0 -8
- package/specialists/prompts/cx-researcher.md +5 -29
- package/specialists/prompts/cx-reviewer.md +2 -6
- package/specialists/prompts/cx-security.md +2 -11
- package/specialists/prompts/cx-sre.md +0 -15
- package/specialists/prompts/cx-test-automation.md +0 -4
- package/specialists/prompts/cx-trace-reviewer.md +2 -2
- package/specialists/prompts/cx-ux-researcher.md +0 -4
- package/specialists/registry.json +29 -29
- package/templates/distribution/run.mjs +36 -7
- package/templates/docs/accessibility-audit.md +56 -0
- package/templates/docs/architecture-review.md +59 -0
- package/templates/docs/code-review-report.md +46 -0
- package/templates/docs/construct_guide.md +14 -14
- package/templates/docs/debug-investigation.md +53 -0
- package/templates/docs/qa-report.md +48 -0
- package/templates/docs/security-audit-report.md +48 -0
- package/templates/docs/task-packet.md +49 -0
- package/templates/docs/verdict.md +40 -0
- package/lib/server/static/index.html +0 -1
package/lib/init-update.mjs
CHANGED
|
@@ -21,38 +21,6 @@ const dryRun = args.includes("--dry-run");
|
|
|
21
21
|
const cwdArg = args.find((arg) => arg.startsWith("--cwd="));
|
|
22
22
|
const cwd = cwdArg ? path.resolve(cwdArg.split("=")[1]) : process.cwd();
|
|
23
23
|
|
|
24
|
-
const REQUIRED_AGENT_SECTIONS = [
|
|
25
|
-
"Operating hierarchy",
|
|
26
|
-
"Start-of-session rules",
|
|
27
|
-
"Maintenance rules",
|
|
28
|
-
"End-of-session rules",
|
|
29
|
-
"Verification rules",
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
async function loadAgentsTemplate(projectName) {
|
|
33
|
-
const modulePath = path.join(import.meta.dirname, "project-init-shared.mjs");
|
|
34
|
-
const module = await import(modulePath);
|
|
35
|
-
if (!module.buildAgentsGuide) {
|
|
36
|
-
throw new Error("buildAgentsGuide not available from project-init-shared.mjs");
|
|
37
|
-
}
|
|
38
|
-
return module.buildAgentsGuide(projectName);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function inferProjectName(targetDir) {
|
|
42
|
-
const packagePath = path.join(targetDir, "package.json");
|
|
43
|
-
if (fs.existsSync(packagePath)) {
|
|
44
|
-
try {
|
|
45
|
-
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
46
|
-
if (pkg.name) return pkg.name;
|
|
47
|
-
} catch { /* fall through */ }
|
|
48
|
-
}
|
|
49
|
-
return path.basename(targetDir);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function findMissingAgentsSections(content) {
|
|
53
|
-
return REQUIRED_AGENT_SECTIONS.filter((section) => !content.toLowerCase().includes(section.toLowerCase()));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
24
|
function ensureProposalDir(targetDir) {
|
|
57
25
|
const dir = path.join(targetDir, ".cx", "proposals");
|
|
58
26
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -66,37 +34,6 @@ function writeProposal(targetDir, fileName, content) {
|
|
|
66
34
|
return proposalPath;
|
|
67
35
|
}
|
|
68
36
|
|
|
69
|
-
function buildAgentsProposal(existingContent, template, missingSections) {
|
|
70
|
-
const missingList = missingSections.map((section) => `- ${section}`).join("\n");
|
|
71
|
-
return [
|
|
72
|
-
"<!--",
|
|
73
|
-
"Construct init:update proposal",
|
|
74
|
-
"Review this file and merge the needed sections into AGENTS.md manually.",
|
|
75
|
-
"The original AGENTS.md was not modified.",
|
|
76
|
-
"-->",
|
|
77
|
-
"",
|
|
78
|
-
"# Proposed AGENTS.md Update",
|
|
79
|
-
"",
|
|
80
|
-
"## Why this proposal exists",
|
|
81
|
-
"",
|
|
82
|
-
"Your current `AGENTS.md` is missing these required sections:",
|
|
83
|
-
missingList,
|
|
84
|
-
"",
|
|
85
|
-
"## Current AGENTS.md",
|
|
86
|
-
"",
|
|
87
|
-
"```md",
|
|
88
|
-
existingContent.trimEnd(),
|
|
89
|
-
"```",
|
|
90
|
-
"",
|
|
91
|
-
"## Current Construct template",
|
|
92
|
-
"",
|
|
93
|
-
"```md",
|
|
94
|
-
template.trimEnd(),
|
|
95
|
-
"```",
|
|
96
|
-
"",
|
|
97
|
-
].join("\n");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
37
|
function buildWorkflowProposal(existingContent) {
|
|
101
38
|
const updatedContent = existingContent.replace(
|
|
102
39
|
/(\s+- run: node bin\/construct doctor\s*\n)/,
|
|
@@ -207,29 +144,12 @@ export function resolveTemplateConflict(conflict, resolution, targetDir) {
|
|
|
207
144
|
async function main() {
|
|
208
145
|
console.log(`Preparing Construct standards update for ${cwd}`);
|
|
209
146
|
|
|
210
|
-
const agentsPath = path.join(cwd, "AGENTS.md");
|
|
211
|
-
if (!fs.existsSync(agentsPath)) {
|
|
212
|
-
console.error("AGENTS.md not found. Run `construct init` first.");
|
|
213
|
-
process.exit(1);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const projectName = inferProjectName(cwd);
|
|
217
|
-
const existingAgents = fs.readFileSync(agentsPath, "utf8");
|
|
218
|
-
const template = await loadAgentsTemplate(projectName);
|
|
219
|
-
const missingSections = findMissingAgentsSections(existingAgents);
|
|
220
|
-
|
|
221
147
|
const planned = [];
|
|
222
148
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
write() {
|
|
228
|
-
return writeProposal(cwd, "AGENTS.md.construct-update.md", buildAgentsProposal(existingAgents, template, missingSections));
|
|
229
|
-
},
|
|
230
|
-
relativePath: proposalPath,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
149
|
+
// Construct's guidance in AGENTS.md/CLAUDE.md is owned by the versioned
|
|
150
|
+
// CONSTRUCT INTEGRATION marker block, kept current by `construct sync`
|
|
151
|
+
// (ADR-0027 §2/§4). init:update scope is opt-in standards a project owner
|
|
152
|
+
// merges by hand: CI checks and template conflicts.
|
|
233
153
|
|
|
234
154
|
const workflowPath = path.join(cwd, ".github", "workflows", "ci.yml");
|
|
235
155
|
if (fs.existsSync(workflowPath)) {
|
package/lib/init.mjs
CHANGED
|
@@ -15,7 +15,6 @@ import fs from "node:fs";
|
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
|
-
buildAgentsGuide,
|
|
19
18
|
buildContextJson,
|
|
20
19
|
buildContextMarkdown,
|
|
21
20
|
buildPlanTemplate,
|
|
@@ -43,14 +42,9 @@ function inferProjectName(targetPath) {
|
|
|
43
42
|
function main() {
|
|
44
43
|
const projectName = inferProjectName(target);
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
skipped,
|
|
50
|
-
filePath: path.join(target, "AGENTS.md"),
|
|
51
|
-
content: buildAgentsGuide(projectName),
|
|
52
|
-
generator: "construct/init",
|
|
53
|
-
});
|
|
45
|
+
// AGENTS.md is user-owned (ADR-0027 §2); Construct contributes only the fenced
|
|
46
|
+
// integration block via injectIntoAgentFile, never a doctrine body here.
|
|
47
|
+
|
|
54
48
|
writeStampedIfMissing({
|
|
55
49
|
targetRoot: target,
|
|
56
50
|
created,
|
|
@@ -75,22 +69,12 @@ function main() {
|
|
|
75
69
|
content: buildContextMarkdown(),
|
|
76
70
|
generator: "construct/init",
|
|
77
71
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
generator: "construct/init",
|
|
85
|
-
});
|
|
86
|
-
writeStampedIfMissing({
|
|
87
|
-
targetRoot: target,
|
|
88
|
-
created,
|
|
89
|
-
skipped,
|
|
90
|
-
filePath: path.join(target, ".cx", "inbox", ".gitkeep"),
|
|
91
|
-
content: "",
|
|
92
|
-
generator: "construct/init",
|
|
93
|
-
});
|
|
72
|
+
// `.cx/` already exists from the context writes above and is gitignored in full
|
|
73
|
+
// (ADR-0027), so a keep file there could never be tracked; `.cx/inbox/` is the local
|
|
74
|
+
// intake drop zone the watcher polls. Create the directories; write no dead keep file.
|
|
75
|
+
|
|
76
|
+
fs.mkdirSync(path.join(target, ".cx", "inbox"), { recursive: true });
|
|
77
|
+
created.push(".cx/inbox/");
|
|
94
78
|
|
|
95
79
|
process.stdout.write(`\nConstruct init → ${target}\n\n`);
|
|
96
80
|
if (created.length) {
|
|
@@ -14,7 +14,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
14
14
|
import { existsSync, copyFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
|
|
17
|
-
export function stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log }) {
|
|
17
|
+
export function stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log, hosts = null }) {
|
|
18
18
|
if (!projectRoot) throw new Error('stageProjectAdapters: projectRoot is required');
|
|
19
19
|
if (!packageRoot) throw new Error('stageProjectAdapters: packageRoot is required');
|
|
20
20
|
const emit = typeof log === 'function' ? log : () => {};
|
|
@@ -30,8 +30,14 @@ export function stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log
|
|
|
30
30
|
return { staged: true, synced: false };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// A `hosts` array restricts which adapter sets sync writes (construct-4xy6);
|
|
34
|
+
// null/empty leaves sync writing every host, preserving the postinstall path.
|
|
35
|
+
|
|
36
|
+
const syncArgs = [syncScript, '--project'];
|
|
37
|
+
if (Array.isArray(hosts) && hosts.length > 0) syncArgs.push(`--hosts=${hosts.join(',')}`);
|
|
38
|
+
|
|
33
39
|
emit(`syncing project adapters into ${projectRoot}/.claude/`);
|
|
34
|
-
const result = spawnSync(process.execPath,
|
|
40
|
+
const result = spawnSync(process.execPath, syncArgs, {
|
|
35
41
|
cwd: projectRoot,
|
|
36
42
|
stdio: 'inherit',
|
|
37
43
|
env: { ...process.env, CONSTRUCT_PROJECT_ROOT: projectRoot },
|
|
@@ -150,7 +150,7 @@ async function defaultModelCaller({ system, user }) {
|
|
|
150
150
|
headers: {
|
|
151
151
|
Authorization: `Bearer ${orKey}`,
|
|
152
152
|
'content-type': 'application/json',
|
|
153
|
-
'HTTP-Referer': 'https://github.com/construct',
|
|
153
|
+
'HTTP-Referer': 'https://github.com/geraldmaron/construct',
|
|
154
154
|
},
|
|
155
155
|
body: JSON.stringify({
|
|
156
156
|
model: modelId.replace(/^openrouter\//, ''),
|
package/lib/knowledge/search.mjs
CHANGED
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* 3. docs/README.md — docs index + contract
|
|
12
12
|
* 4. .cx/knowledge/ — operator-written internal docs
|
|
13
13
|
* 5. Any *.md in docs/cookbook/ — task-oriented recipes
|
|
14
|
+
* 6. <projectRoot>/.cx/knowledge/** — the cwd project's knowledge tree,
|
|
15
|
+
* including `external/research/` written by `construct knowledge add`,
|
|
16
|
+
* so foreign-project queries surface project content alongside bundled docs.
|
|
14
17
|
*
|
|
15
18
|
* Retrieval strategy:
|
|
16
19
|
* Token-based BM25-like scoring over 200-char chunks. Returns top-K chunks
|
|
@@ -30,8 +33,17 @@ const REPO_ROOT = join(MODULE_DIR, '..', '..');
|
|
|
30
33
|
|
|
31
34
|
// ─── Source catalogue ────────────────────────────────────────────────────────
|
|
32
35
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Priority-ordered list of source files to search.
|
|
38
|
+
*
|
|
39
|
+
* `projectRoot` covers the case where Construct is invoked from a foreign
|
|
40
|
+
* project (cwd != construct repo): the project's own `.cx/knowledge/**` —
|
|
41
|
+
* including the `external/research/` tree written by `construct knowledge add
|
|
42
|
+
* --source=research` — joins the source set, so freshly added project knowledge
|
|
43
|
+
* surfaces alongside (and ahead of) the bundled Construct docs. When
|
|
44
|
+
* projectRoot equals repoRoot or is absent, only the bundled set is searched.
|
|
45
|
+
*/
|
|
46
|
+
function buildSourceList(repoRoot = REPO_ROOT, projectRoot = null) {
|
|
35
47
|
const sources = [];
|
|
36
48
|
|
|
37
49
|
const priority = [
|
|
@@ -92,9 +104,45 @@ function buildSourceList(repoRoot = REPO_ROOT) {
|
|
|
92
104
|
}
|
|
93
105
|
}
|
|
94
106
|
|
|
107
|
+
// Project knowledge — the cwd project's `.cx/knowledge/**`. Searched only
|
|
108
|
+
// when the project is distinct from the construct repo (the latter's own
|
|
109
|
+
// .cx/knowledge is already covered above as repoRoot's internal knowledge).
|
|
110
|
+
// Priority 1 puts project content on the same scoring tier as core docs so a
|
|
111
|
+
// freshly added project finding outranks bundled framework material on tied
|
|
112
|
+
// matches; the `source: 'project'` tag lets consumers distinguish origin.
|
|
113
|
+
|
|
114
|
+
if (projectRoot && projectRoot !== repoRoot) {
|
|
115
|
+
const projectKnowledgeRoot = join(projectRoot, '.cx', 'knowledge');
|
|
116
|
+
if (existsSync(projectKnowledgeRoot)) {
|
|
117
|
+
for (const file of walkMarkdown(projectKnowledgeRoot)) {
|
|
118
|
+
sources.push({
|
|
119
|
+
path: file,
|
|
120
|
+
rel: relative(projectRoot, file),
|
|
121
|
+
priority: 1,
|
|
122
|
+
source: 'project',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
95
128
|
return sources;
|
|
96
129
|
}
|
|
97
130
|
|
|
131
|
+
// Recursive .md walk used for the project-knowledge tree, where `external/`
|
|
132
|
+
// nests one subdirectory deeper than the flat internal/reference/how-tos layout.
|
|
133
|
+
|
|
134
|
+
function walkMarkdown(root) {
|
|
135
|
+
const out = [];
|
|
136
|
+
let entries;
|
|
137
|
+
try { entries = readdirSync(root, { withFileTypes: true }); } catch { return out; }
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const full = join(root, entry.name);
|
|
140
|
+
if (entry.isDirectory()) out.push(...walkMarkdown(full));
|
|
141
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
98
146
|
// ─── Chunking ────────────────────────────────────────────────────────────────
|
|
99
147
|
|
|
100
148
|
const CHUNK_CHARS = 400;
|
|
@@ -272,7 +320,7 @@ export function knowledgeSearch({ query, topK = 5, minScore = 0.1, repoRoot, roo
|
|
|
272
320
|
|
|
273
321
|
const root = repoRoot ?? REPO_ROOT;
|
|
274
322
|
const dataDir = rootDir ?? (process.env.CX_DATA_DIR?.trim() || homedir());
|
|
275
|
-
const sources = buildSourceList(root);
|
|
323
|
+
const sources = buildSourceList(root, rootDir);
|
|
276
324
|
|
|
277
325
|
// Build corpus from docs + operator knowledge
|
|
278
326
|
const allChunks = [];
|
|
@@ -318,6 +366,7 @@ export function knowledgeSearch({ query, topK = 5, minScore = 0.1, repoRoot, roo
|
|
|
318
366
|
text: chunk.text,
|
|
319
367
|
heading: chunk.heading,
|
|
320
368
|
file: chunk.source.rel,
|
|
369
|
+
origin: chunk.source.source || 'bundled',
|
|
321
370
|
score: Math.round(score * 100) / 100,
|
|
322
371
|
lineStart: chunk.lineStart,
|
|
323
372
|
}));
|
package/lib/mcp/server.mjs
CHANGED
|
@@ -723,19 +723,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
723
723
|
required: ['query'],
|
|
724
724
|
},
|
|
725
725
|
},
|
|
726
|
-
{
|
|
727
|
-
name: 'rovo_search',
|
|
728
|
-
description: 'Cross-system semantic search via Atlassian Rovo. Searches Jira, Confluence, and other accessible sources. Returns excerpts with source attribution. Does NOT store results in the observation store. Use for broad queries across all sources, not just focal projects.',
|
|
729
|
-
inputSchema: {
|
|
730
|
-
type: 'object',
|
|
731
|
-
properties: {
|
|
732
|
-
query: { type: 'string', description: 'Search query (e.g., "Iverson reliability", "DR failover").' },
|
|
733
|
-
top_k: { type: 'number', description: 'Max results (default: 10, max: 50).' },
|
|
734
|
-
sources: { type: 'string', description: 'Comma-separated source filter (e.g., "jira,confluence").' },
|
|
735
|
-
},
|
|
736
|
-
required: ['query'],
|
|
737
|
-
},
|
|
738
|
-
},
|
|
739
726
|
{
|
|
740
727
|
name: 'efficiency_snapshot',
|
|
741
728
|
description: 'Returns the read-efficiency snapshot for the current session — repeated reads, large reads, hot-spot files, and recommendations. Use this when investigating why a session feels slow or to surface optimization opportunities.',
|
|
@@ -939,6 +926,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
939
926
|
properties: { cwd: { type: 'string', description: 'Project root (default: server cwd).' } },
|
|
940
927
|
},
|
|
941
928
|
},
|
|
929
|
+
{
|
|
930
|
+
name: 'document_export',
|
|
931
|
+
description: 'Convert a markdown file into a distributable PDF, DOCX, or HTML document via Pandoc (DOCX/HTML) and Typst (PDF engine), per ADR-0024. Both engines are optional system binaries discovered at runtime — the tool returns a structured "install <binary>" error when tooling is absent, never crashes. Use `detect_only=true` to check availability without running an export.',
|
|
932
|
+
inputSchema: {
|
|
933
|
+
type: 'object',
|
|
934
|
+
required: ['format'],
|
|
935
|
+
properties: {
|
|
936
|
+
input_path: { type: 'string', description: 'Absolute path to the markdown source file (required unless detect_only=true).' },
|
|
937
|
+
output_path: { type: 'string', description: 'Optional absolute output path; defaults to <input>.<format> next to the source.' },
|
|
938
|
+
format: { type: 'string', enum: ['pdf', 'docx', 'html'], description: 'Target format.' },
|
|
939
|
+
detect_only: { type: 'boolean', description: 'When true, report binary availability without exporting. Default false.' },
|
|
940
|
+
},
|
|
941
|
+
},
|
|
942
|
+
},
|
|
942
943
|
{
|
|
943
944
|
name: 'knowledge_search',
|
|
944
945
|
description: 'Search Construct\'s own documentation, knowledge base, and distilled embed observations. Call this immediately — no approval needed — when the user asks what Construct is, how a feature works, what commands exist, or anything about Construct\'s architecture or configuration. Also searches embed observations from GitHub, Jira, and other configured sources. Returns relevant excerpts with source file and heading.',
|
|
@@ -1176,6 +1177,38 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1176
1177
|
},
|
|
1177
1178
|
},
|
|
1178
1179
|
},
|
|
1180
|
+
{
|
|
1181
|
+
name: 'orchestration_run',
|
|
1182
|
+
description: 'EXECUTE a real multi-specialist orchestration run via the local Construct daemon and return per-specialist output — the executing counterpart to workflow_invoke (which only plans). For MCP hosts with no subagent primitive (VS Code/Copilot, Cursor), this is how you actually run a specialist chain: the engine owns orchestration, this tool is the thin client (ADR-0022). Requires a running daemon (`construct dashboard`); if it is unreachable the tool fails fast with how to start it rather than silently degrading to a single-persona pass. Real specialist output requires the daemon to run the `provider` worker backend (a provider key configured); the default `inline` backend prepares tasks only.',
|
|
1183
|
+
inputSchema: {
|
|
1184
|
+
type: 'object',
|
|
1185
|
+
required: ['request'],
|
|
1186
|
+
properties: {
|
|
1187
|
+
request: { type: 'string', description: 'Natural-language description of the work to orchestrate (e.g. "refactor the auth module and review it for security").' },
|
|
1188
|
+
workflow_type: { type: 'string', description: 'Optional workflow type to shape the plan (e.g. architecture-review, risk-review).' },
|
|
1189
|
+
requested_strategy: { type: 'string', enum: ['orchestrated', 'prompt-only', 'auto'], description: 'Execution strategy (default auto).' },
|
|
1190
|
+
worker_backend: { type: 'string', enum: ['inline', 'provider'], description: 'provider executes specialists (needs a provider key); inline prepares only. Default: daemon config.' },
|
|
1191
|
+
host: { type: 'string', description: 'Host/IDE identifier (advisory).' },
|
|
1192
|
+
host_model: { type: 'string', description: 'Model the host uses, for model resolution.' },
|
|
1193
|
+
host_provider: { type: 'string', description: 'Provider family the host uses, for model resolution.' },
|
|
1194
|
+
file_count: { type: 'number', description: 'Optional planning hint: number of files in scope.' },
|
|
1195
|
+
module_count: { type: 'number', description: 'Optional planning hint: number of modules in scope.' },
|
|
1196
|
+
wait: { type: 'boolean', description: 'Wait for the run to reach a terminal state and return task output (default true). false returns the runId immediately to poll with orchestration_status.' },
|
|
1197
|
+
timeout_ms: { type: 'number', description: 'Max time to wait when wait=true (default 120000). On timeout the runId is returned to poll.' },
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
name: 'orchestration_status',
|
|
1203
|
+
description: 'Inspect orchestration runs on the local Construct daemon: pass run_id for the full record (status, per-task status/executor/output/error), or omit it for a list of recent runs. Fails fast if the daemon is unreachable.',
|
|
1204
|
+
inputSchema: {
|
|
1205
|
+
type: 'object',
|
|
1206
|
+
properties: {
|
|
1207
|
+
run_id: { type: 'string', description: 'Run id to fetch. Omit to list recent runs.' },
|
|
1208
|
+
limit: { type: 'number', description: 'Max runs to list when run_id is omitted (default 20).' },
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
},
|
|
1179
1212
|
],
|
|
1180
1213
|
}));
|
|
1181
1214
|
|
|
@@ -1227,7 +1260,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1227
1260
|
else if (name === 'session_search') result = sessionSearch(args);
|
|
1228
1261
|
else if (name === 'session_save') result = sessionSave(args);
|
|
1229
1262
|
else if (name === 'memory_search') result = await memorySearch(args);
|
|
1230
|
-
else if (name === 'memory_add_observations') result = memoryAddObservations(args);
|
|
1263
|
+
else if (name === 'memory_add_observations') result = await memoryAddObservations(args);
|
|
1231
1264
|
else if (name === 'memory_create_entities') result = memoryCreateEntities(args);
|
|
1232
1265
|
else if (name === 'memory_recent') result = memoryRecent(args);
|
|
1233
1266
|
else if (name === 'rovo_search') result = await rovoSearch(args);
|
|
@@ -1241,6 +1274,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1241
1274
|
const { knowledgeSearch } = await import('../knowledge/search.mjs');
|
|
1242
1275
|
result = knowledgeSearch({ query: args.query, topK: args.top_k, repoRoot: args.repo_root, rootDir: args.root_dir });
|
|
1243
1276
|
}
|
|
1277
|
+
else if (name === 'document_export') {
|
|
1278
|
+
const { detect, exportMarkdown } = await import('../document-export.mjs');
|
|
1279
|
+
if (args.detect_only) {
|
|
1280
|
+
result = detect(args.format);
|
|
1281
|
+
} else {
|
|
1282
|
+
result = exportMarkdown({ inputPath: args.input_path, outputPath: args.output_path, format: args.format });
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1244
1285
|
else if (name === 'profile_show') result = profileShow(args);
|
|
1245
1286
|
else if (name === 'profile_list') result = profileList();
|
|
1246
1287
|
else if (name === 'profile_drafts') result = profileDrafts(args);
|
|
@@ -1258,6 +1299,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1258
1299
|
else if (name === 'workflow_invoke') result = await workflowInvoke(args);
|
|
1259
1300
|
else if (name === 'capability_describe') result = capabilityDescribe(args);
|
|
1260
1301
|
else if (name === 'construct_execution_resolve') result = executionResolve(args);
|
|
1302
|
+
else if (name === 'orchestration_run') { const m = await import('./tools/orchestration-run.mjs'); result = await m.orchestrationRun(args); }
|
|
1303
|
+
else if (name === 'orchestration_status') { const m = await import('./tools/orchestration-run.mjs'); result = await m.orchestrationStatus(args); }
|
|
1261
1304
|
else result = { error: `Unknown tool: ${name}` };
|
|
1262
1305
|
} catch (err) {
|
|
1263
1306
|
result = { error: err.message ?? String(err) };
|
package/lib/mcp/tools/memory.mjs
CHANGED
|
@@ -56,13 +56,13 @@ export async function memorySearch(args) {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function memoryAddObservations(args) {
|
|
59
|
+
export async function memoryAddObservations(args) {
|
|
60
60
|
const cwd = args.cwd ? resolve(String(args.cwd)) : process.cwd();
|
|
61
61
|
const project = args.project || cwd.split('/').pop() || 'unknown';
|
|
62
62
|
const observations = (args.observations || []).slice(0, 10);
|
|
63
63
|
const created = [];
|
|
64
64
|
for (const obs of observations) {
|
|
65
|
-
const record = addObservation(cwd, {
|
|
65
|
+
const record = await addObservation(cwd, {
|
|
66
66
|
role: obs.role || 'unknown',
|
|
67
67
|
category: obs.category || 'insight',
|
|
68
68
|
summary: obs.summary || '',
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mcp/tools/orchestration-run.mjs — MCP client for the local orchestration daemon.
|
|
3
|
+
*
|
|
4
|
+
* Executes a real multi-specialist run by driving the daemon's HTTP API
|
|
5
|
+
* (ADR-0022), so MCP hosts with no subagent primitive — VS Code/Copilot, Cursor —
|
|
6
|
+
* reach orchestrated outcomes through a tool they actually expose: the engine
|
|
7
|
+
* owns orchestration; this tool is the thin client. The daemon URL resolves from
|
|
8
|
+
* dashboard state (else the default loopback port) and the bearer token from
|
|
9
|
+
* ~/.construct/config.env. When the daemon is unreachable the tool fails fast
|
|
10
|
+
* with an actionable error rather than silently degrading to a single-persona
|
|
11
|
+
* pass — the absence of an executable path is made loud, not hidden.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readDashboardState } from '../../service-manager.mjs';
|
|
15
|
+
import { getDashboardToken } from '../../server/auth.mjs';
|
|
16
|
+
|
|
17
|
+
const TERMINAL = ['completed', 'completed-with-failures', 'cancelled', 'error'];
|
|
18
|
+
|
|
19
|
+
function resolveDaemon(env = process.env) {
|
|
20
|
+
const state = readDashboardState();
|
|
21
|
+
const base = (env.CONSTRUCT_ORCHESTRATION_URL || state?.url || `http://${env.BIND_HOST || '127.0.0.1'}:${env.PORT || '4242'}`).replace(/\/$/, '');
|
|
22
|
+
return { base, token: getDashboardToken(), daemonRunning: Boolean(state) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A reachability failure is the fail-fast signal MCP hosts need: report how to
|
|
26
|
+
// start the engine instead of letting the host fall back to a manual pass.
|
|
27
|
+
|
|
28
|
+
function unreachable(base, detail) {
|
|
29
|
+
return {
|
|
30
|
+
error: `Orchestration daemon not reachable at ${base}: ${detail}. Start it with \`construct dashboard\`, then retry. The engine runs orchestration; this host is a thin client (ADR-0022).`,
|
|
31
|
+
daemon: base,
|
|
32
|
+
failFast: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function orchestrationRun(args = {}, { fetchImpl = fetch, env = process.env } = {}) {
|
|
37
|
+
const {
|
|
38
|
+
request,
|
|
39
|
+
workflow_type,
|
|
40
|
+
requested_strategy = 'auto',
|
|
41
|
+
worker_backend,
|
|
42
|
+
host,
|
|
43
|
+
host_model,
|
|
44
|
+
host_provider,
|
|
45
|
+
file_count,
|
|
46
|
+
module_count,
|
|
47
|
+
wait = true,
|
|
48
|
+
timeout_ms = 120000,
|
|
49
|
+
} = args;
|
|
50
|
+
|
|
51
|
+
if (!request || typeof request !== 'string') return { error: 'Missing "request" string describing the work to orchestrate.' };
|
|
52
|
+
|
|
53
|
+
const { base, token } = resolveDaemon(env);
|
|
54
|
+
const headers = { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) };
|
|
55
|
+
const body = JSON.stringify({
|
|
56
|
+
request,
|
|
57
|
+
workflowType: workflow_type,
|
|
58
|
+
requestedStrategy: requested_strategy,
|
|
59
|
+
workerBackend: worker_backend,
|
|
60
|
+
host,
|
|
61
|
+
hostModel: host_model,
|
|
62
|
+
hostProvider: host_provider,
|
|
63
|
+
fileCount: file_count,
|
|
64
|
+
moduleCount: module_count,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let started;
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetchImpl(`${base}/api/orchestration/runs`, { method: 'POST', headers, body });
|
|
70
|
+
if (res.status === 401 || res.status === 403) {
|
|
71
|
+
return { error: `Daemon rejected the request (HTTP ${res.status}). The dashboard token is missing or wrong — check CONSTRUCT_DASHBOARD_TOKEN in ~/.construct/config.env.`, daemon: base, failFast: true };
|
|
72
|
+
}
|
|
73
|
+
const envelope = await res.json();
|
|
74
|
+
if (!res.ok) return { error: `Daemon error (HTTP ${res.status}): ${envelope.error || 'unknown'}`, daemon: base };
|
|
75
|
+
started = envelope.data;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return unreachable(base, err.message);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!wait) {
|
|
81
|
+
return { runId: started.runId, status: started.status, executionMode: started.execution?.executionMode, daemon: base, poll: 'orchestration_status with run_id' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const deadline = Date.now() + timeout_ms;
|
|
85
|
+
let run = started;
|
|
86
|
+
while (!TERMINAL.includes(run.status)) {
|
|
87
|
+
if (Date.now() > deadline) {
|
|
88
|
+
return { runId: started.runId, status: run.status, timedOut: true, daemon: base, note: 'Run is still executing; poll it with orchestration_status.' };
|
|
89
|
+
}
|
|
90
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetchImpl(`${base}/api/orchestration/runs/${encodeURIComponent(started.runId)}`, { headers });
|
|
93
|
+
const envelope = await res.json();
|
|
94
|
+
if (!res.ok) return { error: `Daemon error while polling (HTTP ${res.status}): ${envelope.error || 'unknown'}`, runId: started.runId, daemon: base };
|
|
95
|
+
run = envelope.data;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { error: `Lost connection to the daemon while polling run ${started.runId}: ${err.message}`, runId: started.runId, daemon: base, failFast: true };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
runId: run.runId,
|
|
103
|
+
status: run.status,
|
|
104
|
+
executionMode: run.execution?.executionMode,
|
|
105
|
+
degraded: run.execution?.degraded ?? false,
|
|
106
|
+
degradationReason: run.execution?.degradationReason ?? null,
|
|
107
|
+
tasks: (run.tasks || []).map((t) => ({ id: t.id, role: t.role, status: t.status, executor: t.executor, output: t.output ?? null, reasoning: t.reasoning ?? null, error: t.error ?? null })),
|
|
108
|
+
daemon: base,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function orchestrationStatus(args = {}, { fetchImpl = fetch, env = process.env } = {}) {
|
|
113
|
+
const { run_id, limit = 20 } = args;
|
|
114
|
+
const { base, token } = resolveDaemon(env);
|
|
115
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
116
|
+
const path = run_id ? `/api/orchestration/runs/${encodeURIComponent(run_id)}` : `/api/orchestration/runs?limit=${encodeURIComponent(limit)}`;
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetchImpl(`${base}${path}`, { headers });
|
|
119
|
+
const envelope = await res.json();
|
|
120
|
+
if (!res.ok) return { error: `Daemon error (HTTP ${res.status}): ${envelope.error || 'unknown'}`, daemon: base };
|
|
121
|
+
return envelope.data;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return unreachable(base, err.message);
|
|
124
|
+
}
|
|
125
|
+
}
|