Skip to content

Commit 7b9cc31

Browse files
authored
Merge pull request #639 from Fr-e-d/contrib/sync-1785269253
sync: update 3 file(s) in core/
2 parents 61282db + baf9e78 commit 7b9cc31

3 files changed

Lines changed: 68 additions & 45 deletions

File tree

.gaai/core/contexts/rules/base.rules.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,18 @@ Auxiliary states: `deferred`, `blocked`, `cancelled`, `superseded`, `escalated`.
108108

109109
## Backlog Archiving Rules
110110

111-
A `done` item may be archived (moved from `active.backlog.yaml` to `done/`) once it is terminal. Archiving is a **file split, not a deletion**: the archive lives in the same repository, under version control, and is never pruned.
111+
An item may be archived (moved from `active.backlog.yaml` to `done/`) once its status is `done`, `cancelled`, or `superseded`. Archiving is a **file split, not a deletion**: the archive lives in the same repository, under version control, and is never pruned.
112112

113-
**The resolution set is `active.backlog.yaml``done/*.yaml`.** After archiving, verify: every `dependencies` entry across the active backlog resolves to an item present in **that union**. An id that exists in neither is a broken ref — **zero broken refs allowed**.
113+
**A dependency id resolves if it names a row in any backlog file — `active.backlog.yaml`, `blocked.backlog.yaml`, or `done/*.yaml`.** An id that names a row in none of them is a broken ref: **zero broken refs allowed**. Note this is an *existence* test, not a *satisfaction* test — resolving proves the referenced item is findable, not that it is complete. Whether a dependency is satisfied is a separate, status-based question the scheduler answers.
114114

115115
Dependencies on `done` items are historical (execution order) but must be preserved for traceability — never cleared or stripped. Archiving an item therefore never invalidates an edge pointing at it, and never requires pulling a dependency closure back into the active backlog.
116116

117117
Two further invariants, both mechanically checkable:
118118

119119
1. An id must appear **at most once** in `active.backlog.yaml`. Per-story tooling is id-keyed and silently takes the first match, so a duplicate desynchronises recorded status from actual delivery.
120-
2. An archived item must not be **re-added** to the active backlog under the same id while its archived row still exists, except as a deliberate minimal traceability stub. Prefer moving over copying.
120+
2. An id must live in **exactly one** backlog file. Do not copy a row into the active backlog to make a reference resolve — the resolution set above already covers it, and a second copy violates the single-source-of-truth rule in `backlog.rules.md`. Move, never duplicate.
121121

122-
> **Amended 2026-07-28.** Step 4 previously required every dependency to resolve inside `active.backlog.yaml` alone, which forced a `done` item's entire transitive closure to stay unarchived for as long as any non-terminal item depended on it. Three findings drove the amendment: the scheduler already resolves dependencies over the union (archived ids count as satisfied), so the stricter reading protected nothing operationally; the strict form fought the archive's own purpose, since one live item could pin an arbitrarily large closure in the active file indefinitely; and it was being worked around in practice rather than followed. A rule that is unenforceable, unenforced, and generates churn is worse than one that matches the tooling. The traceability the old step 4 protected is preserved by the union plus the never-strip-edges rule.
122+
> **Amended 2026-07-28.** Step 4 previously required every dependency to resolve inside `active.backlog.yaml` alone, which forced a `done` item's entire transitive closure to stay unarchived for as long as any non-terminal item depended on it. Three findings drove the amendment: the scheduler already resolves dependencies over the union (archived ids count as satisfied), so the stricter reading protected nothing operationally; the strict form fought the archive's own purpose, since one live item could pin an arbitrarily large closure in the active file indefinitely; and it was being worked around in practice rather than followed. A rule that is unenforceable, unenforced, and generates churn is worse than one that matches the tooling. The traceability the old step 4 protected is preserved by the union plus the never-strip-edges rule. A consumer sweep found no tool that resolves dependencies from the active backlog without also reading the archive.
123123
124124
---
125125

