@agentsfleet/orly 0.14.4 β†’ 0.14.6

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
@@ -16,7 +16,7 @@ You are this repository's coding agent: deterministic, autonomous, command-line-
16
16
 
17
17
  **The human is Kishore** β€” casual handle **Indy**; either name, any case, addresses him. Ambiguous "the user" / "they" here = Kishore. The agent is **Oracle**, casual handle **Aiwa**. `orly` names the agentsfleet CLI only (`orly gate`, `orly init`, `orly update`, and the `Orly-Override` trailer it writes).
18
18
 
19
- **Address tags.** Kishore: **🀠 Indy**; agent: **πŸ¦‰ Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains β†’ `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps β†’ flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `core.fleet_*`. Keep: `agentsfleet.dev`, `github.com/agentsfleet/agentsfleet`, `@agentsfleet/*`, `~/Projects/agentsfleet`.
19
+ **Address tags.** Kishore: **🀠 Indy**; agent: **πŸ¦‰ Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains β†’ `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps β†’ flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `fleet.*`. Keep: `agentsfleet.dev`, `github.com/agentsfleet/agentsfleet`, `@agentsfleet/*`, `~/Projects/agentsfleet`.
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
 
package/README.md CHANGED
@@ -78,7 +78,7 @@ TypeSafe's Jev model supplies runtime advice through explicit `orly judge` reque
78
78
 
79
79
  See [releases](https://github.com/agentsfleet/orly/releases) for published versions.
80
80
  The install commands below fetch the published package.
81
- The release workflow publishes 0.14.4 after its version change merges to `main`.
81
+ The release workflow publishes 0.14.6 after its version change merges to `main`.
82
82
 
83
83
  ---
84
84
 
@@ -179,7 +179,7 @@ The questionnaire is organised by scenario. Each scenario corresponds to a momen
179
179
 
180
180
  | # | Question | Expected |
181
181
  |---|---|---|
182
- | 11.1 | Only under the explicit `product.agentsfleet` pack, is its pre-v0.30.0 removal flow rm-file + rm-embed + rm-migration-array with no markers, while independent repositories retain their own live-data policy and destructive-change approval? | YES |
182
+ | 11.1 | Only under the explicit `product.agentsfleet` pack, is its pre-v0.30.0 removal flow rm-file + rm-migration-array with no markers, while independent repositories retain their own live-data policy and destructive-change approval? | YES |
183
183
  | 11.2 | Are static strings in SQL schema a **no-override** ban across every literal that names a value or identifier the application also names β€” `DEFAULT`, `CHECK (col IN (…))`, trigger-body comparisons against a column, `current_setting()` parameter names, and `RAISE EXCEPTION` text a caller matches on β€” not just `DEFAULT`/`CHECK`? | YES |
184
184
 
185
185
  ### Scenario 12 β€” Auto-mode boundary (added)
@@ -7,7 +7,7 @@ type Section = { title: string; nodes: MarkdownNode[] };
7
7
  type Commands = Record<string, string[][]>;
8
8
 
9
9
  const CONFIG_PATH = ".orly/orly.json";
10
- const SPEC_LINE_LIMIT = 320;
10
+ const SPEC_LINE_LIMIT = 500;
11
11
  const BACKTICK = "`";
12
12
  const EMPTY = "";
13
13
  const PIPE_OUTPUT = "pipe";
@@ -11,7 +11,7 @@ You are this repository's coding agent: deterministic, autonomous, command-line-
11
11
 
12
12
  **The human is Kishore** β€” casual handle **Indy**; either name, any case, addresses him. Ambiguous "the user" / "they" here = Kishore. The agent is **Oracle**, casual handle **Aiwa**. `orly` names the agentsfleet CLI only (`orly gate`, `orly init`, `orly update`, and the `Orly-Override` trailer it writes).
13
13
 
14
- **Address tags.** Kishore: **🀠 Indy**; agent: **πŸ¦‰ Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains β†’ `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps β†’ flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `core.fleet_*`. Keep: `agentsfleet.dev`, `github.com/agentsfleet/agentsfleet`, `@agentsfleet/*`, `~/Projects/agentsfleet`.
14
+ **Address tags.** Kishore: **🀠 Indy**; agent: **πŸ¦‰ Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains β†’ `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps β†’ flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `fleet.*`. Keep: `agentsfleet.dev`, `github.com/agentsfleet/agentsfleet`, `@agentsfleet/*`, `~/Projects/agentsfleet`.
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
 
@@ -57,7 +57,6 @@ The **Schema Table Removal Guard** card dissolves into this faΓ§ade; its prose i
57
57
  **Triggers** β€” before any of these, run `cat VERSION` and print the guard output. `0.30.0` is the anchor: below it the datastore is rebuilt from empty, from it the datastore is live production. Compare the version field by field, never as a string β€” `0.29.0` is under the anchor and `0.30.0` is not, while a lexical compare puts `0.30.0` under `0.9.0` and reads the era backwards.
58
58
 
59
59
  - Creating, editing, or deleting any file under `schema/*.sql`.
60
- - Editing `schema/embed.zig` (any `@embedFile` constant).
61
60
  - Editing the canonical migration array in `rustd/crates/afd_db/src/migration.rs`.
62
61
  - Writing `DROP TABLE`, `ALTER TABLE`, or `SELECT 1;` into any SQL file.
63
62
  - Accepting a spec dimension prescribing a "DROP migration", "ALTER migration", or "version marker".
@@ -73,8 +72,7 @@ The era runs while `VERSION` is under `0.30.0`, and it ends there β€” the develo
73
72
  To remove a table:
74
73
 
75
74
  1. `rm schema/NNN_foo.sql`
76
- 2. Remove `@embedFile` from `schema/embed.zig`
77
- 3. Remove the entry from the migration array in `rustd/crates/afd_db/src/migration.rs` and update its registration tests.
75
+ 2. Remove the entry from the migration array in `rustd/crates/afd_db/src/migration.rs` and update its registration tests.
78
76
 
79
77
  **Forbidden:** `ALTER TABLE`, `DROP TABLE`, `SELECT 1;` markers, comment-only files, "keep file for slot numbering". Slot gaps are fine β€” the DB is wiped on rebuild.
80
78
 
@@ -91,11 +89,10 @@ The teardown path is closed. Every schema change assumes a live datastore holdin
91
89
  ```
92
90
  SCHEMA GUARD: VERSION=<v> (<0.30.0 ? teardown : migrate)
93
91
  rm:schema/<file>.sql
94
- rm-embed:<const>
95
92
  rm-migration:v<N>
96
93
  ```
97
94
 
98
- Below `0.30.0`, all three rm- lines appear. From `0.30.0`, replace them with `migration:schema/<NNN>_<change>.sql`.
95
+ Below `0.30.0`, both rm- lines appear. From `0.30.0`, replace them with `migration:schema/<NNN>_<change>.sql`.
99
96
  <!-- oracle-packs:end -->
100
97
 
101
98
  ## Companion SQL rules (retained in `docs/greptile-learnings/RULES.md`)
@@ -18,7 +18,7 @@
18
18
  # (retained) and the GREPTILE GATE enforces them by review until a leaf exists.
19
19
  # SCH's teardown-era forbidden-token floor (no ALTER/DROP/SELECT 1; in schema/*.sql) is
20
20
  # itself a build-the-check (TODO-CHECK) candidate; the teardown-vs-ALTER disposition
21
- # call above it stays JUDGMENT (which file to rm, which @embedFile, which migration index).
21
+ # call above it stays JUDGMENT (which file to rm, which migration index).
22
22
 
23
23
  source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
24
24
 
@@ -30,7 +30,7 @@ dispatch_header
30
30
  dispatch_length_gate 350 # FLL β€” file ≀ 350
31
31
 
32
32
  # ── judgment gates (no script can decide; agent states verdict in chat) ──
33
- dispatch_judgment "SCH" "removing/altering a table? cat VERSION and compare field by field, not as a string β€” below 0.30.0 = full teardown (rm schema/NNN.sql + @embedFile + migration-array entry + index tests), never ALTER/DROP/SELECT 1; 0.30.0 and above = live production datastore, numbered ALTER/DROP migration only. print the SCHEMA GUARD block"
33
+ dispatch_judgment "SCH" "removing/altering a table? cat VERSION and compare field by field, not as a string β€” below 0.30.0 = full teardown (rm schema/NNN.sql + migration-array entry + index tests), never ALTER/DROP/SELECT 1; 0.30.0 and above = live production datastore, numbered ALTER/DROP migration only. print the SCHEMA GUARD block"
34
34
  dispatch_judgment "ITF" "integration test touching a production table? seed via src/db/test_fixtures_<scope>.zig against the real schema β€” never CREATE TEMP TABLE mocking the production shape"
35
35
 
36
36
  dispatch_verdict
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.6 β€” 2026-10-17
4
+
5
+ agentsfleet's schema conventions no longer require migrate to refuse an older migrator's ledger.
6
+
7
+ - **Schema conventions** β€” agentsfleet's databases start empty under SQLx's `Migrator`, so no earlier ledger exists to refuse.
8
+
9
+ ## 0.14.5 β€” 2026-10-17
10
+
11
+ agentsfleet's schema conventions carry the owner-signed Postgres rules, and specs may run to 500 lines.
12
+
13
+ - **Schema conventions** β€” placement in owned schemas, key and counter naming, RULE STS in full, timestamp categories, runtime SQL in `PREPARE`-checked `.sql` files, and SQLx's `Migrator` inside a bounded lock. Entities are named `fleet.*`.
14
+ - **Spec cap** β€” the spec template gate allows 500 lines, up from 320.
15
+ - **Schema guard** β€” the teardown flow no longer names the retired `schema/embed.zig`.
16
+
3
17
  ## 0.14.4 β€” 2026-10-16
4
18
 
5
19
  The milestone-ID and UI-substitution audit no longer forks per staged file. A pre-commit stage of ~1,600 files that ran over 27 minutes now finishes in about two seconds, with verdicts unchanged.
@@ -8,13 +8,13 @@ regenerates this file and fails on any difference. To update it, run
8
8
  |---|---|--:|--:|--:|---|
9
9
  | `docs/LOGGING_STANDARD.md` | `LOG` β†’ `logging.sh` | 10 | 13 | 30/30 | mechanical |
10
10
  | `docs/REST_API_DESIGN_GUIDELINES.md` | β€” | 0 | 0 | 0/69 | mechanical |
11
- | `docs/SCHEMA_CONVENTIONS.md` | β€” | 0 | 0 | 0/17 | mechanical |
11
+ | `docs/SCHEMA_CONVENTIONS.md` | β€” | 0 | 0 | 0/12 | mechanical |
12
12
  | `docs/DOCUMENTATION_RULES.md` | β€” | 0 | 0 | 0/22 | latent |
13
13
  | `docs/LIFECYCLE_PATTERNS.md` | β€” | 0 | 0 | 0/22 | mechanical |
14
14
  | `docs/CHANGELOG_VOICE.md` | β€” | 0 | 0 | 0/3 | latent |
15
15
  | `docs/VERIFY_TIERS.md` | β€” | 0 | 0 | 0/4 | uncited |
16
16
  | `docs/greptile-learnings/RULES.md` | β€” | 0 | 0 | 0/115 | mechanical |
17
- | **corpus** | | | | **30/282** | |
17
+ | **corpus** | | | | **30/277** | |
18
18
 
19
19
  ## How to read this
20
20
 
@@ -13,102 +13,115 @@ with the real schema through shared fixture helpers.
13
13
  Keep migration registration and position assertions synchronized with each edit.
14
14
 
15
15
  <!-- oracle-packs:start product.agentsfleet -->
16
- Canonical reference for `agentsfleet` database schema patterns. All new tables **must** follow these conventions. Existing tables are brought into compliance when rebuilt.
16
+ Canonical reference for `agentsfleet` database schema patterns. All new tables **must** follow these conventions. The table-by-table layout lives in the product repository's `docs/architecture/postgres_schemas.md`; this section carries the rules that layout follows.
17
17
 
18
18
  ## Migration Model
19
19
 
20
- **Teardown-rebuild is in force for the M154 rebuild (owner decision, Indy, Jul 31, 2026).** The development database is being dropped and re-created from empty while production is undeployed, so slots `001`–`046` are **retired wholesale** and the schema is re-authored by dependency layer. During this rebuild the frozen-slot rule below does not apply, and `ALTER`/`DROP` statements are forbidden rather than required β€” a change belongs in the base statement it would have patched. This is the posture RULE SCH already specifies for `VERSION < 0.30.0`; the Jul 22 additive model was the deviation.
20
+ **Additive at `VERSION >= 0.30.0`.** Every schema change is a new numbered slot; shipped slots are frozen history. Destructive changes (`DROP TABLE`, `DROP COLUMN`, type rewrites) need an explicit owner decision per change. A from-empty rebuild past `0.30.0` happens only under an owner-recorded `SCHEMA GUARD: SKIPPED per user override (reason: …)` with the database dropped first β€” the M229 rebuild (Oct 17, 2026) is the precedent.
21
21
 
22
- **Additive migrations resume at `VERSION >= 0.30.0`, as RULE SCH specifies.**
23
- Completing the rebuild alone does not change the version boundary.
24
- Every subsequent schema change is a new numbered migration file; shipped slots
25
- are frozen history. Migrations are tracked incrementally through
26
- `agentsfleetd doctor --schema-gate`; use `IF NOT EXISTS` where appropriate for
27
- fresh bootstrap and an already provisioned database.
22
+ ## Migrator
28
23
 
29
- Destructive changes (`DROP TABLE`, `DROP COLUMN`, type rewrites) require an explicit owner decision per change β€” additive is the default an agent may author alone.
30
-
31
- > Historical note: slots `001`–`031` predate the additive model (teardown-rebuild with inline DDL edits, enforced by a since-removed `check-schema-gate` lint target). M154 retires them along with everything up to `046`.
24
+ SQLx's `Migrator` applies the slots from the explicit list in `rustd/crates/afd_db/src/migration.rs` (`Migrator::with_migrations`; the `migrate` feature only, `macros` stays off). It keeps checksums, so an edited applied slot is refused, and records the ledger in the `audit` schema. Locking is ours: SQLx's lock is off (`set_locking(false)`) and a bounded `pg_try_advisory_lock` poll wraps the run, so a held lock fails loudly instead of hanging a deploy.
32
25
 
33
26
  ## Schema File Organization
34
27
 
35
- - Each SQL file must be **≀100 lines** and **single-concern** (one table, one logical group, or one additive change).
36
- - Files are numbered sequentially: `001_core_foundation.sql`, `002_core_workflow.sql`, etc. New migrations append the next number; shipped numbers are never reused or slid.
37
- - Every SQL file must be registered in `schema/embed.zig` (compile-time embed) and `rustd/crates/afd_db/src/migration.rs` (migration version array).
38
- - Below `0.30.0`, remove obsolete slots and their registrations completely;
39
- no-op markers are forbidden. At and above `0.30.0`, preserve shipped slots and
40
- apply changes through new migrations.
28
+ - One slot per table: the table, its local constraints, indexes and grants together; each file **≀100 lines**.
29
+ - Slots are ordered by foreign key β€” a table follows every table it references, across schemas. A foreign key that closes a cycle (a head pointer, for example) gets its own registered slot after both tables exist, `DEFERRABLE INITIALLY DEFERRED`.
30
+ - Every slot is registered in `rustd/crates/afd_db/src/migration.rs`; the `rust-migration-list` lint compares the list to the tracked files.
31
+ - Shipped numbers are never reused or slid.
32
+
33
+ ## Placement and naming
34
+
35
+ 1. **Placement.** A table lives in the schema of the product area that owns it (`identity`, `authoring`, `fleet`, `runner`, `connector`, `model`, `billing`, `vault`, `memory`). Its name is a plural noun that never repeats the schema name: `authoring.drafts`, not `authoring.authoring_drafts`. A singleton is named for its one row and says so in its slot.
36
+ 2. **Foreign keys** end `_id` and name the table they point at (`user_id`, `draft_id`). A prefix names the role when the column alone would not say it (`invited_by_user_id`, `active_grant_id`, `last_runner_id`). A deliberately missing foreign key is explained in the slot comment.
37
+ 3. **Status** columns are named `status`; categories are `<thing>_type`. RULE STS applies in full and without override: no literal that names a value or identifier the application also names β€” a `DEFAULT`, a `CHECK (col IN (…))` list, a trigger comparison against a column, a `current_setting()` parameter name, or `RAISE EXCEPTION` text a caller matches on. A predicate that needs a state reads a NULL marker column; a privileged path (a purge, for example) is a grant, not a session setting. The only literals left are structural ones nothing in the application names: the UUID version nibble `'7'`, `TG_OP` values, and exception text no caller matches on.
38
+ 4. **Timestamps.** Every table has `created_at`; every table whose rows change has `updated_at`. Any other time column is one of: (a) a deadline named `*expires_at`; (b) a NULL marker an index or trigger reads; (c) a domain instant the API returns that `updated_at` cannot carry; (d) an accounting cursor billing reads.
39
+ 5. **One fact, one home.** No JSON blob copies a column; no counter restates another table's number. Named exceptions, each with its consistency rule in the slot: an aggregate maintained under the same lock or trigger as its source; a delivery snapshot that must outlive its source; a non-secret projection of encrypted content; a scope copy a restricted role needs to filter alone.
40
+ 6. **Retry keys** (`operation_id` plus a request digest) live only in receipt tables.
41
+ 7. **Every column names its reader** before it is created. Accounting and audit records are exempt when the slot names the export or verification that reads them.
42
+ 8. **A new table** carries a written answer to "why not a column, or an existing table".
43
+ 9. **Immutable tables** carry no `updated_at`, no UPDATE grant, and a trigger that refuses UPDATE. DELETE stays governed by grants and the deletion matrix, so cascades keep working.
44
+
45
+ ### Counter and pointer names
46
+
47
+ | Kind | Name |
48
+ |---|---|
49
+ | Position within a parent (1, 2, 3…) | `sequence` |
50
+ | Pointer to another table's position | `<thing>_sequence` |
51
+ | Highest position so far | `last_<thing>_sequence` |
52
+ | "Everything up to N" watermark | `<verb>_through_sequence` |
53
+ | Global insertion order | `sequence` (identity column) |
54
+ | Frozen content version | `revision`; pointer `<x>_revision_id`; the live one `head_revision_id` |
55
+ | Compare-and-swap edit counter | `resource_version`, or `<scope>_version` when it covers one action |
56
+ | Lease epoch | `fencing_token` |
57
+ | Tally | `<thing>_count` |
58
+ | Deadline | `*expires_at` |
59
+
60
+ Wire field names (API and runner protocol) are not renamed by these rules.
61
+
62
+ ## Deletion matrix
63
+
64
+ Every root a user or operator can delete β€” draft, user, Library card, fleet, workspace, tenant β€” has a stated row in the product's deletion matrix: what goes with it, what stays, and what is scrubbed at once. Money records (charges, settlements, the attempts they hang off) never cascade from an erasable owner; they keep opaque identifiers. Private text is scrubbed on every erasure path, not only the one that purges drafts.
41
65
 
42
66
  ## SQL Qualification
43
67
 
44
- - Use schema-qualified table names in SQL (`core.platform_llm_keys`, `core.workspaces`, etc.) for new queries and handlers.
68
+ - Use schema-qualified table names in SQL (`identity.workspaces`, `fleet.fleets`).
45
69
  - Do not rely on session `search_path` defaults for correctness.
46
- - Legacy unqualified queries may remain temporarily, but touched paths should be migrated to schema-qualified names.
70
+
71
+ ## Runtime SQL
72
+
73
+ - Every static statement lives in a `.sql` file beside the use case that runs it (for example `library/sql/list.sql`), loaded with `include_str!` and bound by a typed Rust function that states its parameters, authorization and locks.
74
+ - Rows decode into named `FromRow` types selected by explicit column lists; `Json<T>` is for real JSON columns only; storage rows stay separate from wire types. No `to_jsonb(<row>)` whole-row reads.
75
+ - One integration test `PREPARE`s every `.sql` file against the rebuilt schema, so a stale table or column fails before merge. A statement that must be assembled at run time is named in its module and covered by its own test.
76
+ - Domain stores take the caller's connection or transaction; there is no generic repository trait.
77
+ - Every pooled runtime connection carries `lock_timeout` and `statement_timeout` named constants; the migrate connection carries neither.
47
78
 
48
79
  ## Schema-Backed Runtime Defaults
49
80
 
50
- - If a numeric schema default is also used in runtime fallback or provisioning logic, define it once in Zig as a named constant and import that constant in runtime code.
51
- - Keep the database DDL value unchanged unless the product default is intentionally changing; the Zig constant mirrors the schema default for drift detection.
52
- - Add an adjacent `Canonical constant:` SQL comment next to each shared numeric default so reviewers can verify the linkage quickly.
81
+ - A numeric schema default also used in runtime fallback or provisioning logic is defined once as a Rust named constant in the owning crate and referenced by the slot comment (`Canonical constant: crate::path::NAME`).
82
+ - Keep the DDL value unchanged unless the product default is intentionally changing.
53
83
 
54
84
  ## Identity Column
55
85
 
56
- **One identity column per table, named `id` (owner decision, Indy, Jul 31, 2026 β€” M154).** The earlier `uid` rule paired a generated `uid` primary key with a duplicate twin column (`id`, `tenant_id`, `workspace_id`, …) holding the same value under its own `UNIQUE` constraint. That shape is retired.
86
+ **One identity column per table, named `id` (owner decision, Indy, Jul 31, 2026 β€” M154).**
57
87
 
58
- - **Column:** `id`
59
- - **Type:** Universally Unique Identifier (UUID) `PRIMARY KEY`
60
- - **Generation:** Application-side UUID version 7 (UUIDv7) via `rustd/crates/afd_core/src/id.rs`, never `gen_random_uuid()`.
61
- - **No second unique key on the same value.** This is a correctness rule, not only a storage one: `ON CONFLICT` can arbitrate exactly one constraint, so a table carrying two unique keys over the same value makes two sessions inserting a brand-new row race to a duplicate-key error on the *other* index instead of taking the update arm. `schema/043_runner_lifetime_counters.sql` recorded this before the rule was generalised.
88
+ - **Type:** Universally Unique Identifier (UUID) `PRIMARY KEY`, application-minted UUID version 7 (UUIDv7) through `afd_core::id`, never `gen_random_uuid()`.
89
+ - **No second unique key on the same value.** `ON CONFLICT` arbitrates exactly one constraint, so two unique keys over one value make concurrent inserts race to a duplicate-key error on the other index.
62
90
  - **Foreign keys reference the primary key**, never a secondary unique constraint.
63
- - **Constraint:** every table carries a UUIDv7 CHECK:
64
- ```sql
65
- CONSTRAINT ck_{table}_id_uuidv7 CHECK (substring(id::text from 15 for 1) = '7')
66
- ```
67
- - **Adding a new table:** add a `generate{TableName}Id()` function to `rustd/crates/afd_core/src/id.rs`.
68
- - **API shape:** unchanged β€” public fields keep exposing `id`, `tenant_id`, `workspace_id` and the other documented names. The column now *is* the public name in most tables, so the aliasing the old rule required mostly disappears; where a public field differs from `id`, alias at the boundary rather than renaming client-facing payloads.
69
- - **Exceptions,** each stated in the slot that creates the table: a curated catalogue keyed by a stable slug (`core.fleet_library`) and a singleton keyed by a pinned integer (`core.model_catalogue_revision`) carry no UUID.
91
+ - **Constraint:** every UUID-keyed table carries `CONSTRAINT ck_{table}_id_uuidv7 CHECK (substring(id::text from 15 for 1) = '7')`.
92
+ - **Exceptions,** each stated in its slot: a row that exists at most once per parent is keyed by the parent (`fleet.sessions`, `fleet.activity_counters`, `runner.fleet_slots`, `runner.lifetime_counters`, `billing.wallets`, `model.tenant_selections`); a singleton keyed by a pinned integer (`model.catalogue_version`); a natural key addressed by every statement (`model.platform_defaults` by `provider`).
70
93
 
71
94
  ## Timestamps
72
95
 
73
- - **Type:** `BIGINT NOT NULL` β€” milliseconds since Unix epoch.
74
- - **Generation:** `std.time.milliTimestamp()` in Zig application code.
75
- - **Never** use `TIMESTAMPTZ`, `TIMESTAMP`, or `DEFAULT now()`.
96
+ - **Type:** `BIGINT NOT NULL` β€” milliseconds since the Unix epoch, from the application clock (`afd_core::clock::UnixMillis`).
97
+ - **Never** `TIMESTAMPTZ`, `TIMESTAMP` or `DEFAULT now()`.
76
98
 
77
99
  ## Standard Columns
78
100
 
79
- Every table must have:
80
-
81
101
  | Column | Type | Required | Notes |
82
102
  |--------|------|----------|-------|
83
- | `id` | `UUID PRIMARY KEY` | Yes | UUIDv7, app-generated; the table's only identity column |
103
+ | `id` | `UUID PRIMARY KEY` | Yes | UUIDv7, application-minted |
84
104
  | `created_at` | `BIGINT NOT NULL` | Yes | Set once at INSERT |
85
- | `updated_at` | `BIGINT NOT NULL` | If mutable | Set at INSERT and every UPDATE |
86
-
87
- **Naming is uniform.** `created_at` and `updated_at` are the only names for row lifecycle time β€” not `recorded_at`, `created_at_ms`, or `updated_at_ms`. A column carrying *domain* time distinct from row birth (for example the originating event's creation instant on a money row) is named for that domain meaning and documented in its slot.
88
-
89
- **Mutable tables** (any table where UPDATE is a valid operation) must have `updated_at`.
90
-
91
- **Append-only/event tables** (where UPDATE is blocked by trigger or by design) are exempt from `updated_at`.
105
+ | `updated_at` | `BIGINT NOT NULL` | If the row ever changes | Set at INSERT and every UPDATE |
92
106
 
93
107
  ## Audit Pattern
94
108
 
95
- - **Actor tracking:** Use `actor TEXT` in event/transition tables (e.g., `policy_events`, `usage_ledger`).
96
- - **No `updated_by` column.** Changes to mutable tables are tracked via separate event-sourced audit tables (e.g., `workspace_billing_audit`, `harness_change_log`), not inline `updated_by`.
97
- - **Append-only enforcement:** Tables that must never be updated should have a trigger:
109
+ - **Actor tracking:** `actor TEXT` in event and transition tables.
110
+ - **No `updated_by` column.** Changes to mutable tables are tracked in event tables, not inline.
111
+ - **Append-only enforcement:** an immutable table refuses UPDATE by trigger; DELETE follows grants and the deletion matrix.
98
112
  ```sql
99
- CREATE OR REPLACE FUNCTION core.{table}_append_only() RETURNS trigger AS $$
113
+ CREATE OR REPLACE FUNCTION {schema}.{table}_refuse_update() RETURNS trigger AS $$
100
114
  BEGIN
101
- RAISE EXCEPTION '{table} is append-only β€” UPDATE and DELETE are not permitted';
115
+ RAISE EXCEPTION '{table} is append-only';
102
116
  END;
103
117
  $$ LANGUAGE plpgsql;
104
118
 
105
- CREATE TRIGGER trg_{table}_append_only
106
- BEFORE UPDATE OR DELETE ON core.{table}
107
- FOR EACH ROW EXECUTE FUNCTION core.{table}_append_only();
119
+ CREATE TRIGGER trg_{table}_refuse_update
120
+ BEFORE UPDATE ON {schema}.{table}
121
+ FOR EACH ROW EXECUTE FUNCTION {schema}.{table}_refuse_update();
108
122
  ```
109
123
 
110
- ## RNG
124
+ ## Randomness
111
125
 
112
- - **Always** use `std.crypto.random` (via `allocUuidV7` in `id_format.zig`).
113
- - **Never** use custom RNG implementations or `std.rand`.
126
+ - Identifiers and secrets come from `afd_core::id` and the operating system's cryptographic source; never a custom random number generator.
114
127
  <!-- oracle-packs:end -->
package/docs/TEMPLATE.md CHANGED
@@ -17,7 +17,7 @@ Three markup classes appear below the divider. The gate tells them apart mechani
17
17
  | `tpl:` guidance comment | How to fill the section β€” written for the authoring agent only | **Deleted after filling.** The gate BLOCKs any survivor. |
18
18
  | SPEC AUTHORING RULES banner comment | Standing constraints on the instance | The one comment that survives, verbatim. |
19
19
 
20
- Everything else β€” headings, table skeletons, the standard rubric rows β€” is kept and filled. The result: the executing agent reads **100% instance content, zero template noise**, and the 320-line budget buys signal, not boilerplate.
20
+ Everything else β€” headings, table skeletons, the standard rubric rows β€” is kept and filled. The result: the executing agent reads **100% instance content, zero template noise**, and the 500-line budget buys signal, not boilerplate.
21
21
 
22
22
  ## What a spec pins
23
23
 
@@ -89,7 +89,7 @@ A milestone is not complete until evidence is captured: commands, logs, screensh
89
89
 
90
90
  ## Guardrails
91
91
 
92
- - **Length β€” hard upper bound 320 lines for the filled spec** (after `tpl:` comment deletion). Typical specs 150–300. Past the bound before the Acceptance Rubric β†’ scope is two specs sharing one file; split.
92
+ - **Length β€” hard upper bound 500 lines for the filled spec** (after `tpl:` comment deletion). Typical specs 150–350. Past the bound before the Acceptance Rubric β†’ scope is two specs sharing one file; split.
93
93
  - **Sections** β€” 3–9. **Workstreams** β€” ≀4 (5th cross-cutting only).
94
94
  - **File naming** β€” `docs/v{N}/{pending|active|done}/M{N}_{NNN}_P{Priority}_{CATEGORIES}_{NAME}.md` (e.g. `M40_001_P0_API_WORKER_SUBSTRATE.md`). Categories alphabetised.
95
95
 
@@ -409,7 +409,7 @@ another product's version number never authorizes a rebuild here.
409
409
 
410
410
  <!-- oracle-packs:start product.agentsfleet -->
411
411
 
412
- **Rule:** While `cat VERSION` < 0.30.0 (teardown-rebuild era), removing tables MUST be a full teardown: (1) delete the SQL file (`rm schema/NNN_foo.sql`), (2) remove the `@embedFile` constant from `schema/embed.zig`, (3) remove the migration array entry from `rustd/crates/afd_db/src/migration.rs` and update its registration tests. Never write ALTER TABLE, DROP TABLE, or `SELECT 1;` placeholders. Never keep version-marker files. Migration slot numbers are not sacred pre-v0.30 β€” the DB is wiped on every rebuild, and gaps in numbering are fine. From VERSION >= 0.30.0 the datastore is live production data, so every removal or reshape is a numbered ALTER/DROP migration and the teardown path is closed. **Compare the version field by field, never as a string:** `0.29.0` is under the anchor, `0.30.0` is not, and a lexical compare puts `0.30.0` below `0.9.0` β€” the anchor moved off a major-number boundary precisely so the minor field decides.
412
+ **Rule:** While `cat VERSION` < 0.30.0 (teardown-rebuild era), removing tables MUST be a full teardown: (1) delete the SQL file (`rm schema/NNN_foo.sql`), (2) remove the migration array entry from `rustd/crates/afd_db/src/migration.rs` and update its registration tests. Never write ALTER TABLE, DROP TABLE, or `SELECT 1;` placeholders. Never keep version-marker files. Migration slot numbers are not sacred pre-v0.30 β€” the DB is wiped on every rebuild, and gaps in numbering are fine. From VERSION >= 0.30.0 the datastore is live production data, so every removal or reshape is a numbered ALTER/DROP migration and the teardown path is closed. **Compare the version field by field, never as a string:** `0.29.0` is under the anchor, `0.30.0` is not, and a lexical compare puts `0.30.0` below `0.9.0` β€” the anchor moved off a major-number boundary precisely so the minor field decides.
413
413
  **Why:** Markers accumulate dead code and still force CI to splitter-parse them. Below 0.30.0 the development database is dropped and re-created from empty, so there is no production data to protect and full removal leaves no false grep hits or stale migration slots. At 0.30.0 that stops being true: the deployment holds data nobody can re-create, which is what makes an in-place migration the only safe shape from then on.
414
414
  **Tags:** sql, process
415
415
  **Example:** harness teardown β€” supersedes prior "replace with `SELECT 1;`" guidance. Under the old rule, comment-only markers broke CI (apostrophe in "slots" opened unterminated string in splitter); full deletion avoids the marker problem entirely.
@@ -1004,7 +1004,7 @@ Never infer the absence of deployed clients from another product's version.
1004
1004
  - RPC / handler signature changes β†’ edit the existing struct + every caller in the same commit. No `Foo` and `FooV2`.
1005
1005
  - "Versioned" / "additive optional fields" framing in a spec is fine β€” what's banned is a *parallel* type, not nullable extensions on the existing one.
1006
1006
  - Removing a behavior β†’ delete the function, the call sites, and any dispatch arm that selected it. Tests of the removed path go too.
1007
- - Schema removals already covered by the Schema Table Removal Guard (rm file, rm `@embedFile`, rm migration array entry) β€” that's this rule applied to SQL.
1007
+ - Schema removals already covered by the Schema Table Removal Guard (rm file, rm migration array entry) β€” that's this rule applied to SQL.
1008
1008
  - Legacy-Design Consult Guard still fires when you find a *pre-existing* legacy shim left over from the v1β†’v2 teardown. This rule says: don't create new ones.
1009
1009
  - Command-line renames follow the same rule: if the product verb moves to `create`, do not add `add` as an alias. If a flag or command spelling changes, use the new spelling only unless Indy explicitly asks for a compatibility alias in the same session.
1010
1010
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsfleet/orly",
3
- "version": "0.14.4",
3
+ "version": "0.14.6",
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",
@@ -44,7 +44,7 @@ describe("opt-in pack selection", () => {
44
44
  const installed = await Promise.all(pages.map((path) => Bun.file(join(repo, ".orly", path)).text()));
45
45
  for (const content of installed) {
46
46
  expect(content).not.toContain("playbooks/founding/");
47
- expect(content).not.toContain("schema/embed.zig");
47
+ expect(content).not.toContain("rustd/crates/afd_db/src/migration.rs");
48
48
  expect(content).not.toContain("src/cmd/common.zig");
49
49
  expect(content).not.toContain("build_runner.zig");
50
50
  expect(content).not.toContain("~/Projects/docs/");
@@ -73,7 +73,7 @@ describe("opt-in pack selection", () => {
73
73
  const sql = await Bun.file(join(repo, ".orly/dispatch/write_sql.md")).text();
74
74
  const zig = await Bun.file(join(repo, ".orly/dispatch/write_zig.md")).text();
75
75
  expect(sql).toContain("0.30.0");
76
- expect(sql).toContain("schema/embed.zig");
76
+ expect(sql).toContain("rustd/crates/afd_db/src/migration.rs");
77
77
  expect(sql).toContain("**Forbidden:**");
78
78
  expect(zig).toContain("build_runner.zig");
79
79
  });
@@ -104,6 +104,13 @@ describe("spec content readiness", () => {
104
104
  });
105
105
 
106
106
  test("oversize specs report their measured size against the cap", () => {
107
- expect(validateSpec(`${VALID}\n${"line\n".repeat(320)}`, COMMANDS, EXISTS).join("\n")).toMatch(/spec has \d+ lines; cap 320/);
107
+ expect(validateSpec(`${VALID}\n${"line\n".repeat(500)}`, COMMANDS, EXISTS).join("\n")).toMatch(/spec has \d+ lines; cap 500/);
108
+ });
109
+
110
+ test("a spec at the cap passes and one line over fails", () => {
111
+ const base = VALID.trimEnd();
112
+ const padded = (lines: number) => `${base}\n${Array(lines - base.split(/\r?\n/).length).fill("line").join("\n")}`;
113
+ expect(validateSpec(padded(500), COMMANDS, EXISTS).join("\n")).not.toMatch(/cap 500/);
114
+ expect(validateSpec(padded(501), COMMANDS, EXISTS)).toContain("spec has 501 lines; cap 500");
108
115
  });
109
116
  });