vouchington-tooling 0.45.0 → 0.47.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 (88) hide show
  1. package/README.md +10 -52
  2. package/dist/agent-blackboard/feedback-audit-source.d.mts +4 -10
  3. package/dist/agent-blackboard/feedback-audit-source.mjs +7 -18
  4. package/dist/agent-blackboard/feedback-compose-audit.mjs +6 -14
  5. package/dist/agent-blackboard/feedback-compose.d.mts +1 -4
  6. package/dist/agent-blackboard/feedback-compose.mjs +2 -5
  7. package/dist/agent-blackboard/feedback-fields.mjs +1 -1
  8. package/dist/agent-blackboard/feedback-identity.mjs +1 -1
  9. package/dist/agent-blackboard/feedback-journal-audit.d.mts +6 -8
  10. package/dist/agent-blackboard/feedback-journal-audit.mjs +9 -10
  11. package/dist/agent-blackboard/feedback-journal-sandbox.d.mts +2 -2
  12. package/dist/agent-blackboard/feedback-journal-sandbox.mjs +2 -2
  13. package/dist/agent-blackboard/feedback-outbox-rejected.mjs +1 -1
  14. package/dist/agent-blackboard/feedback-outbox.mjs +2 -2
  15. package/dist/agent-blackboard/index.d.mts +1 -1
  16. package/dist/audit-journal/ci-failures.d.mts +6 -0
  17. package/dist/{session-friction → audit-journal}/ci-failures.mjs +3 -9
  18. package/dist/{session-friction → audit-journal}/journal.d.mts +0 -1
  19. package/dist/{session-friction → audit-journal}/journal.mjs +1 -1
  20. package/dist/audit-journal/session-id.d.mts +1 -0
  21. package/dist/{session-friction → audit-journal}/session-id.mjs +0 -5
  22. package/dist/{session-friction → audit-journal}/text.d.mts +1 -4
  23. package/dist/{session-friction → audit-journal}/text.mjs +4 -22
  24. package/dist/audit-journal/types.d.mts +17 -0
  25. package/dist/cli/commands/gha-runtime-audit.mjs +2 -0
  26. package/dist/gh-cli/env.d.mts +9 -0
  27. package/dist/gh-cli/env.mjs +12 -0
  28. package/dist/gh-cli/exec.d.mts +7 -4
  29. package/dist/gh-cli/exec.mjs +7 -5
  30. package/dist/gh-cli/index.d.mts +1 -0
  31. package/dist/gh-cli/index.mjs +1 -0
  32. package/dist/gha-post-review/github.mjs +2 -1
  33. package/dist/index.d.mts +3 -3
  34. package/dist/index.mjs +2 -2
  35. package/dist/mcp-server/context.d.mts +2 -0
  36. package/dist/mcp-server/dispatch.d.mts +2 -0
  37. package/dist/mcp-server/dispatch.mjs +1 -0
  38. package/dist/mcp-server/harness.test-helpers.d.mts +1 -0
  39. package/dist/mcp-server/harness.test-helpers.mjs +1 -0
  40. package/dist/mcp-server/pr-agent.d.mts +12 -0
  41. package/dist/mcp-server/pr-agent.mjs +34 -0
  42. package/dist/mcp-server/pr-body.d.mts +5 -3
  43. package/dist/mcp-server/pr-body.mjs +12 -13
  44. package/dist/mcp-server/pr-gh.mjs +2 -1
  45. package/dist/mcp-server/pr-input.d.mts +5 -2
  46. package/dist/mcp-server/pr-input.mjs +22 -4
  47. package/dist/mcp-server/pr-live.d.mts +1 -0
  48. package/dist/mcp-server/pr-live.mjs +1 -1
  49. package/dist/mcp-server/pr-prepare.mjs +25 -4
  50. package/dist/mcp-server/server.mjs +1 -0
  51. package/dist/mcp-server/tools-pr.mjs +20 -17
  52. package/dist/{session-friction → private-files}/lock.mjs +3 -2
  53. package/dist/private-files/log-file.d.mts +3 -0
  54. package/dist/private-files/log-file.mjs +53 -0
  55. package/dist/{session-friction → private-files}/utf8.mjs +1 -1
  56. package/dist/retrospective-facts/exec.mjs +2 -1
  57. package/docs/mcp-server.md +29 -10
  58. package/package.json +1 -6
  59. package/dist/session-friction/ci-failures.d.mts +0 -13
  60. package/dist/session-friction/classify.d.mts +0 -2
  61. package/dist/session-friction/classify.mjs +0 -98
  62. package/dist/session-friction/command-segments.d.mts +0 -3
  63. package/dist/session-friction/command-segments.mjs +0 -94
  64. package/dist/session-friction/coverage-ledger.d.mts +0 -10
  65. package/dist/session-friction/coverage-ledger.mjs +0 -75
  66. package/dist/session-friction/directory.d.mts +0 -1
  67. package/dist/session-friction/directory.mjs +0 -71
  68. package/dist/session-friction/index.d.mts +0 -6
  69. package/dist/session-friction/index.mjs +0 -5
  70. package/dist/session-friction/log-file.d.mts +0 -7
  71. package/dist/session-friction/log-file.mjs +0 -95
  72. package/dist/session-friction/log.d.mts +0 -8
  73. package/dist/session-friction/log.mjs +0 -163
  74. package/dist/session-friction/normalize.d.mts +0 -1
  75. package/dist/session-friction/normalize.mjs +0 -81
  76. package/dist/session-friction/report.d.mts +0 -16
  77. package/dist/session-friction/report.mjs +0 -91
  78. package/dist/session-friction/sandbox.d.mts +0 -3
  79. package/dist/session-friction/sandbox.mjs +0 -43
  80. package/dist/session-friction/session-id.d.mts +0 -2
  81. package/dist/session-friction/types.d.mts +0 -80
  82. /package/dist/{session-friction → audit-journal}/audit-block.d.mts +0 -0
  83. /package/dist/{session-friction → audit-journal}/audit-block.mjs +0 -0
  84. /package/dist/{session-friction → audit-journal}/audit-fit.d.mts +0 -0
  85. /package/dist/{session-friction → audit-journal}/audit-fit.mjs +0 -0
  86. /package/dist/{session-friction → audit-journal}/types.mjs +0 -0
  87. /package/dist/{session-friction → private-files}/lock.d.mts +0 -0
  88. /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');
