@ikon85/agent-workflow-kit 0.22.0 → 0.24.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.
@@ -26,7 +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` | consumer-owned capability profile, including optional Worktree Lifecycle activation (Section A4) |
29
+ | `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle, Memory Lifecycle, and Workflow Advisories choices (Sections A4–A6) |
30
30
 
31
31
  ## Idempotency contract — read before writing anything
32
32
 
@@ -57,7 +57,8 @@ Read the current state; don't assume. For every target file, read its first line
57
57
  - `CONTEXT.md`, `CONTEXT-MAP.md`, `docs/adr/` — domain-doc layout.
58
58
  - `docs/agents/`, `docs/agents/skills/`, `docs/conventions/` — prior output of this skill.
59
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` — an existing Worktree Lifecycle choice/profile and hook wiring to adopt.
60
+ - `docs/agents/workflow-capabilities.json`, `.claude/settings.json` — existing capability choices/profile plus Worktree Lifecycle and Workflow Advisories hook wiring to adopt.
61
+ - `.memory/active/`, `.memory/archive/`, and `.memory/receipts/` — consumer-owned Memory Lifecycle policies or recovery evidence to adopt without normalization.
61
62
  - `gh auth status` (if GitHub) — is `gh` authenticated, and with which scopes?
62
63
 
63
64
  ### 2. Section A — Issue tracker
@@ -165,6 +166,58 @@ The default setup entry is
165
166
  may remain the configured `setupEntry` for parity. The handoff advisory always
166
167
  reads that configured entry and never hardcodes a project script.
167
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
+
202
+ ### 2e. Section A6 — Optional Workflow Advisories
203
+
204
+ > Workflow Advisories provides non-blocking, low-noise hints and
205
+ > affected-surface checks around large reads, first-impacting edits, refactors,
206
+ > and Stop. Paths, commands, thresholds, timeouts, and output budgets remain
207
+ > consumer-owned.
208
+
209
+ Read [workflow-advisories.md](./workflow-advisories.md) in full. Reconcile its
210
+ complete `yes / later / no / existing / disable` matrix. If no choice exists,
211
+ ask: *"Should setup activate the portable Workflow Advisories for this
212
+ repository?"* Offer exactly **Yes**, **Later**, and **No**.
213
+
214
+ - **Yes** — reconcile only `workflowAdvisories` in the shared capability
215
+ profile and only the exact kit-owned hook commands named by the seed.
216
+ - **Later / No** — record the choice without hook wiring.
217
+ - **Existing** — adopt profile and wiring byte-safely.
218
+ - **Disable** — remove exact kit-owned commands first, then set `enabled:
219
+ false`; preserve every profile value and unknown key.
220
+
168
221
  ### 3. Section B — Triage labels
169
222
 
170
223
  > 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.
@@ -250,11 +303,17 @@ pre-existing consumer-owned census file. `yes`, `later`, `no`, and an adopted
250
303
  existing census are terminal for ordinary setup reruns. Only an explicit user
251
304
  request changes a recorded choice.
252
305
 
253
- For `docs/agents/workflow-capabilities.json`, reconcile only the selected
306
+ For `docs/agents/workflow-capabilities.json`, reconcile only each selected
254
307
  capability section. Never replace the whole file: Project Release, Memory
255
- Lifecycle, Worktree Lifecycle, future sections, and unknown consumer keys share
256
- this profile. Apply the Worktree Lifecycle transition and hook ownership rules
257
- from [worktree-lifecycle.md](./worktree-lifecycle.md) transactionally.
308
+ Lifecycle, Worktree Lifecycle, Workflow Advisories, future sections, and
309
+ unknown consumer keys share this profile. Apply the transition and hook
310
+ ownership rules from [worktree-lifecycle.md](./worktree-lifecycle.md) and
311
+ [workflow-advisories.md](./workflow-advisories.md) transactionally.
312
+
313
+ For Memory Lifecycle, use only the deterministic setup helper from Section A5.
314
+ Do not copy templates with shell commands or edit the capability profile by
315
+ hand. Report `seeded · adopted · skipped`; do not print memory contents or
316
+ receipt contents.
258
317
 
259
318
  For the **`## Workflow`**, **`## Agent skills`**, and **`## Prod`** blocks, reconcile per section in **both** CLAUDE.md and AGENTS.md that exist:
260
319
 
