@agentsfleet/orly 0.6.1 → 0.6.3

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/AGENTS.md CHANGED
@@ -20,7 +20,7 @@ You are this repository's coding agent: deterministic, autonomous, command-line-
20
20
 
21
21
  MacBook. Languages: Python, Go, Rust, TypeScript, Zig. Tooling: `mise` first, `brew` fallback. Forges: `gh`/`glab`. Commit identity from `git config`, never here.
22
22
 
23
- Dates: `MMM DD, YYYY: HH:MM AM/PM`. Filenames: `{MMM}_{DD}_{HH_MM}`.
23
+ Dates in session notes and log entries: `MMM DD, YYYY: HH:MM AM/PM`. Filenames: `{MMM}_{DD}_{HH_MM}`. Published pages use ISO 8601 (`DOC-23`).
24
24
 
25
25
  **Banned vocabulary:** "contract" and "phase". Use **Prototype → Milestone → Workstream → Section → Dimension → Batch** for the hierarchy; **Punch List**/**Slices** for finer units; **stages** for lifecycle steps (CHORE(open), PLAN, etc.); **rules** / **operating model** for what AGENTS.md enforces. Real-world commercial agreements keep "contract" only when no clearer term exists — prefer `external commitment` / `vendor agreement`.
26
26
 
@@ -28,10 +28,13 @@ Dates: `MMM DD, YYYY: HH:MM AM/PM`. Filenames: `{MMM}_{DD}_{HH_MM}`.
28
28
 
29
29
  **Pre-send self-checks (invariant).** Before any message or durable artifact: scan `\b[A-Z][A-Z0-9]{1,5}\b` for unexpanded acronyms, and whole-word **`phase`** / **`contract`** for banned vocabulary. Skipping = `ACRONYM CHECK: SKIPPED per user override (reason: ...)` / `BANNED-VOCAB CHECK: SKIPPED per user override (reason: ...)`; reasonable only for a verbatim quote that would be distorted, or a real commercial agreement with no clearer word.
30
30
 
31
+
31
32
  **Evidence invariant (pre-send).** Every claim about repository or system state carries its source where the claim is made: `file:line`, the command run and the line of output that decided it, or the `git` / `gh` result. A claim I have not checked opens with `unverified:` and names what would settle it. Numbers name the command that produced them. "The tests pass", "it is merged", "nothing else reads this file", and any count are claims, not context. Skipping = `EVIDENCE CHECK: SKIPPED per user override (reason: ...)`.
32
33
 
33
34
  **Why it is an invariant, not a preference.** An unchecked claim reads exactly like a checked one. Indy cannot tell them apart, so he either trusts a guess or re-derives the work himself, and both cost more than the check did. When the evidence is not reachable, say which decision is blocked on it rather than filling the hole with a plausible sentence. A wrong fact I stated confidently is the one failure he has no defence against.
34
35
 
36
+ **Self-repair boundary (invariant).** An agent repairs a finding unprompted only when the repair has exactly one form and the failure itself names it — a regenerate command, a named set of allowed resolutions, a restore command such as `orly update` for a managed file reported missing. Everything else is reported, not repaired. A finding that turns on what a rule *means*, what a word is worth, or whether the rule is calibrated right is the user's call: report it with the evidence — the finding, its count, the pages it lands on — and stop. Never weaken a rule, add a suppression, or grandfather findings into a baseline to make a check pass; a baseline that excuses existing violations is a list of work pretending to be a gate (patching a harness to silence its hit is already forbidden under Hard Safety — this draws the same line at the finding rather than at the gate). An agent never clears its own gate: it raises findings and repairs the single-form ones, while merging, overriding, and accepting a deferral stay with the user — a worker that both raises and clears its own findings is not gated at all. Detection is always safe and should run continuously; repair is the bounded half. `orly gate`'s `docs.language` criterion is the working shape — it reports a counted finding list and changes nothing.
37
+
35
38
  ## Documentation voice
36
39
 
37
40
  Published docs and OpenAPI prose read `docs/DOCUMENTATION_RULES.md` first via `write_documentation`.
package/README.md CHANGED
@@ -129,11 +129,39 @@ Three checkpoints, all running your own declared commands:
129
129
  The spec is a file on disk, and **its directory is the status**. Only the stages named in the rules move it.
130
130
 
131
131
  ```mermaid
132
- flowchart LR
133
- you["🤠 you: add webhook retries"] --> agent["🦉 agent writes the spec"]
134
- agent --> pending["docs/v1/pending/"]
135
- pending -->|"CHORE(open)"| active["docs/v1/active/"]
136
- active -->|"CHORE(close)"| done["docs/v1/done/"]
132
+ flowchart TB
133
+ you["🤠 you<br/>add webhook retries"]
134
+ agent["🦉 agent<br/>writes the spec"]
135
+ pending["📄 docs/v1/pending/<br/><small>spec committed on main</small>"]
136
+
137
+ you --> agent
138
+ agent --> pending
139
+ pending -->|"CHORE(open)"| plan
140
+
141
+ subgraph active["⚙️ docs/v1/active/"]
142
+ direction TB
143
+
144
+ plan["PLAN<br/><small>understand spec · record baseline</small>"]
145
+ execute["EXECUTE<br/><small>make the change</small>"]
146
+ conform["CONFORM<br/><small>apply matching rule pages</small>"]
147
+ verify["VERIFY<br/><small>run deterministic gates</small>"]
148
+ review["REVIEW<br/><small>review the resulting diff</small>"]
149
+ document["DOCUMENT<br/><small>update docs / evidence</small>"]
150
+ commit["COMMIT<br/><small>record the completed unit</small>"]
151
+
152
+ plan --> execute
153
+ execute --> conform
154
+ conform --> verify
155
+ verify --> review
156
+ review --> document
157
+ document --> commit
158
+
159
+ conform -. "gate fails" .-> execute
160
+ verify -. "gate fails" .-> execute
161
+ review -. "changes required" .-> execute
162
+ end
163
+
164
+ commit -->|"CHORE(close)"| done["✅ docs/v1/done/<br/><small>all gates green</small>"]
137
165
  done --> pr["Pull Request"]
138
166
  ```
139
167
 
@@ -143,11 +171,19 @@ flowchart LR
143
171
  | `docs/v1/active/` | branch cut, baseline recorded, no code until it commits |
144
172
  | `docs/v1/done/` | gates green, Pull Request opens |
145
173
 
146
- Inside `active/`, work runs through: **PLAN → EXECUTE → CONFORM → VERIFY → REVIEW → DOCUMENT → COMMIT**.
174
+ Inside `active/`, work runs through the stages: **PLAN → EXECUTE → CONFORM → VERIFY → REVIEW → DOCUMENT → COMMIT**.
147
175
 
148
- Each edit trips the rule page for its file kind. A red gate sends the agent back to EXECUTE. No stage can be skipped quietly.
176
+ The stages are not a status. `active/` is the status. The stages are the loop that runs on the spec sitting there.
149
177
 
150
- ---
178
+ Three things do three different jobs, and keeping them apart is what makes the lifecycle mechanical:
179
+
180
+ | | Job |
181
+ |---|---|
182
+ | **Directories** | lifecycle state — where the spec sits |
183
+ | **Stages** | execution machinery — what moves the work |
184
+ | **Gates** | transition authority — whether it may advance |
185
+
186
+ Each edit activates the rule page for that file kind. A gate proves whether the work may advance, and any red returns the agent to EXECUTE. No stage can be skipped quietly.
151
187
 
152
188
  ## Your files stay yours
153
189
 
@@ -31,6 +31,7 @@ EXCLUDED_DOCS=(
31
31
  "docs/DISPATCH_ARCHITECTURE.md"
32
32
  "docs/ORLY_ARCHITECTURE.md"
33
33
  "docs/TEMPLATE.md"
34
+ "docs/RELEASE_TEMPLATE.md"
34
35
  "docs/EXECUTE_DOC_READS.md"
35
36
  "docs/HARNESS_VERIFY_OUTPUT.md"
36
37
  "docs/AUTH.md"
@@ -15,7 +15,7 @@ You are this repository's coding agent: deterministic, autonomous, command-line-
15
15
 
16
16
  MacBook. Languages: Python, Go, Rust, TypeScript, Zig. Tooling: `mise` first, `brew` fallback. Forges: `gh`/`glab`. Commit identity from `git config`, never here.
17
17
 
18
- Dates: `MMM DD, YYYY: HH:MM AM/PM`. Filenames: `{MMM}_{DD}_{HH_MM}`.
18
+ Dates in session notes and log entries: `MMM DD, YYYY: HH:MM AM/PM`. Filenames: `{MMM}_{DD}_{HH_MM}`. Published pages use ISO 8601 (`DOC-23`).
19
19
 
20
20
  **Banned vocabulary:** "contract" and "phase". Use **Prototype → Milestone → Workstream → Section → Dimension → Batch** for the hierarchy; **Punch List**/**Slices** for finer units; **stages** for lifecycle steps (CHORE(open), PLAN, etc.); **rules** / **operating model** for what AGENTS.md enforces. Real-world commercial agreements keep "contract" only when no clearer term exists — prefer `external commitment` / `vendor agreement`.
