@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.
Files changed (131) hide show
  1. package/README.md +61 -2
  2. package/out/agent/command.d.ts +75 -4
  3. package/out/agent/command.js +220 -25
  4. package/out/agent/render.d.ts +17 -2
  5. package/out/agent/render.js +157 -14
  6. package/out/agent/session.d.ts +141 -2
  7. package/out/agent/session.js +735 -149
  8. package/out/commands/agents.d.ts +2 -0
  9. package/out/commands/agents.js +79 -0
  10. package/out/commands/ask.d.ts +1 -1
  11. package/out/commands/ask.js +78 -11
  12. package/out/commands/commands-thin.js +79 -17
  13. package/out/commands/config.d.ts +1 -0
  14. package/out/commands/config.js +138 -0
  15. package/out/commands/cost.d.ts +1 -0
  16. package/out/commands/cost.js +167 -0
  17. package/out/commands/hooks.d.ts +1 -0
  18. package/out/commands/hooks.js +83 -0
  19. package/out/commands/install.d.ts +44 -1
  20. package/out/commands/install.js +198 -4
  21. package/out/commands/instructions.d.ts +1 -0
  22. package/out/commands/instructions.js +113 -0
  23. package/out/commands/integrations.d.ts +3 -0
  24. package/out/commands/integrations.js +215 -0
  25. package/out/commands/jobs.d.ts +5 -0
  26. package/out/commands/jobs.js +157 -0
  27. package/out/commands/login.js +188 -36
  28. package/out/commands/memory.d.ts +3 -0
  29. package/out/commands/memory.js +113 -0
  30. package/out/commands/permissions.d.ts +1 -0
  31. package/out/commands/permissions.js +94 -0
  32. package/out/commands/plugins.d.ts +4 -0
  33. package/out/commands/plugins.js +192 -0
  34. package/out/commands/privacy.d.ts +1 -0
  35. package/out/commands/privacy.js +57 -0
  36. package/out/commands/providerKey.d.ts +32 -0
  37. package/out/commands/providerKey.js +108 -0
  38. package/out/commands/repl.d.ts +8 -1
  39. package/out/commands/repl.js +1207 -118
  40. package/out/commands/report.d.ts +39 -0
  41. package/out/commands/report.js +115 -0
  42. package/out/commands/review.d.ts +5 -0
  43. package/out/commands/review.js +223 -0
  44. package/out/commands/sessions.d.ts +23 -0
  45. package/out/commands/sessions.js +115 -0
  46. package/out/commands/settings.d.ts +3 -1
  47. package/out/commands/settings.js +18 -16
  48. package/out/commands/skills.d.ts +21 -0
  49. package/out/commands/skills.js +207 -0
  50. package/out/commands/unattended.d.ts +7 -0
  51. package/out/commands/unattended.js +351 -0
  52. package/out/commands/update.d.ts +1 -0
  53. package/out/commands/update.js +123 -0
  54. package/out/commands/worktrees.d.ts +5 -0
  55. package/out/commands/worktrees.js +186 -0
  56. package/out/executor/browser.d.ts +14 -0
  57. package/out/executor/browser.js +270 -0
  58. package/out/executor/diagnostics.d.ts +2 -0
  59. package/out/executor/diagnostics.js +181 -0
  60. package/out/executor/files.js +270 -40
  61. package/out/executor/git.js +42 -29
  62. package/out/executor/hooks.d.ts +42 -58
  63. package/out/executor/hooks.js +89 -182
  64. package/out/executor/index.d.ts +21 -5
  65. package/out/executor/index.js +160 -14
  66. package/out/executor/paths.d.ts +6 -1
  67. package/out/executor/paths.js +34 -6
  68. package/out/executor/sandbox.d.ts +40 -0
  69. package/out/executor/sandbox.js +299 -0
  70. package/out/executor/shell.d.ts +49 -4
  71. package/out/executor/shell.js +302 -56
  72. package/out/executor/toolservers.d.ts +20 -0
  73. package/out/executor/toolservers.js +189 -0
  74. package/out/executor/worktree.d.ts +9 -0
  75. package/out/executor/worktree.js +119 -0
  76. package/out/graph/read-python.js +2 -1
  77. package/out/lib/attach.d.ts +56 -12
  78. package/out/lib/attach.js +230 -63
  79. package/out/lib/clipboard.d.ts +23 -0
  80. package/out/lib/clipboard.js +182 -0
  81. package/out/lib/commandlist.d.ts +20 -0
  82. package/out/lib/commandlist.js +58 -0
  83. package/out/lib/decision.d.ts +22 -0
  84. package/out/lib/decision.js +50 -0
  85. package/out/lib/fingerprint.d.ts +25 -0
  86. package/out/lib/fingerprint.js +58 -0
  87. package/out/lib/gateway.d.ts +186 -2
  88. package/out/lib/gateway.js +59 -4
  89. package/out/lib/history.d.ts +24 -0
  90. package/out/lib/history.js +137 -0
  91. package/out/lib/ide.d.ts +19 -0
  92. package/out/lib/ide.js +131 -0
  93. package/out/lib/keyboard.d.ts +95 -0
  94. package/out/lib/keyboard.js +383 -0
  95. package/out/lib/machine.d.ts +21 -0
  96. package/out/lib/machine.js +91 -0
  97. package/out/lib/notify.d.ts +4 -0
  98. package/out/lib/notify.js +52 -0
  99. package/out/lib/output.d.ts +48 -0
  100. package/out/lib/output.js +108 -0
  101. package/out/lib/project-ops.d.ts +19 -0
  102. package/out/lib/project-ops.js +146 -0
  103. package/out/lib/project.d.ts +28 -0
  104. package/out/lib/project.js +114 -0
  105. package/out/lib/prompt.js +15 -2
  106. package/out/lib/queue.d.ts +13 -0
  107. package/out/lib/queue.js +116 -0
  108. package/out/lib/readiness.d.ts +19 -0
  109. package/out/lib/readiness.js +170 -1
  110. package/out/lib/self.d.ts +23 -0
  111. package/out/lib/self.js +124 -0
  112. package/out/lib/sessions.d.ts +19 -0
  113. package/out/lib/sessions.js +221 -0
  114. package/out/lib/stdin.d.ts +32 -0
  115. package/out/lib/stdin.js +117 -0
  116. package/out/lib/store.d.ts +40 -0
  117. package/out/lib/store.js +138 -0
  118. package/out/lib/sync.d.ts +18 -0
  119. package/out/lib/sync.js +81 -0
  120. package/out/lib/ui.d.ts +2 -4
  121. package/out/lib/ui.js +31 -25
  122. package/out/lib/voice.js +24 -0
  123. package/out/postinstall.js +42 -17
  124. package/out/providers/anthropic.d.ts +22 -0
  125. package/out/providers/anthropic.js +80 -0
  126. package/out/providers/index.d.ts +8 -0
  127. package/out/providers/index.js +83 -0
  128. package/out/providers/openai.d.ts +11 -0
  129. package/out/providers/openai.js +57 -0
  130. package/out/thin.js +594 -32
  131. package/package.json +9 -49