@@ -0,0 +1,75 @@
1
+ # Workflow Advisories setup contract
2
+
3
+ Workflow Advisories is one opt-in capability backed by the consumer-owned
4
+ `docs/agents/workflow-capabilities.json` profile. Enabling it activates
5
+ non-blocking large-read, baseline, pre-refactor, and affected-surface Stop
6
+ adapters. Every threshold, glob, branch rule, command, timeout, and output
7
+ budget stays consumer-owned.
8
+
9
+ ## Choice matrix
10
+
11
+ | State | Setup action |
12
+ |---|---|
13
+ | `missing` | Ask `yes / later / no`; do not infer or write before the answer. |
14
+ | `yes` | Reconcile the enabled profile and exact kit-owned hook commands. |
15
+ | `later` | Record the retryable deferral; do not activate hooks. |
16
+ | `no` | Record the opt-out; do not activate hooks. |
17
+ | `existing` | Adopt the consumer profile byte-safely and preserve unknown keys. |
18
+ | `disable` | Remove only kit-owned hook wiring, then set `enabled: false`; retain profile values and unknown keys. |
19
+
20
+ ```json workflow-advisories-setup-effects
21
+ [
22
+ {"state":"missing","choice":null,"operations":[]},
23
+ {"state":"yes","choice":"yes","operations":["record-choice","reconcile-profile-enabled","reconcile-hook-wiring"]},
24
+ {"state":"later","choice":"later","operations":["record-choice"]},
25
+ {"state":"no","choice":"no","operations":["record-choice"]},
26
+ {"state":"existing","choice":"yes","operations":["adopt-existing"]},
27
+ {"state":"disable","choice":"yes","operations":["remove-hook-wiring","update-profile-disabled"]}
28
+ ]
29
+ ```
30
+
31
+ ## Profile shape
32
+
33
+ Reconcile only `workflowAdvisories`; preserve every sibling section and unknown
34
+ key. Consumer values are never normalized on adoption.
35
+
36
+ ```json
37
+ {
38
+ "workflowAdvisories": {
39
+ "choice": "yes",
40
+ "enabled": true,
41
+ "largeRead": {"tools": ["Read"], "lineThreshold": 500, "outputBudget": 500},
42
+ "baseline": {
43
+ "sourceGlobs": ["src/**"],
44
+ "branchRegex": "^(?:feat|fix)/",
45
+ "manifestPath": ".agent/baseline.json",
46
+ "stateDir": ".claude/logs/advisory-state",
47
+ "outputBudget": 500
48
+ },
49
+ "preRefactor": {
50
+ "promptMatchers": ["refactor"],
51
+ "surfaces": [],
52
+ "timeoutSeconds": 15,
53
+ "outputBudget": 1000
54
+ },
55
+ "stopChecks": {"surfaces": [], "timeoutSeconds": 30, "outputBudget": 1000}
56
+ }
57
+ }
58
+ ```
59
+
60
+ Empty command surfaces are honest inactive defaults. Setup recommends concrete
61
+ project commands from the tools already present, then asks before activating.
62
+
63
+ ## Hook ownership
64
+
65
+ - PreToolUse on Read:
66
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/recon-size-hint.py"`
67
+ - PreToolUse on Edit/Write/MultiEdit:
68
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/baseline-capture-hint.py"`
69
+ - UserPromptSubmit:
70
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-refactor-sweep.py"`
71
+ - Stop:
72
+ `"$CLAUDE_PROJECT_DIR/.claude/hooks/typecheck-on-stop.sh"`
73
+
74
+ All adapters are non-blocking. A failed or timed-out configured command remains
75
+ visibly failed in hook context; it is never reported as green.
@@ -30,6 +30,7 @@ from pathlib import Path
30
30
  # (or from a linked worktree root), so this resolves to <root>/.claude/logs.
31
31
  LOG_DIR = Path(".claude/logs")
32
32
  _WORKTREE_CORE_MODULE = "_agent_workflow_kit_worktree_lifecycle"
33
+ _ADVISORY_CORE_MODULE = "_agent_workflow_kit_workflow_advisories"
33
34
 
34
35
 
35
36
  def rotate_log_if_needed(log_path: Path, max_bytes: int = 100_000, generations: int = 3) -> None:
@@ -205,6 +206,21 @@ def load_worktree_lifecycle_core():
205
206
  return module
206
207
 
207
208
 
209
+ def load_workflow_advisories_core():
210
+ """Load the shipped Workflow Advisories core without requiring a Python package."""
211
+ existing = sys.modules.get(_ADVISORY_CORE_MODULE)
212
+ if existing is not None:
213
+ return existing
214
+ path = Path(__file__).resolve().parents[2] / "scripts" / "workflow-advisories" / "core.py"
215
+ spec = importlib.util.spec_from_file_location(_ADVISORY_CORE_MODULE, path)
216
+ if spec is None or spec.loader is None:
217
+ raise ImportError(f"cannot load Workflow Advisories core from {path}")
218
+ module = importlib.util.module_from_spec(spec)
219
+ sys.modules[_ADVISORY_CORE_MODULE] = module
220
+ spec.loader.exec_module(module)
221
+ return module
222
+
223
+
208
224
  def hook_event_output(event_name: str, context: str) -> dict:
209
225
  """Canonical non-blocking context payload for Claude hook events."""
