@agentsfleet/orly 0.5.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 (112) hide show
  1. package/.githooks/pre-commit +42 -0
  2. package/.githooks/pre-push +67 -0
  3. package/AGENTS.md +348 -0
  4. package/LICENSE +21 -0
  5. package/README.md +249 -0
  6. package/SOUL.md +90 -0
  7. package/audits/agents-md.md +497 -0
  8. package/audits/agents-md.sh +353 -0
  9. package/audits/cross-tier-rates.sh +105 -0
  10. package/audits/data.sh +185 -0
  11. package/audits/deinit-pairs.sh +224 -0
  12. package/audits/design-tokens.sh +143 -0
  13. package/audits/doc-read.sh +183 -0
  14. package/audits/error-codes.sh +221 -0
  15. package/audits/fixtures/clean.diff +15 -0
  16. package/audits/fixtures/dirty.diff +16 -0
  17. package/audits/lifecycle-anchors.sh +22 -0
  18. package/audits/logging.sh +217 -0
  19. package/audits/msid-ui.sh +144 -0
  20. package/audits/parity-dispatch.sh +51 -0
  21. package/audits/rule-ledger-lib.sh +264 -0
  22. package/audits/rule-ledger.sh +225 -0
  23. package/audits/rule-paths.sh +108 -0
  24. package/audits/spec-template.sh +258 -0
  25. package/audits/sql-mod.sh +102 -0
  26. package/audits/ufs.sh +303 -0
  27. package/bin/orly +26 -0
  28. package/core/operating-model.md +256 -0
  29. package/dispatch/edit_rules.md +69 -0
  30. package/dispatch/lib.sh +213 -0
  31. package/dispatch/lifecycle.md +187 -0
  32. package/dispatch/name_architecture.md +93 -0
  33. package/dispatch/verify.md +111 -0
  34. package/dispatch/write_any.md +740 -0
  35. package/dispatch/write_any.sh +49 -0
  36. package/dispatch/write_auth.md +36 -0
  37. package/dispatch/write_changelog.md +64 -0
  38. package/dispatch/write_documentation.md +45 -0
  39. package/dispatch/write_go.md +24 -0
  40. package/dispatch/write_http.md +45 -0
  41. package/dispatch/write_mdx.md +7 -0
  42. package/dispatch/write_python.md +8 -0
  43. package/dispatch/write_rust.md +38 -0
  44. package/dispatch/write_shell.md +8 -0
  45. package/dispatch/write_spec.md +105 -0
  46. package/dispatch/write_sql.md +112 -0
  47. package/dispatch/write_sql.sh +36 -0
  48. package/dispatch/write_ts_adhere_bun.md +493 -0
  49. package/dispatch/write_ts_adhere_bun.sh +37 -0
  50. package/dispatch/write_zig.md +852 -0
  51. package/dispatch/write_zig.sh +40 -0
  52. package/docs/CHANGELOG_VOICE.md +18 -0
  53. package/docs/DISPATCH_ARCHITECTURE.md +664 -0
  54. package/docs/DISPATCH_REVIEW_DISPOSITION.md +65 -0
  55. package/docs/DOCUMENTATION_RULES.md +209 -0
  56. package/docs/EXECUTE_DOC_READS.md +29 -0
  57. package/docs/HARNESS_VERIFY_OUTPUT.md +40 -0
  58. package/docs/LIFECYCLE_PATTERNS.md +316 -0
  59. package/docs/LOGGING_STANDARD.md +397 -0
  60. package/docs/ORLY_ARCHITECTURE.md +129 -0
  61. package/docs/REST_API_DESIGN_GUIDELINES.md +783 -0
  62. package/docs/RULE_ENFORCEMENT.md +41 -0
  63. package/docs/SCHEMA_CONVENTIONS.md +93 -0
  64. package/docs/TEMPLATE.md +411 -0
  65. package/docs/VERIFY_TIERS.md +57 -0
  66. package/docs/greptile-learnings/README.md +51 -0
  67. package/docs/greptile-learnings/RULES.md +1045 -0
  68. package/fixtures/registry-invalid.json +1 -0
  69. package/fixtures/registry-valid.json +18 -0
  70. package/fixtures/unclosed-pack-block.md +8 -0
  71. package/package.json +63 -0
  72. package/packs/language/go/rules.md +24 -0
  73. package/packs/language/mdx/rules.md +7 -0
  74. package/packs/language/python/rules.md +8 -0
  75. package/packs/language/rust/rules.md +38 -0
  76. package/packs/language/shell/rules.md +8 -0
  77. package/registry.json +405 -0
  78. package/schemas/evidence.schema.json +43 -0
  79. package/schemas/profile.schema.json +12 -0
  80. package/schemas/registry.schema.json +12 -0
  81. package/skills/orly-babysit-prs/SKILL.md +326 -0
  82. package/skills/orly-spec-new/SKILL.md +157 -0
  83. package/skills/orly-write-integration-test/SKILL.md +470 -0
  84. package/skills/orly-write-unit-test/SKILL.md +520 -0
  85. package/src/cli.test.ts +70 -0
  86. package/src/cli.ts +287 -0
  87. package/src/config.test.ts +226 -0
  88. package/src/config.ts +288 -0
  89. package/src/criteria.test.ts +137 -0
  90. package/src/criteria.ts +166 -0
  91. package/src/criteria_spec.ts +127 -0
  92. package/src/criteria_support.ts +53 -0
  93. package/src/gates.test.ts +183 -0
  94. package/src/gates.ts +169 -0
  95. package/src/gates_spec.test.ts +98 -0
  96. package/src/gates_test_support.ts +194 -0
  97. package/src/git_env.ts +20 -0
  98. package/src/install.test.ts +514 -0
  99. package/src/install.ts +447 -0
  100. package/src/install_nested.test.ts +32 -0
  101. package/src/model.test.ts +31 -0
  102. package/src/model.ts +245 -0
  103. package/src/references.test.ts +47 -0
  104. package/src/references.ts +147 -0
  105. package/src/render.test.ts +60 -0
  106. package/src/render.ts +94 -0
  107. package/src/surfaces.test.ts +43 -0
  108. package/src/surfaces.ts +89 -0
  109. package/src/validation.test.ts +11 -0
  110. package/src/validation.ts +87 -0
  111. package/src/verify.test.ts +13 -0
  112. package/src/verify.ts +59 -0
