@agentsfleet/orly 0.10.14 → 0.12.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 (156) hide show
  1. package/.githooks/pre-push +2 -1
  2. package/AGENTS.md +21 -31
  3. package/README.md +94 -28
  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.md +10 -0
  40. package/docs/CHANGELOG_VOICE.md +11 -2
  41. package/docs/EXECUTE_DOC_READS.md +10 -10
  42. package/docs/HARNESS_VERIFY_OUTPUT.md +6 -3
  43. package/docs/JUDGMENTS.md +156 -0
  44. package/docs/LIFECYCLE_PATTERNS.md +26 -16
  45. package/docs/LOGGING_STANDARD.md +39 -19
  46. package/docs/ORLY_ARCHITECTURE.md +77 -6
  47. package/docs/RELEASE_TEMPLATE.md +21 -5
  48. package/docs/REST_API_DESIGN_GUIDELINES.md +86 -21
  49. package/docs/RULE_ENFORCEMENT.md +6 -6
  50. package/docs/SCHEMA_CONVENTIONS.md +24 -3
  51. package/docs/TEMPLATE.md +4 -4
  52. package/docs/VERIFY_TIERS.md +2 -2
  53. package/docs/greptile-learnings/RULES.md +81 -26
  54. package/llms.txt +46 -34
  55. package/package.json +6 -1
  56. package/packs/language/go/rules.md +1 -1
  57. package/packs/language/python/rules.md +1 -1
  58. package/packs/language/rust/rules.md +17 -1
  59. package/packs/language/shell/rules.md +1 -1
  60. package/registry.json +22 -7
  61. package/schemas/profile.schema.json +48 -5
  62. package/skills/orly-babysit-prs/SKILL.md +87 -34
  63. package/skills/orly-spec-new/SKILL.md +19 -6
  64. package/skills/orly-write-integration-test/SKILL.md +4 -4
  65. package/skills/orly-write-unit-test/SKILL.md +10 -1
  66. package/src/cli.test.ts +2 -2
  67. package/src/cli.ts +19 -75
  68. package/src/cli_help.ts +69 -0
  69. package/src/command_limits.ts +20 -0
  70. package/src/command_process.test.ts +109 -0
  71. package/src/command_process.ts +88 -0
  72. package/src/command_runner.test.ts +88 -0
  73. package/src/command_runner.ts +33 -0
  74. package/src/comprehension_cache.test.ts +123 -0
  75. package/src/config.test.ts +5 -4
  76. package/src/config.ts +26 -164
  77. package/src/config_discovery.ts +159 -0
  78. package/src/config_validation.test.ts +56 -0
  79. package/src/config_validation.ts +25 -0
  80. package/src/criteria.test.ts +1 -1
  81. package/src/criteria.ts +10 -26
  82. package/src/criteria_spec.ts +12 -20
  83. package/src/criteria_support.ts +2 -19
  84. package/src/doc_reads.test.ts +42 -0
  85. package/src/doc_rules.ts +2 -2
  86. package/src/evaluation_resources.test.ts +64 -0
  87. package/src/execution/config.ts +22 -0
  88. package/src/execution/plan.test.ts +65 -0
  89. package/src/execution/plan.ts +51 -0
  90. package/src/gates.ts +9 -6
  91. package/src/gates_spec.test.ts +52 -6
  92. package/src/gates_test_support.ts +9 -7
  93. package/src/install.test.ts +38 -222
  94. package/src/install.ts +117 -308
  95. package/src/install_loaders.test.ts +203 -0
  96. package/src/install_nested.test.ts +2 -2
  97. package/src/install_packs.test.ts +66 -6
  98. package/src/installation/hooks.ts +63 -0
  99. package/src/installation/lock.test.ts +97 -0
  100. package/src/installation/lock.ts +56 -0
  101. package/src/installation/migration.test.ts +212 -0
  102. package/src/installation/migration.ts +65 -0
  103. package/src/installation/plan.ts +96 -0
  104. package/src/installation/transaction.ts +126 -0
  105. package/src/installation_preservation.test.ts +48 -0
  106. package/src/judgments/advice.test.ts +38 -0
  107. package/src/judgments/advice.ts +32 -0
  108. package/src/judgments/command.integration.test.ts +47 -0
  109. package/src/judgments/command.ts +90 -0
  110. package/src/judgments/constants.ts +55 -0
  111. package/src/judgments/evidence.test.ts +48 -0
  112. package/src/judgments/evidence.ts +34 -0
  113. package/src/judgments/files.test.ts +62 -0
  114. package/src/judgments/files.ts +73 -0
  115. package/src/judgments/questions.test.ts +19 -0
  116. package/src/judgments/questions.ts +85 -0
  117. package/src/judgments/replay.test.ts +43 -0
  118. package/src/judgments/replay.ts +66 -0
  119. package/src/judgments/runner.integration.test.ts +165 -0
  120. package/src/judgments/runner.ts +97 -0
  121. package/src/judgments/scan.test.ts +19 -0
  122. package/src/judgments/scan.ts +40 -0
  123. package/src/judgments/syntax.test.ts +115 -0
  124. package/src/judgments/syntax.ts +71 -0
  125. package/src/judgments/syntax_worker.ts +70 -0
  126. package/src/judgments/test_support.ts +60 -0
  127. package/src/judgments/transport.integration.test.ts +63 -0
  128. package/src/judgments/transport.ts +35 -0
  129. package/src/judgments/types.test.ts +43 -0
  130. package/src/judgments/types.ts +66 -0
  131. package/src/judgments/wire.test.ts +48 -0
  132. package/src/judgments/wire.ts +40 -0
  133. package/src/loaders.test.ts +13 -16
  134. package/src/loaders.ts +28 -10
  135. package/src/model.test.ts +20 -3
  136. package/src/model.ts +7 -18
  137. package/src/pack_hygiene.test.ts +42 -1
  138. package/src/pack_hygiene.ts +18 -8
  139. package/src/references.ts +15 -7
  140. package/src/release_inventory.test.ts +80 -0
  141. package/src/render.test.ts +1 -1
  142. package/src/render.ts +3 -8
  143. package/src/required_scanner.test.ts +36 -0
  144. package/src/review_poll.test.ts +158 -0
  145. package/src/scanner_scope.test.ts +202 -0
  146. package/src/setup.test.ts +9 -6
  147. package/src/spec_baseline.test.ts +1 -1
  148. package/src/spec_completion.test.ts +74 -0
  149. package/src/spec_obligations.ts +64 -0
  150. package/src/spec_rehearsal.test.ts +182 -0
  151. package/src/spec_template_io.test.ts +11 -9
  152. package/src/surfaces.test.ts +2 -0
  153. package/src/surfaces.ts +1 -1
  154. package/src/validation.ts +7 -4
  155. package/src/verify.test.ts +18 -2
  156. package/src/verify.ts +7 -3
