@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,343 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""anchor_table.py — pure Slices-table logic for `board-sync.py anchor-sync`.
|
|
3
|
+
|
|
4
|
+
A wave-anchor issue body carries a Slices-table that duplicates board-tracked
|
|
5
|
+
volatile data. Hand-maintenance drifts (: stale branch, missing split-row).
|
|
6
|
+
`anchor-sync` regenerates the volatile cells IN PLACE: the table stays ONE
|
|
7
|
+
combined table, stable plan columns (Slice/Gate/…) are preserved verbatim, and
|
|
8
|
+
only the volatile cells are refreshed — and only when the board DISAGREES, so a
|
|
9
|
+
no-drift run reproduces the table byte-identically (AC#4) while a drifted cell
|
|
10
|
+
is corrected. Since the canonical table is the lean 6-column form
|
|
11
|
+
`# | Status | Slice | Sub-Issue | Gate | closes/refs` — Status is the only
|
|
12
|
+
volatile column there; legacy anchors with Branch / Blocked-by columns keep
|
|
13
|
+
working because every column is matched by header NAME and refreshed only
|
|
14
|
+
where it exists.
|
|
15
|
+
|
|
16
|
+
This module is PURE — no gh / no I/O / no board config. The gh-backed fetch and
|
|
17
|
+
the command wiring live in board-sync.py; everything here is directly unit-tested
|
|
18
|
+
(scripts/test_board_sync.py) with plain dicts and strings.
|
|
19
|
+
|
|
20
|
+
Columns are keyed by header NAME, not position — anchors carry free-form `#`
|
|
21
|
+
labels (4b/6b) and a varying last column (schließt/refs vs Blocked by).
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
SLICE_TABLE_START = "<!-- slice-table:start -->"
|
|
29
|
+
SLICE_TABLE_END = "<!-- slice-table:end -->"
|
|
30
|
+
|
|
31
|
+
_SUBISSUE_NUM_RE = re.compile(r"#(\d+)")
|
|
32
|
+
_STATUS_BASE_RE = re.compile(r"^(✅\s*#\d+|🔄|⬜)")
|
|
33
|
+
_STATUS_RANK = {"⬜": 0, "🔄": 1}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --- markdown pipe-table parse / render --------------------------------------
|
|
37
|
+
def split_pipe_row(line: str) -> list[str]:
|
|
38
|
+
"""Cells of a markdown pipe-table row (outer pipes stripped, cells trimmed)."""
|
|
39
|
+
s = line.strip()
|
|
40
|
+
if s.startswith("|"):
|
|
41
|
+
s = s[1:]
|
|
42
|
+
if s.endswith("|"):
|
|
43
|
+
s = s[:-1]
|
|
44
|
+
return [c.strip() for c in s.split("|")]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_separator_row(cells: list[str]) -> bool:
|
|
48
|
+
"""A `|---|:--:|` header-separator row (every cell only dashes/colons)."""
|
|
49
|
+
return bool(cells) and all(c and set(c) <= set("-: ") for c in cells)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_pipe_table(block_lines: list[str]) -> tuple[list[str], list[list[str]]]:
|
|
53
|
+
"""(headers, data_rows) from pipe-table lines. Rows stay as ordered cell-lists
|
|
54
|
+
(not dicts) so extra columns + cell text re-render verbatim. Separator dropped."""
|
|
55
|
+
rows = [split_pipe_row(ln) for ln in block_lines if ln.strip().startswith("|")]
|
|
56
|
+
if not rows:
|
|
57
|
+
return [], []
|
|
58
|
+
return rows[0], [r for r in rows[1:] if not is_separator_row(r)]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def render_pipe_table(headers: list[str], rows: list[list[str]]) -> str:
|
|
62
|
+
"""Render a canonical markdown pipe table (single-space padding)."""
|
|
63
|
+
lines = ["| " + " | ".join(headers) + " |",
|
|
64
|
+
"|" + "|".join("---" for _ in headers) + "|"]
|
|
65
|
+
for r in rows:
|
|
66
|
+
cells = (list(r) + [""] * len(headers))[:len(headers)]
|
|
67
|
+
lines.append("| " + " | ".join(cells) + " |")
|
|
68
|
+
return "\n".join(lines)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_MD_EMPHASIS_RE = re.compile(r"^(\*{1,2}|_{1,2})(.+)\1$")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _strip_markdown_emphasis(cell: str) -> str:
|
|
75
|
+
"""Strip one layer of wrapping markdown emphasis (`**Bold**`, `_em_`) so a
|
|
76
|
+
stylised header still exact-matches. Deliberately NOT a fuzzy/substring
|
|
77
|
+
strip — a genuine variant like 'Sub-Issue (native)' must stay unmatched."""
|
|
78
|
+
m = _MD_EMPHASIS_RE.match(cell)
|
|
79
|
+
return m.group(2) if m else cell
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def col_index(headers: list[str], name: str) -> Optional[int]:
|
|
83
|
+
"""Index of the column whose header — trimmed and stripped of one layer of
|
|
84
|
+
wrapping markdown emphasis — equals name exactly, or None."""
|
|
85
|
+
for i, h in enumerate(headers):
|
|
86
|
+
if _strip_markdown_emphasis(h.strip()) == name:
|
|
87
|
+
return i
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def require_col_index(headers: list[str], name: str) -> int:
|
|
92
|
+
"""`col_index(headers, name)`, or a loud ValueError if this column can't be
|
|
93
|
+
pinned down — e.g. a header variant like 'Sub-Issue (native)' that
|
|
94
|
+
`locate_slice_table` matched on a raw substring but `col_index` can't
|
|
95
|
+
exact/tolerant-match. Never let a silent None flow downstream and get
|
|
96
|
+
misread as "no row has this column": that used to make merge_slice_rows
|
|
97
|
+
treat every board sub-issue as missing and append it as a duplicate row,
|
|
98
|
+
on every run (proven by a 2026-07-02 double-run)."""
|
|
99
|
+
idx = col_index(headers, name)
|
|
100
|
+
if idx is None:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"slice table found but column {name!r} not matched "
|
|
103
|
+
f"(headers: {headers})")
|
|
104
|
+
return idx
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def first_subissue_num(cell: str) -> Optional[int]:
|
|
108
|
+
"""First `#<n>` in a cell — the Sub-Issue key, ignoring annotations like
|
|
109
|
+
`(schließt)`."""
|
|
110
|
+
m = _SUBISSUE_NUM_RE.search(cell or "")
|
|
111
|
+
return int(m.group(1)) if m else None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --- volatile-cell refresh (board → cell) ------------------------------------
|
|
115
|
+
def status_token_from_board(entry: dict, roles: dict) -> str:
|
|
116
|
+
"""Canonical Status token from board state: ✅ #<PR> (merged) · 🔄 (open PR or
|
|
117
|
+
an in-flight board status) · ⬜ (otherwise).
|
|
118
|
+
|
|
119
|
+
Which status NAMES count as in-flight comes from the profile's role map
|
|
120
|
+
(`roles["inProgress"]`/`roles["review"]`) — passed in as a plain
|
|
121
|
+
dict so this module stays pure (no board_config import). Empty roles →
|
|
122
|
+
board-status names can't be interpreted (⬜), PR signals still work."""
|
|
123
|
+
prs = entry.get("prs") or []
|
|
124
|
+
merged = [p for p in prs if p.get("state") == "MERGED"]
|
|
125
|
+
if merged:
|
|
126
|
+
return f"✅ #{merged[-1]['number']}"
|
|
127
|
+
active = {roles.get("inProgress"), roles.get("review")} - {None}
|
|
128
|
+
if [p for p in prs if p.get("state") == "OPEN"] or entry.get("status") in active:
|
|
129
|
+
return "🔄"
|
|
130
|
+
return "⬜"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def status_base(cell: str) -> str:
|
|
134
|
+
"""Leading canonical token of an existing Status cell (✅ #n / 🔄 / ⬜), or ''."""
|
|
135
|
+
m = _STATUS_BASE_RE.match((cell or "").strip())
|
|
136
|
+
return re.sub(r"\s+", " ", m.group(1)).strip() if m else ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _status_rank(base: str) -> int:
|
|
140
|
+
"""Monotone progress rank of a Status base token: ⬜ < 🔄 < ✅; '' = lowest."""
|
|
141
|
+
if base.startswith("✅"):
|
|
142
|
+
return 2
|
|
143
|
+
return _STATUS_RANK.get(base, -1)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def refresh_status_cell(existing: str, entry: dict, roles: dict) -> str:
|
|
147
|
+
"""Refresh a Status cell from the board, MONOTONICALLY (never regress).
|
|
148
|
+
|
|
149
|
+
Keep the existing cell verbatim when the board agrees (preserves hand
|
|
150
|
+
annotations like `(gen-a)`) OR when the board token would regress it — gen-a/gen-b
|
|
151
|
+
slices `Part of` the anchor stay OPEN while a part merges, so the board shows no
|
|
152
|
+
closing PR; a human ✅ must not flip back to ⬜. Only an advance (⬜→🔄→✅, or a
|
|
153
|
+
corrected merge-PR number) replaces the cell."""
|
|
154
|
+
token = status_token_from_board(entry, roles)
|
|
155
|
+
base = status_base(existing)
|
|
156
|
+
if _status_rank(token) < _status_rank(base):
|
|
157
|
+
return existing
|
|
158
|
+
return existing if base == token else token
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def branch_from_board(entry: dict) -> Optional[str]:
|
|
162
|
+
"""headRefName of the relevant linked PR (merged > open > any), or None."""
|
|
163
|
+
prs = entry.get("prs") or []
|
|
164
|
+
chosen = ([p for p in prs if p.get("state") == "MERGED"]
|
|
165
|
+
or [p for p in prs if p.get("state") == "OPEN"] or prs)
|
|
166
|
+
return chosen[-1].get("headRefName") if chosen else None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def refresh_branch_cell(existing: str, entry: dict) -> str:
|
|
170
|
+
"""Replace the branch with the linked PR's head once a PR exists and it differs;
|
|
171
|
+
keep verbatim when it agrees (preserves backticks) or when there is no PR yet
|
|
172
|
+
(the planned branch stays hand-owned)."""
|
|
173
|
+
head = branch_from_board(entry)
|
|
174
|
+
if not head:
|
|
175
|
+
return existing
|
|
176
|
+
if (existing or "").strip().strip("`").strip() == head:
|
|
177
|
+
return existing
|
|
178
|
+
return f"`{head}`"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def render_blocked_cell(blocked_by: list) -> str:
|
|
182
|
+
"""Canonical `Blocked by` cell from native edges: open plain, closed with ✓,
|
|
183
|
+
none → `—`. Machine-owned since (native dependencies are the SSOT)."""
|
|
184
|
+
if not blocked_by:
|
|
185
|
+
return "—"
|
|
186
|
+
ordered = ([b for b in blocked_by if b.get("state") == "open"]
|
|
187
|
+
+ [b for b in blocked_by if b.get("state") != "open"])
|
|
188
|
+
return ", ".join(f"#{b['number']}" + ("" if b.get("state") == "open" else " ✓")
|
|
189
|
+
for b in ordered)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def refresh_blocked_cell(existing: str, entry: dict) -> str:
|
|
193
|
+
"""Refresh a `Blocked by` cell from the entry's native edges.
|
|
194
|
+
|
|
195
|
+
Edges exist → the cell is machine-owned: replace unless already canonical.
|
|
196
|
+
No edges → clear only a cell that still carries stale `#n` refs; a plain
|
|
197
|
+
`—`/empty/prose cell stays verbatim (byte-stable no-drift runs)."""
|
|
198
|
+
blocked_by = entry.get("blocked_by")
|
|
199
|
+
if blocked_by is None:
|
|
200
|
+
return existing
|
|
201
|
+
canonical = render_blocked_cell(blocked_by)
|
|
202
|
+
if not blocked_by and not _SUBISSUE_NUM_RE.search(existing or ""):
|
|
203
|
+
return existing
|
|
204
|
+
return existing if (existing or "").strip() == canonical else canonical
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def merge_slice_rows(headers: list[str], rows: list[list[str]],
|
|
208
|
+
board: dict, roles: dict) -> tuple[list[list[str]], list[int]]:
|
|
209
|
+
"""Refresh Status/Branch per row from board (keyed by the Sub-Issue cell's first
|
|
210
|
+
#n) and append board sub-issues missing from the table. Returns (rows, appended).
|
|
211
|
+
Rows whose sub-issue is absent from the board stay verbatim.
|
|
212
|
+
|
|
213
|
+
Sub-Issue and Status are REQUIRED columns (`require_col_index` raises loudly
|
|
214
|
+
if a header variant can't be matched) — a silent None here used to make
|
|
215
|
+
`seen` stay empty and every board sub-issue get appended as a duplicate row
|
|
216
|
+
on every run."""
|
|
217
|
+
si = require_col_index(headers, "Sub-Issue")
|
|
218
|
+
st = require_col_index(headers, "Status")
|
|
219
|
+
br = col_index(headers, "Branch")
|
|
220
|
+
bl = col_index(headers, "Blocked by")
|
|
221
|
+
slice_i = col_index(headers, "Slice")
|
|
222
|
+
# A board sub-issue counts as present if its #n appears ANYWHERE in the
|
|
223
|
+
# Sub-Issue column — incl. a folded secondary ref like `(schließt)`
|
|
224
|
+
# — so it is never appended as a duplicate row.
|
|
225
|
+
seen: set = set()
|
|
226
|
+
for row in rows:
|
|
227
|
+
if si < len(row):
|
|
228
|
+
seen.update(int(n) for n in _SUBISSUE_NUM_RE.findall(row[si]))
|
|
229
|
+
out: list[list[str]] = []
|
|
230
|
+
for row in rows:
|
|
231
|
+
row = list(row)
|
|
232
|
+
sub = first_subissue_num(row[si]) if si < len(row) else None
|
|
233
|
+
if sub is not None and sub in board:
|
|
234
|
+
if st < len(row):
|
|
235
|
+
row[st] = refresh_status_cell(row[st], board[sub], roles)
|
|
236
|
+
if br is not None and br < len(row):
|
|
237
|
+
row[br] = refresh_branch_cell(row[br], board[sub])
|
|
238
|
+
if bl is not None and bl < len(row):
|
|
239
|
+
row[bl] = refresh_blocked_cell(row[bl], board[sub])
|
|
240
|
+
out.append(row)
|
|
241
|
+
appended: list[int] = []
|
|
242
|
+
for sub, entry in board.items():
|
|
243
|
+
if sub in seen:
|
|
244
|
+
continue
|
|
245
|
+
new = [""] * len(headers)
|
|
246
|
+
new[si] = f"#{sub}"
|
|
247
|
+
new[st] = status_token_from_board(entry, roles)
|
|
248
|
+
if br is not None:
|
|
249
|
+
head = branch_from_board(entry)
|
|
250
|
+
if head:
|
|
251
|
+
new[br] = f"`{head}`"
|
|
252
|
+
if bl is not None and entry.get("blocked_by") is not None:
|
|
253
|
+
new[bl] = render_blocked_cell(entry["blocked_by"])
|
|
254
|
+
if slice_i is not None:
|
|
255
|
+
new[slice_i] = entry.get("title", "")
|
|
256
|
+
out.append(new)
|
|
257
|
+
appended.append(sub)
|
|
258
|
+
return out, appended
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# --- locate / splice the table within the anchor body ------------------------
|
|
262
|
+
def locate_slice_table(lines: list[str]) -> Optional[tuple[int, int]]:
|
|
263
|
+
"""[start, end) of the existing slice table: the header pipe-row that names both
|
|
264
|
+
Status and Sub-Issue, plus the contiguous pipe rows after it.
|
|
265
|
+
|
|
266
|
+
Deliberately a raw substring match (lenient, to find variant tables) — that
|
|
267
|
+
can locate a header whose actual Sub-Issue/Status columns don't `col_index`
|
|
268
|
+
exact-match (e.g. 'Sub-Issue (native)'). That mismatch is caught downstream:
|
|
269
|
+
`merge_slice_rows` requires both columns via `require_col_index`, which
|
|
270
|
+
raises loudly instead of silently duplicating rows."""
|
|
271
|
+
hdr = next((i for i, ln in enumerate(lines)
|
|
272
|
+
if ln.strip().startswith("|") and "Status" in ln and "Sub-Issue" in ln), None)
|
|
273
|
+
if hdr is None:
|
|
274
|
+
return None
|
|
275
|
+
end = hdr + 1
|
|
276
|
+
while end < len(lines) and lines[end].strip().startswith("|"):
|
|
277
|
+
end += 1
|
|
278
|
+
return hdr, end
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def current_slice_table(body: str) -> tuple[list[str], list[list[str]]]:
|
|
282
|
+
"""Parse the live slice table — between markers if present, else the located block."""
|
|
283
|
+
if SLICE_TABLE_START in body and SLICE_TABLE_END in body:
|
|
284
|
+
inner = body.split(SLICE_TABLE_START, 1)[1].split(SLICE_TABLE_END, 1)[0]
|
|
285
|
+
return parse_pipe_table(inner.splitlines())
|
|
286
|
+
bounds = locate_slice_table(body.splitlines())
|
|
287
|
+
if bounds is None:
|
|
288
|
+
return [], []
|
|
289
|
+
return parse_pipe_table(body.splitlines()[bounds[0]:bounds[1]])
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def splice_slice_table(body: str, table_str: str) -> str:
|
|
293
|
+
"""Splice the rendered table between the markers, leaving the rest of the body
|
|
294
|
+
untouched. On the first run (no markers) the existing table block is located and
|
|
295
|
+
replaced with a marked block. Raises if no slice table is found."""
|
|
296
|
+
block = f"{SLICE_TABLE_START}\n{table_str}\n{SLICE_TABLE_END}"
|
|
297
|
+
if SLICE_TABLE_START in body and SLICE_TABLE_END in body:
|
|
298
|
+
pat = re.compile(re.escape(SLICE_TABLE_START) + r".*?" + re.escape(SLICE_TABLE_END),
|
|
299
|
+
re.DOTALL)
|
|
300
|
+
return pat.sub(lambda _m: block, body)
|
|
301
|
+
lines = body.splitlines()
|
|
302
|
+
bounds = locate_slice_table(lines)
|
|
303
|
+
if bounds is None:
|
|
304
|
+
raise ValueError("no slice table found in anchor body "
|
|
305
|
+
"(needs a header row naming both Status and Sub-Issue)")
|
|
306
|
+
start, end = bounds
|
|
307
|
+
result = "\n".join(lines[:start] + block.splitlines() + lines[end:])
|
|
308
|
+
return result + "\n" if body.endswith("\n") else result
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def extract_anchor_board_data(data: dict, status_field_id: str) -> dict:
|
|
312
|
+
"""{sub-issue number: {title, status, prs:[{number,state,headRefName}]}} from the
|
|
313
|
+
anchor sub-issues GraphQL response.
|
|
314
|
+
|
|
315
|
+
The status is matched by FIELD ID (`status_field_id` from the profile),
|
|
316
|
+
not by the display name "Status" — a consumer board may call the field
|
|
317
|
+
anything (mirrors the SessionStart hook's field-ID match)."""
|
|
318
|
+
issue = (data.get("data") or {}).get("repository", {}).get("issue")
|
|
319
|
+
nodes = ((issue or {}).get("subIssues") or {}).get("nodes") or []
|
|
320
|
+
out: dict = {}
|
|
321
|
+
for n in nodes:
|
|
322
|
+
prs = ((n.get("closedByPullRequestsReferences") or {}).get("nodes")) or []
|
|
323
|
+
status = None
|
|
324
|
+
for it in ((n.get("projectItems") or {}).get("nodes") or []):
|
|
325
|
+
for fv in ((it.get("fieldValues") or {}).get("nodes") or []):
|
|
326
|
+
if (fv.get("field") or {}).get("id") == status_field_id and fv.get("name"):
|
|
327
|
+
status = fv["name"]
|
|
328
|
+
break
|
|
329
|
+
if status:
|
|
330
|
+
break
|
|
331
|
+
blocked = ((n.get("blockedBy") or {}).get("nodes")) or []
|
|
332
|
+
out[n["number"]] = {
|
|
333
|
+
"title": n.get("title", ""),
|
|
334
|
+
"state": (n.get("state") or "").lower(),
|
|
335
|
+
"status": status,
|
|
336
|
+
"prs": [{"number": p["number"], "state": p["state"],
|
|
337
|
+
"headRefName": p.get("headRefName")} for p in prs],
|
|
338
|
+
# GraphQL state is UPPERCASE; normalize to the REST lowercase the
|
|
339
|
+
# dependency helpers use everywhere else.
|
|
340
|
+
"blocked_by": [{"number": b["number"],
|
|
341
|
+
"state": (b.get("state") or "").lower()} for b in blocked],
|
|
342
|
+
}
|
|
343
|
+
return out
|