vouchington-tooling 0.44.0 → 0.46.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 (124) hide show
  1. package/README.md +10 -52
  2. package/dist/agent-blackboard/client.d.mts +6 -0
  3. package/dist/agent-blackboard/feedback-audit-source.d.mts +4 -10
  4. package/dist/agent-blackboard/feedback-audit-source.mjs +7 -18
  5. package/dist/agent-blackboard/feedback-compose-audit.mjs +6 -14
  6. package/dist/agent-blackboard/feedback-compose.d.mts +1 -4
  7. package/dist/agent-blackboard/feedback-compose.mjs +2 -5
  8. package/dist/agent-blackboard/feedback-fields.mjs +1 -1
  9. package/dist/agent-blackboard/feedback-identity.mjs +1 -1
  10. package/dist/agent-blackboard/feedback-journal-audit.d.mts +6 -8
  11. package/dist/agent-blackboard/feedback-journal-audit.mjs +9 -10
  12. package/dist/agent-blackboard/feedback-journal-sandbox.d.mts +2 -2
  13. package/dist/agent-blackboard/feedback-journal-sandbox.mjs +2 -2
  14. package/dist/agent-blackboard/feedback-outbox-rejected.mjs +1 -1
  15. package/dist/agent-blackboard/feedback-outbox.mjs +2 -2
  16. package/dist/agent-blackboard/index.d.mts +1 -1
  17. package/dist/audit-journal/ci-failures.d.mts +6 -0
  18. package/dist/{session-friction → audit-journal}/ci-failures.mjs +3 -9
  19. package/dist/{session-friction → audit-journal}/journal.d.mts +0 -1
  20. package/dist/{session-friction → audit-journal}/journal.mjs +1 -1
  21. package/dist/audit-journal/session-id.d.mts +1 -0
  22. package/dist/{session-friction → audit-journal}/session-id.mjs +0 -5
  23. package/dist/{session-friction → audit-journal}/text.d.mts +1 -4
  24. package/dist/{session-friction → audit-journal}/text.mjs +4 -22
  25. package/dist/audit-journal/types.d.mts +17 -0
  26. package/dist/cli/commands/gha-runtime-audit.mjs +2 -0
  27. package/dist/cli/usage.d.mts +1 -1
  28. package/dist/cli/usage.mjs +1 -1
  29. package/dist/gh-cli/env.d.mts +9 -0
  30. package/dist/gh-cli/env.mjs +12 -0
  31. package/dist/gh-cli/exec.d.mts +7 -4
  32. package/dist/gh-cli/exec.mjs +7 -5
  33. package/dist/gh-cli/index.d.mts +1 -0
  34. package/dist/gh-cli/index.mjs +1 -0
  35. package/dist/gha-post-review/github.mjs +2 -1
  36. package/dist/index.d.mts +3 -3
  37. package/dist/index.mjs +2 -2
  38. package/dist/mcp-server/context.d.mts +5 -0
  39. package/dist/mcp-server/dispatch.d.mts +5 -0
  40. package/dist/mcp-server/dispatch.mjs +8 -1
  41. package/dist/mcp-server/fake-blackboard.test-helpers.d.mts +2 -0
  42. package/dist/mcp-server/fake-blackboard.test-helpers.mjs +3 -0
  43. package/dist/mcp-server/handlers-pr.d.mts +1 -1
  44. package/dist/mcp-server/handlers-pr.mjs +6 -97
  45. package/dist/mcp-server/handlers-retrospective-save.d.mts +10 -0
  46. package/dist/mcp-server/handlers-retrospective-save.mjs +138 -0
  47. package/dist/mcp-server/handlers-retrospective.d.mts +1 -12
  48. package/dist/mcp-server/handlers-retrospective.mjs +1 -154
  49. package/dist/mcp-server/handlers-session.mjs +25 -6
  50. package/dist/mcp-server/handlers-snapshot-cleanup.d.mts +7 -0
  51. package/dist/mcp-server/handlers-snapshot-cleanup.mjs +36 -0
  52. package/dist/mcp-server/harness.test-helpers.d.mts +3 -0
  53. package/dist/mcp-server/harness.test-helpers.mjs +2 -0
  54. package/dist/mcp-server/policy-run.d.mts +1 -0
  55. package/dist/mcp-server/policy-run.mjs +1 -0
  56. package/dist/mcp-server/pr-agent.d.mts +12 -0
  57. package/dist/mcp-server/pr-agent.mjs +34 -0
  58. package/dist/mcp-server/pr-body.d.mts +6 -2
  59. package/dist/mcp-server/pr-body.mjs +16 -5
  60. package/dist/mcp-server/pr-defaults.d.mts +3 -0
  61. package/dist/mcp-server/pr-defaults.mjs +7 -2
  62. package/dist/mcp-server/pr-gh.mjs +2 -1
  63. package/dist/mcp-server/pr-input.d.mts +14 -6
  64. package/dist/mcp-server/pr-input.mjs +22 -5
  65. package/dist/mcp-server/pr-live.d.mts +4 -1
  66. package/dist/mcp-server/pr-live.mjs +8 -3
  67. package/dist/mcp-server/pr-mode.d.mts +7 -0
  68. package/dist/mcp-server/pr-mode.mjs +34 -0
  69. package/dist/mcp-server/pr-no-source.d.mts +9 -0
  70. package/dist/mcp-server/pr-no-source.mjs +22 -0
  71. package/dist/mcp-server/pr-prepare.d.mts +25 -0
  72. package/dist/mcp-server/pr-prepare.mjs +124 -0
  73. package/dist/mcp-server/preview-registry.d.mts +25 -0
  74. package/dist/mcp-server/preview-registry.mjs +38 -0
  75. package/dist/mcp-server/retrospective-check.d.mts +3 -0
  76. package/dist/mcp-server/retrospective-check.mjs +38 -0
  77. package/dist/mcp-server/retrospective-input.mjs +2 -2
  78. package/dist/mcp-server/retrospective-preview.d.mts +17 -0
  79. package/dist/mcp-server/retrospective-preview.mjs +60 -0
  80. package/dist/mcp-server/server.mjs +1 -0
  81. package/dist/mcp-server/snapshot-registry.d.mts +25 -0
  82. package/dist/mcp-server/snapshot-registry.mjs +32 -0
  83. package/dist/mcp-server/tools-pr.mjs +28 -16
  84. package/dist/mcp-server/tools-retrospective.mjs +25 -5
  85. package/dist/mcp-server/tools.mjs +6 -0
  86. package/dist/{session-friction → private-files}/lock.mjs +3 -2
  87. package/dist/private-files/log-file.d.mts +3 -0
  88. package/dist/private-files/log-file.mjs +53 -0
  89. package/dist/{session-friction → private-files}/utf8.mjs +1 -1
  90. package/dist/retrospective-facts/exec.mjs +2 -1
  91. package/docs/mcp-server.md +112 -26
  92. package/package.json +3 -8
  93. package/skills/retrospective/SKILL.md +3 -1
  94. package/skills/retrospective-distill/SKILL.md +5 -0
  95. package/dist/session-friction/ci-failures.d.mts +0 -13
  96. package/dist/session-friction/classify.d.mts +0 -2
  97. package/dist/session-friction/classify.mjs +0 -98
  98. package/dist/session-friction/command-segments.d.mts +0 -3
  99. package/dist/session-friction/command-segments.mjs +0 -94
  100. package/dist/session-friction/coverage-ledger.d.mts +0 -10
  101. package/dist/session-friction/coverage-ledger.mjs +0 -75
  102. package/dist/session-friction/directory.d.mts +0 -1
  103. package/dist/session-friction/directory.mjs +0 -71
  104. package/dist/session-friction/index.d.mts +0 -6
  105. package/dist/session-friction/index.mjs +0 -5
  106. package/dist/session-friction/log-file.d.mts +0 -7
  107. package/dist/session-friction/log-file.mjs +0 -95
  108. package/dist/session-friction/log.d.mts +0 -8
  109. package/dist/session-friction/log.mjs +0 -163
  110. package/dist/session-friction/normalize.d.mts +0 -1
  111. package/dist/session-friction/normalize.mjs +0 -81
  112. package/dist/session-friction/report.d.mts +0 -16
  113. package/dist/session-friction/report.mjs +0 -91
  114. package/dist/session-friction/sandbox.d.mts +0 -3
  115. package/dist/session-friction/sandbox.mjs +0 -43
  116. package/dist/session-friction/session-id.d.mts +0 -2
  117. package/dist/session-friction/types.d.mts +0 -80
  118. /package/dist/{session-friction → audit-journal}/audit-block.d.mts +0 -0
  119. /package/dist/{session-friction → audit-journal}/audit-block.mjs +0 -0
  120. /package/dist/{session-friction → audit-journal}/audit-fit.d.mts +0 -0
  121. /package/dist/{session-friction → audit-journal}/audit-fit.mjs +0 -0
  122. /package/dist/{session-friction → audit-journal}/types.mjs +0 -0
  123. /package/dist/{session-friction → private-files}/lock.d.mts +0 -0
  124. /package/dist/{session-friction → private-files}/utf8.d.mts +0 -0
