@ikon85/agent-workflow-kit 0.21.0 → 0.23.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/setup-workflow/SKILL.md +75 -0
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +81 -0
- package/.claude/hooks/slice-handoff-hint.py +25 -0
- package/.claude/skills/setup-workflow/SKILL.md +75 -0
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +81 -0
- package/README.md +22 -0
- package/agent-workflow-kit.package.json +61 -8
- package/assets/memory-templates/meta_decision_layer_choice.md +25 -0
- package/assets/memory-templates/meta_memory_lifecycle.md +25 -0
- package/package.json +2 -1
- package/scripts/memory-lifecycle/index.mjs +2 -0
- package/scripts/memory-lifecycle/setup.mjs +167 -0
- package/scripts/test_worktree_wrapup_contract.py +52 -0
- package/scripts/worktree-lifecycle/README.md +8 -0
- package/scripts/worktree-lifecycle/capabilities.json +8 -0
- package/scripts/worktree-lifecycle/cleanup.py +57 -0
- package/scripts/worktree-lifecycle/core.py +80 -0
- package/scripts/wrapup-land.py +45 -0
- package/src/lib/bundle.mjs +6 -2
|
@@ -26,6 +26,7 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
|
|
|
26
26
|
| `## Agent skills` + `## Prod` in CLAUDE.md **and** AGENTS.md | one-line pointers + deploy target (Sections C/G + Write) |
|
|
27
27
|
| `.github/workflows/agent-workflow-kit-update.yml` | optional tested Kit update pull request for GitHub consumers (Section A2) |
|
|
28
28
|
| `docs/agents/census.md` | optional-census choice, paths, state, and safe disable contract seeded from [census.md](./census.md) (Section A3) |
|
|
29
|
+
| `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle activation and one-time Memory Lifecycle policy seeds (Sections A4–A5) |
|
|
29
30
|
|
|
30
31
|
## Idempotency contract — read before writing anything
|
|
31
32
|
|
|
@@ -56,6 +57,8 @@ Read the current state; don't assume. For every target file, read its first line
|
|
|
56
57
|
- `CONTEXT.md`, `CONTEXT-MAP.md`, `docs/adr/` — domain-doc layout.
|
|
57
58
|
- `docs/agents/`, `docs/agents/skills/`, `docs/conventions/` — prior output of this skill.
|
|
58
59
|
- `docs/agents/census.md`, `.census/profile.json`, `.census/active.json` — an existing census choice or consumer-owned census to adopt.
|
|
60
|
+
- `docs/agents/workflow-capabilities.json`, `.claude/settings.json` — existing capability choices/profile and Worktree Lifecycle hook wiring to adopt.
|
|
61
|
+
- `.memory/active/`, `.memory/archive/`, and `.memory/receipts/` — consumer-owned Memory Lifecycle policies or recovery evidence to adopt without normalization.
|
|
59
62
|
- `gh auth status` (if GitHub) — is `gh` authenticated, and with which scopes?
|
|
60
63
|
|
|
61
64
|
### 2. Section A — Issue tracker
|
|
@@ -135,6 +138,67 @@ activate without rerunning setup. Disable enforcement first, but retain every
|
|
|
135
138
|
consumer-owned profile, scanner, test, and active snapshot unless the user
|
|
136
139
|
separately approves deletion. Repeated runs are no-ops after reconciliation.
|
|
137
140
|
|
|
141
|
+
### 2c. Section A4 — Optional Worktree Lifecycle
|
|
142
|
+
|
|
143
|
+
> Worktree Lifecycle is one consumer capability for isolated setup, shared
|
|
144
|
+
> branch/worktree facts, enforcement, handoff, and safe cleanup. The profile is
|
|
145
|
+
> tracked and consumer-owned; hook wiring activates only after an explicit yes.
|
|
146
|
+
|
|
147
|
+
Read [worktree-lifecycle.md](./worktree-lifecycle.md) in full. Reconcile the
|
|
148
|
+
complete `yes / later / no / existing / disable` matrix from its structured
|
|
149
|
+
contract. If no choice exists, ask: *"Should setup activate the portable
|
|
150
|
+
Worktree Lifecycle for this repository?"* Offer exactly **Yes**, **Later**, and
|
|
151
|
+
**No**.
|
|
152
|
+
|
|
153
|
+
- **Yes** — create or deepen only the `worktreeLifecycle` section in
|
|
154
|
+
`docs/agents/workflow-capabilities.json`, preserve every other section and
|
|
155
|
+
unknown key, and reconcile only the exact kit-owned hook commands listed in
|
|
156
|
+
the seed.
|
|
157
|
+
- **Later / No** — record the choice in the tracked profile without installing
|
|
158
|
+
hook wiring. Ordinary reruns do not ask again.
|
|
159
|
+
- **Existing** — adopt the current section and wiring without normalizing
|
|
160
|
+
consumer-owned values.
|
|
161
|
+
- **Disable** — remove only the exact kit-owned hook commands first, then set
|
|
162
|
+
`enabled: false`; retain the profile, setup policy, and unknown keys.
|
|
163
|
+
|
|
164
|
+
The default setup entry is
|
|
165
|
+
`python3 scripts/worktree-lifecycle/setup.py`; a proven consumer-native helper
|
|
166
|
+
may remain the configured `setupEntry` for parity. The handoff advisory always
|
|
167
|
+
reads that configured entry and never hardcodes a project script.
|
|
168
|
+
|
|
169
|
+
### 2d. Section A5 — Optional Memory Lifecycle
|
|
170
|
+
|
|
171
|
+
> Memory Lifecycle keeps project memories inside consumer-owned roots and makes
|
|
172
|
+
> restore operations previewable, collision-safe, and reversible. The policy
|
|
173
|
+
> templates become fully consumer-owned after their first seed.
|
|
174
|
+
|
|
175
|
+
Read the `memoryLifecycle` section of
|
|
176
|
+
`docs/agents/workflow-capabilities.json` before offering anything. Missing or
|
|
177
|
+
disabled configuration remains unchanged on an ordinary setup rerun. An
|
|
178
|
+
existing enabled section is adopted byte-for-byte; preserve unknown profile
|
|
179
|
+
keys, placement/retention settings, restore grants, and consumer-specific memory
|
|
180
|
+
rows.
|
|
181
|
+
If no section exists, explain the capability and offer an explicit opt-in. Only
|
|
182
|
+
an affirmative consumer decision may run:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
node scripts/memory-lifecycle/setup.mjs --enable
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
That command adds the minimal enabled section while preserving every existing
|
|
189
|
+
top-level profile key, sets restore and pruning approval to `false` unless the
|
|
190
|
+
consumer already recorded values, and seeds
|
|
191
|
+
`meta_decision_layer_choice.md` plus `meta_memory_lifecycle.md` into the
|
|
192
|
+
configured active root exactly once. Existing files are adopted and never
|
|
193
|
+
overwritten. The profile's `templatesSeeded` marker transfers full ownership:
|
|
194
|
+
later setup runs do not recreate a template the consumer edited, moved, or
|
|
195
|
+
deleted.
|
|
196
|
+
Choosing later, declining, or merely running `init`, `update`, or ordinary
|
|
197
|
+
`setup-workflow` performs no Memory Lifecycle write and never changes
|
|
198
|
+
activation, roots, retention, restore, or pruning grants. Template files have
|
|
199
|
+
no kit-managed or `append-managed` region; a future bounded-region contract
|
|
200
|
+
requires a separate decision.
|
|
201
|
+
|
|
138
202
|
### 3. Section B — Triage labels
|
|
139
203
|
|
|
140
204
|
> When `triage` processes an incoming issue it applies labels (or your tracker's equivalent). It needs strings you've actually configured, or it creates duplicates.
|
|
@@ -220,6 +284,17 @@ pre-existing consumer-owned census file. `yes`, `later`, `no`, and an adopted
|
|
|
220
284
|
existing census are terminal for ordinary setup reruns. Only an explicit user
|
|
221
285
|
request changes a recorded choice.
|
|
222
286
|
|
|
287
|
+
For `docs/agents/workflow-capabilities.json`, reconcile only the selected
|
|
288
|
+
capability section. Never replace the whole file: Project Release, Memory
|
|
289
|
+
Lifecycle, Worktree Lifecycle, future sections, and unknown consumer keys share
|
|
290
|
+
this profile. Apply the Worktree Lifecycle transition and hook ownership rules
|
|
291
|
+
from [worktree-lifecycle.md](./worktree-lifecycle.md) transactionally.
|
|
292
|
+
|
|
293
|
+
For Memory Lifecycle, use only the deterministic setup helper from Section A5.
|
|
294
|
+
Do not copy templates with shell commands or edit the capability profile by
|
|
295
|
+
hand. Report `seeded · adopted · skipped`; do not print memory contents or
|
|
296
|
+
receipt contents.
|
|
297
|
+
|
|
223
298
|
For the **`## Workflow`**, **`## Agent skills`**, and **`## Prod`** blocks, reconcile per section in **both** CLAUDE.md and AGENTS.md that exist:
|
|
224
299
|
|
|
225
300
|
- If **both** files exist → write/update the block in **both** (keep them coherent — Codex is a first-class surface).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Worktree Lifecycle setup contract
|
|
2
|
+
|
|
3
|
+
Worktree Lifecycle is one opt-in capability backed by the consumer-owned
|
|
4
|
+
`docs/agents/workflow-capabilities.json` profile. Enabling it activates the
|
|
5
|
+
portable setup entry, shared branch/worktree facts, thin hook adapters, handoff
|
|
6
|
+
advisory, and safe cleanup policy as one unit.
|
|
7
|
+
|
|
8
|
+
## Choice matrix
|
|
9
|
+
|
|
10
|
+
| State | Setup action |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `missing` | Ask `yes / later / no`; do not infer or write before the answer. |
|
|
13
|
+
| `yes` | Reconcile the enabled profile and exact kit-owned hook commands. |
|
|
14
|
+
| `later` | Record the retryable deferral; do not activate hooks. |
|
|
15
|
+
| `no` | Record the opt-out; do not activate hooks. |
|
|
16
|
+
| `existing` | Adopt the consumer profile byte-safely and preserve unknown keys. |
|
|
17
|
+
| `disable` | Remove only kit-owned hook wiring, then set `enabled: false`; retain the profile and unknown keys. |
|
|
18
|
+
|
|
19
|
+
```json worktree-lifecycle-setup-effects
|
|
20
|
+
[
|
|
21
|
+
{
|
|
22
|
+
"state": "missing",
|
|
23
|
+
"choice": null,
|
|
24
|
+
"operations": []
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"state": "yes",
|
|
28
|
+
"choice": "yes",
|
|
29
|
+
"operations": [
|
|
30
|
+
"record-choice",
|
|
31
|
+
"reconcile-profile-enabled",
|
|
32
|
+
"reconcile-hook-wiring"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"state": "later",
|
|
37
|
+
"choice": "later",
|
|
38
|
+
"operations": [
|
|
39
|
+
"record-choice"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"state": "no",
|
|
44
|
+
"choice": "no",
|
|
45
|
+
"operations": [
|
|
46
|
+
"record-choice"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"state": "existing",
|
|
51
|
+
"choice": "yes",
|
|
52
|
+
"operations": [
|
|
53
|
+
"adopt-existing"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"state": "disable",
|
|
58
|
+
"choice": "yes",
|
|
59
|
+
"operations": [
|
|
60
|
+
"remove-hook-wiring",
|
|
61
|
+
"update-profile-disabled"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Hook ownership
|
|
68
|
+
|
|
69
|
+
The exact kit-owned commands are:
|
|
70
|
+
|
|
71
|
+
- SessionStart: `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/branch-context.py"`
|
|
72
|
+
- UserPromptSubmit: `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/slice-handoff-hint.py"`
|
|
73
|
+
- PostToolUse on Bash: `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/branch-watch.py"`
|
|
74
|
+
- PreToolUse on Edit/Write/MultiEdit:
|
|
75
|
+
`python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-worktree.py"`
|
|
76
|
+
- PreToolUse on Bash:
|
|
77
|
+
`python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-worktree-cwd.py"` and
|
|
78
|
+
`python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-worktree-discipline.py"`
|
|
79
|
+
|
|
80
|
+
Preserve unrelated settings, hook groups, profile sections, and unknown keys.
|
|
81
|
+
Repeated reconciliation with the same choice is byte-identical.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""UserPromptSubmit adapter for configured Worktree Lifecycle handoffs."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from _hook_utils import hook_event_output, load_worktree_lifecycle_core
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> int:
|
|
12
|
+
try:
|
|
13
|
+
payload = json.load(sys.stdin)
|
|
14
|
+
core = load_worktree_lifecycle_core()
|
|
15
|
+
profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
|
|
16
|
+
decision = core.evaluate(profile, core.collect_facts(Path.cwd()), "handoff", payload)
|
|
17
|
+
except Exception:
|
|
18
|
+
return 0
|
|
19
|
+
if decision.action == "emit":
|
|
20
|
+
print(json.dumps(hook_event_output(decision.event_name, decision.message)))
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
raise SystemExit(main())
|
|
@@ -26,6 +26,7 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
|
|
|
26
26
|
| `## Agent skills` + `## Prod` in CLAUDE.md **and** AGENTS.md | one-line pointers + deploy target (Sections C/G + Write) |
|
|
27
27
|
| `.github/workflows/agent-workflow-kit-update.yml` | optional tested Kit update pull request for GitHub consumers (Section A2) |
|
|
28
28
|
| `docs/agents/census.md` | optional-census choice, paths, state, and safe disable contract seeded from [census.md](./census.md) (Section A3) |
|
|
29
|
+
| `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle activation and one-time Memory Lifecycle policy seeds (Sections A4–A5) |
|
|
29
30
|
|
|
30
31
|
## Idempotency contract — read before writing anything
|
|
31
32
|
|
|
@@ -56,6 +57,8 @@ Read the current state; don't assume. For every target file, read its first line
|
|
|
56
57
|
- `CONTEXT.md`, `CONTEXT-MAP.md`, `docs/adr/` — domain-doc layout.
|
|
57
58
|
- `docs/agents/`, `docs/agents/skills/`, `docs/conventions/` — prior output of this skill.
|
|
58
59
|
- `docs/agents/census.md`, `.census/profile.json`, `.census/active.json` — an existing census choice or consumer-owned census to adopt.
|
|
60
|
+
- `docs/agents/workflow-capabilities.json`, `.claude/settings.json` — existing capability choices/profile and Worktree Lifecycle hook wiring to adopt.
|
|
61
|
+
- `.memory/active/`, `.memory/archive/`, and `.memory/receipts/` — consumer-owned Memory Lifecycle policies or recovery evidence to adopt without normalization.
|
|
59
62
|
- `gh auth status` (if GitHub) — is `gh` authenticated, and with which scopes?
|
|
60
63
|
|
|
61
64
|
### 2. Section A — Issue tracker
|
|
@@ -135,6 +138,67 @@ activate without rerunning setup. Disable enforcement first, but retain every
|
|
|
135
138
|
consumer-owned profile, scanner, test, and active snapshot unless the user
|
|
136
139
|
separately approves deletion. Repeated runs are no-ops after reconciliation.
|
|
137
140
|
|
|
141
|
+
### 2c. Section A4 — Optional Worktree Lifecycle
|
|
142
|
+
|
|
143
|
+
> Worktree Lifecycle is one consumer capability for isolated setup, shared
|
|
144
|
+
> branch/worktree facts, enforcement, handoff, and safe cleanup. The profile is
|
|
145
|
+
> tracked and consumer-owned; hook wiring activates only after an explicit yes.
|
|
146
|
+
|
|
147
|
+
Read [worktree-lifecycle.md](./worktree-lifecycle.md) in full. Reconcile the
|
|
148
|
+
complete `yes / later / no / existing / disable` matrix from its structured
|
|
149
|
+
contract. If no choice exists, ask: *"Should setup activate the portable
|
|
150
|
+
Worktree Lifecycle for this repository?"* Offer exactly **Yes**, **Later**, and
|
|
151
|
+
**No**.
|
|
152
|
+
|
|
153
|
+
- **Yes** — create or deepen only the `worktreeLifecycle` section in
|
|
154
|
+
`docs/agents/workflow-capabilities.json`, preserve every other section and
|
|
155
|
+
unknown key, and reconcile only the exact kit-owned hook commands listed in
|
|
156
|
+
the seed.
|
|
157
|
+
- **Later / No** — record the choice in the tracked profile without installing
|
|
158
|
+
hook wiring. Ordinary reruns do not ask again.
|
|
159
|
+
- **Existing** — adopt the current section and wiring without normalizing
|
|
160
|
+
consumer-owned values.
|
|
161
|
+
- **Disable** — remove only the exact kit-owned hook commands first, then set
|
|
162
|
+
`enabled: false`; retain the profile, setup policy, and unknown keys.
|
|
163
|
+
|
|
164
|
+
The default setup entry is
|
|
165
|
+
`python3 scripts/worktree-lifecycle/setup.py`; a proven consumer-native helper
|
|
166
|
+
may remain the configured `setupEntry` for parity. The handoff advisory always
|
|
167
|
+
reads that configured entry and never hardcodes a project script.
|
|
168
|
+
|
|
169
|
+
### 2d. Section A5 — Optional Memory Lifecycle
|
|
170
|
+
|
|
171
|
+
> Memory Lifecycle keeps project memories inside consumer-owned roots and makes
|
|
172
|
+
> restore operations previewable, collision-safe, and reversible. The policy
|
|
173
|
+
> templates become fully consumer-owned after their first seed.
|
|
174
|
+
|
|
175
|
+
Read the `memoryLifecycle` section of
|
|
176
|
+
`docs/agents/workflow-capabilities.json` before offering anything. Missing or
|
|
177
|
+
disabled configuration remains unchanged on an ordinary setup rerun. An
|
|
178
|
+
existing enabled section is adopted byte-for-byte; preserve unknown profile
|
|
179
|
+
keys, placement/retention settings, restore grants, and consumer-specific memory
|
|
180
|
+
rows.
|
|
181
|
+
If no section exists, explain the capability and offer an explicit opt-in. Only
|
|
182
|
+
an affirmative consumer decision may run:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
node scripts/memory-lifecycle/setup.mjs --enable
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
That command adds the minimal enabled section while preserving every existing
|
|
189
|
+
top-level profile key, sets restore and pruning approval to `false` unless the
|
|
190
|
+
consumer already recorded values, and seeds
|
|
191
|
+
`meta_decision_layer_choice.md` plus `meta_memory_lifecycle.md` into the
|
|
192
|
+
configured active root exactly once. Existing files are adopted and never
|
|
193
|
+
overwritten. The profile's `templatesSeeded` marker transfers full ownership:
|
|
194
|
+
later setup runs do not recreate a template the consumer edited, moved, or
|
|
195
|
+
deleted.
|
|
196
|
+
Choosing later, declining, or merely running `init`, `update`, or ordinary
|
|
197
|
+
`setup-workflow` performs no Memory Lifecycle write and never changes
|
|
198
|
+
activation, roots, retention, restore, or pruning grants. Template files have
|
|
199
|
+
no kit-managed or `append-managed` region; a future bounded-region contract
|
|
200
|
+
requires a separate decision.
|
|
201
|
+
|
|
138
202
|
### 3. Section B — Triage labels
|
|
139
203
|
|
|
140
204
|
> When `triage` processes an incoming issue it applies labels (or your tracker's equivalent). It needs strings you've actually configured, or it creates duplicates.
|
|
@@ -220,6 +284,17 @@ pre-existing consumer-owned census file. `yes`, `later`, `no`, and an adopted
|
|
|
220
284
|
existing census are terminal for ordinary setup reruns. Only an explicit user
|
|
221
285
|
request changes a recorded choice.
|
|
222
286
|
|
|
287
|
+
For `docs/agents/workflow-capabilities.json`, reconcile only the selected
|
|
288
|
+
capability section. Never replace the whole file: Project Release, Memory
|
|
289
|
+
Lifecycle, Worktree Lifecycle, future sections, and unknown consumer keys share
|
|
290
|
+
this profile. Apply the Worktree Lifecycle transition and hook ownership rules
|
|
291
|
+
from [worktree-lifecycle.md](./worktree-lifecycle.md) transactionally.
|
|
292
|
+
|
|
293
|
+
For Memory Lifecycle, use only the deterministic setup helper from Section A5.
|
|
294
|
+
Do not copy templates with shell commands or edit the capability profile by
|
|
295
|
+
hand. Report `seeded · adopted · skipped`; do not print memory contents or
|
|
296
|
+
receipt contents.
|
|
297
|
+
|
|
223
298
|
For the **`## Workflow`**, **`## Agent skills`**, and **`## Prod`** blocks, reconcile per section in **both** CLAUDE.md and AGENTS.md that exist:
|
|
224
299
|
|
|
225
300
|
- If **both** files exist → write/update the block in **both** (keep them coherent — Codex is a first-class surface).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Worktree Lifecycle setup contract
|
|
2
|
+
|
|
3
|
+
Worktree Lifecycle is one opt-in capability backed by the consumer-owned
|
|
4
|
+
`docs/agents/workflow-capabilities.json` profile. Enabling it activates the
|
|
5
|
+
portable setup entry, shared branch/worktree facts, thin hook adapters, handoff
|
|
6
|
+
advisory, and safe cleanup policy as one unit.
|
|
7
|
+
|
|
8
|
+
## Choice matrix
|
|
9
|
+
|
|
10
|
+
| State | Setup action |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `missing` | Ask `yes / later / no`; do not infer or write before the answer. |
|
|
13
|
+
| `yes` | Reconcile the enabled profile and exact kit-owned hook commands. |
|
|
14
|
+
| `later` | Record the retryable deferral; do not activate hooks. |
|
|
15
|
+
| `no` | Record the opt-out; do not activate hooks. |
|
|
16
|
+
| `existing` | Adopt the consumer profile byte-safely and preserve unknown keys. |
|
|
17
|
+
| `disable` | Remove only kit-owned hook wiring, then set `enabled: false`; retain the profile and unknown keys. |
|
|
18
|
+
|
|
19
|
+
```json worktree-lifecycle-setup-effects
|
|
20
|
+
[
|
|
21
|
+
{
|
|
22
|
+
"state": "missing",
|
|
23
|
+
"choice": null,
|
|
24
|
+
"operations": []
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"state": "yes",
|
|
28
|
+
"choice": "yes",
|
|
29
|
+
"operations": [
|
|
30
|
+
"record-choice",
|
|
31
|
+
"reconcile-profile-enabled",
|
|
32
|
+
"reconcile-hook-wiring"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"state": "later",
|
|
37
|
+
"choice": "later",
|
|
38
|
+
"operations": [
|
|
39
|
+
"record-choice"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"state": "no",
|
|
44
|
+
"choice": "no",
|
|
45
|
+
"operations": [
|
|
46
|
+
"record-choice"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"state": "existing",
|
|
51
|
+
"choice": "yes",
|
|
52
|
+
"operations": [
|
|
53
|
+
"adopt-existing"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"state": "disable",
|
|
58
|
+
"choice": "yes",
|
|
59
|
+
"operations": [
|
|
60
|
+
"remove-hook-wiring",
|
|
61
|
+
"update-profile-disabled"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Hook ownership
|
|
68
|
+
|
|
69
|
+
The exact kit-owned commands are:
|
|
70
|
+
|
|
71
|
+
- SessionStart: `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/branch-context.py"`
|
|
72
|
+
- UserPromptSubmit: `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/slice-handoff-hint.py"`
|
|
73
|
+
- PostToolUse on Bash: `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/branch-watch.py"`
|
|
74
|
+
- PreToolUse on Edit/Write/MultiEdit:
|
|
75
|
+
`python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-worktree.py"`
|
|
76
|
+
- PreToolUse on Bash:
|
|
77
|
+
`python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-worktree-cwd.py"` and
|
|
78
|
+
`python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-worktree-discipline.py"`
|
|
79
|
+
|
|
80
|
+
Preserve unrelated settings, hook groups, profile sections, and unknown keys.
|
|
81
|
+
Repeated reconciliation with the same choice is byte-identical.
|
package/README.md
CHANGED
|
@@ -332,6 +332,28 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
|
|
|
332
332
|
|
|
333
333
|
## Release notes
|
|
334
334
|
|
|
335
|
+
### 0.23.0
|
|
336
|
+
|
|
337
|
+
- added: `assets/memory-templates/meta_decision_layer_choice.md`
|
|
338
|
+
- added: `assets/memory-templates/meta_memory_lifecycle.md`
|
|
339
|
+
- added: `scripts/memory-lifecycle/setup.mjs`
|
|
340
|
+
- changed: `.agents/skills/setup-workflow/SKILL.md`
|
|
341
|
+
- changed: `.claude/skills/setup-workflow/SKILL.md`
|
|
342
|
+
- changed: `scripts/memory-lifecycle/index.mjs`
|
|
343
|
+
|
|
344
|
+
### 0.22.0
|
|
345
|
+
|
|
346
|
+
- added: `.agents/skills/setup-workflow/worktree-lifecycle.md`
|
|
347
|
+
- added: `.claude/hooks/slice-handoff-hint.py`
|
|
348
|
+
- added: `.claude/skills/setup-workflow/worktree-lifecycle.md`
|
|
349
|
+
- added: `scripts/worktree-lifecycle/cleanup.py`
|
|
350
|
+
- changed: `.agents/skills/setup-workflow/SKILL.md`
|
|
351
|
+
- changed: `.claude/skills/setup-workflow/SKILL.md`
|
|
352
|
+
- changed: `scripts/worktree-lifecycle/README.md`
|
|
353
|
+
- changed: `scripts/worktree-lifecycle/capabilities.json`
|
|
354
|
+
- changed: `scripts/worktree-lifecycle/core.py`
|
|
355
|
+
- changed: `scripts/wrapup-land.py`
|
|
356
|
+
|
|
335
357
|
### 0.21.0
|
|
336
358
|
|
|
337
359
|
- added: `.agents/skills/project-release/SKILL.md`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kitVersion": "0.
|
|
2
|
+
"kitVersion": "0.23.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": ".agents/skills/ask-matt/SKILL.md",
|
|
@@ -573,7 +573,7 @@
|
|
|
573
573
|
"kind": "skill",
|
|
574
574
|
"ownerSkill": "setup-workflow",
|
|
575
575
|
"surface": "codex",
|
|
576
|
-
"sha256": "
|
|
576
|
+
"sha256": "247250aa38a81b542600db42cfe4d02eecc1236e8af47df1f4def627f4df0668",
|
|
577
577
|
"mode": 420,
|
|
578
578
|
"origin": "kit"
|
|
579
579
|
},
|
|
@@ -622,6 +622,15 @@
|
|
|
622
622
|
"mode": 420,
|
|
623
623
|
"origin": "kit"
|
|
624
624
|
},
|
|
625
|
+
{
|
|
626
|
+
"path": ".agents/skills/setup-workflow/worktree-lifecycle.md",
|
|
627
|
+
"kind": "skill",
|
|
628
|
+
"ownerSkill": "setup-workflow",
|
|
629
|
+
"surface": "codex",
|
|
630
|
+
"sha256": "5e0914874f0b9f940205fbcfcfbba87d0c2df8f31db104d2c5328a0d20c80616",
|
|
631
|
+
"mode": 420,
|
|
632
|
+
"origin": "kit"
|
|
633
|
+
},
|
|
625
634
|
{
|
|
626
635
|
"path": ".agents/skills/spec-self-critique/scenarios.md",
|
|
627
636
|
"kind": "skill",
|
|
@@ -921,6 +930,13 @@
|
|
|
921
930
|
"mode": 493,
|
|
922
931
|
"origin": "kit"
|
|
923
932
|
},
|
|
933
|
+
{
|
|
934
|
+
"path": ".claude/hooks/slice-handoff-hint.py",
|
|
935
|
+
"kind": "hook",
|
|
936
|
+
"sha256": "664ca09a7e86c267ce7d49bf6b2529ebb9e75a4fd3443548f36f6eb9d4665abf",
|
|
937
|
+
"mode": 493,
|
|
938
|
+
"origin": "kit"
|
|
939
|
+
},
|
|
924
940
|
{
|
|
925
941
|
"path": ".claude/hooks/sync-board-status.py",
|
|
926
942
|
"kind": "hook",
|
|
@@ -1626,7 +1642,7 @@
|
|
|
1626
1642
|
"kind": "skill",
|
|
1627
1643
|
"ownerSkill": "setup-workflow",
|
|
1628
1644
|
"surface": "claude",
|
|
1629
|
-
"sha256": "
|
|
1645
|
+
"sha256": "247250aa38a81b542600db42cfe4d02eecc1236e8af47df1f4def627f4df0668",
|
|
1630
1646
|
"mode": 420,
|
|
1631
1647
|
"origin": "kit"
|
|
1632
1648
|
},
|
|
@@ -1675,6 +1691,15 @@
|
|
|
1675
1691
|
"mode": 420,
|
|
1676
1692
|
"origin": "kit"
|
|
1677
1693
|
},
|
|
1694
|
+
{
|
|
1695
|
+
"path": ".claude/skills/setup-workflow/worktree-lifecycle.md",
|
|
1696
|
+
"kind": "skill",
|
|
1697
|
+
"ownerSkill": "setup-workflow",
|
|
1698
|
+
"surface": "claude",
|
|
1699
|
+
"sha256": "5e0914874f0b9f940205fbcfcfbba87d0c2df8f31db104d2c5328a0d20c80616",
|
|
1700
|
+
"mode": 420,
|
|
1701
|
+
"origin": "kit"
|
|
1702
|
+
},
|
|
1678
1703
|
{
|
|
1679
1704
|
"path": ".claude/skills/spec-self-critique/scenarios.md",
|
|
1680
1705
|
"kind": "skill",
|
|
@@ -1945,6 +1970,20 @@
|
|
|
1945
1970
|
"mode": 420,
|
|
1946
1971
|
"origin": "kit"
|
|
1947
1972
|
},
|
|
1973
|
+
{
|
|
1974
|
+
"path": "assets/memory-templates/meta_decision_layer_choice.md",
|
|
1975
|
+
"kind": "template",
|
|
1976
|
+
"sha256": "b22bd1d0e3daa31cbdbb9a3a81964d7a00d5275e69c92c9bd4ad3b4e40995b41",
|
|
1977
|
+
"mode": 420,
|
|
1978
|
+
"origin": "kit"
|
|
1979
|
+
},
|
|
1980
|
+
{
|
|
1981
|
+
"path": "assets/memory-templates/meta_memory_lifecycle.md",
|
|
1982
|
+
"kind": "template",
|
|
1983
|
+
"sha256": "215e865d4e803ce0b2cd43e88d76501a61269dfa90aae5e6a474941a0b72b0ca",
|
|
1984
|
+
"mode": 420,
|
|
1985
|
+
"origin": "kit"
|
|
1986
|
+
},
|
|
1948
1987
|
{
|
|
1949
1988
|
"path": "docs/agents/security-audit-runbook-template.md",
|
|
1950
1989
|
"kind": "template",
|
|
@@ -2074,7 +2113,14 @@
|
|
|
2074
2113
|
{
|
|
2075
2114
|
"path": "scripts/memory-lifecycle/index.mjs",
|
|
2076
2115
|
"kind": "script",
|
|
2077
|
-
"sha256": "
|
|
2116
|
+
"sha256": "871507e20133ccbb11f2a57f092bcb41aa1f6b7deaaa74df2418573316e6bd96",
|
|
2117
|
+
"mode": 420,
|
|
2118
|
+
"origin": "kit"
|
|
2119
|
+
},
|
|
2120
|
+
{
|
|
2121
|
+
"path": "scripts/memory-lifecycle/setup.mjs",
|
|
2122
|
+
"kind": "script",
|
|
2123
|
+
"sha256": "c2c8885b806a830239ae54314c9dd0400399140870b6e62dc1e8b043dffcb78f",
|
|
2078
2124
|
"mode": 420,
|
|
2079
2125
|
"origin": "kit"
|
|
2080
2126
|
},
|
|
@@ -2151,14 +2197,21 @@
|
|
|
2151
2197
|
{
|
|
2152
2198
|
"path": "scripts/worktree-lifecycle/capabilities.json",
|
|
2153
2199
|
"kind": "doc",
|
|
2154
|
-
"sha256": "
|
|
2200
|
+
"sha256": "e80501bd86bbdb0ba296fc6b172442d8ad128f43e571777698657491279e4305",
|
|
2155
2201
|
"mode": 420,
|
|
2156
2202
|
"origin": "kit"
|
|
2157
2203
|
},
|
|
2204
|
+
{
|
|
2205
|
+
"path": "scripts/worktree-lifecycle/cleanup.py",
|
|
2206
|
+
"kind": "script",
|
|
2207
|
+
"sha256": "370af8840732db3b6ba39b4825686f3980f69f612038123bc86b2c68921f02ca",
|
|
2208
|
+
"mode": 493,
|
|
2209
|
+
"origin": "kit"
|
|
2210
|
+
},
|
|
2158
2211
|
{
|
|
2159
2212
|
"path": "scripts/worktree-lifecycle/core.py",
|
|
2160
2213
|
"kind": "script",
|
|
2161
|
-
"sha256": "
|
|
2214
|
+
"sha256": "d54c4d663c234b7d2e607b99f6090b46104c21ba4326ef03bcb65de39d1cce19",
|
|
2162
2215
|
"mode": 420,
|
|
2163
2216
|
"origin": "kit"
|
|
2164
2217
|
},
|
|
@@ -2172,7 +2225,7 @@
|
|
|
2172
2225
|
{
|
|
2173
2226
|
"path": "scripts/worktree-lifecycle/README.md",
|
|
2174
2227
|
"kind": "doc",
|
|
2175
|
-
"sha256": "
|
|
2228
|
+
"sha256": "5dba0332253c37162ad1b2f1b7bf504a23af85bf297fdaa8920036f610ff754a",
|
|
2176
2229
|
"mode": 420,
|
|
2177
2230
|
"origin": "kit"
|
|
2178
2231
|
},
|
|
@@ -2186,7 +2239,7 @@
|
|
|
2186
2239
|
{
|
|
2187
2240
|
"path": "scripts/wrapup-land.py",
|
|
2188
2241
|
"kind": "script",
|
|
2189
|
-
"sha256": "
|
|
2242
|
+
"sha256": "ee1f567e7be4f7baae222ed3974d39e5efee93edb2a663cacc8e18b567fa837e",
|
|
2190
2243
|
"mode": 493,
|
|
2191
2244
|
"origin": "kit"
|
|
2192
2245
|
},
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Durable Knowledge Placement
|
|
2
|
+
|
|
3
|
+
Use this consumer-owned policy to decide where a learning belongs. Edit the
|
|
4
|
+
categories and examples to match this repository's terminology.
|
|
5
|
+
|
|
6
|
+
## Placement layers
|
|
7
|
+
|
|
8
|
+
- Repository instructions: stable rules that should guide every relevant task.
|
|
9
|
+
- Workflow skill: repeatable procedure with a named entry point and verification.
|
|
10
|
+
- Domain documentation: product language, boundaries, and durable decisions.
|
|
11
|
+
- Local memory: contextual knowledge that helps future sessions but is not yet a
|
|
12
|
+
repository-wide rule.
|
|
13
|
+
|
|
14
|
+
## Decision check
|
|
15
|
+
|
|
16
|
+
Before retaining a learning, record:
|
|
17
|
+
|
|
18
|
+
1. who needs it;
|
|
19
|
+
2. when it should be loaded;
|
|
20
|
+
3. what evidence would make it stale;
|
|
21
|
+
4. which tracked layer is the narrowest durable owner.
|
|
22
|
+
|
|
23
|
+
Prefer a tracked repository layer when the knowledge changes how work should be
|
|
24
|
+
performed. Keep personal preferences and user-profile information outside
|
|
25
|
+
project-owned memories unless the user explicitly places them here.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Memory Lifecycle Policy
|
|
2
|
+
|
|
3
|
+
This file is consumer-owned after its first seed. Edit roots, retention classes,
|
|
4
|
+
and review cadence to match the repository. Kit updates must never overwrite it.
|
|
5
|
+
|
|
6
|
+
## Retention classes
|
|
7
|
+
|
|
8
|
+
- Active: still relevant to current work and safe to load.
|
|
9
|
+
- Archive candidate: useful history that should leave the active set.
|
|
10
|
+
- Protected: user-profile, preference, or other content that must not be pruned
|
|
11
|
+
by an automated repository workflow.
|
|
12
|
+
|
|
13
|
+
## Archive and restore
|
|
14
|
+
|
|
15
|
+
- Archive only inside the configured consumer-owned archive root.
|
|
16
|
+
- Preview every action before mutation.
|
|
17
|
+
- Restore only with an explicit restore grant.
|
|
18
|
+
- Preserve the archive and refuse destination collisions.
|
|
19
|
+
- Record path verdicts and hashes in a content-free, collision-safe receipt.
|
|
20
|
+
|
|
21
|
+
## Pruning
|
|
22
|
+
|
|
23
|
+
Pruning is never implied by setup, update, archive, or restore. It requires a
|
|
24
|
+
separate preview and explicit user approval. A failed or interrupted operation
|
|
25
|
+
must leave the last safe active and archived state recoverable.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikon85/agent-workflow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
".claude/",
|
|
22
22
|
"!.claude/logs/",
|
|
23
23
|
".agents/",
|
|
24
|
+
"assets/",
|
|
24
25
|
"scripts/",
|
|
25
26
|
"docs/",
|
|
26
27
|
"src/",
|
|
@@ -10,6 +10,8 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
10
10
|
import { dirname, isAbsolute, normalize, relative, resolve, sep } from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
|
|
13
|
+
export { setupMemoryLifecycle } from './setup.mjs';
|
|
14
|
+
|
|
13
15
|
async function exists(path) {
|
|
14
16
|
try {
|
|
15
17
|
return await lstat(path);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lstat,
|
|
3
|
+
mkdir,
|
|
4
|
+
readFile,
|
|
5
|
+
rename,
|
|
6
|
+
rm,
|
|
7
|
+
writeFile,
|
|
8
|
+
} from 'node:fs/promises';
|
|
9
|
+
import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const PROFILE_PATH = 'docs/agents/workflow-capabilities.json';
|
|
14
|
+
const TEMPLATE_NAMES = [
|
|
15
|
+
'meta_decision_layer_choice.md',
|
|
16
|
+
'meta_memory_lifecycle.md',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function contained(root, path) {
|
|
20
|
+
if (isAbsolute(path)) return null;
|
|
21
|
+
const normalized = normalize(path);
|
|
22
|
+
if (normalized === '..' || normalized.startsWith(`..${sep}`)) return null;
|
|
23
|
+
const absolute = resolve(root, normalized);
|
|
24
|
+
return relative(resolve(root), absolute).split(sep)[0] === '..' ? null : absolute;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function lstatIfPresent(path) {
|
|
28
|
+
try {
|
|
29
|
+
return await lstat(path);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error.code === 'ENOENT') return null;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function hasSymlink(root, path) {
|
|
37
|
+
let current = resolve(root);
|
|
38
|
+
const segments = relative(current, path).split(sep).filter(Boolean);
|
|
39
|
+
for (const segment of segments) {
|
|
40
|
+
current = resolve(current, segment);
|
|
41
|
+
if ((await lstatIfPresent(current))?.isSymbolicLink()) return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readJson(path) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code === 'ENOENT') return null;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function writeJsonAtomic(path, value) {
|
|
56
|
+
await mkdir(dirname(path), { recursive: true });
|
|
57
|
+
const staged = `${path}.${randomUUID()}.tmp`;
|
|
58
|
+
try {
|
|
59
|
+
await writeFile(staged, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
|
|
60
|
+
await rename(staged, path);
|
|
61
|
+
} finally {
|
|
62
|
+
await rm(staged, { force: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function defaultCapability() {
|
|
67
|
+
return {
|
|
68
|
+
enabled: true,
|
|
69
|
+
activeRoot: '.memory/active',
|
|
70
|
+
archiveRoot: '.memory/archive',
|
|
71
|
+
receiptRoot: '.memory/receipts',
|
|
72
|
+
approvals: { restore: false, prune: false },
|
|
73
|
+
memories: TEMPLATE_NAMES.map((path) => ({ path, enabled: true })),
|
|
74
|
+
templatesSeeded: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function setupMemoryLifecycle({
|
|
79
|
+
projectRoot = process.cwd(),
|
|
80
|
+
templateRoot = join(projectRoot, 'assets', 'memory-templates'),
|
|
81
|
+
decision,
|
|
82
|
+
} = {}) {
|
|
83
|
+
const profilePath = resolve(projectRoot, PROFILE_PATH);
|
|
84
|
+
if (await hasSymlink(projectRoot, profilePath)) {
|
|
85
|
+
throw new Error('memoryLifecycle profile path contains a symlink');
|
|
86
|
+
}
|
|
87
|
+
const profile = await readJson(profilePath);
|
|
88
|
+
const existing = profile?.memoryLifecycle;
|
|
89
|
+
|
|
90
|
+
if (decision !== 'enable') {
|
|
91
|
+
return {
|
|
92
|
+
state: existing?.enabled ? 'enabled' : 'disabled',
|
|
93
|
+
seeded: [],
|
|
94
|
+
adopted: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const capability = {
|
|
99
|
+
...defaultCapability(),
|
|
100
|
+
...existing,
|
|
101
|
+
enabled: true,
|
|
102
|
+
approvals: {
|
|
103
|
+
restore: false,
|
|
104
|
+
prune: false,
|
|
105
|
+
...existing?.approvals,
|
|
106
|
+
},
|
|
107
|
+
templatesSeeded: true,
|
|
108
|
+
};
|
|
109
|
+
const activeRoot = contained(projectRoot, capability.activeRoot);
|
|
110
|
+
if (!activeRoot) throw new Error('memoryLifecycle.activeRoot must stay inside the project');
|
|
111
|
+
if (await hasSymlink(projectRoot, activeRoot)) {
|
|
112
|
+
throw new Error('memoryLifecycle.activeRoot contains a symlink');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const shouldSeed = existing?.templatesSeeded !== true;
|
|
116
|
+
const sources = shouldSeed
|
|
117
|
+
? await Promise.all(TEMPLATE_NAMES.map(async (name) => ({
|
|
118
|
+
name,
|
|
119
|
+
content: await readFile(join(templateRoot, name)),
|
|
120
|
+
})))
|
|
121
|
+
: [];
|
|
122
|
+
const seeded = [];
|
|
123
|
+
const adopted = [];
|
|
124
|
+
const destinations = sources.map(({ name, ...source }) => ({
|
|
125
|
+
...source,
|
|
126
|
+
name,
|
|
127
|
+
destination: contained(activeRoot, name),
|
|
128
|
+
}));
|
|
129
|
+
for (const { destination } of destinations) {
|
|
130
|
+
if (await hasSymlink(activeRoot, destination)) {
|
|
131
|
+
throw new Error('memory policy destination contains a symlink');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const { content, destination } of destinations) {
|
|
135
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
136
|
+
try {
|
|
137
|
+
await writeFile(destination, content, { flag: 'wx' });
|
|
138
|
+
seeded.push(relative(projectRoot, destination));
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error.code !== 'EEXIST') throw error;
|
|
141
|
+
adopted.push(relative(projectRoot, destination));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await writeJsonAtomic(profilePath, {
|
|
146
|
+
schemaVersion: 1,
|
|
147
|
+
...profile,
|
|
148
|
+
memoryLifecycle: capability,
|
|
149
|
+
});
|
|
150
|
+
return { state: 'enabled', seeded, adopted };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const MEMORY_POLICY_TEMPLATES = Object.freeze([...TEMPLATE_NAMES]);
|
|
154
|
+
|
|
155
|
+
async function main(argv) {
|
|
156
|
+
const result = await setupMemoryLifecycle({
|
|
157
|
+
decision: argv.includes('--enable') ? 'enable' : undefined,
|
|
158
|
+
});
|
|
159
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
163
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
164
|
+
process.stderr.write(`${error.message}\n`);
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Wrapup must reuse the shipped Worktree Lifecycle cleanup assessment."""
|
|
3
|
+
|
|
4
|
+
import importlib.util
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
12
|
+
WRAPUP = REPO / "scripts/wrapup-land.py"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_wrapup():
|
|
16
|
+
spec = importlib.util.spec_from_file_location("wrapup_land_worktree_contract", WRAPUP)
|
|
17
|
+
module = importlib.util.module_from_spec(spec)
|
|
18
|
+
spec.loader.exec_module(module)
|
|
19
|
+
return module
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WorktreeCleanupContract(unittest.TestCase):
|
|
23
|
+
def test_active_profile_delegates_removal_safety_to_shared_assessment(self):
|
|
24
|
+
wrapup = load_wrapup()
|
|
25
|
+
calls = []
|
|
26
|
+
|
|
27
|
+
class FakeCore:
|
|
28
|
+
@staticmethod
|
|
29
|
+
def load_profile(path):
|
|
30
|
+
calls.append(("profile", path))
|
|
31
|
+
return {"enabled": True}
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def cleanup_assessment(profile, main, target, merge_target=None):
|
|
35
|
+
calls.append(("assessment", main, target, merge_target))
|
|
36
|
+
return SimpleNamespace(reasons=("dirty worktree",), assumptions="reviewed")
|
|
37
|
+
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
39
|
+
main = Path(tmp)
|
|
40
|
+
profile = main / "docs/agents/workflow-capabilities.json"
|
|
41
|
+
profile.parent.mkdir(parents=True)
|
|
42
|
+
profile.write_text('{"worktreeLifecycle":{"enabled":true}}\n')
|
|
43
|
+
with patch.object(wrapup, "load_worktree_cleanup_core", return_value=FakeCore):
|
|
44
|
+
with self.assertRaises(wrapup.Stop) as stopped:
|
|
45
|
+
wrapup.ensure_worktree_removable(str(main / "wt"), str(main))
|
|
46
|
+
|
|
47
|
+
self.assertIn("shared cleanup guard", stopped.exception.reason)
|
|
48
|
+
self.assertEqual(calls[1][-1], "origin/main")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
unittest.main()
|
|
@@ -31,6 +31,14 @@ the core proves the target is unsafe.
|
|
|
31
31
|
| `enforce-worktree.py` | PreToolUse | blocks tracked main-checkout edits and cross-worktree leaks |
|
|
32
32
|
| `enforce-worktree-cwd.py` | PreToolUse | blocks verification or Git mutation in the wrong checkout |
|
|
33
33
|
| `enforce-worktree-discipline.py` | PreToolUse | routes issue-branch creation through the configured setup entry |
|
|
34
|
+
| `slice-handoff-hint.py` | UserPromptSubmit | names the configured setup entry for a defined slice |
|
|
35
|
+
|
|
36
|
+
## Cleanup
|
|
37
|
+
|
|
38
|
+
`cleanup.py` previews by default. Removal refuses protected, dirty, or unmerged
|
|
39
|
+
worktrees. The assessment reads `ANNAHMEN.md` before removal and returns its
|
|
40
|
+
contents for propagation. `wrapup-land.py` invokes this same assessment after a
|
|
41
|
+
merge and before killing processes or removing the worktree.
|
|
34
42
|
|
|
35
43
|
Claude hook wiring and any Codex adaptation consume this same profile and core.
|
|
36
44
|
An adapter may change only the surface event envelope; it must preserve the
|
|
@@ -3,34 +3,42 @@
|
|
|
3
3
|
"capabilities": [
|
|
4
4
|
{
|
|
5
5
|
"historicalPath": "scripts/setup-worktree.sh",
|
|
6
|
+
"artifact": "scripts/worktree-lifecycle/setup.py",
|
|
6
7
|
"primitives": ["profile", "git-facts", "transactional-setup"]
|
|
7
8
|
},
|
|
8
9
|
{
|
|
9
10
|
"historicalPath": ".claude/hooks/branch-context.py",
|
|
11
|
+
"artifact": ".claude/hooks/branch-context.py",
|
|
10
12
|
"primitives": ["profile", "git-facts", "hook-event-output"]
|
|
11
13
|
},
|
|
12
14
|
{
|
|
13
15
|
"historicalPath": ".claude/hooks/branch-watch.py",
|
|
16
|
+
"artifact": ".claude/hooks/branch-watch.py",
|
|
14
17
|
"primitives": ["profile", "git-facts", "hook-event-output"]
|
|
15
18
|
},
|
|
16
19
|
{
|
|
17
20
|
"historicalPath": ".claude/hooks/enforce-worktree.py",
|
|
21
|
+
"artifact": ".claude/hooks/enforce-worktree.py",
|
|
18
22
|
"primitives": ["profile", "git-facts", "worktree-decision"]
|
|
19
23
|
},
|
|
20
24
|
{
|
|
21
25
|
"historicalPath": ".claude/hooks/enforce-worktree-cwd.py",
|
|
26
|
+
"artifact": ".claude/hooks/enforce-worktree-cwd.py",
|
|
22
27
|
"primitives": ["profile", "git-facts", "worktree-decision"]
|
|
23
28
|
},
|
|
24
29
|
{
|
|
25
30
|
"historicalPath": ".claude/hooks/enforce-worktree-discipline.py",
|
|
31
|
+
"artifact": ".claude/hooks/enforce-worktree-discipline.py",
|
|
26
32
|
"primitives": ["profile", "git-facts", "worktree-decision"]
|
|
27
33
|
},
|
|
28
34
|
{
|
|
29
35
|
"historicalPath": ".claude/hooks/slice-handoff-hint.py",
|
|
36
|
+
"artifact": ".claude/hooks/slice-handoff-hint.py",
|
|
30
37
|
"primitives": ["profile", "git-facts", "hook-event-output"]
|
|
31
38
|
},
|
|
32
39
|
{
|
|
33
40
|
"historicalPath": "scripts/cleanup-worktrees.sh",
|
|
41
|
+
"artifact": "scripts/worktree-lifecycle/cleanup.py",
|
|
34
42
|
"primitives": ["profile", "git-facts", "cleanup-safety"]
|
|
35
43
|
}
|
|
36
44
|
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Preview or safely remove a profile-governed linked worktree."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from core import LifecycleError, cleanup_assessment, load_profile, main_worktree, run
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_args() -> argparse.Namespace:
|
|
15
|
+
parser = argparse.ArgumentParser()
|
|
16
|
+
parser.add_argument("--profile", default="docs/agents/workflow-capabilities.json")
|
|
17
|
+
parser.add_argument("--remove", action="store_true")
|
|
18
|
+
parser.add_argument("worktree")
|
|
19
|
+
return parser.parse_args()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def execute(args: argparse.Namespace) -> dict:
|
|
23
|
+
main = main_worktree(Path.cwd())
|
|
24
|
+
profile_path = Path(args.profile)
|
|
25
|
+
if not profile_path.is_absolute():
|
|
26
|
+
profile_path = main / profile_path
|
|
27
|
+
profile = load_profile(profile_path)
|
|
28
|
+
assessment = cleanup_assessment(profile, main, Path(args.worktree))
|
|
29
|
+
report = {
|
|
30
|
+
"worktree": str(assessment.worktree),
|
|
31
|
+
"branch": assessment.branch,
|
|
32
|
+
"removable": assessment.removable,
|
|
33
|
+
"reasons": list(assessment.reasons),
|
|
34
|
+
"assumptions": assessment.assumptions,
|
|
35
|
+
"removed": False,
|
|
36
|
+
}
|
|
37
|
+
if not args.remove:
|
|
38
|
+
return report
|
|
39
|
+
if not assessment.removable:
|
|
40
|
+
raise LifecycleError("; ".join(assessment.reasons))
|
|
41
|
+
run(["git", "worktree", "remove", str(assessment.worktree)], cwd=main)
|
|
42
|
+
run(["git", "branch", "-d", assessment.branch], cwd=main)
|
|
43
|
+
report["removed"] = True
|
|
44
|
+
return report
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main() -> int:
|
|
48
|
+
try:
|
|
49
|
+
print(json.dumps(execute(parse_args()), ensure_ascii=False, indent=2))
|
|
50
|
+
return 0
|
|
51
|
+
except LifecycleError as error:
|
|
52
|
+
print(f"STOP: {error}", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
raise SystemExit(main())
|
|
@@ -38,6 +38,18 @@ class Decision:
|
|
|
38
38
|
event_name: str = ""
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class CleanupAssessment:
|
|
43
|
+
worktree: Path
|
|
44
|
+
branch: str
|
|
45
|
+
assumptions: str
|
|
46
|
+
reasons: tuple[str, ...]
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def removable(self) -> bool:
|
|
50
|
+
return not self.reasons
|
|
51
|
+
|
|
52
|
+
|
|
41
53
|
def collect_facts(cwd: Path) -> RepoFacts:
|
|
42
54
|
root = Path(run(["git", "rev-parse", "--show-toplevel"], cwd=cwd).stdout.strip()).resolve()
|
|
43
55
|
main = main_worktree(root)
|
|
@@ -104,6 +116,50 @@ def is_tracked(root: Path, relative: str) -> bool:
|
|
|
104
116
|
return result.returncode == 0
|
|
105
117
|
|
|
106
118
|
|
|
119
|
+
def cleanup_assessment(
|
|
120
|
+
profile: WorktreeProfile,
|
|
121
|
+
main: Path,
|
|
122
|
+
target: Path,
|
|
123
|
+
merge_target: str | None = None,
|
|
124
|
+
) -> CleanupAssessment:
|
|
125
|
+
worktree = target.resolve()
|
|
126
|
+
reasons = []
|
|
127
|
+
branch = run(
|
|
128
|
+
["git", "-C", str(worktree), "branch", "--show-current"],
|
|
129
|
+
cwd=main,
|
|
130
|
+
check=False,
|
|
131
|
+
).stdout.strip()
|
|
132
|
+
if worktree not in registered_worktrees(main):
|
|
133
|
+
reasons.append("not a registered worktree")
|
|
134
|
+
if not branch:
|
|
135
|
+
reasons.append("detached or unreadable branch")
|
|
136
|
+
if branch in profile.protected_branches or worktree == main.resolve():
|
|
137
|
+
reasons.append(f"protected worktree branch: {branch or '<unknown>'}")
|
|
138
|
+
status = run(
|
|
139
|
+
["git", "-C", str(worktree), "status", "--porcelain"],
|
|
140
|
+
cwd=main,
|
|
141
|
+
check=False,
|
|
142
|
+
).stdout
|
|
143
|
+
if status.strip():
|
|
144
|
+
reasons.append("dirty worktree")
|
|
145
|
+
if branch and branch not in profile.protected_branches:
|
|
146
|
+
main_branch = merge_target or run(
|
|
147
|
+
["git", "-C", str(main), "branch", "--show-current"],
|
|
148
|
+
cwd=main,
|
|
149
|
+
check=False,
|
|
150
|
+
).stdout.strip()
|
|
151
|
+
merged = run(
|
|
152
|
+
["git", "merge-base", "--is-ancestor", branch, main_branch],
|
|
153
|
+
cwd=main,
|
|
154
|
+
check=False,
|
|
155
|
+
)
|
|
156
|
+
if merged.returncode != 0:
|
|
157
|
+
reasons.append(f"unmerged branch: {branch}")
|
|
158
|
+
assumptions_path = worktree / "ANNAHMEN.md"
|
|
159
|
+
assumptions = assumptions_path.read_text(encoding="utf-8") if assumptions_path.is_file() else ""
|
|
160
|
+
return CleanupAssessment(worktree, branch, assumptions, tuple(reasons))
|
|
161
|
+
|
|
162
|
+
|
|
107
163
|
def edit_decision(
|
|
108
164
|
profile: WorktreeProfile,
|
|
109
165
|
facts: RepoFacts,
|
|
@@ -196,6 +252,28 @@ def branch_create_decision(
|
|
|
196
252
|
return Decision("allow")
|
|
197
253
|
|
|
198
254
|
|
|
255
|
+
def handoff_decision(
|
|
256
|
+
profile: WorktreeProfile,
|
|
257
|
+
facts: RepoFacts,
|
|
258
|
+
payload: dict[str, Any],
|
|
259
|
+
) -> Decision:
|
|
260
|
+
prompt = str(payload.get("prompt") or "")
|
|
261
|
+
pattern = re.compile(
|
|
262
|
+
rf"{re.escape(profile.setup_entry)}\s+(\d+)\s+(\S+)"
|
|
263
|
+
)
|
|
264
|
+
match = pattern.search(prompt)
|
|
265
|
+
if match is None or not facts.is_main_worktree:
|
|
266
|
+
return Decision("skip")
|
|
267
|
+
issue, slug = match.groups()
|
|
268
|
+
command = f"{profile.setup_entry} {issue} {slug}"
|
|
269
|
+
return Decision(
|
|
270
|
+
"emit",
|
|
271
|
+
f"Defined slice detected: create its isolated worktree first with `{command}`, "
|
|
272
|
+
"then perform repository reads from that worktree.",
|
|
273
|
+
"UserPromptSubmit",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
199
277
|
def evaluate(
|
|
200
278
|
profile: WorktreeProfile,
|
|
201
279
|
facts: RepoFacts,
|
|
@@ -216,4 +294,6 @@ def evaluate(
|
|
|
216
294
|
return command_decision(profile, facts, payload)
|
|
217
295
|
if event == "branch-create":
|
|
218
296
|
return branch_create_decision(profile, facts, payload)
|
|
297
|
+
if event == "handoff":
|
|
298
|
+
return handoff_decision(profile, facts, payload)
|
|
219
299
|
return Decision("skip")
|
package/scripts/wrapup-land.py
CHANGED
|
@@ -20,6 +20,7 @@ no --no-verify; the caller diagnoses.
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import argparse
|
|
23
|
+
import importlib.util
|
|
23
24
|
import json
|
|
24
25
|
import os
|
|
25
26
|
import re
|
|
@@ -90,6 +91,44 @@ def load_profile() -> dict:
|
|
|
90
91
|
return {}
|
|
91
92
|
|
|
92
93
|
|
|
94
|
+
def load_worktree_cleanup_core():
|
|
95
|
+
path = Path(__file__).resolve().parent / "worktree-lifecycle" / "core.py"
|
|
96
|
+
module_dir = str(path.parent)
|
|
97
|
+
if module_dir not in sys.path:
|
|
98
|
+
sys.path.insert(0, module_dir)
|
|
99
|
+
spec = importlib.util.spec_from_file_location("_wrapup_worktree_lifecycle", path)
|
|
100
|
+
if spec is None or spec.loader is None:
|
|
101
|
+
raise Stop("cleanup", f"cannot load shared cleanup core from {path}")
|
|
102
|
+
module = importlib.util.module_from_spec(spec)
|
|
103
|
+
sys.modules[spec.name] = module
|
|
104
|
+
spec.loader.exec_module(module)
|
|
105
|
+
return module
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def ensure_worktree_removable(wt: str, main_tree: str):
|
|
109
|
+
profile_path = Path(main_tree) / "docs/agents/workflow-capabilities.json"
|
|
110
|
+
if not profile_path.is_file():
|
|
111
|
+
return None
|
|
112
|
+
core = load_worktree_cleanup_core()
|
|
113
|
+
try:
|
|
114
|
+
profile = core.load_profile(profile_path)
|
|
115
|
+
assessment = core.cleanup_assessment(
|
|
116
|
+
profile,
|
|
117
|
+
Path(main_tree),
|
|
118
|
+
Path(wt),
|
|
119
|
+
merge_target="origin/main",
|
|
120
|
+
)
|
|
121
|
+
except core.LifecycleError as error:
|
|
122
|
+
raise Stop("cleanup", f"shared cleanup guard failed: {error}") from error
|
|
123
|
+
if assessment.reasons:
|
|
124
|
+
raise Stop(
|
|
125
|
+
"cleanup",
|
|
126
|
+
"shared cleanup guard refused worktree removal",
|
|
127
|
+
"; ".join(assessment.reasons),
|
|
128
|
+
)
|
|
129
|
+
return assessment
|
|
130
|
+
|
|
131
|
+
|
|
93
132
|
# ---------- drift log (ANNAHMEN.md) ----------
|
|
94
133
|
|
|
95
134
|
def parse_annahmen(text: str, default_section: str) -> tuple[list[dict], list[str]]:
|
|
@@ -428,6 +467,12 @@ def cmd_land(args) -> dict:
|
|
|
428
467
|
|
|
429
468
|
# Step 2 — kill the worktree's dev server, then Step 4 — teardown
|
|
430
469
|
if wt_exists:
|
|
470
|
+
git(["fetch", "origin", "main"], cwd=main_tree, check=True)
|
|
471
|
+
cleanup = ensure_worktree_removable(wt, main_tree)
|
|
472
|
+
report["cleanup_guard"] = {
|
|
473
|
+
"active": cleanup is not None,
|
|
474
|
+
"assumptions_read": bool(cleanup and cleanup.assumptions),
|
|
475
|
+
}
|
|
431
476
|
report["killed_processes"] = kill_worktree_processes(wt)
|
|
432
477
|
p = git(["worktree", "remove", wt], cwd=main_tree)
|
|
433
478
|
if p.returncode != 0:
|
package/src/lib/bundle.mjs
CHANGED
|
@@ -65,15 +65,18 @@ export const HELPER_FILES = [
|
|
|
65
65
|
{ path: 'scripts/census/delta.mjs', kind: 'script', mode: 0o644 },
|
|
66
66
|
{ path: 'scripts/census/state.mjs', kind: 'script', mode: 0o644 },
|
|
67
67
|
{ path: 'scripts/census/transaction.mjs', kind: 'script', mode: 0o644 },
|
|
68
|
-
// Consumer-owned memory planning and recovery.
|
|
69
|
-
// skill API and the no-write-by-default CLI.
|
|
68
|
+
// Consumer-owned memory planning, one-time policy seeding, and recovery.
|
|
70
69
|
{ path: 'scripts/memory-lifecycle/index.mjs', kind: 'script', mode: 0o644 },
|
|
70
|
+
{ path: 'scripts/memory-lifecycle/setup.mjs', kind: 'script', mode: 0o644 },
|
|
71
|
+
{ path: 'assets/memory-templates/meta_decision_layer_choice.md', kind: 'template', mode: 0o644 },
|
|
72
|
+
{ path: 'assets/memory-templates/meta_memory_lifecycle.md', kind: 'template', mode: 0o644 },
|
|
71
73
|
// Profile-driven Worktree Lifecycle foundation. The setup adapter imports
|
|
72
74
|
// core.py, while capabilities.json keeps the historical 8/8 denominator
|
|
73
75
|
// explicit until the remaining hook and cleanup adapters are activated.
|
|
74
76
|
{ path: 'scripts/worktree-lifecycle/core.py', kind: 'script', mode: 0o644 },
|
|
75
77
|
{ path: 'scripts/worktree-lifecycle/profile.py', kind: 'script', mode: 0o644 },
|
|
76
78
|
{ path: 'scripts/worktree-lifecycle/setup.py', kind: 'script', mode: 0o755 },
|
|
79
|
+
{ path: 'scripts/worktree-lifecycle/cleanup.py', kind: 'script', mode: 0o755 },
|
|
77
80
|
{ path: 'scripts/worktree-lifecycle/capabilities.json', kind: 'doc', mode: 0o644 },
|
|
78
81
|
{ path: 'scripts/worktree-lifecycle/README.md', kind: 'doc', mode: 0o644 },
|
|
79
82
|
// Shared hook utility imported by the shipped hooks (drift-guard,
|
|
@@ -87,6 +90,7 @@ export const HELPER_FILES = [
|
|
|
87
90
|
{ path: '.claude/hooks/enforce-worktree.py', kind: 'hook', mode: 0o755 },
|
|
88
91
|
{ path: '.claude/hooks/enforce-worktree-cwd.py', kind: 'hook', mode: 0o755 },
|
|
89
92
|
{ path: '.claude/hooks/enforce-worktree-discipline.py', kind: 'hook', mode: 0o755 },
|
|
93
|
+
{ path: '.claude/hooks/slice-handoff-hint.py', kind: 'hook', mode: 0o755 },
|
|
90
94
|
{ path: '.claude/hooks/drift-guard.py', kind: 'hook', mode: 0o755 },
|
|
91
95
|
// SessionStart skill-freshness drift-hint (audit-skills names it). For each
|
|
92
96
|
// <skill>/SOURCES.txt it flags sources newer in git than the SKILL.md. Imports
|