melchizedek-agents 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/DOCUMENTATION.md +11 -6
- package/dist/lib/doctor.d.ts +10 -0
- package/dist/lib/doctor.js +33 -4
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/loadSyndicate.js +3 -4
- package/dist/lib/project.d.ts +116 -0
- package/dist/lib/project.js +274 -0
- package/dist/scripts/a2a_server.js +3 -3
- package/dist/scripts/doctor.js +2 -1
- package/dist/scripts/mcp_server.js +2 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,36 @@ the starter pack and the templates), not the repo's full history.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 1.8.0 — 2026-10-18
|
|
10
|
+
|
|
11
|
+
A minor release that gives a repository a manifest (ADR 0138). A
|
|
12
|
+
`melchizedek.json` at the root names the agents folder, the exact engine
|
|
13
|
+
version the project is written for, and how each syndicate ships; the
|
|
14
|
+
bins find it on their own, `melchizedek-doctor` checks the pin, and the
|
|
15
|
+
desktop app opens a repository through it. Nothing changes for a
|
|
16
|
+
deployment that sets `MELCHIZEDEK_AGENTS_DIR`.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **The project manifest, `melchizedek.json` (ADR 0138).** A JSON file at
|
|
21
|
+
a repository's root naming its agents folder (`agents`), the engine
|
|
22
|
+
version it is written for (`engine`, an exact pin), an `ignore` list,
|
|
23
|
+
a `tests` folder, the env variable *names* it needs, and per-file
|
|
24
|
+
shipping records (`syndicates.<file>.deliverable`,
|
|
25
|
+
`publish: { kind: "registry", id }`). New subpath
|
|
26
|
+
`melchizedek-agents/project` and barrel exports: `findProject`,
|
|
27
|
+
`loadProject`, `parseProjectManifest`, `resolveAgentsDir`,
|
|
28
|
+
`listProjectSyndicates`, `checkProject`, `projectManifestSchema`.
|
|
29
|
+
- **The agents directory reads the manifest.** Every loader and bin
|
|
30
|
+
resolves it as: explicit option, `MELCHIZEDEK_AGENTS_DIR`, the nearest
|
|
31
|
+
manifest's `agents` (not above the repository root), then
|
|
32
|
+
`<cwd>/config/agents`. A deployment that sets the variable is
|
|
33
|
+
unchanged.
|
|
34
|
+
- **`melchizedek-doctor` reports the project**: the engine pin against
|
|
35
|
+
the running engine, a missing agents or tests folder, `ignore`
|
|
36
|
+
patterns that match nothing, unset env names, and `syndicates` keys
|
|
37
|
+
with no file. A pin mismatch or a missing folder fails `--check`.
|
|
38
|
+
|
|
9
39
|
## 1.7.0 — 2026-10-17
|
|
10
40
|
|
|
11
41
|
A minor release that lets a syndicate hold code (ADR 0137). A `script:`
|
package/DOCUMENTATION.md
CHANGED
|
@@ -1610,22 +1610,27 @@ subclass), and register it behind a model-id prefix.
|
|
|
1610
1610
|
subagent. `scripts/demo_mcp_server.ts` is a complete server to copy —
|
|
1611
1611
|
tool definitions, SSE wiring, and persistent state in ~250 lines.
|
|
1612
1612
|
|
|
1613
|
-
**Teach your coding agent the framework**: `skills/` holds
|
|
1613
|
+
**Teach your coding agent the framework**: `skills/` holds eleven Agent
|
|
1614
1614
|
Skills (the open SKILL.md standard — a directory per skill, `name` and
|
|
1615
1615
|
`description` frontmatter) that give Claude Code, Codex, Cursor,
|
|
1616
1616
|
OpenCode or Gemini CLI the catalog of syndicates and the procedures in
|
|
1617
1617
|
this document: `melchizedek` (find and run, delegate a task),
|
|
1618
1618
|
`melchizedek-author`, `melchizedek-serve`, `melchizedek-memory`,
|
|
1619
|
-
`melchizedek-models`, `melchizedek-scribe
|
|
1619
|
+
`melchizedek-models`, `melchizedek-scribe`, and five onboarding skills
|
|
1620
|
+
that route a person by the credentials they have — `melchizedek-onboard`
|
|
1621
|
+
(the triage) and `melchizedek-onboard-local`, `-keys`, `-cloud` and
|
|
1622
|
+
`-serve` (the authentication levels each owns). `npx melchizedek-skills
|
|
1620
1623
|
install` copies them into `.claude/skills/` and `.agents/skills/` of the
|
|
1621
1624
|
current project (`--for claude,codex,cursor,opencode,gemini,agents,all`,
|
|
1622
1625
|
`--global`, `--dir <path>`, `--only <names>`, `--force`, `--dry-run`;
|
|
1623
1626
|
`npm run skills:install` in a clone). The installer (`lib/skills.ts`)
|
|
1624
1627
|
copies only from the package's own `skills/` directory, follows no
|
|
1625
|
-
symlinks, and never overwrites a differing file without `--force`.
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
by
|
|
1628
|
+
symlinks, and never overwrites a differing file without `--force`. The
|
|
1629
|
+
onboarding skills are short procedures written by hand that print their
|
|
1630
|
+
facts from `melchizedek-setup --level <id>`; every other SKILL.md body was
|
|
1631
|
+
written by the Scribe syndicate (`config/agents/examples/scribe.yaml`)
|
|
1632
|
+
from a brief of facts and reviewed by a person; `skills/README.md`
|
|
1633
|
+
records the procedure.
|
|
1629
1634
|
|
|
1630
1635
|
## 8. Security notes
|
|
1631
1636
|
|
package/dist/lib/doctor.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type { Platform } from './models/endpoints.ts';
|
|
|
27
27
|
import type { CapabilityGap, CapabilityReport } from './models/capabilities.ts';
|
|
28
28
|
import type { ProviderId } from './models/providerMap.ts';
|
|
29
29
|
import type { RuntimeName, RuntimeSource } from './runtime/runtimeFlag.ts';
|
|
30
|
+
import { type ProjectCheck } from './project.ts';
|
|
30
31
|
/**
|
|
31
32
|
* What a file costs to run: `keyless` (all Ollama), a single provider's id
|
|
32
33
|
* when every agent shares one (`gemini`, `anthropic`, `openai`, `xai` — one
|
|
@@ -169,7 +170,16 @@ export interface DoctorResult {
|
|
|
169
170
|
serving?: DoctorServing;
|
|
170
171
|
/** A stored Sign in with ChatGPT credential (ADR 0126), when one is present or its variables are set. */
|
|
171
172
|
chatgpt?: DoctorChatGpt;
|
|
173
|
+
/** The project manifest (ADR 0138) found from the working directory, when there is one. */
|
|
174
|
+
project?: DoctorProject;
|
|
172
175
|
}
|
|
176
|
+
/** A `melchizedek.json` at or above the working directory: its checks, or why it does not load. */
|
|
177
|
+
export type DoctorProject = ProjectCheck | {
|
|
178
|
+
root: string;
|
|
179
|
+
error: string;
|
|
180
|
+
problems: string[];
|
|
181
|
+
warnings: string[];
|
|
182
|
+
};
|
|
173
183
|
/**
|
|
174
184
|
* A Sign in with ChatGPT credential (ADR 0126): where it is, whether it is
|
|
175
185
|
* signed in and carries OpenAI ids here, and what stops it. Never a token.
|
package/dist/lib/doctor.js
CHANGED
|
@@ -42,6 +42,7 @@ import { OAUTH_HOSTS_ENV } from './tools/oauthHosts.js';
|
|
|
42
42
|
import { CREDENTIAL_HOSTS_ENV, credentialHosts } from './tools/credentialHosts.js';
|
|
43
43
|
import { syndicateCredentialUses } from './tools/credentialUses.js';
|
|
44
44
|
import { credentialHostEnvProblems, unboundCredentialWarning } from './a2a/oauthSetup.js';
|
|
45
|
+
import { checkProject, findProject, isIgnored, ProjectManifestError, resolveAgentsDir } from './project.js';
|
|
45
46
|
/** The `gateway.id` the doctor reports when `MODEL_GATEWAY` names no known gateway; the value itself is never kept. */
|
|
46
47
|
const UNRECOGNISED_GATEWAY = 'unrecognised';
|
|
47
48
|
function resolvable(module) {
|
|
@@ -457,10 +458,11 @@ export function flagMemoryNamespaces(syndicates) {
|
|
|
457
458
|
}
|
|
458
459
|
}
|
|
459
460
|
export function runDoctor(options = {}) {
|
|
460
|
-
const agentsDir =
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
461
|
+
const agentsDir = resolveAgentsDir(options.agentsDir);
|
|
462
|
+
const { project, report: projectReport } = projectFromCwd();
|
|
463
|
+
// The manifest's ignore list applies when the doctor reads the project's own folder.
|
|
464
|
+
const listed = listSyndicateFiles(agentsDir).filter((f) => !project || agentsDir !== project.agentsDir || !isIgnored(project, path.relative(project.root, path.join(agentsDir, f))));
|
|
465
|
+
const syndicates = listed.map((f) => diagnoseSyndicate(f, agentsDir, options.scripts));
|
|
464
466
|
// Which single variable unlocks what. A syndicate counts toward a
|
|
465
467
|
// variable only when that variable is ALL it is missing.
|
|
466
468
|
const byEnv = new Map();
|
|
@@ -541,8 +543,22 @@ export function runDoctor(options = {}) {
|
|
|
541
543
|
providers: providerPaths(),
|
|
542
544
|
...(serving ? { serving } : {}),
|
|
543
545
|
...(chatgpt ? { chatgpt } : {}),
|
|
546
|
+
...(projectReport ? { project: projectReport } : {}),
|
|
544
547
|
};
|
|
545
548
|
}
|
|
549
|
+
/** The project at or above the working directory, checked; a broken manifest becomes a problem, not a throw. */
|
|
550
|
+
function projectFromCwd() {
|
|
551
|
+
try {
|
|
552
|
+
const project = findProject();
|
|
553
|
+
return project ? { project, report: checkProject(project) } : {};
|
|
554
|
+
}
|
|
555
|
+
catch (e) {
|
|
556
|
+
if (!(e instanceof ProjectManifestError))
|
|
557
|
+
throw e;
|
|
558
|
+
const problem = `melchizedek.json: ${e.issues.join('; ')}`;
|
|
559
|
+
return { report: { root: path.dirname(e.file), error: problem, problems: [problem], warnings: [] } };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
546
562
|
/**
|
|
547
563
|
* The Sign in with ChatGPT line (ADR 0126), when a credential file is present
|
|
548
564
|
* or one of its variables is set. `served`: a serving variable is set, so
|
|
@@ -613,6 +629,8 @@ export function checkProblems(result) {
|
|
|
613
629
|
problems.push(`credentials: ${p}`);
|
|
614
630
|
for (const p of result.chatgpt?.problems ?? [])
|
|
615
631
|
problems.push(`chatgpt: ${p}`);
|
|
632
|
+
for (const p of result.project?.problems ?? [])
|
|
633
|
+
problems.push(`project: ${p}`);
|
|
616
634
|
for (const s of result.syndicates) {
|
|
617
635
|
for (const g of s.grants ?? [])
|
|
618
636
|
for (const p of g.hostProblems ?? [])
|
|
@@ -635,6 +653,17 @@ export function renderDoctor(result, opts = {}) {
|
|
|
635
653
|
: `${c.green}${text}${c.reset}`;
|
|
636
654
|
lines.push(`${c.bold}melchizedek doctor${c.reset} ${c.dim}· ${result.agentsDir}${c.reset}`);
|
|
637
655
|
lines.push('');
|
|
656
|
+
// The project manifest (ADR 0138): the engine pin, and what in it is stale.
|
|
657
|
+
if (result.project) {
|
|
658
|
+
const p = result.project;
|
|
659
|
+
const pin = 'engine' in p ? `${p.engine.matches ? c.green + '✓' : c.red + '✗'}${c.reset} engine ${p.engine.pinned}${p.engine.matches ? '' : `${c.red}, ${p.engine.running} running${c.reset}`} · ${p.syndicates.length} syndicate(s) ` : '';
|
|
660
|
+
lines.push(`project ${pin}${c.dim}${p.root}${c.reset}`);
|
|
661
|
+
for (const x of p.problems)
|
|
662
|
+
if (!x.startsWith('engine:'))
|
|
663
|
+
lines.push(` ${c.red}✗ ${x}${c.reset}`);
|
|
664
|
+
for (const x of p.warnings)
|
|
665
|
+
lines.push(` ${c.yellow}! ${x}${c.reset}`);
|
|
666
|
+
}
|
|
638
667
|
// The runtime a turn would run on, and where that came from (ADR 0107).
|
|
639
668
|
if (result.runtime) {
|
|
640
669
|
const rt = result.runtime;
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -67,3 +67,4 @@ export { PROVIDERS, providerForModel, providerKeyPresent, providerStatuses, logP
|
|
|
67
67
|
export { loadEnv } from './loadEnv.ts';
|
|
68
68
|
export { setCredentialSource, credentialValue } from './models/credentialSource.ts';
|
|
69
69
|
export type { CredentialSource } from './models/credentialSource.ts';
|
|
70
|
+
export { PROJECT_MANIFEST, projectManifestSchema, ProjectManifestError, findProject, findProjectRoot, loadProject, parseProjectManifest, resolveAgentsDir, listProjectSyndicates, checkProject, type Project, type ProjectManifest, type ProjectCheck, } from './project.ts';
|
package/dist/lib/index.js
CHANGED
|
@@ -55,3 +55,5 @@ describeCapabilities, capabilitySummary, planTransport, gatewayConfig, gatewayPr
|
|
|
55
55
|
export { loadEnv } from './loadEnv.js';
|
|
56
56
|
// Where credentials are read, by environment variable name (ADR 0133).
|
|
57
57
|
export { setCredentialSource, credentialValue } from './models/credentialSource.js';
|
|
58
|
+
// Project manifest, melchizedek.json (ADR 0138).
|
|
59
|
+
export { PROJECT_MANIFEST, projectManifestSchema, ProjectManifestError, findProject, findProjectRoot, loadProject, parseProjectManifest, resolveAgentsDir, listProjectSyndicates, checkProject, } from './project.js';
|
|
@@ -5,6 +5,7 @@ import { createClient } from '@supabase/supabase-js';
|
|
|
5
5
|
import { hasSupabaseCredentials } from './persistence/supabaseProvider.js';
|
|
6
6
|
import { validateSyndicateConfig } from './syndicateSchema.js';
|
|
7
7
|
import { v1SpellingsOf } from './agentDialect.js';
|
|
8
|
+
import { resolveAgentsDir } from './project.js';
|
|
8
9
|
// ── Variable Injection (private) ──────────────────────────
|
|
9
10
|
const TOKEN_RE = /\{\{(\w+)\}\}/g;
|
|
10
11
|
const FULL_TOKEN_RE = /^\{\{(\w+)\}\}$/;
|
|
@@ -290,10 +291,8 @@ export function readSyndicateFile(filename, options = {}) {
|
|
|
290
291
|
// Security: confine reads to the agents root. `filename` can originate from
|
|
291
292
|
// a network-controlled path segment (see a2a_server dynamic routes), so we
|
|
292
293
|
// must reject any value that resolves outside that root (e.g. "../../.env").
|
|
293
|
-
// The root itself is trusted configuration: option → env → cwd default.
|
|
294
|
-
const agentsDir =
|
|
295
|
-
process.env.MELCHIZEDEK_AGENTS_DIR ??
|
|
296
|
-
path.join(process.cwd(), 'config', 'agents'));
|
|
294
|
+
// The root itself is trusted configuration: option → env → project manifest → cwd default (lib/project.ts).
|
|
295
|
+
const agentsDir = resolveAgentsDir(options.agentsDir);
|
|
297
296
|
let filePath = path.resolve(agentsDir, filename);
|
|
298
297
|
if (filePath !== agentsDir && !filePath.startsWith(agentsDir + path.sep)) {
|
|
299
298
|
throw new Error(`Invalid syndicate path: '${filename}' resolves outside the agents directory`);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/project.ts — the project manifest, `melchizedek.json` (ADR 0138).
|
|
3
|
+
*
|
|
4
|
+
* A project is a folder, usually a git repository, with a `melchizedek.json`
|
|
5
|
+
* at its root. The manifest records what the engine cannot infer from the
|
|
6
|
+
* files: which folder holds the syndicates, which engine version the project
|
|
7
|
+
* is written for, and how each file ships. It holds names, never values: an
|
|
8
|
+
* `env` entry is a variable name, and no field may carry a secret.
|
|
9
|
+
*
|
|
10
|
+
* `findProject()` walks up from a directory to the nearest manifest and stops
|
|
11
|
+
* at the repository root (the first directory holding `.git`) or the home
|
|
12
|
+
* directory, so a manifest outside the repository never steers it.
|
|
13
|
+
* `resolveAgentsDir()` is the one agents-directory chain every loader and bin
|
|
14
|
+
* uses: explicit option → MELCHIZEDEK_AGENTS_DIR → the manifest's `agents` →
|
|
15
|
+
* <cwd>/config/agents. A deployed server sets the variable, so the manifest
|
|
16
|
+
* only changes what a bare command run inside a project finds.
|
|
17
|
+
*
|
|
18
|
+
* Imports only node built-ins, zod and lib/paths.ts.
|
|
19
|
+
*/
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
export declare const PROJECT_MANIFEST = "melchizedek.json";
|
|
22
|
+
export declare const MANIFEST_VERSION = 1;
|
|
23
|
+
export declare const projectManifestSchema: z.ZodObject<{
|
|
24
|
+
manifest: z.ZodNumber;
|
|
25
|
+
engine: z.ZodString;
|
|
26
|
+
agents: z.ZodString;
|
|
27
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
|
+
tests: z.ZodOptional<z.ZodString>;
|
|
29
|
+
env: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
30
|
+
syndicates: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
31
|
+
deliverable: z.ZodOptional<z.ZodEnum<{
|
|
32
|
+
library: "library";
|
|
33
|
+
mcp: "mcp";
|
|
34
|
+
service: "service";
|
|
35
|
+
}>>;
|
|
36
|
+
publish: z.ZodOptional<z.ZodObject<{
|
|
37
|
+
kind: z.ZodLiteral<"registry">;
|
|
38
|
+
id: z.ZodString;
|
|
39
|
+
}, z.core.$loose>>;
|
|
40
|
+
}, z.core.$loose>>>;
|
|
41
|
+
registry: z.ZodOptional<z.ZodObject<{
|
|
42
|
+
kind: z.ZodLiteral<"supabase">;
|
|
43
|
+
url_env: z.ZodOptional<z.ZodString>;
|
|
44
|
+
restart: z.ZodOptional<z.ZodString>;
|
|
45
|
+
}, z.core.$loose>>;
|
|
46
|
+
production: z.ZodOptional<z.ZodObject<{
|
|
47
|
+
ledger: z.ZodOptional<z.ZodObject<{
|
|
48
|
+
kind: z.ZodLiteral<"postgres">;
|
|
49
|
+
url_env: z.ZodString;
|
|
50
|
+
}, z.core.$loose>>;
|
|
51
|
+
}, z.core.$loose>>;
|
|
52
|
+
tools: z.ZodOptional<z.ZodObject<{
|
|
53
|
+
remote: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
54
|
+
mcp_env: z.ZodString;
|
|
55
|
+
}, z.core.$loose>>>;
|
|
56
|
+
}, z.core.$loose>>;
|
|
57
|
+
}, z.core.$loose>;
|
|
58
|
+
export type ProjectManifest = z.infer<typeof projectManifestSchema>;
|
|
59
|
+
export interface Project {
|
|
60
|
+
/** The folder holding `melchizedek.json`. */
|
|
61
|
+
root: string;
|
|
62
|
+
/** Absolute path of the manifest. */
|
|
63
|
+
file: string;
|
|
64
|
+
manifest: ProjectManifest;
|
|
65
|
+
/** `agents`, resolved under the root. */
|
|
66
|
+
agentsDir: string;
|
|
67
|
+
/** `tests`, resolved under the root (default `tests/agents`). */
|
|
68
|
+
testsDir: string;
|
|
69
|
+
}
|
|
70
|
+
/** Thrown for a manifest that does not parse or validate; names the file and fields, never a value. */
|
|
71
|
+
export declare class ProjectManifestError extends Error {
|
|
72
|
+
readonly file: string;
|
|
73
|
+
readonly issues: string[];
|
|
74
|
+
constructor(file: string, issues: string[]);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The nearest folder at or above `from` holding a manifest, without crossing
|
|
78
|
+
* the repository root or the home directory. Undefined when there is none.
|
|
79
|
+
*/
|
|
80
|
+
export declare function findProjectRoot(from?: string): string | undefined;
|
|
81
|
+
/** Parse and validate a manifest's text. Throws `ProjectManifestError`. */
|
|
82
|
+
export declare function parseProjectManifest(text: string, file?: string): ProjectManifest;
|
|
83
|
+
/** Load the project rooted at `root` (the folder holding the manifest). Throws `ProjectManifestError`. */
|
|
84
|
+
export declare function loadProject(root: string): Project;
|
|
85
|
+
/** The project at or above `from`, or undefined when there is none. Throws on a broken manifest. */
|
|
86
|
+
export declare function findProject(from?: string): Project | undefined;
|
|
87
|
+
/** Forget cached manifest lookups (tests, and an embedder that edits the manifest in-process). */
|
|
88
|
+
export declare function clearProjectCache(): void;
|
|
89
|
+
/**
|
|
90
|
+
* The agents directory: `explicit` → MELCHIZEDEK_AGENTS_DIR → the nearest
|
|
91
|
+
* manifest's `agents` → <cwd>/config/agents. Always absolute.
|
|
92
|
+
*/
|
|
93
|
+
export declare function resolveAgentsDir(explicit?: string, env?: NodeJS.ProcessEnv): string;
|
|
94
|
+
/** True when `relFromRoot` (a `/`-separated path relative to the project root) matches an `ignore` pattern. */
|
|
95
|
+
export declare function isIgnored(project: Pick<Project, 'manifest'>, relFromRoot: string): boolean;
|
|
96
|
+
/** The syndicate files directly in the agents folder, as names relative to it, minus `ignore`. */
|
|
97
|
+
export declare function listProjectSyndicates(project: Project): string[];
|
|
98
|
+
/** The installed engine's package version (no commit suffix). */
|
|
99
|
+
export declare function enginePackageVersion(): string;
|
|
100
|
+
export interface ProjectCheck {
|
|
101
|
+
root: string;
|
|
102
|
+
agentsDir: string;
|
|
103
|
+
/** The manifest's engine pin, and the engine running now. */
|
|
104
|
+
engine: {
|
|
105
|
+
pinned: string;
|
|
106
|
+
running: string;
|
|
107
|
+
matches: boolean;
|
|
108
|
+
};
|
|
109
|
+
syndicates: string[];
|
|
110
|
+
/** Stop `melchizedek-doctor --check`. */
|
|
111
|
+
problems: string[];
|
|
112
|
+
/** Shown, never fatal. */
|
|
113
|
+
warnings: string[];
|
|
114
|
+
}
|
|
115
|
+
/** What is wrong with a project: engine pin, missing folders, stale ignores, unset env names. Never reads a value. */
|
|
116
|
+
export declare function checkProject(project: Project, running?: string, env?: NodeJS.ProcessEnv): ProjectCheck;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/project.ts — the project manifest, `melchizedek.json` (ADR 0138).
|
|
3
|
+
*
|
|
4
|
+
* A project is a folder, usually a git repository, with a `melchizedek.json`
|
|
5
|
+
* at its root. The manifest records what the engine cannot infer from the
|
|
6
|
+
* files: which folder holds the syndicates, which engine version the project
|
|
7
|
+
* is written for, and how each file ships. It holds names, never values: an
|
|
8
|
+
* `env` entry is a variable name, and no field may carry a secret.
|
|
9
|
+
*
|
|
10
|
+
* `findProject()` walks up from a directory to the nearest manifest and stops
|
|
11
|
+
* at the repository root (the first directory holding `.git`) or the home
|
|
12
|
+
* directory, so a manifest outside the repository never steers it.
|
|
13
|
+
* `resolveAgentsDir()` is the one agents-directory chain every loader and bin
|
|
14
|
+
* uses: explicit option → MELCHIZEDEK_AGENTS_DIR → the manifest's `agents` →
|
|
15
|
+
* <cwd>/config/agents. A deployed server sets the variable, so the manifest
|
|
16
|
+
* only changes what a bare command run inside a project finds.
|
|
17
|
+
*
|
|
18
|
+
* Imports only node built-ins, zod and lib/paths.ts.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { enginePackageRoot } from './paths.js';
|
|
25
|
+
export const PROJECT_MANIFEST = 'melchizedek.json';
|
|
26
|
+
export const MANIFEST_VERSION = 1;
|
|
27
|
+
/** A path relative to the project root: no absolute paths, no `..` segments. */
|
|
28
|
+
const relPath = z
|
|
29
|
+
.string()
|
|
30
|
+
.min(1)
|
|
31
|
+
.refine((p) => !/^([a-zA-Z]:)?[\\/]/.test(p) && !p.split(/[\\/]/).includes('..'), 'must be relative to the project root and stay inside it');
|
|
32
|
+
const envName = z.string().regex(/^[A-Z][A-Z0-9_]{0,63}$/, 'must be an environment variable name');
|
|
33
|
+
const syndicateRecord = z
|
|
34
|
+
.object({
|
|
35
|
+
deliverable: z.enum(['service', 'library', 'mcp']).optional(),
|
|
36
|
+
publish: z.object({ kind: z.literal('registry'), id: z.string().min(1) }).passthrough().optional(),
|
|
37
|
+
})
|
|
38
|
+
.passthrough();
|
|
39
|
+
export const projectManifestSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
manifest: z.number().int().min(1),
|
|
42
|
+
engine: z.string().regex(/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, 'must be an exact version, e.g. 1.8.0'),
|
|
43
|
+
agents: relPath,
|
|
44
|
+
ignore: z.array(z.string().min(1)).optional(),
|
|
45
|
+
tests: relPath.optional(),
|
|
46
|
+
env: z.array(envName).optional(),
|
|
47
|
+
syndicates: z.record(z.string(), syndicateRecord).optional(),
|
|
48
|
+
registry: z
|
|
49
|
+
.object({ kind: z.literal('supabase'), url_env: envName.optional(), restart: z.string().optional() })
|
|
50
|
+
.passthrough()
|
|
51
|
+
.optional(),
|
|
52
|
+
production: z
|
|
53
|
+
.object({ ledger: z.object({ kind: z.literal('postgres'), url_env: envName }).passthrough().optional() })
|
|
54
|
+
.passthrough()
|
|
55
|
+
.optional(),
|
|
56
|
+
tools: z
|
|
57
|
+
.object({ remote: z.record(z.string(), z.object({ mcp_env: envName }).passthrough()).optional() })
|
|
58
|
+
.passthrough()
|
|
59
|
+
.optional(),
|
|
60
|
+
})
|
|
61
|
+
// Unknown keys are kept: a newer writer's fields survive an older reader.
|
|
62
|
+
.passthrough();
|
|
63
|
+
/** Thrown for a manifest that does not parse or validate; names the file and fields, never a value. */
|
|
64
|
+
export class ProjectManifestError extends Error {
|
|
65
|
+
file;
|
|
66
|
+
issues;
|
|
67
|
+
constructor(file, issues) {
|
|
68
|
+
super(`${file}: ${issues.join('; ')}`);
|
|
69
|
+
this.file = file;
|
|
70
|
+
this.issues = issues;
|
|
71
|
+
this.name = 'ProjectManifestError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The nearest folder at or above `from` holding a manifest, without crossing
|
|
76
|
+
* the repository root or the home directory. Undefined when there is none.
|
|
77
|
+
*/
|
|
78
|
+
export function findProjectRoot(from = process.cwd()) {
|
|
79
|
+
const home = resolve(homedir());
|
|
80
|
+
let dir = resolve(from);
|
|
81
|
+
for (;;) {
|
|
82
|
+
if (existsSync(join(dir, PROJECT_MANIFEST)))
|
|
83
|
+
return dir;
|
|
84
|
+
if (existsSync(join(dir, '.git')) || dir === home)
|
|
85
|
+
return undefined;
|
|
86
|
+
const parent = dirname(dir);
|
|
87
|
+
if (parent === dir)
|
|
88
|
+
return undefined;
|
|
89
|
+
dir = parent;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Parse and validate a manifest's text. Throws `ProjectManifestError`. */
|
|
93
|
+
export function parseProjectManifest(text, file = PROJECT_MANIFEST) {
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = JSON.parse(text);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new ProjectManifestError(file, ['not valid JSON']);
|
|
100
|
+
}
|
|
101
|
+
const parsed = projectManifestSchema.safeParse(raw);
|
|
102
|
+
if (!parsed.success) {
|
|
103
|
+
throw new ProjectManifestError(file, parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`));
|
|
104
|
+
}
|
|
105
|
+
if (parsed.data.manifest > MANIFEST_VERSION) {
|
|
106
|
+
throw new ProjectManifestError(file, [`manifest: version ${parsed.data.manifest} is newer than this engine reads (${MANIFEST_VERSION}); upgrade melchizedek-agents`]);
|
|
107
|
+
}
|
|
108
|
+
return parsed.data;
|
|
109
|
+
}
|
|
110
|
+
/** A path under the root, or an error when it would leave it. */
|
|
111
|
+
function under(root, rel, field, file) {
|
|
112
|
+
const abs = resolve(root, rel);
|
|
113
|
+
if (abs !== root && !abs.startsWith(root + sep))
|
|
114
|
+
throw new ProjectManifestError(file, [`${field}: resolves outside the project root`]);
|
|
115
|
+
return abs;
|
|
116
|
+
}
|
|
117
|
+
/** Load the project rooted at `root` (the folder holding the manifest). Throws `ProjectManifestError`. */
|
|
118
|
+
export function loadProject(root) {
|
|
119
|
+
const r = resolve(root);
|
|
120
|
+
const file = join(r, PROJECT_MANIFEST);
|
|
121
|
+
const manifest = parseProjectManifest(readFileSync(file, 'utf-8'), file);
|
|
122
|
+
return {
|
|
123
|
+
root: r,
|
|
124
|
+
file,
|
|
125
|
+
manifest,
|
|
126
|
+
agentsDir: under(r, manifest.agents, 'agents', file),
|
|
127
|
+
testsDir: under(r, manifest.tests ?? join('tests', 'agents'), 'tests', file),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** The project at or above `from`, or undefined when there is none. Throws on a broken manifest. */
|
|
131
|
+
export function findProject(from = process.cwd()) {
|
|
132
|
+
const root = findProjectRoot(from);
|
|
133
|
+
return root ? loadProject(root) : undefined;
|
|
134
|
+
}
|
|
135
|
+
// The agents-directory chain runs on every syndicate read (an A2A route
|
|
136
|
+
// reads per request), so the manifest lookup is cached per working
|
|
137
|
+
// directory. A broken manifest is cached as "none" and reported by
|
|
138
|
+
// `melchizedek-doctor`, never by a request path.
|
|
139
|
+
const agentsDirCache = new Map();
|
|
140
|
+
/** Forget cached manifest lookups (tests, and an embedder that edits the manifest in-process). */
|
|
141
|
+
export function clearProjectCache() {
|
|
142
|
+
agentsDirCache.clear();
|
|
143
|
+
}
|
|
144
|
+
function manifestAgentsDir(cwd) {
|
|
145
|
+
if (!agentsDirCache.has(cwd)) {
|
|
146
|
+
let dir = null;
|
|
147
|
+
try {
|
|
148
|
+
dir = findProject(cwd)?.agentsDir ?? null;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
dir = null;
|
|
152
|
+
}
|
|
153
|
+
agentsDirCache.set(cwd, dir);
|
|
154
|
+
}
|
|
155
|
+
return agentsDirCache.get(cwd) ?? undefined;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The agents directory: `explicit` → MELCHIZEDEK_AGENTS_DIR → the nearest
|
|
159
|
+
* manifest's `agents` → <cwd>/config/agents. Always absolute.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveAgentsDir(explicit, env = process.env) {
|
|
162
|
+
const fromEnv = env.MELCHIZEDEK_AGENTS_DIR?.trim();
|
|
163
|
+
const cwd = process.cwd();
|
|
164
|
+
return resolve(explicit ?? (fromEnv || undefined) ?? manifestAgentsDir(cwd) ?? join(cwd, 'config', 'agents'));
|
|
165
|
+
}
|
|
166
|
+
/** Glob to RegExp for `*` (one segment), `**` (any depth) and `?`, on `/`-separated paths. */
|
|
167
|
+
function globToRegExp(glob) {
|
|
168
|
+
let re = '';
|
|
169
|
+
for (let i = 0; i < glob.length; i++) {
|
|
170
|
+
const c = glob[i];
|
|
171
|
+
if (c === '*') {
|
|
172
|
+
if (glob[i + 1] === '*') {
|
|
173
|
+
i++;
|
|
174
|
+
if (glob[i + 1] === '/') {
|
|
175
|
+
i++;
|
|
176
|
+
re += '(?:.*/)?';
|
|
177
|
+
}
|
|
178
|
+
else
|
|
179
|
+
re += '.*';
|
|
180
|
+
}
|
|
181
|
+
else
|
|
182
|
+
re += '[^/]*';
|
|
183
|
+
}
|
|
184
|
+
else if (c === '?')
|
|
185
|
+
re += '[^/]';
|
|
186
|
+
else
|
|
187
|
+
re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
188
|
+
}
|
|
189
|
+
return new RegExp(`^${re}$`);
|
|
190
|
+
}
|
|
191
|
+
/** True when `relFromRoot` (a `/`-separated path relative to the project root) matches an `ignore` pattern. */
|
|
192
|
+
export function isIgnored(project, relFromRoot) {
|
|
193
|
+
const p = relFromRoot.split(sep).join('/');
|
|
194
|
+
return (project.manifest.ignore ?? []).some((g) => globToRegExp(g).test(p));
|
|
195
|
+
}
|
|
196
|
+
/** The syndicate files directly in the agents folder, as names relative to it, minus `ignore`. */
|
|
197
|
+
export function listProjectSyndicates(project) {
|
|
198
|
+
if (!existsSync(project.agentsDir))
|
|
199
|
+
return [];
|
|
200
|
+
const rel = relative(project.root, project.agentsDir);
|
|
201
|
+
return readdirSync(project.agentsDir)
|
|
202
|
+
.filter((f) => (f.endsWith('.yaml') || f.endsWith('.yml')) && f !== 'syndicateSchema.yaml')
|
|
203
|
+
.filter((f) => statSync(join(project.agentsDir, f)).isFile())
|
|
204
|
+
.filter((f) => !isIgnored(project, rel ? `${rel}/${f}` : f))
|
|
205
|
+
.sort();
|
|
206
|
+
}
|
|
207
|
+
/** The installed engine's package version (no commit suffix). */
|
|
208
|
+
export function enginePackageVersion() {
|
|
209
|
+
try {
|
|
210
|
+
const root = enginePackageRoot();
|
|
211
|
+
if (root)
|
|
212
|
+
return String(JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')).version);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/* fall through */
|
|
216
|
+
}
|
|
217
|
+
return '0.0.0';
|
|
218
|
+
}
|
|
219
|
+
/** What is wrong with a project: engine pin, missing folders, stale ignores, unset env names. Never reads a value. */
|
|
220
|
+
export function checkProject(project, running = enginePackageVersion(), env = process.env) {
|
|
221
|
+
const problems = [];
|
|
222
|
+
const warnings = [];
|
|
223
|
+
const pinned = project.manifest.engine;
|
|
224
|
+
const matches = pinned === running;
|
|
225
|
+
if (!matches)
|
|
226
|
+
problems.push(`engine: the project pins melchizedek-agents ${pinned}; ${running} is running`);
|
|
227
|
+
if (!existsSync(project.agentsDir))
|
|
228
|
+
problems.push(`agents: ${project.manifest.agents} does not exist`);
|
|
229
|
+
if (project.manifest.tests && !existsSync(project.testsDir))
|
|
230
|
+
problems.push(`tests: ${project.manifest.tests} does not exist`);
|
|
231
|
+
for (const g of project.manifest.ignore ?? []) {
|
|
232
|
+
if (!anyPathMatches(project.root, globToRegExp(g)))
|
|
233
|
+
warnings.push(`ignore: "${g}" matches nothing`);
|
|
234
|
+
}
|
|
235
|
+
for (const name of project.manifest.env ?? [])
|
|
236
|
+
if (!env[name]?.trim())
|
|
237
|
+
warnings.push(`env: ${name} is not set`);
|
|
238
|
+
const syndicates = listProjectSyndicates(project);
|
|
239
|
+
for (const f of Object.keys(project.manifest.syndicates ?? {})) {
|
|
240
|
+
if (!syndicates.includes(f))
|
|
241
|
+
warnings.push(`syndicates: ${f} is not in ${project.manifest.agents}`);
|
|
242
|
+
}
|
|
243
|
+
return { root: project.root, agentsDir: project.agentsDir, engine: { pinned, running, matches }, syndicates, problems, warnings };
|
|
244
|
+
}
|
|
245
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', 'venv', '.venv', '__pycache__', 'dist']);
|
|
246
|
+
/** Whether any path under root (depth-limited, skipping vendor folders) matches. */
|
|
247
|
+
function anyPathMatches(root, re, rel = '', depth = 0) {
|
|
248
|
+
if (depth > 6)
|
|
249
|
+
return false;
|
|
250
|
+
let entries;
|
|
251
|
+
try {
|
|
252
|
+
entries = readdirSync(join(root, rel));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
for (const e of entries) {
|
|
258
|
+
if (SKIP_DIRS.has(e))
|
|
259
|
+
continue;
|
|
260
|
+
const p = rel ? `${rel}/${e}` : e;
|
|
261
|
+
if (re.test(p))
|
|
262
|
+
return true;
|
|
263
|
+
let dir = false;
|
|
264
|
+
try {
|
|
265
|
+
dir = statSync(join(root, p)).isDirectory();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
/* unreadable entry */
|
|
269
|
+
}
|
|
270
|
+
if (dir && anyPathMatches(root, re, p, depth + 1))
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
@@ -97,7 +97,6 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
97
97
|
*/
|
|
98
98
|
import { randomBytes } from 'node:crypto';
|
|
99
99
|
import { readdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
100
|
-
import path from 'node:path';
|
|
101
100
|
import { pathToFileURL } from 'node:url';
|
|
102
101
|
import { createA2AApp } from '../lib/a2a/app.js';
|
|
103
102
|
import { callerTokens, firstOf, hashCallerToken, jwtIdentity, parseCallers, sharedSecret, trustedHeader, } from '../lib/a2a/identity.js';
|
|
@@ -116,6 +115,7 @@ import { callbackIdentity, callbackIdentityProblem, oauthServerSetup } from '../
|
|
|
116
115
|
import { postgresCredentialRows } from '../lib/storage/postgres/credentialStore.js';
|
|
117
116
|
import { setOAuthHosts } from '../lib/tools/oauthHosts.js';
|
|
118
117
|
import { setCredentialHosts } from '../lib/tools/credentialHosts.js';
|
|
118
|
+
import { resolveAgentsDir } from '../lib/project.js';
|
|
119
119
|
// Re-exported for existing importers (tests/failNarration.test.ts).
|
|
120
120
|
export { describeTurnError } from '../lib/a2a/executor.js';
|
|
121
121
|
/** Minimum bearer-secret length below which the server warns. */
|
|
@@ -181,7 +181,7 @@ export function servedFileSyndicates(syndicateName, servedAgents, registryAgents
|
|
|
181
181
|
add(id);
|
|
182
182
|
}
|
|
183
183
|
else {
|
|
184
|
-
const agentsDir =
|
|
184
|
+
const agentsDir = resolveAgentsDir();
|
|
185
185
|
let names = [];
|
|
186
186
|
try {
|
|
187
187
|
names = readdirSync(agentsDir);
|
|
@@ -633,7 +633,7 @@ if (invokedAsMain) {
|
|
|
633
633
|
if (error?.code === 'ENOENT') {
|
|
634
634
|
console.error(`[A2A] Syndicate not found: ${arg || 'syndicate.yaml'}.\n`
|
|
635
635
|
+ ' Pass your syndicate: melchizedek-serve <name>.yaml\n'
|
|
636
|
-
+ ' (agents directory: MELCHIZEDEK_AGENTS_DIR,
|
|
636
|
+
+ ' (agents directory: MELCHIZEDEK_AGENTS_DIR, else the agents folder in melchizedek.json, else <cwd>/config/agents)');
|
|
637
637
|
process.exit(1);
|
|
638
638
|
}
|
|
639
639
|
console.error(error);
|
package/dist/scripts/doctor.js
CHANGED
|
@@ -33,6 +33,7 @@ import { loadEnv } from '../lib/loadEnv.js';
|
|
|
33
33
|
import { checkProblems, renderDoctor, runDoctor } from '../lib/doctor.js';
|
|
34
34
|
import { renderCapabilityMatrix } from '../lib/models/capabilities.js';
|
|
35
35
|
import { assignMemoryNamespace, LEGACY_MEMORY_APP_NAME } from '../lib/memory/namespace.js';
|
|
36
|
+
import { resolveAgentsDir } from '../lib/project.js';
|
|
36
37
|
loadEnv();
|
|
37
38
|
const args = process.argv.slice(2);
|
|
38
39
|
const json = args.includes('--json');
|
|
@@ -52,7 +53,7 @@ function packageScripts() {
|
|
|
52
53
|
if (fixAt !== -1) {
|
|
53
54
|
// ADR 0020: give each named long-term syndicate its own memory namespace.
|
|
54
55
|
// Only the files named are touched; with none named, list the candidates.
|
|
55
|
-
const agentsDir =
|
|
56
|
+
const agentsDir = resolveAgentsDir();
|
|
56
57
|
const files = args.slice(fixAt + 1).filter((a) => !a.startsWith('--'));
|
|
57
58
|
if (files.length === 0) {
|
|
58
59
|
const pending = runDoctor({ agentsDir }).syndicates.filter((s) => s.memory && !s.memory.declared && !s.file.includes('/'));
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import { readdirSync, realpathSync } from 'node:fs';
|
|
39
39
|
import path from 'node:path';
|
|
40
40
|
import { pathToFileURL } from 'node:url';
|
|
41
|
+
import { resolveAgentsDir } from '../lib/project.js';
|
|
41
42
|
/** The bin's flags. Throws, naming the flag, on one it does not know. */
|
|
42
43
|
export function parseMcpArgs(argv) {
|
|
43
44
|
const out = { syndicates: [], http: false, help: false };
|
|
@@ -160,7 +161,7 @@ export async function startMcpServer(argv = process.argv.slice(2)) {
|
|
|
160
161
|
const bindings = () => ({
|
|
161
162
|
current_date: new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }),
|
|
162
163
|
});
|
|
163
|
-
const agentsDir =
|
|
164
|
+
const agentsDir = resolveAgentsDir();
|
|
164
165
|
let files = args.syndicates.map((id) => (id.endsWith('.yaml') || id.endsWith('.yml') ? id : `${id}.yaml`));
|
|
165
166
|
const named = files.length > 0;
|
|
166
167
|
if (!named) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "melchizedek-agents",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "A multi-model, multi-agent orchestration framework — YAML-defined syndicates on the engine's own agent loop, Supabase or Postgres session persistence, pgvector long-term memory. Companion repo to the lyceumagents.com curriculum.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"./a2a/limits": "./dist/lib/a2a/redisLimits.js",
|
|
34
34
|
"./mcp": "./dist/lib/mcp/server.js",
|
|
35
35
|
"./registry": "./dist/lib/registry.js",
|
|
36
|
+
"./project": "./dist/lib/project.js",
|
|
36
37
|
"./server": "./dist/scripts/a2a_server.js",
|
|
37
38
|
"./guards": "./dist/lib/guards/index.js",
|
|
38
39
|
"./storage/postgres": "./dist/lib/storage/postgres/index.js",
|