@@ -0,0 +1,53 @@
1
+ import { closeSync, constants, fstatSync, openSync, readSync, writeSync } from 'node:fs';
2
+ import { decodeUtf8 } from "./utf8.mjs";
3
+ export function openLogFile(path, flags) {
4
+ let descriptor;
5
+ try {
6
+ descriptor = openSync(path, flags | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600);
7
+ }
8
+ catch (error) {
9
+ if (error &&
10
+ typeof error === 'object' &&
11
+ 'code' in error &&
12
+ (error.code === 'ELOOP' || error.code === 'EISDIR' || error.code === 'ENOTDIR'))
13
+ throw new Error('log file must be a regular file');
14
+ throw error;
15
+ }
16
+ try {
17
+ const status = fstatSync(descriptor);
18
+ if (!status.isFile() || status.nlink !== 1)
19
+ throw new Error('log file must be a regular file');
20
+ return descriptor;
21
+ }
22
+ catch (error) {
23
+ closeSync(descriptor);
24
+ throw error;
25
+ }
26
+ }
27
+ export function readLogContent(descriptor, maximumBytes) {
28
+ if (fstatSync(descriptor).size > maximumBytes)
29
+ throw new Error('log file is too large');
30
+ const buffer = Buffer.alloc(maximumBytes + 1);
31
+ let length = 0;
32
+ while (length < buffer.length) {
33
+ const bytes = readSync(descriptor, buffer, length, buffer.length - length, length);
34
+ if (bytes === 0)
35
+ break;
36
+ length += bytes;
37
+ }
38
+ /* v8 ignore next -- detects external growth after the descriptor size check. */
39
+ if (length > maximumBytes)
40
+ throw new Error('log file is too large');
41
+ return decodeUtf8(buffer.subarray(0, length));
42
+ }
43
+ export function writeAll(descriptor, value) {
44
+ const buffer = Buffer.from(value);
45
+ let offset = 0;
46
+ while (offset < buffer.length) {
47
+ const written = writeSync(descriptor, buffer, offset, buffer.length - offset);
48
+ /* v8 ignore next -- a zero-byte synchronous file write is an external I/O failure. */
49
+ if (written === 0)
50
+ throw new Error('log file write made no progress');
51
+ offset += written;
52
+ }
53
+ }
@@ -3,6 +3,6 @@ export function decodeUtf8(value) {
3
3
  return new TextDecoder('utf-8', { fatal: true }).decode(value);
4
4
  }