@@ -2,13 +2,18 @@ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
- import { activeSpecPath, closedSpecPath, runGate } from "./gates";
5
+ import { activeSpecPath, closedSpecPath, runGate, specPathFor } from "./gates";
6
6
  import {
7
7
  cleanupTemporaryDirectories, closedSpecRepository, fixtureRegistry, git, modelFor,
8
8
  newRepository, newSpecRepository, orly, SPEC_RELATIVE, specFixture,
9
9
  } from "./gates_test_support";
10
10
 
11
11
  const BASELINE_FIXTURE = "**Test Baseline:** unit=0 integration=0";
12
+ const OTHER_ACTIVE_SPEC = "docs/v1/active/M100_001_P2_CLI_OTHER.md";
13
+ const CLOSED_SPEC = "docs/v1/done/M99_001_P2_CLI_FIXTURE.md";
14
+ const CLOSED_BRANCH = "feat/closed-owner";
15
+ const OTHER_BRANCH = "feat/other";
16
+ const ACTIVE_SPECS_DIRECTORY = "docs/v1/active";
12
17
 
13
18
  afterEach(cleanupTemporaryDirectories);
14
19
 
@@ -31,6 +36,47 @@ describe("spec discovery", () => {
31
36
  expect(activeSpecPath(project)).toContain("docs/v0.9.2/active");
32
37
  });
33
38
 
