vouchington-tooling 0.11.0 → 0.11.2

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 (66) hide show
  1. package/README.md +17 -0
  2. package/dist/agent-blackboard/snapshot-cleanup-receipt.mjs +2 -0
  3. package/dist/agent-blackboard/snapshot-cleanup-resume.d.mts +1 -1
  4. package/dist/agent-blackboard/snapshot-cleanup-resume.mjs +3 -6
  5. package/dist/agent-harness-config/apply.d.mts +4 -0
  6. package/dist/agent-harness-config/apply.mjs +72 -0
  7. package/dist/agent-harness-config/cli.d.mts +10 -0
  8. package/dist/agent-harness-config/cli.mjs +100 -0
  9. package/dist/agent-harness-config/index.d.mts +4 -0
  10. package/dist/agent-harness-config/index.mjs +3 -0
  11. package/dist/agent-harness-config/merge-json.d.mts +5 -0
  12. package/dist/agent-harness-config/merge-json.mjs +62 -0
  13. package/dist/agent-harness-config/merge-toml.d.mts +6 -0
  14. package/dist/agent-harness-config/merge-toml.mjs +132 -0
  15. package/dist/agent-harness-config/paths.d.mts +3 -0
  16. package/dist/agent-harness-config/paths.mjs +117 -0
  17. package/dist/agent-harness-config/policy.d.mts +14 -0
  18. package/dist/agent-harness-config/policy.mjs +51 -0
  19. package/dist/agent-harness-config/prerequisites.d.mts +2 -0
  20. package/dist/agent-harness-config/prerequisites.mjs +67 -0
  21. package/dist/agent-harness-config/toml-value.d.mts +6 -0
  22. package/dist/agent-harness-config/toml-value.mjs +69 -0
  23. package/dist/agent-harness-config/types.d.mts +88 -0
  24. package/dist/agent-harness-config/types.mjs +1 -0
  25. package/dist/browser-session-runner/watchdog.mjs +1 -0
  26. package/dist/cli/index.mjs +9 -6
  27. package/dist/cli/parse.d.mts +3 -0
  28. package/dist/cli/parse.mjs +2 -0
  29. package/dist/cli/usage.d.mts +1 -1
  30. package/dist/cli/usage.mjs +5 -0
  31. package/dist/gha-artifacts-cleanup/classify.mjs +1 -0
  32. package/dist/gha-post-review/claude-token.mjs +2 -0
  33. package/dist/gha-post-review/github.mjs +1 -0
  34. package/dist/gha-post-review/post.mjs +7 -1
  35. package/dist/gha-pr-checkpoint/codec.mjs +1 -0
  36. package/dist/gha-review-payload/diff.mjs +1 -0
  37. package/dist/gha-review-payload/payload.mjs +2 -0
  38. package/dist/gha-workspace-policy/shared.mjs +1 -0
  39. package/dist/http-link-pagination/index.mjs +1 -0
  40. package/dist/index.d.mts +2 -0
  41. package/dist/index.mjs +1 -0
  42. package/dist/pnpm-deploy/normalize.mjs +1 -0
  43. package/dist/pnpm-install/install-operations.d.mts +2 -1
  44. package/dist/pnpm-install/install-operations.mjs +16 -4
  45. package/dist/pnpm-install/metadata.d.mts +3 -3
  46. package/dist/pnpm-install/metadata.mjs +10 -24
  47. package/dist/pnpm-install/pending-builds.d.mts +6 -14
  48. package/dist/pnpm-install/pending-builds.mjs +23 -91
  49. package/dist/pnpm-install/pnpm-install-fake-pnpm.test-helpers.mjs +15 -1
  50. package/dist/pnpm-install/runner.mjs +27 -59
  51. package/dist/pnpm-install/transition.d.mts +2 -7
  52. package/dist/pnpm-install/transition.mjs +3 -11
  53. package/dist/runner-port-policy/index.mjs +2 -0
  54. package/dist/session-friction/log.mjs +1 -0
  55. package/dist/skill-discovery/manifest.mjs +2 -0
  56. package/dist/sql-ast/constraint-do-block.mjs +1 -0
  57. package/dist/sql-ast/drop-index.mjs +2 -0
  58. package/dist/vitest-blob-manifest/reports-cli.mjs +2 -0
  59. package/docs/agent-harness-config.md +134 -0
  60. package/package.json +8 -1
  61. package/dist/cli/commands/post-review.d.mts +0 -1
  62. package/dist/cli/commands/post-review.mjs +0 -4
  63. package/dist/cli/commands/stage-review-payload.d.mts +0 -1
  64. package/dist/cli/commands/stage-review-payload.mjs +0 -4
  65. package/dist/cli/commands/swift-semantic-equal.d.mts +0 -1
  66. package/dist/cli/commands/swift-semantic-equal.mjs +0 -4
