harnery 0.45.2 → 0.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.d.ts +13 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commands/admission.d.ts.map +1 -1
- package/dist/commands/admission.js +214 -27
- package/dist/commands/agents.d.ts +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +102 -25
- package/dist/commands/browse.d.ts +3 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +172 -8
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +8 -2
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +40 -0
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +4 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +33 -6
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -4
- package/dist/core/agents/render/session-context.d.ts +0 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +1 -6
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +36 -16
- package/dist/core/agents/rules/stop-hook.d.ts +6 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +20 -1
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -4
- package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +6 -2
- package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
- package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference-contract.js +1 -0
- package/dist/core/agents/state/parent-reference.d.ts +18 -0
- package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference.js +119 -0
- package/dist/core/config.d.ts +39 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +63 -0
- package/dist/core/diagnostics/path-identity.d.ts +1 -1
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
- package/dist/core/diagnostics/path-identity.js +51 -6
- package/dist/core/events/v3/canonical.js +43 -14
- package/dist/core/events/v3/index.d.ts +1 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -1
- package/dist/core/events/v3/producers/recorder.d.ts +34 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +117 -13
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +53 -13
- package/dist/core/events/v3/validate.d.ts.map +1 -1
- package/dist/core/events/v3/validate.js +36 -1
- package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
- package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
- package/dist/core/hooks/adapter/native-parent.js +91 -0
- package/dist/core/hooks/cli.js +103 -5
- package/dist/core/hooks/resolve/owner.d.ts +0 -11
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +0 -23
- package/dist/core/hooks/resolve/transcript.d.ts +12 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +54 -0
- package/dist/core/hooks/shell-admission.d.ts +84 -0
- package/dist/core/hooks/shell-admission.d.ts.map +1 -0
- package/dist/core/hooks/shell-admission.js +622 -0
- package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
- package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
- package/dist/core/hooks/shell-commit-tree.js +104 -0
- package/dist/core/inbox/contract.d.ts +12 -0
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/contract.js +16 -0
- package/dist/core/inbox/peer-messages.d.ts +41 -16
- package/dist/core/inbox/peer-messages.d.ts.map +1 -1
- package/dist/core/inbox/peer-messages.js +372 -15
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +83 -3
- package/dist/core/inbox/targets.d.ts +17 -3
- package/dist/core/inbox/targets.d.ts.map +1 -1
- package/dist/core/inbox/targets.js +77 -2
- package/dist/core/presence/git.d.ts +18 -12
- package/dist/core/presence/git.d.ts.map +1 -1
- package/dist/core/presence/git.js +32 -38
- package/dist/core/presence/index.d.ts +2 -2
- package/dist/core/presence/index.d.ts.map +1 -1
- package/dist/core/presence/index.js +8 -6
- package/dist/core/presence/mail.d.ts +133 -0
- package/dist/core/presence/mail.d.ts.map +1 -0
- package/dist/core/presence/mail.js +464 -0
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/storage/modes.d.ts +4 -0
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +4 -0
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +5 -4
- package/dist/lib/admission-history.d.ts +96 -0
- package/dist/lib/admission-history.d.ts.map +1 -0
- package/dist/lib/admission-history.js +196 -0
- package/dist/lib/admission.d.ts +56 -7
- package/dist/lib/admission.d.ts.map +1 -1
- package/dist/lib/admission.js +161 -19
- package/dist/lib/browser/client.d.ts +18 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +100 -12
- package/dist/lib/browser/content-checks.d.ts +3 -1
- package/dist/lib/browser/content-checks.d.ts.map +1 -1
- package/dist/lib/browser/content-checks.js +10 -2
- package/dist/lib/browser/critique.d.ts +32 -0
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +72 -3
- package/dist/lib/browser/geometry.d.ts +2 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +61 -15
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -1
- package/dist/lib/browser/page-review-contracts.d.ts +6 -1
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
- package/dist/lib/browser/page-review-contracts.js +6 -1
- package/dist/lib/browser/page-review-judge.d.ts +3 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +41 -11
- package/dist/lib/browser/page-review-pack.d.ts +3 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +5 -2
- package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +11 -4
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +73 -16
- package/dist/lib/browser/runts.d.ts +4 -2
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +84 -33
- package/dist/lib/browser/windows-chrome.d.ts +14 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +42 -0
- package/dist/lib/browser/windows-login-sync.d.ts +110 -0
- package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
- package/dist/lib/browser/windows-login-sync.js +171 -0
- package/dist/lib/cookies/client.d.ts +30 -5
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +55 -10
- package/dist/lib/cookies/index.d.ts +1 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +1 -1
- package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
- package/dist/lib/docs-metadata-sync.js +41 -2
- package/dist/lib/durable-job.d.ts +8 -0
- package/dist/lib/durable-job.d.ts.map +1 -1
- package/dist/lib/durable-job.js +10 -0
- package/dist/lib/http/client.d.ts +9 -8
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +44 -2
- package/dist/lib/instructions/templates.js +4 -4
- package/package.json +2 -2
- package/src/commander.ts +13 -0
- package/src/commands/admission.ts +272 -29
- package/src/commands/agents.ts +132 -27
- package/src/commands/browse.ts +208 -6
- package/src/commands/fetch.ts +1 -0
- package/src/commands/ledger-v3.ts +8 -2
- package/src/commands/presence.ts +49 -0
- package/src/commands/qa-run.ts +4 -3
- package/src/commands/review-pack.ts +41 -5
- package/src/core/agents/index.ts +6 -0
- package/src/core/agents/live-authority-v3.ts +11 -4
- package/src/core/agents/render/session-context.ts +1 -7
- package/src/core/agents/rules/claim-conflict.ts +50 -18
- package/src/core/agents/rules/stop-hook.ts +28 -1
- package/src/core/agents/session-events.ts +18 -3
- package/src/core/agents/state/heartbeat-reader.ts +3 -0
- package/src/core/agents/state/live-coordination-view.ts +13 -6
- package/src/core/agents/state/parent-reference-contract.ts +14 -0
- package/src/core/agents/state/parent-reference.ts +179 -0
- package/src/core/config.ts +85 -0
- package/src/core/diagnostics/path-identity.ts +52 -6
- package/src/core/events/v3/canonical.ts +46 -13
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/producers/recorder.ts +151 -13
- package/src/core/events/v3/reader.ts +55 -11
- package/src/core/events/v3/validate.ts +42 -1
- package/src/core/hooks/adapter/native-parent.ts +109 -0
- package/src/core/hooks/cli.ts +110 -4
- package/src/core/hooks/resolve/owner.ts +0 -23
- package/src/core/hooks/resolve/transcript.ts +61 -0
- package/src/core/hooks/shell-admission.ts +634 -0
- package/src/core/hooks/shell-commit-tree.ts +102 -0
- package/src/core/inbox/contract.ts +18 -0
- package/src/core/inbox/peer-messages.ts +501 -15
- package/src/core/inbox/service.ts +108 -3
- package/src/core/inbox/targets.ts +94 -3
- package/src/core/presence/git.ts +45 -37
- package/src/core/presence/index.ts +16 -8
- package/src/core/presence/mail.ts +620 -0
- package/src/core/storage/modes.ts +6 -0
- package/src/core/storage/segments.ts +5 -4
- package/src/lib/admission-history.ts +267 -0
- package/src/lib/admission.ts +217 -19
- package/src/lib/browser/client.ts +147 -35
- package/src/lib/browser/content-checks.ts +14 -3
- package/src/lib/browser/critique.ts +113 -3
- package/src/lib/browser/geometry.ts +63 -14
- package/src/lib/browser/index.ts +5 -0
- package/src/lib/browser/page-review-contracts.ts +6 -1
- package/src/lib/browser/page-review-judge.ts +58 -13
- package/src/lib/browser/page-review-pack.ts +13 -2
- package/src/lib/browser/qa-run-contracts.ts +30 -4
- package/src/lib/browser/qa-run.ts +78 -17
- package/src/lib/browser/runts.ts +85 -37
- package/src/lib/browser/windows-chrome.ts +50 -0
- package/src/lib/browser/windows-login-sync.ts +275 -0
- package/src/lib/cookies/client.ts +62 -10
- package/src/lib/cookies/index.ts +3 -0
- package/src/lib/docs-metadata-sync.ts +44 -2
- package/src/lib/durable-job.ts +15 -0
- package/src/lib/http/client.ts +63 -1
- package/src/lib/instructions/templates.ts +4 -4
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admission guard for heavy shell checks. A whole-project typecheck, a full
|
|
3
|
+
* test suite, or a production build can each take gigabytes of memory, and
|
|
4
|
+
* several agents starting them at once on one machine can exhaust it. Asking
|
|
5
|
+
* agents in prose to wrap those commands in `admission run` does not work: the
|
|
6
|
+
* rule has to be read and remembered on every call. This guard rewrites the
|
|
7
|
+
* agent's shell command at PreToolUse instead, so each heavy check waits for a
|
|
8
|
+
* slot on a machine-wide admission resource.
|
|
9
|
+
*
|
|
10
|
+
* Enabled only when the project config names a resource
|
|
11
|
+
* (`admission.guard.resource`). It finds each simple command that is a heavy
|
|
12
|
+
* check and inserts `<bin> admission run --resource <name> --label 'agent:
|
|
13
|
+
* ...' --` in front of it, after any leading VAR=value assignments. The rest
|
|
14
|
+
* of the command (cd, pipes, redirects, && chains) is left byte for byte.
|
|
15
|
+
* Heavy means:
|
|
16
|
+
* - tsc, however launched (tsc, npx/bunx/bun x/pnpm exec/yarn tsc,
|
|
17
|
+
* node_modules/.bin/tsc, node .../typescript/bin/tsc)
|
|
18
|
+
* - next build
|
|
19
|
+
* - bun test with no file or directory named (a full suite)
|
|
20
|
+
* - the package scripts test / test:* (with no further positional
|
|
21
|
+
* argument), build, typecheck, type-check, typecheck:*, check-types, tsc,
|
|
22
|
+
* run through bun run, npm, pnpm, or yarn
|
|
23
|
+
* - bash/sh -c '<script>' whose script contains one of the above
|
|
24
|
+
* Watch modes, --help/--version, and named-file test runs are left alone.
|
|
25
|
+
*
|
|
26
|
+
* Passes through untouched: non-shell tools; a command that already runs
|
|
27
|
+
* `admission run` or `heavy-slot`; HARNERY_ADMISSION_SKIP=1 in the command or
|
|
28
|
+
* the hook's environment; HARNERY_ADMISSION_HELD in the hook's environment
|
|
29
|
+
* (the session already holds a slot); text inside quotes, heredocs, and
|
|
30
|
+
* comments. Any parse failure means no rewrite.
|
|
31
|
+
*/
|
|
32
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*(\[[^\]]*\])?\+?=/;
|
|
33
|
+
const RESERVED = new Set([
|
|
34
|
+
"if",
|
|
35
|
+
"then",
|
|
36
|
+
"else",
|
|
37
|
+
"elif",
|
|
38
|
+
"do",
|
|
39
|
+
"while",
|
|
40
|
+
"until",
|
|
41
|
+
"!",
|
|
42
|
+
"{",
|
|
43
|
+
"}",
|
|
44
|
+
"fi",
|
|
45
|
+
"done",
|
|
46
|
+
"esac",
|
|
47
|
+
]);
|
|
48
|
+
const SEPARATORS = new Set([";", "&&", "||", "|", "|&", "&", "(", ")", ";;", ";&", ";;&"]);
|
|
49
|
+
const REDIRECTS = ["<<<", "<<-", "&>>", "<<", ">>", ">&", "<&", "&>", "<>", ">|", ">", "<"];
|
|
50
|
+
const OPERATORS = [...REDIRECTS, ";;&", ";;", ";&", "&&", "||", "|&"];
|
|
51
|
+
const HEAVY_SCRIPTS = /^(build|typecheck|type-check|typecheck:.*|check-types|tsc)$/;
|
|
52
|
+
const TEST_SCRIPTS = /^(test|test:.*)$/;
|
|
53
|
+
const SHELLS = new Set(["bash", "sh", "dash", "zsh"]);
|
|
54
|
+
const SHELL_C_FLAG = /^-[A-Za-z]*c[A-Za-z]*$/;
|
|
55
|
+
const WATCH_FLAGS = new Set(["-w", "--watch", "--watchAll"]);
|
|
56
|
+
const TSC_INFO_FLAGS = new Set([
|
|
57
|
+
"-v",
|
|
58
|
+
"--version",
|
|
59
|
+
"-h",
|
|
60
|
+
"--help",
|
|
61
|
+
"--init",
|
|
62
|
+
"--showConfig",
|
|
63
|
+
"--all",
|
|
64
|
+
"--listFilesOnly",
|
|
65
|
+
]);
|
|
66
|
+
const BUN_BUILTINS = new Set([
|
|
67
|
+
"test",
|
|
68
|
+
"run",
|
|
69
|
+
"x",
|
|
70
|
+
"build",
|
|
71
|
+
"install",
|
|
72
|
+
"i",
|
|
73
|
+
"add",
|
|
74
|
+
"remove",
|
|
75
|
+
"rm",
|
|
76
|
+
"update",
|
|
77
|
+
"link",
|
|
78
|
+
"unlink",
|
|
79
|
+
"pm",
|
|
80
|
+
"init",
|
|
81
|
+
"create",
|
|
82
|
+
"upgrade",
|
|
83
|
+
"outdated",
|
|
84
|
+
"publish",
|
|
85
|
+
"audit",
|
|
86
|
+
"info",
|
|
87
|
+
"repl",
|
|
88
|
+
"exec",
|
|
89
|
+
"patch",
|
|
90
|
+
]);
|
|
91
|
+
const BUN_RUN_VALUE_FLAGS = new Set([
|
|
92
|
+
"--cwd",
|
|
93
|
+
"--filter",
|
|
94
|
+
"-F",
|
|
95
|
+
"--elide-lines",
|
|
96
|
+
"--env-file",
|
|
97
|
+
"--config",
|
|
98
|
+
"-c",
|
|
99
|
+
"--preload",
|
|
100
|
+
"-r",
|
|
101
|
+
]);
|
|
102
|
+
const BUN_TEST_VALUE_FLAGS = new Set([
|
|
103
|
+
"-t",
|
|
104
|
+
"--test-name-pattern",
|
|
105
|
+
"--timeout",
|
|
106
|
+
"--rerun-each",
|
|
107
|
+
"--preload",
|
|
108
|
+
"-r",
|
|
109
|
+
"--coverage-reporter",
|
|
110
|
+
"--coverage-dir",
|
|
111
|
+
"--reporter",
|
|
112
|
+
"--reporter-outfile",
|
|
113
|
+
"--max-concurrency",
|
|
114
|
+
"--seed",
|
|
115
|
+
"--cwd",
|
|
116
|
+
"--env-file",
|
|
117
|
+
"--config",
|
|
118
|
+
"-c",
|
|
119
|
+
"--path-ignore-patterns",
|
|
120
|
+
]);
|
|
121
|
+
const NPM_VALUE_FLAGS = new Set([
|
|
122
|
+
"-w",
|
|
123
|
+
"--workspace",
|
|
124
|
+
"--prefix",
|
|
125
|
+
"-C",
|
|
126
|
+
"--dir",
|
|
127
|
+
"--filter",
|
|
128
|
+
"-F",
|
|
129
|
+
"--cwd",
|
|
130
|
+
]);
|
|
131
|
+
const RUNNER_VALUE_FLAGS = new Set(["-p", "--package", "-c", "--call"]);
|
|
132
|
+
export const ADMISSION_SKIP_PREFIX = "HARNERY_ADMISSION_SKIP=1";
|
|
133
|
+
/** Claude Code's maximum Bash timeout; a queued wait counts against it. */
|
|
134
|
+
export const BASH_TIMEOUT_MAX_MS = 600_000;
|
|
135
|
+
const LABEL_MAX = 70;
|
|
136
|
+
function baseName(value) {
|
|
137
|
+
return value.split("/").at(-1) ?? value;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Tokens with source offsets. Quotes stay opaque, heredoc bodies and comments
|
|
141
|
+
* are skipped, and `$(` opens a nested command list.
|
|
142
|
+
*/
|
|
143
|
+
function lex(src) {
|
|
144
|
+
const out = [];
|
|
145
|
+
const n = src.length;
|
|
146
|
+
let heredocs = [];
|
|
147
|
+
let pendingHeredoc = null;
|
|
148
|
+
let i = 0;
|
|
149
|
+
while (i < n) {
|
|
150
|
+
const ch = src[i];
|
|
151
|
+
if (ch === "\\" && src[i + 1] === "\n") {
|
|
152
|
+
i += 2;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (ch === "\n") {
|
|
156
|
+
out.push({ kind: "op", op: ";", start: i });
|
|
157
|
+
i += 1;
|
|
158
|
+
for (const doc of heredocs) {
|
|
159
|
+
while (i < n) {
|
|
160
|
+
const found = src.indexOf("\n", i);
|
|
161
|
+
const stop = found === -1 ? n : found;
|
|
162
|
+
const line = src.slice(i, stop);
|
|
163
|
+
i = stop + 1;
|
|
164
|
+
if ((doc.tabs ? line.replace(/^\t+/, "") : line) === doc.end)
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
heredocs = [];
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (ch === " " || ch === "\t" || ch === "\r") {
|
|
172
|
+
i += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (ch === "#") {
|
|
176
|
+
const found = src.indexOf("\n", i);
|
|
177
|
+
i = found === -1 ? n : found;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (ch === "$" && src.startsWith("$(", i) && !src.startsWith("$((", i)) {
|
|
181
|
+
out.push({ kind: "op", op: "(", start: i });
|
|
182
|
+
i += 2;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (";|&()<>".includes(ch)) {
|
|
186
|
+
const op = OPERATORS.find((candidate) => src.startsWith(candidate, i)) ?? ch;
|
|
187
|
+
out.push({ kind: "op", op, start: i });
|
|
188
|
+
if (op === "<<" || op === "<<-")
|
|
189
|
+
pendingHeredoc = op;
|
|
190
|
+
i += op.length;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const start = i;
|
|
194
|
+
let value = "";
|
|
195
|
+
let quoted = false;
|
|
196
|
+
while (i < n && !" \t\r\n;|&()<>".includes(src[i])) {
|
|
197
|
+
const c = src[i];
|
|
198
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
199
|
+
quoted = true;
|
|
200
|
+
let close;
|
|
201
|
+
if (c === "'") {
|
|
202
|
+
close = src.indexOf(c, i + 1);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
let j = i + 1;
|
|
206
|
+
while (j < n && src[j] !== c)
|
|
207
|
+
j += src[j] === "\\" ? 2 : 1;
|
|
208
|
+
close = j < n ? j : -1;
|
|
209
|
+
}
|
|
210
|
+
if (close === -1)
|
|
211
|
+
close = n;
|
|
212
|
+
value += src.slice(i + 1, close);
|
|
213
|
+
i = close + 1;
|
|
214
|
+
}
|
|
215
|
+
else if (c === "$" && src.startsWith("$(", i)) {
|
|
216
|
+
// A substitution inside a word keeps the word opaque to its end.
|
|
217
|
+
let depth = 1;
|
|
218
|
+
i += 2;
|
|
219
|
+
while (i < n && depth) {
|
|
220
|
+
if (src[i] === "(")
|
|
221
|
+
depth += 1;
|
|
222
|
+
else if (src[i] === ")")
|
|
223
|
+
depth -= 1;
|
|
224
|
+
i += 1;
|
|
225
|
+
}
|
|
226
|
+
quoted = true;
|
|
227
|
+
}
|
|
228
|
+
else if (c === "\\" && i + 1 < n) {
|
|
229
|
+
value += src[i + 1];
|
|
230
|
+
i += 2;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
value += c;
|
|
234
|
+
i += 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const word = { value, start, end: Math.min(i, n), quoted };
|
|
238
|
+
if (pendingHeredoc) {
|
|
239
|
+
heredocs.push({ end: word.value, tabs: pendingHeredoc === "<<-" });
|
|
240
|
+
pendingHeredoc = null;
|
|
241
|
+
}
|
|
242
|
+
out.push({ kind: "word", word });
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Each simple command as its argument words: leading assignments, redirect
|
|
248
|
+
* targets, and reserved words removed.
|
|
249
|
+
*/
|
|
250
|
+
function simpleCommands(tokens) {
|
|
251
|
+
const commands = [];
|
|
252
|
+
let words = [];
|
|
253
|
+
let prefixDone = false;
|
|
254
|
+
let skipNext = false;
|
|
255
|
+
for (let k = 0; k < tokens.length; k++) {
|
|
256
|
+
const token = tokens[k];
|
|
257
|
+
const next = tokens[k + 1];
|
|
258
|
+
if (token.kind === "word" &&
|
|
259
|
+
/^\d+$/.test(token.word.value) &&
|
|
260
|
+
!token.word.quoted &&
|
|
261
|
+
next?.kind === "op" &&
|
|
262
|
+
REDIRECTS.includes(next.op) &&
|
|
263
|
+
next.start === token.word.end) {
|
|
264
|
+
continue; // the 2 in 2>&1 is a file descriptor, not an argument
|
|
265
|
+
}
|
|
266
|
+
if (token.kind === "op") {
|
|
267
|
+
if (SEPARATORS.has(token.op)) {
|
|
268
|
+
if (words.length)
|
|
269
|
+
commands.push(words);
|
|
270
|
+
words = [];
|
|
271
|
+
prefixDone = false;
|
|
272
|
+
skipNext = false;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
skipNext = true;
|
|
276
|
+
}
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const word = token.word;
|
|
280
|
+
if (skipNext) {
|
|
281
|
+
skipNext = false;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (!words.length && !word.quoted) {
|
|
285
|
+
if (RESERVED.has(word.value))
|
|
286
|
+
continue;
|
|
287
|
+
if (!prefixDone && ASSIGNMENT.test(word.value))
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
prefixDone = true;
|
|
291
|
+
words.push(word);
|
|
292
|
+
}
|
|
293
|
+
if (words.length)
|
|
294
|
+
commands.push(words);
|
|
295
|
+
return commands;
|
|
296
|
+
}
|
|
297
|
+
/** Arguments that are not flags or flag values. */
|
|
298
|
+
function positional(args, valueFlags = new Set()) {
|
|
299
|
+
const out = [];
|
|
300
|
+
let skip = false;
|
|
301
|
+
for (const arg of args) {
|
|
302
|
+
if (skip) {
|
|
303
|
+
skip = false;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (arg === "--")
|
|
307
|
+
continue;
|
|
308
|
+
if (arg.startsWith("-")) {
|
|
309
|
+
if (valueFlags.has(arg))
|
|
310
|
+
skip = true;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
out.push(arg);
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
function intersects(args, set) {
|
|
318
|
+
return args.some((arg) => set.has(arg));
|
|
319
|
+
}
|
|
320
|
+
function tscHeavy(args) {
|
|
321
|
+
return !intersects(args, TSC_INFO_FLAGS) && !intersects(args, WATCH_FLAGS);
|
|
322
|
+
}
|
|
323
|
+
function scriptHeavy(script, rest) {
|
|
324
|
+
if (intersects(rest, WATCH_FLAGS))
|
|
325
|
+
return false;
|
|
326
|
+
if (HEAVY_SCRIPTS.test(script))
|
|
327
|
+
return true;
|
|
328
|
+
return TEST_SCRIPTS.test(script) && positional(rest).length === 0;
|
|
329
|
+
}
|
|
330
|
+
/** A package binary launched by npx/bunx/yarn/pnpm exec/node_modules/.bin. */
|
|
331
|
+
function runBin(name, args) {
|
|
332
|
+
if (name === "tsc" || name === "typescript" || name.startsWith("typescript@")) {
|
|
333
|
+
return tscHeavy(args);
|
|
334
|
+
}
|
|
335
|
+
if (name === "next" || name.startsWith("next@")) {
|
|
336
|
+
return args[0] === "build" && !args.includes("-h") && !args.includes("--help");
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
/** Skip npx/bunx options to the binary name, then judge it. */
|
|
341
|
+
function runnerBin(args, valueFlags) {
|
|
342
|
+
let i = 0;
|
|
343
|
+
while (i < args.length && args[i].startsWith("-")) {
|
|
344
|
+
i += valueFlags.has(args[i]) ? 2 : 1;
|
|
345
|
+
}
|
|
346
|
+
return i < args.length && runBin(args[i], args.slice(i + 1));
|
|
347
|
+
}
|
|
348
|
+
function skipFlags(args, valueFlags) {
|
|
349
|
+
let i = 0;
|
|
350
|
+
while (i < args.length && args[i].startsWith("-")) {
|
|
351
|
+
i += valueFlags.has(args[i]) ? 2 : 1;
|
|
352
|
+
}
|
|
353
|
+
return i;
|
|
354
|
+
}
|
|
355
|
+
/** True when this simple command is a heavy check. */
|
|
356
|
+
function isHeavy(words, depth = 0) {
|
|
357
|
+
let args = words.map((word) => word.value);
|
|
358
|
+
// Wrappers that are binaries (timeout, nice, nohup, env, stdbuf) or keywords.
|
|
359
|
+
while (args.length) {
|
|
360
|
+
const cmd = baseName(args[0]);
|
|
361
|
+
if (cmd === "timeout") {
|
|
362
|
+
let j = 1;
|
|
363
|
+
while (j < args.length && args[j].startsWith("-")) {
|
|
364
|
+
j += ["-k", "--kill-after", "-s", "--signal"].includes(args[j]) ? 2 : 1;
|
|
365
|
+
}
|
|
366
|
+
args = args.slice(j + 1);
|
|
367
|
+
}
|
|
368
|
+
else if (cmd === "nice") {
|
|
369
|
+
if (args[1] === "-n")
|
|
370
|
+
args = args.slice(3);
|
|
371
|
+
else if (args[1] !== undefined && /^-\d+$/.test(args[1]))
|
|
372
|
+
args = args.slice(2);
|
|
373
|
+
else
|
|
374
|
+
args = args.slice(1);
|
|
375
|
+
}
|
|
376
|
+
else if (["nohup", "time", "command", "exec", "stdbuf", "env"].includes(cmd)) {
|
|
377
|
+
if (cmd === "command" && (args[1] === "-v" || args[1] === "-V"))
|
|
378
|
+
return false;
|
|
379
|
+
let j = 1;
|
|
380
|
+
while (j < args.length &&
|
|
381
|
+
(args[j].startsWith("-") ||
|
|
382
|
+
(cmd === "env" && ASSIGNMENT.test(args[j])))) {
|
|
383
|
+
j += 1;
|
|
384
|
+
}
|
|
385
|
+
args = args.slice(j);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!args.length)
|
|
392
|
+
return false;
|
|
393
|
+
const cmd = baseName(args[0]);
|
|
394
|
+
const rest = args.slice(1);
|
|
395
|
+
if (SHELLS.has(cmd) && depth < 2) {
|
|
396
|
+
for (let k = 0; k < rest.length; k++) {
|
|
397
|
+
if (SHELL_C_FLAG.test(rest[k]) && k + 1 < rest.length) {
|
|
398
|
+
return simpleCommands(lex(rest[k + 1])).some((inner) => isHeavy(inner, depth + 1));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
if (cmd === "tsc")
|
|
404
|
+
return tscHeavy(rest);
|
|
405
|
+
if (cmd === "next")
|
|
406
|
+
return runBin("next", rest);
|
|
407
|
+
if (cmd === "node") {
|
|
408
|
+
const script = rest.find((arg) => !arg.startsWith("-")) ?? "";
|
|
409
|
+
const after = rest.slice(rest.indexOf(script) + 1);
|
|
410
|
+
if (script.endsWith("/tsc") || script.endsWith("typescript/bin/tsc"))
|
|
411
|
+
return tscHeavy(after);
|
|
412
|
+
if (script.endsWith("/next") || script.endsWith("next/dist/bin/next")) {
|
|
413
|
+
return runBin("next", after);
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
if (cmd === "npx" || cmd === "bunx" || cmd === "pnpx")
|
|
418
|
+
return runnerBin(rest, RUNNER_VALUE_FLAGS);
|
|
419
|
+
if (cmd === "bun") {
|
|
420
|
+
const i = skipFlags(rest, BUN_RUN_VALUE_FLAGS);
|
|
421
|
+
if (i >= rest.length)
|
|
422
|
+
return false;
|
|
423
|
+
const sub = rest[i];
|
|
424
|
+
const tail = rest.slice(i + 1);
|
|
425
|
+
if (sub === "x")
|
|
426
|
+
return runnerBin(tail, new Set(["-p", "--package"]));
|
|
427
|
+
if (sub === "test") {
|
|
428
|
+
return !intersects(tail, WATCH_FLAGS) && positional(tail, BUN_TEST_VALUE_FLAGS).length === 0;
|
|
429
|
+
}
|
|
430
|
+
if (sub === "run") {
|
|
431
|
+
const j = skipFlags(tail, BUN_RUN_VALUE_FLAGS);
|
|
432
|
+
if (j >= tail.length)
|
|
433
|
+
return false;
|
|
434
|
+
const script = tail[j];
|
|
435
|
+
return scriptHeavy(script, tail.slice(j + 1)) || runBin(script, tail.slice(j + 1));
|
|
436
|
+
}
|
|
437
|
+
if (BUN_BUILTINS.has(sub) || sub.includes("/") || /\.(ts|js|tsx|mjs|cjs)$/.test(sub)) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
return scriptHeavy(sub, tail) || runBin(sub, tail);
|
|
441
|
+
}
|
|
442
|
+
if (cmd === "npm" || cmd === "pnpm" || cmd === "yarn") {
|
|
443
|
+
const i = skipFlags(rest, NPM_VALUE_FLAGS);
|
|
444
|
+
if (i >= rest.length)
|
|
445
|
+
return false;
|
|
446
|
+
const sub = rest[i];
|
|
447
|
+
const tail = rest.slice(i + 1);
|
|
448
|
+
if (sub === "test" || sub === "t" || sub === "tst")
|
|
449
|
+
return scriptHeavy("test", tail);
|
|
450
|
+
if (["run", "run-script", "rum", "urn"].includes(sub)) {
|
|
451
|
+
const j = skipFlags(tail, NPM_VALUE_FLAGS);
|
|
452
|
+
return j < tail.length && scriptHeavy(tail[j], tail.slice(j + 1));
|
|
453
|
+
}
|
|
454
|
+
if (sub === "exec" || sub === "dlx")
|
|
455
|
+
return runnerBin(tail, RUNNER_VALUE_FLAGS);
|
|
456
|
+
// pnpm <script> and yarn <script|bin> run package scripts and bins directly.
|
|
457
|
+
if (cmd !== "npm")
|
|
458
|
+
return scriptHeavy(sub, tail) || runBin(sub, tail);
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Offset to insert the wrapper: before binary wrappers, after the shell
|
|
465
|
+
* keywords and builtins that `admission run` could not exec.
|
|
466
|
+
*/
|
|
467
|
+
function insertionPoint(words) {
|
|
468
|
+
for (let k = 0; k < words.length; k++) {
|
|
469
|
+
const word = words[k];
|
|
470
|
+
const keyword = !word.quoted &&
|
|
471
|
+
(word.value === "time" ||
|
|
472
|
+
word.value === "command" ||
|
|
473
|
+
word.value === "exec" ||
|
|
474
|
+
(word.value === "-p" && k > 0 && words[k - 1]?.value === "time"));
|
|
475
|
+
if (!keyword)
|
|
476
|
+
return word.start;
|
|
477
|
+
}
|
|
478
|
+
return words[0].start;
|
|
479
|
+
}
|
|
480
|
+
/** POSIX single-quote a word when it needs quoting (shlex.quote semantics). */
|
|
481
|
+
export function shellQuote(value) {
|
|
482
|
+
if (value === "")
|
|
483
|
+
return "''";
|
|
484
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value))
|
|
485
|
+
return value;
|
|
486
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
487
|
+
}
|
|
488
|
+
/** True when a simple command already queues through admission on purpose. */
|
|
489
|
+
function alreadyAdmitted(words) {
|
|
490
|
+
const first = baseName(words[0]?.value ?? "");
|
|
491
|
+
if (first === "heavy-slot")
|
|
492
|
+
return true;
|
|
493
|
+
const values = words.map((word) => word.value);
|
|
494
|
+
const at = values.indexOf("admission");
|
|
495
|
+
return at !== -1 && values[at + 1] === "run";
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Return the command with each heavy simple command wrapped in `wrapper`
|
|
499
|
+
* (a shell-ready prefix such as `harn admission run --resource build`), or
|
|
500
|
+
* null when nothing changes.
|
|
501
|
+
*/
|
|
502
|
+
export function rewriteHeavyCommand(command, wrapper) {
|
|
503
|
+
if (command.includes(ADMISSION_SKIP_PREFIX))
|
|
504
|
+
return null;
|
|
505
|
+
const commands = simpleCommands(lex(command));
|
|
506
|
+
if (commands.some(alreadyAdmitted))
|
|
507
|
+
return null;
|
|
508
|
+
const inserts = [];
|
|
509
|
+
for (const words of commands) {
|
|
510
|
+
if (!isHeavy(words))
|
|
511
|
+
continue;
|
|
512
|
+
const at = insertionPoint(words);
|
|
513
|
+
let label = command
|
|
514
|
+
.slice(words[0].start, words.at(-1).end)
|
|
515
|
+
.split(/\s+/)
|
|
516
|
+
.filter(Boolean)
|
|
517
|
+
.join(" ");
|
|
518
|
+
if (label.length > LABEL_MAX)
|
|
519
|
+
label = `${label.slice(0, LABEL_MAX - 1)}~`;
|
|
520
|
+
inserts.push({ at, text: `${wrapper} --label ${shellQuote(`agent: ${label}`)} -- ` });
|
|
521
|
+
}
|
|
522
|
+
if (!inserts.length)
|
|
523
|
+
return null;
|
|
524
|
+
let out = command;
|
|
525
|
+
for (const { at, text } of inserts.sort((a, b) => b.at - a.at)) {
|
|
526
|
+
out = out.slice(0, at) + text + out.slice(at);
|
|
527
|
+
}
|
|
528
|
+
return out;
|
|
529
|
+
}
|
|
530
|
+
const SHELL_TOOLS = new Set(["bash", "shell", "shell_command", "exec_command", "local_shell"]);
|
|
531
|
+
/** Normalized shell-tool name, or null for any other tool. */
|
|
532
|
+
export function shellToolName(toolName) {
|
|
533
|
+
if (typeof toolName !== "string")
|
|
534
|
+
return null;
|
|
535
|
+
const name = toolName.toLowerCase().replace(/^functions\./, "");
|
|
536
|
+
return SHELL_TOOLS.has(name) ? name : null;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Rewrite a shell tool's input so its heavy checks queue for admission.
|
|
540
|
+
* Handles `command` or `cmd` as a string, or as an argv such as
|
|
541
|
+
* ["bash", "-lc", "<script>"]. Returns null when nothing changes.
|
|
542
|
+
*/
|
|
543
|
+
export function admissionRewriteForTool(toolName, toolInput, wrapper) {
|
|
544
|
+
if (shellToolName(toolName) === null)
|
|
545
|
+
return null;
|
|
546
|
+
let input = toolInput;
|
|
547
|
+
if (typeof input === "string") {
|
|
548
|
+
try {
|
|
549
|
+
input = JSON.parse(input);
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
556
|
+
return null;
|
|
557
|
+
const record = input;
|
|
558
|
+
for (const key of ["command", "cmd"]) {
|
|
559
|
+
const value = record[key];
|
|
560
|
+
if (typeof value === "string") {
|
|
561
|
+
const next = rewriteHeavyCommand(value, wrapper);
|
|
562
|
+
return next === null ? null : { updatedInput: { ...record, [key]: next } };
|
|
563
|
+
}
|
|
564
|
+
if (Array.isArray(value) && value.length && value.every((v) => typeof v === "string")) {
|
|
565
|
+
const argv = value;
|
|
566
|
+
if (argv.length >= 3 &&
|
|
567
|
+
SHELLS.has(baseName(argv[0])) &&
|
|
568
|
+
SHELL_C_FLAG.test(argv[argv.length - 2])) {
|
|
569
|
+
const next = rewriteHeavyCommand(argv[argv.length - 1], wrapper);
|
|
570
|
+
return next === null
|
|
571
|
+
? null
|
|
572
|
+
: { updatedInput: { ...record, [key]: [...argv.slice(0, -1), next] } };
|
|
573
|
+
}
|
|
574
|
+
const next = rewriteHeavyCommand(argv.map(shellQuote).join(" "), wrapper);
|
|
575
|
+
return next === null ? null : { updatedInput: { ...record, [key]: ["bash", "-c", next] } };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* The PreToolUse output that queues a shell command's heavy checks, or null
|
|
582
|
+
* to let the command run as written. Claude Code applies `updatedInput`
|
|
583
|
+
* under the session's own permission rules, so no decision is sent, and its
|
|
584
|
+
* Bash timeout is raised to the maximum because a queued wait counts against
|
|
585
|
+
* it. Codex applies a rewrite only beside `permissionDecision: "allow"`.
|
|
586
|
+
* Cursor and OpenCode have no rewrite channel. Windows-native shells (a
|
|
587
|
+
* drive-letter or UNC workspace) are outside the machine whose queue this
|
|
588
|
+
* guards.
|
|
589
|
+
*/
|
|
590
|
+
export function admissionHookOutput(input) {
|
|
591
|
+
if (input.skip)
|
|
592
|
+
return null;
|
|
593
|
+
if (input.adapter !== "claude-code" && input.adapter !== "codex")
|
|
594
|
+
return null;
|
|
595
|
+
const cwd = typeof input.cwd === "string" ? input.cwd : "";
|
|
596
|
+
if (input.platform === "win32" || /^(?:\\\\|[A-Za-z]:[\\/])/.test(cwd))
|
|
597
|
+
return null;
|
|
598
|
+
const wrapper = `${shellQuote(input.bin)} admission run --resource ${shellQuote(input.resource)}` +
|
|
599
|
+
(input.projectRoot ? ` --project-root ${shellQuote(input.projectRoot)}` : "");
|
|
600
|
+
const rewrite = admissionRewriteForTool(input.toolName, input.toolInput, wrapper);
|
|
601
|
+
if (!rewrite)
|
|
602
|
+
return null;
|
|
603
|
+
const updatedInput = rewrite.updatedInput;
|
|
604
|
+
const output = { hookEventName: "PreToolUse", updatedInput };
|
|
605
|
+
if (input.adapter === "codex") {
|
|
606
|
+
output.permissionDecision = "allow";
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
if (shellToolName(input.toolName) === "bash" && !updatedInput.run_in_background) {
|
|
610
|
+
const current = updatedInput.timeout;
|
|
611
|
+
if (typeof current !== "number" || current < BASH_TIMEOUT_MAX_MS) {
|
|
612
|
+
updatedInput.timeout = BASH_TIMEOUT_MAX_MS;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
output.additionalContext =
|
|
616
|
+
`Harnery queued this command's heavy check for a machine-wide \`${input.resource}\` ` +
|
|
617
|
+
`admission slot, so it may wait first; \`${input.bin} admission status --resource ` +
|
|
618
|
+
`${input.resource}\` shows the queue. Prefix ${ADMISSION_SKIP_PREFIX} to run a check ` +
|
|
619
|
+
`ungated on purpose.`;
|
|
620
|
+
}
|
|
621
|
+
return { hookSpecificOutput: output };
|
|
622
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-commit-tree.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/shell-commit-tree.ts"],"names":[],"mappings":"AAuFA,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,OAAO,EACjB,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAUf"}
|