halfcycle 0.5.1 → 0.5.3
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-plugin/plugin.json +1 -1
- package/bin/bin.bundle.mjs +332 -33
- package/dist/cli.js +285 -104
- package/dist/cli.js.map +3 -3
- package/dist/index.js +221 -44
- package/dist/index.js.map +3 -3
- package/dist/install-plan.d.ts.map +1 -1
- package/dist/install.d.ts +2 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/review.d.ts +3 -3
- package/dist/review.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -125,6 +125,9 @@ var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
|
|
|
125
125
|
var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
|
|
126
126
|
var USER_PROMPT_FINDINGS_REL = ".claude/hooks/user-prompt-findings.sh";
|
|
127
127
|
var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
|
|
128
|
+
var CONTEXT_BUDGET_CHECK_REL = ".claude/hooks/context-budget-check.sh";
|
|
129
|
+
var CONTEXT_DRIFT_CHECK_REL = ".claude/hooks/context-drift-check.sh";
|
|
130
|
+
var CONTEXT_DRIFT_MATCHER = "Edit|Write";
|
|
128
131
|
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
129
132
|
var PRE_TOOL_USE_MATCHER = "^(Bash|Read|Grep|Write|Edit|MultiEdit|NotebookEdit|Agent|PowerShell)$";
|
|
130
133
|
function hookCommand(script, verb) {
|
|
@@ -201,6 +204,20 @@ var HALFCYCLE_HOOK_WIRING = [
|
|
|
201
204
|
timeout: 5,
|
|
202
205
|
command: hookCommand(SESSION_END_MARKER_REL),
|
|
203
206
|
script: SESSION_END_MARKER_REL
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
event: "SessionStart",
|
|
210
|
+
matcher: null,
|
|
211
|
+
timeout: 10,
|
|
212
|
+
command: hookCommand(CONTEXT_BUDGET_CHECK_REL),
|
|
213
|
+
script: CONTEXT_BUDGET_CHECK_REL
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
event: "PostToolUse",
|
|
217
|
+
matcher: CONTEXT_DRIFT_MATCHER,
|
|
218
|
+
timeout: 5,
|
|
219
|
+
command: hookCommand(CONTEXT_DRIFT_CHECK_REL),
|
|
220
|
+
script: CONTEXT_DRIFT_CHECK_REL
|
|
204
221
|
}
|
|
205
222
|
];
|
|
206
223
|
var RETIRED_HOOK_COMMANDS = [
|
|
@@ -210,7 +227,9 @@ var RETIRED_HOOK_COMMANDS = [
|
|
|
210
227
|
"bash ./.claude/hooks/guard-runner.sh hook post-tool-use",
|
|
211
228
|
"bash ./.claude/hooks/guard-runner.sh hook stop",
|
|
212
229
|
"bash ./.claude/hooks/guard-runner.sh hook subagent-stop",
|
|
213
|
-
"bash ./.claude/hooks/session-end-marker.sh"
|
|
230
|
+
"bash ./.claude/hooks/session-end-marker.sh",
|
|
231
|
+
"bash ./.claude/hooks/context-budget-check.sh",
|
|
232
|
+
"bash ./.claude/hooks/context-drift-check.sh"
|
|
214
233
|
];
|
|
215
234
|
var HALFCYCLE_HOOK_COMMANDS = /* @__PURE__ */ new Set([
|
|
216
235
|
...HALFCYCLE_HOOK_WIRING.map((entry) => entry.command),
|
|
@@ -402,7 +421,9 @@ var CODEX_HOOK_WIRING = [
|
|
|
402
421
|
codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 60, GUARD_RUNNER_REL, "hook post-tool-use --agent codex"),
|
|
403
422
|
codexHookRow("Stop", null, 60, GUARD_RUNNER_REL, "hook stop --agent codex"),
|
|
404
423
|
codexHookRow("SubagentStop", null, 60, GUARD_RUNNER_REL, "hook subagent-stop --agent codex"),
|
|
405
|
-
codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL)
|
|
424
|
+
codexHookRow("SessionEnd", null, 3, SESSION_END_MARKER_REL),
|
|
425
|
+
codexHookRow("SessionStart", null, 10, CONTEXT_BUDGET_CHECK_REL),
|
|
426
|
+
codexHookRow("PostToolUse", CODEX_WRITE_CAPABLE_TOOLS.join("|"), 5, CONTEXT_DRIFT_CHECK_REL)
|
|
406
427
|
];
|
|
407
428
|
var RETIRED_CODEX_HOOK_COMMANDS = [];
|
|
408
429
|
function codexHookCommandSet(retired) {
|
|
@@ -3139,16 +3160,43 @@ var ownerWaitDeclarationSchema = z23.object({
|
|
|
3139
3160
|
var ownerWaitDeclarationsSchema = z23.object({ declarations: z23.array(ownerWaitDeclarationSchema) }).strict();
|
|
3140
3161
|
var OWNER_WAIT_TOO_MANY = `This project already has ${String(OWNER_WAIT_ADDITIONS_MAX)} things added to what waits for the owner. Ask the owner to take one off on their settings page in the console first; it cannot be done by typing in chat.`;
|
|
3141
3162
|
|
|
3142
|
-
// ../events/dist/
|
|
3163
|
+
// ../events/dist/owner-wait-answer.js
|
|
3143
3164
|
import { z as z24 } from "zod";
|
|
3165
|
+
var ownerWaitShortIdSchema = z24.string().regex(/^[0-9a-f]{8}$/);
|
|
3166
|
+
var OWNER_WAIT_ANSWER_ACTS = ["approve", "decline"];
|
|
3167
|
+
var ownerWaitAnswerActSchema = z24.enum(OWNER_WAIT_ANSWER_ACTS);
|
|
3168
|
+
var ownerWaitChatRequestSchema = z24.discriminatedUnion("act", [
|
|
3169
|
+
z24.object({ act: z24.literal("approve"), wait: ownerWaitShortIdSchema }).strict(),
|
|
3170
|
+
z24.object({ act: z24.literal("decline"), wait: ownerWaitShortIdSchema, note: scopeNoteWireSchema }).strict()
|
|
3171
|
+
]);
|
|
3172
|
+
function bareOutcome(outcome) {
|
|
3173
|
+
return z24.object({ outcome: z24.literal(outcome) }).strict();
|
|
3174
|
+
}
|
|
3175
|
+
var ownerWaitChatResultSchema = z24.discriminatedUnion("outcome", [
|
|
3176
|
+
z24.object({
|
|
3177
|
+
outcome: z24.literal("recorded"),
|
|
3178
|
+
act: ownerWaitAnswerActSchema,
|
|
3179
|
+
question: z24.string().min(1)
|
|
3180
|
+
}).strict(),
|
|
3181
|
+
z24.object({ outcome: z24.literal("refused"), problems: z24.array(systemDescriptionProblemSchema).min(1) }).strict(),
|
|
3182
|
+
bareOutcome("not-owner"),
|
|
3183
|
+
bareOutcome("no-owner"),
|
|
3184
|
+
bareOutcome("not-found"),
|
|
3185
|
+
bareOutcome("ambiguous"),
|
|
3186
|
+
bareOutcome("not-offered"),
|
|
3187
|
+
bareOutcome("unavailable")
|
|
3188
|
+
]);
|
|
3189
|
+
|
|
3190
|
+
// ../events/dist/autonomy.js
|
|
3191
|
+
import { z as z25 } from "zod";
|
|
3144
3192
|
var AUTONOMY_CHANGED_IN = ["console", "chat"];
|
|
3145
|
-
var autonomyChangedInSchema =
|
|
3146
|
-
var autonomySettingsSchema =
|
|
3147
|
-
on:
|
|
3193
|
+
var autonomyChangedInSchema = z25.enum(AUTONOMY_CHANGED_IN);
|
|
3194
|
+
var autonomySettingsSchema = z25.object({
|
|
3195
|
+
on: z25.boolean(),
|
|
3148
3196
|
// whether builds run on their own
|
|
3149
|
-
mayPublish:
|
|
3197
|
+
mayPublish: z25.boolean(),
|
|
3150
3198
|
// whether builds may publish or release without waiting
|
|
3151
|
-
maySpend:
|
|
3199
|
+
maySpend: z25.boolean(),
|
|
3152
3200
|
// whether builds may spend money or sign up without waiting
|
|
3153
3201
|
changedAt: utcIso8601.nullable(),
|
|
3154
3202
|
// when the setting last changed; null if never
|
|
@@ -3158,37 +3206,37 @@ var autonomySettingsSchema = z24.object({
|
|
|
3158
3206
|
message: "says when and where it changed, or neither",
|
|
3159
3207
|
path: ["changedIn"]
|
|
3160
3208
|
});
|
|
3161
|
-
var autonomyChangeRequestSchema =
|
|
3162
|
-
on:
|
|
3209
|
+
var autonomyChangeRequestSchema = z25.object({
|
|
3210
|
+
on: z25.boolean(),
|
|
3163
3211
|
// the switch
|
|
3164
|
-
mayPublish:
|
|
3212
|
+
mayPublish: z25.boolean(),
|
|
3165
3213
|
// the publish box
|
|
3166
|
-
maySpend:
|
|
3214
|
+
maySpend: z25.boolean()
|
|
3167
3215
|
// the spend box
|
|
3168
3216
|
}).strict();
|
|
3169
3217
|
var AUTONOMY_CHAT_ACTS = ["on", "off"];
|
|
3170
|
-
var autonomyChatActSchema =
|
|
3171
|
-
var autonomyChatRequestSchema =
|
|
3218
|
+
var autonomyChatActSchema = z25.enum(AUTONOMY_CHAT_ACTS);
|
|
3219
|
+
var autonomyChatRequestSchema = z25.object({
|
|
3172
3220
|
act: autonomyChatActSchema
|
|
3173
3221
|
// what the owner typed
|
|
3174
3222
|
}).strict();
|
|
3175
|
-
var autonomyChatResultSchema =
|
|
3176
|
-
|
|
3177
|
-
outcome:
|
|
3178
|
-
on:
|
|
3223
|
+
var autonomyChatResultSchema = z25.discriminatedUnion("outcome", [
|
|
3224
|
+
z25.object({
|
|
3225
|
+
outcome: z25.literal("recorded"),
|
|
3226
|
+
on: z25.boolean()
|
|
3179
3227
|
// whether builds now run on their own
|
|
3180
3228
|
}).strict(),
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3229
|
+
z25.object({ outcome: z25.literal("not-owner") }).strict(),
|
|
3230
|
+
z25.object({ outcome: z25.literal("no-owner") }).strict(),
|
|
3231
|
+
z25.object({ outcome: z25.literal("not-offered") }).strict(),
|
|
3232
|
+
z25.object({ outcome: z25.literal("unavailable") }).strict()
|
|
3185
3233
|
]);
|
|
3186
|
-
var autonomyStateSchema =
|
|
3187
|
-
on:
|
|
3234
|
+
var autonomyStateSchema = z25.object({
|
|
3235
|
+
on: z25.boolean(),
|
|
3188
3236
|
// whether builds run on their own
|
|
3189
|
-
mayPublish:
|
|
3237
|
+
mayPublish: z25.boolean(),
|
|
3190
3238
|
// whether builds may publish or release without waiting
|
|
3191
|
-
maySpend:
|
|
3239
|
+
maySpend: z25.boolean()
|
|
3192
3240
|
// whether builds may spend money or sign up without waiting
|
|
3193
3241
|
}).strict();
|
|
3194
3242
|
var autonomyStateFileSchema = autonomyStateSchema.extend({
|
|
@@ -3197,8 +3245,8 @@ var autonomyStateFileSchema = autonomyStateSchema.extend({
|
|
|
3197
3245
|
}).strict();
|
|
3198
3246
|
var AUTONOMY_STATE_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
3199
3247
|
var AUTONOMY_ACTIVITY_KINDS = ["went-ahead", "waiting", "approved", "closed", "waiting-on-owner"];
|
|
3200
|
-
var autonomyActivityKindSchema =
|
|
3201
|
-
var autonomyActivityRowSchema =
|
|
3248
|
+
var autonomyActivityKindSchema = z25.enum(AUTONOMY_ACTIVITY_KINDS);
|
|
3249
|
+
var autonomyActivityRowSchema = z25.object({
|
|
3202
3250
|
kind: autonomyActivityKindSchema,
|
|
3203
3251
|
// what happened
|
|
3204
3252
|
phase: scopePhaseSchema,
|
|
@@ -3211,6 +3259,8 @@ var autonomyActivityRowSchema = z24.object({
|
|
|
3211
3259
|
// whether the agent says it can be undone
|
|
3212
3260
|
how: statementText(STATEMENT_MAX).optional(),
|
|
3213
3261
|
// how to undo it, when it can be
|
|
3262
|
+
wait: ownerWaitShortIdSchema.optional(),
|
|
3263
|
+
// the short id the owner types to answer a waiting decision
|
|
3214
3264
|
at: utcIso8601
|
|
3215
3265
|
// when it happened
|
|
3216
3266
|
}).strict().superRefine((row, ctx) => {
|
|
@@ -3237,6 +3287,9 @@ var autonomyActivityRowSchema = z24.object({
|
|
|
3237
3287
|
});
|
|
3238
3288
|
}
|
|
3239
3289
|
}
|
|
3290
|
+
if (row.kind !== "waiting" && row.wait !== void 0) {
|
|
3291
|
+
ctx.addIssue({ code: "custom", message: "only a waiting decision is answered by typing", path: ["wait"] });
|
|
3292
|
+
}
|
|
3240
3293
|
if (row.kind === "waiting" && (row.undoable !== void 0 || row.how !== void 0)) {
|
|
3241
3294
|
ctx.addIssue({ code: "custom", message: "a waiting decision has not been made yet", path: ["undoable"] });
|
|
3242
3295
|
}
|
|
@@ -3244,11 +3297,11 @@ var autonomyActivityRowSchema = z24.object({
|
|
|
3244
3297
|
ctx.addIssue({ code: "custom", message: "says how to undo it exactly when it can be undone", path: ["how"] });
|
|
3245
3298
|
}
|
|
3246
3299
|
});
|
|
3247
|
-
var autonomyActivitySchema =
|
|
3248
|
-
rows:
|
|
3300
|
+
var autonomyActivitySchema = z25.object({
|
|
3301
|
+
rows: z25.array(autonomyActivityRowSchema)
|
|
3249
3302
|
// newest first
|
|
3250
3303
|
}).strict();
|
|
3251
|
-
var savedIdSchema =
|
|
3304
|
+
var savedIdSchema = z25.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lower-case letters, digits and single hyphens");
|
|
3252
3305
|
var SAVED_REF_PREFIX = "refs/halfcycle/saved/";
|
|
3253
3306
|
function savedRefOf(id) {
|
|
3254
3307
|
return `${SAVED_REF_PREFIX}${id}`;
|
|
@@ -3269,33 +3322,33 @@ function isGitFolderSegment(segment) {
|
|
|
3269
3322
|
return t === ".git" || /^git~\d+$/.test(t);
|
|
3270
3323
|
}
|
|
3271
3324
|
var SAVED_PATH_KEPT_IN = ["commit", "copy"];
|
|
3272
|
-
var savedPathSchema =
|
|
3273
|
-
path:
|
|
3325
|
+
var savedPathSchema = z25.object({
|
|
3326
|
+
path: z25.string().max(4096).refine(isProjectRelativePath, { message: "must be a path inside the project" }),
|
|
3274
3327
|
// the file, relative to the project
|
|
3275
|
-
keptIn:
|
|
3328
|
+
keptIn: z25.enum(SAVED_PATH_KEPT_IN)
|
|
3276
3329
|
// in the save's commit, or copied beside this file
|
|
3277
3330
|
}).strict();
|
|
3278
|
-
var savedCommitSchema =
|
|
3279
|
-
var savedBranchSchema =
|
|
3280
|
-
name:
|
|
3331
|
+
var savedCommitSchema = z25.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash");
|
|
3332
|
+
var savedBranchSchema = z25.object({
|
|
3333
|
+
name: z25.string().min(1).max(255).refine((n) => !hasControlCharacter(n) && !n.startsWith("-") && !n.includes("..") && n !== "HEAD", {
|
|
3281
3334
|
message: "must be a branch name"
|
|
3282
3335
|
}),
|
|
3283
3336
|
// the branch's name
|
|
3284
3337
|
commit: savedCommitSchema
|
|
3285
3338
|
// the commit it pointed at
|
|
3286
3339
|
}).strict();
|
|
3287
|
-
var savedSchema =
|
|
3340
|
+
var savedSchema = z25.object({
|
|
3288
3341
|
id: savedIdSchema,
|
|
3289
3342
|
// what restore is typed with
|
|
3290
3343
|
savedAt: utcIso8601,
|
|
3291
3344
|
// when the copy was made
|
|
3292
|
-
ref:
|
|
3345
|
+
ref: z25.string().min(1),
|
|
3293
3346
|
// where the saved commit is kept
|
|
3294
|
-
what:
|
|
3347
|
+
what: z25.string().min(1).max(2e3),
|
|
3295
3348
|
// what the action would have changed, in words
|
|
3296
|
-
paths:
|
|
3349
|
+
paths: z25.array(savedPathSchema),
|
|
3297
3350
|
// every saved file
|
|
3298
|
-
branches:
|
|
3351
|
+
branches: z25.array(savedBranchSchema)
|
|
3299
3352
|
// every saved line of work
|
|
3300
3353
|
}).strict().refine((s) => s.ref === savedRefOf(s.id), { message: "must be the ref of this save", path: ["ref"] });
|
|
3301
3354
|
function autonomyC8(agent, question, chose) {
|
|
@@ -4211,6 +4264,8 @@ function kinds() {
|
|
|
4211
4264
|
[".claude/hooks/user-prompt-findings.sh", "check-script"],
|
|
4212
4265
|
[".claude/hooks/session-end-marker.sh", "check-script"],
|
|
4213
4266
|
[".claude/hooks/session-start-marker.sh", "check-script"],
|
|
4267
|
+
[".claude/hooks/context-budget-check.sh", "check-script"],
|
|
4268
|
+
[".claude/hooks/context-drift-check.sh", "check-script"],
|
|
4214
4269
|
[SETTINGS_REL, "settings"],
|
|
4215
4270
|
[MCP_REGISTRATION_REL, "server-entry"],
|
|
4216
4271
|
[GITIGNORE_REL, "ignore-list"],
|
|
@@ -6166,13 +6221,133 @@ var MCP_HEADERS_KNOWN_HEADER_HASHES = [
|
|
|
6166
6221
|
function planMcpHeadersHelperRemoval(targetRepo) {
|
|
6167
6222
|
return decideRetiredRemoval(targetRepo, RETIRED_MCP_HEADERS_HELPER_REL, 1, MCP_HEADERS_KNOWN_HEADER_HASHES);
|
|
6168
6223
|
}
|
|
6224
|
+
var GENERATED_CONTEXT_BUDGET_HEADER = "# Context budget check \u2014 generated by";
|
|
6225
|
+
function generateContextBudgetCheck() {
|
|
6226
|
+
return `#!/usr/bin/env bash
|
|
6227
|
+
${GENERATED_CONTEXT_BUDGET_HEADER} the Halfcycle installer.
|
|
6228
|
+
# Runs when a session starts. It warns, and never blocks, when the instructions
|
|
6229
|
+
# every session loads have grown past their budget, so they can be trimmed
|
|
6230
|
+
# before every later session pays for them:
|
|
6231
|
+
# - AGENTS.md at the project root, over 16384 bytes;
|
|
6232
|
+
# - this project's memory index in Claude Code, over 20480 bytes or 150 lines
|
|
6233
|
+
# (Claude Code cuts the index off at about 25 KB or 200 lines when it loads it).
|
|
6234
|
+
|
|
6235
|
+
cat >/dev/null 2>&1 || true
|
|
6236
|
+
|
|
6237
|
+
SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd -P )" || exit 0
|
|
6238
|
+
PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd -P )" || exit 0
|
|
6239
|
+
|
|
6240
|
+
NOTES=""
|
|
6241
|
+
|
|
6242
|
+
AGENTS_FILE="$PROJECT_ROOT/AGENTS.md"
|
|
6243
|
+
if [ -f "$AGENTS_FILE" ]; then
|
|
6244
|
+
BYTES=$(wc -c < "$AGENTS_FILE" | tr -d ' ')
|
|
6245
|
+
if [ "\${BYTES:-0}" -gt 16384 ]; then
|
|
6246
|
+
NOTES="\${NOTES}AGENTS.md is \${BYTES} bytes, over its budget of 16384. "
|
|
6247
|
+
fi
|
|
6248
|
+
fi
|
|
6249
|
+
|
|
6250
|
+
CONFIG_DIR="\${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
6251
|
+
SLUG=$(printf '%s' "\${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}" | sed 's/[^A-Za-z0-9]/-/g')
|
|
6252
|
+
INDEX="$CONFIG_DIR/projects/$SLUG/memory/MEMORY.md"
|
|
6253
|
+
if [ -f "$INDEX" ]; then
|
|
6254
|
+
BYTES=$(wc -c < "$INDEX" | tr -d ' ')
|
|
6255
|
+
LINES=$(wc -l < "$INDEX" | tr -d ' ')
|
|
6256
|
+
if [ "\${BYTES:-0}" -gt 20480 ] || [ "\${LINES:-0}" -gt 150 ]; then
|
|
6257
|
+
NOTES="\${NOTES}The memory index is \${BYTES} bytes and \${LINES} lines, over its budget of 20480 bytes and 150 lines. "
|
|
6258
|
+
fi
|
|
6259
|
+
fi
|
|
6260
|
+
|
|
6261
|
+
if [ -z "$NOTES" ]; then exit 0; fi
|
|
6262
|
+
|
|
6263
|
+
MESSAGE="Context budget: \${NOTES}Run the context-update command to fold and trim it."
|
|
6264
|
+
printf '{"systemMessage":"%s","hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "$MESSAGE" "$MESSAGE"
|
|
6265
|
+
exit 0
|
|
6266
|
+
`;
|
|
6267
|
+
}
|
|
6268
|
+
var GENERATED_CONTEXT_DRIFT_HEADER = "# Context drift check \u2014 generated by";
|
|
6269
|
+
function generateContextDriftCheck() {
|
|
6270
|
+
return `#!/usr/bin/env bash
|
|
6271
|
+
${GENERATED_CONTEXT_DRIFT_HEADER} the Halfcycle installer.
|
|
6272
|
+
# Context drift detector \u2014 runs after each file edit. If a trigger file was modified,
|
|
6273
|
+
# it asks the session to run the context-update command before the session ends.
|
|
6274
|
+
|
|
6275
|
+
set -euo pipefail
|
|
6276
|
+
|
|
6277
|
+
INPUT=$(cat)
|
|
6278
|
+
|
|
6279
|
+
# First line: the event's name, for the reply. Then one line per edited file, made
|
|
6280
|
+
# absolute against the event's cwd: the edit's file_path, or every path a patch names.
|
|
6281
|
+
# With no python3 on the machine it says nothing, rather than fail after every edit.
|
|
6282
|
+
PARSED=$(HOOK_INPUT="$INPUT" python3 - <<'PYEOF'
|
|
6283
|
+
import json, os, re
|
|
6284
|
+
try:
|
|
6285
|
+
data = json.loads(os.environ["HOOK_INPUT"])
|
|
6286
|
+
except Exception:
|
|
6287
|
+
raise SystemExit(0)
|
|
6288
|
+
tool_input = data.get("tool_input") or {}
|
|
6289
|
+
cwd = data.get("cwd") or os.getcwd()
|
|
6290
|
+
if tool_input.get("file_path"):
|
|
6291
|
+
paths = [tool_input["file_path"]]
|
|
6292
|
+
else:
|
|
6293
|
+
headers = r"^\\*\\*\\* (?:Add File|Update File|Delete File|Move to): (.+)$"
|
|
6294
|
+
paths = re.findall(headers, str(tool_input.get("command", "")), re.M)
|
|
6295
|
+
print(data.get("hook_event_name", ""))
|
|
6296
|
+
for p in paths:
|
|
6297
|
+
print(os.path.realpath(os.path.join(cwd, p.strip())))
|
|
6298
|
+
PYEOF
|
|
6299
|
+
) || exit 0
|
|
6300
|
+
|
|
6301
|
+
if [ -z "$PARSED" ]; then exit 0; fi
|
|
6302
|
+
HOOK_EVENT=$(printf '%s\\n' "$PARSED" | head -n 1)
|
|
6303
|
+
|
|
6304
|
+
# Portable PROJECT_ROOT derivation: the script lives two folders below the root
|
|
6305
|
+
SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd -P )"
|
|
6306
|
+
PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd -P )"
|
|
6307
|
+
|
|
6308
|
+
REASONS=""
|
|
6309
|
+
while IFS= read -r FILE_PATH; do
|
|
6310
|
+
[ -n "$FILE_PATH" ] || continue
|
|
6311
|
+
case "$FILE_PATH" in
|
|
6312
|
+
"$PROJECT_ROOT"/*) ;;
|
|
6313
|
+
*) continue ;;
|
|
6314
|
+
esac
|
|
6315
|
+
|
|
6316
|
+
REL_PATH="\${FILE_PATH#$PROJECT_ROOT/}"
|
|
6317
|
+
|
|
6318
|
+
# Skip the instructions files and each agent's own folders (editing them needs no reminder)
|
|
6319
|
+
case "$REL_PATH" in
|
|
6320
|
+
CLAUDE.md|AGENTS.md|README.md|.claude/*|.codex/*|.agents/*|dist/*|node_modules/*|.git/*)
|
|
6321
|
+
continue ;;
|
|
6322
|
+
esac
|
|
6323
|
+
|
|
6324
|
+
REASON=""
|
|
6325
|
+
case "$REL_PATH" in
|
|
6326
|
+
package.json|package-lock.json) REASON="package.json changed \u2014 check Tech Stack / Commands sections" ;;
|
|
6327
|
+
wrangler.jsonc|wrangler.toml) REASON="wrangler config changed \u2014 check Deployment section" ;;
|
|
6328
|
+
astro.config.*|next.config.*|vite.config.*) REASON="build config changed \u2014 check Tech Stack / Architecture" ;;
|
|
6329
|
+
src/pages/api/*|src/routes/api/*|pages/api/*) REASON="API route changed \u2014 check API Endpoints" ;;
|
|
6330
|
+
scripts/*) REASON="scripts/ changed \u2014 check Commands section" ;;
|
|
6331
|
+
tsconfig.json|.eslintrc*|.prettierrc*) REASON="tool config changed \u2014 check Code Conventions" ;;
|
|
6332
|
+
esac
|
|
6333
|
+
if [ -n "$REASON" ]; then REASONS="\${REASONS}\${REL_PATH} was modified: \${REASON}. "; fi
|
|
6334
|
+
done < <(printf '%s\\n' "$PARSED" | tail -n +2)
|
|
6335
|
+
|
|
6336
|
+
if [ -z "$REASONS" ]; then exit 0; fi
|
|
6337
|
+
|
|
6338
|
+
REMINDER="Context drift detected. \${REASONS}Before ending this session, run the context-update command to audit and update AGENTS.md and the memory folder."
|
|
6339
|
+
python3 -c 'import json, sys; print(json.dumps({"hookSpecificOutput": {"hookEventName": sys.argv[1], "additionalContext": sys.argv[2]}}))' "$HOOK_EVENT" "$REMINDER"
|
|
6340
|
+
`;
|
|
6341
|
+
}
|
|
6169
6342
|
var OWNED_GENERATED_HEADERS = {
|
|
6170
6343
|
".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
|
|
6171
6344
|
".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
|
|
6172
6345
|
".claude/hooks/user-prompt-scope.sh": GENERATED_USER_PROMPT_SCOPE_HEADER,
|
|
6173
6346
|
".claude/hooks/user-prompt-findings.sh": GENERATED_USER_PROMPT_FINDINGS_HEADER,
|
|
6174
6347
|
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
6175
|
-
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
|
|
6348
|
+
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER,
|
|
6349
|
+
".claude/hooks/context-budget-check.sh": GENERATED_CONTEXT_BUDGET_HEADER,
|
|
6350
|
+
".claude/hooks/context-drift-check.sh": GENERATED_CONTEXT_DRIFT_HEADER
|
|
6176
6351
|
};
|
|
6177
6352
|
function hookScriptsThisVersionWrites() {
|
|
6178
6353
|
return [
|
|
@@ -6181,7 +6356,9 @@ function hookScriptsThisVersionWrites() {
|
|
|
6181
6356
|
[".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
6182
6357
|
[".claude/hooks/user-prompt-scope.sh", generateUserPromptScopeHook()],
|
|
6183
6358
|
[".claude/hooks/user-prompt-findings.sh", generateUserPromptFindingsHook()],
|
|
6184
|
-
[".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
|
|
6359
|
+
[".claude/hooks/session-start-marker.sh", generateSessionStartMarker()],
|
|
6360
|
+
[".claude/hooks/context-budget-check.sh", generateContextBudgetCheck()],
|
|
6361
|
+
[".claude/hooks/context-drift-check.sh", generateContextDriftCheck()]
|
|
6185
6362
|
];
|
|
6186
6363
|
}
|
|
6187
6364
|
function generateMcpRegistration(existing, mcpOrigin) {
|