package/README.md CHANGED
@@ -18,6 +18,9 @@ vouchington runner-port-policy
18
18
  vouchington runner-port-policy --file ./policy.json
19
19
  vouchington runner-port-policy --reserved 2200
20
20
  vouchington with-host-lock --name expensive-build --timeout-seconds 60 -- make build
21
+ vouchington agent-harness-config dump
22
+ vouchington agent-harness-config check --global
23
+ vouchington agent-harness-config apply --global --repo /path/to/checkout
21
24
  vouchington gha-runtime-audit --pr-workflow CI --push-workflow '/^Main CI \\(.+\\)$/'
22
25
  vouchington require-up-to-date --remote origin --branch main
23
26
  vouchington gitleaks-directory-scan --config .gitleaks.toml
@@ -143,6 +146,11 @@ import {
143
146
  import { initSqlAst, extractCreateTableMetadata } from 'vouchington-tooling/sql-ast'
144
147
  import { splitSqlStatements, stripSqlComments } from 'vouchington-tooling/sql-scanner'
145
148
  import { auditCiJobRuntime } from 'vouchington-tooling/gha-runtime-audit'
149
+ import {
150
+ applyHarnessConfig,
151
+ checkHarnessConfig,
152
+ dumpHarnessPolicy,
153
+ } from 'vouchington-tooling/agent-harness-config'
146
154
  import {
147
155
  readVitestReportAttempts,
148
156
  writeVitestBlobManifest,
@@ -223,6 +231,15 @@ throws on a `run:` value that is a YAML alias or a multiline PLAIN scalar, shape
223
231
  scan safely. Both functions scan already-in-scope source text — deciding which files count as a
224
232
  shell script or a workflow/action YAML file is left to the caller.
225
233
 
234
+ `agent-harness-config` merges classifier-auto and sandbox keys into Claude, Codex, Grok, and Cursor
235
+ config files. See [docs/agent-harness-config.md](./docs/agent-harness-config.md). `--global` updates
236
+ home-directory configs; `--repo` updates a checkout. It does not copy allowlists, hooks, or plugins.
237
+ Grok uses `permission_mode = "auto"` plus `auto_mode.enabled` and `default_auto_mode` so the
238
+ classifier stays available in plan mode; its defined sandbox profile still requires
239
+ `--sandbox workspace-write` at launch. Cursor uses global `approvalMode = "auto-review"`;
240
+ `unrestricted` disables the sandbox. Repo checks surface user-level trust or mode prerequisites
241
+ instead of claiming that repo files alone activate them.
242
+
226
243
  `checkWorkspaceGatesPolicy` rejects tracked test assertions that hard-code the exact version of a
227
244
  dependency declared by a non-fixture package manifest. Assert dependency membership or placement,
228
245
  or derive a configuration or documentation package spec from that manifest instead.
@@ -35,7 +35,9 @@ function canonical(receipt, signature) {
35
35
  !/^[0-9a-f-]{36}$/.test(token) ||
36
36
  !Array.isArray(partitions))
37
37
  throw new Error('partition directory cleanup receipt is invalid');
38
+ // oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- retain the validated numeric type after runtime receipt checks
38
39
  const normalizedDirectoryDev = directoryDev;
40
+ // oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- retain the validated numeric type after runtime receipt checks
39
41
  const normalizedDirectoryIno = directoryIno;
40
42
  const normalized = partitions.map((partition) => {
41
43
  if (!record(partition) ||
@@ -10,7 +10,7 @@ declare const defaults: {
10
10
  unlink: typeof unlink;
11
11
  };
12
12
  export declare function setResumeFilesystemForTest(overrides?: Partial<typeof defaults>): void;
13
+ export declare function requireResumeReceipt(receipt: SnapshotCleanupReceipt): Promise<void>;
13
14
  export declare function writeResumeReceipt(receipt: SnapshotCleanupReceipt): Promise<void>;
14
15
  export declare function removeResumeReceipt(receipt: SnapshotCleanupReceipt): Promise<void>;
15
- export declare function requireResumeReceipt(receipt: SnapshotCleanupReceipt): Promise<void>;
16
16
  export {};
@@ -25,7 +25,7 @@ function assertResumeFile(info, links = 1) {
25
25
  (typeof process.geteuid === 'function' && info.uid !== process.geteuid()))
26
26
  throw new Error('partition directory cleanup resume metadata is unsafe');
27
27
  }
28
- async function read(receipt) {
28
+ export async function requireResumeReceipt(receipt) {
29
29
  const path = pathFor(receipt);
30
30
  let before = await filesystem.lstat(path);
31
31
  if (before.nlink === 2) {
@@ -80,11 +80,11 @@ export async function writeResumeReceipt(receipt) {
80
80
  await file?.close();
81
81
  await filesystem.unlink(temporary).catch(() => undefined);
82
82
  }
83
- await read(receipt);
83
+ await requireResumeReceipt(receipt);
84
84
  }
85
85
  export async function removeResumeReceipt(receipt) {
86
86
  try {
87
- await read(receipt);
87
+ await requireResumeReceipt(receipt);
88
88
  }
89
89
  catch (error) {
90
90
  if (error.code === 'ENOENT')
@@ -93,6 +93,3 @@ export async function removeResumeReceipt(receipt) {
93
93
  }
94
94
  await filesystem.rm(pathFor(receipt), { force: false });
95
95
  }
96
- export async function requireResumeReceipt(receipt) {
97
- await read(receipt);
98
- }
@@ -0,0 +1,4 @@
1
+ import type { ApplyTarget, HarnessApplyResult, HarnessCheckResult, HarnessConfigOptions, HarnessPlan } from './types.mts';
2
+ export declare function planHarnessConfig(target: ApplyTarget, options?: HarnessConfigOptions): HarnessPlan;
3
+ export declare function checkHarnessConfig(target: ApplyTarget, options?: HarnessConfigOptions): Promise<HarnessCheckResult>;
4
+ export declare function applyHarnessConfig(target: ApplyTarget, options?: HarnessConfigOptions): Promise<HarnessApplyResult>;
@@ -0,0 +1,72 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { applyJsonPatches } from './merge-json.mjs';
4
+ import { applyTomlPatches } from './merge-toml.mjs';
5
+ import { planGlobalFiles, planRepoFiles } from './paths.mjs';
6
+ import { checkHarnessPrerequisites } from './prerequisites.mjs';
7
+ export function planHarnessConfig(target, options) {
8
+ const files = target.kind === 'global' ? planGlobalFiles(options) : planRepoFiles(target.root, options);
9
+ return { files, target };
10
+ }
11
+ async function readOptional(path) {
12
+ try {
13
+ return { exists: true, source: await readFile(path, 'utf8') };
14
+ }
15
+ catch (error) {
16
+ if (error.code === 'ENOENT')
17
+ return { exists: false, source: '' };
18
+ throw error;
19
+ }
20
+ }
21
+ function patched(plan, source) {
22
+ return plan.format === 'json'
23
+ ? applyJsonPatches(source, plan.patches, plan.path)
24
+ : applyTomlPatches(source, plan.patches, plan.path);
25
+ }
26
+ function fileResult(plan, exists, drifts) {
27
+ if (!exists)
28
+ return { action: 'missing', drifts, path: plan.path };
29
+ return { action: drifts.length === 0 ? 'ok' : 'update', drifts, path: plan.path };
30
+ }
31
+ export async function checkHarnessConfig(target, options) {
32
+ const plan = planHarnessConfig(target, options);
33
+ const files = [];
34
+ for (const file of plan.files) {
35
+ const { exists, source } = await readOptional(file.path);
36
+ files.push(fileResult(file, exists, patched(file, source).drifts));
37
+ }
38
+ const prerequisites = await checkHarnessPrerequisites(target.kind === 'repo' ? target.root : undefined, options);
39
+ return {
40
+ compliant: files.every((file) => file.action === 'ok') &&
41
+ prerequisites.every((prerequisite) => prerequisite.satisfied),
42
+ files,
43
+ prerequisites,
44
+ target,
45
+ };
46
+ }
47
+ export async function applyHarnessConfig(target, options) {
48
+ const plan = planHarnessConfig(target, options);
49
+ const files = [];
50
+ const written = [];
51
+ for (const file of plan.files) {
52
+ const { exists, source } = await readOptional(file.path);
53
+ const next = patched(file, source);
54
+ const result = fileResult(file, exists, next.drifts);
55
+ if (result.action === 'ok') {
56
+ files.push(result);
57
+ continue;
58
+ }
59
+ await mkdir(dirname(file.path), { recursive: true });
60
+ await writeFile(file.path, next.text);
61
+ files.push({ ...result, action: exists ? 'update' : 'create' });
62
+ written.push(file.path);
63
+ }
64
+ const prerequisites = await checkHarnessPrerequisites(target.kind === 'repo' ? target.root : undefined, options);
65
+ return {
66
+ compliant: prerequisites.every((prerequisite) => prerequisite.satisfied),
67
+ files,
68
+ prerequisites,
69
+ target,
70
+ written,
71
+ };
72
+ }
@@ -0,0 +1,10 @@
1
+ import { type HarnessId } from './types.mts';
2
+ export interface ParsedAgentHarnessConfig {
3
+ readonly action: 'dump' | 'check' | 'apply';
4
+ readonly harnesses?: readonly HarnessId[];
5
+ readonly home?: string;
6
+ readonly repos: readonly string[];
7
+ readonly global: boolean;
8
+ }
9
+ export declare function parseAgentHarnessConfigArguments(argv: readonly string[]): ParsedAgentHarnessConfig;
10
+ export declare function runAgentHarnessConfigCli(argv: readonly string[]): Promise<number>;
@@ -0,0 +1,100 @@
1
+ import { dumpHarnessPolicy } from './policy.mjs';
2
+ import { applyHarnessConfig, checkHarnessConfig } from './apply.mjs';
3
+ import { HARNESS_IDS } from './types.mjs';
4
+ const AGENT_HARNESS_CONFIG_USAGE = 'agent-harness-config dump|check|apply [--global] [--repo PATH]... [--harness NAME]... [--home PATH]';
5
+ function isHarness(value) {
6
+ return HARNESS_IDS.includes(value);
7
+ }
8
+ export function parseAgentHarnessConfigArguments(argv) {
9
+ const action = argv[0];
10
+ if (action !== 'dump' && action !== 'check' && action !== 'apply') {
11
+ throw new Error(`unknown agent-harness-config action: ${action ?? ''}\n${AGENT_HARNESS_CONFIG_USAGE}`);
12
+ }
13
+ let global = false;
14
+ let home;
15
+ const repos = [];
16
+ const harnesses = [];
17
+ for (let index = 1; index < argv.length; index += 1) {
18
+ const flag = argv[index];
19
+ if (flag === '--global') {
20
+ global = true;
21
+ continue;
22
+ }
23
+ if (flag === '--repo' || flag === '--harness' || flag === '--home') {
24
+ const value = argv[index + 1];
25
+ if (value === undefined)
26
+ throw new Error(`${flag} requires a value\n${AGENT_HARNESS_CONFIG_USAGE}`);
27
+ index += 1;
28
+ if (flag === '--repo')
29
+ repos.push(value);
30
+ else if (flag === '--home')
31
+ home = value;
32
+ else if (!isHarness(value))
33
+ throw new Error(`unknown harness: ${value}`);
34
+ else
35
+ harnesses.push(value);
36
+ continue;
37
+ }
38
+ throw new Error(`unknown agent-harness-config option: ${flag}\n${AGENT_HARNESS_CONFIG_USAGE}`);
39
+ }
40
+ if (action !== 'dump' && !global && repos.length === 0) {
41
+ throw new Error(`check and apply require --global and/or --repo\n${AGENT_HARNESS_CONFIG_USAGE}`);
42
+ }
43
+ return {
44
+ action,
45
+ global,
46
+ repos,
47
+ ...(harnesses.length > 0 ? { harnesses } : {}),
48
+ ...(home === undefined ? {} : { home }),
49
+ };
50
+ }
51
+ function formatDrift(drift) {
52
+ return ` ${drift.path} ${drift.key}: ${JSON.stringify(drift.current)} -> ${JSON.stringify(drift.desired)}`;
53
+ }
54
+ function targets(parsed) {
55
+ const list = [];
56
+ if (parsed.global)
57
+ list.push({ kind: 'global' });
58
+ for (const root of parsed.repos)
59
+ list.push({ kind: 'repo', root });
60
+ return list;
61
+ }
62
+ export async function runAgentHarnessConfigCli(argv) {
63
+ try {
64
+ const parsed = parseAgentHarnessConfigArguments(argv);
65
+ if (parsed.action === 'dump') {
66
+ process.stdout.write(`${JSON.stringify(dumpHarnessPolicy(), null, 2)}\n`);
67
+ return 0;
68
+ }
69
+ const options = {
70
+ ...(parsed.harnesses === undefined ? {} : { harnesses: parsed.harnesses }),
71
+ ...(parsed.home === undefined ? {} : { home: parsed.home }),
72
+ };
73
+ let compliant = true;
74
+ for (const target of targets(parsed)) {
75
+ const result = parsed.action === 'apply'
76
+ ? await applyHarnessConfig(target, options)
77
+ : await checkHarnessConfig(target, options);
78
+ compliant &&=
79
+ parsed.action === 'apply'
80
+ ? result.prerequisites.every((prerequisite) => prerequisite.satisfied)
81
+ : result.compliant;
82
+ for (const file of result.files) {
83
+ if (file.action === 'ok')
84
+ process.stdout.write(`ok ${file.path}\n`);
85
+ else
86
+ process.stdout.write(`${file.action} ${file.path}\n`);
87
+ for (const drift of file.drifts)
88
+ process.stdout.write(`${formatDrift(drift)}\n`);
89
+ }
90
+ for (const prerequisite of result.prerequisites) {
91
+ process.stdout.write(`prerequisite ${prerequisite.harness} ${prerequisite.key} satisfied=${prerequisite.satisfied}: ${prerequisite.message}\n`);
92
+ }
93
+ }
94
+ return compliant ? 0 : 1;
95
+ }
96
+ catch (error) {
97
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
98
+ return 2;
99
+ }
100
+ }
@@ -0,0 +1,4 @@
1
+ export { applyHarnessConfig, checkHarnessConfig, planHarnessConfig } from './apply.mts';
2
+ export { dumpHarnessPolicy, DEFAULT_EXTRA_WRITABLE_ROOTS } from './policy.mts';
3
+ export { HARNESS_IDS } from './types.mts';
4
+ export type { ApplyTarget, FilePlan, FileResult, HarnessApplyResult, HarnessCheckResult, HarnessConfigOptions, HarnessId, HarnessPlan, HarnessPolicyDump, JsonPatch, KeyDrift, TomlPatch, TomlValue, } from './types.mts';
@@ -0,0 +1,3 @@
1
+ export { applyHarnessConfig, checkHarnessConfig, planHarnessConfig } from './apply.mjs';
2
+ export { dumpHarnessPolicy, DEFAULT_EXTRA_WRITABLE_ROOTS } from './policy.mjs';
3
+ export { HARNESS_IDS } from './types.mjs';
@@ -0,0 +1,5 @@
1
+ import type { JsonPatch, KeyDrift } from './types.mts';
2
+ export declare function applyJsonPatches(source: string, patches: readonly JsonPatch[], path: string): {
3
+ drifts: KeyDrift[];
4
+ text: string;
5
+ };
@@ -0,0 +1,62 @@
1
+ import { sameValue } from './policy.mjs';
2
+ function getJsonPath(value, path) {
3
+ let current = value;
4
+ for (const key of path) {
5
+ if (current === null || typeof current !== 'object' || Array.isArray(current))
6
+ return undefined;
7
+ current = current[key];
8
+ }
9
+ return current;
10
+ }
11
+ function setJsonPath(target, path, value) {
12
+ let current = target;
13
+ for (const key of path.slice(0, -1)) {
14
+ const next = current[key];
15
+ if (next === null || typeof next !== 'object' || Array.isArray(next)) {
16
+ current[key] = {};
17
+ }
18
+ current = current[key];
19
+ }
20
+ const last = path.at(-1);
21
+ if (last !== undefined)
22
+ current[last] = value;
23
+ }
24
+ function parseJsonObject(source, path) {
25
+ if (source.trim() === '')
26
+ return {};
27
+ const parsed = JSON.parse(source);
28
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
29
+ throw new Error(`${path}: expected a JSON object`);
30
+ }
31
+ return parsed;
32
+ }
33
+ function unionMerge(current, values) {
34
+ const base = Array.isArray(current) ? current : [];
35
+ const merged = [...base];
36
+ for (const value of values) {
37
+ if (!merged.some((entry) => sameValue(entry, value)))
38
+ merged.push(value);
39
+ }
40
+ return merged;
41
+ }
42
+ function desiredValue(document, patch) {
43
+ if (patch.merge !== 'union')
44
+ return patch.value;
45
+ return unionMerge(getJsonPath(document, patch.path), patch.value);
46
+ }
47
+ export function applyJsonPatches(source, patches, path) {
48
+ const document = parseJsonObject(source, path);
49
+ const resolved = patches.map((patch) => ({ desired: desiredValue(document, patch), patch }));
50
+ const drifts = [];
51
+ for (const { desired, patch } of resolved) {
52
+ const current = getJsonPath(document, patch.path);
53
+ if (!sameValue(current, desired)) {
54
+ drifts.push({ current, desired, key: patch.path.join('.'), path });
55
+ }
56
+ }
57
+ if (drifts.length === 0)
58
+ return { drifts, text: source === '' ? '{}\n' : source };
59
+ for (const { desired, patch } of resolved)
60
+ setJsonPath(document, patch.path, desired);
61
+ return { drifts, text: `${JSON.stringify(document, null, 2)}\n` };
62
+ }
@@ -0,0 +1,6 @@
1
+ import type { KeyDrift, TomlPatch } from './types.mts';
2
+ export declare function readTomlKey(text: string, table: string, key: string): unknown;
3
+ export declare function applyTomlPatches(source: string, patches: readonly TomlPatch[], path: string): {
4
+ drifts: KeyDrift[];
5
+ text: string;
6
+ };
@@ -0,0 +1,132 @@
1
+ import { parse as parseToml } from 'smol-toml';
2
+ import { sameValue } from './policy.mjs';
3
+ import { formatTomlValue, parseTomlValue } from './toml-value.mjs';
4
+ function lineStarts(text) {
5
+ const starts = [0];
6
+ for (let index = 0; index < text.length; index += 1) {
7
+ if (text[index] === '\n')
8
+ starts.push(index + 1);
9
+ }
10
+ return starts;
11
+ }
12
+ function lineAt(text, start) {
13
+ const end = text.indexOf('\n', start);
14
+ return text.slice(start, end === -1 ? text.length : end);
15
+ }
16
+ function isHeaderLine(line) {
17
+ return line.trimStart().startsWith('[');
18
+ }
19
+ function headerName(line) {
20
+ const trimmed = line.trim();
21
+ if (trimmed.startsWith('[[') || !trimmed.startsWith('['))
22
+ return undefined;
23
+ const close = trimmed.indexOf(']');
24
+ if (close <= 1)
25
+ return undefined;
26
+ return trimmed.slice(1, close);
27
+ }
28
+ function tableRange(text, table) {
29
+ const starts = lineStarts(text);
30
+ if (table === '') {
31
+ const first = starts.find((start) => isHeaderLine(lineAt(text, start)));
32
+ return { bodyEnd: first ?? text.length, bodyStart: 0 };
33
+ }
34
+ for (const start of starts) {
35
+ const line = lineAt(text, start);
36
+ if (headerName(line) !== table)
37
+ continue;
38
+ const bodyStart = start + line.length + (text[start + line.length] === '\n' ? 1 : 0);
39
+ const next = starts.find((candidate) => candidate >= bodyStart && isHeaderLine(lineAt(text, candidate)));
40
+ return { bodyEnd: next ?? text.length, bodyStart };
41
+ }
42
+ return { bodyEnd: -1, bodyStart: -1 };
43
+ }
44
+ function keyMatch(text, bodyStart, bodyEnd, key) {
45
+ const pattern = new RegExp(`^[ \\t]*${key.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&')}[ \\t]*=[ \\t]*`, 'm');
46
+ const slice = text.slice(bodyStart, bodyEnd);
47
+ const match = pattern.exec(slice);
48
+ if (!match || match.index === undefined)
49
+ return undefined;
50
+ const equals = bodyStart + match.index + match[0].length;
51
+ return { equals, parsed: parseTomlValue(text, equals) };
52
+ }
53
+ export function readTomlKey(text, table, key) {
54
+ const range = tableRange(text, table);
55
+ if (range.bodyStart < 0)
56
+ return undefined;
57
+ return keyMatch(text, range.bodyStart, range.bodyEnd, key)?.parsed.value;
58
+ }
59
+ function appendTable(text, table, assignment) {
60
+ const prefix = text === '' || text.endsWith('\n') ? text : `${text}\n`;
61
+ const blank = prefix !== '' && !prefix.endsWith('\n\n') ? '\n' : '';
62
+ return `${prefix}${blank}[${table}]\n${assignment}\n`;
63
+ }
64
+ function upsertTomlKey(text, table, key, value) {
65
+ const assignment = `${key} = ${formatTomlValue(value)}`;
66
+ const range = tableRange(text, table);
67
+ if (range.bodyStart < 0)
68
+ return appendTable(text, table, assignment);
69
+ const existing = keyMatch(text, range.bodyStart, range.bodyEnd, key);
70
+ if (existing) {
71
+ return `${text.slice(0, existing.equals)}${formatTomlValue(value)}${text.slice(existing.parsed.end)}`;
72
+ }
73
+ const at = range.bodyEnd;
74
+ const prefix = at > 0 && text[at - 1] !== '\n' ? '\n' : '';
75
+ return `${text.slice(0, at)}${prefix}${assignment}\n${text.slice(at)}`;
76
+ }
77
+ function tomlPatchValue(parsed, table, key) {
78
+ let current = parsed;
79
+ if (table !== '') {
80
+ for (const segment of table.split('.')) {
81
+ /* v8 ignore next 2 -- if `text` parsed, every ancestor of a dotted table header is
82
+ guaranteed to be a table too, so a real TOML parser can never hand back a non-object
83
+ mid-path here. */
84
+ if (current === null || typeof current !== 'object' || Array.isArray(current))
85
+ return undefined;
86
+ current = current[segment];
87
+ }
88
+ }
89
+ /* v8 ignore next 2 -- assertValidToml only reaches a patch's table here once readTomlKey has
90
+ already proven (as a match, or by forcing the write that creates it) that the table is a
91
+ real nested object in this parsed document. */
92
+ if (current === null || typeof current !== 'object' || Array.isArray(current))
93
+ return undefined;
94
+ return current[key];
95
+ }
96
+ function assertValidToml(text, patches, path) {
97
+ let parsed;
98
+ try {
99
+ parsed = parseToml(text);
100
+ }
101
+ catch (error) {
102
+ /* v8 ignore next -- smol-toml's parse() only ever throws TomlError, which extends Error, so
103
+ the non-Error fallback here is unreachable in practice. */
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ throw new Error(`${path}: produced invalid TOML (${message})`);
106
+ }
107
+ for (const patch of patches) {
108
+ const value = tomlPatchValue(parsed, patch.table, patch.key);
109
+ /* v8 ignore next 6 -- backstop for a formatTomlValue/parseToml encoding mismatch; formatTomlValue's
110
+ JSON.stringify-based escaping is a valid TOML basic string in every case this module writes,
111
+ so a real parser always reads back what we just wrote. */
112
+ if (!sameValue(value, patch.value)) {
113
+ const key = patch.table === '' ? patch.key : `${patch.table}.${patch.key}`;
114
+ throw new Error(`${path}: wrote ${key} but a real TOML parser reads back ${JSON.stringify(value)}`);
115
+ }
116
+ }
117
+ }
118
+ export function applyTomlPatches(source, patches, path) {
119
+ const drifts = [];
120
+ let text = source.replaceAll('\r\n', '\n');
121
+ for (const patch of patches) {
122
+ const current = readTomlKey(text, patch.table, patch.key);
123
+ if (sameValue(current, patch.value))
124
+ continue;
125
+ const key = patch.table === '' ? patch.key : `${patch.table}.${patch.key}`;
126
+ drifts.push({ current, desired: patch.value, key, path });
127
+ text = upsertTomlKey(text, patch.table, patch.key, patch.value);
128
+ }
129
+ if (drifts.length > 0)
130
+ assertValidToml(text, patches, path);
131
+ return { drifts, text };
132
+ }
@@ -0,0 +1,3 @@
1
+ import { type FilePlan, type HarnessConfigOptions } from './types.mts';
2
+ export declare function planGlobalFiles(options?: HarnessConfigOptions): FilePlan[];
3
+ export declare function planRepoFiles(root: string, options?: HarnessConfigOptions): FilePlan[];
@@ -0,0 +1,117 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, resolve } from 'node:path';
3
+ import { CLAUDE_DEFAULT_MODE, CODEX_APPROVAL_POLICY, CODEX_APPROVALS_REVIEWER, CODEX_SANDBOX_MODE, CURSOR_APPROVAL_MODE, CURSOR_SANDBOX_MODE, DEFAULT_EXTRA_WRITABLE_ROOTS, GROK_AUTO_MODE_ENABLED, GROK_DEFAULT_AUTO_MODE, GROK_PERMISSION_MODE, } from './policy.mjs';
4
+ import { HARNESS_IDS } from './types.mjs';
5
+ function selected(options) {
6
+ return new Set(options?.harnesses ?? HARNESS_IDS);
7
+ }
8
+ function extraRoots(options) {
9
+ return options?.extraWritableRoots ?? DEFAULT_EXTRA_WRITABLE_ROOTS;
10
+ }
11
+ function wants(set, id) {
12
+ return set.has(id);
13
+ }
14
+ function codexPatches(roots) {
15
+ return {
16
+ format: 'toml',
17
+ patches: [
18
+ { key: 'approval_policy', table: '', value: CODEX_APPROVAL_POLICY },
19
+ { key: 'approvals_reviewer', table: '', value: CODEX_APPROVALS_REVIEWER },
20
+ { key: 'sandbox_mode', table: '', value: CODEX_SANDBOX_MODE },
21
+ { key: 'network_access', table: 'sandbox_workspace_write', value: true },
22
+ { key: 'writable_roots', table: 'sandbox_workspace_write', value: roots },
23
+ ],
24
+ path: '',
25
+ };
26
+ }
27
+ function grokSandboxPatches(roots) {
28
+ return {
29
+ format: 'toml',
30
+ patches: [
31
+ { key: 'extends', table: 'profiles.workspace-write', value: 'workspace' },
32
+ { key: 'read_write', table: 'profiles.workspace-write', value: roots },
33
+ ],
34
+ path: '',
35
+ };
36
+ }
37
+ function homeDir(options) {
38
+ return resolve(options?.home ?? homedir());
39
+ }
40
+ export function planGlobalFiles(options) {
41
+ const harnesses = selected(options);
42
+ const roots = extraRoots(options);
43
+ const home = homeDir(options);
44
+ const files = [];
45
+ if (wants(harnesses, 'claude')) {
46
+ files.push({
47
+ format: 'json',
48
+ patches: [
49
+ { path: ['permissions', 'defaultMode'], value: CLAUDE_DEFAULT_MODE },
50
+ { path: ['sandbox', 'enabled'], value: true },
51
+ { path: ['sandbox', 'failIfUnavailable'], value: true },
52
+ { merge: 'union', path: ['sandbox', 'filesystem', 'allowWrite'], value: roots },
53
+ { path: ['useAutoModeDuringPlan'], value: true },
54
+ ],
55
+ path: join(home, '.claude', 'settings.json'),
56
+ });
57
+ }
58
+ if (wants(harnesses, 'codex'))
59
+ files.push({ ...codexPatches(roots), path: join(home, '.codex', 'config.toml') });
60
+ if (wants(harnesses, 'grok')) {
61
+ files.push({ ...grokSandboxPatches(roots), path: join(home, '.grok', 'sandbox.toml') });
62
+ files.push({
63
+ format: 'toml',
64
+ patches: [
65
+ { key: 'permission_mode', table: 'ui', value: GROK_PERMISSION_MODE },
66
+ { key: 'enabled', table: 'auto_mode', value: GROK_AUTO_MODE_ENABLED },
67
+ { key: 'default_auto_mode', table: '', value: GROK_DEFAULT_AUTO_MODE },
68
+ ],
69
+ path: join(home, '.grok', 'config.toml'),
70
+ });
71
+ }
72
+ if (wants(harnesses, 'cursor')) {
73
+ files.push({
74
+ format: 'json',
75
+ patches: [
76
+ { path: ['approvalMode'], value: CURSOR_APPROVAL_MODE },
77
+ { path: ['sandbox', 'mode'], value: CURSOR_SANDBOX_MODE },
78
+ ],
79
+ path: join(home, '.cursor', 'cli-config.json'),
80
+ });
81
+ }
82
+ return files;
83
+ }
84
+ export function planRepoFiles(root, options) {
85
+ const harnesses = selected(options);
86
+ const roots = extraRoots(options);
87
+ const repo = resolve(root);
88
+ const files = [];
89
+ if (wants(harnesses, 'claude')) {
90
+ files.push({
91
+ format: 'json',
92
+ patches: [
93
+ { path: ['sandbox', 'enabled'], value: true },
94
+ { path: ['sandbox', 'failIfUnavailable'], value: true },
95
+ { merge: 'union', path: ['sandbox', 'filesystem', 'allowWrite'], value: roots },
96
+ ],
97
+ path: join(repo, '.claude', 'settings.json'),
98
+ });
99
+ }
100
+ if (wants(harnesses, 'codex'))
101
+ files.push({ ...codexPatches(roots), path: join(repo, '.codex', 'config.toml') });
102
+ if (wants(harnesses, 'grok'))
103
+ files.push({ ...grokSandboxPatches(roots), path: join(repo, '.grok', 'sandbox.toml') });
104
+ if (wants(harnesses, 'cursor')) {
105
+ files.push({
106
+ format: 'json',
107
+ patches: [
108
+ { path: ['type'], value: 'workspace_readwrite' },
109
+ { path: ['enableSharedBuildCache'], value: true },
110
+ { path: ['networkPolicy', 'default'], value: 'allow' },
111
+ { path: ['additionalReadwritePaths'], value: roots },
112
+ ],
113
+ path: join(repo, '.cursor', 'sandbox.json'),
114
+ });
115
+ }
116
+ return files;
117
+ }
@@ -0,0 +1,14 @@
1
+ import type { HarnessPolicyDump } from './types.mts';
2
+ export declare const DEFAULT_EXTRA_WRITABLE_ROOTS: readonly ['~/.cargo', '~/Library/pnpm', '~/.local/share/pnpm', '~/.pnpm-store', '~/Library/Caches/no-mistakes', '~/Library/Caches/pnpm', '~/.pnpm-state', '~/.cache/no-mistakes', '~/.cache/pnpm', '~/.local/state/pnpm', '/var/folders', '/private/var/folders'];
3
+ export declare const CLAUDE_DEFAULT_MODE = "auto";
4
+ export declare const CODEX_APPROVAL_POLICY = "on-request";
5
+ export declare const CODEX_APPROVALS_REVIEWER = "auto_review";
6
+ export declare const CODEX_SANDBOX_MODE = "workspace-write";
7
+ export declare const GROK_PERMISSION_MODE = "auto";
8
+ export declare const GROK_SANDBOX_PROFILE = "workspace-write";
9
+ export declare const GROK_AUTO_MODE_ENABLED = true;
10
+ export declare const GROK_DEFAULT_AUTO_MODE = true;
11
+ export declare const CURSOR_APPROVAL_MODE = "auto-review";
12
+ export declare const CURSOR_SANDBOX_MODE = "enabled";
13
+ export declare function dumpHarnessPolicy(extraWritableRoots?: readonly string[]): HarnessPolicyDump;
14
+ export declare function sameValue(left: unknown, right: unknown): boolean;