create-pathfinder 4.0.0 → 4.2.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/CLAUDE.md CHANGED
@@ -103,4 +103,5 @@ An adapter carries the canonical skill's frontmatter and a pointer to it, and no
103
103
  - `role` — explicitly override the role the lifecycle would assume
104
104
  - `whereami` — report a compact read-only snapshot of the current session
105
105
  - `skillsmith` — teach and create small local skills
106
+ - `hooksmith` — turn a described guarantee into one verified hook for the active harness
106
107
  - `setup-tracker` — choose the canonical ticket store when it is not local Markdown
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pathfinder",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "Install Pathfinder's ticket-first, human-in-the-loop workflow kit into a Git repository.",
5
5
  "keywords": [
6
6
  "pathfinder",
@@ -0,0 +1,265 @@
1
+ ---
2
+ name: hooksmith
3
+ description: Turn a plain-English automation or deterministic guarantee into the smallest working, correctly scoped, verified hook for the active AI coding harness.
4
+ argument-hint: [what the hook must guarantee or do]
5
+ ---
6
+
7
+ # Hooksmith
8
+
9
+ A skill is reusable reasoning. A hook is deterministic lifecycle automation.
10
+
11
+ > A skill asks. A hook guarantees.
12
+
13
+ Start from the behavior the human describes. Do not start from a harness, an
14
+ event name, a settings file, or a script.
15
+
16
+ A hook is trusted executable configuration: it runs automatically, with the
17
+ human's credentials, every time its trigger matches, and nothing asks first.
18
+ Build one the way you would accept a change to CI.
19
+
20
+ ## 1. Decide whether this should be a hook
21
+
22
+ Answer before anything else, out loud:
23
+
24
+ - deterministic lifecycle behavior — a guarantee, or an automatic reaction to
25
+ something the session did → hook candidate
26
+ - judgment or reasoning workflow → a skill; use `skillsmith`
27
+ - something a human runs when they want it → a script, a command, or a Make
28
+ target
29
+
30
+ Example: "never allow edits to approved Feature specs." Whether the rule holds
31
+ cannot depend on an agent remembering it, and the answer is a path comparison.
32
+ That is a hook.
33
+
34
+ Anti-example: "review whether this implementation actually satisfies the
35
+ Feature." That is a reading of intent against evidence. No trigger expresses it,
36
+ and a hook that approximates it will block correct work and pass incorrect work.
37
+ It belongs in a reviewer skill.
38
+
39
+ Say which of the three the request is. Concluding "this is not a hook" is a
40
+ successful outcome of this skill — stop there and name what to use instead.
41
+
42
+ ## 2. Describe the hook without naming a harness
43
+
44
+ Write the behavior down in these terms, and only these, before looking at any
45
+ harness:
46
+
47
+ - **lifecycle moment** — what point in the session's life this attaches to
48
+ - **trigger** — what narrows it to the cases the human means
49
+ - **action** — what actually happens when it fires
50
+ - **blocking or non-blocking** — must it *stop* something, or only react to it
51
+ - **ownership and scope** — whose sessions this fires in
52
+ - **failure behavior** — what happens when the hook itself errors
53
+ - **verification** — what would prove it works, and what would prove it does not
54
+
55
+ The worked example becomes:
56
+
57
+ > pre-action, blocking file-mutation guard on the approved Feature spec paths,
58
+ > owned by the project or the kit, failing closed only if the check is trivially
59
+ > correct, verified by one edit that must pass and one that must be refused.
60
+
61
+ That paragraph is the portable contract: what the hook must guarantee, stated in
62
+ terms no harness owns. What follows it is four different things, and keeping
63
+ them apart is the point of the rest of this skill — translation onto one
64
+ harness's primitives (step 4), implementation of the action (step 5–6),
65
+ configuration that installs it (step 7), and verification that it actually
66
+ behaves that way (step 8). Only the first is translation.
67
+
68
+ Keep this description in the report. It is what survives a change of harness.
69
+
70
+ ## 3. Identify the active harness and read what it can actually do
71
+
72
+ Name the harness this session is running in. Then inspect its current hook or
73
+ lifecycle capabilities and conventions — its documentation, its configuration
74
+ files, what it already has configured — rather than assuming they match another
75
+ harness or match what you remember.
76
+
77
+ Confirm four things before translating anything:
78
+
79
+ - which lifecycle moments it exposes, and their real names
80
+ - how a trigger is expressed, and how precisely it can narrow
81
+ - whether the moment you need can **block**, or only observe
82
+ - where the configuration lives, and what scopes it offers
83
+
84
+ If the harness exposes no equivalent lifecycle primitive, or exposes one that
85
+ cannot block when the guarantee requires blocking, say so plainly and stop. A
86
+ guarantee the harness cannot enforce is not a hook — it is a rule, a skill, or a
87
+ check the human runs. Do not build something that looks like enforcement and is
88
+ not.
89
+
90
+ ### Claude Code
91
+
92
+ The first harness this skill supports concretely, and a reference for what a
93
+ translation looks like — not the definition of a hook.
94
+
95
+ Under Claude Code, read the current documentation before relying on any detail:
96
+
97
+ - https://code.claude.com/docs/en/hooks.md — reference
98
+ - https://code.claude.com/docs/en/hooks-guide.md — examples
99
+
100
+ That contract changes faster than any skill describing it, which is why nothing
101
+ here restates its event list. Once confirmed against the fetched page, use its
102
+ own vocabulary directly — the lifecycle event names, the matcher and condition
103
+ syntax, the handler types, the settings and plugin locations, the way a handler
104
+ receives input and returns a decision. Naming them is correct here and wrong
105
+ anywhere else.
106
+
107
+ Under any other harness, use that harness's native equivalent on the same terms:
108
+ read its documentation first, then speak its vocabulary.
109
+
110
+ #### The worked example, realized here
111
+
112
+ The step 2 description — *pre-action, blocking file-mutation guard on the
113
+ approved Feature spec paths, owned by the project or the kit* — becomes, under
114
+ this harness and no other:
115
+
116
+ | Portable term | Claude Code realization |
117
+ | --- | --- |
118
+ | lifecycle moment, pre-action | a `PreToolUse` event, which fires before the tool call and can block it |
119
+ | trigger | a matcher on the file-writing tools, narrowed further by an `if` condition on the spec paths |
120
+ | blocking | a denying permission decision, or exit code 2, returned by the handler |
121
+ | ownership, project or kit | `.claude/settings.json` in the repository, or the plugin's `hooks/hooks.json` |
122
+
123
+ Every cell on the right is this harness's dialect. Another harness answers the
124
+ same four questions with different names, or cannot answer one of them at all —
125
+ which is what step 3 is for. Treat the table as an illustration of the shape of
126
+ a translation, and confirm each cell against the documentation you just fetched
127
+ before relying on it; the names and the decision format change.
128
+
129
+ ## 4. Translate, narrowest first
130
+
131
+ Map the description from step 2 onto what step 3 found:
132
+
133
+ - the lifecycle moment → the harness's nearest real moment, blocking-capable if
134
+ the guarantee needs it
135
+ - the trigger → the narrowest expression the harness supports; a trigger that
136
+ fires on everything is a noise problem that hides the cases the hook exists for
137
+ - ownership → the smallest scope that delivers the guarantee. Scope is a blast
138
+ radius: a guarantee one person wants does not belong in everyone's
139
+ configuration, and a project guarantee does not belong in something that ships
140
+ to other people's projects.
141
+
142
+ State every place the translation is imperfect. The guarantee is only as strong
143
+ as the trigger, so say plainly what it does not catch.
144
+
145
+ For a hook that ships with Pathfinder, prefer what the installation environment
146
+ already guarantees. A hook needing a runtime, a package manager, or a dependency
147
+ the kit does not already assume is a hook Pathfinder cannot ship.
148
+
149
+ ### Two approval gates
150
+
151
+ They are independent, and either one can apply:
152
+
153
+ - **Location.** Writing outside the repository — user settings, machine
154
+ configuration — needs approval.
155
+ - **Blast radius.** Installing or changing a hook whose behavior reaches beyond
156
+ the human's own private, local configuration needs approval, wherever the file
157
+ sits. That covers a project hook committed to the repository and a hook
158
+ shipped by Pathfinder or a plugin: both make other people's sessions behave
159
+ differently, and neither is yours to decide.
160
+
161
+ Building and testing a candidate hook in a scratch or local-only place is not
162
+ gated. Ask at the moment of installing or modifying shared automatic behavior,
163
+ not before experimenting.
164
+
165
+ ## 5. Choose the smallest reliable action
166
+
167
+ Prefer, in order: what the harness's own handler types give you for free; a
168
+ short shell command; a small script in a runtime the environment already has.
169
+ Choose a language because it is present and appropriate, not out of habit.
170
+
171
+ The action runs in the harness's environment, not the human's shell. Do not
172
+ assume a project's virtual environment, `PATH`, or dependencies are active. If
173
+ the hook must run a project command, run it as the human would type it, in the
174
+ project directory, and confirm it resolves there — a hook that always fails
175
+ looks exactly like a hook that always works.
176
+
177
+ ## 6. Decide failure and recursion behavior
178
+
179
+ State the choice explicitly:
180
+
181
+ - **fail open** — an unexpected error allows the action. The default. A broken
182
+ hook must not brick a session.
183
+ - **fail closed** — an unexpected error blocks. Only for a real safety boundary,
184
+ and only when the check is simple enough to be obviously correct.
185
+
186
+ A hook attached to the end of a turn must not block the very ending it caused.
187
+ Find the harness's re-entry signal and exit early when it is set. A hook that
188
+ loops is worse than no hook.
189
+
190
+ Keep the action fast. It runs on every match.
191
+
192
+ ## 7. Merge, never overwrite
193
+
194
+ Read the existing configuration before writing. Add this hook to what is already
195
+ there; leave every other moment and every other hook intact. Losing someone's
196
+ unrelated hook is a silent, hard-to-notice failure.
197
+
198
+ ## 8. Prove it, both ways
199
+
200
+ Run the hook, the way this harness runs hooks. Verification is where the
201
+ abstraction ends: use the harness's real invocation, real input shape, and real
202
+ signals.
203
+
204
+ Verify both sides of the boundary:
205
+
206
+ - a case that must be **allowed** — it passes
207
+ - a case that must **trigger, block, or react** — it fires, with a reason a
208
+ reader would understand
209
+
210
+ A hook that responds identically to both is broken, whichever way it responds.
211
+ Fix it and re-run before reporting anything.
212
+
213
+ Never report success from reading the code. A hook that always blocks and a hook
214
+ that always allows are indistinguishable on the page.
215
+
216
+ ## 9. Report
217
+
218
+ State:
219
+
220
+ - the harness-independent description from step 2
221
+ - the harness, and what its capabilities were confirmed to be
222
+ - what was created, and where it lives
223
+ - what triggers it, what it guarantees, and what it does not catch
224
+ - what was actually executed, and what each run proved
225
+ - the one line the human would edit to adjust it
226
+ - how the human can trigger it themselves
227
+ - that the hook runs automatically with their credentials, and should be
228
+ reviewed like any other executable configuration
229
+
230
+ Say plainly what could not be verified, and name anything the harness could not
231
+ enforce.
232
+
233
+ ## Rules
234
+
235
+ - One hook per invocation. Finish it, verify it, report it, stop.
236
+ - Do not treat one harness's event names, configuration files, handler types, or
237
+ input and output contracts as universal. They are that harness's dialect.
238
+ - Do not build a cross-harness abstraction layer, runtime, adapter framework,
239
+ registry, or orchestration system. The abstraction is the reasoning in steps
240
+ 1–2; everything written to disk is native to one harness.
241
+ - Do not write outside the repository — user settings, machine configuration —
242
+ without approval.
243
+ - Do not install or change a hook that affects anyone but the human running this
244
+ session — a project hook in the repository, a hook shipped by Pathfinder or a
245
+ plugin — without approval. Experimenting locally is free; making shared
246
+ automatic behavior is not.
247
+ - Do not add a dependency to make a hook possible. Choose a smaller hook.
248
+ - Do not weaken an existing hook to make a new one fit.
249
+ - Do not report a hook as working without having run it.
250
+
251
+ ## Stop conditions
252
+
253
+ Stop and hand the decision back when:
254
+
255
+ - the behavior turns out to need judgment, and belongs in a skill
256
+ - the active harness has no equivalent primitive, or none that can block when
257
+ blocking is the point
258
+ - its documentation contradicts what the hook would rely on
259
+ - the guarantee cannot be expressed by any available trigger
260
+ - the correct scope is one the human has not approved, or is shared and the
261
+ human has not approved installing it
262
+ - verification cannot distinguish the allowed case from the blocked one
263
+
264
+ Keep version one small and easy to delete. A hook nobody can explain is a hook
265
+ nobody can trust.
@@ -0,0 +1,199 @@
1
+ /**
2
+ * What a human does to turn a generated handler on, and how they turn it off.
3
+ *
4
+ * Pathfinder generates the handler and stops. Activation is native harness
5
+ * configuration in a file Pathfinder does not own, does not read, and never
6
+ * writes — so the only thing this package can offer is the exact text to paste
7
+ * and an honest account of what happens if nobody pastes it.
8
+ *
9
+ * That is why this module is prose and nothing else. It imports no `node:fs`,
10
+ * holds no path to a settings file it might one day open, and returns strings.
11
+ * `test/hooks.test.mjs` asserts both halves of that: this is the one module
12
+ * under `src/` allowed to name a settings file, and it is allowed to precisely
13
+ * because it cannot act on one.
14
+ *
15
+ * The fragment is built from the registry's stable handler path rather than
16
+ * written out, so a fragment that no longer matches what the installer
17
+ * generates is not a thing this package can print.
18
+ *
19
+ * The handler ships mode 0644 and is not executable, whatever its shebang
20
+ * says, so the command runs it through Node. `$CLAUDE_PROJECT_DIR` is what
21
+ * keeps the activation correct from a subdirectory and inside a worktree.
22
+ */
23
+
24
+ import { HARNESSES } from "./harnesses/index.mjs";
25
+ import { hookPath, hooksFor } from "./harnesses/hook.mjs";
26
+
27
+ /** Where a human puts the fragment. The first is the documented default. */
28
+ export const DEFAULT_SURFACE = ".claude/settings.local.json";
29
+ export const SHARED_SURFACE = ".claude/settings.json";
30
+
31
+ /**
32
+ * Does this harness declare that it can run a handler at all?
33
+ *
34
+ * Read from the registry — `hooksDir` is the field that says "this tool has a
35
+ * place handlers go" — rather than from the length of `hooks`, which is the
36
+ * thing being checked. Codex answers no, and that is a correct answer, not a
37
+ * gap: it gets no handler and no activation prose.
38
+ */
39
+ export function supportsActivation(harness) {
40
+ return typeof harness?.hooksDir === "string" && harness.hooksDir !== "";
41
+ }
42
+
43
+ /**
44
+ * The one handler a capable harness activates, or null for a harness that has
45
+ * no place to put one.
46
+ *
47
+ * v1 ships exactly one orientation handler per capable harness, and this is
48
+ * where that assumption is stated instead of assumed. `hooksFor(harness)[0]`
49
+ * reads identically whether the registry holds one handler, none, or three —
50
+ * which is precisely the property that makes it unsafe: a registry that lost
51
+ * its hook would print no fragment and a registry that grew a second one would
52
+ * silently document the first. Both are wrong, and both are silent.
53
+ *
54
+ * So a capable harness that does not resolve to exactly one handler throws.
55
+ * Callers that legitimately have nothing to say — a Codex run — are served by
56
+ * the null, not by the throw.
57
+ *
58
+ * @throws {Error} when the registry and this assumption disagree.
59
+ */
60
+ export function activationHandler(harness) {
61
+ const hooks = hooksFor(harness);
62
+
63
+ if (!supportsActivation(harness)) {
64
+ if (hooks.length > 0) {
65
+ throw new Error(
66
+ `activation: ${harness?.id ?? "harness"} declares ${hooks.length} handler(s) ` +
67
+ "but no hooksDir to put them in",
68
+ );
69
+ }
70
+ return null;
71
+ }
72
+
73
+ if (hooks.length !== 1) {
74
+ throw new Error(
75
+ `activation: ${harness.id} supports session orientation, so exactly one handler ` +
76
+ `must resolve; the registry gives ${hooks.length}`,
77
+ );
78
+ }
79
+
80
+ return hooks[0];
81
+ }
82
+
83
+ /**
84
+ * Every harness this build can print activation for, with its handler.
85
+ *
86
+ * Exported for `validate-kit.py`, which needs the set rather than one harness:
87
+ * checking the guide against `claude-code` alone would pass unchanged on the
88
+ * day a second capable harness is added and left undocumented.
89
+ *
90
+ * Throws for the same reasons `activationHandler` does, which is the point —
91
+ * a validator that enumerates this cannot pass vacuously.
92
+ */
93
+ export function activationTargets() {
94
+ return HARNESSES.map((harness) => ({ harness, hook: activationHandler(harness) })).filter(
95
+ ({ hook }) => hook !== null,
96
+ );
97
+ }
98
+
99
+ /**
100
+ * The command that runs one handler, quoted for a shell.
101
+ *
102
+ * Exported for the documentation build and the tests, which check the site and
103
+ * the installer against one spelling rather than two.
104
+ */
105
+ export function activationCommand(harness, hook) {
106
+ return `node "$CLAUDE_PROJECT_DIR/${hookPath(harness, hook)}"`;
107
+ }
108
+
109
+ /**
110
+ * The native fragment, as the lines a person pastes.
111
+ *
112
+ * The matcher is deliberately omitted, so one entry covers the lifecycle
113
+ * sources without naming any of them.
114
+ *
115
+ * `startup`, `resume`, `clear`, and `compact` were each observed live per
116
+ * source against a real install, not inferred from `startup`. `fork` was not:
117
+ * the reachable surface, `claude --resume --fork-session`, emits
118
+ * `source: "resume"`, so it was covered by driving the handler with a `fork`
119
+ * payload instead. The handler branches on the source for nothing, which is
120
+ * what bounds what that weaker evidence can cost.
121
+ */
122
+ export function activationFragmentLines(harness, hook) {
123
+ return [
124
+ "{",
125
+ ' "hooks": {',
126
+ ' "SessionStart": [',
127
+ " {",
128
+ ' "hooks": [',
129
+ " {",
130
+ ' "type": "command",',
131
+ ` "command": ${JSON.stringify(activationCommand(harness, hook))}`,
132
+ " }",
133
+ " ]",
134
+ " }",
135
+ " ]",
136
+ " }",
137
+ "}",
138
+ ];
139
+ }
140
+
141
+ /**
142
+ * The whole activation note for one harness, as undecorated lines.
143
+ *
144
+ * Returned as text rather than printed, so the two renderings in `cli.mjs`
145
+ * decorate one set of sentences instead of each keeping their own copy — the
146
+ * same reason `outcome.mjs` exists. Empty for a harness with no handler, which
147
+ * is how a Codex run says nothing at all about activation.
148
+ *
149
+ * `dryRun` changes tense and nothing else. A dry run has written no handler, so
150
+ * a note that says "the handler is inert" is describing a file that is not
151
+ * there — the one sentence on that screen a reader could act on and be wrong
152
+ * about. The fragment itself is identical in both modes, because the path it
153
+ * points at is the path the real run will write.
154
+ *
155
+ * The unactivated answer is stated in the same breath as the fragment, because
156
+ * a reader who decides not to paste it is owed the consequence on the same
157
+ * screen: no automatic orientation, and `/whereami` on demand.
158
+ */
159
+ export function activationLines(harness, { dryRun = false } = {}) {
160
+ const hook = activationHandler(harness);
161
+ if (!hook) return [];
162
+
163
+ const opening = dryRun
164
+ ? [
165
+ "The handler would be inert. Once installed it would run only if you add",
166
+ `this to ${DEFAULT_SURFACE}:`,
167
+ ]
168
+ : [
169
+ "The handler is inert. It runs only if you add this to",
170
+ `${DEFAULT_SURFACE}:`,
171
+ ];
172
+
173
+ const closing = dryRun
174
+ ? [
175
+ "Without the fragment there would be no automatic orientation. Nothing else",
176
+ "would change: run /whereami whenever you want the same picture.",
177
+ "To remove it later, delete the handler and drop the fragment. Either order",
178
+ "is fine, and so is doing only one: an unreferenced handler never runs.",
179
+ "A fragment for a missing handler is a no-op that cannot block a session.",
180
+ ]
181
+ : [
182
+ "Without the fragment there is no automatic orientation. Nothing else",
183
+ "changes: run /whereami whenever you want the same picture.",
184
+ "To remove it later, delete the handler and drop the fragment. Either order",
185
+ "is fine, and so is doing only one: an unreferenced handler never runs.",
186
+ "A fragment for a missing handler is a no-op that cannot block a session.",
187
+ ];
188
+
189
+ return [
190
+ ...opening,
191
+ "",
192
+ ...activationFragmentLines(harness, hook),
193
+ "",
194
+ `${DEFAULT_SURFACE} keeps the choice yours and`,
195
+ `per-machine. Use ${SHARED_SURFACE} only to turn it on for everyone`,
196
+ "who clones the repository.",
197
+ ...closing,
198
+ ];
199
+ }