@agentsfleet/orly 0.9.1 → 0.9.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/audits/cross-tier-rates.sh +5 -5
- package/audits/deinit-pairs.sh +11 -5
- package/audits/error-codes.sh +4 -1
- package/audits/logging.sh +18 -8
- package/audits/spec-template.sh +7 -3
- package/audits/sql-mod.sh +5 -2
- package/audits/ufs.sh +33 -6
- package/dispatch/lib.sh +13 -3
- package/dispatch/lifecycle.md +5 -2
- package/dispatch/write_any.sh +2 -2
- package/dispatch/write_zig.md +1 -1
- package/docs/CHANGELOG_VOICE.md +1 -1
- package/docs/EXECUTE_DOC_READS.md +1 -1
- package/docs/LOGGING_STANDARD.md +2 -2
- package/docs/ORLY_ARCHITECTURE.md +1 -1
- package/docs/REST_API_DESIGN_GUIDELINES.md +1 -6
- package/docs/RULE_ENFORCEMENT.md +2 -2
- package/docs/SCHEMA_CONVENTIONS.md +3 -3
- package/docs/TEMPLATE.md +1 -0
- package/package.json +1 -1
- package/skills/orly-write-integration-test/SKILL.md +1 -1
- package/src/cli.ts +7 -1
- package/src/gates.ts +41 -8
- package/src/gates_spec.test.ts +75 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# cross-tier-rates.sh — pin numeric parity of the rate constants
|
|
3
|
-
# across
|
|
3
|
+
# across the daemon + three TypeScript surfaces.
|
|
4
4
|
#
|
|
5
5
|
# RULE UFS extension. The shipped billing surface depends on these constants
|
|
6
6
|
# having identical numeric values everywhere they appear:
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
#
|
|
17
17
|
# Files (one definition site per constant per file):
|
|
18
18
|
#
|
|
19
|
-
#
|
|
19
|
+
# rustd/crates/afd_billing/src/nanos.rs — server enforces the charge
|
|
20
20
|
# ui/packages/website/src/lib/rates.ts — pricing page display
|
|
21
21
|
# ui/packages/app/lib/types.ts — dashboard display
|
|
22
22
|
# cli/src/constants/billing.ts — `agentsfleet doctor --json` billing block
|
|
23
23
|
#
|
|
24
24
|
# A drift between any two = a billing-display lie or a server-vs-CLI disagreement.
|
|
25
|
-
#
|
|
25
|
+
# The daemon is the source of truth (server enforces); the three TS surfaces echo it.
|
|
26
26
|
#
|
|
27
27
|
# Fires in: make harness-verify, make harness-verify-all.
|
|
28
28
|
# Exits 0 clean, 1 on any drift.
|
|
@@ -37,7 +37,7 @@ readonly NAMES=(
|
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
readonly FILES=(
|
|
40
|
-
|
|
40
|
+
rustd/crates/afd_billing/src/nanos.rs
|
|
41
41
|
ui/packages/website/src/lib/rates.ts
|
|
42
42
|
ui/packages/app/lib/types.ts
|
|
43
43
|
cli/src/constants/billing.ts
|
|
@@ -96,7 +96,7 @@ if [[ $FAIL -ne 0 ]]; then
|
|
|
96
96
|
printf " %s\n" "$v" >&2
|
|
97
97
|
done
|
|
98
98
|
printf "\nFix: align all four files on the canonical Zig value\n" >&2
|
|
99
|
-
printf " (
|
|
99
|
+
printf " (rustd/crates/afd_billing/src/nanos.rs is the server-side enforcer).\n" >&2
|
|
100
100
|
exit 1
|
|
101
101
|
fi
|
|
102
102
|
|
package/audits/deinit-pairs.sh
CHANGED
|
@@ -60,11 +60,14 @@ note() { printf "NOTE: %s\n" "$*"; }
|
|
|
60
60
|
# ---------------------------------------------------------------------------
|
|
61
61
|
case "$MODE" in
|
|
62
62
|
--staged|staged)
|
|
63
|
-
|
|
63
|
+
# while read, not mapfile — bash-3.2 portability (see scripts/run-playbook-tests.sh).
|
|
64
|
+
FILES=()
|
|
65
|
+
while IFS= read -r f; do FILES+=("$f"); done < <(git diff --cached --name-only --diff-filter=ACMRT \
|
|
64
66
|
| grep -E '^src/.*\.zig$' || true)
|
|
65
67
|
;;
|
|
66
68
|
--all|all)
|
|
67
|
-
|
|
69
|
+
FILES=()
|
|
70
|
+
while IFS= read -r f; do FILES+=("$f"); done < <(find src -type f -name '*.zig' 2>/dev/null || true)
|
|
68
71
|
;;
|
|
69
72
|
*)
|
|
70
73
|
printf "usage: %s [--staged|--all]\n" "$0" >&2
|
|
@@ -106,10 +109,13 @@ done
|
|
|
106
109
|
# M70 perf: pre-compute file sets in batched greps instead of one grep
|
|
107
110
|
# per file (~760 forks → 2 forks). Reads as "membership sets" for the
|
|
108
111
|
# per-init loop below.
|
|
109
|
-
declare -A
|
|
112
|
+
# A membership set as a newline-delimited string, not declare -A: associative
|
|
113
|
+
# arrays are bash 4+ and macOS ships 3.2. Lookup is a substring test on
|
|
114
|
+
# \n<file>\n, which filenames cannot fake.
|
|
115
|
+
files_with_cleanup=$'\n'
|
|
110
116
|
if [[ ${#present_files[@]} -gt 0 ]]; then
|
|
111
117
|
while IFS= read -r f; do
|
|
112
|
-
[[ -n "$f" ]] && files_with_cleanup
|
|
118
|
+
[[ -n "$f" ]] && files_with_cleanup="${files_with_cleanup}${f}"$'\n'
|
|
113
119
|
done < <(grep -lE "$CLEANUP_RE" "${present_files[@]}" 2>/dev/null || true)
|
|
114
120
|
fi
|
|
115
121
|
|
|
@@ -151,7 +157,7 @@ if [[ -n "$inits_list" ]]; then
|
|
|
151
157
|
fi
|
|
152
158
|
|
|
153
159
|
inits_requires_cleanup=$((inits_requires_cleanup + 1))
|
|
154
|
-
if [[
|
|
160
|
+
if [[ "$files_with_cleanup" != *$'\n'"$f"$'\n'* ]]; then
|
|
155
161
|
inits_unpaired=$((inits_unpaired + 1))
|
|
156
162
|
reason=""
|
|
157
163
|
[[ $heap -eq 1 ]] && reason="heap-return"
|
package/audits/error-codes.sh
CHANGED
|
@@ -108,7 +108,10 @@ gather_used_paths() {
|
|
|
108
108
|
esac
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
mapfile
|
|
111
|
+
# `while read` rather than mapfile: mapfile is bash 4+ and macOS ships 3.2 —
|
|
112
|
+
# the portability rule scripts/run-playbook-tests.sh already records.
|
|
113
|
+
USED_PATHS=()
|
|
114
|
+
while IFS= read -r p; do USED_PATHS+=("$p"); done < <(gather_used_paths)
|
|
112
115
|
if [[ ${#USED_PATHS[@]} -eq 0 ]]; then
|
|
113
116
|
ok "no source files in scope ($MODE)"
|
|
114
117
|
exit 0
|
package/audits/logging.sh
CHANGED
|
@@ -110,7 +110,10 @@ gather_paths() {
|
|
|
110
110
|
esac
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
mapfile
|
|
113
|
+
# `while read` rather than mapfile: mapfile is bash 4+ and macOS ships 3.2 —
|
|
114
|
+
# the portability rule scripts/run-playbook-tests.sh already records.
|
|
115
|
+
FILES=()
|
|
116
|
+
while IFS= read -r p; do FILES+=("$p"); done < <(gather_paths)
|
|
114
117
|
if [[ ${#FILES[@]} -eq 0 ]]; then
|
|
115
118
|
ok "no source files in scope ($MODE)"
|
|
116
119
|
exit 0
|
|
@@ -139,7 +142,9 @@ for f in "${FILES[@]}"; do
|
|
|
139
142
|
done
|
|
140
143
|
|
|
141
144
|
if [[ ${#rust_nontest[@]} -gt 0 ]]; then
|
|
142
|
-
|
|
145
|
+
rust_candidates=()
|
|
146
|
+
while IFS= read -r p; do rust_candidates+=("$p"); done \
|
|
147
|
+
< <(grep -lE '(^|[^[:alnum:]_])(println|eprintln|dbg)!|tracing::(error|warn|info|debug|trace)!' "${rust_nontest[@]}" 2>/dev/null || true)
|
|
143
148
|
fi
|
|
144
149
|
|
|
145
150
|
# ---------------------------------------------------------------------------
|
|
@@ -388,12 +393,17 @@ fi
|
|
|
388
393
|
# ---------------------------------------------------------------------------
|
|
389
394
|
scoped_hits=0
|
|
390
395
|
scoped_eligible=()
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
396
|
+
# Guarded like every sibling loop: bash 3.2 under `set -u` treats "${empty[@]}"
|
|
397
|
+
# as unbound (fixed only in 4.4), so an unguarded expansion aborts the audit in
|
|
398
|
+
# any repository with no Zig sources. macOS ships 3.2.
|
|
399
|
+
if [[ ${#zig_nontest[@]} -gt 0 ]]; then
|
|
400
|
+
for f in "${zig_nontest[@]}"; do
|
|
401
|
+
case "$f" in
|
|
402
|
+
src/logging/*) continue ;;
|
|
403
|
+
esac
|
|
404
|
+
scoped_eligible+=("$f")
|
|
405
|
+
done
|
|
406
|
+
fi
|
|
397
407
|
if [[ ${#scoped_eligible[@]} -gt 0 ]]; then
|
|
398
408
|
while IFS= read -r line; do
|
|
399
409
|
[[ -z "$line" ]] && continue
|
package/audits/spec-template.sh
CHANGED
|
@@ -48,13 +48,17 @@ note() { printf "NOTE: %s\n" "$*"; }
|
|
|
48
48
|
# Discover spec files in scope.
|
|
49
49
|
case "$MODE" in
|
|
50
50
|
--staged|staged)
|
|
51
|
-
|
|
51
|
+
# while read, not mapfile — bash-3.2 portability (see scripts/run-playbook-tests.sh).
|
|
52
|
+
SPECS=()
|
|
53
|
+
while IFS= read -r s; do SPECS+=("$s"); done < <(git diff --cached --name-only --diff-filter=ACMRT | grep -E '^docs/v[0-9]+/(pending|active|done)/.*\.md$' || true)
|
|
52
54
|
;;
|
|
53
55
|
--all|all)
|
|
54
|
-
|
|
56
|
+
SPECS=()
|
|
57
|
+
while IFS= read -r s; do SPECS+=("$s"); done < <(find docs/v[0-9]* -type f -name '*.md' 2>/dev/null | grep -E '/(pending|active)/' || true)
|
|
55
58
|
;;
|
|
56
59
|
--include-done|include-done)
|
|
57
|
-
|
|
60
|
+
SPECS=()
|
|
61
|
+
while IFS= read -r s; do SPECS+=("$s"); done < <(find docs/v[0-9]* -type f -name '*.md' 2>/dev/null | grep -E '/(pending|active|done)/' || true)
|
|
58
62
|
;;
|
|
59
63
|
--file|file)
|
|
60
64
|
# Single named spec, both families — the lifecycle engine's spec.gate
|
package/audits/sql-mod.sh
CHANGED
|
@@ -46,10 +46,13 @@ added_hit() {
|
|
|
46
46
|
|
|
47
47
|
case "$MODE" in
|
|
48
48
|
--staged|staged)
|
|
49
|
-
|
|
49
|
+
# while read, not mapfile — bash-3.2 portability (see scripts/run-playbook-tests.sh).
|
|
50
|
+
FILES=()
|
|
51
|
+
while IFS= read -r f; do FILES+=("$f"); done < <(git diff --cached --name-only --diff-filter=ACMRT -- '*.zig' || true)
|
|
50
52
|
STAGED=1 ;;
|
|
51
53
|
--all|all)
|
|
52
|
-
|
|
54
|
+
FILES=()
|
|
55
|
+
while IFS= read -r f; do FILES+=("$f"); done < <(find src -type f -name '*.zig' 2>/dev/null || true)
|
|
53
56
|
STAGED=0 ;;
|
|
54
57
|
*)
|
|
55
58
|
printf "usage: %s [--staged|--all]\n" "$0" >&2
|
package/audits/ufs.sh
CHANGED
|
@@ -116,7 +116,12 @@ if [ "$MODE" = "--staged" ]; then
|
|
|
116
116
|
else
|
|
117
117
|
scope_files() { git ls-files; }
|
|
118
118
|
fi
|
|
119
|
-
mapfile
|
|
119
|
+
# `while read` rather than mapfile: mapfile is bash 4+ and macOS ships 3.2 —
|
|
120
|
+
# the portability rule scripts/run-playbook-tests.sh already records.
|
|
121
|
+
FILES=()
|
|
122
|
+
while IFS= read -r f; do
|
|
123
|
+
FILES+=("$f")
|
|
124
|
+
done < <(scope_files | while read -r f; do
|
|
120
125
|
is_source "$f" && echo "$f"
|
|
121
126
|
done)
|
|
122
127
|
|
|
@@ -344,9 +349,13 @@ done < <(awk -v re="$NUMERIC_RE" '
|
|
|
344
349
|
|
|
345
350
|
# ── 3. cross-runtime-orphan ─────────────────────────────────────────────────
|
|
346
351
|
# Full-tree ERR_* parity. Scoped to the ERR_* prefix because that is the
|
|
347
|
-
# cross-runtime contract surface — server error codes a client consumes.
|
|
348
|
-
# the source of truth
|
|
349
|
-
#
|
|
352
|
+
# cross-runtime contract surface — server error codes a client consumes. The
|
|
353
|
+
# daemon is the source of truth. A Rust daemon declares its codes through a
|
|
354
|
+
# registry — `ErrorCode::declare("…")` — and the contract a client holds is
|
|
355
|
+
# that STRING, so parity is checked on the string: every client
|
|
356
|
+
# `export const ERR_X = "…"` must name a code the registry declares. Where no
|
|
357
|
+
# registry exists and Zig does, the Zig `pub const ERR_*` NAMES are the
|
|
358
|
+
# oracle instead. Daemon-only codes are fine; a server-internal code needs no
|
|
350
359
|
# client mirror.
|
|
351
360
|
#
|
|
352
361
|
# Scoped BY RUNTIME, not by one repository's directory names. The previous form
|
|
@@ -381,11 +390,29 @@ parity_codes() {
|
|
|
381
390
|
| grep -oE 'ERR_[A-Z][A-Z0-9_]+' | sort -u || true
|
|
382
391
|
}
|
|
383
392
|
|
|
393
|
+
# The Rust registry outranks a Zig mirror: a runner that keeps a Zig subset of
|
|
394
|
+
# the daemon's codes for its own use is a client, not the oracle. `record`
|
|
395
|
+
# mutates globals, so the loop runs in this shell over a here-string — never on
|
|
396
|
+
# the right of a pipe, which would run it in a subshell and drop every finding.
|
|
397
|
+
rust_codes=$(git ls-files -z -- '*.rs' 2>/dev/null \
|
|
398
|
+
| { grep -zvE "$PARITY_EXCLUDE" || true; } \
|
|
399
|
+
| { xargs -0 grep -hoE 'ErrorCode::declare\("[^"]+"\)' 2>/dev/null || true; } \
|
|
400
|
+
| { grep -oE '"[^"]+"' || true; } | tr -d '"' | sort -u)
|
|
384
401
|
zig_err=$(parity_codes '^pub const ERR_[A-Z][A-Z0-9_]+[[:space:]]*=' '*.zig')
|
|
385
402
|
client_err=$(parity_codes '^export const ERR_[A-Z][A-Z0-9_]+[[:space:]]*=' '*.js' '*.jsx' '*.ts' '*.tsx')
|
|
386
403
|
|
|
387
|
-
if [ -
|
|
388
|
-
|
|
404
|
+
if [ -n "$rust_codes" ]; then
|
|
405
|
+
client_decls=$(git ls-files -z -- '*.js' '*.jsx' '*.ts' '*.tsx' 2>/dev/null \
|
|
406
|
+
| { grep -zvE "$PARITY_EXCLUDE" || true; } \
|
|
407
|
+
| { xargs -0 grep -hE '^export const ERR_[A-Z][A-Z0-9_]+[[:space:]]*=[[:space:]]*"[^"]+"' 2>/dev/null || true; })
|
|
408
|
+
while IFS= read -r line; do
|
|
409
|
+
[ -n "$line" ] || continue
|
|
410
|
+
name=$(printf '%s' "$line" | grep -oE 'ERR_[A-Z][A-Z0-9_]+' | head -1)
|
|
411
|
+
code=$(printf '%s' "$line" | sed -E 's/^[^=]*=[[:space:]]*"([^"]+)".*/\1/')
|
|
412
|
+
echo "$rust_codes" | grep -qxF -- "$code" || record "cross-runtime-orphan $name ($code) absent-in-daemon"
|
|
413
|
+
done <<<"$client_decls"
|
|
414
|
+
elif [ -z "$zig_err" ]; then
|
|
415
|
+
ok "audit-ufs: cross-runtime parity skipped — no Rust registry and no Zig ERR_* declared, so this repository has no source of truth to compare against"
|
|
389
416
|
else
|
|
390
417
|
# Every client-side ERR_* must exist in Zig.
|
|
391
418
|
for c in $client_err; do
|
package/dispatch/lib.sh
CHANGED
|
@@ -53,7 +53,10 @@ DISPATCH_RC=0
|
|
|
53
53
|
# Rule-code gloss map — canonical short expansions (full text in RULES.md legend).
|
|
54
54
|
# Keep in sync with RULES.md; dispatch-coverage.sh fails on a code with no
|
|
55
55
|
# gloss entry. snake-of-record: CODE → "Short Gloss".
|
|
56
|
-
declare -A
|
|
56
|
+
# A heredoc of `[CODE]="Gloss"` lines, not declare -A: associative arrays are
|
|
57
|
+
# bash 4+ and macOS ships 3.2. The line shape is load-bearing —
|
|
58
|
+
# evals/dispatch/coverage.sh greps lib.sh for it (checks f and g).
|
|
59
|
+
DISPATCH_GLOSS="$(cat <<'GLOSS'
|
|
57
60
|
[NDC]="No Dead Code"
|
|
58
61
|
[NLR]="No Legacy Retained (touch-it-fix-it)"
|
|
59
62
|
[NLG]="No Legacy compat shims (pre-v2.0.0)"
|
|
@@ -83,10 +86,17 @@ declare -A DISPATCH_GLOSS=(
|
|
|
83
86
|
[ERR-RS]="Rust ERror discipline (one type per crate, Result alias, no stringified cause)"
|
|
84
87
|
[GRP]="GREptile rule audit (diff vs greptile-learnings/RULES.md codes)"
|
|
85
88
|
[LDC]="Legacy-Design Consult (A remove / B patch / C keep)"
|
|
86
|
-
|
|
89
|
+
GLOSS
|
|
90
|
+
)"
|
|
87
91
|
|
|
88
92
|
# Look up a gloss; empty string if the code is unknown (audit will catch it).
|
|
89
|
-
|
|
93
|
+
# Pure parameter expansion — no fork per finding. `[` is escaped so the code
|
|
94
|
+
# is matched literally, not as a glob class.
|
|
95
|
+
dispatch_gloss() {
|
|
96
|
+
local rest; rest="${DISPATCH_GLOSS#*\[$1\]=\"}"
|
|
97
|
+
[[ "$rest" == "$DISPATCH_GLOSS" ]] && return 0
|
|
98
|
+
printf '%s' "${rest%%\"*}"
|
|
99
|
+
}
|
|
90
100
|
|
|
91
101
|
dispatch_init() {
|
|
92
102
|
DISPATCH_LANG="$1"; shift
|
package/dispatch/lifecycle.md
CHANGED
|
@@ -199,8 +199,11 @@ version-sync check passes where defined; branch contains `origin/main` HEAD
|
|
|
199
199
|
**`orly gate pr` follows the spec through the close.** A spec moved to `done/`
|
|
200
200
|
on this branch is still discovered — its `Branch:` header names the branch —
|
|
201
201
|
and every spec criterion runs against it; skip-pass is only for genuinely
|
|
202
|
-
spec-less branches.
|
|
203
|
-
|
|
202
|
+
spec-less branches. A deliberately folded spec retains the exact branch header
|
|
203
|
+
and adds `**Folded-into:** \`M178_001\`` naming the owning spec. Discovery
|
|
204
|
+
validates that relation and gates the sole non-folded owner; two non-folded
|
|
205
|
+
specs on one branch remain a hard error. Mechanical criteria: `spec.moved`
|
|
206
|
+
(`Status: DONE` ⇒ the spec sits under `done/` and was moved on this branch) · `spec.baseline`
|
|
204
207
|
(`Test Baseline:` recorded) · `spec.ordering` (the branch's first commit
|
|
205
208
|
carries the spec — no code before CHORE(open)) · `spec.deferrals` (a deferral
|
|
206
209
|
claim needs the `> Indy (` ack quote in the spec). An Indy-acked deferral that
|
package/dispatch/write_any.sh
CHANGED
|
@@ -33,12 +33,12 @@ dispatch_run_helper "LOG" "logging.sh" "--staged"
|
|
|
33
33
|
dispatch_run_helper "MSID" "msid-ui.sh" "--staged"
|
|
34
34
|
|
|
35
35
|
# ── delegations (run only in the product repo — need state absent from dotfiles) ──
|
|
36
|
-
# ERR's leaf hard-exits if
|
|
36
|
+
# ERR's leaf hard-exits if the product repo's error registry is absent (it
|
|
37
37
|
# cross-references emitted codes against the registry), unlike UFS/LOG/MSID which
|
|
38
38
|
# pass gracefully on empty input. So it can't run in dotfiles — delegate it; the
|
|
39
39
|
# product repo's `make harness-verify` runs error-codes.sh against the real
|
|
40
40
|
# registry. The .md keeps [DETERMINISTIC → ERR]: deterministic, just not run here.
|
|
41
|
-
dispatch_delegate "ERR" "make harness-verify (error-codes.sh — needs
|
|
41
|
+
dispatch_delegate "ERR" "make harness-verify (error-codes.sh — needs the product repo's error registry)"
|
|
42
42
|
|
|
43
43
|
# ── judgment gates (no script can decide; agent states verdict in chat) ──
|
|
44
44
|
dispatch_judgment "GRP" "per-iteration (diff langs change) + end-of-turn: audit the diff against docs/greptile-learnings/RULES.md — one row per applicable code (UFS/ORP/TST-NAM/PRI/EMS/…); \"it's just a label\" is not an exception"
|
package/dispatch/write_zig.md
CHANGED
|
@@ -480,7 +480,7 @@ Rules:
|
|
|
480
480
|
|
|
481
481
|
New production Zig modules must not define SQL statement text inline. Put query
|
|
482
482
|
text in a domain-local `sql.zig` and import it from the state/handler/service
|
|
483
|
-
module.
|
|
483
|
+
module. The shape is a domain-local `sql.zig` beside the module it serves: table names and
|
|
484
484
|
query statements stay grepable in one place, while the public module owns row
|
|
485
485
|
mapping, allocator ownership, and error translation.
|
|
486
486
|
|
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 flow through three pinned files only:** `
|
|
17
|
+
- **Rate constants flow through three pinned files only:** `rustd/crates/afd_billing/src/nanos.rs`, `ui/packages/website/src/lib/rates.ts`, `~/Projects/docs/snippets/rates.mdx`. Bumping a rate requires a paired Pull Request (PR) across the docs repo — there is no automated guard. In `*.mdx`, use `import { STARTER_CREDIT, EVENT_RATE, STAGE_RATE } from "/snippets/rates.mdx"` instead of hand-typing `$5`.
|
|
18
18
|
- **The Mintlify reference Indy pasted (May 1 / May 8 entries) is canonical voice.** Mirror its rhythm, not its product nouns.
|
|
@@ -22,7 +22,7 @@ Every triggered document requires one `📖 DOC READ: <path>` proof-line **per t
|
|
|
22
22
|
| `*.mdx` | `dispatch/write_mdx.md` — structure, front matter, links, code fences, accessibility, and Mintlify isolation. | <!-- oracle-packs:language.mdx -->
|
|
23
23
|
| Log emit (any language; see LOGGING GATE triggers) | `docs/LOGGING_STANDARD.md` — wire format (logfmt), severity ladder, error-code embedding, scope/event discipline, PII redaction, §10A tightenings. LOGGING GATE per edit. |
|
|
24
24
|
| Lifecycle method in `*.zig` (`init|deinit|close|release|destroy|shutdown|dispose|free`) | `docs/LIFECYCLE_PATTERNS.md` — init/deinit pairing, errdefer placement, allocator ownership, defer/errdefer mutual exclusion, §10A tightenings. LIFECYCLE GATE per edit. |
|
|
25
|
-
| `
|
|
25
|
+
| `rustd/crates/afd_api*/src/handler/**` or `public/openapi/**` | `docs/REST_API_DESIGN_GUIDELINES.md` — Quick Checklist; §1–§5 (URL/method/body/response/error), §6 (OpenAPI), §7 (6-place route registration), §8 (`Hx` handler interface), §10 (pre-PR gates). |
|
|
26
26
|
| `ui/packages/**/*.{tsx,jsx,css}`, `app/**/*.{tsx,jsx,css}`, `components/**/*.{tsx,jsx,css}`, repo-root `globals.css`, or any file changing visual tokens / motion / typography | `DESIGN.md` (repo root) or `docs/DESIGN_SYSTEM.md` — whichever the repo carries. Design system source of truth: typography stack, color tokens, the single accent and its currency rule, motion signature, spacing/density, component principles, CLI palette mapping. DOC READ proof-line per turn. |
|
|
27
27
|
| `*.tsx` / `*.jsx` under `ui/packages/{app,website}/` | `dispatch/write_ts_adhere_bun.md` (Design Tokens) — token-utility table (text/tracking/leading/max-w/min-w/spacing/motion/radius/color). DESIGN TOKEN GATE fires per edit; audit via project-side `audits/design-tokens.sh`. |
|
|
28
28
|
| Auth-flow | `docs/AUTH.md`. |
|
package/docs/LOGGING_STANDARD.md
CHANGED
|
@@ -35,7 +35,7 @@ Documented honestly, not aspirationally. Pre-M62 baseline (the fix-pass converge
|
|
|
35
35
|
|
|
36
36
|
- The named module `log` (source: `src/lib/logging/mod.zig`) exposes `scoped(.tag)` returning a logger struct with `.err` / `.warn` / `.info` / `.debug` methods. M62 removed the older free-function helpers (`logErr` / `logErrWithHint` / `logWarnErr`) — there is no compat shim.
|
|
37
37
|
- TypeScript (`cli/src/**`) calls `console.log`/`console.error` directly. No structure. No scope. No severity beyond err vs out.
|
|
38
|
-
- Error-code embedding (`UZ-XXX-NNN`) appears on some `err` lines but not others. The registry (`
|
|
38
|
+
- Error-code embedding (`UZ-XXX-NNN`) appears on some `err` lines but not others. The registry (`rustd/crates/afd_core/src/error_code.rs`) is the source of truth, but enforcement is voluntary.
|
|
39
39
|
- No collector-friendly format. Logs are a mix of free-form English and ad-hoc `key={value}` fragments.
|
|
40
40
|
|
|
41
41
|
The proposed standard below is what every new emit must conform to and what the fix-pass converges existing emits toward.
|
|
@@ -168,7 +168,7 @@ Log the source (`source=env:OPENAI_API_KEY`), never the value, at every level.
|
|
|
168
168
|
|
|
169
169
|
> [DETERMINISTIC → LOG]
|
|
170
170
|
|
|
171
|
-
Every `err` and `warn` record that maps to a domain error MUST carry `error_code=UZ-XXX-NNN` where `UZ-XXX-NNN` is declared in `
|
|
171
|
+
Every `err` and `warn` record that maps to a domain error MUST carry `error_code=UZ-XXX-NNN` where `UZ-XXX-NNN` is declared in `rustd/crates/afd_core/src/error_code.rs`.
|
|
172
172
|
|
|
173
173
|
- **Used-but-undeclared** (`UZ-FAKE-999` appearing in code, no entry in registry): **blocking** in `make lint`.
|
|
174
174
|
- **Declared-but-unreferenced** (registry has `UZ-LEGACY-007`, no code references it): **informational**. Deletion may be deferred to a sweep milestone.
|
|
@@ -115,7 +115,7 @@ surfaces:
|
|
|
115
115
|
{
|
|
116
116
|
"commands": { "conform": [["make", "harness-verify"]],
|
|
117
117
|
"verify.unit": [["make", "test-unit-all"]] },
|
|
118
|
-
"surfaces": { "user": ["src/
|
|
118
|
+
"surfaces": { "user": ["src/http/", "cli/src/"],
|
|
119
119
|
"docs": ["docs/"] }
|
|
120
120
|
}
|
|
121
121
|
```
|
|
@@ -44,7 +44,6 @@ Run this checklist as part of `CHORE(close)` (per `~/.claude/CLAUDE.md` lifecycl
|
|
|
44
44
|
- [ ] **Versioning** — added/renamed/removed surface listed in PR description; deprecation uses `Deprecation` + `Sunset` headers; new response fields declare `x-stability` (§9)
|
|
45
45
|
- [ ] **Tests** — happy path + one error per `hx.fail` + idempotency double-PATCH + `Idempotency-Key` replay (where applicable) + ETag mismatch (§10)
|
|
46
46
|
- [ ] **Logging** — sensitive ID values are DEBUG-only or carry `// log-id-allowed:` comment; secret-shaped fields are write-only or one-time-read (§11)
|
|
47
|
-
- [ ] **`make check-openapi` clean** — bundle in sync, redocly lint, error-schema + URL-shape checks pass (§6)
|
|
48
47
|
- [ ] **The repository's declared `verify.unit` command is clean** — the route-scope table's exhaustive match and its tests cover the auth gate matrix (§10)
|
|
49
48
|
- [ ] **No file over 350 lines** (§10)
|
|
50
49
|
- [ ] **`gitleaks detect` clean** (§10)
|
|
@@ -410,8 +409,6 @@ Don't invent other extensions without amending this doc.
|
|
|
410
409
|
|
|
411
410
|
## §6 — OpenAPI editing
|
|
412
411
|
|
|
413
|
-
**`public/openapi.json` is a build artifact.** Never edit it directly. Edits get wiped on `make check-openapi` and CI's bundle-in-sync gate fails the PR.
|
|
414
|
-
|
|
415
412
|
The source of truth lives under `public/openapi/`:
|
|
416
413
|
|
|
417
414
|
```
|
|
@@ -428,8 +425,7 @@ public/openapi/
|
|
|
428
425
|
|
|
429
426
|
1. Edit the relevant YAML under `public/openapi/paths/<tag>.yaml`.
|
|
430
427
|
2. Add / rename / remove the corresponding `match()` arm in `src/agentsfleetd/http/router.zig`.
|
|
431
|
-
3.
|
|
432
|
-
4. Commit YAML + bundled JSON + `router.zig` together. Splitting these across commits leaves CI red.
|
|
428
|
+
3. Commit YAML + bundled JSON + `router.zig` together. Splitting these across commits leaves CI red.
|
|
433
429
|
|
|
434
430
|
**Router ↔ openapi.json parity is reviewer-enforced.** There is no mechanical gate cross-checking that every `router.match()` arm has a documented openapi path or vice versa. When you add, rename, or remove a route, both surfaces must move in the same diff and the reviewer must verify it. The previous Python parity gate (`audits/check_openapi_sync.py`) and its data file (`route_manifest.zig`, deleted) were retired in M61_002.
|
|
435
431
|
|
|
@@ -715,7 +711,6 @@ Before opening a PR touching any handler:
|
|
|
715
711
|
- [ ] POST endpoints accepting `Idempotency-Key` have a replay test: same key + same body → cached response; same key + different body → 4xx (§2)
|
|
716
712
|
- [ ] Mutable resources have an ETag/`If-Match` test: stale `If-Match` → 412 with current `etag` returned (§4)
|
|
717
713
|
- [ ] OpenAPI updated — endpoint definition, request/response schemas, error responses
|
|
718
|
-
- [ ] `make check-openapi` — bundle in sync, redocly lint, error-schema + URL-shape checks, router parity
|
|
719
714
|
- [ ] `gitleaks detect` clean
|
|
720
715
|
- [ ] No new file over 350 lines
|
|
721
716
|
|
package/docs/RULE_ENFORCEMENT.md
CHANGED
|
@@ -7,14 +7,14 @@ regenerates this file and fails on any difference. To update it, run
|
|
|
7
7
|
| Rule document | enforced by | judged | acknowledged | classified | trigger |
|
|
8
8
|
|---|---|--:|--:|--:|---|
|
|
9
9
|
| `docs/LOGGING_STANDARD.md` | `LOG` → `logging.sh` | 11 | 13 | 30/30 | mechanical |
|
|
10
|
-
| `docs/REST_API_DESIGN_GUIDELINES.md` | — | 0 | 0 | 0/
|
|
10
|
+
| `docs/REST_API_DESIGN_GUIDELINES.md` | — | 0 | 0 | 0/73 | mechanical |
|
|
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
14
|
| `docs/CHANGELOG_VOICE.md` | — | 0 | 0 | 0/3 | latent |
|
|
15
15
|
| `docs/VERIFY_TIERS.md` | — | 0 | 0 | 0/7 | uncited |
|
|
16
16
|
| `docs/greptile-learnings/RULES.md` | — | 0 | 0 | 0/114 | mechanical |
|
|
17
|
-
| **corpus** | | | | **30/
|
|
17
|
+
| **corpus** | | | | **30/287** | |
|
|
18
18
|
|
|
19
19
|
## How to read this
|
|
20
20
|
|
|
@@ -16,7 +16,7 @@ Destructive changes (`DROP TABLE`, `DROP COLUMN`, type rewrites) require an expl
|
|
|
16
16
|
|
|
17
17
|
- Each SQL file must be **≤100 lines** and **single-concern** (one table, one logical group, or one additive change).
|
|
18
18
|
- 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.
|
|
19
|
-
- Every SQL file must be registered in `schema/embed.zig` (compile-time embed) and `
|
|
19
|
+
- Every SQL file must be registered in `schema/embed.zig` (compile-time embed) and `rustd/crates/afd_db/src/migration.rs` (migration version array).
|
|
20
20
|
- No-op stub files (e.g., columns folded into earlier files) are kept for version history but excluded from the migrations array.
|
|
21
21
|
|
|
22
22
|
## SQL Qualification
|
|
@@ -37,14 +37,14 @@ Destructive changes (`DROP TABLE`, `DROP COLUMN`, type rewrites) require an expl
|
|
|
37
37
|
|
|
38
38
|
- **Column:** `id`
|
|
39
39
|
- **Type:** Universally Unique Identifier (UUID) `PRIMARY KEY`
|
|
40
|
-
- **Generation:** Application-side UUID version 7 (UUIDv7) via `
|
|
40
|
+
- **Generation:** Application-side UUID version 7 (UUIDv7) via `rustd/crates/afd_core/src/id.rs`, never `gen_random_uuid()`.
|
|
41
41
|
- **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.
|
|
42
42
|
- **Foreign keys reference the primary key**, never a secondary unique constraint.
|
|
43
43
|
- **Constraint:** every table carries a UUIDv7 CHECK:
|
|
44
44
|
```sql
|
|
45
45
|
CONSTRAINT ck_{table}_id_uuidv7 CHECK (substring(id::text from 15 for 1) = '7')
|
|
46
46
|
```
|
|
47
|
-
- **Adding a new table:** add a `generate{TableName}Id()` function to `
|
|
47
|
+
- **Adding a new table:** add a `generate{TableName}Id()` function to `rustd/crates/afd_core/src/id.rs`.
|
|
48
48
|
- **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.
|
|
49
49
|
- **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.
|
|
50
50
|
|
package/docs/TEMPLATE.md
CHANGED
|
@@ -100,6 +100,7 @@ SPEC AUTHORING RULES (load-bearing — the one comment that survives):
|
|
|
100
100
|
**Categories:** {API | CLI | UI | SKILL | DOCS | OBS | INFRA — alphabetised, one or more}
|
|
101
101
|
**Batch:** B{1-4} — {parallel execution context}
|
|
102
102
|
**Branch:** {feat/mNN-name — added at CHORE(open)}
|
|
103
|
+
<!-- A deliberately folded workstream adds Folded-into metadata per dispatch/lifecycle.md. -->
|
|
103
104
|
**Test Baseline:** set at CHORE(open) — `unit=<N> integration=<M>` from the repository's declared `verify.*` commands (`.oracle/orly.json`)
|
|
104
105
|
**Depends on:** {M{N}_{NNN} (one-line reason), …}
|
|
105
106
|
**Provenance:** human-written | LLM-drafted ({model}, {date}) | agent-generated (pre-spec, {source doc})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentsfleet/orly",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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",
|
|
@@ -80,7 +80,7 @@ Spec asserts "503 on Redis down", code returns 200 → test the spec, flag the c
|
|
|
80
80
|
| **Standard** | New service method, new Redis stream/key, schema change with logic | + T4 + T5 + T6 |
|
|
81
81
|
| **Hardening** | Auth / payment / lease / migration / streaming / anything in the data-loss radius | + T7 (if applicable) + T8 + chaos pass |
|
|
82
82
|
|
|
83
|
-
Auto-detect from diff: `src/auth/**`, `src/agent/leases/**`, `src/runner/**` + `
|
|
83
|
+
Auto-detect from diff: `src/auth/**`, `src/agent/leases/**`, `src/runner/**` + `rustd/crates/afd_fleet/**` (the lease/reclaim/fence + client-daemon surface → T9), schema migrations, streaming handlers → Hardening. CRUD-only against existing schema → Smoke. Default → Standard.
|
|
84
84
|
|
|
85
85
|
Any operation that touches multiple systems or acquires the same resource more than once is **Standard at minimum** and must run T4, even when the endpoint is otherwise CRUD-only.
|
|
86
86
|
|
package/src/cli.ts
CHANGED
|
@@ -15,6 +15,8 @@ type CliResult = { exitCode: number; gate?: string; failedCriterion?: string };
|
|
|
15
15
|
const PASS_RESULT = "pass";
|
|
16
16
|
const NOT_REQUIRED_RESULT = "not-required";
|
|
17
17
|
const ACCEPT_DIRTY_FLAG = "--accept-dirty";
|
|
18
|
+
const HELP_FLAG = "--help";
|
|
19
|
+
const SHORT_HELP_FLAG = "-h";
|
|
18
20
|
const REASON_FLAG = "--reason";
|
|
19
21
|
const PIPE_OUTPUT = "pipe";
|
|
20
22
|
const PACKAGE_MANIFEST = "package.json";
|
|
@@ -59,7 +61,7 @@ async function execute(engineRoot: string, args: string[]): Promise<CliResult> {
|
|
|
59
61
|
|
|
60
62
|
async function run(model: RulesModel, args: string[]): Promise<CliResult> {
|
|
61
63
|
const [command, ...rest] = args;
|
|
62
|
-
if (!command || command ===
|
|
64
|
+
if (!command || command === HELP_FLAG || command === SHORT_HELP_FLAG) {
|
|
63
65
|
printHelp();
|
|
64
66
|
return cliResult(command ? 0 : 1);
|
|
65
67
|
}
|
|
@@ -84,6 +86,10 @@ async function run(model: RulesModel, args: string[]): Promise<CliResult> {
|
|
|
84
86
|
// Read-only: run every gate in order (stop at the first red group), or one
|
|
85
87
|
// named gate. Nothing is ever written.
|
|
86
88
|
function gate(model: RulesModel, args: string[]): CliResult {
|
|
89
|
+
if (args.includes(HELP_FLAG) || args.includes(SHORT_HELP_FLAG)) {
|
|
90
|
+
printHelp();
|
|
91
|
+
return cliResult(0);
|
|
92
|
+
}
|
|
87
93
|
const acceptDirty = args.includes(ACCEPT_DIRTY_FLAG);
|
|
88
94
|
const named = args.find((argument) => !argument.startsWith("-"));
|
|
89
95
|
if (named !== undefined && !isGateName(named)) throw new OrlyError(`unknown gate: ${named} (work, verify, pr)`);
|
package/src/gates.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join, relative } from "node:path";
|
|
2
|
+
import { basename, join, relative } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { criteriaFor, CriterionContext, CriterionResult } from "./criteria";
|
|
5
5
|
import { UNSCOPED_ENVIRONMENT } from "./git_env";
|
|
@@ -14,7 +14,8 @@ const GIT = "git";
|
|
|
14
14
|
const DOCS_DIRECTORY = "docs";
|
|
15
15
|
const ACTIVE_DIRECTORY = "active";
|
|
16
16
|
const DONE_DIRECTORY = "done";
|
|
17
|
-
const BRANCH_HEADER = "Branch
|
|
17
|
+
const BRANCH_HEADER = "Branch";
|
|
18
|
+
const FOLDED_INTO_HEADER = "Folded-into";
|
|
18
19
|
const DEFAULT_BRANCHES = ["main", "master"];
|
|
19
20
|
const UTF8 = "utf8";
|
|
20
21
|
// Accept every real layout: docs/v1/, docs/v2/, docs/v0.9.2/ — cache-kit
|
|
@@ -22,6 +23,8 @@ const UTF8 = "utf8";
|
|
|
22
23
|
const PROTOTYPE_PATTERN = /^v\d+(\.\d+)*$/;
|
|
23
24
|
const MARKDOWN_EXTENSION = ".md";
|
|
24
25
|
const NEWLINE = "\n";
|
|
26
|
+
const MARKDOWN_HEADER_TOKEN_PATTERN = /^\s*\*\*([^*]+):\*\*\s*`([^`]+)`/;
|
|
27
|
+
const SPEC_IDENTIFIER_PATTERN = /^(M\d+_\d+)(?:_|\.md$)/;
|
|
25
28
|
// Strict trailer shape: "Orly-Override: <criterion> (<reason>)". A trailer
|
|
26
29
|
// that does not parse is not an override — the gate stays red rather than
|
|
27
30
|
// guessing what a malformed waiver meant.
|
|
@@ -36,6 +39,12 @@ export type GateReport = {
|
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
export type Override = { criterion: string; reason: string };
|
|
42
|
+
type ClosedSpec = {
|
|
43
|
+
path: string;
|
|
44
|
+
identifier: string | undefined;
|
|
45
|
+
branch: string | undefined;
|
|
46
|
+
foldedInto: string | undefined;
|
|
47
|
+
};
|
|
39
48
|
|
|
40
49
|
// Run one gate: evaluate its criteria fresh from git + the working tree.
|
|
41
50
|
// A red criterion with a matching Orly-Override trailer on the branch is
|
|
@@ -110,10 +119,22 @@ export function activeSpecPath(root: string): string | undefined {
|
|
|
110
119
|
export function closedSpecPath(root: string): string | undefined {
|
|
111
120
|
const branch = gitOutput(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
112
121
|
if (!branch || DEFAULT_BRANCHES.includes(branch)) return undefined;
|
|
113
|
-
const specs = specPathsUnder(root, DONE_DIRECTORY)
|
|
122
|
+
const specs = specPathsUnder(root, DONE_DIRECTORY)
|
|
123
|
+
.map(closedSpec)
|
|
124
|
+
.filter((spec): spec is ClosedSpec => spec !== undefined && spec.branch === branch);
|
|
114
125
|
if (specs.length === 0) return undefined;
|
|
115
|
-
|
|
116
|
-
|
|
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;
|
|
117
138
|
}
|
|
118
139
|
|
|
119
140
|
// An in-flight (active/) spec wins; otherwise the branch's closed spec gates.
|
|
@@ -124,11 +145,23 @@ export function specPathFor(root: string): { path: string; closed: boolean } | u
|
|
|
124
145
|
return closed ? { path: closed, closed: true } : undefined;
|
|
125
146
|
}
|
|
126
147
|
|
|
127
|
-
function
|
|
148
|
+
function closedSpec(path: string): ClosedSpec | undefined {
|
|
128
149
|
try {
|
|
129
|
-
|
|
150
|
+
const metadata: ClosedSpec = {
|
|
151
|
+
path,
|
|
152
|
+
identifier: basename(path).match(SPEC_IDENTIFIER_PATTERN)?.[1],
|
|
153
|
+
branch: undefined,
|
|
154
|
+
foldedInto: undefined,
|
|
155
|
+
};
|
|
156
|
+
for (const line of readFileSync(path, UTF8).split(/\r?\n/)) {
|
|
157
|
+
if (line.startsWith("## ")) break;
|
|
158
|
+
const match = line.match(MARKDOWN_HEADER_TOKEN_PATTERN);
|
|
159
|
+
if (match?.[1] === BRANCH_HEADER) metadata.branch = match[2];
|
|
160
|
+
if (match?.[1] === FOLDED_INTO_HEADER) metadata.foldedInto = match[2];
|
|
161
|
+
}
|
|
162
|
+
return metadata;
|
|
130
163
|
} catch {
|
|
131
|
-
return
|
|
164
|
+
return undefined;
|
|
132
165
|
}
|
|
133
166
|
}
|
|
134
167
|
|
package/src/gates_spec.test.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { afterEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
-
import { activeSpecPath, runGate } from "./gates";
|
|
5
|
+
import { activeSpecPath, closedSpecPath, runGate } from "./gates";
|
|
6
6
|
import {
|
|
7
|
-
cleanupTemporaryDirectories, closedSpecRepository, git, modelFor,
|
|
8
|
-
newRepository, newSpecRepository, SPEC_RELATIVE, specFixture,
|
|
7
|
+
cleanupTemporaryDirectories, closedSpecRepository, fixtureRegistry, git, modelFor,
|
|
8
|
+
newRepository, newSpecRepository, orly, SPEC_RELATIVE, specFixture,
|
|
9
9
|
} from "./gates_test_support";
|
|
10
10
|
|
|
11
11
|
afterEach(cleanupTemporaryDirectories);
|
|
@@ -39,6 +39,78 @@ describe("spec discovery", () => {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
describe("closed-spec follow-through", () => {
|
|
42
|
+
test("two owning specs naming one branch remain a hard error", async () => {
|
|
43
|
+
const project = closedSpecRepository("feat/shared");
|
|
44
|
+
mkdirSync(join(project, "docs/v2/done"), { recursive: true });
|
|
45
|
+
await Bun.write(join(project, "docs/v2/done/M100_001_P2_CLI_SECOND.md"), specFixture("DONE", "feat/shared"));
|
|
46
|
+
|
|
47
|
+
expect(() => closedSpecPath(project)).toThrow("one stream per worktree");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a folded spec yields ownership to the spec it names", async () => {
|
|
51
|
+
const project = closedSpecRepository("feat/shared");
|
|
52
|
+
mkdirSync(join(project, "docs/v2/done"), { recursive: true });
|
|
53
|
+
await Bun.write(
|
|
54
|
+
join(project, "docs/v2/done/M100_001_P2_CLI_FOLDED.md"),
|
|
55
|
+
specFixture("DONE", "feat/shared", ["**Folded-into:** `M99_001`"]),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(closedSpecPath(project)).toEndWith("M99_001_P2_CLI_FIXTURE.md");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("a folded spec must name the exact owner on its branch", async () => {
|
|
62
|
+
const project = closedSpecRepository("feat/shared");
|
|
63
|
+
mkdirSync(join(project, "docs/v2/done"), { recursive: true });
|
|
64
|
+
await Bun.write(
|
|
65
|
+
join(project, "docs/v2/done/M100_001_P2_CLI_FOLDED.md"),
|
|
66
|
+
specFixture("DONE", "feat/shared", ["**Folded-into:** `M404_001`"]),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(() => closedSpecPath(project)).toThrow("must name their owner M99_001");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a folded spec cannot name itself", async () => {
|
|
73
|
+
const project = closedSpecRepository("feat/shared");
|
|
74
|
+
mkdirSync(join(project, "docs/v2/done"), { recursive: true });
|
|
75
|
+
await Bun.write(
|
|
76
|
+
join(project, "docs/v2/done/M100_001_P2_CLI_FOLDED.md"),
|
|
77
|
+
specFixture("DONE", "feat/shared", ["**Folded-into:** `M100_001`"]),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(() => closedSpecPath(project)).toThrow("cannot fold into themselves");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a branch name is not a prefix match", async () => {
|
|
84
|
+
const project = newRepository();
|
|
85
|
+
git(project, "checkout", "-q", "-b", "feat/foo");
|
|
86
|
+
mkdirSync(join(project, "docs/v1/done"), { recursive: true });
|
|
87
|
+
await Bun.write(join(project, "docs/v1/done/M99_001_P2_CLI_OTHER.md"), specFixture("DONE", "feat/foo-2"));
|
|
88
|
+
|
|
89
|
+
expect(closedSpecPath(project)).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("prose mentioning a branch does not declare ownership", async () => {
|
|
93
|
+
const project = newRepository();
|
|
94
|
+
git(project, "checkout", "-q", "-b", "feat/foo");
|
|
95
|
+
mkdirSync(join(project, "docs/v1/done"), { recursive: true });
|
|
96
|
+
await Bun.write(
|
|
97
|
+
join(project, "docs/v1/done/M99_001_P2_CLI_OTHER.md"),
|
|
98
|
+
specFixture("DONE", undefined, ["**Branch:** folded into `feat/foo` rather than taken as its own tree"]),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(closedSpecPath(project)).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("gate help does not discover specs", async () => {
|
|
105
|
+
const project = closedSpecRepository("feat/shared");
|
|
106
|
+
mkdirSync(join(project, "docs/v2/done"), { recursive: true });
|
|
107
|
+
await Bun.write(join(project, "docs/v2/done/M100_001_P2_CLI_SECOND.md"), specFixture("DONE", "feat/shared"));
|
|
108
|
+
|
|
109
|
+
const result = orly(project, fixtureRegistry(project), "gate", "--help");
|
|
110
|
+
expect(result.code).toBe(0);
|
|
111
|
+
expect(result.output).toContain("orly gate");
|
|
112
|
+
});
|
|
113
|
+
|
|
42
114
|
test("a spec closed to done/ is discovered by its Branch: header and still gated", async () => {
|
|
43
115
|
const project = closedSpecRepository("feat/closed");
|
|
44
116
|
const model = await modelFor(project);
|