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:
2026-05-20 09:43:14 +09:00
parent 8c1e56366b
commit 134f52d3d3
5 changed files with 887 additions and 0 deletions

View File

@@ -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)