@illuminis/comprism 0.1.3 → 0.1.4
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 +61 -2
- package/out/agent/command.d.ts +75 -4
- package/out/agent/command.js +220 -25
- package/out/agent/render.d.ts +17 -2
- package/out/agent/render.js +157 -14
- package/out/agent/session.d.ts +141 -2
- package/out/agent/session.js +735 -149
- package/out/commands/agents.d.ts +2 -0
- package/out/commands/agents.js +79 -0
- package/out/commands/ask.d.ts +1 -1
- package/out/commands/ask.js +78 -11
- package/out/commands/commands-thin.js +79 -17
- package/out/commands/config.d.ts +1 -0
- package/out/commands/config.js +138 -0
- package/out/commands/cost.d.ts +1 -0
- package/out/commands/cost.js +167 -0
- package/out/commands/hooks.d.ts +1 -0
- package/out/commands/hooks.js +83 -0
- package/out/commands/install.d.ts +44 -1
- package/out/commands/install.js +198 -4
- package/out/commands/instructions.d.ts +1 -0
- package/out/commands/instructions.js +113 -0
- package/out/commands/integrations.d.ts +3 -0
- package/out/commands/integrations.js +215 -0
- package/out/commands/jobs.d.ts +5 -0
- package/out/commands/jobs.js +157 -0
- package/out/commands/login.js +188 -36
- package/out/commands/memory.d.ts +3 -0
- package/out/commands/memory.js +113 -0
- package/out/commands/permissions.d.ts +1 -0
- package/out/commands/permissions.js +94 -0
- package/out/commands/plugins.d.ts +4 -0
- package/out/commands/plugins.js +192 -0
- package/out/commands/privacy.d.ts +1 -0
- package/out/commands/privacy.js +57 -0
- package/out/commands/providerKey.d.ts +32 -0
- package/out/commands/providerKey.js +108 -0
- package/out/commands/repl.d.ts +8 -1
- package/out/commands/repl.js +1207 -118
- package/out/commands/report.d.ts +39 -0
- package/out/commands/report.js +115 -0
- package/out/commands/review.d.ts +5 -0
- package/out/commands/review.js +223 -0
- package/out/commands/sessions.d.ts +23 -0
- package/out/commands/sessions.js +115 -0
- package/out/commands/settings.d.ts +3 -1
- package/out/commands/settings.js +18 -16
- package/out/commands/skills.d.ts +21 -0
- package/out/commands/skills.js +207 -0
- package/out/commands/unattended.d.ts +7 -0
- package/out/commands/unattended.js +351 -0
- package/out/commands/update.d.ts +1 -0
- package/out/commands/update.js +123 -0
- package/out/commands/worktrees.d.ts +5 -0
- package/out/commands/worktrees.js +186 -0
- package/out/executor/browser.d.ts +14 -0
- package/out/executor/browser.js +270 -0
- package/out/executor/diagnostics.d.ts +2 -0
- package/out/executor/diagnostics.js +181 -0
- package/out/executor/files.js +270 -40
- package/out/executor/git.js +42 -29
- package/out/executor/hooks.d.ts +42 -58
- package/out/executor/hooks.js +89 -182
- package/out/executor/index.d.ts +21 -5
- package/out/executor/index.js +160 -14
- package/out/executor/paths.d.ts +6 -1
- package/out/executor/paths.js +34 -6
- package/out/executor/sandbox.d.ts +40 -0
- package/out/executor/sandbox.js +299 -0
- package/out/executor/shell.d.ts +49 -4
- package/out/executor/shell.js +302 -56
- package/out/executor/toolservers.d.ts +20 -0
- package/out/executor/toolservers.js +189 -0
- package/out/executor/worktree.d.ts +9 -0
- package/out/executor/worktree.js +119 -0
- package/out/graph/read-python.js +2 -1
- package/out/lib/attach.d.ts +56 -12
- package/out/lib/attach.js +230 -63
- package/out/lib/clipboard.d.ts +23 -0
- package/out/lib/clipboard.js +182 -0
- package/out/lib/commandlist.d.ts +20 -0
- package/out/lib/commandlist.js +58 -0
- package/out/lib/decision.d.ts +22 -0
- package/out/lib/decision.js +50 -0
- package/out/lib/fingerprint.d.ts +25 -0
- package/out/lib/fingerprint.js +58 -0
- package/out/lib/gateway.d.ts +186 -2
- package/out/lib/gateway.js +59 -4
- package/out/lib/history.d.ts +24 -0
- package/out/lib/history.js +137 -0
- package/out/lib/ide.d.ts +19 -0
- package/out/lib/ide.js +131 -0
- package/out/lib/keyboard.d.ts +95 -0
- package/out/lib/keyboard.js +383 -0
- package/out/lib/machine.d.ts +21 -0
- package/out/lib/machine.js +91 -0
- package/out/lib/notify.d.ts +4 -0
- package/out/lib/notify.js +52 -0
- package/out/lib/output.d.ts +48 -0
- package/out/lib/output.js +108 -0
- package/out/lib/project-ops.d.ts +19 -0
- package/out/lib/project-ops.js +146 -0
- package/out/lib/project.d.ts +28 -0
- package/out/lib/project.js +114 -0
- package/out/lib/prompt.js +15 -2
- package/out/lib/queue.d.ts +13 -0
- package/out/lib/queue.js +116 -0
- package/out/lib/readiness.d.ts +19 -0
- package/out/lib/readiness.js +170 -1
- package/out/lib/self.d.ts +23 -0
- package/out/lib/self.js +124 -0
- package/out/lib/sessions.d.ts +19 -0
- package/out/lib/sessions.js +221 -0
- package/out/lib/stdin.d.ts +32 -0
- package/out/lib/stdin.js +117 -0
- package/out/lib/store.d.ts +40 -0
- package/out/lib/store.js +138 -0
- package/out/lib/sync.d.ts +18 -0
- package/out/lib/sync.js +81 -0
- package/out/lib/ui.d.ts +2 -4
- package/out/lib/ui.js +31 -25
- package/out/lib/voice.js +24 -0
- package/out/postinstall.js +42 -17
- package/out/providers/anthropic.d.ts +22 -0
- package/out/providers/anthropic.js +80 -0
- package/out/providers/index.d.ts +8 -0
- package/out/providers/index.js +83 -0
- package/out/providers/openai.d.ts +11 -0
- package/out/providers/openai.js +57 -0
- package/out/thin.js +594 -32
- package/package.json +9 -49
package/out/executor/git.js
CHANGED
|
@@ -26,47 +26,45 @@ exports.gitPush = gitPush;
|
|
|
26
26
|
* being reimplemented for git.
|
|
27
27
|
*/
|
|
28
28
|
const shell_1 = require("./shell");
|
|
29
|
-
/**
|
|
29
|
+
/** Every git call is a program and a list of arguments, with no shell.
|
|
30
30
|
*
|
|
31
31
|
* A commit message is written by a model and can contain anything: quotes,
|
|
32
32
|
* backticks, a newline, a semicolon. Pasting one into a shell string is how a
|
|
33
|
-
* message becomes a second command
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
33
|
+
* message becomes a second command, and quoting it for one shell broke it on
|
|
34
|
+
* another: Windows kept the POSIX quotes as part of the message. */
|
|
35
|
+
const git = (args, opts) => (0, shell_1.runProgram)("git", args, opts);
|
|
37
36
|
async function gitStatus(opts) {
|
|
38
37
|
// Porcelain, because the human-readable format changes between versions and
|
|
39
38
|
// the model has to be able to rely on the shape.
|
|
40
|
-
const r = await (
|
|
39
|
+
const r = await git(["status", "--porcelain=v1", "--branch"], opts);
|
|
41
40
|
if (!r.isError && /^exit 0/.test(r.content) && r.content.trim().split("\n").length <= 3) {
|
|
42
41
|
return { ...r, content: `${r.content}\n\n[Nothing has changed in the working tree.]` };
|
|
43
42
|
}
|
|
44
43
|
return r;
|
|
45
44
|
}
|
|
46
45
|
async function gitDiff(opts) {
|
|
47
|
-
const parts = ["
|
|
46
|
+
const parts = ["--no-pager", "diff"];
|
|
48
47
|
if (opts.staged)
|
|
49
48
|
parts.push("--staged");
|
|
50
49
|
// Color off explicitly: a diff full of escape codes costs tokens and reads
|
|
51
50
|
// as noise to a model.
|
|
52
51
|
parts.push("--no-color");
|
|
53
52
|
if (opts.path)
|
|
54
|
-
parts.push("--",
|
|
55
|
-
return (
|
|
53
|
+
parts.push("--", opts.path);
|
|
54
|
+
return git(parts, opts);
|
|
56
55
|
}
|
|
57
56
|
async function gitLog(opts) {
|
|
58
57
|
const limit = Math.max(1, Math.min(opts.limit ?? 20, 100));
|
|
59
58
|
const parts = [
|
|
60
|
-
"
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
`--pretty=${quote("format:%h %an %ar %s")}`,
|
|
59
|
+
"--no-pager", "log", "-n", String(limit),
|
|
60
|
+
// One argument, spaces and all. When this went through a shell it had to be
|
|
61
|
+
// quoted, or git read "%an" as a revision and failed with "ambiguous
|
|
62
|
+
// argument", which reads like a broken repository.
|
|
63
|
+
"--pretty=format:%h %an %ar %s",
|
|
66
64
|
];
|
|
67
65
|
if (opts.path)
|
|
68
|
-
parts.push("--",
|
|
69
|
-
return (
|
|
66
|
+
parts.push("--", opts.path);
|
|
67
|
+
return git(parts, opts);
|
|
70
68
|
}
|
|
71
69
|
async function gitBranch(name, opts) {
|
|
72
70
|
if (!/^[\w./-]{1,120}$/.test(name)) {
|
|
@@ -75,21 +73,27 @@ async function gitBranch(name, opts) {
|
|
|
75
73
|
content: `Refused: ${name} is not a usable branch name. Letters, numbers, dots, dashes, underscores and slashes only.`,
|
|
76
74
|
};
|
|
77
75
|
}
|
|
78
|
-
return (
|
|
76
|
+
return git(["checkout", "-b", name], opts);
|
|
79
77
|
}
|
|
80
78
|
async function gitCommit(message, opts) {
|
|
81
79
|
if (!message.trim()) {
|
|
82
80
|
return { isError: true, content: "Refused: a commit needs a message." };
|
|
83
81
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
// Only the files named: what the agent changed, or what the person named.
|
|
83
|
+
// Never "add everything", which would sweep in the person's own unrelated
|
|
84
|
+
// work (manual 5.17).
|
|
85
|
+
if (!opts.paths?.length) {
|
|
86
|
+
return {
|
|
87
|
+
isError: true,
|
|
88
|
+
content: "Refused: nothing to commit that this session changed. Name the files to commit.",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const staged = await git(["add", "--", ...opts.paths], opts);
|
|
92
|
+
if (staged.isError || !/^exit 0/.test(staged.content))
|
|
93
|
+
return { ...staged, isError: true };
|
|
90
94
|
// Nothing to commit is a normal outcome, not a failure. Reported plainly so
|
|
91
95
|
// the model does not conclude that committing is broken and try again.
|
|
92
|
-
const check = await (
|
|
96
|
+
const check = await git(["diff", "--staged", "--quiet"], opts);
|
|
93
97
|
if (/^exit 0\b/.test(check.content)) {
|
|
94
98
|
return { content: "Nothing to commit: no changes are staged.", summary: "nothing to commit" };
|
|
95
99
|
}
|
|
@@ -97,7 +101,13 @@ async function gitCommit(message, opts) {
|
|
|
97
101
|
// a commit somebody later reads as a colleague's careful work, and the whole
|
|
98
102
|
// record this product keeps depends on that not happening.
|
|
99
103
|
const trailer = "\n\nCo-authored-by: illuminis coding agent <agent@illuminis.ai>";
|
|
100
|
-
|
|
104
|
+
const made = await git(["commit", "-m", message + trailer], opts);
|
|
105
|
+
// A commit hook that refused it is a failed commit, said so (manual 5.17).
|
|
106
|
+
// Its output is the reason; what was staged is left staged for the person.
|
|
107
|
+
if (!/^exit 0/.test(made.content)) {
|
|
108
|
+
return { ...made, isError: true, summary: "commit failed", content: `Commit failed. ${made.content}` };
|
|
109
|
+
}
|
|
110
|
+
return { ...made, summary: `committed ${opts.paths.join(", ")}` };
|
|
101
111
|
}
|
|
102
112
|
/** Send commits to the remote.
|
|
103
113
|
*
|
|
@@ -117,11 +127,14 @@ async function gitPush(branch, opts) {
|
|
|
117
127
|
}
|
|
118
128
|
// What is about to go, named before it goes. A push that reports success
|
|
119
129
|
// without saying what it sent is a push nobody can check afterwards.
|
|
120
|
-
|
|
130
|
+
// A branch the remote has not seen yet has no range to compare with, so the
|
|
131
|
+
// last five commits stand in for it.
|
|
132
|
+
let ahead = await git(["--no-pager", "log", "--oneline", `origin/${branch}..HEAD`], opts);
|
|
133
|
+
if (!/^exit 0/.test(ahead.content))
|
|
134
|
+
ahead = await git(["--no-pager", "log", "--oneline", "-5"], opts);
|
|
121
135
|
// `-u` on a new branch so later pushes need no arguments, and nothing else.
|
|
122
136
|
// No `--force`, no `--force-with-lease`, no `+refs` refspec.
|
|
123
|
-
const
|
|
124
|
-
const pushed = await (0, shell_1.runCommand)(`git push ${flags} origin ${quote(branch)}`.replace(/\s+/g, " "), opts);
|
|
137
|
+
const pushed = await git(["push", ...(opts.createRemote ? ["-u"] : []), "origin", branch], opts);
|
|
125
138
|
if (pushed.isError || !/^exit 0/.test(pushed.content)) {
|
|
126
139
|
return { ...pushed, isError: true };
|
|
127
140
|
}
|
package/out/executor/hooks.d.ts
CHANGED
|
@@ -1,67 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
* Anything else is treated as a regular expression anchored at both ends, so
|
|
9
|
-
* `"git_.*"` covers the git actions without naming all five. A pattern that
|
|
10
|
-
* will not compile matches nothing and says so once, rather than matching
|
|
11
|
-
* everything: a broken pattern that ran a hook on every action would be the
|
|
12
|
-
* dangerous way to be wrong. */
|
|
13
|
-
on: string;
|
|
14
|
-
/** "before" or "after". */
|
|
15
|
-
when: "before" | "after";
|
|
1
|
+
/** The settings files that can hold hooks, sent as read for `comprism hooks`. */
|
|
2
|
+
export declare const FILES: string[];
|
|
3
|
+
/** One hook as the service hands it over. */
|
|
4
|
+
export interface PlannedHook {
|
|
5
|
+
when: string;
|
|
6
|
+
on?: string;
|
|
16
7
|
command: string;
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
what?: string | null;
|
|
9
|
+
timeout_s?: number;
|
|
10
|
+
source?: string;
|
|
19
11
|
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export declare function
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
12
|
+
/** The hooks to run around one action, in order. */
|
|
13
|
+
export interface HookPlan {
|
|
14
|
+
before?: PlannedHook[];
|
|
15
|
+
after?: PlannedHook[];
|
|
16
|
+
}
|
|
17
|
+
/** Set inside a hook's own commands, so nothing they do runs hooks again. */
|
|
18
|
+
export declare const IN_HOOK = "COMPRISM_IN_HOOK";
|
|
19
|
+
export declare function insideHook(): boolean;
|
|
20
|
+
/** The settings files present in this project, as text, for the service. */
|
|
21
|
+
export declare function hookFiles(root: string): Record<string, string>;
|
|
22
|
+
export interface HookContext {
|
|
23
|
+
when: string;
|
|
24
|
+
action?: string;
|
|
25
|
+
args?: Record<string, unknown>;
|
|
26
|
+
facts?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
export interface HookRun {
|
|
29
|
+
hook: PlannedHook;
|
|
30
|
+
ok: boolean;
|
|
31
|
+
timedOut: boolean;
|
|
32
|
+
output: string;
|
|
33
|
+
}
|
|
34
|
+
export interface HooksOutcome {
|
|
35
|
+
runs: HookRun[];
|
|
36
36
|
/** Set when a `before` hook refused the action. */
|
|
37
37
|
refusedBy?: string;
|
|
38
38
|
detail?: string;
|
|
39
|
+
/** `after`, `job_start`, `job_end` and `waiting` hooks that failed. */
|
|
40
|
+
failed: string[];
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
41
|
-
* Run the hooks
|
|
42
|
-
*
|
|
43
|
-
* **A non-zero exit cancels the action.** This is the one place a hook changes
|
|
44
|
-
* what happens, and it is deliberate: "do not let the agent touch anything under
|
|
45
|
-
* generated/" is a real thing to want, and refusing is the only way for a
|
|
46
|
-
* project to say it.
|
|
47
|
-
*
|
|
48
|
-
* It can only ever REFUSE. There is no exit code that approves something the
|
|
49
|
-
* agent was not already allowed to do, because a permission system inside the
|
|
50
|
-
* repository would live exactly where an attacker who got that far already is.
|
|
51
|
-
*/
|
|
52
|
-
export declare function before(root: string, action: string, args: Record<string, unknown>, hooks?: Hook[]): Promise<HookOutcome>;
|
|
53
|
-
/**
|
|
54
|
-
* Run the hooks that come after one action.
|
|
43
|
+
* Run the hooks the service named, one after another, in its order.
|
|
55
44
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* worth losing the work over.
|
|
45
|
+
* Details go in as `COMPRISM_` variables and as one JSON object on standard
|
|
46
|
+
* input. Each hook has its own time limit (30 seconds unless the service said
|
|
47
|
+
* otherwise) and is stopped and reported when it runs over.
|
|
60
48
|
*/
|
|
61
|
-
export declare function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}>;
|
|
65
|
-
/** The example a person copies. Kept in code so the readme and the behavior
|
|
66
|
-
* cannot drift: this is what the reader actually gets. */
|
|
67
|
-
export declare const EXAMPLE: string;
|
|
49
|
+
export declare function runHooks(root: string, hooks: PlannedHook[], ctx: HookContext): Promise<HooksOutcome>;
|
|
50
|
+
/** One line per hook that ran, for the full view and `hooks test` (10.10). */
|
|
51
|
+
export declare function reportLines(outcome: HooksOutcome): string[];
|
package/out/executor/hooks.js
CHANGED
|
@@ -33,215 +33,122 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
37
|
-
exports.
|
|
38
|
-
exports.
|
|
39
|
-
exports.
|
|
40
|
-
exports.
|
|
36
|
+
exports.IN_HOOK = exports.FILES = void 0;
|
|
37
|
+
exports.insideHook = insideHook;
|
|
38
|
+
exports.hookFiles = hookFiles;
|
|
39
|
+
exports.runHooks = runHooks;
|
|
40
|
+
exports.reportLines = reportLines;
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* So the configuration is read from the project itself rather than from our
|
|
55
|
-
* database. A hook that lives in the repository is reviewed like any other
|
|
56
|
-
* change, travels with a branch, and is the same for everybody who works there.
|
|
57
|
-
* A hook stored in our settings would be invisible in the repository and
|
|
58
|
-
* different for whoever configured it.
|
|
59
|
-
*
|
|
60
|
-
* ## What they are for, in practice
|
|
61
|
-
*
|
|
62
|
-
* The overwhelming case is "run the formatter after the agent edits a file", and
|
|
63
|
-
* the reason it earns a feature is that the alternative is telling the model to
|
|
64
|
-
* remember to do it, which it does four times out of five.
|
|
65
|
-
*
|
|
66
|
-
* ## What a hook may not do
|
|
67
|
-
*
|
|
68
|
-
* **It cannot approve anything and it cannot change the decision.** A hook that
|
|
69
|
-
* could turn a refusal into an approval would be a permission system in a file
|
|
70
|
-
* inside the repository, which is the wrong place for one: the repository is
|
|
71
|
-
* exactly what an attacker who has got that far already controls.
|
|
72
|
-
*
|
|
73
|
-
* A hook that fails is reported and does not stop the job, with one exception
|
|
74
|
-
* stated in `before`: a `before` hook exiting non-zero cancels that one action,
|
|
75
|
-
* because "do not let the agent touch generated files" is a real thing to want
|
|
76
|
-
* and refusing is the only way to say it.
|
|
42
|
+
* Hooks: running the person's own commands at the moments the service names
|
|
43
|
+
* (manual 10.9, 10.10).
|
|
44
|
+
*
|
|
45
|
+
* Which hooks run, for which action, in what order and with what time limit is
|
|
46
|
+
* decided by the service and arrives with each action, or with a `run_hooks`
|
|
47
|
+
* request for a moment of the job. Nothing here matches a hook to an action.
|
|
48
|
+
* What is left is the physical part: read the project's settings files when
|
|
49
|
+
* `comprism hooks` asks, and run a command with the details it needs.
|
|
50
|
+
*
|
|
51
|
+
* A `before` hook can refuse its action. Nothing a hook does approves one, and
|
|
52
|
+
* a hook's own commands never set off hooks: they are not the job's actions,
|
|
53
|
+
* and a command started from inside a hook runs with every hook switched off.
|
|
77
54
|
*/
|
|
78
55
|
const fs = __importStar(require("fs"));
|
|
79
56
|
const path = __importStar(require("path"));
|
|
80
57
|
const shell_1 = require("./shell");
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
* The settings file comes first, because that is where every other thing a
|
|
84
|
-
* project says about its agent now lives, and keeping hooks somewhere else was
|
|
85
|
-
* one more folder for a newcomer to know about.
|
|
86
|
-
*
|
|
87
|
-
* `.illuminis/hooks.json` is still read, and last. It is what projects were
|
|
88
|
-
* told to write, and silently ignoring a file somebody wrote on our
|
|
89
|
-
* instructions would mean their formatter stops running with no message
|
|
90
|
-
* anywhere. Only one file is used: the first that has hooks in it wins, so a
|
|
91
|
-
* project that has moved does not get both. */
|
|
92
|
-
const CONFIGS = [
|
|
58
|
+
/** The settings files that can hold hooks, sent as read for `comprism hooks`. */
|
|
59
|
+
exports.FILES = [
|
|
93
60
|
".completionprism/settings.json",
|
|
94
61
|
".completionprism/settings.local.json",
|
|
95
62
|
".comprism/settings.json",
|
|
96
63
|
".claude/settings.json",
|
|
97
64
|
".illuminis/hooks.json",
|
|
98
65
|
];
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
for (const name of
|
|
66
|
+
/** Set inside a hook's own commands, so nothing they do runs hooks again. */
|
|
67
|
+
exports.IN_HOOK = "COMPRISM_IN_HOOK";
|
|
68
|
+
function insideHook() {
|
|
69
|
+
return process.env[exports.IN_HOOK] === "1";
|
|
70
|
+
}
|
|
71
|
+
/** The settings files present in this project, as text, for the service. */
|
|
72
|
+
function hookFiles(root) {
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const name of exports.FILES) {
|
|
108
75
|
const file = path.join(root, name);
|
|
109
|
-
if (!fs.existsSync(file))
|
|
110
|
-
continue;
|
|
111
76
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const usable = hooks.filter((h) => h && typeof h.command === "string" && h.command.trim()
|
|
115
|
-
&& (h.when === "before" || h.when === "after")
|
|
116
|
-
&& typeof h.on === "string" && h.on.trim());
|
|
117
|
-
// A settings file with no hooks section is the ordinary case, and it must
|
|
118
|
-
// not stop the older file being read. Only a file that actually declares
|
|
119
|
-
// hooks ends the search.
|
|
120
|
-
if (usable.length)
|
|
121
|
-
return usable;
|
|
77
|
+
if (fs.existsSync(file))
|
|
78
|
+
out[name] = fs.readFileSync(file, "utf8").slice(0, 200_000);
|
|
122
79
|
}
|
|
123
|
-
catch
|
|
124
|
-
|
|
125
|
-
return [];
|
|
80
|
+
catch {
|
|
81
|
+
/* unreadable is the same as absent to the service */
|
|
126
82
|
}
|
|
127
83
|
}
|
|
128
|
-
return
|
|
84
|
+
return out;
|
|
129
85
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
* Exported because it is the part with the rules in it, and a rule with no
|
|
133
|
-
* test is a rule that drifts.
|
|
134
|
-
*/
|
|
135
|
-
function covers(pattern, action) {
|
|
136
|
-
const p = String(pattern ?? "").trim();
|
|
137
|
-
if (!p || !action)
|
|
138
|
-
return false;
|
|
139
|
-
if (p === "*")
|
|
140
|
-
return true;
|
|
141
|
-
if (p === action)
|
|
142
|
-
return true;
|
|
143
|
-
if (p.includes("|") && !/[\\^$.?*+()[\]{}]/.test(p)) {
|
|
144
|
-
return p.split("|").some((one) => one.trim() === action);
|
|
145
|
-
}
|
|
146
|
-
if (!/[\\^$.?*+()[\]{}|]/.test(p))
|
|
147
|
-
return false;
|
|
148
|
-
try {
|
|
149
|
-
return new RegExp(`^(?:${p})$`).test(action);
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
// A pattern that will not compile matches NOTHING. The other direction,
|
|
153
|
-
// matching everything, would run somebody's script on every action because
|
|
154
|
-
// of a typo, which is the expensive way to be wrong.
|
|
155
|
-
process.stderr.write(` A hook pattern is not valid and was skipped: ${p}\n`);
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
function matching(hooks, action, when) {
|
|
160
|
-
return hooks.filter((h) => h.when === when && covers(h.on, action));
|
|
86
|
+
function fileOf(args) {
|
|
87
|
+
return String(args.path ?? args.file_path ?? args.to ?? "");
|
|
161
88
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return String(args.path ?? args.to ?? "");
|
|
89
|
+
function said(h) {
|
|
90
|
+
return h.what || h.command;
|
|
165
91
|
}
|
|
166
92
|
/**
|
|
167
|
-
* Run the hooks
|
|
168
|
-
*
|
|
169
|
-
* **A non-zero exit cancels the action.** This is the one place a hook changes
|
|
170
|
-
* what happens, and it is deliberate: "do not let the agent touch anything under
|
|
171
|
-
* generated/" is a real thing to want, and refusing is the only way for a
|
|
172
|
-
* project to say it.
|
|
93
|
+
* Run the hooks the service named, one after another, in its order.
|
|
173
94
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
95
|
+
* Details go in as `COMPRISM_` variables and as one JSON object on standard
|
|
96
|
+
* input. Each hook has its own time limit (30 seconds unless the service said
|
|
97
|
+
* otherwise) and is stopped and reported when it runs over.
|
|
177
98
|
*/
|
|
178
|
-
async function
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
99
|
+
async function runHooks(root, hooks, ctx) {
|
|
100
|
+
const outcome = { runs: [], failed: [] };
|
|
101
|
+
if (!hooks.length || insideHook())
|
|
102
|
+
return outcome;
|
|
103
|
+
const args = ctx.args ?? {};
|
|
104
|
+
const input = JSON.stringify({ when: ctx.when, action: ctx.action ?? null, input: args,
|
|
105
|
+
...(ctx.facts ?? {}) });
|
|
106
|
+
for (const hook of hooks) {
|
|
107
|
+
const secs = Math.max(1, Number(hook.timeout_s ?? 30));
|
|
182
108
|
const result = await (0, shell_1.runCommand)(hook.command, {
|
|
183
109
|
root,
|
|
184
|
-
timeoutMs:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
extraEnv: {
|
|
110
|
+
timeoutMs: secs * 1000,
|
|
111
|
+
input,
|
|
112
|
+
label: `hook: ${hook.command}`,
|
|
113
|
+
extraEnv: {
|
|
114
|
+
[exports.IN_HOOK]: "1",
|
|
115
|
+
COMPRISM_WHEN: ctx.when,
|
|
116
|
+
COMPRISM_ACTION: ctx.action ?? "",
|
|
117
|
+
COMPRISM_FILE: fileOf(args),
|
|
118
|
+
COMPRISM_COMMAND: String(args.command ?? ""),
|
|
119
|
+
COMPRISM_OUTCOME: String(ctx.facts?.outcome ?? ""),
|
|
120
|
+
// The earlier names, so a script written for them keeps working.
|
|
121
|
+
ILLUMINIS_ACTION: ctx.action ?? "",
|
|
122
|
+
ILLUMINIS_PATH: fileOf(args),
|
|
123
|
+
},
|
|
188
124
|
});
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
125
|
+
const timedOut = Boolean(result.facts?.timed_out);
|
|
126
|
+
const ok = !result.isError && /^exit 0/.test(result.content);
|
|
127
|
+
const output = timedOut
|
|
128
|
+
? `stopped after ${secs}s, its time limit`
|
|
129
|
+
: result.content.replace(/^exit -?\d+\s*/, "").trim().slice(0, 2000);
|
|
130
|
+
outcome.runs.push({ hook, ok, timedOut, output });
|
|
131
|
+
if (ok)
|
|
132
|
+
continue;
|
|
133
|
+
if (ctx.when === "before") {
|
|
134
|
+
outcome.refusedBy = hook.command;
|
|
135
|
+
outcome.detail = `${hook.what ?? "A hook in this project"} refused this`
|
|
136
|
+
+ (timedOut ? ` (it was stopped after ${secs}s, its time limit)` : "")
|
|
137
|
+
+ (output && !timedOut ? `: ${output.slice(0, 500)}` : ".");
|
|
138
|
+
return outcome;
|
|
196
139
|
}
|
|
140
|
+
outcome.failed.push(`${said(hook)}: ${timedOut ? `stopped after ${secs}s, its time limit` : output.slice(0, 300) || "failed"}`);
|
|
197
141
|
}
|
|
198
|
-
return
|
|
142
|
+
return outcome;
|
|
199
143
|
}
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
async function after(root, action, args, hooks = hooksFor(root)) {
|
|
209
|
-
const applicable = matching(hooks, action, "after");
|
|
210
|
-
const failed = [];
|
|
211
|
-
let ran = 0;
|
|
212
|
-
for (const hook of applicable) {
|
|
213
|
-
const result = await (0, shell_1.runCommand)(hook.command, {
|
|
214
|
-
root, timeoutMs: 120_000,
|
|
215
|
-
extraEnv: { ILLUMINIS_ACTION: action, ILLUMINIS_PATH: targetOf(args) },
|
|
216
|
-
});
|
|
217
|
-
ran += 1;
|
|
218
|
-
if (result.isError || !/^exit 0/.test(result.content)) {
|
|
219
|
-
failed.push(`${hook.what ?? hook.command}: ${result.content.slice(0, 300)}`);
|
|
220
|
-
}
|
|
144
|
+
/** One line per hook that ran, for the full view and `hooks test` (10.10). */
|
|
145
|
+
function reportLines(outcome) {
|
|
146
|
+
const lines = [];
|
|
147
|
+
for (const run of outcome.runs) {
|
|
148
|
+
const mark = run.ok ? "ok " : run.timedOut ? "time" : "fail";
|
|
149
|
+
lines.push(`${mark} hook ${run.hook.when} ${run.hook.command}`);
|
|
150
|
+
for (const l of run.output.split("\n").filter(Boolean).slice(0, 20))
|
|
151
|
+
lines.push(` ${l}`);
|
|
221
152
|
}
|
|
222
|
-
return
|
|
153
|
+
return lines;
|
|
223
154
|
}
|
|
224
|
-
/** The example a person copies. Kept in code so the readme and the behavior
|
|
225
|
-
* cannot drift: this is what the reader actually gets. */
|
|
226
|
-
exports.EXAMPLE = JSON.stringify({
|
|
227
|
-
hooks: [
|
|
228
|
-
{
|
|
229
|
-
on: "write_file|edit_file|multi_edit",
|
|
230
|
-
when: "after",
|
|
231
|
-
command: "npm run lint --silent",
|
|
232
|
-
what: "Lint after anything is written",
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
on: "edit_file",
|
|
236
|
-
when: "after",
|
|
237
|
-
command: "npx prettier --write $ILLUMINIS_PATH",
|
|
238
|
-
what: "Format the file that was just changed",
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
on: "write_file",
|
|
242
|
-
when: "before",
|
|
243
|
-
command: "test \"${ILLUMINIS_PATH#generated/}\" = \"$ILLUMINIS_PATH\"",
|
|
244
|
-
what: "Nothing under generated/ is edited by hand",
|
|
245
|
-
},
|
|
246
|
-
],
|
|
247
|
-
}, null, 2);
|
package/out/executor/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import * as hooks from "./hooks";
|
|
2
|
+
import { ToolServers } from "./toolservers";
|
|
3
|
+
import { RunResult, guessTestCommand, stopEverything, stopForeground } from "./shell";
|
|
4
|
+
export { stopEverything, stopForeground, guessTestCommand };
|
|
3
5
|
export type { RunResult };
|
|
4
6
|
/** Everything a machine with a shell can do. Sent to the server on hello, and
|
|
5
7
|
* the catalog offered to the model is narrowed to it, so a client is never
|
|
@@ -7,23 +9,37 @@ export type { RunResult };
|
|
|
7
9
|
export declare const NATIVE_CAPABILITIES: string[];
|
|
8
10
|
export interface ExecutorOptions {
|
|
9
11
|
root: string;
|
|
12
|
+
/** Folders the person added for this job (manual 2.3). */
|
|
13
|
+
added?: string[];
|
|
10
14
|
/** Called with each chunk of a running command's output, so a terminal can
|
|
11
15
|
* show it as it happens rather than in one block at the end. */
|
|
12
16
|
onOutput?: (chunk: string) => void;
|
|
13
17
|
}
|
|
14
18
|
export declare class NativeExecutor {
|
|
19
|
+
/** What this machine can do. The browser actions only where a browser is
|
|
20
|
+
* installed (manual 10.16); the service offers them only when switched on. */
|
|
15
21
|
readonly capabilities: string[];
|
|
22
|
+
/** The job's browser, started on first use (10.16). */
|
|
23
|
+
private browser?;
|
|
24
|
+
/** Close what this job opened on the machine: its browser. */
|
|
25
|
+
close(): void;
|
|
16
26
|
readonly root: string;
|
|
17
27
|
private readonly onOutput?;
|
|
18
28
|
/** The test command, once it has been worked out or supplied. Remembered so a
|
|
19
29
|
* job that had to be told does not have to be told again. */
|
|
20
30
|
private testCommand?;
|
|
21
|
-
/** What this project asks to happen around the agent's actions, read once. */
|
|
22
|
-
private hooks?;
|
|
23
31
|
/** Whether the last test run passed. The grading signal the whole product
|
|
24
32
|
* rests on, kept so a session can report it when the job ends. */
|
|
25
33
|
lastTestsPassed?: boolean;
|
|
34
|
+
/** The operating system's sandbox, set when the service turns it on for a
|
|
35
|
+
* job (manual 4.12). Absent, commands run with the person's own access. */
|
|
36
|
+
sandbox?: import("./sandbox").Sandbox;
|
|
26
37
|
constructor(opts: ExecutorOptions);
|
|
27
|
-
|
|
38
|
+
/** One executor per helper copy, rooted there and nowhere else. */
|
|
39
|
+
private readonly inCopies;
|
|
40
|
+
/** Tool servers this job started (manual 9.10), stopped when it ends. */
|
|
41
|
+
readonly toolServers: ToolServers;
|
|
42
|
+
execute(name: string, a: Record<string, unknown>, plan?: hooks.HookPlan): Promise<RunResult>;
|
|
43
|
+
private browse;
|
|
28
44
|
private perform;
|
|
29
45
|
}
|