vouchington-tooling 0.18.1 → 0.19.1

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 CHANGED
@@ -10,7 +10,7 @@ npm install @libpg-query/parser
10
10
  # (classic compiler API; typescript@7's package root is version-only)
11
11
  npm install @typescript/typescript6
12
12
  # optional, only for vouchington-tooling/agent-blackboard and agent-blackboard CLI commands
13
- npm install agent-blackboard@^0.5.0
13
+ npm install agent-blackboard@^0.6.0
14
14
  ```
15
15
 
16
16
  ## CLI
@@ -34,6 +34,7 @@ vouchington gha-output name
34
34
  vouchington gha-needs-results
35
35
  vouchington download-with-diagnostics <url> <destination>
36
36
  vouchington download-optional-run-artifacts --pattern 'coverage-*' --dir ./coverage-fallback
37
+ vouchington download-optional-run-artifacts --name coverage-tooling --name coverage-web --dir ./coverage-fallback
37
38
  vouchington host-pressure-diagnostics
38
39
  vouchington allocate-browser-safe-ports 2 --policy ./policy.json --forbidden-ports ./ports.json
39
40
  vouchington diagnose-port-collision --ports "2200 2216"
@@ -57,7 +58,7 @@ vouchington wait-for-apt-locks
57
58
  vouchington retrospective-transcript --jsonl /path/to/transcript.jsonl
58
59
  vouchington retrospective-facts --pr 49 --repo vouchington/vouchington-infra --raw
59
60
  vouchington agent-blackboard probe
60
- vouchington agent-blackboard journal append --session-id <uuid> --agent codex --version 1 --file note.md
61
+ vouchington agent-blackboard journal append --session-id <uuid> --agent codex --version 1 --file note.md --repository vouchington/vouchington-tooling
61
62
  vouchington agent-blackboard journal entries --session-id <uuid>
62
63
  vouchington agent-blackboard snapshot partition --snapshot <snapshot.jsonl> --checksum <sha256> --counts <counts.json>
63
64
  vouchington agent-blackboard snapshot cleanup --snapshot <snapshot.jsonl> --partition-directory <partitions-dir> --receipt <receipt-json>
@@ -91,6 +92,18 @@ download`), and extracts each selected name into its own directory. Ordinary abs
91
92
  `availability=unavailable`. Artifact listing retries up to three times with bounded backoff;
92
93
  exhausted transport errors, invalid names, and cancellation remain hard failures.
93
94
 
