vouchington-tooling 0.24.0 → 0.25.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.
Files changed (63) hide show
  1. package/README.md +43 -0
  2. package/dist/agent-blackboard/client.d.mts +5 -0
  3. package/dist/agent-blackboard/client.mjs +1 -1
  4. package/dist/agent-blackboard/feedback-audit-source.d.mts +13 -0
  5. package/dist/agent-blackboard/feedback-audit-source.mjs +30 -0
  6. package/dist/agent-blackboard/feedback-compose.d.mts +5 -1
  7. package/dist/agent-blackboard/feedback-compose.mjs +8 -18
  8. package/dist/agent-blackboard/feedback-journal-audit.d.mts +20 -0
  9. package/dist/agent-blackboard/feedback-journal-audit.mjs +29 -0
  10. package/dist/agent-blackboard/feedback-journal-sandbox.d.mts +5 -0
  11. package/dist/agent-blackboard/feedback-journal-sandbox.mjs +40 -0
  12. package/dist/agent-blackboard/index.d.mts +9 -8
  13. package/dist/agent-blackboard/index.mjs +7 -34
  14. package/dist/agent-blackboard/journal.d.mts +21 -0
  15. package/dist/agent-blackboard/journal.mjs +45 -0
  16. package/dist/agent-blackboard/session-tools.d.mts +36 -0
  17. package/dist/agent-blackboard/session-tools.mjs +41 -0
  18. package/dist/cli/commands/mcp.d.mts +5 -0
  19. package/dist/cli/commands/mcp.mjs +18 -0
  20. package/dist/cli/index.mjs +5 -5
  21. package/dist/cli/installed-version.d.mts +1 -0
  22. package/dist/cli/installed-version.mjs +5 -0
  23. package/dist/cli/parse-forwarded-commands.d.mts +3 -0
  24. package/dist/cli/parse-forwarded-commands.mjs +2 -0
  25. package/dist/cli/parse.d.mts +3 -0
  26. package/dist/cli/usage.d.mts +1 -1
  27. package/dist/cli/usage.mjs +6 -0
  28. package/dist/mcp-server/args.d.mts +9 -0
  29. package/dist/mcp-server/args.mjs +48 -0
  30. package/dist/mcp-server/context.d.mts +20 -0
  31. package/dist/mcp-server/context.mjs +23 -0
  32. package/dist/mcp-server/dispatch.d.mts +18 -0
  33. package/dist/mcp-server/dispatch.mjs +69 -0
  34. package/dist/mcp-server/fake-blackboard.test-helpers.d.mts +38 -0
  35. package/dist/mcp-server/fake-blackboard.test-helpers.mjs +75 -0
  36. package/dist/mcp-server/git-fixture.test-helpers.d.mts +16 -0
  37. package/dist/mcp-server/git-fixture.test-helpers.mjs +35 -0
  38. package/dist/mcp-server/handlers-journal.d.mts +5 -0
  39. package/dist/mcp-server/handlers-journal.mjs +81 -0
  40. package/dist/mcp-server/handlers-session.d.mts +4 -0
  41. package/dist/mcp-server/handlers-session.mjs +47 -0
  42. package/dist/mcp-server/harness.test-helpers.d.mts +33 -0
  43. package/dist/mcp-server/harness.test-helpers.mjs +48 -0
  44. package/dist/mcp-server/run.d.mts +20 -0
  45. package/dist/mcp-server/run.mjs +21 -0
  46. package/dist/mcp-server/sdk.d.mts +17 -0
  47. package/dist/mcp-server/sdk.mjs +33 -0
  48. package/dist/mcp-server/server.d.mts +9 -0
  49. package/dist/mcp-server/server.mjs +17 -0
  50. package/dist/mcp-server/tools.d.mts +19 -0
  51. package/dist/mcp-server/tools.mjs +118 -0
  52. package/dist/mcp-server/worktree.d.mts +18 -0
  53. package/dist/mcp-server/worktree.mjs +58 -0
  54. package/dist/session-friction/audit-block.d.mts +4 -0
  55. package/dist/session-friction/audit-block.mjs +45 -0
  56. package/dist/session-friction/ci-failures.d.mts +2 -1
  57. package/dist/session-friction/ci-failures.mjs +10 -46
  58. package/dist/session-friction/journal.d.mts +14 -0
  59. package/dist/session-friction/journal.mjs +66 -0
  60. package/dist/session-friction/report.mjs +10 -74
  61. package/docs/mcp-server.md +127 -0
  62. package/package.json +7 -2
  63. package/skills/stacked-prs/SKILL.md +15 -1
package/README.md CHANGED
@@ -19,8 +19,13 @@ npm install @typescript/typescript6
19
19
  npm install vitest
20
20
  # optional, only for vouchington-tooling/agent-blackboard and agent-blackboard CLI commands
21
21
  npm install agent-blackboard@^0.6.0
22
+ # optional, only for `vouchington mcp` (zod is a peer of the SDK)
23
+ npm install @modelcontextprotocol/sdk zod
22
24
  ```
23
25
 
26
+ [`vouchington mcp`](docs/mcp-server.md) serves the agent-blackboard journal tools to an agent over
27
+ stdio, with Claude, Codex, and Cursor registration examples.
28
+
24
29
  ## CLI
25
30
 
26
31
  ```bash