@@ -286,8 +286,10 @@ shape is an error, never a pass.
286
286
  retrospective tools. All three take `sessionId` (required) and `worktree` (optional), need a server
287
287
  launched inside a repository worktree (they run the checkout policy), and share this structured
288
288
  input: `title` (single line, at most 256 characters), `summary`, `impact`, `relatedIssues`
289
- (`[{ number, relation: "closes" | "refs", reason }]`, may be empty), optional `noSourceIssue`
290
- (`{ reason }`, see below), `details` (string array), `base` (default `main`), `agent`, optional
289
+ (`[{ number, relation: "closes" | "refs", reason, repository? }]`, may be empty; the optional
290
+ `repository` is a single `owner/name` and renders the line as lowercase `owner/name#N`), optional
291
+ `noSourceIssue` (`{ reason }`, see below), `details` (string array), `base` (default `main`),
292
+ optional `agent`, optional
291
293
  `model`, optional `mode` (`interactive` | `autonomous`) and `workspaceSetup`. `pr_update` also
292
294
  requires `pr`; `pr_validate` accepts an optional `pr`.
293
295
 
@@ -305,11 +307,25 @@ argument can only narrow `interactive` to `autonomous` (it never widens), so an
305
307
  cannot unlock an interactive-only policy exception. The policy child receives no env markers, so it
306
308
  relies on this value.
307
309
 
