vouchington-tooling 0.40.0 → 0.42.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 +9 -7
- package/bin/policy-runner.mjs +26 -0
- package/dist/agent-blackboard/feedback-codec.d.mts +1 -0
- package/dist/agent-blackboard/feedback-codec.mjs +1 -1
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +3 -1
- package/dist/dependency-license-policy/audit-store.d.mts +1 -4
- package/dist/dependency-license-policy/audit-store.mjs +1 -16
- package/dist/dependency-license-policy/collect.d.mts +0 -1
- package/dist/dependency-license-policy/collect.mjs +2 -2
- package/dist/mcp-server/context.d.mts +11 -2
- package/dist/mcp-server/context.mjs +14 -1
- package/dist/mcp-server/dispatch.d.mts +3 -0
- package/dist/mcp-server/dispatch.mjs +5 -0
- package/dist/mcp-server/handlers-journal.d.mts +14 -1
- package/dist/mcp-server/handlers-journal.mjs +14 -9
- package/dist/mcp-server/handlers-retrospective.d.mts +12 -0
- package/dist/mcp-server/handlers-retrospective.mjs +154 -0
- package/dist/mcp-server/harness.test-helpers.d.mts +2 -0
- package/dist/mcp-server/harness.test-helpers.mjs +1 -0
- package/dist/mcp-server/journal-read.d.mts +9 -0
- package/dist/mcp-server/journal-read.mjs +33 -0
- package/dist/mcp-server/policy-config.d.mts +6 -0
- package/dist/mcp-server/policy-config.mjs +60 -0
- package/dist/mcp-server/policy-run.d.mts +40 -0
- package/dist/mcp-server/policy-run.mjs +116 -0
- package/dist/mcp-server/policy-spawn.d.mts +24 -0
- package/dist/mcp-server/policy-spawn.mjs +97 -0
- package/dist/mcp-server/retrospective-facts-input.d.mts +6 -0
- package/dist/mcp-server/retrospective-facts-input.mjs +25 -0
- package/dist/mcp-server/retrospective-front-matter.d.mts +22 -0
- package/dist/mcp-server/retrospective-front-matter.mjs +88 -0
- package/dist/mcp-server/retrospective-input.d.mts +29 -0
- package/dist/mcp-server/retrospective-input.mjs +145 -0
- package/dist/mcp-server/retrospective-resolve.d.mts +15 -0
- package/dist/mcp-server/retrospective-resolve.mjs +41 -0
- package/dist/mcp-server/tools-retrospective.d.mts +2 -0
- package/dist/mcp-server/tools-retrospective.mjs +48 -0
- package/dist/mcp-server/tools-schema.d.mts +54 -0
- package/dist/mcp-server/tools-schema.mjs +70 -0
- package/dist/mcp-server/tools.d.mts +1 -18
- package/dist/mcp-server/tools.mjs +4 -70
- package/dist/mcp-server/worktree.d.mts +11 -4
- package/dist/mcp-server/worktree.mjs +31 -6
- package/dist/pg-schema-snapshot/generated-arbiter-facts.d.mts +1 -1
- package/docs/mcp-server.md +94 -18
- package/package.json +2 -2
- package/skills/blackboard/SKILL.md +28 -14
- package/skills/manifest.json +4 -4
- package/skills/postgres-node-performance-tuning/SKILL.md +2 -0
- package/skills/postgres-node-performance-tuning/references/performance-patterns.md +23 -0
- package/skills/retrospective/SKILL.md +5 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FeedbackCoverage } from '../agent-blackboard/index.mts';
|
|
2
|
+
import { type PolicyRun } from './policy-run.mts';
|
|
3
|
+
import type { RetrospectiveFrontMatter } from './retrospective-front-matter.mts';
|
|
4
|
+
/** Rejects, naming the field, any argument the document's own front matter contradicts. */
|
|
5
|
+
export declare function checkFrontMatter(front: RetrospectiveFrontMatter, args: {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
workOutcome: string;
|
|
8
|
+
feedbackCoverage: FeedbackCoverage;
|
|
9
|
+
repositories: string[] | undefined;
|
|
10
|
+
}): void;
|
|
11
|
+
/**
|
|
12
|
+
* The canonical (sorted, deduplicated, lowercase) repositories: the argument, else the
|
|
13
|
+
* document's front matter, else the policy's `pullRequestDefaults().repositories`, else an error.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveRepositories(args: string[] | undefined, front: string[] | undefined, policy: Omit<PolicyRun, 'export' | 'input'>): Promise<string[]>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { normalizeRepositories } from "../agent-blackboard/feedback-codec.mjs";
|
|
2
|
+
import { runPolicyExport } from "./policy-run.mjs";
|
|
3
|
+
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
4
|
+
/** Rejects, naming the field, any argument the document's own front matter contradicts. */
|
|
5
|
+
export function checkFrontMatter(front, args) {
|
|
6
|
+
const mismatches = [];
|
|
7
|
+
if (front.sessionId !== undefined && front.sessionId !== args.sessionId)
|
|
8
|
+
mismatches.push('session_id does not match sessionId');
|
|
9
|
+
if (front.workOutcome !== undefined && front.workOutcome !== args.workOutcome)
|
|
10
|
+
mismatches.push('work_outcome does not match workOutcome');
|
|
11
|
+
if (front.feedbackCoverage !== undefined) {
|
|
12
|
+
// Field by field, so the document's JSON key order does not matter; sources stay ordered.
|
|
13
|
+
const { status, sources, droppedCount } = args.feedbackCoverage;
|
|
14
|
+
const given = front.feedbackCoverage;
|
|
15
|
+
if (given.status !== status ||
|
|
16
|
+
given.droppedCount !== droppedCount ||
|
|
17
|
+
!same(given.sources, sources))
|
|
18
|
+
mismatches.push('feedback_coverage does not match feedbackCoverage');
|
|
19
|
+
}
|
|
20
|
+
if (front.repositories !== undefined &&
|
|
21
|
+
args.repositories !== undefined &&
|
|
22
|
+
!same(normalizeRepositories(front.repositories), normalizeRepositories(args.repositories)))
|
|
23
|
+
mismatches.push('repositories does not match repositories');
|
|
24
|
+
if (mismatches.length)
|
|
25
|
+
throw new Error(`retrospective front matter contradicts the arguments: ${mismatches.join('; ')}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The canonical (sorted, deduplicated, lowercase) repositories: the argument, else the
|
|
29
|
+
* document's front matter, else the policy's `pullRequestDefaults().repositories`, else an error.
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveRepositories(args, front, policy) {
|
|
32
|
+
const given = args ?? front;
|
|
33
|
+
if (given !== undefined)
|
|
34
|
+
return normalizeRepositories(given);
|
|
35
|
+
const defaults = await runPolicyExport({ ...policy, export: 'pullRequestDefaults', input: {} });
|
|
36
|
+
const repositories = (defaults.status === 'ok' ? defaults.result : undefined);
|
|
37
|
+
if (Array.isArray(repositories?.repositories) && repositories.repositories.length)
|
|
38
|
+
return normalizeRepositories(repositories.repositories);
|
|
39
|
+
throw new Error('repositories is required: pass it, put it in the front matter, or have the policy ' +
|
|
40
|
+
'pullRequestDefaults export return { repositories }');
|
|
41
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ADDITIVE, COVERAGE, IDENTITY, IDENTITY_REQUIRED, ID_PATTERN, OUTCOMES, READ_ONLY, tool, } from "./tools-schema.mjs";
|
|
2
|
+
export const RETROSPECTIVE_TOOLS = [
|
|
3
|
+
tool('retrospective_check', 'Reports whether the session already holds a retrospective: { sessionId, saved, entries } ' +
|
|
4
|
+
'where each entry is { sourceEventId, timestamp, issues, prs } for an entry of type ' +
|
|
5
|
+
'retrospective, oldest first. sourceEventId narrows the result. A session that was never ' +
|
|
6
|
+
'created reports saved: false.', READ_ONLY, { sourceEventId: { type: 'string', pattern: ID_PATTERN, description: 'Only this event id.' } }),
|
|
7
|
+
tool('retrospective_save', 'Saves the session retrospective after the checkout policy accepts it, and returns the same ' +
|
|
8
|
+
'verified receipt as journal_append (status, receipt, timestamp, pendingCount, ' +
|
|
9
|
+
'worktreePendingCount) plus policy.warnings. Pass exactly one of markdown (a finished ' +
|
|
10
|
+
'document opening with the front matter composeRetrospective writes: date, issues, prs ' +
|
|
11
|
+
'and optionally session_id, description, work_outcome, feedback_coverage, repositories, ' +
|
|
12
|
+
'each line key: <JSON>; any that repeat an argument must equal it) or compose (the retrospective ' +
|
|
13
|
+
'composer input: narrative, date, issues, prs, description, facts, transcript, tools, ' +
|
|
14
|
+
'architecture, optional knownSensitiveValues; the server adds the rest). compose.transcript ' +
|
|
15
|
+
'takes only { sessionId?, harnessSessionId? } (sessionId must be the call sessionId; the ' +
|
|
16
|
+
'transcript is found by harnessSessionId, the harness session UUID, else by sessionId when ' +
|
|
17
|
+
'that is a UUID, in the default session roots of the worktree) or { status, reason } to report it ' +
|
|
18
|
+
'unassessed; any path override is rejected. The server redacts its blackboard token ' +
|
|
19
|
+
'from the document before the policy sees it. dryRun returns ' +
|
|
20
|
+
'{ markdown, policy: { warnings } } and writes nothing. The worktree package.json may ' +
|
|
21
|
+
'declare "vouchington-tooling": { "policy": "<module>" }; its validateRetrospective export ' +
|
|
22
|
+
'runs in a child process, and any error it reports rejects the save with nothing written. ' +
|
|
23
|
+
'Policy tools need a server launched inside a repository worktree. The server owns the ' +
|
|
24
|
+
'timestamp. sourceEventId defaults to retrospective-<sessionId> (retrospective-<sha256 hex of the ' +
|
|
25
|
+
'sessionId> when that exceeds 256 bytes); saving changed content ' +
|
|
26
|
+
'under the same sourceEventId is an event-conflict, so a revision needs a new ' +
|
|
27
|
+
'sourceEventId. Repeat an identical markdown call to retry. compose regenerates the document, ' +
|
|
28
|
+
'so deliver a retained compose save with outbox_flush instead of repeating it. Counts ' +
|
|
29
|
+
'follow journal_append.', ADDITIVE, {
|
|
30
|
+
...IDENTITY,
|
|
31
|
+
mode: { enum: ['interactive', 'autonomous'], description: 'Delivery mode.' },
|
|
32
|
+
workOutcome: { enum: OUTCOMES, description: 'Outcome of the work being reported.' },
|
|
33
|
+
feedbackCoverage: COVERAGE,
|
|
34
|
+
repositories: {
|
|
35
|
+
type: 'array',
|
|
36
|
+
items: { type: 'string' },
|
|
37
|
+
minItems: 1,
|
|
38
|
+
description: 'Exact owner/name repositories; stored sorted, deduplicated and lowercase. Default: ' +
|
|
39
|
+
'the front matter repositories, else the policy pullRequestDefaults repositories; ' +
|
|
40
|
+
'an error when none.',
|
|
41
|
+
},
|
|
42
|
+
markdown: { type: 'string', description: 'Finished retrospective markdown.' },
|
|
43
|
+
compose: { type: 'object', description: 'Composer input; exclusive with markdown.' },
|
|
44
|
+
sourceEventId: { type: 'string', pattern: ID_PATTERN, description: 'Stable event id.' },
|
|
45
|
+
category: { type: 'string', description: 'Optional context; never changes entry type.' },
|
|
46
|
+
dryRun: { type: 'boolean', description: 'Compose and validate only; write nothing.' },
|
|
47
|
+
}, [...IDENTITY_REQUIRED, 'mode', 'workOutcome', 'feedbackCoverage']),
|
|
48
|
+
];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type JsonSchema = Record<string, unknown>;
|
|
2
|
+
export type ToolDefinition = {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: 'object';
|
|
7
|
+
properties: Record<string, JsonSchema>;
|
|
8
|
+
required: string[];
|
|
9
|
+
additionalProperties: false;
|
|
10
|
+
};
|
|
11
|
+
annotations: {
|
|
12
|
+
readOnlyHint: boolean;
|
|
13
|
+
destructiveHint: boolean;
|
|
14
|
+
idempotentHint: boolean;
|
|
15
|
+
openWorldHint: boolean;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare const ID_PATTERN = "^[A-Za-z0-9._:-]+$";
|
|
19
|
+
export declare const PARENT_SESSION_ID: JsonSchema;
|
|
20
|
+
export declare const AGENT: JsonSchema;
|
|
21
|
+
export declare const VERSION: JsonSchema;
|
|
22
|
+
export declare const OUTCOMES: string[];
|
|
23
|
+
export declare const COVERAGE: JsonSchema;
|
|
24
|
+
export declare const READ_ONLY: {
|
|
25
|
+
readOnlyHint: boolean;
|
|
26
|
+
destructiveHint: boolean;
|
|
27
|
+
idempotentHint: boolean;
|
|
28
|
+
openWorldHint: boolean;
|
|
29
|
+
};
|
|
30
|
+
export declare const ADDITIVE: {
|
|
31
|
+
readOnlyHint: boolean;
|
|
32
|
+
destructiveHint: boolean;
|
|
33
|
+
idempotentHint: boolean;
|
|
34
|
+
openWorldHint: boolean;
|
|
35
|
+
};
|
|
36
|
+
export declare const IDEMPOTENT_ADDITIVE: {
|
|
37
|
+
readOnlyHint: boolean;
|
|
38
|
+
destructiveHint: boolean;
|
|
39
|
+
idempotentHint: boolean;
|
|
40
|
+
openWorldHint: boolean;
|
|
41
|
+
};
|
|
42
|
+
export declare const MUTATING: {
|
|
43
|
+
readOnlyHint: boolean;
|
|
44
|
+
destructiveHint: boolean;
|
|
45
|
+
idempotentHint: boolean;
|
|
46
|
+
openWorldHint: boolean;
|
|
47
|
+
};
|
|
48
|
+
export declare function tool(name: string, description: string, annotations: ToolDefinition['annotations'], properties?: Record<string, JsonSchema>, required?: string[]): ToolDefinition;
|
|
49
|
+
export declare const IDENTITY: {
|
|
50
|
+
parentSessionId: JsonSchema;
|
|
51
|
+
agent: JsonSchema;
|
|
52
|
+
version: JsonSchema;
|
|
53
|
+
};
|
|
54
|
+
export declare const IDENTITY_REQUIRED: string[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const ID_PATTERN = '^[A-Za-z0-9._:-]+$';
|
|
2
|
+
const SESSION_ID = {
|
|
3
|
+
type: 'string',
|
|
4
|
+
pattern: ID_PATTERN,
|
|
5
|
+
maxLength: 256,
|
|
6
|
+
description: 'Explicit session id (URL-safe). Never inferred by the server.',
|
|
7
|
+
};
|
|
8
|
+
const WORKTREE = {
|
|
9
|
+
type: 'string',
|
|
10
|
+
description: 'Absolute path of the top level of a git worktree (`git rev-parse --show-toplevel`), not a ' +
|
|
11
|
+
'subdirectory. Defaults to the launch directory worktree; required when the server was ' +
|
|
12
|
+
'launched outside a git worktree.',
|
|
13
|
+
};
|
|
14
|
+
export const PARENT_SESSION_ID = {
|
|
15
|
+
type: ['string', 'null'],
|
|
16
|
+
pattern: ID_PATTERN,
|
|
17
|
+
maxLength: 256,
|
|
18
|
+
description: 'Direct parent session id, or null for a root session.',
|
|
19
|
+
};
|
|
20
|
+
export const AGENT = { type: 'string', maxLength: 128, description: 'Agent name.' };
|
|
21
|
+
export const VERSION = { type: 'string', maxLength: 128, description: 'Agent version.' };
|
|
22
|
+
export const OUTCOMES = [
|
|
23
|
+
'in-progress',
|
|
24
|
+
'success',
|
|
25
|
+
'failure',
|
|
26
|
+
'cancelled',
|
|
27
|
+
'timed-out',
|
|
28
|
+
'no-change',
|
|
29
|
+
'policy-refusal',
|
|
30
|
+
'unknown',
|
|
31
|
+
];
|
|
32
|
+
const COVERAGE_STATUSES = ['complete', 'partial', 'unavailable', 'not-assessed', 'not-started'];
|
|
33
|
+
export const COVERAGE = {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
status: { enum: COVERAGE_STATUSES },
|
|
37
|
+
sources: { type: 'array', items: { type: 'string' } },
|
|
38
|
+
droppedCount: { type: 'integer', minimum: 0 },
|
|
39
|
+
},
|
|
40
|
+
required: ['status'],
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
description: 'Explicit coverage; sources default to [] and droppedCount to 0.',
|
|
43
|
+
};
|
|
44
|
+
export const READ_ONLY = flags(true, false, true);
|
|
45
|
+
export const ADDITIVE = flags(false, false, false);
|
|
46
|
+
export const IDEMPOTENT_ADDITIVE = flags(false, false, true);
|
|
47
|
+
export const MUTATING = flags(false, true, true);
|
|
48
|
+
function flags(readOnly, destructive, idempotent) {
|
|
49
|
+
return {
|
|
50
|
+
readOnlyHint: readOnly,
|
|
51
|
+
destructiveHint: destructive,
|
|
52
|
+
idempotentHint: idempotent,
|
|
53
|
+
openWorldHint: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function tool(name, description, annotations, properties = {}, required = []) {
|
|
57
|
+
return {
|
|
58
|
+
name,
|
|
59
|
+
description,
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: { sessionId: SESSION_ID, worktree: WORKTREE, ...properties },
|
|
63
|
+
required: ['sessionId', ...required],
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
},
|
|
66
|
+
annotations,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export const IDENTITY = { parentSessionId: PARENT_SESSION_ID, agent: AGENT, version: VERSION };
|
|
70
|
+
export const IDENTITY_REQUIRED = ['parentSessionId', 'agent', 'version'];
|
|
@@ -1,19 +1,2 @@
|
|
|
1
|
-
type
|
|
2
|
-
export type ToolDefinition = {
|
|
3
|
-
name: string;
|
|
4
|
-
description: string;
|
|
5
|
-
inputSchema: {
|
|
6
|
-
type: 'object';
|
|
7
|
-
properties: Record<string, JsonSchema>;
|
|
8
|
-
required: string[];
|
|
9
|
-
additionalProperties: false;
|
|
10
|
-
};
|
|
11
|
-
annotations: {
|
|
12
|
-
readOnlyHint: boolean;
|
|
13
|
-
destructiveHint: boolean;
|
|
14
|
-
idempotentHint: boolean;
|
|
15
|
-
openWorldHint: boolean;
|
|
16
|
-
};
|
|
17
|
-
};
|
|
1
|
+
import { type ToolDefinition } from './tools-schema.mts';
|
|
18
2
|
export declare const TOOLS: readonly ToolDefinition[];
|
|
19
|
-
export {};
|
|
@@ -1,62 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
type: 'string',
|
|
4
|
-
pattern: ID_PATTERN,
|
|
5
|
-
maxLength: 256,
|
|
6
|
-
description: 'Explicit session id (URL-safe). Never inferred by the server.',
|
|
7
|
-
};
|
|
8
|
-
const WORKTREE = {
|
|
9
|
-
type: 'string',
|
|
10
|
-
description: 'Absolute path of the top level of a git worktree (`git rev-parse --show-toplevel`), not a ' +
|
|
11
|
-
'subdirectory. Defaults to the launch directory worktree; required when the server was ' +
|
|
12
|
-
'launched outside a git worktree.',
|
|
13
|
-
};
|
|
14
|
-
const PARENT_SESSION_ID = {
|
|
15
|
-
type: ['string', 'null'],
|
|
16
|
-
pattern: ID_PATTERN,
|
|
17
|
-
maxLength: 256,
|
|
18
|
-
description: 'Direct parent session id, or null for a root session.',
|
|
19
|
-
};
|
|
20
|
-
const AGENT = { type: 'string', maxLength: 128, description: 'Agent name.' };
|
|
21
|
-
const VERSION = { type: 'string', maxLength: 128, description: 'Agent version.' };
|
|
22
|
-
const OUTCOMES = [
|
|
23
|
-
'in-progress',
|
|
24
|
-
'success',
|
|
25
|
-
'failure',
|
|
26
|
-
'cancelled',
|
|
27
|
-
'timed-out',
|
|
28
|
-
'no-change',
|
|
29
|
-
'policy-refusal',
|
|
30
|
-
'unknown',
|
|
31
|
-
];
|
|
32
|
-
const COVERAGE_STATUSES = ['complete', 'partial', 'unavailable', 'not-assessed', 'not-started'];
|
|
33
|
-
const READ_ONLY = flags(true, false, true);
|
|
34
|
-
const ADDITIVE = flags(false, false, false);
|
|
35
|
-
const IDEMPOTENT_ADDITIVE = flags(false, false, true);
|
|
36
|
-
const MUTATING = flags(false, true, true);
|
|
37
|
-
function flags(readOnly, destructive, idempotent) {
|
|
38
|
-
return {
|
|
39
|
-
readOnlyHint: readOnly,
|
|
40
|
-
destructiveHint: destructive,
|
|
41
|
-
idempotentHint: idempotent,
|
|
42
|
-
openWorldHint: false,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
function tool(name, description, annotations, properties = {}, required = []) {
|
|
46
|
-
return {
|
|
47
|
-
name,
|
|
48
|
-
description,
|
|
49
|
-
inputSchema: {
|
|
50
|
-
type: 'object',
|
|
51
|
-
properties: { sessionId: SESSION_ID, worktree: WORKTREE, ...properties },
|
|
52
|
-
required: ['sessionId', ...required],
|
|
53
|
-
additionalProperties: false,
|
|
54
|
-
},
|
|
55
|
-
annotations,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
const IDENTITY = { parentSessionId: PARENT_SESSION_ID, agent: AGENT, version: VERSION };
|
|
59
|
-
const IDENTITY_REQUIRED = ['parentSessionId', 'agent', 'version'];
|
|
1
|
+
import { ADDITIVE, AGENT, COVERAGE, IDEMPOTENT_ADDITIVE, IDENTITY, IDENTITY_REQUIRED, ID_PATTERN, MUTATING, OUTCOMES, PARENT_SESSION_ID, READ_ONLY, VERSION, tool, } from "./tools-schema.mjs";
|
|
2
|
+
import { RETROSPECTIVE_TOOLS } from "./tools-retrospective.mjs";
|
|
60
3
|
export const TOOLS = [
|
|
61
4
|
tool('journal_append', 'Appends one journal entry to the caller session and returns the verified read-back receipt. ' +
|
|
62
5
|
'Nothing is written unless every required field validates. Interactive mode retains the ' +
|
|
@@ -78,17 +21,7 @@ export const TOOLS = [
|
|
|
78
21
|
minItems: 1,
|
|
79
22
|
description: 'Exact owner/name repositories this entry concerns.',
|
|
80
23
|
},
|
|
81
|
-
feedbackCoverage:
|
|
82
|
-
type: 'object',
|
|
83
|
-
properties: {
|
|
84
|
-
status: { enum: COVERAGE_STATUSES },
|
|
85
|
-
sources: { type: 'array', items: { type: 'string' } },
|
|
86
|
-
droppedCount: { type: 'integer', minimum: 0 },
|
|
87
|
-
},
|
|
88
|
-
required: ['status'],
|
|
89
|
-
additionalProperties: false,
|
|
90
|
-
description: 'Explicit coverage; sources default to [] and droppedCount to 0.',
|
|
91
|
-
},
|
|
24
|
+
feedbackCoverage: COVERAGE,
|
|
92
25
|
category: { type: 'string', description: 'Optional context; never changes entry type.' },
|
|
93
26
|
}, [
|
|
94
27
|
...IDENTITY_REQUIRED,
|
|
@@ -112,6 +45,7 @@ export const TOOLS = [
|
|
|
112
45
|
'as sourceEventId and diagnostic.', IDEMPOTENT_ADDITIVE),
|
|
113
46
|
tool('session_ensure', 'Idempotently creates a session, or verifies that an existing one has the same identity. ' +
|
|
114
47
|
'Returns the session id and whether it was created.', IDEMPOTENT_ADDITIVE, IDENTITY, IDENTITY_REQUIRED),
|
|
48
|
+
...RETROSPECTIVE_TOOLS,
|
|
115
49
|
tool('snapshot_export', 'Exports non-archived sessions into a private local JSONL file chosen by the blackboard ' +
|
|
116
50
|
'client. Returns the path, counts, checksum, manifest, and cleanupToken; never the records. ' +
|
|
117
51
|
'sessionId identifies the caller.', ADDITIVE, {
|
|
@@ -11,12 +11,19 @@ export declare const runIsolatedGit: RunTextCommand;
|
|
|
11
11
|
* or an unsafe repository, is thrown with the cause rather than reported as "outside a worktree".
|
|
12
12
|
*/
|
|
13
13
|
export declare function launchWorktreeRoot(cwd: string, runGit: RunTextCommand): Promise<string | undefined>;
|
|
14
|
+
/**
|
|
15
|
+
* The worktree paths in `git worktree list --porcelain -z` output: NUL-terminated fields, an empty
|
|
16
|
+
* field ending each record, so a path containing a newline cannot forge a record.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseWorktreeList(output: string): string[];
|
|
14
19
|
/**
|
|
15
20
|
* Resolves the worktree a tool call may act on. It must be an absolute path that, after
|
|
16
|
-
* resolving symlinks, is exactly the top level of a git worktree
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
21
|
+
* resolving symlinks, is exactly the top level of a git worktree. A subdirectory, a relative or
|
|
22
|
+
* nonexistent path, and a non-git directory are rejected. When the server was launched inside a
|
|
23
|
+
* repository, the path must also be one of that repository's worktrees (primary or linked), so an
|
|
24
|
+
* unrelated `git init` directory is refused. A server launched outside any repository accepts the
|
|
25
|
+
* top level of any worktree. Git is asked on every call, so worktrees created after the server
|
|
26
|
+
* started are accepted and removed ones are not. Without `requested`, the launch worktree is used.
|
|
20
27
|
*/
|
|
21
28
|
export declare function resolveWorktree(input: {
|
|
22
29
|
launchRoot: string | undefined;
|
|
@@ -52,12 +52,33 @@ export async function launchWorktreeRoot(cwd, runGit) {
|
|
|
52
52
|
}
|
|
53
53
|
return realpath(output.trim());
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The worktree paths in `git worktree list --porcelain -z` output: NUL-terminated fields, an empty
|
|
57
|
+
* field ending each record, so a path containing a newline cannot forge a record.
|
|
58
|
+
*/
|
|
59
|
+
export function parseWorktreeList(output) {
|
|
60
|
+
return output
|
|
61
|
+
.split('\0')
|
|
62
|
+
.filter((field) => field.startsWith('worktree '))
|
|
63
|
+
.map((field) => field.slice('worktree '.length));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The real paths of every worktree of the repository containing `root` (the primary worktree and
|
|
67
|
+
* each linked one).
|
|
68
|
+
*/
|
|
69
|
+
async function repositoryWorktrees(root, runGit) {
|
|
70
|
+
const output = await runGit(['-C', root, 'worktree', 'list', '--porcelain', '-z']);
|
|
71
|
+
const paths = parseWorktreeList(output).map(canonical);
|
|
72
|
+
return new Set((await Promise.all(paths)).filter((path) => path !== undefined));
|
|
73
|
+
}
|
|
55
74
|
/**
|
|
56
75
|
* Resolves the worktree a tool call may act on. It must be an absolute path that, after
|
|
57
|
-
* resolving symlinks, is exactly the top level of a git worktree
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
76
|
+
* resolving symlinks, is exactly the top level of a git worktree. A subdirectory, a relative or
|
|
77
|
+
* nonexistent path, and a non-git directory are rejected. When the server was launched inside a
|
|
78
|
+
* repository, the path must also be one of that repository's worktrees (primary or linked), so an
|
|
79
|
+
* unrelated `git init` directory is refused. A server launched outside any repository accepts the
|
|
80
|
+
* top level of any worktree. Git is asked on every call, so worktrees created after the server
|
|
81
|
+
* started are accepted and removed ones are not. Without `requested`, the launch worktree is used.
|
|
61
82
|
*/
|
|
62
83
|
export async function resolveWorktree(input) {
|
|
63
84
|
const { launchRoot, requested, runGit } = input;
|
|
@@ -68,8 +89,12 @@ export async function resolveWorktree(input) {
|
|
|
68
89
|
if (!isAbsolute(path))
|
|
69
90
|
throw new Error('worktree must be an absolute path to a git worktree');
|
|
70
91
|
const target = await canonical(path);
|
|
71
|
-
if (target !== undefined && (await launchWorktreeRoot(target, runGit)) === target)
|
|
72
|
-
|
|
92
|
+
if (target !== undefined && (await launchWorktreeRoot(target, runGit)) === target) {
|
|
93
|
+
if (launchRoot === undefined || (await repositoryWorktrees(launchRoot, runGit)).has(target))
|
|
94
|
+
return target;
|
|
95
|
+
throw new Error(`worktree ${path} is not a worktree of the repository this server was launched in ` +
|
|
96
|
+
`(${launchRoot}); \`git worktree list\` there names the worktrees it may act on`);
|
|
97
|
+
}
|
|
73
98
|
throw new Error(`worktree ${path} is not the top level of a git worktree: it must exist and be the ` +
|
|
74
99
|
'directory `git rev-parse --show-toplevel` reports, not a subdirectory of it');
|
|
75
100
|
}
|
|
@@ -11,7 +11,7 @@ type GeneratedArbiterRoot = {
|
|
|
11
11
|
kind: 'parenthesized';
|
|
12
12
|
expression: GeneratedArbiterRoot;
|
|
13
13
|
} | {
|
|
14
|
-
kind: 'functionCall' | 'cast' | 'nullTest' | 'distinctness' | 'parameter' | 'typedLiteral' | 'literal' | 'binary' | 'unary' | 'case' | 'subquery' | 'other';
|
|
14
|
+
kind: 'functionCall' | 'cast' | 'nullTest' | 'distinctness' | 'parameter' | 'typedLiteral' | 'literal' | 'binary' | 'unary' | 'array' | 'case' | 'subquery' | 'other';
|
|
15
15
|
};
|
|
16
16
|
export type GeneratedArbiterExpression = {
|
|
17
17
|
columns: readonly GeneratedArbiterName[];
|
package/docs/mcp-server.md
CHANGED
|
@@ -6,8 +6,8 @@ a tool argument; there is no temp file, `--file`, or replay command. The server
|
|
|
6
6
|
existing `vouchington-tooling/agent-blackboard` exports and adds no provider protocol.
|
|
7
7
|
|
|
8
8
|
Phase 1 of [vouchington-tooling#324](https://github.com/vouchington/vouchington-tooling/issues/324)
|
|
9
|
-
covers the journal, outbox, session, and snapshot tools below.
|
|
10
|
-
are later
|
|
9
|
+
covers the journal, outbox, session, and snapshot tools below. Phase 2 adds the retrospective tools
|
|
10
|
+
and the policy contract they run. Pull request tools are a later phase.
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
@@ -40,8 +40,10 @@ environment. The server never searches for, prints, or mints them.
|
|
|
40
40
|
|
|
41
41
|
## CLI fallback
|
|
42
42
|
|
|
43
|
-
If
|
|
44
|
-
|
|
43
|
+
MCP is the supported path. If it is unavailable, restore it first as the blackboard skill describes.
|
|
44
|
+
As a last resort, use `vouchington agent-blackboard journal append|entries|flush|status` as described
|
|
45
|
+
there; sandboxed Claude Code and Codex withhold the token, so it can only retain records in the
|
|
46
|
+
outbox the server later flushes. Record the fallback and delivery status in the journal and tell the human. The CLI resolves its
|
|
45
47
|
client from its own installation, so install `agent-blackboard` alongside that trusted CLI.
|
|
46
48
|
Run the script with a verified absolute `node` and a cleared environment (`env -i`), never through
|
|
47
49
|
its `#!/usr/bin/env node` line, which takes `node` from a possibly repository-controlled `PATH`.
|
|
@@ -80,15 +82,17 @@ Every tool takes an explicit `sessionId` (`^[A-Za-z0-9._:-]+$`, at most 256 char
|
|
|
80
82
|
server never infers, and an optional `worktree`. Unknown arguments are rejected. Failures come back
|
|
81
83
|
as MCP tool errors (`isError: true`) with an actionable message rather than crashing the server.
|
|
82
84
|
|
|
83
|
-
| Tool
|
|
84
|
-
|
|
|
85
|
-
| `journal_append`
|
|
86
|
-
| `journal_entries`
|
|
87
|
-
| `outbox_status`
|
|
88
|
-
| `outbox_flush`
|
|
89
|
-
| `session_ensure`
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
85
|
+
| Tool | Arguments besides `sessionId` and `worktree` | Returns |
|
|
86
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
87
|
+
| `journal_append` | `parentSessionId`, `agent`, `version`, `mode` (`interactive` or `autonomous`), `markdown`, `sourceEventId`, `workOutcome`, `repositories`, `feedbackCoverage`, optional `category` | `sessionId`, `timestamp`, delivery `status` (`delivered` with a verified receipt, or `pending`), `pendingCount`, `worktreePendingCount` |
|
|
88
|
+
| `journal_entries` | none | `sessionId` and `entries`, every entry oldest first |
|
|
89
|
+
| `outbox_status` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount`, `rejectedCount`, `worktreeRejectedCount` |
|
|
90
|
+
| `outbox_flush` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount`, `rejectedCount`, `worktreeRejectedCount`, `deliveredCount`, `rejected` (when any) |
|
|
91
|
+
| `session_ensure` | `parentSessionId`, `agent`, `version` | `sessionId`, `status` (`created` or `exists`), `archived` |
|
|
92
|
+
| `retrospective_check` | optional `sourceEventId` | `sessionId`, `saved`, `entries` (`sourceEventId`, `timestamp`, `issues`, `prs` of each retrospective, oldest first) |
|
|
93
|
+
| `retrospective_save` | `parentSessionId`, `agent`, `version`, `mode`, `workOutcome`, `feedbackCoverage`, exactly one of `markdown` or `compose`, optional `repositories`, `sourceEventId`, `category`, `dryRun` | as `journal_append` plus `policy.warnings`; with `dryRun`, `markdown` and `policy.warnings` |
|
|
94
|
+
| `snapshot_export` | optional `agent`, `version`, `parentSessionId`, `data`, `dataArrayContains`, `inactiveForHours` | `sessionId`, `path`, `counts`, `checksum`, `manifest`, `cleanupToken` |
|
|
95
|
+
| `session_archive` | none | `sessionId`, `archived: true` |
|
|
92
96
|
|
|
93
97
|
`journal_append` validates the whole feedback envelope with `validateFeedbackEnvelope` before
|
|
94
98
|
anything is written, so a missing required field writes nothing. It returns the verified read-back
|
|
@@ -161,20 +165,92 @@ is unavailable.
|
|
|
161
165
|
The server sends the same list in its MCP `instructions`, so a harness that surfaces server
|
|
162
166
|
instructions shows it to the agent.
|
|
163
167
|
|
|
168
|
+
## Retrospective tools and the policy contract
|
|
169
|
+
|
|
170
|
+
`retrospective_check` reads the session through the same client as `journal_entries` and reports the
|
|
171
|
+
entries whose envelope `type` is `retrospective`. A session that was never created (the blackboard
|
|
172
|
+
answers 404) reports `saved: false`.
|
|
173
|
+
|
|
174
|
+
`retrospective_save` builds a `retrospective` envelope and writes it exactly as `journal_append`
|
|
175
|
+
does (same receipt, `pendingCount`, `worktreePendingCount`, outbox rules, and server-owned
|
|
176
|
+
timestamp; a `timestamp` argument is rejected). The document comes from `markdown` or from
|
|
177
|
+
`compose`, the input of `composeRetrospective` (`narrative`, `date`, `issues`, `prs`,
|
|
178
|
+
`description`, `facts`, `transcript`, `tools`, `architecture`, optional `knownSensitiveValues`;
|
|
179
|
+
unknown keys are rejected, and the journal audit reads through the server's own client).
|
|
180
|
+
`compose.transcript` accepts only `{ sessionId?, harnessSessionId? }` (`sessionId` must equal the
|
|
181
|
+
call's `sessionId`) or `{ status, reason }` for an unassessed transcript. The transcript is found by
|
|
182
|
+
`harnessSessionId`, the harness's own session UUID, else by the call's `sessionId` when that is a
|
|
183
|
+
UUID; otherwise the call fails rather than reporting the transcript unavailable. When the server's environment names its harness session (`CLAUDE_CODE_SESSION_ID`, `CODEX_THREAD_ID`, and so on), only that session's transcript is read (after `/clear` or a thread switch, reconnect the server, or pass `{ status, reason }`); `jsonlPath`, `projectsDir`, `codexSessionsDir`,
|
|
184
|
+
`grokSessionsDir`, and `cwd` are rejected, and the server reads the transcript from the worktree
|
|
185
|
+
and its default session roots. The server's blackboard token, and any `knownSensitiveValues`, are
|
|
186
|
+
redacted from the document before the policy sees it or the envelope is stored. Facts commands
|
|
187
|
+
are killed after 120 seconds. `compose.facts` takes `pr`, `branch`, `noPr`, or `{ status, reason }`;
|
|
188
|
+
`repo` is rejected, so the server's GitHub token never queries a repository other than the
|
|
189
|
+
worktree's. Facts run `git` and `gh` in the worktree as the server's user, honoring its
|
|
190
|
+
repository configuration (which can select helpers that run code), so the worktree, including
|
|
191
|
+
`.git/config`, must be as trusted as the policy module. `dryRun`
|
|
192
|
+
runs every check and returns the document without writing.
|
|
193
|
+
|
|
194
|
+
The document must open with the front matter `composeRetrospective` writes: lines of
|
|
195
|
+
`key: <JSON value>` between two `---` lines. Recognized keys are `date`, `issues`, `prs` (required),
|
|
196
|
+
`session_id`, `description`, `work_outcome`, `feedback_coverage` (`status`, `sources`,
|
|
197
|
+
`droppedCount`), and `repositories`; anything else, a duplicate, or a malformed line is rejected.
|
|
198
|
+
`session_id`, `work_outcome`, `feedback_coverage`, and `repositories` must equal the arguments when
|
|
199
|
+
present. `repositories` resolves from the argument, else the front matter, else the policy's
|
|
200
|
+
`pullRequestDefaults()` result, else an error, and is stored sorted, deduplicated, and lowercase.
|
|
201
|
+
The default `sourceEventId` is `retrospective-<sessionId>`, or `retrospective-<sha256 hex of the
|
|
202
|
+
sessionId>` when that would exceed 256 bytes. Re-saving identical content returns the
|
|
203
|
+
stored receipt and timestamp. Changed content under the same `sourceEventId` is an
|
|
204
|
+
`event-conflict`; a revision needs a new `sourceEventId`. `compose` regenerates the document from
|
|
205
|
+
the live journal and repository, so repeating a `compose` call is not a retry: when an interactive
|
|
206
|
+
save is retained in the outbox, deliver it with `outbox_flush`.
|
|
207
|
+
|
|
208
|
+
### Policy module
|
|
209
|
+
|
|
210
|
+
The consumer's root `package.json` may declare
|
|
211
|
+
`"vouchington-tooling": { "policy": "dev/agent-policy.mts" }`. The path must be relative and, after
|
|
212
|
+
resolving symlinks, stay inside the worktree. Without a declaration the policy checks are skipped.
|
|
213
|
+
Policy tools also require a server launched inside a repository worktree on macOS or Linux (they
|
|
214
|
+
refuse to run on Windows, where the server cannot reap a policy's descendants by process group),
|
|
215
|
+
and refuse to run when `AGENT_BLACKBOARD_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN` is set to a value
|
|
216
|
+
shorter than four characters, which could not be redacted from policy output. Each call runs one named
|
|
217
|
+
export in a fresh child process (`bin/policy-runner.mjs`, started with the server's own Node), never
|
|
218
|
+
inside the server, with the worktree as its working directory and an environment of only `HOME`,
|
|
219
|
+
`PATH` (`/usr/bin`, `/bin`, `/usr/local/bin`, `/opt/homebrew/bin`, the Node directory, and the
|
|
220
|
+
directory of the first `gh` on the server's `PATH`, deduplicated), and `GH_TOKEN` / `GITHUB_TOKEN` when the server has
|
|
221
|
+
them. Blackboard credentials and `NODE_OPTIONS` are not passed. The child still runs as the
|
|
222
|
+
server's user, so this is not a sandbox: on Linux it could read the server's environment from
|
|
223
|
+
`/proc/<server pid>/environ`. Treat the policy module as trusted code. After the runner exits, its
|
|
224
|
+
whole process group is killed, so nothing it started outlives the call. The runner reads
|
|
225
|
+
`{ policyVersion: 1, export, input }` on stdin and writes `{ ok: true, result }` (or
|
|
226
|
+
`{ ok: false, error }`) on stdout; a missing export is skipped.
|
|
227
|
+
|
|
228
|
+
| Export | Input | Result | Timeout |
|
|
229
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ------- |
|
|
230
|
+
| `validateRetrospective` | `sessionId`, `parentSessionId`, `mode`, `workOutcome`, `feedbackCoverage`, `repositories`, `markdown`, `sourceEventId`, `metadata` (`date`, `issues`, `prs`, `description`) | `{ errors: string[], warnings?: string[] }` | 30 s |
|
|
231
|
+
| `pullRequestDefaults` | `{}` | `{ repositories: string[] }` | 120 s |
|
|
232
|
+
| `validatePullRequest` | reserved for the pull request tools | `{ errors: string[], warnings?: string[] }` | 120 s |
|
|
233
|
+
|
|
234
|
+
Any `errors` reject the save and write nothing. A non-zero exit, a timeout (reported as
|
|
235
|
+
`policy <export> timed out after <N>s`), output over 1 MB, malformed JSON, or a result of the wrong
|
|
236
|
+
shape is an error, never a pass.
|
|
237
|
+
|
|
164
238
|
## Worktree validation
|
|
165
239
|
|
|
166
240
|
`worktree` is checked on every call. It must be an absolute path that, after resolving symlinks,
|
|
167
241
|
equals the top level of a git worktree, that is, what `git rev-parse --show-toplevel` prints when
|
|
168
|
-
run in it.
|
|
169
|
-
the
|
|
170
|
-
|
|
242
|
+
run in it. When the server was launched inside a repository, the real path must also equal one of
|
|
243
|
+
the `worktree` entries of `git worktree list --porcelain` run in the launch root (the primary
|
|
244
|
+
worktree or a linked one), so an unrelated repository is refused. A server launched outside any
|
|
245
|
+
repository accepts the top level of any git worktree for the journal tools, but refuses the policy
|
|
246
|
+
tools. Worktrees created after launch are accepted and removed ones are refused. Rejected: a subdirectory of a worktree, a
|
|
171
247
|
relative or nonexistent path, and a directory that is not in a git worktree. Git runs without
|
|
172
248
|
`GIT_*` variables, so `GIT_DIR` cannot redirect the check, and with `LC_ALL=C` and no `LANGUAGE`, so
|
|
173
249
|
its "not a git repository" diagnostic is never translated.
|
|
174
250
|
|
|
175
251
|
`worktree` only chooses where the outbox lives. It is not a trust boundary for loading code: the
|
|
176
|
-
server never imports modules from it
|
|
177
|
-
|
|
252
|
+
server never imports modules from it. The policy tools run the declared module only in a child
|
|
253
|
+
process without blackboard credentials, as described above.
|
|
178
254
|
|
|
179
255
|
When `worktree` is omitted, the launch directory's worktree top level is used if the launch
|
|
180
256
|
directory is inside one, and it is validated like any explicit path. If the server was launched
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -377,7 +377,7 @@
|
|
|
377
377
|
},
|
|
378
378
|
"devDependencies": {
|
|
379
379
|
"@ast-grep/cli": "0.45.3",
|
|
380
|
-
"@modelcontextprotocol/sdk": "1.
|
|
380
|
+
"@modelcontextprotocol/sdk": "1.32.0",
|
|
381
381
|
"@types/picomatch": "^4.0.3",
|
|
382
382
|
"@types/spdx-expression-parse": "4.0.0",
|
|
383
383
|
"agent-blackboard": "^0.6.1",
|