@@ -70,6 +75,7 @@ vouchington agent-blackboard journal append --session-id <uuid> --agent codex --
70
75
  vouchington agent-blackboard journal entries --session-id <uuid>
71
76
  vouchington agent-blackboard snapshot partition --snapshot <snapshot.jsonl> --checksum <sha256> --counts <counts.json>
72
77
  vouchington agent-blackboard snapshot cleanup --snapshot <snapshot.jsonl> --partition-directory <partitions-dir> --receipt <receipt-json>
78
+ vouchington mcp
73
79
  vouchington install-playwright-chromium-arm64
74
80
  vouchington ghcr-package-retention example%2Fapi
75
81
  vouchington harness-admission-lane 4
@@ -578,3 +584,40 @@ aggregate dropped counts below observed friction drops and retains larger caller
578
584
  double counting. Generated frontmatter carries validated `work_outcome` and `feedback_coverage`
579
585
  for consumers to preserve or reject conflicting explicit overrides; trusted mode stays external.
580
586
  Human top-five summaries do not authorize dropping additional durable findings.
587
+
588
+ The `## CI Failures` and `## Sandbox & Permission Audit` sections come from one optional audit
589
+ source; the two inputs are mutually exclusive and supplying both throws before any collector runs.
590
+ `friction: {directory, journalLoader, ...}` keeps reading the session-friction log together with the
591
+ journal and is unchanged. `journal: {journalLoader}` needs no friction directory or log: both
592
+ sections are assessed from the session's journal entries alone, and the report carries
593
+ `coverage.frictionStatus: 'journal-only'` (in place of `empty` or `events`) so the text states that
594
+ no log was observed. `feedbackCoverage.status: 'complete'` accepts either source. It is rejected
595
+ when neither is supplied (the error names the missing source), and a journal-only audit does not
596
+ count as assessed when the loader reports `not-found` (there is no log to establish that the session
597
+ was observed), throws, or is truncated by the scan bounds.
598
+
599
+ ```ts
600
+ await composeRetrospective({ ...input, journal: { journalLoader } })
601
+ ```
602
+
603
+ `journalLoader` is the session-friction `JournalLoader`; `JournalLoader`, `JournalLoadResult`,
604
+ `JournalEntry`, and `JournalAuditOptions` are exported as types from
605
+ `vouchington-tooling/agent-blackboard`. The `## CI Failures` block grammar and its three status
606
+ lines (`failures observed`, `none observed`, `unavailable (<reason>)`) are the same for both sources.
607
+ Journal-only sandbox and permission entries are journal Markdown entries that hold exactly one
608
+ block, escaped and length-bounded like CI failures, and other entries are ignored:
609
+
610
+ ```markdown
611
+ - `sandbox-escalation` — git push — network write blocked
612
+ - Outcome: requested | approved | denied | unknown
613
+ - Evidence: permission prompt shown
614
+ - Disposition: reran with approval
615
+ - `sandbox-failure` — node test — EPERM writing outside the worktree
616
+ - Evidence: stderr showed EPERM
617
+ - Disposition: moved output under TMPDIR
618
+ ```
619
+
620
+ `ambiguous-failure` takes the same shape as `sandbox-failure`. The section reports
621
+ `Status: none observed` or `Status: events observed`, each followed by
622
+ `(journal entries only; no friction log observed)`; a truncated scan reports
623
+ `unavailable (journal scan incomplete)` with the entries read so far.
@@ -15,6 +15,7 @@ export type BlackboardClientModule = {
15
15
  patch(input: unknown): Promise<unknown>;
16
16
  list(input: unknown): Promise<unknown>;
17
17
  get(id: string): Promise<unknown>;
18
+ archive?(sessionId: string): Promise<unknown>;
18
19
  };
19
20
  Entries: new (connection: BlackboardConnection) => {
20
21
  append(input: unknown): Promise<{
@@ -23,6 +24,9 @@ export type BlackboardClientModule = {
23
24
  }>;
24
25
  get(input: unknown): AsyncIterable<unknown>;
25
26
  };
27
+ Snapshots?: new (connection: BlackboardConnection) => {
28
+ export(options?: unknown): Promise<unknown>;
29
+ };
26
30
  };
27
31
  type BlackboardClientLoader = () => Promise<BlackboardClientModule>;
