@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 +5 -5
- package/SOUL.md +4 -5
- package/core/operating-model.md +1 -0
- package/dispatch/lifecycle.md +6 -1
- package/dispatch/write_changelog.md +3 -4
- package/docs/CHANGELOG_VOICE.md +1 -1
- package/docs/RULE_ENFORCEMENT.md +2 -2
- package/package.json +1 -1
- package/src/gates.ts +32 -20
- package/src/gates_spec.test.ts +60 -0
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
|
|
299
|
-
green
|
|
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
|
|
48
|
-
green
|
|
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
|
|
package/core/operating-model.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. <!-- oracle-packs:language.zig -->
|
package/dispatch/lifecycle.md
CHANGED
|
@@ -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.
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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 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.
|
package/docs/RULE_ENFORCEMENT.md
CHANGED
|
@@ -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/
|
|
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/
|
|
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.
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
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(
|
|
124
|
-
.filter((spec): spec is
|
|
146
|
+
.map(specMetadata)
|
|
147
|
+
.filter((spec): spec is SpecMetadata => spec !== undefined && spec.branch === branch);
|
|
125
148
|
if (specs.length === 0) return undefined;
|
|
126
|
-
|
|
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
|
|
160
|
+
function specMetadata(path: string): SpecMetadata | undefined {
|
|
149
161
|
try {
|
|
150
|
-
const metadata:
|
|
162
|
+
const metadata: SpecMetadata = {
|
|
151
163
|
path,
|
|
152
164
|
identifier: basename(path).match(SPEC_IDENTIFIER_PATTERN)?.[1],
|
|
153
165
|
branch: undefined,
|
package/src/gates_spec.test.ts
CHANGED
|
@@ -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", () => {
|