@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,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wrapup
|
|
3
|
+
disable-model-invocation: true
|
|
4
|
+
"description": "Use ONLY when the user types /wrapup. Session-end \"land & clean\" for a finished feature/fix worktree — merges the open PR (= triggers prod deploy), kills the worktree dev server, removes the worktree + local branch, and fast-forwards the main checkout so main is current again, then sweeps merged-branch leftovers (local + stale remote whose PR is merged). If the slice isn't landed yet, it first makes it landable (Step 0): commits a dirty tree (after an .env/secret check), pushes, and opens the PR — reusing one if it already exists. User-triggered only (never auto-invoke, never hook). Aborts hard only on: not in a feature worktree, a detected .env/secret, a rejected push, a conflicting PR, or red (FAILURE) checks."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# wrapup — land PR & tear down worktree
|
|
8
|
+
|
|
9
|
+
Trigger: user types `/wrapup` (optionally with a PR number). **Manual only** — `disable-model-invocation: true`, no hook, no auto-invoke.
|
|
10
|
+
|
|
11
|
+
## ⚠ Spec context
|
|
12
|
+
|
|
13
|
+
**Merging to `main` = prod deploy.** The agent never deploys on its own — the user's `/wrapup` input IS the explicit merge+deploy authorization for that run. Never call this skill from a hook, another skill, or autonomously. Deploy banner before the merge, no y/n confirmation (deliberate); the pre-flight hard stops are non-negotiable.
|
|
14
|
+
|
|
15
|
+
## Execution model — script does mechanics, the agent does judgment
|
|
16
|
+
|
|
17
|
+
All enumerable git/gh plumbing lives in **`scripts/wrapup-land.py`** (`preflight` / `commit` / `land`, JSON report on stdout, exit 1 = STOP with reason in the JSON). It replaces the former Sonnet phase-2 subagent — measured over 120 runs the subagent burned a median 23 model turns on steps with zero judgment content (→ mechanized 2026-07).
|
|
18
|
+
|
|
19
|
+
The agent keeps: retro gate, secret review, commit message, PR body text, drift-fallback candidates, sibling propagation, anchor close, final report — plus **diagnosis whenever the script STOPs**. On any STOP: fix the named cause, re-run (the script is idempotent). **Force NOTHING** — no `--force`, no `-D`, no `--no-verify`.
|
|
20
|
+
|
|
21
|
+
## Flow
|
|
22
|
+
|
|
23
|
+
### 1 · Pre-flight
|
|
24
|
+
Run **in the worktree**:
|
|
25
|
+
```bash
|
|
26
|
+
python3 scripts/wrapup-land.py preflight
|
|
27
|
+
```
|
|
28
|
+
Hard stop (the only pure precondition): not in a feature worktree / on `main` — the script exits 1. The report carries everything the later steps need: dirty files, `.env` hits, secret-grep hits, issue + parent (leaf vs. anchor slice), existing PR + whether its body has a `**Retro:**` line, parsed `ANNAHMEN.md` drift lines, profile values (`retro_values`, `vor_bau`, remote-sweep switch).
|
|
29
|
+
|
|
30
|
+
### 2 · Retro gate (blocking, optional retro-exit — before anything is committed)
|
|
31
|
+
One reminder, not a merge confirmation:
|
|
32
|
+
> "Already ran a retro? **(a)** yes / continue → landing now. **(b)** you want one first → clean abort here, run `/retro`, then `/wrapup` again — repo-file patches then travel in this PR."
|
|
33
|
+
|
|
34
|
+
(b) → exit cleanly, touch nothing (a skill can't pause/resume another skill). **Never auto-run `/retro`.** The answer materializes as the mandatory `**Retro:**` PR-body line in step 4 — that's recording, not a second question. Why the gate lives here: in a foreign project this is the only portable retro touchpoint; a project-local "offer retro before PR" convention usually answers it already.
|
|
35
|
+
|
|
36
|
+
### 3 · Commit (only if the tree is dirty)
|
|
37
|
+
Judgment first, then the guarded commit:
|
|
38
|
+
- **Secret review:** judge the preflight `secret_hits` — a variable *named* `token` isn't a secret; a real key → resolve first, never commit. `.env` files are a mechanical hard block inside the script.
|
|
39
|
+
- **Commit message:** conventional, `<type>(<scope>): <summary> (#<issue>)` — type from the branch prefix, summary from the actual diff, issue from the branch.
|
|
40
|
+
```bash
|
|
41
|
+
python3 scripts/wrapup-land.py commit -m "<message>" # --allow-matches only after judging every hit a false positive
|
|
42
|
+
```
|
|
43
|
+
STOP on hook failure: many `Cannot find module`/TS2307 across **unrelated** files in a node/pnpm repo = stale worktree `node_modules` → `pnpm install --frozen-lockfile`, re-run. Real errors in slice files = legitimate stop. **Never `--no-verify`.**
|
|
44
|
+
|
|
45
|
+
### 4 · Author the PR body (agent-written, script-checked)
|
|
46
|
+
Write title + body to a temp file (inline bodies with backticks crash bash):
|
|
47
|
+
- Leaf issue → `closes #<n>`, **never inside backticks** (GitHub ignores the keyword there). Wave/cluster slice → `Part of #<anchor>`, **never `closes`** (would close the anchor early).
|
|
48
|
+
- **Mandatory `**Retro:**` line** — exactly one of the two `retro_values` from the preflight report (closed set, copy verbatim; `pr-body-check.py` rejects anything else): retro ran → `**Retro:** <value0> — findings under ## Retro / Meta-Findings`; skipped → `**Retro:** <value1> — <reason>`. "Nothing to retro" is the second value + reason, not a third form. Applies to every PR body, including ad-hoc `gh pr create` mid-session.
|
|
49
|
+
- **Assumption drift:** the build-time log `ANNAHMEN.md` is explicitly captured content — its well-formed lines become `annahme-drift` markers **mechanically, no confirmation gate** (`land` merges them into the body; decision 2026-07-06, replaced the old propose+confirm for log entries). Two cases stay with the agent:
|
|
50
|
+
- **Malformed line** (no `#<n>` target) → clarify with the user (fix the target or discard deliberately), never drop silently; `--skip-malformed-drift` only for a deliberately discarded rest.
|
|
51
|
+
- **No log / empty log** → fallback, retro-style: walk the slice's deliberately made or reversed assumptions yourself and present **named candidates** (`- #<n>?: <assumption> → might carry <issue>`); user confirms → write the markers into the body by hand. Zero candidates → say so explicitly ("no drift found — checked: <what>"). The log is the floor, not the ceiling — drift noticed while landing goes in the same way.
|
|
52
|
+
|
|
53
|
+
### 5 · Land
|
|
54
|
+
Run **from the main tree** (the script refuses inside the worktree — an in-worktree shell would survive teardown and the process kill):
|
|
55
|
+
```bash
|
|
56
|
+
python3 scripts/wrapup-land.py land --branch "<branch>" --title "<title>" --body-file /tmp/wrapup-pr-body.md
|
|
57
|
+
```
|
|
58
|
+
One call covers: push → PR create/reuse (+ drift markers merged into the body) → `pr-body-check.py` gate (exit 1 = STOP, exit 2 = fail-open warning) → merge gate (red check / `CONFLICTING` = STOP; fresh-PR `UNKNOWN` mergeable passes — the merge itself is the real gate) → **merge** (`--merge` + `--delete-branch`, verified `MERGED`) → dev-server kill (`.dev-ports` ports + cwd-under-worktree walk, own shell ancestry excluded) → worktree remove (no `--force`; refusal = STOP, check surviving processes first) → main `--ff-only` pull + `branch -d` → issue-close verify (auto-close misses are closed manually) → local merged-branch sweep (`-d` only — squash/rebase-merged branches stay a manual call by design) → remote merged-PR sweep (opt-in `wrapup.remoteBranchSweep` in the board profile; PR-status-authoritative via `ls-remote`; deleted remote branches are restorable from the PR page) → anchor-sync (dry-run diff in the report) + anchor completeness check + `execute-ready-check --mode audit` → **upward propagation:** if the anchor's native parent is a Program-PRD, `program-sync` refreshes its Wellenplan (Status + Issue cells) and checks off mechanically completed Phasen-Gates — the slice event is visible at the program level, not only in the wave (`program_sync` block in the report; skipped when the parent isn't a program).
|
|
59
|
+
|
|
60
|
+
STOP → diagnose in the main conversation, fix, re-run `land` (an already-merged PR resumes at teardown).
|
|
61
|
+
|
|
62
|
+
### 6 · Post-merge (agent)
|
|
63
|
+
- **Deploy banner**, one line: `⚠ PR #<pr> merged → main deploys (~7 min live).`
|
|
64
|
+
- **Sibling propagation:** for each `drift_markers` entry in the land report, append the note to the target issue's `vor_bau` section + re-stamp its `plan_revision`. Log-based markers → **write directly, then show what was written where** (mandatory report — visibility moved from a pre-gate into the report, decision 2026-07-06); fallback candidates the user hasn't confirmed yet → confirm first. Program context widens the target set to unbuilt wave-stubs/leaves and the Program-PRD itself — same append-only mechanism. **Exception:** appends to the Program-PRD or unbuilt wave-stubs do **not** re-stamp `plan_revision` — that stays reserved for structural wave-plan edits via the `to-waves` escalation path; a mere drift note must not stale-block published stubs.
|
|
65
|
+
- **Anchor close:** report says `anchor_complete: true` → `gh issue close <anchor> -c "Wave complete — all slices merged via PR #<pr>."` and verify board status Done. The guard keeps anchors away from every auto-close — this verified close is the only close path; without it the anchor stays silently open after the last slice. **Then re-run the upward propagation**: the land-time `program-sync` ran BEFORE this close, so on a wave-completing slice the Wellenplan still shows 🔄 and the Phasen-Gate stays unchecked — after the board shows Done, run `python3 scripts/board-sync.py program-sync <program-prd#>` once more (the report's `program_sync.program` names it; skip when the report says the parent is not a program). Board auto-rules can lag the close (Close→Done race) — verify Done first, that's what the token reads.
|
|
66
|
+
- **Report**, concise, from the land JSON: PR merged · issue close (auto/manual) · worktree removed · branch deleted · sweep counts local/remote · anchor synced + complete/pending · program propagation (`program_sync`: Wellenplan refreshed / gates checked / skipped) · propagation writes (what → where) · `main` at `<sha>` · deploy running.
|
|
67
|
+
|
|
68
|
+
## Out of scope
|
|
69
|
+
- Live-verify / DoD: must happen **before** `/wrapup` — this skill lands, it does not verify.
|
|
70
|
+
- `/retro`: offered before landing (step 2), never run by this skill.
|
|
71
|
+
- Other worktrees / their servers stay untouched.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zoom-out
|
|
3
|
+
description: "Tell the agent to zoom out and give broader context or a higher-level perspective. Use when you're unfamiliar with a section of code or need to understand how it fits into the bigger picture."
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
I don't know this area of code well. Go up a layer of abstraction. Give me a map of all the relevant modules and callers, using the project's domain glossary vocabulary. (For a full refactor/deepening pass with an HTML report and grilling loop, use `improve-codebase-architecture` instead.)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Third-Party Notices
|
|
2
|
+
|
|
3
|
+
This skill is adapted from **Matt Pocock's skills**
|
|
4
|
+
(https://github.com/mattpocock/skills), `skills/engineering/zoom-out` @ 2bf7005, Copyright (c) 2026 Matt Pocock,
|
|
5
|
+
used under the MIT License. Locally adapted (fork-and-own); see PROVENANCE.md.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Matt Pocock
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
```
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Shared helpers for .claude/hooks/*.py — logging, subprocess wrappers,
|
|
4
|
+
git inspection. Underscore-prefix signals "not a hook, not invoked by
|
|
5
|
+
Claude Code directly".
|
|
6
|
+
|
|
7
|
+
Python adds the script directory to sys.path automatically, so any hook
|
|
8
|
+
in this folder can `from _hook_utils import ...`.
|
|
9
|
+
|
|
10
|
+
Migration notes for hook refactors:
|
|
11
|
+
- `log(name, msg)` takes two args (per-hook name + message). Callers must
|
|
12
|
+
pass their hook name as the first arg — there is no implicit per-module
|
|
13
|
+
log-file binding. Recommended pattern: `HOOK_NAME = "<hook-name>"` as a
|
|
14
|
+
module-level constant in each hook, then `log(HOOK_NAME, msg)` at the
|
|
15
|
+
call sites.
|
|
16
|
+
- `run()` is silent-on-failure: exceptions return "" without logging. The
|
|
17
|
+
pre-refactor `run()` in some hooks (notably enforce-worktree-cwd.py)
|
|
18
|
+
logged `"exec failed: ..."` on subprocess exception. That diagnostic
|
|
19
|
+
log is intentionally dropped — git/subprocess crashes are rare and the
|
|
20
|
+
meaningful Audit-log entries (`BLOCKED cmd=...`) still get written.
|
|
21
|
+
"""
|
|
22
|
+
import re
|
|
23
|
+
import subprocess
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# Resolved relative to CWD at call time. Hooks are invoked from repo root
|
|
28
|
+
# (or from a linked worktree root), so this resolves to <root>/.claude/logs.
|
|
29
|
+
LOG_DIR = Path(".claude/logs")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def rotate_log_if_needed(log_path: Path, max_bytes: int = 100_000, generations: int = 3) -> None:
|
|
33
|
+
"""
|
|
34
|
+
If log_path exceeds max_bytes, rotate it: .log → .log.1 → .log.2 → ... → .log.<generations>,
|
|
35
|
+
oldest dropped. Silent-on-failure.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
if not log_path.exists():
|
|
39
|
+
return
|
|
40
|
+
if log_path.stat().st_size <= max_bytes:
|
|
41
|
+
return
|
|
42
|
+
# Drop the oldest generation
|
|
43
|
+
oldest = log_path.with_suffix(log_path.suffix + f".{generations}")
|
|
44
|
+
if oldest.exists():
|
|
45
|
+
oldest.unlink()
|
|
46
|
+
# Shift .log.<N-1> → .log.<N>, ..., .log.1 → .log.2
|
|
47
|
+
for n in range(generations - 1, 0, -1):
|
|
48
|
+
src = log_path.with_suffix(log_path.suffix + f".{n}")
|
|
49
|
+
dst = log_path.with_suffix(log_path.suffix + f".{n+1}")
|
|
50
|
+
if src.exists():
|
|
51
|
+
src.rename(dst)
|
|
52
|
+
# .log → .log.1
|
|
53
|
+
log_path.rename(log_path.with_suffix(log_path.suffix + ".1"))
|
|
54
|
+
except Exception:
|
|
55
|
+
pass # silent
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def log(name: str, msg: str) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Append `<ISO-timestamp> <msg>\\n` to .claude/logs/<name>.log. Creates
|
|
61
|
+
LOG_DIR if missing. Rotates first if file exceeds 100 KB. Silent-on-failure.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
log_file = LOG_DIR / f"{name}.log"
|
|
66
|
+
rotate_log_if_needed(log_file)
|
|
67
|
+
with log_file.open("a", encoding="utf-8") as f:
|
|
68
|
+
f.write(f"{datetime.now().isoformat(timespec='seconds')} {msg}\n")
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run(cmd: list[str], timeout: int = 5) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Wrapper around subprocess.run. Returns stdout.strip() on success, "" on any failure.
|
|
76
|
+
Failures are NOT logged here (caller decides whether to log).
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
80
|
+
return r.stdout.strip()
|
|
81
|
+
except Exception:
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def run_with_status(cmd: list[str], timeout: int = 15) -> tuple[int, str]:
|
|
86
|
+
"""
|
|
87
|
+
Wrapper that returns (returncode, stdout) — for hooks that branch on exit code.
|
|
88
|
+
Returns (-1, "") on exception.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
92
|
+
return r.returncode, r.stdout
|
|
93
|
+
except Exception:
|
|
94
|
+
return -1, ""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_main_worktree() -> bool:
|
|
98
|
+
"""True if GIT_DIR == GIT_COMMON_DIR (= main tree, not a linked worktree)."""
|
|
99
|
+
git_dir = run(["git", "rev-parse", "--git-dir"])
|
|
100
|
+
git_common = run(["git", "rev-parse", "--git-common-dir"])
|
|
101
|
+
if not git_dir or not git_common:
|
|
102
|
+
return False
|
|
103
|
+
try:
|
|
104
|
+
return Path(git_dir).resolve() == Path(git_common).resolve()
|
|
105
|
+
except Exception:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def current_branch() -> str:
|
|
110
|
+
"""Returns current branch name or "" if detached/not a repo."""
|
|
111
|
+
return run(["git", "branch", "--show-current"])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
_BRANCH_ISSUE_RE = re.compile(r"^(?:feat|fix)/(\d+)-")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def parse_issue_from_branch(branch: str):
|
|
118
|
+
"""Issue number (str) from a `feat/<n>-…` / `fix/<n>-…` branch, else None.
|
|
119
|
+
Shared so hooks don't each re-declare the convention regex."""
|
|
120
|
+
m = _BRANCH_ISSUE_RE.match(branch or "")
|
|
121
|
+
return m.group(1) if m else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def repo_root() -> str:
|
|
125
|
+
"""Returns repo root or "" if not a repo."""
|
|
126
|
+
return run(["git", "rev-parse", "--show-toplevel"])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def is_git_ignored(path: str) -> bool:
|
|
130
|
+
"""True if `path` is git-ignored (safe to edit anywhere — never enters a commit).
|
|
131
|
+
|
|
132
|
+
Uses `git check-ignore -q`: exit 0 = ignored, 1 = not ignored, 128 = error.
|
|
133
|
+
Only exit 0 counts as ignored; on not-matched OR error we return False so the
|
|
134
|
+
caller errs toward enforcing discipline rather than silently allowing an edit.
|
|
135
|
+
Caveat: a force-added tracked file that also matches an ignore pattern reports
|
|
136
|
+
ignored — acceptable, that combination is vanishingly rare in this repo.
|
|
137
|
+
"""
|
|
138
|
+
if not path:
|
|
139
|
+
return False
|
|
140
|
+
rc, _ = run_with_status(["git", "check-ignore", "-q", "--", path], timeout=5)
|
|
141
|
+
return rc == 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def has_feature_worktrees() -> bool:
|
|
145
|
+
"""True if at least one linked worktree exists with a non-main branch."""
|
|
146
|
+
output = run(["git", "worktree", "list", "--porcelain"])
|
|
147
|
+
if not output:
|
|
148
|
+
return False
|
|
149
|
+
for block in output.split("\n\n"):
|
|
150
|
+
m = re.search(r"^branch refs/heads/(.+)$", block, re.MULTILINE)
|
|
151
|
+
if m:
|
|
152
|
+
branch = m.group(1).strip()
|
|
153
|
+
if branch and branch not in ("main", "master"):
|
|
154
|
+
return True
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def count_worktrees() -> int:
|
|
159
|
+
"""Total number of worktrees (incl. main)."""
|
|
160
|
+
output = run(["git", "worktree", "list"])
|
|
161
|
+
if not output:
|
|
162
|
+
return 0
|
|
163
|
+
return len([line for line in output.splitlines() if line.strip()])
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def normalize_to_repo_relative(file_path: str, root: str) -> str | None:
|
|
167
|
+
"""
|
|
168
|
+
Converts absolute paths to repo-relative paths.
|
|
169
|
+
- Relative passthrough.
|
|
170
|
+
- Absolute inside root: relative-to-root.
|
|
171
|
+
- Absolute outside root or empty input: None.
|
|
172
|
+
"""
|
|
173
|
+
if not file_path:
|
|
174
|
+
return None
|
|
175
|
+
p = Path(file_path)
|
|
176
|
+
if not p.is_absolute():
|
|
177
|
+
return file_path
|
|
178
|
+
if not root:
|
|
179
|
+
return None
|
|
180
|
+
try:
|
|
181
|
+
rel = p.resolve().relative_to(Path(root).resolve())
|
|
182
|
+
return str(rel)
|
|
183
|
+
except ValueError:
|
|
184
|
+
return None
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Drift-Guard — PreToolUse block on Write|Edit|MultiEdit of a handoff doc when the
|
|
4
|
+
linked GitHub issue's rooted graph is not execute-ready.
|
|
5
|
+
|
|
6
|
+
Why a hook (not skill prose): the `handoff` skill AND the global
|
|
7
|
+
`grill-with-docs-codex` live OUTSIDE the repo. Only a repo-side hook fires
|
|
8
|
+
regardless of which global skill triggered the Write — the single repo-side net
|
|
9
|
+
covering the -codex path.
|
|
10
|
+
|
|
11
|
+
Coverage (honestly bounded): fires at the handoff / session-boundary Write.
|
|
12
|
+
NOT a board-wide scan, NOT a "grill-exit" event (no clean tool hook for that).
|
|
13
|
+
Accepted gap: Bash redirect / tee / cp into `.handoff/` is not covered — the
|
|
14
|
+
threat model is "skill/agent forgot", not an adversary; handoff writes via Write.
|
|
15
|
+
|
|
16
|
+
Mechanism: self-filters to `.handoff/*.md`; extracts the issue (content anchor
|
|
17
|
+
first, filename fallback); delegates ALL coherence to scripts/execute-ready-check.py
|
|
18
|
+
(--mode handoff). Deny = exit 2 + stderr (house pattern: enforce-worktree.py,
|
|
19
|
+
block-secrets.py). Override: a deliberate `<!-- guard-ack: #<n> r<N> reason:… by-user -->`
|
|
20
|
+
in the content. fail-closed once a target is parsed (the checker enforces that);
|
|
21
|
+
fail-OPEN when no handoff target is identifiable (not the stale handoff we guard).
|
|
22
|
+
|
|
23
|
+
Audit log: .claude/logs/drift-guard.log
|
|
24
|
+
"""
|
|
25
|
+
import importlib.util
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from _hook_utils import log
|
|
32
|
+
|
|
33
|
+
HOOK_NAME = "drift-guard"
|
|
34
|
+
HANDLED_TOOLS = {"Write", "Edit", "MultiEdit"}
|
|
35
|
+
HANDOFF_PATH_RE = re.compile(r"/\.handoff/[^/]*\.md$")
|
|
36
|
+
ISSUE_ANCHOR_RE = re.compile(r"/issues/(\d+)") # content-first: [#n](…/issues/n)
|
|
37
|
+
FILENAME_ISSUE_RE = re.compile(r"(\d+)\.md$") # fallback: <date>-<n>.md
|
|
38
|
+
# Override must be issue-scoped, rev-scoped, reasoned, by-user (Codex R1 — not the cheap `known`).
|
|
39
|
+
GUARD_ACK_RE = re.compile(r"<!--\s*guard-ack:\s*#?\d+\s+r\d+\s+reason:.+\bby-user\s*-->",
|
|
40
|
+
re.IGNORECASE | re.DOTALL)
|
|
41
|
+
|
|
42
|
+
# Load the shared checker (hyphenated filename → importlib). Repo root = parents[2].
|
|
43
|
+
_CHECKER_PATH = Path(__file__).resolve().parents[2] / "scripts" / "execute-ready-check.py"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_CHECKER = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _load_checker():
|
|
50
|
+
"""Load + cache the shared checker (module exec is not free — load once per
|
|
51
|
+
process, not once per call site)."""
|
|
52
|
+
global _CHECKER
|
|
53
|
+
if _CHECKER is None:
|
|
54
|
+
spec = importlib.util.spec_from_file_location("execute_ready_check", _CHECKER_PATH)
|
|
55
|
+
mod = importlib.util.module_from_spec(spec)
|
|
56
|
+
spec.loader.exec_module(mod)
|
|
57
|
+
_CHECKER = mod
|
|
58
|
+
return _CHECKER
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_handoff_write(payload: dict) -> bool:
|
|
62
|
+
if payload.get("tool_name") not in HANDLED_TOOLS:
|
|
63
|
+
return False
|
|
64
|
+
fp = (payload.get("tool_input") or {}).get("file_path", "")
|
|
65
|
+
return bool(HANDOFF_PATH_RE.search(fp))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_content(payload: dict) -> str:
|
|
69
|
+
ti = payload.get("tool_input") or {}
|
|
70
|
+
tool = payload.get("tool_name")
|
|
71
|
+
if tool == "Write":
|
|
72
|
+
return ti.get("content", "") or ""
|
|
73
|
+
if tool == "Edit":
|
|
74
|
+
return ti.get("new_string", "") or ""
|
|
75
|
+
if tool == "MultiEdit":
|
|
76
|
+
return "\n".join((e or {}).get("new_string", "") for e in ti.get("edits", []) or [])
|
|
77
|
+
return ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def extract_issue(payload: dict, content: str):
|
|
81
|
+
# Content anchor first, filename fallback. Deliberately NO branch fallback:
|
|
82
|
+
# a meta/tooling handoff carries no issue (handoff skill supports this) and
|
|
83
|
+
# must fail-open; a branch-based guess would mis-attribute the branch's issue
|
|
84
|
+
# and false-block. None here → should_block() allows (not the stale handoff we guard).
|
|
85
|
+
m = ISSUE_ANCHOR_RE.search(content or "")
|
|
86
|
+
if m:
|
|
87
|
+
return int(m.group(1))
|
|
88
|
+
fp = (payload.get("tool_input") or {}).get("file_path", "")
|
|
89
|
+
m = FILENAME_ISSUE_RE.search(fp)
|
|
90
|
+
if m:
|
|
91
|
+
return int(m.group(1))
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def run_check(issue: int, intent: str) -> dict:
|
|
96
|
+
"""Delegate to the shared checker. Isolated so tests can patch it."""
|
|
97
|
+
try:
|
|
98
|
+
checker = _load_checker()
|
|
99
|
+
return checker.build_and_evaluate(issue, "handoff", intent)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
# checker itself unavailable → fail-closed (a target was identified)
|
|
102
|
+
log(HOOK_NAME, f"checker load/exec failed: {e} → fail-closed")
|
|
103
|
+
return {"deny_recommended": True, "graph_coherent": False,
|
|
104
|
+
"target_buildable": False, "grandfathered": None,
|
|
105
|
+
"violations": [f"#{issue}: checker unavailable ({e}) — fail-closed"]}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _infer_intent(content: str) -> str:
|
|
109
|
+
try:
|
|
110
|
+
return _load_checker().infer_intent(content)
|
|
111
|
+
except Exception:
|
|
112
|
+
return "build"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def should_block(payload: dict):
|
|
116
|
+
"""Returns (block: bool, message: str)."""
|
|
117
|
+
if not is_handoff_write(payload):
|
|
118
|
+
return False, ""
|
|
119
|
+
content = extract_content(payload)
|
|
120
|
+
if GUARD_ACK_RE.search(content):
|
|
121
|
+
log(HOOK_NAME, "guard-ack override present → allow")
|
|
122
|
+
return False, ""
|
|
123
|
+
issue = extract_issue(payload, content)
|
|
124
|
+
if issue is None:
|
|
125
|
+
log(HOOK_NAME, "no identifiable issue target → fail-open allow")
|
|
126
|
+
return False, ""
|
|
127
|
+
intent = _infer_intent(content)
|
|
128
|
+
result = run_check(issue, intent)
|
|
129
|
+
if result.get("deny_recommended"):
|
|
130
|
+
return True, build_block_message(issue, intent, result)
|
|
131
|
+
return False, ""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def build_block_message(issue: int, intent: str, result: dict) -> str:
|
|
135
|
+
lines = [f"DRIFT-GUARD — Handoff für #{issue} BLOCKED (nicht execute-ready):", ""]
|
|
136
|
+
for v in result.get("violations", []):
|
|
137
|
+
lines.append(f" · {v}")
|
|
138
|
+
if intent == "build" and not result.get("target_buildable", True) and result.get("graph_coherent"):
|
|
139
|
+
lines.append(f" · #{issue} ist HITL (gültig, aber nicht baubar) — erst grillen, kein /tdd")
|
|
140
|
+
if result.get("open_blockers"):
|
|
141
|
+
blocked = ", ".join(f"#{n}" for n in result["open_blockers"])
|
|
142
|
+
lines.append(f" · #{issue} ist nativ blockiert durch offene {blocked} "
|
|
143
|
+
f"(Blocking-SSOT = Issue-Dependencies) — erst Blocker landen")
|
|
144
|
+
lines += [
|
|
145
|
+
"",
|
|
146
|
+
"Fix (eines):",
|
|
147
|
+
f" - re-grill #{issue} → Re-Grill-Reconcile gleicht ab + stempelt plan_revision neu, ODER",
|
|
148
|
+
" - bewusster Override in den Handoff-Body:",
|
|
149
|
+
f" <!-- guard-ack: #{issue} r<N> reason:<warum> by-user -->",
|
|
150
|
+
"",
|
|
151
|
+
"(Legacy-Alt-Anker: <!-- guard-legacy --> in den Anker-Issue setzen (einmalig,",
|
|
152
|
+
" NICHT in den Handoff) → ganzer Graph grandfathered, kein Block.)",
|
|
153
|
+
]
|
|
154
|
+
return "\n".join(lines)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main() -> int:
|
|
158
|
+
try:
|
|
159
|
+
payload = json.load(sys.stdin)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
log(HOOK_NAME, f"bad stdin: {e}")
|
|
162
|
+
return 0
|
|
163
|
+
try:
|
|
164
|
+
block, message = should_block(payload)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
log(HOOK_NAME, f"should_block crashed: {e} → allow (do not wedge handoff on hook bug)")
|
|
167
|
+
return 0
|
|
168
|
+
if not block:
|
|
169
|
+
return 0
|
|
170
|
+
print(message, file=sys.stderr)
|
|
171
|
+
log(HOOK_NAME, f"BLOCKED handoff path={(payload.get('tool_input') or {}).get('file_path')!r}")
|
|
172
|
+
return 2
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
sys.exit(main())
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SessionStart soft-hint: flag project skills whose declared source files moved in git
|
|
4
|
+
since the skill's own last commit — a cue to re-check the SKILL.md against reality.
|
|
5
|
+
|
|
6
|
+
Mechanism: each drift-prone skill declares its code/doc anchors in a co-located
|
|
7
|
+
`SOURCES.txt` (one repo-relative path per line; `#` comments and blank lines ignored).
|
|
8
|
+
For each such skill the hook compares `git log -1 --format=%ct` on the SKILL.md vs.
|
|
9
|
+
each source file; a source committed *after* the skill → the skill may be stale.
|
|
10
|
+
|
|
11
|
+
Committing (touching) the SKILL.md resets the signal — that is how you "quittieren".
|
|
12
|
+
Non-blocking: emits SessionStart additionalContext, silent on every failure.
|
|
13
|
+
|
|
14
|
+
Carrier is SOURCES.txt (not SKILL.md frontmatter) on purpose: the skill loader's
|
|
15
|
+
tolerance for extra frontmatter keys is unverified, and a sibling file can never
|
|
16
|
+
break skill loading.
|
|
17
|
+
|
|
18
|
+
Audit log: .claude/logs/skill-drift-hint.log
|
|
19
|
+
"""
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from _hook_utils import log, run, repo_root
|
|
25
|
+
|
|
26
|
+
HOOK_NAME = "skill-drift-hint"
|
|
27
|
+
SKILLS_REL = ".claude/skills"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def git_commit_time(root: str, rel_path: str) -> int | None:
|
|
31
|
+
"""Unix ctime of the last commit touching rel_path; None if untracked/unknown."""
|
|
32
|
+
out = run(["git", "-C", root, "log", "-1", "--format=%ct", "--", rel_path])
|
|
33
|
+
return int(out) if out.isdigit() else None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_sources(sources_file: Path) -> list[str]:
|
|
37
|
+
"""Repo-relative source paths from a SOURCES.txt — skips blanks and `#` comments."""
|
|
38
|
+
try:
|
|
39
|
+
lines = sources_file.read_text(encoding="utf-8").splitlines()
|
|
40
|
+
except Exception:
|
|
41
|
+
return []
|
|
42
|
+
out: list[str] = []
|
|
43
|
+
for line in lines:
|
|
44
|
+
s = line.strip()
|
|
45
|
+
if not s or s.startswith("#"):
|
|
46
|
+
continue
|
|
47
|
+
out.append(s)
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def collect_stale(root: str) -> list[tuple[str, list[str]]]:
|
|
52
|
+
"""For each skill with a SOURCES.txt, list sources newer (in git) than its SKILL.md."""
|
|
53
|
+
skills_dir = Path(root) / SKILLS_REL
|
|
54
|
+
results: list[tuple[str, list[str]]] = []
|
|
55
|
+
if not skills_dir.is_dir():
|
|
56
|
+
return results
|
|
57
|
+
for sources_file in sorted(skills_dir.glob("*/SOURCES.txt")):
|
|
58
|
+
skill_name = sources_file.parent.name
|
|
59
|
+
skill_ct = git_commit_time(root, f"{SKILLS_REL}/{skill_name}/SKILL.md")
|
|
60
|
+
if skill_ct is None:
|
|
61
|
+
continue # skill not committed yet → nothing to compare against
|
|
62
|
+
stale = [
|
|
63
|
+
src
|
|
64
|
+
for src in read_sources(sources_file)
|
|
65
|
+
if (src_ct := git_commit_time(root, src)) is not None and src_ct > skill_ct
|
|
66
|
+
]
|
|
67
|
+
if stale:
|
|
68
|
+
results.append((skill_name, stale))
|
|
69
|
+
return results
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_context(root: str) -> str | None:
|
|
73
|
+
stale = collect_stale(root)
|
|
74
|
+
if not stale:
|
|
75
|
+
return None
|
|
76
|
+
lines: list[str] = []
|
|
77
|
+
for skill_name, sources in stale:
|
|
78
|
+
lines.append(f"⚠ `{skill_name}` — source(s) moved since the skill's last touch:")
|
|
79
|
+
for src in sources:
|
|
80
|
+
lines.append(f" - {src}")
|
|
81
|
+
block = "\n".join(lines)
|
|
82
|
+
return (
|
|
83
|
+
"## Skill freshness (SessionStart hook)\n\n"
|
|
84
|
+
"```\n"
|
|
85
|
+
f"{block}\n"
|
|
86
|
+
"```\n\n"
|
|
87
|
+
"Heads-up (non-blocking): re-check the named SKILL.md against its source — "
|
|
88
|
+
"paths/symbols/line numbers may have moved. If the doc still holds, commit the skill "
|
|
89
|
+
"along with your change (a touch acknowledges the signal). "
|
|
90
|
+
"Source: .claude/hooks/skill-drift-hint.py + a per-skill SOURCES.txt."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main() -> int:
|
|
95
|
+
try:
|
|
96
|
+
json.load(sys.stdin)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
root = repo_root() or "."
|
|
101
|
+
try:
|
|
102
|
+
context = build_context(root)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
log(HOOK_NAME, f"build_context failed: {e}")
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
if context is None:
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
payload = {
|
|
111
|
+
"hookSpecificOutput": {
|
|
112
|
+
"hookEventName": "SessionStart",
|
|
113
|
+
"additionalContext": context,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
print(json.dumps(payload))
|
|
117
|
+
log(HOOK_NAME, f"emitted: {context.count(chr(0x26A0))} skill(s) flagged")
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
sys.exit(main())
|