21
21
 
@@ -23,10 +23,13 @@ Dates: `MMM DD, YYYY: HH:MM AM/PM`. Filenames: `{MMM}_{DD}_{HH_MM}`.
23
23
 
24
24
  **Pre-send self-checks (invariant).** Before any message or durable artifact: scan `\b[A-Z][A-Z0-9]{1,5}\b` for unexpanded acronyms, and whole-word **`phase`** / **`contract`** for banned vocabulary. Skipping = `ACRONYM CHECK: SKIPPED per user override (reason: ...)` / `BANNED-VOCAB CHECK: SKIPPED per user override (reason: ...)`; reasonable only for a verbatim quote that would be distorted, or a real commercial agreement with no clearer word.
25
25
 
26
+ <!-- oracle-packs:end -->
27
+
26
28
  **Evidence invariant (pre-send).** Every claim about repository or system state carries its source where the claim is made: `file:line`, the command run and the line of output that decided it, or the `git` / `gh` result. A claim I have not checked opens with `unverified:` and names what would settle it. Numbers name the command that produced them. "The tests pass", "it is merged", "nothing else reads this file", and any count are claims, not context. Skipping = `EVIDENCE CHECK: SKIPPED per user override (reason: ...)`.
27
29
 
28
30
  **Why it is an invariant, not a preference.** An unchecked claim reads exactly like a checked one. Indy cannot tell them apart, so he either trusts a guess or re-derives the work himself, and both cost more than the check did. When the evidence is not reachable, say which decision is blocked on it rather than filling the hole with a plausible sentence. A wrong fact I stated confidently is the one failure he has no defence against.