.gaai/core/hooks/pre-push.d/16-backlog-dep-integrity.sh

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,50 @@
11
#!/usr/bin/env bash
22
# Pre-push validator : backlog dependency integrity.
33
#
4-
# Enforces the two mechanically-checkable invariants in base.rules.md
5-
# § Backlog Archiving Rules. Both were prose-only for a long time, and both had
6-
# been violated in real backlogs for weeks at a time without anything noticing —
7-
# which is the whole argument for a hook: a rule nothing checks is a rule that
8-
# silently rots.
4+
# Checks two things from base.rules.md § Backlog Archiving Rules that a machine
5+
# can decide. Both were prose-only for a long time, and both had been violated in
6+
# real backlogs for weeks without anything noticing — which is the argument for a
7+
# hook at all: a rule nothing checks is a rule that silently rots.
98
#
10-
# ERROR — a `dependencies` id that exists in NEITHER active.backlog.yaml NOR
11-
# done/*.yaml. Genuinely dangling: a typo, or an item deleted instead of
12-
# archived. Nothing can ever resolve it.
9+
# ERROR — a dependency id that names a row in NO backlog file (active, blocked,
10+
# or any done/*.yaml). Genuinely dangling: a typo, or an item deleted
11+
# instead of archived. Nothing can ever resolve it.
1312
#
14-
# ERROR — a duplicate id WITHIN active.backlog.yaml. Per-story extractors are
15-
# id-keyed and silently take the first match, so a duplicate
16-
# desynchronises recorded status from actual delivery — the failure
17-
# presents as a story looping on retries when its work already merged.
13+
# ERROR — a duplicate id WITHIN active.backlog.yaml. Per-story tooling is
14+
# id-keyed and silently takes the first match, so a duplicate
15+
# desynchronises recorded status from actual delivery — it presents as
16+
# a story looping on retries when its work has already merged.
1817
#
19-
# Deliberately NOT flagged: a dependency that resolves only through done/*.yaml.
20-
# That is correct by base.rules.md — the resolution set is the union of the active
21-
# backlog and the archive, and the scheduler resolves it the same way. Warning on
22-
# it would fire on every push of any mature backlog, and a warning that always
23-
# fires is a warning nobody reads.
18+
# Deliberately NOT flagged: a dependency resolving only through done/*.yaml. That
19+
# is correct — the resolution set is the union of all backlog files, and the
20+
# scheduler resolves it the same way. Warning on it would fire on every push of
21+
# any mature backlog, and a warning that always fires is one nobody reads.
2422
#
25-
# Also not flagged: an id present in both active and an archive file. A minimal
26-
# traceability stub in the active backlog, with the full historical record left in
27-
# the archive, is an explicitly permitted pattern.
23+
# Existence, not satisfaction: this hook answers "is the referenced row findable",
24+
# never "is that dependency complete". Satisfaction is status-based and belongs to
25+
# the scheduler; duplicating that logic here would be a second source of truth.
2826
#
29-
# Cost : ~100ms (PyYAML over the active backlog plus done/*.yaml).
27+
# Fails OPEN when python3 or PyYAML is unavailable. Core tooling does not assume
28+
# PyYAML, and a governance nicety must never be the reason a push is refused on a
29+
# machine whose backlog is fine.
30+
#
31+
# Cost : ~1s on a large backlog (PyYAML over every backlog file).
3032
# Failure mode : push aborted, with each offending id named.
3133

3234
set -e
3335

3436
REPO_ROOT="$(git rev-parse --show-toplevel)"
35-
BACKLOG="$REPO_ROOT/.gaai/project/contexts/backlog/active.backlog.yaml"
37+
BACKLOG="${GAAI_BACKLOG_PATH:-$REPO_ROOT/.gaai/project/contexts/backlog/active.backlog.yaml}"
3638

3739
[ -f "$BACKLOG" ] || exit 0
40+
command -v python3 >/dev/null 2>&1 || exit 0
41+
python3 -c "import yaml" >/dev/null 2>&1 || exit 0
3842

