vouchington-tooling 0.26.0 → 0.28.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 +5 -1
- package/dist/agent-blackboard/feedback-canonical.d.mts +7 -0
- package/dist/agent-blackboard/feedback-canonical.mjs +8 -0
- package/dist/agent-blackboard/feedback-delivery.mjs +8 -6
- package/dist/agent-blackboard/feedback-online.mjs +11 -4
- package/dist/agent-blackboard/feedback-outbox.d.mts +8 -1
- package/dist/agent-blackboard/feedback-outbox.mjs +17 -4
- package/dist/agent-blackboard/feedback-types.d.mts +4 -0
- package/dist/agent-blackboard/index.d.mts +1 -1
- package/dist/agent-blackboard/index.mjs +1 -1
- package/dist/cli/index.mjs +4 -0
- package/dist/cli/parse.d.mts +1 -1
- package/dist/cli/parse.mjs +1 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +2 -0
- package/dist/mcp-server/dispatch.mjs +5 -4
- package/dist/mcp-server/fake-blackboard.test-helpers.d.mts +3 -0
- package/dist/mcp-server/fake-blackboard.test-helpers.mjs +7 -1
- package/dist/mcp-server/handlers-journal.d.mts +0 -2
- package/dist/mcp-server/handlers-journal.mjs +22 -30
- package/dist/mcp-server/handlers-outbox.d.mts +11 -0
- package/dist/mcp-server/handlers-outbox.mjs +45 -0
- package/dist/mcp-server/harness.test-helpers.d.mts +6 -1
- package/dist/mcp-server/harness.test-helpers.mjs +14 -2
- package/dist/mcp-server/server.mjs +5 -1
- package/dist/mcp-server/tools.mjs +12 -10
- package/dist/pnpm-install/pnpm-install-fake-pnpm.test-helpers.mjs +2 -2
- package/docs/mcp-server.md +58 -14
- package/package.json +3 -3
- package/scripts/gha/gha-collaborator-trust.sh +75 -0
- package/skills/blackboard/SKILL.md +27 -17
- package/skills/manifest.json +27 -21
- package/skills/postgres-node-performance-tuning/references/performance-patterns.md +22 -0
- package/skills/postgres-partitioning-uuid-v7/SKILL.md +3 -0
- package/skills/postgres-schema-design/SKILL.md +21 -0
- package/skills/postgres-schema-design/references/naming.md +34 -0
- package/skills/postgres-schema-design/references/relations.md +46 -0
- package/skills/postgres-schema-design/references/table-shapes.md +56 -0
- package/skills/retrospective-distill/SKILL.md +13 -8
package/README.md
CHANGED
|
@@ -80,6 +80,7 @@ vouchington install-playwright-chromium-arm64
|
|
|
80
80
|
vouchington ghcr-package-retention example%2Fapi
|
|
81
81
|
vouchington harness-admission-lane 4
|
|
82
82
|
vouchington harness-assert-gates HARNESS_DISPATCH_ENABLED HARNESS_SHEPHERD_ENABLED
|
|
83
|
+
echo '[{"login":"octocat","type":"User"}]' | vouchington gha-collaborator-trust owner/repo --allow-bot github-actions[bot]
|
|
83
84
|
vouchington nuget-central-version trusted.props candidate.props metadata.json out.props
|
|
84
85
|
vouchington swift-semantic-equal BASE HEAD App.swift
|
|
85
86
|
vouchington post-review
|
|
@@ -554,7 +555,10 @@ Identity, content, and archived-session conflicts fail in either mode while pres
|
|
|
554
555
|
record; they are not reported as transient pending delivery. Replay retains permanently conflicted
|
|
555
556
|
records and continues to deliver unrelated pending records; transient failures stop that replay.
|
|
556
557
|
Verified delivery retains its receipt when local cleanup fails, returning
|
|
557
|
-
`cleanupDiagnostic` with value `"outbox-cleanup-failed"`. A retained or crash-reappearing record is
|
|
558
|
+
`cleanupDiagnostic` with value `"outbox-cleanup-failed"`. A retained or crash-reappearing record is
|
|
559
|
+
safe to replay: an event is identified by session, `sourceEventId`, and content, and a duplicate that
|
|
560
|
+
differs only in `timestamp` is the same event, so it reports the stored record's `timestamp` and
|
|
561
|
+
receipt and the retained duplicate is removed.
|
|
558
562
|
Use `vouchington agent-blackboard journal flush --outbox-directory PATH` to replay unchanged pending
|
|
559
563
|
records after connectivity returns.
|
|
560
564
|
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
import type { FeedbackEnvelope } from './feedback-types.mts';
|
|
1
2
|
export declare function canonicalFeedback(value: unknown): string;
|
|
3
|
+
/**
|
|
4
|
+
* The identity of a feedback event is its session, `sourceEventId`, and content. The `timestamp` is
|
|
5
|
+
* assigned by the server on each attempt, so a retry that differs only in `timestamp` is the same
|
|
6
|
+
* event and is left out of the comparison.
|
|
7
|
+
*/
|
|
8
|
+
export declare function canonicalFeedbackEvent(envelope: FeedbackEnvelope): string;
|
|
@@ -2,6 +2,14 @@ import { isObject } from './snapshot-partition-guards.mjs';
|
|
|
2
2
|
export function canonicalFeedback(value) {
|
|
3
3
|
return JSON.stringify(ordered(value));
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* The identity of a feedback event is its session, `sourceEventId`, and content. The `timestamp` is
|
|
7
|
+
* assigned by the server on each attempt, so a retry that differs only in `timestamp` is the same
|
|
8
|
+
* event and is left out of the comparison.
|
|
9
|
+
*/
|
|
10
|
+
export function canonicalFeedbackEvent(envelope) {
|
|
11
|
+
return canonicalFeedback(Object.fromEntries(Object.entries(envelope).filter(([key]) => key !== 'timestamp')));
|
|
12
|
+
}
|
|
5
13
|
function ordered(value) {
|
|
6
14
|
if (Array.isArray(value))
|
|
7
15
|
return value.map(ordered);
|
|
@@ -18,13 +18,14 @@ export async function writeFeedback(input) {
|
|
|
18
18
|
}
|
|
19
19
|
if (!input.outboxDirectory)
|
|
20
20
|
throw new Error('interactive feedback requires an explicit durable outbox directory');
|
|
21
|
-
|
|
21
|
+
// A retained record for the same event keeps its own timestamp and is the one delivered.
|
|
22
|
+
const record = persistFeedbackOutbox(input.outboxDirectory, {
|
|
23
|
+
identity: input.identity,
|
|
24
|
+
envelope,
|
|
25
|
+
});
|
|
22
26
|
try {
|
|
23
|
-
const receipt = await deliverFeedbackOnline({ ...input, envelope });
|
|
24
|
-
const cleanup = removeFeedbackOutbox(input.outboxDirectory,
|
|
25
|
-
identity: input.identity,
|
|
26
|
-
envelope,
|
|
27
|
-
});
|
|
27
|
+
const receipt = await deliverFeedbackOnline({ ...input, envelope: record.envelope });
|
|
28
|
+
const cleanup = removeFeedbackOutbox(input.outboxDirectory, record);
|
|
28
29
|
return { status: 'delivered', sourceEventId: envelope.sourceEventId, ...cleanup, receipt };
|
|
29
30
|
}
|
|
30
31
|
catch (error) {
|
|
@@ -34,6 +35,7 @@ export async function writeFeedback(input) {
|
|
|
34
35
|
return {
|
|
35
36
|
status: 'pending',
|
|
36
37
|
sourceEventId: envelope.sourceEventId,
|
|
38
|
+
timestamp: record.envelope.timestamp,
|
|
37
39
|
pendingCount: feedbackOutboxStatus(input.outboxDirectory).pendingCount,
|
|
38
40
|
diagnostic,
|
|
39
41
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { feedbackDeadline } from './feedback-deadline.mjs';
|
|
2
2
|
import { FeedbackDeliveryError, feedbackDiagnostic } from './feedback-online-error.mjs';
|
|
3
|
-
import { canonicalFeedback } from './feedback-canonical.mjs';
|
|
3
|
+
import { canonicalFeedback, canonicalFeedbackEvent } from './feedback-canonical.mjs';
|
|
4
4
|
import { loadClient, resolveBlackboardConnection } from './client.mjs';
|
|
5
5
|
import { normalizeRepositories, validateFeedbackEnvelope } from './feedback-codec.mjs';
|
|
6
6
|
import { validateFeedbackIdentity } from './feedback-identity.mjs';
|
|
@@ -48,13 +48,18 @@ async function findEvent(entries, envelope, expectedCreatedAt) {
|
|
|
48
48
|
catch {
|
|
49
49
|
throw new FeedbackDeliveryError('event-conflict');
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
// Same event means same content; the stored `timestamp` may differ from this attempt's.
|
|
52
|
+
if (canonicalFeedbackEvent(entry.data) !== canonicalFeedbackEvent(envelope) ||
|
|
52
53
|
typeof entry.createdAt !== 'string')
|
|
53
54
|
throw new FeedbackDeliveryError('event-conflict');
|
|
54
55
|
if (++matching > 1 && expectedCreatedAt !== undefined)
|
|
55
56
|
throw new FeedbackDeliveryError('event-conflict');
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
// Duplicates of one event (a late write beside a retry) report the earliest stored record, so
|
|
58
|
+
// every retry returns the same timestamp.
|
|
59
|
+
if (expectedCreatedAt === undefined
|
|
60
|
+
? found === undefined || entry.createdAt < found.createdAt
|
|
61
|
+
: entry.createdAt === expectedCreatedAt)
|
|
62
|
+
found = { createdAt: entry.createdAt, timestamp: entry.data.timestamp };
|
|
58
63
|
}
|
|
59
64
|
return found;
|
|
60
65
|
}
|
|
@@ -108,6 +113,7 @@ export async function deliverFeedbackOnline(input, fresh = false) {
|
|
|
108
113
|
sessionId: input.identity.sessionId,
|
|
109
114
|
sourceEventId: input.envelope.sourceEventId,
|
|
110
115
|
createdAt: existing.createdAt,
|
|
116
|
+
timestamp: existing.timestamp,
|
|
111
117
|
verified: true,
|
|
112
118
|
};
|
|
113
119
|
const appended = await entries.append({
|
|
@@ -127,6 +133,7 @@ export async function deliverFeedbackOnline(input, fresh = false) {
|
|
|
127
133
|
sessionId: input.identity.sessionId,
|
|
128
134
|
sourceEventId: input.envelope.sourceEventId,
|
|
129
135
|
createdAt: confirmed.createdAt,
|
|
136
|
+
timestamp: confirmed.timestamp,
|
|
130
137
|
verified: true,
|
|
131
138
|
};
|
|
132
139
|
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { FeedbackOutboxRecord, FeedbackOutboxStatus } from './feedback-types.mts';
|
|
2
2
|
export declare function feedbackOutboxStatus(directory: string): FeedbackOutboxStatus;
|
|
3
3
|
export declare function listFeedbackOutbox(directory: string): FeedbackOutboxRecord[];
|
|
4
|
-
|
|
4
|
+
/** Reads the retained records without creating the outbox directory. */
|
|
5
|
+
export declare function readFeedbackOutbox(directory: string): FeedbackOutboxRecord[];
|
|
6
|
+
/**
|
|
7
|
+
* Retains the record unless the same event is already retained, in which case that record stays:
|
|
8
|
+
* the event is the session, `sourceEventId`, and content, and the earlier `timestamp` wins. The
|
|
9
|
+
* returned record is the one to deliver.
|
|
10
|
+
*/
|
|
11
|
+
export declare function persistFeedbackOutbox(directory: string, record: FeedbackOutboxRecord): FeedbackOutboxRecord;
|
|
5
12
|
export declare function removeFeedbackOutbox(directory: string, record: FeedbackOutboxRecord): {
|
|
6
13
|
pendingCount: number;
|
|
7
14
|
cleanupDiagnostic?: 'outbox-cleanup-failed';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalFeedback } from './feedback-canonical.mjs';
|
|
1
|
+
import { canonicalFeedback, canonicalFeedbackEvent } from './feedback-canonical.mjs';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { closeSync, constants, fsyncSync, fstatSync, openSync, readdirSync, renameSync, unlinkSync, } from 'node:fs';
|
|
4
4
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
@@ -83,6 +83,18 @@ export function listFeedbackOutbox(directory) {
|
|
|
83
83
|
const path = outboxDirectory(directory, true);
|
|
84
84
|
return withFileLock(join(path, '.records'), () => records(path).map((item) => item.record));
|
|
85
85
|
}
|
|
86
|
+
/** Reads the retained records without creating the outbox directory. */
|
|
87
|
+
export function readFeedbackOutbox(directory) {
|
|
88
|
+
const path = outboxDirectory(directory, false);
|
|
89
|
+
if (!ensurePrivateDirectory(path, false))
|
|
90
|
+
return [];
|
|
91
|
+
return withFileLock(join(path, '.records'), () => records(path).map((item) => item.record));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Retains the record unless the same event is already retained, in which case that record stays:
|
|
95
|
+
* the event is the session, `sourceEventId`, and content, and the earlier `timestamp` wins. The
|
|
96
|
+
* returned record is the one to deliver.
|
|
97
|
+
*/
|
|
86
98
|
export function persistFeedbackOutbox(directory, record) {
|
|
87
99
|
validateFeedbackIdentity(record.identity);
|
|
88
100
|
validateFeedbackEnvelope(record.envelope);
|
|
@@ -93,9 +105,10 @@ export function persistFeedbackOutbox(directory, record) {
|
|
|
93
105
|
const existing = current.find((item) => item.path === join(path, name));
|
|
94
106
|
const serialized = JSON.stringify(record);
|
|
95
107
|
if (existing) {
|
|
96
|
-
if (canonicalFeedback(existing.record) !== canonicalFeedback(record)
|
|
108
|
+
if (canonicalFeedback(existing.record.identity) !== canonicalFeedback(record.identity) ||
|
|
109
|
+
canonicalFeedbackEvent(existing.record.envelope) !== canonicalFeedbackEvent(record.envelope))
|
|
97
110
|
throw new Error('feedback source event conflicts with a retained unsent record');
|
|
98
|
-
return
|
|
111
|
+
return existing.record;
|
|
99
112
|
}
|
|
100
113
|
const bytes = Buffer.byteLength(serialized);
|
|
101
114
|
if (bytes > RECORD_MAX_BYTES ||
|
|
@@ -128,7 +141,7 @@ export function persistFeedbackOutbox(directory, record) {
|
|
|
128
141
|
catch { }
|
|
129
142
|
throw error;
|
|
130
143
|
}
|
|
131
|
-
return
|
|
144
|
+
return record;
|
|
132
145
|
});
|
|
133
146
|
}
|
|
134
147
|
export function removeFeedbackOutbox(directory, record) {
|
|
@@ -31,6 +31,8 @@ export type FeedbackReceipt = {
|
|
|
31
31
|
sessionId: string;
|
|
32
32
|
sourceEventId: string;
|
|
33
33
|
createdAt: string;
|
|
34
|
+
/** The `timestamp` of the stored entry, which may be earlier than the attempt that confirmed it. */
|
|
35
|
+
timestamp: string;
|
|
34
36
|
verified: true;
|
|
35
37
|
};
|
|
36
38
|
export type FeedbackDiagnostic = 'blackboard-unavailable' | 'client-unavailable' | 'configuration-invalid' | 'authentication-rejected' | 'identity-conflict' | 'readback-unconfirmed' | 'event-conflict' | 'archived-session' | 'delivery-timeout';
|
|
@@ -43,6 +45,8 @@ export type FeedbackDeliveryResult = {
|
|
|
43
45
|
} | {
|
|
44
46
|
status: 'pending';
|
|
45
47
|
sourceEventId: string;
|
|
48
|
+
/** The `timestamp` of the retained record that a later flush delivers. */
|
|
49
|
+
timestamp: string;
|
|
46
50
|
pendingCount: number;
|
|
47
51
|
diagnostic: FeedbackDiagnostic;
|
|
48
52
|
};
|
|
@@ -19,7 +19,7 @@ export type { BlackboardConnection, BlackboardClientModule, BlackboardClientDepe
|
|
|
19
19
|
export type * from './feedback-types.mts';
|
|
20
20
|
export { createFeedbackEnvelope, validateFeedbackEnvelope } from './feedback-codec.mts';
|
|
21
21
|
export { writeFeedback, verifyFreshFeedback, flushFeedbackOutbox, FeedbackDeliveryError, } from './feedback-delivery.mts';
|
|
22
|
-
export { feedbackOutboxStatus } from './feedback-outbox.mts';
|
|
22
|
+
export { feedbackOutboxStatus, readFeedbackOutbox } from './feedback-outbox.mts';
|
|
23
23
|
export { composeRetrospective } from './feedback-compose.mts';
|
|
24
24
|
export type { RetrospectiveCompositionInput, FeedbackAssessment } from './feedback-compose.mts';
|
|
25
25
|
export type { JournalAuditOptions } from './feedback-journal-audit.mts';
|
|
@@ -61,5 +61,5 @@ export function formatJournalEntries(sessionId, entries) {
|
|
|
61
61
|
export { resolveBlackboardConnection } from './client.mjs';
|
|
62
62
|
export { createFeedbackEnvelope, validateFeedbackEnvelope } from './feedback-codec.mjs';
|
|
63
63
|
export { writeFeedback, verifyFreshFeedback, flushFeedbackOutbox, FeedbackDeliveryError, } from './feedback-delivery.mjs';
|
|
64
|
-
export { feedbackOutboxStatus } from './feedback-outbox.mjs';
|
|
64
|
+
export { feedbackOutboxStatus, readFeedbackOutbox } from './feedback-outbox.mjs';
|
|
65
65
|
export { composeRetrospective } from './feedback-compose.mjs';
|
package/dist/cli/index.mjs
CHANGED
|
@@ -73,6 +73,10 @@ const SCRIPT_PATHS = {
|
|
|
73
73
|
path: 'scripts/gha/harness-admission-lane.sh',
|
|
74
74
|
},
|
|
75
75
|
'harness-assert-gates': { command: 'bash', path: 'scripts/gha/harness-assert-gates.sh' },
|
|
76
|
+
'gha-collaborator-trust': {
|
|
77
|
+
command: 'bash',
|
|
78
|
+
path: 'scripts/gha/gha-collaborator-trust.sh',
|
|
79
|
+
},
|
|
76
80
|
};
|
|
77
81
|
export function runCli(argv = process.argv) {
|
|
78
82
|
const parsed = parseCli(argv);
|
package/dist/cli/parse.d.mts
CHANGED
|
@@ -89,7 +89,7 @@ export type ParsedCli = {
|
|
|
89
89
|
workflowDirectories?: string[];
|
|
90
90
|
actionDirectories?: string[];
|
|
91
91
|
} | ParsedGhaRuntimeAudit | ParsedGhaArtifactsCleanup;
|
|
92
|
-
export type ScriptCommand = 'gha-output' | 'gha-needs-results' | 'download-with-diagnostics' | 'download-optional-run-artifacts' | 'host-pressure-diagnostics' | 'allocate-browser-safe-ports' | 'diagnose-port-collision' | 'prepare-trivy-db' | 'check-cache-size' | 'make-shard-matrix' | 'load-runner-env' | 'clean-workspace' | 'install-github-release' | 'run-with-timeout' | 'lint-links' | 'materialize-pr-context' | 'wait-for-apt-locks' | 'install-playwright-chromium-arm64' | 'ghcr-package-retention' | 'harness-admission-lane' | 'harness-assert-gates';
|
|
92
|
+
export type ScriptCommand = 'gha-output' | 'gha-needs-results' | 'download-with-diagnostics' | 'download-optional-run-artifacts' | 'host-pressure-diagnostics' | 'allocate-browser-safe-ports' | 'diagnose-port-collision' | 'prepare-trivy-db' | 'check-cache-size' | 'make-shard-matrix' | 'load-runner-env' | 'clean-workspace' | 'install-github-release' | 'run-with-timeout' | 'lint-links' | 'materialize-pr-context' | 'wait-for-apt-locks' | 'install-playwright-chromium-arm64' | 'ghcr-package-retention' | 'harness-admission-lane' | 'harness-assert-gates' | 'gha-collaborator-trust';
|
|
93
93
|
export declare function parseCli(argv: readonly string[]): ParsedCli;
|
|
94
94
|
export declare function parseCommandHelp(command: string | undefined, rest: readonly string[]): {
|
|
95
95
|
kind: 'command-help';
|
package/dist/cli/parse.mjs
CHANGED
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 agent-harness-config Apply classifier-auto + sandbox keys to agent harnesses\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 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\nagent-harness-config dump\nagent-harness-config check|apply [--global] [--repo PATH]... [--harness claude|codex|grok|cursor]...\n [--home PATH]\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>...\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 flush --outbox-directory PATH\nInteractive journal append requires --outbox-directory PATH.\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 agent-harness-config Apply classifier-auto + sandbox keys to agent harnesses\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\nagent-harness-config dump\nagent-harness-config check|apply [--global] [--repo PATH]... [--harness claude|codex|grok|cursor]...\n [--home PATH]\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 flush --outbox-directory PATH\nInteractive journal append requires --outbox-directory PATH.\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";
|
|
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
|
@@ -37,6 +37,7 @@ Commands:
|
|
|
37
37
|
ghcr-package-retention Delete old GHCR package versions past KEEP_MIN
|
|
38
38
|
harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out
|
|
39
39
|
harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled
|
|
40
|
+
gha-collaborator-trust Classify authors as write-access collaborators or not
|
|
40
41
|
nuget-central-version Validate a Directory.Packages.props PackageVersion delta
|
|
41
42
|
swift-semantic-equal Compare Swift sources ignoring comments and whitespace
|
|
42
43
|
post-review Post one COMMENT review from a staged payload file
|
|
@@ -111,6 +112,7 @@ install-playwright-chromium-arm64 [name:archive...]
|
|
|
111
112
|
ghcr-package-retention <url-encoded-package>...
|
|
112
113
|
harness-admission-lane <lanes>
|
|
113
114
|
harness-assert-gates <gate>...
|
|
115
|
+
gha-collaborator-trust <owner/repo> [--allow-bot <login>]... < authors.json
|
|
114
116
|
nuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>
|
|
115
117
|
swift-semantic-equal <base> <head> <file.swift>
|
|
116
118
|
post-review
|
|
@@ -2,7 +2,8 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { FeedbackDeliveryError } from '../agent-blackboard/index.mjs';
|
|
3
3
|
import { optionalString, rejectUnknown, requireObject } from './args.mjs';
|
|
4
4
|
import { readSessionId } from './context.mjs';
|
|
5
|
-
import { journalAppend, journalEntries
|
|
5
|
+
import { journalAppend, journalEntries } from './handlers-journal.mjs';
|
|
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';
|
|
8
9
|
import { resolveWorktree } from './worktree.mjs';
|
|
@@ -17,14 +18,14 @@ const HANDLERS = {
|
|
|
17
18
|
};
|
|
18
19
|
const DIAGNOSTIC_HINTS = {
|
|
19
20
|
'identity-conflict': 'the session already exists with a different parent, agent, or version',
|
|
20
|
-
'event-conflict': 'sourceEventId was already used with different content
|
|
21
|
+
'event-conflict': 'sourceEventId was already used with different content; use a new sourceEventId',
|
|
21
22
|
'archived-session': 'the session is archived; write to a new session',
|
|
22
23
|
'authentication-rejected': 'the blackboard rejected AGENT_BLACKBOARD_TOKEN',
|
|
23
24
|
'blackboard-unavailable': 'the blackboard could not be reached or returned an error',
|
|
24
25
|
'configuration-invalid': 'AGENT_BLACKBOARD_URL or AGENT_BLACKBOARD_TOKEN is missing or invalid',
|
|
25
26
|
'client-unavailable': 'agent-blackboard is not installed in the worktree',
|
|
26
|
-
'readback-unconfirmed': 'the write could not be confirmed; retry
|
|
27
|
-
'delivery-timeout': 'delivery timed out; retry
|
|
27
|
+
'readback-unconfirmed': 'the write could not be confirmed; retry the same call (same sourceEventId and content)',
|
|
28
|
+
'delivery-timeout': 'delivery timed out; retry the same call (same sourceEventId and content)',
|
|
28
29
|
};
|
|
29
30
|
function describeError(error) {
|
|
30
31
|
if (error instanceof FeedbackDeliveryError)
|
|
@@ -22,6 +22,8 @@ export type FakeOptions = {
|
|
|
22
22
|
};
|
|
23
23
|
entries?: StoredEntry[];
|
|
24
24
|
entriesError?: unknown;
|
|
25
|
+
/** An entry another caller writes just before the next append, after that append's read. */
|
|
26
|
+
lateEntry?: StoredEntry;
|
|
25
27
|
};
|
|
26
28
|
export type FakeBlackboard = {
|
|
27
29
|
dependencies: BlackboardClientDependencies;
|
|
@@ -29,6 +31,7 @@ export type FakeBlackboard = {
|
|
|
29
31
|
ensure: unknown[];
|
|
30
32
|
patch: unknown[];
|
|
31
33
|
append: unknown[];
|
|
34
|
+
get: unknown[];
|
|
32
35
|
archive: string[];
|
|
33
36
|
export: unknown[];
|
|
34
37
|
};
|
|
@@ -15,6 +15,7 @@ export function fakeBlackboard(options = {}) {
|
|
|
15
15
|
ensure: [],
|
|
16
16
|
patch: [],
|
|
17
17
|
append: [],
|
|
18
|
+
get: [],
|
|
18
19
|
archive: [],
|
|
19
20
|
export: [],
|
|
20
21
|
};
|
|
@@ -44,9 +45,13 @@ export function fakeBlackboard(options = {}) {
|
|
|
44
45
|
}
|
|
45
46
|
if (options.omitArchive)
|
|
46
47
|
delete Sessions.prototype.archive;
|
|
48
|
+
let lateEntry = options.lateEntry;
|
|
47
49
|
class Entries {
|
|
48
50
|
async append(input) {
|
|
49
51
|
calls.append.push(input);
|
|
52
|
+
if (lateEntry)
|
|
53
|
+
stored.push(lateEntry);
|
|
54
|
+
lateEntry = undefined;
|
|
50
55
|
const entry = {
|
|
51
56
|
createdAt: `2026-01-01T00:00:${String(stored.length).padStart(2, '0')}.000Z`,
|
|
52
57
|
data: input.data,
|
|
@@ -54,7 +59,8 @@ export function fakeBlackboard(options = {}) {
|
|
|
54
59
|
stored.push(entry);
|
|
55
60
|
return entry;
|
|
56
61
|
}
|
|
57
|
-
async *get() {
|
|
62
|
+
async *get(input) {
|
|
63
|
+
calls.get.push(input);
|
|
58
64
|
if (options.entriesError !== undefined)
|
|
59
65
|
throw options.entriesError;
|
|
60
66
|
yield* stored;
|
|
@@ -2,5 +2,3 @@ import { type ToolHandler } from './context.mts';
|
|
|
2
2
|
export declare const journalAppend: ToolHandler;
|
|
3
3
|
/** Every entry as the client returns it, oldest first. Nothing is filtered or rewritten. */
|
|
4
4
|
export declare const journalEntries: ToolHandler;
|
|
5
|
-
export declare const outboxStatus: ToolHandler;
|
|
6
|
-
export declare const outboxFlush: ToolHandler;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { appendJournalMarkdown,
|
|
1
|
+
import { appendJournalMarkdown, readJournal, } from '../agent-blackboard/index.mjs';
|
|
2
2
|
import { optionalRecord, optionalString, requiredChoice, requiredString, requiredStringArray, rejectUnknown, } from './args.mjs';
|
|
3
3
|
import { outboxDirectory, readIdentity } from './context.mjs';
|
|
4
|
+
import { outboxCounts } from './handlers-outbox.mjs';
|
|
4
5
|
const MODES = ['interactive', 'autonomous'];
|
|
5
6
|
const OUTCOMES = [
|
|
6
7
|
'in-progress',
|
|
@@ -39,26 +40,35 @@ function readCoverage(args) {
|
|
|
39
40
|
export const journalAppend = async (args, context) => {
|
|
40
41
|
const identity = readIdentity(args, context.sessionId);
|
|
41
42
|
const mode = requiredChoice(args, 'mode', MODES);
|
|
42
|
-
|
|
43
|
-
// default here and returning it lets the caller do that.
|
|
44
|
-
const timestamp = optionalString(args, 'timestamp') ?? new Date().toISOString();
|
|
43
|
+
const sourceEventId = requiredString(args, 'sourceEventId');
|
|
45
44
|
const category = optionalString(args, 'category');
|
|
46
|
-
const
|
|
47
|
-
...identity,
|
|
48
|
-
mode,
|
|
49
|
-
// Interactive delivery needs a durable outbox; autonomous delivery forbids one.
|
|
50
|
-
...(mode === 'interactive' ? { outboxDirectory: outboxDirectory(context.worktree) } : {}),
|
|
45
|
+
const entry = {
|
|
51
46
|
markdown: requiredString(args, 'markdown'),
|
|
52
|
-
sourceEventId: requiredString(args, 'sourceEventId'),
|
|
53
47
|
workOutcome: requiredChoice(args, 'workOutcome', OUTCOMES),
|
|
54
48
|
repositories: requiredStringArray(args, 'repositories'),
|
|
55
49
|
feedbackCoverage: readCoverage(args),
|
|
56
|
-
|
|
50
|
+
};
|
|
51
|
+
// Interactive delivery needs a durable outbox; autonomous delivery forbids one.
|
|
52
|
+
const directory = mode === 'interactive' ? outboxDirectory(context.worktree) : undefined;
|
|
53
|
+
const result = await appendJournalMarkdown({
|
|
54
|
+
...identity,
|
|
55
|
+
...entry,
|
|
56
|
+
mode,
|
|
57
|
+
sourceEventId,
|
|
58
|
+
...(directory === undefined ? {} : { outboxDirectory: directory }),
|
|
57
59
|
...(category === undefined ? {} : { category }),
|
|
58
60
|
env: context.env,
|
|
59
61
|
dependencies: context.dependencies,
|
|
60
62
|
});
|
|
61
|
-
|
|
63
|
+
// The server owns the timestamp: a first append takes the current time, and a retry of the same
|
|
64
|
+
// event (same session, sourceEventId, and content) reports the one already stored or retained.
|
|
65
|
+
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
|
+
const counts = mode === 'interactive'
|
|
69
|
+
? outboxCounts(context.worktree, context.sessionId)
|
|
70
|
+
: { pendingCount: 0, worktreePendingCount: 0 };
|
|
71
|
+
return { sessionId: context.sessionId, ...result, timestamp, ...counts };
|
|
62
72
|
};
|
|
63
73
|
// `Entries.get` documents no order, and `createdAt` is a service-generated ISO 8601 UTC time whose
|
|
64
74
|
// text order is time order. An entry without a usable `createdAt` sorts first rather than throwing.
|
|
@@ -78,21 +88,3 @@ export const journalEntries = async (_args, context) => ({
|
|
|
78
88
|
sessionId: context.sessionId,
|
|
79
89
|
entries: (await readJournal(context.sessionId, context.env, context.dependencies)).toSorted(byCreatedAt),
|
|
80
90
|
});
|
|
81
|
-
export const outboxStatus = async (_args, context) => ({
|
|
82
|
-
sessionId: context.sessionId,
|
|
83
|
-
...feedbackOutboxStatus(outboxDirectory(context.worktree)),
|
|
84
|
-
});
|
|
85
|
-
export const outboxFlush = async (_args, context) => {
|
|
86
|
-
const directory = outboxDirectory(context.worktree);
|
|
87
|
-
// Flushing creates the outbox directory, so an empty or absent outbox is reported as is.
|
|
88
|
-
if (feedbackOutboxStatus(directory).pendingCount === 0)
|
|
89
|
-
return { sessionId: context.sessionId, status: 'empty', pendingCount: 0, deliveredCount: 0 };
|
|
90
|
-
return {
|
|
91
|
-
sessionId: context.sessionId,
|
|
92
|
-
...(await flushFeedbackOutbox({
|
|
93
|
-
directory,
|
|
94
|
-
env: context.env,
|
|
95
|
-
dependencies: context.dependencies,
|
|
96
|
-
})),
|
|
97
|
-
};
|
|
98
|
-
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ToolHandler } from './context.mts';
|
|
2
|
+
/**
|
|
3
|
+
* The outbox belongs to the worktree, not to one session, so two counts are reported. `pendingCount`
|
|
4
|
+
* is the caller's session only; `worktreePendingCount` is every session's records.
|
|
5
|
+
*/
|
|
6
|
+
export declare function outboxCounts(worktree: string, sessionId: string): {
|
|
7
|
+
pendingCount: number;
|
|
8
|
+
worktreePendingCount: number;
|
|
9
|
+
};
|
|
10
|
+
export declare const outboxStatus: ToolHandler;
|
|
11
|
+
export declare const outboxFlush: ToolHandler;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { flushFeedbackOutbox, readFeedbackOutbox } from '../agent-blackboard/index.mjs';
|
|
2
|
+
import { outboxDirectory } from './context.mjs';
|
|
3
|
+
/**
|
|
4
|
+
* The outbox belongs to the worktree, not to one session, so two counts are reported. `pendingCount`
|
|
5
|
+
* is the caller's session only; `worktreePendingCount` is every session's records.
|
|
6
|
+
*/
|
|
7
|
+
export function outboxCounts(worktree, sessionId) {
|
|
8
|
+
// A read that never creates the directory, so asking about an empty outbox leaves no trace.
|
|
9
|
+
const records = readFeedbackOutbox(outboxDirectory(worktree));
|
|
10
|
+
return {
|
|
11
|
+
pendingCount: records.filter((record) => record.identity.sessionId === sessionId).length,
|
|
12
|
+
worktreePendingCount: records.length,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function sessionStatus(pendingCount) {
|
|
16
|
+
return pendingCount === 0 ? 'empty' : 'pending';
|
|
17
|
+
}
|
|
18
|
+
export const outboxStatus = async (_args, context) => {
|
|
19
|
+
const counts = outboxCounts(context.worktree, context.sessionId);
|
|
20
|
+
return { sessionId: context.sessionId, status: sessionStatus(counts.pendingCount), ...counts };
|
|
21
|
+
};
|
|
22
|
+
export const outboxFlush = async (_args, context) => {
|
|
23
|
+
// Flushing creates the outbox directory, so an empty or absent outbox is reported as is.
|
|
24
|
+
if (outboxCounts(context.worktree, context.sessionId).worktreePendingCount === 0)
|
|
25
|
+
return {
|
|
26
|
+
sessionId: context.sessionId,
|
|
27
|
+
status: 'empty',
|
|
28
|
+
pendingCount: 0,
|
|
29
|
+
worktreePendingCount: 0,
|
|
30
|
+
deliveredCount: 0,
|
|
31
|
+
};
|
|
32
|
+
// The flush delivers every session's records; only the counts below are per session.
|
|
33
|
+
const flushed = await flushFeedbackOutbox({
|
|
34
|
+
directory: outboxDirectory(context.worktree),
|
|
35
|
+
env: context.env,
|
|
36
|
+
dependencies: context.dependencies,
|
|
37
|
+
});
|
|
38
|
+
const counts = outboxCounts(context.worktree, context.sessionId);
|
|
39
|
+
return {
|
|
40
|
+
sessionId: context.sessionId,
|
|
41
|
+
...flushed,
|
|
42
|
+
status: sessionStatus(counts.pendingCount),
|
|
43
|
+
...counts,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
@@ -29,5 +29,10 @@ export declare const JOURNAL_ARGS: {
|
|
|
29
29
|
sources: string[];
|
|
30
30
|
droppedCount: number;
|
|
31
31
|
};
|
|
32
|
-
|
|
32
|
+
};
|
|
33
|
+
/** The time `useFixedClock` pins, which is the timestamp the server mints for a first append. */
|
|
34
|
+
export declare const NOW = "2026-01-01T00:00:00.000Z";
|
|
35
|
+
/** Pins `Date` only, so timers and child processes keep running. */
|
|
36
|
+
export declare function useFixedClock(): {
|
|
37
|
+
advance: (milliseconds: number) => void;
|
|
33
38
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { afterEach, beforeEach } from 'vitest';
|
|
2
|
+
import { afterEach, beforeEach, vi } from 'vitest';
|
|
3
3
|
import { callTool } from './dispatch.mjs';
|
|
4
4
|
import { BLACKBOARD_ENV, fakeBlackboard, } from './fake-blackboard.test-helpers.mjs';
|
|
5
5
|
import { createRepoFixture } from './git-fixture.test-helpers.mjs';
|
|
@@ -44,5 +44,17 @@ export const JOURNAL_ARGS = {
|
|
|
44
44
|
workOutcome: 'success',
|
|
45
45
|
repositories: ['owner/repo'],
|
|
46
46
|
feedbackCoverage: { status: 'complete', sources: ['tool-result'], droppedCount: 0 },
|
|
47
|
-
timestamp: '2026-01-01T00:00:00.000Z',
|
|
48
47
|
};
|
|
48
|
+
/** The time `useFixedClock` pins, which is the timestamp the server mints for a first append. */
|
|
49
|
+
export const NOW = '2026-01-01T00:00:00.000Z';
|
|
50
|
+
/** Pins `Date` only, so timers and child processes keep running. */
|
|
51
|
+
export function useFixedClock() {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.useFakeTimers({ toFake: ['Date'] });
|
|
54
|
+
vi.setSystemTime(new Date(NOW));
|
|
55
|
+
});
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
vi.useRealTimers();
|
|
58
|
+
});
|
|
59
|
+
return { advance: (milliseconds) => vi.setSystemTime(new Date(Date.now() + milliseconds)) };
|
|
60
|
+
}
|
|
@@ -3,7 +3,11 @@ import { TOOLS } from './tools.mjs';
|
|
|
3
3
|
export const SERVER_NAME = 'vouchington-tooling';
|
|
4
4
|
const INSTRUCTIONS = 'Journals agent-blackboard feedback for a vouchington consumer. Every tool takes an explicit ' +
|
|
5
5
|
'sessionId and an optional worktree (an absolute path listed by `git worktree list` for the ' +
|
|
6
|
-
'repository this server was launched from).
|
|
6
|
+
'repository this server was launched from). Hosts prefix the tool names differently: Claude ' +
|
|
7
|
+
'Code mcp__vouchington-tooling__<tool>, Codex mcp__vouchington_tooling__<tool>, Grok ' +
|
|
8
|
+
'vouchington-tooling__<tool> (through search_tool and use_tool), Cursor the vouchington-tooling ' +
|
|
9
|
+
'namespace (through GetDynamicTools and CallDynamicTool). Search for journal_append before ' +
|
|
10
|
+
'concluding this server is unavailable. Tool errors are actionable; do not fall back to a CLI.';
|
|
7
11
|
/**
|
|
8
12
|
* Builds the server. The tool list and call handlers go on the low-level `server` because the
|
|
9
13
|
* high-level `registerTool` path needs zod schemas, and this package keeps zod out of its own code.
|