29
- <!-- oracle-packs:end -->
31
+
32
+ **Self-repair boundary (invariant).** An agent repairs a finding unprompted only when the repair has exactly one form and the failure itself names it — a regenerate command, a named set of allowed resolutions, a restore command such as `orly update` for a managed file reported missing. Everything else is reported, not repaired. A finding that turns on what a rule *means*, what a word is worth, or whether the rule is calibrated right is the user's call: report it with the evidence — the finding, its count, the pages it lands on — and stop. Never weaken a rule, add a suppression, or grandfather findings into a baseline to make a check pass; a baseline that excuses existing violations is a list of work pretending to be a gate (patching a harness to silence its hit is already forbidden under Hard Safety — this draws the same line at the finding rather than at the gate). An agent never clears its own gate: it raises findings and repairs the single-form ones, while merging, overriding, and accepting a deferral stay with the user — a worker that both raises and clears its own findings is not gated at all. Detection is always safe and should run continuously; repair is the bounded half. `orly gate`'s `docs.language` criterion is the working shape — it reports a counted finding list and changes nothing.
30
33
 
31
34
  ## Documentation voice
32
35
 
@@ -164,7 +164,7 @@ not 36 defects. They keep every other rule.
164
164
  the retry count and delay, what data is lost, and what survives.