210
226
  return {
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse once-per-branch baseline advisory for impacting edits."""
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from _hook_utils import current_branch, hook_event_output, load_workflow_advisories_core
8
+
9
+
10
+ def main() -> int:
11
+ try:
12
+ payload = json.load(sys.stdin)
13
+ core = load_workflow_advisories_core()
14
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
15
+ branch = payload.get("branch") or current_branch()
16
+ decision = core.baseline_decision(profile, payload, Path.cwd(), branch)
17
+ if decision.context:
18
+ print(json.dumps(hook_event_output(decision.event_name, decision.context)))
19
+ except Exception:
20
+ pass
21
+ return 0
22
+
23
+
24
+ if __name__ == "__main__":
25
+ sys.exit(main())
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ """UserPromptSubmit non-blocking profile-driven pre-refactor command sweep."""
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from _hook_utils import hook_event_output, load_workflow_advisories_core, run
8
+
9
+
10
+ def main() -> int:
11
+ try:
12
+ payload = json.load(sys.stdin)
13
+ if "changed_files" not in payload:
14
+ changed = run(["git", "diff", "--name-only", "HEAD"])
15
+ payload["changed_files"] = changed.splitlines() if changed else []
16
+ core = load_workflow_advisories_core()
17
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
18
+ decision = core.pre_refactor_decision(profile, payload, Path.cwd())
19
+ if decision.context:
20
+ print(json.dumps(hook_event_output(decision.event_name, decision.context)))
21
+ except Exception:
22
+ pass
23
+ return 0
24
+
25
+
26
+ if __name__ == "__main__":
27
+ sys.exit(main())
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse non-blocking advisory for profile-defined large reads."""
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from _hook_utils import hook_event_output, load_workflow_advisories_core
8
+
9
+
10
+ def main() -> int:
11
+ try:
12
+ payload = json.load(sys.stdin)
13
+ core = load_workflow_advisories_core()
14
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
15
+ decision = core.large_read_decision(profile, payload)
16
+ if decision.context:
17
+ print(json.dumps(hook_event_output(decision.event_name, decision.context)))
18
+ except Exception:
19
+ pass
20
+ return 0
21
+
22
+
23
+ if __name__ == "__main__":
24
+ sys.exit(main())
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ """Stop-event adapter for profile-defined affected-surface checks."""
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from _hook_utils import hook_event_output, load_workflow_advisories_core, run
8
+
9
+
10
+ def main() -> int:
11
+ try:
12
+ payload = json.load(sys.stdin)
13
+ if "changed_files" not in payload:
14
+ changed = run(["git", "diff", "--name-only", "HEAD"])
15
+ payload["changed_files"] = changed.splitlines() if changed else []
16
+ core = load_workflow_advisories_core()
17
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
18
+ decision = core.stop_check_decision(profile, payload, Path.cwd())
19
+ if decision.context:
20
+ print(json.dumps(hook_event_output(decision.event_name, decision.context)))
21
+ except Exception:
22
+ pass
23
+ return 0
24
+
25
+
26
+ if __name__ == "__main__":
27
+ sys.exit(main())
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ exec python3 "$(dirname "$0")/typecheck-on-stop.py"
@@ -26,7 +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` | consumer-owned capability profile, including optional Worktree Lifecycle activation (Section A4) |
29
+ | `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle, Memory Lifecycle, and Workflow Advisories choices (Sections A4–A6) |
30
30
 
31
31
  ## Idempotency contract — read before writing anything
32
32
 
@@ -57,7 +57,8 @@ Read the current state; don't assume. For every target file, read its first line
57
57
  - `CONTEXT.md`, `CONTEXT-MAP.md`, `docs/adr/` — domain-doc layout.
58
58
  - `docs/agents/`, `docs/agents/skills/`, `docs/conventions/` — prior output of this skill.
59
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` — an existing Worktree Lifecycle choice/profile and hook wiring to adopt.
60
+ - `docs/agents/workflow-capabilities.json`, `.claude/settings.json` — existing capability choices/profile plus Worktree Lifecycle and Workflow Advisories hook wiring to adopt.
61
+ - `.memory/active/`, `.memory/archive/`, and `.memory/receipts/` — consumer-owned Memory Lifecycle policies or recovery evidence to adopt without normalization.
61
62
  - `gh auth status` (if GitHub) — is `gh` authenticated, and with which scopes?
62
63
 
63
64
  ### 2. Section A — Issue tracker
@@ -165,6 +166,58 @@ The default setup entry is
165
166
  may remain the configured `setupEntry` for parity. The handoff advisory always
166
167
  reads that configured entry and never hardcodes a project script.
167
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
+
202
+ ### 2e. Section A6 — Optional Workflow Advisories
203
+
204
+ > Workflow Advisories provides non-blocking, low-noise hints and
205
+ > affected-surface checks around large reads, first-impacting edits, refactors,
206
+ > and Stop. Paths, commands, thresholds, timeouts, and output budgets remain
207
+ > consumer-owned.
208
+
209
+ Read [workflow-advisories.md](./workflow-advisories.md) in full. Reconcile its
210
+ complete `yes / later / no / existing / disable` matrix. If no choice exists,
211
+ ask: *"Should setup activate the portable Workflow Advisories for this
212
+ repository?"* Offer exactly **Yes**, **Later**, and **No**.
213
+
214
+ - **Yes** — reconcile only `workflowAdvisories` in the shared capability
215
+ profile and only the exact kit-owned hook commands named by the seed.
216
+ - **Later / No** — record the choice without hook wiring.
217
+ - **Existing** — adopt profile and wiring byte-safely.
218
+ - **Disable** — remove exact kit-owned commands first, then set `enabled:
219
+ false`; preserve every profile value and unknown key.
220
+
168
221
  ### 3. Section B — Triage labels
169
222
 
170
223
  > 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.
@@ -250,11 +303,17 @@ pre-existing consumer-owned census file. `yes`, `later`, `no`, and an adopted
250
303
  existing census are terminal for ordinary setup reruns. Only an explicit user
251
304
  request changes a recorded choice.
252
305
 
253
- For `docs/agents/workflow-capabilities.json`, reconcile only the selected
306
+ For `docs/agents/workflow-capabilities.json`, reconcile only each selected
254
307
  capability section. Never replace the whole file: Project Release, Memory
255
- Lifecycle, Worktree Lifecycle, future sections, and unknown consumer keys share
256
- this profile. Apply the Worktree Lifecycle transition and hook ownership rules
257
- from [worktree-lifecycle.md](./worktree-lifecycle.md) transactionally.
308
+ Lifecycle, Worktree Lifecycle, Workflow Advisories, future sections, and
309
+ unknown consumer keys share this profile. Apply the transition and hook
310
+ ownership rules from [worktree-lifecycle.md](./worktree-lifecycle.md) and
311
+ [workflow-advisories.md](./workflow-advisories.md) transactionally.
312
+
313
+ For Memory Lifecycle, use only the deterministic setup helper from Section A5.
314
+ Do not copy templates with shell commands or edit the capability profile by
315
+ hand. Report `seeded · adopted · skipped`; do not print memory contents or
316
+ receipt contents.
258
317
 
259
318
  For the **`## Workflow`**, **`## Agent skills`**, and **`## Prod`** blocks, reconcile per section in **both** CLAUDE.md and AGENTS.md that exist:
260
319
 
@@ -0,0 +1,75 @@
1
+ # Workflow Advisories setup contract
2
+
3
+ Workflow Advisories is one opt-in capability backed by the consumer-owned
4
+ `docs/agents/workflow-capabilities.json` profile. Enabling it activates
5
+ non-blocking large-read, baseline, pre-refactor, and affected-surface Stop
6
+ adapters. Every threshold, glob, branch rule, command, timeout, and output
7
+ budget stays consumer-owned.
8
+
9
+ ## Choice matrix
10
+
11
+ | State | Setup action |
12
+ |---|---|
13
+ | `missing` | Ask `yes / later / no`; do not infer or write before the answer. |
14
+ | `yes` | Reconcile the enabled profile and exact kit-owned hook commands. |
15
+ | `later` | Record the retryable deferral; do not activate hooks. |
16
+ | `no` | Record the opt-out; do not activate hooks. |
17
+ | `existing` | Adopt the consumer profile byte-safely and preserve unknown keys. |
18
+ | `disable` | Remove only kit-owned hook wiring, then set `enabled: false`; retain profile values and unknown keys. |
19
+
20
+ ```json workflow-advisories-setup-effects
21
+ [
22
+ {"state":"missing","choice":null,"operations":[]},
23
+ {"state":"yes","choice":"yes","operations":["record-choice","reconcile-profile-enabled","reconcile-hook-wiring"]},
24
+ {"state":"later","choice":"later","operations":["record-choice"]},
25
+ {"state":"no","choice":"no","operations":["record-choice"]},
26
+ {"state":"existing","choice":"yes","operations":["adopt-existing"]},
27
+ {"state":"disable","choice":"yes","operations":["remove-hook-wiring","update-profile-disabled"]}
28
+ ]
29
+ ```
30
+
31
+ ## Profile shape
32
+
33
+ Reconcile only `workflowAdvisories`; preserve every sibling section and unknown
34
+ key. Consumer values are never normalized on adoption.
35
+
36
+ ```json
37
+ {
38
+ "workflowAdvisories": {
39
+ "choice": "yes",
40
+ "enabled": true,
41
+ "largeRead": {"tools": ["Read"], "lineThreshold": 500, "outputBudget": 500},
42
+ "baseline": {
43
+ "sourceGlobs": ["src/**"],
44
+ "branchRegex": "^(?:feat|fix)/",
45
+ "manifestPath": ".agent/baseline.json",
46
+ "stateDir": ".claude/logs/advisory-state",
47
+ "outputBudget": 500
48
+ },
49
+ "preRefactor": {
50
+ "promptMatchers": ["refactor"],
51
+ "surfaces": [],
52
+ "timeoutSeconds": 15,
53
+ "outputBudget": 1000
54
+ },
55
+ "stopChecks": {"surfaces": [], "timeoutSeconds": 30, "outputBudget": 1000}
56
+ }
57
+ }
58
+ ```
59
+
60
+ Empty command surfaces are honest inactive defaults. Setup recommends concrete
61
+ project commands from the tools already present, then asks before activating.
62
+
63
+ ## Hook ownership
64
+
65
+ - PreToolUse on Read:
66
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/recon-size-hint.py"`
67
+ - PreToolUse on Edit/Write/MultiEdit:
68
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/baseline-capture-hint.py"`
69
+ - UserPromptSubmit:
70
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-refactor-sweep.py"`
71
+ - Stop:
72
+ `"$CLAUDE_PROJECT_DIR/.claude/hooks/typecheck-on-stop.sh"`
73
+
74
+ All adapters are non-blocking. A failed or timed-out configured command remains
75
+ visibly failed in hook context; it is never reported as green.
package/README.md CHANGED
@@ -332,6 +332,29 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
332
332
 
333
333
  ## Release notes
334
334
 
335
+ ### 0.24.0
336
+
337
+ - added: `.agents/skills/setup-workflow/workflow-advisories.md`
338
+ - added: `.claude/hooks/baseline-capture-hint.py`
339
+ - added: `.claude/hooks/pre-refactor-sweep.py`
340
+ - added: `.claude/hooks/recon-size-hint.py`
341
+ - added: `.claude/hooks/typecheck-on-stop.py`
342
+ - added: `.claude/hooks/typecheck-on-stop.sh`
343
+ - added: `.claude/skills/setup-workflow/workflow-advisories.md`
344
+ - added: `scripts/workflow-advisories/core.py`
345
+ - changed: `.agents/skills/setup-workflow/SKILL.md`
346
+ - changed: `.claude/hooks/_hook_utils.py`
347
+ - changed: `.claude/skills/setup-workflow/SKILL.md`
348
+
349
+ ### 0.23.0
350
+
351
+ - added: `assets/memory-templates/meta_decision_layer_choice.md`
352
+ - added: `assets/memory-templates/meta_memory_lifecycle.md`
353
+ - added: `scripts/memory-lifecycle/setup.mjs`
354
+ - changed: `.agents/skills/setup-workflow/SKILL.md`
355
+ - changed: `.claude/skills/setup-workflow/SKILL.md`
356
+ - changed: `scripts/memory-lifecycle/index.mjs`
357
+
335
358
  ### 0.22.0
336
359
 
337
360
  - added: `.agents/skills/setup-workflow/worktree-lifecycle.md`
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.22.0",
2
+ "kitVersion": "0.24.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": "a1145034dbc334fb388be27dfb77cf1d93f3cefc6f760d5f4fa5d3ee98050dff",
576
+ "sha256": "96c0c3ba9ed6b194030a8c45be618caf934e9c4ca8415ea19d726af33cd763cb",
577
577
  "mode": 420,
578
578
  "origin": "kit"
579
579
  },
