@geraldmaron/construct 1.4.1 → 1.4.2
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/bin/construct +2 -1
- package/bin/construct-postinstall.mjs +27 -2
- package/config/tag-vocabulary.json +264 -0
- package/lib/config/schema.mjs +1 -1
- package/lib/doctor/diagnosis.mjs +20 -0
- package/lib/doctor/index.mjs +5 -0
- package/lib/embed/worker.mjs +5 -0
- package/lib/env-config.mjs +10 -2
- package/lib/export-validate.mjs +34 -2
- package/lib/host/readiness.mjs +109 -0
- package/lib/host-disposition.mjs +10 -1
- package/lib/install/stage-project.mjs +41 -4
- package/lib/intake/prepare.mjs +2 -0
- package/lib/mcp/destructive-approval.mjs +57 -0
- package/lib/mcp/server.mjs +208 -34
- package/lib/mcp/tool-rate-limit.mjs +47 -0
- package/lib/mcp/tool-safety.mjs +94 -0
- package/lib/mcp/tool-surface-parity.mjs +60 -0
- package/lib/mcp/tools/orchestration-run.mjs +45 -7
- package/lib/mcp/tools/project.mjs +25 -8
- package/lib/mcp/tools/storage.mjs +9 -1
- package/lib/mcp/tools/web-search-governance.mjs +96 -0
- package/lib/mcp/tools/web-search.mjs +5 -76
- package/lib/mcp-platform-config.mjs +27 -18
- package/lib/oracle/daemon-entry.mjs +6 -0
- package/lib/orchestration/runtime.mjs +81 -42
- package/lib/orchestration/web-capability.mjs +59 -0
- package/lib/orchestration/worker.mjs +263 -19
- package/lib/output-quality.mjs +61 -2
- package/lib/path-policy.mjs +56 -0
- package/lib/providers/secret-audit-wiring.mjs +35 -18
- package/lib/providers/secret-resolver.mjs +28 -13
- package/lib/registry/catalog.mjs +6 -0
- package/lib/registry/loader.mjs +7 -1
- package/lib/registry/validate.mjs +3 -3
- package/lib/sandbox.mjs +1 -1
- package/lib/service-manager.mjs +59 -9
- package/lib/storage/admin.mjs +7 -2
- package/package.json +6 -1
- package/registry/agent-manifest.json +117 -0
- package/registry/capabilities.json +1880 -0
- package/schemas/brand-voice.schema.json +24 -0
- package/schemas/capability-registry.schema.json +72 -0
- package/schemas/certification-run.schema.json +130 -0
- package/schemas/demo-recording.schema.json +46 -0
- package/schemas/eval-dataset.schema.json +79 -0
- package/schemas/execution-capability-profile.schema.json +46 -0
- package/schemas/execution-policy.schema.json +114 -0
- package/schemas/improvement-proposal.schema.json +65 -0
- package/schemas/mcp-tool-output.schema.json +61 -0
- package/schemas/platform-capabilities.schema.json +83 -0
- package/schemas/project-config.schema.json +227 -0
- package/schemas/project-demo.schema.json +60 -0
- package/schemas/provider-behavior-matrix.schema.json +91 -0
- package/schemas/scope.schema.json +197 -0
- package/schemas/specialist-trace.schema.json +107 -0
- package/schemas/team.schema.json +99 -0
- package/schemas/unified-registry.schema.json +548 -0
- package/scripts/sync-specialists.mjs +52 -25
- package/specialists/org/specialists/cx-researcher.json +1 -0
- package/specialists/prompts/cx-researcher.md +9 -8
- package/vendor/pandoc-ext/README.md +3 -0
- package/vendor/pandoc-ext/diagram.lua +687 -0
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* `$(op read 'op://...')` — it is resolved through the `op` CLI and cached for
|
|
13
13
|
* the process lifetime so a single launch prompts at most once per reference.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* The process.env tier follows one precedence ladder shared with loadConstructEnv:
|
|
16
|
+
* when file discovery is enabled (allowAmbient), the file tiers win over an ambient
|
|
17
|
+
* shell export so a stale export cannot shadow a managed config value, with the same
|
|
18
|
+
* carve-out that a materialized credential injected on the env beats a stored op://
|
|
19
|
+
* reference (preferInjectedCredential). A hermetic caller sets allowAmbient:false to
|
|
20
|
+
* suppress all file/rc discovery and use only the directly-injected env.
|
|
20
21
|
*
|
|
21
22
|
* resolveSecret performs the `op read` (and may surface a structured
|
|
22
23
|
* SecretResolutionError); hasSecret only checks presence and never invokes the
|
|
@@ -32,6 +33,7 @@ import os from 'node:os';
|
|
|
32
33
|
import { spawnSync } from 'node:child_process';
|
|
33
34
|
import { discoverAlternateRawForVar } from './credential-sources.mjs';
|
|
34
35
|
import { configDir } from '../config/xdg.mjs';
|
|
36
|
+
import { preferInjectedCredential } from '../env-config.mjs';
|
|
35
37
|
|
|
36
38
|
// The 1Password desktop integration prompts for biometric unlock on the first
|
|
37
39
|
// read after the vault locks, and that interactive round-trip routinely takes
|
|
@@ -181,9 +183,8 @@ function readFromOpEnvCatalog(varName, { env, home }) {
|
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
function rawCandidate(varName, { env, cwd, home }) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
// Pure file/store discovery: the ambient env tier is ranked by the callers
|
|
187
|
+
// (resolveSecret / hasSecret) so one precedence ladder governs env-vs-file.
|
|
187
188
|
// File tier matches loadConstructEnv: the project .env (closest to the work)
|
|
188
189
|
// wins over the machine-wide XDG config.env, which wins over a generic ~/.env.
|
|
189
190
|
const files = [
|
|
@@ -218,18 +219,32 @@ function resolveAndAudit(varName, raw, source, opts) {
|
|
|
218
219
|
|
|
219
220
|
export function resolveSecret(varName, { env = process.env, cwd = process.cwd(), allowAmbient = true, opRead } = {}) {
|
|
220
221
|
const direct = env?.[varName];
|
|
221
|
-
|
|
222
|
+
const hasDirect = typeof direct === 'string' && direct.length > 0;
|
|
223
|
+
|
|
224
|
+
// Hermetic mode: only the directly-injected env is consulted, no file/rc discovery,
|
|
225
|
+
// so a developer key never bleeds into an isolated run.
|
|
222
226
|
if (!allowAmbient) {
|
|
227
|
+
if (hasDirect) return resolveAndAudit(varName, direct, 'env', { opRead });
|
|
223
228
|
emitAudit({ event: 'secret.resolve', varName, source: null, isOpRef: false, ok: false });
|
|
224
229
|
return null;
|
|
225
230
|
}
|
|
231
|
+
|
|
226
232
|
const home = os.homedir();
|
|
227
233
|
const found = rawCandidate(varName, { env, cwd, home });
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
234
|
+
|
|
235
|
+
// Precedence parity with loadConstructEnv: the file tiers win over an ambient shell
|
|
236
|
+
// export, except a materialized credential injected on the env beats a stored op://
|
|
237
|
+
// reference so a single `op run` auth suffices.
|
|
238
|
+
if (found) {
|
|
239
|
+
if (hasDirect && preferInjectedCredential(direct, found.raw)) {
|
|
240
|
+
return resolveAndAudit(varName, direct, 'env', { opRead });
|
|
241
|
+
}
|
|
242
|
+
return resolveAndAudit(varName, found.raw, found.source, { opRead });
|
|
231
243
|
}
|
|
232
|
-
|
|
244
|
+
|
|
245
|
+
if (hasDirect) return resolveAndAudit(varName, direct, 'env', { opRead });
|
|
246
|
+
emitAudit({ event: 'secret.resolve', varName, source: null, isOpRef: false, ok: false });
|
|
247
|
+
return null;
|
|
233
248
|
}
|
|
234
249
|
|
|
235
250
|
export function resolveFirstSecret(varNames, opts = {}) {
|
package/lib/registry/catalog.mjs
CHANGED
|
@@ -71,6 +71,11 @@ function buildCapabilityEdges(cap, { allScripts, pkgScriptsBody }) {
|
|
|
71
71
|
return { cliCommands, npmScripts, workflows };
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function semanticDigest(cap) {
|
|
75
|
+
const { description = '', criticality = '', verification = {} } = cap;
|
|
76
|
+
return JSON.stringify({ description, criticality, verification });
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
export function buildCatalogSnapshot({ rootDir = REPO_ROOT, now = new Date() } = {}) {
|
|
75
80
|
const pkgPath = path.join(rootDir, 'package.json');
|
|
76
81
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
@@ -79,6 +84,7 @@ export function buildCatalogSnapshot({ rootDir = REPO_ROOT, now = new Date() } =
|
|
|
79
84
|
const raw = loadCapabilityRegistry({ rootDir });
|
|
80
85
|
const capabilities = (raw.capabilities ?? []).map((cap) => ({
|
|
81
86
|
...cap,
|
|
87
|
+
_semanticDigest: semanticDigest(cap),
|
|
82
88
|
edges: buildCapabilityEdges(cap, { allScripts, pkgScriptsBody }),
|
|
83
89
|
}));
|
|
84
90
|
|
package/lib/registry/loader.mjs
CHANGED
|
@@ -18,6 +18,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
18
18
|
const ROOT_DIR = path.resolve(__dirname, '..', '..');
|
|
19
19
|
|
|
20
20
|
let _registry = null;
|
|
21
|
+
let _rootDir = null;
|
|
21
22
|
let _orgMtime = null;
|
|
22
23
|
let _legacyOverlayMtime = null;
|
|
23
24
|
|
|
@@ -62,7 +63,10 @@ export function loadRegistry(opts = {}) {
|
|
|
62
63
|
legacyOverlay = null;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
// The cache key must include rootDir: two roots can share an orgMtime/overlay mtime
|
|
67
|
+
// (tmpdir tests, coincident timestamps), and a cache keyed only on mtimes bleeds one
|
|
68
|
+
// root's registry into another — a specialist can then surface without its promptFile.
|
|
69
|
+
if (_registry && _rootDir === rootDir && _orgMtime === orgMtime && _legacyOverlayMtime === legacyOverlayMtime) {
|
|
66
70
|
return _registry;
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -79,6 +83,7 @@ export function loadRegistry(opts = {}) {
|
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
_registry = registry;
|
|
86
|
+
_rootDir = rootDir;
|
|
82
87
|
_orgMtime = orgMtime;
|
|
83
88
|
_legacyOverlayMtime = legacyOverlayMtime;
|
|
84
89
|
|
|
@@ -127,6 +132,7 @@ export function listSpecialists(teamId, opts = {}) {
|
|
|
127
132
|
|
|
128
133
|
export function clearCache() {
|
|
129
134
|
_registry = null;
|
|
135
|
+
_rootDir = null;
|
|
130
136
|
_orgMtime = null;
|
|
131
137
|
_legacyOverlayMtime = null;
|
|
132
138
|
}
|
|
@@ -86,7 +86,7 @@ export function validateCapabilityRegistry({ rootDir = REPO_ROOT, now = Date.now
|
|
|
86
86
|
|
|
87
87
|
const hasTest = ver.functional || ver.hostEmulation || ver.untestableRationale;
|
|
88
88
|
if (!hasTest && (cap.criticality === 'P0' || cap.criticality === 'P1')) {
|
|
89
|
-
|
|
89
|
+
errors.push(`${cap.id}: P0/P1 capability has no functional test or untestableRationale`);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
const surfaces = cap.surfaces ?? {};
|
|
@@ -105,10 +105,10 @@ export function validateCapabilityRegistry({ rootDir = REPO_ROOT, now = Date.now
|
|
|
105
105
|
if (cap.lastValidated) {
|
|
106
106
|
const age = now - Date.parse(cap.lastValidated);
|
|
107
107
|
if (Number.isFinite(age) && age > STALE_MS) {
|
|
108
|
-
|
|
108
|
+
errors.push(`${cap.id}: lastValidated older than 90 days (${cap.lastValidated})`);
|
|
109
109
|
}
|
|
110
110
|
} else if (cap.criticality === 'P0') {
|
|
111
|
-
|
|
111
|
+
errors.push(`${cap.id}: P0 capability never validated (lastValidated null)`);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
for (const dep of cap.dependencies?.skills ?? []) {
|
package/lib/sandbox.mjs
CHANGED
|
@@ -55,7 +55,7 @@ export function createSandbox({ profile = null, seedContext = null } = {}) {
|
|
|
55
55
|
fs.writeFileSync(path.join(root, '.cx', 'context.md'), ctx);
|
|
56
56
|
|
|
57
57
|
const cfg = { version: 1, alias: 'Construct', deployment: { mode: 'solo' } };
|
|
58
|
-
if (profile) cfg.
|
|
58
|
+
if (profile) cfg.scope = profile;
|
|
59
59
|
fs.writeFileSync(path.join(root, 'construct.config.json'), JSON.stringify(cfg, null, 2) + '\n');
|
|
60
60
|
|
|
61
61
|
spawnSync('git', ['init', '--quiet'], { cwd: root, stdio: 'ignore' });
|
package/lib/service-manager.mjs
CHANGED
|
@@ -391,7 +391,16 @@ export async function startServices({
|
|
|
391
391
|
results.push({ name: 'Memory (cm)', url: `http://127.0.0.1:${ports.memory}`, status: 'reused' });
|
|
392
392
|
} else {
|
|
393
393
|
const cm = wrapServiceSpawn('cm', ['serve', '--port', String(ports.memory)], homeDir, { env: liveEnv });
|
|
394
|
-
spawnDetachedFn(cm.command, cm.args, homeDir, 'cm.log');
|
|
394
|
+
const cmSpawn = spawnDetachedFn(cm.command, cm.args, homeDir, 'cm.log');
|
|
395
|
+
try {
|
|
396
|
+
if (cmSpawn?.child?.pid) {
|
|
397
|
+
writeJson(portOwnershipPath(ports.memory, homeDir), {
|
|
398
|
+
pid: cmSpawn.child.pid, command: `${cm.command} ${cm.args.join(' ')}`,
|
|
399
|
+
cwd: process.cwd(), marker: 'construct', constructManaged: true,
|
|
400
|
+
startedAt: new Date().toISOString(),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
} catch { /* ownership record is advisory */ }
|
|
395
404
|
results.push({ name: 'Memory (cm)', url: `http://127.0.0.1:${ports.memory}`, status: 'started' });
|
|
396
405
|
}
|
|
397
406
|
} else if (wants('memory')) {
|
|
@@ -404,7 +413,16 @@ export async function startServices({
|
|
|
404
413
|
results.push({ name: 'OpenCode', url: `http://127.0.0.1:${ports.bridge}`, status: 'reused' });
|
|
405
414
|
} else {
|
|
406
415
|
const opencode = wrapServiceSpawn('opencode', ['serve', '--port', String(ports.bridge)], homeDir, { env: liveEnv });
|
|
407
|
-
spawnDetachedFn(opencode.command, opencode.args, homeDir, 'opencode.log');
|
|
416
|
+
const ocSpawn = spawnDetachedFn(opencode.command, opencode.args, homeDir, 'opencode.log');
|
|
417
|
+
try {
|
|
418
|
+
if (ocSpawn?.child?.pid) {
|
|
419
|
+
writeJson(portOwnershipPath(ports.bridge, homeDir), {
|
|
420
|
+
pid: ocSpawn.child.pid, command: `${opencode.command} ${opencode.args.join(' ')}`,
|
|
421
|
+
cwd: process.cwd(), marker: 'construct', constructManaged: true,
|
|
422
|
+
startedAt: new Date().toISOString(),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
} catch { /* ownership record is advisory */ }
|
|
408
426
|
results.push({ name: 'OpenCode', url: `http://127.0.0.1:${ports.bridge}`, status: 'started' });
|
|
409
427
|
}
|
|
410
428
|
}
|
|
@@ -420,7 +438,16 @@ export async function startServices({
|
|
|
420
438
|
const proxyPath = path.join(INSTALL_ROOT, 'lib', 'bridges', 'copilot-proxy.mjs');
|
|
421
439
|
if (fs.existsSync(proxyPath)) {
|
|
422
440
|
const copilot = wrapServiceSpawn('node', [proxyPath, '--port', String(ports.copilotBridge)], homeDir, { env: liveEnv });
|
|
423
|
-
spawnDetachedFn(copilot.command, copilot.args, homeDir, 'copilot-bridge.log');
|
|
441
|
+
const copilotSpawn = spawnDetachedFn(copilot.command, copilot.args, homeDir, 'copilot-bridge.log');
|
|
442
|
+
try {
|
|
443
|
+
if (copilotSpawn?.child?.pid) {
|
|
444
|
+
writeJson(portOwnershipPath(ports.copilotBridge, homeDir), {
|
|
445
|
+
pid: copilotSpawn.child.pid, command: `${copilot.command} ${copilot.args.join(' ')}`,
|
|
446
|
+
cwd: process.cwd(), marker: 'construct', constructManaged: true,
|
|
447
|
+
startedAt: new Date().toISOString(),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
} catch { /* ownership record is advisory */ }
|
|
424
451
|
results.push({ name: 'Copilot Bridge', url: `http://127.0.0.1:${ports.copilotBridge}`, status: 'started' });
|
|
425
452
|
} else {
|
|
426
453
|
results.push({ name: 'Copilot Bridge', status: 'unavailable', note: 'copilot-proxy binary missing in lib/bridges/' });
|
|
@@ -476,16 +503,39 @@ async function emitServiceFailures(results) {
|
|
|
476
503
|
} catch { /* best effort */ }
|
|
477
504
|
}
|
|
478
505
|
|
|
479
|
-
|
|
506
|
+
// Ownership record path for a port. startServices writes one when it spawns a service;
|
|
507
|
+
// killPortOwners reads it before sending SIGTERM so a foreign process that happens to
|
|
508
|
+
// occupy a configured port is never signalled.
|
|
509
|
+
|
|
510
|
+
function portOwnershipPath(port, homeDir) {
|
|
511
|
+
return path.join(runtimeStateDir(homeDir), `port-${port}.json`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Predicate: does a process record represent a Construct-owned service? A record is
|
|
515
|
+
// owned when it carries a Construct env/marker set at spawn time. A foreign process or
|
|
516
|
+
// a record from before the ownership contract was introduced is not owned.
|
|
517
|
+
|
|
518
|
+
export function isConstructOwnedPort(processRecord, { port } = {}) {
|
|
519
|
+
if (!processRecord || typeof processRecord !== 'object') return false;
|
|
520
|
+
return Boolean(processRecord.marker === 'construct' || processRecord.constructManaged === true);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function killPortOwners(port, spawnSyncFn = spawnSync, homeDir = os.homedir()) {
|
|
480
524
|
if (!port || !Number.isInteger(port) || port <= 0) return false;
|
|
481
525
|
try {
|
|
482
526
|
const result = spawnSyncFn('lsof', ['-t', `-i:${port}`], { encoding: 'utf8' });
|
|
483
527
|
const pids = (result.stdout || '').trim().split(/\s+/).filter(Boolean).map(Number).filter((n) => n > 0);
|
|
484
528
|
if (pids.length === 0) return false;
|
|
529
|
+
|
|
530
|
+
// Only signal a PID if we have a durable Construct ownership record for this port
|
|
531
|
+
// AND it names this exact PID. A foreign owner gets reported, not killed.
|
|
532
|
+
const ownerRecord = readJson(portOwnershipPath(port, homeDir));
|
|
533
|
+
let killed = false;
|
|
485
534
|
for (const pid of pids) {
|
|
486
|
-
|
|
535
|
+
if (!ownerRecord || ownerRecord.pid !== pid || !isConstructOwnedPort(ownerRecord)) continue;
|
|
536
|
+
try { process.kill(pid, 'SIGTERM'); killed = true; } catch { /* already gone */ }
|
|
487
537
|
}
|
|
488
|
-
return
|
|
538
|
+
return killed;
|
|
489
539
|
} catch {
|
|
490
540
|
return false;
|
|
491
541
|
}
|
|
@@ -507,15 +557,15 @@ export async function stopServices({
|
|
|
507
557
|
const envPath = getUserEnvPath(homeDir);
|
|
508
558
|
const envValues = parseEnvFile(envPath);
|
|
509
559
|
const memoryPort = Number(envValues.MEMORY_PORT) || derivedMemoryPort();
|
|
510
|
-
const cmKilled = killPortOwners(memoryPort, spawnSyncFn);
|
|
560
|
+
const cmKilled = killPortOwners(memoryPort, spawnSyncFn, homeDir);
|
|
511
561
|
results.push({ name: 'Memory (cm)', status: cmKilled ? 'stopped' : 'not-running' });
|
|
512
562
|
|
|
513
563
|
const bridgePort = Number(envValues.BRIDGE_PORT) || 5173;
|
|
514
|
-
const openCodeKilled = killPortOwners(bridgePort, spawnSyncFn);
|
|
564
|
+
const openCodeKilled = killPortOwners(bridgePort, spawnSyncFn, homeDir);
|
|
515
565
|
results.push({ name: 'OpenCode', status: openCodeKilled ? 'stopped' : 'not-running' });
|
|
516
566
|
|
|
517
567
|
const copilotBridgePort = Number(envValues.COPILOT_BRIDGE_PORT) || 5174;
|
|
518
|
-
const copilotBridgeKilled = killPortOwners(copilotBridgePort, spawnSyncFn);
|
|
568
|
+
const copilotBridgeKilled = killPortOwners(copilotBridgePort, spawnSyncFn, homeDir);
|
|
519
569
|
results.push({ name: 'Copilot Bridge', status: copilotBridgeKilled ? 'stopped' : 'not-running' });
|
|
520
570
|
|
|
521
571
|
const stopped = results.filter((r) => r.status === 'stopped' || r.status === 'cleaned').map((r) => r.name);
|
package/lib/storage/admin.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { resolveWithinRoot } from '../path-policy.mjs';
|
|
6
7
|
|
|
7
8
|
export function inferProjectName(cwd) {
|
|
8
9
|
return path.basename(cwd) || 'construct';
|
|
@@ -54,8 +55,12 @@ export async function deleteIngestedArtifacts(rootDir, { files = [], confirm = f
|
|
|
54
55
|
let deletedCount = 0;
|
|
55
56
|
const internalDir = path.join(rootDir, '.cx', 'knowledge', 'internal');
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
// Resolve every model-supplied entry against the ingested root first; an escaping
|
|
59
|
+
// path throws before any deletion, so a ../ traversal can neither delete an out-of-root
|
|
60
|
+
// file nor partially delete the set.
|
|
61
|
+
|
|
62
|
+
const targets = files.map((f) => resolveWithinRoot(internalDir, f));
|
|
63
|
+
for (const p of targets) {
|
|
59
64
|
if (fs.existsSync(p)) {
|
|
60
65
|
fs.rmSync(p);
|
|
61
66
|
deletedCount++;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geraldmaron/construct",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "npm@11.5.1",
|
|
6
6
|
"description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"platforms/**",
|
|
27
27
|
"templates/**",
|
|
28
28
|
"templates/distribution/**",
|
|
29
|
+
"schemas/**",
|
|
30
|
+
"registry/**",
|
|
31
|
+
"config/**",
|
|
32
|
+
"vendor/**",
|
|
29
33
|
"scripts/sync-specialists.mjs",
|
|
30
34
|
"README.md",
|
|
31
35
|
".env.example"
|
|
@@ -120,6 +124,7 @@
|
|
|
120
124
|
"@eslint/js": "^9.39.4",
|
|
121
125
|
"c8": "^11.0.0",
|
|
122
126
|
"esbuild": "^0.28.1",
|
|
127
|
+
"postject": "1.0.0-alpha.6",
|
|
123
128
|
"eslint": "^10.4.1",
|
|
124
129
|
"globals": "^17.6.0",
|
|
125
130
|
"ink-testing-library": "^4.0.0"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://construct.dev/schemas/agent-manifest.v1.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"kind": "agent-execution-manifest",
|
|
5
|
+
"description": "How an agent should reach Construct: MCP-first tool surface, with OpenCode as the primary human conversation surface. Source-of-truth values (core tool set, long-tail dispatcher) are verified against lib/mcp/server.mjs by tests/agent-manifest.test.mjs.",
|
|
6
|
+
"sources": {
|
|
7
|
+
"coreTools": "lib/mcp/server.mjs (CORE_TOOL_NAMES)",
|
|
8
|
+
"longTail": "lib/mcp/server.mjs (CONSTRUCT_CALL_TOOL)",
|
|
9
|
+
"surfaceTiers": "lib/registry/surface-map.mjs (SURFACE_TIERS, COMMAND_SURFACE)",
|
|
10
|
+
"capabilities": "registry/capabilities.json",
|
|
11
|
+
"credentials": "lib/mcp/server.mjs (config.env injection), lib/providers/secret-resolver.mjs"
|
|
12
|
+
},
|
|
13
|
+
"toolSurface": {
|
|
14
|
+
"model": "core-plus-long-tail",
|
|
15
|
+
"rationale": "ListTools front-loads a small core; everything else is reachable through the `call` gateway or discovered by intent via `find_tool`, so the schema stays small and small models stop hallucinating tool names. The long tail is reachable, just not front-loaded.",
|
|
16
|
+
"core": [
|
|
17
|
+
{
|
|
18
|
+
"name": "orchestration_policy",
|
|
19
|
+
"use": "Classify intent and get the specialist routing policy for a request."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "orchestration_run",
|
|
23
|
+
"use": "Dispatch the routed specialist chain for a request after classifying it."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "get_skill",
|
|
27
|
+
"use": "Fetch a role or domain skill playbook by id."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "get_template",
|
|
31
|
+
"use": "Read a doc template (prd, adr, runbook, ...) by name before authoring an artifact."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "search_skills",
|
|
35
|
+
"use": "Find relevant skills by query."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "knowledge_search",
|
|
39
|
+
"use": "Search the project knowledge base."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "memory_search",
|
|
43
|
+
"use": "Search durable cross-session memory."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "project_context",
|
|
47
|
+
"use": "Read the active project context and working state."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "summarize_diff",
|
|
51
|
+
"use": "Summarize a code diff for review or handoff."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "find_tool",
|
|
55
|
+
"use": "Find tools by intent when you don't know the exact name; returns ranked tools with schemas."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "author_artifact",
|
|
59
|
+
"use": "Materialize a typed artifact you drafted (PRD/ADR/RFC/…) and run the release gate."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "document_export",
|
|
63
|
+
"use": "Convert a markdown file to PDF/DOCX/HTML/deck and write it to a path."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "publish_run",
|
|
67
|
+
"use": "Run the publish pipeline to export an artifact (figures and demos optional)."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "artifact_workflow",
|
|
71
|
+
"use": "Plan a manifest-backed artifact workflow; validate or export locally."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "workflow_invoke",
|
|
75
|
+
"use": "Invoke an embedded Construct workflow (prd-draft, architecture-review, …)."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "triage_recommend",
|
|
79
|
+
"use": "Recommend intake triage — type, owner, and routing — for a new signal."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "orchestration_readiness",
|
|
83
|
+
"use": "Verify that the active MCP session has Construct orchestration tools attached; returns a typed failure reason and next step."
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"longTail": {
|
|
87
|
+
"tool": "call",
|
|
88
|
+
"use": "Invoke any non-core Construct tool by name: put the tool name in `tool` and its arguments in `args`. Call find_tool to discover the right tool by intent; the catalog of valid names is the `tool` enum.",
|
|
89
|
+
"rationale": "One dispatcher reuses the same handlers, so collapsing the surface costs no capability — every tool stays reachable by name. The manifest deliberately does not enumerate the full tool list; the live catalog is the `call` enum in lib/mcp/server.mjs, and find_tool ranks it by intent. The former gateway name `construct_call` is recovered tolerantly, so a stale name still resolves."
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"humanEntry": {
|
|
93
|
+
"command": "opencode",
|
|
94
|
+
"subcommand": null,
|
|
95
|
+
"use": "A human uses OpenCode as the primary conversation surface after `construct sync` installs the Construct front-door agent, MCP wiring, and runtime plugin. The `construct` CLI remains setup/admin/headless infrastructure.",
|
|
96
|
+
"rationale": "Construct no longer ships a local conversation UI; OpenCode is primary and other public hosts use generated adapters or MCP.",
|
|
97
|
+
"surface": "opencode"
|
|
98
|
+
},
|
|
99
|
+
"credentials": {
|
|
100
|
+
"policy": "mcp-mediated",
|
|
101
|
+
"guidance": "Agents never resolve secrets themselves. The Construct MCP server applies config.env to its process environment at startup, so provider credentials are present for every tool call. The shared secret resolver walks the trusted sources (process env, config.env, ~/.env, project .env, shell rc) and resolves any 1Password reference internally.",
|
|
102
|
+
"doNot": "Do not instruct or run manual `op read` / `op://` lookups, and do not ask the user to paste keys. MCP tool outputs expose only env-key names and a configured boolean, never credential values.",
|
|
103
|
+
"source": "lib/mcp/server.mjs config.env injection; lib/providers/secret-resolver.mjs; lib/providers/credential-bootstrap.mjs"
|
|
104
|
+
},
|
|
105
|
+
"mcpGaps": {
|
|
106
|
+
"note": "True gaps only: capabilities that declare MCP unsupported. Flag variants of an MCP-backed capability and retired commands are not gaps.",
|
|
107
|
+
"gaps": [
|
|
108
|
+
{
|
|
109
|
+
"id": "oracle.meta-review",
|
|
110
|
+
"name": "Oracle Meta-Review",
|
|
111
|
+
"mcp": "unsupported",
|
|
112
|
+
"cliFallback": "construct oracle review",
|
|
113
|
+
"reason": "Declares surfaces.mcp.supported=false in registry/capabilities.json; reach it via the CLI --json twin."
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
}
|