39
+ test.each([false, true])("active ownership follows the branch with quoted header %s", async (quoted) => {
40
+ const project = newSpecRepository();
41
+ const branch = "feat/owned";
42
+ git(project, "checkout", "-q", "-b", branch);
43
+ const header = quoted ? `**Branch:** \`${branch}\`` : `**Branch:** ${branch}`;
44
+ await Bun.write(join(project, SPEC_RELATIVE), specFixture(undefined, undefined, [header]));
45
+ await Bun.write(join(project, OTHER_ACTIVE_SPEC), specFixture(undefined, OTHER_BRANCH));
46
+
47
+ expect(activeSpecPath(project)).toBe(join(project, SPEC_RELATIVE));
48
+ });
49
+
50
+ test.each([false, true])("an unrelated active spec cannot hide a closed owner with quoted header %s", async (quoted) => {
51
+ const branch = CLOSED_BRANCH;
52
+ const project = closedSpecRepository(branch);
53
+ const path = join(project, CLOSED_SPEC);
54
+ const header = quoted ? `**Branch:** \`${branch}\`` : `**Branch:** ${branch}`;
55
+ await Bun.write(path, specFixture("DONE", undefined, [header]));
56
+ mkdirSync(join(project, ACTIVE_SPECS_DIRECTORY), { recursive: true });
57
+ await Bun.write(join(project, OTHER_ACTIVE_SPEC), specFixture(undefined, OTHER_BRANCH));
58
+
59
+ expect(specPathFor(project)).toEqual({ path, closed: true });
60
+ });
61
+
62
+ test("an active spec without branch ownership still gates", async () => {
63
+ const project = closedSpecRepository(CLOSED_BRANCH);
64
+ mkdirSync(join(project, ACTIVE_SPECS_DIRECTORY), { recursive: true });
65
+ await Bun.write(join(project, SPEC_RELATIVE), specFixture());
66
+
67
+ expect(specPathFor(project)).toEqual({ path: join(project, SPEC_RELATIVE), closed: false });
68
+ });
69
+
70
+ test("two active owners on the current branch remain an error", async () => {
71
+ const project = newSpecRepository();
72
+ const branch = "feat/shared-owners";
73
+ git(project, "checkout", "-q", "-b", branch);
74
+ await Bun.write(join(project, SPEC_RELATIVE), specFixture(undefined, branch));
75
+ await Bun.write(join(project, OTHER_ACTIVE_SPEC), specFixture(undefined, branch));
76
+
77
+ expect(() => activeSpecPath(project)).toThrow("one stream per worktree");
78
+ });
79
+
34
80
  test("two active specs are a hard error — one stream per worktree", () => {
35
81
  const project = newSpecRepository();
36
82
  mkdirSync(join(project, "docs/v2/active"), { recursive: true });
@@ -39,12 +85,12 @@ describe("spec discovery", () => {
39
85
  expect(() => activeSpecPath(project)).toThrow("one stream per worktree");
40
86
  });
41
87
 
42
- test("a folded active spec yields ownership to the spec it names", async () => {
88
+ test.each([false, true])("a folded active spec yields ownership with quoted header %s", async (quoted) => {
43
89
  const project = newSpecRepository();
44
90
  mkdirSync(join(project, "docs/v2/active"), { recursive: true });
45
91
  await Bun.write(
46
92
  join(project, "docs/v2/active/M100_001_P2_CLI_FOLDED.md"),
47
- specFixture(undefined, undefined, ["**Folded-into:** `M99_001`"]),
93
+ specFixture(undefined, undefined, [quoted ? "**Folded-into:** `M99_001`" : "**Folded-into:** M99_001"]),
48
94
  );
49
95
 
50
96
  expect(activeSpecPath(project)).toEndWith("M99_001_P2_CLI_FIXTURE.md");
@@ -109,12 +155,12 @@ describe("closed-spec follow-through", () => {
109
155
  expect(() => closedSpecPath(project)).toThrow("one stream per worktree");
110
156
  });
111
157
 
112
- test("a folded spec yields ownership to the spec it names", async () => {
158
+ test.each([false, true])("a folded closed spec yields ownership with quoted header %s", async (quoted) => {
113
159
  const project = closedSpecRepository("feat/shared");
114
160
  mkdirSync(join(project, "docs/v2/done"), { recursive: true });
115
161
  await Bun.write(
116
162
  join(project, "docs/v2/done/M100_001_P2_CLI_FOLDED.md"),
117
- specFixture("DONE", "feat/shared", ["**Folded-into:** `M99_001`"]),
163
+ specFixture("DONE", "feat/shared", [quoted ? "**Folded-into:** `M99_001`" : "**Folded-into:** M99_001"]),
118
164
  );
119
165
 
120
166
  expect(closedSpecPath(project)).toEndWith("M99_001_P2_CLI_FIXTURE.md");
@@ -261,7 +307,7 @@ describe("closed-spec follow-through", () => {
261
307
 
262
308
  const acked = closedSpecRepository("feat/defer", [
263
309
  "- Dimension 1.2 was deferred to follow-up",
264
- '> Indy (2026-08-11 09:00): "defer 1.2, ship the rest" — context: fixture',
310
+ '## Discovery\n\n> Indy (2026-08-11 09:00): "defer 1.2, ship the rest" — context: fixture',
265
311
  ]);
266
312
  const ackedModel = await modelFor(acked);
267
313
  expect(runGate(ackedModel, acked, "pr").results.find((result) => result.name === "spec.deferrals")?.ok).toBeTrue();
@@ -23,9 +23,11 @@ const PIPE_OUTPUT = "pipe";
23
23
  const TRUE_COMMAND = "true";
24
24
  const ACCEPTANCE_RUBRIC_HEADING = "Acceptance Rubric";
25
25
  const FIXTURE = "fixture";
26
- const AUDITS_DIR = "audits";
26
+ const AUDITS_DIR = ".orly/audits";
27
27
  const SPEC_GATE_SCRIPT = `${AUDITS_DIR}/spec-template.sh`;
28
28
  const SPEC_GATE_CONTENT = `${AUDITS_DIR}/spec-template.ts`;
29
+ const SOURCE_SPEC_GATE = "audits/spec-template.sh";
30
+ const SOURCE_SPEC_CONTENT = "audits/spec-template.ts";
29
31
 
30
32
  export const ROOT = resolve(import.meta.dir, "..");
31
33
  export const SPEC_RELATIVE = "docs/v1/active/M99_001_P2_CLI_FIXTURE.md";
@@ -66,13 +68,13 @@ export function fixtureRegistry(project: string): string {
66
68
  return root;
67
69
  }
68
70
 
69
- // The gate resolves commands from `.oracle/orly.json` in the repository under
71
+ // The gate resolves commands from `.orly/orly.json` in the repository under
70
72
  // test, so every fixture repository carries one.
71
73
  export function writeFixtureConfig(project: string, surfaces?: { user: string[]; docs: string[] }, extraCommands?: Record<string, string[][]>): void {
72
74
  const config: Record<string, unknown> = structuredClone(FIXTURE_CONFIG);
73
75
  if (surfaces) config.surfaces = surfaces;
74
76
  if (extraCommands) config.commands = { ...(config.commands as Record<string, unknown>), ...extraCommands };
75
- mkdirSync(join(project, ".oracle"), { recursive: true });
77
+ mkdirSync(join(project, ".orly"), { recursive: true });
76
78
  writeFileSync(join(project, CONFIG_PATH), JSON.stringify(config, undefined, 2));
77
79
  // Committed, exactly as a real repository carries it: a linked worktree gets
78
80
  // its rules config from the checkout, and an uncommitted one would also make
@@ -109,8 +111,8 @@ export function closedSpecRepository(branch: string, extraLines: string[] = []):
109
111
  git(project, "checkout", QUIET, "-b", branch);
110
112
  mkdirSync(join(project, "docs/v1/done"), { recursive: true });
111
113
  mkdirSync(join(project, AUDITS_DIR), { recursive: true });
112
- copyFileSync(join(ROOT, SPEC_GATE_SCRIPT), join(project, SPEC_GATE_SCRIPT));
113
- copyFileSync(join(ROOT, SPEC_GATE_CONTENT), join(project, SPEC_GATE_CONTENT));
114
+ copyFileSync(join(ROOT, SOURCE_SPEC_GATE), join(project, SPEC_GATE_SCRIPT));
115
+ copyFileSync(join(ROOT, SOURCE_SPEC_CONTENT), join(project, SPEC_GATE_CONTENT));
114
116
  Bun.write(join(project, "docs/v1/done/M99_001_P2_CLI_FIXTURE.md"), specFixture("DONE", branch, [
115
117
  `**Baseline revision:** ${gitOutput(project, REV_PARSE, HEAD)}`, BASELINE_EVIDENCE, ...extraLines,
116
118
  ]));
@@ -123,8 +125,8 @@ export function newSpecRepository(): string {
123
125
  const project = newRepository();
124
126
  mkdirSync(join(project, "docs/v1/active"), { recursive: true });
125
127
  mkdirSync(join(project, AUDITS_DIR), { recursive: true });
126
- copyFileSync(join(ROOT, SPEC_GATE_SCRIPT), join(project, SPEC_GATE_SCRIPT));
127
- copyFileSync(join(ROOT, SPEC_GATE_CONTENT), join(project, SPEC_GATE_CONTENT));
128
+ copyFileSync(join(ROOT, SOURCE_SPEC_GATE), join(project, SPEC_GATE_SCRIPT));
129
+ copyFileSync(join(ROOT, SOURCE_SPEC_CONTENT), join(project, SPEC_GATE_CONTENT));
128
130
  Bun.write(join(project, SPEC_RELATIVE), specFixture(IN_PROGRESS, undefined, [
129
131
  `**Baseline revision:** ${gitOutput(project, REV_PARSE, HEAD)}`, BASELINE_EVIDENCE,
130
132
  ]));
@@ -23,9 +23,9 @@ describe("install", () => {
23
23
  expect(result.packs).toContain("language.rust");
24
24
  expect(result.packs).not.toContain("language.zig");
25
25
  expect(existsSync(join(repo, "AGENTS.md"))).toBe(true);
26
- expect(existsSync(join(repo, "dispatch/write_rust.md"))).toBe(true);
27
- expect(existsSync(join(repo, ".githooks/pre-commit"))).toBe(true);
28
- expect(existsSync(join(repo, ".oracle/orly.json"))).toBe(true);
26
+ expect(existsSync(join(repo, ".orly/dispatch/write_rust.md"))).toBe(true);
27
+ expect(existsSync(join(repo, ".orly/hooks/pre-commit"))).toBe(true);
28
+ expect(existsSync(join(repo, ".orly/orly.json"))).toBe(true);
29
29
  });
30
30
 
31
31
  test("a second install over the same target reports zero writes", async () => {
@@ -40,18 +40,18 @@ describe("install", () => {
40
40
  expect(second.skipped.length).toBeGreaterThan(0);
41
41
  });
42
42
 
43
- test("replaces a file it wrote, hand-edited or not — git shows the replacement", async () => {
43
+ test("refuses to replace a managed file edited after installation", async () => {
44
44
  const model = await RulesModel.load(ROOT);
45
45
  const repo = newRepository();
46
46
  await Bun.write(join(repo, "src/lib.rs"), "pub fn main() {}\n");
47
47
  await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
48
- await Bun.write(join(repo, "dispatch/write_rust.md"), "hand-edited\n");
48
+ await Bun.write(join(repo, ".orly/dispatch/write_rust.md"), "hand-edited\n");
49
49
 
50
50
  const result = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
51
51
 
52
- expect(result.ok).toBe(true);
53
- expect(result.written).toContain("dispatch/write_rust.md");
54
- expect(await Bun.file(join(repo, "dispatch/write_rust.md")).text()).not.toBe("hand-edited\n");
52
+ expect(result.ok).toBe(false);
53
+ expect(result.written).toEqual([]);
54
+ expect(await Bun.file(join(repo, ".orly/dispatch/write_rust.md")).text()).toBe("hand-edited\n");
55
55
  });
56
56
 
57
57
  // The other half of the same rule: authorship decides. A file orly never
@@ -60,13 +60,13 @@ describe("install", () => {
60
60
  const model = await RulesModel.load(ROOT);
61
61
  const repo = newRepository();
62
62
  await Bun.write(join(repo, "src/lib.rs"), "pub fn main() {}\n");
63
- await Bun.write(join(repo, "dispatch/write_rust.md"), "the repository's own file\n");
63
+ await Bun.write(join(repo, ".orly/dispatch/write_rust.md"), "the repository's own file\n");
64
64
 
65
65
  const result = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
66
66
 
67
67
  expect(result.ok).toBe(false);
68
- expect(result.errors.some((error) => error.path === "dispatch/write_rust.md")).toBeTrue();
69
- expect(await Bun.file(join(repo, "dispatch/write_rust.md")).text()).toBe("the repository's own file\n");
68
+ expect(result.errors.some((error) => error.path === ".orly/dispatch/write_rust.md")).toBeTrue();
69
+ expect(await Bun.file(join(repo, ".orly/dispatch/write_rust.md")).text()).toBe("the repository's own file\n");
70
70
  expect(existsSync(join(repo, "AGENTS.md"))).toBe(false);
71
71
  });
72
72
 
@@ -75,19 +75,19 @@ describe("install", () => {
75
75
  const repo = newRepository();
76
76
  await Bun.write(join(repo, "src/lib.rs"), "pub fn main() {}\n");
77
77
  await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
78
- await Bun.write(join(repo, "dispatch/write_rust.md"), "hand-edited\n");
78
+ await Bun.write(join(repo, ".orly/dispatch/write_rust.md"), "hand-edited\n");
79
79
 
80
80
  const result = await install(model, { targetRoot: repo, force: true, installHooks: true, orlyVersion: "0.4.0" });
81
81
 
82
82
  expect(result.ok).toBe(true);
83
- expect(await Bun.file(join(repo, "dispatch/write_rust.md")).text()).not.toBe("hand-edited\n");
83
+ expect(await Bun.file(join(repo, ".orly/dispatch/write_rust.md")).text()).not.toBe("hand-edited\n");
84
84
  });
85
85
 
86
86
  test("rejects a target that is not a git repository, naming the fix", async () => {
87
87
  const model = await RulesModel.load(ROOT);
88
88
  const notARepo = mkdtempSync(join(tmpdir(), "orly-install-not-a-repo-"));
89
89
  try {
90
- expect(install(model, { targetRoot: notARepo, force: false, installHooks: true, orlyVersion: "0.4.0" })).rejects.toThrow("git init");
90
+ await expect(install(model, { targetRoot: notARepo, force: false, installHooks: true, orlyVersion: "0.4.0" })).rejects.toThrow("git init");
91
91
  } finally {
92
92
  rmSync(notARepo, { recursive: true, force: true });
93
93
  }
@@ -95,15 +95,15 @@ describe("install", () => {
95
95
 
96
96
  test("rejects a target directory that does not exist at all", async () => {
97
97
  const model = await RulesModel.load(ROOT);
98
- expect(install(model, { targetRoot: join(tmpdir(), "orly-install-never-created"), force: false, installHooks: true, orlyVersion: "0.4.0" })).rejects.toThrow("does not exist");
98
+ await expect(install(model, { targetRoot: join(tmpdir(), "orly-install-never-created"), force: false, installHooks: true, orlyVersion: "0.4.0" })).rejects.toThrow("does not exist");
99
99
  });
100
100
 
101
101
  test("rejects an unknown pack named by the repository's own config, before writing anything", async () => {
102
102
  const model = await RulesModel.load(ROOT);
103
103
  const repo = newRepository();
104
- await Bun.write(join(repo, ".oracle/orly.json"), JSON.stringify({ schema_version: 1, packs: ["language.cobol"], commands: {} }));
104
+ await Bun.write(join(repo, ".orly/orly.json"), JSON.stringify({ schema_version: 1, packs: ["language.cobol"], commands: {} }));
105
105
 
106
- expect(install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" })).rejects.toThrow("unknown pack");
106
+ await expect(install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" })).rejects.toThrow("unknown pack");
107
107
  expect(existsSync(join(repo, "AGENTS.md"))).toBe(false);
108
108
  });
109
109
 
@@ -113,9 +113,9 @@ describe("install", () => {
113
113
 
114
114
  await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
115
115
 
116
- expect(gitOutput(repo, "config", "--get", "core.hooksPath")).toBe(".githooks");
116
+ expect(gitOutput(repo, "config", "--get", "core.hooksPath")).toBe(".orly/hooks");
117
117
  for (const hook of ["pre-commit", "pre-push"]) {
118
- const text = await Bun.file(join(repo, ".githooks", hook)).text();
118
+ const text = await Bun.file(join(repo, ".orly/hooks", hook)).text();
119
119
  expect(text).toContain("GIT_DIR");
120
120
  }
121
121
  });
@@ -126,196 +126,10 @@ describe("install", () => {
126
126
 
127
127
  await install(model, { targetRoot: repo, force: false, installHooks: false, orlyVersion: "0.4.0" });
128
128
 
129
- expect(existsSync(join(repo, ".githooks"))).toBe(false);
129
+ expect(existsSync(join(repo, ".orly/hooks"))).toBe(false);
130
130
  expect(gitOutput(repo, "config", "--get", "core.hooksPath")).toBe("");
131
131
  });
132
132
 
133
- test("an existing AGENTS.md survives byte for byte and orly lands beside it", async () => {
134
- const model = await RulesModel.load(ROOT);
135
- const repo = newRepository();
136
- const theirs = "# Our house rules\n\nAlways run `go vet`. Never touch vendor/.\n";
137
- await Bun.write(join(repo, "AGENTS.md"), theirs);
138
- await Bun.write(join(repo, "m.go"), "package main\n");
139
-
140
- const result = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
141
-
142
- expect(result.ok).toBe(true);
143
- expect(existsSync(join(repo, "AGENTS.orly.md"))).toBe(true);
144
- const host = await Bun.file(join(repo, "AGENTS.md")).text();
145
- expect(host).toStartWith(theirs.trimEnd());
146
- expect(host).toContain("AGENTS.orly.md");
147
- // Their file is theirs: orly owns only the block, never the whole file.
148
- expect((await readConfig(repo))?.managed).not.toContain("AGENTS.md");
149
- });
150
-
151
- test("the orly pointer block is idempotent across repeated installs", async () => {
152
- const model = await RulesModel.load(ROOT);
153
- const repo = newRepository();
154
- await Bun.write(join(repo, "AGENTS.md"), "# Ours\n");
155
- await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
156
- const afterFirst = await Bun.file(join(repo, "AGENTS.md")).text();
157
-
158
- const second = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
159
-
160
- expect(second.ok).toBe(true);
161
- expect(await Bun.file(join(repo, "AGENTS.md")).text()).toBe(afterFirst);
162
- expect(afterFirst.split("<!-- orly:begin -->").length - 1).toBe(1);
163
- });
164
-
165
- // One layout everywhere, so `update` never asks which mode it is in and a
166
- // repository that starts with no rules still has the file to put them in.
167
- test("a repository with no AGENTS.md still gets both files", async () => {
168
- const model = await RulesModel.load(ROOT);
169
- const repo = newRepository();
170
-
171
- await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
172
-
173
- expect(await Bun.file(join(repo, "AGENTS.orly.md")).text()).toStartWith("> **Generated by `orly`.**");
174
- const host = await Bun.file(join(repo, "AGENTS.md")).text();
175
- expect(host).toContain("AGENTS.orly.md");
176
- expect(host).toContain("Write this repository's own rules below");
177
- expect((await readConfig(repo))?.managed).not.toContain("AGENTS.md");
178
- });
179
-
180
- // Anything installed before the split holds orly's render under AGENTS.md.
181
- // That content is generated, so replacing it with the stub loses nothing.
182
- test("a single-file install migrates to the split without losing a repository line", async () => {
183
- const model = await RulesModel.load(ROOT);
184
- const repo = newRepository();
185
- await Bun.write(join(repo, "AGENTS.md"), "> **Generated by `orly`.**\nold single-file layout\n");
186
-
187
- await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
188
-
189
- const host = await Bun.file(join(repo, "AGENTS.md")).text();
190
- expect(host).not.toContain("old single-file layout");
191
- expect(host).toContain("AGENTS.orly.md");
192
- expect(await Bun.file(join(repo, "AGENTS.orly.md")).text()).toStartWith("> **Generated by `orly`.**");
193
- });
194
-
195
- // Rule pages cite `AGENTS.md` meaning orly's rules — true only where they are
196
- // authored. In a consumer that citation has to name the file orly wrote.
197
- test("rule pages cite the file orly actually wrote, not the repository's", async () => {
198
- const model = await RulesModel.load(ROOT);
199
- const repo = newRepository();
200
- await Bun.write(join(repo, "src/lib.rs"), "pub fn main() {}\n");
201
-
202
- const result = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
203
-
204
- expect(result.ok).toBe(true);
205
- const rules = await Bun.file(join(repo, "docs/DOCUMENTATION_RULES.md")).text();
206
- expect(rules).not.toContain("](../AGENTS.md)");
207
- });
208
-
209
- test("update rewrites orly's file and leaves the repository's own alone", async () => {
210
- const model = await RulesModel.load(ROOT);
211
- const repo = newRepository();
212
- await Bun.write(join(repo, "AGENTS.md"), "# Ours\n");
213
- await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
214
- await Bun.write(join(repo, "AGENTS.orly.md"), "hand-edited\n");
215
- const host = await Bun.file(join(repo, "AGENTS.md")).text();
216
-
217
- const second = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
218
-
219
- expect(second.ok).toBe(true);
220
- expect(second.written).toContain("AGENTS.orly.md");
221
- expect(await Bun.file(join(repo, "AGENTS.orly.md")).text()).toStartWith("> **Generated by `orly`.**");
222
- expect(await Bun.file(join(repo, "AGENTS.md")).text()).toBe(host);
223
- });
224
-
225
- test("a hook orly did not write is refused, not clobbered", async () => {
226
- const model = await RulesModel.load(ROOT);
227
- const repo = newRepository();
228
- const theirs = "#!/usr/bin/env bash\necho 'our precious hook'\n";
229
- await Bun.write(join(repo, ".githooks/pre-commit"), theirs);
230
-
231
- const result = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
232
-
233
- expect(result.ok).toBe(false);
234
- expect(result.errors.map((error) => error.path)).toContain(".githooks/pre-commit");
235
- expect(await Bun.file(join(repo, ".githooks/pre-commit")).text()).toBe(theirs);
236
- // A refusal leaves no footprint: the managed files never landed either.
237
- expect(existsSync(join(repo, "AGENTS.md"))).toBe(false);
238
- });
239
-
240
- test("generated_hooks_mark_telemetry_invocation", async () => {
241
- const model = await RulesModel.load(ROOT);
242
- const repo = newRepository();
243
- await Bun.write(join(repo, ".githooks/pre-commit"), "#!/usr/bin/env bash\necho 'ours'\n");
244
-
245
- const result = await install(model, { targetRoot: repo, force: true, installHooks: true, orlyVersion: "0.4.0" });
246
-
247
- expect(result.ok).toBe(true);
248
- expect(await Bun.file(join(repo, ".githooks/pre-commit")).text()).toContain("export ORLY_INVOCATION=hook");
249
- expect(await Bun.file(join(repo, ".githooks/pre-push")).text()).toContain("export ORLY_INVOCATION=hook");
250
- });
251
-
252
- test("--no-hooks installs the rules over an existing hook without touching it", async () => {
253
- const model = await RulesModel.load(ROOT);
254
- const repo = newRepository();
255
- const theirs = "#!/usr/bin/env bash\necho 'ours'\n";
256
- await Bun.write(join(repo, ".githooks/pre-commit"), theirs);
257
-
258
- const result = await install(model, { targetRoot: repo, force: false, installHooks: false, orlyVersion: "0.4.0" });
259
-
260
- expect(result.ok).toBe(true);
261
- expect(await Bun.file(join(repo, ".githooks/pre-commit")).text()).toBe(theirs);
262
- expect(existsSync(join(repo, "AGENTS.md"))).toBe(true);
263
- });
264
-
265
- test("refuses to retarget a hooksPath already claimed by something else", async () => {
266
- const model = await RulesModel.load(ROOT);
267
- const repo = newRepository();
268
- git(repo, "config", "core.hooksPath", "some-other-tools-hooks");
269
-
270
- const result = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
271
-
272
- expect(result.ok).toBe(false);
273
- expect(result.errors.some((error) => error.path === "core.hooksPath")).toBe(true);
274
- expect(gitOutput(repo, "config", "--get", "core.hooksPath")).toBe("some-other-tools-hooks");
275
- expect(existsSync(join(repo, "AGENTS.md"))).toBe(false);
276
- });
277
-
278
- test("--no-hooks proceeds even when hooksPath is claimed by something else", async () => {
279
- const model = await RulesModel.load(ROOT);
280
- const repo = newRepository();
281
- git(repo, "config", "core.hooksPath", "some-other-tools-hooks");
282
-
283
- const result = await install(model, { targetRoot: repo, force: false, installHooks: false, orlyVersion: "0.4.0" });
284
-
285
- expect(result.ok).toBe(true);
286
- expect(gitOutput(repo, "config", "--get", "core.hooksPath")).toBe("some-other-tools-hooks");
287
- });
288
-
289
- test("--force retargets a hooksPath claimed by something else", async () => {
290
- const model = await RulesModel.load(ROOT);
291
- const repo = newRepository();
292
- git(repo, "config", "core.hooksPath", "some-other-tools-hooks");
293
-
294
- const result = await install(model, { targetRoot: repo, force: true, installHooks: true, orlyVersion: "0.4.0" });
295
-
296
- expect(result.ok).toBe(true);
297
- expect(gitOutput(repo, "config", "--get", "core.hooksPath")).toBe(".githooks");
298
- });
299
-
300
- test("re-running init after it already set hooksPath is not a claim by another tool", async () => {
301
- const model = await RulesModel.load(ROOT);
302
- const repo = newRepository();
303
- await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
304
-
305
- const second = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
306
-
307
- expect(second.ok).toBe(true);
308
- });
309
-
310
- // Adversarial review, reproduced: a repository the user cloned can commit a
311
- // symlink at a path init would otherwise write to. mkdirSync/writeFile/
312
- // rename all follow an existing symlink silently — without this refusal,
313
- // every managed file materialises through it into wherever the symlink
314
- // points, outside the repository entirely, with `ok: true` and no warning.
315
- // Adversarial review found this: every other write was guarded, the pointer
316
- // host was not, and it is the one write orly aims at a file it does not own.
317
- // A committed `AGENTS.md -> ../outside/victim` carried the block out of the
318
- // repository and reported a normal success.
319
133
  test("refuses when the pointer host is a symlink escaping the target repository", async () => {
320
134
  const model = await RulesModel.load(ROOT);
321
135
  const repo = newRepository();
@@ -330,7 +144,7 @@ describe("install", () => {
330
144
  expect(result.ok).toBe(false);
331
145
  expect(result.errors.some((error) => error.message.includes("outside the target repository"))).toBe(true);
332
146
  expect(await Bun.file(victim).text()).toBe("PRIVATE\n");
333
- expect(existsSync(join(repo, "AGENTS.orly.md"))).toBe(false);
147
+ expect(existsSync(join(repo, ".orly/AGENTS.md"))).toBe(false);
334
148
  } finally {
335
149
  rmSync(outside, { recursive: true, force: true });
336
150
  }
@@ -346,8 +160,8 @@ describe("install", () => {
346
160
  try {
347
161
  const victim = join(outside, "hookvictim");
348
162
  await Bun.write(victim, "VICTIM\n");
349
- mkdirSync(join(repo, ".githooks"), { recursive: true });
350
- symlinkSync(victim, join(repo, ".githooks/pre-commit"));
163
+ mkdirSync(join(repo, ".orly/hooks"), { recursive: true });
164
+ symlinkSync(victim, join(repo, ".orly/hooks/pre-commit"));
351
165
 
352
166
  const result = await install(model, { targetRoot: repo, force: true, installHooks: true, orlyVersion: "0.4.0" });
353
167
 
@@ -363,7 +177,8 @@ describe("install", () => {
363
177
  const repo = newRepository();
364
178
  const outside = mkdtempSync(join(tmpdir(), "orly-install-outside-"));
365
179
  try {
366
- symlinkSync(outside, join(repo, "dispatch"));
180
+ mkdirSync(join(repo, ".orly"), { recursive: true });
181
+ symlinkSync(outside, join(repo, ".orly/dispatch"));
367
182
 
368
183
  const result = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
369
184
 
@@ -381,7 +196,8 @@ describe("install", () => {
381
196
  const repo = newRepository();
382
197
  const outside = mkdtempSync(join(tmpdir(), "orly-install-outside-hooks-"));
383
198
  try {
384
- symlinkSync(outside, join(repo, ".githooks"));
199
+ mkdirSync(join(repo, ".orly"), { recursive: true });
200
+ symlinkSync(outside, join(repo, ".orly/hooks"));
385
201
 
386
202
  const result = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
387
203
 
@@ -393,12 +209,12 @@ describe("install", () => {
393
209
  }
394
210
  });
395
211
 
396
- test("refuses when the .oracle lock directory path is a symlink escaping the target repository", async () => {
212
+ test("refuses when the .orly lock directory path is a symlink escaping the target repository", async () => {
397
213
  const model = await RulesModel.load(ROOT);
398
214
  const repo = newRepository();
399
215
  const outside = mkdtempSync(join(tmpdir(), "orly-install-outside-oracle-"));
400
216
  try {
401
- symlinkSync(outside, join(repo, ".oracle"));
217
+ symlinkSync(outside, join(repo, ".orly"));
402
218
 
403
219
  const result = await install(model, { targetRoot: repo, force: false, installHooks: false, orlyVersion: "0.4.0" });
404
220
 
@@ -415,7 +231,7 @@ describe("install", () => {
415
231
  // boundary, which it cannot do — every install against a target on a
416
232
  // different filesystem than $TMPDIR (an external drive, a devcontainer's
417
233
  // bind-mounted workspace) hard-crashed with EXDEV. Staging inside the
418
- // target's own .oracle/ makes that structurally impossible to reintroduce:
234
+ // target's own .orly/ makes that structurally impossible to reintroduce:
419
235
  // assert no stage directory or leftover ever appears outside the target.
420
236
  test("stages inside the target repository, not the OS tmp dir", async () => {
421
237
  const model = await RulesModel.load(ROOT);
@@ -430,17 +246,17 @@ describe("install", () => {
430
246
  });
431
247
 
432
248
  // The bug this milestone's own atomicity test caught while fixing the
433
- // above: cleaning up an empty .oracle/ on a refused install must not also
434
- // fire on the success path, where .oracle/ is legitimately empty for one
249
+ // above: cleaning up an empty .orly/ on a refused install must not also
250
+ // fire on the success path, where .orly/ is legitimately empty for one
435
251
  // instant before the caller writes orly.json into it.
436
- test("a successful install leaves .oracle/ intact for the lock the caller writes next", async () => {
252
+ test("a successful install leaves .orly/ intact for the lock the caller writes next", async () => {
437
253
  const model = await RulesModel.load(ROOT);
438
254
  const repo = newRepository();
439
255
 
440
256
  const result = await install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" });
441
257
 
442
258
  expect(result.ok).toBe(true);
443
- expect(existsSync(join(repo, ".oracle", "orly.json"))).toBe(true);
259
+ expect(existsSync(join(repo, ".orly", "orly.json"))).toBe(true);
444
260
  });
445
261
 
446
262
  test("the written config records the engine version and every materialised file", async () => {
@@ -453,8 +269,8 @@ describe("install", () => {
453
269
  const config = await readConfig(repo);
454
270
  expect(config).toBeDefined();
455
271
  expect(config?.orly_version).toBe("0.4.0");
456
- expect(config?.managed).toContain("dispatch/write_rust.md");
457
- expect(config?.managed).toContain(".githooks/pre-commit");
272
+ expect(config?.managed).toContain(".orly/dispatch/write_rust.md");
273
+ expect(config?.managed).toContain(".orly/hooks/pre-commit");
458
274
  });
459
275
 
460
276
  test("refuses atomically when a selected pack's file cites a façade no selected pack provides", async () => {
@@ -482,7 +298,7 @@ describe("install", () => {
482
298
  expect(result.errors[0]?.message).toContain("missing dispatch reference");
483
299
  expect(existsSync(join(repo, "broken.md"))).toBe(false);
484
300
  expect(existsSync(join(repo, "AGENTS.md"))).toBe(false);
485
- expect(existsSync(join(repo, ".oracle"))).toBe(false);
301
+ expect(existsSync(join(repo, ".orly"))).toBe(false);
486
302
  } finally {
487
303
  rmSync(engineRoot, { recursive: true, force: true });
488
304
  }
@@ -507,7 +323,7 @@ describe("install", () => {
507
323
  const model = new RulesModel(engineRoot, registry);
508
324
  const repo = newRepository();
509
325
 
510
- expect(install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" })).rejects.toThrow("packs disagree on shared.md");
326
+ await expect(install(model, { targetRoot: repo, force: false, installHooks: true, orlyVersion: "0.4.0" })).rejects.toThrow("packs disagree on shared.md");
511
327
  } finally {
512
328
  rmSync(engineRoot, { recursive: true, force: true });
513
329
  }