feat(#58): L3 dormant trigger guard -- DORMANT-TRIGGERS.yaml + checker + orchestrator hook
P5-1 docs/architecture/DORMANT-TRIGGERS.yaml -- 5 entries (IMP-16/17/18/19 active + IMP-20 followup-linked #55). P5-2 scripts/check_dormant_triggers.py -- standalone, reads registry, scans tree + diff, writes .orchestrator/dormant_alerts.json, exit 0 always. P5-3 orchestrator.py -- _check_dormant_triggers() helper + Stage 4->5 informational alert branch (skips audit-only, never blocks). P5-4 tests/orchestrator_unit/test_dormant_triggers.py -- 30 cases (yaml schema, registry contents, checker matching, false-positive guards, manual-evidence skip, orchestrator branch, audit bypass, governance ref). P5-5 PROJECT-INTENT-AND-GOVERNANCE.md -- single anti-patterns row referencing the L3 registry as binding contract surface. Tests: pytest -q tests = 337 passed (baseline 307 + 30 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -855,6 +855,47 @@ def _check_audit_commit_scope():
|
||||
bad.append(path)
|
||||
return bad
|
||||
|
||||
# P5-2 (2026-05-20) — Dormant trigger guard (L3 layer, issue #58).
|
||||
# Closed dormant backlog rows (documented:dormant / documented:deferred) carry
|
||||
# implicit "trigger-on-X" contracts. This helper invokes the standalone
|
||||
# checker (scripts/check_dormant_triggers.py) which reads the machine-readable
|
||||
# registry (docs/architecture/DORMANT-TRIGGERS.yaml) and writes activation
|
||||
# candidates to .orchestrator/dormant_alerts.json.
|
||||
#
|
||||
# Guardrails (per Stage 1 scope-lock) :
|
||||
# - Informational only. Returns the alert list; orchestrator never blocks.
|
||||
# - manual_evidence_required / followup-linked entries are skipped INSIDE
|
||||
# the checker (not duplicated here — registry is single source of truth).
|
||||
# - No LLM call. Deterministic subprocess invocation only.
|
||||
# - Fail-open : any subprocess / json error returns [] (no false positives).
|
||||
def _check_dormant_triggers():
|
||||
"""P5-2 — Run scripts/check_dormant_triggers.py and return the alert list.
|
||||
|
||||
Returns: list[dict] of activation-candidate alerts (empty list = no
|
||||
candidates OR script / parse error). Orchestrator never blocks on this."""
|
||||
script_path = Path(PROJECT_DIR) / "scripts" / "check_dormant_triggers.py"
|
||||
if not script_path.exists():
|
||||
return [] # registry / checker not installed yet — fail open
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[sys.executable, str(script_path)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||
cwd=PROJECT_DIR, timeout=30,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
return [] # script error — fail open
|
||||
except Exception:
|
||||
return []
|
||||
alert_path = ORCH_DIR / "dormant_alerts.json"
|
||||
if not alert_path.exists():
|
||||
return []
|
||||
try:
|
||||
payload = json.loads(alert_path.read_text(encoding="utf-8"))
|
||||
alerts = payload.get("alerts", [])
|
||||
return alerts if isinstance(alerts, list) else []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
# P1-5 (2026-05-18) — Stage 2 compact rule (모든 issue 적용).
|
||||
# Stage 2 의 c-role 에 size budget + code snippet 금지 명시. 29 KB plan 차단.
|
||||
COMPACT_PLAN_RULE = """
|
||||
@@ -1430,6 +1471,33 @@ def run_stage(n, title, body, sid):
|
||||
except: pass
|
||||
continue
|
||||
|
||||
# P5-2 (2026-05-20) — Dormant trigger guard (L3 layer, issue #58).
|
||||
# Stage 4 (test-verify) PASS → run dormant trigger checker against the
|
||||
# current change surface. If alerts written, post INFORMATIONAL supplement
|
||||
# comment. NEVER blocks Stage 5 entry (checker is exit 0; helper fail-open).
|
||||
# Audit-only issues skip — their change surface is restricted to audit docs,
|
||||
# which the registry does not watch.
|
||||
if sid == "test-verify" and not _audit_mode(title):
|
||||
alerts = _check_dormant_triggers()
|
||||
if alerts:
|
||||
log(f"ℹ️ Dormant trigger guard: {len(alerts)} activation candidate(s) detected (informational)")
|
||||
try: gitea(f"issues/{n}/comments", "POST", {"body":
|
||||
"ℹ️ **[Orchestrator]** Dormant trigger guard — informational alert (does NOT block Stage 5).\n\n"
|
||||
"The following closed dormant backlog axes have changed-file evidence matching their "
|
||||
"activation triggers. Registry: `docs/architecture/DORMANT-TRIGGERS.yaml`. "
|
||||
"Alert artifact: `.orchestrator/dormant_alerts.json`.\n\n" +
|
||||
"\n".join(
|
||||
f"- **#{a.get('issue')}** {a.get('title')} → "
|
||||
f"`{(a.get('on_trigger') or {}).get('action', '?')}` "
|
||||
f"({len(((a.get('match') or {}).get('files')) or [])} file(s))"
|
||||
for a in alerts[:10]
|
||||
) +
|
||||
("\n - ... (truncated)" if len(alerts) > 10 else "") + "\n\n"
|
||||
"Recommended next step : open a follow-up issue using the `template:` field in the "
|
||||
"registry, OR acknowledge in the next stage comment. Stage 5 proceeds regardless."})
|
||||
except: pass
|
||||
# Never `continue` — checker is informational only (Stage 1 guardrail).
|
||||
|
||||
log(f"✅ {si['label']} — YES (evidence verified)")
|
||||
# stage 완료 = unit counter + remaining tracker 모두 reset
|
||||
update_issue_state(n, continue_same_count=0, last_remaining_units=None)
|
||||
|
||||
Reference in New Issue
Block a user