@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
@@ -0,0 +1,89 @@
1
+ import { isObject, JsonObject, stringArray } from "./model";
2
+ import { UNSCOPED_ENVIRONMENT } from "./git_env";
3
+
4
+ const PIPE_OUTPUT = "pipe";
5
+ const DEFAULT_BRANCHES = ["main", "master"];
6
+ const ORIGIN_PREFIX = "origin/";
7
+ const SURFACES_FIELD = "surfaces";
8
+ const USER_FIELD = "user";
9
+ const DOCS_FIELD = "docs";
10
+ const SPEC_TREE = /^docs\/v[0-9]+\//;
11
+ const TEST_PATH = /(^|\/)tests?\/|\.test\.|_test\.|\.spec\./;
12
+ const MARKDOWN = /\.(md|mdx)$/;
13
+ // One fixed source-extension set across every repo: handlers (.zig), CLI and
14
+ // UI TypeScript, OpenAPI YAML, scripts. A fixed set keeps the check identical
15
+ // everywhere instead of drifting per repository.
16
+ const SOURCE_EXTENSIONS = /\.(zig|ts|tsx|js|jsx|mjs|py|rs|go|sh|sql|yaml|yml|json|toml)$/;
17
+
18
+ export type SurfaceReport = {
19
+ changed: string[];
20
+ code: string[];
21
+ userSurface: string[];
22
+ docs: string[];
23
+ };
24
+
25
+ // Classify the branch diff once per inspection; every criterion reads this
26
+ // report instead of running its own git commands.
27
+ export function classifyBranch(root: string, surfaces: JsonObject | undefined): SurfaceReport {
28
+ const changed = branchDiff(root);
29
+ const user = surfacePrefixes(surfaces, USER_FIELD);
30
+ const docs = surfacePrefixes(surfaces, DOCS_FIELD);
31
+ return {
32
+ changed,
33
+ code: changed.filter(isCode),
34
+ userSurface: changed.filter((path) => isUserSurface(path, user)),
35
+ docs: changed.filter((path) => isDocs(path, docs)),
36
+ };
37
+ }
38
+
39
+ export function isCode(path: string): boolean {
40
+ return !MARKDOWN.test(path) && (SOURCE_EXTENSIONS.test(path) || TEST_PATH.test(path));
41
+ }
42
+
43
+ // User surface: a repository-declared prefix, a real source file, and not a test —
44
+ // a test-only change under a user prefix must not demand a docs edit, or the
45
+ // gate trains everyone to distrust it.
46
+ export function isUserSurface(path: string, prefixes: string[]): boolean {
47
+ if (!prefixes.some((prefix) => path.startsWith(prefix))) return false;
48
+ return SOURCE_EXTENSIONS.test(path) && !TEST_PATH.test(path) && !MARKDOWN.test(path);
49
+ }
50
+
51
+ // Docs: a repository-declared prefix, but never the spec tree — every milestone
52
+ // edits its own spec, so counting docs/v*/ would make the check auto-pass.
53
+ export function isDocs(path: string, prefixes: string[]): boolean {
54
+ if (SPEC_TREE.test(path)) return false;
55
+ return prefixes.some((prefix) => path.startsWith(prefix));
56
+ }
57
+
58
+ export function branchDiff(root: string): string[] {
59
+ const base = defaultMergeBase(root);
60
+ if (!base) return [];
61
+ const output = gitOutput(root, ["diff", "--name-only", `${base}..HEAD`]);
62
+ return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
63
+ }
64
+
65
+ export function defaultMergeBase(root: string): string {
66
+ for (const branch of defaultBranchCandidates(root)) {
67
+ const base = gitOutput(root, ["merge-base", branch, "HEAD"]);
68
+ if (base) return base;
69
+ }
70
+ return "";
71
+ }
72
+
73
+ function defaultBranchCandidates(root: string): string[] {
74
+ const candidates: string[] = [];
75
+ const head = gitOutput(root, ["rev-parse", "--abbrev-ref", "origin/HEAD"]);
76
+ if (head.startsWith(ORIGIN_PREFIX)) candidates.push(head);
77
+ for (const name of DEFAULT_BRANCHES) candidates.push(`${ORIGIN_PREFIX}${name}`, name);
78
+ return candidates;
79
+ }
80
+
81
+ function surfacePrefixes(surfaces: JsonObject | undefined, field: string): string[] {
82
+ if (!isObject(surfaces) || !(field in surfaces)) return [];
83
+ return stringArray(surfaces[field], `${SURFACES_FIELD}.${field}`);
84
+ }
85
+
86
+ function gitOutput(root: string, command: string[]): string {
87
+ const result = Bun.spawnSync(["git", ...command], { cwd: root, env: UNSCOPED_ENVIRONMENT, stdout: PIPE_OUTPUT, stderr: PIPE_OUTPUT });
88
+ return result.exitCode === 0 ? result.stdout.toString().trim() : "";
89
+ }
@@ -0,0 +1,11 @@
1
+ import { expect, test } from "bun:test";
2
+
3
+ import { validateRelativePath } from "./validation";
4
+
5
+ test("relative paths cannot escape the output root", () => {
6
+ const errors: string[] = [];
7
+
8
+ validateRelativePath("../outside.md", "fixture", errors);
9
+
10
+ expect(errors).toEqual(["fixture must stay below the output root: ../outside.md"]);
11
+ });
@@ -0,0 +1,87 @@
1
+ import { existsSync } from "node:fs";
2
+ import { isAbsolute, join, relative } from "node:path";
3
+
4
+ import type { JsonObject } from "./model";
5
+
6
+ export function validateRelativePath(value: unknown, label: string, errors: string[]): void {
7
+ if (!isString(value) || value.length === 0) {
8
+ errors.push(`${label} must be a string`);
9
+ return;
10
+ }
11
+ if (isAbsolute(value) || relative(".", value).split(/[\\/]/).includes("..")) errors.push(`${label} must stay below the output root: ${value}`);
12
+ }
13
+
14
+ // Optional diff-surface prefixes a repository declares: surfaces.user (paths
15
+ // whose change demands a docs update) and surfaces.docs (paths that count as
16
+ // that update).
17
+ export function validateSurfaces(label: string, value: unknown, errors: string[]): void {
18
+ if (value === undefined) return;
19
+ if (!isObject(value)) {
20
+ errors.push(`${label} surfaces must be an object`);
21
+ return;
22
+ }
23
+ for (const [field, prefixes] of Object.entries(value)) {
24
+ if (field !== "user" && field !== "docs") errors.push(`${label} surfaces.${field} is not a known surface`);
25
+ else if (!Array.isArray(prefixes) || !prefixes.every((prefix) => isString(prefix) && prefix.length > 0)) {
26
+ errors.push(`${label} surfaces.${field} must be an array of path prefixes`);
27
+ }
28
+ }
29
+ }
30
+
31
+ export function validateCommands(label: string, value: unknown, errors: string[]): void {
32
+ if (!isObject(value)) {
33
+ errors.push(`${label} commands must be an object`);
34
+ return;
35
+ }
36
+ for (const [name, invocations] of Object.entries(value)) {
37
+ if (!Array.isArray(invocations) || invocations.length === 0) {
38
+ errors.push(`${label} command ${name} must be non-empty`);
39
+ continue;
40
+ }
41
+ for (const invocation of invocations) if (!Array.isArray(invocation) || invocation.length === 0 || !invocation.every((argument) => isString(argument) && argument.length > 0)) errors.push(`${label} command ${name} arguments must be strings`);
42
+ }
43
+ }
44
+
45
+ export function validateActiveRule(
46
+ key: string,
47
+ rule: JsonObject,
48
+ root: string,
49
+ errors: string[],
50
+ ): void {
51
+ if (rule.decision === "mechanical") {
52
+ validateMechanicalRule(key, rule, root, errors);
53
+ return;
54
+ }
55
+ if (rule.decision === "repository") {
56
+ if (!isString(rule.command) || rule.command.length === 0) errors.push(`repository rule ${key} needs a command name`);
57
+ return;
58
+ }
59
+ if (rule.decision !== "judgment") errors.push(`rule ${key} has invalid decision ${String(rule.decision)}`);
60
+ }
61
+
62
+ function validateMechanicalRule(key: string, rule: JsonObject, root: string, errors: string[]): void {
63
+ if (!Array.isArray(rule.checker) || rule.checker.length === 0) errors.push(`mechanical rule ${key} needs a checker`);
64
+ if (!isObject(rule.fixtures)) {
65
+ errors.push(`mechanical rule ${key} needs fixtures`);
66
+ return;
67
+ }
68
+ for (const kind of ["pass", "fail"]) {
69
+ const fixtures = rule.fixtures[kind];
70
+ if (!Array.isArray(fixtures) || fixtures.length === 0) {
71
+ errors.push(`mechanical rule ${key} needs ${kind} fixtures`);
72
+ continue;
73
+ }
74
+ for (const fixture of fixtures) {
75
+ validateRelativePath(fixture, `rule ${key} ${kind} fixture`, errors);
76
+ if (isString(fixture) && !existsSync(join(root, fixture))) errors.push(`rule ${key} ${kind} fixture source is missing: ${fixture}`);
77
+ }
78
+ }
79
+ }
80
+
81
+ function isObject(value: unknown): value is JsonObject {
82
+ return typeof value === "object" && value !== null && !Array.isArray(value);
83
+ }
84
+
85
+ function isString(value: unknown): value is string {
86
+ return typeof value === "string";
87
+ }
@@ -0,0 +1,13 @@
1
+ import { expect, test } from "bun:test";
2
+ import { resolve } from "node:path";
3
+
4
+ import { RulesModel } from "./model";
5
+ import { verifyRenders } from "./verify";
6
+
7
+ const ROOT = resolve(import.meta.dir, "..");
8
+
9
+ test("every profile renders deterministically", async () => {
10
+ const checks = await verifyRenders(await RulesModel.load(ROOT));
11
+
12
+ expect(checks.filter((check) => check.name.endsWith(".idempotent")).every((check) => check.result === "pass")).toBeTrue();
13
+ });
package/src/verify.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ import { localSelection } from "./config";
5
+ import { objectValue, RulesModel } from "./model";
6
+ import { Renderer } from "./render";
7
+
8
+ const PASS_RESULT = "pass";
9
+ const FAIL_RESULT = "fail";
10
+
11
+ export type VerificationCheck = {
12
+ name: string;
13
+ result: typeof PASS_RESULT | typeof FAIL_RESULT;
14
+ detail?: string;
15
+ };
16
+
17
+ // Two proofs: every profile renders the same bytes twice (determinism), and
18
+ // the committed root AGENTS.md matches its render (currency). No stored
19
+ // hashes — the render itself is the reference.
20
+ export async function verifyRenders(model: RulesModel): Promise<VerificationCheck[]> {
21
+ model.validate();
22
+ const checks: VerificationCheck[] = [];
23
+ const renderer = new Renderer(model);
24
+ const local = await localSelection(model, model.root);
25
+ const everything = Object.keys(objectValue(model.registry.packs, "registry packs")).sort();
26
+ for (const [name, packs] of [["local", local.packs], ["all", everything]] as Array<[string, string[]]>) {
27
+ const first = await renderer.renderText(packs, local.commands);
28
+ const second = await renderer.renderText(packs, local.commands);
29
+ checks.push({
30
+ name: `render.${name}.idempotent`,
31
+ result: first === second ? PASS_RESULT : FAIL_RESULT,
32
+ });
33
+ }
34
+ const errors = await renderer.rootErrors(local.packs, local.commands);
35
+ checks.push({ name: "generated.root.current", result: errors.length === 0 ? PASS_RESULT : FAIL_RESULT, detail: errors.join("; ") });
36
+ return checks;
37
+ }
38
+
39
+ export async function writeEvidence(
40
+ model: RulesModel,
41
+ profile: string,
42
+ checks: VerificationCheck[],
43
+ languageModelResult: "pass" | "not-required",
44
+ ): Promise<string> {
45
+ const path = join(model.root, ".oracle/evidence.json");
46
+ mkdirSync(dirname(path), { recursive: true });
47
+ const commit = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: model.root, stdout: "pipe", stderr: "ignore" });
48
+ const evidence = {
49
+ schema_version: 1,
50
+ profile,
51
+ source_commit: commit.exitCode === 0 ? commit.stdout.toString().trim() : "uncommitted",
52
+ result: checks.every((check) => check.result === PASS_RESULT) ? PASS_RESULT : FAIL_RESULT,
53
+ checks,
54
+ llm_result: languageModelResult,
55
+ created_at: new Date().toISOString().replace(/\.\d{3}Z$/, "+00:00"),
56
+ };
57
+ await Bun.write(path, `${JSON.stringify(evidence, null, 2)}\n`);
58
+ return path;
59
+ }