95
+ Name mode takes one or more `--name <artifact>` flags (mutually exclusive with `--pattern`), lists
96
+ the run's artifacts once, and extracts every requested name that is present into `<dir>/<name>`.
97
+ Names are matched literally, in request order, with repeats downloaded once; empty names, `.`, `..`,
98
+ and names containing `/`, `\`, or a line break are rejected before anything is listed. Requested
99
+ names absent from the run are expected (callers pass a superset such as retry-attempt variants) and
100
+ are skipped with a single bounded `::notice::` (the absent count, the first three names, then
101
+ `and N more`) and never one line per name. `availability=available` means at least one requested
102
+ artifact was downloaded; `availability=unavailable` (exit 0) means none was
103
+ present. A present artifact whose download fails is a hard failure: the helper stops at the first
104
+ one, prints `download failed artifact=<name> exit=<n>`, exits non-zero (a downloader exit of 3 is
105
+ reported as 1 so it cannot be mistaken for absence), and writes no `availability` output.
106
+
94
107
  `require-up-to-date` fetches the requested remote branch and fails unless its fetched tip is an
95
108
  ancestor of `HEAD`. `gitleaks-directory-scan` builds and scans isolated staged-index and current
96
109
  nonignored-working-tree mirrors with an explicit config; `--directory` selects the repository root.
@@ -125,7 +138,10 @@ GitHub reports a merged PR whose `baseRefName` is `main`; a merged PR into anoth
125
138
  as not merged to main, and a missing base is unavailable.
126
139
 
127
140
  Agent Blackboard support is optional: only the `agent-blackboard` subpath and its CLI commands
128
- need `agent-blackboard@^0.5.0`. Snapshot cleanup accepts only package-generated temporary paths.
141
+ need `agent-blackboard@^0.6.0`. Snapshot cleanup accepts only package-generated temporary paths.
142
+ `appendJournal` requires a `repositories: string[]` argument. The CLI accepts one or more
143
+ `--repository owner/name` flags; it records the entry's exact repositories and updates the session's
144
+ cumulative repository list before appending.
129
145
  Programmatic callers launched from a different workspace directory pass their own module URL as
130
146
  `dependencies: { resolveFrom: import.meta.url }`; the CLI defaults to the current package context.
131
147
  It captures a target under a private tombstone, validates partition names, permissions, JSONL,
@@ -10,7 +10,12 @@ export type BlackboardClientModule = {
10
10
  Sessions: new (connection: BlackboardConnection) => {
11
11
  ensure(input: unknown): Promise<{
12
12
  status: 'created' | 'exists';
13
+ session: {
14
+ data: Record<string, unknown>;
15
+ archivedAt?: string | null;
16
+ };
13
17
  }>;
18
+ patch(input: unknown): Promise<unknown>;
14
19
  list(input: unknown): Promise<unknown>;
15
20
  get(id: string): Promise<unknown>;
16
21
  };
@@ -32,6 +37,7 @@ export declare function appendJournal(input: {
32
37
  sessionId: string;
33
38
  agent: string;
34
39
  version: string;
40
+ repositories: string[];
35
41
  markdownFile: string;
36
42
  parentSessionId?: string | null;
37
43
  timestamp?: string;
@@ -30,6 +30,7 @@ export async function appendJournal(input) {
30
30
  assertSessionId(input.sessionId);
31
31
  if (input.parentSessionId != null)
32
32
  assertSessionId(input.parentSessionId, 'parent session id');
33
+ const repositories = normalizeRepositories(input.repositories, true);
33
34
  const timestamp = input.timestamp === undefined ? new Date() : new Date(input.timestamp);
34
35
  if (Number.isNaN(timestamp.valueOf()))
35
36
  throw new Error('journal timestamp is not a valid date-time');
@@ -44,18 +45,42 @@ export async function appendJournal(input) {
44
45
  throw new Error(`note file is empty: ${input.markdownFile}`);
45
46
  const connection = resolveBlackboardConnection(input.env);
46
47
  const { Sessions, Entries } = await loadClient(input.dependencies);
47
- await new Sessions(connection).ensure({
48
+ const sessions = new Sessions(connection);
49
+ const ensured = await sessions.ensure({
48
50
  id: input.sessionId,
49
51
  parentSessionId: input.parentSessionId ?? null,
50
52
  agent: input.agent,
51
53
  version: input.version,
52
54
  });
55
+ if (ensured.session.archivedAt != null)
56
+ throw new Error(`session is archived; create a new session: ${input.sessionId}`);
57
+ const current = ensured.session.data.repositories;
58
+ const cumulative = normalizeRepositories(current, false);
59
+ const merged = [...new Set([...cumulative, ...repositories])].sort();
60
+ if (JSON.stringify(current) !== JSON.stringify(merged))
61
+ await sessions.patch({ sessionId: input.sessionId, data: { repositories: merged } });
53
62
  const entry = await new Entries(connection).append({
54
63
  sessionId: input.sessionId,
55
- data: { type: 'journal', markdown, timestamp: timestamp.toISOString() },
64
+ data: { type: 'journal', markdown, timestamp: timestamp.toISOString(), repositories },
56
65
  });
57
66
  return `Journaled to agent-blackboard session ${input.sessionId} (entry created at ${entry.createdAt}).`;
58
67
  }
68
+ function normalizeRepositories(value, required) {
69
+ if (value === undefined && !required)
70
+ return [];
71
+ if (!Array.isArray(value) || (required && value.length === 0))
72
+ throw new Error('repositories must be a non-empty array of owner/name strings');
73
+ const repositories = [];
74
+ for (const candidate of value) {
75
+ if (typeof candidate !== 'string')
76
+ throw new Error('repositories must be a non-empty array of owner/name strings');
77
+ const repository = candidate.trim().toLowerCase();
78
+ if (!/^[a-z0-9-]+\/[a-z0-9._-]+$/.test(repository))
79
+ throw new Error(`invalid repository: ${candidate}`);
80
+ repositories.push(repository);
81
+ }
82
+ return [...new Set(repositories)].sort();
83
+ }
59
84
  export async function readJournal(sessionId, env, dependencies) {
60
85
  assertSessionId(sessionId);
61
86
  const { Entries } = await loadClient(dependencies);
@@ -21,6 +21,11 @@ function isSelection(value) {
21
21
  return false;
22
22
  if (value.data !== undefined && !isObject(value.data))
23
23
  return false;
24
+ if (value.dataArrayContains !== undefined &&
25
+ (!isObject(value.dataArrayContains) ||
26
+ Object.keys(value.dataArrayContains).length === 0 ||
27
+ Object.entries(value.dataArrayContains).some(([key, member]) => key.length === 0 || typeof member !== 'string' || member.length === 0)))
28
+ return false;
24
29
  return (value.inactiveForHours === undefined ||
25
30
  (typeof value.inactiveForHours === 'number' &&
26
31
  Number.isFinite(value.inactiveForHours) &&
@@ -3,6 +3,7 @@ export type SnapshotSelection = {
3
3
  version?: string;
4
4
  parentSessionId?: string | null;
5
5
  data?: Record<string, unknown>;
6
+ dataArrayContains?: Record<string, string>;
6
7
  inactiveForHours?: number;
7
8
  };
8
9
  export type SnapshotCounts = {
@@ -51,7 +51,8 @@ async function runSnapshot(args) {
51
51
  }
52
52
  async function runJournal(args) {
53
53
  const [action, ...flags] = args;
54
- const values = flagsToValues(flags);
54
+ const { repositories, remaining } = action === 'append' ? extractRepositories(flags) : { repositories: [], remaining: flags };
55
+ const values = flagsToValues(remaining);
55
56
  if (action === 'entries') {
56
57
  assertAllowed(values, ['session-id']);
57
58
  const sessionId = required(values, 'session-id');
@@ -76,11 +77,31 @@ async function runJournal(args) {
76
77
  'parent-session-id',
77
78
  'timestamp',
78
79
  ]);
79
- process.stdout.write(`${await appendJournal({ sessionId: required(values, 'session-id'), agent: required(values, 'agent'), version: values.version ?? 'unknown', markdownFile: required(values, 'file'), ...(values['parent-session-id'] ? { parentSessionId: values['parent-session-id'] } : {}), ...('timestamp' in values ? { timestamp: values.timestamp } : {}) })}\n`);
80
+ process.stdout.write(`${await appendJournal({ sessionId: required(values, 'session-id'), agent: required(values, 'agent'), version: values.version ?? 'unknown', repositories: requiredRepositories(repositories), markdownFile: required(values, 'file'), ...(values['parent-session-id'] ? { parentSessionId: values['parent-session-id'] } : {}), ...('timestamp' in values ? { timestamp: values.timestamp } : {}) })}\n`);
80
81
  return 0;
81
82
  }
82
83
  throw new Error('usage: agent-blackboard journal append|entries');
83
84
  }
85
+ function extractRepositories(flags) {
86
+ const repositories = [];
87
+ const remaining = [];
88
+ for (let index = 0; index < flags.length; index += 2) {
89
+ const flag = flags[index];
90
+ const value = flags[index + 1];
91
+ if (!flag?.startsWith('--') || value === undefined)
92
+ throw new Error(`invalid option: ${flag ?? ''}`);
93
+ if (flag === '--repository')
94
+ repositories.push(value);
95
+ else
96
+ remaining.push(flag, value);
97
+ }
98
+ return { repositories, remaining };
99
+ }
100
+ function requiredRepositories(repositories) {
101
+ if (repositories.length === 0)
102
+ throw new Error('--repository is required');
103
+ return repositories;
104
+ }
84
105
  function flagsToValues(flags) {
85
106
  const values = {};
86
107
  for (let index = 0; index < flags.length; index += 2) {
@@ -1,2 +1,2 @@
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\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 --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 --session-id UUID --agent NAME --version VERSION --file PATH [--parent-session-id UUID] [--timestamp ISO8601]\nagent-blackboard journal entries --session-id UUID\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]\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 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\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 --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 --session-id UUID --agent NAME --version VERSION --file PATH --repository OWNER/NAME [--repository OWNER/NAME ...] [--parent-session-id UUID] [--timestamp ISO8601]\nagent-blackboard journal entries --session-id UUID\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]\n";
2
2
  export declare function printUsage(stream?: NodeJS.WritableStream): void;
@@ -83,7 +83,7 @@ gha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--
83
83
  gha-output <name>
84
84
  gha-needs-results [label]
85
85
  download-with-diagnostics <url> <destination> [-- curl-args...]
86
- download-optional-run-artifacts (--name <name> | --pattern <pattern>) --dir <directory>
86
+ download-optional-run-artifacts (--name <name>... | --pattern <pattern>) --dir <directory>
87
87
  host-pressure-diagnostics
88
88
  allocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]
89
89
  diagnose-port-collision [--ports "2200 2216"] [--output-dir PATH]
@@ -116,7 +116,7 @@ retrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH]
116
116
  link-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill
117
117
  retrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]
118
118
  agent-blackboard probe
119
- agent-blackboard journal append --session-id UUID --agent NAME --version VERSION --file PATH [--parent-session-id UUID] [--timestamp ISO8601]
119
+ agent-blackboard journal append --session-id UUID --agent NAME --version VERSION --file PATH --repository OWNER/NAME [--repository OWNER/NAME ...] [--parent-session-id UUID] [--timestamp ISO8601]
120
120
  agent-blackboard journal entries --session-id UUID
121
121
  agent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{"sessions":N,"entries":N,"records":N,"bytes":N}'
122
122
  agent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]
@@ -0,0 +1,17 @@
1
+ export type RunOptions = {
2
+ readonly args?: readonly string[] | ((temporaryDirectory: string) => readonly string[]);
3
+ readonly env?: Readonly<Record<string, string>>;
4
+ readonly ghScript?: string;
5
+ readonly sleepScript?: string;
6
+ };
7
+ export declare function runHelper(options?: RunOptions): {
8
+ output: string;
9
+ temporaryDirectory: string;
10
+ pid: number;
11
+ stdout: string;
12
+ stderr: string;
13
+ status: number | null;
14
+ signal: NodeJS.Signals | null;
15
+ error?: Error;
16
+ };
17
+ export declare function cleanupTemporaryDirectories(): void;
@@ -0,0 +1,50 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { delimiter, join, resolve } from 'node:path';
5
+ const script = resolve('packages/vouchington-tooling/scripts/gha/download-optional-run-artifacts.sh');
6
+ const temporaryDirectories = [];
7
+ export function runHelper(options = {}) {
8
+ const temporaryDirectory = mkdtempSync(join(tmpdir(), 'download-optional-run-artifacts-'));
9
+ temporaryDirectories.push(temporaryDirectory);
10
+ const binDirectory = join(temporaryDirectory, 'bin');
11
+ const githubOutput = join(temporaryDirectory, 'github-output');
12
+ const ghPath = join(binDirectory, 'gh');
13
+ mkdirSync(binDirectory);
14
+ writeFileSync(ghPath, options.ghScript ??
15
+ '#!/bin/sh\nif [ "$1" = api ]; then echo transport-download-control; else printf "downloaded %s\\n" "$*"; fi\n');
16
+ chmodSync(ghPath, 0o755);
17
+ if (options.sleepScript !== undefined) {
18
+ const sleepPath = join(binDirectory, 'sleep');
19
+ writeFileSync(sleepPath, options.sleepScript);
20
+ chmodSync(sleepPath, 0o755);
21
+ }
22
+ const args = typeof options.args === 'function'
23
+ ? options.args(temporaryDirectory)
24
+ : (options.args ?? [
25
+ '--name',
26
+ 'transport-download-control',
27
+ '--dir',
28
+ join(temporaryDirectory, 'coverage-control'),
29
+ ]);
30
+ const result = spawnSync('bash', [script, ...args], {
31
+ encoding: 'utf8',
32
+ env: {
33
+ ...process.env,
34
+ GITHUB_OUTPUT: githubOutput,
35
+ GITHUB_REPOSITORY: 'owner/repo',
36
+ GITHUB_RUN_ID: '1234',
37
+ PATH: binDirectory + delimiter + process.env.PATH,
38
+ ...options.env,
39
+ },
40
+ });
41
+ return {
42
+ ...result,
43
+ output: existsSync(githubOutput) ? readFileSync(githubOutput, 'utf8') : '',
44
+ temporaryDirectory,
45
+ };
46
+ }
47
+ export function cleanupTemporaryDirectories() {
48
+ for (const directory of temporaryDirectories.splice(0))
49
+ rmSync(directory, { force: true, recursive: true });
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.18.1",
3
+ "version": "0.19.1",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -334,13 +334,13 @@
334
334
  "remark-gfm": "4.0.1",
335
335
  "smol-toml": "1.8.0",
336
336
  "spdx-expression-parse": "5.0.0",
337
- "yaml": "2.9.0"
337
+ "yaml": "2.9.1"
338
338
  },
339
339
  "devDependencies": {
340
340
  "@ast-grep/cli": "0.45.3",
341
341
  "@types/picomatch": "^4.0.3",
342
342
  "@types/spdx-expression-parse": "4.0.0",
343
- "agent-blackboard": "^0.5.0"
343
+ "agent-blackboard": "^0.6.0"
344
344
  },
345
345
  "peerDependencies": {
346
346
  "typescript": ">=5"
@@ -2,18 +2,34 @@
2
2
  set -euo pipefail
3
3
 
4
4
  usage() {
5
- echo 'usage: download-optional-run-artifacts.sh (--name <name> | --pattern <pattern>) --dir <directory>' >&2
5
+ echo 'usage: download-optional-run-artifacts.sh (--name <name>... | --pattern <pattern>) --dir <directory>' >&2
6
6
  exit 2
7
7
  }
8
8
 
9
+ contains() {
10
+ local needle="$1" item
11
+ shift
12
+ for item in "$@"; do
13
+ [ "$item" != "$needle" ] || return 0
14
+ done
15
+ return 1
16
+ }
17
+
9
18
  selector=''
10
19
  selector_type=''
20
+ names=()
11
21
  destination=''
12
22
  while [ "$#" -gt 0 ]; do
13
23
  case "$1" in
14
- --name|--pattern)
24
+ --name)
25
+ { [ "$selector_type" != pattern ] && [ "$#" -ge 2 ] && [ -n "$2" ]; } || usage
26
+ selector_type=name
27
+ if [ "${#names[@]}" -eq 0 ] || ! contains "$2" "${names[@]}"; then names+=("$2"); fi
28
+ shift 2
29
+ ;;
30
+ --pattern)
15
31
  [ -z "$selector_type" ] && [ "$#" -ge 2 ] || usage
16
- selector_type="${1#--}"
32
+ selector_type=pattern
17
33
  selector="$2"
18
34
  shift 2
19
35
  ;;
@@ -26,7 +42,8 @@ while [ "$#" -gt 0 ]; do
26
42
  esac
27
43
  done
28
44
 
29
- [ -n "$selector_type" ] && [ -n "$selector" ] && [ -n "$destination" ] || usage
45
+ [ -n "$selector_type" ] && [ -n "$destination" ] || usage
46
+ [ "$selector_type" != pattern ] || [ -n "$selector" ] || usage
30
47
  [ -n "${GITHUB_OUTPUT:-}" ] || { echo 'GITHUB_OUTPUT must be set' >&2; exit 2; }
31
48
  [ -n "${GITHUB_REPOSITORY:-}" ] || { echo 'GITHUB_REPOSITORY must be set' >&2; exit 2; }
32
49
  [ -n "${GITHUB_RUN_ID:-}" ] || { echo 'GITHUB_RUN_ID must be set' >&2; exit 2; }
@@ -47,30 +64,13 @@ esac
47
64
 
48
65
  validate_artifact_name() {
49
66
  case "$1" in
50
- ''|.|..|*/*|*\\*)
67
+ ''|.|..|*/*|*\\*|*$'\n'*|*$'\r'*)
51
68
  echo '[optional-run-artifacts] invalid artifact name' >&2
52
69
  return 2
53
70
  ;;
54
71
  esac
55
72
  }
56
73
 
57
- download_exact() {
58
- artifact="$1"
59
- directory="$2"
60
- validate_artifact_name "$artifact" || return $?
61
- artifacts=$(list_artifact_names) || return $?
62
- found=0
63
- while IFS= read -r candidate; do
64
- if [ "$candidate" = "$artifact" ]; then
65
- found=1
66
- break
67
- fi
68
- done < <(printf '%s\n' "$artifacts")
69
- [ "$found" -eq 1 ] || return 3
70
- echo "[optional-run-artifacts] attempt selector=$selector_type" >&2
71
- GH_HOST="$github_host" gh run download "$GITHUB_RUN_ID" --repo "$repository" --name "$artifact" --dir "$directory"
72
- }
73
-
74
74
  list_artifact_names_once() {
75
75
  GH_HOST="$github_host" gh api \
76
76
  --paginate \
@@ -126,8 +126,47 @@ download_pattern() {
126
126
  done
127
127
  }
128
128
 
129
+ notice_absent() {
130
+ local requested="$1" count=$(($# - 1)) list='' name
131
+ shift
132
+ for name in "${@:1:3}"; do
133
+ list="${list:+$list, }$name"
134
+ done
135
+ [ "$count" -le 3 ] || list="$list and $((count - 3)) more"
136
+ echo "::notice::Optional same-run artifacts absent: $count of $requested requested (${list//\%/%25})" >&2
137
+ }
138
+
139
+ download_names() {
140
+ local name status listed_names
141
+ listed=()
142
+ present=()
143
+ absent=()
144
+ for name in "${names[@]}"; do
145
+ validate_artifact_name "$name" || return $?
146
+ done
147
+ listed_names=$(list_artifact_names) || return $?
148
+ while IFS= read -r name; do
149
+ listed+=("$name")
150
+ done <<< "$listed_names"
151
+ for name in "${names[@]}"; do
152
+ if contains "$name" "${listed[@]}"; then present+=("$name"); else absent+=("$name"); fi
153
+ done
154
+ echo "[optional-run-artifacts] selection selector=name requested=${#names[@]} present=${#present[@]} absent=${#absent[@]}" >&2
155
+ [ "${#absent[@]}" -eq 0 ] || notice_absent "${#names[@]}" "${absent[@]}"
156
+ [ "${#present[@]}" -gt 0 ] || return 3
157
+ for name in "${present[@]}"; do
158
+ echo "[optional-run-artifacts] attempt selector=name artifact=$name" >&2
159
+ if GH_HOST="$github_host" gh run download "$GITHUB_RUN_ID" --repo "$repository" --name "$name" --dir "$destination/$name"; then :; else
160
+ status=$?
161
+ echo "[optional-run-artifacts] download failed artifact=$name exit=$status" >&2
162
+ [ "$status" -ne 3 ] || status=1
163
+ return "$status"
164
+ fi
165
+ done
166
+ }
167
+
129
168
  if [ "$selector_type" = name ]; then
130
- if download_exact "$selector" "$destination"; then status=0; else status=$?; fi
169
+ if download_names; then status=0; else status=$?; fi
131
170
  else
132
171
  if download_pattern; then status=0; else status=$?; fi
133
172
  fi
@@ -139,7 +178,7 @@ else
139
178
  130|143) exit "$status" ;;
140
179
  3)
141
180
  echo 'availability=unavailable' >> "$GITHUB_OUTPUT"
142
- echo "::warning::Optional same-run artifact unavailable; continuing with validated fallback (selector=$selector_type exit=$status)" >&2
181
+ [ "$selector_type" = name ] || echo "::warning::Optional same-run artifact unavailable; continuing with validated fallback (selector=$selector_type exit=$status)" >&2
143
182
  echo "[optional-run-artifacts] result=unavailable selector=$selector_type exit=$status" >&2
144
183
  ;;
145
184
  *)
@@ -14,6 +14,9 @@ subagent-identity rules.
14
14
  evidence, and any tracking reference.
15
15
  2. Preserve the provider's required session and parent-session identity. Never invent, guess,
16
16
  print, search for, or persist credentials outside its documented mechanism.
17
+ When the provider supports structured repository metadata, keep a cumulative list of canonical
18
+ repositories on each agent's own session and tag each entry with only its relevant repositories.
19
+ Update session metadata before appending, and stop if that update fails.
17
20
  3. If the journal service cannot authenticate or persist a mandatory entry, stop and report the
18
21
  blocker rather than silently substituting a local file or memory.
19
22
  4. Read journal entries oldest-first when preparing a retrospective or clustering follow-ups; use
@@ -14,6 +14,8 @@ Read the local `AGENTS.md`, `CLAUDE.md`, and journal guidance first.
14
14
  sources. Do not estimate unknown facts from memory or transcripts.
15
15
  3. Summarize the outcome, plan-versus-actual differences, validation evidence, recurring friction,
16
16
  and actionable process improvements. Keep one-off noise separate from repeatable root causes.
17
+ Preserve the repository attribution of the source entries in the retrospective record when the
18
+ journal supports it; a retrospective spanning repositories names every represented repository.
17
19
  4. Save through the repository's required durable mechanism and report the record identifier plus
18
20
  any follow-up decisions.
19
21
 
@@ -8,7 +8,9 @@ description: Distill completed session records into a small set of verified, act
8
8
  Use when completed retrospectives or journals should become durable follow-up work. Read local
9
9
  `AGENTS.md`, `CLAUDE.md`, issue policy, and journal retention rules before any mutation.
10
10
 
11
- 1. Enumerate only completed, eligible session records. Leave in-progress sessions intact.
11
+ 1. Enumerate only completed, eligible session records. For repository-scoped work, filter sessions
12
+ by repository membership and use only entries attributed to that repository. Keep untagged legacy
13
+ records unclassified instead of inferring their repository. Leave in-progress sessions intact.
12
14
  2. Cluster findings by root cause, favoring a few broad actionable themes over many narrow issues.
13
15
  Treat a finding already linked to an open tracker as context, not a duplicate.
14
16
  3. Verify each candidate against the current base and search existing issues and open changes before
@@ -17,7 +19,9 @@ Use when completed retrospectives or journals should become durable follow-up wo
17
19
  validation. Route every authorized creation through
18
20
  [github-issue](../github-issue/SKILL.md), including its repository gate, label approval, and
19
21
  denied-external tracking behavior.
20
- 5. Archive only records that were fully processed under the repository's retention rules; report
22
+ 5. Archive only records that were fully processed across every represented repository under the
23
+ repository's retention rules. A one-repository pass leaves a multi-repository session active
24
+ until its other repositories have been reviewed. Report
21
25
  created, updated, skipped, and deferred themes with reasons.
22
26
 
23
27
  Use source records only for local verification and leave them in the repository's approved journal
@@ -18,3 +18,6 @@ rules before choosing a runner-specific approach.
18
18
 
19
19
  Read [tautological tests](references/tautological-tests.md) before finishing any test whose
20
20
  assertion is not obviously falsifiable by a defect in the code under test.
21
+
22
+ Read [dependency boundaries](references/dependency-boundaries.md) before asserting on anything a
23
+ dependency produces, and whenever a dependency upgrade breaks a test.
@@ -2,7 +2,9 @@
2
2
 
3
3
  Choose the lowest realistic boundary that can observe the contract. Mock external systems and
4
4
  uncontrolled infrastructure; exercise internal module composition where practical. Test behavior,
5
- failure paths, authorization, and security-relevant validation rather than private calls.
5
+ failure paths, authorization, and security-relevant validation rather than private calls, and
6
+ assert what this repository owns rather than what its [dependencies](dependency-boundaries.md)
7
+ produce.
6
8
 
7
9
  Start with a failing test when the behavior is testable. Finish only when the production path, its
8
10
  public contract, documentation, and generated artifacts move together. Do not leave placeholders or
@@ -0,0 +1,37 @@
1
+ # Dependency boundaries
2
+
3
+ Test the code this repository owns. A dependency's behavior — its algorithms, error messages, output
4
+ formats, internal file layout, exit codes, the URLs and query strings it builds, its pagination and
5
+ retry strategy, and the order or number of calls it makes — belongs to that dependency's own tests.
6
+ An assertion that pins any of it fails when the dependency ships a release rather than when this
7
+ repository regresses, so every upgrade arrives as a red build that no change here caused.
8
+
9
+ Apply the [tautological tests](tautological-tests.md) falsifiability check with ownership added: for
10
+ every assertion, name a defect in this repository's code that would make it fail. If only a
11
+ dependency release could fail it, delete it. If it covers owned logic but also pins incidental
12
+ dependency detail, rewrite it to assert the owned outcome. What the repository chooses or promises
13
+ is owned even when a dependency carries it out — the rules it enables, its thresholds and scopes,
14
+ the arguments, endpoints, and parameters it passes, how it handles a dependency's exit codes and
15
+ errors, and any call count or order its own logic requires — so test it, but through the
16
+ dependency's public API rather than its internals.
17
+
18
+ Watch for these shapes. A version-drift guard deep-imports a package's private files or calls its
19
+ internal functions so the suite notices when upstream behavior changes; it couples the suite to a
20
+ layout the package never promised, so delete it, and when documentation relies on dependency
21
+ behavior, link to the dependency's own documentation instead of restating and pinning it. A
22
+ dependency re-test runs a dependency with fabricated input and asserts what it returns; a wrapper
23
+ that only forwards configuration needs one test proving the configuration arrives, not a second copy
24
+ of the dependency's suite. An upstream-output pin asserts exact dependency-owned messages, markdown,
25
+ JSON envelopes, or renderer markup when the owned contract is narrower; assert the rule identifier,
26
+ status, or owned field instead. A sequence-bound fake answers by call order — chained one-shot mock
27
+ responses, or a queue of canned replies consumed in turn — when the code under test does not own
28
+ that order, so a dependency that adds a request, paginates differently, or reorders its calls hands
29
+ the wrong reply to the wrong call; route the fake by request meaning, such as method and pathname,
30
+ and assert the owned result rather than dependency-incidental URL, query-string, or call-count
31
+ details.
32
+
33
+ When a dependency upgrade breaks a test, first ask whether the test pinned dependency behavior. If
34
+ it did, fix the test by deleting or rewriting it rather than re-pinning the new upstream value, and
35
+ do not add a guard that fails on the next release. If the failure traces to owned code or owned
36
+ configuration — the dependency now rejects arguments this repository passes, or returns a result
37
+ this repository's logic mishandles — it is a real regression, so fix the owned code instead.