vouchington-tooling 0.41.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/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 +2 -1
- 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/docs/mcp-server.md +90 -16
- package/package.json +1 -1
- package/skills/retrospective/SKILL.md +5 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Runs one export of a consumer's policy module in this fresh process, so the MCP server never
|
|
2
|
+
// imports checkout code. argv[2] is the real path of the module. stdin is one JSON object,
|
|
3
|
+
// { policyVersion: 1, export, input }. stdout is one JSON object, { ok: true, result },
|
|
4
|
+
// { ok: true, skipped: true } when the module has no such export, or { ok: false, error }.
|
|
5
|
+
// Console output and direct stdout writes from the policy go to stderr so they cannot corrupt the
|
|
6
|
+
// reply.
|
|
7
|
+
import { readFileSync } from 'node:fs'
|
|
8
|
+
import { pathToFileURL } from 'node:url'
|
|
9
|
+
|
|
10
|
+
const writeReply = process.stdout.write.bind(process.stdout)
|
|
11
|
+
process.stdout.write = process.stderr.write.bind(process.stderr)
|
|
12
|
+
console.log = console.info = console.debug = console.error
|
|
13
|
+
|
|
14
|
+
async function run() {
|
|
15
|
+
const request = JSON.parse(readFileSync(0, 'utf8'))
|
|
16
|
+
if (request.policyVersion !== 1) throw new Error('unsupported policyVersion')
|
|
17
|
+
const policy = await import(pathToFileURL(process.argv[2]).href)
|
|
18
|
+
if (typeof policy[request.export] !== 'function') return { ok: true, skipped: true }
|
|
19
|
+
return { ok: true, result: await policy[request.export](request.input) }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const reply = await run().catch((error) => ({
|
|
23
|
+
ok: false,
|
|
24
|
+
error: String((error && error.stack) || error),
|
|
25
|
+
}))
|
|
26
|
+
writeReply(JSON.stringify(reply), () => process.exit(0))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { normalizeRepositories } from './feedback-fields.mts';
|
|
2
2
|
import type { FeedbackEnvelope } from './feedback-types.mts';
|
|
3
|
+
export declare const FEEDBACK_MARKDOWN_MAX_BYTES = 12000;
|
|
3
4
|
export declare function validateFeedbackEnvelope(value: unknown): asserts value is FeedbackEnvelope;
|
|
4
5
|
export declare function redactFeedbackText(value: string, knownSensitiveValues?: string[]): string;
|
|
5
6
|
/** Bytes by which the envelope exceeds its markdown or whole-envelope limit; 0 when it fits. */
|
|
@@ -2,7 +2,7 @@ import { normalizeRepositories, validateFeedbackText, validateFeedbackReferences
|
|
|
2
2
|
export { normalizeRepositories } from "./feedback-fields.mjs";
|
|
3
3
|
import { isCount, isObject } from "./snapshot-partition-guards.mjs";
|
|
4
4
|
const FEEDBACK_MAX_BYTES = 16_384;
|
|
5
|
-
const FEEDBACK_MARKDOWN_MAX_BYTES = 12_000;
|
|
5
|
+
export const FEEDBACK_MARKDOWN_MAX_BYTES = 12_000;
|
|
6
6
|
const OUTCOMES = [
|
|
7
7
|
'in-progress',
|
|
8
8
|
'success',
|
package/dist/cli/usage.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n require-up-to-date Require HEAD to include a fetched remote branch\n gitleaks-directory-scan Scan a directory with Gitleaks\n ast-grep-examples Run AST-grep rule examples\n ast-grep-pack Print shipped unconditional AST-grep pack paths as JSON\n gha-workspace-policy Check GitHub Actions workspace safety policy\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n gha-collaborator-trust Classify authors as write-access collaborators or not\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n mcp Serve the agent-blackboard journal tools over stdio (MCP)\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n [--median-threshold-floor <seconds>] Off unless set. Must be below the ceiling\n [--median-threshold-ceiling <seconds>] Default 360. Flags a five-sample median above this\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\nrequire-up-to-date --remote <name> --branch <name>\ngitleaks-directory-scan --config <path> [--directory <path>]\nast-grep-examples --rules <directory> --config <path>\nast-grep-pack\ngha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--action-directory <directory>]\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name>... | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports <count> [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\ngha-collaborator-trust <owner/repo> [--allow-bot <login>]... < authors.json\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard journal append --mode interactive|autonomous --source-event-id ID --work-outcome OUTCOME --coverage-status STATUS --session-id UUID --agent NAME --version VERSION --file PATH --repository OWNER/NAME [--repository OWNER/NAME ...] [--parent-session-id UUID] [--timestamp ISO8601] [--coverage-source SOURCE,...] [--dropped-count N] [--outbox-directory PATH]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard journal status --session-id UUID --outbox-directory PATH\nagent-blackboard journal flush --outbox-directory PATH [--session-id UUID]\nInteractive journal append requires --outbox-directory PATH.\nagent-blackboard journal is deprecated and being phased out: agents journal through the mcp server.\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\nmcp\n (no args) Serve journal_append, journal_entries, outbox_status, outbox_flush,\n session_ensure, snapshot_export, and session_archive over stdio.\n Needs the optional @modelcontextprotocol/sdk peer (and zod).\n Launch it from inside the repository's git worktree.\n";
|
|
1
|
+
export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n require-up-to-date Require HEAD to include a fetched remote branch\n gitleaks-directory-scan Scan a directory with Gitleaks\n ast-grep-examples Run AST-grep rule examples\n ast-grep-pack Print shipped unconditional AST-grep pack paths as JSON\n gha-workspace-policy Check GitHub Actions workspace safety policy\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n gha-collaborator-trust Classify authors as write-access collaborators or not\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n mcp Serve the agent-blackboard journal tools over stdio (MCP)\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n [--median-threshold-floor <seconds>] Off unless set. Must be below the ceiling\n [--median-threshold-ceiling <seconds>] Default 360. Flags a five-sample median above this\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\nrequire-up-to-date --remote <name> --branch <name>\ngitleaks-directory-scan --config <path> [--directory <path>]\nast-grep-examples --rules <directory> --config <path>\nast-grep-pack\ngha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--action-directory <directory>]\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name>... | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports <count> [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\ngha-collaborator-trust <owner/repo> [--allow-bot <login>]... < authors.json\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard journal append --mode interactive|autonomous --source-event-id ID --work-outcome OUTCOME --coverage-status STATUS --session-id UUID --agent NAME --version VERSION --file PATH --repository OWNER/NAME [--repository OWNER/NAME ...] [--parent-session-id UUID] [--timestamp ISO8601] [--coverage-source SOURCE,...] [--dropped-count N] [--outbox-directory PATH]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard journal status --session-id UUID --outbox-directory PATH\nagent-blackboard journal flush --outbox-directory PATH [--session-id UUID]\nInteractive journal append requires --outbox-directory PATH.\nagent-blackboard journal is deprecated and being phased out: agents journal through the mcp server.\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\nmcp\n (no args) Serve journal_append, journal_entries, outbox_status, outbox_flush,\n session_ensure, retrospective_check, retrospective_save,\n snapshot_export, and session_archive over stdio.\n Needs the optional @modelcontextprotocol/sdk peer (and zod).\n Launch it from inside the repository's git worktree.\n";
|
|
2
2
|
export declare function printUsage(stream?: NodeJS.WritableStream): void;
|
|
3
3
|
export declare function commandNames(usage?: string): string[];
|
|
4
4
|
export declare function commandUsage(command: string, usage?: string): string;
|
package/dist/cli/usage.mjs
CHANGED
|
@@ -126,7 +126,8 @@ agent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '
|
|
|
126
126
|
agent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]
|
|
127
127
|
mcp
|
|
128
128
|
(no args) Serve journal_append, journal_entries, outbox_status, outbox_flush,
|
|
129
|
-
session_ensure,
|
|
129
|
+
session_ensure, retrospective_check, retrospective_save,
|
|
130
|
+
snapshot_export, and session_archive over stdio.
|
|
130
131
|
Needs the optional @modelcontextprotocol/sdk peer (and zod).
|
|
131
132
|
Launch it from inside the repository's git worktree.
|
|
132
133
|
`;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { BlackboardClientDependencies } from '../agent-blackboard/client.mts';
|
|
2
2
|
import type { FeedbackIdentity } from '../agent-blackboard/feedback-types.mts';
|
|
3
|
+
import type { PolicySpawner } from './policy-spawn.mts';
|
|
3
4
|
import { type Args } from './args.mts';
|
|
4
5
|
/** What a handler may rely on: every field here was validated before the handler ran. */
|
|
5
|
-
type ToolContext = {
|
|
6
|
+
export type ToolContext = {
|
|
6
7
|
sessionId: string;
|
|
7
8
|
worktree: string;
|
|
8
9
|
env: NodeJS.ProcessEnv;
|
|
9
10
|
dependencies: BlackboardClientDependencies;
|
|
11
|
+
/** The launch directory's worktree top level, or undefined when it is not inside one. */
|
|
12
|
+
launchRoot: string | undefined;
|
|
13
|
+
/** Test seam: replaces the child process a policy export runs in. */
|
|
14
|
+
spawnPolicy?: PolicySpawner;
|
|
10
15
|
};
|
|
11
16
|
export type ToolHandler = (args: Args, context: ToolContext) => Promise<Record<string, unknown>>;
|
|
12
17
|
/**
|
|
@@ -15,5 +20,9 @@ export type ToolHandler = (args: Args, context: ToolContext) => Promise<Record<s
|
|
|
15
20
|
*/
|
|
16
21
|
export declare function outboxDirectory(worktree: string): string;
|
|
17
22
|
export declare function readSessionId(args: Args): string;
|
|
23
|
+
/** The caller identity, fully validated before any policy runs or anything is written. */
|
|
18
24
|
export declare function readIdentity(args: Args, sessionId: string): FeedbackIdentity;
|
|
19
|
-
|
|
25
|
+
/** Policy modules run only for a server whose launch repository bounds the worktrees it acts on. */
|
|
26
|
+
export declare function assertPolicyAllowed(context: {
|
|
27
|
+
launchRoot: string | undefined;
|
|
28
|
+
}): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { assertSessionId } from "../agent-blackboard/session-id.mjs";
|
|
3
|
+
import { validateFeedbackIdentity } from "../agent-blackboard/feedback-identity.mjs";
|
|
3
4
|
import { requiredNullableString, requiredString } from "./args.mjs";
|
|
4
5
|
/**
|
|
5
6
|
* The consumer convention for the interactive outbox. It is derived, never accepted from the
|
|
@@ -13,11 +14,23 @@ export function readSessionId(args) {
|
|
|
13
14
|
assertSessionId(value);
|
|
14
15
|
return value;
|
|
15
16
|
}
|
|
17
|
+
/** The caller identity, fully validated before any policy runs or anything is written. */
|
|
16
18
|
export function readIdentity(args, sessionId) {
|
|
17
|
-
|
|
19
|
+
const identity = {
|
|
18
20
|
sessionId,
|
|
19
21
|
parentSessionId: requiredNullableString(args, 'parentSessionId'),
|
|
20
22
|
agent: requiredString(args, 'agent'),
|
|
21
23
|
version: requiredString(args, 'version'),
|
|
22
24
|
};
|
|
25
|
+
validateFeedbackIdentity(identity);
|
|
26
|
+
return identity;
|
|
27
|
+
}
|
|
28
|
+
/** Policy modules run only for a server whose launch repository bounds the worktrees it acts on. */
|
|
29
|
+
export function assertPolicyAllowed(context) {
|
|
30
|
+
// Policy and facts children are reaped by Unix process group; Windows has no equivalent here.
|
|
31
|
+
if (process.platform === 'win32')
|
|
32
|
+
throw new Error('policy tools are not supported on Windows: run the server on macOS or Linux');
|
|
33
|
+
if (context.launchRoot === undefined)
|
|
34
|
+
throw new Error('policy tools require the server to be launched inside a repository worktree: relaunch ' +
|
|
35
|
+
'it from the repository so the worktree argument is bound to that repository');
|
|
23
36
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BlackboardClientDependencies } from '../agent-blackboard/client.mts';
|
|
2
2
|
import type { RunTextCommand } from '../gh-cli/exec.mts';
|
|
3
|
+
import type { PolicySpawner } from './policy-spawn.mts';
|
|
3
4
|
export type ServerEnvironment = {
|
|
4
5
|
/** The launch directory's worktree top level, or undefined when it is not inside one. */
|
|
5
6
|
launchRoot: string | undefined;
|
|
@@ -9,6 +10,8 @@ export type ServerEnvironment = {
|
|
|
9
10
|
resolveFrom?: string | URL;
|
|
10
11
|
/** Test seam: replaces how the consumer's `agent-blackboard` client is loaded. */
|
|
11
12
|
blackboard?: BlackboardClientDependencies;
|
|
13
|
+
/** Test seam: replaces the child process a policy export runs in. */
|
|
14
|
+
spawnPolicy?: PolicySpawner;
|
|
12
15
|
};
|
|
13
16
|
export type ToolCallResult = {
|
|
14
17
|
content: Array<{
|
|
@@ -2,6 +2,7 @@ import { FeedbackDeliveryError } from "../agent-blackboard/index.mjs";
|
|
|
2
2
|
import { optionalString, rejectUnknown, requireObject } from "./args.mjs";
|
|
3
3
|
import { readSessionId } from "./context.mjs";
|
|
4
4
|
import { journalAppend, journalEntries } from "./handlers-journal.mjs";
|
|
5
|
+
import { retrospectiveCheck, retrospectiveSave } from "./handlers-retrospective.mjs";
|
|
5
6
|
import { outboxFlush, outboxStatus } from "./handlers-outbox.mjs";
|
|
6
7
|
import { sessionArchive, sessionEnsure, snapshotExport } from "./handlers-session.mjs";
|
|
7
8
|
import { TOOLS } from "./tools.mjs";
|
|
@@ -12,6 +13,8 @@ const HANDLERS = {
|
|
|
12
13
|
outbox_status: outboxStatus,
|
|
13
14
|
outbox_flush: outboxFlush,
|
|
14
15
|
session_ensure: sessionEnsure,
|
|
16
|
+
retrospective_check: retrospectiveCheck,
|
|
17
|
+
retrospective_save: retrospectiveSave,
|
|
15
18
|
snapshot_export: snapshotExport,
|
|
16
19
|
session_archive: sessionArchive,
|
|
17
20
|
};
|
|
@@ -51,6 +54,8 @@ export async function callTool(name, rawArguments, environment) {
|
|
|
51
54
|
sessionId,
|
|
52
55
|
worktree,
|
|
53
56
|
env: environment.env,
|
|
57
|
+
launchRoot: environment.launchRoot,
|
|
58
|
+
...(environment.spawnPolicy === undefined ? {} : { spawnPolicy: environment.spawnPolicy }),
|
|
54
59
|
// The client resolves from this package's install, like the SDK, never from the worktree.
|
|
55
60
|
dependencies: {
|
|
56
61
|
...environment.blackboard,
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type FeedbackCoverage, type FeedbackDeliveryResult, type WorkOutcome } from '../agent-blackboard/index.mts';
|
|
2
|
+
import { type Args } from './args.mts';
|
|
3
|
+
import { type ToolContext, type ToolHandler } from './context.mts';
|
|
4
|
+
export declare const MODES: readonly ["interactive", "autonomous"];
|
|
5
|
+
export declare const OUTCOMES: readonly WorkOutcome[];
|
|
6
|
+
export declare function readCoverage(args: Args): FeedbackCoverage;
|
|
2
7
|
export declare const journalAppend: ToolHandler;
|
|
8
|
+
/**
|
|
9
|
+
* The tool result for a write: the delivery result plus the server-owned timestamp (a first
|
|
10
|
+
* append takes the current time; a retry of the same event reports the one already stored or
|
|
11
|
+
* retained) and the outbox counts. Autonomous delivery never uses an outbox, so it has nothing
|
|
12
|
+
* pending; interactive delivery reports the outbox as it is after delivery.
|
|
13
|
+
*/
|
|
14
|
+
export declare function deliveryResponse(result: FeedbackDeliveryResult, context: ToolContext, mode: (typeof MODES)[number]): Record<string, unknown>;
|
|
15
|
+
export declare function byCreatedAt(left: unknown, right: unknown): number;
|
|
3
16
|
/** Every entry as the client returns it, oldest first. Nothing is filtered or rewritten. */
|
|
4
17
|
export declare const journalEntries: ToolHandler;
|
|
@@ -2,8 +2,8 @@ import { appendJournalMarkdown, readJournal, } from "../agent-blackboard/index.m
|
|
|
2
2
|
import { optionalRecord, optionalString, requiredChoice, requiredString, requiredStringArray, rejectUnknown, } from "./args.mjs";
|
|
3
3
|
import { outboxDirectory, readIdentity } from "./context.mjs";
|
|
4
4
|
import { outboxCounts } from "./handlers-outbox.mjs";
|
|
5
|
-
const MODES = ['interactive', 'autonomous'];
|
|
6
|
-
const OUTCOMES = [
|
|
5
|
+
export const MODES = ['interactive', 'autonomous'];
|
|
6
|
+
export const OUTCOMES = [
|
|
7
7
|
'in-progress',
|
|
8
8
|
'success',
|
|
9
9
|
'failure',
|
|
@@ -20,7 +20,7 @@ const COVERAGE_STATUSES = [
|
|
|
20
20
|
'not-assessed',
|
|
21
21
|
'not-started',
|
|
22
22
|
];
|
|
23
|
-
function readCoverage(args) {
|
|
23
|
+
export function readCoverage(args) {
|
|
24
24
|
const coverage = optionalRecord(args, 'feedbackCoverage');
|
|
25
25
|
if (coverage === undefined)
|
|
26
26
|
throw new Error('feedbackCoverage is required');
|
|
@@ -60,11 +60,16 @@ export const journalAppend = async (args, context) => {
|
|
|
60
60
|
env: context.env,
|
|
61
61
|
dependencies: context.dependencies,
|
|
62
62
|
});
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
return deliveryResponse(result, context, mode);
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* The tool result for a write: the delivery result plus the server-owned timestamp (a first
|
|
67
|
+
* append takes the current time; a retry of the same event reports the one already stored or
|
|
68
|
+
* retained) and the outbox counts. Autonomous delivery never uses an outbox, so it has nothing
|
|
69
|
+
* pending; interactive delivery reports the outbox as it is after delivery.
|
|
70
|
+
*/
|
|
71
|
+
export function deliveryResponse(result, context, mode) {
|
|
65
72
|
const timestamp = result.status === 'delivered' ? result.receipt.timestamp : result.timestamp;
|
|
66
|
-
// Autonomous delivery never uses an outbox, so it has nothing pending. Interactive delivery
|
|
67
|
-
// reports the outbox as it is after delivery, split into the caller's session and the worktree.
|
|
68
73
|
const counts = mode === 'interactive'
|
|
69
74
|
? outboxCounts(context.worktree, context.sessionId)
|
|
70
75
|
: { pendingCount: 0, worktreePendingCount: 0 };
|
|
@@ -77,14 +82,14 @@ export const journalAppend = async (args, context) => {
|
|
|
77
82
|
...(storedVersion === undefined ? {} : { storedVersion }),
|
|
78
83
|
...counts,
|
|
79
84
|
};
|
|
80
|
-
}
|
|
85
|
+
}
|
|
81
86
|
// `Entries.get` documents no order, and `createdAt` is a service-generated ISO 8601 UTC time whose
|
|
82
87
|
// text order is time order. An entry without a usable `createdAt` sorts first rather than throwing.
|
|
83
88
|
function createdAtOf(entry) {
|
|
84
89
|
const createdAt = entry?.createdAt;
|
|
85
90
|
return typeof createdAt === 'string' ? createdAt : '';
|
|
86
91
|
}
|
|
87
|
-
function byCreatedAt(left, right) {
|
|
92
|
+
export function byCreatedAt(left, right) {
|
|
88
93
|
const leftAt = createdAtOf(left);
|
|
89
94
|
const rightAt = createdAtOf(right);
|
|
90
95
|
if (leftAt === rightAt)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ToolHandler } from './context.mts';
|
|
2
|
+
/** Whether the session already holds a retrospective, and which (sourceEventId, issues, prs). */
|
|
3
|
+
export declare const retrospectiveCheck: ToolHandler;
|
|
4
|
+
/** The default event id, hashed when the session id would push it past the 256-byte limit. */
|
|
5
|
+
export declare function defaultSourceEventId(sessionId: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Saves the session's retrospective: composes it when `compose` is given, checks the arguments
|
|
8
|
+
* against the document's front matter, runs the checkout's `validateRetrospective` policy (in a
|
|
9
|
+
* child process) and writes nothing if it reports errors, then writes through the same verified
|
|
10
|
+
* path as `journal_append`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const retrospectiveSave: ToolHandler;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createFeedbackEnvelope, validateFeedbackEnvelope, writeFeedback, } from "../agent-blackboard/index.mjs";
|
|
3
|
+
import { optionalRecord, optionalString, requiredChoice, requiredString, requiredStringArray, } from "./args.mjs";
|
|
4
|
+
import { assertPolicyAllowed, outboxDirectory, readIdentity } from "./context.mjs";
|
|
5
|
+
import { byCreatedAt, deliveryResponse, MODES, OUTCOMES, readCoverage, } from "./handlers-journal.mjs";
|
|
6
|
+
import { readEntriesIfPresent } from "./journal-read.mjs";
|
|
7
|
+
import { runValidator } from "./policy-run.mjs";
|
|
8
|
+
import { parseRetrospectiveFrontMatter } from "./retrospective-front-matter.mjs";
|
|
9
|
+
import { FEEDBACK_MARKDOWN_MAX_BYTES, redactFeedbackText, } from "../agent-blackboard/feedback-codec.mjs";
|
|
10
|
+
import { composeFromArgs, sensitiveValues } from "./retrospective-input.mjs";
|
|
11
|
+
import { checkFrontMatter, resolveRepositories } from "./retrospective-resolve.mjs";
|
|
12
|
+
function retrospectiveData(entry) {
|
|
13
|
+
const data = entry?.data;
|
|
14
|
+
return data?.type === 'retrospective' ? data : undefined;
|
|
15
|
+
}
|
|
16
|
+
/** Whether the session already holds a retrospective, and which (sourceEventId, issues, prs). */
|
|
17
|
+
export const retrospectiveCheck = async (args, context) => {
|
|
18
|
+
const sourceEventId = optionalString(args, 'sourceEventId');
|
|
19
|
+
const entries = await readEntriesIfPresent(context.sessionId, context.env, context.dependencies);
|
|
20
|
+
const found = (entries ?? [])
|
|
21
|
+
.toSorted(byCreatedAt)
|
|
22
|
+
.flatMap((entry) => {
|
|
23
|
+
const data = retrospectiveData(entry);
|
|
24
|
+
return data === undefined ? [] : [data];
|
|
25
|
+
})
|
|
26
|
+
.filter((data) => sourceEventId === undefined || data.sourceEventId === sourceEventId)
|
|
27
|
+
.map((data) => ({
|
|
28
|
+
sourceEventId: data.sourceEventId,
|
|
29
|
+
timestamp: data.timestamp,
|
|
30
|
+
issues: data.issues ?? [],
|
|
31
|
+
prs: data.prs ?? [],
|
|
32
|
+
}));
|
|
33
|
+
return { sessionId: context.sessionId, saved: found.length > 0, entries: found };
|
|
34
|
+
};
|
|
35
|
+
function readMarkdownSource(args) {
|
|
36
|
+
const compose = optionalRecord(args, 'compose');
|
|
37
|
+
if ((args.markdown === undefined) === (compose === undefined))
|
|
38
|
+
throw new Error('pass exactly one of markdown or compose');
|
|
39
|
+
return compose === undefined ? { markdown: requiredString(args, 'markdown') } : { compose };
|
|
40
|
+
}
|
|
41
|
+
/** The default event id, hashed when the session id would push it past the 256-byte limit. */
|
|
42
|
+
export function defaultSourceEventId(sessionId) {
|
|
43
|
+
const id = `retrospective-${sessionId}`;
|
|
44
|
+
return Buffer.byteLength(id) <= 256
|
|
45
|
+
? id
|
|
46
|
+
: `retrospective-${createHash('sha256').update(sessionId).digest('hex')}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Saves the session's retrospective: composes it when `compose` is given, checks the arguments
|
|
50
|
+
* against the document's front matter, runs the checkout's `validateRetrospective` policy (in a
|
|
51
|
+
* child process) and writes nothing if it reports errors, then writes through the same verified
|
|
52
|
+
* path as `journal_append`.
|
|
53
|
+
*/
|
|
54
|
+
export const retrospectiveSave = async (args, context) => {
|
|
55
|
+
assertPolicyAllowed(context);
|
|
56
|
+
const identity = readIdentity(args, context.sessionId);
|
|
57
|
+
const mode = requiredChoice(args, 'mode', MODES);
|
|
58
|
+
const workOutcome = requiredChoice(args, 'workOutcome', OUTCOMES);
|
|
59
|
+
const feedbackCoverage = readCoverage(args);
|
|
60
|
+
const argRepositories = args.repositories === undefined ? undefined : requiredStringArray(args, 'repositories');
|
|
61
|
+
const sourceEventId = optionalString(args, 'sourceEventId') ?? defaultSourceEventId(context.sessionId);
|
|
62
|
+
const category = optionalString(args, 'category');
|
|
63
|
+
if (args.dryRun !== undefined && typeof args.dryRun !== 'boolean')
|
|
64
|
+
throw new Error('dryRun must be a boolean');
|
|
65
|
+
const policy = {
|
|
66
|
+
worktree: context.worktree,
|
|
67
|
+
env: context.env,
|
|
68
|
+
...(context.spawnPolicy === undefined ? {} : { spawn: context.spawnPolicy }),
|
|
69
|
+
};
|
|
70
|
+
const { markdown: given, compose } = readMarkdownSource(args);
|
|
71
|
+
const secrets = sensitiveValues(compose, context.env);
|
|
72
|
+
// Everything the policy sees is derived from the redacted document, never the raw input.
|
|
73
|
+
const redactedGiven = given === undefined ? undefined : redactFeedbackText(given, secrets);
|
|
74
|
+
// Reject a document that can never be stored before any policy export runs for it.
|
|
75
|
+
if (redactedGiven !== undefined && Buffer.byteLength(redactedGiven) > FEEDBACK_MARKDOWN_MAX_BYTES)
|
|
76
|
+
throw new Error(`markdown must be at most ${FEEDBACK_MARKDOWN_MAX_BYTES} bytes`);
|
|
77
|
+
const early = redactedGiven === undefined ? undefined : parseRetrospectiveFrontMatter(redactedGiven);
|
|
78
|
+
const repositories = await resolveRepositories(argRepositories, early?.repositories, policy);
|
|
79
|
+
const markdown = redactedGiven ??
|
|
80
|
+
redactFeedbackText(await composeFromArgs(compose, {
|
|
81
|
+
sessionId: context.sessionId,
|
|
82
|
+
repositories,
|
|
83
|
+
workOutcome,
|
|
84
|
+
feedbackCoverage,
|
|
85
|
+
worktree: context.worktree,
|
|
86
|
+
env: context.env,
|
|
87
|
+
dependencies: context.dependencies,
|
|
88
|
+
}), secrets);
|
|
89
|
+
const front = early ?? parseRetrospectiveFrontMatter(markdown);
|
|
90
|
+
checkFrontMatter(front, {
|
|
91
|
+
sessionId: context.sessionId,
|
|
92
|
+
workOutcome,
|
|
93
|
+
// The document was redacted before parsing, so compare it with equally redacted sources.
|
|
94
|
+
feedbackCoverage: {
|
|
95
|
+
...feedbackCoverage,
|
|
96
|
+
sources: feedbackCoverage.sources.map((source) => redactFeedbackText(source, secrets)),
|
|
97
|
+
},
|
|
98
|
+
repositories: argRepositories,
|
|
99
|
+
});
|
|
100
|
+
// Building the envelope first rejects empty or oversized markdown before any policy runs.
|
|
101
|
+
const envelope = createFeedbackEnvelope({
|
|
102
|
+
schemaVersion: 1,
|
|
103
|
+
type: 'retrospective',
|
|
104
|
+
sourceEventId,
|
|
105
|
+
// The server owns the timestamp, as in journal_append.
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
date: front.date,
|
|
108
|
+
issues: front.issues,
|
|
109
|
+
prs: front.prs,
|
|
110
|
+
repositories,
|
|
111
|
+
markdown,
|
|
112
|
+
workOutcome,
|
|
113
|
+
feedbackCoverage,
|
|
114
|
+
...(category === undefined ? {} : { category }),
|
|
115
|
+
}, { knownSensitiveValues: secrets });
|
|
116
|
+
const redactedFront = parseRetrospectiveFrontMatter(envelope.markdown);
|
|
117
|
+
validateFeedbackEnvelope(envelope);
|
|
118
|
+
const report = await runValidator({
|
|
119
|
+
...policy,
|
|
120
|
+
export: 'validateRetrospective',
|
|
121
|
+
input: {
|
|
122
|
+
sessionId: context.sessionId,
|
|
123
|
+
parentSessionId: identity.parentSessionId,
|
|
124
|
+
mode,
|
|
125
|
+
workOutcome,
|
|
126
|
+
// The envelope's copy, which is redacted like the markdown.
|
|
127
|
+
feedbackCoverage: envelope.feedbackCoverage,
|
|
128
|
+
repositories,
|
|
129
|
+
markdown: envelope.markdown,
|
|
130
|
+
sourceEventId,
|
|
131
|
+
metadata: {
|
|
132
|
+
date: redactedFront.date,
|
|
133
|
+
issues: redactedFront.issues,
|
|
134
|
+
prs: redactedFront.prs,
|
|
135
|
+
description: redactedFront.description,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
if ('errors' in report && report.errors.length)
|
|
140
|
+
throw new Error(`policy rejected the retrospective:\n- ${report.errors.join('\n- ')}`);
|
|
141
|
+
const warnings = 'warnings' in report ? report.warnings : [];
|
|
142
|
+
if (args.dryRun === true)
|
|
143
|
+
return { markdown: envelope.markdown, policy: { warnings } };
|
|
144
|
+
const directory = mode === 'interactive' ? outboxDirectory(context.worktree) : undefined;
|
|
145
|
+
const result = await writeFeedback({
|
|
146
|
+
identity,
|
|
147
|
+
envelope,
|
|
148
|
+
mode,
|
|
149
|
+
...(directory === undefined ? {} : { outboxDirectory: directory }),
|
|
150
|
+
env: context.env,
|
|
151
|
+
dependencies: context.dependencies,
|
|
152
|
+
});
|
|
153
|
+
return { ...deliveryResponse(result, context, mode), policy: { warnings } };
|
|
154
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ToolCallResult } from './dispatch.mts';
|
|
2
2
|
import { type FakeBlackboard, type FakeOptions } from './fake-blackboard.test-helpers.mts';
|
|
3
3
|
import { type RepoFixture } from './git-fixture.test-helpers.mts';
|
|
4
|
+
import type { PolicySpawner } from './policy-spawn.mts';
|
|
4
5
|
/** A fresh pair of real repositories for every test. */
|
|
5
6
|
export declare function useRepoFixture(): () => RepoFixture;
|
|
6
7
|
export declare function outboxPath(worktree: string): string;
|
|
@@ -12,6 +13,7 @@ export type Harness = {
|
|
|
12
13
|
export declare function harness(fixture: RepoFixture, options?: FakeOptions & {
|
|
13
14
|
env?: NodeJS.ProcessEnv;
|
|
14
15
|
launchRoot?: string | undefined;
|
|
16
|
+
spawnPolicy?: PolicySpawner;
|
|
15
17
|
}): Harness;
|
|
16
18
|
export declare function textOf(result: ToolCallResult): string;
|
|
17
19
|
export declare function jsonOf(result: ToolCallResult): Record<string, unknown>;
|
|
@@ -24,6 +24,7 @@ export function harness(fixture, options = {}) {
|
|
|
24
24
|
env: options.env ?? BLACKBOARD_ENV,
|
|
25
25
|
runGit: runIsolatedGit,
|
|
26
26
|
blackboard: fake.dependencies,
|
|
27
|
+
...(options.spawnPolicy === undefined ? {} : { spawnPolicy: options.spawnPolicy }),
|
|
27
28
|
};
|
|
28
29
|
return { fake, call: (name, args) => callTool(name, args, environment) };
|
|
29
30
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type JournalLoader } from '../agent-blackboard/index.mts';
|
|
2
|
+
import type { BlackboardClientDependencies } from '../agent-blackboard/client.mts';
|
|
3
|
+
/**
|
|
4
|
+
* Every entry of the session through the server's own blackboard client, or undefined when the
|
|
5
|
+
* session was never created (the blackboard answers 404). Any other failure is thrown.
|
|
6
|
+
*/
|
|
7
|
+
export declare function readEntriesIfPresent(sessionId: string, env: NodeJS.ProcessEnv, dependencies: BlackboardClientDependencies): Promise<unknown[] | undefined>;
|
|
8
|
+
/** The journal loader `composeRetrospective` takes, reading through the server's own client. */
|
|
9
|
+
export declare function serverJournalLoader(env: NodeJS.ProcessEnv, dependencies: BlackboardClientDependencies): JournalLoader;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readJournal } from "../agent-blackboard/index.mjs";
|
|
2
|
+
function isNotFound(error) {
|
|
3
|
+
return (typeof error === 'object' &&
|
|
4
|
+
error !== null &&
|
|
5
|
+
(('status' in error && error.status === 404) ||
|
|
6
|
+
('statusCode' in error && error.statusCode === 404)));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Every entry of the session through the server's own blackboard client, or undefined when the
|
|
10
|
+
* session was never created (the blackboard answers 404). Any other failure is thrown.
|
|
11
|
+
*/
|
|
12
|
+
export async function readEntriesIfPresent(sessionId, env, dependencies) {
|
|
13
|
+
try {
|
|
14
|
+
return await readJournal(sessionId, env, dependencies);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (isNotFound(error))
|
|
18
|
+
return undefined;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** The journal loader `composeRetrospective` takes, reading through the server's own client. */
|
|
23
|
+
export function serverJournalLoader(env, dependencies) {
|
|
24
|
+
return async (sessionId) => {
|
|
25
|
+
const entries = await readEntriesIfPresent(sessionId, env, dependencies);
|
|
26
|
+
return entries === undefined
|
|
27
|
+
? { status: 'not-found' }
|
|
28
|
+
: {
|
|
29
|
+
status: 'ok',
|
|
30
|
+
entries: entries,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real path of the policy module the worktree's package.json declares as
|
|
3
|
+
* `"vouchington-tooling": { "policy": "<relative path>" }`, or undefined when it declares none.
|
|
4
|
+
* The path must be relative and, after resolving symlinks, stay inside the worktree.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolvePolicyModule(worktree: string): Promise<string | undefined>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
3
|
+
function packagePolicy(worktree, text) {
|
|
4
|
+
let manifest;
|
|
5
|
+
try {
|
|
6
|
+
manifest = JSON.parse(text);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
throw new Error(`${join(worktree, 'package.json')} is not valid JSON`);
|
|
10
|
+
}
|
|
11
|
+
const section = manifest?.['vouchington-tooling'];
|
|
12
|
+
if (section === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
// A malformed section must not read as "no policy declared", which would skip validation.
|
|
15
|
+
if (section === null || typeof section !== 'object' || Array.isArray(section))
|
|
16
|
+
throw new Error('package.json "vouchington-tooling" must be an object');
|
|
17
|
+
const policy = section.policy;
|
|
18
|
+
if (policy === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (typeof policy !== 'string' || !policy.trim())
|
|
21
|
+
throw new Error('package.json "vouchington-tooling".policy must be a non-empty string path');
|
|
22
|
+
return policy;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The real path of the policy module the worktree's package.json declares as
|
|
26
|
+
* `"vouchington-tooling": { "policy": "<relative path>" }`, or undefined when it declares none.
|
|
27
|
+
* The path must be relative and, after resolving symlinks, stay inside the worktree.
|
|
28
|
+
*/
|
|
29
|
+
export async function resolvePolicyModule(worktree) {
|
|
30
|
+
let text;
|
|
31
|
+
try {
|
|
32
|
+
text = await readFile(join(worktree, 'package.json'), 'utf8');
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
// Only a missing manifest means "no policy"; an unreadable one must not silently disable it.
|
|
36
|
+
if (error.code === 'ENOENT')
|
|
37
|
+
return undefined;
|
|
38
|
+
throw new Error(`${join(worktree, 'package.json')} could not be read: ${error.message}`, {
|
|
39
|
+
cause: error,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const declared = packagePolicy(worktree, text);
|
|
43
|
+
if (declared === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
if (isAbsolute(declared))
|
|
46
|
+
throw new Error(`policy path ${declared} must be relative`);
|
|
47
|
+
const root = await realpath(worktree);
|
|
48
|
+
let resolved;
|
|
49
|
+
try {
|
|
50
|
+
resolved = await realpath(join(root, declared));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error(`policy module ${declared} does not exist in the worktree`);
|
|
54
|
+
}
|
|
55
|
+
const inside = relative(root, resolved);
|
|
56
|
+
// relative(), not a prefix test, so a worktree at the filesystem root still contains its files.
|
|
57
|
+
if (!inside || inside === '..' || inside.startsWith(`..${sep}`) || isAbsolute(inside))
|
|
58
|
+
throw new Error(`policy path ${declared} must stay inside the worktree`);
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type PolicySpawner } from './policy-spawn.mts';
|
|
2
|
+
/** How long each policy export may run before its process is killed, in milliseconds. */
|
|
3
|
+
export declare const POLICY_TIMEOUTS_MS: {
|
|
4
|
+
readonly validateRetrospective: 30000;
|
|
5
|
+
readonly validatePullRequest: 120000;
|
|
6
|
+
readonly pullRequestDefaults: 120000;
|
|
7
|
+
};
|
|
8
|
+
type PolicyExport = keyof typeof POLICY_TIMEOUTS_MS;
|
|
9
|
+
export type PolicyResult = {
|
|
10
|
+
status: 'no-policy';
|
|
11
|
+
} | {
|
|
12
|
+
status: 'skipped';
|
|
13
|
+
} | {
|
|
14
|
+
status: 'ok';
|
|
15
|
+
result: unknown;
|
|
16
|
+
};
|
|
17
|
+
export type PolicyRun = {
|
|
18
|
+
worktree: string;
|
|
19
|
+
export: PolicyExport;
|
|
20
|
+
input: unknown;
|
|
21
|
+
env: NodeJS.ProcessEnv;
|
|
22
|
+
/** Test seam: replaces the real child process. */
|
|
23
|
+
spawn?: PolicySpawner;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Runs one export of the worktree's declared policy module in a fresh child process (never in
|
|
27
|
+
* this one). Resolves `no-policy` when the worktree declares none and `skipped` when the module
|
|
28
|
+
* lacks the export. A timeout, crash, non-zero exit, oversized output, or unusable reply throws:
|
|
29
|
+
* a failed policy is never a pass.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runPolicyExport(run: PolicyRun): Promise<PolicyResult>;
|
|
32
|
+
export type ValidatorReport = {
|
|
33
|
+
errors: string[];
|
|
34
|
+
warnings: string[];
|
|
35
|
+
};
|
|
36
|
+
/** Runs a validator export and checks its result is `{ errors: string[], warnings?: string[] }`. */
|
|
37
|
+
export declare function runValidator(run: PolicyRun): Promise<ValidatorReport | {
|
|
38
|
+
status: 'no-policy' | 'skipped';
|
|
39
|
+
}>;
|
|
40
|
+
export {};
|