5
5
  catch {
6
- throw new Error('session-friction log is not valid UTF-8');
6
+ throw new Error('log file is not valid UTF-8');
7
7
  }
8
8
  }
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { ghSpawnEnv } from "../gh-cli/env.mjs";
2
3
  export const shell = (command, args) => new Promise((resolve) => {
3
- const child = spawn(command, args);
4
+ const child = command === 'gh' ? spawn(command, args, { env: ghSpawnEnv() }) : spawn(command, args);
4
5
  let stdout = '';
5
6
  let stderr = '';
6
7
  child.stdout.setEncoding('utf8');
@@ -82,17 +82,18 @@ Every tool takes an explicit `sessionId` (`^[A-Za-z0-9._:-]+$`, at most 256 char
82
82
  server never infers, and an optional `worktree`. Unknown arguments are rejected. Failures come back
83
83
  as MCP tool errors (`isError: true`) with an actionable message rather than crashing the server.
84
84
 
85
- | Tool | Arguments besides `sessionId` and `worktree` | Returns |
86
- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
87
- | `journal_append` | `parentSessionId`, `agent`, `version`, `mode` (`interactive` or `autonomous`), `markdown`, `sourceEventId`, `workOutcome`, `repositories`, `feedbackCoverage`, optional `category` | `sessionId`, `timestamp`, delivery `status` (`delivered` with a verified receipt, or `pending`), `pendingCount`, `worktreePendingCount` |
88
- | `journal_entries` | none | `sessionId` and `entries`, every entry oldest first |
89
- | `outbox_status` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount`, `rejectedCount`, `worktreeRejectedCount` |
90
- | `outbox_flush` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount`, `rejectedCount`, `worktreeRejectedCount`, `deliveredCount`, `rejected` (when any) |
91
- | `session_ensure` | `parentSessionId`, `agent`, `version` | `sessionId`, `status` (`created` or `exists`), `archived` |
92
- | `retrospective_check` | optional `sourceEventId` | `sessionId`, `saved`, `entries` (`sourceEventId`, `timestamp`, `issues`, `prs` of each retrospective, oldest first) |
93
- | `retrospective_save` | `parentSessionId`, `agent`, `version`, `mode`, `workOutcome`, `feedbackCoverage`, exactly one of `markdown` or `compose`, optional `repositories`, `sourceEventId`, `category`, `dryRun` | as `journal_append` plus `policy.warnings`; with `dryRun`, `markdown` and `policy.warnings` |
94
- | `snapshot_export` | optional `agent`, `version`, `parentSessionId`, `data`, `dataArrayContains`, `inactiveForHours` | `sessionId`, `path`, `counts`, `checksum`, `manifest`, `cleanupToken` |
95
- | `session_archive` | none | `sessionId`, `archived: true` |
85
+ | Tool | Arguments besides `sessionId` and `worktree` | Returns |
86
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
87
+ | `journal_append` | `parentSessionId`, `agent`, `version`, `mode` (`interactive` or `autonomous`), `markdown`, `sourceEventId`, `workOutcome`, `repositories`, `feedbackCoverage`, optional `category` | `sessionId`, `timestamp`, delivery `status` (`delivered` with a verified receipt, or `pending`), `pendingCount`, `worktreePendingCount` |
88
+ | `journal_entries` | none | `sessionId` and `entries`, every entry oldest first |
89
+ | `outbox_status` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount`, `rejectedCount`, `worktreeRejectedCount` |
90
+ | `outbox_flush` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount`, `rejectedCount`, `worktreeRejectedCount`, `deliveredCount`, `rejected` (when any) |
91
+ | `session_ensure` | `parentSessionId`, `agent`, `version` | `sessionId`, `status` (`created` or `exists`), `archived` |
92
+ | `retrospective_check` | optional `sourceEventId` | `sessionId`, `saved`, `entries` (`sourceEventId`, `timestamp`, `issues`, `prs` of each retrospective, oldest first) |
93
+ | `retrospective_save` | `parentSessionId`, `agent`, `version`, `mode`, `workOutcome`, `feedbackCoverage`, exactly one of `markdown` or `compose`, optional `repositories`, `sourceEventId`, `category`, `dryRun`, `knownSensitiveValues` (markdown only); or `previewToken` with identity, `mode` and `sourceEventId` | as `journal_append` plus `policy.warnings`; with `dryRun`, `markdown`, `previewToken` and `policy.warnings` |
94
+ | `snapshot_export` | optional `agent`, `version`, `parentSessionId`, `data`, `dataArrayContains`, `inactiveForHours` | `sessionId`, `path`, `counts`, `checksum`, `manifest`, `cleanupToken` |
95
+ | `snapshot_cleanup` | `cleanupToken` | `sessionId`, `cleaned: true` |
96
+ | `session_archive` | none | `sessionId`, `archived: true` |
96
97
 
97
98
  `journal_append` validates the whole feedback envelope with `validateFeedbackEnvelope` before
98
99
  anything is written, so a missing required field writes nothing. It returns the verified read-back
@@ -146,6 +147,35 @@ outbox does not create the directory. `snapshot_export` never accepts a destinat
146
147
  `agent-blackboard` client chooses a private file and returns its path. `session_archive` is the one
147
148
  destructive tool; archived metadata is immutable, while entries stay appendable.
148
149
 
150
+ ### Snapshot cleanup
151
+
152
+ `snapshot_export` writes a private file in the OS temp directory, and `snapshot_cleanup` is the only
153
+ way to delete it through the server. It was chosen over cleanup inside `session_archive` because
154
+ the exporting session and the archived sessions differ during distillation, and cleanup must run
155
+ whether or not archiving happened. Call it as a finally step after processing, on success and on
156
+ failure.
157
+
158
+ - **No caller-chosen path.** The tool takes only `cleanupToken` (`path`, `directory`, and `receipt`
159
+ are unsupported arguments). The server records the export's file path itself, plus a partition
160
+ directory and its signed receipt when the client reports both, and cleans exactly that record.
161
+ When the client's export carried its own cleanup capability, the server keeps it (never
162
+ returning it) and cleans through the client's `cleanupSnapshotPartitions`, so the client's
163
+ ownership marker goes with the file; otherwise it uses the in-tree `cleanupSnapshotPartitions`.
164
+ - **Server-minted, bound token.** `snapshot_export` replaces the client's `cleanupToken` with 32
165
+ random bytes the server generates, recorded with the caller's `sessionId` and resolved `worktree`.
166
+ The binding is checked before any filesystem call.
167
+ - **Single use.** A successful cleanup spends the token; a repeat fails as unknown and deletes
168
+ nothing. The record is taken synchronously, so concurrent calls cannot both clean.
169
+ - **Failure modes.** An unknown, forged (including a path used as a token), spent, other-session, or
170
+ other-worktree token fails with one message and deletes nothing; a mismatched caller does not spend
171
+ the owner's token. If the cleanup itself fails, the token stays valid and the error carries the
172
+ reason, including the retained tombstone path when a partition directory was half removed, so the
173
+ same call can retry. A file that is already gone counts as cleaned.
174
+ - **Server restart.** Bindings are in memory only. After a restart every earlier token is unknown, so
175
+ the file is left for the operating system to reap from its temp directory; run the distill pass
176
+ again to export a fresh snapshot. The registry also remembers at most 256 exports and forgets the
177
+ oldest first.
178
+
149
179
  The outbox lives at `<worktree>/.local/blackboard-outbox` and is used only in interactive mode.
150
180
  Autonomous mode forbids a filesystem outbox, so a failed delivery is an error.
151
181
 
@@ -168,7 +198,9 @@ instructions shows it to the agent.
168
198
  ## Retrospective tools and the policy contract
169
199
 
170
200
  `retrospective_check` reads the session through the same client as `journal_entries` and reports the
171
- entries whose envelope `type` is `retrospective`. A session that was never created (the blackboard
201
+ entries whose envelope `type` is `retrospective`. A schema-versioned entry counts only when its
202
+ envelope passes `validateFeedbackEnvelope`; a malformed one is not saved, while an unversioned legacy
203
+ entry is still accepted. A session that was never created (the blackboard
172
204
  answers 404) reports `saved: false`.
173
205
 
174
206
  `retrospective_save` builds a `retrospective` envelope and writes it exactly as `journal_append`
@@ -189,7 +221,20 @@ are killed after 120 seconds. `compose.facts` takes `pr`, `branch`, `noPr`, or `
189
221
  worktree's. Facts run `git` and `gh` in the worktree as the server's user, honoring its
190
222
  repository configuration (which can select helpers that run code), so the worktree, including
191
223
  `.git/config`, must be as trusted as the policy module. `dryRun`
192
- runs every check and returns the document without writing.
224
+ runs every check and returns the document without writing, plus a `previewToken`.
225
+
226
+ `knownSensitiveValues` (each at least 4 characters) is also accepted beside `markdown`, and
227
+ redacts the markdown, `category` and `feedbackCoverage` just as `compose.knownSensitiveValues`
228
+ does; with `compose`, pass it inside `compose`. The `previewToken` is 32 random bytes (base64url),
229
+ opaque, single use, and held only in server memory: a restart drops every token, and the registry
230
+ keeps the 32 most recent. It is bound to the `sessionId`, `parentSessionId`, `mode`, the resolved worktree, the `sourceEventId`
231
+ and the exact redacted content. Save it by calling `retrospective_save` again with the identity,
232
+ the same `parentSessionId`, `mode`, `sourceEventId` and `previewToken`, and none of `markdown`, `compose`,
233
+ `workOutcome`, `feedbackCoverage`, `repositories`, `category`, `knownSensitiveValues` or `dryRun`.
234
+ The server stores exactly the previewed content (so a sensitive value never has to be sent
235
+ again), mints a new timestamp, and does not run the policy again. An unknown, spent, restarted or
236
+ mismatched token is one error and the dry run must be repeated; a mismatch does not spend the
237
+ token, and neither does a failed write.
193
238
 
194
239
  The document must open with the front matter `composeRetrospective` writes: lines of
195
240
  `key: <JSON value>` between two `---` lines. Recognized keys are `date`, `issues`, `prs` (required),
@@ -229,7 +274,7 @@ whole process group is killed, so nothing it started outlives the call. The runn
229
274
  | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------- |
230
275
  | `validateRetrospective` | `sessionId`, `parentSessionId`, `mode`, `workOutcome`, `feedbackCoverage`, `repositories`, `markdown`, `sourceEventId`, `metadata` (`date`, `issues`, `prs`, `description`) | `{ errors: string[], warnings?: string[] }` | 30 s |
231
276
  | `pullRequestDefaults` | `{}` from `retrospective_save`; `{ title, base, relatedIssues }` from `pr_create` | `{ repositories?, labels?, milestone?, project? }` (an object) | 120 s |
232
- | `validatePullRequest` | `title`, `body`, `base`, `head`, `mode`, and `pr` for an update | `{ errors: string[], warnings?: string[] }` | 120 s |
277
+ | `validatePullRequest` | `title`, `body`, `base`, `head`, the trusted `mode`, `noSourceIssue` when given, and `pr` when set | `{ errors: string[], warnings?: string[] }` | 120 s |
233
278
 
234
279
  Any `errors` reject the save and write nothing. A non-zero exit, a timeout (reported as
235
280
  `policy <export> timed out after <N>s`), output over 1 MB, malformed JSON, or a result of the wrong
@@ -241,9 +286,45 @@ shape is an error, never a pass.
241
286
  retrospective tools. All three take `sessionId` (required) and `worktree` (optional), need a server
242
287
  launched inside a repository worktree (they run the checkout policy), and share this structured
243
288
  input: `title` (single line, at most 256 characters), `summary`, `impact`, `relatedIssues`
244
- (`[{ number, relation: "closes" | "refs", reason }]`, may be empty), `details` (string array),
245
- `base` (default `main`), `agent`, optional `model`, `mode` (`interactive` | `autonomous`) and
246
- `workspaceSetup`. `pr_update` also requires `pr`; `pr_validate` accepts an optional `pr`.
289
+ (`[{ number, relation: "closes" | "refs", reason }]`, may be empty), optional `noSourceIssue`
290
+ (`{ reason }`, see below), `details` (string array), `base` (default `main`), optional `agent`, optional
291
+ `model`, optional `mode` (`interactive` | `autonomous`) and `workspaceSetup`. `pr_update` also
292
+ requires `pr`; `pr_validate` accepts an optional `pr`.
293
+
294
+ ### Trusted mode
295
+
296
+ The mode the policy sees is derived by the server from its own launch environment, never from the
297
+ caller. It fails closed: when any automation marker is set to something other than an empty
298
+ string, `0` or `false` (`CI`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`,
299
+ `JENKINS_URL`, `TF_BUILD`, `CODEBUILD_BUILD_ID`, `VOUCHINGTON_AUTOMATION`), the mode is
300
+ `autonomous` whatever the caller passes. `CLAUDE_CODE_SESSION_ATTENDED` set to exactly `0` (Claude
301
+ Code's marker for an unattended `claude -p` run, inherited by MCP servers) also means `autonomous`;
302
+ a missing value or `1` never does. This list is a superset of the downstream check it replaces,
303
+ which was exactly `GITHUB_ACTIONS === 'true' || CI === 'true'`. Otherwise the mode is `interactive`. The optional `mode`
304
+ argument can only narrow `interactive` to `autonomous` (it never widens), so an autonomous run
305
+ cannot unlock an interactive-only policy exception. The policy child receives no env markers, so it
306
+ relies on this value.
307
+
308
+ ### Agent provenance
309
+
310
+ The `Agent:` line is derived by the server, never taken from the caller. The agent is the harness
311
+ that launched the server: `CODEX_THREAD_ID` (codex), `CLAUDECODE=1` or `CLAUDE_CODE_SESSION_ID`
312
+ (claude), `CURSOR_AGENT` or `CURSOR_SESSION_ID` (cursor), `GROK_AGENT`, `GROK_HOOK_EVENT` or
313
+ `GROK_SESSION_ID` (grok), in that order; failing those, the MCP client's `initialize`
314
+ `clientInfo.name` when it contains one of those names. The server has no source for the model.
315
+ `agent` is optional: a value naming the derived agent is ignored, any other value is rendered as
316
+ `(caller-claimed agent: <value>)`, and `model` as `(caller-claimed model: <value>)`. When no agent
317
+ is derivable the line says `unknown`. Making `agent` optional is compatible with existing callers,
318
+ which may keep sending it.
319
+
320
+ ### Pull requests with no source issue
321
+
322
+ Pass `noSourceIssue: { reason }` (a single line) with an empty `relatedIssues`; a non-empty
323
+ `relatedIssues` beside it is rejected. The Related issues section then holds the generic statement
324
+ `- No source issue — <reason>` instead of `- None`. A policy may replace the statement by
325
+ exporting `renderNoSourceIssue({ reason })`, returning a non-empty string (30 s). The field also
326
+ reaches `validatePullRequest` (with the trusted `mode`, so the policy can apply an interactive-only
327
+ rule) and `pullRequestDefaults`.
247
328
 
248
329
  ### Rendering
249
330
 
@@ -258,35 +339,40 @@ input: `title` (single line, at most 256 characters), `summary`, `impact`, `rela
258
339
 
259
340
  ## Related issues
260
341
 
261
- - Closes #N — <reason> (or "Refs #N"; "- None" when empty)
342
+ - Closes #N — <reason> (or "Refs #N"; "- None" when empty;
343
+ "- No source issue — <reason>" for noSourceIssue)
262
344
 
263
345
  ## Details
264
346
 
265
347
  <details joined by blank lines>
266
348
 
267
349
  Workspace setup: <workspaceSetup>
268
- Agent: <agent>[ (<model>)] session <sessionId>
350
+ Agent: <derived agent | unknown>[ (caller-claimed ...)] session <sessionId>
269
351
  Device: <os user>@<lowercased hostname>
270
352
  Worktree: <path after "/worktrees/", "main" for the main worktree, else the directory name>
271
353
  ```
272
354
 
273
- The head branch is the worktree's current branch; a detached HEAD, or a head equal to `base`, is
274
- rejected. The body must stay within GitHub's 65536 characters. `pr_create` sees no stack membership,
355
+ The head branch is the worktree's current branch (for `pr_validate` with `pr`, the live pull
356
+ request's head, which may equal `base` for a fork); otherwise a detached HEAD, or a head equal to `base`, is rejected. The body must stay within GitHub's 65536 characters. `pr_create` sees no stack membership,
275
357
  so a consumer policy may reject any `base` other than the default branch; create stacked layers
276
358
  with the repository's stack tooling instead.
277
359
 
278
360
  ### Tools
279
361
 
280
362
  - `pr_validate` (read-only): renders the body, runs the policy `validatePullRequest({ title, body,
281
- base, head, mode, pr? })` export and returns `{ body, errors, warnings }`. With `pr`, the live
282
- body is read (never written) so its Shepherd Journal is preserved as in `pr_update`.
363
+ base, head, mode, noSourceIssue?, pr? })` export and returns `{ body, errors, warnings }`. With
364
+ `pr`, the live body is read (never written) so its Shepherd Journal is preserved as in
365
+ `pr_update`. The liveness checks of `pr_update` do not apply: any state (merged or closed
366
+ included) and any head are accepted, and the policy sees the pull request's own head and base.
283
367
  - `pr_create` (additive): renders and validates (errors block), then calls the policy
284
- `pullRequestDefaults({ title, base, relatedIssues })` export for optional `labels`, `milestone`
368
+ `pullRequestDefaults({ title, base, relatedIssues, noSourceIssue? })` export for optional `labels`, `milestone`
285
369
  and `project`. It checks the head is pushed and current, writes the body to a mode 0600 temp file
286
370
  (removed afterwards) and runs `gh pr create --draft` with the labels, `--milestone` and
287
371
  `--project`. Returns `{ number, url, warnings }`.
288
- - `pr_update` (mutating): reads `gh pr view <pr> --json body,headRefName,baseRefName,state`,
289
- rejects a pull request that is not `OPEN` or whose head is not the worktree branch, and validates
372
+ - `pr_update` (mutating): reads `gh pr view <pr> --json body,headRefName,headRefOid,baseRefName,state`,
373
+ rejects a pull request that is not `OPEN` or whose head is neither the worktree branch nor the
374
+ worktree `HEAD` commit (an alias branch at the live head SHA is accepted and the live head name is
375
+ used), and validates
290
376
  against the live base (an explicit `base` must equal it). It renders, appends the live Shepherd
291
377
  Journal (a `<details><summary>Shepherd Journal</summary>` block, or a legacy `## Shepherd Journal`
292
378
  section to the next H2 or the end; examples in fenced code blocks are ignored), validates with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.44.0",