310
+ ### Agent provenance
311
+
312
+ The `Agent:` line is derived by the server, never taken from the caller. The agent is the harness
313
+ that launched the server: `CODEX_THREAD_ID` (codex), `CLAUDECODE=1` or `CLAUDE_CODE_SESSION_ID`
314
+ (claude), `CURSOR_AGENT` or `CURSOR_SESSION_ID` (cursor), `GROK_AGENT`, `GROK_HOOK_EVENT` or
315
+ `GROK_SESSION_ID` (grok), in that order; failing those, the MCP client's `initialize`
316
+ `clientInfo.name` when it contains one of those names. The server has no source for the model.
317
+ `agent` is optional: a value naming the derived agent is ignored, any other value is rendered as
318
+ `(caller-claimed agent: <value>)`, and `model` as `(caller-claimed model: <value>)`. When no agent
319
+ is derivable the line says `unknown`. Making `agent` optional is compatible with existing callers,
320
+ which may keep sending it.
321
+
308
322
  ### Pull requests with no source issue
309
323
 
310
- Pass `noSourceIssue: { reason }` (a single line) with an empty `relatedIssues`; a non-empty
311
- `relatedIssues` beside it is rejected. The Related issues section then holds the generic statement
312
- `- No source issue — <reason>` instead of `- None`. A policy may replace the statement by
324
+ Pass `noSourceIssue: { reason }` (a single line) with an empty `relatedIssues`, or with a
325
+ `relatedIssues` whose every entry is `refs`; a `closes` entry beside it is rejected. The Related
326
+ issues section then holds the generic statement `- No source issue — <reason>` instead of `- None`,
327
+ preceded by the `- Refs … — <reason>` lines when there are any (the statement is always the last
328
+ line). A policy may replace the statement by
313
329
  exporting `renderNoSourceIssue({ reason })`, returning a non-empty string (30 s). The field also
314
330
  reaches `validatePullRequest` (with the trusted `mode`, so the policy can apply an interactive-only
315
331
  rule) and `pullRequestDefaults`.
@@ -327,15 +343,16 @@ rule) and `pullRequestDefaults`.
327
343
 
328
344
  ## Related issues
329
345
 
330
- - Closes #N — <reason> (or "Refs #N"; "- None" when empty;
331
- "- No source issue — <reason>" for noSourceIssue)
346
+ - Closes #N — <reason> (or "Refs #N"; owner/name#N with `repository`; "- None" when empty;
347
+ "- No source issue — <reason>" for noSourceIssue, rendered
348
+ after any Refs lines)
332
349
 
333
350
  ## Details
334
351
 
335
352
  <details joined by blank lines>
336
353
 
337
354
  Workspace setup: <workspaceSetup>
338
- Agent: <agent>[ (<model>)] session <sessionId>
355
+ Agent: <derived agent | unknown>[ (caller-claimed ...)] session <sessionId>
339
356
  Device: <os user>@<lowercased hostname>
340
357
  Worktree: <path after "/worktrees/", "main" for the main worktree, else the directory name>
341
358
  ```
@@ -357,8 +374,10 @@ with the repository's stack tooling instead.
357
374
  and `project`. It checks the head is pushed and current, writes the body to a mode 0600 temp file
358
375
  (removed afterwards) and runs `gh pr create --draft` with the labels, `--milestone` and
359
376
  `--project`. Returns `{ number, url, warnings }`.
360
- - `pr_update` (mutating): reads `gh pr view <pr> --json body,headRefName,baseRefName,state`,
361
- rejects a pull request that is not `OPEN` or whose head is not the worktree branch, and validates
377
+ - `pr_update` (mutating): reads `gh pr view <pr> --json body,headRefName,headRefOid,baseRefName,state`,
378
+ rejects a pull request that is not `OPEN` or whose head is neither the worktree branch nor the
379
+ worktree `HEAD` commit (an alias branch at the live head SHA is accepted and the live head name is
380
+ used), and validates
362
381
  against the live base (an explicit `base` must equal it). It renders, appends the live Shepherd
363
382
  Journal (a `<details><summary>Shepherd Journal</summary>` block, or a legacy `## Shepherd Journal`
364
383
  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.45.0",
