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.
- package/README.md +10 -52
- package/dist/agent-blackboard/feedback-audit-source.d.mts +4 -10
- package/dist/agent-blackboard/feedback-audit-source.mjs +7 -18
- package/dist/agent-blackboard/feedback-compose-audit.mjs +6 -14
- package/dist/agent-blackboard/feedback-compose.d.mts +1 -4
- package/dist/agent-blackboard/feedback-compose.mjs +2 -5
- package/dist/agent-blackboard/feedback-fields.mjs +1 -1
- package/dist/agent-blackboard/feedback-identity.mjs +1 -1
- package/dist/agent-blackboard/feedback-journal-audit.d.mts +6 -8
- package/dist/agent-blackboard/feedback-journal-audit.mjs +9 -10
- package/dist/agent-blackboard/feedback-journal-sandbox.d.mts +2 -2
- package/dist/agent-blackboard/feedback-journal-sandbox.mjs +2 -2
- package/dist/agent-blackboard/feedback-outbox-rejected.mjs +1 -1
- package/dist/agent-blackboard/feedback-outbox.mjs +2 -2
- package/dist/agent-blackboard/index.d.mts +1 -1
- package/dist/audit-journal/ci-failures.d.mts +6 -0
- package/dist/{session-friction → audit-journal}/ci-failures.mjs +3 -9
- package/dist/{session-friction → audit-journal}/journal.d.mts +0 -1
- package/dist/{session-friction → audit-journal}/journal.mjs +1 -1
- package/dist/audit-journal/session-id.d.mts +1 -0
- package/dist/{session-friction → audit-journal}/session-id.mjs +0 -5
- package/dist/{session-friction → audit-journal}/text.d.mts +1 -4
- package/dist/{session-friction → audit-journal}/text.mjs +4 -22
- package/dist/audit-journal/types.d.mts +17 -0
- package/dist/cli/commands/gha-runtime-audit.mjs +2 -0
- package/dist/gh-cli/env.d.mts +9 -0
- package/dist/gh-cli/env.mjs +12 -0
- package/dist/gh-cli/exec.d.mts +7 -4
- package/dist/gh-cli/exec.mjs +7 -5
- package/dist/gh-cli/index.d.mts +1 -0
- package/dist/gh-cli/index.mjs +1 -0
- package/dist/gha-post-review/github.mjs +2 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/mcp-server/context.d.mts +2 -0
- package/dist/mcp-server/dispatch.d.mts +2 -0
- package/dist/mcp-server/dispatch.mjs +1 -0
- package/dist/mcp-server/harness.test-helpers.d.mts +1 -0
- package/dist/mcp-server/harness.test-helpers.mjs +1 -0
- package/dist/mcp-server/pr-agent.d.mts +12 -0
- package/dist/mcp-server/pr-agent.mjs +34 -0
- package/dist/mcp-server/pr-body.d.mts +5 -3
- package/dist/mcp-server/pr-body.mjs +12 -13
- package/dist/mcp-server/pr-gh.mjs +2 -1
- package/dist/mcp-server/pr-input.d.mts +5 -2
- package/dist/mcp-server/pr-input.mjs +22 -4
- package/dist/mcp-server/pr-live.d.mts +1 -0
- package/dist/mcp-server/pr-live.mjs +1 -1
- package/dist/mcp-server/pr-prepare.mjs +25 -4
- package/dist/mcp-server/server.mjs +1 -0
- package/dist/mcp-server/tools-pr.mjs +20 -17
- package/dist/{session-friction → private-files}/lock.mjs +3 -2
- package/dist/private-files/log-file.d.mts +3 -0
- package/dist/private-files/log-file.mjs +53 -0
- package/dist/{session-friction → private-files}/utf8.mjs +1 -1
- package/dist/retrospective-facts/exec.mjs +2 -1
- package/docs/mcp-server.md +29 -10
- package/package.json +1 -6
- package/dist/session-friction/ci-failures.d.mts +0 -13
- package/dist/session-friction/classify.d.mts +0 -2
- package/dist/session-friction/classify.mjs +0 -98
- package/dist/session-friction/command-segments.d.mts +0 -3
- package/dist/session-friction/command-segments.mjs +0 -94
- package/dist/session-friction/coverage-ledger.d.mts +0 -10
- package/dist/session-friction/coverage-ledger.mjs +0 -75
- package/dist/session-friction/directory.d.mts +0 -1
- package/dist/session-friction/directory.mjs +0 -71
- package/dist/session-friction/index.d.mts +0 -6
- package/dist/session-friction/index.mjs +0 -5
- package/dist/session-friction/log-file.d.mts +0 -7
- package/dist/session-friction/log-file.mjs +0 -95
- package/dist/session-friction/log.d.mts +0 -8
- package/dist/session-friction/log.mjs +0 -163
- package/dist/session-friction/normalize.d.mts +0 -1
- package/dist/session-friction/normalize.mjs +0 -81
- package/dist/session-friction/report.d.mts +0 -16
- package/dist/session-friction/report.mjs +0 -91
- package/dist/session-friction/sandbox.d.mts +0 -3
- package/dist/session-friction/sandbox.mjs +0 -43
- package/dist/session-friction/session-id.d.mts +0 -2
- package/dist/session-friction/types.d.mts +0 -80
- /package/dist/{session-friction → audit-journal}/audit-block.d.mts +0 -0
- /package/dist/{session-friction → audit-journal}/audit-block.mjs +0 -0
- /package/dist/{session-friction → audit-journal}/audit-fit.d.mts +0 -0
- /package/dist/{session-friction → audit-journal}/audit-fit.mjs +0 -0
- /package/dist/{session-friction → audit-journal}/types.mjs +0 -0
- /package/dist/{session-friction → private-files}/lock.d.mts +0 -0
- /package/dist/{session-friction → private-files}/utf8.d.mts +0 -0
package/dist/gh-cli/exec.d.mts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/** A text-mode command runner: argv in, stdout out. Rejects on a non-zero exit. */
|
|
2
2
|
export type RunTextCommand = (args: string[]) => Promise<string>;
|
|
3
3
|
/** The `child_process.execFile` shape `createCommandRunner` wraps — injectable for tests. */
|
|
4
|
-
export type ExecFileText = (command: string, args: string[]
|
|
4
|
+
export type ExecFileText = (command: string, args: string[], options?: {
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
}) => Promise<{
|
|
5
7
|
stdout: string;
|
|
6
8
|
}>;
|
|
7
9
|
/**
|
|
8
10
|
* Builds a `RunTextCommand` bound to a fixed binary. The default `exec` wraps `execFile` via
|
|
9
11
|
* `promisify` so a test can inject a fake without spawning a real process; `runGh`/`runGit` below
|
|
10
|
-
* are this factory applied to the two binaries this module cares about.
|
|
12
|
+
* are this factory applied to the two binaries this module cares about. `buildEnv` maps the
|
|
13
|
+
* parent environment to the one the child is spawned with (identity by default).
|
|
11
14
|
*/
|
|
12
|
-
export declare function createCommandRunner(command: string, exec?: ExecFileText): RunTextCommand;
|
|
13
|
-
/** Runs `gh` and returns its stdout. */
|
|
15
|
+
export declare function createCommandRunner(command: string, exec?: ExecFileText, buildEnv?: (env: NodeJS.ProcessEnv) => NodeJS.ProcessEnv): RunTextCommand;
|
|
16
|
+
/** Runs `gh` and returns its stdout, with `CLICOLOR_FORCE` removed so `--json` output stays plain. */
|
|
14
17
|
export declare const runGh: RunTextCommand;
|
|
15
18
|
/** Runs `git` and returns its stdout. */
|
|
16
19
|
export declare const runGit: RunTextCommand;
|
package/dist/gh-cli/exec.mjs
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
+
import { ghSpawnEnv } from "./env.mjs";
|
|
3
4
|
const execFileAsync = promisify(execFile);
|
|
4
5
|
/**
|
|
5
6
|
* Builds a `RunTextCommand` bound to a fixed binary. The default `exec` wraps `execFile` via
|
|
6
7
|
* `promisify` so a test can inject a fake without spawning a real process; `runGh`/`runGit` below
|
|
7
|
-
* are this factory applied to the two binaries this module cares about.
|
|
8
|
+
* are this factory applied to the two binaries this module cares about. `buildEnv` maps the
|
|
9
|
+
* parent environment to the one the child is spawned with (identity by default).
|
|
8
10
|
*/
|
|
9
|
-
export function createCommandRunner(command, exec = (cmd, args) => execFileAsync(cmd, args)) {
|
|
11
|
+
export function createCommandRunner(command, exec = (cmd, args, options) => execFileAsync(cmd, args, { ...options, encoding: 'utf8' }), buildEnv = (env) => env) {
|
|
10
12
|
return async (args) => {
|
|
11
|
-
const { stdout } = await exec(command, args);
|
|
13
|
+
const { stdout } = await exec(command, args, { env: buildEnv(process.env) });
|
|
12
14
|
return stdout;
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
|
-
/** Runs `gh` and returns its stdout. */
|
|
16
|
-
export const runGh = createCommandRunner('gh');
|
|
17
|
+
/** Runs `gh` and returns its stdout, with `CLICOLOR_FORCE` removed so `--json` output stays plain. */
|
|
18
|
+
export const runGh = createCommandRunner('gh', undefined, ghSpawnEnv);
|
|
17
19
|
/** Runs `git` and returns its stdout. */
|
|
18
20
|
export const runGit = createCommandRunner('git');
|
package/dist/gh-cli/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createCommandRunner, runGh, runGit } from './exec.mts';
|
|
2
|
+
export { ghSpawnEnv } from './env.mts';
|
|
2
3
|
export type { ExecFileText, RunTextCommand } from './exec.mts';
|
|
3
4
|
export { DiffCommandError, processDiffCommand } from './diff.mts';
|
|
4
5
|
export type { DiffBlockCallback, DiffCommand } from './diff.mts';
|
package/dist/gh-cli/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createCommandRunner, runGh, runGit } from "./exec.mjs";
|
|
2
|
+
export { ghSpawnEnv } from "./env.mjs";
|
|
2
3
|
export { DiffCommandError, processDiffCommand } from "./diff.mjs";
|
|
3
4
|
export { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from "./body-length.mjs";
|
|
4
5
|
export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from "./pr-create.mjs";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { appendFileSync } from 'node:fs';
|
|
3
|
+
import { ghSpawnEnv } from "../gh-cli/env.mjs";
|
|
3
4
|
import { ReviewPayloadError } from "../gha-review-payload/index.mjs";
|
|
4
5
|
import { readRegularReviewPayload } from "../gha-review-payload/index.mjs";
|
|
5
6
|
import { createActionsClaudeTokenIo, withClaudeAppToken } from "./claude-token.mjs";
|
|
@@ -11,7 +12,7 @@ export function createGhExec(exec = execFileSync) {
|
|
|
11
12
|
return (args, options) => exec('gh', [...args], {
|
|
12
13
|
encoding: 'utf8',
|
|
13
14
|
input: options?.input,
|
|
14
|
-
env: options?.env ?? process.env,
|
|
15
|
+
env: ghSpawnEnv(options?.env ?? process.env),
|
|
15
16
|
}).trim();
|
|
16
17
|
}
|
|
17
18
|
export function writePostedOutput(posted, commentCount, outputPath = process.env.GITHUB_OUTPUT) {
|
package/dist/index.d.mts
CHANGED
|
@@ -8,8 +8,8 @@ export { runRetrospectiveFacts } from './retrospective-facts/index.mts';
|
|
|
8
8
|
export type { CommandExecutor, CommandResult, RetrospectiveFactsOptions, } from './retrospective-facts/index.mts';
|
|
9
9
|
export { appendJournal, assertSessionId, cleanupSnapshotPartitions, partitionSnapshot, probeBlackboard, readJournal, resolveBlackboardConnection, } from './agent-blackboard/index.mts';
|
|
10
10
|
export type { BlackboardConnection, SnapshotChecksum, SnapshotCleanupReceipt, SnapshotCleanupOptions, SnapshotCounts, SnapshotManifest, SnapshotPartition, SnapshotPartitionOptions, SnapshotPartitionResult, SnapshotSelection, } from './agent-blackboard/index.mts';
|
|
11
|
-
export {
|
|
12
|
-
export type {
|
|
11
|
+
export { isConformingCiFailureBlock } from './audit-journal/ci-failures.mts';
|
|
12
|
+
export type { JournalEntry, JournalLoader, JournalLoadResult } from './audit-journal/types.mts';
|
|
13
13
|
export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOnRunnerUnreservedEphemeralPort, loadRunnerPortPolicy, runnerPortPolicy, validateRunnerPortPolicy, } from './runner-port-policy/index.mts';
|
|
14
14
|
export type { EphemeralListenerOptions, RunnerPortPolicy } from './runner-port-policy/index.mts';
|
|
15
15
|
export { extractCreateTableMetadata, extractDefaultFunction, extractFuncCallArgColumnNames, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from './sql-ast/index.mts';
|
|
@@ -95,7 +95,7 @@ export { validateResolvedPinDelta } from './swift-resolved-pin-delta/index.mts';
|
|
|
95
95
|
export type { ResolvedDocument, ResolvedPin, ValidateResolvedPinDeltaOptions, } from './swift-resolved-pin-delta/index.mts';
|
|
96
96
|
export { DEFAULT_MAX_DIAGNOSTIC_REPORTS, DEFAULT_MAX_FORMATTED_DIAGNOSTIC_REPORTS, formatDiagnosticReportSummaries, HARD_MAX_DIAGNOSTIC_REPORTS, readDiagnosticReportSummaries, summarizeDiagnosticReport, } from './vitest-diagnostics/index.mts';
|
|
97
97
|
export type { DiagnosticReportLimitOptions, DiagnosticReportSummary, } from './vitest-diagnostics/index.mts';
|
|
98
|
-
export { createCommandRunner, runGh, runGit } from './gh-cli/index.mts';
|
|
98
|
+
export { createCommandRunner, ghSpawnEnv, runGh, runGit } from './gh-cli/index.mts';
|
|
99
99
|
export type { ExecFileText, RunTextCommand } from './gh-cli/index.mts';
|
|
100
100
|
export { DiffCommandError, processDiffCommand } from './gh-cli/index.mts';
|
|
101
101
|
export type { DiffBlockCallback, DiffCommand } from './gh-cli/index.mts';
|
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ export { HARNESS_IDS, inspectHarnessEnvironment, selectHarnessSession, } from ".
|
|
|
4
4
|
export { codexChildren, codexIdentity, computeTranscriptFacts, formatTranscriptFacts, formatUnavailable, resolveTranscriptFile, runRetrospectiveTranscript, } from "./retrospective-transcript/index.mjs";
|
|
5
5
|
export { runRetrospectiveFacts } from "./retrospective-facts/index.mjs";
|
|
6
6
|
export { appendJournal, assertSessionId, cleanupSnapshotPartitions, partitionSnapshot, probeBlackboard, readJournal, resolveBlackboardConnection, } from "./agent-blackboard/index.mjs";
|
|
7
|
-
export {
|
|
7
|
+
export { isConformingCiFailureBlock } from "./audit-journal/ci-failures.mjs";
|
|
8
8
|
export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOnRunnerUnreservedEphemeralPort, loadRunnerPortPolicy, runnerPortPolicy, validateRunnerPortPolicy, } from "./runner-port-policy/index.mjs";
|
|
9
9
|
export { extractCreateTableMetadata, extractDefaultFunction, extractFuncCallArgColumnNames, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from "./sql-ast/index.mjs";
|
|
10
10
|
export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from "./sql-scanner/index.mjs";
|
|
@@ -52,7 +52,7 @@ export { normalizeSwiftSource } from "./swift-semantic-equal/index.mjs";
|
|
|
52
52
|
export { isSwiftCodeOffset, parseUniqueSwiftBinaryTargetChecksum, } from "./swift-source-offset/index.mjs";
|
|
53
53
|
export { validateResolvedPinDelta } from "./swift-resolved-pin-delta/index.mjs";
|
|
54
54
|
export { DEFAULT_MAX_DIAGNOSTIC_REPORTS, DEFAULT_MAX_FORMATTED_DIAGNOSTIC_REPORTS, formatDiagnosticReportSummaries, HARD_MAX_DIAGNOSTIC_REPORTS, readDiagnosticReportSummaries, summarizeDiagnosticReport, } from "./vitest-diagnostics/index.mjs";
|
|
55
|
-
export { createCommandRunner, runGh, runGit } from "./gh-cli/index.mjs";
|
|
55
|
+
export { createCommandRunner, ghSpawnEnv, runGh, runGit } from "./gh-cli/index.mjs";
|
|
56
56
|
export { DiffCommandError, processDiffCommand } from "./gh-cli/index.mjs";
|
|
57
57
|
export { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from "./gh-cli/index.mjs";
|
|
58
58
|
export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from "./gh-cli/index.mjs";
|
|
@@ -21,6 +21,8 @@ export type ToolContext = {
|
|
|
21
21
|
runGit: RunTextCommand;
|
|
22
22
|
/** Test seam: replaces locating and running gh. */
|
|
23
23
|
runGh?: GhRunner;
|
|
24
|
+
/** The connected MCP client's `clientInfo.name`, once `initialize` has completed. */
|
|
25
|
+
clientName?: () => string | undefined;
|
|
24
26
|
/** Test seam: replaces the operating system user and host name. */
|
|
25
27
|
device?: () => {
|
|
26
28
|
user: string;
|
|
@@ -16,6 +16,8 @@ export type ServerEnvironment = {
|
|
|
16
16
|
spawnPolicy?: PolicySpawner;
|
|
17
17
|
/** Test seam: replaces locating and running gh. */
|
|
18
18
|
runGh?: GhRunner;
|
|
19
|
+
/** The connected MCP client's `clientInfo.name`, once `initialize` has completed. */
|
|
20
|
+
clientName?: () => string | undefined;
|
|
19
21
|
/** Test seam: replaces the operating system user and host name. */
|
|
20
22
|
device?: () => {
|
|
21
23
|
user: string;
|
|
@@ -67,6 +67,7 @@ export async function callTool(name, rawArguments, environment) {
|
|
|
67
67
|
runGit: environment.runGit,
|
|
68
68
|
snapshots: environment.snapshots ?? PROCESS_SNAPSHOTS,
|
|
69
69
|
...(environment.runGh === undefined ? {} : { runGh: environment.runGh }),
|
|
70
|
+
...(environment.clientName === undefined ? {} : { clientName: environment.clientName }),
|
|
70
71
|
...(environment.device === undefined ? {} : { device: environment.device }),
|
|
71
72
|
...(environment.spawnPolicy === undefined ? {} : { spawnPolicy: environment.spawnPolicy }),
|
|
72
73
|
// The client resolves from this package's install, like the SDK, never from the worktree.
|
|
@@ -23,6 +23,7 @@ export declare function harness(fixture: RepoFixture, options?: FakeOptions & {
|
|
|
23
23
|
host: string;
|
|
24
24
|
};
|
|
25
25
|
snapshots?: SnapshotRegistry;
|
|
26
|
+
clientName?: () => string | undefined;
|
|
26
27
|
}): Harness;
|
|
27
28
|
export declare function textOf(result: ToolCallResult): string;
|
|
28
29
|
export declare function jsonOf(result: ToolCallResult): Record<string, unknown>;
|
|
@@ -28,6 +28,7 @@ export function harness(fixture, options = {}) {
|
|
|
28
28
|
...(options.runGh === undefined ? {} : { runGh: options.runGh }),
|
|
29
29
|
...(options.device === undefined ? {} : { device: options.device }),
|
|
30
30
|
...(options.snapshots === undefined ? {} : { snapshots: options.snapshots }),
|
|
31
|
+
...(options.clientName === undefined ? {} : { clientName: options.clientName }),
|
|
31
32
|
};
|
|
32
33
|
return { fake, call: (name, args) => callTool(name, args, environment) };
|
|
33
34
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type HarnessId } from '../agent-harness-identity/index.mts';
|
|
2
|
+
/**
|
|
3
|
+
* The harness that launched this server, from the launch environment first and the MCP client's
|
|
4
|
+
* `initialize` `clientInfo.name` second. Neither comes from a tool argument.
|
|
5
|
+
*/
|
|
6
|
+
export declare function deriveAgent(env: NodeJS.ProcessEnv, clientName?: string): HarnessId | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* The text of the `Agent:` line before the session. The agent is the derived harness; a caller
|
|
9
|
+
* `agent` that does not name it, or any `model`, is shown as caller-claimed and never replaces it.
|
|
10
|
+
* With nothing derivable the agent is `unknown`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function agentLabel(derived: HarnessId | undefined, claimedAgent: string | undefined, claimedModel: string | undefined): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { inspectHarnessEnvironment } from "../agent-harness-identity/index.mjs";
|
|
2
|
+
const PRECEDENCE = ['codex', 'claude', 'cursor', 'grok'];
|
|
3
|
+
/**
|
|
4
|
+
* The harness that launched this server, from the launch environment first and the MCP client's
|
|
5
|
+
* `initialize` `clientInfo.name` second. Neither comes from a tool argument.
|
|
6
|
+
*/
|
|
7
|
+
export function deriveAgent(env, clientName) {
|
|
8
|
+
const found = inspectHarnessEnvironment(env);
|
|
9
|
+
const present = {
|
|
10
|
+
codex: Boolean(found.codex.sessionId),
|
|
11
|
+
claude: found.claude.compatMode || Boolean(found.claude.sessionId),
|
|
12
|
+
cursor: found.cursor.agentMarker || Boolean(found.cursor.sessionId),
|
|
13
|
+
grok: found.grok.agentMarker || found.grok.hookEvent || Boolean(found.grok.sessionId),
|
|
14
|
+
};
|
|
15
|
+
const fromEnv = PRECEDENCE.find((id) => present[id]);
|
|
16
|
+
if (fromEnv !== undefined)
|
|
17
|
+
return fromEnv;
|
|
18
|
+
const name = clientName?.toLowerCase() ?? '';
|
|
19
|
+
return PRECEDENCE.find((id) => name.includes(id));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The text of the `Agent:` line before the session. The agent is the derived harness; a caller
|
|
23
|
+
* `agent` that does not name it, or any `model`, is shown as caller-claimed and never replaces it.
|
|
24
|
+
* With nothing derivable the agent is `unknown`.
|
|
25
|
+
*/
|
|
26
|
+
export function agentLabel(derived, claimedAgent, claimedModel) {
|
|
27
|
+
const notes = [];
|
|
28
|
+
const claim = claimedAgent?.trim();
|
|
29
|
+
if (claim && !(derived !== undefined && claim.toLowerCase().includes(derived)))
|
|
30
|
+
notes.push(`caller-claimed agent: ${claim}`);
|
|
31
|
+
if (claimedModel)
|
|
32
|
+
notes.push(`caller-claimed model: ${claimedModel}`);
|
|
33
|
+
return `${derived ?? 'unknown'}${notes.length ? ` (${notes.join('; ')})` : ''}`;
|
|
34
|
+
}
|
|
@@ -7,12 +7,14 @@ export type PullRequestProvenance = {
|
|
|
7
7
|
host: string;
|
|
8
8
|
/** The checkout label, see {@link worktreeLabel}. */
|
|
9
9
|
worktree: string;
|
|
10
|
+
/** The Agent line text before the session, see `agentLabel`. */
|
|
11
|
+
agent: string;
|
|
10
12
|
};
|
|
11
13
|
/**
|
|
12
14
|
* The pull request body: the Summary, Impact, Related issues and Details sections followed by the
|
|
13
|
-
* provenance lines. With `noSourceIssue` the Related issues section
|
|
14
|
-
* (default {@link defaultNoSourceStatement}) instead of `- None`. Pure;
|
|
15
|
-
* device and worktree facts.
|
|
15
|
+
* provenance lines. With `noSourceIssue` the Related issues section ends with `noSourceStatement`
|
|
16
|
+
* (default {@link defaultNoSourceStatement}), after any `Refs` lines, instead of `- None`. Pure;
|
|
17
|
+
* the caller supplies the device and worktree facts.
|
|
16
18
|
*/
|
|
17
19
|
export declare function renderPullRequestBody(input: PullRequestInput, provenance: PullRequestProvenance, noSourceStatement?: string): string;
|
|
18
20
|
/**
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
|
+
import { formatReferenceKey } from "../gh-cli/closing-refs.mjs";
|
|
2
3
|
function relatedLine(issue) {
|
|
3
4
|
const verb = issue.relation === 'closes' ? 'Closes' : 'Refs';
|
|
4
|
-
|
|
5
|
+
const [owner, repo] = issue.repository?.split('/') ?? [];
|
|
6
|
+
return `- ${verb} ${formatReferenceKey({ number: issue.number, owner, repo })} — ${issue.reason}`;
|
|
5
7
|
}
|
|
6
8
|
/** The default no-source statement; a policy `renderNoSourceIssue` export may replace it. */
|
|
7
9
|
function defaultNoSourceStatement(reason) {
|
|
@@ -9,19 +11,16 @@ function defaultNoSourceStatement(reason) {
|
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* The pull request body: the Summary, Impact, Related issues and Details sections followed by the
|
|
12
|
-
* provenance lines. With `noSourceIssue` the Related issues section
|
|
13
|
-
* (default {@link defaultNoSourceStatement}) instead of `- None`. Pure;
|
|
14
|
-
* device and worktree facts.
|
|
14
|
+
* provenance lines. With `noSourceIssue` the Related issues section ends with `noSourceStatement`
|
|
15
|
+
* (default {@link defaultNoSourceStatement}), after any `Refs` lines, instead of `- None`. Pure;
|
|
16
|
+
* the caller supplies the device and worktree facts.
|
|
15
17
|
*/
|
|
16
18
|
export function renderPullRequestBody(input, provenance, noSourceStatement) {
|
|
17
|
-
const related = input.relatedIssues.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
: (noSourceStatement ?? defaultNoSourceStatement(input.noSourceIssue.reason)),
|
|
23
|
-
];
|
|
24
|
-
const agent = input.model === undefined ? input.agent : `${input.agent} (${input.model})`;
|
|
19
|
+
const related = input.relatedIssues.map(relatedLine);
|
|
20
|
+
if (input.noSourceIssue !== undefined)
|
|
21
|
+
related.push(noSourceStatement ?? defaultNoSourceStatement(input.noSourceIssue.reason));
|
|
22
|
+
if (!related.length)
|
|
23
|
+
related.push('- None');
|
|
25
24
|
return [
|
|
26
25
|
'## Summary',
|
|
27
26
|
'',
|
|
@@ -40,7 +39,7 @@ export function renderPullRequestBody(input, provenance, noSourceStatement) {
|
|
|
40
39
|
input.details.join('\n\n'),
|
|
41
40
|
'',
|
|
42
41
|
`Workspace setup: ${input.workspaceSetup}`,
|
|
43
|
-
`Agent: ${agent} session ${provenance.sessionId}`,
|
|
42
|
+
`Agent: ${provenance.agent} session ${provenance.sessionId}`,
|
|
44
43
|
`Device: ${provenance.user}@${provenance.host.toLowerCase()}`,
|
|
45
44
|
`Worktree: ${provenance.worktree}`,
|
|
46
45
|
'',
|
|
@@ -3,6 +3,7 @@ import { constants } from 'node:fs';
|
|
|
3
3
|
import { access, stat } from 'node:fs/promises';
|
|
4
4
|
import { delimiter, isAbsolute, join } from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import { ghSpawnEnv } from "../gh-cli/env.mjs";
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
const FALLBACKS = ['/opt/homebrew/bin/gh', '/usr/local/bin/gh', '/usr/bin/gh'];
|
|
8
9
|
const MAX_BUFFER_BYTES = 16_000_000;
|
|
@@ -36,7 +37,7 @@ export async function locateGh(env, fallbacks = FALLBACKS) {
|
|
|
36
37
|
* GH_REPO, which would point gh at a repository other than the validated worktree's.
|
|
37
38
|
*/
|
|
38
39
|
export function ghEnvironment(env) {
|
|
39
|
-
const { AGENT_BLACKBOARD_URL: _url, AGENT_BLACKBOARD_TOKEN: _token, GH_REPO: _repo, ...rest } = env;
|
|
40
|
+
const { AGENT_BLACKBOARD_URL: _url, AGENT_BLACKBOARD_TOKEN: _token, GH_REPO: _repo, ...rest } = ghSpawnEnv(env);
|
|
40
41
|
return rest;
|
|
41
42
|
}
|
|
42
43
|
/** A runner that locates gh once, on first use, from the server's environment. */
|
|
@@ -4,19 +4,22 @@ export type RelatedIssue = {
|
|
|
4
4
|
number: number;
|
|
5
5
|
relation: 'closes' | 'refs';
|
|
6
6
|
reason: string;
|
|
7
|
+
/** `owner/name` of a cross-repository issue (rendered lowercase); absent for this repository. */
|
|
8
|
+
repository?: string;
|
|
7
9
|
};
|
|
8
10
|
export type PullRequestInput = {
|
|
9
11
|
title: string;
|
|
10
12
|
summary: string;
|
|
11
13
|
impact: string;
|
|
12
14
|
relatedIssues: RelatedIssue[];
|
|
13
|
-
/**
|
|
15
|
+
/** Declares no source issue; relatedIssues beside it must all be `refs`. */
|
|
14
16
|
noSourceIssue?: {
|
|
15
17
|
reason: string;
|
|
16
18
|
};
|
|
17
19
|
details: string[];
|
|
18
20
|
base: string;
|
|
19
|
-
|
|
21
|
+
/** The caller's claim only; the rendered Agent line comes from `agentLabel`. */
|
|
22
|
+
agent?: string;
|
|
20
23
|
model?: string;
|
|
21
24
|
/** The trusted mode, see {@link trustedMode}; never the caller's word alone. */
|
|
22
25
|
mode: PullRequestMode;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasClosingIssueReference } from "../gh-cli/closing-refs.mjs";
|
|
1
2
|
import { optionalString, rejectUnknown, requireObject, requiredChoice, requiredString, requiredStringArray, } from "./args.mjs";
|
|
2
3
|
import { trustedMode } from "./pr-mode.mjs";
|
|
3
4
|
const PR_MODES = ['interactive', 'autonomous'];
|
|
@@ -9,17 +10,30 @@ export function positiveInteger(value, label) {
|
|
|
9
10
|
throw new Error(`${label} must be a positive integer`);
|
|
10
11
|
return value;
|
|
11
12
|
}
|
|
13
|
+
// The owner is a GitHub login: alphanumerics and single hyphens, at most 39 characters.
|
|
14
|
+
const REPOSITORY_RE = /^(?=[^/]{1,39}\/)[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\/[\w.-]+$/;
|
|
15
|
+
function readRepository(issue, label) {
|
|
16
|
+
if (issue.repository === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
// Not lowercased here: redaction runs on this value first, and rendering lowercases it.
|
|
19
|
+
const value = singleLine(issue, 'repository');
|
|
20
|
+
if (!REPOSITORY_RE.test(value))
|
|
21
|
+
throw new Error(`${label} must be a single owner/name`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
12
24
|
function readRelatedIssues(args) {
|
|
13
25
|
const value = args.relatedIssues;
|
|
14
26
|
if (!Array.isArray(value))
|
|
15
27
|
throw new Error('relatedIssues is required and must be an array');
|
|
16
28
|
return value.map((item, index) => {
|
|
17
29
|
const issue = requireObject(item);
|
|
18
|
-
rejectUnknown(issue, ['number', 'relation', 'reason']);
|
|
30
|
+
rejectUnknown(issue, ['number', 'relation', 'reason', 'repository']);
|
|
31
|
+
const repository = readRepository(issue, `relatedIssues[${index}].repository`);
|
|
19
32
|
return {
|
|
20
33
|
number: positiveInteger(issue.number, `relatedIssues[${index}].number`),
|
|
21
34
|
relation: requiredChoice(issue, 'relation', RELATIONS),
|
|
22
35
|
reason: requiredString(issue, 'reason'),
|
|
36
|
+
...(repository === undefined ? {} : { repository }),
|
|
23
37
|
};
|
|
24
38
|
});
|
|
25
39
|
}
|
|
@@ -28,8 +42,12 @@ function readNoSourceIssue(args, related) {
|
|
|
28
42
|
return undefined;
|
|
29
43
|
const value = requireObject(args.noSourceIssue);
|
|
30
44
|
rejectUnknown(value, ['reason']);
|
|
31
|
-
if (related.
|
|
32
|
-
throw new Error('noSourceIssue
|
|
45
|
+
if (related.some((issue) => issue.relation === 'closes'))
|
|
46
|
+
throw new Error('noSourceIssue cannot be combined with a relatedIssues entry that closes');
|
|
47
|
+
related.forEach((issue, index) => {
|
|
48
|
+
if (hasClosingIssueReference(issue.reason))
|
|
49
|
+
throw new Error(`relatedIssues[${index}].reason must not contain a closing reference beside noSourceIssue`);
|
|
50
|
+
});
|
|
33
51
|
return { reason: singleLine(value, 'reason') };
|
|
34
52
|
}
|
|
35
53
|
function readDetails(args) {
|
|
@@ -70,7 +88,7 @@ export function readPullRequestInput(args, env) {
|
|
|
70
88
|
...(noSourceIssue === undefined ? {} : { noSourceIssue }),
|
|
71
89
|
details: readDetails(args),
|
|
72
90
|
base: optionalString(args, 'base') ?? 'main',
|
|
73
|
-
agent: singleLine(args, 'agent'),
|
|
91
|
+
...(args.agent === undefined ? {} : { agent: singleLine(args, 'agent') }),
|
|
74
92
|
...(model === undefined ? {} : { model }),
|
|
75
93
|
mode: trustedMode(env, requested),
|
|
76
94
|
// It renders on its own provenance line; a newline would forge the lines after it.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { findShepherdJournal } from "./pr-journal.mjs";
|
|
2
|
-
const FIELDS = ['body', 'headRefName', 'baseRefName', 'state'];
|
|
2
|
+
const FIELDS = ['body', 'headRefName', 'headRefOid', 'baseRefName', 'state'];
|
|
3
3
|
/** The live pull request as gh reports it, whatever its state. */
|
|
4
4
|
export async function readLivePullRequest(gh, pr) {
|
|
5
5
|
const output = await gh(['pr', 'view', String(pr), '--json', FIELDS.join(',')]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { hostname, userInfo } from 'node:os';
|
|
2
2
|
import { validateGitHubBodyLength } from "../gh-cli/body-length.mjs";
|
|
3
3
|
import { assertPolicyAllowed } from "./context.mjs";
|
|
4
|
+
import { agentLabel, deriveAgent } from "./pr-agent.mjs";
|
|
4
5
|
import { renderPullRequestBody, worktreeLabel } from "./pr-body.mjs";
|
|
5
6
|
import { createGhRunner, ghEnvironment } from "./pr-gh.mjs";
|
|
6
7
|
import { checkTitleLength, readPullRequestInput } from "./pr-input.mjs";
|
|
@@ -41,6 +42,20 @@ async function liveFor(kind, context, pr) {
|
|
|
41
42
|
? livePullRequest(ghIn(context), pr)
|
|
42
43
|
: readLivePullRequest(ghIn(context), pr);
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The head to validate. An update accepts the worktree branch, or an alias branch whose HEAD is
|
|
47
|
+
* the live head commit, and then uses the live head name; anything else is rejected.
|
|
48
|
+
*/
|
|
49
|
+
async function headFor(kind, git, live, pr, worktreeHead) {
|
|
50
|
+
if (worktreeHead === undefined)
|
|
51
|
+
return live.headRefName;
|
|
52
|
+
if (live === undefined || kind !== 'update' || live.headRefName === worktreeHead)
|
|
53
|
+
return worktreeHead;
|
|
54
|
+
const sha = (await git(['rev-parse', 'HEAD'])).trim();
|
|
55
|
+
if (sha !== live.headRefOid)
|
|
56
|
+
throw new Error(`pull request ${pr} has head ${live.headRefName} at ${live.headRefOid}, not the worktree branch ${worktreeHead} at ${sha}`);
|
|
57
|
+
return live.headRefName;
|
|
58
|
+
}
|
|
44
59
|
/** Renders the body, preserves the live journal for an update, and runs the policy validator. */
|
|
45
60
|
export async function prepare(args, context, kind, pr) {
|
|
46
61
|
assertPolicyAllowed(context);
|
|
@@ -53,14 +68,15 @@ export async function prepare(args, context, kind, pr) {
|
|
|
53
68
|
const live = await liveFor(kind, context, pr);
|
|
54
69
|
// A read-only validation of an existing pull request judges the head that pull request has.
|
|
55
70
|
const worktreeHead = live !== undefined && kind === 'validate' ? undefined : await currentBranch(git);
|
|
56
|
-
|
|
71
|
+
// The local branch is checked before any diagnostic or alias resolution can echo or drop it.
|
|
72
|
+
if (worktreeHead !== undefined && secrets.some((secret) => worktreeHead.includes(secret)))
|
|
73
|
+
throw new Error('the worktree branch name contains a server credential: rename the branch');
|
|
74
|
+
const head = await headFor(kind, git, live, pr, worktreeHead);
|
|
57
75
|
// Branch names are refs passed to gh as is: redacting one would name a branch that does not exist.
|
|
58
76
|
if (secrets.some((secret) => head.includes(secret)))
|
|
59
77
|
throw new Error('the worktree branch name contains a server credential: rename the branch');
|
|
60
78
|
if (secrets.some((secret) => args.base?.includes(secret)))
|
|
61
79
|
throw new Error('base contains a server credential: use another base branch');
|
|
62
|
-
if (live !== undefined && kind === 'update' && live.headRefName !== head)
|
|
63
|
-
throw new Error(`pull request ${pr} has head ${live.headRefName}, not the worktree branch ${head}`);
|
|
64
80
|
const base = live === undefined
|
|
65
81
|
? given.base
|
|
66
82
|
: liveBase(pr, live, redactCredentials(args.base, secrets));
|
|
@@ -70,7 +86,12 @@ export async function prepare(args, context, kind, pr) {
|
|
|
70
86
|
throw new Error(`head branch ${head} must differ from base ${base}`);
|
|
71
87
|
const device = (context.device ?? (() => ({ user: userInfo().username, host: hostname() })))();
|
|
72
88
|
const statement = await noSourceStatement(policyFor(context), input);
|
|
73
|
-
let body = renderPullRequestBody(input, {
|
|
89
|
+
let body = renderPullRequestBody(input, {
|
|
90
|
+
sessionId: context.sessionId,
|
|
91
|
+
...device,
|
|
92
|
+
worktree: await label(context, git),
|
|
93
|
+
agent: agentLabel(deriveAgent(context.env, context.clientName?.()), input.agent, input.model),
|
|
94
|
+
}, statement);
|
|
74
95
|
if (live !== undefined)
|
|
75
96
|
body = preserveShepherdJournal(body, live.body);
|
|
76
97
|
body = redactCredentials(body, secrets);
|
|
@@ -15,6 +15,7 @@ const INSTRUCTIONS = 'Journals agent-blackboard feedback for a vouchington consu
|
|
|
15
15
|
*/
|
|
16
16
|
export function createMcpServer(sdk, environment, version) {
|
|
17
17
|
const mcp = new sdk.McpServer({ name: SERVER_NAME, version }, { instructions: INSTRUCTIONS });
|
|
18
|
+
environment.clientName ??= () => mcp.server.getClientVersion()?.name;
|
|
18
19
|
mcp.server.registerCapabilities({ tools: {} });
|
|
19
20
|
mcp.server.setRequestHandler(sdk.ListToolsRequestSchema, async () => ({ tools: [...TOOLS] }));
|
|
20
21
|
mcp.server.setRequestHandler(sdk.CallToolRequestSchema, async (request) => callTool(request.params.name, request.params.arguments, environment));
|
|
@@ -12,14 +12,20 @@ const FIELDS = {
|
|
|
12
12
|
impact: text('Who or what is affected, as markdown.'),
|
|
13
13
|
relatedIssues: {
|
|
14
14
|
type: 'array',
|
|
15
|
-
description: 'Issues this pull request closes or refs. Empty when it has none; with no source issue ' +
|
|
16
|
-
'
|
|
15
|
+
description: 'Issues this pull request closes or refs. Empty when it has none; with no source issue, ' +
|
|
16
|
+
'give noSourceIssue as well (beside a non-empty list every entry must be refs).',
|
|
17
17
|
items: {
|
|
18
18
|
type: 'object',
|
|
19
19
|
properties: {
|
|
20
20
|
number: { type: 'integer', minimum: 1 },
|
|
21
21
|
relation: { enum: ['closes', 'refs'] },
|
|
22
22
|
reason: text('Why the issue is related.'),
|
|
23
|
+
repository: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
pattern: '^(?=[^/]{1,39}/)[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*/[\\w.-]+$',
|
|
26
|
+
description: 'Optional owner/name (lowercased) of the issue repository, rendered as ' +
|
|
27
|
+
'owner/name#N; omit for an issue in this repository. Allowed for closes and refs.',
|
|
28
|
+
},
|
|
23
29
|
},
|
|
24
30
|
required: ['number', 'relation', 'reason'],
|
|
25
31
|
additionalProperties: false,
|
|
@@ -27,9 +33,10 @@ const FIELDS = {
|
|
|
27
33
|
},
|
|
28
34
|
noSourceIssue: {
|
|
29
35
|
type: 'object',
|
|
30
|
-
description: 'Declares a pull request with no source issue
|
|
31
|
-
'
|
|
32
|
-
'passed to the policy, which may allow it
|
|
36
|
+
description: 'Declares a pull request with no source issue; rejected beside a closes relatedIssues ' +
|
|
37
|
+
'entry, allowed beside refs-only ones. Rendered inside Related issues after any Refs ' +
|
|
38
|
+
'lines (the policy may supply the wording) and passed to the policy, which may allow it ' +
|
|
39
|
+
'only in interactive mode.',
|
|
33
40
|
properties: { reason: text('Single-line reason there is no source issue.') },
|
|
34
41
|
required: ['reason'],
|
|
35
42
|
additionalProperties: false,
|
|
@@ -41,8 +48,12 @@ const FIELDS = {
|
|
|
41
48
|
description: 'Markdown paragraphs or bullets, joined by blank lines.',
|
|
42
49
|
},
|
|
43
50
|
base: { type: 'string', minLength: 1, description: 'Base branch. Default: main.' },
|
|
44
|
-
agent: text('
|
|
45
|
-
|
|
51
|
+
agent: text('Optional, and never trusted. The server derives the Agent line from its own launch ' +
|
|
52
|
+
'environment (harness markers) and the MCP client name; a value that does not name the ' +
|
|
53
|
+
'derived agent is rendered as "caller-claimed", and with nothing derivable the agent is ' +
|
|
54
|
+
'"unknown".'),
|
|
55
|
+
model: text('Optional, and never trusted. The server cannot derive the model, so it is rendered as ' +
|
|
56
|
+
'"caller-claimed model: <model>" in parentheses after the agent.'),
|
|
46
57
|
mode: {
|
|
47
58
|
enum: ['interactive', 'autonomous'],
|
|
48
59
|
description: 'Optional. The server derives the mode from its own launch environment and fails closed: ' +
|
|
@@ -51,15 +62,7 @@ const FIELDS = {
|
|
|
51
62
|
},
|
|
52
63
|
workspaceSetup: text('Text for the Workspace setup line, such as how the worktree was prepared.'),
|
|
53
64
|
};
|
|
54
|
-
const REQUIRED = [
|
|
55
|
-
'title',
|
|
56
|
-
'summary',
|
|
57
|
-
'impact',
|
|
58
|
-
'relatedIssues',
|
|
59
|
-
'details',
|
|
60
|
-
'agent',
|
|
61
|
-
'workspaceSetup',
|
|
62
|
-
];
|
|
65
|
+
const REQUIRED = ['title', 'summary', 'impact', 'relatedIssues', 'details', 'workspaceSetup'];
|
|
63
66
|
const RENDERING = 'The server renders the body from the structured input (Summary, Impact, Related issues, ' +
|
|
64
67
|
'Details, then Workspace setup, Agent, Device and Worktree lines) and runs the checkout ' +
|
|
65
68
|
'policy validatePullRequest export in a child process; the head branch is the ' +
|
|
@@ -79,7 +82,7 @@ export const PR_TOOLS = [
|
|
|
79
82
|
"repository's stack tooling. Returns { number, url, warnings }.", openWorld(ADDITIVE), FIELDS, REQUIRED),
|
|
80
83
|
tool('pr_update', "Replaces an open pull request's title and body. " +
|
|
81
84
|
RENDERING +
|
|
82
|
-
"The pull request's head branch must be the
|
|
85
|
+
"The pull request's head must be the worktree branch, or the worktree HEAD must be the pull request's head commit (an alias branch); the live head name is then used. The live Shepherd Journal is " +
|
|
83
86
|
'preserved and nothing else is carried over from the live body. Validation errors block ' +
|
|
84
87
|
'the update. Returns { number, url, warnings }.', openWorld(MUTATING), { ...FIELDS, pr: PR_NUMBER }, [...REQUIRED, 'pr']),
|
|
85
88
|
];
|
|
@@ -131,6 +131,7 @@ function removeStaleLock(lockPath) {
|
|
|
131
131
|
return true;
|
|
132
132
|
}
|
|
133
133
|
catch {
|
|
134
|
+
/* v8 ignore next -- requires an unexpected filesystem failure during stale-lock recovery. */
|
|
134
135
|
return false;
|
|
135
136
|
}
|
|
136
137
|
});
|
|
@@ -174,7 +175,7 @@ export function withFileLock(path, action) {
|
|
|
174
175
|
throw statusError;
|
|
175
176
|
}
|
|
176
177
|
if (!lockStatus.isFile())
|
|
177
|
-
throw new Error('
|
|
178
|
+
throw new Error('log lock must be a regular file');
|
|
178
179
|
if (removeStaleLock(lockPath))
|
|
179
180
|
continue;
|
|
180
181
|
waitForLock();
|
|
@@ -189,5 +190,5 @@ export function withFileLock(path, action) {
|
|
|
189
190
|
unlinkSync(lockPath);
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
|
-
throw new Error(`could not acquire
|
|
193
|
+
throw new Error(`could not acquire file lock for ${basename(path)}`);
|
|
193
194
|
}
|