@agentsfleet/orly 0.10.14 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/.githooks/pre-push +2 -1
  2. package/AGENTS.md +21 -31
  3. package/README.md +65 -29
  4. package/SOUL.md +13 -23
  5. package/audits/agents-md.md +67 -57
  6. package/audits/deinit-pairs.sh +13 -18
  7. package/audits/design-tokens.sh +9 -23
  8. package/audits/doc-read.sh +10 -33
  9. package/audits/doc-reads.ts +41 -0
  10. package/audits/error-codes.sh +18 -25
  11. package/audits/logging-rust.awk +145 -0
  12. package/audits/logging.sh +10 -166
  13. package/audits/msid-ui.sh +45 -18
  14. package/audits/rule-ledger-lib.sh +9 -6
  15. package/audits/rust-error.sh +20 -15
  16. package/audits/scope.sh +88 -0
  17. package/audits/spec-template.sh +2 -2
  18. package/audits/spec-template.ts +1 -1
  19. package/audits/sql-mod.sh +11 -14
  20. package/audits/ufs-strings.awk +164 -0
  21. package/audits/ufs.sh +10 -188
  22. package/core/operating-model.md +8 -8
  23. package/dispatch/edit_rules.md +8 -6
  24. package/dispatch/lib.sh +41 -26
  25. package/dispatch/lifecycle.md +12 -5
  26. package/dispatch/name_architecture.md +8 -3
  27. package/dispatch/verify.md +1 -1
  28. package/dispatch/write_any.md +10 -3
  29. package/dispatch/write_changelog.md +7 -4
  30. package/dispatch/write_go.md +1 -1
  31. package/dispatch/write_http.md +5 -7
  32. package/dispatch/write_python.md +1 -1
  33. package/dispatch/write_rust.md +17 -1
  34. package/dispatch/write_shell.md +1 -1
  35. package/dispatch/write_spec.md +3 -1
  36. package/dispatch/write_sql.md +28 -6
  37. package/dispatch/write_ts_adhere_bun.md +11 -0
  38. package/dispatch/write_zig.md +53 -21
  39. package/docs/CHANGELOG_VOICE.md +11 -2
  40. package/docs/EXECUTE_DOC_READS.md +10 -10
  41. package/docs/HARNESS_VERIFY_OUTPUT.md +6 -3
  42. package/docs/JUDGMENTS.md +153 -0
  43. package/docs/LIFECYCLE_PATTERNS.md +26 -16
  44. package/docs/LOGGING_STANDARD.md +39 -19
  45. package/docs/ORLY_ARCHITECTURE.md +77 -6
  46. package/docs/RELEASE_TEMPLATE.md +21 -5
  47. package/docs/REST_API_DESIGN_GUIDELINES.md +86 -21
  48. package/docs/RULE_ENFORCEMENT.md +6 -6
  49. package/docs/SCHEMA_CONVENTIONS.md +24 -3
  50. package/docs/TEMPLATE.md +4 -4
  51. package/docs/VERIFY_TIERS.md +2 -2
  52. package/docs/greptile-learnings/RULES.md +81 -26
  53. package/llms.txt +37 -34
  54. package/package.json +6 -1
  55. package/packs/language/go/rules.md +1 -1
  56. package/packs/language/python/rules.md +1 -1
  57. package/packs/language/rust/rules.md +17 -1
  58. package/packs/language/shell/rules.md +1 -1
  59. package/registry.json +22 -7
  60. package/schemas/profile.schema.json +48 -5
  61. package/skills/orly-babysit-prs/SKILL.md +87 -34
  62. package/skills/orly-spec-new/SKILL.md +19 -6
  63. package/skills/orly-write-integration-test/SKILL.md +4 -4
  64. package/skills/orly-write-unit-test/SKILL.md +10 -1
  65. package/src/cli.test.ts +2 -2
  66. package/src/cli.ts +19 -75
  67. package/src/cli_help.ts +69 -0
  68. package/src/command_limits.ts +20 -0
  69. package/src/command_process.test.ts +109 -0
  70. package/src/command_process.ts +88 -0
  71. package/src/command_runner.test.ts +88 -0
  72. package/src/command_runner.ts +33 -0
  73. package/src/comprehension_cache.test.ts +123 -0
  74. package/src/config.test.ts +5 -4
  75. package/src/config.ts +26 -164
  76. package/src/config_discovery.ts +159 -0
  77. package/src/config_validation.test.ts +56 -0
  78. package/src/config_validation.ts +25 -0
  79. package/src/criteria.test.ts +1 -1
  80. package/src/criteria.ts +10 -26
  81. package/src/criteria_spec.ts +12 -20
  82. package/src/criteria_support.ts +2 -19
  83. package/src/doc_reads.test.ts +42 -0
  84. package/src/doc_rules.ts +2 -2
  85. package/src/evaluation_resources.test.ts +64 -0
  86. package/src/execution/config.ts +22 -0
  87. package/src/execution/plan.test.ts +65 -0
  88. package/src/execution/plan.ts +51 -0
  89. package/src/gates.ts +9 -6
  90. package/src/gates_spec.test.ts +52 -6
  91. package/src/gates_test_support.ts +9 -7
  92. package/src/install.test.ts +38 -222
  93. package/src/install.ts +117 -308
  94. package/src/install_loaders.test.ts +203 -0
  95. package/src/install_nested.test.ts +2 -2
  96. package/src/install_packs.test.ts +66 -6
  97. package/src/installation/hooks.ts +63 -0
  98. package/src/installation/lock.test.ts +97 -0
  99. package/src/installation/lock.ts +56 -0
  100. package/src/installation/migration.test.ts +212 -0
  101. package/src/installation/migration.ts +65 -0
  102. package/src/installation/plan.ts +96 -0
  103. package/src/installation/transaction.ts +126 -0
  104. package/src/installation_preservation.test.ts +48 -0
  105. package/src/judgments/advice.test.ts +38 -0
  106. package/src/judgments/advice.ts +32 -0
  107. package/src/judgments/command.integration.test.ts +47 -0
  108. package/src/judgments/command.ts +90 -0
  109. package/src/judgments/constants.ts +55 -0
  110. package/src/judgments/evidence.test.ts +48 -0
  111. package/src/judgments/evidence.ts +34 -0
  112. package/src/judgments/files.test.ts +62 -0
  113. package/src/judgments/files.ts +73 -0
  114. package/src/judgments/questions.test.ts +19 -0
  115. package/src/judgments/questions.ts +85 -0
  116. package/src/judgments/replay.test.ts +43 -0
  117. package/src/judgments/replay.ts +66 -0
  118. package/src/judgments/runner.integration.test.ts +165 -0
  119. package/src/judgments/runner.ts +97 -0
  120. package/src/judgments/scan.test.ts +19 -0
  121. package/src/judgments/scan.ts +40 -0
  122. package/src/judgments/syntax.test.ts +115 -0
  123. package/src/judgments/syntax.ts +71 -0
  124. package/src/judgments/syntax_worker.ts +70 -0
  125. package/src/judgments/test_support.ts +60 -0
  126. package/src/judgments/transport.integration.test.ts +63 -0
  127. package/src/judgments/transport.ts +35 -0
  128. package/src/judgments/types.test.ts +43 -0
  129. package/src/judgments/types.ts +66 -0
  130. package/src/judgments/wire.test.ts +48 -0
  131. package/src/judgments/wire.ts +40 -0
  132. package/src/loaders.test.ts +13 -16
  133. package/src/loaders.ts +28 -10
  134. package/src/model.test.ts +20 -3
  135. package/src/model.ts +7 -18
  136. package/src/pack_hygiene.test.ts +42 -1
  137. package/src/pack_hygiene.ts +18 -8
  138. package/src/references.ts +15 -7
  139. package/src/release_inventory.test.ts +80 -0
  140. package/src/render.test.ts +1 -1
  141. package/src/render.ts +3 -8
  142. package/src/required_scanner.test.ts +36 -0
  143. package/src/review_poll.test.ts +158 -0
  144. package/src/scanner_scope.test.ts +202 -0
  145. package/src/setup.test.ts +9 -6
  146. package/src/spec_baseline.test.ts +1 -1
  147. package/src/spec_completion.test.ts +74 -0
  148. package/src/spec_obligations.ts +64 -0
  149. package/src/spec_rehearsal.test.ts +182 -0
  150. package/src/spec_template_io.test.ts +11 -9
  151. package/src/surfaces.test.ts +2 -0
  152. package/src/surfaces.ts +1 -1
  153. package/src/validation.ts +7 -4
  154. package/src/verify.test.ts +18 -2
  155. package/src/verify.ts +7 -3
