@agentsfleet/orly 0.10.0 → 0.10.2
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/audits/design-tokens.sh +57 -5
- package/dispatch/write_changelog.md +4 -2
- package/docs/CHANGELOG_VOICE.md +1 -1
- package/package.json +1 -1
- package/src/config.test.ts +36 -1
- package/src/config.ts +48 -1
- package/src/install.ts +16 -2
package/audits/design-tokens.sh
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# design-tokens.sh — enforce design-system token discipline
|
|
3
|
-
#
|
|
2
|
+
# design-tokens.sh — enforce design-system token discipline and flat fills
|
|
3
|
+
# across the app, website, and shared design system.
|
|
4
4
|
#
|
|
5
5
|
# Dispatch façade: dispatch/write_ts_adhere_bun.md (Design Tokens / DESIGN TOKEN GATE)
|
|
6
6
|
# Fires in: CONFORM (via `make harness-verify` in `agentsfleet`).
|
|
@@ -62,6 +62,23 @@ PATTERNS=(
|
|
|
62
62
|
'(text|bg|border)-(red|blue|green|yellow|purple|indigo|violet|pink|orange|gray|slate|zinc|neutral)-[0-9]:::use semantic tokens (text-destructive, text-error, text-success, …)'
|
|
63
63
|
)
|
|
64
64
|
|
|
65
|
+
readonly GRADIENT_REGEX='(repeating-)?(linear|radial|conic)-gradient[[:space:]]*\('
|
|
66
|
+
|
|
67
|
+
# Consumers may select a font role, but only the design system defines it.
|
|
68
|
+
check_font_ownership() {
|
|
69
|
+
local f="$1"
|
|
70
|
+
case "$f" in ui/packages/design-system/*) return ;; esac
|
|
71
|
+
awk '
|
|
72
|
+
{
|
|
73
|
+
source = $0
|
|
74
|
+
gsub(/font-family[[:space:]]*:[[:space:]]*var\(--ff-(sans|mono|display)\)/, "")
|
|
75
|
+
invalid = /font-family[[:space:]]*:|fontFamily[[:space:]]*:[[:space:]]*["\047]|--(ff|font)-(sans|mono|display)[[:space:]]*:|font-\[/
|
|
76
|
+
if (FILENAME ~ /^ui\/packages\/app\// && /font-display|DisplayXL|DisplayLG/) invalid = 1
|
|
77
|
+
if (invalid) printf "%s:%d:%s\n", FILENAME, FNR, source
|
|
78
|
+
}
|
|
79
|
+
' "$f"
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
# ── File scope ───────────────────────────────────────────────────────────
|
|
66
83
|
in_scope() {
|
|
67
84
|
local f="$1"
|
|
@@ -70,8 +87,17 @@ in_scope() {
|
|
|
70
87
|
*tests/e2e/*) return 1 ;;
|
|
71
88
|
ui/packages/app/*.tsx|ui/packages/app/*.jsx) return 0 ;;
|
|
72
89
|
ui/packages/website/*.tsx|ui/packages/website/*.jsx) return 0 ;;
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
*) return 1 ;;
|
|
91
|
+
esac
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
in_gradient_scope() {
|
|
95
|
+
local f="$1"
|
|
96
|
+
case "$f" in
|
|
97
|
+
*.test.*|*.spec.*|*/tests/*|*/coverage/*|*/dist/*|*/.next/*|*/node_modules/*) return 1 ;;
|
|
98
|
+
ui/packages/app/*.css|ui/packages/app/*.js|ui/packages/app/*.jsx|ui/packages/app/*.mjs|ui/packages/app/*.ts|ui/packages/app/*.tsx) return 0 ;;
|
|
99
|
+
ui/packages/website/*.css|ui/packages/website/*.js|ui/packages/website/*.jsx|ui/packages/website/*.mjs|ui/packages/website/*.ts|ui/packages/website/*.tsx) return 0 ;;
|
|
100
|
+
ui/packages/design-system/*.css|ui/packages/design-system/*.js|ui/packages/design-system/*.jsx|ui/packages/design-system/*.mjs|ui/packages/design-system/*.ts|ui/packages/design-system/*.tsx) return 0 ;;
|
|
75
101
|
*) return 1 ;;
|
|
76
102
|
esac
|
|
77
103
|
}
|
|
@@ -79,12 +105,14 @@ in_scope() {
|
|
|
79
105
|
case "$MODE" in
|
|
80
106
|
--staged|staged)
|
|
81
107
|
FILES=$(git diff --cached --name-only --diff-filter=ACMRT 2>/dev/null || true)
|
|
108
|
+
GRADIENT_CANDIDATES="$FILES"
|
|
82
109
|
;;
|
|
83
110
|
--all|all)
|
|
84
111
|
# `git ls-files` reports the index, which includes staged content —
|
|
85
112
|
# so pre-commit-style invocations see staged-but-not-committed files
|
|
86
113
|
# without needing a `--staged` flag.
|
|
87
114
|
FILES=$(git ls-files -- 'ui/packages/app/*.tsx' 'ui/packages/app/*.jsx' 'ui/packages/website/*.tsx' 'ui/packages/website/*.jsx' 2>/dev/null || true)
|
|
115
|
+
GRADIENT_CANDIDATES=$(git ls-files -- ui/packages/app ui/packages/website ui/packages/design-system 2>/dev/null || true)
|
|
88
116
|
;;
|
|
89
117
|
*)
|
|
90
118
|
echo "usage: $0 [--all|--staged]" >&2
|
|
@@ -127,10 +155,34 @@ if [ "${#SCOPED_FILES[@]}" -gt 0 ]; then
|
|
|
127
155
|
done
|
|
128
156
|
fi
|
|
129
157
|
|
|
158
|
+
GRADIENT_FILES=()
|
|
159
|
+
while IFS= read -r f; do
|
|
160
|
+
[ -z "$f" ] && continue
|
|
161
|
+
[ ! -f "$f" ] && continue
|
|
162
|
+
in_gradient_scope "$f" || continue
|
|
163
|
+
GRADIENT_FILES+=("$f")
|
|
164
|
+
while IFS= read -r match; do
|
|
165
|
+
[ -z "$match" ] && continue
|
|
166
|
+
printf '%s\n -> define fonts in design-system tokens.css; use sans for app UI and mono for technical values\n' "$match"
|
|
167
|
+
SEEN_FILES_LIST="${SEEN_FILES_LIST}${f}"$'\n'
|
|
168
|
+
FAIL=1
|
|
169
|
+
done < <(check_font_ownership "$f")
|
|
170
|
+
done <<< "$GRADIENT_CANDIDATES"
|
|
171
|
+
|
|
172
|
+
if [ "${#GRADIENT_FILES[@]}" -gt 0 ]; then
|
|
173
|
+
while IFS= read -r match; do
|
|
174
|
+
[ -z "$match" ] && continue
|
|
175
|
+
f="${match%%:*}"
|
|
176
|
+
printf '%s\n -> use a solid design-system color token; gradients are forbidden\n' "$match"
|
|
177
|
+
SEEN_FILES_LIST="${SEEN_FILES_LIST}${f}"$'\n'
|
|
178
|
+
FAIL=1
|
|
179
|
+
done < <(grep -nHE "$GRADIENT_REGEX" "${GRADIENT_FILES[@]}" 2>/dev/null || true)
|
|
180
|
+
fi
|
|
181
|
+
|
|
130
182
|
FILES_WITH_VIOLATIONS=$(printf '%s' "$SEEN_FILES_LIST" | sort -u | grep -c . || true)
|
|
131
183
|
|
|
132
184
|
if [ "$FAIL" = "0" ]; then
|
|
133
|
-
echo "OK: design-token discipline —
|
|
185
|
+
echo "OK: design-token discipline — named utilities and flat fills verified (mode=$MODE)"
|
|
134
186
|
exit 0
|
|
135
187
|
fi
|
|
136
188
|
|
|
@@ -40,8 +40,10 @@ a gate skip.
|
|
|
40
40
|
names, money amounts stay verbatim.
|
|
41
41
|
- **Internal cleanup** gets aggressive trimming, not a paragraph.
|
|
42
42
|
- **History is archived, not rewritten** — past entries are an immutable record.
|
|
43
|
-
- **Rate constants**
|
|
44
|
-
|
|
43
|
+
- **Rate constants** are pinned by `cross_runtime_rates.rs`, which reads the
|
|
44
|
+
app and Command-Line Interface (CLI) mirrors against the Rust constant. A
|
|
45
|
+
changelog money or rate claim must match those, and `rates.mdx` in the docs
|
|
46
|
+
repository, which no test can reach.
|
|
45
47
|
|
|
46
48
|
## Required output (self-audit line before committing the changelog)
|
|
47
49
|
|
package/docs/CHANGELOG_VOICE.md
CHANGED
|
@@ -14,5 +14,5 @@ Editing `~/Projects/docs/changelog.mdx` or any other Mintlify `<Update>` block.
|
|
|
14
14
|
- **Internal cleanup / refactor entries get the most aggressive trimming.** One lead paragraph + one bullet list. Skip "Test coverage" sections unless the test count is the headline. Indy's exact direction: *"Keep internal code cleanup, refactor to a minimal."*
|
|
15
15
|
- **Never drop load-bearing facts.** Error codes (`UZ-AUTH-003`), endpoint paths + method + body shape + status code, env var names + defaults, schema column / table names, CLI subcommand + flag names, migration steps, money amounts. Tighten prose, not meaning.
|
|
16
16
|
- **Historical entries are archives.** Brevity-pass them; never rewrite the past. A typo correction (e.g. `$0.001` → `$0.01` when it was never true) is allowed and must be called out in the commit message.
|
|
17
|
-
- **Rate constants
|
|
17
|
+
- **Rate constants stay pinned across the runtime surfaces, and a test says so.** `rustd/crates/afd_billing/tests/cross_runtime_rates.rs` reads `ui/packages/app/lib/types.ts` and `cli/src/constants/billing.ts` and fails when either drifts from `rustd/crates/afd_billing/src/nanos.rs` — a test beside the constant it defends, not a gate nothing invokes. Public docs are the one surface it cannot reach: when a rate changes, update `~/Projects/docs/snippets/rates.mdx` in a paired Pull Request (PR). In `*.mdx`, import its named values rather than hand-typing money amounts.
|
|
18
18
|
- **The Mintlify reference Indy pasted (May 1 / May 8 entries) is canonical voice.** Mirror its rhythm, not its product nouns.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentsfleet/orly",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
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/config.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ 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, managedDrift, readConfig, readConfigSync, seedConfig, selectPacks, staleVersion, writeConfig } from "./config";
|
|
6
|
+
import { CONFIG_PATH, contentDigest, managedDrift, readConfig, readConfigSync, seedConfig, selectPacks, staleVersion, writeConfig } from "./config";
|
|
7
7
|
import { RulesModel } from "./model";
|
|
8
8
|
|
|
9
9
|
const ROOT = resolve(import.meta.dir, "..");
|
|
@@ -156,6 +156,40 @@ describe("staleVersion", () => {
|
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
describe("managedDrift — content", () => {
|
|
160
|
+
test("a managed file edited after orly wrote it is drift", async () => {
|
|
161
|
+
// The gap this closes was not hypothetical: a consuming repository added a
|
|
162
|
+
// gradient ban and a font-ownership check straight to a materialised gate
|
|
163
|
+
// script, the pack never learned them, and the next update deleted them —
|
|
164
|
+
// with doctor green the whole way, because it asked only whether the file
|
|
165
|
+
// EXISTED. A gate whose checks can be removed under a passing verifier is
|
|
166
|
+
// not a gate.
|
|
167
|
+
const root = scratch();
|
|
168
|
+
const wrote = "check_one\ncheck_two\n";
|
|
169
|
+
writeFileSync(join(root, "audit.sh"), wrote);
|
|
170
|
+
const config = { ...(await seedConfig(root)), managed: ["audit.sh"], digests: { "audit.sh": contentDigest(new TextEncoder().encode(wrote)) } };
|
|
171
|
+
expect(managedDrift(root, config)).toEqual([]);
|
|
172
|
+
|
|
173
|
+
writeFileSync(join(root, "audit.sh"), "check_one\n");
|
|
174
|
+
const drift = managedDrift(root, config);
|
|
175
|
+
expect(drift).toHaveLength(1);
|
|
176
|
+
expect(drift[0]).toContain("audit.sh");
|
|
177
|
+
expect(drift[0]).toContain("edited after orly wrote it");
|
|
178
|
+
// Deliberately NOT "run orly update": the local content may be the better
|
|
179
|
+
// version, and update discards it. The source is where it has to go.
|
|
180
|
+
expect(drift[0]).toContain("move the change into the pack source");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("a file with no recorded digest makes no claim", async () => {
|
|
184
|
+
// Two cases share this path: a config written before digests existed, and a
|
|
185
|
+
// hook orly manages but did not author. Neither may be reported as drift.
|
|
186
|
+
const root = scratch();
|
|
187
|
+
writeFileSync(join(root, "hook.sh"), "the repository's own\n");
|
|
188
|
+
const config = { ...(await seedConfig(root)), managed: ["hook.sh"], digests: {} };
|
|
189
|
+
expect(managedDrift(root, config)).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
159
193
|
describe("managedDrift", () => {
|
|
160
194
|
test("every managed file present reports no drift", async () => {
|
|
161
195
|
const root = scratch();
|
|
@@ -223,6 +257,7 @@ describe("writeConfig", () => {
|
|
|
223
257
|
commands: { conform: [["make", "audit"]] },
|
|
224
258
|
surfaces: { user: ["src/"], docs: ["docs/"] },
|
|
225
259
|
managed: ["AGENTS.md"],
|
|
260
|
+
digests: {},
|
|
226
261
|
};
|
|
227
262
|
await writeConfig(root, mine);
|
|
228
263
|
await writeConfig(root, { ...(await readConfig(root))!, orly_version: "0.4.0", managed: ["AGENTS.md", "dispatch/write_rust.md"] });
|
package/src/config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
2
3
|
import { dirname, extname, join, relative } from "node:path";
|
|
3
4
|
|
|
@@ -16,6 +17,9 @@ const EXTENSIONS_FIELD = "extensions";
|
|
|
16
17
|
const PACKS_FIELD = "packs";
|
|
17
18
|
const VERSION_FIELD = "orly_version";
|
|
18
19
|
const MANAGED_FIELD = "managed";
|
|
20
|
+
const DIGESTS_FIELD = "digests";
|
|
21
|
+
const DIGEST_ALGORITHM = "sha256";
|
|
22
|
+
const DIGEST_ENCODING = "hex";
|
|
19
23
|
const COMMANDS_FIELD = "commands";
|
|
20
24
|
const CONFORM_COMMAND = "conform";
|
|
21
25
|
const MAKE_COMMAND = "make";
|
|
@@ -65,6 +69,7 @@ export type RepoConfig = {
|
|
|
65
69
|
commands: Record<string, string[][]>;
|
|
66
70
|
surfaces: JsonObject | undefined;
|
|
67
71
|
managed: string[];
|
|
72
|
+
digests: Record<string, string>;
|
|
68
73
|
};
|
|
69
74
|
|
|
70
75
|
export function configPath(targetRoot: string): string {
|
|
@@ -109,9 +114,26 @@ function parseConfig(value: JsonObject): RepoConfig {
|
|
|
109
114
|
commands: readCommands(value[COMMANDS_FIELD]),
|
|
110
115
|
surfaces: isObject(value.surfaces) ? value.surfaces : undefined,
|
|
111
116
|
managed: stringArray(value[MANAGED_FIELD] ?? [], `${CONFIG_PATH} ${MANAGED_FIELD}`),
|
|
117
|
+
digests: readDigests(value[DIGESTS_FIELD]),
|
|
112
118
|
};
|
|
113
119
|
}
|
|
114
120
|
|
|
121
|
+
// Absent in a config written before digests existed, and absent per-file for
|
|
122
|
+
// anything orly did not author (a hook it declined to overwrite). Both cases
|
|
123
|
+
// read as "no claim", which is what keeps an older checkout green until its
|
|
124
|
+
// next update rather than failing it for a record it never had.
|
|
125
|
+
function readDigests(value: unknown): Record<string, string> {
|
|
126
|
+
if (!isObject(value)) return {};
|
|
127
|
+
const digests: Record<string, string> = {};
|
|
128
|
+
for (const [path, digest] of Object.entries(value)) if (isString(digest)) digests[path] = digest;
|
|
129
|
+
return digests;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// The digest orly records for content it wrote, and recomputes to check it.
|
|
133
|
+
export function contentDigest(bytes: Uint8Array): string {
|
|
134
|
+
return createHash(DIGEST_ALGORITHM).update(bytes).digest(DIGEST_ENCODING);
|
|
135
|
+
}
|
|
136
|
+
|
|
115
137
|
// What orly installed here, and whether the engine has moved since.
|
|
116
138
|
export function staleVersion(config: RepoConfig, installedVersion: string): string | undefined {
|
|
117
139
|
if (config.orly_version === installedVersion) return undefined;
|
|
@@ -152,9 +174,33 @@ export function managedDrift(targetRoot: string, config: RepoConfig): string[] {
|
|
|
152
174
|
...present
|
|
153
175
|
.filter((relativePath) => ignored.has(relativePath))
|
|
154
176
|
.map((relativePath) => `managed file is ignored by git: ${relativePath} — orly wrote it and a clone will not receive it; scope the .gitignore rule that excludes it`),
|
|
177
|
+
...editedManaged(targetRoot, config, present),
|
|
155
178
|
].sort();
|
|
156
179
|
}
|
|
157
180
|
|
|
181
|
+
// A managed file whose CONTENT no longer matches what orly wrote.
|
|
182
|
+
//
|
|
183
|
+
// The half `managedDrift` was missing, and the gap was not theoretical: a
|
|
184
|
+
// consuming repository added two checks straight to a materialised gate script,
|
|
185
|
+
// the pack never learned them, and the next update deleted them — while doctor
|
|
186
|
+
// reported green throughout, because it verified that the file EXISTED and
|
|
187
|
+
// never that it still said what orly put there. A gate whose checks can be
|
|
188
|
+
// removed under a passing verifier is not a gate.
|
|
189
|
+
//
|
|
190
|
+
// The remedy for a hit is deliberately not `orly update`: the local content may
|
|
191
|
+
// be the better version, as it was in that case. Update overwrites it; the
|
|
192
|
+
// message says to move the change into the source instead, which is the only
|
|
193
|
+
// way it reaches every other consumer.
|
|
194
|
+
function editedManaged(targetRoot: string, config: RepoConfig, present: string[]): string[] {
|
|
195
|
+
return present.flatMap((relativePath) => {
|
|
196
|
+
const recorded = config.digests[relativePath];
|
|
197
|
+
if (!recorded) return [];
|
|
198
|
+
const actual = contentDigest(readFileSync(join(targetRoot, relativePath)));
|
|
199
|
+
if (actual === recorded) return [];
|
|
200
|
+
return [`managed file was edited after orly wrote it: ${relativePath} — move the change into the pack source, or \`orly update\` to discard it`];
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
158
204
|
// Key order is fixed so a rewrite produces a reviewable diff: orly's own two
|
|
159
205
|
// fields sit at the ends, the repository's three in the middle, untouched.
|
|
160
206
|
export async function writeConfig(targetRoot: string, config: RepoConfig): Promise<string> {
|
|
@@ -168,6 +214,7 @@ export async function writeConfig(targetRoot: string, config: RepoConfig): Promi
|
|
|
168
214
|
commands: config.commands,
|
|
169
215
|
...(config.surfaces ? { surfaces: config.surfaces } : {}),
|
|
170
216
|
managed: [...config.managed].sort(),
|
|
217
|
+
digests: Object.fromEntries(Object.entries(config.digests).sort(([a], [b]) => a.localeCompare(b))),
|
|
171
218
|
};
|
|
172
219
|
await Bun.write(path, `${JSON.stringify(ordered, undefined, JSON_INDENT)}${NEWLINE}`);
|
|
173
220
|
return path;
|
|
@@ -177,7 +224,7 @@ export async function writeConfig(targetRoot: string, config: RepoConfig): Promi
|
|
|
177
224
|
// from what the repository already builds with; an empty set is honest rather
|
|
178
225
|
// than wrong, and says so in the gate's own failure message.
|
|
179
226
|
export async function seedConfig(targetRoot: string): Promise<RepoConfig> {
|
|
180
|
-
return { schema_version: CONFIG_SCHEMA_VERSION, orly_version: "", packs: [], commands: await sniffCommands(targetRoot), surfaces: undefined, managed: [] };
|
|
227
|
+
return { schema_version: CONFIG_SCHEMA_VERSION, orly_version: "", packs: [], commands: await sniffCommands(targetRoot), surfaces: undefined, managed: [], digests: {} };
|
|
181
228
|
}
|
|
182
229
|
|
|
183
230
|
// What this repository installed and runs with, read from its own `.oracle/`.
|
package/src/install.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, mkdtempSync, readdirSync, realpathSync, renameSy
|
|
|
2
2
|
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { UNSCOPED_ENVIRONMENT } from "./git_env";
|
|
5
|
-
import { CONFIG_PATH, readConfig, seedConfig, selectPacks, writeConfig } from "./config";
|
|
5
|
+
import { CONFIG_PATH, contentDigest, readConfig, seedConfig, selectPacks, writeConfig } from "./config";
|
|
6
6
|
import { applyMode, assertWritableInside, hashContent, isBelow, isString, JsonObject, modeLabel, objectArray, objectValue, OrlyError, RulesModel, stringArray } from "./model";
|
|
7
7
|
import { AGENTS_FILENAME, GENERATED_BANNER, installLoaders, Layout, loaderTargets, ORLY_AGENTS_FILENAME } from "./loaders";
|
|
8
8
|
import { referenceClosureErrors, renderProfileText } from "./references";
|
|
@@ -116,7 +116,16 @@ export async function install(model: RulesModel, options: InstallOptions): Promi
|
|
|
116
116
|
skipped.push(...hooks.all.filter((path) => !hooks.written.includes(path)));
|
|
117
117
|
// One file carries both halves: the repository's own fields pass through
|
|
118
118
|
// untouched, orly's two record what this run installed and wrote.
|
|
119
|
-
|
|
119
|
+
// Digests cover `planned` only. Hooks are managed — orly restores a missing
|
|
120
|
+
// one — but it does not always AUTHOR them: it refuses to overwrite a hook it
|
|
121
|
+
// did not write. Recording a digest for a file whose content belongs to the
|
|
122
|
+
// repository would report the repository's own edits as drift.
|
|
123
|
+
await writeConfig(targetRoot, {
|
|
124
|
+
...config,
|
|
125
|
+
orly_version: options.orlyVersion,
|
|
126
|
+
managed: [...planned.map((file) => file.target), ...hooks.all],
|
|
127
|
+
digests: Object.fromEntries(planned.map((file) => [file.target, contentDigest(file.content)])),
|
|
128
|
+
});
|
|
120
129
|
if (!existing) written.push(CONFIG_PATH);
|
|
121
130
|
return { ok: true, packs, written: written.sort(), skipped: skipped.sort(), errors: [] };
|
|
122
131
|
}
|
|
@@ -370,6 +379,11 @@ function retargetRulesCitations(text: string, orlyFile: string): string {
|
|
|
370
379
|
return text
|
|
371
380
|
.replaceAll(`](${AGENTS_FILENAME})`, `](${orlyFile})`)
|
|
372
381
|
.replaceAll(`](../${AGENTS_FILENAME})`, `](../${orlyFile})`)
|
|
382
|
+
// The relative spelling first: `../AGENTS.md` contains `AGENTS.md`, so the
|
|
383
|
+
// bare rule below would rewrite its tail and leave the `../` stranded.
|
|
384
|
+
// Missing it is what rendered `[`../AGENTS.md`](../AGENTS.orly.md)` into
|
|
385
|
+
// four managed pages — a label naming one file over a link to another.
|
|
386
|
+
.replaceAll(`\`../${AGENTS_FILENAME}\``, `\`../${orlyFile}\``)
|
|
373
387
|
.replaceAll(`\`${AGENTS_FILENAME}\``, `\`${orlyFile}\``);
|
|
374
388
|
}
|
|
375
389
|
|