@@ -613,6 +613,15 @@
613
613
  "mode": 420,
614
614
  "origin": "kit"
615
615
  },
616
+ {
617
+ "path": ".agents/skills/setup-workflow/workflow-advisories.md",
618
+ "kind": "skill",
619
+ "ownerSkill": "setup-workflow",
620
+ "surface": "codex",
621
+ "sha256": "320f893a6e148f0bfabe1f1be81112728ae4fed84f7505dc630e5b0d931ec32c",
622
+ "mode": 420,
623
+ "origin": "kit"
624
+ },
616
625
  {
617
626
  "path": ".agents/skills/setup-workflow/workflow-overview.md",
618
627
  "kind": "skill",
@@ -877,10 +886,17 @@
877
886
  {
878
887
  "path": ".claude/hooks/_hook_utils.py",
879
888
  "kind": "hook",
880
- "sha256": "58a7a8ad6cd4f1660e95343d7a0c637cf8aee92948a9c554befa5851723b3f48",
889
+ "sha256": "26c645eaa7cb87e899ab3900e9f6711658848cab8a18a29656ae3ca30736d172",
881
890
  "mode": 420,
882
891
  "origin": "kit"
883
892
  },
893
+ {
894
+ "path": ".claude/hooks/baseline-capture-hint.py",
895
+ "kind": "hook",
896
+ "sha256": "8f5cddfbb567afc677cf51f39d37e10804bf03e93d7d2fac7793cb5a42473ed3",
897
+ "mode": 493,
898
+ "origin": "kit"
899
+ },
884
900
  {
885
901
  "path": ".claude/hooks/branch-context.py",
886
902
  "kind": "hook",
@@ -923,6 +939,20 @@
923
939
  "mode": 493,
924
940
  "origin": "kit"
925
941
  },
942
+ {
943
+ "path": ".claude/hooks/pre-refactor-sweep.py",
944
+ "kind": "hook",
945
+ "sha256": "24e8554da18b00f2dbbcf6a3e186f9fb675a4b18471ce60e0ac63000f95a4906",
946
+ "mode": 493,
947
+ "origin": "kit"
948
+ },
949
+ {
950
+ "path": ".claude/hooks/recon-size-hint.py",
951
+ "kind": "hook",
952
+ "sha256": "3fd702d8875552ce3b79b5db1dda51403031bcacc80901a13553d2f60a2deb9b",
953
+ "mode": 493,
954
+ "origin": "kit"
955
+ },
926
956
  {
927
957
  "path": ".claude/hooks/skill-drift-hint.py",
928
958
  "kind": "hook",
@@ -944,6 +974,20 @@
944
974
  "mode": 493,
945
975
  "origin": "kit"
946
976
  },
977
+ {
978
+ "path": ".claude/hooks/typecheck-on-stop.py",
979
+ "kind": "hook",
980
+ "sha256": "459e5f25f5fe97c62687eff1d3e7e76051ba8390e2b2908fc50c16ea53a95250",
981
+ "mode": 493,
982
+ "origin": "kit"
983
+ },
984
+ {
985
+ "path": ".claude/hooks/typecheck-on-stop.sh",
986
+ "kind": "hook",
987
+ "sha256": "1bb57f53d53d18af92a8c7ce63f46be0658ae7d675a95d387646b4232001f1bc",
988
+ "mode": 493,
989
+ "origin": "kit"
990
+ },
947
991
  {
948
992
  "path": ".claude/skills/ask-matt/SKILL.md",
949
993
  "kind": "skill",
@@ -1642,7 +1686,7 @@
1642
1686
  "kind": "skill",
1643
1687
  "ownerSkill": "setup-workflow",
1644
1688
  "surface": "claude",
1645
- "sha256": "a1145034dbc334fb388be27dfb77cf1d93f3cefc6f760d5f4fa5d3ee98050dff",
1689
+ "sha256": "96c0c3ba9ed6b194030a8c45be618caf934e9c4ca8415ea19d726af33cd763cb",
1646
1690
  "mode": 420,
1647
1691
  "origin": "kit"
1648
1692
  },
@@ -1682,6 +1726,15 @@
1682
1726
  "mode": 420,
1683
1727
  "origin": "kit"
1684
1728
  },