165
165
  22. **DOC-22 — Audience boundary.** User pages state guarantees, not internals.
166
166
  Operator pages state exact dependencies and versions.
167
- 23. **DOC-23 — Units and time.** Numbers carry units. Dates use ISO 8601. Times
167
+ 23. **DOC-23 — Units and time.** Numbers carry units. Dates on a published page use ISO 8601. Times
168
168
  use Coordinated Universal Time (UTC).
169
169
  24. **DOC-24 — Stable errors.** Every product error has a stable Identifier (ID)
170
170
  and an anchor that explains why it happened, how to fix it, and how to
@@ -195,10 +195,11 @@ not 36 defects. They keep every other rule.
195
195
 
196
196
  ## Enforcement
197
197
 
198
- 33. **DOC-33 — Mechanical checks.** Pre-commit checks block banned words,
199
- undefined abbreviations, long sentences, long paragraphs, missing front
200
- matter, missing or unordered sections, command blocks without output,
201
- options without defaults, heading violations, and procedures in tables.
198
+ 33. **DOC-33 — Mechanical checks.** `orly gate verify` checks banned words,
199
+ long sentences, long paragraphs, the em-dash budget, and heading order.
200
+ Front matter, section order, command blocks without output, options
201
+ without defaults, and procedures in tables are rules a reader enforces;
202
+ no check reads them yet. This list names what runs, not what is wished.
202
203
  34. **DOC-34 — Review checks.** Review checks one-idea sentences, pronoun
203
204
  clarity, resource lifecycle, failure behavior, implementation leaks, and
204
205
  meaning preserved by simplification.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsfleet/orly",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Repository-scoped engineering harness: renders agent rules, materialises the gates that enforce them, and proves the PR boundary — for any coding agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -182,6 +182,33 @@ describe("managedDrift", () => {
182
182
  expect(findings[1]).toContain("zzz.md");
183
183
  expect(findings[0]).toContain("orly update");
184
184
  });
185
+
186
+ // The failure this pair exists for: agentsfleet ignored .claude/skills/ while
187
+ // .oracle/orly.json declared four files under it managed. Every local
188
+ // checkout passed `orly doctor` because the files sat on disk, and CI failed
189
+ // against a clone that never received them.
190
+ test("a managed file git refuses to track is drift, even though it is present", async () => {
191
+ const root = scratch();
192
+ Bun.spawnSync(["git", "init"], { cwd: root });
193
+ mkdirSync(join(root, ".claude/skills/orly-spec-new"), { recursive: true });
194
+ writeFileSync(join(root, ".claude/skills/orly-spec-new/SKILL.md"), "skill\n");
195
+ writeFileSync(join(root, ".gitignore"), ".claude/skills/\n");
196
+ const config = { ...(await seedConfig(root)), managed: [".claude/skills/orly-spec-new/SKILL.md"] };
197
+ const findings = managedDrift(root, config);
198
+ expect(findings).toHaveLength(1);
199
+ expect(findings[0]).toContain("ignored by git");
200
+ expect(findings[0]).toContain(".claude/skills/orly-spec-new/SKILL.md");
201
+ });
202
+
203
+ test("the same file reports nothing once the ignore is scoped to spare it", async () => {
204
+ const root = scratch();
205
+ Bun.spawnSync(["git", "init"], { cwd: root });
206
+ mkdirSync(join(root, ".claude/skills/orly-spec-new"), { recursive: true });
207
+ writeFileSync(join(root, ".claude/skills/orly-spec-new/SKILL.md"), "skill\n");
208
+ writeFileSync(join(root, ".gitignore"), ".claude/skills/*\n!.claude/skills/orly-spec-new/\n");
209
+ const config = { ...(await seedConfig(root)), managed: [".claude/skills/orly-spec-new/SKILL.md"] };
210
+ expect(managedDrift(root, config)).toEqual([]);
211
+ });
185
212
  });
