@ikon85/agent-workflow-kit 0.10.0
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/skills/ask-matt/SKILL.md +139 -0
- package/.agents/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/audit-skills/SKILL.md +145 -0
- package/.agents/skills/board-to-waves/SKILL.md +135 -0
- package/.agents/skills/code-review/SKILL.md +64 -0
- package/.agents/skills/codebase-design/DEEPENING.md +37 -0
- package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.agents/skills/codebase-design/SKILL.md +114 -0
- package/.agents/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/codex-adapter-sync/SKILL.md +151 -0
- package/.agents/skills/decision-gate/SKILL.md +67 -0
- package/.agents/skills/diagnose/SKILL.md +136 -0
- package/.agents/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/domain-modeling/SKILL.md +76 -0
- package/.agents/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/git-worktree-recover/SKILL.md +176 -0
- package/.agents/skills/grill-me/SKILL.md +54 -0
- package/.agents/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/grill-with-docs/SKILL.md +137 -0
- package/.agents/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/implement/SKILL.md +15 -0
- package/.agents/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
- package/.agents/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
- package/.agents/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/.agents/skills/improve-codebase-architecture/SKILL.md +88 -0
- package/.agents/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/kit-release/SKILL.md +56 -0
- package/.agents/skills/local-ci/SKILL.md +94 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +261 -0
- package/.agents/skills/orchestrate-wave/references/builder-contract.md +67 -0
- package/.agents/skills/prototype/LOGIC.md +79 -0
- package/.agents/skills/prototype/SKILL.md +30 -0
- package/.agents/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/prototype/UI.md +112 -0
- package/.agents/skills/research/SKILL.md +12 -0
- package/.agents/skills/research/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/.agents/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/retro/SKILL.md +209 -0
- package/.agents/skills/scale-check/SKILL.md +141 -0
- package/.agents/skills/security-audit/SKILL.md +67 -0
- package/.agents/skills/setup-workflow/SKILL.md +187 -0
- package/.agents/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/setup-workflow/board-sync.md +111 -0
- package/.agents/skills/setup-workflow/code-review.md +26 -0
- package/.agents/skills/setup-workflow/domain.md +51 -0
- package/.agents/skills/setup-workflow/issue-tracker-github.md +35 -0
- package/.agents/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
- package/.agents/skills/setup-workflow/issue-tracker-local.md +30 -0
- package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
- package/.agents/skills/setup-workflow/spec-completeness-seed.md +44 -0
- package/.agents/skills/setup-workflow/spec-self-critique-seed.md +43 -0
- package/.agents/skills/setup-workflow/triage-labels.md +18 -0
- package/.agents/skills/setup-workflow/workflow-overview.md +33 -0
- package/.agents/skills/spec-self-critique/SKILL.md +152 -0
- package/.agents/skills/spec-self-critique/scenarios.md +82 -0
- package/.agents/skills/tdd/SKILL.md +120 -0
- package/.agents/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/tdd/deep-modules.md +33 -0
- package/.agents/skills/tdd/interface-design.md +31 -0
- package/.agents/skills/tdd/mocking.md +59 -0
- package/.agents/skills/tdd/refactoring.md +10 -0
- package/.agents/skills/tdd/tests.md +61 -0
- package/.agents/skills/to-issues/SKILL.md +325 -0
- package/.agents/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
- package/.agents/skills/to-prd/SKILL.md +180 -0
- package/.agents/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/to-waves/SKILL.md +260 -0
- package/.agents/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
- package/.agents/skills/triage/AGENT-BRIEF.md +168 -0
- package/.agents/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/.agents/skills/triage/SKILL.md +122 -0
- package/.agents/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/verify-spike/SKILL.md +39 -0
- package/.agents/skills/wayfinder/SKILL.md +127 -0
- package/.agents/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/wrapup/SKILL.md +71 -0
- package/.agents/skills/wrapup/SOURCES.txt +6 -0
- package/.agents/skills/zoom-out/SKILL.md +7 -0
- package/.agents/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/hooks/_hook_utils.py +184 -0
- package/.claude/hooks/drift-guard.py +176 -0
- package/.claude/hooks/skill-drift-hint.py +122 -0
- package/.claude/hooks/sync-board-status.py +191 -0
- package/.claude/skills/ask-matt/SKILL.md +139 -0
- package/.claude/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/audit-skills/SKILL.md +145 -0
- package/.claude/skills/board-to-waves/SKILL.md +148 -0
- package/.claude/skills/code-review/SKILL.md +64 -0
- package/.claude/skills/codebase-design/DEEPENING.md +37 -0
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.claude/skills/codebase-design/SKILL.md +114 -0
- package/.claude/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/codex-build/SKILL.md +126 -0
- package/.claude/skills/codex-build/THIRD-PARTY-NOTICES.md +35 -0
- package/.claude/skills/codex-review/SKILL.md +145 -0
- package/.claude/skills/codex-review/THIRD-PARTY-NOTICES.md +31 -0
- package/.claude/skills/decision-gate/SKILL.md +67 -0
- package/.claude/skills/diagnose/SKILL.md +136 -0
- package/.claude/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/.claude/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.claude/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/domain-modeling/SKILL.md +76 -0
- package/.claude/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/git-guardrails-claude-code/SKILL.md +96 -0
- package/.claude/skills/git-guardrails-claude-code/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +28 -0
- package/.claude/skills/git-worktree-recover/SKILL.md +182 -0
- package/.claude/skills/grill-me/SKILL.md +54 -0
- package/.claude/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/grill-me-codex/SKILL.md +158 -0
- package/.claude/skills/grill-me-codex/THIRD-PARTY-NOTICES.md +32 -0
- package/.claude/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/grill-with-docs/SKILL.md +137 -0
- package/.claude/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/grill-with-docs-codex/ADR-FORMAT.md +47 -0
- package/.claude/skills/grill-with-docs-codex/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/grill-with-docs-codex/SKILL.md +233 -0
- package/.claude/skills/grill-with-docs-codex/THIRD-PARTY-NOTICES.md +33 -0
- package/.claude/skills/implement/SKILL.md +15 -0
- package/.claude/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
- package/.claude/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
- package/.claude/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +88 -0
- package/.claude/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/kit-release/SKILL.md +56 -0
- package/.claude/skills/local-ci/SKILL.md +94 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +269 -0
- package/.claude/skills/orchestrate-wave/references/builder-contract.md +67 -0
- package/.claude/skills/prototype/LOGIC.md +79 -0
- package/.claude/skills/prototype/SKILL.md +30 -0
- package/.claude/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/prototype/UI.md +112 -0
- package/.claude/skills/research/SKILL.md +12 -0
- package/.claude/skills/research/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/.claude/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/retro/SKILL.md +209 -0
- package/.claude/skills/scale-check/SKILL.md +141 -0
- package/.claude/skills/security-audit/SKILL.md +67 -0
- package/.claude/skills/setup-pre-commit/SKILL.md +104 -0
- package/.claude/skills/setup-pre-commit/scripts/pre-commit.template.sh +35 -0
- package/.claude/skills/setup-workflow/SKILL.md +187 -0
- package/.claude/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/setup-workflow/board-sync.md +111 -0
- package/.claude/skills/setup-workflow/code-review.md +26 -0
- package/.claude/skills/setup-workflow/domain.md +51 -0
- package/.claude/skills/setup-workflow/issue-tracker-github.md +35 -0
- package/.claude/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
- package/.claude/skills/setup-workflow/issue-tracker-local.md +30 -0
- package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
- package/.claude/skills/setup-workflow/spec-completeness-seed.md +44 -0
- package/.claude/skills/setup-workflow/spec-self-critique-seed.md +43 -0
- package/.claude/skills/setup-workflow/triage-labels.md +18 -0
- package/.claude/skills/setup-workflow/workflow-overview.md +33 -0
- package/.claude/skills/skill-manifest.json +389 -0
- package/.claude/skills/spec-self-critique/SKILL.md +152 -0
- package/.claude/skills/spec-self-critique/scenarios.md +82 -0
- package/.claude/skills/tdd/SKILL.md +120 -0
- package/.claude/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/tdd/deep-modules.md +33 -0
- package/.claude/skills/tdd/interface-design.md +31 -0
- package/.claude/skills/tdd/mocking.md +59 -0
- package/.claude/skills/tdd/refactoring.md +10 -0
- package/.claude/skills/tdd/tests.md +61 -0
- package/.claude/skills/to-issues/SKILL.md +325 -0
- package/.claude/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
- package/.claude/skills/to-prd/SKILL.md +180 -0
- package/.claude/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/to-waves/SKILL.md +260 -0
- package/.claude/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
- package/.claude/skills/triage/AGENT-BRIEF.md +168 -0
- package/.claude/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/.claude/skills/triage/SKILL.md +122 -0
- package/.claude/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/verify-spike/SKILL.md +39 -0
- package/.claude/skills/wayfinder/SKILL.md +127 -0
- package/.claude/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/wrapup/SKILL.md +81 -0
- package/.claude/skills/wrapup/SOURCES.txt +6 -0
- package/.claude/skills/write-a-skill/GLOSSARY.md +201 -0
- package/.claude/skills/write-a-skill/SKILL.md +88 -0
- package/.claude/skills/write-a-skill/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/zoom-out/SKILL.md +7 -0
- package/.claude/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
- package/LICENSE +28 -0
- package/PROVENANCE.md +39 -0
- package/README.md +619 -0
- package/agent-workflow-kit.package.json +1939 -0
- package/docs/agents/board-sync.md +71 -0
- package/docs/agents/code-review.md +14 -0
- package/docs/agents/domain.md +7 -0
- package/docs/agents/issue-tracker.md +12 -0
- package/docs/agents/security-audit-runbook-template.md +205 -0
- package/docs/agents/skills/orchestrate-wave.md +13 -0
- package/docs/agents/skills/spec-self-critique.md +18 -0
- package/docs/agents/triage-labels.md +10 -0
- package/docs/agents/wave-anchor-template.md +88 -0
- package/docs/conventions/spec-completeness.md +23 -0
- package/docs/index.html +56 -0
- package/docs/methodology.html +488 -0
- package/docs/methodology.svg +93 -0
- package/docs/workflow.png +0 -0
- package/package.json +40 -0
- package/scripts/anchor_table.py +343 -0
- package/scripts/board-sync.py +1232 -0
- package/scripts/board_config.py +147 -0
- package/scripts/board_fields.py +192 -0
- package/scripts/build-kit.mjs +116 -0
- package/scripts/build-kit.test.mjs +59 -0
- package/scripts/check-kit-staleness.mjs +45 -0
- package/scripts/check-kit-staleness.test.mjs +19 -0
- package/scripts/execute-ready-check.py +516 -0
- package/scripts/issue_deps.py +79 -0
- package/scripts/kit-release.mjs +100 -0
- package/scripts/kit-release.test.mjs +67 -0
- package/scripts/lib/audit-refs.mjs +146 -0
- package/scripts/lib/scrub.mjs +113 -0
- package/scripts/lib/scrub.test.mjs +187 -0
- package/scripts/loc_offender_core.py +196 -0
- package/scripts/loc_offender_gate.py +182 -0
- package/scripts/node_kind.py +68 -0
- package/scripts/portability_profile_scan.py +53 -0
- package/scripts/pr-body-check.py +263 -0
- package/scripts/program_graph.py +125 -0
- package/scripts/program_graph_parse.py +275 -0
- package/scripts/program_graph_validate.py +256 -0
- package/scripts/program_sync.py +160 -0
- package/scripts/release-delta-guard.mjs +96 -0
- package/scripts/release-delta-guard.test.mjs +53 -0
- package/scripts/test_board_sync_create_idempotency.py +104 -0
- package/scripts/test_dist_kit_smoke.py +199 -0
- package/scripts/test_kit_docs_language_census.py +178 -0
- package/scripts/test_skill_code_review_seed.py +89 -0
- package/scripts/test_skill_frontmatter_lint.py +226 -0
- package/scripts/test_skill_gh_lint.py +124 -0
- package/scripts/test_skill_language_census.py +254 -0
- package/scripts/test_skill_portability_lint.py +773 -0
- package/scripts/test_skill_publish_audit.py +300 -0
- package/scripts/test_skill_selfcontainment_lint.py +255 -0
- package/scripts/test_skill_setup_workflow_seeds.py +200 -0
- package/scripts/test_skill_stale_name_lint.py +126 -0
- package/scripts/test_skill_surface_refs.py +300 -0
- package/scripts/test_skill_trailing_artifact_lint.py +71 -0
- package/scripts/wrapup-land.py +573 -0
- package/src/cli.mjs +70 -0
- package/src/commands/diff.mjs +6 -0
- package/src/commands/init.mjs +59 -0
- package/src/commands/uninstall.mjs +43 -0
- package/src/commands/update.mjs +95 -0
- package/src/lib/atomicWrite.mjs +49 -0
- package/src/lib/bundle.mjs +133 -0
- package/src/lib/hash.mjs +13 -0
- package/src/lib/manifest.mjs +58 -0
- package/src/lib/sentinel.mjs +20 -0
- package/src/lib/settings.mjs +23 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
execute-ready-check.py — single source of truth for "execute-ready" graph
|
|
4
|
+
coherence.
|
|
5
|
+
|
|
6
|
+
Four callers share this one checker so the parse/coherence rules live + are
|
|
7
|
+
tested ONCE:
|
|
8
|
+
- .claude/hooks/drift-guard.py --mode handoff (blocks handoff on deny_recommended)
|
|
9
|
+
- to-issues §7 exit --mode audit (non-blocking warn — as in 1d)
|
|
10
|
+
- grill-with-docs Re-Grill --mode audit
|
|
11
|
+
- wrapup Land-Reconcile --mode audit
|
|
12
|
+
|
|
13
|
+
Two separate axes (Codex R1/R2):
|
|
14
|
+
- graph_coherent : every node in the rooted local graph is well-formed
|
|
15
|
+
(Anker: plan_revision + no bucket/ready-for-agent;
|
|
16
|
+
child/leaf: exactly one bucket, plan_revision coherent).
|
|
17
|
+
- target_buildable : only for the handoff target — AFK = buildable,
|
|
18
|
+
HITL = valid-but-not-buildable.
|
|
19
|
+
deny_recommended = !graph_coherent
|
|
20
|
+
OR (mode==handoff AND intent==build AND target leaf AND !buildable).
|
|
21
|
+
- shape_warnings : provenance-NEUTRAL anchor form check (template
|
|
22
|
+
sections + body header). audit-only, anchor-only, and
|
|
23
|
+
NEVER part of graph_coherent/deny_recommended — a loud
|
|
24
|
+
nudge for output-uniformity, not a gate.
|
|
25
|
+
|
|
26
|
+
Rooted local graph = handoff-target → its native parent (lift) → that parent's
|
|
27
|
+
direct children. NOT a board-wide scan. Atomar leaf (no parent) = its own node only.
|
|
28
|
+
|
|
29
|
+
gh access is a thin, mockable layer (reuses scripts/board-sync.py parent-of /
|
|
30
|
+
children-of). The pure functions below carry the logic + the unit tests.
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
execute-ready-check.py --issue <n> --mode handoff|audit [--intent build|grill] [--json]
|
|
34
|
+
Exit 0 always (it is a checker, not a gate); the hook decides blocking.
|
|
35
|
+
|
|
36
|
+
Marker conventions (CANONICAL — the skills cross-reference this block; all are
|
|
37
|
+
grep-bar HTML comments living in issue/PR/handoff bodies):
|
|
38
|
+
<!-- guard-ack: #<n> r<N> reason:<text> by-user --> deliberate handoff override (drift-guard.py)
|
|
39
|
+
<!-- guard-legacy --> grandfathered alt-anchor → warn, not block
|
|
40
|
+
<!-- handoff-intent: build|grill --> handoff intent (else inferred from a /grill cmd)
|
|
41
|
+
<!-- final-cut-depends-on: #<n> --> leaf's final cut hangs on #n; #n CLOSED → block
|
|
42
|
+
<!-- annahme-drift: {"target":"#<n>",...} --> drift propagation — consumed by wrapup Step 5e
|
|
43
|
+
(NOT parsed here)
|
|
44
|
+
<!-- prd: program --> Programm-PRD marker (node_kind.py) — this
|
|
45
|
+
target roots at itself, never lifts
|
|
46
|
+
|
|
47
|
+
Audit log: .claude/logs/execute-ready-check.log
|
|
48
|
+
"""
|
|
49
|
+
import argparse
|
|
50
|
+
import json
|
|
51
|
+
import re
|
|
52
|
+
import subprocess
|
|
53
|
+
import sys
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
57
|
+
from board_config import ConfigError, load_board_config # noqa: E402
|
|
58
|
+
from node_kind import ANCHOR, LEAF, PROGRAM, ROOT_KINDS, WAVE_STUB, classify_node # noqa: E402,F401
|
|
59
|
+
# Native issue dependencies are the blocking SSOT; the `## Blocked by`
|
|
60
|
+
# body section is only their machine-written mirror (issue_deps.py, shared with
|
|
61
|
+
# board-sync.py). Drift semantics: warn in audit, deny at handoff.
|
|
62
|
+
from issue_deps import open_blocker_numbers, parse_blocked_by_numbers # noqa: E402
|
|
63
|
+
|
|
64
|
+
# Project-specific label + heading come from the board profile (no inline
|
|
65
|
+
# constants → published kit stays project-neutral).
|
|
66
|
+
try:
|
|
67
|
+
_CFG = load_board_config()
|
|
68
|
+
except ConfigError as exc:
|
|
69
|
+
print(f"[FAIL] execute-ready-check: Board-Profil nicht verfügbar — {exc}", file=sys.stderr)
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
|
|
72
|
+
LOG_DIR = Path(".claude/logs")
|
|
73
|
+
LOG_NAME = "execute-ready-check"
|
|
74
|
+
|
|
75
|
+
# --- markers / patterns -----------------------------------------------------
|
|
76
|
+
# Broad: a line that *intends* to be the plan_revision marker (for malformed detection).
|
|
77
|
+
PLAN_REV_BROAD = re.compile(r"\*\*plan_revision:\*\*")
|
|
78
|
+
# Strict: a well-formed `**plan_revision:** r<N>`.
|
|
79
|
+
PLAN_REV_STRICT = re.compile(r"\*\*plan_revision:\*\*\s*r(\d+)\b")
|
|
80
|
+
HEADING_RE = re.compile(r"^#{1,6}\s")
|
|
81
|
+
VOR_BAU_RE = re.compile(r"^#{1,6}\s*" + re.escape(_CFG["headings"]["vorBau"]), re.MULTILINE)
|
|
82
|
+
FINAL_CUT_RE = re.compile(r"<!--\s*final-cut-depends-on:\s*#?(\d+)\s*-->")
|
|
83
|
+
GUARD_LEGACY_RE = re.compile(r"<!--\s*guard-legacy\s*-->")
|
|
84
|
+
HANDOFF_INTENT_RE = re.compile(r"<!--\s*handoff-intent:\s*(build|grill)\s*-->")
|
|
85
|
+
GRILL_CMD_RE = re.compile(r"/grill(?:-me|-with-docs)?(?:-codex)?\b")
|
|
86
|
+
READY_LABEL = _CFG["labels"]["readyForAgent"]
|
|
87
|
+
# Read defensively: `labels.programType` (a filterable Programm-PRD label) is
|
|
88
|
+
# added by a LATER slice — this checker must keep working before that
|
|
89
|
+
# key exists, so `program` classification is 100% marker-driven (node_kind.py).
|
|
90
|
+
CLUSTER_TYPE_LABEL = _CFG["labels"].get("clusterType")
|
|
91
|
+
WAVE_STUB_LABEL = _CFG["labels"].get("waveStub")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def classify(node: dict) -> str:
|
|
95
|
+
"""Node kind (program|anchor|wave_stub|leaf) bound to this repo's board
|
|
96
|
+
profile — see node_kind.py for the precedence rules."""
|
|
97
|
+
return classify_node(node, cluster_type_label=CLUSTER_TYPE_LABEL,
|
|
98
|
+
wave_stub_label=WAVE_STUB_LABEL)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# NOTE: log() + _run() below are intentional small isolation copies of
|
|
102
|
+
# _hook_utils.{log,run_with_status} — scripts/ tools (cf. board-sync.py) do not
|
|
103
|
+
# import from .claude/hooks/. Keep them thin; do not add a third copy elsewhere.
|
|
104
|
+
def log(msg: str) -> None:
|
|
105
|
+
try:
|
|
106
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
from datetime import datetime
|
|
108
|
+
with (LOG_DIR / f"{LOG_NAME}.log").open("a", encoding="utf-8") as f:
|
|
109
|
+
f.write(f"{datetime.now().isoformat(timespec='seconds')} {msg}\n")
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --- pure parsers -----------------------------------------------------------
|
|
115
|
+
def parse_plan_revision(body: str):
|
|
116
|
+
"""Return (rev:int|None, status). status ∈ {ok, missing, malformed, multiple, misplaced}.
|
|
117
|
+
|
|
118
|
+
'Top' = after leading metadata HTML comments, before the first Markdown
|
|
119
|
+
heading. Blockquote lines (quoted old bodies) are ignored so a stale marker
|
|
120
|
+
inside a `>` quote cannot fake coherence.
|
|
121
|
+
"""
|
|
122
|
+
if not body:
|
|
123
|
+
return None, "missing"
|
|
124
|
+
lines = body.splitlines()
|
|
125
|
+
marker_idx = [
|
|
126
|
+
i for i, ln in enumerate(lines)
|
|
127
|
+
if not ln.lstrip().startswith(">") and PLAN_REV_BROAD.search(ln)
|
|
128
|
+
]
|
|
129
|
+
if not marker_idx:
|
|
130
|
+
return None, "missing"
|
|
131
|
+
if len(marker_idx) > 1:
|
|
132
|
+
return None, "multiple"
|
|
133
|
+
i = marker_idx[0]
|
|
134
|
+
m = PLAN_REV_STRICT.search(lines[i])
|
|
135
|
+
if not m:
|
|
136
|
+
return None, "malformed"
|
|
137
|
+
first_heading = next((j for j, ln in enumerate(lines) if HEADING_RE.match(ln)), len(lines))
|
|
138
|
+
if i > first_heading:
|
|
139
|
+
return int(m.group(1)), "misplaced"
|
|
140
|
+
return int(m.group(1)), "ok"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def parse_bucket(labels, body: str) -> str:
|
|
144
|
+
"""afk | hitl | ambiguous. AFK = ready-for-agent + no Vor-Bau; HITL = no
|
|
145
|
+
ready-for-agent + Vor-Bau present. Both or neither → ambiguous."""
|
|
146
|
+
has_ready = READY_LABEL in (labels or [])
|
|
147
|
+
has_vorbau = bool(VOR_BAU_RE.search(body or ""))
|
|
148
|
+
if has_ready and not has_vorbau:
|
|
149
|
+
return "afk"
|
|
150
|
+
if has_vorbau and not has_ready:
|
|
151
|
+
return "hitl"
|
|
152
|
+
return "ambiguous"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def infer_intent(content: str, marker_intent: str = None) -> str:
|
|
156
|
+
"""build | grill. Explicit handoff-intent marker wins; else infer from a
|
|
157
|
+
grill command in the handoff content; else default build."""
|
|
158
|
+
if marker_intent in ("build", "grill"):
|
|
159
|
+
return marker_intent
|
|
160
|
+
if content:
|
|
161
|
+
mk = HANDOFF_INTENT_RE.search(content)
|
|
162
|
+
if mk:
|
|
163
|
+
return mk.group(1)
|
|
164
|
+
if GRILL_CMD_RE.search(content):
|
|
165
|
+
return "grill"
|
|
166
|
+
return "build"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def parse_final_cut_depends(body: str):
|
|
170
|
+
m = FINAL_CUT_RE.search(body or "")
|
|
171
|
+
return int(m.group(1)) if m else None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def is_legacy(body: str) -> bool:
|
|
175
|
+
# Marker-only by design. Date-based grandfathering (createdAt < enforcement
|
|
176
|
+
# date) is intentionally NOT implemented — the explicit `<!-- guard-legacy -->`
|
|
177
|
+
# marker is the single grandfather path (alt-anchors like get tagged once).
|
|
178
|
+
return bool(GUARD_LEGACY_RE.search(body or ""))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# --- anchor shape audit (non-blocking) -------------------------------
|
|
182
|
+
# Provenance-NEUTRAL form check: does a promoted anchor carry the uniform
|
|
183
|
+
# wave-anchor-template shape? Emitted ONLY as shape_warnings in --mode audit;
|
|
184
|
+
# NEVER fed into violations / graph_coherent / deny_recommended. A missing
|
|
185
|
+
# section is a loud nudge, never a handoff block — folding it into deny would
|
|
186
|
+
# recreate exactly the provenance-harness rejected. The hard block stays
|
|
187
|
+
# bucket + coherence only.
|
|
188
|
+
#
|
|
189
|
+
# The SSOT template (docs/agents/wave-anchor-template.md) went English-only
|
|
190
|
+
# — `Welle`/`Herkunft`/`Entscheidungen` stay accepted so EXISTING
|
|
191
|
+
# German anchors keep auditing clean, and `Wave`/`Origin`/`Decisions` are
|
|
192
|
+
# accepted alongside so anchors built from the now-English template do too.
|
|
193
|
+
_WAVE_HEADER_RE = re.compile(r"\*\*\s*(?:Welle|Wave)\s+\d+\s*[—–-]", re.IGNORECASE)
|
|
194
|
+
# `## Herkunft`/`## Origin` (new) or the legacy `## Cluster-Herkunft` all satisfy the check.
|
|
195
|
+
_SHAPE_SECTIONS = (
|
|
196
|
+
("Herkunft/Origin", re.compile(r"^#{1,6}\s*(?:(?:Cluster-)?Herkunft|Origin)\b", re.MULTILINE)),
|
|
197
|
+
("Entscheidungen/Decisions", re.compile(r"^#{1,6}\s*(?:Entscheidungen|Decisions)\b", re.MULTILINE)),
|
|
198
|
+
("Slices", re.compile(r"^#{1,6}\s*Slices\b", re.MULTILINE)),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def evaluate_anchor_shape(body: str) -> list[str]:
|
|
203
|
+
"""Non-blocking form check for a promoted anchor body.
|
|
204
|
+
|
|
205
|
+
Returns human-readable shape warnings (missing template sections / body
|
|
206
|
+
header). NEVER contributes to violations — uniformity is a loud nudge, not
|
|
207
|
+
a gate.
|
|
208
|
+
"""
|
|
209
|
+
warnings = []
|
|
210
|
+
if not _WAVE_HEADER_RE.search(body or ""):
|
|
211
|
+
warnings.append("anchor body header `**Wave N — …**` missing")
|
|
212
|
+
for name, rx in _SHAPE_SECTIONS:
|
|
213
|
+
if not rx.search(body or ""):
|
|
214
|
+
warnings.append(f"anchor section `## {name}` missing")
|
|
215
|
+
return warnings
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# --- pure coherence ---------------------------------------------------------
|
|
219
|
+
def evaluate_graph(target, parent=None, siblings=None, *, mode="handoff",
|
|
220
|
+
intent="build", closed_lookup=None, truncated=False,
|
|
221
|
+
root_kind=None, native_blockers=None) -> dict:
|
|
222
|
+
"""Evaluate the rooted local graph. Nodes are dicts {number, body, labels}.
|
|
223
|
+
|
|
224
|
+
parent=None → atomar leaf (graph = [target]).
|
|
225
|
+
parent given → target's local root + its children (siblings, incl.
|
|
226
|
+
target if target is a child of that root).
|
|
227
|
+
root_kind=None → target was LIFTED to `parent` (a real, different
|
|
228
|
+
parent) or is the atomar leaf above — target_kind
|
|
229
|
+
resolves to "leaf".
|
|
230
|
+
root_kind=ANCHOR/PROGRAM → target IS its own root (`parent` == `target`,
|
|
231
|
+
`siblings` == target's own children) — an anchor or
|
|
232
|
+
Programm-PRD never lifts to a native parent it might
|
|
233
|
+
also have.
|
|
234
|
+
|
|
235
|
+
Each CHILD in `siblings` is classified independently (`classify_node`):
|
|
236
|
+
only LEAF children go through the bucket + plan_revision-match checks —
|
|
237
|
+
an anchor/wave_stub/program child carries neither a bucket nor a shared
|
|
238
|
+
plan_revision domain with its parent, and is audited on its own terms
|
|
239
|
+
when IT is the checker's target (rooted at itself via ROOT_KINDS).
|
|
240
|
+
"""
|
|
241
|
+
violations = []
|
|
242
|
+
closed_lookup = closed_lookup or {}
|
|
243
|
+
grandfathered = None # set to the anchor # when a legacy-tagged anchor grandfathers its graph
|
|
244
|
+
|
|
245
|
+
def check_rev_ok(node, kind_label, suppress_rev_status=False):
|
|
246
|
+
rev, st = parse_plan_revision(node["body"])
|
|
247
|
+
if st != "ok" and not is_legacy(node["body"]) and not suppress_rev_status:
|
|
248
|
+
violations.append(f"#{node['number']}{kind_label}: plan_revision {st}")
|
|
249
|
+
return rev, st
|
|
250
|
+
|
|
251
|
+
if parent is None:
|
|
252
|
+
# atomar leaf
|
|
253
|
+
check_rev_ok(target, "")
|
|
254
|
+
if parse_bucket(target["labels"], target["body"]) == "ambiguous" \
|
|
255
|
+
and not is_legacy(target["body"]):
|
|
256
|
+
violations.append(f"#{target['number']}: ambiguous bucket (ready-for-agent vs Vor Bau zu klären)")
|
|
257
|
+
target_kind = LEAF
|
|
258
|
+
else:
|
|
259
|
+
# Anchor tagged <!-- guard-legacy --> grandfathers the whole rooted graph
|
|
260
|
+
# (Q4=A: tag once → free). Constrained: pre-convention classes
|
|
261
|
+
# (plan_revision status, anchor-rev mismatch) are suppressed graph-wide;
|
|
262
|
+
# ambiguous-bucket only for CLOSED children — an OPEN child's bucket and
|
|
263
|
+
# the structural ready-on-anchor check stay live (new incoherence visible).
|
|
264
|
+
anchor_legacy = is_legacy(parent["body"])
|
|
265
|
+
if anchor_legacy:
|
|
266
|
+
grandfathered = parent["number"]
|
|
267
|
+
arev, ast = check_rev_ok(parent, " (anchor)")
|
|
268
|
+
if READY_LABEL in (parent["labels"] or []):
|
|
269
|
+
violations.append(f"#{parent['number']} (anchor): ready-for-agent on an anchor")
|
|
270
|
+
for child in (siblings or []):
|
|
271
|
+
if classify(child) != LEAF:
|
|
272
|
+
# anchor/wave_stub/program sibling — no bucket, no shared
|
|
273
|
+
# plan_revision domain with `parent`.
|
|
274
|
+
continue
|
|
275
|
+
crev, cst = check_rev_ok(child, "", suppress_rev_status=anchor_legacy)
|
|
276
|
+
legacy = is_legacy(child["body"])
|
|
277
|
+
child_closed = (child.get("state") or "").upper() == "CLOSED"
|
|
278
|
+
suppress_bucket = legacy or (anchor_legacy and child_closed)
|
|
279
|
+
if parse_bucket(child["labels"], child["body"]) == "ambiguous" and not suppress_bucket:
|
|
280
|
+
violations.append(f"#{child['number']}: ambiguous bucket")
|
|
281
|
+
if cst == "ok" and ast == "ok" and crev != arev and not legacy and not anchor_legacy:
|
|
282
|
+
violations.append(f"#{child['number']}: plan_revision r{crev} != anchor r{arev}")
|
|
283
|
+
target_kind = root_kind if root_kind in ROOT_KINDS else LEAF
|
|
284
|
+
|
|
285
|
+
# Fix B (b): final-cut dependency closed without resolution
|
|
286
|
+
fc = parse_final_cut_depends(target["body"])
|
|
287
|
+
if fc is not None:
|
|
288
|
+
fc_state = closed_lookup.get(fc)
|
|
289
|
+
if fc_state == "closed":
|
|
290
|
+
violations.append(f"#{target['number']}: final-cut depends on #{fc} which is CLOSED")
|
|
291
|
+
elif fc_state == "unresolved":
|
|
292
|
+
# gh could not resolve the dependency (network/auth) — a silent
|
|
293
|
+
# "open" default here would let a stale/unverifiable final-cut
|
|
294
|
+
# dependency slip an unbuilt handoff through.
|
|
295
|
+
violations.append(
|
|
296
|
+
f"#{target['number']}: final-cut dependency #{fc} could not be "
|
|
297
|
+
f"verified via gh (network/auth) — fail-closed")
|
|
298
|
+
|
|
299
|
+
if truncated:
|
|
300
|
+
violations.append("graph too large for guard (>100 children) — cannot prove completeness")
|
|
301
|
+
|
|
302
|
+
# Native blocking SSOT. native_blockers ∈ None (not instrumented —
|
|
303
|
+
# pure-call back-compat) | "unresolved" (gh failed → fail-closed
|
|
304
|
+
# pattern) | list of {number, state, …}. Being blocked is NOT incoherence:
|
|
305
|
+
# it gates only a BUILD handoff of a leaf (audit shows it informationally).
|
|
306
|
+
# Mirror drift: warn in audit, violation (deny) only at handoff.
|
|
307
|
+
open_blockers: list = []
|
|
308
|
+
drift_warnings: list = []
|
|
309
|
+
if native_blockers == "unresolved":
|
|
310
|
+
violations.append(
|
|
311
|
+
f"#{target['number']}: native blocked-by edges could not be verified "
|
|
312
|
+
f"via gh (network/auth) — fail-closed")
|
|
313
|
+
elif native_blockers is not None:
|
|
314
|
+
open_blockers = open_blocker_numbers(native_blockers)
|
|
315
|
+
mirror = set(parse_blocked_by_numbers(target["body"]))
|
|
316
|
+
native = {b["number"] for b in native_blockers}
|
|
317
|
+
if mirror != native:
|
|
318
|
+
msg = (f"#{target['number']}: `## Blocked by` mirror {sorted(mirror)} != "
|
|
319
|
+
f"native dependencies {sorted(native)} — API is the truth "
|
|
320
|
+
f"(repair: scripts/board-sync.py dep-add/dep-remove)")
|
|
321
|
+
if mode == "handoff":
|
|
322
|
+
violations.append(msg)
|
|
323
|
+
else:
|
|
324
|
+
drift_warnings.append(msg)
|
|
325
|
+
|
|
326
|
+
if target_kind in ROOT_KINDS:
|
|
327
|
+
target_buildable = True
|
|
328
|
+
else:
|
|
329
|
+
target_buildable = parse_bucket(target["labels"], target["body"]) == "afk"
|
|
330
|
+
|
|
331
|
+
graph_coherent = len(violations) == 0
|
|
332
|
+
deny_recommended = (not graph_coherent) or (
|
|
333
|
+
mode == "handoff" and intent == "build" and target_kind == LEAF
|
|
334
|
+
and (not target_buildable or bool(open_blockers))
|
|
335
|
+
)
|
|
336
|
+
# Non-blocking, audit-only, anchor-only (a Programm-PRD's body follows
|
|
337
|
+
# PROGRAM-PRD-FORMAT, not wave-anchor-template — the shape check would
|
|
338
|
+
# only produce spurious nudges there). Intentionally NOT part of
|
|
339
|
+
# graph_coherent/deny_recommended (see evaluate_anchor_shape docstring).
|
|
340
|
+
shape_warnings = (
|
|
341
|
+
evaluate_anchor_shape(target["body"])
|
|
342
|
+
if mode == "audit" and target_kind == ANCHOR
|
|
343
|
+
else []
|
|
344
|
+
)
|
|
345
|
+
return {
|
|
346
|
+
"graph_coherent": graph_coherent,
|
|
347
|
+
"target_buildable": target_buildable,
|
|
348
|
+
"deny_recommended": deny_recommended,
|
|
349
|
+
"violations": violations,
|
|
350
|
+
"grandfathered": grandfathered,
|
|
351
|
+
"shape_warnings": shape_warnings,
|
|
352
|
+
"open_blockers": open_blockers,
|
|
353
|
+
"drift_warnings": drift_warnings,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# --- thin gh layer (mocked in tests via the functions above) ----------------
|
|
358
|
+
def _run(cmd, timeout=15):
|
|
359
|
+
try:
|
|
360
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
361
|
+
return r.returncode, r.stdout.strip()
|
|
362
|
+
except Exception:
|
|
363
|
+
return -1, ""
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def fetch_issue(number: int):
|
|
367
|
+
"""{number, body, labels:[str], state} via `gh issue view`, or None on failure."""
|
|
368
|
+
rc, out = _run(["gh", "issue", "view", str(number), "--json",
|
|
369
|
+
"number,body,labels,state"])
|
|
370
|
+
if rc != 0 or not out:
|
|
371
|
+
return None
|
|
372
|
+
try:
|
|
373
|
+
d = json.loads(out)
|
|
374
|
+
except json.JSONDecodeError:
|
|
375
|
+
return None
|
|
376
|
+
return {
|
|
377
|
+
"number": d.get("number"),
|
|
378
|
+
"body": d.get("body") or "",
|
|
379
|
+
"labels": [lb.get("name", "") for lb in d.get("labels", [])],
|
|
380
|
+
"state": d.get("state", ""),
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def fetch_parent(number: int):
|
|
385
|
+
"""Parent issue number, or None (FREI = atomar)."""
|
|
386
|
+
rc, out = _run(["python3", "scripts/board-sync.py", "parent-of", str(number)])
|
|
387
|
+
if rc != 0 or not out or out.strip() == "FREI":
|
|
388
|
+
return None
|
|
389
|
+
try:
|
|
390
|
+
return int(out.strip())
|
|
391
|
+
except ValueError:
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def fetch_blocked_by(number: int):
|
|
396
|
+
"""Normalized native blocked-by list via board-sync `deps --json`, or
|
|
397
|
+
"unresolved" on any failure (fail-closed at the caller pattern)."""
|
|
398
|
+
rc, out = _run(["python3", "scripts/board-sync.py", "deps", str(number), "--json"])
|
|
399
|
+
if rc != 0 or not out:
|
|
400
|
+
return "unresolved"
|
|
401
|
+
try:
|
|
402
|
+
return json.loads(out)
|
|
403
|
+
except json.JSONDecodeError:
|
|
404
|
+
return "unresolved"
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def fetch_children(number: int):
|
|
408
|
+
"""(child_numbers, truncated). truncated=True if exactly 100 came back
|
|
409
|
+
(board-sync subIssues(first:100) — can't prove completeness)."""
|
|
410
|
+
rc, out = _run(["python3", "scripts/board-sync.py", "children-of", str(number)])
|
|
411
|
+
if rc != 0 or not out:
|
|
412
|
+
return [], False
|
|
413
|
+
# one issue number per line — anchor per-line so a stray warning/word can't
|
|
414
|
+
# contribute spurious digits.
|
|
415
|
+
nums = [int(m.group(1)) for m in re.finditer(r"^\s*(\d+)\s*$", out, re.MULTILINE)]
|
|
416
|
+
return nums, len(nums) >= 100
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def build_and_evaluate(issue_number: int, mode: str, intent: str,
|
|
420
|
+
handoff_content: str = "") -> dict:
|
|
421
|
+
"""Fetch the rooted local graph via gh + evaluate. fail-closed: if a target
|
|
422
|
+
is identified but gh cannot resolve it, recommend deny (the MUSS-property)."""
|
|
423
|
+
target = fetch_issue(issue_number)
|
|
424
|
+
if target is None:
|
|
425
|
+
return _fail_closed(issue_number, "could not verify via gh (network/auth)")
|
|
426
|
+
|
|
427
|
+
closed_lookup = {}
|
|
428
|
+
fc = parse_final_cut_depends(target["body"])
|
|
429
|
+
if fc is not None:
|
|
430
|
+
st = fetch_issue(fc)
|
|
431
|
+
if st is None:
|
|
432
|
+
# gh could not resolve the dependency at all → "unresolved", NOT a
|
|
433
|
+
# silent "open" default (that would hide an unverifiable final-cut
|
|
434
|
+
# dependency instead of denying it).
|
|
435
|
+
closed_lookup[fc] = "unresolved"
|
|
436
|
+
else:
|
|
437
|
+
closed_lookup[fc] = "closed" if st.get("state", "").upper() == "CLOSED" else "open"
|
|
438
|
+
|
|
439
|
+
native_blockers = fetch_blocked_by(issue_number)
|
|
440
|
+
|
|
441
|
+
node_kind = classify(target)
|
|
442
|
+
if node_kind in ROOT_KINDS:
|
|
443
|
+
# An anchor or a Programm-PRD is its OWN root — it never lifts to a
|
|
444
|
+
# native parent it might also have (a Welle-Anker under a PRD, or the
|
|
445
|
+
# PRD itself).. `fetch_parent` is intentionally never called here.
|
|
446
|
+
children, truncated = fetch_children(issue_number)
|
|
447
|
+
siblings = [fetch_issue(c) for c in children] if children else []
|
|
448
|
+
if any(s is None for s in siblings):
|
|
449
|
+
return _fail_closed(issue_number, "child fetch failed")
|
|
450
|
+
return evaluate_graph(target, parent=target, siblings=siblings, mode=mode,
|
|
451
|
+
intent=intent, closed_lookup=closed_lookup,
|
|
452
|
+
truncated=truncated, root_kind=node_kind,
|
|
453
|
+
native_blockers=native_blockers)
|
|
454
|
+
|
|
455
|
+
# leaf / wave_stub — lift to the native parent if one exists (2-level
|
|
456
|
+
# rooted-local model), else atomar leaf.
|
|
457
|
+
parent_n = fetch_parent(issue_number)
|
|
458
|
+
if parent_n is None:
|
|
459
|
+
return evaluate_graph(target, mode=mode, intent=intent, closed_lookup=closed_lookup,
|
|
460
|
+
native_blockers=native_blockers)
|
|
461
|
+
|
|
462
|
+
parent = fetch_issue(parent_n)
|
|
463
|
+
if parent is None:
|
|
464
|
+
return _fail_closed(issue_number, "parent fetch failed")
|
|
465
|
+
children, truncated = fetch_children(parent_n)
|
|
466
|
+
siblings = [fetch_issue(c) for c in children] if children else [target]
|
|
467
|
+
if any(s is None for s in siblings):
|
|
468
|
+
return _fail_closed(issue_number, "sibling fetch failed")
|
|
469
|
+
return evaluate_graph(target, parent=parent, siblings=siblings, mode=mode,
|
|
470
|
+
intent=intent, closed_lookup=closed_lookup, truncated=truncated,
|
|
471
|
+
native_blockers=native_blockers)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _fail_closed(number, why):
|
|
475
|
+
log(f"issue={number} {why} → fail-closed deny")
|
|
476
|
+
return {"graph_coherent": False, "target_buildable": False,
|
|
477
|
+
"deny_recommended": True, "grandfathered": None, "shape_warnings": [],
|
|
478
|
+
"open_blockers": [], "drift_warnings": [],
|
|
479
|
+
"violations": [f"#{number}: {why} — fail-closed (could not verify graph)"]}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def main() -> int:
|
|
483
|
+
ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
|
|
484
|
+
ap.add_argument("--issue", type=int, required=True)
|
|
485
|
+
ap.add_argument("--mode", choices=["handoff", "audit"], default="audit")
|
|
486
|
+
ap.add_argument("--intent", choices=["build", "grill"], default="build")
|
|
487
|
+
ap.add_argument("--json", action="store_true")
|
|
488
|
+
args = ap.parse_args()
|
|
489
|
+
|
|
490
|
+
result = build_and_evaluate(args.issue, args.mode, args.intent)
|
|
491
|
+
log(f"issue={args.issue} mode={args.mode} intent={args.intent} "
|
|
492
|
+
f"coherent={result['graph_coherent']} buildable={result['target_buildable']} "
|
|
493
|
+
f"deny={result['deny_recommended']} violations={len(result['violations'])}")
|
|
494
|
+
|
|
495
|
+
if args.json:
|
|
496
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
497
|
+
else:
|
|
498
|
+
flag = "DENY" if result["deny_recommended"] else "OK"
|
|
499
|
+
print(f"[{flag}] #{args.issue} coherent={result['graph_coherent']} "
|
|
500
|
+
f"buildable={result['target_buildable']}")
|
|
501
|
+
if result.get("grandfathered"):
|
|
502
|
+
print(f" ℹ legacy graph grandfathered via #{result['grandfathered']}")
|
|
503
|
+
for v in result["violations"]:
|
|
504
|
+
print(f" - {v}")
|
|
505
|
+
if result.get("open_blockers"):
|
|
506
|
+
blocked = ", ".join(f"#{n}" for n in result["open_blockers"])
|
|
507
|
+
print(f" ⛔ natively blocked by open {blocked} (gates a build handoff)")
|
|
508
|
+
for w in result.get("drift_warnings", []):
|
|
509
|
+
print(f" ~ drift (non-blocking in audit): {w}")
|
|
510
|
+
for w in result.get("shape_warnings", []):
|
|
511
|
+
print(f" ~ shape (non-blocking): {w}")
|
|
512
|
+
return 0
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
if __name__ == "__main__":
|
|
516
|
+
sys.exit(main())
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""issue_deps.py — pure `## Blocked by` body-mirror logic.
|
|
3
|
+
|
|
4
|
+
Native GitHub issue dependencies (REST `dependencies/blocked_by`) are the SSOT
|
|
5
|
+
for blocking edges; the `## Blocked by` body section is a MACHINE-WRITTEN
|
|
6
|
+
mirror kept for readability (mail/export/search) and for trackers without
|
|
7
|
+
native dependencies (kit portability). On conflict the API wins.
|
|
8
|
+
|
|
9
|
+
Pure module by design (same rationale as anchor_table.py): render/splice/parse
|
|
10
|
+
carry no gh calls and no board_config, so board-sync.py (writer) and
|
|
11
|
+
execute-ready-check.py (drift reader) share one grammar without a subprocess
|
|
12
|
+
seam. Writers: board-sync.py `dep-add`/`dep-remove` — never hand-edited.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
_SECTION_HEADING = "## Blocked by"
|
|
19
|
+
_HEADING_RE = re.compile(r"^#{1,6}\s")
|
|
20
|
+
_BULLET_NUM_RE = re.compile(r"^-\s+#(\d+)\b")
|
|
21
|
+
|
|
22
|
+
_MIRROR_NOTE = ("*(Maschinell gepflegter Spiegel — die nativen "
|
|
23
|
+
"Issue-Dependencies (API) sind die Wahrheit; Pflege via "
|
|
24
|
+
"`scripts/board-sync.py dep-add`/`dep-remove`.)*")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def render_blocked_by_section(blockers: list[dict]) -> str:
|
|
28
|
+
"""The full mirror section for a non-empty blocker list."""
|
|
29
|
+
lines = [_SECTION_HEADING, ""]
|
|
30
|
+
lines += [f"- #{b['number']} ({b['state']}) — {b['title']}".rstrip()
|
|
31
|
+
for b in blockers]
|
|
32
|
+
lines += ["", _MIRROR_NOTE]
|
|
33
|
+
return "\n".join(lines)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _locate_section(lines: list[str]) -> tuple[int, int] | None:
|
|
37
|
+
"""(start, end) line span of the existing section, end exclusive."""
|
|
38
|
+
start = next((i for i, ln in enumerate(lines)
|
|
39
|
+
if ln.strip() == _SECTION_HEADING), None)
|
|
40
|
+
if start is None:
|
|
41
|
+
return None
|
|
42
|
+
end = next((i for i in range(start + 1, len(lines))
|
|
43
|
+
if _HEADING_RE.match(lines[i])), len(lines))
|
|
44
|
+
return start, end
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def splice_blocked_by_section(body: str, blockers: list[dict]) -> str:
|
|
48
|
+
"""Replace/append/remove the mirror section in `body`. Idempotent."""
|
|
49
|
+
lines = (body or "").splitlines()
|
|
50
|
+
span = _locate_section(lines)
|
|
51
|
+
section = render_blocked_by_section(blockers).splitlines() if blockers else []
|
|
52
|
+
if span is None:
|
|
53
|
+
if not section:
|
|
54
|
+
return body
|
|
55
|
+
head = "\n".join(lines).rstrip("\n")
|
|
56
|
+
return f"{head}\n\n" + "\n".join(section) + "\n"
|
|
57
|
+
start, end = span
|
|
58
|
+
new = lines[:start] + section + (["" ] if section and end < len(lines) else []) + lines[end:]
|
|
59
|
+
# collapse a doubled blank line left by removal
|
|
60
|
+
out = re.sub(r"\n{3,}", "\n\n", "\n".join(new))
|
|
61
|
+
return out if out.endswith("\n") else out + "\n"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def open_blocker_numbers(blockers: list[dict]) -> list[int]:
|
|
65
|
+
"""Numbers of the still-open blockers — the only ones that gate a build.
|
|
66
|
+
Lives here (not in board-sync.py) so writer AND drift reader share the
|
|
67
|
+
one "open" predicate."""
|
|
68
|
+
return [b["number"] for b in blockers if b.get("state") == "open"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_blocked_by_numbers(body: str) -> list[int]:
|
|
72
|
+
"""Issue numbers listed in the mirror section (drift-check reader side)."""
|
|
73
|
+
lines = (body or "").splitlines()
|
|
74
|
+
span = _locate_section(lines)
|
|
75
|
+
if span is None:
|
|
76
|
+
return []
|
|
77
|
+
start, end = span
|
|
78
|
+
return [int(m.group(1)) for ln in lines[start + 1:end]
|
|
79
|
+
if (m := _BULLET_NUM_RE.match(ln.strip()))]
|