vouchington-tooling 0.41.0 → 0.43.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/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/gh-cli/pr-create.d.mts +2 -0
- package/dist/gh-cli/pr-create.mjs +5 -1
- package/dist/mcp-server/context.d.mts +22 -2
- package/dist/mcp-server/context.mjs +14 -1
- package/dist/mcp-server/dispatch.d.mts +11 -0
- package/dist/mcp-server/dispatch.mjs +12 -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-pr.d.mts +4 -0
- package/dist/mcp-server/handlers-pr.mjs +143 -0
- 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 +10 -1
- package/dist/mcp-server/harness.test-helpers.mjs +4 -1
- 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/pr-body-file.d.mts +4 -0
- package/dist/mcp-server/pr-body-file.mjs +22 -0
- package/dist/mcp-server/pr-body.d.mts +20 -0
- package/dist/mcp-server/pr-body.mjs +47 -0
- package/dist/mcp-server/pr-defaults.d.mts +16 -0
- package/dist/mcp-server/pr-defaults.mjs +35 -0
- package/dist/mcp-server/pr-gh.d.mts +16 -0
- package/dist/mcp-server/pr-gh.mjs +56 -0
- package/dist/mcp-server/pr-input.d.mts +26 -0
- package/dist/mcp-server/pr-input.mjs +62 -0
- package/dist/mcp-server/pr-journal.d.mts +12 -0
- package/dist/mcp-server/pr-journal.mjs +86 -0
- package/dist/mcp-server/pr-live.d.mts +24 -0
- package/dist/mcp-server/pr-live.mjs +49 -0
- package/dist/mcp-server/pr-tools.test-helpers.d.mts +54 -0
- package/dist/mcp-server/pr-tools.test-helpers.mjs +67 -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-pr.d.mts +2 -0
- package/dist/mcp-server/tools-pr.mjs +69 -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 +6 -70
- package/dist/mcp-server/worktree.d.mts +11 -4
- package/dist/mcp-server/worktree.mjs +31 -6
- package/docs/mcp-server.md +164 -16
- package/package.json +1 -1
- package/skills/pr-description/SKILL.md +8 -0
- package/skills/retrospective/SKILL.md +5 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const KEYS = [
|
|
2
|
+
'date',
|
|
3
|
+
'issues',
|
|
4
|
+
'prs',
|
|
5
|
+
'session_id',
|
|
6
|
+
'description',
|
|
7
|
+
'work_outcome',
|
|
8
|
+
'feedback_coverage',
|
|
9
|
+
'repositories',
|
|
10
|
+
];
|
|
11
|
+
const isString = (value) => typeof value === 'string';
|
|
12
|
+
const isStrings = (value) => Array.isArray(value) && value.every(isString);
|
|
13
|
+
const isReference = (value) => (isString(value) && value.trim() !== '') || (Number.isSafeInteger(value) && value > 0);
|
|
14
|
+
function fail(message) {
|
|
15
|
+
throw new Error(`retrospective front matter: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
function parseLines(block) {
|
|
18
|
+
const values = new Map();
|
|
19
|
+
// composeRetrospective joins its sections with blank lines, so blank lines are allowed.
|
|
20
|
+
for (const line of block.split('\n').filter((candidate) => candidate !== '')) {
|
|
21
|
+
const separator = line.indexOf(': ');
|
|
22
|
+
const key = separator < 0 ? '' : line.slice(0, separator);
|
|
23
|
+
if (!KEYS.includes(key))
|
|
24
|
+
fail(separator < 0 || !/^[a-z_]+$/.test(key)
|
|
25
|
+
? `malformed line ${JSON.stringify(line)}; each line is "key: <JSON value>"`
|
|
26
|
+
: `unknown key ${key}`);
|
|
27
|
+
if (values.has(key))
|
|
28
|
+
fail(`duplicate key ${key}`);
|
|
29
|
+
try {
|
|
30
|
+
values.set(key, JSON.parse(line.slice(separator + 2)));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
fail(`${key} is not a JSON value`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return values;
|
|
37
|
+
}
|
|
38
|
+
function coverageOf(coverage) {
|
|
39
|
+
if (typeof coverage !== 'object' ||
|
|
40
|
+
coverage === null ||
|
|
41
|
+
Object.keys(coverage).some((key) => !['status', 'sources', 'droppedCount'].includes(key)) ||
|
|
42
|
+
!isString(coverage.status) ||
|
|
43
|
+
!isStrings(coverage.sources) ||
|
|
44
|
+
!Number.isSafeInteger(coverage.droppedCount))
|
|
45
|
+
fail('feedback_coverage must be { status, sources, droppedCount }');
|
|
46
|
+
return coverage;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parses the block from the leading `---` line to the next `---` line. Every line is
|
|
50
|
+
* `key: <JSON value>`; unknown keys, duplicates, malformed lines and ill-typed values are errors,
|
|
51
|
+
* and `date`, `issues`, and `prs` are required.
|
|
52
|
+
*/
|
|
53
|
+
export function parseRetrospectiveFrontMatter(markdown) {
|
|
54
|
+
const block = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(markdown.replaceAll('\r\n', '\n'))?.[1];
|
|
55
|
+
if (block === undefined)
|
|
56
|
+
fail('the document must start with a block between two "---" lines, as composeRetrospective writes');
|
|
57
|
+
const values = parseLines(block);
|
|
58
|
+
const { date, issues, prs } = Object.fromEntries(values);
|
|
59
|
+
if (!isString(date) || !/^\d{4}-\d{2}-\d{2}$/.test(date))
|
|
60
|
+
fail('date must be a YYYY-MM-DD string');
|
|
61
|
+
for (const [name, list] of [
|
|
62
|
+
['issues', issues],
|
|
63
|
+
['prs', prs],
|
|
64
|
+
])
|
|
65
|
+
if (!Array.isArray(list) || !list.every(isReference))
|
|
66
|
+
fail(`${name} must be an array of positive integers or non-empty strings`);
|
|
67
|
+
const text = (key) => {
|
|
68
|
+
const value = values.get(key);
|
|
69
|
+
if (value !== undefined && !isString(value))
|
|
70
|
+
fail(`${key} must be a string`);
|
|
71
|
+
return value;
|
|
72
|
+
};
|
|
73
|
+
const repositories = values.get('repositories');
|
|
74
|
+
if (repositories !== undefined && !isStrings(repositories))
|
|
75
|
+
fail('repositories must be an array of strings');
|
|
76
|
+
const coverage = values.get('feedback_coverage');
|
|
77
|
+
const parsed = {
|
|
78
|
+
date,
|
|
79
|
+
issues: issues,
|
|
80
|
+
prs: prs,
|
|
81
|
+
sessionId: text('session_id'),
|
|
82
|
+
description: text('description'),
|
|
83
|
+
workOutcome: text('work_outcome'),
|
|
84
|
+
feedbackCoverage: coverage === undefined ? undefined : coverageOf(coverage),
|
|
85
|
+
repositories: repositories,
|
|
86
|
+
};
|
|
87
|
+
return Object.fromEntries(Object.entries(parsed).filter(([, value]) => value !== undefined));
|
|
88
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type FeedbackCoverage, type WorkOutcome } from '../agent-blackboard/index.mts';
|
|
2
|
+
import type { BlackboardClientDependencies } from '../agent-blackboard/client.mts';
|
|
3
|
+
import { type Args } from './args.mts';
|
|
4
|
+
/** The longest a facts subprocess may run before it is killed. */
|
|
5
|
+
export declare const FACTS_TIMEOUT_MS = 120000;
|
|
6
|
+
type ComposeBase = {
|
|
7
|
+
sessionId: string;
|
|
8
|
+
repositories: string[];
|
|
9
|
+
workOutcome: WorkOutcome;
|
|
10
|
+
feedbackCoverage: FeedbackCoverage;
|
|
11
|
+
worktree: string;
|
|
12
|
+
env: NodeJS.ProcessEnv;
|
|
13
|
+
dependencies: BlackboardClientDependencies;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Facts commands run in the worktree and never see the blackboard credentials, NODE_OPTIONS, or
|
|
17
|
+
* GIT_* overrides that could point git away from the validated worktree.
|
|
18
|
+
*/
|
|
19
|
+
export declare function worktreeExecutor(worktree: string, env: NodeJS.ProcessEnv, timeoutMs?: number): (command: string, args: string[]) => Promise<{
|
|
20
|
+
ok: boolean;
|
|
21
|
+
stdout: string;
|
|
22
|
+
stderr: string;
|
|
23
|
+
exitCode: number | null;
|
|
24
|
+
}>;
|
|
25
|
+
/** The caller's `compose.knownSensitiveValues` plus the server's blackboard token, when set. */
|
|
26
|
+
export declare function sensitiveValues(compose: Args | undefined, env: NodeJS.ProcessEnv): string[];
|
|
27
|
+
/** Builds the retrospective markdown with `composeRetrospective`, rejecting unknown input keys. */
|
|
28
|
+
export declare function composeFromArgs(compose: Args, base: ComposeBase): Promise<string>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { composeRetrospective, } from "../agent-blackboard/index.mjs";
|
|
2
|
+
import { rejectUnknown } from "./args.mjs";
|
|
3
|
+
import { inspectHarnessEnvironment, selectHarnessSession, } from "../agent-harness-identity/index.mjs";
|
|
4
|
+
import { SESSION_ID } from "../retrospective-transcript/resolve-transcript-file.mjs";
|
|
5
|
+
import { serverJournalLoader } from "./journal-read.mjs";
|
|
6
|
+
import { checkFactsInput } from "./retrospective-facts-input.mjs";
|
|
7
|
+
import { spawnPolicy } from "./policy-spawn.mjs";
|
|
8
|
+
const HARNESSES = ['codex', 'claude', 'cursor', 'grok'];
|
|
9
|
+
const COMPOSE_KEYS = [
|
|
10
|
+
'narrative',
|
|
11
|
+
'date',
|
|
12
|
+
'issues',
|
|
13
|
+
'prs',
|
|
14
|
+
'description',
|
|
15
|
+
'facts',
|
|
16
|
+
'transcript',
|
|
17
|
+
'tools',
|
|
18
|
+
'architecture',
|
|
19
|
+
'knownSensitiveValues',
|
|
20
|
+
];
|
|
21
|
+
const COMPOSE_REQUIRED = COMPOSE_KEYS.filter((key) => key !== 'knownSensitiveValues');
|
|
22
|
+
/** No `repo`: a caller must not aim the server's GitHub token at another repository. */
|
|
23
|
+
const FACTS_KEYS = ['pr', 'branch', 'noPr', 'status', 'reason'];
|
|
24
|
+
/** No path overrides: the server reads the session's transcript from its own default roots. */
|
|
25
|
+
const TRANSCRIPT_KEYS = ['sessionId', 'harnessSessionId', 'status', 'reason'];
|
|
26
|
+
/** The longest a facts subprocess may run before it is killed. */
|
|
27
|
+
export const FACTS_TIMEOUT_MS = 120_000;
|
|
28
|
+
const MAX_OUTPUT_BYTES = 10_000_000;
|
|
29
|
+
function section(compose, name, keys) {
|
|
30
|
+
const value = compose[name];
|
|
31
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
32
|
+
throw new Error(`compose.${name} must be an object`);
|
|
33
|
+
rejectUnknown(value, keys);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Facts commands run in the worktree and never see the blackboard credentials, NODE_OPTIONS, or
|
|
38
|
+
* GIT_* overrides that could point git away from the validated worktree.
|
|
39
|
+
*/
|
|
40
|
+
export function worktreeExecutor(worktree, env, timeoutMs = FACTS_TIMEOUT_MS) {
|
|
41
|
+
const safe = Object.fromEntries(Object.entries(env).filter(([name]) => !name.startsWith('AGENT_BLACKBOARD_') &&
|
|
42
|
+
!name.startsWith('GIT_') &&
|
|
43
|
+
name !== 'NODE_OPTIONS'));
|
|
44
|
+
// Each command runs in its own process group, killed whole when it exits or times out, so a
|
|
45
|
+
// helper that git or gh starts cannot outlive the call with an inherited GH_TOKEN.
|
|
46
|
+
return async (command, args) => {
|
|
47
|
+
let outcome;
|
|
48
|
+
try {
|
|
49
|
+
outcome = await spawnPolicy({
|
|
50
|
+
command,
|
|
51
|
+
args,
|
|
52
|
+
cwd: worktree,
|
|
53
|
+
env: safe,
|
|
54
|
+
input: '',
|
|
55
|
+
timeoutMs,
|
|
56
|
+
maxOutputBytes: MAX_OUTPUT_BYTES,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return { ok: false, stdout: '', stderr: error.message, exitCode: null };
|
|
61
|
+
}
|
|
62
|
+
const { stdout, stderr, exitCode, timedOut, overflowed } = outcome;
|
|
63
|
+
if (timedOut || overflowed) {
|
|
64
|
+
const reason = timedOut
|
|
65
|
+
? `timed out after ${timeoutMs} ms`
|
|
66
|
+
: `wrote more than ${MAX_OUTPUT_BYTES} bytes`;
|
|
67
|
+
return { ok: false, stdout, stderr: `${stderr}${command} ${reason}`, exitCode: null };
|
|
68
|
+
}
|
|
69
|
+
return { ok: exitCode === 0, stdout, stderr, exitCode };
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** The caller's `compose.knownSensitiveValues` plus the server's blackboard token, when set. */
|
|
73
|
+
export function sensitiveValues(compose, env) {
|
|
74
|
+
const given = compose?.knownSensitiveValues;
|
|
75
|
+
if (given !== undefined && (!Array.isArray(given) || given.some((v) => typeof v !== 'string')))
|
|
76
|
+
throw new Error('compose.knownSensitiveValues must be an array of strings');
|
|
77
|
+
// Redaction ignores values under four characters, so promising to redact one would be false.
|
|
78
|
+
if (given?.some((value) => value.length < 4))
|
|
79
|
+
throw new Error('compose.knownSensitiveValues entries must be at least 4 characters');
|
|
80
|
+
// Facts commands inherit the GitHub tokens, so their output is redacted of those values too.
|
|
81
|
+
const credentials = [env.AGENT_BLACKBOARD_TOKEN, env.GH_TOKEN, env.GITHUB_TOKEN].filter((value) => Boolean(value));
|
|
82
|
+
const values = [...(given ?? []), ...credentials];
|
|
83
|
+
// The composer JSON-escapes structured fields, so each value is also redacted in that form.
|
|
84
|
+
const escaped = values.map((value) => JSON.stringify(value).slice(1, -1));
|
|
85
|
+
// Longest first, so a shorter value that is a prefix of a credential cannot break its match.
|
|
86
|
+
return [...new Set([...values, ...escaped])].sort((a, b) => b.length - a.length);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The transcript collector input: the server's worktree and default session roots only. A caller
|
|
90
|
+
* may name the session (it must be this one), give the harness's own UUID session id when the
|
|
91
|
+
* blackboard session id is not one, or report the transcript as unassessed.
|
|
92
|
+
*/
|
|
93
|
+
function transcriptInput(transcript, base) {
|
|
94
|
+
if ('status' in transcript) {
|
|
95
|
+
if ('sessionId' in transcript || 'harnessSessionId' in transcript)
|
|
96
|
+
throw new Error('compose.transcript: sessionId and harnessSessionId are exclusive with status');
|
|
97
|
+
return transcript;
|
|
98
|
+
}
|
|
99
|
+
// A reason without a status is malformed, not a request to read the transcript.
|
|
100
|
+
if ('reason' in transcript)
|
|
101
|
+
throw new Error('compose.transcript.reason requires a status');
|
|
102
|
+
if (transcript.sessionId !== undefined && transcript.sessionId !== base.sessionId)
|
|
103
|
+
throw new Error('compose.transcript.sessionId must equal the sessionId of the call');
|
|
104
|
+
const harnessId = transcript.harnessSessionId ?? base.sessionId;
|
|
105
|
+
// Transcripts are found by the harness's UUID; never let a lookup fail into "unavailable".
|
|
106
|
+
if (typeof harnessId !== 'string' || !SESSION_ID.test(harnessId))
|
|
107
|
+
throw new Error('compose.transcript needs a UUID session id: pass harnessSessionId (the harness session ' +
|
|
108
|
+
'UUID) or { status, reason }');
|
|
109
|
+
// When the harness that launched the server names its session, no other transcript is read.
|
|
110
|
+
const own = selectHarnessSession(inspectHarnessEnvironment(base.env), HARNESSES)?.sessionId;
|
|
111
|
+
if (own !== undefined && own.toLowerCase() !== harnessId.toLowerCase())
|
|
112
|
+
// A server can outlive its session (Claude Code /clear, a Codex thread switch).
|
|
113
|
+
throw new Error('compose.transcript may read only the harness session that launched the server: if the ' +
|
|
114
|
+
'session changed since (/clear, a thread switch), reconnect the server (/mcp in Claude ' +
|
|
115
|
+
'Code, or restart Codex), or pass { status, reason }');
|
|
116
|
+
return { sessionId: harnessId, cwd: base.worktree };
|
|
117
|
+
}
|
|
118
|
+
function factsInput(facts, base) {
|
|
119
|
+
return checkFactsInput(facts)
|
|
120
|
+
? { ...facts, execute: worktreeExecutor(base.worktree, base.env) }
|
|
121
|
+
: facts;
|
|
122
|
+
}
|
|
123
|
+
/** Builds the retrospective markdown with `composeRetrospective`, rejecting unknown input keys. */
|
|
124
|
+
export async function composeFromArgs(compose, base) {
|
|
125
|
+
rejectUnknown(compose, COMPOSE_KEYS);
|
|
126
|
+
const missing = COMPOSE_REQUIRED.filter((key) => compose[key] === undefined);
|
|
127
|
+
if (missing.length)
|
|
128
|
+
throw new Error(`compose is missing: ${missing.join(', ')}`);
|
|
129
|
+
if (typeof compose.narrative !== 'string' || !compose.narrative.trim())
|
|
130
|
+
throw new Error('compose.narrative must be a non-empty string');
|
|
131
|
+
const facts = section(compose, 'facts', FACTS_KEYS);
|
|
132
|
+
const transcript = section(compose, 'transcript', TRANSCRIPT_KEYS);
|
|
133
|
+
const input = {
|
|
134
|
+
...compose,
|
|
135
|
+
sessionId: base.sessionId,
|
|
136
|
+
repositories: base.repositories,
|
|
137
|
+
workOutcome: base.workOutcome,
|
|
138
|
+
feedbackCoverage: base.feedbackCoverage,
|
|
139
|
+
facts: factsInput(facts, base),
|
|
140
|
+
transcript: transcriptInput(transcript, base),
|
|
141
|
+
knownSensitiveValues: sensitiveValues(compose, base.env),
|
|
142
|
+
journal: { journalLoader: serverJournalLoader(base.env, base.dependencies) },
|
|
143
|
+
};
|
|
144
|
+
return composeRetrospective(input);
|
|
145
|
+
}
|
|
@@ -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,69 @@
|
|
|
1
|
+
import { ADDITIVE, MUTATING, READ_ONLY, tool, } from "./tools-schema.mjs";
|
|
2
|
+
/** These tools read or write GitHub, outside the server's own state. */
|
|
3
|
+
const openWorld = (annotations) => ({
|
|
4
|
+
...annotations,
|
|
5
|
+
openWorldHint: true,
|
|
6
|
+
});
|
|
7
|
+
const text = (description) => ({ type: 'string', minLength: 1, description });
|
|
8
|
+
const PR_NUMBER = { type: 'integer', minimum: 1, description: 'Pull request number.' };
|
|
9
|
+
const FIELDS = {
|
|
10
|
+
title: { type: 'string', minLength: 1, maxLength: 256, description: 'Single-line PR title.' },
|
|
11
|
+
summary: text('What the change does, as markdown.'),
|
|
12
|
+
impact: text('Who or what is affected, as markdown.'),
|
|
13
|
+
relatedIssues: {
|
|
14
|
+
type: 'array',
|
|
15
|
+
description: 'Issues this pull request closes or refs; may be empty.',
|
|
16
|
+
items: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
number: { type: 'integer', minimum: 1 },
|
|
20
|
+
relation: { enum: ['closes', 'refs'] },
|
|
21
|
+
reason: text('Why the issue is related.'),
|
|
22
|
+
},
|
|
23
|
+
required: ['number', 'relation', 'reason'],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
details: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
minItems: 1,
|
|
30
|
+
items: { type: 'string', minLength: 1 },
|
|
31
|
+
description: 'Markdown paragraphs or bullets, joined by blank lines.',
|
|
32
|
+
},
|
|
33
|
+
base: { type: 'string', minLength: 1, description: 'Base branch. Default: main.' },
|
|
34
|
+
agent: text('Agent name for the Agent line.'),
|
|
35
|
+
model: text('Optional model name, rendered in parentheses after the agent.'),
|
|
36
|
+
mode: { enum: ['interactive', 'autonomous'], description: 'Delivery mode, given to the policy.' },
|
|
37
|
+
workspaceSetup: text('Text for the Workspace setup line, such as how the worktree was prepared.'),
|
|
38
|
+
};
|
|
39
|
+
const REQUIRED = [
|
|
40
|
+
'title',
|
|
41
|
+
'summary',
|
|
42
|
+
'impact',
|
|
43
|
+
'relatedIssues',
|
|
44
|
+
'details',
|
|
45
|
+
'agent',
|
|
46
|
+
'mode',
|
|
47
|
+
'workspaceSetup',
|
|
48
|
+
];
|
|
49
|
+
const RENDERING = 'The server renders the body from the structured input (Summary, Impact, Related issues, ' +
|
|
50
|
+
'Details, then Workspace setup, Agent, Device and Worktree lines) and runs the checkout ' +
|
|
51
|
+
'policy validatePullRequest export in a child process; the head branch is the ' +
|
|
52
|
+
"worktree's current branch. Policy tools need a server launched inside a repository worktree. ";
|
|
53
|
+
export const PR_TOOLS = [
|
|
54
|
+
tool('pr_validate', 'Renders the pull request body and validates it with the checkout policy; writes nothing. ' +
|
|
55
|
+
RENDERING +
|
|
56
|
+
'Pass pr to validate an update of that pull request: the live body is read and its Shepherd ' +
|
|
57
|
+
'Journal preserved. Returns { body, errors, warnings }.', openWorld(READ_ONLY), { ...FIELDS, pr: PR_NUMBER }, REQUIRED),
|
|
58
|
+
tool('pr_create', 'Creates a draft pull request from the worktree branch. ' +
|
|
59
|
+
RENDERING +
|
|
60
|
+
'Validation errors block creation. The policy pullRequestDefaults export may add labels, a ' +
|
|
61
|
+
'milestone and a project. The branch must already be pushed and current. It sees no stack ' +
|
|
62
|
+
'membership, so a policy may reject a non-default base; create stacked layers with the ' +
|
|
63
|
+
"repository's stack tooling. Returns { number, url, warnings }.", openWorld(ADDITIVE), FIELDS, REQUIRED),
|
|
64
|
+
tool('pr_update', "Replaces an open pull request's title and body. " +
|
|
65
|
+
RENDERING +
|
|
66
|
+
"The pull request's head branch must be the worktree branch. The live Shepherd Journal is " +
|
|
67
|
+
'preserved and nothing else is carried over from the live body. Validation errors block ' +
|
|
68
|
+
'the update. Returns { number, url, warnings }.', openWorld(MUTATING), { ...FIELDS, pr: PR_NUMBER }, [...REQUIRED, 'pr']),
|
|
69
|
+
];
|
|
@@ -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 {};
|