yarramate 1.25.1 → 1.26.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/README.md +6 -2
- package/dist/adapters/mcp-cli.d.ts +45 -1
- package/dist/adapters/mcp-cli.js +297 -50
- package/dist/adapters/visual/protocol-contract.d.ts +29 -2
- package/dist/adapters/visual/session-server.js +27 -6
- package/dist/adapters/visual/session-store.js +1 -0
- package/dist/adapters/visual/view-identity.d.ts +13 -0
- package/dist/adapters/visual/view-identity.js +29 -11
- package/dist/adapters/visual/wire.d.ts +18 -0
- package/dist/adapters/visual/workspace-model.js +11 -1
- package/dist/visual-app/assets/{elk.bundled-CLux5E_P.js → elk.bundled-CYZFv-C6.js} +1 -1
- package/dist/visual-app/assets/{index-DGkU2CSB.js → index-CRYHv5jy.js} +6 -2
- package/dist/visual-app/assets/index-DdE5s692.css +1 -0
- package/dist/visual-app/index.html +2 -2
- package/dist/visual-app-lib/editor.js +7827 -7497
- package/dist/visual-app-lib/styles.css +1 -1
- package/dist/visual-app-lib/types/adapters/visual/protocol-contract.d.ts +29 -2
- package/dist/visual-app-lib/types/adapters/visual/view-identity.d.ts +13 -0
- package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +18 -0
- package/dist/visual-app-lib/types/visual-app/App.d.ts +7 -1
- package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +36 -0
- package/dist/visual-app-lib/types/visual-app/mount.d.ts +9 -1
- package/dist/visual-app-lib/types/visual-app/open-questions.d.ts +18 -2
- package/dist/visual-app-lib/types/visual-app/question-verbs.d.ts +54 -0
- package/dist/visual-app-lib/types/visual-app/session-client.d.ts +3 -0
- package/dist/visual-app-lib/types/visual-app/state.d.ts +4 -1
- package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +13 -0
- package/docs/CONSUMING-YARRAMATE.md +17 -5
- package/package.json +1 -1
- package/schema/yarramate-visual-event.schema.json +324 -73
- package/schema/yarramate-visual-response.schema.json +230 -56
- package/skills/yarramate-architecture/SKILL.md +6 -2
- package/skills/yarramate-architecture/references/visual-conversations.md +21 -3
- package/dist/visual-app/assets/index-CJVWkpLt.css +0 -1
package/README.md
CHANGED
|
@@ -163,8 +163,12 @@ diff — merging is the human acceptance step, not yours.
|
|
|
163
163
|
verdicts, and attestations, with a `path:line` citation per cell.
|
|
164
164
|
- `init` writes the discovery pointer into both `AGENTS.md` and
|
|
165
165
|
`CLAUDE.md`, so this section finds you rather than the reverse.
|
|
166
|
-
- `yarramate-mcp` exposes
|
|
167
|
-
|
|
166
|
+
- `yarramate-mcp` exposes the whole loop over MCP stdio: `yarramate_ask`,
|
|
167
|
+
`yarramate_design`, `yarramate_apply`, `yarramate_check`,
|
|
168
|
+
`yarramate_reconcile` and `yarramate_export`. Every tool call runs the same
|
|
169
|
+
CLI; `apply` is the one write and lands the same atomic batch. Start it
|
|
170
|
+
inside the repository, or anywhere with `--workspace <path>`, and the
|
|
171
|
+
`workspace` argument becomes optional (#514).
|
|
168
172
|
- In Claude Code, this repository is its own plugin marketplace:
|
|
169
173
|
|
|
170
174
|
```sh
|
|
@@ -5,5 +5,49 @@ interface JsonRpcRequest {
|
|
|
5
5
|
readonly method: string;
|
|
6
6
|
readonly params?: Record<string, unknown>;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Where a tool call resolves its workspace when the call names none
|
|
10
|
+
* (#514). In order: the `--workspace` the server was started with, then the
|
|
11
|
+
* conventional `.yarramate/workspace.yaml` under the working directory. A
|
|
12
|
+
* desktop app starts the server from a directory that is not the repository,
|
|
13
|
+
* so the first is the one that matters there; a terminal agent runs it from
|
|
14
|
+
* the repository, so the second is what it gets for free.
|
|
15
|
+
*/
|
|
16
|
+
export interface ServerOptions {
|
|
17
|
+
readonly workspace?: string;
|
|
18
|
+
}
|
|
19
|
+
/** What a tool call needs from the server beyond its own arguments. */
|
|
20
|
+
export interface ToolContext {
|
|
21
|
+
readonly cwd: string;
|
|
22
|
+
readonly workspace: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The loop, in two sentences, on every tool. A desktop-app agent connecting
|
|
26
|
+
* for the first time has never seen the skill file; the tool list is the only
|
|
27
|
+
* place it learns that design asks, apply lands, and design asks again.
|
|
28
|
+
*/
|
|
29
|
+
export declare const LOOP = "The loop: call yarramate_design for the top open question, answer it with the person, land the answer with yarramate_apply, then call yarramate_design again. Every tool takes the same optional `workspace`; omit it to use the workspace this server was started with, or .yarramate/workspace.yaml under its working directory.";
|
|
30
|
+
export declare const resolveWorkspace: (input: Record<string, unknown>, context: ToolContext) => string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* The CLI runs inside the repository: a manifest's globs resolve against the
|
|
33
|
+
* manifest, but a record's contracts, coverage and evidence name files by
|
|
34
|
+
* their repository-root path, which is the directory that holds `.yarramate`.
|
|
35
|
+
* A desktop app starts this server anywhere, so a workspace given as a path
|
|
36
|
+
* from elsewhere is run as the CLI would be run by a person standing in that
|
|
37
|
+
* repository: working directory at the root, manifest path relative to it.
|
|
38
|
+
*/
|
|
39
|
+
export declare const workingDirectoryFor: (workspace: string, cwd: string) => {
|
|
40
|
+
readonly cwd: string;
|
|
41
|
+
readonly workspace: string;
|
|
42
|
+
};
|
|
43
|
+
/** The tool list a client sees; exported so a test can read it without stdio. */
|
|
44
|
+
export declare const toolCatalogue: {
|
|
45
|
+
name: string;
|
|
46
|
+
description: string;
|
|
47
|
+
inputSchema: Record<string, unknown>;
|
|
48
|
+
}[];
|
|
49
|
+
export declare const handleRequest: (request: JsonRpcRequest, context?: ToolContext) => void;
|
|
50
|
+
/** `yarramate-mcp [--workspace <path>]`; anything else is refused with usage. */
|
|
51
|
+
export declare const parseServerOptions: (argv: readonly string[]) => ServerOptions | undefined;
|
|
52
|
+
export declare const serverUsage = "Usage:\n yarramate-mcp [--workspace <workspace.yaml>]\n yarramate-mcp --version\n";
|
|
9
53
|
export {};
|
package/dist/adapters/mcp-cli.js
CHANGED
|
@@ -1,20 +1,95 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
2
5
|
import { createInterface } from 'node:readline';
|
|
3
6
|
import { isMainModule, packageVersion, versionResult, } from '../cli-support.js';
|
|
4
7
|
import { runCli } from '../cli.js';
|
|
8
|
+
/**
|
|
9
|
+
* The loop, in two sentences, on every tool. A desktop-app agent connecting
|
|
10
|
+
* for the first time has never seen the skill file; the tool list is the only
|
|
11
|
+
* place it learns that design asks, apply lands, and design asks again.
|
|
12
|
+
*/
|
|
13
|
+
export const LOOP = 'The loop: call yarramate_design for the top open question, answer it with the person, land the answer with yarramate_apply, then call yarramate_design again. Every tool takes the same optional `workspace`; omit it to use the workspace this server was started with, or .yarramate/workspace.yaml under its working directory.';
|
|
5
14
|
const workspaceProperty = {
|
|
6
15
|
workspace: {
|
|
7
16
|
type: 'string',
|
|
8
|
-
description:
|
|
17
|
+
description: "Path to the workspace manifest, for example .yarramate/workspace.yaml. Optional: defaults to the server's --workspace, then to .yarramate/workspace.yaml under the working directory. Desktop apps start the server outside the repository, so give the full path there.",
|
|
9
18
|
},
|
|
10
19
|
};
|
|
20
|
+
const CONVENTIONAL_WORKSPACE = join('.yarramate', 'workspace.yaml');
|
|
21
|
+
/** The one refusal that is the server's own rather than the CLI's. */
|
|
22
|
+
const noWorkspace = () => ({
|
|
23
|
+
exitCode: 2,
|
|
24
|
+
stdout: '',
|
|
25
|
+
stderr: 'No workspace to work on. Pass `workspace` (the path to workspace.yaml) in the call, or start yarramate-mcp with --workspace <path>; a server started inside a repository that holds .yarramate/workspace.yaml needs neither.\n',
|
|
26
|
+
});
|
|
27
|
+
export const resolveWorkspace = (input, context) => {
|
|
28
|
+
if (typeof input.workspace === 'string' && input.workspace.length > 0) {
|
|
29
|
+
return input.workspace;
|
|
30
|
+
}
|
|
31
|
+
if (context.workspace !== undefined)
|
|
32
|
+
return context.workspace;
|
|
33
|
+
return existsSync(resolve(context.cwd, CONVENTIONAL_WORKSPACE))
|
|
34
|
+
? CONVENTIONAL_WORKSPACE
|
|
35
|
+
: undefined;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The CLI runs inside the repository: a manifest's globs resolve against the
|
|
39
|
+
* manifest, but a record's contracts, coverage and evidence name files by
|
|
40
|
+
* their repository-root path, which is the directory that holds `.yarramate`.
|
|
41
|
+
* A desktop app starts this server anywhere, so a workspace given as a path
|
|
42
|
+
* from elsewhere is run as the CLI would be run by a person standing in that
|
|
43
|
+
* repository: working directory at the root, manifest path relative to it.
|
|
44
|
+
*/
|
|
45
|
+
export const workingDirectoryFor = (workspace, cwd) => {
|
|
46
|
+
const manifest = resolve(cwd, workspace);
|
|
47
|
+
const holder = dirname(manifest);
|
|
48
|
+
const root = basename(holder) === '.yarramate' ? dirname(holder) : holder;
|
|
49
|
+
return { cwd: root, workspace: relative(root, manifest) };
|
|
50
|
+
};
|
|
51
|
+
const withWorkspace = (input, context, run) => {
|
|
52
|
+
const named = resolveWorkspace(input, context);
|
|
53
|
+
if (named === undefined)
|
|
54
|
+
return noWorkspace();
|
|
55
|
+
const { cwd, workspace } = workingDirectoryFor(named, context.cwd);
|
|
56
|
+
return run(workspace, cwd);
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* A scratch directory for one call, removed before the answer goes back.
|
|
60
|
+
* `apply` reads its operations from a path and `export` writes some kinds only
|
|
61
|
+
* to a directory; an MCP call carries neither, so the adapter lends both a
|
|
62
|
+
* place on disk for the duration of the call and nothing longer.
|
|
63
|
+
*/
|
|
64
|
+
const withScratch = (use) => {
|
|
65
|
+
const directory = mkdtempSync(join(tmpdir(), 'yarramate-mcp-'));
|
|
66
|
+
try {
|
|
67
|
+
return use(directory);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
rmSync(directory, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const operationsText = (operations) => {
|
|
74
|
+
if (typeof operations === 'string')
|
|
75
|
+
return operations;
|
|
76
|
+
if (typeof operations === 'object' && operations !== null) {
|
|
77
|
+
// JSON is YAML; the CLI's parser reads it unchanged.
|
|
78
|
+
return JSON.stringify(operations, null, 2);
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
};
|
|
82
|
+
const refuse = (message) => ({
|
|
83
|
+
exitCode: 2,
|
|
84
|
+
stdout: '',
|
|
85
|
+
stderr: `${message}\n`,
|
|
86
|
+
});
|
|
11
87
|
const tools = [
|
|
12
88
|
{
|
|
13
89
|
name: 'yarramate_ask',
|
|
14
|
-
description:
|
|
90
|
+
description: `The consumed-now read surface. Without a query: orientation — check verdict, drift summary, open-question count, and the backlog in dependency order. With a query: free text matches concept ids, names, and descriptions and returns the connected slice; exact subject ids (document-id#local-id) and projection paths address precisely. Set mode for the roster (subjects), declarable vocabulary (kinds), build order (next), or the full open-questions report (open). ${LOOP}`,
|
|
15
91
|
inputSchema: {
|
|
16
92
|
type: 'object',
|
|
17
|
-
required: ['workspace'],
|
|
18
93
|
properties: {
|
|
19
94
|
...workspaceProperty,
|
|
20
95
|
query: {
|
|
@@ -33,26 +108,24 @@ const tools = [
|
|
|
33
108
|
},
|
|
34
109
|
},
|
|
35
110
|
},
|
|
36
|
-
|
|
37
|
-
const workspace = String(input.workspace);
|
|
111
|
+
run: (input, context) => withWorkspace(input, context, (workspace, cwd) => {
|
|
38
112
|
if (typeof input.mode === 'string') {
|
|
39
|
-
return ['ask', workspace, `--${input.mode}`, '--json'];
|
|
113
|
+
return runCli(['ask', workspace, `--${input.mode}`, '--json'], cwd);
|
|
40
114
|
}
|
|
41
115
|
if (typeof input.query === 'string' && input.query.length > 0) {
|
|
42
116
|
const budget = typeof input.budget === 'number'
|
|
43
117
|
? ['--budget', String(input.budget)]
|
|
44
118
|
: ['--json'];
|
|
45
|
-
return ['ask', workspace, input.query, ...budget];
|
|
119
|
+
return runCli(['ask', workspace, input.query, ...budget], cwd);
|
|
46
120
|
}
|
|
47
|
-
return ['ask', workspace, '--json'];
|
|
48
|
-
},
|
|
121
|
+
return runCli(['ask', workspace, '--json'], cwd);
|
|
122
|
+
}),
|
|
49
123
|
},
|
|
50
124
|
{
|
|
51
125
|
name: 'yarramate_design',
|
|
52
|
-
description:
|
|
126
|
+
description: `The design interview, one stateless step: the top open question with its subject slice, materiality, progress, and the operations skeleton that would answer it. Ask the person, land their answer with yarramate_apply, then call this again; the next question is computed from the record, never remembered. A question whose authority is human is for the person to decide, not the agent. ${LOOP}`,
|
|
53
127
|
inputSchema: {
|
|
54
128
|
type: 'object',
|
|
55
|
-
required: ['workspace'],
|
|
56
129
|
properties: {
|
|
57
130
|
...workspaceProperty,
|
|
58
131
|
subject: {
|
|
@@ -61,61 +134,203 @@ const tools = [
|
|
|
61
134
|
},
|
|
62
135
|
},
|
|
63
136
|
},
|
|
64
|
-
|
|
137
|
+
run: (input, context) => withWorkspace(input, context, (workspace, cwd) => runCli([
|
|
65
138
|
'design',
|
|
66
|
-
|
|
139
|
+
workspace,
|
|
67
140
|
...(typeof input.subject === 'string'
|
|
68
141
|
? ['--subject', input.subject]
|
|
69
142
|
: []),
|
|
70
143
|
'--json',
|
|
71
|
-
],
|
|
144
|
+
], cwd)),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'yarramate_apply',
|
|
148
|
+
description: `Lands answers in the record: one yarramate/operations/v1 document, applied as an atomic batch by the same CLI command a person runs. Any invalid operation refuses the whole batch and nothing is written; the result names each diagnostic with its source location. Writes are spliced into the native documents, never re-serialized, so the diff is exactly the answer. ${LOOP}`,
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
required: ['operations'],
|
|
152
|
+
properties: {
|
|
153
|
+
...workspaceProperty,
|
|
154
|
+
operations: {
|
|
155
|
+
description: 'The operations document: `format: yarramate/operations/v1` with an `operations` list. Each operation names its `op` and its `document` (the native document path, for example .yarramate/architecture/main.yaml) and nests the record under the key its op names: add-concept carries `concept: { id, kind, name, description?, status?, ... }`, add-relationship carries `relationship: { id, kind, from, to, description? }`, update-concept and update-relationship carry the same key with only the fields that change, delete-* and rename-* carry the id (and `to` for a rename). Shape, as JSON: {"format":"yarramate/operations/v1","operations":[{"op":"add-concept","document":".yarramate/architecture/main.yaml","concept":{"id":"ops-portal","kind":"applicationComponent","name":"Operations portal"}}]}. YAML or JSON text, or the equivalent JSON object.',
|
|
156
|
+
oneOf: [{ type: 'string' }, { type: 'object' }],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
run: (input, context) => withWorkspace(input, context, (workspace, cwd) => {
|
|
161
|
+
const text = operationsText(input.operations);
|
|
162
|
+
if (text === undefined) {
|
|
163
|
+
return refuse('yarramate_apply needs `operations`: a yarramate/operations/v1 document as YAML or JSON text, or as an object.');
|
|
164
|
+
}
|
|
165
|
+
return withScratch((directory) => {
|
|
166
|
+
const path = join(directory, 'operations.yaml');
|
|
167
|
+
writeFileSync(path, text, 'utf8');
|
|
168
|
+
return runCli(['apply', path, workspace, '--json'], cwd);
|
|
169
|
+
});
|
|
170
|
+
}),
|
|
72
171
|
},
|
|
73
172
|
{
|
|
74
173
|
name: 'yarramate_check',
|
|
75
|
-
description:
|
|
174
|
+
description: `Deterministic correctness check of a workspace; returns the machine-readable check result. Never a quality or completeness judgement. Run it after every apply. ${LOOP}`,
|
|
76
175
|
inputSchema: {
|
|
77
176
|
type: 'object',
|
|
78
|
-
required: ['workspace'],
|
|
79
177
|
properties: workspaceProperty,
|
|
80
178
|
},
|
|
81
|
-
|
|
179
|
+
run: (input, context) => withWorkspace(input, context, (workspace, cwd) => runCli(['check', workspace, '--json'], cwd)),
|
|
82
180
|
},
|
|
83
181
|
{
|
|
84
182
|
name: 'yarramate_reconcile',
|
|
85
|
-
description:
|
|
183
|
+
description: `Compare declared architecture with evaluated evidence; returns the reconciliation report with contradicted, unknown, and not-observed findings. ${LOOP}`,
|
|
86
184
|
inputSchema: {
|
|
87
185
|
type: 'object',
|
|
88
|
-
required: ['workspace'],
|
|
89
186
|
properties: workspaceProperty,
|
|
90
187
|
},
|
|
91
|
-
|
|
188
|
+
run: (input, context) => withWorkspace(input, context, (workspace, cwd) => runCli(['reconcile', workspace], cwd)),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'yarramate_export',
|
|
192
|
+
description: `Derives a deliverable from the record and returns it as text: markdown (a projection rendered as prose; needs projection), rtm (the requirements traceability matrix), graph (the compiled semantic graph as JSON), briefs (one brief per subject of a projection; needs projection). xlsx and likec4 write binary or multi-file output, so they need out (and likec4 needs project) and return what was written. ${LOOP}`,
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
required: ['kind'],
|
|
196
|
+
properties: {
|
|
197
|
+
...workspaceProperty,
|
|
198
|
+
kind: {
|
|
199
|
+
type: 'string',
|
|
200
|
+
enum: ['markdown', 'rtm', 'graph', 'briefs', 'xlsx', 'likec4'],
|
|
201
|
+
},
|
|
202
|
+
projection: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
description: 'Projection path, relative to the repository root unless absolute; required for markdown, briefs and xlsx',
|
|
205
|
+
},
|
|
206
|
+
out: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
description: 'Where to write, relative to the repository root (the directory that holds .yarramate) unless absolute: a file for markdown, graph and xlsx; a directory for rtm, briefs and likec4. Optional for the text kinds, which then return the text instead.',
|
|
209
|
+
},
|
|
210
|
+
project: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
description: 'The likec4-project.yaml, required for likec4',
|
|
213
|
+
},
|
|
214
|
+
budget: {
|
|
215
|
+
type: 'integer',
|
|
216
|
+
minimum: 1,
|
|
217
|
+
description: 'Approximate token budget per brief (briefs only)',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
run: (input, context) => withWorkspace(input, context, (workspace, cwd) => {
|
|
222
|
+
const kind = typeof input.kind === 'string' ? input.kind : '';
|
|
223
|
+
const projection = typeof input.projection === 'string' ? input.projection : undefined;
|
|
224
|
+
const out = typeof input.out === 'string' ? input.out : undefined;
|
|
225
|
+
const budget = typeof input.budget === 'number'
|
|
226
|
+
? ['--budget', String(input.budget)]
|
|
227
|
+
: [];
|
|
228
|
+
if (kind === 'markdown' || kind === 'graph') {
|
|
229
|
+
if (kind === 'markdown' && projection === undefined) {
|
|
230
|
+
return refuse('yarramate_export markdown needs `projection`: the path of the view to render.');
|
|
231
|
+
}
|
|
232
|
+
return runCli([
|
|
233
|
+
'export',
|
|
234
|
+
kind,
|
|
235
|
+
...(projection === undefined ? [] : [projection]),
|
|
236
|
+
workspace,
|
|
237
|
+
...(out === undefined ? [] : ['--out', out]),
|
|
238
|
+
], cwd);
|
|
239
|
+
}
|
|
240
|
+
if (kind === 'rtm') {
|
|
241
|
+
if (out !== undefined) {
|
|
242
|
+
return runCli(['export', 'rtm', workspace, '--out', out], cwd);
|
|
243
|
+
}
|
|
244
|
+
return withScratch((directory) => {
|
|
245
|
+
const result = runCli(['export', 'rtm', workspace, '--out', directory], cwd);
|
|
246
|
+
if (result.exitCode !== 0)
|
|
247
|
+
return result;
|
|
248
|
+
return {
|
|
249
|
+
exitCode: 0,
|
|
250
|
+
stdout: readFileSync(join(directory, 'RTM.md'), 'utf8'),
|
|
251
|
+
stderr: '',
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (kind === 'briefs') {
|
|
256
|
+
if (projection === undefined) {
|
|
257
|
+
return refuse('yarramate_export briefs needs `projection`: the path of the view whose subjects get a brief each.');
|
|
258
|
+
}
|
|
259
|
+
if (out !== undefined) {
|
|
260
|
+
return runCli([
|
|
261
|
+
'export',
|
|
262
|
+
'briefs',
|
|
263
|
+
projection,
|
|
264
|
+
workspace,
|
|
265
|
+
'--out',
|
|
266
|
+
out,
|
|
267
|
+
...budget,
|
|
268
|
+
], cwd);
|
|
269
|
+
}
|
|
270
|
+
return withScratch((directory) => {
|
|
271
|
+
const result = runCli([
|
|
272
|
+
'export',
|
|
273
|
+
'briefs',
|
|
274
|
+
projection,
|
|
275
|
+
workspace,
|
|
276
|
+
'--out',
|
|
277
|
+
directory,
|
|
278
|
+
...budget,
|
|
279
|
+
], cwd);
|
|
280
|
+
if (result.exitCode !== 0)
|
|
281
|
+
return result;
|
|
282
|
+
const files = readdirSync(directory).sort();
|
|
283
|
+
return {
|
|
284
|
+
exitCode: 0,
|
|
285
|
+
stdout: files
|
|
286
|
+
.map((file) => `<!-- ${file} -->\n${readFileSync(join(directory, file), 'utf8')}`)
|
|
287
|
+
.join('\n'),
|
|
288
|
+
stderr: '',
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (kind === 'xlsx') {
|
|
293
|
+
if (projection === undefined || out === undefined) {
|
|
294
|
+
return refuse('yarramate_export xlsx needs `projection` and `out`: the view to export and the .xlsx path to write; a workbook is binary and cannot come back as text.');
|
|
295
|
+
}
|
|
296
|
+
return runCli(['export', 'xlsx', projection, workspace, '--out', out], cwd);
|
|
297
|
+
}
|
|
298
|
+
if (kind === 'likec4') {
|
|
299
|
+
const project = typeof input.project === 'string' ? input.project : undefined;
|
|
300
|
+
if (project === undefined || out === undefined) {
|
|
301
|
+
return refuse('yarramate_export likec4 needs `project` (the likec4-project.yaml) and `out` (the directory to write).');
|
|
302
|
+
}
|
|
303
|
+
return runCli(['export', 'likec4', project, out, workspace], cwd);
|
|
304
|
+
}
|
|
305
|
+
return refuse('yarramate_export needs `kind`: one of markdown, rtm, graph, briefs, xlsx, likec4.');
|
|
306
|
+
}),
|
|
92
307
|
},
|
|
93
308
|
];
|
|
309
|
+
/** The tool list a client sees; exported so a test can read it without stdio. */
|
|
310
|
+
export const toolCatalogue = tools.map(({ name, description, inputSchema }) => ({
|
|
311
|
+
name,
|
|
312
|
+
description,
|
|
313
|
+
inputSchema,
|
|
314
|
+
}));
|
|
94
315
|
const respond = (id, result) => {
|
|
95
316
|
process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`);
|
|
96
317
|
};
|
|
97
318
|
const respondError = (id, code, message) => {
|
|
98
319
|
process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } })}\n`);
|
|
99
320
|
};
|
|
100
|
-
export const handleRequest = (request) => {
|
|
321
|
+
export const handleRequest = (request, context = { cwd: process.cwd(), workspace: undefined }) => {
|
|
101
322
|
const id = request.id ?? null;
|
|
102
323
|
if (request.method === 'initialize') {
|
|
103
324
|
respond(id, {
|
|
104
325
|
protocolVersion: '2025-06-18',
|
|
105
326
|
capabilities: { tools: {} },
|
|
106
327
|
serverInfo: { name: 'yarramate', version: packageVersion },
|
|
107
|
-
instructions:
|
|
328
|
+
instructions: `The architecture record of a YarraMate workspace. The native documents in the repository are canonical; every read renders them, and yarramate_apply is the one write, the same atomic batch the CLI lands. ${LOOP}`,
|
|
108
329
|
});
|
|
109
330
|
return;
|
|
110
331
|
}
|
|
111
332
|
if (request.method === 'tools/list') {
|
|
112
|
-
respond(id, {
|
|
113
|
-
tools: tools.map(({ name, description, inputSchema }) => ({
|
|
114
|
-
name,
|
|
115
|
-
description,
|
|
116
|
-
inputSchema,
|
|
117
|
-
})),
|
|
118
|
-
});
|
|
333
|
+
respond(id, { tools: toolCatalogue });
|
|
119
334
|
return;
|
|
120
335
|
}
|
|
121
336
|
if (request.method === 'tools/call') {
|
|
@@ -129,7 +344,7 @@ export const handleRequest = (request) => {
|
|
|
129
344
|
const input = typeof params.arguments === 'object' && params.arguments !== null
|
|
130
345
|
? params.arguments
|
|
131
346
|
: {};
|
|
132
|
-
const result =
|
|
347
|
+
const result = tool.run(input, context);
|
|
133
348
|
respond(id, {
|
|
134
349
|
content: [
|
|
135
350
|
{
|
|
@@ -147,6 +362,27 @@ export const handleRequest = (request) => {
|
|
|
147
362
|
respondError(id, -32601, `Method "${request.method}" not found`);
|
|
148
363
|
}
|
|
149
364
|
};
|
|
365
|
+
/** `yarramate-mcp [--workspace <path>]`; anything else is refused with usage. */
|
|
366
|
+
export const parseServerOptions = (argv) => {
|
|
367
|
+
let workspace;
|
|
368
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
369
|
+
const option = argv[index];
|
|
370
|
+
if (option === '--workspace') {
|
|
371
|
+
const value = argv[index + 1];
|
|
372
|
+
if (value === undefined ||
|
|
373
|
+
value.startsWith('-') ||
|
|
374
|
+
workspace !== undefined) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
workspace = value;
|
|
378
|
+
index += 1;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
return { workspace };
|
|
384
|
+
};
|
|
385
|
+
export const serverUsage = 'Usage:\n yarramate-mcp [--workspace <workspace.yaml>]\n yarramate-mcp --version\n';
|
|
150
386
|
if (isMainModule(import.meta.url, process.argv[1])) {
|
|
151
387
|
if (process.argv[2] === '--version') {
|
|
152
388
|
const result = versionResult('yarramate-mcp');
|
|
@@ -154,25 +390,36 @@ if (isMainModule(import.meta.url, process.argv[1])) {
|
|
|
154
390
|
process.exitCode = result.exitCode;
|
|
155
391
|
}
|
|
156
392
|
else {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
393
|
+
const options = parseServerOptions(process.argv.slice(2));
|
|
394
|
+
if (options === undefined) {
|
|
395
|
+
process.stderr.write(serverUsage);
|
|
396
|
+
process.exitCode = 2;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
const context = {
|
|
400
|
+
cwd: process.cwd(),
|
|
401
|
+
workspace: options.workspace,
|
|
402
|
+
};
|
|
403
|
+
const lines = createInterface({ input: process.stdin });
|
|
404
|
+
lines.on('line', (line) => {
|
|
405
|
+
const text = line.trim();
|
|
406
|
+
if (text.length === 0)
|
|
407
|
+
return;
|
|
408
|
+
let request;
|
|
409
|
+
try {
|
|
410
|
+
request = JSON.parse(text);
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
respondError(null, -32700, 'Parse error');
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
handleRequest(request, context);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
respondError(request.id ?? null, -32603, error instanceof Error ? error.message : String(error));
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
177
424
|
}
|
|
178
425
|
}
|
|
@@ -118,6 +118,29 @@ export interface VisualChoiceSelectedPayload {
|
|
|
118
118
|
readonly choiceId: string;
|
|
119
119
|
readonly optionId: string;
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* The reviewer hands one open question to the agent (#515, ADR 0151). The
|
|
123
|
+
* phrasing rides along so the transcript, the journal and the agent read the
|
|
124
|
+
* same words; the agent still computes the step from the record, because the
|
|
125
|
+
* question is a reading of the model at one moment and the model may have
|
|
126
|
+
* moved since the row was drawn.
|
|
127
|
+
*/
|
|
128
|
+
export interface VisualQuestionDelegatePayload {
|
|
129
|
+
readonly questionId: string;
|
|
130
|
+
/** Null for a workspace-scoped question, which names no subject. */
|
|
131
|
+
readonly subjectId: string | null;
|
|
132
|
+
readonly question: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The agent's answer to a delegated question: operations it PROPOSES, which
|
|
136
|
+
* the browser stages into the reviewer's changeset and the reviewer commits
|
|
137
|
+
* (ADR 0151). The agent never writes (ADR 0084, ADR 0088); this is how it
|
|
138
|
+
* answers without doing so. `note` is the line the transcript shows.
|
|
139
|
+
*/
|
|
140
|
+
export interface VisualOperationsProposePayload {
|
|
141
|
+
readonly note: string;
|
|
142
|
+
readonly operations: readonly YarramateOperation[];
|
|
143
|
+
}
|
|
121
144
|
export interface VisualViewNavigatePayload {
|
|
122
145
|
readonly viewId: string;
|
|
123
146
|
readonly requiresAttention: boolean;
|
|
@@ -410,6 +433,10 @@ export type VisualBrowserInput = {
|
|
|
410
433
|
readonly type: "session.end";
|
|
411
434
|
readonly lastAcknowledgedSequence: number;
|
|
412
435
|
readonly payload: VisualBrowserSessionEndPayload;
|
|
436
|
+
} | {
|
|
437
|
+
readonly type: "question.delegate";
|
|
438
|
+
readonly lastAcknowledgedSequence: number;
|
|
439
|
+
readonly payload: VisualQuestionDelegatePayload;
|
|
413
440
|
};
|
|
414
441
|
interface VisualEventEnvelope<Type extends string, Payload> {
|
|
415
442
|
readonly format: "yarramate/visual-event/v1";
|
|
@@ -420,7 +447,7 @@ interface VisualEventEnvelope<Type extends string, Payload> {
|
|
|
420
447
|
readonly timestamp: string;
|
|
421
448
|
readonly payload: Payload;
|
|
422
449
|
}
|
|
423
|
-
export type VisualEvent = VisualEventEnvelope<"chat.message", VisualChatMessagePayload> | VisualEventEnvelope<"choice.selected", VisualChoiceSelectedPayload> | VisualEventEnvelope<"view.navigate", VisualViewNavigatePayload> | VisualEventEnvelope<"session.end", VisualSessionEndPayload> | VisualEventEnvelope<"browser.connected", VisualBrowserConnectedPayload> | VisualEventEnvelope<"browser.disconnected", VisualBrowserDisconnectedPayload> | VisualEventEnvelope<"filter.query", VisualFilterQueryPayload> | VisualEventEnvelope<"changeset.commit", VisualChangesetCommitPayload> | VisualEventEnvelope<"layout.save", VisualLayoutSavePayload>;
|
|
450
|
+
export type VisualEvent = VisualEventEnvelope<"chat.message", VisualChatMessagePayload> | VisualEventEnvelope<"choice.selected", VisualChoiceSelectedPayload> | VisualEventEnvelope<"question.delegate", VisualQuestionDelegatePayload> | VisualEventEnvelope<"view.navigate", VisualViewNavigatePayload> | VisualEventEnvelope<"session.end", VisualSessionEndPayload> | VisualEventEnvelope<"browser.connected", VisualBrowserConnectedPayload> | VisualEventEnvelope<"browser.disconnected", VisualBrowserDisconnectedPayload> | VisualEventEnvelope<"filter.query", VisualFilterQueryPayload> | VisualEventEnvelope<"changeset.commit", VisualChangesetCommitPayload> | VisualEventEnvelope<"layout.save", VisualLayoutSavePayload>;
|
|
424
451
|
/**
|
|
425
452
|
* The filter a chat turn resolved to. The agent states `query` and nothing
|
|
426
453
|
* else: the runtime evaluates it against the same graph a `filter.query`
|
|
@@ -499,7 +526,7 @@ interface VisualResponseEnvelope<Type extends string, Payload> {
|
|
|
499
526
|
readonly timestamp: string;
|
|
500
527
|
readonly payload: Payload;
|
|
501
528
|
}
|
|
502
|
-
export type VisualResponse = VisualResponseEnvelope<"chat.response", VisualChatResponsePayload> | VisualResponseEnvelope<"agent.status", VisualAgentStatusPayload> | VisualResponseEnvelope<"choice.present", VisualChoicePresentPayload> | VisualResponseEnvelope<"handoff.complete", VisualHandoffSummary> | VisualResponseEnvelope<"diagnostic", VisualDiagnosticPayload>;
|
|
529
|
+
export type VisualResponse = VisualResponseEnvelope<"chat.response", VisualChatResponsePayload> | VisualResponseEnvelope<"agent.status", VisualAgentStatusPayload> | VisualResponseEnvelope<"choice.present", VisualChoicePresentPayload> | VisualResponseEnvelope<"operations.propose", VisualOperationsProposePayload> | VisualResponseEnvelope<"handoff.complete", VisualHandoffSummary> | VisualResponseEnvelope<"diagnostic", VisualDiagnosticPayload>;
|
|
503
530
|
export type VisualHandoffDecision = "completed" | "cancelled" | "failed";
|
|
504
531
|
export type VisualTerminationReason = "user-ended" | "child-failed" | "browser-timeout" | "main-cancelled" | "server-failed" | "compiler-failed";
|
|
505
532
|
export interface VisualHandoff extends VisualHandoffSummary {
|
|
@@ -72,6 +72,7 @@ const CONTENT_TYPES = {
|
|
|
72
72
|
const TURN_COMPLETING = {
|
|
73
73
|
"chat.response": true,
|
|
74
74
|
"choice.present": true,
|
|
75
|
+
"operations.propose": true,
|
|
75
76
|
"handoff.complete": true,
|
|
76
77
|
diagnostic: true,
|
|
77
78
|
};
|
|
@@ -272,6 +273,8 @@ const eventFrom = (input, envelope) => {
|
|
|
272
273
|
return { ...envelope, type: input.type, payload: input.payload };
|
|
273
274
|
case "choice.selected":
|
|
274
275
|
return { ...envelope, type: input.type, payload: input.payload };
|
|
276
|
+
case "question.delegate":
|
|
277
|
+
return { ...envelope, type: input.type, payload: input.payload };
|
|
275
278
|
case "view.navigate":
|
|
276
279
|
return { ...envelope, type: input.type, payload: input.payload };
|
|
277
280
|
case "session.end":
|
|
@@ -836,7 +839,9 @@ export const startVisualServer = async (options) => {
|
|
|
836
839
|
const recordEvent = (event) => {
|
|
837
840
|
// A session on its way out waits on nobody, and asking past the question
|
|
838
841
|
// is how a reviewer declines to answer it.
|
|
839
|
-
if (event.type === "chat.message" ||
|
|
842
|
+
if (event.type === "chat.message" ||
|
|
843
|
+
event.type === "question.delegate" ||
|
|
844
|
+
event.type === "session.end") {
|
|
840
845
|
pendingChoice = null;
|
|
841
846
|
}
|
|
842
847
|
if (event.type === "session.end")
|
|
@@ -849,6 +854,15 @@ export const startVisualServer = async (options) => {
|
|
|
849
854
|
});
|
|
850
855
|
return;
|
|
851
856
|
}
|
|
857
|
+
if (event.type === "question.delegate") {
|
|
858
|
+
// The line the reviewer sees for handing a question over (#515).
|
|
859
|
+
transcript.push({
|
|
860
|
+
id: event.eventId,
|
|
861
|
+
speaker: "reviewer",
|
|
862
|
+
text: `Answer via your agent: ${event.payload.question}`,
|
|
863
|
+
});
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
852
866
|
if (event.type !== "choice.selected")
|
|
853
867
|
return;
|
|
854
868
|
if (pendingChoice?.choiceId === event.payload.choiceId)
|
|
@@ -874,9 +888,11 @@ export const startVisualServer = async (options) => {
|
|
|
874
888
|
}
|
|
875
889
|
const text = response.type === "chat.response"
|
|
876
890
|
? response.payload.text
|
|
877
|
-
: response.type === "
|
|
878
|
-
? response.payload.
|
|
879
|
-
:
|
|
891
|
+
: response.type === "operations.propose"
|
|
892
|
+
? response.payload.note
|
|
893
|
+
: response.type === "handoff.complete"
|
|
894
|
+
? response.payload.summary
|
|
895
|
+
: undefined;
|
|
880
896
|
if (text === undefined)
|
|
881
897
|
return;
|
|
882
898
|
transcript.push({ id: response.responseId, speaker: "agent", text });
|
|
@@ -1157,7 +1173,10 @@ export const startVisualServer = async (options) => {
|
|
|
1157
1173
|
}
|
|
1158
1174
|
// A session that was started without a conversation has no chat and no
|
|
1159
1175
|
// choices to make; the diagram, and moving around it, is all it granted.
|
|
1160
|
-
|
|
1176
|
+
// Delegating a question is a chat turn by another door (#515): it
|
|
1177
|
+
// needs the same agent the composer needs.
|
|
1178
|
+
const granted = input.value.type === "chat.message" ||
|
|
1179
|
+
input.value.type === "question.delegate"
|
|
1161
1180
|
? capabilities.chat
|
|
1162
1181
|
: input.value.type === "choice.selected"
|
|
1163
1182
|
? capabilities.choices
|
|
@@ -1677,7 +1696,9 @@ export const startVisualServer = async (options) => {
|
|
|
1677
1696
|
// The agent cannot hold a conversation a diagram-only session never
|
|
1678
1697
|
// offered; the browser has nowhere to show one.
|
|
1679
1698
|
if (!capabilities.chat &&
|
|
1680
|
-
(response.type === "chat.response" ||
|
|
1699
|
+
(response.type === "chat.response" ||
|
|
1700
|
+
response.type === "choice.present" ||
|
|
1701
|
+
response.type === "operations.propose")) {
|
|
1681
1702
|
respondJson(server, 409, {
|
|
1682
1703
|
accepted: false,
|
|
1683
1704
|
diagnostics: [
|