@ikon85/agent-workflow-kit 0.39.0 → 0.41.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/grill-me/SKILL.md +1 -1
- package/.agents/skills/grill-with-docs/SKILL.md +1 -1
- package/.agents/skills/orchestrate-wave/SKILL.md +4 -4
- package/.agents/skills/setup-workflow/SKILL.md +50 -0
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +23 -1
- package/.claude/skills/grill-me/SKILL.md +1 -1
- package/.claude/skills/grill-with-docs/SKILL.md +1 -1
- package/.claude/skills/orchestrate-wave/SKILL.md +4 -4
- package/.claude/skills/setup-workflow/SKILL.md +50 -0
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +23 -1
- package/README.md +33 -0
- package/agent-workflow-kit.package.json +29 -13
- package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +1 -1
- package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +84 -0
- package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +101 -0
- package/package.json +1 -1
- package/scripts/pr-body-check.py +15 -2
- package/scripts/test_pr_body_check.py +10 -0
- package/scripts/test_worktree_ignore_seed.py +320 -0
- package/scripts/worktree-lifecycle/README.md +18 -0
- package/scripts/worktree-lifecycle/ignore_seed.py +226 -0
- package/scripts/worktree-lifecycle/plan-artifacts.json +37 -0
- package/src/lib/bundle.mjs +6 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Offer the planning-artifact ignore rules for a consumer `.gitignore`.
|
|
3
|
+
|
|
4
|
+
The shipped skills write `PLAN.md`, `PLAN-REVIEW-LOG.md`, and `ANNAHMEN.md`
|
|
5
|
+
into a session worktree, but `.gitignore` is a consumer file the kit does not
|
|
6
|
+
own: `init` and `update` never touch it. This helper is the one place that
|
|
7
|
+
closes that gap, and it closes it only when a user explicitly approves the
|
|
8
|
+
offer inside `/setup-workflow` (ADR 0008).
|
|
9
|
+
|
|
10
|
+
Contract:
|
|
11
|
+
|
|
12
|
+
- `preview` reads and reports; it never writes.
|
|
13
|
+
- `apply` appends one marker block and nothing else. It never rewrites,
|
|
14
|
+
reorders, or removes an existing line, so a second run is a byte-identical
|
|
15
|
+
no-op once the rules are in place.
|
|
16
|
+
- Rules a consumer already has — by any pattern, including a wildcard — are
|
|
17
|
+
reported as covered and never duplicated.
|
|
18
|
+
- A marker block that the consumer has since edited is theirs: the helper
|
|
19
|
+
blocks instead of repairing it.
|
|
20
|
+
- Nothing here runs during `init`/`update` reconciliation.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
python3 scripts/worktree-lifecycle/ignore_seed.py preview [--repo PATH] [--json]
|
|
24
|
+
python3 scripts/worktree-lifecycle/ignore_seed.py apply [--repo PATH] [--json]
|
|
25
|
+
|
|
26
|
+
Exit codes: 0 previewed/appended/nothing-to-do · 2 blocked · 1 error.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import json
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
MANIFEST = Path(__file__).resolve().parent / "plan-artifacts.json"
|
|
39
|
+
|
|
40
|
+
BLOCK_START = "# >>> agent-workflow-kit: planning-artifacts/v1 >>>"
|
|
41
|
+
BLOCK_END = "# <<< agent-workflow-kit: planning-artifacts/v1 <<<"
|
|
42
|
+
BLOCK_NOTE = (
|
|
43
|
+
"# Planning artifacts the agent workflow skills write into a session\n"
|
|
44
|
+
"# worktree. They are session scratch, never part of the change. Added by\n"
|
|
45
|
+
"# setup-workflow on request; this block is yours to edit or delete.\n"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class IgnoreSeedError(RuntimeError):
|
|
50
|
+
"""A safe, user-visible refusal."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class IgnorePlan:
|
|
55
|
+
"""What an approval would append — and nothing more."""
|
|
56
|
+
|
|
57
|
+
repo: Path
|
|
58
|
+
gitignore_exists: bool
|
|
59
|
+
already_ignored: tuple[str, ...]
|
|
60
|
+
pending: tuple[str, ...]
|
|
61
|
+
tracked: tuple[str, ...]
|
|
62
|
+
block: str | None
|
|
63
|
+
status: str
|
|
64
|
+
detail: str
|
|
65
|
+
|
|
66
|
+
def as_dict(self) -> dict:
|
|
67
|
+
return {
|
|
68
|
+
"status": self.status,
|
|
69
|
+
"detail": self.detail,
|
|
70
|
+
"gitignoreExists": self.gitignore_exists,
|
|
71
|
+
"alreadyIgnored": list(self.already_ignored),
|
|
72
|
+
"pending": list(self.pending),
|
|
73
|
+
"tracked": list(self.tracked),
|
|
74
|
+
"block": self.block,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_artifacts(manifest: Path = MANIFEST) -> tuple[str, ...]:
|
|
79
|
+
"""Return the kit-declared planning artifacts, in declaration order."""
|
|
80
|
+
try:
|
|
81
|
+
document = json.loads(manifest.read_text(encoding="utf-8"))
|
|
82
|
+
return tuple(entry["path"] for entry in document["artifacts"])
|
|
83
|
+
except (OSError, json.JSONDecodeError, KeyError, TypeError) as error:
|
|
84
|
+
raise IgnoreSeedError(f"cannot read planning-artifact manifest: {error}") from error
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _git(repo: Path, *args: str) -> subprocess.CompletedProcess:
|
|
88
|
+
try:
|
|
89
|
+
return subprocess.run(
|
|
90
|
+
["git", *args], cwd=repo, capture_output=True, text=True,
|
|
91
|
+
)
|
|
92
|
+
except OSError as error: # pragma: no cover - git absent
|
|
93
|
+
raise IgnoreSeedError(f"cannot run git: {error}") from error
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_ignored(repo: Path, path: str) -> bool:
|
|
97
|
+
"""True when any ignore rule already matches `path`.
|
|
98
|
+
|
|
99
|
+
`--no-index` asks the pattern question, not the index question, so an
|
|
100
|
+
artifact that a consumer once committed still reports its real rule state.
|
|
101
|
+
"""
|
|
102
|
+
return _git(repo, "check-ignore", "-q", "--no-index", "--", path).returncode == 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_tracked(repo: Path, path: str) -> bool:
|
|
106
|
+
"""True when the artifact is committed — an ignore rule cannot untrack it."""
|
|
107
|
+
result = _git(repo, "ls-files", "--error-unmatch", "--", path)
|
|
108
|
+
return result.returncode == 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def render_block(paths: tuple[str, ...]) -> str:
|
|
112
|
+
"""The exact text an approval appends."""
|
|
113
|
+
listed = "".join(f"{path}\n" for path in paths)
|
|
114
|
+
return f"{BLOCK_START}\n{BLOCK_NOTE}{listed}{BLOCK_END}\n"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _read_gitignore(repo: Path) -> tuple[bool, str]:
|
|
118
|
+
target = repo / ".gitignore"
|
|
119
|
+
try:
|
|
120
|
+
return True, target.read_text(encoding="utf-8")
|
|
121
|
+
except FileNotFoundError:
|
|
122
|
+
return False, ""
|
|
123
|
+
except OSError as error:
|
|
124
|
+
raise IgnoreSeedError(f"cannot read {target}: {error}") from error
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def plan(repo: Path, manifest: Path = MANIFEST) -> IgnorePlan:
|
|
128
|
+
"""Report what an approval would change. Reads only."""
|
|
129
|
+
repo = Path(repo)
|
|
130
|
+
if not (repo / ".git").exists():
|
|
131
|
+
raise IgnoreSeedError(f"not a git repository: {repo}")
|
|
132
|
+
artifacts = load_artifacts(manifest)
|
|
133
|
+
exists, text = _read_gitignore(repo)
|
|
134
|
+
covered = tuple(path for path in artifacts if is_ignored(repo, path))
|
|
135
|
+
pending = tuple(path for path in artifacts if path not in covered)
|
|
136
|
+
tracked = tuple(path for path in artifacts if is_tracked(repo, path))
|
|
137
|
+
has_block = BLOCK_START in text
|
|
138
|
+
|
|
139
|
+
if not pending:
|
|
140
|
+
return IgnorePlan(
|
|
141
|
+
repo=repo, gitignore_exists=exists, already_ignored=covered,
|
|
142
|
+
pending=(), tracked=tracked, block=None, status="nothing-to-do",
|
|
143
|
+
detail="every planning artifact is already ignored",
|
|
144
|
+
)
|
|
145
|
+
if has_block:
|
|
146
|
+
return IgnorePlan(
|
|
147
|
+
repo=repo, gitignore_exists=exists, already_ignored=covered,
|
|
148
|
+
pending=pending, tracked=tracked, block=None, status="blocked",
|
|
149
|
+
detail=(
|
|
150
|
+
"the marker block exists but no longer covers "
|
|
151
|
+
f"{', '.join(pending)} — that block is consumer-owned; "
|
|
152
|
+
"edit .gitignore yourself"
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
return IgnorePlan(
|
|
156
|
+
repo=repo, gitignore_exists=exists, already_ignored=covered,
|
|
157
|
+
pending=pending, tracked=tracked, block=render_block(pending),
|
|
158
|
+
status="append",
|
|
159
|
+
detail=f"would append {len(pending)} rule(s) in one marker block",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def apply(repo: Path, manifest: Path = MANIFEST) -> IgnorePlan:
|
|
164
|
+
"""Append the previewed block. Never rewrites an existing line."""
|
|
165
|
+
decision = plan(repo, manifest)
|
|
166
|
+
if decision.status != "append":
|
|
167
|
+
return decision
|
|
168
|
+
target = Path(repo) / ".gitignore"
|
|
169
|
+
_, text = _read_gitignore(Path(repo))
|
|
170
|
+
prefix = text
|
|
171
|
+
if prefix and not prefix.endswith("\n"):
|
|
172
|
+
prefix += "\n"
|
|
173
|
+
if prefix and not prefix.endswith("\n\n"):
|
|
174
|
+
prefix += "\n"
|
|
175
|
+
try:
|
|
176
|
+
target.write_text(prefix + decision.block, encoding="utf-8")
|
|
177
|
+
except OSError as error:
|
|
178
|
+
raise IgnoreSeedError(f"cannot write {target}: {error}") from error
|
|
179
|
+
return IgnorePlan(
|
|
180
|
+
repo=decision.repo, gitignore_exists=True,
|
|
181
|
+
already_ignored=decision.already_ignored, pending=decision.pending,
|
|
182
|
+
tracked=decision.tracked, block=decision.block, status="appended",
|
|
183
|
+
detail=f"appended {len(decision.pending)} rule(s) to .gitignore",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _report(decision: IgnorePlan, *, as_json: bool) -> None:
|
|
188
|
+
if as_json:
|
|
189
|
+
print(json.dumps(decision.as_dict(), indent=2))
|
|
190
|
+
return
|
|
191
|
+
print(f"{decision.status} — {decision.detail}")
|
|
192
|
+
if decision.already_ignored:
|
|
193
|
+
print(f" already ignored: {', '.join(decision.already_ignored)}")
|
|
194
|
+
if decision.tracked:
|
|
195
|
+
print(
|
|
196
|
+
" tracked in git (an ignore rule cannot untrack these): "
|
|
197
|
+
f"{', '.join(decision.tracked)}"
|
|
198
|
+
)
|
|
199
|
+
if decision.block:
|
|
200
|
+
label = "appended" if decision.status == "appended" else "would append"
|
|
201
|
+
print(f" {label}:")
|
|
202
|
+
for line in decision.block.rstrip("\n").split("\n"):
|
|
203
|
+
print(f" {line}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
207
|
+
parser = argparse.ArgumentParser(prog="ignore_seed.py")
|
|
208
|
+
parser.add_argument("command", choices=("preview", "apply"))
|
|
209
|
+
parser.add_argument("--repo", default=".")
|
|
210
|
+
parser.add_argument("--json", action="store_true")
|
|
211
|
+
return parser.parse_args(argv)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main(argv: list[str]) -> int:
|
|
215
|
+
args = parse_args(argv)
|
|
216
|
+
try:
|
|
217
|
+
decision = plan(args.repo) if args.command == "preview" else apply(args.repo)
|
|
218
|
+
except IgnoreSeedError as error:
|
|
219
|
+
print(str(error), file=sys.stderr)
|
|
220
|
+
return 1
|
|
221
|
+
_report(decision, as_json=args.json)
|
|
222
|
+
return 2 if decision.status == "blocked" else 0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"artifacts": [
|
|
4
|
+
{
|
|
5
|
+
"path": "PLAN.md",
|
|
6
|
+
"writtenBy": [
|
|
7
|
+
"grill-me",
|
|
8
|
+
"grill-with-docs",
|
|
9
|
+
"codex-review",
|
|
10
|
+
"grill-me-codex",
|
|
11
|
+
"grill-with-docs-codex",
|
|
12
|
+
"codex-build",
|
|
13
|
+
"orchestrate-wave"
|
|
14
|
+
],
|
|
15
|
+
"why": "the locked plan a planning session hands to the implementing session"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "PLAN-REVIEW-LOG.md",
|
|
19
|
+
"writtenBy": [
|
|
20
|
+
"codex-review",
|
|
21
|
+
"grill-me-codex",
|
|
22
|
+
"grill-with-docs-codex",
|
|
23
|
+
"codex-build"
|
|
24
|
+
],
|
|
25
|
+
"why": "the append-only cross-model review transcript of that plan"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "ANNAHMEN.md",
|
|
29
|
+
"writtenBy": [
|
|
30
|
+
"tdd",
|
|
31
|
+
"orchestrate-wave",
|
|
32
|
+
"wrapup"
|
|
33
|
+
],
|
|
34
|
+
"why": "the build-time assumption-drift log wrapup propagates before teardown"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
package/src/lib/bundle.mjs
CHANGED
|
@@ -147,6 +147,12 @@ export const HELPER_FILES = [
|
|
|
147
147
|
{ path: 'scripts/worktree-lifecycle/session.py', kind: 'script', mode: 0o755 },
|
|
148
148
|
{ path: 'scripts/worktree-lifecycle/capabilities.json', kind: 'doc', mode: 0o644 },
|
|
149
149
|
{ path: 'scripts/worktree-lifecycle/README.md', kind: 'doc', mode: 0o644 },
|
|
150
|
+
// Offered planning-artifact ignore rules for /setup-workflow Section A11.
|
|
151
|
+
// `.gitignore` is consumer-owned, so only an approved setup step may append
|
|
152
|
+
// the block — init/update reconciliation never reach this. plan-artifacts.json
|
|
153
|
+
// is the kit-owned declaration it reads. Invokable CLI → 0o755.
|
|
154
|
+
{ path: 'scripts/worktree-lifecycle/plan-artifacts.json', kind: 'doc', mode: 0o644 },
|
|
155
|
+
{ path: 'scripts/worktree-lifecycle/ignore_seed.py', kind: 'script', mode: 0o755 },
|
|
150
156
|
// Shared hook utility imported by the shipped hooks (drift-guard,
|
|
151
157
|
// sync-board-status). Library (imported, not run) → 0o644. MUST ship or those
|
|
152
158
|
// hooks ImportError on arrival.
|