@@ -26,47 +26,45 @@ exports.gitPush = gitPush;
26
26
  * being reimplemented for git.
27
27
  */
28
28
  const shell_1 = require("./shell");
29
- /** Arguments passed as a list and quoted, never interpolated into a string.
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
- function quote(value) {
35
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
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 (0, shell_1.runCommand)("git status --porcelain=v1 --branch", opts);
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 = ["git", "--no-pager", "diff"];
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("--", quote(opts.path));
55
- return (0, shell_1.runCommand)(parts.join(" "), opts);
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
- "git", "--no-pager", "log", `-n ${limit}`,
61
- // Quoted. Everything here goes through a shell, so an unquoted format string
62
- // is split on its spaces and git reads "%an" as a revision. It fails with
63
- // "ambiguous argument", which reads like a broken repository rather than a
64
- // quoting mistake, and cost a test run to diagnose.
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("--", quote(opts.path));
69
- return (0, shell_1.runCommand)(parts.join(" "), opts);
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 (0, shell_1.runCommand)(`git checkout -b ${quote(name)}`, opts);
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
- const stage = opts.paths?.length
85
- ? `git add -- ${opts.paths.map(quote).join(" ")}`
86
- : "git add -A";
87
- const staged = await (0, shell_1.runCommand)(stage, opts);
88
- if (staged.isError)
89
- return staged;
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 (0, shell_1.runCommand)("git diff --staged --quiet", opts);
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
- return (0, shell_1.runCommand)(`git commit -m ${quote(message + trailer)}`, opts);
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
- const ahead = await (0, shell_1.runCommand)(`git --no-pager log --oneline origin/${quote(branch)}..HEAD 2>/dev/null || git --no-pager log --oneline -5`, opts);
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 flags = opts.createRemote ? "-u" : "";
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
  }
@@ -1,67 +1,51 @@
1
- export interface Hook {
2
- /** Which actions this runs around. Three forms, all common in the market:
3
- *
4
- * "edit_file" exactly that one
5
- * "write_file|edit_file" any in the list
6
- * "*" every one
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
- /** Words a person reads in the timeline when it runs. */
18
- what?: string;
8
+ what?: string | null;
9
+ timeout_s?: number;
10
+ source?: string;
19
11
  }