package/src/model.ts ADDED
@@ -0,0 +1,245 @@
1
+ import { chmodSync, existsSync, lstatSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
3
+
4
+ import { validateActiveRule, validateRelativePath } from "./validation";
5
+
6
+ export type JsonObject = Record<string, unknown>;
7
+
8
+ const HASH_ALGORITHM = "sha256";
9
+ const HASH_ENCODING = "hex";
10
+ const MODE_EXECUTABLE = "0755";
11
+ const MODE_REGULAR = "0644";
12
+ const EXECUTABLE_BITS = 0o755;
13
+ const OCTAL = 8;
14
+
15
+ const NON_EXECUTABLE_MODE = 0o644;
16
+ const EXECUTABLE_MODE = 0o755;
17
+ const JSON_EXTENSION = ".json";
18
+ const REPOSITORIES_LABEL = "repositories";
19
+ const REGISTRY_LABEL = "registry";
20
+ const REGISTRY_PACKS_LABEL = "registry packs";
21
+ const REGISTRY_RULES_LABEL = "registry rules";
22
+ const CORE_DOCUMENTS_LABEL = "core documents";
23
+ const MANAGED_FILES_LABEL = "managed files";
24
+ const ACTIVE_STATE = "active";
25
+ const PARENT_SEGMENT = "..";
26
+
27
+ export class OrlyError extends Error {}
28
+
29
+ // The one boundary check every filesystem-writing path in orly relies on —
30
+ // previously three independent copies (repository.ts, references.ts, and an
31
+ // install.ts symlink-escape check found by adversarial review), which is
32
+ // itself a risk: a security check that can drift out of sync across copies.
33
+ export function isBelow(path: string, root: string): boolean {
34
+ const candidate = relative(root, path);
35
+ return candidate === "" || (!candidate.startsWith(PARENT_SEGMENT) && !isAbsolute(candidate));
36
+ }
37
+
38
+ // A destination a caller has not yet written may not exist; find the nearest
39
+ // ancestor that does, so its real (symlink-resolved) location can be checked
40
+ // before anything is created under it.
41
+ export function nearestExistingAncestor(path: string): string {
42
+ let current = path;
43
+ while (!existsSync(current)) {
44
+ const parent = dirname(current);
45
+ if (parent === current) return current;
46
+ current = parent;
47
+ }
48
+ return current;
49
+ }
50
+
51
+ // A committed symlink inside a target repository — planted by that
52
+ // repository's own author, not the invoking user — otherwise lets any
53
+ // materialising write follow it and land outside the target entirely.
54
+ // mkdirSync/writeFile/rename all follow symlinks silently; this must run
55
+ // before every one of them, not just once at the top of an operation.
56
+ export function assertWritableInside(root: string, relativeTarget: string, kind: string): void {
57
+ const destination = join(root, relativeTarget);
58
+ const ancestor = nearestExistingAncestor(dirname(destination));
59
+ const resolvedAncestor = realpathSync(ancestor);
60
+ const resolvedRoot = realpathSync(root);
61
+ if (!isBelow(resolvedAncestor, resolvedRoot)) {
62
+ throw new OrlyError(`refusing to write ${kind} outside the target repository: ${relativeTarget} resolves through a symlink to ${resolvedAncestor}`);
63
+ }
64
+ // The destination itself, not only the directory holding it. A symlinked
65
+ // *file* — a committed `AGENTS.md -> ../../elsewhere` — has an ancestor
66
+ // squarely inside the repository, so the check above passes and the write
67
+ // follows the link out. Every write orly makes goes through here, so this is
68
+ // the one place the file case has to be caught.
69
+ if (!isSymbolicLink(destination)) return;
70
+ const resolvedDestination = safeRealpath(destination);
71
+ if (resolvedDestination !== undefined && !isBelow(resolvedDestination, resolvedRoot)) {
72
+ throw new OrlyError(`refusing to write ${kind} outside the target repository: ${relativeTarget} is a symlink to ${resolvedDestination}`);
73
+ }
74
+ }
75
+
76
+ function isSymbolicLink(path: string): boolean {
77
+ try {
78
+ return lstatSync(path).isSymbolicLink();
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ // A dangling link resolves nowhere; judge it by where it points, not where it
85
+ // lands, so a link to a not-yet-created file outside the repository still fails.
86
+ function safeRealpath(path: string): string | undefined {
87
+ try {
88
+ return realpathSync(path);
89
+ } catch {
90
+ return resolve(dirname(path), readlinkSync(path));
91
+ }
92
+ }
93
+
94
+ export class RulesModel {
95
+ readonly root: string;
96
+ readonly registry: JsonObject;
97
+
98
+ constructor(
99
+ root: string,
100
+ registry: JsonObject,
101
+ ) {
102
+ this.root = root;
103
+ this.registry = registry;
104
+ }
105
+
106
+ static async load(root: string): Promise<RulesModel> {
107
+ const registry = await readJsonObject(join(root, "registry.json"));
108
+ return new RulesModel(root, registry);
109
+ }
110
+
111
+ validate(): void {
112
+ const errors: string[] = [];
113
+ this.validateRegistry(errors);
114
+ this.validateRules(errors);
115
+ if (errors.length > 0) throw new OrlyError(errors.join("\n"));
116
+ }
117
+
118
+ private validateRegistry(errors: string[]): void {
119
+ if (this.registry.schema_version !== 1) errors.push("registry schema_version must equal 1");
120
+ const documents = this.registry.core_documents;
121
+ if (!Array.isArray(documents) || documents.length === 0) errors.push("registry core_documents must be a non-empty array");
122
+ else for (const document of documents) this.requireSource(document, "core document", errors);
123
+ const packs = this.registry.packs;
124
+ if (!isObject(packs) || Object.keys(packs).length === 0) {
125
+ errors.push("registry packs must be a non-empty object");
126
+ return;
127
+ }
128
+ for (const [name, value] of Object.entries(packs)) this.validatePack(name, value, errors);
129
+ }
130
+
131
+ private validatePack(name: string, value: unknown, errors: string[]): void {
132
+ if (!isObject(value)) {
133
+ errors.push(`pack ${name} must be an object`);
134
+ return;
135
+ }
136
+ if (!Array.isArray(value.extensions)) errors.push(`pack ${name} extensions must be an array`);
137
+ if (!Array.isArray(value.managed_files)) {
138
+ errors.push(`pack ${name} managed_files must be an array`);
139
+ return;
140
+ }
141
+ for (const file of value.managed_files) {
142
+ if (!isObject(file)) {
143
+ errors.push(`pack ${name} managed file must be an object`);
144
+ continue;
145
+ }
146
+ this.requireSource(file.source, `pack ${name}`, errors);
147
+ validateRelativePath(file.target, `pack ${name} managed target`, errors);
148
+ }
149
+ }
150
+
151
+ private validateRules(errors: string[]): void {
152
+ if (!Array.isArray(this.registry.rules)) {
153
+ errors.push("registry rules must be an array");
154
+ return;
155
+ }
156
+ const packs = isObject(this.registry.packs) ? this.registry.packs : {};
157
+ const keys = new Set<string>();
158
+ for (const value of this.registry.rules) {
159
+ if (!isObject(value)) {
160
+ errors.push("registry rule must be an object");
161
+ continue;
162
+ }
163
+ const key = value.key;
164
+ if (!isString(key) || key.length === 0) {
165
+ errors.push("registry rule key must be a string");
166
+ continue;
167
+ }
168
+ if (keys.has(key)) errors.push(`duplicate rule key: ${key}`);
169
+ keys.add(key);
170
+ if (![ACTIVE_STATE, "draft", "retired"].includes(String(value.state))) errors.push(`rule ${key} has invalid state`);
171
+ if (!isString(value.pack) || !(value.pack in packs)) errors.push(`rule ${key} selects unknown pack ${String(value.pack)}`);
172
+ if (value.state === ACTIVE_STATE) validateActiveRule(key, value, this.root, errors);
173
+ }
174
+ }
175
+
176
+ private requireSource(source: unknown, label: string, errors: string[]): void {
177
+ if (!isString(source) || source.length === 0) {
178
+ errors.push(`${label} source must be a string`);
179
+ return;
180
+ }
181
+ validateRelativePath(source, `${label} source`, errors);
182
+ if (!existsSync(join(this.root, source))) errors.push(`${label} source is missing: ${source}`);
183
+ }
184
+ }
185
+
186
+ // The published package ships no repositories.json — it names only this
187
+ // machine's own checkouts, which a stranger's install has no use for and
188
+ // this repo's owner has no reason to publish. Its absence is the normal
189
+ // case for anyone but the engine's own maintainer, not a broken install.
190
+ export async function readJsonObject(path: string): Promise<JsonObject> {
191
+ try {
192
+ const value: unknown = await Bun.file(path).json();
193
+ if (!isObject(value)) throw new Error("root value is not an object");
194
+ return value;
195
+ } catch (error) {
196
+ throw new OrlyError(`cannot read JSON object from ${path}: ${String(error)}`);
197
+ }
198
+ }
199
+
200
+ export function isObject(value: unknown): value is JsonObject {
201
+ return typeof value === "object" && value !== null && !Array.isArray(value);
202
+ }
203
+
204
+ export function isString(value: unknown): value is string {
205
+ return typeof value === "string";
206
+ }
207
+
208
+ export function objectValue(value: unknown, label: string): JsonObject {
209
+ if (!isObject(value)) throw new OrlyError(`${label} must be an object`);
210
+ return value;
211
+ }
212
+
213
+ export function stringArray(value: unknown, label: string): string[] {
214
+ if (!Array.isArray(value) || !value.every(isString)) throw new OrlyError(`${label} must be an array of strings`);
215
+ return value;
216
+ }
217
+
218
+ export function objectArray(value: unknown, label: string): JsonObject[] {
219
+ if (!Array.isArray(value) || !value.every(isObject)) throw new OrlyError(`${label} must be an array of objects`);
220
+ return value;
221
+ }
222
+
223
+ // What a materialised file is, reduced to the two properties that decide
224
+ // whether it still is what orly wrote: its bytes and whether it is runnable.
225
+ export function hashContent(content: string | Uint8Array): string {
226
+ return new Bun.CryptoHasher(HASH_ALGORITHM).update(content).digest(HASH_ENCODING);
227
+ }
228
+
229
+ export function modeLabel(path: string): string {
230
+ return normalizedMode(path) === EXECUTABLE_BITS ? MODE_EXECUTABLE : MODE_REGULAR;
231
+ }
232
+
233
+ export function applyMode(path: string, mode: string): void {
234
+ chmodSync(path, Number.parseInt(mode, OCTAL));
235
+ }
236
+
237
+ export function normalizedMode(path: string): number {
238
+ return (lstatSync(path).mode & 0o111) === 0 ? NON_EXECUTABLE_MODE : EXECUTABLE_MODE;
239
+ }
240
+
241
+
242
+ export function setNormalizedMode(path: string, source: string): void {
243
+ chmodSync(path, normalizedMode(source));
244
+ }
245
+
@@ -0,0 +1,47 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { referenceClosureErrors, renderProfileText } from "./references";
7
+
8
+ describe("renderProfileText", () => {
9
+ test("keeps selected pack lines and removes their marker", () => {
10
+ const content = "before\nselected <!-- oracle-packs:language.rust -->\nremoved <!-- oracle-packs:language.zig -->\nafter";
11
+
12
+ const rendered = renderProfileText(
13
+ content,
14
+ new Set(["language.rust"]),
15
+ new Set(["language.rust", "language.zig"]),
16
+ "fixture.md",
17
+ );
18
+
19
+ expect(rendered).toBe("before\nselected\nafter");
20
+ });
21
+
22
+ test("rejects unknown pack markers", () => {
23
+ expect(() => renderProfileText(
24
+ "before\n<!-- oracle-packs:language.unknown -->\nafter",
25
+ new Set(["language.rust"]),
26
+ new Set(["language.rust"]),
27
+ "fixture.md",
28
+ )).toThrow("unknown orly pack marker");
29
+ });
30
+ });
31
+
32
+ describe("referenceClosureErrors", () => {
33
+ test("flags missing dispatch references in every rendered Markdown file", async () => {
34
+ const output = mkdtempSync(join(tmpdir(), "orly-references-test-"));
35
+ try {
36
+ mkdirSync(join(output, "docs"));
37
+ const documentPath = join(output, "docs/EXECUTE_DOC_READS.md");
38
+ await Bun.write(documentPath, "| `*.rs` | `dispatch/write_rust.md` |\n");
39
+
40
+ const errors = await referenceClosureErrors(output, [documentPath]);
41
+
42
+ expect(errors).toContain("missing dispatch reference: docs/EXECUTE_DOC_READS.md:1 -> dispatch/write_rust.md");
43
+ } finally {
44
+ rmSync(output, { recursive: true, force: true });
45
+ }
46
+ });
47
+ });
@@ -0,0 +1,147 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
3
+
4
+ import { isBelow, OrlyError } from "./model";
5
+
6
+ const PACK_LINE = /^(.*?)[ \t]*<!--[ \t]*oracle-packs:([^>]+)[ \t]*-->[ \t]*$/;
7
+ const PACK_START = /^[ \t]*<!--[ \t]*oracle-packs:start ([^>]+)[ \t]*-->[ \t]*$/;
8
+ const PACK_END = /^[ \t]*<!--[ \t]*oracle-packs:end[ \t]*-->[ \t]*$/;
9
+ const CODE_FENCE = /^[ \t]*(?:```|~~~)/;
10
+ const MARKDOWN_LINK = /\[[^\]]*\]\(([^)]+)\)/g;
11
+ const DISPATCH_REFERENCE = /dispatch\/[A-Za-z0-9_.-]+\.md/g;
12
+ const COMMENT_OPEN = "<!--";
13
+ const COMMENT_CLOSE = "-->";
14
+ const NEWLINE = "\n";
15
+
16
+ export function renderProfileText(
17
+ content: string,
18
+ selectedPacks: Set<string>,
19
+ knownPacks: Set<string>,
20
+ source: string,
21
+ ): string {
22
+ return walkPackMarkers(content, selectedPacks, knownPacks, source).join(NEWLINE).trim();
23
+ }
24
+
25
+ function walkPackMarkers(
26
+ content: string,
27
+ selectedPacks: Set<string>,
28
+ knownPacks: Set<string>,
29
+ source: string,
30
+ ): string[] {
31
+ const rendered: string[] = [];
32
+ let activeBlock: string[] | undefined;
33
+ let includeBlock = true;
34
+ const lines = content.split(/\r?\n/);
35
+ for (const [index, line] of lines.entries()) {
36
+ const lineNumber = index + 1;
37
+ const start = line.match(PACK_START);
38
+ if (start) {
39
+ if (activeBlock) throw new OrlyError(`${source}:${lineNumber}: nested orly pack block`);
40
+ activeBlock = packNames(start[1] ?? "", knownPacks, source, lineNumber);
41
+ includeBlock = activeBlock.some((name) => selectedPacks.has(name));
42
+ continue;
43
+ }
44
+ if (PACK_END.test(line)) {
45
+ if (!activeBlock) throw new OrlyError(`${source}:${lineNumber}: unmatched orly pack block end`);
46
+ activeBlock = undefined;
47
+ includeBlock = true;
48
+ continue;
49
+ }
50
+ if (activeBlock && !includeBlock) continue;
51
+ const match = line.match(PACK_LINE);
52
+ if (match) {
53
+ const names = packNames(match[2] ?? "", knownPacks, source, lineNumber);
54
+ if (names.some((name) => selectedPacks.has(name))) rendered.push((match[1] ?? "").trimEnd());
55
+ continue;
56
+ }
57
+ rendered.push(line);
58
+ }
59
+ if (activeBlock) throw new OrlyError(`${source}: unclosed orly pack block`);
60
+ return rendered;
61
+ }
62
+
63
+ // `fallbackRoot` is the repository the staged tree is about to land in. A file
64
+ // the install skipped because its source and target are the same path — the
65
+ // engine checkout updating itself — is absent from the stage but present in
66
+ // that repository, and a citation of it resolves. Without the fallback, a
67
+ // self-update fails closure against files it deliberately left alone.
68
+ export async function referenceClosureErrors(
69
+ outputRoot: string,
70
+ renderedPaths: string[],
71
+ fallbackRoot?: string,
72
+ ): Promise<string[]> {
73
+ const errors = new Set<string>();
74
+ const root = resolve(outputRoot);
75
+ const present = (path: string): boolean =>
76
+ existsSync(path) || (fallbackRoot !== undefined && existsSync(join(fallbackRoot, relative(root, path))));
77
+ for (const sourcePath of renderedPaths.filter((path) => extname(path) === ".md").sort()) {
78
+ const relativeSource = relative(outputRoot, sourcePath).replaceAll("\\", "/");
79
+ const content = await Bun.file(sourcePath).text();
80
+ let inComment = false;
81
+ let inFence = false;
82
+ let fenceOpenedAt: number | undefined;
83
+ for (const [index, line] of content.split(/\r?\n/).entries()) {
84
+ const lineNumber = index + 1;
85
+ // A fenced block is a code sample, not prose citing a file. A shell
86
+ // one-liner carrying `sed -E 's#.*[:/]([^:/]+/[^/]+)$#\1#'` matches the
87
+ // markdown-link shape exactly, and grading it demanded a file named
88
+ // after the regex.
89
+ if (CODE_FENCE.test(line)) {
90
+ fenceOpenedAt = inFence ? undefined : lineNumber;
91
+ inFence = !inFence;
92
+ continue;
93
+ }
94
+ if (inFence) continue;
95
+ const commented = inComment;
96
+ inComment = commentStateAfter(line, inComment);
97
+ // A path inside an HTML comment is authoring guidance — `docs/TEMPLATE.md`
98
+ // lists a per-surface menu of dispatch façades a spec author might cite.
99
+ // Grading those as live citations would force every repository to carry
100
+ // every language pack just to satisfy a comment naming the alternatives.
101
+ if (commented || inComment) continue;
102
+ for (const match of line.matchAll(MARKDOWN_LINK)) {
103
+ const rawTarget = match[1] ?? "";
104
+ const target = markdownTarget(rawTarget);
105
+ if (!target) continue;
106
+ const resolved = resolve(dirname(sourcePath), target);
107
+ if (!isBelow(resolved, root)) errors.add(`snapshot reference escapes repository: ${relativeSource}:${lineNumber} -> ${rawTarget}`);
108
+ else if (!present(resolved)) errors.add(`missing snapshot reference: ${relativeSource}:${lineNumber} -> ${rawTarget}`);
109
+ }
110
+ for (const target of line.matchAll(DISPATCH_REFERENCE)) {
111
+ const path = target[0];
112
+ if (!present(join(outputRoot, path))) errors.add(`missing dispatch reference: ${relativeSource}:${lineNumber} -> ${path}`);
113
+ }
114
+ }
115
+ // Failing open here would skip every citation after the stray fence and
116
+ // report clean, so an unbalanced fence is itself the finding.
117
+ if (inFence) errors.add(`unclosed code fence: ${relativeSource}:${fenceOpenedAt ?? 0} — every reference after it went ungraded`);
118
+ }
119
+ return [...errors].sort();
120
+ }
121
+
122
+ // Whether the line leaves the scanner inside an HTML comment. Pack markers open
123
+ // and close on one line, so they never flip the state; a `<!-- tpl:` block that
124
+ // runs to a later `-->` does.
125
+ function commentStateAfter(line: string, inComment: boolean): boolean {
126
+ let open = inComment;
127
+ for (let index = 0; index < line.length; index += 1) {
128
+ if (!open && line.startsWith(COMMENT_OPEN, index)) { open = true; index += COMMENT_OPEN.length - 1; continue; }
129
+ if (open && line.startsWith(COMMENT_CLOSE, index)) { open = false; index += COMMENT_CLOSE.length - 1; }
130
+ }
131
+ return open;
132
+ }
133
+
134
+ function packNames(value: string, knownPacks: Set<string>, source: string, lineNumber: number): string[] {
135
+ const names = value.split(",").map((name) => name.trim()).filter(Boolean);
136
+ if (names.length === 0) throw new OrlyError(`${source}:${lineNumber}: orly pack marker must name a pack`);
137
+ const unknown = names.filter((name) => !knownPacks.has(name)).sort();
138
+ if (unknown.length > 0) throw new OrlyError(`${source}:${lineNumber}: unknown orly pack marker: ${unknown.join(", ")}`);
139
+ return names;
140
+ }
141
+
142
+ function markdownTarget(rawTarget: string): string | undefined {
143
+ const first = rawTarget.trim().split(/\s+/, 1)[0]?.replace(/^<|>$/g, "") ?? "";
144
+ const target = decodeURIComponent(first.split("#", 1)[0] ?? "");
145
+ if (!target || isAbsolute(target) || /^(https?:|mailto:|app:)/.test(target)) return undefined;
146
+ return target;
147
+ }
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { resolve } from "node:path";
3
+
4
+ import { RulesModel } from "./model";
5
+ import { Renderer } from "./render";
6
+
7
+ const ROOT = resolve(import.meta.dir, "..");
8
+
9
+ // Pack sets stand in for the profiles they replaced: the renderer now takes the
10
+ // list directly, so a test names exactly the packs whose filtering it asserts.
11
+ const AGENTSFLEET_PACKS = ["universal.authoring", "language.zig", "domain.sql", "product.agentsfleet"];
12
+ const CACHE_KIT_PACKS = ["universal.authoring", "language.rust", "language.shell", "domain.documentation"];
13
+ const DOCS_PACKS = ["universal.authoring", "language.mdx", "domain.documentation", "domain.changelog"];
14
+ const KERNEL_PACKS = ["universal.authoring", "language.zig", "language.rust", "domain.sql"];
15
+ // This repository's own selection, as `.oracle/orly.json` declares it — the
16
+ // root render must stay current against exactly what `orly update` writes.
17
+ const DOTFILES_PACKS = ["universal.authoring", "language.zig", "language.typescript", "language.javascript", "language.rust", "language.go", "language.python", "language.shell", "language.mdx", "domain.sql", "domain.http", "domain.auth", "domain.documentation", "domain.changelog", "workflow.specifications", "workflow.governance", "product.agentsfleet", "persona.indy", "workflow.skills"];
18
+ const DOTFILES_COMMANDS = { conform: [["make", "audit"]], "verify.unit": [["bun", "test", "src"]] };
19
+
20
+ describe("Renderer", () => {
21
+ test("renders byte-stable text for a pack set", async () => {
22
+ const renderer = new Renderer(await RulesModel.load(ROOT));
23
+
24
+ expect(await renderer.renderText(AGENTSFLEET_PACKS)).toBe(await renderer.renderText(AGENTSFLEET_PACKS));
25
+ });
26
+
27
+ test("emits CommonMark and Markdown JSX compatible headers", async () => {
28
+ const text = await new Renderer(await RulesModel.load(ROOT)).renderText(DOCS_PACKS);
29
+
30
+ expect(text.startsWith("> **Generated by `orly`.**")).toBeTrue();
31
+ expect(text).not.toContain("Ruleset digest");
32
+ });
33
+
34
+ test("filters unselected pack rows from the core document", async () => {
35
+ const renderer = new Renderer(await RulesModel.load(ROOT));
36
+ const agentsfleet = await renderer.renderText(AGENTSFLEET_PACKS);
37
+
38
+ expect(agentsfleet).toContain("dispatch/write_zig.md");
39
+ expect(agentsfleet).not.toContain("dispatch/write_rust.md");
40
+
41
+ const cacheKit = await renderer.renderText(CACHE_KIT_PACKS);
42
+ expect(cacheKit).toContain("dispatch/write_rust.md");
43
+ expect(cacheKit).not.toContain("dispatch/write_zig.md");
44
+ });
45
+
46
+ test("the root render is current and reference-closed", async () => {
47
+ const renderer = new Renderer(await RulesModel.load(ROOT));
48
+
49
+ expect(await renderer.rootErrors(DOTFILES_PACKS, DOTFILES_COMMANDS)).toEqual([]);
50
+ });
51
+
52
+ test("an unclosed pack block fails the render", async () => {
53
+ const source = await RulesModel.load(ROOT);
54
+ const registry = structuredClone(source.registry);
55
+ registry.core_documents = ["fixtures/unclosed-pack-block.md"];
56
+ const model = new RulesModel(source.root, registry);
57
+
58
+ expect(new Renderer(model).renderText(KERNEL_PACKS)).rejects.toThrow("unclosed orly pack block");
59
+ });
60
+ });
package/src/render.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { lstatSync, mkdirSync } from "node:fs";
2
+ import { dirname, join, relative } from "node:path";
3
+
4
+ import {
5
+ isString,
6
+ JsonObject,
7
+ objectValue,
8
+ OrlyError,
9
+ RulesModel,
10
+ stringArray,
11
+ } from "./model";
12
+ import { referenceClosureErrors, renderProfileText } from "./references";
13
+
14
+ const NEWLINE = "\n";
15
+ const REGISTRY_PACKS_LABEL = "registry packs";
16
+ const TABLE_SEPARATOR = "|---|---|";
17
+ const AGENTS_FILENAME = "AGENTS.md";
18
+
19
+ // One render target: the repository root's AGENTS.md. Dispatch façades and
20
+ // docs are referenced in place — they are the sources, not copies — so the
21
+ // only generated artifact is the one file agent runtimes auto-load.
22
+ export class Renderer {
23
+ readonly model: RulesModel;
24
+
25
+ constructor(model: RulesModel) {
26
+ this.model = model;
27
+ }
28
+
29
+ async renderText(packs: string[], commands: Record<string, string[][]> = {}, projectRoot?: string): Promise<string> {
30
+ const sections = [
31
+ "> **Generated by `orly`.**\n> Do not edit. Update the source rule in `core/` or the pack it names,\n> then commit — every clone reads the committed rules.",
32
+ ];
33
+ const { selected, known } = this.packSelection(packs);
34
+ for (const document of stringArray(this.model.registry.core_documents, "core documents")) {
35
+ sections.push(renderProfileText(await Bun.file(join(this.model.root, document)).text(), selected, known, document));
36
+ }
37
+ sections.push(this.packTable(packs));
38
+ sections.push(this.commandTable(commands));
39
+ if (projectRoot) {
40
+ const projectPath = join(projectRoot, "AGENTS.project.md");
41
+ if (await Bun.file(projectPath).exists()) sections.push((await Bun.file(projectPath).text()).trim());
42
+ }
43
+ return `${sections.filter(Boolean).join("\n\n---\n\n")}\n`;
44
+ }
45
+
46
+ // Currency check: the committed root AGENTS.md must byte-match the render.
47
+ async rootErrors(packs: string[], commands: Record<string, string[][]> = {}): Promise<string[]> {
48
+ const target = join(this.model.root, AGENTS_FILENAME);
49
+ if (!(await Bun.file(target).exists())) return [`missing generated rules: ${relative(this.model.root, target)}`];
50
+ if (lstatSync(target).isSymbolicLink()) return [`generated rules must be a real file: ${AGENTS_FILENAME}`];
51
+ const expected = await this.renderText(packs, commands);
52
+ const actual = await Bun.file(target).text();
53
+ if (expected !== actual) return [`generated rules are stale: run \`orly update\``];
54
+ return [];
55
+ }
56
+
57
+ private packSelection(packs: string[]): { selected: Set<string>; known: Set<string> } {
58
+ return { selected: new Set(packs), known: new Set(Object.keys(objectValue(this.model.registry.packs, REGISTRY_PACKS_LABEL))) };
59
+ }
60
+
61
+ private packTable(selected: string[]): string {
62
+ const rows = ["# Selected rule packs", "", "| Pack | Extensions |", TABLE_SEPARATOR];
63
+ const packs = objectValue(this.model.registry.packs, REGISTRY_PACKS_LABEL);
64
+ if (selected.length === Object.keys(packs).length) rows.push("| All registered packs | Full source inventory |");
65
+ else for (const name of selected) {
66
+ const pack = objectValue(packs[name], `pack ${name}`);
67
+ const extensions = stringArray(pack.extensions, `pack ${name} extensions`).join(", ") || "path or action trigger";
68
+ rows.push(`| \`${name}\` | ${extensions} |`);
69
+ }
70
+ if (selected.length === 0) rows.push("| None | Global behavior only |");
71
+ return rows.join(NEWLINE);
72
+ }
73
+
74
+ private commandTable(commands: Record<string, string[][]>): string {
75
+ const rows = ["# Repository commands", "", "| Responsibility | Commands |", TABLE_SEPARATOR];
76
+ for (const name of Object.keys(commands).sort()) {
77
+ const invocations = commands[name];
78
+ if (!Array.isArray(invocations)) continue;
79
+ const rendered = invocations.map((invocation) => Array.isArray(invocation) ? `\`${invocation.map((argument) => quoteArgument(String(argument))).join(" ")}\`` : "").join("<br>");
80
+ rows.push(`| \`${name}\` | ${rendered} |`);
81
+ }
82
+ if (Object.keys(commands).length === 0) rows.push("| None | Declare them in `.oracle/orly.json` |");
83
+ return rows.join(NEWLINE);
84
+ }
85
+ }
86
+
87
+ async function writeText(path: string, content: string): Promise<void> {
88
+ mkdirSync(dirname(path), { recursive: true });
89
+ await Bun.write(path, content);
90
+ }
91
+
92
+ function quoteArgument(value: string): string {
93
+ return /^[A-Za-z0-9_./:=+-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
94
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { isCode, isDocs, isUserSurface } from "./surfaces";
4
+
5
+ const USER_PREFIXES = ["src/agentsfleetd/http/", "public/openapi/", "cli/src/", "ui/packages/app/"];
6
+ const DOCS_PREFIXES = ["README.md", "docs/"];
7
+
8
+ describe("user surface", () => {
9
+ test("handlers, openapi, cli, and app TypeScript count", () => {
10
+ expect(isUserSurface("src/agentsfleetd/http/handlers/fleets.zig", USER_PREFIXES)).toBeTrue();
11
+ expect(isUserSurface("public/openapi/paths/fleets.yaml", USER_PREFIXES)).toBeTrue();
12
+ expect(isUserSurface("cli/src/commands/deploy.ts", USER_PREFIXES)).toBeTrue();
13
+ expect(isUserSurface("ui/packages/app/pages/fleet.tsx", USER_PREFIXES)).toBeTrue();
14
+ });
15
+
16
+ test("tests, markdown, and other packages do not count", () => {
17
+ expect(isUserSurface("src/agentsfleetd/http/handlers/fleets.test.zig", USER_PREFIXES)).toBeFalse();
18
+ expect(isUserSurface("ui/packages/app/tests/fleet.spec.ts", USER_PREFIXES)).toBeFalse();
19
+ expect(isUserSurface("public/openapi/AGENTS.md", USER_PREFIXES)).toBeFalse();
20
+ expect(isUserSurface("ui/packages/website/pages/index.tsx", USER_PREFIXES)).toBeFalse();
21
+ expect(isUserSurface("src/lib/pool.zig", USER_PREFIXES)).toBeFalse();
22
+ });
23
+ });
24
+
25
+ describe("docs surface", () => {
26
+ test("docs pages and README count; the spec tree never does", () => {
27
+ expect(isDocs("docs/REST_API_DESIGN_GUIDELINES.md", DOCS_PREFIXES)).toBeTrue();
28
+ expect(isDocs("README.md", DOCS_PREFIXES)).toBeTrue();
29
+ expect(isDocs("docs/v1/active/M01_001_P2_CLI_DOCS_PROCESS_AS_CODE.md", DOCS_PREFIXES)).toBeFalse();
30
+ expect(isDocs("docs/v2/pending/M52_001_P2_API_X.md", DOCS_PREFIXES)).toBeFalse();
31
+ expect(isDocs("src/agentsfleetd/main.zig", DOCS_PREFIXES)).toBeFalse();
32
+ });
33
+ });
34
+
35
+ describe("code classification", () => {
36
+ test("source and test files are code; prose is not", () => {
37
+ expect(isCode("src/lifecycle.ts")).toBeTrue();
38
+ expect(isCode("src/agentsfleetd/tests.zig")).toBeTrue();
39
+ expect(isCode("ui/packages/app/tests/fleet.spec.ts")).toBeTrue();
40
+ expect(isCode("docs/VERIFY_TIERS.md")).toBeFalse();
41
+ expect(isCode("SOUL.md")).toBeFalse();
42
+ });
43
+ });