28
32
  export type BlackboardClientDependencies = {
@@ -31,4 +35,5 @@ export type BlackboardClientDependencies = {
31
35
  };
32
36
  export declare function resolveBlackboardConnection(env?: NodeJS.ProcessEnv): BlackboardConnection;
33
37
  export declare function loadClient(dependencies?: BlackboardClientDependencies): Promise<BlackboardClientModule>;
38
+ export declare function isMissingModuleError(error: unknown): boolean;
34
39
  export {};
@@ -34,7 +34,7 @@ async function defaultClientLoader(resolveFrom) {
34
34
  const specifier = pathToFileURL(consumerRequire.resolve('agent-blackboard')).href;
35
35
  return (await import(__rewriteRelativeImportExtension(specifier)));
36
36
  }
37
- function isMissingModuleError(error) {
37
+ export function isMissingModuleError(error) {
38
38
  return (error instanceof Error &&
39
39
  'code' in error &&
40
40
  (error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_FOUND'));
@@ -0,0 +1,13 @@
1
+ import { type SessionFrictionReport, type SessionFrictionReportOptions } from '../session-friction/index.mts';
2
+ import { type JournalAuditOptions, type JournalAuditReport } from './feedback-journal-audit.mts';
3
+ export type AuditSourceInput = {
4
+ friction?: SessionFrictionReportOptions;
5
+ journal?: JournalAuditOptions;
6
+ };
7
+ export type AuditReport = SessionFrictionReport | JournalAuditReport;
8
+ export declare function assertSingleAuditSource(input: AuditSourceInput): void;
9
+ /** Builds the CI-failure and sandbox sections from the friction log or from journal entries alone. */
10
+ export declare function buildAuditReport(sessionId: string, input: AuditSourceInput): Promise<AuditReport>;
11
+ /** Only a real log status or an explicit journal-only scan counts as assessed. */
12
+ export declare function auditAssessed({ coverage }: AuditReport): boolean;
13
+ export declare function completeCoverageError(input: AuditSourceInput): Error;
@@ -0,0 +1,30 @@
1
+ import { buildSessionFrictionReport, } from '../session-friction/index.mjs';
2
+ import { buildJournalAuditReport, } from './feedback-journal-audit.mjs';
3
+ const ASSESSED_SOURCE_ERROR = 'complete feedback coverage requires assessed available factual and finding sources';
4
+ const NOT_ASSESSED_MARKDOWN = '## CI Failures\nStatus: unavailable (not assessed)\n\n## Sandbox & Permission Audit\nStatus: unavailable (not assessed)';
5
+ export function assertSingleAuditSource(input) {
6
+ if (input.friction && input.journal)
7
+ throw new Error('composition accepts either a friction or a journal audit source, not both');
8
+ }
9
+ /** Builds the CI-failure and sandbox sections from the friction log or from journal entries alone. */
10
+ export async function buildAuditReport(sessionId, input) {
11
+ if (input.friction)
12
+ return buildSessionFrictionReport(sessionId, input.friction);
13
+ if (input.journal)
14
+ return buildJournalAuditReport(sessionId, input.journal);
15
+ return {
16
+ coverage: { journalStatus: 'unavailable', frictionStatus: 'absent', truncated: false },
17
+ markdown: NOT_ASSESSED_MARKDOWN,
18
+ };
19
+ }
20
+ /** Only a real log status or an explicit journal-only scan counts as assessed. */
21
+ export function auditAssessed({ coverage }) {
22
+ return (coverage.journalStatus === 'complete' &&
23
+ ['empty', 'events', 'journal-only'].includes(coverage.frictionStatus) &&
24
+ !coverage.truncated);
25
+ }
26
+ export function completeCoverageError(input) {
27
+ return new Error(input.friction || input.journal
28
+ ? ASSESSED_SOURCE_ERROR
29
+ : `${ASSESSED_SOURCE_ERROR} (no friction or journal source supplied for CI failures and sandbox audit)`);
30
+ }
@@ -1,6 +1,7 @@
1
1
  import { type RetrospectiveFactsOptions } from '../retrospective-facts/index.mts';
2
2
  import { type ResolveOptions } from '../retrospective-transcript/index.mts';
3
- import { type SessionFrictionReportOptions } from '../session-friction/index.mts';
3
+ import type { SessionFrictionReportOptions } from '../session-friction/index.mts';
4
+ import type { JournalAuditOptions } from './feedback-journal-audit.mts';
4
5
  import type { FeedbackCoverage, FeedbackReference, WorkOutcome } from './feedback-types.mts';
5
6
  export type FeedbackAssessment = {
6
7
  status: 'findings' | 'none-observed' | 'not-assessed' | 'unavailable';
@@ -28,7 +29,10 @@ export type RetrospectiveCompositionInput = {
28
29
  narrative: string;
29
30
  facts: Omit<RetrospectiveFactsOptions, 'raw'> | Unassessed;
30
31
  transcript: ResolveOptions | Unassessed;
32
+ /** CI failures and sandbox audit from a friction log plus journal. Exclusive with `journal`. */
31
33
  friction?: SessionFrictionReportOptions;
34
+ /** CI failures and sandbox audit from journal entries alone (`frictionStatus: 'journal-only'`). */
35
+ journal?: JournalAuditOptions;
32
36
  tools: FeedbackAssessment;
33
37
  architecture: FeedbackAssessment;
34
38
  knownSensitiveValues?: string[];
@@ -1,6 +1,6 @@
1
1
  import { runRetrospectiveFactsReport, } from '../retrospective-facts/index.mjs';
2
2
  import { runRetrospectiveTranscriptReport, } from '../retrospective-transcript/index.mjs';
3
- import { buildSessionFrictionReport, } from '../session-friction/index.mjs';
3
+ import { assertSingleAuditSource, auditAssessed, buildAuditReport, completeCoverageError, } from './feedback-audit-source.mjs';
4
4
  import { createFeedbackEnvelope, redactFeedbackText } from './feedback-codec.mjs';
5
5
  import { validateFeedbackText } from './feedback-fields.mjs';
6
6
  function assessment(title, value) {
@@ -32,6 +32,7 @@ function unavailable(marker, input) {
32
32
  return `${marker}\nStatus: ${input.status.replaceAll('-', ' ')} (${input.reason})`;
33
33
  }
34
34
  export async function composeRetrospective(input) {
35
+ assertSingleAuditSource(input);
35
36
  const unavailableFacts = 'status' in input.facts
36
37
  ? {
37
38
  markdown: unavailable('=== Retrospective Facts ===', input.facts),
@@ -44,34 +45,23 @@ export async function composeRetrospective(input) {
44
45
  coverage: input.transcript.status,
45
46
  }
46
47
  : undefined;
47
- const [facts, transcript, friction] = await Promise.all([
48
+ const [facts, transcript, audit] = await Promise.all([
48
49
  'status' in input.facts
49
50
  ? Promise.resolve(unavailableFacts)
50
51
  : runRetrospectiveFactsReport({ ...input.facts, raw: false }),
51
52
  'status' in input.transcript
52
53
  ? Promise.resolve(unavailableTranscript)
53
54
  : runRetrospectiveTranscriptReport(input.transcript),
54
- input.friction
55
- ? buildSessionFrictionReport(input.sessionId, input.friction)
56
- : Promise.resolve({
57
- coverage: {
58
- journalStatus: 'unavailable',
59
- frictionStatus: 'absent',
60
- truncated: false,
61
- },
62
- markdown: '## CI Failures\nStatus: unavailable (not assessed)\n\n## Sandbox & Permission Audit\nStatus: unavailable (not assessed)',
63
- }),
55
+ buildAuditReport(input.sessionId, input),
64
56
  ]);
65
57
  if (input.feedbackCoverage.status === 'complete' &&
66
58
  (facts.coverage !== 'complete' ||
67
59
  transcript.coverage !== 'complete' ||
68
60
  ['not-assessed', 'unavailable'].includes(input.tools.status) ||
69
61
  ['not-assessed', 'unavailable'].includes(input.architecture.status) ||
70
- friction.coverage.journalStatus !== 'complete' ||
71
- !['empty', 'events'].includes(friction.coverage.frictionStatus) ||
72
- friction.coverage.truncated))
73
- throw new Error('complete feedback coverage requires assessed available factual and finding sources');
74
- if ((friction.coverage.droppedCount ?? 0) > input.feedbackCoverage.droppedCount)
62
+ !auditAssessed(audit)))
63
+ throw completeCoverageError(input);
64
+ if ((audit.coverage.droppedCount ?? 0) > input.feedbackCoverage.droppedCount)
75
65
  throw new Error('feedback coverage dropped count must include observed friction drops');
76
66
  const markdown = [
77
67
  '---',
@@ -88,7 +78,7 @@ export async function composeRetrospective(input) {
88
78
  `## Outcome\nWork outcome: ${input.workOutcome}\nFeedback coverage: ${input.feedbackCoverage.status}\nDropped records: ${input.feedbackCoverage.droppedCount}`,
89
79
  `## Verifiable Facts\n${facts.markdown.trim()}`,
90
80
  `## Transcript Facts\n${transcript.markdown.trim()}`,
91
- friction.markdown,
81
+ audit.markdown,
92
82
  assessment('Tool Findings', input.tools),
93
83
  assessment('Architecture Findings', input.architecture),
94
84
  ].join('\n\n');
@@ -0,0 +1,20 @@
1
+ import type { JournalLoader, SessionFrictionCoverage } from '../session-friction/types.mts';
2
+ export type JournalAuditOptions = {
3
+ journalLoader: JournalLoader;
4
+ };
5
+ /** `journal-only` says both sections were assessed from journal entries, with no log observed. */
6
+ export type JournalAuditReport = {
7
+ coverage: {
8
+ journalStatus: SessionFrictionCoverage['journalStatus'];
9
+ frictionStatus: 'journal-only';
10
+ truncated: false;
11
+ droppedCount?: never;
12
+ };
13
+ markdown: string;
14
+ };
15
+ /**
16
+ * Builds `## CI Failures` and `## Sandbox & Permission Audit` from the session's journal entries
17
+ * alone. Unlike `buildSessionFrictionReport`, a session with no journal is unavailable rather than
18
+ * empty: without a log, nothing else establishes that the session was observed.
19
+ */
20
+ export declare function buildJournalAuditReport(sessionId: string, options: JournalAuditOptions): Promise<JournalAuditReport>;
@@ -0,0 +1,29 @@
1
+ import { buildCiFailuresSection, getConformingGroups, incompleteCiSection, } from '../session-friction/ci-failures.mjs';
2
+ import { scanJournal } from '../session-friction/journal.mjs';
3
+ import { validateSessionId } from '../session-friction/session-id.mjs';
4
+ import { getConformingSandboxBlocks, journalSandboxSection, unavailableSandboxSection, } from './feedback-journal-sandbox.mjs';
5
+ function report(journalStatus, markdown) {
6
+ return { coverage: { journalStatus, frictionStatus: 'journal-only', truncated: false }, markdown };
7
+ }
8
+ function unavailable(reason) {
9
+ return report('unavailable', `## CI Failures\nStatus: unavailable (${reason})\n\n${unavailableSandboxSection(reason)}`);
10
+ }
11
+ /**
12
+ * Builds `## CI Failures` and `## Sandbox & Permission Audit` from the session's journal entries
13
+ * alone. Unlike `buildSessionFrictionReport`, a session with no journal is unavailable rather than
14
+ * empty: without a log, nothing else establishes that the session was observed.
15
+ */
16
+ export async function buildJournalAuditReport(sessionId, options) {
17
+ validateSessionId(sessionId);
18
+ const scanned = await scanJournal(sessionId, options.journalLoader);
19
+ if (scanned.status === 'unreachable')
20
+ return unavailable('blackboard unreachable');
21
+ if (scanned.status === 'not-found')
22
+ return unavailable('no journal for session');
23
+ const ci = getConformingGroups(scanned.entries);
24
+ const sandbox = getConformingSandboxBlocks(scanned.entries);
25
+ if (scanned.truncated)
26
+ return report('partial', `${incompleteCiSection(ci)}\n\n${unavailableSandboxSection('journal scan incomplete', sandbox)}`);
27
+ const journal = { status: 'ok', markdownBlocks: ci, truncated: false };
28
+ return report('complete', `${buildCiFailuresSection(sessionId, journal, 'journal-only')}\n\n${journalSandboxSection(sandbox)}`);
29
+ }
@@ -0,0 +1,5 @@
1
+ import type { JournalEntry } from '../session-friction/types.mts';
2
+ /** Journal entries that hold exactly one sandbox or permission block, made paste-safe. */
3
+ export declare function getConformingSandboxBlocks(entries: Iterable<JournalEntry>): string[];
4
+ export declare function unavailableSandboxSection(reason: string, blocks?: string[]): string;
5
+ export declare function journalSandboxSection(blocks: string[]): string;
@@ -0,0 +1,40 @@
1
+ import { conformingBlocks, matchAuditBlock } from '../session-friction/audit-block.mjs';
2
+ const SANDBOX_SECTION_HEADER = '## Sandbox & Permission Audit';
3
+ const SOURCE = 'journal entries only; no friction log observed';
4
+ const EVIDENCE = /^ {2}- Evidence: .*[^\s]$/;
5
+ const DISPOSITION = /^ {2}- Disposition: .*[^\s]$/;
6
+ const ESCALATION = [
7
+ /^- `sandbox-escalation` — .+ — .*[^\s]$/,
8
+ /^ {2}- Outcome: (?:requested|approved|denied|unknown)$/,
9
+ EVIDENCE,
10
+ DISPOSITION,
11
+ ];
12
+ const FAILURE = [
13
+ /^- `(?:sandbox-failure|ambiguous-failure)` — .+ — .*[^\s]$/,
14
+ EVIDENCE,
15
+ DISPOSITION,
16
+ ];
17
+ const FIELD_PREFIXES = [
18
+ '- `sandbox-escalation` — ',
19
+ '- `sandbox-failure` — ',
20
+ '- `ambiguous-failure` — ',
21
+ ' - Outcome: ',
22
+ ' - Evidence: ',
23
+ ' - Disposition: ',
24
+ ];
25
+ function matchBlock(markdown) {
26
+ return (matchAuditBlock(markdown, ESCALATION, FIELD_PREFIXES) ??
27
+ matchAuditBlock(markdown, FAILURE, FIELD_PREFIXES));
28
+ }
29
+ /** Journal entries that hold exactly one sandbox or permission block, made paste-safe. */
30
+ export function getConformingSandboxBlocks(entries) {
31
+ return conformingBlocks(entries, matchBlock);
32
+ }
33
+ export function unavailableSandboxSection(reason, blocks = []) {
34
+ return [`${SANDBOX_SECTION_HEADER}\nStatus: unavailable (${reason})`, ...blocks].join('\n\n');
35
+ }
36
+ export function journalSandboxSection(blocks) {
37
+ if (blocks.length === 0)
38
+ return `${SANDBOX_SECTION_HEADER}\nStatus: none observed (${SOURCE})`;
39
+ return `${SANDBOX_SECTION_HEADER}\nStatus: events observed (${SOURCE})\n\n${blocks.join('\n\n')}`;
40
+ }
@@ -1,17 +1,16 @@
1
- import type { FeedbackCoverage, FeedbackDeliveryOptions, FeedbackDeliveryResult, FeedbackIdentity, WorkOutcome } from './feedback-types.mts';
1
+ import type { FeedbackDeliveryResult } from './feedback-types.mts';
2
+ import { type JournalInput } from './journal.mts';
2
3
  import { type BlackboardClientDependencies } from './client.mts';
3
4
  export { cleanupSnapshotPartitions, partitionSnapshot } from './snapshot.mts';
4
5
  export { assertSessionId } from './session-id.mts';
6
+ export { appendJournalMarkdown } from './journal.mts';
7
+ export type { JournalInput } from './journal.mts';
8
+ export { archiveBlackboardSession, ensureBlackboardSession, exportSnapshot, } from './session-tools.mts';
9
+ export type { EnsureSessionOutcome, SnapshotExportOutcome } from './session-tools.mts';
5
10
  export type * from './snapshot-types.mts';
6
11
  export declare function probeBlackboard(env?: NodeJS.ProcessEnv, dependencies?: BlackboardClientDependencies): Promise<void>;
7
- export declare function appendJournal(input: Omit<FeedbackDeliveryOptions, 'identity' | 'envelope'> & FeedbackIdentity & {
8
- repositories: string[];
12
+ export declare function appendJournal(input: JournalInput & {
9
13
  markdownFile: string;
10
- sourceEventId: string;
11
- workOutcome: WorkOutcome;
12
- feedbackCoverage: FeedbackCoverage;
13
- timestamp?: string;
14
- category?: string;
15
14
  }): Promise<FeedbackDeliveryResult>;
16
15
  export declare function readJournal(sessionId: string, env?: NodeJS.ProcessEnv, dependencies?: BlackboardClientDependencies): Promise<unknown[]>;
17
16
  export declare function formatJournalEntries(sessionId: string, entries: unknown[]): string;
@@ -23,3 +22,5 @@ export { writeFeedback, verifyFreshFeedback, flushFeedbackOutbox, FeedbackDelive
23
22
  export { feedbackOutboxStatus } from './feedback-outbox.mts';
24
23
  export { composeRetrospective } from './feedback-compose.mts';
25
24
  export type { RetrospectiveCompositionInput, FeedbackAssessment } from './feedback-compose.mts';
25
+ export type { JournalAuditOptions } from './feedback-journal-audit.mts';
26
+ export type { JournalEntry, JournalLoader, JournalLoadResult } from '../session-friction/types.mts';
@@ -1,55 +1,28 @@
1
- import { createFeedbackEnvelope, normalizeRepositories } from './feedback-codec.mjs';
2
- import { writeFeedback } from './feedback-delivery.mjs';
1
+ import { appendJournalMarkdown, checkJournalInput } from './journal.mjs';
3
2
  import { loadClient, resolveBlackboardConnection, } from './client.mjs';
4
3
  import { readFile } from 'node:fs/promises';
5
4
  import { assertSessionId } from './session-id.mjs';
6
5
  export { cleanupSnapshotPartitions, partitionSnapshot } from './snapshot.mjs';
7
6
  export { assertSessionId } from './session-id.mjs';
7
+ export { appendJournalMarkdown } from './journal.mjs';
8
+ export { archiveBlackboardSession, ensureBlackboardSession, exportSnapshot, } from './session-tools.mjs';
8
9
  export async function probeBlackboard(env, dependencies) {
9
10
  const { Sessions } = await loadClient(dependencies);
10
11
  await new Sessions(resolveBlackboardConnection(env)).list({ limit: 1 });
11
12
  }
12
13
  export async function appendJournal(input) {
13
- assertSessionId(input.sessionId);
14
- if (input.parentSessionId != null)
15
- assertSessionId(input.parentSessionId, 'parent session id');
16
- const repositories = normalizeRepositories(input.repositories);
17
- const timestamp = input.timestamp === undefined ? new Date() : new Date(input.timestamp);
18
- if (Number.isNaN(timestamp.valueOf()))
19
- throw new Error('journal timestamp is not a valid date-time');
14
+ const { markdownFile, ...journal } = input;
15
+ checkJournalInput(journal);
20
16
  let markdown;
21
17
  try {
22
- markdown = new TextDecoder('utf-8', { fatal: true }).decode(await readFile(input.markdownFile));
18
+ markdown = new TextDecoder('utf-8', { fatal: true }).decode(await readFile(markdownFile));
23
19
  }
24
20
  catch {
25
21
  throw new Error('note file is not valid UTF-8 or cannot be read');
26
22
  }
27
23
  if (!markdown)
28
24
  throw new Error('note file is empty');
29
- return writeFeedback({
30
- identity: {
31
- sessionId: input.sessionId,
32
- parentSessionId: input.parentSessionId,
33
- agent: input.agent,
34
- version: input.version,
35
- },
36
- envelope: createFeedbackEnvelope({
37
- schemaVersion: 1,
38
- type: 'journal',
39
- sourceEventId: input.sourceEventId,
40
- timestamp: timestamp.toISOString(),
41
- repositories,
42
- markdown,
43
- workOutcome: input.workOutcome,
44
- feedbackCoverage: input.feedbackCoverage,
45
- ...(input.category === undefined ? {} : { category: input.category }),
46
- }),
47
- mode: input.mode,
48
- ...(input.outboxDirectory === undefined ? {} : { outboxDirectory: input.outboxDirectory }),
49
- ...(input.env === undefined ? {} : { env: input.env }),
50
- ...(input.dependencies === undefined ? {} : { dependencies: input.dependencies }),
51
- ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
52
- });
25
+ return appendJournalMarkdown({ ...journal, markdown });
53
26
  }
54
27
  export async function readJournal(sessionId, env, dependencies) {
55
28
  assertSessionId(sessionId);
@@ -0,0 +1,21 @@
1
+ import type { FeedbackCoverage, FeedbackDeliveryOptions, FeedbackDeliveryResult, FeedbackIdentity, WorkOutcome } from './feedback-types.mts';
2
+ export type JournalInput = Omit<FeedbackDeliveryOptions, 'identity' | 'envelope'> & FeedbackIdentity & {
3
+ repositories: string[];
4
+ sourceEventId: string;
5
+ workOutcome: WorkOutcome;
6
+ feedbackCoverage: FeedbackCoverage;
7
+ timestamp?: string;
8
+ category?: string;
9
+ };
10
+ /** Validates the identity, repositories, and timestamp before any note text is read. */
11
+ export declare function checkJournalInput(input: JournalInput): {
12
+ repositories: string[];
13
+ timestamp: Date;
14
+ };
15
+ /**
16
+ * Appends a journal entry whose markdown is already in memory. `appendJournal` reads a note file
17
+ * and delegates here; callers that hold the text (for example the MCP server) skip the file.
18
+ */
19
+ export declare function appendJournalMarkdown(input: JournalInput & {
20
+ markdown: string;
21
+ }): Promise<FeedbackDeliveryResult>;
@@ -0,0 +1,45 @@
1
+ import { createFeedbackEnvelope, normalizeRepositories } from './feedback-codec.mjs';
2
+ import { writeFeedback } from './feedback-delivery.mjs';
3
+ import { assertSessionId } from './session-id.mjs';
4
+ /** Validates the identity, repositories, and timestamp before any note text is read. */
5
+ export function checkJournalInput(input) {
6
+ assertSessionId(input.sessionId);
7
+ if (input.parentSessionId != null)
8
+ assertSessionId(input.parentSessionId, 'parent session id');
9
+ const repositories = normalizeRepositories(input.repositories);
10
+ const timestamp = input.timestamp === undefined ? new Date() : new Date(input.timestamp);
11
+ if (Number.isNaN(timestamp.valueOf()))
12
+ throw new Error('journal timestamp is not a valid date-time');
13
+ return { repositories, timestamp };
14
+ }
15
+ /**
16
+ * Appends a journal entry whose markdown is already in memory. `appendJournal` reads a note file
17
+ * and delegates here; callers that hold the text (for example the MCP server) skip the file.
18
+ */
19
+ export async function appendJournalMarkdown(input) {
20
+ const { repositories, timestamp } = checkJournalInput(input);
21
+ return writeFeedback({
22
+ identity: {
23
+ sessionId: input.sessionId,
24
+ parentSessionId: input.parentSessionId,
25
+ agent: input.agent,
26
+ version: input.version,
27
+ },
28
+ envelope: createFeedbackEnvelope({
29
+ schemaVersion: 1,
30
+ type: 'journal',
31
+ sourceEventId: input.sourceEventId,
32
+ timestamp: timestamp.toISOString(),
33
+ repositories,
34
+ markdown: input.markdown,
35
+ workOutcome: input.workOutcome,
36
+ feedbackCoverage: input.feedbackCoverage,
37
+ ...(input.category === undefined ? {} : { category: input.category }),
38
+ }),
39
+ mode: input.mode,
40
+ ...(input.outboxDirectory === undefined ? {} : { outboxDirectory: input.outboxDirectory }),
41
+ ...(input.env === undefined ? {} : { env: input.env }),
42
+ ...(input.dependencies === undefined ? {} : { dependencies: input.dependencies }),
43
+ ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),
44
+ });
45
+ }
@@ -0,0 +1,36 @@
1
+ import { type BlackboardClientDependencies } from './client.mts';
2
+ import type { FeedbackIdentity } from './feedback-types.mts';
3
+ import type { SnapshotChecksum, SnapshotCounts, SnapshotManifest, SnapshotSelection } from './snapshot-types.mts';
4
+ type ClientOptions = {
5
+ env?: NodeJS.ProcessEnv;
6
+ dependencies?: BlackboardClientDependencies;
7
+ };
8
+ export type EnsureSessionOutcome = {
9
+ sessionId: string;
10
+ status: 'created' | 'exists';
11
+ archived: boolean;
12
+ };
13
+ export type SnapshotExportOutcome = {
14
+ path: string;
15
+ counts: SnapshotCounts;
16
+ checksum: SnapshotChecksum;
17
+ manifest: SnapshotManifest;
18
+ cleanupToken?: string;
19
+ };
20
+ /** Idempotently creates the session; an existing session must match the supplied identity. */
21
+ export declare function ensureBlackboardSession(input: FeedbackIdentity & ClientOptions): Promise<EnsureSessionOutcome>;
22
+ /** Marks a session distilled. Metadata becomes immutable; entries stay appendable. */
23
+ export declare function archiveBlackboardSession(input: {
24
+ sessionId: string;
25
+ } & ClientOptions): Promise<{
26
+ sessionId: string;
27
+ archived: true;
28
+ }>;
29
+ /**
30
+ * Exports a snapshot into a private file the client generates. Callers cannot choose the
31
+ * destination, so a long-lived process with credentials never writes to an agent-chosen path.
32
+ */
33
+ export declare function exportSnapshot(input: {
34
+ selection?: SnapshotSelection;
35
+ } & ClientOptions): Promise<SnapshotExportOutcome>;
36
+ export {};
@@ -0,0 +1,41 @@
1
+ import { loadClient, resolveBlackboardConnection, } from './client.mjs';
2
+ import { validateFeedbackIdentity } from './feedback-identity.mjs';
3
+ import { assertSessionId } from './session-id.mjs';
4
+ import { isObject } from './snapshot-partition-guards.mjs';
5
+ const UPGRADE_HINT = 'upgrade agent-blackboard in the consumer package';
6
+ /** Idempotently creates the session; an existing session must match the supplied identity. */
7
+ export async function ensureBlackboardSession(input) {
8
+ const { sessionId, parentSessionId, agent, version } = input;
9
+ validateFeedbackIdentity({ sessionId, parentSessionId, agent, version });
10
+ const { Sessions } = await loadClient(input.dependencies);
11
+ const { status, session } = await new Sessions(resolveBlackboardConnection(input.env)).ensure({
12
+ id: sessionId,
13
+ parentSessionId,
14
+ agent,
15
+ version,
16
+ });
17
+ return { sessionId, status, archived: session.archivedAt != null };
18
+ }
19
+ /** Marks a session distilled. Metadata becomes immutable; entries stay appendable. */
20
+ export async function archiveBlackboardSession(input) {
21
+ assertSessionId(input.sessionId);
22
+ const { Sessions } = await loadClient(input.dependencies);
23
+ const sessions = new Sessions(resolveBlackboardConnection(input.env));
24
+ if (typeof sessions.archive !== 'function')
25
+ throw new Error(`the installed agent-blackboard client cannot archive sessions; ${UPGRADE_HINT}`);
26
+ await sessions.archive(input.sessionId);
27
+ return { sessionId: input.sessionId, archived: true };
28
+ }
29
+ /**
30
+ * Exports a snapshot into a private file the client generates. Callers cannot choose the
31
+ * destination, so a long-lived process with credentials never writes to an agent-chosen path.
32
+ */
33
+ export async function exportSnapshot(input) {
34
+ const { Snapshots } = await loadClient(input.dependencies);
35
+ if (!Snapshots)
36
+ throw new Error(`the installed agent-blackboard client cannot export snapshots; ${UPGRADE_HINT}`);
37
+ const result = await new Snapshots(resolveBlackboardConnection(input.env)).export(input.selection === undefined ? {} : { selection: input.selection });
38
+ if (!isObject(result) || typeof result.path !== 'string')
39
+ throw new Error('agent-blackboard returned an unexpected snapshot export result');
40
+ return result;
41
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * `vouchington mcp`: serves the agent-blackboard journal tools over stdio. It resolves once the
3
+ * server is connected; the open stdin keeps the process alive until the client disconnects.
4
+ */
5
+ export declare function runMcpCommand(args: string[]): Promise<number>;
@@ -0,0 +1,18 @@
1
+ import { runMcpServer } from '../../mcp-server/run.mjs';
2
+ import { readInstalledVersion } from '../installed-version.mjs';
3
+ /**
4
+ * `vouchington mcp`: serves the agent-blackboard journal tools over stdio. It resolves once the
5
+ * server is connected; the open stdin keeps the process alive until the client disconnects.
6
+ */
7
+ export async function runMcpCommand(args) {
8
+ try {
9
+ if (args.length > 0)
10
+ throw new Error('usage: vouchington mcp (takes no arguments; it serves stdio)');
11
+ await runMcpServer({ cwd: process.cwd(), env: process.env, version: readInstalledVersion() });
12
+ return 0;
13
+ }
14
+ catch (error) {
15
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
16
+ return 1;
17
+ }
18
+ }
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, realpathSync } from 'node:fs';
2
+ import { realpathSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { runAgentHarnessConfigCli } from '../agent-harness-config/cli.mjs';
6
6
  import { runPostReviewCli } from '../gha-post-review/cli.mjs';
7
7
  import { runStageReviewPayloadCli } from '../gha-review-payload/cli.mjs';
8
- import { readPackageVersion } from '../package-version.mjs';
9
8
  import { runSwiftSemanticEqualCli } from '../swift-semantic-equal/cli.mjs';
10
9
  import { runGhaArtifactsCleanup } from './commands/gha-artifacts-cleanup.mjs';
11
10
  import { runGhaRuntimeAudit } from './commands/gha-runtime-audit.mjs';
@@ -21,11 +20,13 @@ import { runRetrospectiveTranscriptCommand } from './commands/retrospective-tran
21
20
  import { runLinkSkill } from './commands/link-skill.mjs';
22
21
  import { runRetrospectiveFactsCommand } from './commands/retrospective-facts.mjs';
23
22
  import { runAgentBlackboardCommand } from './commands/agent-blackboard.mjs';
23
+ import { runMcpCommand } from './commands/mcp.mjs';
24
24
  import { runWithHostLock } from './commands/with-host-lock.mjs';
25
25
  import { runRequireUpToDate } from './commands/require-up-to-date.mjs';
26
26
  import { runAstGrepExamplesCommand } from './commands/ast-grep-examples.mjs';
27
27
  import { runAstGrepPackCommand } from './commands/ast-grep-pack.mjs';
28
28
  import { runGhaWorkspacePolicy } from './commands/gha-workspace-policy.mjs';
29
+ import { readInstalledVersion } from './installed-version.mjs';
29
30
  import { parseCli } from './parse.mjs';
30
31
  import { packageScriptPath } from './script-path.mjs';
31
32
  import { commandUsage, printUsage } from './usage.mjs';
@@ -129,6 +130,8 @@ export function runCli(argv = process.argv) {
129
130
  return runRetrospectiveFactsCommand(parsed.args);
130
131
  case 'agent-blackboard':
131
132
  return runAgentBlackboardCommand(parsed.args);
133
+ case 'mcp':
134
+ return runMcpCommand(parsed.args);
132
135
  case 'require-up-to-date':
133
136
  return runRequireUpToDate(parsed);
134
137
  case 'gitleaks-directory-scan':
@@ -145,9 +148,6 @@ export function runCli(argv = process.argv) {
145
148
  return runGhaWorkspacePolicy(parsed);
146
149
  }
147
150
  }
148
- function readInstalledVersion() {
149
- return readPackageVersion(JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')));
150
- }
151
151
  export function isMainModule(metaUrl, argv1) {
152
152
  if (argv1 === undefined)
153
153
  return false;
@@ -0,0 +1 @@
1
+ export declare function readInstalledVersion(): string;