1729
+ {
1730
+ "path": ".claude/skills/setup-workflow/workflow-advisories.md",
1731
+ "kind": "skill",
1732
+ "ownerSkill": "setup-workflow",
1733
+ "surface": "claude",
1734
+ "sha256": "320f893a6e148f0bfabe1f1be81112728ae4fed84f7505dc630e5b0d931ec32c",
1735
+ "mode": 420,
1736
+ "origin": "kit"
1737
+ },
1685
1738
  {
1686
1739
  "path": ".claude/skills/setup-workflow/workflow-overview.md",
1687
1740
  "kind": "skill",
@@ -1970,6 +2023,20 @@
1970
2023
  "mode": 420,
1971
2024
  "origin": "kit"
1972
2025
  },
2026
+ {
2027
+ "path": "assets/memory-templates/meta_decision_layer_choice.md",
2028
+ "kind": "template",
2029
+ "sha256": "b22bd1d0e3daa31cbdbb9a3a81964d7a00d5275e69c92c9bd4ad3b4e40995b41",
2030
+ "mode": 420,
2031
+ "origin": "kit"
2032
+ },
2033
+ {
2034
+ "path": "assets/memory-templates/meta_memory_lifecycle.md",
2035
+ "kind": "template",
2036
+ "sha256": "215e865d4e803ce0b2cd43e88d76501a61269dfa90aae5e6a474941a0b72b0ca",
2037
+ "mode": 420,
2038
+ "origin": "kit"
2039
+ },
1973
2040
  {
1974
2041
  "path": "docs/agents/security-audit-runbook-template.md",
1975
2042
  "kind": "template",
@@ -2099,7 +2166,14 @@
2099
2166
  {
2100
2167
  "path": "scripts/memory-lifecycle/index.mjs",
2101
2168
  "kind": "script",
2102
- "sha256": "d83a71891e9c070179a00d6413b18d073061eef75e50919a7f28b8510f7ca20d",
2169
+ "sha256": "871507e20133ccbb11f2a57f092bcb41aa1f6b7deaaa74df2418573316e6bd96",
2170
+ "mode": 420,
2171
+ "origin": "kit"
2172
+ },
2173
+ {
2174
+ "path": "scripts/memory-lifecycle/setup.mjs",
2175
+ "kind": "script",
2176
+ "sha256": "c2c8885b806a830239ae54314c9dd0400399140870b6e62dc1e8b043dffcb78f",
2103
2177
  "mode": 420,
2104
2178
  "origin": "kit"
2105
2179
  },
@@ -2173,6 +2247,13 @@
2173
2247
  "mode": 420,
2174
2248
  "origin": "kit"
2175
2249
  },