3
+ "version": "0.47.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",
@@ -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;
@@ -1,75 +0,0 @@
1
- import { closeSync, constants, fsyncSync, lstatSync, openSync, renameSync } from 'node:fs';
2
- import { dirname } from 'node:path';
3
- import { openLogFile, readLogContent, writeAll, LOG_MAX_BYTES } from "./log-file.mjs";
4
- const LEDGER_MAX_BYTES = 192;
5
- function missing(error) {
6
- return !!error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
7
- }
8
- /** Caller holds the event log lock; incomplete atomic updates must never imply complete capture. */
9
- export function readCoverageLedger(path) {
10
- try {
11
- lstatSync(`${path}.coverage.pending`);
12
- throw new Error('session-friction coverage update is incomplete');
13
- }
14
- catch (error) {
15
- if (!missing(error))
16
- throw error;
17
- }
18
- let descriptor;
19
- try {
20
- descriptor = openLogFile(`${path}.coverage`, constants.O_RDONLY);
21
- }
22
- catch (error) {
23
- if (missing(error))
24
- return undefined;
25
- throw error;
26
- }
27
- try {
28
- const value = JSON.parse(readLogContent(descriptor, LEDGER_MAX_BYTES));
29
- if (!value || typeof value !== 'object')
30
- throw new Error('invalid session-friction coverage ledger');
31
- // oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- establish a validated object view
32
- const item = value;
33
- if (Object.keys(item).length !== 4 ||
34
- item.schemaVersion !== 1 ||
35
- !Number.isSafeInteger(item.retainedEvents) ||
36
- item.retainedEvents < 0 ||
37
- item.retainedEvents > 500 ||
38
- !Number.isSafeInteger(item.logBytes) ||
39
- item.logBytes < 0 ||
40
- item.logBytes > LOG_MAX_BYTES ||
41
- !Number.isSafeInteger(item.droppedCount) ||
42
- item.droppedCount < 1)
43
- throw new Error('invalid session-friction coverage ledger');
44
- return item;
45
- }
46
- finally {
47
- closeSync(descriptor);
48
- }
49
- }
50
- /** The fixed-size atomic sidecar keeps saturation work independent of the accumulated drop count. */
51
- export function writeCoverageLedger(path, ledger) {
52
- if (!Number.isSafeInteger(ledger.droppedCount))
53
- throw new Error('session-friction dropped count overflow; capture incomplete');
54
- const temporary = `${path}.coverage.pending`;
55
- const descriptor = openSync(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
56
- try {
57
- syncDirectory(path);
58
- writeAll(descriptor, JSON.stringify(ledger));
59
- fsyncSync(descriptor);
60
- }
61
- finally {
62
- closeSync(descriptor);
63
- }
64
- renameSync(temporary, `${path}.coverage`);
65
- syncDirectory(path);
66
- }
67
- function syncDirectory(path) {
68
- const descriptor = openSync(dirname(path), constants.O_RDONLY | constants.O_NOFOLLOW);
69
- try {
70
- fsyncSync(descriptor);
71
- }
72
- finally {
73
- closeSync(descriptor);
74
- }
75
- }
@@ -1 +0,0 @@
1
- export declare function ensurePrivateDirectory(directory: string, create: boolean): boolean;
@@ -1,71 +0,0 @@
1
- import { chmodSync, lstatSync, mkdirSync, statSync } from 'node:fs';
2
- import { dirname } from 'node:path';
3
- const PRIVATE_DIRECTORY_MODE = 0o700;
4
- function isCode(error, code) {
5
- return Boolean(error && typeof error === 'object' && 'code' in error && error.code === code);
6
- }
7
- function directoryChain(directory) {
8
- const chain = [directory];
9
- let current = directory;
10
- while (dirname(current) !== current) {
11
- current = dirname(current);
12
- chain.push(current);
13
- }
14
- return chain.reverse();
15
- }
16
- export function ensurePrivateDirectory(directory, create) {
17
- const chain = directoryChain(directory);
18
- const effectiveUserId = process.geteuid?.();
19
- if (effectiveUserId === undefined)
20
- throw new Error('session-friction requires POSIX directory permissions');
21
- for (const [index, path] of chain.entries()) {
22
- let created = false;
23
- let status;
24
- try {
25
- status = lstatSync(path);
26
- }
27
- catch (error) {
28
- /* v8 ignore next -- unexpected filesystem errors are propagated unchanged. */
29
- if (!isCode(error, 'ENOENT'))
30
- throw error;
31
- if (!create)
32
- return false;
33
- try {
34
- mkdirSync(path, { mode: PRIVATE_DIRECTORY_MODE });
35
- created = true;
36
- }
37
- catch (mkdirError) {
38
- /* v8 ignore next -- another process can create the directory after lstat. */
39
- if (!isCode(mkdirError, 'EEXIST'))
40
- throw mkdirError;
41
- }
42
- status = lstatSync(path);
43
- }
44
- const leaf = index === chain.length - 1;
45
- if (status.isSymbolicLink()) {
46
- if (leaf || status.uid !== 0)
47
- throw new Error('session-friction log directory must be a private directory');
48
- try {
49
- status = statSync(path);
50
- }
51
- catch {
52
- throw new Error('session-friction log directory must be a private directory');
53
- }
54
- }
55
- if (!status.isDirectory())
56
- throw new Error('session-friction log directory must be a private directory');
57
- if (created)
58
- chmodSync(path, PRIVATE_DIRECTORY_MODE);
59
- if (!created) {
60
- const ownedByCaller = status.uid === effectiveUserId;
61
- const ownedByRoot = status.uid === 0;
62
- const writableByOthers = (status.mode & 0o022) !== 0;
63
- const sticky = (status.mode & 0o1000) !== 0;
64
- if ((leaf && ((status.mode & 0o777) !== PRIVATE_DIRECTORY_MODE || !ownedByCaller)) ||
65
- (!leaf && !ownedByCaller && !ownedByRoot) ||
66
- (!leaf && writableByOthers && !sticky))
67
- throw new Error('session-friction log directory must be a private directory');
68
- }
69
- }
70
- return true;
71
- }
@@ -1,6 +0,0 @@
1
- export { classifyFrictionObservation } from './classify.mts';
2
- export { normalizeCommandPrefix } from './normalize.mts';
3
- export { FRICTION_LOG_MAX_EVENTS, readFrictionLog, recordFriction } from './log.mts';
4
- export { buildSessionFrictionReport, loadFrictionReportInputs, renderFrictionReport, type FrictionReportInputs, } from './report.mts';
5
- export { isConformingCiFailureBlock } from './ci-failures.mts';
6
- export type { FrictionEvent, FrictionEventKind, FrictionLogOptions, FrictionLogReadResult, FrictionObservation, JournalEntry, JournalLoadResult, JournalLoader, PermissionRequestObservation, SessionFrictionReport, SessionFrictionCoverage, SessionFrictionReportOptions, ToolResultObservation, } from './types.mts';
@@ -1,5 +0,0 @@
1
- export { classifyFrictionObservation } from "./classify.mjs";
2
- export { normalizeCommandPrefix } from "./normalize.mjs";
3
- export { FRICTION_LOG_MAX_EVENTS, readFrictionLog, recordFriction } from "./log.mjs";
4
- export { buildSessionFrictionReport, loadFrictionReportInputs, renderFrictionReport, } from "./report.mjs";
5
- export { isConformingCiFailureBlock } from "./ci-failures.mjs";
@@ -1,7 +0,0 @@
1
- import type { FrictionEvent } from './types.mts';
2
- export declare const LOG_MAX_BYTES = 2000000;
3
- export declare function validEvent(value: unknown): value is FrictionEvent;
4
- export declare function openLogFile(path: string, flags: number): number;
5
- export declare function readLogContent(descriptor: number, maximumBytes?: number): string;
6
- export declare function validEventCount(content: string, limit: number): number;
7
- export declare function writeAll(descriptor: number, value: string): void;