@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.
- package/.githooks/pre-push +2 -1
- package/AGENTS.md +21 -31
- package/README.md +65 -29
- package/SOUL.md +13 -23
- package/audits/agents-md.md +67 -57
- package/audits/deinit-pairs.sh +13 -18
- package/audits/design-tokens.sh +9 -23
- package/audits/doc-read.sh +10 -33
- package/audits/doc-reads.ts +41 -0
- package/audits/error-codes.sh +18 -25
- package/audits/logging-rust.awk +145 -0
- package/audits/logging.sh +10 -166
- package/audits/msid-ui.sh +45 -18
- package/audits/rule-ledger-lib.sh +9 -6
- package/audits/rust-error.sh +20 -15
- package/audits/scope.sh +88 -0
- package/audits/spec-template.sh +2 -2
- package/audits/spec-template.ts +1 -1
- package/audits/sql-mod.sh +11 -14
- package/audits/ufs-strings.awk +164 -0
- package/audits/ufs.sh +10 -188
- package/core/operating-model.md +8 -8
- package/dispatch/edit_rules.md +8 -6
- package/dispatch/lib.sh +41 -26
- package/dispatch/lifecycle.md +12 -5
- package/dispatch/name_architecture.md +8 -3
- package/dispatch/verify.md +1 -1
- package/dispatch/write_any.md +10 -3
- package/dispatch/write_changelog.md +7 -4
- package/dispatch/write_go.md +1 -1
- package/dispatch/write_http.md +5 -7
- package/dispatch/write_python.md +1 -1
- package/dispatch/write_rust.md +17 -1
- package/dispatch/write_shell.md +1 -1
- package/dispatch/write_spec.md +3 -1
- package/dispatch/write_sql.md +28 -6
- package/dispatch/write_ts_adhere_bun.md +11 -0
- package/dispatch/write_zig.md +53 -21
- package/docs/CHANGELOG_VOICE.md +11 -2
- package/docs/EXECUTE_DOC_READS.md +10 -10
- package/docs/HARNESS_VERIFY_OUTPUT.md +6 -3
- package/docs/JUDGMENTS.md +153 -0
- package/docs/LIFECYCLE_PATTERNS.md +26 -16
- package/docs/LOGGING_STANDARD.md +39 -19
- package/docs/ORLY_ARCHITECTURE.md +77 -6
- package/docs/RELEASE_TEMPLATE.md +21 -5
- package/docs/REST_API_DESIGN_GUIDELINES.md +86 -21
- package/docs/RULE_ENFORCEMENT.md +6 -6
- package/docs/SCHEMA_CONVENTIONS.md +24 -3
- package/docs/TEMPLATE.md +4 -4
- package/docs/VERIFY_TIERS.md +2 -2
- package/docs/greptile-learnings/RULES.md +81 -26
- package/llms.txt +37 -34
- package/package.json +6 -1
- package/packs/language/go/rules.md +1 -1
- package/packs/language/python/rules.md +1 -1
- package/packs/language/rust/rules.md +17 -1
- package/packs/language/shell/rules.md +1 -1
- package/registry.json +22 -7
- package/schemas/profile.schema.json +48 -5
- package/skills/orly-babysit-prs/SKILL.md +87 -34
- package/skills/orly-spec-new/SKILL.md +19 -6
- package/skills/orly-write-integration-test/SKILL.md +4 -4
- package/skills/orly-write-unit-test/SKILL.md +10 -1
- package/src/cli.test.ts +2 -2
- package/src/cli.ts +19 -75
- package/src/cli_help.ts +69 -0
- package/src/command_limits.ts +20 -0
- package/src/command_process.test.ts +109 -0
- package/src/command_process.ts +88 -0
- package/src/command_runner.test.ts +88 -0
- package/src/command_runner.ts +33 -0
- package/src/comprehension_cache.test.ts +123 -0
- package/src/config.test.ts +5 -4
- package/src/config.ts +26 -164
- package/src/config_discovery.ts +159 -0
- package/src/config_validation.test.ts +56 -0
- package/src/config_validation.ts +25 -0
- package/src/criteria.test.ts +1 -1
- package/src/criteria.ts +10 -26
- package/src/criteria_spec.ts +12 -20
- package/src/criteria_support.ts +2 -19
- package/src/doc_reads.test.ts +42 -0
- package/src/doc_rules.ts +2 -2
- package/src/evaluation_resources.test.ts +64 -0
- package/src/execution/config.ts +22 -0
- package/src/execution/plan.test.ts +65 -0
- package/src/execution/plan.ts +51 -0
- package/src/gates.ts +9 -6
- package/src/gates_spec.test.ts +52 -6
- package/src/gates_test_support.ts +9 -7
- package/src/install.test.ts +38 -222
- package/src/install.ts +117 -308
- package/src/install_loaders.test.ts +203 -0
- package/src/install_nested.test.ts +2 -2
- package/src/install_packs.test.ts +66 -6
- package/src/installation/hooks.ts +63 -0
- package/src/installation/lock.test.ts +97 -0
- package/src/installation/lock.ts +56 -0
- package/src/installation/migration.test.ts +212 -0
- package/src/installation/migration.ts +65 -0
- package/src/installation/plan.ts +96 -0
- package/src/installation/transaction.ts +126 -0
- package/src/installation_preservation.test.ts +48 -0
- package/src/judgments/advice.test.ts +38 -0
- package/src/judgments/advice.ts +32 -0
- package/src/judgments/command.integration.test.ts +47 -0
- package/src/judgments/command.ts +90 -0
- package/src/judgments/constants.ts +55 -0
- package/src/judgments/evidence.test.ts +48 -0
- package/src/judgments/evidence.ts +34 -0
- package/src/judgments/files.test.ts +62 -0
- package/src/judgments/files.ts +73 -0
- package/src/judgments/questions.test.ts +19 -0
- package/src/judgments/questions.ts +85 -0
- package/src/judgments/replay.test.ts +43 -0
- package/src/judgments/replay.ts +66 -0
- package/src/judgments/runner.integration.test.ts +165 -0
- package/src/judgments/runner.ts +97 -0
- package/src/judgments/scan.test.ts +19 -0
- package/src/judgments/scan.ts +40 -0
- package/src/judgments/syntax.test.ts +115 -0
- package/src/judgments/syntax.ts +71 -0
- package/src/judgments/syntax_worker.ts +70 -0
- package/src/judgments/test_support.ts +60 -0
- package/src/judgments/transport.integration.test.ts +63 -0
- package/src/judgments/transport.ts +35 -0
- package/src/judgments/types.test.ts +43 -0
- package/src/judgments/types.ts +66 -0
- package/src/judgments/wire.test.ts +48 -0
- package/src/judgments/wire.ts +40 -0
- package/src/loaders.test.ts +13 -16
- package/src/loaders.ts +28 -10
- package/src/model.test.ts +20 -3
- package/src/model.ts +7 -18
- package/src/pack_hygiene.test.ts +42 -1
- package/src/pack_hygiene.ts +18 -8
- package/src/references.ts +15 -7
- package/src/release_inventory.test.ts +80 -0
- package/src/render.test.ts +1 -1
- package/src/render.ts +3 -8
- package/src/required_scanner.test.ts +36 -0
- package/src/review_poll.test.ts +158 -0
- package/src/scanner_scope.test.ts +202 -0
- package/src/setup.test.ts +9 -6
- package/src/spec_baseline.test.ts +1 -1
- package/src/spec_completion.test.ts +74 -0
- package/src/spec_obligations.ts +64 -0
- package/src/spec_rehearsal.test.ts +182 -0
- package/src/spec_template_io.test.ts +11 -9
- package/src/surfaces.test.ts +2 -0
- package/src/surfaces.ts +1 -1
- package/src/validation.ts +7 -4
- package/src/verify.test.ts +18 -2
- package/src/verify.ts +7 -3
package/src/install.test.ts
CHANGED
|
@@ -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, ".
|
|
28
|
-
expect(existsSync(join(repo, ".
|
|
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("
|
|
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(
|
|
53
|
-
expect(result.written).
|
|
54
|
-
expect(await Bun.file(join(repo, "dispatch/write_rust.md")).text()).
|
|
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, ".
|
|
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(".
|
|
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, ".
|
|
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, ".
|
|
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, "
|
|
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, ".
|
|
350
|
-
symlinkSync(victim, join(repo, ".
|
|
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
|
-
|
|
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
|
-
|
|
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 .
|
|
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, ".
|
|
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 .
|
|
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 .
|
|
434
|
-
// fire on the success path, where .
|
|
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 .
|
|
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, ".
|
|
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(".
|
|
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, ".
|
|
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
|
}
|