@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 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, symlink included, is your answer to the question and orly does not touch it.
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.0",
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",
@@ -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