3
+ "version": "0.46.0",
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": {
@@ -71,11 +71,6 @@
71
71
  "default": "./dist/agent-blackboard/index.mjs"
72
72
  },
73
73
  "./mcp-server/bin": "./bin/vouchington-mcp.mjs",
74
- "./session-friction": {
75
- "types": "./dist/session-friction/index.d.mts",
76
- "import": "./dist/session-friction/index.mjs",
77
- "default": "./dist/session-friction/index.mjs"
78
- },
79
74
  "./sql-ast": {
80
75
  "types": "./dist/sql-ast/index.d.mts",
81
76
  "import": "./dist/sql-ast/index.mjs",
@@ -391,13 +386,13 @@
391
386
  "yaml": "2.9.1"
392
387
  },
393
388
  "devDependencies": {
394
- "@ast-grep/cli": "0.45.3",
389
+ "@ast-grep/cli": "0.50.0",
395
390
  "@modelcontextprotocol/sdk": "1.32.1",
396
391
  "@types/picomatch": "^4.0.3",
397
392
  "@types/spdx-expression-parse": "4.0.0",
398
393
  "agent-blackboard": "^0.6.1",
399
394
  "no-mistakes": "0.83.0",
400
- "smol-toml": "1.9.0",
395
+ "smol-toml": "1.9.1",
401
396
  "typescript5": "npm:typescript@5.9.3"
402
397
  },
