carrick 0.3.82 → 0.3.83

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.
Files changed (73) hide show
  1. package/README.md +46 -19
  2. package/bin/carrick.mjs +45 -2
  3. package/dist/contract.d.ts +17 -0
  4. package/dist/contract.js.map +1 -1
  5. package/dist/hook/apply-patch.d.ts +13 -0
  6. package/dist/hook/apply-patch.js +100 -0
  7. package/dist/hook/apply-patch.js.map +1 -0
  8. package/dist/hook/post-edit.d.ts +30 -1
  9. package/dist/hook/post-edit.js +95 -24
  10. package/dist/hook/post-edit.js.map +1 -1
  11. package/dist/hook/reuse.d.ts +97 -0
  12. package/dist/hook/reuse.js +245 -0
  13. package/dist/hook/reuse.js.map +1 -0
  14. package/dist/hook/stop.d.ts +3 -0
  15. package/dist/hook/stop.js +76 -0
  16. package/dist/hook/stop.js.map +1 -0
  17. package/dist/hook/user-prompt.d.ts +9 -0
  18. package/dist/hook/user-prompt.js +79 -0
  19. package/dist/hook/user-prompt.js.map +1 -0
  20. package/dist/init/codex.d.ts +51 -0
  21. package/dist/init/codex.js +167 -0
  22. package/dist/init/codex.js.map +1 -0
  23. package/dist/init/connect.d.ts +13 -0
  24. package/dist/init/connect.js +21 -15
  25. package/dist/init/connect.js.map +1 -1
  26. package/dist/init/doctor.d.ts +36 -0
  27. package/dist/init/doctor.js +97 -2
  28. package/dist/init/doctor.js.map +1 -1
  29. package/dist/init/files.d.ts +15 -0
  30. package/dist/init/files.js +20 -1
  31. package/dist/init/files.js.map +1 -1
  32. package/dist/init/outdated.d.ts +54 -0
  33. package/dist/init/outdated.js +175 -0
  34. package/dist/init/outdated.js.map +1 -0
  35. package/dist/init/output.d.ts +35 -3
  36. package/dist/init/output.js +115 -23
  37. package/dist/init/output.js.map +1 -1
  38. package/dist/init/projects.d.ts +27 -13
  39. package/dist/init/projects.js +48 -50
  40. package/dist/init/projects.js.map +1 -1
  41. package/dist/init/remove.d.ts +0 -2
  42. package/dist/init/remove.js +61 -15
  43. package/dist/init/remove.js.map +1 -1
  44. package/dist/init/repos.d.ts +34 -0
  45. package/dist/init/repos.js +77 -0
  46. package/dist/init/repos.js.map +1 -1
  47. package/dist/init/run.d.ts +73 -6
  48. package/dist/init/run.js +357 -94
  49. package/dist/init/run.js.map +1 -1
  50. package/dist/init/settings.d.ts +20 -0
  51. package/dist/init/settings.js +42 -4
  52. package/dist/init/settings.js.map +1 -1
  53. package/dist/init/task-skills.d.ts +27 -0
  54. package/dist/init/task-skills.js +64 -1
  55. package/dist/init/task-skills.js.map +1 -1
  56. package/dist/init/workspace-file.d.ts +73 -0
  57. package/dist/init/workspace-file.js +173 -0
  58. package/dist/init/workspace-file.js.map +1 -0
  59. package/package.json +6 -6
  60. package/plugin/hooks/hooks.json +11 -0
  61. package/sidecar/dist/src/capture/check-classify.d.ts +10 -1
  62. package/sidecar/dist/src/capture/check-classify.js +66 -8
  63. package/sidecar/dist/src/capture/check-deep.d.ts +16 -4
  64. package/sidecar/dist/src/capture/check-deep.js +21 -17
  65. package/sidecar/dist/src/capture/check-fields.d.ts +83 -0
  66. package/sidecar/dist/src/capture/check-fields.js +259 -0
  67. package/sidecar/dist/src/capture/check-probe.d.ts +21 -1
  68. package/sidecar/dist/src/capture/check-probe.js +39 -0
  69. package/sidecar/dist/src/capture/check.js +9 -2
  70. package/templates/skills/carrick-census.md +18 -9
  71. package/templates/skills/carrick-drift.md +7 -1
  72. package/templates/skills/carrick-impact.md +3 -1
  73. package/templates/skills/carrick-reuse.md +43 -26