39-
python3 - "$REPO_ROOT" <<'PY'
43+
python3 - "$BACKLOG" <<'PY'
4044
import sys, glob, os, yaml
4145
42-
root = sys.argv[1]
43-
backlog_dir = os.path.join(root, ".gaai/project/contexts/backlog")
44-
active_path = os.path.join(backlog_dir, "active.backlog.yaml")
46+
active_path = sys.argv[1]
47+
backlog_dir = os.path.dirname(active_path)
4548
4649
4750
def load(path):
@@ -54,27 +57,46 @@ def load(path):
5457
5558
try:
5659
active = load(active_path)
57-
except Exception:
58-
# The YAML-parse validator owns parse errors and reports them properly.
60+
except Exception as exc:
61+
print(f"⚠ pre-push: could not parse {os.path.basename(active_path)} "
62+
f"({exc.__class__.__name__}) — dependency check skipped.", file=sys.stderr)
5963
sys.exit(0)
6064
61-
active_ids = {i["id"] for i in active if isinstance(i, dict) and "id" in i}
6265
63-
archived_ids = set()
64-
for path in sorted(glob.glob(os.path.join(backlog_dir, "done", "*.yaml"))):
66+
def deps_of(item):
67+
"""The scheduler accepts `dependencies:` and `depends_on:`, list or string."""
68+
out = []
69+
for key in ("dependencies", "depends_on"):
70+
raw = item.get(key)
71+
if raw is None:
72+
continue
73+
if isinstance(raw, str):
74+
raw = [p.strip() for p in raw.strip("[]").split(",")]
75+
if isinstance(raw, (list, tuple)):
76+
out.extend(d.strip() for d in raw if isinstance(d, str) and d.strip())
77+
return out
78+
79+
80+
# Resolution set = every backlog file: active, blocked, and the archive.
81+
resolvable = set()
82+
sibling_files = [active_path]
83+
blocked_path = os.path.join(backlog_dir, "blocked.backlog.yaml")
84+
if os.path.isfile(blocked_path):
85+
sibling_files.append(blocked_path)
86+
sibling_files.extend(sorted(glob.glob(os.path.join(backlog_dir, "done", "*.yaml"))))
87+
88+
for path in sibling_files:
6589
try:
66-
archived_ids.update(
67-
i["id"] for i in load(path) if isinstance(i, dict) and "id" in i
90+
resolvable.update(
91+
i["id"] for i in load(path)
92+
if isinstance(i, dict) and isinstance(i.get("id"), str)
6893
)
6994
except Exception:
7095
continue
7196
72-
# base.rules.md: the resolution set is active ∪ done/*.yaml.
73-
resolvable = active_ids | archived_ids
74-
7597
seen, duplicates = set(), set()
7698
for item in active:
77-
if not isinstance(item, dict) or "id" not in item:
99+
if not isinstance(item, dict) or not isinstance(item.get("id"), str):
78100
continue
79101
if item["id"] in seen:
80102
duplicates.add(item["id"])
@@ -84,7 +106,7 @@ dangling = []
84106
for item in active:
85107
if not isinstance(item, dict):
86108
continue
87-
for dep in item.get("dependencies") or []:
109+
for dep in deps_of(item):
88110
if dep not in resolvable:
89111
dangling.append((item.get("id", "?"), dep))
90112
@@ -102,9 +124,9 @@ if dangling:
102124
print("❌ pre-push: backlog dependencies that exist nowhere — push aborted.", file=sys.stderr)
103125
for sid, dep in dangling:
104126
print(f" {sid} -> {dep}", file=sys.stderr)
105-
print(" The id is in neither active.backlog.yaml nor done/*.yaml: a typo, or an", file=sys.stderr)
106-
print(" item deleted instead of archived.", file=sys.stderr)
107-
print(" Fix the id, or replace the story's dependency set via the repair-deps action.", file=sys.stderr)
127+
print(" The id names no row in active.backlog.yaml, blocked.backlog.yaml or", file=sys.stderr)
128+
print(" done/*.yaml: a typo, or an item deleted instead of archived.", file=sys.stderr)
129+
print(" Fix the id, or correct that item's dependency list in the backlog.", file=sys.stderr)
108130
109131
if duplicates or dangling:
110132
print("", file=sys.stderr)

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
## [Unreleased]
1111

1212
### Changed
13+
- fix: correct the amended rule and the core hook after review FAIL
1314
-: resolve Plan/QA daemon-prompt $GAAI_* tokens at construction time
1415
- test: repair 37 pre-existing daemon-state-machine failures (origin/staging fixtures)
1516
- fix: reconcile plan-prompt EXECUTE-NOW block with sanctioned plan-blocked exits

0 commit comments

Comments
 (0)