186
213
 
187
214
  describe("writeConfig", () => {
package/src/config.ts CHANGED
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { dirname, extname, join, relative } from "node:path";
3
3
 
4
4
  import { assertWritableInside, isObject, isString, JsonObject, objectValue, OrlyError, readJsonObject, RulesModel, stringArray } from "./model";
5
+ import { UNSCOPED_ENVIRONMENT } from "./git_env";
5
6
  import { validateCommands, validateSurfaces } from "./validation";
6
7
 
7
8
  const ORACLE_DIRECTORY = ".oracle";
@@ -117,14 +118,41 @@ export function staleVersion(config: RepoConfig, installedVersion: string): stri
117
118
  return `ruleset was installed by orly ${config.orly_version || "an unrecorded version"}, installed engine is ${installedVersion} — run \`orly update\``;
118
119
  }
119
120
 
120
- // A managed file that is gone. Edits are not drift: the tree is a git
121
- // repository, so `git diff` shows a hand edit and shows `orly update` replacing
122
- // it, both before either is committed.
121
+ const PIPE_OUTPUT = "pipe";
122
+
123
+ // Which of these paths git refuses to track. `check-ignore` prints one ignored
124
+ // path per line and exits 1 when none match, which is not an error here.
125
+ function ignoredPaths(targetRoot: string, paths: string[]): Set<string> {
126
+ if (paths.length === 0) return new Set();
127
+ const result = Bun.spawnSync(["git", "check-ignore", "--", ...paths], {
128
+ cwd: targetRoot,
129
+ env: UNSCOPED_ENVIRONMENT,
130
+ stdout: PIPE_OUTPUT,
131
+ stderr: PIPE_OUTPUT,
132
+ });
133
+ const printed = result.stdout.toString().trim();
134
+ if (printed.length === 0) return new Set();
135
+ return new Set(printed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
136
+ }
137
+
138
+ // A managed file that is gone, or one git will not carry. Edits are not drift:
139
+ // the tree is a git repository, so `git diff` shows a hand edit and shows
140
+ // `orly update` replacing it, both before either is committed.
141
+ //
142
+ // An ignored file is drift even though it sits right there on disk. orly wrote
143
+ // it, the local checkout gates on it, and a clone never receives it — so the
144
+ // author passes and everybody else fails, which is the one failure the install
145
+ // record exists to rule out.
123
146
  export function managedDrift(targetRoot: string, config: RepoConfig): string[] {
124
- return config.managed
125
- .filter((relativePath) => !existsSync(join(targetRoot, relativePath)))
126
- .map((relativePath) => `managed file is missing: ${relativePath} — run \`orly update\` to restore it`)
127
- .sort();
147
+ const missing = config.managed.filter((relativePath) => !existsSync(join(targetRoot, relativePath)));
148
+ const present = config.managed.filter((relativePath) => !missing.includes(relativePath));
149
+ const ignored = ignoredPaths(targetRoot, present);
150
+ return [
151
+ ...missing.map((relativePath) => `managed file is missing: ${relativePath} — run \`orly update\` to restore it`),
152
+ ...present
153
+ .filter((relativePath) => ignored.has(relativePath))
154
+ .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`),
155
+ ].sort();
128
156
  }
129
157
 
130
158
  // Key order is fixed so a rewrite produces a reviewable diff: orly's own two