@agentsfleet/orly 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/loaders.test.ts +49 -1
- package/src/model.ts +11 -1
package/README.md
CHANGED
|
@@ -199,7 +199,7 @@ Each edit activates the rule page for that file kind. A gate proves whether the
|
|
|
199
199
|
|
|
200
200
|
`AGENTS.md` stays yours — orly writes its rules beside it and points at them from one delimited block.
|
|
201
201
|
|
|
202
|
-
The last two files exist because installing a rules file is not the same as delivering it, and no runtime loads `AGENTS.orly.md` on its own. Codex and Amp auto-load `AGENTS.md`, which carries the pointer block onward. Claude Code loads `CLAUDE.md` and nothing else. opencode loads only what its `instructions` name. So orly writes the one import line each of those needs, and leaves any file you already wrote alone — a `CLAUDE.md` of your own
|
|
202
|
+
The last two files exist because installing a rules file is not the same as delivering it, and no runtime loads `AGENTS.orly.md` on its own. Codex and Amp auto-load `AGENTS.md`, which carries the pointer block onward. Claude Code loads `CLAUDE.md` and nothing else. opencode loads only what its `instructions` name. So orly writes the one import line each of those needs, and leaves any file you already wrote alone — a `CLAUDE.md` of your own is your answer to the question and orly does not touch it — including a symlink, whether it points at your rules file, somewhere else in the repository, or at nothing yet. Only a link out of the repository is refused, because a write through it would land outside the repository you ran orly in.
|
|
203
203
|
|
|
204
204
|
> [!WARNING]
|
|
205
205
|
> orly refuses to replace a hook or rule page it did not write. `--force` and `--no-hooks` are the ways through. A refused run changes nothing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentsfleet/orly",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Repository-scoped engineering harness: renders agent rules, materialises the gates that enforce them, and proves the PR boundary \u2014 for any coding agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/loaders.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { existsSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, symlinkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { cleanupTemporaryDirectories, newRepository, ROOT } from "./gates_test_support";
|
|
@@ -73,6 +73,54 @@ describe("runtime loaders", () => {
|
|
|
73
73
|
expect(managed).not.toContain("AGENTS.md");
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
// The block is delimited so a re-run replaces exactly what the last run
|
|
77
|
+
// wrote. What surrounds it is the repository's prose and must survive an
|
|
78
|
+
// update that changes the block's own wording — as this release does.
|
|
79
|
+
test("a stale pointer block is replaced in place, and nothing either side of it", async () => {
|
|
80
|
+
const repo = newRepository();
|
|
81
|
+
await Bun.write(join(repo, "AGENTS.md"), "# Ours\n\nkeep me\n\n<!-- orly:begin -->\nstale pointer\n<!-- orly:end -->\n\ntrailing prose\n");
|
|
82
|
+
|
|
83
|
+
const result = await installInto(repo);
|
|
84
|
+
|
|
85
|
+
expect(result.written).toContain("AGENTS.md");
|
|
86
|
+
const host = await Bun.file(join(repo, "AGENTS.md")).text();
|
|
87
|
+
expect(host).toContain("keep me");
|
|
88
|
+
expect(host).toContain("trailing prose");
|
|
89
|
+
expect(host).not.toContain("stale pointer");
|
|
90
|
+
expect(host).toContain("\n@AGENTS.orly.md\n");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// The migration path for every repository installed before the loaders
|
|
94
|
+
// existed, and for every later change to the loader's own wording: the
|
|
95
|
+
// banner is the authorship evidence, so orly's own file is orly's to refresh.
|
|
96
|
+
test("a CLAUDE.md orly wrote itself is refreshed, not left stale", async () => {
|
|
97
|
+
const repo = newRepository();
|
|
98
|
+
await Bun.write(join(repo, "CLAUDE.md"), "> **Generated by `orly`.**\n> an older loader\n\n@ELSEWHERE.md\n");
|
|
99
|
+
|
|
100
|
+
const result = await installInto(repo);
|
|
101
|
+
|
|
102
|
+
expect(result.written).toContain("CLAUDE.md");
|
|
103
|
+
const loader = await Bun.file(join(repo, "CLAUDE.md")).text();
|
|
104
|
+
expect(loader).toContain("\n@AGENTS.md\n");
|
|
105
|
+
expect(loader).not.toContain("ELSEWHERE");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// A link orly cannot resolve is still someone's deliberate wiring. Writing
|
|
109
|
+
// through it would create the file it points at, which is a decision the
|
|
110
|
+
// repository never asked orly to make.
|
|
111
|
+
test("a dangling CLAUDE.md symlink is left alone, not written through", async () => {
|
|
112
|
+
const repo = newRepository();
|
|
113
|
+
symlinkSync("nowhere.md", join(repo, "CLAUDE.md"));
|
|
114
|
+
|
|
115
|
+
const result = await installInto(repo);
|
|
116
|
+
|
|
117
|
+
expect(result.ok).toBe(true);
|
|
118
|
+
expect(result.written).not.toContain("CLAUDE.md");
|
|
119
|
+
expect(result.skipped).toContain("CLAUDE.md");
|
|
120
|
+
expect(existsSync(join(repo, "nowhere.md"))).toBe(false);
|
|
121
|
+
expect(lstatSync(join(repo, "CLAUDE.md")).isSymbolicLink()).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
76
124
|
test("a repository's own CLAUDE.md is reported, never edited", async () => {
|
|
77
125
|
const repo = newRepository();
|
|
78
126
|
const theirs = "# Our own Claude instructions\n";
|
package/src/model.ts
CHANGED
|
@@ -83,11 +83,21 @@ function isSymbolicLink(path: string): boolean {
|
|
|
83
83
|
|
|
84
84
|
// A dangling link resolves nowhere; judge it by where it points, not where it
|
|
85
85
|
// lands, so a link to a not-yet-created file outside the repository still fails.
|
|
86
|
+
//
|
|
87
|
+
// The directory holding it is resolved too, and has to be: the caller compares
|
|
88
|
+
// this against a realpath'd root, and a lexical answer put a link pointing
|
|
89
|
+
// squarely INSIDE the repository on the wrong side of that comparison wherever
|
|
90
|
+
// the repository itself sits behind a symlink — which is every macOS temporary
|
|
91
|
+
// directory, and plenty of real checkouts. The refusal even named the file it
|
|
92
|
+
// was about to protect, one path component at a time.
|
|
93
|
+
//
|
|
94
|
+
// Resolving the parent cannot fail here: this runs only where the destination
|
|
95
|
+
// itself lstat'd as a link, which a missing parent makes impossible.
|
|
86
96
|
function safeRealpath(path: string): string | undefined {
|
|
87
97
|
try {
|
|
88
98
|
return realpathSync(path);
|
|
89
99
|
} catch {
|
|
90
|
-
return resolve(dirname(path), readlinkSync(path));
|
|
100
|
+
return resolve(realpathSync(dirname(path)), readlinkSync(path));
|
|
91
101
|
}
|
|
92
102
|
}
|
|
93
103
|
|