@@ -0,0 +1,97 @@
1
+ /** One function the working tree declares and the index does not. */
2
+ export type NewFunction = {
3
+ /** As the source spells it. */
4
+ name: string;
5
+ /** Repo-relative path of the file that declares it. */
6
+ file: string;
7
+ /** The commit the index this was compared against was taken at. */
8
+ indexCommit: string;
9
+ };
10
+ /** A session's accumulated record. */
11
+ export type SessionRecord = {
12
+ /** Every new function this session has seen, in the order it saw them. */
13
+ found: NewFunction[];
14
+ /** `file::name` for the ones a nudge has already listed. */
15
+ nudged: string[];
16
+ /** RFC 3339, when this file was last written. Drives the prune below. */
17
+ updated: string;
18
+ };
19
+ /**
20
+ * How many entries one nudge names.
21
+ *
22
+ * `find_similar` takes twenty, so a longer list would be cut by the agent
23
+ * making the call. Cutting it here keeps the cut in one place and keeps the
24
+ * line readable.
25
+ */
26
+ export declare const MAX_ENTRIES = 20;
27
+ /** Where one session's record lives. */
28
+ export declare function sessionFile(sessionId: string, home?: string): string | null;
29
+ /** The directory `carrick remove` deletes. */
30
+ export declare function sessionsDir(home?: string): string;
31
+ /** A session's record, or an empty one. Never throws. */
32
+ export declare function readSession(sessionId: string, home?: string): SessionRecord;
33
+ /**
34
+ * Drop session files nothing will read again.
35
+ *
36
+ * A session id is never reused, so every file here is dead the moment its
37
+ * conversation ends and only the clock can say when that was. Without this the
38
+ * directory grows one file per session forever.
39
+ */
40
+ export declare function prune(home?: string, now?: number): void;
41
+ /**
42
+ * Delete every session record on this machine, for `carrick remove`.
43
+ *
44
+ * Returns how many files went, so the command can stay silent about a machine
45
+ * that never recorded one. The remover lives beside the writer, which is the
46
+ * rule every other half of `carrick remove` follows (carrick#1034).
47
+ */
48
+ export declare function removeSessions(home?: string): number;
49
+ /**
50
+ * Add what one edit found, keeping the first sighting of each function.
51
+ *
52
+ * Deduplicated on file and name: an agent edits one file several times in a
53
+ * task, and every one of those edits re-reports the same new function.
54
+ */
55
+ export declare function record(sessionId: string, found: NewFunction[], home?: string): SessionRecord;
56
+ /** What a nudge has not yet named, oldest first. */
57
+ export declare function pending(record: SessionRecord): NewFunction[];
58
+ /** Mark entries as named, so the next stop of the same task says nothing. */
59
+ export declare function markNudged(sessionId: string, entries: NewFunction[], home?: string): void;
60
+ /** What one delivery of the nudge said, for the hook that delivered it. */
61
+ export type Drained = {
62
+ /** The line to hand the model. */
63
+ line: string;
64
+ /** How many functions it names. */
65
+ named: number;
66
+ /** How many the session has recorded in all, named or not. */
67
+ found: number;
68
+ };
69
+ /**
70
+ * The nudge for one session, marked as spoken, or nothing to say (carrick#1335).
71
+ *
72
+ * The one place the sequence lives, because two hooks deliver it: Claude Code's
73
+ * `Stop`, and Codex's `UserPromptSubmit`, which is the only non-blocking
74
+ * model-visible event Codex gives us. Both read the same store, speak the same
75
+ * line and mark the same set; only the event name on the wire differs.
76
+ *
77
+ * The marking happens here, before the caller writes anything, and that order
78
+ * is the point: a nudge that is written and not recorded fires again on the
79
+ * next stop, and a task is several stops.
80
+ */
81
+ export declare function drain(sessionId: string, home?: string): Drained | null;
82
+ /**
83
+ * The one line the Stop hook hands the model.
84
+ *
85
+ * Three things it has to carry, and each is load-bearing:
86
+ *
87
+ * * the names, so the call can be made without another read;
88
+ * * `description` entries rather than `name`, because `find_similar` resolves
89
+ * a `name` against the index and these are exactly the functions it does not
90
+ * hold — a `name` entry for one of them comes back as an error;
91
+ * * the two limits, because they decide what an empty answer means. The
92
+ * comparison is against the index's commit, which is this repo's default
93
+ * branch as the last scan saw it, so a function written earlier on this
94
+ * branch is listed here too. And the index holds no body hash: the match is
95
+ * on what a function is described as doing, not on its source.
96
+ */
97
+ export declare function nudge(entries: NewFunction[]): string;
@@ -0,0 +1,245 @@
1
+ // What one session has added that the index does not hold, and the one line
2
+ // the agent is told about it at the end of the task (carrick#1330).
3
+ //
4
+ // The measured problem: a reuse check that waits to be remembered does not
5
+ // run. Prose alone reached the index in none of five runs; a hook reached it
6
+ // in all five. So the check has to be delivered, not documented — but delivery
7
+ // costs a model turn, and most edits add no function, so it cannot ride the
8
+ // edit. The split is: the post-edit hook RECORDS silently (it already re-checks
9
+ // the edited file, and the list of new functions comes back in that answer),
10
+ // and the Stop hook SPEAKS once, with everything the task accumulated.
11
+ //
12
+ // The store is a file per session under `~/.carrick/sessions`, outside every
13
+ // repository: a scratch file about one conversation is not a thing to put in a
14
+ // user's tree, and a workspace can hold several repos while a session is one.
15
+ // Nothing here ever throws at its caller — an edit and a stop must not fail
16
+ // because a state file could not be written.
17
+ //
18
+ // Reference: `docs/reference/task-skills.md`, "The reuse nudge".
19
+ import fs from "node:fs";
20
+ import os from "node:os";
21
+ import path from "node:path";
22
+ /**
23
+ * How many entries one nudge names.
24
+ *
25
+ * `find_similar` takes twenty, so a longer list would be cut by the agent
26
+ * making the call. Cutting it here keeps the cut in one place and keeps the
27
+ * line readable.
28
+ */
29
+ export const MAX_ENTRIES = 20;
30
+ /** Session files older than this are removed on the next write. */
31
+ const KEEP_DAYS = 7;
32
+ /** A file name a session id can never escape from. */
33
+ function safeId(sessionId) {
34
+ return /^[A-Za-z0-9._-]{1,128}$/.test(sessionId) ? sessionId : null;
35
+ }
36
+ /** Where one session's record lives. */
37
+ export function sessionFile(sessionId, home = os.homedir()) {
38
+ const id = safeId(sessionId);
39
+ return id === null ? null : path.join(home, ".carrick", "sessions", `${id}.json`);
40
+ }
41
+ /** The directory `carrick remove` deletes. */
42
+ export function sessionsDir(home = os.homedir()) {
43
+ return path.join(home, ".carrick", "sessions");
44
+ }
45
+ /** The key a `nudged` list holds an entry under. */
46
+ function key(entry) {
47
+ return `${entry.file}::${entry.name}`;
48
+ }
49
+ /** A session's record, or an empty one. Never throws. */
50
+ export function readSession(sessionId, home) {
51
+ const empty = { found: [], nudged: [], updated: "" };
52
+ const file = sessionFile(sessionId, home);
53
+ if (file === null)
54
+ return empty;
55
+ let body;
56
+ try {
57
+ body = fs.readFileSync(file, "utf8");
58
+ }
59
+ catch {
60
+ return empty;
61
+ }
62
+ try {
63
+ const parsed = JSON.parse(body);
64
+ if (typeof parsed !== "object" || parsed === null)
65
+ return empty;
66
+ const record = parsed;
67
+ return {
68
+ found: Array.isArray(record.found)
69
+ ? record.found.filter((entry) => typeof entry?.name === "string" && typeof entry?.file === "string")
70
+ : [],
71
+ nudged: Array.isArray(record.nudged)
72
+ ? record.nudged.filter((entry) => typeof entry === "string")
73
+ : [],
74
+ updated: typeof record.updated === "string" ? record.updated : "",
75
+ };
76
+ }
77
+ catch {
78
+ // A half-written file is a file to replace, not a reason to stop: nobody
79
+ // edits this by hand and nothing downstream depends on its history.
80
+ return empty;
81
+ }
82
+ }
83
+ function write(sessionId, record, home) {
84
+ const file = sessionFile(sessionId, home);
85
+ if (file === null)
86
+ return;
87
+ try {
88
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
89
+ fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
90
+ }
91
+ catch {
92
+ // Nothing to do and nothing to say: the nudge is a nicety and the edit it
93
+ // rides behind is not.
94
+ }
95
+ prune(home);
96
+ }
97
+ /**
98
+ * Drop session files nothing will read again.
99
+ *
100
+ * A session id is never reused, so every file here is dead the moment its
101
+ * conversation ends and only the clock can say when that was. Without this the
102
+ * directory grows one file per session forever.
103
+ */
104
+ export function prune(home, now = Date.now()) {
105
+ const dir = sessionsDir(home);
106
+ let names;
107
+ try {
108
+ names = fs.readdirSync(dir);
109
+ }
110
+ catch {
111
+ return;
112
+ }
113
+ const cutoff = now - KEEP_DAYS * 24 * 60 * 60 * 1000;
114
+ for (const name of names) {
115
+ const file = path.join(dir, name);
116
+ try {
117
+ if (fs.statSync(file).mtimeMs < cutoff)
118
+ fs.unlinkSync(file);
119
+ }
120
+ catch {
121
+ // A file another process is removing at the same moment is already gone.
122
+ }
123
+ }
124
+ }
125
+ /**
126
+ * Delete every session record on this machine, for `carrick remove`.
127
+ *
128
+ * Returns how many files went, so the command can stay silent about a machine
129
+ * that never recorded one. The remover lives beside the writer, which is the
130
+ * rule every other half of `carrick remove` follows (carrick#1034).
131
+ */
132
+ export function removeSessions(home) {
133
+ const dir = sessionsDir(home);
134
+ let names;
135
+ try {
136
+ names = fs.readdirSync(dir);
137
+ }
138
+ catch {
139
+ return 0;
140
+ }
141
+ let removed = 0;
142
+ for (const name of names) {
143
+ try {
144
+ fs.unlinkSync(path.join(dir, name));
145
+ removed += 1;
146
+ }
147
+ catch {
148
+ // Already gone, or not ours to remove.
149
+ }
150
+ }
151
+ try {
152
+ fs.rmdirSync(dir);
153
+ }
154
+ catch {
155
+ // Something else is in it; the records are gone either way.
156
+ }
157
+ return removed;
158
+ }
159
+ /**
160
+ * Add what one edit found, keeping the first sighting of each function.
161
+ *
162
+ * Deduplicated on file and name: an agent edits one file several times in a
163
+ * task, and every one of those edits re-reports the same new function.
164
+ */
165
+ export function record(sessionId, found, home) {
166
+ const current = readSession(sessionId, home);
167
+ const seen = new Set(current.found.map(key));
168
+ for (const entry of found) {
169
+ if (seen.has(key(entry)))
170
+ continue;
171
+ seen.add(key(entry));
172
+ current.found.push(entry);
173
+ }
174
+ current.updated = new Date().toISOString();
175
+ write(sessionId, current, home);
176
+ return current;
177
+ }
178
+ /** What a nudge has not yet named, oldest first. */
179
+ export function pending(record) {
180
+ const nudged = new Set(record.nudged);
181
+ return record.found.filter((entry) => !nudged.has(key(entry)));
182
+ }
183
+ /** Mark entries as named, so the next stop of the same task says nothing. */
184
+ export function markNudged(sessionId, entries, home) {
185
+ const current = readSession(sessionId, home);
186
+ const nudged = new Set(current.nudged);
187
+ for (const entry of entries)
188
+ nudged.add(key(entry));
189
+ current.nudged = [...nudged];
190
+ current.updated = new Date().toISOString();
191
+ write(sessionId, current, home);
192
+ }
193
+ /**
194
+ * The nudge for one session, marked as spoken, or nothing to say (carrick#1335).
195
+ *
196
+ * The one place the sequence lives, because two hooks deliver it: Claude Code's
197
+ * `Stop`, and Codex's `UserPromptSubmit`, which is the only non-blocking
198
+ * model-visible event Codex gives us. Both read the same store, speak the same
199
+ * line and mark the same set; only the event name on the wire differs.
200
+ *
201
+ * The marking happens here, before the caller writes anything, and that order
202
+ * is the point: a nudge that is written and not recorded fires again on the
203
+ * next stop, and a task is several stops.
204
+ */
205
+ export function drain(sessionId, home) {
206
+ const current = readSession(sessionId, home);
207
+ const waiting = pending(current);
208
+ if (waiting.length === 0)
209
+ return null;
210
+ const line = nudge(waiting);
211
+ markNudged(sessionId, waiting, home);
212
+ return { line, named: waiting.length, found: current.found.length };
213
+ }
214
+ /**
215
+ * The one line the Stop hook hands the model.
216
+ *
217
+ * Three things it has to carry, and each is load-bearing:
218
+ *
219
+ * * the names, so the call can be made without another read;
220
+ * * `description` entries rather than `name`, because `find_similar` resolves
221
+ * a `name` against the index and these are exactly the functions it does not
222
+ * hold — a `name` entry for one of them comes back as an error;
223
+ * * the two limits, because they decide what an empty answer means. The
224
+ * comparison is against the index's commit, which is this repo's default
225
+ * branch as the last scan saw it, so a function written earlier on this
226
+ * branch is listed here too. And the index holds no body hash: the match is
227
+ * on what a function is described as doing, not on its source.
228
+ */
229
+ export function nudge(entries) {
230
+ const named = entries.slice(0, MAX_ENTRIES);
231
+ const commits = [...new Set(named.map((entry) => entry.indexCommit).filter(Boolean))];
232
+ const at = commits.length === 1
233
+ ? `the index at ${commits[0].slice(0, 7)}`
234
+ : "the index this workspace holds";
235
+ const list = named
236
+ .map((entry) => `${entry.name} (${entry.file})`)
237
+ .join(", ");
238
+ const more = entries.length > named.length ? ` ${entries.length - named.length} more are not listed.` : "";
239
+ return [
240
+ `This task added ${named.length} function${named.length === 1 ? "" : "s"} that ${at} does not hold: ${list}.${more}`,
241
+ `Run the carrick-reuse skill now: one find_similar call, one \`description\` entry per function (a \`name\` entry resolves against the index, and these are not in it).`,
242
+ `Two limits on the answer: the index was taken at ${commits.length === 1 ? commits[0].slice(0, 7) : "this workspace's last scan"} on the default branch, so anything added on this branch is compared against that; and the comparison is on what each function is described as doing, not on its source.`,
243
+ ].join(" ");
244
+ }
245
+ //# sourceMappingURL=reuse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reuse.js","sourceRoot":"","sources":["../../src/hook/reuse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ /** The one channel a Stop hook has that the model reads. */
3
+ export declare function emission(context: string): string;
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ // Stop hook: one reuse nudge per task, or nothing at all (carrick#1330).
3
+ //
4
+ // What it prints, and why that shape: Claude Code delivers a Stop hook's
5
+ // `hookSpecificOutput.additionalContext` to the MODEL and lets the turn
6
+ // continue — non-error feedback the model can act on. The alternatives were
7
+ // both wrong for this: `systemMessage` is shown to the user and never reaches
8
+ // the model (measured, carrick session 2026-09-03 — a Stop hook fired in 21
9
+ // sessions and changed nothing), and `decision: "block"` reaches the model by
10
+ // refusing to let the turn end, which is a block. This nudge is advice.
11
+ //
12
+ // It is silent whenever it has nothing new to say: no session id, no recorded
13
+ // functions, or every recorded function already named by an earlier stop of
14
+ // the same session. A stop after a stop is the common case — a task is
15
+ // several turns — so "already nudged" is the state this has to get right or
16
+ // the line repeats on every turn until the session ends.
17
+ //
18
+ // It exits 0 on every path, like the other two.
19
+ //
20
+ // Reference: `docs/reference/task-skills.md`, "The reuse nudge" — the three
21
+ // parts, the channel measurement behind this shape, and what Codex lacks.
22
+ import { createLogger } from "../log.js";
23
+ import { resolveChannel } from "../channel.js";
24
+ import { drain } from "./reuse.js";
25
+ const log = createLogger("carrick-stop");
26
+ async function readStdin() {
27
+ const chunks = [];
28
+ for await (const chunk of process.stdin)
29
+ chunks.push(Buffer.from(chunk));
30
+ return Buffer.concat(chunks).toString("utf8");
31
+ }
32
+ /** The one channel a Stop hook has that the model reads. */
33
+ export function emission(context) {
34
+ return JSON.stringify({
35
+ hookSpecificOutput: {
36
+ hookEventName: "Stop",
37
+ additionalContext: context,
38
+ },
39
+ });
40
+ }
41
+ async function main() {
42
+ // `off` silences every surface this package has, and this is one of them.
43
+ // The `lsp` channel is NOT excluded: it decides who delivers a verdict about
44
+ // an edited file, and this says nothing about a verdict.
45
+ if (resolveChannel({ hooksInstalled: true }).channel === "off") {
46
+ log("CARRICK_CHANNEL=off; printing nothing");
47
+ return;
48
+ }
49
+ let payload;
50
+ try {
51
+ payload = JSON.parse((await readStdin()) || "{}");
52
+ }
53
+ catch (error) {
54
+ log("unparseable payload", String(error));
55
+ return;
56
+ }
57
+ const session = payload.session_id;
58
+ if (!session) {
59
+ log("no session id in the payload; nothing to look up");
60
+ return;
61
+ }
62
+ // `drain` reads the store, builds the line and marks the set as spoken, in
63
+ // that order. Codex's `UserPromptSubmit` hook calls the same function
64
+ // (carrick#1335); the difference between the two hosts is the event name on
65
+ // the wire and nothing else.
66
+ const spoken = drain(session);
67
+ if (spoken === null) {
68
+ log(`nothing new in ${session}: everything recorded has been named`);
69
+ return;
70
+ }
71
+ process.stdout.write(emission(spoken.line));
72
+ log(`named ${spoken.named} new function(s) to the model, of ${spoken.found} recorded`);
73
+ }
74
+ await main();
75
+ process.exit(0);
76
+ //# sourceMappingURL=stop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stop.js","sourceRoot":"","sources":["../../src/hook/stop.ts"],"names":[],"mappings":";AACA,yEAAyE;AACzE,EAAE;AACF,yEAAyE;AACzE,wEAAwE;AACxE,4EAA4E;AAC5E,8EAA8E;AAC9E,4EAA4E;AAC5E,8EAA8E;AAC9E,wEAAwE;AACxE,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,uEAAuE;AACvE,4EAA4E;AAC5E,yDAAyD;AACzD,EAAE;AACF,gDAAgD;AAChD,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAE1E,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,GAAG,GAAG,YAAY,CAAC,cAAc,CAAC,CAAC;AAQzC,KAAK,UAAU,SAAS;IACtB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACzE,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,kBAAkB,EAAE;YAClB,aAAa,EAAE,MAAM;YACrB,iBAAiB,EAAE,OAAO;SAC3B;KACF,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,0EAA0E;IAC1E,6EAA6E;IAC7E,yDAAyD;IACzD,IAAI,cAAc,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAC/D,GAAG,CAAC,uCAAuC,CAAC,CAAC;QAC7C,OAAO;IACT,CAAC;IAED,IAAI,OAAgB,CAAC;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,SAAS,EAAE,CAAC,IAAI,IAAI,CAAY,CAAC;IAC/D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,qBAAqB,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1C,OAAO;IACT,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC;IACnC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,GAAG,CAAC,kDAAkD,CAAC,CAAC;QACxD,OAAO;IACT,CAAC;IAED,2EAA2E;IAC3E,sEAAsE;IACtE,4EAA4E;IAC5E,6BAA6B;IAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9B,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,GAAG,CAAC,kBAAkB,OAAO,sCAAsC,CAAC,CAAC;QACrE,OAAO;IACT,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5C,GAAG,CAAC,SAAS,MAAM,CAAC,KAAK,qCAAqC,MAAM,CAAC,KAAK,WAAW,CAAC,CAAC;AACzF,CAAC;AAED,MAAM,IAAI,EAAE,CAAC;AACb,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The one channel a `UserPromptSubmit` hook has that the model reads.
4
+ *
5
+ * Codex parses this with `deny_unknown_fields`
6
+ * (`codex-rs/hooks/src/schema.rs`), so the object is exactly these two keys and
7
+ * a wrong event name is a parse failure rather than a silent drop.
8
+ */
9
+ export declare function emission(context: string): string;
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // UserPromptSubmit hook: the reuse nudge, on Codex (carrick#1335).
3
+ //
4
+ // Same nudge, same store, same once-per-set marking as the Stop hook this
5
+ // package installs for Claude Code — `drain` is the shared half and the only
6
+ // difference is which event carries the line.
7
+ //
8
+ // Why this event and not `Stop`. Codex accepts `additionalContext` on
9
+ // `PreToolUse`, `PostToolUse`, `SessionStart`, `UserPromptSubmit` and
10
+ // `SubagentStart`, and warns "this event cannot emit additionalContext" for
11
+ // every other one (`codex-rs/hooks/src/engine/discovery.rs`). A Codex `Stop`
12
+ // hook can reach the model only by refusing to let the turn end, and this nudge
13
+ // is advice, never a block. `UserPromptSubmit` is the one event that is both
14
+ // model-visible and non-blocking there.
15
+ //
16
+ // What that costs: the line arrives with the NEXT prompt rather than at the end
17
+ // of the task that earned it, and a run that ends on that task never sees it.
18
+ // The named-once marking is what makes the late delivery safe — the set is
19
+ // spoken once, whenever the next prompt comes, and never again.
20
+ //
21
+ // It exits 0 on every path, like the other three. A prompt never fails because
22
+ // Carrick had nothing to say.
23
+ //
24
+ // Reference: `docs/reference/task-skills.md`, "The reuse nudge".
25
+ import { createLogger } from "../log.js";
26
+ import { resolveChannel } from "../channel.js";
27
+ import { drain } from "./reuse.js";
28
+ const log = createLogger("carrick-user-prompt");
29
+ async function readStdin() {
30
+ const chunks = [];
31
+ for await (const chunk of process.stdin)
32
+ chunks.push(Buffer.from(chunk));
33
+ return Buffer.concat(chunks).toString("utf8");
34
+ }
35
+ /**
36
+ * The one channel a `UserPromptSubmit` hook has that the model reads.
37
+ *
38
+ * Codex parses this with `deny_unknown_fields`
39
+ * (`codex-rs/hooks/src/schema.rs`), so the object is exactly these two keys and
40
+ * a wrong event name is a parse failure rather than a silent drop.
41
+ */
42
+ export function emission(context) {
43
+ return JSON.stringify({
44
+ hookSpecificOutput: {
45
+ hookEventName: "UserPromptSubmit",
46
+ additionalContext: context,
47
+ },
48
+ });
49
+ }
50
+ async function main() {
51
+ // `off` silences every surface this package has, and this is one of them.
52
+ if (resolveChannel({ hooksInstalled: true }).channel === "off") {
53
+ log("CARRICK_CHANNEL=off; printing nothing");
54
+ return;
55
+ }
56
+ let payload;
57
+ try {
58
+ payload = JSON.parse((await readStdin()) || "{}");
59
+ }
60
+ catch (error) {
61
+ log("unparseable payload", String(error));
62
+ return;
63
+ }
64
+ const session = payload.session_id;
65
+ if (!session) {
66
+ log("no session id in the payload; nothing to look up");
67
+ return;
68
+ }
69
+ const spoken = drain(session);
70
+ if (spoken === null) {
71
+ log(`nothing new in ${session}: everything recorded has been named`);
72
+ return;
73
+ }
74
+ process.stdout.write(emission(spoken.line));
75
+ log(`named ${spoken.named} new function(s) to the model, of ${spoken.found} recorded`);
76
+ }
77
+ await main();
78
+ process.exit(0);
79
+ //# sourceMappingURL=user-prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-prompt.js","sourceRoot":"","sources":["../../src/hook/user-prompt.ts"],"names":[],"mappings":";AACA,mEAAmE;AACnE,EAAE;AACF,0EAA0E;AAC1E,6EAA6E;AAC7E,8CAA8C;AAC9C,EAAE;AACF,sEAAsE;AACtE,sEAAsE;AACtE,4EAA4E;AAC5E,6EAA6E;AAC7E,gFAAgF;AAChF,6EAA6E;AAC7E,wCAAwC;AACxC,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,2EAA2E;AAC3E,gEAAgE;AAChE,EAAE;AACF,+EAA+E;AAC/E,8BAA8B;AAC9B,EAAE;AACF,iEAAiE;AAEjE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,GAAG,GAAG,YAAY,CAAC,qBAAqB,CAAC,CAAC;AAMhD,KAAK,UAAU,SAAS;IACtB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACzE,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,kBAAkB,EAAE;YAClB,aAAa,EAAE,kBAAkB;YACjC,iBAAiB,EAAE,OAAO;SAC3B;KACF,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,0EAA0E;IAC1E,IAAI,cAAc,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAC/D,GAAG,CAAC,uCAAuC,CAAC,CAAC;QAC7C,OAAO;IACT,CAAC;IAED,IAAI,OAAgB,CAAC;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,SAAS,EAAE,CAAC,IAAI,IAAI,CAAY,CAAC;IAC/D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,qBAAqB,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1C,OAAO;IACT,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC;IACnC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,GAAG,CAAC,kDAAkD,CAAC,CAAC;QACxD,OAAO;IACT,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9B,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,GAAG,CAAC,kBAAkB,OAAO,sCAAsC,CAAC,CAAC;QACrE,OAAO;IACT,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5C,GAAG,CAAC,SAAS,MAAM,CAAC,KAAK,qCAAqC,MAAM,CAAC,KAAK,WAAW,CAAC,CAAC;AACzF,CAAC;AAED,MAAM,IAAI,EAAE,CAAC;AACb,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,51 @@
1
+ import { type HookGroup, type InstalledHook } from "./settings.ts";
2
+ /** Codex's project config folder, relative to the workspace. */
3
+ export declare const CODEX_DIR = ".codex";
4
+ /** The file init writes, relative to the workspace. */
5
+ export declare const CODEX_HOOKS_FILE: string;
6
+ /** What this package registers with Codex, and the only entries it removes. */
7
+ export declare function codexHooks(command?: string): Record<string, HookGroup[]>;
8
+ /** What `codexHooks` writes, flattened the way `installedCarrickHooks` reads. */
9
+ export declare function expectedCodexHooks(command?: string): InstalledHook[];
10
+ /**
11
+ * Whether Codex is set up for this workspace.
12
+ *
13
+ * The test is its project config folder, which is the layer Codex reads project
14
+ * hooks and project config from and the folder this file lives in. A workspace
15
+ * with no `.codex/` has nothing of Codex's in it, and `carrick doctor` says
16
+ * nothing about Codex there rather than warning every Claude Code user about a
17
+ * host they do not run.
18
+ */
19
+ export declare function codexInUse(workspace: string): boolean;
20
+ /** Our entries in a `.codex/hooks.json`, or an empty list where there is none. */
21
+ export declare function installedCodexHooks(workspace: string): InstalledHook[];
22
+ /** The file as it is, or null when it is not there. Throws nothing but I/O. */
23
+ export declare function readHooksFile(workspace: string): string | null;
24
+ export type CodexWrite = "written" | "unchanged";
25
+ /**
26
+ * Write our entries into the project's Codex hooks file, merging.
27
+ *
28
+ * Throws on a file that is not JSON, exactly as the `.claude` writer does: a
29
+ * file somebody hand-edited into something unparseable is reported, never
30
+ * overwritten.
31
+ */
32
+ export declare function writeCodexHooks(workspace: string, command: string): CodexWrite;
33
+ /**
34
+ * Whether what is left of a hooks file is nothing but the shape of one.
35
+ *
36
+ * `.codex/hooks.json` differs from a settings file in who it belongs to: init
37
+ * is the reason it exists, so a file holding no hooks and nothing else after
38
+ * our entries come out is ours to delete rather than an empty husk to leave in
39
+ * somebody's repository. A `description`, a key we never write, or anyone
40
+ * else's entry all make it theirs.
41
+ */
42
+ export declare function isEmptyHooksDocument(body: string): boolean;
43
+ export type CodexRemoval = "removed" | "file removed" | null;
44
+ /**
45
+ * Take our entries out again, and the file with them where it held only ours.
46
+ *
47
+ * Null when there was nothing of ours there, so `carrick remove` stays silent
48
+ * about a machine that never installed it. A `.codex` directory emptied by the
49
+ * deletion goes too; one holding anything else of Codex's stays.
50
+ */
51
+ export declare function uninstallCodexHooks(workspace: string): CodexRemoval;