@@ -0,0 +1,69 @@
1
+ const TELEMETRY_CONFIG_KEY = "telemetry";
2
+ const ANONYMOUS_TELEMETRY_CONFIG = JSON.stringify({ [TELEMETRY_CONFIG_KEY]: "anonymous" });
3
+ const OFF_TELEMETRY_CONFIG = JSON.stringify({ [TELEMETRY_CONFIG_KEY]: "off" });
4
+
5
+ const HELP_TEXT = `orly — prove the boundary; carry the rules
6
+
7
+ Gates (read-only; no PR without every criterion green or a recorded override):
8
+ orly gate [--accept-dirty] run work → verify → pr; stop at first red
9
+ orly gate <work|verify|pr> run one gate
10
+ work does this commit conform? the declared conform command; no git
11
+ state, so a commit hook's own dirty tree never blocks it
12
+ verify does the work hold up? configuration, docs language, and the
13
+ non-test verify.* set; unfinished Sections may be pushed
14
+ pr can this ship? branch, tree, pushed, every spec criterion, and
15
+ every declared verify.* command
16
+ orly override <CRITERION> --reason <REASON>
17
+ empty commit with an Orly-Override trailer
18
+ Commands default to 30 minutes and 64 mebibytes of combined output each.
19
+ Set limits.<command>.timeout_ms and output_bytes in .orly/orly.json to override.
20
+ Exceeding either limit fails the check and prints the saved output path.
21
+
22
+ Advice (explicit; hooks stay offline):
23
+ orly judge <plan|verify|review|document> --input <MANIFEST>
24
+ exact replay of bounded Jev advice
25
+ orly judge --help input selectors, questions and refresh
26
+ orly lifecycle [--json] inspect stages, declared local checks and
27
+ resource ownership; remote adapter disabled
28
+
29
+ Install (the repository is the unit — no checkout of this package required):
30
+ orly init [--force] [--no-hooks] [--with <PACK>] [--dry-run] [--json]
31
+ materialise rules, gates, hooks, and a
32
+ seeded .orly/orly.json. A repository that
33
+ already has an AGENTS.md keeps it: orly's
34
+ rules land as .orly/AGENTS.md, reached by a
35
+ pointer block in the file you own. One
36
+ import line lands per runtime that needs
37
+ one (CLAUDE.md, opencode.json), so the
38
+ rules load rather than wait to be noticed;
39
+ a loader you wrote yourself is left alone.
40
+ orly update [--force] [--with <PACK>] [--dry-run] [--json]
41
+ re-materialise at the installed engine version
42
+
43
+ --with <PACK> record an opt-in pack (repeatable) in
44
+ .orly/orly.json, so every clone selects it
45
+ --dry-run show what would be written; change nothing
46
+
47
+ orly doctor check this repository's installed rules
48
+ against what orly would write today
49
+ orly --version the installed package version
50
+
51
+ Usage telemetry (off by default):
52
+ Consent file: ~/.config/agentsfleet/orly/.orly.json
53
+ State-root override: AGENTSFLEET_STATE_DIR
54
+ Set ${ANONYMOUS_TELEMETRY_CONFIG} to send command, gate, and packaged Orly skill
55
+ names, outcome, failed criterion, duration, Orly version, operating system, architecture,
56
+ invocation type, timestamps, and random event, session, and installation IDs.
57
+ Set ${OFF_TELEMETRY_CONFIG} to write and send nothing.
58
+ ORLY_TELEMETRY=off|anonymous changes one process; ORLY_TELEMETRY_OFF=1 wins.
59
+ Orly never collects source, paths, repositories, branches, arguments, prompts,
60
+ output, environment values, raw errors, or personal and account details.
61
+
62
+ Ruleset authoring (an orly checkout only — refused from an installed package):
63
+ orly verify the rules render the same twice, and the
64
+ committed copy matches that render`;
65
+
66
+ export function printHelp(): void {
67
+ // logging: command help is the program interface.
68
+ console.log(HELP_TEXT);
69
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+
3
+ export const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60 * 1_000;
4
+ export const DEFAULT_COMMAND_OUTPUT_BYTES = 64 * 1024 * 1024;
5
+ export const SUPERVISOR_GRACE_MS = 5000;
6
+ const MAX_COMMAND_TIMEOUT_MS = 2 ** 31 - 1 - SUPERVISOR_GRACE_MS;
7
+ export const DEFAULT_COMMAND_LIMITS = Object.freeze({ timeout_ms: DEFAULT_COMMAND_TIMEOUT_MS, output_bytes: DEFAULT_COMMAND_OUTPUT_BYTES });
8
+ export const commandLimitsSchema = z.object({
9
+ timeout_ms: z.number().int().positive().max(MAX_COMMAND_TIMEOUT_MS).optional(),
10
+ output_bytes: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional(),
11
+ }).strict();
12
+ export type CommandLimits = typeof DEFAULT_COMMAND_LIMITS;
13
+ export type LaneLimits = Record<string, z.infer<typeof commandLimitsSchema>>;
14
+
15
+ export function limitsFor(limits: LaneLimits | undefined, lane: string): CommandLimits {
16
+ return {
17
+ timeout_ms: limits?.[lane]?.timeout_ms ?? DEFAULT_COMMAND_TIMEOUT_MS,
18
+ output_bytes: limits?.[lane]?.output_bytes ?? DEFAULT_COMMAND_OUTPUT_BYTES,
19
+ };
20
+ }
@@ -0,0 +1,109 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import { cleanupTemporaryDirectories, ROOT, temporaryDirectory } from "./gates_test_support";
6
+ import { UNSCOPED_ENVIRONMENT } from "./git_env";
7
+
8
+ const PROCESS_SOURCE = "src/command_process.ts";
9
+ const RESULT_FILE = "result.json";
10
+ const ENCODING = "utf8";
11
+ const TEST_TIMEOUT_MS = 5_000;
12
+ const WORKLOAD_TIMEOUT_MS = 10_000;
13
+ const CHILD_START_POLL_MS = 10;
14
+ const CHILD_START_ATTEMPTS = 200;
15
+ const REPEATED_STOP_REFUSAL = `const initialKill = process.kill;
16
+ const stoppedGroups = new Set<number>();
17
+ process.kill = ((pid: number, signal?: NodeJS.Signals | number) => {
18
+ if (pid < 0 && signal === "SIGKILL") {
19
+ if (stoppedGroups.has(pid)) throw Object.assign(new Error("group already stopped"), { code: "EPERM" });
20
+ stoppedGroups.add(pid);
21
+ }
22
+ return initialKill(pid, signal);
23
+ }) as typeof process.kill;
24
+ `;
25
+
26
+ afterEach(cleanupTemporaryDirectories);
27
+
28
+ test("a failed pid receipt write stops the already started workload", () => {
29
+ const root = temporaryDirectory();
30
+ const pidFile = join(root, "owned.pid");
31
+ const script = join(root, "command_process.ts");
32
+ const source = readFileSync(join(ROOT, PROCESS_SOURCE), ENCODING);
33
+ const waitForChild = `for (let attempt = 0; attempt < ${CHILD_START_ATTEMPTS} && !await Bun.file(${JSON.stringify(pidFile)}).exists(); attempt++) await Bun.sleep(${CHILD_START_POLL_MS});`;
34
+ writeFileSync(script, source.replace("writeFileSync(join(request.receipt, COMMAND_FILES.pid),", `${waitForChild}\n writeFileSync(join(request.receipt, COMMAND_FILES.pid),`));
35
+ mkdirSync(join(root, "child.pid"));
36
+ const request = join(root, "request.json");
37
+ writeFileSync(request, JSON.stringify({ command: ["sh", "-c", `echo $$ > '${pidFile}'; exec sleep 10`], limits: { timeout_ms: WORKLOAD_TIMEOUT_MS, output_bytes: 4096 }, receipt: root, parent_pid: process.pid }));
38
+ let pid: number | undefined;
39
+ try {
40
+ const supervisor = Bun.spawnSync([process.execPath, script, request], { env: UNSCOPED_ENVIRONMENT, stdout: "pipe", stderr: "pipe", timeout: TEST_TIMEOUT_MS });
41
+ expect(supervisor.exitCode).not.toBe(0);
42
+ expect(supervisor.stderr.toString()).toContain("EISDIR");
43
+ expect(existsSync(pidFile)).toBeTrue();
44
+ const workloadPid = Number(readFileSync(pidFile, ENCODING));
45
+ pid = workloadPid;
46
+ expect(() => process.kill(workloadPid, 0)).toThrow();
47
+ } finally {
48
+ if (pid) { try { process.kill(-pid, "SIGKILL"); } catch { /* owned group already stopped */ } }
49
+ }
50
+ });
51
+
52
+ test("a successful deadline stop is not repeated during supervisor cleanup", () => {
53
+ const root = temporaryDirectory();
54
+ const script = join(root, "command_process.ts");
55
+ writeFileSync(script, REPEATED_STOP_REFUSAL + readFileSync(join(ROOT, PROCESS_SOURCE), ENCODING));
56
+ const request = join(root, "request.json");
57
+ writeFileSync(request, JSON.stringify({ command: ["sh", "-c", "echo measured; sleep 3"], limits: { timeout_ms: 100, output_bytes: 4096 }, receipt: root, parent_pid: process.pid }));
58
+ const child = Bun.spawnSync([process.execPath, script, request], { env: UNSCOPED_ENVIRONMENT, stdout: "pipe", stderr: "pipe", timeout: TEST_TIMEOUT_MS });
59
+ expect(child.exitCode, child.stderr.toString()).toBe(0);
60
+ expect(child.stderr.toString()).toBe("");
61
+ expect(JSON.parse(readFileSync(join(root, RESULT_FILE), ENCODING)).failure).toBe("deadline exceeded");
62
+ expect(readFileSync(join(root, "stdout.log"), ENCODING)).toBe("measured\n");
63
+ });
64
+
65
+ test("killing the gate owner stops its detached workload", async () => {
66
+ const root = temporaryDirectory();
67
+ const pidFile = join(root, "owned.pid");
68
+ const wrapper = join(root, "gate.ts");
69
+ writeFileSync(wrapper, `import { runCommand } from ${JSON.stringify(join(ROOT, "src/command_runner.ts"))}; runCommand(${JSON.stringify(root)}, ["sh", "-c", "echo $$ > owned.pid; sleep 10"], {timeout_ms: ${WORKLOAD_TIMEOUT_MS}, output_bytes: 4096});`);
70
+ const gate = Bun.spawn([process.execPath, wrapper], { cwd: root, env: { ...UNSCOPED_ENVIRONMENT, TMPDIR: root }, stdout: "pipe", stderr: "pipe" });
71
+ let pid: number | undefined;
72
+ try {
73
+ for (let attempt = 0; attempt < 200 && !existsSync(pidFile); attempt++) await Bun.sleep(10);
74
+ expect(existsSync(pidFile)).toBeTrue();
75
+ pid = Number(readFileSync(pidFile, ENCODING));
76
+ gate.kill("SIGKILL");
77
+ await gate.exited;
78
+ let alive = true;
79
+ for (let attempt = 0; attempt < 100 && alive; attempt++) {
80
+ await Bun.sleep(10);
81
+ try { process.kill(pid, 0); } catch { alive = false; }
82
+ }
83
+ expect(alive).toBeFalse();
84
+ } finally {
85
+ gate.kill("SIGKILL");
86
+ await gate.exited;
87
+ if (pid) { try { process.kill(-pid, "SIGKILL"); } catch { /* owned group already stopped */ } }
88
+ }
89
+ }, TEST_TIMEOUT_MS);
90
+
91
+ test.each(["SIGTERM", "SIGINT", "SIGHUP"] as const)("supervisor %s stops its owned workload", async (signal) => {
92
+ const root = temporaryDirectory();
93
+ const request = join(root, "request.json");
94
+ writeFileSync(request, JSON.stringify({ command: ["sh", "-c", "echo ready; sleep 10"], limits: { timeout_ms: WORKLOAD_TIMEOUT_MS, output_bytes: 4096 }, receipt: root, parent_pid: process.pid }));
95
+ const supervisor = Bun.spawn([process.execPath, join(ROOT, PROCESS_SOURCE), request], { env: UNSCOPED_ENVIRONMENT, stdout: "pipe", stderr: "pipe" });
96
+ try {
97
+ for (let attempt = 0; attempt < 200 && !existsSync(join(root, "child.pid")); attempt++) await Bun.sleep(10);
98
+ expect(existsSync(join(root, "child.pid"))).toBeTrue();
99
+ await Bun.sleep(20);
100
+ supervisor.kill(signal);
101
+ expect(await supervisor.exited).toBe(0);
102
+ expect(JSON.parse(readFileSync(join(root, RESULT_FILE), ENCODING)).failure).toBe(`supervisor received ${signal}`);
103
+ const pid = Number(readFileSync(join(root, "child.pid"), ENCODING));
104
+ expect(() => process.kill(pid, 0)).toThrow();
105
+ } finally {
106
+ supervisor.kill("SIGKILL");
107
+ await supervisor.exited;
108
+ }
109
+ }, TEST_TIMEOUT_MS);
@@ -0,0 +1,88 @@
1
+ import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { CommandLimits } from "./command_limits";
5
+
6
+ const PRIVATE_FILE_MODE = 0o600;
7
+ const TEXT_ENCODING = "utf8";
8
+ const KILL_SIGNAL = "SIGKILL";
9
+ const MISSING_PROCESS = "ESRCH";
10
+ const PIPE_OUTPUT = "pipe";
11
+ const OWNER_POLL_MS = 100;
12
+ const TERMINATION_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"] as const;
13
+ export const COMMAND_FILES = {
14
+ result: "result.json", pid: "child.pid", stdout: "stdout.log", stderr: "stderr.log",
15
+ } as const;
16
+
17
+ type Request = { command: string[]; limits: CommandLimits; receipt: string; parent_pid: number };
18
+ export type CommandResult = { exit_code: number | null; elapsed_ms: number; output_bytes: number; failure?: string };
19
+
20
+ export function stopOwnedGroup(pid: number): void {
21
+ if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("invalid owned process group");
22
+ try { process.kill(-pid, KILL_SIGNAL); }
23
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== MISSING_PROCESS) throw error; }
24
+ }
25
+
26
+ async function capture(stream: ReadableStream<Uint8Array>, file: string, onBytes: (size: number) => void): Promise<void> {
27
+ for await (const chunk of stream) {
28
+ appendFileSync(file, chunk);
29
+ onBytes(chunk.byteLength);
30
+ }
31
+ }
32
+
33
+ async function run(request: Request): Promise<CommandResult> {
34
+ const started = performance.now();
35
+ const stdout = join(request.receipt, COMMAND_FILES.stdout);
36
+ const stderr = join(request.receipt, COMMAND_FILES.stderr);
37
+ for (const file of [stdout, stderr]) writeFileSync(file, "", { mode: PRIVATE_FILE_MODE });
38
+ let child;
39
+ try {
40
+ child = Bun.spawn(request.command, { detached: true, stdin: "ignore", stdout: PIPE_OUTPUT, stderr: PIPE_OUTPUT });
41
+ } catch (error) {
42
+ return { exit_code: null, elapsed_ms: Math.ceil(performance.now() - started), output_bytes: 0, failure: `${request.command[0] ?? "command"} could not be run: ${error instanceof Error ? error.message : String(error)}` };
43
+ }
44
+ let stopped = false;
45
+ try {
46
+ writeFileSync(join(request.receipt, COMMAND_FILES.pid), String(child.pid), { mode: PRIVATE_FILE_MODE });
47
+ let outputBytes = 0;
48
+ let failure: string | undefined;
49
+ const stop = (reason: string) => {
50
+ failure ??= reason;
51
+ if (stopped) return;
52
+ stopOwnedGroup(child.pid);
53
+ stopped = true;
54
+ };
55
+ const timer = setTimeout(() => stop("deadline exceeded"), request.limits.timeout_ms);
56
+ const owner = setInterval(() => {
57
+ try { process.kill(request.parent_pid, 0); }
58
+ catch (error) { if ((error as NodeJS.ErrnoException).code === MISSING_PROCESS) stop("gate owner exited"); }
59
+ }, OWNER_POLL_MS);
60
+ const signals = TERMINATION_SIGNALS.map((signal) => {
61
+ const handler = () => stop(`supervisor received ${signal}`);
62
+ process.on(signal, handler);
63
+ return { signal, handler };
64
+ });
65
+ try {
66
+ const onBytes = (size: number) => {
67
+ outputBytes += size;
68
+ if (outputBytes > request.limits.output_bytes) stop("output limit exceeded");
69
+ };
70
+ await Promise.all([capture(child.stdout, stdout, onBytes), capture(child.stderr, stderr, onBytes), child.exited]);
71
+ if (child.signalCode) failure ??= `stopped by ${child.signalCode}`;
72
+ return { exit_code: child.exitCode, elapsed_ms: Math.ceil(performance.now() - started), output_bytes: outputBytes, ...(failure ? { failure } : {}) };
73
+ } finally {
74
+ clearTimeout(timer);
75
+ clearInterval(owner);
76
+ for (const { signal, handler } of signals) process.off(signal, handler);
77
+ }
78
+ } finally {
79
+ if (!stopped) stopOwnedGroup(child.pid);
80
+ await child.exited;
81
+ }
82
+ }
83
+
84
+ if (import.meta.main) {
85
+ const request: Request = JSON.parse(readFileSync(Bun.argv[2]!, TEXT_ENCODING));
86
+ const result = await run(request);
87
+ writeFileSync(join(request.receipt, COMMAND_FILES.result), JSON.stringify(result), { mode: PRIVATE_FILE_MODE });
88
+ }
@@ -0,0 +1,88 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { runCommand } from "./command_runner";
7
+ import { criteriaFor } from "./criteria";
8
+ import { CONFIG_PATH } from "./config";
9
+ import { cleanupTemporaryDirectories, modelFor, newRepository } from "./gates_test_support";
10
+
11
+ const PRIVATE_DIRECTORY_MODE = 0o700;
12
+ const PRIVATE_FILE_MODE = 0o600;
13
+ const PERMISSION_MASK = 0o777;
14
+ const CHILD_FILE = "child.pid";
15
+ const UNIT_LANE = "verify.unit";
16
+ const roots: string[] = [];
17
+
18
+ afterEach(() => {
19
+ cleanupTemporaryDirectories();
20
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
21
+ });
22
+
23
+ function scratch(): string {
24
+ const root = mkdtempSync(join(tmpdir(), "orly-command-test-"));
25
+ roots.push(root);
26
+ return root;
27
+ }
28
+
29
+ function receipt(detail: string): string {
30
+ const path = detail.split("complete captured output: ")[1];
31
+ expect(path).toBeDefined();
32
+ roots.push(path!);
33
+ expect(statSync(path!).mode & PERMISSION_MASK).toBe(PRIVATE_DIRECTORY_MODE);
34
+ for (const file of ["stdout.log", "stderr.log"]) expect(statSync(join(path!, file)).mode & PERMISSION_MASK).toBe(PRIVATE_FILE_MODE);
35
+ return path!;
36
+ }
37
+
38
+ async function expectChildStopped(root: string): Promise<void> {
39
+ expect(existsSync(join(root, CHILD_FILE))).toBeTrue();
40
+ const pid = Number(readFileSync(join(root, CHILD_FILE), "utf8").trim());
41
+ for (let attempt = 0; attempt < 100; attempt++) {
42
+ try { process.kill(pid, 0); } catch { return; }
43
+ await Bun.sleep(10);
44
+ }
45
+ throw new Error(`owned child ${pid} survived the command limit`);
46
+ }
47
+
48
+ test("a deadline stops the owned child group and keeps captured output", async () => {
49
+ const root = scratch();
50
+ const result = runCommand(root, ["sh", "-c", `sleep 30 & echo $! > ${CHILD_FILE}; echo started; echo detail >&2; wait`], { timeout_ms: 300, output_bytes: 4096 });
51
+ expect(result.ok).toBeFalse();
52
+ expect(result.detail).toContain("deadline exceeded");
53
+ expect(result.detail).toContain("300 ms deadline");
54
+ const path = receipt(result.detail);
55
+ expect(readFileSync(join(path, "stdout.log"), "utf8")).toBe("started\n");
56
+ expect(readFileSync(join(path, "stderr.log"), "utf8")).toBe("detail\n");
57
+ await expectChildStopped(root);
58
+ });
59
+
60
+ test("the output budget covers both streams and stops the owned child group", async () => {
61
+ const root = scratch();
62
+ const chunk = "123456789012345678901234";
63
+ const started = performance.now();
64
+ const result = runCommand(root, ["sh", "-c", `sleep 30 & echo $! > ${CHILD_FILE}; printf ${chunk}; printf ${chunk} >&2; wait`], { timeout_ms: 3000, output_bytes: 32 });
65
+ expect(performance.now() - started).toBeLessThan(1500);
66
+ expect(result.ok).toBeFalse();
67
+ expect(result.detail).toContain("output limit exceeded");
68
+ expect(result.detail).toContain("48 bytes / 32 bytes");
69
+ const path = receipt(result.detail);
70
+ expect(readFileSync(join(path, "stdout.log"), "utf8")).toBe(chunk);
71
+ expect(readFileSync(join(path, "stderr.log"), "utf8")).toBe(chunk);
72
+ await expectChildStopped(root);
73
+ });
74
+
75
+ test("an explicit lane override applies through the public gate criteria", async () => {
76
+ const root = newRepository();
77
+ const model = await modelFor(root);
78
+ writeFileSync(join(root, CONFIG_PATH), JSON.stringify({
79
+ schema_version: 1, commands: { conform: [["true"]], [UNIT_LANE]: [["sh", "-c", "echo measured; sleep 3"]] },
80
+ limits: { [UNIT_LANE]: { timeout_ms: 200, output_bytes: 4096 } },
81
+ }));
82
+ const context = { root, model, acceptDirty: false };
83
+ const check = criteriaFor("pr", context).find((entry) => entry.name === `cmd.${UNIT_LANE}`)!;
84
+ const result = check.evaluate(context);
85
+ expect(result.ok).toBeFalse();
86
+ expect(result.detail).toContain("200 ms deadline");
87
+ expect(readFileSync(join(receipt(result.detail), "stdout.log"), "utf8")).toBe("measured\n");
88
+ });
@@ -0,0 +1,33 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { DEFAULT_COMMAND_LIMITS, SUPERVISOR_GRACE_MS, type CommandLimits } from "./command_limits";
6
+ import { UNSCOPED_ENVIRONMENT } from "./git_env";
7
+ import { COMMAND_FILES, stopOwnedGroup, type CommandResult } from "./command_process";
8
+ import type { Verdict } from "./criteria_support";
9
+
10
+ const PRIVATE_FILE_MODE = 0o600;
11
+ const TEXT_ENCODING = "utf8";
12
+ const PIPE_OUTPUT = "pipe";
13
+ const RECEIPT_PREFIX = "orly-command-";
14
+
15
+ export function runCommand(root: string, command: string[], limits: CommandLimits = DEFAULT_COMMAND_LIMITS): Verdict {
16
+ const receipt = mkdtempSync(join(tmpdir(), RECEIPT_PREFIX));
17
+ const request = join(receipt, "request.json");
18
+ writeFileSync(request, JSON.stringify({ command, limits, receipt, parent_pid: process.pid }), { mode: PRIVATE_FILE_MODE });
19
+ const supervisor = Bun.spawnSync([process.execPath, join(import.meta.dir, "command_process.ts"), request], {
20
+ cwd: root, env: UNSCOPED_ENVIRONMENT, stdout: PIPE_OUTPUT, stderr: PIPE_OUTPUT,
21
+ detached: true, timeout: limits.timeout_ms + SUPERVISOR_GRACE_MS, killSignal: "SIGKILL",
22
+ });
23
+ if (supervisor.exitCode !== 0) {
24
+ stopOwnedGroup(supervisor.pid);
25
+ try { stopOwnedGroup(Number(readFileSync(join(receipt, COMMAND_FILES.pid), TEXT_ENCODING))); } catch { /* child may not have started */ }
26
+ return { ok: false, detail: `command supervisor failed (${supervisor.signalCode ?? supervisor.exitCode}); complete captured output: ${receipt}; ${supervisor.stderr.toString().trim()}` };
27
+ }
28
+ const result: CommandResult = JSON.parse(readFileSync(join(receipt, COMMAND_FILES.result), TEXT_ENCODING));
29
+ if (result.failure) return { ok: false, detail: `${result.failure}; ${result.elapsed_ms} ms / ${limits.timeout_ms} ms deadline; ${result.output_bytes} bytes / ${limits.output_bytes} bytes output budget; complete captured output: ${receipt}` };
30
+ const output = [COMMAND_FILES.stdout, COMMAND_FILES.stderr].map((file) => readFileSync(join(receipt, file), TEXT_ENCODING)).filter(Boolean).join("\n").trim();
31
+ rmSync(receipt, { recursive: true, force: true });
32
+ return { ok: result.exit_code === 0, detail: output ? `exit ${result.exit_code}:\n${output}` : result.exit_code === 0 ? "exit 0" : `exit ${result.exit_code}: no output` };
33
+ }
@@ -0,0 +1,123 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { copyFileSync, mkdirSync, readFileSync, readdirSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import { cleanupTemporaryDirectories, newRepository, ROOT } from "./gates_test_support";
6
+ import { UNSCOPED_ENVIRONMENT } from "./git_env";
7
+
8
+ const SCRIPT = "evals/llms/run.sh";
9
+ const AGENT_NAMES = ["claude", "codex", "amp", "opencode"];
10
+ const CALLS = "calls.txt";
11
+ const AGENTS = "AGENTS.md";
12
+ const FACADE = "dispatch/write_fixture.md";
13
+ const PIPE_OUTPUT = "pipe";
14
+ const TEXT_ENCODING = "utf8";
15
+ const CASE_TIMEOUT_MS = 30_000;
16
+
17
+ afterEach(cleanupTemporaryDirectories);
18
+
19
+ function fixture(missingHost = false) {
20
+ const root = newRepository();
21
+ for (const file of [SCRIPT, "evals/llms/agents.sh", "evals/llms/fixtures.sh", "evals/llms/cache.ts", "skills/orly-write-unit-test/SKILL.md", "skills/orly-write-integration-test/SKILL.md"]) {
22
+ mkdirSync(join(root, file, ".."), { recursive: true });
23
+ copyFileSync(join(ROOT, file), join(root, file));
24
+ }
25
+ mkdirSync(join(root, "dispatch"));
26
+ writeFileSync(join(root, AGENTS), "# Deterministic rules\n");
27
+ writeFileSync(join(root, FACADE), "Require the declared check.\n");
28
+ writeFileSync(join(root, "evals/llms/fixtures.jsonl"), JSON.stringify({ id: "one", mode: "control", expect: "YES", q: "Does the rule require its check?", why: "Local adapter control.", ctx: [FACADE] }) + "\n");
29
+ const tools = join(root, "tools");
30
+ mkdirSync(tools);
31
+ for (const name of ["bash", "sh", "bun", "python3", "timeout", "dirname", "cat", "mktemp", "rm", "ln", "chmod", "cut", "grep", "tr", "sed", "awk", "mkdir", "basename", "git"]) {
32
+ const target = Bun.which(name);
33
+ if (target) symlinkSync(target, join(tools, name));
34
+ }
35
+ for (const host of AGENT_NAMES.filter((name) => !missingHost || name !== "opencode")) {
36
+ writeFileSync(join(tools, host), `#!/bin/sh
37
+ printf '%s\n' '${host}' >> "$ORLY_STUB_CALLS"
38
+ answer=YES
39
+ if [ '${host}' = opencode ] && [ "\${ORLY_STUB_FAIL:-0}" = 1 ]; then answer=NO; fi
40
+ if [ '${host}' = codex ]; then
41
+ cat >/dev/null
42
+ while [ "$#" -gt 0 ]; do
43
+ if [ "$1" = --output-last-message ]; then shift; printf 'VERDICT: %s\n' "$answer" > "$1"; break; fi
44
+ shift
45
+ done
46
+ else
47
+ if [ '${host}' != opencode ]; then cat >/dev/null; fi
48
+ printf 'VERDICT: %s\n' "$answer"
49
+ fi
50
+ `, { mode: 0o755 });
51
+ }
52
+ const env = { ...UNSCOPED_ENVIRONMENT, PATH: tools, ORLY_STUB_CALLS: join(root, CALLS) };
53
+ return { root, env };
54
+ }
55
+
56
+ function run(f: ReturnType<typeof fixture>, args: string[] = [], fail = true) {
57
+ const result = Bun.spawnSync([Bun.which("bash")!, join(f.root, SCRIPT), ...args], {
58
+ cwd: f.root, env: { ...f.env, ORLY_STUB_FAIL: fail ? "1" : "0" }, stdout: PIPE_OUTPUT, stderr: PIPE_OUTPUT, timeout: 30_000,
59
+ });
60
+ return { code: result.exitCode, output: result.stdout.toString() + result.stderr.toString() };
61
+ }
62
+
63
+ function calls(f: ReturnType<typeof fixture>): number { return readFileSync(join(f.root, CALLS), TEXT_ENCODING).trim().split("\n").length; }
64
+
65
+ test("changed prompt bytes invalidate saved results without a new commit", () => {
66
+ const f = fixture();
67
+ const first = run(f);
68
+ expect(first.code, first.output).toBe(1);
69
+ expect(calls(f)).toBe(4);
70
+ expect(run(f).output).toContain("resumed from journal");
71
+ expect(calls(f)).toBe(4);
72
+ writeFileSync(join(f.root, AGENTS), "# Changed deterministic rule\n");
73
+ const changed = run(f);
74
+ expect(changed.code).toBe(1);
75
+ expect(changed.output).not.toContain("resumed from journal");
76
+ expect(calls(f)).toBe(8);
77
+ writeFileSync(join(f.root, FACADE), "A changed check remains required.\n");
78
+ expect(run(f).output).not.toContain("resumed from journal");
79
+ expect(calls(f)).toBe(12);
80
+ }, CASE_TIMEOUT_MS);
81
+
82
+ test("host selection and adapter settings cannot reuse incompatible saved proof", () => {
83
+ const f = fixture();
84
+ run(f);
85
+ const partial = run(f, ["--agent", "claude"]);
86
+ expect(partial.code).toBe(0);
87
+ expect(partial.output).toContain("partial comprehension coverage");
88
+ expect(partial.output).not.toContain("live comprehension passed");
89
+ expect(calls(f)).toBe(5);
90
+ mkdirSync(join(f.root, ".claude"));
91
+ writeFileSync(join(f.root, ".claude/settings.json"), JSON.stringify({ model: "changed-local-control" }));
92
+ expect(run(f).output).not.toContain("resumed from journal");
93
+ expect(calls(f)).toBe(9);
94
+ }, CASE_TIMEOUT_MS);
95
+
96
+ test("malformed journal counts are discarded instead of becoming a pass", () => {
97
+ const f = fixture();
98
+ run(f);
99
+ const journalRoot = join(f.root, ".llmevals-journal");
100
+ const runDirectory = join(journalRoot, readdirSync(journalRoot)[0]!);
101
+ const path = join(runDirectory, "claude");
102
+ const saved = JSON.parse(readFileSync(path, TEXT_ENCODING));
103
+ writeFileSync(path, JSON.stringify({ ...saved, status: "PASS", correct: 0, total: 0 }));
104
+ expect(run(f).code).toBe(1);
105
+ expect(calls(f)).toBe(5);
106
+ }, CASE_TIMEOUT_MS);
107
+
108
+ test("a missing required host keeps full coverage incomplete", () => {
109
+ const f = fixture(true);
110
+ const result = run(f, [], false);
111
+ expect(result.code).toBe(1);
112
+ expect(result.output).toContain("requested agent absent: opencode");
113
+ expect(result.output).not.toContain("live comprehension passed");
114
+ expect(calls(f)).toBe(3);
115
+ }, CASE_TIMEOUT_MS);
116
+
117
+ test("full completion requires every selected fixture on all required local stub hosts", () => {
118
+ const f = fixture();
119
+ const result = run(f, [], false);
120
+ expect(result.code).toBe(0);
121
+ expect(result.output).toContain("graded=4/4");
122
+ expect(calls(f)).toBe(4);
123
+ }, CASE_TIMEOUT_MS);
@@ -3,7 +3,8 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:
3
3
  import { tmpdir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
5
 
6
- import { CONFIG_PATH, contentDigest, managedDrift, readConfig, readConfigSync, seedConfig, selectPacks, staleVersion, writeConfig } from "./config";
6
+ import { selectPacks } from "./config_discovery";
7
+ import { CONFIG_PATH, contentDigest, managedDrift, readConfig, readConfigSync, seedConfig, staleVersion, writeConfig } from "./config";
7
8
  import { RulesModel } from "./model";
8
9
 
9
10
  const ROOT = resolve(import.meta.dir, "..");
@@ -20,7 +21,7 @@ function scratch(): string {
20
21
  const temporary: string[] = [];
21
22
 
22
23
  function writeConfigText(root: string, body: unknown): void {
23
- mkdirSync(join(root, ".oracle"), { recursive: true });
24
+ mkdirSync(join(root, ".orly"), { recursive: true });
24
25
  writeFileSync(join(root, CONFIG_PATH), JSON.stringify(body));
25
26
  }
26
27
 
@@ -236,7 +237,7 @@ describe("managedDrift", () => {
236
237
  });
237
238
 
238
239
  // The failure this pair exists for: agentsfleet ignored .claude/skills/ while
239
- // .oracle/orly.json declared four files under it managed. Every local
240
+ // .orly/orly.json declared four files under it managed. Every local
240
241
  // checkout passed `orly doctor` because the files sat on disk, and CI failed
241
242
  // against a clone that never received them.
242
243
  test("a managed file git refuses to track is drift, even though it is present", async () => {
@@ -294,7 +295,7 @@ describe("writeConfig", () => {
294
295
  expect((await readConfig(root))?.managed).toEqual(["aaa.md", "zzz.md"]);
295
296
  });
296
297
 
297
- test("writeConfig creates the .oracle/ parent directory", async () => {
298
+ test("writeConfig creates the .orly/ parent directory", async () => {
298
299
  const root = scratch();
299
300
  await writeConfig(root, await seedConfig(root));
300
301
  expect(existsSync(join(root, CONFIG_PATH))).toBeTrue();