20
- /** What this project asks for, or nothing.
21
- *
22
- * A malformed file is ignored with a warning rather than failing the job. A
23
- * project whose hook configuration has a trailing comma should not be a project
24
- * where the agent refuses to work, and the person who broke it is not usually
25
- * the person now trying to get something done.
26
- */
27
- export declare function hooksFor(root: string): Hook[];
28
- /** Does this hook's matcher cover this action.
29
- *
30
- * Exported because it is the part with the rules in it, and a rule with no
31
- * test is a rule that drifts.
32
- */
33
- export declare function covers(pattern: string, action: string): boolean;
34
- export interface HookOutcome {
35
- ran: number;
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 that come before one action.
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
- * A failure here is reported and nothing else: the action has already happened,
57
- * and refusing it afterwards is not a thing that can be done. The overwhelming
58
- * case is a formatter, and a formatter that fails is worth knowing about and not
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 after(root: string, action: string, args: Record<string, unknown>, hooks?: Hook[]): Promise<{
62
- ran: number;
63
- failed: string[];
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[];
@@ -33,215 +33,122 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.EXAMPLE = void 0;
37
- exports.hooksFor = hooksFor;
38
- exports.covers = covers;
39
- exports.before = before;
40
- exports.after = after;
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
- * Something of your own, run before or after the agent acts.
43
- *
44
- * Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
45
- * item C18. What Claude Code calls hooks.
46
- *
47
- * ## Why this belongs on the client
48
- *
49
- * A hook is the customer's own command, running in their own project, on their
50
- * own machine. Nothing about it is ours to run: the command is theirs, the
51
- * environment is theirs, and the thing it usually wants to do is reformat a
52
- * file that only exists there.
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
- /** Where a project declares its hooks, in the order they are tried.
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
- /** What this project asks for, or nothing.
100
- *
101
- * A malformed file is ignored with a warning rather than failing the job. A
102
- * project whose hook configuration has a trailing comma should not be a project
103
- * where the agent refuses to work, and the person who broke it is not usually
104
- * the person now trying to get something done.
105
- */
106
- function hooksFor(root) {
107
- for (const name of CONFIGS) {
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
- const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
113
- const hooks = Array.isArray(parsed.hooks) ? parsed.hooks : [];
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 (err) {
124
- process.stderr.write(` ${name} could not be read (${err.message}), so no hooks ran.\n`);
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
- /** Does this hook's matcher cover this action.
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
- /** The path the action is about, for a hook that wants to know. */
163
- function targetOf(args) {
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 that come before one action.
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
- * It can only ever REFUSE. There is no exit code that approves something the
175
- * agent was not already allowed to do, because a permission system inside the
176
- * repository would live exactly where an attacker who got that far already is.
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 before(root, action, args, hooks = hooksFor(root)) {
179
- const applicable = matching(hooks, action, "before");
180
- let ran = 0;
181
- for (const hook of applicable) {
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: 60_000,
185
- // What the hook is about, as environment rather than as arguments, so an
186
- // existing script needs no wrapper to read it.
187
- extraEnv: { ILLUMINIS_ACTION: action, ILLUMINIS_PATH: targetOf(args) },
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
- ran += 1;
190
- if (result.isError || !/^exit 0/.test(result.content)) {
191
- return {
192
- ran,
193
- refusedBy: hook.command,
194
- detail: `${hook.what ?? "A check in this project"} refused this: ${result.content.slice(0, 500)}`,
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 { ran };
142
+ return outcome;
199
143
  }
200
- /**
201
- * Run the hooks that come after one action.
202
- *
203
- * A failure here is reported and nothing else: the action has already happened,
204
- * and refusing it afterwards is not a thing that can be done. The overwhelming
205
- * case is a formatter, and a formatter that fails is worth knowing about and not
206
- * worth losing the work over.
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 { ran, failed };
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);
@@ -1,5 +1,7 @@
1
- import { RunResult, guessTestCommand, stopEverything } from "./shell";
2
- export { stopEverything, guessTestCommand };
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
- execute(name: string, a: Record<string, unknown>): Promise<RunResult>;
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
  }