2250
+ {
2251
+ "path": "scripts/workflow-advisories/core.py",
2252
+ "kind": "script",
2253
+ "sha256": "7923d328f2a4e8b0ac8823cba5779003a52c6fabeb012157af49c3c5e13ccfc9",
2254
+ "mode": 420,
2255
+ "origin": "kit"
2256
+ },
2176
2257
  {
2177
2258
  "path": "scripts/worktree-lifecycle/capabilities.json",
2178
2259
  "kind": "doc",
@@ -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.22.0",
3
+ "version": "0.24.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,185 @@
1
+ """Profile-driven decision core for non-blocking Workflow Advisories."""
2
+ from __future__ import annotations
3
+
4
+ import fnmatch
5
+ import json
6
+ import re
7
+ import subprocess
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Decision:
14
+ context: str | None
15
+ event_name: str
16
+
17
+
18
+ def load_profile(path: Path) -> dict:
19
+ body = json.loads(path.read_text(encoding="utf-8"))
20
+ section = body.get("workflowAdvisories", {})
21
+ return section if section.get("enabled") is True else {}
22
+
23
+
24
+ def _line_count(path: Path) -> int:
25
+ count = 0
26
+ last = b""
27
+ with path.open("rb") as handle:
28
+ while chunk := handle.read(64 * 1024):
29
+ count += chunk.count(b"\n")
30
+ last = chunk[-1:]
31
+ return count if not last or last == b"\n" else count + 1
32
+
33
+
34
+ def _bounded(message: str, budget: int) -> str:
35
+ if budget <= 0 or len(message) <= budget:
36
+ return message
37
+ return message[: max(0, budget - 1)] + "…"
38
+
39
+
40
+ def large_read_decision(profile: dict, payload: dict) -> Decision:
41
+ config = profile.get("largeRead", {})
42
+ if payload.get("tool_name") not in config.get("tools", []):
43
+ return Decision(None, "PreToolUse")
44
+ raw_path = payload.get("tool_input", {}).get("file_path")
45
+ if not raw_path:
46
+ return Decision(None, "PreToolUse")
47
+ path = Path(raw_path)
48
+ try:
49
+ lines = _line_count(path)
50
+ except (OSError, ValueError):
51
+ return Decision(None, "PreToolUse")
52
+ threshold = int(config.get("lineThreshold", 0))
53
+ if lines < threshold:
54
+ return Decision(None, "PreToolUse")
55
+ message = (
56
+ f"Large read advisory: {path.name} has {lines} lines "
57
+ f"(profile threshold {threshold}). Prefer a bounded read or delegated recon."
58
+ )
59
+ return Decision(_bounded(message, int(config.get("outputBudget", 500))), "PreToolUse")
60
+
61
+
62
+ def _repo_relative(root: Path, raw_path: str) -> str | None:
63
+ try:
64
+ return str(Path(raw_path).resolve().relative_to(root.resolve()))
65
+ except (OSError, ValueError):
66
+ return None
67
+
68
+
69
+ def _valid_baseline(path: Path, branch: str) -> bool:
70
+ try:
71
+ body = json.loads(path.read_text(encoding="utf-8"))
72
+ except (OSError, ValueError, TypeError):
73
+ return False
74
+ return (
75
+ body.get("branch") == branch
76
+ and bool(body.get("capturedAt"))
77
+ and bool(body.get("sources"))
78
+ )
79
+
80
+
81
+ def baseline_decision(profile: dict, payload: dict, root: Path, branch: str) -> Decision:
82
+ config = profile.get("baseline", {})
83
+ if payload.get("tool_name") not in {"Edit", "Write", "MultiEdit"}:
84
+ return Decision(None, "PreToolUse")
85
+ if not re.search(config.get("branchRegex", r"$^"), branch):
86
+ return Decision(None, "PreToolUse")
87
+ raw_path = payload.get("tool_input", {}).get("file_path")
88
+ relative = _repo_relative(root, raw_path) if raw_path else None
89
+ if not relative or not any(
90
+ fnmatch.fnmatch(relative, pattern) for pattern in config.get("sourceGlobs", [])
91
+ ):
92
+ return Decision(None, "PreToolUse")
93
+ manifest = root / config.get("manifestPath", ".agent/baseline.json")
94
+ if _valid_baseline(manifest, branch):
95
+ return Decision(None, "PreToolUse")
96
+ state_dir = root / config.get("stateDir", ".claude/logs/advisory-state")
97
+ marker = state_dir / f"{re.sub(r'[^A-Za-z0-9._-]', '-', branch)}.hinted"
98
+ if marker.exists():
99
+ return Decision(None, "PreToolUse")
100
+ state_dir.mkdir(parents=True, exist_ok=True)
101
+ marker.write_text(f"{branch}\n", encoding="utf-8")
102
+ message = (
103
+ f"Baseline advisory for {branch}: capture a valid baseline before the first "
104
+ f"impacting edit to {relative}. Empty or stale manifests do not count."
105
+ )
106
+ return Decision(_bounded(message, int(config.get("outputBudget", 500))), "PreToolUse")
107
+
108
+
109
+ def pre_refactor_decision(profile: dict, payload: dict, root: Path) -> Decision:
110
+ config = profile.get("preRefactor", {})
111
+ prompt = payload.get("prompt", "")
112
+ if not any(
113
+ re.search(pattern, prompt, re.IGNORECASE)
114
+ for pattern in config.get("promptMatchers", [])
115
+ ):
116
+ return Decision(None, "UserPromptSubmit")
117
+ changed = payload.get("changed_files", [])
118
+ commands: list[list[str]] = []
119
+ seen: set[tuple[str, ...]] = set()
120
+ for surface in config.get("surfaces", []):
121
+ if not any(
122
+ fnmatch.fnmatch(path, pattern)
123
+ for path in changed
124
+ for pattern in surface.get("globs", [])
125
+ ):
126
+ continue
127
+ for command in surface.get("commands", []):
128
+ key = tuple(command)
129
+ if key not in seen:
130
+ commands.append(command)
131
+ seen.add(key)
132
+ return _command_decision(
133
+ commands, config, root, "Pre-refactor sweep:", "UserPromptSubmit",
134
+ )
135
+
136
+
137
+ def _command_decision(
138
+ commands: list[list[str]], config: dict, root: Path, heading: str, event_name: str,
139
+ ) -> Decision:
140
+ if not commands:
141
+ return Decision(None, event_name)
142
+ timeout = float(config.get("timeoutSeconds", 15))
143
+ lines = [heading]
144
+ for command in commands:
145
+ try:
146
+ result = subprocess.run(
147
+ command, cwd=root, capture_output=True, text=True, timeout=timeout,
148
+ )
149
+ detail = (result.stdout + result.stderr).strip()
150
+ verdict = "PASS" if result.returncode == 0 else f"FAIL (exit {result.returncode})"
151
+ except subprocess.TimeoutExpired as error:
152
+ detail = ((error.stdout or "") + (error.stderr or "")).strip()
153
+ verdict = f"FAIL (timeout {timeout:g}s)"
154
+ except OSError as error:
155
+ detail = str(error)
156
+ verdict = "FAIL (exec)"
157
+ lines.append(f"- {verdict}: {' '.join(command)}")
158
+ if detail:
159
+ lines.append(f" {detail}")
160
+ return Decision(
161
+ _bounded("\n".join(lines), int(config.get("outputBudget", 1000))),
162
+ event_name,
163
+ )
164
+
165
+
166
+ def stop_check_decision(profile: dict, payload: dict, root: Path) -> Decision:
167
+ config = profile.get("stopChecks", {})
168
+ changed = payload.get("changed_files", [])
169
+ commands: list[list[str]] = []
170
+ seen: set[tuple[str, ...]] = set()
171
+ for surface in config.get("surfaces", []):
172
+ if not any(
173
+ fnmatch.fnmatch(path, pattern)
174
+ for path in changed
175
+ for pattern in surface.get("globs", [])
176
+ ):
177
+ continue
178
+ command = surface.get("command", [])
179
+ key = tuple(command)
180
+ if command and key not in seen:
181
+ commands.append(command)
182
+ seen.add(key)
183
+ return _command_decision(
184
+ commands, config, root, "Changed-surface stop checks:", "Stop",
185
+ )
@@ -65,9 +65,11 @@ 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. The module is both the stable
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.
@@ -89,6 +91,14 @@ export const HELPER_FILES = [
89
91
  { path: '.claude/hooks/enforce-worktree-cwd.py', kind: 'hook', mode: 0o755 },
90
92
  { path: '.claude/hooks/enforce-worktree-discipline.py', kind: 'hook', mode: 0o755 },
91
93
  { path: '.claude/hooks/slice-handoff-hint.py', kind: 'hook', mode: 0o755 },
94
+ // Profile-driven non-blocking change-lifecycle advisories. The shell Stop
95
+ // entry delegates to its sibling Python adapter; decisions stay in core.py.
96
+ { path: 'scripts/workflow-advisories/core.py', kind: 'script', mode: 0o644 },
97
+ { path: '.claude/hooks/recon-size-hint.py', kind: 'hook', mode: 0o755 },
98
+ { path: '.claude/hooks/baseline-capture-hint.py', kind: 'hook', mode: 0o755 },
99
+ { path: '.claude/hooks/pre-refactor-sweep.py', kind: 'hook', mode: 0o755 },
100
+ { path: '.claude/hooks/typecheck-on-stop.py', kind: 'hook', mode: 0o755 },
101
+ { path: '.claude/hooks/typecheck-on-stop.sh', kind: 'hook', mode: 0o755 },
92
102
  { path: '.claude/hooks/drift-guard.py', kind: 'hook', mode: 0o755 },
93
103
  // SessionStart skill-freshness drift-hint (audit-skills names it). For each
94
104
  // <skill>/SOURCES.txt it flags sources newer in git than the SKILL.md. Imports