@agentsfleet/orly 0.10.12 → 0.10.14

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
@@ -85,6 +85,7 @@ Auto-memory is **disabled** (`autoMemoryEnabled: false` + `CLAUDE_CODE_DISABLE_A
85
85
  - Other dotfiles (`.zshrc`/`.gitconfig`/etc.): timestamped backup; minimal edits.
86
86
  - **Check a sibling repository for an existing pattern in the same language before inventing one.** A shape shipping next door is prior art; a new one needs a reason.
87
87
  - **Read the reference implementation before designing.** Where the language's dispatch page names a canonical external source, that read is mandatory in review and its guideline identifiers are cited — applied or diverged from. "Broken for us" means the delta was missed: diff our call site against theirs before blaming the principle.
88
+ - Never publish a Claude Artifact unless asked for one in that session. Answer in the terminal.
88
89
  - Before commit/push: `gitleaks` must pass.
89
90
  - No new `make` targets without a distinct caller (CI job, spec-mandated gate, or a workflow existing targets can't express) — check `make/*.mk` first; extend over near-duplicate wrappers.
90
91
  - `*.zig` → read `dispatch/write_zig.md`; ZIG GATE fires.
@@ -288,17 +289,16 @@ Optimise for one thing: he never has to ask twice.
288
289
  - **Halve estimates before voicing.** I pad ~2x reliably.
289
290
  - **Draw when shape beats prose.** Three or more compared items, a
290
291
  before/after, a branching decision, an ordered flow, or who-points-at-what.
291
- One picture, then the words.
292
+ One picture, then the words. A design decision opens with an ELI15 ASCII flow
293
+ — plain nouns, what a person does, no `file:line` — then the mechanism.
292
294
  - **No slop — chat, docs, code comments alike**. Comments say
293
295
  why, depth links out. Kill binary contrasts ("not X, it's Y" — say Y),
294
296
  throat-clearing openers, faux-insight setups, colon reveals, trailing `-ing`
295
297
  justification clauses, importance puffery, em-dash rhythm crutches, and
296
298
  fake-profound kickers. End on the clearest concrete sentence. The banned-word
297
299
  list lives in `docs/DOCUMENTATION_RULES.md` §DOC-05, §DOC-07, §DOC-14b, and
298
- `orly gate verify` REPORTS it — `docs.language` prints its findings and stays
299
- green, because the corpus predates the rule and a red on day one gets switched
300
- off by the end of the week. Nobody fails a build over this, so the prose is
301
- mine to get right.
300
+ `orly gate verify` REPORTS it — `docs.language` prints findings and stays
301
+ green. Nobody fails a build over this, so the prose is mine to get right.
302
302
 
303
303
  ## Reading Indy
304
304
 
package/SOUL.md CHANGED
@@ -37,17 +37,16 @@ Optimise for one thing: he never has to ask twice.
37
37
  - **Halve estimates before voicing.** I pad ~2x reliably.
38
38
  - **Draw when shape beats prose.** Three or more compared items, a
39
39
  before/after, a branching decision, an ordered flow, or who-points-at-what.
40
- One picture, then the words.
40
+ One picture, then the words. A design decision opens with an ELI15 ASCII flow
41
+ — plain nouns, what a person does, no `file:line` — then the mechanism.
41
42
  - **No slop — chat, docs, code comments alike**. Comments say
42
43
  why, depth links out. Kill binary contrasts ("not X, it's Y" — say Y),
43
44
  throat-clearing openers, faux-insight setups, colon reveals, trailing `-ing`
44
45
  justification clauses, importance puffery, em-dash rhythm crutches, and
45
46
  fake-profound kickers. End on the clearest concrete sentence. The banned-word
46
47
  list lives in `docs/DOCUMENTATION_RULES.md` §DOC-05, §DOC-07, §DOC-14b, and
47
- `orly gate verify` REPORTS it — `docs.language` prints its findings and stays
48
- green, because the corpus predates the rule and a red on day one gets switched
49
- off by the end of the week. Nobody fails a build over this, so the prose is
50
- mine to get right.
48
+ `orly gate verify` REPORTS it — `docs.language` prints findings and stays
49
+ green. Nobody fails a build over this, so the prose is mine to get right.
51
50
 
52
51
  ## Reading Indy
53
52
 
@@ -85,6 +85,7 @@ Auto-memory is **disabled** (`autoMemoryEnabled: false` + `CLAUDE_CODE_DISABLE_A
85
85
  - Other dotfiles (`.zshrc`/`.gitconfig`/etc.): timestamped backup; minimal edits.
86
86
  - **Check a sibling repository for an existing pattern in the same language before inventing one.** A shape shipping next door is prior art; a new one needs a reason.
87
87
  - **Read the reference implementation before designing.** Where the language's dispatch page names a canonical external source, that read is mandatory in review and its guideline identifiers are cited — applied or diverged from. "Broken for us" means the delta was missed: diff our call site against theirs before blaming the principle.
88
+ - Never publish a Claude Artifact unless asked for one in that session. Answer in the terminal.
88
89
  - Before commit/push: `gitleaks` must pass.
89
90
  - No new `make` targets without a distinct caller (CI job, spec-mandated gate, or a workflow existing targets can't express) — check `make/*.mk` first; extend over near-duplicate wrappers.
90
91
  - `*.zig` → read `dispatch/write_zig.md`; ZIG GATE fires. <!-- oracle-packs:language.zig -->
@@ -258,7 +258,12 @@ and every spec criterion runs against it; skip-pass is only for genuinely
258
258
  spec-less branches. A deliberately folded spec retains the exact branch header
259
259
  and adds `**Folded-into:** \`M178_001\`` naming the owning spec. Discovery
260
260
  validates that relation and gates the sole non-folded owner; two non-folded
261
- specs on one branch remain a hard error. Mechanical criteria: `spec.moved`
261
+ specs on one branch remain a hard error. **The same relation holds in
262
+ `active/`, from the moment the fold is decided.** A second workstream may sit
263
+ beside an open stream while it names that stream through `Folded-into`, and the
264
+ gates run against the owner — a fold is not a second stream, and it was decided
265
+ when the scope was, not when the spec moved. Extra `active/` specs without the
266
+ header are still the hard error they were: one stream per worktree. Mechanical criteria: `spec.moved`
262
267
  (`Status: DONE` ⇒ the spec sits under `done/` and was moved on this branch) · `spec.baseline`
263
268
  (`Test Baseline:` recorded) · `spec.ordering` (the branch's first commit
264
269
  carries the spec — no code before CHORE(open)) · `spec.deferrals` (a deferral
@@ -40,10 +40,9 @@ 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** 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.
43
+ - **Rate constants** are declared once, in `afd_billing`'s `nanos.rs`, with the
44
+ nanos denominator in `afd_core::money`. A changelog money or rate claim must
45
+ match those — and `rates.mdx` in the docs repository, which no test reaches.
47
46
 
48
47
  ## Required output (self-audit line before committing the changelog)
49
48
 
@@ -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 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.
17
+ - **Rate constants are declared once, so a money claim has one thing to match.** `rustd/crates/afd_billing/src/nanos.rs` holds the rates and `afd_core::money` holds the nanos denominator; the daemon charges from them and every client renders the figure it was served. A changelog rate or money claim is checked against those constants, never against a client. Public docs are the surface nothing reaches: 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.
@@ -11,10 +11,10 @@ regenerates this file and fails on any difference. To update it, run
11
11
  | `docs/SCHEMA_CONVENTIONS.md` | — | 0 | 0 | 0/16 | mechanical |
12
12
  | `docs/DOCUMENTATION_RULES.md` | — | 0 | 0 | 0/22 | latent |
13
13
  | `docs/LIFECYCLE_PATTERNS.md` | — | 0 | 0 | 0/22 | mechanical |
14
- | `docs/CHANGELOG_VOICE.md` | — | 0 | 0 | 0/3 | latent |
14
+ | `docs/CHANGELOG_VOICE.md` | — | 0 | 0 | 0/4 | latent |
15
15
  | `docs/VERIFY_TIERS.md` | — | 0 | 0 | 0/4 | uncited |
16
16
  | `docs/greptile-learnings/RULES.md` | — | 0 | 0 | 0/114 | mechanical |
17
- | **corpus** | | | | **30/276** | |
17
+ | **corpus** | | | | **30/277** | |
18
18
 
19
19
  ## How to read this
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsfleet/orly",
3
- "version": "0.10.12",
3
+ "version": "0.10.14",
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/gates.ts CHANGED
@@ -39,7 +39,7 @@ export type GateReport = {
39
39
  };
40
40
 
41
41
  export type Override = { criterion: string; reason: string };
42
- type ClosedSpec = {
42
+ type SpecMetadata = {
43
43
  path: string;
44
44
  identifier: string | undefined;
45
45
  branch: string | undefined;
@@ -105,11 +105,34 @@ export function branchOverrides(root: string): Override[] {
105
105
  return overrides;
106
106
  }
107
107
 
108
+ // One stream per worktree, and a fold is not a second stream. Extra specs in
109
+ // active/ are allowed only when each names the one non-folded owner through
110
+ // Folded-into — the same relation closedSpecPath enforces in done/. Without
111
+ // this, folding a workstream into an open stream was impossible during the
112
+ // work and legal only after the close, which is the wrong way round: the fold
113
+ // is decided when the scope is, not when the spec moves.
108
114
  export function activeSpecPath(root: string): string | undefined {
109
- const specs = specPathsUnder(root, ACTIVE_DIRECTORY);
115
+ const specs = specPathsUnder(root, ACTIVE_DIRECTORY).map(specMetadata)
116
+ .filter((spec): spec is SpecMetadata => spec !== undefined);
110
117
  if (specs.length === 0) return undefined;
111
- if (specs.length > 1) throw new OrlyError(`more than one active spec — one stream per worktree:${NEWLINE}${specs.join(NEWLINE)}`);
112
- return specs[0];
118
+ return owningSpec(specs, "active spec", "active specs").path;
119
+ }
120
+
121
+ // The fold relation, shared by both discovery paths: exactly one non-folded
122
+ // owner, every other spec naming it, and no spec naming itself.
123
+ function owningSpec(specs: SpecMetadata[], one: string, many: string): SpecMetadata {
124
+ const owners = specs.filter((spec) => spec.foldedInto === undefined);
125
+ if (owners.length === 0) throw new OrlyError(`every ${one} is folded — one owning stream is required:${NEWLINE}${specs.map((spec) => spec.path).join(NEWLINE)}`);
126
+ if (owners.length > 1) throw new OrlyError(`more than one ${one} — one stream per worktree:${NEWLINE}${owners.map((spec) => spec.path).join(NEWLINE)}`);
127
+ const owner = owners[0];
128
+ if (!owner) throw new OrlyError(`${one} ownership could not be resolved`);
129
+ const folded = specs.filter((spec) => spec.foldedInto !== undefined);
130
+ if (folded.length > 0 && !owner.identifier) throw new OrlyError(`the owning ${one} has no milestone and workstream identifier: ${owner.path}`);
131
+ const selfFolds = folded.filter((spec) => spec.identifier === spec.foldedInto);
132
+ if (selfFolds.length > 0) throw new OrlyError(`folded ${many} cannot fold into themselves:${NEWLINE}${selfFolds.map((spec) => spec.path).join(NEWLINE)}`);
133
+ const invalidFolds = folded.filter((spec) => spec.foldedInto !== owner.identifier);
134
+ if (invalidFolds.length > 0) throw new OrlyError(`folded ${many} must name their owner ${owner.identifier}:${NEWLINE}${invalidFolds.map((spec) => spec.path).join(NEWLINE)}`);
135
+ return owner;
113
136
  }
114
137
 
115
138
  // Closed-spec follow-through: CHORE(close) moves the spec to done/, and the
@@ -120,21 +143,10 @@ export function closedSpecPath(root: string): string | undefined {
120
143
  const branch = gitOutput(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
121
144
  if (!branch || DEFAULT_BRANCHES.includes(branch)) return undefined;
122
145
  const specs = specPathsUnder(root, DONE_DIRECTORY)
123
- .map(closedSpec)
124
- .filter((spec): spec is ClosedSpec => spec !== undefined && spec.branch === branch);
146
+ .map(specMetadata)
147
+ .filter((spec): spec is SpecMetadata => spec !== undefined && spec.branch === branch);
125
148
  if (specs.length === 0) return undefined;
126
- const owners = specs.filter((spec) => spec.foldedInto === undefined);
127
- if (owners.length === 0) throw new OrlyError(`every done/ spec naming branch ${branch} is folded — one owning stream is required:${NEWLINE}${specs.map((spec) => spec.path).join(NEWLINE)}`);
128
- if (owners.length > 1) throw new OrlyError(`more than one done/ spec names branch ${branch} — one stream per worktree:${NEWLINE}${owners.map((spec) => spec.path).join(NEWLINE)}`);
129
- const owner = owners[0];
130
- if (!owner) throw new OrlyError(`done/ spec ownership could not be resolved for branch ${branch}`);
131
- const folded = specs.filter((spec) => spec.foldedInto !== undefined);
132
- if (folded.length > 0 && !owner.identifier) throw new OrlyError(`the owning done/ spec for branch ${branch} has no milestone and workstream identifier: ${owner.path}`);
133
- const selfFolds = folded.filter((spec) => spec.identifier === spec.foldedInto);
134
- if (selfFolds.length > 0) throw new OrlyError(`folded done/ specs naming branch ${branch} cannot fold into themselves:${NEWLINE}${selfFolds.map((spec) => spec.path).join(NEWLINE)}`);
135
- const invalidFolds = folded.filter((spec) => spec.foldedInto !== owner.identifier);
136
- if (invalidFolds.length > 0) throw new OrlyError(`folded done/ specs naming branch ${branch} must name their owner ${owner.identifier}:${NEWLINE}${invalidFolds.map((spec) => spec.path).join(NEWLINE)}`);
137
- return owner.path;
149
+ return owningSpec(specs, `done/ spec naming branch ${branch}`, `done/ specs naming branch ${branch}`).path;
138
150
  }
139
151
 
140
152
  // An in-flight (active/) spec wins; otherwise the branch's closed spec gates.
@@ -145,9 +157,9 @@ export function specPathFor(root: string): { path: string; closed: boolean } | u
145
157
  return closed ? { path: closed, closed: true } : undefined;
146
158
  }
147
159
 
148
- function closedSpec(path: string): ClosedSpec | undefined {
160
+ function specMetadata(path: string): SpecMetadata | undefined {
149
161
  try {
150
- const metadata: ClosedSpec = {
162
+ const metadata: SpecMetadata = {
151
163
  path,
152
164
  identifier: basename(path).match(SPEC_IDENTIFIER_PATTERN)?.[1],
153
165
  branch: undefined,
@@ -38,6 +38,66 @@ describe("spec discovery", () => {
38
38
 
39
39
  expect(() => activeSpecPath(project)).toThrow("one stream per worktree");
40
40
  });
41
+
42
+ test("a folded active spec yields ownership to the spec it names", async () => {
43
+ const project = newSpecRepository();
44
+ mkdirSync(join(project, "docs/v2/active"), { recursive: true });
45
+ await Bun.write(
46
+ join(project, "docs/v2/active/M100_001_P2_CLI_FOLDED.md"),
47
+ specFixture(undefined, undefined, ["**Folded-into:** `M99_001`"]),
48
+ );
49
+
50
+ expect(activeSpecPath(project)).toEndWith("M99_001_P2_CLI_FIXTURE.md");
51
+ });
52
+
53
+ test("a folded active spec must name the exact owner", async () => {
54
+ const project = newSpecRepository();
55
+ mkdirSync(join(project, "docs/v2/active"), { recursive: true });
56
+ await Bun.write(
57
+ join(project, "docs/v2/active/M100_001_P2_CLI_FOLDED.md"),
58
+ specFixture(undefined, undefined, ["**Folded-into:** `M404_001`"]),
59
+ );
60
+
61
+ expect(() => activeSpecPath(project)).toThrow("must name their owner M99_001");
62
+ });
63
+
64
+ test("a folded active spec cannot name itself", async () => {
65
+ const project = newSpecRepository();
66
+ mkdirSync(join(project, "docs/v2/active"), { recursive: true });
67
+ await Bun.write(
68
+ join(project, "docs/v2/active/M100_001_P2_CLI_FOLDED.md"),
69
+ specFixture(undefined, undefined, ["**Folded-into:** `M100_001`"]),
70
+ );
71
+
72
+ expect(() => activeSpecPath(project)).toThrow("cannot fold into themselves");
73
+ });
74
+
75
+ test("every active spec folded leaves no owning stream", async () => {
76
+ const project = newRepository();
77
+ mkdirSync(join(project, "docs/v2/active"), { recursive: true });
78
+ await Bun.write(
79
+ join(project, "docs/v2/active/M100_001_P2_CLI_FOLDED.md"),
80
+ specFixture(undefined, undefined, ["**Folded-into:** `M99_001`"]),
81
+ );
82
+ await Bun.write(
83
+ join(project, "docs/v2/active/M101_001_P2_CLI_ALSO_FOLDED.md"),
84
+ specFixture(undefined, undefined, ["**Folded-into:** `M99_001`"]),
85
+ );
86
+
87
+ expect(() => activeSpecPath(project)).toThrow("one owning stream is required");
88
+ });
89
+
90
+ test("a folded active spec still gates through its owner", async () => {
91
+ const project = newSpecRepository();
92
+ const model = await modelFor(project);
93
+ mkdirSync(join(project, "docs/v2/active"), { recursive: true });
94
+ await Bun.write(
95
+ join(project, "docs/v2/active/M100_001_P2_CLI_FOLDED.md"),
96
+ specFixture(undefined, undefined, ["**Folded-into:** `M99_001`"]),
97
+ );
98
+
99
+ expect(runGate(model, project, "work").results.find((result) => result.name === "cmd.conform")?.ok).toBeTrue();
100
+ });
41
101
  });
42
102
 
43
103
  describe("closed-spec follow-through", () => {