403
398
  "peerDependencies": {
@@ -15,7 +15,9 @@ local `AGENTS.md`, and journal guidance first.
15
15
  1. Read the agent's own journal oldest-first and check for an existing retrospective. Capture a
16
16
  material new observation before filing a follow-up issue. Preserve entry-level repository tags.
17
17
  2. Generate routine repository and transcript facts with the approved composer: pass `compose` to
18
- the `retrospective_save` MCP tool and preview the composition with `dryRun`, never a CLI. Keep exact factual
18
+ the `retrospective_save` MCP tool and preview the composition with `dryRun`, never a CLI. Save the preview by passing its
19
+ `previewToken` to `retrospective_save` (no markdown); put sensitive values only in
20
+ `compose.knownSensitiveValues`, or `knownSensitiveValues` for a markdown save. Keep exact factual
19
21
  section markers generated, preserve unavailable counts, and never replace missing evidence with
20
22
  invented zero statistics. Do not paste raw collectors' output or manually reconstruct facts.
21
23
  3. Record work outcome, feedback coverage, and delivery independently. Success, failure,
@@ -126,6 +126,11 @@ difference is a conflicting payload and stays quarantined.
126
126
  sessions into retrospective and stale (checkpoint-only, journal-only, zero-entry child, and
127
127
  zero-entry root).
128
128
 
129
+ Cleanup, as a finally step: whether processing succeeded or failed, call `snapshot_cleanup` with
130
+ the `cleanupToken` from `snapshot_export`, with the same `sessionId` and `worktree`, once the
131
+ snapshot is no longer needed. The token is single-use and server-restart-scoped; if cleanup fails,
132
+ retry the same call.
133
+
129
134
  Use source records only for local verification and leave them in the repository's approved journal
130
135
  or retention system. Public issue bodies contain only the minimum bounded facts or redacted
131
136
  summaries needed to establish the problem, proposed work, relevant areas, and validation. Never
@@ -1,13 +0,0 @@
1
- import type { FrictionLogReadResult, JournalEntry } from './types.mts';
2
- import { type AuditRedactor } from './text.mts';
3
- export declare function isConformingCiFailureBlock(markdown: string): boolean;
4
- export declare function getConformingGroups(entries: Iterable<JournalEntry>, redact?: AuditRedactor, maxBytes?: number): string[];
5
- export declare function incompleteCiSection(markdownBlocks: string[]): string;
6
- export declare function buildCiFailuresSection(sessionId: string, journal: {
7
- status: 'ok';
8
- markdownBlocks: string[];
9
- truncated: boolean;
10
- } | {
11
- status: 'unreachable';
12
- diagnostic: string;
13
- }, frictionStatus: FrictionLogReadResult['status'] | 'journal-only'): string;
@@ -1,2 +0,0 @@
1
- import type { FrictionEvent, FrictionObservation } from './types.mts';
2
- export declare function classifyFrictionObservation(observation: FrictionObservation): Omit<FrictionEvent, 'timestamp'> | null;
@@ -1,98 +0,0 @@
1
- import { normalizeCommandPrefix } from "./normalize.mjs";
2
- import { boundedText, isWellFormedUnicode, normalizeAuditText } from "./text.mjs";
3
- const FAILURE_TOKENS = [
4
- ['Operation not permitted', /Operation not permitted/i],
5
- ['E2BIG', /\bE2BIG\b/i],
6
- ['EPERM', /\bEPERM\b/i],
7
- ];
8
- const CONNECTION_REFUSED = /\bECONNREFUSED\b/i;
9
- const URL_USERINFO = /([a-z][a-z0-9+.-]{0,31}:\/\/)[^\s/?#@]*@/gi;
10
- const STDERR_INSPECTION_MAX_LENGTH = 100_000;
11
- const COMMAND_INSPECTION_MAX_LENGTH = 10_000;
12
- const IPV4_OCTET = '(?:25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)';
13
- const LOOPBACK_ADDRESSES = [
14
- new RegExp(String.raw `(?<![0-9a-z_.:-])(?:127\.${IPV4_OCTET}(?:\.${IPV4_OCTET}){2}|localhost)(?![0-9a-z_.-])`, 'i'),
15
- /(?<![0-9a-z_.-])\[(?:::1|0:0:0:0:0:0:0:1)(?:%[a-z0-9_.-]+)?\](?![0-9a-z_.-])/i,
16
- /(?<![0-9a-z_.:\x5b\x5d-])(?:::1|0:0:0:0:0:0:0:1)(?:%[a-z0-9_.-]+)?(?::\d{1,5})?(?![0-9a-z_.:\x5b\x5d-])/i,
17
- ];
18
- const DETAIL_MAX_LENGTH = 1_000;
19
- function boundedTail(value, maximum) {
20
- let start = Math.max(0, value.length - maximum);
21
- const first = value.charCodeAt(start);
22
- const previous = value.charCodeAt(start - 1);
23
- if (first >= 0xdc00 && first <= 0xdfff && previous >= 0xd800 && previous <= 0xdbff)
24
- start++;
25
- return value.slice(start);
26
- }
27
- function boundedStderr(value) {
28
- if (value.length <= STDERR_INSPECTION_MAX_LENGTH)
29
- return value;
30
- const headLength = Math.floor((STDERR_INSPECTION_MAX_LENGTH - 1) / 2);
31
- const head = boundedText(value, headLength);
32
- const tail = boundedTail(value, STDERR_INSPECTION_MAX_LENGTH - 1 - head.length);
33
- return `${head}\n${tail}`;
34
- }
35
- function withoutUrlUserinfo(line) {
36
- return line.includes('://') && line.includes('@') ? line.replace(URL_USERINFO, '$1') : line;
37
- }
38
- export function classifyFrictionObservation(observation) {
39
- if (!observation || typeof observation !== 'object')
40
- return null;
41
- if (typeof observation.command !== 'string')
42
- return null;
43
- const command = boundedText(observation.command, COMMAND_INSPECTION_MAX_LENGTH);
44
- if (!isWellFormedUnicode(command) || normalizeAuditText(command) === '')
45
- return null;
46
- const commandPrefix = normalizeCommandPrefix(command, observation.commandWrappers);
47
- if (commandPrefix === '' || !isWellFormedUnicode(commandPrefix))
48
- return null;
49
- if (observation.type === 'permission-request') {
50
- return {
51
- kind: 'sandbox-escalation',
52
- commandPrefix,
53
- detail: 'permission-request',
54
- outcome: 'requested',
55
- };
56
- }
57
- if (observation.type !== 'tool-result')
58
- return null;
59
- if (observation.escalationDetail !== undefined &&
60
- typeof observation.escalationDetail !== 'string')
61
- return null;
62
- const escalationDetail = normalizeAuditText(boundedText(observation.escalationDetail ?? '', DETAIL_MAX_LENGTH));
63
- if (!isWellFormedUnicode(escalationDetail))
64
- return null;
65
- if (observation.permissionOutcome !== undefined &&
66
- !['approved', 'denied'].includes(observation.permissionOutcome))
67
- return null;
68
- const permission = escalationDetail || observation.permissionOutcome
69
- ? {
70
- kind: 'sandbox-escalation',
71
- commandPrefix,
72
- detail: escalationDetail || `permission-${observation.permissionOutcome}`,
73
- outcome: observation.permissionOutcome ?? 'unknown',
74
- }
75
- : null;
76
- const rawStderr = observation.structuredStderr;
77
- if (rawStderr !== undefined && (typeof rawStderr !== 'string' || !isWellFormedUnicode(rawStderr)))
78
- return null;
79
- const stderr = rawStderr ? boundedStderr(rawStderr) : rawStderr;
80
- if (stderr === undefined)
81
- return permission;
82
- const token = FAILURE_TOKENS.find(([, pattern]) => pattern.test(stderr))?.[0];
83
- if (token) {
84
- const failure = { kind: 'sandbox-failure', detail: `stderr matched "${token}"` };
85
- return permission ? { ...permission, failure } : { ...failure, commandPrefix };
86
- }
87
- if (stderr.split(/\r\n|[\r\n]/).some((rawLine) => {
88
- const line = withoutUrlUserinfo(rawLine);
89
- return (CONNECTION_REFUSED.test(line) && LOOPBACK_ADDRESSES.some((pattern) => pattern.test(line)));
90
- })) {
91
- const failure = {
92
- kind: 'ambiguous-failure',
93
- detail: 'localhost connection refused; sandbox cause unverified',
94
- };
95
- return permission ? { ...permission, failure } : { ...failure, commandPrefix };
96
- }
97
- return permission;
98
- }
@@ -1,3 +0,0 @@
1
- export declare const REDACTED_TOKEN = "[REDACTED]";
2
- export declare function splitCommand(command: string): string[][];
3
- export declare function stripAssignments(tokens: string[]): string[];
@@ -1,94 +0,0 @@
1
- export const REDACTED_TOKEN = '[REDACTED]';
2
- const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
3
- const ENV_OPTIONS_WITH_ARGS = new Set(['-C', '--chdir', '-S', '--split-string', '-u', '--unset']);
4
- const ENV_OPTIONS_WITHOUT_ARGS = new Set([
5
- '-i',
6
- '--ignore-environment',
7
- '-0',
8
- '--null',
9
- '-v',
10
- '--debug',
11
- ]);
12
- export function splitCommand(command) {
13
- const result = [];
14
- let segment = [];
15
- let word = '';
16
- let quote;
17
- let escaped = false;
18
- const flushWord = () => {
19
- if (word)
20
- segment.push(word);
21
- word = '';
22
- };
23
- const flushSegment = () => {
24
- flushWord();
25
- if (segment.length)
26
- result.push(segment);
27
- segment = [];
28
- };
29
- for (const char of command) {
30
- if (escaped) {
31
- word += char;
32
- escaped = false;
33
- }
34
- else if (char === '\\' && quote !== "'") {
35
- escaped = true;
36
- }
37
- else if (quote) {
38
- if (char === quote)
39
- quote = undefined;
40
- else
41
- word += char;
42
- }
43
- else if (char === '"' || char === "'")
44
- quote = char;
45
- else if (';&|<>\n\r'.includes(char))
46
- flushSegment();
47
- else if (/\s/.test(char))
48
- flushWord();
49
- else
50
- word += char;
51
- }
52
- if (escaped)
53
- word += '\\';
54
- flushSegment();
55
- return result;
56
- }
57
- export function stripAssignments(tokens) {
58
- let index = 0;
59
- while (index < tokens.length && ENV_ASSIGNMENT.test(tokens[index]))
60
- index++;
61
- const wrapper = tokens[index];
62
- // Assignments without an executable are represented by a safe sentinel.
63
- if (wrapper !== 'env' && !wrapper?.endsWith('/env'))
64
- return index < tokens.length ? tokens.slice(index) : index ? [REDACTED_TOKEN] : tokens;
65
- index++;
66
- while (index < tokens.length) {
67
- const option = tokens[index];
68
- if (option === '--') {
69
- index++;
70
- break;
71
- }
72
- const optionName = option.split('=', 1)[0];
73
- const splitStringOption = optionName === '-S' || (optionName.length >= 5 && '--split-string'.startsWith(optionName));
74
- if (splitStringOption)
75
- return [REDACTED_TOKEN];
76
- const abbreviatedArgument = optionName.length >= 5 && '--chdir'.startsWith(optionName);
77
- if (ENV_OPTIONS_WITH_ARGS.has(optionName) || abbreviatedArgument) {
78
- index += option.includes('=') ? 1 : 2;
79
- continue;
80
- }
81
- if (ENV_OPTIONS_WITHOUT_ARGS.has(option)) {
82
- index++;
83
- continue;
84
- }
85
- if (option.startsWith('-'))
86
- return [REDACTED_TOKEN];
87
- break;
88
- }
89
- while (index < tokens.length && ENV_ASSIGNMENT.test(tokens[index]))
90
- index++;
91
- if (index < tokens.length)
92
- return tokens.slice(index);
93
- return tokens.some((token) => ENV_ASSIGNMENT.test(token)) ? [REDACTED_TOKEN] : [];
94
- }
@@ -1,10 +0,0 @@
1
- export type CoverageLedger = {
2
- schemaVersion: 1;
3
- retainedEvents: number;
4
- logBytes: number;
5
- droppedCount: number;
6
- };
7
- /** Caller holds the event log lock; incomplete atomic updates must never imply complete capture. */
8
- export declare function readCoverageLedger(path: string): CoverageLedger | undefined;
9
- /** The fixed-size atomic sidecar keeps saturation work independent of the accumulated drop count. */
10
- export declare function writeCoverageLedger(path: string, ledger: CoverageLedger): void;